diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000000..1bfa7a41ccc9 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,53 @@ +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- + +FROM node:8 + +# Avoid warnings by switching to noninteractive +ENV DEBIAN_FRONTEND=noninteractive + +# The node image comes with a base non-root 'node' user which this Dockerfile +# gives sudo access. However, for Linux, this user's GID/UID must match your local +# user UID/GID to avoid permission issues with bind mounts. Update USER_UID / USER_GID +# if yours is not 1000. See https://aka.ms/vscode-remote/containers/non-root-user. +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +# Configure apt and install packages +RUN apt-get update \ + && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \ + # + # Verify git and needed tools are installed + && apt-get -y install git iproute2 procps \ + # + # Remove outdated yarn from /opt and install via package + # so it can be easily updated via apt-get upgrade yarn + && rm -rf /opt/yarn-* \ + && rm -f /usr/local/bin/yarn \ + && rm -f /usr/local/bin/yarnpkg \ + && apt-get install -y curl apt-transport-https lsb-release \ + && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ + && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ + && apt-get update \ + && apt-get -y install --no-install-recommends yarn \ + # + # Install tslint and typescript globally + && npm install -g tslint typescript \ + # + # [Optional] Update a non-root user to match UID/GID - see https://aka.ms/vscode-remote/containers/non-root-user. + && if [ "$USER_GID" != "1000" ]; then groupmod node --gid $USER_GID; fi \ + && if [ "$USER_UID" != "1000" ]; then usermod --uid $USER_UID node; fi \ + # [Optional] Add add sudo support for non-root user + && apt-get install -y sudo \ + && echo node ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/node \ + && chmod 0440 /etc/sudoers.d/node \ + # + # Clean up + && apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* + +# Switch back to dialog for any ad-hoc use of apt-get +ENV DEBIAN_FRONTEND= diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000000..17c0189dcafd --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,26 @@ +// For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at +// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/typescript-node-8 +{ + "name": "Node.js 8 & TypeScript", + "dockerFile": "Dockerfile", + + // Use 'settings' to set *default* container specific settings.json values on container create. + // You can edit these settings after create using File > Preferences > Settings > Remote. + "settings": { + "terminal.integrated.shell.linux": "/bin/bash" + }, + + // Uncomment the next line if you want to publish any ports. + // "appPort": [], + + // Uncomment the next line to run commands after the container is created. + // "postCreateCommand": "yarn install", + + // Uncomment the next line to use a non-root user. On Linux, this will prevent + // new files getting created as root, but you may need to update the USER_UID + // and USER_GID in .devcontainer/Dockerfile to match your user if not 1000. + // "runArgs": [ "-u", "node" ], + + // Add the IDs of extensions you want installed when the container is created in the array below. + "extensions": ["ms-vscode.vscode-typescript-tslint-plugin"] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..4f92a467d0ef --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Tab indentation +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +# The indent size used in the `package.json` file cannot be changed +# https://github.com/npm/npm/pull/3180#issuecomment-16336516 +[{.travis.yml,npm-shrinkwrap.json,package.json}] +indent_style = space +indent_size = 4 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000000..fabe7499fa8c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,1503 @@ +# Disable ESLint on all existing js, jsx, ts and tsx files in build/ and src/, +# except for src/client/pythonEnvironments and src/test/pythonEnvironments. + +build/constants.js +build/util.js + +build/ci/postInstall.js +build/ci/scripts/runFunctionalTests.js +build/ci/performance/checkPerformanceResults.js +build/ci/performance/createNewPerformanceBenchmark.js +build/ci/performance/savePerformanceResults.js + +build/webpack/webpack.datascience-ui.config.js +build/webpack/webpack.extension.config.js +build/webpack/webpack.datascience-ui-notebooks.config.js +build/webpack/plugins/less-plugin-base64.js +build/webpack/webpack.datascience-ui.config.builder.js +build/webpack/pdfkit.js +build/webpack/webpack.extension.dependencies.config.js +build/webpack/common.js +build/webpack/webpack.datascience-ui-viewers.config.js +build/webpack/webpack.datascience-ui-renderers.config.js +build/webpack/loaders/fixNodeFetch.js +build/webpack/loaders/remarkLoader.js +build/webpack/loaders/jsonloader.js +build/webpack/loaders/externalizeDependencies.js + +build/tslint-rules/messagesMustBeLocalizedRule.js +build/tslint-rules/baseRuleWalker.js + +build/debug/replaceWithWebBrowserPanel.js + +src/test/mocks/mementos.ts +src/test/mocks/process.ts +src/test/mocks/moduleInstaller.ts +src/test/mocks/proc.ts +src/test/mocks/autoSelector.ts +src/test/mocks/vsc/extHostedTypes.ts +src/test/mocks/vsc/uuid.ts +src/test/mocks/vsc/strings.ts +src/test/mocks/vsc/charCode.ts +src/test/mocks/vsc/htmlContent.ts +src/test/mocks/vsc/selection.ts +src/test/mocks/vsc/position.ts +src/test/mocks/vsc/uri.ts +src/test/mocks/vsc/telemetryReporter.ts +src/test/mocks/vsc/range.ts +src/test/mocks/vsc/index.ts +src/test/mocks/vsc/arrays.ts + +src/test/smoke/common.ts +src/test/smoke/datascience.smoke.test.ts +src/test/smoke/runInTerminal.smoke.test.ts +src/test/smoke/languageServer.smoke.test.ts + +src/test/analysisEngineTest.ts +src/test/ciConstants.ts +src/test/common.ts +src/test/constants.ts +src/test/core.ts +src/test/debuggerTest.ts +src/test/extension-version.functional.test.ts +src/test/fixtures.ts +src/test/index.ts +src/test/initialize.ts +src/test/mockClasses.ts +src/test/multiRootTest.ts +src/test/performanceTest.ts +src/test/proc.ts +src/test/serviceRegistry.ts +src/test/smokeTest.ts +src/test/standardTest.ts +src/test/startupTelemetry.unit.test.ts +src/test/sourceMapSupport.test.ts +src/test/sourceMapSupport.unit.test.ts +src/test/testBootstrap.ts +src/test/testLogger.ts +src/test/testRunner.ts +src/test/textUtils.ts +src/test/unittests.ts +src/test/vscode-mock.ts + +src/test/interpreters/mocks.ts +src/test/interpreters/interpreterVersion.unit.test.ts +src/test/interpreters/virtualEnvs/index.unit.test.ts +src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts +src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts +src/test/interpreters/autoSelection/proxy.unit.test.ts +src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts +src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts +src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts +src/test/interpreters/autoSelection/index.unit.test.ts +src/test/interpreters/autoSelection/rules/settings.unit.test.ts +src/test/interpreters/autoSelection/rules/cached.unit.test.ts +src/test/interpreters/autoSelection/rules/winRegistry.unit.test.ts +src/test/interpreters/autoSelection/rules/base.unit.test.ts +src/test/interpreters/autoSelection/rules/currentPath.unit.test.ts +src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts +src/test/interpreters/autoSelection/rules/system.unit.test.ts +src/test/interpreters/virtualEnvManager.unit.test.ts +src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts +src/test/interpreters/interpreterService.unit.test.ts +src/test/interpreters/activation/service.unit.test.ts +src/test/interpreters/activation/wrapperEnvironmentActivationService.unit.test.ts +src/test/interpreters/activation/preWarmVariables.unit.test.ts +src/test/interpreters/activation/terminalEnvironmentActivationService.unit.test.ts +src/test/interpreters/helpers.unit.test.ts +src/test/interpreters/serviceRegistry.unit.test.ts +src/test/interpreters/currentPathService.unit.test.ts +src/test/interpreters/display/progressDisplay.unit.test.ts +src/test/interpreters/display/interpreterSelectionTip.unit.test.ts +src/test/interpreters/display.unit.test.ts + +src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts +src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts +src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts + +src/test/install/channelManager.channels.test.ts +src/test/install/channelManager.messages.test.ts + +src/test/terminals/serviceRegistry.unit.test.ts +src/test/terminals/activation.unit.test.ts +src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts +src/test/terminals/codeExecution/helper.test.ts +src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts + +src/test/markdown/restTextConverter.test.ts + +src/test/languageServers/jedi/autocomplete/base.test.ts +src/test/languageServers/jedi/autocomplete/pep526.test.ts +src/test/languageServers/jedi/autocomplete/pep484.test.ts +src/test/languageServers/jedi/signature/signature.jedi.test.ts +src/test/languageServers/jedi/completionSource.unit.test.ts +src/test/languageServers/jedi/symbolProvider.unit.test.ts +src/test/languageServers/jedi/pythonSignatureProvider.unit.test.ts +src/test/languageServers/jedi/definitions/parallel.jedi.test.ts +src/test/languageServers/jedi/definitions/navigation.test.ts +src/test/languageServers/jedi/definitions/hover.jedi.test.ts + +src/test/providers/foldingProvider.test.ts +src/test/providers/importSortProvider.unit.test.ts +src/test/providers/terminal.unit.test.ts +src/test/providers/repl.unit.test.ts +src/test/providers/codeActionProvider/main.unit.test.ts +src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts +src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts +src/test/providers/shebangCodeLenseProvider.unit.test.ts +src/test/providers/serviceRegistry.unit.test.ts + +src/test/activation/aaTesting.unit.test.ts +src/test/activation/activationService.unit.test.ts +src/test/activation/languageServer/downloadChannelRules.unit.test.ts +src/test/activation/languageServer/platformData.unit.test.ts +src/test/activation/languageServer/languageServer.unit.test.ts +src/test/activation/languageServer/languageServerFolderService.unit.test.ts +src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts +src/test/activation/languageServer/languageServerPackageService.test.ts +src/test/activation/languageServer/manager.unit.test.ts +src/test/activation/languageServer/analysisOptions.unit.test.ts +src/test/activation/languageServer/languageServerCompatibilityService.unit.test.ts +src/test/activation/languageServer/activator.unit.test.ts +src/test/activation/languageServer/downloader.unit.test.ts +src/test/activation/languageServer/languageServerExtension.unit.test.ts +src/test/activation/languageServer/languageClientFactory.unit.test.ts +src/test/activation/languageServer/outputChannel.unit.test.ts +src/test/activation/languageServer/languageServerPackageService.unit.test.ts +src/test/activation/activeResource.unit.test.ts +src/test/activation/serviceRegistry.unit.test.ts +src/test/activation/node/languageServerFolderService.unit.test.ts +src/test/activation/node/languageServerChangeHandler.unit.test.ts +src/test/activation/node/activator.unit.test.ts +src/test/activation/extensionSurvey.unit.test.ts +src/test/activation/activationManager.unit.test.ts + +src/test/utils/interpreters.ts +src/test/utils/fs.ts + +src/test/language/braceCounter.unit.test.ts +src/test/language/textRangeCollection.unit.test.ts +src/test/language/characterStream.unit.test.ts +src/test/language/languageConfiguration.unit.test.ts +src/test/language/textIterator.unit.test.ts +src/test/language/textBuilder.unit.test.ts +src/test/language/textRange.unit.test.ts +src/test/language/tokenizer.unit.test.ts + +src/test/api.functional.test.ts + +src/test/testing/argsService.test.ts +src/test/testing/mocks.ts +src/test/testing/debugger.test.ts +src/test/testing/serviceRegistry.ts +src/test/testing/unittest/unittest.test.ts +src/test/testing/unittest/unittest.discovery.unit.test.ts +src/test/testing/unittest/unittest.diagnosticService.unit.test.ts +src/test/testing/unittest/unittest.discovery.test.ts +src/test/testing/unittest/unittest.run.test.ts +src/test/testing/unittest/unittest.argsService.unit.test.ts +src/test/testing/unittest/unittest.unit.test.ts +src/test/testing/codeLenses/testFiles.unit.test.ts +src/test/testing/nosetest/nosetest.test.ts +src/test/testing/nosetest/nosetest.discovery.unit.test.ts +src/test/testing/nosetest/nosetest.disovery.test.ts +src/test/testing/nosetest/nosetest.argsService.unit.test.ts +src/test/testing/nosetest/nosetest.run.test.ts +src/test/testing/pytest/pytest.testMessageService.test.ts +src/test/testing/pytest/pytest_unittest_parser_data.ts +src/test/testing/pytest/pytest.discovery.test.ts +src/test/testing/pytest/pytest.test.ts +src/test/testing/pytest/pytest_run_tests_data.ts +src/test/testing/pytest/pytest.argsService.unit.test.ts +src/test/testing/pytest/pytest.run.test.ts +src/test/testing/pytest/services/discoveryService.unit.test.ts +src/test/testing/configurationFactory.unit.test.ts +src/test/testing/rediscover.test.ts +src/test/testing/helper.ts +src/test/testing/navigation/fileNavigator.unit.test.ts +src/test/testing/navigation/functionNavigator.unit.test.ts +src/test/testing/navigation/suiteNavigator.unit.test.ts +src/test/testing/navigation/serviceRegistry.unit.test.ts +src/test/testing/navigation/commandHandlers.unit.test.ts +src/test/testing/navigation/helper.unit.test.ts +src/test/testing/navigation/symbolNavigator.unit.test.ts +src/test/testing/configuration.unit.test.ts +src/test/testing/explorer/testTreeViewItem.unit.test.ts +src/test/testing/explorer/treeView.unit.test.ts +src/test/testing/explorer/testExplorerCommandHandler.unit.test.ts +src/test/testing/explorer/failedTestHandler.unit.test.ts +src/test/testing/explorer/testTreeViewProvider.unit.test.ts +src/test/testing/explorer/explorerTestData.ts +src/test/testing/stoppingDiscoverAndTest.test.ts +src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts +src/test/testing/common/argsHelper.unit.test.ts +src/test/testing/common/trackEnablement.unit.test.ts +src/test/testing/common/debugLauncher.unit.test.ts +src/test/testing/common/managers/baseTestManager.unit.test.ts +src/test/testing/common/managers/testConfigurationManager.unit.test.ts +src/test/testing/common/xUnitParser.unit.test.ts +src/test/testing/common/testUtils.unit.test.ts +src/test/testing/common/testVisitors/resultResetVisitor.unit.test.ts +src/test/testing/common/services/discoveredTestParser.unit.test.ts +src/test/testing/common/services/storageService.unit.test.ts +src/test/testing/common/services/testStatusService.unit.test.ts +src/test/testing/common/services/testResultsService.unit.test.ts +src/test/testing/common/services/discovery.unit.test.ts +src/test/testing/common/services/configSettingService.unit.test.ts +src/test/testing/common/services/contextService.unit.test.ts +src/test/testing/main.unit.test.ts +src/test/testing/results.ts +src/test/testing/display/picker.functional.test.ts +src/test/testing/display/main.unit.test.ts +src/test/testing/display/picker.unit.test.ts + +src/test/common/exitCIAfterTestReporter.ts +src/test/common/crypto.unit.test.ts +src/test/common/configuration/service.test.ts +src/test/common/configuration/service.unit.test.ts +src/test/common/net/fileDownloader.unit.test.ts +src/test/common/net/httpClient.unit.test.ts +src/test/common/moduleInstaller.test.ts +src/test/common/terminals/activator/index.unit.test.ts +src/test/common/terminals/activator/base.unit.test.ts +src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts +src/test/common/terminals/activation.conda.unit.test.ts +src/test/common/terminals/shellDetector.unit.test.ts +src/test/common/terminals/activation.bash.unit.test.ts +src/test/common/terminals/commandPrompt.unit.test.ts +src/test/common/terminals/service.unit.test.ts +src/test/common/terminals/synchronousTerminalService.unit.test.ts +src/test/common/terminals/serviceRegistry.unit.test.ts +src/test/common/terminals/helper.unit.test.ts +src/test/common/terminals/activation.commandPrompt.unit.test.ts +src/test/common/terminals/factory.unit.test.ts +src/test/common/terminals/pyenvActivationProvider.unit.test.ts +src/test/common/terminals/activation.unit.test.ts +src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts +src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts +src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts +src/test/common/misc.test.ts +src/test/common/socketStream.test.ts +src/test/common/configSettings.test.ts +src/test/common/experiments/service.unit.test.ts +src/test/common/experiments/manager.unit.test.ts +src/test/common/experiments/telemetry.unit.test.ts +src/test/common/platform/filesystem.unit.test.ts +src/test/common/platform/pathUtils.functional.test.ts +src/test/common/platform/errors.unit.test.ts +src/test/common/platform/platformService.test.ts +src/test/common/platform/utils.ts +src/test/common/platform/fs-temp.unit.test.ts +src/test/common/platform/fs-temp.functional.test.ts +src/test/common/platform/serviceRegistry.unit.test.ts +src/test/common/platform/filesystem.functional.test.ts +src/test/common/platform/fs-paths.unit.test.ts +src/test/common/platform/fs-paths.functional.test.ts +src/test/common/platform/filesystem.test.ts +src/test/common/utils/async.unit.test.ts +src/test/common/utils/text.unit.test.ts +src/test/common/utils/regexp.unit.test.ts +src/test/common/utils/cacheUtils.unit.test.ts +src/test/common/utils/decorators.unit.test.ts +src/test/common/utils/localize.functional.test.ts +src/test/common/utils/version.unit.test.ts +src/test/common/utils/workerPool.functional.test.ts +src/test/common/configSettings/configSettings.pythonPath.unit.test.ts +src/test/common/configSettings/configSettings.unit.test.ts +src/test/common/featureDeprecationManager.unit.test.ts +src/test/common/dotnet/compatibilityService.unit.test.ts +src/test/common/dotnet/serviceRegistry.unit.test.ts +src/test/common/dotnet/services/linuxCompatibilityService.unit.test.ts +src/test/common/dotnet/services/winCompatibilityService.unit.test.ts +src/test/common/dotnet/services/unknownOsCompatibilityService.unit.test.ts +src/test/common/dotnet/services/macCompatibilityService.unit.test.ts +src/test/common/serviceRegistry.unit.test.ts +src/test/common/extensions.unit.test.ts +src/test/common/variables/envVarsService.functional.test.ts +src/test/common/variables/envVarsService.test.ts +src/test/common/variables/envVarsService.unit.test.ts +src/test/common/variables/serviceRegistry.unit.test.ts +src/test/common/variables/environmentVariablesProvider.unit.test.ts +src/test/common/variables/envVarsProvider.multiroot.test.ts +src/test/common/nuget/nugetService.unit.test.ts +src/test/common/nuget/azureBobStoreRepository.functional.test.ts +src/test/common/nuget/nugetRepository.unit.test.ts +src/test/common/nuget/azureBobStoreRepository.unit.test.ts +src/test/common/helpers.test.ts +src/test/common/application/commands/reloadCommand.unit.test.ts +src/test/common/installer/channelManager.unit.test.ts +src/test/common/installer/condaInstaller.unit.test.ts +src/test/common/installer/installer.unit.test.ts +src/test/common/installer/pipInstaller.unit.test.ts +src/test/common/installer/installer.invalidPath.unit.test.ts +src/test/common/installer/moduleInstaller.unit.test.ts +src/test/common/installer/pipEnvInstaller.unit.test.ts +src/test/common/installer/productPath.unit.test.ts +src/test/common/installer/serviceRegistry.unit.test.ts +src/test/common/installer/poetryInstaller.unit.test.ts +src/test/common/installer/extensionBuildInstaller.unit.test.ts +src/test/common/socketCallbackHandler.test.ts +src/test/common/installer.test.ts +src/test/common/process/decoder.test.ts +src/test/common/process/pythonDaemonPool.unit.test.ts +src/test/common/process/processFactory.unit.test.ts +src/test/common/process/pythonToolService.unit.test.ts +src/test/common/process/proc.observable.test.ts +src/test/common/process/currentProcess.test.ts +src/test/common/process/serviceRegistry.unit.test.ts +src/test/common/process/pythonProc.simple.multiroot.test.ts +src/test/common/process/execFactory.test.ts +src/test/common/process/pythonEnvironment.unit.test.ts +src/test/common/process/logger.unit.test.ts +src/test/common/process/pythonDaemonPool.functional.test.ts +src/test/common/process/proc.exec.test.ts +src/test/common/process/pythonDaemon.functional.test.ts +src/test/common/process/pythonProcess.unit.test.ts +src/test/common/process/pythonExecutionFactory.unit.test.ts +src/test/common/process/proc.unit.test.ts +src/test/common/asyncDump.ts +src/test/common/interpreterPathService.unit.test.ts +src/test/common/insidersBuild/downloadChannelRules.unit.test.ts +src/test/common/insidersBuild/insidersExtensionPrompt.unit.test.ts +src/test/common/insidersBuild/downloadChannelService.unit.test.ts +src/test/common/insidersBuild/insidersExtensionService.unit.test.ts + +src/test/pythonFiles/formatting/dummy.ts + +src/test/format/extension.dispatch.test.ts +src/test/format/extension.format.ds.test.ts +src/test/format/extension.onTypeFormat.test.ts +src/test/format/extension.lineFormatter.test.ts +src/test/format/extension.sort.test.ts +src/test/format/extension.onEnterFormat.test.ts +src/test/format/extension.format.test.ts +src/test/format/format.helper.test.ts +src/test/format/formatter.unit.test.ts + +src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts +src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts +src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts +src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts +src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts +src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts +src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts +src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts +src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts +src/test/debugger/extension/configuration/resolvers/base.unit.test.ts +src/test/debugger/extension/configuration/resolvers/common.ts +src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts +src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts +src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts +src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts +src/test/debugger/extension/banner.unit.test.ts +src/test/debugger/extension/adapter/adapter.test.ts +src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts +src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts +src/test/debugger/extension/adapter/factory.unit.test.ts +src/test/debugger/extension/adapter/activator.unit.test.ts +src/test/debugger/extension/adapter/logging.unit.test.ts +src/test/debugger/extension/serviceRegistry.unit.test.ts +src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts +src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts +src/test/debugger/extension/attachQuickPick/provider.unit.test.ts +src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts +src/test/debugger/extension/attachQuickPick/factory.unit.test.ts +src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts +src/test/debugger/utils.ts +src/test/debugger/common/constants.ts +src/test/debugger/common/protocolparser.test.ts +src/test/debugger/envVars.test.ts + +src/test/startPage/startPage.unit.test.ts +src/test/startPage/startPage.functional.test.tsx + +src/test/telemetry/index.unit.test.ts +src/test/telemetry/importTracker.unit.test.ts +src/test/telemetry/envFileTelemetry.unit.test.ts +src/test/telemetry/extensionInstallTelemetry.unit.test.ts + +src/test/linters/pylint.unit.test.ts +src/test/linters/mypy.unit.test.ts +src/test/linters/lint.provider.test.ts +src/test/linters/lint.unit.test.ts +src/test/linters/linter.availability.unit.test.ts +src/test/linters/common.ts +src/test/linters/lintengine.test.ts +src/test/linters/lint.multilinter.test.ts +src/test/linters/lint.test.ts +src/test/linters/linterinfo.unit.test.ts +src/test/linters/serviceRegistry.unit.test.ts +src/test/linters/pylint.test.ts +src/test/linters/lint.manager.unit.test.ts +src/test/linters/lint.args.test.ts +src/test/linters/linterCommands.unit.test.ts +src/test/linters/lint.functional.test.ts +src/test/linters/linterManager.unit.test.ts +src/test/linters/lint.multiroot.test.ts + +src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts +src/test/application/diagnostics/checks/upgradeCodeRunner.unit.test.ts +src/test/application/diagnostics/checks/pythonPathDeprecated.unit.test.ts +src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts +src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts +src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts +src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts +src/test/application/diagnostics/checks/envPathVariable.unit.test.ts +src/test/application/diagnostics/checks/lsNotSupported.unit.test.ts +src/test/application/diagnostics/applicationDiagnostics.unit.test.ts +src/test/application/diagnostics/promptHandler.unit.test.ts +src/test/application/diagnostics/sourceMapSupportService.unit.test.ts +src/test/application/diagnostics/serviceRegistry.unit.test.ts +src/test/application/diagnostics/filter.unit.test.ts +src/test/application/diagnostics/commands/ignore.unit.test.ts +src/test/application/diagnostics/commands/launchBrowser.unit.test.ts +src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts +src/test/application/diagnostics/commands/factory.unit.test.ts +src/test/application/misc/joinMailingListPrompt.unit.test.ts + +src/test/performance/load.perf.test.ts + +src/test/datascience/mockLanguageServerCache.ts +src/test/datascience/debugLocationTracker.unit.test.ts +src/test/datascience/mockLiveShare.ts +src/test/datascience/liveshare.functional.test.tsx +src/test/datascience/mountedWebViewFactory.ts +src/test/datascience/data-viewing/dataViewerPDependencyService.unit.test.ts +src/test/datascience/data-viewing/dataViewer.unit.test.ts +src/test/datascience/mockPythonService.ts +src/test/datascience/testHelpersCore.ts +src/test/datascience/shiftEnterBanner.unit.test.ts +src/test/datascience/executionServiceMock.ts +src/test/datascience/mockJupyterManager.ts +src/test/datascience/mockCommandManager.ts +src/test/datascience/mockCustomEditorService.ts +src/test/datascience/mockInputBox.ts +src/test/datascience/reactHelpers.ts +src/test/datascience/helpers.ts +src/test/datascience/jupyterUriProviderRegistration.unit.test.ts +src/test/datascience/mockDocumentManager.ts +src/test/datascience/errorHandler.unit.test.ts +src/test/datascience/cellMatcher.unit.test.ts +src/test/datascience/crossProcessLock.unit.test.ts +src/test/datascience/uiTests/helpers.ts +src/test/datascience/uiTests/webBrowserPanel.ts +src/test/datascience/uiTests/notebookUi.ts +src/test/datascience/uiTests/webBrowserPanelProvider.ts +src/test/datascience/uiTests/recorder.ts +src/test/datascience/uiTests/notebookHelpers.ts +src/test/datascience/uiTests/ipywidget.ui.functional.test.ts +src/test/datascience/mockWorkspaceConfiguration.ts +src/test/datascience/mockTextEditor.ts +src/test/datascience/mockLanguageServerAnalysisOptions.ts +src/test/datascience/mockLanguageServerProxy.ts +src/test/datascience/interactiveWindowCommandListener.unit.test.ts +src/test/datascience/trustedNotebooks.functional.test.tsx +src/test/datascience/mockPythonSettings.ts +src/test/datascience/progress/progressReporter.unit.test.ts +src/test/datascience/progress/decorators.unit.test.ts +src/test/datascience/kernel-launcher/kernelDaemonPool.unit.test.ts +src/test/datascience/kernel-launcher/kernelDaemonPoolPreWarmer.unit.test.ts +src/test/datascience/kernel-launcher/kernelLauncherDaemon.unit.test.ts +src/test/datascience/ipywidgets/ipyWidgetScriptSourceProvider.unit.test.ts +src/test/datascience/ipywidgets/cdnWidgetScriptSourceProvider.unit.test.ts +src/test/datascience/ipywidgets/localWidgetScriptSourceProvider.unit.test.ts +src/test/datascience/ipywidgets/incompatibleWidgetHandler.unit.test.ts +src/test/datascience/mockKernelFinder.ts +src/test/datascience/datascienceSurveyBanner.unit.test.ts +src/test/datascience/intellisense.functional.test.tsx +src/test/datascience/nativeEditor.toolbar.functional.test.tsx +src/test/datascience/mockDocument.ts +src/test/datascience/raw-kernel/rawKernelTestHelpers.ts +src/test/datascience/raw-kernel/rawKernel.functional.test.ts +src/test/datascience/color.test.ts +src/test/datascience/nativeEditorViewTracker.unit.test.ts +src/test/datascience/mockCode2ProtocolConverter.ts +src/test/datascience/mockFileSystem.ts +src/test/datascience/interactive-common/trustService.unit.test.ts +src/test/datascience/interactive-common/notebookProvider.unit.test.ts +src/test/datascience/interactive-common/notebookServerProvider.unit.test.ts +src/test/datascience/interactive-common/trustCommandHandler.unit.test.ts +src/test/datascience/mockStatusProvider.ts +src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/webpack.config.js +src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.eslintrc.js +src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/typings/python.d.ts +src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/extension.ts +src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/serverPicker.ts +src/test/datascience/common.unit.test.ts +src/test/datascience/testexecutionLogger.ts +src/test/datascience/interactiveWindow.functional.test.tsx +src/test/datascience/mockQuickPick.ts +src/test/datascience/dsTestSetup.ts +src/test/datascience/mockLanguageServer.ts +src/test/datascience/debugger.functional.test.tsx +src/test/datascience/testInteractiveWindowProvider.ts +src/test/datascience/dataScienceIocContainer.ts +src/test/datascience/dataviewer.functional.test.tsx +src/test/datascience/jupyterUtils.unit.test.ts +src/test/datascience/preWarmVariables.unit.test.ts +src/test/datascience/mockJupyterNotebook.ts +src/test/datascience/remoteTestHelpers.ts +src/test/datascience/mockWorkspaceFolder.ts +src/test/datascience/variableexplorer.functional.test.tsx +src/test/datascience/mockJupyterSession.ts +src/test/datascience/jupyterUriProviderRegistration.functional.test.ts +src/test/datascience/mockJupyterRequest.ts +src/test/datascience/inputHistory.unit.test.ts +src/test/datascience/jupyterHelpers.ts +src/test/datascience/mockJupyterServer.ts +src/test/datascience/mockJupyterManagerFactory.ts +src/test/datascience/mainState.unit.test.ts +src/test/datascience/mockDebugService.ts +src/test/datascience/nativeEditorTestHelpers.tsx +src/test/datascience/datascience.unit.test.ts +src/test/datascience/kernelLauncher.functional.test.ts +src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts +src/test/datascience/interactive-ipynb/nativeEditorProvider.functional.test.ts +src/test/datascience/kernelFinder.unit.test.ts +src/test/datascience/plotViewer.functional.test.tsx +src/test/datascience/mockExtensions.ts +src/test/datascience/mockProtocol2CodeConverter.ts +src/test/datascience/editor-integration/helpers.ts +src/test/datascience/editor-integration/cellhashprovider.unit.test.ts +src/test/datascience/editor-integration/gotocell.functional.test.ts +src/test/datascience/editor-integration/codelensprovider.unit.test.ts +src/test/datascience/editor-integration/codewatcher.unit.test.ts +src/test/datascience/jupyterPasswordConnect.unit.test.ts +src/test/datascience/commands/serverSelector.unit.test.ts +src/test/datascience/commands/commandRegistry.unit.test.ts +src/test/datascience/commands/notebookCommands.functional.test.ts +src/test/datascience/testHelpers.tsx +src/test/datascience/notebook.functional.test.ts +src/test/datascience/mockLanguageClient.ts +src/test/datascience/errorHandler.functional.test.tsx +src/test/datascience/notebook/notebookStorage.unit.test.ts +src/test/datascience/notebook/notebookTrust.ds.test.ts +src/test/datascience/notebook/rendererExtensionDownloader.unit.test.ts +src/test/datascience/notebook/survey.unit.test.ts +src/test/datascience/notebook/interrupRestart.ds.test.ts +src/test/datascience/notebook/contentProvider.ds.test.ts +src/test/datascience/notebook/helper.ts +src/test/datascience/notebook/contentProvider.unit.test.ts +src/test/datascience/notebook/edit.ds.test.ts +src/test/datascience/notebook/rendererExension.unit.test.ts +src/test/datascience/notebook/saving.ds.test.ts +src/test/datascience/notebook/notebookEditorProvider.ds.test.ts +src/test/datascience/notebook/helpers.unit.test.ts +src/test/datascience/notebook/executionService.ds.test.ts +src/test/datascience/notebook/cellOutput.ds.test.ts +src/test/datascience/interactiveWindowTestHelpers.tsx +src/test/datascience/export/exportUtil.test.ts +src/test/datascience/export/exportToHTML.test.ts +src/test/datascience/export/exportToPython.test.ts +src/test/datascience/export/exportFileOpener.unit.test.ts +src/test/datascience/export/exportManager.test.ts +src/test/datascience/intellisense.unit.test.ts +src/test/datascience/nativeEditor.functional.test.tsx +src/test/datascience/markdownManipulation.unit.test.ts +src/test/datascience/interactivePanel.functional.test.tsx +src/test/datascience/variableTestHelpers.ts +src/test/datascience/activation.unit.test.ts +src/test/datascience/testPersistentStateFactory.ts +src/test/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.unit.test.ts +src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts +src/test/datascience/jupyter/interpreter/jupyterInterpreterStateStore.unit.test.ts +src/test/datascience/jupyter/interpreter/jupyterInterpreterService.unit.test.ts +src/test/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand.unit.test.ts +src/test/datascience/jupyter/interpreter/jupyterInterpreterSelector.unit.test.ts +src/test/datascience/jupyter/serverSelector.unit.test.ts +src/test/datascience/jupyter/kernels/kernelSelector.unit.test.ts +src/test/datascience/jupyter/kernels/kernelSelections.unit.test.ts +src/test/datascience/jupyter/kernels/kernelDependencyService.unit.test.ts +src/test/datascience/jupyter/kernels/kernelSwitcher.unit.test.ts +src/test/datascience/jupyter/kernels/kernelService.unit.test.ts +src/test/datascience/jupyter/jupyterCellOutputMimeTypeTracker.unit.test.ts +src/test/datascience/jupyter/jupyterSession.unit.test.ts +src/test/datascience/jupyter/jupyterConnection.unit.test.ts +src/test/datascience/jupyter/serverCache.unit.test.ts +src/test/datascience/mockWorkspaceConfig.ts +src/test/datascience/mountedWebView.ts +src/test/datascience/execution.unit.test.ts +src/test/datascience/mockProcessService.ts +src/test/datascience/testNativeEditorProvider.ts +src/test/datascience/cellFactory.unit.test.ts + +src/test/refactor/extension.refactor.extract.method.test.ts +src/test/refactor/extension.refactor.extract.var.test.ts +src/test/refactor/rename.test.ts + +src/test/workspaceSymbols/provider.unit.test.ts +src/test/workspaceSymbols/common.ts +src/test/workspaceSymbols/main.unit.test.ts +src/test/workspaceSymbols/generator.unit.test.ts + +src/ipywidgets/types/require.js.d.ts +src/ipywidgets/types/index.d.ts +src/ipywidgets/webpack.config.js +src/ipywidgets/scripts/copyfiles.js +src/ipywidgets/scripts/clean.js +src/ipywidgets/src/manager.ts +src/ipywidgets/src/widgetLoader.ts +src/ipywidgets/src/libembed.ts +src/ipywidgets/src/index.ts +src/ipywidgets/src/embed.ts +src/ipywidgets/src/signal.ts +src/ipywidgets/src/documentContext.ts + +src/datascience-ui/native-editor/index.tsx +src/datascience-ui/native-editor/nativeCell.tsx +src/datascience-ui/native-editor/addCellLine.tsx +src/datascience-ui/native-editor/toolbar.tsx +src/datascience-ui/native-editor/nativeEditor.tsx +src/datascience-ui/native-editor/redux/mapping.ts +src/datascience-ui/native-editor/redux/actions.ts +src/datascience-ui/native-editor/redux/reducers/movement.ts +src/datascience-ui/native-editor/redux/reducers/index.ts +src/datascience-ui/native-editor/redux/reducers/creation.ts +src/datascience-ui/native-editor/redux/reducers/execution.ts +src/datascience-ui/native-editor/redux/reducers/effects.ts +src/datascience-ui/native-editor/redux/store.ts +src/datascience-ui/renderers/index.tsx +src/datascience-ui/renderers/webviewApi.d.ts +src/datascience-ui/renderers/constants.ts +src/datascience-ui/renderers/render.tsx +src/datascience-ui/plot/index.tsx +src/datascience-ui/plot/testSvg.ts +src/datascience-ui/plot/toolbar.tsx +src/datascience-ui/plot/mainPanel.tsx +src/datascience-ui/ipywidgets/manager.ts +src/datascience-ui/ipywidgets/container.tsx +src/datascience-ui/ipywidgets/types.ts +src/datascience-ui/ipywidgets/index.ts +src/datascience-ui/ipywidgets/kernel.ts +src/datascience-ui/ipywidgets/requirejsRegistry.ts +src/datascience-ui/ipywidgets/incompatibleWidgetHandler.ts +src/datascience-ui/interactive-common/trimmedOutputLink.tsx +src/datascience-ui/interactive-common/trustMessage.tsx +src/datascience-ui/interactive-common/variableExplorerRowRenderer.tsx +src/datascience-ui/interactive-common/variableExplorerHeaderCellFormatter.tsx +src/datascience-ui/interactive-common/code.tsx +src/datascience-ui/interactive-common/buildSettingsCss.ts +src/datascience-ui/interactive-common/markdown.tsx +src/datascience-ui/interactive-common/editor.tsx +src/datascience-ui/interactive-common/mainState.ts +src/datascience-ui/interactive-common/collapseButton.tsx +src/datascience-ui/interactive-common/utils.ts +src/datascience-ui/interactive-common/images.d.ts +src/datascience-ui/interactive-common/tokenizer.ts +src/datascience-ui/interactive-common/cellInput.tsx +src/datascience-ui/interactive-common/variableExplorerEmptyRows.tsx +src/datascience-ui/interactive-common/variablePanel.tsx +src/datascience-ui/interactive-common/jupyterInfo.tsx +src/datascience-ui/interactive-common/executionCount.tsx +src/datascience-ui/interactive-common/handlers.ts +src/datascience-ui/interactive-common/variableExplorer.tsx +src/datascience-ui/interactive-common/intellisenseProvider.ts +src/datascience-ui/interactive-common/variableExplorerButtonCellFormatter.tsx +src/datascience-ui/interactive-common/markdownManipulation.ts +src/datascience-ui/interactive-common/variableExplorerCellFormatter.tsx +src/datascience-ui/interactive-common/cellOutput.tsx +src/datascience-ui/interactive-common/informationMessages.tsx +src/datascience-ui/interactive-common/redux/helpers.ts +src/datascience-ui/interactive-common/redux/reducers/helpers.ts +src/datascience-ui/interactive-common/redux/reducers/monaco.ts +src/datascience-ui/interactive-common/redux/reducers/transfer.ts +src/datascience-ui/interactive-common/redux/reducers/types.ts +src/datascience-ui/interactive-common/redux/reducers/variables.ts +src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts +src/datascience-ui/interactive-common/redux/reducers/kernel.ts +src/datascience-ui/interactive-common/redux/postOffice.ts +src/datascience-ui/interactive-common/redux/store.ts +src/datascience-ui/interactive-common/transforms.tsx +src/datascience-ui/interactive-common/contentPanel.tsx +src/datascience-ui/interactive-common/inputHistory.ts +src/datascience-ui/history-react/index.tsx +src/datascience-ui/history-react/interactivePanel.tsx +src/datascience-ui/history-react/interactiveCell.tsx +src/datascience-ui/history-react/redux/mapping.ts +src/datascience-ui/history-react/redux/actions.ts +src/datascience-ui/history-react/redux/reducers/index.ts +src/datascience-ui/history-react/redux/reducers/creation.ts +src/datascience-ui/history-react/redux/reducers/execution.ts +src/datascience-ui/history-react/redux/reducers/effects.ts +src/datascience-ui/history-react/redux/store.ts +src/datascience-ui/react-common/arePathsSame.ts +src/datascience-ui/react-common/imageButton.tsx +src/datascience-ui/react-common/monacoHelpers.ts +src/datascience-ui/react-common/svgViewer.tsx +src/datascience-ui/react-common/relativeImage.tsx +src/datascience-ui/react-common/progress.tsx +src/datascience-ui/react-common/styleInjector.tsx +src/datascience-ui/react-common/reduxUtils.ts +src/datascience-ui/react-common/monacoEditor.tsx +src/datascience-ui/react-common/textMeasure.ts +src/datascience-ui/react-common/flyout.tsx +src/datascience-ui/react-common/logger.ts +src/datascience-ui/react-common/svgList.tsx +src/datascience-ui/react-common/constants.ts +src/datascience-ui/react-common/settingsReactSide.ts +src/datascience-ui/react-common/locReactSide.ts +src/datascience-ui/react-common/button.tsx +src/datascience-ui/react-common/themeDetector.ts +src/datascience-ui/react-common/image.tsx +src/datascience-ui/react-common/event.ts +src/datascience-ui/react-common/codicon/codicon.ts +src/datascience-ui/react-common/postOffice.ts +src/datascience-ui/react-common/errorBoundary.tsx +src/datascience-ui/common/main.ts +src/datascience-ui/common/cellFactory.ts +src/datascience-ui/common/index.ts +src/datascience-ui/startPage/index.tsx +src/datascience-ui/startPage/startPage.tsx +src/datascience-ui/data-explorer/index.tsx +src/datascience-ui/data-explorer/cellFormatter.tsx +src/datascience-ui/data-explorer/globalJQueryImports.ts +src/datascience-ui/data-explorer/emptyRowsView.tsx +src/datascience-ui/data-explorer/progressBar.tsx +src/datascience-ui/data-explorer/testData.ts +src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx +src/datascience-ui/data-explorer/mainPanel.tsx +src/datascience-ui/data-explorer/reactSlickGrid.tsx + +src/client/interpreter/interpreterService.ts +src/client/interpreter/configuration/interpreterComparer.ts +src/client/interpreter/configuration/interpreterSelector/commands/base.ts +src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts +src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts +src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts +src/client/interpreter/configuration/pythonPathUpdaterService.ts +src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts +src/client/interpreter/configuration/types.ts +src/client/interpreter/configuration/services/globalUpdaterService.ts +src/client/interpreter/configuration/services/workspaceUpdaterService.ts +src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts +src/client/interpreter/serviceRegistry.ts +src/client/interpreter/helpers.ts +src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts +src/client/interpreter/virtualEnvs/types.ts +src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts +src/client/interpreter/virtualEnvs/index.ts +src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts +src/client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation.ts +src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService.ts +src/client/interpreter/autoSelection/types.ts +src/client/interpreter/autoSelection/constants.ts +src/client/interpreter/autoSelection/proxy.ts +src/client/interpreter/autoSelection/rules/baseRule.ts +src/client/interpreter/autoSelection/rules/winRegistry.ts +src/client/interpreter/autoSelection/rules/settings.ts +src/client/interpreter/autoSelection/rules/currentPath.ts +src/client/interpreter/autoSelection/rules/cached.ts +src/client/interpreter/autoSelection/rules/workspaceEnv.ts +src/client/interpreter/autoSelection/rules/system.ts +src/client/interpreter/autoSelection/index.ts +src/client/interpreter/interpreterVersion.ts +src/client/interpreter/contracts.ts +src/client/interpreter/activation/wrapperEnvironmentActivationService.ts +src/client/interpreter/activation/terminalEnvironmentActivationService.ts +src/client/interpreter/activation/preWarmVariables.ts +src/client/interpreter/activation/types.ts +src/client/interpreter/activation/service.ts +src/client/interpreter/locators/types.ts +src/client/interpreter/display/shebangCodeLensProvider.ts +src/client/interpreter/display/index.ts +src/client/interpreter/display/progressDisplay.ts +src/client/interpreter/display/interpreterSelectionTip.ts + +src/client/api.ts +src/client/constants.ts +src/client/extension.ts +src/client/extensionActivation.ts +src/client/extensionInit.ts +src/client/sourceMapSupport.ts +src/client/startupTelemetry.ts + +src/client/typeFormatters/blockFormatProvider.ts +src/client/typeFormatters/contracts.ts +src/client/typeFormatters/codeBlockFormatProvider.ts +src/client/typeFormatters/onEnterFormatter.ts +src/client/typeFormatters/dispatcher.ts + +src/client/terminals/serviceRegistry.ts +src/client/terminals/activation.ts +src/client/terminals/types.ts +src/client/terminals/codeExecution/helper.ts +src/client/terminals/codeExecution/djangoShellCodeExecution.ts +src/client/terminals/codeExecution/repl.ts +src/client/terminals/codeExecution/terminalCodeExecution.ts +src/client/terminals/codeExecution/codeExecutionManager.ts +src/client/terminals/codeExecution/djangoContext.ts + +src/client/providers/objectDefinitionProvider.ts +src/client/providers/serviceRegistry.ts +src/client/providers/symbolProvider.ts +src/client/providers/completionSource.ts +src/client/providers/renameProvider.ts +src/client/providers/hoverProvider.ts +src/client/providers/itemInfoSource.ts +src/client/providers/formatProvider.ts +src/client/providers/importSortProvider.ts +src/client/providers/replProvider.ts +src/client/providers/codeActionProvider/main.ts +src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts +src/client/providers/codeActionProvider/pythonCodeActionProvider.ts +src/client/providers/types.ts +src/client/providers/docStringFoldingProvider.ts +src/client/providers/linterProvider.ts +src/client/providers/providerUtilities.ts +src/client/providers/simpleRefactorProvider.ts +src/client/providers/completionProvider.ts +src/client/providers/jediProxy.ts +src/client/providers/definitionProvider.ts +src/client/providers/referenceProvider.ts +src/client/providers/terminalProvider.ts +src/client/providers/signatureProvider.ts + +src/client/activation/serviceRegistry.ts +src/client/activation/languageServer/manager.ts +src/client/activation/languageServer/languageServerExtension.ts +src/client/activation/languageServer/languageServerProxy.ts +src/client/activation/languageServer/languageClientFactory.ts +src/client/activation/languageServer/platformData.ts +src/client/activation/languageServer/languageServerCompatibilityService.ts +src/client/activation/languageServer/languageServerPackageRepository.ts +src/client/activation/languageServer/languageServerFolderService.ts +src/client/activation/languageServer/outputChannel.ts +src/client/activation/languageServer/languageServerPackageService.ts +src/client/activation/languageServer/analysisOptions.ts +src/client/activation/languageServer/activator.ts +src/client/activation/commands.ts +src/client/activation/activationManager.ts +src/client/activation/progress.ts +src/client/activation/extensionSurvey.ts +src/client/activation/types.ts +src/client/activation/common/languageServerChangeHandler.ts +src/client/activation/common/activatorBase.ts +src/client/activation/common/languageServerFolderService.ts +src/client/activation/common/languageServerPackageService.ts +src/client/activation/common/downloader.ts +src/client/activation/common/packageRepository.ts +src/client/activation/common/analysisOptions.ts +src/client/activation/common/downloadChannelRules.ts +src/client/activation/aaTesting.ts +src/client/activation/refCountedLanguageServer.ts +src/client/activation/jedi.ts +src/client/activation/languageClientMiddleware.ts +src/client/activation/activationService.ts +src/client/activation/node/manager.ts +src/client/activation/node/cancellationUtils.ts +src/client/activation/node/languageServerProxy.ts +src/client/activation/node/languageClientFactory.ts +src/client/activation/node/languageServerPackageRepository.ts +src/client/activation/node/languageServerFolderService.ts +src/client/activation/node/languageServerPackageService.ts +src/client/activation/node/analysisOptions.ts +src/client/activation/node/activator.ts +src/client/activation/none/activator.ts + +src/client/formatters/blackFormatter.ts +src/client/formatters/serviceRegistry.ts +src/client/formatters/helper.ts +src/client/formatters/dummyFormatter.ts +src/client/formatters/autoPep8Formatter.ts +src/client/formatters/lineFormatter.ts +src/client/formatters/types.ts +src/client/formatters/yapfFormatter.ts +src/client/formatters/baseFormatter.ts + +src/client/language/languageConfiguration.ts +src/client/language/characters.ts +src/client/language/textRangeCollection.ts +src/client/language/tokenizer.ts +src/client/language/characterStream.ts +src/client/language/textIterator.ts +src/client/language/types.ts +src/client/language/iterableTextRange.ts +src/client/language/braceCounter.ts +src/client/language/unicode.ts +src/client/language/textBuilder.ts + +src/client/testing/serviceRegistry.ts +src/client/testing/unittest/main.ts +src/client/testing/unittest/helper.ts +src/client/testing/unittest/testConfigurationManager.ts +src/client/testing/unittest/socketServer.ts +src/client/testing/unittest/runner.ts +src/client/testing/unittest/services/parserService.ts +src/client/testing/unittest/services/argsService.ts +src/client/testing/unittest/services/discoveryService.ts +src/client/testing/codeLenses/main.ts +src/client/testing/codeLenses/testFiles.ts +src/client/testing/nosetest/main.ts +src/client/testing/nosetest/testConfigurationManager.ts +src/client/testing/nosetest/runner.ts +src/client/testing/nosetest/services/parserService.ts +src/client/testing/nosetest/services/argsService.ts +src/client/testing/nosetest/services/discoveryService.ts +src/client/testing/main.ts +src/client/testing/pytest/main.ts +src/client/testing/pytest/testConfigurationManager.ts +src/client/testing/pytest/runner.ts +src/client/testing/pytest/services/argsService.ts +src/client/testing/pytest/services/discoveryService.ts +src/client/testing/pytest/services/testMessageService.ts +src/client/testing/configurationFactory.ts +src/client/testing/navigation/serviceRegistry.ts +src/client/testing/navigation/symbolProvider.ts +src/client/testing/navigation/helper.ts +src/client/testing/navigation/commandHandler.ts +src/client/testing/navigation/suiteNavigator.ts +src/client/testing/navigation/types.ts +src/client/testing/navigation/functionNavigator.ts +src/client/testing/navigation/fileNavigator.ts +src/client/testing/explorer/testTreeViewItem.ts +src/client/testing/explorer/testTreeViewProvider.ts +src/client/testing/explorer/commandHandlers.ts +src/client/testing/explorer/failedTestHandler.ts +src/client/testing/explorer/treeView.ts +src/client/testing/types.ts +src/client/testing/common/argumentsHelper.ts +src/client/testing/common/enablementTracker.ts +src/client/testing/common/debugLauncher.ts +src/client/testing/common/managers/testConfigurationManager.ts +src/client/testing/common/managers/baseTestManager.ts +src/client/testing/common/types.ts +src/client/testing/common/constants.ts +src/client/testing/common/testUtils.ts +src/client/testing/common/xUnitParser.ts +src/client/testing/common/updateTestSettings.ts +src/client/testing/common/testVisitors/visitor.ts +src/client/testing/common/testVisitors/flatteningVisitor.ts +src/client/testing/common/testVisitors/resultResetVisitor.ts +src/client/testing/common/runner.ts +src/client/testing/common/services/discoveredTestParser.ts +src/client/testing/common/services/contextService.ts +src/client/testing/common/services/testResultsService.ts +src/client/testing/common/services/storageService.ts +src/client/testing/common/services/types.ts +src/client/testing/common/services/unitTestDiagnosticService.ts +src/client/testing/common/services/testsStatusService.ts +src/client/testing/common/services/discovery.ts +src/client/testing/common/services/configSettingService.ts +src/client/testing/common/services/testManagerService.ts +src/client/testing/common/services/workspaceTestManagerService.ts +src/client/testing/display/main.ts +src/client/testing/display/picker.ts +src/client/testing/configuration.ts + +src/client/common/configuration/service.ts +src/client/common/serviceRegistry.ts +src/client/common/helpers.ts +src/client/common/net/browser.ts +src/client/common/net/fileDownloader.ts +src/client/common/net/httpClient.ts +src/client/common/net/socket/socketCallbackHandler.ts +src/client/common/net/socket/socketServer.ts +src/client/common/net/socket/SocketStream.ts +src/client/common/asyncDisposableRegistry.ts +src/client/common/editor.ts +src/client/common/contextKey.ts +src/client/common/markdown/restTextConverter.ts +src/client/common/featureDeprecationManager.ts +src/client/common/experiments/manager.ts +src/client/common/experiments/groups.ts +src/client/common/experiments/telemetry.ts +src/client/common/experiments/service.ts +src/client/common/refBool.ts +src/client/common/open.ts +src/client/common/platform/serviceRegistry.ts +src/client/common/platform/errors.ts +src/client/common/platform/fs-temp.ts +src/client/common/platform/fs-paths.ts +src/client/common/platform/platformService.ts +src/client/common/platform/types.ts +src/client/common/platform/constants.ts +src/client/common/platform/fileSystem.ts +src/client/common/platform/registry.ts +src/client/common/platform/pathUtils.ts +src/client/common/persistentState.ts +src/client/common/terminal/activator/base.ts +src/client/common/terminal/activator/powershellFailedHandler.ts +src/client/common/terminal/activator/index.ts +src/client/common/terminal/helper.ts +src/client/common/terminal/syncTerminalService.ts +src/client/common/terminal/factory.ts +src/client/common/terminal/types.ts +src/client/common/terminal/commandPrompt.ts +src/client/common/terminal/service.ts +src/client/common/terminal/shellDetector.ts +src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts +src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts +src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts +src/client/common/terminal/shellDetectors/settingsShellDetector.ts +src/client/common/terminal/shellDetectors/baseShellDetector.ts +src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts +src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts +src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts +src/client/common/terminal/environmentActivationProviders/commandPrompt.ts +src/client/common/terminal/environmentActivationProviders/bash.ts +src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts +src/client/common/utils/decorators.ts +src/client/common/utils/enum.ts +src/client/common/utils/async.ts +src/client/common/utils/text.ts +src/client/common/utils/localize.ts +src/client/common/utils/regexp.ts +src/client/common/utils/platform.ts +src/client/common/utils/multiStepInput.ts +src/client/common/utils/stopWatch.ts +src/client/common/utils/random.ts +src/client/common/utils/serializers.ts +src/client/common/utils/icons.ts +src/client/common/utils/sysTypes.ts +src/client/common/utils/version.ts +src/client/common/utils/misc.ts +src/client/common/utils/logging.ts +src/client/common/utils/cacheUtils.ts +src/client/common/utils/workerPool.ts +src/client/common/crypto.ts +src/client/common/extensions.ts +src/client/common/dotnet/compatibilityService.ts +src/client/common/dotnet/serviceRegistry.ts +src/client/common/dotnet/types.ts +src/client/common/dotnet/services/unknownOsCompatibilityService.ts +src/client/common/dotnet/services/macCompatibilityService.ts +src/client/common/dotnet/services/linuxCompatibilityService.ts +src/client/common/dotnet/services/windowsCompatibilityService.ts +src/client/common/types.ts +src/client/common/logger.ts +src/client/common/configSettings.ts +src/client/common/constants.ts +src/client/common/variables/serviceRegistry.ts +src/client/common/variables/environment.ts +src/client/common/variables/types.ts +src/client/common/variables/environmentVariablesProvider.ts +src/client/common/variables/sysTypes.ts +src/client/common/variables/systemVariables.ts +src/client/common/nuget/azureBlobStoreNugetRepository.ts +src/client/common/nuget/nugetRepository.ts +src/client/common/nuget/types.ts +src/client/common/nuget/nugetService.ts +src/client/common/cancellation.ts +src/client/common/interpreterPathService.ts +src/client/common/startPage/startPage.ts +src/client/common/startPage/types.ts +src/client/common/startPage/startPageMessageListener.ts +src/client/common/application/customEditorService.ts +src/client/common/application/commands.ts +src/client/common/application/applicationShell.ts +src/client/common/application/languageService.ts +src/client/common/application/notebook.ts +src/client/common/application/clipboard.ts +src/client/common/application/workspace.ts +src/client/common/application/debugSessionTelemetry.ts +src/client/common/application/extensions.ts +src/client/common/application/types.ts +src/client/common/application/activeResource.ts +src/client/common/application/commandManager.ts +src/client/common/application/documentManager.ts +src/client/common/application/webPanels/webPanelProvider.ts +src/client/common/application/webPanels/webPanel.ts +src/client/common/application/debugService.ts +src/client/common/application/commands/reloadCommand.ts +src/client/common/application/terminalManager.ts +src/client/common/application/applicationEnvironment.ts +src/client/common/errors/errorUtils.ts +src/client/common/errors/moduleNotInstalledError.ts +src/client/common/installer/serviceRegistry.ts +src/client/common/installer/productNames.ts +src/client/common/installer/condaInstaller.ts +src/client/common/installer/extensionBuildInstaller.ts +src/client/common/installer/productInstaller.ts +src/client/common/installer/channelManager.ts +src/client/common/installer/moduleInstaller.ts +src/client/common/installer/types.ts +src/client/common/installer/poetryInstaller.ts +src/client/common/installer/pipEnvInstaller.ts +src/client/common/installer/productService.ts +src/client/common/installer/pipInstaller.ts +src/client/common/installer/productPath.ts +src/client/common/process/currentProcess.ts +src/client/common/process/processFactory.ts +src/client/common/process/serviceRegistry.ts +src/client/common/process/pythonDaemon.ts +src/client/common/process/pythonToolService.ts +src/client/common/process/internal/python.ts +src/client/common/process/internal/scripts/testing_tools.ts +src/client/common/process/internal/scripts/vscode_datascience_helpers.ts +src/client/common/process/internal/scripts/index.ts +src/client/common/process/pythonDaemonPool.ts +src/client/common/process/pythonDaemonFactory.ts +src/client/common/process/types.ts +src/client/common/process/logger.ts +src/client/common/process/baseDaemon.ts +src/client/common/process/constants.ts +src/client/common/process/pythonProcess.ts +src/client/common/process/proc.ts +src/client/common/process/pythonEnvironment.ts +src/client/common/process/decoder.ts +src/client/common/process/pythonExecutionFactory.ts +src/client/common/insidersBuild/insidersExtensionPrompt.ts +src/client/common/insidersBuild/insidersExtensionService.ts +src/client/common/insidersBuild/types.ts +src/client/common/insidersBuild/downloadChannelService.ts +src/client/common/insidersBuild/downloadChannelRules.ts + +src/client/debugger/extension/configuration/providers/moduleLaunch.ts +src/client/debugger/extension/configuration/providers/flaskLaunch.ts +src/client/debugger/extension/configuration/providers/fileLaunch.ts +src/client/debugger/extension/configuration/providers/remoteAttach.ts +src/client/debugger/extension/configuration/providers/djangoLaunch.ts +src/client/debugger/extension/configuration/providers/providerFactory.ts +src/client/debugger/extension/configuration/providers/pyramidLaunch.ts +src/client/debugger/extension/configuration/providers/pidAttach.ts +src/client/debugger/extension/configuration/resolvers/base.ts +src/client/debugger/extension/configuration/resolvers/helper.ts +src/client/debugger/extension/configuration/resolvers/launch.ts +src/client/debugger/extension/configuration/resolvers/attach.ts +src/client/debugger/extension/configuration/types.ts +src/client/debugger/extension/configuration/debugConfigurationService.ts +src/client/debugger/extension/configuration/launch.json/updaterService.ts +src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts +src/client/debugger/extension/configuration/launch.json/completionProvider.ts +src/client/debugger/extension/banner.ts +src/client/debugger/extension/serviceRegistry.ts +src/client/debugger/extension/adapter/remoteLaunchers.ts +src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts +src/client/debugger/extension/adapter/factory.ts +src/client/debugger/extension/adapter/types.ts +src/client/debugger/extension/adapter/activator.ts +src/client/debugger/extension/adapter/logging.ts +src/client/debugger/extension/types.ts +src/client/debugger/extension/hooks/eventHandlerDispatcher.ts +src/client/debugger/extension/hooks/types.ts +src/client/debugger/extension/hooks/constants.ts +src/client/debugger/extension/hooks/childProcessAttachHandler.ts +src/client/debugger/extension/hooks/childProcessAttachService.ts +src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts +src/client/debugger/extension/attachQuickPick/factory.ts +src/client/debugger/extension/attachQuickPick/types.ts +src/client/debugger/extension/attachQuickPick/psProcessParser.ts +src/client/debugger/extension/attachQuickPick/provider.ts +src/client/debugger/extension/attachQuickPick/picker.ts +src/client/debugger/extension/helpers/protocolParser.ts +src/client/debugger/types.ts +src/client/debugger/constants.ts + +src/client/languageServices/jediProxyFactory.ts +src/client/languageServices/proposeLanguageServerBanner.ts + +src/client/telemetry/types.ts +src/client/telemetry/importTracker.ts +src/client/telemetry/constants.ts +src/client/telemetry/index.ts +src/client/telemetry/envFileTelemetry.ts +src/client/telemetry/extensionInstallTelemetry.ts + +src/client/linters/pydocstyle.ts +src/client/linters/serviceRegistry.ts +src/client/linters/linterAvailability.ts +src/client/linters/lintingEngine.ts +src/client/linters/prospector.ts +src/client/linters/pycodestyle.ts +src/client/linters/linterInfo.ts +src/client/linters/bandit.ts +src/client/linters/linterCommands.ts +src/client/linters/flake8.ts +src/client/linters/errorHandlers/baseErrorHandler.ts +src/client/linters/errorHandlers/errorHandler.ts +src/client/linters/errorHandlers/notInstalled.ts +src/client/linters/errorHandlers/standard.ts +src/client/linters/types.ts +src/client/linters/mypy.ts +src/client/linters/baseLinter.ts +src/client/linters/constants.ts +src/client/linters/linterManager.ts +src/client/linters/pylama.ts +src/client/linters/pylint.ts + +src/client/application/serviceRegistry.ts +src/client/application/types.ts +src/client/application/diagnostics/surceMapSupportService.ts +src/client/application/diagnostics/base.ts +src/client/application/diagnostics/applicationDiagnostics.ts +src/client/application/diagnostics/serviceRegistry.ts +src/client/application/diagnostics/filter.ts +src/client/application/diagnostics/checks/upgradeCodeRunner.ts +src/client/application/diagnostics/checks/powerShellActivation.ts +src/client/application/diagnostics/checks/envPathVariable.ts +src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts +src/client/application/diagnostics/checks/pythonPathDeprecated.ts +src/client/application/diagnostics/checks/lsNotSupported.ts +src/client/application/diagnostics/checks/macPythonInterpreter.ts +src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts +src/client/application/diagnostics/checks/pythonInterpreter.ts +src/client/application/diagnostics/promptHandler.ts +src/client/application/diagnostics/types.ts +src/client/application/diagnostics/constants.ts +src/client/application/diagnostics/commands/base.ts +src/client/application/diagnostics/commands/ignore.ts +src/client/application/diagnostics/commands/factory.ts +src/client/application/diagnostics/commands/execVSCCommand.ts +src/client/application/diagnostics/commands/types.ts +src/client/application/diagnostics/commands/launchBrowser.ts +src/client/application/misc/joinMailingListPrompt.ts + +src/client/datascience/baseJupyterSession.ts +src/client/datascience/data-viewing/jupyterVariableDataProviderFactory.ts +src/client/datascience/data-viewing/dataViewerMessageListener.ts +src/client/datascience/data-viewing/jupyterVariableDataProvider.ts +src/client/datascience/data-viewing/types.ts +src/client/datascience/data-viewing/dataViewer.ts +src/client/datascience/data-viewing/dataViewerDependencyService.ts +src/client/datascience/data-viewing/dataViewerFactory.ts +src/client/datascience/shiftEnterBanner.ts +src/client/datascience/dataScienceSurveyBanner.ts +src/client/datascience/serviceRegistry.ts +src/client/datascience/gather/gatherLogger.ts +src/client/datascience/gather/gatherListener.ts +src/client/datascience/webViewHost.ts +src/client/datascience/context/activeEditorContext.ts +src/client/datascience/progress/progressReporter.ts +src/client/datascience/progress/messages.ts +src/client/datascience/progress/types.ts +src/client/datascience/progress/decorator.ts +src/client/datascience/codeCssGenerator.ts +src/client/datascience/kernel-launcher/helpers.ts +src/client/datascience/kernel-launcher/kernelFinder.ts +src/client/datascience/kernel-launcher/kernelProcess.ts +src/client/datascience/kernel-launcher/types.ts +src/client/datascience/kernel-launcher/kernelDaemonPool.ts +src/client/datascience/kernel-launcher/kernelLauncherDaemon.ts +src/client/datascience/kernel-launcher/kernelDaemon.ts +src/client/datascience/kernel-launcher/kernelDaemonPreWarmer.ts +src/client/datascience/kernel-launcher/kernelLauncher.ts +src/client/datascience/ipywidgets/localWidgetScriptSourceProvider.ts +src/client/datascience/ipywidgets/ipyWidgetScriptSourceProvider.ts +src/client/datascience/ipywidgets/ipyWidgetScriptSource.ts +src/client/datascience/ipywidgets/cdnWidgetScriptSourceProvider.ts +src/client/datascience/ipywidgets/types.ts +src/client/datascience/ipywidgets/remoteWidgetScriptSourceProvider.ts +src/client/datascience/ipywidgets/constants.ts +src/client/datascience/ipywidgets/ipyWidgetMessageDispatcher.ts +src/client/datascience/ipywidgets/ipywidgetHandler.ts +src/client/datascience/ipywidgets/ipyWidgetMessageDispatcherFactory.ts +src/client/datascience/themeFinder.ts +src/client/datascience/multiplexingDebugService.ts +src/client/datascience/interactive-window/identity.ts +src/client/datascience/interactive-window/interactiveWindow.ts +src/client/datascience/interactive-window/interactiveWindowCommandListener.ts +src/client/datascience/interactive-window/interactiveWindowProvider.ts +src/client/datascience/datascience.ts +src/client/datascience/liveshare/liveshare.ts +src/client/datascience/liveshare/serviceProxy.ts +src/client/datascience/liveshare/liveshareProxy.ts +src/client/datascience/liveshare/postOffice.ts +src/client/datascience/jupyterUriProviderRegistration.ts +src/client/datascience/messages.ts +src/client/datascience/raw-kernel/rawJupyterSession.ts +src/client/datascience/raw-kernel/rawNotebookProvider.ts +src/client/datascience/raw-kernel/rawNotebookSupportedService.ts +src/client/datascience/raw-kernel/liveshare/guestRawNotebookProvider.ts +src/client/datascience/raw-kernel/liveshare/hostRawNotebookProvider.ts +src/client/datascience/raw-kernel/rawKernel.ts +src/client/datascience/raw-kernel/rawSession.ts +src/client/datascience/raw-kernel/rawSocket.ts +src/client/datascience/raw-kernel/rawNotebookProviderWrapper.ts +src/client/datascience/crossProcessLock.ts +src/client/datascience/debugLocationTrackerFactory.ts +src/client/datascience/preWarmVariables.ts +src/client/datascience/common.ts +src/client/datascience/kernelSocketWrapper.ts +src/client/datascience/jupyterDebugService.ts +src/client/datascience/utils.ts +src/client/datascience/interactive-common/serialization.ts +src/client/datascience/interactive-common/showPlotListener.ts +src/client/datascience/interactive-common/debugListener.ts +src/client/datascience/interactive-common/interactiveBase.ts +src/client/datascience/interactive-common/types.ts +src/client/datascience/interactive-common/linkProvider.ts +src/client/datascience/interactive-common/notebookUsageTracker.ts +src/client/datascience/interactive-common/interactiveWindowTypes.ts +src/client/datascience/interactive-common/synchronization.ts +src/client/datascience/interactive-common/notebookProvider.ts +src/client/datascience/interactive-common/interactiveWindowMessageListener.ts +src/client/datascience/interactive-common/intellisense/wordHelper.ts +src/client/datascience/interactive-common/intellisense/intellisenseDocument.ts +src/client/datascience/interactive-common/intellisense/intellisenseLine.ts +src/client/datascience/interactive-common/intellisense/conversion.ts +src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts +src/client/datascience/interactive-common/notebookServerProvider.ts +src/client/datascience/activation.ts +src/client/datascience/jupyterUriProviderWrapper.ts +src/client/datascience/types.ts +src/client/datascience/errorHandler/errorHandler.ts +src/client/datascience/cellMatcher.ts +src/client/datascience/notebookStorage/notebookModel.ts +src/client/datascience/notebookStorage/notebookModelEditEvent.ts +src/client/datascience/notebookStorage/notebookStorageProvider.ts +src/client/datascience/notebookStorage/nativeEditorStorage.ts +src/client/datascience/notebookStorage/factory.ts +src/client/datascience/notebookStorage/types.ts +src/client/datascience/notebookStorage/nativeEditorProvider.ts +src/client/datascience/notebookStorage/vscNotebookModel.ts +src/client/datascience/notebookStorage/baseModel.ts +src/client/datascience/debugLocationTracker.ts +src/client/datascience/plotting/plotViewerMessageListener.ts +src/client/datascience/plotting/types.ts +src/client/datascience/plotting/plotViewer.ts +src/client/datascience/plotting/plotViewerProvider.ts +src/client/datascience/constants.ts +src/client/datascience/monacoMessages.ts +src/client/datascience/interactive-ipynb/nativeEditorRunByLineListener.ts +src/client/datascience/interactive-ipynb/nativeEditorViewTracker.ts +src/client/datascience/interactive-ipynb/trustCommandHandler.ts +src/client/datascience/interactive-ipynb/nativeEditorOldWebView.ts +src/client/datascience/interactive-ipynb/nativeEditorSynchronizer.ts +src/client/datascience/interactive-ipynb/nativeEditor.ts +src/client/datascience/interactive-ipynb/trustService.ts +src/client/datascience/interactive-ipynb/digestStorage.ts +src/client/datascience/interactive-ipynb/nativeEditorProviderOld.ts +src/client/datascience/interactive-ipynb/nativeEditorCommandListener.ts +src/client/datascience/interactive-ipynb/autoSaveService.ts +src/client/datascience/editor-integration/hoverProvider.ts +src/client/datascience/editor-integration/codeLensFactory.ts +src/client/datascience/editor-integration/codewatcher.ts +src/client/datascience/editor-integration/decorator.ts +src/client/datascience/editor-integration/codelensprovider.ts +src/client/datascience/editor-integration/cellhashprovider.ts +src/client/datascience/commands/commandLineSelector.ts +src/client/datascience/commands/notebookCommands.ts +src/client/datascience/commands/exportCommands.ts +src/client/datascience/commands/commandRegistry.ts +src/client/datascience/commands/serverSelector.ts +src/client/datascience/cellFactory.ts +src/client/datascience/notebook/contentProvider.ts +src/client/datascience/notebook/notebookDisposeService.ts +src/client/datascience/notebook/serviceRegistry.ts +src/client/datascience/notebook/notebookEditor.ts +src/client/datascience/notebook/notebookEditorCompatibilitySupport.ts +src/client/datascience/notebook/kernelProvider.ts +src/client/datascience/notebook/integration.ts +src/client/datascience/notebook/types.ts +src/client/datascience/notebook/notebookEditorProvider.ts +src/client/datascience/notebook/constants.ts +src/client/datascience/notebook/notebookEditorProviderWrapper.ts +src/client/datascience/notebook/renderer.ts +src/client/datascience/notebook/rendererExtensionDownloader.ts +src/client/datascience/notebook/helpers/multiCancellationToken.ts +src/client/datascience/notebook/helpers/helpers.ts +src/client/datascience/notebook/helpers/executionHelpers.ts +src/client/datascience/notebook/survey.ts +src/client/datascience/notebook/rendererExtension.ts +src/client/datascience/export/exportToHTML.ts +src/client/datascience/export/exportToPython.ts +src/client/datascience/export/exportUtil.ts +src/client/datascience/export/exportManager.ts +src/client/datascience/export/types.ts +src/client/datascience/export/exportToPDF.ts +src/client/datascience/export/exportManagerFilePicker.ts +src/client/datascience/export/exportBase.ts +src/client/datascience/export/exportDependencyChecker.ts +src/client/datascience/export/exportFileOpener.ts +src/client/datascience/notebookAndInteractiveTracker.ts +src/client/datascience/statusProvider.ts +src/client/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand.ts +src/client/datascience/jupyter/interpreter/jupyterCommand.ts +src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts +src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts +src/client/datascience/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.ts +src/client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.ts +src/client/datascience/jupyter/interpreter/jupyterInterpreterSelector.ts +src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts +src/client/datascience/jupyter/kernels/kernelSelector.ts +src/client/datascience/jupyter/kernels/jupyterKernelPromiseFailedError.ts +src/client/datascience/jupyter/kernels/helpers.ts +src/client/datascience/jupyter/kernels/kernelExecution.ts +src/client/datascience/jupyter/kernels/kernelSwitcher.ts +src/client/datascience/jupyter/kernels/kernelService.ts +src/client/datascience/jupyter/kernels/kernelProvider.ts +src/client/datascience/jupyter/kernels/types.ts +src/client/datascience/jupyter/kernels/kernelSelections.ts +src/client/datascience/jupyter/kernels/jupyterKernelSpec.ts +src/client/datascience/jupyter/kernels/kernelDependencyService.ts +src/client/datascience/jupyter/kernels/kernel.ts +src/client/datascience/jupyter/kernels/cellExecution.ts +src/client/datascience/jupyter/jupyterNotebook.ts +src/client/datascience/jupyter/jupyterExecutionFactory.ts +src/client/datascience/jupyter/jupyterSession.ts +src/client/datascience/jupyter/serverPreload.ts +src/client/datascience/jupyter/jupyterRequest.ts +src/client/datascience/jupyter/jupyterNotebookProvider.ts +src/client/datascience/jupyter/commandLineSelector.ts +src/client/datascience/jupyter/jupyterVariables.ts +src/client/datascience/jupyter/jupyterDebugger.ts +src/client/datascience/jupyter/liveshare/hostJupyterServer.ts +src/client/datascience/jupyter/liveshare/roleBasedFactory.ts +src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts +src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts +src/client/datascience/jupyter/liveshare/responseQueue.ts +src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts +src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts +src/client/datascience/jupyter/liveshare/utils.ts +src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts +src/client/datascience/jupyter/liveshare/guestJupyterSessionManagerFactory.ts +src/client/datascience/jupyter/liveshare/types.ts +src/client/datascience/jupyter/liveshare/guestJupyterServer.ts +src/client/datascience/jupyter/liveshare/serverCache.ts +src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts +src/client/datascience/jupyter/kernelVariables.ts +src/client/datascience/jupyter/jupyterSessionManagerFactory.ts +src/client/datascience/jupyter/jupyterDebuggerPortBlockedError.ts +src/client/datascience/jupyter/jupyterConnectError.ts +src/client/datascience/jupyter/jupyterExecution.ts +src/client/datascience/jupyter/debuggerVariableRegistration.ts +src/client/datascience/jupyter/jupyterDebuggerRemoteNotSupported.ts +src/client/datascience/jupyter/jupyterConnection.ts +src/client/datascience/jupyter/jupyterPasswordConnect.ts +src/client/datascience/jupyter/jupyterDebuggerNotInstalledError.ts +src/client/datascience/jupyter/jupyterSelfCertsError.ts +src/client/datascience/jupyter/jupyterDebuggerPortNotAvailableError.ts +src/client/datascience/jupyter/jupyterWebSocket.ts +src/client/datascience/jupyter/jupyterServer.ts +src/client/datascience/jupyter/jupyterInvalidKernelError.ts +src/client/datascience/jupyter/jupyterExporter.ts +src/client/datascience/jupyter/notebookStarter.ts +src/client/datascience/jupyter/jupyterZMQBinariesNotFoundError.ts +src/client/datascience/jupyter/serverSelector.ts +src/client/datascience/jupyter/jupyterUtils.ts +src/client/datascience/jupyter/jupyterSessionManager.ts +src/client/datascience/jupyter/jupyterDataRateLimitError.ts +src/client/datascience/jupyter/variableScriptLoader.ts +src/client/datascience/jupyter/jupyterServerWrapper.ts +src/client/datascience/jupyter/jupyterImporter.ts +src/client/datascience/jupyter/jupyterInstallError.ts +src/client/datascience/jupyter/oldJupyterVariables.ts +src/client/datascience/jupyter/jupyterInterruptError.ts +src/client/datascience/jupyter/invalidNotebookFileError.ts +src/client/datascience/jupyter/jupyterWaitForIdleError.ts +src/client/datascience/jupyter/jupyterCellOutputMimeTypeTracker.ts +src/client/datascience/jupyter/debuggerVariables.ts +src/client/datascience/dataScienceFileSystem.ts +src/client/logging/levels.ts +src/client/logging/transports.ts +src/client/logging/_global.ts +src/client/logging/logger.ts +src/client/logging/util.ts +src/client/logging/index.ts +src/client/logging/formatters.ts +src/client/logging/trace.ts +src/client/ioc/serviceManager.ts +src/client/ioc/container.ts +src/client/ioc/types.ts +src/client/ioc/index.ts +src/client/refactor/proxy.ts +src/client/workspaceSymbols/main.ts +src/client/workspaceSymbols/contracts.ts +src/client/workspaceSymbols/generator.ts +src/client/workspaceSymbols/parser.ts +src/client/workspaceSymbols/provider.ts + diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000000..3e84dbf6fe39 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,103 @@ +{ + "env": { + "node": true, + "es6": true, + "mocha": true + }, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "airbnb", + "plugin:@typescript-eslint/recommended", + "plugin:import/errors", + "plugin:import/warnings", + "plugin:import/typescript" + ], + "rules": { + // Overriding ESLint rules with Typescript-specific ones + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-ignore": "allow-with-description" + } + ], + "no-bitwise": "off", + "no-dupe-class-members": "off", + "@typescript-eslint/no-dupe-class-members": "error", + "no-empty-function": "off", + "@typescript-eslint/no-empty-function": ["error"], + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error", + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": [ + "error", + { + "functions": false + } + ], + "no-useless-constructor": "off", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/no-var-requires": "off", + // Other rules + "func-names": "off", + "import/extensions": "off", + "import/namespace": "off", + "import/no-extraneous-dependencies": "off", + "import/no-unresolved": [ + "error", + { + "ignore": ["monaco-editor", "vscode"] + } + ], + "import/prefer-default-export": "off", + "indent": [ + "error", + 4, + { + "SwitchCase": 1 + } + ], + "linebreak-style": "off", + "max-len": [ + "warn", + { + "code": 120, + "ignorePattern": "^import\\s.+\\sfrom\\s.+;$", + "ignoreStrings": true, + "ignoreTemplateLiterals": true, + "ignoreUrls": true + } + ], + "no-confusing-arrow": [ + "error", + { + "allowParens": true + } + ], + "no-console": "off", + "no-control-regex": "off", + "no-extend-native": "off", + "no-multi-str": "off", + "no-param-reassign": "off", + "no-prototype-builtins": "off", + "no-template-curly-in-string": "off", + "no-underscore-dangle": "off", + "no-useless-escape": "off", + "no-void": [ + "error", + { + "allowAsStatement": true + } + ], + "operator-assignment": "off", + "react/jsx-filename-extension": [ + 1, + { + "extensions": [".tsx"] + } + ], + "strict": "off" + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..e25c2877c07f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +package.json text eol=lf +package-lock.json text eol=lf +requirements.txt text eol=lf diff --git a/.github/ISSUE_TEMPLATE/1_ds_bug_report.md b/.github/ISSUE_TEMPLATE/1_ds_bug_report.md new file mode 100644 index 000000000000..f9f2a490d336 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_ds_bug_report.md @@ -0,0 +1,49 @@ +--- +name: Bug report for Notebook Editor, Interactive Window, Python Editor cells +about: Create a report to help us improve +labels: type-bug, data science +--- + +# Bug: Notebook Editor, Interactive Window, Editor cells + + + +## Steps to cause the bug to occur + +1. + +## Actual behavior + +## Expected behavior + + + +### Your Jupyter and/or Python environment + +_Please provide as much info as you readily know_ + +- **Jupyter server running:** Local | Remote | N/A +- **Extension version:** 20YY.MM.#####-xxx +- **VS Code version:** #.## +- **Setting python.jediEnabled:** true | false +- **Setting python.languageServer:** Jedi | Microsoft | None +- **Python and/or Anaconda version:** #.#.# +- **OS:** Windows | Mac | Linux (distro): +- **Virtual environment:** conda | venv | virtualenv | N/A | ... + +## Python Output + + + +Microsoft Data Science for VS Code Engineering Team: @rchiodo, @IanMatthewHuff, @DavidKutu, @DonJayamanne, @greazer, @joyceerhl diff --git a/.github/ISSUE_TEMPLATE/2_bug_report.md b/.github/ISSUE_TEMPLATE/2_bug_report.md new file mode 100644 index 000000000000..63ebddf67b47 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_bug_report.md @@ -0,0 +1,54 @@ +--- +name: General bug report +about: Create a report to help us improve +labels: classify, type-bug +--- + + + +## Environment data + +- VS Code version: XXX +- Extension version (available under the Extensions sidebar): XXX +- OS and version: XXX +- Python version (& distribution if applicable, e.g. Anaconda): XXX +- Type of virtual environment used (N/A | venv | virtualenv | conda | ...): XXX +- Relevant/affected Python packages and their versions: XXX +- Relevant/affected Python-related VS Code extensions and their versions: XXX +- Value of the `python.languageServer` setting: XXX + +[**NOTE**: If you suspect that your issue is related to the Microsoft Python Language Server (`python.languageServer: 'Microsoft'`), please download our new language server [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) from the VS Code marketplace to see if that fixes your issue] + +## Expected behaviour + +XXX + +## Actual behaviour + +XXX + +## Steps to reproduce: + +[**NOTE**: Self-contained, minimal reproducing code samples are **extremely** helpful and will expedite addressing your issue] + +1. XXX + + + +## Logs + +
+ +Output for Python in the Output panel (ViewOutput, change the drop-down the upper-right of the Output panel to Python) + + +

+ +``` +XXX +``` + +

+
diff --git a/.github/ISSUE_TEMPLATE/3_ds_feature_request.md b/.github/ISSUE_TEMPLATE/3_ds_feature_request.md new file mode 100644 index 000000000000..71876c9f3aad --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3_ds_feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature request for Notebook Editor, Interactive Window, Editor cells +about: Suggest an idea for this project +labels: type-enhancement, data science +--- + +# Feature: Notebook Editor, Interactive Window, Python Editor cells + + + +## Description + +Microsoft Data Science for VS Code Engineering Team: @rchiodo, @IanMatthewHuff, @DavidKutu, @DonJayamanne, @greazer, @joyceerhl diff --git a/.github/ISSUE_TEMPLATE/4_feature_request.md b/.github/ISSUE_TEMPLATE/4_feature_request.md new file mode 100644 index 000000000000..e98872ab0d10 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4_feature_request.md @@ -0,0 +1,9 @@ +--- +name: General feature request +about: Suggest an idea for this project +labels: classify, type-enhancement +--- + + + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..ef0e2aae348f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Stack Overflow + url: https://stackoverflow.com/questions/tagged/visual-studio-code+python + about: Please ask questions here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000000..c1b633b61d84 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +For # + + + +- [ ] Pull request represents a single change (i.e. not fixing disparate/unrelated things in a single PR). +- [ ] Title summarizes what is changing. +- [ ] Has a [news entry](https://github.com/Microsoft/vscode-python/tree/main/news) file (remember to thank yourself!). +- [ ] Appropriate comments and documentation strings in the code. +- [ ] Has sufficient logging. +- [ ] Has telemetry for enhancements. +- [ ] Unit tests & system/integration tests are added/updated. +- [ ] [Test plan](https://github.com/Microsoft/vscode-python/blob/main/.github/test_plan.md) is updated as appropriate. +- [ ] [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/main/package-lock.json) has been regenerated by running `npm install` (if dependencies have changed). +- [ ] The wiki is updated with any design decisions/details. diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml new file mode 100644 index 000000000000..787bb999022e --- /dev/null +++ b/.github/actions/build-vsix/action.yml @@ -0,0 +1,34 @@ +name: 'Build VSIX' +description: "Build the extension's VSIX" + +outputs: + path: + description: 'Path to the VSIX' + value: 'ms-python-insiders.vsix' + +runs: + using: 'composite' + steps: + # For faster/better builds of sdists. + - run: python -m pip install wheel + shell: bash + + - run: python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt + shell: bash + + - run: | + python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python ./pythonFiles/install_debugpy.py + shell: bash + + - run: npm ci --prefer-offline + shell: bash + + # Use the GITHUB_RUN_ID environment variable to update the build number. + # GITHUB_RUN_ID is a unique number for each run within a repository. + # This number does not change if you re-run the workflow run. + - run: npm run updateBuildNumber -- --buildNumber $GITHUB_RUN_ID + shell: bash + + - run: npm run package + shell: bash diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..e2e077e3a7d1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: / + schedule: + interval: daily + + - package-ecosystem: 'pip' + directory: / + schedule: + interval: daily + + - package-ecosystem: 'pip' + directory: /news + schedule: + interval: monthly + # Activate when we feel ready to keep up with frequency. + # - package-ecosystem: 'npm' + # directory: / + # schedule: + # interval: daily diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 000000000000..16fbb5233be9 --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,2 @@ +daysUntilLock: 7 +lockComment: false diff --git a/.github/release_plan.md b/.github/release_plan.md new file mode 100644 index 000000000000..540e141b56f1 --- /dev/null +++ b/.github/release_plan.md @@ -0,0 +1,92 @@ +# Prerequisites + +- Python 3.7 and higher +- run `python3 -m pip install --user -r news/requirements.txt` + +# Release candidate (Monday, XXX XX) + +- [ ] Announce the code freeze on both Teams and e-mail, leave enough time for teams to surface any last minute issues that need to get in before freeze. Make sure debugger and Language Server teams are looped in as well. +- [ ] Update `main` for the release + - [ ] Create a branch against `main` for a pull request + - [ ] Change the version in [`package.json`](https://github.com/Microsoft/vscode-python/blob/main/package.json) from a `-dev` suffix to `-rc` (🤖) + - [ ] Run `npm install` to make sure [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/main/package.json) is up-to-date (🤖) + - [ ] Extension will pick up latest version of `debugpy`. If you need to pin to a particular version see `install_debugpy.py`. + - [ ] Update `languageServerVersion` in `package.json` to point to the latest version of the [Language Server](https://github.com/Microsoft/python-language-server). Check with the language server team if this needs updating. + - [ ] Update [`CHANGELOG.md`](https://github.com/Microsoft/vscode-python/blob/main/CHANGELOG.md) (🤖) + - [ ] Run [`news`](https://github.com/Microsoft/vscode-python/tree/main/news) (typically `python news --final --update CHANGELOG.md | code-insiders -`) + - [ ] Copy over the "Thanks" section from the previous release into the "Thanks" section for the new release + - [ ] Make sure the "Thanks" section is up-to-date (e.g. compare to versions in [`requirements.txt`](https://github.com/microsoft/vscode-python/blob/main/requirements.txt)) + - [ ] Touch up news entries (e.g. add missing periods) + - [ ] Check the Markdown rendering to make sure everything looks good + - [ ] Add any relevant news entries for `debugpy` and the language server if they were updated + - [ ] Update [`ThirdPartyNotices-Distribution.txt`](https://github.com/Microsoft/vscode-python/blob/main/ThirdPartyNotices-Distribution.txt) by using https://tools.opensource.microsoft.com/notice (Notes for this process are in the Team OneNote under Python VS Code -> Dev Process -> Third-Party Notices / TPN file) + - [ ] Update [`ThirdPartyNotices-Repository.txt`](https://github.com/Microsoft/vscode-python/blob/main/ThirdPartyNotices-Repository.txt) as appropriate. This file is manually edited so you can check with the teams if anything needs to be added here. + - [ ] Create a pull request against `main` (🤖) + - [ ] Merge pull request into `main` +- [ ] Update the [`release` branch](https://github.com/microsoft/vscode-python/branches) + - [ ] If there are `release` branches that are two versions old (e.g. release-2020.[current month - 2]) you can delete them at this time + - [ ] Create a new `release-YYYY.MM` branch from `main` +- [ ] Update `main` post-release (🤖) + - [ ] Bump the version number to the next monthly ("YYYY.MM.0-dev") release in the `main` branch + - [ ] `package.json` + - [ ] `package-lock.json` + - [ ] Create a pull request against `main` + - [ ] Merge pull request into `main` +- [ ] Announce the code freeze is over on the same channels +- [ ] Update [Component Governance](https://dev.azure.com/ms/vscode-python/_componentGovernance) (Click on "microsoft/vscode-python" on that page). Notes are in the OneNote under Python VS Code -> Dev Process -> Component Governance. + - [ ] Provide details for any automatically detected npm dependencies + - [ ] Manually add any repository dependencies +- [ ] GDPR bookkeeping (@brettcannon) (🤖; Notes in OneNote under Python VS Code -> Dev Process -> GDPR) +- [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython) + - new features + - settings changes + - etc. (ask the team) +- [ ] Schedule a bug bash. Aim for close after freeze so there is still time to fix release bugs before release. Ask teams before bash for specific areas that need testing. +- [ ] Begin drafting a [blog](http://aka.ms/pythonblog) post. Contact the PM team for this. +- [ ] Ask CTI to test the release candidate + +# Final (Monday, XXX XX) + +## Preparation + +- [ ] Make sure the [appropriate pull requests](https://github.com/microsoft/vscode-docs/pulls) for the [documentation](https://code.visualstudio.com/docs/python/python-tutorial) -- including the [WOW](https://code.visualstudio.com/docs/languages/python) page -- are ready +- [ ] Final updates to the `release-YYYY.MM` branch + - [ ] Create a branch against `release-YYYY.MM` for a pull request + - [ ] Update the version in [`package.json`](https://github.com/Microsoft/vscode-python/blob/main/package.json) to remove the `-rc` (🤖) + - [ ] Run `npm install` to make sure [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/main/package.json) is up-to-date (the only update should be the version number if `package-lock.json` has been kept up-to-date) (🤖) + - [ ] Update [`CHANGELOG.md`](https://github.com/Microsoft/vscode-python/blob/main/CHANGELOG.md) (🤖) + - [ ] Update version and date for the release section + - [ ] Run [`news`](https://github.com/Microsoft/vscode-python/tree/main/news) and copy-and-paste new entries (typically `python news --final | code-insiders -`; quite possibly nothing new to add) + - [ ] Update [`ThirdPartyNotices-Distribution.txt`](https://github.com/Microsoft/vscode-python/blob/main/ThirdPartyNotices-Distribution.txt) by using https://tools.opensource.microsoft.com/notice (🤖; see team notes) + - [ ] Update [`ThirdPartyNotices-Repository.txt`](https://github.com/Microsoft/vscode-python/blob/main/ThirdPartyNotices-Repository.txt) manually if necessary + - [ ] Create pull request against `release-YYYY.MM` (🤖) + - [ ] Merge pull request into `release-YYYY.MM` +- [ ] Make sure component governance is happy + +## Release + +- [ ] Publish the release via Azure DevOps + - [ ] Make sure [CI](https://github.com/Microsoft/vscode-python/blob/main/CONTRIBUTING.md) is passing. Try a re-run on any failing CI test stages. If tests still won't pass contact the owning team. + - [ ] On Azure DevOps on the page for the CI run after it succeeds there will now be a "Releases" tab which is populated with a release entry. Click that entry to go to the release page, which shows the "Upload" and "Publish" stages + - [ ] Click the deploy button on the "Upload" stage and make sure that it succeeds + - [ ] Make sure no extraneous files are being included in the `.vsix` file (make sure to check for hidden files) + - [ ] Click the deploy button on the "Publish" stage, this will push out the release to the public + - [ ] From a VSCode instance uninstall the python extension. After the publish see if the new version is available from the extensions tab. Download it and quick sanity check to make sure the extension loads. +- [ ] Create a [GitHub release](https://github.com/microsoft/vscode-python/releases) + - [ ] The previous publish step should have created a release here, but it needs to be edited + - [ ] Edit the tag to match the version of the released extension + - [ ] Copy the changelog entry into the release as the description +- [ ] Publish [documentation changes](https://github.com/Microsoft/vscode-docs/pulls?q=is%3Apr+is%3Aopen+label%3Apython) +- [ ] Publish the [blog](http://aka.ms/pythonblog) post +- [ ] Determine if a hotfix is needed +- [ ] Merge `release-YYYY.MM` back into `main`. Don't overwrite the `-dev` version in package.json. (🤖) + +## Clean up after _this_ release + +- [ ] Go through [`info needed` issues](https://github.com/Microsoft/vscode-python/issues?q=is%3Aopen+label%3A%22info+needed%22+-label%3A%22data+science%22+sort%3Aupdated-asc) and close any that have no activity for over a month (🤖) +- [ ] GDPR bookkeeping (🤖) + +## Prep for the _next_ release + +- [ ] Create a new [release plan](https://raw.githubusercontent.com/microsoft/vscode-python/main/.github/release_plan.md) (🤖) +- [ ] [(Un-)pin](https://help.github.com/en/articles/pinning-an-issue-to-your-repository) [release plan issues](https://github.com/Microsoft/vscode-python/labels/release%20plan) (🤖) diff --git a/.github/test_plan.md b/.github/test_plan.md new file mode 100644 index 000000000000..fe7e2ee9d424 --- /dev/null +++ b/.github/test_plan.md @@ -0,0 +1,630 @@ +# Test plan + +## Environment + +- OS: XXX (Windows, macOS, latest Ubuntu LTS) + - Shell: XXX (Command Prompt, PowerShell, bash, fish) +- Python + - Distribution: XXX (CPython, miniconda) + - Version: XXX (2.7, latest 3.x) +- VS Code: XXX (Insiders) + +## Tests + +**ALWAYS**: + +- Check the `Output` window under `Python` for logged errors +- Have `Developer Tools` open to detect any errors +- Consider running the tests in a multi-folder workspace +- Focus on in-development features (i.e. experimental debugger and language server) + +
+ Scenarios + +### [Environment](https://code.visualstudio.com/docs/python/environments) + +#### Interpreters + +- [ ] Interpreter is [shown in the status bar](https://code.visualstudio.com/docs/python/environments#_choosing-an-environment) +- [ ] An interpreter can be manually specified using the [`Select Interpreter` command](https://code.visualstudio.com/docs/python/environments#_choosing-an-environment) +- [ ] Detected system-installed interpreters +- [ ] Detected an Anaconda installation +- [ ] (Linux/macOS) Detected all interpreters installed w/ [pyenv](https://github.com/pyenv/pyenv) detected +- [ ] [`"python.pythonPath"`](https://code.visualstudio.com/docs/python/environments#_manually-specifying-an-interpreter) triggers an update in the status bar +- [ ] `Run Python File in Terminal` +- [ ] `Run Selection/Line in Python Terminal` + - [ ] Right-click + - [ ] Command + - [ ] `Shift+Enter` + +#### Terminal + +Sample file: + +```python +import requests +request = requests.get("https://drive.google.com/uc?export=download&id=1_9On2-nsBQIw3JiY43sWbrF8EjrqrR4U") +with open("survey2017.zip", "wb") as file: + file.write(request.content) +import zipfile +with zipfile.ZipFile('survey2017.zip') as zip: + zip.extractall('survey2017') +import shutil, os +shutil.move('survey2017/survey_results_public.csv','survey2017.csv') +shutil.rmtree('survey2017') +os.remove('survey2017.zip') +``` + +- [ ] _Shift+Enter_ to send selected code in sample file to terminal works + +#### Virtual environments + +**ALWAYS**: + +- Use the latest version of Anaconda +- Realize that `conda` is slow +- Create an environment with a space in their path somewhere as well as upper and lowercase characters +- Make sure that you do not have `python.pythonPath` specified in your `settings.json` when testing automatic detection +- Do note that the `Select Interpreter` drop-down window scrolls + +- [ ] Detected a single virtual environment at the top-level of the workspace folder on Mac when when `python` command points to default Mac Python installation or `python` command fails in the terminal. + - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) +- [ ] Detected a single virtual environment at the top-level of the workspace folder on Windows when `python` fails in the terminal. + - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) +- [ ] Detected a single virtual environment at the top-level of the workspace folder + - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) + - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works + - [ ] Steals focus + - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal + - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart +- [ ] Detect multiple virtual environments contained in the directory specified in `"python.venvPath"` +- [ ] Detected all [conda environments created with an interpreter](https://code.visualstudio.com/docs/python/environments#_conda-environments) + - [ ] Appropriate suffix label specified in status bar (e.g. `(condaenv)`) + - [ ] Prompted to install Pylint + - [ ] Asked whether to install using conda or pip + - [ ] Installs into environment + - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works + - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal + - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart +- [ ] (Linux/macOS until [`-m` is supported](https://github.com/Microsoft/vscode-python/issues/978)) Detected the virtual environment created by [pipenv](https://docs.pipenv.org/) + - [ ] Appropriate suffix label specified in status bar (e.g. `(pipenv)`) + - [ ] Prompt to install Pylint uses `pipenv install --dev` + - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works + - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal + - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart +- [ ] (Linux/macOS) Virtual environments created under `{workspaceFolder}/.direnv/python-{python_version}` are detected (for [direnv](https://direnv.net/) and its [`layout python3`](https://github.com/direnv/direnv/blob/master/stdlib.sh) support) + - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) + +#### [Environment files](https://code.visualstudio.com/docs/python/environments#_environment-variable-definitions-file) + +Sample files: + +```python +# example.py +import os +print('Hello,', os.environ.get('WHO'), '!') +``` + +``` +# .env +WHO=world +PYTHONPATH=some/path/somewhere +SPAM='hello ${WHO}' +``` + +**ALWAYS**: + +- Make sure to use `Reload Window` between tests to reset your environment +- Note that environment files only apply under the debugger and Jedi + +- [ ] Environment variables in a `.env` file are exposed when running under the debugger +- [ ] `"python.envFile"` allows for specifying an environment file manually (e.g. Jedi picks up `PYTHONPATH` changes) +- [ ] `envFile` in a `launch.json` configuration works +- [ ] simple variable substitution works + +#### [Debugging](https://code.visualstudio.com/docs/python/environments#_python-interpreter-for-debugging) + +- [ ] `pythonPath` setting in your `launch.json` overrides your `python.pythonPath` default setting + +### [Linting](https://code.visualstudio.com/docs/python/linting) + +**ALWAYS**: + +- Check under the `Problems` tab to see e.g. if a linter is raising errors + +#### Language server + +- [ ] LS is downloaded using HTTP (no SSL) when the "http.proxyStrictSSL" setting is false +- [ ] An item with a cloud icon appears in the status bar indicating progress while downloading the language server +- [ ] Installing [`requests`](https://pypi.org/project/requests/) in virtual environment is detected + - [ ] Import of `requests` without package installed is flagged as unresolved + - [ ] Create a virtual environment + - [ ] Install `requests` into the virtual environment + +#### Pylint/default linting + +[Prompting to install Pylint is covered under `Environments` above] + +For testing the disablement of the default linting rules for Pylint: + +```ini +# pylintrc +[MESSAGES CONTROL] +enable=bad-names +``` + +```python3 +# example.py +foo = 42 # Marked as a disallowed name. +``` + +- [ ] Installation via the prompt installs Pylint as appropriate + - [ ] Uses `--user` for system-install of Python + - [ ] Installs into a virtual environment environment directly +- [ ] Pylint works +- [ ] `"python.linting.pylintUseMinimalCheckers": false` turns off the default rules w/ no `pylintrc` file present +- [ ] The existence of a `pylintrc` file turns off the default rules + +#### Other linters + +**Note**: + +- You can use the `Run Linting` command to run a newly installed linter +- When the extension installs a new linter, it turns off all other linters + +- [ ] flake8 works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] mypy works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] pycodestyle works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] prospector works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] pydocstyle works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] pylama works + - [ ] `Select linter` lists the linter and installs it if necessary +- [ ] 3 or more linters work simultaneously (make sure you have turned on the linters in your `settings.json`) + - [ ] `Run Linting` runs all activated linters + - [ ] `"python.linting.enabled": false` disables all linters + - [ ] The `Enable Linting` command changes `"python.linting.enabled"` +- [ ] `"python.linting.lintOnSave` works + +### [Editing](https://code.visualstudio.com/docs/python/editing) + +#### [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense) + +Please also test for general accuracy on the most "interesting" code you can find. + +- [ ] `"python.autoComplete.extraPaths"` works +- [ ] `"python.autocomplete.addBrackets": true` causes auto-completion of functions to append `()` +- [ ] Auto-completions works + +#### [Formatting](https://code.visualstudio.com/docs/python/editing#_formatting) + +Sample file: + +```python +# There should be _some_ change after running `Format Document`. +import os,sys; +def foo():pass +``` + +- [ ] Prompted to install a formatter if none installed and `Format Document` is run + - [ ] Installing `autopep8` works + - [ ] Installing `black` works + - [ ] Installing `yapf` works +- [ ] Formatters work with default settings (i.e. `"python.formatting.provider"` is specified but not matching `*Path`or `*Args` settings) + - [ ] autopep8 + - [ ] black + - [ ] yapf +- [ ] Formatters work when appropriate `*Path` and `*Args` settings are specified (use absolute paths; use `~` if possible) + - [ ] autopep8 + - [ ] black + - [ ] yapf +- [ ] `"editor.formatOnType": true` works and has expected results + +#### [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring) + +- [ ] [`Extract Variable`](https://code.visualstudio.com/docs/python/editing#_extract-variable) works + - [ ] You are prompted to install `rope` if it is not already available +- [ ] [`Extract method`](https://code.visualstudio.com/docs/python/editing#_extract-method) works + - [ ] You are prompted to install `rope` if it is not already available +- [ ] [`Sort Imports`](https://code.visualstudio.com/docs/python/editing#_sort-imports) works + +### [Debugging](https://code.visualstudio.com/docs/python/debugging) + +- [ ] [Configurations](https://code.visualstudio.com/docs/python/debugging#_debugging-specific-app-types) work (see [`package.json`](https://github.com/Microsoft/vscode-python/blob/main/package.json) and the `"configurationSnippets"` section for all of the possible configurations) +- [ ] Running code from start to finish w/ no special debugging options (e.g. no breakpoints) +- [ ] Breakpoint-like things + - [ ] Breakpoint + - [ ] Set + - [ ] Hit + - [ ] Conditional breakpoint + - [ ] Expression + - [ ] Set + - [ ] Hit + - [ ] Hit count + - [ ] Set + - [ ] Hit + - [ ] Logpoint + - [ ] Set + - [ ] Hit +- [ ] Stepping + - [ ] Over + - [ ] Into + - [ ] Out +- [ ] Can inspect variables + - [ ] Through hovering over variable in code + - [ ] `Variables` section of debugger sidebar +- [ ] [Remote debugging](https://code.visualstudio.com/docs/python/debugging#_remote-debugging) works + - [ ] ... over SSH + - [ ] ... on other branches +- [ ] [App Engine](https://code.visualstudio.com/docs/python/debugging#_google-app-engine-debugging) + +### [Unit testing](https://code.visualstudio.com/docs/python/unit-testing) + +#### [`unittest`](https://code.visualstudio.com/docs/python/unit-testing#_unittest-configuration-settings) + +```python +import unittest + +MODULE_SETUP = False + + +def setUpModule(): + global MODULE_SETUP + MODULE_SETUP = True + + +class PassingSetupTests(unittest.TestCase): + CLASS_SETUP = False + METHOD_SETUP = False + + @classmethod + def setUpClass(cls): + cls.CLASS_SETUP = True + + def setUp(self): + self.METHOD_SETUP = True + + def test_setup(self): + self.assertTrue(MODULE_SETUP) + self.assertTrue(self.CLASS_SETUP) + self.assertTrue(self.METHOD_SETUP) + + +class PassingTests(unittest.TestCase): + + def test_passing(self): + self.assertEqual(42, 42) + + def test_passing_still(self): + self.assertEqual("silly walk", "silly walk") + + +class FailingTests(unittest.TestCase): + + def test_failure(self): + self.assertEqual(42, -13) + + def test_failure_still(self): + self.assertEqual("I'm right!", "no, I am!") +``` + +- [ ] `Run All Unit Tests` triggers the prompt to configure the test runner +- [ ] Tests are discovered (as shown by code lenses on each test) + - [ ] Code lens for a class runs all tests for that class + - [ ] Code lens for a method runs just that test + - [ ] `Run Test` works + - [ ] `Debug Test` works + - [ ] Module/suite setup methods are also run (run the `test_setup` method to verify) +- [ ] while debugging tests, an uncaught exception in a test does not + cause `debugpy` to raise `SystemExit` exception. + +#### [`pytest`](https://code.visualstudio.com/docs/python/unit-testing#_pytest-configuration-settings) + +```python +def test_passing(): + assert 42 == 42 + +def test_failure(): + assert 42 == -13 +``` + +- [ ] `Run All Unit Tests` triggers the prompt to configure the test runner + - [ ] `pytest` gets installed +- [ ] Tests are discovered (as shown by code lenses on each test) + - [ ] `Run Test` works + - [ ] `Debug Test` works +- [ ] A `Diagnostic` is shown in the problems pane for each failed/skipped test + - [ ] The `Diagnostic`s are organized according to the file the test was executed from (not necessarily the file it was defined in) + - [ ] The appropriate `DiagnosticRelatedInformation` is shown for each `Diagnostic` + - [ ] The `DiagnosticRelatedInformation` reflects the traceback for the test + +#### [`nose`](https://code.visualstudio.com/docs/python/unit-testing#_nose-configuration-settings) + +```python +def test_passing(): + assert 42 == 42 + +def test_failure(): + assert 42 == -13 +``` + +- [ ] `Run All Unit Tests` triggers the prompt to configure the test runner + - [ ] Nose gets installed +- [ ] Tests are discovered (as shown by code lenses on each test) + - [ ] `Run Test` works + - [ ] `Debug Test` works + +#### General + +- [ ] Code lenses appears + - [ ] `Run Test` lens works (and status bar updates as appropriate) + - [ ] `Debug Test` lens works + - [ ] Appropriate ✔/❌ shown for each test +- [ ] Status bar is functioning + - [ ] Appropriate test results displayed + - [ ] `Run All Unit Tests` works + - [ ] `Discover Unit Tests` works (resets tests result display in status bar) + - [ ] `Run Unit Test Method ...` works + - [ ] `View Unit Test Output` works + - [ ] After having at least one failure, `Run Failed Tests` works +- [ ] `Configure Unit Tests` works + - [ ] quick pick for framework (and its settings) + - [ ] selected framework enabled in workspace settings + - [ ] framework's config added (and old config removed) + - [ ] other frameworks disabled in workspace settings +- [ ] `Configure Unit Tests` does not close if it loses focus +- [ ] Cancelling configuration does not leave incomplete settings +- [ ] The first `"request": "test"` entry in launch.json is used for running unit tests + +### [Data Science](https://code.visualstudio.com/docs/python/jupyter-support) + +#### P0 Test Scenarios + +- [ ] Start and connect to local Jupyter server + 1. Open the file src/test/datascience/manualTestFiles/manualTestFile.py in VSCode + 1. At the top of the file it will list the things that you need installed in your Python environment + 1. On the first cell click `Run Below` + 1. Interactive Window should open, show connection information, and execute cells + 1. The first thing in the window should have a line like this: `Jupyter Server URI: http://localhost:[port number]/?token=[token value]` +- [ ] Verify Basic Notebook Editor + 1. Create a new file in VS code with the extension .ipynb + 1. Open the file + 1. The Notebook Editor should open + 1. Verify that there is a single cell in the notebook editor + 1. Add `print('bar')` to that cell + 1. Run the cell + 1. Verify that `bar` shows up below the input + 1. Add a cell with the topmost hover bar + 1. Verify the cell appears above all others + 1. Add a cell at the bottom with the bottom most hover bar + 1. Verify the cell appears below all cells + 1. Select a cell + 1. Add a cell with the plus button on the cell + 1. Verify cell appears below + 1. Repeat with the topmost toolbar +- [ ] Verify basic outputs + 1. Run all the cells in manualTestFile.py + 1. Check to make sure that no outputs have errors + 1. Verify that graphs and progress bars are shown +- [ ] Verify export / import + 1. With the results from `Start and connect to local server` open click the `Export as Jupyter Notebook` button in the Interactive Window + 1. Choose a file location and save the generated .ipynb file + 1. When the prompt comes up in the lower right choose to open the file in the browser + 1. The file should open in the web browser and contain the output from the Interactive Window + 1. Try the same steps and choose to open the file in the ipynb editor. + 1. The file should open in the Notebook Editor. +- [ ] Verify text entry + 1. In the Interactive Window type in some new code `print('testing')` and submit it to the Interactive Windows + 1. Verify the output from what you added +- [ ] Verify dark and light main themes + 1. Repeat the `Start and connect to local server` and `Verify basic outputs` steps using `Default Dark+` and `Default Light+` themes +- [ ] Verify Variable Explorer + 1. After manualTestFile.py has been run drop down the Variables section at the top of the Interactive Window + 1. In the Variables list there should be an entry for all variables created. These variables might change as more is added to manualTestFile.py. + 1. Check that variables have expected values. They will be truncated for longer items + 1. Sort the list ascending and descending by Type. Also sort the list ascending and descending by Count. Values like (X, Y) use the first X value for Count sort ordering + 1. Check that list, Series, ndarray, and DataFrame types have a button to "Show variable in data viewer" on the right + 1. In the Interactive Window input box add a new variable. Verify that it is added into the Variable Explorer + 1. Export the contents of the interactive window as a notebook and open the notebook editor + 1. Find the first cell and click on the Run Below button + 1. Open the variable explorer and verify the same variables are there + 1. Add a new cell with a variable in it. + 1. Run the cell and verify the variable shows up in the variable explorer +- [ ] Verify Data Explorer + 1. From the listed types in the Variable explorer open up the Data Viewer by clicking the button or double clicking the row + 1. Inspect the data in the Data Viewer for the expected values + [ ] Verify Sorting and Filtering + 1. Open up the myDataFrame item + 1. Sort the name column ascending and descending + 1. Sort one of the numerical columns ascending and descending + 1. Click the Filter Rows button + 1. In the name filter box input 'a' to filter to just name with an a in them + 1. In one of the numerical columns input a number 1 - 9 to filter to just that column + 1. Open the myList variable in the explorer + 1. Make sure that you can scroll all the way to the end of the entries + [ ] Verify notebook outputs + 1. Open the src/test/datascience/manualTestFiles/manualTestFile.py in VSCode. + 1. Run all of the cells in the file. + 1. Interactive Window should open + 1. Export the cells in the interactive window and open the notebook editor + 1. Run all the cells in the notebook editor and verify the same outputs appear as in the interactive window +- [ ] Verify Notebook Editor Intellisense + 1. Open the src/test/datascience/manualTestFiles/manualTestFile.py in VSCode. + 1. Run all of the cells in the file. + 1. Interactive Window should open + 1. Export the cells in the interactive window and open the notebook editor + 1. Hover over each cell in the notebook editor and verify you get hover intellisense + 1. Add a new cell in between cells + 1. Add `import sys` and `sys.executable` to the cell + 1. Move the cell around and verify intellisense hover still works on the `import sys` + 1. Delete and readd the cell and verify intellisense hover still works. +- [ ] Verify Notebook Keyboard Shortcuts + 1. Using the notebook generated from the manualTestFile.py, do the following + 1. Select a cell by clicking on it + 1. Move selection up and down with j,k and arrow keys. + 1. Focus a cell by double clicking on it or hitting the enter key when selected + 1. Move selection through the code with the arrow keys. + 1. Verify selection travels between focused cells + 1. Hit escape when a cell has focus and verify it becomes selected instead of focused + 1. Hit `y` on a markdown cell when not focused and see that it becomes a code cell + 1. Hit `m` on a code cell when not focused and see that it becomes a markdown cell + 1. Hit `l` on a code cell and verify line numbers appear + 1. Hit `o` on a code cell with output and verify that outputs disappear + 1. Hit `d` + `d` and verify a cell is deleted. + 1. Hit `z` and verify a deleted cell reappears + 1. Hit `a` and verify the selected cell moves up + 1. Hit `b` and verify the selected cell moves down + 1. Hit `shift+enter` and verify a cell runs and selection moves to the next cell + 1. Hit `alt+enter` and verify a cell runs and a new cell is added below + 1. Hit `ctrl+enter` and verify a cell runs and selection does not change +- [ ] Verify debugging + 1. Open the file src/test/datascience/manualTestFiles/manualTestFile.py in VSCode + 1. On the first cell click `Run Below` + 1. Interactive Window should open, show connection information, and execute cells + 1. Go back to the first cell and click `Debug Cell` + 1. Debugger should start and have an ip indicator on the first line of the cell + 1. Step through the debugger. + 1. Verify the variables tab of the debugger shows variables. + 1. Verify the variables explorer window shows output not available while debugging + 1. When you get to the end of the cell, the debugger should stop + 1. Output from the cell should show up in the Interactive Window (sometimes you have to finish debugging the cell first) + +#### P1 Test Scenarios + +- [ ] Connect to a `remote` server + 1. Open up a valid python command prompt that can run `jupyter notebook` (a default Anaconda prompt works well) + 1. Run `jupyter notebook` to start up a local Jupyter server + 1. In the command window that launched Jupyter look for the server / token name like so: http://localhost:8888/?token=bf9eae43641cd75015df9104f814b8763ef0e23ffc73720d + 1. Run the command `Python: Select Jupyter server URI` then `Type in the URI to connect to a running jupyter server` + 1. Input the server / token name here + 1. Now run the cells in the manualTestFile.py + 1. Verify that you see the server name in the initial connection message + 1. Verify the outputs of the cells +- [ ] Interactive Window commands + - [ ] Verify per-cell commands + 1. Expand and collapse the input area of a cell + 1. Use the `X` button to remove a cell + 1. Use the `Goto Code` button to jump to the part of the .py file that submitted the code + - [ ] Verify top menu commands + 1. Use `X` to delete all cells + 1. Undo the delete action with `Undo` + 1. Redo the delete action with `Redo` + 1. In manualTestFile.py modify the trange command in the progress bar from 100 to 2000. Run the Cell. As the cell is running hit the `Interrupt iPython Kernel` button + 1. The progress bar should be interrupted and you should see a KeyboardInterrupt error message in the output + 1. Test the `Restart iPython kernel` command. Kernel should be restarted and you should see a status output message for the kernel restart + 1. Use the expand all input and collapse all input commands to collapse all cell inputs +- [ ] Verify theming works + 1. Start Python Interactive window + 1. Add a cell with some comments + 1. Switch VS Code theme to something else + 1. Check that the cell you just added updates the comment color + 1. Switch back and forth between a 'light' and a 'dark' theme + 1. Check that the cell switches colors + 1. Check that the buttons on the top change to their appropriate 'light' or 'dark' versions + 1. Enable the 'ignoreVscodeTheme' setting + 1. Close the Python Interactive window and reopen it. The theme in just the 'Python Interactive' window should be light + 1. Switch to a dark theme. Make sure the interactive window remains in the light theme. +- [ ] Verify code lenses + 1. Check that `Run Cell` `Run Above` and `Run Below` all do the correct thing +- [ ] Verify context menu navigation commands + 1. Check the `Run Current Cell` and `Run Current Cell And Advance` context menu commands + 1. If run on the last cell of the file `Run Current Cell And Advance` should create a new empty cell and advance to it +- [ ] Verify command palette commands + 1. Close the Interactive Window then pick `Python: Show Interactive Window` + 1. Restart the kernel and pick `Python: Run Current File In Python Interactive Window` it should run the whole file again +- [ ] Verify shift-enter + 1. Move to the top cell in the .py file + 1. Shift-enter should run each cell and advance to the next + 1. Shift-enter on the final cell should create a new cell and move to it +- [ ] Verify file without cells + 1. Open the manualTestFileNoCells.py file + 1. Select a chunk of code, shift-enter should send it to the terminal + 1. Open VSCode settings, change `Send Selection To Interactive Window` to true + 1. Select a chunk of code, shift-enter should send that selection to the Interactive Windows + 1. Move your cursor to a line, but don't select anything. Shift-enter should send that line to the Interactive Window +- [ ] Multiple installs + 1. Close and re-open VSCode to make sure that all jupyter servers are closed + 1. Also make sure you are set to locally launch Jupyter and not to connect to an existing URI + 1. In addition to your main testing environment install a new python or miniconda install (conda won't work as it has Jupyter by default) + 1. In VS code change the python interpreter to the new install + 1. Try `Run Cell` + 1. You should get a message that Jupyter was not found and that it is defaulting back to launch on the python instance that has Jupyter +- [ ] LiveShare Support + 1. Install the LiveShare VSCode Extension + 1. Open manualTestFile.py in VSCode + 1. Run the first cell in the file + 1. Switch to the `Live Share` tab in VS Code and start a session + - [ ] Verify server start + 1. Jupyter server instance should appear in the live share tab + 1. Open another window of VSCode + 1. Connect the second instance of VSCode as a Guest to the first Live Share session + 1. After the workspace opens, open the manualTestFile.py on the Guest instance + 1. On the Guest instance run a cell from the file, both via the codelens and via the command palette `Run Cell` command + - [ ] Verify results + 1. Output should show up on the Guest Interactive Window + 1. Same output should show up in the Host Interactive Window + 1. On the Host instance run a cell from the file, both via the codelens and via the command palette + - [ ] Verify results + 1. Output should show up on the Guest Interactive Window + 1. Same output should show up in the Host Interactive Window + 1. Export the file to a notebook + 1. Open the notebook editor on the host + 1. Run a cell on the host + 1. Verify the editor opens on the guest and the cell is run there too +- [ ] Jupyter Hub support + + 1. Windows install instructions + + 1. Install Docker Desktop onto a machine + 1. Create a folder with a file 'Dockerfile' in it. + 1. Mark the file to look like so: + + ``` + ARG BASE_CONTAINER=jupyterhub/jupyterhub + FROM $BASE_CONTAINER + + USER root + + USER $NB_UID + ``` + + 1. From a command prompt (in the same folder as the Dockerfile), run `docker build -t jupyterhubcontainer:1.0 .` + 1. Run `docker container create --name jupyterhub jupyterhubcontainer:1.0 jupyterhub` + 1. From the docker desktop app, start the jupyterhub container. + 1. From the docker desktop app, run the CLI + + 1. OR Mac / Linux install instructions + 1. Install docker + 1. From the terminal `docker run -p 8000:8000 -d --name jupyterhub jupyterhub/jupyterhub jupyterhub` + 1. Open a terminal in the docker container with `docker exec -it jupyterhub bash` + 1. From that terminal run `python3 -m pip install notebook` + 1. From the new command prompt, run `adduser testuser` + 1. Follow the series of prompts to add a password for this user + 1. Open VS code + 1. Open a folder with a python file in it. + 1. Run the `Python: Specify local or remote Jupyter server for connections` command. + 1. Pick 'Existing' + 1. Enter `http://localhost:8000` (assuming the jupyter hub container was successful in launching) + 1. Reload VS code and reopen this folder. + 1. Run a cell in a python file. + [ ] Verify results 1. Verify you are asked first for a user name and then a password. 1. Verify a cell runs once you enter the user name and password 1. Verify that the python that is running in the interactive window is from the docker container (if on windows it should show a linux path) + +#### P2 Test Scenarios + +- [ ] Directory change + - [ ] Verify directory change in export + 1. Follow the previous steps for export, but export the ipynb to a directory outside of the current workspace + 1. Open the file in the browser, you should get an initial cell added to change directory back to your workspace directory + - [ ] Verify directory change in import + 1. Follow the previous steps for import, but import an ipynb that is located outside of your current workspace + 1. Open the file in the editor. There should be python code at the start to change directory to the previous location of the .ipynb file +- [ ] Interactive Window input history history + 1. Start up an Interactive Window session + 1. Input several lines into the Interactive Window terminal + 1. Press up to verify that those previously entered lines show in the Interactive Window terminal history +- [ ] Extra themes 1. Try several of the themes that come with VSCode that are not the default Dark+ and Light+ +
diff --git a/.github/workflows/insiders.yml b/.github/workflows/insiders.yml new file mode 100644 index 000000000000..3f21070166c5 --- /dev/null +++ b/.github/workflows/insiders.yml @@ -0,0 +1,492 @@ +name: Insiders Build + +on: + push: + branches: + - main + +env: + PYTHON_VERSION: 3.8 + MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). Also enables a reporter which exits the process running the tests if it haven't already. + CACHE_NPM_DEPS: cache-npm + CACHE_OUT_DIRECTORY: cache-out-directory + CACHE_PIP_DEPS: cache-pip + # Key for the cache created at the end of the the 'Cache ./pythonFiles/lib/python' step. + CACHE_PYTHONFILES: cache-pvsc-pythonFiles + ARTIFACT_NAME_VSIX: ms-python-insiders-vsix + VSIX_NAME: ms-python-insiders.vsix + COVERAGE_REPORTS: tests-coverage-reports + TEST_RESULTS_DIRECTORY: . + +jobs: + build-vsix: + name: Build VSIX + runs-on: ubuntu-latest + if: github.repository == 'microsoft/vscode-python' + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache pip files + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{runner.os}}-${{env.CACHE_PIP_DEPS}}-${{env.PYTHON_VERSION}}-${{hashFiles('requirements.txt')}}-${{hashFiles('build/debugger-install-requirements.txt')}} + + - name: Cache npm files + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} + + - name: Use Python ${{env.PYTHON_VERSION}} + uses: actions/setup-python@v2 + with: + python-version: ${{env.PYTHON_VERSION}} + + - name: Upgrade pip + run: python -m pip install -U pip + + - name: Build VSIX + uses: ./.github/actions/build-vsix + id: build-vsix + + - name: Rename VSIX + if: steps.build-vsix.outputs.path != env.VSIX_NAME + run: mv ${{ steps.build-vsix.outputs.path }} ${{ env.VSIX_NAME }} + + - uses: actions/upload-artifact@v2 + with: + name: ${{env.ARTIFACT_NAME_VSIX}} + path: ${{env.VSIX_NAME}} + + lint: + name: Lint + runs-on: ubuntu-latest + if: github.repository == 'microsoft/vscode-python' + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache pip files + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{runner.os}}-${{env.CACHE_PIP_DEPS}}-${{env.PYTHON_VERSION}}-${{hashFiles('requirements.txt')}}-${{hashFiles('build/debugger-install-requirements.txt')}} + + - name: Cache npm files + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} + + - name: Install dependencies (npm ci) + run: npm ci --prefer-offline + + - name: Run gulp prePublishNonBundle + run: npx gulp prePublishNonBundle + + - name: Cache the out/ directory + uses: actions/cache@v2 + with: + path: ./out + key: ${{runner.os}}-${{env.CACHE_OUT_DIRECTORY}}-${{hashFiles('src/**')}} + + - name: Check dependencies + run: npm run checkDependencies + + - name: Run linting on TypeScript code + run: npx tslint --project tsconfig.json + + - name: Run prettier on TypeScript code + run: npx prettier 'src/**/*.ts*' --check + + - name: Run prettier on JavaScript code + run: npx prettier 'build/**/*.js' --check + + - name: Use Python ${{env.PYTHON_VERSION}} + uses: actions/setup-python@v2 + with: + python-version: ${{env.PYTHON_VERSION}} + + - name: Run Black on Python code + run: | + python -m pip install -U black + python -m black . --check + working-directory: pythonFiles + + ### Non-smoke tests + tests: + name: Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + if: github.repository == 'microsoft/vscode-python' + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the UNIX case. + os: [ubuntu-latest, windows-latest] + # Run the tests on the oldest and most recent versions of Python. + python: [2.7, 3.8] + test-suite: [ts-unit, python-unit, venv, single-workspace, multi-workspace, debugger, functional] + env: + # Something in Node 12.16.0 breaks the TS debug adapter, and ubuntu-latest bundles Node 12.16.1. + # We can remove this when we switch over to the python-based DA in https://github.com/microsoft/vscode-python/issues/7136. + # See https://github.com/microsoft/ptvsd/issues/2068 + # At this point pinning is only needed for consistency. We no longer have TS debug adapter. + NODE_VERSION: 12.15.0 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache pip files + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{runner.os}}-${{env.CACHE_PIP_DEPS}}-${{env.PYTHON_VERSION}}-${{hashFiles('requirements.txt')}}-${{hashFiles('build/debugger-install-requirements.txt')}} + + - name: Cache npm files + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} + + - name: Cache compiled TS files + # Use an id for this step so that its cache-hit output can be accessed and checked in the next step. + id: out-cache + uses: actions/cache@v2 + with: + path: ./out + key: ${{runner.os}}-${{env.CACHE_OUT_DIRECTORY}}-${{hashFiles('src/**')}} + + - name: Install dependencies (npm ci) + run: npm ci + + - name: Compile if not cached + run: npx gulp prePublishNonBundle + if: steps.out-cache.outputs.cache-hit == false + + - name: Use Python ${{matrix.python}} + uses: actions/setup-python@v2 + with: + python-version: ${{matrix.python}} + + - name: Use Node ${{env.NODE_VERSION}} + uses: actions/setup-node@v2.1.1 + with: + node-version: ${{env.NODE_VERSION}} + + - name: Install Python requirements + run: | + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt + # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: pip install ipython requirements + run: | + python -m pip install numpy + python -m pip install --upgrade -r ./build/ipython-test-requirements.txt + if: matrix.test-suite == 'python-unit' + + - name: Install debugpy wheels (python 3.8) + run: | + python -m pip install wheel + python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python ./pythonFiles/install_debugpy.py + shell: bash + if: matrix.test-suite == 'debugger' && matrix.python == 3.8 + + - name: Install debugpy (python 2.7) + run: | + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + shell: bash + if: matrix.test-suite == 'debugger' && matrix.python == 2.7 + + - name: Install functional test requirements + run: | + python -m pip install numpy + python -m pip install --upgrade -r ./build/functional-test-requirements.txt + if: matrix.test-suite == 'functional' + + - name: Prepare pipenv for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + python -m pip install pipenv + python -m pipenv run python ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} pipenvPath + + - name: Prepare virtualenv for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + python -m pip install virtualenv + python -m virtualenv .virtualenv/ + if ('${{matrix.os}}' -match 'windows-latest') { + & ".virtualenv/Scripts/python.exe" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} virtualEnvPath + } else { + & ".virtualenv/bin/python" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} virtualEnvPath + } + + - name: Prepare venv for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' && startsWith(matrix.python, 3.) + run: | + python -m venv .venv + if ('${{matrix.os}}' -match 'windows-latest') { + & ".venv/Scripts/python.exe" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} venvPath + } else { + & ".venv/bin/python" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} venvPath + } + + - name: Prepare conda for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + # 1. For `terminalActivation.testvirtualenvs.test.ts` + if ('${{matrix.os}}' -match 'windows-latest') { + $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python.exe + $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath Scripts | Join-Path -ChildPath conda + } else{ + $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath bin | Join-Path -ChildPath python + $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath bin | Join-Path -ChildPath conda + } + & $condaPythonPath ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} condaExecPath $condaExecPath + & $condaPythonPath ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} condaPath + + # 2. For `interpreterLocatorService.testvirtualenvs.ts` + + & $condaExecPath create -n "test_env1" -y python + & $condaExecPath create -p "./test_env2" -y python + & $condaExecPath create -p "~/test_env3" -y python + + - name: Set CI_PYTHON_PATH and CI_DISABLE_AUTO_SELECTION + run: | + echo "::set-env name=CI_PYTHON_PATH::python" + echo "::set-env name=CI_DISABLE_AUTO_SELECTION::1" + shell: bash + if: matrix.test-suite != 'ts-unit' + + # Run TypeScript unit tests only for Python 3.X. + - name: Run TypeScript unit tests + run: npm run test:unittests:cover + if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, 3.) + + # Upload unit test coverage reports for later use in the "reports" job. + - name: Upload unit test coverage reports + uses: actions/upload-artifact@v1 + with: + name: ${{runner.os}}-${{env.COVERAGE_REPORTS}} + path: .nyc_output + if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, 3.) + + # Run the Python and IPython tests in our codebase. + - name: Run Python and IPython unit tests + run: | + python pythonFiles/tests/run_all.py + python -m IPython pythonFiles/tests/run_all.py + if: matrix.test-suite == 'python-unit' + + # The virtual environment based tests use the `testSingleWorkspace` set of tests + # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, + # which is set in the "Prepare environment for venv tests" step. + # We also use a third-party GitHub Action to install xvfb on Linux, + # run tests and then clean up the process once the tests ran. + # See https://github.com/GabrielBB/xvfb-action + - name: Run venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + CI_PYTHON_VERSION: ${{matrix.python}} + uses: GabrielBB/xvfb-action@v1.4 + with: + run: npm run testSingleWorkspace + if: matrix.test-suite == 'venv' + + - name: Run single-workspace tests + env: + CI_PYTHON_VERSION: ${{matrix.python}} + uses: GabrielBB/xvfb-action@v1.4 + with: + run: npm run testSingleWorkspace + if: matrix.test-suite == 'single-workspace' + + - name: Run multi-workspace tests + env: + CI_PYTHON_VERSION: ${{matrix.python}} + uses: GabrielBB/xvfb-action@v1.4 + with: + run: npm run testMultiWorkspace + if: matrix.test-suite == 'multi-workspace' + + - name: Run debugger tests + env: + CI_PYTHON_VERSION: ${{matrix.python}} + uses: GabrielBB/xvfb-action@v1.4 + with: + run: npm run testDebugger + if: matrix.test-suite == 'debugger' + + - name: Run functional tests + run: npm run test:functional + if: matrix.test-suite == 'functional' + + smoke-tests: + name: Smoke tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + if: github.repository == 'microsoft/vscode-python' + needs: [build-vsix] + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the UNIX case. + os: [ubuntu-latest, windows-latest] + python: [3.8] + steps: + # Need the source to have the tests available. + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache pip files + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{runner.os}}-${{env.CACHE_PIP_DEPS}}-${{env.PYTHON_VERSION}}-${{hashFiles('requirements.txt')}}-${{hashFiles('build/debugger-install-requirements.txt')}} + + - name: Cache npm files + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} + + - name: Use Python ${{matrix.python}} + uses: actions/setup-python@v2 + with: + python-version: ${{matrix.python}} + + - name: Install dependencies (npm ci) + run: npm ci --prefer-offline + + - name: pip install system test requirements + run: | + python -m pip install --upgrade -r build/test-requirements.txt + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + shell: bash + + - name: pip install ipython requirements + run: | + python -m pip install numpy + python -m pip install --upgrade -r ./build/ipython-test-requirements.txt + + - name: pip install jupyter + run: | + python -m pip install --upgrade jupyter + + # Save time by reusing bits from the VSIX. + - name: Download VSIX + uses: actions/download-artifact@v2 + with: + name: ${{env.ARTIFACT_NAME_VSIX}} + + # Compile the test files. + - name: Prepare for smoke tests + run: npx tsc -p ./ + shell: bash + + - name: Set CI_PYTHON_PATH and CI_DISABLE_AUTO_SELECTION + run: | + echo "::set-env name=CI_PYTHON_PATH::python" + echo "::set-env name=CI_DISABLE_AUTO_SELECTION::1" + shell: bash + + - name: Run smoke tests + env: + DISPLAY: 10 + uses: GabrielBB/xvfb-action@v1.4 + with: + run: node --no-force-async-hooks-checks ./out/test/smokeTest.js + + coverage: + name: Coverage reports upload + runs-on: ubuntu-latest + if: github.repository == 'microsoft/vscode-python' + needs: [tests, smoke-tests] + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache npm files + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} + + - name: Install dependencies (npm ci) + run: npm ci --prefer-offline + + # It isn't possible to specify a regex for artifact names, so we have to download each artifact manually. + # The name pattern is ${{runner.os}}-${{env.COVERAGE_REPORTS}}, and possible values for runner.os are `Linux`, `Windows`, or `macOS`. + # See https://help.github.com/en/actions/reference/contexts-and-expression-syntax-for-github-actions#runner-context + - name: Download Ubuntu test coverage artifacts + uses: actions/download-artifact@v1 + with: + name: Linux-${{env.COVERAGE_REPORTS}} + + - name: Extract Ubuntu coverage artifacts to ./nyc_output + run: | + mkdir .nyc_output + mv Linux-${{env.COVERAGE_REPORTS}}/* .nyc_output + rm -r Linux-${{env.COVERAGE_REPORTS}} + + - name: Generate coverage reports + run: npm run test:cover:report + continue-on-error: true + + - name: Upload coverage to codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage/cobertura-coverage.xml + + upload: + name: Upload VSIX to Azure Blob Storage + runs-on: ubuntu-latest + if: github.repository == 'microsoft/vscode-python' + needs: [tests, smoke-tests, build-vsix] + env: + BLOB_CONTAINER_NAME: extension-builds + BLOB_NAME: ms-python-gha-insiders.vsix # So named to avoid clobbering Azure Pipelines upload. + + steps: + - name: Download VSIX + uses: actions/download-artifact@v2 + with: + name: ${{ env.ARTIFACT_NAME_VSIX }} + + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Upload to Blob Storage + run: az storage blob upload --file ${{ env.VSIX_NAME }} --account-name pvsc --container-name ${{ env.BLOB_CONTAINER_NAME }} --name ${{ env.BLOB_NAME }} --auth-mode login + + - name: Get URL to uploaded VSIX + run: az storage blob url --account-name pvsc --container-name ${{ env.BLOB_CONTAINER_NAME }} --name ${{ env.BLOB_NAME }} --auth-mode login diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..71eb74d3213a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,397 @@ +name: Release Build + +on: + push: + branches: + - 'release' + - 'release/*' + - 'release-*' + +env: + PYTHON_VERSION: 3.8 + MOCHA_REPORTER_JUNIT: false # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). Also enables a reporter which exits the process running the tests if it hasn't already. + CACHE_NPM_DEPS: cache-npm + CACHE_OUT_DIRECTORY: cache-out-directory + CACHE_PIP_DEPS: cache-pip + # Key for the cache created at the end of the the 'Cache ./pythonFiles/lib/python' step. + CACHE_PYTHONFILES: cache-pvsc-pythonFiles + ARTIFACT_NAME_VSIX: ms-python-release-vsix + VSIX_NAME: ms-python-release.vsix + COVERAGE_REPORTS: tests-coverage-reports + TEST_RESULTS_DIRECTORY: . + +jobs: + build-vsix: + name: Build VSIX + runs-on: ubuntu-latest + if: github.repository == 'microsoft/vscode-python' + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache pip files + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{runner.os}}-${{env.CACHE_PIP_DEPS}}-${{env.PYTHON_VERSION}}-${{hashFiles('requirements.txt')}}-${{hashFiles('build/debugger-install-requirements.txt')}} + + - name: Cache npm files + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} + + - name: Use Python ${{env.PYTHON_VERSION}} + uses: actions/setup-python@v2 + with: + python-version: ${{env.PYTHON_VERSION}} + + - name: Upgrade pip + run: python -m pip install -U pip + + - name: Build VSIX + uses: ./.github/actions/build-vsix + id: build-vsix + + - name: Rename VSIX + if: steps.build-vsix.outputs.path != env.VSIX_NAME + run: mv ${{ steps.build-vsix.outputs.path }} ${{ env.VSIX_NAME }} + + - uses: actions/upload-artifact@v2 + with: + name: ${{ env.ARTIFACT_NAME_VSIX }} + path: ${{ env.VSIX_NAME }} + + lint: + # Unlike for the insiders build, we skip linting file formatting as that + # will be caught when we merge back into 'main'. + name: Lint + runs-on: ubuntu-latest + if: github.repository == 'microsoft/vscode-python' + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache npm files + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} + + - name: Install dependencies (npm ci) + run: npm ci --prefer-offline + + - name: Run gulp prePublishNonBundle + run: npx gulp prePublishNonBundle + + - name: Cache the out/ directory + uses: actions/cache@v2 + with: + path: ./out + key: ${{runner.os}}-${{env.CACHE_OUT_DIRECTORY}}-${{hashFiles('src/**')}} + + - name: Check dependencies + run: npm run checkDependencies + + - name: Run linting on TypeScript code + run: npx tslint --project tsconfig.json + + ### Non-smoke tests + tests: + name: Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + if: github.repository == 'microsoft/vscode-python' + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the UNIX case. + os: [ubuntu-latest, windows-latest] + # Run the tests on the oldest and most recent versions of Python. + python: [2.7, 3.8] + test-suite: [ts-unit, python-unit, venv, single-workspace, multi-workspace, debugger, functional] + env: + # Something in Node 12.16.0 breaks the TS debug adapter, and ubuntu-latest bundles Node 12.16.1. + # We can remove this when we switch over to the python-based DA in https://github.com/microsoft/vscode-python/issues/7136. + # See https://github.com/microsoft/ptvsd/issues/2068 + # At this point pinning is only needed for consistency. We no longer have TS debug adapter. + NODE_VERSION: 12.15.0 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache pip files + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{runner.os}}-${{env.CACHE_PIP_DEPS}}-${{env.PYTHON_VERSION}}-${{hashFiles('requirements.txt')}}-${{hashFiles('build/debugger-install-requirements.txt')}} + + - name: Cache npm files + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} + + - name: Cache compiled TS files + # Use an id for this step so that its cache-hit output can be accessed and checked in the next step. + id: out-cache + uses: actions/cache@v2 + with: + path: ./out + key: ${{runner.os}}-${{env.CACHE_OUT_DIRECTORY}}-${{hashFiles('src/**')}} + + - name: Install dependencies (npm ci) + run: npm ci + + - name: Compile if not cached + run: npx gulp prePublishNonBundle + if: steps.out-cache.outputs.cache-hit == false + + - name: Use Python ${{matrix.python}} + uses: actions/setup-python@v2 + with: + python-version: ${{matrix.python}} + + - name: Use Node ${{env.NODE_VERSION}} + uses: actions/setup-node@v2.1.1 + with: + node-version: ${{env.NODE_VERSION}} + + - name: Install Python requirements + run: | + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt + # We need to have debugpy so that tests relying on it keep passing, but we don't need install_debugpy's logic in the test phase. + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: pip install ipython requirements + run: | + python -m pip install numpy + python -m pip install --upgrade -r ./build/ipython-test-requirements.txt + if: matrix.test-suite == 'python-unit' + + - name: Install debugpy wheels (python 3.8) + run: | + python -m pip install wheel + python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python ./pythonFiles/install_debugpy.py + shell: bash + if: matrix.test-suite == 'debugger' && matrix.python == 3.8 + + - name: Install debugpy (python 2.7) + run: | + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + shell: bash + if: matrix.test-suite == 'debugger' && matrix.python == 2.7 + + - name: Install functional test requirements + run: | + python -m pip install numpy + python -m pip install --upgrade -r ./build/functional-test-requirements.txt + if: matrix.test-suite == 'functional' + + - name: Prepare pipenv for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + python -m pip install pipenv + python -m pipenv run python ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} pipenvPath + + - name: Prepare virtualenv for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + python -m pip install virtualenv + python -m virtualenv .virtualenv/ + if ('${{matrix.os}}' -match 'windows-latest') { + & ".virtualenv/Scripts/python.exe" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} virtualEnvPath + } else { + & ".virtualenv/bin/python" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} virtualEnvPath + } + + - name: Prepare venv for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' && startsWith(matrix.python, 3.) + run: | + python -m venv .venv + if ('${{matrix.os}}' -match 'windows-latest') { + & ".venv/Scripts/python.exe" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} venvPath + } else { + & ".venv/bin/python" ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} venvPath + } + + - name: Prepare conda for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + # 1. For `terminalActivation.testvirtualenvs.test.ts` + if ('${{matrix.os}}' -match 'windows-latest') { + $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python.exe + $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath Scripts | Join-Path -ChildPath conda + } else{ + $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath bin | Join-Path -ChildPath python + $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath bin | Join-Path -ChildPath conda + } + & $condaPythonPath ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} condaExecPath $condaExecPath + & $condaPythonPath ./build/ci/addEnvPath.py ${{env.PYTHON_VIRTUAL_ENVS_LOCATION}} condaPath + + # 2. For `interpreterLocatorService.testvirtualenvs.ts` + + & $condaExecPath create -n "test_env1" -y python + & $condaExecPath create -p "./test_env2" -y python + & $condaExecPath create -p "~/test_env3" -y python + + - name: Set CI_PYTHON_PATH and CI_DISABLE_AUTO_SELECTION + run: | + echo "::set-env name=CI_PYTHON_PATH::python" + echo "::set-env name=CI_DISABLE_AUTO_SELECTION::1" + shell: bash + if: matrix.test-suite != 'ts-unit' + + # Run TypeScript unit tests only for Python 3.X. + - name: Run TypeScript unit tests + run: npm run test:unittests:cover + if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, 3.) + + # Run the Python and IPython tests in our codebase. + - name: Run Python and IPython unit tests + run: | + python pythonFiles/tests/run_all.py + python -m IPython pythonFiles/tests/run_all.py + if: matrix.test-suite == 'python-unit' + + # The virtual environment based tests use the `testSingleWorkspace` set of tests + # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, + # which is set in the "Prepare environment for venv tests" step. + # We also use a third-party GitHub Action to install xvfb on Linux, + # run tests and then clean up the process once the tests ran. + # See https://github.com/GabrielBB/xvfb-action + - name: Run venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + CI_PYTHON_VERSION: ${{matrix.python}} + uses: GabrielBB/xvfb-action@v1.4 + with: + run: npm run testSingleWorkspace + if: matrix.test-suite == 'venv' + + - name: Run single-workspace tests + env: + CI_PYTHON_VERSION: ${{matrix.python}} + uses: GabrielBB/xvfb-action@v1.4 + with: + run: npm run testSingleWorkspace + if: matrix.test-suite == 'single-workspace' + + - name: Run multi-workspace tests + env: + CI_PYTHON_VERSION: ${{matrix.python}} + uses: GabrielBB/xvfb-action@v1.4 + with: + run: npm run testMultiWorkspace + if: matrix.test-suite == 'multi-workspace' + + - name: Run debugger tests + env: + CI_PYTHON_VERSION: ${{matrix.python}} + uses: GabrielBB/xvfb-action@v1.4 + with: + run: npm run testDebugger + if: matrix.test-suite == 'debugger' + + - name: Run functional tests + run: npm run test:functional + if: matrix.test-suite == 'functional' + + smoke-tests: + name: Smoke tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + if: github.repository == 'microsoft/vscode-python' + needs: [build-vsix] + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the UNIX case. + os: [ubuntu-latest, windows-latest] + python: [3.8] + steps: + # Need the source to have the tests available. + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache pip files + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{runner.os}}-${{env.CACHE_PIP_DEPS}}-${{env.PYTHON_VERSION}}-${{hashFiles('requirements.txt')}}-${{hashFiles('build/debugger-install-requirements.txt')}} + + - name: Cache npm files + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{runner.os}}-${{env.CACHE_NPM_DEPS}}-${{hashFiles('package-lock.json')}} + + - name: Use Python ${{matrix.python}} + uses: actions/setup-python@v2 + with: + python-version: ${{matrix.python}} + + - name: Install dependencies (npm ci) + run: npm ci --prefer-offline + + - name: pip install system test requirements + run: | + python -m pip install --upgrade -r build/test-requirements.txt + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + shell: bash + + - name: pip install ipython requirements + run: | + python -m pip install numpy + python -m pip install --upgrade -r ./build/ipython-test-requirements.txt + + - name: pip install jupyter + run: | + python -m pip install --upgrade jupyter + + # Save time by reusing bits from the VSIX. + - name: Download VSIX + uses: actions/download-artifact@v2 + with: + name: ${{env.ARTIFACT_NAME_VSIX}} + + # Compile the test files. + - name: Prepare for smoke tests + run: npx tsc -p ./ + shell: bash + + - name: Set CI_PYTHON_PATH and CI_DISABLE_AUTO_SELECTION + run: | + echo "::set-env name=CI_PYTHON_PATH::python" + echo "::set-env name=CI_DISABLE_AUTO_SELECTION::1" + shell: bash + + - name: Run smoke tests + env: + DISPLAY: 10 + uses: GabrielBB/xvfb-action@v1.4 + with: + run: node --no-force-async-hooks-checks ./out/test/smokeTest.js diff --git a/.gitignore b/.gitignore index ae5a02bec0eb..a343e9929c81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,43 @@ .DS_Store +.huskyrc.json out -node_modules -*.pyc \ No newline at end of file +log.log +**/node_modules +*.pyc +*.vsix +**/.vscode/.ropeproject/** +**/testFiles/**/.cache/** +*.noseids +.nyc_output +.vscode-test +__pycache__ +npm-debug.log +**/.mypy_cache/** +!yarn.lock +coverage/ +cucumber-report.json +**/.vscode-test/** +**/.vscode test/** +**/.vscode-smoke/** +**/.venv*/ +port.txt +precommit.hook +pythonFiles/lib/** +debug_coverage*/** +languageServer/** +languageServer.*/** +bin/** +obj/** +.pytest_cache +tmp/** +.python-version +.vs/ +test-results*.xml +xunit-test-results.xml +build/ci/performance/performance-results.json +!build/ +debug*.log +debugpy*.log +pydevd*.log +nodeLanguageServer/** +nodeLanguageServer.*/** diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000000..16cc2ccdf1e8 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@types:registry=https://registry.npmjs.org diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000000..764b945d130f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v12.15.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000000..953832c3207c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +# Ignore the pythonEnvironments/ folder because we use ESLint there instead +src/client/pythonEnvironments/* +src/test/pythonEnvironments/* diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000000..e584661edd38 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,21 @@ +module.exports = { + singleQuote: true, + printWidth: 120, + tabWidth: 4, + endOfLine: 'auto', + trailingComma: 'none', + overrides: [ + { + files: ['*.yml', '*.yaml'], + options: { + tabWidth: 2 + } + }, + { + files: ['**/datascience/serviceRegistry.ts'], + options: { + printWidth: 240 + } + } + ] +}; diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 000000000000..cee889d8051b --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,5 @@ +sonar.sources=src/client,src/datascience-ui +sonar.exclusions=src/datascience-ui/**/codicon*.* +sonar.tests=src/test +sonar.cfamily.build-wrapper-output.bypass=true +sonar.cpd.exclusions=src/datascience-ui/**/redux/actions.ts,src/client/**/raw-kernel/rawKernel.ts,src/client/datascience/jupyter/*ariable*.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000000..508eae4c5ddc --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "ms-vscode.vscode-typescript-tslint-plugin", + "editorconfig.editorconfig", + "esbenp.prettier-vscode" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index ede160a80b00..97916b2b985b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,41 +1,309 @@ -// A launch configuration that compiles the extension and then opens it inside a new window -{ - "version": "0.1.0", - "configurations": [ - { - "name": "Launch Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceRoot}" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/out", - "preLaunchTask": "npm" - }, - { - "name": "Attach", - "type": "node", - "request": "attach", - "sourceMaps": true, - "port": 6004 - }, - { - "name": "Attach to python debug server", - "type": "node", - "request": "launch", - "runtimeArgs": [ - "--harmony" - ], - "program": "${workspaceRoot}/out/client/debugger/Main.js", - "stopOnEntry": false, - "args": [ - "--server=4711" - ], - "sourceMaps": true, - "outDir": "${workspaceRoot}/out/client" - } - ] -} \ No newline at end of file +// A launch configuration that compiles the extension and then opens it inside a new window +{ + "version": "0.1.0", + "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "stopOnEntry": false, + "smartStep": true, + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"], + "env": { + // Enable this to turn on redux logging during debugging + "XVSC_PYTHON_FORCE_LOGGING": "1", + // Enable this to try out new experiments locally + "VSC_PYTHON_LOAD_EXPERIMENTS_FROM_FILE": "1", + // Enable this to log telemetry to the output during debugging + "XVSC_PYTHON_LOG_TELEMETRY": "1", + // Enable this to log debugger output. Directory must exist ahead of time + "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex" + } + }, + { + "name": "Extension (DS UI in Browser)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "stopOnEntry": false, + "smartStep": true, + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Inject DS WebBrowser UI", + "env": { + "VSC_PYTHON_DS_UI_PROMPT": "1" + }, + "skipFiles": ["/**"] + }, + { + "name": "Extension inside container", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/data"], + "stopOnEntry": false, + "smartStep": true, + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile" + }, + { + "name": "Python: Current File with iPython", + "type": "python", + "request": "launch", + "module": "IPython", + "console": "integratedTerminal", + "args": ["${file}"] // Additional args should be prefixed with a '--' first. + }, + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, + { + "name": "Tests (Debugger, VS Code, *.test.ts)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/src/testMultiRootWkspc/multi.code-workspace", + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "stopOnEntry": false, + "sourceMaps": true, + "smartStep": true, + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "env": { + "IS_CI_SERVER_TEST_DEBUGGER": "1" + }, + "skipFiles": ["/**"] + }, + { + // Note, for the smoke test you want to debug, you may need to copy the file, + // rename it and remove a check for only smoke tests. + "name": "Tests (Smoke, VS Code, *.test.ts)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/src/testMultiRootWkspc/smokeTests", + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "env": { + "VSC_PYTHON_CI_TEST_GREP": "Smoke Test" + }, + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] + }, + { + "name": "Tests (Single Workspace, VS Code, *.test.ts)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/src/test", + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "env": { + "VSC_PYTHON_CI_TEST_GREP": "" // Modify this to run a subset of the single workspace tests + }, + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] + }, + { + "name": "Tests (DataScience, *.ds.test.ts)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/src/test/datascience", + "--disable-extensions", + "--enable-proposed-api", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "env": { + "VSC_PYTHON_CI_TEST_GREP": "", // Modify this to run a subset of the single workspace tests + "VSC_PYTHON_CI_TEST_INVERT_GREP": "", // Initialize this to invert the grep (exclude tests with value defined in grep). + + "VSC_PYTHON_LOAD_EXPERIMENTS_FROM_FILE": "true", + "TEST_FILES_SUFFIX": "ds.test" + }, + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] + }, + { + "name": "Tests (Multiroot, VS Code, *.test.ts)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/src/testMultiRootWkspc/multi.code-workspace", + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "stopOnEntry": false, + "sourceMaps": true, + "smartStep": true, + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] + }, + { + "name": "Unit Tests (without VS Code, *.unit.test.ts)", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "stopOnEntry": false, + "sourceMaps": true, + "args": [ + "./out/test/**/*.unit.test.js", + "--require=out/test/unittests.js", + "--ui=tdd", + "--recursive", + "--colors", + //"--grep", "", + "--timeout=300000" + ], + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] + }, + { + "name": "Unit Tests (fast, without VS Code and without react/monaco, *.unit.test.ts)", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "stopOnEntry": false, + "sourceMaps": true, + "args": [ + "./out/test/**/*.unit.test.js", + "--require=out/test/unittests.js", + "--ui=tdd", + "--recursive", + "--colors", + // "--grep", "", + "--timeout=300000", + "--fast" + ], + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] + }, + { + "name": "Functional Tests (without VS Code, *.functional.test.ts)", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "stopOnEntry": false, + "sourceMaps": true, + "args": [ + "./out/test/**/*.functional.test.js", + "--require=out/test/unittests.js", + "--ui=tdd", + "--recursive", + "--colors", + // "--grep", "", + "--timeout=300000", + "--exit" + ], + "env": { + // Remove `X` prefix to test with real browser to host DS ui (for DS functional tests). + "XVSC_PYTHON_DS_UI_BROWSER": "1", + // Remove `X` prefix to test with real python (for DS functional tests). + "XVSCODE_PYTHON_ROLLING": "1", + // Remove 'X' to turn on all logging in the debug output + "XVSC_PYTHON_FORCE_LOGGING": "1", + // Remove `X` prefix and update path to test with real python interpreter (for DS functional tests). + "XCI_PYTHON_PATH": "", + // Remove 'X' prefix to dump output for debugger. Directory has to exist prior to launch + "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output" + }, + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] + }, + { + "name": "Functional DS UI Tests (without VS Code, *.ui.functional.test.ts)", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "stopOnEntry": false, + "sourceMaps": true, + "args": [ + "./out/test/**/*.ui.functional.test.js", + "--require=out/test/unittests.js", + "--ui=tdd", + "--recursive", + "--colors", + //"--grep", "", + "--timeout=300000", + "--fast" + ], + "env": { + // Remove `X` prefix to test with real browser to host DS ui (for DS functional tests). + "XVSC_PYTHON_DS_UI_BROWSER": "1", + // Remove `X` prefix to test with real python (for DS functional tests). + "XVSCODE_PYTHON_ROLLING": "1", + // Remove 'X' to turn on all logging in the debug output + "XVSC_PYTHON_FORCE_LOGGING": "1", + // Remove `X` prefix and update path to test with real python interpreter (for DS functional tests). + "XCI_PYTHON_PATH": "" + }, + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] + }, + { + "type": "node", + "request": "launch", + "name": "Gulp tasks (helpful for debugging gulpfile.js)", + "program": "${workspaceFolder}/node_modules/gulp/bin/gulp.js", + "args": ["watch"], + "skipFiles": ["/**"] + }, + { + "name": "Node: Current File", + "program": "${file}", + "request": "launch", + "skipFiles": [ + "/**" + ], + "type": "pwa-node" + }, + { + "name": "Python: Current File", + "type": "python", + "justMyCode": true, + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f5aa9cf942a..b2dc13efe26e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,60 @@ // Place your settings in this file to overwrite default and user settings. { - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version -} \ No newline at end of file + "files.exclude": { + "out": true, // set this to true to hide the "out" folder with the compiled JS files + "**/*.pyc": true, + ".nyc_output": true, + "obj": true, + "bin": true, + "**/__pycache__": true, + "**/node_modules": true, + ".vscode-test": false, + ".vscode test": false, + "**/.mypy_cache/**": true, + "**/.ropeproject/**": true + }, + "search.exclude": { + "out": true, // set this to false to include "out" folder in search results + "**/node_modules": true, + "coverage": true, + "languageServer*/**": true, + ".vscode-test": true, + ".vscode test": true + }, + "[python]": { + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.formatOnSave": true + }, + "[javascript]": { + "editor.formatOnSave": true + }, + "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version + "tslint.enable": true, + "python.linting.enabled": false, + "python.testing.promptToConfigure": false, + "python.workspaceSymbols.enabled": false, + "python.formatting.provider": "black", + "typescript.preferences.quoteStyle": "single", + "javascript.preferences.quoteStyle": "single", + "typescriptHero.imports.stringQuoteStyle": "'", + "prettier.printWidth": 120, + "prettier.singleQuote": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true, + "source.fixAll.tslint": true + }, + "python.languageServer": "Microsoft", + "python.analysis.logLevel": "Trace", + "python.analysis.downloadChannel": "beta", + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true, + "cucumberautocomplete.skipDocStringsFormat": true, + "python.linting.flake8Args": [ + // Match what black does. + "--max-line-length=88" + ], + "typescript.preferences.importModuleSpecifier": "relative", + "debug.javascript.usePreview": false +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d31b15910eed..6f0b07bc7214 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,30 +1,60 @@ -// Available variables which can be used inside of strings. -// ${workspaceRoot}: the root folder of the team -// ${file}: the current opened file -// ${fileBasename}: the current opened file's basename -// ${fileDirname}: the current opened file's dirname -// ${fileExtname}: the current opened file's extension -// ${cwd}: the current working directory of the spawned process - -// A task runner that calls a custom npm script that compiles the extension. { - "version": "0.1.0", - - // we want to run npm - "command": "npm", - - // the command is a shell script - "isShellCommand": true, - - // show the output window only if unrecognized errors occur. - "showOutput": "silent", - - // we run the custom script "compile" as defined in package.json - "args": ["run", "compile", "--loglevel", "silent"], - - // The tsc compiler is started in watching mode - "isWatching": true, - - // use the standard tsc in watch mode problem matcher to find compile problems in the output. - "problemMatcher": "$tsc-watch" -} \ No newline at end of file + "version": "2.0.0", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared" + }, + "tasks": [ + { + "label": "Compile", + "type": "npm", + "script": "compile", + "isBackground": true, + "problemMatcher": [ + "$tsc-watch", + { + "base": "$tslint5", + "fileLocation": "relative" + } + ], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Compile Web Views", + "type": "npm", + "script": "compile-webviews-watch", + "isBackground": true, + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$tsc-watch", + "$ts-checker-webpack-watch" + ] + }, + { + "label": "Run Unit Tests", + "type": "npm", + "script": "test:unittests", + "group": { + "kind": "test", + "isDefault": true + } + }, + { + "label": "Inject DS WebBrowser UI", + "type": "shell", + "command": "node", + "args": [ + "build/debug/replaceWithWebBrowserPanel.js" + ], + "problemMatcher": [] + } + ] +} diff --git a/.vscodeignore b/.vscodeignore index 93e28ff2fdff..d0da6f84ddf9 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,9 +1,112 @@ -.vscode/** -typings/** -out/test/** -test/** -src/** **/*.map +**/*.analyzer.html +!out/datascience-ui/common/node_modules/**/* +!out/datascience-ui/notebook/commons.initial.bundle.js.map +!out/datascience-ui/notebook/interactiveWindow.js.map +!out/datascience-ui/notebook/nativeEditor.js.map +!out/datascience-ui/viewers/commons.initial.bundle.js.map +!out/datascience-ui/viewers/dataExplorer.js.map +!out/datascience-ui/viewers/plotViewer.js.map +*.vsix +.appveyor.yml +.editorconfig +.env +.eslintrc +.gitattributes .gitignore -tsconfig.json +.gitmodules +.huskyrc.json +.npmrc +.nvmrc +.nycrc +.travis.yml +CODE_OF_CONDUCT.md +CODING_STANDARDS.md +CONTRIBUTING.md +CONTRIBUTING - LANGUAGE SERVER.md +coverconfig.json +cucumber-report.json +gulpfile.js +package.datascience-ui.dependencies.json +package-lock.json +packageExtension.cmd +pvsc-dev-ext.py +pvsc.code-workspace +PYTHON_INTERACTIVE_TROUBLESHOOTING.md +requirements.in +sprint-planning.github-issues +test.ipynb +travis*.log +tsconfig*.json +tsfmt.json +tslint.json +typings.json vsc-extension-quickstart.md +vscode-python-signing.* +webpack.config.js +webpack.datascience-*.config.js + +.devcontainer/** +.github/** +.mocha-reporter/** +.nvm/** +.nyc_output +.prettierrc.js +.sonarcloud.properties +.venv/** +.vscode/** +.vscode-test/** +.vscode test/** +languageServer/** +languageServer.*/** +nodeLanguageServer/** +nodeLanguageServer.*/** +bin/** +build/** +BuildOutput/** +coverage/** +data/** +debug_coverage*/** +images/**/*.gif +images/**/*.png +ipywidgets/** +news/** +node_modules/** +obj/** +out/**/*.stats.json +out/client/**/*.analyzer.html +out/coverconfig.json +out/datascience-ui/**/*.analyzer.html +out/datascience-ui/common/** +out/datascience-ui/**/*.js.LICENSE +out/datascience-ui/data-explorer/** +out/datascience-ui/datascience-ui/** +out/datascience-ui/history-react/** +out/datascience-ui/interactive-common/** +out/datascience-ui/ipywidgets/** +out/datascience-ui/native-editor/** +out/datascience-ui/notebook/datascience-ui/** +out/datascience-ui/notebook/ipywidgets/** +out/datascience-ui/notebook/index.*.html +out/datascience-ui/plot/** +out/datascience-ui/react-common/** +out/datascience-ui/viewers/datascience-ui/** +out/datascience-ui/viewers/ipywidgets/** +out/datascience-ui/viewers/index.*.html +out/pythonFiles/** +out/src/** +out/test/** +out/testMultiRootWkspc/** +precommit.hook +pythonFiles/**/*.pyc +pythonFiles/lib/**/*.dist-info/** +pythonFiles/lib/**/*.egg-info/** +pythonFiles/lib/python/bin/** +pythonFiles/tests/** +requirements.txt +scripts/** +src/** +test/** +tmp/** +typings/** +types/** diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000000..e997502b5489 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7061 @@ +# Changelog + +## 2020.8.4 (2 September 2020) + +### Enhancements + +1. Make Jupyter Server name clickable to select Jupyter server. + ([#13656](https://github.com/Microsoft/vscode-python/issues/13656)) + +### Fixes + +1. Fixed connection to a Compute Instance from the quickpicks history options. + ([#13387](https://github.com/Microsoft/vscode-python/issues/13387)) +1. Fixed the behavior of the 'python.showStartPage' setting. + ([#13347](https://github.com/microsoft/vscode-python/issues/13347)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [Pylance](https://github.com/microsoft/pylance-release) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.8.3 (31 August 2020) + +### Enhancements + +1. Add telemetry about the install source for the extension. + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [Pylance](https://github.com/microsoft/pylance-release) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.8.2 (27 August 2020) + +### Enhancements + +1. Update "Tip" notification for new users to either show the existing tip, a link to a feedback survey or nothing. + ([#13535](https://github.com/Microsoft/vscode-python/issues/13535)) + +### Fixes + +1. Fix saving during close and auto backup to actually save a notebook. + ([#11711](https://github.com/Microsoft/vscode-python/issues/11711)) +1. Show the server display string that the user is going to connect to after selecting a compute instance and reloading the window. + ([#13551](https://github.com/Microsoft/vscode-python/issues/13551)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [Pylance](https://github.com/microsoft/pylance-release) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.8.1 (20 August 2020) + +### Fixes + +1. Update LSP to latest to resolve problems with LS settings. + ([#13511](https://github.com/microsoft/vscode-python/pull/13511)) +1. Update debugger to address terminal input issues. +1. Added tooltip to indicate status of server connection + ([#13543](https://github.com/Microsoft/vscode-python/issues/13543)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [Pylance](https://github.com/microsoft/pylance-release) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.8.0 (12 August 2020) + +### Enhancements + +1. Cell id and cell metadata are now passed as the metadata field for execute_request messages. + (thanks [stisa](https://github.com/stisa/)) + ([#13252](https://github.com/Microsoft/vscode-python/issues/13252)) +1. Add "Restart Language Server" command. + ([#3073](https://github.com/Microsoft/vscode-python/issues/3073)) +1. Support multiple and per file interactive windows. See the description for the new 'python.dataScience.interactiveWindowMode' setting. + ([#3104](https://github.com/Microsoft/vscode-python/issues/3104)) +1. Add cell editing shortcuts for python interactive cells. (thanks [@earthastronaut](https://github.com/earthastronaut/)). + ([#12414](https://github.com/Microsoft/vscode-python/issues/12414)) +1. Allow `python.dataScience.runStartupCommands` to be an array. (thanks [@janosh](https://github.com/janosh)). + ([#12827](https://github.com/Microsoft/vscode-python/issues/12827)) +1. Remember remote kernel ids when reopening notebooks. + ([#12828](https://github.com/Microsoft/vscode-python/issues/12828)) +1. The file explorer dialog now has an appropriate title when browsing for an interpreter. (thanks [ziebam](https://github.com/ziebam)). + ([#12959](https://github.com/Microsoft/vscode-python/issues/12959)) +1. Warn users if they are connecting over http without a token. + ([#12980](https://github.com/Microsoft/vscode-python/issues/12980)) +1. Allow a custom display string for remote servers as part of the remote Jupyter server provider extensibility point. + ([#12988](https://github.com/Microsoft/vscode-python/issues/12988)) +1. Update to the latest version of [`jedi`](https://github.com/davidhalter/jedi) (`0.17.2`). This adds support for Python 3.9 and fixes some bugs, but is expected to be the last release to support Python 2.7 and 3.5. (thanks [Peter Law](https://github.com/PeterJCLaw/)). + ([#13037](https://github.com/Microsoft/vscode-python/issues/13037)) +1. Expose `Pylance` setting in `python.languageServer`. If [Pylance extension](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) is not installed, prompt user to install it. + ([#13122](https://github.com/Microsoft/vscode-python/issues/13122)) +1. Added "pythonArgs" to debugpy launch.json schema. + ([#13218](https://github.com/Microsoft/vscode-python/issues/13218)) +1. Use jupyter inspect to get signature of dynamic functions in notebook editor when language server doesn't provide enough hint. + ([#13259](https://github.com/Microsoft/vscode-python/issues/13259)) +1. The gather icon will change and get disabled while gather is executing. + ([#13177](https://github.com/microsoft/vscode-python/issues/13177)) + +### Fixes + +1. Gathered notebooks will now use the same kernelspec as the notebook it was created from. + ([#10924](https://github.com/Microsoft/vscode-python/issues/10924)) +1. Don't loop selection through all failed tests every time tests are run. + ([#11743](https://github.com/Microsoft/vscode-python/issues/11743)) +1. Some tools (like pytest) rely on the existence of `sys.path[0]`, so + deleting it in the isolation script can sometimes cause problems. The + solution is to point `sys.path[0]` to a bogus directory that we know + does not exist (assuming noone modifies the extension install dir). + ([#11875](https://github.com/Microsoft/vscode-python/issues/11875)) +1. Fix missing css for some ipywidget output. + ([#12202](https://github.com/Microsoft/vscode-python/issues/12202)) +1. Delete backing untitled ipynb notebook files as soon as the remote session has been created. + ([#12510](https://github.com/Microsoft/vscode-python/issues/12510)) +1. Make the data science variable explorer support high contrast color theme. + ([#12766](https://github.com/Microsoft/vscode-python/issues/12766)) +1. The change in PR #12795 led to one particular test suite to take longer + to run. Here we increase the timeout for that suite to get the test + passing. + ([#12833](https://github.com/Microsoft/vscode-python/issues/12833)) +1. Refactor data science filesystem usage to correctly handle files which are potentially remote. + ([#12931](https://github.com/Microsoft/vscode-python/issues/12931)) +1. Allow custom Jupyter server URI providers to have an expiration on their authorization headers. + ([#12987](https://github.com/Microsoft/vscode-python/issues/12987)) +1. If a webpanel fails to load, dispose our webviewhost so that it can try again. + ([#13106](https://github.com/Microsoft/vscode-python/issues/13106)) +1. Ensure terminal is not shown or activated if hideFromUser is set to true. + ([#13117](https://github.com/Microsoft/vscode-python/issues/13117)) +1. Do not automatically start kernel for untrusted notebooks. + ([#13124](https://github.com/Microsoft/vscode-python/issues/13124)) +1. Fix settings links to open correctly in the notebook editor. + ([#13156](https://github.com/Microsoft/vscode-python/issues/13156)) +1. "a" and "b" Jupyter shortcuts should not automatically enter edit mode. + ([#13165](https://github.com/Microsoft/vscode-python/issues/13165)) +1. Scope custom notebook keybindings to Jupyter Notebooks. + ([#13172](https://github.com/Microsoft/vscode-python/issues/13172)) +1. Rename "Count" column in variable explorer to "Size". + ([#13205](https://github.com/Microsoft/vscode-python/issues/13205)) +1. Handle `Save As` of preview Notebooks. + ([#13235](https://github.com/Microsoft/vscode-python/issues/13235)) + +### Code Health + +1. Move non-mock jupyter nightly tests to use raw kernel by default. + ([#10772](https://github.com/Microsoft/vscode-python/issues/10772)) +1. Add new services to data science IOC container and rename misspelled service. + ([#12809](https://github.com/Microsoft/vscode-python/issues/12809)) +1. Disable Notebook icons when Notebook is not trusted. + ([#12893](https://github.com/Microsoft/vscode-python/issues/12893)) +1. Removed control tower code for the start page. + ([#12919](https://github.com/Microsoft/vscode-python/issues/12919)) +1. Add better tests for trusted notebooks in the classic notebook editor. + ([#12966](https://github.com/Microsoft/vscode-python/issues/12966)) +1. Custom renderers for `png/jpeg` images in `Notebooks`. + ([#12977](https://github.com/Microsoft/vscode-python/issues/12977)) +1. Fix broken nightly variable explorer tests. + ([#13075](https://github.com/Microsoft/vscode-python/issues/13075)) +1. Fix nightly flake test failures for startup and shutdown native editor test. + ([#13171](https://github.com/Microsoft/vscode-python/issues/13171)) +1. Fix failing interactive window and variable explorer tests. + ([#13269](https://github.com/Microsoft/vscode-python/issues/13269)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [Pylance](https://github.com/microsoft/pylance-release) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.7.1 (22 July 2020) + +1. Fix language server setting when provided an invalid value, send config event more consistently. + ([#13064](https://github.com/Microsoft/vscode-python/pull/13064)) +1. Add banner for pylance, and remove old LS experiment. + ([#12817](https://github.com/microsoft/vscode-python/pull/12817)) + +## 2020.7.0 (16 July 2020) + +### Enhancements + +1. Support connecting to Jupyter hub servers. Use either the base url of the server (i.e. 'https://111.11.11.11:8000') or your user folder (i.e. 'https://111.11.11.11:8000/user/theuser). + Works with password authentication. + ([#9679](https://github.com/Microsoft/vscode-python/issues/9679)) +1. Added "argsExpansion" to debugpy launch.json schema. + ([#11678](https://github.com/Microsoft/vscode-python/issues/11678)) +1. The extension will now automatically load if a `pyproject.toml` file is present in the workspace root directory. + (thanks [Brandon White](https://github.com/BrandonLWhite)) + ([#12056](https://github.com/Microsoft/vscode-python/issues/12056)) +1. Add ability to check and update whether a notebook is trusted. + ([#12146](https://github.com/Microsoft/vscode-python/issues/12146)) +1. Support formatting of Notebook Cells when using the VS Code Insiders API for Notebooks. + ([#12195](https://github.com/Microsoft/vscode-python/issues/12195)) +1. Added exporting notebooks to HTML. + ([#12375](https://github.com/Microsoft/vscode-python/issues/12375)) +1. Change stock launch.json "attach" config to use "connect". + ([#12446](https://github.com/Microsoft/vscode-python/issues/12446)) +1. Update to the latest version of [`jedi`](https://github.com/davidhalter/jedi) (`0.17.1`). This brings completions for Django (via [`django-stubs`](https://github.com/typeddjango/django-stubs)) as well as support for Python 3.9 and various bugfixes (mostly around generic type annotations). (thanks [Peter Law](https://gitlab.com/PeterJCLaw/)) + ([#12486](https://github.com/Microsoft/vscode-python/issues/12486)) +1. Prompt users that we have deleted pythonPath from their workspace settings when in `Deprecate PythonPath` experiment. + ([#12533](https://github.com/Microsoft/vscode-python/issues/12533)) +1. Changed public API for execution to return an object and provide a callback which is called when interpreter setting changes. + ([#12596](https://github.com/Microsoft/vscode-python/issues/12596)) +1. Allow users to opt out of us checking whether their notebooks can be trusted. This setting is turned off by default and must be manually enabled. + ([#12611](https://github.com/Microsoft/vscode-python/issues/12611)) +1. Include the JUPYTER_PATH environment variable when searching the disk for kernels. + ([#12694](https://github.com/Microsoft/vscode-python/issues/12694)) +1. Added exporting to python, HTML and PDF from the interactive window. + ([#12732](https://github.com/Microsoft/vscode-python/issues/12732)) +1. Show a prompt asking user to upgrade Code runner to new version to keep using it when in Deprecate PythonPath experiment. + ([#12764](https://github.com/Microsoft/vscode-python/issues/12764)) +1. Opening notebooks in the preview Notebook editor for [Visual Studio Code Insiders](https://code.visualstudio.com/insiders/). + ([#10496](https://github.com/Microsoft/vscode-python/issues/10496)) + +### Fixes + +1. Ensure we only have a single isort process running on a single file. + ([#10579](https://github.com/Microsoft/vscode-python/issues/10579)) +1. Provided a method for external partners to participate in jupyter server URI picking/authentication. + ([#10993](https://github.com/Microsoft/vscode-python/issues/10993)) +1. Check for hideFromUser before activating current terminal. + ([#11122](https://github.com/Microsoft/vscode-python/issues/11122)) +1. In Markdown cells, turn HTML links to markdown links so that nteract renders them. + ([#11254](https://github.com/Microsoft/vscode-python/issues/11254)) +1. Prevent incorrect ipywidget display (double plots) due to synchronization issues. + ([#11281](https://github.com/Microsoft/vscode-python/issues/11281)) +1. Removed the Kernel Selection toolbar from the Interactive Window when using a local Jupyter Server. + To show it again, set the setting 'Python > Data Science > Show Kernel Selection On Interactive Window'. + ([#11347](https://github.com/Microsoft/vscode-python/issues/11347)) +1. Get Jupyter connections to work with a Windows store installed Python/Jupyter combination. + ([#11412](https://github.com/Microsoft/vscode-python/issues/11412)) +1. Disable hover intellisense in the interactive window unless the code is expanded. + ([#11459](https://github.com/Microsoft/vscode-python/issues/11459)) +1. Make layout of markdown editors much faster to open. + ([#11584](https://github.com/Microsoft/vscode-python/issues/11584)) +1. Watermark in the interactive window can appear on top of entered text. + ([#11691](https://github.com/Microsoft/vscode-python/issues/11691)) +1. Jupyter can fail to run a kernel if the user's environment contains non string values. + ([#11749](https://github.com/Microsoft/vscode-python/issues/11749)) +1. On Mac meta+Z commands are performing both cell and editor undos. + ([#11758](https://github.com/Microsoft/vscode-python/issues/11758)) +1. Paste can sometimes double paste into a notebook or interactive window editor. + ([#11796](https://github.com/Microsoft/vscode-python/issues/11796)) +1. Fix jupyter connections going down when azure-storage or other extensions with node-fetch are installed. + ([#11830](https://github.com/Microsoft/vscode-python/issues/11830)) +1. Variables should not flash when running by line. + ([#12046](https://github.com/Microsoft/vscode-python/issues/12046)) +1. Discard changes on Notebooks when the user selects 'Don't Save' on the save changes dialog. + ([#12180](https://github.com/Microsoft/vscode-python/issues/12180)) +1. Disable `Extract variable & method` commands in `Notebook Cells`. + ([#12206](https://github.com/Microsoft/vscode-python/issues/12206)) +1. Disable linting in Notebook Cells. + ([#12208](https://github.com/Microsoft/vscode-python/issues/12208)) +1. Register services before extension activates. + ([#12227](https://github.com/Microsoft/vscode-python/issues/12227)) +1. Infinite loop of asking to reload the extension when enabling custom editor. + ([#12231](https://github.com/Microsoft/vscode-python/issues/12231)) +1. Fix raw kernel autostart and remove jupyter execution from interactive base. + ([#12330](https://github.com/Microsoft/vscode-python/issues/12330)) +1. If we fail to start a raw kernel daemon then fall back to using process execution. + ([#12355](https://github.com/Microsoft/vscode-python/issues/12355)) +1. Fix the export button from the interactive window to export again. + ([#12460](https://github.com/Microsoft/vscode-python/issues/12460)) +1. Process Jupyter messages synchronously when possible. + ([#12588](https://github.com/Microsoft/vscode-python/issues/12588)) +1. Open variable explorer when opening variable explorer during debugging. + ([#12773](https://github.com/Microsoft/vscode-python/issues/12773)) +1. Use the given interpreter for launching the non-daemon python + ([#12821](https://github.com/Microsoft/vscode-python/issues/12821)) +1. Correct the color of the 'Collapse All' button in the Interactive Window + ([#12838](https://github.com/microsoft/vscode-python/issues/12838)) + +### Code Health + +1. Move all logging to the Python output channel. + ([#9837](https://github.com/Microsoft/vscode-python/issues/9837)) +1. Add a functional test that opens both the interactive window and a notebook at the same time. + ([#11445](https://github.com/Microsoft/vscode-python/issues/11445)) +1. Added setting `python.logging.level` which carries the logging level value the extension will log at. + ([#11699](https://github.com/Microsoft/vscode-python/issues/11699)) +1. Monkeypatch `console.*` calls to the logger only in CI. + ([#11896](https://github.com/Microsoft/vscode-python/issues/11896)) +1. Replace python.dataScience.ptvsdDistPath with python.dataScience.debugpyDistPath. + ([#11993](https://github.com/Microsoft/vscode-python/issues/11993)) +1. Rename ptvsd to debugpy in Telemetry. + ([#11996](https://github.com/Microsoft/vscode-python/issues/11996)) +1. Update JSDoc annotations for many of the APIs (thanks [Anthony Shaw](https://github.com/tonybaloney)) + ([#12101](https://github.com/Microsoft/vscode-python/issues/12101)) +1. Refactor `LinterId` to an enum instead of a string union. + (thanks to [Anthony Shaw](https://github.com/tonybaloney)) + ([#12116](https://github.com/Microsoft/vscode-python/issues/12116)) +1. Remove webserver used to host contents in WebViews. + ([#12140](https://github.com/Microsoft/vscode-python/issues/12140)) +1. Inline interface due to issues with custom types when using `ts-node`. + ([#12238](https://github.com/Microsoft/vscode-python/issues/12238)) +1. Fix linux nightly tests so they run and report results. Also seems to get rid of stream destroyed messages for raw kernel. + ([#12539](https://github.com/Microsoft/vscode-python/issues/12539)) +1. Log ExP experiments the user belongs to in the output panel. + ([#12656](https://github.com/Microsoft/vscode-python/issues/12656)) +1. Add more telemetry for "Select Interpreter" command. + ([#12722](https://github.com/Microsoft/vscode-python/issues/12722)) +1. Add tests for trusted notebooks. + ([#12554](https://github.com/Microsoft/vscode-python/issues/12554)) +1. Update categories in `package.json`. + ([#12844](https://github.com/Microsoft/vscode-python/issues/12844)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.6.3 (30 June 2020) + +### Fixes + +1. Correctly check for ZMQ support, previously it could allow ZMQ to be supported when zmq could not be imported. + ([#12585](https://github.com/Microsoft/vscode-python/issues/12585)) +1. Auto indentation no longer working for notebooks and interactive window. + ([#12389](https://github.com/Microsoft/vscode-python/issues/12389)) +1. Add telemetry for tracking run by line. + ([#12580](https://github.com/Microsoft/vscode-python/issues/12580)) +1. Add more telemetry to distinguish how is the start page opened. + ([#12603](https://github.com/microsoft/vscode-python/issues/12603)) +1. Stop looking for mspythonconfig.json file in subfolders. + ([#12614](https://github.com/Microsoft/vscode-python/issues/12614)) + +## 2020.6.2 (25 June 2020) + +### Fixes + +1. Fix `linting.pylintEnabled` setting check. + ([#12285](https://github.com/Microsoft/vscode-python/issues/12285)) +1. Don't modify LS settings if jediEnabled does not exist. + ([#12429](https://github.com/Microsoft/vscode-python/issues/12429)) + +## 2020.6.1 (17 June 2020) + +### Fixes + +1. Fixed issue when `python.jediEnabled` setting was not removed and `python.languageServer` setting was not updated. + ([#12429](https://github.com/Microsoft/vscode-python/issues/12429)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.6.0 (16 June 2020) + +### Enhancements + +1. Removed `python.jediEnabled` setting in favor of `python.languageServer`. Instead of `"python.jediEnabled": true` please use `"python.languageServer": "Jedi"`. + ([#7010](https://github.com/Microsoft/vscode-python/issues/7010)) +1. Added a start page for the extension. It opens to new users or when there is a new release. It can be disabled with the setting 'Python: Show Start Page'. + ([#11057](https://github.com/Microsoft/vscode-python/issues/11057)) +1. Preliminary support using other languages for the kernel. + ([#11919](https://github.com/Microsoft/vscode-python/issues/11919)) +1. Enable the use of the custom editor for native notebooks. + ([#10744](https://github.com/Microsoft/vscode-python/issues/10744)) + +### Fixes + +1. Ensure sorting imports in a modified file picks up the proper configuration. + thanks [Peter Law](https://github.com/PeterJCLaw)) + ([#4891](https://github.com/Microsoft/vscode-python/issues/4891)) +1. Made variable explorer (from IPython Notebook interface) resizable. + ([#5382](https://github.com/Microsoft/vscode-python/issues/5382)) +1. Add junit family to pytest runner args to remove pytest warning. + ([#10673](https://github.com/Microsoft/vscode-python/issues/10673)) +1. Switch order of restart and cancel buttons in interactive window to be consistent with ordering in notebook toolbar. + ([#11091](https://github.com/Microsoft/vscode-python/issues/11091)) +1. Support opening other URI schemes besides 'file' and 'vsls'. + ([#11393](https://github.com/Microsoft/vscode-python/issues/11393)) +1. Fix issue with formatting when the first line is blank. + ([#11416](https://github.com/Microsoft/vscode-python/issues/11416)) +1. Force interactive window to always scroll long output. Don't allow scrollbars within scrollbars. + ([#11421](https://github.com/Microsoft/vscode-python/issues/11421)) +1. Hover on notebooks or interactive window seems to stutter. + ([#11422](https://github.com/Microsoft/vscode-python/issues/11422)) +1. Make shift+tab work again in the interactive window. Escaping focus from the prompt is now relegated to 'Shift+Esc'. + ([#11495](https://github.com/Microsoft/vscode-python/issues/11495)) +1. Keep import and export working with raw kernel mode. Also allow for installing dependencies if running an import before jupyter was ever launched. + ([#11501](https://github.com/Microsoft/vscode-python/issues/11501)) +1. Extra kernels that just say "Python 3 - python" are showing up in the raw kernel kernel picker. + ([#11552](https://github.com/Microsoft/vscode-python/issues/11552)) +1. Fix intermittent launch failure with raw kernels on windows. + ([#11574](https://github.com/Microsoft/vscode-python/issues/11574)) +1. Don't register a kernelspec when switching to an interpreter in raw kernel mode. + ([#11575](https://github.com/Microsoft/vscode-python/issues/11575)) +1. Keep the notebook input prompt up if you focus out of vscode. + ([#11581](https://github.com/Microsoft/vscode-python/issues/11581)) +1. Fix install message to reference run by line instead of debugging. + ([#11661](https://github.com/Microsoft/vscode-python/issues/11661)) +1. Run by line does not scroll to the line that is being run. + ([#11662](https://github.com/Microsoft/vscode-python/issues/11662)) +1. For direct kernel connection, don't replace a notebook's metadata default kernelspec with a new kernelspec on startup. + ([#11672](https://github.com/Microsoft/vscode-python/issues/11672)) +1. Fixes issue with importing `debupy` in interactive window. + ([#11686](https://github.com/Microsoft/vscode-python/issues/11686)) +1. Reopen all notebooks when rerunning the extension (including untitled ones). + ([#11711](https://github.com/Microsoft/vscode-python/issues/11711)) +1. Make sure to clear 'outputPrepend' when rerunning cells and to also only ever add it once to a cell. + (thanks [Barry Nolte](https://github.com/BarryNolte)) + ([#11726](https://github.com/Microsoft/vscode-python/issues/11726)) +1. Disable pre-warming of Kernel Daemons when user does not belong to the `LocalZMQKernel - experiment` experiment. + ([#11751](https://github.com/Microsoft/vscode-python/issues/11751)) +1. When switching to an invalid kernel (one that is registered but cannot start) in raw mode respect the launch timeout that is passed in. + ([#11752](https://github.com/Microsoft/vscode-python/issues/11752)) +1. Make `python.dataScience.textOutputLimit` apply on subsequent rerun. We were letting the 'outputPrepend' metadata persist from run to run. + (thanks [Barry Nolte](https://github.com/BarryNolte)) + ([#11777](https://github.com/Microsoft/vscode-python/issues/11777)) +1. Use `${command:python.interpreterPath}` to get selected interpreter path in `launch.json` and `tasks.json`. + ([#11789](https://github.com/Microsoft/vscode-python/issues/11789)) +1. Restarting a kernel messes up run by line. + ([#11793](https://github.com/Microsoft/vscode-python/issues/11793)) +1. Correctly show kernel status in raw kernel mode. + ([#11797](https://github.com/Microsoft/vscode-python/issues/11797)) +1. Hovering over variables in a python file can show two hover values if the interactive window is closed and reopened. + ([#11800](https://github.com/Microsoft/vscode-python/issues/11800)) +1. Make sure to use webView.cspSource for all csp sources. + ([#11855](https://github.com/Microsoft/vscode-python/issues/11855)) +1. Use command line arguments to launch our raw kernels as opposed to a connection file. The connection file seems to be causing issues in particular on windows CI machines with permissions. + ([#11883](https://github.com/Microsoft/vscode-python/issues/11883)) +1. Improve our status reporting when launching and connecting to a raw kernel. + ([#11951](https://github.com/Microsoft/vscode-python/issues/11951)) +1. Prewarm raw kernels based on raw kernel support and don't prewarm if jupyter autostart is disabled. + ([#11956](https://github.com/Microsoft/vscode-python/issues/11956)) +1. Don't flood the hard drive when typing in a large notebook file. + ([#12058](https://github.com/Microsoft/vscode-python/issues/12058)) +1. Disable run-by-line and continue buttons in run by line mode when running. + ([#12169](https://github.com/Microsoft/vscode-python/issues/12169)) +1. Disable `Sort Imports` command in `Notebook Cells`. + ([#12193](https://github.com/Microsoft/vscode-python/issues/12193)) +1. Fix debugger continue event to actually change a cell. + ([#12155](https://github.com/Microsoft/vscode-python/issues/12155)) +1. Make Jedi the Default value for the python.languageServer setting. + ([#12225](https://github.com/Microsoft/vscode-python/issues/12225)) +1. Make stop during run by line interrupt the kernel. + ([#12249](https://github.com/Microsoft/vscode-python/issues/12249)) +1. Have raw kernel respect the jupyter server disable auto start setting. + ([#12246](https://github.com/Microsoft/vscode-python/issues/12246)) + +### Code Health + +1. Use ts-loader as a tyepscript loader in webpack. + ([#9061](https://github.com/Microsoft/vscode-python/issues/9061)) +1. Fixed typo from unitest -> unittest. + (thanks [Rameez Khan](https://github.com/Rxmeez)). + ([#10919](https://github.com/Microsoft/vscode-python/issues/10919)) +1. Make functional tests more deterministic. + ([#11058](https://github.com/Microsoft/vscode-python/issues/11058)) +1. Reenable CDN unit tests. + ([#11442](https://github.com/Microsoft/vscode-python/issues/11442)) +1. Run by line for notebook cells minimal implementation. + ([#11607](https://github.com/Microsoft/vscode-python/issues/11607)) +1. Get shape and count when showing debugger variables. + ([#11657](https://github.com/Microsoft/vscode-python/issues/11657)) +1. Add more tests to verify data frames can be opened. + ([#11658](https://github.com/Microsoft/vscode-python/issues/11658)) +1. Support data tips overtop of python files that have had cells run. + ([#11659](https://github.com/Microsoft/vscode-python/issues/11659)) +1. Functional test for run by line functionality. + ([#11660](https://github.com/Microsoft/vscode-python/issues/11660)) +1. Fixed typo in a test from lanaguage -> language. + (thanks [Ashwin Ramaswami](https://github.com/epicfaace)). + ([#11775](https://github.com/Microsoft/vscode-python/issues/11775)) +1. Add bitness information to interpreter telemetry. + ([#11904](https://github.com/Microsoft/vscode-python/issues/11904)) +1. Fix failing linux debugger tests. + ([#11935](https://github.com/Microsoft/vscode-python/issues/11935)) +1. Faster unit tests on CI Pipeline. + ([#12017](https://github.com/Microsoft/vscode-python/issues/12017)) +1. Ensure we can use proposed VS Code API with `ts-node`. + ([#12025](https://github.com/Microsoft/vscode-python/issues/12025)) +1. Faster node unit tests on Azure pipeline. + ([#12027](https://github.com/Microsoft/vscode-python/issues/12027)) +1. Use [deemon](https://www.npmjs.com/package/deemon) package for background compilation with support for restarting VS Code during development. + ([#12059](https://github.com/Microsoft/vscode-python/issues/12059)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.5.3 (10 June 2020) + +1. Update `debugpy` to use `1.0.0b11` or greater. + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.5.2 (8 June 2020) + +### Fixes + +1. Double-check for interpreters when running diagnostics before displaying the "Python is not installed" message. + ([#11870](https://github.com/Microsoft/vscode-python/issues/11870)) +1. Ensure user cannot belong to all experiments in an experiment group. + ([#11943](https://github.com/Microsoft/vscode-python/issues/11943)) +1. Ensure extension features are started when in `Deprecate PythonPath` experiment and opening a file without any folder opened. + ([#12177](https://github.com/Microsoft/vscode-python/issues/12177)) + +### Code Health + +1. Integrate VS Code experiment framework in the extension. + ([#10790](https://github.com/Microsoft/vscode-python/issues/10790)) +1. Update telemetry on errors and exceptions to use [vscode-extension-telemetry](https://www.npmjs.com/package/vscode-extension-telemetry). + ([#11597](https://github.com/Microsoft/vscode-python/issues/11597)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.5.1 (19 May 2020) + +### Fixes + +1. Do not execute shebang as an interpreter until user has clicked on the codelens enclosing the shebang. + ([#11687](https://github.com/Microsoft/vscode-python/issues/11687)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.5.0 (12 May 2020) + +### Enhancements + +1. Added ability to manually enter a path to interpreter in the select interpreter dropdown. + ([#216](https://github.com/Microsoft/vscode-python/issues/216)) +1. Add status bar item with icon when installing Insiders/Stable build. + (thanks to [ErwanDL](https://github.com/ErwanDL/)) + ([#10495](https://github.com/Microsoft/vscode-python/issues/10495)) +1. Support for language servers that don't allow incremental document updates inside of notebooks and the interactive window. + ([#10818](https://github.com/Microsoft/vscode-python/issues/10818)) +1. Add telemetry for "Python is not installed" prompt. + ([#10885](https://github.com/Microsoft/vscode-python/issues/10885)) +1. Add basic liveshare support for raw kernels. + ([#10988](https://github.com/Microsoft/vscode-python/issues/10988)) +1. Do a one-off transfer of existing values for `python.pythonPath` setting to new Interpreter storage if in DeprecatePythonPath experiment. + ([#11052](https://github.com/Microsoft/vscode-python/issues/11052)) +1. Ensure the language server can query pythonPath when in the Deprecate PythonPath experiment. + ([#11083](https://github.com/Microsoft/vscode-python/issues/11083)) +1. Added prompt asking users to delete `python.pythonPath` key from their workspace settings when in Deprecate PythonPath experiment. + ([#11108](https://github.com/Microsoft/vscode-python/issues/11108)) +1. Added `getDebuggerPackagePath` extension API to get the debugger package path. + ([#11236](https://github.com/Microsoft/vscode-python/issues/11236)) +1. Expose currently selected interpreter path using API. + ([#11294](https://github.com/Microsoft/vscode-python/issues/11294)) +1. Show a prompt asking user to upgrade Code runner to new version to keep using it when in Deprecate PythonPath experiment. + ([#11327](https://github.com/Microsoft/vscode-python/issues/11327)) +1. Rename string `${config:python.pythonPath}` which is used in `launch.json` to refer to interpreter path set in settings, to `${config:python.interpreterPath}`. + ([#11446](https://github.com/Microsoft/vscode-python/issues/11446)) + +### Fixes + +1. Added 'Enable Scrolling For Cell Outputs' setting. Works together with the 'Max Output Size' setting. + ([#9801](https://github.com/Microsoft/vscode-python/issues/9801)) +1. Fix ctrl+enter on markdown cells. Now they render. + ([#10006](https://github.com/Microsoft/vscode-python/issues/10006)) +1. Cancelling the prompt to restart the kernel should not leave the toolbar buttons disabled. + ([#10356](https://github.com/Microsoft/vscode-python/issues/10356)) +1. Getting environment variables of activated environments should ignore the setting `python.terminal.activateEnvironment`. + ([#10370](https://github.com/Microsoft/vscode-python/issues/10370)) +1. Show notebook path when listing remote kernels. + ([#10521](https://github.com/Microsoft/vscode-python/issues/10521)) +1. Allow filtering on '0' for the Data Viewer. + ([#10552](https://github.com/Microsoft/vscode-python/issues/10552)) +1. Allow interrupting the kernel more than once. + ([#10587](https://github.com/Microsoft/vscode-python/issues/10587)) +1. Make error links in exception tracebacks support multiple cells in the stack and extra spaces. + ([#10708](https://github.com/Microsoft/vscode-python/issues/10708)) +1. Add channel property onto returned ZMQ messages. + ([#10785](https://github.com/Microsoft/vscode-python/issues/10785)) +1. Fix problem with shape not being computed for some types in the variable explorer. + ([#10825](https://github.com/Microsoft/vscode-python/issues/10825)) +1. Enable cell related commands when a Python file is already open. + ([#10884](https://github.com/Microsoft/vscode-python/issues/10884)) +1. Fix issue with parsing long conda environment names. + ([#10942](https://github.com/Microsoft/vscode-python/issues/10942)) +1. Hide progress indicator once `Interactive Window` has loaded. + ([#11065](https://github.com/Microsoft/vscode-python/issues/11065)) +1. Do not perform pipenv interpreter discovery on extension activation. + Fix for [CVE-2020-1171](https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2020-1171). + ([#11127](https://github.com/Microsoft/vscode-python/issues/11127)) +1. Ensure arguments are included in log messages when using decorators. + ([#11153](https://github.com/Microsoft/vscode-python/issues/11153)) +1. Fix for opening the interactive window when no workspace is open. + ([#11291](https://github.com/Microsoft/vscode-python/issues/11291)) +1. Conda environments working with raw kernels. + ([#11306](https://github.com/Microsoft/vscode-python/issues/11306)) +1. Ensure isolate script is passed as command argument when installing modules. + ([#11399](https://github.com/Microsoft/vscode-python/issues/11399)) +1. Make raw kernel launch respect launched resource environment. + ([#11451](https://github.com/Microsoft/vscode-python/issues/11451)) +1. When using a kernelspec without a fully qualified python path make sure we use the resource to get the active interpreter. + ([#11469](https://github.com/Microsoft/vscode-python/issues/11469)) +1. For direct kernel launch correctly detect if interpreter has changed since last launch. + ([#11530](https://github.com/Microsoft/vscode-python/issues/11530)) +1. Performance improvements when executing multiple cells in `Notebook` and `Interactive Window`. + ([#11576](https://github.com/Microsoft/vscode-python/issues/11576)) +1. Ensure kernel daemons are disposed correctly when closing notebooks. + ([#11579](https://github.com/Microsoft/vscode-python/issues/11579)) +1. When VS quits, make sure to save contents of notebook for next reopen. + ([#11557](https://github.com/Microsoft/vscode-python/issues/11557)) +1. Fix scrolling when clicking in the interactive window to not jump around. + ([#11554](https://github.com/Microsoft/vscode-python/issues/11554)) +1. Setting "Data Science: Run Startup Commands" is now limited to being a user setting. + Fix for [CVE-2020-1192](https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2020-1192). + +### Code Health + +1. Enable the `Self Cert` tests for Notebooks. + ([#10447](https://github.com/Microsoft/vscode-python/issues/10447)) +1. Remove deprecated telemetry and old way of searching for `Jupyter`. + ([#10809](https://github.com/Microsoft/vscode-python/issues/10809)) +1. Add telemetry for pipenv interpreter discovery. + ([#11128](https://github.com/Microsoft/vscode-python/issues/11128)) +1. Update to the latest version of [`jedi`](https://github.com/davidhalter/jedi) (`0.17`). Note that this may be the last version of Jedi to support Python 2 and Python 3.5. (#11221; thanks Peter Law) + ([#11221](https://github.com/Microsoft/vscode-python/issues/11221)) +1. Lazy load types from `jupyterlab/services` and similar `npm modules`. + ([#11297](https://github.com/Microsoft/vscode-python/issues/11297)) +1. Remove IJMPConnection implementation while maintaining tests written for it. + ([#11470](https://github.com/Microsoft/vscode-python/issues/11470)) +1. Implement an IJupyterVariables provider for the debugger. + ([#11542](https://github.com/Microsoft/vscode-python/issues/11542)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.4.1 (27 April 2020) + +### Fixes + +1. Use node FS APIs when searching for python. This is a temporary change until VSC FS APIs are fixed. + ([#10850](https://github.com/Microsoft/vscode-python/issues/10850)) +1. Show unhandled widget messages in the jupyter output window. + ([#11239](https://github.com/Microsoft/vscode-python/issues/11239)) +1. Warn when using a version of the widget `qgrid` greater than `1.1.1` with the recommendation to downgrade to `1.1.1`. + ([#11245](https://github.com/Microsoft/vscode-python/issues/11245)) +1. Allow user modules import when discovering tests. + ([#11264](https://github.com/Microsoft/vscode-python/issues/11264)) +1. Fix issue where downloading ipywidgets from the CDN might be busy. + ([#11274](https://github.com/Microsoft/vscode-python/issues/11274)) +1. Error: Timeout is shown after running any widget more than once. + ([#11334](https://github.com/Microsoft/vscode-python/issues/11334)) +1. Change "python.dataScience.runStartupCommands" commands to be a global setting, not a workspace setting. + ([#11352](https://github.com/Microsoft/vscode-python/issues/11352)) +1. Closing the interactive window shuts down other active notebook sessions. + ([#11404](https://github.com/Microsoft/vscode-python/issues/11404)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.4.0 (20 April 2020) + +### Enhancements + +1. Add support for `ipywidgets`. + ([#3429](https://github.com/Microsoft/vscode-python/issues/3429)) +1. Support output and interact ipywidgets. + ([#9524](https://github.com/Microsoft/vscode-python/issues/9524)) +1. Support using 'esc' or 'ctrl+u' to clear the contents of the interactive window input box. + ([#10198](https://github.com/Microsoft/vscode-python/issues/10198)) +1. Use new interpreter storage supporting multiroot workspaces when in Deprecate PythonPath experiment. + ([#10325](https://github.com/Microsoft/vscode-python/issues/10325)) +1. Modified `Select interpreter` command to support setting interpreter at workspace level. + ([#10372](https://github.com/Microsoft/vscode-python/issues/10372)) +1. Added a command `Clear Workspace Interpreter Setting` to clear value of Python interpreter from workspace settings. + ([#10374](https://github.com/Microsoft/vscode-python/issues/10374)) +1. Support reverse connection ("listen" in launch.json) from debug adapter to VSCode. + ([#10437](https://github.com/Microsoft/vscode-python/issues/10437)) +1. Use specific icons when downloading MPLS and Insiders instead of the spinner. + ([#10495](https://github.com/Microsoft/vscode-python/issues/10495)) +1. Notebook metadata is now initialized in alphabetical order. + ([#10571](https://github.com/Microsoft/vscode-python/issues/10571)) +1. Added command translations for Hindi Language. + (thanks [Pai026](https://github.com/Pai026/)) + ([#10711](https://github.com/Microsoft/vscode-python/issues/10711)) +1. Prompt when an "untrusted" workspace Python environment is to be auto selected when in Deprecate PythonPath experiment. + ([#10879](https://github.com/Microsoft/vscode-python/issues/10879)) +1. Added a command `Reset stored info for untrusted Interpreters` to reset "untrusted" interpreters storage when in Deprecate PythonPath experiment. + ([#10912](https://github.com/Microsoft/vscode-python/issues/10912)) +1. Added a user setting `python.defaultInterpreterPath` to set up the default interpreter path when in Deprecate PythonPath experiment. + ([#11021](https://github.com/Microsoft/vscode-python/issues/11021)) +1. Hide "untrusted" interpreters from 'Select interpreter' dropdown list when in DeprecatePythonPath Experiment. + ([#11046](https://github.com/Microsoft/vscode-python/issues/11046)) +1. Make spacing of icons on notebook toolbars match spacing on other VS code toolbars. + ([#10464](https://github.com/Microsoft/vscode-python/issues/10464)) +1. Make jupyter server status centered in the UI and use the same font as the rest of VS code. + ([#10465](https://github.com/Microsoft/vscode-python/issues/10465)) +1. Performa validation of interpreter only when a Notebook is opened instead of when extension activates. + ([#10893](https://github.com/Microsoft/vscode-python/issues/10893)) +1. Scrolling in cells doesn't happen on new line. + ([#10952](https://github.com/Microsoft/vscode-python/issues/10952)) +1. Ensure images in workspace folder are supported within markdown cells in a `Notebook`. + ([#11040](https://github.com/Microsoft/vscode-python/issues/11040)) +1. Make sure ipywidgets have a white background so they display in dark themes. + ([#11060](https://github.com/Microsoft/vscode-python/issues/11060)) +1. Arrowing down through cells put the cursor in the wrong spot. + ([#11094](https://github.com/Microsoft/vscode-python/issues/11094)) + +### Fixes + +1. Ensure plot fits within the page of the `PDF`. + ([#9403](https://github.com/Microsoft/vscode-python/issues/9403)) +1. Fix typing in output of cells to not delete or modify any cells. + ([#9519](https://github.com/Microsoft/vscode-python/issues/9519)) +1. Show an error when ipywidgets cannot be found. + ([#9523](https://github.com/Microsoft/vscode-python/issues/9523)) +1. Experiments no longer block on telemetry. + ([#10008](https://github.com/Microsoft/vscode-python/issues/10008)) +1. Fix interactive window debugging after running cells in a notebook. + ([#10206](https://github.com/Microsoft/vscode-python/issues/10206)) +1. Fix problem with Data Viewer not working when builtin functions are overridden (like max). + ([#10280](https://github.com/Microsoft/vscode-python/issues/10280)) +1. Fix interactive window debugging when debugging the first cell to be run. + ([#10395](https://github.com/Microsoft/vscode-python/issues/10395)) +1. Fix interactive window debugging for extra lines in a function. + ([#10396](https://github.com/Microsoft/vscode-python/issues/10396)) +1. Notebook metadata is now initialized in the correct place. + ([#10544](https://github.com/Microsoft/vscode-python/issues/10544)) +1. Fix save button not working on notebooks. + ([#10647](https://github.com/Microsoft/vscode-python/issues/10647)) +1. Fix toolbars on 3rd party widgets to show correct icons. + ([#10734](https://github.com/Microsoft/vscode-python/issues/10734)) +1. Clicking or double clicking in output of a cell selects or gives focus to a cell. It should only affect the controls in the output. + ([#10749](https://github.com/Microsoft/vscode-python/issues/10749)) +1. Fix for notebooks not becoming dirty when changing a kernel. + ([#10795](https://github.com/Microsoft/vscode-python/issues/10795)) +1. Auto save for focusChange is not respected when switching to non text documents. Menu focus will still not cause a save (no callback from VS code for this), but should work for switching to other apps and non text documents. + ([#10853](https://github.com/Microsoft/vscode-python/issues/10853)) +1. Handle display.update inside of cells. + ([#10873](https://github.com/Microsoft/vscode-python/issues/10873)) +1. ZMQ should not cause local server to fail. + ([#10877](https://github.com/Microsoft/vscode-python/issues/10877)) +1. Fixes issue with spaces in debugger paths when using `getRemoteLauncherCommand`. + ([#10905](https://github.com/Microsoft/vscode-python/issues/10905)) +1. Fix output and interact widgets to work again. + ([#10915](https://github.com/Microsoft/vscode-python/issues/10915)) +1. Make sure the same python is used for the data viewer as the notebook so that pandas can be found. + ([#10926](https://github.com/Microsoft/vscode-python/issues/10926)) +1. Ensure user code in cell is preserved between cell execution and cell edits. + ([#10949](https://github.com/Microsoft/vscode-python/issues/10949)) +1. Make sure the interpreter in the notebook matches the kernel. + ([#10953](https://github.com/Microsoft/vscode-python/issues/10953)) +1. Jupyter notebooks and interactive window crashing on startup. + ([#11035](https://github.com/Microsoft/vscode-python/issues/11035)) +1. Fix perf problems after running the interactive window for an extended period of time. + ([#10971](https://github.com/Microsoft/vscode-python/issues/10971)) +1. Fix problem with opening a notebook in jupyter after saving in VS code. + ([#11151](https://github.com/Microsoft/vscode-python/issues/11151)) +1. Fix CTRL+Z and Z for undo on notebooks. + ([#11160](https://github.com/Microsoft/vscode-python/issues/11160)) +1. Fix saving to PDF for viewed plots. + ([#11157](https://github.com/Microsoft/vscode-python/issues/11157)) +1. Fix scrolling in a notebook whenever resizing or opening. + ([#11238](https://github.com/Microsoft/vscode-python/issues/11238)) + +### Code Health + +1. Add conda environments to nightly test runs. + ([#10134](https://github.com/Microsoft/vscode-python/issues/10134)) +1. Refactor the extension activation code to split on phases. + ([#10454](https://github.com/Microsoft/vscode-python/issues/10454)) +1. Added a kernel launcher to spawn python kernels without Jupyter. + ([#10479](https://github.com/Microsoft/vscode-python/issues/10479)) +1. Add ZMQ library to extension. + ([#10483](https://github.com/Microsoft/vscode-python/issues/10483)) +1. Added test harness for `ipywidgets` in `notebooks`. + ([#10655](https://github.com/Microsoft/vscode-python/issues/10655)) +1. Run internal modules and scripts in isolated manner. + This helps avoid problems like shadowing stdlib modules. + ([#10681](https://github.com/Microsoft/vscode-python/issues/10681)) +1. Add telemetry for .env files. + ([#10780](https://github.com/Microsoft/vscode-python/issues/10780)) +1. Update prettier to latest version. + ([#10837](https://github.com/Microsoft/vscode-python/issues/10837)) +1. Update typescript to `3.8`. + ([#10839](https://github.com/Microsoft/vscode-python/issues/10839)) +1. Add telemetry around ipywidgets usage, failures, and overhead. + ([#11027](https://github.com/Microsoft/vscode-python/issues/11027)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.3.2 (2 April 2020) + +### Fixes + +1. Update `debugpy` to latest (v1.0.0b5). Fixes issue with connections with multi-process. + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.3.1 (31 March 2020) + +### Fixes + +1. Update `debugpy` to latest (v1.0.0b4). Fixes issue with locale. + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.3.0 (19 March 2020) + +### Enhancements + +1. Make interactive window wrap like the notebook editor does. + ([#4466](https://github.com/Microsoft/vscode-python/issues/4466)) +1. Support scrolling beyond the last line in the notebook editor and the interactive window. Uses the `editor.scrollBeyondLastLine` setting. + ([#7892](https://github.com/Microsoft/vscode-python/issues/7892)) +1. Allow user to override the arguments passed to Jupyter on startup. To change the arguments, run the 'Python: Specify Jupyter command line arguments" command. + ([#8698](https://github.com/Microsoft/vscode-python/issues/8698)) +1. When entering remote Jupyter Server, default the input value to uri in clipboard. + ([#9163](https://github.com/Microsoft/vscode-python/issues/9163)) +1. Added a command to allow users to select a kernel for a `Notebook`. + ([#9228](https://github.com/Microsoft/vscode-python/issues/9228)) +1. When saving new `notebooks`, default to the current workspace folder. + ([#9331](https://github.com/Microsoft/vscode-python/issues/9331)) +1. When the output of a cell gets trimmed for the first time, the user will be informed of it and which setting changes it. + ([#9401](https://github.com/Microsoft/vscode-python/issues/9401)) +1. Change the parameters for when a Data Science survey prompt comes up. After opening 5 notebooks (ever) or running 100 cells (ever). + ([#10186](https://github.com/Microsoft/vscode-python/issues/10186)) +1. Show quickfixes for launch.json. + ([#10245](https://github.com/Microsoft/vscode-python/issues/10245)) + +### Fixes + +1. Jupyter autocompletion will only show magic commands on empty lines, preventing them of appearing in functions. + ([#10023](https://github.com/Microsoft/vscode-python/issues/10023)) +1. Remove extra lines at the end of the file when formatting with Black. + ([#1877](https://github.com/Microsoft/vscode-python/issues/1877)) +1. Capitalize `Activate.ps1` in code for PowerShell Core on Linux. + ([#2607](https://github.com/Microsoft/vscode-python/issues/2607)) +1. Change interactive window to use the python interpreter associated with the file being run. + ([#3123](https://github.com/Microsoft/vscode-python/issues/3123)) +1. Make line numbers in errors for the Interactive window match the original file and make them clickable for jumping back to an error location. + ([#6370](https://github.com/Microsoft/vscode-python/issues/6370)) +1. Fix magic commands that return 'paged' output. + ([#6900](https://github.com/Microsoft/vscode-python/issues/6900)) +1. Ensure model is updated with user changes after user types into the editor. + ([#8589](https://github.com/Microsoft/vscode-python/issues/8589)) +1. Fix latex output from a code cell to render correctly. + ([#8742](https://github.com/Microsoft/vscode-python/issues/8742)) +1. Toggling cell type from `code` to `markdown` will not set focus to the editor in cells of a `Notebook`. + ([#9102](https://github.com/Microsoft/vscode-python/issues/9102)) +1. Remove whitespace from code before pushing to the interactive window. + ([#9116](https://github.com/Microsoft/vscode-python/issues/9116)) +1. Have sys info show that we have connected to an existing server. + ([#9132](https://github.com/Microsoft/vscode-python/issues/9132)) +1. Fix IPython.clear_output to behave like Jupyter. + ([#9174](https://github.com/Microsoft/vscode-python/issues/9174)) +1. Jupyter output tab was not showing anything when connecting to a remote server. + ([#9177](https://github.com/Microsoft/vscode-python/issues/9177)) +1. Fixed our css generation from custom color themes which caused the Data Viewer to not load. + ([#9242](https://github.com/Microsoft/vscode-python/issues/9242)) +1. Allow a user to skip switching to a kernel if the kernel dies during startup. + ([#9250](https://github.com/Microsoft/vscode-python/issues/9250)) +1. Clean up interative window styling and set focus to input box if clicking in the interactive window. + ([#9282](https://github.com/Microsoft/vscode-python/issues/9282)) +1. Change icon spacing to match vscode icon spacing in native editor toolbars and interactive window toolbar. + ([#9283](https://github.com/Microsoft/vscode-python/issues/9283)) +1. Display diff viewer for `ipynb` files without opening `Notebooks`. + ([#9395](https://github.com/Microsoft/vscode-python/issues/9395)) +1. Python environments will not be activated in terminals hidden from the user. + ([#9503](https://github.com/Microsoft/vscode-python/issues/9503)) +1. Disable `Restart Kernel` and `Interrupt Kernel` buttons when a `kernel` has not yet started. + ([#9731](https://github.com/Microsoft/vscode-python/issues/9731)) +1. Fixed an issue with multiple latex formulas in the same '\$\$' block. + ([#9766](https://github.com/Microsoft/vscode-python/issues/9766)) +1. Make notebook editor and interactive window honor undocumented editor.scrollbar.verticalScrollbarSize option + increase default to match vscode. + ([#9803](https://github.com/Microsoft/vscode-python/issues/9803)) +1. Ensure that invalid kernels don't hang notebook startup or running. + ([#9845](https://github.com/Microsoft/vscode-python/issues/9845)) +1. Switching kernels should disable the run/interrupt/restart buttons. + ([#9935](https://github.com/Microsoft/vscode-python/issues/9935)) +1. Prompt to install `pandas` if not found when opening the `Data Viewer`. + ([#9944](https://github.com/Microsoft/vscode-python/issues/9944)) +1. Prompt to reload VS Code when changing the Jupyter Server connection. + ([#9945](https://github.com/Microsoft/vscode-python/issues/9945)) +1. Support opening spark dataframes in the data viewer. + ([#9959](https://github.com/Microsoft/vscode-python/issues/9959)) +1. Make sure metadata in a cell survives execution. + ([#9997](https://github.com/Microsoft/vscode-python/issues/9997)) +1. Fix run all cells to force each cell to finish before running the next one. + ([#10016](https://github.com/Microsoft/vscode-python/issues/10016)) +1. Fix interrupts from always thinking a restart occurred. + ([#10050](https://github.com/Microsoft/vscode-python/issues/10050)) +1. Do not delay activation of extension by waiting for terminal to get activated. + ([#10094](https://github.com/Microsoft/vscode-python/issues/10094)) +1. LiveShare can prevent the jupyter server from starting if it crashes. + ([#10097](https://github.com/Microsoft/vscode-python/issues/10097)) +1. Mark `poetry.lock` file as toml syntax. + (thanks to [remcohaszing](https://github.com/remcohaszing/)) + ([#10111](https://github.com/Microsoft/vscode-python/issues/10111)) +1. Hide input in `Interactive Window` based on the setting `allowInput`. + ([#10124](https://github.com/Microsoft/vscode-python/issues/10124)) +1. Fix scrolling for output to consistently scroll even during execution. + ([#10137](https://github.com/Microsoft/vscode-python/issues/10137)) +1. Correct image backgrounds for notebook editor. + ([#10154](https://github.com/Microsoft/vscode-python/issues/10154)) +1. Fix empty variables to show an empty string in the Notebook/Interactive Window variable explorer. + ([#10204](https://github.com/Microsoft/vscode-python/issues/10204)) +1. In addition to updating current working directory also add on our notebook file path to sys.path to match Jupyter. + ([#10227](https://github.com/Microsoft/vscode-python/issues/10227)) +1. Ensure message (about trimmed output) displayed in an output cell looks like a link. + ([#10231](https://github.com/Microsoft/vscode-python/issues/10231)) +1. Users can opt into or opt out of experiments in remote scenarios. + ([#10232](https://github.com/Microsoft/vscode-python/issues/10232)) +1. Ensure to correctly return env variables of the activated interpreter, when dealing with non-workspace interpreters. + ([#10250](https://github.com/Microsoft/vscode-python/issues/10250)) +1. Update kernel environments before each run to use the latest environment. Only do this for kernel specs created by the python extension. + ([#10255](https://github.com/Microsoft/vscode-python/issues/10255)) +1. Don't start up and shutdown an extra Jupyter notebook on server startup. + ([#10311](https://github.com/Microsoft/vscode-python/issues/10311)) +1. When you install missing dependencies for Jupyter successfully in an active interpreter also set that interpreter as the Jupyter selected interpreter. + ([#10359](https://github.com/Microsoft/vscode-python/issues/10359)) +1. Ensure default `host` is not set, if `connect` or `listen` settings are available. + ([#10597](https://github.com/Microsoft/vscode-python/issues/10597)) + +### Code Health + +1. Use the new VS Code filesystem API as much as possible. + ([#6911](https://github.com/Microsoft/vscode-python/issues/6911)) +1. Functional tests using real jupyter can take 30-90 seconds each. Most of this time is searching for interpreters. Cache the interpreter search. + ([#7997](https://github.com/Microsoft/vscode-python/issues/7997)) +1. Use Python 3.8 in tests run on Azure DevOps. + ([#8298](https://github.com/Microsoft/vscode-python/issues/8298)) +1. Display `Commands` related to `Interactive Window` and `Notebooks` only when necessary. + ([#8869](https://github.com/Microsoft/vscode-python/issues/8869)) +1. Change cursor styles of buttons `pointer` in `Interactive Window` and `Native Editor`. + ([#9341](https://github.com/Microsoft/vscode-python/issues/9341)) +1. Update Jedi to 0.16.0. + ([#9765](https://github.com/Microsoft/vscode-python/issues/9765)) +1. Update version of `VSCode` in `package.json` to `1.42`. + ([#10046](https://github.com/Microsoft/vscode-python/issues/10046)) +1. Capture `mimetypes` of cell outputs. + ([#10182](https://github.com/Microsoft/vscode-python/issues/10182)) +1. Use debugpy in the core extension instead of ptvsd. + ([#10184](https://github.com/Microsoft/vscode-python/issues/10184)) +1. Add telemetry for imports in notebooks. + ([#10209](https://github.com/Microsoft/vscode-python/issues/10209)) +1. Update data science component to use `debugpy`. + ([#10211](https://github.com/Microsoft/vscode-python/issues/10211)) +1. Use new MacOS VM in Pipelines. + ([#10288](https://github.com/Microsoft/vscode-python/issues/10288)) +1. Split the windows PR tests into two sections so they do not time out. + ([#10293](https://github.com/Microsoft/vscode-python/issues/10293)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.2.3 (21 February 2020) + +### Fixes + +1. Ensure to correctly return env variables of the activated interpreter, when dealing with non-workspace interpreters. + ([#10250](https://github.com/Microsoft/vscode-python/issues/10250)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.2.2 (19 February 2020) + +### Fixes + +1. Improve error messaging when the jupyter notebook cannot be started. + ([#9904](https://github.com/Microsoft/vscode-python/issues/9904)) +1. Clear variables in notebooks and interactive-window when restarting. + ([#9991](https://github.com/Microsoft/vscode-python/issues/9991)) +1. Re-install `Jupyter` instead of installing `kernelspec` if `kernelspec` cannot be found in the python environment. + ([#10071](https://github.com/Microsoft/vscode-python/issues/10071)) +1. Fixes problem with showing ndarrays in the data viewer. + ([#10074](https://github.com/Microsoft/vscode-python/issues/10074)) +1. Fix data viewer not opening on certain data frames. + ([#10075](https://github.com/Microsoft/vscode-python/issues/10075)) +1. Fix svg mimetype so it shows up correctly in richest mimetype order. + ([#10168](https://github.com/Microsoft/vscode-python/issues/10168)) +1. Perf improvements to executing startup code for `Data Science` features when extension loads. + ([#10170](https://github.com/Microsoft/vscode-python/issues/10170)) + +### Code Health + +1. Add telemetry to track notebook languages + ([#9819](https://github.com/Microsoft/vscode-python/issues/9819)) +1. Telemetry around kernels not working and installs not working. + ([#9883](https://github.com/Microsoft/vscode-python/issues/9883)) +1. Change select kernel telemetry to track duration till quick pick appears. + ([#10049](https://github.com/Microsoft/vscode-python/issues/10049)) +1. Track cold/warm times to execute notebook cells. + ([#10176](https://github.com/Microsoft/vscode-python/issues/10176)) +1. Telemetry to capture connections to `localhost` using the connect to remote Jupyter server feature. + ([#10098](https://github.com/Microsoft/vscode-python/issues/10098)) +1. Telemetry to capture perceived startup times of Jupyter and time to execute a cell. + ([#10212](https://github.com/Microsoft/vscode-python/issues/10212)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.2.1 (12 February 2020) + +### Fixes + +1. Re-install `Jupyter` instead of installing `kernelspec` if `kernelspec` cannot be found in the python environment. + ([#10071](https://github.com/Microsoft/vscode-python/issues/10071)) +1. Fix zh-tw localization file loading issue. + (thanks to [ChenKB91](https://github.com/ChenKB91/)) + ([#10072](https://github.com/Microsoft/vscode-python/issues/10072)) + +### Note + +1. Please only set the `python.languageServer` setting if you want to turn IntelliSense off. To switch between language servers, please keep using the `python.jediEnabled` setting for now. + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.2.0 (11 February 2020) + +### Enhancements + +1. Support opting in and out of an experiment group. + ([#6816](https://github.com/Microsoft/vscode-python/issues/6816)) +1. Add `python.languageServer` setting with values of `Jedi` (acts same as `jediEnabled`), + `Microsoft` for the Microsoft Python Language Server and `None`, which suppresses + editor support in the extension so neither Jedi nor Microsoft Python Language Server + start. `None` is useful for those users who prefer using other extensions for the + editor functionality. + ([#7010](https://github.com/Microsoft/vscode-python/issues/7010)) +1. Automatically start the Jupyter server when opening a notebook or the interative window, or when either of those has happened in the last 7 days. This behavior can be disabled with the 'python.dataScience.disableJupyterAutoStart' setting. + ([#7232](https://github.com/Microsoft/vscode-python/issues/7232)) +1. Add support for rendering local images within markdown cells in the `Notebook Editor`. + ([#7704](https://github.com/Microsoft/vscode-python/issues/7704)) +1. Add progress indicator for starting of jupyter with details of each stage. + ([#7868](https://github.com/Microsoft/vscode-python/issues/7868)) +1. Use a dedicated Python Interpreter for starting `Jupyter Notebook Server`. + This can be changed using the command `Select Interpreter to start Jupyter server` from the `Command Palette`. + ([#8623](https://github.com/Microsoft/vscode-python/issues/8623)) +1. Implement pid quick pick for attach cases with the new debug adapter. + ([#8701](https://github.com/Microsoft/vscode-python/issues/8701)) +1. Provide attach to pid configuration via picker. + ([#8702](https://github.com/Microsoft/vscode-python/issues/8702)) +1. Support for custom python debug adapter. + ([#8720](https://github.com/Microsoft/vscode-python/issues/8720)) +1. Remove insiders re-enroll prompt. + ([#8775](https://github.com/Microsoft/vscode-python/issues/8775)) +1. Attach to pid picker - bodyblock users who are not in the new debugger experiment. + ([#8935](https://github.com/Microsoft/vscode-python/issues/8935)) +1. Pass `-y` to `conda installer` to disable the prompt to install, as user has already ok'ed this action. + ([#9194](https://github.com/Microsoft/vscode-python/issues/9194)) +1. Updated `ptvsd` debugger to version v5.0.0a12. + ([#9310](https://github.com/Microsoft/vscode-python/issues/9310)) +1. Use common code to manipulate notebook cells. + ([#9386](https://github.com/Microsoft/vscode-python/issues/9386)) +1. Add support for `Find` in the `Notebook Editor`. + ([#9470](https://github.com/Microsoft/vscode-python/issues/9470)) +1. Update Chinese (Traditional) translation. + (thanks [pan93412](https://github.com/pan93412)) + ([#9548](https://github.com/Microsoft/vscode-python/issues/9548)) +1. Look for Conda interpreters in `~/opt/*conda*/` directory as well. + ([#9701](https://github.com/Microsoft/vscode-python/issues/9701)) + +### Fixes + +1. add --ip=127.0.0.1 argument of jupyter server when running in k8s container + ([#9976](https://github.com/Microsoft/vscode-python/issues/9976)) +1. Correct the server and kernel text for when not connected to a server. + ([#9933](https://github.com/Microsoft/vscode-python/issues/9933)) +1. Make sure to clear variable list on restart kernel. + ([#9740](https://github.com/Microsoft/vscode-python/issues/9740)) +1. Use the autoStart server when available. + ([#9926](https://github.com/Microsoft/vscode-python/issues/9926)) +1. Removed unnecessary warning when executing cells that use Scrapbook, + Fix an html crash when using not supported mime types + ([#9796](https://github.com/microsoft/vscode-python/issues/9796)) +1. Fixed the focus on the interactive window when pressing ctrl + 1/ ctrl + 2 + ([#9693](https://github.com/microsoft/vscode-python/issues/9693)) +1. Fix variable explorer in Interactive and Notebook editors from interfering with execution. + ([#5980](https://github.com/Microsoft/vscode-python/issues/5980)) +1. Fix a crash when using pytest to discover doctests with unknown line number. + (thanks [Olivier Grisel](https://github.com/ogrisel/)) + ([#7487](https://github.com/Microsoft/vscode-python/issues/7487)) +1. Don't show any install product prompts if interpreter is not selected. + ([#7750](https://github.com/Microsoft/vscode-python/issues/7750)) +1. Allow PYTHONWARNINGS to be set and not have it interfere with the launching of Jupyter notebooks. + ([#8496](https://github.com/Microsoft/vscode-python/issues/8496)) +1. Pressing Esc in the config quickpick now cancels debugging. + ([#8626](https://github.com/Microsoft/vscode-python/issues/8626)) +1. Support resolveCompletionItem so that we can get Jedi docstrings in Notebook Editor and Interactive Window. + ([#8706](https://github.com/Microsoft/vscode-python/issues/8706)) +1. Disable interrupt, export, and restart buttons when already performing an interrupt, export, or restart for Notebooks and the Interactive window. + ([#8716](https://github.com/Microsoft/vscode-python/issues/8716)) +1. Icons now cannot be overwritten by styles in cell outputs. + ([#8946](https://github.com/Microsoft/vscode-python/issues/8946)) +1. Command palette (and other keyboard shortcuts) don't work from the Interactive/Notebook editor in the insider's build (or when setting 'useWebViewServer'). + ([#8976](https://github.com/Microsoft/vscode-python/issues/8976)) +1. Fix issue that prevented language server diagnostics from being published. + ([#9096](https://github.com/Microsoft/vscode-python/issues/9096)) +1. Fixed the native editor toolbar so it won't overlap. + ([#9140](https://github.com/Microsoft/vscode-python/issues/9140)) +1. Selectively render output and monaco editor to improve performance. + ([#9204](https://github.com/Microsoft/vscode-python/issues/9204)) +1. Set test debug console default to be `internalConsole`. + ([#9259](https://github.com/Microsoft/vscode-python/issues/9259)) +1. Fix the Data Science "Enable Plot Viewer" setting to pass figure_formats correctly when turned off. + ([#9420](https://github.com/Microsoft/vscode-python/issues/9420)) +1. Shift+Enter can no longer send multiple lines to the interactive window. + ([#9437](https://github.com/Microsoft/vscode-python/issues/9437)) +1. Shift+Enter can no longer run code in the terminal. + ([#9439](https://github.com/Microsoft/vscode-python/issues/9439)) +1. Scrape output to get the details of the registered kernel. + ([#9444](https://github.com/Microsoft/vscode-python/issues/9444)) +1. Update `ptvsd` debugger to version v5.0.0a11. Fixes signing for `inject_dll_x86.exe`. + ([#9474](https://github.com/Microsoft/vscode-python/issues/9474)) +1. Disable use of `conda run`. + ([#9490](https://github.com/Microsoft/vscode-python/issues/9490)) +1. Improvements to responsiveness of code completions in `Notebook` cells and `Interactive Window`. + ([#9494](https://github.com/Microsoft/vscode-python/issues/9494)) +1. Revert changes related to calling `mypy` with relative paths. + ([#9496](https://github.com/Microsoft/vscode-python/issues/9496)) +1. Remove default `pathMappings` for attach to local process by process Id. + ([#9533](https://github.com/Microsoft/vscode-python/issues/9533)) +1. Ensure event handler is bound to the right context. + ([#9539](https://github.com/Microsoft/vscode-python/issues/9539)) +1. Use the correct interpreter when creating the Python execution service used as a fallback by the Daemon. + ([#9566](https://github.com/Microsoft/vscode-python/issues/9566)) +1. Ensure environment variables are always strings in `launch.json`. + ([#9568](https://github.com/Microsoft/vscode-python/issues/9568)) +1. Fix error in developer console about serializing gather rules. + ([#9571](https://github.com/Microsoft/vscode-python/issues/9571)) +1. Do not open the output panel when building workspace symbols. + ([#9603](https://github.com/Microsoft/vscode-python/issues/9603)) +1. Use an activated environment python process to check if modules are installed. + ([#9643](https://github.com/Microsoft/vscode-python/issues/9643)) +1. When hidden 'useWebViewServer' is true, clicking on links in Notebook output don't work. + ([#9645](https://github.com/Microsoft/vscode-python/issues/9645)) +1. Always use latest version of the debugger when building extension. + ([#9652](https://github.com/Microsoft/vscode-python/issues/9652)) +1. Fix background for interactive window copy icon. + ([#9658](https://github.com/Microsoft/vscode-python/issues/9658)) +1. Fix text in markdown cells being lost when clicking away. + ([#9719](https://github.com/Microsoft/vscode-python/issues/9719)) +1. Fix debugging of Interactive Window cells. Don't start up a second notebook at Interactive Window startup. + ([#9780](https://github.com/Microsoft/vscode-python/issues/9780)) +1. When comitting intellisense in Notebook Editor with Jedi place code in correct position. + ([#9857](https://github.com/Microsoft/vscode-python/issues/9857)) +1. Ignore errors coming from stat(), where appropriate. + ([#9901](https://github.com/Microsoft/vscode-python/issues/9901)) + +### Code Health + +1. Use [prettier](https://prettier.io/) as the `TypeScript` formatter and [Black](https://github.com/psf/black) as the `Python` formatter within the extension. + ([#2012](https://github.com/Microsoft/vscode-python/issues/2012)) +1. Use `vanillajs` for build scripts (instead of `typescript`, avoids the step of having to transpile). + ([#5674](https://github.com/Microsoft/vscode-python/issues/5674)) +1. Remove npx from webpack build as it [breaks on windows](https://github.com/npm/npx/issues/5) on npm 6.11+ and doesn't seem to be getting fixes. Update npm to current version. + ([#7197](https://github.com/Microsoft/vscode-python/issues/7197)) +1. Clean up npm dependencies. + ([#8302](https://github.com/Microsoft/vscode-python/issues/8302)) +1. Update version of node to `12.4.0`. + ([#8453](https://github.com/Microsoft/vscode-python/issues/8453)) +1. Use a hidden terminal to retrieve environment variables of an activated Python Interpreter. + ([#8928](https://github.com/Microsoft/vscode-python/issues/8928)) +1. Fix broken LiveShare connect via codewatcher test. + ([#9005](https://github.com/Microsoft/vscode-python/issues/9005)) +1. Refactor `webpack` build scripts to build `DS` bundles using separate config files. + ([#9055](https://github.com/Microsoft/vscode-python/issues/9055)) +1. Change how we handle keyboard input for our functional editor tests. + ([#9084](https://github.com/Microsoft/vscode-python/issues/9084)) +1. Fix working directory path verification for notebook tests. + ([#9191](https://github.com/Microsoft/vscode-python/issues/9191)) +1. Update Jedi to 0.15.2 and parso to 0.5.2. + ([#9243](https://github.com/Microsoft/vscode-python/issues/9243)) +1. Added a test performance measuring pipeline. + ([#9421](https://github.com/Microsoft/vscode-python/issues/9421)) +1. Audit existing telemetry events for datascience or ds_internal. + ([#9626](https://github.com/Microsoft/vscode-python/issues/9626)) +1. CI failure on Data science memoize-one dependency being removed. + ([#9646](https://github.com/Microsoft/vscode-python/issues/9646)) +1. Make sure to check dependencies during PRs. + ([#9714](https://github.com/Microsoft/vscode-python/issues/9714)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.1.0 (6 January 2020) + +### Enhancements + +1. Added experiment for reloading feature of debugging web apps. + ([#3473](https://github.com/Microsoft/vscode-python/issues/3473)) +1. Activate conda environment using path when name is not available. + ([#3834](https://github.com/Microsoft/vscode-python/issues/3834)) +1. Add QuickPick dropdown option _Run All/Debug All_ when clicking on a Code Lens for a parametrized test to be able to run/debug all belonging test variants at once. + (thanks to [Philipp Loose](https://github.com/phloose)) + ([#5608](https://github.com/Microsoft/vscode-python/issues/5608)) +1. Use Octicons in Code Lenses. (thanks [Aidan Dang](https://github.com/AidanGG)) + ([#7192](https://github.com/Microsoft/vscode-python/issues/7192)) +1. Improve startup performance of Jupyter by using a Python daemon. + ([#7242](https://github.com/Microsoft/vscode-python/issues/7242)) +1. Automatically indent following `async for` and `async with` statements. + ([#7344](https://github.com/Microsoft/vscode-python/issues/7344)) +1. Added extension option `activateEnvInCurrentTerminal` to detect if environment should be activated in the current open terminal. + ([#7665](https://github.com/Microsoft/vscode-python/issues/7665)) +1. Add telemetry for usage of activateEnvInCurrentTerminal setting. + ([#8004](https://github.com/Microsoft/vscode-python/issues/8004)) +1. Support multiprocess debugging using the new python debug adapter. + ([#8105](https://github.com/Microsoft/vscode-python/issues/8105)) +1. Support a per interpreter language server so that notebooks that aren't using the currently selected python can still have intellisense. + ([#8206](https://github.com/Microsoft/vscode-python/issues/8206)) +1. Add "processId" key in launch.json to enable attach-to-local-pid scenarios when using the new debug adapter. + ([#8384](https://github.com/Microsoft/vscode-python/issues/8384)) +1. Populate survey links with variables + ([#8484](https://github.com/Microsoft/vscode-python/issues/8484)) +1. Support the ability to take input from users inside of a notebook or the Interactive Window. + ([#8601](https://github.com/Microsoft/vscode-python/issues/8601)) +1. Create an MRU list for Jupyter notebook servers. + ([#8613](https://github.com/Microsoft/vscode-python/issues/8613)) +1. Add icons to the quick pick list for specifying the Jupyter server URI. + ([#8753](https://github.com/Microsoft/vscode-python/issues/8753)) +1. Added kernel status and selection toolbar to the notebook editor. + ([#8866](https://github.com/Microsoft/vscode-python/issues/8866)) +1. Updated `ptvsd` debugger to version v5.0.0a9. + ([#8930](https://github.com/Microsoft/vscode-python/issues/8930)) +1. Add ability to select an existing remote `kernel`. + ([#4644](https://github.com/Microsoft/vscode-python/issues/4644)) +1. Notify user when starting jupyter times out and added `Jupyter` output panel to display output from Jupyter. + ([#9068](https://github.com/Microsoft/vscode-python/issues/9068)) + +### Fixes + +1. Add implementations for `python.workspaceSymbols.rebuildOnStart` and `python.workspaceSymbols.rebuildOnFileSave`. + ([#793](https://github.com/Microsoft/vscode-python/issues/793)) +1. Use relative paths when invoking mypy. + (thanks to [yxliang01](https://github.com/yxliang01)) + ([#5326](https://github.com/Microsoft/vscode-python/issues/5326)) +1. Make the dataviewer open a window much faster. Total load time is the same, but initial response is much faster. + ([#6729](https://github.com/Microsoft/vscode-python/issues/6729)) +1. Make sure the data viewer for notebooks comes up as soon as the user clicks. + ([#6840](https://github.com/Microsoft/vscode-python/issues/6840)) +1. Support saving plotly graphs in the Interactive Window or inside of a notebook. + ([#7221](https://github.com/Microsoft/vscode-python/issues/7221)) +1. Change 0th line in output to 1th in flake8. + (thanks to [Ma007ks](https://github.com/Ma007ks/)) + ([#7349](https://github.com/Microsoft/vscode-python/issues/7349)) +1. Support local images in markdown and output for notebooks. + ([#7704](https://github.com/Microsoft/vscode-python/issues/7704)) +1. Default notebookFileRoot to match the file that a notebook was opened with (or the first file run for the interactive window). + ([#7780](https://github.com/Microsoft/vscode-python/issues/7780)) +1. Execution count and output are cleared from the .ipynb file when the user clicks the 'Clear All Output'. + ([#7853](https://github.com/Microsoft/vscode-python/issues/7853)) +1. Fix clear_output(True) to work in notebook cells. + ([#7970](https://github.com/Microsoft/vscode-python/issues/7970)) +1. Prevented '\$0' from appearing inside brackets when using intellisense autocomplete. + ([#8101](https://github.com/Microsoft/vscode-python/issues/8101)) +1. Intellisense can sometimes not appear in notebooks or the interactive window, especially when something is a large list. + ([#8140](https://github.com/Microsoft/vscode-python/issues/8140)) +1. Correctly update interpreter and kernel info in the metadata. + ([#8223](https://github.com/Microsoft/vscode-python/issues/8223)) +1. Dataframe viewer should use the same interpreter as the active notebook is using. + ([#8227](https://github.com/Microsoft/vscode-python/issues/8227)) +1. 'breakpoint' line shows up in the interactive window when debugging a cell. + ([#8260](https://github.com/Microsoft/vscode-python/issues/8260)) +1. Run above should include all code, and not just cells above. + ([#8403](https://github.com/Microsoft/vscode-python/issues/8403)) +1. Fix issue with test discovery when using `unittest` with `--pattern` flag. + ([#8465](https://github.com/Microsoft/vscode-python/issues/8465)) +1. Set focus to the corresponding `Native Notebook Editor` when opening an `ipynb` file again. + ([#8506](https://github.com/Microsoft/vscode-python/issues/8506)) +1. Fix using all environment variables when running in integrated terminal. + ([#8584](https://github.com/Microsoft/vscode-python/issues/8584)) +1. Fix display of SVG images from previously executed ipynb files. + ([#8600](https://github.com/Microsoft/vscode-python/issues/8600)) +1. Fixes that the test selection drop-down did not open when a code lens for a parameterized test was clicked on windows. + ([#8627](https://github.com/Microsoft/vscode-python/issues/8627)) +1. Changes to how `node-fetch` is bundled in the extension. + ([#8665](https://github.com/Microsoft/vscode-python/issues/8665)) +1. Re-enable support for source-maps. + ([#8686](https://github.com/Microsoft/vscode-python/issues/8686)) +1. Fix order for print/display outputs in a notebook cell. + ([#8739](https://github.com/Microsoft/vscode-python/issues/8739)) +1. Fix scrolling inside of intellisense hover windows for notebooks. + ([#8843](https://github.com/Microsoft/vscode-python/issues/8843)) +1. Fix scrolling in large cells. + ([#8895](https://github.com/Microsoft/vscode-python/issues/8895)) +1. Set `python.workspaceSymbols.enabled` to false by default. + ([#9046](https://github.com/Microsoft/vscode-python/issues/9046)) +1. Add ability to pick a remote kernel. + ([#3763](https://github.com/Microsoft/vscode-python/issues/3763)) +1. Do not set "redirectOutput": true by default when not specified in launch.json, unless "console" is "internalConsole". + ([#8865](https://github.com/Microsoft/vscode-python/issues/8865)) +1. Fix slowdown in Notebook editor caused by using global storage for too much data. + ([#8961](https://github.com/Microsoft/vscode-python/issues/8961)) +1. 'y' and 'm' keys toggle cell type but also add a 'y' or 'm' to the cell. + ([#9078](https://github.com/Microsoft/vscode-python/issues/9078)) +1. Remove unnecessary matplotlib import from first cell. + ([#9099](https://github.com/Microsoft/vscode-python/issues/9099)) +1. Two 'default' options in the select a Jupyter server URI picker. + ([#9101](https://github.com/Microsoft/vscode-python/issues/9101)) +1. Plot viewer never opens. + ([#9114](https://github.com/Microsoft/vscode-python/issues/9114)) +1. Fix color contrast for kernel selection control. + ([#9138](https://github.com/Microsoft/vscode-python/issues/9138)) +1. Disconnect between displayed server and connected server in Kernel selection UI. + ([#9151](https://github.com/Microsoft/vscode-python/issues/9151)) +1. Eliminate extra storage space from global storage on first open of a notebook that had already written to storage. + ([#9159](https://github.com/Microsoft/vscode-python/issues/9159)) +1. Change kernel selection MRU to just save connection time and don't try to connect when popping the list. Plus add unit tests for it. + ([#9171](https://github.com/Microsoft/vscode-python/issues/9171)) + +### Code Health + +1. Re-enable our mac 3.7 debugger tests as a blocking ptvsd issue has been resolved. + ([#6646](https://github.com/Microsoft/vscode-python/issues/6646)) +1. Use "conda run" (instead of using the "python.pythonPath" setting directly) when executing + Python and an Anaconda environment is selected. + ([#7696](https://github.com/Microsoft/vscode-python/issues/7696)) +1. Change state management for react code to use redux. + ([#7949](https://github.com/Microsoft/vscode-python/issues/7949)) +1. Pass resource when accessing VS Code settings. + ([#8001](https://github.com/Microsoft/vscode-python/issues/8001)) +1. Adjust some notebook and interactive window telemetry. + ([#8254](https://github.com/Microsoft/vscode-python/issues/8254)) +1. Added a new telemetry event called `DATASCIENCE.NATIVE.OPEN_NOTEBOOK_ALL` that fires every time the user opens a jupyter notebook by any means. + ([#8262](https://github.com/Microsoft/vscode-python/issues/8262)) +1. Create python daemon for execution of python code. + ([#8451](https://github.com/Microsoft/vscode-python/issues/8451)) +1. Update npm package `https-proxy-agent` by updating the packages that pull it in. + ([#8537](https://github.com/Microsoft/vscode-python/issues/8537)) +1. Improve startup times of unit tests by optionally ignoring some bootstrapping required for `monaco` and `react` tests. + ([#8564](https://github.com/Microsoft/vscode-python/issues/8564)) +1. Skip checking dependencies on CI in PRs. + ([#8840](https://github.com/Microsoft/vscode-python/issues/8840)) +1. Fix installation of sqlite on CI linux machines. + ([#8883](https://github.com/Microsoft/vscode-python/issues/8883)) +1. Fix the "convert to python" functional test failure. + ([#8899](https://github.com/Microsoft/vscode-python/issues/8899)) +1. Remove unused auto-save-enabled telemetry. + ([#8906](https://github.com/Microsoft/vscode-python/issues/8906)) +1. Added ability to wait for completion of the installation of modules. + ([#8952](https://github.com/Microsoft/vscode-python/issues/8952)) +1. Fix failing Data Viewer functional tests. + ([#8992](https://github.com/Microsoft/vscode-python/issues/8992)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.11.1 (22 November 2019) + +### Fixes + +1. Some LaTeX equations do not print in notebooks or the interactive window. + ([#8673](https://github.com/Microsoft/vscode-python/issues/8673)) +1. Converting to python script no longer working from a notebook. + ([#8677](https://github.com/Microsoft/vscode-python/issues/8677)) +1. Fixes to starting `Jupyter` in a `Docker` container. + ([#8661](https://github.com/Microsoft/vscode-python/issues/8661)) +1. Ensure arguments are generated correctly for `getRemoteLauncherCommand` when in debugger experiment. + ([#8685](https://github.com/Microsoft/vscode-python/issues/8685)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.11.0 (18 November 2019) + +### Enhancements + +1. Add Vega support into our list of transforms for output. + ([#4125](https://github.com/Microsoft/vscode-python/issues/4125)) +1. Add `.flake8` file association as ini-file. + (thanks [thernstig](https://github.com/thernstig/)) + ([#6506](https://github.com/Microsoft/vscode-python/issues/6506)) +1. Provide user feedback when searching for a Jupyter server to use and allow the user to cancel this process. + ([#7262](https://github.com/Microsoft/vscode-python/issues/7262)) +1. By default, don't change matplotlib themes and place all plots on a white background regardless of VS Code theme. Add a setting to allow for plots to try to theme. + ([#8000](https://github.com/Microsoft/vscode-python/issues/8000)) +1. Prompt to open exported `Notebook` in the `Notebook Editor`. + ([#8078](https://github.com/Microsoft/vscode-python/issues/8078)) +1. Add commands translation for Farsi locale. + (thanks [Nikronic](https://github.com/Nikronic)) + ([#8092](https://github.com/Microsoft/vscode-python/issues/8092)) +1. Enhance "select a workspace" message when selecting interpreter. + (thanks [Nikolay Kondratyev](https://github.com/kondratyev-nv/)) + ([#8103](https://github.com/Microsoft/vscode-python/issues/8103)) +1. Add logging support for python debug adapter. + ([#8106](https://github.com/Microsoft/vscode-python/issues/8106)) +1. Style adjustments to line numbers (color and width) in the `Native Editor`, to line up with VS Code styles. + ([#8289](https://github.com/Microsoft/vscode-python/issues/8289)) +1. Added command translations for Turkish. + (thanks to [alioguzhan](https://github.com/alioguzhan/)) + ([#8320](https://github.com/Microsoft/vscode-python/issues/8320)) +1. Toolbar was updated to take less space and be reached more easily. + ([#8366](https://github.com/Microsoft/vscode-python/issues/8366)) + +### Fixes + +1. Fix running a unittest file executing only the first test. + (thanks [Nikolay Kondratyev](https://github.com/kondratyev-nv/)) + ([#4567](https://github.com/Microsoft/vscode-python/issues/4567)) +1. Force the pytest root dir to always be the workspace root folder. + ([#6548](https://github.com/Microsoft/vscode-python/issues/6548)) +1. The notebook editor will now treat wrapped lines as different lines, so moving in cells and between cells with the arrow keys (and j and k) will be easier. + ([#7227](https://github.com/Microsoft/vscode-python/issues/7227)) +1. During test discovery, ignore tests generated by pytest plugins (like pep8). + Tests like that were causing discovery to fail. + ([#7287](https://github.com/Microsoft/vscode-python/issues/7287)) +1. When exporting a notebook editor to python script don't use the temp file location for generating the export. + ([#7567](https://github.com/Microsoft/vscode-python/issues/7567)) +1. Unicode symbol used to mark skipped tests was almost not visible on Linux and Windows. + ([#7705](https://github.com/Microsoft/vscode-python/issues/7705)) +1. Editing cells in a notebook, closing VS code, and then reopening will not have the cell content visible. + ([#7754](https://github.com/Microsoft/vscode-python/issues/7754)) +1. Sonar warnings. + ([#7812](https://github.com/Microsoft/vscode-python/issues/7812)) +1. Remove --ci flag from install_ptvsd.py to fix execution of "Setup" instructions from CONTRIBUTING.md. + ([#7814](https://github.com/Microsoft/vscode-python/issues/7814)) +1. Add telemetry for control groups in debug adapter experiments. + ([#7817](https://github.com/Microsoft/vscode-python/issues/7817)) +1. Allow the language server to pick a default caching mode. + ([#7821](https://github.com/Microsoft/vscode-python/issues/7821)) +1. Respect ignoreVSCodeTheme setting and correctly swap icons when changing from light to dark color themes. + ([#7847](https://github.com/Microsoft/vscode-python/issues/7847)) +1. 'Clear All Output' now deletes execution count for all cells. + ([#7853](https://github.com/Microsoft/vscode-python/issues/7853)) +1. If a Jupyter server fails to start, allow user to retry without having to restart VS code. + ([#7865](https://github.com/Microsoft/vscode-python/issues/7865)) +1. Fix strings of commas appearing in text/html output in the notebook editor. + ([#7873](https://github.com/Microsoft/vscode-python/issues/7873)) +1. When creating a new blank notebook, it has existing text in it already. + ([#7980](https://github.com/Microsoft/vscode-python/issues/7980)) +1. Can now include a LaTeX-style equation without surrounding the equation with '\$' in a markdown cell. + ([#7992](https://github.com/Microsoft/vscode-python/issues/7992)) +1. Make a spinner appear during executing a cell. + ([#8003](https://github.com/Microsoft/vscode-python/issues/8003)) +1. Signature help is overflowing out of the signature help widget on the Notebook Editor. + ([#8006](https://github.com/Microsoft/vscode-python/issues/8006)) +1. Ensure intellisense (& similar widgets/popups) are dispaled for one cell in the Notebook editor. + ([#8007](https://github.com/Microsoft/vscode-python/issues/8007)) +1. Correctly restart Jupyter sessions when the active interpreter is changed. + ([#8019](https://github.com/Microsoft/vscode-python/issues/8019)) +1. Clear up wording around jupyterServerURI and remove the quick pick from the flow of setting that. + ([#8021](https://github.com/Microsoft/vscode-python/issues/8021)) +1. Use actual filename comparison for filename equality checks. + ([#8022](https://github.com/Microsoft/vscode-python/issues/8022)) +1. Opening a notebook a second time round with changes (made from another editor) should be preserved. + ([#8025](https://github.com/Microsoft/vscode-python/issues/8025)) +1. Minimize the GPU impact of the interactive window and the notebook editor. + ([#8039](https://github.com/Microsoft/vscode-python/issues/8039)) +1. Store version of the `Python` interpreter (kernel) in the notebook metadata when running cells. + ([#8064](https://github.com/Microsoft/vscode-python/issues/8064)) +1. Make shift+enter not take focus unless about to add a new cell. + ([#8069](https://github.com/Microsoft/vscode-python/issues/8069)) +1. When checking the version of `pandas`, use the same interpreter used to start `Jupyter`. + ([#8084](https://github.com/Microsoft/vscode-python/issues/8084)) +1. Make brackets and paranthesis auto complete in the Notebook Editor and Interactive Window (based on editor settings). + ([#8086](https://github.com/Microsoft/vscode-python/issues/8086)) +1. Cannot create more than one blank notebook. + ([#8132](https://github.com/Microsoft/vscode-python/issues/8132)) +1. Fix for code disappearing after switching between markdown and code in a Notebook Editor. + ([#8141](https://github.com/Microsoft/vscode-python/issues/8141)) +1. Support `⌘+s` keyboard shortcut for saving `Notebooks`. + ([#8151](https://github.com/Microsoft/vscode-python/issues/8151)) +1. Fix closing a Notebook Editor to actually wait for the kernel to restart. + ([#8167](https://github.com/Microsoft/vscode-python/issues/8167)) +1. Inserting a cell in a notebook can sometimes cause the contents to be the cell below it. + ([#8194](https://github.com/Microsoft/vscode-python/issues/8194)) +1. Scroll the notebook editor when giving focus or changing line of a code cell. + ([#8205](https://github.com/Microsoft/vscode-python/issues/8205)) +1. Prevent code from changing in the Notebook Editor while running a cell. + ([#8215](https://github.com/Microsoft/vscode-python/issues/8215)) +1. When updating the Python extension, unsaved changes to notebooks are lost. + ([#8263](https://github.com/Microsoft/vscode-python/issues/8263)) +1. Fix CI to use Python 3.7.5. + ([#8296](https://github.com/Microsoft/vscode-python/issues/8296)) +1. Correctly transition markdown cells into code cells. + ([#8386](https://github.com/Microsoft/vscode-python/issues/8386)) +1. Fix cells being erased when saving and then changing focus to another cell. + ([#8399](https://github.com/Microsoft/vscode-python/issues/8399)) +1. Add a white background for most non-text mimetypes. This lets stuff like Atlair look good in dark mode. + ([#8423](https://github.com/Microsoft/vscode-python/issues/8423)) +1. Export to python button is blue in native editor. + ([#8424](https://github.com/Microsoft/vscode-python/issues/8424)) +1. CTRL+Z is deleting cells. It should only undo changes inside of the code for a cell. 'Z' and 'SHIFT+Z' are for undoing/redoing cell adds/moves. + ([#7999](https://github.com/Microsoft/vscode-python/issues/7999)) +1. Ensure clicking `ctrl+s` in a new `notebook` prompts the user to select a file once instead of twice. + ([#8138](https://github.com/Microsoft/vscode-python/issues/8138)) +1. Creating a new blank notebook should not require a search for jupyter. + ([#8481](https://github.com/Microsoft/vscode-python/issues/8481)) +1. Arrowing up and down through cells can lose code that was just typed. + ([#8491](https://github.com/Microsoft/vscode-python/issues/8491)) +1. After pasting code, arrow keys don't navigate in a cell. + ([#8495](https://github.com/Microsoft/vscode-python/issues/8495)) +1. Typing 'z' in a cell causes the cell to disappear. + ([#8594](https://github.com/Microsoft/vscode-python/issues/8594)) + +### Code Health + +1. Add unit tests for src/client/common/process/pythonProcess.ts. + ([#6065](https://github.com/Microsoft/vscode-python/issues/6065)) +1. Remove try...catch around use of vscode.env.shell. + ([#6912](https://github.com/Microsoft/vscode-python/issues/6912)) +1. Test plan needed to be updated to include support for the Notebook Editor. + ([#7593](https://github.com/Microsoft/vscode-python/issues/7593)) +1. Add test step to get correct pywin32 installed with python 3.6 on windows. + ([#7798](https://github.com/Microsoft/vscode-python/issues/7798)) +1. Update Test Explorer icons to match new VS Code icons. + ([#7809](https://github.com/Microsoft/vscode-python/issues/7809)) +1. Fix native editor mime type functional test. + ([#7877](https://github.com/Microsoft/vscode-python/issues/7877)) +1. Fix variable explorer loading test. + ([#7878](https://github.com/Microsoft/vscode-python/issues/7878)) +1. Add telemetry to capture usage of features in the `Notebook Editor` for `Data Science` features. + ([#7908](https://github.com/Microsoft/vscode-python/issues/7908)) +1. Fix debug temporary functional test for Mac / Linux. + ([#7994](https://github.com/Microsoft/vscode-python/issues/7994)) +1. Variable explorer tests failing on nightly. + ([#8124](https://github.com/Microsoft/vscode-python/issues/8124)) +1. Timeout with new waitForMessage in native editor tests. + ([#8255](https://github.com/Microsoft/vscode-python/issues/8255)) +1. Remove code used to track perf of creation classes. + ([#8280](https://github.com/Microsoft/vscode-python/issues/8280)) +1. Update TypeScript to `3.7`. + ([#8395](https://github.com/Microsoft/vscode-python/issues/8395)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [pyparsing](https://pypi.org/project/pyparsing/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.10.1 (22 October 2019) + +### Enhancements + +1. Support other variables for notebookFileRoot besides ${workspaceRoot}. Specifically allow things like ${fileDirName} so that the dir of the first file run in the interactive window is used for the current directory. + ([#4441](https://github.com/Microsoft/vscode-python/issues/4441)) +1. Add command palette commands for native editor (run all cells, run selected cell, add new cell). And remove interactive window commands from contexts where they don't apply. + ([#7800](https://github.com/Microsoft/vscode-python/issues/7800)) +1. Added ability to auto-save chagnes made to the notebook. + ([#7831](https://github.com/Microsoft/vscode-python/issues/7831)) + +### Fixes + +1. Fix regression to allow connection to servers with no token and no password and add functional test for this scenario + ([#7137](https://github.com/Microsoft/vscode-python/issues/7137)) +1. Perf improvements for opening notebooks with more than 100 cells. + ([#7483](https://github.com/Microsoft/vscode-python/issues/7483)) +1. Fix jupyter server startup hang when xeus-cling kernel is installed. + ([#7569](https://github.com/Microsoft/vscode-python/issues/7569)) +1. Make interactive window and native take their fontSize and fontFamily from the settings in VS Code. + ([#7624](https://github.com/Microsoft/vscode-python/issues/7624)) +1. Fix a hang in the Interactive window when connecting guest to host after the host has already started the interactive window. + ([#7638](https://github.com/Microsoft/vscode-python/issues/7638)) +1. Change the default cell marker to '# %%' instead of '#%%' to prevent linter errors in python files with markers. + Also added a new setting to change this - 'python.dataScience.defaultCellMarker'. + ([#7674](https://github.com/Microsoft/vscode-python/issues/7674)) +1. When there's no workspace open, use the directory of the opened file as the root directory for a jupyter session. + ([#7688](https://github.com/Microsoft/vscode-python/issues/7688)) +1. Fix selection and focus not updating when clicking around in a notebook editor. + ([#7802](https://github.com/Microsoft/vscode-python/issues/7802)) +1. Fix add new cell buttons in the notebook editor to give the new cell focus. + ([#7820](https://github.com/Microsoft/vscode-python/issues/7820)) +1. Do not use the PTVSD package version in the folder name for the wheel experiment. + ([#7836](https://github.com/Microsoft/vscode-python/issues/7836)) +1. Prevent updates to the cell text when cell execution of the same cell has commenced or completed. + ([#7844](https://github.com/Microsoft/vscode-python/issues/7844)) +1. Hide the parameters intellisense widget in the `Notebook Editor` when it is not longer required. + ([#7851](https://github.com/Microsoft/vscode-python/issues/7851)) +1. Allow the "Create New Blank Jupyter Notebook" command to be run when the python extension is not loaded yet. + ([#7888](https://github.com/Microsoft/vscode-python/issues/7888)) +1. Ensure the `*.trie` files related to `font kit` npm module are copied into the output directory as part of the `Webpack` bundling operation. + ([#7899](https://github.com/Microsoft/vscode-python/issues/7899)) +1. CTRL+S is not saving a Notebook file. + ([#7904](https://github.com/Microsoft/vscode-python/issues/7904)) +1. When automatically opening the `Notebook Editor`, then ignore uris that do not have a `file` scheme + ([#7905](https://github.com/Microsoft/vscode-python/issues/7905)) +1. Minimize the changes to an ipynb file when saving - preserve metadata and spacing. + ([#7960](https://github.com/Microsoft/vscode-python/issues/7960)) +1. Fix intellisense popping up in the wrong spot when first typing in a cell. + ([#8009](https://github.com/Microsoft/vscode-python/issues/8009)) +1. Fix python.dataScience.maxOutputSize to be honored again. + ([#8010](https://github.com/Microsoft/vscode-python/issues/8010)) +1. Fix markdown disappearing after editing and hitting the escape key. + ([#8045](https://github.com/Microsoft/vscode-python/issues/8045)) + +### Code Health + +1. Add functional tests for notebook editor's use of the variable list. + ([#7369](https://github.com/Microsoft/vscode-python/issues/7369)) +1. More functional tests for the notebook editor. + ([#7372](https://github.com/Microsoft/vscode-python/issues/7372)) +1. Update version of `@types/vscode`. + ([#7832](https://github.com/Microsoft/vscode-python/issues/7832)) +1. Use `Webview.asWebviewUri` to generate a URI for use in the `Webview Panel` instead of hardcoding the resource `vscode-resource`. + ([#7834](https://github.com/Microsoft/vscode-python/issues/7834)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.10.0 (8 October 2019) + +### Enhancements + +1. Experimental version of a native editor for ipynb files. + ([#5959](https://github.com/Microsoft/vscode-python/issues/5959)) +1. Added A/A testing. + ([#6793](https://github.com/Microsoft/vscode-python/issues/6793)) +1. Opt insiders users into beta language server by default. + ([#7108](https://github.com/Microsoft/vscode-python/issues/7108)) +1. Add basic liveshare support for native. + ([#7235](https://github.com/Microsoft/vscode-python/issues/7235)) +1. Change main toolbar to match design spec. + ([#7240](https://github.com/Microsoft/vscode-python/issues/7240)) +1. Telemetry for native editor support. + ([#7252](https://github.com/Microsoft/vscode-python/issues/7252)) +1. Change Variable Explorer to use a sticky button on the main toolbar. + ([#7354](https://github.com/Microsoft/vscode-python/issues/7354)) +1. Add left side navigation bar to native editor. + ([#7377](https://github.com/Microsoft/vscode-python/issues/7377)) +1. Add middle toolbar to a native editor cell. + ([#7378](https://github.com/Microsoft/vscode-python/issues/7378)) +1. Indented the status bar for outputs and changed the background color in the native editor. + ([#7379](https://github.com/Microsoft/vscode-python/issues/7379)) +1. Added a setting `python.experiments.enabled` to enable/disable A/B tests within the extension. + ([#7410](https://github.com/Microsoft/vscode-python/issues/7410)) +1. Add a play button for all users. + ([#7423](https://github.com/Microsoft/vscode-python/issues/7423)) +1. Add a command to show the `Language Server` output panel. + ([#7459](https://github.com/Microsoft/vscode-python/issues/7459)) +1. Make empty notebooks (from File | New File) contain at least one cell. + ([#7516](https://github.com/Microsoft/vscode-python/issues/7516)) +1. Add "clear all output" button to native editor. + ([#7517](https://github.com/Microsoft/vscode-python/issues/7517)) +1. Add support for ptvsd and debug adapter experiments in remote debugging API. + ([#7549](https://github.com/Microsoft/vscode-python/issues/7549)) +1. Support other variables for `notebookFileRoot` besides `${workspaceRoot}`. Specifically allow things like `${fileDirName}` so that the directory of the first file run in the interactive window is used for the current directory. + ([#4441](https://github.com/Microsoft/vscode-python/issues/4441)) + +### Fixes + +1. Replaced occurrences of `pep8` with `pycodestyle.` + All mentions of pep8 have been replaced with pycodestyle. + Add script to replace outdated settings with the new ones in user settings.json + - python.linting.pep8Args -> python.linting.pycodestyleArgs + - python.linting.pep8CategorySeverity.E -> python.linting.pycodestyleCategorySeverity.E + - python.linting.pep8CategorySeverity.W -> python.linting.pycodestyleCategorySeverity.W + - python.linting.pep8Enabled -> python.linting.pycodestyleEnabled + - python.linting.pep8Path -> python.linting.pycodestylePath + - (thanks [Marsfan](https://github.com/Marsfan)) + ([#410](https://github.com/Microsoft/vscode-python/issues/410)) +1. Do not change `foreground` colors in test statusbar. + ([#4387](https://github.com/Microsoft/vscode-python/issues/4387)) +1. Set the `__file__` variable whenever running code so that `__file__` usage works in the interactive window. + ([#5459](https://github.com/Microsoft/vscode-python/issues/5459)) +1. Ensure Windows Store install of Python is displayed in the statusbar. + ([#5926](https://github.com/Microsoft/vscode-python/issues/5926)) +1. Fix loging for determining python path from workspace of active text editor (thanks [Eric Bajumpaa (@SteelPhase)](https://github.com/SteelPhase)). + ([#6282](https://github.com/Microsoft/vscode-python/issues/6282)) +1. Changed the way scrolling is treated. Now we only check for the position of the scroll, the size of the cell won't matter. + Still the interactive window will snap to the bottom if you already are at the bottom, and will stay in place if you are not. Like a chat window. + Tested to work with: + - regular code + - dataframes + - big and regular plots + Turned the check of the scroll at the bottom from checking equal to checking a range to make it work with fractions. + ([#6580](https://github.com/Microsoft/vscode-python/issues/6580)) +1. Changed the name of the setting 'Run Magic Commands' to 'Run Startup Commands' to avoid confusion. + ([#6842](https://github.com/Microsoft/vscode-python/issues/6842)) +1. Fix the debugger being installed even when available from the VSCode install. + ([#6907](https://github.com/Microsoft/vscode-python/issues/6907)) +1. Fixes to detection of shell. + ([#6928](https://github.com/Microsoft/vscode-python/issues/6928)) +1. Delete the old session immediately after session restart instead of on close. + ([#6975](https://github.com/Microsoft/vscode-python/issues/6975)) +1. Add support for the new JUnit XML format used by pytest 5.1+. + ([#6990](https://github.com/Microsoft/vscode-python/issues/6990)) +1. Set a content security policy on webviews. + ([#7007](https://github.com/Microsoft/vscode-python/issues/7007)) +1. Fix regression to allow connection to servers with no token and no password and add functional test for this scenario. + ([#7137](https://github.com/Microsoft/vscode-python/issues/7137)) +1. Resolve variables such as `${workspaceFolder}` in the `envFile` setting of `launch.json`. + ([#7210](https://github.com/Microsoft/vscode-python/issues/7210)) +1. Fixed A/B testing sampling. + ([#7218](https://github.com/Microsoft/vscode-python/issues/7218)) +1. Added commands for 'dd', 'ctrl + enter', 'alt + enter', 'a', 'b', 'j', 'k' in the native Editor to behave just like JupyterLabs. + ([#7229](https://github.com/Microsoft/vscode-python/issues/7229)) +1. Add support for CTRL+S when the native editor has input focus (best we can do without true editor support) + Also fix issue with opening two or more not gaining focus correctly. + ([#7238](https://github.com/Microsoft/vscode-python/issues/7238)) +1. Fix monaco editor layout perf. + ([#7241](https://github.com/Microsoft/vscode-python/issues/7241)) +1. Fix 'history' in the input box for the interactive window to work again. Up arrow and down arrow should now scroll through the things already typed in. + ([#7253](https://github.com/Microsoft/vscode-python/issues/7253)) +1. Fix plot viewer to allow exporting again. + ([#7257](https://github.com/Microsoft/vscode-python/issues/7257)) +1. Make ipynb files auto save on shutting down VS code as our least bad option at the moment. + ([#7258](https://github.com/Microsoft/vscode-python/issues/7258)) +1. Update icons to newer look. + ([#7261](https://github.com/Microsoft/vscode-python/issues/7261)) +1. The native editor will now wrap all its content instead of showing a horizontal scrollbar. + ([#7272](https://github.com/Microsoft/vscode-python/issues/7272)) +1. Deprecate the 'runMagicCommands' datascience setting. + ([#7294](https://github.com/Microsoft/vscode-python/issues/7294)) +1. Fix white icon background and finish update all icons to new style. + ([#7302](https://github.com/Microsoft/vscode-python/issues/7302)) +1. Fixes to display `Python` specific debug configurations in `launch.json`. + ([#7304](https://github.com/Microsoft/vscode-python/issues/7304)) +1. Fixed intellisense support on the native editor. + ([#7316](https://github.com/Microsoft/vscode-python/issues/7316)) +1. Fix double opening an ipynb file to still use the native editor. + ([#7318](https://github.com/Microsoft/vscode-python/issues/7318)) +1. 'j' and 'k' were reversed for navigating through the native editor. + ([#7330](https://github.com/Microsoft/vscode-python/issues/7330)) +1. 'a' keyboard shortcut doesn't add a cell above if current cell is the first. + ([#7334](https://github.com/Microsoft/vscode-python/issues/7334)) +1. Add the 'add cell' line between cells, on cells, and at the bottom and top. + ([#7362](https://github.com/Microsoft/vscode-python/issues/7362)) +1. Runtime errors cause the run button to disappear. + ([#7370](https://github.com/Microsoft/vscode-python/issues/7370)) +1. Surface jupyter notebook search errors to the user. + ([#7392](https://github.com/Microsoft/vscode-python/issues/7392)) +1. Allow cells to be re-executed on second open of an ipynb file. + ([#7417](https://github.com/Microsoft/vscode-python/issues/7417)) +1. Implement dirty file tracking for notebooks so that on reopening of VS code they are shown in the dirty state. + Canceling the save will get them back to their on disk state. + ([#7418](https://github.com/Microsoft/vscode-python/issues/7418)) +1. Make ipynb files change to dirty when moving/deleting/changing cells. + ([#7439](https://github.com/Microsoft/vscode-python/issues/7439)) +1. Initial collapse / expand state broken by native liveshare work / gather. + ([#7445](https://github.com/Microsoft/vscode-python/issues/7445)) +1. Converting a native markdown cell to code removes the markdown source. + ([#7446](https://github.com/Microsoft/vscode-python/issues/7446)) +1. Text is cut off on the right hand side of a notebook editor. + ([#7472](https://github.com/Microsoft/vscode-python/issues/7472)) +1. Added a prompt asking users to enroll back in the insiders program. + ([#7473](https://github.com/Microsoft/vscode-python/issues/7473)) +1. Fix collapse bar and add new line spacing for the native editor. + ([#7489](https://github.com/Microsoft/vscode-python/issues/7489)) +1. Add new cell top most toolbar button should take selection into account when adding a cell. + ([#7490](https://github.com/Microsoft/vscode-python/issues/7490)) +1. Move up and move down arrows in native editor are different sizes. + ([#7494](https://github.com/Microsoft/vscode-python/issues/7494)) +1. Fix jedi intellisense in the notebook editor to be performant. + ([#7497](https://github.com/Microsoft/vscode-python/issues/7497)) +1. The add cell line should have a hover cursor. + ([#7508](https://github.com/Microsoft/vscode-python/issues/7508)) +1. Toolbar in the middle of a notebook cell should show up on hover. + ([#7515](https://github.com/Microsoft/vscode-python/issues/7515)) +1. 'z' key will now undo cell deletes/adds/moves. + ([#7518](https://github.com/Microsoft/vscode-python/issues/7518)) +1. Rename and restyle the save as python file button. + ([#7519](https://github.com/Microsoft/vscode-python/issues/7519)) +1. Fix for changing a file in the status bar to a notebook/jupyter file to open the new native notebook editor. + ([#7521](https://github.com/Microsoft/vscode-python/issues/7521)) +1. Running a cell by clicking the mouse should behave like shift+enter and move to the next cell (or add one to the bottom). + ([#7522](https://github.com/Microsoft/vscode-python/issues/7522)) +1. Output color makes a text only notebook with a lot of cells hard to read. Change output color to be the same as the background like Jupyter does. + ([#7526](https://github.com/Microsoft/vscode-python/issues/7526)) +1. Fix data viewer sometimes showing no data at all (especially on small datasets). + ([#7530](https://github.com/Microsoft/vscode-python/issues/7530)) +1. First run of run all cells doesn't run the first cell first. + ([#7558](https://github.com/Microsoft/vscode-python/issues/7558)) +1. Saving an untitled notebook editor doesn't change the tab to have the new file name. + ([#7561](https://github.com/Microsoft/vscode-python/issues/7561)) +1. Closing and reopening a notebook doesn't reset the execution count. + ([#7565](https://github.com/Microsoft/vscode-python/issues/7565)) +1. After restarting kernel, variables don't reset in the notebook editor. + ([#7573](https://github.com/Microsoft/vscode-python/issues/7573)) +1. CTRL+1/CTRL+2 had stopped working in the interactive window. + ([#7597](https://github.com/Microsoft/vscode-python/issues/7597)) +1. Ensure the insiders prompt only shows once. + ([#7606](https://github.com/Microsoft/vscode-python/issues/7606)) +1. Added prompt to flip "inheritEnv" setting to false to fix conda activation issue. + ([#7607](https://github.com/Microsoft/vscode-python/issues/7607)) +1. Toggling line numbers and output was not possible in the notebook editor. + ([#7610](https://github.com/Microsoft/vscode-python/issues/7610)) +1. Align execution count with first line of a cell. + ([#7611](https://github.com/Microsoft/vscode-python/issues/7611)) +1. Fix debugging cells to work when the python executable has spaces in the path. + ([#7627](https://github.com/Microsoft/vscode-python/issues/7627)) +1. Add switch channel commands into activationEvents to fix `command 'Python.swichToDailyChannel' not found`. + ([#7636](https://github.com/Microsoft/vscode-python/issues/7636)) +1. Goto cell code lens was not scrolling. + ([#7639](https://github.com/Microsoft/vscode-python/issues/7639)) +1. Make interactive window and native take their `fontSize` and `fontFamily` from the settings in VS Code. + ([#7624](https://github.com/Microsoft/vscode-python/issues/7624)) +1. Fix a hang in the Interactive window when connecting guest to host after the host has already started the interactive window. + ([#7638](https://github.com/Microsoft/vscode-python/issues/7638)) +1. When there's no workspace open, use the directory of the opened file as the root directory for a Jupyter session. + ([#7688](https://github.com/Microsoft/vscode-python/issues/7688)) +1. Allow the language server to pick a default caching mode. + ([#7821](https://github.com/Microsoft/vscode-python/issues/7821)) + +### Code Health + +1. Use jsonc-parser instead of strip-json-comments. + (thanks [Mikhail Bulash](https://github.com/mikeroll/)) + ([#4819](https://github.com/Microsoft/vscode-python/issues/4819)) +1. Remove `donjamayanne.jupyter` integration. + (thanks [Mikhail Bulash](https://github.com/mikeroll/)) + ([#6052](https://github.com/Microsoft/vscode-python/issues/6052)) +1. Drop `python.updateSparkLibrary` command. + (thanks [Mikhail Bulash](https://github.com/mikeroll/)) + ([#6091](https://github.com/Microsoft/vscode-python/issues/6091)) +1. Re-enabled smoke tests (refactored in `node.js` with [puppeteer](https://github.com/GoogleChrome/puppeteer)). + ([#6511](https://github.com/Microsoft/vscode-python/issues/6511)) +1. Handle situations where language client is disposed earlier than expected. + ([#6865](https://github.com/Microsoft/vscode-python/issues/6865)) +1. Put Data science functional tests that use real jupyter into their own test pipeline. + ([#7066](https://github.com/Microsoft/vscode-python/issues/7066)) +1. Send telemetry for what language server is chosen. + ([#7109](https://github.com/Microsoft/vscode-python/issues/7109)) +1. Add telemetry to measure debugger start up performance. + ([#7332](https://github.com/Microsoft/vscode-python/issues/7332)) +1. Decouple the DS location tracker from the debug session telemetry. + ([#7352](https://github.com/Microsoft/vscode-python/issues/7352)) +1. Test scaffolding for notebook editor. + ([#7367](https://github.com/Microsoft/vscode-python/issues/7367)) +1. Add functional tests for notebook editor's use of the variable list. + ([#7369](https://github.com/Microsoft/vscode-python/issues/7369)) +1. Tests for the notebook editor for different mime types. + ([#7371](https://github.com/Microsoft/vscode-python/issues/7371)) +1. Split Cell class for different views. + ([#7376](https://github.com/Microsoft/vscode-python/issues/7376)) +1. Refactor Azure Pipelines to use stages. + ([#7431](https://github.com/Microsoft/vscode-python/issues/7431)) +1. Add unit tests to guarantee that the extension version in the main branch has the '-dev' suffix. + ([#7471](https://github.com/Microsoft/vscode-python/issues/7471)) +1. Add a smoke test for the `Interactive Window`. + ([#7653](https://github.com/Microsoft/vscode-python/issues/7653)) +1. Download PTVSD wheels (for the new PTVSD) as part of CI. + ([#7028](https://github.com/Microsoft/vscode-python/issues/7028)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.9.1 (6 September 2019) + +### Fixes + +1. Fixes to automatic scrolling on the interactive window. + ([#6580](https://github.com/Microsoft/vscode-python/issues/6580)) + +## 2019.9.0 (3 September 2019) + +### Enhancements + +1. Get "select virtual environment for the workspace" prompt to show up regardless of pythonpath setting. + ([#5499](https://github.com/Microsoft/vscode-python/issues/5499)) +1. Changes to telemetry with regards to discovery of python environments. + ([#5593](https://github.com/Microsoft/vscode-python/issues/5593)) +1. Update Jedi to 0.15.1 and parso to 0.5.1. + ([#6294](https://github.com/Microsoft/vscode-python/issues/6294)) +1. Moved Language Server logging to its own output channel. + ([#6559](https://github.com/Microsoft/vscode-python/issues/6559)) +1. Interactive window will only snap to the bottom if the user is already in the bottom, like a chat window. + ([#6580](https://github.com/Microsoft/vscode-python/issues/6580)) +1. Add debug command code lenses when in debug mode. + ([#6672](https://github.com/Microsoft/vscode-python/issues/6672)) +1. Implemented prompt for survey. + ([#6752](https://github.com/Microsoft/vscode-python/issues/6752)) +1. Add code gathering tools. + ([#6810](https://github.com/Microsoft/vscode-python/issues/6810)) +1. Added a setting called 'Run Magic Commands'. The input should be python code to be executed when the interactive window is loading. + ([#6842](https://github.com/Microsoft/vscode-python/issues/6842)) +1. Added a setting so the user can decide if they want the debugger to debug only their code, or also debug external libraries. + ([#6870](https://github.com/Microsoft/vscode-python/issues/6870)) +1. Implemented prompt for survey using A/B test framework. + ([#6957](https://github.com/Microsoft/vscode-python/issues/6957)) + +### Fixes + +1. Delete the old session immediatly after session restart instead of on close + ([#6975](https://github.com/Microsoft/vscode-python/issues/6975)) +1. Add support for the "pathMappings" setting in "launch" debug configs. + ([#3568](https://github.com/Microsoft/vscode-python/issues/3568)) +1. Supports error codes like ABC123 as used in plugins. + ([#4074](https://github.com/Microsoft/vscode-python/issues/4074)) +1. Fixes to insertion of commas when inserting generated debug configurations in `launch.json`. + ([#5531](https://github.com/Microsoft/vscode-python/issues/5531)) +1. Fix code lenses shown for pytest. + ([#6303](https://github.com/Microsoft/vscode-python/issues/6303)) +1. Make data viewer change row height according to font size in settings. + ([#6614](https://github.com/Microsoft/vscode-python/issues/6614)) +1. Fix miniconda environments to work. + ([#6802](https://github.com/Microsoft/vscode-python/issues/6802)) +1. Drop dedent-on-enter for "return" statements. It will be addressed in https://github.com/microsoft/vscode-python/issues/6564. + ([#6813](https://github.com/Microsoft/vscode-python/issues/6813)) +1. Show PTVSD exceptions to the user. + ([#6818](https://github.com/Microsoft/vscode-python/issues/6818)) +1. Tweaked message for restarting VS Code to use a Python Extension insider build + (thanks [Marsfan](https://github.com/Marsfan)). + ([#6838](https://github.com/Microsoft/vscode-python/issues/6838)) +1. Do not execute empty code cells or render them in the interactive window when sent from the editor or input box. + ([#6839](https://github.com/Microsoft/vscode-python/issues/6839)) +1. Fix failing functional tests (for pytest) in the extension. + ([#6940](https://github.com/Microsoft/vscode-python/issues/6940)) +1. Fix ptvsd typo in descriptions. + ([#7097](https://github.com/Microsoft/vscode-python/issues/7097)) + +### Code Health + +1. Update the message and the link displayed when `Language Server` isn't supported. + ([#5969](https://github.com/Microsoft/vscode-python/issues/5969)) +1. Normalize path separators in stack traces. + ([#6460](https://github.com/Microsoft/vscode-python/issues/6460)) +1. Update `package.json` to define supported languages for breakpoints. + Update telemetry code to hardcode Telemetry Key in code (removed from `package.json`). + ([#6469](https://github.com/Microsoft/vscode-python/issues/6469)) +1. Functional tests for DataScience Error Handler. + ([#6697](https://github.com/Microsoft/vscode-python/issues/6697)) +1. Move .env file handling into the extension. This is in preparation to switch to the out-of-proc debug adapter from ptvsd. + ([#6770](https://github.com/Microsoft/vscode-python/issues/6770)) +1. Track enablement of a test framework. + ([#6783](https://github.com/Microsoft/vscode-python/issues/6783)) +1. Track how code was sent to the terminal (via `command` or `UI`). + ([#6801](https://github.com/Microsoft/vscode-python/issues/6801)) +1. Upload coverage reports to [codecov](https://codecov.io/gh/microsoft/vscode-python). + ([#6938](https://github.com/Microsoft/vscode-python/issues/6938)) +1. Bump version of [PTVSD](https://pypi.org/project/ptvsd/) to `4.3.2`. + + - Fix an issue with Jump to cursor command. [#1667](https://github.com/microsoft/ptvsd/issues/1667) + - Fix "Unable to find threadStateIndex for the current thread" message in terminal. [#1587](https://github.com/microsoft/ptvsd/issues/1587) + - Fixes crash when using python 3.7.4. [#1688](https://github.com/microsoft/ptvsd/issues/1688) + ([#6961](https://github.com/Microsoft/vscode-python/issues/6961)) + +1. Move nightly functional tests to use mock jupyter and create a new pipeline for flakey tests which use real jupyter. + ([#7066](https://github.com/Microsoft/vscode-python/issues/7066)) +1. Corrected spelling of name for method to be `hasConfigurationFileInWorkspace`. + ([#7072](https://github.com/Microsoft/vscode-python/issues/7072)) +1. Fix functional test failures due to new WindowsStoreInterpreter addition. + ([#7081](https://github.com/Microsoft/vscode-python/issues/7081)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.8.0 (6 August 2019) + +### Enhancements + +1. Added ability to auto update Insiders build of extension. + ([#2772](https://github.com/Microsoft/vscode-python/issues/2772)) +1. Add an icon for the "Run Python File in Terminal" command. + ([#5321](https://github.com/Microsoft/vscode-python/issues/5321)) +1. Hook up ptvsd debugger to Jupyter UI. + ([#5900](https://github.com/Microsoft/vscode-python/issues/5900)) +1. Improved keyboard and screen reader support for the data explorer. + ([#6019](https://github.com/Microsoft/vscode-python/issues/6019)) +1. Provide code mapping service for debugging cells. + ([#6318](https://github.com/Microsoft/vscode-python/issues/6318)) +1. Change copy back to code button in the interactive window to insert wherever the current selection is. + ([#6350](https://github.com/Microsoft/vscode-python/issues/6350)) +1. Add new 'goto cell' code lens on every cell that is run from a file. + ([#6359](https://github.com/Microsoft/vscode-python/issues/6359)) +1. Allow for cancelling all cells when an error occurs. Backed by 'stopOnError' setting. + ([#6366](https://github.com/Microsoft/vscode-python/issues/6366)) +1. Added Code Lens and Snippet to add new cell. + ([#6367](https://github.com/Microsoft/vscode-python/issues/6367)) +1. Support hitting breakpoints in actual source code for interactive window debugging. + ([#6376](https://github.com/Microsoft/vscode-python/issues/6376)) +1. Give the option to install ptvsd if user is missing it and tries to debug. + ([#6378](https://github.com/Microsoft/vscode-python/issues/6378)) +1. Add support for remote debugging of Jupyter cells. + ([#6379](https://github.com/Microsoft/vscode-python/issues/6379)) +1. Make the input box more visible to new users. + ([#6381](https://github.com/Microsoft/vscode-python/issues/6381)) +1. Add feature flag `python.dataScience.magicCommandsAsComments` so linters and other tools can work with them. + (thanks [Janosh Riebesell](https://github.com/janosh)) + ([#6408](https://github.com/Microsoft/vscode-python/issues/6408)) +1. Support break on enter for debugging a cell. + ([#6449](https://github.com/Microsoft/vscode-python/issues/6449)) +1. instead of asking the user to select an installer, we now autodetect the environment being used, and use that installer. + ([#6569](https://github.com/Microsoft/vscode-python/issues/6569)) +1. Remove "Debug cell" action from data science code lenses for markdown cells. + (thanks [Janosh Riebesell](https://github.com/janosh)) + ([#6588](https://github.com/Microsoft/vscode-python/issues/6588)) +1. Add debug command code lenses when in debug mode + ([#6672](https://github.com/Microsoft/vscode-python/issues/6672)) + +### Fixes + +1. Fix `executeInFileDir` for when a file is not in a workspace. + (thanks [Bet4](https://github.com/bet4it/)) + ([#1062](https://github.com/Microsoft/vscode-python/issues/1062)) +1. Fix indentation after string literals containing escaped characters. + ([#4241](https://github.com/Microsoft/vscode-python/issues/4241)) +1. The extension will now prompt to auto install jupyter in case its not found. + ([#5682](https://github.com/Microsoft/vscode-python/issues/5682)) +1. Append `--allow-prereleases` to black installation command so pipenv can properly resolve it. + ([#5756](https://github.com/Microsoft/vscode-python/issues/5756)) +1. Remove existing positional arguments when running single pytest tests. + ([#5757](https://github.com/Microsoft/vscode-python/issues/5757)) +1. Fix shift+enter to work when code lens are turned off. + ([#5879](https://github.com/Microsoft/vscode-python/issues/5879)) +1. Prompt to insall test framework only if test frame is not already installed. + ([#5919](https://github.com/Microsoft/vscode-python/issues/5919)) +1. Trim stream text output at the server to prevent sending massive strings of overwritten data. + ([#6001](https://github.com/Microsoft/vscode-python/issues/6001)) +1. Detect `shell` in Visual Studio Code using the Visual Studio Code API. + ([#6050](https://github.com/Microsoft/vscode-python/issues/6050)) +1. Make long running output not crash the extension host. Also improve perf of streaming. + ([#6222](https://github.com/Microsoft/vscode-python/issues/6222)) +1. Opting out of telemetry correctly opts out of A/B testing. + ([#6270](https://github.com/Microsoft/vscode-python/issues/6270)) +1. Add error messages if data_rate_limit is exceeded on remote (or local) connection. + ([#6273](https://github.com/Microsoft/vscode-python/issues/6273)) +1. Add pytest-xdist's -n option to the list of supported pytest options. + ([#6293](https://github.com/Microsoft/vscode-python/issues/6293)) +1. Simplify the import regex to minimize performance overhead. + ([#6319](https://github.com/Microsoft/vscode-python/issues/6319)) +1. Clarify regexes used for decreasing indentation. + ([#6333](https://github.com/Microsoft/vscode-python/issues/6333)) +1. Add new plot viewer button images and fix button colors in different themes. + ([#6336](https://github.com/Microsoft/vscode-python/issues/6336)) +1. Update telemetry property name for Jedi memory usage. + ([#6339](https://github.com/Microsoft/vscode-python/issues/6339)) +1. Fix png scaling on non standard DPI. Add 'enablePlotViewer' setting to allow user to render PNGs instead of SVG files. + ([#6344](https://github.com/Microsoft/vscode-python/issues/6344)) +1. Do best effort to download the experiments and use it in the very first session only. + ([#6348](https://github.com/Microsoft/vscode-python/issues/6348)) +1. Linux can pick the wrong kernel to use when starting the interactive window. + ([#6375](https://github.com/Microsoft/vscode-python/issues/6375)) +1. Add missing keys for data science interactive window button tooltips in `package.nls.json`. + ([#6386](https://github.com/Microsoft/vscode-python/issues/6386)) +1. Fix overwriting of cwd in the path list when discovering tests. + ([#6417](https://github.com/Microsoft/vscode-python/issues/6417)) +1. Fixes a bug in pytest test discovery. + (thanks Rainer Dreyer) + ([#6463](https://github.com/Microsoft/vscode-python/issues/6463)) +1. Fix debugging to work on restarting the jupyter kernel. + ([#6502](https://github.com/Microsoft/vscode-python/issues/6502)) +1. Escape key in the interactive window moves to the delete button when auto complete is open. Escape should only move when no autocomplete is open. + ([#6507](https://github.com/Microsoft/vscode-python/issues/6507)) +1. Render plots as png, but save an svg for exporting/image viewing. Speeds up plot rendering. + ([#6526](https://github.com/Microsoft/vscode-python/issues/6526)) +1. Import get_ipython at the start of each imported jupyter notebook if there are line magics in the file + ([#6574](https://github.com/Microsoft/vscode-python/issues/6574)) +1. Fix a problem where we retrieved and rendered old codelenses for multiple imports of jupyter notebooks if cells in the resultant import file were executed without saving the file to disk. + ([#6582](https://github.com/Microsoft/vscode-python/issues/6582)) +1. PTVSD install for jupyter debugging should check version without actually importing into the jupyter kernel. + ([#6592](https://github.com/Microsoft/vscode-python/issues/6592)) +1. Fix pandas version parsing to handle strings. + ([#6595](https://github.com/Microsoft/vscode-python/issues/6595)) +1. Unpin the version of ptvsd in the install and add `-U`. + ([#6718](https://github.com/Microsoft/vscode-python/issues/6718)) +1. Fix stepping when more than one blank line at the end of a cell. + ([#6719](https://github.com/Microsoft/vscode-python/issues/6719)) +1. Render plots as png, but save an svg for exporting/image viewing. Speeds up plot rendering. + ([#6724](https://github.com/Microsoft/vscode-python/issues/6724)) +1. Fix random occurrences of output not concatenating correctly in the interactive window. + ([#6728](https://github.com/Microsoft/vscode-python/issues/6728)) +1. In order to debug without '#%%' defined in a file, support a Debug Entire File. + ([#6730](https://github.com/Microsoft/vscode-python/issues/6730)) +1. Add support for "Run Below" back. + ([#6737](https://github.com/Microsoft/vscode-python/issues/6737)) +1. Fix the 'Variables not available while debugging' message to be more descriptive. + ([#6740](https://github.com/Microsoft/vscode-python/issues/6740)) +1. Make breakpoints on enter always be the case unless 'stopOnFirstLineWhileDebugging' is set. + ([#6743](https://github.com/Microsoft/vscode-python/issues/6743)) +1. Remove Debug Cell and Run Cell from the command palette. They should both be 'Debug Current Cell' and 'Run Current Cell' + ([#6754](https://github.com/Microsoft/vscode-python/issues/6754)) +1. Make the dataviewer open a window much faster. Total load time is the same, but initial response is much faster. + ([#6729](https://github.com/Microsoft/vscode-python/issues/6729)) +1. Debugging an untitled file causes an error 'Untitled-1 cannot be opened'. + ([#6738](https://github.com/Microsoft/vscode-python/issues/6738)) +1. Eliminate 'History\_\' from the problems list when using the interactive panel. + ([#6748](https://github.com/Microsoft/vscode-python/issues/6748)) + +### Code Health + +1. Log processes executed behind the scenes in the extension output panel. + ([#1131](https://github.com/Microsoft/vscode-python/issues/1131)) +1. Specify `pyramid.scripts.pserve` when creating a debug configuration for Pyramid + apps instead of trying to calculate the location of the `pserve` command. + ([#2427](https://github.com/Microsoft/vscode-python/issues/2427)) +1. UI Tests using [selenium](https://selenium-python.readthedocs.io/index.html) & [behave](https://behave.readthedocs.io/en/latest/). + ([#4692](https://github.com/Microsoft/vscode-python/issues/4692)) +1. Upload coverage reports to [coveralls](https://coveralls.io/github/microsoft/vscode-python). + ([#5999](https://github.com/Microsoft/vscode-python/issues/5999)) +1. Upgrade Jedi to version 0.13.3. + ([#6013](https://github.com/Microsoft/vscode-python/issues/6013)) +1. Add unit tests for `client/activation/serviceRegistry.ts`. + ([#6163](https://github.com/Microsoft/vscode-python/issues/6163)) +1. Remove `test.ipynb` from the root folder. + ([#6212](https://github.com/Microsoft/vscode-python/issues/6212)) +1. Fail the `smoke tests` CI job when the smoke tests fail. + ([#6253](https://github.com/Microsoft/vscode-python/issues/6253)) +1. Add a bunch of perf measurements to telemetry. + ([#6283](https://github.com/Microsoft/vscode-python/issues/6283)) +1. Retry failing debugger test (retry due to intermittent issues on `Azure Pipelines`). + ([#6322](https://github.com/Microsoft/vscode-python/issues/6322)) +1. Update version of `isort` to `4.3.21`. + ([#6369](https://github.com/Microsoft/vscode-python/issues/6369)) +1. Functional test for debugging jupyter cells. + ([#6377](https://github.com/Microsoft/vscode-python/issues/6377)) +1. Consolidate telemetry. + ([#6451](https://github.com/Microsoft/vscode-python/issues/6451)) +1. Removed npm package `vscode`, and added to use `vscode-test` and `@types/vscode` (see [here](https://code.visualstudio.com/updates/v1_36#_splitting-vscode-package-into-typesvscode-and-vscodetest) for more info). + ([#6456](https://github.com/Microsoft/vscode-python/issues/6456)) +1. Fix the variable explorer exclude test to be less strict. + ([#6525](https://github.com/Microsoft/vscode-python/issues/6525)) +1. Merge ArgumentsHelper unit tests into one file. + ([#6583](https://github.com/Microsoft/vscode-python/issues/6583)) +1. Fix jupyter remote tests to respect new notebook 6.0 output format. + ([#6625](https://github.com/Microsoft/vscode-python/issues/6625)) +1. Unit Tests for DataScience Error Handler. + ([#6670](https://github.com/Microsoft/vscode-python/issues/6670)) +1. Fix DataExplorer tests after accessibility fixes. + ([#6711](https://github.com/Microsoft/vscode-python/issues/6711)) +1. Bump version of [PTVSD](https://pypi.org/project/ptvsd/) to 4.3.0. + ([#6771](https://github.com/Microsoft/vscode-python/issues/6771)) + - Support for Jupyter debugging + - Support for ipython cells + - API to enable and disable tracing via ptvsd.tracing + - ptvsd.enable_attach accepts address=('localhost', 0) and returns server port + - Known issue: Unable to find threadStateIndex for the current thread. curPyThread ([#11587](https://github.com/microsoft/ptvsd/issues/1587)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.6.1 (9 July 2019) + +### Fixes + +1. Fixes to A/B testing. + ([#6400](https://github.com/microsoft/vscode-python/issues/6400)) + +## 2019.6.0 (25 June 2019) + +### Enhancements + +1. Dedent selected code before sending it to the terminal. + ([#2837](https://github.com/Microsoft/vscode-python/issues/2837)) +1. Allow password for remote authentication. + ([#3624](https://github.com/Microsoft/vscode-python/issues/3624)) +1. Add support for sub process debugging, when debugging tests. + ([#4525](https://github.com/Microsoft/vscode-python/issues/4525)) +1. Change title of `Discover Tests` to `Discovering` when discovering tests. + ([#4562](https://github.com/Microsoft/vscode-python/issues/4562)) +1. Add an extra viewer for plots in the interactive window. + ([#4967](https://github.com/Microsoft/vscode-python/issues/4967)) +1. Allow for self signed certificates for jupyter connections. + ([#4987](https://github.com/Microsoft/vscode-python/issues/4987)) +1. Add support for A/B testing and staged rollouts of new functionality. + ([#5042](https://github.com/Microsoft/vscode-python/issues/5042)) +1. Removed `--nothreading` flag from the `Django` debug configuration. + ([#5116](https://github.com/Microsoft/vscode-python/issues/5116)) +1. Test Explorer : Remove time from all nodes except the tests. + ([#5120](https://github.com/Microsoft/vscode-python/issues/5120)) +1. Add support for a copy back to source. + ([#5286](https://github.com/Microsoft/vscode-python/issues/5286)) +1. Add visual separation between the variable explorer and the rest of the Interactive Window content. + ([#5389](https://github.com/Microsoft/vscode-python/issues/5389)) +1. Changes placeholder label in testConfigurationManager.ts from 'Select the directory containing the unit tests' to 'Select the directory containing the tests'. + (thanks [James Flynn](https://github.com/james-flynn-ie/)) + ([#5602](https://github.com/Microsoft/vscode-python/issues/5602)) +1. Updated labels in File > Preferences > Settings. It now states 'Pytest' where it stated 'Py Test'. + (thanks [James Flynn](https://github.com/james-flynn-ie/)) + ([#5603](https://github.com/Microsoft/vscode-python/issues/5603)) +1. Updated label for "Enable unit testing for Pytest" to remove the word "unit". + (thanks [James Flynn](https://github.com/james-flynn-ie/)) + ([#5604](https://github.com/Microsoft/vscode-python/issues/5604)) +1. Importing a notebook should show the output of the notebook in the Python Interactive window. This feature can be turned off with the 'previewImportedNotebooksInInteractivePane' setting. + ([#5675](https://github.com/Microsoft/vscode-python/issues/5675)) +1. Add flag to auto preview an ipynb file when opened. + ([#5790](https://github.com/Microsoft/vscode-python/issues/5790)) +1. Change pytest description from configuration menu. + ([#5832](https://github.com/Microsoft/vscode-python/issues/5832)) +1. Support faster restart of the kernel by creating two kernels (two python processes running under the covers). + ([#5876](https://github.com/Microsoft/vscode-python/issues/5876)) +1. Allow a 'Dont ask me again' option for restarting the kernel. + ([#5951](https://github.com/Microsoft/vscode-python/issues/5951)) +1. Added experiment to always display the test explorer. + ([#6211](https://github.com/Microsoft/vscode-python/issues/6211)) + +### Fixes + +1. Added support for activation of conda environments in `powershell`. + ([#668](https://github.com/Microsoft/vscode-python/issues/668)) +1. Provide `pathMappings` to debugger when attaching to child processes. + ([#3568](https://github.com/Microsoft/vscode-python/issues/3568)) +1. Add virtualenvwrapper default virtual environment location to the `python.venvFolders` config setting. + ([#4642](https://github.com/Microsoft/vscode-python/issues/4642)) +1. Advance to the next cell if cursor is in the current cell and user clicks 'Run Cell'. + ([#5067](https://github.com/Microsoft/vscode-python/issues/5067)) +1. Fix localhost path mappings to lowercase the drive letter on Windows. + ([#5362](https://github.com/Microsoft/vscode-python/issues/5362)) +1. Fix import/export paths to be escaped on windows. + ([#5386](https://github.com/Microsoft/vscode-python/issues/5386)) +1. Support loading larger dataframes in the dataviewer (anything more than 1000 columns will still be slow, but won't crash). + ([#5469](https://github.com/Microsoft/vscode-python/issues/5469)) +1. Fix magics running from a python file. + ([#5537](https://github.com/Microsoft/vscode-python/issues/5537)) +1. Change scrolling to not animate to workaround async updates breaking the animation. + ([#5560](https://github.com/Microsoft/vscode-python/issues/5560)) +1. Add support for opening hyperlinks from the interactive window. + ([#5630](https://github.com/Microsoft/vscode-python/issues/5630)) +1. Remove extra padding in the dataviewer. + ([#5653](https://github.com/Microsoft/vscode-python/issues/5653)) +1. Add 'Add empty cell to file' command. Shortcut for having to type '#%%'. + ([#5667](https://github.com/Microsoft/vscode-python/issues/5667)) +1. Add 'ctrl+enter' as a keyboard shortcut for run current cell (runs without advancing) + ([#5673](https://github.com/Microsoft/vscode-python/issues/5673)) +1. Adjust input box prompt to look more an IPython console prompt. + ([#5729](https://github.com/Microsoft/vscode-python/issues/5729)) +1. Jupyter-notebook exists after shutdown. + ([#5731](https://github.com/Microsoft/vscode-python/issues/5731)) +1. Fix horizontal scrolling in the Interactive Window. + ([#5734](https://github.com/Microsoft/vscode-python/issues/5734)) +1. Fix problem with using up/down arrows in autocomplete. + ([#5774](https://github.com/Microsoft/vscode-python/issues/5774)) +1. Fix latex and markdown scrolling. + ([#5775](https://github.com/Microsoft/vscode-python/issues/5775)) +1. Add support for jupyter controls that clear. + ([#5801](https://github.com/Microsoft/vscode-python/issues/5801)) +1. Fix up arrow on signature help closing the help. + ([#5813](https://github.com/Microsoft/vscode-python/issues/5813)) +1. Make the interactive window respect editor cursor and blink style. + ([#5814](https://github.com/Microsoft/vscode-python/issues/5814)) +1. Remove extra overlay on editor when matching parentheses. + ([#5815](https://github.com/Microsoft/vscode-python/issues/5815)) +1. Fix theme color missing errors inside interactive window. + ([#5827](https://github.com/Microsoft/vscode-python/issues/5827)) +1. Fix problem with shift+enter not working after using goto source. + ([#5829](https://github.com/Microsoft/vscode-python/issues/5829)) +1. Fix CI failures related to history import changes. + ([#5844](https://github.com/Microsoft/vscode-python/issues/5844)) +1. Disable quoting of paths sent to the debugger as arguments. + ([#5861](https://github.com/Microsoft/vscode-python/issues/5861)) +1. Fix shift+enter to work in newly created files with cells. + ([#5879](https://github.com/Microsoft/vscode-python/issues/5879)) +1. Fix nightly failures caused by new jupyter command line. + ([#5883](https://github.com/Microsoft/vscode-python/issues/5883)) +1. Improve accessibility of the 'Python Interactive' window. + ([#5884](https://github.com/Microsoft/vscode-python/issues/5884)) +1. Auto preview notebooks on import. + ([#5891](https://github.com/Microsoft/vscode-python/issues/5891)) +1. Fix liveloss test to not have so many dependencies. + ([#5909](https://github.com/Microsoft/vscode-python/issues/5909)) +1. Fixes to detection of the shell. + ([#5916](https://github.com/Microsoft/vscode-python/issues/5916)) +1. Fixes to activation of Conda environments. + ([#5929](https://github.com/Microsoft/vscode-python/issues/5929)) +1. Fix themes in the interactive window that use 3 color hex values (like Cobalt2). + ([#5950](https://github.com/Microsoft/vscode-python/issues/5950)) +1. Fix jupyter services node-fetch connection issue. + ([#5956](https://github.com/Microsoft/vscode-python/issues/5956)) +1. Allow selection and running of indented code in the python interactive window. + ([#5983](https://github.com/Microsoft/vscode-python/issues/5983)) +1. Account for files being opened in Visual Studio Code that do not belong to a workspace. + ([#6624](https://github.com/Microsoft/vscode-python/issues/6624)) +1. Accessibility pass on plot viewer + ([#6020](https://github.com/Microsoft/vscode-python/issues/6020)) +1. Allow for both password and self cert server to work together + ([#6265](https://github.com/Microsoft/vscode-python/issues/6265)) +1. Fix pdf export in release bits. + ([#6277](https://github.com/Microsoft/vscode-python/issues/6277)) + +### Code Health + +1. Add code coverage reporting. + ([#4472](https://github.com/Microsoft/vscode-python/issues/4472)) +1. Minimize data sent as part of the `ERROR` telemetry event. + ([#4602](https://github.com/Microsoft/vscode-python/issues/4602)) +1. Fixes to decorator tests. + ([#5085](https://github.com/Microsoft/vscode-python/issues/5085)) +1. Add sorting test for DataViewer. + ([#5415](https://github.com/Microsoft/vscode-python/issues/5415)) +1. Rename "unit test" to "tests" from drop menu when clicking on "Run Tests" on the status bar. + ([#5605](https://github.com/Microsoft/vscode-python/issues/5605)) +1. Added telemetry to track memory usage of the `Jedi Language Server` process. + ([#5726](https://github.com/Microsoft/vscode-python/issues/5726)) +1. Fix nightly functional tests from timing out during process cleanup. + ([#5870](https://github.com/Microsoft/vscode-python/issues/5870)) +1. Change how telemetry is sent for the 'shift+enter' banner. + ([#5887](https://github.com/Microsoft/vscode-python/issues/5887)) +1. Fixes to gulp script used to bundle the extension with `WebPack`. + ([#5932](https://github.com/Microsoft/vscode-python/issues/5932)) +1. Tighten up the import-matching regex to minimize false-positives. + ([#5988](https://github.com/Microsoft/vscode-python/issues/5988)) +1. Merge multiple coverage reports into one. + ([#6000](https://github.com/Microsoft/vscode-python/issues/6000)) +1. Fix DataScience nightly tests. + ([#6032](https://github.com/Microsoft/vscode-python/issues/6032)) +1. Update version of TypeScript to 3.5. + ([#6033](https://github.com/Microsoft/vscode-python/issues/6033)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.20](https://pypi.org/project/isort/4.3.20/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.5.18875 (6 June 2019) + +### Fixes + +1. Disable quoting of paths sent to the debugger as arguments. + ([#5861](https://github.com/microsoft/vscode-python/issues/5861)) +1. Fixes to activation of Conda environments. + ([#5929](https://github.com/microsoft/vscode-python/issues/5929)) + +## 2019.5.18678 (5 June 2019) + +### Fixes + +1. Fixes to detection of the shell. + ([#5916](https://github.com/microsoft/vscode-python/issues/5916)) + +## 2019.5.18875 (6 June 2019) + +### Fixes + +1. Disable quoting of paths sent to the debugger as arguments. + ([#5861](https://github.com/microsoft/vscode-python/issues/5861)) +1. Fixes to activation of Conda environments. + ([#5929](https://github.com/microsoft/vscode-python/issues/5929)) + +## 2019.5.18678 (5 June 2019) + +### Fixes + +1. Fixes to detection of the shell. + ([#5916](https://github.com/microsoft/vscode-python/issues/5916)) + +## 2019.5.18426 (4 June 2019) + +### Fixes + +1. Changes to identification of `shell` for the activation of environments in the terminal. + ([#5743](https://github.com/microsoft/vscode-python/issues/5743)) + +## 2019.5.17517 (30 May 2019) + +### Fixes + +1. Revert changes related to pathMappings in `launch.json` for `debugging` [#3568](https://github.com/Microsoft/vscode-python/issues/3568) + ([#5833](https://github.com/microsoft/vscode-python/issues/5833)) + +## 2019.5.17059 (28 May 2019) + +### Enhancements + +1. Add visual separation between the variable explorer and the rest of the Interactive Window content + ([#5389](https://github.com/Microsoft/vscode-python/issues/5389)) +1. Show a message when no variables are defined + ([#5228](https://github.com/Microsoft/vscode-python/issues/5228)) +1. Variable explorer UI fixes via PM / designer + ([#5274](https://github.com/Microsoft/vscode-python/issues/5274)) +1. Allow column sorting in variable explorer + ([#5281](https://github.com/Microsoft/vscode-python/issues/5281)) +1. Provide basic intellisense in Interactive Windows, using the language server. + ([#5342](https://github.com/Microsoft/vscode-python/issues/5342)) +1. Add support for Jupyter autocomplete data in Interactive Window. + ([#5346](https://github.com/Microsoft/vscode-python/issues/5346)) +1. Swap getsizeof size value for something more sensible in the variable explorer + ([#5368](https://github.com/Microsoft/vscode-python/issues/5368)) +1. Pass parent debug session to child debug sessions using new DA API + ([#5464](https://github.com/Microsoft/vscode-python/issues/5464)) + +### Fixes + +1. Advance to the next cell if cursor is in the current cell and user clicks 'Run Cell' + ([#5067](https://github.com/Microsoft/vscode-python/issues/5067)) +1. Fix import/export paths to be escaped on windows. + ([#5386](https://github.com/Microsoft/vscode-python/issues/5386)) +1. Fix magics running from a python file. + ([#5537](https://github.com/Microsoft/vscode-python/issues/5537)) +1. Change scrolling to not animate to workaround async updates breaking the animation. + ([#5560](https://github.com/Microsoft/vscode-python/issues/5560)) +1. Add support for opening hyperlinks from the interactive window. + ([#5630](https://github.com/Microsoft/vscode-python/issues/5630)) +1. Add 'Add empty cell to file' command. Shortcut for having to type '#%%' + ([#5667](https://github.com/Microsoft/vscode-python/issues/5667)) +1. Add 'ctrl+enter' as a keyboard shortcut for run current cell (runs without advancing) + ([#5673](https://github.com/Microsoft/vscode-python/issues/5673)) +1. Adjust input box prompt to look more an IPython console prompt. + ([#5729](https://github.com/Microsoft/vscode-python/issues/5729)) +1. Fix horizontal scrolling in the Interactive Window + ([#5734](https://github.com/Microsoft/vscode-python/issues/5734)) +1. Fix problem with using up/down arrows in autocomplete. + ([#5774](https://github.com/Microsoft/vscode-python/issues/5774)) +1. Fix latex and markdown scrolling. + ([#5775](https://github.com/Microsoft/vscode-python/issues/5775)) +1. Use the correct activation script for conda environments + ([#4402](https://github.com/Microsoft/vscode-python/issues/4402)) +1. Improve pipenv error messages (thanks [David Lechner](https://github.com/dlech)) + ([#4866](https://github.com/Microsoft/vscode-python/issues/4866)) +1. Quote paths returned by debugger API + ([#4966](https://github.com/Microsoft/vscode-python/issues/4966)) +1. Reliably end test tasks in Azure Pipelines. + ([#5129](https://github.com/Microsoft/vscode-python/issues/5129)) +1. Append `--pre` to black installation command so pipenv can properly resolve it. + (thanks [Erin O'Connell](https://github.com/erinxocon)) + ([#5171](https://github.com/Microsoft/vscode-python/issues/5171)) +1. Make background cell color useable in all themes. + ([#5236](https://github.com/Microsoft/vscode-python/issues/5236)) +1. Filtered rows shows 'fetching' instead of No rows. + ([#5278](https://github.com/Microsoft/vscode-python/issues/5278)) +1. Always show pytest's output when it fails. + ([#5313](https://github.com/Microsoft/vscode-python/issues/5313)) +1. Value 'None' sometimes shows up in the Count column of the variable explorer + ([#5387](https://github.com/Microsoft/vscode-python/issues/5387)) +1. Multi-dimensional arrays don't open in the data viewer. + ([#5395](https://github.com/Microsoft/vscode-python/issues/5395)) +1. Fix sorting of lists with numbers and missing entries. + ([#5414](https://github.com/Microsoft/vscode-python/issues/5414)) +1. Fix error with bad len() values in variable explorer + ([#5420](https://github.com/Microsoft/vscode-python/issues/5420)) +1. Remove trailing commas from JSON files. + (thanks [Romain](https://github.com/quarthex)) + ([#5437](https://github.com/Microsoft/vscode-python/issues/5437)) +1. Handle missing index columns and non trivial data types for columns. + ([#5452](https://github.com/Microsoft/vscode-python/issues/5452)) +1. Fix ignoreVscodeTheme to play along with dynamic theme updates. Also support setting in the variable explorer. + ([#5480](https://github.com/Microsoft/vscode-python/issues/5480)) +1. Fix matplotlib updating for dark theme after restarting + ([#5486](https://github.com/Microsoft/vscode-python/issues/5486)) +1. Add dev flag to poetry installer. + (thanks [Yan Pashkovsky](https://github.com/Yanpas)) + ([#5496](https://github.com/Microsoft/vscode-python/issues/5496)) +1. Default `PYTHONPATH` to an empty string if the environment variable is not defined. + ([#5579](https://github.com/Microsoft/vscode-python/issues/5579)) +1. Fix problems if other language kernels are installed that are using python under the covers (bash is one such example). + ([#5586](https://github.com/Microsoft/vscode-python/issues/5586)) +1. Allow collapsed code to affect intellisense. + ([#5631](https://github.com/Microsoft/vscode-python/issues/5631)) +1. Eliminate search support in the mini-editors in the Python Interactive window. + ([#5637](https://github.com/Microsoft/vscode-python/issues/5637)) +1. Fix perf problem with intellisense in the Interactive Window. + ([#5697](https://github.com/Microsoft/vscode-python/issues/5697)) +1. Using "request": "launch" item in launch.json for debugging sends pathMappings + ([#3568](https://github.com/Microsoft/vscode-python/issues/3568)) +1. Fix perf issues with long collections and variable explorer + ([#5511](https://github.com/Microsoft/vscode-python/issues/5511)) +1. Changed synchronous file system operation into async + ([#4895](https://github.com/Microsoft/vscode-python/issues/4895)) +1. Update ptvsd to [4.2.10](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.10). + - No longer switch off getpass on import. + - Fixes a crash on evaluate request. + - Fix a issue with running no-debug. + - Fixes issue with forwarding sys.stdin.read(). + - Remove sys.prefix form library roots. + +### Code Health + +1. Deprecate [travis](https://travis-ci.org/) in favor of [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/). + ([#4024](https://github.com/Microsoft/vscode-python/issues/4024)) +1. Smoke Tests must be run on nightly and CI on Azdo + ([#5090](https://github.com/Microsoft/vscode-python/issues/5090)) +1. Increase timeout and retries in Jupyter wait for idle + ([#5430](https://github.com/Microsoft/vscode-python/issues/5430)) +1. Update manual test plan for Variable Explorer and Data Viewer + ([#5476](https://github.com/Microsoft/vscode-python/issues/5476)) +1. Auto-update version number in `CHANGELOG.md` in the CI pipeline. + ([#5523](https://github.com/Microsoft/vscode-python/issues/5523)) +1. Fix security issues. + ([#5538](https://github.com/Microsoft/vscode-python/issues/5538)) +1. Send logging output into a text file on CI server. + ([#5651](https://github.com/Microsoft/vscode-python/issues/5651)) +1. Fix python 2.7 and 3.5 variable explorer nightly tests + ([#5433](https://github.com/Microsoft/vscode-python/issues/5433)) +1. Update isort to version 4.3.20. + (Thanks [Andrew Blakey](https://github.com/ablakey)) + ([#5642](https://github.com/Microsoft/vscode-python/issues/5642)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.20](https://pypi.org/project/isort/4.3.20/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.4.1 (24 April 2019) + +### Fixes + +1. Remove trailing commas in JSON files. + (thanks [Romain](https://github.com/quarthex)) + ([#5437](https://github.com/Microsoft/vscode-python/issues/5437)) + +## 2019.4.0 (23 April 2019) + +### Enhancements + +1. Download the language server using HTTP if `http.proxyStrictSSL` is set to `true`. + ([#2849](https://github.com/Microsoft/vscode-python/issues/2849)) +1. Launch the `Python` debug configuration UI when manually adding entries into the `launch.json` file. + ([#3321](https://github.com/Microsoft/vscode-python/issues/3321)) +1. Add tracking of 'current' cell in the editor. Also add cell boundaries for non active cell. + ([#3542](https://github.com/Microsoft/vscode-python/issues/3542)) +1. Change default behavior of debugger to display return values. + ([#3754](https://github.com/Microsoft/vscode-python/issues/3754)) +1. Replace setting `debugStdLib` with `justMyCode` + ([#4032](https://github.com/Microsoft/vscode-python/issues/4032)) +1. Change "Unit Test" phrasing to "Test" or "Testing". + ([#4384](https://github.com/Microsoft/vscode-python/issues/4384)) +1. Auto expand tree view in `Test Explorer` to display failed tests. + ([#4386](https://github.com/Microsoft/vscode-python/issues/4386)) +1. Add a data grid control and web view panel. + ([#4675](https://github.com/Microsoft/vscode-python/issues/4675)) +1. Add support for viewing dataframes, lists, dicts, nparrays. + ([#4677](https://github.com/Microsoft/vscode-python/issues/4677)) +1. Auto-expand the first level of the test explorer tree view. + ([#4767](https://github.com/Microsoft/vscode-python/issues/4767)) +1. Use `Python` code for discovery of tests when using `pytest`. + ([#4795](https://github.com/Microsoft/vscode-python/issues/4795)) +1. Intergrate the variable explorer into the header better and refactor HTML and CSS. + ([#4800](https://github.com/Microsoft/vscode-python/issues/4800)) +1. Integrate the variable viewer with the IJupyterVariable interface. + ([#4802](https://github.com/Microsoft/vscode-python/issues/4802)) +1. Include number of skipped tests in Test Data item tooltip. + ([#4849](https://github.com/Microsoft/vscode-python/issues/4849)) +1. Add prompt to select virtual environment for the worskpace. + ([#4908](https://github.com/Microsoft/vscode-python/issues/4908)) +1. Prompt to turn on Pylint if a `pylintrc` or `.pylintrc` file is found. + ([#4941](https://github.com/Microsoft/vscode-python/issues/4941)) +1. Variable explorer handles new cell submissions. + ([#4948](https://github.com/Microsoft/vscode-python/issues/4948)) +1. Pass one at getting our data grid styled correctly to match vscode styles and the spec. + ([#4998](https://github.com/Microsoft/vscode-python/issues/4998)) +1. Ensure `Language Server` can start without [ICU](http://site.icu-project.org/home). + ([#5043](https://github.com/Microsoft/vscode-python/issues/5043)) +1. Support running under docker. + ([#5047](https://github.com/Microsoft/vscode-python/issues/5047)) +1. Add exclude list to variable viewer. + ([#5104](https://github.com/Microsoft/vscode-python/issues/5104)) +1. Display a tip to the user informing them of the ability to change the interpreter from the statusbar. + ([#5180](https://github.com/Microsoft/vscode-python/issues/5180)) +1. Hook up the variable explorer to the data frame explorer. + ([#5187](https://github.com/Microsoft/vscode-python/issues/5187)) +1. Remove the debug config snippets (rely on handler instead). + ([#5189](https://github.com/Microsoft/vscode-python/issues/5189)) +1. Add setting to just enable/disable the data science codelens. + ([#5211](https://github.com/Microsoft/vscode-python/issues/5211)) +1. Change settings from `python.unitTest.*` to `python.testing.*`. + ([#5219](https://github.com/Microsoft/vscode-python/issues/5219)) +1. Add telemetry for variable explorer and turn on by default. + ([#5337](https://github.com/Microsoft/vscode-python/issues/5337)) +1. Show a message when no variables are defined + ([#5228](https://github.com/Microsoft/vscode-python/issues/5228)) +1. Variable explorer UI fixes via PM / designer + ([#5274](https://github.com/Microsoft/vscode-python/issues/5274)) +1. Allow column sorting in variable explorer + ([#5281](https://github.com/Microsoft/vscode-python/issues/5281)) +1. Swap getsizeof size value for something more sensible in the variable explorer + ([#5368](https://github.com/Microsoft/vscode-python/issues/5368)) + +### Fixes + +1. Ignore the extension's Python files when debugging. + ([#3201](https://github.com/Microsoft/vscode-python/issues/3201)) +1. Dispose processes started within the extension during. + ([#3331](https://github.com/Microsoft/vscode-python/issues/3331)) +1. Fix problem with errors not showing up for import when no jupyter installed. + ([#3958](https://github.com/Microsoft/vscode-python/issues/3958)) +1. Fix tabs in comments to come out in cells. + ([#4029](https://github.com/Microsoft/vscode-python/issues/4029)) +1. Use configuration API and provide Resource when retrieving settings. + ([#4486](https://github.com/Microsoft/vscode-python/issues/4486)) +1. When debugging, the extension correctly uses custom `.env` files. + ([#4537](https://github.com/Microsoft/vscode-python/issues/4537)) +1. Accomadate trailing commands in the JSON contents of `launch.json` file. + ([#4543](https://github.com/Microsoft/vscode-python/issues/4543)) +1. Kill liveshare sessions if a guest connects without the python extension installed. + ([#4947](https://github.com/Microsoft/vscode-python/issues/4947)) +1. Shutting down a session should not cause the host to stop working. + ([#4949](https://github.com/Microsoft/vscode-python/issues/4949)) +1. Fix cell spacing issues. + ([#4979](https://github.com/Microsoft/vscode-python/issues/4979)) +1. Fix hangs in functional tests. + ([#4992](https://github.com/Microsoft/vscode-python/issues/4992)) +1. Fix triple quoted comments in cells to not affect anything. + ([#5012](https://github.com/Microsoft/vscode-python/issues/5012)) +1. Restarting the kernel will eventually force Jupyter server to shutdown if it doesn't come back. + ([#5025](https://github.com/Microsoft/vscode-python/issues/5025)) +1. Adjust styling for data viewer. + ([#5058](https://github.com/Microsoft/vscode-python/issues/5058)) +1. Fix MimeTypes test after we stopped stripping comments. + ([#5086](https://github.com/Microsoft/vscode-python/issues/5086)) +1. No prompt displayed to install pylint. + ([#5087](https://github.com/Microsoft/vscode-python/issues/5087)) +1. Fix scrolling in the interactive window. + ([#5131](https://github.com/Microsoft/vscode-python/issues/5131)) +1. Default colors when theme.json cannot be found. + Fix Python interactive window to update when theme changes. + ([#5136](https://github.com/Microsoft/vscode-python/issues/5136)) +1. Replace 'Run Above' and 'Run Below' in the palette with 'Run Cells Above Cursor' and 'Run Current Cell and Below'. + ([#5143](https://github.com/Microsoft/vscode-python/issues/5143)) +1. Variables not cleared after a kernel restart. + ([#5244](https://github.com/Microsoft/vscode-python/issues/5244)) +1. Fix variable explorer to work in Live Share. + ([#5277](https://github.com/Microsoft/vscode-python/issues/5277)) +1. Update matplotlib based on theme changes. + ([#5294](https://github.com/Microsoft/vscode-python/issues/5294)) +1. Restrict files from being processed by `Language Server` only when in a mult-root workspace. + ([#5333](https://github.com/Microsoft/vscode-python/issues/5333)) +1. Fix dataviewer header column alignment. + ([#5351](https://github.com/Microsoft/vscode-python/issues/5351)) +1. Make background cell color useable in all themes. + ([#5236](https://github.com/Microsoft/vscode-python/issues/5236)) +1. Filtered rows shows 'fetching' instead of No rows. + ([#5278](https://github.com/Microsoft/vscode-python/issues/5278)) +1. Multi-dimensional arrays don't open in the data viewer. + ([#5395](https://github.com/Microsoft/vscode-python/issues/5395)) +1. Fix sorting of lists with numbers and missing entries. + ([#5414](https://github.com/Microsoft/vscode-python/issues/5414)) +1. Fix error with bad len() values in variable explorer + ([#5420](https://github.com/Microsoft/vscode-python/issues/5420)) +1. Update ptvsd to [4.2.8](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.8). + - Path mapping bug fixes. + - Fix for hang when using debug console. + - Fix for set next statement. + - Fix for multi-threading. + +### Code Health + +1. Enable typescript's strict mode. + ([#611](https://github.com/Microsoft/vscode-python/issues/611)) +1. Update to use `Node` version `10.5.0`. + ([#1138](https://github.com/Microsoft/vscode-python/issues/1138)) +1. Update `launch.json` to use `internalConsole` instead of none. + ([#4321](https://github.com/Microsoft/vscode-python/issues/4321)) +1. Change flaky tests (relying on File System Watchers) into unit tests. + ([#4468](https://github.com/Microsoft/vscode-python/issues/4468)) +1. Corrected Smoke test failure for 'Run Python File In Terminal'. + ([#4515](https://github.com/Microsoft/vscode-python/issues/4515)) +1. Drop (official) support for Python 3.4. + ([#4528](https://github.com/Microsoft/vscode-python/issues/4528)) +1. Support debouncing decorated async methods. + ([#4641](https://github.com/Microsoft/vscode-python/issues/4641)) +1. Add functional tests for pytest adapter script. + ([#4739](https://github.com/Microsoft/vscode-python/issues/4739)) +1. Remove the use of timers in unittest code. Simulate the passing of time instead. + ([#4776](https://github.com/Microsoft/vscode-python/issues/4776)) +1. Add functional testing for variable explorer. + ([#4803](https://github.com/Microsoft/vscode-python/issues/4803)) +1. Add tests for variable explorer Python files. + ([#4804](https://github.com/Microsoft/vscode-python/issues/4804)) +1. Add real environment variables provider on to get functional tests to pass on macOS. + ([#4820](https://github.com/Microsoft/vscode-python/issues/4820)) +1. Handle done on all jupyter requests to make sure an unhandle exception isn't passed on shutdown. + ([#4827](https://github.com/Microsoft/vscode-python/issues/4827)) +1. Clean up language server initialization and configuration. + ([#4832](https://github.com/Microsoft/vscode-python/issues/4832)) +1. Hash imports of top-level packages to see what users need supported. + ([#4852](https://github.com/Microsoft/vscode-python/issues/4852)) +1. Have `tpn` clearly state why a project's license entry in the configuration file is considered stale. + ([#4865](https://github.com/Microsoft/vscode-python/issues/4865)) +1. Kill the test process on CI, 10s after the tests have completed. + ([#4905](https://github.com/Microsoft/vscode-python/issues/4905)) +1. Remove hardcoded Azdo Pipeline of 30m, leaving it to the default of 60m. + ([#4914](https://github.com/Microsoft/vscode-python/issues/4914)) +1. Use the `Python` interpreter prescribed by CI instead of trying to locate the best possible one. + ([#4920](https://github.com/Microsoft/vscode-python/issues/4920)) +1. Skip linter tests correctly. + ([#4923](https://github.com/Microsoft/vscode-python/issues/4923)) +1. Remove redundant compilation step on CI. + ([#4926](https://github.com/Microsoft/vscode-python/issues/4926)) +1. Dispose handles to timers created from using `setTimeout`. + ([#4930](https://github.com/Microsoft/vscode-python/issues/4930)) +1. Ensure sockets get disposed along with other resources. + ([#4935](https://github.com/Microsoft/vscode-python/issues/4935)) +1. Fix intermittent test failure with listeners. + ([#4936](https://github.com/Microsoft/vscode-python/issues/4936)) +1. Update `mocha` to the latest version. + ([#4937](https://github.com/Microsoft/vscode-python/issues/4937)) +1. Remove redundant mult-root tests. + ([#4943](https://github.com/Microsoft/vscode-python/issues/4943)) +1. Fix intermittent test failure with kernel shutdown. + ([#4951](https://github.com/Microsoft/vscode-python/issues/4951)) +1. Update version of [isort](https://pypi.org/project/isort/) to `4.3.17` + ([#5059](https://github.com/Microsoft/vscode-python/issues/5059)) +1. Fix typo and use constants instead of hardcoded command names. + (thanks [Allan Wang](https://github.com/AllanWang)) + ([#5204](https://github.com/Microsoft/vscode-python/issues/5204)) +1. Add datascience specific settings to telemetry gathered. Make sure to scrape any strings of PII. + ([#5212](https://github.com/Microsoft/vscode-python/issues/5212)) +1. Add telemetry around people hitting 'no' on the enable interactive shift enter. + Reword the message to be more descriptive. + ([#5213](https://github.com/Microsoft/vscode-python/issues/5213)) +1. Fix failing variable explorer test. + ([#5348](https://github.com/Microsoft/vscode-python/issues/5348)) +1. Reliably end test tasks in Azure Pipelines. + ([#5129](https://github.com/Microsoft/vscode-python/issues/5129)) +1. Deprecate [travis](https://travis-ci.org/) in favor of [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/). + ([#4024](https://github.com/Microsoft/vscode-python/issues/4024)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.3.3 (8 April 2019) + +### Fixes + +1. Update ptvsd to [4.2.7](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.7). + - Fix issues related to debugging Django templates. +1. Update the Python language server to 0.2.47. + +### Code Health + +1. Capture telemetry to track switching to and from the Language Server. + ([#5162](https://github.com/Microsoft/vscode-python/issues/5162)) + +## 2019.3.2 (2 April 2019) + +### Fixes + +1. Fix regression preventing the expansion of variables in the watch window and the debug console. + ([#5035](https://github.com/Microsoft/vscode-python/issues/5035)) +1. Display survey banner (again) for Language Server when using current Language Server. + ([#5064](https://github.com/Microsoft/vscode-python/issues/5064)) +1. Update ptvsd to [4.2.6](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.6). + ([#5083](https://github.com/Microsoft/vscode-python/issues/5083)) + - Fix issue with expanding variables in watch window and hover. + - Fix issue with launching a sub-module. + +### Code Health + +1. Capture telemetry to track which installer was used when installing packages via the extension. + ([#5063](https://github.com/Microsoft/vscode-python/issues/5063)) + +## 2019.3.1 (28 March 2019) + +### Enhancements + +1. Use the download same logic for `stable` channel of the `Language Server` as that in `beta`. + ([#4960](https://github.com/Microsoft/vscode-python/issues/4960)) + +### Code Health + +1. Capture telemetry when tests are disabled.. + ([#4801](https://github.com/Microsoft/vscode-python/issues/4801)) + +## 2019.3.6139 (26 March 2019) + +### Enhancements + +1. Add support for poetry to install packages. + ([#1871](https://github.com/Microsoft/vscode-python/issues/1871)) +1. Disabled opening the output pane when sorting imports via isort fails. + (thanks [chrised](https://github.com/chrised/)) + ([#2522](https://github.com/Microsoft/vscode-python/issues/2522)) +1. Remove run all cells codelens and replace with run above and run below commands + Add run to and from line commands in context menu + ([#4259](https://github.com/Microsoft/vscode-python/issues/4259)) +1. Support multi-root workspaces in test explorer. + ([#4268](https://github.com/Microsoft/vscode-python/issues/4268)) +1. Added support for fetching variable values from the jupyter server + ([#4291](https://github.com/Microsoft/vscode-python/issues/4291)) +1. Added commands translation for polish locale. + (thanks [pypros](https://github.com/pypros/)) + ([#4435](https://github.com/Microsoft/vscode-python/issues/4435)) +1. Show sub-tests in a subtree in the test explorer. + ([#4503](https://github.com/Microsoft/vscode-python/issues/4503)) +1. Add support for palette commands for Live Share scenarios. + ([#4520](https://github.com/Microsoft/vscode-python/issues/4520)) +1. Retain state of tests when auto discovering tests. + ([#4576](https://github.com/Microsoft/vscode-python/issues/4576)) +1. Update icons and tooltip in test explorer indicating status of test files/suites + ([#4583](https://github.com/Microsoft/vscode-python/issues/4583)) +1. Add 'ignoreVscodeTheme' setting to allow a user to skip using the theme for VS Code in the Python Interactive Window. + ([#4640](https://github.com/Microsoft/vscode-python/issues/4640)) +1. Add telemetry around imports. + ([#4718](https://github.com/Microsoft/vscode-python/issues/4718)) +1. Update status of test suite when all tests pass + ([#4727](https://github.com/Microsoft/vscode-python/issues/4727)) +1. Add button to ignore the message warning about the use of the macOS system install of Python. + (thanks [Alina Lobastova](https://github.com/alina7091)) + ([#4448](https://github.com/Microsoft/vscode-python/issues/4448)) +1. Add "Run In Interactive" command to run the contents of a file not cell by cell. Group data science context commands in one group. Add run file command to explorer context menu. + ([#4855](https://github.com/Microsoft/vscode-python/issues/4855)) + +### Fixes + +1. Add 'errorBackgroundColor' (defaults to white/#FFFFFF) for errors in the Interactive Window. Computes foreground based on background. + ([#3175](https://github.com/Microsoft/vscode-python/issues/3175)) +1. If selection is being sent to the Interactive Windows still allow for context menu commands to run selection in terminal or run file in terminal + ([#4207](https://github.com/Microsoft/vscode-python/issues/4207)) +1. Support multiline comments for markdown cells + ([#4215](https://github.com/Microsoft/vscode-python/issues/4215)) +1. Conda activation fails when there is a space in the env name + ([#4243](https://github.com/Microsoft/vscode-python/issues/4243)) +1. Fixes to ensure tests work in multi-root workspaces. + ([#4268](https://github.com/Microsoft/vscode-python/issues/4268)) +1. Allow Interactive Window to run commands as both `-m jupyter command` and as `-m command` + ([#4306](https://github.com/Microsoft/vscode-python/issues/4306)) +1. Fix shift enter to send selection when cells are defined. + ([#4413](https://github.com/Microsoft/vscode-python/issues/4413)) +1. Test explorer icon should be hidden when tests are disabled + ([#4494](https://github.com/Microsoft/vscode-python/issues/4494)) +1. Fix double running of cells with the context menu + ([#4532](https://github.com/Microsoft/vscode-python/issues/4532)) +1. Show an "unknown" icon when test status is unknown. + ([#4578](https://github.com/Microsoft/vscode-python/issues/4578)) +1. Add sys info when switching interpreters + ([#4588](https://github.com/Microsoft/vscode-python/issues/4588)) +1. Display test explorer when discovery has been run. + ([#4590](https://github.com/Microsoft/vscode-python/issues/4590)) +1. Resolve `pythonPath` before comparing it to shebang + ([#4601](https://github.com/Microsoft/vscode-python/issues/4601)) +1. When sending selection to the Interactive Window nothing selected should send the entire line + ([#4604](https://github.com/Microsoft/vscode-python/issues/4604)) +1. Provide telemetry for when we show the shift+enter banner and if the user clicks yes + ([#4636](https://github.com/Microsoft/vscode-python/issues/4636)) +1. Better error message when connecting to remote server + ([#4666](https://github.com/Microsoft/vscode-python/issues/4666)) +1. Fix problem with restart never finishing + ([#4691](https://github.com/Microsoft/vscode-python/issues/4691)) +1. Fixes to ensure we invoke the right command when running a parameterized test function. + ([#4713](https://github.com/Microsoft/vscode-python/issues/4713)) +1. Handle view state changes for the Python Interactive window so that it gains focus when appropriate. (CTRL+1/2/3 etc should give focus to the interactive window) + ([#4733](https://github.com/Microsoft/vscode-python/issues/4733)) +1. Don't have "run all above" on first cell and don't start history for empty code runs + ([#4743](https://github.com/Microsoft/vscode-python/issues/4743)) +1. Perform case insensitive comparison of Python Environment paths + ([#4797](https://github.com/Microsoft/vscode-python/issues/4797)) +1. Ensure `Jedi` uses the currently selected interpreter. + (thanks [Selim Belhaouane](https://github.com/selimb)) + ([#4687](https://github.com/Microsoft/vscode-python/issues/4687)) +1. Multiline comments with text on the first line break Python Interactive window execution. + ([#4791](https://github.com/Microsoft/vscode-python/issues/4791)) +1. Fix status bar when using Live Share or just starting the Python Interactive window. + ([#4853](https://github.com/Microsoft/vscode-python/issues/4853)) +1. Change the names of our "Run All Cells Above" and "Run Cell and All Below" commands to be more concise + ([#4876](https://github.com/Microsoft/vscode-python/issues/4876)) +1. Ensure the `Python` output panel does not steal focus when there errors in the `Language Server`. + ([#4868](https://github.com/Microsoft/vscode-python/issues/4868)) +1. Update ptvsd to [4.2.5](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.5). + ([#4932](https://github.com/Microsoft/vscode-python/issues/4932)) + - Fix issues with django and jinja2 exceptions. + - Detaching sometimes throws ValueError. + - StackTrace request respecting just-my-code. + - Don't give error redirecting output with pythonw. + - Fix for stop on entry issue. +1. Update the Python language server to 0.2.31. + +### Code Health + +1. Add a Python script to run PyTest correctly for discovery. + ([#4033](https://github.com/Microsoft/vscode-python/issues/4033)) +1. Ensure post npm install scripts do not fail when run more than once. + ([#4109](https://github.com/Microsoft/vscode-python/issues/4109)) +1. Improve Azure DevOps pipeline for PR validation. Added speed improvements, documented the process better, and simplified what happens in PR validation. + ([#4123](https://github.com/Microsoft/vscode-python/issues/4123)) +1. Move to new Azure DevOps instance and bring the Nightly CI build closer to running cleanly by skipping tests and improving reporting transparency. + ([#4336](https://github.com/Microsoft/vscode-python/issues/4336)) +1. Add more logging to diagnose issues getting the Python Interactive window to show up. + Add checks for Conda activation never finishing. + ([#4424](https://github.com/Microsoft/vscode-python/issues/4424)) +1. Update `nyc` and remove `gulp-watch` and `gulp-debounced-watch`. + ([#4490](https://github.com/Microsoft/vscode-python/issues/4490)) +1. Force WS to at least 3.3.1 to alleviate security concerns. + ([#4497](https://github.com/Microsoft/vscode-python/issues/4497)) +1. Add tests for Live Share support. + ([#4521](https://github.com/Microsoft/vscode-python/issues/4521)) +1. Fix running Live Share support in a release build. + ([#4529](https://github.com/Microsoft/vscode-python/issues/4529)) +1. Delete the `pvsc-dev-ext.py` file as it was not being properly maintained. + ([#4530](https://github.com/Microsoft/vscode-python/issues/4530)) +1. Increase timeouts for loading of extension when preparing to run tests. + ([#4540](https://github.com/Microsoft/vscode-python/issues/4540)) +1. Exclude files `travis*.log`, `pythonFiles/tests/**`, `types/**` from the extension. + ([#4554](https://github.com/Microsoft/vscode-python/issues/4554)) +1. Exclude `*.vsix` from source control. + ([#4556](https://github.com/Microsoft/vscode-python/issues/4556)) +1. Add more logging for ECONNREFUSED errors and Jupyter server crashes + ([#4573](https://github.com/Microsoft/vscode-python/issues/4573)) +1. Add travis task to verify bundle can be created. + ([#4711](https://github.com/Microsoft/vscode-python/issues/4711)) +1. Add manual test plan for data science + ([#4716](https://github.com/Microsoft/vscode-python/issues/4716)) +1. Fix Live Share nightly functional tests + ([#4757](https://github.com/Microsoft/vscode-python/issues/4757)) +1. Make cancel test and server cache test more robust + ([#4818](https://github.com/Microsoft/vscode-python/issues/4818)) +1. Generalize code used to parse Test results service + ([#4796](https://github.com/Microsoft/vscode-python/issues/4796)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.2.2 (6 March 2019) + +### Fixes + +1. If selection is being sent to the Interactive Windows still allow for context menu commands to run selection in terminal or run file in terminal + ([#4207](https://github.com/Microsoft/vscode-python/issues/4207)) +1. When sending selection to the Interactive Window nothing selected should send the entire line + ([#4604](https://github.com/Microsoft/vscode-python/issues/4604)) +1. Provide telemetry for when we show the shift-enter banner and if the user clicks yes + ([#4636](https://github.com/Microsoft/vscode-python/issues/4636)) + +## 2019.2.5433 (27 Feb 2019) + +### Fixes + +1. Exclude files `travis*.log`, `pythonFiles/tests/**`, `types/**` from the extension. + ([#4554](https://github.com/Microsoft/vscode-python/issues/4554)) + ([#4566](https://github.com/Microsoft/vscode-python/issues/4566)) + +## 2019.2.0 (26 Feb 2019) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Support launch configs for debugging tests. + ([#332](https://github.com/Microsoft/vscode-python/issues/332)) +1. Add way to send code to the Python Interactive window without having to put `#%%` into a file. + ([#3171](https://github.com/Microsoft/vscode-python/issues/3171)) +1. Support simple variable substitution in `.env` files. + ([#3275](https://github.com/Microsoft/vscode-python/issues/3275)) +1. Support live share in Python interactive window (experimental). + ([#3581](https://github.com/Microsoft/vscode-python/issues/3581)) +1. Strip comments before sending so shell command and multiline jupyter magics work correctly. + ([#4064](https://github.com/Microsoft/vscode-python/issues/4064)) +1. Add a build number to our released builds. + ([#4183](https://github.com/Microsoft/vscode-python/issues/4183)) +1. Prompt the user to send shift-enter to the interactive window. + ([#4184](https://github.com/Microsoft/vscode-python/issues/4184)) +1. Added Dutch translation. + (thanks [Robin Martijn](https://github.com/Bowero) with the feedback of [Michael van Tellingen](https://github.com/mvantellingen)) + ([#4186](https://github.com/Microsoft/vscode-python/issues/4186)) +1. Add the Test Activity view. + ([#4272](https://github.com/Microsoft/vscode-python/issues/4272)) +1. Added action buttons to top of Test Explorer. + ([#4275](https://github.com/Microsoft/vscode-python/issues/4275)) +1. Navigation to test output from Test Explorer. + ([#4279](https://github.com/Microsoft/vscode-python/issues/4279)) +1. Add the command 'Configure Unit Tests'. + ([#4286](https://github.com/Microsoft/vscode-python/issues/4286)) +1. Do not update unit test settings if configuration is cancelled. + ([#4287](https://github.com/Microsoft/vscode-python/issues/4287)) +1. Keep testing configuration alive when losing UI focus. + ([#4288](https://github.com/Microsoft/vscode-python/issues/4288)) +1. Display test activity only when tests have been discovered. + ([#4317](https://github.com/Microsoft/vscode-python/issues/4317)) +1. Added a button to configure unit tests when prompting users that tests weren't discovered. + ([#4318](https://github.com/Microsoft/vscode-python/issues/4318)) +1. Use VSC API to open browser window + ([#4322](https://github.com/Microsoft/vscode-python/issues/4322)) +1. Don't shut down the notebook server on window close. + ([#4348](https://github.com/Microsoft/vscode-python/issues/4348)) +1. Added command `Show Output` to display the `Python` output panel. + ([#4362](https://github.com/Microsoft/vscode-python/issues/4362)) +1. Fix order of icons in test explorer and items. + ([#4364](https://github.com/Microsoft/vscode-python/issues/4364)) +1. Run failed tests icon should only appear if and when a test has failed. + ([#4371](https://github.com/Microsoft/vscode-python/issues/4371)) +1. Update ptvsd to [4.2.4](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.4). + ([#4457](https://github.com/Microsoft/vscode-python/issues/4457)) + - Validate breakpoint targets. + - Properly exclude certain files from showing up in the debugger. + +### Fixes + +1. Add support for multi root workspaces with the new language server server. + ([#3008](https://github.com/Microsoft/vscode-python/issues/3008)) +1. Move linting tests to unit-testing for better reliability. + ([#3914](https://github.com/Microsoft/vscode-python/issues/3914)) +1. Allow "Run Cell" code lenses on non-local files. + ([#3995](https://github.com/Microsoft/vscode-python/issues/3995)) +1. Functional test for the input portion of the python interactive window. + ([#4057](https://github.com/Microsoft/vscode-python/issues/4057)) +1. Fix hitting the up arrow on the input prompt for the Python Interactive window to behave like the terminal window when only 1 item in the history. + ([#4145](https://github.com/Microsoft/vscode-python/issues/4145)) +1. Fix problem with webview panel not being dockable anywhere but view column 2. + ([#4237](https://github.com/Microsoft/vscode-python/issues/4237)) +1. More fixes for history in the Python Interactive window input prompt. + ([#4255](https://github.com/Microsoft/vscode-python/issues/4255)) +1. Fix precedence in `parsePyTestModuleCollectionResult`. + (thanks [Tammo Ippen](https://github.com/tammoippen)) + ([#4360](https://github.com/Microsoft/vscode-python/issues/4360)) +1. Revert pipenv activation to not use `pipenv` shell.` + ([#4394](https://github.com/Microsoft/vscode-python/issues/4394)) +1. Fix shift enter to send selection when cells are defined. + ([#4413](https://github.com/Microsoft/vscode-python/issues/4413)) +1. Icons should display only in test explorer. + ([#4418](https://github.com/Microsoft/vscode-python/issues/4418)) +1. Update ptvsd to [4.2.4](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.4). + ([#4457](https://github.com/Microsoft/vscode-python/issues/4457)) + - `BreakOnSystemExitZero` now respected. + - Fix a bug causing breakpoints not to be hit when attached to a remote target. +1. Fix double running of cells with the context menu + ([#4532](https://github.com/Microsoft/vscode-python/issues/4532)) +1. Update the Python language server to 0.1.80. + +### Code Health + +1. Fix all typescript errors when compiled in strict mode. + ([#611](https://github.com/Microsoft/vscode-python/issues/611)) +1. Get functional tests running nightly again. + ([#3973](https://github.com/Microsoft/vscode-python/issues/3973)) +1. Turn on strict type checking (typescript compiling) for Datascience code. + ([#4058](https://github.com/Microsoft/vscode-python/issues/4058)) +1. Turn on strict typescript compile for the data science react code. + ([#4091](https://github.com/Microsoft/vscode-python/issues/4091)) +1. Fix issue causing debugger tests to timeout on CI servers. + ([#4148](https://github.com/Microsoft/vscode-python/issues/4148)) +1. Don't register language server onTelemetry when downloadLanguageServer is false. + ([#4199](https://github.com/Microsoft/vscode-python/issues/4199)) +1. Fixes to smoke tests on CI. + ([#4201](https://github.com/Microsoft/vscode-python/issues/4201)) + +## 2019.1.0 (29 Jan 2019) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Add the capability to have custom regex's for cell/markdown matching + ([#4065](https://github.com/Microsoft/vscode-python/issues/4065)) +1. Perform all validation checks in the background + ([#3019](https://github.com/Microsoft/vscode-python/issues/3019)) +1. Watermark for Python Interactive input prompt + ([#4111](https://github.com/Microsoft/vscode-python/issues/4111)) +1. Create diagnostics for failed/skipped tests that were run with pytest. + (thanks [Chris NeJame](https://github.com/SalmonMode/)) + ([#120](https://github.com/Microsoft/vscode-python/issues/120)) +1. Add the python.pipenvPath config setting. + ([#978](https://github.com/Microsoft/vscode-python/issues/978)) +1. Add localRoot and remoteRoot defaults for Remote Debugging configuration in `launch.json`. + ([#1385](https://github.com/Microsoft/vscode-python/issues/1385)) +1. Activate `pipenv` environments in the shell using the command `pipenv shell`. + ([#2855](https://github.com/Microsoft/vscode-python/issues/2855)) +1. Use Pylint message names instead of codes + (thanks to [Roman Kornev](https://github.com/RomanKornev/)) + ([#2906](https://github.com/Microsoft/vscode-python/issues/2906)) +1. Add ability to enter Python code directly into the Python Interactive window + ([#3101](https://github.com/Microsoft/vscode-python/issues/3101)) +1. Allow interactive window inputs to either be collapsed by default or totally hidden + ([#3266](https://github.com/Microsoft/vscode-python/issues/3266)) +1. Notify the user when language server extraction fails + ([#3268](https://github.com/Microsoft/vscode-python/issues/3268)) +1. Indent on enter after line continuations. + ([#3284](https://github.com/Microsoft/vscode-python/issues/3284)) +1. Improvements to automatic selection of the Python interpreter. + ([#3369](https://github.com/Microsoft/vscode-python/issues/3369)) +1. Add support for column numbers for problems returned by `mypy`. + (thanks [Eric Traut](https://github.com/erictraut)) + ([#3597](https://github.com/Microsoft/vscode-python/issues/3597)) +1. Display actionable message when language server is not supported + ([#3634](https://github.com/Microsoft/vscode-python/issues/3634)) +1. Make sure we are looking for conda in all the right places + ([#3641](https://github.com/Microsoft/vscode-python/issues/3641)) +1. Improvements to message displayed when linter is not installed + ([#3659](https://github.com/Microsoft/vscode-python/issues/3659)) +1. Improvements to message displayed when Python path is invalid (in launch.json) + ([#3661](https://github.com/Microsoft/vscode-python/issues/3661)) +1. Add the Jupyter Server URI to the Interactive Window info cell + ([#3668](https://github.com/Microsoft/vscode-python/issues/3668)) +1. Handle errors happening during extension activation. + ([#3740](https://github.com/Microsoft/vscode-python/issues/3740)) +1. Validate Mac Interpreters in the background. + ([#3908](https://github.com/Microsoft/vscode-python/issues/3908)) +1. When cell inputs to Python Interactive are hidden, don't show cells without any output + ([#3981](https://github.com/Microsoft/vscode-python/issues/3981)) + +### Fixes + +1. Have the new export commands use our directory change code + ([#4140](https://github.com/Microsoft/vscode-python/issues/4140)) +1. Theme should not be exported without output when doing an export. + ([#4141](https://github.com/Microsoft/vscode-python/issues/4141)) +1. Deleting all cells should not remove the input prompt + ([#4152](https://github.com/Microsoft/vscode-python/issues/4152)) +1. Fix ctrl+c to work in code that has already been entered + ([#4168](https://github.com/Microsoft/vscode-python/issues/4168)) +1. Auto-select virtual environment in multi-root workspaces + ([#3501](https://github.com/Microsoft/vscode-python/issues/3501)) +1. Validate interpreter in multi-root workspaces + ([#3502](https://github.com/Microsoft/vscode-python/issues/3502)) +1. Allow clicking anywhere in an input cell to give focus to the input box for the Python Interactive window + ([#4076](https://github.com/Microsoft/vscode-python/issues/4076)) +1. Cursor in Interactive Windows now appears on whitespace + ([#4081](https://github.com/Microsoft/vscode-python/issues/4081)) +1. Fix problem with double scrollbars when typing in the input window. Make code wrap instead. + ([#4084](https://github.com/Microsoft/vscode-python/issues/4084)) +1. Remove execution count from the prompt cell. + ([#4086](https://github.com/Microsoft/vscode-python/issues/4086)) +1. Make sure showing a plain Python Interactive window lists out the sys info + ([#4088](https://github.com/Microsoft/vscode-python/issues/4088)) +1. Fix Python interactive window up/down arrows in the input prompt to behave like a terminal. + ([#4092](https://github.com/Microsoft/vscode-python/issues/4092)) +1. Handle stdout changes with updates to pytest 4.1.x series (without breaking 4.0.x series parsing). + ([#4099](https://github.com/Microsoft/vscode-python/issues/4099)) +1. Fix bug affecting multiple linters used in a workspace. + (thanks [Ilia Novoselov](https://github.com/nullie)) + ([#2571](https://github.com/Microsoft/vscode-python/issues/2571)) +1. Activate any selected Python Environment when running unit tests. + ([#3330](https://github.com/Microsoft/vscode-python/issues/3330)) +1. Ensure extension does not start multiple language servers. + ([#3346](https://github.com/Microsoft/vscode-python/issues/3346)) +1. Add support for running an entire file in the Python Interactive window + ([#3362](https://github.com/Microsoft/vscode-python/issues/3362)) +1. When in multi-root workspace, store selected python path in the `settings.json` file of the workspace folder. + ([#3419](https://github.com/Microsoft/vscode-python/issues/3419)) +1. Fix console wrapping in output so that console based status bars and spinners work. + ([#3529](https://github.com/Microsoft/vscode-python/issues/3529)) +1. Support other virtual environments besides conda + ([#3537](https://github.com/Microsoft/vscode-python/issues/3537)) +1. Fixed tests related to the `onEnter` format provider. + ([#3674](https://github.com/Microsoft/vscode-python/issues/3674)) +1. Lowering threshold for Language Server support on a platform. + ([#3693](https://github.com/Microsoft/vscode-python/issues/3693)) +1. Survive missing kernelspecs as a default will be created. + ([#3699](https://github.com/Microsoft/vscode-python/issues/3699)) +1. Activate the extension when loading ipynb files + ([#3734](https://github.com/Microsoft/vscode-python/issues/3734)) +1. Don't restart the Jupyter server on any settings change. Also don't throw interpreter changed events on unrelated settings changes. + ([#3749](https://github.com/Microsoft/vscode-python/issues/3749)) +1. Support whitespace (tabs and spaces) in output + ([#3757](https://github.com/Microsoft/vscode-python/issues/3757)) +1. Ensure file names are not captured when sending telemetry for unit tests. + ([#3767](https://github.com/Microsoft/vscode-python/issues/3767)) +1. Address problem with Python Interactive icons not working in insider's build. VS Code is more restrictive on what files can load in a webview. + ([#3775](https://github.com/Microsoft/vscode-python/issues/3775)) +1. Fix output so that it wraps '<' entries in <xmp> to allow html like tags to be output. + ([#3824](https://github.com/Microsoft/vscode-python/issues/3824)) +1. Keep the Jupyter remote server URI input box open so you can copy and paste into it easier + ([#3856](https://github.com/Microsoft/vscode-python/issues/3856)) +1. Changes to how source maps are enabled and disabled in the extension. + ([#3905](https://github.com/Microsoft/vscode-python/issues/3905)) +1. Clean up command names for data science + ([#3925](https://github.com/Microsoft/vscode-python/issues/3925)) +1. Add more data when we get an unknown mime type + ([#3945](https://github.com/Microsoft/vscode-python/issues/3945)) +1. Match dots in ignorePatterns globs; fixes .venv not being ignored + (thanks to [Russell Davis](https://github.com/russelldavis)) + ([#3947](https://github.com/Microsoft/vscode-python/issues/3947)) +1. Remove duplicates from interpreters listed in the interpreter selection list. + ([#3953](https://github.com/Microsoft/vscode-python/issues/3953)) +1. Add telemetry for local versus remote connect + ([#3985](https://github.com/Microsoft/vscode-python/issues/3985)) +1. Add new maxOutputSize setting for text output in the Python Interactive window. -1 means infinite, otherwise the number of pixels. + ([#4010](https://github.com/Microsoft/vscode-python/issues/4010)) +1. fix `pythonPath` typo (thanks [David Lechner](https://github.com/dlech)) + ([#4047](https://github.com/Microsoft/vscode-python/issues/4047)) +1. Fix a type in generated header comment when importing a notebook: `DataSciece` --> `DataScience`. + (thanks [sunt05](https://github.com/sunt05)) + ([#4048](https://github.com/Microsoft/vscode-python/issues/4048)) +1. Allow clicking anywhere in an input cell to give focus to the input box for the Python Interactive window + ([#4076](https://github.com/Microsoft/vscode-python/issues/4076)) +1. Fix problem with double scrollbars when typing in the input window. Make code wrap instead. + ([#4084](https://github.com/Microsoft/vscode-python/issues/4084)) +1. Remove execution count from the prompt cell. + ([#4086](https://github.com/Microsoft/vscode-python/issues/4086)) +1. Make sure showing a plain Python Interactive window lists out the sys info + ([#4088](https://github.com/Microsoft/vscode-python/issues/4088)) + +### Code Health + +1. Fix build issue with code.tsx + ([#4156](https://github.com/Microsoft/vscode-python/issues/4156)) +1. Expose an event to notify changes to settings instead of casting settings to concrete class. + ([#642](https://github.com/Microsoft/vscode-python/issues/642)) +1. Created system test to ensure terminal gets activated with anaconda environment + ([#1521](https://github.com/Microsoft/vscode-python/issues/1521)) +1. Added system tests to ensure terminal gets activated with virtualenv environment + ([#1522](https://github.com/Microsoft/vscode-python/issues/1522)) +1. Added system test to ensure terminal gets activated with pipenv + ([#1523](https://github.com/Microsoft/vscode-python/issues/1523)) +1. Fix flaky tests related to auto selection of virtual environments. + ([#2339](https://github.com/Microsoft/vscode-python/issues/2339)) +1. Use enums for event names instead of constants. + ([#2904](https://github.com/Microsoft/vscode-python/issues/2904)) +1. Add tests for clicking buttons in history pane + ([#3084](https://github.com/Microsoft/vscode-python/issues/3084)) +1. Add tests for clear and delete buttons in the history pane + ([#3087](https://github.com/Microsoft/vscode-python/issues/3087)) +1. Add tests for clicking buttons on individual cells + ([#3092](https://github.com/Microsoft/vscode-python/issues/3092)) +1. Handle a 404 when trying to download the language server + ([#3267](https://github.com/Microsoft/vscode-python/issues/3267)) +1. Ensure new warnings are not ignored when bundling the extension with WebPack. + ([#3468](https://github.com/Microsoft/vscode-python/issues/3468)) +1. Update our CI/nightly full build to a YAML definition build in Azure DevOps. + ([#3555](https://github.com/Microsoft/vscode-python/issues/3555)) +1. Add mock of Jupyter API to allow functional tests to run more quickly and more consistently. + ([#3556](https://github.com/Microsoft/vscode-python/issues/3556)) +1. Use Jedi if Language Server fails to activate + ([#3633](https://github.com/Microsoft/vscode-python/issues/3633)) +1. Fix the timeout for DataScience functional tests + ([#3682](https://github.com/Microsoft/vscode-python/issues/3682)) +1. Fixed language server smoke tests. + ([#3684](https://github.com/Microsoft/vscode-python/issues/3684)) +1. Add a functional test for interactive window remote connect scenario + ([#3714](https://github.com/Microsoft/vscode-python/issues/3714)) +1. Detect usage of `xonsh` shells (this does **not** add support for `xonsh` itself) + ([#3746](https://github.com/Microsoft/vscode-python/issues/3746)) +1. Remove `src/server` folder, as this is no longer required. + ([#3781](https://github.com/Microsoft/vscode-python/issues/3781)) +1. Bugfix to `pvsc-dev-ext.py` where arguments to git would not be passed on POSIX-based environments. Extended `pvsc-dev-ext.py setup` command with 2 + optional flags-- `--repo` and `--branch` to override the default git repository URL and the branch used to clone and install the extension. + (thanks [Anthony Shaw](https://github.com/tonybaloney/)) + ([#3837](https://github.com/Microsoft/vscode-python/issues/3837)) +1. Improvements to execution times of CI on Travis. + ([#3899](https://github.com/Microsoft/vscode-python/issues/3899)) +1. Add telemetry to check if global interpreter is used in workspace. + ([#3901](https://github.com/Microsoft/vscode-python/issues/3901)) +1. Make sure to search for the best Python when launching the non default interpreter. + ([#3916](https://github.com/Microsoft/vscode-python/issues/3916)) +1. Add tests for expand / collapse and hiding of cell inputs mid run + ([#3982](https://github.com/Microsoft/vscode-python/issues/3982)) +1. Move `splitParent` from `string.ts` into tests folder. + ([#3988](https://github.com/Microsoft/vscode-python/issues/3988)) +1. Ensure `debounce` decorator cannot be applied to async functions. + ([#4055](https://github.com/Microsoft/vscode-python/issues/4055)) + +## 2018.12.1 (14 Dec 2018) + +### Fixes + +1. Lowering threshold for Language Server support on a platform. + ([#3693](https://github.com/Microsoft/vscode-python/issues/3693)) +1. Fix bug affecting multiple linters used in a workspace. + (thanks [Ilia Novoselov](https://github.com/nullie)) + ([#3700](https://github.com/Microsoft/vscode-python/issues/3700)) + +## 2018.12.0 (13 Dec 2018) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Load the configured language server in the background during extension activation. + ([#3020](https://github.com/Microsoft/vscode-python/issues/3020)) +1. Display progress indicator when activating the language server and validating user setup. + ([#3082](https://github.com/Microsoft/vscode-python/issues/3082)) +1. Allow for connection to a remote `Jupyter` server. + ([#3316](https://github.com/Microsoft/vscode-python/issues/3316)) +1. Allow users to request the 'Install missing Linter' prompt to not show again for `pylint`. + ([#3349](https://github.com/Microsoft/vscode-python/issues/3349)) +1. Add the `Jupyter` server URI to the interactive window info cell. + ([#3668](https://github.com/Microsoft/vscode-python/issues/3668)) + +### Fixes + +1. Updated logic used to determine whether the Microsoft Python Language Server is supported. + ([#2729](https://github.com/Microsoft/vscode-python/issues/2729)) +1. Add export from the Python interactive window as a notebook file. + ([#3109](https://github.com/Microsoft/vscode-python/issues/3109)) +1. Fix issue with the `unittest` runner where test suite/module initialization methods were not for a single test method. + (thanks [Alex Yu](https://github.com/alexander-yu)) + ([#3295](https://github.com/Microsoft/vscode-python/issues/3295)) +1. Activate `conda` prior to running `jupyter` for the Python interactive window. + ([#3341](https://github.com/Microsoft/vscode-python/issues/3341)) +1. Respect value defined for `pylintEnabled` in user `settings.json`. + ([#3388](https://github.com/Microsoft/vscode-python/issues/3388)) +1. Expand variables in `pythonPath` before validating it. + ([#3392](https://github.com/Microsoft/vscode-python/issues/3392)) +1. Clear cached display name of Python if interpreter changes. + ([#3406](https://github.com/Microsoft/vscode-python/issues/3406)) +1. Run in the workspace directory by default for the interactive window. + ([#3407](https://github.com/Microsoft/vscode-python/issues/3407)) +1. Create a default config when starting a local `Jupyter` server to resolve potential conflicts with user's custom configuration. + ([#3475](https://github.com/Microsoft/vscode-python/issues/3475)) +1. Add support for running Python interactive commands from the command palette. + ([#3476](https://github.com/Microsoft/vscode-python/issues/3476)) +1. Handle interrupts crashing the kernel. + ([#3511](https://github.com/Microsoft/vscode-python/issues/3511)) +1. Revert `ctags` argument from `--extras` to `--extra`. + ([#3517](https://github.com/Microsoft/vscode-python/issues/3517)) +1. Fix problems with `jupyter` startup related to custom configurations. + ([#3533](https://github.com/Microsoft/vscode-python/issues/3533)) +1. Fix crash when `kernelspec` is missing path or language. + ([#3561](https://github.com/Microsoft/vscode-python/issues/3561)) +1. Update the Microsoft Python Language Server to 0.1.72/[2018.12.1](https://github.com/Microsoft/python-language-server/releases/tag/2018.12.1) ([#3657](https://github.com/Microsoft/vscode-python/issues/3657)): + - Properly resolve namespace packages and relative imports. + - `Go to Definition` now supports namespace packages. + - Fixed `null` reference exceptions. + - Fixed erroneously reporting `None`, `True`, and `False` as undefined. + +### Code Health + +1. Pin python dependencies bundled with the extension in a `requirements.txt` file. + ([#2965](https://github.com/Microsoft/vscode-python/issues/2965)) +1. Remove scripts that bundled the extension using the old way, without webpack. + ([#3479](https://github.com/Microsoft/vscode-python/issues/3479)) +1. Fix environment variable token in Azure DevOps YAML. + ([#3630](https://github.com/Microsoft/vscode-python/issues/3630)) +1. Add missing imports and enable functional tests. + ([#3649](https://github.com/Microsoft/vscode-python/issues/3649)) +1. Enable code coverage for unit tests and functional tests. + ([#3650](https://github.com/Microsoft/vscode-python/issues/3650)) +1. Add logging for improved diagnostics. + ([#3460](https://github.com/Microsoft/vscode-python/issues/3460)) + +## 2018.11.0 (29 Nov 2018) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.13.1](https://pypi.org/project/jedi/0.13.1/) + and [parso 0.3.1](https://pypi.org/project/parso/0.3.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Update Jedi to 0.13.1 and parso 0.3.1. + ([#2667](https://github.com/Microsoft/vscode-python/issues/2667)) +1. Make diagnostic message actionable when opening a workspace with no currently selected Python interpreter. + ([#2983](https://github.com/Microsoft/vscode-python/issues/2983)) +1. Expose an API that can be used by other extensions to interact with the Python Extension. + ([#3121](https://github.com/Microsoft/vscode-python/issues/3121)) +1. Updated the language server to [0.1.65](https://github.com/Microsoft/python-language-server/releases/tag/2018.11.1): + - Improved `formatOnType` so it handles mismatched braces better + ([#3482](https://github.com/Microsoft/vscode-python/issues/3482)) + +### Fixes + +1. Have `ctags` use the `--extras` option instead of `--extra`. + (thanks to [Brandy Sandrowicz](https://github.com/bsandrow)) + ([#793](https://github.com/Microsoft/vscode-python/issues/793)) +1. Always use bundled version of [`ptvsd`](https://github.com/microsoft/ptvsd), unless specified. + To use a custom version of `ptvsd` in the debugger, add `customDebugger` into your `launch.json` configuration as follows: + ```json + "type": "python", + "request": "launch", + "customDebugger": true + ``` + ([#3283](https://github.com/Microsoft/vscode-python/issues/3283)) +1. Fix problems with virtual environments not matching the loaded python when running cells. + ([#3294](https://github.com/Microsoft/vscode-python/issues/3294)) +1. Add button for interrupting the jupyter kernel + ([#3314](https://github.com/Microsoft/vscode-python/issues/3314)) +1. Auto select `Python Interpreter` prior to validation of interpreters and changes to messages displayed. + ([#3326](https://github.com/Microsoft/vscode-python/issues/3326)) +1. Fix Jupyter server connection issues involving IP addresses, base_url, and missing tokens + ([#3332](https://github.com/Microsoft/vscode-python/issues/3332)) +1. Make `nbconvert` in a installation not prevent notebooks from starting. + ([#3343](https://github.com/Microsoft/vscode-python/issues/3343)) +1. Re-run Jupyter notebook setup when the kernel is restarted. This correctly picks up dark color themes for matplotlib. + ([#3418](https://github.com/Microsoft/vscode-python/issues/3418)) +1. Update the language server to [0.1.65](https://github.com/Microsoft/python-language-server/releases/tag/2018.11.1): + - Fixed `null` reference exception when executing "Find symbol in workspace" + - Fixed `null` argument exception that could happen when a function used tuples + - Fixed issue when variables in nested list comprehensions were marked as undefined + - Fixed exception that could be thrown with certain generic syntax + ([#3482](https://github.com/Microsoft/vscode-python/issues/3482)) + +### Code Health + +1. Added basic integration tests for the new Language Server. + ([#2041](https://github.com/Microsoft/vscode-python/issues/2041)) +1. Add smoke tests for the extension. + ([#3021](https://github.com/Microsoft/vscode-python/issues/3021)) +1. Improvements to the `webpack configuration` file used to build the Data Science UI components. + Added pre-build validations to ensure all npm modules used by Data Science UI components are registered. + ([#3122](https://github.com/Microsoft/vscode-python/issues/3122)) +1. Removed `IsTestExecution` guard from around data science banner calls + ([#3246](https://github.com/Microsoft/vscode-python/issues/3246)) +1. Unit tests for `CodeLensProvider` and `CodeWatcher` + ([#3264](https://github.com/Microsoft/vscode-python/issues/3264)) +1. Use `EXTENSION_ROOT_DIR` instead of `__dirname` in preparation for bundling of extension. + ([#3317](https://github.com/Microsoft/vscode-python/issues/3317)) +1. Add YAML file specification for CI builds + ([#3350](https://github.com/Microsoft/vscode-python/issues/3350)) +1. Stop running CI tests against the `main` branch of ptvsd. + ([#3414](https://github.com/Microsoft/vscode-python/issues/3414)) +1. Be more aggressive in searching for a Python environment that can run Jupyter + (make sure to cleanup any kernelspecs that are created during this process). + ([#3433](https://github.com/Microsoft/vscode-python/issues/3433)) + +## 2018.10.1 (09 Nov 2018) + +### Fixes + +1. When attempting to 'Run Cell', get error - Cannot read property 'length' of null + ([#3286](https://github.com/Microsoft/vscode-python/issues/3286)) + +## 2018.10.0 (08 Nov 2018) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- Microsoft Python Language Server +- ptvsd +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Add support for code completion in the debug console window. + ([#1076](https://github.com/Microsoft/vscode-python/issues/1076)) +1. Add a new simple snippet for `if __name__ == '__main__':` block. The snippet can be accessed by typing `__main__` + (thanks [R S Nikhil Krishna](https://github.com/rsnk96/)) + ([#2242](https://github.com/Microsoft/vscode-python/issues/2242)) +1. Add Python Interactive mode for data science. + ([#2302](https://github.com/Microsoft/vscode-python/issues/2302)) +1. Added a debugger setting to show return values of functions while stepping. + ([#2463](https://github.com/Microsoft/vscode-python/issues/2463)) +1. Enable on-type formatting from language server + ([#2690](https://github.com/Microsoft/vscode-python/issues/2690)) +1. Add [bandit](https://pypi.org/project/bandit/) to supported linters. + (thanks [Steven Demurjian Jr.](https://github.com/demus/)) + ([#2775](https://github.com/Microsoft/vscode-python/issues/2775)) +1. Ensure `python.condaPath` supports paths relative to `Home`. E.g. `"python.condaPath":"~/anaconda3/bin/conda"`. + ([#2781](https://github.com/Microsoft/vscode-python/issues/2781)) +1. Updated the [language server](https://github.com/Microsoft/python-language-server) to [0.1.57/2018.11.0](https://github.com/Microsoft/python-language-server/releases/tag/2018.11.0) (from 2018.10.0) + and the [debugger](https://pypi.org/project/ptvsd/) to + [4.2.0](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.0) (from 4.1.3). Highlights include: + - Language server + - Completion support for [`collections.namedtuple`](https://docs.python.org/3/library/collections.html#collections.namedtuple). + - Support [`typing.NewType`](https://docs.python.org/3/library/typing.html#typing.NewType) + and [`typing.TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar). + - Debugger + - Add support for sub-process debugging (set `"subProcess": true` in your `launch.json` to use). + - Add support for [pyside2](https://pypi.org/project/PySide2/). +1. Add localization of strings. Localized versions are specified in the package.nls.\.json files. + ([#463](https://github.com/Microsoft/vscode-python/issues/463)) +1. Clear cached list of interpreters when an interpeter is created in the workspace folder (this allows for virtual environments created in one's workspace folder to be detectable immediately). + ([#656](https://github.com/Microsoft/vscode-python/issues/656)) +1. Pylint is no longer enabled by default when using the language server. Users that have not configured pylint but who have installed it in their workspace will be asked if they'd like to enable it. + ([#974](https://github.com/Microsoft/vscode-python/issues/974)) + +### Fixes + +1. Support "conda activate" after 4.4.0. + ([#1882](https://github.com/Microsoft/vscode-python/issues/1882)) +1. Fix installation of codna packages when conda environment contains spaces. + ([#2015](https://github.com/Microsoft/vscode-python/issues/2015)) +1. Ensure `python.formatting.blackPath` supports paths relative to `Home`. E.g. `"python.formatting.blackPath":"~/venv/bin/black"`. + ([#2274](https://github.com/Microsoft/vscode-python/issues/2274)) +1. Correct errors with timing, resetting, and exceptions, related to unittest during discovery and execution of tests. Re-enable `unittest.test` suite. + ([#2692](https://github.com/Microsoft/vscode-python/issues/2692)) +1. Fix colon-triggered block formatting. + ([#2714](https://github.com/Microsoft/vscode-python/issues/2714)) +1. Ensure relative paths to python interpreters in `python.pythonPath` of `settings.json` are prefixed with `./` or `.\\` (depending on the OS). + ([#2744](https://github.com/Microsoft/vscode-python/issues/2744)) +1. Give preference to PTSVD in current path. + ([#2818](https://github.com/Microsoft/vscode-python/issues/2818)) +1. Fixed a typo in the Python interpreter selection balloon for macOS. + (thanks [Joe Graham](https://github.com/joe-graham)) + ([#2868](https://github.com/Microsoft/vscode-python/issues/2868)) +1. Updated the [language server](https://github.com/Microsoft/python-language-server) to [0.1.57/2018.11.0](https://github.com/Microsoft/python-language-server/releases/tag/2018.11.0) (from 2018.10.0) + and the [debugger](https://pypi.org/project/ptvsd/) to + [4.2.0](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.0) (from 4.1.3). Highlights include: + - Language server + - Completions on generic containers work (e.g. `x: List[T]` now have completions for `x`, not just `x[]`). + - Fixed issues relating to `Go to Definition` for `from ... import` statements. + - `None` is no longer flagged as undefined. + - `BadSourceException` should no longer be raised. + - Fixed a null reference exception when handling certain function overloads. + - Debugger + - Properly deal with handled or unhandled exception in top level frames. + - Any folder ending with `site-packages` is considered a library. + - Treat any code not in `site-packages` as user code. + - Handle case where no completions are provided by the debugger. + +### Code Health + +1. Remove test-specific code from `configSettings.ts` class. + ([#2678](https://github.com/Microsoft/vscode-python/issues/2678)) +1. Add a unit test for the MyPy output regex. + ([#2696](https://github.com/Microsoft/vscode-python/issues/2696)) +1. Update all npm dependencies to use the caret operator. + ([#2746](https://github.com/Microsoft/vscode-python/issues/2746)) +1. Move contents of the folder `src/utils` into `src/client/common/utils`. + ([#2748](https://github.com/Microsoft/vscode-python/issues/2748)) +1. Moved languageServer-related files to a languageServer folder. + ([#2756](https://github.com/Microsoft/vscode-python/issues/2756)) +1. Skip known failing tests for specific OS and Python version combinations to get CI running cleanly. + ([#2795](https://github.com/Microsoft/vscode-python/issues/2795)) +1. Move the linting error code out of the linting message and let [VS Code manage it in the Problems panel](https://code.visualstudio.com/updates/v1_28#_problems-panel) + (Thanks [Nafly Mohammed](https://github.com/naflymim)). + ([#2815](https://github.com/Microsoft/vscode-python/issues/2815)) +1. Remove code related to the old debugger. + ([#2828](https://github.com/Microsoft/vscode-python/issues/2828)) +1. Upgrade Gulp to 4.0.0. + ([#2909](https://github.com/Microsoft/vscode-python/issues/2909)) +1. Remove pre-commit hooks. + ([#2963](https://github.com/Microsoft/vscode-python/issues/2963)) +1. Only perform Black-related formatting tests when the current Python-version supports it. + ([#2999](https://github.com/Microsoft/vscode-python/issues/2999)) +1. Move language server downloads to the CDN. + ([#3000](https://github.com/Microsoft/vscode-python/issues/3000)) +1. Pin extension to a minimum version of the language server. + ([#3125](https://github.com/Microsoft/vscode-python/issues/3125)) + +## 2018.9.2 (29 Oct 2018) + +### Fixes + +1. Update version of `vscode-extension-telemetry` to resolve issue with regards to spawning of numerous `powershell` processes. + ([#2996](https://github.com/Microsoft/vscode-python/issues/2996)) + +### Code Health + +1. Forward telemetry from the language server. + ([#2940](https://github.com/Microsoft/vscode-python/issues/2940)) + +## 2018.9.1 (18 Oct 2018) + +### Fixes + +1. Disable activation of conda environments in PowerShell. + ([#2732](https://github.com/Microsoft/vscode-python/issues/2732)) +1. Add logging along with some some improvements to the load times of the extension. + ([#2827](https://github.com/Microsoft/vscode-python/issues/2827)) +1. Stop `normalizationForInterpreter.py` script from returning CRCRLF line-endings. + ([#2857](https://github.com/Microsoft/vscode-python/issues/2857)) + +### Code Health + +1. Add ability to publish extension builds from `release` branches into the blob store. + ([#2874](https://github.com/Microsoft/vscode-python/issues/2874)) + +## 2018.9.0 (9 Oct 2018) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server 2018.9.0](https://github.com/Microsoft/python-language-server/releases/tag/2018.9.0) +- [ptvsd 4.1.3](https://github.com/Microsoft/ptvsd/releases/tag/v4.1.3) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Adds support for code completion in the debug console window. + ([#1076](https://github.com/Microsoft/vscode-python/issues/1076)) +1. Auto activate Python Environment in terminals (disable with `"python.terminal.activateEnvironment": false`). + ([#1387](https://github.com/Microsoft/vscode-python/issues/1387)) +1. Add support for activation of `pyenv` environments in the Terminal. + ([#1526](https://github.com/Microsoft/vscode-python/issues/1526)) +1. Display a message with options when user selects the default macOS Python interpreter. + ([#1689](https://github.com/Microsoft/vscode-python/issues/1689)) +1. Add debug configuration snippet for modules for the debugger. + ([#2175](https://github.com/Microsoft/vscode-python/issues/2175)) +1. Search for python interpreters in all paths found in the `PATH`/`Path` environment variable. + ([#2398](https://github.com/Microsoft/vscode-python/issues/2398)) +1. Add telemetry to download, extract, and analyze, phases of the Python Language Server. + ([#2461](https://github.com/Microsoft/vscode-python/issues/2461)) +1. The `pvsc-dev-ext.py` script now captures `stderr` for more informative exceptions + when execution fails. + ([#2483](https://github.com/Microsoft/vscode-python/issues/2483)) +1. Display notification when attempting to debug without selecting a python interpreter. + ([#2494](https://github.com/Microsoft/vscode-python/issues/2494)) +1. Add support for out of band updates to the language server. + ([#2580](https://github.com/Microsoft/vscode-python/issues/2580)) +1. Ensure status bar with interpreter information takes priority over other items. + ([#2617](https://github.com/Microsoft/vscode-python/issues/2617)) +1. Add Python Language Server version to the survey banner URL presented to some users. + ([#2630](https://github.com/Microsoft/vscode-python/issues/2630)) +1. Language server now provides rename functionality. + ([#2650](https://github.com/Microsoft/vscode-python/issues/2650)) +1. Search for default known paths for conda environments on windows. + ([#2794](https://github.com/Microsoft/vscode-python/issues/2794) +1. Add [bandit](https://pypi.org/project/bandit/) to supported linters. + (thanks [Steven Demurjian](https://github.com/demus)) + ([#2775](https://github.com/Microsoft/vscode-python/issues/2775)) + +### Fixes + +1. Improvements to the display format of interpreter information in the list of interpreters. + ([#1352](https://github.com/Microsoft/vscode-python/issues/1352)) +1. Deprecate the use of the setting `python.autoComplete.preloadModules`. Recommendation is to utilize the new language server (change the setting `"python.jediEnabled": false`). + ([#1704](https://github.com/Microsoft/vscode-python/issues/1704)) +1. Add a new `python.condaPath` setting to use if conda is not found on `PATH`. + ([#1944](https://github.com/Microsoft/vscode-python/issues/1944)) +1. Ensure code is executed when the last line of selected code is indented. + ([#2167](https://github.com/Microsoft/vscode-python/issues/2167)) +1. Stop duplicate initializations of the Python Language Server's progress reporter. + ([#2297](https://github.com/Microsoft/vscode-python/issues/2297)) +1. Fix the regex expression to match MyPy linter messages that expects the file name to have a `.py` extension, that isn't always the case, to catch any filename. + E.g., .pyi files that describes interfaces wouldn't get the linter messages to Problems tab. + ([#2380](https://github.com/Microsoft/vscode-python/issues/2380)) +1. Do not use variable substitution when updating `python.pythonPath`. This matters + because VS Code does not do variable substitution in settings values. + ([#2459](https://github.com/Microsoft/vscode-python/issues/2459)) +1. Use a python script to launch the debugger, instead of using `-m` which requires changes to the `PYTHONPATH` variable. + ([#2509](https://github.com/Microsoft/vscode-python/issues/2509)) +1. Provide paths from `PYTHONPATH` environment variable to the language server, as additional search locations of Python modules. + ([#2518](https://github.com/Microsoft/vscode-python/issues/2518)) +1. Fix issue preventing debugger user survey banner from opening. + ([#2557](https://github.com/Microsoft/vscode-python/issues/2557)) +1. Use folder name of the Python interpreter as the name of the virtual environment. + ([#2562](https://github.com/Microsoft/vscode-python/issues/2562)) +1. Give preference to bitness information retrieved from the Python interpreter over what's been retrieved from Windows Registry. + ([#2563](https://github.com/Microsoft/vscode-python/issues/2563)) +1. Use the environment folder name for environments without environment names in the Conda Environments list file. + ([#2577](https://github.com/Microsoft/vscode-python/issues/2577)) +1. Update environment variable naming convention for `SPARK_HOME`, when stored in `settings.json`. + ([#2628](https://github.com/Microsoft/vscode-python/issues/2628)) +1. Fix debug adapter `Attach` test. + ([#2655](https://github.com/Microsoft/vscode-python/issues/2655)) +1. Fix colon-triggered block formatting. + ([#2714](https://github.com/Microsoft/vscode-python/issues/2714)) +1. Use full path to activate command in conda environments on windows when python.condaPath is set. + ([#2753](https://github.com/Microsoft/vscode-python/issues/2753)) + +### Code Health + +1. Fix broken CI on Azure DevOps. + ([#2549](https://github.com/Microsoft/vscode-python/issues/2549)) +1. Upgraded our version of `request` to `2.87.0`. + ([#2621](https://github.com/Microsoft/vscode-python/issues/2621)) +1. Include the version of language server in telemetry. + ([#2702](https://github.com/Microsoft/vscode-python/issues/2702)) +1. Update `vscode-extension-telemetry` to `0.0.22`. + ([#2745](https://github.com/Microsoft/vscode-python/issues/2745)) + +## 2018.8.0 (04 September 2018) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [4.1.1](https://pypi.org/project/ptvsd/4.1.1/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Improved language server startup time by 40%. + ([#1865](https://github.com/Microsoft/vscode-python/issues/1865)) +1. Add pip dependency support to the conda `environment.yml` YAML schema support + (thanks [Mark Edwards](https://github.com/markedwards)). + ([#2119](https://github.com/Microsoft/vscode-python/issues/2119)) +1. Added a German translation. (thanks to [bschley](https://github.com/bschley) and by means of [berndverst](https://github.com/berndverst) and [croth1](https://github.com/croth1) for the reviews) + ([#2203](https://github.com/Microsoft/vscode-python/issues/2203)) +1. The new setting `python.analysis.diagnosticPublishDelay` allows you to control + when language server publishes diagnostics. Default is 1 second after the user + activity, such a typing, ceases. If diagnostic is clear (i.e. errors got fixed), + the publishing is immediate. + ([#2270](https://github.com/Microsoft/vscode-python/issues/2270)) +1. Language server now supports hierarchical document outline per language server protocol 4.4+ and VS Code 1.26+. + ([#2384](https://github.com/Microsoft/vscode-python/issues/2384)) +1. Make use of the `http.proxy` field in `settings.json` when downloading the Python Language Server. + ([#2385](https://github.com/Microsoft/vscode-python/issues/2385)) + +### Fixes + +1. Fix debugger issue that prevented users from copying the value of a variable from the Variables debugger window. + ([#1398](https://github.com/Microsoft/vscode-python/issues/1398)) +1. Enable code lenses for tests when using the new language server. + ([#1948](https://github.com/Microsoft/vscode-python/issues/1948)) +1. Fix null reference exception in the language server causing server initialization to fail. The exception happened when search paths contained a folder that did not exist. + ([#2017](https://github.com/Microsoft/vscode-python/issues/2017)) +1. Language server now populates document outline with all symbols instead of just top-level ones. + ([#2050](https://github.com/Microsoft/vscode-python/issues/2050)) +1. Ensure test count values in the status bar represent the correct number of tests that were discovered and run. + ([#2143](https://github.com/Microsoft/vscode-python/issues/2143)) +1. Fixed issue in the language server when documentation for a function always produced "Documentation is still being calculated, please try again soon". + ([#2179](https://github.com/Microsoft/vscode-python/issues/2179)) +1. Change linter message parsing so it respects `python.linting.maxNumberOfProblems`. + (thanks [Scott Saponas](https://github.com/saponas/)) + ([#2198](https://github.com/Microsoft/vscode-python/issues/2198)) +1. Fixed language server issue when it could enter infinite loop reloading modules. + ([#2207](https://github.com/Microsoft/vscode-python/issues/2207)) +1. Ensure workspace `pipenv` environment is not labeled as a `virtual env`. + ([#2223](https://github.com/Microsoft/vscode-python/issues/2223)) +1. Improve reliability of document outline population with language server. + ([#2224](https://github.com/Microsoft/vscode-python/issues/2224)) +1. Language server now correctly handles `with` statement when `__enter__` is + declared in a base class. + ([#2240](https://github.com/Microsoft/vscode-python/issues/2240)) +1. Fix `visualstudio_py_testLauncher` to stop breaking out of test discovery too soon. + ([#2241](https://github.com/Microsoft/vscode-python/issues/2241)) +1. Notify the user when the language server does not support their platform. + ([#2245](https://github.com/Microsoft/vscode-python/issues/2245)) +1. Fix issue with survey not opening in a browser for Windows users. + ([#2252](https://github.com/Microsoft/vscode-python/issues/2252)) +1. Correct banner survey question text to reference the Python Language Server. + ([#2253](https://github.com/Microsoft/vscode-python/issues/2253)) +1. Fixed issue in the language server when typing dot under certain conditions produced null reference exception. + ([#2262](https://github.com/Microsoft/vscode-python/issues/2262)) +1. Fix error when switching from new language server to the old `Jedi` language server. + ([#2281](https://github.com/Microsoft/vscode-python/issues/2281)) +1. Unpin Pylint from < 2.0 (prospector was upgraded and isn't stuck on that any longer) + ([#2284](https://github.com/Microsoft/vscode-python/issues/2284)) +1. Add support for breaking into the first line of code in the new debugger. + ([#2299](https://github.com/Microsoft/vscode-python/issues/2299)) +1. Show the debugger survey banner for only a subset of users. + ([#2300](https://github.com/Microsoft/vscode-python/issues/2300)) +1. Ensure Flask debug configuration launches flask in a debug environment with the Flask debug mode disabled. + This is necessary to ensure the custom debugger takes precedence over the interactive debugger, and live reloading is disabled. + http://flask.pocoo.org/docs/1.0/api/#flask.Flask.debug + ([#2309](https://github.com/Microsoft/vscode-python/issues/2309)) +1. Language server now correctly merges data from typeshed and the Python library. + ([#2345](https://github.com/Microsoft/vscode-python/issues/2345)) +1. Fix pytest >= 3.7 test discovery. + ([#2347](https://github.com/Microsoft/vscode-python/issues/2347)) +1. Update the downloaded Python language server nuget package filename to + `Python-Language-Server-{OSType}.beta.nupkg`. + ([#2362](https://github.com/Microsoft/vscode-python/issues/2362)) +1. Added setting to control language server log output. Default is now 'error' so there should be much less noise in the output. + ([#2405](https://github.com/Microsoft/vscode-python/issues/2405)) +1. Fix `experimental` debugger when debugging Python files with Unicode characters in the file path. + ([#688](https://github.com/Microsoft/vscode-python/issues/688)) +1. Ensure stepping out of debugged code does not take user into `PTVSD` debugger code. + ([#767](https://github.com/Microsoft/vscode-python/issues/767)) +1. Upgrade `pythonExperimental` to `python` in `launch.json`. + ([#2478](https://github.com/Microsoft/vscode-python/issues/2478)) + +### Code Health + +1. Revert change that moved IExperimentalDebuggerBanner into a common location. + ([#2195](https://github.com/Microsoft/vscode-python/issues/2195)) +1. Decorate `EventEmitter` within a `try..catch` to play nice with other extensions performing the same operation. + ([#2196](https://github.com/Microsoft/vscode-python/issues/2196)) +1. Change the default interpreter to favor Python 3 over Python 2. + ([#2266](https://github.com/Microsoft/vscode-python/issues/2266)) +1. Deprecate command `Python: Build Workspace Symbols` when using the language server. + ([#2267](https://github.com/Microsoft/vscode-python/issues/2267)) +1. Pin version of `pylint` to `3.6.3` to allow ensure `pylint` gets installed on Travis with Python2.7. + ([#2305](https://github.com/Microsoft/vscode-python/issues/2305)) +1. Remove some of the debugger tests and fix some minor debugger issues. + ([#2307](https://github.com/Microsoft/vscode-python/issues/2307)) +1. Only use the current stable version of PTVSD in CI builds/releases. + ([#2432](https://github.com/Microsoft/vscode-python/issues/2432)) + +## 2018.7.1 (23 July 2018) + +### Fixes + +1. Update the language server to code as of + [651468731500ec1cc644029c3666c57b82f77d76](https://github.com/Microsoft/PTVS/commit/651468731500ec1cc644029c3666c57b82f77d76). + ([#2233](https://github.com/Microsoft/vscode-python/issues/2233)) + +## 2018.7.0 (18 July 2018) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.11a5](https://pypi.org/project/ptvsd/4.1.11a5/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Language server now reports code analysis progress in the status bar. + ([#1591](https://github.com/Microsoft/vscode-python/issues/1591)) +1. Only report Language Server download progress once. + ([#2000](https://github.com/Microsoft/vscode-python/issues/2000)) +1. Messages changes to reflect name of the language server: 'Microsoft Python Language Server'; + folder name changed from `analysis` to `languageServer`. + ([#2107](https://github.com/Microsoft/vscode-python/issues/2107)) +1. Set default analysis for language server to open files only. + ([#2113](https://github.com/Microsoft/vscode-python/issues/2113)) +1. Add two popups to the extension: one to ask users to move to the new language server, the other to request feedback from users of that language server. + ([#2127](https://github.com/Microsoft/vscode-python/issues/2127)) + +### Fixes + +1. Ensure dunder variables are always displayed in code completion when using the new language server. + ([#2013](https://github.com/Microsoft/vscode-python/issues/2013)) +1. Store testId for files & suites during unittest discovery. + ([#2044](https://github.com/Microsoft/vscode-python/issues/2044)) +1. `editor.formatOnType` no longer adds space after `*` in multi-line arguments. + ([#2048](https://github.com/Microsoft/vscode-python/issues/2048)) +1. Fix bug where tooltips would popup whenever a comma is typed within a string. + ([#2057](https://github.com/Microsoft/vscode-python/issues/2057)) +1. Change keyboard shortcut for `Run Selection/Line in Python Terminal` to not + interfere with the Find/Replace dialog box. + ([#2068](https://github.com/Microsoft/vscode-python/issues/2068)) +1. Relax validation of the environment `Path` variable. + ([#2076](https://github.com/Microsoft/vscode-python/issues/2076)) +1. `editor.formatOnType` is more reliable handling floating point numbers. + ([#2079](https://github.com/Microsoft/vscode-python/issues/2079)) +1. Change the default port used in remote debugging using `Experimental` debugger to `5678`. + ([#2146](https://github.com/Microsoft/vscode-python/issues/2146)) +1. Register test manager when using the new language server. + ([#2186](https://github.com/Microsoft/vscode-python/issues/2186)) + +### Code Health + +1. Removed pre-commit hook that ran unit tests. + ([#1986](https://github.com/Microsoft/vscode-python/issues/1986)) +1. Pass OS type to the debugger. + ([#2128](https://github.com/Microsoft/vscode-python/issues/2128)) +1. Ensure 'languageServer' directory is excluded from the build output. + ([#2150](https://github.com/Microsoft/vscode-python/issues/2150)) +1. Change the download links of the language server files. + ([#2180](https://github.com/Microsoft/vscode-python/issues/2180)) + +## 2018.6.0 (20 June 2018) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.11a5](https://pypi.org/project/ptvsd/4.1.11a5/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Add setting to control automatic test discovery on save, `python.unitTest.autoTestDiscoverOnSaveEnabled`. + (thanks [Lingyu Li](http://github.com/lingyv-li/)) + ([#1037](https://github.com/Microsoft/vscode-python/issues/1037)) +1. Add `gevent` launch configuration option to enable debugging of gevent monkey patched code. + (thanks [Bence Nagy](https://github.com/underyx)) + ([#127](https://github.com/Microsoft/vscode-python/issues/127)) +1. Add support for the `"source.organizeImports"` setting for `"editor.codeActionsOnSave"` (thanks [Nathan Gaberel](https://github.com/n6g7)); you can turn this on just for Python using: + ```json + "[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + } + ``` + ([#156](https://github.com/Microsoft/vscode-python/issues/156)) +1. Added Spanish translation. + (thanks [Mario Rubio](https://github.com/mario-mra/)) + ([#1902](https://github.com/Microsoft/vscode-python/issues/1902)) +1. Add a French translation (thanks to [Jérémy](https://github.com/PixiBixi) for + the initial patch, and thanks to [Nathan Gaberel](https://github.com/n6g7), + [Bruno Alla](https://github.com/browniebroke), and + [Tarek Ziade](https://github.com/tarekziade) for reviews). + ([#1959](https://github.com/Microsoft/vscode-python/issues/1959)) +1. Add syntax highlighting for [Pipenv](http://pipenv.readthedocs.io/en/latest/)-related + files (thanks [Nathan Gaberel](https://github.com/n6g7)). + ([#995](https://github.com/Microsoft/vscode-python/issues/995)) + +### Fixes + +1. Modified to change error message displayed when path to a tool (`linter`, `formatter`, etc) is invalid. + ([#1064](https://github.com/Microsoft/vscode-python/issues/1064)) +1. Improvements to the logic used to parse the arguments passed into the test frameworks. + ([#1070](https://github.com/Microsoft/vscode-python/issues/1070)) +1. Ensure navigation to definitions follows imports and is transparent to decoration. + (thanks [Peter Law](https://github.com/PeterJCLaw)) + ([#1638](https://github.com/Microsoft/vscode-python/issues/1638)) +1. Fix for intellisense failing when using the new `Outline` feature. + ([#1721](https://github.com/Microsoft/vscode-python/issues/1721)) +1. When debugging unit tests, use the `env` file configured in `settings.json` under `python.envFile`. + ([#1759](https://github.com/Microsoft/vscode-python/issues/1759)) +1. Fix to display all interpreters in the interpreter list when a workspace contains a `Pipfile`. + ([#1800](https://github.com/Microsoft/vscode-python/issues/1800)) +1. Use file system API to perform file path comparisons when performing code navigation. + (thanks to [bstaint](https://github.com/bstaint) for the problem diagnosis) + ([#1811](https://github.com/Microsoft/vscode-python/issues/1811)) +1. Automatically add path mappings for remote debugging when attaching to the localhost. + ([#1829](https://github.com/Microsoft/vscode-python/issues/1829)) +1. Change keyboard shortcut for `Run Selection/Line in Python Terminal` to `Shift+Enter`. + ([#1875](https://github.com/Microsoft/vscode-python/issues/1875)) +1. Fix unhandled rejected promises in unit tests. + ([#1919](https://github.com/Microsoft/vscode-python/issues/1919)) +1. Fix debugger issue that causes the debugger to hang and silently exit stepping over a line of code instantiating an ITK vector object. + ([#459](https://github.com/Microsoft/vscode-python/issues/459)) + +### Code Health + +1. Add telemetry to capture type of python interpreter used in workspace. + ([#1237](https://github.com/Microsoft/vscode-python/issues/1237)) +1. Enabled multi-thrreaded debugger tests for the `experimental` debugger. + ([#1250](https://github.com/Microsoft/vscode-python/issues/1250)) +1. Log relevant environment information when the existence of `pipenv` cannot be determined. + ([#1338](https://github.com/Microsoft/vscode-python/issues/1338)) +1. Use [dotenv](https://www.npmjs.com/package/dotenv) package to parse [environment variables definition files](https://code.visualstudio.com/docs/python/environments#_environment-variable-definitions-file). + ([#1376](https://github.com/Microsoft/vscode-python/issues/1376)) +1. Move from yarn to npm. + ([#1402](https://github.com/Microsoft/vscode-python/issues/1402)) +1. Fix django and flask debugger tests when using the experimental debugger. + ([#1407](https://github.com/Microsoft/vscode-python/issues/1407)) +1. Capture telemetry for the usage of the `Create Terminal` command along with other instances when a terminal is created implicitly. + ([#1542](https://github.com/Microsoft/vscode-python/issues/1542)) +1. Add telemetry to capture availability of Python 3, version of Python used in workspace and the number of workspace folders. + ([#1545](https://github.com/Microsoft/vscode-python/issues/1545)) +1. Ensure all CI tests (except for debugger) are no longer allowed to fail. + ([#1614](https://github.com/Microsoft/vscode-python/issues/1614)) +1. Capture telemetry for the usage of the feature that formats a line as you type (`editor.formatOnType`). + ([#1766](https://github.com/Microsoft/vscode-python/issues/1766)) +1. Capture telemetry for the new debugger. + ([#1767](https://github.com/Microsoft/vscode-python/issues/1767)) +1. Capture telemetry for usage of the setting `python.autocomplete.addBrackets` + ([#1770](https://github.com/Microsoft/vscode-python/issues/1770)) +1. Speed up githook by skipping commits not containing any `.ts` files. + ([#1803](https://github.com/Microsoft/vscode-python/issues/1803)) +1. Update typescript package to 2.9.1. + ([#1815](https://github.com/Microsoft/vscode-python/issues/1815)) +1. Log Conda not existing message as an information instead of an error. + ([#1817](https://github.com/Microsoft/vscode-python/issues/1817)) +1. Make use of `ILogger` to log messages instead of using `console.error`. + ([#1821](https://github.com/Microsoft/vscode-python/issues/1821)) +1. Update `parso` package to 0.2.1. + ([#1833](https://github.com/Microsoft/vscode-python/issues/1833)) +1. Update `isort` package to 4.3.4. + ([#1842](https://github.com/Microsoft/vscode-python/issues/1842)) +1. Add better exception handling when parsing responses received from the Jedi language service. + ([#1867](https://github.com/Microsoft/vscode-python/issues/1867)) +1. Resolve warnings in CI Tests and fix some broken CI tests. + ([#1885](https://github.com/Microsoft/vscode-python/issues/1885)) +1. Reduce sample count used to capture performance metrics in order to reduce time taken to complete the tests. + ([#1887](https://github.com/Microsoft/vscode-python/issues/1887)) +1. Ensure workspace information is passed into installer when determining whether a product/tool is installed. + ([#1893](https://github.com/Microsoft/vscode-python/issues/1893)) +1. Add JUnit file output to enable CI integration with VSTS. + ([#1897](https://github.com/Microsoft/vscode-python/issues/1897)) +1. Log unhandled rejected promises when running unit tests. + ([#1918](https://github.com/Microsoft/vscode-python/issues/1918)) +1. Add ability to run tests without having to launch VS Code. + ([#1922](https://github.com/Microsoft/vscode-python/issues/1922)) +1. Fix rename refactoring unit tests. + ([#1953](https://github.com/Microsoft/vscode-python/issues/1953)) +1. Fix failing test on Mac when validating the path of a python interperter. + ([#1957](https://github.com/Microsoft/vscode-python/issues/1957)) +1. Display banner prompting user to complete a survey for the use of the `Experimental Debugger`. + ([#1968](https://github.com/Microsoft/vscode-python/issues/1968)) +1. Use a glob pattern to look for `conda` executables. + ([#256](https://github.com/Microsoft/vscode-python/issues/256)) +1. Create tests to measure activation times for the extension. + ([#932](https://github.com/Microsoft/vscode-python/issues/932)) + +## 2018.5.0 (05 Jun 2018) + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.2.15](https://pypi.org/project/isort/4.2.15/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.0](https://pypi.org/project/parso/0.2.0/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.1a5](https://pypi.org/project/ptvsd/4.1.1a5/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +And thanks to the many other projects which users can optionally choose from +and install to work with the extension. Without them the extension would not be +nearly as feature-rich and useful as it is. + +### Enhancements + +1. Add support for the [Black formatter](https://pypi.org/project/black/) + (thanks to [Josh Smeaton](https://github.com/jarshwah) for the initial patch) + ([#1153](https://github.com/Microsoft/vscode-python/issues/1153)) +1. Add the command `Discover Unit Tests`. + ([#1474](https://github.com/Microsoft/vscode-python/issues/1474)) +1. Auto detect `*.jinja2` and `*.j2` extensions as Jinja templates, to enable debugging of Jinja templates. + ([#1484](https://github.com/Microsoft/vscode-python/issues/1484)) + +### Fixes + +1. Ensure debugger breaks on `assert` failures. + ([#1194](https://github.com/Microsoft/vscode-python/issues/1194)) +1. Ensure debugged program is terminated when `Stop` debugging button is clicked. + ([#1345](https://github.com/Microsoft/vscode-python/issues/1345)) +1. Fix indentation when function contains type hints. + ([#1461](https://github.com/Microsoft/vscode-python/issues/1461)) +1. Ensure python environment activation works as expected within a multi-root workspace. + ([#1476](https://github.com/Microsoft/vscode-python/issues/1476)) +1. Close communication channel before exiting the test runner. + ([#1529](https://github.com/Microsoft/vscode-python/issues/1529)) +1. Allow for negative column numbers in messages returned by `pylint`. + ([#1628](https://github.com/Microsoft/vscode-python/issues/1628)) +1. Modify the `FLASK_APP` environment variable in the flask debug configuration to include just the name of the application file. + ([#1634](https://github.com/Microsoft/vscode-python/issues/1634)) +1. Ensure the display name of an interpreter does not get prefixed twice with the words `Python`. + ([#1651](https://github.com/Microsoft/vscode-python/issues/1651)) +1. Enable code refactoring when using the new Analysis Engine. + ([#1774](https://github.com/Microsoft/vscode-python/issues/1774)) +1. `editor.formatOnType` no longer breaks numbers formatted with underscores. + ([#1779](https://github.com/Microsoft/vscode-python/issues/1779)) +1. `editor.formatOnType` now better handles multiline function arguments + ([#1796](https://github.com/Microsoft/vscode-python/issues/1796)) +1. `Go to Definition` now works for functions which have numbers that use `_` as a separator (as part of our Jedi 0.12.0 upgrade). + ([#180](https://github.com/Microsoft/vscode-python/issues/180)) +1. Display documentation for auto completion items when the feature to automatically insert of brackets for selected item is turned on. + ([#452](https://github.com/Microsoft/vscode-python/issues/452)) +1. Ensure empty paths do not get added into `sys.path` by the Jedi language server. (this was fixed in the previous release in [#1471](https://github.com/Microsoft/vscode-python/pull/1471)) + ([#677](https://github.com/Microsoft/vscode-python/issues/677)) +1. Resolves rename refactor issue that removes the last line of the source file when the line is being refactored and source does not end with an EOL. + ([#695](https://github.com/Microsoft/vscode-python/issues/695)) +1. Ensure the prompt to install missing packages is not displayed more than once. + ([#980](https://github.com/Microsoft/vscode-python/issues/980)) + +### Code Health + +1. Add syntax highlighting to `constraints.txt` files to match that of `requirements.txt` files. + (thanks [Waleed Sehgal](https://github.com/waleedsehgal)) + ([#1053](https://github.com/Microsoft/vscode-python/issues/1053)) +1. Refactor unit testing functionality to improve testability of individual components. + ([#1068](https://github.com/Microsoft/vscode-python/issues/1068)) +1. Add unit tests for evaluating expressions in the experimental debugger. + ([#1109](https://github.com/Microsoft/vscode-python/issues/1109)) +1. Add tests to ensure custom arguments get passed into python program when using the experimental debugger. + ([#1280](https://github.com/Microsoft/vscode-python/issues/1280)) +1. Ensure custom environment variables are always used when spawning any process from within the extension. + ([#1339](https://github.com/Microsoft/vscode-python/issues/1339)) +1. Add tests for hit count breakpoints for the experimental debugger. + ([#1410](https://github.com/Microsoft/vscode-python/issues/1410)) +1. Ensure none of the npm packages (used by the extension) rely on native dependencies. + ([#1416](https://github.com/Microsoft/vscode-python/issues/1416)) +1. Remove explicit initialization of `PYTHONPATH` with the current workspace path in unit testing of modules with the experimental debugger. + ([#1465](https://github.com/Microsoft/vscode-python/issues/1465)) +1. Flag `program` in `launch.json` configuration items as an optional attribute. + ([#1503](https://github.com/Microsoft/vscode-python/issues/1503)) +1. Remove unused setting `disablePromptForFeatures`. + ([#1551](https://github.com/Microsoft/vscode-python/issues/1551)) +1. Remove unused Unit Test setting `debugHost`. + ([#1552](https://github.com/Microsoft/vscode-python/issues/1552)) +1. Create a new API to retrieve interpreter details with the ability to cache the details. + ([#1569](https://github.com/Microsoft/vscode-python/issues/1569)) +1. Add tests for log points in the experimental debugger. + ([#1582](https://github.com/Microsoft/vscode-python/issues/1582)) +1. Update typescript package to 2.8.3. + ([#1604](https://github.com/Microsoft/vscode-python/issues/1604)) +1. Fix typescript compilation error. + ([#1623](https://github.com/Microsoft/vscode-python/issues/1623)) +1. Fix unit tests used to test flask template debugging on AppVeyor for the experimental debugger. + ([#1640](https://github.com/Microsoft/vscode-python/issues/1640)) +1. Change yarn install script to include the keyword `--lock-file`. + (thanks [Lingyu Li](https://github.com/lingyv-li/)) + ([#1682](https://github.com/Microsoft/vscode-python/issues/1682)) +1. Run unit tests as a pre-commit hook. + ([#1703](https://github.com/Microsoft/vscode-python/issues/1703)) +1. Update debug capabilities to add support for the setting `supportTerminateDebuggee` due to an upstream update from [PTVSD](https://github.com/Microsoft/ptvsd/issues). + ([#1719](https://github.com/Microsoft/vscode-python/issues/1719)) +1. Build and upload development build of the extension to the Azure blob store even if CI tests fail on the `main` branch. + ([#1730](https://github.com/Microsoft/vscode-python/issues/1730)) +1. Changes to the script used to upload the extension to the Azure blob store. + ([#1732](https://github.com/Microsoft/vscode-python/issues/1732)) +1. Prompt user to reload Visual Studio Code when toggling between the analysis engines. + ([#1747](https://github.com/Microsoft/vscode-python/issues/1747)) +1. Fix typo in unit test. + ([#1794](https://github.com/Microsoft/vscode-python/issues/1794)) +1. Fix failing Prospector unit tests and add more tests for linters (with and without workspaces). + ([#1836](https://github.com/Microsoft/vscode-python/issues/1836)) +1. Ensure `Outline` view doesn't overload the language server with too many requests, while user is editing text in the editor. + ([#1856](https://github.com/Microsoft/vscode-python/issues/1856)) + +## 2018.4.0 (2 May 2018) + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.2.15](https://pypi.org/project/isort/4.2.15/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.0](https://pypi.org/project/parso/0.2.0/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.1a1](https://pypi.org/project/ptvsd/4.1.1a1/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +And a special thanks to [Patryk Zawadzki](https://github.com/patrys) for all of +his help on [our issue tracker](https://github.com/Microsoft/vscode-python)! + +### Enhancements + +1. Enable debugging of Jinja templates in the experimental debugger. + This is made possible with the addition of the `jinja` setting in the `launch.json` file as follows: + ```json + "request": "launch or attach", + ... + "jinja": true + ``` + ([#1206](https://github.com/Microsoft/vscode-python/issues/1206)) +1. Remove empty spaces from the selected text of the active editor when executing in a terminal. + ([#1207](https://github.com/Microsoft/vscode-python/issues/1207)) +1. Add prelimnary support for remote debugging using the experimental debugger. + Attach to a Python program started using the command `python -m ptvsd --server --port 9091 --file pythonFile.py` + ([#1229](https://github.com/Microsoft/vscode-python/issues/1229)) +1. Add support for [logpoints](https://code.visualstudio.com/docs/editor/debugging#_logpoints) in the experimental debugger. + ([#1306](https://github.com/Microsoft/vscode-python/issues/1306)) +1. Set focus to the terminal upon creation of a terminal using the `Python: Create Terminal` command. + ([#1315](https://github.com/Microsoft/vscode-python/issues/1315)) +1. Save the python file before running it in the terminal using the command/menu `Run Python File in Terminal`. + ([#1316](https://github.com/Microsoft/vscode-python/issues/1316)) +1. Added support for source references (remote debugging without having the source code locally) in the experimental debugger. + ([#1333](https://github.com/Microsoft/vscode-python/issues/1333)) +1. Add `Ctrl+Enter` keyboard shortcut for `Run Selection/Line in Python Terminal`. + ([#1349](https://github.com/Microsoft/vscode-python/issues/1349)) +1. Settings configured within the `debugOptions` property of `launch.json` for the old debugger are now defined as individual (boolean) properties in the new experimental debugger (e.g. `"debugOptions": ["RedirectOutput"]` becomes `"redirectOutput": true`). + ([#1395](https://github.com/Microsoft/vscode-python/issues/1395)) +1. Intergrate Jedi 0.12. See https://github.com/davidhalter/jedi/issues/1063#issuecomment-381417297 for details. + ([#1400](https://github.com/Microsoft/vscode-python/issues/1400)) +1. Enable Jinja template debugging as a default behaviour when using the Watson debug configuration for debugging of Watson applications. + ([#1480](https://github.com/Microsoft/vscode-python/issues/1480)) +1. Enable Jinja template debugging as a default behavior when debugging Pyramid applications. + ([#1492](https://github.com/Microsoft/vscode-python/issues/1492)) +1. Add prelimnary support for remote debugging using the experimental debugger. + Attach to a Python program after having imported `ptvsd` and enabling the debugger to attach as follows: + ```python + import ptvsd + ptvsd.enable_attach(('0.0.0.0', 5678)) + ``` + Additional capabilities: + - `ptvsd.break_into_debugger()` to break into the attached debugger. + - `ptvsd.wait_for_attach(timeout)` to cause the program to wait until a debugger attaches. + - `ptvsd.is_attached()` to determine whether a debugger is attached to the program. + ([#907](https://github.com/Microsoft/vscode-python/issues/907)) + +### Fixes + +1. Use an existing method to identify the active interpreter. + ([#1015](https://github.com/Microsoft/vscode-python/issues/1015)) +1. Fix `go to definition` functionality across files. + ([#1033](https://github.com/Microsoft/vscode-python/issues/1033)) +1. IntelliSense under Python 2 for inherited attributes works again (thanks to an upgraded Jedi). + ([#1072](https://github.com/Microsoft/vscode-python/issues/1072)) +1. Reverted change that ended up considering symlinked interpreters as duplicate interpreter. + ([#1192](https://github.com/Microsoft/vscode-python/issues/1192)) +1. Display errors returned by the PipEnv command when identifying the corresponding environment. + ([#1254](https://github.com/Microsoft/vscode-python/issues/1254)) +1. When `editor.formatOnType` is on, don't add a space for `*args` or `**kwargs` + ([#1257](https://github.com/Microsoft/vscode-python/issues/1257)) +1. When `editor.formatOnType` is on, don't add a space between a string type specifier and the string literal + ([#1257](https://github.com/Microsoft/vscode-python/issues/1257)) +1. Reduce the frequency within which the memory usage of the language server is checked, also ensure memory usage is not checked unless language server functionality is used. + ([#1277](https://github.com/Microsoft/vscode-python/issues/1277)) +1. Ensure interpreter file exists on the file system before including into list of interpreters. + ([#1305](https://github.com/Microsoft/vscode-python/issues/1305)) +1. Do not have the formatter consider single-quoted string multiline even if it is not terminated. + ([#1364](https://github.com/Microsoft/vscode-python/issues/1364)) +1. IntelliSense works in module-level `if` statements (thanks to Jedi 0.12.0 upgrade). + ([#142](https://github.com/Microsoft/vscode-python/issues/142)) +1. Clicking the codelens `Run Test` on a test class should run that specific test class instead of all tests in the file. + ([#1472](https://github.com/Microsoft/vscode-python/issues/1472)) +1. Clicking the codelens `Run Test` on a test class or method should run that specific test instead of all tests in the file. + ([#1473](https://github.com/Microsoft/vscode-python/issues/1473)) +1. Check whether the selected python interpreter is valid before starting the language server. Failing to do so could result in the extension failing to load. + ([#1487](https://github.com/Microsoft/vscode-python/issues/1487)) +1. Fixes the issue where Conda environments created using the latest version of Anaconda are not activated in Powershell. + ([#1520](https://github.com/Microsoft/vscode-python/issues/1520)) +1. Increase the delay for the activation of environments in Powershell terminals. + ([#1533](https://github.com/Microsoft/vscode-python/issues/1533)) +1. Fix activation of environments with spaces in the python path when using Powershell. + ([#1534](https://github.com/Microsoft/vscode-python/issues/1534)) +1. Ensure Flask application is launched with multi-threading disabled, when run in the CI tests. + ([#1535](https://github.com/Microsoft/vscode-python/issues/1535)) +1. IntelliSense works appropriately when a project contains multiple files with the same name (thanks to Jedi 0.12.0 update). + ([#178](https://github.com/Microsoft/vscode-python/issues/178)) +1. Add blank lines to separate blocks of indented code (function defs, classes, and the like) so as to ensure the code can be run within a Python interactive prompt. + ([#259](https://github.com/Microsoft/vscode-python/issues/259)) +1. Provide type details appropriate for the iterable in a `for` loop when the line has a `# type` comment. + ([#338](https://github.com/Microsoft/vscode-python/issues/338)) +1. Parameter hints following an f-string work again. + ([#344](https://github.com/Microsoft/vscode-python/issues/344)) +1. When `editor.formatOnType` is on, don't indent after a single-line statement block + ([#726](https://github.com/Microsoft/vscode-python/issues/726)) +1. Fix debugging of Pyramid applications on Windows. + ([#737](https://github.com/Microsoft/vscode-python/issues/737)) + +### Code Health + +1. Improved developer experience of the Python Extension on Windows. + ([#1216](https://github.com/Microsoft/vscode-python/issues/1216)) +1. Parallelize jobs (unit tests) on CI server. + ([#1247](https://github.com/Microsoft/vscode-python/issues/1247)) +1. Run CI tests against the release version and main branch of PTVSD (experimental debugger), allowing tests to fail against the main branch of PTVSD. + ([#1253](https://github.com/Microsoft/vscode-python/issues/1253)) +1. Only trigger the extension for `file` and `untitled` in preparation for + [Visual Studio Live Share](https://aka.ms/vsls) + (thanks to [Jonathan Carter](https://github.com/lostintangent)) + ([#1298](https://github.com/Microsoft/vscode-python/issues/1298)) +1. Ensure all unit tests run on Travis use the right Python interpreter. + ([#1318](https://github.com/Microsoft/vscode-python/issues/1318)) +1. Pin all production dependencies. + ([#1374](https://github.com/Microsoft/vscode-python/issues/1374)) +1. Add support for [hit count breakpoints](https://code.visualstudio.com/docs/editor/debugging#_advanced-breakpoint-topics) in the experimental debugger. + ([#1409](https://github.com/Microsoft/vscode-python/issues/1409)) +1. Ensure custom environment variables defined in `.env` file are passed onto the `pipenv` command. + ([#1428](https://github.com/Microsoft/vscode-python/issues/1428)) +1. Remove unwanted python packages no longer used in unit tests. + ([#1494](https://github.com/Microsoft/vscode-python/issues/1494)) +1. Register language server functionality in the extension against specific resource types supporting the python language. + ([#1530](https://github.com/Microsoft/vscode-python/issues/1530)) + +## 2018.3.1 (29 Mar 2018) + +### Fixes + +1. Fixes issue that causes linter to fail when file path contains spaces. + ([#1239](https://github.com/Microsoft/vscode-python/issues/1239)) + +## 2018.3.0 (28 Mar 2018) + +### Enhancements + +1. Add a PySpark debug configuration for the experimental debugger. + ([#1029](https://github.com/Microsoft/vscode-python/issues/1029)) +1. Add a Pyramid debug configuration for the experimental debugger. + ([#1030](https://github.com/Microsoft/vscode-python/issues/1030)) +1. Add a Watson debug configuration for the experimental debugger. + ([#1031](https://github.com/Microsoft/vscode-python/issues/1031)) +1. Add a Scrapy debug configuration for the experimental debugger. + ([#1032](https://github.com/Microsoft/vscode-python/issues/1032)) +1. When using pipenv, install packages (such as linters, test frameworks) in dev-packages. + ([#1110](https://github.com/Microsoft/vscode-python/issues/1110)) +1. Added commands translation for italian locale. + (thanks [Dotpys](https://github.com/Dotpys/)) ([#1152](https://github.com/Microsoft/vscode-python/issues/1152)) +1. Add support for Django Template debugging in experimental debugger. + ([#1189](https://github.com/Microsoft/vscode-python/issues/1189)) +1. Add support for Flask Template debugging in experimental debugger. + ([#1190](https://github.com/Microsoft/vscode-python/issues/1190)) +1. Add support for Jinja template debugging. ([#1210](https://github.com/Microsoft/vscode-python/issues/1210)) +1. When debugging, use `Integrated Terminal` as the default console. + ([#526](https://github.com/Microsoft/vscode-python/issues/526)) +1. Disable the display of errors messages when rediscovering of tests fail in response to changes to files, e.g. don't show a message if there's a syntax error in the test code. + ([#704](https://github.com/Microsoft/vscode-python/issues/704)) +1. Bundle python dependencies (PTVSD package) in the extension for the experimental debugger. + ([#741](https://github.com/Microsoft/vscode-python/issues/741)) +1. Add support for experimental debugger when debugging Python Unit Tests. + ([#906](https://github.com/Microsoft/vscode-python/issues/906)) +1. Support `Debug Console` as a `console` option for the Experimental Debugger. + ([#950](https://github.com/Microsoft/vscode-python/issues/950)) +1. Enable syntax highlighting for `requirements.in` files as used by + e.g. [pip-tools](https://github.com/jazzband/pip-tools) + (thanks [Lorenzo Villani](https://github.com/lvillani)) + ([#961](https://github.com/Microsoft/vscode-python/issues/961)) +1. Add support to read name of Pipfile from environment variable. + ([#999](https://github.com/Microsoft/vscode-python/issues/999)) + +### Fixes + +1. Fixes issue that causes debugging of unit tests to hang indefinitely. ([#1009](https://github.com/Microsoft/vscode-python/issues/1009)) +1. Add ability to disable the check on memory usage of language server (Jedi) process. + To turn off this check, add `"python.jediMemoryLimit": -1` to your user or workspace settings (`settings.json`) file. + ([#1036](https://github.com/Microsoft/vscode-python/issues/1036)) +1. Ignore test results when debugging unit tests. + ([#1043](https://github.com/Microsoft/vscode-python/issues/1043)) +1. Fixes auto formatting of conditional statements containing expressions with `<=` symbols. + ([#1096](https://github.com/Microsoft/vscode-python/issues/1096)) +1. Resolve debug configuration information in `launch.json` when debugging without opening a python file. + ([#1098](https://github.com/Microsoft/vscode-python/issues/1098)) +1. Disables auto completion when editing text at the end of a comment string. + ([#1123](https://github.com/Microsoft/vscode-python/issues/1123)) +1. Ensures file paths are properly encoded when passing them as arguments to linters. + ([#199](https://github.com/Microsoft/vscode-python/issues/199)) +1. Fix occasionally having unverified breakpoints + ([#87](https://github.com/Microsoft/vscode-python/issues/87)) +1. Ensure conda installer is not used for non-conda environments. + ([#969](https://github.com/Microsoft/vscode-python/issues/969)) +1. Fixes issue that display incorrect interpreter briefly before updating it to the right value. + ([#981](https://github.com/Microsoft/vscode-python/issues/981)) + +### Code Health + +1. Exclude 'news' folder from getting packaged into the extension. + ([#1020](https://github.com/Microsoft/vscode-python/issues/1020)) +1. Remove Jupyter commands. + (thanks [Yu Zhang](https://github.com/neilsustc)) + ([#1034](https://github.com/Microsoft/vscode-python/issues/1034)) +1. Trigger incremental build compilation only when typescript files are modified. + ([#1040](https://github.com/Microsoft/vscode-python/issues/1040)) +1. Updated npm dependencies in devDependencies and fix TypeScript compilation issues. + ([#1042](https://github.com/Microsoft/vscode-python/issues/1042)) +1. Enable unit testing of stdout and stderr redirection for the experimental debugger. + ([#1048](https://github.com/Microsoft/vscode-python/issues/1048)) +1. Update npm package `vscode-extension-telemetry` to fix the warning 'os.tmpDir() deprecation'. + (thanks [osya](https://github.com/osya)) + ([#1066](https://github.com/Microsoft/vscode-python/issues/1066)) +1. Prevent the debugger stepping into JS code while developing the extension when debugging async TypeScript code. + ([#1090](https://github.com/Microsoft/vscode-python/issues/1090)) +1. Increase timeouts for the debugger unit tests. + ([#1094](https://github.com/Microsoft/vscode-python/issues/1094)) +1. Change the command used to install pip on AppVeyor to avoid installation errors. + ([#1107](https://github.com/Microsoft/vscode-python/issues/1107)) +1. Check whether a document is active when detecthing changes in the active document. + ([#1114](https://github.com/Microsoft/vscode-python/issues/1114)) +1. Remove SIGINT handler in debugger adapter, thereby preventing it from shutting down the debugger. + ([#1122](https://github.com/Microsoft/vscode-python/issues/1122)) +1. Improve compilation speed of the extension's TypeScript code. + ([#1146](https://github.com/Microsoft/vscode-python/issues/1146)) +1. Changes to how debug options are passed into the experimental version of PTVSD (debugger). + ([#1168](https://github.com/Microsoft/vscode-python/issues/1168)) +1. Ensure file paths are not sent in telemetry when running unit tests. + ([#1180](https://github.com/Microsoft/vscode-python/issues/1180)) +1. Change `DjangoDebugging` to `Django` in `debugOptions` of launch.json. + ([#1198](https://github.com/Microsoft/vscode-python/issues/1198)) +1. Changed property name used to capture the trigger source of Unit Tests. ([#1213](https://github.com/Microsoft/vscode-python/issues/1213)) +1. Enable unit testing of the experimental debugger on CI servers + ([#742](https://github.com/Microsoft/vscode-python/issues/742)) +1. Generate code coverage for debug adapter unit tests. + ([#778](https://github.com/Microsoft/vscode-python/issues/778)) +1. Execute prospector as a module (using -m). + ([#982](https://github.com/Microsoft/vscode-python/issues/982)) +1. Launch unit tests in debug mode as opposed to running and attaching the debugger to the already-running interpreter. + ([#983](https://github.com/Microsoft/vscode-python/issues/983)) + +## 2018.2.1 (09 Mar 2018) + +### Fixes + +1. Check for `Pipfile` and not `pipfile` when looking for pipenv usage + (thanks to [Will Thompson for the fix](https://github.com/wjt)) + +## 2018.2.0 (08 Mar 2018) + +[Release pushed by one week] + +### Thanks + +We appreciate everyone who contributed to this release (including +those who reported bugs or provided feedback)! + +A special thanks goes out to the following external contributors who +contributed code in this release: + +- [Andrea D'Amore](https://github.com/Microsoft/vscode-python/commits?author=anddam) +- [Tzu-ping Chung](https://github.com/Microsoft/vscode-python/commits?author=uranusjr) +- [Elliott Beach](https://github.com/Microsoft/vscode-python/commits?author=elliott-beach) +- [Manuja Jay](https://github.com/Microsoft/vscode-python/commits?author=manujadev) +- [philipwasserman](https://github.com/Microsoft/vscode-python/commits?author=philipwasserman) + +### Enhancements + +1. Experimental support for PTVSD 4.0.0-alpha (too many issues to list) +1. Speed increases in interpreter selection ([#952](https://github.com/Microsoft/vscode-python/issues/952)) +1. Support for [direnv](https://direnv.net/) + ([#36](https://github.com/Microsoft/vscode-python/issues/36)) +1. Support for pipenv virtual environments; do note that using pipenv + automatically drops all other interpreters from the list of + possible interpreters as pipenv prefers to "own" your virtual + environment + ([#404](https://github.com/Microsoft/vscode-python/issues/404)) +1. Support for pyenv installs of Python + ([#847](https://github.com/Microsoft/vscode-python/issues/847)) +1. Support `editor.formatOnType` ([#640](https://github.com/Microsoft/vscode-python/issues/640)) +1. Added a `zh-tw` translation ([#](https://github.com/Microsoft/vscode-python/pull/841)) +1. Prompting to install a linter now allows for disabling that specific + linter as well as linters globally + ([#971](https://github.com/Microsoft/vscode-python/issues/971)) + +### Fixes + +1. Work around a bug in Pylint when the default linter rules are + enabled and running Python 2.7 which triggered `--py3k` checks + to be activated, e.g. all `print` statements to be flagged as + errors + ([#722](https://github.com/Microsoft/vscode-python/issues/722)) +1. Better detection of when a virtual environment is selected, leading + to the extension accurately leaving off `--user` when installing + Pylint ([#808](https://github.com/Microsoft/vscode-python/issues/808)) +1. Better detection of a `pylintrc` is available to automatically disable our + default Pylint checks + ([#728](https://github.com/Microsoft/vscode-python/issues/728), + [#788](https://github.com/Microsoft/vscode-python/issues/788), + [#838](https://github.com/Microsoft/vscode-python/issues/838), + [#442](https://github.com/Microsoft/vscode-python/issues/442)) +1. Fix `Got to Python object` ([#403](https://github.com/Microsoft/vscode-python/issues/403)) +1. When reformatting a file, put the temporary file in the workspace + folder so e.g. yapf detect their configuration files appropriately + ([#730](https://github.com/Microsoft/vscode-python/issues/730)) +1. The banner to suggest someone installs Python now says `Download` + instead of `Close` ([#844](https://github.com/Microsoft/vscode-python/issues/844)) +1. Formatting while typing now treats `.` and `@` as operators, + preventing the incorrect insertion of whitespace + ([#840](https://github.com/Microsoft/vscode-python/issues/840)) +1. Debugging from a virtual environment named `env` now works + ([#691](https://github.com/Microsoft/vscode-python/issues/691)) +1. Disabling linting in a single folder of a mult-root workspace no + longer disables it for all folders ([#862](https://github.com/Microsoft/vscode-python/issues/862)) +1. Fix the default debugger settings for Flask apps + ([#573](https://github.com/Microsoft/vscode-python/issues/573)) +1. Format paths correctly when sending commands through WSL and git-bash; + this does not lead to official support for either terminal + ([#895](https://github.com/Microsoft/vscode-python/issues/895)) +1. Prevent run-away Jedi processes from consuming too much memory by + automatically killing the process; reload VS Code to start the + process again if desired + ([#926](https://github.com/Microsoft/vscode-python/issues/926), + [#263](https://github.com/Microsoft/vscode-python/issues/263)) +1. Support multiple linters again + ([#913](https://github.com/Microsoft/vscode-python/issues/913)) +1. Don't over-escape markup found in docstrings + ([#911](https://github.com/Microsoft/vscode-python/issues/911), + [#716](https://github.com/Microsoft/vscode-python/issues/716), + [#627](https://github.com/Microsoft/vscode-python/issues/627), + [#692](https://github.com/Microsoft/vscode-python/issues/692)) +1. Fix when the `Problems` pane lists file paths prefixed with `git:` + ([#916](https://github.com/Microsoft/vscode-python/issues/916)) +1. Fix inline documentation when an odd number of quotes exists + ([#786](https://github.com/Microsoft/vscode-python/issues/786)) +1. Don't erroneously warn macOS users about using the system install + of Python when a virtual environment is already selected + ([#804](https://github.com/Microsoft/vscode-python/issues/804)) +1. Fix activating multiple linters without requiring a reload of + VS Code + ([#971](https://github.com/Microsoft/vscode-python/issues/971)) + +### Code Health + +1. Upgrade to Jedi 0.11.1 + ([#674](https://github.com/Microsoft/vscode-python/issues/674), + [#607](https://github.com/Microsoft/vscode-python/issues/607), + [#99](https://github.com/Microsoft/vscode-python/issues/99)) +1. Removed the banner announcing the extension moving over to + Microsoft ([#830](https://github.com/Microsoft/vscode-python/issues/830)) +1. Renamed the default debugger configurations ([#412](https://github.com/Microsoft/vscode-python/issues/412)) +1. Remove some error logging about not finding conda + ([#864](https://github.com/Microsoft/vscode-python/issues/864)) + +## 2018.1.0 (01 Feb 2018) + +### Thanks + +Thanks to everyone who contributed to this release, including +the following people who contributed code: + +- [jpfarias](https://github.com/jpfarias) +- [Hongbo He](https://github.com/graycarl) +- [JohnstonCode](https://github.com/JohnstonCode) +- [Yuichi Nukiyama](https://github.com/YuichiNukiyama) +- [MichaelSuen](https://github.com/MichaelSuen-thePointer) + +### Fixed issues + +- Support cached interpreter locations for faster interpreter selection ([#666](https://github.com/Microsoft/vscode-python/issues/259)) +- Sending a block of code with multiple global-level scopes now works ([#259](https://github.com/Microsoft/vscode-python/issues/259)) +- Automatic activation of virtual or conda environment in terminal when executing Python code/file ([#383](https://github.com/Microsoft/vscode-python/issues/383)) +- Introduce a `Python: Create Terminal` to create a terminal that activates the selected virtual/conda environment ([#622](https://github.com/Microsoft/vscode-python/issues/622)) +- Add a `ko-kr` translation ([#540](https://github.com/Microsoft/vscode-python/pull/540)) +- Add a `ru` translation ([#411](https://github.com/Microsoft/vscode-python/pull/411)) +- Performance improvements to detection of virtual environments in current workspace ([#372](https://github.com/Microsoft/vscode-python/issues/372)) +- Correctly detect 64-bit python ([#414](https://github.com/Microsoft/vscode-python/issues/414)) +- Display parameter information while typing ([#70](https://github.com/Microsoft/vscode-python/issues/70)) +- Use `localhost` instead of `0.0.0.0` when starting debug servers ([#205](https://github.com/Microsoft/vscode-python/issues/205)) +- Ability to configure host name of debug server ([#227](https://github.com/Microsoft/vscode-python/issues/227)) +- Use environment variable PYTHONPATH defined in `.env` for intellisense and code navigation ([#316](https://github.com/Microsoft/vscode-python/issues/316)) +- Support path variable when debugging ([#436](https://github.com/Microsoft/vscode-python/issues/436)) +- Ensure virtual environments can be created in `.env` directory ([#435](https://github.com/Microsoft/vscode-python/issues/435), [#482](https://github.com/Microsoft/vscode-python/issues/482), [#486](https://github.com/Microsoft/vscode-python/issues/486)) +- Reload environment variables from `.env` without having to restart VS Code ([#183](https://github.com/Microsoft/vscode-python/issues/183)) +- Support debugging of Pyramid framework on Windows ([#519](https://github.com/Microsoft/vscode-python/issues/519)) +- Code snippet for `pubd` ([#545](https://github.com/Microsoft/vscode-python/issues/545)) +- Code clean up ([#353](https://github.com/Microsoft/vscode-python/issues/353), [#352](https://github.com/Microsoft/vscode-python/issues/352), [#354](https://github.com/Microsoft/vscode-python/issues/354), [#456](https://github.com/Microsoft/vscode-python/issues/456), [#491](https://github.com/Microsoft/vscode-python/issues/491), [#228](https://github.com/Microsoft/vscode-python/issues/228), [#549](https://github.com/Microsoft/vscode-python/issues/545), [#594](https://github.com/Microsoft/vscode-python/issues/594), [#617](https://github.com/Microsoft/vscode-python/issues/617), [#556](https://github.com/Microsoft/vscode-python/issues/556)) +- Move to `yarn` from `npm` ([#421](https://github.com/Microsoft/vscode-python/issues/421)) +- Add code coverage for extension itself ([#464](https://github.com/Microsoft/vscode-python/issues/464)) +- Releasing [insiders build](https://pvsc.blob.core.windows.net/extension-builds/ms-python-insiders.vsix) of the extension and uploading to cloud storage ([#429](https://github.com/Microsoft/vscode-python/issues/429)) +- Japanese translation ([#434](https://github.com/Microsoft/vscode-python/pull/434)) +- Russian translation ([#411](https://github.com/Microsoft/vscode-python/pull/411)) +- Support paths with spaces when generating tags with `Build Workspace Symbols` ([#44](https://github.com/Microsoft/vscode-python/issues/44)) +- Add ability to configure the linters ([#572](https://github.com/Microsoft/vscode-python/issues/572)) +- Add default set of rules for Pylint ([#554](https://github.com/Microsoft/vscode-python/issues/554)) +- Prompt to install formatter if not available ([#524](https://github.com/Microsoft/vscode-python/issues/524)) +- work around `editor.formatOnSave` failing when taking more then 750ms ([#124](https://github.com/Microsoft/vscode-python/issues/124), [#590](https://github.com/Microsoft/vscode-python/issues/590), [#624](https://github.com/Microsoft/vscode-python/issues/624), [#427](https://github.com/Microsoft/vscode-python/issues/427), [#492](https://github.com/Microsoft/vscode-python/issues/492)) +- Function argument completion no longer automatically includes the default argument ([#522](https://github.com/Microsoft/vscode-python/issues/522)) +- When sending a selection to the terminal, keep the focus in the editor window ([#60](https://github.com/Microsoft/vscode-python/issues/60)) +- Install packages for non-environment Pythons as `--user` installs ([#527](https://github.com/Microsoft/vscode-python/issues/527)) +- No longer suggest the system Python install on macOS when running `Select Interpreter` as it's too outdated (e.g. lacks `pip`) ([#440](https://github.com/Microsoft/vscode-python/issues/440)) +- Fix potential hang from Intellisense ([#423](https://github.com/Microsoft/vscode-python/issues/423)) + +## Version 0.9.1 (19 December 2017) + +- Fixes the compatibility issue with the [Visual Studio Code Tools for AI](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-ai) [#432](https://github.com/Microsoft/vscode-python/issues/432) +- Display runtime errors encountered when running a python program without debugging [#454](https://github.com/Microsoft/vscode-python/issues/454) + +## Version 0.9.0 (14 December 2017) + +- Translated the commands to simplified Chinese [#240](https://github.com/Microsoft/vscode-python/pull/240) (thanks [Wai Sui kei](https://github.com/WaiSiuKei)) +- Change all links to point to their Python 3 equivalents instead of Python 2[#203](https://github.com/Microsoft/vscode-python/issues/203) +- Respect `{workspaceFolder}` [#258](https://github.com/Microsoft/vscode-python/issues/258) +- Running a program using Ctrl-F5 will work more than once [#25](https://github.com/Microsoft/vscode-python/issues/25) +- Removed the feedback service to rely on VS Code's own support (which fixed an issue of document reformatting failing) [#245](https://github.com/Microsoft/vscode-python/issues/245), [#303](https://github.com/Microsoft/vscode-python/issues/303), [#363](https://github.com/Microsoft/vscode-python/issues/365) +- Do not create empty '.vscode' directory [#253](https://github.com/Microsoft/vscode-python/issues/253), [#277](https://github.com/Microsoft/vscode-python/issues/277) +- Ensure python execution environment handles unicode characters [#393](https://github.com/Microsoft/vscode-python/issues/393) +- Remove Jupyter support in favour of the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=donjayamanne.jupyter) [#223](https://github.com/microsoft/vscode-python/issues/223) + +### `conda` + +- Support installing Pylint using conda or pip when an Anaconda installation of Python is selected as the active interpreter [#301](https://github.com/Microsoft/vscode-python/issues/301) +- Add JSON schema support for conda's meta.yaml [#281](https://github.com/Microsoft/vscode-python/issues/281) +- Add JSON schema support for conda's environment.yml [#280](https://github.com/Microsoft/vscode-python/issues/280) +- Add JSON schema support for .condarc [#189](https://github.com/Microsoft/vscode-python/issues/280) +- Ensure company name 'Continuum Analytics' is replaced with 'Ananconda Inc' in the list of interpreters [#390](https://github.com/Microsoft/vscode-python/issues/390) +- Display the version of the interpreter instead of conda [#378](https://github.com/Microsoft/vscode-python/issues/378) +- Detect Anaconda on Linux even if it is not in the current path [#22](https://github.com/Microsoft/vscode-python/issues/22) + +### Interpreter selection + +- Fixes in the discovery and display of interpreters, including virtual environments [#56](https://github.com/Microsoft/vscode-python/issues/56) +- Retrieve the right value from the registry when determining the version of an interpreter on Windows [#389](https://github.com/Microsoft/vscode-python/issues/389) + +### Intellisense + +- Fetch intellisense details on-demand instead of for all possible completions [#152](https://github.com/Microsoft/vscode-python/issues/152) +- Disable auto completion in comments and strings [#110](https://github.com/Microsoft/vscode-python/issues/110), [#921](https://github.com/Microsoft/vscode-python/issues/921), [#34](https://github.com/Microsoft/vscode-python/issues/34) + +### Linting + +- Deprecate `python.linting.lintOnTextChange` [#313](https://github.com/Microsoft/vscode-python/issues/313), [#297](https://github.com/Microsoft/vscode-python/issues/297), [#28](https://github.com/Microsoft/vscode-python/issues/28), [#272](https://github.com/Microsoft/vscode-python/issues/272) +- Refactor code for executing linters (fixes running the proper linter under the selected interpreter) [#351](https://github.com/Microsoft/vscode-python/issues/351), [#397](https://github.com/Microsoft/vscode-python/issues/397) +- Don't attempt to install linters when not in a workspace [#42](https://github.com/Microsoft/vscode-python/issues/42) +- Honour `python.linting.enabled` [#26](https://github.com/Microsoft/vscode-python/issues/26) +- Don't display message 'Linter pylint is not installed' when changing settings [#260](https://github.com/Microsoft/vscode-python/issues/260) +- Display a meaningful message if pip is unavailable to install necessary module such as 'pylint' [#266](https://github.com/Microsoft/vscode-python/issues/266) +- Improvement environment variable parsing in the debugging (allows for embedded `=`) [#149](https://github.com/Microsoft/vscode-python/issues/149), [#361](https://github.com/Microsoft/vscode-python/issues/361) + +### Debugging + +- Improve selecting the port used when debugging [#304](https://github.com/Microsoft/vscode-python/pull/304) +- Don't block debugging in other extensions [#58](https://github.com/Microsoft/vscode-python/issues/58) +- Don't trigger an error to the Console Window when trying to debug an invalid Python file [#157](https://github.com/Microsoft/vscode-python/issues/157) +- No longer prompt to `Press any key to continue . . .` once debugging finishes [#239](https://github.com/Microsoft/vscode-python/issues/239) +- Do not start the extension when debugging non-Python projects [#57](https://github.com/Microsoft/vscode-python/issues/57) +- Support custom external terminals in debugger [#250](https://github.com/Microsoft/vscode-python/issues/250), [#114](https://github.com/Microsoft/vscode-python/issues/114) +- Debugging a python program should not display the message 'Cannot read property …' [#247](https://github.com/Microsoft/vscode-python/issues/247) + +### Testing + +- Refactor unit test library execution code [#350](https://github.com/Microsoft/vscode-python/issues/350) + +### Formatting + +- Deprecate the setting `python.formatting.formatOnSave` with an appropriate message [#285](https://github.com/Microsoft/vscode-python/issues/285), [#309](https://github.com/Microsoft/vscode-python/issues/309) + +## Version 0.8.0 (9 November 2017) + +- Add support for multi-root workspaces [#1228](https://github.com/DonJayamanne/pythonVSCode/issues/1228), [#1302](https://github.com/DonJayamanne/pythonVSCode/pull/1302), [#1328](https://github.com/DonJayamanne/pythonVSCode/issues/1328), [#1357](https://github.com/DonJayamanne/pythonVSCode/pull/1357) +- Add code snippet for `ipdb` [#1141](https://github.com/DonJayamanne/pythonVSCode/pull/1141) +- Add ability to resolving environment variables in path to `mypy` [#1195](https://github.com/DonJayamanne/pythonVSCode/issues/1195) +- Add ability to disable a linter globally and disable prompts to install linters [#1207](https://github.com/DonJayamanne/pythonVSCode/issues/1207) +- Auto-selecting an interpreter from a virtual environment if only one is found in the root directory of the project [#1216](https://github.com/DonJayamanne/pythonVSCode/issues/1216) +- Add support for specifying the working directory for unit tests [#1155](https://github.com/DonJayamanne/pythonVSCode/issues/1155), [#1185](https://github.com/DonJayamanne/pythonVSCode/issues/1185) +- Add syntax highlighting of pip requirements files [#1247](https://github.com/DonJayamanne/pythonVSCode/pull/1247) +- Add ability to select an interpreter even when a workspace is not open [#1260](https://github.com/DonJayamanne/pythonVSCode/issues/1260), [#1263](https://github.com/DonJayamanne/pythonVSCode/pull/1263) +- Display a code lens to change the selected interpreter to the one specified in the shebang line [#1257](https://github.com/DonJayamanne/pythonVSCode/pull/1257), [#1263](https://github.com/DonJayamanne/pythonVSCode/pull/1263), [#1267](https://github.com/DonJayamanne/pythonVSCode/pull/1267), [#1280](https://github.com/DonJayamanne/pythonVSCode/issues/1280), [#1261](https://github.com/DonJayamanne/pythonVSCode/issues/1261), [#1290](https://github.com/DonJayamanne/pythonVSCode/pull/1290) +- Expand list of interpreters displayed for selection [#1147](https://github.com/DonJayamanne/pythonVSCode/issues/1147), [#1148](https://github.com/DonJayamanne/pythonVSCode/issues/1148), [#1224](https://github.com/DonJayamanne/pythonVSCode/pull/1224), [#1240](https://github.com/DonJayamanne/pythonVSCode/pull/1240) +- Display details of current or selected interpreter in statusbar [#1147](https://github.com/DonJayamanne/pythonVSCode/issues/1147), [#1217](https://github.com/DonJayamanne/pythonVSCode/issues/1217) +- Ensure paths in workspace symbols are not prefixed with `.vscode` [#816](https://github.com/DonJayamanne/pythonVSCode/issues/816), [#1066](https://github.com/DonJayamanne/pythonVSCode/pull/1066), [#829](https://github.com/DonJayamanne/pythonVSCode/issues/829) +- Ensure paths in `PYTHONPATH` environment variable are delimited using the OS-specific path delimiter [#832](https://github.com/DonJayamanne/pythonVSCode/issues/832) +- Ensure `Rope` is not packaged with the extension [#1208](https://github.com/DonJayamanne/pythonVSCode/issues/1208), [#1207](https://github.com/DonJayamanne/pythonVSCode/issues/1207), [#1243](https://github.com/DonJayamanne/pythonVSCode/pull/1243), [#1229](https://github.com/DonJayamanne/pythonVSCode/issues/1229) +- Ensure ctags are rebuilt as expected upon file save [#624](https://github.com/DonJayamanne/pythonVSCode/issues/1212) +- Ensure right test method is executed when two test methods exist with the same name in different classes [#1203](https://github.com/DonJayamanne/pythonVSCode/issues/1203) +- Ensure unit tests run successfully on Travis for both Python 2.7 and 3.6 [#1255](https://github.com/DonJayamanne/pythonVSCode/pull/1255), [#1241](https://github.com/DonJayamanne/pythonVSCode/issues/1241), [#1315](https://github.com/DonJayamanne/pythonVSCode/issues/1315) +- Fix building of ctags when a path contains a space [#1064](https://github.com/DonJayamanne/pythonVSCode/issues/1064), [#1144](https://github.com/DonJayamanne/pythonVSCode/issues/1144),, [#1213](https://github.com/DonJayamanne/pythonVSCode/pull/1213) +- Fix autocompletion in unsaved Python files [#1194](https://github.com/DonJayamanne/pythonVSCode/issues/1194) +- Fix running of test methods in nose [#597](https://github.com/DonJayamanne/pythonVSCode/issues/597), [#1225](https://github.com/DonJayamanne/pythonVSCode/pull/1225) +- Fix to disable linting of diff windows [#1221](https://github.com/DonJayamanne/pythonVSCode/issues/1221), [#1244](https://github.com/DonJayamanne/pythonVSCode/pull/1244) +- Fix docstring formatting [#1188](https://github.com/DonJayamanne/pythonVSCode/issues/1188) +- Fix to ensure language features can run in parallel without interference with one another [#1314](https://github.com/DonJayamanne/pythonVSCode/issues/1314), [#1318](https://github.com/DonJayamanne/pythonVSCode/pull/1318) +- Fix to ensure unit tests can be debugged more than once per run [#948](https://github.com/DonJayamanne/pythonVSCode/issues/948), [#1353](https://github.com/DonJayamanne/pythonVSCode/pull/1353) +- Fix to ensure parameterized unit tests can be debugged [#1284](https://github.com/DonJayamanne/pythonVSCode/issues/1284), [#1299](https://github.com/DonJayamanne/pythonVSCode/pull/1299) +- Fix issue that causes debugger to freeze/hang [#1041](https://github.com/DonJayamanne/pythonVSCode/issues/1041), [#1354](https://github.com/DonJayamanne/pythonVSCode/pull/1354) +- Fix to support unicode characters in Python tests [#1282](https://github.com/DonJayamanne/pythonVSCode/issues/1282), [#1291](https://github.com/DonJayamanne/pythonVSCode/pull/1291) +- Changes as a result of VS Code API changes [#1270](https://github.com/DonJayamanne/pythonVSCode/issues/1270), [#1288](https://github.com/DonJayamanne/pythonVSCode/pull/1288), [#1372](https://github.com/DonJayamanne/pythonVSCode/issues/1372), [#1300](https://github.com/DonJayamanne/pythonVSCode/pull/1300), [#1298](https://github.com/DonJayamanne/pythonVSCode/issues/1298) +- Updates to Readme [#1212](https://github.com/DonJayamanne/pythonVSCode/issues/1212), [#1222](https://github.com/DonJayamanne/pythonVSCode/issues/1222) +- Fix executing a command under PowerShell [#1098](https://github.com/DonJayamanne/pythonVSCode/issues/1098) + +## Version 0.7.0 (3 August 2017) + +- Displaying internal documentation [#1008](https://github.com/DonJayamanne/pythonVSCode/issues/1008), [#10860](https://github.com/DonJayamanne/pythonVSCode/issues/10860) +- Fixes to 'async with' snippet [#1108](https://github.com/DonJayamanne/pythonVSCode/pull/1108), [#996](https://github.com/DonJayamanne/pythonVSCode/issues/996) +- Add support for environment variable in unit tests [#1074](https://github.com/DonJayamanne/pythonVSCode/issues/1074) +- Fixes to unit test code lenses not being displayed [#1115](https://github.com/DonJayamanne/pythonVSCode/issues/1115) +- Fix to empty brackets being added [#1110](https://github.com/DonJayamanne/pythonVSCode/issues/1110), [#1031](https://github.com/DonJayamanne/pythonVSCode/issues/1031) +- Fix debugging of Django applications [#819](https://github.com/DonJayamanne/pythonVSCode/issues/819), [#999](https://github.com/DonJayamanne/pythonVSCode/issues/999) +- Update isort to the latest version [#1134](https://github.com/DonJayamanne/pythonVSCode/issues/1134), [#1135](https://github.com/DonJayamanne/pythonVSCode/pull/1135) +- Fix issue causing intellisense and similar functionality to stop working [#1072](https://github.com/DonJayamanne/pythonVSCode/issues/1072), [#1118](https://github.com/DonJayamanne/pythonVSCode/pull/1118), [#1089](https://github.com/DonJayamanne/pythonVSCode/issues/1089) +- Bunch of unit tests and code cleanup +- Resolve issue where navigation to decorated function goes to decorator [#742](https://github.com/DonJayamanne/pythonVSCode/issues/742) +- Go to symbol in workspace leads to nonexisting files [#816](https://github.com/DonJayamanne/pythonVSCode/issues/816), [#829](https://github.com/DonJayamanne/pythonVSCode/issues/829) + +## Version 0.6.9 (22 July 2017) + +- Fix to enure custom linter paths are respected [#1106](https://github.com/DonJayamanne/pythonVSCode/issues/1106) + +## Version 0.6.8 (20 July 2017) + +- Add new editor menu 'Run Current Unit Test File' [#1061](https://github.com/DonJayamanne/pythonVSCode/issues/1061) +- Changed 'mypy-lang' to mypy [#930](https://github.com/DonJayamanne/pythonVSCode/issues/930), [#998](https://github.com/DonJayamanne/pythonVSCode/issues/998), [#505](https://github.com/DonJayamanne/pythonVSCode/issues/505) +- Using "Python -m" to launch linters [#716](https://github.com/DonJayamanne/pythonVSCode/issues/716), [#923](https://github.com/DonJayamanne/pythonVSCode/issues/923), [#1059](https://github.com/DonJayamanne/pythonVSCode/issues/1059) +- Add PEP 526 AutoCompletion [#1102](https://github.com/DonJayamanne/pythonVSCode/pull/1102), [#1101](https://github.com/DonJayamanne/pythonVSCode/issues/1101) +- Resolved issues in Go To and Peek Definitions [#1085](https://github.com/DonJayamanne/pythonVSCode/pull/1085), [#961](https://github.com/DonJayamanne/pythonVSCode/issues/961), [#870](https://github.com/DonJayamanne/pythonVSCode/issues/870) + +## Version 0.6.7 (02 July 2017) + +- Updated icon from jpg to png (transparent background) + +## Version 0.6.6 (02 July 2017) + +- Provide details of error with solution for changes to syntax in launch.json [#1047](https://github.com/DonJayamanne/pythonVSCode/issues/1047), [#1025](https://github.com/DonJayamanne/pythonVSCode/issues/1025) +- Provide a warning about known issues with having pyenv.cfg whilst debugging [#913](https://github.com/DonJayamanne/pythonVSCode/issues/913) +- Create .vscode directory if not found [#1043](https://github.com/DonJayamanne/pythonVSCode/issues/1043) +- Highlighted text due to linter errors is off by one column [#965](https://github.com/DonJayamanne/pythonVSCode/issues/965), [#970](https://github.com/DonJayamanne/pythonVSCode/pull/970) +- Added preliminary support for WSL Bash and Cygwin [#1049](https://github.com/DonJayamanne/pythonVSCode/pull/1049) +- Ability to configure the linter severity levels [#941](https://github.com/DonJayamanne/pythonVSCode/pull/941), [#895](https://github.com/DonJayamanne/pythonVSCode/issues/895) +- Fixes to unit tests [#1051](https://github.com/DonJayamanne/pythonVSCode/pull/1051), [#1050](https://github.com/DonJayamanne/pythonVSCode/pull/1050) +- Outdent lines following `continue`, `break` and `return` [#1050](https://github.com/DonJayamanne/pythonVSCode/pull/1050) +- Change location of cache for Jedi files [#1035](https://github.com/DonJayamanne/pythonVSCode/pull/1035) +- Fixes to the way directories are searched for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569), [#1040](https://github.com/DonJayamanne/pythonVSCode/pull/1040) +- Handle outputs from Python packages that interfere with the way autocompletion is handled [#602](https://github.com/DonJayamanne/pythonVSCode/issues/602) + +## Version 0.6.5 (13 June 2017) + +- Fix error in launch.json [#1006](https://github.com/DonJayamanne/pythonVSCode/issues/1006) +- Detect current workspace interpreter when selecting interpreter [#1006](https://github.com/DonJayamanne/pythonVSCode/issues/979) +- Disable output buffering when debugging [#1005](https://github.com/DonJayamanne/pythonVSCode/issues/1005) +- Updated snippets to use correct placeholder syntax [#976](https://github.com/DonJayamanne/pythonVSCode/pull/976) +- Fix hover and auto complete unit tests [#1012](https://github.com/DonJayamanne/pythonVSCode/pull/1012) +- Fix hover definition variable test for Python 3.5 [#1013](https://github.com/DonJayamanne/pythonVSCode/pull/1013) +- Better formatting of docstring [#821](https://github.com/DonJayamanne/pythonVSCode/pull/821), [#919](https://github.com/DonJayamanne/pythonVSCode/pull/919) +- Supporting more paths when searching for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) +- Increase buffer output (to support detection large number of tests) [#927](https://github.com/DonJayamanne/pythonVSCode/issues/927) + +## Version 0.6.4 (4 May 2017) + +- Fix dates in changelog [#899](https://github.com/DonJayamanne/pythonVSCode/pull/899) +- Using charriage return or line feeds to split a document into multiple lines [#917](https://github.com/DonJayamanne/pythonVSCode/pull/917), [#821](https://github.com/DonJayamanne/pythonVSCode/issues/821) +- Doc string not being displayed [#888](https://github.com/DonJayamanne/pythonVSCode/issues/888) +- Supporting paths that begin with the ~/ [#909](https://github.com/DonJayamanne/pythonVSCode/issues/909) +- Supporting more paths when searching for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) +- Supporting ~/ paths when providing the path to ctag file [#910](https://github.com/DonJayamanne/pythonVSCode/issues/910) +- Disable linting of python files opened in diff viewer [#896](https://github.com/DonJayamanne/pythonVSCode/issues/896) +- Added a new command `Go to Python Object` [#928](https://github.com/DonJayamanne/pythonVSCode/issues/928) +- Restored the menu item to rediscover tests [#863](https://github.com/DonJayamanne/pythonVSCode/issues/863) +- Changes to rediscover tests when test files are altered and saved [#863](https://github.com/DonJayamanne/pythonVSCode/issues/863) + +## Version 0.6.3 (19 April 2017) + +- Fix debugger issue [#893](https://github.com/DonJayamanne/pythonVSCode/issues/893) +- Improvements to debugging unit tests (check if string starts with, instead of comparing equality) [#797](https://github.com/DonJayamanne/pythonVSCode/issues/797) + +## Version 0.6.2 (13 April 2017) + +- Fix incorrect indenting [#880](https://github.com/DonJayamanne/pythonVSCode/issues/880) + +### Thanks + +- [Yuwei Ba](https://github.com/ibigbug) + +## Version 0.6.1 (10 April 2017) + +- Add support for new variable syntax in upcoming VS Code release [#774](https://github.com/DonJayamanne/pythonVSCode/issues/774), [#855](https://github.com/DonJayamanne/pythonVSCode/issues/855), [#873](https://github.com/DonJayamanne/pythonVSCode/issues/873), [#823](https://github.com/DonJayamanne/pythonVSCode/issues/823) +- Resolve issues in code refactoring [#802](https://github.com/DonJayamanne/pythonVSCode/issues/802), [#824](https://github.com/DonJayamanne/pythonVSCode/issues/824), [#825](https://github.com/DonJayamanne/pythonVSCode/pull/825) +- Changes to labels in Python Interpreter lookup [#815](https://github.com/DonJayamanne/pythonVSCode/pull/815) +- Resolve Typos [#852](https://github.com/DonJayamanne/pythonVSCode/issues/852) +- Use fully qualitified Python Path when installing dependencies [#866](https://github.com/DonJayamanne/pythonVSCode/issues/866) +- Commands for running tests from a file [#502](https://github.com/DonJayamanne/pythonVSCode/pull/502) +- Fix Sorting of imports when path contains spaces [#811](https://github.com/DonJayamanne/pythonVSCode/issues/811) +- Fixing occasional failure of linters [#793](https://github.com/DonJayamanne/pythonVSCode/issues/793), [#833](https://github.com/DonJayamanne/pythonVSCode/issues/838), [#860](https://github.com/DonJayamanne/pythonVSCode/issues/860) +- Added ability to pre-load some modules to improve autocompletion [#581](https://github.com/DonJayamanne/pythonVSCode/issues/581) + +### Thanks + +- [Ashwin Mathews](https://github.com/ajmathews) +- [Alexander Ioannidis](https://github.com/slint) +- [Andreas Schlapsi](https://github.com/aschlapsi) + +## Version 0.6.0 (10 March 2017) + +- Moved Jupyter functionality into a separate extension [Jupyter]() +- Updated readme [#779](https://github.com/DonJayamanne/pythonVSCode/issues/779) +- Changing default arguments of `mypy` [#658](https://github.com/DonJayamanne/pythonVSCode/issues/658) +- Added ability to disable formatting [#559](https://github.com/DonJayamanne/pythonVSCode/issues/559) +- Fixing ability to run a Python file in a terminal [#784](https://github.com/DonJayamanne/pythonVSCode/issues/784) +- Added support for Proxy settings when installing Python packages using Pip [#778](https://github.com/DonJayamanne/pythonVSCode/issues/778) + +## Version 0.5.9 (3 March 2017) + +- Fixed navigating to definitions [#711](https://github.com/DonJayamanne/pythonVSCode/issues/711) +- Support auto detecting binaries from Python Path [#716](https://github.com/DonJayamanne/pythonVSCode/issues/716) +- Setting PYTHONPATH environment variable [#686](https://github.com/DonJayamanne/pythonVSCode/issues/686) +- Improving Linter performance, killing redundant processes [4a8319e](https://github.com/DonJayamanne/pythonVSCode/commit/4a8319e0859f2d49165c9a08fe147a647d03ece9) +- Changed default path of the CATAS file to `.vscode/tags` [#722](https://github.com/DonJayamanne/pythonVSCode/issues/722) +- Add parsing severity level for flake8 and pep8 linters [#709](https://github.com/DonJayamanne/pythonVSCode/pull/709) +- Fix to restore function descriptions (intellisense) [#727](https://github.com/DonJayamanne/pythonVSCode/issues/727) +- Added default configuration for debugging Pyramid [#287](https://github.com/DonJayamanne/pythonVSCode/pull/287) +- Feature request: Run current line in Terminal [#738](https://github.com/DonJayamanne/pythonVSCode/issues/738) +- Miscellaneous improvements to hover provider [6a7a3f3](https://github.com/DonJayamanne/pythonVSCode/commit/6a7a3f32ab8add830d13399fec6f0cdd14cd66fc), [6268306](https://github.com/DonJayamanne/pythonVSCode/commit/62683064d01cfc2b76d9be45587280798a96460b) +- Fixes to rename refactor (due to 'LF' EOL in Windows) [#748](https://github.com/DonJayamanne/pythonVSCode/pull/748) +- Fixes to ctag file being generated in home folder when no workspace is opened [#753](https://github.com/DonJayamanne/pythonVSCode/issues/753) +- Fixes to ctag file being generated in home folder when no workspace is opened [#753](https://github.com/DonJayamanne/pythonVSCode/issues/753) +- Disabling auto-completion in single line comments [#74](https://github.com/DonJayamanne/pythonVSCode/issues/74) +- Fixes to debugging of modules [#518](https://github.com/DonJayamanne/pythonVSCode/issues/518) +- Displaying unit test status icons against unit test code lenses [#678](https://github.com/DonJayamanne/pythonVSCode/issues/678) +- Fix issue where causing 'python.python-debug.startSession' not found message to be displayed when debugging single file [#708](https://github.com/DonJayamanne/pythonVSCode/issues/708) +- Ability to include packages directory when generating tags file [#735](https://github.com/DonJayamanne/pythonVSCode/issues/735) +- Fix issue where running selected text in terminal does not work [#758](https://github.com/DonJayamanne/pythonVSCode/issues/758) +- Fix issue where disabling linter doesn't disable it (when no workspace is open) [#763](https://github.com/DonJayamanne/pythonVSCode/issues/763) +- Search additional directories for Python Interpreters (~/.virtualenvs, ~/Envs, ~/.pyenv) [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) +- Added ability to pre-load some modules to improve autocompletion [#581](https://github.com/DonJayamanne/pythonVSCode/issues/581) +- Removed invalid default value in launch.json file [#586](https://github.com/DonJayamanne/pythonVSCode/issues/586) +- Added ability to configure the pylint executable path [#766](https://github.com/DonJayamanne/pythonVSCode/issues/766) +- Fixed single file debugger to ensure the Python interpreter configured in python.PythonPath is being used [#769](https://github.com/DonJayamanne/pythonVSCode/issues/769) + +## Version 0.5.8 (3 February 2017) + +- Fixed a bug in [debugging single files without a launch configuration](https://code.visualstudio.com/updates/v1_9#_debugging-without-a-launch-configuration) [#700](https://github.com/DonJayamanne/pythonVSCode/issues/700) +- Fixed error when starting REPL [#692](https://github.com/DonJayamanne/pythonVSCode/issues/692) + +## Version 0.5.7 (3 February 2017) + +- Added support for [debugging single files without a launch configuration](https://code.visualstudio.com/updates/v1_9#_debugging-without-a-launch-configuration) +- Adding support for debug snippets [#660](https://github.com/DonJayamanne/pythonVSCode/issues/660) +- Ability to run a selected text in a Django shell [#652](https://github.com/DonJayamanne/pythonVSCode/issues/652) +- Adding support for the use of a customized 'isort' for sorting of imports [#632](https://github.com/DonJayamanne/pythonVSCode/pull/632) +- Debugger auto-detecting python interpreter from the path provided [#688](https://github.com/DonJayamanne/pythonVSCode/issues/688) +- Showing symbol type on hover [#657](https://github.com/DonJayamanne/pythonVSCode/pull/657) +- Fixes to running Python file when terminal uses Powershell [#651](https://github.com/DonJayamanne/pythonVSCode/issues/651) +- Fixes to linter issues when displaying Git diff view for Python files [#665](https://github.com/DonJayamanne/pythonVSCode/issues/665) +- Fixes to 'Go to definition' functionality [#662](https://github.com/DonJayamanne/pythonVSCode/issues/662) +- Fixes to Jupyter cells numbered larger than '10' [#681](https://github.com/DonJayamanne/pythonVSCode/issues/681) + +## Version 0.5.6 (16 January 2017) + +- Added support for Python 3.6 [#646](https://github.com/DonJayamanne/pythonVSCode/issues/646), [#631](https://github.com/DonJayamanne/pythonVSCode/issues/631), [#619](https://github.com/DonJayamanne/pythonVSCode/issues/619), [#613](https://github.com/DonJayamanne/pythonVSCode/issues/613) +- Autodetect in python path in virtual environments [#353](https://github.com/DonJayamanne/pythonVSCode/issues/353) +- Add syntax highlighting of code samples in hover defintion [#555](https://github.com/DonJayamanne/pythonVSCode/issues/555) +- Launch REPL for currently selected interpreter [#560](https://github.com/DonJayamanne/pythonVSCode/issues/560) +- Fixes to debugging of modules [#589](https://github.com/DonJayamanne/pythonVSCode/issues/589) +- Reminder to install jedi and ctags in Quick Start [#642](https://github.com/DonJayamanne/pythonVSCode/pull/642) +- Improvements to Symbol Provider [#622](https://github.com/DonJayamanne/pythonVSCode/pull/622) +- Changes to disable unit test prompts for workspace [#559](https://github.com/DonJayamanne/pythonVSCode/issues/559) +- Minor fixes [#627](https://github.com/DonJayamanne/pythonVSCode/pull/627) + +## Version 0.5.5 (25 November 2016) + +- Fixes to debugging of unittests (nose and pytest) [#543](https://github.com/DonJayamanne/pythonVSCode/issues/543) +- Fixes to debugging of Django [#546](https://github.com/DonJayamanne/pythonVSCode/issues/546) + +## Version 0.5.4 (24 November 2016) + +- Fixes to installing missing packages [#544](https://github.com/DonJayamanne/pythonVSCode/issues/544) +- Fixes to indentation of blocks of code [#432](https://github.com/DonJayamanne/pythonVSCode/issues/432) +- Fixes to debugging of unittests [#543](https://github.com/DonJayamanne/pythonVSCode/issues/543) +- Fixes to extension when a workspace (folder) isn't open [#542](https://github.com/DonJayamanne/pythonVSCode/issues/542) + +## Version 0.5.3 (23 November 2016) + +- Added support for [PySpark](http://spark.apache.org/docs/0.9.0/python-programming-guide.html) [#539](https://github.com/DonJayamanne/pythonVSCode/pull/539), [#540](https://github.com/DonJayamanne/pythonVSCode/pull/540) +- Debugging unittests (UnitTest, pytest, nose) [#333](https://github.com/DonJayamanne/pythonVSCode/issues/333) +- Displaying progress for formatting [#327](https://github.com/DonJayamanne/pythonVSCode/issues/327) +- Auto indenting `else:` inside `if` and similar code blocks [#432](https://github.com/DonJayamanne/pythonVSCode/issues/432) +- Prefixing new lines with '#' when new lines are added in the middle of a comment string [#365](https://github.com/DonJayamanne/pythonVSCode/issues/365) +- Debugging python modules [#518](https://github.com/DonJayamanne/pythonVSCode/issues/518), [#354](https://github.com/DonJayamanne/pythonVSCode/issues/354) + - Use new debug configuration `Python Module` +- Added support for workspace symbols using Exuberant CTags [#138](https://github.com/DonJayamanne/pythonVSCode/issues/138) + - New command `Python: Build Workspace Symbols` +- Added ability for linter to ignore paths or files [#501](https://github.com/DonJayamanne/pythonVSCode/issues/501) + - Add the following setting in `settings.json` + +```python + "python.linting.ignorePatterns": [ + ".vscode/*.py", + "**/site-packages/**/*.py" + ], +``` + +- Automatically adding brackets when autocompleting functions/methods [#425](https://github.com/DonJayamanne/pythonVSCode/issues/425) + - To enable this feature, turn on the setting `"python.autoComplete.addBrackets": true` +- Running nose tests with the arguments '--with-xunit' and '--xunit-file' [#517](https://github.com/DonJayamanne/pythonVSCode/issues/517) +- Added support for workspaceRootFolderName in settings.json [#525](https://github.com/DonJayamanne/pythonVSCode/pull/525), [#522](https://github.com/DonJayamanne/pythonVSCode/issues/522) +- Added support for workspaceRootFolderName in settings.json [#525](https://github.com/DonJayamanne/pythonVSCode/pull/525), [#522](https://github.com/DonJayamanne/pythonVSCode/issues/522) +- Fixes to running code in terminal [#515](https://github.com/DonJayamanne/pythonVSCode/issues/515) + +## Version 0.5.2 + +- Fix issue with mypy linter [#505](https://github.com/DonJayamanne/pythonVSCode/issues/505) +- Fix auto completion for files with different encodings [#496](https://github.com/DonJayamanne/pythonVSCode/issues/496) +- Disable warnings when debugging Django version prior to 1.8 [#479](https://github.com/DonJayamanne/pythonVSCode/issues/479) +- Prompt to save changes when refactoring without saving any changes [#441](https://github.com/DonJayamanne/pythonVSCode/issues/441) +- Prompt to save changes when renaminv without saving any changes [#443](https://github.com/DonJayamanne/pythonVSCode/issues/443) +- Use editor indentation size when refactoring code [#442](https://github.com/DonJayamanne/pythonVSCode/issues/442) +- Add support for custom jedi paths [#500](https://github.com/DonJayamanne/pythonVSCode/issues/500) + +## Version 0.5.1 + +- Prompt to install linter if not installed [#255](https://github.com/DonJayamanne/pythonVSCode/issues/255) +- Prompt to configure and install test framework +- Added support for pylama [#495](https://github.com/DonJayamanne/pythonVSCode/pull/495) +- Partial support for PEP484 +- Linting python files when they are opened [#462](https://github.com/DonJayamanne/pythonVSCode/issues/462) +- Fixes to unit tests discovery [#307](https://github.com/DonJayamanne/pythonVSCode/issues/307), + [#459](https://github.com/DonJayamanne/pythonVSCode/issues/459) +- Fixes to intellisense [#438](https://github.com/DonJayamanne/pythonVSCode/issues/438), + [#433](https://github.com/DonJayamanne/pythonVSCode/issues/433), + [#457](https://github.com/DonJayamanne/pythonVSCode/issues/457), + [#436](https://github.com/DonJayamanne/pythonVSCode/issues/436), + [#434](https://github.com/DonJayamanne/pythonVSCode/issues/434), + [#447](https://github.com/DonJayamanne/pythonVSCode/issues/447), + [#448](https://github.com/DonJayamanne/pythonVSCode/issues/448), + [#293](https://github.com/DonJayamanne/pythonVSCode/issues/293), + [#381](https://github.com/DonJayamanne/pythonVSCode/pull/381) +- Supporting additional search paths for interpreters on windows [#446](https://github.com/DonJayamanne/pythonVSCode/issues/446) +- Fixes to code refactoring [#440](https://github.com/DonJayamanne/pythonVSCode/issues/440), + [#467](https://github.com/DonJayamanne/pythonVSCode/issues/467), + [#468](https://github.com/DonJayamanne/pythonVSCode/issues/468), + [#445](https://github.com/DonJayamanne/pythonVSCode/issues/445) +- Fixes to linters [#463](https://github.com/DonJayamanne/pythonVSCode/issues/463) + [#439](https://github.com/DonJayamanne/pythonVSCode/issues/439), +- Bug fix in handling nosetest arguments [#407](https://github.com/DonJayamanne/pythonVSCode/issues/407) +- Better error handling when linter fails [#402](https://github.com/DonJayamanne/pythonVSCode/issues/402) +- Restoring extension specific formatting [#421](https://github.com/DonJayamanne/pythonVSCode/issues/421) +- Fixes to debugger (unwanted breakpoints) [#392](https://github.com/DonJayamanne/pythonVSCode/issues/392), [#379](https://github.com/DonJayamanne/pythonVSCode/issues/379) +- Support spaces in python path when executing in terminal [#428](https://github.com/DonJayamanne/pythonVSCode/pull/428) +- Changes to snippets [#429](https://github.com/DonJayamanne/pythonVSCode/pull/429) +- Marketplace changes [#430](https://github.com/DonJayamanne/pythonVSCode/pull/430) +- Cleanup and miscellaneous fixes (typos, keyboard bindings and the liks) + +## Version 0.5.0 + +- Remove dependency on zmq when using Jupyter or IPython (pure python solution) +- Added a default keybinding for `Jupyter:Run Selection/Line` of `ctrl+alt+enter` +- Changes to update settings.json with path to python using [native API](https://github.com/DonJayamanne/pythonVSCode/commit/bce22a2b4af87eaf40669c6360eff3675280cdad) +- Changes to use [native API](https://github.com/DonJayamanne/pythonVSCode/commit/bce22a2b4af87eaf40669c6360eff3675280cdad) for formatting when saving documents +- Reusing existing terminal instead of creating new terminals +- Limiting linter messages to opened documents (hide messages if document is closed) [#375](https://github.com/DonJayamanne/pythonVSCode/issues/375) +- Resolving extension load errors when [#375](https://github.com/DonJayamanne/pythonVSCode/issues/375) +- Fixes to discovering unittests [#386](https://github.com/DonJayamanne/pythonVSCode/issues/386) +- Fixes to sending code to terminal on Windows [#387](https://github.com/DonJayamanne/pythonVSCode/issues/387) +- Fixes to executing python file in terminal on Windows [#385](https://github.com/DonJayamanne/pythonVSCode/issues/385) +- Fixes to launching local help (documentation) on Linux +- Fixes to typo in configuration documentation [#391](https://github.com/DonJayamanne/pythonVSCode/pull/391) +- Fixes to use `python.pythonPath` when sorting imports [#393](https://github.com/DonJayamanne/pythonVSCode/pull/393) +- Fixes to linters to handle situations when line numbers aren't returned [#399](https://github.com/DonJayamanne/pythonVSCode/pull/399) +- Fixes to signature tooltips when docstring is very long [#368](https://github.com/DonJayamanne/pythonVSCode/issues/368), [#113](https://github.com/DonJayamanne/pythonVSCode/issues/113) + +## Version 0.4.2 + +- Fix for autocompletion and code navigation with unicode characters [#372](https://github.com/DonJayamanne/pythonVSCode/issues/372), [#364](https://github.com/DonJayamanne/pythonVSCode/issues/364) + +## Version 0.4.1 + +- Debugging of [Django templates](https://github.com/DonJayamanne/pythonVSCode/wiki/Debugging-Django#templates) +- Linting with [mypy](https://github.com/DonJayamanne/pythonVSCode/wiki/Linting#mypy) +- Improved error handling when loading [Jupyter/IPython]() +- Fixes to unittests + +## Version 0.4.0 + +- Added support for [Jupyter/IPython]() +- Added local help (offline documentation) +- Added ability to pass in extra arguments to interpreter when executing scripts ([#316](https://github.com/DonJayamanne/pythonVSCode/issues/316)) +- Added ability set current working directory as the script file directory, when to executing a Python script +- Rendering intellisense icons correctly ([#322](https://github.com/DonJayamanne/pythonVSCode/issues/322)) +- Changes to capitalization of context menu text ([#320](https://github.com/DonJayamanne/pythonVSCode/issues/320)) +- Bug fix to running pydocstyle linter on windows ([#317](https://github.com/DonJayamanne/pythonVSCode/issues/317)) +- Fixed performance issues with regards to code navigation, displaying code Symbols and the like ([#324](https://github.com/DonJayamanne/pythonVSCode/issues/324)) +- Fixed code renaming issue when renaming imports ([#325](https://github.com/DonJayamanne/pythonVSCode/issues/325)) +- Fixed issue with the execution of the command `python.execInTerminal` via a shortcut ([#340](https://github.com/DonJayamanne/pythonVSCode/issues/340)) +- Fixed issue with code refactoring ([#363](https://github.com/DonJayamanne/pythonVSCode/issues/363)) + +## Version 0.3.24 + +- Added support for clearing cached tests [#307](https://github.com/DonJayamanne/pythonVSCode/issues/307) +- Added support for executing files in terminal with spaces in paths [#308](https://github.com/DonJayamanne/pythonVSCode/issues/308) +- Fix issue related to running unittests on Windows [#309](https://github.com/DonJayamanne/pythonVSCode/issues/309) +- Support custom environment variables when launching external terminal [#311](https://github.com/DonJayamanne/pythonVSCode/issues/311) + +## Version 0.3.23 + +- Added support for the attribute supportsRunInTerminal attribute in debugger [#304](https://github.com/DonJayamanne/pythonVSCode/issues/304) +- Changes to ensure remote debugging resolves remote paths correctly [#302](https://github.com/DonJayamanne/pythonVSCode/issues/302) +- Added support for custom pytest and nosetest paths [#301](https://github.com/DonJayamanne/pythonVSCode/issues/301) +- Resolved issue in `Watch` window displaying `" + ], +``` + +...this will only run the suite with the tests you care about during a test run (be sure to set the debugger to run the `Debug Unit Tests` launcher). + +### Running Functional Tests + +Functional tests are those in files with extension `.functional.test.ts`. +These tests are similar to system tests in scope, but are run like unit tests. + +You can run functional tests in a similar way to that for unit tests: + +- via the "Functional Tests" launch option, or +- on the command line via `npm run test:functional` + +### Running System Tests + +Note: System tests are those in files with extension `.test*.ts` but which are neither `.functional.test.ts` nor `.unit.test.ts`. + +1. Make sure you have compiled all code (done automatically when using incremental building) +1. Ensure you have disabled breaking into 'Uncaught Exceptions' when running the Unit Tests +1. For the linters and formatters tests to pass successfully, you will need to have those corresponding Python libraries installed locally by using the `./requirements.txt` and `build/test-requirements.txt` files +1. Run the tests via `npm run` or the Debugger launch options (you can "Start Without Debugging"). +1. **Note** you will be running tests under the default Python interpreter for the system. + +You can also run the tests from the command-line (after compiling): + +```shell +npm run testSingleWorkspace # will launch the VSC UI +npm run testMultiWorkspace # will launch the VSC UI +``` + +#### Customising the Test Run + +If you want to change which tests are run or which version of Python is used, +you can do this by setting environment variables. The same variables work when +running from the command line or launching from within VSCode, though the +mechanism used to specify them changes a little. + +- Setting `CI_PYTHON_PATH` lets you change the version of python the tests are executed with +- Setting `VSC_PYTHON_CI_TEST_GREP` lets you filter the tests by name + +_`CI_PYTHON_PATH`_ + +In some tests a Python executable is actually run. The default executable is +`python` (for now). Unless you've run the tests inside a virtual environment, +this will almost always mean Python 2 is used, which probably isn't what you +want. + +By setting the `CI_PYTHON_PATH` environment variable you can +control the exact Python executable that gets used. If the executable +you specify isn't on `$PATH` then be sure to use an absolute path. + +This is also the mechanism for testing against other versions of Python. + +_`VSC_PYTHON_CI_TEST_GREP`_ + +This environment variable allows providing a regular expression which will +be matched against suite and test "names" to be run. By default all tests +are run. + +For example, to run only the tests in the `Sorting` suite (from +[`src/test/format/extension.sort.test.ts`](https://github.com/Microsoft/vscode-python/blob/84f9c7a174111/src/test/format/extension.sort.test.ts)) +you would set the value to `Sorting`. To run the `ProcessService` and +`ProcessService Observable` tests which relate to `stderr` handling, you might +use the value `ProcessService.*stderr`. + +Be sure to escape any grep-sensitive characters in your suite name. + +In some rare cases in the "system" tests the `VSC_PYTHON_CI_TEST_GREP` +environment variable is ignored. If that happens then you will need to +temporarily modify the `const grep =` line in +[`src/test/index.ts`](https://github.com/Microsoft/vscode-python/blob/84f9c7a174111/src/test/index.ts#L64). + +_Launching from VSCode_ + +In order to set environment variables when launching the tests from VSCode you +should edit the `launch.json` file. For example you can add the following to the +appropriate configuration you want to run to change the interpreter used during +testing: + +```js + "env": { + "CI_PYTHON_PATH": "/absolute/path/to/interpreter/of/choice/python" + } +``` + +_On the command line_ + +The mechanism to set environment variables on the command line will vary based +on your system, however most systems support a syntax like the following for +setting a single variable for a subprocess: + +```shell +VSC_PYTHON_CI_TEST_GREP=Sorting npm run testSingleWorkspace +``` + +### Testing Python Scripts + +The extension has a number of scripts in ./pythonFiles. Tests for these +scripts are found in ./pythonFiles/tests. To run those tests: + +- `python2.7 pythonFiles/tests/run_all.py` +- `python3 -m pythonFiles.tests` + +By default, functional tests are included. To exclude them: + +`python3 -m pythonFiles.tests --no-functional` + +To run only the functional tests: + +`python3 -m pythonFiles.tests --functional` + +### Standard Debugging + +Clone the repo into any directory, open that directory in VSCode, and use the `Extension` launch option within VSCode. + +### Debugging the Python Extension Debugger + +The easiest way to debug the Python Debugger (in our opinion) is to clone this git repo directory into [your](https://code.visualstudio.com/docs/extensions/install-extension#_your-extensions-folder) extensions directory. +From there use the `Extension + Debugger` launch option. + +### Coding Standards + +Information on our coding standards can be found [here](https://github.com/Microsoft/vscode-python/blob/main/CODING_STANDARDS.md). +We have CI tests to ensure the code committed will adhere to the above coding standards. \*You can run this locally by executing the command `npx gulp precommit` or use the `precommit` Task. + +Messages displayed to the user must be localized using/created constants from/in the [localize.ts](https://github.com/Microsoft/vscode-python/blob/main/src/client/common/utils/localize.ts) file. + +## Development process + +To effectively contribute to this extension, it helps to know how its +development process works. That way you know not only why the +project maintainers do what they do to keep this project running +smoothly, but it allows you to help out by noticing when a step is +missed or to learn in case someday you become a project maintainer as +well! + +### Helping others + +First and foremost, we try to be helpful to users of the extension. +We monitor +[Stack Overflow questions](https://stackoverflow.com/questions/tagged/visual-studio-code+python) +to see where people might need help. We also try to respond to all +issues in some way in a timely manner (typically in less than one +business day, definitely no more than a week). We also answer +questions that reach us in other ways, e.g. Twitter. + +For pull requests, we aim to review any externally contributed PR no later +than the next sprint from when it was submitted (see +[Release Cycle](#release-cycle) below for our sprint schedule). + +### Release cycle + +Planning is done as one week sprints. We start a sprint every Thursday. +All [P0](https://github.com/Microsoft/vscode-python/labels/P0) issues are expected +to be fixed in the current sprint, else the next release will be blocked. +[P1](https://github.com/Microsoft/vscode-python/labels/P1) issues are a +top-priority and we try to close before the next release. All other issues are +considered best-effort for that sprint. + +The extension aims to do a new release once a month. A +[release plan](https://github.com/Microsoft/vscode-python/labels/release%20plan) +is created for each release to help track anything that requires a +person to do (long-term this project aims to automate as much of the +development process as possible). + +All development is actively done in the `main` branch of the +repository. This allows us to have a +[development build](#development-build) which is expected to be stable at +all times. Once we reach a release candidate, it becomes +our [release branch](https://github.com/microsoft/vscode-python/branches). +At that point only what is in the release branch will make it into the next +release. + +### Issue triaging + +#### Classifying issues + +To help actively track what stage +[issues](https://github.com/Microsoft/vscode-python/issues) +are at, various labels are used. The following label types are expected to +be set on all open issues (otherwise the issue is not considered triaged): + +1. `needs`/`triage`/`classify` +1. `feature` +1. `type` + +These labels cover what is blocking the issue from closing, what is affected by +the issue, and what kind of issue it is. (The `feature` label should be `feature-*` if the issue doesn't fit into any other `feature` label appropriately.) + +It is also very important to make the title accurate. People often write very brief, quick titles or ones that describe what they think the problem is. By updating the title to be appropriately descriptive for what _you_ think the issue is, you not only make finding older issues easier, but you also help make sure that you and the original reporter agree on what the issue is. + +#### Post-classification + +Once an issue has been appropriately classified, there are two keys ways to help out. One is to go through open issues that +have a merged fix and verify that the fix did in fact work. The other is to try to fix issues marked as `needs PR`. + +### Pull requests + +Key details that all pull requests are expected to handle should be +in the [pull request template](https://github.com/Microsoft/vscode-python/blob/main/.github/PULL_REQUEST_TEMPLATE.md). We do expect CI to be passing for a pull request before we will consider merging it. + +### Versioning + +Starting in 2018, the extension switched to +[calendar versioning](http://calver.org/) since the extension +auto-updates and thus there is no need to track its version +number for backwards-compatibility. In 2020, the extension switched to +having the the major version be the year of release, the minor version the +release count for that year, and the build number is a number that increments +for every build. +For example the first release made in 2020 is `2020.1.`. + +## Releasing + +Overall steps for releasing are covered in the +[release plan](https://github.com/Microsoft/vscode-python/labels/release%20plan) +([template](https://github.com/Microsoft/vscode-python/blob/main/.github/release_plan.md)). + +### Building a release + +To create a release _build_, follow the steps outlined in the [release plan](https://github.com/Microsoft/vscode-python/labels/release%20plan) (which has a [template](https://github.com/Microsoft/vscode-python/blob/main/.github/release_plan.md)). + +## Local Build + +Steps to build the extension on your machine once you've cloned the repo: + +```bash +> npm install -g vsce +# Perform the next steps in the vscode-python folder. +> npm ci +> python3 -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt +# For python 3.6 and lower use this command to install the debugger +> python3 -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy +# For python 3.7 and greater use this command to install the debugger +> python3 -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt +> python3 ./pythonFiles/install_debugpy.py +> npm run clean +> npm run package # This step takes around 10 minutes. +``` + +Resulting in a `ms-python-insiders.vsix` file in your `vscode-python` folder. + +⚠️ If you made changes to `package.json`, run `npm install` (instead of `npm ci`) to update `package-lock.json` and install dependencies all at once. + +## Development Build + +If you would like to use the latest version of the extension as committed to `main` that has passed our test suite, then you may set the `"python.insidersChannel"` setting to `"daily"` or `"weekly"` based on how often you would like the extension to check for updates. + +You may also download and install the extension manually from the following +[location](https://pvsc.blob.core.windows.net/extension-builds/ms-python-insiders.vsix). +Once you have downloaded the +[ms-python-insiders.vsix](https://pvsc.blob.core.windows.net/extension-builds/ms-python-insiders.vsix) +file, please follow the instructions on +[this page](https://code.visualstudio.com/docs/editor/extension-gallery#_install-from-a-vsix) +to install the extension. Do note that the manual install will not automatically update to newer builds unless you set the `"python.insidersChannel"` setting (it will get replaced with released versions from the Marketplace once they are newer than the version install manually). diff --git a/LICENSE b/LICENSE index 67734473d340..8cb179cdb694 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +Copyright (c) Microsoft Corporation. All rights reserved. -Copyright (c) 2015 DonJayamanne +MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -12,7 +12,7 @@ furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER diff --git a/PYTHON_INTERACTIVE_TROUBLESHOOTING.md b/PYTHON_INTERACTIVE_TROUBLESHOOTING.md new file mode 100644 index 000000000000..03688ee0a986 --- /dev/null +++ b/PYTHON_INTERACTIVE_TROUBLESHOOTING.md @@ -0,0 +1,66 @@ +# Troubleshooting Jupyter issues in the Python Interactive Window or Notebook Editor + +This document is intended to help troubleshoot problems with starting Jupyter in the Python Interactive Window or Notebook Editor. + +--- + +## Jupyter Not Starting + +This error can happen when + +- Jupyter is out of date +- Jupyter is not installed +- You picked the wrong Python environment (one that doesn't have Jupyter installed). + +### The first step is to verify you are running the Python environment that you have Jupyter installed into. + +The first time that you start the Interactive Window or the Notebook Editor VS Code will attempt to locate a Python environment that has Jupyter installed in it and can start a notebook. + +The first Python interpreter to check will be the one selected with the selection dropdown on the bottom left of the VS Code window: + +![selector](resources/PythonSelector.png) + +Once a suitable interpreter with Jupyter has been located, VS Code will continue to use that interpreter for starting up Jupyter servers. +If no interpreters are found with Jupyter installed a popup message will ask if you would like to install Jupyter into the current interpreter. + +![install Jupyter](resources/InstallJupyter.png) + +If you would like to change from using the saved Python interpreter to a new interpreter for launching Jupyter just use the "Python: Select interpreter to start Jupyter server" VS Code command to change it. + +### The second step is to check that jupyter isn't giving any errors on startup. + +Run the following command from an environment that matches the Python you selected: +`python -m jupyter notebook --version` + +If this command shows any warnings, you need to upgrade or fix the warnings to continue with this version of Python. +If this command says 'no module named jupyter', you need to install Jupyter. + +### How to install Jupyter + +You can do this in a number of different ways: + +#### Anaconda + +Anaconda is a popular Python distribution. It makes it super easy to get Jupyter up and running. + +If you're already using Anaconda, follow these steps to get Jupyter + +1. Start anaconda environment +1. Run 'conda install jupyter' +1. Restart VS Code +1. Pick the conda version of Python in the python selector + +Otherwise you can install Anaconda and pick the default options +https://www.anaconda.com/download + +#### Pip + +You can also install Jupyter using pip. + +1. python -m pip install --upgrade pip +1. python -m pip install jupyter +1. Restart VS Code +1. Pick the Python environment you did the pip install in + +For more information see +http://jupyter.org/install diff --git a/README.md b/README.md index cdc22e73ef1d..68479b49d41f 100644 --- a/README.md +++ b/README.md @@ -1,231 +1,94 @@ -# Python -Linting, Debugging (multi-threaded, web apps, remote), Intellisense, auto-completion, code formatting, snippets, unit testing, and more. - -##[Wiki](https://github.com/DonJayamanne/pythonVSCode/wiki) -Once installed, do remember to [configure the path](https://github.com/DonJayamanne/pythonVSCode/wiki/Python-Path-and-Version) to the python executable. - -If you're working in a [virtualenv](https://virtualenv.readthedocs.org/), you can reference the `python` interpreter from your virtualenv (ie `~/.virtualenvs/XXX/bin/python`). - -##Features -* Linting (Prospector, PyLint, Pep8, Flake8, pydocstyle with config files and plugins) -* Intellisense and autocompletion -* Auto indenting -* Code formatting (autopep8, yapf, with config files) -* Renaming, Viewing references, Going to definitions, Go to Symbols -* View signature and similar by hovering over a function or method -* Debugging with support for local variables, arguments, expressions, watch window, stack information, break points -* Debugging Multiple threads (Web Applications - Flask, etc) and expanding values (on Windows and Mac) -* Debugging remote processes (attaching to local and remote process) -* Debugging with support for shebang (windows) -* Debugging with custom environment variables -* Unit testing (unittests and nosetests, with config files) -* Sorting imports -* Snippets - -##[Issues and Feature Requests](https://github.com/DonJayamanne/pythonVSCode/issues) - -## Feature Details (with configuration) -* IDE Features - + Auto indenting - + Rename and navigate to symbols - + Go to, Peek and hover definition - + Find all references - + View Signature - + Sorting Import statements (use "Python: Sort Imports" command) -* [Intellisense and Autocomplete](https://github.com/DonJayamanne/pythonVSCode/wiki/Autocomplete-Intellisense) - + Full intellisense - + Support for docstring - + Ability to include custom module paths (e.g. include paths for libraries like Google App Engine, etc) - + Use the setting python.autoComplete.extraPaths = [] - + For instance getting autocomplete/intellisense for Google App Engine, add the following to your settings file: -```json -"python.autoComplete.extraPaths": [ - "C:/Program Files (x86)/Google/google_appengine", - "C:/Program Files (x86)/Google/google_appengine/lib" ] -``` -* [Code formatting](https://github.com/DonJayamanne/pythonVSCode/wiki/Formatting) - + Auto formatting of code upon saving changes (default to 'Off') - + Use either yapf or autopep8 for code formatting (defaults to autopep8) - + auutopep8 configuration files supported - + yapf configuration files supported -* [Linting](https://github.com/DonJayamanne/pythonVSCode/wiki/Linting) - + It can be turned off (default is turn it on and use pylint) - + pylint can be turned on/off (default is on), supports standard configuaration files - + pep8 can be turned on/off (default is off), supports standard configuaration files - + flake8 can be turned on/off (default is on), supports standard configuaration files - + pydocstyle can be turned on/off (default is on), supports standard configuaration files - + prospector can be turned on/off (default is on) - + Different categories of errors reported by pylint can be configured as warnings, errors, information or hits - + Path to pylint, pep8 and flake8 and pep8 can be configured - + Custom plugins such as pylint plugin for Django can be easily used by modifying the settings as follows: -```json -"python.linting.pylintPath": "pylint --load-plugins pylint_django" -``` -* [Debuggging](https://github.com/DonJayamanne/pythonVSCode/wiki/Debugging) - + Watch window - + Evaluate Expressions - + Step through code (Step in, Step out, Continue) - + Add/remove break points - + Local variables and arguments - + Multiple Threads and Web Applications (such as Flask) (Windows and Mac) - + Expanding values (viewing children, properties, etc) (Windows and Mac) - + Conditional breakpoints - + Remote debugging -* Unit Testing - + unittests (default is on) - + nosetests (default is off) - + Test resutls are displayed in the "Python" output window - + Future release will display results in a more structured manner integrated into the IDE -* Snippets - - -![Image of Generate Features](https://raw.githubusercontent.com/DonJayamanne/pythonVSCode/master/images/general.gif) - -![Image of Go To Definition](https://raw.githubusercontent.com/DonJayamanne/pythonVSCode/master/images/goToDef.gif) - -![Image of Renaming and Find all References](https://raw.githubusercontent.com/DonJayamanne/pythonVSCode/master/images/rename.gif) - -![Image of Debugging](https://raw.githubusercontent.com/DonJayamanne/pythonVSCode/master/images/standardDebugging.gif) - -![Image of Multi Threaded Debugging](https://raw.githubusercontent.com/DonJayamanne/pythonVSCode/master/images/flaskDebugging.gif) - -![Image of Pausing](https://raw.githubusercontent.com/DonJayamanne/pythonVSCode/master/images/break.gif) - -## Requirements -* Python is installed on the current system - + Path to python can be configured -* Pylint is installed for linting (optional) - + pip install pylint -* Pep8 is installed for linting (optional) - + pip install pep8 -* Flake8 is installed for linting (optional) - + pip install flake8 -* pydocstyle is installed for linting (optional) - + pip install pydocstyle -* prospector is installed for linting (optional) - + pip install prospector -* Autopep8 is installed for code formatting (optional) - + pip install pep8 - + pip install --upgrade autopep8 -* Yapf is installed for code formatting (optional) - + pip install yapf -* nosetests for unit testing (optional) - + pip install nose - -## Change Log - -### Version 0.3.9 -* Fixed auto indenting issues [#137](https://github.com/DonJayamanne/pythonVSCode/issues/137) - -### Version 0.3.8 -* Added support for linting using prospector [#130](https://github.com/DonJayamanne/pythonVSCode/pull/130) -* Fixed issue where environment variables weren't being inherited by the debugger [#109](https://github.com/DonJayamanne/pythonVSCode/issues/109) and [#77](https://github.com/DonJayamanne/pythonVSCode/issues/77) - -### Version 0.3.7 -* Added support for auto indenting of some keywords [#83](https://github.com/DonJayamanne/pythonVSCode/issues/83) -* Added support for launching console apps for Mac [#128](https://github.com/DonJayamanne/pythonVSCode/issues/128) -* Fixed issue where configuration files for pylint, pep8 and flake8 commands weren't being read correctly [#117](https://github.com/DonJayamanne/pythonVSCode/issues/117) - -### Version 0.3.6 -* Added support for linting using pydocstyle [#56](https://github.com/DonJayamanne/pythonVSCode/issues/56) -* Added support for auto-formatting documents upon saving (turned off by default) [#27](https://github.com/DonJayamanne/pythonVSCode/issues/27) -* Added support to configure the output window for linting, formatting and unit test messages [#112](https://github.com/DonJayamanne/pythonVSCode/issues/112) - -### Version 0.3.5 -* Fixed printing of unicode characters when evaulating expressions [#73](https://github.com/DonJayamanne/pythonVSCode/issues/73) - -### Version 0.3.4 -* Updated snippets -* Fixes to remote debugging [#65](https://github.com/DonJayamanne/pythonVSCode/issues/65) -* Fixes related to code navigation [#58](https://github.com/DonJayamanne/pythonVSCode/issues/58) and [#78](https://github.com/DonJayamanne/pythonVSCode/pull/78) -* Changes to allow code navigation for methods - -### Version 0.3.2 -* Ability to control how debugger breaks into exceptions raised (always break, never break or only break if unhandled) -* Disabled displaying of errors, as there are a few instances when errors are displayed in the IDE when not required - -### Version 0.3.1 -* Remote debugging (updated documentation and fixed minor issues) -* Fixed issues with formatting of files when path contains spaces - -### Version 0.3.0 -* Remote debugging (attaching to local and remote processes) -* Debugging with support for shebang -* Support for passing environment variables to debug program -* Improved error handling in the extension - -### Version 0.2.9 -* Added support for debugging django applications - + Debugging templates is not supported at this stage - -### Version 0.2.8 -* Added support for conditional break points -* Added ability to optionally display the shell window (Windows Only, Mac is coming soon) - + Allowing an interactive shell window, which isn't supported in VSCode. -* Added support for optionally breaking into python code as soon as debugger starts -* Fixed debugging when current thread is busy processing. -* Updated documentation with samples and instructions - -### Version 0.2.4 -* Fixed issue where debugger would break into all exceptions -* Added support for breaking on all and uncaught exceptions -* Added support for pausing (breaking) into a running program while debugging. - -### Version 0.2.3 -* Fixed termination of debugger - -### Version 0.2.2 -* Improved debugger for Mac, with support for Multi threading, Web Applications, expanding properties, etc -* (Debugging now works on both Windows and Mac) -* Debugging no longer uses PDB - -### Version 0.2.1 -* Improved debugger for Windows, with support for Multi threading, debugging Multi-threaded apps, Web Applications, expanding properties, etc -* Added support for relative paths for extra paths in additional libraries for Auto Complete -* Fixed a bug where paths to custom Python versions weren't respected by the previous (PDB) debugger -* NOTE: PDB Debugger is still supported - -### Version 0.1.3 -* Fixed linting when using pylint - -### Version 0.1.2 -* Fixed autoformatting of code (falling over when using yapf8) - -### Version 0.1.1 -* Added support for linting using flake8 -* Added support for unit testing using unittest and nosetest -* Added support for custom module paths for improved intellisense and autocomplete -* Modifications to debugger to display console output (generated using 'print' and the like) -* Modifications to debugger to accept arguments - -### Version 0.1.0 -* Fixed linting of files on Mac -* Added support for linting using pep8 -* Added configuration support for pep8 and pylint -* Added support for configuring paths for pep8, pylint and autopep8 -* Added snippets -* Added support for formatting using yapf -* Added a number of configuration settings - -### Version 0.0.4 -* Added support for linting using Pylint (configuring pylint is coming soon) -* Added support for sorting Imports (Using the command "Pythong: Sort Imports") -* Added support for code formatting using Autopep8 (configuring autopep8 is coming soon) -* Added ability to view global variables, arguments, add and remove break points - -### Version 0.0.3 -* Added support for debugging using PDB - - -## Debugging Instructions -* Use the Python debugger, set the name of the startup program - - -## Source - -[Github](https://github.com/DonJayamanne/pythonVSCode) - - -## License - -[MIT](https://raw.githubusercontent.com/DonJayamanne/pythonVSCode/master/LICENSE) +# Python extension for Visual Studio Code + +A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (for all [actively supported versions](https://devguide.python.org/#status-of-python-branches) of the language: 2.7, >=3.6), including features such as IntelliSense, linting, debugging, code navigation, code formatting, Jupyter notebook support, refactoring, variable explorer, test explorer, snippets, and more! + +## Quick start + +- **Step 1.** [Install a supported version of Python on your system](https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites) (note: that the system install of Python on macOS is not supported). +- **Step 2.** Install the Python extension for Visual Studio Code. +- **Step 3.** Open or create a Python file and start coding! + +## Set up your environment + + + +- Select your Python interpreter by clicking on the status bar + + + +- Configure the debugger through the Debug Activity Bar + + + +- Configure tests by running the `Configure Tests` command + + + +## Jupyter Notebook quick start + +- Open or create a Jupyter Notebook file (.ipynb) and start coding in our Notebook Editor! + + + +For more information you can: + +- [Follow our Python tutorial](https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites) with step-by-step instructions for building a simple app. +- Check out the [Python documentation on the VS Code site](https://code.visualstudio.com/docs/languages/python) for general information about using the extension. +- Check out the [Jupyter Notebook documentation on the VS Code site](https://code.visualstudio.com/docs/python/jupyter-support) for information about using Jupyter Notebooks in VS Code. + +## Useful commands + +Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/Linux) and type in one of the following commands: + +| Command | Description | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Python: Select Interpreter` | Switch between Python interpreters, versions, and environments. | +| `Python: Start REPL` | Start an interactive Python REPL using the selected interpreter in the VS Code terminal. | +| `Python: Run Python File in Terminal` | Runs the active Python file in the VS Code terminal. You can also run a Python file by right-clicking on the file and selecting `Run Python File in Terminal`. | +| `Python: Select Linter` | Switch from Pylint to Flake8 or other supported linters. | +| `Format Document` | Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/editing#_formatting) in the `settings.json` file. | +| `Python: Configure Tests` | Select a test framework and configure it to display the Test Explorer. | + +To see all available Python commands, open the Command Palette and type `Python`. + +## Feature details + +Learn more about the rich features of the Python extension: + +- [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense): Edit your code with auto-completion, code navigation, syntax checking and more +- [Linting](https://code.visualstudio.com/docs/python/linting): Get additional code analysis with Pylint, Flake8 and more +- [Code formatting](https://code.visualstudio.com/docs/python/editing#_formatting): Format your code with black, autopep or yapf + +- [Debugging](https://code.visualstudio.com/docs/python/debugging): Debug your Python scripts, web apps, remote or multi-threaded processes + +- [Testing](https://code.visualstudio.com/docs/python/unit-testing): Run and debug tests through the Test Explorer with unittest, pytest or nose + +- [Jupyter Notebooks](https://code.visualstudio.com/docs/python/jupyter-support): Create and edit Jupyter Notebooks, add and run code cells, render plots, visualize variables through the variable explorer, visualize dataframes with the data viewer, and more + +- [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments + +- [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction, method extraction and import sorting + +## Supported locales + +The extension is available in multiple languages: `de`, `en`, `es`, `fa`, `fr`, `it`, `ja`, `ko-kr`, `nl`, `pl`, `pt-br`, `ru`, `tr`, `zh-cn`, `zh-tw` + +## Questions, issues, feature requests, and contributions + +- If you have a question about how to accomplish something with the extension, please [ask on Stack Overflow](https://stackoverflow.com/questions/tagged/visual-studio-code+python) +- If you come across a problem with the extension, please [file an issue](https://github.com/microsoft/vscode-python) +- Contributions are always welcome! Please see our [contributing guide](https://github.com/Microsoft/vscode-python/blob/main/CONTRIBUTING.md) for more details +- Any and all feedback is appreciated and welcome! + - If someone has already [filed an issue](https://github.com/Microsoft/vscode-python) that encompasses your feedback, please leave a 👍/👎 reaction on the issue + - Otherwise please file a new issue +- If you're interested in the development of the extension, you can read about our [development process](https://github.com/Microsoft/vscode-python/blob/main/CONTRIBUTING.md#development-process) + +## Data and telemetry + +The Microsoft Python Extension for Visual Studio Code collects usage +data and sends it to Microsoft to help improve our products and +services. Read our +[privacy statement](https://privacy.microsoft.com/privacystatement) to +learn more. This extension respects the `telemetry.enableTelemetry` +setting which you can learn more about at +https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..1ceb287afafa --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability]() of a security vulnerability, please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + +- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + diff --git a/ThirdPartyNotices-Distribution.txt b/ThirdPartyNotices-Distribution.txt new file mode 100644 index 000000000000..a9048af0d757 --- /dev/null +++ b/ThirdPartyNotices-Distribution.txt @@ -0,0 +1,45875 @@ +NOTICES AND INFORMATION +Do Not Translate or Localize + +This software incorporates material from third parties. +Microsoft makes certain open source code available at https://3rdpartysource.microsoft.com, +or you may send a check or money order for US $5.00, including the product name, +the open source component name, platform, and version number, to: + +Source Code Compliance Team +Microsoft Corporation +One Microsoft Way +Redmond, WA 98052 +USA + +Notwithstanding any other terms, you may reverse engineer this software to the extent +required to debug changes to any libraries licensed under the GNU Lesser General Public License. + + +------------------------------------------------------------------- + +@blueprintjs/core 3.22.3 - Apache-2.0 +https://github.com/palantir/blueprint#readme +Copyright 2015 Palantir Technologies, Inc. +Copyright 2016 Palantir Technologies, Inc. +Copyright 2017 Palantir Technologies, Inc. +Copyright 2018 Palantir Technologies, Inc. +Copyright 2019 Palantir Technologies, Inc. +Copyright 2015-present Palantir Technologies, Inc. +Copyright 2019-present Palantir Technologies, Inc. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@blueprintjs/icons 3.13.0 - Apache-2.0 +https://github.com/palantir/blueprint#readme +Copyright 2015 Palantir Technologies, Inc. +Copyright 2017 Palantir Technologies, Inc. +Copyright 2018 Palantir Technologies, Inc. +Copyright 2017-present Palantir Technologies, Inc. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@blueprintjs/select 3.11.2 - Apache-2.0 +https://github.com/palantir/blueprint#readme +Copyright 2017 Palantir Technologies, Inc. +Copyright 2018 Palantir Technologies, Inc. +Copyright 2017-present Palantir Technologies, Inc. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +acorn-node 1.7.0 - Apache-2.0 +https://github.com/browserify/acorn-node +Copyright (c) 2016 Jordan Gensler +Copyright (c) 2017-2018 by Adrian Heine +Copyright 2018 Renee Kooi + +# [Apache License 2.0](https://spdx.org/licenses/Apache-2.0) + +Copyright 2018 Renée Kooi + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +> http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +## acorn-bigint + +The code in the `lib/bigint` folder is compiled from code licensed as MIT: + +> Copyright (C) 2017-2018 by Adrian Heine +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. + +Find the source code at https://github.com/acornjs/acorn-bigint. + +## acorn-import-meta + +The code in the `lib/import-meta` folder is compiled from code licensed as MIT: + +> Copyright (C) 2017-2018 by Adrian Heine +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. + +Find the source code at https://github.com/acornjs/acorn-import-meta. + +## acorn-dynamic-import + +The code in the `lib/dynamic-import` folder is licensed as MIT: + +> MIT License +> +> Copyright (c) 2016 Jordan Gensler +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all +> copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +> SOFTWARE. + +Find the source code at https://github.com/kesne/acorn-dynamic-import. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +aria-query 3.0.0 - Apache-2.0 +https://github.com/A11yance/aria-query#readme + +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by +the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all +other entities that control, are controlled by, or are under common +control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or +otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity +exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation +source, and configuration files. + +"Object" form shall mean any form resulting from mechanical +transformation or translation of a Source form, including but +not limited to compiled object code, generated documentation, +and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or +Object form, made available under the License, as indicated by a +copyright notice that is included in or attached to the work +(an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object +form, that is based on (or derived from) the Work and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. For the purposes +of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, +the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including +the original version of the Work and any modifications or additions +to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner +or by an individual or Legal Entity authorized to submit on behalf of +the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, +and issue tracking systems that are managed by, or on behalf of, the +Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity +on behalf of whom a Contribution has been received by Licensor and +subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the +Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of +this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable +(except as stated in this section) patent license to make, have made, +use, offer to sell, sell, import, and otherwise transfer the Work, +where such license applies only to those patent claims licensable +by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You +institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work +or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the +Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You +meet the following conditions: + +(a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices +stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works +that You distribute, all copyright, patent, trademark, and +attribution notices from the Source form of the Work, +excluding those notices that do not pertain to any part of +the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its +distribution, then any Derivative Works that You distribute must +include a readable copy of the attribution notices contained +within such NOTICE file, excluding those notices that do not +pertain to any part of the Derivative Works, in at least one +of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, +within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and +do not modify the License. You may add Your own attribution +notices within Derivative Works that You distribute, alongside +or as an addendum to the NOTICE text from the Work, provided +that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and +may provide additional or different license terms and conditions +for use, reproduction, or distribution of Your modifications, or +for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with +the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work +by You to the Licensor shall be under the terms and conditions of +this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify +the terms of any separate license agreement you may have executed +with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, +except as required for reasonable and customary use in describing the +origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or +agreed to in writing, Licensor provides the Work (and each +Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied, including, without limitation, any warranties or conditions +of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, +unless required by applicable law (such as deliberate and grossly +negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, +incidental, or consequential damages of any character arising as a +result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses), even if such Contributor +has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, +and charge a fee for, acceptance of support, warranty, indemnity, +or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only +on Your own behalf and on Your sole responsibility, not on behalf +of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following +boilerplate notice, with the fields enclosed by brackets "{}" +replaced with your own identifying information. (Don't include +the brackets!) The text should be enclosed in the appropriate +comment syntax for the file format. We also recommend that a +file or class name and description of purpose be included on the +same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright {yyyy} {name of copyright owner} + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +aws-sign2 0.7.0 - Apache-2.0 +https://github.com/mikeal/aws-sign#readme +Copyright 2010 LearnBoost + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +azure-storage 2.10.3 - Apache-2.0 +http://github.com/Azure/azure-storage-node +Copyright (c) Microsoft and contributors. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +caseless 0.12.0 - Apache-2.0 +https://github.com/mikeal/caseless#readme + +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +1. Definitions. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. +END OF TERMS AND CONDITIONS + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +detect-libc 1.0.3 - Apache-2.0 +https://github.com/lovell/detect-libc#readme +Copyright 2017 Lovell Fuller + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +diff-match-patch 1.0.4 - Apache-2.0 +https://github.com/JackuB/diff-match-patch#readme +Copyright 2018 + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ejs 2.7.4 - Apache-2.0 +https://github.com/mde/ejs +Copyright Joyent, Inc. and other Node contributors. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +estree-is-function 1.0.0 - Apache-2.0 +https://github.com/goto-bus-stop/estree-is-function +Copyright 2017 Renee Kooi + +# [Apache License 2.0](https://spdx.org/licenses/Apache-2.0) + +Copyright 2017 Renée Kooi + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +> http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fast-diff 1.2.0 - Apache-2.0 +https://github.com/jhchen/fast-diff#readme +Copyright 2006 Google Inc. http://code.google.com/p/google-diff-match-patch + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + + + "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + + + + "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + + + + "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + + + "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + + + + "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + + + + "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + + + + "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + + + + "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + + + + "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + + + + "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); + +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software + +distributed under the License is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and + +limitations under the License. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +forever-agent 0.6.1 - Apache-2.0 +https://github.com/mikeal/forever-agent + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +get-assigned-identifiers 1.2.0 - Apache-2.0 +https://github.com/goto-bus-stop/get-assigned-identifiers +Copyright 2017 Renee Kooi + +# [Apache License 2.0](https://spdx.org/licenses/Apache-2.0) + +Copyright 2017 Renée Kooi + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +> http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +less-plugin-inline-urls 1.2.0 - Apache-2.0 +http://lesscss.org + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +log4js 6.1.2 - Apache-2.0 +https://log4js-node.github.io/log4js-node/ +Copyright 2015 Gareth Jones + +Copyright 2015 Gareth Jones (with contributions from many other people) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +oauth-sign 0.9.0 - Apache-2.0 +https://github.com/mikeal/oauth-sign#readme + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +reflect-metadata 0.1.13 - Apache-2.0 +http://rbuckton.github.io/reflect-metadata +Copyright (c) Microsoft. +Copyright (c) 2016 Brian Terlson +Copyright (c) 2015 Nicolas Bevacqua +Copyright (c) Microsoft Corporation. + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +request 2.88.0 - Apache-2.0 +https://github.com/request/request#readme +Copyright 2010-2012 Mikeal Rogers + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +rxjs 6.5.4 - Apache-2.0 +https://github.com/ReactiveX/RxJS +Copyright Google Inc. +Copyright (c) Microsoft Corporation. +Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +rxjs-compat 6.5.4 - Apache-2.0 +(c) this.destination.next +(c) this.destination.error +Copyright (c) Microsoft Corporation. +Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +scope-analyzer 2.0.5 - Apache-2.0 +https://github.com/goto-bus-stop/scope-analyzer +Copyright 2017 Renee Kooi + +# [Apache License 2.0](https://spdx.org/licenses/Apache-2.0) + +Copyright 2017 Renée Kooi + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +> http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +spdx-correct 3.1.0 - Apache-2.0 +https://github.com/jslicense/spdx-correct.js#readme + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tslib 1.10.0 - Apache-2.0 +http://typescriptlang.org/ +Copyright (c) Microsoft Corporation. + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tslib 1.9.3 - Apache-2.0 +http://typescriptlang.org/ +Copyright (c) Microsoft Corporation. + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tslib 1.9.0 - Apache-2.0 +http://typescriptlang.org/ +Copyright (c) Microsoft Corporation. + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tslint 5.20.1 - Apache-2.0 +https://palantir.github.io/tslint +Copyright 2018 +Copyright 2013 Palantir Technologies, Inc. +Copyright 2014 Palantir Technologies, Inc. +Copyright 2015 Palantir Technologies, Inc. +Copyright 2016 Palantir Technologies, Inc. +Copyright 2017 Palantir Technologies, Inc. +Copyright 2018 Palantir Technologies, Inc. +Copyright 2019 Palantir Technologies, Inc. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tunnel-agent 0.6.0 - Apache-2.0 +https://github.com/mikeal/tunnel-agent#readme + +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and + +You must cause any modified files to carry prominent notices stating that You changed the files; and + +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +xml-name-validator 3.0.0 - Apache-2.0 +https://github.com/jsdom/xml-name-validator#readme + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +browser-process-hrtime 0.1.3 - BSD-2-Clause +https://github.com/kumavis/browser-process-hrtime#readme +Copyright 2014 + +Copyright 2014 kumavis + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +css-select 1.2.0 - BSD-2-Clause +https://github.com/fb55/css-select#readme +Copyright (c) Felix Bohm + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +css-what 2.1.3 - BSD-2-Clause +https://github.com/fb55/css-what#readme +Copyright (c) Felix Bohm + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +domelementtype 2.0.1 - BSD-2-Clause +https://github.com/fb55/domelementtype#readme +Copyright (c) Felix Bohm + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +domelementtype 1.3.1 - BSD-2-Clause +https://github.com/fb55/domelementtype#readme +Copyright (c) Felix Bohm + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +domhandler 3.0.0 - BSD-2-Clause +https://github.com/fb55/DomHandler#readme +Copyright (c) Felix Bohm + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +domhandler 2.4.2 - BSD-2-Clause +https://github.com/fb55/DomHandler#readme +Copyright (c) Felix Bohm + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +domutils 1.5.1 - BSD-2-Clause +https://github.com/FB55/domutils +Copyright (c) Felix Bohm + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +domutils 2.1.0 - BSD-2-Clause +https://github.com/fb55/domutils#readme +Copyright (c) Felix Bohm + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +domutils 1.7.0 - BSD-2-Clause +https://github.com/FB55/domutils#readme +Copyright (c) Felix Bohm + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +entities 1.1.2 - BSD-2-Clause +https://github.com/fb55/entities#readme +Copyright (c) Felix Bohm +(c) // http://mathiasbynens.be/notes/javascript-encoding + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +entities 2.0.3 - BSD-2-Clause +https://github.com/fb55/entities#readme +Copyright (c) Felix Bohm + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +escodegen 1.2.0 - BSD-2-Clause +http://github.com/Constellation/escodegen +Copyright (c) 2012 Kris Kowal +Copyright (c) 2012 John Freeman +Copyright (c) 2012 Yusuke Suzuki +Copyright (c) 2012-2013 Mathias Bynens +Copyright (c) 2014 Yusuke Suzuki +Copyright (c) 2013 Irakli Gozalishvili +Copyright (c) 2009-2011, Mozilla Foundation and contributors +Copyright (c) 2012 Arpad Borsos +Copyright (c) 2012-2013 Yusuke Suzuki +Copyright (c) 2011-2012 Ariya Hidayat +Copyright (c) 2012 Yusuke Suzuki (http://github.com/Constellation) +Copyright (c) 2012 Joost-Wim Boekesteijn +Copyright (c) 2012 Robert Gust-Bardon +Copyright (c) 2012-2013 Michael Ficarra + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +escodegen 1.11.1 - BSD-2-Clause +http://github.com/estools/escodegen +Copyright (c) 2014 Ivan Nikulin +Copyright (c) 2012 Kris Kowal +Copyright (c) 2012 John Freeman +Copyright (c) 2015 Ingvar Stepanyan +Copyright (c) 2012 Yusuke Suzuki +Copyright (c) 2012-2013 Mathias Bynens +Copyright (c) 2013 Irakli Gozalishvili +Copyright (c) 2012 Arpad Borsos +Copyright (c) 2012-2014 Yusuke Suzuki +Copyright (c) 2011-2012 Ariya Hidayat +Copyright (c) 2012 Yusuke Suzuki (http://github.com/Constellation) +Copyright (c) 2012 Joost-Wim Boekesteijn +Copyright (c) 2012 Robert Gust-Bardon +Copyright (c) 2012-2013 Michael Ficarra + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +escodegen 1.9.1 - BSD-2-Clause +http://github.com/estools/escodegen +Copyright (c) 2014 Ivan Nikulin +Copyright (c) 2012 Kris Kowal +Copyright (c) 2012 John Freeman +Copyright (c) 2015 Ingvar Stepanyan +Copyright (c) 2012 Yusuke Suzuki +Copyright (c) 2012-2013 Mathias Bynens +Copyright (c) 2013 Irakli Gozalishvili +Copyright (c) 2012 Arpad Borsos +Copyright (c) 2012-2014 Yusuke Suzuki +Copyright (c) 2011-2012 Ariya Hidayat +Copyright (c) 2012 Yusuke Suzuki (http://github.com/Constellation) +Copyright (c) 2012 Joost-Wim Boekesteijn +Copyright (c) 2012 Robert Gust-Bardon +Copyright (c) 2012-2013 Michael Ficarra + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +esprima 4.0.1 - BSD-2-Clause +http://esprima.org +Copyright JS Foundation and other contributors, https://js.foundation + +Copyright JS Foundation and other contributors, https://js.foundation/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +esprima 3.1.3 - BSD-2-Clause +http://esprima.org +Copyright JS Foundation and other contributors, https://js.foundation + +Copyright JS Foundation and other contributors, https://js.foundation/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +esprima 1.0.4 - BSD-2-Clause +http://esprima.org +Copyright (c) 2012 Mathias Bynens +Copyright (c) 2012 Kris Kowal +Copyright (c) 2011 Yusuke Suzuki +Copyright (c) 2012 Yusuke Suzuki +Copyright (c) 2011 Ariya Hidayat +Copyright (c) 2012 Ariya Hidayat +Copyright (c) 2011 Arpad Borsos +Copyright (c) 2012 Arpad Borsos +Copyright (c) 2012 Joost-Wim Boekesteijn +Copyright (c) 2012, 2011 Ariya Hidayat (http://ariya.ofilabs.com/about) and other contributors. + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +estraverse 1.5.1 - BSD-2-Clause +https://github.com/Constellation/estraverse +Copyright (c) 2012 Ariya Hidayat +Copyright (c) 2012-2013 Yusuke Suzuki +Copyright (c) 2012-2013 Yusuke Suzuki (http://github.com/Constellation) + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +estraverse 4.3.0 - BSD-2-Clause +https://github.com/estools/estraverse +Copyright (c) 2014 Yusuke Suzuki +Copyright (c) 2012 Ariya Hidayat +Copyright (c) 2012-2013 Yusuke Suzuki +Copyright (c) 2012-2016 Yusuke Suzuki (http://github.com/Constellation) + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +esutils 1.1.6 - BSD-2-Clause +https://github.com/Constellation/esutils +Copyright (c) 2014 Ivan Nikulin +Copyright (c) 2013 Yusuke Suzuki +Copyright (c) 2013-2014 Yusuke Suzuki +Copyright (c) 2013 Yusuke Suzuki (http://github.com/Constellation) + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +esutils 1.0.0 - BSD-2-Clause +https://github.com/Constellation/esutils +Copyright (c) 2013 Yusuke Suzuki +Copyright (c) 2013 Yusuke Suzuki (http://github.com/Constellation) + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +esutils 2.0.3 - BSD-2-Clause +https://github.com/estools/esutils +Copyright (c) 2014 Ivan Nikulin +Copyright (c) 2013 Yusuke Suzuki +Copyright (c) 2013-2014 Yusuke Suzuki +Copyright (c) 2013 Yusuke Suzuki (http://github.com/Constellation) + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +extract-zip 1.7.0 - BSD-2-Clause +https://github.com/maxogden/extract-zip#readme +Copyright (c) 2014 Max Ogden and other contributors + +Copyright (c) 2014 Max Ogden and other contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +http-cache-semantics 4.1.0 - BSD-2-Clause +https://github.com/kornelski/http-cache-semantics#readme +Copyright 2016-2018 Kornel Lesinski + +Copyright 2016-2018 Kornel Lesiński + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +http-cache-semantics 3.8.1 - BSD-2-Clause +https://github.com/pornel/http-cache-semantics#readme + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +normalize-package-data 2.5.0 - BSD-2-Clause +https://github.com/npm/normalize-package-data#readme +Copyright (c) Meryn Stol +Copyright (c) 2013 Meryn Stol + +This package contains code originally written by Isaac Z. Schlueter. +Used with permission. + +Copyright (c) Meryn Stol ("Author") +All rights reserved. + +The BSD License + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +nth-check 1.0.2 - BSD-2-Clause +https://github.com/fb55/nth-check +Copyright (c) Felix Bohm + +Copyright (c) Felix Böhm +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +onecolor 3.1.0 - BSD-2-Clause +https://github.com/One-com/one-color#readme +Copyright (c) 2011, One.com + +Copyright (c) 2011, One.com +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Neither the name of the author nor the names of contributors may + be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +sax 0.5.8 - BSD-2-Clause +https://github.com/isaacs/sax-js +Copyright (c) Isaac Z. Schlueter +copyright-software-short-notice-20021231.html' W3C Software Short +copyright' Copyright 2012 W3C http://www.csail.mit.edu/' title Massachusetts Institute of Technology' MIT , http://www.ercim.org/' + +Copyright (c) Isaac Z. Schlueter ("Author") +All rights reserved. + +The BSD License + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +The file "examples/strict.dtd" is licensed by the W3C and used according +to the terms of the W3C SOFTWARE NOTICE AND LICENSE. See LICENSE-W3C.html +for details. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +terser 4.6.3 - BSD-2-Clause +https://terser.org +Copyright 2012 (c) Mihai Bazon +Copyright 2012-2018 (c) Mihai Bazon + +UglifyJS is released under the BSD license: + +Copyright 2012-2018 (c) Mihai Bazon + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above + copyright notice, this list of conditions and the following + disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +uri-js 4.2.2 - BSD-2-Clause +https://github.com/garycourt/uri-js +(c) 2011 Gary Court. +Copyright 2011 Gary Court. +Copyright (c) 2008 Ariel Flesler +Copyright (c) 2009 John Resig, Jorn Zaefferer + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +winreg 1.2.4 - BSD-2-Clause +http://fresc81.github.io/node-winreg +Copyright (c) 2016 Paul +Copyright (c) 2016, Paul Bottin + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyter-widgets/base 2.0.2 - BSD-3-Clause +https://github.com/jupyter-widgets/ipywidgets#readme +Copyright (c) 2014 Adam Krebs +(c) 2015 Adam Krebs, Jimmy Yuen Ho Wong +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors +Copyright (c) 2010-2015 Jeremy Ashkenas, DocumentCloud +(c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors Backbone + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyter-widgets/controls 1.5.3 - BSD-3-Clause +https://github.com/jupyter-widgets/ipywidgets#readme +Copyright (c) 2014 Dan Le +Copyright (c) 2014-2017, PhosphorJS +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyter-widgets/jupyterlab-manager 1.1.0 - BSD-3-Clause +https://github.com/jupyter-widgets/ipywidgets +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyter-widgets/output 2.0.1 - BSD-3-Clause +https://github.com/jupyter-widgets/ipywidgets#readme +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyter-widgets/schema 0.4.0 - BSD-3-Clause +https://github.com/jupyter-widgets/ipywidgets#readme +Copyright (c) Jupyter Development Team. + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/application 1.2.2 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/apputils 1.2.2 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2017, Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors +Copyright (c) 2014-2016, Jupyter Development Team. +Copyright (c) 2014-2017, Jupyter Development Team. + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/attachments 1.2.2 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/cells 1.2.3 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/codeeditor 1.2.0 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors +Copyright (c) 2014-2016, Jupyter Development Team. + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/codemirror 1.2.2 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/coreutils 3.1.0 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/coreutils 3.2.0 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/docmanager 1.2.2 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/docregistry 1.2.2 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/filebrowser 1.2.2 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/logconsole 1.0.3 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/mainmenu 1.2.2 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/notebook 1.2.3 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/observables 2.4.0 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/outputarea 1.2.3 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/rendermime 1.2.2 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/rendermime-interfaces 1.5.0 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/services 4.2.0 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +(c) 2011 Gary Court. +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/statusbar 1.2.2 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jupyterlab/ui-components 1.2.1 - BSD-3-Clause +https://github.com/jupyterlab/jupyterlab +Copyright (c) Jupyter Development Team. +Copyright (c) 2015 Project Jupyter Contributors + +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@nteract/commutable 7.2.9 - BSD-3-Clause +Copyright (c) 2016, nteract contributors + +Copyright (c) 2016, nteract contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of nteract nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@nteract/messaging 7.0.4 - BSD-3-Clause +Copyright (c) 2016, nteract contributors + +Copyright (c) 2016, nteract contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of nteract nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@nteract/transform-plotly 6.0.0 - BSD-3-Clause +Copyright (c) 2016, nteract contributors + +Copyright (c) 2016, nteract contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of nteract nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@nteract/transform-vega 6.0.3 - BSD-3-Clause +Copyright (c) 2016, nteract contributors + +Copyright (c) 2016, nteract contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of nteract nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/algorithm 1.1.3 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/algorithm 1.2.0 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/application 1.7.3 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/collections 1.2.0 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS +Copyright (c) 2014-2018, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/commands 1.7.2 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/coreutils 1.3.1 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/disposable 1.3.1 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/disposable 1.2.0 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/domutils 1.1.4 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS +Copyright (c) 2014-2019, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/dragdrop 1.4.1 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/keyboard 1.1.3 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/messaging 1.3.0 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/properties 1.1.3 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/signaling 1.3.1 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/signaling 1.2.3 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/virtualdom 1.2.0 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@phosphor/widgets 1.9.3 - BSD-3-Clause +https://github.com/phosphorjs/phosphor +Copyright (c) 2014-2017, PhosphorJS + +Copyright (c) 2014-2017, PhosphorJS Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@sinonjs/commons 1.7.0 - BSD-3-Clause +https://github.com/sinonjs/commons#readme +Copyright (c) 2018 + +BSD 3-Clause License + +Copyright (c) 2018, Sinon.JS +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@sinonjs/formatio 4.0.1 - BSD-3-Clause +https://sinonjs.github.io/formatio/ +Copyright (c) 2010-2012, Christian Johansen (christian@cjohansen.no) and August Lilleaas (august.lilleaas@gmail.com). + +(The BSD License) + +Copyright (c) 2010-2012, Christian Johansen (christian@cjohansen.no) and +August Lilleaas (august.lilleaas@gmail.com). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Christian Johansen nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@sinonjs/samsam 4.2.0 - BSD-3-Clause +http://sinonjs.github.io/samsam/ +Copyright (c) 2010-2012, Christian Johansen, christian@cjohansen.no and August Lilleaas, august.lilleaas@gmail.com. + +(The BSD License) + +Copyright (c) 2010-2012, Christian Johansen, christian@cjohansen.no and +August Lilleaas, august.lilleaas@gmail.com. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Christian Johansen nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +bcrypt-pbkdf 1.0.2 - BSD-3-Clause +https://github.com/joyent/node-bcrypt-pbkdf#readme +Copyright 2016, Joyent Inc +Copyright (c) 2013 Ted Unangst +Copyright 1997 Niels Provos + +The Blowfish portions are under the following license: + +Blowfish block cipher for OpenBSD +Copyright 1997 Niels Provos +All rights reserved. + +Implementation advice by David Mazieres . + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +The bcrypt_pbkdf portions are under the following license: + +Copyright (c) 2013 Ted Unangst + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +Performance improvements (Javascript-specific): + +Copyright 2016, Joyent Inc +Author: Alex Wilson + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +charenc 0.0.2 - BSD-3-Clause +https://github.com/pvorb/node-charenc#readme +Copyright (c) 2009, Jeff Mott. +Copyright (c) 2011, Paul Vorbach. + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +crypt 0.0.2 - BSD-3-Clause +https://github.com/pvorb/node-crypt#readme +Copyright (c) 2009, Jeff Mott. +Copyright (c) 2011, Paul Vorbach. + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3 3.5.17 - BSD-3-Clause +http://d3js.org +Copyright (c) 2010-2016, Michael Bostock + +Copyright (c) 2010-2016, Michael Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name Michael Bostock may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-array 1.2.4 - BSD-3-Clause +https://d3js.org/d3-array/ +Copyright 2018 Mike Bostock +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-array 2.3.3 - BSD-3-Clause +https://d3js.org/d3-array/ +Copyright 2019 Mike Bostock +Copyright 2010-2018 Mike Bostock +Copyright 2018 Vladimir Agafonkin. + +Copyright 2010-2018 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-cloud 1.2.5 - BSD-3-Clause +https://www.jasondavies.com/wordcloud/ +Copyright 2017 Mike Bostock. +Copyright (c) 2013, Jason Davies. + +Copyright (c) 2013, Jason Davies. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * The name Jason Davies may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL JASON DAVIES BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-collection 1.0.7 - BSD-3-Clause +https://d3js.org/d3-collection/ +Copyright 2018 Mike Bostock +Copyright 2010-2016, Mike Bostock + +Copyright 2010-2016, Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-color 1.4.0 - BSD-3-Clause +https://d3js.org/d3-color/ +Copyright 2019 Mike Bostock +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-color 1.2.8 - BSD-3-Clause +https://d3js.org/d3-color/ +Copyright 2019 Mike Bostock +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-contour 1.3.2 - BSD-3-Clause +https://d3js.org/d3-contour/ +Copyright 2018 Mike Bostock +Copyright 2012-2017 Mike Bostock + +Copyright 2012-2017 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-dispatch 1.0.5 - BSD-3-Clause +https://d3js.org/d3-dispatch/ +Copyright 2018 Mike Bostock +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-dsv 1.1.1 - BSD-3-Clause +https://d3js.org/d3-dsv/ +Copyright 2019 Mike Bostock +Copyright 2013-2016 Mike Bostock + +Copyright 2013-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-dsv 0.1.14 - BSD-3-Clause +https://github.com/d3/d3-dsv +Copyright 2013-2015 Mike Bostock + +Copyright 2013-2015 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-ease 1.0.5 - BSD-3-Clause +https://d3js.org/d3-ease/ +Copyright 2018 Mike Bostock +Copyright 2001 Robert Penner +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +Copyright 2001 Robert Penner +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-force 2.0.1 - BSD-3-Clause +https://d3js.org/d3-force/ +Copyright 2019 Mike Bostock +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-format 0.4.2 - BSD-3-Clause +https://github.com/d3/d3-format +Copyright 2010-2015 Mike Bostock + +Copyright 2010-2015 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-format 1.3.2 - BSD-3-Clause +https://d3js.org/d3-format/ +Copyright 2018 Mike Bostock +Copyright 2010-2015 Mike Bostock + +Copyright 2010-2015 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-format 1.4.1 - BSD-3-Clause +https://d3js.org/d3-format/ +Copyright 2019 Mike Bostock +Copyright 2010-2015 Mike Bostock + +Copyright 2010-2015 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-geo 1.11.6 - BSD-3-Clause +https://d3js.org/d3-geo/ +(c) , cc cos +Copyright 2019 Mike Bostock +Copyright 2010-2016 Mike Bostock +Copyright (c) 2008-2012, Charles Karney + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This license applies to GeographicLib, versions 1.12 and later. + +Copyright (c) 2008-2012, Charles Karney + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-hierarchy 1.1.8 - BSD-3-Clause +https://d3js.org/d3-hierarchy/ +Copyright 2018 Mike Bostock +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-interpolate 1.3.2 - BSD-3-Clause +https://d3js.org/d3-interpolate/ +Copyright 2018 Mike Bostock +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-path 1.0.7 - BSD-3-Clause +https://d3js.org/d3-path/ +Copyright 2018 Mike Bostock +Copyright 2015-2016 Mike Bostock + +Copyright 2015-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-path 1.0.8 - BSD-3-Clause +https://d3js.org/d3-path/ +Copyright 2019 Mike Bostock +Copyright 2015-2016 Mike Bostock + +Copyright 2015-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-quadtree 1.0.1 - BSD-3-Clause +https://d3js.org/d3-quadtree/ +Copyright 2016 Mike Bostock. +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-request 1.0.6 - BSD-3-Clause +https://d3js.org/d3-request/ +Copyright 2017 Mike Bostock. +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-scale 3.2.0 - BSD-3-Clause +https://d3js.org/d3-scale/ +Copyright 2019 Mike Bostock +Copyright 2010-2015 Mike Bostock + +Copyright 2010-2015 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-scale-chromatic 1.5.0 - BSD-3-Clause +https://d3js.org/d3-scale-chromatic/ +Copyright 2019 Mike Bostock +Copyright 2010-2018 Mike Bostock +Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State University. + +Copyright 2010-2018 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Apache-Style Software License for ColorBrewer software and ColorBrewer Color +Schemes + +Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State +University. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-selection 1.4.0 - BSD-3-Clause +https://d3js.org/d3-selection/ +Copyright 2019 Mike Bostock +Copyright (c) 2010-2018, Michael Bostock + +Copyright (c) 2010-2018, Michael Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name Michael Bostock may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-shape 1.3.5 - BSD-3-Clause +https://d3js.org/d3-shape/ +Copyright 2019 Mike Bostock +Copyright 2010-2015 Mike Bostock + +Copyright 2010-2015 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-time 1.0.11 - BSD-3-Clause +https://d3js.org/d3-time/ +Copyright 2019 Mike Bostock +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-time 1.1.0 - BSD-3-Clause +https://d3js.org/d3-time/ +Copyright 2019 Mike Bostock +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-time 0.1.1 - BSD-3-Clause +https://github.com/d3/d3-time +Copyright 2010-2015 Mike Bostock + +Copyright 2010-2015 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-time-format 2.1.3 - BSD-3-Clause +https://d3js.org/d3-time-format/ +Copyright 2018 Mike Bostock +Copyright 2010-2017 Mike Bostock + +Copyright 2010-2017 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-time-format 0.2.1 - BSD-3-Clause +https://github.com/d3/d3-time-format +Copyright 2010-2015 Mike Bostock + +Copyright 2010-2015 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-timer 1.0.9 - BSD-3-Clause +https://d3js.org/d3-timer/ +Copyright 2018 Mike Bostock +Copyright 2010-2016 Mike Bostock + +Copyright 2010-2016 Mike Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +datalib 1.9.2 - BSD-3-Clause +https://github.com/vega/datalib#readme +Copyright (c) 2015, University of Washington Interactive Data Lab + +Copyright (c) 2015, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +diff 4.0.1 - BSD-3-Clause +https://github.com/kpdecker/jsdiff#readme +Copyright (c) 2009-2015, Kevin Decker + +Software License Agreement (BSD License) + +Copyright (c) 2009-2015, Kevin Decker + +All rights reserved. + +Redistribution and use of this software in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of Kevin Decker nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +duplexer2 0.1.4 - BSD-3-Clause +https://github.com/deoxxa/duplexer2#readme +Copyright (c) 2013, Deoxxa Development + +Copyright (c) 2013, Deoxxa Development +====================================== +All rights reserved. +-------------------- + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of Deoxxa Development nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY DEOXXA DEVELOPMENT ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL DEOXXA DEVELOPMENT BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +duplexer3 0.1.4 - BSD-3-Clause +https://github.com/floatdrop/duplexer3 +Copyright (c) 2013, Deoxxa Development + +Copyright (c) 2013, Deoxxa Development +====================================== +All rights reserved. +-------------------- + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of Deoxxa Development nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY DEOXXA DEVELOPMENT ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL DEOXXA DEVELOPMENT BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +hoist-non-react-statics 3.3.1 - BSD-3-Clause +https://github.com/mridgway/hoist-non-react-statics#readme +Copyright 2015, Yahoo! Inc. +Copyright (c) 2015, Yahoo! Inc. + +Software License Agreement (BSD License) +======================================== + +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +---------------------------------------------------- + +Redistribution and use of this software in source and binary forms, with or +without modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Yahoo! Inc. nor the names of YUI's contributors may be + used to endorse or promote products derived from this software without + specific prior written permission of Yahoo! Inc. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +istanbul-lib-coverage 3.0.0 - BSD-3-Clause +https://istanbul.js.org/ +Copyright 2012-2015 Yahoo! Inc. +Copyright 2012-2015, Yahoo Inc. + +Copyright 2012-2015 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +istanbul-lib-hook 3.0.0 - BSD-3-Clause +https://istanbul.js.org/ +Copyright 2012-2015 Yahoo! Inc. +Copyright 2012-2015, Yahoo Inc. + +Copyright 2012-2015 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +istanbul-lib-instrument 4.0.0 - BSD-3-Clause +https://istanbul.js.org/ +Copyright 2012-2015 Yahoo! Inc. +Copyright 2012-2015, Yahoo Inc. + +Copyright 2012-2015 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +istanbul-lib-report 3.0.0 - BSD-3-Clause +https://istanbul.js.org/ +Copyright 2012-2015 Yahoo! Inc. +Copyright 2012-2015, Yahoo Inc. + +Copyright 2012-2015 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +istanbul-lib-source-maps 4.0.0 - BSD-3-Clause +https://istanbul.js.org/ +Copyright 2015 Yahoo! Inc. +Copyright 2015, Yahoo Inc. +Copyright 2012-2015, Yahoo Inc. + +Copyright 2015 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +istanbul-reports 3.0.0 - BSD-3-Clause +https://istanbul.js.org/ +(c) Sindre Sorhus +Copyright 2012-2015 Yahoo! Inc. +Copyright 2012-2015, Yahoo Inc. +Copyright (c) Facebook, Inc. and its affiliates. + +Copyright 2012-2015 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jpeg-js 0.3.7 - BSD-3-Clause +https://github.com/eugeneware/jpeg-js#readme +Copyright 2011 +Copyright (c) 2014, Eugene Ware +Copyright (c) 2008, Adobe Systems Incorporated + +Copyright (c) 2014, Eugene Ware +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of Eugene Ware nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY EUGENE WARE ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL EUGENE WARE BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lolex 5.1.2 - BSD-3-Clause +http://github.com/sinonjs/lolex +Copyright (c) 2013 +Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no. + +Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +md5 2.2.1 - BSD-3-Clause +https://github.com/pvorb/node-md5#readme +Copyright (c) 2009, Jeff Mott. +Copyright (c) 2011-2012, Paul Vorbach. +Copyright (c) 2011-2015, Paul Vorbach. + +Copyright © 2011-2012, Paul Vorbach. +Copyright © 2009, Jeff Mott. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +* Neither the name Crypto-JS nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +nise 3.0.0 - BSD-3-Clause +https://github.com/sinonjs/nise#readme +Copyright (c) 2013 +Copyright (c) 2010-2017, Christian Johansen, christian@cjohansen.no +Copyright jQuery Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +(The BSD License) + +Copyright (c) 2010-2017, Christian Johansen, christian@cjohansen.no +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Christian Johansen nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +parse-cache-control 1.0.1 - BSD-3-Clause +https://github.com/roryf/parse-cache-control +Copyright (c) 2012-2014, Walmart and other contributors. + +Copyright (c) 2012-2014, Walmart and other contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of any contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * * * + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +qs 6.5.2 - BSD-3-Clause +https://github.com/ljharb/qs +Copyright (c) 2014 Nathan LaFreniere and other contributors. + +Copyright (c) 2014 Nathan LaFreniere and other contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of any contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * * * + +The complete list of contributors can be found at: https://github.com/hapijs/qs/graphs/contributors + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-transition-group 2.9.0 - BSD-3-Clause +https://github.com/reactjs/react-transition-group#readme +(c) Sindre Sorhus +Copyright 2013-present, Facebook, Inc. +Copyright (c) 2013-present, Facebook, Inc. +Copyright (c) 2018, React Community Forked from React (https://github.com/facebook/react) + +BSD 3-Clause License + +Copyright (c) 2018, React Community +Forked from React (https://github.com/facebook/react) Copyright 2013-present, Facebook, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +rw 1.3.3 - BSD-3-Clause +https://github.com/mbostock/rw +Copyright (c) 2014-2016, Michael Bostock + +Copyright (c) 2014-2016, Michael Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name Michael Bostock may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +serialize-javascript 2.1.2 - BSD-3-Clause +https://github.com/yahoo/serialize-javascript +Copyright 2014 Yahoo! Inc. +Copyright (c) 2014, Yahoo! Inc. + +Copyright 2014 Yahoo! Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the Yahoo! Inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +sinon 8.0.1 - BSD-3-Clause +https://sinonjs.org/ +Copyright (c) 2013 +Copyright (c) 2009-2015, Kevin Decker +Copyright (c) 2010-2017, Christian Johansen, christian@cjohansen.no +Copyright jQuery Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +(The BSD License) + +Copyright (c) 2010-2017, Christian Johansen, christian@cjohansen.no +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Christian Johansen nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +source-map 0.1.43 - BSD-3-Clause +https://github.com/mozilla/source-map +Copyright 2011 The Closure Compiler +Copyright 2011 Mozilla Foundation and contributors +Copyright 2012 Mozilla Foundation and contributors +Copyright 2014 Mozilla Foundation and contributors +Copyright 2009-2011 Mozilla Foundation and contributors +Copyright (c) 2009-2011, Mozilla Foundation and contributors + + +Copyright (c) 2009-2011, Mozilla Foundation and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the names of the Mozilla Foundation nor the names of project + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +source-map 0.5.7 - BSD-3-Clause +https://github.com/mozilla/source-map +Copyright 2011 The Closure Compiler +Copyright 2011 Mozilla Foundation and contributors +Copyright 2014 Mozilla Foundation and contributors +Copyright 2009-2011 Mozilla Foundation and contributors +Copyright (c) 2009-2011, Mozilla Foundation and contributors + + +Copyright (c) 2009-2011, Mozilla Foundation and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the names of the Mozilla Foundation nor the names of project + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +source-map 0.6.1 - BSD-3-Clause +https://github.com/mozilla/source-map +Copyright 2011 The Closure Compiler +Copyright 2011 Mozilla Foundation and contributors +Copyright 2014 Mozilla Foundation and contributors +Copyright 2009-2011 Mozilla Foundation and contributors +Copyright (c) 2009-2011, Mozilla Foundation and contributors + + +Copyright (c) 2009-2011, Mozilla Foundation and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the names of the Mozilla Foundation nor the names of project + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +sprintf-js 1.0.3 - BSD-3-Clause +https://github.com/alexei/sprintf.js#readme +Copyright (c) 2007-2014, Alexandru Marasteanu + +Copyright (c) 2007-2014, Alexandru Marasteanu +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of this software nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tough-cookie 2.4.3 - BSD-3-Clause +https://github.com/salesforce/tough-cookie +Copyright (c) 2015, Salesforce.com, Inc. +Copyright (c) 2018, Salesforce.com, Inc. + +Copyright (c) 2015, Salesforce.com, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega 5.7.3 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2013, Jason Davies. +Copyright 2018 Vladimir Agafonkin. +Copyright (c) 2012 Mathias Bynens +Copyright (c) 2013 Mathias Bynens +Copyright (c) 2012 Kris Kowal +Copyright (c) 2013 Thaddee Tyl +Copyright (c) 2012 Yusuke Suzuki +Copyright (c) 2011 Ariya Hidayat +Copyright (c) 2012 Ariya Hidayat +Copyright (c) 2013 Ariya Hidayat +Copyright (c) 2012 Arpad Borsos +Copyright (c) 2012 Joost-Wim Boekesteijn +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-canvas 1.2.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-crossfilter 4.0.1 - BSD-3-Clause + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-dataflow 5.4.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-embed 4.2.5 - BSD-3-Clause +https://github.com/vega/vega-embed#readme +Copyright (c) Microsoft Corporation. +Copyright (c) 2015, University of Washington Interactive Data Lab + +Copyright (c) 2015, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-encode 4.4.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-event-selector 2.0.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-expression 2.6.2 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2012 Mathias Bynens +Copyright (c) 2013 Mathias Bynens +Copyright (c) 2012 Kris Kowal +Copyright (c) 2013 Thaddee Tyl +Copyright (c) 2012 Yusuke Suzuki +Copyright (c) 2011 Ariya Hidayat +Copyright (c) 2012 Ariya Hidayat +Copyright (c) 2013 Ariya Hidayat +Copyright (c) 2012 Arpad Borsos +Copyright (c) 2012 Joost-Wim Boekesteijn +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-force 4.0.3 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-functions 5.4.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-geo 4.1.0 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-hierarchy 4.0.3 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-lite 3.4.0 - BSD-3-Clause +https://vega.github.io/vega-lite/ +Copyright (c) Microsoft Corporation. +Copyright (c) 2012 Mathias Bynens +Copyright (c) 2013 Mathias Bynens +Copyright (c) 2012 Kris Kowal +Copyright (c) 2013 Thaddee Tyl +Copyright (c) 2012 Yusuke Suzuki +Copyright (c) 2011 Ariya Hidayat +Copyright (c) 2012 Ariya Hidayat +Copyright (c) 2013 Ariya Hidayat +Copyright (c) 2012 Arpad Borsos +Copyright (c) 2015, University of Washington Interactive Data Lab. +Copyright (c) 2012 Joost-Wim Boekesteijn + +Copyright (c) 2015, University of Washington Interactive Data Lab. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the University of Washington Interactive Data Lab + nor the names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-loader 4.1.2 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-parser 5.10.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-projection 1.3.0 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-regression 1.0.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-runtime 5.0.2 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-scale 4.1.3 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-scenegraph 4.3.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-schema-url-parser 1.1.0 - BSD-3-Clause +https://github.com/vega/schema#readme +Copyright (c) 2017, Vega + +BSD 3-Clause License + +Copyright (c) 2017, Vega +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-selections 5.0.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-statistics 1.6.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-themes 2.5.0 - BSD-3-Clause +https://github.com/vega/vega-themes#readme +Copyright (c) 2016, University of Washington Interactive Data Lab + +Copyright (c) 2016, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-tooltip 0.18.1 - BSD-3-Clause +https://github.com/vega/vega-tooltip#readme +Copyright 2016 Interactive Data Lab and contributors + +Copyright 2016 Interactive Data Lab and contributors + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-transforms 4.4.3 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-typings 0.7.2 - BSD-3-Clause +https://github.com/vega/vega#readme + +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-typings 0.10.2 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-util 1.10.0 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-util 1.12.0 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-view 5.3.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-view-transforms 4.4.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-voronoi 4.1.1 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vega-wordcloud 4.0.2 - BSD-3-Clause +https://github.com/vega/vega#readme +Copyright (c) 2013, Jason Davies. +Copyright (c) 2015-2018, University of Washington Interactive Data Lab + +Copyright (c) 2015-2018, University of Washington Interactive Data Lab +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +spdx-exceptions 2.2.0 - CC-BY-3.0 +https://github.com/kemitchell/spdx-exceptions.json#readme +Copyright (c) 2010-2015 Linux Foundation and its Contributors. + +Creative Commons Attribution 3.0 Unported CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. + + 1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. + + b. "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined above) for the purposes of this License. + + c. "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership. + + d. "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. + + e. "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. + + f. "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. + + g. "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. + + h. "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. + + i. "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. + + 2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. + + 3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; + + b. to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified."; + + c. to Distribute and Publicly Perform the Work including as incorporated in Collections; and, + + d. to Distribute and Publicly Perform Adaptations. + + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; + + ii. Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and, + + iii. Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License. + + The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved. + + 4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(b), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(b), as requested. + + b. If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv), consistent with Section 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4 (b) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. + + c. Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise. + + 5. Representations, Warranties and Disclaimer + + UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + + 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 7. Termination + + a. This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. + + b. Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. + + 8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. + + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. + + c. If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + + d. No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. + + e. This License may not be modified without the mutual written agreement of the Licensor and You. + + f. The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. + +Creative Commons Notice + +Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. + +Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of this License. + +Creative Commons may be contacted at http://creativecommons.org/. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-font-family-system-ui 3.0.0 - CC0-1.0 +https://github.com/JLHwung/postcss-font-family-system-ui#readme + +# CC0 1.0 Universal + +## Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an “owner”) of an original work of +authorship and/or a database (each, a “Work”). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific works +(“Commons”) that the public can reliably and without fear of later claims of +infringement build upon, modify, incorporate in other works, reuse and +redistribute as freely as possible in any form whatsoever and for any purposes, +including without limitation commercial purposes. These owners may contribute +to the Commons to promote the ideal of a free culture and the further +production of creative, cultural and scientific works, or to gain reputation or +greater distribution for their Work in part through the use and efforts of +others. + +For these and/or other purposes and motivations, and without any expectation of +additional consideration or compensation, the person associating CC0 with a +Work (the “Affirmer”), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and +publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be + protected by copyright and related or neighboring rights (“Copyright and + Related Rights”). Copyright and Related Rights include, but are not limited + to, the following: + 1. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + 2. moral rights retained by the original author(s) and/or performer(s); + 3. publicity and privacy rights pertaining to a person’s image or likeness + depicted in a Work; + 4. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(i), below; + 5. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + 6. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + 7. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations + thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, + applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and + unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright + and Related Rights and associated claims and causes of action, whether now + known or unknown (including existing as well as future claims and causes of + action), in the Work (i) in all territories worldwide, (ii) for the maximum + duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “Waiver”). Affirmer + makes the Waiver for the benefit of each member of the public at large and + to the detriment of Affirmer’s heirs and successors, fully intending that + such Waiver shall not be subject to revocation, rescission, cancellation, + termination, or any other legal or equitable action to disrupt the quiet + enjoyment of the Work by the public as contemplated by Affirmer’s express + Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be + judged legally invalid or ineffective under applicable law, then the Waiver + shall be preserved to the maximum extent permitted taking into account + Affirmer’s express Statement of Purpose. In addition, to the extent the + Waiver is so judged Affirmer hereby grants to each affected person a + royalty-free, non transferable, non sublicensable, non exclusive, + irrevocable and unconditional license to exercise Affirmer’s Copyright and + Related Rights in the Work (i) in all territories worldwide, (ii) for the + maximum duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “License”). The License + shall be deemed effective as of the date CC0 was applied by Affirmer to the + Work. Should any part of the License for any reason be judged legally + invalid or ineffective under applicable law, such partial invalidity or + ineffectiveness shall not invalidate the remainder of the License, and in + such case Affirmer hereby affirms that he or she will not (i) exercise any + of his or her remaining Copyright and Related Rights in the Work or (ii) + assert any associated claims and causes of action with respect to the Work, + in either case contrary to Affirmer’s express Statement of Purpose. + +4. Limitations and Disclaimers. + 1. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + 2. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or + otherwise, including without limitation warranties of title, + merchantability, fitness for a particular purpose, non infringement, or + the absence of latent or other defects, accuracy, or the present or + absence of errors, whether or not discoverable, all to the greatest + extent permissible under applicable law. + 3. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person’s Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the Work. + 4. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-nesting 4.2.1 - CC0-1.0 +https://github.com/jonathantneal/postcss-nesting#readme + +# CC0 1.0 Universal + +## Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an “owner”) of an original work of +authorship and/or a database (each, a “Work”). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific works +(“Commons”) that the public can reliably and without fear of later claims of +infringement build upon, modify, incorporate in other works, reuse and +redistribute as freely as possible in any form whatsoever and for any purposes, +including without limitation commercial purposes. These owners may contribute +to the Commons to promote the ideal of a free culture and the further +production of creative, cultural and scientific works, or to gain reputation or +greater distribution for their Work in part through the use and efforts of +others. + +For these and/or other purposes and motivations, and without any expectation of +additional consideration or compensation, the person associating CC0 with a +Work (the “Affirmer”), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and +publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be + protected by copyright and related or neighboring rights (“Copyright and + Related Rights”). Copyright and Related Rights include, but are not limited + to, the following: + 1. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + 2. moral rights retained by the original author(s) and/or performer(s); + 3. publicity and privacy rights pertaining to a person’s image or likeness + depicted in a Work; + 4. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(i), below; + 5. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + 6. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + 7. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations + thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer’s heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer’s express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer’s express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, non +transferable, non sublicensable, non exclusive, irrevocable and unconditional +license to exercise Affirmer’s Copyright and Related Rights in the Work (i) in +all territories worldwide, (ii) for the maximum duration provided by applicable +law or treaty (including future time extensions), (iii) in any current or +future medium and for any number of copies, and (iv) for any purpose +whatsoever, including without limitation commercial, advertising or promotional +purposes (the “License”). The License shall be deemed effective as of the date +CC0 was applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder of the +License, and in such case Affirmer hereby affirms that he or she will not (i) +exercise any of his or her remaining Copyright and Related Rights in the Work +or (ii) assert any associated claims and causes of action with respect to the +Work, in either case contrary to Affirmer’s express Statement of Purpose. + +4. Limitations and Disclaimers. + 1. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + 2. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, statutory + or otherwise, including without limitation warranties of title, + merchantability, fitness for a particular purpose, non infringement, or + the absence of latent or other defects, accuracy, or the present or + absence of errors, whether or not discoverable, all to the greatest + extent permissible under applicable law. + 3. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person’s Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the Work. + 4. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-pseudo-class-any-link 4.0.0 - CC0-1.0 +https://github.com/jonathantneal/postcss-pseudo-class-any-link#readme + +# CC0 1.0 Universal + +## Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an “owner”) of an original work of +authorship and/or a database (each, a “Work”). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific works +(“Commons”) that the public can reliably and without fear of later claims of +infringement build upon, modify, incorporate in other works, reuse and +redistribute as freely as possible in any form whatsoever and for any purposes, +including without limitation commercial purposes. These owners may contribute +to the Commons to promote the ideal of a free culture and the further +production of creative, cultural and scientific works, or to gain reputation or +greater distribution for their Work in part through the use and efforts of +others. + +For these and/or other purposes and motivations, and without any expectation of +additional consideration or compensation, the person associating CC0 with a +Work (the “Affirmer”), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and +publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be + protected by copyright and related or neighboring rights (“Copyright and + Related Rights”). Copyright and Related Rights include, but are not limited + to, the following: + 1. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + 2. moral rights retained by the original author(s) and/or performer(s); + 3. publicity and privacy rights pertaining to a person’s image or likeness + depicted in a Work; + 4. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(i), below; + 5. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + 6. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + 7. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations + thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer’s heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer’s express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer’s express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, non +transferable, non sublicensable, non exclusive, irrevocable and unconditional +license to exercise Affirmer’s Copyright and Related Rights in the Work (i) in +all territories worldwide, (ii) for the maximum duration provided by applicable +law or treaty (including future time extensions), (iii) in any current or +future medium and for any number of copies, and (iv) for any purpose +whatsoever, including without limitation commercial, advertising or promotional +purposes (the “License”). The License shall be deemed effective as of the date +CC0 was applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder of the +License, and in such case Affirmer hereby affirms that he or she will not (i) +exercise any of his or her remaining Copyright and Related Rights in the Work +or (ii) assert any associated claims and causes of action with respect to the +Work, in either case contrary to Affirmer’s express Statement of Purpose. + +4. Limitations and Disclaimers. + 1. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + 2. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, statutory + or otherwise, including without limitation warranties of title, + merchantability, fitness for a particular purpose, non infringement, or + the absence of latent or other defects, accuracy, or the present or + absence of errors, whether or not discoverable, all to the greatest + extent permissible under applicable law. + 3. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person’s Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the Work. + 4. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +spdx-license-ids 3.0.5 - CC0-1.0 +https://github.com/shinnn/spdx-license-ids#readme + +Creative Commons Legal Code + +CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + + 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. + + 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. + + 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. + + 4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@istanbuljs/load-nyc-config 1.0.0 - ISC +https://github.com/istanbuljs/load-nyc-config#readme +Copyright (c) 2019 + +ISC License + +Copyright (c) 2019, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +abbrev 1.1.1 - ISC +https://github.com/isaacs/abbrev-js#readme +Copyright Isaac Z. Schlueter and Contributors +Copyright (c) Isaac Z. Schlueter and Contributors + +This software is dual-licensed under the ISC and MIT licenses. +You may use this software under EITHER of the following licenses. + +---------- + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---------- + +Copyright Isaac Z. Schlueter and Contributors +All rights reserved. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +anymatch 2.0.0 - ISC +https://github.com/micromatch/anymatch +Copyright (c) 2014 Elan Shanker + +The ISC License + +Copyright (c) 2014 Elan Shanker + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +aproba 1.2.0 - ISC +https://github.com/iarna/aproba +Copyright (c) 2015, Rebecca Turner + +Copyright (c) 2015, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +are-we-there-yet 1.1.5 - ISC +https://github.com/iarna/are-we-there-yet +Copyright (c) 2015, Rebecca Turner + +Copyright (c) 2015, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ast-types-flow 0.0.7 - ISC +https://github.com/kyldvs/ast-types-flow#readme + +ISC License + +Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") + +Copyright (c) 1995-2003 by Internet Software Consortium + +Permission to use, copy, modify, and /or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +block-stream 0.0.9 - ISC +https://github.com/isaacs/block-stream#readme +Copyright (c) Isaac Z. Schlueter +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +boolbase 1.0.0 - ISC +https://github.com/fb55/boolbase + +ISC License + +Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") + +Copyright (c) 1995-2003 by Internet Software Consortium + +Permission to use, copy, modify, and /or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cacache 12.0.3 - ISC +https://github.com/npm/cacache#readme +Copyright (c) npm, Inc. + +ISC License + +Copyright (c) npm, Inc. + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE COPYRIGHT HOLDER DISCLAIMS +ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE +USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cacache 13.0.1 - ISC +https://github.com/npm/cacache#readme +Copyright (c) npm, Inc. + +ISC License + +Copyright (c) npm, Inc. + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE COPYRIGHT HOLDER DISCLAIMS +ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE +USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +chownr 1.1.2 - ISC +https://github.com/isaacs/chownr#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cliui 5.0.0 - ISC +https://github.com/yargs/cliui#readme +Copyright (c) 2015 + +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cliui 3.2.0 - ISC +https://github.com/yargs/cliui#readme +Copyright (c) 2015 + +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cliui 6.0.0 - ISC +https://github.com/yargs/cliui#readme +Copyright (c) 2015 + +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +console-control-strings 1.1.0 - ISC +https://github.com/iarna/console-control-strings#readme +Copyright (c) 2014, Rebecca Turner + +Copyright (c) 2014, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +copy-concurrently 1.0.5 - ISC +https://www.npmjs.com/package/copy-concurrently +Copyright (c) 2017, Rebecca Turner + +Copyright (c) 2017, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d 1.0.1 - ISC +https://github.com/medikoo/d#readme +Copyright (c) 2013-2019, Mariusz Nowak, medikoo, medikoo.com + +ISC License + +Copyright (c) 2013-2019, Mariusz Nowak, @medikoo, medikoo.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +d3-delaunay 5.1.6 - ISC +https://github.com/d3/d3-delaunay +Copyright 2019 Mapbox, Inc. +Copyright 2018 Observable, Inc. +Copyright 2019 Mike Bostock https://github.com/mapbox/delaunator + +Copyright 2018 Observable, Inc. + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +delaunator 4.0.1 - ISC +https://github.com/mapbox/delaunator#readme +Copyright (c) 2017, Mapbox + +ISC License + +Copyright (c) 2017, Mapbox + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +es5-ext 0.10.50 - ISC +https://github.com/medikoo/es5-ext#readme +Copyright (c) 2008 Matsuza +Copyright (c) 2011-2019, Mariusz Nowak, medikoo, medikoo.com + +ISC License + +Copyright (c) 2011-2019, Mariusz Nowak, @medikoo, medikoo.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +es6-weak-map 2.0.3 - ISC +https://github.com/medikoo/es6-weak-map#readme +Copyright (c) 2013-2018, Mariusz Nowak, medikoo, medikoo.com + +ISC License + +Copyright (c) 2013-2018, Mariusz Nowak, @medikoo, medikoo.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +figgy-pudding 3.5.1 - ISC +https://github.com/zkat/figgy-pudding#readme +Copyright (c) npm, Inc. + +ISC License + +Copyright (c) npm, Inc. + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE COPYRIGHT HOLDER DISCLAIMS +ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE +USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +flatted 2.0.1 - ISC +https://github.com/WebReflection/flatted#readme +(c) 2018, Andrea Giammarchi, (ISC) +Copyright (c) 2018, Andrea Giammarchi, WebReflection + +ISC License + +Copyright (c) 2018, Andrea Giammarchi, @WebReflection + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +foreground-child 2.0.0 - ISC +https://github.com/tapjs/foreground-child#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fs-minipass 1.2.7 - ISC +https://github.com/npm/fs-minipass#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fs-minipass 2.0.0 - ISC +https://github.com/npm/fs-minipass#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fs-write-stream-atomic 1.0.10 - ISC +https://github.com/npm/fs-write-stream-atomic +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fs.realpath 1.0.0 - ISC +https://github.com/isaacs/fs.realpath#readme +Copyright (c) Isaac Z. Schlueter and Contributors +Copyright Joyent, Inc. and other Node contributors. + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---- + +This library bundles a version of the `fs.realpath` and `fs.realpathSync` +methods from Node.js v0.10 under the terms of the Node.js MIT license. + +Node's license follows, also included at the header of `old.js` which contains +the licensed code: + + Copyright Joyent, Inc. and other Node contributors. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fstream 1.0.12 - ISC +https://github.com/npm/fstream#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +gauge 2.7.4 - ISC +https://github.com/iarna/gauge +Copyright (c) 2014, Rebecca Turner + +Copyright (c) 2014, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +get-caller-file 1.0.3 - ISC +https://github.com/stefanpenner/get-caller-file#readme +Copyright 2018 Stefan Penner + +ISC License (ISC) +Copyright 2018 Stefan Penner + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +get-caller-file 2.0.5 - ISC +https://github.com/stefanpenner/get-caller-file#readme +Copyright 2018 Stefan Penner + +ISC License (ISC) +Copyright 2018 Stefan Penner + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +glob 7.1.4 - ISC +https://github.com/isaacs/node-glob#readme + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +## Glob Logo + +Glob's logo created by Tanya Brassie , licensed +under a Creative Commons Attribution-ShareAlike 4.0 International License +https://creativecommons.org/licenses/by-sa/4.0/ + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +glob 7.1.6 - ISC +https://github.com/isaacs/node-glob#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +## Glob Logo + +Glob's logo created by Tanya Brassie , licensed +under a Creative Commons Attribution-ShareAlike 4.0 International License +https://creativecommons.org/licenses/by-sa/4.0/ + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +glob-parent 3.1.0 - ISC +https://github.com/es128/glob-parent +Copyright (c) 2015 Elan Shanker + +The ISC License + +Copyright (c) 2015 Elan Shanker + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +graceful-fs 4.2.0 - ISC +https://github.com/isaacs/node-graceful-fs#readme +Copyright (c) Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +graceful-fs 4.2.3 - ISC +https://github.com/isaacs/node-graceful-fs#readme +Copyright (c) Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +har-schema 2.0.0 - ISC +https://github.com/ahmadnassri/har-schema +Copyright (c) 2015, Ahmad Nassri +copyright ahmadnassri.com (https://www.ahmadnassri.com/) + +Copyright (c) 2015, Ahmad Nassri + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has-unicode 2.0.1 - ISC +https://github.com/iarna/has-unicode +Copyright (c) 2014, Rebecca Turner + +Copyright (c) 2014, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ignore-walk 3.0.1 - ISC +https://github.com/isaacs/ignore-walk#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +infer-owner 1.0.4 - ISC +https://github.com/npm/infer-owner#readme +Copyright (c) npm, Inc. and Contributors + +The ISC License + +Copyright (c) npm, Inc. and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +inflight 1.0.6 - ISC +https://github.com/isaacs/inflight +Copyright (c) Isaac Z. Schlueter + +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +inherits 2.0.3 - ISC +https://github.com/isaacs/inherits#readme +Copyright (c) Isaac Z. Schlueter + +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +inherits 2.0.4 - ISC +https://github.com/isaacs/inherits#readme +Copyright (c) Isaac Z. Schlueter + +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ini 1.3.5 - ISC +https://github.com/isaacs/ini#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +isexe 2.0.0 - ISC +https://github.com/isaacs/isexe#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +istanbul-lib-processinfo 2.0.2 - ISC +https://github.com/istanbuljs/istanbul-lib-processinfo#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +json-stringify-safe 5.0.1 - ISC +https://github.com/isaacs/json-stringify-safe +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +linear-layout-vector 0.0.1 - ISC + +ISC License + +Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") + +Copyright (c) 1995-2003 by Internet Software Consortium + +Permission to use, copy, modify, and /or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lru-cache 4.1.5 - ISC +https://github.com/isaacs/node-lru-cache#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +minimalistic-assert 1.0.1 - ISC +https://github.com/calvinmetcalf/minimalistic-assert +Copyright 2015 Calvin Metcalf + +Copyright 2015 Calvin Metcalf + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +minimatch 3.0.4 - ISC +https://github.com/isaacs/minimatch#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +minipass 2.9.0 - ISC +https://github.com/isaacs/minipass#readme +Copyright (c) npm, Inc. and Contributors + +The ISC License + +Copyright (c) npm, Inc. and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +minipass 3.1.1 - ISC +https://github.com/isaacs/minipass#readme +Copyright (c) npm, Inc. and Contributors + +The ISC License + +Copyright (c) npm, Inc. and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +minipass-collect 1.0.2 - ISC +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +minipass-flush 1.0.5 - ISC +https://github.com/isaacs/minipass-flush#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +minipass-pipeline 1.2.2 - ISC +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +move-concurrently 1.0.1 - ISC +https://www.npmjs.com/package/move-concurrently +Copyright (c) 2017, Rebecca Turner + +Copyright (c) 2017, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +mute-stream 0.0.7 - ISC +https://github.com/isaacs/mute-stream#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +nopt 4.0.1 - ISC +https://github.com/npm/nopt#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +npm-bundled 1.0.6 - ISC +https://github.com/npm/npm-bundled#readme +Copyright (c) npm, Inc. and Contributors + +The ISC License + +Copyright (c) npm, Inc. and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +npm-packlist 1.4.6 - ISC +https://www.npmjs.com/package/npm-packlist +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +npmlog 4.1.2 - ISC +https://github.com/npm/npmlog#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +nyc 15.0.0 - ISC +https://istanbul.js.org/ +Copyright (c) 2015 + +ISC License + +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +once 1.4.0 - ISC +https://github.com/isaacs/once#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +osenv 0.1.5 - ISC +https://github.com/npm/osenv#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +package-hash 4.0.0 - ISC +https://github.com/novemberborn/package-hash#readme +Copyright (c) 2016-2017, Mark Wubben (novemberborn.net) + +ISC License (ISC) +Copyright (c) 2016-2017, Mark Wubben (novemberborn.net) + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +parse-asn1 5.1.5 - ISC +https://github.com/crypto-browserify/parse-asn1#readme +Copyright (c) 2017, crypto-browserify contributors + +Copyright (c) 2017, crypto-browserify contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +path-posix 1.0.0 - ISC +https://github.com/jden/node-path-posix +Copyright Joyent, Inc. and other Node contributors. + +Node's license follows: + +==== + +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +==== + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-color-gray 4.1.0 - ISC +https://github.com/postcss/postcss-color-gray#readme +(c) 2017 Shinnosuke Watanabe +Copyright 2017 Shinnosuke Watanabe + +ISC License (ISC) +Copyright 2017 Shinnosuke Watanabe + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +promise-inflight 1.0.1 - ISC +https://github.com/iarna/promise-inflight#readme +Copyright (c) 2017, Rebecca Turner + +Copyright (c) 2017, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +proto-list 1.2.4 - ISC +https://github.com/isaacs/proto-list#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pseudomap 1.0.2 - ISC +https://github.com/isaacs/pseudomap#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +range-inclusive 1.0.2 - ISC +https://github.com/emilbayes/range-inclusive#readme +Copyright (c) 2015, Emil Bay + +Copyright (c) 2015, Emil Bay + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +remove-trailing-separator 1.1.0 - ISC +https://github.com/darsain/remove-trailing-separator#readme + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +request-promise-core 1.1.2 - ISC +https://github.com/request/promise-core#readme +Copyright (c) 2016, Nicolai Kamenzky and contributors + +ISC License + +Copyright (c) 2016, Nicolai Kamenzky and contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +request-promise-native 1.0.7 - ISC +https://github.com/request/request-promise-native#readme +Copyright (c) 2017, Nicolai Kamenzky and contributors + +ISC License + +Copyright (c) 2017, Nicolai Kamenzky and contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +require-main-filename 1.0.1 - ISC +https://github.com/yargs/require-main-filename#readme +Copyright (c) 2016 + +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +require-main-filename 2.0.0 - ISC +https://github.com/yargs/require-main-filename#readme +Copyright (c) 2016 + +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +rimraf 3.0.0 - ISC +https://github.com/isaacs/rimraf#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +rimraf 2.7.1 - ISC +https://github.com/isaacs/rimraf#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +rimraf 3.0.2 - ISC +https://github.com/isaacs/rimraf#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +run-queue 1.0.3 - ISC +https://npmjs.com/package/run-queue + +ISC License + +Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") + +Copyright (c) 1995-2003 by Internet Software Consortium + +Permission to use, copy, modify, and /or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +sax 1.2.4 - ISC +https://github.com/isaacs/sax-js#readme +Copyright (c) Isaac Z. Schlueter and Contributors +Copyright Mathias Bynens + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +==== + +`String.fromCodePoint` by Mathias Bynens used according to terms of MIT +License, as follows: + + Copyright Mathias Bynens + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +saxes 3.1.11 - ISC +https://github.com/lddubeau/saxes#readme +Copyright (c) Isaac Z. Schlueter and Contributors +Copyright Mathias Bynens + +The ISC License + +Copyright (c) Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +==== + +The following license is the one that governed sax, from which saxes +was forked. Isaac Schlueter is not *directly* involved with saxes so +don't go bugging him for saxes issues. + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +==== + +`String.fromCodePoint` by Mathias Bynens is no longer used, but it can +still be found in old commits. It was once used according to terms of +MIT License, as follows: + + Copyright Mathias Bynens + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +semver 6.3.0 - ISC +https://github.com/npm/node-semver#readme +Copyright Isaac Z. +Copyright Isaac Z. Schlueter +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +semver 5.7.0 - ISC +https://github.com/npm/node-semver#readme +Copyright Isaac Z. +Copyright Isaac Z. Schlueter +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +semver 5.7.1 - ISC +https://github.com/npm/node-semver#readme +Copyright Isaac Z. +Copyright Isaac Z. Schlueter +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +set-blocking 2.0.0 - ISC +https://github.com/yargs/set-blocking#readme +Copyright (c) 2016 + +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +setprototypeof 1.1.1 - ISC +https://github.com/wesleytodd/setprototypeof +Copyright (c) 2015, Wes Todd + +Copyright (c) 2015, Wes Todd + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +signal-exit 3.0.2 - ISC +https://github.com/tapjs/signal-exit +Copyright (c) 2015 + +The ISC License + +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +spawn-wrap 2.0.0 - ISC +https://github.com/istanbuljs/spawn-wrap#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ssri 7.1.0 - ISC +https://github.com/npm/ssri#readme +Copyright (c) npm, Inc. + +ISC License + +Copyright (c) npm, Inc. + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE COPYRIGHT HOLDER DISCLAIMS +ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE +USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +stealthy-require 1.1.1 - ISC +https://github.com/analog-nico/stealthy-require#readme +Copyright (c) 2017, Nicolai Kamenzky and contributors + +ISC License + +Copyright (c) 2017, Nicolai Kamenzky and contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tar 2.2.2 - ISC +https://github.com/isaacs/node-tar#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License +Copyright (c) Isaac Z. Schlueter and Contributors +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tar 4.4.13 - ISC +https://github.com/npm/node-tar#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +test-exclude 6.0.0 - ISC +https://istanbul.js.org/ +Copyright (c) 2016 + +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +topojson-client 3.1.0 - ISC +https://github.com/topojson/topojson-client +Copyright 2019 Mike Bostock +Copyright 2012-2019 Michael Bostock + +Copyright 2012-2019 Michael Bostock + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +type 1.0.1 - ISC +https://github.com/medikoo/type#readme + +ISC License + +Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") + +Copyright (c) 1995-2003 by Internet Software Consortium + +Permission to use, copy, modify, and /or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unique-filename 1.1.1 - ISC +https://github.com/iarna/unique-filename +Copyright npm, Inc + +Copyright npm, Inc + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +which 1.3.1 - ISC +https://github.com/isaacs/node-which#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +which 2.0.2 - ISC +https://github.com/isaacs/node-which#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +which-module 2.0.0 - ISC +https://github.com/nexdrew/which-module#readme +Copyright (c) 2016 + +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +which-module 1.0.0 - ISC +https://github.com/nexdrew/which-module#readme +Copyright (c) 2016 + +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +wide-align 1.1.3 - ISC +https://github.com/iarna/wide-align#readme +Copyright (c) 2015, Rebecca Turner + +Copyright (c) 2015, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +wrappy 1.0.2 - ISC +https://github.com/npm/wrappy +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +write-file-atomic 3.0.1 - ISC +https://github.com/npm/write-file-atomic +Copyright (c) 2015, Rebecca Turner + +Copyright (c) 2015, Rebecca Turner + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +y18n 4.0.0 - ISC +https://github.com/yargs/y18n +Copyright (c) 2015 + +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +yallist 4.0.0 - ISC +https://github.com/isaacs/yallist#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +yallist 3.1.1 - ISC +https://github.com/isaacs/yallist#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +yallist 2.1.2 - ISC +https://github.com/isaacs/yallist#readme +Copyright (c) Isaac Z. Schlueter and Contributors + +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +yargs-parser 5.0.0 - ISC +https://github.com/yargs/yargs-parser#readme +Copyright (c) 2016 + +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +yargs-parser 18.1.2 - ISC +https://github.com/yargs/yargs-parser#readme +Copyright (c) 2016 + +Copyright (c) 2016, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/cli 7.8.4 - MIT +https://babeljs.io/ +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/code-frame 7.5.5 - MIT +https://babeljs.io/ +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/code-frame 7.0.0 - MIT +https://babeljs.io/ +Copyright (c) 2014-2018 Sebastian McKenzie + +MIT License + +Copyright (c) 2014-2018 Sebastian McKenzie + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/core 7.7.7 - MIT +https://babeljs.io/ +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/generator 7.7.7 - MIT +https://babeljs.io/ +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/helper-function-name 7.7.4 - MIT +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/helper-get-function-arity 7.7.4 - MIT +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/helper-split-export-declaration 7.7.4 - MIT +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/helpers 7.7.4 - MIT +https://babeljs.io/ +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/parser 7.7.7 - MIT +https://babeljs.io/ +Copyright (c) 2012-2014 by various contributors + +Copyright (C) 2012-2014 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/polyfill 7.4.4 - MIT +https://babeljs.io/ +(c) 2019 Denis Pushkarev +copyright (c) 2019 Denis Pushkarev +Copyright (c) 2014-present, Facebook, Inc. +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/register 7.9.0 - MIT +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/runtime 7.8.3 - MIT +https://babeljs.io/docs/en/next/babel-runtime +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/runtime 7.5.4 - MIT +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/runtime 7.6.3 - MIT +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/template 7.7.4 - MIT +https://babeljs.io/ +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/traverse 7.7.4 - MIT +https://babeljs.io/ +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@babel/types 7.7.4 - MIT +https://babeljs.io/ +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +MIT License + +Copyright (c) 2014-present Sebastian McKenzie and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@istanbuljs/schema 0.1.2 - MIT +https://github.com/istanbuljs/schema#readme +Copyright (c) 2019 CFWare, LLC + +MIT License + +Copyright (c) 2019 CFWare, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@jest/types 24.9.0 - MIT +https://github.com/facebook/jest#readme +Copyright (c) Facebook, Inc. and its affiliates. + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@loadable/component 5.12.0 - MIT +https://github.com/gregberge/loadable-components#readme +Copyright 2019 Greg Berge + +Copyright 2019 Greg Bergé + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@nteract/types 6.0.4 - MIT +Copyright (c) 2016, nteract contributors + +Copyright (c) 2016, nteract contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of nteract nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@nteract/vega-embed-v2 1.1.0 - MIT +Copyright (c) 2018 + +MIT License + +Copyright (c) 2018 nteract + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@nteract/vega-embed-v3 1.1.1 - MIT +Copyright (c) 2018 + +MIT License + +Copyright (c) 2018 nteract + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@sindresorhus/is 0.14.0 - MIT +https://github.com/sindresorhus/is#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@szmarczak/http-timer 1.1.2 - MIT +https://github.com/szmarczak/http-timer#readme +Copyright (c) 2018 Szymon Marczak + +MIT License + +Copyright (c) 2018 Szymon Marczak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@testing-library/dom 6.11.0 - MIT +https://github.com/testing-library/dom-testing-library#readme +Copyright (c) 2017 Kent C. Dodds +Copyright (c) Facebook, Inc. and its affiliates. + +The MIT License (MIT) +Copyright (c) 2017 Kent C. Dodds + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@testing-library/react 9.4.0 - MIT +https://github.com/testing-library/react-testing-library#readme +Copyright (c) 2017 Kent C. Dodds +Copyright (c) 2014-present, Facebook, Inc. +Copyright (c) Facebook, Inc. and its affiliates. + +The MIT License (MIT) +Copyright (c) 2017 Kent C. Dodds + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/backbone 1.4.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/body-parser 1.17.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/caseless 0.12.2 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/clone 0.1.30 - MIT + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/color-name 1.1.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/concat-stream 1.6.0 - MIT + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/connect 3.4.32 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/cookiejar 2.1.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/cors 2.8.6 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/debug 4.1.5 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/dedent 0.7.0 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/dom4 2.0.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/events 3.0.0 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/express 4.17.2 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/express-serve-static-core 4.17.0 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/fast-json-stable-stringify 2.0.0 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/form-data 0.0.33 - MIT + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/hoist-non-react-statics 3.3.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/istanbul-lib-coverage 2.0.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/istanbul-lib-report 1.1.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/istanbul-reports 1.1.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/loadable__component 5.10.0 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/mime 2.0.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/minimatch 3.0.3 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/node 10.14.18 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/node 8.10.58 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/node-fetch 2.5.7 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/prop-types 15.7.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/qs 6.5.3 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/range-parser 1.2.3 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/react-redux 7.1.5 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/redux-logger 3.0.7 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/serve-static 1.13.3 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/sinon 7.5.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/socket.io 2.1.4 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/superagent 3.8.7 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/tcp-port-used 1.0.0 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/testing-library__dom 6.11.1 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/testing-library__react 9.1.2 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/tough-cookie 2.3.5 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/underscore 1.9.4 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/uuid 7.0.2 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/vscode 1.47.0 - MIT + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/vscode 1.46.0 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/yargs 13.0.5 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@types/yargs-parser 15.0.0 - MIT +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +1to2 1.0.0 - MIT +Copyright (c) 2014 3VOT + +The MIT License (MIT) + +Copyright (c) 2014 3VOT + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +accepts 1.3.7 - MIT +https://github.com/jshttp/accepts#readme +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +acorn 5.7.4 - MIT +https://github.com/acornjs/acorn +Copyright (c) 2012-2018 by various contributors + +Copyright (C) 2012-2018 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +acorn 7.1.1 - MIT +https://github.com/acornjs/acorn +Copyright (c) 2012-2018 by various contributors + +Copyright (C) 2012-2018 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +acorn 6.4.1 - MIT +https://github.com/acornjs/acorn +Copyright (c) 2012-2018 by various contributors + +Copyright (C) 2012-2018 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +acorn-dynamic-import 4.0.0 - MIT +https://github.com/kesne/acorn-dynamic-import +Copyright (c) 2016 Jordan Gensler + +MIT License + +Copyright (c) 2016 Jordan Gensler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +acorn-walk 6.2.0 - MIT +https://github.com/acornjs/acorn +Copyright (c) 2012-2018 by various contributors + +Copyright (C) 2012-2018 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +after 0.8.2 - MIT +https://github.com/Raynos/after#readme +Copyright (c) 2011 Raynos. + +Copyright (c) 2011 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +aggregate-error 3.0.1 - MIT +https://github.com/sindresorhus/aggregate-error#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ajv 6.12.2 - MIT +https://github.com/epoberezkin/ajv +(c) 2011 Gary Court. +Copyright 2011 Gary Court. +Copyright (c) 2015-2017 Evgeny Poberezkin + +The MIT License (MIT) + +Copyright (c) 2015-2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ajv 6.12.0 - MIT +https://github.com/epoberezkin/ajv +(c) 2011 Gary Court. +Copyright 2011 Gary Court. +Copyright (c) 2015-2017 Evgeny Poberezkin + +The MIT License (MIT) + +Copyright (c) 2015-2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ajv 6.10.1 - MIT +https://github.com/epoberezkin/ajv +(c) 2011 Gary Court. +Copyright 2011 Gary Court. +Copyright (c) 2015-2017 Evgeny Poberezkin + +The MIT License (MIT) + +Copyright (c) 2015-2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ansi-colors 4.1.1 - MIT +https://github.com/doowb/ansi-colors +Copyright (c) 2015-present, Brian Woodward. +Copyright (c) 2019, Brian Woodward (https://github.com/doowb). + +The MIT License (MIT) + +Copyright (c) 2015-present, Brian Woodward. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ansi-regex 4.1.0 - MIT +https://github.com/chalk/ansi-regex#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ansi-regex 3.0.0 - MIT +https://github.com/chalk/ansi-regex#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ansi-regex 2.1.1 - MIT +https://github.com/chalk/ansi-regex#readme +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ansi-regex 5.0.0 - MIT +https://github.com/chalk/ansi-regex#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ansi-styles 3.2.1 - MIT +https://github.com/chalk/ansi-styles#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ansi-styles 4.2.1 - MIT +https://github.com/chalk/ansi-styles#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ansi-wrap 0.1.0 - MIT +https://github.com/jonschlinkert/ansi-wrap +Copyright (c) 2015 Jon Schlinkert +Copyright (c) 2015, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2015, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +append-buffer 1.0.2 - MIT +https://github.com/doowb/append-buffer +Copyright (c) 2017, Brian Woodward. +Copyright (c) 2017, Brian Woodward (https://doowb.com). + +The MIT License (MIT) + +Copyright (c) 2017, Brian Woodward. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +append-transform 2.0.0 - MIT +https://github.com/istanbuljs/append-transform#readme +Copyright (c) James Talmage +(c) James Talmage (https://github.com/jamestalmage) + +The MIT License (MIT) + +Copyright (c) James Talmage (github.com/jamestalmage) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +arch 2.1.1 - MIT +https://github.com/feross/arch +Copyright (c) Feross Aboukhadijeh +Copyright (c) Feross Aboukhadijeh (http://feross.org). + +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +archy 1.0.0 - MIT +https://github.com/substack/node-archy + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +argparse 1.0.10 - MIT +https://github.com/nodeca/argparse#readme +Copyright (c) 2012 by Vitaly Puzrin +Copyright (c) 2012 Vitaly Puzrin (https://github.com/puzrin). + +(The MIT License) + +Copyright (C) 2012 by Vitaly Puzrin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +arr-diff 4.0.0 - MIT +https://github.com/jonschlinkert/arr-diff +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +arr-flatten 1.1.0 - MIT +https://github.com/jonschlinkert/arr-flatten +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +arr-union 3.1.0 - MIT +https://github.com/jonschlinkert/arr-union +Copyright (c) 2014-2016, Jon Schlinkert. +Copyright (c) 2016 Jon Schlinkert (https://github.com/jonschlinkert) + +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +array-from 2.1.1 - MIT +https://github.com/studio-b12/array-from#readme +(c) Studio B12 GmbH +Copyright (c) 2015-2016 Studio B12 GmbH + +Copyright © 2015-2016 Studio B12 GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +array-slice 1.1.0 - MIT +https://github.com/jonschlinkert/array-slice +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +array-uniq 1.0.3 - MIT +https://github.com/sindresorhus/array-uniq#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +array-unique 0.3.2 - MIT +https://github.com/jonschlinkert/array-unique +Copyright (c) 2014-2016, Jon Schlinkert +Copyright (c) 2014-2015, Jon Schlinkert. +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +arraybuffer.slice 0.0.7 - MIT +https://github.com/rase-/arraybuffer.slice +Copyright (c) 2013 Rase + +Copyright (C) 2013 Rase- + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +asap 2.0.6 - MIT +https://github.com/kriskowal/asap#readme +Copyright 2009-2014 + + +Copyright 2009–2014 Contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +asn1 0.2.4 - MIT +https://github.com/joyent/node-asn1#readme +Copyright (c) 2011 Mark Cavage +Copyright 2011 Mark Cavage + +Copyright (c) 2011 Mark Cavage, All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +assert-plus 1.0.0 - MIT +https://github.com/mcavage/node-assert-plus#readme +Copyright 2015 Joyent, Inc. +Copyright (c) 2012 Mark Cavage +Copyright (c) 2012, Mark Cavage. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +assertion-error 1.1.0 - MIT +https://github.com/chaijs/assertion-error#readme +Copyright (c) 2013 Jake Luer +Copyright (c) 2013 Jake Luer (http://qualiancy.com) + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +assign-symbols 1.0.0 - MIT +https://github.com/jonschlinkert/assign-symbols +Copyright (c) 2015 Jon Schlinkert +Copyright (c) 2015, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2015, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ast-transform 0.0.0 - MIT +https://github.com/hughsk/ast-transform +Copyright (c) 2014 Hugh Kennedy + +## The MIT License (MIT) ## + +Copyright (c) 2014 Hugh Kennedy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ast-types 0.7.8 - MIT +http://github.com/benjamn/ast-types +Copyright (c) 2013 Ben Newman + +Copyright (c) 2013 Ben Newman + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +async 2.6.2 - MIT +https://caolan.github.io/async/ +Copyright (c) 2010-2018 Caolan McMahon + +Copyright (c) 2010-2018 Caolan McMahon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +async-each 1.0.3 - MIT +https://github.com/paulmillr/async-each/ +Copyright (c) 2016 Paul Miller (paulmillr.com) (http://paulmillr.com) + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +async-limiter 1.0.0 - MIT +https://github.com/strml/async-limiter#readme +Copyright (c) 2017 Samuel Reed + +The MIT License (MIT) +Copyright (c) 2017 Samuel Reed + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +asynckit 0.4.0 - MIT +https://github.com/alexindigo/asynckit#readme +Copyright (c) 2016 Alex Indigo + +The MIT License (MIT) + +Copyright (c) 2016 Alex Indigo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +autoprefixer 7.2.6 - MIT +https://github.com/postcss/autoprefixer#readme +Copyright 2013 Andrey Sitnik + +The MIT License (MIT) + +Copyright 2013 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +aws4 1.8.0 - MIT +https://github.com/mhart/aws4#readme +Copyright 2013 Michael Hart (michael.hart.au@gmail.com) + +Copyright 2013 Michael Hart (michael.hart.au@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +axios 0.19.2 - MIT +https://github.com/axios/axios +Copyright (c) 2014-present Matt Zabriskie + +Copyright (c) 2014-present Matt Zabriskie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +babel-runtime 6.26.0 - MIT + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +backbone 1.2.3 - MIT +https://github.com/jashkenas/backbone +(c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors Backbone + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +backo2 1.0.2 - MIT +https://github.com/mokesmokes/backo + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +backports.functools-lru-cache 1.6.1 - MIT +Copyright Jason R. Coombs + +Copyright Jason R. Coombs + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +bail 1.0.5 - MIT +https://github.com/wooorm/bail#readme +(c) Titus Wormer +Copyright (c) 2015 Titus Wormer + +(The MIT License) + +Copyright (c) 2015 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +balanced-match 0.4.2 - MIT +https://github.com/juliangruber/balanced-match +Copyright (c) 2013 Julian Gruber + +(MIT) + +Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +balanced-match 1.0.0 - MIT +https://github.com/juliangruber/balanced-match +Copyright (c) 2013 Julian Gruber + +(MIT) + +Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +balanced-match 0.1.0 - MIT +https://github.com/juliangruber/balanced-match +Copyright (c) 2013 Julian Gruber + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +base 0.11.2 - MIT +https://github.com/node-base/base +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +base64-arraybuffer 0.1.5 - MIT +https://github.com/niklasvh/base64-arraybuffer +Copyright (c) 2012 Niklas von Hertzen + +Copyright (c) 2012 Niklas von Hertzen + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +base64-js 0.0.8 - MIT +https://github.com/beatgammit/base64-js +Copyright (c) 2014 + +The MIT License (MIT) + +Copyright (c) 2014 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +base64-js 1.3.0 - MIT +https://github.com/beatgammit/base64-js +Copyright (c) 2014 + +The MIT License (MIT) + +Copyright (c) 2014 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +base64id 2.0.0 - MIT +https://github.com/faeldt/base64id#readme +Copyright (c) 2012-2016 Kristian Faeldt + +(The MIT License) + +Copyright (c) 2012-2016 Kristian Faeldt + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +better-assert 1.0.2 - MIT +https://github.com/visionmedia/better-assert +Copyright (c) 2012 TJ Holowaychuk + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +binary-extensions 1.13.1 - MIT +https://github.com/sindresorhus/binary-extensions#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) +(c) Sindre Sorhus (https://sindresorhus.com), Paul Miller (https://paulmillr.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +bl 4.0.2 - MIT +https://github.com/rvagg/bl +Copyright (c) 2013-2019 bl contributors + +The MIT License (MIT) +===================== + +Copyright (c) 2013-2019 bl contributors +---------------------------------- + +*bl contributors listed at * + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +blob 0.0.5 - MIT +https://github.com/webmodules/blob +Copyright (c) 2014 Rase + +MIT License + +Copyright (C) 2014 Rase- + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +bluebird 3.5.5 - MIT +https://github.com/petkaantonov/bluebird +Copyright (c) 2013-2017 Petka Antonov +Copyright (c) 2013-2018 Petka Antonov + +The MIT License (MIT) + +Copyright (c) 2013-2018 Petka Antonov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +brace-expansion 1.1.11 - MIT +https://github.com/juliangruber/brace-expansion +Copyright (c) 2013 Julian Gruber + +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +braces 2.3.2 - MIT +https://github.com/micromatch/braces +Copyright (c) 2014-2018, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +braces 3.0.2 - MIT +https://github.com/micromatch/braces +Copyright (c) 2014-2018, Jon Schlinkert. +Copyright (c) 2019, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +brfs 1.6.1 - MIT +https://github.com/substack/brfs + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +brfs 2.0.2 - MIT +https://github.com/substack/brfs + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +brotli 1.3.2 - MIT +https://github.com/devongovett/brotli.js +Copyright 2013 Google Inc. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +browser-resolve 1.11.3 - MIT +https://github.com/shtylman/node-browser-resolve#readme +Copyright (c) 2013-2015 Roman Shtylman + +The MIT License (MIT) + +Copyright (c) 2013-2015 Roman Shtylman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +browserify-mime 1.2.9 - MIT +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +browserify-optional 1.0.1 - MIT +https://github.com/devongovett/browserify-optional + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +browserslist 2.11.3 - MIT +https://github.com/ai/browserslist#readme +Copyright 2014 Andrey Sitnik + +The MIT License (MIT) + +Copyright 2014 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +buffer 4.9.2 - MIT +https://github.com/feross/buffer +Copyright (c) Feross Aboukhadijeh, and other contributors. +Copyright (c) Feross Aboukhadijeh (http://feross.org), and other contributors. + +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh, and other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +buffer 5.6.0 - MIT +https://github.com/feross/buffer +Copyright (c) Feross Aboukhadijeh, and other contributors. +Copyright (c) Feross Aboukhadijeh (http://feross.org), and other contributors. + +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh, and other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +buffer-crc32 0.2.13 - MIT +https://github.com/brianloveswords/buffer-crc32 +Copyright (c) 2013 Brian J. Brennan + +The MIT License + +Copyright (c) 2013 Brian J. Brennan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +buffer-equal 0.0.1 - MIT +https://github.com/substack/node-buffer-equal + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +buffer-equal 1.0.0 - MIT +https://github.com/substack/node-buffer-equal#readme + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +buffer-from 1.1.1 - MIT +https://github.com/LinusU/buffer-from#readme +Copyright (c) 2016, 2018 Linus Unneback + +MIT License + +Copyright (c) 2016, 2018 Linus Unnebäck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +buffer-json 2.0.0 - MIT +https://github.com/jprichardson/buffer-json#readme + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +bytes 3.1.0 - MIT +https://github.com/visionmedia/bytes.js#readme +Copyright (c) 2015 Jed Watson +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson +Copyright (c) 2012-2014 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cache-base 1.0.1 - MIT +https://github.com/jonschlinkert/cache-base +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cache-loader 4.1.0 - MIT +https://github.com/webpack-contrib/cache-loader +Copyright JS Foundation and other contributors + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cacheable-request 6.1.0 - MIT +https://github.com/lukechilds/cacheable-request#readme +(c) Luke Childs +Copyright (c) 2017 Luke Childs + +MIT License + +Copyright (c) 2017 Luke Childs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +caching-transform 4.0.0 - MIT +https://github.com/istanbuljs/caching-transform#readme +Copyright (c) James Talmage +(c) James Talmage (https://github.com/jamestalmage) + +The MIT License (MIT) + +Copyright (c) James Talmage (github.com/jamestalmage) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +camelcase 5.3.1 - MIT +https://github.com/sindresorhus/camelcase#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +camelcase 3.0.0 - MIT +https://github.com/sindresorhus/camelcase#readme +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +caniuse-api 2.0.0 - MIT +https://github.com/nyalab/caniuse-api#readme +Copyright (c) 2014 Sebastien Balayn + +The MIT License (MIT) + +Copyright (c) 2014 Sébastien Balayn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +canvas 2.6.0 - MIT +https://github.com/Automattic/node-canvas +copyright On Windows +Copyright (c) 2010 LearnBoost +Copyright (c) 2011 LearnBoost +Copyright (c) 2010 LearnBoost, and contributors +Copyright (c) 2014 Automattic, Inc and contributors + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +chai 4.2.0 - MIT +http://chaijs.com +Copyright (c) 2013 +Copyright (c) 2017 Chai.js Assertion Library +Copyright (c) 2013 Jake Luer +Copyright (c) 2011 Jake Luer +Copyright (c) 2013 Jake Luer +Copyright (c) 2011-2014 Jake Luer +Copyright (c) 2011-2016 Jake Luer +Copyright (c) 2012-2014 Jake Luer +Copyright (c) 2012-2016 Jake Luer +Copyright (c) 2012-2015 Sakthipriyan Vairamani + +MIT License + +Copyright (c) 2017 Chai.js Assertion Library + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +chai-http 4.3.0 - MIT +https://github.com/chaijs/chai-http#readme +Copyright (c) 2013-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson +Copyright (c) Jake Luer +Copyright Joyent, Inc. and other Node contributors. +Copyright (c) 2011-2012 Jake Luer + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +chalk 2.4.2 - MIT +https://github.com/chalk/chalk#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +character-entities 1.2.4 - MIT +https://github.com/wooorm/character-entities#readme +(c) Titus Wormer +Copyright (c) 2015 Titus Wormer + +(The MIT License) + +Copyright (c) 2015 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +character-entities-legacy 1.1.4 - MIT +https://github.com/wooorm/character-entities-legacy#readme +(c) Titus Wormer +Copyright (c) 2015 Titus Wormer + +(The MIT License) + +Copyright (c) 2015 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +character-reference-invalid 1.1.4 - MIT +https://github.com/wooorm/character-reference-invalid#readme +(c) Titus Wormer +Copyright (c) 2015 Titus Wormer + +(The MIT License) + +Copyright (c) 2015 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +check-error 1.0.2 - MIT +https://github.com/chaijs/check-error#readme +Copyright (c) 2012-2016 Jake Luer +Copyright (c) 2013 Jake Luer (http://alogicalparadox.com) + +Copyright (c) 2013 Jake Luer (http://alogicalparadox.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cheerio 1.0.0-rc.3 - MIT +https://github.com/cheeriojs/cheerio#readme +Copyright (c) 2016 Matt Mueller + +MIT License + +Copyright (c) 2016 Matt Mueller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +chokidar 2.1.8 - MIT +https://github.com/paulmillr/chokidar +Copyright (c) 2012-2019 Paul Miller (https://paulmillr.com) & Elan Shanker + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +class-utils 0.3.6 - MIT +https://github.com/jonschlinkert/class-utils +Copyright (c) 2015, 2017-2018, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015, 2017-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +classnames 2.2.6 - MIT +https://github.com/JedWatson/classnames#readme +Copyright (c) 2017 Jed Watson. + +The MIT License (MIT) + +Copyright (c) 2017 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +clean-stack 2.2.0 - MIT +https://github.com/sindresorhus/clean-stack#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +clone 2.1.2 - MIT +https://github.com/pvorb/node-clone#readme +Copyright (c) 2011-2015 Paul Vorbach +Copyright (c) 2011-2016 Paul Vorbach (https://paul.vorba.ch/) and contributors (https://github.com/pvorb/clone/graphs/contributors). + +Copyright © 2011-2015 Paul Vorbach + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +clone 1.0.4 - MIT +https://github.com/pvorb/node-clone#readme +Copyright (c) 2011-2015 Paul Vorbach +Copyright (c) 2011-2015 Paul Vorbach (http://paul.vorba.ch/) and contributors (https://github.com/pvorb/node-clone/graphs/contributors). + +Copyright © 2011-2015 Paul Vorbach + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +clone-buffer 1.0.0 - MIT +https://github.com/gulpjs/clone-buffer#readme +Copyright (c) 2016 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2016 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +clone-response 1.0.2 - MIT +https://github.com/lukechilds/clone-response +(c) Luke Childs +Copyright (c) 2017 Luke Childs + +MIT License + +Copyright (c) 2017 Luke Childs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +clone-stats 1.0.0 - MIT +https://github.com/hughsk/clone-stats +Copyright (c) 2014 Hugh Kennedy + +## The MIT License (MIT) ## + +Copyright (c) 2014 Hugh Kennedy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +clone-stats 0.0.1 - MIT +https://github.com/hughsk/clone-stats +Copyright (c) 2014 Hugh Kennedy + +## The MIT License (MIT) ## + +Copyright (c) 2014 Hugh Kennedy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cloneable-readable 1.1.3 - MIT +https://github.com/mcollina/cloneable-readable#readme +Copyright (c) 2016 Matteo Collina + +The MIT License (MIT) + +Copyright (c) 2016 Matteo Collina + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +clsx 1.0.4 - MIT +https://github.com/lukeed/clsx#readme +(c) Luke Edwards (https://lukeed.com) +Copyright (c) Luke Edwards (lukeed.com) + +MIT License + +Copyright (c) Luke Edwards (lukeed.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +co 4.6.0 - MIT +https://github.com/tj/co#readme +Copyright (c) 2014 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2014 TJ Holowaychuk <tj@vision-media.ca> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +code-point-at 1.1.0 - MIT +https://github.com/sindresorhus/code-point-at#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +codemirror 5.47.0 - MIT +https://codemirror.net +copyright (c) by Lennart Ochel +copyright AtomicPages LLC 2014 +copyright (c) 2015 by Calin Barbat +copyright (c) HicknHack Software Gmbh +Copyright (c) 2011 by MarkLogic Corporation +copyright (c) 2015 by Grzegorz Mazur Loosely +copyright (c) 2016 Jared Dean, SAS Institute +copyright (c) by Marijn Haverbeke and others +Copyright (c) 2017 by Marijn Haverbeke and others + +MIT License + +Copyright (C) 2017 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +collapse-white-space 1.0.6 - MIT +https://github.com/wooorm/collapse-white-space#readme +(c) Titus Wormer +Copyright (c) 2015 Titus Wormer + +(The MIT License) + +Copyright (c) 2015 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +collection-visit 1.0.0 - MIT +https://github.com/jonschlinkert/collection-visit +Copyright (c) 2015, 2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015, 2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +color 0.11.4 - MIT +https://github.com/qix-/color#readme +Copyright (c) 2012 Heather Arthur + +Copyright (c) 2012 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +color 2.0.1 - MIT +https://github.com/Qix-/color#readme +Copyright (c) 2012 Heather Arthur + +Copyright (c) 2012 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +color 1.0.3 - MIT +https://github.com/qix-/color#readme +Copyright (c) 2012 Heather Arthur + +Copyright (c) 2012 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +color 3.0.0 - MIT +https://github.com/Qix-/color#readme +Copyright (c) 2012 Heather Arthur + +Copyright (c) 2012 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +color-convert 2.0.1 - MIT +https://github.com/Qix-/color-convert#readme +Copyright (c) 2011-2016, Heather Arthur and Josh Junon. +Copyright (c) 2011-2016 Heather Arthur + +Copyright (c) 2011-2016 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +color-convert 1.9.3 - MIT +https://github.com/Qix-/color-convert#readme +Copyright (c) 2011-2016, Heather Arthur and Josh Junon. +Copyright (c) 2011-2016 Heather Arthur + +Copyright (c) 2011-2016 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +color-name 1.1.3 - MIT +https://github.com/dfcreative/color-name +Copyright (c) 2015 Dmitry Ivanov + +The MIT License (MIT) +Copyright (c) 2015 Dmitry Ivanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +color-name 1.1.4 - MIT +https://github.com/colorjs/color-name +Copyright (c) 2015 Dmitry Ivanov + +The MIT License (MIT) +Copyright (c) 2015 Dmitry Ivanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +color-string 1.5.3 - MIT +https://github.com/Qix-/color-string#readme +Copyright (c) 2011 Heather Arthur + +Copyright (c) 2011 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +color-string 0.3.0 - MIT +https://github.com/harthur/color-string +Copyright (c) 2011 Heather Arthur + +Copyright (c) 2011 Heather Arthur + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +colornames 1.1.1 - MIT +https://github.com/timoxley/colornames#readme +Copyright (c) 2015 Tim Oxley + +The MIT License (MIT) + +Copyright (c) 2015 Tim Oxley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +colors 1.3.3 - MIT +https://github.com/Marak/colors.js +Copyright (c) Marak Squires +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Original Library + - Copyright (c) Marak Squires + +Additional Functionality + - Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +colorspace 1.1.2 - MIT +https://github.com/3rd-Eden/colorspace +Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman + +The MIT License (MIT) + +Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman, the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +combined-stream 1.0.8 - MIT +https://github.com/felixge/node-combined-stream +Copyright (c) 2011 Debuggable Limited + +Copyright (c) 2011 Debuggable Limited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +commander 2.20.0 - MIT +https://github.com/tj/commander.js#readme +Copyright (c) 2011 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2011 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +commander 4.1.1 - MIT +https://github.com/tj/commander.js#readme +Copyright (c) 2011 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2011 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +commander 2.20.3 - MIT +https://github.com/tj/commander.js#readme +Copyright (c) 2011 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2011 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +component-bind 1.0.0 - MIT +https://github.com/component/bind + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +component-emitter 1.2.1 - MIT +https://github.com/component/emitter#readme +Copyright (c) 2014 Component + +(The MIT License) + +Copyright (c) 2014 Component contributors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +component-emitter 1.3.0 - MIT +https://github.com/component/emitter#readme +Copyright (c) 2014 Component + +(The MIT License) + +Copyright (c) 2014 Component contributors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +component-inherit 0.0.3 - MIT +https://github.com/component/inherit + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +concat-map 0.0.1 - MIT +https://github.com/substack/node-concat-map + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +concat-stream 1.6.2 - MIT +https://github.com/maxogden/concat-stream#readme +Copyright (c) 2013 Max Ogden + +The MIT License + +Copyright (c) 2013 Max Ogden + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +config-chain 1.1.12 - MIT +http://github.com/dominictarr/config-chain +Copyright (c) 2011 Dominic Tarr + +Copyright (c) 2011 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +console-browserify 1.2.0 - MIT +https://github.com/browserify/console-browserify +Copyright (c) 2012 Raynos. + +Copyright (c) 2012 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +content-disposition 0.5.3 - MIT +https://github.com/jshttp/content-disposition#readme +Copyright (c) 2014-2017 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +content-type 1.0.4 - MIT +https://github.com/jshttp/content-type#readme +Copyright (c) 2015 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +convert-source-map 1.6.0 - MIT +https://github.com/thlorenz/convert-source-map +Copyright 2013 Thorsten Lorenz. + +Copyright 2013 Thorsten Lorenz. +All rights reserved. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +convert-source-map 1.7.0 - MIT +https://github.com/thlorenz/convert-source-map +Copyright 2013 Thorsten Lorenz. + +Copyright 2013 Thorsten Lorenz. +All rights reserved. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cookie 0.3.1 - MIT +https://github.com/jshttp/cookie +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cookie-signature 1.0.6 - MIT +https://github.com/visionmedia/node-cookie-signature +Copyright (c) 2012 LearnBoost + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cookiejar 2.1.2 - MIT +https://github.com/bmeck/node-cookiejar#readme +Copyright (c) 2013 Bradley Meck + +The MIT License (MIT) +Copyright (c) 2013 Bradley Meck + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +copy-descriptor 0.1.1 - MIT +https://github.com/jonschlinkert/copy-descriptor +Copyright (c) 2015, Jon Schlinkert. +Copyright (c) 2015-2016, Jon Schlinkert + +The MIT License (MIT) + +Copyright (c) 2015-2016, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +core-js 1.2.7 - MIT +https://github.com/zloirock/core-js#readme +(c) 2016 Denis Pushkarev +Copyright (c) 2015 Denis Pushkarev + +Copyright (c) 2015 Denis Pushkarev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +core-js 2.6.9 - MIT +https://github.com/zloirock/core-js#readme +(c) 2019 Denis Pushkarev +copyright (c) 2019 Denis Pushkarev +Copyright (c) 2014-2019 Denis Pushkarev + +Copyright (c) 2014-2019 Denis Pushkarev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +core-util-is 1.0.2 - MIT +https://github.com/isaacs/core-util-is#readme +Copyright Joyent, Inc. and other Node contributors. + +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cors 2.8.5 - MIT +https://github.com/expressjs/cors#readme +Copyright (c) 2013 Troy Goode + +(The MIT License) + +Copyright (c) 2013 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +create-react-context 0.3.0 - MIT +https://github.com/thejameskyle/create-react-context#readme +Copyright (c) 2017-present James Kyle + +Copyright (c) 2017-present James Kyle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cross-env 6.0.3 - MIT +https://github.com/kentcdodds/cross-env#readme +Copyright (c) 2017 Kent C. Dodds + +The MIT License (MIT) +Copyright (c) 2017 Kent C. Dodds + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cross-spawn 7.0.1 - MIT +https://github.com/moxystudio/node-cross-spawn +Copyright (c) 2018 Made With MOXY Lda + +The MIT License (MIT) + +Copyright (c) 2018 Made With MOXY Lda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cross-spawn 5.1.0 - MIT +https://github.com/IndigoUnited/node-cross-spawn#readme +Copyright (c) 2014 IndigoUnited + +Copyright (c) 2014 IndigoUnited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cross-spawn 6.0.5 - MIT +https://github.com/moxystudio/node-cross-spawn +Copyright (c) 2018 Made With MOXY Lda + +The MIT License (MIT) + +Copyright (c) 2018 Made With MOXY Lda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +crypto-js 3.3.0 - MIT +http://github.com/brix/crypto-js +(c) 2012 by Cedric Mesnil. +Copyright (c) 2009-2013 Jeff Mott +Copyright (c) 2013-2016 Evan Vosberg + +# License + +[The MIT License (MIT)](http://opensource.org/licenses/MIT) + +Copyright (c) 2009-2013 Jeff Mott +Copyright (c) 2013-2016 Evan Vosberg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +css-color-function 1.3.3 - MIT +https://github.com/ianstormtaylor/css-color-function#readme +Copyright (c) 2013 Ian Storm Taylor + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +css-unit-converter 1.1.1 - MIT +https://github.com/andyjansson/css-unit-converter +Copyright 2015 Andy Jansson + +The MIT License (MIT) + +Copyright 2015 Andy Jansson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +csstype 2.6.6 - MIT +https://github.com/frenic/csstype#readme +Copyright (c) 2017-2018 Fredrik Nicol + +Copyright (c) 2017-2018 Fredrik Nicol + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +csstype 2.6.9 - MIT +https://github.com/frenic/csstype#readme +Copyright (c) 2017-2018 Fredrik Nicol + +Copyright (c) 2017-2018 Fredrik Nicol + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cyclist 1.0.1 - MIT +https://github.com/mafintosh/cyclist +Copyright (c) 2015 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2015 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +cyclist 0.2.2 - MIT + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +dashdash 1.14.1 - MIT +https://github.com/trentm/node-dashdash#readme +Copyright 2016 Trent Mick +Copyright 2016 Joyent, Inc. +Copyright (c) 2013 Joyent Inc. +Copyright (c) 2013 Trent Mick. + +# This is the MIT license + +Copyright (c) 2013 Trent Mick. All rights reserved. +Copyright (c) 2013 Joyent Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +date-format 3.0.0 - MIT +https://github.com/nomiddlename/date-format#readme +Copyright (c) 2013 Gareth Jones + +The MIT License (MIT) + +Copyright (c) 2013 Gareth Jones + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +date-format 2.1.0 - MIT +https://github.com/nomiddlename/date-format#readme +Copyright (c) 2013 Gareth Jones + +The MIT License (MIT) + +Copyright (c) 2013 Gareth Jones + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +debug 3.2.6 - MIT +https://github.com/visionmedia/debug#readme +Copyright (c) 2014 TJ Holowaychuk +Copyright (c) 2014-2017 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2014 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +debug 4.1.1 - MIT +https://github.com/visionmedia/debug#readme +Copyright (c) 2014 TJ Holowaychuk +Copyright (c) 2014-2017 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2014 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +debug 3.1.0 - MIT +https://github.com/visionmedia/debug#readme +Copyright (c) 2014 TJ Holowaychuk +Copyright (c) 2014-2017 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2014 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +debug 2.6.9 - MIT +https://github.com/visionmedia/debug#readme +Copyright (c) 2014 TJ Holowaychuk +Copyright (c) 2014-2016 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2014 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +debug 4.1.0 - MIT +https://github.com/visionmedia/debug#readme +Copyright (c) 2014 TJ Holowaychuk +Copyright (c) 2014-2017 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2014 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +decamelize 1.2.0 - MIT +https://github.com/sindresorhus/decamelize#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +decode-uri-component 0.2.0 - MIT +https://github.com/samverschueren/decode-uri-component#readme +(c) Sam Verschueren (https://github.com/SamVerschueren) +Copyright (c) Sam Verschueren + +The MIT License (MIT) + +Copyright (c) Sam Verschueren (github.com/SamVerschueren) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +decompress 4.2.1 - MIT +https://github.com/kevva/decompress#readme +(c) Kevin Martensson (https://github.com/kevva) +Copyright (c) Kevin Martensson + +MIT License + +Copyright (c) Kevin Mårtensson (github.com/kevva) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +decompress-response 4.2.1 - MIT +https://github.com/sindresorhus/decompress-response#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +decompress-response 3.3.0 - MIT +https://github.com/sindresorhus/decompress-response#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +`The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +dedent 0.7.0 - MIT +https://github.com/dmnd/dedent +Copyright (c) 2015 Desmond Brand (dmnd@desmondbrand.com) + +The MIT License (MIT) + +Copyright (c) 2015 Desmond Brand (dmnd@desmondbrand.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +deemon 1.4.0 - MIT +https://github.com/joaomoreno/deemon#readme + +MIT License + +Copyright (c) 2020 João Moreno + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +deep-assign 1.0.0 - MIT +https://github.com/sindresorhus/deep-assign#readme +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +deep-diff 0.3.8 - MIT +https://github.com/flitbit/diff#readme +Copyright (c) 2011-2013 Phillip Clark + +Copyright (c) 2011-2013 Phillip Clark + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +deep-eql 3.0.1 - MIT +https://github.com/chaijs/deep-eql#readme +Copyright (c) 2013 +Copyright (c) 2013 Jake Luer +Copyright (c) 2013 Jake Luer (http://alogicalparadox.com) + +Copyright (c) 2013 Jake Luer (http://alogicalparadox.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +deep-equal 1.1.1 - MIT +https://github.com/substack/node-deep-equal#readme +Copyright (c) 2012, 2013, 2014 James Halliday , 2009 Thomas Robinson <280north.com> + +MIT License + +Copyright (c) 2012, 2013, 2014 James Halliday , 2009 Thomas Robinson <280north.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +deep-equal 1.0.1 - MIT +https://github.com/substack/node-deep-equal#readme + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +deep-extend 0.6.0 - MIT +https://github.com/unclechu/node-deep-extend +Copyright (c) 2013-2018 Viacheslav Lotsmanov +Copyright (c) 2013-2018, Viacheslav Lotsmanov + +The MIT License (MIT) + +Copyright (c) 2013-2018, Viacheslav Lotsmanov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +deep-is 0.1.3 - MIT +https://github.com/thlorenz/deep-is +Copyright (c) 2009 Thomas Robinson <280north.com> +Copyright (c) 2012 James Halliday +Copyright (c) 2012, 2013 Thorsten Lorenz + +Copyright (c) 2012, 2013 Thorsten Lorenz +Copyright (c) 2012 James Halliday +Copyright (c) 2009 Thomas Robinson <280north.com> + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +default-require-extensions 3.0.0 - MIT +https://github.com/avajs/default-require-extensions#readme +Copyright (c) Node.js contributors, James Talmage + +MIT License + +Copyright (c) Node.js contributors, James Talmage (github.com/jamestalmage) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +defer-to-connect 1.1.3 - MIT +https://github.com/szmarczak/defer-to-connect#readme +Copyright (c) 2018 Szymon Marczak + +MIT License + +Copyright (c) 2018 Szymon Marczak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +define-properties 1.1.3 - MIT +https://github.com/ljharb/define-properties#readme +Copyright (c) 2015 Jordan Harband + +The MIT License (MIT) + +Copyright (C) 2015 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +define-property 1.0.0 - MIT +https://github.com/jonschlinkert/define-property +Copyright (c) 2015, 2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015, 2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +define-property 2.0.2 - MIT +https://github.com/jonschlinkert/define-property +Copyright (c) 2015-2018, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +define-property 0.2.5 - MIT +https://github.com/jonschlinkert/define-property +Copyright (c) 2015 Jon Schlinkert +Copyright (c) 2015, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2015, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +delayed-stream 1.0.0 - MIT +https://github.com/felixge/node-delayed-stream +Copyright (c) 2011 Debuggable Limited + +Copyright (c) 2011 Debuggable Limited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +delegates 1.0.0 - MIT +https://github.com/visionmedia/node-delegates#readme +Copyright (c) 2015 TJ Holowaychuk + +Copyright (c) 2015 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +depd 1.1.2 - MIT +https://github.com/dougwilson/nodejs-depd#readme +Copyright (c) 2014 Douglas Christopher Wilson +Copyright (c) 2015 Douglas Christopher Wilson +Copyright (c) 2014-2015 Douglas Christopher Wilson +Copyright (c) 2014-2017 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +des.js 1.0.1 - MIT +https://github.com/indutny/des.js#readme +Copyright Fedor Indutny, 2015. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +destroy 1.0.4 - MIT +https://github.com/stream-utils/destroy +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +detect-file 1.0.0 - MIT +https://github.com/doowb/detect-file +Copyright (c) 2016-2017, Brian Woodward. +Copyright (c) 2017, Brian Woodward (https://github.com/doowb). + +The MIT License (MIT) + +Copyright (c) 2016-2017, Brian Woodward. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +detect-indent 6.0.0 - MIT +https://github.com/sindresorhus/detect-indent#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +detect-newline 2.1.0 - MIT +https://github.com/sindresorhus/detect-newline#readme +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +dfa 1.2.0 - MIT +https://github.com/devongovett/dfa#readme + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +diagnostic-channel 0.2.0 - MIT +https://github.com/Microsoft/node-diagnostic-channel +Copyright (c) Microsoft Corporation. + + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +diagnostics 1.1.1 - MIT +https://github.com/bigpipe/diagnostics +Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman + +The MIT License (MIT) + +Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman, the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +dns-packet 5.2.1 - MIT +https://github.com/mafintosh/dns-packet +Copyright (c) 2016 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2016 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +dns-socket 4.2.0 - MIT +https://github.com/mafintosh/dns-socket +Copyright (c) 2016 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2016 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +dom-helpers 3.4.0 - MIT +https://github.com/jquense/dom-helpers#readme +(c) 2010-2015 Thomas Fuchs +Copyright (c) 2015 Jason Quense +Copyright 2013-2014, Facebook, Inc. +Copyright 2014-2015, Facebook, Inc. + +The MIT License (MIT) + +Copyright (c) 2015 Jason Quense + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +dom-serializer 0.1.1 - MIT +https://github.com/cheeriojs/dom-renderer#readme +Copyright (c) 2014 + +License + +(The MIT License) + +Copyright (c) 2014 The cheeriojs contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +dom-serializer 0.2.2 - MIT +https://github.com/cheeriojs/dom-renderer#readme +Copyright (c) 2014 + +License + +(The MIT License) + +Copyright (c) 2014 The cheeriojs contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +dom4 2.1.5 - MIT +https://github.com/WebReflection/dom4 +(c) Andrea Giammarchi +Copyright (c) 2013-2015 by Andrea Giammarchi + +Copyright (C) 2013-2015 by Andrea Giammarchi - @WebReflection + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +duplexer 0.1.1 - MIT +https://github.com/Raynos/duplexer +Copyright (c) 2012 Raynos. + +Copyright (c) 2012 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +duplexify 3.7.1 - MIT +https://github.com/mafintosh/duplexify +Copyright (c) 2014 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ecc-jsbn 0.1.2 - MIT +https://github.com/quartzjer/ecc-jsbn +Copyright (c) 2003-2005 Tom Wu +Copyright (c) 2014 Jeremie Miller + +The MIT License (MIT) + +Copyright (c) 2014 Jeremie Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ee-first 1.1.1 - MIT +https://github.com/jonathanong/ee-first +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +emoji-regex 7.0.3 - MIT +https://mths.be/emoji-regex +Copyright Mathias Bynens + +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +emoji-regex 8.0.0 - MIT +https://mths.be/emoji-regex +Copyright Mathias Bynens + +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +emojis-list 3.0.0 - MIT +https://nidecoc.io/Kikobeats/emojis-list +Copyright (c) 2015 Kiko Beats +(c) Kiko Beats (http://www.kikobeats.com) + +The MIT License (MIT) + +Copyright © 2015 Kiko Beats + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +enabled 1.0.2 - MIT +https://github.com/bigpipe/enabled#readme +Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman + +The MIT License (MIT) + +Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman, the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +encodeurl 1.0.2 - MIT +https://github.com/pillarjs/encodeurl#readme +Copyright (c) 2016 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +encoding 0.1.12 - MIT +https://github.com/andris9/encoding#readme +Copyright (c) 2012-2014 Andris Reinman + +Copyright (c) 2012-2014 Andris Reinman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +end-of-stream 1.4.4 - MIT +https://github.com/mafintosh/end-of-stream +Copyright (c) 2014 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +end-of-stream 1.4.1 - MIT +https://github.com/mafintosh/end-of-stream +Copyright (c) 2014 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +engine.io 3.4.0 - MIT +https://github.com/socketio/engine.io +Copyright (c) 2014 Guillermo Rauch + +(The MIT License) + +Copyright (c) 2014 Guillermo Rauch + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +engine.io-client 3.4.0 - MIT +https://github.com/socketio/engine.io-client +Copyright (c) 2014 Automattic, Inc. +Copyright (c) 2012 Niklas von Hertzen +Copyright (c) 2014-2015 Automattic + +(The MIT License) + +Copyright (c) 2014-2015 Automattic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +engine.io-parser 2.2.0 - MIT +https://github.com/socketio/engine.io-parser +Copyright (c) 2016 Guillermo Rauch + +(The MIT License) + +Copyright (c) 2016 Guillermo Rauch (@rauchg) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +env-variable 0.0.5 - MIT +https://github.com/3rd-Eden/env-variable +Copyright 2014 Arnout Kazemier + +Copyright 2014 Arnout Kazemier + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +errno 0.1.7 - MIT +https://github.com/rvagg/node-errno#readme +Copyright (c) 2012-2015 Rod Vagg (https://github.com/rvagg) ( rvagg (https://twitter.com/rvagg)) + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +error-ex 1.3.2 - MIT +https://github.com/qix-/node-error-ex#readme +Copyright (c) 2015 JD Ballard + +The MIT License (MIT) + +Copyright (c) 2015 JD Ballard + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +es-abstract 1.17.0 - MIT +https://github.com/ljharb/es-abstract#readme +(c) Object C +Copyright (c) 2015 Jordan Harband + +The MIT License (MIT) + +Copyright (C) 2015 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +es-to-primitive 1.2.0 - MIT +https://github.com/ljharb/es-to-primitive#readme +Copyright (c) 2015 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2015 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +es-to-primitive 1.2.1 - MIT +https://github.com/ljharb/es-to-primitive#readme +Copyright (c) 2015 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2015 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +es6-iterator 2.0.3 - MIT +https://github.com/medikoo/es6-iterator#readme +Copyright (c) 2013-2017 Mariusz Nowak (www.medikoo.com) + +The MIT License (MIT) + +Copyright (C) 2013-2017 Mariusz Nowak (www.medikoo.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +es6-map 0.1.5 - MIT +https://github.com/medikoo/es6-map#readme +Copyright (c) 2013 Mariusz Nowak (www.medikoo.com) + +Copyright (C) 2013 Mariusz Nowak (www.medikoo.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +es6-promise 4.2.8 - MIT +https://github.com/stefanpenner/es6-promise +Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors + +Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +es6-promisify 5.0.0 - MIT +https://github.com/digitaldesignlabs/es6-promisify#readme + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +es6-set 0.1.5 - MIT +https://github.com/medikoo/es6-set#readme +Copyright (c) 2013 Mariusz Nowak (www.medikoo.com) + +Copyright (C) 2013 Mariusz Nowak (www.medikoo.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +es6-symbol 3.1.1 - MIT +https://github.com/medikoo/es6-symbol#readme +Copyright (c) 2013-2015 Mariusz Nowak (www.medikoo.com) + +Copyright (C) 2013-2015 Mariusz Nowak (www.medikoo.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +escape-html 1.0.3 - MIT +https://github.com/component/escape-html +Copyright (c) 2015 Andreas Lubbe +Copyright (c) 2012-2013 TJ Holowaychuk +Copyright (c) 2015 Tiancheng Timothy Gu + +(The MIT License) + +Copyright (c) 2012-2013 TJ Holowaychuk +Copyright (c) 2015 Andreas Lubbe +Copyright (c) 2015 Tiancheng "Timothy" Gu + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +escape-string-regexp 1.0.5 - MIT +https://github.com/sindresorhus/escape-string-regexp +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +eslint-config-prettier 6.9.0 - MIT +https://github.com/prettier/eslint-config-prettier#readme +Copyright (c) 2017, 2018, 2019 Simon Lydell and contributors + +The MIT License (MIT) + +Copyright (c) 2017, 2018, 2019 Simon Lydell and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +eslint-plugin-prettier 3.1.2 - MIT +https://github.com/prettier/eslint-plugin-prettier#readme +Copyright (c) 2017 Andres Suarez and Teddy Katz + +# The MIT License (MIT) + +Copyright © 2017 Andres Suarez and Teddy Katz + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +eslint-plugin-prettier 2.7.0 - MIT +https://github.com/prettier/eslint-plugin-prettier#readme +Copyright (c) 2017 Andres Suarez and Teddy Katz + +The MIT License (MIT) +===================== + +Copyright © 2017 Andres Suarez and Teddy Katz + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +event-emitter 0.3.5 - MIT +https://github.com/medikoo/event-emitter#readme +Copyright (c) 2012-2015 Mariusz Nowak (www.medikoo.com) + +Copyright (C) 2012-2015 Mariusz Nowak (www.medikoo.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +event-stream 3.3.4 - MIT +http://github.com/dominictarr/event-stream +Copyright (c) 2011 Dominic Tarr + +The MIT License (MIT) + +Copyright (c) 2011 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +execa 1.0.0 - MIT +https://github.com/sindresorhus/execa#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +expand-brackets 2.1.4 - MIT +https://github.com/jonschlinkert/expand-brackets +Copyright (c) 2015-2016, Jon Schlinkert +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2016, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +expand-tilde 2.0.2 - MIT +https://github.com/jonschlinkert/expand-tilde +Copyright (c) 2015 Jon Schlinkert. +Copyright (c) 2015-2016, Jon Schlinkert. +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +extend 3.0.2 - MIT +https://github.com/justmoon/node-extend#readme +Copyright (c) 2014 Stefan Thomas + +The MIT License (MIT) + +Copyright (c) 2014 Stefan Thomas + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +extend-shallow 2.0.1 - MIT +https://github.com/jonschlinkert/extend-shallow +Copyright (c) 2015 Jon Schlinkert +Copyright (c) 2014-2015, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2014-2015, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +extend-shallow 3.0.2 - MIT +https://github.com/jonschlinkert/extend-shallow +Copyright (c) 2014-2015, 2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2015, 2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +extglob 2.0.4 - MIT +https://github.com/micromatch/extglob +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +extsprintf 1.3.0 - MIT +https://github.com/davepacheco/node-extsprintf +Copyright (c) 2012, Joyent, Inc. + +Copyright (c) 2012, Joyent, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +falafel 2.2.0 - MIT +https://github.com/substack/node-falafel#readme + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fast-deep-equal 2.0.1 - MIT +https://github.com/epoberezkin/fast-deep-equal#readme +Copyright (c) 2017 Evgeny Poberezkin + +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fast-deep-equal 3.1.1 - MIT +https://github.com/epoberezkin/fast-deep-equal#readme +Copyright (c) 2017 Evgeny Poberezkin + +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fast-json-stable-stringify 2.0.0 - MIT +https://github.com/epoberezkin/fast-json-stable-stringify + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fast-levenshtein 2.0.6 - MIT +https://github.com/hiddentao/fast-levenshtein#readme +Copyright (c) 2013 Ramesh Nair (http://www.hiddentao.com/) + +(MIT License) + +Copyright (c) 2013 [Ramesh Nair](http://www.hiddentao.com/) + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fast-plist 0.1.2 - MIT +https://github.com/Microsoft/node-fast-plist#readme +Copyright (c) Microsoft Corporation. + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fast-safe-stringify 2.0.6 - MIT +https://github.com/davidmarkclements/fast-safe-stringify#readme +Copyright (c) 2016 David Mark Clements +Copyright (c) 2017 David Mark Clements & Matteo Collina +Copyright (c) 2018 David Mark Clements, Matteo Collina & Ruben Bridgewater + +The MIT License (MIT) + +Copyright (c) 2016 David Mark Clements +Copyright (c) 2017 David Mark Clements & Matteo Collina +Copyright (c) 2018 David Mark Clements, Matteo Collina & Ruben Bridgewater + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fast-xml-parser 3.16.0 - MIT +https://github.com/NaturalIntelligence/fast-xml-parser#readme +Copyright (c) 2017 Amit Kumar Gupta +Copyright 2013 Timothy J Fontaine + +MIT License + +Copyright (c) 2017 Amit Kumar Gupta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +If you use this library in a public repository then you give us the right to mention your company name and logo in user's list without further permission required, but you can request them to be taken down within 30 days. + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fbjs 0.8.17 - MIT +https://github.com/facebook/fbjs#readme +Copyright 2015-present Facebook. +copyright 2014-2015 Jon Schlinkert +Copyright (c) 2013-present, Facebook, Inc. +Copyright (c) 2014-present, Facebook, Inc. +copyright 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. + +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fd-slicer 1.1.0 - MIT +https://github.com/andrewrk/node-fd-slicer#readme +Copyright (c) 2014 Andrew Kelley + +Copyright (c) 2014 Andrew Kelley + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fecha 2.3.3 - MIT +https://github.com/taylorhakes/fecha +Copyright (c) 2015 Taylor Hakes + +The MIT License (MIT) + +Copyright (c) 2015 Taylor Hakes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +figures 2.0.0 - MIT +https://github.com/sindresorhus/figures#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +file-loader 5.1.0 - MIT +https://github.com/webpack-contrib/file-loader +Copyright JS Foundation and other contributors + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fill-range 7.0.1 - MIT +https://github.com/jonschlinkert/fill-range +Copyright (c) 2014-present, Jon Schlinkert. +Copyright (c) 2019, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fill-range 4.0.0 - MIT +https://github.com/jonschlinkert/fill-range +Copyright (c) 2014-2017, Jon Schlinkert +Copyright (c) 2014-2015, 2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +find-cache-dir 3.3.1 - MIT +https://github.com/avajs/find-cache-dir#readme +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +find-cache-dir 3.2.0 - MIT +https://github.com/avajs/find-cache-dir#readme +Copyright (c) James Talmage + +MIT License + +Copyright (c) James Talmage (github.com/jamestalmage) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +find-up 2.1.0 - MIT +https://github.com/sindresorhus/find-up#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +find-up 3.0.0 - MIT +https://github.com/sindresorhus/find-up#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +find-up 4.1.0 - MIT +https://github.com/sindresorhus/find-up#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +findup-sync 3.0.0 - MIT +https://github.com/gulpjs/findup-sync#readme +Copyright (c) 2013-2018 Ben Alman , Blaine Bublitz , and Eric Schoffstall + +The MIT License (MIT) + +Copyright (c) 2013-2018 Ben Alman , Blaine Bublitz , and Eric Schoffstall + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +flatten 1.0.3 - MIT +https://github.com/mk-pmb/flatten-js/#readme +Copyright (c) 2016 Joshua Holbrook + +The MIT License (MIT) + +Copyright (c) 2016 Joshua Holbrook + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +flush-write-stream 1.1.1 - MIT +https://github.com/mafintosh/flush-write-stream +Copyright (c) 2015 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2015 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +follow-redirects 1.5.10 - MIT +https://github.com/follow-redirects/follow-redirects +Copyright 2014-present Olivier Lalonde , James Talmage , Ruben Verborgh + +Copyright 2014–present Olivier Lalonde , James Talmage , Ruben Verborgh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fontkit 1.8.0 - MIT + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +for-in 1.0.2 - MIT +https://github.com/jonschlinkert/for-in +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +for-own 1.0.0 - MIT +https://github.com/jonschlinkert/for-own +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2014-2015, 2017, Jon Schlinkert +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2015, 2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +foreach 2.0.5 - MIT +https://github.com/manuelstofer/foreach +Copyright (c) 2013 Manuel Stofer + +The MIT License + +Copyright (c) 2013 Manuel Stofer + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fork-ts-checker-webpack-plugin 4.1.6 - MIT +https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#readme + +MIT License + +Copyright (c) 2020 TypeStrong + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +form-data 3.0.0 - MIT +https://github.com/form-data/form-data#readme +Copyright (c) 2012 Felix Geisendorfer (felix@debuggable.com) and contributors + +Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +form-data 2.3.3 - MIT +https://github.com/form-data/form-data#readme +Copyright (c) 2012 Felix Geisendorfer (felix@debuggable.com) and contributors + +Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +formidable 1.2.1 - MIT +https://github.com/felixge/node-formidable +Copyright (c) 2011 Felix Geisendorfer + +Copyright (C) 2011 Felix Geisendörfer + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +forwarded 0.1.2 - MIT +https://github.com/jshttp/forwarded#readme +Copyright (c) 2014-2017 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fragment-cache 0.2.1 - MIT +https://github.com/jonschlinkert/fragment-cache +Copyright (c) 2016-2017, Jon Schlinkert. +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2016-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +free-style 3.1.0 - MIT +https://github.com/blakeembrey/free-style +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fresh 0.5.2 - MIT +https://github.com/jshttp/fresh#readme +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2016-2017 Douglas Christopher Wilson +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2016-2017 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2016-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +from 0.1.7 - MIT +https://github.com/dominictarr/from#readme +Copyright (c) 2011 Dominic Tarr + +The MIT License + +Copyright (c) 2011 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +from2 2.3.0 - MIT +https://github.com/hughsk/from2 +Copyright (c) 2014 Hugh Kennedy + +## The MIT License (MIT) ## + +Copyright (c) 2014 Hugh Kennedy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fromentries 1.2.0 - MIT +https://github.com/feross/fromentries +Copyright (c) Feross Aboukhadijeh +Copyright (c) Feross Aboukhadijeh (http://feross.org). + +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fs-extra 4.0.3 - MIT +https://github.com/jprichardson/node-fs-extra +Copyright (c) 2011-2017 JP Richardson +Copyright (c) 2011-2017 JP Richardson (https://github.com/jprichardson) +Copyright (c) 2014-2016 Jonathan Ong me@jongleberry.com and Contributors + +(The MIT License) + +Copyright (c) 2011-2017 JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fs-extra 8.1.0 - MIT +https://github.com/jprichardson/node-fs-extra +Copyright (c) 2011-2017 JP Richardson +Copyright (c) 2011-2017 JP Richardson (https://github.com/jprichardson) +Copyright (c) 2014-2016 Jonathan Ong me@jongleberry.com and Contributors + +(The MIT License) + +Copyright (c) 2011-2017 JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fs-mkdirp-stream 1.0.0 - MIT +https://github.com/gulpjs/fs-mkdirp-stream#readme +Copyright 2010 James Halliday +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors (Originally based on code from node-mkdirp - MIT/X11 license - Copyright 2010 James Halliday) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +function-bind 1.1.1 - MIT +https://github.com/Raynos/function-bind +Copyright (c) 2013 Raynos. + +Copyright (c) 2013 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +fuzzy 0.1.3 - MIT +https://github.com/mattyork/fuzzy +Copyright (c) 2012 Matt York +Copyright (c) 2015 Matt York + +Copyright (c) 2012 Matt York + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +get-func-name 2.0.0 - MIT +https://github.com/chaijs/get-func-name#readme +Copyright (c) 2012-2016 Jake Luer +Copyright (c) 2013 Jake Luer (http://alogicalparadox.com) + +Copyright (c) 2013 Jake Luer (http://alogicalparadox.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +get-port 3.2.0 - MIT +https://github.com/sindresorhus/get-port#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +get-stream 3.0.0 - MIT +https://github.com/sindresorhus/get-stream#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +get-stream 5.1.0 - MIT +https://github.com/sindresorhus/get-stream#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +get-stream 4.1.0 - MIT +https://github.com/sindresorhus/get-stream#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +get-value 2.0.6 - MIT +https://github.com/jonschlinkert/get-value +Copyright (c) 2014-2015, Jon Schlinkert. +Copyright (c) 2014-2016, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +getpass 0.1.7 - MIT +https://github.com/arekinath/node-getpass#readme +Copyright Joyent, Inc. +Copyright 2016, Joyent, Inc. + +Copyright Joyent, Inc. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +glob-stream 6.1.0 - MIT +https://github.com/gulpjs/glob-stream#readme +Copyright (c) 2015-2017 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2015-2017 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +global-modules 1.0.0 - MIT +https://github.com/jonschlinkert/global-modules +Copyright (c) 2015-2017 Jon Schlinkert. +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +global-prefix 1.0.2 - MIT +https://github.com/jonschlinkert/global-prefix +Copyright (c) 2015-2017 Jon Schlinkert. +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +got 9.6.0 - MIT +https://github.com/sindresorhus/got#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +gud 1.0.0 - MIT +https://github.com/jamiebuilds/global-unique-id#readme + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +gulp-azure-storage 0.11.1 - MIT +https://github.com/joaomoreno/gulp-azure-storage#readme + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +gulp-cli 2.2.0 - MIT +http://gulpjs.com +Copyright (c) 2014 Jason Jarrett +Copyright (c) 2012 Tyler Kellen, contributors +Copyright (c) 2015 Blaine Bublitz, Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2015 Blaine Bublitz, Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +gulp-untar 0.0.8 - MIT +https://github.com/jmerrifield/gulp-untar#readme +(c) Jon Merrifield (http://www.jmerrifield.com) + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +gulp-vinyl-zip 2.1.2 - MIT +https://github.com/joaomoreno/gulp-vinyl-zip + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +har-validator 5.1.3 - MIT +https://github.com/ahmadnassri/node-har-validator +Copyright (c) 2018 Ahmad Nassri + +MIT License + +Copyright (c) 2018 Ahmad Nassri + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has 1.0.3 - MIT +https://github.com/tarruda/has +Copyright (c) 2013 Thiago de Arruda + +Copyright (c) 2013 Thiago de Arruda + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has-binary2 1.0.3 - MIT +Copyright (c) 2014 Kevin Roark + +The MIT License (MIT) + +Copyright (c) 2014 Kevin Roark + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has-cors 1.1.0 - MIT +https://github.com/component/has-cors + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has-flag 3.0.0 - MIT +https://github.com/sindresorhus/has-flag#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has-flag 4.0.0 - MIT +https://github.com/sindresorhus/has-flag#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has-symbols 1.0.0 - MIT +https://github.com/ljharb/has-symbols#readme +Copyright (c) 2016 Jordan Harband + +MIT License + +Copyright (c) 2016 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has-symbols 1.0.1 - MIT +https://github.com/ljharb/has-symbols#readme +Copyright (c) 2016 Jordan Harband + +MIT License + +Copyright (c) 2016 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has-value 0.3.1 - MIT +https://github.com/jonschlinkert/has-value +Copyright (c) 2014-2016, Jon Schlinkert. +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has-value 1.0.0 - MIT +https://github.com/jonschlinkert/has-value +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has-values 0.1.4 - MIT +https://github.com/jonschlinkert/has-values +Copyright (c) 2014-2015, Jon Schlinkert. +Copyright (c) 2014-2016, Jon Schlinkert. +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +has-values 1.0.0 - MIT +https://github.com/jonschlinkert/has-values +Copyright (c) 2014-2017, Jon Schlinkert +Copyright (c) 2014-2015, 2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +hash-base 3.0.4 - MIT +https://github.com/crypto-browserify/hash-base +Copyright (c) 2016 Kirill Fomichev + +The MIT License (MIT) + +Copyright (c) 2016 Kirill Fomichev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +hash.js 1.1.7 - MIT +https://github.com/indutny/hash.js +Copyright Fedor Indutny, 2014. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +hasha 5.1.0 - MIT +https://github.com/sindresorhus/hasha#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +homedir-polyfill 1.0.3 - MIT +https://github.com/doowb/homedir-polyfill +Copyright (c) 2016 Brian Woodward +Copyright (c) 2016 - 2019, Brian Woodward (https://github.com/doowb). + +The MIT License (MIT) + +Copyright (c) 2016 Brian Woodward + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +html-encoding-sniffer 1.0.2 - MIT +https://github.com/jsdom/html-encoding-sniffer#readme +Copyright (c) 2016 Domenic Denicola + +Copyright © 2016 Domenic Denicola + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +html-escaper 2.0.0 - MIT +https://github.com/WebReflection/html-escaper +Copyright (c) 2017-present by Andrea Giammarchi + +Copyright (C) 2017-present by Andrea Giammarchi - @WebReflection + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +html-to-react 1.4.3 - MIT +https://github.com/aknuds1/html-to-react +Copyright (c) 2015 Mike Nikles + +The MIT License (MIT) + +Copyright (c) 2015 Mike Nikles +Copyright (c) 2020 Arve Knudsen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +htmlparser2 3.10.1 - MIT +https://github.com/fb55/htmlparser2#readme +Copyright 2010, 2011, Chris Winberry + +Copyright 2010, 2011, Chris Winberry . All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +htmlparser2 4.1.0 - MIT +https://github.com/fb55/htmlparser2#readme +Copyright 2010, 2011, Chris Winberry + +Copyright 2010, 2011, Chris Winberry . All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +http-basic 8.1.3 - MIT +https://github.com/ForbesLindesay/http-basic#readme +Copyright (c) 2014 Forbes Lindesay + +Copyright (c) 2014 Forbes Lindesay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +http-errors 1.7.3 - MIT +https://github.com/jshttp/http-errors#readme +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com + + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +http-proxy-agent 2.1.0 - MIT +https://github.com/TooTallNate/node-http-proxy-agent#readme +Copyright (c) 2013 Nathan Rajlich + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +http-response-object 3.0.2 - MIT +https://github.com/ForbesLindesay/http-response-object#readme +Copyright (c) 2014 Forbes Lindesay + +Copyright (c) 2014 Forbes Lindesay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +http-signature 1.2.0 - MIT +https://github.com/joyent/node-http-signature/ +Copyright Joyent, Inc. +Copyright 2012 Joyent, Inc. +Copyright 2015 Joyent, Inc. +Copyright (c) 2011 Joyent, Inc. + +Copyright Joyent, Inc. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +https-proxy-agent 2.2.4 - MIT +https://github.com/TooTallNate/node-https-proxy-agent#readme +Copyright (c) 2013 Nathan Rajlich + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +https-proxy-agent 3.0.1 - MIT +https://github.com/TooTallNate/node-https-proxy-agent#readme +Copyright (c) 2013 Nathan Rajlich + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +iconv-lite 0.4.24 - MIT +https://github.com/ashtuchkin/iconv-lite +Copyright (c) Microsoft Corporation. +Copyright (c) 2011 Alexander Shtuchkin + +Copyright (c) 2011 Alexander Shtuchkin + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +iferr 0.1.5 - MIT +https://github.com/shesek/iferr +Copyright (c) 2014 Nadav Ivgi + +The MIT License (MIT) + +Copyright (c) 2014 Nadav Ivgi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +immutable 4.0.0-rc.12 - MIT +https://facebook.github.com/immutable-js +Copyright (c) 2014-present, Facebook, Inc. + +MIT License + +Copyright (c) 2014-present, Facebook, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +import-cwd 2.1.0 - MIT +https://github.com/sindresorhus/import-cwd#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +import-from 2.1.0 - MIT +https://github.com/sindresorhus/import-from#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +imurmurhash 0.1.4 - MIT +https://github.com/jensyt/imurmurhash-js +Copyright (c) 2013 Gary Court, Jens Taylor + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +indent-string 4.0.0 - MIT +https://github.com/sindresorhus/indent-string#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +indexes-of 1.0.1 - MIT +https://github.com/dominictarr/indexes-of +Copyright (c) 2013 Dominic Tarr + +Copyright (c) 2013 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +indexof 0.0.1 - MIT + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +interpret 1.2.0 - MIT +https://github.com/gulpjs/interpret#readme +Copyright (c) 2014-2018 Tyler Kellen , Blaine Bublitz , and Eric Schoffstall + +Copyright (c) 2014-2018 Tyler Kellen , Blaine Bublitz , and Eric Schoffstall + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +inversify 4.13.0 - MIT +http://inversify.io +Copyright (c) 2015-2017 Remo H. Jansen +Copyright (c) 2015-2017 Remo H. Jansen (http://www.remojansen.com) + +The MIT License (MIT) + +Copyright (c) 2015-2017 Remo H. Jansen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +invert-kv 1.0.0 - MIT +https://github.com/sindresorhus/invert-kv +(c) Sindre Sorhus (http://sindresorhus.com) + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ip 1.1.5 - MIT +https://github.com/indutny/node-ip +Copyright Fedor Indutny, 2012. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ip-regex 4.1.0 - MIT +https://github.com/sindresorhus/ip-regex#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ip-regex 2.1.0 - MIT +https://github.com/sindresorhus/ip-regex#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ipaddr.js 1.9.0 - MIT +https://github.com/whitequark/ipaddr.js#readme +Copyright (c) 2011-2017 + +Copyright (C) 2011-2017 whitequark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-absolute 1.0.0 - MIT +https://github.com/jonschlinkert/is-absolute +Copyright (c) 2009-2014, TJ Holowaychuk +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2009-2014, TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-accessor-descriptor 0.1.6 - MIT +https://github.com/jonschlinkert/is-accessor-descriptor +Copyright (c) 2015, Jon Schlinkert. +Copyright (c) 2015 Jon Schlinkert (https://github.com/jonschlinkert) + +The MIT License (MIT) + +Copyright (c) 2015, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-accessor-descriptor 1.0.0 - MIT +https://github.com/jonschlinkert/is-accessor-descriptor +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-alphabetical 1.0.4 - MIT +https://github.com/wooorm/is-alphabetical#readme +(c) Titus Wormer +Copyright (c) 2016 Titus Wormer + +(The MIT License) + +Copyright (c) 2016 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-alphanumerical 1.0.4 - MIT +https://github.com/wooorm/is-alphanumerical#readme +(c) Titus Wormer +Copyright (c) 2016 Titus Wormer + +(The MIT License) + +Copyright (c) 2016 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-arguments 1.0.4 - MIT +https://github.com/ljharb/is-arguments +Copyright (c) 2014 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2014 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-arrayish 0.2.1 - MIT +https://github.com/qix-/node-is-arrayish#readme +Copyright (c) 2015 JD Ballard + +The MIT License (MIT) + +Copyright (c) 2015 JD Ballard + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-arrayish 0.3.2 - MIT +https://github.com/qix-/node-is-arrayish#readme +Copyright (c) 2015 JD Ballard + +The MIT License (MIT) + +Copyright (c) 2015 JD Ballard + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-binary-path 1.0.1 - MIT +https://github.com/sindresorhus/is-binary-path +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-buffer 1.1.6 - MIT +https://github.com/feross/is-buffer#readme +Copyright (c) Feross Aboukhadijeh +Copyright (c) Feross Aboukhadijeh (http://feross.org). + +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-callable 1.1.4 - MIT +https://github.com/ljharb/is-callable#readme +Copyright (c) 2015 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2015 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-callable 1.1.5 - MIT +https://github.com/ljharb/is-callable#readme +Copyright (c) 2015 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2015 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-data-descriptor 1.0.0 - MIT +https://github.com/jonschlinkert/is-data-descriptor +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-data-descriptor 0.1.4 - MIT +https://github.com/jonschlinkert/is-data-descriptor +Copyright (c) 2015, Jon Schlinkert. +Copyright (c) 2015 Jon Schlinkert (https://github.com/jonschlinkert) + +The MIT License (MIT) + +Copyright (c) 2015, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-date-object 1.0.1 - MIT +https://github.com/ljharb/is-date-object#readme +Copyright (c) 2015 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2015 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-decimal 1.0.4 - MIT +https://github.com/wooorm/is-decimal#readme +(c) Titus Wormer +Copyright (c) 2016 Titus Wormer + +(The MIT License) + +Copyright (c) 2016 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-descriptor 0.1.6 - MIT +https://github.com/jonschlinkert/is-descriptor +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-descriptor 1.0.2 - MIT +https://github.com/jonschlinkert/is-descriptor +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-extendable 1.0.1 - MIT +https://github.com/jonschlinkert/is-extendable +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-extendable 0.1.1 - MIT +https://github.com/jonschlinkert/is-extendable +Copyright (c) 2015 Jon Schlinkert +Copyright (c) 2015, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2015, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-extglob 2.1.1 - MIT +https://github.com/jonschlinkert/is-extglob +Copyright (c) 2014-2016, Jon Schlinkert. +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-fullwidth-code-point 2.0.0 - MIT +https://github.com/sindresorhus/is-fullwidth-code-point#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-fullwidth-code-point 3.0.0 - MIT +https://github.com/sindresorhus/is-fullwidth-code-point#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-fullwidth-code-point 1.0.0 - MIT +https://github.com/sindresorhus/is-fullwidth-code-point +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-glob 4.0.1 - MIT +https://github.com/micromatch/is-glob +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2019, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-glob 3.1.0 - MIT +https://github.com/jonschlinkert/is-glob +Copyright (c) 2014-2016, Jon Schlinkert. +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-hexadecimal 1.0.4 - MIT +https://github.com/wooorm/is-hexadecimal#readme +(c) Titus Wormer +Copyright (c) 2016 Titus Wormer + +(The MIT License) + +Copyright (c) 2016 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-ip 2.0.0 - MIT +https://github.com/sindresorhus/is-ip#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-ip 3.1.0 - MIT +https://github.com/sindresorhus/is-ip#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-negated-glob 1.0.0 - MIT +https://github.com/jonschlinkert/is-negated-glob +Copyright (c) 2016 Jon Schlinkert +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2016 Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-number 7.0.0 - MIT +https://github.com/jonschlinkert/is-number +Copyright (c) 2014-present, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-number 3.0.0 - MIT +https://github.com/jonschlinkert/is-number +Copyright (c) 2014-2016, Jon Schlinkert +Copyright (c) 2014-2015, Jon Schlinkert. +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-number 4.0.0 - MIT +https://github.com/jonschlinkert/is-number +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-obj 1.0.1 - MIT +https://github.com/sindresorhus/is-obj#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-online 8.2.1 - MIT +https://github.com/sindresorhus/is-online#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-path-inside 1.0.1 - MIT +https://github.com/sindresorhus/is-path-inside#readme +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-plain-object 2.0.4 - MIT +https://github.com/jonschlinkert/is-plain-object +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-regex 1.0.5 - MIT +https://github.com/ljharb/is-regex +Copyright (c) 2014 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2014 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-regex 1.0.4 - MIT +https://github.com/ljharb/is-regex +Copyright (c) 2014 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2014 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-relative 1.0.0 - MIT +https://github.com/jonschlinkert/is-relative +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-retry-allowed 1.1.0 - MIT +https://github.com/floatdrop/is-retry-allowed#readme +(c) Vsevolod Strukchinsky (http://github.com/floatdrop) +Copyright (c) Vsevolod Strukchinsky + +The MIT License (MIT) + +Copyright (c) Vsevolod Strukchinsky (github.com/floatdrop) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-stream 1.1.0 - MIT +https://github.com/sindresorhus/is-stream#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-stream 2.0.0 - MIT +https://github.com/sindresorhus/is-stream#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-symbol 1.0.2 - MIT +https://github.com/ljharb/is-symbol#readme +Copyright (c) 2015 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2015 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-typedarray 1.0.0 - MIT +https://github.com/hughsk/is-typedarray + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-unc-path 1.0.0 - MIT +https://github.com/jonschlinkert/is-unc-path +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-url 1.2.4 - MIT +https://github.com/segmentio/is-url#readme + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-utf8 0.2.1 - MIT +https://github.com/wayfind/is-utf8#readme +Copyright (c) 2014 Wei Fanzhe + +The MIT License (MIT) + +Copyright (C) 2014 Wei Fanzhe + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +   +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-valid-glob 1.0.0 - MIT +https://github.com/jonschlinkert/is-valid-glob +Copyright (c) 2015-2017, Jon Schlinkert +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-whitespace-character 1.0.4 - MIT +https://github.com/wooorm/is-whitespace-character#readme +(c) Titus Wormer +Copyright (c) 2016 Titus Wormer + +(The MIT License) + +Copyright (c) 2016 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-windows 1.0.2 - MIT +https://github.com/jonschlinkert/is-windows +Copyright (c) 2015-2018, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-word-character 1.0.4 - MIT +https://github.com/wooorm/is-word-character#readme +(c) Titus Wormer +Copyright (c) 2016 Titus Wormer + +(The MIT License) + +Copyright (c) 2016 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is-wsl 1.1.0 - MIT +https://github.com/sindresorhus/is-wsl#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +is2 2.0.1 - MIT +http://github.com/stdarg/is2 +Copyright (c) 2011 Enrico Marino +Copyright (c) 2013 Edmond Meinfelder +Copyright (c) 2013,2014 Edmond Meinfelder +Copyright (c) 2011 Enrico Marino +Copyright (c) 2013,2014 Edmond Meinfelder + +The MIT License (MIT) + +Copyright (c) 2013 Edmond Meinfelder + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +isarray 2.0.1 - MIT +https://github.com/juliangruber/isarray +Copyright (c) 2013 Julian Gruber + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +isarray 0.0.1 - MIT +https://github.com/juliangruber/isarray +Copyright (c) 2013 Julian Gruber + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +isarray 1.0.0 - MIT +https://github.com/juliangruber/isarray +Copyright (c) 2013 Julian Gruber + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +isnumeric 0.2.0 - MIT +http://ilee.co.uk +(c) Lee Crossley (http://ilee.co.uk/) + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +isobject 3.0.1 - MIT +https://github.com/jonschlinkert/isobject +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +isobject 2.1.0 - MIT +https://github.com/jonschlinkert/isobject +Copyright (c) 2014-2015, Jon Schlinkert. +Copyright (c) 2014-2016, Jon Schlinkert. +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +isomorphic-fetch 2.2.1 - MIT +https://github.com/matthew-andrews/isomorphic-fetch/issues +Copyright (c) 2015 Matt Andrews + +The MIT License (MIT) + +Copyright (c) 2015 Matt Andrews + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +isort 4.3.21 - MIT +Copyright (c) 2015 Helen Sherwood-Taylor +Copyright (c) 2013 Timothy Edmund Crosley + +The MIT License (MIT) + +Copyright (c) 2013 Timothy Edmund Crosley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +isstream 0.1.2 - MIT +https://github.com/rvagg/isstream +Copyright (c) 2015 Rod Vagg +Copyright (c) 2015 Rod Vagg rvagg (https://twitter.com/rvagg) + +The MIT License (MIT) +===================== + +Copyright (c) 2015 Rod Vagg +--------------------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jedi 0.17.2 - MIT +Copyright (c) <2013> +Copyright (c) Maxim Kurnikov. +copyright u'jedi contributors +copyright (c) 2014 by Armin Ronacher. +Copyright (c) 2015 Jukka Lehtosalo and contributors + +All contributions towards Jedi are MIT licensed. + +------------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) <2013> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +Copyright (c) Maxim Kurnikov. +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +The "typeshed" project is licensed under the terms of the Apache license, as +reproduced below. + += = = = = + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + += = = = = + +Parts of typeshed are licensed under different licenses (like the MIT +license), reproduced below. + += = = = = + +The MIT License + +Copyright (c) 2015 Jukka Lehtosalo and contributors + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + += = = = = + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jest-docblock 21.2.0 - MIT +https://github.com/facebook/jest#readme +Copyright (c) 2014-present, Facebook, Inc. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jest-worker 24.9.0 - MIT +https://github.com/facebook/jest#readme +Copyright (c) Facebook, Inc. and its affiliates. + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jquery-ui 1.12.1 - MIT +http://jqueryui.com +Copyright 2011, John Resig +Copyright 2011, The Dojo Foundation +Copyright (c) 2015 Alexander Schmitz +Copyright (c) 2010-2014, The Dojo Foundation +Copyright Software Freedom Conservancy, Inc. +Copyright (c) 2011 John Resig, http://jquery.com +(c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. +Copyright (c) 2009 Thomas Robinson <280north.com> +Copyright 2012 Anton Kovalyov (http://jshint.com) +Copyright jQuery Foundation and other contributors +Copyright Joyent, Inc. and other Node contributors. +Copyright (c) 2002 Douglas Crockford (www.JSLint.com) +Copyright 2012 jQuery Foundation and other contributors +Copyright 2014 jQuery Foundation and other contributors +Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh) +Copyright (c) 2013, Brandon Aaron (http://brandon.aaron.sh) +Copyright 2013 jQuery Foundation, Inc. and other contributors +Copyright 2005, 2012 jQuery Foundation, Inc. and other contributors +Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors +Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors +Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors +Copyright jQuery Foundation and other contributors http://jquery.com +Copyright Software Freedom Conservancy, Inc. http://jquery.org/license +Copyright jQuery Foundation and other contributors, https://jquery.org +Copyright 2012 jQuery Foundation and other contributors http://jquery.com +Copyright 2013 jQuery Foundation and other contributors http://jquery.com +Copyright 2014 jQuery Foundation and other contributors http://jquery.com +Copyright 2006 Google Inc. http://code.google.com/p/google-diff-match-patch +Copyright 2007, 2014 jQuery Foundation and other contributors, https://jquery.org + +Copyright jQuery Foundation and other contributors, https://jquery.org/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery-ui + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code contained within the demos directory. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. + + +Copyright (c) 2013, Brandon Aaron (http://brandon.aaron.sh) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Copyright (c) 2011 John Resig, http://jquery.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Copyright (c) 2011 John Resig, http://jquery.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +The MIT License (MIT) + +Copyright (c) 2015 Alexander Schmitz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +Copyright (c) 2011 John Resig, http://jquery.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Copyright 2012 Anton Kovalyov (http://jshint.com) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +Copyright Software Freedom Conservancy, Inc. +http://jquery.org/license + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +js-tokens 4.0.0 - MIT +https://github.com/lydell/js-tokens#readme +Copyright 2014, 2015, 2016, 2017, 2018 Simon Lydell +Copyright (c) 2014, 2015, 2016, 2017, 2018 Simon Lydell + +The MIT License (MIT) + +Copyright (c) 2014, 2015, 2016, 2017, 2018 Simon Lydell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +js-yaml 3.13.1 - MIT +https://github.com/nodeca/js-yaml +Copyright (c) 2011-2015 by Vitaly Puzrin + +(The MIT License) + +Copyright (C) 2011-2015 by Vitaly Puzrin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jsbn 0.1.1 - MIT +https://github.com/andyperlitch/jsbn#readme +Copyright (c) 2005 Tom Wu +Copyright (c) 2003-2005 Tom Wu +Copyright (c) 2005-2009 Tom Wu + +Licensing +--------- + +This software is covered under the following copyright: + +/* + * Copyright (c) 2003-2005 Tom Wu + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, + * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY + * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + * + * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF + * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT + * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * In addition, the following condition applies: + * + * All redistributions must retain an intact copy of this copyright notice + * and disclaimer. + */ + +Address all questions regarding this license to: + + Tom Wu + tjw@cs.Stanford.EDU + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +json-buffer 3.0.0 - MIT +https://github.com/dominictarr/json-buffer +Copyright (c) 2013 Dominic Tarr + +Copyright (c) 2013 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +json-edm-parser 0.1.2 - MIT +https://github.com/yaxia/json-edm-parser#readme +Copyright (c) 2016 Yang Xia + +The MIT License (MIT) + +Copyright (c) 2016 Yang Xia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +json-parse-better-errors 1.0.2 - MIT +https://github.com/zkat/json-parse-better-errors#readme +Copyright 2017 Kat Marchan + +Copyright 2017 Kat Marchán + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +json-schema-traverse 0.4.1 - MIT +https://github.com/epoberezkin/json-schema-traverse#readme +Copyright (c) 2017 Evgeny Poberezkin + +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +json-stable-stringify 1.0.1 - MIT +https://github.com/substack/json-stable-stringify + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +json-stable-stringify-without-jsonify 1.0.1 - MIT +https://github.com/samn/json-stable-stringify + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +json-stringify-pretty-compact 2.0.0 - MIT +https://github.com/lydell/json-stringify-pretty-compact#readme +Copyright (c) 2014, 2016, 2017, 2019 Simon Lydell + +The MIT License (MIT) + +Copyright (c) 2014, 2016, 2017, 2019 Simon Lydell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +json5 2.1.0 - MIT +http://json5.org/ +Copyright (c) 2012-2018 Aseem Kishore, and others + +MIT License + +Copyright (c) 2012-2018 Aseem Kishore, and [others]. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +[others]: https://github.com/json5/json5/contributors + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jsonc-parser 2.1.0 - MIT +https://github.com/Microsoft/node-jsonc-parser#readme +Copyright (c) Microsoft +Copyright 2018, Microsoft +Copyright (c) Microsoft Corporation. + +The MIT License (MIT) + +Copyright (c) Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jsonfile 4.0.0 - MIT +https://github.com/jprichardson/node-jsonfile#readme +Copyright 2012-2016, JP Richardson +Copyright (c) 2012-2015, JP Richardson + +(The MIT License) + +Copyright (c) 2012-2015, JP Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jsonparse 1.2.0 - MIT +https://github.com/creationix/jsonparse +Copyright (c) 2012 Tim Caswell +Copyright (c) 2011-2012 Tim Caswell + +The MIT License + +Copyright (c) 2012 Tim Caswell + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jsonparse 1.3.1 - MIT +https://github.com/creationix/jsonparse#readme +Copyright (c) 2012 Tim Caswell +Copyright (c) 2011-2012 Tim Caswell + +The MIT License + +Copyright (c) 2012 Tim Caswell + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jsprim 1.4.1 - MIT +https://github.com/joyent/node-jsprim#readme +Copyright (c) 2012, Joyent, Inc. + +Copyright (c) 2012, Joyent, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +keyv 3.0.0 - MIT +https://github.com/lukechilds/keyv +(c) Luke Childs +Copyright (c) 2017 Luke Childs + +MIT License + +Copyright (c) 2017 Luke Childs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +kind-of 3.2.2 - MIT +https://github.com/jonschlinkert/kind-of +Copyright (c) 2014-2017, Jon Schlinkert +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +kind-of 4.0.0 - MIT +https://github.com/jonschlinkert/kind-of +Copyright (c) 2014-2017, Jon Schlinkert +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +kind-of 6.0.3 - MIT +https://github.com/jonschlinkert/kind-of +Copyright (c) 2014-2017, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +kind-of 5.1.0 - MIT +https://github.com/jonschlinkert/kind-of +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +kuler 1.0.1 - MIT +https://github.com/3rd-Eden/kuler +Copyright 2014 Arnout Kazemier + +Copyright 2014 Arnout Kazemier + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lazystream 1.0.0 - MIT +https://github.com/jpommerening/node-lazystream +Copyright (c) 2013 J. Pommerening, contributors. + +Copyright (c) 2013 J. Pommerening, contributors. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lcid 1.0.0 - MIT +https://github.com/sindresorhus/lcid +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lead 1.0.0 - MIT +https://github.com/gulpjs/lead#readme +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +levn 0.3.0 - MIT +https://github.com/gkz/levn +Copyright (c) George Zahariev + +Copyright (c) George Zahariev + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +line-by-line 0.1.6 - MIT +https://github.com/Osterjour/line-by-line +Copyright (c) 2012 Markus von der Wehd +Copyright (c) 2012 Markus von der Wehd + + +Copyright (c) 2012 Markus von der Wehd + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +linebreak 1.0.2 - MIT +https://github.com/devongovett/linebreaker +Copyright (c) 1991-2014 Unicode, Inc. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lines-and-columns 1.1.6 - MIT +https://github.com/eventualbuddha/lines-and-columns#readme +Copyright (c) 2015 Brian Donovan + +The MIT License (MIT) + +Copyright (c) 2015 Brian Donovan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +load-json-file 1.1.0 - MIT +https://github.com/sindresorhus/load-json-file +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +loader-utils 1.4.0 - MIT +https://github.com/webpack/loader-utils#readme +Copyright JS Foundation and other contributors + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +locate-path 2.0.0 - MIT +https://github.com/sindresorhus/locate-path#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +locate-path 3.0.0 - MIT +https://github.com/sindresorhus/locate-path#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +locate-path 5.0.0 - MIT +https://github.com/sindresorhus/locate-path#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash 4.17.19 - MIT +https://lodash.com/ +Copyright OpenJS Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright OpenJS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash-es 4.17.15 - MIT +https://lodash.com/custom-builds +Copyright OpenJS Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright OpenJS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash.assign 4.2.0 - MIT +https://lodash.com/ +Copyright jQuery Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash.clonedeep 4.5.0 - MIT +https://lodash.com/ +Copyright jQuery Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash.escape 4.0.1 - MIT +https://lodash.com/ +Copyright jQuery Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash.escaperegexp 4.1.2 - MIT +https://lodash.com/ +Copyright jQuery Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash.isplainobject 4.0.6 - MIT +https://lodash.com/ +Copyright jQuery Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash.isstring 4.0.1 - MIT +https://lodash.com/ +Copyright 2012-2016 The Dojo Foundation +Copyright 2009-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright 2009-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright 2012-2016 The Dojo Foundation +Based on Underscore.js, copyright 2009-2016 Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash.memoize 4.1.2 - MIT +https://lodash.com/ +Copyright jQuery Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash.mergewith 4.6.2 - MIT +https://lodash.com/ +Copyright OpenJS Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright OpenJS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash.union 4.6.0 - MIT +https://lodash.com/ +Copyright jQuery Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lodash.uniq 4.5.0 - MIT +https://lodash.com/ +Copyright jQuery Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright jQuery Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +logform 2.1.2 - MIT +https://github.com/winstonjs/logform#readme +Copyright (c) 2017 Charlie Robbins & the Contributors. + +MIT License + +Copyright (c) 2017 Charlie Robbins & the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +loose-envify 1.4.0 - MIT +https://github.com/zertosh/loose-envify +Copyright (c) 2015 Andres Suarez + +The MIT License (MIT) + +Copyright (c) 2015 Andres Suarez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lower-case 1.1.4 - MIT +https://github.com/blakeembrey/lower-case +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lowercase-keys 1.0.1 - MIT +https://github.com/sindresorhus/lowercase-keys#readme +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +lowercase-keys 2.0.0 - MIT +https://github.com/sindresorhus/lowercase-keys#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +magic-string 0.22.5 - MIT +https://github.com/rich-harris/magic-string#readme + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +make-dir 3.1.0 - MIT +https://github.com/sindresorhus/make-dir#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +make-dir 1.3.0 - MIT +https://github.com/sindresorhus/make-dir#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +make-dir 3.0.0 - MIT +https://github.com/sindresorhus/make-dir#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +map-cache 0.2.2 - MIT +https://github.com/jonschlinkert/map-cache +Copyright (c) 2015, Jon Schlinkert. +Copyright (c) 2015-2016, Jon Schlinkert. +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +map-stream 0.1.0 - MIT +http://github.com/dominictarr/map-stream +Copyright (c) 2011 Dominic Tarr + +Copyright (c) 2011 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +map-visit 1.0.0 - MIT +https://github.com/jonschlinkert/map-visit +Copyright (c) 2015-2017, Jon Schlinkert +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +markdown-escapes 1.0.4 - MIT +https://github.com/wooorm/markdown-escapes#readme +(c) Titus Wormer +Copyright (c) 2016 Titus Wormer + +(The MIT License) + +Copyright (c) 2016 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +markdown-it 8.4.2 - MIT +https://github.com/markdown-it/markdown-it#readme +(c) (tm) +Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin. +Copyright Joyent, Inc. and other Node contributors. + +Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +marked 0.7.0 - MIT +https://marked.js.org +Copyright (c) 2011-2013, Christopher Jeffrey +Copyright (c) 2011-2014, Christopher Jeffrey +Copyright (c) 2011-2018, Christopher Jeffrey. +Copyright (c) 2004, John Gruber http://daringfireball.net +Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) +Copyright (c) 2011-2018, Christopher Jeffrey. (MIT Licensed) https://github.com/markedjs/marked + +# License information + +## Contribution License Agreement + +If you contribute code to this project, you are implicitly allowing your code +to be distributed under the MIT license. You are also implicitly verifying that +all code is your original work. `` + +## Marked + +Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Markdown + +Copyright © 2004, John Gruber +http://daringfireball.net/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +math-expression-evaluator 1.2.22 - MIT +https://github.com/bugwheels94/math-expression-evaluator#readme +Copyright (c) Ankit +Copyright (c) 2015 Ankit G. + +The MIT License (MIT) + +Copyright (c) 2015 Ankit G. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +md5.js 1.3.4 - MIT +https://github.com/crypto-browserify/md5.js +Copyright (c) 2016 Kirill Fomichev + +The MIT License (MIT) + +Copyright (c) 2016 Kirill Fomichev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +media-typer 0.3.0 - MIT +https://github.com/jshttp/media-typer +Copyright (c) 2014 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +memoize-one 5.1.1 - MIT +https://github.com/alexreardon/memoize-one#readme +Copyright (c) 2019 Alexander Reardon + +MIT License + +Copyright (c) 2019 Alexander Reardon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +memory-fs 0.5.0 - MIT +https://github.com/webpack/memory-fs +Copyright (c) 2012-2014 Tobias Koppers +Copyright JS Foundation and other contributors + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +merge-descriptors 1.0.1 - MIT +https://github.com/component/merge-descriptors +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +merge-source-map 1.0.4 - MIT +https://github.com/keik/merge-source-map#readme + +The MIT License (MIT) + +Copyright (c) keik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +merge-stream 2.0.0 - MIT +https://github.com/grncdr/merge-stream#readme +Copyright (c) Stephen Sugden (stephensugden.com) + +The MIT License (MIT) + +Copyright (c) Stephen Sugden (stephensugden.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +methods 1.1.2 - MIT +https://github.com/jshttp/methods +Copyright (c) 2013-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson +Copyright (c) 2013-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2013-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +microevent.ts 0.1.1 - MIT +https://github.com/DirtyHairy/microevent#readme +Copyright (c) 2016 Christian Speckner + +The MIT License (MIT) + +Copyright (c) 2016 Christian Speckner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +micromatch 3.1.10 - MIT +https://github.com/micromatch/micromatch +Copyright (c) 2014-2018, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +mime 1.6.0 - MIT +https://github.com/broofa/node-mime#readme +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +The MIT License (MIT) + +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +mime 2.4.4 - MIT +https://github.com/broofa/node-mime#readme +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +The MIT License (MIT) + +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +mime-db 1.40.0 - MIT +https://github.com/jshttp/mime-db#readme +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +mime-types 2.1.24 - MIT +https://github.com/jshttp/mime-types#readme +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +mimic-fn 1.2.0 - MIT +https://github.com/sindresorhus/mimic-fn#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +mimic-response 1.0.1 - MIT +https://github.com/sindresorhus/mimic-response#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +mimic-response 2.0.0 - MIT +https://github.com/sindresorhus/mimic-response#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +minimist 1.2.5 - MIT +https://github.com/substack/minimist + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +minizlib 1.3.3 - MIT +https://github.com/isaacs/minizlib#readme +Copyright Isaac Z. Schlueter and Contributors +Copyright Joyent, Inc. and other Node contributors. + +Minizlib was created by Isaac Z. Schlueter. +It is a derivative work of the Node.js project. + +""" +Copyright Isaac Z. Schlueter and Contributors +Copyright Node.js contributors. All rights reserved. +Copyright Joyent, Inc. and other Node contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +mixin-deep 1.3.2 - MIT +https://github.com/jonschlinkert/mixin-deep +Copyright (c) 2014-2015, 2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2015, 2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +mkdirp 0.5.5 - MIT +https://github.com/substack/node-mkdirp#readme +Copyright 2010 James Halliday (mail@substack.net) + +Copyright 2010 James Halliday (mail@substack.net) + +This project is free software released under the MIT/X11 license: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +moment 2.24.0 - MIT +http://momentjs.com +Copyright (c) JS Foundation and other contributors + +Copyright (c) JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +monaco-editor 0.18.1 - MIT +https://github.com/Microsoft/monaco-editor#readme +Copyright (c) 2015 +Copyright (c) Joshaven Potter +Copyright (c) 2014 Taylor Hakes +Copyright (c) Artyom Shalkhakov. +Copyright (c) 2015 David Owens II +Copyright (c) 2014 Forbes Lindesay +Copyright (c) 2015 Nicolas Bevacqua +Copyright (c) Microsoft Corporation. +Copyright (c) 2008 Microsoft Corporation +Copyright (c) David Owens II, owensd.io. +Copyright Drifty Co. http://drifty.com/. +Copyright (c) 2016 - present Microsoft Corporation +Copyright Joyent, Inc. and other Node contributors. +Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors. +Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors. +Copyright (c) 2011-2014, Christopher Jeffrey (https://github.com/chjj/) +Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) https://github.com/markedjs/marked + +The MIT License (MIT) + +Copyright (c) 2016 - present Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +monaco-editor-textmate 2.2.1 - MIT +https://github.com/NeekSandhu/monaco-editor-textmate#readme +Copyright (c) 2018 Neek Sandhu + +The MIT License (MIT) + +Copyright (c) 2018 Neek Sandhu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +monaco-textmate 3.0.1 - MIT +https://github.com/NeekSandhu/monaco-textmate#readme +Copyright (c) Microsoft Corporation. + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ms 2.1.2 - MIT +https://github.com/zeit/ms#readme +Copyright (c) 2016 Zeit, Inc. + +The MIT License (MIT) + +Copyright (c) 2016 Zeit, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ms 2.1.1 - MIT +https://github.com/zeit/ms#readme +Copyright (c) 2016 Zeit, Inc. + +The MIT License (MIT) + +Copyright (c) 2016 Zeit, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ms 2.0.0 - MIT +https://github.com/zeit/ms#readme +Copyright (c) 2016 Zeit, Inc. + +The MIT License (MIT) + +Copyright (c) 2016 Zeit, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +named-js-regexp 1.3.5 - MIT +https://github.com/edvinv/named-js-regexp#readme +Copyright (c) 2015 + +The MIT License + +Copyright (c) 2015, @edvinv + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +nan 2.14.0 - MIT +https://github.com/nodejs/nan#readme +Copyright (c) 2018 NAN WG Members +Copyright (c) 2018 NAN contributors +Copyright Joyent, Inc. and other Node contributors. +Copyright (c) 2018 NAN contributors - Rod Vagg + +The MIT License (MIT) +===================== + +Copyright (c) 2018 NAN contributors +----------------------------------- + +*NAN contributors listed at * + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +nanomatch 1.2.13 - MIT +https://github.com/micromatch/nanomatch +Copyright (c) 2016-2018, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2016-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +negotiator 0.6.2 - MIT +https://github.com/jshttp/negotiator#readme +Copyright (c) 2012 Federico Romero +Copyright (c) 2014 Federico Romero +Copyright (c) 2012 Isaac Z. Schlueter +Copyright (c) 2012-2014 Federico Romero +Copyright (c) 2012-2014 Isaac Z. Schlueter +Copyright (c) 2015 Douglas Christopher Wilson +Copyright (c) 2014-2015 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2012-2014 Federico Romero +Copyright (c) 2012-2014 Isaac Z. Schlueter +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +next-tick 1.0.0 - MIT +https://github.com/medikoo/next-tick#readme +Copyright (c) 2012-2016 Mariusz Nowak + +The MIT License + +Copyright (C) 2012-2016 Mariusz Nowak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +nice-try 1.0.5 - MIT +https://github.com/electerious/nice-try +Copyright (c) 2018 Tobias Reich + +The MIT License (MIT) + +Copyright (c) 2018 Tobias Reich + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +no-case 2.3.2 - MIT +https://github.com/blakeembrey/no-case +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +nocache 2.1.0 - MIT +https://helmetjs.github.io/docs/nocache/ + +The MIT License (MIT) + +Copyright (c) 2014-2019 Evan Hahn, Adam Baldwin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +node-fetch 2.6.0 - MIT +https://github.com/bitinn/node-fetch +Copyright (c) 2016 David Frank + +The MIT License (MIT) + +Copyright (c) 2016 David Frank + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +node-fetch 1.7.3 - MIT +https://github.com/bitinn/node-fetch +Copyright (c) 2016 David Frank + +The MIT License (MIT) + +Copyright (c) 2016 David Frank + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +node-gyp-build 4.2.1 - MIT +https://github.com/prebuild/node-gyp-build +Copyright (c) 2017 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2017 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +node-preload 0.2.1 - MIT +https://github.com/cfware/node-preload#readme +Copyright (c) 2019 CFWare, LLC + +MIT License + +Copyright (c) 2019 CFWare, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +node-stream-zip 1.8.2 - MIT +https://github.com/antelle/node-stream-zip +Copyright (c) 2015 Antelle https://github.com/antelle +(c) 2015 Antelle https://github.com/antelle/node-stream-zip/blob/master/LICENSE +Copyright (c) 2012 Another-D-Mention Software and other contributors, http://www.another-d-mention.ro +Portions copyright https://github.com/cthackers/adm-zip https://raw.githubusercontent.com/cthackers/adm-zip/master/LICENSE + +Copyright (c) 2015 Antelle https://github.com/antelle + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +== dependency license: adm-zip == + +Copyright (c) 2012 Another-D-Mention Software and other contributors, +http://www.another-d-mention.ro/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +normalize-path 3.0.0 - MIT +https://github.com/jonschlinkert/normalize-path +Copyright (c) 2014-2018, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +normalize-path 2.1.1 - MIT +https://github.com/jonschlinkert/normalize-path +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +normalize-range 0.1.2 - MIT +https://github.com/jamestalmage/normalize-range#readme +Copyright (c) James Talmage +(c) James Talmage (http://github.com/jamestalmage) + +The MIT License (MIT) + +Copyright (c) James Talmage (github.com/jamestalmage) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +normalize-url 4.5.0 - MIT +https://github.com/sindresorhus/normalize-url#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +normalize.css 8.0.1 - MIT +https://necolas.github.io/normalize.css +Copyright (c) Nicolas Gallagher and Jonathan Neal + +# The MIT License (MIT) + +Copyright © Nicolas Gallagher and Jonathan Neal + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +now-and-later 2.0.1 - MIT +https://github.com/gulpjs/now-and-later#readme +Copyright (c) 2014 Blaine Bublitz, Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2014 Blaine Bublitz, Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +npm-run-path 2.0.2 - MIT +https://github.com/sindresorhus/npm-run-path#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +num2fraction 1.2.2 - MIT +https://github.com/yisibl/num2fraction#readme +Copyright (c) 2014 PostCSS + +The MIT License (MIT) + +Copyright (c) 2014 PostCSS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +number-is-nan 1.0.1 - MIT +https://github.com/sindresorhus/number-is-nan#readme +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +nwsapi 2.1.4 - MIT +http://javascript.nwbox.com/nwsapi/ +Copyright (c) 2007-2017 Diego Perini +Copyright (c) 2007-2019 Diego Perini +Copyright (c) 2007-2019 Diego Perini (http://www.iport.it/) + +Copyright (c) 2007-2019 Diego Perini (http://www.iport.it/) + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +object-assign 4.1.1 - MIT +https://github.com/sindresorhus/object-assign#readme +(c) Sindre Sorhus +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +object-component 0.0.3 - MIT + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +object-copy 0.1.0 - MIT +https://github.com/jonschlinkert/object-copy +Copyright (c) 2016, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +object-inspect 1.4.1 - MIT +https://github.com/substack/object-inspect +Copyright Joyent, Inc. and other Node contributors. + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +object-inspect 1.7.0 - MIT +https://github.com/substack/object-inspect +Copyright (c) 2013 James Halliday + +MIT License + +Copyright (c) 2013 James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +object-keys 1.1.1 - MIT +https://github.com/ljharb/object-keys#readme +Copyright (c) 2013 Jordan Harband + +The MIT License (MIT) + +Copyright (C) 2013 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +object-visit 1.0.1 - MIT +https://github.com/jonschlinkert/object-visit +Copyright (c) 2015, 2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015, 2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +object.assign 4.1.0 - MIT +https://github.com/ljharb/object.assign#readme +Copyright (c) 2014 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2014 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +object.getownpropertydescriptors 2.0.3 - MIT +https://github.com/ljharb/object.getownpropertydescriptors#readme +Copyright (c) 2015 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2015 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +object.pick 1.3.0 - MIT +https://github.com/jonschlinkert/object.pick +Copyright (c) 2014-2016, Jon Schlinkert. +Copyright (c) 2014-2015 Jon Schlinkert, contributors. +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +on-finished 2.3.0 - MIT +https://github.com/jshttp/on-finished +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +one-time 0.0.4 - MIT +https://github.com/unshiftio/one-time +Copyright (c) 2015 Unshift.io, Arnout Kazemier + +The MIT License (MIT) + +Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +onigasm 2.2.2 - MIT +https://github.com/NeekSandhu/onigasm#readme +Copyright (c) 2018 Neek Sandhu + +The MIT License (MIT) + +Copyright (c) 2018 Neek Sandhu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +optionator 0.8.2 - MIT +https://github.com/gkz/optionator +Copyright (c) George Zahariev + +Copyright (c) George Zahariev + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ordered-read-streams 1.0.1 - MIT +https://github.com/armed/ordered-read-streams#readme +Copyright (c) 2014 Artem Medeusheyev + +The MIT License (MIT) + +Copyright (c) 2014 Artem Medeusheyev + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +os-homedir 1.0.2 - MIT +https://github.com/sindresorhus/os-homedir#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +os-locale 1.4.0 - MIT +https://github.com/sindresorhus/os-locale +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +os-tmpdir 1.0.2 - MIT +https://github.com/sindresorhus/os-tmpdir#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-any 2.1.0 - MIT +https://github.com/sindresorhus/p-any#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-cancelable 2.0.0 - MIT +https://github.com/sindresorhus/p-cancelable#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-cancelable 1.1.0 - MIT +https://github.com/sindresorhus/p-cancelable#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-finally 1.0.0 - MIT +https://github.com/sindresorhus/p-finally#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-limit 2.2.0 - MIT +https://github.com/sindresorhus/p-limit#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-locate 2.0.0 - MIT +https://github.com/sindresorhus/p-locate#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-locate 3.0.0 - MIT +https://github.com/sindresorhus/p-locate#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-locate 4.1.0 - MIT +https://github.com/sindresorhus/p-locate#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-map 3.0.0 - MIT +https://github.com/sindresorhus/p-map#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-some 4.1.0 - MIT +https://github.com/sindresorhus/p-some#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-timeout 3.2.0 - MIT +https://github.com/sindresorhus/p-timeout#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-try 1.0.0 - MIT +https://github.com/sindresorhus/p-try#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +p-try 2.2.0 - MIT +https://github.com/sindresorhus/p-try#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pako 0.2.9 - MIT +https://github.com/nodeca/pako +Copyright (c) 2014-2016 by Vitaly Puzrin + +(The MIT License) + +Copyright (C) 2014-2016 by Vitaly Puzrin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +parallel-transform 1.1.0 - MIT +https://github.com/mafintosh/parallel-transform +Copyright 2013 Mathias Buus + +Copyright 2013 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +parallel-transform 1.2.0 - MIT +https://github.com/mafintosh/parallel-transform#readme +Copyright 2013 Mathias Buus + +Copyright 2013 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +parse-json 2.2.0 - MIT +https://github.com/sindresorhus/parse-json +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +parse-passwd 1.0.0 - MIT +https://github.com/doowb/parse-passwd +Copyright (c) 2016 Brian Woodward +Copyright (c) 2016, Brian Woodward (https://github.com/doowb). + +The MIT License (MIT) + +Copyright (c) 2016 Brian Woodward + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +parse5 3.0.3 - MIT +https://github.com/inikulin/parse5 +Copyright (c) 2013-2016 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin) + +Copyright (c) 2013-2016 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +parseqs 0.0.5 - MIT +https://github.com/get/querystring +Copyright (c) 2015 Gal Koren + +The MIT License (MIT) + +Copyright (c) 2015 Gal Koren + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +parseuri 0.0.5 - MIT +https://github.com/get/parseuri +Copyright (c) 2014 Gal Koren + +The MIT License (MIT) + +Copyright (c) 2014 Gal Koren + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +parseurl 1.3.3 - MIT +https://github.com/pillarjs/parseurl#readme +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2017 Douglas Christopher Wilson +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2017 Douglas Christopher Wilson + + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +parso 0.7.0 - MIT +Copyright (c) <2013-2017> +Copyright 2006 Google, Inc. +copyright u'parso contributors +Copyright (c) 2010 by Armin Ronacher. +Copyright David Halter and Contributors +Copyright 2004-2005 Elemental Security, Inc. +Copyright 2014 David Halter and Contributors +Copyright (c) 2014-2016 Ian Lee +Copyright (c) 2017-???? Dave Halter +Copyright (c) 2006-2009 Johann C. Rocholl +Copyright 2010 by Armin Ronacher. :license Flask Design License +Copyright (c) 2009-2014 Florent Xicluna +Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015 Python Software Foundation + +All contributions towards parso are MIT licensed. + +Some Python files have been taken from the standard library and are therefore +PSF licensed. Modifications on these files are dual licensed (both MIT and +PSF). These files are: + +- parso/pgen2/* +- parso/tokenize.py +- parso/token.py +- test/test_pgen2.py + +Also some test files under test/normalizer_issue_files have been copied from +https://github.com/PyCQA/pycodestyle (Expat License == MIT License). + +------------------------------------------------------------------------------- +The MIT License (MIT) + +Copyright (c) <2013-2017> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------------------- + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015 Python Software Foundation; All Rights Reserved" +are retained in Python alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +Copyright (c) 2010 by Armin Ronacher. + +Some rights reserved. + +Redistribution and use in source and binary forms of the theme, with or +without modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +We kindly ask you to only use these themes in an unmodified manner just +for Flask and Flask-related products, not for unrelated projects. If you +like the visual style and want to use it for your own projects, please +consider making some larger changes to the themes (such as changing +font faces, sizes, colors or margins). + +THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pascalcase 0.1.1 - MIT +https://github.com/jonschlinkert/pascalcase +Copyright (c) 2015 Jon Schlinkert +Copyright (c) 2015, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2015, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +path-dirname 1.0.2 - MIT +https://github.com/es128/path-dirname#readme +Copyright (c) Elan Shanker and Node.js contributors. + + +The MIT License (MIT) + +Copyright (c) Elan Shanker and Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +path-exists 4.0.0 - MIT +https://github.com/sindresorhus/path-exists#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +path-exists 3.0.0 - MIT +https://github.com/sindresorhus/path-exists#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +path-is-absolute 1.0.1 - MIT +https://github.com/sindresorhus/path-is-absolute#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +path-key 2.0.1 - MIT +https://github.com/sindresorhus/path-key#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +path-key 3.1.1 - MIT +https://github.com/sindresorhus/path-key#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +path-parse 1.0.6 - MIT +https://github.com/jbgutierrez/path-parse#readme +Copyright (c) 2015 Javier Blanco +(c) Javier Blanco (http://jbgutierrez.info) + +The MIT License (MIT) + +Copyright (c) 2015 Javier Blanco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +path-to-regexp 1.8.0 - MIT +https://github.com/pillarjs/path-to-regexp#readme +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pathval 1.1.0 - MIT +https://github.com/chaijs/pathval +Copyright (c) 2011-2013 Jake Luer jake@alogicalparadox.com +Copyright (c) 2012-2014 Jake Luer + +MIT License + +Copyright (c) 2011-2013 Jake Luer jake@alogicalparadox.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pdfkit 0.11.0 - MIT +http://pdfkit.org/ +(c) 2012 by Cedric Mesnil. +Copyright 2013 Google Inc. +Copyright (c) 2011 Devon Govett +Copyright (c) 2014 Devon Govett +copyright (c) 2019 Denis Pushkarev +Copyright (c) 2014-present, Facebook, Inc. +(c) 1995-2013 Jean-loup Gailly and Mark Adler +(c) 2014-2017 Vitaly Puzrin and Andrey Tupitsin +Copyright (c) 2009 Thomas Robinson <280north.com> +Copyright Joyent, Inc. and other Node contributors. +Copyright (c) 1985, 1987, 1988, 1989, 1997 Adobe Systems Incorporated. +Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. +Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. +Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. +Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. + +MIT LICENSE +Copyright (c) 2014 Devon Govett + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pend 1.2.0 - MIT +Copyright (c) 2014 Andrew Kelley + +The MIT License (Expat) + +Copyright (c) 2014 Andrew Kelley + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +performance-now 0.2.0 - MIT +https://github.com/meryn/performance-now +Copyright (c) 2013 Meryn Stol + +Copyright (c) 2013 Meryn Stol + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +performance-now 2.1.0 - MIT +https://github.com/braveg1rl/performance-now +Copyright (c) 2013 Braveg1rl +Copyright (c) 2017 Braveg1rl + +Copyright (c) 2013 Braveg1rl + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pidusage 1.2.0 - MIT +https://github.com/soyuka/pidusage +Copyright (c) 2014 + +The MIT License (MIT) + +Copyright (c) 2014 soyuka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pify 3.0.0 - MIT +https://github.com/sindresorhus/pify#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pify 2.3.0 - MIT +https://github.com/sindresorhus/pify +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pinkie 2.0.4 - MIT +(c) Vsevolod Strukchinsky (http://github.com/floatdrop) +Copyright (c) Vsevolod Strukchinsky + +The MIT License (MIT) + +Copyright (c) Vsevolod Strukchinsky (github.com/floatdrop) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pinkie-promise 2.0.1 - MIT +https://github.com/floatdrop/pinkie-promise +(c) Vsevolod Strukchinsky (http://github.com/floatdrop) +Copyright (c) Vsevolod Strukchinsky + +The MIT License (MIT) + +Copyright (c) Vsevolod Strukchinsky (github.com/floatdrop) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pixrem 4.0.1 - MIT +https://github.com/robwierzbowski/node-pixrem +Copyright (c) 2013 Robert Wierzbowski, contributors + +Copyright (c) 2013 Robert Wierzbowski, contributors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pkg-dir 4.2.0 - MIT +https://github.com/sindresorhus/pkg-dir#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pleeease-filters 4.0.0 - MIT +https://github.com/iamvdo/pleeease-filters#readme +Copyright (c) 2012 - 2013 Christian Schepp Schaefer +(c) 2014 Vincent De Oliveira · iamvdo (https://github.com/iamvdo) + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +plotly.js-dist 1.54.5 - MIT +https://github.com/plotly/plotly.js#readme +(c) Va Y +(c) Wa Y +(c) Sindre Sorhus +Copyright 2018 Mike Bostock +Copyright 2019 Mike Bostock. +Copyright (c) 2011 Google Inc. +Copyright (c) 2014-2015, Jon Schlinkert. +Copyright (c) 2009 Thomas Robinson <280north.com> +Copyright Joyent, Inc. and other Node contributors. +copyright 2016 Sean Connelly (@voidqk), http://syntheti.cc +Copyright (c) 2010-2013 Arthur Clemens, arthur@visiblearea.com +(c) Copyright 2016, Sean Connelly (@voidqk), http://syntheti.cc +(c) Copyright 2017, Sean Connelly (@voidqk), http://syntheti.cc +Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors + +The MIT License (MIT) + +Copyright (c) 2020 Plotly, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pn 1.1.0 - MIT +https://github.com/cscott/node-pn#readme +Copyright (c) 2014-2018 C. Scott Ananian + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +png-js 1.0.0 - MIT +https://github.com/devongovett/png.js#readme +Copyright (c) 2011 Devon Govett +Copyright (c) 2017 Devon Govett +Copyright (c) 2011 Mozilla Foundation + +MIT License + +Copyright (c) 2017 Devon Govett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pngjs 3.4.0 - MIT +https://github.com/lukeapage/pngjs +Copyright (c) 2015 Luke Page +Copyright (c) 2012 Kuba Niegowski +(c) 1995-2013 Jean-loup Gailly and Mark Adler +(c) 2014-2017 Vitaly Puzrin and Andrey Tupitsin +Copyright (c) 2009 Thomas Robinson <280north.com> +Copyright Joyent, Inc. and other Node contributors. + +pngjs2 original work Copyright (c) 2015 Luke Page & Original Contributors +pngjs derived work Copyright (c) 2012 Kuba Niegowski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +popper.js 1.16.0 - MIT +https://popper.js.org/ +copyright 2016 Federico Zivolo +Copyright (c) Federico Zivolo 2019 +Copyright (c) 2016 Federico Zivolo and contributors + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +portfinder 1.0.25 - MIT +https://github.com/indexzero/node-portfinder#readme +(c) 2011, Charlie Robbins +Copyright (c) 2012 Charlie Robbins + +node-portfinder + +Copyright (c) 2012 Charlie Robbins + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +posix-character-classes 0.1.1 - MIT +https://github.com/jonschlinkert/posix-character-classes +Copyright (c) 2016-2017, Jon Schlinkert +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2016-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss 6.0.23 - MIT +https://postcss.org/ +Copyright 2013 Andrey Sitnik + +The MIT License (MIT) + +Copyright 2013 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss 7.0.27 - MIT +https://postcss.org/ +Copyright 2013 Andrey Sitnik + +The MIT License (MIT) + +Copyright 2013 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-attribute-case-insensitive 2.0.0 - MIT +https://github.com/Semigradsky/postcss-attribute-case-insensitive#readme +Copyright 2016 Dmitry Semigradsky + +The MIT License (MIT) + +Copyright 2016 Dmitry Semigradsky + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-calc 6.0.2 - MIT +https://github.com/postcss/postcss-calc#readme +Copyright (c) 2014 Maxime Thirouin + +The MIT License (MIT) + +Copyright (c) 2014 Maxime Thirouin + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-color-function 4.1.0 - MIT +https://github.com/postcss/postcss-color-function#readme +Copyright (c) 2017 Maxime Thirouin & Ian Storm Taylor + +The MIT License (MIT) + +Copyright (c) 2017 Maxime Thirouin & Ian Storm Taylor + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-color-hex-alpha 3.0.0 - MIT +https://github.com/postcss/postcss-color-hex-alpha#readme +Copyright (c) 2014 Maxime Thirouin + +The MIT License (MIT) + +Copyright (c) 2014 Maxime Thirouin + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-color-hsl 2.0.0 - MIT +https://github.com/dmarchena/postcss-color-hsl +Copyright (c) 2016 David Marchena + +MIT License + +Copyright (c) 2016 David Marchena + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-color-hwb 3.0.0 - MIT +https://github.com/postcss/postcss-color-hwb#readme +Copyright (c) 2014 Maxime Thirouin + +The MIT License (MIT) + +Copyright (c) 2014 Maxime Thirouin + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-color-rebeccapurple 3.1.0 - MIT +https://github.com/postcss/postcss-color-rebeccapurple#readme +Copyright (c) 2014 Maxime Thirouin + +The MIT License (MIT) + +Copyright (c) 2014 Maxime Thirouin + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-color-rgb 2.0.0 - MIT +https://github.com/dmarchena/postcss-color-rgb +Copyright 2016 David Marchena + +The MIT License (MIT) + +Copyright 2016 David Marchena + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-color-rgba-fallback 3.0.0 - MIT +https://github.com/postcss/postcss-color-rgba-fallback#readme +Copyright (c) 2015 Guillaume Demesy + +The MIT License (MIT) + +Copyright (c) 2015 Guillaume Démésy + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-cssnext 3.1.0 - MIT +http://cssnext.io/ +Copyright (c) 2014 Maxime Thirouin + +The MIT License (MIT) + +Copyright (c) 2014 Maxime Thirouin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-custom-media 6.0.0 - MIT +https://github.com/postcss/postcss-custom-media#readme +Copyright (c) 2017 Maxime Thirouin & Nicolas Gallagher + +The MIT License (MIT) + +Copyright (c) 2017 Maxime Thirouin & Nicolas Gallagher + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-custom-properties 6.3.1 - MIT +https://github.com/postcss/postcss-custom-properties#readme +Copyright (c) 2017 Maxime Thirouin, Nicolas Gallagher & TJ Holowaychuk + +The MIT License (MIT) + +Copyright (c) 2017 Maxime Thirouin, Nicolas Gallagher & TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-custom-selectors 4.0.1 - MIT +https://github.com/postcss/postcss-custom-selectors#readme +Copyright (c) 2017 PostCSS + +The MIT License (MIT) + +Copyright (c) 2017 PostCSS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-font-variant 3.0.0 - MIT +https://github.com/postcss/postcss-font-variant#readme +Copyright (c) 2014 Maxime Thirouin & Ian Storm Taylor + +The MIT License (MIT) + +Copyright (c) 2014 Maxime Thirouin & Ian Storm Taylor + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-image-set-polyfill 0.3.5 - MIT +https://github.com/SuperOl3g/postcss-image-set-polyfill#readme +Copyright 2015 Alex + +The MIT License (MIT) + +Copyright 2015 Alex + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-import 12.0.1 - MIT +https://github.com/postcss/postcss-import#readme +Copyright (c) Maxime Thirouin, Jason Campbell & Kevin Martensson + +The MIT License (MIT) + +Copyright (c) Maxime Thirouin, Jason Campbell & Kevin Mårtensson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-initial 2.0.0 - MIT +https://github.com/maximkoretskiy/postcss-initial#readme +Copyright 2015 Maksim Koretskiy + +The MIT License (MIT) + +Copyright 2015 Maksim Koretskiy + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-load-config 2.1.0 - MIT +https://github.com/michael-ciniawsky/postcss-load-config#readme +Copyright (c) Michael Ciniawsky + +License (MIT) + +Copyright (c) Michael Ciniawsky + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-loader 3.0.0 - MIT +https://github.com/postcss/postcss-loader#readme +Copyright 2017 Andrey Sitnik + +License (MIT) + +Copyright 2017 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-media-minmax 3.0.0 - MIT +https://github.com/postcss/postcss-media-minmax#readme +Copyright (c) 2014 PostCSS + +The MIT License (MIT) + +Copyright (c) 2014 PostCSS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-media-query-parser 0.2.3 - MIT +https://github.com/dryoma/postcss-media-query-parser + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-message-helpers 2.0.0 - MIT +https://github.com/MoOx/postcss-message-helpers +Copyright (c) 2014 Maxime Thirouin + +The MIT License (MIT) + +Copyright (c) 2014 Maxime Thirouin + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-pseudoelements 5.0.0 - MIT +https://github.com/axa-ch/postcss-pseudoelements + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-replace-overflow-wrap 2.0.0 - MIT +https://github.com/MattDiMu/postcss-replace-overflow-wrap +Copyright 2016 Matthias Muller + +The MIT License (MIT) + +Copyright 2016 Matthias Müller + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-selector-matches 3.0.1 - MIT +https://github.com/postcss/postcss-selector-matches#readme +Copyright (c) 2017 Maxime Thirouin + +The MIT License (MIT) + +Copyright (c) 2017 Maxime Thirouin + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-selector-not 3.0.1 - MIT +https://github.com/postcss/postcss-selector-not#readme +Copyright (c) 2017 Maxime Thirouin + +The MIT License (MIT) + +Copyright (c) 2017 Maxime Thirouin + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-selector-parser 2.2.3 - MIT +https://github.com/postcss/postcss-selector-parser +Copyright (c) Ben Briggs (http://beneb.info) + +Copyright (c) Ben Briggs (http://beneb.info) + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-values-parser 1.5.0 - MIT +https://github.com/lesshint/postcss-values-parser#readme +Copyright (c) Andrew Powell + +Copyright (c) Andrew Powell + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +prelude-ls 1.1.2 - MIT +http://preludels.com +Copyright (c) George Zahariev + +Copyright (c) George Zahariev + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +prepend-http 2.0.0 - MIT +https://github.com/sindresorhus/prepend-http#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +prettier 2.0.2 - MIT +https://prettier.io +Copyright Google Inc. +Copyright (c) 2014-2016 Teambition +Copyright (c) Microsoft Corporation. +Copyright (c) 2014-2015, Jon Schlinkert. +Copyright (c) 2014-2016, Jon Schlinkert. +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) James Long and contributors +Copyright (c) 2014-present, Jon Schlinkert. +Copyright (c) Facebook, Inc. and its affiliates. +Copyright (c) 2014 Ivan Nikulin +Copyright 2014, 2015, 2016, 2017, 2018 Simon Lydell +Copyright (c) 2013 Yusuke Suzuki +Copyright (c) 2013-2014 Yusuke Suzuki + +Copyright © James Long and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +prettier-linter-helpers 1.0.0 - MIT +https://github.com/prettier/prettier-linter-helpers#readme +Copyright (c) 2017 Andres Suarez and Teddy Katz + +# The MIT License (MIT) + +Copyright © 2017 Andres Suarez and Teddy Katz + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pretty-format 24.9.0 - MIT +https://github.com/facebook/jest#readme +Copyright (c) Facebook, Inc. and its affiliates. + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +process-nextick-args 2.0.1 - MIT +https://github.com/calvinmetcalf/process-nextick-args +Copyright (c) 2015 Calvin Metcalf + +# Copyright (c) 2015 Calvin Metcalf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.** + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +process-nextick-args 1.0.7 - MIT +https://github.com/calvinmetcalf/process-nextick-args +Copyright (c) 2015 Calvin Metcalf + +# Copyright (c) 2015 Calvin Metcalf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.** + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +process-on-spawn 1.0.0 - MIT +https://github.com/cfware/process-on-spawn#readme +Copyright (c) 2019 CFWare, LLC + +MIT License + +Copyright (c) 2019 CFWare, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +progress 2.0.3 - MIT +https://github.com/visionmedia/node-progress#readme +Copyright (c) 2011 TJ Holowaychuk +Copyright (c) 2017 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2017 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +promise 8.0.3 - MIT +https://github.com/then/promise#readme +Copyright (c) 2014 Forbes Lindesay + +Copyright (c) 2014 Forbes Lindesay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +promise 7.3.1 - MIT +https://github.com/then/promise#readme +Copyright (c) 2014 Forbes Lindesay + +Copyright (c) 2014 Forbes Lindesay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +prop-types 15.7.2 - MIT +https://facebook.github.io/react/ +(c) Sindre Sorhus +Copyright (c) 2013-present, Facebook, Inc. +Copyright (c) Facebook, Inc. and its affiliates. + +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +proxy-addr 2.0.5 - MIT +https://github.com/jshttp/proxy-addr#readme +Copyright (c) 2014-2016 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +proxy-from-env 1.1.0 - MIT +https://github.com/Rob--W/proxy-from-env#readme +Copyright (c) 2016-2018 Rob Wu + +The MIT License + +Copyright (C) 2016-2018 Rob Wu + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +prr 1.0.1 - MIT +https://github.com/rvagg/prr +Copyright (c) 2014 Rod Vagg +(c) 2013 Rod Vagg https://github.com/rvagg/prr +Copyright (c) 2013 Rod Vagg rvagg (https://twitter.com/rvagg) + +The MIT License (MIT) +===================== + +Copyright (c) 2014 Rod Vagg +--------------------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +psl 1.2.0 - MIT +https://github.com/lupomontero/psl#readme +Copyright (c) 2017 Lupo Montero lupomontero@gmail.com +Copyright (c) 2017 Lupo Montero + +The MIT License (MIT) + +Copyright (c) 2017 Lupo Montero lupomontero@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +public-ip 3.2.0 - MIT +https://github.com/sindresorhus/public-ip#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pump 3.0.0 - MIT +https://github.com/mafintosh/pump#readme +Copyright (c) 2014 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pump 2.0.1 - MIT +https://github.com/mafintosh/pump#readme +Copyright (c) 2014 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pumpify 1.5.1 - MIT +https://github.com/mafintosh/pumpify +Copyright (c) 2014 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +punycode 2.1.1 - MIT +https://mths.be/punycode +Copyright Mathias Bynens + +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +punycode 1.4.1 - MIT +https://mths.be/punycode +Copyright Mathias Bynens + +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +python-jsonrpc-server 0.2.0 - MIT +Copyright 2017 Palantir Technologies, Inc. +Copyright 2018 Palantir Technologies, Inc. + +The MIT License (MIT) + +Copyright 2017 Palantir Technologies, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +querystringify 2.1.1 - MIT +https://github.com/unshiftio/querystringify +Copyright (c) 2015 Unshift.io, Arnout Kazemier + +The MIT License (MIT) + +Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +queue 4.5.1 - MIT +https://github.com/jessetane/queue#readme +Copyright (c) 2014 Jesse Tane + +The MIT License (MIT) +Copyright (c) 2014 Jesse Tane + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +quote-stream 1.0.2 - MIT +https://github.com/substack/quote-stream + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +raf 3.4.1 - MIT +https://github.com/chrisdickinson/raf#readme +Copyright 2013 Chris Dickinson + +Copyright 2013 Chris Dickinson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ramda 0.27.0 - MIT +https://ramdajs.com/ +(c) 2014 J. C. Phillipps. +Copyright (c) 2013-2018 Scott Sauyet and Michael Hurley + +The MIT License (MIT) + +Copyright (c) 2013-2018 Scott Sauyet and Michael Hurley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react 16.8.6 - MIT +https://reactjs.org/ +(c) Sindre Sorhus +Copyright (c) 2013-present, Facebook, Inc. +Copyright (c) Facebook, Inc. and its affiliates. + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-dom 16.8.6 - MIT +https://reactjs.org/ +Copyright (c) 2013-present, Facebook, Inc. +Copyright (c) Facebook, Inc. and its affiliates. + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-draggable 4.4.2 - MIT +https://github.com/mzabriskie/react-draggable +Copyright (c) 2017 Jed Watson. +Copyright (c) 2014-2016 Matt Zabriskie. +Copyright (c) 2013-present, Facebook, Inc. + +(MIT License) + +Copyright (c) 2014-2016 Matt Zabriskie. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-is 16.8.6 - MIT +https://reactjs.org/ +Copyright (c) Facebook, Inc. and its affiliates. + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-is 16.11.0 - MIT +https://reactjs.org/ +Copyright (c) Facebook, Inc. and its affiliates. + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-lifecycles-compat 3.0.4 - MIT +https://github.com/reactjs/react-lifecycles-compat#readme +Copyright (c) 2013-present, Facebook, Inc. + +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-markdown 4.3.1 - MIT +https://github.com/rexxars/react-markdown#readme +Copyright (c) 2015 Espen Hovlandsdal +Copyright (c) 2014-2015, Jon Schlinkert. +(c) Espen Hovlandsdal (https://espen.codes/) +Copyright (c) Facebook, Inc. and its affiliates. + +The MIT License (MIT) + +Copyright (c) 2015 Espen Hovlandsdal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-motion 0.5.2 - MIT +https://github.com/chenglou/react-motion#readme +(c) Sindre Sorhus +Copyright (c) 2015 React Motion +Copyright (c) 2013-present, Facebook, Inc. +Copyright (c) 2014-present, Facebook, Inc. + +The MIT License (MIT) + +Copyright (c) 2015 React Motion authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-move 2.9.1 - MIT +https://github.com/react-tools/react-move#readme +(c) Sindre Sorhus +Copyright (c) 2013-present, Facebook, Inc. +Copyright (c) 2017 Steven Hall and Tanner Linsley + + +MIT License + +Copyright (c) 2017 Steven Hall and Tanner Linsley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-popper 1.3.7 - MIT +https://github.com/souporserious/react-popper +Copyright (c) 2018 React Popper + +The MIT License (MIT) + +Copyright (c) 2018 React Popper authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-redux 7.1.1 - MIT +https://github.com/reduxjs/react-redux +(c) Sindre Sorhus +Copyright 2015, Yahoo! Inc. +Copyright (c) 2015-present Dan Abramov +Copyright (c) 2013-present, Facebook, Inc. + +The MIT License (MIT) + +Copyright (c) 2015-present Dan Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-svg-pan-zoom 3.1.0 - MIT +https://chrvadala.github.io/react-svg-pan-zoom/ +(c) Sindre Sorhus +Copyright 2009-2015 +(c) 2019 Denis Pushkarev +Copyright (c) 2016 Jed Watson. +Copyright (c) 2017 Jed Watson. +Copyright (c) 2015 Andreas Lubbe +(c) ,null c&&ic(c.type),b b.value,null +Copyright (c) 2012-2013 TJ Holowaychuk +Copyright (c) 2015 Tiancheng Timothy Gu +Copyright (c) 2014-2017, Jon Schlinkert. +Copyright (c) 2016 https://github.com/chrvadala +Copyright (c) Facebook, Inc. and its affiliates. +Copyright (c) 2016 Federico Zivolo and contributors +Copyright 2013-2016 by Paul Miller (http://paulmillr.com) + +MIT License + +Copyright (c) 2016 https://github.com/chrvadala + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-svgmt 1.1.8 - MIT +https://hugozap.github.io/react-svgmt +Copyright (c) 2014 +Copyright (c) 2017 Hugo Zapata +Copyright (c) 2014-2015 Waybury + +The MIT License (MIT) + +Copyright (c) 2017 Hugo Zapata + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +react-virtualized 9.21.1 - MIT +https://github.com/bvaughn/react-virtualized +Copyright (c) 2015 Brian Vaughn +Copyright (c) 2013-present, Facebook, Inc. + +The MIT License (MIT) + +Copyright (c) 2015 Brian Vaughn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +read-cache 1.0.0 - MIT +https://github.com/TrySound/read-cache#readme +(c) Bogdan Chadkin +Copyright 2016 Bogdan Chadkin + +The MIT License (MIT) + +Copyright 2016 Bogdan Chadkin + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +read-pkg 1.1.0 - MIT +https://github.com/sindresorhus/read-pkg +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +readable-stream 2.3.6 - MIT +https://github.com/nodejs/readable-stream#readme +Copyright Joyent, Inc. and other Node contributors. + +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +readable-stream 3.4.0 - MIT +https://github.com/nodejs/readable-stream#readme +Copyright Joyent, Inc. and other Node contributors. + +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +readable-stream 2.0.6 - MIT +https://github.com/nodejs/readable-stream#readme +Copyright Joyent, Inc. and other Node contributors. + +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +readable-stream 2.3.7 - MIT +https://github.com/nodejs/readable-stream#readme +Copyright Joyent, Inc. and other Node contributors. + +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +readdirp 2.2.1 - MIT +https://github.com/paulmillr/readdirp +Copyright (c) 2012-2015 Thorsten Lorenz + +This software is released under the MIT license: + +Copyright (c) 2012-2015 Thorsten Lorenz + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +reduce-css-calc 1.3.0 - MIT +https://github.com/MoOx/reduce-css-calc#readme +Copyright (c) 2014 Maxime Thirouin & Joakim Bengtson + +The MIT License (MIT) + +Copyright (c) 2014 Maxime Thirouin & Joakim Bengtson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +reduce-css-calc 2.1.7 - MIT +https://github.com/MoOx/reduce-css-calc#readme +Copyright (c) 2014 Maxime Thirouin & Joakim Bengtson + +The MIT License (MIT) + +Copyright (c) 2014 Maxime Thirouin & Joakim Bengtson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +reduce-function-call 1.0.3 - MIT +https://github.com/MoOx/reduce-function-call#readme +Copyright (c) Maxime Thirouin + +The MIT License (MIT) + +Copyright (c) Maxime Thirouin + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +redux 3.7.2 - MIT +http://redux.js.org +Copyright (c) 2015-present Dan Abramov + +The MIT License (MIT) + +Copyright (c) 2015-present Dan Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +redux 4.0.4 - MIT +http://redux.js.org +Copyright (c) 2015-present Dan Abramov + +The MIT License (MIT) + +Copyright (c) 2015-present Dan Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +redux-logger 3.0.6 - MIT +https://github.com/theaqua/redux-logger#readme +Copyright (c) 2016 Eugene Rodionov + +Copyright (c) 2016 Eugene Rodionov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +regenerator-runtime 0.13.2 - MIT +Copyright (c) 2014-present, Facebook, Inc. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +regenerator-runtime 0.11.1 - MIT +Copyright (c) 2014-present, Facebook, Inc. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +regex-not 1.0.2 - MIT +https://github.com/jonschlinkert/regex-not +Copyright (c) 2016, 2018, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2016, 2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +regexp.prototype.flags 1.3.0 - MIT +https://github.com/es-shims/RegExp.prototype.flags#readme +Copyright (c) 2014 Jordan Harband + +The MIT License (MIT) + +Copyright (C) 2014 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +remove-bom-buffer 3.0.0 - MIT +https://github.com/jonschlinkert/remove-bom-buffer +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +remove-bom-stream 1.2.0 - MIT +https://github.com/gulpjs/remove-bom-stream#readme +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +remove-files-webpack-plugin 1.4.0 - MIT +https://github.com/Amaimersion/remove-files-webpack-plugin/blob/master/README.md + +MIT License + +Copyright (c) 2020 Sergey Kuznetsov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +repeat-element 1.1.3 - MIT +https://github.com/jonschlinkert/repeat-element +Copyright (c) 2015-present, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +repeat-string 1.6.1 - MIT +https://github.com/jonschlinkert/repeat-string +Copyright (c) 2014-2015, Jon Schlinkert. +Copyright (c) 2014-2016, Jon Schlinkert. +Copyright (c) 2016, Jon Schlinkert (http://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +replace-ext 1.0.0 - MIT +https://github.com/gulpjs/replace-ext#readme +Copyright (c) 2014 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2014 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +replace-ext 0.0.1 - MIT +http://github.com/wearefractal/replace-ext +Copyright (c) 2014 Fractal + +Copyright (c) 2014 Fractal + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +request-progress 3.0.0 - MIT +https://github.com/IndigoUnited/node-request-progress#readme +Copyright (c) 2012 IndigoUnited + +Copyright (c) 2012 IndigoUnited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +require-directory 2.1.1 - MIT +https://github.com/troygoode/node-require-directory/ +Copyright (c) 2011 Troy Goode + +The MIT License (MIT) + +Copyright (c) 2011 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +requirejs 2.3.6 - MIT +http://github.com/jrburke/r.js +Copyright 2011 The Closure Compiler +Copyright (c) 2012-2014, The Dojo Foundation +Copyright 2011 Mozilla Foundation and contributors +Copyright 2014 Mozilla Foundation and contributors +Copyright jQuery Foundation and other contributors. +Copyright 2012 (c) Mihai Bazon +Copyright 2009-2011 Mozilla Foundation and contributors +Copyright JS Foundation and other contributors, https://js.foundation + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +requires-port 1.0.0 - MIT +https://github.com/unshiftio/requires-port +Copyright (c) 2015 Unshift.io, Arnout Kazemier + +The MIT License (MIT) + +Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +resize-observer-polyfill 1.5.1 - MIT +https://github.com/que-etc/resize-observer-polyfill +Copyright (c) 2016 Denis Rul + +The MIT License (MIT) + +Copyright (c) 2016 Denis Rul + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +resolve 1.11.1 - MIT +https://github.com/browserify/resolve#readme + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +resolve 1.1.7 - MIT +https://github.com/substack/node-resolve#readme + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +resolve-dir 1.0.1 - MIT +https://github.com/jonschlinkert/resolve-dir +Copyright (c) 2015, Jon Schlinkert. +Copyright (c) 2015-2016, Jon Schlinkert +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2016, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +resolve-from 5.0.0 - MIT +https://github.com/sindresorhus/resolve-from#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +resolve-options 1.1.0 - MIT +https://github.com/gulpjs/resolve-options#readme +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +resolve-url 0.2.1 - MIT +https://github.com/lydell/resolve-url +Copyright (c) 2013 Simon Lydell +Copyright 2014 Simon Lydell X11 + +The MIT License (MIT) + +Copyright (c) 2013 Simon Lydell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +responselike 1.0.2 - MIT +https://github.com/lukechilds/responselike#readme +(c) Luke Childs +Copyright (c) 2017 Luke Childs + +Copyright (c) 2017 Luke Childs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +restructure 0.5.4 - MIT +https://github.com/devongovett/restructure + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ret 0.1.15 - MIT +https://github.com/fent/ret.js#readme +Copyright (c) 2011 by Roly Fentanes + +Copyright (C) 2011 by Roly Fentanes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +rfdc 1.1.4 - MIT +https://github.com/davidmarkclements/rfdc#readme +Copyright 2019 David Mark Clements + +Copyright 2019 "David Mark Clements " + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +rgb 0.1.0 - MIT +https://github.com/kamicane/rgb + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +rgb-hex 2.1.0 - MIT +https://github.com/sindresorhus/rgb-hex#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +safe-buffer 5.1.2 - MIT +https://github.com/feross/safe-buffer +Copyright (c) Feross Aboukhadijeh +Copyright (c) Feross Aboukhadijeh (http://feross.org) + +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +safe-regex 1.1.0 - MIT +https://github.com/substack/safe-regex + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +safer-buffer 2.1.2 - MIT +https://github.com/ChALkeR/safer-buffer#readme +Copyright (c) 2018 Nikita Skovoroda + +MIT License + +Copyright (c) 2018 Nikita Skovoroda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +sanitize-html 1.20.1 - MIT +https://github.com/punkave/sanitize-html#readme +Copyright 2011 The Closure Compiler +Copyright (c) 2013, 2014, 2015 P'unk Avenue LLC +Copyright 2011 Mozilla Foundation and contributors +Copyright 2014 Mozilla Foundation and contributors +Copyright Joyent, Inc. and other Node contributors. +Copyright 2009-2011 Mozilla Foundation and contributors +(c) // http://mathiasbynens.be/notes/javascript-encoding +Copyright 2012-2016 The Dojo Foundation +Copyright JS Foundation and other contributors +Copyright jQuery Foundation and other contributors +Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +Copyright 2009-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + +Copyright (c) 2013, 2014, 2015 P'unk Avenue LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +scheduler 0.13.6 - MIT +https://reactjs.org/ +Copyright (c) Facebook, Inc. and its affiliates. + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +schema-utils 2.6.6 - MIT +https://github.com/webpack/schema-utils +Copyright JS Foundation and other contributors + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +schema-utils 2.6.2 - MIT +https://github.com/webpack/schema-utils +Copyright JS Foundation and other contributors + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +schema-utils 2.6.5 - MIT +https://github.com/webpack/schema-utils +Copyright JS Foundation and other contributors + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +set-value 2.0.1 - MIT +https://github.com/jonschlinkert/set-value +Copyright (c) 2014-2017, Jon Schlinkert +Copyright (c) 2014-2015, 2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2014-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +setimmediate 1.0.5 - MIT +https://github.com/yuzujs/setImmediate#readme +Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola + +Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +shallow-copy 0.0.1 - MIT +https://github.com/substack/shallow-copy + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +shebang-command 2.0.0 - MIT +https://github.com/kevva/shebang-command#readme +Copyright (c) Kevin Martensson + +MIT License + +Copyright (c) Kevin Mårtensson (github.com/kevva) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +shebang-command 1.2.0 - MIT +https://github.com/kevva/shebang-command#readme +(c) Kevin Martensson (http://github.com/kevva) +Copyright (c) Kevin Martensson + +The MIT License (MIT) + +Copyright (c) Kevin Martensson (github.com/kevva) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +shebang-regex 1.0.0 - MIT +https://github.com/sindresorhus/shebang-regex +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +shebang-regex 3.0.0 - MIT +https://github.com/sindresorhus/shebang-regex#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +simple-concat 1.0.0 - MIT +https://github.com/feross/simple-concat +Copyright (c) Feross Aboukhadijeh +Copyright (c) Feross Aboukhadijeh (http://feross.org). + +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +simple-get 3.1.0 - MIT +https://github.com/feross/simple-get +Copyright (c) Feross Aboukhadijeh +Copyright (c) Feross Aboukhadijeh (http://feross.org). + +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +simple-swizzle 0.2.2 - MIT +https://github.com/qix-/node-simple-swizzle#readme +Copyright (c) 2015 Josh Junon + +The MIT License (MIT) + +Copyright (c) 2015 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +slash 1.0.0 - MIT +https://github.com/sindresorhus/slash +(c) Sindre Sorhus (http://sindresorhus.com) + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +slickgrid 2.4.17 - MIT +https://github.com/6pac/SlickGrid#readme +(c) jQuery Foundation +(c) 2012 Julian Aubourg +(c) 2009-2016 Michael Leibman +Copyright (c) 2008 Ariel Flesler +Copyright 2011-2015 Twitter, Inc. +Copyright (c) 2010 Three Dub Media +(c) 2005, 2014 jQuery Foundation, Inc. +Copyright (c) 2011-2014, The Dojo Foundation +Copyright (c) 2009 John Resig, Jorn Zaefferer +Copyright 2011, Software Freedom Conservancy, Inc. +Copyright jQuery Foundation and other contributors +Copyright 2012 jQuery Foundation and other contributors +Copyright 2014 jQuery Foundation and other contributors +Copyright 2015 jQuery Foundation and other contributors +Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about) +Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net) +(c) 2005, 2013 jQuery Foundation, Inc. and other contributors +Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors +Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors +Copyright (c) 2009-2019 Michael Leibman and Ben McIntyre, http://github.com/6pac/slickgrid + +Copyright (c) 2009-2019 Michael Leibman and Ben McIntyre, http://github.com/6pac/slickgrid + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +snapdragon 0.8.2 - MIT +https://github.com/jonschlinkert/snapdragon +Copyright (c) 2015-2016, Jon Schlinkert. +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +snapdragon-node 2.1.1 - MIT +https://github.com/jonschlinkert/snapdragon-node +Copyright (c) 2017, Jon Schlinkert +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +snapdragon-util 3.0.1 - MIT +https://github.com/jonschlinkert/snapdragon-util +Copyright (c) 2017, Jon Schlinkert +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +socket.io 2.3.0 - MIT +https://github.com/socketio/socket.io#readme +Copyright (c) 2014-2018 Automattic + +(The MIT License) + +Copyright (c) 2014-2018 Automattic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +socket.io-adapter 1.1.2 - MIT +https://github.com/socketio/socket.io-adapter#readme +Copyright (c) 2014 Guillermo Rauch + +(The MIT License) + +Copyright (c) 2014 Guillermo Rauch + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +socket.io-client 2.3.0 - MIT +https://github.com/Automattic/socket.io-client#readme +(c) 2014-2019 Guillermo Rauch +Copyright (c) 2014 Guillermo Rauch +Copyright (c) 2012 Niklas von Hertzen + +The MIT License (MIT) + +Copyright (c) 2014 Guillermo Rauch + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +socket.io-parser 3.4.0 - MIT +https://github.com/Automattic/socket.io-parser#readme +Copyright (c) 2014 Guillermo Rauch + +(The MIT License) + +Copyright (c) 2014 Guillermo Rauch + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +socket.io-parser 3.3.0 - MIT +https://github.com/Automattic/socket.io-parser#readme +Copyright (c) 2014 Guillermo Rauch + +(The MIT License) + +Copyright (c) 2014 Guillermo Rauch + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +source-map-resolve 0.5.2 - MIT +https://github.com/lydell/source-map-resolve#readme +Copyright 2014 Simon Lydell X11 +Copyright 2017 Simon Lydell X11 +Copyright 2014, 2017 Simon Lydell X11 +Copyright (c) 2014, 2015, 2016, 2017 Simon Lydell +Copyright 2014, 2015, 2016, 2017 Simon Lydell X11 + +The MIT License (MIT) + +Copyright (c) 2014, 2015, 2016, 2017 Simon Lydell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +source-map-support 0.5.16 - MIT +https://github.com/evanw/node-source-map-support#readme +Copyright (c) 2014 Evan Wallace + +The MIT License (MIT) + +Copyright (c) 2014 Evan Wallace + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +source-map-url 0.4.0 - MIT +https://github.com/lydell/source-map-url#readme +Copyright (c) 2014 Simon Lydell +Copyright 2014 Simon Lydell X11 + +The MIT License (MIT) + +Copyright (c) 2014 Simon Lydell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +spdx-expression-parse 3.0.0 - MIT +https://github.com/jslicense/spdx-expression-parse.js#readme +Copyright (c) 2015 Kyle E. Mitchell + +The MIT License + +Copyright (c) 2015 Kyle E. Mitchell & other authors listed in AUTHORS + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +split 0.3.3 - MIT +http://github.com/dominictarr/split +Copyright (c) 2011 Dominic Tarr + +Copyright (c) 2011 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +split-string 3.1.0 - MIT +https://github.com/jonschlinkert/split-string +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +srcset 1.0.0 - MIT +https://github.com/sindresorhus/srcset#readme +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +sshpk 1.16.1 - MIT +https://github.com/arekinath/node-sshpk#readme +Copyright Joyent, Inc. +Copyright 2015 Joyent, Inc. +Copyright 2016 Joyent, Inc. +Copyright 2017 Joyent, Inc. +Copyright 2018 Joyent, Inc. + +Copyright Joyent, Inc. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +stack-trace 0.0.10 - MIT +https://github.com/felixge/node-stack-trace +Copyright (c) 2011 Felix Geisendorfer (felix@debuggable.com) + +Copyright (c) 2011 Felix Geisendörfer (felix@debuggable.com) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +state-toggle 1.0.3 - MIT +https://github.com/wooorm/state-toggle#readme +(c) Titus Wormer +Copyright (c) 2016 Titus Wormer + +(The MIT License) + +Copyright (c) 2016 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +static-eval 2.0.2 - MIT +https://github.com/substack/static-eval + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +static-extend 0.1.2 - MIT +https://github.com/jonschlinkert/static-extend +Copyright (c) 2016, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +static-module 3.0.3 - MIT +https://github.com/substack/static-module + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +static-module 2.2.5 - MIT +https://github.com/substack/static-module + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +statuses 1.5.0 - MIT +https://github.com/jshttp/statuses#readme +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson + + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +stream-combiner 0.0.4 - MIT +https://github.com/dominictarr/stream-combiner +Copyright (c) 2012 Dominic Tarr + +Copyright (c) 2012 'Dominic Tarr' + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +stream-shift 1.0.0 - MIT +https://github.com/mafintosh/stream-shift +Copyright (c) 2016 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2016 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +streamroller 2.2.3 - MIT +https://github.com/nomiddlename/streamroller#readme +Copyright (c) 2013 Gareth Jones + +The MIT License (MIT) + +Copyright (c) 2013 Gareth Jones + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +string_decoder 0.10.31 - MIT +https://github.com/rvagg/string_decoder +Copyright Joyent, Inc. and other Node contributors. + +Copyright Joyent, Inc. and other Node contributors. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +string_decoder 1.2.0 - MIT +https://github.com/nodejs/string_decoder +Copyright Joyent, Inc. and other Node contributors. + +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +string_decoder 1.1.1 - MIT +https://github.com/nodejs/string_decoder +Copyright Joyent, Inc. and other Node contributors. + +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +string-argv 0.3.1 - MIT +https://github.com/mccormicka/string-argv +Copyright 2014 Anthony McCormick + +The MIT License (MIT) + +Copyright 2014 Anthony McCormick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +string-width 1.0.2 - MIT +https://github.com/sindresorhus/string-width#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +string-width 4.2.0 - MIT +https://github.com/sindresorhus/string-width#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +string-width 3.1.0 - MIT +https://github.com/sindresorhus/string-width#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +string-width 2.1.1 - MIT +https://github.com/sindresorhus/string-width#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +string.prototype.trimleft 2.1.1 - MIT +https://github.com/es-shims/String.prototype.trimLeft#readme +Copyright (c) 2015 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2015 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +string.prototype.trimright 2.1.1 - MIT +https://github.com/es-shims/String.prototype.trimRight#readme +Copyright (c) 2015 Jordan Harband + +The MIT License (MIT) + +Copyright (c) 2015 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +strip-ansi 4.0.0 - MIT +https://github.com/chalk/strip-ansi#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +strip-ansi 6.0.0 - MIT +https://github.com/chalk/strip-ansi#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +strip-ansi 3.0.1 - MIT +https://github.com/chalk/strip-ansi +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +strip-ansi 5.2.0 - MIT +https://github.com/chalk/strip-ansi#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +strip-bom 2.0.0 - MIT +https://github.com/sindresorhus/strip-bom +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +strip-bom 4.0.0 - MIT +https://github.com/sindresorhus/strip-bom#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +strip-eof 1.0.0 - MIT +https://github.com/sindresorhus/strip-eof +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +strip-json-comments 2.0.1 - MIT +https://github.com/sindresorhus/strip-json-comments#readme +(c) Sindre Sorhus (http://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +sudo-prompt 8.2.5 - MIT +https://github.com/jorangreef/sudo-prompt#readme +Copyright (c) 2015 Joran Dirk Greef + +The MIT License (MIT) + +Copyright (c) 2015 Joran Dirk Greef + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +superagent 3.8.3 - MIT +https://github.com/visionmedia/superagent#readme +Copyright (c) 2014-2016 TJ Holowaychuk + +(The MIT License) + +Copyright (c) 2014-2016 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +supports-color 7.1.0 - MIT +https://github.com/chalk/supports-color#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +supports-color 6.1.0 - MIT +https://github.com/chalk/supports-color#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +supports-color 5.5.0 - MIT +https://github.com/chalk/supports-color#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +svg-to-pdfkit 0.1.8 - MIT +https://github.com/alafr/SVG-to-PDFKit#readme +(c) www.w3.org +Copyright 2013 Google Inc. +Copyright (c) 2011 Devon Govett +copyright (c) 2018 Denis Pushkarev +Copyright (c) 2014-present, Facebook, Inc. +(c) 1995-2013 Jean-loup Gailly and Mark Adler +Copyright (c) 2019 SVG-to-PDFKit contributors +(c) 2014-2017 Vitaly Puzrin and Andrey Tupitsin +Copyright (c) 2009 Thomas Robinson <280north.com> +Copyright Joyent, Inc. and other Node contributors. +Copyright 2008 World Wide Web Consortium, Massachusetts Institute +Copyright 2009 World Wide Web Consortium, Massachusetts Institute +Copyright (c) 1985, 1987, 1988, 1989, 1997 Adobe Systems Incorporated. +Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. +Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. +Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. +Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. +Copyright 2009 World Wide Web Consortium, (Massachusetts Institute of Technology, European Research Consortium for Informatics and Mathematics (ERCIM), Keio University). + +The MIT License (MIT) + +Copyright (c) 2019 SVG-to-PDFKit contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +symbol-observable 1.2.0 - MIT +https://github.com/blesh/symbol-observable#readme +Copyright (c) Ben Lesh +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) +Copyright (c) Ben Lesh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +symbol-tree 3.2.4 - MIT +https://github.com/jsdom/js-symbol-tree#symbol-tree +Copyright (c) 2015 Joris van der Wel + +The MIT License (MIT) + +Copyright (c) 2015 Joris van der Wel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +sync-request 6.1.0 - MIT +https://github.com/ForbesLindesay/sync-request#readme +Copyright (c) 2014 Forbes Lindesay + +Copyright (c) 2014 Forbes Lindesay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +sync-rpc 1.3.6 - MIT +https://github.com/ForbesLindesay/sync-rpc#readme +Copyright (c) 2013 Dominic Tarr +Copyright (c) 2017 Forbes Lindesay (https://github.com/ForbesLindesay) + +# The MIT License (MIT) + +Copyright (c) 2017 [Forbes Lindesay](https://github.com/ForbesLindesay) + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +> SOFTWARE. + + +Copyright (c) 2013 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tas-client 0.0.875 - MIT +Copyright (c) Microsoft Corporation. + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tcp-port-used 1.0.1 - MIT +https://github.com/stdarg/tcp-port-used +Copyright (c) 2013 + +The MIT License (MIT) + +Copyright (c) 2013 jut-io + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +terser-webpack-plugin 1.4.3 - MIT +https://github.com/webpack-contrib/terser-webpack-plugin +Copyright JS Foundation and other contributors + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +terser-webpack-plugin 2.3.2 - MIT +https://github.com/webpack-contrib/terser-webpack-plugin +Copyright JS Foundation and other contributors + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +text-hex 1.0.0 - MIT +https://github.com/3rd-Eden/text-hex +Copyright (c) 2014-2015 Arnout Kazemier + +The MIT License (MIT) + +Copyright (c) 2014-2015 Arnout Kazemier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +text-table 0.2.0 - MIT +https://github.com/substack/text-table + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +then-request 6.0.2 - MIT +https://github.com/then/then-request#readme +Copyright (c) 2014 Forbes Lindesay + +Copyright (c) 2014 Forbes Lindesay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +thread-loader 2.1.3 - MIT +https://github.com/webpack-contrib/thread-loader +Copyright JS Foundation and other contributors + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +throttleit 1.0.0 - MIT +https://github.com/component/throttle + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +through 2.3.8 - MIT +https://github.com/dominictarr/through +Copyright (c) 2011 Dominic Tarr + +The MIT License + +Copyright (c) 2011 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +through2 2.0.5 - MIT +https://github.com/rvagg/through2#readme +Copyright (c) Rod Vagg +Copyright (c) Rod Vagg rvagg (https://twitter.com/rvagg) and additional contributors + +# The MIT License (MIT) + +**Copyright (c) Rod Vagg (the "Original Author") and additional contributors** + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +through2-filter 3.0.0 - MIT +https://github.com/brycebaril/through2-filter#readme +Copyright (c) Bryce B. Baril + +(The MIT License) + +Copyright (c) Bryce B. Baril + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +timed-out 4.0.1 - MIT +https://github.com/floatdrop/timed-out#readme +(c) Vsevolod Strukchinsky (floatdrop@gmail.com) +Copyright (c) Vsevolod Strukchinsky + +The MIT License (MIT) + +Copyright (c) Vsevolod Strukchinsky + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +timers-browserify 2.0.11 - MIT +https://github.com/jryans/timers-browserify +Copyright Joyent, Inc. and other Node contributors. +Copyright (c) 2012 J. Ryan Stinnett + +# timers-browserify + +This project uses the [MIT](http://jryans.mit-license.org/) license: + + Copyright © 2012 J. Ryan Stinnett + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the “Software”), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + +# lib/node + +The `lib/node` directory borrows files from joyent/node which uses the following license: + + Copyright Joyent, Inc. and other Node contributors. All rights reserved. + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tiny-inflate 1.0.3 - MIT +https://github.com/devongovett/tiny-inflate +Copyright (c) 2015-present Devon Govett + +MIT License + +Copyright (c) 2015-present Devon Govett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tmp 0.0.29 - MIT +http://github.com/raszi/node-tmp +Copyright (c) 2014 KARASZI Istvan +Copyright (c) 2011-2015 KARASZI Istvan + +The MIT License (MIT) + +Copyright (c) 2014 KARASZI István + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +to-absolute-glob 2.0.2 - MIT +https://github.com/jonschlinkert/to-absolute-glob +Copyright (c) 2015-2016, Jon Schlinkert +Copyright (c) 2016, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2016, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +to-array 0.1.4 - MIT +https://github.com/Raynos/to-array +Copyright (c) 2012 Raynos. + +Copyright (c) 2012 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +to-object-path 0.3.0 - MIT +https://github.com/jonschlinkert/to-object-path +Copyright (c) 2015 Jon Schlinkert +Copyright (c) 2015, Jon Schlinkert. +Copyright (c) 2015-2016, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2015-2016, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +to-readable-stream 1.0.0 - MIT +https://github.com/sindresorhus/to-readable-stream#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +to-regex 3.0.2 - MIT +https://github.com/jonschlinkert/to-regex +Copyright (c) 2016-2018, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2016-2018, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +to-regex-range 2.1.1 - MIT +https://github.com/micromatch/to-regex-range +Copyright (c) 2015-2017, Jon Schlinkert +Copyright (c) 2015, 2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +to-regex-range 5.0.1 - MIT +https://github.com/micromatch/to-regex-range +Copyright (c) 2015-present, Jon Schlinkert. +Copyright (c) 2019, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +to-through 2.0.0 - MIT +https://github.com/gulpjs/to-through#readme +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +toidentifier 1.0.0 - MIT +https://github.com/component/toidentifier#readme +Copyright (c) 2016 Douglas Christopher Wilson +Copyright (c) 2016 Douglas Christopher Wilson + +MIT License + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +transformation-matrix 2.0.5 - MIT +https://github.com/chrvadala/transformation-matrix#readme +Copyright (c) 2017 https://github.com/chrvadala + +MIT License + +Copyright (c) 2017 https://github.com/chrvadala + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tree-kill 1.2.2 - MIT +https://github.com/pkrumins/node-tree-kill +Copyright (c) 2018 Peter Krumins + +MIT License + +Copyright (c) 2018 Peter Krumins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +trim-trailing-lines 1.1.3 - MIT +https://github.com/wooorm/trim-trailing-lines#readme +(c) Titus Wormer +Copyright (c) 2015 Titus Wormer + +(The MIT License) + +Copyright (c) 2015 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +triple-beam 1.3.0 - MIT +https://github.com/winstonjs/triple-beam#readme +Copyright (c) 2017 +(c) 2010 Charlie Robbins + +MIT License + +Copyright (c) 2017 winstonjs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +trough 1.0.5 - MIT +https://github.com/wooorm/trough#readme +(c) Titus Wormer +Copyright (c) 2016 Titus Wormer + +(The MIT License) + +Copyright (c) 2016 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ts-mockito 2.5.0 - MIT +https://github.com/NagRock/ts-mockito#readme +Copyright (c) 2016 NagRock + +MIT License + +Copyright (c) 2016 NagRock + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tslint-config-prettier 1.18.0 - MIT +https://github.com/prettier/tslint-config-prettier#readme +Copyright 2017 Alex Jover Morales + +Copyright 2017 Alex Jover Morales + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tslint-eslint-rules 5.4.0 - MIT +Copyright (c) 2015 Vitor Buzinaro, Victor Schiavi + +Copyright (c) 2015 Vitor Buzinaro, Victor Schiavi + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tslint-plugin-prettier 2.1.0 - MIT +https://github.com/prettier/tslint-plugin-prettier#readme +Copyright (c) Ika + +MIT License + +Copyright (c) Ika (https://github.com/ikatyang) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tsutils 2.28.0 - MIT +https://github.com/ajafff/tsutils#readme +Copyright (c) 2017 Klaus Meinhardt + +The MIT License (MIT) + +Copyright (c) 2017 Klaus Meinhardt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tsutils 2.29.0 - MIT +https://github.com/ajafff/tsutils#readme +Copyright (c) 2017 Klaus Meinhardt + +The MIT License (MIT) + +Copyright (c) 2017 Klaus Meinhardt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +type-check 0.3.2 - MIT +https://github.com/gkz/type-check +Copyright (c) George Zahariev + +Copyright (c) George Zahariev + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +type-detect 4.0.8 - MIT +https://github.com/chaijs/type-detect#readme +Copyright (c) 2013 +Copyright (c) 2013 Jake Luer (http://alogicalparadox.com) + +Copyright (c) 2013 Jake Luer (http://alogicalparadox.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +type-is 1.6.18 - MIT +https://github.com/jshttp/type-is#readme +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +typed-styles 0.0.7 - MIT +Copyright (c) 2017 Artur Kenzhaev + +MIT License + +Copyright (c) 2017 Artur Kenzhaev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +typedarray 0.0.6 - MIT +https://github.com/substack/typedarray +Copyright (c) 2012, Joshua Bell +Copyright (c) 2010, Linden Research, Inc. + +/* + Copyright (c) 2010, Linden Research, Inc. + Copyright (c) 2012, Joshua Bell + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + $/LicenseInfo$ + */ + +// Original can be found at: +// https://bitbucket.org/lindenlab/llsd +// Modifications by Joshua Bell inexorabletash@gmail.com +// https://github.com/inexorabletash/polyfill + +// ES3/ES5 implementation of the Krhonos Typed Array Specification +// Ref: http://www.khronos.org/registry/typedarray/specs/latest/ +// Date: 2011-02-01 +// +// Variations: +// * Allows typed_array.get/set() as alias for subscripts (typed_array[]) + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +typedarray-to-buffer 3.1.5 - MIT +http://feross.org +Copyright (c) Feross Aboukhadijeh +Copyright (c) Feross Aboukhadijeh (http://feross.org). + +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +typestyle 2.1.0 - MIT +https://github.com/typestyle/typestyle#readme +Copyright (c) 2016 + +MIT License + +Copyright (c) 2016 typestyle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ua-parser-js 0.7.20 - MIT +http://github.com/faisalman/ua-parser-js +(c) 1996-2007 the VideoLAN team +Copyright (c) 2012-2019 Faisal Salman +Copyright (c) 2012-2019 Faisal Salman + +MIT License + +Copyright (c) 2012-2019 Faisal Salman <> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +uint64be 1.0.1 - MIT +https://github.com/mafintosh/uint64be +Copyright (c) 2015 Mathias Buus + +The MIT License (MIT) + +Copyright (c) 2015 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unc-path-regex 0.1.2 - MIT +https://github.com/regexhq/unc-path-regex +Copyright (c) 2015 Jon Schlinkert +Copyright (c) 2015, Jon Schlinkert. + +The MIT License (MIT) + +Copyright (c) 2015, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +underscore 1.8.3 - MIT +http://underscorejs.org +Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +(c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors Underscore + +Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative +Reporters & Editors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unherit 1.1.3 - MIT +https://github.com/wooorm/unherit#readme +(c) Titus Wormer +Copyright (c) 2015 Titus Wormer + +(The MIT License) + +Copyright (c) 2015 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unicode 10.0.0 - MIT +http://github.com/eversport/node-unicodetable +Copyright (c) 2014 + +Copyright (c) 2014 ▟ ▖▟ ▖(dodo) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unicode-properties 1.3.1 - MIT +https://github.com/devongovett/unicode-properties +Copyright 2018 + +Copyright 2018 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unicode-trie 1.0.0 - MIT +https://github.com/devongovett/unicode-trie +Copyright 2018 + +Copyright 2018 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unicode-trie 0.3.1 - MIT +https://github.com/devongovett/unicode-trie + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unicode-trie 2.0.0 - MIT +https://github.com/devongovett/unicode-trie +Copyright 2018 + +Copyright 2018 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +union-value 1.0.1 - MIT +https://github.com/jonschlinkert/union-value +Copyright (c) 2015-2017, Jon Schlinkert +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +uniq 1.0.1 - MIT +https://github.com/mikolalysenko/uniq +(c) 2013 Mikola Lysenko. +Copyright (c) 2013 Mikola Lysenko + + +The MIT License (MIT) + +Copyright (c) 2013 Mikola Lysenko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unique-stream 2.3.1 - MIT +https://github.com/eugeneware/unique-stream#readme +Copyright 2014 Eugene Ware + +Copyright 2014 Eugene Ware + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unist-util-remove-position 1.1.4 - MIT +https://github.com/syntax-tree/unist-util-remove-position#readme +(c) Titus Wormer +Copyright (c) 2016 Titus Wormer + +(The MIT License) + +Copyright (c) 2016 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +units-css 0.4.0 - MIT +https://github.com/alexdunphy/units +(c) Alex Dunphy +(c) Lee Crossley (http://ilee.co.uk/) + +(c) Alex Dunphy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +(c) Alex Dunphy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +universalify 0.1.2 - MIT +https://github.com/RyanZim/universalify#readme +Copyright (c) 2017, Ryan Zimmerman + +(The MIT License) + +Copyright (c) 2017, Ryan Zimmerman + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unpipe 1.0.0 - MIT +https://github.com/stream-utils/unpipe +Copyright (c) 2015 Douglas Christopher Wilson +Copyright (c) 2015 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +unset-value 1.0.0 - MIT +https://github.com/jonschlinkert/unset-value +Copyright (c) 2015, 2017, Jon Schlinkert. +Copyright (c) 2017, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015, 2017, Jon Schlinkert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +untildify 3.0.3 - MIT +https://github.com/sindresorhus/untildify#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +upath 1.1.2 - MIT +http://github.com/anodynos/upath/ +Copyright (c) 2019 Angelos Pikoulas +Copyright (c) 2014-2017 Angelos Pikoulas (agelos.pikoulas@gmail.com) + +Copyright(c) 2014-2017 Angelos Pikoulas (agelos.pikoulas@gmail.com) + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +upath 1.2.0 - MIT +http://github.com/anodynos/upath/ +Copyright (c) 2019 Angelos Pikoulas +Copyright (c) 2014-2019 Angelos Pikoulas (agelos.pikoulas@gmail.com) + +Copyright(c) 2014-2019 Angelos Pikoulas (agelos.pikoulas@gmail.com) + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +upper-case 1.1.3 - MIT +https://github.com/blakeembrey/upper-case +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +urix 0.1.0 - MIT +https://github.com/lydell/urix +Copyright (c) 2013 Simon Lydell +Copyright 2014 Simon Lydell X11 + +The MIT License (MIT) + +Copyright (c) 2013 Simon Lydell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +url-parse 1.4.7 - MIT +https://github.com/unshiftio/url-parse#readme +Copyright (c) 2015 Unshift.io, Arnout Kazemier + +The MIT License (MIT) + +Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +url-parse-lax 3.0.0 - MIT +https://github.com/sindresorhus/url-parse-lax#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +use 3.1.1 - MIT +https://github.com/jonschlinkert/use +Copyright (c) 2015-2017, Jon Schlinkert. +Copyright (c) 2015-present, Jon Schlinkert. +Copyright (c) 2018, Jon Schlinkert (https://github.com/jonschlinkert). + +The MIT License (MIT) + +Copyright (c) 2015-present, Jon Schlinkert. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +util-deprecate 1.0.2 - MIT +https://github.com/TooTallNate/util-deprecate +Copyright (c) 2014 Nathan Rajlich + +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +utils-merge 1.0.1 - MIT +https://github.com/jaredhanson/utils-merge#readme +Copyright (c) 2013-2017 Jared Hanson +Copyright (c) 2013-2017 Jared Hanson < http://jaredhanson.net/ (http://jaredhanson.net/)> + +The MIT License (MIT) + +Copyright (c) 2013-2017 Jared Hanson + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +uuid 3.3.2 - MIT +https://github.com/kelektiv/node-uuid#readme +Copyright 2011, Sebastian Tschan https://blueimp.net +Copyright (c) 2010-2016 Robert Kieffer and other contributors +Copyright (c) Paul Johnston 1999 - 2009 Other contributors Greg Holt, Andrew Kepert, Ydnar, Lostinet + +The MIT License (MIT) + +Copyright (c) 2010-2016 Robert Kieffer and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +uuid 7.0.3 - MIT +https://github.com/uuidjs/uuid#readme +Copyright 2011, Sebastian Tschan https://blueimp.net +Copyright (c) 2010-2016 Robert Kieffer and other contributors +Copyright (c) Paul Johnston 1999 - 2009 Other contributors Greg Holt, Andrew Kepert, Ydnar, Lostinet + +The MIT License (MIT) + +Copyright (c) 2010-2016 Robert Kieffer and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +uuid 3.3.3 - MIT +https://github.com/kelektiv/node-uuid#readme +Copyright 2011, Sebastian Tschan https://blueimp.net +Copyright (c) 2010-2016 Robert Kieffer and other contributors +Copyright (c) Paul Johnston 1999 - 2009 Other contributors Greg Holt, Andrew Kepert, Ydnar, Lostinet + +The MIT License (MIT) + +Copyright (c) 2010-2016 Robert Kieffer and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +validator 9.4.1 - MIT +http://github.com/chriso/validator.js +Copyright (c) 2016 Chris O'Hara +Copyright (c) 2017 Chris O'Hara + +Copyright (c) 2016 Chris O'Hara + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +value-or-function 3.0.0 - MIT +https://github.com/gulpjs/value-or-function#readme +Copyright (c) 2015 Blaine Bublitz, Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2015 Blaine Bublitz, Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vary 1.1.2 - MIT +https://github.com/jshttp/vary#readme +Copyright (c) 2014-2017 Douglas Christopher Wilson + +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +verror 1.10.0 - MIT +https://github.com/davepacheco/node-verror +Copyright (c) 2016, Joyent, Inc. + +Copyright (c) 2016, Joyent, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vfile-location 2.0.6 - MIT +https://github.com/vfile/vfile-location#readme +(c) Titus Wormer +Copyright (c) 2016 Titus Wormer + +(The MIT License) + +Copyright (c) 2016 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +viewport-dimensions 0.2.0 - MIT +https://github.com/alexdunphy/viewport +(c) Alex Dunphy + +(c) Alex Dunphy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vinyl 1.2.0 - MIT +http://github.com/gulpjs/vinyl +Copyright (c) 2013 Fractal + +Copyright (c) 2013 Fractal + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vinyl 2.2.0 - MIT +https://github.com/gulpjs/vinyl#readme +Copyright (c) 2013 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2013 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vinyl 2.0.2 - MIT +https://github.com/gulpjs/vinyl#readme +Copyright (c) 2013 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2013 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vinyl-fs 3.0.3 - MIT +https://github.com/gulpjs/vinyl-fs#readme +Copyright (c) 2013-2017 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2013-2017 Blaine Bublitz , Eric Schoffstall and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vinyl-sourcemap 1.1.0 - MIT +https://github.com/gulpjs/vinyl-sourcemap#readme +Copyright (c) 2014, Florian Reiterer +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors + +The MIT License (MIT) + +Copyright (c) 2017 Blaine Bublitz , Eric Schoffstall and other contributors (Based on code from gulp-sourcemaps - ISC License - Copyright (c) 2014, Florian Reiterer) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vlq 0.2.3 - MIT +https://github.com/Rich-Harris/vlq#readme +Copyright (c) 2017 these people (https://github.com/Rich-Harris/vlq/graphs/contributors) + +Copyright (c) 2017 [these people](https://github.com/Rich-Harris/vlq/graphs/contributors) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vm-browserify 1.1.2 - MIT +https://github.com/substack/vm-browserify#readme + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vscode-debugadapter 1.35.0 - MIT +https://github.com/Microsoft/vscode-debugadapter-node#readme +Copyright (c) Microsoft Corporation. + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vscode-debugprotocol 1.35.0 - MIT +https://github.com/Microsoft/vscode-debugadapter-node#readme +Copyright (c) Microsoft Corporation. + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vscode-jsonrpc 6.0.0-next.3 - MIT +https://github.com/Microsoft/vscode-languageserver-node#readme +Copyright (c) Microsoft Corporation. + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vscode-languageclient 7.0.0-next.6 - MIT +https://github.com/Microsoft/vscode-languageserver-node#readme +Copyright (c) Microsoft Corporation. +Copyright (c) Isaac Z. Schlueter and Contributors + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vscode-languageserver 7.0.0-next.4 - MIT +https://github.com/Microsoft/vscode-languageserver-node#readme +Copyright (c) Microsoft Corporation. + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vscode-languageserver-protocol 3.16.0-next.5 - MIT +https://github.com/Microsoft/vscode-languageserver-node#readme +Copyright (c) TypeFox and others. +Copyright (c) Microsoft Corporation. + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vscode-languageserver-types 3.16.0-next.2 - MIT +https://github.com/Microsoft/vscode-languageserver-node#readme +Copyright (c) Microsoft Corporation. + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vscode-tas-client 0.0.864 - MIT + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vscode-test 1.2.3 - MIT +https://github.com/Microsoft/vscode-test#readme +Copyright (c) Microsoft Corporation. + +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +w3c-hr-time 1.0.1 - MIT + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +w3c-xmlserializer 1.1.2 - MIT +https://github.com/jsdom/w3c-xmlserializer#readme + +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +wait-for-expect 3.0.1 - MIT +https://github.com/TheBrainFamily/wait-for-expect#readme +Copyright (c) 2018 The Brain Software House + +MIT License + +Copyright (c) 2018 The Brain Software House + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +warning 4.0.3 - MIT +https://github.com/BerkeleyTrue/warning +Copyright (c) 2013-present, Facebook, Inc. +Copyright (c) 2014-present, Facebook, Inc. + +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +webpack-bundle-analyzer 3.6.0 - MIT +https://github.com/webpack-contrib/webpack-bundle-analyzer +Copyright 2015, Yahoo! Inc. +Copyright (c) 2017 Jed Watson. +(c) Michel Weststrate 2015 - 2018 +Copyright 2002-2017, Carrot Search +Copyright JS Foundation and other contributors +copyright 2018 Jason Mulligan + +Copyright JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +webpack-require-from 1.8.0 - MIT +https://github.com/agoldis/webpack-require-from#readme +Copyright (c) 2017 Andrew Goldis + +MIT License + +Copyright (c) 2017 Andrew Goldis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +webpack-sources 1.4.3 - MIT +https://github.com/webpack/webpack-sources#readme +Copyright (c) 2017 JS Foundation and other contributors + +MIT License + +Copyright (c) 2017 JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +whatwg-encoding 1.0.5 - MIT +https://github.com/jsdom/whatwg-encoding#readme +Copyright (c) 2016-2018 Domenic Denicola + +Copyright © 2016–2018 Domenic Denicola + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +whatwg-fetch 3.0.0 - MIT +https://github.com/github/fetch#readme +Copyright (c) 2014-2016 GitHub, Inc. + +Copyright (c) 2014-2016 GitHub, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +winston 3.2.1 - MIT +https://github.com/winstonjs/winston#readme +(c) 2015 Nimrod Becker +(c) 2010 Charlie Robbins +(c) 2016 Charlie Robbins +(c) 2011 Daniel Aristizabal +Copyright (c) 2010 Charlie Robbins + +Copyright (c) 2010 Charlie Robbins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +winston-transport 4.3.0 - MIT +https://github.com/winstonjs/winston-transport#readme +Copyright (c) 2015 Charlie Robbins & the contributors. + +The MIT License (MIT) + +Copyright (c) 2015 Charlie Robbins & the contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +wordwrap 1.0.0 - MIT +https://github.com/substack/node-wordwrap#readme + +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +wrap-ansi 6.2.0 - MIT +https://github.com/chalk/wrap-ansi#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +wrap-ansi 2.1.0 - MIT +https://github.com/chalk/wrap-ansi#readme +(c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +wrap-ansi 5.1.0 - MIT +https://github.com/chalk/wrap-ansi#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ws 7.2.3 - MIT +https://github.com/websockets/ws +Copyright (c) 2011 Einar Otto Stangvik + +The MIT License (MIT) + +Copyright (c) 2011 Einar Otto Stangvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ws 7.2.0 - MIT +https://github.com/websockets/ws +Copyright (c) 2011 Einar Otto Stangvik + +The MIT License (MIT) + +Copyright (c) 2011 Einar Otto Stangvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ws 6.1.4 - MIT +https://github.com/websockets/ws +Copyright (c) 2011 Einar Otto Stangvik + +The MIT License (MIT) + +Copyright (c) 2011 Einar Otto Stangvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +ws 6.2.1 - MIT +https://github.com/websockets/ws +Copyright (c) 2011 Einar Otto Stangvik + +The MIT License (MIT) + +Copyright (c) 2011 Einar Otto Stangvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +xml2js 0.2.8 - MIT +https://github.com/Leonidas-from-XIV/node-xml2js +Copyright 2010, 2011, 2012, 2013. + +Copyright 2010, 2011, 2012, 2013. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +xml2js 0.4.19 - MIT +https://github.com/Leonidas-from-XIV/node-xml2js +Copyright 2010, 2011, 2012, 2013. + +Copyright 2010, 2011, 2012, 2013. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +xmlbuilder 9.0.7 - MIT +http://github.com/oozcitak/xmlbuilder-js +Copyright (c) 2013 Ozgur Ozcitak + +The MIT License (MIT) + +Copyright (c) 2013 Ozgur Ozcitak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +xmlchars 2.1.1 - MIT +https://github.com/lddubeau/xmlchars#readme +copyright Louis-Dominique Dubeau +Copyright Louis-Dominique Dubeau and contributors + +Copyright Louis-Dominique Dubeau and contributors to xmlchars + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +xmlhttprequest 1.8.0 - MIT +https://github.com/driverdan/node-XMLHttpRequest +Copyright (c) 2010 passive.ly LLC + + Copyright (c) 2010 passive.ly LLC + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +xmlhttprequest-ssl 1.5.5 - MIT +https://github.com/mjwwit/node-XMLHttpRequest#readme +Copyright (c) 2010 passive.ly LLC + + Copyright (c) 2010 passive.ly LLC + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +xtend 4.0.2 - MIT +https://github.com/Raynos/xtend +Copyright (c) 2012-2014 Raynos. + +The MIT License (MIT) +Copyright (c) 2012-2014 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +yargs 15.3.1 - MIT +https://yargs.js.org/ +Copyright 2014 +Copyright (c) 2011 Andrei Mackenzie +Copyright 2010 James Halliday (mail@substack.net) + +Copyright 2010 James Halliday (mail@substack.net) +Modified work Copyright 2014 Contributors (ben@npmjs.com) + +This project is free software released under the MIT/X11 license: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +yargs 13.3.0 - MIT +https://yargs.js.org/ +Copyright 2014 +Copyright (c) 2011 Andrei Mackenzie +Copyright 2010 James Halliday (mail@substack.net) +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Copyright 2010 James Halliday (mail@substack.net) +Modified work Copyright 2014 Contributors (ben@npmjs.com) + +This project is free software released under the MIT/X11 license: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +yauzl 2.10.0 - MIT +https://github.com/thejoshwolfe/yauzl +Copyright (c) 2014 Josh Wolfe + +The MIT License (MIT) + +Copyright (c) 2014 Josh Wolfe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +yazl 2.5.1 - MIT +https://github.com/thejoshwolfe/yazl +Copyright (c) 2014 Josh Wolfe + +The MIT License (MIT) + +Copyright (c) 2014 Josh Wolfe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +yeast 0.1.2 - MIT +https://github.com/unshiftio/yeast +Copyright (c) 2015 Unshift.io, Arnout Kazemier + +The MIT License (MIT) + +Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +zeromq 6.0.0-beta.6 - MIT +https://github.com/zeromq/zeromq.js#readme +Copyright (c) 2017 +Copyright (c) 2011 TJ Holowaychuk +Copyright 2017-2019 Rolf Timmermans +Copyright (c) 2010, 2011 Justin Tulloss +Copyright (c) 2017-2019 Rolf Timmermans + +Copyright 2017-2019 Rolf Timmermans + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +The MIT License (MIT) +===================== + +Copyright (c) 2017 Node.js API collaborators +----------------------------------- + +*Node.js API collaborators listed at * + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +futures 3.3.0 - Python-2.0 +Copyright 2009 Brian Quinlan. +copyright u'2009-2011, Brian Quinlan +Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python +alone or in any derivative version, provided, however, that PSF's +License Agreement and PSF's notice of copyright, i.e., "Copyright (c) +2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights +Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +postcss-apply 0.8.0 - Unlicense +https://github.com/pascalduez/postcss-apply + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +tweetnacl 0.14.5 - Unlicense +https://tweetnacl.js.org + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +typescript-char 0.0.0 - Unlicense +https://github.com/mason-lang/typescript-char#readme + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and + +successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +@sheerun/mutationobserver-shim 0.3.2 - WTFPL +Copyright (c) 2014 Graeme Yeates + +Copyright © 2014 Graeme Yeates + +This work is free. You can redistribute it and/or modify it under the +terms of the Do What The Fuck You Want To Public License, Version 2, +as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. + +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +chai-as-promised 7.1.1 - WTFPL +https://github.com/domenic/chai-as-promised#readme +Copyright (c) 2004 Sam Hocevar +Copyright (c) 2012-2016 Domenic Denicola + +Copyright © 2012–2016 Domenic Denicola + +This work is free. You can redistribute it and/or modify it under the +terms of the Do What The Fuck You Want To Public License, Version 2, +as published by Sam Hocevar. See below for more details. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +truncate-utf8-bytes 1.0.2 - WTFPL +https://github.com/parshap/truncate-utf8-bytes#readme + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + +Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +utf8-byte-length 1.0.4 - WTFPL +https://github.com/parshap/utf8-byte-length#readme + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + +Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +json-schema 0.2.3 - AFL-2.1 OR BSD-3-Clause +https://github.com/kriszyp/json-schema#readme +Copyright (c) 2007 Kris Zyp SitePen (www.sitepen.com) + +AFL-2.1 OR BSD-3-Clause + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +rc 1.2.8 - BSD-2-Clause OR (MIT OR Apache-2.0) +https://github.com/dominictarr/rc#readme +Copyright (c) 2011 Dominic Tarr +Copyright (c) 2013, Dominic Tarr + +The MIT License + +Copyright (c) 2011 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +amdefine 1.0.1 - BSD-3-Clause OR MIT +http://github.com/jrburke/amdefine +Copyright (c) 2011-2016, The Dojo Foundation + +amdefine is released under two licenses: new BSD, and MIT. You may pick the +license that best suits your development needs. The text of both licenses are +provided below. + + +The "New" BSD License: +---------------------- + +Copyright (c) 2011-2016, The Dojo Foundation +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Dojo Foundation nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +MIT License +----------- + +Copyright (c) 2011-2016, The Dojo Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +sanitize-filename 1.6.3 - ISC OR WTFPL OR (ISC AND WTFPL) +https://github.com/parshap/node-sanitize-filename#readme +Copyright (c) 2004 Sam Hocevar + +This project is licensed under the [WTFPL][] and [ISC][] licenses. + +[WTFPL]: https://en.wikipedia.org/wiki/WTFPL +[ISC]: https://opensource.org/licenses/ISC + +## WTFPL + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar \ + +Everyone is permitted to copy and distribute verbatim or modified copies +of this license document, and changing it is allowed as long as the name +is changed. + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR +COPYING, DISTRIBUTION AND MODIFICATION + +0. You just DO WHAT THE FUCK YOU WANT TO. + +## ISC + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +pause-stream 0.0.11 - MIT AND Apache-2.0 +Copyright (c) 2013 Dominic Tarr + +Dual Licensed MIT and Apache 2 + +The MIT License + +Copyright (c) 2013 Dominic Tarr + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + ----------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2013 Dominic Tarr + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +type-fest 0.8.1 - MIT OR (CC0-1.0 AND MIT) +https://github.com/sindresorhus/type-fest#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +atob 2.1.2 - MIT OR Apache-2.0 +https://git.coolaj86.com/coolaj86/atob.js.git +Copyright 2015 AJ ONeal +Copyright (c) 2015 AJ ONeal +copyright 2012-2018 AJ ONeal + +At your option you may choose either of the following licenses: + + * The MIT License (MIT) + * The Apache License 2.0 (Apache-2.0) + + +The MIT License (MIT) + +Copyright (c) 2015 AJ ONeal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015 AJ ONeal + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +type-fest 0.3.1 - MIT OR CC0-1.0 +https://github.com/sindresorhus/type-fest#readme +Copyright (c) Sindre Sorhus (sindresorhus.com) + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +jsonify 0.0.0 - OTHER + +OTHER + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +vsls 0.3.1291 - OTHER +https://aka.ms/vsls +Copyright (c) Microsoft Corporation. + +MICROSOFT PRE-RELEASE SOFTWARE LICENSE TERMS + +MICROSOFT VISUAL STUDIO LIVE SHARE SOFTWARE + +These license terms are an agreement between Microsoft Corporation (or based on where you live, one of its affiliates) and you. They apply to the pre-release software named above. The terms also apply to any Microsoft services or updates for the software, except to the extent those have additional terms. + +IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW. + +1. INSTALLATION AND USE RIGHTS. You may install and use any number of copies of the software to evaluate it as you develop and test your software applications. You may use the software only with Microsoft Visual Studio or Visual Studio Code. The software works in tandem with an associated preview release service, as described below. + +2. PRE-RELEASE SOFTWARE. The software is a pre-release version. It may not work the way a final version of the software will. Microsoft may change it for the final, commercial version. We also may not release a commercial version. Microsoft is not obligated to provide maintenance, technical support or updates to you for the software. + +3. ASSOCIATED ONLINE SERVICES. + + a. Microsoft Azure Services. Some features of the software provide access to, or rely on, Azure online services, including an associated Azure online service to the software, Visual Studio Live Share (the “corresponding service”). The use of those services (but not the software) is governed by the separate terms and privacy policies in the agreement under which you obtained the Azure services at https://go.microsoft.com/fwLink/p/?LinkID=233178 (and, with respect to the corresponding service, the additional terms below). Please read them. The services may not be available in all regions. + + b. Limited Availability. The corresponding service is currently in “Preview,” and therefore, we may change or discontinue the corresponding service at any time without notice. Any changes or updates to the corresponding service may cause the software to stop working and may result in the deletion of any data stored on the corresponding service. You may not receive notice prior to these updates. + +4. Licenses for other components. The software may include third party components with separate legal notices or governed by other agreements, as described in the ThirdPartyNotices file accompanying the software. Even if such components are governed by other agreements, the disclaimers and the limitations on and exclusions of damages below also apply. + +5. DATA. + + a. Data Collection. The software may collect information about you and your use of the software, and send that to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may opt out of many of these scenarios, but not all, as described in the product documentation. In using the software, you must comply with applicable law. You can learn more about data collection and use in the help documentation and the privacy statement at http://go.microsoft.com/fwlink/?LinkId=398505. Your use of the software operates as your consent to these practices. + + b. Processing of Personal Data. To the extent Microsoft is a processor or subprocessor of personal data in connection with the software, Microsoft makes the commitments in the European Union General Data Protection Regulation Terms of the Online Services Terms to all customers effective May 25, 2018, at http://go.microsoft.com/?linkid=9840733. + +6. FEEDBACK. If you give feedback about the software to Microsoft, you give to Microsoft, without charge, the right to use, share and commercialize your feedback in any way and for any purpose. You will not give feedback that is subject to a license that requires Microsoft to license its software or documentation to third parties because we include your feedback in them. These rights survive this agreement. + +7. SCOPE OF LICENSE. The software is licensed, not sold. This agreement only gives you some rights to use the software. Microsoft reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the software only as expressly permitted in this agreement. In doing so, you must comply with any technical limitations in the software that only allow you to use it in certain ways. For example, if Microsoft technically limits or disables extensibility for the software, you may not extend the software by, among other things, loading or injecting into the software any non-Microsoft add-ins, macros, or packages; modifying the software registry settings; or adding features or functionality equivalent to that found in other Visual Studio products. You may not: + + * work around any technical limitations in the software; + + * reverse engineer, decompile or disassemble the software, or attempt to do so, except and only to the extent required by third party licensing terms governing use of certain open source components that may be included with the software; + + * remove, minimize, block or modify any notices of Microsoft or its suppliers in the software; + + * use the software in any way that is against the law; or + + * share, publish, rent or lease the software, or provide the software as a stand-alone offering for others to use. + +8. UPDATES. The software may periodically check for updates and download and install them for you. You may obtain updates only from Microsoft or authorized sources. Microsoft may need to update your system to provide you with updates. You agree to receive these automatic updates without any additional notice. Updates may not include or support all existing software features, services, or peripheral devices. + +9. EXPORT RESTRICTIONS. You must comply with all domestic and international export laws and regulations that apply to the software, which include restrictions on destinations, end users and end use. For further information on export restrictions, visit (aka.ms/exporting). + +10. SUPPORT SERVICES. Because the software is “as is,” we may not provide support services for it. + +11. ENTIRE AGREEMENT. This agreement, and the terms for supplements, updates, Internet-based services and support services that you use, are the entire agreement for the software and support services. + +12. APPLICABLE LAW. If you acquired the software in the United States, Washington State law applies to interpretation of and claims for breach of this agreement, and the laws of the state where you live apply to all other claims. If you acquired the software in any other country, its laws apply. + +13. CONSUMER RIGHTS; REGIONAL VARIATIONS. This agreement describes certain legal rights. You may have other rights, including consumer rights, under the laws of your state or country. Separate and apart from your relationship with Microsoft, you may also have rights with respect to the party from which you acquired the software. This agreement does not change those other rights if the laws of your state or country do not permit it to do so. For example, if you acquired the software in one of the below regions, or mandatory country law applies, then the following provisions apply to you: + + a. Australia. You have statutory guarantees under the Australian Consumer Law and nothing in this agreement is intended to affect those rights. + + b. Canada. If you acquired the software in Canada, you may stop receiving updates by turning off the automatic update feature, disconnecting your device from the Internet (if and when you re-connect to the Internet, however, the software will resume checking for and installing updates), or uninstalling the software. The product documentation, if any, may also specify how to turn off updates for your specific device or software. + + c. Germany and Austria. + + (i) Warranty. The properly licensed software will perform substantially as described in any Microsoft materials that accompany the software. However, Microsoft gives no contractual guarantee in relation to the licensed software. + + (ii) Limitation of Liability. In case of intentional conduct, gross negligence, claims based on the Product Liability Act, as well as, in case of death or personal or physical injury, Microsoft is liable according to the statutory law. + + Subject to the foregoing clause (ii), Microsoft will only be liable for slight negligence if Microsoft is in breach of such material contractual obligations, the fulfillment of which facilitate the due performance of this agreement, the breach of which would endanger the purpose of this agreement and the compliance with which a party may constantly trust in (so-called "cardinal obligations"). In other cases of slight negligence, Microsoft will not be liable for slight negligence. + +14. LEGAL EFFECT. This agreement describes certain legal rights. You may have other rights under the laws of your country. You may also have rights with respect to the party from whom you acquired the software. This agreement does not change your rights under the laws of your country if the laws of your country do not permit it to do so. + +15. DISCLAIMER OF WARRANTY. THE SOFTWARE IS LICENSED “AS-IS.” YOU BEAR THE RISK OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES OR CONDITIONS. TO THE EXTENT PERMITTED UNDER YOUR LOCAL LAWS, MICROSOFT EXCLUDES THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. + +16. LIMITATION ON AND EXCLUSION OF DAMAGES. YOU CAN RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES. + + This limitation applies to (a) anything related to the software, services, content (including code) on third party Internet sites, or third party programs; and (b) claims for breach of contract, breach of warranty, guarantee or condition, strict liability, negligence, or other tort to the extent permitted by applicable law. + + It also applies even if Microsoft knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your country may not allow the exclusion or limitation of incidental, consequential or other damages. + +Please note: As the software is distributed in Quebec, Canada, some of the clauses in this agreement are provided below in French. + +Remarque : Ce logiciel étant distribué au Québec, Canada, certaines des clauses dans ce contrat sont fournies ci-dessous en français. + +EXONÉRATION DE GARANTIE. Le logiciel visé par une licence est offert « tel quel ». Toute utilisation de ce logiciel est à votre seule risque et péril. Microsoft n’accorde aucune autre garantie expresse. Vous pouvez bénéficier de droits additionnels en vertu du droit local sur la protection des consommateurs, que ce contrat ne peut modifier. La ou elles sont permises par le droit locale, les garanties implicites de qualité marchande, d’adéquation à un usage particulier et d’absence de contrefaçon sont exclues. + +LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES DOMMAGES. Vous pouvez obtenir de Microsoft et de ses fournisseurs une indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US. Vous ne pouvez prétendre à aucune indemnisation pour les autres dommages, y compris les dommages spéciaux, indirects ou accessoires et pertes de bénéfices. + +Cette limitation concerne : + +* tout ce qui est relié au logiciel, aux services ou au contenu (y compris le code) figurant sur des sites Internet tiers ou dans des programmes tiers ; et + +* les réclamations au titre de violation de contrat ou de garantie, ou au titre de responsabilité stricte, de négligence ou d’une autre faute dans la limite autorisée par la loi en vigueur. + +Elle s’applique également, même si Microsoft connaissait ou devrait connaître l’éventualité d’un tel dommage. Si votre pays n’autorise pas l’exclusion ou la limitation de responsabilité pour les dommages indirects, accessoires ou de quelque nature que ce soit, il se peut que la limitation ou l’exclusion ci-dessus ne s’appliquera pas à votre égard. + +EFFET JURIDIQUE. Le présent contrat décrit certains droits juridiques. Vous pourriez avoir d’autres droits prévus par les lois de votre pays. Le présent contrat ne modifie pas les droits que vous confèrent les lois de votre pays si celles-ci ne le permettent pas. + + +------------------------------------------------------------------- + +------------------------------------------------------------------- + +path-is-inside 1.0.2 - WTFPL OR MIT +https://github.com/domenic/path-is-inside#readme +Copyright (c) 2004 Sam Hocevar +Copyright (c) 2013-2016 Domenic Denicola + +Dual licensed under WTFPL and MIT: + +--- + +Copyright © 2013–2016 Domenic Denicola + +This work is free. You can redistribute it and/or modify it under the +terms of the Do What The Fuck You Want To Public License, Version 2, +as published by Sam Hocevar. See below for more details. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + +--- + +The MIT License (MIT) + +Copyright © 2013–2016 Domenic Denicola + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +------------------------------------------------------------------- diff --git a/ThirdPartyNotices-Repository.txt b/ThirdPartyNotices-Repository.txt new file mode 100644 index 000000000000..1162db48bc9c --- /dev/null +++ b/ThirdPartyNotices-Repository.txt @@ -0,0 +1,1170 @@ + +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION +Do Not Translate or Localize + +Microsoft Python extension for Visual Studio Code incorporates third party material from the projects listed below. The original copyright notice and the license under which Microsoft received such third party material are set forth below. Microsoft reserves all other rights not expressly granted, whether by implication, estoppel or otherwise. + +1. Go for Visual Studio Code (https://github.com/Microsoft/vscode-go) +2. Files from the Python Project (https://www.python.org/) +3. Google Diff Match and Patch (https://github.com/GerHobbelt/google-diff-match-patch) +4. enchannel-zmq-backend (https://github.com/nteract/enchannel-zmq-backend) +6. omnisharp-vscode (https://github.com/OmniSharp/omnisharp-vscode) +8. PTVS (https://github.com/Microsoft/PTVS) +9. Python documentation (https://docs.python.org/) +10. python-functools32 (https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py) +11. pythonVSCode (https://github.com/DonJayamanne/pythonVSCode) +12. Sphinx (http://sphinx-doc.org/) +13. nteract (https://github.com/nteract/nteract) +14. less-plugin-inline-urls (https://github.com/less/less-plugin-inline-urls/) +15. jupyter-notebook (https://github.com/jupyter/notebook) +16. ipywidgets (https://github.com/jupyter-widgets) +17. vscode-cpptools (https://github.com/microsoft/vscode-cpptools) +18. font-awesome (https://github.com/FortAwesome/Font-Awesome) + +%% +Go for Visual Studio Code NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF +Go for Visual Studio Code NOTICES, INFORMATION, AND LICENSE + +%% Files from the Python Project NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights +Reserved" are retained in Python alone or in any derivative version prepared by +Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (C) 2006-2010 Python Software Foundation + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF Files from the Python Project NOTICES, INFORMATION, AND LICENSE + +%% Google Diff Match and Patch NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= + * Copyright 2006 Google Inc. + * http://code.google.com/p/google-diff-match-patch/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +========================================= +END OF Google Diff Match and Patch NOTICES, INFORMATION, AND LICENSE + +%% omnisharp-vscode NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF omnisharp-vscode NOTICES, INFORMATION, AND LICENSE + +%% PTVS NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Copyright (c) Microsoft Corporation. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +========================================= +END OF PTVS NOTICES, INFORMATION, AND LICENSE + +%% Python documentation NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Terms and conditions for accessing or otherwise using Python +PSF LICENSE AGREEMENT FOR PYTHON 2.7.13 +1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and + the Individual or Organization ("Licensee") accessing and otherwise using Python + 2.7.13 software in source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, + analyze, test, perform and/or display publicly, prepare derivative works, + distribute, and otherwise use Python 2.7.13 alone or in any derivative + version, provided, however, that PSF's License Agreement and PSF's notice of + copyright, i.e., "Copyright © 2001-2017 Python Software Foundation; All Rights + Reserved" are retained in Python 2.7.13 alone or in any derivative version + prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on or + incorporates Python 2.7.13 or any part thereof, and wants to make the + derivative work available to others as provided herein, then Licensee hereby + agrees to include in any such work a brief summary of the changes made to Python + 2.7.13. + +4. PSF is making Python 2.7.13 available to Licensee on an "AS IS" basis. + PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF + EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR + WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE + USE OF PYTHON 2.7.13 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 2.7.13 + FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.7.13, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any relationship + of agency, partnership, or joint venture between PSF and Licensee. This License + Agreement does not grant permission to use PSF trademarks or trade name in a + trademark sense to endorse or promote products or services of Licensee, or any + third party. + +8. By copying, installing or otherwise using Python 2.7.13, Licensee agrees + to be bound by the terms and conditions of this License Agreement. +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an office at + 160 Saratoga Avenue, Santa Clara, CA 95051, and the Individual or Organization + ("Licensee") accessing and otherwise using this software in source or binary + form and its associated documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License Agreement, + BeOpen hereby grants Licensee a non-exclusive, royalty-free, world-wide license + to reproduce, analyze, test, perform and/or display publicly, prepare derivative + works, distribute, and otherwise use the Software alone or in any derivative + version, provided, however, that the BeOpen Python License is retained in the + Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" basis. + BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF + EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND DISCLAIMS ANY REPRESENTATION OR + WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE + USE OF THE SOFTWARE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR + ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, + MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY DERIVATIVE THEREOF, EVEN IF + ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all respects + by the law of the State of California, excluding conflict of law provisions. + Nothing in this License Agreement shall be deemed to create any relationship of + agency, partnership, or joint venture between BeOpen and Licensee. This License + Agreement does not grant permission to use BeOpen trademarks or trade names in a + trademark sense to endorse or promote products or services of Licensee, or any + third party. As an exception, the "BeOpen Python" logos available at + http://www.pythonlabs.com/logos.html may be used according to the permissions + granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee agrees to be + bound by the terms and conditions of this License Agreement. +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +1. This LICENSE AGREEMENT is between the Corporation for National Research + Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 + ("CNRI"), and the Individual or Organization ("Licensee") accessing and + otherwise using Python 1.6.1 software in source or binary form and its + associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, + analyze, test, perform and/or display publicly, prepare derivative works, + distribute, and otherwise use Python 1.6.1 alone or in any derivative version, + provided, however, that CNRI's License Agreement and CNRI's notice of copyright, + i.e., "Copyright © 1995-2001 Corporation for National Research Initiatives; All + Rights Reserved" are retained in Python 1.6.1 alone or in any derivative version + prepared by Licensee. Alternately, in lieu of CNRI's License Agreement, + Licensee may substitute the following text (omitting the quotes): "Python 1.6.1 + is made available subject to the terms and conditions in CNRI's License + Agreement. This Agreement together with Python 1.6.1 may be located on the + Internet using the following unique, persistent identifier (known as a handle): + 1895.22/1013. This Agreement may also be obtained from a proxy server on the + Internet using the following URL: http://hdl.handle.net/1895.22/1013." + +3. In the event Licensee prepares a derivative work that is based on or + incorporates Python 1.6.1 or any part thereof, and wants to make the derivative + work available to others as provided herein, then Licensee hereby agrees to + include in any such work a brief summary of the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" basis. CNRI + MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, + BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY + OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF + PYTHON 1.6.1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 1.6.1 FOR + ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +7. This License Agreement shall be governed by the federal intellectual property + law of the United States, including without limitation the federal copyright + law, and, to the extent such U.S. federal law does not apply, by the law of the + Commonwealth of Virginia, excluding Virginia's conflict of law provisions. + Notwithstanding the foregoing, with regard to derivative works based on Python + 1.6.1 that incorporate non-separable material that was previously distributed + under the GNU General Public License (GPL), the law of the Commonwealth of + Virginia shall govern this License Agreement only as to issues arising under or + with respect to Paragraphs 4, 5, and 7 of this License Agreement. Nothing in + this License Agreement shall be deemed to create any relationship of agency, + partnership, or joint venture between CNRI and Licensee. This License Agreement + does not grant permission to use CNRI trademarks or trade name in a trademark + sense to endorse or promote products or services of Licensee, or any third + party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, installing + or otherwise using Python 1.6.1, Licensee agrees to be bound by the terms and + conditions of this License Agreement. +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +Copyright © 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The +Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that +the above copyright notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting documentation, and that +the name of Stichting Mathematisch Centrum or CWI not be used in advertising or +publicity pertaining to distribution of the software without specific, written +prior permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT +OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. +========================================= +END OF Python documentation NOTICES, INFORMATION, AND LICENSE + +%% python-functools32 NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012 Python Software Foundation; All Rights Reserved" are retained in Python +alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF python-functools32 NOTICES, INFORMATION, AND LICENSE + +%% pythonVSCode NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2015 DonJayamanne + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF pythonVSCode NOTICES, INFORMATION, AND LICENSE + +%% Sphinx NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Copyright (c) 2007-2017 by the Sphinx team (see AUTHORS file). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF Sphinx NOTICES, INFORMATION, AND LICENSE + + +%% nteract NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Copyright (c) 2016, nteract contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of nteract nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +========================================= +END OF nteract NOTICES, INFORMATION, AND LICENSE + +%% less-plugin-inline-urls NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +========================================= +END OF less-plugin-inline-urls NOTICES, INFORMATION, AND LICENSE + +%% jupyter notebook NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +# Licensing terms + +This project is licensed under the terms of the Modified BSD License +(also known as New or Revised or 3-Clause BSD), as follows: + +- Copyright (c) 2001-2015, IPython Development Team +- Copyright (c) 2015-, Jupyter Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the Jupyter Development Team nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## About the Jupyter Development Team + +The Jupyter Development Team is the set of all contributors to the Jupyter project. +This includes all of the Jupyter subprojects. + +The core team that coordinates development on GitHub can be found here: +https://github.com/jupyter/. + +## Our Copyright Policy + +Jupyter uses a shared copyright model. Each contributor maintains copyright +over their contributions to Jupyter. But, it is important to note that these +contributions are typically only changes to the repositories. Thus, the Jupyter +source code, in its entirety is not the copyright of any single person or +institution. Instead, it is the collective copyright of the entire Jupyter +Development Team. If individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should indicate +their copyright in the commit message of the change, when they commit the +change to one of the Jupyter repositories. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + + # Copyright (c) Jupyter Development Team. + # Distributed under the terms of the Modified BSD License. + +========================================= +END OF jupyter notebook NOTICES, INFORMATION, AND LICENSE + +%% ipywidgets NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF ipywidgets NOTICES, INFORMATION, AND LICENSE + +%% vscode-cpptools NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +vscode-cpptools + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the Software), to deal in the +Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT + +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF vscode-cpptools NOTICES, INFORMATION, AND LICENSE + +%% enchannel-zmq-backend NOTICES, INFORMATION, AND LICENSE BEGIN HERE +Copyright (c) 2016, +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of enchannel-zmq-backend nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF enchannel-zmq-backend NOTICES, INFORMATION, AND LICENSE +======= + +%% font-awesome NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Font Name - FontAwesome + +Font Awesome Free License +------------------------- + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license/free. + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) +In the Font Awesome Free download, the CC BY 4.0 license applies to all icons +packaged as SVG and JS file types. + +# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) +In the Font Awesome Free download, the SIL OFL license applies to all icons +packaged as web and desktop font files. + +# Code: MIT License (https://opensource.org/licenses/MIT) +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +# Attribution +Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +# Brand Icons +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** +========================================= +END OF font-awesome NOTICES, INFORMATION, AND LICENSE \ No newline at end of file diff --git a/build/.eslintrc b/build/.eslintrc new file mode 100644 index 000000000000..8a44c2cfe2cc --- /dev/null +++ b/build/.eslintrc @@ -0,0 +1,41 @@ +{ + "root": true, + "env": { + "node": true, + "es6": true, + "mocha": true + }, + "extends": [ + "airbnb" + ], + "rules": { + "comma-dangle": "off", + "func-names": "off", + "import/no-extraneous-dependencies": [ + "error", + { + "peerDependencies": true, + "devDependencies": true + } + ], + "import/prefer-default-export": "off", + "indent": [ + "error", + 4, + { + "SwitchCase": 1 + } + ], + "max-len": [ + "error", + { + "code": 120 + } + ], + "no-console": "off", + "no-param-reassign": "off", + "no-underscore-dangle": "off", + "no-useless-escape": "off", + "strict": "off" + } +} diff --git a/build/.mocha-multi-reporters.config b/build/.mocha-multi-reporters.config new file mode 100644 index 000000000000..539aa1c15b60 --- /dev/null +++ b/build/.mocha-multi-reporters.config @@ -0,0 +1,3 @@ +{ + "reporterEnabled": "spec,mocha-junit-reporter" +} diff --git a/build/.mocha.functional.json b/build/.mocha.functional.json new file mode 100644 index 000000000000..3cebb9367a52 --- /dev/null +++ b/build/.mocha.functional.json @@ -0,0 +1,12 @@ +{ + "spec": "./out/test/**/*.functional.test.js", + "require": ["out/test/unittests.js"], + "exclude": "out/**/*.jsx", + "reporter": "mocha-multi-reporters", + "reporter-option": "configFile=build/.mocha-multi-reporters.config", + "ui": "tdd", + "recursive": true, + "colors": true, + "exit": true, + "timeout": 180000 +} diff --git a/build/.mocha.functional.perf.json b/build/.mocha.functional.perf.json new file mode 100644 index 000000000000..d67cbb73e8f7 --- /dev/null +++ b/build/.mocha.functional.perf.json @@ -0,0 +1,11 @@ +{ + "spec": "./out/test/**/*.functional.test.js", + "exclude-out": "out/**/*.jsx", + "require": ["out/test/unittests.js"], + "reporter": "spec", + "ui": "tdd", + "recursive": true, + "colors": true, + "exit": true, + "timeout": 180000 +} diff --git a/build/.mocha.perf.config b/build/.mocha.perf.config new file mode 100644 index 000000000000..50ae73444d09 --- /dev/null +++ b/build/.mocha.perf.config @@ -0,0 +1,6 @@ +{ + "reporterEnabled": "spec,xunit", + "xunitReporterOptions": { + "output": "xunit-test-results.xml" + } +} diff --git a/build/.mocha.performance.json b/build/.mocha.performance.json new file mode 100644 index 000000000000..4e398d556887 --- /dev/null +++ b/build/.mocha.performance.json @@ -0,0 +1,12 @@ +{ + "spec": "./out/test/**/*.functional.test.js", + "require": ["out/test/unittests.js"], + "reporter": "mocha-multi-reporters", + "reporter-option": "configFile=build/.mocha.perf.config", + "ui": "tdd", + "recursive": true, + "colors": true, + "exit": true, + "timeout": 30000, + "grep": "DataScience" +} diff --git a/build/.mocha.unittests.js.json b/build/.mocha.unittests.js.json new file mode 100644 index 000000000000..a0bc134c7dc8 --- /dev/null +++ b/build/.mocha.unittests.js.json @@ -0,0 +1,9 @@ +{ + "spec": "./out/test/**/*.unit.test.js", + "require": ["source-map-support/register", "out/test/unittests.js"], + "reporter": "mocha-multi-reporters", + "reporter-option": "configFile=build/.mocha-multi-reporters.config", + "ui": "tdd", + "recursive": true, + "colors": true +} diff --git a/build/.mocha.unittests.json b/build/.mocha.unittests.json new file mode 100644 index 000000000000..b4dcc393c7eb --- /dev/null +++ b/build/.mocha.unittests.json @@ -0,0 +1,9 @@ +{ + "spec": "./out/test/**/*.unit.test.js", + "require": ["out/test/unittests.js"], + "reporter": "mocha-multi-reporters", + "reporter-option": "configFile=build/.mocha-multi-reporters.config", + "ui": "tdd", + "recursive": true, + "colors": true +} diff --git a/build/.mocha.unittests.ts.json b/build/.mocha.unittests.ts.json new file mode 100644 index 000000000000..b20e02bfa96f --- /dev/null +++ b/build/.mocha.unittests.ts.json @@ -0,0 +1,9 @@ +{ + "spec": "./src/test/**/*.unit.test.ts", + "require": ["ts-node/register", "out/test/unittests.js"], + "reporter": "mocha-multi-reporters", + "reporter-option": "configFile=build/.mocha-multi-reporters.config", + "ui": "tdd", + "recursive": true, + "colors": true +} diff --git a/build/.nycrc b/build/.nycrc new file mode 100644 index 000000000000..e9540ef130f2 --- /dev/null +++ b/build/.nycrc @@ -0,0 +1,9 @@ +{ + "extends": "@istanbuljs/nyc-config-typescript", + "all": true, + "include": [ + "src/client/**/*.ts", "src/test/**/*.js", + "src/datascience-ui/**/*.ts", "src/datascience-ui/**/*.js" + ], + "exclude": ["src/test/**/*.ts", "src/test/**/*.js"] +} \ No newline at end of file diff --git a/build/ci/TSAOptions.json b/build/ci/TSAOptions.json new file mode 100644 index 000000000000..8edad9f01b84 --- /dev/null +++ b/build/ci/TSAOptions.json @@ -0,0 +1,6 @@ +{ + "projectName": "PVSC", + "areaPath": "PVSC\\Security", + "iterationPath": "PVSC", + "allTools": true +} diff --git a/build/ci/addEnvPath.py b/build/ci/addEnvPath.py new file mode 100644 index 000000000000..abad9ec3b5c9 --- /dev/null +++ b/build/ci/addEnvPath.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +#Adds the virtual environment's executable path to json file + +import json,sys +import os.path +jsonPath = sys.argv[1] +key = sys.argv[2] + +if os.path.isfile(jsonPath): + with open(jsonPath, 'r') as read_file: + data = json.load(read_file) +else: + directory = os.path.dirname(jsonPath) + if not os.path.exists(directory): + os.makedirs(directory) + with open(jsonPath, 'w+') as read_file: + data = {} + data = {} +with open(jsonPath, 'w') as outfile: + if key == 'condaExecPath': + data[key] = sys.argv[3] + else: + data[key] = sys.executable + json.dump(data, outfile, sort_keys=True, indent=4) diff --git a/build/ci/codecov.yml b/build/ci/codecov.yml new file mode 100644 index 000000000000..8ed52955f093 --- /dev/null +++ b/build/ci/codecov.yml @@ -0,0 +1,27 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 0 + round: down + range: '70...100' + + status: + project: yes + patch: yes + changes: no + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + branch: !release* + layout: 'header, diff, files' + behavior: default + require_changes: no diff --git a/build/ci/conda_base.yml b/build/ci/conda_base.yml new file mode 100644 index 000000000000..0467c61b793d --- /dev/null +++ b/build/ci/conda_base.yml @@ -0,0 +1,5 @@ +pandas +jupyter +numpy +matplotlib +pip diff --git a/build/ci/conda_env_1.yml b/build/ci/conda_env_1.yml new file mode 100644 index 000000000000..8d5ef6398197 --- /dev/null +++ b/build/ci/conda_env_1.yml @@ -0,0 +1,8 @@ +name: conda_env_1 +dependencies: + - python=3.7 + - pandas + - jupyter + - numpy + - matplotlib + - pip diff --git a/build/ci/conda_env_2.yml b/build/ci/conda_env_2.yml new file mode 100644 index 000000000000..6ccdb55b80b7 --- /dev/null +++ b/build/ci/conda_env_2.yml @@ -0,0 +1,8 @@ +name: conda_env_2 +dependencies: + - python=3.8 + - pandas + - jupyter + - numpy + - matplotlib + - pip diff --git a/build/ci/performance/DS_test_benchmark.json b/build/ci/performance/DS_test_benchmark.json new file mode 100644 index 000000000000..5d83e811700a --- /dev/null +++ b/build/ci/performance/DS_test_benchmark.json @@ -0,0 +1,542 @@ +[ + { + "name": "DataScience gotocell tests Basic execution", + "time": 0.135 + }, + { + "name": "DataScience gotocell tests Basic edit", + "time": 0.199 + }, + { + "name": "DataScience Error Handler Functional Tests Jupyter not installed", + "time": 0.215 + }, + { + "name": "DataScience Intellisense tests Simple autocomplete", + "time": 0.623 + }, + { + "name": "DataScience Intellisense tests Multiple interpreters", + "time": 0.535 + }, + { + "name": "DataScience Intellisense tests Jupyter autocomplete", + "time": 0.425 + }, + { + "name": "DataScience Intellisense tests Jupyter autocomplete not timeout", + "time": 0.496 + }, + { + "name": "DataScience Intellisense tests Filtered Jupyter autocomplete, verify magic commands appear", + "time": 0.409 + }, + { + "name": "DataScience Intellisense tests Filtered Jupyter autocomplete, verify magic commands are filtered", + "time": 0.366 + }, + { + "name": "DataScience Interactive Panel Input Cell is displayed", + "time": 0.121 + }, + { + "name": "DataScience Interactive Window output tests Simple text", + "time": 0.528 + }, + { + "name": "DataScience Interactive Window output tests Clear output", + "time": 0.495 + }, + { + "name": "DataScience Interactive Window output tests Hide inputs", + "time": 0.559 + }, + { + "name": "DataScience Interactive Window output tests Show inputs", + "time": 0.481 + }, + { + "name": "DataScience Interactive Window output tests Expand inputs", + "time": 0.496 + }, + { + "name": "DataScience Interactive Window output tests Ctrl + 1/Ctrl + 2", + "time": 0.427 + }, + { + "name": "DataScience Interactive Window output tests Escape/Ctrl+U", + "time": 0.365 + }, + { + "name": "DataScience Interactive Window output tests Click outside cells sets focus to input box", + "time": 0.447 + }, + { + "name": "DataScience Interactive Window output tests Collapse / expand cell", + "time": 0.568 + }, + { + "name": "DataScience Interactive Window output tests Hide / show cell", + "time": 0.486 + }, + { + "name": "DataScience Interactive Window output tests Mime Types", + "time": 1.141 + }, + { + "name": "DataScience Interactive Window output tests Undo/redo commands", + "time": 0.74 + }, + { + "name": "DataScience Interactive Window output tests Click buttons", + "time": 0.706 + }, + { + "name": "DataScience Interactive Window output tests Export", + "time": 0.462 + }, + { + "name": "DataScience Interactive Window output tests Dispose test", + "time": 0.466 + }, + { + "name": "DataScience Interactive Window output tests Editor Context", + "time": 0.469 + }, + { + "name": "DataScience Interactive Window output tests Simple input", + "time": 0.516 + }, + { + "name": "DataScience Interactive Window output tests Copy to source input", + "time": 0.561 + }, + { + "name": "DataScience Interactive Window output tests Multiple input", + "time": 0.96 + }, + { + "name": "DataScience Interactive Window output tests Restart with session failure", + "time": 0.89 + }, + { + "name": "DataScience Interactive Window output tests Gather code run from text editor", + "time": 0.485 + }, + { + "name": "DataScience Interactive Window output tests Gather code run from input box", + "time": 0.502 + }, + { + "name": "DataScience Interactive Window output tests Copy back to source", + "time": 0.296 + }, + { + "name": "DataScience Interactive Window output tests Limit text output", + "time": 0.431 + }, + { + "name": "DataScience Interactive Window output tests Type in input", + "time": 0.617 + }, + { + "name": "DataScience LiveShare tests Host alone", + "time": 0.473 + }, + { + "name": "DataScience LiveShare tests Host Shutdown and Run", + "time": 0.596 + }, + { + "name": "DataScience LiveShare tests Going through codewatcher", + "time": 0.711 + }, + { + "name": "DataScience LiveShare tests Export from guest", + "time": 0.138 + }, + { + "name": "DataScience LiveShare tests Guest does not have extension", + "time": 1.839 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Simple text", + "time": 0.596 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Invalid session still runs", + "time": 0.515 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Invalid kernel still runs", + "time": 0.563 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Mime Types", + "time": 1.979 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Click buttons", + "time": 0.551 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Server already loaded", + "time": 2.183 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Server load skipped", + "time": 0.672 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Convert to python", + "time": 0.563 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Save As", + "time": 0.516 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests RunAllCells", + "time": 0.629 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Startup and shutdown", + "time": 1.198 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Failure", + "time": 0.745 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Selection/Focus Markdown saved when selecting another cell", + "time": 0.464 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Model updates Add a cell and undo", + "time": 0.179 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Model updates Edit a cell and undo", + "time": 0.962 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Model updates Remove, move, and undo", + "time": 0.339 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Model updates Update as user types into editor (update redux store and model)", + "time": 0.609 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Model updates Updates are not lost when switching to markdown (update redux store and model)", + "time": 0.698 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Traverse cells by using ArrowUp and ArrowDown, k and j", + "time": 0.307 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Traverse cells by using ArrowUp and ArrowDown, k and j", + "time": 0.155 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Pressing 'Shift+Enter' on a selected cell executes the cell and advances to the next cell", + "time": 0.529 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Pressing 'Ctrl+Enter' on a selected cell executes the cell and cell selection is not changed", + "time": 0.283 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Pressing 'Alt+Enter' on a selected cell adds a new cell below it", + "time": 0.158 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Auto brackets work", + "time": 0.343 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Navigating cells using up/down keys while focus is set to editor", + "time": 0.154 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Navigating cells using up/down keys through code & markdown cells, while focus is set to editor", + "time": 1.632 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Pressing 'a' on a selected cell adds a cell at the current position", + "time": 0.201 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Pressing 'b' on a selected cell adds a cell after the current position", + "time": 0.21 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Toggle visibility of output", + "time": 0.358 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Toggle markdown and code modes using 'y' and 'm' keys (cells should not be focused)", + "time": 0.274 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Toggle markdown and code modes using 'y' and 'm' keys & ensure changes to cells is preserved", + "time": 0.568 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Test undo using the key 'z'", + "time": 2.229 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Test save using the key 'ctrl+s' on Windows", + "time": 0.785 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Test save using the key 'ctrl+s' on Mac", + "time": 1.685 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Test save using the key 'cmd+s' on a Mac", + "time": 0.655 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Keyboard Shortcuts Test save using the key 'cmd+s' on a Windows", + "time": 1.671 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Auto Save Auto save notebook every 1s", + "time": 3.03 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Auto Save File saved with same format", + "time": 2.031 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Auto Save Should not auto save notebook, ever", + "time": 5.039 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Auto Save Auto save notebook when focus changes from active editor to none", + "time": 0.425 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Auto Save Auto save notebook when focus changes from active editor to something else", + "time": 0.435 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Auto Save Should not auto save notebook when active editor changes", + "time": 5.014 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Auto Save Auto save notebook when window state changes to being not focused", + "time": 0.456 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Auto Save Auto save notebook when window state changes to being focused", + "time": 0.649 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Auto Save Auto save notebook when window state changes to being focused for focusChange", + "time": 0.471 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Auto Save Auto save notebook when window state changes to being not focused for focusChange", + "time": 0.48 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Auto Save Auto save notebook when view state changes", + "time": 0.495 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Update Metadata Update notebook metadata on execution", + "time": 0.885 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Clear Outputs Clear Outputs in WebView", + "time": 0.479 + }, + { + "name": "DataScience Native Editor Without Custom Editor API Editor tests Clear Outputs Clear execution_count and outputs in notebook", + "time": 0.508 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Simple text", + "time": 0.966 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Invalid session still runs", + "time": 0.894 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Invalid kernel still runs", + "time": 0.946 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Mime Types", + "time": 3.283 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Click buttons", + "time": 0.984 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Server already loaded", + "time": 2.298 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Server load skipped", + "time": 0.778 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Convert to python", + "time": 1.022 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests RunAllCells", + "time": 1.048 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Startup and shutdown", + "time": 2.3 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Failure", + "time": 1.219 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Selection/Focus Markdown saved when selecting another cell", + "time": 1.319 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Model updates Add a cell and undo", + "time": 0.257 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Model updates Edit a cell and undo", + "time": 2.682 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Model updates Remove, move, and undo", + "time": 0.709 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Model updates Update as user types into editor (update redux store and model)", + "time": 1.802 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Model updates Updates are not lost when switching to markdown (update redux store and model)", + "time": 1.91 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Traverse cells by using ArrowUp and ArrowDown, k and j", + "time": 0.341 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Traverse cells by using ArrowUp and ArrowDown, k and j", + "time": 0.164 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Pressing 'Shift+Enter' on a selected cell executes the cell and advances to the next cell", + "time": 0.987 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Pressing 'Ctrl+Enter' on a selected cell executes the cell and cell selection is not changed", + "time": 0.425 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Pressing 'Alt+Enter' on a selected cell adds a new cell below it", + "time": 0.287 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Auto brackets work", + "time": 0.796 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Navigating cells using up/down keys while focus is set to editor", + "time": 0.146 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Navigating cells using up/down keys through code & markdown cells, while focus is set to editor", + "time": 3.292 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Pressing 'a' on a selected cell adds a cell at the current position", + "time": 0.331 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Pressing 'b' on a selected cell adds a cell after the current position", + "time": 0.349 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Toggle visibility of output", + "time": 0.509 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Toggle markdown and code modes using 'y' and 'm' keys (cells should not be focused)", + "time": 0.358 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Keyboard Shortcuts Toggle markdown and code modes using 'y' and 'm' keys & ensure changes to cells is preserved", + "time": 1.251 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Update Metadata Update notebook metadata on execution", + "time": 1.242 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Clear Outputs Clear Outputs in WebView", + "time": 0.593 + }, + { + "name": "DataScience Native Editor With Custom Editor API Editor tests Clear Outputs Clear execution_count and outputs in notebook", + "time": 0.614 + }, + { + "name": "DataScience notebook tests Verify manual working directory", + "time": 0.117 + }, + { + "name": "DataScience notebook tests Verify ${fileDirname} working directory", + "time": 0.353 + }, + { + "name": "DataScience notebook tests Change Interpreter", + "time": 0.218 + }, + { + "name": "DataScience notebook tests Restart kernel", + "time": 0.492 + }, + { + "name": "DataScience notebook tests Cancel execution", + "time": 0.318 + }, + { + "name": "DataScience notebook tests Interrupt kernel", + "time": 15.383 + }, + { + "name": "DataScience notebook tests MimeTypes", + "time": 0.603 + }, + { + "name": "DataScience notebook tests Non default config fails", + "time": 0.104 + }, + { + "name": "DataScience notebook tests Invalid kernel spec works", + "time": 0.151 + }, + { + "name": "DataScience notebook tests Server cache working", + "time": 0.255 + }, + { + "name": "DataScience notebook tests Server death", + "time": 0.282 + }, + { + "name": "DataScience notebook tests Execution logging", + "time": 0.152 + } +] diff --git a/build/ci/performance/checkPerformanceResults.js b/build/ci/performance/checkPerformanceResults.js new file mode 100644 index 000000000000..f632fd0b5224 --- /dev/null +++ b/build/ci/performance/checkPerformanceResults.js @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +const fs = require('fs'); +const path = require('path'); +const constants = require('../../constants'); + +const benchmarkFile = path.join(constants.ExtensionRootDir, 'build', 'ci', 'performance', 'DS_test_benchmark.json'); +const performanceResultsFile = path.join( + constants.ExtensionRootDir, + 'build', + 'ci', + 'performance', + 'performance-results.json' +); +const errorMargin = 1.1; +let failedTests = ''; + +function getFailingTimesString(missedTimes) { + let printValue = ''; + for (const time of missedTimes) { + printValue += String(time) + ', '; + } + return printValue.substring(0, printValue.length - 2); +} + +fs.readFile(benchmarkFile, 'utf8', (benchmarkError, benchmark) => { + if (benchmarkError) { + throw benchmarkError; + } + + fs.readFile(performanceResultsFile, 'utf8', (performanceResultsFileError, performanceData) => { + if (performanceResultsFileError) { + throw performanceResultsFileError; + } + + const benchmarkJson = JSON.parse(benchmark); + const performanceJson = JSON.parse(performanceData); + + performanceJson.forEach((result) => { + const cleanTimes = result.times.filter((x) => x !== 'S' && x !== 'F'); + const n = cleanTimes.length; + const testcase = benchmarkJson.find((x) => x.name === result.name); + + if (testcase && testcase.time !== 'S') { + if (n === 0 && result.times.every((t) => t === 'F')) { + // Test failed every time + failedTests += 'Failed every time: ' + testcase.name + '\n'; + } else { + let missedTimes = []; + for (let time of cleanTimes) { + if (parseFloat(time) > parseFloat(testcase.time) * errorMargin) { + missedTimes.push(parseFloat(time)); + } + } + + if (missedTimes.length >= 2) { + const skippedTimes = result.times.filter((t) => t === 'S'); + const failedTimes = result.times.filter((t) => t === 'F'); + + failedTests += + 'Performance is slow in: ' + + testcase.name + + '.\n\tBenchmark time: ' + + String(parseFloat(testcase.time) * errorMargin) + + '\n\tTimes the test missed the benchmark: ' + + missedTimes.length + + '\n\tFailing times: ' + + getFailingTimesString(missedTimes) + + '\n\tTimes it was skipped: ' + + skippedTimes.length + + '\n\tTimes it failed: ' + + failedTimes.length + + '\n'; + } + } + } + }); + + // Delete performance-results.json + fs.unlink(performanceResultsFile, (deleteError) => { + if (deleteError) { + if (failedTests.length > 0) { + console.log(failedTests); + } + throw deleteError; + } + }); + + if (failedTests.length > 0) { + throw new Error(failedTests); + } + }); +}); diff --git a/build/ci/performance/createNewPerformanceBenchmark.js b/build/ci/performance/createNewPerformanceBenchmark.js new file mode 100644 index 000000000000..7cdf93d3ac81 --- /dev/null +++ b/build/ci/performance/createNewPerformanceBenchmark.js @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +const fastXmlParser = require('fast-xml-parser'); +const fs = require('fs'); +const path = require('path'); +const constants = require('../../constants'); + +const xmlFile = path.join(constants.ExtensionRootDir, 'xunit-test-results.xml'); +let performanceData = []; + +function getTime(testcase) { + if (testcase.failure) { + return 'F'; + } else if (testcase.skipped === '') { + return 'S'; + } + return parseFloat(testcase.time); +} + +fs.readFile(xmlFile, 'utf8', (xmlReadError, xmlData) => { + if (xmlReadError) { + throw xmlReadError; + } + + if (fastXmlParser.validate(xmlData)) { + const defaultOptions = { + attributeNamePrefix: '', + ignoreAttributes: false + }; + const jsonObj = fastXmlParser.parse(xmlData, defaultOptions); + + jsonObj.testsuite.testcase.forEach((testcase) => { + const test = { + name: testcase.classname + ' ' + testcase.name, + time: getTime(testcase) + }; + + if (test.time !== 'S' && test.time > 0.1) { + performanceData.push(test); + } + }); + + fs.writeFile( + path.join(constants.ExtensionRootDir, 'build', 'ci', 'performance', 'DS_test_benchmark.json'), + JSON.stringify(performanceData, null, 2), + (writeResultsError) => { + if (writeResultsError) { + throw writeResultsError; + } + console.log('DS_test_benchmark.json was saved!'); + } + ); + } +}); diff --git a/build/ci/performance/perfJobs.yaml b/build/ci/performance/perfJobs.yaml new file mode 100644 index 000000000000..f902ff0c5f9d --- /dev/null +++ b/build/ci/performance/perfJobs.yaml @@ -0,0 +1,79 @@ +jobs: + - job: Testing_Round_1 + timeoutInMinutes: 120 + strategy: + matrix: + Python37: + PythonVersion: '3.7' + pool: DSPerf + steps: + - task: Npm@1 + displayName: 'npm ci' + inputs: + workingDir: ${{ parameters.workingDirectory }} + command: custom + verbose: true + customCommand: ci + - task: Gulp@0 + displayName: 'Compile and check for errors' + inputs: + targets: 'prePublishNonBundle' + - powershell: | + mocha --require source-map-support/register --config ./build/.mocha.performance.json + node ./build/ci/performance/savePerformanceResults.js + + - job: Testing_Round_2 + dependsOn: + - Testing_Round_1 + timeoutInMinutes: 120 + strategy: + matrix: + Python37: + PythonVersion: '3.7' + pool: DSPerf + steps: + - powershell: | + mocha --require source-map-support/register --config ./build/.mocha.performance.json + node ./build/ci/performance/savePerformanceResults.js + + - job: Testing_Round_3 + dependsOn: + - Testing_Round_2 + timeoutInMinutes: 120 + strategy: + matrix: + Python37: + PythonVersion: '3.7' + pool: DSPerf + steps: + - powershell: | + mocha --require source-map-support/register --config ./build/.mocha.performance.json + node ./build/ci/performance/savePerformanceResults.js + + - job: Testing_Round_4 + dependsOn: + - Testing_Round_3 + timeoutInMinutes: 120 + strategy: + matrix: + Python37: + PythonVersion: '3.7' + pool: DSPerf + steps: + - powershell: | + mocha --require source-map-support/register --config ./build/.mocha.performance.json + node ./build/ci/performance/savePerformanceResults.js + + - job: Testing_Round_5 + dependsOn: + - Testing_Round_4 + timeoutInMinutes: 120 + strategy: + matrix: + Python37: + PythonVersion: '3.7' + pool: DSPerf + steps: + - powershell: | + mocha --require source-map-support/register --config ./build/.mocha.performance.json + node ./build/ci/performance/savePerformanceResults.js diff --git a/build/ci/performance/savePerformanceResults.js b/build/ci/performance/savePerformanceResults.js new file mode 100644 index 000000000000..456eb254331b --- /dev/null +++ b/build/ci/performance/savePerformanceResults.js @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +const fastXmlParser = require('fast-xml-parser'); +const fs = require('fs'); +const path = require('path'); +const constants = require('../../constants'); + +const xmlFile = path.join(constants.ExtensionRootDir, 'xunit-test-results.xml'); +const jsonFile = path.join(constants.ExtensionRootDir, 'build', 'ci', 'performance', 'performance-results.json'); +let performanceData = []; + +function getTime(testcase) { + if (testcase.failure) { + return 'F'; + } else if (testcase.skipped === '') { + return 'S'; + } + return parseFloat(testcase.time); +} + +fs.readFile(xmlFile, 'utf8', (xmlReadError, xmlData) => { + if (xmlReadError) { + throw xmlReadError; + } + + if (fastXmlParser.validate(xmlData)) { + const defaultOptions = { + attributeNamePrefix: '', + ignoreAttributes: false + }; + const jsonObj = fastXmlParser.parse(xmlData, defaultOptions); + + fs.readFile(jsonFile, 'utf8', (jsonReadError, data) => { + if (jsonReadError) { + // File doesn't exist, so we create it + jsonObj.testsuite.testcase.forEach((testcase) => { + const test = { + name: testcase.classname + ' ' + testcase.name, + times: [getTime(testcase)] + }; + + performanceData.push(test); + }); + } else { + performanceData = JSON.parse(data); + + jsonObj.testsuite.testcase.forEach((testcase) => { + let test = performanceData.find((x) => x.name === testcase.classname + ' ' + testcase.name); + let time = getTime(testcase); + + if (test) { + // if the test name is already there, we add the new time + test.times.push(time); + } else { + // if its not there, we add the whole thing + const test = { + name: testcase.classname + ' ' + testcase.name, + times: [time] + }; + + performanceData.push(test); + } + }); + } + + fs.writeFile( + path.join(constants.ExtensionRootDir, 'build', 'ci', 'performance', 'performance-results.json'), + JSON.stringify(performanceData, null, 2), + (writeResultsError) => { + if (writeResultsError) { + throw writeResultsError; + } + console.log('performance-results.json was saved!'); + } + ); + }); + } +}); diff --git a/build/ci/performance/vscode-python-performance.yaml b/build/ci/performance/vscode-python-performance.yaml new file mode 100644 index 000000000000..c0de78757a36 --- /dev/null +++ b/build/ci/performance/vscode-python-performance.yaml @@ -0,0 +1,26 @@ +name: '$(Year:yyyy).$(Month).0.$(BuildID)-weekly-performance-test' + +trigger: none +pr: none +schedules: + - cron: '0 0 * * sat' + displayName: Weekly Performance Test + branches: + include: + - main + +stages: + - stage: Test_performance + jobs: + - template: perfJobs.yaml + + - stage: Results + dependsOn: + - Test_performance + jobs: + - job: CheckResults + timeoutInMinutes: 5 + pool: DSPerf + steps: + - powershell: | + node ./build/ci/performance/checkPerformanceResults.js diff --git a/build/ci/postInstall.js b/build/ci/postInstall.js new file mode 100644 index 000000000000..be42594e0504 --- /dev/null +++ b/build/ci/postInstall.js @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +var colors = require('colors/safe'); +var fs = require('fs'); +var path = require('path'); +var constants_1 = require('../constants'); +/** + * In order to compile the extension in strict mode, one of the dependencies (@jupyterlab) has some files that + * just won't compile in strict mode. + * Unfortunately we cannot fix it by overriding their type definitions + * Note: that has been done for a few of the JupyterLabl files (see typings/index.d.ts). + * The solution is to modify the type definition file after `npm install`. + */ +function fixJupyterLabDTSFiles() { + var relativePath = path.join( + 'node_modules', + '@jupyterlab', + 'services', + 'node_modules', + '@jupyterlab', + 'coreutils', + 'lib', + 'settingregistry.d.ts' + ); + var filePath = path.join(constants_1.ExtensionRootDir, relativePath); + if (!fs.existsSync(filePath)) { + throw new Error("Type Definition file from JupyterLab not found '" + filePath + "' (pvsc post install script)"); + } + var fileContents = fs.readFileSync(filePath, { encoding: 'utf8' }); + if (fileContents.indexOf('[key: string]: ISchema | undefined;') > 0) { + // tslint:disable-next-line:no-console + console.log(colors.blue(relativePath + ' file already updated (by Python VSC)')); + return; + } + if (fileContents.indexOf('[key: string]: ISchema;') > 0) { + var replacedText = fileContents.replace('[key: string]: ISchema;', '[key: string]: ISchema | undefined;'); + if (fileContents === replacedText) { + throw new Error("Fix for JupyterLabl file 'settingregistry.d.ts' failed (pvsc post install script)"); + } + fs.writeFileSync(filePath, replacedText); + // tslint:disable-next-line:no-console + console.log(colors.green(relativePath + ' file updated (by Python VSC)')); + } else { + // tslint:disable-next-line:no-console + console.log(colors.red(relativePath + ' file does not need updating.')); + } +} + +/** + * In order to get raw kernels working, we reuse the default kernel that jupyterlab ships. + * However it expects to be talking to a websocket which is serializing the messages to strings. + * Our raw kernel is not a web socket and needs to do its own serialization. To do so, we make a copy + * of the default kernel with the serialization stripped out. This is simpler than making a copy of the module + * at runtime. + */ +function createJupyterKernelWithoutSerialization() { + var relativePath = path.join('node_modules', '@jupyterlab', 'services', 'lib', 'kernel', 'default.js'); + var filePath = path.join(constants_1.ExtensionRootDir, relativePath); + if (!fs.existsSync(filePath)) { + throw new Error("Jupyter lab default kernel not found '" + filePath + "' (pvsc post install script)"); + } + var fileContents = fs.readFileSync(filePath, { encoding: 'utf8' }); + var replacedContents = fileContents.replace( + /^const serialize =.*$/gm, + 'const serialize = { serialize: (a) => a, deserialize: (a) => a };' + ); + if (replacedContents === fileContents) { + throw new Error('Jupyter lab default kernel cannot be made non serializing'); + } + var destPath = path.join(path.dirname(filePath), 'nonSerializingKernel.js'); + fs.writeFileSync(destPath, replacedContents); + console.log(colors.green(destPath + ' file generated (by Python VSC)')); +} + +fixJupyterLabDTSFiles(); +createJupyterKernelWithoutSerialization(); diff --git a/build/ci/scripts/runFunctionalTests.js b/build/ci/scripts/runFunctionalTests.js new file mode 100644 index 000000000000..4171900586da --- /dev/null +++ b/build/ci/scripts/runFunctionalTests.js @@ -0,0 +1,82 @@ +// This script will run all of the functional tests for each functional test file in sequence +// This prevents mocha from running out of memory when running all of the tests +// +// This could potentially be improved to run tests in parallel (try that later) +// +// Additionally this was written in python at first but running python on an azure +// machine may pick up an invalid environment for the subprocess. Node doesn't have this problem +var path = require('path'); +var glob = require('glob'); +var child_process = require('child_process'); + +// Create a base for the output file +var originalMochaFile = process.env['MOCHA_FILE']; +var mochaFile = originalMochaFile || './test-results.xml'; +var mochaBaseFile = path.join(path.dirname(mochaFile), path.basename(mochaFile, '.xml')); +var mochaFileExt = '.xml'; + +// Wrap async code in a function so can wait till done +async function main() { + console.log('Globbing files for functional tests'); + + // Glob all of the files that we usually send to mocha as a group (see mocha.functional.opts.xml) + var files = await new Promise((resolve, reject) => { + glob('./out/test/**/*.functional.test.js', (ex, res) => { + if (ex) { + reject(ex); + } else { + resolve(res); + } + }); + }); + + // Iterate over them, running mocha on each + var returnCode = 0; + + // Go through each one at a time + try { + for (var index = 0; index < files.length; index += 1) { + // Each run with a file will expect a $MOCHA_FILE$ variable. Generate one for each + // Note: this index is used as a pattern when setting mocha file in the test_phases.yml + var subMochaFile = `${mochaBaseFile}_${index}_${path.basename(files[index])}${mochaFileExt}`; + process.env['MOCHA_FILE'] = subMochaFile; + var exitCode = await new Promise((resolve) => { + // Spawn the sub node process + var proc = child_process.fork('./node_modules/mocha/bin/_mocha', [ + files[index], + '--require=out/test/unittests.js', + '--exclude=out/**/*.jsx', + '--reporter=mocha-multi-reporters', + '--reporter-option=configFile=build/.mocha-multi-reporters.config', + '--ui=tdd', + '--recursive', + '--colors', + '--exit', + '--timeout=180000' + ]); + proc.on('exit', resolve); + }); + + // If failed keep track + if (exitCode !== 0) { + console.log(`Functional tests for ${files[index]} failed.`); + returnCode = exitCode; + } + } + } catch (ex) { + console.log(`Functional tests run failure: ${ex}.`); + returnCode = -1; + } + + // Reset the mocha file variable + if (originalMochaFile) { + process.env['MOCHA_FILE'] = originalMochaFile; + } + + // Indicate error code + console.log(`Functional test run result: ${returnCode}`); + process.exit(returnCode); +} + +// Call the main function. It will exit when promise is finished. +main(); diff --git a/build/ci/static_analysis/credscan/CredScanSuppressions.json b/build/ci/static_analysis/credscan/CredScanSuppressions.json new file mode 100644 index 000000000000..a3e8d5418561 --- /dev/null +++ b/build/ci/static_analysis/credscan/CredScanSuppressions.json @@ -0,0 +1,13 @@ +{ + "tool": "Credential Scanner", + "suppressions": [ + { + "file": "src\\test\\datascience\\serverConfigFiles\\jkey.key", + "_justification": "Key file used for testing purposes, it is not a key relating to anything real" + }, + { + "file": "src\\test\\datascience\\serverConfigFiles\\remotePassword.py", + "_justification": "The secret in this file used here for testing." + } + ] +} diff --git a/build/ci/static_analysis/policheck/exceptions.mdb b/build/ci/static_analysis/policheck/exceptions.mdb new file mode 100644 index 000000000000..d68ac945d67c Binary files /dev/null and b/build/ci/static_analysis/policheck/exceptions.mdb differ diff --git a/build/ci/templates/globals.yml b/build/ci/templates/globals.yml new file mode 100644 index 000000000000..03457023e99e --- /dev/null +++ b/build/ci/templates/globals.yml @@ -0,0 +1,13 @@ +variables: + PythonVersion: '3.8' # Always use latest version. + NodeVersion: '12.8.1' # Check version of node used in VS Code. + NpmVersion: '6.13.4' + MOCHA_FILE: '$(Build.ArtifactStagingDirectory)/test-junit.xml' # All test files will write their JUnit xml output to this file, clobbering the last time it was written. + MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). + VSC_PYTHON_FORCE_LOGGING: true # Enable this to turn on console output for the logger + VSC_PYTHON_LOG_FILE: '$(Build.ArtifactStagingDirectory)/pvsc.log' + VSC_PYTHON_WEBVIEW_LOG_FILE: '$(Build.ArtifactStagingDirectory)/pvsc_webview.log' + CI_BRANCH_NAME: ${Build.SourceBranchName} + npm_config_cache: $(Pipeline.Workspace)/.npm + vmImageMacOS: 'macOS-10.15' + TS_NODE_FILES: true # Temporarily enabled to allow using types from vscode.proposed.d.ts from ts-node (for tests). diff --git a/build/ci/templates/jobs/build_compile.yml b/build/ci/templates/jobs/build_compile.yml new file mode 100644 index 000000000000..13b6915f7e3d --- /dev/null +++ b/build/ci/templates/jobs/build_compile.yml @@ -0,0 +1,50 @@ +# Overview: +# Generic jobs template to compile and build extension + +jobs: + - job: Compile + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: ../steps/compile.yml + + - job: Build + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: ../steps/build.yml + + - job: Dependencies + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: ../steps/dependencies.yml + + - job: Hygiene + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: ../steps/initialization.yml + parameters: + PythonVersion: $(PythonVersion) + workingDirectory: $(Build.SourcesDirectory) + compile: 'false' + installVSCEorNPX: 'false' + + - bash: npx tslint --project tsconfig.json + displayName: 'Lint' + workingDirectory: $(Build.SourcesDirectory) + + - bash: npx prettier "src/**/*.ts*" --check + displayName: 'Code Format (TypeScript)' + workingDirectory: $(Build.SourcesDirectory) + + - bash: npx prettier "build/**/*.js" --check + displayName: 'Code Format (JavaScript)' + workingDirectory: $(Build.SourcesDirectory) + + - bash: | + python -m pip install -U black + python -m black . --check + displayName: 'Code Format (Python)' + workingDirectory: $(Build.SourcesDirectory)/pythonFiles diff --git a/build/ci/templates/jobs/coverage.yml b/build/ci/templates/jobs/coverage.yml new file mode 100644 index 000000000000..50fb4af733e0 --- /dev/null +++ b/build/ci/templates/jobs/coverage.yml @@ -0,0 +1,8 @@ +jobs: + - job: Coverage + pool: + vmImage: 'ubuntu-16.04' + variables: + TestsToRun: 'testUnitTests' + steps: + - template: ../steps/merge_upload_coverage.yml diff --git a/build/ci/templates/steps/build.yml b/build/ci/templates/steps/build.yml new file mode 100644 index 000000000000..decf72c27dd3 --- /dev/null +++ b/build/ci/templates/steps/build.yml @@ -0,0 +1,59 @@ +# ----------------------------------------------------------------------------------------------------------------------------- +# Overview: +# ----------------------------------------------------------------------------------------------------------------------------- +# Set of steps used to compile and build the extension +# +# ----------------------------------------------------------------------------------------------------------------------------- +# Variables +# ----------------------------------------------------------------------------------------------------------------------------- +# 1. build +# Mandatory +# Possible values, `true` or `false`. +# If `true`, means we need to build the VSIX, else just compile. + +steps: + - template: initialization.yml + parameters: + PythonVersion: $(PythonVersion) + workingDirectory: $(Build.SourcesDirectory) + compile: 'false' + + - bash: | + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt + failOnStderr: true + displayName: 'pip install requirements' + + - bash: | + python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python ./pythonFiles/install_debugpy.py + failOnStderr: true + displayName: 'Install DEBUGPY wheels' + + - bash: npm run clean + displayName: 'Clean' + + - bash: | + npm run updateBuildNumber -- --buildNumber $BUILD_BUILDID + displayName: 'Update dev Version of Extension (main)' + condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'main')) + + - bash: | + npm run updateBuildNumber -- --buildNumber $BUILD_BUILDID --updateChangelog + displayName: 'Update release Version of Extension' + condition: and(succeeded(), startsWith(variables['Build.SourceBranchName'], 'release')) + + - bash: | + npm run package + displayName: 'Build VSIX' + + - task: CopyFiles@2 + inputs: + contents: '*.vsix' + targetFolder: $(Build.ArtifactStagingDirectory) + displayName: 'Copy VSIX' + + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: $(Build.ArtifactStagingDirectory) + artifactName: VSIX + displayName: 'Publish VSIX to Artifacts' diff --git a/build/ci/templates/steps/compile.yml b/build/ci/templates/steps/compile.yml new file mode 100644 index 000000000000..e35db11613a8 --- /dev/null +++ b/build/ci/templates/steps/compile.yml @@ -0,0 +1,14 @@ +# Compiles the source + +steps: + - template: initialization.yml + parameters: + PythonVersion: $(PythonVersion) + workingDirectory: $(Build.SourcesDirectory) + compile: 'false' + installVSCEorNPX: 'false' + + - task: Gulp@0 + displayName: 'Compile and check for errors' + inputs: + targets: 'prePublishNonBundle' diff --git a/build/ci/templates/steps/dependencies.yml b/build/ci/templates/steps/dependencies.yml new file mode 100644 index 000000000000..5850b0def046 --- /dev/null +++ b/build/ci/templates/steps/dependencies.yml @@ -0,0 +1,20 @@ +# ----------------------------------------------------------------------------------------------------------------------------- +# Overview: +# ----------------------------------------------------------------------------------------------------------------------------- +# Set of steps used to validate dependencies for the extension. In a separate pipeline because this +# can take a long time. +# + +steps: + - template: initialization.yml + parameters: + PythonVersion: $(PythonVersion) + workingDirectory: $(Build.SourcesDirectory) + compile: 'false' + + - bash: npm run clean + displayName: 'Clean' + + # This is a slow process, hence do this as a separate step + - bash: npm run checkDependencies + displayName: 'Check Dependencies' diff --git a/build/ci/templates/steps/generate_upload_coverage.yml b/build/ci/templates/steps/generate_upload_coverage.yml new file mode 100644 index 000000000000..608a2fff8bdc --- /dev/null +++ b/build/ci/templates/steps/generate_upload_coverage.yml @@ -0,0 +1,23 @@ +steps: + # Generate the coverage reports. + - bash: npm run test:cover:report + displayName: 'run test:cover:report' + condition: contains(variables['TestsToRun'], 'testUnitTests') + failOnStderr: false + + # Publish Code Coverage Results + - task: PublishCodeCoverageResults@1 + displayName: 'Publish test:unittests coverage results' + condition: contains(variables['TestsToRun'], 'testUnitTests') + inputs: + codeCoverageTool: 'cobertura' + summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml' + reportDirectory: '$(System.DefaultWorkingDirectory)/coverage' + + - bash: cat ./coverage/cobertura-coverage.xml | ./node_modules/.bin/codecov --pipe + displayName: 'Upload coverage to codecov' + continueOnError: true + condition: contains(variables['TestsToRun'], 'testUnitTests') + failOnStderr: false + env: + CODECOV_TOKEN: $(CODECOV_TOKEN) diff --git a/build/ci/templates/steps/initialization.yml b/build/ci/templates/steps/initialization.yml new file mode 100644 index 000000000000..ed6c6b0c4fb4 --- /dev/null +++ b/build/ci/templates/steps/initialization.yml @@ -0,0 +1,135 @@ +# ----------------------------------------------------------------------------------------------------------------------------- +# Overview: +# ----------------------------------------------------------------------------------------------------------------------------- +# Set of common steps required when initializing a job. +# +# ----------------------------------------------------------------------------------------------------------------------------- +# Variables +# ----------------------------------------------------------------------------------------------------------------------------- +# 1. workingDirectory +# Mandatory +# Working directory. +# 2. PythonVersion +# Python version to be used. +# If not provided, python will not be setup on CI. +parameters: + workingDirectory: '' + PythonVersion: '' + compile: 'true' + sqlite: 'false' + installVSCEorNPX: 'true' + +steps: + - bash: | + printenv + displayName: 'Show all env vars' + condition: eq(variables['system.debug'], 'true') + + - task: NodeTool@0 + displayName: 'Use Node $(NodeVersion)' + inputs: + versionSpec: $(NodeVersion) + + - task: UsePythonVersion@0 + displayName: 'Use Python ${{ parameters.PythonVersion }}' + condition: and(succeeded(), not(eq('${{ parameters.PythonVersion }}', ''))) + inputs: + versionSpec: ${{ parameters.PythonVersion }} + + # Install the a version of python that works with sqlite3 until this bug is addressed + # https://mseng.visualstudio.com/AzureDevOps/_workitems/edit/1535830 + # + # This task will only run if variable `NeedsPythonFunctionalReqs` is true. + - bash: | + sudo apt-get update + sudo apt-get install libsqlite3-dev + version=$(python -V 2>&1 | grep -Po '(?<=Python )(.+)') + wget https://www.python.org/ftp/python/$version/Python-$version.tar.xz + tar xvf Python-$version.tar.xz + cd Python-$version + ./configure --enable-loadable-sqlite-extensions --with-ensurepip=install --prefix=$HOME/py-$version + make + sudo make install + sudo chmod -R 777 $HOME/py-$version + export PATH=$HOME/py-$version/bin:$PATH + sudo ln -s $HOME/py-$version/bin/python3 $HOME/py-$version/bin/python + echo '##vso[task.prependpath]'$HOME/py-$version/bin + displayName: 'Setup python to run with sqlite on 3.*' + condition: and(succeeded(), eq('${{ parameters.sqlite }}', 'true'), not(eq('${{ parameters.PythonVersion }}', '')), not(eq('${{ parameters.PythonVersion }}', '2.7')), eq(variables['Agent.Os'], 'Linux')) + + - task: Npm@1 + displayName: 'Use NPM $(NpmVersion)' + inputs: + command: custom + verbose: true + customCommand: 'install -g npm@$(NpmVersion)' + + # In the past installs of npm, node-pre-gyp 0.12.0 was installed. + # However in the latest versions, 0.11.0 is getting isntalled. + # - bash: | + # npm uninstall canvas + # npm i -g node-pre-gyp@0.12.0 + # npm i -D node-pre-gyp@0.12.0 + # npm install -D canvas --build-from-source + # displayName: 'Uninstall canvas and install build from source (only for Linux)' + # condition: and(succeeded(), eq(variables['Agent.Os'], 'Linux')) + + # See the help here on how to cache npm + # https://docs.microsoft.com/en-us/azure/devops/pipelines/caching/?view=azure-devops#nodejsnpm + - task: CacheBeta@0 + inputs: + key: npm | $(Agent.OS) | package-lock.json + path: $(npm_config_cache) + restoreKeys: | + npm | $(Agent.OS) + displayName: Cache npm + + - task: Npm@1 + displayName: 'npm ci' + inputs: + workingDir: ${{ parameters.workingDirectory }} + command: custom + verbose: true + customCommand: ci + + # On Mac, the command `node` doesn't always point to the current node version. + # Debugger tests use the variable process.env.NODE_PATH + - script: | + export NODE_PATH=`which node` + echo $NODE_PATH + echo '##vso[task.setvariable variable=NODE_PATH]'$NODE_PATH + displayName: 'Setup NODE_PATH for extension (Debugger Tests)' + condition: and(succeeded(), eq(variables['agent.os'], 'Darwin')) + + # Install vsce + - bash: | + npm install -g vsce + displayName: 'Install vsce' + condition: and(succeeded(), eq('${{ parameters.installVSCEorNPX }}', 'true')) + + - bash: npx tsc -p ./ + displayName: 'compile (npx tsc -p ./)' + workingDirectory: ${{ parameters.workingDirectory }} + condition: and(succeeded(), eq('${{ parameters.compile }}', 'true')) + + # https://code.visualstudio.com/api/working-with-extensions/continuous-integration#azure-pipelines + - bash: | + set -e + /usr/bin/Xvfb :10 -ac >> /tmp/Xvfb.out 2>&1 & + disown -ar + displayName: 'Start Display Server (xvfb) to launch VS Code)' + condition: and(succeeded(), eq(variables['Agent.Os'], 'Linux')) + + - bash: python -m pip install -U pip + displayName: 'Install pip' + # # Show all versions installed/available on PATH if in verbose mode. + # # Example command line (windows pwsh): + # # > Write-Host Node ver: $(& node -v) NPM Ver: $(& npm -v) Python ver: $(& python --version)" + # - bash: | + # echo AVAILABLE DEPENDENCY VERSIONS + # echo Node Version = `node -v` + # echo NPM Version = `npm -v` + # echo Python Version = `python --version` + # echo Gulp Version = `gulp --version` + # condition: and(succeeded(), eq(variables['system.debug'], 'true')) + # displayName: Show Dependency Versions diff --git a/build/ci/templates/steps/merge_upload_coverage.yml b/build/ci/templates/steps/merge_upload_coverage.yml new file mode 100644 index 000000000000..51ef76aac61c --- /dev/null +++ b/build/ci/templates/steps/merge_upload_coverage.yml @@ -0,0 +1,29 @@ +steps: + - template: initialization.yml + parameters: + workingDirectory: $(Build.SourcesDirectory) + compile: 'false' + + # Download previously generated coverage artifacts + - task: DownloadPipelineArtifact@2 + inputs: + patterns: '**/.nyc_output/**' + displayName: 'Download .nyc_output coverage artifacts' + condition: always() + + # Now that we have downloaded artifacts from `coverage-output-`, copy them + # into the root directory (they'll go under `.nyc_output/...`) + # These are the coverage output files that can be merged and then we can generate a report from them. + # This step results in downloading all individual `./nyc_output` results in coverage + # from all different test outputs. + # Running the process of generating reports, will result in generation of + # reports from all coverage data, i.e. we're basically combining all to generate a single merged report. + - bash: | + cp -r $(Pipeline.Workspace)/coverage-output-*/.nyc_output/ ./ + cd .nyc_output/ + ls -dlU .*/ */ + ls + displayName: 'Copy ./.nyc_output' + condition: always() + + - template: generate_upload_coverage.yml diff --git a/build/ci/templates/test_phases.yml b/build/ci/templates/test_phases.yml new file mode 100644 index 000000000000..f486b07616b2 --- /dev/null +++ b/build/ci/templates/test_phases.yml @@ -0,0 +1,556 @@ +# To use this step template from a job, use the following code: +# ```yaml +# steps: +# template: path/to/this/dir/test_phases.yml +# ``` +# +# Your job using this template *must* supply these values: +# - TestsToRun: 'testA, testB, ..., testN' - the list of tests to execute, see the list above. +# +# Your job using this template *may* supply these values: +# - NeedsPythonTestReqs: [true|false] - install the test-requirements prior to running tests. False if not set. +# - NeedsPythonFunctionalReqs: [true|false] - install the functional-requirements prior to running tests. False if not set. +# - NeedsIPythonReqs: [true|false] - install the ipython-test-requirements prior to running tests. False if not set. +# - PythonVersion: 'M.m' - the Python version to run. DefaultPythonVersion (from globals.yml) if not set. +# - NodeVersion: 'x.y.z' - Node version to use. DefaultNodeVersion (from globals.yml) if not set. + +## Supported `TestsToRun` values, multiples are allowed separated by commas or spaces: +# +# 'testUnitTests' +# 'pythonUnitTests' +# 'pythonInternalTools' +# 'testSingleWorkspace' +# 'testMultiWorkspace' +# 'testDebugger' +# 'testFunctional' +# 'testPerformance' +# 'venvTests' + +steps: + - template: steps/initialization.yml + parameters: + PythonVersion: $(PythonVersion) + workingDirectory: $(Build.SourcesDirectory) + compile: 'false' + sqlite: $(NeedsIPythonReqs) + + # When running unit tests, we need to just compile extension code (not webviews & the like). + - task: Gulp@0 + displayName: 'gulp compile' + inputs: + targets: 'compile' + condition: and(succeeded(), contains(variables['TestsToRun'], 'testUnitTests')) + + # Run the `prePublishNonBundle` gulp task to build the binaries we will be testing. + # This produces the .js files required into the out/ folder. + # Example command line (windows pwsh): + # > gulp prePublishNonBundle + - task: Gulp@0 + displayName: 'gulp prePublishNonBundle' + inputs: + targets: 'prePublishNonBundle' + condition: and(succeeded(), not(contains(variables['TestsToRun'], 'testSmoke')), not(contains(variables['TestsToRun'], 'testUnitTests'))) + + # Run the typescript unit tests. + # + # This will only run if the string 'testUnitTests' exists in variable `TestsToRun` + # + # Example command line (windows pwsh): + # > npm run test:unittests:cover + - bash: | + npm run test:unittests:cover + displayName: 'run test:unittests' + condition: and(succeeded(), contains(variables['TestsToRun'], 'testUnitTests')) + + # Upload the test results to Azure DevOps to facilitate test reporting in their UX. + - task: PublishTestResults@2 + displayName: 'Publish test:unittests results' + condition: contains(variables['TestsToRun'], 'testUnitTests') + inputs: + testResultsFiles: '$(MOCHA_FILE)' + testRunTitle: 'unittests-$(Agent.Os)-Py$(pythonVersion)' + buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' + buildConfiguration: 'UnitTests' + + - task: CopyFiles@2 + inputs: + sourceFolder: '$(Build.SourcesDirectory)/.nyc_output' + targetFolder: '$(Build.ArtifactStagingDirectory)/nyc/.nyc_output' + displayName: 'Copy nyc_output to publish as artificat' + condition: contains(variables['TestsToRun'], 'testUnitTests') + + # Upload Code Coverage Results (to be merged later). + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: '$(Build.ArtifactStagingDirectory)/nyc' + artifactName: 'coverage-output-$(Agent.Os)' + condition: contains(variables['TestsToRun'], 'testUnitTests') + + - template: steps/generate_upload_coverage.yml + + # Install the requirements for the Python or the system tests. This includes the supporting libs that + # we ship in our extension such as DEBUGPY and Jedi. + # + # This task will only run if variable `NeedsPythonTestReqs` is true. + # + # Example command line (windows pwsh): + # > python -m pip install -m -U pip + # > python -m pip install --upgrade -r build/test-requirements.txt + # > python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt + - bash: | + python -m pip install --upgrade -r build/test-requirements.txt + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt + python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade --pre debugpy + displayName: 'pip install system test requirements' + condition: and(succeeded(), eq(variables['NeedsPythonTestReqs'], 'true')) + + # Install the requirements for functional tests. + # + # This task will only run if variable `NeedsPythonFunctionalReqs` is true. + # + # Example command line (windows pwsh): + # > python -m pip install numpy + # > python -m pip install --upgrade -r build/functional-test-requirements.txt + # > python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt + - bash: | + python -m pip install numpy + python -m pip install --upgrade -r ./build/functional-test-requirements.txt + python -c "import sys;print(sys.executable)" + displayName: 'pip install functional requirements' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true')) + + # Add CONDA to the path so anaconda works + # + # This task will only run if variable `NeedsPythonFunctionalReqs` is true. + - bash: | + echo "##vso[task.prependpath]$CONDA/bin" + displayName: 'Add conda to the path' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), ne(variables['Agent.Os'], 'Windows_NT')) + + # Add CONDA to the path so anaconda works (windows) + # + # This task will only run if variable `NeedsPythonFunctionalReqs` is true. + - powershell: | + Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" + displayName: 'Add conda to the path' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), eq(variables['Agent.Os'], 'Windows_NT')) + + # On MAC let CONDA update install paths + - bash: | + sudo chown -R $USER $CONDA + displayName: 'Give CONDA permission to its own files' + condition: and(succeeded(), eq(variables['Agent.Os'], 'Darwin')) + + # Create the two anaconda environments + # + # This task will only run if variable `NeedsPythonFunctionalReqs` is true. + # + - script: | + conda env create --quiet --force --file build/ci/conda_env_1.yml + conda env create --quiet --force --file build/ci/conda_env_2.yml + displayName: 'Create CONDA Environments' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true')) + + # Run the pip installs in the 3 environments (darwin linux) + - bash: | + source activate base + conda install --quiet -y --file build/ci/conda_base.yml + python -m pip install --upgrade -r build/conda-functional-requirements.txt + source activate conda_env_1 + python -m pip install --upgrade -r build/conda-functional-requirements.txt + source activate conda_env_2 + python -m pip install --upgrade -r build/conda-functional-requirements.txt + conda deactivate + displayName: 'Install Pip requirements for CONDA envs' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), ne(variables['Agent.Os'], 'Windows_NT')) + + # Run the pip installs in the 3 environments (windows) + - script: | + call activate base + call conda install --quiet -y --file build/ci/conda_base.yml + python -m pip install --upgrade -r build/conda-functional-requirements.txt + call activate conda_env_1 + python -m pip install --upgrade -r build/conda-functional-requirements.txt + call activate conda_env_2 + python -m pip install --upgrade -r build/conda-functional-requirements.txt + displayName: 'Install Pip requirements for CONDA envs' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), eq(variables['Agent.Os'], 'Windows_NT')) + + # Downgrade pywin32 on Windows due to bug https://github.com/jupyter/notebook/issues/4909 + # + # This task will only run if variable `NeedsPythonFunctionalReqs` is true. + - bash: | + python -m pip install --upgrade pywin32==224 + displayName: 'Downgrade pywin32 on Windows / Python 3.6' + condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), eq(variables['Agent.Os'], 'Windows_NT'), eq(variables['PythonVersion'], '3.6')) + + # Install the requirements for ipython tests. + # + # This task will only run if variable `NeedsIPythonReqs` is true. + # + # Example command line (windows pwsh): + # > python -m pip install numpy + # > python -m pip install --upgrade -r build/ipython-test-requirements.txt + # > python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt + - bash: | + python -m pip install numpy + python -m pip install --upgrade -r ./build/ipython-test-requirements.txt + displayName: 'pip install ipython requirements' + condition: and(succeeded(), eq(variables['NeedsIPythonReqs'], 'true')) + + # Install jupyter for smoke tests. + - bash: | + python -m pip install --upgrade jupyter + displayName: 'pip install jupyter' + condition: and(succeeded(), eq(variables['NeedsIPythonReqs'], 'true'), contains(variables['TestsToRun'], 'testSmoke')) + + - bash: | + python -m pip --disable-pip-version-check install -r build/debugger-install-requirements.txt + python ./pythonFiles/install_debugpy.py + failOnStderr: true + displayName: 'Install DEBUGPY wheels' + condition: and(eq(variables['NeedsPythonTestReqs'], 'true'), eq(variables['PythonVersion'], '3.7')) + + # Run the Python unit tests in our codebase. Produces a JUnit-style log file that + # will be uploaded after all tests are complete. + # + # This task only runs if the string 'pythonUnitTests' exists in variable `TestsToRun`. + # + # Example command line (windows pwsh): + # > python -m pip install -m -U pip + # > python -m pip install -U -r build/test-requirements.txt + # > python pythonFiles/tests/run_all.py --color=yes --junit-xml=python-tests-junit.xml + - bash: | + python pythonFiles/tests/run_all.py --color=no --junit-xml=$COMMON_TESTRESULTSDIRECTORY/python-tests-junit.xml + displayName: 'Python unittests' + condition: and(succeeded(), contains(variables['TestsToRun'], 'pythonUnitTests')) + + # Upload the test results to Azure DevOps to facilitate test reporting in their UX. + - task: PublishTestResults@2 + displayName: 'Publish Python unittests results' + condition: contains(variables['TestsToRun'], 'pythonUnitTests') + inputs: + testResultsFiles: 'python-tests-junit.xml' + searchFolder: '$(Common.TestResultsDirectory)' + testRunTitle: 'pythonUnitTests-$(Agent.Os)-Py$(pythonVersion)' + buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' + buildConfiguration: 'UnitTests' + + # Run the Python IPython tests in our codebase. Produces a JUnit-style log file that + # will be uploaded after all tests are complete. + # + # This task only runs if the string 'pythonIPythonTests' exists in variable `TestsToRun`. + # + # Example command line (windows pwsh): + # > python -m pip install -m -U pip + # > python -m pip install -U -r build/test-requirements.txt + # > python pythonFiles/tests/run_all.py --color=yes --junit-xml=python-tests-junit.xml + - bash: | + python -m IPython pythonFiles/tests/run_all.py -- --color=no --junit-xml=$COMMON_TESTRESULTSDIRECTORY/ipython-tests-junit.xml + displayName: 'Python ipython tests' + condition: and(succeeded(), contains(variables['TestsToRun'], 'pythonIPythonTests')) + + # Upload the test results to Azure DevOps to facilitate test reporting in their UX. + - task: PublishTestResults@2 + displayName: 'Publish IPython test results' + condition: contains(variables['TestsToRun'], 'pythonIPythonTests') + inputs: + testResultsFiles: 'ipython-tests-junit.xml' + searchFolder: '$(Common.TestResultsDirectory)' + testRunTitle: 'pythonIPythonTests-$(Agent.Os)-Py$(pythonVersion)' + buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' + buildConfiguration: 'UnitTests' + + # Run the News tool tests. + # + # This task only runs if the string 'pythonInternalTools' exists in variable `TestsToRun` + # + # Example command line (windows pwsh): + # > python -m pip install -U -r news/requirements.txt + - script: | + python -m pip install --upgrade -r news/requirements.txt + python -m pytest news --color=yes --junit-xml=$COMMON_TESTRESULTSDIRECTORY/python-news-junit.xml + displayName: 'Run Python tests for news' + condition: and(succeeded(), contains(variables['TestsToRun'], 'pythonInternalTools')) + + # Upload the test results to Azure DevOps to facilitate test reporting in their UX. + - task: PublishTestResults@2 + displayName: 'Publish Python tests for news results' + condition: contains(variables['TestsToRun'], 'pythonInternalTools') + inputs: + testResultsFiles: 'python-news-junit.xml' + searchFolder: '$(Common.TestResultsDirectory)' + testRunTitle: 'news-$(Agent.Os)-Py$(pythonVersion)' + buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' + buildConfiguration: 'UnitTests' + + # Venv tests: Prepare the various virtual environments and record their details into the + # JSON file that venvTests require to run. + # + # This task only runs if the string 'venvTests' exists in variable 'TestsToRun' + # + # This task has a bunch of steps, all of which are to fill the `EnvPath` struct found in + # the file: + # `src/test/common/terminals/environmentActionProviders/terminalActivation.testvirtualenvs.ts` + # + # Example command line (windows pwsh): + # // This is done in powershell. Copy/paste the code below. + - pwsh: | + # venv/bin or venv\\Scripts (windows)? + $environmentExecutableFolder = 'bin' + if ($Env:AGENT_OS -match '.*Windows.*') { + $environmentExecutableFolder = 'Scripts' + } + + # pipenv + python -m pip install pipenv + python -m pipenv run python build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) pipenvPath + + # venv + # what happens when running under Python 2.7? + python -m venv .venv + & ".venv/$environmentExecutableFolder/python" ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) venvPath + + # virtualenv + python -m pip install virtualenv + python -m virtualenv .virtualenv + & ".virtualenv/$environmentExecutableFolder/python" ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) virtualEnvPath + + # conda + + # 1. For `terminalActivation.testvirtualenvs.test.ts` + + $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath $environmentExecutableFolder | Join-Path -ChildPath conda + if( '$(Agent.Os)' -match '.*Windows.*' ){ + $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python + } else{ + $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath $environmentExecutableFolder | Join-Path -ChildPath python + & $condaPythonPath ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) condaExecPath $condaExecPath + } + & $condaPythonPath ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) condaPath + + # 2. For `interpreterLocatorService.testvirtualenvs.ts` + + & $condaExecPath create -n "test_env1" -y python + & $condaExecPath create -p "./test_env2" -y python + & $condaExecPath create -p "$Env:HOME/test_env3" -y python + + # Set the TEST_FILES_SUFFIX + Write-Host '##vso[task.setvariable variable=TEST_FILES_SUFFIX;]testvirtualenvs' + + displayName: 'Prepare Venv-Test Environment' + condition: and(succeeded(), contains(variables['TestsToRun'], 'venvTests')) + + # Run the virtual environment based tests. + # This set of tests is simply using the `testSingleWorkspace` set of tests, but + # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, which + # got set in the Prepare Venv-Test Environment task above. + # **Note**: Azure DevOps tasks set environment variables via a specially formatted + # string sent to stdout. + # + # This task only runs if the string 'venvTests' exists in variable 'TestsToRun' + # + # Example command line (windows pwsh): + # > $Env:TEST_FILES_SUFFIX=testvirtualenvs + # > npm run testSingleWorkspace + - script: | + cat $PYTHON_VIRTUAL_ENVS_LOCATION + + npm run testSingleWorkspace + + displayName: 'Run Venv-Tests' + condition: and(succeeded(), contains(variables['TestsToRun'], 'venvTests')) + env: + DISPLAY: :10 + + # Upload the test results to Azure DevOps to facilitate test reporting in their UX. + - task: PublishTestResults@2 + displayName: 'Publish Venv-Tests results' + condition: contains(variables['TestsToRun'], 'venvTests') + inputs: + testResultsFiles: '$(MOCHA_FILE)' + testRunTitle: 'venvTest-$(Agent.Os)-Py$(pythonVersion)' + buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' + buildConfiguration: 'SystemTests' + + # Set the CI_PYTHON_PATH variable that forces VS Code system tests to use + # the specified Python interpreter. + # + # This is how to set an environment variable in the Azure DevOps pipeline, write + # a specially formatted string to stdout. For details, please see + # https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#set-in-script + # + # Example command line (windows pwsd): + # > $Env:CI_PYTHON_PATH=(& python -c 'import sys;print(sys.executable)') + - script: | + python -c "from __future__ import print_function;import sys;print('##vso[task.setvariable variable=CI_PYTHON_PATH;]{}'.format(sys.executable))" + displayName: 'Set CI_PYTHON_PATH' + + # Run the functional tests with each file split. + # + # This task only runs if the string 'testFunctional' exists in variable `TestsToRun`. + # + # Note it is crucial this uses npm to start the runFunctionalTests.js. Otherwise the + # environment will be messed up. + # + # Example command line (windows pwsh): + # > node build/ci/scripts/runFunctionalTests.js + - script: | + npm run test:functional:split + displayName: 'Run functional split' + condition: and(succeeded(), contains(variables['TestsToRun'], 'testFunctional'), eq(variables['SplitFunctionalTests'], 'true')) + env: + DISPLAY: :10 + + # Run the functional tests when not splitting + # + # This task only runs if the string 'testFunctional' exists in variable `TestsToRun`. + # + # Example command line (windows pwsh): + # > node build/ci/scripts/runFunctionalTests.js + - script: | + npm run test:functional + displayName: 'Run functional tests' + condition: and(succeeded(), contains(variables['TestsToRun'], 'testFunctional'), not(eq(variables['SplitFunctionalTests'], 'true'))) + env: + DISPLAY: :10 + + # Upload the test results to Azure DevOps to facilitate test reporting in their UX. + - task: PublishTestResults@2 + displayName: 'Publish functional tests results' + condition: contains(variables['TestsToRun'], 'testFunctional') + inputs: + testResultsFiles: '$(Build.ArtifactStagingDirectory)/test-junit*.xml' + testRunTitle: 'functional-$(Agent.Os)-Py$(pythonVersion)' + buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' + buildConfiguration: 'FunctionalTests' + + # Run the single workspace tests. + # + # This task only runs if the string 'testSingleWorkspace' exists in variable `TestsToRun`. + # + # Example command line (windows pwsh): + # > npm run testSingleWorkspace + - script: | + npm run testSingleWorkspace + displayName: 'Run single workspace tests' + condition: and(succeeded(), contains(variables['TestsToRun'], 'testSingleWorkspace')) + env: + DISPLAY: :10 + + # Run the single workspace tests in VS Code Insiders. + - script: | + npm run testDataScience + continueOnError: true + displayName: 'Run DataScience Tests in VSCode Insiders' + condition: and(succeeded(), contains(variables['TestsToRun'], 'testDataScience')) + env: + DISPLAY: :10 + VSC_PYTHON_CI_TEST_VSC_CHANNEL: 'insiders' + VSC_PYTHON_LOAD_EXPERIMENTS_FROM_FILE: 'true' + TEST_FILES_SUFFIX: 'ds.test' + + # Upload the test results to Azure DevOps to facilitate test reporting in their UX. + - task: PublishTestResults@2 + displayName: 'Publish single workspace tests results' + condition: contains(variables['TestsToRun'], 'testSingleWorkspace') + inputs: + testResultsFiles: '$(MOCHA_FILE)' + testRunTitle: 'singleWorkspace-$(Agent.Os)-Py$(pythonVersion)' + buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' + buildConfiguration: 'SystemTests' + + # Run the multi-workspace tests. + # + # This task only runs if the string 'testMultiWorkspace' exists in variable `TestsToRun`. + # + # Example command line (windows pwsh): + # > npm run testMultiWorkspace + - script: | + npm run testMultiWorkspace + displayName: 'Run multi-workspace tests' + condition: and(succeeded(), contains(variables['TestsToRun'], 'testMultiWorkspace')) + env: + DISPLAY: :10 + + # Upload the test results to Azure DevOps to facilitate test reporting in their UX. + - task: PublishTestResults@2 + displayName: 'Publish multi-workspace tests results' + condition: contains(variables['TestsToRun'], 'testMultiWorkspace') + inputs: + testResultsFiles: '$(MOCHA_FILE)' + testRunTitle: 'multiWorkspace-$(Agent.Os)-Py$(pythonVersion)' + buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' + buildConfiguration: 'SystemTests' + + # Run the debugger integration tests. + # + # This task only runs if the string 'testDebugger' exists in variable `TestsToRun`. + # + # Example command line (windows pwsh): + # > npm run testDebugger + - script: | + npm run testDebugger + displayName: 'Run debugger tests' + condition: and(succeeded(), contains(variables['TestsToRun'], 'testDebugger')) + env: + DISPLAY: :10 + + # Upload the test results to Azure DevOps to facilitate test reporting in their UX. + - task: PublishTestResults@2 + displayName: 'Publish debugger tests results' + condition: contains(variables['TestsToRun'], 'testDebugger') + inputs: + testResultsFiles: '$(MOCHA_FILE)' + testRunTitle: 'debugger-$(Agent.Os)-Py$(pythonVersion)' + buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' + buildConfiguration: 'SystemTests' + + # Run the performance tests. + # + # This task only runs if the string 'testPerformance' exists in variable `TestsToRun`. + # + # Example command line (windows pwsh): + # > npm run testPerformance + - script: | + npm run testPerformance + displayName: 'Run Performance Tests' + condition: and(succeeded(), contains(variables['TestsToRun'], 'testPerformance')) + env: + DISPLAY: :10 + + # Run the smoke tests. + # + # This task only runs if the string 'testSmoke' exists in variable `TestsToRun`. + # + # Example command line (windows pwsh): + # > npm run clean + # > npm run updateBuildNumber -- --buildNumber 0.0.0-local + # > npm run package + # > npx gulp clean:cleanExceptTests + # > npm run testSmoke + - bash: | + npm install -g vsce + npm run clean + npx tsc -p ./ + mkdir -p ./tmp/client/logging + cp -r ./out/client/logging ./tmp/client + npx gulp clean:cleanExceptTests + cp -r ./out/test ./tmp/test + npm run updateBuildNumber -- --buildNumber $BUILD_BUILDID + npm run package + npx gulp clean:cleanExceptTests + mkdir -p ./out/client/logging + cp -r ./tmp/client/logging ./out/client + cp -r ./tmp/test ./out/test + node --no-force-async-hooks-checks ./out/test/smokeTest.js + displayName: 'Run Smoke Tests' + condition: and(succeeded(), contains(variables['TestsToRun'], 'testSmoke')) + env: + DISPLAY: :10 + + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: $(Build.ArtifactStagingDirectory) + artifactName: $(Agent.JobName) + condition: always() diff --git a/build/ci/vscode-python-ci-manual.yaml b/build/ci/vscode-python-ci-manual.yaml new file mode 100644 index 000000000000..bad74182082c --- /dev/null +++ b/build/ci/vscode-python-ci-manual.yaml @@ -0,0 +1,296 @@ +# manual CI build + +name: '$(Year:yyyy).$(Month).0.$(BuildID)-manual' + +trigger: none +pr: none + +# Variables that are available for the entire pipeline. +variables: + - template: templates/globals.yml + +stages: + - stage: Build + jobs: + - template: templates/jobs/build_compile.yml + + # Each item in each matrix has a number of possible values it may + # define. They are detailed in templates/test_phases.yml. The only + # required value is "TestsToRun". + + - stage: Linux + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + # with mocks + # focused on small units (i.e. functions) + # and tightly controlled dependencies + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + # no mocks, no vscode + # focused on integration + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + # no mocks, with vscode + # focused on integration + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + # no mocks, with vscode + # focused on integration + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + # This is for the venvTests to use, not needed if you don't run venv tests... + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + #maxParallel: 3 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + # This is the oldest Python 3 version we support. + - job: 'Py36' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.6' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.6' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.6' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + #maxParallel: 3 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + # This is the oldest Python 3 version we support. + - job: 'Py35' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.5' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.5' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.5' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.5' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + PythonVersion: '3.5' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + #maxParallel: 3 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + - stage: Mac + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + #maxParallel: 3 + pool: + vmImage: '$(vmImageMacOS)' + steps: + - template: templates/test_phases.yml + + # This is the oldest Python 3 version we support. + - job: 'Py35' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.5' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.5' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.5' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.5' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + PythonVersion: '3.5' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + #maxParallel: 3 + pool: + vmImage: '$(vmImageMacOS)' + steps: + - template: templates/test_phases.yml + + - stage: Windows + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + #maxParallel: 3 + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml + + # This is the oldest Python 3 version we support. + - job: 'Py35' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.5' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.5' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.5' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.5' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + PythonVersion: '3.5' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + #maxParallel: 3 + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml + + - stage: Reports + dependsOn: + - Linux + - Mac + - Windows + condition: always() + jobs: + - template: templates/jobs/coverage.yml diff --git a/build/ci/vscode-python-ci-static-analysis.yaml b/build/ci/vscode-python-ci-static-analysis.yaml new file mode 100644 index 000000000000..09076a7e6127 --- /dev/null +++ b/build/ci/vscode-python-ci-static-analysis.yaml @@ -0,0 +1,89 @@ +# CI build (PR merge) + +name: 'VSCode-Python-ci-static-analysis' + +# Notes: Only trigger a commit for main and release, and skip build/rebuild +# on changes in the news and .vscode folders. +trigger: + branches: + include: ['main', 'release*'] + paths: + exclude: ['/news/1 Enhancements', '/news/2 Fixes', '/news/3 Code Health', '/.vscode'] + +# Not the PR build for merges to main and release. +pr: none + +jobs: + - job: 'Static_Analysis' + pool: + vmImage: 'windows-latest' + + steps: + - task: PoliCheck@1 + inputs: + inputType: 'Basic' + targetType: 'F' + targetArgument: '$(Build.SourcesDirectory)' + result: 'PoliCheck.xml' + continueOnError: true + + - task: AntiMalware@3 + inputs: + InputType: 'Basic' + ScanType: 'CustomScan' + FileDirPath: '$(Build.SourcesDirectory)' + EnableServices: true + SupportLogOnError: false + TreatSignatureUpdateFailureAs: 'Warning' + SignatureFreshness: 'UpToDate' + TreatStaleSignatureAs: 'Error' + continueOnError: true + + - task: AutoApplicability@1 + inputs: + ExternalRelease: true + IsSoftware: true + continueOnError: true + + - task: VulnerabilityAssessment@0 + continueOnError: true + + - task: ESLint@1 + inputs: + Configuration: 'recommended' + TargetType: 'eslint' + ErrorLevel: 'warn' + continueOnError: true + + - task: CredScan@3 + continueOnError: true + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.x' + addToPath: true + architecture: 'x64' + + - task: CmdLine@2 + inputs: + script: | + python -m pip install -U pip + python -m pip install bandit + python -m bandit -s B101 -x "$(Build.SourcesDirectory)\pythonFiles\tests\**\*"-r "$(Build.SourcesDirectory)\pythonFiles" + continueOnError: true + + - task: SdtReport@2 + inputs: + GdnExportAllTools: true + + - task: PublishSecurityAnalysisLogs@3 + inputs: + ArtifactName: 'CodeAnalysisLogs' + ArtifactType: 'Container' + AllTools: true + ToolLogsNotFoundAction: 'Standard' + + - task: TSAUpload@2 + inputs: + GdnPublishTsaOnboard: true + GdnPublishTsaConfigFile: '$(Build.SourcesDirectory)\build\ci\TSAOptions.json' diff --git a/build/ci/vscode-python-ci.yaml b/build/ci/vscode-python-ci.yaml new file mode 100644 index 000000000000..31d40da97d76 --- /dev/null +++ b/build/ci/vscode-python-ci.yaml @@ -0,0 +1,194 @@ +# CI build (PR merge) + +name: '$(Year:yyyy).$(Month).0.$(BuildID)-ci' + +# Notes: Only trigger a commit for main and release, and skip build/rebuild +# on changes in the news and .vscode folders. +trigger: + branches: + include: ['main', 'release*'] + paths: + exclude: ['/news/1 Enhancements', '/news/2 Fixes', '/news/3 Code Health', '/.vscode'] + +# Not the PR build for merges to main and release. +pr: none + +# Variables that are available for the entire pipeline. +variables: + - template: templates/globals.yml + +stages: + - stage: Build + jobs: + - template: templates/jobs/build_compile.yml + + # Each item in each matrix has a number of possible values it may + # define. They are detailed in templates/test_phases.yml. The only + # required value is "TestsToRun". + + - stage: Linux + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + # with mocks + # focused on small units (i.e. functions) + # and tightly controlled dependencies + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + # no mocks, no vscode + # focused on integration + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + # no mocks, with vscode + # focused on integration + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + # no mocks, with vscode + # focused on integration + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + # This is for the venvTests to use, not needed if you don't run venv tests... + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + maxParallel: 2 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + # This is the oldest Python 3 version we support. + - job: 'Py35' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.5' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Venv': + PythonVersion: '3.5' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + PythonVersion: '3.5' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + maxParallel: 2 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + - stage: Mac + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + maxParallel: 2 + pool: + vmImage: '$(vmImageMacOS)' + steps: + - template: templates/test_phases.yml + + - stage: Windows + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + maxParallel: 2 + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml + + - stage: Reports + dependsOn: + - Linux + - Mac + - Windows + condition: always() + jobs: + - template: templates/jobs/coverage.yml diff --git a/build/ci/vscode-python-nightly-ci.yaml b/build/ci/vscode-python-nightly-ci.yaml new file mode 100644 index 000000000000..228909a49162 --- /dev/null +++ b/build/ci/vscode-python-nightly-ci.yaml @@ -0,0 +1,510 @@ +# Nightly build + +name: '$(Year:yyyy).$(Month).0.$(BuildID)-nightly' + +# Not the CI build, see `vscode-python-ci.yaml`. +trigger: none + +# Not the PR build for merges to main and release. +pr: none + +schedules: + - cron: '0 8 * * 1-5' + # Daily midnight PST build, runs Monday - Friday always + displayName: Nightly build + branches: + include: + - main + - release* + always: true + +# Variables that are available for the entire pipeline. +variables: + - template: templates/globals.yml + +stages: + - stage: Build + jobs: + - template: templates/jobs/build_compile.yml + + # Each item in each matrix has a number of possible values it may + # define. They are detailed in templates/test_phases.yml. The only + # required value is "TestsToRun". + + - stage: Linux + dependsOn: + - Build + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + # This is for the venvTests to use, not needed if you don't run venv tests... + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + maxParallel: 1 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + - job: 'Py36' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.6' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.6' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.6' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + PythonVersion: '3.6' + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + PythonVersion: '3.6' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.6' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + - job: 'Py35' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.5' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.5' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.5' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + PythonVersion: '3.5' + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + PythonVersion: '3.5' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.5' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + - job: 'Py27' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '2.7' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '2.7' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '2.7' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + PythonVersion: '2.7' + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + PythonVersion: '2.7' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + # Note: Virtual env tests use `venv` and won't currently work with Python 2.7 + # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + - stage: Mac + dependsOn: + - Build + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + maxParallel: 1 + pool: + vmImage: '$(vmImageMacOS)' + steps: + - template: templates/test_phases.yml + + - job: 'Py36' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.6' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.6' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.6' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + PythonVersion: '3.6' + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + PythonVersion: '3.6' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.6' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 + pool: + vmImage: '$(vmImageMacOS)' + steps: + - template: templates/test_phases.yml + + - job: 'Py35' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.5' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.5' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.5' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + PythonVersion: '3.5' + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + PythonVersion: '3.5' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.5' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 + pool: + vmImage: '$(vmImageMacOS)' + steps: + - template: templates/test_phases.yml + + - job: 'Py27' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '2.7' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '2.7' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '2.7' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + PythonVersion: '2.7' + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + PythonVersion: '2.7' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + # Note: Virtual env tests use `venv` and won't currently work with Python 2.7 + # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 + pool: + vmImage: '$(vmImageMacOS)' + steps: + - template: templates/test_phases.yml + + - stage: Windows + dependsOn: + - Build + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Venv': + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + maxParallel: 1 + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml + + - job: 'Py36' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.6' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.6' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.6' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + PythonVersion: '3.6' + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + PythonVersion: '3.6' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.6' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml + + - job: 'Py35' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '3.5' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '3.5' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '3.5' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + PythonVersion: '3.5' + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + PythonVersion: '3.5' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + 'Venv': + PythonVersion: '3.5' + TestsToRun: 'venvTests' + NeedsPythonTestReqs: true + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml + + - job: 'Py27' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Unit': + PythonVersion: '2.7' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + PythonVersion: '2.7' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + PythonVersion: '2.7' + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'Multi Workspace': + PythonVersion: '2.7' + TestsToRun: 'testMultiWorkspace' + NeedsPythonTestReqs: true + 'Debugger': + PythonVersion: '2.7' + TestsToRun: 'testDebugger' + NeedsPythonTestReqs: true + # Note: Virtual env tests use `venv` and won't currently work with Python 2.7 + # Note: We only run the smoke tests with the latest Python release. + maxParallel: 1 + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml + + - stage: Reports + dependsOn: + - Linux + - Mac + - Windows + condition: always() + jobs: + - template: templates/jobs/coverage.yml diff --git a/build/ci/vscode-python-nightly-flake-ci.yaml b/build/ci/vscode-python-nightly-flake-ci.yaml new file mode 100644 index 000000000000..db168f479927 --- /dev/null +++ b/build/ci/vscode-python-nightly-flake-ci.yaml @@ -0,0 +1,90 @@ +# Nightly build + +name: '$(Year:yyyy).$(Month).0.$(BuildID)-nightly-flake' + +# Not the CI build, see `vscode-python-nightly-flake-ci.yaml`. +trigger: none + +# Not the PR build for merges to main and release. +pr: none + +schedules: + - cron: '0 8 * * 1-5' + # Daily midnight PST build, runs Monday - Friday always + displayName: Nightly Flake build + branches: + include: + - main + - release* + always: true + +# Variables that are available for the entire pipeline. +variables: + - template: templates/globals.yml + +stages: + - stage: Build + jobs: + - template: templates/jobs/build_compile.yml + + # Each item in each matrix has a number of possible values it may + # define. They are detailed in templates/test_phases.yml. The only + # required value is "TestsToRun". + + - stage: Linux + dependsOn: + - Build + jobs: + - job: 'Py3x_Linux' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + VSCODE_PYTHON_ROLLING: true + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + - stage: Mac + dependsOn: + - Build + jobs: + - job: 'Py3x_Mac' + dependsOn: [] + timeoutInMinutes: 120 + strategy: + matrix: + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + VSCODE_PYTHON_ROLLING: true + pool: + vmImage: '$(vmImageMacOS)' + steps: + - template: templates/test_phases.yml + + - stage: Windows + dependsOn: + - Build + jobs: + - job: 'Py3x_Windows' + dependsOn: [] + timeoutInMinutes: 180 + strategy: + matrix: + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + VSCODE_PYTHON_ROLLING: true + SplitFunctionalTests: true + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml diff --git a/build/ci/vscode-python-pr-validation.yaml b/build/ci/vscode-python-pr-validation.yaml new file mode 100644 index 000000000000..fa817bf379b5 --- /dev/null +++ b/build/ci/vscode-python-pr-validation.yaml @@ -0,0 +1,134 @@ +# PR Validation build. + +name: '$(Year:yyyy).$(Month).0.$(BuildID)-pr' + +# Notes: Only trigger a PR build for main and release, and skip build/rebuild +# on changes in the news and .vscode folders. +pr: + autoCancel: true + branches: + include: + - 'main' + - 'release*' + - 'ds*' + - 'logging-changes-and-drop-old-debugger' + paths: + exclude: ['/news/1 Enhancements', '/news/2 Fixes', '/news/3 Code Health', '/.vscode'] + +# Not the CI build for merges to main and release. +trigger: none + +# Variables that are available for the entire pipeline. +variables: + - template: templates/globals.yml + +stages: + - stage: Build + jobs: + - template: templates/jobs/build_compile.yml + + # Each item in each matrix has a number of possible values it may + # define. They are detailed in templates/test_phases.yml. The only + # required value is "TestsToRun". + + - stage: Linux + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + strategy: + matrix: + 'NodeUnit': + TestsToRun: 'testUnitTests' + NeedsPythonTestReqs: false + NeedsIPythonReqs: false + 'Unit': + TestsToRun: 'pythonUnitTests, pythonInternalTools, pythonIPythonTests' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + SplitFunctionalTests: false + 'Single Workspace': + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + 'DataScience': + TestsToRun: 'testDataScience' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Smoke': + TestsToRun: 'testSmoke' + NeedsPythonTestReqs: true + NeedsIPythonReqs: true + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + - job: 'Py27' + dependsOn: [] + strategy: + matrix: + 'Unit': + PythonVersion: '2.7' + # Note: "pythonInternalTools" tests are 3.7+. + TestsToRun: 'pythonUnitTests' + NeedsPythonTestReqs: true + 'Functional': + PythonVersion: '2.7' + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + pool: + vmImage: 'ubuntu-16.04' + steps: + - template: templates/test_phases.yml + + - stage: Mac + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + strategy: + matrix: + # This gives us our best functional coverage for the OS. + 'Functional+Single': + TestsToRun: 'testfunctional, testSingleWorkspace' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + pool: + vmImage: '$(vmImageMacOS)' + steps: + - template: templates/test_phases.yml + + - stage: Windows + dependsOn: [] + jobs: + - job: 'Py3x' + dependsOn: [] + timeoutInMinutes: 90 + strategy: + matrix: + # This gives us our best functional coverage for the OS. + 'Functional': + TestsToRun: 'testfunctional' + NeedsPythonTestReqs: true + NeedsPythonFunctionalReqs: true + 'Single Workspace': + TestsToRun: 'testSingleWorkspace' + NeedsPythonTestReqs: true + pool: + vmImage: 'vs2017-win2016' + steps: + - template: templates/test_phases.yml + + - stage: Reports + dependsOn: + - Linux + - Mac + - Windows + condition: always() + jobs: + - template: templates/jobs/coverage.yml diff --git a/build/conda-functional-requirements.txt b/build/conda-functional-requirements.txt new file mode 100644 index 000000000000..a00a76726786 --- /dev/null +++ b/build/conda-functional-requirements.txt @@ -0,0 +1,26 @@ +# List of requirements for conda environments that cannot be installed using conda +livelossplot +versioneer +flake8 +autopep8 +bandit +black ; python_version>='3.6' +yapf +pylint +pycodestyle +pydocstyle +nose +pytest==4.6.9 # Last version of pytest with Python 2.7 support +rope +flask +django +isort +pathlib2>=2.2.0 ; python_version<'3.6' # Python 2.7 compatibility (pytest) +pythreejs +ipysheet +ipyvolume +beakerx +py4j +bqplot +K3D +debugpy \ No newline at end of file diff --git a/build/constants.js b/build/constants.js new file mode 100644 index 000000000000..60109ea4ce0e --- /dev/null +++ b/build/constants.js @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +const util = require('./util'); +exports.ExtensionRootDir = util.ExtensionRootDir; +// This is a list of files that existed before MS got the extension. +exports.existingFiles = util.getListOfFiles('existingFiles.json'); +exports.contributedFiles = util.getListOfFiles('contributedFiles.json'); +exports.isWindows = /^win/.test(process.platform); +exports.isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; diff --git a/build/contributedFiles.json b/build/contributedFiles.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/build/contributedFiles.json @@ -0,0 +1 @@ +[] diff --git a/build/debug/replaceWithWebBrowserPanel.js b/build/debug/replaceWithWebBrowserPanel.js new file mode 100644 index 000000000000..21c293117535 --- /dev/null +++ b/build/debug/replaceWithWebBrowserPanel.js @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const path = require('path'); +const fs = require('fs-extra'); +const common = require('../constants'); + +// Used to debug extension with the DS UI loaded in a web browser. +// We don't want to include test code into extension, hence just patch the js code for debugging. +// This code is used in fucntional ui tests. + +const fileToModify = path.join(common.ExtensionRootDir, 'out/client/common/installer/serviceRegistry.js'); +const fileContents = fs.readFileSync(fileToModify).toString(); + +const newInjection = 'require("../../../test/datascience/uiTests/webBrowserPanelProvider").WebBrowserPanelProvider'; +const oldInjection = 'webPanelProvider_1.WebPanelProvider'; + +if (fileContents.indexOf(oldInjection) === -1 && fileContents.indexOf(newInjection) === -1) { + throw new Error('Unable to modify serviceRegistry.js for WebBrowser debugging'); +} +if (fileContents.indexOf(oldInjection)) { + const newFileContents = fileContents.replace(oldInjection, newInjection); + fs.writeFileSync(fileToModify, newFileContents); +} diff --git a/build/debugger-install-requirements.txt b/build/debugger-install-requirements.txt new file mode 100644 index 000000000000..6ee0765db4b3 --- /dev/null +++ b/build/debugger-install-requirements.txt @@ -0,0 +1,2 @@ +# Requirements needed to run install_debugpy.py +packaging diff --git a/build/existingFiles.json b/build/existingFiles.json new file mode 100644 index 000000000000..4dbf1af34b78 --- /dev/null +++ b/build/existingFiles.json @@ -0,0 +1,557 @@ +[ + "src/client/activation/activationService.ts", + "src/client/activation/downloadChannelRules.ts", + "src/client/activation/downloader.ts", + "src/client/activation/hashVerifier.ts", + "src/client/activation/interpreterDataService.ts", + "src/client/activation/jedi.ts", + "src/client/activation/languageServer/languageServer.ts", + "src/client/activation/languageServer/languageServerFolderService.ts", + "src/client/activation/languageServer/languageServerHashes.ts", + "src/client/activation/languageServer/languageServerPackageRepository.ts", + "src/client/activation/languageServer/languageServerPackageService.ts", + "src/client/activation/platformData.ts", + "src/client/activation/progress.ts", + "src/client/activation/serviceRegistry.ts", + "src/client/activation/types.ts", + "src/client/api.ts", + "src/client/application/diagnostics/applicationDiagnostics.ts", + "src/client/application/diagnostics/base.ts", + "src/client/application/diagnostics/checks/envPathVariable.ts", + "src/client/application/diagnostics/checks/invalidDebuggerType.ts", + "src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts", + "src/client/application/diagnostics/checks/powerShellActivation.ts", + "src/client/application/diagnostics/checks/pythonInterpreter.ts", + "src/client/application/diagnostics/commands/base.ts", + "src/client/application/diagnostics/commands/execVSCCommand.ts", + "src/client/application/diagnostics/commands/factory.ts", + "src/client/application/diagnostics/commands/ignore.ts", + "src/client/application/diagnostics/commands/launchBrowser.ts", + "src/client/application/diagnostics/commands/types.ts", + "src/client/application/diagnostics/constants.ts", + "src/client/application/diagnostics/filter.ts", + "src/client/application/diagnostics/promptHandler.ts", + "src/client/application/diagnostics/serviceRegistry.ts", + "src/client/application/diagnostics/types.ts", + "src/client/application/serviceRegistry.ts", + "src/client/application/types.ts", + "src/client/common/application/applicationEnvironment.ts", + "src/client/common/application/applicationShell.ts", + "src/client/common/application/commandManager.ts", + "src/client/common/application/debugService.ts", + "src/client/common/application/documentManager.ts", + "src/client/common/application/extensions.ts", + "src/client/common/application/terminalManager.ts", + "src/client/common/application/types.ts", + "src/client/common/application/workspace.ts", + "src/client/common/configSettingMonitor.ts", + "src/client/common/configSettings.ts", + "src/client/common/configuration/service.ts", + "src/client/common/constants.ts", + "src/client/common/contextKey.ts", + "src/client/common/editor.ts", + "src/client/common/envFileParser.ts", + "src/client/common/errors/errorUtils.ts", + "src/client/common/errors/moduleNotInstalledError.ts", + "src/client/common/extensions.ts", + "src/client/common/featureDeprecationManager.ts", + "src/client/common/helpers.ts", + "src/client/common/installer/channelManager.ts", + "src/client/common/installer/condaInstaller.ts", + "src/client/common/installer/moduleInstaller.ts", + "src/client/common/installer/pipEnvInstaller.ts", + "src/client/common/installer/pipInstaller.ts", + "src/client/common/installer/productInstaller.ts", + "src/client/common/installer/productNames.ts", + "src/client/common/installer/productPath.ts", + "src/client/common/installer/productService.ts", + "src/client/common/installer/serviceRegistry.ts", + "src/client/common/installer/types.ts", + "src/client/common/logger.ts", + "src/client/common/markdown/restTextConverter.ts", + "src/client/common/net/browser.ts", + "src/client/common/net/httpClient.ts", + "src/client/common/net/socket/socketCallbackHandler.ts", + "src/client/common/net/socket/socketServer.ts", + "src/client/common/net/socket/SocketStream.ts", + "src/client/common/nuget/azureBlobStoreNugetRepository.ts", + "src/client/common/nuget/nugetRepository.ts", + "src/client/common/nuget/nugetService.ts", + "src/client/common/nuget/types.ts", + "src/client/common/open.ts", + "src/client/common/persistentState.ts", + "src/client/common/platform/constants.ts", + "src/client/common/platform/fileSystem.ts", + "src/client/common/platform/osinfo.ts", + "src/client/common/platform/pathUtils.ts", + "src/client/common/platform/platformService.ts", + "src/client/common/platform/registry.ts", + "src/client/common/platform/serviceRegistry.ts", + "src/client/common/platform/types.ts", + "src/client/common/process/constants.ts", + "src/client/common/process/currentProcess.ts", + "src/client/common/process/decoder.ts", + "src/client/common/process/proc.ts", + "src/client/common/process/processFactory.ts", + "src/client/common/process/pythonExecutionFactory.ts", + "src/client/common/process/pythonProcess.ts", + "src/client/common/process/pythonToolService.ts", + "src/client/common/process/serviceRegistry.ts", + "src/client/common/process/types.ts", + "src/client/common/serviceRegistry.ts", + "src/client/common/terminal/activator/base.ts", + "src/client/common/terminal/activator/index.ts", + "src/client/common/terminal/activator/powershellFailedHandler.ts", + "src/client/common/terminal/commandPrompt.ts", + "src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts", + "src/client/common/terminal/environmentActivationProviders/bash.ts", + "src/client/common/terminal/environmentActivationProviders/commandPrompt.ts", + "src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts", + "src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts", + "src/client/common/terminal/factory.ts", + "src/client/common/terminal/helper.ts", + "src/client/common/terminal/service.ts", + "src/client/common/terminal/types.ts", + "src/client/common/types.ts", + "src/client/common/util.ts", + "src/client/common/utils/async.ts", + "src/client/common/utils/decorators.ts", + "src/client/common/utils/enum.ts", + "src/client/common/utils/fs.ts", + "src/client/common/utils/localize.ts", + "src/client/common/utils/logging.ts", + "src/client/common/utils/misc.ts", + "src/client/common/utils/platform.ts", + "src/client/common/utils/random.ts", + "src/client/common/utils/stopWatch.ts", + "src/client/common/utils/string.ts", + "src/client/common/utils/sysTypes.ts", + "src/client/common/utils/text.ts", + "src/client/common/utils/version.ts", + "src/client/common/variables/environment.ts", + "src/client/common/variables/environmentVariablesProvider.ts", + "src/client/common/variables/serviceRegistry.ts", + "src/client/common/variables/systemVariables.ts", + "src/client/common/variables/sysTypes.ts", + "src/client/common/variables/types.ts", + "src/client/debugger/constants.ts", + "src/client/debugger/extension/banner.ts", + "src/client/debugger/extension/configuration/baseProvider.ts", + "src/client/debugger/extension/configuration/configurationProviderUtils.ts", + "src/client/debugger/extension/configuration/pythonV2Provider.ts", + "src/client/debugger/extension/configuration/types.ts", + "src/client/debugger/extension/hooks/childProcessAttachHandler.ts", + "src/client/debugger/extension/hooks/childProcessAttachService.ts", + "src/client/debugger/extension/hooks/constants.ts", + "src/client/debugger/extension/hooks/eventHandlerDispatcher.ts", + "src/client/debugger/extension/hooks/processTerminationHandler.ts", + "src/client/debugger/extension/hooks/processTerminationService.ts", + "src/client/debugger/extension/hooks/types.ts", + "src/client/debugger/extension/serviceRegistry.ts", + "src/client/debugger/extension/types.ts", + "src/client/debugger/types.ts", + "src/client/extension.ts", + "src/client/formatters/autoPep8Formatter.ts", + "src/client/formatters/baseFormatter.ts", + "src/client/formatters/blackFormatter.ts", + "src/client/formatters/dummyFormatter.ts", + "src/client/formatters/helper.ts", + "src/client/formatters/lineFormatter.ts", + "src/client/formatters/serviceRegistry.ts", + "src/client/formatters/types.ts", + "src/client/formatters/yapfFormatter.ts", + "src/client/interpreter/configuration/interpreterComparer.ts", + "src/client/interpreter/configuration/interpreterSelector.ts", + "src/client/interpreter/configuration/pythonPathUpdaterService.ts", + "src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts", + "src/client/interpreter/configuration/services/globalUpdaterService.ts", + "src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts", + "src/client/interpreter/configuration/services/workspaceUpdaterService.ts", + "src/client/interpreter/configuration/types.ts", + "src/client/interpreter/contracts.ts", + "src/client/interpreter/display/index.ts", + "src/client/interpreter/display/shebangCodeLensProvider.ts", + "src/client/interpreter/helpers.ts", + "src/client/interpreter/interpreterService.ts", + "src/client/interpreter/interpreterVersion.ts", + "src/client/interpreter/locators/helpers.ts", + "src/client/interpreter/locators/index.ts", + "src/client/interpreter/locators/services/baseVirtualEnvService.ts", + "src/client/interpreter/locators/services/cacheableLocatorService.ts", + "src/client/interpreter/locators/services/conda.ts", + "src/client/interpreter/locators/services/condaEnvFileService.ts", + "src/client/interpreter/locators/services/condaEnvService.ts", + "src/client/interpreter/locators/services/condaHelper.ts", + "src/client/interpreter/locators/services/condaService.ts", + "src/client/interpreter/locators/services/currentPathService.ts", + "src/client/interpreter/locators/services/globalVirtualEnvService.ts", + "src/client/interpreter/locators/services/KnownPathsService.ts", + "src/client/interpreter/locators/services/pipEnvService.ts", + "src/client/interpreter/locators/services/windowsRegistryService.ts", + "src/client/interpreter/locators/services/workspaceVirtualEnvService.ts", + "src/client/interpreter/serviceRegistry.ts", + "src/client/interpreter/virtualEnvs/index.ts", + "src/client/interpreter/virtualEnvs/types.ts", + "src/client/ioc/container.ts", + "src/client/ioc/index.ts", + "src/client/ioc/serviceManager.ts", + "src/client/ioc/types.ts", + "src/client/jupyter/provider.ts", + "src/client/language/braceCounter.ts", + "src/client/language/characters.ts", + "src/client/language/characterStream.ts", + "src/client/language/iterableTextRange.ts", + "src/client/language/textBuilder.ts", + "src/client/language/textIterator.ts", + "src/client/language/textRangeCollection.ts", + "src/client/language/tokenizer.ts", + "src/client/language/types.ts", + "src/client/language/unicode.ts", + "src/client/languageServices/jediProxyFactory.ts", + "src/client/languageServices/proposeLanguageServerBanner.ts", + "src/client/linters/bandit.ts", + "src/client/linters/baseLinter.ts", + "src/client/linters/errorHandlers/baseErrorHandler.ts", + "src/client/linters/errorHandlers/errorHandler.ts", + "src/client/linters/errorHandlers/notInstalled.ts", + "src/client/linters/errorHandlers/standard.ts", + "src/client/linters/flake8.ts", + "src/client/linters/linterCommands.ts", + "src/client/linters/linterInfo.ts", + "src/client/linters/linterManager.ts", + "src/client/linters/lintingEngine.ts", + "src/client/linters/mypy.ts", + "src/client/linters/pycodestyle.ts", + "src/client/linters/prospector.ts", + "src/client/linters/pydocstyle.ts", + "src/client/linters/pylama.ts", + "src/client/linters/pylint.ts", + "src/client/linters/serviceRegistry.ts", + "src/client/linters/types.ts", + "src/client/providers/codeActionsProvider.ts", + "src/client/providers/completionProvider.ts", + "src/client/providers/completionSource.ts", + "src/client/providers/definitionProvider.ts", + "src/client/providers/docStringFoldingProvider.ts", + "src/client/providers/formatProvider.ts", + "src/client/providers/hoverProvider.ts", + "src/client/providers/importSortProvider.ts", + "src/client/providers/itemInfoSource.ts", + "src/client/providers/jediProxy.ts", + "src/client/providers/linterProvider.ts", + "src/client/providers/objectDefinitionProvider.ts", + "src/client/providers/providerUtilities.ts", + "src/client/providers/referenceProvider.ts", + "src/client/providers/renameProvider.ts", + "src/client/providers/replProvider.ts", + "src/client/providers/serviceRegistry.ts", + "src/client/providers/signatureProvider.ts", + "src/client/providers/simpleRefactorProvider.ts", + "src/client/providers/symbolProvider.ts", + "src/client/providers/terminalProvider.ts", + "src/client/providers/types.ts", + "src/client/refactor/contracts.ts", + "src/client/refactor/proxy.ts", + "src/client/telemetry/constants.ts", + "src/client/telemetry/index.ts", + "src/client/telemetry/types.ts", + "src/client/telemetry/vscode-extension-telemetry.d.ts", + "src/client/terminals/activation.ts", + "src/client/terminals/codeExecution/codeExecutionManager.ts", + "src/client/terminals/codeExecution/djangoContext.ts", + "src/client/terminals/codeExecution/djangoShellCodeExecution.ts", + "src/client/terminals/codeExecution/helper.ts", + "src/client/terminals/codeExecution/repl.ts", + "src/client/terminals/codeExecution/terminalCodeExecution.ts", + "src/client/terminals/serviceRegistry.ts", + "src/client/terminals/types.ts", + "src/client/typeFormatters/blockFormatProvider.ts", + "src/client/typeFormatters/codeBlockFormatProvider.ts", + "src/client/typeFormatters/contracts.ts", + "src/client/typeFormatters/dispatcher.ts", + "src/client/typeFormatters/onEnterFormatter.ts", + "src/client/testing/codeLenses/main.ts", + "src/client/testing/codeLenses/testFiles.ts", + "src/client/testing/common/argumentsHelper.ts", + "src/client/testing/common/constants.ts", + "src/client/testing/common/debugLauncher.ts", + "src/client/testing/common/managers/baseTestManager.ts", + "src/client/testing/common/managers/testConfigurationManager.ts", + "src/client/testing/common/runner.ts", + "src/client/testing/common/services/configSettingService.ts", + "src/client/testing/common/services/storageService.ts", + "src/client/testing/common/services/testManagerService.ts", + "src/client/testing/common/services/testResultsService.ts", + "src/client/testing/common/services/workspaceTestManagerService.ts", + "src/client/testing/common/testUtils.ts", + "src/client/testing/common/testVisitors/flatteningVisitor.ts", + "src/client/testing/common/testVisitors/folderGenerationVisitor.ts", + "src/client/testing/common/testVisitors/resultResetVisitor.ts", + "src/client/testing/common/types.ts", + "src/client/testing/common/xUnitParser.ts", + "src/client/testing/configuration.ts", + "src/client/testing/configurationFactory.ts", + "src/client/testing/display/main.ts", + "src/client/testing/display/picker.ts", + "src/client/testing/main.ts", + "src/client/testing/nosetest/main.ts", + "src/client/testing/nosetest/runner.ts", + "src/client/testing/nosetest/services/argsService.ts", + "src/client/testing/nosetest/services/discoveryService.ts", + "src/client/testing/nosetest/services/parserService.ts", + "src/client/testing/nosetest/testConfigurationManager.ts", + "src/client/testing/pytest/main.ts", + "src/client/testing/pytest/runner.ts", + "src/client/testing/pytest/services/argsService.ts", + "src/client/testing/pytest/services/discoveryService.ts", + "src/client/testing/pytest/services/parserService.ts", + "src/client/testing/pytest/testConfigurationManager.ts", + "src/client/testing/serviceRegistry.ts", + "src/client/testing/types.ts", + "src/client/testing/unittest/helper.ts", + "src/client/testing/unittest/main.ts", + "src/client/testing/unittest/runner.ts", + "src/client/testing/unittest/services/argsService.ts", + "src/client/testing/unittest/services/discoveryService.ts", + "src/client/testing/unittest/services/parserService.ts", + "src/client/testing/unittest/socketServer.ts", + "src/client/testing/unittest/testConfigurationManager.ts", + "src/client/workspaceSymbols/contracts.ts", + "src/client/workspaceSymbols/generator.ts", + "src/client/workspaceSymbols/main.ts", + "src/client/workspaceSymbols/parser.ts", + "src/client/workspaceSymbols/provider.ts", + "src/server/dummy.ts", + "src/test/aaFirstTest/aaFirstTest.test.ts", + "src/test/activation/activationService.unit.test.ts", + "src/test/activation/downloadChannelRules.unit.test.ts", + "src/test/activation/downloader.unit.test.ts", + "src/test/activation/excludeFiles.ls.test.ts", + "src/test/activation/languageServer/languageServer.unit.test.ts", + "src/test/activation/languageServer/languageServerFolderService.unit.test.ts", + "src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts", + "src/test/activation/languageServer/languageServerPackageService.test.ts", + "src/test/activation/languageServer/languageServerPackageService.unit.test.ts", + "src/test/activation/platformData.unit.test.ts", + "src/test/application/diagnostics/applicationDiagnostics.unit.test.ts", + "src/test/application/diagnostics/checks/envPathVariable.unit.test.ts", + "src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts", + "src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts", + "src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts", + "src/test/application/diagnostics/commands/factory.unit.test.ts", + "src/test/application/diagnostics/commands/ignore.unit.test.ts", + "src/test/application/diagnostics/commands/launchBrowser.unit.test.ts", + "src/test/application/diagnostics/filter.unit.test.ts", + "src/test/application/diagnostics/promptHandler.unit.test.ts", + "src/test/autocomplete/base.test.ts", + "src/test/autocomplete/pep484.test.ts", + "src/test/autocomplete/pep526.test.ts", + "src/test/ciConstants.ts", + "src/test/common.ts", + "src/test/common/configSettings.multiroot.test.ts", + "src/test/common/configSettings.test.ts", + "src/test/common/configSettings.unit.test.ts", + "src/test/common/configuration/service.test.ts", + "src/test/common/extensions.unit.test.ts", + "src/test/common/featureDeprecationManager.unit.test.ts", + "src/test/common/helpers.test.ts", + "src/test/common/installer.test.ts", + "src/test/common/installer/installer.invalidPath.unit.test.ts", + "src/test/common/installer/installer.unit.test.ts", + "src/test/common/installer/moduleInstaller.unit.test.ts", + "src/test/common/installer/productPath.unit.test.ts", + "src/test/common/localize.unit.test.ts", + "src/test/common/misc.test.ts", + "src/test/common/moduleInstaller.test.ts", + "src/test/common/net/httpClient.unit.test.ts", + "src/test/common/nuget/azureBobStoreRepository.test.ts", + "src/test/common/nuget/nugetRepository.unit.test.ts", + "src/test/common/nuget/nugetService.unit.test.ts", + "src/test/common/platform/filesystem.unit.test.ts", + "src/test/common/platform/osinfo.unit.test.ts", + "src/test/common/platform/platformService.unit.test.ts", + "src/test/common/process/currentProcess.test.ts", + "src/test/common/process/decoder.test.ts", + "src/test/common/process/execFactory.test.ts", + "src/test/common/process/proc.exec.test.ts", + "src/test/common/process/proc.observable.test.ts", + "src/test/common/process/proc.unit.test.ts", + "src/test/common/process/pythonProc.simple.multiroot.test.ts", + "src/test/common/socketCallbackHandler.test.ts", + "src/test/common/socketStream.test.ts", + "src/test/common/terminals/activation.bash.unit.test.ts", + "src/test/common/terminals/activation.commandPrompt.unit.test.ts", + "src/test/common/terminals/activation.conda.unit.test.ts", + "src/test/common/terminals/activation.unit.test.ts", + "src/test/common/terminals/activator/base.unit.test.ts", + "src/test/common/terminals/activator/index.unit.test.ts", + "src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts", + "src/test/common/terminals/commandPrompt.unit.test.ts", + "src/test/common/terminals/factory.unit.test.ts", + "src/test/common/terminals/helper.activation.unit.test.ts", + "src/test/common/terminals/helper.unit.test.ts", + "src/test/common/terminals/pyenvActivationProvider.unit.test.ts", + "src/test/common/terminals/service.unit.test.ts", + "src/test/common/utils/async.unit.test.ts", + "src/test/common/utils/platform.unit.test.ts", + "src/test/common/utils/string.unit.test.ts", + "src/test/common/utils/text.unit.test.ts", + "src/test/common/utils/version.unit.test.ts", + "src/test/common/variables/envVarsProvider.multiroot.test.ts", + "src/test/common/variables/envVarsService.test.ts", + "src/test/configuration/interpreterSelector.unit.test.ts", + "src/test/constants.ts", + "src/test/core.ts", + "src/test/debugger/capabilities.test.ts", + "src/test/debugger/common/constants.ts", + "src/test/debugger/common/debugStreamProvider.test.ts", + "src/test/debugger/common/protocoloLogger.test.ts", + "src/test/debugger/common/protocolparser.test.ts", + "src/test/debugger/common/protocolWriter.test.ts", + "src/test/debugger/debugClient.ts", + "src/test/debugger/envVars.test.ts", + "src/test/debugger/extension/banner.unit.test.ts", + "src/test/debugger/extension/configProvider/provider.attach.unit.test.ts", + "src/test/debugger/extension/configProvider/provider.unit.test.ts", + "src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts", + "src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts", + "src/test/debugger/extension/hooks/processTerminationHandler.unit.test.ts", + "src/test/debugger/extension/hooks/processTerminationService.test.ts", + "src/test/debugger/launcherScriptProvider.unit.test.ts", + "src/test/debugger/misc.test.ts", + "src/test/debugger/portAndHost.test.ts", + "src/test/debugger/run.test.ts", + "src/test/debugger/utils.ts", + "src/test/debuggerTest.ts", + "src/test/definitions/hover.jedi.test.ts", + "src/test/definitions/hover.ls.test.ts", + "src/test/definitions/navigation.test.ts", + "src/test/definitions/parallel.jedi.test.ts", + "src/test/definitions/parallel.ls.test.ts", + "src/test/format/extension.dispatch.test.ts", + "src/test/format/extension.format.test.ts", + "src/test/format/extension.formatOnSave.test.ts", + "src/test/format/extension.lineFormatter.test.ts", + "src/test/format/extension.onEnterFormat.test.ts", + "src/test/format/extension.onTypeFormat.test.ts", + "src/test/format/extension.sort.test.ts", + "src/test/format/format.helper.test.ts", + "src/test/index.ts", + "src/test/initialize.ts", + "src/test/install/channelManager.channels.test.ts", + "src/test/install/channelManager.messages.test.ts", + "src/test/interpreters/condaEnvFileService.unit.test.ts", + "src/test/interpreters/condaEnvService.unit.test.ts", + "src/test/interpreters/condaHelper.unit.test.ts", + "src/test/interpreters/condaService.unit.test.ts", + "src/test/interpreters/currentPathService.unit.test.ts", + "src/test/interpreters/display.unit.test.ts", + "src/test/interpreters/helper.unit.test.ts", + "src/test/interpreters/interpreterService.unit.test.ts", + "src/test/interpreters/interpreterVersion.unit.test.ts", + "src/test/interpreters/knownPathService.unit.test.ts", + "src/test/interpreters/locators/helpers.unit.test.ts", + "src/test/interpreters/locators/index.unit.test.ts", + "src/test/interpreters/mocks.ts", + "src/test/interpreters/pipEnvService.unit.test.ts", + "src/test/interpreters/pythonPathUpdater.test.ts", + "src/test/interpreters/venv.unit.test.ts", + "src/test/interpreters/virtualEnvManager.unit.test.ts", + "src/test/interpreters/virtualEnvs/index.unit.test.ts", + "src/test/interpreters/windowsRegistryService.unit.test.ts", + "src/test/language/characterStream.test.ts", + "src/test/language/textIterator.test.ts", + "src/test/language/textRange.test.ts", + "src/test/language/textRangeCollection.test.ts", + "src/test/language/tokenizer.test.ts", + "src/test/linters/lint.args.test.ts", + "src/test/linters/lint.commands.test.ts", + "src/test/linters/lint.manager.test.ts", + "src/test/linters/lint.multiroot.test.ts", + "src/test/linters/lint.provider.test.ts", + "src/test/linters/lint.test.ts", + "src/test/linters/lintengine.test.ts", + "src/test/linters/mypy.unit.test.ts", + "src/test/linters/pylint.test.ts", + "src/test/markdown/restTextConverter.test.ts", + "src/test/mockClasses.ts", + "src/test/mocks/mementos.ts", + "src/test/mocks/moduleInstaller.ts", + "src/test/mocks/proc.ts", + "src/test/mocks/process.ts", + "src/test/mocks/vsc/arrays.ts", + "src/test/mocks/vsc/extHostedTypes.ts", + "src/test/mocks/vsc/htmlContent.ts", + "src/test/mocks/vsc/index.ts", + "src/test/mocks/vsc/position.ts", + "src/test/mocks/vsc/range.ts", + "src/test/mocks/vsc/selection.ts", + "src/test/mocks/vsc/strings.ts", + "src/test/mocks/vsc/telemetryReporter.ts", + "src/test/mocks/vsc/uri.ts", + "src/test/multiRootTest.ts", + "src/test/performance/load.perf.test.ts", + "src/test/performanceTest.ts", + "src/test/providers/codeActionsProvider.test.ts", + "src/test/providers/completionSource.unit.test.ts", + "src/test/providers/foldingProvider.test.ts", + "src/test/providers/importSortProvider.unit.test.ts", + "src/test/providers/pythonSignatureProvider.unit.test.ts", + "src/test/providers/repl.unit.test.ts", + "src/test/providers/shebangCodeLenseProvider.test.ts", + "src/test/providers/symbolProvider.unit.test.ts", + "src/test/providers/terminal.unit.test.ts", + "src/test/pythonFiles/formatting/dummy.ts", + "src/test/refactor/extension.refactor.extract.method.test.ts", + "src/test/refactor/extension.refactor.extract.var.test.ts", + "src/test/refactor/rename.test.ts", + "src/test/serviceRegistry.ts", + "src/test/signature/signature.jedi.test.ts", + "src/test/signature/signature.ls.test.ts", + "src/test/standardTest.ts", + "src/test/stub.ts", + "src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts", + "src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts", + "src/test/terminals/codeExecution/helper.test.ts", + "src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts", + "src/test/testRunner.ts", + "src/test/textUtils.ts", + "src/test/unittests.ts", + "src/test/testing/argsService.test.ts", + "src/test/testing/banners/languageServerSurvey.unit.test.ts", + "src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts", + "src/test/testing/common/argsHelper.unit.test.ts", + "src/test/testing/common/debugLauncher.test.ts", + "src/test/testing/common/managers/testConfigurationManager.unit.test.ts", + "src/test/testing/common/services/configSettingService.unit.test.ts", + "src/test/testing/configuration.unit.test.ts", + "src/test/testing/configurationFactory.unit.test.ts", + "src/test/testing/debugger.test.ts", + "src/test/testing/display/main.test.ts", + "src/test/testing/helper.ts", + "src/test/testing/mocks.ts", + "src/test/testing/nosetest/nosetest.argsService.unit.test.ts", + "src/test/testing/nosetest/nosetest.discovery.unit.test.ts", + "src/test/testing/nosetest/nosetest.disovery.test.ts", + "src/test/testing/nosetest/nosetest.run.test.ts", + "src/test/testing/nosetest/nosetest.test.ts", + "src/test/testing/pytest/pytest_unittest_parser_data.ts", + "src/test/testing/pytest/pytest.argsService.unit.test.ts", + "src/test/testing/pytest/pytest.discovery.test.ts", + "src/test/testing/pytest/pytest.discovery.unit.test.ts", + "src/test/testing/pytest/pytest.run.test.ts", + "src/test/testing/pytest/pytest.test.ts", + "src/test/testing/pytest/pytest.testparser.unit.test.ts", + "src/test/testing/rediscover.test.ts", + "src/test/testing/serviceRegistry.ts", + "src/test/testing/stoppingDiscoverAndTest.test.ts", + "src/test/testing/unittest/unittest.argsService.unit.test.ts", + "src/test/testing/unittest/unittest.discovery.test.ts", + "src/test/testing/unittest/unittest.discovery.unit.test.ts", + "src/test/testing/unittest/unittest.run.test.ts", + "src/test/testing/unittest/unittest.test.ts", + "src/test/vscode-mock.ts", + "src/test/workspaceSymbols/common.ts", + "src/test/workspaceSymbols/multiroot.test.ts", + "src/test/workspaceSymbols/standard.test.ts" +] diff --git a/build/functional-test-requirements.txt b/build/functional-test-requirements.txt new file mode 100644 index 000000000000..d2f1977a7be4 --- /dev/null +++ b/build/functional-test-requirements.txt @@ -0,0 +1,2 @@ +# List of requirements for functional tests +versioneer diff --git a/build/ipython-test-requirements.txt b/build/ipython-test-requirements.txt new file mode 100644 index 000000000000..688e039a4461 --- /dev/null +++ b/build/ipython-test-requirements.txt @@ -0,0 +1,4 @@ +# List of requirements for ipython tests +numpy +pandas +ipython diff --git a/build/test-requirements.txt b/build/test-requirements.txt new file mode 100644 index 000000000000..90239895741d --- /dev/null +++ b/build/test-requirements.txt @@ -0,0 +1,21 @@ +# Install flake8 first, as both flake8 and autopep8 require pycodestyle, +# but flake8 has a tighter pinning. +flake8 +autopep8 +bandit +black ; python_version>='3.6' +yapf +pylint +pycodestyle +prospector==1.2.0 # Last version of prospector with Python 2.7 support +pydocstyle +nose +pytest < 6.0.0; python_version > '2.7' # Tests do not support pytest 6 yet. +rope +flask +django +isort + # Python 2.7 compatibility (pytest) +pytest==4.6.9; python_version == '2.7' +pathlib2>=2.2.0; python_version == '2.7' +py==1.8.1; python_version == '2.7' diff --git a/build/tslint-rules/baseRuleWalker.js b/build/tslint-rules/baseRuleWalker.js new file mode 100644 index 000000000000..b8ce93d4179d --- /dev/null +++ b/build/tslint-rules/baseRuleWalker.js @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +const path = require('path'); +const Lint = require('tslint'); +const util = require('../util'); +class BaseRuleWalker extends Lint.RuleWalker { + shouldIgnoreCurrentFile(node, filesToIgnore) { + const sourceFile = node.getSourceFile(); + if (sourceFile && sourceFile.fileName) { + const filename = path.resolve(util.ExtensionRootDir, sourceFile.fileName); + if (filesToIgnore.indexOf(filename.replace(/\//g, path.sep)) >= 0) { + return true; + } + } + return false; + } +} +exports.BaseRuleWalker = BaseRuleWalker; diff --git a/build/tslint-rules/messagesMustBeLocalizedRule.js b/build/tslint-rules/messagesMustBeLocalizedRule.js new file mode 100644 index 000000000000..acf4beaba811 --- /dev/null +++ b/build/tslint-rules/messagesMustBeLocalizedRule.js @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +const path = require('path'); +const Lint = require('tslint'); +const ts = require('typescript'); +const util = require('../util'); +const baseRuleWalker = require('./baseRuleWalker'); +const methodNames = [ + // From IApplicationShell (vscode.window): + 'showErrorMessage', + 'showInformationMessage', + 'showWarningMessage', + 'setStatusBarMessage', + // From IOutputChannel (vscode.OutputChannel): + 'appendLine', + 'appendLine' +]; +// tslint:ignore-next-line:no-suspicious-comments +// TODO: Ideally we would not ignore any files. +const ignoredFiles = util.getListOfFiles('unlocalizedFiles.json'); +const ignoredPrefix = path.normalize('src/test'); +const failureMessage = 'Messages must be localized in the Python Extension (use src/client/common/utils/localize.ts)'; +class NoStringLiteralsInMessages extends baseRuleWalker.BaseRuleWalker { + visitCallExpression(node) { + if (!this.shouldIgnoreNode(node)) { + node.arguments + .filter((arg) => ts.isStringLiteral(arg) || ts.isTemplateLiteral(arg)) + .forEach((arg) => { + this.addFailureAtNode(arg, failureMessage); + }); + } + super.visitCallExpression(node); + } + shouldIgnoreCurrentFile(node) { + //console.log(''); + //console.log(node.getSourceFile().fileName); + //console.log(ignoredFiles); + if (super.shouldIgnoreCurrentFile(node, ignoredFiles)) { + return true; + } + const sourceFile = node.getSourceFile(); + if (sourceFile && sourceFile.fileName) { + if (sourceFile.fileName.startsWith(ignoredPrefix)) { + return true; + } + } + return false; + } + shouldIgnoreNode(node) { + if (this.shouldIgnoreCurrentFile(node)) { + return true; + } + if (!ts.isPropertyAccessExpression(node.expression)) { + return true; + } + const prop = node.expression; + if (methodNames.indexOf(prop.name.text) < 0) { + return true; + } + return false; + } +} +class Rule extends Lint.Rules.AbstractRule { + apply(sourceFile) { + return this.applyWithWalker(new NoStringLiteralsInMessages(sourceFile, this.getOptions())); + } +} +Rule.FAILURE_STRING = failureMessage; +exports.Rule = Rule; diff --git a/build/unlocalizedFiles.json b/build/unlocalizedFiles.json new file mode 100644 index 000000000000..4da3d450af23 --- /dev/null +++ b/build/unlocalizedFiles.json @@ -0,0 +1,26 @@ +[ + "src/client/activation/activationService.ts", + "src/client/common/installer/channelManager.ts", + "src/client/common/installer/moduleInstaller.ts", + "src/client/common/installer/productInstaller.ts", + "src/client/debugger/extension/hooks/childProcessAttachService.ts", + "src/client/formatters/baseFormatter.ts", + "src/client/formatters/blackFormatter.ts", + "src/client/interpreter/configuration/pythonPathUpdaterService.ts", + "src/client/linters/errorHandlers/notInstalled.ts", + "src/client/linters/errorHandlers/standard.ts", + "src/client/linters/linterCommands.ts", + "src/client/linters/prospector.ts", + "src/client/providers/importSortProvider.ts", + "src/client/providers/objectDefinitionProvider.ts", + "src/client/providers/simpleRefactorProvider.ts", + "src/client/pythonEnvironments/discovery/locators/services/pipEnvService.ts", + "src/client/terminals/codeExecution/helper.ts", + "src/client/testing/common/debugLauncher.ts", + "src/client/testing/common/managers/baseTestManager.ts", + "src/client/testing/common/services/discovery.ts", + "src/client/testing/configuration.ts", + "src/client/testing/display/main.ts", + "src/client/testing/main.ts", + "src/client/workspaceSymbols/generator.ts" +] diff --git a/build/util.js b/build/util.js new file mode 100644 index 000000000000..93a60e7fb19b --- /dev/null +++ b/build/util.js @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +const fs = require('fs'); +const path = require('path'); +exports.ExtensionRootDir = path.dirname(__dirname); +function getListOfFiles(filename) { + filename = path.normalize(filename); + if (!path.isAbsolute(filename)) { + filename = path.join(__dirname, filename); + } + const data = fs.readFileSync(filename).toString(); + const files = JSON.parse(data); + return files.map((file) => { + return path.join(exports.ExtensionRootDir, file.replace(/\//g, path.sep)); + }); +} +exports.getListOfFiles = getListOfFiles; diff --git a/build/webpack/common.js b/build/webpack/common.js new file mode 100644 index 000000000000..4d3ef5a2367a --- /dev/null +++ b/build/webpack/common.js @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +const glob = require('glob'); +const path = require('path'); +const webpack_bundle_analyzer = require('webpack-bundle-analyzer'); +const constants = require('../constants'); +exports.nodeModulesToExternalize = [ + 'unicode/category/Lu', + 'unicode/category/Ll', + 'unicode/category/Lt', + 'unicode/category/Lo', + 'unicode/category/Lm', + 'unicode/category/Nl', + 'unicode/category/Mn', + 'unicode/category/Mc', + 'unicode/category/Nd', + 'unicode/category/Pc', + '@jupyterlab/services', + 'azure-storage', + 'request', + 'request-progress', + 'source-map-support', + 'diff-match-patch', + 'sudo-prompt', + 'node-stream-zip', + 'xml2js', + 'vsls/vscode', + 'pdfkit/js/pdfkit.standalone', + 'crypto-js', + 'fontkit', + 'linebreak', + 'png-js', + 'zeromq' +]; +exports.nodeModulesToReplacePaths = [...exports.nodeModulesToExternalize]; +function getDefaultPlugins(name) { + const plugins = []; + // Only run the analyzer on a local machine or if required + if (!constants.isCI || process.env.VSC_PYTHON_FORCE_ANALYZER) { + plugins.push( + new webpack_bundle_analyzer.BundleAnalyzerPlugin({ + analyzerMode: 'static', + reportFilename: `${name}.analyzer.html`, + generateStatsFile: true, + statsFilename: `${name}.stats.json`, + openAnalyzer: false // Open file manually if you want to see it :) + }) + ); + } + return plugins; +} +exports.getDefaultPlugins = getDefaultPlugins; +function getListOfExistingModulesInOutDir() { + const outDir = path.join(constants.ExtensionRootDir, 'out', 'client'); + const files = glob.sync('**/*.js', { sync: true, cwd: outDir }); + return files.map((filePath) => `./${filePath.slice(0, -3)}`); +} +exports.getListOfExistingModulesInOutDir = getListOfExistingModulesInOutDir; diff --git a/build/webpack/loaders/externalizeDependencies.js b/build/webpack/loaders/externalizeDependencies.js new file mode 100644 index 000000000000..ff2fc0b81ea2 --- /dev/null +++ b/build/webpack/loaders/externalizeDependencies.js @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const common = require('../common'); +function replaceModule(prefixRegex, prefix, contents, moduleName, quotes) { + const stringToSearch = `${prefixRegex}${quotes}${moduleName}${quotes}`; + const stringToReplaceWith = `${prefix}${quotes}./node_modules/${moduleName}${quotes}`; + return contents.replace(new RegExp(stringToSearch, 'gm'), stringToReplaceWith); +} +// tslint:disable:no-default-export no-invalid-this +function default_1(source) { + common.nodeModulesToReplacePaths.forEach((moduleName) => { + if (source.indexOf(moduleName) > 0) { + source = replaceModule('import\\(', 'import(', source, moduleName, '"'); + source = replaceModule('import\\(', 'import(', source, moduleName, "'"); + source = replaceModule('require\\(', 'require(', source, moduleName, '"'); + source = replaceModule('require\\(', 'require(', source, moduleName, "'"); + source = replaceModule('from ', 'from ', source, moduleName, '"'); + source = replaceModule('from ', 'from ', source, moduleName, "'"); + } + }); + return source; +} +exports.default = default_1; diff --git a/build/webpack/loaders/fixNodeFetch.js b/build/webpack/loaders/fixNodeFetch.js new file mode 100644 index 000000000000..0d648d131349 --- /dev/null +++ b/build/webpack/loaders/fixNodeFetch.js @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const path = require('path'); +const constants = require('../../constants'); + +const nodeFetchIndexFile = path.join( + constants.ExtensionRootDir, + 'node_modules', + '@jupyterlab', + 'services', + 'node_modules', + 'node-fetch', + 'lib', + 'index.js' +); +// On windows replace `\` with `\\`, else we get an error in webpack (Module parse failed: Octal literal in strict mode). +const nodeFetchFile = constants.isWindows ? nodeFetchIndexFile.replace(/\\/g, '\\\\') : nodeFetchIndexFile; + +/** + * Node fetch has an es6 module file. That gets bundled into @jupyterlab/services. + * However @jupyterlab/services/serverconnection.js is written such that it uses fetch from either node or browser. + * We need to force the browser version for things to work correctly. + * + * @export + * @param {string} source + * @returns + */ +exports.default = function (source) { + if (source.indexOf("require('node-fetch')") > 0) { + source = source.replace(/require\('node-fetch'\)/g, `require('${nodeFetchFile}')`); + } + return source; +}; diff --git a/build/webpack/loaders/jsonloader.js b/build/webpack/loaders/jsonloader.js new file mode 100644 index 000000000000..5ec3c7038681 --- /dev/null +++ b/build/webpack/loaders/jsonloader.js @@ -0,0 +1,7 @@ +// For some reason this has to be in commonjs format + +module.exports = function (source) { + // Just inline the source and fix up defaults so that they don't + // mess up the logic in the setOptions.js file + return `module.exports = ${source}\nmodule.exports.default = false`; +}; diff --git a/build/webpack/loaders/remarkLoader.js b/build/webpack/loaders/remarkLoader.js new file mode 100644 index 000000000000..5ec3c7038681 --- /dev/null +++ b/build/webpack/loaders/remarkLoader.js @@ -0,0 +1,7 @@ +// For some reason this has to be in commonjs format + +module.exports = function (source) { + // Just inline the source and fix up defaults so that they don't + // mess up the logic in the setOptions.js file + return `module.exports = ${source}\nmodule.exports.default = false`; +}; diff --git a/build/webpack/nativeOrInteractivePicker.html b/build/webpack/nativeOrInteractivePicker.html new file mode 100644 index 000000000000..46d6f0e7eb52 --- /dev/null +++ b/build/webpack/nativeOrInteractivePicker.html @@ -0,0 +1,8 @@ + + + + + Click here to Open Native Editor
+ Click here to Open Interactive Window + + diff --git a/build/webpack/pdfkit.js b/build/webpack/pdfkit.js new file mode 100644 index 000000000000..5c31590a3924 --- /dev/null +++ b/build/webpack/pdfkit.js @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/* +This file is only used when using webpack for bundling. +We have a dummy file so that webpack doesn't fall over when trying to bundle pdfkit. +Just point it to a dummy file (this file). +Once webpack is done, we override the pdfkit.js file in the externalized node modules directory +with the actual source of pdfkit that needs to be used by nodejs (our extension code). +*/ + +class PDFDocument {} +module.exports = PDFDocument; diff --git a/build/webpack/plugins/less-plugin-base64.js b/build/webpack/plugins/less-plugin-base64.js new file mode 100644 index 000000000000..7c42013b4e55 --- /dev/null +++ b/build/webpack/plugins/less-plugin-base64.js @@ -0,0 +1,64 @@ +// Most of this was based on https://github.com/less/less-plugin-inline-urls +// License for this was included in the ThirdPartyNotices-Repository.txt +const less = require('less'); + +class Base64MimeTypeNode { + constructor() { + this.value = 'image/svg+xml;base64'; + this.type = 'Base64MimeTypeNode'; + } + + eval(context) { + return this; + } +} + +class Base64Visitor { + constructor() { + this.visitor = new less.visitors.Visitor(this); + + // Set to a preEval visitor to make sure this runs before + // any evals + this.isPreEvalVisitor = true; + + // Make sure this is a replacing visitor so we remove the old data. + this.isReplacing = true; + } + + run(root) { + return this.visitor.visit(root); + } + + visitUrl(URLNode, visitArgs) { + // Return two new nodes in the call. One that has the mime type and other with the node. The data-uri + // evaluator will transform this into a base64 string + return new less.tree.Call( + 'data-uri', + [new Base64MimeTypeNode(), URLNode.value], + URLNode.index || 0, + URLNode.currentFileInfo + ); + } +} +/* + * This was originally used to perform less on uris and turn them into base64 encoded so they can be loaded into + * a webpack html. There's one caveat though. Less and webpack don't play well together. It runs the less at the root dir. + * This means in order to use this in a less file, you need to qualify the urls as if they come from the root dir. + * Example: + * url("./foo.svg") + * becomes + * url("./src/datascience-ui/history-react/images/foo.svg") + */ +class Base64Plugin { + constructor() {} + + install(less, pluginManager) { + pluginManager.addVisitor(new Base64Visitor()); + } + + printUsage() { + console.log('Base64 Plugin. Add to your webpack.config.js as a plugin to convert URLs to base64 inline'); + } +} + +module.exports = Base64Plugin; diff --git a/build/webpack/webpack.datascience-ui-notebooks.config.js b/build/webpack/webpack.datascience-ui-notebooks.config.js new file mode 100644 index 000000000000..e661d5ee7620 --- /dev/null +++ b/build/webpack/webpack.datascience-ui-notebooks.config.js @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const builder = require('./webpack.datascience-ui.config.builder'); +module.exports = [builder.notebooks]; diff --git a/build/webpack/webpack.datascience-ui-renderers.config.js b/build/webpack/webpack.datascience-ui-renderers.config.js new file mode 100644 index 000000000000..022a483c113a --- /dev/null +++ b/build/webpack/webpack.datascience-ui-renderers.config.js @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const builder = require('./webpack.datascience-ui.config.builder'); +module.exports = [builder.renderers]; diff --git a/build/webpack/webpack.datascience-ui-viewers.config.js b/build/webpack/webpack.datascience-ui-viewers.config.js new file mode 100644 index 000000000000..89e3f2c56fdd --- /dev/null +++ b/build/webpack/webpack.datascience-ui-viewers.config.js @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const builder = require('./webpack.datascience-ui.config.builder'); +module.exports = [builder.viewers]; diff --git a/build/webpack/webpack.datascience-ui.config.builder.js b/build/webpack/webpack.datascience-ui.config.builder.js new file mode 100644 index 000000000000..c85046241aa6 --- /dev/null +++ b/build/webpack/webpack.datascience-ui.config.builder.js @@ -0,0 +1,339 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// Note to editors, if you change this file you have to restart compile-webviews. +// It doesn't reload the config otherwise. +const common = require('./common'); +const webpack = require('webpack'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const FixDefaultImportPlugin = require('webpack-fix-default-import-plugin'); +const path = require('path'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); +const constants = require('../constants'); +const configFileName = 'tsconfig.datascience-ui.json'; +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); + +// Any build on the CI is considered production mode. +const isProdBuild = constants.isCI || process.argv.includes('--mode'); + +function getEntry(bundle) { + switch (bundle) { + case 'notebook': + return { + nativeEditor: ['babel-polyfill', `./src/datascience-ui/native-editor/index.tsx`], + interactiveWindow: ['babel-polyfill', `./src/datascience-ui/history-react/index.tsx`] + }; + case 'renderers': + return { + renderers: ['babel-polyfill', `./src/datascience-ui/renderers/index.tsx`] + }; + case 'viewers': + return { + plotViewer: ['babel-polyfill', `./src/datascience-ui/plot/index.tsx`], + dataExplorer: ['babel-polyfill', `./src/datascience-ui/data-explorer/index.tsx`], + startPage: ['babel-polyfill', `./src/datascience-ui/startPage/index.tsx`] + }; + default: + throw new Error(`Bundle not supported ${bundle}`); + } +} + +function getPlugins(bundle) { + const plugins = [ + new ForkTsCheckerWebpackPlugin({ + checkSyntacticErrors: true, + tsconfig: configFileName, + reportFiles: ['src/datascience-ui/**/*.{ts,tsx}'], + memoryLimit: 9096 + }) + ]; + if (isProdBuild) { + plugins.push(...common.getDefaultPlugins(bundle)); + } + switch (bundle) { + case 'notebook': + plugins.push( + new MonacoWebpackPlugin({ + languages: [] // force to empty so onigasm will be used + }), + new HtmlWebpackPlugin({ + template: path.join(__dirname, '/nativeOrInteractivePicker.html'), + chunks: [], + filename: 'index.html' + }), + new HtmlWebpackPlugin({ + template: 'src/datascience-ui/native-editor/index.html', + chunks: ['monaco', 'commons', 'nativeEditor'], + filename: 'index.nativeEditor.html' + }), + new HtmlWebpackPlugin({ + template: 'src/datascience-ui/history-react/index.html', + chunks: ['monaco', 'commons', 'interactiveWindow'], + filename: 'index.interactiveWindow.html' + }) + ); + break; + case 'renderers': { + const definePlugin = new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('production') + } + }); + + plugins.push(...(isProdBuild ? [definePlugin] : [])); + break; + } + case 'viewers': { + const definePlugin = new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('production') + } + }); + + plugins.push( + ...(isProdBuild ? [definePlugin] : []), + ...[ + new HtmlWebpackPlugin({ + template: 'src/datascience-ui/plot/index.html', + indexUrl: `${constants.ExtensionRootDir}/out/1`, + chunks: ['commons', 'plotViewer'], + filename: 'index.plotViewer.html' + }), + new HtmlWebpackPlugin({ + template: 'src/datascience-ui/data-explorer/index.html', + indexUrl: `${constants.ExtensionRootDir}/out/1`, + chunks: ['commons', 'dataExplorer'], + filename: 'index.dataExplorer.html' + }), + new HtmlWebpackPlugin({ + template: 'src/datascience-ui/startPage/index.html', + indexUrl: `${constants.ExtensionRootDir}/out/1`, + chunks: ['commons', 'startPage'], + filename: 'index.startPage.html' + }) + ] + ); + break; + } + default: + throw new Error(`Bundle not supported ${bundle}`); + } + + return plugins; +} + +function buildConfiguration(bundle) { + // Folder inside `datascience-ui` that will be created and where the files will be dumped. + const bundleFolder = bundle; + const filesToCopy = []; + if (bundle === 'notebook') { + // Include files only for notebooks. + filesToCopy.push( + ...[ + { + from: path.join(constants.ExtensionRootDir, 'out/ipywidgets/dist/ipywidgets.js'), + to: path.join(constants.ExtensionRootDir, 'out', 'datascience-ui', bundleFolder) + }, + { + from: path.join(constants.ExtensionRootDir, 'node_modules/font-awesome/**/*'), + to: path.join(constants.ExtensionRootDir, 'out', 'datascience-ui', bundleFolder, 'node_modules') + } + ] + ); + } + const config = { + context: constants.ExtensionRootDir, + entry: getEntry(bundle), + output: { + path: path.join(constants.ExtensionRootDir, 'out', 'datascience-ui', bundleFolder), + filename: '[name].js', + chunkFilename: `[name].bundle.js` + }, + mode: 'development', // Leave as is, we'll need to see stack traces when there are errors. + devtool: isProdBuild ? 'source-map' : 'inline-source-map', + optimization: { + minimize: isProdBuild, + minimizer: isProdBuild ? [new TerserPlugin({ sourceMap: true })] : [], + moduleIds: 'hashed', // (doesn't re-generate bundles unnecessarily) https://webpack.js.org/configuration/optimization/#optimizationmoduleids. + splitChunks: { + chunks: 'all', + cacheGroups: { + // These are bundles that will be created and loaded when page first loads. + // These must be added to the page along with the main entry point. + // Smaller they are, the faster the load in SSH. + // Interactive and native editors will share common code in commons. + commons: { + name: 'commons', + chunks: 'initial', + minChunks: bundle === 'notebook' ? 2 : 1, // We want at least one shared bundle (2 for notebooks, as we want monago split into another). + filename: '[name].initial.bundle.js' + }, + // Even though nteract has been split up, some of them are large as nteract alone is large. + // This will ensure nteract (just some of the nteract) goes into a separate bundle. + // Webpack will bundle others separately when loading them asynchronously using `await import(...)` + nteract: { + name: 'nteract', + chunks: 'all', + minChunks: 2, + test(module, _chunks) { + // `module.resource` contains the absolute path of the file on disk. + // Look for `node_modules/monaco...`. + const path = require('path'); + return ( + module.resource && + module.resource.includes(`${path.sep}node_modules${path.sep}@nteract`) + ); + } + }, + // Bundling `plotly` with nteract isn't the best option, as this plotly alone is 6mb. + // This will ensure it is in a seprate bundle, hence small files for SSH scenarios. + plotly: { + name: 'plotly', + chunks: 'all', + minChunks: 1, + test(module, _chunks) { + // `module.resource` contains the absolute path of the file on disk. + // Look for `node_modules/monaco...`. + const path = require('path'); + return ( + module.resource && module.resource.includes(`${path.sep}node_modules${path.sep}plotly`) + ); + } + }, + // Monaco is a monster. For SSH again, we pull this into a seprate bundle. + // This is only a solution for SSH. + // Ideal solution would be to dynamically load monaoc `await import`, that way it will benefit UX and SSH. + // This solution doesn't improve UX, as we still need to wait for monaco to load. + monaco: { + name: 'monaco', + chunks: 'all', + minChunks: 1, + test(module, _chunks) { + // `module.resource` contains the absolute path of the file on disk. + // Look for `node_modules/monaco...`. + const path = require('path'); + return ( + module.resource && module.resource.includes(`${path.sep}node_modules${path.sep}monaco`) + ); + } + } + } + }, + chunkIds: 'named' + }, + node: { + fs: 'empty' + }, + plugins: [ + new FixDefaultImportPlugin(), + new CopyWebpackPlugin( + [ + { from: './**/*.png', to: '.' }, + { from: './**/*.svg', to: '.' }, + { from: './**/*.css', to: '.' }, + { from: './**/*theme*.json', to: '.' }, + { + from: path.join(constants.ExtensionRootDir, 'node_modules/requirejs/require.js'), + to: path.join(constants.ExtensionRootDir, 'out', 'datascience-ui', bundleFolder) + }, + ...filesToCopy + ], + { context: 'src' } + ), + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 100 + }), + ...getPlugins(bundle) + ], + externals: ['log4js'], + resolve: { + // Add '.ts' and '.tsx' as resolvable extensions. + extensions: ['.ts', '.tsx', '.js', '.json', '.svg'] + }, + + module: { + rules: [ + { + test: /\.tsx?$/, + use: [ + { loader: 'cache-loader' }, + { + loader: 'thread-loader', + options: { + // there should be 1 cpu for the fork-ts-checker-webpack-plugin + workers: require('os').cpus().length - 1, + workerNodeArgs: ['--max-old-space-size=9096'], + poolTimeout: isProdBuild ? 1000 : Infinity // set this to Infinity in watch mode - see https://github.com/webpack-contrib/thread-loader + } + }, + { + loader: 'ts-loader', + options: { + happyPackMode: true, // IMPORTANT! use happyPackMode mode to speed-up compilation and reduce errors reported to webpack + configFile: configFileName, + // Faster (turn on only on CI, for dev we don't need this). + transpileOnly: true, + reportFiles: ['src/datascience-ui/**/*.{ts,tsx}'] + } + } + ] + }, + { + test: /\.svg$/, + use: ['svg-inline-loader'] + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'] + }, + { + test: /\.js$/, + include: /node_modules.*remark.*default.*js/, + use: [ + { + loader: path.resolve('./build/webpack/loaders/remarkLoader.js'), + options: {} + } + ] + }, + { + test: /\.json$/, + type: 'javascript/auto', + include: /node_modules.*remark.*/, + use: [ + { + loader: path.resolve('./build/webpack/loaders/jsonloader.js'), + options: {} + } + ] + }, + { + test: /\.(png|woff|woff2|eot|gif|ttf)$/, + use: [ + { + loader: 'url-loader?limit=100000', + options: { esModule: false } + } + ] + }, + { + test: /\.less$/, + use: ['style-loader', 'css-loader', 'less-loader'] + } + ] + } + }; + + if (bundle === 'renderers') { + delete config.optimization; + } + return config; +} + +exports.notebooks = buildConfiguration('notebook'); +exports.viewers = buildConfiguration('viewers'); +exports.renderers = buildConfiguration('renderers'); diff --git a/build/webpack/webpack.datascience-ui.config.js b/build/webpack/webpack.datascience-ui.config.js new file mode 100644 index 000000000000..5edb7cf8166a --- /dev/null +++ b/build/webpack/webpack.datascience-ui.config.js @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const builder = require('./webpack.datascience-ui.config.builder'); +module.exports = [builder.notebooks, builder.viewers]; diff --git a/build/webpack/webpack.extension.config.js b/build/webpack/webpack.extension.config.js new file mode 100644 index 000000000000..8e10d6a34317 --- /dev/null +++ b/build/webpack/webpack.extension.config.js @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +const copyWebpackPlugin = require('copy-webpack-plugin'); +const removeFilesWebpackPlugin = require('remove-files-webpack-plugin'); +const path = require('path'); +const tsconfig_paths_webpack_plugin = require('tsconfig-paths-webpack-plugin'); +const constants = require('../constants'); +const common = require('./common'); +// tslint:disable-next-line:no-var-requires no-require-imports +const configFileName = path.join(constants.ExtensionRootDir, 'tsconfig.extension.json'); +// Some modules will be pre-genearted and stored in out/.. dir and they'll be referenced via NormalModuleReplacementPlugin +// We need to ensure they do not get bundled into the output (as they are large). +const existingModulesInOutDir = common.getListOfExistingModulesInOutDir(); +const config = { + mode: 'production', + target: 'node', + entry: { + extension: './src/client/extension.ts' + }, + devtool: 'source-map', + node: { + __dirname: false + }, + module: { + rules: [ + { + // JupyterServices imports node-fetch. + test: /@jupyterlab[\\\/]services[\\\/].*js$/, + use: [ + { + loader: path.join(__dirname, 'loaders', 'fixNodeFetch.js') + } + ] + }, + { + test: /\.ts$/, + use: [ + { + loader: path.join(__dirname, 'loaders', 'externalizeDependencies.js') + } + ] + }, + { + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + loader: 'ts-loader' + } + ] + }, + { enforce: 'post', test: /unicode-properties[\/\\]index.js$/, loader: 'transform-loader?brfs' }, + { enforce: 'post', test: /fontkit[\/\\]index.js$/, loader: 'transform-loader?brfs' }, + { enforce: 'post', test: /linebreak[\/\\]src[\/\\]linebreaker.js/, loader: 'transform-loader?brfs' } + ] + }, + externals: ['vscode', 'commonjs', ...existingModulesInOutDir], + plugins: [ + ...common.getDefaultPlugins('extension'), + new copyWebpackPlugin([ + { + from: './node_modules/pdfkit/js/pdfkit.standalone.js', + to: './node_modules/pdfkit/js/pdfkit.standalone.js' + } + ]), + // ZMQ requires prebuilds to be in our node_modules directory. So recreate the ZMQ structure. + // However we don't webpack to manage this, so it was part of the excluded modules. Delete it from there + // so at runtime we pick up the original structure. + new removeFilesWebpackPlugin({ after: { include: ['./out/client/node_modules/zeromq.js'], log: false } }), + new copyWebpackPlugin([{ from: './node_modules/zeromq/**/*.js' }]), + new copyWebpackPlugin([{ from: './node_modules/zeromq/**/*.node' }]), + new copyWebpackPlugin([{ from: './node_modules/zeromq/**/*.json' }]), + new copyWebpackPlugin([{ from: './node_modules/node-gyp-build/**/*' }]) + ], + resolve: { + alias: { + // Pointing pdfkit to a dummy js file so webpack doesn't fall over. + // Since pdfkit has been externalized (it gets updated with the valid code by copying the pdfkit files + // into the right destination). + pdfkit: path.resolve(__dirname, 'pdfkit.js') + }, + extensions: ['.ts', '.js'], + plugins: [new tsconfig_paths_webpack_plugin.TsconfigPathsPlugin({ configFile: configFileName })] + }, + output: { + filename: '[name].js', + path: path.resolve(constants.ExtensionRootDir, 'out', 'client'), + libraryTarget: 'commonjs2', + devtoolModuleFilenameTemplate: '../../[resource-path]' + } +}; +// tslint:disable-next-line:no-default-export +exports.default = config; diff --git a/build/webpack/webpack.extension.dependencies.config.js b/build/webpack/webpack.extension.dependencies.config.js new file mode 100644 index 000000000000..3ef870b67fd3 --- /dev/null +++ b/build/webpack/webpack.extension.dependencies.config.js @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// tslint:disable-next-line: no-require-imports +const copyWebpackPlugin = require('copy-webpack-plugin'); +const path = require('path'); +const constants = require('../constants'); +const common = require('./common'); +const entryItems = {}; +common.nodeModulesToExternalize.forEach((moduleName) => { + entryItems[`node_modules/${moduleName}`] = `./node_modules/${moduleName}`; +}); +const config = { + mode: 'production', + target: 'node', + context: constants.ExtensionRootDir, + entry: entryItems, + devtool: 'source-map', + node: { + __dirname: false + }, + module: { + rules: [ + { + // JupyterServices imports node-fetch. + test: /@jupyterlab[\\\/]services[\\\/].*js$/, + use: [ + { + loader: path.join(__dirname, 'loaders', 'fixNodeFetch.js') + } + ] + }, + { enforce: 'post', test: /unicode-properties[\/\\]index.js$/, loader: 'transform-loader?brfs' }, + { enforce: 'post', test: /fontkit[\/\\]index.js$/, loader: 'transform-loader?brfs' }, + { enforce: 'post', test: /linebreak[\/\\]src[\/\\]linebreaker.js/, loader: 'transform-loader?brfs' } + ] + }, + externals: ['vscode', 'commonjs'], + plugins: [ + ...common.getDefaultPlugins('dependencies'), + // vsls requires our package.json to be next to node_modules. It's how they + // 'find' the calling extension. + new copyWebpackPlugin([{ from: './package.json', to: '.' }]), + // onigasm requires our onigasm.wasm to be in node_modules + new copyWebpackPlugin([ + { from: './node_modules/onigasm/lib/onigasm.wasm', to: './node_modules/onigasm/lib/onigasm.wasm' } + ]) + ], + resolve: { + alias: { + // Pointing pdfkit to a dummy js file so webpack doesn't fall over. + // Since pdfkit has been externalized (it gets updated with the valid code by copying the pdfkit files + // into the right destination). + pdfkit: path.resolve(__dirname, 'pdfkit.js') + }, + extensions: ['.js'] + }, + output: { + filename: '[name].js', + path: path.resolve(constants.ExtensionRootDir, 'out', 'client'), + libraryTarget: 'commonjs2', + devtoolModuleFilenameTemplate: '../../[resource-path]' + } +}; +// tslint:disable-next-line:no-default-export +exports.default = config; diff --git a/customEditor.json b/customEditor.json new file mode 100644 index 000000000000..c539fe3a3a7a --- /dev/null +++ b/customEditor.json @@ -0,0 +1,19 @@ +{ + "activationEvents": [ + "onCustomEditor:ms-python.python.notebook.ipynb" + ], + "contributes": { + "customEditors": [ + { + "viewType": "ms-python.python.notebook.ipynb", + "displayName": "Jupyter Notebook", + "selector": [ + { + "filenamePattern": "*.ipynb" + } + ], + "priority": "default" + } + ] + } +} \ No newline at end of file diff --git a/data/.vscode/settings.json b/data/.vscode/settings.json new file mode 100644 index 000000000000..99acc159fcaa --- /dev/null +++ b/data/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "/usr/bin/python3" +} diff --git a/data/test.py b/data/test.py new file mode 100644 index 000000000000..3b316dc1e8d1 --- /dev/null +++ b/data/test.py @@ -0,0 +1,2 @@ +#%% +print('hello') diff --git a/experiments.json b/experiments.json new file mode 100644 index 000000000000..2ce03b4ed5e8 --- /dev/null +++ b/experiments.json @@ -0,0 +1,182 @@ +[ + { + "name": "AlwaysDisplayTestExplorer - experiment", + "salt": "AlwaysDisplayTestExplorer", + "min": 80, + "max": 100 + }, + { + "name": "AlwaysDisplayTestExplorer - control", + "salt": "AlwaysDisplayTestExplorer", + "min": 0, + "max": 20 + }, + { + "name": "ShowPlayIcon - start", + "salt": "ShowPlayIcon", + "max": 100, + "min": 0 + }, + { + "name": "ShowExtensionSurveyPrompt - enabled", + "salt": "ShowExtensionSurveyPrompt", + "max": 100, + "min": 80 + }, + { + "name": "ShowExtensionSurveyPrompt - control", + "salt": "ShowExtensionSurveyPrompt", + "min": 0, + "max": 20 + }, + { + "name": "DebugAdapterFactory - control", + "salt": "DebugAdapterFactory", + "min": 0, + "max": 0 + }, + { + "name": "DebugAdapterFactory - experiment", + "salt": "DebugAdapterFactory", + "min": 0, + "max": 100 + }, + { + "name": "PtvsdWheels37 - control", + "salt": "DebugAdapterFactory", + "min": 0, + "max": 0 + }, + { + "name": "PtvsdWheels37 - experiment", + "salt": "DebugAdapterFactory", + "min": 0, + "max": 100 + }, + { + "name": "Reload - control", + "salt": "DebugAdapterFactory", + "min": 0, + "max": 0 + }, + { + "name": "Reload - experiment", + "salt": "DebugAdapterFactory", + "min": 0, + "max": 0 + }, + { + "name": "UseTerminalToGetActivatedEnvVars - experiment", + "salt": "UseTerminalToGetActivatedEnvVars", + "min": 0, + "max": 0 + }, + { + "name": "UseTerminalToGetActivatedEnvVars - control", + "salt": "UseTerminalToGetActivatedEnvVars", + "min": 0, + "max": 100 + }, + { + "name": "AA_testing - control", + "salt": "AA_testing", + "min": 0, + "max": 10 + }, + { + "name": "AA_testing - experiment", + "salt": "AA_testing", + "min": 90, + "max": 100 + }, + { + "name": "LocalZMQKernel - experiment", + "salt": "LocalZMQKernel", + "min": 0, + "max": 75 + }, + { + "name": "LocalZMQKernel - control", + "salt": "LocalZMQKernel", + "min": 75, + "max": 100 + }, + { + "name": "CollectLSRequestTiming - experiment", + "salt": "CollectLSRequestTiming", + "min": 0, + "max": 10 + }, + { + "name": "CollectLSRequestTiming - control", + "salt": "CollectLSRequestTiming", + "min": 10, + "max": 100 + }, + { + "name": "CollectNodeLSRequestTiming - experiment", + "salt": "CollectNodeLSRequestTiming", + "min": 0, + "max": 100 + }, + { + "name": "CollectNodeLSRequestTiming - control", + "salt": "CollectNodeLSRequestTiming", + "min": 0, + "max": 0 + }, + { + "name": "EnableIPyWidgets - experiment", + "salt": "EnableIPyWidgets", + "min": 0, + "max": 100 + }, + { + "name": "EnableIPyWidgets - control", + "salt": "EnableIPyWidgets", + "min": 0, + "max": 0 + }, + { + "name": "DeprecatePythonPath - experiment", + "salt": "DeprecatePythonPath", + "min": 0, + "max": 20 + }, + { + "name": "DeprecatePythonPath - control", + "salt": "DeprecatePythonPath", + "min": 80, + "max": 100 + }, + { + "name": "RunByLine - experiment", + "salt": "RunByLine", + "min": 0, + "max": 40 + }, + { + "name": "RunByLine - control", + "salt": "RunByLine", + "min": 40, + "max": 100 + }, + { + "name": "CustomEditorSupport - control", + "salt": "CustomEditorSupport", + "min": 0, + "max": 100 + }, + { + "name": "CustomEditorSupport - experiment", + "salt": "CustomEditorSupport", + "max": 0, + "min": 0 + }, + { + "name": "NativeNotebook - experiment", + "salt": "CustomEditorSupport", + "max": 0, + "min": 0 + } +] diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 000000000000..036eb353592f --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,1105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* jshint node: true */ +/* jshint esversion: 6 */ + +'use strict'; + +const gulp = require('gulp'); +const filter = require('gulp-filter'); +const es = require('event-stream'); +const tsfmt = require('typescript-formatter'); +const tslint = require('tslint'); +const relative = require('relative'); +const ts = require('gulp-typescript'); +const cp = require('child_process'); +const spawn = require('cross-spawn'); +const colors = require('colors/safe'); +const path = require('path'); +const del = require('del'); +const sourcemaps = require('gulp-sourcemaps'); +const fs = require('fs-extra'); +const fsExtra = require('fs-extra'); +const glob = require('glob'); +const _ = require('lodash'); +const nativeDependencyChecker = require('node-has-native-dependencies'); +const flat = require('flat'); +const argv = require('yargs').argv; +const os = require('os'); +const rmrf = require('rimraf'); + +const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; + +const noop = function () {}; +/** + * Hygiene works by creating cascading subsets of all our files and + * passing them through a sequence of checks. Here are the current subsets, + * named according to the checks performed on them. Each subset contains + * the following one, as described in mathematical notation: + * + * all ⊃ indentation ⊃ typescript + */ + +const all = ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.d.ts', 'src/**/*.js', 'src/**/*.jsx']; + +const tsFilter = ['src/**/*.ts*', '!out/**/*']; + +const indentationFilter = ['src/**/*.ts*', '!**/typings/**/*']; + +const tslintFilter = [ + 'src/**/*.ts*', + 'test/**/*.ts*', + '!**/node_modules/**', + '!out/**/*', + '!images/**/*', + '!.vscode/**/*', + '!pythonFiles/**/*', + '!resources/**/*', + '!snippets/**/*', + '!syntaxes/**/*', + '!**/typings/**/*', + '!**/*.d.ts' +]; + +gulp.task('compile', (done) => { + let failed = false; + const tsProject = ts.createProject('tsconfig.json'); + tsProject + .src() + .pipe(tsProject()) + .on('error', () => (failed = true)) + .js.pipe(gulp.dest('out')) + .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); +}); + +gulp.task('precommit', (done) => run({ exitOnError: true, mode: 'staged' }, done)); + +gulp.task('hygiene-watch', () => gulp.watch(tsFilter, gulp.series('hygiene-modified'))); + +gulp.task('hygiene', (done) => run({ mode: 'compile', skipFormatCheck: true, skipIndentationCheck: true }, done)); + +gulp.task( + 'hygiene-modified', + gulp.series('compile', (done) => run({ mode: 'changes' }, done)) +); + +gulp.task('watch', gulp.parallel('hygiene-modified', 'hygiene-watch')); + +// Duplicate to allow duplicate task in tasks.json (one ith problem matching, and one without) +gulp.task('watchProblems', gulp.parallel('hygiene-modified', 'hygiene-watch')); + +gulp.task('hygiene-watch-branch', () => gulp.watch(tsFilter, gulp.series('hygiene-branch'))); + +gulp.task('hygiene-all', (done) => run({ mode: 'all' }, done)); + +gulp.task('hygiene-branch', (done) => run({ mode: 'diffMain' }, done)); + +gulp.task('output:clean', () => del(['coverage'])); + +gulp.task('clean:cleanExceptTests', () => del(['clean:vsix', 'out/client', 'out/datascience-ui', 'out/server'])); +gulp.task('clean:vsix', () => del(['*.vsix'])); +gulp.task('clean:out', () => del(['out'])); +gulp.task('clean:ipywidgets', () => spawnAsync('npm', ['run', 'build-ipywidgets-clean'], webpackEnv)); + +gulp.task('clean', gulp.parallel('output:clean', 'clean:vsix', 'clean:out')); + +gulp.task('checkNativeDependencies', (done) => { + if (hasNativeDependencies()) { + done(new Error('Native dependencies detected')); + } + done(); +}); + +gulp.task('compile-ipywidgets', () => buildIPyWidgets()); + +const webpackEnv = { NODE_OPTIONS: '--max_old_space_size=9096' }; + +async function buildIPyWidgets() { + // if the output ipywidgest file exists, then no need to re-build. + // Barely changes. If making changes, then re-build manually. + if (!isCI && fs.existsSync(path.join(__dirname, 'out/ipywidgets/dist/ipywidgets.js'))) { + return; + } + await spawnAsync('npm', ['run', 'build-ipywidgets'], webpackEnv); +} +gulp.task('compile-notebooks', async () => { + await buildWebPackForDevOrProduction('./build/webpack/webpack.datascience-ui-notebooks.config.js'); +}); + +gulp.task('compile-renderers', async () => { + await buildWebPackForDevOrProduction('./build/webpack/webpack.datascience-ui-renderers.config.js'); +}); + +gulp.task('compile-viewers', async () => { + await buildWebPackForDevOrProduction('./build/webpack/webpack.datascience-ui-viewers.config.js'); +}); + +gulp.task('compile-webviews', gulp.series('compile-ipywidgets', 'compile-notebooks', 'compile-viewers')); + +gulp.task( + 'check-datascience-dependencies', + gulp.series( + (done) => { + if (process.env.VSC_PYTHON_FORCE_ANALYZER) { + process.env.XVSC_PYTHON_FORCE_ANALYZER = '1'; + } + process.env.VSC_PYTHON_FORCE_ANALYZER = '1'; + done(); + }, + 'compile-webviews', + () => checkDatascienceDependencies(), + (done) => { + if (!process.env.XVSC_PYTHON_FORCE_ANALYZER) { + delete process.env.VSC_PYTHON_FORCE_ANALYZER; + } + done(); + } + ) +); + +async function buildWebPackForDevOrProduction(configFile, configNameForProductionBuilds) { + if (configNameForProductionBuilds) { + await buildWebPack(configNameForProductionBuilds, ['--config', configFile], webpackEnv); + } else { + await spawnAsync('npm', ['run', 'webpack', '--', '--config', configFile, '--mode', 'production'], webpackEnv); + } +} +gulp.task('webpack', async () => { + // Build node_modules. + await buildWebPackForDevOrProduction('./build/webpack/webpack.extension.dependencies.config.js', 'production'); + // Build DS stuff (separately as it uses far too much memory and slows down CI). + // Individually is faster on CI. + await buildIPyWidgets(); + await buildWebPackForDevOrProduction('./build/webpack/webpack.datascience-ui-notebooks.config.js', 'production'); + await buildWebPackForDevOrProduction('./build/webpack/webpack.datascience-ui-viewers.config.js', 'production'); + await buildWebPackForDevOrProduction('./build/webpack/webpack.extension.config.js', 'extension'); + await buildWebPackForDevOrProduction('./build/webpack/webpack.datascience-ui-renderers.config.js', 'production'); +}); + +gulp.task('updateBuildNumber', async () => { + await updateBuildNumber(argv); +}); + +async function updateBuildNumber(args) { + if (args && args.buildNumber) { + // Edit the version number from the package.json + const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8'); + const packageJson = JSON.parse(packageJsonContents); + + // Change version number + const versionParts = packageJson['version'].split('.'); + const buildNumberPortion = + versionParts.length > 2 ? versionParts[2].replace(/(\d+)/, args.buildNumber) : args.buildNumber; + const newVersion = + versionParts.length > 1 + ? `${versionParts[0]}.${versionParts[1]}.${buildNumberPortion}` + : packageJson['version']; + packageJson['version'] = newVersion; + + // Write back to the package json + await fsExtra.writeFile('package.json', JSON.stringify(packageJson, null, 4), 'utf-8'); + + // Update the changelog.md if we are told to (this should happen on the release branch) + if (args.updateChangelog) { + const changeLogContents = await fsExtra.readFile('CHANGELOG.md', 'utf-8'); + const fixedContents = changeLogContents.replace( + /##\s*(\d+)\.(\d+)\.(\d+)\s*\(/, + `## $1.$2.${buildNumberPortion} (` + ); + + // Write back to changelog.md + await fsExtra.writeFile('CHANGELOG.md', fixedContents, 'utf-8'); + } + } else { + throw Error('buildNumber argument required for updateBuildNumber task'); + } +} + +async function buildWebPack(webpackConfigName, args, env) { + // Remember to perform a case insensitive search. + const allowedWarnings = getAllowedWarningsForWebPack(webpackConfigName).map((item) => item.toLowerCase()); + const stdOut = await spawnAsync( + 'npm', + ['run', 'webpack', '--', ...args, ...['--mode', 'production', '--devtool', 'source-map']], + env + ); + const stdOutLines = stdOut + .split(os.EOL) + .map((item) => item.trim()) + .filter((item) => item.length > 0); + // Remember to perform a case insensitive search. + const warnings = stdOutLines + .filter((item) => item.startsWith('WARNING in ')) + .filter( + (item) => + allowedWarnings.findIndex((allowedWarning) => + item.toLowerCase().startsWith(allowedWarning.toLowerCase()) + ) == -1 + ); + const errors = stdOutLines.some((item) => item.startsWith('ERROR in')); + if (errors) { + throw new Error(`Errors in ${webpackConfigName}, \n${warnings.join(', ')}\n\n${stdOut}`); + } + if (warnings.length > 0) { + throw new Error( + `Warnings in ${webpackConfigName}, Check gulpfile.js to see if the warning should be allowed., \n\n${stdOut}` + ); + } +} +function getAllowedWarningsForWebPack(buildConfig) { + switch (buildConfig) { + case 'production': + return [ + 'WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).', + 'WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.', + 'WARNING in webpack performance recommendations:', + 'WARNING in ./node_modules/vsls/vscode.js', + 'WARNING in ./node_modules/encoding/lib/iconv-loader.js', + 'WARNING in ./node_modules/ws/lib/BufferUtil.js', + 'WARNING in ./node_modules/ws/lib/buffer-util.js', + 'WARNING in ./node_modules/ws/lib/Validation.js', + 'WARNING in ./node_modules/ws/lib/validation.js', + 'WARNING in ./node_modules/@jupyterlab/services/node_modules/ws/lib/buffer-util.js', + 'WARNING in ./node_modules/@jupyterlab/services/node_modules/ws/lib/validation.js', + 'WARNING in ./node_modules/any-promise/register.js', + 'WARNING in ./node_modules/log4js/lib/appenders/index.js', + 'WARNING in ./node_modules/log4js/lib/clustering.js', + 'WARNING in ./node_modules/diagnostic-channel-publishers/dist/src/azure-coretracing.pub.js', + 'WARNING in ./node_modules/applicationinsights/out/AutoCollection/NativePerformance.js' + ]; + case 'extension': + return [ + 'WARNING in ./node_modules/encoding/lib/iconv-loader.js', + 'WARNING in ./node_modules/ws/lib/BufferUtil.js', + 'WARNING in ./node_modules/ws/lib/buffer-util.js', + 'WARNING in ./node_modules/ws/lib/Validation.js', + 'WARNING in ./node_modules/ws/lib/validation.js', + 'WARNING in ./node_modules/any-promise/register.js', + 'remove-files-plugin@1.4.0:', + 'WARNING in ./node_modules/@jupyterlab/services/node_modules/ws/lib/buffer-util.js', + 'WARNING in ./node_modules/@jupyterlab/services/node_modules/ws/lib/validation.js', + 'WARNING in ./node_modules/@jupyterlab/services/node_modules/ws/lib/Validation.js', + 'WARNING in ./node_modules/diagnostic-channel-publishers/dist/src/azure-coretracing.pub.js', + 'WARNING in ./node_modules/applicationinsights/out/AutoCollection/NativePerformance.js' + ]; + case 'debugAdapter': + return [ + 'WARNING in ./node_modules/vscode-uri/lib/index.js', + 'WARNING in ./node_modules/diagnostic-channel-publishers/dist/src/azure-coretracing.pub.js', + 'WARNING in ./node_modules/applicationinsights/out/AutoCollection/NativePerformance.js' + ]; + default: + throw new Error('Unknown WebPack Configuration'); + } +} +gulp.task('renameSourceMaps', async () => { + // By default source maps will be disabled in the extension. + // Users will need to use the command `python.enableSourceMapSupport` to enable source maps. + const extensionSourceMap = path.join(__dirname, 'out', 'client', 'extension.js.map'); + await fs.rename(extensionSourceMap, `${extensionSourceMap}.disabled`); +}); + +gulp.task('verifyBundle', async () => { + const matches = await glob.sync(path.join(__dirname, '*.vsix')); + if (!matches || matches.length == 0) { + throw new Error('Bundle does not exist'); + } else { + console.log(`Bundle ${matches[0]} exists.`); + } +}); + +gulp.task('prePublishBundle', gulp.series('webpack', 'renameSourceMaps')); +gulp.task('checkDependencies', gulp.series('checkNativeDependencies', 'check-datascience-dependencies')); +gulp.task('prePublishNonBundle', gulp.series('compile', 'compile-webviews')); + +gulp.task('installPythonRequirements', async () => { + const args = [ + '-m', + 'pip', + '--disable-pip-version-check', + 'install', + '-t', + './pythonFiles/lib/python', + '--no-cache-dir', + '--implementation', + 'py', + '--no-deps', + '--upgrade', + '-r', + './requirements.txt' + ]; + const success = await spawnAsync(process.env.CI_PYTHON_PATH || 'python3', args, undefined, true) + .then(() => true) + .catch((ex) => { + console.error("Failed to install Python Libs using 'python3'", ex); + return false; + }); + if (!success) { + console.info("Failed to install Python Libs using 'python3', attempting to install using 'python'"); + await spawnAsync('python', args).catch((ex) => + console.error("Failed to install Python Libs using 'python'", ex) + ); + } +}); + +// See https://github.com/microsoft/vscode-python/issues/7136 +gulp.task('installDebugpy', async () => { + // Install dependencies needed for 'install_debugpy.py' + const depsArgs = [ + '-m', + 'pip', + '--disable-pip-version-check', + 'install', + '-t', + './pythonFiles/lib/temp', + '-r', + './build/debugger-install-requirements.txt' + ]; + const successWithWheelsDeps = await spawnAsync(process.env.CI_PYTHON_PATH || 'python3', depsArgs, undefined, true) + .then(() => true) + .catch((ex) => { + console.error("Failed to install new DEBUGPY wheels using 'python3'", ex); + return false; + }); + if (!successWithWheelsDeps) { + console.info( + "Failed to install dependencies need by 'install_debugpy.py' using 'python3', attempting to install using 'python'" + ); + await spawnAsync('python', depsArgs).catch((ex) => + console.error("Failed to install dependencies need by 'install_debugpy.py' using 'python'", ex) + ); + } + + // Install new DEBUGPY with wheels for python 3.7 + const wheelsArgs = ['./pythonFiles/install_debugpy.py']; + const wheelsEnv = { PYTHONPATH: './pythonFiles/lib/temp' }; + const successWithWheels = await spawnAsync(process.env.CI_PYTHON_PATH || 'python3', wheelsArgs, wheelsEnv, true) + .then(() => true) + .catch((ex) => { + console.error("Failed to install new DEBUGPY wheels using 'python3'", ex); + return false; + }); + if (!successWithWheels) { + console.info("Failed to install new DEBUGPY wheels using 'python3', attempting to install using 'python'"); + await spawnAsync('python', wheelsArgs, wheelsEnv).catch((ex) => + console.error("Failed to install DEBUGPY wheels using 'python'", ex) + ); + } + + rmrf.sync('./pythonFiles/lib/temp'); +}); + +gulp.task('installPythonLibs', gulp.series('installPythonRequirements', 'installDebugpy')); + +function uploadExtension(uploadBlobName) { + const azure = require('gulp-azure-storage'); + const rename = require('gulp-rename'); + return gulp + .src('*python*.vsix') + .pipe(rename(uploadBlobName)) + .pipe( + azure.upload({ + account: process.env.AZURE_STORAGE_ACCOUNT, + key: process.env.AZURE_STORAGE_ACCESS_KEY, + container: process.env.AZURE_STORAGE_CONTAINER + }) + ); +} + +gulp.task('uploadDeveloperExtension', () => uploadExtension('ms-python-insiders.vsix')); +gulp.task('uploadReleaseExtension', () => + uploadExtension(`ms-python-${process.env.TRAVIS_BRANCH || process.env.BUILD_SOURCEBRANCHNAME}.vsix`) +); + +function spawnAsync(command, args, env, rejectOnStdErr = false) { + env = env || {}; + env = { ...process.env, ...env }; + return new Promise((resolve, reject) => { + let stdOut = ''; + console.info(`> ${command} ${args.join(' ')}`); + const proc = spawn(command, args, { cwd: __dirname, env }); + proc.stdout.on('data', (data) => { + // Log output on CI (else travis times out when there's not output). + stdOut += data.toString(); + if (isCI) { + console.log(data.toString()); + } + }); + proc.stderr.on('data', (data) => { + console.error(data.toString()); + if (rejectOnStdErr) { + reject(data.toString()); + } + }); + proc.on('close', () => resolve(stdOut)); + proc.on('error', (error) => reject(error)); + }); +} + +/** + * Analyzes the dependencies pulled in by WebPack. + * Details on the structure of the stats json file can be found here https://webpack.js.org/api/stats/ + * + * We go through the stats file and check all node modules that are part of the bundle(s). + * If they are in the bundle, they are used, hence they need to be registered in the `package.datascience-ui.dependencies.json` file. + * If not found, this will throw an error with the list of those dependencies. + * If a dependency is no longer use, this will throw an error with the details of the module to be removed from the `package.datascience-ui.dependencies.json` file. + * + */ +async function checkDatascienceDependencies() { + const existingModulesFileName = 'package.datascience-ui.dependencies.json'; + const existingModulesFile = path.join(__dirname, existingModulesFileName); + const existingModulesList = JSON.parse(await fsExtra.readFile(existingModulesFile).then((data) => data.toString())); + const existingModules = new Set(existingModulesList); + const existingModulesCopy = new Set(existingModulesList); + + const newModules = new Set(); + const packageLock = JSON.parse(await fsExtra.readFile('package-lock.json').then((data) => data.toString())); + const modulesInPackageLock = Object.keys(packageLock.dependencies); + + // Right now the script only handles two parts in the dependency name (with one '/'). + // If we have dependencies with more than one '/', then update this code. + if (modulesInPackageLock.some((dependency) => dependency.indexOf('/') !== dependency.lastIndexOf('/'))) { + throwAndLogError("Dependencies detected with more than one '/', please update this script."); + } + + /** + * Processes the output in a webpack stat file. + * + * @param {string} statFile + */ + async function processWebpackStatFile(statFile) { + /** @type{import("webpack").Stats.ToJsonOutput} */ + const json = await fsExtra.readFile(statFile).then((data) => JSON.parse(data.toString())); + json.children.forEach((child) => { + child.chunks.forEach((chunk) => { + processModules(chunk.modules); + (chunk.origins || []).forEach((origin) => processOriginOrReason(origin)); + }); + }); + json.chunks.forEach((chunk) => { + processModules(chunk.modules); + (chunk.origins || []).forEach((origin) => processOriginOrReason(origin)); + }); + } + + /** + * @param {string} name Name of module to find. + * @param {string} moduleName1 Another name of module to find. + * @param {string} moduleName2 Yet another name of module to find. + * @returns + */ + function findModule(name, moduleName1, moduleName2) { + // If the module name contains `?`, then its a webpack loader that can be ignored. + if (name.includes('loader') && (name.includes('?') || name.includes('!'))) { + return; + } + const matchedModules = modulesInPackageLock.filter( + (dependency) => dependency === moduleName2 || dependency === moduleName1 || dependency === name + ); + switch (matchedModules.length) { + case 0: + throwAndLogError( + `Dependency not found in package-lock.json, Dependency = '${name}, ${moduleName1}, ${moduleName2}'` + ); + break; + case 1: + break; + default: { + throwAndLogError(`Exact Dependency not found in package-lock.json, Dependency = '${name}'`); + } + } + + const moduleName = matchedModules[0]; + if (existingModulesCopy.has(moduleName)) { + existingModulesCopy.delete(moduleName); + } + if (existingModules.has(moduleName) || newModules.has(moduleName)) { + return; + } + newModules.add(moduleName); + } + + /** + * Processes webpack stat Modules. + * + * @param modules { Array. } + * @returns + */ + function processModules(modules) { + (modules || []).forEach(processModule); + } + + /** + * Processes a webpack stat Module. + * + * @param module { import("webpack").Stats.FnModules } + * @returns + */ + function processModule(module) { + const name = module.name; + + if (!name.includes('/node_modules')) { + processReasons(module.reasons); + processModules(module.modules); + return; + } + + let nameWithoutNodeModules = name.substring('/node_modules'.length); + // Special case expose-loader. + if (nameWithoutNodeModules.startsWith('/expose-loader')) { + nameWithoutNodeModules = nameWithoutNodeModules.substring( + nameWithoutNodeModules.indexOf('/node_modules') + '/node_modules'.length + ); + } + + let moduleName1 = nameWithoutNodeModules.split('/')[1]; + moduleName1 = moduleName1.endsWith('!.') ? moduleName1.substring(0, moduleName1.length - 2) : moduleName1; + const moduleName2 = `${nameWithoutNodeModules.split('/')[1]}/${nameWithoutNodeModules.split('/')[2]}`; + + findModule(name, moduleName1, moduleName2); + + processModules(module.modules); + processReasons(module.reasons); + } + + /** + * Processes a origin or a reason object from a webpack stat. + * + * @param {*} origin + * @returns + */ + function processOriginOrReason(origin) { + if (!origin || !origin.name) { + return; + } + const name = origin.name; + if (!name.includes('/node_modules')) { + processReasons(origin.reasons); + return; + } + + let nameWithoutNodeModules = name.substring('/node_modules'.length); + // Special case expose-loader. + if (nameWithoutNodeModules.startsWith('/expose-loader')) { + nameWithoutNodeModules = nameWithoutNodeModules.substring( + nameWithoutNodeModules.indexOf('/node_modules') + '/node_modules'.length + ); + } + + let moduleName1 = nameWithoutNodeModules.split('/')[1]; + moduleName1 = moduleName1.endsWith('!.') ? moduleName1.substring(0, moduleName1.length - 2) : moduleName1; + const moduleName2 = `${nameWithoutNodeModules.split('/')[1]}/${nameWithoutNodeModules.split('/')[2]}`; + + findModule(name, moduleName1, moduleName2); + + processReasons(origin.reasons); + } + + /** + * Processes the `reasons` property of a webpack stat module object. + * + * @param {*} reasons + */ + function processReasons(reasons) { + reasons = (reasons || []) + .map((reason) => reason.userRequest) + .filter((item) => typeof item === 'string' && !item.startsWith('.')); + reasons.forEach((item) => processOriginOrReason(item)); + } + + await processWebpackStatFile(path.join(__dirname, 'out', 'datascience-ui', 'notebook', 'notebook.stats.json')); + await processWebpackStatFile(path.join(__dirname, 'out', 'datascience-ui', 'viewers', 'viewers.stats.json')); + + const errorMessages = []; + if (newModules.size > 0) { + errorMessages.push( + `Add the untracked dependencies '${Array.from(newModules.values()).join( + ', ' + )}' to ${existingModulesFileName}` + ); + } + if (existingModulesCopy.size > 0) { + errorMessages.push( + `Remove the unused '${Array.from(existingModulesCopy.values()).join( + ', ' + )}' dependencies from ${existingModulesFileName}` + ); + } + if (errorMessages.length > 0) { + throwAndLogError(errorMessages.join('\n')); + } +} +function throwAndLogError(message) { + if (message.length > 0) { + console.error(colors.red(message)); + throw new Error(message); + } +} +function hasNativeDependencies() { + let nativeDependencies = nativeDependencyChecker.check(path.join(__dirname, 'node_modules')); + if (!Array.isArray(nativeDependencies) || nativeDependencies.length === 0) { + return false; + } + const dependencies = JSON.parse(spawn.sync('npm', ['ls', '--json', '--prod']).stdout.toString()); + const jsonProperties = Object.keys(flat.flatten(dependencies)); + nativeDependencies = _.flatMap(nativeDependencies, (item) => + path.dirname(item.substring(item.indexOf('node_modules') + 'node_modules'.length)).split(path.sep) + ) + .filter((item) => item.length > 0) + .filter((item) => !item.includes('zeromq')) // This is a known native. Allow this one for now + .filter( + (item) => + jsonProperties.findIndex((flattenedDependency) => + flattenedDependency.endsWith(`dependencies.${item}.version`) + ) >= 0 + ); + if (nativeDependencies.length > 0) { + console.error('Native dependencies detected', nativeDependencies); + return true; + } + return false; +} + +/** + * @typedef {Object} hygieneOptions - creates a new type named 'SpecialType' + * @property {'changes'|'staged'|'all'|'compile'|'diffMain'} [mode=] - Mode. + * @property {boolean=} skipIndentationCheck - Skip indentation checks. + * @property {boolean=} skipFormatCheck - Skip format checks. + * @property {boolean=} skipLinter - Skip linter. + */ + +/** + * + * @param {hygieneOptions} options + */ +function getTsProject(options) { + return ts.createProject('tsconfig.json'); +} + +let configuration; +/** + * + * @param {hygieneOptions} options + */ +function getLinter(options) { + configuration = configuration ? configuration : tslint.Configuration.findConfiguration(null, '.'); + const program = tslint.Linter.createProgram('./tsconfig.json'); + const linter = new tslint.Linter({ formatter: 'json' }, program); + return { linter, configuration }; +} +let compilationInProgress = false; +let reRunCompilation = false; +/** + * + * @param {hygieneOptions} options + * @returns {NodeJS.ReadWriteStream} + */ +const hygiene = (options, done) => { + done = done || noop; + if (compilationInProgress) { + reRunCompilation = true; + return done(); + } + const fileListToProcess = options.mode === 'compile' ? undefined : getFileListToProcess(options); + if ( + Array.isArray(fileListToProcess) && + fileListToProcess !== all && + fileListToProcess.filter((item) => item.endsWith('.ts')).length === 0 + ) { + return done(); + } + + const started = new Date().getTime(); + compilationInProgress = true; + options = options || {}; + let errorCount = 0; + + const indentation = es.through(function (file) { + file.contents + .toString('utf8') + .split(/\r\n|\r|\n/) + .forEach((line, i) => { + if (/^\s*$/.test(line) || /^\S+.*$/.test(line)) { + // Empty or whitespace lines are OK. + } else if (/^(\s\s\s\s)+.*/.test(line)) { + // Good indent. + } else if (/^[\t]+.*/.test(line)) { + console.error( + file.relative + + '(' + + (i + 1) + + ',1): Bad whitespace indentation (use 4 spaces instead of tabs or other)' + ); + errorCount++; + } + }); + + this.emit('data', file); + }); + + const formatOptions = { verify: true, tsconfig: true, tslint: true, editorconfig: true, tsfmt: true }; + const formatting = es.map(function (file, cb) { + tsfmt + .processString(file.path, file.contents.toString('utf8'), formatOptions) + .then((result) => { + if (result.error) { + let message = result.message.trim(); + let formattedMessage = ''; + if (message.startsWith(__dirname)) { + message = message.substr(__dirname.length); + message = message.startsWith(path.sep) ? message.substr(1) : message; + const index = message.indexOf('.ts '); + if (index === -1) { + formattedMessage = colors.red(message); + } else { + const file = message.substr(0, index + 3); + const errorMessage = message.substr(index + 4).trim(); + formattedMessage = `${colors.red(file)} ${errorMessage}`; + } + } else { + formattedMessage = colors.red(message); + } + console.error(formattedMessage); + errorCount++; + } + cb(null, file); + }) + .catch(cb); + }); + + let reportedLinterFailures = []; + /** + * Report the linter failures + * @param {any[]} failures + */ + function reportLinterFailures(failures) { + return ( + failures + .map((failure) => { + const name = failure.name || failure.fileName; + const position = failure.startPosition; + const line = position.lineAndCharacter ? position.lineAndCharacter.line : position.line; + const character = position.lineAndCharacter + ? position.lineAndCharacter.character + : position.character; + + // Output in format similar to tslint for the linter to pickup. + const message = `ERROR: (${failure.ruleName}) ${relative(__dirname, name)}[${line + 1}, ${ + character + 1 + }]: ${failure.failure}`; + if (reportedLinterFailures.indexOf(message) === -1) { + console.error(message); + reportedLinterFailures.push(message); + return true; + } else { + return false; + } + }) + .filter((reported) => reported === true).length > 0 + ); + } + + const { linter, configuration } = getLinter(options); + const tsl = es.through(function (file) { + const contents = file.contents.toString('utf8'); + if (isCI) { + // Don't print anything to the console, we'll do that. + console.log('.'); + } + // Yes this is a hack, but tslinter doesn't provide an option to prevent this. + const oldWarn = console.warn; + console.warn = () => {}; + linter.failures = []; + linter.fixes = []; + linter.lint(file.relative, contents, configuration.results); + console.warn = oldWarn; + const result = linter.getResult(); + if (result.failureCount > 0 || result.errorCount > 0) { + const reported = reportLinterFailures(result.failures); + if (result.failureCount && reported) { + errorCount += result.failureCount; + } + if (result.errorCount && reported) { + errorCount += result.errorCount; + } + } + this.emit('data', file); + }); + + const tsFiles = []; + const tscFilesTracker = es.through(function (file) { + tsFiles.push(file.path.replace(/\\/g, '/')); + tsFiles.push(file.path); + this.emit('data', file); + }); + + const tsProject = getTsProject(options); + + const tsc = function () { + function customReporter() { + return { + error: function (error, typescript) { + const fullFilename = error.fullFilename || ''; + const relativeFilename = error.relativeFilename || ''; + if (tsFiles.findIndex((file) => fullFilename === file || relativeFilename === file) === -1) { + return; + } + console.error(`Error: ${error.message}`); + errorCount += 1; + }, + finish: function () { + // forget the summary. + console.log('Finished compilation'); + } + }; + } + const reporter = customReporter(); + return tsProject(reporter); + }; + + const files = options.mode === 'compile' ? tsProject.src() : getFilesToProcess(fileListToProcess); + const dest = options.mode === 'compile' ? './out' : '.'; + let result = files.pipe(filter((f) => f && f.stat && !f.stat.isDirectory())); + + if (!options.skipIndentationCheck) { + result = result.pipe(filter(indentationFilter)).pipe(indentation); + } + + result = result.pipe(filter(tslintFilter)); + + if (!options.skipFormatCheck) { + // result = result + // .pipe(formatting); + } + + if (!options.skipLinter) { + result = result.pipe(tsl); + } + let totalTime = 0; + result = result + .pipe(tscFilesTracker) + .pipe(sourcemaps.init()) + .pipe(tsc()) + .pipe( + sourcemaps.mapSources(function (sourcePath, file) { + let tsFileName = path.basename(file.path).replace(/js$/, 'ts'); + const qualifiedSourcePath = path.dirname(file.path).replace('out/', 'src/').replace('out\\', 'src\\'); + if (!fs.existsSync(path.join(qualifiedSourcePath, tsFileName))) { + const tsxFileName = path.basename(file.path).replace(/js$/, 'tsx'); + if (!fs.existsSync(path.join(qualifiedSourcePath, tsxFileName))) { + console.error(`ERROR: (source-maps) ${file.path}[1,1]: Source file not found`); + } else { + tsFileName = tsxFileName; + } + } + return path.join(path.relative(path.dirname(file.path), qualifiedSourcePath), tsFileName); + }) + ) + .pipe(sourcemaps.write('.', { includeContent: false })) + .pipe(gulp.dest(dest)) + .pipe( + es.through(null, function () { + if (errorCount > 0) { + const errorMessage = `Hygiene failed with errors 👎 . Check 'gulpfile.js' (completed in ${ + new Date().getTime() - started + }ms).`; + console.error(colors.red(errorMessage)); + exitHandler(options); + } else { + console.log( + colors.green( + `Hygiene passed with 0 errors 👍 (completed in ${new Date().getTime() - started}ms).` + ) + ); + } + // Reset error counter. + errorCount = 0; + reportedLinterFailures = []; + compilationInProgress = false; + if (reRunCompilation) { + reRunCompilation = false; + setTimeout(() => { + hygiene(options, done); + }, 10); + } + done(); + this.emit('end'); + }) + ) + .on('error', (ex) => { + exitHandler(options, ex); + done(); + }); + + return result; +}; + +/** + * @typedef {Object} runOptions + * @property {boolean=} exitOnError - Exit on error. + * @property {'changes'|'staged'|'all'} [mode=] - Mode. + * @property {string[]=} files - Optional list of files to be modified. + * @property {boolean=} skipIndentationCheck - Skip indentation checks. + * @property {boolean=} skipFormatCheck - Skip format checks. + * @property {boolean=} skipLinter - Skip linter. + * @property {boolean=} watch - Watch mode. + */ + +/** + * Run the linters. + * @param {runOptions} options + * @param {Error} ex + */ +function exitHandler(options, ex) { + console.error(); + if (ex) { + console.error(ex); + console.error(colors.red(ex)); + } + if (options.exitOnError) { + console.log('exit'); + process.exit(1); + } +} + +/** + * Run the linters. + * @param {runOptions} options + */ +function run(options, done) { + done = done || noop; + options = options ? options : {}; + options.exitOnError = typeof options.exitOnError === 'undefined' ? isCI : options.exitOnError; + process.once('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + exitHandler(options); + }); + + // Clear screen each time + console.log('\x1Bc'); + const startMessage = `Hygiene starting`; + console.log(colors.blue(startMessage)); + + hygiene(options, done); +} + +function git(args) { + let result = cp.spawnSync('git', args, { encoding: 'utf-8' }); + return result.output.join('\n'); +} + +function getStagedFilesSync() { + const out = git(['diff', '--cached', '--name-only']); + return out.split(/\r?\n/).filter((l) => !!l); +} +function getAddedFilesSync() { + const out = git(['status', '-u', '-s']); + return out + .split(/\r?\n/) + .filter((l) => !!l) + .filter((l) => _.intersection(['A', '?', 'U'], l.substring(0, 2).trim().split('')).length > 0) + .map((l) => path.join(__dirname, l.substring(2).trim())); +} +function getAzureDevOpsVarValue(varName) { + return process.env[varName.replace(/\./g, '_').toUpperCase()]; +} +function getModifiedFilesSync() { + if (isCI) { + const isAzurePR = getAzureDevOpsVarValue('System.PullRequest.SourceBranch') !== undefined; + const isTravisPR = process.env.TRAVIS_PULL_REQUEST !== undefined && process.env.TRAVIS_PULL_REQUEST !== 'true'; + if (!isAzurePR && !isTravisPR) { + return []; + } + const targetBranch = process.env.TRAVIS_BRANCH || getAzureDevOpsVarValue('System.PullRequest.TargetBranch'); + if (targetBranch !== 'main') { + return []; + } + + const repo = process.env.TRAVIS_REPO_SLUG || getAzureDevOpsVarValue('Build.Repository.Name'); + const originOrUpstream = + repo.toUpperCase() === 'MICROSOFT/VSCODE-PYTHON' || + repo.toUpperCase() === 'VSCODE-PYTHON-DATASCIENCE/VSCODE-PYTHON' + ? 'origin' + : 'upstream'; + + // If on CI, get a list of modified files comparing against + // PR branch and main of current (assumed 'origin') repo. + try { + cp.execSync(`git remote set-branches --add ${originOrUpstream} main`, { + encoding: 'utf8', + cwd: __dirname + }); + cp.execSync('git fetch', { encoding: 'utf8', cwd: __dirname }); + } catch (ex) { + return []; + } + const cmd = `git diff --name-only HEAD ${originOrUpstream}/main`; + console.info(cmd); + const out = cp.execSync(cmd, { encoding: 'utf8', cwd: __dirname }); + return out + .split(/\r?\n/) + .filter((l) => !!l) + .filter((l) => l.length > 0) + .map((l) => l.trim().replace(/\//g, path.sep)) + .map((l) => path.join(__dirname, l)); + } else { + const out = cp.execSync('git status -u -s', { encoding: 'utf8' }); + return out + .split(/\r?\n/) + .filter((l) => !!l) + .filter( + (l) => _.intersection(['M', 'A', 'R', 'C', 'U', '?'], l.substring(0, 2).trim().split('')).length > 0 + ) + .map((l) => path.join(__dirname, l.substring(2).trim().replace(/\//g, path.sep))); + } +} + +function getDifferentFromMainFilesSync() { + const out = git(['diff', '--name-status', 'main']); + return out + .split(/\r?\n/) + .filter((l) => !!l) + .map((l) => path.join(__dirname, l.substring(2).trim())); +} + +/** + * @param {hygieneOptions} options + */ +function getFilesToProcess(fileList) { + const gulpSrcOptions = { base: '.' }; + return gulp.src(fileList, gulpSrcOptions); +} + +/** + * @param {hygieneOptions} options + */ +function getFileListToProcess(options) { + const mode = options ? options.mode : 'all'; + const gulpSrcOptions = { base: '.' }; + + // If we need only modified files, then filter the glob. + if (options && options.mode === 'changes') { + return getModifiedFilesSync().filter((f) => fs.existsSync(f)); + } + + if (options && options.mode === 'staged') { + return getStagedFilesSync().filter((f) => fs.existsSync(f)); + } + + if (options && options.mode === 'diffMain') { + return getDifferentFromMainFilesSync().filter((f) => fs.existsSync(f)); + } + + return all; +} + +exports.hygiene = hygiene; + +// this allows us to run hygiene via CLI (e.g. `node gulfile.js`). +if (require.main === module) { + run({ exitOnError: true, mode: 'staged' }, () => {}); +} diff --git a/icon.png b/icon.png new file mode 100644 index 000000000000..5ae724858d3b Binary files /dev/null and b/icon.png differ diff --git a/images/ConfigureDebugger.gif b/images/ConfigureDebugger.gif new file mode 100644 index 000000000000..359e1a7493fd Binary files /dev/null and b/images/ConfigureDebugger.gif differ diff --git a/images/ConfigureTests.gif b/images/ConfigureTests.gif new file mode 100644 index 000000000000..6da0100d593b Binary files /dev/null and b/images/ConfigureTests.gif differ diff --git a/images/InterpreterSelectionZoom.gif b/images/InterpreterSelectionZoom.gif new file mode 100644 index 000000000000..576a438a7d3b Binary files /dev/null and b/images/InterpreterSelectionZoom.gif differ diff --git a/images/JavascriptProfiler.png b/images/JavascriptProfiler.png new file mode 100644 index 000000000000..f26e1480c021 Binary files /dev/null and b/images/JavascriptProfiler.png differ diff --git a/images/OpenOrCreateNotebook.gif b/images/OpenOrCreateNotebook.gif new file mode 100644 index 000000000000..98256a97f190 Binary files /dev/null and b/images/OpenOrCreateNotebook.gif differ diff --git a/images/break.gif b/images/break.gif deleted file mode 100644 index a918ea0cd06e..000000000000 Binary files a/images/break.gif and /dev/null differ diff --git a/images/dataviewer.gif b/images/dataviewer.gif new file mode 100644 index 000000000000..b2f5e080a401 Binary files /dev/null and b/images/dataviewer.gif differ diff --git a/images/debugDemo.gif b/images/debugDemo.gif new file mode 100644 index 000000000000..48d7fed55691 Binary files /dev/null and b/images/debugDemo.gif differ diff --git a/images/flaskDebugging.gif b/images/flaskDebugging.gif deleted file mode 100644 index bfe120a9da80..000000000000 Binary files a/images/flaskDebugging.gif and /dev/null differ diff --git a/images/general.gif b/images/general.gif index d3e770bb97cf..50c95613d68d 100644 Binary files a/images/general.gif and b/images/general.gif differ diff --git a/images/goToDef.gif b/images/goToDef.gif deleted file mode 100644 index c9ed1d359195..000000000000 Binary files a/images/goToDef.gif and /dev/null differ diff --git a/images/icon.svg b/images/icon.svg deleted file mode 100644 index 6c6619d5b76f..000000000000 --- a/images/icon.svg +++ /dev/null @@ -1 +0,0 @@ -python-logo-generic \ No newline at end of file diff --git a/images/interactive.gif b/images/interactive.gif new file mode 100644 index 000000000000..f8080fa94576 Binary files /dev/null and b/images/interactive.gif differ diff --git a/images/kernelchange.gif b/images/kernelchange.gif new file mode 100644 index 000000000000..a414e2252efa Binary files /dev/null and b/images/kernelchange.gif differ diff --git a/images/plotviewer.gif b/images/plotviewer.gif new file mode 100644 index 000000000000..d1f6524f3769 Binary files /dev/null and b/images/plotviewer.gif differ diff --git a/images/remoteserver.gif b/images/remoteserver.gif new file mode 100644 index 000000000000..be836e673b72 Binary files /dev/null and b/images/remoteserver.gif differ diff --git a/images/rename.gif b/images/rename.gif deleted file mode 100644 index 7df50c520fd4..000000000000 Binary files a/images/rename.gif and /dev/null differ diff --git a/images/runbyline.gif b/images/runbyline.gif new file mode 100644 index 000000000000..1c0679f9a458 Binary files /dev/null and b/images/runbyline.gif differ diff --git a/images/savetopythonfile.png b/images/savetopythonfile.png new file mode 100644 index 000000000000..ccb0a7bfa387 Binary files /dev/null and b/images/savetopythonfile.png differ diff --git a/images/standardDebugging.gif b/images/standardDebugging.gif deleted file mode 100644 index 4053d6e811d4..000000000000 Binary files a/images/standardDebugging.gif and /dev/null differ diff --git a/images/unittest.gif b/images/unittest.gif new file mode 100644 index 000000000000..1511bccb7392 Binary files /dev/null and b/images/unittest.gif differ diff --git a/images/variableexplorer.png b/images/variableexplorer.png new file mode 100644 index 000000000000..0f6c9f73b4de Binary files /dev/null and b/images/variableexplorer.png differ diff --git a/languages/pip-requirements.json b/languages/pip-requirements.json new file mode 100644 index 000000000000..746aa3ac3e2e --- /dev/null +++ b/languages/pip-requirements.json @@ -0,0 +1,5 @@ +{ + "comments": { + "lineComment": "#" + } +} diff --git a/news/.vscode/settings.json b/news/.vscode/settings.json new file mode 100644 index 000000000000..2b759d72bd35 --- /dev/null +++ b/news/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "python.languageServer": "Microsoft", + "python.formatting.provider": "black", + "editor.formatOnSave": true, + "python.testing.pytestArgs": ["."], + "python.testing.unittestEnabled": false, + "python.testing.nosetestsEnabled": false, + "python.testing.pytestEnabled": true +} diff --git a/news/1 Enhancements/12932.md b/news/1 Enhancements/12932.md new file mode 100644 index 000000000000..5075b7151f76 --- /dev/null +++ b/news/1 Enhancements/12932.md @@ -0,0 +1 @@ +Upgraded isort to `5.3.2`. diff --git a/news/1 Enhancements/13061.md b/news/1 Enhancements/13061.md new file mode 100644 index 000000000000..6b65b12e6243 --- /dev/null +++ b/news/1 Enhancements/13061.md @@ -0,0 +1,2 @@ +Remove default "--no-reload" from debug configurations. +(thanks [ian910297](https://github.com/ian910297)) diff --git a/news/1 Enhancements/13716.md b/news/1 Enhancements/13716.md new file mode 100644 index 000000000000..9289e62b8c4a --- /dev/null +++ b/news/1 Enhancements/13716.md @@ -0,0 +1 @@ +Show a general warning prompt pointing to the upgrade guide when users attempt to run isort5 using deprecated settings. diff --git a/news/1 Enhancements/5578.md b/news/1 Enhancements/5578.md new file mode 100644 index 000000000000..7fc44ac63861 --- /dev/null +++ b/news/1 Enhancements/5578.md @@ -0,0 +1 @@ +Docstrings are added to `class` and `def` snippets (thanks [alannt777](https://github.com/alannt777/)) \ No newline at end of file diff --git a/news/1 Enhancements/README.md b/news/1 Enhancements/README.md new file mode 100644 index 000000000000..2a159b65f4f0 --- /dev/null +++ b/news/1 Enhancements/README.md @@ -0,0 +1,2 @@ +Changes that add new features. + diff --git a/news/2 Fixes/10270.md b/news/2 Fixes/10270.md new file mode 100644 index 000000000000..b90f01f1e949 --- /dev/null +++ b/news/2 Fixes/10270.md @@ -0,0 +1 @@ +Fixed the output being trimmed. Tables that start with empty space will now display correctly. diff --git a/news/2 Fixes/11729.md b/news/2 Fixes/11729.md new file mode 100644 index 000000000000..912cb3af7ab5 --- /dev/null +++ b/news/2 Fixes/11729.md @@ -0,0 +1,3 @@ +#11729 +Prevent test discovery from picking up stdout from low level file descriptors +(thanks [Ryo Miyajima](https://github.com/sergeant-wizard)) diff --git a/news/2 Fixes/12245.md b/news/2 Fixes/12245.md new file mode 100644 index 000000000000..e493d890bb56 --- /dev/null +++ b/news/2 Fixes/12245.md @@ -0,0 +1 @@ +Fix opening new blank notebooks when using the VS code custom editor API. \ No newline at end of file diff --git a/news/2 Fixes/12760.md b/news/2 Fixes/12760.md new file mode 100644 index 000000000000..f47a84a5c3fb --- /dev/null +++ b/news/2 Fixes/12760.md @@ -0,0 +1 @@ +Support starting kernels with the same directory as the notebook. \ No newline at end of file diff --git a/news/2 Fixes/12949.md b/news/2 Fixes/12949.md new file mode 100644 index 000000000000..0e16948f227f --- /dev/null +++ b/news/2 Fixes/12949.md @@ -0,0 +1 @@ +Fixed `Sort imports` command with setuptools version `49.2`. diff --git a/news/2 Fixes/12962.md b/news/2 Fixes/12962.md new file mode 100644 index 000000000000..c94aeff6e07f --- /dev/null +++ b/news/2 Fixes/12962.md @@ -0,0 +1 @@ +Do not fail interpreter discovery if accessing Windows registry fails. diff --git a/news/2 Fixes/13258.md b/news/2 Fixes/13258.md new file mode 100644 index 000000000000..f0dafc1af18c --- /dev/null +++ b/news/2 Fixes/13258.md @@ -0,0 +1 @@ +Prevent daemon from trying to prewarm an execution service. \ No newline at end of file diff --git a/news/2 Fixes/13338.md b/news/2 Fixes/13338.md new file mode 100644 index 000000000000..052f45ac03ad --- /dev/null +++ b/news/2 Fixes/13338.md @@ -0,0 +1 @@ +Respect stop on error setting for executing cells in native notebook. \ No newline at end of file diff --git a/news/2 Fixes/13409.md b/news/2 Fixes/13409.md new file mode 100644 index 000000000000..fc7d478c9e4b --- /dev/null +++ b/news/2 Fixes/13409.md @@ -0,0 +1 @@ +Native notebook launch doesn't hang if the kernel does not start, and notifies the user of the failure. Also does not show the first cell as executing until the kernel is actually started and connected. \ No newline at end of file diff --git a/news/2 Fixes/13493.md b/news/2 Fixes/13493.md new file mode 100644 index 000000000000..c7c567cfed4a --- /dev/null +++ b/news/2 Fixes/13493.md @@ -0,0 +1 @@ +Fix path to isolated script on Windows shell_exec. diff --git a/news/2 Fixes/13509.md b/news/2 Fixes/13509.md new file mode 100644 index 000000000000..62188ee43dba --- /dev/null +++ b/news/2 Fixes/13509.md @@ -0,0 +1 @@ +Updating other cells with display.update does not work in native notebooks. \ No newline at end of file diff --git a/news/2 Fixes/13520.md b/news/2 Fixes/13520.md new file mode 100644 index 000000000000..6ee5043c3184 --- /dev/null +++ b/news/2 Fixes/13520.md @@ -0,0 +1 @@ +Fix for notebook using the first kernel every time. It will now use the language in the notebook to determine the most appropriate kernel. \ No newline at end of file diff --git a/news/2 Fixes/13553.md b/news/2 Fixes/13553.md new file mode 100644 index 000000000000..05434a9f1c16 --- /dev/null +++ b/news/2 Fixes/13553.md @@ -0,0 +1 @@ +Shift+enter should execute current cell and select the next cell. diff --git a/news/2 Fixes/13612.md b/news/2 Fixes/13612.md new file mode 100644 index 000000000000..c74758c73759 --- /dev/null +++ b/news/2 Fixes/13612.md @@ -0,0 +1,2 @@ +Fixes typo in export command registration. +(thanks [Anton Kosyakov](https://github.com/akosyakov/)) diff --git a/news/2 Fixes/13706.md b/news/2 Fixes/13706.md new file mode 100644 index 000000000000..fb3074fa7820 --- /dev/null +++ b/news/2 Fixes/13706.md @@ -0,0 +1 @@ +Fix the behavior of the 'python.showStartPage' setting. diff --git a/news/2 Fixes/README.md b/news/2 Fixes/README.md new file mode 100644 index 000000000000..cc5e1020961d --- /dev/null +++ b/news/2 Fixes/README.md @@ -0,0 +1 @@ +Changes that fix broken behaviour. diff --git a/news/3 Code Health/13103.md b/news/3 Code Health/13103.md new file mode 100644 index 000000000000..a22bfbd98e3c --- /dev/null +++ b/news/3 Code Health/13103.md @@ -0,0 +1 @@ +Fix bandit issues in vscode_datascience_helpers. \ No newline at end of file diff --git a/news/3 Code Health/13411.md b/news/3 Code Health/13411.md new file mode 100644 index 000000000000..5cce02971318 --- /dev/null +++ b/news/3 Code Health/13411.md @@ -0,0 +1 @@ +Cast type to `any` to get around issues with `ts-node` (`ts-node` is used by `nyc` for code coverage). diff --git a/news/3 Code Health/13459.md b/news/3 Code Health/13459.md new file mode 100644 index 000000000000..b1a4e4d8d88d --- /dev/null +++ b/news/3 Code Health/13459.md @@ -0,0 +1 @@ +Drop support for Python 3.5 (it reaches end-of-life on September 13, 2020 and isort 5 does not support it). diff --git a/news/3 Code Health/13501.md b/news/3 Code Health/13501.md new file mode 100644 index 000000000000..440c4d45789b --- /dev/null +++ b/news/3 Code Health/13501.md @@ -0,0 +1 @@ +Fix nightly flake test issue with timeout waiting for kernel. \ No newline at end of file diff --git a/news/3 Code Health/13542.md b/news/3 Code Health/13542.md new file mode 100644 index 000000000000..33b462f5b54e --- /dev/null +++ b/news/3 Code Health/13542.md @@ -0,0 +1 @@ +Disable sorting tests for Python 2.7 as isort5 is not compatible with Python 2.7. diff --git a/news/3 Code Health/13605.md b/news/3 Code Health/13605.md new file mode 100644 index 000000000000..248a8f7d61fe --- /dev/null +++ b/news/3 Code Health/13605.md @@ -0,0 +1 @@ +Fix nightly flake test current directory failing test. \ No newline at end of file diff --git a/news/3 Code Health/13645.md b/news/3 Code Health/13645.md new file mode 100644 index 000000000000..c7c4adb9a668 --- /dev/null +++ b/news/3 Code Health/13645.md @@ -0,0 +1 @@ +Rename the `master` branch to `main`. diff --git a/news/3 Code Health/13647.md b/news/3 Code Health/13647.md new file mode 100644 index 000000000000..9b86400b03c4 --- /dev/null +++ b/news/3 Code Health/13647.md @@ -0,0 +1 @@ +Remove usage of the terms "blacklist" and "whitelist". diff --git a/news/3 Code Health/13726.md b/news/3 Code Health/13726.md new file mode 100644 index 000000000000..f7180bab124c --- /dev/null +++ b/news/3 Code Health/13726.md @@ -0,0 +1 @@ +Fix a test failure and warning when running test adapter tests under pytest 5 diff --git a/news/3 Code Health/13729.md b/news/3 Code Health/13729.md new file mode 100644 index 000000000000..a27cd06e3784 --- /dev/null +++ b/news/3 Code Health/13729.md @@ -0,0 +1 @@ +Remove unused imports from data science ipython test files. \ No newline at end of file diff --git a/news/3 Code Health/13734.md b/news/3 Code Health/13734.md new file mode 100644 index 000000000000..40e0edd43fe5 --- /dev/null +++ b/news/3 Code Health/13734.md @@ -0,0 +1 @@ +Fix nighly failure with beakerx. \ No newline at end of file diff --git a/news/3 Code Health/README.md b/news/3 Code Health/README.md new file mode 100644 index 000000000000..10619f41f3a4 --- /dev/null +++ b/news/3 Code Health/README.md @@ -0,0 +1 @@ +Changes that should not be user-facing. diff --git a/news/README.md b/news/README.md new file mode 100644 index 000000000000..f26d25030fab --- /dev/null +++ b/news/README.md @@ -0,0 +1,62 @@ +# News + +Our changelog is automatically generated from individual news entry files. +This alleviates the burden of having to go back and try to figure out +what changed in a release. It also helps tie pull requests back to the +issue(s) it addresses. Finally, it avoids merge conflicts between pull requests +which would occur if multiple pull requests tried to edit the changelog. + +If a change does not warrant a news entry, the `skip news` label can be added +to a pull request to signal this fact. + +## Entries + +Each news entry is represented by a Markdown file that contains the +relevant details of what changed. The file name of the news entry is +the issue that corresponds to the change along with an optional nonce in +case a single issue corresponds to multiple changes. The directory +the news entry is saved in specifies what section of the changelog the +change corresponds to. External contributors should also make sure to +thank themselves for taking the time and effort to contribute. + +As an example, a change corresponding to a bug reported in issue #42 +would be saved in the `1 Fixes` directory and named `42.md` +(or `42-nonce_value.md` if there was a need for multiple entries +regarding issue #42) and could contain the following: + +```markdown +[Answer]() +to the Ultimate Question of Life, the Universe, and Everything! +(thanks [Don Jaymanne](https://github.com/donjayamanne/)) +``` + +This would then be made into an entry in the changelog that was in the +`Fixes` section, contained the details as found in the file, and tied +to issue #42. + +## Generating the changelog + +The `announce` script can do 3 possible things: + +1. Validate that the changelog _could_ be successfully generated +2. Generate the changelog entries +3. Generate the changelog entries **and** `git-rm` the news entry files + +The first option is used in CI to make sure any added news entries +will not cause trouble at release time. The second option is for +filling in the changelog for interim releases, e.g. a beta release. +The third option is for final releases that get published to the +[VS Code marketplace](https://marketplace.visualstudio.com/VSCode). + +For options 2 & 3, the changelog is sent to stdout so it can be temporarily +saved to a file: + +```sh +python3 news > entry.txt +``` + +It can also be redirected to an editor buffer, e.g.: + +```sh +python3 news | code-insiders - +``` diff --git a/news/__main__.py b/news/__main__.py new file mode 100644 index 000000000000..b496ec1d0c8c --- /dev/null +++ b/news/__main__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import runpy + +runpy.run_module('announce', run_name='__main__', alter_sys=True) diff --git a/news/announce.py b/news/announce.py new file mode 100644 index 000000000000..d4d7dd1cfd66 --- /dev/null +++ b/news/announce.py @@ -0,0 +1,193 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Generate the changelog. + +Usage: announce [--dry_run | --interim | --final] [--update=] [] + +""" +import dataclasses +import datetime +import enum +import json +import operator +import os +import pathlib +import re +import subprocess +import sys + +import docopt + + +FILENAME_RE = re.compile(r"(?P\d+)(?P-\S+)?\.md") + + +@dataclasses.dataclass +class NewsEntry: + """Representation of a news entry.""" + + issue_number: int + description: str + path: pathlib.Path + + +def news_entries(directory): + """Yield news entries in the directory. + + Entries are sorted by issue number. + + """ + entries = [] + for path in directory.iterdir(): + if path.name == "README.md": + continue + match = FILENAME_RE.match(path.name) + if match is None: + raise ValueError(f"{path} has a bad file name") + issue = int(match.group("issue")) + try: + entry = path.read_text("utf-8") + except UnicodeDecodeError as exc: + raise ValueError(f"'{path}' is not encoded as UTF-8") from exc + if "\ufeff" in entry: + raise ValueError(f"'{path}' contains the BOM") + entries.append(NewsEntry(issue, entry, path)) + entries.sort(key=operator.attrgetter("issue_number")) + yield from entries + + +@dataclasses.dataclass +class SectionTitle: + """Create a data object for a section of the changelog.""" + + index: int + title: str + path: pathlib.Path + + +def sections(directory): + """Yield the sections in their appropriate order.""" + found = [] + for path in directory.iterdir(): + if not path.is_dir() or path.name.startswith((".", "_")): + continue + position, sep, title = path.name.partition(" ") + if not sep: + print( + f"directory {path.name!r} is missing a ranking; skipping", + file=sys.stderr, + ) + continue + found.append(SectionTitle(int(position), title, path)) + return sorted(found, key=operator.attrgetter("index")) + + +def gather(directory): + """Gather all the entries together.""" + data = [] + for section in sections(directory): + data.append((section, list(news_entries(section.path)))) + return data + + +def entry_markdown(entry): + """Generate the Markdown for the specified entry.""" + enumerated_item = "1. " + indent = " " * len(enumerated_item) + issue_url = ( + f"https://github.com/Microsoft/vscode-python/issues/{entry.issue_number}" + ) + issue_md = f"([#{entry.issue_number}]({issue_url}))" + entry_lines = entry.description.strip().splitlines() + formatted_lines = [f"{enumerated_item}{entry_lines[0]}"] + formatted_lines.extend(f"{indent}{line}" for line in entry_lines[1:]) + formatted_lines.append(f"{indent}{issue_md}") + return "\n".join(formatted_lines) + + +def changelog_markdown(data): + """Generate the Markdown for the release.""" + changelog = [] + for section, entries in data: + changelog.append(f"### {section.title}") + changelog.append("") + changelog.extend(map(entry_markdown, entries)) + changelog.append("") + return "\n".join(changelog) + + +def git_rm(path): + """Run git-rm on the path.""" + status = subprocess.run( + ["git", "rm", os.fspath(path.resolve())], + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + try: + status.check_returncode() + except Exception: + print(status.stdout, file=sys.stderr) + raise + + +def cleanup(data): + """Remove news entries from git and disk.""" + for section, entries in data: + for entry in entries: + git_rm(entry.path) + + +class RunType(enum.Enum): + """Possible run-time options.""" + + dry_run = 0 + interim = 1 + final = 2 + + +def complete_news(version, entry, previous_news): + """Prepend a news entry to the previous news file.""" + title, _, previous_news = previous_news.partition("\n") + title = title.strip() + previous_news = previous_news.strip() + section_title = (f"## {version} ({datetime.date.today().strftime('%d %B %Y')})" + ).replace("(0", "(") + # TODO: Insert the "Thank you!" section (in monthly releases)? + return f"{title}\n\n{section_title}\n\n{entry.strip()}\n\n\n{previous_news}" + + +def main(run_type, directory, news_file=None): + directory = pathlib.Path(directory) + data = gather(directory) + markdown = changelog_markdown(data) + if news_file: + with open(news_file, "r", encoding="utf-8") as file: + previous_news = file.read() + package_config_path = pathlib.Path(news_file).parent / "package.json" + config = json.loads(package_config_path.read_text()) + new_news = complete_news(config["version"], markdown, previous_news) + if run_type == RunType.dry_run: + print(f"would be written to {news_file}:") + print() + print(new_news) + else: + with open(news_file, "w", encoding="utf-8") as file: + file.write(new_news) + else: + print(markdown) + if run_type == RunType.final: + cleanup(data) + + +if __name__ == "__main__": + arguments = docopt.docopt(__doc__) + for possible_run_type in RunType: + if arguments[f"--{possible_run_type.name}"]: + run_type = possible_run_type + break + else: + run_type = RunType.interim + directory = arguments[""] or pathlib.Path(__file__).parent + main(run_type, directory, arguments["--update"]) diff --git a/news/requirements.in b/news/requirements.in new file mode 100644 index 000000000000..ab0e1cc5187e --- /dev/null +++ b/news/requirements.in @@ -0,0 +1,2 @@ +docopt +pytest diff --git a/news/requirements.txt b/news/requirements.txt new file mode 100644 index 000000000000..2656a4bd16de --- /dev/null +++ b/news/requirements.txt @@ -0,0 +1,17 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile +# +attrs==19.3.0 # via pytest +docopt==0.6.2 # via -r news/requirements.in +iniconfig==1.0.1 # via pytest +more-itertools==8.0.0 # via pytest +packaging==20.4 # via pytest +pluggy==0.13.1 # via pytest +py==1.9.0 # via pytest +pyparsing==2.4.5 # via packaging +pytest==6.0.1 # via -r news/requirements.in +six==1.13.0 # via packaging +toml==0.10.1 # via pytest diff --git a/news/test_announce.py b/news/test_announce.py new file mode 100644 index 000000000000..acc125a7c360 --- /dev/null +++ b/news/test_announce.py @@ -0,0 +1,208 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import codecs +import datetime +import pathlib + +import docopt +import pytest + +import announce as ann + + +@pytest.fixture +def directory(tmpdir): + """Fixture to create a temp directory wrapped in a pathlib.Path object.""" + return pathlib.Path(tmpdir) + + +def test_news_entry_formatting(directory): + issue = 42 + normal_entry = directory / f"{issue}.md" + nonce_entry = directory / f"{issue}-nonce.md" + body = "Hello, world!" + normal_entry.write_text(body, encoding="utf-8") + nonce_entry.write_text(body, encoding="utf-8") + results = list(ann.news_entries(directory)) + assert len(results) == 2 + for result in results: + assert result.issue_number == issue + assert result.description == body + + +def test_news_entry_sorting(directory): + oldest_entry = directory / "45.md" + newest_entry = directory / "123.md" + oldest_entry.write_text("45", encoding="utf-8") + newest_entry.write_text("123", encoding="utf-8") + results = list(ann.news_entries(directory)) + assert len(results) == 2 + assert results[0].issue_number == 45 + assert results[1].issue_number == 123 + + +def test_only_utf8(directory): + entry = directory / "42.md" + entry.write_text("Hello, world", encoding="utf-16") + with pytest.raises(ValueError): + list(ann.news_entries(directory)) + + +def test_no_bom_allowed(directory): + entry = directory / "42.md" + entry.write_bytes(codecs.BOM_UTF8 + "Hello, world".encode("utf-8")) + with pytest.raises(ValueError): + list(ann.news_entries(directory)) + + +def test_bad_news_entry_file_name(directory): + entry = directory / "bunk.md" + entry.write_text("Hello, world!") + with pytest.raises(ValueError): + list(ann.news_entries(directory)) + + +def test_news_entry_README_skipping(directory): + entry = directory / "README.md" + entry.write_text("Hello, world!") + assert len(list(ann.news_entries(directory))) == 0 + + +def test_sections_sorting(directory): + dir2 = directory / "2 Hello" + dir1 = directory / "1 World" + dir2.mkdir() + dir1.mkdir() + results = list(ann.sections(directory)) + assert [found.title for found in results] == ["World", "Hello"] + + +def test_sections_naming(directory): + (directory / "Hello").mkdir() + assert not ann.sections(directory) + + +def test_gather(directory): + fixes = directory / "2 Fixes" + fixes.mkdir() + fix1 = fixes / "1.md" + fix1.write_text("Fix 1", encoding="utf-8") + fix2 = fixes / "3.md" + fix2.write_text("Fix 2", encoding="utf-8") + enhancements = directory / "1 Enhancements" + enhancements.mkdir() + enhancement1 = enhancements / "2.md" + enhancement1.write_text("Enhancement 1", encoding="utf-8") + enhancement2 = enhancements / "4.md" + enhancement2.write_text("Enhancement 2", encoding="utf-8") + results = ann.gather(directory) + assert len(results) == 2 + section, entries = results[0] + assert section.title == "Enhancements" + assert len(entries) == 2 + assert entries[0].description == "Enhancement 1" + assert entries[1].description == "Enhancement 2" + section, entries = results[1] + assert len(entries) == 2 + assert section.title == "Fixes" + assert entries[0].description == "Fix 1" + assert entries[1].description == "Fix 2" + + +def test_entry_markdown(): + markdown = ann.entry_markdown(ann.NewsEntry(42, "Hello, world!", None)) + assert "42" in markdown + assert "Hello, world!" in markdown + assert "https://github.com/Microsoft/vscode-python/issues/42" in markdown + + +def test_changelog_markdown(): + data = [ + ( + ann.SectionTitle(1, "Enhancements", None), + [ + ann.NewsEntry(2, "Enhancement 1", None), + ann.NewsEntry(4, "Enhancement 2", None), + ], + ), + ( + ann.SectionTitle(1, "Fixes", None), + [ann.NewsEntry(1, "Fix 1", None), ann.NewsEntry(3, "Fix 2", None)], + ), + ] + markdown = ann.changelog_markdown(data) + assert "### Enhancements" in markdown + assert "### Fixes" in markdown + assert "1" in markdown + assert "Fix 1" in markdown + assert "2" in markdown + assert "Enhancement 1" in markdown + assert "https://github.com/Microsoft/vscode-python/issues/2" in markdown + assert "3" in markdown + assert "Fix 2" in markdown + assert "https://github.com/Microsoft/vscode-python/issues/3" in markdown + assert "4" in markdown + assert "Enhancement 2" in markdown + + +def test_cleanup(directory, monkeypatch): + rm_path = None + + def fake_git_rm(path): + nonlocal rm_path + rm_path = path + + monkeypatch.setattr(ann, "git_rm", fake_git_rm) + fixes = directory / "2 Fixes" + fixes.mkdir() + fix1 = fixes / "1.md" + fix1.write_text("Fix 1", encoding="utf-8") + results = ann.gather(directory) + assert len(results) == 1 + ann.cleanup(results) + section, entries = results.pop() + assert len(entries) == 1 + assert rm_path == entries[0].path + + +TITLE = "# Our most excellent changelog" +OLD_NEWS = f"""\ +## 2018.12.0 (31 Dec 2018) + +We did things! + +## 2017.11.16 (16 Nov 2017) + +We started going stuff. +""" +NEW_NEWS = """\ +We fixed all the things! + +### Code Health + +We deleted all the code to fix all the things. ;) +""" + + +def test_complete_news(): + version = "2019.3.0" + # Remove leading `0`. + date = datetime.date.today().strftime("%d %B %Y").lstrip("0") + news = ann.complete_news(version, NEW_NEWS, f"{TITLE}\n\n\n{OLD_NEWS}") + expected = f"{TITLE}\n\n## {version} ({date})\n\n{NEW_NEWS.strip()}\n\n\n{OLD_NEWS.strip()}" + assert news == expected + + +def test_cli(): + for option in ("--" + opt for opt in ["dry_run", "interim", "final"]): + args = docopt.docopt(ann.__doc__, [option]) + assert args[option] + args = docopt.docopt(ann.__doc__, ["./news"]) + assert args[""] == "./news" + args = docopt.docopt(ann.__doc__, ["--dry_run", "./news"]) + assert args["--dry_run"] + assert args[""] == "./news" + args = docopt.docopt(ann.__doc__, ["--update", "CHANGELOG.md", "./news"]) + assert args["--update"] == "CHANGELOG.md" + assert args[""] == "./news" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000000..0d76244b1ab9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,28467 @@ +{ + "name": "python", + "version": "2020.9.0-dev", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/cli": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.8.4.tgz", + "integrity": "sha512-XXLgAm6LBbaNxaGhMAznXXaxtCWfuv6PIDJ9Alsy9JYTOh+j2jJz+L/162kkfU1j/pTSxK1xGmlwI4pdIMkoag==", + "dev": true, + "requires": { + "chokidar": "^2.1.8", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.0.0", + "lodash": "^4.17.13", + "make-dir": "^2.1.0", + "slash": "^2.0.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.5.4.tgz", + "integrity": "sha512-+DaeBEpYq6b2+ZmHx3tHspC+ZRflrvLqwfv8E3hNr5LVQoyBnL8RPKSBCg+rK2W2My9PWlujBiqd0ZPsR9Q6zQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.5.0", + "@babel/helpers": "^7.5.4", + "@babel/parser": "^7.5.0", + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.5.0", + "@babel/types": "^7.5.0", + "convert-source-map": "^1.1.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.11", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.0.tgz", + "integrity": "sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA==", + "dev": true, + "requires": { + "@babel/types": "^7.5.0", + "jsesc": "^2.5.1", + "lodash": "^4.17.11", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", + "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", + "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-builder-react-jsx": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz", + "integrity": "sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0", + "esutils": "^2.0.0" + } + }, + "@babel/helper-call-delegate": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", + "integrity": "sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.4.4", + "@babel/traverse": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-define-map": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz", + "integrity": "sha512-IX3Ln8gLhZpSuqHJSnTNBWGDE9kdkTEWl21A/K7PQ00tseBwbqCHTvNLHSBd9M0R5rER4h5Rsvj9vw0R5SieBg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/types": "^7.4.4", + "lodash": "^4.17.11" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", + "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-function-name": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz", + "integrity": "sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz", + "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", + "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-module-transforms": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz", + "integrity": "sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/template": "^7.4.4", + "@babel/types": "^7.4.4", + "lodash": "^4.17.11" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", + "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", + "dev": true + }, + "@babel/helper-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.4.4.tgz", + "integrity": "sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q==", + "dev": true, + "requires": { + "lodash": "^4.17.11" + } + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", + "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-wrap-function": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-replace-supers": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz", + "integrity": "sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.0.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/traverse": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", + "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", + "dev": true, + "requires": { + "@babel/template": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", + "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "dev": true, + "requires": { + "@babel/types": "^7.4.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", + "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", + "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/template": "^7.1.0", + "@babel/traverse": "^7.1.0", + "@babel/types": "^7.2.0" + } + }, + "@babel/helpers": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.4.tgz", + "integrity": "sha512-6LJ6xwUEJP51w0sIgKyfvFMJvIb9mWAfohJp0+m6eHJigkFdcH8duZ1sfhn0ltJRzwUIT/yqqhdSfRpCpL7oow==", + "dev": true, + "requires": { + "@babel/template": "^7.4.4", + "@babel/traverse": "^7.5.0", + "@babel/types": "^7.5.0" + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.0.tgz", + "integrity": "sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA==", + "dev": true + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", + "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0", + "@babel/plugin-syntax-async-generators": "^7.2.0" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", + "integrity": "sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", + "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-json-strings": "^7.2.0" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.4.tgz", + "integrity": "sha512-KCx0z3y7y8ipZUMAEEJOyNi11lMb/FOPUjjB113tfowgw0c16EGYos7worCKBcUAh2oG+OBnoUhsnTSoLpV9uA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz", + "integrity": "sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", + "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", + "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", + "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", + "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", + "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", + "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", + "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz", + "integrity": "sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-remap-async-to-generator": "^7.1.0" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", + "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.4.4.tgz", + "integrity": "sha512-jkTUyWZcTrwxu5DD4rWz6rDB5Cjdmgz6z7M7RLXOJyCUkFBawssDGcGh8M/0FTSB87avyJI1HsTwUXp9nKA1PA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "lodash": "^4.17.11" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.4.4.tgz", + "integrity": "sha512-/e44eFLImEGIpL9qPxSRat13I5QNRgBLu2hOQJCF7VLy/otSM/sypV1+XaIw5+502RX/+6YaSAPmldk+nhHDPw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-define-map": "^7.4.4", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.4.4", + "@babel/helper-split-export-declaration": "^7.4.4", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", + "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz", + "integrity": "sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz", + "integrity": "sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz", + "integrity": "sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", + "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", + "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz", + "integrity": "sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", + "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz", + "integrity": "sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz", + "integrity": "sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz", + "integrity": "sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-simple-access": "^7.1.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz", + "integrity": "sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.4.4", + "@babel/helper-plugin-utils": "^7.0.0", + "babel-plugin-dynamic-import-node": "^2.3.0" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz", + "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.1.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz", + "integrity": "sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg==", + "dev": true, + "requires": { + "regexp-tree": "^0.1.6" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz", + "integrity": "sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz", + "integrity": "sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.1.0" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", + "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", + "dev": true, + "requires": { + "@babel/helper-call-delegate": "^7.4.4", + "@babel/helper-get-function-arity": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz", + "integrity": "sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-react-display-name": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz", + "integrity": "sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz", + "integrity": "sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg==", + "dev": true, + "requires": { + "@babel/helper-builder-react-jsx": "^7.3.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz", + "integrity": "sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.5.0.tgz", + "integrity": "sha512-58Q+Jsy4IDCZx7kqEZuSDdam/1oW8OdDX8f+Loo6xyxdfg1yF0GE2XNJQSTZCaMol93+FBzpWiPEwtbMloAcPg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", + "integrity": "sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.0" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz", + "integrity": "sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.5.0.tgz", + "integrity": "sha512-LmPIZOAgTLl+86gR9KjLXex6P/lRz1fWEjTz6V6QZMmKie51ja3tvzdwORqhHc4RWR8TcZ5pClpRWs0mlaA2ng==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "resolve": "^1.8.1", + "semver": "^5.5.1" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", + "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz", + "integrity": "sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", + "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.0.0" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", + "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", + "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz", + "integrity": "sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-regex": "^7.4.4", + "regexpu-core": "^4.5.4" + } + }, + "@babel/polyfill": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.4.4.tgz", + "integrity": "sha512-WlthFLfhQQhh+A2Gn5NSFl0Huxz36x86Jn+E9OW7ibK8edKPq+KLy4apM1yDpQ8kJOVi1OVjpP4vSDLdrI04dg==", + "dev": true, + "requires": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", + "dev": true + } + } + }, + "@babel/preset-env": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.5.4.tgz", + "integrity": "sha512-hFnFnouyRNiH1rL8YkX1ANCNAUVC8Djwdqfev8i1415tnAG+7hlA5zhZ0Q/3Q5gkop4HioIPbCEWAalqcbxRoQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-async-generator-functions": "^7.2.0", + "@babel/plugin-proposal-dynamic-import": "^7.5.0", + "@babel/plugin-proposal-json-strings": "^7.2.0", + "@babel/plugin-proposal-object-rest-spread": "^7.5.4", + "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-json-strings": "^7.2.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", + "@babel/plugin-transform-arrow-functions": "^7.2.0", + "@babel/plugin-transform-async-to-generator": "^7.5.0", + "@babel/plugin-transform-block-scoped-functions": "^7.2.0", + "@babel/plugin-transform-block-scoping": "^7.4.4", + "@babel/plugin-transform-classes": "^7.4.4", + "@babel/plugin-transform-computed-properties": "^7.2.0", + "@babel/plugin-transform-destructuring": "^7.5.0", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/plugin-transform-duplicate-keys": "^7.5.0", + "@babel/plugin-transform-exponentiation-operator": "^7.2.0", + "@babel/plugin-transform-for-of": "^7.4.4", + "@babel/plugin-transform-function-name": "^7.4.4", + "@babel/plugin-transform-literals": "^7.2.0", + "@babel/plugin-transform-member-expression-literals": "^7.2.0", + "@babel/plugin-transform-modules-amd": "^7.5.0", + "@babel/plugin-transform-modules-commonjs": "^7.5.0", + "@babel/plugin-transform-modules-systemjs": "^7.5.0", + "@babel/plugin-transform-modules-umd": "^7.2.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.4.5", + "@babel/plugin-transform-new-target": "^7.4.4", + "@babel/plugin-transform-object-super": "^7.2.0", + "@babel/plugin-transform-parameters": "^7.4.4", + "@babel/plugin-transform-property-literals": "^7.2.0", + "@babel/plugin-transform-regenerator": "^7.4.5", + "@babel/plugin-transform-reserved-words": "^7.2.0", + "@babel/plugin-transform-shorthand-properties": "^7.2.0", + "@babel/plugin-transform-spread": "^7.2.0", + "@babel/plugin-transform-sticky-regex": "^7.2.0", + "@babel/plugin-transform-template-literals": "^7.4.4", + "@babel/plugin-transform-typeof-symbol": "^7.2.0", + "@babel/plugin-transform-unicode-regex": "^7.4.4", + "@babel/types": "^7.5.0", + "browserslist": "^4.6.0", + "core-js-compat": "^3.1.1", + "invariant": "^2.2.2", + "js-levenshtein": "^1.1.3", + "semver": "^5.5.0" + } + }, + "@babel/preset-react": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.0.0.tgz", + "integrity": "sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-transform-react-display-name": "^7.0.0", + "@babel/plugin-transform-react-jsx": "^7.0.0", + "@babel/plugin-transform-react-jsx-self": "^7.0.0", + "@babel/plugin-transform-react-jsx-source": "^7.0.0" + } + }, + "@babel/register": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.9.0.tgz", + "integrity": "sha512-Tv8Zyi2J2VRR8g7pC5gTeIN8Ihultbmk0ocyNz8H2nEZbmhp1N6q0A1UGsQbDvGP/sNinQKUHf3SqXwqjtFv4Q==", + "dev": true, + "requires": { + "find-cache-dir": "^2.0.0", + "lodash": "^4.17.13", + "make-dir": "^2.1.0", + "pirates": "^4.0.0", + "source-map-support": "^0.5.16" + }, + "dependencies": { + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, + "@babel/runtime": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.4.tgz", + "integrity": "sha512-Na84uwyImZZc3FKf4aUF1tysApzwf3p2yuFBIyBfbzT5glzKTdvYI4KVW4kcgjrzoGUjC7w3YyCHcJKaRxsr2Q==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "@babel/runtime-corejs2": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.5.4.tgz", + "integrity": "sha512-sHv74OzyZ18d6tjHU0HmlVES3+l+lydkOMTiKsJSTGWcTBpIMfXLEgduahlJrQjknW9RCQAqLIEdLOHjBmq/hg==", + "dev": true, + "requires": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", + "dev": true + } + } + }, + "@babel/runtime-corejs3": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.5.tgz", + "integrity": "sha512-RMafpmrNB5E/bwdSphLr8a8++9TosnyJp98RZzI6VOx2R2CCMpsXXXRvmI700O9oEKpXdZat6oEK68/F0zjd4A==", + "dev": true, + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.4" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + } + } + }, + "@babel/template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", + "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.4.4", + "@babel/types": "^7.4.4" + } + }, + "@babel/traverse": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.0.tgz", + "integrity": "sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/generator": "^7.5.0", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.5.0", + "@babel/types": "^7.5.0", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.11" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "@babel/types": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.0.tgz", + "integrity": "sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.11", + "to-fast-properties": "^2.0.0" + } + }, + "@blueprintjs/core": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-3.22.3.tgz", + "integrity": "sha512-IaxkvJyF+4VCvAjMvyHtJ4qUiQNwgPu4zIxLRo1cBsu30gHjYzwe+xDdssBR7yRnXARguM6bkI523w+yJOerCA==", + "dev": true, + "requires": { + "@blueprintjs/icons": "^3.12.0", + "@types/dom4": "^2.0.1", + "classnames": "^2.2", + "dom4": "^2.1.5", + "normalize.css": "^8.0.1", + "popper.js": "^1.15.0", + "react-lifecycles-compat": "^3.0.4", + "react-popper": "^1.3.7", + "react-transition-group": "^2.9.0", + "resize-observer-polyfill": "^1.5.1", + "tslib": "~1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + } + } + }, + "@blueprintjs/icons": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.13.0.tgz", + "integrity": "sha512-fvXGsAJ66SSjeHv3OeXjLEdKdPJ3wVztjhJQCAd51uebhj3FJ16EDDvO7BqBw5FyVkLkU11KAxSoCFZt7TC9GA==", + "dev": true, + "requires": { + "classnames": "^2.2", + "tslib": "~1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + } + } + }, + "@blueprintjs/select": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@blueprintjs/select/-/select-3.11.2.tgz", + "integrity": "sha512-fU0Km6QI/ayWhzYeu9N1gTj0+L0XUO4KB3u2LfJXgj648UGY8F4HX2ETdJ+XPdtsu6TesrIL7ghMQhtLcvafBg==", + "dev": true, + "requires": { + "@blueprintjs/core": "^3.20.0", + "classnames": "^2.2", + "tslib": "~1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + } + } + }, + "@emotion/babel-utils": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/@emotion/babel-utils/-/babel-utils-0.6.10.tgz", + "integrity": "sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow==", + "dev": true, + "requires": { + "@emotion/hash": "^0.6.6", + "@emotion/memoize": "^0.6.6", + "@emotion/serialize": "^0.9.1", + "convert-source-map": "^1.5.1", + "find-root": "^1.1.0", + "source-map": "^0.7.2" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "@emotion/hash": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz", + "integrity": "sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ==", + "dev": true + }, + "@emotion/memoize": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz", + "integrity": "sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ==", + "dev": true + }, + "@emotion/serialize": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.9.1.tgz", + "integrity": "sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ==", + "dev": true, + "requires": { + "@emotion/hash": "^0.6.6", + "@emotion/memoize": "^0.6.6", + "@emotion/unitless": "^0.6.7", + "@emotion/utils": "^0.8.2" + } + }, + "@emotion/stylis": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.7.1.tgz", + "integrity": "sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ==", + "dev": true + }, + "@emotion/unitless": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.7.tgz", + "integrity": "sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg==", + "dev": true + }, + "@emotion/utils": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.8.2.tgz", + "integrity": "sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw==", + "dev": true + }, + "@enonic/fnv-plus": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@enonic/fnv-plus/-/fnv-plus-1.3.0.tgz", + "integrity": "sha512-BCN9uNWH8AmiP7BXBJqEinUY9KXalmRzo+L0cB/mQsmFfzODxwQrbvxCHXUNH2iP+qKkWYtB4vyy8N62PViMFw==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz", + "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + } + } + }, + "@gulp-sourcemaps/identity-map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-1.0.2.tgz", + "integrity": "sha512-ciiioYMLdo16ShmfHBXJBOFm3xPC4AuwO4xeRpFeHz7WK9PYsWCmigagG2XyzZpubK4a3qNKoUBDhbzHfa50LQ==", + "dev": true, + "requires": { + "acorn": "^5.0.3", + "css": "^2.2.1", + "normalize-path": "^2.1.1", + "source-map": "^0.6.0", + "through2": "^2.0.3" + }, + "dependencies": { + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", + "dev": true, + "requires": { + "normalize-path": "^2.0.1", + "through2": "^2.0.3" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "@icons/material": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "dev": true + }, + "@istanbuljs/load-nyc-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", + "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/nyc-config-typescript": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-0.1.3.tgz", + "integrity": "sha512-EzRFg92bRSD1W/zeuNkeGwph0nkWf+pP2l/lYW4/5hav7RjKKBN5kV1Ix7Tvi0CMu3pC4Wi/U7rNisiJMR3ORg==", + "dev": true + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, + "@jupyter-widgets/base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@jupyter-widgets/base/-/base-2.0.2.tgz", + "integrity": "sha512-nNpD+RGJ0As74XxDSGMeObfXSZ8XPBFHJ1AyugzYxpmxIigB2n3DxTyonASkR/3hXwxl3/nXBxHGlxQGs/+nOA==", + "dev": true, + "requires": { + "@jupyterlab/services": "^4.0.0", + "@phosphor/coreutils": "^1.2.0", + "@phosphor/messaging": "^1.2.1", + "@phosphor/widgets": "^1.3.0", + "@types/backbone": "^1.4.1", + "@types/lodash": "^4.14.134", + "backbone": "1.2.3", + "base64-js": "^1.2.1", + "jquery": "^3.1.1", + "lodash": "^4.17.4" + } + }, + "@jupyter-widgets/controls": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@jupyter-widgets/controls/-/controls-1.5.3.tgz", + "integrity": "sha512-eigiIdhYeziKslm+pddZSz5RkzbkDB1C1O25K/S1+yzHm5DTR4iPt00vliOn2wokTvZUsmaC2JPL05eaVAf2iA==", + "dev": true, + "requires": { + "@jupyter-widgets/base": "^2.0.2", + "@phosphor/algorithm": "^1.1.0", + "@phosphor/domutils": "^1.1.0", + "@phosphor/messaging": "^1.2.1", + "@phosphor/signaling": "^1.2.0", + "@phosphor/widgets": "^1.3.0", + "d3-format": "^1.3.0", + "jquery": "^3.1.1", + "jquery-ui": "^1.12.1", + "underscore": "^1.8.3" + } + }, + "@jupyter-widgets/jupyterlab-manager": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@jupyter-widgets/jupyterlab-manager/-/jupyterlab-manager-1.1.0.tgz", + "integrity": "sha512-2dKdzl43nMvmr8A/AtonPb23SWWTQW9KjwCmTjWcWMfMsAcmAg5yLY+KfmQE59RnVA/xEcgdb53y1/ADuNbzhg==", + "dev": true, + "requires": { + "@jupyter-widgets/base": "^2.0.2", + "@jupyter-widgets/controls": "^1.5.3", + "@jupyter-widgets/output": "^2.0.1", + "@jupyterlab/application": "^1.2.0", + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/docregistry": "^1.2.0", + "@jupyterlab/logconsole": "^1.0.0", + "@jupyterlab/mainmenu": "^1.2.0", + "@jupyterlab/notebook": "^1.2.0", + "@jupyterlab/outputarea": "^1.2.0", + "@jupyterlab/rendermime": "^1.2.0", + "@jupyterlab/rendermime-interfaces": "^1.5.0", + "@jupyterlab/services": "^4.2.0", + "@phosphor/algorithm": "^1.1.0", + "@phosphor/coreutils": "^1.3.0", + "@phosphor/disposable": "^1.1.1", + "@phosphor/messaging": "^1.2.1", + "@phosphor/properties": "^1.1.0", + "@phosphor/signaling": "^1.2.0", + "@phosphor/widgets": "^1.3.0", + "@types/backbone": "^1.4.1", + "jquery": "^3.1.1", + "semver": "^6.1.1" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + }, + "dependencies": { + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@jupyter-widgets/output": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@jupyter-widgets/output/-/output-2.0.1.tgz", + "integrity": "sha512-8yljIRbn98XNAoLnTOGEiBGKc7SH/ZEueatcJqu8FBMv3FN8hUKc2mgPLfcrlFRDwfe09p51j793oBUYdJUKPg==", + "dev": true, + "requires": { + "@jupyter-widgets/base": "^2.0.2" + } + }, + "@jupyter-widgets/schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@jupyter-widgets/schema/-/schema-0.4.0.tgz", + "integrity": "sha512-0MAZ6hLOCe2dYiUvEAfYvWKD7zV9AdkC4AoIEQiWqAai9Pq06oPNWMMg6x+J0ZaNnZWqR2c16f62ehd57Ql7Zw==" + }, + "@jupyterlab/application": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/application/-/application-1.2.2.tgz", + "integrity": "sha512-HNHVR6ApLdS542ndCmvD0GBitNuLeQwAP2cc+L7fSu67KG4CiMaPyJfyFuQKcMyZbWb5ngebDf8qMgyUX8+gwQ==", + "dev": true, + "requires": { + "@jupyterlab/apputils": "^1.2.2", + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/docregistry": "^1.2.2", + "@jupyterlab/rendermime": "^1.2.2", + "@jupyterlab/rendermime-interfaces": "^1.5.0", + "@jupyterlab/services": "^4.2.0", + "@jupyterlab/ui-components": "^1.2.1", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/application": "^1.7.0", + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/messaging": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "@phosphor/widgets": "^1.9.0", + "font-awesome": "~4.7.0" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/apputils": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/apputils/-/apputils-1.2.2.tgz", + "integrity": "sha512-9JFub/RTxL/9w2oyihDrUGk/hJW06PbeE7aIyl4S92ePK6trl3q/N1nPqEoC0N1Kj3KmMt9wFpNHt7BYHyVoQw==", + "dev": true, + "requires": { + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/services": "^4.2.0", + "@jupyterlab/ui-components": "^1.2.1", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/domutils": "^1.1.3", + "@phosphor/messaging": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "@phosphor/virtualdom": "^1.2.0", + "@phosphor/widgets": "^1.9.0", + "@types/react": "~16.8.18", + "react": "~16.8.4", + "react-dom": "~16.8.4", + "sanitize-html": "~1.20.1" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/attachments": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/attachments/-/attachments-1.2.2.tgz", + "integrity": "sha512-ME7aWyEs4piW6tCQbJ4zfuX6XipX07Wb9vVcW7SuRPlDp7lIUi4nPINkMO2us/lTsuS0/ZjgXjb+VKu4CbTYFQ==", + "dev": true, + "requires": { + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/observables": "^2.4.0", + "@jupyterlab/rendermime": "^1.2.2", + "@jupyterlab/rendermime-interfaces": "^1.5.0", + "@phosphor/disposable": "^1.3.0", + "@phosphor/signaling": "^1.3.0" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/cells": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@jupyterlab/cells/-/cells-1.2.3.tgz", + "integrity": "sha512-7ZyXdq/bcQAlPS5ACAGSB/T2NLSnMhMSmItyYq1j77VCKAh2RW5IJEdGuHfUn+6+wBrBIcF3/P4wwzPGVUtCgw==", + "dev": true, + "requires": { + "@jupyterlab/apputils": "^1.2.2", + "@jupyterlab/attachments": "^1.2.2", + "@jupyterlab/codeeditor": "^1.2.0", + "@jupyterlab/codemirror": "^1.2.2", + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/filebrowser": "^1.2.2", + "@jupyterlab/observables": "^2.4.0", + "@jupyterlab/outputarea": "^1.2.3", + "@jupyterlab/rendermime": "^1.2.2", + "@jupyterlab/services": "^4.2.0", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/dragdrop": "^1.3.0", + "@phosphor/messaging": "^1.3.0", + "@phosphor/signaling": "^1.3.0", + "@phosphor/virtualdom": "^1.2.0", + "@phosphor/widgets": "^1.9.0", + "react": "~16.8.4" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/codeeditor": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/codeeditor/-/codeeditor-1.2.0.tgz", + "integrity": "sha512-toejhF/a80X10SZyvEnsnnlS9SxR5W4cz67ju7e/2lsZ8RMwZEDDJAJXyW3mw/EEjt8oVRNP2QpM8L5clE9XyQ==", + "dev": true, + "requires": { + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/observables": "^2.4.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/dragdrop": "^1.3.0", + "@phosphor/messaging": "^1.3.0", + "@phosphor/signaling": "^1.3.0", + "@phosphor/widgets": "^1.9.0" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/codemirror": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/codemirror/-/codemirror-1.2.2.tgz", + "integrity": "sha512-hVPpUa5FnuseJj+ZXIUtjP2nP4fjJB/Fwwc5EdpQHiogwpCR6xibyGdSlkRWmIEzFBp4C0yAB2XbqJt13Ofkkg==", + "dev": true, + "requires": { + "@jupyterlab/apputils": "^1.2.2", + "@jupyterlab/codeeditor": "^1.2.0", + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/observables": "^2.4.0", + "@jupyterlab/statusbar": "^1.2.2", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/signaling": "^1.3.0", + "@phosphor/widgets": "^1.9.0", + "codemirror": "~5.47.0", + "react": "~16.8.4" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/coreutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.1.0.tgz", + "integrity": "sha512-ZqgzDUyanyvc86gtCrIbc1M6iniKHYmWNWHvWOcnq3KIP3wk3grchsTYPTfQDxcUS6F04baPGp/KohEU2ml40Q==", + "requires": { + "@phosphor/commands": "^1.6.3", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.2.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.2.3", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@jupyterlab/docmanager": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/docmanager/-/docmanager-1.2.2.tgz", + "integrity": "sha512-JlA/4rUIumH2sZndTzd2/f8Aevegi4eXvUrvZY6cA3fG9Nir6hHSBznUfaL3iR2YHmnxTM5hUwAK8jnjwmLuig==", + "dev": true, + "requires": { + "@jupyterlab/apputils": "^1.2.2", + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/docregistry": "^1.2.2", + "@jupyterlab/services": "^4.2.0", + "@jupyterlab/statusbar": "^1.2.2", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/messaging": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "@phosphor/widgets": "^1.9.0", + "react": "~16.8.4" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/docregistry": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/docregistry/-/docregistry-1.2.2.tgz", + "integrity": "sha512-2c3nA+PsTsZ8h5Z80EE38Iz7LJTpxmk57c7aZ0LWw65eAf+c071w9xUdWRg9apmXz4o7NJsMeDm6Dns+7+AZow==", + "dev": true, + "requires": { + "@jupyterlab/apputils": "^1.2.2", + "@jupyterlab/codeeditor": "^1.2.0", + "@jupyterlab/codemirror": "^1.2.2", + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/observables": "^2.4.0", + "@jupyterlab/rendermime": "^1.2.2", + "@jupyterlab/rendermime-interfaces": "^1.5.0", + "@jupyterlab/services": "^4.2.0", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/messaging": "^1.3.0", + "@phosphor/signaling": "^1.3.0", + "@phosphor/widgets": "^1.9.0" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/filebrowser": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/filebrowser/-/filebrowser-1.2.2.tgz", + "integrity": "sha512-xs01yLt6LBCDaCDVowthRQT+oIFuLQ9n3unwMctHZLi1aeo5cJVzBq/OKMBNJcMAOMAn6HSAEvKwfSnWkpKuiw==", + "dev": true, + "requires": { + "@jupyterlab/apputils": "^1.2.2", + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/docmanager": "^1.2.2", + "@jupyterlab/docregistry": "^1.2.2", + "@jupyterlab/services": "^4.2.0", + "@jupyterlab/statusbar": "^1.2.2", + "@jupyterlab/ui-components": "^1.2.1", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/domutils": "^1.1.3", + "@phosphor/dragdrop": "^1.3.0", + "@phosphor/messaging": "^1.3.0", + "@phosphor/signaling": "^1.3.0", + "@phosphor/widgets": "^1.9.0", + "react": "~16.8.4" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/logconsole": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@jupyterlab/logconsole/-/logconsole-1.0.3.tgz", + "integrity": "sha512-W/YCr57UwH11O+6iyWyLILffBeIGDAzCY6B/Yb97+YH4Cgdxovmy0zlTazLLBD+Pju7oDqJht4OQonDc0XchIA==", + "dev": true, + "requires": { + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/outputarea": "^1.2.3", + "@jupyterlab/rendermime": "^1.2.2", + "@jupyterlab/services": "^4.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/messaging": "^1.3.0", + "@phosphor/signaling": "^1.3.0", + "@phosphor/widgets": "^1.9.0" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/mainmenu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/mainmenu/-/mainmenu-1.2.2.tgz", + "integrity": "sha512-hNFvNvsjbqzM1GQp3tF/34j7Y9eR1zoGUuZBsAkge3W0Nsri8fUXfPwBTfOFBXhIx4j9aB4vZhCrgU91eNt7MA==", + "dev": true, + "requires": { + "@jupyterlab/apputils": "^1.2.2", + "@jupyterlab/services": "^4.2.0", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/widgets": "^1.9.0" + }, + "dependencies": { + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/notebook": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@jupyterlab/notebook/-/notebook-1.2.3.tgz", + "integrity": "sha512-+LGIgWRTIaCVTFG8v3Ps1v0lb717Q8ojwvKvv1DdH4g/nbd6KiO5LXKbeprhZ55nyHmxJMzD6Zu87YzDSea5aA==", + "dev": true, + "requires": { + "@jupyterlab/apputils": "^1.2.2", + "@jupyterlab/cells": "^1.2.3", + "@jupyterlab/codeeditor": "^1.2.0", + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/docregistry": "^1.2.2", + "@jupyterlab/observables": "^2.4.0", + "@jupyterlab/rendermime": "^1.2.2", + "@jupyterlab/services": "^4.2.0", + "@jupyterlab/statusbar": "^1.2.2", + "@jupyterlab/ui-components": "^1.2.1", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/domutils": "^1.1.3", + "@phosphor/dragdrop": "^1.3.0", + "@phosphor/messaging": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "@phosphor/virtualdom": "^1.2.0", + "@phosphor/widgets": "^1.9.0", + "react": "~16.8.4" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/observables": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/observables/-/observables-2.4.0.tgz", + "integrity": "sha512-M/fhAnPqd6F4Zwt4IIsvHCkJmwbSw1Tko/hUXgdUQG86lPsJiTOh98sB3qwV1gtzb9oFF+kH21XsHnQZ6Yl6Pw==", + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/messaging": "^1.3.0", + "@phosphor/signaling": "^1.3.0" + }, + "dependencies": { + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/outputarea": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@jupyterlab/outputarea/-/outputarea-1.2.3.tgz", + "integrity": "sha512-7bFq4sUwoQfAS4KlDu8ogKwo2xGelj5YnVIxw4jlVY2+InMptfhltCI4q8qwvxgTAXVW9m+8QGC4ZHgtRdJt7Q==", + "dev": true, + "requires": { + "@jupyterlab/apputils": "^1.2.2", + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/observables": "^2.4.0", + "@jupyterlab/rendermime": "^1.2.2", + "@jupyterlab/rendermime-interfaces": "^1.5.0", + "@jupyterlab/services": "^4.2.0", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/messaging": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "@phosphor/widgets": "^1.9.0" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/rendermime": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/rendermime/-/rendermime-1.2.2.tgz", + "integrity": "sha512-Meptrw869XQ5LUVOTpKUZNzJPcAO1rCc71Ch1jrPM4VRq3wkGvFGN5p82z2qWPOWXIG5IcQbfTnqpWLDVNzqMw==", + "dev": true, + "requires": { + "@jupyterlab/apputils": "^1.2.2", + "@jupyterlab/codemirror": "^1.2.2", + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/observables": "^2.4.0", + "@jupyterlab/rendermime-interfaces": "^1.5.0", + "@jupyterlab/services": "^4.2.0", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/messaging": "^1.3.0", + "@phosphor/signaling": "^1.3.0", + "@phosphor/widgets": "^1.9.0", + "lodash.escape": "^4.0.1", + "marked": "^0.7.0" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/rendermime-interfaces": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/rendermime-interfaces/-/rendermime-interfaces-1.5.0.tgz", + "integrity": "sha512-k6DjX/srKl1FA1CZyrAzz1qA2v1arXUIAmbEddZ5L3O+dnvDlOKjkI/NexaRQvmQ62aziSln+wKrr2P1JPNmGg==", + "dev": true, + "requires": { + "@phosphor/coreutils": "^1.3.1", + "@phosphor/widgets": "^1.9.0" + } + }, + "@jupyterlab/services": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-4.2.0.tgz", + "integrity": "sha512-diqiWCYLROwZoeZ46hO4oCINQKr3S789GOA3drw5gDie18/u1nJcvegnY9Oo58xHINnKTs0rKZmFgTe2DvYgQg==", + "requires": { + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/observables": "^2.4.0", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/signaling": "^1.3.0", + "node-fetch": "^2.6.0", + "ws": "^7.0.0" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, + "ws": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.0.tgz", + "integrity": "sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==", + "requires": { + "async-limiter": "^1.0.0" + } + } + } + }, + "@jupyterlab/statusbar": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/statusbar/-/statusbar-1.2.2.tgz", + "integrity": "sha512-1SIyW3pqoqD1y7g5WvGB2UJASQ6uikE1HtRLj67UEstdTfiZyVPSutTZJrXqkRqsB7jPEU4EfwCLUFXEbZkLZw==", + "dev": true, + "requires": { + "@jupyterlab/apputils": "^1.2.2", + "@jupyterlab/codeeditor": "^1.2.0", + "@jupyterlab/coreutils": "^3.2.0", + "@jupyterlab/services": "^4.2.0", + "@jupyterlab/ui-components": "^1.2.1", + "@phosphor/algorithm": "^1.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/messaging": "^1.3.0", + "@phosphor/signaling": "^1.3.0", + "@phosphor/widgets": "^1.9.0", + "react": "~16.8.4", + "typestyle": "^2.0.1" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@jupyterlab/ui-components": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jupyterlab/ui-components/-/ui-components-1.2.1.tgz", + "integrity": "sha512-GUtIRwTmFnlJaPUM8SiFw1STmsyMVGjchLKqIoQnn0qYAJvaSUGyRqqoSD5iIpwov6OHCOOyxH6fQ5OAtH1kwA==", + "dev": true, + "requires": { + "@blueprintjs/core": "^3.9.0", + "@blueprintjs/select": "^3.3.0", + "@jupyterlab/coreutils": "^3.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/messaging": "^1.3.0", + "@phosphor/virtualdom": "^1.2.0", + "@phosphor/widgets": "^1.9.0", + "react": "~16.8.4", + "typestyle": "^2.0.1" + }, + "dependencies": { + "@jupyterlab/coreutils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz", + "integrity": "sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.0", + "ajv": "^6.5.5", + "json5": "^2.1.0", + "minimist": "~1.2.0", + "moment": "^2.24.0", + "path-posix": "~1.0.0", + "url-parse": "~1.4.3" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@loadable/component": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.12.0.tgz", + "integrity": "sha512-eDG7FPZ8tCFA/mqu2IrYV6eS+UxGBo21PwtEV9QpkpYrx25xKRXzJUm36yfQPK3o7jXu43xpPkwiU4mLWcjJlw==", + "requires": { + "@babel/runtime": "^7.7.7", + "hoist-non-react-statics": "^3.3.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz", + "integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==", + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw==", + "requires": { + "react-is": "^16.7.0" + } + } + } + }, + "@mapbox/polylabel": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/polylabel/-/polylabel-1.0.2.tgz", + "integrity": "sha1-xXFGGbZa3QgmOOoGAn5psUUA76Y=", + "dev": true, + "requires": { + "tinyqueue": "^1.1.0" + } + }, + "@npmcli/move-file": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz", + "integrity": "sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + } + } + }, + "@nteract/commutable": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/@nteract/commutable/-/commutable-7.2.9.tgz", + "integrity": "sha512-QwNp8LO4vq5j1W1ls2ZMpwS4E1YIk92ZmfZLy7QLbNumLrkxP/Q0SWNyxRGfugAsWco+8ORqFf/tY8ePBUCcxg==", + "requires": { + "immutable": "^4.0.0-rc.12", + "uuid": "^7.0.0" + }, + "dependencies": { + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } + } + }, + "@nteract/markdown": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nteract/markdown/-/markdown-3.0.1.tgz", + "integrity": "sha512-Dr79niruytzbcDsLLS5NoPIzvVJ3YkJX18O0hmZAmrVn2Ni8L5VW+ZymWsSgRBogDgrYQzhV4L1MmIC/X6rbUg==", + "dev": true, + "requires": { + "@babel/runtime-corejs2": "^7.0.0", + "@nteract/mathjax": "^3.0.1", + "babel-runtime": "^6.26.0", + "react-markdown": "^4.0.0" + } + }, + "@nteract/mathjax": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nteract/mathjax/-/mathjax-3.0.1.tgz", + "integrity": "sha512-jL4a2KjY9wzcg7dG3HvufKMXOk+FxWQ6BFH6p3fwJUerbf3SF6H/RlaLxuMgFP+Tv+sHVXo7FHTTTIIsRyuv8g==", + "dev": true, + "requires": { + "@babel/runtime-corejs2": "^7.0.0", + "babel-runtime": "^6.26.0" + } + }, + "@nteract/messaging": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@nteract/messaging/-/messaging-7.0.4.tgz", + "integrity": "sha512-n0vggzsrRFEtte5SgMVMbgjo19qXa5khDGhvXt4qqmNY3gOSnNSQMpVkXbno6fQXreKOYQ1uO8YIlQTDfOt/Nw==", + "requires": { + "@nteract/types": "^6.0.4", + "@types/uuid": "^7.0.0", + "lodash.clonedeep": "^4.5.0", + "rxjs": "^6.3.3", + "uuid": "^7.0.0" + }, + "dependencies": { + "@types/uuid": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.2.tgz", + "integrity": "sha512-8Ly3zIPTnT0/8RCU6Kg/G3uTICf9sRwYOpUzSIM3503tLIKcnJPRuinHhXngJUy2MntrEf6dlpOHXJju90Qh5w==" + }, + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } + } + }, + "@nteract/octicons": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@nteract/octicons/-/octicons-0.5.1.tgz", + "integrity": "sha512-7d42YfTbaiL2cF8txcQirDCNTbhmKfSoJSRlFMnEH11D0e0XUk/dxEWRGxxLrbP3SApY84hF/9uP3u1p6jqiAQ==", + "dev": true, + "requires": { + "@babel/runtime-corejs2": "^7.0.0", + "babel-runtime": "^6.26.0" + } + }, + "@nteract/plotly": { + "version": "1.48.3", + "resolved": "https://registry.npmjs.org/@nteract/plotly/-/plotly-1.48.3.tgz", + "integrity": "sha512-1Km5MtjyUL9POZjU6LMzH/npFUYIC6vewH0Gd2Ua1FON3qN1KCRoJ8em5Gkp+K57QJRpMET1F4z4N9d3U9HOqA==", + "dev": true + }, + "@nteract/styled-blueprintjsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@nteract/styled-blueprintjsx/-/styled-blueprintjsx-1.1.1.tgz", + "integrity": "sha512-y5HLOPqbjapP7918rpp0o6Y6a8ZGZyINFPUQYoOc7iN8KqIOrNvWnhMISgzGwmXv8T+wDZFycSOnPxDbH7tBUA==", + "dev": true, + "requires": { + "@babel/runtime-corejs2": "^7.0.0", + "babel-runtime": "^6.26.0", + "styled-jsx": "^3.1.0" + } + }, + "@nteract/transform-dataresource": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@nteract/transform-dataresource/-/transform-dataresource-4.5.2.tgz", + "integrity": "sha512-48m/Xd14+vyNGp11qrnts9APKu8kSqZLaI+VJpgErZieakleESaVbleg4EiTlU6j/p7DvihBSKDTuFXuBOpyUA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.0.0", + "@babel/runtime-corejs2": "^7.0.0", + "@nteract/octicons": "^0.5.1", + "@nteract/styled-blueprintjsx": "^1.1.1", + "@nteract/transform-plotly": "^3.2.5", + "d3-collection": "^1.0.7", + "d3-scale": "^2.1.2", + "d3-shape": "^1.2.2", + "d3-time-format": "^2.0.5", + "lodash": "^4.17.4", + "moment": "^2.18.1", + "numeral": "^2.0.6", + "react-color": "^2.14.1", + "react-hot-loader": "^4.1.2", + "react-table": "^6.8.6", + "react-table-hoc-fixed-columns": "1.0.2", + "semiotic": "^1.15.6", + "styled-jsx": "^3.1.0", + "tv4": "^1.3.0" + }, + "dependencies": { + "@nteract/transform-plotly": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@nteract/transform-plotly/-/transform-plotly-3.2.5.tgz", + "integrity": "sha512-tQi47Ly1b/YLNJWhUV8gKVkweJb6ugfGg4XylG2wnLVDoaM8ObJrHAtY74S+Kg9e88N8ExNPy9ulG2xhhSrMag==", + "dev": true, + "requires": { + "@babel/runtime-corejs2": "^7.0.0", + "@nteract/plotly": "^1.0.0", + "babel-runtime": "^6.26.0", + "lodash": "^4.17.4" + } + } + } + }, + "@nteract/transform-geojson": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@nteract/transform-geojson/-/transform-geojson-3.2.5.tgz", + "integrity": "sha512-crP5yFXAFNgE8CQzwcvCbBkbcSdJhn7YruiYreo2gU48fbX8YXOXRMwFsk31jVcMqE9/fYh9zA9sHX38UjKg/w==", + "dev": true, + "requires": { + "@babel/runtime-corejs2": "^7.0.0", + "babel-runtime": "^6.26.0", + "leaflet": "^1.0.3" + } + }, + "@nteract/transform-model-debug": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@nteract/transform-model-debug/-/transform-model-debug-3.2.5.tgz", + "integrity": "sha512-vmtuPfMuClpFo9j9LLX9EqwEkT8B6jrYUSO30YeG2Ct9G1fPlmvmGBtIJSgHv7Km3W4FlVvwglsenFBg/ff6Og==", + "dev": true, + "requires": { + "@babel/runtime-corejs2": "^7.0.0", + "babel-runtime": "^6.26.0" + } + }, + "@nteract/transform-plotly": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@nteract/transform-plotly/-/transform-plotly-6.0.0.tgz", + "integrity": "sha512-RnKPL/UxY0slrXtsP/7ShqZAgXSQjohOVyQcXBXZzqaC3KSd+esJhHYtYzTK2obaLv4YZwjhlLupcVolslN03A==", + "dev": true, + "requires": { + "lodash": "^4.17.4", + "plotly.js-dist": "^1.48.3" + } + }, + "@nteract/transform-vdom": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@nteract/transform-vdom/-/transform-vdom-2.2.5.tgz", + "integrity": "sha512-q6FbWlrSEWUmQpDV1DBPcw5FZpUcQbKOQ2a59vY/qcQ/Qjh1KUCC+gortso+WIE4P36eHZRxKz5ptCu5i47OLg==", + "dev": true, + "requires": { + "@babel/runtime-corejs2": "^7.0.0", + "babel-runtime": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "@nteract/transform-vega": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@nteract/transform-vega/-/transform-vega-6.0.3.tgz", + "integrity": "sha512-sUY9EeCguUfP3G6AI4yg0jUOT2r7eB1VvuQGxjGj0fyhWnYIZR3xv80/aq2J6iACSBxazV+rU2Ne4abWYy6nwQ==", + "dev": true, + "requires": { + "@nteract/vega-embed-v2": "^1.1.0", + "@nteract/vega-embed-v3": "^1.1.1", + "lodash": "^4.17.4", + "vega": "^5.4.0", + "vega-embed": "^4.2.0", + "vega-lite": "^3.3.0" + } + }, + "@nteract/transforms": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@nteract/transforms/-/transforms-4.4.7.tgz", + "integrity": "sha512-G84BeBfzfdAg3cAV78ES9/yxe2ZNitg7j8MCKKx4iNS9vqeQbO92vyBsBW2D6qlddefU/RB5/HUvvtNUpd0ZRw==", + "dev": true, + "requires": { + "@babel/runtime-corejs2": "^7.0.0", + "@nteract/markdown": "^3.0.1", + "@nteract/mathjax": "^3.0.1", + "@nteract/transform-vdom": "^2.2.5", + "ansi-to-react": "^3.3.5", + "babel-runtime": "^6.26.0", + "react-json-tree": "^0.11.0" + } + }, + "@nteract/types": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@nteract/types/-/types-6.0.4.tgz", + "integrity": "sha512-Td7+ql+7XukuHzP2b4qiq7xjD7y6hLJAnXfX4aikjYMzUGXNUAfODqeetKEXDRODFIcwr/jcGewIlWKwedqgLQ==", + "requires": { + "@nteract/commutable": "^7.2.9", + "immutable": "^4.0.0-rc.12", + "rxjs": "^6.3.3", + "uuid": "^7.0.0" + }, + "dependencies": { + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } + } + }, + "@nteract/vega-embed-v2": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nteract/vega-embed-v2/-/vega-embed-v2-1.1.0.tgz", + "integrity": "sha512-5sCymGk2qzb1BC8P8MTkD0L9rG/8gPb8jyQ2Fz5aqftblgZ+DooHNcR4oi4T49mhRv0zLjqstBwDxx5JmzNdtw==", + "dev": true, + "requires": { + "d3": "3.5.17", + "d3-cloud": "^1.2.5", + "datalib": "^1.9.1", + "json-stable-stringify": "^1.0.1" + } + }, + "@nteract/vega-embed-v3": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@nteract/vega-embed-v3/-/vega-embed-v3-1.1.1.tgz", + "integrity": "sha512-tPMGmYI3aJIu4+HI03Rtub6yjCmACuvtVTjrq8+h2/OIgxBIrzaexo/QCdTKgV4bg0Pd88hskwZ8heqdMFDoHw==", + "dev": true, + "requires": { + "d3": "3.5.17", + "d3-cloud": "^1.2.5", + "d3-request": "^1.0.6", + "d3-scale-chromatic": "^1.3.3", + "datalib": "^1.9.1", + "json-stable-stringify": "^1.0.1" + } + }, + "@phosphor/algorithm": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.1.3.tgz", + "integrity": "sha512-+dkdYTBglR+qGnLVQdCvYojNZMGxf+xSl1Jeksha3pm7niQktSFz2aR5gEPu/nI5LM8T8slTpqE4Pjvq8P+IVA==" + }, + "@phosphor/application": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@phosphor/application/-/application-1.7.3.tgz", + "integrity": "sha512-ohxrW7rv5Tms4PSyPRZT6YArZQQGQNG4MgTeFzkoLJ+7mp/BcbFuvEoaV1/CUKQArofl0DCkKDOTOIkXP+/32A==", + "dev": true, + "requires": { + "@phosphor/commands": "^1.7.2", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/widgets": "^1.9.3" + } + }, + "@phosphor/collections": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/collections/-/collections-1.2.0.tgz", + "integrity": "sha512-T9/0EjSuY6+ga2LIFRZ0xupciOR3Qnyy8Q95lhGTC0FXZUFwC8fl9e8On6IcwasCszS+1n8dtZUWSIynfgdpzw==", + "requires": { + "@phosphor/algorithm": "^1.2.0" + }, + "dependencies": { + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" + } + } + }, + "@phosphor/commands": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@phosphor/commands/-/commands-1.7.2.tgz", + "integrity": "sha512-iSyBIWMHsus323BVEARBhuVZNnVel8USo+FIPaAxGcq+icTSSe6+NtSxVQSmZblGN6Qm4iw6I6VtiSx0e6YDgQ==", + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.1", + "@phosphor/domutils": "^1.1.4", + "@phosphor/keyboard": "^1.1.3", + "@phosphor/signaling": "^1.3.1" + }, + "dependencies": { + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@phosphor/coreutils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/coreutils/-/coreutils-1.3.1.tgz", + "integrity": "sha512-9OHCn8LYRcPU/sbHm5v7viCA16Uev3gbdkwqoQqlV+EiauDHl70jmeL7XVDXdigl66Dz0LI11C99XOxp+s3zOA==" + }, + "@phosphor/disposable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.2.0.tgz", + "integrity": "sha512-4PoWoffdrLyWOW5Qv7I8//owvZmv57YhaxetAMWeJl13ThXc901RprL0Gxhtue2ZxL2PtUjM1207HndKo2FVjA==", + "requires": { + "@phosphor/algorithm": "^1.1.3", + "@phosphor/signaling": "^1.2.3" + } + }, + "@phosphor/domutils": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@phosphor/domutils/-/domutils-1.1.4.tgz", + "integrity": "sha512-ivwq5TWjQpKcHKXO8PrMl+/cKqbgxPClPiCKc1gwbMd+6hnW5VLwNG0WBzJTxCzXK43HxX18oH+tOZ3E04wc3w==" + }, + "@phosphor/dragdrop": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@phosphor/dragdrop/-/dragdrop-1.4.1.tgz", + "integrity": "sha512-77paMoubIWk7pdwA2GVFkqba1WP48hTZZvS17N30+KVOeWfSqBL3flPSnW2yC4y6FnOP2PFOCtuPIbQv+pYhCA==", + "dev": true, + "requires": { + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.1" + }, + "dependencies": { + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@phosphor/keyboard": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@phosphor/keyboard/-/keyboard-1.1.3.tgz", + "integrity": "sha512-dzxC/PyHiD6mXaESRy6PZTd9JeK+diwG1pyngkyUf127IXOEzubTIbu52VSdpGBklszu33ws05BAGDa4oBE4mQ==" + }, + "@phosphor/messaging": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@phosphor/messaging/-/messaging-1.3.0.tgz", + "integrity": "sha512-k0JE+BTMKlkM335S2AmmJxoYYNRwOdW5jKBqLgjJdGRvUQkM0+2i60ahM45+J23atGJDv9esKUUBINiKHFhLew==", + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/collections": "^1.2.0" + }, + "dependencies": { + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" + } + } + }, + "@phosphor/properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@phosphor/properties/-/properties-1.1.3.tgz", + "integrity": "sha512-GiglqzU77s6+tFVt6zPq9uuyu/PLQPFcqZt914ZhJ4cN/7yNI/SLyMzpYZ56IRMXvzK9TUgbRna6URE3XAwFUg==" + }, + "@phosphor/signaling": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.2.3.tgz", + "integrity": "sha512-DMwS0m9OgfY5ljpTsklRQPUQpTyg4obz85FyImRDacUVxUVbas95djIDEbU4s1TMzdHBBO+gfki3V4giXUvXzw==", + "requires": { + "@phosphor/algorithm": "^1.1.3" + } + }, + "@phosphor/virtualdom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/virtualdom/-/virtualdom-1.2.0.tgz", + "integrity": "sha512-L9mKNhK2XtVjzjuHLG2uYuepSz8uPyu6vhF4EgCP0rt0TiLYaZeHwuNu3XeFbul9DMOn49eBpye/tfQVd4Ks+w==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + }, + "dependencies": { + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + } + } + }, + "@phosphor/widgets": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@phosphor/widgets/-/widgets-1.9.3.tgz", + "integrity": "sha512-61jsxloDrW/+WWQs8wOgsS5waQ/MSsXBuhONt0o6mtdeL93HVz7CYO5krOoot5owammfF6oX1z0sDaUYIYgcPA==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/commands": "^1.7.2", + "@phosphor/coreutils": "^1.3.1", + "@phosphor/disposable": "^1.3.1", + "@phosphor/domutils": "^1.1.4", + "@phosphor/dragdrop": "^1.4.1", + "@phosphor/keyboard": "^1.1.3", + "@phosphor/messaging": "^1.3.0", + "@phosphor/properties": "^1.1.3", + "@phosphor/signaling": "^1.3.1", + "@phosphor/virtualdom": "^1.2.0" + }, + "dependencies": { + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==", + "dev": true + }, + "@phosphor/disposable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.1.tgz", + "integrity": "sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0", + "@phosphor/signaling": "^1.3.1" + } + }, + "@phosphor/signaling": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.1.tgz", + "integrity": "sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==", + "dev": true, + "requires": { + "@phosphor/algorithm": "^1.2.0" + } + } + } + }, + "@sheerun/mutationobserver-shim": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz", + "integrity": "sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==", + "dev": true + }, + "@sindresorhus/df": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/df/-/df-2.1.0.tgz", + "integrity": "sha1-0gjPJ+BvC7R20U197M19cm6ao4k=", + "dev": true, + "requires": { + "execa": "^0.2.2" + }, + "dependencies": { + "execa": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.2.2.tgz", + "integrity": "sha1-4urUcsLDGq1vc/GslW7vReEjIMs=", + "dev": true, + "requires": { + "cross-spawn-async": "^2.1.1", + "npm-run-path": "^1.0.0", + "object-assign": "^4.0.1", + "path-key": "^1.0.0", + "strip-eof": "^1.0.0" + } + }, + "npm-run-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-1.0.0.tgz", + "integrity": "sha1-9cMr9ZX+ga6Sfa7FLoL4sACsPI8=", + "dev": true, + "requires": { + "path-key": "^1.0.0" + } + }, + "path-key": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-1.0.0.tgz", + "integrity": "sha1-XVPVeAGWRsDWiADbThRua9wqx68=", + "dev": true + } + } + }, + "@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.0.tgz", + "integrity": "sha512-qbk9AP+cZUsKdW1GJsBpxPKFmCJ0T8swwzVje3qFd+AkQb74Q/tiuzrdfFg8AD2g5HH/XbE/I8Uc1KYHVYWfhg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/formatio": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-4.0.1.tgz", + "integrity": "sha512-asIdlLFrla/WZybhm0C8eEzaDNNrzymiTqHMeJl6zPW2881l3uuVRpm0QlRQEjqYWv6CcKMGYME3LbrLJsORBw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^4.2.0" + } + }, + "@sinonjs/samsam": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-4.2.0.tgz", + "integrity": "sha512-yG7QbUz38ZPIegfuSMEcbOo0kkLGmPa8a0Qlz4dk7+cXYALDScWjIZzAm/u2+Frh+bcdZF6wZJZwwuJjY0WAjA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "array-from": "^2.1.1", + "lodash.get": "^4.4.2" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@stroncium/procfs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@stroncium/procfs/-/procfs-1.1.1.tgz", + "integrity": "sha512-c8A7hTAu1FgWJTfOQNqp0N9K/9amlVktNRQidzeeX4dTJpSNl2Y65s9BSfnMlFFGMGP+rBvrb0WTTv7ad6MJ9A==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "@testing-library/dom": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-6.11.0.tgz", + "integrity": "sha512-Pkx9LMIGshyNbfmecjt18rrAp/ayMqGH674jYER0SXj0iG9xZc+zWRjk2Pg9JgPBDvwI//xGrI/oOQkAi4YEew==", + "dev": true, + "requires": { + "@babel/runtime": "^7.6.2", + "@sheerun/mutationobserver-shim": "^0.3.2", + "@types/testing-library__dom": "^6.0.0", + "aria-query": "3.0.0", + "pretty-format": "^24.9.0", + "wait-for-expect": "^3.0.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz", + "integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + } + } + }, + "@testing-library/react": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-9.4.0.tgz", + "integrity": "sha512-XdhDWkI4GktUPsz0AYyeQ8M9qS/JFie06kcSnUVcpgOwFjAu9vhwR83qBl+lw9yZWkbECjL8Hd+n5hH6C0oWqg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.7.6", + "@testing-library/dom": "^6.11.0", + "@types/testing-library__react": "^9.1.2" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.3.tgz", + "integrity": "sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + } + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true + }, + "@types/ansi-regex": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/ansi-regex/-/ansi-regex-4.0.0.tgz", + "integrity": "sha512-r1W316vjsZXn1/csLC4HcCJs6jIHIzksHJd7xx+Dl+PAb0S2Dh9cR8ZsIMEfGmbBtP7JNWlf2KKahSkDP6rg3g==", + "dev": true + }, + "@types/anymatch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", + "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", + "dev": true + }, + "@types/backbone": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@types/backbone/-/backbone-1.4.1.tgz", + "integrity": "sha512-KYfGuQy4d2vvYXbn0uHFZ6brFLndatTMomxBlljpbWf4kFpA3BG/6LA3ec+J9iredrX6eAVI7sm9SVAvwiIM6g==", + "dev": true, + "requires": { + "@types/jquery": "*", + "@types/underscore": "*" + } + }, + "@types/body-parser": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz", + "integrity": "sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", + "dev": true + }, + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, + "@types/chai-arrays": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/chai-arrays/-/chai-arrays-1.0.2.tgz", + "integrity": "sha512-/kgYvj5Pwiv/bOlJ6c5GlRF/W6lUGSLrpQGl/7Gg6w7tvBYcf0iF91+wwyuwDYGO2zM0wNpcoPixZVif8I/r6g==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/chai-as-promised": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz", + "integrity": "sha512-MFiW54UOSt+f2bRw8J7LgQeIvE/9b4oGvwU7XW30S9QGAiHGnU/fmiOprsyMkdmH2rl8xSPc0/yrQw8juXU6bQ==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/cheerio": { + "version": "0.22.12", + "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.12.tgz", + "integrity": "sha512-aczowyAJNfrkBV+HS8DyAA87OnvkqGrrOmm5s7V6Jbgimzv/1ZoAy91cLJX8GQrUS60KufD7EIzA2LbK8HV4hg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/clean-css": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-A1HQhQ0hkvqqByJMgg+Wiv9p9XdoYEzuwm11SVo1mX2/4PSdhjcrUlilJQoqLscIheC51t1D5g+EFWCXZ2VTQQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/clone": { + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/@types/clone/-/clone-0.1.30.tgz", + "integrity": "sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ=", + "dev": true + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-OU2+C7X+5Gs42JZzXoto7yOQ0A0=", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha512-aRnpPa7ysx3aNW60hTiCtLHlQaIFsXFCgQlpakNgDNVFzbtusSY8PwjAQgRWfSk0ekNoBjO51eQRB6upA9uuyw==", + "dev": true + }, + "@types/copy-webpack-plugin": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@types/copy-webpack-plugin/-/copy-webpack-plugin-4.4.4.tgz", + "integrity": "sha512-D8NyCMfHFWi638Cn5gi88mELGrISQdzDAcmOk4j70bzm0kLey99Zp5xsFsL44qPi+a22tcB39U5jiJiV2XMd9A==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*", + "@types/webpack": "*" + } + }, + "@types/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", + "dev": true + }, + "@types/decompress": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.3.tgz", + "integrity": "sha512-W24e3Ycz1UZPgr1ZEDHlK4XnvOr+CpJH3qNsFeqXwwlW/9END9gxn3oJSsp7gYdiQxrXUHwUUd3xuzVz37MrZQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@types/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==", + "dev": true + }, + "@types/del": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/del/-/del-3.0.1.tgz", + "integrity": "sha512-y6qRq6raBuu965clKgx6FHuiPu3oHdtmzMPXi8Uahsjdq1L6DL5fS/aY5/s71YwM7k6K1QIWvem5vNwlnNGIkQ==", + "dev": true, + "requires": { + "@types/glob": "*" + } + }, + "@types/diff-match-patch": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz", + "integrity": "sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A==", + "dev": true + }, + "@types/dom4": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/dom4/-/dom4-2.0.1.tgz", + "integrity": "sha512-kSkVAvWmMZiCYtvqjqQEwOmvKwcH+V4uiv3qPQ8pAh1Xl39xggGEo8gHUqV4waYGHezdFw0rKBR8Jt0CrQSDZA==", + "dev": true + }, + "@types/download": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@types/download/-/download-6.2.4.tgz", + "integrity": "sha512-Lo5dy3ai6LNnbL663sgdzqL1eib11u1yKH6w3v3IXEOO4kRfQpMn1qWUTaumcHLACjFp1RcBx9tUXEvJoR3vcA==", + "dev": true, + "requires": { + "@types/decompress": "*", + "@types/got": "^8", + "@types/node": "*" + } + }, + "@types/enzyme": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.2.tgz", + "integrity": "sha512-tAFqfBVAxaxOCCcCpFrv6yFDg/hwL1KtJc7dsOaO+BBdSppTcyRP7jO2ze5va7NDa8iSFfAaZ9NeuybmMulu0w==", + "dev": true, + "requires": { + "@types/cheerio": "*", + "@types/react": "*" + } + }, + "@types/enzyme-adapter-react-16": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.5.tgz", + "integrity": "sha512-K7HLFTkBDN5RyRmU90JuYt8OWEY2iKUn43SDWEoBOXd/PowUWjLZ3Q6qMBiQuZeFYK/TOstaZxsnI0fXoAfLpg==", + "dev": true, + "requires": { + "@types/enzyme": "*" + } + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/event-stream": { + "version": "3.3.34", + "resolved": "https://registry.npmjs.org/@types/event-stream/-/event-stream-3.3.34.tgz", + "integrity": "sha512-LLiivgWKii4JeMzFy3trrxqkRrVSdue8WmbXyHuSJLwNrhIQU5MTrc65jhxEPwMyh5HR1xevSdD+k2nnSRKw9g==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true + }, + "@types/express": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.2.tgz", + "integrity": "sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.0.tgz", + "integrity": "sha512-Xnub7w57uvcBqFdIGoRg1KhNOeEj0vB6ykUM7uFWyxvbdE89GFyqgmUcanAriMr4YOxNFZBAWkfcWIb4WBPt3g==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha512-mky/O83TXmGY39P1H9YbUpjV6l6voRYlufqfFCvel8l1phuy8HRjdWc1rrPuN53ITBJlbyMSV6z3niOySO5pgQ==", + "dev": true + }, + "@types/form-data": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", + "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/fs-extra": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.1.0.tgz", + "integrity": "sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha512-TiNg8R1kjDde5Pub9F9vCwZA/BNW9HeXP5b9j7Qucqncy/McfPZ6xze/EyBdXS5FhMIGN6Fx3vg75l5KHy3V1Q==", + "dev": true + }, + "@types/glob": { + "version": "5.0.36", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.36.tgz", + "integrity": "sha512-KEzSKuP2+3oOjYYjujue6Z3Yqis5HKA1BsIC+jZ1v3lrRNdsqyNNtX0rQf6LSuI4DJJ2z5UV//zBZCcvM0xikg==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/got": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/@types/got/-/got-8.3.5.tgz", + "integrity": "sha512-AaXSrIF99SjjtPVNmCmYb388HML+PKEJb/xmj4SbL2ZO0hHuETZZzyDIKfOqaEoAHZEuX4sC+FRFrHYJoIby6A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dev": true, + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "@types/html-minifier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@types/html-minifier/-/html-minifier-3.5.3.tgz", + "integrity": "sha512-j1P/4PcWVVCPEy5lofcHnQ6BtXz9tHGiFPWzqm7TtGuWZEfCHEP446HlkSNc9fQgNJaJZ6ewPtp2aaFla/Uerg==", + "dev": true, + "requires": { + "@types/clean-css": "*", + "@types/relateurl": "*", + "@types/uglify-js": "*" + } + }, + "@types/html-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-in9rViBsTRB4ZApndZ12It68nGzSMHVK30JD7c49iLIHMFeTPbP7I7wevzMv7re2o0k5TlU6Ry/beyrmgWX7Bg==", + "dev": true, + "requires": { + "@types/html-minifier": "*", + "@types/tapable": "*", + "@types/webpack": "*" + } + }, + "@types/iconv-lite": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/iconv-lite/-/iconv-lite-0.0.1.tgz", + "integrity": "sha1-qjuL2ivlErGuCgV7lC6GnDcKVWk=", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", + "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", + "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/jquery": { + "version": "1.10.35", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-1.10.35.tgz", + "integrity": "sha512-SVtqEcudm7yjkTwoRA1gC6CNMhGDdMx4Pg8BPdiqI7bXXdCn1BPmtxgeWYQOgDxrq53/5YTlhq5ULxBEAlWIBg==", + "dev": true + }, + "@types/jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-XHMNZFQ0Ih3A4/NTWAO15+OsQafPKnQCanN0FYGbsTM/EoI5EoEAvvkF51/DQC2BT5low4tomp7k2RLMlriA5Q==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^4.0.0" + } + }, + "@types/json-schema": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, + "@types/loadable__component": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@types/loadable__component/-/loadable__component-5.10.0.tgz", + "integrity": "sha512-AaDP1VxV3p7CdPOtOTl3ALgQ6ES4AxJKO9UGj9vJonq/w2yERxwdzFiWNQFh9fEDXEzjxujBlM2RmSJtHV1/pA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/loader-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/loader-utils/-/loader-utils-1.1.3.tgz", + "integrity": "sha512-euKGFr2oCB3ASBwG39CYJMR3N9T0nanVqXdiH7Zu/Nqddt6SmFRxytq/i2w9LQYNQekEtGBz+pE3qG6fQTNvRg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/webpack": "*" + } + }, + "@types/lodash": { + "version": "4.14.136", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.136.tgz", + "integrity": "sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA==", + "dev": true + }, + "@types/md5": { + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.1.33.tgz", + "integrity": "sha512-8+X960EtKLoSblhauxLKy3zzotagjoj3Jt1Tx9oaxUdZEPIBl+mkrUz6PNKpzJgkrKSN9YgkWTA29c0KnLshmA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/memoize-one": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/memoize-one/-/memoize-one-4.1.1.tgz", + "integrity": "sha512-+9djKUUn8hOyktLCfCy4hLaIPgDNovaU36fsnZe9trFHr6ddlbIn2q0SEsnkCkNR+pBWEU440Molz/+Mpyf+gQ==", + "dev": true + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", + "dev": true + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, + "@types/nock": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz", + "integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "10.17.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.29.tgz", + "integrity": "sha512-zLo9rjUeQ5+QVhOufDwrb3XKyso31fJBJnk9wUUQIBDExF/O4LryvpOfozfUaxgqifTnlt7FyqsAPXUq5yFZSA==", + "dev": true + }, + "@types/node-fetch": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", + "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "@types/pdfkit": { + "version": "0.7.36", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.7.36.tgz", + "integrity": "sha512-9eRA6MuW+n78yU3HhoIrDxjyAX2++B5MpLDYqHOnaRTquCw+5sYXT+QN8E1eSaxvNUwlRfU3tOm4UzTeGWmBqg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/promisify-node": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@types/promisify-node/-/promisify-node-0.4.0.tgz", + "integrity": "sha1-3MceY8Cr9oYbrn0S9swzlceDk2s=", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz", + "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==", + "dev": true + }, + "@types/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-Jugo5V/1bS0fRhy2z8+cUAHEyWOATaz4rbyLVvcFs7+dXp5HfwpEwzF1Q11bB10ApUqHf+yTauxI0UXQDwGrbA==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/react": { + "version": "16.8.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.23.tgz", + "integrity": "sha512-abkEOIeljniUN9qB5onp++g0EY38h7atnDHxwKUFz1r3VH1+yG1OKi2sNPTyObL40goBmfKFpdii2lEzwLX1cA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "@types/react-dom": { + "version": "16.8.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.8.4.tgz", + "integrity": "sha512-eIRpEW73DCzPIMaNBDP5pPIpK1KXyZwNgfxiVagb5iGiz6da+9A5hslSX6GAQKdO7SayVCS/Fr2kjqprgAvkfA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-json-tree": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/@types/react-json-tree/-/react-json-tree-0.6.11.tgz", + "integrity": "sha512-HP0Sf0ZHjCi1FHLJxh/pLaxaevEW6ILlV2C5Dn3EZFTkLjWkv+EVf/l/zvtmoU9ZwuO/3TKVeWK/700UDxunTw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-redux": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.5.tgz", + "integrity": "sha512-ZoNGQMDxh5ENY7PzU7MVonxDzS1l/EWiy8nUhDqxFqUZn4ovboCyvk4Djf68x6COb7vhGTKjyjxHxtFdAA5sUA==", + "dev": true, + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "@types/react-virtualized": { + "version": "9.21.2", + "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.2.tgz", + "integrity": "sha512-Q6geJaDd8FlBw3ilD4ODferTyVtYAmDE3d7+GacfwN0jPt9rD9XkeuPjcHmyIwTrMXuLv1VIJmRxU9WQoQFBJw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, + "@types/redux-logger": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.7.tgz", + "integrity": "sha512-oV9qiCuowhVR/ehqUobWWkXJjohontbDGLV88Be/7T4bqMQ3kjXwkFNL7doIIqlbg3X2PC5WPziZ8/j/QHNQ4A==", + "dev": true, + "requires": { + "redux": "^3.6.0" + }, + "dependencies": { + "redux": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", + "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "dev": true, + "requires": { + "lodash": "^4.2.1", + "lodash-es": "^4.2.1", + "loose-envify": "^1.1.0", + "symbol-observable": "^1.0.3" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + } + } + }, + "@types/relateurl": { + "version": "0.2.28", + "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz", + "integrity": "sha1-a9p9uGU/piZD9e5p6facEaOS46Y=", + "dev": true + }, + "@types/request": { + "version": "2.48.1", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", + "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", + "dev": true, + "requires": { + "@types/caseless": "*", + "@types/form-data": "*", + "@types/node": "*", + "@types/tough-cookie": "*" + } + }, + "@types/semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", + "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/shortid": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", + "integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=", + "dev": true + }, + "@types/sinon": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.1.tgz", + "integrity": "sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==", + "dev": true + }, + "@types/sinonjs__fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz", + "integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==", + "dev": true + }, + "@types/socket.io": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.4.tgz", + "integrity": "sha512-cI98INy7tYnweTsUlp8ocveVdAxENUThO0JsLSCs51cjOP2yV5Mqo5QszMDPckyRRA+PO6+wBgKvGvHUCc23TQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "@types/stack-trace": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.29.tgz", + "integrity": "sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g==", + "dev": true + }, + "@types/superagent": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", + "integrity": "sha512-9KhCkyXv268A2nZ1Wvu7rQWM+BmdYUVkycFeNnYrUL5Zwu7o8wPQ3wBfW59dDP+wuoxw0ww8YKgTNv8j/cgscA==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/tapable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.4.tgz", + "integrity": "sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==", + "dev": true + }, + "@types/tcp-port-used": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/tcp-port-used/-/tcp-port-used-1.0.0.tgz", + "integrity": "sha512-UbspV5WZNhfM55HyvLEFyVc5n6K6OKuKep0mzvsgoUXQU1FS42GbePjreBnTCoKXfNzK/3/RJVCRlUDTuszFPg==" + }, + "@types/temp": { + "version": "0.8.34", + "resolved": "https://registry.npmjs.org/@types/temp/-/temp-0.8.34.tgz", + "integrity": "sha512-oLa9c5LHXgS6UimpEVp08De7QvZ+Dfu5bMQuWyMhf92Z26Q10ubEMOWy9OEfUdzW7Y/sDWVHmUaLFtmnX/2j0w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/testing-library__dom": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-6.11.1.tgz", + "integrity": "sha512-ImChHtQqmjwraRLqBC2sgSQFtczeFvBmBcfhTYZn/3KwXbyD07LQykEQ0xJo7QHc1GbVvf7pRyGaIe6PkCdxEw==", + "dev": true, + "requires": { + "pretty-format": "^24.3.0" + } + }, + "@types/testing-library__react": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@types/testing-library__react/-/testing-library__react-9.1.2.tgz", + "integrity": "sha512-CYaMqrswQ+cJACy268jsLAw355DZtPZGt3Jwmmotlcu8O/tkoXBI6AeZ84oZBJsIsesozPKzWzmv/0TIU+1E9Q==", + "dev": true, + "requires": { + "@types/react-dom": "*", + "@types/testing-library__dom": "*" + } + }, + "@types/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", + "dev": true + }, + "@types/tough-cookie": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", + "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==", + "dev": true + }, + "@types/uglify-js": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", + "integrity": "sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + } + }, + "@types/underscore": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.9.4.tgz", + "integrity": "sha512-CjHWEMECc2/UxOZh0kpiz3lEyX2Px3rQS9HzD20lxMvx571ivOBQKeLnqEjxUY0BMgp6WJWo/pQLRBwMW5v4WQ==", + "dev": true + }, + "@types/untildify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/untildify/-/untildify-3.0.0.tgz", + "integrity": "sha512-FTktI3Y1h+gP9GTjTvXBP5v8xpH4RU6uS9POoBcGy4XkS2Np6LNtnP1eiNNth4S7P+qw2c/rugkwBasSHFzJEg==", + "dev": true + }, + "@types/uuid": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.5.tgz", + "integrity": "sha512-MNL15wC3EKyw1VLF+RoVO4hJJdk9t/Hlv3rt1OL65Qvuadm4BYo6g9ZJQqoq7X8NBFSsQXgAujWciovh2lpVjA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/vscode": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.48.0.tgz", + "integrity": "sha512-sZJKzsJz1gSoFXcOJWw3fnKl2sseUgZmvB4AJZS+Fea+bC/jfGPVhmFL/FfQHld/TKtukVONsmoD3Pkyx9iadg==", + "dev": true + }, + "@types/vscode-notebook-renderer": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@types/vscode-notebook-renderer/-/vscode-notebook-renderer-1.48.0.tgz", + "integrity": "sha512-V7ifnlOgvVP3Ud1mM6EO98+oWRk9bt59INUL3Q0o06cAw45jOfos8pAFjDyCEHd1n6d3Q0ObPzdB0jgg8TdgDA==", + "dev": true + }, + "@types/webpack": { + "version": "4.4.34", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.4.34.tgz", + "integrity": "sha512-GnEBgjHsfO1M7DIQ0dAupSofcmDItE3Zsu3reK8SQpl/6N0rtUQxUmQzVFAS5ou/FGjsYKjXAWfItLZ0kNFTfQ==", + "dev": true, + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "source-map": "^0.6.0" + } + }, + "@types/webpack-bundle-analyzer": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.13.1.tgz", + "integrity": "sha512-9M9jingj0izx1VfglYYJ+dvd0YCMlFgtrSCeb+C3VIQP8hmTXGJ5qqVeqiBnv0ffMyeKWqyij4K2F4VBcazQNw==", + "dev": true, + "requires": { + "@types/webpack": "*" + } + }, + "@types/webpack-sources": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.6.tgz", + "integrity": "sha512-FtAWR7wR5ocJ9+nP137DV81tveD/ZgB1sadnJ/axUGM3BUVfRPx8oQNMtv3JNfTeHx3VP7cXiyfR/jmtEsVHsQ==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.6.1" + } + }, + "@types/winreg": { + "version": "1.2.30", + "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.30.tgz", + "integrity": "sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg=", + "dev": true + }, + "@types/ws": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz", + "integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==", + "dev": true, + "requires": { + "@types/events": "*", + "@types/node": "*" + } + }, + "@types/xml2js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.4.tgz", + "integrity": "sha512-O6Xgai01b9PB3IGA0lRIp1Ex3JBcxGDhdO0n3NIIpCyDOAjxcIGQFmkvgJpP8anTrthxOUQjBfLdRRi0Zn/TXA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.5.tgz", + "integrity": "sha512-CF/+sxTO7FOwbIRL4wMv0ZYLCRfMid2HQpzDRyViH7kSpfoAFiMdGqKIxb1PxWfjtQXQhnQuD33lvRHNwr809Q==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", + "dev": true + }, + "@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", + "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "3.10.1", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", + "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", + "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.10.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", + "dev": true, + "requires": { + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "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" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "@webassemblyjs/ast": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", + "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", + "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", + "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", + "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", + "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", + "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", + "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "mamacro": "^0.0.3" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", + "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", + "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", + "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", + "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", + "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", + "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/helper-wasm-section": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-opt": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", + "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", + "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", + "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", + "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/floating-point-hex-parser": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-code-frame": "1.8.5", + "@webassemblyjs/helper-fsm": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", + "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "abab": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", + "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==" + }, + "acorn-globals": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz", + "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==", + "dev": true, + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + } + } + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "acorn-node": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.7.0.tgz", + "integrity": "sha512-XhahLSsCB6X6CJbe+uNu3Mn9sJBNFxtBN9NLgAOQovfS6Kh0lDUtmlclhjn9CvEK7A7YyRU13PXlNcpSiLI9Yw==", + "requires": { + "acorn": "^6.1.1", + "acorn-dynamic-import": "^4.0.0", + "acorn-walk": "^6.1.1", + "xtend": "^4.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" + } + } + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" + }, + "address": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/address/-/address-1.0.3.tgz", + "integrity": "sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg==", + "dev": true + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "airbnb-prop-types": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.13.2.tgz", + "integrity": "sha512-2FN6DlHr6JCSxPPi25EnqGaXC4OC3/B3k1lCd6MMYrZ51/Gf/1qDfaR+JElzWa+Tl7cY2aYOlsYJGFeQyVHIeQ==", + "dev": true, + "requires": { + "array.prototype.find": "^2.0.4", + "function.prototype.name": "^1.1.0", + "has": "^1.0.3", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object.assign": "^4.1.0", + "object.entries": "^1.1.0", + "prop-types": "^15.7.2", + "prop-types-exact": "^1.2.0", + "react-is": "^16.8.6" + } + }, + "ajv": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.1.tgz", + "integrity": "sha512-w1YQaVGNC6t2UCPjEawK/vo/dG8OOrVtUmhBT1uJJYxbl5kU2Tj3v6LGqBcsysN1yhuCStJCCA3GqdvKY8sqXQ==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "optional": true + }, + "anser": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.8.tgz", + "integrity": "sha512-tVHucTCKIt9VRrpQKzPtOlwm/3AmyQ7J+QE29ixFnvuE2hm83utEVrN7jJapYkHV6hI0HOHkEX9TOMCzHtwvuA==", + "dev": true + }, + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "ansi-to-html": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.6.11.tgz", + "integrity": "sha512-88XZtrcwrfkyn6fGstHnkaF1kl7hGtNCYh4vSmItgEV+6JnQHryDBf7udF4f2RhTRQmYvJvPcTtqgaqrxzc9oA==", + "dev": true, + "requires": { + "entities": "^1.1.1" + } + }, + "ansi-to-react": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/ansi-to-react/-/ansi-to-react-3.3.5.tgz", + "integrity": "sha512-uAI8NOh+/5PC1poTnLwhuO7whaxPst1lZCeq+7P7hlP0A6GRXjXu1f5qprTwT3NHtjIWyMcFJAL0Im0HyB2XeQ==", + "dev": true, + "requires": { + "@babel/runtime-corejs2": "^7.0.0", + "anser": "^1.4.1", + "babel-runtime": "^6.26.0", + "escape-carriage": "^1.2.0" + } + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", + "dev": true, + "requires": { + "buffer-equal": "^1.0.0" + }, + "dependencies": { + "buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", + "dev": true + } + } + }, + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, + "applicationinsights": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.7.4.tgz", + "integrity": "sha512-XFLsNlcanpjFhHNvVWEfcm6hr7lu9znnb6Le1Lk5RE03YUV9X2B2n2MfM4kJZRrUdV+C0hdHxvWyv+vWoLfY7A==", + "requires": { + "cls-hooked": "^4.2.2", + "continuation-local-storage": "^3.2.1", + "diagnostic-channel": "0.2.0", + "diagnostic-channel-publishers": "^0.3.3" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "arch": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", + "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==" + }, + "archive-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", + "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "dev": true, + "requires": { + "file-type": "^4.2.0" + }, + "dependencies": { + "file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", + "dev": true + } + } + }, + "archiver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.0.0.tgz", + "integrity": "sha512-5QeR6Xc5hSA9X1rbQfcuQ6VZuUXOaEdB65Dhmk9duuRJHYif/ZyJfuyJqsQrj34PFjU5emv5/MmfgA8un06onw==", + "dev": true, + "requires": { + "archiver-utils": "^2.0.0", + "async": "^2.0.0", + "buffer-crc32": "^0.2.1", + "glob": "^7.0.0", + "readable-stream": "^2.0.0", + "tar-stream": "^1.5.0", + "zip-stream": "^2.0.1" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "archiver-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.0.0.tgz", + "integrity": "sha512-JRBgcVvDX4Mwu2RBF8bBaHcQCSxab7afsxAPYDQ5W+19quIPP5CfKE7Ql+UHs9wYvwsaNR8oDuhtf5iqrKmzww==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "graceful-fs": "^4.1.0", + "lazystream": "^1.0.0", + "lodash.assign": "^4.2.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.toarray": "^4.4.0", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "argv": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", + "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", + "dev": true + }, + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-filter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", + "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", + "dev": true, + "requires": { + "make-iterator": "^1.0.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", + "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", + "dev": true, + "requires": { + "make-iterator": "^1.0.0" + } + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", + "dev": true + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=" + }, + "array-includes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + } + } + }, + "array-initial": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", + "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", + "dev": true, + "requires": { + "array-slice": "^1.0.0", + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "array-last": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", + "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", + "dev": true, + "requires": { + "is-number": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, + "array-sort": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", + "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", + "dev": true, + "requires": { + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "array.prototype.find": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.0.tgz", + "integrity": "sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.13.0" + } + }, + "array.prototype.flat": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz", + "integrity": "sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.10.0", + "function-bind": "^1.1.1" + } + }, + "array.prototype.flatmap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz", + "integrity": "sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + } + } + }, + "array.prototype.map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz", + "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.4" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + } + } + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "ast-transform": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/ast-transform/-/ast-transform-0.0.0.tgz", + "integrity": "sha1-dJRAWIh9goPhidlUYAlHvJj+AGI=", + "requires": { + "escodegen": "~1.2.0", + "esprima": "~1.0.4", + "through": "~2.3.4" + }, + "dependencies": { + "escodegen": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.2.0.tgz", + "integrity": "sha1-Cd55Z3kcyVi3+Jot220jRRrzJ+E=", + "requires": { + "esprima": "~1.0.4", + "estraverse": "~1.5.0", + "esutils": "~1.0.0", + "source-map": "~0.1.30" + } + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" + }, + "estraverse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=" + }, + "esutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=" + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "ast-types": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz", + "integrity": "sha1-kC0uDWDQcb3NRtwRXhgJ7RHBOKk=" + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", + "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", + "requires": { + "lodash": "^4.17.11" + } + }, + "async-done": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + } + } + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "async-hook-jl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", + "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", + "requires": { + "stack-chain": "^1.3.7" + } + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "async-listener": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", + "requires": { + "semver": "^5.3.0", + "shimmer": "^1.1.0" + } + }, + "async-settle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", + "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "dev": true, + "requires": { + "async-done": "^1.2.2" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "autoprefixer": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.2.6.tgz", + "integrity": "sha512-Iq8TRIB+/9eQ8rbGhcP7ct5cYb/3qjNYAR2SnzLCEcwF6rvVOax8+9+fccgXk4bEhQGjOZd5TLhsksmAdsbGqQ==", + "dev": true, + "requires": { + "browserslist": "^2.11.3", + "caniuse-lite": "^1.0.30000805", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^6.0.17", + "postcss-value-parser": "^3.2.3" + }, + "dependencies": { + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000792", + "electron-to-chromium": "^1.3.30" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "axe-core": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.5.5.tgz", + "integrity": "sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==", + "dev": true + }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", + "dev": true + }, + "azure-devops-node-api": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-7.2.0.tgz", + "integrity": "sha512-pMfGJ6gAQ7LRKTHgiRF+8iaUUeGAI0c8puLaqHLc7B8AR7W6GJLozK9RFeUHFjEGybC9/EB3r67WPd7e46zQ8w==", + "dev": true, + "requires": { + "os": "0.1.1", + "tunnel": "0.0.4", + "typed-rest-client": "1.2.0", + "underscore": "1.8.3" + } + }, + "azure-storage": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/azure-storage/-/azure-storage-2.10.3.tgz", + "integrity": "sha512-IGLs5Xj6kO8Ii90KerQrrwuJKexLgSwYC4oLWmc11mzKe7Jt2E5IVg+ZQ8K53YWZACtVTMBNO3iGuA+4ipjJxQ==", + "requires": { + "browserify-mime": "~1.2.9", + "extend": "^3.0.2", + "json-edm-parser": "0.1.2", + "md5.js": "1.3.4", + "readable-stream": "~2.0.0", + "request": "^2.86.0", + "underscore": "~1.8.3", + "uuid": "^3.0.0", + "validator": "~9.4.1", + "xml2js": "0.2.8", + "xmlbuilder": "^9.0.7" + }, + "dependencies": { + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "xml2js": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz", + "integrity": "sha1-m4FpCTFjH/CdGVdUn69U9PmAs8I=", + "requires": { + "sax": "0.5.x" + } + } + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-loader": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", + "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==", + "dev": true, + "requires": { + "find-cache-dir": "^2.0.0", + "loader-utils": "^1.0.2", + "mkdirp": "^0.5.1", + "pify": "^4.0.1" + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", + "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-emotion": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz", + "integrity": "sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/babel-utils": "^0.6.4", + "@emotion/hash": "^0.6.2", + "@emotion/memoize": "^0.6.1", + "@emotion/stylis": "^0.7.0", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "find-root": "^1.1.0", + "mkdirp": "^0.5.1", + "source-map": "^0.5.7", + "touch": "^2.0.1" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "babel-plugin-inline-json-import": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/babel-plugin-inline-json-import/-/babel-plugin-inline-json-import-0.3.2.tgz", + "integrity": "sha512-QNNJx08KjmMT25Cw7rAPQ6dlREDPiZGDyApHL8KQ9vrQHbrr4PTi7W8g1tMMZPz0jEMd39nx/eH7xjnDNxq5sA==", + "dev": true, + "requires": { + "decache": "^4.5.1" + } + }, + "babel-plugin-macros": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.6.1.tgz", + "integrity": "sha512-6W2nwiXme6j1n2erPOnmRiWfObUhWH7Qw1LMi9XZy8cj+KtESu3T6asZvtk5bMQQjX8te35o7CFueiSdL/2NmQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.4.2", + "cosmiconfig": "^5.2.0", + "resolve": "^1.10.0" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", + "dev": true + }, + "babel-plugin-transform-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", + "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + }, + "dependencies": { + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + }, + "dependencies": { + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + } + } + }, + "bach": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", + "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", + "dev": true, + "requires": { + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" + } + }, + "backbone": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.2.3.tgz", + "integrity": "sha1-wiz9B/yG676uYdGJKe0RXpmdZbk=", + "dev": true, + "requires": { + "underscore": ">=1.7.0" + } + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", + "dev": true + }, + "bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base16": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", + "integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=", + "dev": true + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", + "dev": true + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "dev": true, + "requires": { + "callsite": "1.0.0" + } + }, + "bfj": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz", + "integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "check-types": "^8.0.3", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=", + "dev": true + }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", + "dev": true + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "bluebird": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "bootstrap": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", + "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==", + "dev": true + }, + "bootstrap-less": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/bootstrap-less/-/bootstrap-less-3.3.8.tgz", + "integrity": "sha1-cfKd1af//t/onxYFu63+CjONrlM=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "brfs": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brfs/-/brfs-2.0.2.tgz", + "integrity": "sha512-IrFjVtwu4eTJZyu8w/V2gxU7iLTtcHih67sgEdzrhjLBMHp2uYefUBfdM4k2UvcuWMgV7PQDZHSLeNWnLFKWVQ==", + "requires": { + "quote-stream": "^1.0.1", + "resolve": "^1.1.5", + "static-module": "^3.0.2", + "through2": "^2.0.0" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "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" + } + }, + "static-module": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/static-module/-/static-module-3.0.3.tgz", + "integrity": "sha512-RDaMYaI5o/ym0GkCqL/PlD1Pn216omp8fY81okxZ6f6JQxWW5tptOw9reXoZX85yt/scYvbWIt6uoszeyf+/MQ==", + "requires": { + "acorn-node": "^1.3.0", + "concat-stream": "~1.6.0", + "convert-source-map": "^1.5.1", + "duplexer2": "~0.1.4", + "escodegen": "~1.9.0", + "has": "^1.0.1", + "magic-string": "^0.22.4", + "merge-source-map": "1.0.4", + "object-inspect": "~1.4.0", + "readable-stream": "~2.3.3", + "scope-analyzer": "^2.0.1", + "shallow-copy": "~0.0.1", + "static-eval": "^2.0.2", + "through2": "~2.0.3" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "brotli": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.2.tgz", + "integrity": "sha1-UlqcrU/LqWR119OI9q7LE+7VL0Y=", + "requires": { + "base64-js": "^1.1.2" + } + }, + "browser-process-hrtime": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "dev": true + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + } + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-mime": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz", + "integrity": "sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8=" + }, + "browserify-optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-optional/-/browserify-optional-1.0.1.tgz", + "integrity": "sha1-HhNyLP3g2F8SFnbCpyztUzoBiGk=", + "requires": { + "ast-transform": "0.0.0", + "ast-types": "^0.7.0", + "browser-resolve": "^1.8.1" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + }, + "dependencies": { + "pako": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "dev": true + } + } + }, + "browserslist": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.4.tgz", + "integrity": "sha512-ErJT8qGfRt/VWHSr1HeqZzz50DvxHtr1fVL1m5wf20aGrG8e1ce8fpZ2EjZEfs09DDZYSvtRaDlMpWslBf8Low==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000981", + "electron-to-chromium": "^1.3.188", + "node-releases": "^1.1.25" + } + }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, + "buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "buffer-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-json/-/buffer-json-2.0.0.tgz", + "integrity": "sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "cacache": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", + "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "cache-loader": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-4.1.0.tgz", + "integrity": "sha512-ftOayxve0PwKzBF/GLsZNC9fJBXl8lkZE3TOsjkboHfVHVkL39iUEs1FO07A33mizmci5Dudt38UZrrYXDtbhw==", + "dev": true, + "requires": { + "buffer-json": "^2.0.0", + "find-cache-dir": "^3.0.0", + "loader-utils": "^1.2.3", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "schema-utils": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "schema-utils": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz", + "integrity": "sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "dev": true, + "requires": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + }, + "dependencies": { + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "dev": true + } + } + }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", + "dev": true + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-api": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-2.0.0.tgz", + "integrity": "sha1-sd21pZZrFvSNxJmERNS7xsfZ2DQ=", + "dev": true, + "requires": { + "browserslist": "^2.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + }, + "dependencies": { + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000792", + "electron-to-chromium": "^1.3.30" + } + } + } + }, + "caniuse-lite": { + "version": "1.0.30000983", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000983.tgz", + "integrity": "sha512-/llD1bZ6qwNkt41AsvjsmwNOoA4ZB+8iqmf5LVyeSXuBODT/hAMFNVOh84NdUzoiYiSKqo5vQ3ZzeYHSi/olDQ==", + "dev": true + }, + "canvas": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.0.tgz", + "integrity": "sha512-bEO9f1ThmbknLPxCa8Es7obPlN9W3stB1bo7njlhOFKIdUTldeTqXCh9YclCPAi2pSQs84XA0jq/QEZXSzgyMw==", + "dev": true, + "requires": { + "nan": "^2.14.0", + "node-pre-gyp": "^0.11.0", + "simple-get": "^3.0.3" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "caw": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", + "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", + "dev": true, + "requires": { + "get-proxy": "^2.0.0", + "isurl": "^1.0.0-alpha5", + "tunnel-agent": "^0.6.0", + "url-to-options": "^1.0.1" + } + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chai-arrays": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chai-arrays/-/chai-arrays-2.0.0.tgz", + "integrity": "sha512-jWAvZu1BV8tL3pj0iosBECzzHEg+XB1zSnMjJGX83bGi/1GlGdDO7J/A0sbBBS6KJT0FVqZIzZW9C6WLiMkHpQ==", + "dev": true + }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, + "chai-http": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/chai-http/-/chai-http-4.3.0.tgz", + "integrity": "sha512-zFTxlN7HLMv+7+SPXZdkd5wUlK+KxH6Q7bIEMiEx0FK3zuuMqL7cwICAQ0V1+yYRozBburYuxN1qZstgHpFZQg==", + "dev": true, + "requires": { + "@types/chai": "4", + "@types/superagent": "^3.8.3", + "cookiejar": "^2.1.1", + "is-ip": "^2.0.0", + "methods": "^1.1.2", + "qs": "^6.5.1", + "superagent": "^3.7.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true + }, + "character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true + }, + "character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "check-types": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", + "integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==", + "dev": true + }, + "cheerio": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", + "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", + "dev": true, + "requires": { + "css-select": "~1.2.0", + "dom-serializer": "~0.1.1", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash": "^4.15.0", + "parse5": "^3.0.1" + }, + "dependencies": { + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "dev": true, + "requires": { + "@types/node": "*" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "chownr": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", + "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, + "clean-css": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + } + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", + "dev": true + }, + "clone-deep": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", + "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.0", + "shallow-clone": "^1.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + } + } + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "cls-hooked": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", + "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", + "requires": { + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.1" + } + }, + "clsx": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.0.4.tgz", + "integrity": "sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg==", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "codecov": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.7.1.tgz", + "integrity": "sha512-JHWxyPTkMLLJn9SmKJnwAnvY09kg2Os2+Ux+GG7LwZ9g8gzDDISpIN5wAsH1UBaafA/yGcd3KofMaorE8qd6Lw==", + "dev": true, + "requires": { + "argv": "0.0.2", + "ignore-walk": "3.0.3", + "js-yaml": "3.13.1", + "teeny-request": "6.0.1", + "urlgrey": "0.4.4" + }, + "dependencies": { + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + } + } + }, + "codemirror": { + "version": "5.47.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.47.0.tgz", + "integrity": "sha512-kV49Fr+NGFHFc/Imsx6g180hSlkGhuHxTSDDmDHOuyln0MQYFLixDY4+bFkBVeCEiepYfDimAF/e++9jPJk4QA==", + "dev": true + }, + "collapse-white-space": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", + "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", + "dev": true + }, + "collection-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", + "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", + "dev": true, + "requires": { + "arr-map": "^2.0.2", + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + } + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true + }, + "colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" + }, + "colors": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==" + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true + }, + "commandpost": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.4.0.tgz", + "integrity": "sha512-aE2Y4MTFJ870NuB/+2z1cXBhSBBzRydVVjzhFC4gtenEhpnj15yu0qptWGJsO9YGrcPZ3ezX8AWb1VA391MKpQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "compare-module-exports": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/compare-module-exports/-/compare-module-exports-2.1.0.tgz", + "integrity": "sha512-3Lc0sTIuX1jmY2K2RrXRJOND6KsRTX2D4v3+eu1PDptsuJZVK4LZc852eZa9I+avj0NrUKlTNgqvccNOH6mbGg==", + "dev": true + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "compress-commons": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", + "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", + "dev": true, + "requires": { + "buffer-crc32": "^0.2.1", + "crc32-stream": "^2.0.0", + "normalize-path": "^2.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "config-chain": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "confusing-browser-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz", + "integrity": "sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "continuation-local-storage": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", + "requires": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" + } + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "copy-props": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz", + "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", + "dev": true, + "requires": { + "each-props": "^1.3.0", + "is-plain-object": "^2.0.1" + } + }, + "copy-webpack-plugin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz", + "integrity": "sha512-P15M5ZC8dyCjQHWwd4Ia/dm0SgVvZJMYeykVIVYXbGyqO4dWB5oyPHp9i7wjwo5LhtlhKbiBCdS2NvM07Wlybg==", + "dev": true, + "requires": { + "cacache": "^12.0.3", + "find-cache-dir": "^2.1.0", + "glob-parent": "^3.1.0", + "globby": "^7.1.1", + "is-glob": "^4.0.1", + "loader-utils": "^1.2.3", + "minimatch": "^3.0.4", + "normalize-path": "^3.0.0", + "p-limit": "^2.2.1", + "schema-utils": "^1.0.0", + "serialize-javascript": "^2.1.2", + "webpack-log": "^2.0.0" + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "serialize-javascript": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", + "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", + "dev": true + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + } + } + }, + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", + "dev": true + }, + "core-js-compat": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.4.tgz", + "integrity": "sha512-Z5zbO9f1d0YrJdoaQhphVAnKPimX92D6z8lCGphH89MNRxlL1prI9ExJPqVwP0/kgkQCv8c4GJGT8X16yUncOg==", + "dev": true, + "requires": { + "browserslist": "^4.6.2", + "core-js-pure": "3.1.4", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz", + "integrity": "sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==", + "dev": true + } + } + }, + "core-js-pure": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.1.4.tgz", + "integrity": "sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + } + }, + "cp-file": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", + "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "make-dir": "^2.0.0", + "nested-error-stacks": "^2.0.0", + "pify": "^4.0.1", + "safe-buffer": "^5.0.1" + } + }, + "cpx-fixed": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/cpx-fixed/-/cpx-fixed-1.6.0.tgz", + "integrity": "sha512-0X6cfGWup886yM/ma/3I2DASv+QSDhlvPoSzHi1DZrYwl7QIFvSKtk60i2kTyRvUbxzrqLwP1At/1TSD2pGCqg==", + "dev": true, + "requires": { + "co": "^4.6.0", + "debounce": "^1.1.0", + "debug": "^3.1.0", + "duplexer": "^0.1.1", + "fs-extra": "^6.0.1", + "glob": "^7.1.2", + "glob2base": "0.0.12", + "minimatch": "^3.0.4", + "resolve": "^1.8.1", + "safe-buffer": "^5.1.2", + "shell-quote": "^1.6.1", + "subarg": "^1.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "fs-extra": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "requires": { + "buffer": "^5.1.0" + } + }, + "crc32-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", + "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", + "dev": true, + "requires": { + "crc": "^3.4.4", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-emotion": { + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-9.2.12.tgz", + "integrity": "sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA==", + "dev": true, + "requires": { + "@emotion/hash": "^0.6.2", + "@emotion/memoize": "^0.6.1", + "@emotion/stylis": "^0.7.0", + "@emotion/unitless": "^0.6.2", + "csstype": "^2.5.2", + "stylis": "^3.5.0", + "stylis-rule-sheet": "^0.0.10" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "create-react-context": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz", + "integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==", + "dev": true, + "requires": { + "gud": "^1.0.0", + "warning": "^4.0.3" + } + }, + "cross-env": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz", + "integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "cross-spawn-async": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz", + "integrity": "sha1-hF/wwINKPe2dFg2sptOQkGuyiMw=", + "dev": true, + "requires": { + "lru-cache": "^4.0.0", + "which": "^1.2.8" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "crypto-js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", + "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + }, + "css": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", + "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "source-map": "^0.6.1", + "source-map-resolve": "^0.5.2", + "urix": "^0.1.0" + } + }, + "css-color-function": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/css-color-function/-/css-color-function-1.3.3.tgz", + "integrity": "sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4=", + "dev": true, + "requires": { + "balanced-match": "0.1.0", + "color": "^0.11.0", + "debug": "^3.1.0", + "rgb": "~0.1.0" + }, + "dependencies": { + "balanced-match": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.1.0.tgz", + "integrity": "sha1-tQS9BYabOSWd0MXvw12EMXbczEo=", + "dev": true + }, + "color": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "dev": true, + "requires": { + "clone": "^1.0.2", + "color-convert": "^1.3.0", + "color-string": "^0.3.0" + } + }, + "color-string": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "dev": true, + "requires": { + "color-name": "^1.0.0" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "css-loader": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.1.tgz", + "integrity": "sha512-+ZHAZm/yqvJ2kDtPne3uX0C+Vr3Zn5jFn2N4HywtS5ujwvsVkyg0VArEXpl3BgczDA8anieki1FIzhchX4yrDw==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "css-selector-tokenizer": "^0.7.0", + "icss-utils": "^2.1.0", + "loader-utils": "^1.0.2", + "lodash": "^4.17.11", + "postcss": "^6.0.23", + "postcss-modules-extract-imports": "^1.2.0", + "postcss-modules-local-by-default": "^1.2.0", + "postcss-modules-scope": "^1.1.0", + "postcss-modules-values": "^1.3.0", + "postcss-value-parser": "^3.3.0", + "source-list-map": "^2.0.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" + }, + "dependencies": { + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + } + } + }, + "css-selector-tokenizer": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", + "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", + "dev": true, + "requires": { + "cssesc": "^0.1.0", + "fastparse": "^1.1.1", + "regexpu-core": "^1.0.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "dev": true, + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + } + } + }, + "css-unit-converter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.1.tgz", + "integrity": "sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=", + "dev": true + }, + "css-what": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", + "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", + "dev": true + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "dev": true + }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "cssstyle": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.3.0.tgz", + "integrity": "sha512-wXsoRfsRfsLVNaVzoKdqvEmK/5PFaEXNspVT22Ots6K/cnJdpoDKuQFw+qlMiXnmaif1OgeC466X1zISgAOcGg==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + } + }, + "csstype": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz", + "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==", + "dev": true + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "dev": true + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "d3": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", + "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=", + "dev": true + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "dev": true + }, + "d3-bboxCollide": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/d3-bboxCollide/-/d3-bboxCollide-1.0.4.tgz", + "integrity": "sha512-Sc8FKGGeejlowLW1g/0WBrVcbd++SBRW4N8OuZhVeRAfwlTL96+75JKlFfHweYdYRui1zPabfNXZrNaphBjS+w==", + "dev": true, + "requires": { + "d3-quadtree": "1.0.1" + } + }, + "d3-brush": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.6.tgz", + "integrity": "sha512-lGSiF5SoSqO5/mYGD5FAeGKKS62JdA1EV7HPrU2b5rTX4qEJJtpjaGLJngjnkewQy7UnGstnFd3168wpf5z76w==", + "dev": true, + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "dev": true, + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-cloud": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-cloud/-/d3-cloud-1.2.5.tgz", + "integrity": "sha512-4s2hXZgvs0CoUIw31oBAGrHt9Kt/7P9Ik5HIVzISFiWkD0Ga2VLAuO/emO/z1tYIpE7KG2smB4PhMPfFMJpahw==", + "dev": true, + "requires": { + "d3-dispatch": "^1.0.3" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", + "dev": true + }, + "d3-color": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.2.8.tgz", + "integrity": "sha512-yeANXzP37PHk0DbSTMNPhnJD+Nn4G//O5E825bR6fAfHH43hobSBpgB9G9oWVl9+XgUaQ4yCnsX1H+l8DoaL9A==", + "dev": true + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "dev": true, + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-delaunay": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.1.6.tgz", + "integrity": "sha512-VF6bxon2bn1cdXuesInEtVKlE4aUfq5IjE5y0Jl2aZP1yvLsf0QENqQxNhjS4vq95EmYKauA30ofTwvREtPSXA==", + "dev": true, + "requires": { + "delaunator": "4" + } + }, + "d3-dispatch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz", + "integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g==", + "dev": true + }, + "d3-drag": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.3.tgz", + "integrity": "sha512-8S3HWCAg+ilzjJsNtWW1Mutl74Nmzhb9yU6igspilaJzeZVFktmY6oO9xOh5TDk+BM2KrNFjttZNoJJmDnkjkg==", + "dev": true, + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-0.1.14.tgz", + "integrity": "sha1-mDPNYaWj6B4DJjoc54903lah27g=", + "dev": true + }, + "d3-ease": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz", + "integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ==", + "dev": true + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "dev": true, + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz", + "integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ==", + "dev": true + }, + "d3-geo": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.6.tgz", + "integrity": "sha512-z0J8InXR9e9wcgNtmVnPTj0TU8nhYT6lD/ak9may2PdKqXIeHUr8UbFLoCtrPYNsjv6YaLvSDQVl578k6nm7GA==", + "dev": true, + "requires": { + "d3-array": "1" + } + }, + "d3-glyphedge": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-glyphedge/-/d3-glyphedge-1.2.0.tgz", + "integrity": "sha512-F49fyMXMLYDHvqvxSmuGZrtIWeWLZWxar82WL1CJDBDPk4z6GUGSG4wX7rdv7N7R/YazAyMMnpOL0YQcmTLlOQ==", + "dev": true + }, + "d3-hexbin": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/d3-hexbin/-/d3-hexbin-0.2.2.tgz", + "integrity": "sha1-nFg32s/UcasFM3qeke8Qv8T5iDE=", + "dev": true + }, + "d3-hierarchy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", + "integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w==", + "dev": true + }, + "d3-interpolate": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", + "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==", + "dev": true, + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.7.tgz", + "integrity": "sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA==", + "dev": true + }, + "d3-polygon": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz", + "integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w==", + "dev": true + }, + "d3-quadtree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.1.tgz", + "integrity": "sha1-E74CViTxEEBe1DU2xQaq7Bme1ZE=", + "dev": true + }, + "d3-request": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-request/-/d3-request-1.0.6.tgz", + "integrity": "sha512-FJj8ySY6GYuAJHZMaCQ83xEYE4KbkPkmxZ3Hu6zA1xxG2GD+z6P+Lyp+zjdsHf0xEbp2xcluDI50rCS855EQ6w==", + "dev": true, + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-dsv": "1", + "xmlhttprequest": "1" + }, + "dependencies": { + "d3-dsv": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz", + "integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==", + "dev": true, + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + } + } + }, + "d3-sankey-circular": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/d3-sankey-circular/-/d3-sankey-circular-0.25.0.tgz", + "integrity": "sha512-maYak22afBAvmybeaopd1cVUNTIroEHhWCmh19gEQ+qgOhBkTav8YeP3Uw4OV/K4OksWaQrhhBOE4Rcxgc2JbQ==", + "dev": true, + "requires": { + "d3-array": "^1.2.1", + "d3-collection": "^1.0.4", + "d3-shape": "^1.2.0" + } + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "dev": true, + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "dev": true, + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz", + "integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==", + "dev": true + }, + "d3-shape": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz", + "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==", + "dev": true, + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz", + "integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==", + "dev": true + }, + "d3-time-format": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz", + "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==", + "dev": true, + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz", + "integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg==", + "dev": true + }, + "d3-transition": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz", + "integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==", + "dev": true, + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==", + "dev": true + }, + "damerau-levenshtein": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", + "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "datalib": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/datalib/-/datalib-1.9.2.tgz", + "integrity": "sha512-sV49o/1J3VdtTlJpsvPYT39WfUxyZGTO2rEGhEJt2eNY7LN2Z9K7nq3fOjgYMQtbuL0dVCWvmtxT2hpCgwx9Mg==", + "dev": true, + "requires": { + "d3-dsv": "0.1", + "d3-format": "0.4", + "d3-time": "0.1", + "d3-time-format": "0.2", + "request": "^2.67.0", + "sync-request": "^6.0.0", + "topojson-client": "^3.0.0" + }, + "dependencies": { + "d3-format": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-0.4.2.tgz", + "integrity": "sha1-qnWcHlquX6javJq3gZxQL8a1aHU=", + "dev": true + }, + "d3-time": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-0.1.1.tgz", + "integrity": "sha1-OM4qe7R6QDFhOCPd5GiOWOiSrls=", + "dev": true + }, + "d3-time-format": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-0.2.1.tgz", + "integrity": "sha1-hG45638iZ2aS2GBAxI6fpU/Yvxg=", + "dev": true, + "requires": { + "d3-time": "~0.1.1" + } + } + } + }, + "date-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz", + "integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==" + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debounce": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", + "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "debug-fabulous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", + "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", + "dev": true, + "requires": { + "debug": "3.X", + "memoizee": "0.4.X", + "object-assign": "4.X" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "decache": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/decache/-/decache-4.5.1.tgz", + "integrity": "sha512-5J37nATc6FmOTLbcsr9qx7Nm28qQyg1SK4xyEHqM0IBkNhWFp0Sm+vKoWYHD8wq+OUEb9jLyaKFfzzd1A9hcoA==", + "dev": true, + "requires": { + "callsite": "^1.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "requires": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "dev": true, + "requires": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, + "deemon": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/deemon/-/deemon-1.4.0.tgz", + "integrity": "sha512-S0zK5tNTdVFsJZVUeKi/CYJn4zzhW0Y55lwXzv2hVxb7ajzAHf91BhE5y2xvx1X7czIZ6PHLPDj00TVAmylVXw==", + "dev": true, + "requires": { + "bl": "^4.0.2", + "tree-kill": "^1.2.2" + }, + "dependencies": { + "bl": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", + "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + } + } + }, + "deep-assign": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-1.0.0.tgz", + "integrity": "sha1-sJJ0O+hCfcYh6gBnzex+cN0Z83s=", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "deep-diff": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz", + "integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "dev": true + }, + "default-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", + "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", + "dev": true, + "requires": { + "kind-of": "^5.0.2" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + } + } + }, + "default-resolution": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", + "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", + "dev": true + }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "dev": true, + "requires": { + "globby": "^6.1.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "p-map": "^1.1.1", + "pify": "^3.0.0", + "rimraf": "^2.2.8" + }, + "dependencies": { + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "delaunator": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", + "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "denodeify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", + "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "detect-indent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz", + "integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true + }, + "detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true + }, + "detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "dev": true, + "requires": { + "address": "^1.0.1", + "debug": "^2.6.0" + } + }, + "dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" + }, + "diagnostic-channel": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz", + "integrity": "sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=", + "requires": { + "semver": "^5.3.0" + } + }, + "diagnostic-channel-publishers": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.4.tgz", + "integrity": "sha512-SZ1zMfFiEabf4Qx0Og9V1gMsRoqz3O+5ENkVcNOfI+SMJ3QhQsdEoKX99r0zvreagXot2parPxmrwwUM/ja8ug==" + }, + "diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "requires": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "didyoumean": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.1.tgz", + "integrity": "sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "diff-match-patch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz", + "integrity": "sha512-Uv3SW8bmH9nAtHKaKSanOQmj2DnlH65fUpcrMdfdaOxUG02QQ4YGZ8AE7kKOMisF7UqvOlGKVYWRvezdncW9lg==" + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "requires": { + "path-type": "^3.0.0" + } + }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", + "dev": true + }, + "dns-packet": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.2.1.tgz", + "integrity": "sha512-JHj2yJeKOqlxzeuYpN1d56GfhzivAxavNwHj9co3qptECel27B1rLY5PifJAvubsInX5pGLDjAHuCfCUc2Zv/w==", + "requires": { + "ip": "^1.1.5" + } + }, + "dns-socket": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dns-socket/-/dns-socket-4.2.0.tgz", + "integrity": "sha512-4XuD3z28jht3jvHbiom6fAipgG5LkjYeDLrX5OH8cbl0AtzTyUUAxGckcW8T7z0pLfBBV5qOiuC4wUEohk6FrQ==", + "requires": { + "dns-packet": "^5.1.2" + } + }, + "doctrine": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", + "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=", + "dev": true, + "requires": { + "esutils": "^1.1.6", + "isarray": "0.0.1" + }, + "dependencies": { + "esutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz", + "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "requires": { + "utila": "~0.4" + } + }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.1.2" + } + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "dom-walk": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", + "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=", + "dev": true + }, + "dom4": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/dom4/-/dom4-2.1.5.tgz", + "integrity": "sha512-gJbnVGq5zaBUY0lUh0LUEVGYrtN75Ks8ZwpwOYvnVFrKy/qzXK4R/1WuLIFExWj/tBxbRAkTzZUGJHXmqsBNjQ==", + "dev": true + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "download": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/download/-/download-7.1.0.tgz", + "integrity": "sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==", + "dev": true, + "requires": { + "archive-type": "^4.0.0", + "caw": "^2.0.1", + "content-disposition": "^0.5.2", + "decompress": "^4.2.0", + "ext-name": "^5.0.0", + "file-type": "^8.1.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^8.3.1", + "make-dir": "^1.2.0", + "p-event": "^2.1.0", + "pify": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "requires": { + "readable-stream": "^2.0.2" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "each-props": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.189", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.189.tgz", + "integrity": "sha512-C26Kv6/rLNmGDaPR5HORMtTQat9aWBBKjQk9aFtN1Bk6cQBSw8cYdsel/mcrQlNlMMjt1sAKsTYqf77+sK2uTw==", + "dev": true + }, + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "requires": { + "shimmer": "^1.2.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "emotion": { + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/emotion/-/emotion-9.2.12.tgz", + "integrity": "sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ==", + "dev": true, + "requires": { + "babel-plugin-emotion": "^9.2.11", + "create-emotion": "^9.2.12" + } + }, + "enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", + "requires": { + "env-variable": "0.0.x" + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "dev": true, + "requires": { + "iconv-lite": "~0.4.13" + } + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "engine.io": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.0.tgz", + "integrity": "sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "0.3.1", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "ws": "^7.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ws": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz", + "integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==", + "dev": true + } + } + }, + "engine.io-client": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.0.tgz", + "integrity": "sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA==", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", + "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", + "dev": true, + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" + } + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + } + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "env-variable": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", + "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" + }, + "enzyme": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.10.0.tgz", + "integrity": "sha512-p2yy9Y7t/PFbPoTvrWde7JIYB2ZyGC+NgTNbVEGvZ5/EyoYSr9aG/2rSbVvyNvMHEhw9/dmGUJHWtfQIEiX9pg==", + "dev": true, + "requires": { + "array.prototype.flat": "^1.2.1", + "cheerio": "^1.0.0-rc.2", + "function.prototype.name": "^1.1.0", + "has": "^1.0.3", + "html-element-map": "^1.0.0", + "is-boolean-object": "^1.0.0", + "is-callable": "^1.1.4", + "is-number-object": "^1.0.3", + "is-regex": "^1.0.4", + "is-string": "^1.0.4", + "is-subset": "^0.1.1", + "lodash.escape": "^4.0.1", + "lodash.isequal": "^4.5.0", + "object-inspect": "^1.6.0", + "object-is": "^1.0.1", + "object.assign": "^4.1.0", + "object.entries": "^1.0.4", + "object.values": "^1.0.4", + "raf": "^3.4.0", + "rst-selector-parser": "^2.2.3", + "string.prototype.trim": "^1.1.2" + }, + "dependencies": { + "object-inspect": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", + "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", + "dev": true + } + } + }, + "enzyme-adapter-react-16": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.14.0.tgz", + "integrity": "sha512-7PcOF7pb4hJUvjY7oAuPGpq3BmlCig3kxXGi2kFx0YzJHppqX1K8IIV9skT1IirxXlu8W7bneKi+oQ10QRnhcA==", + "dev": true, + "requires": { + "enzyme-adapter-utils": "^1.12.0", + "has": "^1.0.3", + "object.assign": "^4.1.0", + "object.values": "^1.1.0", + "prop-types": "^15.7.2", + "react-is": "^16.8.6", + "react-test-renderer": "^16.0.0-0", + "semver": "^5.7.0" + } + }, + "enzyme-adapter-utils": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.0.tgz", + "integrity": "sha512-wkZvE0VxcFx/8ZsBw0iAbk3gR1d9hK447ebnSYBf95+r32ezBq+XDSAvRErkc4LZosgH8J7et7H7/7CtUuQfBA==", + "dev": true, + "requires": { + "airbnb-prop-types": "^2.13.2", + "function.prototype.name": "^1.1.0", + "object.assign": "^4.1.0", + "object.fromentries": "^2.0.0", + "prop-types": "^15.7.2", + "semver": "^5.6.0" + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + } + } + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "es-get-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", + "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", + "dev": true, + "requires": { + "es-abstract": "^1.17.4", + "has-symbols": "^1.0.1", + "is-arguments": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + } + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es5-ext": { + "version": "0.10.50", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", + "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "^1.0.0" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "escape-carriage": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.0.tgz", + "integrity": "sha512-ATWi5MD8QlAGQOeMgI8zTp671BG8aKvAC0M7yenlxU4CRLGO/sKthxVUyjiOFKjHdIo+6dZZUNFgHFeVEaKfGQ==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", + "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "eslint": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.1.tgz", + "integrity": "sha512-/2rX2pfhyUG0y+A123d0ccXtMm7DV7sH1m3lk9nk2DZ2LReq39FXHueR9xZwshE5MdfSf0xunSaMWRqyIA6M1w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.1.3", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.0", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^1.3.0", + "espree": "^7.3.0", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-config-airbnb": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.2.0.tgz", + "integrity": "sha512-Fz4JIUKkrhO0du2cg5opdyPKQXOI2MvF8KUvN2710nJMT6jaRUpRE2swrJftAjVGL7T1otLM5ieo5RqS1v9Udg==", + "dev": true, + "requires": { + "eslint-config-airbnb-base": "^14.2.0", + "object.assign": "^4.1.0", + "object.entries": "^1.1.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object.entries": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz", + "integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "has": "^1.0.3" + } + } + } + }, + "eslint-config-airbnb-base": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.0.tgz", + "integrity": "sha512-Snswd5oC6nJaevs3nZoLSTvGJBvzTfnBqOIArkf3cbyTyq9UD79wOk8s+RiL6bhca0p/eRO6veczhf6A/7Jy8Q==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.9", + "object.assign": "^4.1.0", + "object.entries": "^1.1.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object.entries": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz", + "integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "has": "^1.0.3" + } + } + } + }, + "eslint-config-prettier": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", + "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + } + } + }, + "eslint-plugin-import": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz", + "integrity": "sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.3", + "eslint-module-utils": "^2.6.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.1", + "read-pkg-up": "^2.0.0", + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + } + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz", + "integrity": "sha512-i1S+P+c3HOlBJzMFORRbC58tHa65Kbo8b52/TwCwSKLohwvpfT5rm2GjGWzOHTEuq4xxf2aRlHHTtmExDQOP+g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.10.2", + "aria-query": "^4.2.2", + "array-includes": "^3.1.1", + "ast-types-flow": "^0.0.7", + "axe-core": "^3.5.4", + "axobject-query": "^2.1.2", + "damerau-levenshtein": "^1.0.6", + "emoji-regex": "^9.0.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.4.1", + "language-tags": "^1.0.5" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.5.tgz", + "integrity": "sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + } + }, + "emoji-regex": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.0.0.tgz", + "integrity": "sha512-6p1NII1Vm62wni/VR/cUMauVQoxmLVb9csqQlvLz+hO2gk8U2UYDfXHQSUYIBKmZwAKz867IDqG7B+u0mj+M6w==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + } + } + }, + "eslint-plugin-prettier": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz", + "integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-react": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.20.6.tgz", + "integrity": "sha512-kidMTE5HAEBSLu23CUDvj8dc3LdBU0ri1scwHBZjI41oDv4tjsWZKU7MQccFzH1QYPYhsnTF2ovh7JlcIcmxgg==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flatmap": "^1.2.3", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.4.1", + "object.entries": "^1.1.2", + "object.fromentries": "^2.0.2", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "resolve": "^1.17.0", + "string.prototype.matchall": "^4.0.2" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object.entries": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz", + "integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "has": "^1.0.3" + } + }, + "object.fromentries": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz", + "integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.0.tgz", + "integrity": "sha512-YKBY+kilK5wrwIdQnCF395Ya6nDro3EAMoe+2xFkmyklyhF16fH83TrQOo9zbZIDxBsXFgBbywta/0JKRNFDkw==", + "dev": true + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + } + } + }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "estree-is-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-is-function/-/estree-is-function-1.0.0.tgz", + "integrity": "sha512-nSCWn1jkSq2QAtkaVLJZY2ezwcFO161HVc174zL1KPW3RJ+O6C3eJb8Nx7OXzvhoEv+nLgSR1g71oWUHUDTrJA==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true, + "requires": { + "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" + } + }, + "events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", + "dev": true + }, + "eventsource": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz", + "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=", + "dev": true, + "requires": { + "original": ">=0.0.5" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "expose-loader": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-0.7.5.tgz", + "integrity": "sha512-iPowgKUZkTPX5PznYsmifVj9Bob0w2wTHVkt/eYNPSzyebkUgIedmskf/kcfEIWpiWjg3JRjnW+a17XypySMuw==", + "dev": true + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, + "ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "requires": { + "mime-db": "^1.28.0" + } + }, + "ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "requires": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "dev": true, + "requires": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "falafel": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.2.0.tgz", + "integrity": "sha512-9eKUh65oZiESUxlwYhwIKntuTzWiQxlN9osHug0F+eDNekenqFFZGctlEFdhs6jXfwPncZhu8Qsly8P/t3JBxA==", + "requires": { + "acorn": "^7.1.1", + "foreach": "^2.0.5", + "isarray": "0.0.1", + "object-keys": "^1.0.6" + }, + "dependencies": { + "acorn": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, + "fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "dev": true, + "requires": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "fast-plist": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/fast-plist/-/fast-plist-0.1.2.tgz", + "integrity": "sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg=", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz", + "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==" + }, + "fast-xml-parser": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.16.0.tgz", + "integrity": "sha512-U+bpScacfgnfNfIKlWHDu4u6rtOaCyxhblOLJ8sZPkhsjgGqdZmVPBhdOyvdMGCDt8CsAv+cssOP3NzQptNt2w==", + "dev": true + }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fbjs": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", + "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "dev": true, + "requires": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "fecha": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", + "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "file-loader": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-5.1.0.tgz", + "integrity": "sha512-u/VkLGskw3Ue59nyOwUwXI/6nuBCo7KBkniB/l7ICwr/7cPNGsL1WCXUp3GB0qgOOKU1TiP49bv4DZF/LJqprg==", + "dev": true, + "requires": { + "loader-utils": "^1.4.0", + "schema-utils": "^2.5.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==", + "dev": true, + "requires": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + } + } + } + }, + "file-type": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", + "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", + "dev": true + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "filemanager-webpack-plugin-fixed": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/filemanager-webpack-plugin-fixed/-/filemanager-webpack-plugin-fixed-2.0.9.tgz", + "integrity": "sha512-iet9rDGF3HjHwObDy2FwzBFtiW59e+/wImVdqYXp5siRI4ESfxJyflOhpr7zrnJ4FcTS+rrIdk5E/Zgh9XtS0Q==", + "dev": true, + "requires": { + "archiver": "^3.0.0", + "cpx-fixed": "^1.6.0", + "fs-extra": "^7.0.0", + "make-dir": "^1.1.0", + "mv": "^2.1.1", + "rimraf": "^2.6.2" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "dev": true + }, + "filenamify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", + "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", + "dev": true, + "requires": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "filesize": { + "version": "3.5.11", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.5.11.tgz", + "integrity": "sha512-ZH7loueKBoDb7yG9esn1U+fgq7BzlzW6NRi5/rMdxIZ05dj7GFD/Xc5rq2CDt5Yq86CyfSYVyx4242QQNZbx1g==", + "dev": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-index": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", + "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=", + "dev": true + }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", + "dev": true + } + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==" + }, + "flatten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", + "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=" + }, + "fontkit": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.8.0.tgz", + "integrity": "sha512-EFDRCca7khfQWYu1iFhsqeABpi87f03MBdkT93ZE6YhqCdMzb5Eojb6c4dlJikGv5liuhByyzA7ikpIPTSBWbQ==", + "requires": { + "babel-runtime": "^6.11.6", + "brfs": "^1.4.0", + "brotli": "^1.2.0", + "browserify-optional": "^1.0.0", + "clone": "^1.0.1", + "deep-equal": "^1.0.0", + "dfa": "^1.0.0", + "restructure": "^0.5.3", + "tiny-inflate": "^1.0.2", + "unicode-properties": "^1.0.0", + "unicode-trie": "^0.3.0" + }, + "dependencies": { + "brfs": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz", + "integrity": "sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==", + "requires": { + "quote-stream": "^1.0.1", + "resolve": "^1.1.5", + "static-module": "^2.2.0", + "through2": "^2.0.0" + } + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "fork-ts-checker-webpack-plugin": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz", + "integrity": "sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "chalk": "^2.4.1", + "micromatch": "^3.1.10", + "minimatch": "^3.0.4", + "semver": "^5.6.0", + "tapable": "^1.0.0", + "worker-rpc": "^0.1.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + } + } + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "dev": true + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "free-style": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/free-style/-/free-style-3.1.0.tgz", + "integrity": "sha512-vJujYSIyT30iDoaoeigNAxX4yB1RUrh+N2ZMhIElMr3BvCuGXOw7XNJMEEJkDUeamK2Rnb/IKFGKRKlTWIGRWA==", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fromentries": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", + "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", + "dev": true + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "dev": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + } + }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, + "fs-walk": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/fs-walk/-/fs-walk-0.0.1.tgz", + "integrity": "sha1-9/yRw64e6tB8mYvF0N1B8tvr0zU=", + "dev": true, + "requires": { + "async": "*" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", + "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1", + "node-pre-gyp": "*" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "3.2.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "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" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.9.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "0.5.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.14.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + } + }, + "nopt": { + "version": "4.0.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "2.3.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "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" + } + }, + "rimraf": { + "version": "2.7.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.1", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.13", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.1.1", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "function.prototype.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.0.tgz", + "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "is-callable": "^1.1.3" + } + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha1-THbsL/CsGjap3M+aAN+GIweNTtg=" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "get-assigned-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", + "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==" + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" + }, + "get-proxy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", + "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", + "dev": true, + "requires": { + "npm-conf": "^1.1.0" + } + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "requires": { + "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-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "glob-watcher": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz", + "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==", + "dev": true, + "requires": { + "anymatch": "^2.0.0", + "async-done": "^1.2.0", + "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", + "just-debounce": "^1.0.0", + "object.defaults": "^1.1.0" + } + }, + "glob2base": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", + "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", + "dev": true, + "requires": { + "find-index": "^0.1.1" + } + }, + "global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dev": true, + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + } + } + }, + "glogg": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "dev": true, + "requires": { + "sparkles": "^1.0.0" + } + }, + "got": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", + "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", + "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==", + "dev": true + }, + "gulp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", + "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", + "dev": true, + "requires": { + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "gulp-cli": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz", + "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.1.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.0.1", + "yargs": "^7.1.0" + } + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + } + } + } + }, + "gulp-azure-storage": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/gulp-azure-storage/-/gulp-azure-storage-0.11.1.tgz", + "integrity": "sha512-csOwItwZV1P9GLsORVQy+CFwjYDdHNrBol89JlHdlhGx0fTgJBc1COTRZbjGRyRjgdUuVguo3YLl4ToJ10/SIQ==", + "dev": true, + "requires": { + "azure-storage": "^2.10.2", + "delayed-stream": "0.0.6", + "event-stream": "3.3.4", + "mime": "^1.3.4", + "progress": "^1.1.8", + "queue": "^3.0.10", + "streamifier": "^0.1.1", + "vinyl": "^2.2.0", + "vinyl-fs": "^3.0.3", + "yargs": "^15.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "delayed-stream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.6.tgz", + "integrity": "sha1-omRst+w9XXd0YUZwp6Zd4MFz7bw=", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + } + }, + "yargs-parser": { + "version": "18.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.2.tgz", + "integrity": "sha512-hlIPNR3IzC1YuL1c2UwwDKpXlNFBqD1Fswwh1khz5+d8Cq/8yc/Mn0i+rQXduu8hcrFKvO7Eryk+09NecTQAAQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "gulp-chmod": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-chmod/-/gulp-chmod-2.0.0.tgz", + "integrity": "sha1-AMOQuSigeZslGsz2MaoJ4BzGKZw=", + "dev": true, + "requires": { + "deep-assign": "^1.0.0", + "stat-mode": "^0.2.0", + "through2": "^2.0.0" + } + }, + "gulp-filter": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-5.1.0.tgz", + "integrity": "sha1-oF4Rr/sHz33PQafeHLe2OsN4PnM=", + "dev": true, + "requires": { + "multimatch": "^2.0.0", + "plugin-error": "^0.1.2", + "streamfilter": "^1.0.5" + } + }, + "gulp-gunzip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gulp-gunzip/-/gulp-gunzip-1.1.0.tgz", + "integrity": "sha512-3INeprGyz5fUtAs75k6wVslGuRZIjKAoQp39xA7Bz350ReqkrfYaLYqjZ67XyIfLytRXdzeX04f+DnBduYhQWw==", + "dev": true, + "requires": { + "through2": "~2.0.3", + "vinyl": "~2.0.1" + }, + "dependencies": { + "vinyl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.0.2.tgz", + "integrity": "sha1-CjcT2NTpIhxY8QyhbAEWyeJe2nw=", + "dev": true, + "requires": { + "clone": "^1.0.0", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "is-stream": "^1.1.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + } + } + } + }, + "gulp-rename": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.4.0.tgz", + "integrity": "sha512-swzbIGb/arEoFK89tPY58vg3Ok1bw+d35PfUNwWqdo7KM4jkmuGA78JiDNqR+JeZFaeeHnRg9N7aihX3YPmsyg==", + "dev": true + }, + "gulp-sourcemaps": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-2.6.5.tgz", + "integrity": "sha512-SYLBRzPTew8T5Suh2U8jCSDKY+4NARua4aqjj8HOysBh2tSgT9u4jc1FYirAdPx1akUxxDeK++fqw6Jg0LkQRg==", + "dev": true, + "requires": { + "@gulp-sourcemaps/identity-map": "1.X", + "@gulp-sourcemaps/map-sources": "1.X", + "acorn": "5.X", + "convert-source-map": "1.X", + "css": "2.X", + "debug-fabulous": "1.X", + "detect-newline": "2.X", + "graceful-fs": "4.X", + "source-map": "~0.6.0", + "strip-bom-string": "1.X", + "through2": "2.X" + }, + "dependencies": { + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true + } + } + }, + "gulp-typescript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-4.0.2.tgz", + "integrity": "sha512-Hhbn5Aa2l3T+tnn0KqsG6RRJmcYEsr3byTL2nBpNBeAK8pqug9Od4AwddU4JEI+hRw7mzZyjRbB8DDWR6paGVA==", + "dev": true, + "requires": { + "ansi-colors": "^1.0.1", + "plugin-error": "^0.1.2", + "source-map": "^0.6.1", + "through2": "^2.0.3", + "vinyl": "^2.1.0", + "vinyl-fs": "^3.0.0" + } + }, + "gulp-untar": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/gulp-untar/-/gulp-untar-0.0.8.tgz", + "integrity": "sha512-mqW7v2uvrxd8IoCCwJ04sPYgWjR3Gsi6yfhVWBK3sFMDP7FuoT7GNmxrCMwkk4RWqQohx8DRv+cDq4SRDXATGA==", + "dev": true, + "requires": { + "event-stream": "3.3.4", + "streamifier": "~0.1.1", + "tar": "^2.2.1", + "through2": "~2.0.3", + "vinyl": "^1.2.0" + }, + "dependencies": { + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "dev": true + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true, + "requires": { + "clone": "^1.0.0", + "clone-stats": "^0.0.1", + "replace-ext": "0.0.1" + } + } + } + }, + "gulp-vinyl-zip": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.2.tgz", + "integrity": "sha512-wJn09jsb8PyvUeyFF7y7ImEJqJwYy40BqL9GKfJs6UGpaGW9A+N68Q+ajsIpb9AeR6lAdjMbIdDPclIGo1/b7Q==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "queue": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/queue/-/queue-4.5.1.tgz", + "integrity": "sha512-AMD7w5hRXcFSb8s9u38acBZ+309u6GsiibP4/0YacJeaurRshogB7v/ZcVPxP5gD5+zIw6ixRHdutiYUJfwKHw==", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + } + } + }, + "gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "dev": true, + "requires": { + "glogg": "^1.0.0" + } + }, + "gzip-size": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", + "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", + "dev": true, + "requires": { + "duplexer": "^0.1.1" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "dev": true, + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", + "dev": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "dev": true, + "requires": { + "has-symbol-support-x": "^1.4.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasha": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.1.0.tgz", + "integrity": "sha512-OFPDWmzPN1l7atOV1TgBVmNtBxaIysToK6Ve9DK+vT6pYuklw/nPNT+HJbZi0KDcI6vWB+9tgvZ5YD7fA3CXcA==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + } + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hoist-non-react-statics": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz", + "integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==", + "dev": true, + "requires": { + "react-is": "^16.7.0" + } + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "dev": true + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "html-element-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.0.1.tgz", + "integrity": "sha512-BZSfdEm6n706/lBfXKWa4frZRZcT5k1cOusw95ijZsHlI+GdgY0v95h6IzO3iIDf2ROwq570YTwqNPqHcNMozw==", + "dev": true, + "requires": { + "array-filter": "^1.0.0" + } + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "html-escaper": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", + "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", + "dev": true + }, + "html-minifier": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", + "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", + "dev": true, + "requires": { + "camel-case": "3.0.x", + "clean-css": "4.2.x", + "commander": "2.17.x", + "he": "1.2.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.4.x" + }, + "dependencies": { + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true + } + } + }, + "html-to-react": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.4.3.tgz", + "integrity": "sha512-txe09A3vxW8yEZGJXJ1is5gGDfBEVACmZDSgwDyH5EsfRdOubBwBCg63ZThZP0xBn0UE4FyvMXZXmohusCxDcg==", + "dev": true, + "requires": { + "domhandler": "^3.0", + "htmlparser2": "^4.1.0", + "lodash.camelcase": "^4.3.0", + "ramda": "^0.27" + }, + "dependencies": { + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", + "dev": true + }, + "domhandler": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz", + "integrity": "sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1" + } + }, + "domutils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.1.0.tgz", + "integrity": "sha512-CD9M0Dm1iaHfQ1R/TI+z3/JWp/pgub0j4jIQKH89ARR4ATAV2nbaOQS5XxU9maJP5jHaPdDDQSEHuE2UmpUTKg==", + "dev": true, + "requires": { + "dom-serializer": "^0.2.1", + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0" + } + }, + "entities": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", + "dev": true + }, + "htmlparser2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", + "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + } + } + }, + "html-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", + "dev": true, + "requires": { + "html-minifier": "^3.2.3", + "loader-utils": "^0.2.16", + "lodash": "^4.17.3", + "pretty-error": "^2.0.2", + "tapable": "^1.0.0", + "toposort": "^1.0.0", + "util.promisify": "1.0.0" + }, + "dependencies": { + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + } + } + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "http-basic": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz", + "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==", + "dev": true, + "requires": { + "caseless": "^0.12.0", + "concat-stream": "^1.6.2", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + } + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-parser-js": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", + "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", + "dev": true + }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "dev": true, + "requires": { + "agent-base": "4", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "dev": true, + "requires": { + "@types/node": "^10.0.3" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "husky": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/husky/-/husky-1.3.1.tgz", + "integrity": "sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.7", + "execa": "^1.0.0", + "find-up": "^3.0.0", + "get-stdin": "^6.0.0", + "is-ci": "^2.0.0", + "pkg-dir": "^3.0.0", + "please-upgrade-node": "^3.1.1", + "read-pkg": "^4.0.1", + "run-node": "^1.0.0", + "slash": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", + "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", + "dev": true, + "requires": { + "normalize-package-data": "^2.3.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0" + } + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, + "icss-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", + "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "immutable": { + "version": "4.0.0-rc.12", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0-rc.12.tgz", + "integrity": "sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A==" + }, + "import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "dev": true, + "requires": { + "import-from": "^2.1.0" + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.0.4", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rx-lite": "^4.0.8", + "rx-lite-aggregates": "^4.0.8", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "internal-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", + "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", + "dev": true, + "requires": { + "es-abstract": "^1.17.0-next.1", + "has": "^1.0.3", + "side-channel": "^1.0.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + } + } + }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "dev": true + }, + "into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "dev": true, + "requires": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + } + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "inversify": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-4.13.0.tgz", + "integrity": "sha512-O5d8y7gKtyRwrvTLZzYET3kdFjqUy58sGpBYMARF13mzqDobpfBXVOPLH7HmnD2VR6Q+1HzZtslGvsdQfeb0SA==" + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", + "dev": true + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true + }, + "is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, + "requires": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + } + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-boolean-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.0.tgz", + "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true + }, + "is-ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", + "integrity": "sha1-aO6gfooKCpTC0IDdZ0xzGrKkYas=", + "dev": true, + "requires": { + "ip-regex": "^2.0.0" + } + }, + "is-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", + "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", + "dev": true + }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "dev": true + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-number-object": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.3.tgz", + "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=", + "dev": true + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "dev": true + }, + "is-online": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/is-online/-/is-online-8.2.1.tgz", + "integrity": "sha512-853p45I2b//EDV7n1Rbk60f/fy1KQlp1IkTjV/K2EyAD/h8qXrIAxwIbZ8c4K5p5yDsQlTWUrxocgW/aBtNfYQ==", + "requires": { + "got": "^9.6.0", + "p-any": "^2.0.0", + "p-timeout": "^3.0.0", + "public-ip": "^3.0.0" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + } + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "requires": { + "p-finally": "^1.0.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-root": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-1.0.0.tgz", + "integrity": "sha1-B7bCM7w5TNnQK6FclmvWZg1jQtU=", + "dev": true + }, + "is-set": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", + "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-string": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz", + "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=", + "dev": true + }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", + "dev": true + }, + "is-whitespace-character": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", + "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-word-character": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", + "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "is2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz", + "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==", + "requires": { + "deep-is": "^0.1.3", + "ip-regex": "^2.1.0", + "is-url": "^1.2.2" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isnumeric": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/isnumeric/-/isnumeric-0.2.0.tgz", + "integrity": "sha1-ojR7o2DeGeM9D/1ZD933dVy/LmQ=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "dev": true, + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + }, + "dependencies": { + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "dev": true, + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + } + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "requires": { + "append-transform": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.0.tgz", + "integrity": "sha512-Nm4wVHdo7ZXSG30KjZ2Wl5SU/Bw7bDx1PdaiIFzEStdjs0H12mOTncn1GVYuqQSaZxpg87VGBRsVRPGD2cD1AQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@babel/parser": "^7.7.5", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/core": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.7.tgz", + "integrity": "sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.7", + "@babel/helpers": "^7.7.4", + "@babel/parser": "^7.7.7", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.7.tgz", + "integrity": "sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", + "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", + "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", + "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helpers": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.4.tgz", + "integrity": "sha512-ak5NGZGJ6LV85Q1Zc9gn2n+ayXOizryhjSUBTdu5ih1tlVCJeuQENzc4ItyCVhINVXvIT/ZQ4mheGIsfBkpskg==", + "dev": true, + "requires": { + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/parser": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz", + "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==", + "dev": true + }, + "@babel/template": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", + "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/traverse": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", + "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.4", + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", + "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "dev": true, + "requires": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + } + }, + "iterate-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz", + "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==", + "dev": true + }, + "iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", + "dev": true, + "requires": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + } + }, + "jest-docblock": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", + "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", + "dev": true + }, + "jest-worker": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.3.0.tgz", + "integrity": "sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jpeg-js": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.3.7.tgz", + "integrity": "sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==", + "dev": true + }, + "jquery": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz", + "integrity": "sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ==", + "dev": true + }, + "jquery-ui": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.12.1.tgz", + "integrity": "sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE=", + "dev": true + }, + "js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "jsdom": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.1.1.tgz", + "integrity": "sha512-cQZRBB33arrDAeCrAEWn1U3SvrvC8XysBua9Oqg1yWrsY/gYcusloJC3RZJXuY5eehSCmws8f2YeliCqGSkrtQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^6.1.1", + "acorn-globals": "^4.3.2", + "array-equal": "^1.0.0", + "cssom": "^0.3.6", + "cssstyle": "^1.2.2", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.1", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.1.4", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^7.0.0", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "escodegen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", + "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", + "dev": true, + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "dev": true + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "ws": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.0.tgz", + "integrity": "sha512-Swie2C4fs7CkwlHu1glMePLYJJsWjzhl1vm3ZaLplD0h7OMkZyZ6kLTB/OagiU923bZrPFXuDTeEqaEN4NWG4g==", + "dev": true, + "requires": { + "async-limiter": "^1.0.0" + } + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, + "json-edm-parser": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz", + "integrity": "sha1-HmCw/vG8CvZ7wNFG393lSGzWFbQ=", + "requires": { + "jsonparse": "~1.2.0" + } + }, + "json-loader": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", + "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-pretty-compact": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz", + "integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json2csv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-4.5.2.tgz", + "integrity": "sha512-Te2Knce3VfLKyurH3AolM6Y781ZE+R3jQ+0YQ0HYLiubyicST/19vML24e1dpScaaEQb+1Q1t5IcGXr6esM9Lw==", + "dev": true, + "requires": { + "commander": "^2.15.1", + "jsonparse": "^1.3.1", + "lodash.get": "^4.4.2" + }, + "dependencies": { + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + } + } + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "json5": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "requires": { + "minimist": "^1.2.0" + } + }, + "jsonc-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.1.0.tgz", + "integrity": "sha512-n9GrT8rrr2fhvBbANa1g+xFmgGK5X91KFeDwlKQ3+SJfmH5+tKv/M/kahx/TXOMflfWHKGKqKyfHQaLKTNzJ6w==" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonparse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz", + "integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jsx-ast-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", + "integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "object.assign": "^4.1.0" + } + }, + "just-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", + "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", + "dev": true + }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, + "keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "requires": { + "json-buffer": "3.0.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "requires": { + "colornames": "^1.1.1" + } + }, + "labella": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/labella/-/labella-1.1.4.tgz", + "integrity": "sha1-xsxaNA6N80DrM1YzaD6lm4KMMi0=", + "dev": true + }, + "language-subtag-registry": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz", + "integrity": "sha512-KPMwROklF4tEx283Xw0pNKtfTj1gZ4UByp4EsIFWLgBavJltF4TiYPc39k06zSTsLzxTVXXDSpbwaQXaFB4Qeg==", + "dev": true + }, + "language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "dev": true, + "requires": { + "language-subtag-registry": "~0.3.2" + } + }, + "last-run": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", + "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", + "dev": true, + "requires": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" + } + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "dev": true, + "requires": { + "flush-write-stream": "^1.0.2" + } + }, + "leaflet": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.5.1.tgz", + "integrity": "sha512-ekM9KAeG99tYisNBg0IzEywAlp0hYI5XRipsqRXyRTeuU8jcuntilpp+eFf5gaE0xubc9RuSNIVtByEKwqFV0w==", + "dev": true + }, + "less": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/less/-/less-3.9.0.tgz", + "integrity": "sha512-31CmtPEZraNUtuUREYjSqRkeETFdyEHSEPAGq4erDlUXtda7pzNmctdljdIagSb589d/qXGWiiP31R5JVf+v0w==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.4.1", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", + "request": "^2.83.0", + "source-map": "~0.6.0" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + } + } + }, + "less-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-5.0.0.tgz", + "integrity": "sha512-bquCU89mO/yWLaUq0Clk7qCsKhsF/TZpJUzETRvJa9KSVEL9SO3ovCvdEHISBhrC81OwC8QSVX7E0bzElZj9cg==", + "dev": true, + "requires": { + "clone": "^2.1.1", + "loader-utils": "^1.1.0", + "pify": "^4.0.1" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + } + } + }, + "less-plugin-inline-urls": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/less-plugin-inline-urls/-/less-plugin-inline-urls-1.2.0.tgz", + "integrity": "sha1-XdqwegwlcfGihVz5Kd3J78Hjisw=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "dev": true, + "requires": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + } + }, + "line-by-line": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/line-by-line/-/line-by-line-0.1.6.tgz", + "integrity": "sha512-MmwVPfOyp0lWnEZ3fBA8Ah4pMFvxO6WgWovqZNu7Y4J0TNnGcsV4S1LzECHbdgqk1hoHc2mFP1Axc37YUqwafg==" + }, + "linear-layout-vector": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/linear-layout-vector/-/linear-layout-vector-0.0.1.tgz", + "integrity": "sha1-OYEU1zA7bsx/1rJzr3uEAdi6nHA=", + "dev": true + }, + "linebreak": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.0.2.tgz", + "integrity": "sha512-bJwSRsJeAmaZYnkcwl5sCQNfSDAhBuXxb6L27tb+qkBRtUQSSTUa5bcgCPD6hFEkRNlpWHfK7nFMmcANU7ZP1w==", + "requires": { + "base64-js": "0.0.8", + "brfs": "^2.0.2", + "unicode-trie": "^1.0.0" + }, + "dependencies": { + "base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=" + }, + "unicode-trie": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-1.0.0.tgz", + "integrity": "sha512-v5raLKsobbFbWLMoX9+bChts/VhPPj3XpkNr/HbqkirXR1DPk8eo9IYKyvk0MQZFkaoRsFj2Rmaqgi2rfAZYtA==", + "requires": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + } + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + }, + "lodash-es": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", + "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "lodash.curry": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", + "integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA=", + "dev": true + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", + "dev": true + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", + "dev": true + }, + "lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=", + "dev": true + }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.flow": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", + "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "lodash.tail": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", + "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", + "dev": true + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", + "dev": true + }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2" + } + }, + "log4js": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.1.2.tgz", + "integrity": "sha512-knS4Y30pC1e0n7rfx3VxcLOdBCsEo0o6/C7PVTGxdVK+5b1TYOSGQPn9FDcrhkoQBV29qwmA2mtkznPAQKnxQg==", + "requires": { + "date-format": "^3.0.0", + "debug": "^4.1.1", + "flatted": "^2.0.1", + "rfdc": "^1.1.4", + "streamroller": "^2.2.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "logform": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", + "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^2.3.3", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", + "dev": true, + "requires": { + "es5-ext": "~0.10.2" + } + }, + "magic-string": { + "version": "0.22.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", + "requires": { + "vlq": "^0.2.2" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "markdown-escapes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", + "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", + "dev": true + }, + "marked": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", + "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", + "dev": true + }, + "martinez-polygon-clipping": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.1.5.tgz", + "integrity": "sha1-gc4+soZ82RiKILkKzybyP7To7kI=", + "dev": true, + "requires": { + "bintrees": "^1.0.1", + "tinyqueue": "^1.1.0" + } + }, + "matchdep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", + "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", + "dev": true, + "requires": { + "findup-sync": "^2.0.0", + "micromatch": "^3.0.4", + "resolve": "^1.4.0", + "stack-trace": "0.0.10" + }, + "dependencies": { + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "material-colors": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", + "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", + "dev": true + }, + "math-expression-evaluator": { + "version": "1.2.22", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz", + "integrity": "sha512-L0j0tFVZBQQLeEjmWOvDLoRciIY8gQGWahvkztXUal8jH8R5Rlqo9GCvgqvXcy9LQhEWdQCVvzqAbxgYNt4blQ==", + "dev": true + }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "requires": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "mdast-add-list-metadata": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz", + "integrity": "sha512-fB/VP4MJ0LaRsog7hGPxgOrSL3gE/2uEdZyDuSEnKCv/8IkYHiDkIQSbChiJoHyxZZXZ9bzckyRk+vNxFzh8rA==", + "dev": true, + "requires": { + "unist-util-visit-parents": "1.1.2" + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "dependencies": { + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + } + } + }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==", + "dev": true + }, + "memoizee": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz", + "integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.45", + "es6-weak-map": "^2.0.2", + "event-emitter": "^0.3.5", + "is-promise": "^2.1", + "lru-queue": "0.1", + "next-tick": "1", + "timers-ext": "^0.1.5" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "merge-source-map": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", + "integrity": "sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=", + "requires": { + "source-map": "^0.5.6" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "microevent.ts": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", + "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "dev": true, + "requires": { + "dom-walk": "^0.1.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "dev": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "dev": true, + "requires": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "dependencies": { + "for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", + "dev": true + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "mocha": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.1.tgz", + "integrity": "sha512-p7FuGlYH8t7gaiodlFreseLxEmxTgvyG9RgPHODFPySNhwUehu8NIb0vdSt3WFckSneswZ0Un5typYcWElk7HQ==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.3.1", + "debug": "3.2.6", + "diff": "4.0.2", + "escape-string-regexp": "1.0.5", + "find-up": "4.1.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.0", + "minimatch": "3.0.4", + "ms": "2.1.2", + "object.assign": "4.1.0", + "promise.allsettled": "1.0.2", + "serialize-javascript": "4.0.0", + "strip-json-comments": "3.0.1", + "supports-color": "7.1.0", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.0.0", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.1" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz", + "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.3.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "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-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "readdirp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", + "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.7" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "mocha-junit-reporter": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-1.23.0.tgz", + "integrity": "sha512-pmpnEO4iDTmLfrT2RKqPsc5relG4crnDSGmXPuGogdda27A7kLujDNJV4EbTbXlVBCZXggN9rQYPEWMkOv4AAA==", + "dev": true, + "requires": { + "debug": "^2.2.0", + "md5": "^2.1.0", + "mkdirp": "~0.5.1", + "strip-ansi": "^4.0.0", + "xml": "^1.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "mocha-multi-reporters": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz", + "integrity": "sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI=", + "dev": true, + "requires": { + "debug": "^3.1.0", + "lodash": "^4.16.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "monaco-editor": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.18.1.tgz", + "integrity": "sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw==", + "dev": true + }, + "monaco-editor-textmate": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/monaco-editor-textmate/-/monaco-editor-textmate-2.2.1.tgz", + "integrity": "sha512-RYTNNfvyjK15M0JA8WIi9UduU10eX5724UGNKnaA8MSetehjThGENctUTuKaxPk/k3pq59QzaQ/C06A44iJd3Q==", + "dev": true + }, + "monaco-editor-webpack-plugin": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.7.0.tgz", + "integrity": "sha512-oItymcnlL14Sjd7EF7q+CMhucfwR/2BxsqrXIBrWL6LQplFfAfV+grLEQRmVHeGSBZ/Gk9ptzfueXnWcoEcFuA==", + "dev": true, + "requires": { + "@types/webpack": "^4.4.19" + } + }, + "monaco-textmate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/monaco-textmate/-/monaco-textmate-3.0.1.tgz", + "integrity": "sha512-ZxxY3OsqUczYP1sGqo97tu+CJmMBwuSW+dL0WEBdDhOZ5G1zntw72hvBc68ZQAirosWvbDKgN1dL5k173QtFww==", + "dev": true, + "requires": { + "fast-plist": "^0.1.2" + } + }, + "moo": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz", + "integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==", + "dev": true + }, + "mount-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mount-point/-/mount-point-3.0.0.tgz", + "integrity": "sha1-Zly57evoDREOZY21bDHQrvUaj5c=", + "dev": true, + "requires": { + "@sindresorhus/df": "^1.0.1", + "pify": "^2.3.0", + "pinkie-promise": "^2.0.1" + }, + "dependencies": { + "@sindresorhus/df": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/df/-/df-1.0.1.tgz", + "integrity": "sha1-xptm9S9vzdKHyAffIQMF2694UA0=", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "move-file": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/move-file/-/move-file-1.2.0.tgz", + "integrity": "sha512-USHrRmxzGowUWAGBbJPdFjHzEqtxDU03pLHY0Rfqgtnq+q8FOIs8wvkkf+Udmg77SJKs47y9sI0jJvQeYsmiCA==", + "dev": true, + "requires": { + "cp-file": "^6.1.0", + "make-dir": "^3.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multimatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", + "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", + "dev": true, + "requires": { + "array-differ": "^1.0.0", + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "minimatch": "^3.0.0" + } + }, + "mute-stdout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "dev": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "dev": true, + "requires": { + "glob": "^6.0.1" + } + } + } + }, + "named-js-regexp": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.5.tgz", + "integrity": "sha512-XO0DPujDP9IWpkt690iWLreKztb/VB811DGl5N3z7BfhkMJuiVZXOi6YN/fEB9qkvtMVTgSZDW8pzdVt8vj/FA==" + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true + }, + "nanoid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.0.3.tgz", + "integrity": "sha512-NbaoqdhIYmY6FXDRB4eYtDVC9Z9eCbn8TyaiC16LNKtpPv/aqa0tOPD8y6gNE4yUNnaZ7LLhYtXOev/6+cBtfw==", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "dev": true + }, + "nearley": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.16.0.tgz", + "integrity": "sha512-Tr9XD3Vt/EujXbZBv6UAHYoLUSMQAxSsTnm9K3koXzjzNWY195NqALeyrzLZBKzAkL3gl92BcSogqrHjD8QuUg==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "moo": "^0.4.3", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6", + "semver": "^5.4.1" + } + }, + "needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "dev": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + } + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "nested-error-stacks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", + "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "nise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-3.0.0.tgz", + "integrity": "sha512-EObFx5tioBMePHpU/gGczaY2YDqL255iwjmZwswu2CiwEW8xIGrr3E2xij+efIppS1nLQo9NyXSIUySGHUOhHQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/formatio": "^4.0.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^5.0.1", + "path-to-regexp": "^1.7.0" + } + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "requires": { + "lower-case": "^1.1.1" + } + }, + "nocache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", + "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==", + "dev": true + }, + "nock": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", + "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, + "node-gyp-build": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.1.tgz", + "integrity": "sha512-XyCKXsqZfLqHep1hhsMncoXuUNt/cXCjg1+8CLbu69V1TKuPiOeSGbL9n+k/ByKH8UT0p4rdIX8XkTRZV0i7Sw==" + }, + "node-has-native-dependencies": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/node-has-native-dependencies/-/node-has-native-dependencies-1.0.2.tgz", + "integrity": "sha1-MVLsl1O2ZB5NMi0YXdSTBkmto9o=", + "dev": true, + "requires": { + "fs-walk": "0.0.1" + } + }, + "node-html-parser": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.1.16.tgz", + "integrity": "sha512-cfqTZIYDdp5cGh3NvCD5dcEDP7hfyni7WgyFacmDynLlIZaF3GVlRk8yMARhWp/PobWt1KaCV8VKdP5LKWiVbg==", + "dev": true, + "requires": { + "he": "1.1.1" + }, + "dependencies": { + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + } + } + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + } + } + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "dev": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + }, + "dependencies": { + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "node-releases": { + "version": "1.1.25", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.25.tgz", + "integrity": "sha512-fI5BXuk83lKEoZDdH3gRhtsNgh05/wZacuXkgbiYkceE7+QIMXOg98n9ZV7mz27B+kFHnqHcUpscZZlGRSmTpQ==", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + }, + "node-stream-zip": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.8.2.tgz", + "integrity": "sha512-zwP2F/R28Oqtl0gOLItk5QjJ6jEU8XO4kaUMgeqvCyXPgdCZlm8T/5qLMiNy+moJCBCiMQAaX7aVMRhT0t2vkQ==" + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "dev": true, + "requires": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + }, + "dependencies": { + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + } + } + }, + "normalize.css": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", + "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==", + "dev": true + }, + "now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "dev": true, + "requires": { + "once": "^1.3.2" + } + }, + "npm-bundled": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", + "dev": true + }, + "npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "dev": true, + "requires": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "npm-packlist": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.6.tgz", + "integrity": "sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg==", + "dev": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "numeral": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", + "integrity": "sha1-StCAk21EPCVhrtnyGX7//iX05QY=", + "dev": true + }, + "nwsapi": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", + "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==", + "dev": true + }, + "nyc": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.0.0.tgz", + "integrity": "sha512-qcLBlNCKMDVuKb7d1fpxjPR8sHeMVX0CHarXAVzrVWoFrigCkYR8xcrjfXSPi5HXM7EU78L6ywO7w1c5rZNCNg==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.0", + "js-yaml": "^3.13.1", + "make-dir": "^3.0.0", + "node-preload": "^0.2.0", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "uuid": "^3.3.3", + "yargs": "^15.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-cache-dir": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.2.0.tgz", + "integrity": "sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.0", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "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" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "uuid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", + "dev": true + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz", + "integrity": "sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==" + }, + "object-is": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + } + } + }, + "object.entries": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", + "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "object.fromentries": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.0.tgz", + "integrity": "sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.11.0", + "function-bind": "^1.1.1", + "has": "^1.0.1" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + } + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "object.reduce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", + "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + } + } + }, + "object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "one-time": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", + "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" + }, + "onecolor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/onecolor/-/onecolor-3.1.0.tgz", + "integrity": "sha512-YZSypViXzu3ul5LMu/m6XjJ9ol8qAy9S2VjHl5E6UlhUH1KGKWabyEJifn0Jjpw23bYDzC2ucKMPGiH5kfwSGQ==", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + } + } + }, + "onigasm": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/onigasm/-/onigasm-2.2.2.tgz", + "integrity": "sha512-TQTMk+RmPYx4sGzNAgV0q7At7PABDNHVqZBlC4aRXHg8hpCdemLOF0qq0gUCjwUbc7mhJMBOo3XpTRYwyr45Gw==", + "requires": { + "lru-cache": "^4.1.1" + } + }, + "opener": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", + "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", + "dev": true + }, + "opn": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.2.0.tgz", + "integrity": "sha512-Jd/GpzPyHF4P2/aNOVmS3lfMSWV9J7cOhCG1s08XCEAsPkB7lp6ddiU0J7XzyQRDUh8BqJ7PchfINjR8jyofRQ==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "requires": { + "url-parse": "^1.4.3" + } + }, + "os": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/os/-/os-0.1.1.tgz", + "integrity": "sha1-IIhF6J4ZOtTZcUdLk5R3NqVtE/M=", + "dev": true + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-any": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-any/-/p-any-2.1.0.tgz", + "integrity": "sha512-JAERcaMBLYKMq+voYw36+x5Dgh47+/o7yuv2oQYuSSUml4YeqJEFznBrY2UeEkoSHqBua6hz518n/PsowTYLLg==", + "requires": { + "p-cancelable": "^2.0.0", + "p-some": "^4.0.0", + "type-fest": "^0.3.0" + }, + "dependencies": { + "p-cancelable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" + }, + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==" + } + } + }, + "p-cancelable": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", + "dev": true + }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-event": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", + "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", + "dev": true, + "requires": { + "p-timeout": "^2.0.1" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "dev": true + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, + "p-some": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-some/-/p-some-4.1.0.tgz", + "integrity": "sha512-MF/HIbq6GeBqTrTIl5OJubzkGU+qfFhAFi0gnTAK6rgEIJIknEiABHOTtQu4e6JiXjIwuMPMUFQzyHh5QjCl1g==", + "requires": { + "aggregate-error": "^3.0.0", + "p-cancelable": "^2.0.0" + }, + "dependencies": { + "p-cancelable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" + } + } + }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "dev": true, + "requires": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "requires": { + "no-case": "^2.2.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + }, + "dependencies": { + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + } + } + }, + "parse-asn1": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", + "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104=", + "dev": true + }, + "parse-entities": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", + "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", + "dev": true, + "requires": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", + "dev": true, + "requires": { + "semver": "^5.1.0" + } + }, + "parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "dev": true, + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8=" + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true, + "requires": { + "through": "~2.3" + } + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "pdfkit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.11.0.tgz", + "integrity": "sha512-1s9gaumXkYxcVF1iRtSmLiISF2r4nHtsTgpwXiK8Swe+xwk/1pm8FJjYqN7L3x13NsWnGyUFntWcO8vfqq+wwA==", + "requires": { + "crypto-js": "^3.1.9-1", + "fontkit": "^1.8.0", + "linebreak": "^1.0.2", + "png-js": "^1.0.0" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pidusage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-1.2.0.tgz", + "integrity": "sha512-OGo+iSOk44HRJ8q15AyG570UYxcm5u+R99DI8Khu8P3tKGkVu5EZX4ywHglWSTMNNXQ274oeGpYrvFEhDIFGPg==" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pixrem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pixrem/-/pixrem-4.0.1.tgz", + "integrity": "sha1-LaSh3m7EQjxfw3lOkwuB1EkOxoY=", + "dev": true, + "requires": { + "browserslist": "^2.0.0", + "postcss": "^6.0.0", + "reduce-css-calc": "^1.2.7" + }, + "dependencies": { + "browserslist": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000792", + "electron-to-chromium": "^1.3.30" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "playwright-chromium": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/playwright-chromium/-/playwright-chromium-0.13.0.tgz", + "integrity": "sha512-Ex9y563Vn8cnoBgMOcYLGdAVVC1xrBk/aWZ66AmOD77lk6v0x0cthLcbPX9h+tfkcWCFQQz1OWQR6V3+c3KUFA==", + "dev": true, + "requires": { + "playwright-core": "=0.13.0" + } + }, + "playwright-core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-0.13.0.tgz", + "integrity": "sha512-bH9cOQhmbdXJnHX22PRZ79IdXv5wmLc9tHob2PmkT5qFilopT3INAIJcKkaLN1E/GqIDp0xGdB88Vr5f86g8pQ==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "extract-zip": "^2.0.0", + "https-proxy-agent": "^3.0.0", + "jpeg-js": "^0.3.6", + "mime": "^2.4.4", + "pngjs": "^3.4.0", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "rimraf": "^3.0.2", + "ws": "^6.1.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "extract-zip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.0.tgz", + "integrity": "sha512-i42GQ498yibjdvIhivUsRslx608whtGoFIhF26Z7O4MYncBxp8CwalOs1lnHy21A9sIohWO2+uiE4SRtC9JXDg==", + "dev": true, + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "please-upgrade-node": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz", + "integrity": "sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ==", + "dev": true, + "requires": { + "semver-compare": "^1.0.0" + } + }, + "pleeease-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pleeease-filters/-/pleeease-filters-4.0.0.tgz", + "integrity": "sha1-ZjKy+wVkjSdY2GU4T7zteeHMrsc=", + "dev": true, + "requires": { + "onecolor": "^3.0.4", + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "plotly.js-dist": { + "version": "1.55.2", + "resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-1.55.2.tgz", + "integrity": "sha512-1QBBIPnh/G+8w9h/6pr7DQGm3t/XZptskUxJQsqTJJ1uth4xsDj2ltkmzujI5cG4lZadurJnfrJbFof+LHiyHg==", + "dev": true + }, + "plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" + } + }, + "arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", + "dev": true + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", + "dev": true, + "requires": { + "kind-of": "^1.1.0" + } + }, + "kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", + "dev": true + } + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, + "pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "dev": true + }, + "polygon-offset": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/polygon-offset/-/polygon-offset-0.3.1.tgz", + "integrity": "sha1-aaZWXwsn+na1Jw1cB5sLosjwu6M=", + "dev": true, + "requires": { + "martinez-polygon-clipping": "^0.1.5" + } + }, + "popper.js": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.0.tgz", + "integrity": "sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw==", + "dev": true + }, + "portfinder": { + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz", + "integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==", + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.1" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz", + "integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-apply": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/postcss-apply/-/postcss-apply-0.8.0.tgz", + "integrity": "sha1-FOVEu7XLbxweBIhXll15rgZrE0M=", + "dev": true, + "requires": { + "babel-runtime": "^6.23.0", + "balanced-match": "^0.4.2", + "postcss": "^6.0.0" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-attribute-case-insensitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-2.0.0.tgz", + "integrity": "sha1-lNxCLI+QmX8WvTOjZUu77AhJY7Q=", + "dev": true, + "requires": { + "postcss": "^6.0.0", + "postcss-selector-parser": "^2.2.3" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-calc": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-6.0.2.tgz", + "integrity": "sha512-fiznXjEN5T42Qm7qqMCVJXS3roaj9r4xsSi+meaBVe7CJBl8t/QLOXu02Z2E6oWAMWIvCuF6JrvzFekmVEbOKA==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss": "^7.0.2", + "postcss-selector-parser": "^2.2.2", + "reduce-css-calc": "^2.0.0" + }, + "dependencies": { + "reduce-css-calc": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.7.tgz", + "integrity": "sha512-fDnlZ+AybAS3C7Q9xDq5y8A2z+lT63zLbynew/lur/IR24OQF5x98tfNwf79mzEdfywZ0a2wpM860FhFfMxZlA==", + "dev": true, + "requires": { + "css-unit-converter": "^1.1.1", + "postcss-value-parser": "^3.3.0" + } + } + } + }, + "postcss-color-function": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-color-function/-/postcss-color-function-4.1.0.tgz", + "integrity": "sha512-2/fuv6mP5Lt03XbRpVfMdGC8lRP1sykme+H1bR4ARyOmSMB8LPSjcL6EAI1iX6dqUF+jNEvKIVVXhan1w/oFDQ==", + "dev": true, + "requires": { + "css-color-function": "~1.3.3", + "postcss": "^6.0.23", + "postcss-message-helpers": "^2.0.0", + "postcss-value-parser": "^3.3.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-color-gray": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-4.1.0.tgz", + "integrity": "sha512-L4iLKQLdqChz6ZOgGb6dRxkBNw78JFYcJmBz1orHpZoeLtuhDDGegRtX9gSyfoCIM7rWZ3VNOyiqqvk83BEN+w==", + "dev": true, + "requires": { + "color": "^2.0.1", + "postcss": "^6.0.14", + "postcss-message-helpers": "^2.0.0", + "reduce-function-call": "^1.0.2" + }, + "dependencies": { + "color": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color/-/color-2.0.1.tgz", + "integrity": "sha512-ubUCVVKfT7r2w2D3qtHakj8mbmKms+tThR8gI8zEYCbUBl8/voqFGt3kgBqGwXAopgXybnkuOq+qMYCRrp4cXw==", + "dev": true, + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-color-hex-alpha": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-3.0.0.tgz", + "integrity": "sha1-HlPmyKyyN5Vej9CLfs2xuLgwn5U=", + "dev": true, + "requires": { + "color": "^1.0.3", + "postcss": "^6.0.1", + "postcss-message-helpers": "^2.0.0" + }, + "dependencies": { + "color": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-1.0.3.tgz", + "integrity": "sha1-5I6DLYXxTvaU+0aIEcLVz+cptV0=", + "dev": true, + "requires": { + "color-convert": "^1.8.2", + "color-string": "^1.4.0" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-color-hsl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hsl/-/postcss-color-hsl-2.0.0.tgz", + "integrity": "sha1-EnA2ZvoxBDDj8wpFTawThjF9WEQ=", + "dev": true, + "requires": { + "postcss": "^6.0.1", + "postcss-value-parser": "^3.3.0", + "units-css": "^0.4.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-color-hwb": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hwb/-/postcss-color-hwb-3.0.0.tgz", + "integrity": "sha1-NAKxnvTYSXVAwftQcr6YY8qVVx4=", + "dev": true, + "requires": { + "color": "^1.0.3", + "postcss": "^6.0.1", + "postcss-message-helpers": "^2.0.0", + "reduce-function-call": "^1.0.2" + }, + "dependencies": { + "color": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-1.0.3.tgz", + "integrity": "sha1-5I6DLYXxTvaU+0aIEcLVz+cptV0=", + "dev": true, + "requires": { + "color-convert": "^1.8.2", + "color-string": "^1.4.0" + } + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-color-rebeccapurple": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-3.1.0.tgz", + "integrity": "sha512-212hJUk9uSsbwO5ECqVjmh/iLsmiVL1xy9ce9TVf+X3cK/ZlUIlaMdoxje/YpsL9cmUH3I7io+/G2LyWx5rg1g==", + "dev": true, + "requires": { + "postcss": "^6.0.22", + "postcss-values-parser": "^1.5.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-color-rgb": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rgb/-/postcss-color-rgb-2.0.0.tgz", + "integrity": "sha1-FFOcinExSUtILg3RzCZf9lFLUmM=", + "dev": true, + "requires": { + "postcss": "^6.0.1", + "postcss-value-parser": "^3.3.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-color-rgba-fallback": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rgba-fallback/-/postcss-color-rgba-fallback-3.0.0.tgz", + "integrity": "sha1-N9XJNToHoJJwkSqCYGu0Kg1wLAQ=", + "dev": true, + "requires": { + "postcss": "^6.0.6", + "postcss-value-parser": "^3.3.0", + "rgb-hex": "^2.1.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-cssnext": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-cssnext/-/postcss-cssnext-3.1.0.tgz", + "integrity": "sha512-awPDhI4OKetcHCr560iVCoDuP6e/vn0r6EAqdWPpAavJMvkBSZ6kDpSN4b3mB3Ti57hQMunHHM8Wvx9PeuYXtA==", + "dev": true, + "requires": { + "autoprefixer": "^7.1.1", + "caniuse-api": "^2.0.0", + "chalk": "^2.0.1", + "pixrem": "^4.0.0", + "pleeease-filters": "^4.0.0", + "postcss": "^6.0.5", + "postcss-apply": "^0.8.0", + "postcss-attribute-case-insensitive": "^2.0.0", + "postcss-calc": "^6.0.0", + "postcss-color-function": "^4.0.0", + "postcss-color-gray": "^4.0.0", + "postcss-color-hex-alpha": "^3.0.0", + "postcss-color-hsl": "^2.0.0", + "postcss-color-hwb": "^3.0.0", + "postcss-color-rebeccapurple": "^3.0.0", + "postcss-color-rgb": "^2.0.0", + "postcss-color-rgba-fallback": "^3.0.0", + "postcss-custom-media": "^6.0.0", + "postcss-custom-properties": "^6.1.0", + "postcss-custom-selectors": "^4.0.1", + "postcss-font-family-system-ui": "^3.0.0", + "postcss-font-variant": "^3.0.0", + "postcss-image-set-polyfill": "^0.3.5", + "postcss-initial": "^2.0.0", + "postcss-media-minmax": "^3.0.0", + "postcss-nesting": "^4.0.1", + "postcss-pseudo-class-any-link": "^4.0.0", + "postcss-pseudoelements": "^5.0.0", + "postcss-replace-overflow-wrap": "^2.0.0", + "postcss-selector-matches": "^3.0.1", + "postcss-selector-not": "^3.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-custom-media": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-6.0.0.tgz", + "integrity": "sha1-vlMnhBEOyylQRPtTlaGABushpzc=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-custom-properties": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-6.3.1.tgz", + "integrity": "sha512-zoiwn4sCiUFbr4KcgcNZLFkR6gVQom647L+z1p/KBVHZ1OYwT87apnS42atJtx6XlX2yI7N5fjXbFixShQO2QQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "postcss": "^6.0.18" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-custom-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-4.0.1.tgz", + "integrity": "sha1-eBOC+UxS5yfvXKR3bqKt9JphE4I=", + "dev": true, + "requires": { + "postcss": "^6.0.1", + "postcss-selector-matches": "^3.0.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-font-family-system-ui": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-family-system-ui/-/postcss-font-family-system-ui-3.0.0.tgz", + "integrity": "sha512-58G/hTxMSSKlIRpcPUjlyo6hV2MEzvcVO2m4L/T7Bb2fJTG4DYYfQjQeRvuimKQh1V1sOzCIz99g+H2aFNtlQw==", + "dev": true, + "requires": { + "postcss": "^6.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-font-variant": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-3.0.0.tgz", + "integrity": "sha1-CMzIj2BQuoLtjvLMdsDGprQfGD4=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-image-set-polyfill": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/postcss-image-set-polyfill/-/postcss-image-set-polyfill-0.3.5.tgz", + "integrity": "sha1-Dxk0E3AM8fgr05Bm7wFtZaShgYE=", + "dev": true, + "requires": { + "postcss": "^6.0.1", + "postcss-media-query-parser": "^0.2.3" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-import": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-12.0.1.tgz", + "integrity": "sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==", + "dev": true, + "requires": { + "postcss": "^7.0.1", + "postcss-value-parser": "^3.2.3", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-initial": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-2.0.0.tgz", + "integrity": "sha1-cnFfczbgu3k1HZnuZcSiU6hEG6Q=", + "dev": true, + "requires": { + "lodash.template": "^4.2.4", + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-load-config": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.0.tgz", + "integrity": "sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.0", + "import-cwd": "^2.0.0" + } + }, + "postcss-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", + "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "postcss": "^7.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^1.0.0" + } + }, + "postcss-media-minmax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-3.0.0.tgz", + "integrity": "sha1-Z1JWA3pD70C8Twdgv9BtTcadSNI=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", + "dev": true + }, + "postcss-message-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", + "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=", + "dev": true + }, + "postcss-modules-extract-imports": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz", + "integrity": "sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw==", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "dev": true, + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "dev": true, + "requires": { + "css-selector-tokenizer": "^0.7.0", + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "dev": true, + "requires": { + "icss-replace-symbols": "^1.1.0", + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-nesting": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-4.2.1.tgz", + "integrity": "sha512-IkyWXICwagCnlaviRexi7qOdwPw3+xVVjgFfGsxmztvRVaNxAlrypOIKqDE5mxY+BVxnId1rnUKBRQoNE2VDaA==", + "dev": true, + "requires": { + "postcss": "^6.0.11" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-pseudo-class-any-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-4.0.0.tgz", + "integrity": "sha1-kVKgYT00UHIFE+iJKFS65C0O5o4=", + "dev": true, + "requires": { + "postcss": "^6.0.1", + "postcss-selector-parser": "^2.2.3" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-pseudoelements": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudoelements/-/postcss-pseudoelements-5.0.0.tgz", + "integrity": "sha1-7vGU6NUkZFylIKlJ6V5RjoEkAss=", + "dev": true, + "requires": { + "postcss": "^6.0.0" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-replace-overflow-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-2.0.0.tgz", + "integrity": "sha1-eU22+qVPjbEAhUOSqTr0V2i04ls=", + "dev": true, + "requires": { + "postcss": "^6.0.1" + }, + "dependencies": { + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-selector-matches": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-3.0.1.tgz", + "integrity": "sha1-5WNAEeE5UIgYYbvdWMLQER/8lqs=", + "dev": true, + "requires": { + "balanced-match": "^0.4.2", + "postcss": "^6.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-selector-not": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-3.0.1.tgz", + "integrity": "sha1-Lk2y8JZTNsAefOx9tsYN/3ZzNdk=", + "dev": true, + "requires": { + "balanced-match": "^0.4.2", + "postcss": "^6.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + } + } + } + }, + "postcss-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", + "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "dev": true, + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "postcss-values-parser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-1.5.0.tgz", + "integrity": "sha512-3M3p+2gMp0AH3da530TlX8kiO1nxdTnc3C6vr8dMxRLIlh8UYkz0/wcwptSXjhtx2Fr0TySI7a+BHDQ8NL7LaQ==", + "dev": true, + "requires": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, + "postinstall-build": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.3.tgz", + "integrity": "sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, + "prettier": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.2.tgz", + "integrity": "sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "dev": true, + "requires": { + "renderkid": "^2.0.1", + "utila": "~0.4" + } + }, + "pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "requires": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + } + }, + "pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "requires": { + "asap": "~2.0.3" + } + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "promise.allsettled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz", + "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==", + "dev": true, + "requires": { + "array.prototype.map": "^1.0.1", + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "iterate-value": "^1.0.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + } + } + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "prop-types-exact": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", + "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", + "dev": true, + "requires": { + "has": "^1.0.3", + "object.assign": "^4.1.0", + "reflect.ownkeys": "^0.2.0" + } + }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "dev": true + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", + "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==" + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "public-ip": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/public-ip/-/public-ip-3.2.0.tgz", + "integrity": "sha512-DBq4o955zhrhESG4z6GkLN9mtY9NT/JOjEV8pvnYy3bjVQOQF0J5lJNwWLbEWwNstyNFJlY7JxCPFq4bdXSabw==", + "requires": { + "dns-socket": "^4.2.0", + "got": "^9.6.0", + "is-ip": "^3.1.0" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + } + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "ip-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.1.0.tgz", + "integrity": "sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA==" + }, + "is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "requires": { + "ip-regex": "^4.0.0" + } + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "pure-color": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz", + "integrity": "sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4=", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dev": true, + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==" + }, + "queue": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/queue/-/queue-3.1.0.tgz", + "integrity": "sha1-bEnQHwCeIlZ4h4nyv/rGuLmZBYU=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "quote-stream": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz", + "integrity": "sha1-hJY/jJwmuULhU/7rU6rnRlK34LI=", + "requires": { + "buffer-equal": "0.0.1", + "minimist": "^1.1.3", + "through2": "^2.0.0" + } + }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dev": true, + "requires": { + "performance-now": "^2.1.0" + } + }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", + "dev": true + }, + "ramda": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz", + "integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==", + "dev": true + }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "requires": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "range-inclusive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/range-inclusive/-/range-inclusive-1.0.2.tgz", + "integrity": "sha1-Rs2KsjevVZKKXDjzpQoXiSDhpQk=", + "dev": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", + "dev": true + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "react": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", + "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + } + }, + "react-annotation": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/react-annotation/-/react-annotation-2.1.6.tgz", + "integrity": "sha512-r/WT9ylhhTXAbYS/8MJOy/oKgO/G8DQ02fDmeEVPUtPq3VEG1Q7BUGYIJLWgWwNSRmgTLGIo4RIqYbQ9t1f0aA==", + "dev": true, + "requires": { + "prop-types": "15.6.2", + "viz-annotation": "0.0.3" + }, + "dependencies": { + "prop-types": { + "version": "15.6.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", + "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", + "dev": true, + "requires": { + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + } + } + }, + "react-base16-styling": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz", + "integrity": "sha1-OFjyTpxN2MvT9wLz901YHKKRcmk=", + "dev": true, + "requires": { + "base16": "^1.0.0", + "lodash.curry": "^4.0.1", + "lodash.flow": "^3.3.0", + "pure-color": "^1.2.0" + } + }, + "react-color": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.17.3.tgz", + "integrity": "sha512-1dtO8LqAVotPIChlmo6kLtFS1FP89ll8/OiA8EcFRDR+ntcK+0ukJgByuIQHRtzvigf26dV5HklnxDIvhON9VQ==", + "dev": true, + "requires": { + "@icons/material": "^0.2.4", + "lodash": "^4.17.11", + "material-colors": "^1.2.1", + "prop-types": "^15.5.10", + "reactcss": "^1.2.0", + "tinycolor2": "^1.4.1" + } + }, + "react-data-grid": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-data-grid/-/react-data-grid-6.1.0.tgz", + "integrity": "sha512-N1UtiHvsowEPzhx0VPqQKvGgSza/YNljczbisFDGMjawiGApS2taMv7h+EDXDx49CdaA6ur4eYS0z10x63IUpw==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "react-is-deprecated": "^0.1.2", + "shallowequal": "^1.1.0" + } + }, + "react-dev-utils": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-5.0.3.tgz", + "integrity": "sha512-Mvs6ofsc2xTjeZIrMaIfbXfsPVrbdVy/cVqq6SAacnqfMlcBpDuivhWZ1ODGeJ8HgmyWTLH971PYjj/EPCDVAw==", + "dev": true, + "requires": { + "address": "1.0.3", + "babel-code-frame": "6.26.0", + "chalk": "1.1.3", + "cross-spawn": "5.1.0", + "detect-port-alt": "1.1.6", + "escape-string-regexp": "1.0.5", + "filesize": "3.5.11", + "global-modules": "1.0.0", + "gzip-size": "3.0.0", + "inquirer": "3.3.0", + "is-root": "1.0.0", + "opn": "5.2.0", + "react-error-overlay": "^4.0.1", + "recursive-readdir": "2.2.1", + "shell-quote": "1.6.1", + "sockjs-client": "1.1.5", + "strip-ansi": "3.0.1", + "text-table": "0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "dependencies": { + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "react-dom": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz", + "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + } + }, + "react-draggable": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.2.tgz", + "integrity": "sha512-zLQs4R4bnBCGnCVTZiD8hPsHtkiJxgMpGDlRESM+EHQo8ysXhKJ2GKdJ8UxxLJdRVceX1j19jy+hQS2wHislPQ==", + "requires": { + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + } + }, + "react-error-overlay": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-4.0.1.tgz", + "integrity": "sha512-xXUbDAZkU08aAkjtUvldqbvI04ogv+a1XdHxvYuHPYKIVk/42BIOD0zSKTHAWV4+gDy3yGm283z2072rA2gdtw==", + "dev": true + }, + "react-hot-loader": { + "version": "4.12.6", + "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.12.6.tgz", + "integrity": "sha512-tRXWgF5MhQSEXX3EHIplCOWCzSg+ye7ddHeQLt7Z+CaZMeEfeCL2/uSGITIzWXOQYhefnLX8IZtr2cff4xIrww==", + "dev": true, + "requires": { + "fast-levenshtein": "^2.0.6", + "global": "^4.3.0", + "hoist-non-react-statics": "^3.3.0", + "loader-utils": "^1.1.0", + "prop-types": "^15.6.1", + "react-lifecycles-compat": "^3.0.4", + "shallowequal": "^1.0.2", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "react-is": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", + "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" + }, + "react-is-deprecated": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/react-is-deprecated/-/react-is-deprecated-0.1.2.tgz", + "integrity": "sha1-MBFI+G6kKP6OZz7KejchYLdXnb0=", + "dev": true + }, + "react-json-tree": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.11.2.tgz", + "integrity": "sha512-aYhUPj1y5jR3ZQ+G3N7aL8FbTyO03iLwnVvvEikLcNFqNTyabdljo9xDftZndUBFyyyL0aK3qGO9+8EilILHUw==", + "dev": true, + "requires": { + "babel-runtime": "^6.6.1", + "prop-types": "^15.5.8", + "react-base16-styling": "^0.5.1" + } + }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "dev": true + }, + "react-markdown": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-4.3.1.tgz", + "integrity": "sha512-HQlWFTbDxTtNY6bjgp3C3uv1h2xcjCSi1zAEzfBW9OwJJvENSYiLXWNXN5hHLsoqai7RnZiiHzcnWdXk2Splzw==", + "dev": true, + "requires": { + "html-to-react": "^1.3.4", + "mdast-add-list-metadata": "1.0.1", + "prop-types": "^15.7.2", + "react-is": "^16.8.6", + "remark-parse": "^5.0.0", + "unified": "^6.1.5", + "unist-util-visit": "^1.3.0", + "xtend": "^4.0.1" + } + }, + "react-motion": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/react-motion/-/react-motion-0.5.2.tgz", + "integrity": "sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==", + "dev": true, + "requires": { + "performance-now": "^0.2.0", + "prop-types": "^15.5.8", + "raf": "^3.1.0" + }, + "dependencies": { + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true + } + } + }, + "react-move": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/react-move/-/react-move-2.9.1.tgz", + "integrity": "sha512-5qKYsJrKKpSypEaaYyR2HBbBgX65htRqKDa8o5OGDkq2VfklmTCbLawtYFpdmcJRqbz4jCYpzo2Rrsazq9HA8Q==", + "dev": true, + "requires": { + "@babel/runtime": "^7.2.0", + "d3-interpolate": "^1.3.2", + "d3-timer": "^1.0.9", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, + "react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==", + "dev": true, + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "^0.3.0", + "deep-equal": "^1.1.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + }, + "dependencies": { + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + } + } + }, + "react-redux": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.1.tgz", + "integrity": "sha512-QsW0vcmVVdNQzEkrgzh2W3Ksvr8cqpAv5FhEk7tNEft+5pp7rXxAudTz3VOPawRkLIepItpkEIyLcN/VVXzjTg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.5.5", + "hoist-non-react-statics": "^3.3.0", + "invariant": "^2.2.4", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.9.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.6.3.tgz", + "integrity": "sha512-kq6anf9JGjW8Nt5rYfEuGRaEAaH1mkv3Bbu6rYvLOpPh/RusSJXuKPEAoZ7L7gybZkchE8+NV5g9vKF4AGAtsA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.2" + } + }, + "react-is": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz", + "integrity": "sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==", + "dev": true + } + } + }, + "react-svg-pan-zoom": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-svg-pan-zoom/-/react-svg-pan-zoom-3.1.0.tgz", + "integrity": "sha512-hmDUarqhNnCwuZumV9Pw7o5inW7lda4sX2U1vDK2B2slrSfNu1jbelOp6aaOEyUF7WzMA1xrpH6NBWvk4UeUTQ==", + "dev": true, + "requires": { + "prop-types": "^15.7.2", + "transformation-matrix": "^2.0.0" + } + }, + "react-svgmt": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/react-svgmt/-/react-svgmt-1.1.8.tgz", + "integrity": "sha512-3xu7iWuHIbqM2hv4eMsAN1mZKz6EnXTPAcE4mMX/NwuYY5uUKJBoKDAmxY+I6KXHx2SpYJtKAqe1a5jEehteZg==", + "dev": true, + "requires": { + "d3-ease": "^1.0.3", + "react-motion": "^0.5.2", + "react-move": "^2.7.0" + } + }, + "react-table": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/react-table/-/react-table-6.10.0.tgz", + "integrity": "sha512-s/mQLI1+mNvlae45MfAZyZ04YIT3jUzWJqx34s0tfwpDdgJkpeK6vyzwMUkKFCpGODBxpjBOekYZzcEmk+2FiQ==", + "dev": true, + "requires": { + "classnames": "^2.2.5" + } + }, + "react-table-hoc-fixed-columns": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/react-table-hoc-fixed-columns/-/react-table-hoc-fixed-columns-1.0.2.tgz", + "integrity": "sha512-0i2IEhGgFOibxoA1FOvABmuxcA7kCcw2lB5UPMX3RzS2wah1gvq6U128JNjvnG/P+yKvx54X+lwEEQ7ovjzMcA==", + "dev": true, + "requires": { + "classnames": "^2.2.6", + "emotion": "^9.2.3", + "uniqid": "^5.0.3" + } + }, + "react-test-renderer": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.6.tgz", + "integrity": "sha512-H2srzU5IWYT6cZXof6AhUcx/wEyJddQ8l7cLM/F7gDXYyPr4oq+vCIxJYXVGhId1J706sqziAjuOEjyNkfgoEw==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.13.6" + } + }, + "react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "dev": true, + "requires": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, + "react-virtualized": { + "version": "9.21.1", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.1.tgz", + "integrity": "sha512-E53vFjRRMCyUTEKuDLuGH1ld/9TFzjf/fFW816PE4HFXWZorESbSTYtiZz1oAjra0MminaUU1EnvUxoGuEFFPA==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "clsx": "^1.0.1", + "dom-helpers": "^2.4.0 || ^3.0.0", + "linear-layout-vector": "0.0.1", + "loose-envify": "^1.3.0", + "prop-types": "^15.6.0", + "react-lifecycles-compat": "^3.0.4" + } + }, + "reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "dev": true, + "requires": { + "lodash": "^4.0.1" + } + }, + "read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "dev": true, + "requires": { + "mute-stream": "~0.0.4" + } + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "dependencies": { + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + } + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "recursive-readdir": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.1.tgz", + "integrity": "sha1-kO8jHQd4xc4JPJpI105cVCLROpk=", + "dev": true, + "requires": { + "minimatch": "3.0.3" + }, + "dependencies": { + "minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", + "dev": true, + "requires": { + "brace-expansion": "^1.0.0" + } + } + } + }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "dev": true, + "requires": { + "balanced-match": "^0.4.2", + "math-expression-evaluator": "^1.2.14", + "reduce-function-call": "^1.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + } + } + }, + "reduce-function-call": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", + "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "redux": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz", + "integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + }, + "dependencies": { + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + } + } + }, + "redux-logger": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", + "integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=", + "dev": true, + "requires": { + "deep-diff": "^0.3.5" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "reflect.ownkeys": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", + "integrity": "sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=", + "dev": true + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", + "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", + "dev": true, + "requires": { + "regenerate": "^1.4.0" + } + }, + "regenerator-runtime": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==" + }, + "regenerator-transform": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.0.tgz", + "integrity": "sha512-rtOelq4Cawlbmq9xuMR5gdFmv7ku/sFoB7sRiywx7aq53bc52b4j6zvH7Te1Vt/X2YveDKnCGUbioieU7FEL3w==", + "dev": true, + "requires": { + "private": "^0.1.6" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexp-tree": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.11.tgz", + "integrity": "sha512-7/l/DgapVVDzZobwMCCgMlqiqyLFJ0cduo/j+3BcDJIB+yJdsYCfKuI3l/04NV+H/rfNRdPIDbXNZHM9XvQatg==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0.tgz", + "integrity": "sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + } + } + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "regexpu-core": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.4.tgz", + "integrity": "sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.0.2", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.1.0" + } + }, + "regjsgen": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", + "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==", + "dev": true + }, + "regjsparser": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", + "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "regression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regression/-/regression-2.0.1.tgz", + "integrity": "sha1-jSnD6CJKEIUMNeM36FqLL6w7DIc=", + "dev": true + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "relative": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/relative/-/relative-3.0.2.tgz", + "integrity": "sha1-Dc2OxUpdNaPBXhBFA9ZTdbWlNn8=", + "dev": true, + "requires": { + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "remark-parse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz", + "integrity": "sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA==", + "dev": true, + "requires": { + "collapse-white-space": "^1.0.2", + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-whitespace-character": "^1.0.0", + "is-word-character": "^1.0.0", + "markdown-escapes": "^1.0.0", + "parse-entities": "^1.1.0", + "repeat-string": "^1.5.4", + "state-toggle": "^1.0.0", + "trim": "0.0.1", + "trim-trailing-lines": "^1.0.0", + "unherit": "^1.0.4", + "unist-util-remove-position": "^1.0.0", + "vfile-location": "^2.0.0", + "xtend": "^4.0.1" + } + }, + "remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + } + }, + "remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", + "dev": true, + "requires": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + } + }, + "remove-files-webpack-plugin": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/remove-files-webpack-plugin/-/remove-files-webpack-plugin-1.4.0.tgz", + "integrity": "sha512-qlHn4EHNjWi6LiNV6aWCXROKjQg28sF/VxJ2FK+p5pPUQyLIBGlBAs/TUMxdVeToHSxq2RuA2XGgxcaDeTRnUg==", + "dev": true, + "requires": { + "@types/webpack": "^4.41.6", + "trash": "^6.1.1" + }, + "dependencies": { + "@types/webpack": { + "version": "4.41.7", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.7.tgz", + "integrity": "sha512-OQG9viYwO0V1NaNV7d0n79V+n6mjOV30CwgFPIfTzwmk8DHbt+C4f2aBGdCYbo3yFyYD6sjXfqqOjwkl1j+ulA==", + "dev": true, + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" + } + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "renderkid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.3.tgz", + "integrity": "sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==", + "dev": true, + "requires": { + "css-select": "^1.1.0", + "dom-converter": "^0.2", + "htmlparser2": "^3.3.0", + "strip-ansi": "^3.0.0", + "utila": "^0.4.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "replace-homedir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", + "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1", + "is-absolute": "^1.0.0", + "remove-trailing-separator": "^1.1.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "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" + } + }, + "request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", + "requires": { + "throttleit": "^1.0.0" + } + }, + "request-promise-core": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "dev": true, + "requires": { + "lodash": "^4.17.11" + } + }, + "request-promise-native": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", + "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "dev": true, + "requires": { + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "requirejs": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", + "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true + }, + "resolve": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", + "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", + "dev": true, + "requires": { + "value-or-function": "^3.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "restructure": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-0.5.4.tgz", + "integrity": "sha1-9U591WNZD7NP1r9Vh2EJrsyyjeg=", + "requires": { + "browserify-optional": "^1.0.0" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rewiremock": { + "version": "3.13.7", + "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.13.7.tgz", + "integrity": "sha512-U6iFfdXPiNtIBDcJWmspl/nhVk1EANkXLq2GM78T3ZfegvO5EW0TgNzExLh5iHXFJKQr//SmH9iloK/s4O7UqA==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "compare-module-exports": "^2.1.0", + "lodash.some": "^4.6.0", + "lodash.template": "^4.4.0", + "node-libs-browser": "^2.1.0", + "path-parse": "^1.0.5", + "wipe-node-cache": "^2.1.0", + "wipe-webpack-cache": "^2.1.0" + } + }, + "rfdc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz", + "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==" + }, + "rgb": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/rgb/-/rgb-0.1.0.tgz", + "integrity": "sha1-vieykej+/+rBvZlylyG/pA/AN7U=", + "dev": true + }, + "rgb-hex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/rgb-hex/-/rgb-hex-2.1.0.tgz", + "integrity": "sha1-x3PF/iJoolV42SU5qCp6XOU77aY=", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "roughjs-es5": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/roughjs-es5/-/roughjs-es5-0.1.0.tgz", + "integrity": "sha512-NMjzoBgSYk8qEYLSxzxytS20sfdQV7zg119FZjFDjIDwaqodFcf7QwzKbqM64VeAYF61qogaPLk3cs8Gb+TqZA==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0" + } + }, + "rst-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", + "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", + "dev": true, + "requires": { + "lodash.flattendeep": "^4.4.0", + "nearley": "^2.7.10" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "run-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", + "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", + "dev": true + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=", + "dev": true + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "*" + } + }, + "rxjs": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", + "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "requires": { + "tslib": "^1.9.0" + } + }, + "rxjs-compat": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.5.4.tgz", + "integrity": "sha512-rkn+lbOHUQOurdd74J/hjmDsG9nFx0z66fvnbs8M95nrtKvNqCKdk7iZqdY51CGmDemTQk+kUPy4s8HVOHtkfA==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "requires": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "sanitize-html": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.20.1.tgz", + "integrity": "sha512-txnH8TQjaQvg2Q0HY06G6CDJLVYCpbnxrdO0WN8gjCKaU5J0KbyGYhZxx5QJg3WLZ1lB7XU9kDkfrCXUozqptA==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "htmlparser2": "^3.10.0", + "lodash.clonedeep": "^4.5.0", + "lodash.escaperegexp": "^4.1.2", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.mergewith": "^4.6.1", + "postcss": "^7.0.5", + "srcset": "^1.0.0", + "xtend": "^4.0.1" + } + }, + "sass-loader": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz", + "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==", + "dev": true, + "requires": { + "clone-deep": "^2.0.1", + "loader-utils": "^1.0.1", + "lodash.tail": "^4.1.1", + "neo-async": "^2.5.0", + "pify": "^3.0.0", + "semver": "^5.5.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" + }, + "saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "dev": true, + "requires": { + "xmlchars": "^2.1.1" + } + }, + "scheduler": { + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", + "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "scope-analyzer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/scope-analyzer/-/scope-analyzer-2.0.5.tgz", + "integrity": "sha512-+U5H0417mnTEstCD5VwOYO7V4vYuSqwqjFap40ythe67bhMFL5C3UgPwyBv7KDJsqUBIKafOD57xMlh1rN7eaw==", + "requires": { + "array-from": "^2.1.1", + "es6-map": "^0.1.5", + "es6-set": "^0.1.5", + "es6-symbol": "^3.1.1", + "estree-is-function": "^1.0.0", + "get-assigned-identifiers": "^1.1.0" + } + }, + "seek-bzip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", + "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "dev": true, + "requires": { + "commander": "~2.8.1" + }, + "dependencies": { + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + } + } + }, + "semiotic": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/semiotic/-/semiotic-1.19.11.tgz", + "integrity": "sha512-TIiVTnKFonApiWZTN7AfJ9ltAUOl0wBYK5tFd1NCMFQJcbqDFAGrjfle1VHM2ARovNEbYKnl2a3N6zRwSKfJlg==", + "dev": true, + "requires": { + "@mapbox/polylabel": "1", + "d3-array": "^1.2.0", + "d3-bboxCollide": "^1.0.3", + "d3-brush": "^1.0.6", + "d3-chord": "^1.0.4", + "d3-collection": "^1.0.1", + "d3-contour": "^1.1.1", + "d3-force": "^1.0.2", + "d3-glyphedge": "^1.2.0", + "d3-hexbin": "^0.2.2", + "d3-hierarchy": "^1.1.3", + "d3-interpolate": "^1.1.5", + "d3-polygon": "^1.0.5", + "d3-sankey-circular": "0.25.0", + "d3-scale": "^1.0.3", + "d3-selection": "^1.1.0", + "d3-shape": "^1.2.0", + "d3-voronoi": "^1.0.2", + "json2csv": "^4.5.1", + "labella": "1.1.4", + "memoize-one": "4.0.0", + "object-assign": "4.1.1", + "polygon-offset": "0.3.1", + "promise": "8.0.1", + "prop-types": "15.6.0", + "react-annotation": "^2.1.6", + "regression": "^2.0.1", + "roughjs-es5": "0.1.0", + "semiotic-mark": "0.3.1", + "svg-path-bounding-box": "1.0.4" + }, + "dependencies": { + "d3-scale": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.7.tgz", + "integrity": "sha512-KvU92czp2/qse5tUfGms6Kjig0AhHOwkzXG0+PqIJB3ke0WUv088AHMZI0OssO9NCkXt4RP8yju9rpH8aGB7Lw==", + "dev": true, + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-color": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "memoize-one": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.0.tgz", + "integrity": "sha512-wdpOJ4XBejprGn/xhd1i2XR8Dv1A25FJeIvR7syQhQlz9eXsv+06llcvcmBxlWVGv4C73QBsWA8kxvZozzNwiQ==", + "dev": true + }, + "promise": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.0.1.tgz", + "integrity": "sha1-5F1osAoXZHttpxG/he1u1HII9FA=", + "dev": true, + "requires": { + "asap": "~2.0.3" + } + }, + "prop-types": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", + "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "dev": true, + "requires": { + "fbjs": "^0.8.16", + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" + } + } + } + }, + "semiotic-mark": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/semiotic-mark/-/semiotic-mark-0.3.1.tgz", + "integrity": "sha512-j7CsNannyJi68Yg5DXDZJrw3wEssBTaeGEvGMaTqPuBlM1kPFXYWvS0dpRzsT/Yopn/kRRyooDR+l6zQCwV+EQ==", + "dev": true, + "requires": { + "d3-interpolate": "^1.1.5", + "d3-scale": "^1.0.3", + "d3-selection": "^1.1.0", + "d3-shape": "^1.2.0", + "d3-transition": "^1.0.3", + "prop-types": "^15.6.0", + "roughjs-es5": "0.1.0" + }, + "dependencies": { + "d3-scale": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.7.tgz", + "integrity": "sha512-KvU92czp2/qse5tUfGms6Kjig0AhHOwkzXG0+PqIJB3ke0WUv088AHMZI0OssO9NCkXt4RP8yju9rpH8aGB7Lw==", + "dev": true, + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-color": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + } + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true + }, + "semver-greatest-satisfied-range": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", + "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", + "dev": true, + "requires": { + "sver-compat": "^1.5.0" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shallow-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", + "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", + "dev": true, + "requires": { + "is-extendable": "^0.1.1", + "kind-of": "^5.0.0", + "mixin-object": "^2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "shallow-copy": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", + "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=" + }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "~0.0.0", + "array-map": "~0.0.0", + "array-reduce": "~0.0.0", + "jsonify": "~0.0.0" + }, + "dependencies": { + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + } + } + }, + "shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "shortid": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.14.tgz", + "integrity": "sha512-4UnZgr9gDdA1kaKj/38IiudfC3KHKhDc1zi/HSxd9FQDR0VLwH3/y79tZJLsVYPsJgIjeHjqIWaWVRJUj9qZOQ==", + "dev": true, + "requires": { + "nanoid": "^2.0.0" + } + }, + "side-channel": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz", + "integrity": "sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==", + "dev": true, + "requires": { + "es-abstract": "^1.18.0-next.0", + "object-inspect": "^1.8.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz", + "integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + } + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", + "dev": true + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "dev": true, + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + }, + "dependencies": { + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dev": true, + "requires": { + "mimic-response": "^2.0.0" + } + }, + "mimic-response": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.0.0.tgz", + "integrity": "sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ==", + "dev": true + } + } + }, + "simple-html-tokenizer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz", + "integrity": "sha1-BcLuxXn//+FFoDCsJs/qYbmA+r4=", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "sinon": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-8.0.1.tgz", + "integrity": "sha512-vbXMHBszVioyPsuRDLEiPEgvkZnbjfdCFvLYV4jONNJqZNLWTwZ/gYSNh3SuiT1w9MRXUz+S7aX0B4Ar2XI8iw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/formatio": "^4.0.1", + "@sinonjs/samsam": "^4.0.1", + "diff": "^4.0.1", + "lolex": "^5.1.2", + "nise": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, + "slickgrid": { + "version": "2.4.17", + "resolved": "https://registry.npmjs.org/slickgrid/-/slickgrid-2.4.17.tgz", + "integrity": "sha512-saxVD9URoBD2M/Sl+7fLWE125/Cp1j0YhkRMPke4Hwdk31q/lihNv8I2o70cM5GRmoeWJKW7tnhNraDEe89jEg==", + "dev": true, + "requires": { + "jquery": ">=1.8.0", + "jquery-ui": ">=1.8.0" + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "socket.io": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", + "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", + "dev": true, + "requires": { + "debug": "~4.1.0", + "engine.io": "~3.4.0", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.3.0", + "socket.io-parser": "~3.4.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "socket.io-adapter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", + "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==", + "dev": true + }, + "socket.io-client": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "dev": true, + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + }, + "socket.io-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", + "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + } + } + }, + "socket.io-parser": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.0.tgz", + "integrity": "sha512-/G/VOI+3DBp0+DJKW4KesGnQkQPFmUCbA/oO2QGT6CWxU7hLGWqU3tyuzeSK/dqcyeHsQg1vTe9jiZI8GU9SCQ==", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + } + } + }, + "sockjs-client": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.5.tgz", + "integrity": "sha1-G7fA9yIsQPQq3xT0RCy9Eml3GoM=", + "dev": true, + "requires": { + "debug": "^2.6.6", + "eventsource": "0.1.6", + "faye-websocket": "~0.11.0", + "inherits": "^2.0.1", + "json3": "^3.3.2", + "url-parse": "^1.1.8" + } + }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "dev": true, + "requires": { + "sort-keys": "^1.0.0" + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", + "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "sparkles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "dev": true + }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "dependencies": { + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true, + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "srcset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-1.0.0.tgz", + "integrity": "sha1-pWad4StC87HV6D7QPHEEb8SPQe8=", + "dev": true, + "requires": { + "array-uniq": "^1.0.2", + "number-is-nan": "^1.0.0" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "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" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stack-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=" + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs=", + "dev": true + }, + "stat-mode": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", + "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", + "dev": true + }, + "state-toggle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", + "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", + "dev": true + }, + "static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "requires": { + "escodegen": "^1.8.1" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "static-module": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/static-module/-/static-module-2.2.5.tgz", + "integrity": "sha512-D8vv82E/Kpmz3TXHKG8PPsCPg+RAX6cbCOyvjM6x04qZtQ47EtJFVwRsdov3n5d6/6ynrOY9XB4JkaZwB2xoRQ==", + "requires": { + "concat-stream": "~1.6.0", + "convert-source-map": "^1.5.1", + "duplexer2": "~0.1.4", + "escodegen": "~1.9.0", + "falafel": "^2.1.0", + "has": "^1.0.1", + "magic-string": "^0.22.4", + "merge-source-map": "1.0.4", + "object-inspect": "~1.4.0", + "quote-stream": "~1.0.2", + "readable-stream": "~2.3.3", + "shallow-copy": "~0.0.1", + "static-eval": "^2.0.0", + "through2": "~2.0.3" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true, + "requires": { + "duplexer": "~0.1.1" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "dev": true, + "requires": { + "stubs": "^3.0.0" + } + }, + "stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "dev": true + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "streamfilter": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-1.0.7.tgz", + "integrity": "sha512-Gk6KZM+yNA1JpW0KzlZIhjo3EaBJDkYfXtYSbOwNIQ7Zd6006E6+sCFlW1NDvFG/vnXhKmw6TJJgiEQg/8lXfQ==", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "streamifier": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz", + "integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=", + "dev": true + }, + "streamroller": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.3.tgz", + "integrity": "sha512-AegmvQsscTRhHVO46PhCDerjIpxi7E+d2GxgUDu+nzw/HuLnUdxHWr6WQ+mVn/4iJgMKKFFdiUwFcFRDvcjCtw==", + "requires": { + "date-format": "^2.1.0", + "debug": "^4.1.1", + "fs-extra": "^8.1.0" + }, + "dependencies": { + "date-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", + "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==" + }, + "string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "string.prototype.matchall": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz", + "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "has-symbols": "^1.0.1", + "internal-slot": "^1.0.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + } + } + }, + "string.prototype.trim": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", + "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.0", + "function-bind": "^1.0.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + } + } + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + } + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=", + "dev": true + }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "requires": { + "is-natural-number": "^4.0.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "dev": true + }, + "style-loader": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz", + "integrity": "sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^1.0.0" + } + }, + "styled-jsx": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-3.2.1.tgz", + "integrity": "sha512-gM/WOrWYRpWReivzQqetEGohUc/TJSvUoZ5T/UJxJZIsVIPlRQLnp7R8Oue4q49sI08EBRQjQl2oBL3sfdrw2g==", + "dev": true, + "requires": { + "babel-plugin-syntax-jsx": "6.18.0", + "babel-types": "6.26.0", + "convert-source-map": "1.6.0", + "loader-utils": "1.2.3", + "source-map": "0.7.3", + "string-hash": "1.1.3", + "stylis": "3.5.4", + "stylis-rule-sheet": "0.0.10" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "stylis": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", + "dev": true + }, + "stylis-rule-sheet": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", + "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==", + "dev": true + }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "requires": { + "minimist": "^1.1.0" + } + }, + "sudo-prompt": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-8.2.5.tgz", + "integrity": "sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==" + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "sver-compat": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", + "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", + "dev": true, + "requires": { + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "svg-inline-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/svg-inline-loader/-/svg-inline-loader-0.8.0.tgz", + "integrity": "sha512-rynplY2eXFrdNomL1FvyTFQlP+dx0WqbzHglmNtA9M4IHRC3no2aPAl3ny9lUpJzFzFMZfWRK5YIclNU+FRePA==", + "dev": true, + "requires": { + "loader-utils": "^0.2.11", + "object-assign": "^4.0.1", + "simple-html-tokenizer": "^0.1.1" + }, + "dependencies": { + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + } + } + }, + "svg-inline-react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/svg-inline-react/-/svg-inline-react-3.1.0.tgz", + "integrity": "sha512-c39AIRQOUXLMD8fQ2rHmK1GOSO3tVuZk61bAXqIT05uhhm3z4VtQFITQSwyhL0WA2uxoJAIhPd2YV0CYQOolSA==", + "dev": true, + "requires": { + "prop-types": "^15.5.0" + } + }, + "svg-path-bounding-box": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/svg-path-bounding-box/-/svg-path-bounding-box-1.0.4.tgz", + "integrity": "sha1-7XPfODyLR4abZQjwWPV0j4gzwHA=", + "dev": true, + "requires": { + "svgpath": "^2.0.0" + } + }, + "svg-to-pdfkit": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/svg-to-pdfkit/-/svg-to-pdfkit-0.1.8.tgz", + "integrity": "sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==", + "requires": { + "pdfkit": ">=0.8.1" + } + }, + "svgpath": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svgpath/-/svgpath-2.2.2.tgz", + "integrity": "sha512-7cXFbkZvPkZpKLC+3QIfyUd3/Un/CvJONjTD3Gz5qLuEa73StPOt8kZjTi9apxO6zwCaza0bPNnmzTyrQ4qQlw==", + "dev": true + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "sync-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", + "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==", + "dev": true, + "requires": { + "http-response-object": "^3.0.1", + "sync-rpc": "^1.2.1", + "then-request": "^6.0.0" + } + }, + "sync-rpc": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz", + "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", + "dev": true, + "requires": { + "get-port": "^3.1.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + }, + "dependencies": { + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "tas-client": { + "version": "0.0.950", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.0.950.tgz", + "integrity": "sha512-AvCNjvfouxJyKln+TsobOBO5KmXklL9+FlxrEPlIgaixy1TxCC2v2Vs/MflCiyHlGl+BeIStP4oAVPqo5c0pIA==", + "requires": { + "axios": "^0.19.0" + } + }, + "tcp-port-used": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.1.tgz", + "integrity": "sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q==", + "requires": { + "debug": "4.1.0", + "is2": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", + "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "teeny-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz", + "integrity": "sha512-TAK0c9a00ELOqLrZ49cFxvPVogMUFaWY8dUsQc/0CuQPGF+BOxOQzXfE413BAk2kLomwNplvdtMpeaeGWmoc2g==", + "dev": true, + "requires": { + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^4.0.0", + "node-fetch": "^2.2.0", + "stream-events": "^1.0.5", + "uuid": "^3.3.2" + }, + "dependencies": { + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dev": true, + "requires": { + "agent-base": "5", + "debug": "4" + }, + "dependencies": { + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "dev": true + } + } + } + } + }, + "terser": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", + "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + } + }, + "terser-webpack-plugin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-3.1.0.tgz", + "integrity": "sha512-cjdZte66fYkZ65rQ2oJfrdCAkkhJA7YLYk5eGOcGCSGlq0ieZupRdjedSQXYknMPo2IveQL+tPdrxUkERENCFA==", + "dev": true, + "requires": { + "cacache": "^15.0.5", + "find-cache-dir": "^3.3.1", + "jest-worker": "^26.2.1", + "p-limit": "^3.0.2", + "schema-utils": "^2.6.6", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.8.0", + "webpack-sources": "^1.4.3" + }, + "dependencies": { + "ajv": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "cacache": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.0.5.tgz", + "integrity": "sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==", + "dev": true, + "requires": { + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.0", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", + "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "ssri": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", + "integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "tar": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.2.tgz", + "integrity": "sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.0", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "then-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz", + "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==", + "dev": true, + "requires": { + "@types/concat-stream": "^1.6.0", + "@types/form-data": "0.0.33", + "@types/node": "^8.0.0", + "@types/qs": "^6.2.31", + "caseless": "~0.12.0", + "concat-stream": "^1.6.0", + "form-data": "^2.2.0", + "http-basic": "^8.1.1", + "http-response-object": "^3.0.1", + "promise": "^8.0.0", + "qs": "^6.4.0" + }, + "dependencies": { + "@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "8.10.58", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.58.tgz", + "integrity": "sha512-NNcUk/rAdR7Pie7WiA5NHp345dTkD62qaxqscQXVIjCjog/ZXsrG8Wo7dZMZAzE7PSpA+qR2S3TYTeFCKuBFxQ==", + "dev": true + }, + "promise": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.0.3.tgz", + "integrity": "sha512-HeRDUL1RJiLhyA0/grn+PTShlBAcLuh/1BJGtrvjwbvRDCTLLMEz9rOGCV+R3vHY4MixIuoMEd9Yq/XvsTPcjw==", + "dev": true, + "requires": { + "asap": "~2.0.6" + } + } + } + }, + "thread-loader": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-2.1.3.tgz", + "integrity": "sha512-wNrVKH2Lcf8ZrWxDF/khdlLlsTMczdcwPA9VEK4c2exlEPynYWxi9op3nPTo5lAnDIkE0rQEB3VBP+4Zncc9Hg==", + "dev": true, + "requires": { + "loader-runner": "^2.3.1", + "loader-utils": "^1.1.0", + "neo-async": "^2.6.0" + } + }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "dev": true, + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "dev": true + }, + "timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, + "timers-browserify": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", + "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "timers-ext": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", + "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "dev": true, + "requires": { + "es5-ext": "~0.10.46", + "next-tick": "1" + } + }, + "tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, + "tinycolor2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", + "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=", + "dev": true + }, + "tinyqueue": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz", + "integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==", + "dev": true + }, + "tmp": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", + "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", + "requires": { + "os-tmpdir": "~1.0.1" + } + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", + "dev": true, + "requires": { + "through2": "^2.0.3" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "dev": true, + "requires": { + "commander": "2" + } + }, + "toposort": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", + "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", + "dev": true + }, + "touch": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/touch/-/touch-2.0.2.tgz", + "integrity": "sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "transform-loader": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/transform-loader/-/transform-loader-0.2.4.tgz", + "integrity": "sha1-5ch4d7qW1R0/IlNoWHtG4ibRzsk=", + "dev": true, + "requires": { + "loader-utils": "^1.0.2" + } + }, + "transformation-matrix": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.0.5.tgz", + "integrity": "sha512-S6L67Z8V3WEyPm2/zDh3I3bO0OQwv88dh7IY2dIOVBfIZJ4WQGdEKOsh7phTgYkvfAmHRxfOPNt1ixN/zR6D/A==", + "dev": true + }, + "trash": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/trash/-/trash-6.1.1.tgz", + "integrity": "sha512-4i56lCmz2RG6WZN018hf4L75L5HboaFuKkHx3wDG/ihevI99e0OgFyl8w6G4ioqBm62V4EJqCy5xw3vQSNXU8A==", + "dev": true, + "requires": { + "@stroncium/procfs": "^1.0.0", + "globby": "^7.1.1", + "is-path-inside": "^3.0.2", + "make-dir": "^3.0.0", + "move-file": "^1.1.0", + "p-map": "^3.0.0", + "p-try": "^2.2.0", + "uuid": "^3.3.2", + "xdg-trashdir": "^2.1.1" + }, + "dependencies": { + "is-path-inside": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", + "dev": true + }, + "make-dir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + }, + "trim": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", + "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", + "dev": true + }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "trim-trailing-lines": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz", + "integrity": "sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA==", + "dev": true + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "dev": true + }, + "truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", + "requires": { + "utf8-byte-length": "^1.0.1" + } + }, + "tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "dev": true + }, + "ts-loader": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.4.5.tgz", + "integrity": "sha512-XYsjfnRQCBum9AMRZpk2rTYSVpdZBpZK+kDh0TeT3kxmQNBDVIeUjdPjY5RZry4eIAb8XHc4gYSUiUWPYvzSRw==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^3.1.4", + "semver": "^5.0.1" + } + }, + "ts-mock-imports": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-mock-imports/-/ts-mock-imports-1.3.0.tgz", + "integrity": "sha512-cCrVcRYsp84eDvPict0ZZD/D7ppQ0/JSx4ve6aEU8DjlsaWRJWV6ADMovp2sCuh6pZcduLFoIYhKTDU2LARo7Q==", + "dev": true + }, + "ts-mockito": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.5.0.tgz", + "integrity": "sha512-b3qUeMfghRq5k5jw3xNJcnU9RKhqKnRn0k9v9QkN+YpuawrFuMIiGwzFZCpdi5MHy26o7YPnK8gag2awURl3nA==", + "dev": true, + "requires": { + "lodash": "^4.17.5" + } + }, + "ts-node": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.3.0.tgz", + "integrity": "sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + }, + "dependencies": { + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + } + } + }, + "tsconfig-paths": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.8.0.tgz", + "integrity": "sha512-zZEYFo4sjORK8W58ENkRn9s+HmQFkkwydDG7My5s/fnfr2YYCaiyXe/HBUcIgU8epEKOXwiahOO+KZYjiXlWyQ==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "deepmerge": "^2.0.1", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "tsconfig-paths-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "tsconfig-paths": "^3.4.0" + } + }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + }, + "tslint": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tslint-config-prettier": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz", + "integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==", + "dev": true + }, + "tslint-eslint-rules": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz", + "integrity": "sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w==", + "dev": true, + "requires": { + "doctrine": "0.7.2", + "tslib": "1.9.0", + "tsutils": "^3.0.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", + "dev": true + }, + "tsutils": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.14.0.tgz", + "integrity": "sha512-SmzGbB0l+8I0QwsPgjooFRaRvHLBLNYM8SeQ0k6rtNDru5sCGeLJcZdwilNndN+GysuFjF5EIYgN8GfFG6UeUw==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tslint-microsoft-contrib": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.2.1.tgz", + "integrity": "sha512-PDYjvpo0gN9IfMULwKk0KpVOPMhU6cNoT9VwCOLeDl/QS8v8W2yspRpFFuUS7/c5EIH/n8ApMi8TxJAz1tfFUA==", + "dev": true, + "requires": { + "tsutils": "^2.27.2 <2.29.0" + }, + "dependencies": { + "tsutils": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz", + "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tslint-plugin-prettier": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslint-plugin-prettier/-/tslint-plugin-prettier-2.3.0.tgz", + "integrity": "sha512-F9e4K03yc9xuvv+A0v1EmjcnDwpz8SpCD8HzqSDe0eyg34cBinwn9JjmnnRrNAs4HdleRQj7qijp+P/JTxt4vA==", + "dev": true, + "requires": { + "eslint-plugin-prettier": "^2.2.0", + "lines-and-columns": "^1.1.6", + "tslib": "^1.7.1" + }, + "dependencies": { + "eslint-plugin-prettier": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.7.0.tgz", + "integrity": "sha512-CStQYJgALoQBw3FsBzH0VOVDRnJ/ZimUlpLm226U8qgqYJfPOY/CPK6wyRInMxh73HSKg5wyRwdS4BVYYHwokA==", + "dev": true, + "requires": { + "fast-diff": "^1.1.1", + "jest-docblock": "^21.0.0" + } + } + } + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz", + "integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tv4": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", + "integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM=", + "dev": true + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", + "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typed-react-markdown": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/typed-react-markdown/-/typed-react-markdown-0.1.0.tgz", + "integrity": "sha1-HDra9CvB8NjGoJsKyAhfNt8KNn8=", + "dev": true, + "requires": { + "@types/react": "^0.14.44" + }, + "dependencies": { + "@types/react": { + "version": "0.14.57", + "resolved": "https://registry.npmjs.org/@types/react/-/react-0.14.57.tgz", + "integrity": "sha1-GHioZU+v3R04G4RXKStkM0mMW2I=", + "dev": true + } + } + }, + "typed-rest-client": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.2.0.tgz", + "integrity": "sha512-FrUshzZ1yxH8YwGR29PWWnfksLEILbWJydU7zfIRkyH7kAEzB62uMAl2WY6EyolWpLpVHeJGgQm45/MaruaHpw==", + "dev": true, + "requires": { + "tunnel": "0.0.4", + "underscore": "1.8.3" + } + }, + "typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typemoq": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", + "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "lodash": "^4.17.4", + "postinstall-build": "^5.0.1" + } + }, + "typescript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz", + "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==", + "dev": true + }, + "typescript-char": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-char/-/typescript-char-0.0.0.tgz", + "integrity": "sha1-VY/tpzfHZaYQtzfu+7F3Xum8jas=" + }, + "typescript-formatter": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.2.tgz", + "integrity": "sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==", + "dev": true, + "requires": { + "commandpost": "^1.0.0", + "editorconfig": "^0.15.0" + } + }, + "typestyle": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typestyle/-/typestyle-2.1.0.tgz", + "integrity": "sha512-6uCYPdG4xWLeEcl9O0GtNFnNGhami+irKiLsXSuvWHC/aTS7wdj49WeikWAKN+xHN3b1hm+9v0svwwgSBhCsNA==", + "dev": true, + "requires": { + "csstype": "2.6.9", + "free-style": "3.1.0" + }, + "dependencies": { + "csstype": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.9.tgz", + "integrity": "sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q==", + "dev": true + } + } + }, + "ua-parser-js": { + "version": "0.7.20", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz", + "integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==", + "dev": true + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "uglify-js": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", + "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", + "dev": true, + "requires": { + "commander": "~2.19.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + } + } + }, + "uint64be": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uint64be/-/uint64be-1.0.1.tgz", + "integrity": "sha1-H3FUIC8qG4rzU4cd2mUb80zpPpU=" + }, + "unbzip2-stream": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", + "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "undertaker": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", + "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "bach": "^1.0.0", + "collection-map": "^1.0.0", + "es6-weak-map": "^2.0.1", + "last-run": "^1.1.0", + "object.defaults": "^1.0.0", + "object.reduce": "^1.0.0", + "undertaker-registry": "^1.0.0" + } + }, + "undertaker-registry": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", + "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", + "dev": true + }, + "unherit": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", + "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", + "dev": true, + "requires": { + "inherits": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "unicode": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/unicode/-/unicode-10.0.0.tgz", + "integrity": "sha1-5dUcHbk7bHGguHngsMSvfm/faI4=" + }, + "unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", + "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", + "dev": true + }, + "unicode-properties": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.3.1.tgz", + "integrity": "sha512-nIV3Tf3LcUEZttY/2g4ZJtGXhWwSkuLL+rCu0DIAMbjyVPj+8j5gNVz4T/sVbnQybIsd5SFGkPKg/756OY6jlA==", + "requires": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + }, + "dependencies": { + "unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "requires": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + } + } + }, + "unicode-property-aliases-ecmascript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", + "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", + "dev": true + }, + "unicode-trie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz", + "integrity": "sha1-1nHd3YkQGgi6w3tqUWEBBgIFIIU=", + "requires": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "unified": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", + "integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==", + "dev": true, + "requires": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^1.1.0", + "trough": "^1.0.0", + "vfile": "^2.0.0", + "x-is-string": "^0.1.0" + } + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "uniqid": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-5.0.3.tgz", + "integrity": "sha512-R2qx3X/LYWSdGRaluio4dYrPXAJACTqyUjuyXHoJLBUOIfmMcnYOyY2d6Y4clZcIz5lK6ZaI0Zzmm0cPfsIqzQ==", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "dev": true, + "requires": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "unist-util-is": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", + "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", + "dev": true + }, + "unist-util-remove-position": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz", + "integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==", + "dev": true, + "requires": { + "unist-util-visit": "^1.1.0" + } + }, + "unist-util-stringify-position": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", + "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==", + "dev": true + }, + "unist-util-visit": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", + "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", + "dev": true, + "requires": { + "unist-util-visit-parents": "^2.0.0" + }, + "dependencies": { + "unist-util-visit-parents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", + "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", + "dev": true, + "requires": { + "unist-util-is": "^3.0.0" + } + } + } + }, + "unist-util-visit-parents": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz", + "integrity": "sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q==", + "dev": true + }, + "units-css": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/units-css/-/units-css-0.4.0.tgz", + "integrity": "sha1-1iKGU6UZg9fBb/KPi53Dsf/tOgc=", + "dev": true, + "requires": { + "isnumeric": "^0.2.0", + "viewport-dimensions": "^0.2.0" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "untildify": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz", + "integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==" + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "dev": true + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-join": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", + "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=", + "dev": true + }, + "url-loader": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz", + "integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "mime": "^2.0.3", + "schema-utils": "^1.0.0" + }, + "dependencies": { + "mime": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", + "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "dev": true + } + } + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "requires": { + "prepend-http": "^2.0.0" + } + }, + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", + "dev": true + }, + "urlgrey": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", + "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", + "dev": true + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, + "utf8-byte-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", + "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=" + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "v8-compile-cache": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", + "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", + "dev": true + }, + "v8flags": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", + "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validator": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", + "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" + }, + "value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "vega": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/vega/-/vega-5.7.3.tgz", + "integrity": "sha512-HCg5qgykM9drXPpjo9eJB9VWa/vHoDr5lXkjdk5iZBZI3fTajvAF/oFw6nM1l5P+eWibNlg9TFXn5vF4438uJw==", + "dev": true, + "requires": { + "vega-crossfilter": "^4.0.1", + "vega-dataflow": "^5.4.1", + "vega-encode": "^4.4.1", + "vega-event-selector": "^2.0.1", + "vega-expression": "^2.6.2", + "vega-force": "^4.0.3", + "vega-functions": "^5.4.1", + "vega-geo": "^4.1.0", + "vega-hierarchy": "^4.0.3", + "vega-loader": "^4.1.2", + "vega-parser": "^5.10.1", + "vega-projection": "^1.3.0", + "vega-regression": "^1.0.1", + "vega-runtime": "^5.0.2", + "vega-scale": "^4.1.3", + "vega-scenegraph": "^4.3.1", + "vega-statistics": "^1.6.1", + "vega-transforms": "^4.4.3", + "vega-typings": "^0.10.2", + "vega-util": "^1.12.0", + "vega-view": "^5.3.1", + "vega-view-transforms": "^4.4.1", + "vega-voronoi": "^4.1.1", + "vega-wordcloud": "^4.0.2" + } + }, + "vega-canvas": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-1.2.1.tgz", + "integrity": "sha512-k/S3EPeJ37D7fYDhv4sEg7fNWVpLheQY7flfLyAmJU7aSwCMgw8cZJi0CKHchJeculssfH+41NCqvRB1QtaJnw==", + "dev": true + }, + "vega-crossfilter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-4.0.1.tgz", + "integrity": "sha512-wLNS4JzKaOLj8EAzI/v8XBJjUWMRWYSu6EeQF4o9Opq/78u87Ol9Lc5I27UHsww5dNNH/tHubAV4QPIXnGOp5Q==", + "dev": true, + "requires": { + "d3-array": "^2.0.3", + "vega-dataflow": "^5.1.0", + "vega-util": "^1.8.0" + }, + "dependencies": { + "d3-array": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.3.3.tgz", + "integrity": "sha512-syv3wp0U5aB6toP2zb2OdBkhTy1MWDsCAaYk6OXJZv+G4u7bSWEmYgxLoFyc88RQUhZYGCebW9a9UD1gFi5+MQ==", + "dev": true + } + } + }, + "vega-dataflow": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-5.4.1.tgz", + "integrity": "sha512-NZASrIGel2ZD+HiJsozMPO7qNB3INLFWQez6KI+gPpKQIhsz7jWzG/TBK57A8NOLJYPc6VBLiygCmdJbr5E+sA==", + "dev": true, + "requires": { + "vega-loader": "^4.0.0", + "vega-util": "^1.11.0" + } + }, + "vega-embed": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/vega-embed/-/vega-embed-4.2.5.tgz", + "integrity": "sha512-3iUv5oU5y/sa7jC+shw79hPmHMpWMhMTGSovtl3+O98hLq7LQgordWKgoxKcqwhSIHMIgj+cInTNPWM4kru7Ug==", + "dev": true, + "requires": { + "d3-selection": "^1.4.0", + "json-stringify-pretty-compact": "^2.0.0", + "semver": "^6.3.0", + "vega-schema-url-parser": "^1.1.0", + "vega-themes": "^2.3.2", + "vega-tooltip": "^0.18.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "vega-encode": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-4.4.1.tgz", + "integrity": "sha512-PtfH+k7Hie7T0ywrg/nI/rhMnpNr8Rg627XR8pzSrM1CbDoMbfFmQsw33tXUT8k/8u/dPLR1klgAtCHUCh7jjA==", + "dev": true, + "requires": { + "d3-array": "^2.3.1", + "d3-format": "^1.4.1", + "d3-interpolate": "^1.3.2", + "d3-time-format": "^2.1.3", + "vega-dataflow": "^5.4.0", + "vega-scale": "^4.1.2", + "vega-util": "^1.11.2" + }, + "dependencies": { + "d3-array": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.3.3.tgz", + "integrity": "sha512-syv3wp0U5aB6toP2zb2OdBkhTy1MWDsCAaYk6OXJZv+G4u7bSWEmYgxLoFyc88RQUhZYGCebW9a9UD1gFi5+MQ==", + "dev": true + }, + "d3-format": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.1.tgz", + "integrity": "sha512-TUswGe6hfguUX1CtKxyG2nymO+1lyThbkS1ifLX0Sr+dOQtAD5gkrffpHnx+yHNKUZ0Bmg5T4AjUQwugPDrm0g==", + "dev": true + } + } + }, + "vega-event-selector": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-2.0.1.tgz", + "integrity": "sha512-FGU1PefYhW9An6zVs6TE5f/XGYsIispxFErG/p9KThxL22IC90WVZzMQXKN9M8OcARq5OyWjHg3qa9Qp/Z6OJw==", + "dev": true + }, + "vega-expression": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-2.6.2.tgz", + "integrity": "sha512-vh8GVkAL/KtsgcdrdKdEnysZn/InIuRrkF7U+CG1eAmupMucPY/Rpu0nCdYb4CLC/xNRHx/NMFidLztQUjZJQg==", + "dev": true, + "requires": { + "vega-util": "^1.11.0" + } + }, + "vega-force": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-4.0.3.tgz", + "integrity": "sha512-4stItN4jD9H1CENaCz4jXRNS1Bi9cozMOUjX2824FeJENi2RZSiAZAaGbscgerZQ/jbNcOHD8PHpC2pWldEvGA==", + "dev": true, + "requires": { + "d3-force": "^2.0.1", + "vega-dataflow": "^5.4.0", + "vega-util": "^1.11.0" + }, + "dependencies": { + "d3-force": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.0.1.tgz", + "integrity": "sha512-zh73/N6+MElRojiUG7vmn+3vltaKon7iD5vB/7r9nUaBeftXMzRo5IWEG63DLBCto4/8vr9i3m9lwr1OTJNiCg==", + "dev": true, + "requires": { + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + } + } + }, + "vega-functions": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-5.4.1.tgz", + "integrity": "sha512-f/smrE+ujMZgSWiTNVmdSNu3npDZMrHGc+MjjQ2pjJixcQRKTz1SbnmpogFhCgvKa6qBSw7hrZX8/TKGh4nuiA==", + "dev": true, + "requires": { + "d3-array": "^2.3.2", + "d3-color": "^1.4.0", + "d3-format": "^1.4.1", + "d3-geo": "^1.11.6", + "d3-time-format": "^2.1.3", + "vega-dataflow": "^5.4.1", + "vega-expression": "^2.6.2", + "vega-scale": "^4.1.3", + "vega-scenegraph": "^4.3.1", + "vega-selections": "^5.0.1", + "vega-statistics": "^1.6.1", + "vega-util": "^1.12.0" + }, + "dependencies": { + "d3-array": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.3.3.tgz", + "integrity": "sha512-syv3wp0U5aB6toP2zb2OdBkhTy1MWDsCAaYk6OXJZv+G4u7bSWEmYgxLoFyc88RQUhZYGCebW9a9UD1gFi5+MQ==", + "dev": true + }, + "d3-color": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.0.tgz", + "integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg==", + "dev": true + }, + "d3-format": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.1.tgz", + "integrity": "sha512-TUswGe6hfguUX1CtKxyG2nymO+1lyThbkS1ifLX0Sr+dOQtAD5gkrffpHnx+yHNKUZ0Bmg5T4AjUQwugPDrm0g==", + "dev": true + } + } + }, + "vega-geo": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-4.1.0.tgz", + "integrity": "sha512-3xo0hpmfVdhbdB0MWAppBu/DGTHn7+4g+psUVBu9LRii4XUNlmi+YmJzO4Ph6tv2zxrfZfIKMXid9+V5MzWn+g==", + "dev": true, + "requires": { + "d3-array": "^2.3.1", + "d3-contour": "^1.3.2", + "d3-geo": "^1.11.6", + "vega-dataflow": "^5.1.1", + "vega-projection": "^1.3.0", + "vega-util": "^1.11.2" + }, + "dependencies": { + "d3-array": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.3.3.tgz", + "integrity": "sha512-syv3wp0U5aB6toP2zb2OdBkhTy1MWDsCAaYk6OXJZv+G4u7bSWEmYgxLoFyc88RQUhZYGCebW9a9UD1gFi5+MQ==", + "dev": true + } + } + }, + "vega-hierarchy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-4.0.3.tgz", + "integrity": "sha512-9wNe+KyKqZW1S4++jCC38HuAhZbqNhfY7gOvwiMLjsp65tMtRETrtvYfHkULClm3UokUIX54etAXREAGW7znbw==", + "dev": true, + "requires": { + "d3-hierarchy": "^1.1.8", + "vega-dataflow": "^5.4.0", + "vega-util": "^1.11.0" + } + }, + "vega-lite": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-3.4.0.tgz", + "integrity": "sha512-RJg9uBNh5g0hA8xTzAcALUfNx0cEq7E7xx+vxPEGSMgI8z+A5KlE9u4jUx6nKu7Mjg1qZO8WOyWCmBS1kdFWPg==", + "dev": true, + "requires": { + "@types/clone": "~0.1.30", + "@types/fast-json-stable-stringify": "^2.0.0", + "clone": "~2.1.2", + "fast-deep-equal": "~2.0.1", + "fast-json-stable-stringify": "~2.0.0", + "json-stringify-pretty-compact": "~2.0.0", + "tslib": "~1.10.0", + "vega-event-selector": "~2.0.0", + "vega-expression": "~2.6.0", + "vega-typings": "0.7.2", + "vega-util": "~1.10.0", + "yargs": "~13.3.0" + }, + "dependencies": { + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "vega-typings": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-0.7.2.tgz", + "integrity": "sha512-BReB2qRERA/Ke+QoxKDQ7fES25A9Q3qKRm1CJxwvpLGhAl4k5cGDORx6yW+J3rFHMzpJlmdRM+kb489EuphxZQ==", + "dev": true, + "requires": { + "vega-util": "^1.10.0" + } + }, + "vega-util": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.10.0.tgz", + "integrity": "sha512-fTGnTG7FhtTG9tiYDL3k5s8YHqB71Ml5+aC9B7eaBygeB8GKXBrcbTXLOzoCRxT3Jr5cRhr99PMBu0AkqmhBog==", + "dev": true + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "vega-loader": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-4.1.2.tgz", + "integrity": "sha512-Zuxvl0K3jmLWyns+8ACajtp7oGfV+M7IeSQybq3xf7BvlGca2h8XA3qiBUXzYULkwZ4PstB56XoJ0tur2Rgs6A==", + "dev": true, + "requires": { + "d3-dsv": "^1.1.1", + "d3-time-format": "^2.1.3", + "node-fetch": "^2.6.0", + "topojson-client": "^3.0.1", + "vega-util": "^1.11.0" + }, + "dependencies": { + "d3-dsv": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz", + "integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==", + "dev": true, + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", + "dev": true + } + } + }, + "vega-parser": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-5.10.1.tgz", + "integrity": "sha512-NtjFhuXEGlc6ZOc+LeG7Gecmwal/UPu8WDI5wCjBWDogGgpeuRfQQ1haLz2KMsZhq5e2aUmk7iTuH/CuUoeGEA==", + "dev": true, + "requires": { + "vega-dataflow": "^5.4.1", + "vega-event-selector": "^2.0.1", + "vega-expression": "^2.6.2", + "vega-functions": "^5.4.1", + "vega-scale": "^4.1.3", + "vega-util": "^1.12.0" + } + }, + "vega-projection": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-1.3.0.tgz", + "integrity": "sha512-BFOc/XSVVW96WIAAyiUcppCeegniibiKGX0OLbGpQ5WIbeDHsbCXqnkeBpD5wsjvPXaiQRHTZ0PZ8VvCoCQV+g==", + "dev": true, + "requires": { + "d3-geo": "^1.11.6" + } + }, + "vega-regression": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-1.0.1.tgz", + "integrity": "sha512-eeQnLccWHAs2rovu2x3G50reF3Die9QoUGy/dMAO6sbDDA7B5s5qW3uq1NNnG93l3Ch84lO71qytxDBTdaQThA==", + "dev": true, + "requires": { + "d3-array": "^2.3.1", + "vega-dataflow": "^5.2.1", + "vega-statistics": "^1.4.0", + "vega-util": "^1.11.0" + }, + "dependencies": { + "d3-array": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.3.3.tgz", + "integrity": "sha512-syv3wp0U5aB6toP2zb2OdBkhTy1MWDsCAaYk6OXJZv+G4u7bSWEmYgxLoFyc88RQUhZYGCebW9a9UD1gFi5+MQ==", + "dev": true + } + } + }, + "vega-runtime": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-5.0.2.tgz", + "integrity": "sha512-Cuv+RY6kprH+vtNERg6xP4dgcdYGD2ZnxPxJNEtGi7dmtQQTBa1s7jQ0VDXTolsO6lKJ3B7np2GzKJYwevgj1A==", + "dev": true, + "requires": { + "vega-dataflow": "^5.1.1", + "vega-util": "^1.11.0" + } + }, + "vega-scale": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-4.1.3.tgz", + "integrity": "sha512-hpLrEFntN18e+eRAxa8b8malSbNVQyziKmUMGI1Za8ZB64cYj+A/G87ePE0ExSymfrvc/Xulh4VQZNxkPJll4w==", + "dev": true, + "requires": { + "d3-array": "^2.3.2", + "d3-interpolate": "^1.3.2", + "d3-scale": "^3.1.0", + "d3-time": "^1.1.0", + "vega-util": "^1.11.0" + }, + "dependencies": { + "d3-array": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.3.3.tgz", + "integrity": "sha512-syv3wp0U5aB6toP2zb2OdBkhTy1MWDsCAaYk6OXJZv+G4u7bSWEmYgxLoFyc88RQUhZYGCebW9a9UD1gFi5+MQ==", + "dev": true + }, + "d3-scale": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.0.tgz", + "integrity": "sha512-1RnLYPmH3f2E96hSsCr3ok066myuAxoH3+pnlJAedeMOp7jeW7A+GZHAyVWWaStfphyPEBiDoLFA9zl+DcnC2Q==", + "dev": true, + "requires": { + "d3-array": "1.2.0 - 2", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "dev": true + } + } + }, + "vega-scenegraph": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-4.3.1.tgz", + "integrity": "sha512-+hzfFFvvZKgGg7L7+RlRMY/II35/Ty8lw7CiEXRyKA2jxsp3eIWuppiaPgB0IQeVooJh6z4UNcKbNq3SUAvgDA==", + "dev": true, + "requires": { + "d3-path": "^1.0.8", + "d3-shape": "^1.3.5", + "vega-canvas": "^1.2.1", + "vega-loader": "^4.1.2", + "vega-util": "^1.11.2" + }, + "dependencies": { + "d3-path": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz", + "integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg==", + "dev": true + } + } + }, + "vega-schema-url-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vega-schema-url-parser/-/vega-schema-url-parser-1.1.0.tgz", + "integrity": "sha512-Tc85J2ofMZZOsxiqDM9sbvfsa+Vdo3GwNLjEEsPOsCDeYqsUHKAlc1IpbbhPLZ6jusyM9Lk0e1izF64GGklFDg==", + "dev": true + }, + "vega-selections": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-5.0.1.tgz", + "integrity": "sha512-cSqsH8Jg2QirQVLRfXfK0Odz+LOMo8iDtL9NANkv6/JehbqXAof0TfhgLu5gmCgzL3z+FUmzj2C4PizQGgkTtw==", + "dev": true, + "requires": { + "vega-expression": "^2.6.1", + "vega-util": "^1.11.0" + } + }, + "vega-statistics": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-1.6.1.tgz", + "integrity": "sha512-EX+IkELoYLnygcUXcFhcdWwzmalHzKLvSVTx9deGMyPOsCZRd0Lwu/SGwrIctoYa5r9DBR0/bGxQAC0t/UoSdA==", + "dev": true, + "requires": { + "d3-array": "^2.3.2" + }, + "dependencies": { + "d3-array": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.3.3.tgz", + "integrity": "sha512-syv3wp0U5aB6toP2zb2OdBkhTy1MWDsCAaYk6OXJZv+G4u7bSWEmYgxLoFyc88RQUhZYGCebW9a9UD1gFi5+MQ==", + "dev": true + } + } + }, + "vega-themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/vega-themes/-/vega-themes-2.5.0.tgz", + "integrity": "sha512-mkyYhcRhmMBWLfvCBPTVx0S/OnxeIfVY/TmFfYP5sPdW8X1kMyHtLI34bMhzosPrkhNyHsC8FNHJyU/dOQnX4A==", + "dev": true + }, + "vega-tooltip": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/vega-tooltip/-/vega-tooltip-0.18.1.tgz", + "integrity": "sha512-g/i69QLTVhGeHNT8k646Qr8SFss9kbnt6XmU9ujjqgaW5B/p1FPUrMzFh/88rMF704EHYyBH7Aj3t0ds1cCHbQ==", + "dev": true, + "requires": { + "vega-util": "^1.10.0" + } + }, + "vega-transforms": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-4.4.3.tgz", + "integrity": "sha512-8GxMm1CecRzNG89zr4JroOfPsWCkb9lWL5owSBsB+AalyJaFjxYwKuCM6L2OsAKID3su3aeQ0oSBQTR3v0nqPA==", + "dev": true, + "requires": { + "d3-array": "^2.3.2", + "vega-dataflow": "^5.4.1", + "vega-statistics": "^1.6.1", + "vega-util": "^1.12.0" + }, + "dependencies": { + "d3-array": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.3.3.tgz", + "integrity": "sha512-syv3wp0U5aB6toP2zb2OdBkhTy1MWDsCAaYk6OXJZv+G4u7bSWEmYgxLoFyc88RQUhZYGCebW9a9UD1gFi5+MQ==", + "dev": true + } + } + }, + "vega-typings": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-0.10.2.tgz", + "integrity": "sha512-W/V6oyoBizlXt0FJnuY5/Y46/dsmfcPTxRaUCtFBf0nyoWBsmO66EYj24xCHJ6pgfHEEbBRLPfrfrvlKiRPaMQ==", + "dev": true, + "requires": { + "vega-util": "^1.11.0" + } + }, + "vega-util": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.12.0.tgz", + "integrity": "sha512-eN1PAQVDyEOcwild2Fk1gbkzkqgDHNujG2/akYRtBzkhtz2EttrVIDwBkWqV/Q+VvEINEksb7TI3Wv7qVQFR5g==", + "dev": true + }, + "vega-view": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-5.3.1.tgz", + "integrity": "sha512-pKbpmR1uz9YOsqocx6KWEHD1EwwpOVIfRH6h8g5xQLiUQP37bohfKZ2vL05MQCupXN2dNYdp51iyETbMJMj7nQ==", + "dev": true, + "requires": { + "d3-array": "^2.3.1", + "d3-timer": "^1.0.9", + "vega-dataflow": "^5.2.1", + "vega-functions": "^5.3.1", + "vega-runtime": "^5.0.1", + "vega-scenegraph": "^4.2.0", + "vega-util": "^1.11.0" + }, + "dependencies": { + "d3-array": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.3.3.tgz", + "integrity": "sha512-syv3wp0U5aB6toP2zb2OdBkhTy1MWDsCAaYk6OXJZv+G4u7bSWEmYgxLoFyc88RQUhZYGCebW9a9UD1gFi5+MQ==", + "dev": true + } + } + }, + "vega-view-transforms": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-4.4.1.tgz", + "integrity": "sha512-BgjGsHDLHGTN2FgVRcepuvAcKnMUUtXZVemmJHPrY4LI5iJiCS9+HkZPVcVWD1+vheSNXMmZV7Skn7qn7zAcRg==", + "dev": true, + "requires": { + "vega-dataflow": "^5.1.1", + "vega-scenegraph": "^4.3.0", + "vega-util": "^1.11.2" + } + }, + "vega-voronoi": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-4.1.1.tgz", + "integrity": "sha512-agLmr+UGxJs5KB9D8GeZqxgeWWGoER/eVHPcFFPgVuoNBsrqf2bdoltmIkRnpiRsQnGCibGixhFEDCc9GGNAww==", + "dev": true, + "requires": { + "d3-delaunay": "^5.1.3", + "vega-dataflow": "^5.1.1", + "vega-util": "^1.11.0" + } + }, + "vega-wordcloud": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-4.0.2.tgz", + "integrity": "sha512-nV9bRKjRGcmcQV5wXvOvWes4T5937t3RF+Rm1d03YVAzZpOcVKk9uBuVSeFYBLX2XcDBVe4HK54qDoOTFftHMw==", + "dev": true, + "requires": { + "vega-canvas": "^1.2.0", + "vega-dataflow": "^5.1.1", + "vega-scale": "^4.0.0", + "vega-statistics": "^1.2.5", + "vega-util": "^1.8.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vfile": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", + "integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==", + "dev": true, + "requires": { + "is-buffer": "^1.1.4", + "replace-ext": "1.0.0", + "unist-util-stringify-position": "^1.0.0", + "vfile-message": "^1.0.0" + } + }, + "vfile-location": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz", + "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==", + "dev": true + }, + "vfile-message": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz", + "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", + "dev": true, + "requires": { + "unist-util-stringify-position": "^1.1.1" + } + }, + "viewport-dimensions": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz", + "integrity": "sha1-3nQHR9tTh/0XJfUXXpG6x2r982w=", + "dev": true + }, + "vinyl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + } + } + }, + "vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", + "dev": true, + "requires": { + "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" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "viz-annotation": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/viz-annotation/-/viz-annotation-0.0.3.tgz", + "integrity": "sha512-kkZ+mZHx+zfAZhBuA/PGzA7ZpKUZFjtgBqVcbPfCrOIrHjERcHyrNhxYfx1ZZu9uyZLO0i/qcoMOtY5HpavSlg==", + "dev": true, + "requires": { + "d3-shape": "~1.0.4" + }, + "dependencies": { + "d3-shape": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.0.6.tgz", + "integrity": "sha1-sJ4wXPDHxrmpjJDmtC9i2sS8/Vs=", + "dev": true, + "requires": { + "d3-path": "1" + } + } + } + }, + "vlq": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", + "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==" + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + }, + "vsce": { + "version": "1.65.0", + "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.65.0.tgz", + "integrity": "sha512-1bGyeoaxjhNVz9fVAqUzGWc1e5CxsxZFpVSnS/anRVyZju0y4DJCPi685WkBsRYU/lfp3AI1smpatuSfb2Lllg==", + "dev": true, + "requires": { + "azure-devops-node-api": "^7.2.0", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.1", + "commander": "^2.8.1", + "denodeify": "^1.2.1", + "didyoumean": "^1.2.1", + "glob": "^7.0.6", + "lodash": "^4.17.10", + "markdown-it": "^8.3.1", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "osenv": "^0.1.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^5.1.0", + "tmp": "0.0.29", + "typed-rest-client": "1.2.0", + "url-join": "^1.1.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "dependencies": { + "linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "markdown-it": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", + "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + } + } + }, + "vscode-debugadapter": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.35.0.tgz", + "integrity": "sha512-Au90Iowj6TuD5uDMaTnxOjl/9hQN0Yoky1TV1Cjjr7jPdxTQpALBRW09Y2LzkIXUVICXlAqxWL9zL8BpzI30jg==", + "requires": { + "mkdirp": "^0.5.1", + "vscode-debugprotocol": "1.35.0" + } + }, + "vscode-debugadapter-testsupport": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/vscode-debugadapter-testsupport/-/vscode-debugadapter-testsupport-1.35.0.tgz", + "integrity": "sha512-4emLt6JOk4iKqC2aWNJupOtrK6JwYAZ6KppqvKASN6B1s063VoqI18QhUB1CeoKwNaN1LIG3VPv2xM8HKOjyDA==", + "dev": true, + "requires": { + "vscode-debugprotocol": "1.35.0" + } + }, + "vscode-debugprotocol": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.35.0.tgz", + "integrity": "sha512-+OMm11R1bGYbpIJ5eQIkwoDGFF4GvBz3Ztl6/VM+/RNNb2Gjk2c0Ku+oMmfhlTmTlPCpgHBsH4JqVCbUYhu5bA==" + }, + "vscode-extension-telemetry": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.4.tgz", + "integrity": "sha512-9U2pUZ/YwZBfA8CkBrHwMxjnq9Ab+ng8daJWJzEQ6CAxlZyRhmck23bx2lqqpEwGWJCiuceQy4k0Me6llEB4zw==", + "requires": { + "applicationinsights": "1.7.4" + } + }, + "vscode-jsonrpc": { + "version": "6.0.0-next.5", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.5.tgz", + "integrity": "sha512-IAgsltQPwg/pXOPsdXgbUTCaO9VSKZwirZN5SGtkdYQ/R3VjeC4v00WTVvoNayWMZpoC3O9u0ogqmsKzKhVasQ==" + }, + "vscode-languageclient": { + "version": "7.0.0-next.9", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0-next.9.tgz", + "integrity": "sha512-lFO+rN/i72CM2va6iKXq1lD7pJg8J93KEXf0w0boWVqU+DJhWzLrV3pXl8Xk1nCv//qOAyhlc/nx2KZCTeRF/A==", + "requires": { + "semver": "^6.3.0", + "vscode-languageserver-protocol": "3.16.0-next.7" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "vscode-languageserver": { + "version": "7.0.0-next.7", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0-next.7.tgz", + "integrity": "sha512-6La+usoCF0OnCYIbOLBTmqgzaZSAyIBpnsLjS/g7AnI8Pv9v6yzC38VhTvBoip4/ZBwf1U8vpdwatD/qCSKeWA==", + "requires": { + "vscode-languageserver-protocol": "3.16.0-next.7" + } + }, + "vscode-languageserver-protocol": { + "version": "3.16.0-next.7", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.7.tgz", + "integrity": "sha512-tOjrg+K3RddJ547zpC9/LAgTbzadkPuHlqJFFWIcKjVhiJOh73XyY+Ngcu9wukGaTsuSGjJ0W8rlmwanixa0FQ==", + "requires": { + "vscode-jsonrpc": "6.0.0-next.5", + "vscode-languageserver-types": "3.16.0-next.3" + } + }, + "vscode-languageserver-types": { + "version": "3.16.0-next.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.3.tgz", + "integrity": "sha512-s/z5ZqSe7VpoXJ6JQcvwRiPPA3nG0nAcJ/HH03zoU6QaFfnkcgPK+HshC3WKPPnC2G08xA0iRB6h7kmyBB5Adg==" + }, + "vscode-tas-client": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/vscode-tas-client/-/vscode-tas-client-0.1.4.tgz", + "integrity": "sha512-sC+kvLUwb6ecC7+ZoxzDtvvktVUJ3jZq6mvJpfYHeLlbj4hUpNsZ79u65/mukoO8E8C7UQUCLdWdyn/evp+oNA==", + "requires": { + "tas-client": "0.0.950" + } + }, + "vscode-test": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.2.3.tgz", + "integrity": "sha512-mKRTNso33NaUULiPBFg6zRjyntjcCpIgkrogyPQuKlvoQREQR8jLKN5UD4L5rkTSD+oBhcKtaLR2/g34FexURw==", + "dev": true, + "requires": { + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.4", + "rimraf": "^2.6.3" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "vsls": { + "version": "0.3.1291", + "resolved": "https://registry.npmjs.org/vsls/-/vsls-0.3.1291.tgz", + "integrity": "sha512-8yJPN9p7k+XYyczOVtQmpun4K1CRDsw/hdnIzT/c40r5bIkpptfsBlHmmLemoIV+CAHvrTLdWKEf5OtRvdcn9A==" + }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "dev": true, + "requires": { + "browser-process-hrtime": "^0.1.2" + } + }, + "w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "dev": true, + "requires": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, + "wait-for-expect": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.1.tgz", + "integrity": "sha512-3Ha7lu+zshEG/CeHdcpmQsZnnZpPj/UsG3DuKO8FskjuDbkx3jE3845H+CuwZjA2YWYDfKMU2KhnCaXMLd3wVw==", + "dev": true + }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "watchpack": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", + "integrity": "sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA==", + "dev": true, + "requires": { + "chokidar": "^2.1.8", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "webpack": { + "version": "4.35.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.35.3.tgz", + "integrity": "sha512-xggQPwr9ILlXzz61lHzjvgoqGU08v5+Wnut19Uv3GaTtzN4xBTcwnobodrXE142EL1tOiS5WVEButooGzcQzTA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/wasm-edit": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "acorn": "^6.2.0", + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0", + "chrome-trace-event": "^1.0.0", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.0", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.3.0", + "loader-utils": "^1.1.0", + "memory-fs": "~0.4.1", + "micromatch": "^3.1.8", + "mkdirp": "~0.5.0", + "neo-async": "^2.5.0", + "node-libs-browser": "^2.0.0", + "schema-utils": "^1.0.0", + "tapable": "^1.1.0", + "terser-webpack-plugin": "^1.1.0", + "watchpack": "^1.5.0", + "webpack-sources": "^1.3.0" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "lru-cache": { + "version": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "pump": { + "version": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "terser-webpack-plugin": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", + "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^2.1.2", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "dependencies": { + "serialize-javascript": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", + "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", + "dev": true + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + } + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "webpack-bundle-analyzer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.6.0.tgz", + "integrity": "sha512-orUfvVYEfBMDXgEKAKVvab5iQ2wXneIEorGNsyuOyVYpjYrI7CUOhhXNDd3huMwQ3vNNWWlGP+hzflMFYNzi2g==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-walk": "^6.1.1", + "bfj": "^6.1.1", + "chalk": "^2.4.1", + "commander": "^2.18.0", + "ejs": "^2.6.1", + "express": "^4.16.3", + "filesize": "^3.6.1", + "gzip-size": "^5.0.0", + "lodash": "^4.17.15", + "mkdirp": "^0.5.1", + "opener": "^1.5.1", + "ws": "^6.0.0" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "filesize": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", + "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", + "dev": true + }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + } + } + } + }, + "webpack-cli": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.5.tgz", + "integrity": "sha512-w0j/s42c5UhchwTmV/45MLQnTVwRoaUTu9fM5LuyOd/8lFoCNCELDogFoecx5NzRUndO0yD/gF2b02XKMnmAWQ==", + "dev": true, + "requires": { + "chalk": "2.4.2", + "cross-spawn": "6.0.5", + "enhanced-resolve": "4.1.0", + "findup-sync": "3.0.0", + "global-modules": "2.0.0", + "import-local": "2.0.0", + "interpret": "1.2.0", + "loader-utils": "1.2.3", + "supports-color": "6.1.0", + "v8-compile-cache": "2.0.3", + "yargs": "13.2.4" + }, + "dependencies": { + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "requires": { + "global-prefix": "^3.0.0" + } + }, + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", + "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.0" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "webpack-fix-default-import-plugin": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/webpack-fix-default-import-plugin/-/webpack-fix-default-import-plugin-1.0.3.tgz", + "integrity": "sha1-iCuOTRqpPEjLj9r4Rvx52G+C8U8=", + "dev": true + }, + "webpack-merge": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.1.tgz", + "integrity": "sha512-4p8WQyS98bUJcCvFMbdGZyZmsKuWjWVnVHnAS3FFg0HDaRVrPbkivx2RYCre8UiemD67RsiFFLfn4JhLAin8Vw==", + "dev": true, + "requires": { + "lodash": "^4.17.5" + } + }, + "webpack-node-externals": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-1.7.2.tgz", + "integrity": "sha512-ajerHZ+BJKeCLviLUUmnyd5B4RavLF76uv3cs6KNuO8W+HuQaEs0y0L7o40NQxdPy5w0pcv8Ew7yPUAQG0UdCg==", + "dev": true + }, + "webpack-require-from": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/webpack-require-from/-/webpack-require-from-1.8.0.tgz", + "integrity": "sha512-4vaPWQZD3vl3WM2mnjWunyx56uUbPj44ZKlpPUd+Ro2jrOtZQOaB2I5FE222uIChzeFfS7A7rtcWRLraPHE7TA==", + "dev": true + }, + "webpack-sources": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", + "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "websocket-driver": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", + "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.4.0 <0.4.11", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==", + "dev": true + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", + "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "why-is-node-running": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.1.0.tgz", + "integrity": "sha512-oLmJ1uZOaKra+GDmYcUHMnVhi4CnZnlt4IE3J05ZDSEAiejeB5dMoR4a4rGcMWRy1Avx24dGTw8yxJ/+EmwPBQ==", + "dev": true, + "requires": { + "stackback": "0.0.2" + } + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "winreg": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", + "integrity": "sha1-ugZWKbepJRMOFXeRCM9UCZDpjRs=" + }, + "winston": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", + "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", + "requires": { + "async": "^2.6.1", + "diagnostics": "^1.1.1", + "is-stream": "^1.1.0", + "logform": "^2.1.1", + "one-time": "0.0.4", + "readable-stream": "^3.1.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.3.0" + } + }, + "winston-transport": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", + "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", + "requires": { + "readable-stream": "^2.3.6", + "triple-beam": "^1.2.0" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "wipe-node-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wipe-node-cache/-/wipe-node-cache-2.1.0.tgz", + "integrity": "sha512-Vdash0WV9Di/GeYW9FJrAZcPjGK4dO7M/Be/sJybguEgcM7As0uwLyvewZYqdlepoh7Rj4ZJKEdo8uX83PeNIw==", + "dev": true + }, + "wipe-webpack-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wipe-webpack-cache/-/wipe-webpack-cache-2.1.0.tgz", + "integrity": "sha512-OXzQMGpA7MnQQ8AG+uMl5mWR2ezy6fw1+DMHY+wzYP1qkF1jrek87psLBmhZEj+er4efO/GD4R8jXWFierobaA==", + "dev": true, + "requires": { + "wipe-node-cache": "^2.1.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "worker-rpc": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz", + "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==", + "dev": true, + "requires": { + "microevent.ts": "~0.1.1" + } + }, + "workerpool": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", + "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", + "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "wtfnode": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.8.0.tgz", + "integrity": "sha512-A5jm/0REykxUac1q4Q5kv+hDIiacvqVpwIoXzCQcRL7syeEKucVVOxyLLrt+jIiZoXfla3lnsxUw/cmWXIaGWA==", + "dev": true + }, + "x-is-string": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", + "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", + "dev": true + }, + "xdg-basedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", + "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + }, + "xdg-trashdir": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xdg-trashdir/-/xdg-trashdir-2.1.1.tgz", + "integrity": "sha512-KcVhPaOu2ZurYNHSRTf1+ZHORkTZGCQ+u0JHN17QixRISJq4pXOnjt/lQcehvtHL5QAKhSzKgyjrcNnPdkPBHA==", + "dev": true, + "requires": { + "@sindresorhus/df": "^2.1.0", + "mount-point": "^3.0.0", + "pify": "^2.2.0", + "user-home": "^2.0.0", + "xdg-basedir": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + }, + "dependencies": { + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + } + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, + "xmlchars": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.1.1.tgz", + "integrity": "sha512-7hew1RPJ1iIuje/Y01bGD/mXokXxegAgVS+e+E0wSi2ILHQkYAH1+JXARwTjZSM4Z4Z+c73aKspEcqj+zPPL/w==", + "dev": true + }, + "xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=", + "dev": true + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "requires": { + "camelcase": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + } + } + }, + "yargs-unparser": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz", + "integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "decamelize": "^1.2.0", + "flat": "^4.1.0", + "is-plain-obj": "^1.1.0", + "yargs": "^14.2.3" + }, + "dependencies": { + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + } + }, + "yargs-parser": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3" + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + }, + "yn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.0.tgz", + "integrity": "sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg==", + "dev": true + }, + "zeromq": { + "version": "6.0.0-beta.6", + "resolved": "https://registry.npmjs.org/zeromq/-/zeromq-6.0.0-beta.6.tgz", + "integrity": "sha512-wLf6M7pBHijl+BRltUL2VoDpgbQcOZetiX8UzycHL8CcYFxYnRrpoG5fi3UX3+Umavz1lk4/dGaQez8qiDgr/Q==", + "requires": { + "node-gyp-build": "^4.1.0" + } + }, + "zip-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.0.1.tgz", + "integrity": "sha512-c+eUhhkDpaK87G/py74wvWLtz2kzMPNCCkUApkun50ssE0oQliIQzWpTnwjB+MTKVIf2tGzIgHyqW/Y+W77ecQ==", + "dev": true, + "requires": { + "archiver-utils": "^2.0.0", + "compress-commons": "^1.2.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "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" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + } + } +} diff --git a/package.datascience-ui.dependencies.json b/package.datascience-ui.dependencies.json new file mode 100644 index 000000000000..c42689044d43 --- /dev/null +++ b/package.datascience-ui.dependencies.json @@ -0,0 +1,307 @@ +[ + "@babel/runtime", + "@babel/runtime-corejs2", + "@blueprintjs/core", + "@blueprintjs/icons", + "@blueprintjs/select", + "@emotion/hash", + "@emotion/memoize", + "@emotion/stylis", + "@emotion/unitless", + "@icons/material", + "@jupyter-widgets/controls", + "@jupyterlab/services", + "@loadable/component", + "@mapbox/polylabel", + "@nteract/markdown", + "@nteract/mathjax", + "@nteract/octicons", + "@nteract/styled-blueprintjsx", + "@nteract/transform-dataresource", + "@nteract/transform-geojson", + "@nteract/transform-model-debug", + "@nteract/transform-plotly", + "@nteract/transform-vdom", + "@nteract/transform-vega", + "@nteract/transforms", + "@nteract/vega-embed-v2", + "@nteract/vega-embed-v3", + "@phosphor/algorithm", + "@phosphor/coreutils", + "@phosphor/properties", + "@phosphor/signaling", + "ajv", + "anser", + "ansi-regex", + "ansi-to-html", + "ansi-to-react", + "asn1.js", + "babel-polyfill", + "babel-runtime", + "bail", + "base16", + "base64-js", + "bintrees", + "bn.js", + "bootstrap-less", + "brorand", + "browserify-aes", + "browserify-cipher", + "browserify-des", + "browserify-rsa", + "browserify-sign", + "buffer-xor", + "canvas", + "character-entities-legacy", + "character-reference-invalid", + "cipher-base", + "classnames", + "clsx", + "collapse-white-space", + "core-util-is", + "create-ecdh", + "create-emotion", + "create-hash", + "create-hmac", + "create-react-context", + "crypto-browserify", + "css-loader", + "d3", + "d3-array", + "d3-bboxCollide", + "d3-brush", + "d3-chord", + "d3-cloud", + "d3-collection", + "d3-color", + "d3-contour", + "d3-delaunay", + "d3-dispatch", + "d3-drag", + "d3-dsv", + "d3-ease", + "d3-force", + "d3-format", + "d3-geo", + "d3-glyphedge", + "d3-hexbin", + "d3-hierarchy", + "d3-interpolate", + "d3-path", + "d3-polygon", + "d3-quadtree", + "d3-request", + "d3-sankey-circular", + "d3-scale", + "d3-scale-chromatic", + "d3-selection", + "d3-shape", + "d3-time", + "d3-time-format", + "d3-timer", + "d3-transition", + "d3-voronoi", + "datalib", + "define-properties", + "delaunator", + "des.js", + "diffie-hellman", + "dom-helpers", + "dom4", + "elliptic", + "emotion", + "entities", + "escape-carriage", + "events", + "evp_bytestokey", + "extend", + "fast-deep-equal", + "fast-json-stable-stringify", + "fast-plist", + "function-bind", + "gud", + "has", + "hash-base", + "hash.js", + "hmac-drbg", + "hoist-non-react-statics", + "ieee754", + "inherits", + "invariant", + "is-alphabetical", + "is-alphanumerical", + "is-arguments", + "is-buffer", + "is-date-object", + "is-decimal", + "is-hexadecimal", + "is-online", + "is-plain-obj", + "is-regex", + "is-whitespace-character", + "is-word-character", + "isarray", + "json-schema-traverse", + "json-stable-stringify", + "json-stringify-pretty-compact", + "json2csv", + "json5", + "jsonify", + "labella", + "leaflet", + "linear-layout-vector", + "lodash", + "lodash.curry", + "lodash.flow", + "lru-cache", + "markdown-escapes", + "martinez-polygon-clipping", + "material-colors", + "md5.js", + "mdast-add-list-metadata", + "miller-rabin", + "minimalistic-assert", + "minimalistic-crypto-utils", + "minimist", + "moment", + "monaco-editor", + "monaco-editor-textmate", + "monaco-textmate", + "node-libs-browser", + "numeral", + "object-assign", + "object-is", + "object-keys", + "onigasm", + "parse-asn1", + "parse-entities", + "path-browserify", + "path-posix", + "pbkdf2", + "plotly.js-dist", + "polygon-offset", + "popper.js", + "process", + "prop-types", + "pseudomap", + "public-encrypt", + "public-ip", + "pure-color", + "querystringify", + "randombytes", + "randomfill", + "react", + "react-annotation", + "react-base16-styling", + "react-color", + "react-data-grid", + "react-dom", + "react-draggable", + "react-hot-loader", + "react-is", + "react-json-tree", + "react-lifecycles-compat", + "react-markdown", + "react-popper", + "react-redux", + "react-svg-pan-zoom", + "react-svgmt", + "react-table", + "react-table-hoc-fixed-columns", + "react-transition-group", + "react-virtualized", + "reactcss", + "redux", + "redux-logger", + "regexp.prototype.flags", + "regression", + "remark-parse", + "repeat-string", + "replace-ext", + "requires-port", + "resize-observer-polyfill", + "ripemd160", + "roughjs-es5", + "rxjs", + "rxjs-compat", + "safe-buffer", + "scheduler", + "semiotic", + "semiotic-mark", + "semver", + "setimmediate", + "sha.js", + "slickgrid", + "state-toggle", + "stream-browserify", + "string-hash", + "string_decoder", + "style-loader", + "styled-jsx", + "stylis-rule-sheet", + "svg-inline-react", + "svg-path-bounding-box", + "svgpath", + "timers-browserify", + "tinycolor2", + "tinyqueue", + "topojson-client", + "transformation-matrix", + "trim", + "trim-trailing-lines", + "trough", + "tslib", + "unherit", + "unified", + "uniqid", + "unist-util-is", + "unist-util-remove-position", + "unist-util-stringify-position", + "unist-util-visit", + "unist-util-visit-parents", + "uri-js", + "url-parse", + "util", + "util-deprecate", + "uuid", + "vega", + "vega-canvas", + "vega-crossfilter", + "vega-dataflow", + "vega-embed", + "vega-encode", + "vega-event-selector", + "vega-expression", + "vega-force", + "vega-functions", + "vega-geo", + "vega-hierarchy", + "vega-lite", + "vega-loader", + "vega-parser", + "vega-projection", + "vega-regression", + "vega-runtime", + "vega-scale", + "vega-scenegraph", + "vega-schema-url-parser", + "vega-selections", + "vega-statistics", + "vega-themes", + "vega-tooltip", + "vega-transforms", + "vega-util", + "vega-view", + "vega-view-transforms", + "vega-voronoi", + "vega-wordcloud", + "vfile", + "vfile-location", + "vfile-message", + "viz-annotation", + "vm-browserify", + "warning", + "x-is-string", + "xtend", + "yallist" +] diff --git a/package.json b/package.json index 691d1a17fe66..f6397c5b32bc 100644 --- a/package.json +++ b/package.json @@ -1,489 +1,3778 @@ { - "name": "python", - "displayName": "Python", - "description": "Linting, Debugging (multi-threaded, remote), Intellisense, auto-completion, code formatting, snippets, and more.", - "version": "0.3.9", - "publisher": "donjayamanne", - "license": "MIT", - "homepage": "https://github.com/DonJayamanne/pythonVSCode/blob/master/README.md", - "repository": { - "type": "git", - "url": "https://github.com/DonJayamanne/pythonVSCode" - }, - "bugs": { - "url": "https://github.com/DonJayamanne/pythonVSCode/issues" - }, - "icon": "images/icon.svg", - "galleryBanner": { - "color": "#1e415e", - "theme": "dark" - }, - "engines": { - "vscode": "^0.10.8" - }, - "categories": [ - "Languages", - "Debuggers", - "Linters", - "Snippets", - "Other" - ], - "activationEvents": [ - "onLanguage:python", - "onCommand:python.sortImports", - "onCommand:python.runtests" - ], - "main": "./out/client/extension", - "contributes": { - "snippets": [ - { - "language": "python", - "path": "./snippets/python.json" - } + "name": "python", + "displayName": "Python", + "description": "Linting, Debugging (multi-threaded, remote), Intellisense, Jupyter Notebooks, code formatting, refactoring, unit tests, snippets, and more.", + "version": "2020.9.0-dev", + "featureFlags": { + "usingNewInterpreterStorage": true + }, + "languageServerVersion": "0.5.30", + "publisher": "ms-python", + "enableProposedApi": true, + "author": { + "name": "Microsoft Corporation" + }, + "license": "MIT", + "homepage": "https://github.com/Microsoft/vscode-python", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-python" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-python/issues" + }, + "qna": "https://stackoverflow.com/questions/tagged/visual-studio-code+python", + "icon": "icon.png", + "galleryBanner": { + "color": "#1e415e", + "theme": "dark" + }, + "engines": { + "vscode": "^1.48.0" + }, + "keywords": [ + "python", + "django", + "unittest", + "multi-root ready" ], - "commands": [ - { - "command": "python.sortImports", - "title": "Python: Sort Imports" - }, - { - "command": "python.runtests", - "title": "Python: Run Unit Tests" - } + "categories": [ + "Programming Languages", + "Debuggers", + "Linters", + "Snippets", + "Formatters", + "Other", + "Extension Packs", + "Data Science", + "Machine Learning", + "Notebooks" ], - "debuggers": [ - { - "type": "python", - "label": "Python", - "enableBreakpointsFor": { - "languageIds": [ - "python" - ] - }, - "program": "./out/client/debugger/Main.js", - "runtime": "node", - "configurationAttributes": { - "launch": { - "required": [ - "program" + "activationEvents": [ + "onLanguage:python", + "onLanguage:jupyter", + "onDebugResolve:python", + "onCommand:python.execInTerminal", + "onCommand:python.sortImports", + "onCommand:python.runtests", + "onCommand:python.debugtests", + "onCommand:python.setInterpreter", + "onCommand:python.setShebangInterpreter", + "onCommand:python.viewTestUI", + "onCommand:python.viewLanguageServerOutput", + "onCommand:python.viewTestOutput", + "onCommand:python.viewOutput", + "onCommand:python.datascience.viewJupyterOutput", + "onCommand:python.datascience.export", + "onCommand:python.datascience.exportAsPythonScript", + "onCommand:python.datascience.exportToHTML", + "onCommand:python.datascience.exportToPDF", + "onCommand:python.selectAndRunTestMethod", + "onCommand:python.selectAndDebugTestMethod", + "onCommand:python.selectAndRunTestFile", + "onCommand:python.runCurrentTestFile", + "onCommand:python.runFailedTests", + "onCommand:python.execSelectionInTerminal", + "onCommand:python.execSelectionInDjangoShell", + "onCommand:python.buildWorkspaceSymbols", + "onCommand:python.startREPL", + "onCommand:python.goToPythonObject", + "onCommand:python.setLinter", + "onCommand:python.enableLinting", + "onCommand:python.createTerminal", + "onCommand:python.discoverTests", + "onCommand:python.configureTests", + "onCommand:python.switchOffInsidersChannel", + "onCommand:python.switchToDailyChannel", + "onCommand:python.switchToWeeklyChannel", + "onCommand:python.clearWorkspaceInterpreter", + "onCommand:python.resetInterpreterSecurityStorage", + "onCommand:python.datascience.createnewnotebook", + "onCommand:python.startPage.open", + "onCommand:python.datascience.createnewinteractive", + "onCommand:python.datascience.importnotebook", + "onCommand:python.datascience.importnotebookfile", + "onCommand:python.datascience.opennotebook", + "onCommand:python.datascience.opennotebookInPreviewEditor", + "onCommand:python.datascience.selectjupyteruri", + "onCommand:python.datascience.exportfileasnotebook", + "onCommand:python.datascience.exportfileandoutputasnotebook", + "onCommand:python.datascience.selectJupyterInterpreter", + "onCommand:python.datascience.selectjupytercommandline", + "onCommand:python.enableSourceMapSupport", + "onNotebookEditor:jupyter-notebook", + "workspaceContains:mspythonconfig.json", + "workspaceContains:pyproject.toml" + ], + "main": "./out/client/extension", + "contributes": { + "snippets": [ + { + "language": "python", + "path": "./snippets/python.json" + } + ], + "keybindings": [ + { + "command": "python.execSelectionInTerminal", + "key": "shift+enter", + "when": "editorTextFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && !python.datascience.ownsSelection && !notebookEditorFocused" + }, + { + "command": "python.datascience.execSelectionInteractive", + "key": "shift+enter", + "when": "editorTextFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && python.datascience.ownsSelection && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.runcurrentcelladvance", + "key": "shift+enter", + "when": "editorTextFocus && !editorHasSelection && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.runcurrentcell", + "key": "ctrl+enter", + "when": "editorTextFocus && !editorHasSelection && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.runcurrentcellandaddbelow", + "key": "alt+enter", + "when": "editorTextFocus && !editorHasSelection && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "mac": "F", + "win": "F", + "linux": "F", + "key": "F", + "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", + "command": "notebook.find" + }, + { + "mac": "K", + "win": "K", + "linux": "K", + "key": "K", + "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", + "command": "list.focusUp" + }, + { + "mac": "J", + "win": "J", + "linux": "J", + "key": "J", + "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", + "command": "list.focusDown" + }, + { + "mac": "A", + "win": "A", + "linux": "A", + "key": "A", + "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", + "command": "notebook.cell.insertCodeCellAbove" + }, + { + "mac": "B", + "win": "B", + "linux": "B", + "key": "B", + "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", + "command": "notebook.cell.insertCodeCellBelow" + }, + { + "mac": "D D", + "win": "D D", + "linux": "D D", + "key": "D D", + "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", + "command": "notebook.cell.delete" + }, + { + "mac": "Z", + "win": "Z", + "linux": "Z", + "key": "Z", + "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", + "command": "notebook.undo" + }, + { + "mac": "C", + "win": "C", + "linux": "C", + "key": "C", + "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", + "command": "notebook.cell.copy" + }, + { + "mac": "X", + "win": "X", + "linux": "X", + "key": "X", + "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", + "command": "notebook.cell.cut" + }, + { + "mac": "V", + "win": "V", + "linux": "V", + "key": "V", + "when": "notebookEditorFocused && !inputFocus && notebookViewType == jupyter-notebook", + "command": "notebook.cell.paste" + }, + { + "mac": "ctrl+shift+-", + "win": "ctrl+shift+-", + "linux": "ctrl+shift+-", + "when": "editorTextFocus && inputFocus && notebookEditorFocused && notebookViewType == jupyter-notebook", + "command": "notebook.cell.split" + }, + { + "command": "python.datascience.insertCellBelowPosition", + "key": "ctrl+; s", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.insertCellBelow", + "key": "ctrl+; b", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.insertCellAbove", + "key": "ctrl+; a", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.deleteCells", + "key": "ctrl+; x", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.extendSelectionByCellAbove", + "key": "ctrl+alt+shift+[", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.extendSelectionByCellBelow", + "key": "ctrl+alt+shift+]", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.moveCellsUp", + "key": "ctrl+; u", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.moveCellsDown", + "key": "ctrl+; d", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.changeCellToMarkdown", + "key": "ctrl+; m", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.changeCellToCode", + "key": "ctrl+; c", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.gotoNextCellInFile", + "key": "ctrl+alt+]", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.gotoPrevCellInFile", + "key": "ctrl+alt+[", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.selectCellContents", + "key": "ctrl+alt+\\", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.selectCell", + "key": "ctrl+alt+shift+\\", + "when": "editorTextFocus && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + } + ], + "commands": [ + { + "command": "python.enableSourceMapSupport", + "title": "%python.command.python.enableSourceMapSupport.title%", + "category": "Python" + }, + { + "command": "python.sortImports", + "title": "%python.command.python.sortImports.title%", + "category": "Python Refactor" + }, + { + "command": "python.startREPL", + "title": "%python.command.python.startREPL.title%", + "category": "Python" + }, + { + "command": "python.createTerminal", + "title": "%python.command.python.createTerminal.title%", + "category": "Python" + }, + { + "command": "python.buildWorkspaceSymbols", + "title": "%python.command.python.buildWorkspaceSymbols.title%", + "category": "Python" + }, + { + "command": "python.openTestNodeInEditor", + "title": "Open", + "icon": { + "light": "resources/light/open-file.svg", + "dark": "resources/dark/open-file.svg" + } + }, + { + "command": "python.runTestNode", + "title": "Run", + "icon": { + "light": "resources/light/start.svg", + "dark": "resources/dark/start.svg" + } + }, + { + "command": "python.debugTestNode", + "title": "Debug", + "icon": { + "light": "resources/light/debug.svg", + "dark": "resources/dark/debug.svg" + } + }, + { + "command": "python.runtests", + "title": "%python.command.python.runtests.title%", + "category": "Python", + "icon": { + "light": "resources/light/run-tests.svg", + "dark": "resources/dark/run-tests.svg" + } + }, + { + "command": "python.debugtests", + "title": "%python.command.python.debugtests.title%", + "category": "Python", + "icon": { + "light": "resources/light/debug.svg", + "dark": "resources/dark/debug.svg" + } + }, + { + "command": "python.execInTerminal", + "title": "%python.command.python.execInTerminal.title%", + "category": "Python" + }, + { + "command": "python.execInTerminal-icon", + "title": "%python.command.python.execInTerminal.title%", + "category": "Python", + "icon": { + "light": "resources/light/run-file.svg", + "dark": "resources/dark/run-file.svg" + } + }, + { + "command": "python.setInterpreter", + "title": "%python.command.python.setInterpreter.title%", + "category": "Python" + }, + { + "command": "python.switchOffInsidersChannel", + "title": "%python.command.python.switchOffInsidersChannel.title%", + "category": "Python" + }, + { + "command": "python.switchToDailyChannel", + "title": "%python.command.python.switchToDailyChannel.title%", + "category": "Python" + }, + { + "command": "python.switchToWeeklyChannel", + "title": "%python.command.python.switchToWeeklyChannel.title%", + "category": "Python" + }, + { + "command": "python.clearWorkspaceInterpreter", + "title": "%python.command.python.clearWorkspaceInterpreter.title%", + "category": "Python" + }, + { + "command": "python.resetInterpreterSecurityStorage", + "title": "%python.command.python.resetInterpreterSecurityStorage.title%", + "category": "Python" + }, + { + "command": "python.refactorExtractVariable", + "title": "%python.command.python.refactorExtractVariable.title%", + "category": "Python Refactor" + }, + { + "command": "python.refactorExtractMethod", + "title": "%python.command.python.refactorExtractMethod.title%", + "category": "Python Refactor" + }, + { + "command": "python.viewTestOutput", + "title": "%python.command.python.viewTestOutput.title%", + "category": "Python", + "icon": { + "light": "resources/light/repl.svg", + "dark": "resources/dark/repl.svg" + } + }, + { + "command": "python.datascience.viewJupyterOutput", + "title": "%python.command.python.datascience.viewJupyterOutput.title%", + "category": "Python" + }, + { + "command": "python.datascience.export", + "title": "%DataScience.notebookExportAs%", + "category": "Python", + "icon": { + "light": "resources/light/export_to_python.svg", + "dark": "resources/dark/export_to_python.svg" + }, + "enablement": "notebookViewType == jupyter-notebook && python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.exportAsPythonScript", + "title": "%python.command.python.datascience.exportAsPythonScript.title%", + "category": "Python" + }, + { + "command": "python.datascience.exportToHTML", + "title": "%python.command.python.datascience.exportToHTML.title%", + "category": "Python" + }, + { + "command": "python.datascience.exportToPDF", + "title": "%python.command.python.datascience.exportToPDF.title%", + "category": "Python" + }, + { + "command": "python.datascience.selectJupyterInterpreter", + "title": "%python.command.python.datascience.selectJupyterInterpreter.title%", + "category": "Python" + }, + { + "command": "python.viewLanguageServerOutput", + "title": "%python.command.python.viewLanguageServerOutput.title%", + "category": "Python", + "enablement": "python.hasLanguageServerOutputChannel" + }, + { + "command": "python.viewOutput", + "title": "%python.command.python.viewOutput.title%", + "category": "Python", + "icon": { + "light": "resources/light/repl.svg", + "dark": "resources/dark/repl.svg" + } + }, + { + "command": "python.selectAndRunTestMethod", + "title": "%python.command.python.selectAndRunTestMethod.title%", + "category": "Python" + }, + { + "command": "python.selectAndDebugTestMethod", + "title": "%python.command.python.selectAndDebugTestMethod.title%", + "category": "Python" + }, + { + "command": "python.selectAndRunTestFile", + "title": "%python.command.python.selectAndRunTestFile.title%", + "category": "Python" + }, + { + "command": "python.runCurrentTestFile", + "title": "%python.command.python.runCurrentTestFile.title%", + "category": "Python" + }, + { + "command": "python.runFailedTests", + "title": "%python.command.python.runFailedTests.title%", + "category": "Python", + "icon": { + "light": "resources/light/run-failed-tests.svg", + "dark": "resources/dark/run-failed-tests.svg" + } + }, + { + "command": "python.discoverTests", + "title": "%python.command.python.discoverTests.title%", + "category": "Python", + "icon": { + "light": "resources/light/refresh.svg", + "dark": "resources/dark/refresh.svg" + } + }, + { + "command": "python.discoveringTests", + "title": "%python.command.python.discoveringTests.title%", + "category": "Python", + "icon": { + "light": "resources/light/discovering-tests.svg", + "dark": "resources/dark/discovering-tests.svg" + } + }, + { + "command": "python.stopTests", + "title": "%python.command.python.stopTests.title%", + "category": "Python", + "icon": { + "light": "resources/light/stop.svg", + "dark": "resources/dark/stop.svg" + } + }, + { + "command": "python.configureTests", + "title": "%python.command.python.configureTests.title%", + "category": "Python" + }, + { + "command": "python.execSelectionInTerminal", + "title": "%python.command.python.execSelectionInTerminal.title%", + "category": "Python" + }, + { + "command": "python.execSelectionInDjangoShell", + "title": "%python.command.python.execSelectionInDjangoShell.title%", + "category": "Python" + }, + { + "command": "python.goToPythonObject", + "title": "%python.command.python.goToPythonObject.title%", + "category": "Python" + }, + { + "command": "python.setLinter", + "title": "%python.command.python.setLinter.title%", + "category": "Python" + }, + { + "command": "python.enableLinting", + "title": "%python.command.python.enableLinting.title%", + "category": "Python" + }, + { + "command": "python.runLinting", + "title": "%python.command.python.runLinting.title%", + "category": "Python" + }, + { + "command": "python.datascience.runcurrentcell", + "title": "%python.command.python.datascience.runcurrentcell.title%", + "category": "Python" + }, + { + "command": "python.datascience.debugcell", + "title": "%python.command.python.datascience.debugcell.title%", + "category": "Python" + }, + { + "command": "python.datascience.debugstepover", + "title": "%python.command.python.datascience.debugstepover.title%", + "category": "Python" + }, + { + "command": "python.datascience.debugstop", + "title": "%python.command.python.datascience.debugstop.title%", + "category": "Python" + }, + { + "command": "python.datascience.debugcontinue", + "title": "%python.command.python.datascience.debugcontinue.title%", + "category": "Python" + }, + { + "command": "python.datascience.insertCellBelowPosition", + "title": "%python.command.python.datascience.insertCellBelowPosition.title%", + "category": "Python" + }, + { + "command": "python.datascience.insertCellBelow", + "title": "%python.command.python.datascience.insertCellBelow.title%", + "category": "Python" + }, + { + "command": "python.datascience.insertCellAbove", + "title": "%python.command.python.datascience.insertCellAbove.title%", + "category": "Python" + }, + { + "command": "python.datascience.deleteCells", + "title": "%python.command.python.datascience.deleteCells.title%", + "category": "Python" + }, + { + "command": "python.datascience.selectCell", + "title": "%python.command.python.datascience.selectCell.title%", + "category": "Python" + }, + { + "command": "python.datascience.selectCellContents", + "title": "%python.command.python.datascience.selectCellContents.title%", + "category": "Python" + }, + { + "command": "python.datascience.extendSelectionByCellAbove", + "title": "%python.command.python.datascience.extendSelectionByCellAbove.title%", + "category": "Python" + }, + { + "command": "python.datascience.extendSelectionByCellBelow", + "title": "%python.command.python.datascience.extendSelectionByCellBelow.title%", + "category": "Python" + }, + { + "command": "python.datascience.moveCellsUp", + "title": "%python.command.python.datascience.moveCellsUp.title%", + "category": "Python" + }, + { + "command": "python.datascience.moveCellsDown", + "title": "%python.command.python.datascience.moveCellsDown.title%", + "category": "Python" + }, + { + "command": "python.datascience.changeCellToMarkdown", + "title": "%python.command.python.datascience.changeCellToMarkdown.title%", + "category": "Python" + }, + { + "command": "python.datascience.changeCellToCode", + "title": "%python.command.python.datascience.changeCellToCode.title%", + "category": "Python" + }, + { + "command": "python.datascience.gotoNextCellInFile", + "title": "%python.command.python.datascience.gotoNextCellInFile.title%", + "category": "Python" + }, + { + "command": "python.datascience.gotoPrevCellInFile", + "title": "%python.command.python.datascience.gotoPrevCellInFile.title%", + "category": "Python" + }, + { + "command": "python.datascience.runcurrentcelladvance", + "title": "%python.command.python.datascience.runcurrentcelladvance.title%", + "category": "Python" + }, + { + "command": "python.datascience.runcurrentcellandallbelow.palette", + "title": "%python.command.python.datascience.runcurrentcellandallbelow.palette.title%", + "category": "Python" + }, + { + "command": "python.datascience.runallcellsabove.palette", + "title": "%python.command.python.datascience.runallcellsabove.palette.title%", + "category": "Python" + }, + { + "command": "python.datascience.debugcurrentcell.palette", + "title": "%python.command.python.datascience.debugcurrentcell.palette.title%", + "category": "Python" + }, + { + "command": "python.datascience.execSelectionInteractive", + "title": "%python.command.python.datascience.execSelectionInteractive.title%", + "category": "Python" + }, + { + "command": "python.datascience.createnewinteractive", + "title": "%python.command.python.datascience.createnewinteractive.title%", + "category": "Python" + }, + { + "command": "python.datascience.runFileInteractive", + "title": "%python.command.python.datascience.runFileInteractive.title%", + "category": "Python" + }, + { + "command": "python.datascience.debugFileInteractive", + "title": "%python.command.python.datascience.debugFileInteractive.title%", + "category": "Python" + }, + { + "command": "python.datascience.runallcells", + "title": "%python.command.python.datascience.runallcells.title%", + "category": "Python" + }, + { + "command": "python.datascience.runallcellsabove", + "title": "%python.command.python.datascience.runallcellsabove.title%", + "category": "Python" + }, + { + "command": "python.datascience.runcellandallbelow", + "title": "%python.command.python.datascience.runcellandallbelow.title%", + "category": "Python" + }, + { + "command": "python.datascience.runcell", + "title": "%python.command.python.datascience.runcell.title%", + "category": "Python" + }, + { + "command": "python.datascience.runtoline", + "title": "%python.command.python.datascience.runtoline.title%", + "category": "Python" + }, + { + "command": "python.datascience.runfromline", + "title": "%python.command.python.datascience.runfromline.title%", + "category": "Python" + }, + { + "command": "python.datascience.selectjupyteruri", + "title": "%python.command.python.datascience.selectjupyteruri.title%", + "category": "Python", + "when": "python.datascience.featureenabled" + }, + { + "command": "python.datascience.selectjupytercommandline", + "title": "%python.command.python.datascience.selectjupytercommandline.title%", + "category": "Python", + "when": "python.datascience.featureenabled" + }, + { + "command": "python.datascience.importnotebook", + "title": "%python.command.python.datascience.importnotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.importnotebookfile", + "title": "%python.command.python.datascience.importnotebookfile.title%", + "category": "Python" + }, + { + "command": "python.datascience.opennotebook", + "title": "%python.command.python.datascience.opennotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.opennotebookInPreviewEditor", + "title": "%python.command.python.datascience.opennotebookInPreviewEditor.title%", + "category": "Python" + }, + { + "command": "python.datascience.exportoutputasnotebook", + "title": "%python.command.python.datascience.exportoutputasnotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.exportfileasnotebook", + "title": "%python.command.python.datascience.exportfileasnotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.exportfileandoutputasnotebook", + "title": "%python.command.python.datascience.exportfileandoutputasnotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.undocells", + "title": "%python.command.python.datascience.undocells.title%", + "category": "Python" + }, + { + "command": "python.datascience.redocells", + "title": "%python.command.python.datascience.redocells.title%", + "category": "Python" + }, + { + "command": "python.datascience.notebookeditor.undocells", + "title": "%python.command.python.datascience.undocells.title%", + "category": "Python" + }, + { + "command": "python.datascience.notebookeditor.redocells", + "title": "%python.command.python.datascience.redocells.title%", + "category": "Python" + }, + { + "command": "python.datascience.removeallcells", + "title": "%python.command.python.datascience.removeallcells.title%", + "category": "Python" + }, + { + "command": "python.datascience.interruptkernel", + "title": "%python.command.python.datascience.interruptkernel.title%", + "category": "Python" + }, + { + "command": "python.datascience.restartkernel", + "title": "%python.command.python.datascience.restartkernel.title%", + "category": "Python" + }, + { + "command": "python.datascience.notebookeditor.removeallcells", + "title": "%python.command.python.datascience.notebookeditor.removeallcells.title%", + "category": "Python" + }, + { + "command": "python.datascience.notebookeditor.interruptkernel", + "title": "%python.command.python.datascience.interruptkernel.title%", + "category": "Python" + }, + { + "command": "python.datascience.notebookeditor.restartkernel", + "title": "%python.command.python.datascience.restartkernel.title%", + "category": "Python", + "icon": { + "light": "resources/light/restart-kernel.svg", + "dark": "resources/dark/restart-kernel.svg" + }, + "enablement": "python.datascience.notebookeditor.canrestartNotebookkernel" + }, + { + "command": "python.datascience.notebookeditor.trust", + "title": "%DataScience.trustNotebookCommandTitle%", + "category": "Python", + "icon": { + "light": "resources/light/un-trusted.svg", + "dark": "resources/dark/un-trusted.svg" + }, + "enablement": "notebookViewType == jupyter-notebook && !python.datascience.isnotebooktrusted && python.datascience.trustfeatureenabled" + }, + { + "command": "python.datascience.notebookeditor.runallcells", + "title": "%python.command.python.datascience.notebookeditor.runallcells.title%", + "category": "Python" + }, + { + "command": "python.datascience.notebookeditor.runselectedcell", + "title": "%python.command.python.datascience.notebookeditor.runselectedcell.title%", + "category": "Python" + }, + { + "command": "python.datascience.notebookeditor.addcellbelow", + "title": "%python.command.python.datascience.notebookeditor.addcellbelow.title%", + "category": "Python" + }, + { + "command": "python.datascience.notebookeditor.removeallcells", + "title": "%python.command.python.datascience.notebookeditor.removeallcells.title%", + "category": "Python" + }, + { + "command": "python.datascience.notebookeditor.expandallcells", + "title": "%python.command.python.datascience.notebookeditor.expandallcells.title%", + "category": "Python" + }, + { + "command": "python.datascience.notebookeditor.collapseallcells", + "title": "%python.command.python.datascience.notebookeditor.collapseallcells.title%", + "category": "Python" + }, + { + "command": "python.datascience.expandallcells", + "title": "%python.command.python.datascience.expandallcells.title%", + "category": "Python" + }, + { + "command": "python.datascience.collapseallcells", + "title": "%python.command.python.datascience.collapseallcells.title%", + "category": "Python" + }, + { + "command": "python.datascience.addcellbelow", + "title": "%python.command.python.datascience.addcellbelow.title%", + "category": "Python" + }, + { + "command": "python.datascience.createnewnotebook", + "title": "%python.command.python.datascience.createnewnotebook.title%", + "category": "Python" + }, + { + "command": "python.startPage.open", + "title": "%python.command.python.startPage.open.title%", + "category": "Python" + }, + { + "command": "python.datascience.scrolltocell", + "title": "%python.command.python.datascience.scrolltocell.title%", + "category": "Python" + }, + { + "command": "python.analysis.clearCache", + "title": "%python.command.python.analysis.clearCache.title%", + "category": "Python" + }, + { + "command": "python.datascience.switchKernel", + "title": "%DataScience.selectKernel%", + "category": "Python", + "enablement": "python.datascience.isnativeactive" + }, + { + "command": "python.datascience.gatherquality", + "title": "DataScience.gatherQuality", + "category": "Python" + }, + { + "command": "python.datascience.latestExtension", + "title": "DataScience.latestExtension", + "category": "Python" + }, + { + "command": "python.analysis.restartLanguageServer", + "title": "%python.command.python.analysis.restartLanguageServer.title%", + "category": "Python" + } + ], + "menus": { + "editor/context": [ + { + "command": "python.refactorExtractVariable", + "title": "Refactor: Extract Variable", + "group": "Refactor", + "when": "editorHasSelection && editorLangId == python && !notebookEditorFocused" + }, + { + "command": "python.refactorExtractMethod", + "title": "Refactor: Extract Method", + "group": "Refactor", + "when": "editorHasSelection && editorLangId == python && !notebookEditorFocused" + }, + { + "command": "python.sortImports", + "title": "Refactor: Sort Imports", + "group": "Refactor", + "when": "editorLangId == python && !notebookEditorFocused" + }, + { + "command": "python.execSelectionInTerminal", + "group": "Python", + "when": "editorFocus && editorLangId == python" + }, + { + "command": "python.execSelectionInDjangoShell", + "group": "Python", + "when": "editorHasSelection && editorLangId == python && python.isDjangoProject" + }, + { + "when": "resourceLangId == python", + "command": "python.execInTerminal", + "group": "Python" + }, + { + "when": "resourceLangId == python", + "command": "python.runCurrentTestFile", + "group": "Python" + }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.runallcells", + "group": "Python2" + }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.runcurrentcell", + "group": "Python2" + }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.runcurrentcelladvance", + "group": "Python2" + }, + { + "command": "python.datascience.runFileInteractive", + "group": "Python2", + "when": "editorFocus && editorLangId == python && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.runfromline", + "group": "Python2", + "when": "editorFocus && editorLangId == python && python.datascience.ownsSelection && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.runtoline", + "group": "Python2", + "when": "editorFocus && editorLangId == python && python.datascience.ownsSelection && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.execSelectionInteractive", + "group": "Python2", + "when": "editorFocus && editorLangId == python && python.datascience.featureenabled && python.datascience.ownsSelection && !notebookEditorFocused" + }, + { + "when": "editorFocus && editorLangId == python && resourceLangId == jupyter && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.importnotebook", + "group": "Python3@1" + }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.exportfileasnotebook", + "group": "Python3@2" + }, + { + "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.exportfileandoutputasnotebook", + "group": "Python3@3" + } ], - "properties": { - "program": { - "type": "string", - "description": "Absolute path to the program.", - "default": "${file}" - }, - "pythonPath": { - "type": "string", - "description": "Path (fully qualified) to python executable.", - "default": "" - }, - "args": { - "type": "array", - "description": "Command line arguments passed to the program", - "default": [], - "items": { - "type": "string" + "editor/title": [ + { + "command": "python.execInTerminal-icon", + "title": "%python.command.python.execInTerminal.title%", + "group": "navigation", + "when": "resourceLangId == python && python.showPlayIcon" + }, + { + "command": "python.datascience.notebookeditor.restartkernel", + "title": "%python.command.python.datascience.restartkernel.title%", + "group": "navigation", + "when": "resourceLangId == jupyter && notebookViewType == 'jupyter-notebook'" + }, + { + "command": "python.datascience.notebookeditor.trust", + "title": "%DataScience.trustNotebookCommandTitle%", + "group": "navigation@1", + "when": "resourceLangId == jupyter && notebookViewType == 'jupyter-notebook' && !python.datascience.isnotebooktrusted && python.datascience.trustfeatureenabled" + }, + { + "command": "python.datascience.export", + "title": "%DataScience.notebookExportAs%", + "group": "navigation", + "when": "resourceLangId == jupyter && notebookViewType == 'jupyter-notebook' && python.datascience.isnotebooktrusted" } - }, - "stopOnEntry": { - "type": "boolean", - "description": "Automatically stop after launch.", - "default": true - }, - "externalConsole": { - "type": "boolean", - "description": "Launch debug target in external console window.", - "default": false - }, - "cwd": { - "type": "string", - "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave null).", - "default": null - }, - "debugOptions": { - "type": "array", - "description": "Advanced options, view read me for further details.", - "items": { - "type": "string", - "enum": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput", - "DebugStdLib", - "BreakOnSystemExitZero", - "DjangoDebugging" - ] - }, - "default": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ] - }, - "exceptionHandling": { - "description": "List of exception types and how they are handled during debugging (ignore, always break or break only if unhandled).", - "properties": { - "ignore": { + ], + "explorer/context": [ + { + "when": "resourceLangId == python && !busyTests && !notebookEditorFocused", + "command": "python.runtests", + "group": "Python" + }, + { + "when": "resourceLangId == python && !busyTests && !notebookEditorFocused", + "command": "python.debugtests", + "group": "Python" + }, + { + "when": "resourceLangId == python", + "command": "python.execInTerminal", + "group": "Python" + }, + { + "when": "resourceLangId == python && python.datascience.featureenabled && !notebookEditorFocused", + "command": "python.datascience.runFileInteractive", + "group": "Python2" + }, + { + "when": "resourceLangId == jupyter", + "command": "python.datascience.opennotebook", + "group": "Python" + }, + { + "when": "resourceLangId == jupyter && python.vscode.channel == 'insiders'", + "command": "python.datascience.opennotebookInPreviewEditor", + "group": "Python" + }, + { + "when": "resourceLangId == jupyter", + "command": "python.datascience.importnotebookfile", + "group": "Python" + } + ], + "commandPalette": [ + { + "command": "python.datascience.exportAsPythonScript", + "title": "%python.command.python.datascience.exportAsPythonScript.title%", + "category": "Python", + "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.exportToHTML", + "title": "%python.command.python.datascience.exportToHTML.title%", + "category": "Python", + "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.exportToPDF", + "title": "%python.command.python.datascience.exportToPDF.title%", + "category": "Python", + "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted" + }, + { + "command": "python.switchOffInsidersChannel", + "title": "%python.command.python.switchOffInsidersChannel.title%", + "category": "Python", + "when": "config.python.insidersChannel != 'default'" + }, + { + "command": "python.switchToDailyChannel", + "title": "%python.command.python.switchToDailyChannel.title%", + "category": "Python", + "when": "config.python.insidersChannel != 'daily'" + }, + { + "command": "python.switchToWeeklyChannel", + "title": "%python.command.python.switchToWeeklyChannel.title%", + "category": "Python", + "when": "config.python.insidersChannel != 'weekly'" + }, + { + "command": "python.clearWorkspaceInterpreter", + "title": "%python.command.python.clearWorkspaceInterpreter.title%", + "category": "Python" + }, + { + "command": "python.resetInterpreterSecurityStorage", + "title": "%python.command.python.resetInterpreterSecurityStorage.title%", + "category": "Python" + }, + { + "command": "python.viewOutput", + "title": "%python.command.python.viewOutput.title%", + "category": "Python" + }, + { + "command": "python.runTestNode", + "title": "Run", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.discoveringTests", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.stopTests", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.debugTestNode", + "title": "Debug", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.openTestNodeInEditor", + "title": "Open", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.insertCellBelowPosition", + "title": "%python.command.python.datascience.insertCellBelowPosition.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.insertCellBelow", + "title": "%python.command.python.datascience.insertCellBelow.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.insertCellAbove", + "title": "%python.command.python.datascience.insertCellAbove.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.deleteCells", + "title": "%python.command.python.datascience.deleteCells.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.selectCell", + "title": "%python.command.python.datascience.selectCell.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.selectCellContents", + "title": "%python.command.python.datascience.selectCellContents.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.extendSelectionByCellAbove", + "title": "%python.command.python.datascience.extendSelectionByCellAbove.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.extendSelectionByCellBelow", + "title": "%python.command.python.datascience.extendSelectionByCellBelow.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.moveCellsUp", + "title": "%python.command.python.datascience.moveCellsUp.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.moveCellsDown", + "title": "%python.command.python.datascience.moveCellsDown.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.changeCellToMarkdown", + "title": "%python.command.python.datascience.changeCellToMarkdown.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.changeCellToCode", + "title": "%python.command.python.datascience.changeCellToCode.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.gotoNextCellInFile", + "title": "%python.command.python.datascience.gotoNextCellInFile.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.gotoPrevCellInFile", + "title": "%python.command.python.datascience.gotoPrevCellInFile.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.runcurrentcell", + "title": "%python.command.python.datascience.runcurrentcell.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, + { + "command": "python.datascience.runcurrentcelladvance", + "title": "%python.command.python.datascience.runcurrentcelladvance.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, + { + "command": "python.datascience.runcurrentcellandallbelow.palette", + "title": "%python.command.python.datascience.runcurrentcellandallbelow.palette.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, + { + "command": "python.datascience.runallcellsabove.palette", + "title": "%python.command.python.datascience.runallcellsabove.palette.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, + { + "command": "python.datascience.debugcurrentcell.palette", + "title": "%python.command.python.datascience.debugcurrentcell.palette.title%", + "category": "Python", + "when": "editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled" + }, + { + "command": "python.datascience.createnewinteractive", + "title": "%python.command.python.datascience.createnewinteractive.title%", + "category": "Python", + "when": "python.datascience.featureenabled" + }, + { + "command": "python.datascience.runallcells", + "title": "%python.command.python.datascience.runallcells.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, + { + "command": "python.datascience.scrolltocell", + "title": "%python.command.python.datascience.scrolltocell.title%", + "category": "Python", + "when": "false" + }, + { + "command": "python.datascience.debugcell", + "title": "%python.command.python.datascience.debugcell.title%", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.runcell", + "title": "%python.command.python.datascience.runcell.title%", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.runFileInteractive", + "title": "%python.command.python.datascience.runFileInteractive.title%", + "category": "Python", + "when": "editorLangId == python && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.debugFileInteractive", + "title": "%python.command.python.datascience.debugFileInteractive.title%", + "category": "Python", + "when": "editorLangId == python && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.importnotebook", + "title": "%python.command.python.datascience.importnotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.opennotebook", + "title": "%python.command.python.datascience.opennotebook.title%", + "category": "Python" + }, + { + "command": "python.datascience.exportfileasnotebook", + "title": "%python.command.python.datascience.exportfileasnotebook.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonorinteractiveeactive && !notebookEditorFocused" + }, + { + "command": "python.datascience.exportfileandoutputasnotebook", + "title": "%python.command.python.datascience.exportfileandoutputasnotebook.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonorinteractiveeactive && !notebookEditorFocused" + }, + { + "command": "python.datascience.undocells", + "title": "%python.command.python.datascience.undocells.title%", + "category": "Python", + "when": "python.datascience.haveinteractivecells && python.datascience.featureenabled && python.datascience.ispythonorinteractiveeactive && !notebookEditorFocused" + }, + { + "command": "python.datascience.redocells", + "title": "%python.command.python.datascience.redocells.title%", + "category": "Python", + "when": "python.datascience.haveredoablecells && python.datascience.featureenabled && python.datascience.ispythonorinteractiveornativeeactive && !notebookEditorFocused" + }, + { + "command": "python.datascience.removeallcells", + "title": "%python.command.python.datascience.removeallcells.title%", + "category": "Python", + "when": "python.datascience.haveinteractivecells && python.datascience.featureenabled && python.datascience.ispythonorinteractiveeactive" + }, + { + "command": "python.datascience.interruptkernel", + "title": "%python.command.python.datascience.interruptkernel.title%", + "category": "Python", + "when": "python.datascience.haveinteractive && python.datascience.featureenabled && python.datascience.ispythonorinteractiveeactive" + }, + { + "command": "python.datascience.restartkernel", + "title": "%python.command.python.datascience.restartkernel.title%", + "category": "Python", + "when": "python.datascience.haveinteractive && python.datascience.featureenabled && python.datascience.ispythonorinteractiveeactive" + }, + { + "command": "python.datascience.notebookeditor.undocells", + "title": "%python.command.python.datascience.undocells.title%", + "category": "Python", + "when": "python.datascience.haveinteractivecells && python.datascience.featureenabled && python.datascience.isnativeactive && !notebookEditorFocused && python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.notebookeditor.redocells", + "title": "%python.command.python.datascience.redocells.title%", + "category": "Python", + "when": "python.datascience.havenativeredoablecells && python.datascience.featureenabled && python.datascience.isnativeactive && !notebookEditorFocused&& python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.notebookeditor.removeallcells", + "title": "%python.command.python.datascience.notebookeditor.removeallcells.title%", + "category": "Python", + "when": "python.datascience.havenativecells && python.datascience.featureenabled && python.datascience.isnativeactive && python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.notebookeditor.interruptkernel", + "title": "%python.command.python.datascience.interruptkernel.title%", + "category": "Python", + "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.notebookeditor.restartkernel", + "title": "%python.command.python.datascience.restartkernel.title%", + "category": "Python", + "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.notebookeditor.trust", + "title": "%DataScience.trustNotebookCommandTitle%", + "category": "Python", + "when": "python.datascience.featureenabled && notebookEditorFocused && !python.datascience.isnotebooktrusted && python.datascience.trustfeatureenabled" + }, + { + "command": "python.datascience.notebookeditor.runallcells", + "title": "%python.command.python.datascience.notebookeditor.runallcells.title%", + "category": "Python", + "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.notebookeditor.runselectedcell", + "title": "%python.command.python.datascience.notebookeditor.runselectedcell.title%", + "category": "Python", + "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.havecellselected && !notebookEditorFocused && python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.notebookeditor.runselectedcell", + "title": "%python.command.python.datascience.notebookeditor.runselectedcell.title%", + "category": "Python", + "when": "python.datascience.isnativeactive && python.datascience.featureenabled && notebookEditorFocused && python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.notebookeditor.addcellbelow", + "title": "%python.command.python.datascience.notebookeditor.addcellbelow.title%", + "category": "Python", + "when": "python.datascience.isnativeactive && python.datascience.featureenabled && python.datascience.isnotebooktrusted" + }, + { + "command": "python.datascience.notebookeditor.removeallcells", + "title": "%python.command.python.datascience.removeallcells.title%", + "category": "Python", + "when": "python.datascience.featureenabled && notebookEditorFocused" + }, + { + "command": "python.datascience.notebookeditor.expandallcells", + "title": "%python.command.python.datascience.expandallcells.title%", + "category": "Python", + "when": "python.datascience.featureenabled && notebookEditorFocused" + }, + { + "command": "python.datascience.notebookeditor.collapseallcells", + "title": "%python.command.python.datascience.collapseallcells.title%", + "category": "Python", + "when": "python.datascience.featureenabled && notebookEditorFocused" + }, + { + "command": "python.datascience.expandallcells", + "title": "%python.command.python.datascience.expandallcells.title%", + "category": "Python", + "when": "python.datascience.isinteractiveactive && python.datascience.featureenabled" + }, + { + "command": "python.datascience.collapseallcells", + "title": "%python.command.python.datascience.collapseallcells.title%", + "category": "Python", + "when": "python.datascience.isinteractiveactive && python.datascience.featureenabled" + }, + { + "command": "python.datascience.exportoutputasnotebook", + "title": "%python.command.python.datascience.exportoutputasnotebook.title%", + "category": "Python", + "when": "python.datascience.isinteractiveactive && python.datascience.featureenabled" + }, + { + "command": "python.datascience.runcellandallbelow", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.runallcellsabove", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.debugcontinue", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.debugstop", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.debugstepover", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.debugcell", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.addcellbelow", + "title": "%python.command.python.datascience.addcellbelow.title%", + "category": "Python", + "when": "python.datascience.hascodecells && python.datascience.featureenabled && python.datascience.ispythonornativeactive" + }, + { + "command": "python.datascience.createnewnotebook", + "title": "%python.command.python.datascience.createnewnotebook.title%", + "category": "Python" + }, + { + "command": "python.startPage.open", + "title": "%python.command.python.startPage.open.title%", + "category": "Python" + }, + { + "command": "python.datascience.runtoline", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.runfromline", + "category": "Python", + "when": "config.noExists" + }, + { + "command": "python.datascience.execSelectionInteractive", + "category": "Python", + "when": "editorLangId == python && python.datascience.featureenabled && !notebookEditorFocused" + }, + { + "command": "python.datascience.switchKernel", + "title": "%DataScience.selectKernel%", + "category": "Python", + "when": "python.datascience.isnativeactive" + }, + { + "command": "python.datascience.gatherquality", + "title": "%DataScience.gatherQuality%", + "category": "Python", + "when": "false" + }, + { + "command": "python.datascience.latestExtension", + "title": "%DataScience.latestExtension%", + "category": "Python", + "when": "false" + }, + { + "command": "python.datascience.export", + "title": "%DataScience.notebookExportAs%", + "category": "Python", + "when": "false" + } + ], + "view/title": [ + { + "command": "python.debugtests", + "when": "view == python_tests && !busyTests", + "group": "navigation@3" + }, + { + "command": "python.runtests", + "when": "view == python_tests && !busyTests", + "group": "navigation@1" + }, + { + "command": "python.stopTests", + "when": "view == python_tests && busyTests", + "group": "navigation@1" + }, + { + "command": "python.discoverTests", + "when": "view == python_tests && !busyTests", + "group": "navigation@4" + }, + { + "command": "python.discoveringTests", + "when": "view == python_tests && discoveringTests", + "group": "navigation@4" + }, + { + "command": "python.runFailedTests", + "when": "view == python_tests && hasFailedTests && !busyTests", + "group": "navigation@2" + }, + { + "command": "python.viewTestOutput", + "when": "view == python_tests", + "group": "navigation@5" + } + ], + "view/item/context": [ + { + "command": "python.runtests", + "when": "view == python_tests && viewItem == testWorkspaceFolder && !busyTests", + "group": "inline@0" + }, + { + "command": "python.debugtests", + "when": "view == python_tests && viewItem == testWorkspaceFolder && !busyTests", + "group": "inline@1" + }, + { + "command": "python.discoverTests", + "when": "view == python_tests && viewItem == testWorkspaceFolder && !busyTests", + "group": "inline@2" + }, + { + "command": "python.openTestNodeInEditor", + "when": "view == python_tests && viewItem == function", + "group": "inline@2" + }, + { + "command": "python.debugTestNode", + "when": "view == python_tests && viewItem == function && !busyTests", + "group": "inline@1" + }, + { + "command": "python.runTestNode", + "when": "view == python_tests && viewItem == function && !busyTests", + "group": "inline@0" + }, + { + "command": "python.openTestNodeInEditor", + "when": "view == python_tests && viewItem == file", + "group": "inline@2" + }, + { + "command": "python.debugTestNode", + "when": "view == python_tests && viewItem == file && !busyTests", + "group": "inline@1" + }, + { + "command": "python.runTestNode", + "when": "view == python_tests && viewItem == file && !busyTests", + "group": "inline@0" + }, + { + "command": "python.openTestNodeInEditor", + "when": "view == python_tests && viewItem == suite", + "group": "inline@2" + }, + { + "command": "python.debugTestNode", + "when": "view == python_tests && viewItem == suite && !busyTests", + "group": "inline@1" + }, + { + "command": "python.runTestNode", + "when": "view == python_tests && viewItem == suite && !busyTests", + "group": "inline@0" + } + ] + }, + "breakpoints": [ + { + "language": "python" + }, + { + "language": "html" + }, + { + "language": "jinja" + } + ], + "debuggers": [ + { + "type": "python", + "label": "Python", + "languages": [ + "python" + ], + "variables": { + "pickProcess": "python.pickLocalProcess" + }, + "configurationSnippets": [], + "configurationAttributes": { + "launch": { + "properties": { + "module": { + "type": "string", + "description": "Name of the module to be debugged.", + "default": "" + }, + "program": { + "type": "string", + "description": "Absolute path to the program.", + "default": "${file}" + }, + "pythonPath": { + "type": "string", + "description": "Path (fully qualified) to python executable. Defaults to the value in settings", + "default": "${command:python.interpreterPath}" + }, + "pythonArgs": { + "type": "array", + "description": "Command-line arguments passed to the Python interpreter. To pass arguments to the debug target, use \"args\".", + "default": [], + "items": { + "type": "string" + } + }, + "args": { + "type": "array", + "description": "Command line arguments passed to the program", + "default": [], + "items": { + "type": "string" + } + }, + "stopOnEntry": { + "type": "boolean", + "description": "Automatically stop after launch.", + "default": false + }, + "showReturnValue": { + "type": "boolean", + "description": "Show return value of functions when stepping.", + "default": true + }, + "console": { + "enum": [ + "internalConsole", + "integratedTerminal", + "externalTerminal" + ], + "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.", + "default": "integratedTerminal" + }, + "cwd": { + "type": "string", + "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", + "default": "${workspaceFolder}" + }, + "env": { + "type": "object", + "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.", + "default": {}, + "additionalProperties": { + "type": "string" + } + }, + "envFile": { + "type": "string", + "description": "Absolute path to a file containing environment variable definitions.", + "default": "${workspaceFolder}/.env" + }, + "port": { + "type": "number", + "description": "Debug port (default is 0, resulting in the use of a dynamic port).", + "default": 0 + }, + "host": { + "type": "string", + "description": "IP address of the of the local debug server (default is localhost).", + "default": "localhost" + }, + "pathMappings": { + "type": "array", + "label": "Path mappings.", + "items": { + "type": "object", + "label": "Path mapping", + "required": [ + "localRoot", + "remoteRoot" + ], + "properties": { + "localRoot": { + "type": "string", + "label": "Local source root.", + "default": "${workspaceFolder}" + }, + "remoteRoot": { + "type": "string", + "label": "Remote source root.", + "default": "" + } + } + }, + "default": [] + }, + "logToFile": { + "type": "boolean", + "description": "Enable logging of debugger events to a log file.", + "default": false + }, + "redirectOutput": { + "type": "boolean", + "description": "Redirect output.", + "default": true + }, + "justMyCode": { + "type": "boolean", + "description": "Debug only user-written code.", + "default": true + }, + "gevent": { + "type": "boolean", + "description": "Enable debugging of gevent monkey-patched code.", + "default": false + }, + "django": { + "type": "boolean", + "description": "Django debugging.", + "default": false + }, + "jinja": { + "enum": [ + true, + false, + null + ], + "description": "Jinja template debugging (e.g. Flask).", + "default": null + }, + "sudo": { + "type": "boolean", + "description": "Running debug program under elevated permissions (on Unix).", + "default": false + }, + "pyramid": { + "type": "boolean", + "description": "Whether debugging Pyramid applications", + "default": false + }, + "subProcess": { + "type": "boolean", + "description": "Whether to enable Sub Process debugging", + "default": false + } + } + }, + "test": { + "properties": { + "pythonPath": { + "type": "string", + "description": "Path (fully qualified) to python executable. Defaults to the value in settings", + "default": "${command:python.interpreterPath}" + }, + "stopOnEntry": { + "type": "boolean", + "description": "Automatically stop after launch.", + "default": false + }, + "showReturnValue": { + "type": "boolean", + "description": "Show return value of functions when stepping.", + "default": true + }, + "console": { + "enum": [ + "internalConsole", + "integratedTerminal", + "externalTerminal" + ], + "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.", + "default": "internalConsole" + }, + "cwd": { + "type": "string", + "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", + "default": "${workspaceFolder}" + }, + "env": { + "type": "object", + "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.", + "default": {}, + "additionalProperties": { + "type": "string" + } + }, + "envFile": { + "type": "string", + "description": "Absolute path to a file containing environment variable definitions.", + "default": "${workspaceFolder}/.env" + }, + "redirectOutput": { + "type": "boolean", + "description": "Redirect output.", + "default": true + }, + "justMyCode": { + "type": "boolean", + "description": "Debug only user-written code.", + "default": true + } + } + }, + "attach": { + "properties": { + "connect": { + "type": "object", + "label": "Attach by connecting to debugpy over a socket.", + "properties": { + "port": { + "type": "number", + "description": "Port to connect to." + }, + "host": { + "type": "string", + "description": "Hostname or IP address to connect to.", + "default": "127.0.0.1" + } + }, + "required": [ + "port" + ] + }, + "listen": { + "type": "object", + "label": "Attach by listening for incoming socket connection from debugpy", + "properties": { + "port": { + "type": "number", + "description": "Port to listen on." + }, + "host": { + "type": "string", + "description": "Hostname or IP address of the interface to listen on.", + "default": "127.0.0.1" + } + }, + "required": [ + "port" + ] + }, + "port": { + "type": "number", + "description": "Port to connect to." + }, + "host": { + "type": "string", + "description": "Hostname or IP address to connect to.", + "default": "127.0.0.1" + }, + "pathMappings": { + "type": "array", + "label": "Path mappings.", + "items": { + "type": "object", + "label": "Path mapping", + "required": [ + "localRoot", + "remoteRoot" + ], + "properties": { + "localRoot": { + "type": "string", + "label": "Local source root.", + "default": "${workspaceFolder}" + }, + "remoteRoot": { + "type": "string", + "label": "Remote source root.", + "default": "" + } + } + }, + "default": [] + }, + "logToFile": { + "type": "boolean", + "description": "Enable logging of debugger events to a log file.", + "default": false + }, + "redirectOutput": { + "type": "boolean", + "description": "Redirect output.", + "default": true + }, + "justMyCode": { + "type": "boolean", + "description": "Debug only user-written code.", + "default": true + }, + "django": { + "type": "boolean", + "description": "Django debugging.", + "default": false + }, + "jinja": { + "enum": [ + true, + false, + null + ], + "description": "Jinja template debugging (e.g. Flask).", + "default": null + }, + "subProcess": { + "type": "boolean", + "description": "Whether to enable Sub Process debugging", + "default": false + }, + "showReturnValue": { + "type": "boolean", + "description": "Show return value of functions when stepping.", + "default": true + }, + "processId": { + "anyOf": [ + { + "enum": [ + "${command:pickProcess}" + ], + "description": "Use process picker to select a process to attach, or Process ID as integer.", + "default": "${command:pickProcess}" + }, + { + "type": "integer", + "description": "ID of the local process to attach to." + } + ] + } + } + } + } + } + ], + "configuration": { + "type": "object", + "title": "Python", + "properties": { + "python.diagnostics.sourceMapsEnabled": { + "type": "boolean", + "default": false, + "description": "Enable source map support for meaningful stack traces in error logs.", + "scope": "application" + }, + "python.autoComplete.addBrackets": { + "type": "boolean", + "default": false, + "description": "Automatically add brackets for functions.", + "scope": "resource" + }, + "python.autoComplete.extraPaths": { "type": "array", - "description": "Never break into these exceptions, e.g. 'copy.Error'", "default": [], + "description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", + "scope": "resource" + }, + "python.autoComplete.showAdvancedMembers": { + "type": "boolean", + "default": true, + "description": "Controls appearance of methods with double underscores in the completion list.", + "scope": "resource" + }, + "python.autoComplete.typeshedPaths": { + "type": "array", "items": { - "type": "string" - } - }, - "always": { + "type": "string" + }, + "default": [], + "description": "Specifies paths to local typeshed repository clone(s) for the Python language server.", + "scope": "resource" + }, + "python.autoUpdateLanguageServer": { + "type": "boolean", + "default": true, + "description": "Automatically update the language server.", + "scope": "application" + }, + "python.logging.level": { + "type": "string", + "default": "error", + "enum": [ + "off", + "error", + "warn", + "info", + "debug" + ], + "description": "The logging level the extension logs at, defaults to 'error'", + "scope": "machine" + }, + "python.experiments.enabled": { + "type": "boolean", + "default": true, + "description": "Enables/disables A/B tests.", + "scope": "machine" + }, + "python.defaultInterpreterPath": { + "type": "string", + "default": "python", + "description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path.", + "scope": "machine" + }, + "python.experiments.optInto": { "type": "array", - "description": "Always break into these exceptions, e.g. 'copy.Error'", "default": [], "items": { - "type": "string" - } - }, - "unhandled": { + "enum": [ + "AlwaysDisplayTestExplorer - experiment", + "ShowExtensionSurveyPrompt - enabled", + "Reload - experiment", + "AA_testing - experiment", + "LocalZMQKernel - experiment", + "NativeNotebook - experiment", + "CustomEditorSupport - experiment", + "UseTerminalToGetActivatedEnvVars - experiment", + "CollectLSRequestTiming - experiment", + "CollectNodeLSRequestTiming - experiment", + "DeprecatePythonPath - experiment", + "RunByLine - experiment", + "tryPylance", + "All" + ] + }, + "description": "List of experiment to opt into. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/Experiments for more details.", + "scope": "machine" + }, + "python.experiments.optOutFrom": { + "type": "array", + "default": [], + "items": { + "enum": [ + "AlwaysDisplayTestExplorer - experiment", + "ShowExtensionSurveyPrompt - enabled", + "Reload - experiment", + "AA_testing - experiment", + "LocalZMQKernel - experiment", + "NativeNotebook - experiment", + "CustomEditorSupport - experiment", + "UseTerminalToGetActivatedEnvVars - experiment", + "CollectLSRequestTiming - experiment", + "CollectNodeLSRequestTiming - experiment", + "DeprecatePythonPath - experiment", + "RunByLine - experiment", + "tryPylance", + "All" + ] + }, + "description": "List of experiment to opt out of. If empty, user is assigned the default experiment groups. See https://github.com/microsoft/vscode-python/wiki/Experiments for more details.", + "scope": "machine" + }, + "python.dataScience.allowImportFromNotebook": { + "type": "boolean", + "default": true, + "description": "Allows a user to import a jupyter notebook into a python file anytime one is opened.", + "scope": "resource" + }, + "python.dataScience.widgetScriptSources": { + "type": "array", + "default": [], + "items": { + "type": "string", + "enum": [ + "jsdelivr.com", + "unpkg.com" + ], + "enumDescriptions": [ + "Loads widget (javascript) scripts from https://www.jsdelivr.com/", + "Loads widget (javascript) scripts from https://unpkg.com/" + ] + }, + "uniqueItems": true, + "markdownDescription": "Defines the location and order of the sources where scripts files for Widgets are downloaded from (e.g. ipywidgest, bqplot, beakerx, ipyleaflet, etc). Not selecting any of these could result in widgets not rendering or function correctly. See [here](https://aka.ms/PVSCIPyWidgets) for more information. Once updated you will need to restart the Kernel.", + "scope": "machine" + }, + "python.dataScience.askForLargeDataFrames": { + "type": "boolean", + "default": true, + "description": "Warn the user before trying to open really large data frames.", + "scope": "application" + }, + "python.dataScience.askForKernelRestart": { + "type": "boolean", + "default": true, + "description": "Warn the user before restarting a kernel.", + "scope": "application" + }, + "python.dataScience.enabled": { + "type": "boolean", + "default": true, + "description": "Enable the experimental data science features in the python extension.", + "scope": "resource" + }, + "python.dataScience.exportWithOutputEnabled": { + "type": "boolean", + "default": false, + "description": "Enable exporting a python file into a jupyter notebook and run all cells when doing so.", + "scope": "resource" + }, + "python.dataScience.jupyterLaunchTimeout": { + "type": "number", + "default": 60000, + "description": "Amount of time (in ms) to wait for the Jupyter Notebook server to start.", + "scope": "resource" + }, + "python.dataScience.jupyterLaunchRetries": { + "type": "number", + "default": 3, + "description": "Number of times to attempt to connect to the Jupyter Notebook", + "scope": "resource" + }, + "python.dataScience.jupyterServerURI": { + "type": "string", + "default": "local", + "description": "When a Notebook Editor or Interactive Window session is started, create the kernel on the specified Jupyter server. Select 'local' to create a new Jupyter server on this local machine.", + "scope": "resource" + }, + "python.dataScience.jupyterCommandLineArguments": { + "type": "array", + "default": [], + "description": "When a Notebook Editor or Interactive Window Jupyter server is started, these arguments will be passed to it. By default this list is generated by the Python Extension.", + "scope": "resource" + }, + "python.dataScience.notebookFileRoot": { + "type": "string", + "default": "${fileDirname}", + "description": "Set the root directory for loading files for the Python Interactive window.", + "scope": "resource" + }, + "python.dataScience.searchForJupyter": { + "type": "boolean", + "default": true, + "description": "Search all installed Python interpreters for a Jupyter installation when starting the Python Interactive window", + "scope": "resource" + }, + "python.dataScience.changeDirOnImportExport": { + "type": "boolean", + "default": false, + "description": "When importing or exporting a Jupyter Notebook add a directory change command to allow relative path loading to work.", + "scope": "resource" + }, + "python.dataScience.useDefaultConfigForJupyter": { + "type": "boolean", + "default": true, + "description": "When running Jupyter locally, create a default empty Jupyter config for the Python Interactive window", + "scope": "resource" + }, + "python.dataScience.jupyterInterruptTimeout": { + "type": "number", + "default": 10000, + "description": "Amount of time (in ms) to wait for an interrupt before asking to restart the Jupyter kernel.", + "scope": "resource" + }, + "python.dataScience.allowInput": { + "type": "boolean", + "default": true, + "description": "Allow the inputting of python code directly into the Python Interactive window" + }, + "python.dataScience.showCellInputCode": { + "type": "boolean", + "default": true, + "description": "Show cell input code.", + "scope": "resource" + }, + "python.dataScience.collapseCellInputCodeByDefault": { + "type": "boolean", + "default": true, + "description": "Collapse cell input code by default.", + "scope": "resource" + }, + "python.dataScience.maxOutputSize": { + "type": "number", + "default": 400, + "description": "Maximum size (in pixels) of text output in the Notebook Editor before a scrollbar appears. First enable scrolling for cell outputs in settings.", + "scope": "resource" + }, + "python.dataScience.alwaysScrollOnNewCell": { + "type": "boolean", + "default": false, + "description": "Automatically scroll the interactive window to show the output of the last statement executed. If false, the interactive window will only automatically scroll if the bottom of the prior cell is visible.", + "scope": "resource" + }, + "python.dataScience.showKernelSelectionOnInteractiveWindow": { + "type": "boolean", + "default": false, + "description": "When set to true, enables the kernel selector in the Interactive Window. By default, the Interactive Window will use your Python Interpreters kernel.", + "scope": "resource" + }, + "python.dataScience.enableScrollingForCellOutputs": { + "type": "boolean", + "default": true, + "description": "Enables scrolling for large cell outputs in the Notebook Editor. This setting does not apply to the Python Interactive Window.", + "scope": "resource" + }, + "python.dataScience.errorBackgroundColor": { + "type": "string", + "default": "#FFFFFF", + "description": "Background color (in hex) for exception messages in the Python Interactive window.", + "scope": "resource", + "deprecationMessage": "No longer necessary as the theme colors are used for error messages" + }, + "python.dataScience.sendSelectionToInteractiveWindow": { + "type": "boolean", + "default": false, + "description": "Determines if selected code in a python file will go to the terminal or the Python Interactive window when hitting shift+enter", + "scope": "resource" + }, + "python.dataScience.showJupyterVariableExplorer": { + "type": "boolean", + "default": true, + "description": "Show the variable explorer in the Python Interactive window.", + "deprecationMessage": "This setting no longer applies. It is ignored.", + "scope": "resource" + }, + "python.dataScience.variableExplorerExclude": { + "type": "string", + "default": "module;function;builtin_function_or_method", + "description": "Types to exclude from showing in the Python Interactive variable explorer", + "scope": "resource" + }, + "python.dataScience.codeRegularExpression": { + "type": "string", + "default": "^(#\\s*%%|#\\s*\\|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])", + "description": "Regular expression used to identify code cells. All code until the next match is considered part of this cell. \nDefaults to '^(#\\s*%%|#\\s*\\|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])' if left blank", + "scope": "resource" + }, + "python.dataScience.defaultCellMarker": { + "type": "string", + "default": "# %%", + "description": "Cell marker used for delineating a cell in a python file.", + "scope": "resource" + }, + "python.dataScience.markdownRegularExpression": { + "type": "string", + "default": "^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\)", + "description": "Regular expression used to identify markdown cells. All comments after this expression are considered part of the markdown. \nDefaults to '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\)' if left blank", + "scope": "resource" + }, + "python.dataScience.allowLiveShare": { + "type": "boolean", + "default": true, + "description": "Allow the Python Interactive window to be shared during a Live Share session", + "scope": "resource" + }, + "python.dataScience.ignoreVscodeTheme": { + "type": "boolean", + "default": false, + "description": "Don't use the VS Code theme in the Python Interactive window (requires reload of VS Code). This forces the Python Interactive window to use 'Light +(default light)' and disables matplotlib defaults.", + "scope": "resource" + }, + "python.dataScience.themeMatplotlibPlots": { + "type": "boolean", + "default": false, + "description": "In the Python Interactive window and Notebook Editor theme matplotlib outputs to match the VS Code editor theme.", + "scope": "resource" + }, + "python.dataScience.liveShareConnectionTimeout": { + "type": "number", + "default": 1000, + "description": "Amount of time to wait for guest connections to verify they have the Python extension installed.", + "scope": "application" + }, + "python.dataScience.decorateCells": { + "type": "boolean", + "default": true, + "description": "Draw a highlight behind the currently active cell.", + "scope": "resource" + }, + "python.dataScience.enableCellCodeLens": { + "type": "boolean", + "default": true, + "description": "Enables code lens for 'cells' in a python file.", + "scope": "resource" + }, + "python.dataScience.enableAutoMoveToNextCell": { + "type": "boolean", + "default": true, + "description": "Enables moving to the next cell when clicking on a 'Run Cell' code lens.", + "scope": "resource" + }, + "python.dataScience.autoPreviewNotebooksInInteractivePane": { + "type": "boolean", + "deprecationMessage": "No longer supported. Notebooks open directly in their own editor now.", + "default": false, + "description": "When opening ipynb files, automatically preview the contents in the Python Interactive window.", + "scope": "resource" + }, + "python.dataScience.useNotebookEditor": { + "type": "boolean", + "default": true, + "description": "Automatically open .ipynb files in the Notebook Editor.", + "scope": "resource" + }, + "python.dataScience.allowUnauthorizedRemoteConnection": { + "type": "boolean", + "default": false, + "description": "Allow for connecting the Python Interactive window to a https Jupyter server that does not have valid certificates. This can be a security risk, so only use for known and trusted servers.", + "scope": "resource" + }, + "python.dataScience.enablePlotViewer": { + "type": "boolean", + "default": true, + "description": "Modify plot output so that it can be expanded into a plot viewer window.", + "scope": "resource" + }, + "python.dataScience.gatherToScript": { + "type": "boolean", + "default": false, + "description": "Gather code to a python script rather than a notebook.", + "scope": "resource" + }, + "python.dataScience.gatherSpecPath": { + "type": "string", + "default": "", + "description": "This setting specifies a folder that contains additional or replacement spec files used for analysis.", + "scope": "resource" + }, + "python.dataScience.codeLenses": { + "type": "string", + "default": "python.datascience.runcell, python.datascience.runallcellsabove, python.datascience.debugcell", + "description": "Set of commands to put as code lens above a cell. Defaults to 'python.datascience.runcell, python.datascience.runallcellsabove, python.datascience.debugcell'", + "scope": "resource" + }, + "python.dataScience.debugCodeLenses": { + "type": "string", + "default": "python.datascience.debugcontinue, python.datascience.debugstop, python.datascience.debugstepover", + "description": "Set of debug commands to put as code lens above a cell while debugging.", + "scope": "resource" + }, + "python.dataScience.debugpyDistPath": { + "type": "string", + "default": "", + "description": "Path to debugpy bits for debugging cells.", + "scope": "resource" + }, + "python.dataScience.stopOnFirstLineWhileDebugging": { + "type": "boolean", + "default": true, + "description": "When debugging a cell, stop on the first line.", + "scope": "resource" + }, + "python.dataScience.remoteDebuggerPort": { + "type": "number", + "default": -1, + "description": "When debugging a cell, open this port on the remote box. If -1 is specified, a random port between 8889 and 9000 will be attempted.", + "scope": "resource" + }, + "python.dataScience.disableJupyterAutoStart": { + "type": "boolean", + "default": false, + "description": "When true, disables Jupyter from being automatically started for you. You must instead run a cell to start Jupyter.", + "scope": "resource" + }, + "python.dataScience.textOutputLimit": { + "type": "number", + "default": 20000, + "description": "Limit the amount of text in Python Interactive cell text output to this value. 0 to allow any amount of characters.", + "scope": "resource" + }, + "python.dataScience.colorizeInputBox": { + "type": "boolean", + "default": true, + "description": "Whether or not to use the theme's peek color as the background for the input box.", + "scope": "resource" + }, + "python.dataScience.stopOnError": { + "type": "boolean", + "default": true, + "description": "Stop running cells if a cell throws an exception.", + "scope": "resource" + }, + "python.dataScience.addGotoCodeLenses": { + "type": "boolean", + "default": true, + "description": "After running a cell, add a 'Goto' code lens on the cell. Note, disabling all code lenses disables this code lens as well.", + "scope": "resource" + }, + "python.dataScience.variableQueries": { + "type": "array", + "description": "Language to query mapping for returning the list of active variables in a Jupyter kernel. Used by the Variable Explorer in both the Interactive Window and Notebooks. Example: \n'[\n{\n \"language\": \"python\",\n \"query\": \"%who_ls\",\n \"parseExpr\": \"'(\\\\w+)'\"\n}\n]'", + "scope": "machine", + "examples": [ + [ + { + "language": "python", + "query": "_rwho_ls = %who_ls\\nprint(_rwho_ls)", + "parseExpr": "'(\\w+)'" + }, + { + "language": "julia", + "query": "whos", + "parseExpr": "'(\\w+)'" + } + ] + ] + }, + "python.dataScience.interactiveWindowMode": { + "type": "string", + "enum": [ + "perFile", + "single", + "multiple" + ], + "scope": "resource", + "description": "Behavior of the Python Interactive Window. 'perFile' will create a new interactive window for every file that runs a cell. 'single' allows a single window. 'multiple' allows the creation of multiple.", + "default": "multiple" + }, + "python.disableInstallationCheck": { + "type": "boolean", + "default": false, + "description": "Whether to check if Python is installed (also warn when using the macOS-installed Python).", + "scope": "resource" + }, + "python.envFile": { + "type": "string", + "description": "Absolute path to a file containing environment variable definitions.", + "default": "${workspaceFolder}/.env", + "scope": "resource" + }, + "python.formatting.autopep8Args": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.formatting.autopep8Path": { + "type": "string", + "default": "autopep8", + "description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.formatting.provider": { + "type": "string", + "default": "autopep8", + "description": "Provider for formatting. Possible options include 'autopep8', 'black', and 'yapf'.", + "enum": [ + "autopep8", + "black", + "yapf", + "none" + ], + "scope": "resource" + }, + "python.formatting.blackArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.formatting.blackPath": { + "type": "string", + "default": "black", + "description": "Path to Black, you can use a custom version of Black by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.formatting.yapfArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.formatting.yapfPath": { + "type": "string", + "default": "yapf", + "description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.globalModuleInstallation": { + "type": "boolean", + "default": false, + "description": "Whether to install Python modules globally when not using an environment.", + "scope": "resource" + }, + "python.jediMemoryLimit": { + "type": "number", + "default": 0, + "description": "Memory limit for the Jedi completion engine in megabytes. Zero (default) means 1024 MB. -1 means unlimited (disable memory limit check)", + "scope": "resource" + }, + "python.jediPath": { + "type": "string", + "default": "", + "description": "Path to directory containing the Jedi library (this path will contain the 'Jedi' sub directory). Note: since Jedi depends on Parso, if using this setting you will need to ensure a suitable version of Parso is available.", + "scope": "resource" + }, + "python.languageServer": { + "type": "string", + "enum": [ + "Jedi", + "Pylance", + "Microsoft", + "None" + ], + "default": "Jedi", + "description": "Defines type of the language server.", + "scope": "window" + }, + "python.analysis.diagnosticPublishDelay": { + "type": "integer", + "default": 1000, + "description": "Delay before diagnostic messages are transferred to the problems list (in milliseconds).", + "scope": "resource" + }, + "python.analysis.errors": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of diagnostics messages to be shown as errors.", + "scope": "resource" + }, + "python.analysis.warnings": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of diagnostics messages to be shown as warnings.", + "scope": "resource" + }, + "python.analysis.information": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of diagnostics messages to be shown as information.", + "scope": "resource" + }, + "python.analysis.disabled": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of suppressed diagnostic messages.", + "scope": "resource" + }, + "python.analysis.typeshedPaths": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "Paths to Typeshed stub folders. Default is Typeshed installed with the language server. Change requires restart.", + "scope": "resource" + }, + "python.analysis.cacheFolderPath": { + "type": "string", + "description": "Path to a writable folder where analyzer can cache its data. Change requires restart.", + "scope": "resource" + }, + "python.analysis.memory.keepLibraryAst": { + "type": "boolean", + "default": false, + "description": "Allows code analysis to keep parser trees in memory. Increases memory consumption but may improve performance with large library analysis.", + "scope": "resource" + }, + "python.analysis.logLevel": { + "type": "string", + "enum": [ + "Error", + "Warning", + "Information", + "Trace" + ], + "default": "Error", + "description": "Defines type of log messages language server writes into the output window.", + "scope": "resource" + }, + "python.analysis.symbolsHierarchyDepthLimit": { + "type": "integer", + "default": 10, + "description": "Limits depth of the symbol tree in the document outline.", + "scope": "resource" + }, + "python.linting.enabled": { + "type": "boolean", + "default": true, + "description": "Whether to lint Python files.", + "scope": "resource" + }, + "python.linting.flake8Args": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.flake8CategorySeverity.E": { + "type": "string", + "default": "Error", + "description": "Severity of Flake8 message type 'E'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.flake8CategorySeverity.F": { + "type": "string", + "default": "Error", + "description": "Severity of Flake8 message type 'F'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.flake8CategorySeverity.W": { + "type": "string", + "default": "Warning", + "description": "Severity of Flake8 message type 'W'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.flake8Enabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using flake8", + "scope": "resource" + }, + "python.linting.flake8Path": { + "type": "string", + "default": "flake8", + "description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.ignorePatterns": { + "type": "array", + "description": "Patterns used to exclude files or folders from being linted.", + "default": [ + ".vscode/*.py", + "**/site-packages/**/*.py" + ], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.lintOnSave": { + "type": "boolean", + "default": true, + "description": "Whether to lint Python files when saved.", + "scope": "resource" + }, + "python.linting.maxNumberOfProblems": { + "type": "number", + "default": 100, + "description": "Controls the maximum number of problems produced by the server.", + "scope": "resource" + }, + "python.linting.banditArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.banditEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using bandit.", + "scope": "resource" + }, + "python.linting.banditPath": { + "type": "string", + "default": "bandit", + "description": "Path to bandit, you can use a custom version of bandit by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.mypyArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [ + "--ignore-missing-imports", + "--follow-imports=silent", + "--show-column-numbers" + ], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.mypyCategorySeverity.error": { + "type": "string", + "default": "Error", + "description": "Severity of Mypy message type 'Error'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.mypyCategorySeverity.note": { + "type": "string", + "default": "Information", + "description": "Severity of Mypy message type 'Note'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.mypyEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using mypy.", + "scope": "resource" + }, + "python.linting.mypyPath": { + "type": "string", + "default": "mypy", + "description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pycodestyleArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.pycodestyleCategorySeverity.E": { + "type": "string", + "default": "Error", + "description": "Severity of pycodestyle message type 'E'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pycodestyleCategorySeverity.W": { + "type": "string", + "default": "Warning", + "description": "Severity of pycodestyle message type 'W'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pycodestyleEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using pycodestyle", + "scope": "resource" + }, + "python.linting.pycodestylePath": { + "type": "string", + "default": "pycodestyle", + "description": "Path to pycodestyle, you can use a custom version of pycodestyle by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.prospectorArgs": { "type": "array", - "description": "Break into these exceptions if they aren't handled, e.g. 'copy.Error'", + "description": "Arguments passed in. Each argument is a separate item in the array.", "default": [], "items": { - "type": "string" + "type": "string" + }, + "scope": "resource" + }, + "python.linting.prospectorEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using prospector.", + "scope": "resource" + }, + "python.linting.prospectorPath": { + "type": "string", + "default": "prospector", + "description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pydocstyleArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.pydocstyleEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using pydocstyle", + "scope": "resource" + }, + "python.linting.pydocstylePath": { + "type": "string", + "default": "pydocstyle", + "description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pylamaArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.pylamaEnabled": { + "type": "boolean", + "default": false, + "description": "Whether to lint Python files using pylama.", + "scope": "resource" + }, + "python.linting.pylamaPath": { + "type": "string", + "default": "pylama", + "description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pylintArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.linting.pylintCategorySeverity.convention": { + "type": "string", + "default": "Information", + "description": "Severity of Pylint message type 'Convention/C'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pylintCategorySeverity.error": { + "type": "string", + "default": "Error", + "description": "Severity of Pylint message type 'Error/E'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pylintCategorySeverity.fatal": { + "type": "string", + "default": "Error", + "description": "Severity of Pylint message type 'Fatal/F'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pylintCategorySeverity.refactor": { + "type": "string", + "default": "Hint", + "description": "Severity of Pylint message type 'Refactor/R'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pylintCategorySeverity.warning": { + "type": "string", + "default": "Warning", + "description": "Severity of Pylint message type 'Warning/W'.", + "enum": [ + "Hint", + "Error", + "Information", + "Warning" + ], + "scope": "resource" + }, + "python.linting.pylintEnabled": { + "type": "boolean", + "default": true, + "description": "Whether to lint Python files using pylint.", + "scope": "resource" + }, + "python.linting.pylintPath": { + "type": "string", + "default": "pylint", + "description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.linting.pylintUseMinimalCheckers": { + "type": "boolean", + "default": true, + "description": "Whether to run Pylint with minimal set of rules.", + "scope": "resource" + }, + "python.pythonPath": { + "type": "string", + "default": "python", + "description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.condaPath": { + "type": "string", + "default": "", + "description": "Path to the conda executable to use for activation (version 4.4+).", + "scope": "resource" + }, + "python.pipenvPath": { + "type": "string", + "default": "pipenv", + "description": "Path to the pipenv executable to use for activation.", + "scope": "resource" + }, + "python.poetryPath": { + "type": "string", + "default": "poetry", + "description": "Path to the poetry executable.", + "scope": "resource" + }, + "python.sortImports.args": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.sortImports.path": { + "type": "string", + "description": "Path to isort script, default using inner version", + "default": "", + "scope": "resource" + }, + "python.terminal.activateEnvironment": { + "type": "boolean", + "default": true, + "description": "Activate Python Environment in Terminal created using the Extension.", + "scope": "resource" + }, + "python.terminal.executeInFileDir": { + "type": "boolean", + "default": false, + "description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.", + "scope": "resource" + }, + "python.terminal.launchArgs": { + "type": "array", + "default": [], + "description": "Python launch arguments to use when executing a file in the terminal.", + "scope": "resource" + }, + "python.terminal.activateEnvInCurrentTerminal": { + "type": "boolean", + "default": false, + "description": "Activate Python Environment in the current Terminal on load of the Extension.", + "scope": "resource" + }, + "python.testing.cwd": { + "type": "string", + "default": null, + "description": "Optional working directory for tests.", + "scope": "resource" + }, + "python.testing.debugPort": { + "type": "number", + "default": 3000, + "description": "Port number used for debugging of tests.", + "scope": "resource" + }, + "python.testing.nosetestArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.testing.nosetestsEnabled": { + "type": "boolean", + "default": false, + "description": "Enable testing using nosetests.", + "scope": "resource" + }, + "python.testing.nosetestPath": { + "type": "string", + "default": "nosetests", + "description": "Path to nosetests, you can use a custom version of nosetests by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.testing.promptToConfigure": { + "type": "boolean", + "default": true, + "description": "Prompt to configure a test framework if potential tests directories are discovered.", + "scope": "resource" + }, + "python.testing.pytestArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.testing.pytestEnabled": { + "type": "boolean", + "default": false, + "description": "Enable testing using pytest.", + "scope": "resource" + }, + "python.testing.pytestPath": { + "type": "string", + "default": "pytest", + "description": "Path to pytest (pytest), you can use a custom version of pytest by modifying this setting to include the full path.", + "scope": "resource" + }, + "python.testing.unittestArgs": { + "type": "array", + "description": "Arguments passed in. Each argument is a separate item in the array.", + "default": [ + "-v", + "-s", + ".", + "-p", + "*test*.py" + ], + "items": { + "type": "string" + }, + "scope": "resource" + }, + "python.testing.unittestEnabled": { + "type": "boolean", + "default": false, + "description": "Enable testing using unittest.", + "scope": "resource" + }, + "python.testing.autoTestDiscoverOnSaveEnabled": { + "type": "boolean", + "default": true, + "description": "Enable auto run test discovery when saving a test file.", + "scope": "resource" + }, + "python.venvFolders": { + "type": "array", + "default": [], + "description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).", + "scope": "resource", + "items": { + "type": "string" } - } + }, + "python.venvPath": { + "type": "string", + "default": "", + "description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", + "scope": "resource" + }, + "python.workspaceSymbols.ctagsPath": { + "type": "string", + "default": "ctags", + "description": "Fully qualified path to the ctags executable (else leave as ctags, assuming it is in current path).", + "scope": "resource" + }, + "python.workspaceSymbols.enabled": { + "type": "boolean", + "default": false, + "description": "Set to 'true' to enable ctags to provide Workspace Symbols.", + "scope": "resource" + }, + "python.workspaceSymbols.exclusionPatterns": { + "type": "array", + "default": [ + "**/site-packages/**" + ], + "items": { + "type": "string" + }, + "description": "Pattern used to exclude files and folders from ctags See http://ctags.sourceforge.net/ctags.html.", + "scope": "resource" + }, + "python.workspaceSymbols.rebuildOnFileSave": { + "type": "boolean", + "default": true, + "description": "Whether to re-build the tags file on when changes made to python files are saved.", + "scope": "resource" + }, + "python.workspaceSymbols.rebuildOnStart": { + "type": "boolean", + "default": true, + "description": "Whether to re-build the tags file on start (defaults to true).", + "scope": "resource" + }, + "python.workspaceSymbols.tagFilePath": { + "type": "string", + "default": "${workspaceFolder}/.vscode/tags", + "description": "Fully qualified path to tag file (exuberant ctag file), used to provide workspace symbols.", + "scope": "resource" + }, + "python.dataScience.magicCommandsAsComments": { + "type": "boolean", + "default": false, + "description": "Uncomment shell assignments (#!), line magic (#!%) and cell magic (#!%%) when parsing code cells.", + "scope": "resource" + }, + "python.dataScience.runMagicCommands": { + "type": "string", + "default": "", + "deprecationMessage": "This setting has been deprecated in favor of 'runStartupCommands'.", + "description": "A series of Python instructions or iPython magic commands separated by '\\n' that will be executed when the interactive window loads.", + "scope": "application" + }, + "python.dataScience.runStartupCommands": { + "type": "array", + "default": "", + "description": "A series of Python instructions or iPython magic commands. Can be either an array of strings or a single string with commands separated by '\\n'. Commands will be silently executed whenever the interactive window loads. For instance, set this to '%load_ext autoreload\\n%autoreload 2' to automatically reload changes made to imported files without having to restart the interactive session.", + "scope": "application" + }, + "python.dataScience.debugJustMyCode": { + "type": "boolean", + "default": true, + "description": "When debugging, debug just my code.", + "scope": "resource" + }, + "python.dataScience.alwaysTrustNotebooks": { + "type": "boolean", + "default": false, + "markdownDescription": "Enabling this setting will automatically trust any opened notebook and therefore display markdown and render code cells. You will no longer be prompted to trust individual notebooks and harmful code could automatically run. \n\n[Learn more.](https://aka.ms/trusted-notebooks)", + "scope": "machine" + }, + "python.insidersChannel": { + "type": "string", + "default": "off", + "description": "Set to \"weekly\" or \"daily\" to automatically download and install the latest Insiders builds of the python extension, which include upcoming features and bug fixes.", + "enum": [ + "off", + "weekly", + "daily" + ], + "scope": "application" + }, + "python.showStartPage": { + "type": "boolean", + "default": true, + "description": "Show the Python Start Page when a new update is released.", + "scope": "application" } - }, - "env": { - "type": "object", - "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.", - "default": null - } } - }, - "attach": { - "required": [ - "localRoot", - "remoteRoot" - ], - "properties": { - "localRoot": { - "type": "string", - "description": "Local source root that corrresponds to the 'remoteRoot'.", - "default": "${workspaceRoot}" - }, - "remoteRoot": { - "type": "string", - "description": "The source root of the remote host.", - "default": null - }, - "port": { - "type": "number", - "description": "Debug port to attach", - "default": null - }, - "host": { - "type": "string", - "description": "IP Address of the of remote server (default is localhost or use 127.0.0.1).", - "default": "localhost" - }, - "secret": { - "type": "string", - "description": "Secret used to authenticate for remote debugging.", - "default": "" - } - } - } }, - "initialConfigurations": [ - { - "name": "Python", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "program": "${file}", - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" - ] - }, - { - "name": "Python Console App", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "program": "${file}", - "externalConsole": true, - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit" - ] - }, - { - "name": "Django", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "program": "${workspaceRoot}/manage.py", - "args": [ - "runserver", - "--noreload" - ], - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput", - "DjangoDebugging" - ] - }, - { - "name": "Watson", - "type": "python", - "request": "launch", - "stopOnEntry": true, - "program": "${workspaceRoot}/console.py", - "args": [ - "dev", - "runserver", - "--noreload=True" - ], - "debugOptions": [ - "WaitOnAbnormalExit", - "WaitOnNormalExit", - "RedirectOutput" + "languages": [ + { + "id": "pip-requirements", + "aliases": [ + "pip requirements", + "requirements.txt" + ], + "filenames": [ + "requirements.txt", + "constraints.txt", + "requirements.in" + ], + "filenamePatterns": [ + "*-requirements.txt", + "requirements-*.txt", + "constraints-*.txt", + "*-constraints.txt", + "*-requirements.in", + "requirements-*.in" + ], + "configuration": "./languages/pip-requirements.json" + }, + { + "id": "yaml", + "filenames": [ + ".condarc" + ] + }, + { + "id": "toml", + "filenames": [ + "poetry.lock", + "Pipfile" + ] + }, + { + "id": "json", + "filenames": [ + "Pipfile.lock" + ] + }, + { + "id": "ini", + "filenames": [ + ".flake8" + ] + }, + { + "id": "jinja", + "extensions": [ + ".jinja2", + ".j2" + ], + "aliases": [ + "Jinja" + ] + }, + { + "id": "jupyter", + "aliases": [ + "Jupyter", + "Notebook" + ], + "extensions": [ + ".ipynb" + ] + } + ], + "grammars": [ + { + "language": "pip-requirements", + "scopeName": "source.pip-requirements", + "path": "./syntaxes/pip-requirements.tmLanguage.json" + } + ], + "jsonValidation": [ + { + "fileMatch": ".condarc", + "url": "./schemas/condarc.json" + }, + { + "fileMatch": "environment.yml", + "url": "./schemas/conda-environment.json" + }, + { + "fileMatch": "meta.yaml", + "url": "./schemas/conda-meta.json" + } + ], + "yamlValidation": [ + { + "fileMatch": ".condarc", + "url": "./schemas/condarc.json" + }, + { + "fileMatch": "environment.yml", + "url": "./schemas/conda-environment.json" + }, + { + "fileMatch": "meta.yaml", + "url": "./schemas/conda-meta.json" + } + ], + "views": { + "test": [ + { + "id": "python_tests", + "name": "Python", + "when": "testsDiscovered" + } ] - } - ] - } - ], - "configuration": { - "type": "object", - "title": "Python Configuration", - "properties": { - "python.pythonPath": { - "type": "string", - "default": "python", - "description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path." - }, - "python.devOptions": { - "type": "array", - "default": [], - "description": "Advanced options used to enable new features or produce detailed diagnostics to identify extension issues." - }, - "python.linting.enabled": { - "type": "boolean", - "default": true, - "description": "Whether to lint Python files." - }, - "python.linting.prospectorEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using prospector." - }, - "python.linting.pylintEnabled": { - "type": "boolean", - "default": true, - "description": "Whether to lint Python files using pylint." - }, - "python.linting.pep8Enabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using pep8" - }, - "python.linting.flake8Enabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using flake8" - }, - "python.linting.pydocstyleEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using pydocstyle" - }, - "python.linting.lintOnTextChange": { - "type": "boolean", - "default": true, - "description": "Whether to lint Python files when modified." - }, - "python.linting.lintOnSave": { - "type": "boolean", - "default": true, - "description": "Whether to lint Python files when saved." }, - "python.linting.maxNumberOfProblems": { - "type": "number", - "default": 100, - "description": "Controls the maximum number of problems produced by the server." - }, - "python.linting.pylintCategorySeverity.convention": { - "type": "string", - "default": "Hint", - "description": "Severity of Pylint message type 'Convention/C'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ] - }, - "python.linting.pylintCategorySeverity.refactor": { - "type": "string", - "default": "Hint", - "description": "Severity of Pylint message type 'Refactor/R'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ] - }, - "python.linting.pylintCategorySeverity.warning": { - "type": "string", - "default": "Warning", - "description": "Severity of Pylint message type 'Warning/W'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ] - }, - "python.linting.pylintCategorySeverity.error": { - "type": "string", - "default": "Error", - "description": "Severity of Pylint message type 'Error/E'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ] - }, - "python.linting.pylintCategorySeverity.fatal": { - "type": "string", - "default": "Error", - "description": "Severity of Pylint message type 'Fatal/F'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ] - }, - "python.linting.prospectorPath": { - "type": "string", - "default": "prospector", - "description": "Path to Prospector, you can use a custom version of prospector by modifying this setting to include the full path." - }, - "python.linting.prospectorSourcePath": { - "type": "string", - "default": "", - "description": "Path to python source (relative to workspace)." - }, - "python.linting.prospectorExtraCommands": { - "type": "string", - "default": "", - "description": "Extra command line options to be passed to (ex: path to profile to be used)." - }, - "python.linting.pylintPath": { - "type": "string", - "default": "pylint", - "description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path." - }, - "python.linting.pep8Path": { - "type": "string", - "default": "pep8", - "description": "Path to pep8, you can use a custom version of pep8 by modifying this setting to include the full path." - }, - "python.linting.flake8Path": { - "type": "string", - "default": "flake8", - "description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path." - }, - "python.linting.pydocStylePath": { - "type": "string", - "default": "pydocstyle", - "description": "Path to pydocstyle, you can use a custom version of pydocstyle by modifying this setting to include the full path." - }, - "python.linting.outputWindow": { - "type": "string", - "default": "Python", - "description": "The output window name for the linting messages, defaults to Python output window." - }, - "python.formatting.provider": { - "type": "string", - "default": "autopep8", - "description": "Provider for formatting. Possible options include 'autopep8' and 'yapf'.", - "enum": [ - "autopep8", - "yapf" - ] - }, - "python.formatting.autopep8Path": { - "type": "string", - "default": "autopep8", - "description": "Path to autopep8, you can use a custom version of autopep8 by modifying this setting to include the full path." - }, - "python.formatting.yapfPath": { - "type": "string", - "default": "yapf", - "description": "Path to yapf, you can use a custom version of yapf by modifying this setting to include the full path." - }, - "python.formatting.formatOnSave": { - "type": "boolean", - "default": false, - "description": "Format the document upon saving." - }, - "python.formatting.outputWindow": { - "type": "string", - "default": "Python", - "description": "The output window name for the formatting messages, defaults to Python output window." - }, - "python.autoComplete.extraPaths": { - "type": "array", - "default": [], - "description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list." - }, - "python.unitTest.nosetestsEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to enable or disable unit testing using nosetests." - }, - "python.unitTest.nosetestPath": { - "type": "string", - "default": "nosetests", - "description": "Path to nosetests, you can use a custom version of nosetests by modifying this setting to include the full path." - }, - "python.unitTest.unittestEnabled": { - "type": "boolean", - "default": true, - "description": "Whether to enable or disable unit testing using standard unittest (built into Python)." - }, - "python.unitTest.outputWindow": { - "type": "string", - "default": "Python", - "description": "The output window name for the unit test messages, defaults to Python output window." - } - } + "notebookOutputRenderer": [ + { + "id": "jupyter-notebook-renderer", + "entrypoint": "./out/datascience-ui/renderers/renderers.js", + "displayName": "Jupyter Notebook Renderer", + "mimeTypes": [ + "application/geo+json", + "application/vdom.v1+json", + "application/vnd.dataresource+json", + "application/vnd.plotly.v1+json", + "application/vnd.vega.v2+json", + "application/vnd.vega.v3+json", + "application/vnd.vega.v4+json", + "application/vnd.vega.v5+json", + "application/vnd.vegalite.v1+json", + "application/vnd.vegalite.v2+json", + "application/vnd.vegalite.v3+json", + "application/vnd.vegalite.v4+json", + "application/x-nteract-model-debug+json", + "image/gif", + "image/png", + "image/jpeg", + "text/latex", + "text/vnd.plotly.v1+html" + ] + } + ], + "notebookProvider": [ + { + "viewType": "jupyter-notebook", + "displayName": "Jupyter Notebook (preview)", + "selector": [ + { + "filenamePattern": "*.ipynb" + } + ], + "priority": "option" + } + ] + }, + "scripts": { + "package": "gulp clean && gulp prePublishBundle && vsce package -o ms-python-insiders.vsix", + "compile": "tsc -watch -p ./", + "compiled": "deemon npm run compile", + "kill-compiled": "deemon --kill npm run compile", + "compile-webviews-watch": "gulp compile-ipywidgets && cross-env NODE_OPTIONS=--max_old_space_size=9096 webpack --config ./build/webpack/webpack.datascience-ui.config.js --watch", + "compile-webviews-watchd": "deemon npm run compile-webviews-watch", + "kill-compile-webviews-watchd": "deemon --kill npm run compile-webviews-watch", + "build-ipywidgets": "npm run build-ipywidgets-clean && npm run build-ipywidgets-compile && npm run build-ipywidgets-webpack", + "build-ipywidgets-clean": "node ./src/ipywidgets/scripts/clean.js", + "build-ipywidgets-compile": "tsc -p ./src/ipywidgets && rimraf ./out/tsconfig.tsbuildinfo && node ./src/ipywidgets/scripts/copyfiles.js", + "build-ipywidgets-webpack": "cross-env NODE_OPTIONS=--max_old_space_size=9096 webpack --config ./src/ipywidgets/webpack.config.js", + "checkDependencies": "gulp checkDependencies", + "postinstall": "node ./build/ci/postInstall.js", + "test": "node ./out/test/standardTest.js && node ./out/test/multiRootTest.js", + "test:unittests": "mocha --config ./build/.mocha.unittests.js.json", + "test:unittests:cover": "nyc --no-clean --nycrc-path build/.nycrc mocha --config ./build/.mocha.unittests.ts.json", + "test:functional": "mocha --require source-map-support/register --config ./build/.mocha.functional.json", + "test:functional:perf": "node --inspect-brk ./node_modules/mocha/bin/_mocha --require source-map-support/register --config ./build/.mocha.functional.perf.json", + "test:functional:memleak": "node --inspect-brk ./node_modules/mocha/bin/_mocha --require source-map-support/register --config ./build/.mocha.functional.json", + "test:functional:cover": "npm run test:functional", + "test:functional:split": "node ./build/ci/scripts/runFunctionalTests.js", + "test:cover:report": "nyc --nycrc-path build/.nycrc report --reporter=text --reporter=html --reporter=text-summary --reporter=cobertura", + "testDebugger": "node ./out/test/testBootstrap.js ./out/test/debuggerTest.js", + "testSingleWorkspace": "node ./out/test/testBootstrap.js ./out/test/standardTest.js", + "pretestDataScience": "node ./out/test/datascience/dsTestSetup.js", + "testDataScience": "cross-env CODE_TESTS_WORKSPACE=src/test/datascience VSC_PYTHON_CI_TEST_VSC_CHANNEL=insiders TEST_FILES_SUFFIX=ds.test VSC_PYTHON_FORCE_LOGGING=1 VSC_PYTHON_LOAD_EXPERIMENTS_FROM_FILE=true node ./out/test/testBootstrap.js ./out/test/standardTest.js", + "testMultiWorkspace": "node ./out/test/testBootstrap.js ./out/test/multiRootTest.js", + "testPerformance": "node ./out/test/testBootstrap.js ./out/test/performanceTest.js", + "testSmoke": "node ./out/test/smokeTest.js", + "lint-staged": "node gulpfile.js", + "lint": "tslint src/**/*.ts -t verbose", + "prettier-fix": "prettier 'src/**/*.ts*' --write && prettier 'build/**/*.js' --write", + "clean": "gulp clean", + "updateBuildNumber": "gulp updateBuildNumber", + "verifyBundle": "gulp verifyBundle", + "webpack": "webpack" + }, + "dependencies": { + "@jupyter-widgets/schema": "^0.4.0", + "@jupyterlab/coreutils": "^3.1.0", + "@jupyterlab/services": "^4.2.0", + "@loadable/component": "^5.12.0", + "@nteract/messaging": "^7.0.0", + "@types/tcp-port-used": "^1.0.0", + "ansi-regex": "^4.1.0", + "arch": "^2.1.0", + "azure-storage": "^2.10.3", + "detect-indent": "^6.0.0", + "diff-match-patch": "^1.0.0", + "fast-deep-equal": "^2.0.1", + "font-awesome": "^4.7.0", + "fs-extra": "^4.0.3", + "fuzzy": "^0.1.3", + "get-port": "^3.2.0", + "glob": "^7.1.2", + "hash.js": "^1.1.7", + "iconv-lite": "^0.4.21", + "inversify": "^4.11.1", + "is-online": "^8.2.1", + "jsonc-parser": "^2.0.3", + "line-by-line": "^0.1.6", + "lodash": "^4.17.19", + "log4js": "^6.1.2", + "md5": "^2.2.1", + "minimatch": "^3.0.4", + "named-js-regexp": "^1.3.3", + "node-fetch": "^2.6.0", + "node-stream-zip": "^1.6.0", + "onigasm": "^2.2.2", + "pdfkit": "^0.11.0", + "pidusage": "^1.2.0", + "portfinder": "^1.0.25", + "react-draggable": "^4.4.2", + "reflect-metadata": "^0.1.12", + "request": "^2.87.0", + "request-progress": "^3.0.0", + "rxjs": "^6.5.4", + "rxjs-compat": "^6.5.4", + "sanitize-filename": "^1.6.3", + "semver": "^5.5.0", + "stack-trace": "0.0.10", + "string-argv": "^0.3.1", + "strip-ansi": "^5.2.0", + "sudo-prompt": "^8.2.0", + "svg-to-pdfkit": "^0.1.8", + "tcp-port-used": "^1.0.1", + "tmp": "^0.0.29", + "tree-kill": "^1.2.2", + "typescript-char": "^0.0.0", + "uint64be": "^1.0.1", + "unicode": "^10.0.0", + "untildify": "^3.0.2", + "vscode-debugadapter": "^1.28.0", + "vscode-debugprotocol": "^1.28.0", + "vscode-extension-telemetry": "0.1.4", + "vscode-jsonrpc": "6.0.0-next.5", + "vscode-languageclient": "7.0.0-next.9", + "vscode-languageserver": "7.0.0-next.7", + "vscode-languageserver-protocol": "3.16.0-next.7", + "vscode-tas-client": "^0.1.4", + "vsls": "^0.3.1291", + "winreg": "^1.2.4", + "winston": "^3.2.1", + "ws": "^6.0.0", + "xml2js": "^0.4.19", + "zeromq": "^6.0.0-beta.6" + }, + "devDependencies": { + "@babel/cli": "^7.8.4", + "@babel/core": "^7.4.4", + "@babel/plugin-transform-runtime": "^7.4.4", + "@babel/polyfill": "^7.4.4", + "@babel/preset-env": "^7.1.0", + "@babel/preset-react": "^7.0.0", + "@babel/register": "^7.9.0", + "@blueprintjs/select": "^3.11.2", + "@enonic/fnv-plus": "^1.3.0", + "@istanbuljs/nyc-config-typescript": "^0.1.3", + "@jupyter-widgets/base": "^2.0.1", + "@jupyter-widgets/controls": "^1.5.2", + "@jupyter-widgets/jupyterlab-manager": "^1.0.2", + "@jupyter-widgets/output": "^2.0.1", + "@nteract/transform-dataresource": "^4.3.5", + "@nteract/transform-geojson": "^3.2.3", + "@nteract/transform-model-debug": "^3.2.3", + "@nteract/transform-plotly": "^6.0.0", + "@nteract/transform-vega": "^6.0.3", + "@nteract/transforms": "^4.4.7", + "@phosphor/widgets": "^1.9.3", + "@sinonjs/fake-timers": "^6.0.1", + "@testing-library/react": "^9.4.0", + "@types/ansi-regex": "^4.0.0", + "@types/chai": "^4.1.2", + "@types/chai-arrays": "^1.0.2", + "@types/chai-as-promised": "^7.1.0", + "@types/copy-webpack-plugin": "^4.4.2", + "@types/cors": "^2.8.6", + "@types/debug": "^4.1.5", + "@types/dedent": "^0.7.0", + "@types/del": "^3.0.0", + "@types/diff-match-patch": "^1.0.32", + "@types/download": "^6.2.2", + "@types/enzyme": "^3.1.14", + "@types/enzyme-adapter-react-16": "^1.0.3", + "@types/event-stream": "^3.3.33", + "@types/fs-extra": "^5.0.1", + "@types/get-port": "^3.2.0", + "@types/glob": "^5.0.35", + "@types/html-webpack-plugin": "^3.2.0", + "@types/iconv-lite": "^0.0.1", + "@types/jsdom": "^11.12.0", + "@types/loadable__component": "^5.10.0", + "@types/loader-utils": "^1.1.3", + "@types/lodash": "^4.14.104", + "@types/md5": "^2.1.32", + "@types/memoize-one": "^4.1.1", + "@types/mocha": "^5.2.7", + "@types/nock": "^10.0.3", + "@types/node": "^10.14.18", + "@types/node-fetch": "^2.5.7", + "@types/pdfkit": "^0.7.36", + "@types/promisify-node": "^0.4.0", + "@types/react": "^16.4.14", + "@types/react-dom": "^16.0.8", + "@types/react-json-tree": "^0.6.8", + "@types/react-redux": "^7.1.5", + "@types/react-virtualized": "^9.21.2", + "@types/redux-logger": "^3.0.7", + "@types/request": "^2.47.0", + "@types/semver": "^5.5.0", + "@types/shortid": "^0.0.29", + "@types/sinon": "^7.5.1", + "@types/sinonjs__fake-timers": "^6.0.1", + "@types/socket.io": "^2.1.4", + "@types/stack-trace": "0.0.29", + "@types/temp": "^0.8.32", + "@types/tmp": "0.0.33", + "@types/untildify": "^3.0.0", + "@types/uuid": "^3.4.3", + "@types/vscode": "^1.47.0", + "@types/vscode-notebook-renderer": "^1.48.0", + "@types/webpack-bundle-analyzer": "^2.13.0", + "@types/winreg": "^1.2.30", + "@types/ws": "^6.0.1", + "@types/xml2js": "^0.4.2", + "@typescript-eslint/eslint-plugin": "^3.7.0", + "@typescript-eslint/parser": "^3.7.0", + "acorn": "^6.4.1", + "ansi-to-html": "^0.6.7", + "babel-loader": "^8.0.3", + "babel-plugin-inline-json-import": "^0.3.1", + "babel-plugin-transform-runtime": "^6.23.0", + "babel-polyfill": "^6.26.0", + "bootstrap": "^4.3.1", + "bootstrap-less": "^3.3.8", + "brfs": "^2.0.2", + "cache-loader": "^4.1.0", + "canvas": "^2.6.0", + "chai": "^4.1.2", + "chai-arrays": "^2.0.0", + "chai-as-promised": "^7.1.1", + "chai-http": "^4.3.0", + "codecov": "^3.7.1", + "colors": "^1.2.1", + "copy-webpack-plugin": "^5.1.1", + "cors": "^2.8.5", + "cross-env": "^6.0.3", + "cross-spawn": "^6.0.5", + "css-loader": "^1.0.1", + "dedent": "^0.7.0", + "deemon": "^1.4.0", + "del": "^3.0.0", + "download": "^7.0.0", + "enzyme": "^3.7.0", + "enzyme-adapter-react-16": "^1.6.0", + "eslint": "^7.2.0", + "eslint-config-airbnb": "^18.2.0", + "eslint-config-prettier": "^6.9.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-prettier": "^3.1.2", + "eslint-plugin-react": "^7.20.3", + "eslint-plugin-react-hooks": "^4.0.0", + "event-stream": "3.3.4", + "expose-loader": "^0.7.5", + "express": "^4.17.1", + "extract-zip": "^1.6.7", + "fast-xml-parser": "^3.16.0", + "file-loader": "^5.1.0", + "filemanager-webpack-plugin-fixed": "^2.0.9", + "flat": "^4.0.0", + "fork-ts-checker-webpack-plugin": "^4.1.6", + "gulp": "^4.0.0", + "gulp-azure-storage": "^0.11.1", + "gulp-chmod": "^2.0.0", + "gulp-filter": "^5.1.0", + "gulp-gunzip": "^1.1.0", + "gulp-rename": "^1.4.0", + "gulp-sourcemaps": "^2.6.4", + "gulp-typescript": "^4.0.1", + "gulp-untar": "0.0.8", + "gulp-vinyl-zip": "^2.1.2", + "html-webpack-plugin": "^3.2.0", + "husky": "^1.1.2", + "immutable": "^4.0.0-rc.12", + "jsdom": "^15.0.0", + "json-loader": "^0.5.7", + "less": "^3.9.0", + "less-loader": "^5.0.0", + "less-plugin-inline-urls": "^1.2.0", + "loader-utils": "^1.1.0", + "lolex": "^5.1.2", + "memoize-one": "^5.1.1", + "mocha": "^8.1.1", + "mocha-junit-reporter": "^1.17.0", + "mocha-multi-reporters": "^1.1.7", + "monaco-editor": "0.18.1", + "monaco-editor-textmate": "^2.2.1", + "monaco-editor-webpack-plugin": "^1.7.0", + "monaco-textmate": "^3.0.1", + "nocache": "^2.1.0", + "nock": "^10.0.6", + "node-has-native-dependencies": "^1.0.2", + "node-html-parser": "^1.1.13", + "nyc": "^15.0.0", + "playwright-chromium": "^0.13.0", + "plotly.js-dist": "^1.55.2", + "postcss": "^7.0.27", + "postcss-cssnext": "^3.1.0", + "postcss-import": "^12.0.1", + "postcss-loader": "^3.0.0", + "prettier": "^2.0.2", + "range-inclusive": "^1.0.2", + "raw-loader": "^0.5.1", + "react": "^16.5.2", + "react-data-grid": "^6.0.2-0", + "react-dev-utils": "^5.0.2", + "react-dom": "^16.5.2", + "react-json-tree": "^0.11.0", + "react-redux": "^7.1.1", + "react-svg-pan-zoom": "^3.1.0", + "react-svgmt": "^1.1.8", + "react-virtualized": "^9.21.1", + "redux": "^4.0.4", + "redux-logger": "^3.0.6", + "relative": "^3.0.2", + "remove-files-webpack-plugin": "^1.4.0", + "requirejs": "^2.3.6", + "rewiremock": "^3.13.0", + "rimraf": "^3.0.2", + "sass-loader": "^7.1.0", + "serialize-javascript": "^3.1.0", + "shortid": "^2.2.8", + "sinon": "^8.0.1", + "slickgrid": "^2.4.17", + "socket.io": "^2.3.0", + "source-map-support": "^0.5.12", + "style-loader": "^0.23.1", + "styled-jsx": "^3.1.0", + "svg-inline-loader": "^0.8.0", + "svg-inline-react": "^3.1.0", + "terser-webpack-plugin": "^3.1.0", + "thread-loader": "^2.1.3", + "transform-loader": "^0.2.4", + "ts-loader": "^5.3.0", + "ts-mock-imports": "^1.3.0", + "ts-mockito": "^2.5.0", + "ts-node": "^8.3.0", + "tsconfig-paths-webpack-plugin": "^3.2.0", + "tslint": "^5.20.1", + "tslint-config-prettier": "^1.18.0", + "tslint-eslint-rules": "^5.1.0", + "tslint-microsoft-contrib": "^5.0.3", + "tslint-plugin-prettier": "^2.1.0", + "typed-react-markdown": "^0.1.0", + "typemoq": "^2.1.0", + "typescript": "^4.0.2", + "typescript-formatter": "^7.1.0", + "unicode-properties": "^1.3.1", + "url-loader": "^1.1.2", + "uuid": "^3.3.2", + "vinyl-fs": "^3.0.3", + "vsce": "^1.59.0", + "vscode-debugadapter-testsupport": "^1.27.0", + "vscode-test": "^1.2.3", + "webpack": "^4.33.0", + "webpack-bundle-analyzer": "^3.6.0", + "webpack-cli": "^3.1.2", + "webpack-fix-default-import-plugin": "^1.0.3", + "webpack-merge": "^4.1.4", + "webpack-node-externals": "^1.7.2", + "webpack-require-from": "^1.8.0", + "why-is-node-running": "^2.0.3", + "wtfnode": "^0.8.0", + "yargs": "^15.3.1" + }, + "__metadata": { + "id": "f1f59ae4-9318-4f3c-a9b5-81b2eaa5f8a5", + "publisherDisplayName": "Microsoft", + "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" } - }, - "scripts": { - "vscode:prepublish": "node ./node_modules/vscode/bin/compile", - "compile": "node ./node_modules/vscode/bin/compile -watch -p ./ && installServerIntoExtension ./out ./src/server/package.json ./src/server/tsconfig.json", - "postinstall": "node ./node_modules/vscode/bin/install" - }, - "dependencies": { - "fs-extra": "^0.26.5", - "line-by-line": "^0.1.4", - "named-js-regexp": "^1.3.1", - "prepend-file": "^1.3.0", - "tmp": "0.0.28", - "tree-kill": "^1.0.0", - "uint64be": "^1.0.1", - "vscode-debugadapter": "^1.0.1", - "vscode-debugprotocol": "^1.0.1", - "vscode-languageclient": "^1.1.0", - "vscode-languageserver": "^1.1.0" - }, - "devDependencies": { - "typescript": "^1.7.5", - "vscode": "^0.11.0" - } } diff --git a/package.nls.de.json b/package.nls.de.json new file mode 100644 index 000000000000..77ec14b3ee9c --- /dev/null +++ b/package.nls.de.json @@ -0,0 +1,31 @@ +{ + "python.command.python.sortImports.title": "Sortieren der Importe", + "python.command.python.startREPL.title": "Starten des REPL", + "python.command.python.createTerminal.title": "Terminal erstellen", + "python.command.python.buildWorkspaceSymbols.title": "Arbeitsplatz-Symbole erstellen", + "python.command.python.runtests.title": "Alle Unittests ausführen", + "python.command.python.debugtests.title": "Alle Unittests debuggen", + "python.command.python.execInTerminal.title": "Python-Datei im Terminal ausführen", + "python.command.python.setInterpreter.title": "Interpreter auswählen", + "python.command.python.refactorExtractVariable.title": "Variable extrahieren", + "python.command.python.refactorExtractMethod.title": "Methode extrahieren", + "python.command.python.viewTestOutput.title": "Unittest-Ausgabe anzeigen", + "python.command.python.selectAndRunTestMethod.title": "Unittest-Methode ausführen ...", + "python.command.python.selectAndDebugTestMethod.title": "Unittest-Debug-Methode ausführen ...", + "python.command.python.selectAndRunTestFile.title": "Unittest-Datei ausführen ...", + "python.command.python.runCurrentTestFile.title": "Ausgewählte Unittest-Datei ausführen", + "python.command.python.runFailedTests.title": "Fehlerhafte Unittests ausführen", + "python.command.python.discoverTests.title": "Unittests durchsuchen", + "python.command.python.execSelectionInTerminal.title": "Selektion/Reihe in Python-Terminal ausführen", + "python.command.python.execSelectionInDjangoShell.title": "Selektion/Reihe in Django-Shell ausführen", + "python.command.python.goToPythonObject.title": "Gehe zu Python-Objekt", + "python.command.python.setLinter.title": "Linter auswählen", + "python.command.python.enableLinting.title": "Linting aktivieren", + "python.command.python.runLinting.title": "Linting ausführen", + "python.snippet.launch.standard.label": "Python: Aktuelle Datei", + "python.snippet.launch.module.label": "Python: Modul", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Pyramid-Anwendung", + "python.snippet.launch.attach.label": "Python: Anfügen" +} diff --git a/package.nls.es.json b/package.nls.es.json new file mode 100644 index 000000000000..227e268dbc8d --- /dev/null +++ b/package.nls.es.json @@ -0,0 +1,31 @@ +{ + "python.command.python.sortImports.title": "Ordenar importaciones", + "python.command.python.startREPL.title": "Nuevo REPL", + "python.command.python.createTerminal.title": "Nueva terminal", + "python.command.python.buildWorkspaceSymbols.title": "Compilar símbolos del área de trabajo", + "python.command.python.runtests.title": "Ejecutar todas las pruebas unitarias", + "python.command.python.debugtests.title": "Depurar todas las pruebas unitarias", + "python.command.python.execInTerminal.title": "Ejecutar archivo Python en la terminal", + "python.command.python.setInterpreter.title": "Seleccionar intérprete", + "python.command.python.refactorExtractVariable.title": "Extraer variable", + "python.command.python.refactorExtractMethod.title": "Extraer método", + "python.command.python.viewTestOutput.title": "Mostrar resultados de la prueba unitaria", + "python.command.python.selectAndRunTestMethod.title": "Método de ejecución de pruebas unitarias ...", + "python.command.python.selectAndDebugTestMethod.title": "Método de depuración de pruebas unitarias ...", + "python.command.python.selectAndRunTestFile.title": "Ejecutar archivo de prueba unitaria ...", + "python.command.python.runCurrentTestFile.title": "Ejecutar archivo de prueba unitaria actual", + "python.command.python.runFailedTests.title": "Ejecutar pruebas unitarias fallidas", + "python.command.python.discoverTests.title": "Encontrar pruebas unitarias", + "python.command.python.execSelectionInTerminal.title": "Ejecutar línea/selección en la terminal", + "python.command.python.execSelectionInDjangoShell.title": "Ejecutar línea/selección en el intérprete de Django", + "python.command.python.goToPythonObject.title": "Ir al objeto de Python", + "python.command.python.setLinter.title": "Seleccionar Linter", + "python.command.python.enableLinting.title": "Habilitar Linting", + "python.command.python.runLinting.title": "Ejecutar Linting", + "python.snippet.launch.standard.label": "Python: Archivo actual", + "python.snippet.launch.module.label": "Python: Módulo", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Pyramid", + "python.snippet.launch.attach.label": "Python: Adjuntar" +} diff --git a/package.nls.fr.json b/package.nls.fr.json new file mode 100644 index 000000000000..ad28a94d6e09 --- /dev/null +++ b/package.nls.fr.json @@ -0,0 +1,30 @@ +{ + "python.command.python.sortImports.title": "Trier les imports", + "python.command.python.startREPL.title": "Démarrer la console interactive", + "python.command.python.createTerminal.title": "Créer un terminal", + "python.command.python.buildWorkspaceSymbols.title": "Construire les symboles de l'espace de travail", + "python.command.python.runtests.title": "Exécuter tous les tests unitaires", + "python.command.python.debugtests.title": "Déboguer tous les tests unitaires", + "python.command.python.execInTerminal.title": "Exécuter le script Python dans un terminal", + "python.command.python.setInterpreter.title": "Sélectionner l'interpreteur", + "python.command.python.refactorExtractVariable.title": "Extraire la variable", + "python.command.python.refactorExtractMethod.title": "Extraire la méthode", + "python.command.python.viewTestOutput.title": "Afficher la sortie des tests unitaires", + "python.command.python.selectAndRunTestMethod.title": "Exécuter la méthode de test unitaire ...", + "python.command.python.selectAndDebugTestMethod.title": "Déboguer la méthode de test unitaire ...", + "python.command.python.selectAndRunTestFile.title": "Exécuter le fichier de test unitaire ...", + "python.command.python.runCurrentTestFile.title": "Exécuter le fichier de test unitaire courant", + "python.command.python.runFailedTests.title": "Exécuter les derniers test unitaires échoués", + "python.command.python.execSelectionInTerminal.title": "Exécuter la ligne/sélection dans un terminal Python", + "python.command.python.execSelectionInDjangoShell.title": "Exécuter la ligne/sélection dans un shell Django", + "python.command.python.goToPythonObject.title": "Se rendre à l'objet Python", + "python.command.python.setLinter.title": "Sélectionner le linter", + "python.command.python.enableLinting.title": "Activer le linting", + "python.command.python.runLinting.title": "Exécuter le linting", + "python.snippet.launch.standard.label": "Python : Fichier actuel", + "python.snippet.launch.module.label": "Python: Module", + "python.snippet.launch.django.label": "Python : Django", + "python.snippet.launch.flask.label": "Python : Flask", + "python.snippet.launch.pyramid.label": "Python : application Pyramid", + "python.snippet.launch.attach.label": "Python: Attacher" +} diff --git a/package.nls.it.json b/package.nls.it.json new file mode 100644 index 000000000000..c16d6ce74241 --- /dev/null +++ b/package.nls.it.json @@ -0,0 +1,31 @@ +{ + "python.command.python.sortImports.title": "Ordina gli import", + "python.command.python.startREPL.title": "Apri nuova REPL", + "python.command.python.createTerminal.title": "Apri nuovo terminale", + "python.command.python.buildWorkspaceSymbols.title": "Compila simboli dello spazio di lavoro", + "python.command.python.runtests.title": "Esegui tutti i test", + "python.command.python.debugtests.title": "Esegui debug di tutti i test", + "python.command.python.execInTerminal.title": "Esegui file Python nel terminale", + "python.command.python.setInterpreter.title": "Seleziona interprete", + "python.command.python.refactorExtractVariable.title": "Estrai variable", + "python.command.python.refactorExtractMethod.title": "Estrai metodo", + "python.command.python.viewTestOutput.title": "Mostra output dei test", + "python.command.python.selectAndRunTestMethod.title": "Esegui metodo di test ...", + "python.command.python.selectAndDebugTestMethod.title": "Esegui debug del metodo di test ...", + "python.command.python.selectAndRunTestFile.title": "Esegui file di test ...", + "python.command.python.runCurrentTestFile.title": "Esegui file di test attuale", + "python.command.python.runFailedTests.title": "Esegui test falliti", + "python.command.python.execSelectionInTerminal.title": "Esegui selezione/linea nel terminale di Python", + "python.command.python.execSelectionInDjangoShell.title": "Esegui selezione/linea nella shell Django", + "python.command.python.goToPythonObject.title": "Vai a oggetto Python", + "python.command.python.setLinter.title": "Selezione Linter", + "python.command.python.enableLinting.title": "Attiva Linting", + "python.command.python.runLinting.title": "Esegui Linting", + "python.snippet.launch.standard.label": "Python: File corrente", + "python.snippet.launch.module.label": "Python: Modulo", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Applicazione Pyramid", + "python.snippet.launch.attach.label": "Python: Allega", + "ExtensionSurveyBanner.bannerLabelYes": "Sì, prenderò il sondaggio ora" +} diff --git a/package.nls.ja.json b/package.nls.ja.json new file mode 100644 index 000000000000..a2db0a823636 --- /dev/null +++ b/package.nls.ja.json @@ -0,0 +1,26 @@ +{ + "python.command.python.sortImports.title": "import 文を並び替える", + "python.command.python.startREPL.title": "REPL を開始", + "python.command.python.buildWorkspaceSymbols.title": "ワークスペースのシンボルをビルド", + "python.command.python.runtests.title": "すべての単体テストを実行", + "python.command.python.debugtests.title": "すべての単体テストをデバッグ", + "python.command.python.execInTerminal.title": "ターミナルで Python ファイルを実行", + "python.command.python.setInterpreter.title": "インタープリターを選択", + "python.command.python.refactorExtractVariable.title": "変数を抽出", + "python.command.python.refactorExtractMethod.title": "メソッドを抽出", + "python.command.python.viewTestOutput.title": "単体テストの出力を表示", + "python.command.python.selectAndRunTestMethod.title": "単体テストメソッドを実行...", + "python.command.python.selectAndDebugTestMethod.title": "単体テストメソッドをデバッグ...", + "python.command.python.selectAndRunTestFile.title": "単体テストファイルを実行...", + "python.command.python.runCurrentTestFile.title": "現在の単体テストファイルを実行", + "python.command.python.runFailedTests.title": "失敗した単体テストを実行", + "python.command.python.execSelectionInTerminal.title": "Python ターミナルで選択範囲/行を実行", + "python.command.python.execSelectionInDjangoShell.title": "Django シェルで選択範囲/行を実行", + "python.command.python.goToPythonObject.title": "Python オブジェクトに移動", + "python.snippet.launch.standard.label": "Python: Current File", + "python.snippet.launch.module.label": "Python: モジュール", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Pyramid アプリケーション", + "python.snippet.launch.attach.label": "Python: アタッチ" +} diff --git a/package.nls.json b/package.nls.json new file mode 100644 index 000000000000..b9368dd66b90 --- /dev/null +++ b/package.nls.json @@ -0,0 +1,598 @@ +{ + "python.command.python.sortImports.title": "Sort Imports", + "python.command.python.startREPL.title": "Start REPL", + "python.command.python.createTerminal.title": "Create Terminal", + "python.command.python.buildWorkspaceSymbols.title": "Build Workspace Symbols", + "python.command.python.runtests.title": "Run All Tests", + "python.command.python.debugtests.title": "Debug All Tests", + "python.command.python.execInTerminal.title": "Run Python File in Terminal", + "python.command.python.setInterpreter.title": "Select Interpreter", + "python.command.python.switchOffInsidersChannel.title": "Switch to Default Channel", + "python.command.python.switchToDailyChannel.title": "Switch to Insiders Daily Channel", + "python.command.python.switchToWeeklyChannel.title": "Switch to Insiders Weekly Channel", + "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", + "python.command.python.resetInterpreterSecurityStorage.title": "Reset Stored Info for Untrusted Interpreters", + "python.command.python.refactorExtractVariable.title": "Extract Variable", + "python.command.python.refactorExtractMethod.title": "Extract Method", + "python.command.python.viewOutput.title": "Show Output", + "python.command.python.viewTestOutput.title": "Show Test Output", + "python.command.python.datascience.viewJupyterOutput.title": "Show Jupyter Output", + "python.command.python.datascience.exportAsPythonScript.title": "Export as Python Script", + "python.command.python.datascience.exportToHTML.title": "Export to HTML", + "python.command.python.datascience.exportToPDF.title": "Export to PDF", + "DataScience.checkingIfImportIsSupported": "Checking if import is supported", + "DataScience.installingMissingDependencies": "Installing missing dependencies", + "DataScience.exportNotebookToPython": "Exporting Notebook to Python", + "DataScience.performingExport": "Performing Export", + "DataScience.convertingToPDF": "Converting to PDF", + "DataScience.exportHTMLQuickPickLabel": "HTML", + "DataScience.exportPDFQuickPickLabel": "PDF", + "DataScience.openExportedFileMessage": "Would you like to open the exported file?", + "DataScience.openExportFileYes": "Yes", + "DataScience.openExportFileNo": "No", + "DataScience.failedExportMessage": "Export failed.", + "DataScience.exportToPDFDependencyMessage": "If you have not installed xelatex (TeX) you will need to do so before you can export to PDF, for further instructions please look [here](https://nbconvert.readthedocs.io/en/latest/install.html#installing-tex). \r\nTo avoid installing xelatex (TeX) you might want to try exporting to HTML and using your browsers \"Print to PDF\" feature.", + "DataScience.launchNotebookTrustPrompt": "A notebook could execute harmful code when opened. Some outputs have been hidden. Do you trust this notebook? [Learn more.](https://aka.ms/trusted-notebooks)", + "DataScience.launchNotebookTrustPrompt.yes": "Trust", + "DataScience.launchNotebookTrustPrompt.no": "Do not trust", + "DataScience.launchNotebookTrustPrompt.trustAllNotebooks": "Trust all notebooks", + "DataScience.insecureSessionMessage": "Connecting over HTTP without a token may be an insecure connection. Do you want to connect to a possibly insecure server?", + "DataScience.insecureSessionDenied": "Denied connection to insecure server.", + "python.command.python.viewLanguageServerOutput.title": "Show Language Server Output", + "python.command.python.selectAndRunTestMethod.title": "Run Test Method ...", + "python.command.python.selectAndDebugTestMethod.title": "Debug Test Method ...", + "python.command.python.selectAndRunTestFile.title": "Run Test File ...", + "python.command.python.runCurrentTestFile.title": "Run Current Test File", + "python.command.python.runFailedTests.title": "Run Failed Tests", + "python.command.python.discoverTests.title": "Discover Tests", + "python.command.python.discoveringTests.title": "Discovering...", + "python.command.python.stopTests.title": "Stop", + "python.command.python.configureTests.title": "Configure Tests", + "python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal", + "python.command.python.execSelectionInDjangoShell.title": "Run Selection/Line in Django Shell", + "python.command.python.goToPythonObject.title": "Go to Python Object", + "python.command.python.setLinter.title": "Select Linter", + "python.command.python.enableLinting.title": "Enable Linting", + "python.command.python.runLinting.title": "Run Linting", + "python.command.python.datascience.runFileInteractive.title": "Run Current File in Python Interactive Window", + "python.command.python.datascience.debugFileInteractive.title": "Debug Current File in Python Interactive Window", + "python.command.python.datascience.runallcells.title": "Run All Cells", + "python.command.python.datascience.notebookeditor.runallcells.title": "Run All Notebook Cells", + "python.command.python.datascience.runallcellsabove.title": "Run Above", + "python.command.python.datascience.runcellandallbelow.title": "Run Below", + "python.command.python.datascience.runallcellsabove.palette.title": "Run Cells Above Current Cell", + "python.command.python.datascience.runcurrentcellandallbelow.palette.title": "Run Current Cell and Below", + "python.command.python.datascience.debugcurrentcell.palette.title": "Debug Current Cell", + "python.command.python.datascience.debugcell.title": "Debug Cell", + "python.command.python.datascience.debugstepover.title": "Step Over", + "python.command.python.datascience.debugcontinue.title": "Continue", + "python.command.python.datascience.debugstop.title": "Stop", + "python.command.python.datascience.runtoline.title": "Run To Line in Python Interactive Window", + "python.command.python.datascience.runfromline.title": "Run From Line in Python Interactive Window", + "python.command.python.datascience.runcurrentcell.title": "Run Current Cell", + "python.command.python.datascience.runcurrentcelladvance.title": "Run Current Cell And Advance", + "python.command.python.datascience.execSelectionInteractive.title": "Run Selection/Line in Python Interactive Window", + "python.command.python.datascience.runcell.title": "Run Cell", + "python.command.python.datascience.insertCellBelowPosition.title": "Insert Cell Below Position", + "python.command.python.datascience.insertCellBelow.title": "Insert Cell Below", + "python.command.python.datascience.insertCellAbove.title": "Insert Cell Above", + "python.command.python.datascience.deleteCells.title": "Delete Selected Cells", + "python.command.python.datascience.selectCell.title": "Select Cell", + "python.command.python.datascience.selectCellContents.title": "Select Cell Contents", + "python.command.python.datascience.extendSelectionByCellAbove.title": "Extend Selection By Cell Above", + "python.command.python.datascience.extendSelectionByCellBelow.title": "Extend Selection By Cell Below", + "python.command.python.datascience.moveCellsUp.title": "Move Selected Cells Up", + "python.command.python.datascience.moveCellsDown.title": "Move Selected Cells Down", + "python.command.python.datascience.changeCellToMarkdown.title": "Change Cell to Markdown", + "python.command.python.datascience.changeCellToCode.title": "Change Cell to Code", + "python.command.python.datascience.gotoNextCellInFile.title": "Go to Next Cell", + "python.command.python.datascience.gotoPrevCellInFile.title": "Go to Previous Cell", + "python.command.python.datascience.showhistorypane.title": "Show Python Interactive Window", + "python.command.python.datascience.createnewinteractive.title": "Create Python Interactive Window", + "python.command.python.datascience.selectjupyteruri.title": "Specify local or remote Jupyter server for connections", + "python.command.python.datascience.selectjupytercommandline.title": "Specify Jupyter command line arguments", + "python.command.python.datascience.importnotebook.title": "Import Jupyter Notebook", + "python.command.python.datascience.opennotebook.title": "Open in Notebook Editor", + "python.command.python.datascience.opennotebookInPreviewEditor.title": "Open in preview Notebook Editor", + "python.command.python.datascience.importnotebookfile.title": "Convert to Python Script", + "python.command.python.enableSourceMapSupport.title": "Enable Source Map Support For Extension Debugging", + "python.command.python.datascience.exportoutputasnotebook.title": "Export Python Interactive Window as Jupyter Notebook", + "python.command.python.datascience.exportfileasnotebook.title": "Export Current Python File as Jupyter Notebook", + "python.command.python.datascience.exportfileandoutputasnotebook.title": "Export Current Python File and Output as Jupyter Notebook", + "python.command.python.datascience.undocells.title": "Undo Last Python Interactive Action", + "python.command.python.datascience.redocells.title": "Redo Last Python Interactive Action", + "python.command.python.datascience.removeallcells.title": "Delete All Python Interactive Cells", + "python.command.python.datascience.notebookeditor.removeallcells.title": "Delete All Notebook Editor Cells", + "python.command.python.datascience.notebookeditor.runselectedcell.title": "Run Selected Notebook Cell", + "python.command.python.datascience.notebookeditor.addcellbelow.title": "Add Empty Cell to Notebook File", + "python.command.python.datascience.interruptkernel.title": "Interrupt IPython Kernel", + "python.command.python.datascience.restartkernel.title": "Restart IPython Kernel", + "python.command.python.datascience.expandallcells.title": "Expand All Python Interactive Cells", + "python.command.python.datascience.collapseallcells.title": "Collapse All Python Interactive Cells", + "python.command.python.datascience.addcellbelow.title": "Add Empty Cell to File", + "python.command.python.datascience.scrolltocell.title": "Scroll Cell Into View", + "python.command.python.datascience.createnewnotebook.title": "Create New Blank Jupyter Notebook", + "python.command.python.startPage.open.title": "Open Start Page", + "python.command.python.datascience.selectJupyterInterpreter.title": "Select Interpreter to start Jupyter server", + "Datascience.currentlySelectedJupyterInterpreterForPlaceholder": "current: {0}", + "python.command.python.analysis.clearCache.title": "Clear Module Analysis Cache", + "python.command.python.analysis.restartLanguageServer.title": "Restart Language Server", + "python.snippet.launch.standard.label": "Python: Current File", + "python.snippet.launch.module.label": "Python: Module", + "python.snippet.launch.module.default": "enter-your-module-name", + "python.snippet.launch.attach.label": "Python: Remote Attach", + "python.snippet.launch.attachpid.label": "Python: Attach using Process Id", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Pyramid Application", + "Pylance.proposePylanceMessage": "Try out a new faster, feature-rich language server for Python by Microsoft, Pylance! Install the extension now.", + "Pylance.tryItNow": "Try it now", + "Pylance.remindMeLater": "Remind me later", + "Pylance.installPylanceMessage": "Pylance extension is not installed. Click Yes to open Pylance installation page.", + "Pylance.pylanceNotInstalledMessage": "Pylance extension is not installed.", + "Pylance.pylanceInstalledReloadPromptMessage": "Pylance extension is now installed. Reload window to activate?", + "DataScience.unknownMimeTypeFormat": "Mime type {0} is not currently supported.", + "DataScience.historyTitle": "Python Interactive", + "DataScience.dataExplorerTitle": "Data Viewer", + "DataScience.badWebPanelFormatString": "

{0} is not a valid file name

", + "DataScience.sessionDisposed": "Cannot execute code, session has been disposed.", + "DataScience.rawKernelProcessNotStarted": "Raw kernel process was not able to start.", + "DataScience.rawKernelProcessExitBeforeConnect": "Raw kernel process exited before connecting.", + "DataScience.passwordFailure": "Failed to connect to password protected server. Check that password is correct.", + "DataScience.exportDialogTitle": "Export to Jupyter Notebook", + "DataScience.exportDialogFilter": "Jupyter Notebooks", + "DataScience.exportDialogComplete": "Notebook written to {0}", + "DataScience.exportDialogFailed": "Failed to export notebook. {0}", + "DataScience.exportOpenQuestion": "Open in browser", + "DataScience.exportOpenQuestion1": "Open in editor", + "DataScience.notebookExportAs": "Export As", + "DataScience.exportAsQuickPickPlaceholder": "Export As...", + "DataScience.exportPythonQuickPickLabel": "Python Script", + "DataScience.collapseInputTooltip": "Collapse input block", + "DataScience.collapseVariableExplorerTooltip": "Hide variables active in jupyter kernel", + "DataScience.collapseVariableExplorerLabel": "Variables", + "DataScience.expandVariableExplorerTooltip": "Show variables active in jupyter kernel", + "DataScience.close": "Close", + "DataScience.variableLoadingValue": "Loading...", + "DataScience.importDialogTitle": "Import Jupyter Notebook", + "DataScience.importDialogFilter": "Jupyter Notebooks", + "DataScience.notebookCheckForImportYes": "Import", + "DataScience.reloadRequired": "Please reload the window for new settings to take effect.", + "DataScience.notebookCheckForImportNo": "Later", + "DataScience.notebookCheckForImportDontAskAgain": "Don't Ask Again", + "DataScience.notebookCheckForImportTitle": "Do you want to import the Jupyter Notebook into Python code?", + "DataScience.jupyterNotSupported": "Jupyter cannot be started. Error attempting to locate jupyter: {0}", + "DataScience.jupyterNotSupportedBecauseOfEnvironment": "Activating {0} to run Jupyter failed with {1}.", + "DataScience.jupyterNbConvertNotSupported": "Importing notebooks requires Jupyter nbconvert to be installed.", + "DataScience.jupyterLaunchNoURL": "Failed to find the URL of the launched Jupyter notebook server", + "DataScience.jupyterLaunchTimedOut": "The Jupyter notebook server failed to launch in time", + "DataScience.jupyterSelfCertFail": "The security certificate used by server {0} was not issued by a trusted certificate authority.\r\nThis may indicate an attempt to steal your information.\r\nDo you want to enable the Allow Unauthorized Remote Connection setting for this workspace to allow you to connect?", + "DataScience.jupyterSelfCertEnable": "Yes, connect anyways", + "DataScience.jupyterSelfCertClose": "No, close the connection", + "DataScience.jupyterServerCrashed": "Jupyter server crashed. Unable to connect. \r\nError code from jupyter: {0}", + "DataScience.rawConnectionDisplayName": "Direct kernel connection", + "DataScience.rawConnectionBrokenError": "Direct kernel connection broken", + "DataScience.pythonInteractiveHelpLink": "Get more help", + "DataScience.markdownHelpInstallingMissingDependencies": "See [https://aka.ms/pyaiinstall](https://aka.ms/pyaiinstall) for help on installing Jupyter and related dependencies.", + "DataScience.importingFormat": "Importing {0}", + "DataScience.startingJupyter": "Starting Jupyter server", + "DataScience.connectingToJupyter": "Connecting to Jupyter server", + "DataScience.connectingToIPyKernel": "Connecting to IPython kernel", + "DataScience.connectedToIPyKernel": "Connected.", + "DataScience.connected": "Connected", + "DataScience.disconnected": "Disconnected", + "Experiments.inGroup": "User belongs to experiment group '{0}'", + "Interpreters.RefreshingInterpreters": "Refreshing Python Interpreters", + "Interpreters.entireWorkspace": "Entire workspace", + "Interpreters.pythonInterpreterPath": "Python interpreter path: {0}", + "Interpreters.LoadingInterpreters": "Loading Python Interpreters", + "Interpreters.unsafeInterpreterMessage": "We found a Python environment in this workspace. Do you want to select it to start up the features in the Python extension? Only accept if you trust this environment.", + "Interpreters.condaInheritEnvMessage": "We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change \"terminal.integrated.inheritEnv\" to false in your user settings.", + "Logging.CurrentWorkingDirectory": "cwd:", + "InterpreterQuickPickList.quickPickListPlaceholder": "Current: {0}", + "InterpreterQuickPickList.enterPath.detail": "Enter path or find an existing interpreter", + "InterpreterQuickPickList.enterPath.label": "Enter interpreter path...", + "InterpreterQuickPickList.enterPath.placeholder": "Enter path to a Python interpreter.", + "InterpreterQuickPickList.browsePath.label": "Find...", + "InterpreterQuickPickList.browsePath.detail": "Browse your file system to find a Python interpreter.", + "InterpreterQuickPickList.browsePath.title": "Select Python interpreter", + "diagnostics.upgradeCodeRunner": "Please update the Code Runner extension for it to be compatible with the Python extension.", + "Common.bannerLabelYes": "Yes", + "Common.bannerLabelNo": "No", + "Common.doNotShowAgain": "Do not show again", + "Common.reload": "Reload", + "Common.moreInfo": "More Info", + "Common.and": "and", + "Common.ok": "Ok", + "Common.install": "Install", + "Common.learnMore": "Learn more", + "Common.reportThisIssue": "Report this issue", + "CommonSurvey.remindMeLaterLabel": "Remind me later", + "CommonSurvey.yesLabel": "Yes, take survey now", + "CommonSurvey.noLabel": "No, thanks", + "OutputChannelNames.languageServer": "Python Language Server", + "OutputChannelNames.python": "Python", + "OutputChannelNames.pythonTest": "Python Test Log", + "OutputChannelNames.jupyter": "Jupyter", + "ExtensionSurveyBanner.bannerMessage": "Can you please take 2 minutes to tell us how the Python extension is working for you?", + "ExtensionSurveyBanner.bannerLabelYes": "Yes, take survey now", + "ExtensionSurveyBanner.bannerLabelNo": "No, thanks", + "ExtensionSurveyBanner.maybeLater": "Maybe later", + "ExtensionChannels.installingInsidersMessage": "Installing Insiders... ", + "ExtensionChannels.installingStableMessage": "Installing Stable... ", + "ExtensionChannels.installationCompleteMessage": "complete.", + "ExtensionChannels.downloadingInsidersMessage": "Downloading Insiders Extension... ", + "ExtensionChannels.yesWeekly": "Yes, weekly", + "ExtensionChannels.yesDaily": "Yes, daily", + "ExtensionChannels.promptMessage": "We noticed you are using Visual Studio Code Insiders. Would you like to use the Insiders build of the Python extension?", + "ExtensionChannels.reloadToUseInsidersMessage": "Please reload Visual Studio Code to use the insiders build of the Python extension.", + "ExtensionChannels.downloadCompletedOutputMessage": "Insiders build download complete.", + "ExtensionChannels.startingDownloadOutputMessage": "Starting download for Insiders build.", + "Interpreters.environmentPromptMessage": "We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?", + "DataScience.reloadAfterChangingJupyterServerConnection": "Please reload VS Code when changing the Jupyter Server connection.", + "DataScience.restartKernelMessage": "Do you want to restart the IPython kernel? All variables will be lost.", + "DataScience.restartKernelMessageYes": "Yes", + "DataScience.restartKernelMessageNo": "No", + "DataScience.restartingKernelFailed": "Kernel restart failed. Jupyter server is hung. Please reload VS Code.", + "DataScience.interruptingKernelFailed": "Kernel interrupt failed. Jupyter server is hung. Please reload VS Code.", + "DataScienceSurveyBanner.bannerMessage": "Can you please take 2 minutes to tell us how the Python Data Science features are working for you?", + "DataScienceSurveyBanner.bannerLabelYes": "Yes, take survey now", + "DataScienceSurveyBanner.bannerLabelNo": "No, thanks", + "InteractiveShiftEnterBanner.bannerMessage": "Would you like to run code in the 'Python Interactive' window (an IPython console) for 'shift-enter'? Select 'No' to continue to run code in the Python Terminal. This can be changed later in settings.", + "DataScience.restartingKernelStatus": "Restarting IPython Kernel", + "DataScience.executingCode": "Executing Cell", + "DataScience.collapseAll": "Collapse all cell inputs", + "DataScience.expandAll": "Expand all cell inputs", + "DataScience.export": "Export as Jupyter notebook", + "DataScience.restartServer": "Restart IPython kernel", + "DataScience.undo": "Undo", + "DataScience.redo": "Redo", + "DataScience.clearAll": "Remove all cells", + "DataScience.pythonVersionHeader": "Python version:", + "DataScience.pythonRestartHeader": "Restarted kernel:", + "DataScience.pythonNewHeader": "Started new kernel:", + "DataScience.pythonConnectHeader": "Connected to kernel:", + "DataScience.executingCodeFailure": "Executing code failed : {0}", + "DataScience.inputWatermark": "Type code here and press shift-enter to run", + "DataScience.deleteButtonTooltip": "Remove cell", + "DataScience.gotoCodeButtonTooltip": "Go to code", + "DataScience.copyBackToSourceButtonTooltip": "Paste code into file", + "DataScience.copyToClipboardButtonTooltip": "Copy source to clipboard", + "DataScience.plotOpen": "Expand image", + "Linter.enableLinter": "Enable {0}", + "Linter.enablePylint": "You have a pylintrc file in your workspace. Do you want to enable pylint?", + "Linter.replaceWithSelectedLinter": "Multiple linters are enabled in settings. Replace with '{0}'?", + "Installer.noCondaOrPipInstaller": "There is no Conda or Pip installer available in the selected environment.", + "Installer.noPipInstaller": "There is no Pip installer available in the selected environment.", + "Installer.searchForHelp": "Search for help", + "DataScience.libraryNotInstalled": "Data Science library {0} is not installed. Install?", + "DataScience.libraryRequiredToLaunchJupyterNotInstalled": "Data Science library {0} is not installed.", + "DataScience.librariesRequiredToLaunchJupyterNotInstalled": "Data Science libraries {0} are not installed.", + "DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter": "Data Science library {1} is not installed in interpreter {0}.", + "DataScience.librariesRequiredToLaunchJupyterNotInstalledInterpreter": "Data Science libraries {1} are not installed in interpreter {0}.", + "DataScience.jupyterInstall": "Install", + "DataScience.jupyterSelectURIPrompt": "Enter the URI of the running Jupyter server", + "DataScience.jupyterSelectURIInvalidURI": "Invalid URI specified", + "DataScience.jupyterSelectUserAndPasswordTitle": "Enter your user name and password to connect to Jupyter Hub", + "DataScience.jupyterSelectUserPrompt": "Enter your user name", + "DataScience.jupyterSelectPasswordPrompt": "Enter your password", + "DataScience.jupyterNotebookFailure": "Jupyter notebook failed to launch. \r\n{0}", + "DataScience.jupyterNotebookConnectFailed": "Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}", + "DataScience.jupyterNotebookRemoteConnectFailed": "Failed to connect to remote Jupyter notebook.\r\nCheck that the Jupyter Server URI setting has a valid running server specified.\r\n{0}\r\n{1}", + "DataScience.jupyterNotebookRemoteConnectSelfCertsFailed": "Failed to connect to remote Jupyter notebook.\r\nSpecified server is using self signed certs. Enable Allow Unauthorized Remote Connection setting to connect anyways\r\n{0}\r\n{1}", + "DataScience.notebookVersionFormat": "Jupyter Notebook Version: {0}", + "DataScience.jupyterKernelSpecNotFound": "Cannot create a Jupyter kernel spec and none are available for use", + "DataScience.jupyterKernelSpecModuleNotFound": "'Kernelspec' module not installed in the selected interpreter ({0}).\n Please re-install or update 'jupyter'.", + "DataScience.jupyterGetVariablesBadResults": "Failed to fetch variable info from the Jupyter server.", + "DataScience.liveShareConnectFailure": "Cannot connect to host Jupyter session. URI not found.", + "DataScience.liveShareCannotSpawnNotebooks": "Spawning Jupyter notebooks is not supported over a live share connection", + "DataScience.liveShareCannotImportNotebooks": "Importing notebooks is not currently supported over a live share connection", + "DataScience.liveShareHostFormat": "{0} Jupyter Server", + "DataScience.liveShareSyncFailure": "Synchronization failure during live share startup.", + "DataScience.liveShareServiceFailure": "Failure starting '{0}' service during live share connection.", + "DataScience.documentMismatch": "Cannot run cells, duplicate documents for {0} found.", + "DataScience.pythonInteractiveCreateFailed": "Failure to create a 'Python Interactive' window. Try reinstalling the Python extension.", + "diagnostics.removedPythonPathFromSettings": "We removed the \"python.pythonPath\" setting from your settings.json file as the setting is no longer used by the Python extension. You can get the path of your selected interpreter in the Python output channel. [Learn more](https://aka.ms/AA7jfor).", + "diagnostics.warnSourceMaps": "Source map support is enabled in the Python Extension, this will adversely impact performance of the extension.", + "diagnostics.disableSourceMaps": "Disable Source Map Support", + "diagnostics.warnBeforeEnablingSourceMaps": "Enabling source map support in the Python Extension will adversely impact performance of the extension.", + "diagnostics.enableSourceMapsAndReloadVSC": "Enable and reload Window", + "diagnostics.lsNotSupported": "Your operating system does not meet the minimum requirements of the Python Language Server. Reverting to the alternative autocompletion provider, Jedi.", + "diagnostics.invalidPythonPathInDebuggerSettings": "You need to select a Python interpreter before you start debugging.\n\nTip: click on \"Select Python Interpreter\" in the status bar.", + "diagnostics.invalidPythonPathInDebuggerLaunch": "The Python path in your debug configuration is invalid.", + "diagnostics.invalidDebuggerTypeDiagnostic": "Your launch.json file needs to be updated to change the \"pythonExperimental\" debug configurations to use the \"python\" debugger type, otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?", + "diagnostics.consoleTypeDiagnostic": "Your launch.json file needs to be updated to change the console type string from \"none\" to \"internalConsole\", otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?", + "diagnostics.justMyCodeDiagnostic": "Configuration \"debugStdLib\" in launch.json is no longer supported. It's recommended to replace it with \"justMyCode\", which is the exact opposite of using \"debugStdLib\". Would you like to automatically update your launch.json file to do that?", + "diagnostics.checkIsort5UpgradeGuide": "We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.", + "diagnostics.yesUpdateLaunch": "Yes, update launch.json", + "diagnostics.invalidTestSettings": "Your settings needs to be updated to change the setting \"python.unitTest.\" to \"python.testing.\", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?", + "DataScience.interruptKernel": "Interrupt IPython kernel", + "DataScience.clearAllOutput": "Clear All Output", + "DataScience.exportingFormat": "Exporting {0}", + "DataScience.exportCancel": "Cancel", + "Common.canceled": "Canceled", + "Common.cancel": "Cancel", + "Common.yesPlease": "Yes, please", + "DataScience.importChangeDirectoryComment": "{0} Change working directory from the workspace root to the ipynb file location. Turn this addition off with the DataScience.changeDirOnImportExport setting", + "DataScience.exportChangeDirectoryComment": "# Change directory to VSCode workspace root so that relative path loads work correctly. Turn this addition off with the DataScience.changeDirOnImportExport setting", + "DataScience.interruptKernelStatus": "Interrupting IPython Kernel", + "DataScience.restartKernelAfterInterruptMessage": "Interrupting the kernel timed out. Do you want to restart the kernel instead? All variables will be lost.", + "DataScience.pythonInterruptFailedHeader": "Keyboard interrupt crashed the kernel. Kernel restarted.", + "DataScience.sysInfoURILabel": "Jupyter Server URI: ", + "DataScience.jupyterStartTimedout": "Starting Jupyter has timedout. Please check the 'Jupyter' output panel for further details.", + "DataScience.startingJupyterLogMessage": "Starting Jupyter from {0} with command line {1}", + "DataScience.jupyterCommandLineDefaultLabel": "Default (recommended)", + "DataScience.jupyterCommandLineDefaultDetail": "The Python extension will determine the appropriate command line for Jupyter", + "DataScience.jupyterCommandLineCustomLabel": "Custom", + "DataScience.jupyterCommandLineCustomDetail": "Customize the command line passed to Jupyter on startup", + "DataScience.jupyterCommandLineReloadQuestion": "Please reload the window when changing the Jupyter command line.", + "DataScience.jupyterCommandLineReloadAnswer": "Reload", + "DataScience.jupyterCommandLineQuickPickPlaceholder": "Choose an option", + "DataScience.jupyterCommandLineQuickPickTitle": "Pick command line for Jupyter", + "DataScience.jupyterCommandLinePrompt": "Enter your custom command line for Jupyter", + "Common.loadingPythonExtension": "Python extension loading...", + "debug.selectConfigurationTitle": "Select a debug configuration", + "debug.selectConfigurationPlaceholder": "Debug Configuration", + "debug.launchJsonConfigurationsCompletionLabel": "Python", + "debug.launchJsonConfigurationsCompletionDescription": "Select a Python debug configuration", + "debug.debugFileConfigurationLabel": "Python File", + "debug.debugFileConfigurationDescription": "Debug the currently active Python file", + "debug.debugModuleConfigurationLabel": "Module", + "debug.debugModuleConfigurationDescription": "Debug a Python module by invoking it with '-m'", + "debug.moduleEnterModuleTitle": "Debug Module", + "debug.moduleEnterModulePrompt": "Enter a Python module/package name", + "debug.moduleEnterModuleDefault": "enter-your-module-name", + "debug.moduleEnterModuleInvalidNameError": "Enter a valid module name", + "debug.remoteAttachConfigurationLabel": "Remote Attach", + "debug.remoteAttachConfigurationDescription": "Attach to a remote debug server", + "debug.attachRemoteHostTitle": "Remote Debugging", + "debug.attachRemoteHostPrompt": "Enter the host name", + "debug.attachRemoteHostValidationError": "Enter a valid host name or IP address", + "debug.attachRemotePortTitle": "Remote Debugging", + "debug.attachRemotePortPrompt": "Enter the port number that the debug server is listening on", + "debug.attachRemotePortValidationError": "Enter a valid port number", + "debug.attachPidConfigurationLabel": "Attach using Process ID", + "debug.attachPidConfigurationDescription": "Attach to a local process", + "debug.debugDjangoConfigurationLabel": "Django", + "debug.debugDjangoConfigurationDescription": "Launch and debug a Django web application", + "debug.djangoEnterManagePyPathTitle": "Debug Django", + "debug.djangoEnterManagePyPathPrompt": "Enter the path to manage.py ('${workspaceFolderToken}' points to the root of the current workspace folder)", + "debug.djangoEnterManagePyPathInvalidFilePathError": "Enter a valid Python file path", + "debug.debugFlaskConfigurationLabel": "Flask", + "debug.debugFlaskConfigurationDescription": "Launch and debug a Flask web application", + "debug.flaskEnterAppPathOrNamePathTitle": "Debug Flask", + "debug.flaskEnterAppPathOrNamePathPrompt": "Enter the path to the application, e.g. 'app.py' or 'app'", + "debug.flaskEnterAppPathOrNamePathInvalidNameError": "Enter a valid name", + "debug.debugPyramidConfigurationLabel": "Pyramid", + "debug.debugPyramidConfigurationDescription": "Web Application", + "debug.pyramidEnterDevelopmentIniPathTitle": "Debug Pyramid", + "debug.pyramidEnterDevelopmentIniPathPrompt": "`Enter the path to development.ini ('${workspaceFolderToken}' points to the root of the current workspace folder)`", + "debug.pyramidEnterDevelopmentIniPathInvalidFilePathError": "Enter a valid file path", + "Testing.testErrorDiagnosticMessage": "Error", + "Testing.testFailDiagnosticMessage": "Fail", + "Testing.testSkippedDiagnosticMessage": "Skipped", + "Testing.configureTests": "Configure Test Framework", + "Testing.disableTests": "Disable Tests", + "Common.openOutputPanel": "Show output", + "LanguageService.lsFailedToStart": "We encountered an issue starting the language server. Reverting to Jedi language engine. Check the Python output panel for details.", + "LanguageService.lsFailedToDownload": "We encountered an issue downloading the language server. Reverting to Jedi language engine. Check the Python output panel for details.", + "LanguageService.lsFailedToExtract": "We encountered an issue extracting the language server. Reverting to Jedi language engine. Check the Python output panel for details.", + "LanguageService.downloadFailedOutputMessage": "Language server download failed", + "LanguageService.extractionFailedOutputMessage": "Language server extraction failed", + "LanguageService.extractionCompletedOutputMessage": "Language server download complete", + "LanguageService.extractionDoneOutputMessage": "done", + "LanguageService.reloadVSCodeIfSeachPathHasChanged": "Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly", + "LanguageService.startingJedi": "Starting Jedi Python language engine.", + "LanguageService.startingMicrosoft": "Starting Microsoft Python language server.", + "LanguageService.startingPylance": "Starting Pylance language server.", + "LanguageService.startingNone": "Editor support is inactive since language server is set to None.", + "LanguageService.reloadAfterLanguageServerChange": "Please reload the window switching between language servers.", + "AttachProcess.unsupportedOS": "Operating system '{0}' not supported.", + "AttachProcess.attachTitle": "Attach to process", + "AttachProcess.selectProcessPlaceholder": "Select the process to attach to", + "AttachProcess.noProcessSelected": "No process selected", + "AttachProcess.refreshList": "Refresh process list", + "DataScience.variableExplorerNameColumn": "Name", + "DataScience.variableExplorerTypeColumn": "Type", + "DataScience.variableExplorerCountColumn": "Size", + "DataScience.variableExplorerValueColumn": "Value", + "DataScience.showDataExplorerTooltip": "Show variable in data viewer.", + "DataScience.dataExplorerInvalidVariableFormat": "'{0}' is not an active variable.", + "DataScience.jupyterGetVariablesExecutionError": "Failure during variable extraction:\r\n{0}", + "DataScience.loadingMessage": "loading ...", + "DataScience.fetchingDataViewer": "Fetching data ...", + "DataScience.noRowsInDataViewer": "No rows match current filter", + "DataScience.jupyterServer": "Jupyter Server", + "DataScience.trustNotebookCommandTitle": "Trust notebook", + "DataScience.notebookIsTrusted": "Trusted", + "DataScience.notebookIsNotTrusted": "Not Trusted", + "DataScience.noKernel": "No Kernel", + "DataScience.serverNotStarted": "Not Started", + "DataScience.localJupyterServer": "local", + "DataScience.pandasTooOldForViewingFormat": "Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data.", + "DataScience.pandasRequiredForViewing": "Python package 'pandas' is required for viewing data.", + "DataScience.valuesColumn": "values", + "DataScience.liveShareInvalid": "One or more guests in the session do not have the Python [extension](https://marketplace.visualstudio.com/itemdetails?itemName=ms-python.python) installed.\r\nYour Live Share session cannot continue and will be closed.", + "diagnostics.updateSettings": "Yes, update settings", + "Common.noIWillDoItLater": "No, I will do it later", + "Common.notNow": "Not now", + "Common.gotIt": "Got it!", + "Interpreters.selectInterpreterTip": "Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar", + "DataScience.noRowsInVariableExplorer": "No variables defined", + "DataScience.tooManyColumnsMessage": "Variables with over a 1000 columns may take a long time to display. Are you sure you wish to continue?", + "DataScience.tooManyColumnsYes": "Yes", + "DataScience.tooManyColumnsNo": "No", + "DataScience.tooManyColumnsDontAskAgain": "Don't Ask Again", + "DataScience.filterRowsButton": "Filter Rows", + "DataScience.filterRowsTooltip": "Allows filtering multiple rows. Use =, >, or < signs to filter numeric values.", + "DataScience.previewHeader": "--- Begin preview of {0} ---", + "DataScience.previewFooter": "--- End preview of {0} ---", + "DataScience.previewStatusMessage": "Generating preview of {0}", + "DataScience.plotViewerTitle": "Plots", + "DataScience.exportPlotTitle": "Save plot image", + "DataScience.pdfFilter": "PDF", + "DataScience.pngFilter": "PNG", + "DataScience.svgFilter": "SVG", + "DataScience.previousPlot": "Previous", + "DataScience.nextPlot": "Next", + "DataScience.panPlot": "Pan", + "DataScience.zoomInPlot": "Zoom in", + "DataScience.zoomOutPlot": "Zoom out", + "DataScience.exportPlot": "Export to different formats", + "DataScience.deletePlot": "Remove", + "DataScience.collapseSingle": "Collapse", + "DataScience.expandSingle": "Expand", + "DataScience.editSection": "Input new cells here.", + "DataScience.restartKernelMessageDontAskAgain": "Yes, and Don't Ask Again", + "DataScience.selectedImageListLabel": "Selected Image", + "DataScience.imageListLabel": "Image", + "DataScience.exportImageFailed": "Error exporting image: {0}", + "downloading.file": "Downloading {0}...", + "downloading.file.progress": "{0}{1} of {2} KB ({3}%)", + "DataScience.jupyterDataRateExceeded": "Cannot view variable because data rate exceeded. Please restart your server with a higher data rate limit. For example, --NotebookApp.iopub_data_rate_limit=10000000000.0", + "DataScience.addCellBelowCommandTitle": "Add cell", + "DataScience.debugCellCommandTitle": "Debug Cell", + "DataScience.debugStepOverCommandTitle": "Step over", + "DataScience.debugContinueCommandTitle": "Continue", + "DataScience.debugStopCommandTitle": "Stop", + "DataScience.runCurrentCellAndAddBelow": "Run current and add cell below", + "DataScience.variableExplorerDisabledDuringDebugging": "Please see the Debug Side Bar's VARIABLES section.", + "DataScience.jupyterDebuggerNotInstalledError": "Pip module {0} is required for debugging cells. You will need to install it to debug cells.", + "DataScience.jupyterDebuggerOutputParseError": "Unable to parse {0} output, please log an issue with https://github.com/microsoft/vscode-python", + "DataScience.jupyterDebuggerPortNotAvailableError": "Port {0} cannot be opened for debugging. Please specify a different port in the remoteDebuggerPort setting.", + "DataScience.jupyterDebuggerPortBlockedError": "Port {0} cannot be connected to for debugging. Please let port {0} through your firewall.", + "DataScience.jupyterDebuggerPortNotAvailableSearchError": "Ports in the range {0}-{1} cannot be found for debugging. Please specify a port in the remoteDebuggerPort setting.", + "DataScience.jupyterDebuggerPortBlockedSearchError": "A port cannot be connected to for debugging. Please let ports {0}-{1} through your firewall.", + "DataScience.jupyterDebuggerInstallNew": "Pip module {0} is required for debugging cells. Install {0} and continue to debug cell?", + "DataScience.jupyterDebuggerInstallNewRunByLine": "Pip module {0} is required for running by line. Install {0} and continue to run by line?", + "DataScience.jupyterDebuggerInstallUpdate": "The version of {0} installed does not support debugging cells. Update {0} to newest version and continue to debug cell?", + "DataScience.jupyterDebuggerInstallUpdateRunByLine": "The version of {0} installed does not support running by line. Update {0} to newest version and continue to run by line?", + "DataScience.jupyterDebuggerInstallYes": "Yes", + "DataScience.jupyterDebuggerInstallNo": "No", + "DataScience.cellStopOnErrorFormatMessage": "{0} cells were canceled due to an error in the previous cell.", + "DataScience.instructionComments": "# To add a new cell, type '{0}'\n# To add a new markdown cell, type '{0} [markdown]'\n", + "DataScience.scrollToCellTitleFormatMessage": "Go to [{0}]", + "DataScience.remoteDebuggerNotSupported": "Debugging while attached to a remote server is not currently supported.", + "DataScience.save": "Save notebook", + "DataScience.invalidNotebookFileError": "{0} is not a valid notebook file. Check the file for correct json.", + "DataScience.nativeEditorTitle": "Notebook Editor", + "DataScience.untitledNotebookFileName": "Untitled", + "DataScience.dirtyNotebookMessage1": "Do you want to save the changes you made to {0}?", + "DataScience.dirtyNotebookMessage2": "Your changes will be lost if you don't save them.", + "DataScience.dirtyNotebookYes": "Save", + "DataScience.dirtyNotebookNo": "Don't Save", + "DataScience.dirtyNotebookCancel": "Cancel", + "DataScience.dirtyNotebookDialogTitle": "Save", + "DataScience.dirtyNotebookDialogFilter": "Jupyter Notebooks", + "DataScience.exportAsPythonFileTooltip": "Convert and save to a python script", + "DataScience.exportAsPythonFileTitle": "Save as Python File", + "DataScience.runCell": "Run cell", + "DataScience.deleteCell": "Delete cell", + "DataScience.moveCellUp": "Move cell up", + "DataScience.moveCellDown": "Move cell down", + "DataScience.moveSelectedCellUp": "Move selected cell up", + "DataScience.insertBelow": "Insert cell below", + "DataScience.insertAbove": "Insert cell above", + "DataScience.addCell": "Add cell", + "DataScience.runAll": "Run all cells", + "DataScience.convertingToPythonFile": "Converting ipynb to python file", + "DataScience.untitledNotebookMessage": "Your changes will be lost if you don't save them.", + "DataScience.untitledNotebookYes": "Save", + "DataScience.untitledNotebookNo": "Cancel", + "DataScience.noInterpreter": "No python selected", + "DataScience.notebookNotFound": "python -m jupyter notebook --version is not running", + "DataScience.findJupyterCommandProgress": "Active interpreter does not support {0}. Searching for the best available interpreter.", + "DataScience.findJupyterCommandProgressCheckInterpreter": "Checking {0}.", + "DataScience.findJupyterCommandProgressSearchCurrentPath": "Searching current path.", + "DataScience.gatherError": "Gather internal error", + "DataScience.gatheredScriptDescription": "# This file was generated by the Gather Extension.\n# It requires version 2020.7.94776 (or newer) of the Python Extension.\n#\n# The intent is that it contains only the code required to produce\n# the same results as the cell originally selected for gathering.\n# Please note that the Python analysis is quite conservative, so if\n# it is unsure whether a line of code is necessary for execution, it\n# will err on the side of including it.\n#\n# Please let us know if you are satisfied with what was gathered here:\n# https://aka.ms/gatherfeedback\n\n", + "DataScience.gatheredNotebookDescriptionInMarkdown": "## Gathered Notebook\nGathered from ```{0}```\n\n| | |\n|---|---|\n|   |This notebook was generated by the Gather Extension. It requires version 2020.7.94776 (or newer) of the Python Extension, please update [here](https://command:python.datascience.latestExtension). The intent is that it contains only the code and cells required to produce the same results as the cell originally selected for gathering. Please note that the Python analysis is quite conservative, so if it is unsure whether a line of code is necessary for execution, it will err on the side of including it.|\n\n**Are you satisfied with the code that was gathered?**\n\n[Yes](https://command:python.datascience.gatherquality?yes) [No](https://command:python.datascience.gatherquality?no)", + "DataScience.savePngTitle": "Save Image", + "DataScience.jupyterSelectURIQuickPickTitle": "Pick how to connect to Jupyter", + "DataScience.jupyterSelectURIQuickPickPlaceholder": "Choose an option", + "DataScience.jupyterSelectURILocalLabel": "Default", + "DataScience.jupyterSelectURILocalDetail": "VS Code will automatically start a server for you on the localhost", + "DataScience.jupyterSelectURINewLabel": "Existing", + "DataScience.jupyterSelectURINewDetail": "Specify the URI of an existing server", + "DataScience.jupyterSelectURIMRUDetail": "Last Connection: {0}", + "DataScience.jupyterSelectURIRunningDetailFormat": "Last activity {0}. {1} existing connections.", + "DataScience.jupyterSelectURINotRunningDetail": "Cannot connect at this time. Status unknown.", + "DataScience.fallbackToUseActiveInterpeterAsKernel": "Couldn't find kernel '{0}' that the notebook was created with. Using the current interpreter.", + "DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel": "Couldn't find kernel '{0}' that the notebook was created with. Registering a new kernel using the current interpreter.", + "DataScience.fallBackToPromptToUseActiveInterpreterOrSelectAKernel": "Couldn't find kernel '{0}' that the notebook was created with.", + "DataScience.selectKernel": "Select a Kernel", + "DataScience.selectDifferentKernel": "Select a different Kernel", + "DataScience.selectDifferentJupyterInterpreter": "Select a different Interpreter", + "DataScience.selectJupyterInterpreter": "Select an Interpreter to start Jupyter", + "products.installingModule": "Installing {0}", + "DataScience.switchingKernelProgress": "Switching kernel to '{0}'", + "DataScience.sessionStartFailedWithKernel": "Failed to start a session for the Kernel '{0}'. \nView Jupyter [log](command:{1}) for further details.", + "DataScience.waitingForJupyterSessionToBeIdle": "Waiting for Jupyter Session to be idle", + "DataScience.gettingListOfKernelsForLocalConnection": "Fetching Kernels", + "DataScience.gettingListOfKernelsForRemoteConnection": "Fetching Kernels", + "DataScience.gettingListOfKernelSpecs": "Fetching Kernel specs", + "DataScience.startingJupyterNotebook": "Starting Jupyter Notebook", + "DataScience.registeringKernel": "Registering Kernel", + "DataScience.trimmedOutput": "Output was trimmed for performance reasons.\nTo see the full output set the setting \"python.dataScience.textOutputLimit\" to 0.", + "DataScience.connectingToJupyterUri": "Connecting to Jupyter server at {0}", + "DataScience.createdNewNotebook": "{0}: Creating new notebook ", + "DataScience.createdNewKernel": "{0}: Kernel started: {1}", + "DataScience.kernelInvalid": "Kernel {0} is not usable. Check the Jupyter output tab for more information.", + "OutdatedDebugger.updateDebuggerMessage": "We noticed you are attaching to ptvsd (Python debugger), which was deprecated on May 1st, 2020. Please switch to [debugpy](https://aka.ms/migrateToDebugpy).", + "DataScience.nativeDependencyFail": "We cannot launch a jupyter server because your OS is not supported. Select an already running server if you wish to continue. {0}", + "DataScience.selectNewServer": "Pick Running Server", + "DataScience.jupyterSelectURIRemoteLabel": "Existing", + "DataScience.jupyterSelectURIQuickPickTitleRemoteOnly": "Pick an already running jupyter server", + "DataScience.jupyterSelectURIRemoteDetail": "Specify the URI of an existing server", + "DataScience.gatherQuality": "Did gather work as desired?", + "DataScience.latestExtension": "Download the latest version of the Python Extension", + "DataScience.loadClassFailedWithNoInternet": "Error loading {0}:{1}. Internet connection required for loading 3rd party widgets.", + "DataScience.useCDNForWidgets": "Widgets require us to download supporting files from a 3rd party website. Click [here](https://aka.ms/PVSCIPyWidgets) for more information.", + "DataScience.loadThirdPartyWidgetScriptsPostEnabled": "Please restart the Kernel when changing the setting 'python.dataScience.widgetScriptSources'.", + "DataScience.enableCDNForWidgetsSetting": "Widgets require us to download supporting files from a 3rd party website. Click here to enable this or click here for more information. (Error loading {0}:{1}).", + "DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork": "Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected.", + "DataScience.unhandledMessage": "Unhandled kernel message from a widget: {0} : {1}", + "DataScience.qgridWidgetScriptVersionCompatibilityWarning": "Unable to load a compatible version of the widget 'qgrid'. Consider downgrading to version 1.1.1.", + "DataScience.kernelStarted": "Started kernel {0}", + "StartPage.getStarted": "Python - Get Started", + "StartPage.pythonExtensionTitle": "Python Extension", + "StartPage.createJupyterNotebook": "Create a Jupyter Notebook", + "StartPage.notebookDescription": "- Run \"\" in the Command Palette (
Shift + Command + P
)
- Explore our
sample notebook
to learn about notebook features", + "StartPage.createAPythonFile": "Create a Python File", + "StartPage.pythonFileDescription": "- Create a
new file
with a .py extension", + "StartPage.openInteractiveWindow": "Use the Interactive Window to develop Python Scripts", + "StartPage.interactiveWindowDesc": "- You can create cells on a Python file by typing \"#%%\"
- Use \"
Shift + Enter
\" to run a cell, the output will be shown in the interactive window", + "StartPage.releaseNotes": "Take a look at our Release Notes to learn more about the latest features.", + "StartPage.tutorialAndDoc": "Explore more features in our Tutorials or check Documentation for tips and troubleshooting.", + "StartPage.dontShowAgain": "Don't show this page again", + "StartPage.helloWorld": "Hello world", + "StartPage.sampleNotebook": "Notebooks intro", + "StartPage.openFolder": "Open a Folder or Workspace", + "StartPage.folderDesc": "- Open a
Folder

- Open a
Workspace
", + "DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter": "{0} requires {1} to be installed.", + "DataScience.runByLine": "Run by line (F10)", + "DataScience.stopRunByLine": "Stop", + "DataScience.couldNotInstallLibrary": "Could not install {0}. If pip is not available, please use the package manager of your choice to manually install this library into your Python environment.", + "DataScience.rawKernelSessionFailed": "Unable to start session for kernel {0}. Select another kernel to launch with.", + "DataScience.rawKernelConnectingSession": "Connecting to kernel.", + "DataScience.reloadCustomEditor": "Please reload VS Code to use the custom editor API", + "DataScience.reloadVSCodeNotebookEditor": "Please reload VS Code to use the Notebook Editor", + "DataScience.step": "Run next line (F10)", + "DataScience.usingPreviewNotebookWithOtherNotebookWarning": "Opening the same file in the Preview Notebook Editor and stable Notebook Editor is not recommended. Doing so could result in data loss or corruption of notebooks.", + "DataScience.previewNotebookOnlySupportedInVSCInsiders": "The Preview Notebook Editor is supported only in the Insiders version of Visual Studio Code.", + "DataScienceNotebookSurveyBanner.bannerMessage": "Can you please take 2 minutes to tell us how the Preview Notebook Editor is working for you?", + "DataScience.unknownServerUri": "Server URI cannot be used. Did you uninstall an extension that provided a Jupyter server connection?", + "DataScienceRendererExtension.installingExtension": "Installing Notebook Renderers extension...", + "DataScienceRendererExtension.installationCompleteMessage": "complete.", + "DataScienceRendererExtension.startingDownloadOutputMessage": "Starting download of Notebook Renderers extension.", + "DataScienceRendererExtension.downloadingMessage": "Downloading Notebook Renderers Extension...", + "DataScienceRendererExtension.downloadCompletedOutputMessage": "Notebook Renderers extension download complete.", + "DataScience.uriProviderDescriptionFormat": "{0} (From {1} extension)", + "DataScience.unknownPackage": "unknown", + "DataScience.interactiveWindowTitle": "Python Interactive", + "DataScience.interactiveWindowTitleFormat": "Python Interactive - {0}", + "DataScience.interactiveWindowModeBannerTitle": "Do you want to open a new Python Interactive window for this file? [More Information](command:workbench.action.openSettings?%5B%22python.dataScience.interactiveWindowMode%22%5D).", + "DataScience.interactiveWindowModeBannerSwitchYes": "Yes", + "DataScience.interactiveWindowModeBannerSwitchAlways": "Always", + "DataScience.interactiveWindowModeBannerSwitchNo": "No" +} diff --git a/package.nls.ko-kr.json b/package.nls.ko-kr.json new file mode 100644 index 000000000000..5309a9b07b81 --- /dev/null +++ b/package.nls.ko-kr.json @@ -0,0 +1,26 @@ +{ + "python.command.python.sortImports.title": "Import문 정렬", + "python.command.python.startREPL.title": "REPL 시작", + "python.command.python.buildWorkspaceSymbols.title": "작업 영역 기호 빌드", + "python.command.python.runtests.title": "모든 단위 테스트 실행", + "python.command.python.debugtests.title": "모든 단위 테스트 디버그", + "python.command.python.execInTerminal.title": "터미널에서 Python 파일 실행", + "python.command.python.setInterpreter.title": "인터프리터 선택", + "python.command.python.refactorExtractVariable.title": "변수 추출", + "python.command.python.refactorExtractMethod.title": "메서드 추출", + "python.command.python.viewTestOutput.title": "단위 테스트 결과 보기", + "python.command.python.selectAndRunTestMethod.title": "단위 테스트 메서드 실행 ...", + "python.command.python.selectAndDebugTestMethod.title": "단위 테스트 메서드 디버그 ...", + "python.command.python.selectAndRunTestFile.title": "단위 테스트 파일 실행 ...", + "python.command.python.runCurrentTestFile.title": "현재 단위 테스트 파일 실행", + "python.command.python.runFailedTests.title": "실패한 단위 테스트 실행", + "python.command.python.execSelectionInTerminal.title": "Python 터미널에서 선택 영역/줄 실행", + "python.command.python.execSelectionInDjangoShell.title": "Django 셸에서 선택 영역/줄 실행", + "python.command.python.goToPythonObject.title": " Python 객체로 이동", + "python.snippet.launch.standard.label": "Python: Current File", + "python.snippet.launch.module.label": "Python: 모듈", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Pyramid 응용 프로그램", + "python.snippet.launch.attach.label": "Python: 연결" +} diff --git a/package.nls.nl.json b/package.nls.nl.json new file mode 100644 index 000000000000..ba4968e78535 --- /dev/null +++ b/package.nls.nl.json @@ -0,0 +1,153 @@ +{ + "python.command.python.sortImports.title": "Import sorteren", + "python.command.python.startREPL.title": "REPL starten", + "python.command.python.createTerminal.title": "Terminal aanmaken", + "python.command.python.buildWorkspaceSymbols.title": "Werkruimte-symbolen aanmaken", + "python.command.python.runtests.title": "Alle unittests uitvoeren", + "python.command.python.debugtests.title": "Alle unittests debuggen", + "python.command.python.execInTerminal.title": "Python-bestand in terminal uitvoeren", + "python.command.python.setInterpreter.title": "Interpreter selecteren", + "python.command.python.refactorExtractVariable.title": "Variabelen selecteren", + "python.command.python.refactorExtractMethod.title": "Methode selecteren", + "python.command.python.viewTestOutput.title": "Unittest-resultaat laten zien", + "python.command.python.selectAndRunTestMethod.title": "Unittest-methode uitvoeren ...", + "python.command.python.selectAndDebugTestMethod.title": "Unittest-methode debuggen ...", + "python.command.python.selectAndRunTestFile.title": "Unittest-bestand uitvoeren ...", + "python.command.python.runCurrentTestFile.title": "Huidige unittest-bestand uitvoeren", + "python.command.python.runFailedTests.title": "Gefaalde unittests uitvoeren", + "python.command.python.discoverTests.title": "Unittests doorzoeken", + "python.command.python.execSelectionInTerminal.title": "Selectie/rij in Python-terminal uitvoeren", + "python.command.python.execSelectionInDjangoShell.title": "Selectie/rij in Django-shell uitvoeren", + "python.command.python.goToPythonObject.title": "Naar Python-object gaan", + "python.command.python.setLinter.title": "Linter selecteren", + "python.command.python.enableLinting.title": "Linting activeren", + "python.command.python.runLinting.title": "Linting uitvoeren", + "python.command.python.datascience.runallcells.title": "Alle cellen uitvoeren", + "python.command.python.datascience.runcurrentcell.title": "Huidige cel uitvoeren", + "python.command.python.datascience.runcurrentcelladvance.title": "Huidige cel uitvoeren en doorgaan", + "python.command.python.datascience.runcell.title": "Cel uitvoeren", + "python.command.python.datascience.selectjupyteruri.title": "Jupyter-server-URI specificeren", + "python.command.python.datascience.importnotebook.title": "Jupyter-notebook importeren", + "python.command.python.datascience.importnotebookonfile.title": "Jupyter-notebook importeren", + "python.command.python.enableSourceMapSupport.title": "Bronkaartondersteuning voor extensie-debugging inschakelen", + "python.command.python.datascience.exportoutputasnotebook.title": "Interactief Python-venster als Jupyter-notebook exporteren", + "python.command.python.datascience.exportfileasnotebook.title": "Huidige Python-bestand als Jupyter-notebook exporteren", + "python.command.python.datascience.exportfileandoutputasnotebook.title": "Huidige Python-bestand exporteren en outputten als Jupyter-notebook", + "python.command.python.datascience.undocells.title": "Laatste interactieve Python-actie ongedaan maken", + "python.command.python.datascience.redocells.title": "Laatste interactieve Python-actie opnieuw uitvoeren", + "python.command.python.datascience.removeallcells.title": "Alle interactieve Python-cellen verwijderen", + "python.command.python.datascience.interruptkernel.title": "IPython-kernel onderbreken", + "python.command.python.datascience.restartkernel.title": "IPython-kernel herstarten", + "python.command.python.datascience.expandallcells.title": "Alle interactieve Python-vensters openen", + "python.command.python.datascience.collapseallcells.title": "Alle interactieve Python-vensters sluiten", + "python.snippet.launch.standard.label": "Python: Huidige bestand", + "python.snippet.launch.module.label": "Python: Module", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Pyramid-applicatie", + "python.snippet.launch.attach.label": "Python: aankoppelen", + "ExtensionSurveyBanner.bannerLabelYes": "Ja, neem nu deel aan het onderzoek", + "ExtensionSurveyBanner.bannerLabelNo": "Nee, bedankt", + "LanguageService.lsFailedToStart": "We zijn een probleem tegengekomen bij het starten van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.", + "LanguageService.lsFailedToDownload": "We zijn een probleem tegengekomen bij het downloaden van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.", + "LanguageService.lsFailedToExtract": "We zijn een probleem tegengekomen bij het uitpakken van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.", + "DataScience.unknownMimeTypeFormat": "Mime type {0} wordt momenteel niet ondersteund.", + "DataScience.historyTitle": "Interactieve Python", + "DataScience.badWebPanelFormatString": "

{0} is geen geldige bestandsnaam

", + "DataScience.sessionDisposed": "Kan code niet uitvoeren, de sessie is gesloten.", + "DataScience.exportDialogTitle": "Exporteren naar Jupyter-notebook", + "DataScience.exportDialogFilter": "Jupyter-notebooks", + "DataScience.exportDialogComplete": "Notebook geschreven naar {0}", + "DataScience.exportDialogFailed": "Niet gelukt om te exporteren naar Jupyter-notebook. {0}", + "DataScience.exportOpenQuestion": "In browser openen", + "DataScience.collapseInputTooltip": "Invoerblok sluiten", + "DataScience.importDialogTitle": "Jupyter-notebook importeren", + "DataScience.importDialogFilter": "Jupyter-notebooks", + "DataScience.notebookCheckForImportYes": "Importeer", + "DataScience.notebookCheckForImportNo": "Later", + "DataScience.notebookCheckForImportDontAskAgain": "Niet opnieuw vragen", + "DataScience.notebookCheckForImportTitle": "Wil je het Jupyter-notebook importeren als Python-code?", + "DataScience.jupyterNbConvertNotSupported": "Notebooks importeren vereist Jupyter-nbconvert geinstalleerd.", + "DataScience.jupyterLaunchTimedOut": "Het is niet gelukt de Jupyter-notebook-server op tijd te starten", + "DataScience.jupyterLaunchNoURL": "Het is niet gelukt de URL van de gestarte Jupyter-notebook-server te vinden", + "DataScience.pythonInteractiveHelpLink": "Krijg meer hulp", + "DataScience.importingFormat": "Aan het importeren {0}", + "DataScience.startingJupyter": "Jupyter-server starten", + "DataScience.connectingToJupyter": "Met Jupyter-server verbinden", + "Interpreters.RefreshingInterpreters": "Python-Interpreters verversen", + "Interpreters.LoadingInterpreters": "Python-Interpreters laden", + "DataScience.restartKernelMessage": "Wil je de IPython-kernel herstarten? Alle variabelen zullen verloren gaan.", + "DataScience.restartKernelMessageYes": "Herstarten", + "DataScience.restartKernelMessageNo": "Annuleren", + "DataScienceSurveyBanner.bannerMessage": "Zou je alsjeblieft 2 minuten kunnen nemen om ons te vertellen hoe de Python-data-science-functionaliteiten voor jou werkt?", + "DataScienceSurveyBanner.bannerLabelYes": "Ja, neem nu deel aan het onderzoek", + "DataScienceSurveyBanner.bannerLabelNo": "Nee, bedankt", + "DataScience.restartingKernelStatus": "IPython-Kernel herstarten", + "DataScience.executingCode": "Cel aan het uitvoeren", + "DataScience.collapseAll": "Alle cel-invoeren sluiten", + "DataScience.expandAll": "Alle cel-invoeren openen", + "DataScience.export": "Als Jupyter-Notebook exporteren", + "DataScience.restartServer": "IPython-kernel herstarten", + "DataScience.undo": "Herhaal", + "DataScience.redo": "Opnieuw uitvoeren", + "DataScience.clearAll": "Alle cellen verwijderen", + "DataScience.pythonVersionHeader": "Python versie:", + "DataScience.pythonRestartHeader": "Kernel herstart:", + "Linter.InstalledButNotEnabled": "Linter {0} is geinstalleerd maar niet ingeschakeld.", + "Linter.replaceWithSelectedLinter": "Meerdere linters zijn ingeschakeld in de instellingen. Vervangen met '{0}'?", + "DataScience.jupyterNotebookFailure": "Jupyter-notebook kon niet starten. \r\n{0}", + "DataScience.jupyterNotebookConnectFailed": "Verbinden met Jupiter-notebook is niet gelukt. \r\n{0}\r\n{1}", + "DataScience.notebookVersionFormat": "Jupyter-notebook versie: {0}", + "DataScience.jupyterKernelSpecNotFound": "Kan geen Jupyter-kernel-spec aanmaken en er zijn er geen beschikbaar voor gebruik", + "diagnostics.warnSourceMaps": "Bronkaartondersteuning is ingeschakeld in de Python-extensie, dit zal een ongunstige impact hebben op de uitvoering van de extensie.", + "diagnostics.disableSourceMaps": "Bronkaartondersteuning uitschakelen", + "diagnostics.warnBeforeEnablingSourceMaps": "Bronkaartondersteuning inschakelen in de Python-extensie zal een ongunstige impact hebben op de uitvoering van de extensie.", + "diagnostics.enableSourceMapsAndReloadVSC": "Venster inschakelen en herladen", + "diagnostics.lsNotSupported": "Uw besturingssysteem voldoet niet aan de minimumeisen van de language server. Aan het terugschakelen naar het alternatief, Jedi.", + "DataScience.interruptKernel": "IPython-kernel onderbreken", + "DataScience.exportingFormat": "Aan het exporteren {0}", + "DataScience.exportCancel": "Annuleren", + "Common.canceled": "Geannuleerd", + "DataScience.importChangeDirectoryComment": "{0} De werkmap van de werkruimte root naar de ipynb-bestandslocatie veranderen. Schakel deze toevoeging uit met de instelling DataScience.changeDirOnImportExport", + "DataScience.exportChangeDirectoryComment": "# De map wijzigen naar de VSCode-werktuimte root zodat de relatieve pad-ladingen correct werken. Schakel deze toevoeging uit met de instelling DataScience.changeDirOnImportExport", + "DataScience.interruptKernelStatus": "IPython-kernel onderbreken", + "DataScience.restartKernelAfterInterruptMessage": "Het onderbreken van de kernel duurde te lang. Wil je de kernel in plaats daarvan herstarten? Alle variabelen zullen verloren gaan.", + "DataScience.pythonInterruptFailedHeader": "Toetsenbord-interrupt liet de kernel crashen. Kernel herstart.", + "DataScience.sysInfoURILabel": "Jupyter-server URI: ", + "Common.loadingPythonExtension": "Python-extensie aan het laden...", + "debug.selectConfigurationTitle": "Een debug-configuratie selecteren", + "debug.selectConfigurationPlaceholder": "Debug-configuratie", + "debug.debugFileConfigurationLabel": "Python-bestand", + "debug.debugFileConfigurationDescription": "Python-bestand debuggen", + "debug.debugModuleConfigurationLabel": "Module", + "debug.debugModuleConfigurationDescription": "Python module/package debuggen", + "debug.remoteAttachConfigurationLabel": "Extern aankoppelen", + "debug.remoteAttachConfigurationDescription": "Een externe Python-applicatie debuggen", + "debug.debugDjangoConfigurationLabel": "Django", + "debug.debugDjangoConfigurationDescription": "Web-applicatie", + "debug.debugFlaskConfigurationLabel": "Flask", + "debug.debugFlaskConfigurationDescription": "Web-applicatie", + "debug.debugPyramidConfigurationLabel": "Pyramid", + "debug.debugPyramidConfigurationDescription": "Web-applicatie", + "debug.djangoEnterManagePyPathTitle": "Django debuggen", + "debug.djangoEnterManagePyPathPrompt": "Voer een pad in naar manage.py ('${workspaceFolderToken}' verwijzen naar de root van de huidige werkruimtemap)", + "debug.djangoEnterManagePyPathInvalidFilePathError": "Voer een geldig Python-bestandspad in", + "debug.flaskEnterAppPathOrNamePathTitle": "Flask debuggen", + "debug.flaskEnterAppPathOrNamePathPrompt": "Voer een pad in naar een applicatie, bijvoorbeeld 'app.py' of 'app'", + "debug.flaskEnterAppPathOrNamePathInvalidNameError": "Voer een geldige naam in", + "debug.moduleEnterModuleTitle": "Module debuggen", + "debug.moduleEnterModulePrompt": "Voer Python module/package naam in", + "debug.moduleEnterModuleInvalidNameError": "Voer een geldige naam in", + "debug.pyramidEnterDevelopmentIniPathTitle": "Pyramid debuggen", + "debug.pyramidEnterDevelopmentIniPathPrompt": "`Voer een pad in naar development.ini ('${workspaceFolderToken}' verwijzen naar de root van de huidige werkruimtemap)`", + "debug.pyramidEnterDevelopmentIniPathInvalidFilePathError": "Voer een geldig bestandspad in", + "debug.attachRemotePortTitle": "Extern debuggen", + "debug.attachRemotePortPrompt": "Voer een port-nummer in", + "debug.attachRemotePortValidationError": "Voer een geldig port-nummer in", + "debug.attachRemoteHostTitle": "Extern debuggen", + "debug.attachRemoteHostPrompt": "Voer een hostname of IP-adres in", + "debug.attachRemoteHostValidationError": "Voer een geldige hostname of IP-adres in", + "Testing.testErrorDiagnosticMessage": "Error", + "Testing.testFailDiagnosticMessage": "Mislukt", + "Testing.testSkippedDiagnosticMessage": "Overgeslagen" +} diff --git a/package.nls.pl.json b/package.nls.pl.json new file mode 100644 index 000000000000..0e6c5a0ec3d7 --- /dev/null +++ b/package.nls.pl.json @@ -0,0 +1,46 @@ +{ + "python.command.python.sortImports.title": "Sortuj importy", + "python.command.python.startREPL.title": "Uruchom REPL", + "python.command.python.createTerminal.title": "Otwórz Terminal", + "python.command.python.buildWorkspaceSymbols.title": "Zbuduj symbole dla przestrzeni roboczej", + "python.command.python.runtests.title": "Uruchom wszystkie testy jednostkowe", + "python.command.python.debugtests.title": "Debuguj wszystkie testy jednostkowe", + "python.command.python.execInTerminal.title": "Uruchom plik pythonowy w terminalu", + "python.command.python.setInterpreter.title": "Wybierz wersję interpretera", + "python.command.python.refactorExtractVariable.title": "Wyodrębnij zmienną", + "python.command.python.refactorExtractMethod.title": "Wyodrębnij metodę", + "python.command.python.viewOutput.title": "Pokaż wyniki", + "python.command.python.viewTestOutput.title": "Pokaż wyniki testów jednostkowych", + "python.command.python.selectAndRunTestMethod.title": "Uruchom metodę testów jednostkowych ...", + "python.command.python.selectAndDebugTestMethod.title": "Debuguj metodę testów jednostkowych ...", + "python.command.python.selectAndRunTestFile.title": "Uruchom plik z testami jednostkowymi ...", + "python.command.python.runCurrentTestFile.title": "Uruchom bieżący plik z testami jednostkowymi", + "python.command.python.runFailedTests.title": "Uruchom testy jednostkowe, które się nie powiodły", + "python.command.python.discoverTests.title": "Wyszukaj testy jednostkowe", + "python.command.python.configureTests.title": "Konfiguruj testy jednostkowe", + "python.command.python.execSelectionInTerminal.title": "Uruchom zaznaczony obszar w interpreterze Pythona", + "python.command.python.execSelectionInDjangoShell.title": "Uruchom zaznaczony obszar w powłoce Django", + "python.command.python.goToPythonObject.title": "Idź do obiektu pythonowego", + "python.command.python.setLinter.title": "Wybierz linter", + "python.command.python.enableLinting.title": "Włącz linting", + "python.command.python.runLinting.title": "Uruchom linting", + "python.command.python.datascience.runallcells.title": "Uruchom wszystkie komórki", + "python.command.python.datascience.runcurrentcell.title": "Uruchom bieżącą komórkę", + "python.command.python.datascience.runcurrentcelladvance.title": "Uruchom bieżące komórki i pokaż", + "python.command.python.datascience.execSelectionInteractive.title": "Uruchom zaznaczony obszar w oknie IPythona", + "python.command.python.datascience.runcell.title": "Uruchom komórki", + "python.command.python.datascience.selectjupyteruri.title": "Podaj identyfikator URI serwera Jupyter", + "python.command.python.datascience.importnotebook.title": "Importuj notatnik Jupyter", + "python.command.python.datascience.importnotebookonfile.title": "Importuj notatnik Jupyter", + "python.command.python.enableSourceMapSupport.title": "Włącz obsługę map źródłowych do debugowania rozszerzeń", + "python.command.python.datascience.exportoutputasnotebook.title": "Eksportuj okno IPython jako notatnik Jupyter", + "python.command.python.datascience.exportfileasnotebook.title": "Eksportuj bieżący plik Pythona jako notatnik Jupytera", + "python.command.python.datascience.exportfileandoutputasnotebook.title": "Eksportuj bieżący plik Pythona i jego wyniki jako notatnik Jupytera", + "python.command.python.datascience.undocells.title": "Cofnij ostatnią akcję IPythona", + "python.command.python.datascience.redocells.title": "Ponów ostatnią akcję IPythona", + "python.command.python.datascience.removeallcells.title": "Usuń wszystkie komórki IPythona", + "python.command.python.datascience.interruptkernel.title": "Przerwij IPython Kernel", + "python.command.python.datascience.restartkernel.title": "Restartuj IPython Kernel", + "python.command.python.datascience.expandallcells.title": "Rozwiń wszystkie komórki IPythona", + "python.command.python.datascience.collapseallcells.title": "Zwiń wszystkie komórki IPythona" +} diff --git a/package.nls.pt-br.json b/package.nls.pt-br.json new file mode 100644 index 000000000000..1acc94053ca0 --- /dev/null +++ b/package.nls.pt-br.json @@ -0,0 +1,31 @@ +{ + "python.command.python.sortImports.title": "Ordenar Importações", + "python.command.python.startREPL.title": "Iniciar REPL", + "python.command.python.createTerminal.title": "Criar Terminal", + "python.command.python.buildWorkspaceSymbols.title": "Construir Símbolos da Área de Trabalho", + "python.command.python.runtests.title": "Executar Todos os Testes Unitários", + "python.command.python.debugtests.title": "Depurar Todos os Testes Unitários", + "python.command.python.execInTerminal.title": "Executar Arquivo no Terminal", + "python.command.python.setInterpreter.title": "Selecionar Interpretador", + "python.command.python.refactorExtractVariable.title": "Extrair Variável", + "python.command.python.refactorExtractMethod.title": "Extrair Método", + "python.command.python.viewTestOutput.title": "Exibir Resultados dos Testes Unitários", + "python.command.python.selectAndRunTestMethod.title": "Executar Testes Unitários do Método ...", + "python.command.python.selectAndDebugTestMethod.title": "Depurar Testes Unitários do Método ...", + "python.command.python.selectAndRunTestFile.title": "Executar Arquivo de Testes Unitários ...", + "python.command.python.runCurrentTestFile.title": "Executar o Arquivo de Testes Unitários Atual", + "python.command.python.runFailedTests.title": "Executar Testes Unitários com Falhas", + "python.command.python.discoverTests.title": "Descobrir Testes Unitários", + "python.command.python.execSelectionInTerminal.title": "Executar Seleção/Linha no Terminal", + "python.command.python.execSelectionInDjangoShell.title": "Executar Seleção/Linha no Django Shell", + "python.command.python.goToPythonObject.title": "Ir para Objeto Python", + "python.command.python.setLinter.title": "Selecionar Linter", + "python.command.python.enableLinting.title": "Habilitar Linting", + "python.command.python.runLinting.title": "Executar Linting", + "python.snippet.launch.standard.label": "Python: Arquivo Atual", + "python.snippet.launch.module.label": "Python: Módulo", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Aplicação Pyramid", + "python.snippet.launch.attach.label": "Python: Anexar" +} diff --git a/package.nls.ru.json b/package.nls.ru.json new file mode 100644 index 000000000000..9b6b0a5661da --- /dev/null +++ b/package.nls.ru.json @@ -0,0 +1,41 @@ +{ + "python.command.python.sortImports.title": "Отсортировать Imports", + "python.command.python.startREPL.title": "Открыть REPL", + "python.command.python.buildWorkspaceSymbols.title": "Собрать символы рабочего пространства", + "python.command.python.runtests.title": "Запустить все тесты", + "python.command.python.debugtests.title": "Запустить все тесты под отладчиком", + "python.command.python.execInTerminal.title": "Выполнить файл в консоли", + "python.command.python.setInterpreter.title": "Выбрать интерпретатор", + "python.command.python.refactorExtractVariable.title": "Извлечь в переменную", + "python.command.python.refactorExtractMethod.title": "Извлечь в метод", + "python.command.python.viewTestOutput.title": "Показать вывод теста", + "python.command.python.selectAndRunTestMethod.title": "Запусть тестовый метод...", + "python.command.python.selectAndDebugTestMethod.title": "Отладить тестовый метод...", + "python.command.python.selectAndRunTestFile.title": "Запустить тестовый файл...", + "python.command.python.runCurrentTestFile.title": "Запустить текущий тестовый файл", + "python.command.python.runFailedTests.title": "Запустить непрошедшие тесты", + "python.command.python.discoverTests.title": "Обнаружить тесты", + "python.command.python.execSelectionInTerminal.title": "Выполнить выбранный текст или текущую строку в консоли", + "python.command.python.execSelectionInDjangoShell.title": "Выполнить выбранный текст или текущую строку в оболочке Django", + "python.command.python.goToPythonObject.title": "Перейти к объекту Python", + "python.command.python.setLinter.title": "Выбрать анализатор кода", + "python.command.python.enableLinting.title": "Включить анализатор кода", + "python.command.python.runLinting.title": "Выполнить анализ кода", + "python.snippet.launch.standard.label": "Python: Текущий файл", + "python.snippet.launch.module.label": "Python: Модуль", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Приложение Pyramid", + "python.snippet.launch.attach.label": "Python: Подключить отладчик", + "ExtensionSurveyBanner.bannerLabelYes": "Да, открыть опрос сейчас", + "ExtensionSurveyBanner.bannerLabelNo": "Нет, спасибо", + "ExtensionSurveyBanner.maybeLater": "Может быть, позже", + "ExtensionSurveyBanner.bannerMessage": "Не могли бы вы потратить пару минут на опрос о языковом сервере Pylance?", + "Pylance.proposePylanceMessage": "Попробуйте новый языковый сервер для Python от Microsoft: Pylance! Установите расширение Pylance.", + "Pylance.tryItNow": "Да, хочу", + "Pylance.remindMeLater": "Напомните позже", + "Pylance.installPylanceMessage": "Расширение Pylance не установлено. Нажмите Да чтобы открыть страницу установки Pylance.", + "Pylance.pylanceNotInstalledMessage": "Расширение Pylance не установлено.", + "Pylance.pylanceInstalledReloadPromptMessage": "Расширение Pylance установлено. Перезагрузить окно для его активации?", + "LanguageService.reloadAfterLanguageServerChange": "Пожалуйста, перезагрузите окно после смены типа языкового сервера." +} diff --git a/package.nls.tr.json b/package.nls.tr.json new file mode 100644 index 000000000000..0e648bb38fdf --- /dev/null +++ b/package.nls.tr.json @@ -0,0 +1,33 @@ +{ + "python.command.python.sortImports.title": "Import İfadelerini Sırala", + "python.command.python.startREPL.title": "REPL Başlat", + "python.command.python.createTerminal.title": "Terminal Oluştur", + "python.command.python.buildWorkspaceSymbols.title": "Çalışma Alanındaki Sembolleri Derle", + "python.command.python.runtests.title": "Testleri Çalıştır", + "python.command.python.debugtests.title": "Testleri Debug Et", + "python.command.python.execInTerminal.title": "Terminalde Çalıştır", + "python.command.python.setInterpreter.title": "Bir Interpreter Seçin", + "python.command.python.refactorExtractVariable.title": "Değişken Çıkar", + "python.command.python.refactorExtractMethod.title": "Metot Çıkar", + "python.command.python.viewTestOutput.title": "Test Çıktısını Görüntüle", + "python.command.python.selectAndRunTestMethod.title": "Test Metodu Çalıştır", + "python.command.python.selectAndDebugTestMethod.title": "Test Metodu Debug Et", + "python.command.python.selectAndRunTestFile.title": "Bir Test Dosyası Seç ve Çalıştır", + "python.command.python.runCurrentTestFile.title": "Aktif Test Dosyasını Çalıştır", + "python.command.python.runFailedTests.title": "Başarısız Testleri Çalıştır", + "python.command.python.discoverTests.title": "Testleri Keşfet", + "python.command.python.discoveringTests.title": "Testler Keşfediliyor...", + "python.command.python.execSelectionInTerminal.title": "Seçimi/Satırı Terminalde Çalıştır", + "python.command.python.execSelectionInDjangoShell.title": "Seçimi/Satırı Django Shell'inde Çalıştır", + "python.command.python.goToPythonObject.title": "Python Nesnesine Git", + "python.command.python.setLinter.title": "Bir Linter Seç", + "python.command.python.enableLinting.title": "Linting'i Aktifleştir", + "python.command.python.runLinting.title": "Linter Çalıştır", + "python.snippet.launch.standard.label": "Python: Geçerli Dosya", + "python.snippet.launch.module.label": "Python: Modül", + "python.snippet.launch.module.default": "modül-adını-yazın", + "python.snippet.launch.attach.label": "Python: Remote Attach", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Pyramid Uygulaması" +} diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json new file mode 100644 index 000000000000..aaf5460a3c55 --- /dev/null +++ b/package.nls.zh-cn.json @@ -0,0 +1,31 @@ +{ + "python.command.python.sortImports.title": "排序 import 语句", + "python.command.python.startREPL.title": "启动 REPL", + "python.command.python.createTerminal.title": "创建终端", + "python.command.python.buildWorkspaceSymbols.title": "构建工作区符号", + "python.command.python.runtests.title": "运行所有单元测试", + "python.command.python.debugtests.title": "调试所有单元测试", + "python.command.python.execInTerminal.title": "在终端中运行 Python 文件", + "python.command.python.setInterpreter.title": "选择解析器", + "python.command.python.refactorExtractVariable.title": "提取变量", + "python.command.python.refactorExtractMethod.title": "提取方法", + "python.command.python.viewTestOutput.title": "显示单元测试输出", + "python.command.python.selectAndRunTestMethod.title": "运行单元测试方法...", + "python.command.python.selectAndDebugTestMethod.title": "调试单元测试方法...", + "python.command.python.selectAndRunTestFile.title": "运行单元测试文件...", + "python.command.python.runCurrentTestFile.title": "运行当前单元测试文件", + "python.command.python.runFailedTests.title": "运行失败的单元测试", + "python.command.python.discoverTests.title": "检测单元测试", + "python.command.python.execSelectionInTerminal.title": "在 Python 终端中运行选定内容/行", + "python.command.python.execSelectionInDjangoShell.title": "在 Django Shell 中运行选定内容/行", + "python.command.python.goToPythonObject.title": "转到 Python 对象", + "python.command.python.setLinter.title": "选择 Linter 插件", + "python.command.python.enableLinting.title": "启用 Linting", + "python.command.python.runLinting.title": "运行 Linting", + "python.snippet.launch.standard.label": "Python: 当前文件", + "python.snippet.launch.module.label": "Python: 模块", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Pyramid 应用", + "python.snippet.launch.attach.label": "Python: 附加" +} diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json new file mode 100644 index 000000000000..96eb10983a9a --- /dev/null +++ b/package.nls.zh-tw.json @@ -0,0 +1,159 @@ +{ + "python.command.python.sortImports.title": "排序 Import 語句", + "python.command.python.startREPL.title": "啟動 REPL", + "python.command.python.createTerminal.title": "建立終端機", + "python.command.python.buildWorkspaceSymbols.title": "建構工作區符號", + "python.command.python.runtests.title": "執行所有單元測試", + "python.command.python.debugtests.title": "偵錯所有單元測試", + "python.command.python.execInTerminal.title": "在終端機中執行 Python 檔案", + "python.command.python.setInterpreter.title": "選擇直譯器", + "python.command.python.refactorExtractVariable.title": "提取變數", + "python.command.python.refactorExtractMethod.title": "提取方法", + "python.command.python.viewTestOutput.title": "顯示單元測試輸出", + "python.command.python.selectAndRunTestMethod.title": "執行單元測試方法…", + "python.command.python.selectAndDebugTestMethod.title": "偵錯單元測試方法…", + "python.command.python.selectAndRunTestFile.title": "執行單元測試檔案…", + "python.command.python.runCurrentTestFile.title": "執行目前單元測試檔案", + "python.command.python.runFailedTests.title": "執行失敗的單元測試", + "python.command.python.execSelectionInTerminal.title": "在 Python 終端機中執行選定內容/行", + "python.command.python.execSelectionInDjangoShell.title": "在 Django Shell 中執行選定內容/行", + "python.command.python.goToPythonObject.title": "跳至 Python 物件", + "python.command.python.setLinter.title": "選擇 Linter", + "python.command.python.enableLinting.title": "啟用 Linting", + "python.command.python.runLinting.title": "執行 Linting", + "python.snippet.launch.standard.label": "Python: 目前檔案", + "python.snippet.launch.module.label": "Python: 模組", + "python.snippet.launch.django.label": "Python: Django", + "python.snippet.launch.flask.label": "Python: Flask", + "python.snippet.launch.pyramid.label": "Python: Pyramid 程式", + "python.snippet.launch.attach.label": "Python: 附加", + "python.command.python.discoverTests.title": "探索 Unit 測試項目", + "python.command.python.switchOffInsidersChannel.title": "切換至預設頻道", + "python.command.python.switchToDailyChannel.title": "切換至 Insiders 每日頻道", + "python.command.python.switchToWeeklyChannel.title": "切換至 Insiders 每週頻道", + "python.command.python.viewOutput.title": "顯示輸出", + "python.command.python.datascience.viewJupyterOutput.title": "顯示 Jupyter 輸出", + "python.command.python.viewLanguageServerOutput.title": "顯示語言伺服器輸出", + "python.command.python.discoveringTests.title": "正在探索...", + "python.command.python.stopTests.title": "停止", + "python.command.python.configureTests.title": "設定測試", + "python.command.python.enableSourceMapSupport.title": "啟用供偵錯延伸模組的原始碼映射 (Source Map) 支援", + "python.command.python.analysis.clearCache.title": "清除模組分析快取", + "python.snippet.launch.module.default": "請輸入-模組-名稱", + "python.snippet.launch.attachpid.label": "Python: 使用處理程序 ID 連結", + "LanguageService.lsFailedToStart": "啟動語言伺服器時遇到問題。改回使用替代方案 \"Jedi\"。請檢查 Python 輸出面板以取得更多資訊。", + "LanguageService.lsFailedToDownload": "下載語言伺服器時遇到問題。改回使用替代方案 \"Jedi\"。請檢查 Python 輸出面板以取得更多資訊。", + "LanguageService.lsFailedToExtract": "擷取語言伺服器時遇到問題。改回使用替代方案 \"Jedi\"。請檢查 Python 輸出面板以取得更多資訊。", + "Experiments.inGroup": "使用者屬於 \"{0}\" 實驗性群組", + "Interpreters.RefreshingInterpreters": "正在重新整理 Python 解譯器", + "Interpreters.LoadingInterpreters": "正在載入 Python 解譯器", + "Interpreters.condaInheritEnvMessage": "我們發覺到您在使用 conda 環境。如果你在整合式終端器中使用這個環境時遇到問題,建議您讓 Python 延伸模組變更使用者設定中的 \"terminal.integrated.inheritEnv\" 為 false。", + "Logging.CurrentWorkingDirectory": "cwd:", + "Common.doNotShowAgain": "不再顯示", + "Common.reload": "重新載入", + "Common.moreInfo": "更多資訊", + "OutputChannelNames.languageServer": "Python 語言伺服器", + "OutputChannelNames.python": "Python", + "OutputChannelNames.pythonTest": "Python 測試記錄", + "OutputChannelNames.jupyter": "Jupyter", + "ExtensionSurveyBanner.bannerMessage": "請問您是否可以用兩分鐘的時間,告訴我們 Python 延伸模組在您環境中的運作情況?", + "ExtensionSurveyBanner.bannerLabelYes": "是,現在填寫調查", + "ExtensionSurveyBanner.bannerLabelNo": "不了,謝謝", + "ExtensionSurveyBanner.maybeLater": "等一下", + "ExtensionChannels.installingInsidersMessage": "正在安裝 Insiders... ", + "ExtensionChannels.installingStableMessage": "正在安裝穩定版... ", + "ExtensionChannels.installationCompleteMessage": "完成。", + "ExtensionChannels.downloadingInsidersMessage": "正在下載 Insiders 延伸模組... ", + "ExtensionChannels.yesWeekly": "是,每週", + "ExtensionChannels.yesDaily": "是,每天", + "ExtensionChannels.promptMessage": "我們發覺到您在使用 Visual Studio Code Insiders。請問您是否想使用 Python 延伸模組的 Insiders 組建?", + "ExtensionChannels.reloadToUseInsidersMessage": "請重新載入 Visual Studio Code 以使用 Python 延伸模組的 Insiders 組建。", + "ExtensionChannels.downloadCompletedOutputMessage": "Insiders 組建下載完成。", + "ExtensionChannels.startingDownloadOutputMessage": "開始下載 Insiders 組建。", + "Interpreters.environmentPromptMessage": "We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?", + "InteractiveShiftEnterBanner.bannerMessage": "Would you like to run code in the 'Python Interactive' window (an IPython console) for 'shift-enter'? Select 'No' to continue to run code in the Python Terminal. This can be changed later in settings.", + "InteractiveShiftEnterBanner.bannerLabelYes": "是", + "InteractiveShiftEnterBanner.bannerLabelNo": "否", + "Linter.enableLinter": "啟用 {0}", + "Linter.enablePylint": "您的工作區有 pylintrc 檔案。是否啟用 pylint?", + "Linter.replaceWithSelectedLinter": "設定中啟用了多個 Linter。是否用 '{0}' 取代?", + "Installer.noCondaOrPipInstaller": "選取環境中沒有可用的 Conda 或 Pip 安裝工具。", + "Installer.noPipInstaller": "選取環境中沒有可用的 Pip 安裝工具。", + "Installer.searchForHelp": "搜尋說明", + "diagnostics.warnSourceMaps": "已在 Python 延伸模組中啟用原始碼映射 (Source Map) 支援,這會降低延伸模組效能。", + "diagnostics.disableSourceMaps": "停用原始碼映射 (Source Map) 支援", + "diagnostics.warnBeforeEnablingSourceMaps": "在 Python 延伸模組中啟用原始碼映射 (Source Map) 支援會降低延伸模組效能。", + "diagnostics.enableSourceMapsAndReloadVSC": "啟用並重新載入視窗", + "diagnostics.lsNotSupported": "您的作業系統不符合 Python 語言伺服器的最低需求。改回使用替代自動完成提供者 \"Jedi\"。", + "diagnostics.invalidPythonPathInDebuggerSettings": "開始偵錯前,您需要選取 Python 解譯器。\n\n小提示:按一下狀態列的 \"選擇 Python 解譯器\"。", + "diagnostics.invalidPythonPathInDebuggerLaunch": "偵錯設定檔的 Python 路徑無效。", + "diagnostics.invalidDebuggerTypeDiagnostic": "Your launch.json file needs to be updated to change the \"pythonExperimental\" debug configurations to use the \"python\" debugger type, otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?", + "diagnostics.consoleTypeDiagnostic": "Your launch.json file needs to be updated to change the console type string from \"none\" to \"internalConsole\", otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?", + "diagnostics.justMyCodeDiagnostic": "Configuration \"debugStdLib\" in launch.json is no longer supported. It's recommended to replace it with \"justMyCode\", which is the exact opposite of using \"debugStdLib\". Would you like to automatically update your launch.json file to do that?", + "diagnostics.yesUpdateLaunch": "是,更新 launch.json", + "diagnostics.invalidTestSettings": "Your settings needs to be updated to change the setting \"python.unitTest.\" to \"python.testing.\", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?", + "Common.canceled": "已取消", + "Common.cancel": "取消", + "Common.loadingPythonExtension": "正在載入 Python 延伸模組...", + "debug.selectConfigurationTitle": "選擇偵錯設定檔", + "debug.selectConfigurationPlaceholder": "偵錯設定檔", + "debug.launchJsonConfigurationsCompletionLabel": "Python", + "debug.launchJsonConfigurationsCompletionDescription": "選取 Python 偵錯設定檔", + "debug.debugFileConfigurationLabel": "Python 檔案", + "debug.debugFileConfigurationDescription": "偵錯目前使用中的 Python 檔案", + "debug.debugModuleConfigurationLabel": "模組", + "debug.debugModuleConfigurationDescription": "使用 '-m' 叫用以偵錯 Python 模組", + "debug.moduleEnterModuleTitle": "偵錯模組", + "debug.moduleEnterModulePrompt": "輸入 Python 模組 / 套件名稱", + "debug.moduleEnterModuleDefault": "輸入-模組-名稱", + "debug.moduleEnterModuleInvalidNameError": "請輸入有效的模組名稱", + "debug.remoteAttachConfigurationLabel": "遠端連結", + "debug.remoteAttachConfigurationDescription": "連結到遠端偵錯伺服器", + "debug.attachRemoteHostTitle": "遠端偵錯", + "debug.attachRemoteHostPrompt": "輸入主機名稱", + "debug.attachRemoteHostValidationError": "請輸入有效的主機名稱或 IP 位址", + "debug.attachRemotePortTitle": "遠端偵錯", + "debug.attachRemotePortPrompt": "輸入偵錯伺服器正在監聽的連線埠號。", + "debug.attachRemotePortValidationError": "請輸入有效的連線埠號。", + "debug.attachPidConfigurationLabel": "使用處理程序 ID 連結", + "debug.attachPidConfigurationDescription": "連結至本機處理程序", + "debug.debugDjangoConfigurationLabel": "Django", + "debug.debugDjangoConfigurationDescription": "執行並偵錯 Django 網路應用程式", + "debug.djangoEnterManagePyPathTitle": "偵錯 Django", + "debug.djangoEnterManagePyPathPrompt": "請輸入 manage.py 的路徑 ('${workspaceFolderToken}' 指向目前工作區資料夾的根目錄)", + "debug.djangoEnterManagePyPathInvalidFilePathError": "請輸入有效的 Python 檔案路徑", + "debug.debugFlaskConfigurationLabel": "Flask", + "debug.debugFlaskConfigurationDescription": "執行並偵錯 Flask 網路應用程式", + "debug.flaskEnterAppPathOrNamePathTitle": "偵錯 Flask", + "debug.flaskEnterAppPathOrNamePathPrompt": "請輸入應用程式路徑。例如:'app.py' 或 'app'", + "debug.flaskEnterAppPathOrNamePathInvalidNameError": "請輸入有效名稱", + "debug.debugPyramidConfigurationLabel": "Pyramid", + "debug.debugPyramidConfigurationDescription": "網路應用程式", + "debug.pyramidEnterDevelopmentIniPathTitle": "偵錯 Pyramid", + "debug.pyramidEnterDevelopmentIniPathPrompt": "`請輸入 development.ini 的路徑 ('${workspaceFolderToken}' 指向目前工作區資料夾的根目錄)`", + "debug.pyramidEnterDevelopmentIniPathInvalidFilePathError": "請輸入有效的檔案路徑", + "Testing.testErrorDiagnosticMessage": "錯誤", + "Testing.testFailDiagnosticMessage": "失敗", + "Testing.testSkippedDiagnosticMessage": "略過", + "Testing.configureTests": "設定測試框架", + "Testing.disableTests": "停用測試", + "Common.openOutputPanel": "顯示輸出", + "LanguageService.downloadFailedOutputMessage": "下載語言伺服器失敗", + "LanguageService.extractionFailedOutputMessage": "擷取語言伺服器失敗", + "LanguageService.extractionCompletedOutputMessage": "下載語言伺服器完成", + "LanguageService.extractionDoneOutputMessage": "完成", + "LanguageService.reloadVSCodeIfSeachPathHasChanged": "已為此 Python 解譯器變更搜尋路徑。請重新載入延伸模組以確保 IntelliSense 能夠正常運作", + "AttachProcess.unsupportedOS": "不支援 '{0}' 作業系統。", + "AttachProcess.attachTitle": "連結至處理程序", + "AttachProcess.selectProcessPlaceholder": "選擇要連結的處理程序", + "AttachProcess.noProcessSelected": "沒有選取的處理程序", + "AttachProcess.refreshList": "重新整理處理程序列表", + "diagnostics.updateSettings": "是,更新設定", + "Common.noIWillDoItLater": "否,我稍候再做", + "Common.notNow": "先不要", + "Common.gotIt": "懂了!", + "Interpreters.selectInterpreterTip": "小提示:您能透過按下狀態列中的 Python 版本,變更 Python 延伸模組使用的 Python 解譯器。", + "downloading.file": "正在下載 {0}...", + "downloading.file.progress": "目前 {0}{1},總共 {2} KB ({3}%)", + "products.installingModule": "正在安裝 {0}" +} diff --git a/pvsc.code-workspace b/pvsc.code-workspace new file mode 100644 index 000000000000..45bddaa1b591 --- /dev/null +++ b/pvsc.code-workspace @@ -0,0 +1,53 @@ +{ + "folders": [ + { + "path": ".", + "name": "vscode-python" + }, + { + "path": "pythonFiles" + }, + { + "path": "src/ipywidgets" + }, + { + "path": "../vscode-notebook-renderers", + "name": "vscode-notebook-renderers" + } + ], + "settings": { + "typescript.tsdk": "./node_modules/typescript/lib", + "search.exclude": { + "**/node_modules/**": true, + "**/.vscode test/insider/**": true, + "**/.vscode test/stable/**": true, + "**/.vscode-test/insider/**": true, + "**/.vscode-test/stable/**": true, + "**/out/**": true + } + }, + "launch": { + "configurations": [ + // This configuration allows one to debug multiple extensions at a time. + // The assumption here is that vscode-notebook-renderers is in the same folder as the python extension. + // User is expected to start the compile tasks for both extensions before using this launch config. + { + "type": "extensionHost", + "request": "launch", + "name": "Python + Renderer Extension", + "args": [ + "--enable-proposed-api", + "--extensionDevelopmentPath=${workspaceFolder:vscode-python}", + "--extensionDevelopmentPath=${workspaceFolder:vscode-notebook-renderers}" + ], + "outFiles": [ + "${workspaceFolder:vscode-python}/out/**/*.js", + "!${workspaceFolder:vscode-python}/**/node_modules**/*", + "${workspaceFolder:vscode-notebook-renderers}/out/**/*.js", + "!${workspaceFolder:vscode-notebook-renderers}/**/node_modules**/*" + ] + } + ], + "compounds": [] + } +} diff --git a/pythonFiles/.env b/pythonFiles/.env new file mode 100644 index 000000000000..8ae3557bcd8d --- /dev/null +++ b/pythonFiles/.env @@ -0,0 +1 @@ +PYTHONPATH=./lib/python diff --git a/pythonFiles/.vscode/settings.json b/pythonFiles/.vscode/settings.json new file mode 100644 index 000000000000..0f49d48f2e86 --- /dev/null +++ b/pythonFiles/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "files.exclude": { + "**/__pycache__/**": true, + "**/**/*.pyc": true + }, + "python.formatting.provider": "black" +} diff --git a/pythonFiles/Notebooks intro.ipynb b/pythonFiles/Notebooks intro.ipynb new file mode 100644 index 000000000000..3f28b072ed69 --- /dev/null +++ b/pythonFiles/Notebooks intro.ipynb @@ -0,0 +1,203 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating a new notebook\n", + "\n", + "\n", + "\n", + "1.

Open the command palette with the shortcut: ", + " \n", + " Ctrl/Command\n", + " +\n", + " Shift\n", + " +\n", + " P\n", + "

\n", + "\n", + "2.

Search for the command Create New Blank Jupyter Notebook

\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "# How to get back to the start page\n", + "\n", + "1.

Open the command palette with the shortcut:\n", + " \n", + " Ctrl/Command\n", + " +\n", + " Shift\n", + " +\n", + " P\n", + "

\n", + "\n", + "2.

Search for the command Python: Open Start Page

\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "# Getting started\n", + "\n", + "You are currently viewing what we call our Notebook Editor. It is an interactive document based on Jupyter Notebooks that supports the intermixing of code, outputs and markdown documentation. \n", + "\n", + "This cell is a markdown cell. To edit the text in this cell, simply click on the cell to change it into edit mode.\n", + "\n", + "

The next cell below is a code cell. You can switch a cell between code and markdown by clicking on the code \n", + "\n", + "/markdown \n", + "\n", + " icons or using the keyboard shortcut\n", + " \n", + " M\n", + " \n", + "and\n", + " \n", + " Y\n", + " \n", + "respectively.

\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print('hello world')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*

To execute the code in the cell above, click on the cell to select it and then either press the play \n", + "\n", + "\n", + " button in the cell toolbar, or use the keyboard shortcut\n", + " \n", + " Ctrl/Command\n", + " +\n", + " Enter\n", + " \n", + "

\n", + "* To edit the code, just click in cell and start editing.\n", + "*

To add a new cell below, click the Add Cell icon \n", + "\n", + "\n", + " \n", + "at the bottom left of the cell or enter command mode with the\n", + " \n", + " ESC\n", + " \n", + "Key and then use the keyboard shortcut\n", + " \n", + " B\n", + " \n", + "to create the new cell below.

\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "# Features\n", + "\n", + "**Variable explorer**\n", + "\n", + "

To view all your active variables and their current values in the notebook, click on the variable explorer icon \n", + "\n", + "\n", + "\n", + "in the top toolbar.

\n", + "\n", + "\n", + "\n", + "**Data Viewer**\n", + "\n", + "

To view your data frame in a more visual \"Excel\" like format, open the variable explorer and to the left of any dataframe object, you will see the data viewer icon\n", + "\n", + "\n", + "\n", + "which you can click to open the data viewer.

\n", + "\n", + "\n", + "\n", + "**Convert to Python File**\n", + "\n", + "

To export your notebook to a Python file (.py), click on the Convert to Python script icon \n", + "\n", + "\n", + "\n", + "\n", + "in the top toolbar \n", + "

\n", + "\n", + "\n", + "\n", + "**Plot Viewer**\n", + "\n", + "

If you have a graph (such as matplotlib) in your output, you'll notice if you hover over the graph, the Plot Viewer icon \n", + "\n", + "\n", + "\n", + "will appear in the top left. Click the icon to open up the graph in the Plotviewer which allows you to zoom on your plots and export it in formats such as png and jpeg.

\n", + "\n", + "\n", + "\n", + "**Switching Kernels**\n", + "\n", + "The notebook editor will detect all kernels in your system by default. To change your notebook kernel, click on the kernel status in the top toolbar at the far right. For example, your kernel status may say \"Python 3: Idle\". This will open up the kernel selector where you can choose your desired kernel.\n", + "\n", + "\n", + "\n", + "**Remote Jupyter Server**\n", + "\n", + "

To connect to a remote Jupyter server, open the command prompt and search for the command Specify remote or local Jupyter server for connections. Then select Existing and enter the remote Jupyter server URL. Afterwards, you'll be prompted to reload the window and the Notebook will be opened connected to the remote Jupyter server.

\n", + "\n", + "", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "-Rh3-Vt9Nev9" + }, + "source": [ + "---\n", + "## More Resources\n", + "\n", + "- [Data science tutorial for Visual Studio Code](https://code.visualstudio.com/docs/python/data-science-tutorial)\n", + "- [Jupyter Notebooks in Visual Studio Code documentation](https://code.visualstudio.com/docs/python/jupyter-support)\n" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/pythonFiles/PythonTools/.idea/.name b/pythonFiles/PythonTools/.idea/.name deleted file mode 100644 index b2fab7c1b991..000000000000 --- a/pythonFiles/PythonTools/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -PythonTools \ No newline at end of file diff --git a/pythonFiles/PythonTools/.idea/PythonTools.iml b/pythonFiles/PythonTools/.idea/PythonTools.iml deleted file mode 100644 index 6f63a63ccb63..000000000000 --- a/pythonFiles/PythonTools/.idea/PythonTools.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/pythonFiles/PythonTools/.idea/encodings.xml b/pythonFiles/PythonTools/.idea/encodings.xml deleted file mode 100644 index 97626ba45445..000000000000 --- a/pythonFiles/PythonTools/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/pythonFiles/PythonTools/.idea/misc.xml b/pythonFiles/PythonTools/.idea/misc.xml deleted file mode 100644 index df245c422e07..000000000000 --- a/pythonFiles/PythonTools/.idea/misc.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pythonFiles/PythonTools/.idea/modules.xml b/pythonFiles/PythonTools/.idea/modules.xml deleted file mode 100644 index 589977f75833..000000000000 --- a/pythonFiles/PythonTools/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/pythonFiles/PythonTools/.idea/workspace.xml b/pythonFiles/PythonTools/.idea/workspace.xml deleted file mode 100644 index 6ff1d97cbc07..000000000000 --- a/pythonFiles/PythonTools/.idea/workspace.xml +++ /dev/null @@ -1,594 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1461282320379 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - file://$PROJECT_DIR$/visualstudio_py_launcher.py - 56 - - - file://$PROJECT_DIR$/visualstudio_py_launcher.py - 58 - - - file://$PROJECT_DIR$/visualstudio_py_launcher.py - 87 - - - file://$PROJECT_DIR$/visualstudio_py_launcher.py - 90 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 48 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 51 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 53 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 1536 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 1531 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 2488 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 615 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 608 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 605 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 602 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 599 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 597 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 907 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 902 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 900 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 1530 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 2498 - - - file://$PROJECT_DIR$/visualstudio_py_util.py - 111 - - - file://$PROJECT_DIR$/visualstudio_py_util.py - 115 - - - file://$PROJECT_DIR$/visualstudio_py_debugger.py - 1418 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pythonFiles/PythonTools/visualstudio_py_debugger.py b/pythonFiles/PythonTools/visualstudio_py_debugger.py deleted file mode 100644 index 8f9677e91f21..000000000000 --- a/pythonFiles/PythonTools/visualstudio_py_debugger.py +++ /dev/null @@ -1,2531 +0,0 @@ -# Python Tools for Visual Studio -# Copyright(c) Microsoft Corporation -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the License); you may not use -# this file except in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS -# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY -# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -# MERCHANTABLITY OR NON-INFRINGEMENT. -# -# See the Apache Version 2.0 License for specific language governing -# permissions and limitations under the License. - -from __future__ import with_statement - -__author__ = "Microsoft Corporation " -__version__ = "3.0.0.0" - -# This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) -# attach scenario, it is loaded on the injected debugger attach thread, and if threading module -# hasn't been loaded already, it will assume that the thread on which it is being loaded is the -# main thread. This will cause issues when the thread goes away after attach completes. -_threading = None - -import sys -import ctypes -try: - import thread -except ImportError: - import _thread as thread -import socket -import struct -import weakref -import traceback -import types -import bisect -from os import path -import ntpath -import runpy -import datetime -from codecs import BOM_UTF8 - -try: - # In the local attach scenario, visualstudio_py_util is injected into globals() - # by PyDebugAttach before loading this module, and cannot be imported. - _vspu = visualstudio_py_util -except: - try: - import visualstudio_py_util as _vspu - except ImportError: - import ptvsd.visualstudio_py_util as _vspu - -to_bytes = _vspu.to_bytes -exec_file = _vspu.exec_file -exec_module = _vspu.exec_module -exec_code = _vspu.exec_code -read_bytes = _vspu.read_bytes -read_int = _vspu.read_int -read_string = _vspu.read_string -write_bytes = _vspu.write_bytes -write_int = _vspu.write_int -write_string = _vspu.write_string -safe_repr = _vspu.SafeRepr() - -try: - # In the local attach scenario, visualstudio_py_repl is injected into globals() - # by PyDebugAttach before loading this module, and cannot be imported. - _vspr = visualstudio_py_repl -except: - try: - import visualstudio_py_repl as _vspr - except ImportError: - import ptvsd.visualstudio_py_repl as _vspr - -try: - import stackless -except ImportError: - stackless = None - -try: - xrange -except: - xrange = range - -if sys.platform == 'cli': - import clr - from System.Runtime.CompilerServices import ConditionalWeakTable - IPY_SEEN_MODULES = ConditionalWeakTable[object, object]() - -# Import encodings early to avoid import on the debugger thread, which may cause deadlock -from encodings import utf_8 - -# WARNING: Avoid imports beyond this point, specifically on the debugger thread, as this may cause -# deadlock where the debugger thread performs an import while a user thread has the import lock - -# save start_new_thread so we can call it later, we'll intercept others calls to it. - -debugger_dll_handle = None -DETACHED = True -def thread_creator(func, args, kwargs = {}, *extra_args): - if not isinstance(args, tuple): - # args is not a tuple. This may be because we have become bound to a - # class, which has offset our arguments by one. - if isinstance(kwargs, tuple): - func, args = args, kwargs - kwargs = extra_args[0] if len(extra_args) > 0 else {} - - return _start_new_thread(new_thread_wrapper, (func, args, kwargs)) - -_start_new_thread = thread.start_new_thread -THREADS = {} -THREADS_LOCK = thread.allocate_lock() -MODULES = [] - -BREAK_ON_SYSTEMEXIT_ZERO = False -DEBUG_STDLIB = False -DJANGO_DEBUG = False - -# Py3k compat - alias unicode to str -try: - unicode -except: - unicode = str - -# A value of a synthesized child. The string is passed through to the variable list, and type is not displayed at all. -class SynthesizedValue(object): - def __init__(self, repr_value='', len_value=None): - self.repr_value = repr_value - self.len_value = len_value - def __repr__(self): - return self.repr_value - def __len__(self): - return self.len_value - -# Specifies list of files not to debug. Can be extended by other modules -# (the REPL does this for $attach support and not stepping into the REPL). -DONT_DEBUG = [path.normcase(__file__), path.normcase(_vspu.__file__)] -if sys.version_info >= (3, 3): - DONT_DEBUG.append(path.normcase('')) -if sys.version_info >= (3, 5): - DONT_DEBUG.append(path.normcase('')) - -# Contains information about all breakpoints in the process. Keys are line numbers on which -# there are breakpoints in any file, and values are dicts. For every line number, the -# corresponding dict contains all the breakpoints that fall on that line. The keys in that -# dict are tuples of the form (filename, breakpoint_id), each entry representing a single -# breakpoint, and values are BreakpointInfo objects. -# -# For example, given the following breakpoints: -# -# 1. In 'main.py' at line 10. -# 2. In 'main.py' at line 20. -# 3. In 'module.py' at line 10. -# -# the contents of BREAKPOINTS would be: -# {10: {('main.py', 1): ..., ('module.py', 3): ...}, 20: {('main.py', 2): ... }} -BREAKPOINTS = {} - -# Contains information about all pending (i.e. not yet bound) breakpoints in the process. -# Elements are BreakpointInfo objects. -PENDING_BREAKPOINTS = set() - -# Must be in sync with enum PythonBreakpointConditionKind in PythonBreakpoint.cs -BREAKPOINT_CONDITION_ALWAYS = 0 -BREAKPOINT_CONDITION_WHEN_TRUE = 1 -BREAKPOINT_CONDITION_WHEN_CHANGED = 2 - -# Must be in sync with enum PythonBreakpointPassCountKind in PythonBreakpoint.cs -BREAKPOINT_PASS_COUNT_ALWAYS = 0 -BREAKPOINT_PASS_COUNT_EVERY = 1 -BREAKPOINT_PASS_COUNT_WHEN_EQUAL = 2 -BREAKPOINT_PASS_COUNT_WHEN_EQUAL_OR_GREATER = 3 - -class BreakpointInfo(object): - __slots__ = [ - 'breakpoint_id', 'filename', 'lineno', 'condition_kind', 'condition', - 'pass_count_kind', 'pass_count', 'is_bound', 'last_condition_value', - 'hit_count' - ] - - # For "when changed" breakpoints, this is used as the initial value of last_condition_value, - # such that it is guaranteed to not compare equal to any other value that it will get later. - _DUMMY_LAST_VALUE = object() - - def __init__(self, breakpoint_id, filename, lineno, condition_kind, condition, pass_count_kind, pass_count): - self.breakpoint_id = breakpoint_id - self.filename = filename - self.lineno = lineno - self.condition_kind = condition_kind - self.condition = condition - self.pass_count_kind = pass_count_kind - self.pass_count = pass_count - self.is_bound = False - self.last_condition_value = BreakpointInfo._DUMMY_LAST_VALUE - self.hit_count = 0 - - @staticmethod - def find_by_id(breakpoint_id): - for line, bp_dict in BREAKPOINTS.items(): - for (filename, bp_id), bp in bp_dict.items(): - if bp_id == breakpoint_id: - return bp - return None - -# lock for calling .send on the socket -send_lock = thread.allocate_lock() - -class _SendLockContextManager(object): - """context manager for send lock. Handles both acquiring/releasing the - send lock as well as detaching the debugger if the remote process - is disconnected""" - - def __enter__(self): - # mark that we're about to do socket I/O so we won't deliver - # debug events when we're debugging the standard library - cur_thread = get_thread_from_id(thread.get_ident()) - if cur_thread is not None: - cur_thread.is_sending = True - - send_lock.acquire() - - def __exit__(self, exc_type, exc_value, tb): - send_lock.release() - - # start sending debug events again - cur_thread = get_thread_from_id(thread.get_ident()) - if cur_thread is not None: - cur_thread.is_sending = False - - if exc_type is not None: - detach_threads() - detach_process() - # swallow the exception, we're no longer debugging - return True - -_SendLockCtx = _SendLockContextManager() - -SEND_BREAK_COMPLETE = False - -STEPPING_OUT = -1 # first value, we decrement below this -STEPPING_NONE = 0 -STEPPING_BREAK = 1 -STEPPING_LAUNCH_BREAK = 2 -STEPPING_ATTACH_BREAK = 3 -STEPPING_INTO = 4 -STEPPING_OVER = 5 # last value, we increment past this. - -USER_STEPPING = (STEPPING_OUT, STEPPING_INTO, STEPPING_OVER) - -FRAME_KIND_NONE = 0 -FRAME_KIND_PYTHON = 1 -FRAME_KIND_DJANGO = 2 - -DJANGO_BUILTINS = {'True': True, 'False': False, 'None': None} - -PYTHON_EVALUATION_RESULT_REPR_KIND_NORMAL = 0 # regular repr and hex repr (if applicable) for the evaluation result; length is len(result) -PYTHON_EVALUATION_RESULT_REPR_KIND_RAW = 1 # repr is raw representation of the value - see TYPES_WITH_RAW_REPR; length is len(repr) -PYTHON_EVALUATION_RESULT_REPR_KIND_RAWLEN = 2 # same as above, but only the length is reported, not the actual value - -PYTHON_EVALUATION_RESULT_EXPANDABLE = 1 -PYTHON_EVALUATION_RESULT_METHOD_CALL = 2 -PYTHON_EVALUATION_RESULT_SIDE_EFFECTS = 4 -PYTHON_EVALUATION_RESULT_RAW = 8 -PYTHON_EVALUATION_RESULT_HAS_RAW_REPR = 16 - -# Don't show attributes of these types if they come from the class (assume they are methods). -METHOD_TYPES = ( - types.FunctionType, - types.MethodType, - types.BuiltinFunctionType, - type("".__repr__), # method-wrapper -) - -# repr() for these types can be used as input for eval() to get the original value. -# float is intentionally not included because it is not always round-trippable (e.g inf, nan). -TYPES_WITH_ROUND_TRIPPING_REPR = set((type(None), int, bool, str, unicode)) -if sys.version[0] == '3': - TYPES_WITH_ROUND_TRIPPING_REPR.add(bytes) -else: - TYPES_WITH_ROUND_TRIPPING_REPR.add(long) - -# repr() for these types can be used as input for eval() to get the original value, provided that the same is true for all their elements. -COLLECTION_TYPES_WITH_ROUND_TRIPPING_REPR = set((tuple, list, set, frozenset)) - -# eval(repr(x)), but optimized for common types for which it is known that result == x. -def eval_repr(x): - def is_repr_round_tripping(x): - # Do exact type checks here - subclasses can override __repr__. - if type(x) in TYPES_WITH_ROUND_TRIPPING_REPR: - return True - elif type(x) in COLLECTION_TYPES_WITH_ROUND_TRIPPING_REPR: - # All standard sequence types are round-trippable if their elements are. - return all((is_repr_round_tripping(item) for item in x)) - else: - return False - if is_repr_round_tripping(x): - return x - else: - return eval(repr(x), {}) - -# key is type, value is function producing the raw repr -TYPES_WITH_RAW_REPR = { - unicode: (lambda s: s) -} - -# bytearray is 2.6+ -try: - # getfilesystemencoding is used here because it effectively corresponds to the notion of "locale encoding": - # current ANSI codepage on Windows, LC_CTYPE on Linux, UTF-8 on OS X - which is exactly what we want. - TYPES_WITH_RAW_REPR[bytearray] = lambda b: b.decode(sys.getfilesystemencoding(), 'ignore') -except: - pass - -if sys.version[0] == '3': - TYPES_WITH_RAW_REPR[bytes] = TYPES_WITH_RAW_REPR[bytearray] -else: - TYPES_WITH_RAW_REPR[str] = TYPES_WITH_RAW_REPR[unicode] - -if sys.version[0] == '3': - # work around a crashing bug on CPython 3.x where they take a hard stack overflow - # we'll never see this exception but it'll allow us to keep our try/except handler - # the same across all versions of Python - class StackOverflowException(Exception): pass -else: - StackOverflowException = RuntimeError - -ASBR = to_bytes('ASBR') -SETL = to_bytes('SETL') -THRF = to_bytes('THRF') -DETC = to_bytes('DETC') -NEWT = to_bytes('NEWT') -EXTT = to_bytes('EXTT') -EXIT = to_bytes('EXIT') -EXCP = to_bytes('EXCP') -MODL = to_bytes('MODL') -STPD = to_bytes('STPD') -BRKS = to_bytes('BRKS') -BRKF = to_bytes('BRKF') -BRKH = to_bytes('BRKH') -BRKC = to_bytes('BRKC') -BKHC = to_bytes('BKHC') -LOAD = to_bytes('LOAD') -EXCE = to_bytes('EXCE') -EXCR = to_bytes('EXCR') -CHLD = to_bytes('CHLD') -OUTP = to_bytes('OUTP') -REQH = to_bytes('REQH') -LAST = to_bytes('LAST') - -def get_thread_from_id(id): - THREADS_LOCK.acquire() - try: - return THREADS.get(id) - finally: - THREADS_LOCK.release() - -def should_send_frame(frame): - return (frame is not None and - frame.f_code not in DEBUG_ENTRYPOINTS and - path.normcase(frame.f_code.co_filename) not in DONT_DEBUG) - -KNOWN_DIRECTORIES = set((None, '')) -KNOWN_ZIPS = set() - -def is_file_in_zip(filename): - parent, name = path.split(path.abspath(filename)) - if parent in KNOWN_DIRECTORIES: - return False - elif parent in KNOWN_ZIPS: - return True - elif path.isdir(parent): - KNOWN_DIRECTORIES.add(parent) - return False - else: - KNOWN_ZIPS.add(parent) - return True - -def lookup_builtin(name, frame): - try: - return frame.f_builtins.get(bits) - except: - # http://ironpython.codeplex.com/workitem/30908 - builtins = frame.f_globals['__builtins__'] - if not isinstance(builtins, dict): - builtins = builtins.__dict__ - return builtins.get(name) - -def lookup_local(frame, name): - bits = name.split('.') - obj = frame.f_locals.get(bits[0]) or frame.f_globals.get(bits[0]) or lookup_builtin(bits[0], frame) - bits.pop(0) - while bits and obj is not None and type(obj) is types.ModuleType: - obj = getattr(obj, bits.pop(0), None) - return obj - -if sys.version_info[0] >= 3: - _EXCEPTIONS_MODULE = 'builtins' -else: - _EXCEPTIONS_MODULE = 'exceptions' - -def get_exception_name(exc_type): - if exc_type.__module__ == _EXCEPTIONS_MODULE: - return exc_type.__name__ - else: - return exc_type.__module__ + '.' + exc_type.__name__ - -# These constants come from Visual Studio - enum_EXCEPTION_STATE -BREAK_MODE_NEVER = 0 -BREAK_MODE_ALWAYS = 1 -BREAK_MODE_UNHANDLED = 32 - -BREAK_TYPE_NONE = 0 -BREAK_TYPE_UNHANDLED = 1 -BREAK_TYPE_HANDLED = 2 - -class ExceptionBreakInfo(object): - BUILT_IN_HANDLERS = { - path.normcase(''): ((None, None, '*'),), - path.normcase('build\\bdist.win32\\egg\\pkg_resources.py'): ((None, None, '*'),), - path.normcase('build\\bdist.win-amd64\\egg\\pkg_resources.py'): ((None, None, '*'),), - } - - def __init__(self): - self.default_mode = BREAK_MODE_UNHANDLED - self.break_on = { } - self.handler_cache = dict(self.BUILT_IN_HANDLERS) - self.handler_lock = thread.allocate_lock() - self.add_exception('exceptions.IndexError', BREAK_MODE_NEVER) - self.add_exception('builtins.IndexError', BREAK_MODE_NEVER) - self.add_exception('exceptions.KeyError', BREAK_MODE_NEVER) - self.add_exception('builtins.KeyError', BREAK_MODE_NEVER) - self.add_exception('exceptions.AttributeError', BREAK_MODE_NEVER) - self.add_exception('builtins.AttributeError', BREAK_MODE_NEVER) - self.add_exception('exceptions.StopIteration', BREAK_MODE_NEVER) - self.add_exception('builtins.StopIteration', BREAK_MODE_NEVER) - self.add_exception('exceptions.GeneratorExit', BREAK_MODE_NEVER) - self.add_exception('builtins.GeneratorExit', BREAK_MODE_NEVER) - - def clear(self): - self.default_mode = BREAK_MODE_UNHANDLED - self.break_on.clear() - self.handler_cache = dict(self.BUILT_IN_HANDLERS) - - def should_break(self, thread, ex_type, ex_value, trace): - probe_stack() - name = get_exception_name(ex_type) - mode = self.break_on.get(name, self.default_mode) - break_type = BREAK_TYPE_NONE - if mode & BREAK_MODE_ALWAYS: - if self.is_handled(thread, ex_type, ex_value, trace): - break_type = BREAK_TYPE_HANDLED - else: - break_type = BREAK_TYPE_UNHANDLED - elif (mode & BREAK_MODE_UNHANDLED) and not self.is_handled(thread, ex_type, ex_value, trace): - break_type = BREAK_TYPE_UNHANDLED - - if break_type: - if issubclass(ex_type, SystemExit): - if not BREAK_ON_SYSTEMEXIT_ZERO: - if not ex_value or (isinstance(ex_value, SystemExit) and not ex_value.code): - break_type = BREAK_TYPE_NONE - - return break_type - - def is_handled(self, thread, ex_type, ex_value, trace): - if trace is None: - # get out if we didn't get a traceback - return False - - if trace.tb_next is not None: - if should_send_frame(trace.tb_next.tb_frame) and should_debug_code(trace.tb_next.tb_frame.f_code): - # don't break if this is not the top of the traceback, - # unless the previous frame was not debuggable - return True - - cur_frame = trace.tb_frame - - while should_send_frame(cur_frame) and cur_frame.f_code is not None and cur_frame.f_code.co_filename is not None: - filename = path.normcase(cur_frame.f_code.co_filename) - if is_file_in_zip(filename): - # File is in a zip, so assume it handles exceptions - return True - - if not is_same_py_file(filename, __file__): - handlers = self.handler_cache.get(filename) - - if handlers is None: - # req handlers for this file from the debug engine - self.handler_lock.acquire() - - with _SendLockCtx: - write_bytes(conn, REQH) - write_string(conn, filename) - - # wait for the handler data to be received - self.handler_lock.acquire() - self.handler_lock.release() - - handlers = self.handler_cache.get(filename) - - if handlers is None: - # no code available, so assume unhandled - return False - - line = cur_frame.f_lineno - for line_start, line_end, expressions in handlers: - if line_start is None or line_start <= line < line_end: - if '*' in expressions: - return True - - for text in expressions: - try: - res = lookup_local(cur_frame, text) - if res is not None and issubclass(ex_type, res): - return True - except: - pass - - cur_frame = cur_frame.f_back - - return False - - def add_exception(self, name, mode=BREAK_MODE_UNHANDLED): - if name.startswith(_EXCEPTIONS_MODULE + '.'): - name = name[len(_EXCEPTIONS_MODULE) + 1:] - self.break_on[name] = mode - -BREAK_ON = ExceptionBreakInfo() - -def probe_stack(depth = 10): - """helper to make sure we have enough stack space to proceed w/o corrupting - debugger state.""" - if depth == 0: - return - probe_stack(depth - 1) - -PREFIXES = [path.normcase(sys.prefix)] -# If we're running in a virtual env, DEBUG_STDLIB should respect this too. -if hasattr(sys, 'base_prefix'): - PREFIXES.append(path.normcase(sys.base_prefix)) -if hasattr(sys, 'real_prefix'): - PREFIXES.append(path.normcase(sys.real_prefix)) - -def should_debug_code(code): - if not code or not code.co_filename: - return False - - filename = path.normcase(code.co_filename) - if not DEBUG_STDLIB: - for prefix in PREFIXES: - if prefix != '' and filename.startswith(prefix): - return False - - for dont_debug_file in DONT_DEBUG: - if is_same_py_file(filename, dont_debug_file): - return False - - if is_file_in_zip(filename): - # file in inside an egg or zip, so we can't debug it - return False - - return True - -attach_lock = thread.allocate() -attach_sent_break = False - -local_path_to_vs_path = {} - -def breakpoint_path_match(vs_path, local_path): - vs_path_norm = path.normcase(vs_path) - local_path_norm = path.normcase(local_path) - if local_path_to_vs_path.get(local_path_norm) == vs_path_norm: - return True - - # Walk the local filesystem from local_path up, matching agains win_path component by component, - # and stop when we no longer see an __init__.py. This should give a reasonably close approximation - # of matching the package name. - while True: - local_path, local_name = path.split(local_path) - vs_path, vs_name = ntpath.split(vs_path) - # Match the last component in the path. If one or both components are unavailable, then - # we have reached the root on the corresponding path without successfully matching. - if not local_name or not vs_name or path.normcase(local_name) != path.normcase(vs_name): - return False - # If we have an __init__.py, this module was inside the package, and we still need to match - # thatpackage, so walk up one level and keep matching. Otherwise, we've walked as far as we - # needed to, and matched all names on our way, so this is a match. - if not path.exists(path.join(local_path, '__init__.py')): - break - - local_path_to_vs_path[local_path_norm] = vs_path_norm - return True - -def update_all_thread_stacks(blocking_thread = None, check_is_blocked = True): - THREADS_LOCK.acquire() - all_threads = list(THREADS.values()) - THREADS_LOCK.release() - - for cur_thread in all_threads: - if cur_thread is blocking_thread: - continue - - cur_thread._block_starting_lock.acquire() - if not check_is_blocked or not cur_thread._is_blocked: - # release the lock, we're going to run user code to evaluate the frames - cur_thread._block_starting_lock.release() - - frames = cur_thread.get_frame_list() - - # re-acquire the lock and make sure we're still not blocked. If so send - # the frame list. - cur_thread._block_starting_lock.acquire() - if not check_is_blocked or not cur_thread._is_blocked: - cur_thread.send_frame_list(frames) - - cur_thread._block_starting_lock.release() - -DJANGO_BREAKPOINTS = {} - -class DjangoBreakpointInfo(object): - def __init__(self, filename): - self._line_locations = None - self.filename = filename - self.breakpoints = {} - - def add_breakpoint(self, lineno, brkpt_id): - self.breakpoints[lineno] = brkpt_id - - def remove_breakpoint(self, lineno): - del self.breakpoints[lineno] - - @property - def line_locations(self): - if self._line_locations is None: - # we need to calculate our line number offset information - try: - contents = open(self.filename, 'rb') - except: - # file not available, locked, etc... - pass - else: - with contents: - line_info = [] - file_len = 0 - for line in contents: - line_len = len(line) - if not line_info and line.startswith(BOM_UTF8): - line_len -= len(BOM_UTF8) # Strip the BOM, Django seems to ignore this... - if line.endswith(to_bytes('\r\n')): - line_len -= 1 # Django normalizes newlines to \n - file_len += line_len - line_info.append(file_len) - contents.close() - self._line_locations = line_info - - return self._line_locations - - def get_line_range(self, start, end): - line_locs = self.line_locations - if line_locs is not None: - low_line = bisect.bisect_right(line_locs, start) - hi_line = bisect.bisect_right(line_locs, end) - - return low_line, hi_line - - return (None, None) - - def should_break(self, start, end): - low_line, hi_line = self.get_line_range(start, end) - if low_line is not None and hi_line is not None: - # low_line/hi_line is 0 based, self.breakpoints is 1 based - for i in xrange(low_line+1, hi_line+2): - bkpt_id = self.breakpoints.get(i) - if bkpt_id is not None: - return True, bkpt_id - - return False, 0 - -def get_django_frame_source(frame): - if frame.f_code.co_name == 'render': - self_obj = frame.f_locals.get('self', None) - if self_obj is None: - return None - name = type(self_obj).__name__ - if name in ('Template', 'TextNode'): - return None - source_obj = getattr(self_obj, 'source', None) - if source_obj and hasattr(source_obj, '__len__') and len(source_obj) == 2: - return str(source_obj[0]), source_obj[1] - - token_obj = getattr(self_obj, 'token', None) - if token_obj is None: - return None - template_obj = getattr(frame.f_locals.get('context', None), 'template', None) - if template_obj is None: - return None - template_name = getattr(template_obj, 'origin', None) - position = getattr(token_obj, 'position', None) - if template_name and position: - return str(template_name), position - - - return None - -class ModuleExitFrame(object): - def __init__(self, real_frame): - self.real_frame = real_frame - self.f_lineno = real_frame.f_lineno + 1 - - def __getattr__(self, name): - return getattr(self.real_frame, name) - -class Thread(object): - def __init__(self, id = None): - if id is not None: - self.id = id - else: - self.id = thread.get_ident() - self._events = {'call' : self.handle_call, - 'line' : self.handle_line, - 'return' : self.handle_return, - 'exception' : self.handle_exception, - 'c_call' : self.handle_c_call, - 'c_return' : self.handle_c_return, - 'c_exception' : self.handle_c_exception, - } - self.cur_frame = None - self.stepping = STEPPING_NONE - self.unblock_work = None - self._block_lock = thread.allocate_lock() - self._block_lock.acquire() - self._block_starting_lock = thread.allocate_lock() - self._is_blocked = False - self._is_working = False - self.stopped_on_line = None - self.detach = False - self.trace_func = self.trace_func # replace self.trace_func w/ a bound method so we don't need to re-create these regularly - self.prev_trace_func = None - self.trace_func_stack = [] - self.reported_process_loaded = False - self.django_stepping = None - self.is_sending = False - - # stackless changes - if stackless is not None: - self._stackless_attach() - - if sys.platform == 'cli': - self.frames = [] - - if sys.platform == 'cli': - # workaround an IronPython bug where we're sometimes missing the back frames - # http://ironpython.codeplex.com/workitem/31437 - def push_frame(self, frame): - self.cur_frame = frame - self.frames.append(frame) - - def pop_frame(self): - self.frames.pop() - self.cur_frame = self.frames[-1] - else: - def push_frame(self, frame): - self.cur_frame = frame - - def pop_frame(self): - self.cur_frame = self.cur_frame.f_back - - def _stackless_attach(self): - try: - stackless.tasklet.trace_function - except AttributeError: - # the tasklets need to be traced on a case by case basis - # sys.trace needs to be called within their calling context - def __call__(tsk, *args, **kwargs): - f = tsk.tempval - def new_f(old_f, args, kwargs): - sys.settrace(self.trace_func) - try: - if old_f is not None: - return old_f(*args, **kwargs) - finally: - sys.settrace(None) - - tsk.tempval = new_f - stackless.tasklet.setup(tsk, f, args, kwargs) - return tsk - - def settrace(tsk, tb): - if hasattr(tsk.frame, "f_trace"): - tsk.frame.f_trace = tb - sys.settrace(tb) - - self.__oldstacklesscall__ = stackless.tasklet.__call__ - stackless.tasklet.settrace = settrace - stackless.tasklet.__call__ = __call__ - if sys.platform == 'cli': - self.frames = [] - - if sys.platform == 'cli': - # workaround an IronPython bug where we're sometimes missing the back frames - # http://ironpython.codeplex.com/workitem/31437 - def push_frame(self, frame): - self.cur_frame = frame - self.frames.append(frame) - - def pop_frame(self): - self.frames.pop() - self.cur_frame = self.frames[-1] - else: - def push_frame(self, frame): - self.cur_frame = frame - - def pop_frame(self): - self.cur_frame = self.cur_frame.f_back - - def context_dispatcher(self, old, new): - self.stepping = STEPPING_NONE - # for those tasklets that started before we started tracing - # we need to make sure that the trace is set by patching - # it in the context switch - if old and new: - if hasattr(new.frame, "f_trace") and not new.frame.f_trace: - sys.call_tracing(new.settrace,(self.trace_func,)) - - def _stackless_schedule_cb(self, prev, next): - current = stackless.getcurrent() - if not current: - return - current_tf = current.trace_function - - try: - current.trace_function = None - self.stepping = STEPPING_NONE - - # If the current frame has no trace function, we may need to get it - # from the previous frame, depending on how we ended up in the - # callback. - if current_tf is None: - f_back = current.frame.f_back - if f_back is not None: - current_tf = f_back.f_trace - - if next is not None: - # Assign our trace function to the current stack - f = next.frame - if next is current: - f = f.f_back - while f: - if isinstance(f, types.FrameType): - f.f_trace = self.trace_func - f = f.f_back - next.trace_function = self.trace_func - finally: - current.trace_function = current_tf - - def trace_func(self, frame, event, arg): - # If we're so far into process shutdown that sys is already gone, just stop tracing. - if sys is None: - return None - elif self.is_sending: - # https://pytools.codeplex.com/workitem/1864 - # we're currently doing I/O w/ the socket, we don't want to deliver - # any breakpoints or async breaks because we'll deadlock. Continue - # to return the trace function so all of our frames remain - # balanced. A better way to deal with this might be to do - # sys.settrace(None) when we take the send lock, but that's much - # more difficult because our send context manager is used both - # inside and outside of the trace function, and so is used when - # tracing is enabled and disabled, and so it's very easy to get our - # current frame tracking to be thrown off... - return self.trace_func - - try: - # if should_debug_code(frame.f_code) is not true during attach - # the current frame is None and a pop_frame will cause an exception and - # break the debugger - if self.cur_frame is None: - # happens during attach, we need frame for blocking - self.push_frame(frame) - if self.stepping == STEPPING_BREAK and should_debug_code(frame.f_code): - if self.detach: - if stackless is not None: - stackless.set_schedule_callback(None) - stackless.tasklet.__call__ = self.__oldstacklesscall__ - sys.settrace(None) - return None - - self.async_break() - - return self._events[event](frame, arg) - except (StackOverflowException, KeyboardInterrupt): - # stack overflow, disable tracing - return self.trace_func - - def handle_call(self, frame, arg): - self.push_frame(frame) - - if DJANGO_BREAKPOINTS: - source_obj = get_django_frame_source(frame) - if source_obj is not None: - origin, (start, end) = source_obj - - active_bps = DJANGO_BREAKPOINTS.get(origin.lower()) - should_break = False - if active_bps is not None: - should_break, bkpt_id = active_bps.should_break(start, end) - if should_break: - probe_stack() - update_all_thread_stacks(self) - self.block(lambda: (report_breakpoint_hit(bkpt_id, self.id), mark_all_threads_for_break(skip_thread = self))) - if not should_break and self.django_stepping: - self.django_stepping = None - self.stepping = STEPPING_OVER - self.block_maybe_attach() - - if frame.f_code.co_name == '' and frame.f_code.co_filename not in ['', '']: - probe_stack() - code, module = new_module(frame) - if not DETACHED: - report_module_load(module) - - # see if this module causes new break points to be bound - bound = set() - for pending_bp in PENDING_BREAKPOINTS: - if try_bind_break_point(code.co_filename, module, pending_bp): - bound.add(pending_bp) - PENDING_BREAKPOINTS.difference_update(bound) - - stepping = self.stepping - if stepping is not STEPPING_NONE and should_debug_code(frame.f_code): - if stepping == STEPPING_INTO: - # block when we hit the 1st line, not when we're on the function def - self.stepping = STEPPING_OVER - # empty stopped_on_line so that we will break even if it is - # the same line - self.stopped_on_line = None - elif stepping >= STEPPING_OVER: - self.stepping += 1 - elif stepping <= STEPPING_OUT: - self.stepping -= 1 - - if (sys.platform == 'cli' and - frame.f_code.co_name == '' and - not IPY_SEEN_MODULES.TryGetValue(frame.f_code)[0]): - IPY_SEEN_MODULES.Add(frame.f_code, None) - # work around IronPython bug - http://ironpython.codeplex.com/workitem/30127 - self.handle_line(frame, arg) - - # forward call to previous trace function, if any, saving old trace func for when we return - old_trace_func = self.prev_trace_func - if old_trace_func is not None: - self.trace_func_stack.append(old_trace_func) - self.prev_trace_func = None # clear first incase old_trace_func stack overflows - self.prev_trace_func = old_trace_func(frame, 'call', arg) - - return self.trace_func - - def should_block_on_frame(self, frame): - if not should_debug_code(frame.f_code): - return False - # It is still possible that we're somewhere in standard library code, but that code was invoked by our - # internal debugger machinery (e.g. socket.sendall or text encoding while tee'ing print output to VS). - # We don't want to block on any of that, either, so walk the stack and see if we hit debugger frames - # at some point below the non-debugger ones. - while frame is not None: - # There is usually going to be a debugger frame at the very bottom of the stack - the one that - # invoked user code on this thread when starting debugging. If we reached that, then everything - # above is user code, so we know that we do want to block here. - if frame.f_code in DEBUG_ENTRYPOINTS: - break - # Otherwise, check if it's some other debugger code. - filename = path.normcase(frame.f_code.co_filename) - is_debugger_frame = False - for debugger_file in DONT_DEBUG: - if is_same_py_file(filename, debugger_file): - # If it is, then the frames above it on the stack that we have just walked through - # were for debugger internal purposes, and we do not want to block here. - return False - frame = frame.f_back - return True - - def handle_line(self, frame, arg): - if not DETACHED: - # resolve whether step_complete and/or handling_breakpoints - step_complete = False - handle_breakpoints = True - stepping = self.stepping - if stepping is not STEPPING_NONE: # check for the common case of no stepping first... - if ((stepping == STEPPING_OVER or stepping == STEPPING_INTO) and frame.f_lineno != self.stopped_on_line): - if self.should_block_on_frame(frame): # don't step complete in our own debugger / non-user code - step_complete = True - elif stepping == STEPPING_LAUNCH_BREAK or stepping == STEPPING_ATTACH_BREAK: - # If launching rather than attaching, don't break into initial Python code needed to set things up - if stepping == STEPPING_LAUNCH_BREAK and (not MODULES or not self.should_block_on_frame(frame)): - handle_breakpoints = False - else: - step_complete = True - - # handle breakpoints - hit_bp_id = None - if BREAKPOINTS and handle_breakpoints: - bp = BREAKPOINTS.get(frame.f_lineno) - if bp is not None: - for (filename, bp_id), bp in bp.items(): - if filename != frame.f_code.co_filename: - # When the breakpoint is bound, the filename is updated to match co_filename of - # the module to which it was bound, so only exact matches are considered hits. - if bp.is_bound: - continue - # Otherwise, use relaxed path check that tries to handle differences between - # local and remote filesystems for remote scenarios: - if not breakpoint_path_match(filename, frame.f_code.co_filename): - continue - - # If we got here, filename and line number both match. - - # Check condition to see if we actually hit this breakpoint. - if bp.condition_kind != BREAKPOINT_CONDITION_ALWAYS: - try: - res = eval(bp.condition, frame.f_globals, frame.f_locals) - if bp.condition_kind == BREAKPOINT_CONDITION_WHEN_CHANGED: - last_val = bp.last_condition_value - bp.last_condition_value = res - if last_val == res: - # Condition didn't change, breakpoint not hit. - continue - else: - if not res: - # Condition isn't true, breakpoint not hit. - continue - except: - # If anything goes wrong while evaluating condition, breakpoint is hit. - pass - - # If we got here, then condition matched, and we need to update the hit count - # (even if we don't end up signaling the breakpoint because of pass count). - bp.hit_count += 1 - - # Check the new hit count against pass count. - if bp.pass_count_kind != BREAKPOINT_PASS_COUNT_ALWAYS: - pass_count_kind = bp.pass_count_kind - pass_count = bp.pass_count - hit_count = bp.hit_count - if pass_count_kind == BREAKPOINT_PASS_COUNT_EVERY: - if (hit_count % pass_count) != 0: - continue - elif pass_count_kind == BREAKPOINT_PASS_COUNT_WHEN_EQUAL: - if hit_count != pass_count: - continue - elif pass_count_kind == BREAKPOINT_PASS_COUNT_WHEN_EQUAL_OR_GREATER: - if hit_count < pass_count: - continue - - # If we got here, then condition and pass count both match, so we should notify VS. - hit_bp_id = bp_id - - # There may be other breakpoints for the same file/line, and we need to update - # their hit counts, too, so keep looping. If more than one is hit, it's fine, - # we will just signal the last one. - - if hit_bp_id is not None: - # handle case where both hitting a breakpoint and step complete by reporting the breakpoint - # if the reported breakpoint is a tracepoint, report the step complete if/when the tracepoint is auto-resumed - probe_stack() - update_all_thread_stacks(self) - self.block(lambda: (report_breakpoint_hit(hit_bp_id, self.id), mark_all_threads_for_break(skip_thread = self)), step_complete) - - elif step_complete: - self.block_maybe_attach() - - # forward call to previous trace function, if any, updating trace function appropriately - old_trace_func = self.prev_trace_func - if old_trace_func is not None: - self.prev_trace_func = None # clear first incase old_trace_func stack overflows - self.prev_trace_func = old_trace_func(frame, 'line', arg) - - return self.trace_func - - def handle_return(self, frame, arg): - self.pop_frame() - - if not DETACHED: - stepping = self.stepping - # only update stepping state when this frame is debuggable (matching handle_call) - if stepping is not STEPPING_NONE and should_debug_code(frame.f_code): - if stepping > STEPPING_OVER: - self.stepping -= 1 - elif stepping < STEPPING_OUT: - self.stepping += 1 - elif stepping in USER_STEPPING: - if self.cur_frame is None or frame.f_code.co_name == "" : - # only return to user code modules - if self.should_block_on_frame(frame): - # restore back the module frame for the step out of a module - self.push_frame(ModuleExitFrame(frame)) - self.stepping = STEPPING_NONE - update_all_thread_stacks(self) - self.block(lambda: (report_step_finished(self.id), mark_all_threads_for_break(skip_thread = self))) - self.pop_frame() - elif self.should_block_on_frame(self.cur_frame): - # if we're returning into non-user code then don't block in the - # non-user code, wait until we hit user code again - self.stepping = STEPPING_NONE - update_all_thread_stacks(self) - self.block(lambda: (report_step_finished(self.id), mark_all_threads_for_break(skip_thread = self))) - - # forward call to previous trace function, if any - old_trace_func = self.prev_trace_func - if old_trace_func is not None: - old_trace_func(frame, 'return', arg) - - # restore previous frames trace function if there is one - if self.trace_func_stack: - self.prev_trace_func = self.trace_func_stack.pop() - - def handle_exception(self, frame, arg): - if self.stepping == STEPPING_ATTACH_BREAK: - self.block_maybe_attach() - - if not DETACHED and should_debug_code(frame.f_code): - break_type = BREAK_ON.should_break(self, *arg) - if break_type: - update_all_thread_stacks(self) - self.block(lambda: report_exception(frame, arg, self.id, break_type)) - - # forward call to previous trace function, if any, updating the current trace function - # with a new one if available - old_trace_func = self.prev_trace_func - if old_trace_func is not None: - self.prev_trace_func = old_trace_func(frame, 'exception', arg) - - return self.trace_func - - def handle_c_call(self, frame, arg): - # break points? - pass - - def handle_c_return(self, frame, arg): - # step out of ? - pass - - def handle_c_exception(self, frame, arg): - pass - - def block_maybe_attach(self): - will_block_now = True - if self.stepping == STEPPING_ATTACH_BREAK: - # only one thread should send the attach break in - attach_lock.acquire() - global attach_sent_break - if attach_sent_break: - will_block_now = False - attach_sent_break = True - attach_lock.release() - - probe_stack() - stepping = self.stepping - self.stepping = STEPPING_NONE - def block_cond(): - if will_block_now: - if stepping == STEPPING_OVER or stepping == STEPPING_INTO: - report_step_finished(self.id) - return mark_all_threads_for_break(skip_thread = self) - else: - if not DETACHED: - if stepping == STEPPING_ATTACH_BREAK: - self.reported_process_loaded = True - return report_process_loaded(self.id) - update_all_thread_stacks(self) - self.block(block_cond) - - def async_break(self): - def async_break_send(): - with _SendLockCtx: - sent_break_complete = False - global SEND_BREAK_COMPLETE - if SEND_BREAK_COMPLETE == True or SEND_BREAK_COMPLETE == self.id: - # multiple threads could be sending this... - SEND_BREAK_COMPLETE = False - sent_break_complete = True - write_bytes(conn, ASBR) - write_int(conn, self.id) - - if sent_break_complete: - # if we have threads which have not broken yet capture their frame list and - # send it now. If they block we'll send an updated (and possibly more accurate - if - # there are any thread locals) list of frames. - update_all_thread_stacks(self) - - self.stepping = STEPPING_NONE - self.block(async_break_send) - - def block(self, block_lambda, keep_stopped_on_line = False): - """blocks the current thread until the debugger resumes it""" - assert not self._is_blocked - #assert self.id == thread.get_ident(), 'wrong thread identity' + str(self.id) + ' ' + str(thread.get_ident()) # we should only ever block ourselves - - # send thread frames before we block - self.enum_thread_frames_locally() - - if not keep_stopped_on_line: - self.stopped_on_line = self.cur_frame.f_lineno - - # need to synchronize w/ sending the reason we're blocking - self._block_starting_lock.acquire() - self._is_blocked = True - block_lambda() - self._block_starting_lock.release() - - while not DETACHED: - self._block_lock.acquire() - if self.unblock_work is None: - break - - # the debugger wants us to do something, do it, and then block again - self._is_working = True - self.unblock_work() - self.unblock_work = None - self._is_working = False - - self._block_starting_lock.acquire() - assert self._is_blocked - self._is_blocked = False - self._block_starting_lock.release() - - def unblock(self): - """unblocks the current thread allowing it to continue to run""" - assert self._is_blocked - assert self.id != thread.get_ident() # only someone else should unblock us - - self._block_lock.release() - - def schedule_work(self, work): - self.unblock_work = work - self.unblock() - - def run_on_thread(self, text, cur_frame, execution_id, frame_kind, repr_kind = PYTHON_EVALUATION_RESULT_REPR_KIND_NORMAL): - self._block_starting_lock.acquire() - - if not self._is_blocked: - report_execution_error('', execution_id) - elif not self._is_working: - self.schedule_work(lambda : self.run_locally(text, cur_frame, execution_id, frame_kind, repr_kind)) - else: - report_execution_error('', execution_id) - - self._block_starting_lock.release() - - def run_on_thread_no_report(self, text, cur_frame, frame_kind): - self._block_starting_lock.acquire() - - if not self._is_blocked: - pass - elif not self._is_working: - self.schedule_work(lambda : self.run_locally_no_report(text, cur_frame, frame_kind)) - else: - pass - - self._block_starting_lock.release() - - def enum_child_on_thread(self, text, cur_frame, execution_id, frame_kind): - self._block_starting_lock.acquire() - if not self._is_working and self._is_blocked: - self.schedule_work(lambda : self.enum_child_locally(text, cur_frame, execution_id, frame_kind)) - self._block_starting_lock.release() - else: - self._block_starting_lock.release() - report_children(execution_id, []) - - def get_locals(self, cur_frame, frame_kind): - if frame_kind == FRAME_KIND_DJANGO: - locs = {} - # iterate going forward, so later items replace earlier items - for d in cur_frame.f_locals['context'].dicts: - # hasattr check to defend against someone passing a bad dictionary value - # and us breaking the app. - if hasattr(d, 'keys') and d != DJANGO_BUILTINS: - for key in d.keys(): - locs[key] = d[key] - else: - locs = cur_frame.f_locals - return locs - - def locals_to_fast(self, frame): - try: - ltf = ctypes.pythonapi.PyFrame_LocalsToFast - ltf.argtypes = [ctypes.py_object, ctypes.c_int] - ltf(frame, 1) - except: - pass - - def compile(self, text, cur_frame): - try: - code = compile(text, '', 'eval') - except: - code = compile(text, '', 'exec') - return code - - def run_locally(self, text, cur_frame, execution_id, frame_kind, repr_kind = PYTHON_EVALUATION_RESULT_REPR_KIND_NORMAL): - try: - code = self.compile(text, cur_frame) - res = eval(code, cur_frame.f_globals, self.get_locals(cur_frame, frame_kind)) - self.locals_to_fast(cur_frame) - # Report any updated variable values first - self.enum_thread_frames_locally() - report_execution_result(execution_id, res, repr_kind) - except: - # Report any updated variable values first - self.enum_thread_frames_locally() - report_execution_exception(execution_id, sys.exc_info()) - - def run_locally_no_report(self, text, cur_frame, frame_kind): - code = self.compile(text, cur_frame) - res = eval(code, cur_frame.f_globals, self.get_locals(cur_frame, frame_kind)) - self.locals_to_fast(cur_frame) - sys.displayhook(res) - - def enum_child_locally(self, expr, cur_frame, execution_id, frame_kind): - try: - code = compile(expr, cur_frame.f_code.co_name, 'eval') - res = eval(code, cur_frame.f_globals, self.get_locals(cur_frame, frame_kind)) - - children = [] # [(name, expression, value, flags)] - - # Process attributes. - - cls_dir = set(dir(type(res))) - res_dict = getattr(res, '__dict__', {}) - res_slots = set(getattr(res, '__slots__', ())) - - for attr_name in dir(res): - try: - # Skip special attributes. - if attr_name.startswith('__') and attr_name.endswith('__'): - continue - attr_value = getattr(res, attr_name) - # If it comes from the class and is not shadowed by any instance attribute, filter it out if it looks like a method. - if attr_name in cls_dir and attr_name not in res_dict and attr_name not in res_slots: - if isinstance(attr_value, METHOD_TYPES): - continue - children.append((attr_name, expr + '.' + attr_name, attr_value, 0)) - except: - # Skip this attribute if we can't process it. - pass - - # Process items, if this is a collection. - - try: - if hasattr(res, '__iter__') and iter(res) is res: - # An iterable object that is its own iterator - iterators, generators, enumerate() etc. These can only be iterated once, so - # don't try to iterate them immediately. Instead, provide a child item that will do so when expanded, to give user full control. - children.append(('Results View', 'tuple(' + expr + ')', SynthesizedValue('Expanding the Results View will run the iterator'), PYTHON_EVALUATION_RESULT_METHOD_CALL | PYTHON_EVALUATION_RESULT_SIDE_EFFECTS)) - enum = () - elif isinstance(res, dict) or (hasattr(res, 'items') and hasattr(res, 'has_key')): - # Dictionary-like object. - try: - enum = res.viewitems() - enum_expr = expr + '.viewitems()' - children.append(('viewitems()', enum_expr, SynthesizedValue(), PYTHON_EVALUATION_RESULT_METHOD_CALL)) - except: - enum = res.items() - enum_expr = expr + '.items()' - children.append(('items()', enum_expr, SynthesizedValue(), PYTHON_EVALUATION_RESULT_METHOD_CALL)) - enum_var = '(k, v)' - enum = enumerate(enum) - else: - # Indexable or enumerable object. - enum = enumerate(enumerate(res)) - enum_expr = expr - enum_var = 'v' - except: - enum = () - - for index, (key, item) in enum: - try: - if len(children) > 10000: - # Report at most 10000 items. - children.append(('[...]', None, 'Evaluation halted because sequence has too many items', 0)) - break - - key_repr = safe_repr(key) - - # Some objects are enumerable but not indexable, or repr(key) is not a valid Python expression. For those, we - # cannot use obj[key] to get the item by its key, and have to retrieve it by index from enumerate() instead. - try: - item_by_key = res[eval_repr(key)] - use_index = item is not item_by_key - except: - use_index = True - else: - use_index = False - - item_name = '[' + key_repr + ']' - if use_index: - item_expr = 'next((v for i, %s in enumerate(%s) if i == %s))' % (enum_var, enum_expr, index) - else: - item_expr = expr + item_name - - children.append((item_name, item_expr, item, 0)) - - except: - # Skip this item if we can't process it. - pass - - report_children(execution_id, children) - - except: - report_children(execution_id, []) - - def get_frame_list(self): - frames = [] - cur_frame = self.cur_frame - - while should_send_frame(cur_frame): - # calculate the ending line number - lineno = cur_frame.f_code.co_firstlineno - try: - linetable = cur_frame.f_code.co_lnotab - except: - try: - lineno = cur_frame.f_code.Span.End.Line - except: - lineno = -1 - else: - for line_incr in linetable[1::2]: - if sys.version >= '3': - lineno += line_incr - else: - lineno += ord(line_incr) - - frame_locals = cur_frame.f_locals - var_names = cur_frame.f_code.co_varnames - - source_obj = None - if DJANGO_DEBUG: - source_obj = get_django_frame_source(cur_frame) - if source_obj is not None: - frame_locals = self.get_locals(cur_frame, FRAME_KIND_DJANGO) - var_names = frame_locals - - if source_obj is not None: - process_globals_in_functions = False - elif frame_locals is cur_frame.f_globals: - var_names = frame_locals - process_globals_in_functions = False - else: - process_globals_in_functions = True - - # collect frame locals - vars = [] - treated = set() - self.collect_variables(vars, frame_locals, var_names, treated) - if process_globals_in_functions: - # collect closed over variables used locally (frame_locals not already treated based on var_names) - self.collect_variables(vars, frame_locals, frame_locals, treated) - # collect globals used locally, skipping undefined found in builtins - f_globals = cur_frame.f_globals - if f_globals: # ensure globals to work with (IPy may have None for cur_frame.f_globals for frames within stdlib) - self.collect_variables(vars, f_globals, cur_frame.f_code.co_names, treated, skip_unknown = True) - - frame_info = None - - if source_obj is not None: - origin, (start, end) = source_obj - - filename = str(origin) - bp_info = DJANGO_BREAKPOINTS.get(filename.lower()) - if bp_info is None: - DJANGO_BREAKPOINTS[filename.lower()] = bp_info = DjangoBreakpointInfo(filename) - - low_line, hi_line = bp_info.get_line_range(start, end) - if low_line is not None and hi_line is not None: - frame_kind = FRAME_KIND_DJANGO - frame_info = ( - low_line + 1, - hi_line + 1, - low_line + 1, - cur_frame.f_code.co_name, - str(origin), - 0, - vars, - FRAME_KIND_DJANGO, - get_code_filename(cur_frame.f_code), - cur_frame.f_lineno - ) - - if frame_info is None: - frame_info = ( - cur_frame.f_code.co_firstlineno, - lineno, - cur_frame.f_lineno, - cur_frame.f_code.co_name, - get_code_filename(cur_frame.f_code), - cur_frame.f_code.co_argcount, - vars, - FRAME_KIND_PYTHON, - None, - None - ) - - frames.append(frame_info) - - cur_frame = cur_frame.f_back - - return frames - - def collect_variables(self, vars, objects, names, treated, skip_unknown = False): - for name in names: - if name not in treated: - try: - obj = objects[name] - try: - if sys.version[0] == '2' and type(obj) is types.InstanceType: - type_name = "instance (" + obj.__class__.__name__ + ")" - else: - type_name = type(obj).__name__ - except: - type_name = 'unknown' - except: - if skip_unknown: - continue - obj = SynthesizedValue('', len_value=0) - type_name = 'unknown' - vars.append((name, type(obj), safe_repr(obj), safe_hex_repr(obj), type_name, get_object_len(obj))) - treated.add(name) - - def send_frame_list(self, frames, thread_name = None): - with _SendLockCtx: - write_bytes(conn, THRF) - write_int(conn, self.id) - write_string(conn, thread_name) - - # send the frame count - write_int(conn, len(frames)) - for firstlineno, lineno, curlineno, name, filename, argcount, variables, frameKind, sourceFile, sourceLine in frames: - # send each frame - write_int(conn, firstlineno) - write_int(conn, lineno) - write_int(conn, curlineno) - - write_string(conn, name) - write_string(conn, filename) - write_int(conn, argcount) - - write_int(conn, frameKind) - if frameKind == FRAME_KIND_DJANGO: - write_string(conn, sourceFile) - write_int(conn, sourceLine) - - write_int(conn, len(variables)) - for name, type_obj, safe_repr_obj, hex_repr_obj, type_name, obj_len in variables: - write_string(conn, name) - write_object(conn, type_obj, safe_repr_obj, hex_repr_obj, type_name, obj_len) - - def enum_thread_frames_locally(self): - global _threading - if _threading is None: - import threading - _threading = threading - self.send_frame_list(self.get_frame_list(), getattr(_threading.currentThread(), 'name', 'Python Thread')) - -class Module(object): - """tracks information about a loaded module""" - - CurrentLoadIndex = 0 - - def __init__(self, filename): - # TODO: Module.CurrentLoadIndex thread safety - self.module_id = Module.CurrentLoadIndex - Module.CurrentLoadIndex += 1 - self.filename = filename - -def get_code(func): - return getattr(func, 'func_code', None) or getattr(func, '__code__', None) - -class DebuggerExitException(Exception): pass - -def add_break_point(bp): - cur_bp = BREAKPOINTS.get(bp.lineno) - if cur_bp is None: - cur_bp = BREAKPOINTS[bp.lineno] = dict() - cur_bp[(bp.filename, bp.breakpoint_id)] = bp - -def try_bind_break_point(mod_filename, module, bp): - if module.filename.lower() == path.abspath(bp.filename).lower(): - bp.filename = mod_filename - bp.is_bound = True - add_break_point(bp) - report_breakpoint_bound(bp.breakpoint_id) - return True - return False - -def mark_all_threads_for_break(stepping = STEPPING_BREAK, skip_thread = None): - THREADS_LOCK.acquire() - for thread in THREADS.values(): - if thread is skip_thread: - continue - thread.stepping = stepping - THREADS_LOCK.release() - -class DebuggerLoop(object): - - instance = None - - def __init__(self, conn): - DebuggerLoop.instance = self - self.conn = conn - self.repl_backend = None - self.command_table = { - to_bytes('stpi') : self.command_step_into, - to_bytes('stpo') : self.command_step_out, - to_bytes('stpv') : self.command_step_over, - to_bytes('brkp') : self.command_set_breakpoint, - to_bytes('brkc') : self.command_set_breakpoint_condition, - to_bytes('bkpc') : self.command_set_breakpoint_pass_count, - to_bytes('bkgh') : self.command_get_breakpoint_hit_count, - to_bytes('bksh') : self.command_set_breakpoint_hit_count, - to_bytes('brkr') : self.command_remove_breakpoint, - to_bytes('brka') : self.command_break_all, - to_bytes('resa') : self.command_resume_all, - to_bytes('rest') : self.command_resume_thread, - to_bytes('ares') : self.command_auto_resume, - to_bytes('exec') : self.command_execute_code, - to_bytes('chld') : self.command_enum_children, - to_bytes('setl') : self.command_set_lineno, - to_bytes('detc') : self.command_detach, - to_bytes('clst') : self.command_clear_stepping, - to_bytes('sexi') : self.command_set_exception_info, - to_bytes('sehi') : self.command_set_exception_handler_info, - to_bytes('bkdr') : self.command_remove_django_breakpoint, - to_bytes('bkda') : self.command_add_django_breakpoint, - to_bytes('crep') : self.command_connect_repl, - to_bytes('drep') : self.command_disconnect_repl, - to_bytes('lack') : self.command_last_ack, - } - - def loop(self): - try: - while True: - inp = read_bytes(conn, 4) - cmd = self.command_table.get(inp) - if cmd is not None: - cmd() - else: - if inp: - print ('unknown command', inp) - break - except DebuggerExitException: - pass - except socket.error: - pass - except: - traceback.print_exc() - - def command_step_into(self): - tid = read_int(self.conn) - thread = get_thread_from_id(tid) - if thread is not None: - assert thread._is_blocked - thread.stepping = STEPPING_INTO - self.command_resume_all() - - def command_step_out(self): - tid = read_int(self.conn) - thread = get_thread_from_id(tid) - if thread is not None: - assert thread._is_blocked - thread.stepping = STEPPING_OUT - self.command_resume_all() - - def command_step_over(self): - # set step over - tid = read_int(self.conn) - thread = get_thread_from_id(tid) - if thread is not None: - assert thread._is_blocked - if DJANGO_DEBUG: - source_obj = get_django_frame_source(thread.cur_frame) - if source_obj is not None: - thread.django_stepping = True - self.command_resume_all() - return - - thread.stepping = STEPPING_OVER - self.command_resume_all() - - def command_set_breakpoint(self): - breakpoint_id = read_int(self.conn) - lineno = read_int(self.conn) - filename = read_string(self.conn) - condition_kind = read_int(self.conn) - condition = read_string(self.conn) - pass_count_kind = read_int(self.conn) - pass_count = read_int(self.conn) - bp = BreakpointInfo(breakpoint_id, filename, lineno, condition_kind, condition, pass_count_kind, pass_count) - - for mod_filename, module in MODULES: - if try_bind_break_point(mod_filename, module, bp): - break - else: - # Failed to bind break point (e.g. module is not loaded yet); report as pending. - add_break_point(bp) - PENDING_BREAKPOINTS.add(bp) - report_breakpoint_failed(breakpoint_id) - - def command_set_breakpoint_condition(self): - breakpoint_id = read_int(self.conn) - kind = read_int(self.conn) - condition = read_string(self.conn) - - bp = BreakpointInfo.find_by_id(breakpoint_id) - if bp is not None: - bp.condition_kind = kind - bp.condition = condition - - def command_set_breakpoint_pass_count(self): - breakpoint_id = read_int(self.conn) - kind = read_int(self.conn) - count = read_int(self.conn) - - bp = BreakpointInfo.find_by_id(breakpoint_id) - if bp is not None: - bp.pass_count_kind = kind - bp.pass_count = count - - def command_set_breakpoint_hit_count(self): - breakpoint_id = read_int(self.conn) - count = read_int(self.conn) - - bp = BreakpointInfo.find_by_id(breakpoint_id) - if bp is not None: - bp.hit_count = count - - def command_get_breakpoint_hit_count(self): - req_id = read_int(self.conn) - breakpoint_id = read_int(self.conn) - - bp = BreakpointInfo.find_by_id(breakpoint_id) - count = 0 - if bp is not None: - count = bp.hit_count - - with _SendLockCtx: - write_bytes(conn, BKHC) - write_int(conn, req_id) - write_int(conn, count) - - def command_remove_breakpoint(self): - line_no = read_int(self.conn) - brkpt_id = read_int(self.conn) - cur_bp = BREAKPOINTS.get(line_no) - if cur_bp is not None: - for file, id in cur_bp: - if id == brkpt_id: - del cur_bp[file, id] - if not cur_bp: - del BREAKPOINTS[line_no] - break - - def command_remove_django_breakpoint(self): - line_no = read_int(self.conn) - brkpt_id = read_int(self.conn) - filename = read_string(self.conn) - - bp_info = DJANGO_BREAKPOINTS.get(filename.lower()) - if bp_info is not None: - bp_info.remove_breakpoint(line_no) - - def command_add_django_breakpoint(self): - brkpt_id = read_int(self.conn) - line_no = read_int(self.conn) - filename = read_string(self.conn) - bp_info = DJANGO_BREAKPOINTS.get(filename.lower()) - if bp_info is None: - DJANGO_BREAKPOINTS[filename.lower()] = bp_info = DjangoBreakpointInfo(filename) - - bp_info.add_breakpoint(line_no, brkpt_id) - - def command_connect_repl(self): - port_num = read_int(self.conn) - _start_new_thread(self.connect_to_repl_backend, (port_num,)) - - def connect_to_repl_backend(self, port_num): - DONT_DEBUG.append(path.normcase(_vspr.__file__)) - self.repl_backend = _vspr.DebugReplBackend(self) - self.repl_backend.connect_from_debugger(port_num) - self.repl_backend.execution_loop() - - def connect_to_repl_backend_using_socket(self, sock): - DONT_DEBUG.append(path.normcase(_vspr.__file__)) - self.repl_backend = _vspr.DebugReplBackend(self) - self.repl_backend.connect_from_debugger_using_socket(sock) - self.repl_backend.execution_loop() - - def command_disconnect_repl(self): - if self.repl_backend is not None: - self.repl_backend.disconnect_from_debugger() - self.repl_backend = None - - def command_break_all(self): - global SEND_BREAK_COMPLETE - SEND_BREAK_COMPLETE = True - mark_all_threads_for_break() - - def command_resume_all(self): - # resume all - THREADS_LOCK.acquire() - all_threads = list(THREADS.values()) - THREADS_LOCK.release() - for thread in all_threads: - thread._block_starting_lock.acquire() - if thread.stepping == STEPPING_BREAK or thread.stepping == STEPPING_ATTACH_BREAK: - thread.stepping = STEPPING_NONE - if thread._is_blocked: - thread.unblock() - thread._block_starting_lock.release() - - def command_resume_thread(self): - tid = read_int(self.conn) - THREADS_LOCK.acquire() - thread = THREADS[tid] - THREADS_LOCK.release() - - if thread.reported_process_loaded: - thread.reported_process_loaded = False - self.command_resume_all() - else: - thread.unblock() - - def command_auto_resume(self): - tid = read_int(self.conn) - THREADS_LOCK.acquire() - thread = THREADS[tid] - THREADS_LOCK.release() - - stepping = thread.stepping - if ((stepping == STEPPING_OVER or stepping == STEPPING_INTO) and thread.cur_frame.f_lineno != thread.stopped_on_line): - report_step_finished(tid) - else: - self.command_resume_all() - - def command_set_exception_info(self): - BREAK_ON.clear() - BREAK_ON.default_mode = read_int(self.conn) - - break_on_count = read_int(self.conn) - for i in xrange(break_on_count): - mode = read_int(self.conn) - name = read_string(self.conn) - BREAK_ON.add_exception(name, mode) - - def command_set_exception_handler_info(self): - try: - filename = read_string(self.conn) - - statement_count = read_int(self.conn) - handlers = [] - for _ in xrange(statement_count): - line_start, line_end = read_int(self.conn), read_int(self.conn) - - expressions = set() - text = read_string(self.conn).strip() - while text != '-': - expressions.add(text) - text = read_string(self.conn) - - if not expressions: - expressions = set('*') - - handlers.append((line_start, line_end, expressions)) - - BREAK_ON.handler_cache[filename] = handlers - finally: - BREAK_ON.handler_lock.release() - - def command_clear_stepping(self): - tid = read_int(self.conn) - - thread = get_thread_from_id(tid) - if thread is not None: - thread.stepping = STEPPING_NONE - - def command_set_lineno(self): - tid = read_int(self.conn) - fid = read_int(self.conn) - lineno = read_int(self.conn) - try: - THREADS_LOCK.acquire() - THREADS[tid].cur_frame.f_lineno = lineno - newline = THREADS[tid].cur_frame.f_lineno - THREADS_LOCK.release() - with _SendLockCtx: - write_bytes(self.conn, SETL) - write_int(self.conn, 1) - write_int(self.conn, tid) - write_int(self.conn, newline) - except: - with _SendLockCtx: - write_bytes(self.conn, SETL) - write_int(self.conn, 0) - write_int(self.conn, tid) - write_int(self.conn, 0) - - def command_execute_code(self): - # execute given text in specified frame - text = read_string(self.conn) - tid = read_int(self.conn) # thread id - fid = read_int(self.conn) # frame id - eid = read_int(self.conn) # execution id - frame_kind = read_int(self.conn) - repr_kind = read_int(self.conn) - - thread, cur_frame = self.get_thread_and_frame(tid, fid, frame_kind) - if thread is not None and cur_frame is not None: - thread.run_on_thread(text, cur_frame, eid, frame_kind, repr_kind) - - def execute_code_no_report(self, text, tid, fid, frame_kind): - # execute given text in specified frame, without sending back the results - thread, cur_frame = self.get_thread_and_frame(tid, fid, frame_kind) - if thread is not None and cur_frame is not None: - thread.run_locally_no_report(text, cur_frame, frame_kind) - - def command_enum_children(self): - # execute given text in specified frame - text = read_string(self.conn) - tid = read_int(self.conn) # thread id - fid = read_int(self.conn) # frame id - eid = read_int(self.conn) # execution id - frame_kind = read_int(self.conn) # frame kind - - thread, cur_frame = self.get_thread_and_frame(tid, fid, frame_kind) - if thread is not None and cur_frame is not None: - thread.enum_child_on_thread(text, cur_frame, eid, frame_kind) - - def get_thread_and_frame(self, tid, fid, frame_kind): - thread = get_thread_from_id(tid) - cur_frame = None - - if thread is not None: - cur_frame = thread.cur_frame - for i in xrange(fid): - cur_frame = cur_frame.f_back - - return thread, cur_frame - - def command_detach(self): - detach_threads() - - # unload debugger DLL - global debugger_dll_handle - if debugger_dll_handle is not None: - k32 = ctypes.WinDLL('kernel32') - k32.FreeLibrary.argtypes = [ctypes.c_void_p] - k32.FreeLibrary(debugger_dll_handle) - debugger_dll_handle = None - - with _SendLockCtx: - write_bytes(conn, DETC) - detach_process() - - for callback in DETACH_CALLBACKS: - callback() - - raise DebuggerExitException() - - def command_last_ack(self): - last_ack_event.set() - -DETACH_CALLBACKS = [] - -def new_thread_wrapper(func, posargs, kwargs): - cur_thread = new_thread() - try: - sys.settrace(cur_thread.trace_func) - func(*posargs, **kwargs) - finally: - THREADS_LOCK.acquire() - if not cur_thread.detach: - del THREADS[cur_thread.id] - THREADS_LOCK.release() - - if not DETACHED: - report_thread_exit(cur_thread) - -def report_new_thread(new_thread): - ident = new_thread.id - with _SendLockCtx: - write_bytes(conn, NEWT) - write_int(conn, ident) - -def report_all_threads(): - THREADS_LOCK.acquire() - all_threads = list(THREADS.values()) - THREADS_LOCK.release() - for cur_thread in all_threads: - report_new_thread(cur_thread) - -def report_thread_exit(old_thread): - ident = old_thread.id - with _SendLockCtx: - write_bytes(conn, EXTT) - write_int(conn, ident) - -def report_exception(frame, exc_info, tid, break_type): - exc_type = exc_info[0] - exc_name = get_exception_name(exc_type) - exc_value = exc_info[1] - tb_value = exc_info[2] - - if type(exc_value) is tuple: - # exception object hasn't been created yet, create it now - # so we can get the correct msg. - exc_value = exc_type(*exc_value) - - excp_text = str(exc_value) - - with _SendLockCtx: - write_bytes(conn, EXCP) - write_string(conn, exc_name) - write_int(conn, tid) - write_int(conn, break_type) - write_string(conn, excp_text) - -def new_module(frame): - mod = Module(get_code_filename(frame.f_code)) - MODULES.append((frame.f_code.co_filename, mod)) - - return frame.f_code, mod - -def report_module_load(mod): - with _SendLockCtx: - write_bytes(conn, MODL) - write_int(conn, mod.module_id) - write_string(conn, mod.filename) - -def report_step_finished(tid): - with _SendLockCtx: - write_bytes(conn, STPD) - write_int(conn, tid) - -def report_breakpoint_bound(id): - with _SendLockCtx: - write_bytes(conn, BRKS) - write_int(conn, id) - -def report_breakpoint_failed(id): - with _SendLockCtx: - write_bytes(conn, BRKF) - write_int(conn, id) - -def report_breakpoint_hit(id, tid): - with _SendLockCtx: - write_bytes(conn, BRKH) - write_int(conn, id) - write_int(conn, tid) - -def report_process_loaded(tid): - with _SendLockCtx: - write_bytes(conn, LOAD) - write_int(conn, tid) - -def report_execution_error(exc_text, execution_id): - with _SendLockCtx: - write_bytes(conn, EXCE) - write_int(conn, execution_id) - write_string(conn, exc_text) - -def report_execution_exception(execution_id, exc_info): - try: - exc_text = str(exc_info[1]) - except: - exc_text = 'An exception was thrown' - - report_execution_error(exc_text, execution_id) - -def safe_hex_repr(obj): - try: - return hex(obj) - except: - return None - -def get_object_len(obj): - try: - return len(obj) - except: - return None - -def report_execution_result(execution_id, result, repr_kind = PYTHON_EVALUATION_RESULT_REPR_KIND_NORMAL): - if repr_kind == PYTHON_EVALUATION_RESULT_REPR_KIND_NORMAL: - flags = 0 - obj_repr = safe_repr(result) - obj_len = get_object_len(result) - hex_repr = safe_hex_repr(result) - else: - flags = PYTHON_EVALUATION_RESULT_RAW - hex_repr = None - for cls, raw_repr in TYPES_WITH_RAW_REPR.items(): - if isinstance(result, cls): - try: - obj_repr = raw_repr(result) - except: - obj_repr = None - break - obj_len = get_object_len(obj_repr) - if repr_kind == PYTHON_EVALUATION_RESULT_REPR_KIND_RAWLEN: - obj_repr = None - - res_type = type(result) - type_name = type(result).__name__ - - with _SendLockCtx: - write_bytes(conn, EXCR) - write_int(conn, execution_id) - write_object(conn, res_type, obj_repr, hex_repr, type_name, obj_len, flags) - -def report_children(execution_id, children): - children = [(name, expression, flags, safe_repr(result), safe_hex_repr(result), type(result), type(result).__name__, get_object_len(result)) for name, expression, result, flags in children] - with _SendLockCtx: - write_bytes(conn, CHLD) - write_int(conn, execution_id) - write_int(conn, len(children)) - for name, expression, flags, obj_repr, hex_repr, res_type, type_name, obj_len in children: - write_string(conn, name) - write_string(conn, expression) - write_object(conn, res_type, obj_repr, hex_repr, type_name, obj_len, flags) - -def get_code_filename(code): - return path.abspath(code.co_filename) - -NONEXPANDABLE_TYPES = [int, str, bool, float, object, type(None), unicode] -try: - NONEXPANDABLE_TYPES.append(long) -except NameError: pass - -def write_object(conn, obj_type, obj_repr, hex_repr, type_name, obj_len, flags = 0): - write_string(conn, obj_repr) - write_string(conn, hex_repr) - if obj_type is SynthesizedValue: - write_string(conn, '') - else: - write_string(conn, type_name) - if obj_type not in NONEXPANDABLE_TYPES and obj_len != 0: - flags |= PYTHON_EVALUATION_RESULT_EXPANDABLE - try: - for cls in TYPES_WITH_RAW_REPR: - if issubclass(obj_type, cls): - flags |= PYTHON_EVALUATION_RESULT_HAS_RAW_REPR - break - except: # guard against broken issubclass for types which aren't actually types, like vtkclass - pass - write_int(conn, obj_len or 0) - write_int(conn, flags) - -debugger_thread_id = -1 -_INTERCEPTING_FOR_ATTACH = False - -def intercept_threads(for_attach = False): - thread.start_new_thread = thread_creator - thread.start_new = thread_creator - - # If threading has already been imported (i.e. we're attaching), we must hot-patch threading._start_new_thread - # so that new threads started using it will be intercepted by our code. - # - # On the other hand, if threading has not been imported, we must not import it ourselves, because it will then - # treat the current thread as the main thread, which is incorrect when attaching because this code is executing - # on an ephemeral debugger attach thread that will go away shortly. We don't need to hot-patch it in that case - # anyway, because it will pick up the new thread.start_new_thread that we have set above when it's imported. - global _threading - if _threading is None and 'threading' in sys.modules: - import threading - _threading = threading - _threading._start_new_thread = thread_creator - - global _INTERCEPTING_FOR_ATTACH - _INTERCEPTING_FOR_ATTACH = for_attach - -## Modified parameters by Don Jayamanne -# Accept current Process id to pass back to debugger -def attach_process(port_num, debug_id, debug_options, currentPid, report = False, block = False): - global conn - for i in xrange(50): - try: - conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - conn.connect(('127.0.0.1', port_num)) - write_string(conn, debug_id) - write_int(conn, 0) # success - ## Begin modification by Don Jayamanne - # Pass current Process id to pass back to debugger - write_int(conn, currentPid) # success - ## End Modification by Don Jayamanne - break - except: - import time - time.sleep(50./1000) - else: - raise Exception('failed to attach') - attach_process_from_socket(conn, debug_options, report, block) - -def attach_process_from_socket(sock, debug_options, report = False, block = False): - global conn, attach_sent_break, DETACHED, DEBUG_STDLIB, BREAK_ON_SYSTEMEXIT_ZERO, DJANGO_DEBUG - - BREAK_ON_SYSTEMEXIT_ZERO = 'BreakOnSystemExitZero' in debug_options - DJANGO_DEBUG = 'DjangoDebugging' in debug_options - - if '' in PREFIXES: - # If one or more of the prefixes are empty, we can't reliably distinguish stdlib - # from user code, so override stdlib-only mode and allow to debug everything. - DEBUG_STDLIB = True - else: - DEBUG_STDLIB = 'DebugStdLib' in debug_options - - wait_on_normal_exit = 'WaitOnNormalExit' in debug_options - wait_on_abnormal_exit = 'WaitOnAbnormalExit' in debug_options - - def _excepthook(exc_type, exc_value, exc_tb): - # Display the exception and wait on exit - if exc_type is SystemExit: - if (wait_on_abnormal_exit and exc_value.code) or (wait_on_normal_exit and not exc_value.code): - print_exception(exc_type, exc_value, exc_tb) - do_wait() - else: - print_exception(exc_type, exc_value, exc_tb) - if wait_on_abnormal_exit: - do_wait() - sys.excepthook = sys.__excepthook__ = _excepthook - - conn = sock - attach_sent_break = False - - # start the debugging loop - global debugger_thread_id - debugger_thread_id = _start_new_thread(DebuggerLoop(conn).loop, ()) - - for mod_name, mod_value in sys.modules.items(): - try: - filename = getattr(mod_value, '__file__', None) - if filename is not None: - try: - fullpath = path.abspath(filename) - except: - pass - else: - MODULES.append((filename, Module(fullpath))) - except: - traceback.print_exc() - - if report: - THREADS_LOCK.acquire() - all_threads = list(THREADS.values()) - if block: - main_thread = THREADS[thread.get_ident()] - THREADS_LOCK.release() - for cur_thread in all_threads: - report_new_thread(cur_thread) - for filename, module in MODULES: - report_module_load(module) - DETACHED = False - - if block: - main_thread.block(lambda: report_process_loaded(thread.get_ident())) - - # intercept all new thread requests - if not _INTERCEPTING_FOR_ATTACH: - intercept_threads() - - if 'RedirectOutput' in debug_options: - enable_output_redirection() - -# Try to detach cooperatively, notifying the debugger as we do so. -def detach_process_and_notify_debugger(): - if DebuggerLoop.instance: - try: - DebuggerLoop.instance.command_detach() - except DebuggerExitException: # successfully detached - return - except: # swallow anything else, and forcibly detach below - pass - detach_process() - -def detach_process(): - global DETACHED - DETACHED = True - if not _INTERCEPTING_FOR_ATTACH: - if isinstance(sys.stdout, _DebuggerOutput): - sys.stdout = sys.stdout.old_out - if isinstance(sys.stderr, _DebuggerOutput): - sys.stderr = sys.stderr.old_out - - if not _INTERCEPTING_FOR_ATTACH: - thread.start_new_thread = _start_new_thread - thread.start_new = _start_new_thread - -def detach_threads(): - # tell all threads to stop tracing... - THREADS_LOCK.acquire() - all_threads = list(THREADS.items()) - THREADS_LOCK.release() - - for tid, pyThread in all_threads: - if not _INTERCEPTING_FOR_ATTACH: - pyThread.detach = True - pyThread.stepping = STEPPING_BREAK - - if pyThread._is_blocked: - pyThread.unblock() - - if not _INTERCEPTING_FOR_ATTACH: - THREADS_LOCK.acquire() - THREADS.clear() - THREADS_LOCK.release() - - BREAKPOINTS.clear() - -def new_thread(tid = None, set_break = False, frame = None): - # called during attach w/ a thread ID provided. - if tid == debugger_thread_id: - return None - - cur_thread = Thread(tid) - THREADS_LOCK.acquire() - THREADS[cur_thread.id] = cur_thread - THREADS_LOCK.release() - cur_thread.push_frame(frame) - if set_break: - cur_thread.stepping = STEPPING_ATTACH_BREAK - if not DETACHED: - report_new_thread(cur_thread) - return cur_thread - -def new_external_thread(): - thread = new_thread() - if not attach_sent_break: - # we are still doing the attach, make this thread break. - thread.stepping = STEPPING_ATTACH_BREAK - elif SEND_BREAK_COMPLETE: - # user requested break all, make this thread break - thread.stepping = STEPPING_BREAK - - sys.settrace(thread.trace_func) - -def do_wait(): - try: - import msvcrt - except ImportError: - sys.__stdout__.write('Press Enter to continue . . . ') - sys.__stdout__.flush() - sys.__stdin__.read(1) - else: - sys.__stdout__.write('Press any key to continue . . . ') - sys.__stdout__.flush() - msvcrt.getch() - -def enable_output_redirection(): - sys.stdout = _DebuggerOutput(sys.stdout, is_stdout = True) - sys.stderr = _DebuggerOutput(sys.stderr, is_stdout = False) - -def connect_repl_using_socket(sock): - _start_new_thread(DebuggerLoop.instance.connect_to_repl_backend_using_socket, (sock,)) - -class _DebuggerOutput(object): - """file like object which redirects output to the repl window.""" - errors = 'strict' - - def __init__(self, old_out, is_stdout): - self.is_stdout = is_stdout - self.old_out = old_out - if sys.version >= '3.' and hasattr(old_out, 'buffer'): - self.buffer = DebuggerBuffer(old_out.buffer) - - def flush(self): - if self.old_out: - self.old_out.flush() - - def writelines(self, lines): - for line in lines: - self.write(line) - - @property - def encoding(self): - return 'utf8' - - def write(self, value): - if not DETACHED: - probe_stack(3) - with _SendLockCtx: - write_bytes(conn, OUTP) - write_int(conn, thread.get_ident()) - write_string(conn, value) - if self.old_out: - self.old_out.write(value) - - def isatty(self): - return True - - def next(self): - pass - - @property - def name(self): - if self.is_stdout: - return "" - else: - return "" - - def __getattr__(self, name): - return getattr(self.old_out, name) - -class DebuggerBuffer(object): - def __init__(self, old_buffer): - self.buffer = old_buffer - - def write(self, data): - if not DETACHED: - probe_stack(3) - str_data = utf_8.decode(data)[0] - with _SendLockCtx: - write_bytes(conn, OUTP) - write_int(conn, thread.get_ident()) - write_string(conn, str_data) - self.buffer.write(data) - - def flush(self): - self.buffer.flush() - - def truncate(self, pos = None): - return self.buffer.truncate(pos) - - def tell(self): - return self.buffer.tell() - - def seek(self, pos, whence = 0): - return self.buffer.seek(pos, whence) - -def is_same_py_file(file1, file2): - """compares 2 filenames accounting for .pyc files""" - if file1.endswith('.pyc') or file1.endswith('.pyo'): - file1 = file1[:-1] - if file2.endswith('.pyc') or file2.endswith('.pyo'): - file2 = file2[:-1] - - return file1 == file2 - -def print_exception(exc_type, exc_value, exc_tb): - # remove debugger frames from the top and bottom of the traceback - tb = traceback.extract_tb(exc_tb) - for i in [0, -1]: - while tb: - frame_file = path.normcase(tb[i][0]) - if not any(is_same_py_file(frame_file, f) for f in DONT_DEBUG): - break - del tb[i] - - # print the traceback - if tb: - print('Traceback (most recent call last):') - for out in traceback.format_list(tb): - sys.stderr.write(out) - - # print the exception - for out in traceback.format_exception_only(exc_type, exc_value): - sys.stdout.write(out) - -def parse_debug_options(s): - return set([opt.strip() for opt in s.split(',')]) - -## Modified parameters by Don Jayamanne -# Accept current Process id to pass back to debugger -def debug(file, port_num, debug_id, debug_options, currentPid, run_as = 'script'): - # remove us from modules so there's no trace of us - sys.modules['$visualstudio_py_debugger'] = sys.modules['visualstudio_py_debugger'] - __name__ = '$visualstudio_py_debugger' - del sys.modules['visualstudio_py_debugger'] - - wait_on_normal_exit = 'WaitOnNormalExit' in debug_options - - ## Begin modification by Don Jayamanne - # Pass current Process id to pass back to debugger - attach_process(port_num, debug_id, debug_options, currentPid, report = True) - ## End Modification by Don Jayamanne - - # setup the current thread - cur_thread = new_thread() - cur_thread.stepping = STEPPING_LAUNCH_BREAK - - # start tracing on this thread - sys.settrace(cur_thread.trace_func) - - # now execute main file - globals_obj = {'__name__': '__main__'} - try: - if run_as == 'module': - exec_module(file, globals_obj) - elif run_as == 'code': - exec_code(file, '', globals_obj) - else: - exec_file(file, globals_obj) - finally: - sys.settrace(None) - THREADS_LOCK.acquire() - del THREADS[cur_thread.id] - THREADS_LOCK.release() - report_thread_exit(cur_thread) - - # Give VS debugger a chance to process commands - # by waiting for ack of "last" command - global _threading - if _threading is None: - import threading - _threading = threading - global last_ack_event - last_ack_event = _threading.Event() - with _SendLockCtx: - write_bytes(conn, LAST) - last_ack_event.wait(5) - - if wait_on_normal_exit: - do_wait() - -# Code objects for functions which are going to be at the bottom of the stack, right below the first -# stack frame for user code. When we walk the stack to determine whether to report or block on a given -# frame, hitting any of these means that we walked all the frames that we needed to look at. -DEBUG_ENTRYPOINTS = set(( - get_code(debug), - get_code(exec_file), - get_code(exec_module), - get_code(exec_code), - get_code(new_thread_wrapper) -)) diff --git a/pythonFiles/PythonTools/visualstudio_py_launcher.py b/pythonFiles/PythonTools/visualstudio_py_launcher.py deleted file mode 100644 index 12b2140c4f72..000000000000 --- a/pythonFiles/PythonTools/visualstudio_py_launcher.py +++ /dev/null @@ -1,92 +0,0 @@ -# Python Tools for Visual Studio -# Copyright(c) Microsoft Corporation -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the License); you may not use -# this file except in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS -# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY -# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -# MERCHANTABLITY OR NON-INFRINGEMENT. -# -# See the Apache Version 2.0 License for specific language governing -# permissions and limitations under the License. - -""" -Starts Debugging, expected to start with normal program -to start as first argument and directory to run from as -the second argument. -""" - -__author__ = "Microsoft Corporation " -__version__ = "3.0.0.0" - -import os -import os.path -import sys -import traceback -try: - import visualstudio_py_debugger as vspd -except: - traceback.print_exc() - print(''' -Internal error detected. Please copy the above traceback and report at -http://go.microsoft.com/fwlink/?LinkId=293415 - -Press Enter to close. . .''') - try: - raw_input() - except NameError: - input() - sys.exit(1) - -# Arguments are: -# 1. Working directory. -# 2. VS debugger port to connect to. -# 3. GUID for the debug session. -# 4. Debug options (as integer - see enum PythonDebugOptions). -# 5. '-m' or '-c' to override the default run-as mode. [optional] -# 6. Startup script name. -# 7. Script arguments. - -# change to directory we expected to start from -os.chdir(sys.argv[1]) - -port_num = int(sys.argv[2]) -debug_id = sys.argv[3] -debug_options = vspd.parse_debug_options(sys.argv[4]) -del sys.argv[0:5] - -# set run_as mode appropriately -run_as = 'script' -if sys.argv and sys.argv[0] == '-m': - run_as = 'module' - del sys.argv[0] -if sys.argv and sys.argv[0] == '-c': - run_as = 'code' - del sys.argv[0] - -# preserve filename before we del sys -filename = sys.argv[0] - -# fix sys.path to be the script file dir -sys.path[0] = '' - -# exclude ourselves from being debugged -vspd.DONT_DEBUG.append(os.path.normcase(__file__)) - -## Begin modification by Don Jayamanne -# Get current Process id to pass back to debugger -currentPid = os.getpid() -## End Modification by Don Jayamanne - -# remove all state we imported -del sys, os - -# and start debugging -## Begin modification by Don Jayamanne -# Pass current Process id to pass back to debugger -vspd.debug(filename, port_num, debug_id, debug_options, currentPid, run_as) -## End Modification by Don Jayamanne diff --git a/pythonFiles/PythonTools/visualstudio_py_repl.py b/pythonFiles/PythonTools/visualstudio_py_repl.py deleted file mode 100644 index 4f7529fec542..000000000000 --- a/pythonFiles/PythonTools/visualstudio_py_repl.py +++ /dev/null @@ -1,1381 +0,0 @@ -# Python Tools for Visual Studio -# Copyright(c) Microsoft Corporation -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the License); you may not use -# this file except in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS -# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY -# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -# MERCHANTABLITY OR NON-INFRINGEMENT. -# -# See the Apache Version 2.0 License for specific language governing -# permissions and limitations under the License. - -from __future__ import with_statement - -__author__ = "Microsoft Corporation " -__version__ = "3.0.0.0" - -# This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) -# attach scenario, it is loaded on the injected debugger attach thread, and if threading module -# hasn't been loaded already, it will assume that the thread on which it is being loaded is the -# main thread. This will cause issues when the thread goes away after attach completes. - -try: - import thread -except ImportError: - # Renamed in Python3k - import _thread as thread -try: - from ssl import SSLError -except: - SSLError = None - -import sys -import socket -import select -import time -import struct -import imp -import traceback -import random -import os -import inspect -import types -from collections import deque - -try: - # In the local attach scenario, visualstudio_py_util is injected into globals() - # by PyDebugAttach before loading this module, and cannot be imported. - _vspu = visualstudio_py_util -except: - try: - import visualstudio_py_util as _vspu - except ImportError: - import ptvsd.visualstudio_py_util as _vspu -to_bytes = _vspu.to_bytes -read_bytes = _vspu.read_bytes -read_int = _vspu.read_int -read_string = _vspu.read_string -write_bytes = _vspu.write_bytes -write_int = _vspu.write_int -write_string = _vspu.write_string - -try: - unicode -except NameError: - unicode = str - -try: - BaseException -except NameError: - # BaseException not defined until Python 2.5 - BaseException = Exception - -DEBUG = os.environ.get('DEBUG_REPL') is not None - -__all__ = ['ReplBackend', 'BasicReplBackend', 'BACKEND'] - -def _debug_write(out): - if DEBUG: - sys.__stdout__.write(out) - sys.__stdout__.flush() - - -class SafeSendLock(object): - """a lock which ensures we're released if we take a KeyboardInterrupt exception acquiring it""" - def __init__(self): - self.lock = thread.allocate_lock() - - def __enter__(self): - self.acquire() - - def __exit__(self, exc_type, exc_value, tb): - self.release() - - def acquire(self): - try: - self.lock.acquire() - except KeyboardInterrupt: - try: - self.lock.release() - except: - pass - raise - - def release(self): - self.lock.release() - -def _command_line_to_args_list(cmdline): - """splits a string into a list using Windows command line syntax.""" - args_list = [] - - if cmdline and cmdline.strip(): - from ctypes import c_int, c_voidp, c_wchar_p - from ctypes import byref, POINTER, WinDLL - - clta = WinDLL('shell32').CommandLineToArgvW - clta.argtypes = [c_wchar_p, POINTER(c_int)] - clta.restype = POINTER(c_wchar_p) - - lf = WinDLL('kernel32').LocalFree - lf.argtypes = [c_voidp] - - pNumArgs = c_int() - r = clta(cmdline, byref(pNumArgs)) - if r: - for index in range(0, pNumArgs.value): - if sys.hexversion >= 0x030000F0: - argval = r[index] - else: - argval = r[index].encode('ascii', 'replace') - args_list.append(argval) - lf(r) - else: - sys.stderr.write('Error parsing script arguments:\n') - sys.stderr.write(cmdline + '\n') - - return args_list - - -class UnsupportedReplException(Exception): - def __init__(self, reason): - self.reason = reason - -# save the start_new_thread so we won't debug/break into the REPL comm thread. -start_new_thread = thread.start_new_thread -class ReplBackend(object): - """back end for executing REPL code. This base class handles all of the -communication with the remote process while derived classes implement the -actual inspection and introspection.""" - _MRES = to_bytes('MRES') - _SRES = to_bytes('SRES') - _MODS = to_bytes('MODS') - _IMGD = to_bytes('IMGD') - _PRPC = to_bytes('PRPC') - _RDLN = to_bytes('RDLN') - _STDO = to_bytes('STDO') - _STDE = to_bytes('STDE') - _DBGA = to_bytes('DBGA') - _DETC = to_bytes('DETC') - _DPNG = to_bytes('DPNG') - _DXAM = to_bytes('DXAM') - - _MERR = to_bytes('MERR') - _SERR = to_bytes('SERR') - _ERRE = to_bytes('ERRE') - _EXIT = to_bytes('EXIT') - _DONE = to_bytes('DONE') - _MODC = to_bytes('MODC') - - def __init__(self): - import threading - self.conn = None - self.send_lock = SafeSendLock() - self.input_event = threading.Lock() - self.input_event.acquire() # lock starts acquired (we use it like a manual reset event) - self.input_string = None - self.exit_requested = False - - def connect(self, port): - self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.conn.connect(('127.0.0.1', port)) - - # start a new thread for communicating w/ the remote process - start_new_thread(self._repl_loop, ()) - - def connect_using_socket(self, socket): - self.conn = socket - start_new_thread(self._repl_loop, ()) - - def _repl_loop(self): - """loop on created thread which processes communicates with the REPL window""" - try: - while True: - if self.check_for_exit_repl_loop(): - break - - # we receive a series of 4 byte commands. Each command then - # has it's own format which we must parse before continuing to - # the next command. - self.flush() - self.conn.settimeout(10) - - # 2.x raises SSLError in case of timeout (http://bugs.python.org/issue10272) - if SSLError: - timeout_exc_types = (socket.timeout, SSLError) - else: - timeout_exc_types = socket.timeout - try: - inp = read_bytes(self.conn, 4) - except timeout_exc_types: - r, w, x = select.select([], [], [self.conn], 0) - if x: - # an exception event has occured on the socket... - raise - continue - - self.conn.settimeout(None) - if inp == '': - break - self.flush() - - cmd = ReplBackend._COMMANDS.get(inp) - if cmd is not None: - cmd(self) - except: - _debug_write('error in repl loop') - _debug_write(traceback.format_exc()) - self.exit_process() - - time.sleep(2) # try and exit gracefully, then interrupt main if necessary - - if sys.platform == 'cli': - # just kill us as fast as possible - import System - System.Environment.Exit(1) - - self.interrupt_main() - - def check_for_exit_repl_loop(self): - return False - - def _cmd_run(self): - """runs the received snippet of code""" - self.run_command(read_string(self.conn)) - - def _cmd_abrt(self): - """aborts the current running command""" - # abort command, interrupts execution of the main thread. - self.interrupt_main() - - def _cmd_exit(self): - """exits the interactive process""" - self.exit_requested = True - self.exit_process() - - def _cmd_mems(self): - """gets the list of members available for the given expression""" - expression = read_string(self.conn) - try: - name, inst_members, type_members = self.get_members(expression) - except: - with self.send_lock: - write_bytes(self.conn, ReplBackend._MERR) - _debug_write('error in eval') - _debug_write(traceback.format_exc()) - else: - with self.send_lock: - write_bytes(self.conn, ReplBackend._MRES) - write_string(self.conn, name) - self._write_member_dict(inst_members) - self._write_member_dict(type_members) - - def _cmd_sigs(self): - """gets the signatures for the given expression""" - expression = read_string(self.conn) - try: - sigs = self.get_signatures(expression) - except: - with self.send_lock: - write_bytes(self.conn, ReplBackend._SERR) - _debug_write('error in eval') - _debug_write(traceback.format_exc()) - else: - with self.send_lock: - write_bytes(self.conn, ReplBackend._SRES) - # single overload - write_int(self.conn, len(sigs)) - for doc, args, vargs, varkw, defaults in sigs: - # write overload - write_string(self.conn, (doc or '')[:4096]) - arg_count = len(args) + (vargs is not None) + (varkw is not None) - write_int(self.conn, arg_count) - - def_values = [''] * (len(args) - len(defaults)) + ['=' + d for d in defaults] - for arg, def_value in zip(args, def_values): - write_string(self.conn, (arg or '') + def_value) - if vargs is not None: - write_string(self.conn, '*' + vargs) - if varkw is not None: - write_string(self.conn, '**' + varkw) - - def _cmd_setm(self): - global exec_mod - """sets the current module which code will execute against""" - mod_name = read_string(self.conn) - self.set_current_module(mod_name) - - def _cmd_sett(self): - """sets the current thread and frame which code will execute against""" - thread_id = read_int(self.conn) - frame_id = read_int(self.conn) - frame_kind = read_int(self.conn) - self.set_current_thread_and_frame(thread_id, frame_id, frame_kind) - - def _cmd_mods(self): - """gets the list of available modules""" - try: - res = self.get_module_names() - res.sort() - except: - res = [] - - with self.send_lock: - write_bytes(self.conn, ReplBackend._MODS) - write_int(self.conn, len(res)) - for name, filename in res: - write_string(self.conn, name) - write_string(self.conn, filename) - - def _cmd_inpl(self): - """handles the input command which returns a string of input""" - self.input_string = read_string(self.conn) - self.input_event.release() - - def _cmd_excf(self): - """handles executing a single file""" - filename = read_string(self.conn) - args = read_string(self.conn) - self.execute_file(filename, args) - - def _cmd_excx(self): - """handles executing a single file, module or process""" - filetype = read_string(self.conn) - filename = read_string(self.conn) - args = read_string(self.conn) - self.execute_file_ex(filetype, filename, args) - - def _cmd_debug_attach(self): - import visualstudio_py_debugger - port = read_int(self.conn) - id = read_string(self.conn) - debug_options = visualstudio_py_debugger.parse_debug_options(read_string(self.conn)) - self.attach_process(port, id, debug_options) - - _COMMANDS = { - to_bytes('run '): _cmd_run, - to_bytes('abrt'): _cmd_abrt, - to_bytes('exit'): _cmd_exit, - to_bytes('mems'): _cmd_mems, - to_bytes('sigs'): _cmd_sigs, - to_bytes('mods'): _cmd_mods, - to_bytes('setm'): _cmd_setm, - to_bytes('sett'): _cmd_sett, - to_bytes('inpl'): _cmd_inpl, - to_bytes('excf'): _cmd_excf, - to_bytes('excx'): _cmd_excx, - to_bytes('dbga'): _cmd_debug_attach, - } - - def _write_member_dict(self, mem_dict): - write_int(self.conn, len(mem_dict)) - for name, type_name in mem_dict.items(): - write_string(self.conn, name) - write_string(self.conn, type_name) - - def on_debugger_detach(self): - with self.send_lock: - write_bytes(self.conn, ReplBackend._DETC) - - def init_debugger(self): - from os import path - sys.path.append(path.dirname(__file__)) - import visualstudio_py_debugger - visualstudio_py_debugger.DONT_DEBUG.append(path.normcase(__file__)) - new_thread = visualstudio_py_debugger.new_thread() - sys.settrace(new_thread.trace_func) - visualstudio_py_debugger.intercept_threads(True) - - def send_image(self, filename): - with self.send_lock: - write_bytes(self.conn, ReplBackend._IMGD) - write_string(self.conn, filename) - - def write_png(self, image_bytes): - with self.send_lock: - write_bytes(self.conn, ReplBackend._DPNG) - write_int(self.conn, len(image_bytes)) - write_bytes(self.conn, image_bytes) - - def write_xaml(self, xaml_bytes): - with self.send_lock: - write_bytes(self.conn, ReplBackend._DXAM) - write_int(self.conn, len(xaml_bytes)) - write_bytes(self.conn, xaml_bytes) - - def send_prompt(self, ps1, ps2, update_all = True): - """sends the current prompt to the interactive window""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._PRPC) - write_string(self.conn, ps1) - write_string(self.conn, ps2) - write_int(self.conn, update_all) - - def send_error(self): - """reports that an error occured to the interactive window""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._ERRE) - - def send_exit(self): - """reports the that the REPL process has exited to the interactive window""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._EXIT) - - def send_command_executed(self): - with self.send_lock: - write_bytes(self.conn, ReplBackend._DONE) - - def send_modules_changed(self): - with self.send_lock: - write_bytes(self.conn, ReplBackend._MODC) - - def read_line(self): - """reads a line of input from standard input""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._RDLN) - self.input_event.acquire() - return self.input_string - - def write_stdout(self, value): - """writes a string to standard output in the remote console""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._STDO) - write_string(self.conn, value) - - def write_stderr(self, value): - """writes a string to standard input in the remote console""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._STDE) - write_string(self.conn, value) - - ################################################################ - # Implementation of execution, etc... - - def execution_loop(self): - """starts processing execution requests""" - raise NotImplementedError - - def run_command(self, command): - """runs the specified command which is a string containing code""" - raise NotImplementedError - - def execute_file(self, filename, args): - """executes the given filename as the main module""" - return self.execute_file_ex('script', filename, args) - - def execute_file_ex(self, filetype, filename, args): - """executes the given filename as a 'script', 'module' or 'process'.""" - raise NotImplementedError - - def interrupt_main(self): - """aborts the current running command""" - raise NotImplementedError - - def exit_process(self): - """exits the REPL process""" - raise NotImplementedError - - def get_members(self, expression): - """returns a tuple of the type name, instance members, and type members""" - raise NotImplementedError - - def get_signatures(self, expression): - """returns doc, args, vargs, varkw, defaults.""" - raise NotImplementedError - - def set_current_module(self, module): - """sets the module which code executes against""" - raise NotImplementedError - - def set_current_thread_and_frame(self, thread_id, frame_id, frame_kind): - """sets the current thread and frame which code will execute against""" - raise NotImplementedError - - def get_module_names(self): - """returns a list of module names""" - raise NotImplementedError - - def flush(self): - """flushes the stdout/stderr buffers""" - raise NotImplementedError - - def attach_process(self, port, debugger_id, debug_options): - """starts processing execution requests""" - raise NotImplementedError - -def exit_work_item(): - sys.exit(0) - - -if sys.platform == 'cli': - # We need special handling to reset the abort for keyboard interrupt exceptions - class ReplAbortException(Exception): pass - - import clr - clr.AddReference('Microsoft.Dynamic') - clr.AddReference('Microsoft.Scripting') - clr.AddReference('IronPython') - from Microsoft.Scripting import KeyboardInterruptException - from Microsoft.Scripting import ParamDictionaryAttribute - from IronPython.Runtime.Operations import PythonOps - from IronPython.Runtime import PythonContext - from Microsoft.Scripting import SourceUnit, SourceCodeKind - from Microsoft.Scripting.Runtime import Scope - - python_context = clr.GetCurrentRuntime().GetLanguage(PythonContext) - - from System import DBNull, ParamArrayAttribute - builtin_method_descriptor_type = type(list.append) - - import System - NamespaceType = type(System) - -class _OldClass: - pass - -_OldClassType = type(_OldClass) -_OldInstanceType = type(_OldClass()) - -class BasicReplBackend(ReplBackend): - future_bits = 0x3e010 # code flags used to mark future bits - - """Basic back end which executes all Python code in-proc""" - def __init__(self, mod_name = '__main__', launch_file = None): - import threading - ReplBackend.__init__(self) - if mod_name is not None: - if sys.platform == 'cli': - self.exec_mod = Scope() - self.exec_mod.__name__ = '__main__' - else: - sys.modules[mod_name] = self.exec_mod = imp.new_module(mod_name) - else: - self.exec_mod = sys.modules['__main__'] - - self.launch_file = launch_file - self.code_flags = 0 - self.execute_item = None - self.execute_item_lock = threading.Lock() - self.execute_item_lock.acquire() # lock starts acquired (we use it like manual reset event) - - def init_connection(self): - sys.stdout = _ReplOutput(self, is_stdout = True) - sys.stderr = _ReplOutput(self, is_stdout = False) - sys.stdin = _ReplInput(self) - if sys.platform == 'cli': - import System - System.Console.SetOut(DotNetOutput(self, True)) - System.Console.SetError(DotNetOutput(self, False)) - - def connect(self, port): - ReplBackend.connect(self, port) - self.init_connection() - - def connect_using_socket(self, socket): - ReplBackend.connect_using_socket(self, socket) - self.init_connection() - - - def run_file_as_main(self, filename, args): - f = open(filename, 'rb') - try: - contents = f.read().replace(to_bytes('\r\n'), to_bytes('\n')) - finally: - f.close() - sys.argv = [filename] - sys.argv.extend(_command_line_to_args_list(args)) - self.exec_mod.__file__ = filename - if sys.platform == 'cli': - code = python_context.CreateSnippet(contents, None, SourceCodeKind.File) - code.Execute(self.exec_mod) - else: - self.code_flags = 0 - real_file = filename - if isinstance(filename, unicode) and unicode is not str: - # http://pytools.codeplex.com/workitem/696 - # We need to encode the unicode filename here, Python 2.x will throw trying - # to convert it to ASCII instead of the filesystem encoding. - real_file = filename.encode(sys.getfilesystemencoding()) - code = compile(contents, real_file, 'exec') - self.code_flags |= (code.co_flags & BasicReplBackend.future_bits) - exec(code, self.exec_mod.__dict__, self.exec_mod.__dict__) - - def python_executor(self, code): - """we can't close over unbound variables in execute_code_work_item -due to the exec, so we do it here""" - def func(): - code.Execute(self.exec_mod) - return func - - def execute_code_work_item(self): - _debug_write('Executing: ' + repr(self.current_code)) - stripped_code = self.current_code.strip() - - if sys.platform == 'cli': - code_to_send = '' - for line in stripped_code.split('\n'): - stripped = line.strip() - if (stripped.startswith('#') or not stripped) and not code_to_send: - continue - code_to_send += line + '\n' - - code = python_context.CreateSnippet(code_to_send, None, SourceCodeKind.InteractiveCode) - dispatcher = clr.GetCurrentRuntime().GetLanguage(PythonContext).GetCommandDispatcher() - if dispatcher is not None: - dispatcher(self.python_executor(code)) - else: - code.Execute(self.exec_mod) - else: - code = compile(self.current_code, '', 'single', self.code_flags) - self.code_flags |= (code.co_flags & BasicReplBackend.future_bits) - exec(code, self.exec_mod.__dict__, self.exec_mod.__dict__) - self.current_code = None - - def run_one_command(self, cur_modules, cur_ps1, cur_ps2): - # runs a single iteration of an input, execute file, etc... - # This is extracted into it's own method so we play nice w/ IronPython thread abort. - # Otherwise we have a nested exception hanging around and the 2nd abort doesn't - # work (that's probably an IronPython bug) - try: - new_modules = self._get_cur_module_set() - try: - if new_modules != cur_modules: - self.send_modules_changed() - except: - pass - cur_modules = new_modules - - self.execute_item_lock.acquire() - - if self.check_for_exit_execution_loop(): - return True, None, None, None - - if self.execute_item is not None: - try: - self.execute_item() - finally: - self.execute_item = None - - try: - self.send_command_executed() - except SocketError: - return True, None, None, None - - try: - if cur_ps1 != sys.ps1 or cur_ps2 != sys.ps2: - new_ps1 = str(sys.ps1) - new_ps2 = str(sys.ps2) - - self.send_prompt(new_ps1, new_ps2) - - cur_ps1 = new_ps1 - cur_ps2 = new_ps2 - except: - pass - except SystemExit: - self.send_error() - self.send_exit() - # wait for ReplEvaluator to send back exit requested which will indicate - # that all the output has been processed. - while not self.exit_requested: - time.sleep(.25) - return True, None, None, None - except BaseException: - _debug_write('Exception') - exc_type, exc_value, exc_tb = sys.exc_info() - if sys.platform == 'cli': - if isinstance(exc_value.clsException, System.Threading.ThreadAbortException): - try: - System.Threading.Thread.ResetAbort() - except SystemError: - pass - sys.stderr.write('KeyboardInterrupt') - else: - # let IronPython format the exception so users can do -X:ExceptionDetail or -X:ShowClrExceptions - exc_next = self.skip_internal_frames(exc_tb) - sys.stderr.write(''.join(traceback.format_exception(exc_type, exc_value, exc_next))) - else: - exc_next = self.skip_internal_frames(exc_tb) - sys.stderr.write(''.join(traceback.format_exception(exc_type, exc_value, exc_next))) - - try: - self.send_error() - except SocketError: - _debug_write('err sending DONE') - return True, None, None, None - - return False, cur_modules, cur_ps1, cur_ps2 - - def skip_internal_frames(self, tb): - """return the first frame outside of the repl/debugger code""" - while tb is not None and self.is_internal_frame(tb): - tb = tb.tb_next - return tb - - def is_internal_frame(self, tb): - """return true if the frame is from internal code (repl or debugger)""" - f = tb.tb_frame - co = f.f_code - filename = co.co_filename - return filename.endswith('visualstudio_py_repl.py') or filename.endswith('visualstudio_py_debugger.py') - - def execution_loop(self): - """loop on the main thread which is responsible for executing code""" - - if sys.platform == 'cli' and sys.version_info[:3] < (2, 7, 1): - # IronPython doesn't support thread.interrupt_main until 2.7.1 - import System - self.main_thread = System.Threading.Thread.CurrentThread - - # save our selves so global lookups continue to work (required pre-2.6)... - cur_modules = set() - try: - cur_ps1 = sys.ps1 - cur_ps2 = sys.ps2 - except: - # CPython/IronPython don't set sys.ps1 for non-interactive sessions, Jython and PyPy do - sys.ps1 = cur_ps1 = '>>> ' - sys.ps2 = cur_ps2 = '... ' - - self.send_prompt(cur_ps1, cur_ps2) - - # launch the startup script if one has been specified - if self.launch_file: - try: - self.run_file_as_main(self.launch_file, '') - except: - print('error in launching startup script:') - traceback.print_exc() - - while True: - exit, cur_modules, cur_ps1, cur_ps2 = self.run_one_command(cur_modules, cur_ps1, cur_ps2) - if exit: - return - - def check_for_exit_execution_loop(self): - return False - - def execute_script_work_item(self): - self.run_file_as_main(self.current_code, self.current_args) - - def execute_module_work_item(self): - new_argv = [''] + _command_line_to_args_list(self.current_args) - old_argv = sys.argv - import runpy - try: - sys.argv = new_argv - runpy.run_module(self.current_code, alter_sys=True) - except Exception: - traceback.print_exc() - finally: - sys.argv = old_argv - - def execute_process_work_item(self): - try: - from subprocess import Popen, PIPE, STDOUT - import codecs - out_codec = codecs.lookup(sys.stdout.encoding) - - proc = Popen( - '"%s" %s' % (self.current_code, self.current_args), - stdout=PIPE, - stderr=STDOUT, - bufsize=0, - ) - - for line in proc.stdout: - print(out_codec.decode(line, 'replace')[0].rstrip('\r\n')) - except Exception: - traceback.print_exc() - - @staticmethod - def _get_cur_module_set(): - """gets the set of modules avoiding exceptions if someone puts something - weird in there""" - - try: - return set(sys.modules) - except: - res = set() - for name in sys.modules: - try: - res.add(name) - except: - pass - return res - - - def run_command(self, command): - self.current_code = command - self.execute_item = self.execute_code_work_item - self.execute_item_lock.release() - - def execute_file_ex(self, filetype, filename, args): - self.current_code = filename - self.current_args = args - self.execute_item = getattr(self, 'execute_%s_work_item' % filetype, None) - self.execute_item_lock.release() - - def interrupt_main(self): - # acquire the send lock so we dont interrupt while we're communicting w/ the debugger - with self.send_lock: - if sys.platform == 'cli' and sys.version_info[:3] < (2, 7, 1): - # IronPython doesn't get thread.interrupt_main until 2.7.1 - self.main_thread.Abort(ReplAbortException()) - else: - thread.interrupt_main() - - def exit_process(self): - self.execute_item = exit_work_item - try: - self.execute_item_lock.release() - except: - pass - sys.exit(0) - - def get_members(self, expression): - """returns a tuple of the type name, instance members, and type members""" - getattr_func = getattr - if not expression: - all_members = {} - if sys.platform == 'cli': - code = python_context.CreateSnippet('vars()', None, SourceCodeKind.AutoDetect) - items = code.Execute(self.exec_mod) - else: - items = self.exec_mod.__dict__ - - for key, value in items.items(): - all_members[key] = self.get_type_name(value) - return '', all_members, {} - else: - if sys.platform == 'cli': - code = python_context.CreateSnippet(expression, None, SourceCodeKind.AutoDetect) - val = code.Execute(self.exec_mod) - - code = python_context.CreateSnippet('dir(' + expression + ')', None, SourceCodeKind.AutoDetect) - members = code.Execute(self.exec_mod) - - code = python_context.CreateSnippet('lambda value, name: getattr(value, name)', None, SourceCodeKind.AutoDetect) - getattr_func = code.Execute(self.exec_mod) - else: - val = eval(expression, self.exec_mod.__dict__, self.exec_mod.__dict__) - members = dir(val) - - return self.collect_members(val, members, getattr_func) - - def collect_members(self, val, members, getattr_func): - t = type(val) - - inst_members = {} - if hasattr(val, '__dict__'): - # collect the instance members - try: - for mem_name in val.__dict__: - mem_t = self._get_member_type(val, mem_name, True, getattr_func) - if mem_t is not None: - inst_members[mem_name] = mem_t - except: - pass - - # collect the type members - - type_members = {} - for mem_name in members: - if mem_name not in inst_members: - mem_t = self._get_member_type(val, mem_name, False, getattr_func) - if mem_t is not None: - type_members[mem_name] = mem_t - - - return t.__module__ + '.' + t.__name__, inst_members, type_members - - def get_ipy_sig(self, obj, ctor): - args = [] - vargs = None - varkw = None - defaults = [] - for param in ctor.GetParameters(): - if param.IsDefined(ParamArrayAttribute, False): - vargs = param.Name - elif param.IsDefined(ParamDictionaryAttribute, False): - varkw = param.Name - else: - args.append(param.Name) - - if param.DefaultValue is not DBNull.Value: - defaults.append(repr(param.DefaultValue)) - - return obj.__doc__, args, vargs, varkw, tuple(defaults) - - def get_signatures(self, expression): - if sys.platform == 'cli': - code = python_context.CreateSnippet(expression, None, SourceCodeKind.AutoDetect) - val = code.Execute(self.exec_mod) - else: - val = eval(expression, self.exec_mod.__dict__, self.exec_mod.__dict__) - - return self.collect_signatures(val) - - def collect_signatures(self, val): - doc = val.__doc__ - type_obj = None - if isinstance(val, type) or isinstance(val, _OldClassType): - type_obj = val - val = val.__init__ - - try: - args, vargs, varkw, defaults = inspect.getargspec(val) - except TypeError: - # we're not doing inspect on a Python function... - if sys.platform == 'cli': - if type_obj is not None: - clr_type = clr.GetClrType(type_obj) - ctors = clr_type.GetConstructors() - return [self.get_ipy_sig(type_obj, ctor) for ctor in ctors] - elif type(val) is types.BuiltinFunctionType: - return [self.get_ipy_sig(target, target.Targets[0]) for target in val.Overloads.Functions] - elif type(val) is builtin_method_descriptor_type: - val = PythonOps.GetBuiltinMethodDescriptorTemplate(val) - return [self.get_ipy_sig(target, target.Targets[0]) for target in val.Overloads.Functions] - raise - - remove_self = type_obj is not None or (type(val) is types.MethodType and - ((sys.version_info >= (3,) and val.__self__ is not None) or - (sys.version_info < (3,) and val.im_self is not None))) - - if remove_self: - # remove self for instance methods and types - args = args[1:] - - if defaults is not None: - defaults = [repr(default) for default in defaults] - else: - defaults = [] - return [(doc, args, vargs, varkw, defaults)] - - def set_current_module(self, module): - mod = sys.modules.get(module) - if mod is not None: - _debug_write('Setting module to ' + module) - if sys.platform == 'cli': - self.exec_mod = clr.GetClrType(type(sys)).GetProperty('Scope', System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(sys, ()) - else: - self.exec_mod = mod - else: - _debug_write('Unknown module ' + module) - - def get_module_names(self): - res = [] - for name, module in sys.modules.items(): - try: - if name != 'visualstudio_py_repl' and name != '$visualstudio_py_debugger': - if sys.platform == 'cli' and type(module) is NamespaceType: - self.get_namespaces(name, module, res) - else: - filename = getattr(module, '__file__', '') or '' - res.append((name, filename)) - - except: - pass - return res - - def get_namespaces(self, basename, namespace, names): - names.append((basename, '')) - try: - for name in dir(namespace): - new_name = basename + '.' + name - new_namespace = getattr(namespace, name) - - if type(new_namespace) is NamespaceType: - self.get_namespaces(new_name, new_namespace, names) - except: - pass - - def flush(self): - sys.stdout.flush() - - def do_detach(self): - import visualstudio_py_debugger - visualstudio_py_debugger.DETACH_CALLBACKS.remove(self.do_detach) - self.on_debugger_detach() - - def attach_process(self, port, debugger_id, debug_options): - def execute_attach_process_work_item(): - import visualstudio_py_debugger - visualstudio_py_debugger.DETACH_CALLBACKS.append(self.do_detach) - visualstudio_py_debugger.attach_process(port, debugger_id, debug_options, report=True, block=True) - - self.execute_item = execute_attach_process_work_item - self.execute_item_lock.release() - - @staticmethod - def get_type_name(val): - try: - mem_t = type(val) - mem_t_name = mem_t.__module__ + '.' + mem_t.__name__ - return mem_t_name - except: - pass - - @staticmethod - def _get_member_type(inst, name, from_dict, getattr_func = None): - try: - if from_dict: - val = inst.__dict__[name] - elif type(inst) is _OldInstanceType: - val = getattr_func(inst.__class__, name) - else: - val = getattr_func(type(inst), name) - mem_t_name = BasicReplBackend.get_type_name(val) - return mem_t_name - except: - if not from_dict: - try: - return BasicReplBackend.get_type_name(getattr_func(inst, name)) - except: - pass - return - -class DebugReplBackend(BasicReplBackend): - def __init__(self, debugger): - BasicReplBackend.__init__(self, None, None) - self.debugger = debugger - self.thread_id = None - self.frame_id = None - self.frame_kind = None - self.disconnect_requested = False - - def init_connection(self): - sys.stdout = _ReplOutput(self, is_stdout = True, old_out = sys.stdout) - sys.stderr = _ReplOutput(self, is_stdout = False, old_out = sys.stderr) - if sys.platform == 'cli': - import System - self.old_cli_stdout = System.Console.Out - self.old_cli_stderr = System.Console.Error - System.Console.SetOut(DotNetOutput(self, True, System.Console.Out)) - System.Console.SetError(DotNetOutput(self, False, System.Console.Error)) - - def connect_from_debugger(self, port): - ReplBackend.connect(self, port) - self.init_connection() - - def connect_from_debugger_using_socket(self, socket): - ReplBackend.connect_using_socket(self, socket) - self.init_connection() - - def disconnect_from_debugger(self): - sys.stdout = sys.stdout.old_out - sys.stderr = sys.stderr.old_out - if sys.platform == 'cli': - System.Console.SetOut(self.old_cli_stdout) - System.Console.SetError(self.old_cli_stderr) - del self.old_cli_stdout - del self.old_cli_stderr - - # this tells both _repl_loop and execution_loop, each - # running on its own worker thread, to exit - self.disconnect_requested = True - self.execute_item_lock.release() - - def set_current_thread_and_frame(self, thread_id, frame_id, frame_kind): - self.thread_id = thread_id - self.frame_id = frame_id - self.frame_kind = frame_kind - self.exec_mod = None - - def execute_code_work_item(self): - if self.exec_mod is not None: - BasicReplBackend.execute_code_work_item(self) - else: - try: - self.debugger.execute_code_no_report(self.current_code, self.thread_id, self.frame_id, self.frame_kind) - finally: - self.current_code = None - - def get_members(self, expression): - """returns a tuple of the type name, instance members, and type members""" - if self.exec_mod is not None: - return BasicReplBackend.get_members(self, expression) - else: - thread, cur_frame = self.debugger.get_thread_and_frame(self.thread_id, self.frame_id, self.frame_kind) - return self.get_members_for_frame(expression, thread, cur_frame, self.frame_kind) - - def get_signatures(self, expression): - """returns doc, args, vargs, varkw, defaults.""" - if self.exec_mod is not None: - return BasicReplBackend.get_signatures(self, expression) - else: - thread, cur_frame = self.debugger.get_thread_and_frame(self.thread_id, self.frame_id, self.frame_kind) - return self.get_signatures_for_frame(expression, thread, cur_frame, self.frame_kind) - - def get_members_for_frame(self, expression, thread, cur_frame, frame_kind): - """returns a tuple of the type name, instance members, and type members""" - getattr_func = getattr - if not expression: - all_members = {} - if sys.platform == 'cli': - code = python_context.CreateSnippet('vars()', None, SourceCodeKind.AutoDetect) - globals = code.Execute(Scope(cur_frame.f_globals)) - locals = code.Execute(Scope(thread.get_locals(cur_frame, frame_kind))) - else: - globals = cur_frame.f_globals - locals = thread.get_locals(cur_frame, frame_kind) - - for key, value in globals.items(): - all_members[key] = self.get_type_name(value) - - for key, value in locals.items(): - all_members[key] = self.get_type_name(value) - - return '', all_members, {} - else: - if sys.platform == 'cli': - scope = Scope(cur_frame.f_globals) - - code = python_context.CreateSnippet(expression, None, SourceCodeKind.AutoDetect) - val = code.Execute(scope) - - code = python_context.CreateSnippet('dir(' + expression + ')', None, SourceCodeKind.AutoDetect) - members = code.Execute(scope) - - code = python_context.CreateSnippet('lambda value, name: getattr(value, name)', None, SourceCodeKind.AutoDetect) - getattr_func = code.Execute(scope) - else: - val = eval(expression, cur_frame.f_globals, thread.get_locals(cur_frame, frame_kind)) - members = dir(val) - - return self.collect_members(val, members, getattr_func) - - def get_signatures_for_frame(self, expression, thread, cur_frame, frame_kind): - if sys.platform == 'cli': - code = python_context.CreateSnippet(expression, None, SourceCodeKind.AutoDetect) - val = code.Execute(Scope(cur_frame.f_globals)) - else: - val = eval(expression, cur_frame.f_globals, thread.get_locals(cur_frame, frame_kind)) - - return self.collect_signatures(val) - - def set_current_module(self, module): - if module == '': - self.exec_mod = None - else: - BasicReplBackend.set_current_module(self, module) - - def check_for_exit_repl_loop(self): - return self.disconnect_requested - - def check_for_exit_execution_loop(self): - return self.disconnect_requested - -class _ReplOutput(object): - """file like object which redirects output to the repl window.""" - errors = None - - def __init__(self, backend, is_stdout, old_out = None): - self.name = "" if is_stdout else "" - self.backend = backend - self.old_out = old_out - self.is_stdout = is_stdout - self.pipe = None - - def flush(self): - if self.old_out: - self.old_out.flush() - - def fileno(self): - if self.pipe is None: - self.pipe = os.pipe() - thread.start_new_thread(self.pipe_thread, (), {}) - - return self.pipe[1] - - def pipe_thread(self): - while True: - data = os.read(self.pipe[0], 1) - if data == '\r': - data = os.read(self.pipe[0], 1) - if data == '\n': - self.write('\n') - else: - self.write('\r' + data) - else: - self.write(data) - - @property - def encoding(self): - return 'utf8' - - def writelines(self, lines): - for line in lines: - self.write(line) - self.write('\n') - - def write(self, value): - _debug_write('printing ' + repr(value) + '\n') - if self.is_stdout: - self.backend.write_stdout(value) - else: - self.backend.write_stderr(value) - if self.old_out: - self.old_out.write(value) - - def isatty(self): - return True - - def next(self): - pass - - -class _ReplInput(object): - """file like object which redirects input from the repl window""" - def __init__(self, backend): - self.backend = backend - - def readline(self): - return self.backend.read_line() - - def readlines(self, size = None): - res = [] - while True: - line = self.readline() - if line is not None: - res.append(line) - else: - break - - return res - - def xreadlines(self): - return self - - def write(self, *args): - raise IOError("File not open for writing") - - def flush(self): pass - - def isatty(self): - return True - - def __iter__(self): - return self - - def next(self): - return self.readline() - - -if sys.platform == 'cli': - import System - class DotNetOutput(System.IO.TextWriter): - def __new__(cls, backend, is_stdout, old_out=None): - return System.IO.TextWriter.__new__(cls) - - def __init__(self, backend, is_stdout, old_out=None): - self.backend = backend - self.is_stdout = is_stdout - self.old_out = old_out - - def Write(self, value, *args): - if self.old_out: - self.old_out.Write(value, *args) - - if not args: - if type(value) is str or type(value) is System.Char: - if self.is_stdout: - self.backend.write_stdout(str(value).replace('\r\n', '\n')) - else: - self.backend.write_stderr(str(value).replace('\r\n', '\n')) - else: - super(DotNetOutput, self).Write.Overloads[object](value) - else: - self.Write(System.String.Format(value, *args)) - - def WriteLine(self, value, *args): - if self.old_out: - self.old_out.WriteLine(value, *args) - - if not args: - if type(value) is str or type(value) is System.Char: - if self.is_stdout: - self.backend.write_stdout(str(value).replace('\r\n', '\n') + '\n') - else: - self.backend.write_stderr(str(value).replace('\r\n', '\n') + '\n') - else: - super(DotNetOutput, self).WriteLine.Overloads[object](value) - else: - self.WriteLine(System.String.Format(value, *args)) - - @property - def Encoding(self): - return System.Text.Encoding.UTF8 - - -BACKEND = None - -def _run_repl(): - from optparse import OptionParser - - parser = OptionParser(prog='repl', description='Process REPL options') - parser.add_option('--port', dest='port', - help='the port to connect back to') - parser.add_option('--launch_file', dest='launch_file', - help='the script file to run on startup') - parser.add_option('--execution_mode', dest='backend', - help='the backend to use') - parser.add_option('--enable-attach', dest='enable_attach', - action="store_true", default=False, - help='enable attaching the debugger via $attach') - - (options, args) = parser.parse_args() - - # kick off repl - # make us available under our "normal" name, not just __main__ which we'll likely replace. - sys.modules['visualstudio_py_repl'] = sys.modules['__main__'] - global __name__ - __name__ = 'visualstudio_py_repl' - - backend_type = BasicReplBackend - backend_error = None - if options.backend is not None and options.backend.lower() != 'standard': - try: - split_backend = options.backend.split('.') - backend_mod_name = '.'.join(split_backend[:-1]) - backend_name = split_backend[-1] - backend_type = getattr(__import__(backend_mod_name), backend_name) - except UnsupportedReplException: - backend_error = sys.exc_info()[1].reason - except: - backend_error = traceback.format_exc() - - # fix sys.path so that cwd is where the project lives. - sys.path[0] = '.' - # remove all of our parsed args in case we have a launch file that cares... - sys.argv = args or [''] - - global BACKEND - BACKEND = backend_type(launch_file=options.launch_file) - BACKEND.connect(int(options.port)) - - if options.enable_attach: - BACKEND.init_debugger() - - if backend_error is not None: - sys.stderr.write('Error using selected REPL back-end:\n') - sys.stderr.write(backend_error + '\n') - sys.stderr.write('Using standard backend instead\n') - - # execute code on the main thread which we can interrupt - BACKEND.execution_loop() - -if __name__ == '__main__': - try: - _run_repl() - except: - if DEBUG: - _debug_write(traceback.format_exc()) - _debug_write('exiting') - input() - raise diff --git a/pythonFiles/PythonTools/visualstudio_py_util.py b/pythonFiles/PythonTools/visualstudio_py_util.py deleted file mode 100644 index 49227337b1fb..000000000000 --- a/pythonFiles/PythonTools/visualstudio_py_util.py +++ /dev/null @@ -1,625 +0,0 @@ -# Python Tools for Visual Studio -# Copyright(c) Microsoft Corporation -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the License); you may not use -# this file except in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS -# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY -# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -# MERCHANTABLITY OR NON-INFRINGEMENT. -# -# See the Apache Version 2.0 License for specific language governing -# permissions and limitations under the License. - -__author__ = "Microsoft Corporation " -__version__ = "3.0.0.0" - -# This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) -# attach scenario, it is loaded on the injected debugger attach thread, and if threading module -# hasn't been loaded already, it will assume that the thread on which it is being loaded is the -# main thread. This will cause issues when the thread goes away after attach completes. - -import imp -import os -import sys -import struct - -# Import encodings early to avoid import on the debugger thread, which may cause deadlock -from encodings import utf_8, ascii - -# WARNING: Avoid imports beyond this point, specifically on the debugger thread, as this may cause -# deadlock where the debugger thread performs an import while a user thread has the import lock - -# Py3k compat - alias unicode to str, and xrange to range -try: - unicode -except: - unicode = str -try: - xrange -except: - xrange = range - - -if sys.version_info[0] >= 3: - def to_bytes(cmd_str): - return ascii.Codec.encode(cmd_str)[0] -else: - def to_bytes(cmd_str): - return cmd_str - -def exec_code(code, file, global_variables): - '''Executes the provided code as if it were the original script provided - to python.exe. The functionality is similar to `runpy.run_path`, which was - added in Python 2.7/3.2. - - The following values in `global_variables` will be set to the following - values, if they are not already set:: - __name__ = '' - __file__ = file - __package__ = __name__.rpartition('.')[0] # 2.6 and later - __cached__ = None # 3.2 and later - __loader__ = None # 3.3 and later - - The `sys.modules` entry for ``__name__`` will be set to a new module, and - ``sys.path[0]`` will be changed to the value of `file` without the filename. - Both values are restored when this function exits. - ''' - original_main = sys.modules.get('__main__') - - global_variables = dict(global_variables) - mod_name = global_variables.setdefault('__name__', '') - mod = sys.modules[mod_name] = imp.new_module(mod_name) - mod.__dict__.update(global_variables) - global_variables = mod.__dict__ - global_variables.setdefault('__file__', file) - if sys.version_info[0] >= 3 or sys.version_info[1] >= 6: - global_variables.setdefault('__package__', mod_name.rpartition('.')[0]) - if sys.version_info[0] >= 3: - if sys.version_info[1] >= 2: - global_variables.setdefault('__cached__', None) - if sys.version_info[1] >= 3: - try: - global_variables.setdefault('__loader__', original_main.__loader__) - except AttributeError: - pass - - sys.path[0] = os.path.split(file)[0] - code_obj = compile(code, file, 'exec') - exec(code_obj, global_variables) - -def exec_file(file, global_variables): - '''Executes the provided script as if it were the original script provided - to python.exe. The functionality is similar to `runpy.run_path`, which was - added in Python 2.7/3.2. - - The following values in `global_variables` will be set to the following - values, if they are not already set:: - __name__ = '' - __file__ = file - __package__ = __name__.rpartition('.')[0] # 2.6 and later - __cached__ = None # 3.2 and later - __loader__ = sys.modules['__main__'].__loader__ # 3.3 and later - - The `sys.modules` entry for ``__name__`` will be set to a new module, and - ``sys.path[0]`` will be changed to the value of `file` without the filename. - Both values are restored when this function exits. - ''' - f = open(file, "rb") - try: - code = f.read().replace(to_bytes('\r\n'), to_bytes('\n')) + to_bytes('\n') - finally: - f.close() - exec_code(code, file, global_variables) - -def exec_module(module, global_variables): - '''Executes the provided module as if it were provided as '-m module'. The - functionality is implemented using `runpy.run_module`, which was added in - Python 2.5. - ''' - import runpy - runpy.run_module(module, global_variables, run_name=global_variables.get('__name__'), alter_sys=True) - -UNICODE_PREFIX = to_bytes('U') -ASCII_PREFIX = to_bytes('A') -NONE_PREFIX = to_bytes('N') - - -def read_bytes(conn, count): - b = to_bytes('') - while len(b) < count: - b += conn.recv(count - len(b)) - return b - - -def write_bytes(conn, b): - conn.sendall(b) - - -def read_int(conn): - return struct.unpack('!q', read_bytes(conn, 8))[0] - - -def write_int(conn, i): - write_bytes(conn, struct.pack('!q', i)) - - -def read_string(conn): - """ reads length of text to read, and then the text encoded in UTF-8, and returns the string""" - strlen = read_int(conn) - if not strlen: - return '' - res = to_bytes('') - while len(res) < strlen: - res = res + conn.recv(strlen - len(res)) - - res = utf_8.decode(res)[0] - if sys.version_info[0] == 2 and sys.platform != 'cli': - # Py 2.x, we want an ASCII string if possible - try: - res = ascii.Codec.encode(res)[0] - except UnicodeEncodeError: - pass - - return res - - -def write_string(conn, s): - if s is None: - write_bytes(conn, NONE_PREFIX) - elif isinstance(s, unicode): - b = utf_8.encode(s)[0] - b_len = len(b) - write_bytes(conn, UNICODE_PREFIX) - write_int(conn, b_len) - if b_len > 0: - write_bytes(conn, b) - else: - s_len = len(s) - write_bytes(conn, ASCII_PREFIX) - write_int(conn, s_len) - if s_len > 0: - write_bytes(conn, s) - -class SafeRepr(object): - # String types are truncated to maxstring_outer when at the outer- - # most level, and truncated to maxstring_inner characters inside - # collections. - maxstring_outer = 2 ** 16 - maxstring_inner = 30 - if sys.version_info >= (3, 0): - string_types = (str, bytes) - set_info = (set, '{', '}', False) - frozenset_info = (frozenset, 'frozenset({', '})', False) - else: - string_types = (str, unicode) - set_info = (set, 'set([', '])', False) - frozenset_info = (frozenset, 'frozenset([', '])', False) - - # Collection types are recursively iterated for each limit in - # maxcollection. - maxcollection = (15, 10) - - # Specifies type, prefix string, suffix string, and whether to include a - # comma if there is only one element. (Using a sequence rather than a - # mapping because we use isinstance() to determine the matching type.) - collection_types = [ - (tuple, '(', ')', True), - (list, '[', ']', False), - frozenset_info, - set_info, - ] - try: - from collections import deque - collection_types.append((deque, 'deque([', '])', False)) - except: - pass - - # type, prefix string, suffix string, item prefix string, item key/value separator, item suffix string - dict_types = [(dict, '{', '}', '', ': ', '')] - try: - from collections import OrderedDict - dict_types.append((OrderedDict, 'OrderedDict([', '])', '(', ', ', ')')) - except: - pass - - # All other types are treated identically to strings, but using - # different limits. - maxother_outer = 2 ** 16 - maxother_inner = 30 - - def __call__(self, obj): - try: - return ''.join(self._repr(obj, 0)) - except: - try: - return 'An exception was raised: %r' % sys.exc_info()[1] - except: - return 'An exception was raised' - - def _repr(self, obj, level): - '''Returns an iterable of the parts in the final repr string.''' - - try: - obj_repr = type(obj).__repr__ - except: - obj_repr = None - - def has_obj_repr(t): - r = t.__repr__ - try: - return obj_repr == r - except: - return obj_repr is r - - for t, prefix, suffix, comma in self.collection_types: - if isinstance(obj, t) and has_obj_repr(t): - return self._repr_iter(obj, level, prefix, suffix, comma) - - for t, prefix, suffix, item_prefix, item_sep, item_suffix in self.dict_types: - if isinstance(obj, t) and has_obj_repr(t): - return self._repr_dict(obj, level, prefix, suffix, item_prefix, item_sep, item_suffix) - - for t in self.string_types: - if isinstance(obj, t) and has_obj_repr(t): - return self._repr_str(obj, level) - - if self._is_long_iter(obj): - return self._repr_long_iter(obj) - - return self._repr_other(obj, level) - - # Determines whether an iterable exceeds the limits set in maxlimits, and is therefore unsafe to repr(). - def _is_long_iter(self, obj, level = 0): - try: - # Strings have their own limits (and do not nest). Because they don't have __iter__ in 2.x, this - # check goes before the next one. - if isinstance(obj, self.string_types): - return len(obj) > self.maxstring_inner - - # If it's not an iterable (and not a string), it's fine. - if not hasattr(obj, '__iter__'): - return False - - # Iterable is its own iterator - this is a one-off iterable like generator or enumerate(). We can't - # really count that, but repr() for these should not include any elements anyway, so we can treat it - # the same as non-iterables. - if obj is iter(obj): - return False - - # xrange reprs fine regardless of length. - if isinstance(obj, xrange): - return False - - # numpy and scipy collections (ndarray etc) have self-truncating repr, so they're always safe. - try: - module = type(obj).__module__.partition('.')[0] - if module in ('numpy', 'scipy'): - return False - except: - pass - - # Iterables that nest too deep are considered long. - if level >= len(self.maxcollection): - return True - - # It is too long if the length exceeds the limit, or any of its elements are long iterables. - if hasattr(obj, '__len__'): - try: - l = len(obj) - except: - l = None - if l is not None and l > self.maxcollection[level]: - return True - return any((self._is_long_iter(item, level + 1) for item in obj)) - return any(i > self.maxcollection[level] or self._is_long_iter(item, level + 1) for i, item in enumerate(obj)) - - except: - # If anything breaks, assume the worst case. - return True - - def _repr_iter(self, obj, level, prefix, suffix, comma_after_single_element = False): - yield prefix - - if level >= len(self.maxcollection): - yield '...' - else: - count = self.maxcollection[level] - yield_comma = False - for item in obj: - if yield_comma: - yield ', ' - yield_comma = True - - count -= 1 - if count <= 0: - yield '...' - break - - for p in self._repr(item, 100 if item is obj else level + 1): - yield p - else: - if comma_after_single_element and count == self.maxcollection[level] - 1: - yield ',' - yield suffix - - def _repr_long_iter(self, obj): - try: - obj_repr = '<%s, len() = %s>' % (type(obj).__name__, len(obj)) - except: - try: - obj_repr = '<' + type(obj).__name__ + '>' - except: - obj_repr = '' - yield obj_repr - - def _repr_dict(self, obj, level, prefix, suffix, item_prefix, item_sep, item_suffix): - if not obj: - yield prefix + suffix - return - if level >= len(self.maxcollection): - yield prefix + '...' + suffix - return - - yield prefix - - count = self.maxcollection[level] - yield_comma = False - - try: - sorted_keys = sorted(obj) - except Exception: - sorted_keys = list(obj) - - for key in sorted_keys: - if yield_comma: - yield ', ' - yield_comma = True - - count -= 1 - if count <= 0: - yield '...' - break - - yield item_prefix - for p in self._repr(key, level + 1): - yield p - - yield item_sep - - try: - item = obj[key] - except Exception: - yield '' - else: - for p in self._repr(item, 100 if item is obj else level + 1): - yield p - yield item_suffix - - yield suffix - - def _repr_str(self, obj, level): - return self._repr_obj(obj, level, self.maxstring_inner, self.maxstring_outer) - - def _repr_other(self, obj, level): - return self._repr_obj(obj, level, self.maxother_inner, self.maxother_outer) - - def _repr_obj(self, obj, level, limit_inner, limit_outer): - try: - obj_repr = repr(obj) - except: - try: - obj_repr = object.__repr__(obj) - except: - try: - obj_repr = '' - except: - obj_repr = '' - - limit = limit_inner if level > 0 else limit_outer - - if limit >= len(obj_repr): - yield obj_repr - return - - # Slightly imprecise calculations - we may end up with a string that is - # up to 3 characters longer than limit. If you need precise formatting, - # you are using the wrong class. - left_count, right_count = max(1, int(2 * limit / 3)), max(1, int(limit / 3)) - - yield obj_repr[:left_count] - yield '...' - yield obj_repr[-right_count:] - - - def _selftest(self): - # Test the string limiting somewhat automatically - tests = [] - tests.append((7, 9, 'A' * (5))) - tests.append((self.maxstring_outer + 3, self.maxstring_inner + 3 + 2, 'A' * (self.maxstring_outer + 10))) - if sys.version_info >= (3, 0): - tests.append((self.maxstring_outer + 4, self.maxstring_inner + 4 + 2, bytes('A', 'ascii') * (self.maxstring_outer + 10))) - else: - tests.append((self.maxstring_outer + 4, self.maxstring_inner + 4 + 2, unicode('A') * (self.maxstring_outer + 10))) - - for limit1, limit2, value in tests: - assert len(self(value)) <= limit1 <= len(repr(value)), (len(self(value)), limit1, len(repr(value)), value) - assert len(self([value])) <= limit2 <= len(repr([value])), (len(self([value])), limit2, len(repr([value])), self([value])) - - def test(source, expected): - actual = self(source) - if actual != expected: - print("Source " + repr(source)) - print("Expect " + expected) - print("Actual " + actual) - print("") - assert False - - def re_test(source, pattern): - import re - actual = self(source) - if not re.match(pattern, actual): - print("Source " + repr(source)) - print("Pattern " + pattern) - print("Actual " + actual) - print("") - assert False - - for ctype, _prefix, _suffix, comma in self.collection_types: - for i in range(len(self.maxcollection)): - prefix = _prefix * (i + 1) - if comma: - suffix = _suffix + ("," + _suffix) * i - else: - suffix = _suffix * (i + 1) - #print("ctype = " + ctype.__name__ + ", maxcollection[" + str(i) + "] == " + str(self.maxcollection[i])) - c1 = ctype(range(self.maxcollection[i] - 1)) - inner_repr = prefix + ', '.join(str(j) for j in c1) - c2 = ctype(range(self.maxcollection[i])) - c3 = ctype(range(self.maxcollection[i] + 1)) - for j in range(i): - c1, c2, c3 = ctype((c1,)), ctype((c2,)), ctype((c3,)) - test(c1, inner_repr + suffix) - test(c2, inner_repr + ", ..." + suffix) - test(c3, inner_repr + ", ..." + suffix) - - if ctype is set: - # Cannot recursively add sets to sets - break - - # Assume that all tests apply equally to all iterable types and only - # test with lists. - c1 = list(range(self.maxcollection[0] * 2)) - c2 = [c1 for _ in range(self.maxcollection[0] * 2)] - c1_expect = '[' + ', '.join(str(j) for j in range(self.maxcollection[0] - 1)) + ', ...]' - test(c1, c1_expect) - c1_expect2 = '[' + ', '.join(str(j) for j in range(self.maxcollection[1] - 1)) + ', ...]' - c2_expect = '[' + ', '.join(c1_expect2 for _ in range(self.maxcollection[0] - 1)) + ', ...]' - test(c2, c2_expect) - - # Ensure dict keys and values are limited correctly - d1 = {} - d1_key = 'a' * self.maxstring_inner * 2 - d1[d1_key] = d1_key - re_test(d1, "{'a+\.\.\.a+': 'a+\.\.\.a+'}") - d2 = {d1_key : d1} - re_test(d2, "{'a+\.\.\.a+': {'a+\.\.\.a+': 'a+\.\.\.a+'}}") - d3 = {d1_key : d2} - if len(self.maxcollection) == 2: - re_test(d3, "{'a+\.\.\.a+': {'a+\.\.\.a+': {\.\.\.}}}") - else: - re_test(d3, "{'a+\.\.\.a+': {'a+\.\.\.a+': {'a+\.\.\.a+': 'a+\.\.\.a+'}}}") - - # Ensure empty dicts work - test({}, '{}') - - # Ensure dict keys are sorted - d1 = {} - d1['c'] = None - d1['b'] = None - d1['a'] = None - test(d1, "{'a': None, 'b': None, 'c': None}") - - if sys.version_info >= (3, 0): - # Ensure dicts with unsortable keys do not crash - d1 = {} - for _ in range(100): - d1[object()] = None - try: - list(sorted(d1)) - assert False, "d1.keys() should be unorderable" - except TypeError: - pass - self(d1) - - # Test with objects with broken repr implementations - class TestClass(object): - def __repr__(self): - raise NameError - try: - repr(TestClass()) - assert False, "TestClass().__repr__ should have thrown" - except NameError: - pass - self(TestClass()) - - # Test with objects with long repr implementations - class TestClass(object): - repr_str = '<' + 'A' * self.maxother_outer * 2 + '>' - def __repr__(self): - return self.repr_str - re_test(TestClass(), r'\') - - # Test collections that don't override repr - class TestClass(dict): pass - test(TestClass(), '{}') - class TestClass(list): pass - test(TestClass(), '[]') - - # Test collections that override repr - class TestClass(dict): - def __repr__(self): return 'MyRepr' - test(TestClass(), 'MyRepr') - class TestClass(list): - def __init__(self, iter = ()): list.__init__(self, iter) - def __repr__(self): return 'MyRepr' - test(TestClass(), 'MyRepr') - - # Test collections and iterables with long repr - test(TestClass(xrange(0, 15)), 'MyRepr') - test(TestClass(xrange(0, 16)), '') - test(TestClass([TestClass(xrange(0, 10))]), 'MyRepr') - test(TestClass([TestClass(xrange(0, 11))]), '') - - # Test strings inside long iterables - test(TestClass(['a' * (self.maxcollection[1] + 1)]), 'MyRepr') - test(TestClass(['a' * (self.maxstring_inner + 1)]), '') - - # Test range - if sys.version[0] == '2': - range_name = 'xrange' - else: - range_name = 'range' - test(xrange(1, self.maxcollection[0] + 1), '%s(1, %s)' % (range_name, self.maxcollection[0] + 1)) - - # Test directly recursive collections - c1 = [1, 2] - c1.append(c1) - test(c1, '[1, 2, [...]]') - d1 = {1: None} - d1[2] = d1 - test(d1, '{1: None, 2: {...}}') - - # Find the largest possible repr and ensure it is below our arbitrary - # limit (8KB). - coll = '-' * (self.maxstring_outer * 2) - for limit in reversed(self.maxcollection[1:]): - coll = [coll] * (limit * 2) - dcoll = {} - for i in range(self.maxcollection[0]): - dcoll[str(i) * self.maxstring_outer] = coll - text = self(dcoll) - #try: - # text_repr = repr(dcoll) - #except MemoryError: - # print('Memory error raised while creating repr of test data') - # text_repr = '' - #print('len(SafeRepr()(dcoll)) = ' + str(len(text)) + ', len(repr(coll)) = ' + str(len(text_repr))) - assert len(text) < 8192 - - # Test numpy types - they should all use their native reprs, even arrays exceeding limits - try: - import numpy as np - except ImportError: - print('WARNING! could not import numpy - skipping all numpy tests.') - else: - test(np.int32(123), repr(np.int32(123))) - test(np.float64(123.456), repr(np.float64(123.456))) - test(np.zeros(self.maxcollection[0] + 1), repr(np.zeros(self.maxcollection[0] + 1))); - -if __name__ == '__main__': - print('Running tests...') - SafeRepr()._selftest() diff --git a/pythonFiles/completion.py b/pythonFiles/completion.py index 866a527c36a4..fd8a15d8df28 100644 --- a/pythonFiles/completion.py +++ b/pythonFiles/completion.py @@ -1,48 +1,76 @@ import os +import os.path import io import re import sys import json import traceback -sys.path.append(os.path.dirname(__file__)) -import jedi -# remove jedi from path after we import it so it will not be completed -sys.path.pop(0) +import platform -WORD_RE = re.compile(r'\w') +jediPreview = False + + +class RedirectStdout(object): + def __init__(self, new_stdout=None): + """If stdout is None, redirect to /dev/null""" + self._new_stdout = new_stdout or open(os.devnull, "w") + + def __enter__(self): + sys.stdout.flush() + self.oldstdout_fno = os.dup(sys.stdout.fileno()) + os.dup2(self._new_stdout.fileno(), 1) + + def __exit__(self, exc_type, exc_value, traceback): + self._new_stdout.flush() + os.dup2(self.oldstdout_fno, 1) + os.close(self.oldstdout_fno) class JediCompletion(object): basic_types = { - 'module': 'import', - 'instance': 'variable', - 'statement': 'value', - 'param': 'variable', + "module": "import", + "instance": "variable", + "statement": "value", + "param": "variable", } def __init__(self): self.default_sys_path = sys.path - self._input = io.open(sys.stdin.fileno(), encoding='utf-8') + self._input = io.open(sys.stdin.fileno(), encoding="utf-8") + if (os.path.sep == "/") and (platform.uname()[2].find("Microsoft") > -1): + # WSL; does not support UNC paths + self.drive_mount = "/mnt/" + elif sys.platform == "cygwin": + # cygwin + self.drive_mount = "/cygdrive/" + else: + # Do no normalization, e.g. Windows build of Python. + # Could add additional test: ((os.path.sep == '/') and os.path.isdir('/mnt/c')) + # However, this may have more false positives trying to identify Windows/*nix hybrids + self.drive_mount = "" def _get_definition_type(self, definition): - is_built_in = definition.in_builtin_module - if definition.type not in ['import', 'keyword'] and is_built_in(): - return 'builtin' - if definition.type in ['statement'] and definition.name.isupper(): - return 'constant' - return self.basic_types.get(definition.type, definition.type) + # if definition.type not in ['import', 'keyword'] and is_built_in(): + # return 'builtin' + try: + if definition.type in ["statement"] and definition.name.isupper(): + return "constant" + return self.basic_types.get(definition.type, definition.type) + except Exception: + return "builtin" def _additional_info(self, completion): """Provide additional information about the completion object.""" - if completion._definition is None: - return '' - if completion.type == 'statement': - nodes_to_display = ['InstanceElement', 'String', 'Node', 'Lambda', - 'Number'] - return ''.join(c.get_code() for c in - completion._definition.children if type(c).__name__ - in nodes_to_display).replace('\n', '') - return '' + if not hasattr(completion, "_definition") or completion._definition is None: + return "" + if completion.type == "statement": + nodes_to_display = ["InstanceElement", "String", "Node", "Lambda", "Number"] + return "".join( + c.get_code() + for c in completion._definition.children + if type(c).__name__ in nodes_to_display + ).replace("\n", "") + return "" @classmethod def _get_top_level_module(cls, path): @@ -53,18 +81,18 @@ def _get_top_level_module(cls, path): is to find the higher python module available from filepath. """ _path, _ = os.path.split(path) - if os.path.isfile(os.path.join(_path, '__init__.py')): + if os.path.isfile(os.path.join(_path, "__init__.py")): return cls._get_top_level_module(_path) return path def _generate_signature(self, completion): - """Generate signature with function arguments. - """ - if not hasattr(completion, 'params'): - return '' - return '%s(%s)' % ( + """Generate signature with function arguments.""" + if completion.type in ["module"] or not hasattr(completion, "params"): + return "" + return "%s(%s)" % ( completion.name, - ', '.join(param.description for param in completion.params)) + ", ".join(p.description[6:] for p in completion.params if p), + ) def _get_call_signatures(self, script): """Extract call signatures from jedi.api.Script object in failsafe way. @@ -77,25 +105,93 @@ def _get_call_signatures(self, script): call_signatures = script.call_signatures() except KeyError: call_signatures = [] + except: + call_signatures = [] for signature in call_signatures: for pos, param in enumerate(signature.params): if not param.name: continue - if param.name == 'self' and pos == 0: - continue - if WORD_RE.match(param.name) is None: + + name = self._get_param_name(param) + if param.name == "self" and pos == 0: continue - try: - name, value = param.description.split('=') - except ValueError: - name = param.description - value = None - if name.startswith('*'): + if name.startswith("*"): continue + + value = self._get_param_value(param) _signatures.append((signature, name, value)) return _signatures - def _serialize_completions(self, script, identifier=None, prefix=''): + def _get_param_name(self, p): + if p.name.startswith("param "): + return p.name[6:] # drop leading 'param ' + return p.name + + def _get_param_value(self, p): + pair = p.description.split("=") + if len(pair) > 1: + return pair[1] + return None + + def _get_call_signatures_with_args(self, script): + """Extract call signatures from jedi.api.Script object in failsafe way. + + Returns: + Array with dictionary + """ + _signatures = [] + try: + call_signatures = script.call_signatures() + except KeyError: + call_signatures = [] + for signature in call_signatures: + sig = { + "name": "", + "description": "", + "docstring": "", + "paramindex": 0, + "params": [], + "bracketstart": [], + } + sig["description"] = signature.description + try: + sig["docstring"] = signature.docstring() + sig["raw_docstring"] = signature.docstring(raw=True) + except Exception: + sig["docstring"] = "" + sig["raw_docstring"] = "" + + sig["name"] = signature.name + sig["paramindex"] = signature.index + sig["bracketstart"].append(signature.index) + + _signatures.append(sig) + for pos, param in enumerate(signature.params): + if not param.name: + continue + + name = self._get_param_name(param) + if param.name == "self" and pos == 0: + continue + + value = self._get_param_value(param) + paramDocstring = "" + try: + paramDocstring = param.docstring() + except Exception: + paramDocstring = "" + + sig["params"].append( + { + "name": name, + "value": value, + "docstring": paramDocstring, + "description": param.description, + } + ) + return _signatures + + def _serialize_completions(self, script, identifier=None, prefix=""): """Serialize response to be read from VSCode. Args: @@ -110,49 +206,88 @@ def _serialize_completions(self, script, identifier=None, prefix=''): _completions = [] for signature, name, value in self._get_call_signatures(script): - if not self.fuzzy_matcher and not name.lower().startswith( - prefix.lower()): + if not self.fuzzy_matcher and not name.lower().startswith(prefix.lower()): continue _completion = { - 'type': 'property', - 'rightLabel': self._additional_info(signature) + "type": "property", + "raw_type": "", + "rightLabel": self._additional_info(signature), } + _completion["description"] = "" + _completion["raw_docstring"] = "" + # we pass 'text' here only for fuzzy matcher if value: - _completion['snippet'] = '%s=${1:%s}$0' % (name, value) - _completion['text'] = '%s=%s' % (name, value) - else: - _completion['snippet'] = '%s=$1$0' % name - _completion['text'] = name - _completion['displayText'] = name - if self.show_doc_strings: - _completion['description'] = signature.docstring() + _completion["snippet"] = "%s=${1:%s}$0" % (name, value) + _completion["text"] = "%s=" % (name) else: - _completion['description'] = self._generate_signature( - signature) + _completion["snippet"] = "%s=$1$0" % name + _completion["text"] = name + _completion["displayText"] = name _completions.append(_completion) try: completions = script.completions() except KeyError: completions = [] + except: + completions = [] for completion in completions: - if self.show_doc_strings: - description = completion.docstring() - else: - description = self._generate_signature(completion) - _completion = { - 'text': completion.name, - 'type': self._get_definition_type(completion), - 'description': description, - 'rightLabel': self._additional_info(completion) - } - if any([c['text'].split('=')[0] == _completion['text'] - for c in _completions]): - # ignore function arguments we already have - continue + try: + _completion = { + "text": completion.name, + "type": self._get_definition_type(completion), + "raw_type": completion.type, + "rightLabel": self._additional_info(completion), + } + except Exception: + continue + + for c in _completions: + if c["text"] == _completion["text"]: + c["type"] = _completion["type"] + c["raw_type"] = _completion["raw_type"] + + if any( + [c["text"].split("=")[0] == _completion["text"] for c in _completions] + ): + # ignore function arguments we already have + continue _completions.append(_completion) - return json.dumps({'id': identifier, 'results': _completions}) + return json.dumps({"id": identifier, "results": _completions}) + + def _serialize_methods(self, script, identifier=None, prefix=""): + _methods = [] + try: + completions = script.completions() + except KeyError: + return [] + + for completion in completions: + if completion.name == "__autocomplete_python": + instance = completion.parent().name + break + else: + instance = "self.__class__" + + for completion in completions: + params = [] + if hasattr(completion, "params"): + params = [p.description for p in completion.params if p] + if completion.parent().type == "class": + _methods.append( + { + "parent": completion.parent().name, + "instance": instance, + "name": completion.name, + "params": params, + "moduleName": completion.module_name, + "fileName": completion.module_path, + "line": completion.line, + "column": completion.column, + } + ) + return json.dumps({"id": identifier, "results": _methods}) def _serialize_arguments(self, script, identifier=None): """Serialize response to be read from VSCode. @@ -164,25 +299,74 @@ def _serialize_arguments(self, script, identifier=None): Returns: Serialized string to send to VSCode. """ - seen = set() - arguments = [] - i = 1 - for _, name, value in self._get_call_signatures(script): - if not value: - arg = '${%s:%s}' % (i, name) - elif self.use_snippets == 'all': - arg = '%s=${%s:%s}' % (name, i, value) + return json.dumps( + {"id": identifier, "results": self._get_call_signatures_with_args(script)} + ) + + def _top_definition(self, definition): + for d in definition.goto_assignments(): + if d == definition: + continue + if d.type == "import": + return self._top_definition(d) else: - continue - if name not in seen: - seen.add(name) - arguments.append(arg) - i += 1 - snippet = '%s$0' % ', '.join(arguments) - return json.dumps({'id': identifier, 'results': [], - 'arguments': snippet}) + return d + return definition - def _serialize_definitions(self, definitions, identifier=None): + def _extract_range_jedi_0_11_1(self, definition): + from parso.utils import split_lines + + # get the scope range + try: + if definition.type in ["class", "function"]: + tree_name = definition._name.tree_name + scope = tree_name.get_definition() + start_line = scope.start_pos[0] - 1 + start_column = scope.start_pos[1] + # get the lines + code = scope.get_code(include_prefix=False) + lines = split_lines(code) + # trim the lines + lines = "\n".join(lines).rstrip().split("\n") + end_line = start_line + len(lines) - 1 + end_column = len(lines[-1]) - 1 + else: + symbol = definition._name.tree_name + start_line = symbol.start_pos[0] - 1 + start_column = symbol.start_pos[1] + end_line = symbol.end_pos[0] - 1 + end_column = symbol.end_pos[1] + return { + "start_line": start_line, + "start_column": start_column, + "end_line": end_line, + "end_column": end_column, + } + except Exception as e: + return { + "start_line": definition.line - 1, + "start_column": definition.column, + "end_line": definition.line - 1, + "end_column": definition.column, + } + + def _extract_range(self, definition): + """Provides the definition range of a given definition + + For regular symbols it returns the start and end location of the + characters making up the symbol. + + For scoped containers it will return the entire definition of the + scope. + + The scope that jedi provides ends with the first character of the next + scope so it's not ideal. For vscode we need the scope to end with the + last character of actual code. That's why we extract the lines that + make up our scope and trim the trailing whitespace. + """ + return self._extract_range_jedi_0_11_1(definition) + + def _get_definitionsx(self, definitions, identifier=None, ignoreNoModulePath=False): """Serialize response to be read from VSCode. Args: @@ -192,47 +376,145 @@ def _serialize_definitions(self, definitions, identifier=None): Returns: Serialized string to send to VSCode. """ + _definitions = [] + for definition in definitions: + try: + if definition.type == "import": + definition = self._top_definition(definition) + definitionRange = { + "start_line": 0, + "start_column": 0, + "end_line": 0, + "end_column": 0, + } + module_path = "" + if hasattr(definition, "module_path") and definition.module_path: + module_path = definition.module_path + definitionRange = self._extract_range(definition) + else: + if not ignoreNoModulePath: + continue + try: + parent = definition.parent() + container = parent.name if parent.type != "module" else "" + except Exception: + container = "" - def _top_definition(definition): - for d in definition.goto_assignments(): - if d == definition: - continue - if d.type == 'import': - return _top_definition(d) - else: - return d - return definition + try: + docstring = definition.docstring() + rawdocstring = definition.docstring(raw=True) + except Exception: + docstring = "" + rawdocstring = "" + _definition = { + "text": definition.name, + "type": self._get_definition_type(definition), + "raw_type": definition.type, + "fileName": module_path, + "container": container, + "range": definitionRange, + "description": definition.description, + "docstring": docstring, + "raw_docstring": rawdocstring, + "signature": self._generate_signature(definition), + } + _definitions.append(_definition) + except Exception as e: + pass + return _definitions + + def _serialize_definitions(self, definitions, identifier=None): + """Serialize response to be read from VSCode. + + Args: + definitions: List of jedi.api.classes.Definition objects. + identifier: Unique completion identifier to pass back to VSCode. + Returns: + Serialized string to send to VSCode. + """ _definitions = [] for definition in definitions: try: if definition.module_path: - if definition.type == 'import': - definition = _top_definition(definition) - + if definition.type == "import": + definition = self._top_definition(definition) + if not definition.module_path: + continue + try: + parent = definition.parent() + container = parent.name if parent.type != "module" else "" + except Exception: + container = "" + + try: + docstring = definition.docstring() + rawdocstring = definition.docstring(raw=True) + except Exception: + docstring = "" + rawdocstring = "" _definition = { - 'text': definition.name, - 'type': self._get_definition_type(definition), - 'fileName': definition.module_path, - 'line': definition.line - 1, - 'column': definition.column + "text": definition.name, + "type": self._get_definition_type(definition), + "raw_type": definition.type, + "fileName": definition.module_path, + "container": container, + "range": self._extract_range(definition), + "description": definition.description, + "docstring": docstring, + "raw_docstring": rawdocstring, } _definitions.append(_definition) except Exception as e: pass - return json.dumps({'id': identifier, 'results': _definitions}) + return json.dumps({"id": identifier, "results": _definitions}) + + def _serialize_tooltip(self, definitions, identifier=None): + _definitions = [] + for definition in definitions: + signature = definition.name + description = None + if definition.type in ["class", "function"]: + signature = self._generate_signature(definition) + try: + description = definition.docstring(raw=True).strip() + except Exception: + description = "" + if not description and not hasattr(definition, "get_line_code"): + # jedi returns an empty string for compiled objects + description = definition.docstring().strip() + if definition.type == "module": + signature = definition.full_name + try: + description = definition.docstring(raw=True).strip() + except Exception: + description = "" + if not description and hasattr(definition, "get_line_code"): + # jedi returns an empty string for compiled objects + description = definition.docstring().strip() + _definition = { + "type": self._get_definition_type(definition), + "text": definition.name, + "description": description, + "docstring": description, + "signature": signature, + } + _definitions.append(_definition) + return json.dumps({"id": identifier, "results": _definitions}) def _serialize_usages(self, usages, identifier=None): - _usages = [] - for usage in usages: - _usages.append({ - 'name': usage.name, - 'moduleName': usage.module_name, - 'fileName': usage.module_path, - 'line': usage.line, - 'column': usage.column, - }) - return json.dumps({'id': identifier, 'results': _usages}) + _usages = [] + for usage in usages: + _usages.append( + { + "name": usage.name, + "moduleName": usage.module_name, + "fileName": usage.module_path, + "line": usage.line, + "column": usage.column, + } + ) + return json.dumps({"id": identifier, "results": _usages}) def _deserialize(self, request): """Deserialize request from VSCode. @@ -256,64 +538,157 @@ def _set_request_config(self, config): config: Dictionary with config values. """ sys.path = self.default_sys_path - self.use_snippets = config.get('useSnippets') - self.show_doc_strings = config.get('showDescriptions', True) - self.fuzzy_matcher = config.get('fuzzyMatcher', False) + self.use_snippets = config.get("useSnippets") + self.show_doc_strings = config.get("showDescriptions", True) + self.fuzzy_matcher = config.get("fuzzyMatcher", False) jedi.settings.case_insensitive_completion = config.get( - 'caseInsensitiveCompletion', True) - for path in config.get('extraPaths', []): + "caseInsensitiveCompletion", True + ) + for path in config.get("extraPaths", []): if path and path not in sys.path: sys.path.insert(0, path) - def _process_request(self, request): - """Accept serialized request from VSCode and write response. + def _normalize_request_path(self, request): + """Normalize any Windows paths received by a *nix build of + Python. Does not alter the reverse os.path.sep=='\\', + i.e. *nix paths received by a Windows build of Python. """ + if "path" in request: + if not self.drive_mount: + return + newPath = request["path"].replace("\\", "/") + if newPath[0:1] == "/": + # is absolute path with no drive letter + request["path"] = newPath + elif newPath[1:2] == ":": + # is path with drive letter, only absolute can be mapped + request["path"] = self.drive_mount + newPath[0:1].lower() + newPath[2:] + else: + # is relative path + request["path"] = newPath + + def _process_request(self, request): + """Accept serialized request from VSCode and write response.""" request = self._deserialize(request) - self._set_request_config(request.get('config', {})) + self._set_request_config(request.get("config", {})) - path = self._get_top_level_module(request.get('path', '')) - if path not in sys.path: + self._normalize_request_path(request) + path = self._get_top_level_module(request.get("path", "")) + if len(path) > 0 and path not in sys.path: sys.path.insert(0, path) - lookup = request.get('lookup', 'completions') + lookup = request.get("lookup", "completions") - if lookup == 'names': - return self._write_response(self._serialize_definitions( + if lookup == "names": + return self._serialize_definitions( jedi.api.names( - source=request['source'], - path=request.get('path', ''), - all_scopes=True), - request['id'])) - - script = jedi.api.Script( - source=request['source'], line=request['line'] + 1, - column=request['column'], path=request.get('path', '')) - - if lookup == 'definitions': - return self._write_response(self._serialize_definitions( - script.goto_assignments(), request['id'])) - elif lookup == 'arguments': - return self._write_response(self._serialize_arguments( - script, request['id'])) - elif lookup == 'usages': - return self._write_response(self._serialize_usages( - script.usages(), request['id'])) + source=request.get("source", None), + path=request.get("path", ""), + all_scopes=True, + ), + request["id"], + ) + + script = jedi.Script( + source=request.get("source", None), + line=request["line"] + 1, + column=request["column"], + path=request.get("path", ""), + project=jedi.get_default_project(os.path.dirname(path)), + sys_path=sys.path, + ) + + if lookup == "definitions": + defs = self._get_definitionsx( + script.goto_assignments(follow_imports=True), request["id"] + ) + return json.dumps({"id": request["id"], "results": defs}) + if lookup == "tooltip": + if jediPreview: + defs = [] + try: + defs = self._get_definitionsx( + script.goto_definitions(), request["id"], True + ) + except: + pass + try: + if len(defs) == 0: + defs = self._get_definitionsx( + script.goto_assignments(), request["id"], True + ) + except: + pass + return json.dumps({"id": request["id"], "results": defs}) + else: + try: + return self._serialize_tooltip( + script.goto_definitions(), request["id"] + ) + except: + return json.dumps({"id": request["id"], "results": []}) + elif lookup == "arguments": + return self._serialize_arguments(script, request["id"]) + elif lookup == "usages": + return self._serialize_usages(script.usages(), request["id"]) + elif lookup == "methods": + return self._serialize_methods( + script, request["id"], request.get("prefix", "") + ) else: - return self._write_response( - self._serialize_completions(script, request['id'], - request.get('prefix', ''))) + return self._serialize_completions( + script, request["id"], request.get("prefix", "") + ) def _write_response(self, response): - sys.stdout.write(response + '\n') + sys.stdout.write(response + "\n") sys.stdout.flush() def watch(self): while True: try: - self._process_request(self._input.readline()) + rq = self._input.readline() + if len(rq) == 0: + # Reached EOF - indication our parent process is gone. + sys.stderr.write( + "Received EOF from the standard input,exiting" + "\n" + ) + sys.stderr.flush() + return + with RedirectStdout(): + response = self._process_request(rq) + self._write_response(response) + except Exception: - sys.stderr.write(traceback.format_exc() + '\n') + sys.stderr.write(traceback.format_exc() + "\n") sys.stderr.flush() -if __name__ == '__main__': + +if __name__ == "__main__": + cachePrefix = "v" + modulesToLoad = "" + if len(sys.argv) > 2 and sys.argv[1] == "custom": + jediPath = sys.argv[2] + jediPreview = True + cachePrefix = "custom_v" + if len(sys.argv) > 3: + modulesToLoad = sys.argv[3] + else: + # release + jediPath = os.path.join(os.path.dirname(__file__), "lib", "python") + if len(sys.argv) > 1: + modulesToLoad = sys.argv[1] + + sys.path.insert(0, jediPath) + import jedi + + if jediPreview: + jedi.settings.cache_directory = os.path.join( + jedi.settings.cache_directory, + cachePrefix + jedi.__version__.replace(".", ""), + ) + # remove jedi from path after we import it so it will not be completed + sys.path.pop(0) + if len(modulesToLoad) > 0: + jedi.preload_module(*modulesToLoad.split(",")) JediCompletion().watch() diff --git a/pythonFiles/formatYapf.py b/pythonFiles/formatYapf.py deleted file mode 100644 index 87684e34039e..000000000000 --- a/pythonFiles/formatYapf.py +++ /dev/null @@ -1,2 +0,0 @@ -import yapf -yapf.run_main() diff --git a/pythonFiles/install_debugpy.py b/pythonFiles/install_debugpy.py new file mode 100644 index 000000000000..cb21efd91927 --- /dev/null +++ b/pythonFiles/install_debugpy.py @@ -0,0 +1,63 @@ +import io +import json +import os +import urllib.request as url_lib +import zipfile +from packaging.version import parse as version_parser + + +EXTENSION_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python") +DEBUGGER_PACKAGE = "debugpy" +DEBUGGER_PYTHON_VERSIONS = ("cp37",) + + +def _contains(s, parts=()): + return any(p for p in parts if p in s) + + +def _get_package_data(): + json_uri = "https://pypi.org/pypi/{0}/json".format(DEBUGGER_PACKAGE) + # Response format: https://warehouse.readthedocs.io/api-reference/json/#project + # Release metadata format: https://github.com/pypa/interoperability-peps/blob/master/pep-0426-core-metadata.rst + with url_lib.urlopen(json_uri) as response: + return json.loads(response.read()) + + +def _get_debugger_wheel_urls(data, version): + return list( + r["url"] + for r in data["releases"][version] + if _contains(r["url"], DEBUGGER_PYTHON_VERSIONS) + ) + + +def _download_and_extract(root, url, version): + root = os.getcwd() if root is None or root == "." else root + prefix = os.path.join("debugpy-{0}.data".format(version), "purelib") + with url_lib.urlopen(url) as response: + # Extract only the contents of the purelib subfolder (parent folder of debugpy), + # since debugpy files rely on the presence of a 'debugpy' folder. + with zipfile.ZipFile(io.BytesIO(response.read()), "r") as wheel: + for zip_info in wheel.infolist(): + # Ignore dist info since we are merging multiple wheels + if ".dist-info" in zip_info.filename: + continue + # Normalize path for Windows, the wheel folder structure + # uses forward slashes. + normalized = os.path.normpath(zip_info.filename) + # Flatten the folder structure. + zip_info.filename = normalized.split(prefix)[-1] + wheel.extract(zip_info, root) + + +def main(root): + data = _get_package_data() + latest_version = max(data["releases"].keys(), key=version_parser) + + for url in _get_debugger_wheel_urls(data, latest_version): + _download_and_extract(root, url, latest_version) + + +if __name__ == "__main__": + main(DEBUGGER_DEST) diff --git a/pythonFiles/interpreterInfo.py b/pythonFiles/interpreterInfo.py new file mode 100644 index 000000000000..bb284e729d71 --- /dev/null +++ b/pythonFiles/interpreterInfo.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import sys + +obj = {} +obj["versionInfo"] = tuple(sys.version_info) +obj["sysPrefix"] = sys.prefix +obj["version"] = sys.version +obj["is64Bit"] = sys.maxsize > 2 ** 32 + +print(json.dumps(obj)) diff --git a/pythonFiles/isort/__init__.py b/pythonFiles/isort/__init__.py deleted file mode 100644 index 0f09c4a43299..000000000000 --- a/pythonFiles/isort/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -"""__init__.py. - -Defines the isort module to include the SortImports utility class as well as any defined settings. - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -""" - -from __future__ import absolute_import, division, print_function, unicode_literals - -from . import settings -from .isort import SortImports - -__version__ = "4.2.2" diff --git a/pythonFiles/isort/hooks.py b/pythonFiles/isort/hooks.py deleted file mode 100644 index 15b6d4087c96..000000000000 --- a/pythonFiles/isort/hooks.py +++ /dev/null @@ -1,82 +0,0 @@ -"""isort.py. - -Defines a git hook to allow pre-commit warnings and errors about import order. - -usage: - exit_code = git_hook(strict=True) - -Copyright (C) 2015 Helen Sherwood-Taylor - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -""" -import subprocess - -from isort import SortImports - - -def get_output(command): - """ - Run a command and return raw output - - :param str command: the command to run - :returns: the stdout output of the command - """ - return subprocess.check_output(command.split()) - - -def get_lines(command): - """ - Run a command and return lines of output - - :param str command: the command to run - :returns: list of whitespace-stripped lines output by command - """ - stdout = get_output(command) - return [line.strip().decode('utf-8') for line in stdout.splitlines()] - - -def git_hook(strict=False): - """ - Git pre-commit hook to check staged files for isort errors - - :param bool strict - if True, return number of errors on exit, - causing the hook to fail. If False, return zero so it will - just act as a warning. - - :return number of errors if in strict mode, 0 otherwise. - """ - - # Get list of files modified and staged - diff_cmd = "git diff-index --cached --name-only --diff-filter=ACMRTUXB HEAD" - files_modified = get_lines(diff_cmd) - - errors = 0 - for filename in files_modified: - if filename.endswith('.py'): - # Get the staged contents of the file - staged_cmd = "git show :%s" % filename - staged_contents = get_output(staged_cmd) - - sort = SortImports( - file_path=filename, - file_contents=staged_contents.decode(), - check=True - ) - - if sort.incorrectly_sorted: - errors += 1 - - return errors if strict else 0 diff --git a/pythonFiles/isort/isort.py b/pythonFiles/isort/isort.py deleted file mode 100644 index cc956eb1ef97..000000000000 --- a/pythonFiles/isort/isort.py +++ /dev/null @@ -1,866 +0,0 @@ -"""isort.py. - -Exposes a simple library to sort through imports within Python code - -usage: - SortImports(file_name) -or: - sorted = SortImports(file_contents=file_contents).output - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -""" -from __future__ import absolute_import, division, print_function, unicode_literals - -import codecs -import copy -import io -import itertools -import os -import re -import sys -from collections import namedtuple -from datetime import datetime -from difflib import unified_diff -from glob import glob -from sys import path as PYTHONPATH -from sys import stdout - -from . import settings -from .natural import nsorted -from .pie_slice import * - -KNOWN_SECTION_MAPPING = { - 'STDLIB': 'STANDARD_LIBRARY', - 'FUTURE': 'FUTURE_LIBRARY', - 'FIRSTPARTY': 'FIRST_PARTY', - 'THIRDPARTY': 'THIRD_PARTY', -} - - -class SortImports(object): - incorrectly_sorted = False - skipped = False - - def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, check=False, - show_diff=False, settings_path=None, ask_to_apply=False, **setting_overrides): - if not settings_path and file_path: - settings_path = os.path.dirname(os.path.abspath(file_path)) - settings_path = settings_path or os.getcwd() - - self.config = settings.from_path(settings_path).copy() - for key, value in itemsview(setting_overrides): - access_key = key.replace('not_', '').lower() - # The sections config needs to retain order and can't be converted to a set. - if access_key != 'sections' and type(self.config.get(access_key)) in (list, tuple): - if key.startswith('not_'): - self.config[access_key] = list(set(self.config[access_key]).difference(value)) - else: - self.config[access_key] = list(set(self.config[access_key]).union(value)) - else: - self.config[key] = value - - indent = str(self.config['indent']) - if indent.isdigit(): - indent = " " * int(indent) - else: - indent = indent.strip("'").strip('"') - if indent.lower() == "tab": - indent = "\t" - self.config['indent'] = indent - - self.place_imports = {} - self.import_placements = {} - self.remove_imports = [self._format_simplified(removal) for removal in self.config.get('remove_imports', [])] - self.add_imports = [self._format_natural(addition) for addition in self.config.get('add_imports', [])] - self._section_comments = ["# " + value for key, value in itemsview(self.config) if - key.startswith('import_heading') and value] - - self.file_encoding = 'utf-8' - file_name = file_path - self.file_path = file_path or "" - if file_path: - file_path = os.path.abspath(file_path) - if settings.should_skip(file_path, self.config): - self.skipped = True - if self.config['verbose']: - print("WARNING: {0} was skipped as it's listed in 'skip' setting" - " or matches a glob in 'skip_glob' setting".format(file_path)) - file_contents = None - elif not file_contents: - self.file_path = file_path - self.file_encoding = coding_check(file_path) - with codecs.open(file_path, encoding=self.file_encoding) as file_to_import_sort: - file_contents = file_to_import_sort.read() - - if file_contents is None or ("isort:" + "skip_file") in file_contents: - return - - self.in_lines = file_contents.split("\n") - self.original_length = len(self.in_lines) - if (self.original_length > 1 or self.in_lines[:1] not in ([], [""])) or self.config.get('force_adds', False): - for add_import in self.add_imports: - self.in_lines.append(add_import) - self.number_of_lines = len(self.in_lines) - - self.out_lines = [] - self.comments = {'from': {}, 'straight': {}, 'nested': {}, 'above': {'straight': {}, 'from': {}}} - self.imports = {} - self.as_map = {} - - section_names = self.config.get('sections') - self.sections = namedtuple('Sections', section_names)(*[n for n in section_names]) - for section in itertools.chain(self.sections, self.config['forced_separate']): - self.imports[section] = {'straight': set(), 'from': {}} - - self.index = 0 - self.import_index = -1 - self._first_comment_index_start = -1 - self._first_comment_index_end = -1 - self._parse() - if self.import_index != -1: - self._add_formatted_imports() - - self.length_change = len(self.out_lines) - self.original_length - while self.out_lines and self.out_lines[-1].strip() == "": - self.out_lines.pop(-1) - self.out_lines.append("") - - self.output = "\n".join(self.out_lines) - if self.config.get('atomic', False): - try: - compile(self._strip_top_comments(self.out_lines), self.file_path, 'exec', 0, 1) - except SyntaxError: - self.output = file_contents - self.incorrectly_sorted = True - try: - compile(self._strip_top_comments(self.in_lines), self.file_path, 'exec', 0, 1) - print("ERROR: {0} isort would have introduced syntax errors, please report to the project!". \ - format(self.file_path)) - except SyntaxError: - print("ERROR: {0} File contains syntax errors.".format(self.file_path)) - - return - if check: - if self.output.replace("\n", "").replace(" ", "") == file_contents.replace("\n", "").replace(" ", ""): - if self.config['verbose']: - print("SUCCESS: {0} Everything Looks Good!".format(self.file_path)) - else: - print("ERROR: {0} Imports are incorrectly sorted.".format(self.file_path)) - self.incorrectly_sorted = True - if show_diff or self.config.get('show_diff', False) is True: - self._show_diff(file_contents) - return - - if show_diff or self.config.get('show_diff', False) is True: - self._show_diff(file_contents) - elif write_to_stdout: - stdout.write(self.output) - elif file_name: - if ask_to_apply: - if self.output == file_contents: - return - self._show_diff(file_contents) - answer = None - while answer not in ('yes', 'y', 'no', 'n', 'quit', 'q'): - answer = input("Apply suggested changes to '{0}' [Y/n/q]?".format(self.file_path)).lower() - if answer in ('no', 'n'): - return - if answer in ('quit', 'q'): - sys.exit(1) - with codecs.open(self.file_path, encoding=self.file_encoding, mode='w') as output_file: - output_file.write(self.output) - - def _show_diff(self, file_contents): - for line in unified_diff( - file_contents.splitlines(1), - self.output.splitlines(1), - fromfile=self.file_path + ':before', - tofile=self.file_path + ':after', - fromfiledate=str(datetime.fromtimestamp(os.path.getmtime(self.file_path)) - if self.file_path else datetime.now()), - tofiledate=str(datetime.now()) - ): - stdout.write(line) - - @staticmethod - def _strip_top_comments(lines): - """Strips # comments that exist at the top of the given lines""" - lines = copy.copy(lines) - while lines and lines[0].startswith("#"): - lines = lines[1:] - return "\n".join(lines) - - def place_module(self, moduleName): - """Tries to determine if a module is a python std import, third party import, or project code: - - if it can't determine - it assumes it is project code - - """ - for forced_separate in self.config['forced_separate']: - if moduleName.startswith(forced_separate) or moduleName.startswith("." + forced_separate): - return forced_separate - - if moduleName.startswith("."): - return self.sections.LOCALFOLDER - - # Try to find most specific placement instruction match (if any) - parts = moduleName.split('.') - module_names_to_check = ['.'.join(parts[:first_k]) for first_k in range(len(parts), 0, -1)] - for module_name_to_check in module_names_to_check: - for placement in reversed(self.sections): - known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) - config_key = 'known_{0}'.format(known_placement.lower()) - if module_name_to_check in self.config.get(config_key, []): - return placement - - paths = PYTHONPATH - virtual_env = self.config.get('virtual_env') or os.environ.get('VIRTUAL_ENV') - if virtual_env: - paths += [p for p in glob("{0}/lib/python*/site-packages".format(virtual_env)) - if p not in paths] - - for prefix in paths: - module_path = "/".join((prefix, moduleName.replace(".", "/"))) - package_path = "/".join((prefix, moduleName.split(".")[0])) - if (os.path.exists(module_path + ".py") or os.path.exists(module_path + ".so") or - (os.path.exists(package_path) and os.path.isdir(package_path))): - if "site-packages" in prefix or "dist-packages" in prefix: - return self.sections.THIRDPARTY - elif "python2" in prefix.lower() or "python3" in prefix.lower(): - return self.sections.STDLIB - else: - return self.config['default_section'] - - return self.config['default_section'] - - def _get_line(self): - """Returns the current line from the file while incrementing the index.""" - line = self.in_lines[self.index] - self.index += 1 - return line - - @staticmethod - def _import_type(line): - """If the current line is an import line it will return its type (from or straight)""" - if "isort:skip" in line: - return - elif line.startswith('import '): - return "straight" - elif line.startswith('from '): - return "from" - - def _at_end(self): - """returns True if we are at the end of the file.""" - return self.index == self.number_of_lines - - @staticmethod - def _module_key(module_name, config, sub_imports=False): - prefix = "" - module_name = str(module_name) - if sub_imports and config['order_by_type']: - if module_name.isupper() and len(module_name) > 1: - prefix = "A" - elif module_name[0:1].isupper(): - prefix = "B" - else: - prefix = "C" - module_name = module_name.lower() - return "{0}{1}{2}".format(module_name in config['force_to_top'] and "A" or "B", prefix, - config['length_sort'] and (str(len(module_name)) + ":" + module_name) or module_name) - - def _add_comments(self, comments, original_string=""): - """ - Returns a string with comments added - """ - return comments and "{0} # {1}".format(self._strip_comments(original_string)[0], - "; ".join(comments)) or original_string - - def _wrap(self, line): - """ - Returns an import wrapped to the specified line-length, if possible. - """ - wrap_mode = self.config.get('multi_line_output', 0) - if len(line) > self.config['line_length'] and wrap_mode != settings.WrapModes.NOQA: - for splitter in ("import", "."): - exp = r"\b" + re.escape(splitter) + r"\b" - if re.search(exp, line) and not line.strip().startswith(splitter): - line_parts = re.split(exp, line) - next_line = [] - while (len(line) + 2) > (self.config['wrap_length'] or self.config['line_length']) and line_parts: - next_line.append(line_parts.pop()) - line = splitter.join(line_parts) - if not line: - line = next_line.pop() - - cont_line = self._wrap(self.config['indent'] + splitter.join(next_line).lstrip()) - if self.config['use_parentheses']: - return "{0}{1} (\n{2})".format(line, splitter, cont_line) - return "{0}{1} \\\n{2}".format(line, splitter, cont_line) - elif len(line) > self.config['line_length'] and wrap_mode == settings.WrapModes.NOQA: - if "# NOQA" not in line: - return "{0} # NOQA".format(line) - - return line - - def _add_straight_imports(self, straight_modules, section, section_output): - for module in straight_modules: - if module in self.remove_imports: - continue - - if module in self.as_map: - import_definition = "import {0} as {1}".format(module, self.as_map[module]) - else: - import_definition = "import {0}".format(module) - - comments_above = self.comments['above']['straight'].pop(module, None) - if comments_above: - section_output.extend(comments_above) - section_output.append(self._add_comments(self.comments['straight'].get(module), import_definition)) - - def _add_from_imports(self, from_modules, section, section_output): - for module in from_modules: - if module in self.remove_imports: - continue - - import_start = "from {0} import ".format(module) - from_imports = list(self.imports[section]['from'][module]) - from_imports = nsorted(from_imports, key=lambda key: self._module_key(key, self.config, True)) - if self.remove_imports: - from_imports = [line for line in from_imports if not "{0}.{1}".format(module, line) in - self.remove_imports] - - for from_import in copy.copy(from_imports): - submodule = module + "." + from_import - import_as = self.as_map.get(submodule, False) - if import_as: - import_definition = "{0} as {1}".format(from_import, import_as) - if self.config['combine_as_imports'] and not ("*" in from_imports and - self.config['combine_star']): - from_imports[from_imports.index(from_import)] = import_definition - else: - import_statement = self._wrap(import_start + import_definition) - comments = self.comments['straight'].get(submodule) - import_statement = self._add_comments(comments, import_statement) - section_output.append(import_statement) - from_imports.remove(from_import) - - if from_imports: - comments = self.comments['from'].pop(module, ()) - if "*" in from_imports and self.config['combine_star']: - import_statement = self._wrap(self._add_comments(comments, "{0}*".format(import_start))) - elif self.config['force_single_line']: - import_statements = [] - for from_import in from_imports: - single_import_line = self._add_comments(comments, import_start + from_import) - comment = self.comments['nested'].get(module, {}).pop(from_import, None) - if comment: - single_import_line += "{0} {1}".format(comments and ";" or " #", comment) - import_statements.append(self._wrap(single_import_line)) - comments = None - import_statement = "\n".join(import_statements) - else: - star_import = False - if "*" in from_imports: - section_output.append(self._add_comments(comments, "{0}*".format(import_start))) - from_imports.remove('*') - star_import = True - comments = None - - for from_import in copy.copy(from_imports): - comment = self.comments['nested'].get(module, {}).pop(from_import, None) - if comment: - single_import_line = self._add_comments(comments, import_start + from_import) - single_import_line += "{0} {1}".format(comments and ";" or " #", comment) - above_comments = self.comments['above']['from'].pop(module, None) - if above_comments: - section_output.extend(above_comments) - section_output.append(self._wrap(single_import_line)) - from_imports.remove(from_import) - comments = None - - if star_import: - import_statement = import_start + (", ").join(from_imports) - else: - import_statement = self._add_comments(comments, import_start + (", ").join(from_imports)) - if not from_imports: - import_statement = "" - if len(from_imports) > 1 and ( - len(import_statement) > self.config['line_length'] - or self.config.get('force_grid_wrap') - ): - output_mode = settings.WrapModes._fields[self.config.get('multi_line_output', - 0)].lower() - formatter = getattr(self, "_output_" + output_mode, self._output_grid) - dynamic_indent = " " * (len(import_start) + 1) - indent = self.config['indent'] - line_length = self.config['wrap_length'] or self.config['line_length'] - import_statement = formatter(import_start, copy.copy(from_imports), - dynamic_indent, indent, line_length, comments) - if self.config['balanced_wrapping']: - lines = import_statement.split("\n") - line_count = len(lines) - if len(lines) > 1: - minimum_length = min([len(line) for line in lines[:-1]]) - else: - minimum_length = 0 - new_import_statement = import_statement - while (len(lines[-1]) < minimum_length and - len(lines) == line_count and line_length > 10): - import_statement = new_import_statement - line_length -= 1 - new_import_statement = formatter(import_start, copy.copy(from_imports), - dynamic_indent, indent, line_length, comments) - lines = new_import_statement.split("\n") - elif len(import_statement) > self.config['line_length']: - import_statement = self._wrap(import_statement) - - if import_statement: - above_comments = self.comments['above']['from'].pop(module, None) - if above_comments: - section_output.extend(above_comments) - section_output.append(import_statement) - - def _add_formatted_imports(self): - """Adds the imports back to the file. - - (at the index of the first import) sorted alphabetically and split between groups - - """ - if self.config.get('force_alphabetical_sort', False): - from_output = [] - straight_output = [] - for section in itertools.chain(self.sections, self.config['forced_separate']): - straight_modules = list(self.imports[section]['straight']) - from_modules = list(self.imports[section]['from'].keys()) - - self._add_from_imports(from_modules, section, from_output) - self._add_straight_imports(straight_modules, section, straight_output) - - new_from_output = [] - new_straight_output = [] - for line in from_output: - for element in line.split('\n'): - new_from_output.append(element) - for line in straight_output: - for element in line.split('\n'): - new_straight_output.append(element) - - - sorted_from = sorted(new_from_output, key=lambda import_string: import_string.lower()) - sorted_straight = sorted(new_straight_output, key=lambda import_string: import_string.lower()) - output = (sorted_from + [''] + sorted_straight) if (sorted_from and sorted_straight) else \ - (sorted_from or sorted_straight) - else: - output = [] - for section in itertools.chain(self.sections, self.config['forced_separate']): - straight_modules = list(self.imports[section]['straight']) - straight_modules = nsorted(straight_modules, key=lambda key: self._module_key(key, self.config)) - from_modules = sorted(list(self.imports[section]['from'].keys())) - from_modules = nsorted(from_modules, key=lambda key: self._module_key(key, self.config, )) - - section_output = [] - if self.config.get('from_first', False): - self._add_from_imports(from_modules, section, section_output) - self._add_straight_imports(straight_modules, section, section_output) - else: - self._add_straight_imports(straight_modules, section, section_output) - self._add_from_imports(from_modules, section, section_output) - - if self.config.get('force_sort_within_sections', False): - def by_module(line): - line = re.sub('^from ', '', line) - line = re.sub('^import ', '', line) - if not self.config['order_by_type']: - line = line.lower() - return line - section_output = nsorted(section_output, key=by_module) - - if section_output: - section_name = section - if section_name in self.place_imports: - self.place_imports[section_name] = section_output - continue - - section_title = self.config.get('import_heading_' + str(section_name).lower(), '') - if section_title: - section_comment = "# {0}".format(section_title) - if not section_comment in self.out_lines[0:1]: - section_output.insert(0, section_comment) - output += section_output + ([''] * self.config['lines_between_sections']) - - while [character.strip() for character in output[-1:]] == [""]: - output.pop() - - output_at = 0 - if self.import_index < self.original_length: - output_at = self.import_index - elif self._first_comment_index_end != -1 and self._first_comment_index_start <= 2: - output_at = self._first_comment_index_end - self.out_lines[output_at:0] = output - - imports_tail = output_at + len(output) - while [character.strip() for character in self.out_lines[imports_tail: imports_tail + 1]] == [""]: - self.out_lines.pop(imports_tail) - - if len(self.out_lines) > imports_tail: - next_construct = "" - self._in_quote = False - for line in self.out_lines[imports_tail:]: - if not self._skip_line(line) and not line.strip().startswith("#") and line.strip(): - next_construct = line - break - - if self.config['lines_after_imports'] != -1: - self.out_lines[imports_tail:0] = ["" for line in range(self.config['lines_after_imports'])] - elif next_construct.startswith("def") or next_construct.startswith("class") or \ - next_construct.startswith("@"): - self.out_lines[imports_tail:0] = ["", ""] - else: - self.out_lines[imports_tail:0] = [""] - - if self.place_imports: - new_out_lines = [] - for index, line in enumerate(self.out_lines): - new_out_lines.append(line) - if line in self.import_placements: - new_out_lines.extend(self.place_imports[self.import_placements[line]]) - if len(self.out_lines) <= index or self.out_lines[index + 1].strip() != "": - new_out_lines.append("") - self.out_lines = new_out_lines - - def _output_grid(self, statement, imports, white_space, indent, line_length, comments): - statement += "(" + imports.pop(0) - while imports: - next_import = imports.pop(0) - next_statement = self._add_comments(comments, statement + ", " + next_import) - if len(next_statement.split("\n")[-1]) + 1 > line_length: - statement = (self._add_comments(comments, "{0},".format(statement)) + - "\n{0}{1}".format(white_space, next_import)) - comments = None - else: - statement += ", " + next_import - return statement + ("," if self.config['include_trailing_comma'] else "") + ")" - - def _output_vertical(self, statement, imports, white_space, indent, line_length, comments): - first_import = self._add_comments(comments, imports.pop(0) + ",") + "\n" + white_space - return "{0}({1}{2}{3})".format( - statement, - first_import, - (",\n" + white_space).join(imports), - "," if self.config['include_trailing_comma'] else "", - ) - - def _output_hanging_indent(self, statement, imports, white_space, indent, line_length, comments): - statement += imports.pop(0) - while imports: - next_import = imports.pop(0) - next_statement = self._add_comments(comments, statement + ", " + next_import) - if len(next_statement.split("\n")[-1]) + 3 > line_length: - next_statement = (self._add_comments(comments, "{0}, \\".format(statement)) + - "\n{0}{1}".format(indent, next_import)) - comments = None - statement = next_statement - return statement - - def _output_vertical_hanging_indent(self, statement, imports, white_space, indent, line_length, comments): - return "{0}({1}\n{2}{3}{4}\n)".format( - statement, - self._add_comments(comments), - indent, - (",\n" + indent).join(imports), - "," if self.config['include_trailing_comma'] else "", - ) - - def _output_vertical_grid_common(self, statement, imports, white_space, indent, line_length, comments): - statement += self._add_comments(comments, "(") + "\n" + indent + imports.pop(0) - while imports: - next_import = imports.pop(0) - next_statement = "{0}, {1}".format(statement, next_import) - if len(next_statement.split("\n")[-1]) + 1 > line_length: - next_statement = "{0},\n{1}{2}".format(statement, indent, next_import) - statement = next_statement - if self.config['include_trailing_comma']: - statement += ',' - return statement - - def _output_vertical_grid(self, statement, imports, white_space, indent, line_length, comments): - return self._output_vertical_grid_common(statement, imports, white_space, indent, line_length, comments) + ")" - - def _output_vertical_grid_grouped(self, statement, imports, white_space, indent, line_length, comments): - return self._output_vertical_grid_common(statement, imports, white_space, indent, line_length, comments) + "\n)" - - def _output_noqa(self, statement, imports, white_space, indent, line_length, comments): - retval = '{0}{1}'.format(statement, ' '.join(imports)) - comment_str = ' '.join(comments) - if comments: - if len(retval) + 4 + len(comment_str) <= line_length: - return '{0} # {1}'.format(retval, comment_str) - else: - if len(retval) <= line_length: - return retval - if comments: - if "NOQA" in comments: - return '{0} # {1}'.format(retval, comment_str) - else: - return '{0} # NOQA {1}'.format(retval, comment_str) - else: - return '{0} # NOQA'.format(retval) - - @staticmethod - def _strip_comments(line, comments=None): - """Removes comments from import line.""" - if comments is None: - comments = [] - - new_comments = False - comment_start = line.find("#") - if comment_start != -1: - comments.append(line[comment_start + 1:].strip()) - new_comments = True - line = line[:comment_start] - - return line, comments, new_comments - - @staticmethod - def _format_simplified(import_line): - import_line = import_line.strip() - if import_line.startswith("from "): - import_line = import_line.replace("from ", "") - import_line = import_line.replace(" import ", ".") - elif import_line.startswith("import "): - import_line = import_line.replace("import ", "") - - return import_line - - @staticmethod - def _format_natural(import_line): - import_line = import_line.strip() - if not import_line.startswith("from ") and not import_line.startswith("import "): - if not "." in import_line: - return "import {0}".format(import_line) - parts = import_line.split(".") - end = parts.pop(-1) - return "from {0} import {1}".format(".".join(parts), end) - - return import_line - - def _skip_line(self, line): - skip_line = self._in_quote - if self.index == 1 and line.startswith("#"): - self._in_top_comment = True - return True - elif self._in_top_comment: - if not line.startswith("#"): - self._in_top_comment = False - self._first_comment_index_end = self.index - - if '"' in line or "'" in line: - index = 0 - if self._first_comment_index_start == -1: - self._first_comment_index_start = self.index - while index < len(line): - if line[index] == "\\": - index += 1 - elif self._in_quote: - if line[index:index + len(self._in_quote)] == self._in_quote: - self._in_quote = False - if self._first_comment_index_end < self._first_comment_index_start: - self._first_comment_index_end = self.index - elif line[index] in ("'", '"'): - long_quote = line[index:index + 3] - if long_quote in ('"""', "'''"): - self._in_quote = long_quote - index += 2 - else: - self._in_quote = line[index] - elif line[index] == "#": - break - index += 1 - - return skip_line or self._in_quote or self._in_top_comment - - def _strip_syntax(self, import_string): - import_string = import_string.replace("_import", "[[i]]") - for remove_syntax in ['\\', '(', ')', ',']: - import_string = import_string.replace(remove_syntax, " ") - import_list = import_string.split() - for key in ('from', 'import'): - if key in import_list: - import_list.remove(key) - import_string = ' '.join(import_list) - import_string = import_string.replace("[[i]]", "_import") - return import_string.replace("{ ", "{|").replace(" }", "|}") - - def _parse(self): - """Parses a python file taking out and categorizing imports.""" - self._in_quote = False - self._in_top_comment = False - while not self._at_end(): - line = self._get_line() - skip_line = self._skip_line(line) - - if line in self._section_comments and not skip_line: - if self.import_index == -1: - self.import_index = self.index - 1 - continue - - if "isort:" + "imports-" in line and line.startswith("#"): - section = line.split("isort:" + "imports-")[-1].split()[0] - self.place_imports[section.upper()] = [] - self.import_placements[line] = section.upper() - - if ";" in line: - for part in (part.strip() for part in line.split(";")): - if part and not part.startswith("from ") and not part.startswith("import "): - skip_line = True - - import_type = self._import_type(line) - if not import_type or skip_line: - self.out_lines.append(line) - continue - - for line in (line.strip() for line in line.split(";")): - import_type = self._import_type(line) - if not import_type: - self.out_lines.append(line) - continue - - line = line.replace("\t", " ") - if self.import_index == -1: - self.import_index = self.index - 1 - - nested_comments = {} - import_string, comments, new_comments = self._strip_comments(line) - stripped_line = [part for part in self._strip_syntax(import_string).strip().split(" ") if part] - - if import_type == "from" and len(stripped_line) == 2 and stripped_line[1] != "*" and new_comments: - nested_comments[stripped_line[-1]] = comments[0] - - if "(" in line and not self._at_end(): - while not line.strip().endswith(")") and not self._at_end(): - line, comments, new_comments = self._strip_comments(self._get_line(), comments) - stripped_line = self._strip_syntax(line).strip() - if import_type == "from" and stripped_line and not " " in stripped_line and new_comments: - nested_comments[stripped_line] = comments[-1] - import_string += "\n" + line - else: - while line.strip().endswith("\\"): - line, comments, new_comments = self._strip_comments(self._get_line(), comments) - stripped_line = self._strip_syntax(line).strip() - if import_type == "from" and stripped_line and not " " in stripped_line and new_comments: - nested_comments[stripped_line] = comments[-1] - if import_string.strip().endswith(" import") or line.strip().startswith("import "): - import_string += "\n" + line - else: - import_string = import_string.rstrip().rstrip("\\") + line.lstrip() - - if import_type == "from": - parts = import_string.split(" import ") - from_import = parts[0].split(" ") - import_string = " import ".join([from_import[0] + " " + "".join(from_import[1:])] + parts[1:]) - - imports = [item.replace("{|", "{ ").replace("|}", " }") for item in - self._strip_syntax(import_string).split()] - if "as" in imports and (imports.index('as') + 1) < len(imports): - while "as" in imports: - index = imports.index('as') - if import_type == "from": - module = imports[0] + "." + imports[index - 1] - self.as_map[module] = imports[index + 1] - else: - module = imports[index - 1] - self.as_map[module] = imports[index + 1] - if not self.config['combine_as_imports']: - self.comments['straight'][module] = comments - comments = [] - del imports[index:index + 2] - if import_type == "from": - import_from = imports.pop(0) - placed_module = self.place_module(import_from) - if placed_module == '': - print( - "WARNING: could not place module {0} of line {1} --" - " Do you need to define a default section?".format(import_from, line) - ) - root = self.imports[placed_module][import_type] - for import_name in imports: - associated_commment = nested_comments.get(import_name) - if associated_commment: - self.comments['nested'].setdefault(import_from, {})[import_name] = associated_commment - comments.pop(comments.index(associated_commment)) - if comments: - self.comments['from'].setdefault(import_from, []).extend(comments) - - if len(self.out_lines) > max(self.import_index, self._first_comment_index_end, 1) - 1: - last = self.out_lines and self.out_lines[-1].rstrip() or "" - while last.startswith("#") and not last.endswith('"""') and not last.endswith("'''"): - self.comments['above']['from'].setdefault(import_from, []).insert(0, self.out_lines.pop(-1)) - if len(self.out_lines) > max(self.import_index - 1, self._first_comment_index_end, 1) - 1: - last = self.out_lines[-1].rstrip() - else: - last = "" - if self.index - 1 == self.import_index: - self.import_index -= len(self.comments['above']['from'].get(import_from, [])) - - if root.get(import_from, False): - root[import_from].update(imports) - else: - root[import_from] = set(imports) - else: - for module in imports: - if comments: - self.comments['straight'][module] = comments - comments = None - - if len(self.out_lines) > max(self.import_index, self._first_comment_index_end, 1) - 1: - last = self.out_lines and self.out_lines[-1].rstrip() or "" - while last.startswith("#") and not last.endswith('"""') and not last.endswith("'''"): - self.comments['above']['straight'].setdefault(module, []).insert(0, - self.out_lines.pop(-1)) - if len(self.out_lines) > max(self.import_index - 1, self._first_comment_index_end, - 1) - 1: - last = self.out_lines[-1].rstrip() - else: - last = "" - if self.index - 1 == self.import_index: - self.import_index -= len(self.comments['above']['straight'].get(module, [])) - placed_module = self.place_module(module) - if placed_module == '': - print( - "WARNING: could not place module {0} of line {1} --" - " Do you need to define a default section?".format(import_from, line) - ) - self.imports[placed_module][import_type].add(module) - - -def coding_check(fname, default='utf-8'): - - # see https://www.python.org/dev/peps/pep-0263/ - pattern = re.compile(br'coding[:=]\s*([-\w.]+)') - - coding = default - with io.open(fname, 'rb') as f: - for line_number, line in enumerate(f, 1): - groups = re.findall(pattern, line) - if groups: - coding = groups[0].decode('ascii') - break - if line_number > 2: - break - - return coding diff --git a/pythonFiles/isort/main.py b/pythonFiles/isort/main.py deleted file mode 100644 index 5dbc84c0b835..000000000000 --- a/pythonFiles/isort/main.py +++ /dev/null @@ -1,278 +0,0 @@ -#! /usr/bin/env python -''' Tool for sorting imports alphabetically, and automatically separated into sections. - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -''' -from __future__ import absolute_import, division, print_function, unicode_literals - -import argparse -import glob -import os -import sys - -import setuptools - -from isort import SortImports, __version__ -from isort.settings import DEFAULT_SECTIONS, default, from_path, should_skip - -from .pie_slice import * - - -INTRO = """ -/#######################################################################\\ - - `sMMy` - .yyyy- ` - ##soos## ./o. - ` ``..-..` ``...`.`` ` ```` ``-ssso``` - .s:-y- .+osssssso/. ./ossss+:so+:` :+o-`/osso:+sssssssso/ - .s::y- osss+.``.`` -ssss+-.`-ossso` ssssso/::..::+ssss:::. - .s::y- /ssss+//:-.` `ssss+ `ssss+ sssso` :ssss` - .s::y- `-/+oossssso/ `ssss/ sssso ssss/ :ssss` - .y-/y- ````:ssss` ossso. :ssss: ssss/ :ssss. - `/so:` `-//::/osss+ `+ssss+-/ossso: /sso- `osssso/. - \/ `-/oooo++/- .:/++:/++/-` .. `://++/. - - - isort your Python imports for you so you don't have to - - VERSION {0} - -\########################################################################/ -""".format(__version__) - - -def iter_source_code(paths, config, skipped): - """Iterate over all Python source files defined in paths.""" - for path in paths: - if os.path.isdir(path): - if should_skip(path, config): - skipped.append(path) - continue - - for dirpath, dirnames, filenames in os.walk(path, topdown=True): - for dirname in list(dirnames): - if should_skip(dirname, config): - skipped.append(dirname) - dirnames.remove(dirname) - for filename in filenames: - if filename.endswith('.py'): - if should_skip(filename, config): - skipped.append(filename) - else: - yield os.path.join(dirpath, filename) - else: - yield path - - -class ISortCommand(setuptools.Command): - """The :class:`ISortCommand` class is used by setuptools to perform - imports checks on registered modules. - """ - - description = "Run isort on modules registered in setuptools" - user_options = [] - - def initialize_options(self): - default_settings = default.copy() - for (key, value) in itemsview(default_settings): - setattr(self, key, value) - - def finalize_options(self): - "Get options from config files." - self.arguments = {} - computed_settings = from_path(os.getcwd()) - for (key, value) in itemsview(computed_settings): - self.arguments[key] = value - - def distribution_files(self): - """Find distribution packages.""" - # This is verbatim from flake8 - if self.distribution.packages: - package_dirs = self.distribution.package_dir or {} - for package in self.distribution.packages: - pkg_dir = package - if package in package_dirs: - pkg_dir = package_dirs[package] - elif '' in package_dirs: - pkg_dir = package_dirs[''] + os.path.sep + pkg_dir - yield pkg_dir.replace('.', os.path.sep) - - if self.distribution.py_modules: - for filename in self.distribution.py_modules: - yield "%s.py" % filename - # Don't miss the setup.py file itself - yield "setup.py" - - def run(self): - arguments = self.arguments - wrong_sorted_files = False - arguments['check'] = True - for path in self.distribution_files(): - for python_file in glob.iglob(os.path.join(path, '*.py')): - try: - incorrectly_sorted = SortImports(python_file, **arguments).incorrectly_sorted - if incorrectly_sorted: - wrong_sorted_files = True - except IOError as e: - print("WARNING: Unable to parse file {0} due to {1}".format(file_name, e)) - if wrong_sorted_files: - exit(1) - - -def create_parser(): - parser = argparse.ArgumentParser(description='Sort Python import definitions alphabetically ' - 'within logical sections.') - parser.add_argument('files', nargs='*', help='One or more Python source files that need their imports sorted.') - parser.add_argument('-y', '--apply', dest='apply', action='store_true', - help='Tells isort to apply changes recursively without asking') - parser.add_argument('-l', '--lines', help='[Deprecated] The max length of an import line (used for wrapping ' - 'long imports).', - dest='line_length', type=int) - parser.add_argument('-w', '--line-width', help='The max length of an import line (used for wrapping long imports).', - dest='line_length', type=int) - parser.add_argument('-s', '--skip', help='Files that sort imports should skip over. If you want to skip multiple ' - 'files you should specify twice: --skip file1 --skip file2.', dest='skip', action='append') - parser.add_argument('-ns', '--dont-skip', help='Files that sort imports should never skip over.', - dest='not_skip', action='append') - parser.add_argument('-sg', '--skip-glob', help='Files that sort imports should skip over.', dest='skip_glob', - action='append') - parser.add_argument('-t', '--top', help='Force specific imports to the top of their appropriate section.', - dest='force_to_top', action='append') - parser.add_argument('-f', '--future', dest='known_future_library', action='append', - help='Force sortImports to recognize a module as part of the future compatibility libraries.') - parser.add_argument('-b', '--builtin', dest='known_standard_library', action='append', - help='Force sortImports to recognize a module as part of the python standard library.') - parser.add_argument('-o', '--thirdparty', dest='known_third_party', action='append', - help='Force sortImports to recognize a module as being part of a third party library.') - parser.add_argument('-p', '--project', dest='known_first_party', action='append', - help='Force sortImports to recognize a module as being part of the current python project.') - parser.add_argument('-m', '--multi_line', dest='multi_line_output', type=int, choices=[0, 1, 2, 3, 4, 5], - help='Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, ' - '5-vert-grid-grouped).') - parser.add_argument('-i', '--indent', help='String to place for indents defaults to " " (4 spaces).', - dest='indent', type=str) - parser.add_argument('-a', '--add_import', dest='add_imports', action='append', - help='Adds the specified import line to all files, ' - 'automatically determining correct placement.') - parser.add_argument('-af', '--force_adds', dest='force_adds', action='store_true', - help='Forces import adds even if the original file is empty.') - parser.add_argument('-r', '--remove_import', dest='remove_imports', action='append', - help='Removes the specified import from all files.') - parser.add_argument('-ls', '--length_sort', help='Sort imports by their string length.', - dest='length_sort', action='store_true', default=False) - parser.add_argument('-d', '--stdout', help='Force resulting output to stdout, instead of in-place.', - dest='write_to_stdout', action='store_true') - parser.add_argument('-c', '--check-only', action='store_true', default=False, dest="check", - help='Checks the file for unsorted / unformatted imports and prints them to the ' - 'command line without modifying the file.') - parser.add_argument('-sl', '--force-single-line-imports', dest='force_single_line', action='store_true', - help='Forces all from imports to appear on their own line') - parser.add_argument('--force_single_line_imports', dest='force_single_line', action='store_true', - help=argparse.SUPPRESS) - parser.add_argument('-sd', '--section-default', dest='default_section', - help='Sets the default section for imports (by default FIRSTPARTY) options: ' + - str(DEFAULT_SECTIONS)) - parser.add_argument('-df', '--diff', dest='show_diff', default=False, action='store_true', - help="Prints a diff of all the changes isort would make to a file, instead of " - "changing it in place") - parser.add_argument('-e', '--balanced', dest='balanced_wrapping', action='store_true', - help='Balances wrapping to produce the most consistent line length possible') - parser.add_argument('-rc', '--recursive', dest='recursive', action='store_true', - help='Recursively look for Python files of which to sort imports') - parser.add_argument('-ot', '--order-by-type', dest='order_by_type', - action='store_true', help='Order imports by type in addition to alphabetically') - parser.add_argument('-ac', '--atomic', dest='atomic', action='store_true', - help="Ensures the output doesn't save if the resulting file contains syntax errors.") - parser.add_argument('-cs', '--combine-star', dest='combine_star', action='store_true', - help="Ensures that if a star import is present, nothing else is imported from that namespace.") - parser.add_argument('-ca', '--combine-as', dest='combine_as_imports', action='store_true', - help="Combines as imports on the same line.") - parser.add_argument('-tc', '--trailing-comma', dest='include_trailing_comma', action='store_true', - help='Includes a trailing comma on multi line imports that include parentheses.') - parser.add_argument('-v', '--version', action='store_true', dest='show_version') - parser.add_argument('-vb', '--verbose', action='store_true', dest="verbose", - help='Shows verbose output, such as when files are skipped or when a check is successful.') - parser.add_argument('-q', '--quiet', action='store_true', dest="quiet", - help='Shows extra quiet output, only errors are outputted.') - parser.add_argument('-sp', '--settings-path', dest="settings_path", - help='Explicitly set the settings path instead of auto determining based on file location.') - parser.add_argument('-ff', '--from-first', dest='from_first', - help="Switches the typical ordering preference, showing from imports first then straight ones.") - parser.add_argument('-wl', '--wrap-length', dest='wrap_length', - help="Specifies how long lines that are wrapped should be, if not set line_length is used.") - parser.add_argument('-fgw', '--force-grid-wrap', action='store_true', dest="force_grid_wrap", - help='Force from imports to be grid wrapped regardless of line length') - parser.add_argument('-fas', '--force-alphabetical-sort', action='store_true', dest="force_alphabetical_sort", - help='Force all imports to be sorted as a single section') - parser.add_argument('-fss', '--force-sort-within-sections', action='store_true', dest="force_sort_within_sections", - help='Force imports to be sorted by module, independant of import_type') - - - arguments = dict((key, value) for (key, value) in itemsview(vars(parser.parse_args())) if value) - return arguments - - -def main(): - arguments = create_parser() - if arguments.get('show_version'): - print(INTRO) - return - - file_names = arguments.pop('files', []) - if file_names == ['-']: - SortImports(file_contents=sys.stdin.read(), write_to_stdout=True, **arguments) - else: - if not file_names: - file_names = ['.'] - arguments['recursive'] = True - if not arguments.get('apply', False): - arguments['ask_to_apply'] = True - config = from_path(os.path.abspath(file_names[0]) or os.getcwd()).copy() - config.update(arguments) - wrong_sorted_files = False - skipped = [] - if arguments.get('recursive', False): - file_names = iter_source_code(file_names, config, skipped) - num_skipped = 0 - if config.get('verbose', False) or config.get('show_logo', False): - print(INTRO) - for file_name in file_names: - try: - sort_attempt = SortImports(file_name, **arguments) - incorrectly_sorted = sort_attempt.incorrectly_sorted - if arguments.get('check', False) and incorrectly_sorted: - wrong_sorted_files = True - if sort_attempt.skipped: - num_skipped += 1 - except IOError as e: - print("WARNING: Unable to parse file {0} due to {1}".format(file_name, e)) - if wrong_sorted_files: - exit(1) - - num_skipped += len(skipped) - if num_skipped and not arguments.get('quiet', False): - if config['verbose']: - for was_skipped in skipped: - print("WARNING: {0} was skipped as it's listed in 'skip' setting" - " or matches a glob in 'skip_glob' setting".format(was_skipped)) - print("Skipped {0} files".format(num_skipped)) - - -if __name__ == "__main__": - main() diff --git a/pythonFiles/isort/natural.py b/pythonFiles/isort/natural.py deleted file mode 100644 index 0529fa60bb95..000000000000 --- a/pythonFiles/isort/natural.py +++ /dev/null @@ -1,47 +0,0 @@ -"""isort/natural.py. - -Enables sorting strings that contain numbers naturally - -usage: - natural.nsorted(list) - -Copyright (C) 2013 Timothy Edmund Crosley - -Implementation originally from @HappyLeapSecond stack overflow user in response to: - http://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -""" -import re - - -def _atoi(text): - return int(text) if text.isdigit() else text - - -def _natural_keys(text): - return [_atoi(c) for c in re.split('(\d+)', text)] - - -def nsorted(to_sort, key=None): - """Returns a naturally sorted list""" - if key is None: - key_callback = _natural_keys - else: - def key_callback(item): - return _natural_keys(key(item)) - - return sorted(to_sort, key=key_callback) diff --git a/pythonFiles/isort/pie_slice.py b/pythonFiles/isort/pie_slice.py deleted file mode 100644 index 5cef39b19245..000000000000 --- a/pythonFiles/isort/pie_slice.py +++ /dev/null @@ -1,528 +0,0 @@ -"""pie_slice/overrides.py. - -Overrides Python syntax to conform to the Python3 version as much as possible using a '*' import - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copie_slice of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copie_slice or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -""" -from __future__ import absolute_import - -import abc -import functools -import sys -from numbers import Integral - -__version__ = "1.1.0" - -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 -VERSION = sys.version_info - -native_dict = dict -native_round = round -native_filter = filter -native_map = map -native_zip = zip -native_range = range -native_str = str -native_chr = chr -native_input = input -native_next = next -native_object = object - -common = ['native_dict', 'native_round', 'native_filter', 'native_map', 'native_range', 'native_str', 'native_chr', - 'native_input', 'PY2', 'PY3', 'u', 'itemsview', 'valuesview', 'keysview', 'execute', 'integer_types', - 'native_next', 'native_object', 'with_metaclass', 'OrderedDict', 'lru_cache'] - - -def with_metaclass(meta, *bases): - """Enables use of meta classes across Python Versions. taken from jinja2/_compat.py. - - Use it like this:: - - class BaseForm(object): - pass - - class FormType(type): - pass - - class Form(with_metaclass(FormType, BaseForm)): - pass - - """ - class metaclass(meta): - __call__ = type.__call__ - __init__ = type.__init__ - def __new__(cls, name, this_bases, d): - if this_bases is None: - return type.__new__(cls, name, (), d) - return meta(name, bases, d) - return metaclass('temporary_class', None, {}) - - -def unmodified_isinstance(*bases): - """When called in the form - - MyOverrideClass(unmodified_isinstance(BuiltInClass)) - - it allows calls against passed in built in instances to pass even if there not a subclass - - """ - class UnmodifiedIsInstance(type): - if sys.version_info[0] == 2 and sys.version_info[1] <= 6: - - @classmethod - def __instancecheck__(cls, instance): - if cls.__name__ in (str(base.__name__) for base in bases): - return isinstance(instance, bases) - - subclass = getattr(instance, '__class__', None) - subtype = type(instance) - instance_type = getattr(abc, '_InstanceType', None) - if not instance_type: - class test_object: - pass - instance_type = type(test_object) - if subtype is instance_type: - subtype = subclass - if subtype is subclass or subclass is None: - return cls.__subclasscheck__(subtype) - return (cls.__subclasscheck__(subclass) or cls.__subclasscheck__(subtype)) - else: - @classmethod - def __instancecheck__(cls, instance): - if cls.__name__ in (str(base.__name__) for base in bases): - return isinstance(instance, bases) - - return type.__instancecheck__(cls, instance) - - return with_metaclass(UnmodifiedIsInstance, *bases) - - -if PY3: - import urllib - import builtins - from urllib import parse - - integer_types = (int, ) - - def u(string): - return string - - def itemsview(collection): - return collection.items() - - def valuesview(collection): - return collection.values() - - def keysview(collection): - return collection.keys() - - urllib.quote = parse.quote - urllib.quote_plus = parse.quote_plus - urllib.unquote = parse.unquote - urllib.unquote_plus = parse.unquote_plus - urllib.urlencode = parse.urlencode - execute = getattr(builtins, 'exec') - if VERSION[1] < 2: - def callable(entity): - return hasattr(entity, '__call__') - common.append('callable') - - __all__ = common + ['urllib'] -else: - from itertools import ifilter as filter - from itertools import imap as map - from itertools import izip as zip - from decimal import Decimal, ROUND_HALF_EVEN - - import codecs - str = unicode - chr = unichr - input = raw_input - range = xrange - integer_types = (int, long) - - import sys - stdout = sys.stdout - stderr = sys.stderr - reload(sys) - sys.stdout = stdout - sys.stderr = stderr - sys.setdefaultencoding('utf-8') - - def _create_not_allowed(name): - def _not_allow(*args, **kwargs): - raise NameError("name '{0}' is not defined".format(name)) - _not_allow.__name__ = name - return _not_allow - - for removed in ('apply', 'cmp', 'coerce', 'execfile', 'raw_input', 'unpacks'): - globals()[removed] = _create_not_allowed(removed) - - def u(s): - if isinstance(s, unicode): - return s - else: - return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") - - def execute(_code_, _globs_=None, _locs_=None): - """Execute code in a namespace.""" - if _globs_ is None: - frame = sys._getframe(1) - _globs_ = frame.f_globals - if _locs_ is None: - _locs_ = frame.f_locals - del frame - elif _locs_ is None: - _locs_ = _globs_ - exec("""exec _code_ in _globs_, _locs_""") - - class _dict_view_base(object): - __slots__ = ('_dictionary', ) - - def __init__(self, dictionary): - self._dictionary = dictionary - - def __repr__(self): - return "{0}({1})".format(self.__class__.__name__, str(list(self.__iter__()))) - - def __unicode__(self): - return str(self.__repr__()) - - def __str__(self): - return str(self.__unicode__()) - - class dict_keys(_dict_view_base): - __slots__ = () - - def __iter__(self): - return self._dictionary.iterkeys() - - class dict_values(_dict_view_base): - __slots__ = () - - def __iter__(self): - return self._dictionary.itervalues() - - class dict_items(_dict_view_base): - __slots__ = () - - def __iter__(self): - return self._dictionary.iteritems() - - def itemsview(collection): - return dict_items(collection) - - def valuesview(collection): - return dict_values(collection) - - def keysview(collection): - return dict_keys(collection) - - class dict(unmodified_isinstance(native_dict)): - def has_key(self, *args, **kwargs): - return AttributeError("'dict' object has no attribute 'has_key'") - - def items(self): - return dict_items(self) - - def keys(self): - return dict_keys(self) - - def values(self): - return dict_values(self) - - def round(number, ndigits=None): - return_int = False - if ndigits is None: - return_int = True - ndigits = 0 - if hasattr(number, '__round__'): - return number.__round__(ndigits) - - if ndigits < 0: - raise NotImplementedError('negative ndigits not supported yet') - exponent = Decimal('10') ** (-ndigits) - d = Decimal.from_float(number).quantize(exponent, - rounding=ROUND_HALF_EVEN) - if return_int: - return int(d) - else: - return float(d) - - def next(iterator): - try: - iterator.__next__() - except Exception: - native_next(iterator) - - class FixStr(type): - def __new__(cls, name, bases, dct): - if '__str__' in dct: - dct['__unicode__'] = dct['__str__'] - dct['__str__'] = lambda self: self.__unicode__().encode('utf-8') - return type.__new__(cls, name, bases, dct) - - if sys.version_info[1] <= 6: - def __instancecheck__(cls, instance): - if cls.__name__ == "object": - return isinstance(instance, native_object) - - subclass = getattr(instance, '__class__', None) - subtype = type(instance) - instance_type = getattr(abc, '_InstanceType', None) - if not instance_type: - class test_object: - pass - instance_type = type(test_object) - if subtype is instance_type: - subtype = subclass - if subtype is subclass or subclass is None: - return cls.__subclasscheck__(subtype) - return (cls.__subclasscheck__(subclass) or cls.__subclasscheck__(subtype)) - else: - def __instancecheck__(cls, instance): - if cls.__name__ == "object": - return isinstance(instance, native_object) - return type.__instancecheck__(cls, instance) - - class object(with_metaclass(FixStr, object)): - pass - - __all__ = common + ['round', 'dict', 'apply', 'cmp', 'coerce', 'execfile', 'raw_input', 'unpacks', 'str', 'chr', - 'input', 'range', 'filter', 'map', 'zip', 'object'] - -if sys.version_info[0] == 2 and sys.version_info[1] < 7: - # OrderedDict - # Copyright (c) 2009 Raymond Hettinger - # - # Permission is hereby granted, free of charge, to any person - # obtaining a copy of this software and associated documentation files - # (the "Software"), to deal in the Software without restriction, - # including without limitation the rights to use, copy, modify, merge, - # publish, distribute, sublicense, and/or sell copies of the Software, - # and to permit persons to whom the Software is furnished to do so, - # subject to the following conditions: - # - # The above copyright notice and this permission notice shall be - # included in all copies or substantial portions of the Software. - # - # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - # OTHER DEALINGS IN THE SOFTWARE. - - from UserDict import DictMixin - - class OrderedDict(dict, DictMixin): - - def __init__(self, *args, **kwds): - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__end - except AttributeError: - self.clear() - self.update(*args, **kwds) - - def clear(self): - self.__end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.__map = {} # key --> [key, prev, next] - dict.clear(self) - - def __setitem__(self, key, value): - if key not in self: - end = self.__end - curr = end[1] - curr[2] = end[1] = self.__map[key] = [key, curr, end] - dict.__setitem__(self, key, value) - - def __delitem__(self, key): - dict.__delitem__(self, key) - key, prev, next = self.__map.pop(key) - prev[2] = next - next[1] = prev - - def __iter__(self): - end = self.__end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self): - end = self.__end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - def popitem(self, last=True): - if not self: - raise KeyError('dictionary is empty') - if last: - key = reversed(self).next() - else: - key = iter(self).next() - value = self.pop(key) - return key, value - - def __reduce__(self): - items = [[k, self[k]] for k in self] - tmp = self.__map, self.__end - del self.__map, self.__end - inst_dict = vars(self).copy() - self.__map, self.__end = tmp - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def keys(self): - return list(self) - - setdefault = DictMixin.setdefault - update = DictMixin.update - pop = DictMixin.pop - values = DictMixin.values - items = DictMixin.items - iterkeys = DictMixin.iterkeys - itervalues = DictMixin.itervalues - iteritems = DictMixin.iteritems - - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - - def copy(self): - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - if isinstance(other, OrderedDict): - if len(self) != len(other): - return False - for p, q in zip(self.items(), other.items()): - if p != q: - return False - return True - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other -else: - from collections import OrderedDict - - -if sys.version_info < (3, 2): - try: - from threading import Lock - except ImportError: - from dummy_threading import Lock - - from functools import wraps - - def lru_cache(maxsize=100): - """Least-recently-used cache decorator. - Taking from: https://github.com/MiCHiLU/python-functools32/blob/master/functools32/functools32.py - with slight modifications. - If *maxsize* is set to None, the LRU features are disabled and the cache - can grow without bound. - Arguments to the cached function must be hashable. - View the cache statistics named tuple (hits, misses, maxsize, currsize) with - f.cache_info(). Clear the cache and statistics with f.cache_clear(). - Access the underlying function with f.__wrapped__. - See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used - - """ - def decorating_function(user_function, tuple=tuple, sorted=sorted, len=len, KeyError=KeyError): - hits, misses = [0], [0] - kwd_mark = (object(),) # separates positional and keyword args - lock = Lock() - - if maxsize is None: - CACHE = dict() - - @wraps(user_function) - def wrapper(*args, **kwds): - key = args - if kwds: - key += kwd_mark + tuple(sorted(kwds.items())) - try: - result = CACHE[key] - hits[0] += 1 - return result - except KeyError: - pass - result = user_function(*args, **kwds) - CACHE[key] = result - misses[0] += 1 - return result - else: - CACHE = OrderedDict() - - @wraps(user_function) - def wrapper(*args, **kwds): - key = args - if kwds: - key += kwd_mark + tuple(sorted(kwds.items())) - with lock: - cached = CACHE.get(key, None) - if cached: - del CACHE[key] - CACHE[key] = cached - hits[0] += 1 - return cached - result = user_function(*args, **kwds) - with lock: - CACHE[key] = result # record recent use of this key - misses[0] += 1 - while len(CACHE) > maxsize: - CACHE.popitem(last=False) - return result - - def cache_info(): - """Report CACHE statistics.""" - with lock: - return _CacheInfo(hits[0], misses[0], maxsize, len(CACHE)) - - def cache_clear(): - """Clear the CACHE and CACHE statistics.""" - with lock: - CACHE.clear() - hits[0] = misses[0] = 0 - - wrapper.cache_info = cache_info - wrapper.cache_clear = cache_clear - return wrapper - - return decorating_function - -else: - from functools import lru_cache diff --git a/pythonFiles/isort/pylama_isort.py b/pythonFiles/isort/pylama_isort.py deleted file mode 100644 index 6fa235f9cbdb..000000000000 --- a/pythonFiles/isort/pylama_isort.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -import sys - -from pylama.lint import Linter as BaseLinter - -from .isort import SortImports - - -class Linter(BaseLinter): - - def allow(self, path): - """Determine if this path should be linted.""" - return path.endswith('.py') - - def run(self, path, **meta): - """Lint the file. Return an array of error dicts if appropriate.""" - with open(os.devnull, 'w') as devnull: - # Suppress isort messages - sys.stdout = devnull - - if SortImports(path, check=True).incorrectly_sorted: - return [{ - 'lnum': 0, - 'col': 0, - 'text': 'Incorrectly sorted imports.', - 'type': 'ISORT' - }] - else: - return [] diff --git a/pythonFiles/isort/settings.py b/pythonFiles/isort/settings.py deleted file mode 100644 index 43cdab9311cf..000000000000 --- a/pythonFiles/isort/settings.py +++ /dev/null @@ -1,218 +0,0 @@ -"""isort/settings.py. - -Defines how the default settings for isort should be loaded - -(First from the default setting dictionary at the top of the file, then overridden by any settings - in ~/.isort.cfg if there are any) - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -""" -from __future__ import absolute_import, division, print_function, unicode_literals - -import fnmatch -import os -from collections import namedtuple - -from .pie_slice import * - -try: - import configparser -except ImportError: - import ConfigParser as configparser - -MAX_CONFIG_SEARCH_DEPTH = 25 # The number of parent directories isort will look for a config file within -DEFAULT_SECTIONS = ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER") - -WrapModes = ('GRID', 'VERTICAL', 'HANGING_INDENT', 'VERTICAL_HANGING_INDENT', 'VERTICAL_GRID', 'VERTICAL_GRID_GROUPED', 'NOQA') -WrapModes = namedtuple('WrapModes', WrapModes)(*range(len(WrapModes))) - -# Note that none of these lists must be complete as they are simply fallbacks for when included auto-detection fails. -default = {'force_to_top': [], - 'skip': ['__init__.py', ], - 'skip_glob': [], - 'line_length': 79, - 'wrap_length': 0, - 'sections': DEFAULT_SECTIONS, - 'known_future_library': ['__future__'], - 'known_standard_library': ["abc", "anydbm", "argparse", "array", "asynchat", "asyncore", "atexit", "base64", - "BaseHTTPServer", "bisect", "bz2", "calendar", "cgitb", "cmd", "codecs", - "collections", "commands", "compileall", "ConfigParser", "contextlib", "Cookie", - "copy", "cPickle", "cProfile", "cStringIO", "csv", "datetime", "dbhash", "dbm", - "decimal", "difflib", "dircache", "dis", "doctest", "dumbdbm", "EasyDialogs", - "errno", "exceptions", "filecmp", "fileinput", "fnmatch", "fractions", - "functools", "gc", "gdbm", "getopt", "getpass", "gettext", "glob", "grp", "gzip", - "hashlib", "heapq", "hmac", "imaplib", "imp", "inspect", "io", "itertools", "json", - "linecache", "locale", "logging", "mailbox", "math", "mhlib", "mmap", - "multiprocessing", "operator", "optparse", "os", "pdb", "pickle", "pipes", - "pkgutil", "platform", "plistlib", "pprint", "profile", "pstats", "pwd", "pyclbr", - "pydoc", "Queue", "random", "re", "readline", "resource", "rlcompleter", - "robotparser", "sched", "select", "shelve", "shlex", "shutil", "signal", - "SimpleXMLRPCServer", "site", "sitecustomize", "smtpd", "smtplib", "socket", - "SocketServer", "sqlite3", "string", "StringIO", "struct", "subprocess", "sys", - "sysconfig", "tabnanny", "tarfile", "tempfile", "textwrap", "threading", "time", - "timeit", "trace", "traceback", "unittest", "urllib", "urllib2", "urlparse", - "usercustomize", "uuid", "warnings", "weakref", "webbrowser", "whichdb", "xml", - "xmlrpclib", "zipfile", "zipimport", "zlib", 'builtins', '__builtin__', 'thread', - "binascii", "statistics", "unicodedata", "fcntl"], - 'known_third_party': ['google.appengine.api'], - 'known_first_party': [], - 'multi_line_output': WrapModes.GRID, - 'forced_separate': [], - 'indent': ' ' * 4, - 'length_sort': False, - 'add_imports': [], - 'remove_imports': [], - 'force_single_line': False, - 'default_section': 'FIRSTPARTY', - 'import_heading_future': '', - 'import_heading_stdlib': '', - 'import_heading_thirdparty': '', - 'import_heading_firstparty': '', - 'import_heading_localfolder': '', - 'balanced_wrapping': False, - 'use_parentheses': False, - 'order_by_type': True, - 'atomic': False, - 'lines_after_imports': -1, - 'lines_between_sections': 1, - 'combine_as_imports': False, - 'combine_star': False, - 'include_trailing_comma': False, - 'from_first': False, - 'verbose': False, - 'quiet': False, - 'force_adds': False, - 'force_alphabetical_sort': False, - 'force_grid_wrap': False, - 'force_sort_within_sections': False, - 'show_diff': False} - - -@lru_cache() -def from_path(path): - computed_settings = default.copy() - _update_settings_with_config(path, '.editorconfig', '~/.editorconfig', ('*', '*.py', '**.py'), computed_settings) - _update_settings_with_config(path, '.isort.cfg', '~/.isort.cfg', ('settings', 'isort'), computed_settings) - _update_settings_with_config(path, 'setup.cfg', None, ('isort', ), computed_settings) - return computed_settings - - -def _update_settings_with_config(path, name, default, sections, computed_settings): - editor_config_file = default and os.path.expanduser(default) - tries = 0 - current_directory = path - while current_directory and tries < MAX_CONFIG_SEARCH_DEPTH: - potential_path = os.path.join(current_directory, native_str(name)) - if os.path.exists(potential_path): - editor_config_file = potential_path - break - - new_directory = os.path.split(current_directory)[0] - if current_directory == new_directory: - break - current_directory = new_directory - tries += 1 - - if editor_config_file and os.path.exists(editor_config_file): - _update_with_config_file(editor_config_file, sections, computed_settings) - - -def _update_with_config_file(file_path, sections, computed_settings): - settings = _get_config_data(file_path, sections).copy() - if not settings: - return - - if file_path.endswith(".editorconfig"): - indent_style = settings.pop('indent_style', "").strip() - indent_size = settings.pop('indent_size', "").strip() - if indent_style == "space": - computed_settings['indent'] = " " * (indent_size and int(indent_size) or 4) - elif indent_style == "tab": - computed_settings['indent'] = "\t" * (indent_size and int(indent_size) or 1) - - max_line_length = settings.pop('max_line_length', "").strip() - if max_line_length: - computed_settings['line_length'] = int(max_line_length) - - for key, value in itemsview(settings): - access_key = key.replace('not_', '').lower() - existing_value_type = type(default.get(access_key, '')) - if existing_value_type in (list, tuple): - # sections has fixed order values; no adding or substraction from any set - if access_key == 'sections': - computed_settings[access_key] = tuple(_as_list(value)) - else: - existing_data = set(computed_settings.get(access_key, default.get(access_key))) - if key.startswith('not_'): - computed_settings[access_key] = list(existing_data.difference(_as_list(value))) - else: - computed_settings[access_key] = list(existing_data.union(_as_list(value))) - elif existing_value_type == bool and value.lower().strip() == "false": - computed_settings[access_key] = False - elif key.startswith('known_'): - computed_settings[access_key] = list(_as_list(value)) - else: - computed_settings[access_key] = existing_value_type(value) - - -def _as_list(value): - return filter(bool, [item.strip() for item in value.split(",")]) - - -@lru_cache() -def _get_config_data(file_path, sections): - with open(file_path, 'rU') as config_file: - if file_path.endswith(".editorconfig"): - line = "\n" - last_position = config_file.tell() - while line: - line = config_file.readline() - if "[" in line: - config_file.seek(last_position) - break - last_position = config_file.tell() - - config = configparser.SafeConfigParser() - config.readfp(config_file) - settings = dict() - for section in sections: - if config.has_section(section): - settings.update(dict(config.items(section))) - - return settings - - return {} - - -def should_skip(filename, config): - """Returns True if the file should be skipped based on the passed in settings.""" - for skip_path in config['skip']: - if skip_path.endswith(filename): - return True - - position = os.path.split(filename) - while position[1]: - if position[1] in config['skip']: - return True - position = os.path.split(position[0]) - - for glob in config['skip_glob']: - if fnmatch.fnmatch(filename, glob): - return True - - return False diff --git a/pythonFiles/jedi/.DS_Store b/pythonFiles/jedi/.DS_Store deleted file mode 100644 index 23c6f80f8d82..000000000000 Binary files a/pythonFiles/jedi/.DS_Store and /dev/null differ diff --git a/pythonFiles/jedi/__init__.py b/pythonFiles/jedi/__init__.py deleted file mode 100644 index ca99329cda9c..000000000000 --- a/pythonFiles/jedi/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Jedi is a static analysis tool for Python that can be used in IDEs/editors. Its -historic focus is autocompletion, but does static analysis for now as well. -Jedi is fast and is very well tested. It understands Python on a deeper level -than all other static analysis frameworks for Python. - -Jedi has support for two different goto functions. It's possible to search for -related names and to list all names in a Python file and infer them. Jedi -understands docstrings and you can use Jedi autocompletion in your REPL as -well. - -Jedi uses a very simple API to connect with IDE's. There's a reference -implementation as a `VIM-Plugin `_, -which uses Jedi's autocompletion. We encourage you to use Jedi in your IDEs. -It's really easy. - -To give you a simple example how you can use the Jedi library, here is an -example for the autocompletion feature: - ->>> import jedi ->>> source = ''' -... import datetime -... datetime.da''' ->>> script = jedi.Script(source, 3, len('datetime.da'), 'example.py') ->>> script - ->>> completions = script.completions() ->>> completions #doctest: +ELLIPSIS -[, , ...] ->>> print(completions[0].complete) -te ->>> print(completions[0].name) -date - -As you see Jedi is pretty simple and allows you to concentrate on writing a -good text editor, while still having very good IDE features for Python. -""" - -__version__ = '0.9.0' - -from jedi.api import Script, Interpreter, NotFoundError, set_debug_function -from jedi.api import preload_module, defined_names, names -from jedi import settings diff --git a/pythonFiles/jedi/__main__.py b/pythonFiles/jedi/__main__.py deleted file mode 100644 index b26397138312..000000000000 --- a/pythonFiles/jedi/__main__.py +++ /dev/null @@ -1,43 +0,0 @@ -from sys import argv -from os.path import join, dirname, abspath, isdir - - -if len(argv) == 2 and argv[1] == 'repl': - # don't want to use __main__ only for repl yet, maybe we want to use it for - # something else. So just use the keyword ``repl`` for now. - print(join(dirname(abspath(__file__)), 'api', 'replstartup.py')) -elif len(argv) > 1 and argv[1] == 'linter': - """ - This is a pre-alpha API. You're not supposed to use it at all, except for - testing. It will very likely change. - """ - import jedi - import sys - - if '--debug' in sys.argv: - jedi.set_debug_function() - - for path in sys.argv[2:]: - if path.startswith('--'): - continue - if isdir(path): - import fnmatch - import os - - paths = [] - for root, dirnames, filenames in os.walk(path): - for filename in fnmatch.filter(filenames, '*.py'): - paths.append(os.path.join(root, filename)) - else: - paths = [path] - - try: - for path in paths: - for error in jedi.Script(path=path)._analysis(): - print(error) - except Exception: - if '--pdb' in sys.argv: - import pdb - pdb.post_mortem() - else: - raise diff --git a/pythonFiles/jedi/_compatibility.py b/pythonFiles/jedi/_compatibility.py deleted file mode 100644 index 63c76b9a9cb6..000000000000 --- a/pythonFiles/jedi/_compatibility.py +++ /dev/null @@ -1,205 +0,0 @@ -""" -To ensure compatibility from Python ``2.6`` - ``3.3``, a module has been -created. Clearly there is huge need to use conforming syntax. -""" -import sys -import imp -import os -import re -try: - import importlib -except ImportError: - pass - -is_py3 = sys.version_info[0] >= 3 -is_py33 = is_py3 and sys.version_info.minor >= 3 -is_py26 = not is_py3 and sys.version_info[1] < 7 - - -def find_module_py33(string, path=None): - loader = importlib.machinery.PathFinder.find_module(string, path) - - if loader is None and path is None: # Fallback to find builtins - try: - loader = importlib.find_loader(string) - except ValueError as e: - # See #491. Importlib might raise a ValueError, to avoid this, we - # just raise an ImportError to fix the issue. - raise ImportError("Originally " + repr(e)) - - if loader is None: - raise ImportError("Couldn't find a loader for {0}".format(string)) - - try: - is_package = loader.is_package(string) - if is_package: - module_path = os.path.dirname(loader.path) - module_file = None - else: - module_path = loader.get_filename(string) - module_file = open(module_path, 'rb') - except AttributeError: - # ExtensionLoader has not attribute get_filename, instead it has a - # path attribute that we can use to retrieve the module path - try: - module_path = loader.path - module_file = open(loader.path, 'rb') - except AttributeError: - module_path = string - module_file = None - finally: - is_package = False - - return module_file, module_path, is_package - - -def find_module_pre_py33(string, path=None): - module_file, module_path, description = imp.find_module(string, path) - module_type = description[2] - return module_file, module_path, module_type is imp.PKG_DIRECTORY - - -find_module = find_module_py33 if is_py33 else find_module_pre_py33 -find_module.__doc__ = """ -Provides information about a module. - -This function isolates the differences in importing libraries introduced with -python 3.3 on; it gets a module name and optionally a path. It will return a -tuple containin an open file for the module (if not builtin), the filename -or the name of the module if it is a builtin one and a boolean indicating -if the module is contained in a package. -""" - - -# unicode function -try: - unicode = unicode -except NameError: - unicode = str - -if is_py3: - u = lambda s: s -else: - u = lambda s: s.decode('utf-8') - -u.__doc__ = """ -Decode a raw string into unicode object. Do nothing in Python 3. -""" - -# exec function -if is_py3: - def exec_function(source, global_map): - exec(source, global_map) -else: - eval(compile("""def exec_function(source, global_map): - exec source in global_map """, 'blub', 'exec')) - -# re-raise function -if is_py3: - def reraise(exception, traceback): - raise exception.with_traceback(traceback) -else: - eval(compile(""" -def reraise(exception, traceback): - raise exception, None, traceback -""", 'blub', 'exec')) - -reraise.__doc__ = """ -Re-raise `exception` with a `traceback` object. - -Usage:: - - reraise(Exception, sys.exc_info()[2]) - -""" - -class Python3Method(object): - def __init__(self, func): - self.func = func - - def __get__(self, obj, objtype): - if obj is None: - return lambda *args, **kwargs: self.func(*args, **kwargs) - else: - return lambda *args, **kwargs: self.func(obj, *args, **kwargs) - - -def use_metaclass(meta, *bases): - """ Create a class with a metaclass. """ - if not bases: - bases = (object,) - return meta("HackClass", bases, {}) - - -try: - encoding = sys.stdout.encoding - if encoding is None: - encoding = 'utf-8' -except AttributeError: - encoding = 'ascii' - - -def u(string): - """Cast to unicode DAMMIT! - Written because Python2 repr always implicitly casts to a string, so we - have to cast back to a unicode (and we now that we always deal with valid - unicode, because we check that in the beginning). - """ - if is_py3: - return str(string) - elif not isinstance(string, unicode): - return unicode(str(string), 'UTF-8') - return string - -try: - import builtins # module name in python 3 -except ImportError: - import __builtin__ as builtins - - -import ast - - -def literal_eval(string): - # py3.0, py3.1 and py32 don't support unicode literals. Support those, I - # don't want to write two versions of the tokenizer. - if is_py3 and sys.version_info.minor < 3: - if re.match('[uU][\'"]', string): - string = string[1:] - return ast.literal_eval(string) - - -try: - from itertools import zip_longest -except ImportError: - from itertools import izip_longest as zip_longest # Python 2 - - -def no_unicode_pprint(dct): - """ - Python 2/3 dict __repr__ may be different, because of unicode differens - (with or without a `u` prefix). Normally in doctests we could use `pprint` - to sort dicts and check for equality, but here we have to write a separate - function to do that. - """ - import pprint - s = pprint.pformat(dct) - print(re.sub("u'", "'", s)) - - -def utf8_repr(func): - """ - ``__repr__`` methods in Python 2 don't allow unicode objects to be - returned. Therefore cast them to utf-8 bytes in this decorator. - """ - def wrapper(self): - result = func(self) - if isinstance(result, unicode): - return result.encode('utf-8') - else: - return result - - if is_py3: - return func - else: - return wrapper diff --git a/pythonFiles/jedi/api/__init__.py b/pythonFiles/jedi/api/__init__.py deleted file mode 100644 index 18ca0fc706cd..000000000000 --- a/pythonFiles/jedi/api/__init__.py +++ /dev/null @@ -1,713 +0,0 @@ -""" -The API basically only provides one class. You can create a :class:`Script` and -use its methods. - -Additionally you can add a debug function with :func:`set_debug_function`. - -.. warning:: Please, note that Jedi is **not thread safe**. -""" -import re -import os -import warnings -import sys -from itertools import chain - -from jedi._compatibility import unicode, builtins -from jedi.parser import Parser, load_grammar -from jedi.parser.tokenize import source_tokens -from jedi.parser import tree -from jedi.parser.user_context import UserContext, UserContextParser -from jedi import debug -from jedi import settings -from jedi import common -from jedi import cache -from jedi.api import keywords -from jedi.api import classes -from jedi.api import interpreter -from jedi.api import usages -from jedi.api import helpers -from jedi.evaluate import Evaluator -from jedi.evaluate import representation as er -from jedi.evaluate import compiled -from jedi.evaluate import imports -from jedi.evaluate.cache import memoize_default -from jedi.evaluate.helpers import FakeName, get_module_names -from jedi.evaluate.finder import global_names_dict_generator, filter_definition_names -from jedi.evaluate import analysis - -# Jedi uses lots and lots of recursion. By setting this a little bit higher, we -# can remove some "maximum recursion depth" errors. -sys.setrecursionlimit(2000) - - -class NotFoundError(Exception): - """A custom error to avoid catching the wrong exceptions. - - .. deprecated:: 0.9.0 - Not in use anymore, Jedi just returns no goto result if you're not on a - valid name. - .. todo:: Remove! - """ - - -class Script(object): - """ - A Script is the base for completions, goto or whatever you want to do with - |jedi|. - - You can either use the ``source`` parameter or ``path`` to read a file. - Usually you're going to want to use both of them (in an editor). - - :param source: The source code of the current file, separated by newlines. - :type source: str - :param line: The line to perform actions on (starting with 1). - :type line: int - :param column: The column of the cursor (starting with 0). - :type column: int - :param path: The path of the file in the file system, or ``''`` if - it hasn't been saved yet. - :type path: str or None - :param encoding: The encoding of ``source``, if it is not a - ``unicode`` object (default ``'utf-8'``). - :type encoding: str - :param source_encoding: The encoding of ``source``, if it is not a - ``unicode`` object (default ``'utf-8'``). - :type encoding: str - """ - def __init__(self, source=None, line=None, column=None, path=None, - encoding='utf-8', source_path=None, source_encoding=None): - if source_path is not None: - warnings.warn("Use path instead of source_path.", DeprecationWarning) - path = source_path - if source_encoding is not None: - warnings.warn("Use encoding instead of source_encoding.", DeprecationWarning) - encoding = source_encoding - - self._orig_path = path - self.path = None if path is None else os.path.abspath(path) - - if source is None: - with open(path) as f: - source = f.read() - - self.source = common.source_to_unicode(source, encoding) - lines = common.splitlines(self.source) - line = max(len(lines), 1) if line is None else line - if not (0 < line <= len(lines)): - raise ValueError('`line` parameter is not in a valid range.') - - line_len = len(lines[line - 1]) - column = line_len if column is None else column - if not (0 <= column <= line_len): - raise ValueError('`column` parameter is not in a valid range.') - self._pos = line, column - - cache.clear_time_caches() - debug.reset_time() - self._grammar = load_grammar('grammar%s.%s' % sys.version_info[:2]) - self._user_context = UserContext(self.source, self._pos) - self._parser = UserContextParser(self._grammar, self.source, path, - self._pos, self._user_context, - self._parsed_callback) - self._evaluator = Evaluator(self._grammar) - debug.speed('init') - - def _parsed_callback(self, parser): - module = self._evaluator.wrap(parser.module) - imports.add_module(self._evaluator, unicode(module.name), module) - - @property - def source_path(self): - """ - .. deprecated:: 0.7.0 - Use :attr:`.path` instead. - .. todo:: Remove! - """ - warnings.warn("Use path instead of source_path.", DeprecationWarning) - return self.path - - def __repr__(self): - return '<%s: %s>' % (self.__class__.__name__, repr(self._orig_path)) - - def completions(self): - """ - Return :class:`classes.Completion` objects. Those objects contain - information about the completions, more than just names. - - :return: Completion objects, sorted by name and __ comes last. - :rtype: list of :class:`classes.Completion` - """ - def get_completions(user_stmt, bs): - # TODO this closure is ugly. it also doesn't work with - # simple_complete (used for Interpreter), somehow redo. - module = self._evaluator.wrap(self._parser.module()) - names, level, only_modules, unfinished_dotted = \ - helpers.check_error_statements(module, self._pos) - completion_names = [] - if names is not None: - imp_names = tuple(str(n) for n in names if n.end_pos < self._pos) - i = imports.Importer(self._evaluator, imp_names, module, level) - completion_names = i.completion_names(self._evaluator, only_modules) - - # TODO this paragraph is necessary, but not sure it works. - context = self._user_context.get_context() - if not next(context).startswith('.'): # skip the path - if next(context) == 'from': - # completion is just "import" if before stands from .. - if unfinished_dotted: - return completion_names - else: - return set([keywords.keyword('import').name]) - - if isinstance(user_stmt, tree.Import): - module = self._parser.module() - completion_names += imports.completion_names(self._evaluator, - user_stmt, self._pos) - return completion_names - - if names is None and not isinstance(user_stmt, tree.Import): - if not path and not dot: - # add keywords - completion_names += keywords.completion_names( - self._evaluator, - user_stmt, - self._pos, - module) - # TODO delete? We should search for valid parser - # transformations. - completion_names += self._simple_complete(path, dot, like) - return completion_names - - debug.speed('completions start') - path = self._user_context.get_path_until_cursor() - # Dots following an int are not the start of a completion but a float - # literal. - if re.search(r'^\d\.$', path): - return [] - path, dot, like = helpers.completion_parts(path) - - user_stmt = self._parser.user_stmt_with_whitespace() - - b = compiled.builtin - completion_names = get_completions(user_stmt, b) - - if not dot: - # add named params - for call_sig in self.call_signatures(): - # Allow protected access, because it's a public API. - module = call_sig._name.get_parent_until() - # Compiled modules typically don't allow keyword arguments. - if not isinstance(module, compiled.CompiledObject): - for p in call_sig.params: - # Allow access on _definition here, because it's a - # public API and we don't want to make the internal - # Name object public. - if p._definition.stars == 0: # no *args/**kwargs - completion_names.append(p._name) - - needs_dot = not dot and path - - comps = [] - comp_dct = {} - for c in set(completion_names): - n = str(c) - if settings.case_insensitive_completion \ - and n.lower().startswith(like.lower()) \ - or n.startswith(like): - if isinstance(c.parent, (tree.Function, tree.Class)): - # TODO I think this is a hack. It should be an - # er.Function/er.Class before that. - c = self._evaluator.wrap(c.parent).name - new = classes.Completion(self._evaluator, c, needs_dot, len(like)) - k = (new.name, new.complete) # key - if k in comp_dct and settings.no_completion_duplicates: - comp_dct[k]._same_name_completions.append(new) - else: - comp_dct[k] = new - comps.append(new) - - debug.speed('completions end') - - return sorted(comps, key=lambda x: (x.name.startswith('__'), - x.name.startswith('_'), - x.name.lower())) - - def _simple_complete(self, path, dot, like): - if not path and not dot: - scope = self._parser.user_scope() - if not scope.is_scope(): # Might be a flow (if/while/etc). - scope = scope.get_parent_scope() - names_dicts = global_names_dict_generator( - self._evaluator, - self._evaluator.wrap(scope), - self._pos - ) - completion_names = [] - for names_dict, pos in names_dicts: - names = list(chain.from_iterable(names_dict.values())) - if not names: - continue - completion_names += filter_definition_names(names, self._parser.user_stmt(), pos) - elif self._get_under_cursor_stmt(path) is None: - return [] - else: - scopes = list(self._prepare_goto(path, True)) - completion_names = [] - debug.dbg('possible completion scopes: %s', scopes) - for s in scopes: - names = [] - for names_dict in s.names_dicts(search_global=False): - names += chain.from_iterable(names_dict.values()) - - completion_names += filter_definition_names(names, self._parser.user_stmt()) - return completion_names - - def _prepare_goto(self, goto_path, is_completion=False): - """ - Base for completions/goto. Basically it returns the resolved scopes - under cursor. - """ - debug.dbg('start: %s in %s', goto_path, self._parser.user_scope()) - - user_stmt = self._parser.user_stmt_with_whitespace() - if not user_stmt and len(goto_path.split('\n')) > 1: - # If the user_stmt is not defined and the goto_path is multi line, - # something's strange. Most probably the backwards tokenizer - # matched to much. - return [] - - if isinstance(user_stmt, tree.Import): - i, _ = helpers.get_on_import_stmt(self._evaluator, self._user_context, - user_stmt, is_completion) - if i is None: - return [] - scopes = [i] - else: - # just parse one statement, take it and evaluate it - eval_stmt = self._get_under_cursor_stmt(goto_path) - if eval_stmt is None: - return [] - - module = self._evaluator.wrap(self._parser.module()) - names, level, _, _ = helpers.check_error_statements(module, self._pos) - if names: - names = [str(n) for n in names] - i = imports.Importer(self._evaluator, names, module, level) - return i.follow() - - scopes = self._evaluator.eval_element(eval_stmt) - - return scopes - - @memoize_default() - def _get_under_cursor_stmt(self, cursor_txt, start_pos=None): - tokenizer = source_tokens(cursor_txt) - r = Parser(self._grammar, cursor_txt, tokenizer=tokenizer) - try: - # Take the last statement available that is not an endmarker. - # And because it's a simple_stmt, we need to get the first child. - stmt = r.module.children[-2].children[0] - except (AttributeError, IndexError): - return None - - user_stmt = self._parser.user_stmt() - if user_stmt is None: - # Set the start_pos to a pseudo position, that doesn't exist but - # works perfectly well (for both completions in docstrings and - # statements). - pos = start_pos or self._pos - else: - pos = user_stmt.start_pos - - stmt.move(pos[0] - 1, pos[1]) # Moving the offset. - stmt.parent = self._parser.user_scope() - return stmt - - def goto_definitions(self): - """ - Return the definitions of a the path under the cursor. goto function! - This follows complicated paths and returns the end, not the first - definition. The big difference between :meth:`goto_assignments` and - :meth:`goto_definitions` is that :meth:`goto_assignments` doesn't - follow imports and statements. Multiple objects may be returned, - because Python itself is a dynamic language, which means depending on - an option you can have two different versions of a function. - - :rtype: list of :class:`classes.Definition` - """ - def resolve_import_paths(scopes): - for s in scopes.copy(): - if isinstance(s, imports.ImportWrapper): - scopes.remove(s) - scopes.update(resolve_import_paths(set(s.follow()))) - return scopes - - goto_path = self._user_context.get_path_under_cursor() - context = self._user_context.get_context() - definitions = set() - if next(context) in ('class', 'def'): - definitions = set([self._evaluator.wrap(self._parser.user_scope())]) - else: - # Fetch definition of callee, if there's no path otherwise. - if not goto_path: - definitions = set(signature._definition - for signature in self.call_signatures()) - - if re.match('\w[\w\d_]*$', goto_path) and not definitions: - user_stmt = self._parser.user_stmt() - if user_stmt is not None and user_stmt.type == 'expr_stmt': - for name in user_stmt.get_defined_names(): - if name.start_pos <= self._pos <= name.end_pos: - # TODO scaning for a name and then using it should be - # the default. - definitions = set(self._evaluator.goto_definition(name)) - - if not definitions and goto_path: - definitions = set(self._prepare_goto(goto_path)) - - definitions = resolve_import_paths(definitions) - names = [s.name for s in definitions] - defs = [classes.Definition(self._evaluator, name) for name in names] - return helpers.sorted_definitions(set(defs)) - - def goto_assignments(self): - """ - Return the first definition found. Imports and statements aren't - followed. Multiple objects may be returned, because Python itself is a - dynamic language, which means depending on an option you can have two - different versions of a function. - - :rtype: list of :class:`classes.Definition` - """ - results = self._goto() - d = [classes.Definition(self._evaluator, d) for d in set(results)] - return helpers.sorted_definitions(d) - - def _goto(self, add_import_name=False): - """ - Used for goto_assignments and usages. - - :param add_import_name: Add the the name (if import) to the result. - """ - def follow_inexistent_imports(defs): - """ Imports can be generated, e.g. following - `multiprocessing.dummy` generates an import dummy in the - multiprocessing module. The Import doesn't exist -> follow. - """ - definitions = set(defs) - for d in defs: - if isinstance(d.parent, tree.Import) \ - and d.start_pos == (0, 0): - i = imports.ImportWrapper(self._evaluator, d.parent).follow(is_goto=True) - definitions.remove(d) - definitions |= follow_inexistent_imports(i) - return definitions - - goto_path = self._user_context.get_path_under_cursor() - context = self._user_context.get_context() - user_stmt = self._parser.user_stmt() - user_scope = self._parser.user_scope() - - stmt = self._get_under_cursor_stmt(goto_path) - if stmt is None: - return [] - - if user_scope is None: - last_name = None - else: - # Try to use the parser if possible. - last_name = user_scope.name_for_position(self._pos) - - if last_name is None: - last_name = stmt - while not isinstance(last_name, tree.Name): - try: - last_name = last_name.children[-1] - except AttributeError: - # Doesn't have a name in it. - return [] - - if next(context) in ('class', 'def'): - # The cursor is on a class/function name. - user_scope = self._parser.user_scope() - definitions = set([user_scope.name]) - elif isinstance(user_stmt, tree.Import): - s, name = helpers.get_on_import_stmt(self._evaluator, - self._user_context, user_stmt) - - definitions = self._evaluator.goto(name) - else: - # The Evaluator.goto function checks for definitions, but since we - # use a reverse tokenizer, we have new name_part objects, so we - # have to check the user_stmt here for positions. - if isinstance(user_stmt, tree.ExprStmt) \ - and isinstance(last_name.parent, tree.ExprStmt): - for name in user_stmt.get_defined_names(): - if name.start_pos <= self._pos <= name.end_pos: - return [name] - - defs = self._evaluator.goto(last_name) - definitions = follow_inexistent_imports(defs) - return definitions - - def usages(self, additional_module_paths=()): - """ - Return :class:`classes.Definition` objects, which contain all - names that point to the definition of the name under the cursor. This - is very useful for refactoring (renaming), or to show all usages of a - variable. - - .. todo:: Implement additional_module_paths - - :rtype: list of :class:`classes.Definition` - """ - temp, settings.dynamic_flow_information = \ - settings.dynamic_flow_information, False - try: - user_stmt = self._parser.user_stmt() - definitions = self._goto(add_import_name=True) - if not definitions and isinstance(user_stmt, tree.Import): - # For not defined imports (goto doesn't find something, we take - # the name as a definition. This is enough, because every name - # points to it. - name = user_stmt.name_for_position(self._pos) - if name is None: - # Must be syntax - return [] - definitions = [name] - - if not definitions: - # Without a definition for a name we cannot find references. - return [] - - if not isinstance(user_stmt, tree.Import): - # import case is looked at with add_import_name option - definitions = usages.usages_add_import_modules(self._evaluator, - definitions) - - module = set([d.get_parent_until() for d in definitions]) - module.add(self._parser.module()) - names = usages.usages(self._evaluator, definitions, module) - - for d in set(definitions): - names.append(classes.Definition(self._evaluator, d)) - finally: - settings.dynamic_flow_information = temp - - return helpers.sorted_definitions(set(names)) - - def call_signatures(self): - """ - Return the function object of the call you're currently in. - - E.g. if the cursor is here:: - - abs(# <-- cursor is here - - This would return the ``abs`` function. On the other hand:: - - abs()# <-- cursor is here - - This would return ``None``. - - :rtype: list of :class:`classes.CallSignature` - """ - call_txt, call_index, key_name, start_pos = self._user_context.call_signature() - if call_txt is None: - return [] - - stmt = self._get_under_cursor_stmt(call_txt, start_pos) - if stmt is None: - return [] - - with common.scale_speed_settings(settings.scale_call_signatures): - origins = cache.cache_call_signatures(self._evaluator, stmt, - self.source, self._pos) - debug.speed('func_call followed') - - return [classes.CallSignature(self._evaluator, o.name, stmt, call_index, key_name) - for o in origins if hasattr(o, 'py__call__')] - - def _analysis(self): - def check_types(types): - for typ in types: - try: - f = typ.iter_content - except AttributeError: - pass - else: - check_types(f()) - - #statements = set(chain(*self._parser.module().used_names.values())) - nodes, imp_names, decorated_funcs = \ - analysis.get_module_statements(self._parser.module()) - # Sort the statements so that the results are reproducible. - for n in imp_names: - imports.ImportWrapper(self._evaluator, n).follow() - for node in sorted(nodes, key=lambda obj: obj.start_pos): - check_types(self._evaluator.eval_element(node)) - - for dec_func in decorated_funcs: - er.Function(self._evaluator, dec_func).get_decorated_func() - - ana = [a for a in self._evaluator.analysis if self.path == a.path] - return sorted(set(ana), key=lambda x: x.line) - - -class Interpreter(Script): - """ - Jedi API for Python REPLs. - - In addition to completion of simple attribute access, Jedi - supports code completion based on static code analysis. - Jedi can complete attributes of object which is not initialized - yet. - - >>> from os.path import join - >>> namespace = locals() - >>> script = Interpreter('join().up', [namespace]) - >>> print(script.completions()[0].name) - upper - """ - - def __init__(self, source, namespaces, **kwds): - """ - Parse `source` and mixin interpreted Python objects from `namespaces`. - - :type source: str - :arg source: Code to parse. - :type namespaces: list of dict - :arg namespaces: a list of namespace dictionaries such as the one - returned by :func:`locals`. - - Other optional arguments are same as the ones for :class:`Script`. - If `line` and `column` are None, they are assumed be at the end of - `source`. - """ - if type(namespaces) is not list or len(namespaces) == 0 or \ - any([type(x) is not dict for x in namespaces]): - raise TypeError("namespaces must be a non-empty list of dict") - - super(Interpreter, self).__init__(source, **kwds) - self.namespaces = namespaces - - # Don't use the fast parser, because it does crazy stuff that we don't - # need in our very simple and small code here (that is always - # changing). - self._parser = UserContextParser(self._grammar, self.source, - self._orig_path, self._pos, - self._user_context, self._parsed_callback, - use_fast_parser=False) - interpreter.add_namespaces_to_parser(self._evaluator, namespaces, - self._parser.module()) - - def _simple_complete(self, path, dot, like): - user_stmt = self._parser.user_stmt_with_whitespace() - is_simple_path = not path or re.search('^[\w][\w\d.]*$', path) - if isinstance(user_stmt, tree.Import) or not is_simple_path: - return super(Interpreter, self)._simple_complete(path, dot, like) - else: - class NamespaceModule(object): - def __getattr__(_, name): - for n in self.namespaces: - try: - return n[name] - except KeyError: - pass - raise AttributeError() - - def __dir__(_): - gen = (n.keys() for n in self.namespaces) - return list(set(chain.from_iterable(gen))) - - paths = path.split('.') if path else [] - - namespaces = (NamespaceModule(), builtins) - for p in paths: - old, namespaces = namespaces, [] - for n in old: - try: - namespaces.append(getattr(n, p)) - except Exception: - pass - - completion_names = [] - for namespace in namespaces: - for name in dir(namespace): - if name.lower().startswith(like.lower()): - scope = self._parser.module() - n = FakeName(name, scope) - completion_names.append(n) - return completion_names - - -def defined_names(source, path=None, encoding='utf-8'): - """ - Get all definitions in `source` sorted by its position. - - This functions can be used for listing functions, classes and - data defined in a file. This can be useful if you want to list - them in "sidebar". Each element in the returned list also has - `defined_names` method which can be used to get sub-definitions - (e.g., methods in class). - - :rtype: list of classes.Definition - - .. deprecated:: 0.9.0 - Use :func:`names` instead. - .. todo:: Remove! - """ - warnings.warn("Use call_signatures instead.", DeprecationWarning) - return names(source, path, encoding) - - -def names(source=None, path=None, encoding='utf-8', all_scopes=False, - definitions=True, references=False): - """ - Returns a list of `Definition` objects, containing name parts. - This means you can call ``Definition.goto_assignments()`` and get the - reference of a name. - The parameters are the same as in :py:class:`Script`, except or the - following ones: - - :param all_scopes: If True lists the names of all scopes instead of only - the module namespace. - :param definitions: If True lists the names that have been defined by a - class, function or a statement (``a = b`` returns ``a``). - :param references: If True lists all the names that are not listed by - ``definitions=True``. E.g. ``a = b`` returns ``b``. - """ - def def_ref_filter(_def): - is_def = _def.is_definition() - return definitions and is_def or references and not is_def - - # Set line/column to a random position, because they don't matter. - script = Script(source, line=1, column=0, path=path, encoding=encoding) - defs = [classes.Definition(script._evaluator, name_part) - for name_part in get_module_names(script._parser.module(), all_scopes)] - return sorted(filter(def_ref_filter, defs), key=lambda x: (x.line, x.column)) - - -def preload_module(*modules): - """ - Preloading modules tells Jedi to load a module now, instead of lazy parsing - of modules. Usful for IDEs, to control which modules to load on startup. - - :param modules: different module names, list of string. - """ - for m in modules: - s = "import %s as x; x." % m - Script(s, 1, len(s), None).completions() - - -def set_debug_function(func_cb=debug.print_to_stdout, warnings=True, - notices=True, speed=True): - """ - Define a callback debug function to get all the debug messages. - - :param func_cb: The callback function for debug messages, with n params. - """ - debug.debug_function = func_cb - debug.enable_warning = warnings - debug.enable_notice = notices - debug.enable_speed = speed diff --git a/pythonFiles/jedi/api/classes.py b/pythonFiles/jedi/api/classes.py deleted file mode 100644 index a1d42bd0ffe2..000000000000 --- a/pythonFiles/jedi/api/classes.py +++ /dev/null @@ -1,735 +0,0 @@ -""" -The :mod:`jedi.api.classes` module contains the return classes of the API. -These classes are the much bigger part of the whole API, because they contain -the interesting information about completion and goto operations. -""" -import warnings -from itertools import chain -import re - -from jedi._compatibility import unicode, use_metaclass -from jedi import settings -from jedi import common -from jedi.parser import tree -from jedi.evaluate.cache import memoize_default, CachedMetaClass -from jedi.evaluate import representation as er -from jedi.evaluate import iterable -from jedi.evaluate import imports -from jedi.evaluate import compiled -from jedi.api import keywords -from jedi.evaluate.finder import filter_definition_names - - -def defined_names(evaluator, scope): - """ - List sub-definitions (e.g., methods in class). - - :type scope: Scope - :rtype: list of Definition - """ - dct = scope.names_dict - names = list(chain.from_iterable(dct.values())) - names = filter_definition_names(names, scope) - return [Definition(evaluator, d) for d in sorted(names, key=lambda s: s.start_pos)] - - -class BaseDefinition(object): - _mapping = { - 'posixpath': 'os.path', - 'riscospath': 'os.path', - 'ntpath': 'os.path', - 'os2emxpath': 'os.path', - 'macpath': 'os.path', - 'genericpath': 'os.path', - 'posix': 'os', - '_io': 'io', - '_functools': 'functools', - '_sqlite3': 'sqlite3', - '__builtin__': '', - 'builtins': '', - } - - _tuple_mapping = dict((tuple(k.split('.')), v) for (k, v) in { - 'argparse._ActionsContainer': 'argparse.ArgumentParser', - '_sre.SRE_Match': 're.MatchObject', - '_sre.SRE_Pattern': 're.RegexObject', - }.items()) - - def __init__(self, evaluator, name): - self._evaluator = evaluator - self._name = name - """ - An instance of :class:`jedi.parser.reprsentation.Name` subclass. - """ - self._definition = evaluator.wrap(self._name.get_definition()) - self.is_keyword = isinstance(self._definition, keywords.Keyword) - - # generate a path to the definition - self._module = name.get_parent_until() - if self.in_builtin_module(): - self.module_path = None - else: - self.module_path = self._module.path - """Shows the file path of a module. e.g. ``/usr/lib/python2.7/os.py``""" - - @property - def name(self): - """ - Name of variable/function/class/module. - - For example, for ``x = None`` it returns ``'x'``. - - :rtype: str or None - """ - return unicode(self._name) - - @property - def start_pos(self): - """ - .. deprecated:: 0.7.0 - Use :attr:`.line` and :attr:`.column` instead. - .. todo:: Remove! - """ - warnings.warn("Use line/column instead.", DeprecationWarning) - return self._name.start_pos - - @property - def type(self): - """ - The type of the definition. - - Here is an example of the value of this attribute. Let's consider - the following source. As what is in ``variable`` is unambiguous - to Jedi, :meth:`jedi.Script.goto_definitions` should return a list of - definition for ``sys``, ``f``, ``C`` and ``x``. - - >>> from jedi import Script - >>> source = ''' - ... import keyword - ... - ... class C: - ... pass - ... - ... class D: - ... pass - ... - ... x = D() - ... - ... def f(): - ... pass - ... - ... for variable in [keyword, f, C, x]: - ... variable''' - - >>> script = Script(source) - >>> defs = script.goto_definitions() - - Before showing what is in ``defs``, let's sort it by :attr:`line` - so that it is easy to relate the result to the source code. - - >>> defs = sorted(defs, key=lambda d: d.line) - >>> defs # doctest: +NORMALIZE_WHITESPACE - [, , - , ] - - Finally, here is what you can get from :attr:`type`: - - >>> defs[0].type - 'module' - >>> defs[1].type - 'class' - >>> defs[2].type - 'instance' - >>> defs[3].type - 'function' - - """ - stripped = self._definition - if isinstance(stripped, er.InstanceElement): - stripped = stripped.var - - if isinstance(stripped, compiled.CompiledObject): - return stripped.api_type() - elif isinstance(stripped, iterable.Array): - return 'instance' - elif isinstance(stripped, tree.Import): - return 'import' - - string = type(stripped).__name__.lower().replace('wrapper', '') - if string == 'exprstmt': - return 'statement' - else: - return string - - def _path(self): - """The path to a module/class/function definition.""" - path = [] - par = self._definition - while par is not None: - if isinstance(par, tree.Import): - path += imports.ImportWrapper(self._evaluator, self._name).import_path - break - try: - name = par.name - except AttributeError: - pass - else: - if isinstance(par, er.ModuleWrapper): - # TODO just make the path dotted from the beginning, we - # shouldn't really split here. - path[0:0] = par.py__name__().split('.') - break - else: - path.insert(0, unicode(name)) - par = par.parent - return path - - @property - def module_name(self): - """ - The module name. - - >>> from jedi import Script - >>> source = 'import json' - >>> script = Script(source, path='example.py') - >>> d = script.goto_definitions()[0] - >>> print(d.module_name) # doctest: +ELLIPSIS - json - """ - return str(self._module.name) - - def in_builtin_module(self): - """Whether this is a builtin module.""" - return isinstance(self._module, compiled.CompiledObject) - - @property - def line(self): - """The line where the definition occurs (starting with 1).""" - if self.in_builtin_module(): - return None - return self._name.start_pos[0] - - @property - def column(self): - """The column where the definition occurs (starting with 0).""" - if self.in_builtin_module(): - return None - return self._name.start_pos[1] - - def docstring(self, raw=False): - r""" - Return a document string for this completion object. - - Example: - - >>> from jedi import Script - >>> source = '''\ - ... def f(a, b=1): - ... "Document for function f." - ... ''' - >>> script = Script(source, 1, len('def f'), 'example.py') - >>> doc = script.goto_definitions()[0].docstring() - >>> print(doc) - f(a, b=1) - - Document for function f. - - Notice that useful extra information is added to the actual - docstring. For function, it is call signature. If you need - actual docstring, use ``raw=True`` instead. - - >>> print(script.goto_definitions()[0].docstring(raw=True)) - Document for function f. - - """ - if raw: - return _Help(self._definition).raw() - else: - return _Help(self._definition).full() - - @property - def doc(self): - """ - .. deprecated:: 0.8.0 - Use :meth:`.docstring` instead. - .. todo:: Remove! - """ - warnings.warn("Use docstring() instead.", DeprecationWarning) - return self.docstring() - - @property - def raw_doc(self): - """ - .. deprecated:: 0.8.0 - Use :meth:`.docstring` instead. - .. todo:: Remove! - """ - warnings.warn("Use docstring() instead.", DeprecationWarning) - return self.docstring(raw=True) - - @property - def description(self): - """A textual description of the object.""" - return unicode(self._name) - - @property - def full_name(self): - """ - Dot-separated path of this object. - - It is in the form of ``[.[...]][.]``. - It is useful when you want to look up Python manual of the - object at hand. - - Example: - - >>> from jedi import Script - >>> source = ''' - ... import os - ... os.path.join''' - >>> script = Script(source, 3, len('os.path.join'), 'example.py') - >>> print(script.goto_definitions()[0].full_name) - os.path.join - - Notice that it correctly returns ``'os.path.join'`` instead of - (for example) ``'posixpath.join'``. - - """ - path = [unicode(p) for p in self._path()] - # TODO add further checks, the mapping should only occur on stdlib. - if not path: - return None # for keywords the path is empty - - with common.ignored(KeyError): - path[0] = self._mapping[path[0]] - for key, repl in self._tuple_mapping.items(): - if tuple(path[:len(key)]) == key: - path = [repl] + path[len(key):] - - return '.'.join(path if path[0] else path[1:]) - - def goto_assignments(self): - defs = self._evaluator.goto(self._name) - return [Definition(self._evaluator, d) for d in defs] - - @memoize_default() - def _follow_statements_imports(self): - """ - Follow both statements and imports, as far as possible. - """ - if self._definition.isinstance(tree.ExprStmt): - return self._evaluator.eval_statement(self._definition) - elif self._definition.isinstance(tree.Import): - return imports.ImportWrapper(self._evaluator, self._name).follow() - else: - return [self._definition] - - @property - @memoize_default() - def params(self): - """ - Raises an ``AttributeError``if the definition is not callable. - Otherwise returns a list of `Definition` that represents the params. - """ - followed = self._follow_statements_imports() - if not followed or not hasattr(followed[0], 'py__call__'): - raise AttributeError() - followed = followed[0] # only check the first one. - - if followed.type == 'funcdef': - if isinstance(followed, er.InstanceElement): - params = followed.params[1:] - else: - params = followed.params - elif followed.isinstance(er.compiled.CompiledObject): - params = followed.params - else: - try: - sub = followed.get_subscope_by_name('__init__') - params = sub.params[1:] # ignore self - except KeyError: - return [] - return [_Param(self._evaluator, p.name) for p in params] - - def parent(self): - scope = self._definition.get_parent_scope() - scope = self._evaluator.wrap(scope) - return Definition(self._evaluator, scope.name) - - def __repr__(self): - return "<%s %s>" % (type(self).__name__, self.description) - - -class Completion(BaseDefinition): - """ - `Completion` objects are returned from :meth:`api.Script.completions`. They - provide additional information about a completion. - """ - def __init__(self, evaluator, name, needs_dot, like_name_length): - super(Completion, self).__init__(evaluator, name) - - self._needs_dot = needs_dot - self._like_name_length = like_name_length - - # Completion objects with the same Completion name (which means - # duplicate items in the completion) - self._same_name_completions = [] - - def _complete(self, like_name): - dot = '.' if self._needs_dot else '' - append = '' - if settings.add_bracket_after_function \ - and self.type == 'Function': - append = '(' - - if settings.add_dot_after_module: - if isinstance(self._definition, tree.Module): - append += '.' - if isinstance(self._definition, tree.Param): - append += '=' - - name = str(self._name) - if like_name: - name = name[self._like_name_length:] - return dot + name + append - - @property - def complete(self): - """ - Return the rest of the word, e.g. completing ``isinstance``:: - - isinstan# <-- Cursor is here - - would return the string 'ce'. It also adds additional stuff, depending - on your `settings.py`. - """ - return self._complete(True) - - @property - def name_with_symbols(self): - """ - Similar to :attr:`name`, but like :attr:`name` - returns also the symbols, for example:: - - list() - - would return ``.append`` and others (which means it adds a dot). - """ - return self._complete(False) - - @property - def description(self): - """Provide a description of the completion object.""" - if self._definition is None: - return '' - t = self.type - if t == 'statement' or t == 'import': - desc = self._definition.get_code() - else: - desc = '.'.join(unicode(p) for p in self._path()) - - line = '' if self.in_builtin_module else '@%s' % self.line - return '%s: %s%s' % (t, desc, line) - - def __repr__(self): - return '<%s: %s>' % (type(self).__name__, self._name) - - def docstring(self, raw=False, fast=True): - """ - :param fast: Don't follow imports that are only one level deep like - ``import foo``, but follow ``from foo import bar``. This makes - sense for speed reasons. Completing `import a` is slow if you use - the ``foo.docstring(fast=False)`` on every object, because it - parses all libraries starting with ``a``. - """ - definition = self._definition - if isinstance(definition, tree.Import): - i = imports.ImportWrapper(self._evaluator, self._name) - if len(i.import_path) > 1 or not fast: - followed = self._follow_statements_imports() - if followed: - # TODO: Use all of the followed objects as input to Documentation. - definition = followed[0] - - if raw: - return _Help(definition).raw() - else: - return _Help(definition).full() - - @property - def type(self): - """ - The type of the completion objects. Follows imports. For a further - description, look at :attr:`jedi.api.classes.BaseDefinition.type`. - """ - if isinstance(self._definition, tree.Import): - i = imports.ImportWrapper(self._evaluator, self._name) - if len(i.import_path) <= 1: - return 'module' - - followed = self.follow_definition() - if followed: - # Caveat: Only follows the first one, ignore the other ones. - # This is ok, since people are almost never interested in - # variations. - return followed[0].type - return super(Completion, self).type - - @memoize_default() - def _follow_statements_imports(self): - # imports completion is very complicated and needs to be treated - # separately in Completion. - definition = self._definition - if definition.isinstance(tree.Import): - i = imports.ImportWrapper(self._evaluator, self._name) - return i.follow() - return super(Completion, self)._follow_statements_imports() - - @memoize_default() - def follow_definition(self): - """ - Return the original definitions. I strongly recommend not using it for - your completions, because it might slow down |jedi|. If you want to - read only a few objects (<=20), it might be useful, especially to get - the original docstrings. The basic problem of this function is that it - follows all results. This means with 1000 completions (e.g. numpy), - it's just PITA-slow. - """ - defs = self._follow_statements_imports() - return [Definition(self._evaluator, d.name) for d in defs] - - -class Definition(use_metaclass(CachedMetaClass, BaseDefinition)): - """ - *Definition* objects are returned from :meth:`api.Script.goto_assignments` - or :meth:`api.Script.goto_definitions`. - """ - def __init__(self, evaluator, definition): - super(Definition, self).__init__(evaluator, definition) - - @property - def description(self): - """ - A description of the :class:`.Definition` object, which is heavily used - in testing. e.g. for ``isinstance`` it returns ``def isinstance``. - - Example: - - >>> from jedi import Script - >>> source = ''' - ... def f(): - ... pass - ... - ... class C: - ... pass - ... - ... variable = f if random.choice([0,1]) else C''' - >>> script = Script(source, column=3) # line is maximum by default - >>> defs = script.goto_definitions() - >>> defs = sorted(defs, key=lambda d: d.line) - >>> defs - [, ] - >>> str(defs[0].description) # strip literals in python2 - 'def f' - >>> str(defs[1].description) - 'class C' - - """ - d = self._definition - if isinstance(d, er.InstanceElement): - d = d.var - - if isinstance(d, compiled.CompiledObject): - typ = d.api_type() - if typ == 'instance': - typ = 'class' # The description should be similar to Py objects. - d = typ + ' ' + d.name.get_code() - elif isinstance(d, iterable.Array): - d = 'class ' + d.type - elif isinstance(d, (tree.Class, er.Class, er.Instance)): - d = 'class ' + unicode(d.name) - elif isinstance(d, (er.Function, tree.Function)): - d = 'def ' + unicode(d.name) - elif isinstance(d, tree.Module): - # only show module name - d = 'module %s' % self.module_name - elif isinstance(d, tree.Param): - d = d.get_code().strip() - if d.endswith(','): - d = d[:-1] # Remove the comma. - else: # ExprStmt - try: - first_leaf = d.first_leaf() - except AttributeError: - # `d` is already a Leaf (Name). - first_leaf = d - # Remove the prefix, because that's not what we want for get_code - # here. - old, first_leaf.prefix = first_leaf.prefix, '' - try: - d = d.get_code() - finally: - first_leaf.prefix = old - # Delete comments: - d = re.sub('#[^\n]+\n', ' ', d) - # Delete multi spaces/newlines - return re.sub('\s+', ' ', d).strip() - - @property - def desc_with_module(self): - """ - In addition to the definition, also return the module. - - .. warning:: Don't use this function yet, its behaviour may change. If - you really need it, talk to me. - - .. todo:: Add full path. This function is should return a - `module.class.function` path. - """ - position = '' if self.in_builtin_module else '@%s' % (self.line) - return "%s:%s%s" % (self.module_name, self.description, position) - - @memoize_default() - def defined_names(self): - """ - List sub-definitions (e.g., methods in class). - - :rtype: list of Definition - """ - defs = self._follow_statements_imports() - # For now we don't want base classes or evaluate decorators. - defs = [d.base if isinstance(d, (er.Class, er.Function)) else d for d in defs] - iterable = (defined_names(self._evaluator, d) for d in defs) - iterable = list(iterable) - return list(chain.from_iterable(iterable)) - - def is_definition(self): - """ - Returns True, if defined as a name in a statement, function or class. - Returns False, if it's a reference to such a definition. - """ - return self._name.is_definition() - - def __eq__(self, other): - return self._name.start_pos == other._name.start_pos \ - and self.module_path == other.module_path \ - and self.name == other.name \ - and self._evaluator == other._evaluator - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash((self._name.start_pos, self.module_path, self.name, self._evaluator)) - - -class CallSignature(Definition): - """ - `CallSignature` objects is the return value of `Script.function_definition`. - It knows what functions you are currently in. e.g. `isinstance(` would - return the `isinstance` function. without `(` it would return nothing. - """ - def __init__(self, evaluator, executable_name, call_stmt, index, key_name): - super(CallSignature, self).__init__(evaluator, executable_name) - self._index = index - self._key_name = key_name - self._call_stmt = call_stmt - - @property - def index(self): - """ - The Param index of the current call. - Returns None if the index cannot be found in the curent call. - """ - if self._key_name is not None: - for i, param in enumerate(self.params): - if self._key_name == param.name: - return i - if self.params and self.params[-1]._name.get_definition().stars == 2: - return i - else: - return None - - if self._index >= len(self.params): - - for i, param in enumerate(self.params): - # *args case - if param._name.get_definition().stars == 1: - return i - return None - return self._index - - @property - def bracket_start(self): - """ - The indent of the bracket that is responsible for the last function - call. - """ - return self._call_stmt.end_pos - - @property - def call_name(self): - """ - .. deprecated:: 0.8.0 - Use :attr:`.name` instead. - .. todo:: Remove! - - The name (e.g. 'isinstance') as a string. - """ - warnings.warn("Use name instead.", DeprecationWarning) - return unicode(self.name) - - @property - def module(self): - """ - .. deprecated:: 0.8.0 - Use :attr:`.module_name` for the module name. - .. todo:: Remove! - """ - return self._executable.get_parent_until() - - def __repr__(self): - return '<%s: %s index %s>' % (type(self).__name__, self._name, - self.index) - - -class _Param(Definition): - """ - Just here for backwards compatibility. - """ - def get_code(self): - """ - .. deprecated:: 0.8.0 - Use :attr:`.description` and :attr:`.name` instead. - .. todo:: Remove! - - A function to get the whole code of the param. - """ - warnings.warn("Use description instead.", DeprecationWarning) - return self.description - - -class _Help(object): - """ - Temporary implementation, will be used as `Script.help() or something in - the future. - """ - def __init__(self, definition): - self._name = definition - - def full(self): - try: - return self._name.doc - except AttributeError: - return self.raw() - - def raw(self): - """ - The raw docstring ``__doc__`` for any object. - - See :attr:`doc` for example. - """ - try: - return self._name.raw_doc - except AttributeError: - return '' diff --git a/pythonFiles/jedi/api/helpers.py b/pythonFiles/jedi/api/helpers.py deleted file mode 100644 index b1b3f6e441c9..000000000000 --- a/pythonFiles/jedi/api/helpers.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -Helpers for the API -""" -import re - -from jedi.parser import tree as pt -from jedi.evaluate import imports - - -def completion_parts(path_until_cursor): - """ - Returns the parts for the completion - :return: tuple - (path, dot, like) - """ - match = re.match(r'^(.*?)(\.|)(\w?[\w\d]*)$', path_until_cursor, flags=re.S) - return match.groups() - - -def sorted_definitions(defs): - # Note: `or ''` below is required because `module_path` could be - return sorted(defs, key=lambda x: (x.module_path or '', x.line or 0, x.column or 0)) - - -def get_on_import_stmt(evaluator, user_context, user_stmt, is_like_search=False): - """ - Resolve the user statement, if it is an import. Only resolve the - parts until the user position. - """ - name = user_stmt.name_for_position(user_context.position) - if name is None: - return None, None - - i = imports.ImportWrapper(evaluator, name) - return i, name - - -def check_error_statements(module, pos): - for error_statement in module.error_statement_stacks: - if error_statement.first_type in ('import_from', 'import_name') \ - and error_statement.first_pos < pos <= error_statement.next_start_pos: - return importer_from_error_statement(error_statement, pos) - return None, 0, False, False - - -def importer_from_error_statement(error_statement, pos): - def check_dotted(children): - for name in children[::2]: - if name.start_pos <= pos: - yield name - - names = [] - level = 0 - only_modules = True - unfinished_dotted = False - for typ, nodes in error_statement.stack: - if typ == 'dotted_name': - names += check_dotted(nodes) - if nodes[-1] == '.': - # An unfinished dotted_name - unfinished_dotted = True - elif typ == 'import_name': - if nodes[0].start_pos <= pos <= nodes[0].end_pos: - # We are on the import. - return None, 0, False, False - elif typ == 'import_from': - for node in nodes: - if node.start_pos >= pos: - break - elif isinstance(node, pt.Node) and node.type == 'dotted_name': - names += check_dotted(node.children) - elif node in ('.', '...'): - level += len(node.value) - elif isinstance(node, pt.Name): - names.append(node) - elif node == 'import': - only_modules = False - - return names, level, only_modules, unfinished_dotted diff --git a/pythonFiles/jedi/api/interpreter.py b/pythonFiles/jedi/api/interpreter.py deleted file mode 100644 index 595435c61589..000000000000 --- a/pythonFiles/jedi/api/interpreter.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -TODO Some parts of this module are still not well documented. -""" -import inspect -import re - -from jedi._compatibility import builtins -from jedi import debug -from jedi.common import source_to_unicode -from jedi.cache import underscore_memoization -from jedi.evaluate import compiled -from jedi.evaluate.compiled.fake import get_module -from jedi.parser import tree as pt -from jedi.parser import load_grammar -from jedi.parser.fast import FastParser -from jedi.evaluate import helpers -from jedi.evaluate import iterable -from jedi.evaluate import representation as er - - -def add_namespaces_to_parser(evaluator, namespaces, parser_module): - for namespace in namespaces: - for key, value in namespace.items(): - # Name lookups in an ast tree work by checking names_dict. - # Therefore we just add fake names to that and we're done. - arr = parser_module.names_dict.setdefault(key, []) - arr.append(LazyName(evaluator, parser_module, key, value)) - - -class LazyName(helpers.FakeName): - def __init__(self, evaluator, module, name, value): - super(LazyName, self).__init__(name) - self._module = module - self._evaluator = evaluator - self._value = value - self._name = name - - def is_definition(self): - return True - - @property - @underscore_memoization - def parent(self): - """ - Creating fake statements for the interpreter. - """ - obj = self._value - parser_path = [] - if inspect.ismodule(obj): - module = obj - else: - names = [] - try: - o = obj.__objclass__ - names.append(obj.__name__) - obj = o - except AttributeError: - pass - - try: - module_name = obj.__module__ - names.insert(0, obj.__name__) - except AttributeError: - # Unfortunately in some cases like `int` there's no __module__ - module = builtins - else: - # TODO this import is wrong. Yields x for x.y.z instead of z - module = __import__(module_name) - parser_path = names - raw_module = get_module(self._value) - - found = [] - try: - path = module.__file__ - except AttributeError: - pass - else: - path = re.sub('c$', '', path) - if path.endswith('.py'): - # cut the `c` from `.pyc` - with open(path) as f: - source = source_to_unicode(f.read()) - mod = FastParser(load_grammar(), source, path[:-1]).module - if parser_path: - assert len(parser_path) == 1 - found = self._evaluator.find_types(mod, parser_path[0], search_global=True) - else: - found = [self._evaluator.wrap(mod)] - - if not found: - debug.warning('Possibly an interpreter lookup for Python code failed %s', - parser_path) - - if not found: - evaluated = compiled.CompiledObject(obj) - if evaluated == builtins: - # The builtins module is special and always cached. - evaluated = compiled.builtin - found = [evaluated] - - content = iterable.AlreadyEvaluated(found) - stmt = pt.ExprStmt([self, pt.Operator(pt.zero_position_modifier, - '=', (0, 0), ''), content]) - stmt.parent = self._module - return stmt - - @parent.setter - def parent(self, value): - """Needed because the super class tries to set parent.""" diff --git a/pythonFiles/jedi/api/keywords.py b/pythonFiles/jedi/api/keywords.py deleted file mode 100644 index 4feb39fdd44f..000000000000 --- a/pythonFiles/jedi/api/keywords.py +++ /dev/null @@ -1,117 +0,0 @@ -import pydoc -import keyword - -from jedi._compatibility import is_py3 -from jedi import common -from jedi.evaluate import compiled -from jedi.evaluate.helpers import FakeName -from jedi.parser.tree import Leaf -try: - from pydoc_data import topics as pydoc_topics -except ImportError: - # Python 2.6 - import pydoc_topics - -if is_py3: - keys = keyword.kwlist -else: - keys = keyword.kwlist + ['None', 'False', 'True'] - - -def has_inappropriate_leaf_keyword(pos, module): - relevant_errors = filter( - lambda error: error.first_pos[0] == pos[0], - module.error_statement_stacks) - - for error in relevant_errors: - if error.next_token in keys: - return True - - return False - -def completion_names(evaluator, stmt, pos, module): - keyword_list = all_keywords() - - if not isinstance(stmt, Leaf) or has_inappropriate_leaf_keyword(pos, module): - keyword_list = filter( - lambda keyword: not keyword.only_valid_as_leaf, - keyword_list - ) - return [keyword.name for keyword in keyword_list] - - -def all_keywords(pos=(0,0)): - return set([Keyword(k, pos) for k in keys]) - - -def keyword(string, pos=(0,0)): - if string in keys: - return Keyword(string, pos) - else: - return None - - -def get_operator(string, pos): - return Keyword(string, pos) - - -keywords_only_valid_as_leaf = ( - 'continue', - 'break', -) - - -class Keyword(object): - def __init__(self, name, pos): - self.name = FakeName(name, self, pos) - self.start_pos = pos - self.parent = compiled.builtin - - def get_parent_until(self): - return self.parent - - @property - def only_valid_as_leaf(self): - return self.name.value in keywords_only_valid_as_leaf - - @property - def names(self): - """ For a `parsing.Name` like comparision """ - return [self.name] - - @property - def docstr(self): - return imitate_pydoc(self.name) - - def __repr__(self): - return '<%s: %s>' % (type(self).__name__, self.name) - - -def imitate_pydoc(string): - """ - It's not possible to get the pydoc's without starting the annoying pager - stuff. - """ - # str needed because of possible unicode stuff in py2k (pydoc doesn't work - # with unicode strings) - string = str(string) - h = pydoc.help - with common.ignored(KeyError): - # try to access symbols - string = h.symbols[string] - string, _, related = string.partition(' ') - - get_target = lambda s: h.topics.get(s, h.keywords.get(s)) - while isinstance(string, str): - string = get_target(string) - - try: - # is a tuple now - label, related = string - except TypeError: - return '' - - try: - return pydoc_topics.topics[label] if pydoc_topics else '' - except KeyError: - return '' diff --git a/pythonFiles/jedi/api/replstartup.py b/pythonFiles/jedi/api/replstartup.py deleted file mode 100644 index 5bfcc8ce889e..000000000000 --- a/pythonFiles/jedi/api/replstartup.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -To use Jedi completion in Python interpreter, add the following in your shell -setup (e.g., ``.bashrc``):: - - export PYTHONSTARTUP="$(python -m jedi repl)" - -Then you will be able to use Jedi completer in your Python interpreter:: - - $ python - Python 2.7.2+ (default, Jul 20 2012, 22:15:08) - [GCC 4.6.1] on linux2 - Type "help", "copyright", "credits" or "license" for more information. - >>> import os - >>> os.path.join().split().in # doctest: +SKIP - os.path.join().split().index os.path.join().split().insert - -""" -import jedi.utils -from jedi import __version__ as __jedi_version__ - -print('REPL completion using Jedi %s' % __jedi_version__) -jedi.utils.setup_readline() - -del jedi - -# Note: try not to do many things here, as it will contaminate global -# namespace of the interpreter. diff --git a/pythonFiles/jedi/api/usages.py b/pythonFiles/jedi/api/usages.py deleted file mode 100644 index ecb885639032..000000000000 --- a/pythonFiles/jedi/api/usages.py +++ /dev/null @@ -1,49 +0,0 @@ -from jedi._compatibility import unicode -from jedi.api import classes -from jedi.parser import tree -from jedi.evaluate import imports - - -def usages(evaluator, definition_names, mods): - """ - :param definitions: list of Name - """ - def compare_array(definitions): - """ `definitions` are being compared by module/start_pos, because - sometimes the id's of the objects change (e.g. executions). - """ - result = [] - for d in definitions: - module = d.get_parent_until() - result.append((module, d.start_pos)) - return result - - search_name = unicode(list(definition_names)[0]) - compare_definitions = compare_array(definition_names) - mods |= set([d.get_parent_until() for d in definition_names]) - definitions = [] - for m in imports.get_modules_containing_name(evaluator, mods, search_name): - try: - check_names = m.used_names[search_name] - except KeyError: - continue - for name in check_names: - - result = evaluator.goto(name) - if [c for c in compare_array(result) if c in compare_definitions]: - definitions.append(classes.Definition(evaluator, name)) - # Previous definitions might be imports, so include them - # (because goto might return that import name). - compare_definitions += compare_array([name]) - return definitions - - -def usages_add_import_modules(evaluator, definitions): - """ Adds the modules of the imports """ - new = set() - for d in definitions: - imp_or_stmt = d.get_definition() - if isinstance(imp_or_stmt, tree.Import): - s = imports.ImportWrapper(evaluator, d) - new |= set(s.follow(is_goto=True)) - return set(definitions) | new diff --git a/pythonFiles/jedi/cache.py b/pythonFiles/jedi/cache.py deleted file mode 100644 index 56769d0d8953..000000000000 --- a/pythonFiles/jedi/cache.py +++ /dev/null @@ -1,336 +0,0 @@ -""" -This caching is very important for speed and memory optimizations. There's -nothing really spectacular, just some decorators. The following cache types are -available: - -- module caching (`load_parser` and `save_parser`), which uses pickle and is - really important to assure low load times of modules like ``numpy``. -- ``time_cache`` can be used to cache something for just a limited time span, - which can be useful if there's user interaction and the user cannot react - faster than a certain time. - -This module is one of the reasons why |jedi| is not thread-safe. As you can see -there are global variables, which are holding the cache information. Some of -these variables are being cleaned after every API usage. -""" -import time -import os -import sys -import json -import hashlib -import gc -import inspect -import shutil -import re -try: - import cPickle as pickle -except ImportError: - import pickle - -from jedi import settings -from jedi import common -from jedi import debug - -_time_caches = {} - -# for fast_parser, should not be deleted -parser_cache = {} - - -class ParserCacheItem(object): - def __init__(self, parser, change_time=None): - self.parser = parser - if change_time is None: - change_time = time.time() - self.change_time = change_time - - -def clear_time_caches(delete_all=False): - """ Jedi caches many things, that should be completed after each completion - finishes. - - :param delete_all: Deletes also the cache that is normally not deleted, - like parser cache, which is important for faster parsing. - """ - global _time_caches - - if delete_all: - for cache in _time_caches.values(): - cache.clear() - parser_cache.clear() - else: - # normally just kill the expired entries, not all - for tc in _time_caches.values(): - # check time_cache for expired entries - for key, (t, value) in list(tc.items()): - if t < time.time(): - # delete expired entries - del tc[key] - - -def time_cache(time_add_setting): - """ - s - This decorator works as follows: Call it with a setting and after that - use the function with a callable that returns the key. - But: This function is only called if the key is not available. After a - certain amount of time (`time_add_setting`) the cache is invalid. - """ - def _temp(key_func): - dct = {} - _time_caches[time_add_setting] = dct - - def wrapper(*args, **kwargs): - generator = key_func(*args, **kwargs) - key = next(generator) - try: - expiry, value = dct[key] - if expiry > time.time(): - return value - except KeyError: - pass - - value = next(generator) - time_add = getattr(settings, time_add_setting) - if key is not None: - dct[key] = time.time() + time_add, value - return value - return wrapper - return _temp - - -@time_cache("call_signatures_validity") -def cache_call_signatures(evaluator, call, source, user_pos): - """This function calculates the cache key.""" - index = user_pos[0] - 1 - lines = common.splitlines(source) - - before_cursor = lines[index][:user_pos[1]] - other_lines = lines[call.start_pos[0]:index] - whole = '\n'.join(other_lines + [before_cursor]) - before_bracket = re.match(r'.*\(', whole, re.DOTALL) - - module_path = call.get_parent_until().path - yield None if module_path is None else (module_path, before_bracket, call.start_pos) - yield evaluator.eval_element(call) - - -def underscore_memoization(func): - """ - Decorator for methods:: - - class A(object): - def x(self): - if self._x: - self._x = 10 - return self._x - - Becomes:: - - class A(object): - @underscore_memoization - def x(self): - return 10 - - A now has an attribute ``_x`` written by this decorator. - """ - name = '_' + func.__name__ - - def wrapper(self): - try: - return getattr(self, name) - except AttributeError: - result = func(self) - if inspect.isgenerator(result): - result = list(result) - setattr(self, name, result) - return result - - return wrapper - - -def memoize_method(method): - """A normal memoize function.""" - def wrapper(self, *args, **kwargs): - dct = self.__dict__.setdefault('_memoize_method_dct', {}) - key = (args, frozenset(kwargs.items())) - try: - return dct[key] - except KeyError: - result = method(self, *args, **kwargs) - dct[key] = result - return result - return wrapper - - -def cache_star_import(func): - @time_cache("star_import_cache_validity") - def wrapper(self): - yield self.base # The cache key - yield func(self) - return wrapper - - -def _invalidate_star_import_cache_module(module, only_main=False): - """ Important if some new modules are being reparsed """ - try: - t, modules = _time_caches['star_import_cache_validity'][module] - except KeyError: - pass - else: - del _time_caches['star_import_cache_validity'][module] - - -def invalidate_star_import_cache(path): - """On success returns True.""" - try: - parser_cache_item = parser_cache[path] - except KeyError: - pass - else: - _invalidate_star_import_cache_module(parser_cache_item.parser.module) - - -def load_parser(path): - """ - Returns the module or None, if it fails. - """ - p_time = os.path.getmtime(path) if path else None - try: - parser_cache_item = parser_cache[path] - if not path or p_time <= parser_cache_item.change_time: - return parser_cache_item.parser - else: - # In case there is already a module cached and this module - # has to be reparsed, we also need to invalidate the import - # caches. - _invalidate_star_import_cache_module(parser_cache_item.parser.module) - except KeyError: - if settings.use_filesystem_cache: - return ParserPickling.load_parser(path, p_time) - - -def save_parser(path, parser, pickling=True): - try: - p_time = None if path is None else os.path.getmtime(path) - except OSError: - p_time = None - pickling = False - - item = ParserCacheItem(parser, p_time) - parser_cache[path] = item - if settings.use_filesystem_cache and pickling: - ParserPickling.save_parser(path, item) - - -class ParserPickling(object): - - version = 24 - """ - Version number (integer) for file system cache. - - Increment this number when there are any incompatible changes in - parser representation classes. For example, the following changes - are regarded as incompatible. - - - Class name is changed. - - Class is moved to another module. - - Defined slot of the class is changed. - """ - - def __init__(self): - self.__index = None - self.py_tag = 'cpython-%s%s' % sys.version_info[:2] - """ - Short name for distinguish Python implementations and versions. - - It's like `sys.implementation.cache_tag` but for Python < 3.3 - we generate something similar. See: - http://docs.python.org/3/library/sys.html#sys.implementation - - .. todo:: Detect interpreter (e.g., PyPy). - """ - - def load_parser(self, path, original_changed_time): - try: - pickle_changed_time = self._index[path] - except KeyError: - return None - if original_changed_time is not None \ - and pickle_changed_time < original_changed_time: - # the pickle file is outdated - return None - - with open(self._get_hashed_path(path), 'rb') as f: - try: - gc.disable() - parser_cache_item = pickle.load(f) - finally: - gc.enable() - - debug.dbg('pickle loaded: %s', path) - parser_cache[path] = parser_cache_item - return parser_cache_item.parser - - def save_parser(self, path, parser_cache_item): - self.__index = None - try: - files = self._index - except KeyError: - files = {} - self._index = files - - with open(self._get_hashed_path(path), 'wb') as f: - pickle.dump(parser_cache_item, f, pickle.HIGHEST_PROTOCOL) - files[path] = parser_cache_item.change_time - - self._flush_index() - - @property - def _index(self): - if self.__index is None: - try: - with open(self._get_path('index.json')) as f: - data = json.load(f) - except (IOError, ValueError): - self.__index = {} - else: - # 0 means version is not defined (= always delete cache): - if data.get('version', 0) != self.version: - self.clear_cache() - self.__index = {} - else: - self.__index = data['index'] - return self.__index - - def _remove_old_modules(self): - # TODO use - change = False - if change: - self._flush_index(self) - self._index # reload index - - def _flush_index(self): - data = {'version': self.version, 'index': self._index} - with open(self._get_path('index.json'), 'w') as f: - json.dump(data, f) - self.__index = None - - def clear_cache(self): - shutil.rmtree(self._cache_directory()) - - def _get_hashed_path(self, path): - return self._get_path('%s.pkl' % hashlib.md5(path.encode("utf-8")).hexdigest()) - - def _get_path(self, file): - dir = self._cache_directory() - if not os.path.exists(dir): - os.makedirs(dir) - return os.path.join(dir, file) - - def _cache_directory(self): - return os.path.join(settings.cache_directory, self.py_tag) - - -# is a singleton -ParserPickling = ParserPickling() diff --git a/pythonFiles/jedi/common.py b/pythonFiles/jedi/common.py deleted file mode 100644 index 0e9afd11fb5a..000000000000 --- a/pythonFiles/jedi/common.py +++ /dev/null @@ -1,154 +0,0 @@ -""" A universal module with functions / classes without dependencies. """ -import sys -import contextlib -import functools -import re -from ast import literal_eval - -from jedi._compatibility import unicode, reraise -from jedi import settings - - -class UncaughtAttributeError(Exception): - """ - Important, because `__getattr__` and `hasattr` catch AttributeErrors - implicitly. This is really evil (mainly because of `__getattr__`). - `hasattr` in Python 2 is even more evil, because it catches ALL exceptions. - Therefore this class originally had to be derived from `BaseException` - instead of `Exception`. But because I removed relevant `hasattr` from - the code base, we can now switch back to `Exception`. - - :param base: return values of sys.exc_info(). - """ - - -def safe_property(func): - return property(reraise_uncaught(func)) - - -def reraise_uncaught(func): - """ - Re-throw uncaught `AttributeError`. - - Usage: Put ``@rethrow_uncaught`` in front of the function - which does **not** suppose to raise `AttributeError`. - - AttributeError is easily get caught by `hasattr` and another - ``except AttributeError`` clause. This becomes problem when you use - a lot of "dynamic" attributes (e.g., using ``@property``) because you - can't distinguish if the property does not exist for real or some code - inside of the "dynamic" attribute through that error. In a well - written code, such error should not exist but getting there is very - difficult. This decorator is to help us getting there by changing - `AttributeError` to `UncaughtAttributeError` to avoid unexpected catch. - This helps us noticing bugs earlier and facilitates debugging. - - .. note:: Treating StopIteration here is easy. - Add that feature when needed. - """ - @functools.wraps(func) - def wrapper(*args, **kwds): - try: - return func(*args, **kwds) - except AttributeError: - exc_info = sys.exc_info() - reraise(UncaughtAttributeError(exc_info[1]), exc_info[2]) - return wrapper - - -class PushBackIterator(object): - def __init__(self, iterator): - self.pushes = [] - self.iterator = iterator - self.current = None - - def push_back(self, value): - self.pushes.append(value) - - def __iter__(self): - return self - - def next(self): - """ Python 2 Compatibility """ - return self.__next__() - - def __next__(self): - if self.pushes: - self.current = self.pushes.pop() - else: - self.current = next(self.iterator) - return self.current - - -@contextlib.contextmanager -def scale_speed_settings(factor): - a = settings.max_executions - b = settings.max_until_execution_unique - settings.max_executions *= factor - settings.max_until_execution_unique *= factor - try: - yield - finally: - settings.max_executions = a - settings.max_until_execution_unique = b - - -def indent_block(text, indention=' '): - """This function indents a text block with a default of four spaces.""" - temp = '' - while text and text[-1] == '\n': - temp += text[-1] - text = text[:-1] - lines = text.split('\n') - return '\n'.join(map(lambda s: indention + s, lines)) + temp - - -@contextlib.contextmanager -def ignored(*exceptions): - """ - Context manager that ignores all of the specified exceptions. This will - be in the standard library starting with Python 3.4. - """ - try: - yield - except exceptions: - pass - - -def source_to_unicode(source, encoding=None): - def detect_encoding(): - """ - For the implementation of encoding definitions in Python, look at: - - http://www.python.org/dev/peps/pep-0263/ - - http://docs.python.org/2/reference/lexical_analysis.html#encoding-declarations - """ - byte_mark = literal_eval(r"b'\xef\xbb\xbf'") - if source.startswith(byte_mark): - # UTF-8 byte-order mark - return 'utf-8' - - first_two_lines = re.match(r'(?:[^\n]*\n){0,2}', str(source)).group(0) - possible_encoding = re.search(r"coding[=:]\s*([-\w.]+)", - first_two_lines) - if possible_encoding: - return possible_encoding.group(1) - else: - # the default if nothing else has been set -> PEP 263 - return encoding if encoding is not None else 'iso-8859-1' - - if isinstance(source, unicode): - # only cast str/bytes - return source - - # cast to unicode by default - return unicode(source, detect_encoding(), 'replace') - - -def splitlines(string): - """ - A splitlines for Python code. In contrast to Python's ``str.splitlines``, - looks at form feeds and other special characters as normal text. Just - splits ``\n`` and ``\r\n``. - Also different: Returns ``['']`` for an empty string input. - """ - return re.split('\n|\r\n', string) diff --git a/pythonFiles/jedi/debug.py b/pythonFiles/jedi/debug.py deleted file mode 100644 index 5c59a7b6de04..000000000000 --- a/pythonFiles/jedi/debug.py +++ /dev/null @@ -1,95 +0,0 @@ -from jedi._compatibility import encoding, is_py3, u -import inspect -import os -import time - -try: - if os.name == 'nt': - # does not work on Windows, as pyreadline and colorama interfere - raise ImportError - else: - # Use colorama for nicer console output. - from colorama import Fore, init - from colorama import initialise - # pytest resets the stream at the end - causes troubles. Since after - # every output the stream is reset automatically we don't need this. - initialise.atexit_done = True - init() -except ImportError: - class Fore(object): - RED = '' - GREEN = '' - YELLOW = '' - RESET = '' - -NOTICE = object() -WARNING = object() -SPEED = object() - -enable_speed = False -enable_warning = False -enable_notice = False - -# callback, interface: level, str -debug_function = None -ignored_modules = ['jedi.evaluate.builtin', 'jedi.parser'] -_debug_indent = -1 -_start_time = time.time() - - -def reset_time(): - global _start_time, _debug_indent - _start_time = time.time() - _debug_indent = -1 - - -def increase_indent(func): - """Decorator for makin """ - def wrapper(*args, **kwargs): - global _debug_indent - _debug_indent += 1 - try: - result = func(*args, **kwargs) - finally: - _debug_indent -= 1 - return result - return wrapper - - -def dbg(message, *args): - """ Looks at the stack, to see if a debug message should be printed. """ - if debug_function and enable_notice: - frm = inspect.stack()[1] - mod = inspect.getmodule(frm[0]) - if not (mod.__name__ in ignored_modules): - i = ' ' * _debug_indent - debug_function(NOTICE, i + 'dbg: ' + message % tuple(u(repr(a)) for a in args)) - - -def warning(message, *args): - if debug_function and enable_warning: - i = ' ' * _debug_indent - debug_function(WARNING, i + 'warning: ' + message % tuple(u(repr(a)) for a in args)) - - -def speed(name): - if debug_function and enable_speed: - now = time.time() - i = ' ' * _debug_indent - debug_function(SPEED, i + 'speed: ' + '%s %s' % (name, now - _start_time)) - - -def print_to_stdout(level, str_out): - """ The default debug function """ - if level == NOTICE: - col = Fore.GREEN - elif level == WARNING: - col = Fore.RED - else: - col = Fore.YELLOW - if not is_py3: - str_out = str_out.encode(encoding, 'replace') - print(col + str_out + Fore.RESET) - - -# debug_function = print_to_stdout diff --git a/pythonFiles/jedi/evaluate/__init__.py b/pythonFiles/jedi/evaluate/__init__.py deleted file mode 100644 index c8dca1403e8e..000000000000 --- a/pythonFiles/jedi/evaluate/__init__.py +++ /dev/null @@ -1,379 +0,0 @@ -""" -Evaluation of Python code in |jedi| is based on three assumptions: - -* The code uses as least side effects as possible. Jedi understands certain - list/tuple/set modifications, but there's no guarantee that Jedi detects - everything (list.append in different modules for example). -* No magic is being used: - - - metaclasses - - ``setattr()`` / ``__import__()`` - - writing to ``globals()``, ``locals()``, ``object.__dict__`` -* The programmer is not a total dick, e.g. like `this - `_ :-) - -The actual algorithm is based on a principle called lazy evaluation. If you -don't know about it, google it. That said, the typical entry point for static -analysis is calling ``eval_statement``. There's separate logic for -autocompletion in the API, the evaluator is all about evaluating an expression. - -Now you need to understand what follows after ``eval_statement``. Let's -make an example:: - - import datetime - datetime.date.toda# <-- cursor here - -First of all, this module doesn't care about completion. It really just cares -about ``datetime.date``. At the end of the procedure ``eval_statement`` will -return the ``date`` class. - -To *visualize* this (simplified): - -- ``Evaluator.eval_statement`` doesn't do much, because there's no assignment. -- ``Evaluator.eval_element`` cares for resolving the dotted path -- ``Evaluator.find_types`` searches for global definitions of datetime, which - it finds in the definition of an import, by scanning the syntax tree. -- Using the import logic, the datetime module is found. -- Now ``find_types`` is called again by ``eval_element`` to find ``date`` - inside the datetime module. - -Now what would happen if we wanted ``datetime.date.foo.bar``? Two more -calls to ``find_types``. However the second call would be ignored, because the -first one would return nothing (there's no foo attribute in ``date``). - -What if the import would contain another ``ExprStmt`` like this:: - - from foo import bar - Date = bar.baz - -Well... You get it. Just another ``eval_statement`` recursion. It's really -easy. Python can obviously get way more complicated then this. To understand -tuple assignments, list comprehensions and everything else, a lot more code had -to be written. - -Jedi has been tested very well, so you can just start modifying code. It's best -to write your own test first for your "new" feature. Don't be scared of -breaking stuff. As long as the tests pass, you're most likely to be fine. - -I need to mention now that lazy evaluation is really good because it -only *evaluates* what needs to be *evaluated*. All the statements and modules -that are not used are just being ignored. -""" - -import copy -from itertools import chain - -from jedi.parser import tree -from jedi import debug -from jedi.evaluate import representation as er -from jedi.evaluate import imports -from jedi.evaluate import recursion -from jedi.evaluate import iterable -from jedi.evaluate.cache import memoize_default -from jedi.evaluate import stdlib -from jedi.evaluate import finder -from jedi.evaluate import compiled -from jedi.evaluate import precedence -from jedi.evaluate import param -from jedi.evaluate import helpers - - -class Evaluator(object): - def __init__(self, grammar): - self.grammar = grammar - self.memoize_cache = {} # for memoize decorators - # To memorize modules -> equals `sys.modules`. - self.modules = {} # like `sys.modules`. - self.compiled_cache = {} # see `compiled.create()` - self.recursion_detector = recursion.RecursionDetector() - self.execution_recursion_detector = recursion.ExecutionRecursionDetector() - self.analysis = [] - - def wrap(self, element): - if isinstance(element, tree.Class): - return er.Class(self, element) - elif isinstance(element, tree.Function): - if isinstance(element, tree.Lambda): - return er.LambdaWrapper(self, element) - else: - return er.Function(self, element) - elif isinstance(element, (tree.Module)) \ - and not isinstance(element, er.ModuleWrapper): - return er.ModuleWrapper(self, element) - else: - return element - - def find_types(self, scope, name_str, position=None, search_global=False, - is_goto=False): - """ - This is the search function. The most important part to debug. - `remove_statements` and `filter_statements` really are the core part of - this completion. - - :param position: Position of the last statement -> tuple of line, column - :return: List of Names. Their parents are the types. - """ - f = finder.NameFinder(self, scope, name_str, position) - scopes = f.scopes(search_global) - if is_goto: - return f.filter_name(scopes) - return f.find(scopes, search_global) - - @memoize_default(default=[], evaluator_is_first_arg=True) - @recursion.recursion_decorator - @debug.increase_indent - def eval_statement(self, stmt, seek_name=None): - """ - The starting point of the completion. A statement always owns a call - list, which are the calls, that a statement does. In case multiple - names are defined in the statement, `seek_name` returns the result for - this name. - - :param stmt: A `tree.ExprStmt`. - """ - debug.dbg('eval_statement %s (%s)', stmt, seek_name) - types = self.eval_element(stmt.get_rhs()) - - if seek_name: - types = finder.check_tuple_assignments(types, seek_name) - - first_operation = stmt.first_operation() - if first_operation not in ('=', None) and not isinstance(stmt, er.InstanceElement): # TODO don't check for this. - # `=` is always the last character in aug assignments -> -1 - operator = copy.copy(first_operation) - operator.value = operator.value[:-1] - name = str(stmt.get_defined_names()[0]) - parent = self.wrap(stmt.get_parent_scope()) - left = self.find_types(parent, name, stmt.start_pos, search_global=True) - if isinstance(stmt.get_parent_until(tree.ForStmt), tree.ForStmt): - # Iterate through result and add the values, that's possible - # only in for loops without clutter, because they are - # predictable. - for r in types: - left = precedence.calculate(self, left, operator, [r]) - types = left - else: - types = precedence.calculate(self, left, operator, types) - debug.dbg('eval_statement result %s', types) - return types - - @memoize_default(evaluator_is_first_arg=True) - def eval_element(self, element): - if isinstance(element, iterable.AlreadyEvaluated): - return list(element) - elif isinstance(element, iterable.MergedNodes): - return iterable.unite(self.eval_element(e) for e in element) - - debug.dbg('eval_element %s@%s', element, element.start_pos) - if isinstance(element, (tree.Name, tree.Literal)) or tree.is_node(element, 'atom'): - return self._eval_atom(element) - elif isinstance(element, tree.Keyword): - # For False/True/None - if element.value in ('False', 'True', 'None'): - return [compiled.builtin.get_by_name(element.value)] - else: - return [] - elif element.isinstance(tree.Lambda): - return [er.LambdaWrapper(self, element)] - elif element.isinstance(er.LambdaWrapper): - return [element] # TODO this is no real evaluation. - elif element.type == 'expr_stmt': - return self.eval_statement(element) - elif element.type == 'power': - types = self._eval_atom(element.children[0]) - for trailer in element.children[1:]: - if trailer == '**': # has a power operation. - raise NotImplementedError - types = self.eval_trailer(types, trailer) - - return types - elif element.type in ('testlist_star_expr', 'testlist',): - # The implicit tuple in statements. - return [iterable.ImplicitTuple(self, element)] - elif element.type in ('not_test', 'factor'): - types = self.eval_element(element.children[-1]) - for operator in element.children[:-1]: - types = list(precedence.factor_calculate(self, types, operator)) - return types - elif element.type == 'test': - # `x if foo else y` case. - return (self.eval_element(element.children[0]) + - self.eval_element(element.children[-1])) - elif element.type == 'operator': - # Must be an ellipsis, other operators are not evaluated. - return [] # Ignore for now. - elif element.type == 'dotted_name': - types = self._eval_atom(element.children[0]) - for next_name in element.children[2::2]: - types = list(chain.from_iterable(self.find_types(typ, next_name) - for typ in types)) - return types - else: - return precedence.calculate_children(self, element.children) - - def _eval_atom(self, atom): - """ - Basically to process ``atom`` nodes. The parser sometimes doesn't - generate the node (because it has just one child). In that case an atom - might be a name or a literal as well. - """ - if isinstance(atom, tree.Name): - # This is the first global lookup. - stmt = atom.get_definition() - scope = stmt.get_parent_until(tree.IsScope, include_current=True) - if isinstance(stmt, tree.CompFor): - stmt = stmt.get_parent_until((tree.ClassOrFunc, tree.ExprStmt)) - if stmt.type != 'expr_stmt': - # We only need to adjust the start_pos for statements, because - # there the name cannot be used. - stmt = atom - return self.find_types(scope, atom, stmt.start_pos, search_global=True) - elif isinstance(atom, tree.Literal): - return [compiled.create(self, atom.eval())] - else: - c = atom.children - # Parentheses without commas are not tuples. - if c[0] == '(' and not len(c) == 2 \ - and not(tree.is_node(c[1], 'testlist_comp') - and len(c[1].children) > 1): - return self.eval_element(c[1]) - try: - comp_for = c[1].children[1] - except (IndexError, AttributeError): - pass - else: - if isinstance(comp_for, tree.CompFor) and c[0] != '{': - return [iterable.Comprehension.from_atom(self, atom)] - return [iterable.Array(self, atom)] - - def eval_trailer(self, types, trailer): - trailer_op, node = trailer.children[:2] - if node == ')': # `arglist` is optional. - node = () - new_types = [] - for typ in types: - debug.dbg('eval_trailer: %s in scope %s', trailer, typ) - if trailer_op == '.': - new_types += self.find_types(typ, node) - elif trailer_op == '(': - new_types += self.execute(typ, node, trailer) - elif trailer_op == '[': - try: - get = typ.get_index_types - except AttributeError: - debug.warning("TypeError: '%s' object is not subscriptable" - % typ) - else: - new_types += get(self, node) - return new_types - - def execute_evaluated(self, obj, *args): - """ - Execute a function with already executed arguments. - """ - args = [iterable.AlreadyEvaluated([arg]) for arg in args] - return self.execute(obj, args) - - @debug.increase_indent - def execute(self, obj, arguments=(), trailer=None): - if not isinstance(arguments, param.Arguments): - arguments = param.Arguments(self, arguments, trailer) - - if obj.isinstance(er.Function): - obj = obj.get_decorated_func() - - debug.dbg('execute: %s %s', obj, arguments) - try: - # Some stdlib functions like super(), namedtuple(), etc. have been - # hard-coded in Jedi to support them. - return stdlib.execute(self, obj, arguments) - except stdlib.NotInStdLib: - pass - - try: - func = obj.py__call__ - except AttributeError: - debug.warning("no execution possible %s", obj) - return [] - else: - types = func(self, arguments) - debug.dbg('execute result: %s in %s', types, obj) - return types - - def goto_definition(self, name): - def_ = name.get_definition() - if def_.type == 'expr_stmt' and name in def_.get_defined_names(): - return self.eval_statement(def_, name) - call = helpers.call_of_name(name) - return self.eval_element(call) - - def goto(self, name): - def resolve_implicit_imports(names): - for name in names: - if isinstance(name.parent, helpers.FakeImport): - # Those are implicit imports. - s = imports.ImportWrapper(self, name) - for n in s.follow(is_goto=True): - yield n - else: - yield name - - stmt = name.get_definition() - par = name.parent - if par.type == 'argument' and par.children[1] == '=' and par.children[0] == name: - # Named param goto. - trailer = par.parent - if trailer.type == 'arglist': - trailer = trailer.parent - if trailer.type != 'classdef': - if trailer.type == 'decorator': - types = self.eval_element(trailer.children[1]) - else: - i = trailer.parent.children.index(trailer) - to_evaluate = trailer.parent.children[:i] - types = self.eval_element(to_evaluate[0]) - for trailer in to_evaluate[1:]: - types = self.eval_trailer(types, trailer) - param_names = [] - for typ in types: - try: - params = typ.params - except AttributeError: - pass - else: - param_names += [param.name for param in params - if param.name.value == name.value] - return param_names - elif isinstance(par, tree.ExprStmt) and name in par.get_defined_names(): - # Only take the parent, because if it's more complicated than just - # a name it's something you can "goto" again. - return [name] - elif isinstance(par, (tree.Param, tree.Function, tree.Class)) and par.name is name: - return [name] - elif isinstance(stmt, tree.Import): - modules = imports.ImportWrapper(self, name).follow(is_goto=True) - return list(resolve_implicit_imports(modules)) - elif par.type == 'dotted_name': # Is a decorator. - index = par.children.index(name) - if index > 0: - new_dotted = helpers.deep_ast_copy(par) - new_dotted.children[index - 1:] = [] - types = self.eval_element(new_dotted) - return resolve_implicit_imports(iterable.unite( - self.find_types(typ, name, is_goto=True) for typ in types - )) - - scope = name.get_parent_scope() - if tree.is_node(name.parent, 'trailer'): - call = helpers.call_of_name(name, cut_own_trailer=True) - types = self.eval_element(call) - return resolve_implicit_imports(iterable.unite( - self.find_types(typ, name, is_goto=True) for typ in types - )) - else: - if stmt.type != 'expr_stmt': - # We only need to adjust the start_pos for statements, because - # there the name cannot be used. - stmt = name - return self.find_types(scope, name, stmt.start_pos, - search_global=True, is_goto=True) diff --git a/pythonFiles/jedi/evaluate/analysis.py b/pythonFiles/jedi/evaluate/analysis.py deleted file mode 100644 index d4a411f42b68..000000000000 --- a/pythonFiles/jedi/evaluate/analysis.py +++ /dev/null @@ -1,302 +0,0 @@ -""" -Module for statical analysis. -""" -from jedi import debug -from jedi.parser import tree -from jedi.evaluate.compiled import CompiledObject - - -CODES = { - 'attribute-error': (1, AttributeError, 'Potential AttributeError.'), - 'name-error': (2, NameError, 'Potential NameError.'), - 'import-error': (3, ImportError, 'Potential ImportError.'), - 'type-error-generator': (4, TypeError, "TypeError: 'generator' object is not subscriptable."), - 'type-error-too-many-arguments': (5, TypeError, None), - 'type-error-too-few-arguments': (6, TypeError, None), - 'type-error-keyword-argument': (7, TypeError, None), - 'type-error-multiple-values': (8, TypeError, None), - 'type-error-star-star': (9, TypeError, None), - 'type-error-star': (10, TypeError, None), - 'type-error-operation': (11, TypeError, None), -} - - -class Error(object): - def __init__(self, name, module_path, start_pos, message=None): - self.path = module_path - self._start_pos = start_pos - self.name = name - if message is None: - message = CODES[self.name][2] - self.message = message - - @property - def line(self): - return self._start_pos[0] - - @property - def column(self): - return self._start_pos[1] - - @property - def code(self): - # The class name start - first = self.__class__.__name__[0] - return first + str(CODES[self.name][0]) - - def __unicode__(self): - return '%s:%s:%s: %s %s' % (self.path, self.line, self.column, - self.code, self.message) - - def __str__(self): - return self.__unicode__() - - def __eq__(self, other): - return (self.path == other.path and self.name == other.name - and self._start_pos == other._start_pos) - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash((self.path, self._start_pos, self.name)) - - def __repr__(self): - return '<%s %s: %s@%s,%s>' % (self.__class__.__name__, - self.name, self.path, - self._start_pos[0], self._start_pos[1]) - - -class Warning(Error): - pass - - -def add(evaluator, name, jedi_obj, message=None, typ=Error, payload=None): - from jedi.evaluate.iterable import MergedNodes - while isinstance(jedi_obj, MergedNodes): - if len(jedi_obj) != 1: - # TODO is this kosher? - return - jedi_obj = list(jedi_obj)[0] - - exception = CODES[name][1] - if _check_for_exception_catch(evaluator, jedi_obj, exception, payload): - return - - module_path = jedi_obj.get_parent_until().path - instance = typ(name, module_path, jedi_obj.start_pos, message) - debug.warning(str(instance)) - evaluator.analysis.append(instance) - - -def _check_for_setattr(instance): - """ - Check if there's any setattr method inside an instance. If so, return True. - """ - module = instance.get_parent_until() - try: - stmts = module.used_names['setattr'] - except KeyError: - return False - - return any(instance.start_pos < stmt.start_pos < instance.end_pos - for stmt in stmts) - - -def add_attribute_error(evaluator, scope, name): - message = ('AttributeError: %s has no attribute %s.' % (scope, name)) - from jedi.evaluate.representation import Instance - # Check for __getattr__/__getattribute__ existance and issue a warning - # instead of an error, if that happens. - if isinstance(scope, Instance): - typ = Warning - try: - scope.get_subscope_by_name('__getattr__') - except KeyError: - try: - scope.get_subscope_by_name('__getattribute__') - except KeyError: - if not _check_for_setattr(scope): - typ = Error - else: - typ = Error - - payload = scope, name - add(evaluator, 'attribute-error', name, message, typ, payload) - - -def _check_for_exception_catch(evaluator, jedi_obj, exception, payload=None): - """ - Checks if a jedi object (e.g. `Statement`) sits inside a try/catch and - doesn't count as an error (if equal to `exception`). - Also checks `hasattr` for AttributeErrors and uses the `payload` to compare - it. - Returns True if the exception was catched. - """ - def check_match(cls, exception): - try: - return isinstance(cls, CompiledObject) and issubclass(exception, cls.obj) - except TypeError: - return False - - def check_try_for_except(obj, exception): - # Only nodes in try - iterator = iter(obj.children) - for branch_type in iterator: - colon = next(iterator) - suite = next(iterator) - if branch_type == 'try' \ - and not (branch_type.start_pos < jedi_obj.start_pos <= suite.end_pos): - return False - - for node in obj.except_clauses(): - if node is None: - return True # An exception block that catches everything. - else: - except_classes = evaluator.eval_element(node) - for cls in except_classes: - from jedi.evaluate import iterable - if isinstance(cls, iterable.Array) and cls.type == 'tuple': - # multiple exceptions - for c in cls.values(): - if check_match(c, exception): - return True - else: - if check_match(cls, exception): - return True - - def check_hasattr(node, suite): - try: - assert suite.start_pos <= jedi_obj.start_pos < suite.end_pos - assert node.type == 'power' - base = node.children[0] - assert base.type == 'name' and base.value == 'hasattr' - trailer = node.children[1] - assert trailer.type == 'trailer' - arglist = trailer.children[1] - assert arglist.type == 'arglist' - from jedi.evaluate.param import Arguments - args = list(Arguments(evaluator, arglist).unpack()) - # Arguments should be very simple - assert len(args) == 2 - - # Check name - key, values = args[1] - assert len(values) == 1 - names = evaluator.eval_element(values[0]) - assert len(names) == 1 and isinstance(names[0], CompiledObject) - assert names[0].obj == str(payload[1]) - - # Check objects - key, values = args[0] - assert len(values) == 1 - objects = evaluator.eval_element(values[0]) - return payload[0] in objects - except AssertionError: - return False - - obj = jedi_obj - while obj is not None and not obj.isinstance(tree.Function, tree.Class): - if obj.isinstance(tree.Flow): - # try/except catch check - if obj.isinstance(tree.TryStmt) and check_try_for_except(obj, exception): - return True - # hasattr check - if exception == AttributeError and obj.isinstance(tree.IfStmt, tree.WhileStmt): - if check_hasattr(obj.children[1], obj.children[3]): - return True - obj = obj.parent - - return False - - -def get_module_statements(module): - """ - Returns the statements used in a module. All these statements should be - evaluated to check for potential exceptions. - """ - def check_children(node): - try: - children = node.children - except AttributeError: - return [] - else: - nodes = [] - for child in children: - nodes += check_children(child) - if child.type == 'trailer': - c = child.children - if c[0] == '(' and c[1] != ')': - if c[1].type != 'arglist': - if c[1].type == 'argument': - nodes.append(c[1].children[-1]) - else: - nodes.append(c[1]) - else: - for argument in c[1].children: - if argument.type == 'argument': - nodes.append(argument.children[-1]) - elif argument.type != 'operator': - nodes.append(argument) - return nodes - - def add_nodes(nodes): - new = set() - for node in nodes: - if isinstance(node, tree.Flow): - children = node.children - if node.type == 'for_stmt': - children = children[2:] # Don't want to include the names. - # Pick the suite/simple_stmt. - new |= add_nodes(children) - elif node.type in ('simple_stmt', 'suite'): - new |= add_nodes(node.children) - elif node.type in ('return_stmt', 'yield_expr'): - try: - new.add(node.children[1]) - except IndexError: - pass - elif node.type not in ('whitespace', 'operator', 'keyword', - 'parameters', 'decorated', 'except_clause') \ - and not isinstance(node, (tree.ClassOrFunc, tree.Import)): - new.add(node) - - try: - children = node.children - except AttributeError: - pass - else: - for next_node in children: - new.update(check_children(node)) - if next_node.type != 'keyword' and node.type != 'expr_stmt': - new.add(node) - return new - - nodes = set() - import_names = set() - decorated_funcs = [] - for scope in module.walk(): - for imp in set(scope.imports): - import_names |= set(imp.get_defined_names()) - if imp.is_nested(): - import_names |= set(path[-1] for path in imp.paths()) - - children = scope.children - if isinstance(scope, tree.ClassOrFunc): - children = children[2:] # We don't want to include the class name. - nodes |= add_nodes(children) - - for flow in scope.flows: - if flow.type == 'for_stmt': - nodes.add(flow.children[3]) - elif flow.type == 'try_stmt': - nodes.update(e for e in flow.except_clauses() if e is not None) - - try: - decorators = scope.get_decorators() - except AttributeError: - pass - else: - if decorators: - decorated_funcs.append(scope) - return nodes, import_names, decorated_funcs diff --git a/pythonFiles/jedi/evaluate/cache.py b/pythonFiles/jedi/evaluate/cache.py deleted file mode 100644 index b44f812accd2..000000000000 --- a/pythonFiles/jedi/evaluate/cache.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -- the popular ``memoize_default`` works like a typical memoize and returns the - default otherwise. -- ``CachedMetaClass`` uses ``memoize_default`` to do the same with classes. -""" - -import inspect - -NO_DEFAULT = object() - - -def memoize_default(default=NO_DEFAULT, evaluator_is_first_arg=False, second_arg_is_evaluator=False): - """ This is a typical memoization decorator, BUT there is one difference: - To prevent recursion it sets defaults. - - Preventing recursion is in this case the much bigger use than speed. I - don't think, that there is a big speed difference, but there are many cases - where recursion could happen (think about a = b; b = a). - """ - def func(function): - def wrapper(obj, *args, **kwargs): - if evaluator_is_first_arg: - cache = obj.memoize_cache - elif second_arg_is_evaluator: # needed for meta classes - cache = args[0].memoize_cache - else: - cache = obj._evaluator.memoize_cache - - try: - memo = cache[function] - except KeyError: - memo = {} - cache[function] = memo - - key = (obj, args, frozenset(kwargs.items())) - if key in memo: - return memo[key] - else: - if default is not NO_DEFAULT: - memo[key] = default - rv = function(obj, *args, **kwargs) - if inspect.isgenerator(rv): - rv = list(rv) - memo[key] = rv - return rv - return wrapper - return func - - -class CachedMetaClass(type): - """ - This is basically almost the same than the decorator above, it just caches - class initializations. Either you do it this way or with decorators, but - with decorators you lose class access (isinstance, etc). - """ - @memoize_default(None, second_arg_is_evaluator=True) - def __call__(self, *args, **kwargs): - return super(CachedMetaClass, self).__call__(*args, **kwargs) diff --git a/pythonFiles/jedi/evaluate/compiled/__init__.py b/pythonFiles/jedi/evaluate/compiled/__init__.py deleted file mode 100644 index 67d3f37119d3..000000000000 --- a/pythonFiles/jedi/evaluate/compiled/__init__.py +++ /dev/null @@ -1,531 +0,0 @@ -""" -Imitate the parser representation. -""" -import inspect -import re -import sys -import os -from functools import partial - -from jedi._compatibility import builtins as _builtins, unicode -from jedi import debug -from jedi.cache import underscore_memoization, memoize_method -from jedi.evaluate.sys_path import get_sys_path -from jedi.parser.tree import Param, Base, Operator, zero_position_modifier -from jedi.evaluate.helpers import FakeName -from . import fake - - -_sep = os.path.sep -if os.path.altsep is not None: - _sep += os.path.altsep -_path_re = re.compile('(?:\.[^{0}]+|[{0}]__init__\.py)$'.format(re.escape(_sep))) -del _sep - - -class CheckAttribute(object): - """Raises an AttributeError if the attribute X isn't available.""" - def __init__(self, func): - self.func = func - # Remove the py in front of e.g. py__call__. - self.check_name = func.__name__[2:] - - def __get__(self, instance, owner): - # This might raise an AttributeError. That's wanted. - getattr(instance.obj, self.check_name) - return partial(self.func, instance) - - -class CompiledObject(Base): - # comply with the parser - start_pos = 0, 0 - path = None # modules have this attribute - set it to None. - used_names = {} # To be consistent with modules. - - def __init__(self, obj, parent=None): - self.obj = obj - self.parent = parent - - @property - def py__call__(self): - def actual(evaluator, params): - if inspect.isclass(self.obj): - from jedi.evaluate.representation import Instance - return [Instance(evaluator, self, params)] - else: - return list(self._execute_function(evaluator, params)) - - # Might raise an AttributeError, which is intentional. - self.obj.__call__ - return actual - - @CheckAttribute - def py__class__(self, evaluator): - return CompiledObject(self.obj.__class__, parent=self.parent) - - @CheckAttribute - def py__mro__(self, evaluator): - return tuple(create(evaluator, cls, self.parent) for cls in self.obj.__mro__) - - @CheckAttribute - def py__bases__(self, evaluator): - return tuple(create(evaluator, cls) for cls in self.obj.__bases__) - - def py__bool__(self): - return bool(self.obj) - - def py__file__(self): - return self.obj.__file__ - - def is_class(self): - return inspect.isclass(self.obj) - - @property - def doc(self): - return inspect.getdoc(self.obj) or '' - - @property - def params(self): - params_str, ret = self._parse_function_doc() - tokens = params_str.split(',') - if inspect.ismethoddescriptor(self._cls().obj): - tokens.insert(0, 'self') - params = [] - for p in tokens: - parts = [FakeName(part) for part in p.strip().split('=')] - if len(parts) > 1: - parts.insert(1, Operator(zero_position_modifier, '=', (0, 0))) - params.append(Param(parts, self)) - return params - - def __repr__(self): - return '<%s: %s>' % (type(self).__name__, repr(self.obj)) - - @underscore_memoization - def _parse_function_doc(self): - if self.doc is None: - return '', '' - - return _parse_function_doc(self.doc) - - def api_type(self): - if fake.is_class_instance(self.obj): - return 'instance' - - cls = self._cls().obj - if inspect.isclass(cls): - return 'class' - elif inspect.ismodule(cls): - return 'module' - elif inspect.isbuiltin(cls) or inspect.ismethod(cls) \ - or inspect.ismethoddescriptor(cls): - return 'function' - - @property - def type(self): - """Imitate the tree.Node.type values.""" - cls = self._cls().obj - if inspect.isclass(cls): - return 'classdef' - elif inspect.ismodule(cls): - return 'file_input' - elif inspect.isbuiltin(cls) or inspect.ismethod(cls) \ - or inspect.ismethoddescriptor(cls): - return 'funcdef' - - @underscore_memoization - def _cls(self): - # Ensures that a CompiledObject is returned that is not an instance (like list) - if fake.is_class_instance(self.obj): - try: - c = self.obj.__class__ - except AttributeError: - # happens with numpy.core.umath._UFUNC_API (you get it - # automatically by doing `import numpy`. - c = type(None) - return CompiledObject(c, self.parent) - return self - - @property - def names_dict(self): - # For compatibility with `representation.Class`. - return self.names_dicts(False)[0] - - def names_dicts(self, search_global, is_instance=False): - return self._names_dict_ensure_one_dict(is_instance) - - @memoize_method - def _names_dict_ensure_one_dict(self, is_instance): - """ - search_global shouldn't change the fact that there's one dict, this way - there's only one `object`. - """ - return [LazyNamesDict(self._cls(), is_instance)] - - def get_subscope_by_name(self, name): - if name in dir(self._cls().obj): - return CompiledName(self._cls(), name).parent - else: - raise KeyError("CompiledObject doesn't have an attribute '%s'." % name) - - def get_index_types(self, evaluator, index_array=()): - # If the object doesn't have `__getitem__`, just raise the - # AttributeError. - if not hasattr(self.obj, '__getitem__'): - debug.warning('Tried to call __getitem__ on non-iterable.') - return [] - if type(self.obj) not in (str, list, tuple, unicode, bytes, bytearray, dict): - # Get rid of side effects, we won't call custom `__getitem__`s. - return [] - - result = [] - from jedi.evaluate.iterable import create_indexes_or_slices - for typ in create_indexes_or_slices(evaluator, index_array): - index = None - try: - index = typ.obj - new = self.obj[index] - except (KeyError, IndexError, TypeError, AttributeError): - # Just try, we don't care if it fails, except for slices. - if isinstance(index, slice): - result.append(self) - else: - result.append(CompiledObject(new)) - if not result: - try: - for obj in self.obj: - result.append(CompiledObject(obj)) - except TypeError: - pass # self.obj maynot have an __iter__ method. - return result - - @property - def name(self): - # might not exist sometimes (raises AttributeError) - return FakeName(self._cls().obj.__name__, self) - - def _execute_function(self, evaluator, params): - if self.type != 'funcdef': - return - - for name in self._parse_function_doc()[1].split(): - try: - bltn_obj = _create_from_name(builtin, builtin, name) - except AttributeError: - continue - else: - if isinstance(bltn_obj, CompiledObject) and bltn_obj.obj is None: - # We want everything except None. - continue - for result in evaluator.execute(bltn_obj, params): - yield result - - @property - @underscore_memoization - def subscopes(self): - """ - Returns only the faked scopes - the other ones are not important for - internal analysis. - """ - module = self.get_parent_until() - faked_subscopes = [] - for name in dir(self._cls().obj): - f = fake.get_faked(module.obj, self.obj, name) - if f: - f.parent = self - faked_subscopes.append(f) - return faked_subscopes - - def is_scope(self): - return True - - def get_self_attributes(self): - return [] # Instance compatibility - - def get_imports(self): - return [] # Builtins don't have imports - - -class LazyNamesDict(object): - """ - A names_dict instance for compiled objects, resembles the parser.tree. - """ - def __init__(self, compiled_obj, is_instance): - self._compiled_obj = compiled_obj - self._is_instance = is_instance - - def __iter__(self): - return (v[0].value for v in self.values()) - - @memoize_method - def __getitem__(self, name): - try: - getattr(self._compiled_obj.obj, name) - except AttributeError: - raise KeyError('%s in %s not found.' % (name, self._compiled_obj)) - return [CompiledName(self._compiled_obj, name)] - - def values(self): - obj = self._compiled_obj.obj - - values = [] - for name in dir(obj): - try: - values.append(self[name]) - except KeyError: - # The dir function can be wrong. - pass - - # dir doesn't include the type names. - if not inspect.ismodule(obj) and obj != type and not self._is_instance: - values += _type_names_dict.values() - return values - - -class CompiledName(FakeName): - def __init__(self, obj, name): - super(CompiledName, self).__init__(name) - self._obj = obj - self.name = name - - def __repr__(self): - try: - name = self._obj.name # __name__ is not defined all the time - except AttributeError: - name = None - return '<%s: (%s).%s>' % (type(self).__name__, name, self.name) - - def is_definition(self): - return True - - @property - @underscore_memoization - def parent(self): - module = self._obj.get_parent_until() - return _create_from_name(module, self._obj, self.name) - - @parent.setter - def parent(self, value): - pass # Just ignore this, FakeName tries to overwrite the parent attribute. - - -def dotted_from_fs_path(fs_path, sys_path=None): - """ - Changes `/usr/lib/python3.4/email/utils.py` to `email.utils`. I.e. - compares the path with sys.path and then returns the dotted_path. If the - path is not in the sys.path, just returns None. - """ - if sys_path is None: - sys_path = get_sys_path() - - if os.path.basename(fs_path).startswith('__init__.'): - # We are calculating the path. __init__ files are not interesting. - fs_path = os.path.dirname(fs_path) - - # prefer - # - UNIX - # /path/to/pythonX.Y/lib-dynload - # /path/to/pythonX.Y/site-packages - # - Windows - # C:\path\to\DLLs - # C:\path\to\Lib\site-packages - # over - # - UNIX - # /path/to/pythonX.Y - # - Windows - # C:\path\to\Lib - path = '' - for s in sys_path: - if (fs_path.startswith(s) and len(path) < len(s)): - path = s - return _path_re.sub('', fs_path[len(path):].lstrip(os.path.sep)).replace(os.path.sep, '.') - - -def load_module(path=None, name=None): - if path is not None: - dotted_path = dotted_from_fs_path(path) - else: - dotted_path = name - - sys_path = get_sys_path() - if dotted_path is None: - p, _, dotted_path = path.partition(os.path.sep) - sys_path.insert(0, p) - - temp, sys.path = sys.path, sys_path - try: - __import__(dotted_path) - except RuntimeError: - if 'PySide' in dotted_path or 'PyQt' in dotted_path: - # RuntimeError: the PyQt4.QtCore and PyQt5.QtCore modules both wrap - # the QObject class. - # See https://github.com/davidhalter/jedi/pull/483 - return None - raise - except ImportError: - # If a module is "corrupt" or not really a Python module or whatever. - debug.warning('Module %s not importable.', path) - return None - finally: - sys.path = temp - - # Just access the cache after import, because of #59 as well as the very - # complicated import structure of Python. - module = sys.modules[dotted_path] - - return CompiledObject(module) - - -docstr_defaults = { - 'floating point number': 'float', - 'character': 'str', - 'integer': 'int', - 'dictionary': 'dict', - 'string': 'str', -} - - -def _parse_function_doc(doc): - """ - Takes a function and returns the params and return value as a tuple. - This is nothing more than a docstring parser. - - TODO docstrings like utime(path, (atime, mtime)) and a(b [, b]) -> None - TODO docstrings like 'tuple of integers' - """ - # parse round parentheses: def func(a, (b,c)) - try: - count = 0 - start = doc.index('(') - for i, s in enumerate(doc[start:]): - if s == '(': - count += 1 - elif s == ')': - count -= 1 - if count == 0: - end = start + i - break - param_str = doc[start + 1:end] - except (ValueError, UnboundLocalError): - # ValueError for doc.index - # UnboundLocalError for undefined end in last line - debug.dbg('no brackets found - no param') - end = 0 - param_str = '' - else: - # remove square brackets, that show an optional param ( = None) - def change_options(m): - args = m.group(1).split(',') - for i, a in enumerate(args): - if a and '=' not in a: - args[i] += '=None' - return ','.join(args) - - while True: - param_str, changes = re.subn(r' ?\[([^\[\]]+)\]', - change_options, param_str) - if changes == 0: - break - param_str = param_str.replace('-', '_') # see: isinstance.__doc__ - - # parse return value - r = re.search('-[>-]* ', doc[end:end + 7]) - if r is None: - ret = '' - else: - index = end + r.end() - # get result type, which can contain newlines - pattern = re.compile(r'(,\n|[^\n-])+') - ret_str = pattern.match(doc, index).group(0).strip() - # New object -> object() - ret_str = re.sub(r'[nN]ew (.*)', r'\1()', ret_str) - - ret = docstr_defaults.get(ret_str, ret_str) - - return param_str, ret - - -class Builtin(CompiledObject): - @memoize_method - def get_by_name(self, name): - return self.names_dict[name][0].parent - - -def _a_generator(foo): - """Used to have an object to return for generators.""" - yield 42 - yield foo - - -def _create_from_name(module, parent, name): - faked = fake.get_faked(module.obj, parent.obj, name) - # only functions are necessary. - if faked is not None: - faked.parent = parent - return faked - - try: - obj = getattr(parent.obj, name) - except AttributeError: - # happens e.g. in properties of - # PyQt4.QtGui.QStyleOptionComboBox.currentText - # -> just set it to None - obj = None - return CompiledObject(obj, parent) - - -builtin = Builtin(_builtins) -magic_function_class = CompiledObject(type(load_module), parent=builtin) -generator_obj = CompiledObject(_a_generator(1.0)) -_type_names_dict = builtin.get_by_name('type').names_dict -none_obj = builtin.get_by_name('None') -false_obj = builtin.get_by_name('False') -true_obj = builtin.get_by_name('True') -object_obj = builtin.get_by_name('object') - - -def keyword_from_value(obj): - if obj is None: - return none_obj - elif obj is False: - return false_obj - elif obj is True: - return true_obj - else: - raise NotImplementedError - - -def compiled_objects_cache(func): - def wrapper(evaluator, obj, parent=builtin, module=None): - # Do a very cheap form of caching here. - key = id(obj), id(parent), id(module) - try: - return evaluator.compiled_cache[key][0] - except KeyError: - result = func(evaluator, obj, parent, module) - # Need to cache all of them, otherwise the id could be overwritten. - evaluator.compiled_cache[key] = result, obj, parent, module - return result - return wrapper - - -@compiled_objects_cache -def create(evaluator, obj, parent=builtin, module=None): - """ - A very weird interface class to this module. The more options provided the - more acurate loading compiled objects is. - """ - - if not inspect.ismodule(obj): - faked = fake.get_faked(module and module.obj, obj) - if faked is not None: - faked.parent = parent - return faked - - try: - if parent == builtin and obj.__module__ in ('builtins', '__builtin__'): - return builtin.get_by_name(obj.__name__) - except AttributeError: - pass - - return CompiledObject(obj, parent) diff --git a/pythonFiles/jedi/evaluate/compiled/fake.py b/pythonFiles/jedi/evaluate/compiled/fake.py deleted file mode 100644 index 0037cfc77a80..000000000000 --- a/pythonFiles/jedi/evaluate/compiled/fake.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Loads functions that are mixed in to the standard library. E.g. builtins are -written in C (binaries), but my autocompletion only understands Python code. By -mixing in Python code, the autocompletion should work much better for builtins. -""" - -import os -import inspect - -from jedi._compatibility import is_py3, builtins, unicode -from jedi.parser import Parser, load_grammar -from jedi.parser import tree as pt -from jedi.evaluate.helpers import FakeName - -modules = {} - - -def _load_faked_module(module): - module_name = module.__name__ - if module_name == '__builtin__' and not is_py3: - module_name = 'builtins' - - try: - return modules[module_name] - except KeyError: - path = os.path.dirname(os.path.abspath(__file__)) - try: - with open(os.path.join(path, 'fake', module_name) + '.pym') as f: - source = f.read() - except IOError: - modules[module_name] = None - return - grammar = load_grammar('grammar3.4') - module = Parser(grammar, unicode(source), module_name).module - modules[module_name] = module - - if module_name == 'builtins' and not is_py3: - # There are two implementations of `open` for either python 2/3. - # -> Rename the python2 version (`look at fake/builtins.pym`). - open_func = search_scope(module, 'open') - open_func.children[1] = FakeName('open_python3') - open_func = search_scope(module, 'open_python2') - open_func.children[1] = FakeName('open') - return module - - -def search_scope(scope, obj_name): - for s in scope.subscopes: - if str(s.name) == obj_name: - return s - - -def get_module(obj): - if inspect.ismodule(obj): - return obj - try: - obj = obj.__objclass__ - except AttributeError: - pass - - try: - imp_plz = obj.__module__ - except AttributeError: - # Unfortunately in some cases like `int` there's no __module__ - return builtins - else: - return __import__(imp_plz) - - -def _faked(module, obj, name): - # Crazy underscore actions to try to escape all the internal madness. - if module is None: - module = get_module(obj) - - faked_mod = _load_faked_module(module) - if faked_mod is None: - return - - # Having the module as a `parser.representation.module`, we need to scan - # for methods. - if name is None: - if inspect.isbuiltin(obj): - return search_scope(faked_mod, obj.__name__) - elif not inspect.isclass(obj): - # object is a method or descriptor - cls = search_scope(faked_mod, obj.__objclass__.__name__) - if cls is None: - return - return search_scope(cls, obj.__name__) - else: - if obj == module: - return search_scope(faked_mod, name) - else: - cls = search_scope(faked_mod, obj.__name__) - if cls is None: - return - return search_scope(cls, name) - - -def get_faked(module, obj, name=None): - obj = obj.__class__ if is_class_instance(obj) else obj - result = _faked(module, obj, name) - if result is None or isinstance(result, pt.Class): - # We're not interested in classes. What we want is functions. - return None - else: - # Set the docstr which was previously not set (faked modules don't - # contain it). - doc = '"""%s"""' % obj.__doc__ # TODO need escapes. - suite = result.children[-1] - string = pt.String(pt.zero_position_modifier, doc, (0, 0), '') - new_line = pt.Whitespace('\n', (0, 0), '') - docstr_node = pt.Node('simple_stmt', [string, new_line]) - suite.children.insert(2, docstr_node) - return result - - -def is_class_instance(obj): - """Like inspect.* methods.""" - return not (inspect.isclass(obj) or inspect.ismodule(obj) - or inspect.isbuiltin(obj) or inspect.ismethod(obj) - or inspect.ismethoddescriptor(obj) or inspect.iscode(obj) - or inspect.isgenerator(obj)) diff --git a/pythonFiles/jedi/evaluate/compiled/fake/_functools.pym b/pythonFiles/jedi/evaluate/compiled/fake/_functools.pym deleted file mode 100644 index 909ef03fc3dd..000000000000 --- a/pythonFiles/jedi/evaluate/compiled/fake/_functools.pym +++ /dev/null @@ -1,9 +0,0 @@ -class partial(): - def __init__(self, func, *args, **keywords): - self.__func = func - self.__args = args - self.__keywords = keywords - - def __call__(self, *args, **kwargs): - # TODO should be **dict(self.__keywords, **kwargs) - return self.__func(*(self.__args + args), **self.__keywords) diff --git a/pythonFiles/jedi/evaluate/compiled/fake/_sqlite3.pym b/pythonFiles/jedi/evaluate/compiled/fake/_sqlite3.pym deleted file mode 100644 index 2151e652b401..000000000000 --- a/pythonFiles/jedi/evaluate/compiled/fake/_sqlite3.pym +++ /dev/null @@ -1,26 +0,0 @@ -def connect(database, timeout=None, isolation_level=None, detect_types=None, factory=None): - return Connection() - - -class Connection(): - def cursor(self): - return Cursor() - - -class Cursor(): - def cursor(self): - return Cursor() - - def fetchone(self): - return Row() - - def fetchmany(self, size=cursor.arraysize): - return [self.fetchone()] - - def fetchall(self): - return [self.fetchone()] - - -class Row(): - def keys(self): - return [''] diff --git a/pythonFiles/jedi/evaluate/compiled/fake/_sre.pym b/pythonFiles/jedi/evaluate/compiled/fake/_sre.pym deleted file mode 100644 index 217be5633982..000000000000 --- a/pythonFiles/jedi/evaluate/compiled/fake/_sre.pym +++ /dev/null @@ -1,99 +0,0 @@ -def compile(): - class SRE_Match(): - endpos = int() - lastgroup = int() - lastindex = int() - pos = int() - string = str() - regs = ((int(), int()),) - - def __init__(self, pattern): - self.re = pattern - - def start(self): - return int() - - def end(self): - return int() - - def span(self): - return int(), int() - - def expand(self): - return str() - - def group(self, nr): - return str() - - def groupdict(self): - return {str(): str()} - - def groups(self): - return (str(),) - - class SRE_Pattern(): - flags = int() - groupindex = {} - groups = int() - pattern = str() - - def findall(self, string, pos=None, endpos=None): - """ - findall(string[, pos[, endpos]]) --> list. - Return a list of all non-overlapping matches of pattern in string. - """ - return [str()] - - def finditer(self, string, pos=None, endpos=None): - """ - finditer(string[, pos[, endpos]]) --> iterator. - Return an iterator over all non-overlapping matches for the - RE pattern in string. For each match, the iterator returns a - match object. - """ - yield SRE_Match(self) - - def match(self, string, pos=None, endpos=None): - """ - match(string[, pos[, endpos]]) --> match object or None. - Matches zero or more characters at the beginning of the string - pattern - """ - return SRE_Match(self) - - def scanner(self, string, pos=None, endpos=None): - pass - - def search(self, string, pos=None, endpos=None): - """ - search(string[, pos[, endpos]]) --> match object or None. - Scan through string looking for a match, and return a corresponding - MatchObject instance. Return None if no position in the string matches. - """ - return SRE_Match(self) - - def split(self, string, maxsplit=0]): - """ - split(string[, maxsplit = 0]) --> list. - Split string by the occurrences of pattern. - """ - return [str()] - - def sub(self, repl, string, count=0): - """ - sub(repl, string[, count = 0]) --> newstring - Return the string obtained by replacing the leftmost non-overlapping - occurrences of pattern in string by the replacement repl. - """ - return str() - - def subn(self, repl, string, count=0): - """ - subn(repl, string[, count = 0]) --> (newstring, number of subs) - Return the tuple (new_string, number_of_subs_made) found by replacing - the leftmost non-overlapping occurrences of pattern with the - replacement repl. - """ - return (str(), int()) - - return SRE_Pattern() diff --git a/pythonFiles/jedi/evaluate/compiled/fake/_weakref.pym b/pythonFiles/jedi/evaluate/compiled/fake/_weakref.pym deleted file mode 100644 index 8d21a2c4a7c6..000000000000 --- a/pythonFiles/jedi/evaluate/compiled/fake/_weakref.pym +++ /dev/null @@ -1,8 +0,0 @@ -def proxy(object, callback=None): - return object - -class weakref(): - def __init__(self, object, callback=None): - self.__object = object - def __call__(self): - return self.__object diff --git a/pythonFiles/jedi/evaluate/compiled/fake/builtins.pym b/pythonFiles/jedi/evaluate/compiled/fake/builtins.pym deleted file mode 100644 index 1ed9b0b20857..000000000000 --- a/pythonFiles/jedi/evaluate/compiled/fake/builtins.pym +++ /dev/null @@ -1,259 +0,0 @@ -""" -Pure Python implementation of some builtins. -This code is not going to be executed anywhere. -These implementations are not always correct, but should work as good as -possible for the auto completion. -""" - - -def next(iterator, default=None): - if random.choice([0, 1]): - if hasattr("next"): - return iterator.next() - else: - return iterator.__next__() - else: - if default is not None: - return default - - -def iter(collection, sentinel=None): - if sentinel: - yield collection() - else: - for c in collection: - yield c - - -def range(start, stop=None, step=1): - return [0] - - -class file(): - def __iter__(self): - yield '' - def next(self): - return '' - - -class xrange(): - # Attention: this function doesn't exist in Py3k (there it is range). - def __iter__(self): - yield 1 - - def count(self): - return 1 - - def index(self): - return 1 - - -def open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True): - import io - return io.TextIOWrapper(file, mode, buffering, encoding, errors, newline, closefd) - - -def open_python2(name, mode=None, buffering=None): - return file(name, mode, buffering) - - -#-------------------------------------------------------- -# descriptors -#-------------------------------------------------------- -class property(): - def __init__(self, fget, fset=None, fdel=None, doc=None): - self.fget = fget - self.fset = fset - self.fdel = fdel - self.__doc__ = doc - - def __get__(self, obj, cls): - return self.fget(obj) - - def __set__(self, obj, value): - self.fset(obj, value) - - def __delete__(self, obj): - self.fdel(obj) - - def setter(self, func): - self.fset = func - return self - - def getter(self, func): - self.fget = func - return self - - def deleter(self, func): - self.fdel = func - return self - - -class staticmethod(): - def __init__(self, func): - self.__func = func - - def __get__(self, obj, cls): - return self.__func - - -class classmethod(): - def __init__(self, func): - self.__func = func - - def __get__(self, obj, cls): - def _method(*args, **kwargs): - return self.__func(cls, *args, **kwargs) - return _method - - -#-------------------------------------------------------- -# array stuff -#-------------------------------------------------------- -class list(): - def __init__(self, iterable=[]): - self.__iterable = [] - for i in iterable: - self.__iterable += [i] - - def __iter__(self): - for i in self.__iterable: - yield i - - def __getitem__(self, y): - return self.__iterable[y] - - def pop(self): - return self.__iterable[-1] - - -class tuple(): - def __init__(self, iterable=[]): - self.__iterable = [] - for i in iterable: - self.__iterable += [i] - - def __iter__(self): - for i in self.__iterable: - yield i - - def __getitem__(self, y): - return self.__iterable[y] - - def index(self): - return 1 - - def count(self): - return 1 - - -class set(): - def __init__(self, iterable=[]): - self.__iterable = iterable - - def __iter__(self): - for i in self.__iterable: - yield i - - def pop(self): - return list(self.__iterable)[-1] - - def copy(self): - return self - - def difference(self, other): - return self - other - - def intersection(self, other): - return self & other - - def symmetric_difference(self, other): - return self ^ other - - def union(self, other): - return self | other - - -class frozenset(): - def __init__(self, iterable=[]): - self.__iterable = iterable - - def __iter__(self): - for i in self.__iterable: - yield i - - def copy(self): - return self - - -class dict(): - def __init__(self, **elements): - self.__elements = elements - - def clear(self): - # has a strange docstr - pass - - def get(self, k, d=None): - # TODO implement - try: - #return self.__elements[k] - pass - except KeyError: - return d - - def setdefault(self, k, d): - # TODO maybe also return the content - return d - - -class enumerate(): - def __init__(self, sequence, start=0): - self.__sequence = sequence - - def __iter__(self): - for i in self.__sequence: - yield 1, i - - def __next__(self): - return next(self.__iter__()) - - def next(self): - return next(self.__iter__()) - - -class reversed(): - def __init__(self, sequence): - self.__sequence = sequence - - def __iter__(self): - for i in self.__sequence: - yield i - - def __next__(self): - return next(self.__iter__()) - - def next(self): - return next(self.__iter__()) - - -def sorted(iterable, cmp=None, key=None, reverse=False): - return iterable - - -#-------------------------------------------------------- -# basic types -#-------------------------------------------------------- -class int(): - def __init__(self, x, base=None): - pass - - -class str(): - def __init__(self, obj): - pass - - -class type(): - def mro(): - return [object] diff --git a/pythonFiles/jedi/evaluate/compiled/fake/datetime.pym b/pythonFiles/jedi/evaluate/compiled/fake/datetime.pym deleted file mode 100644 index 823ac5b7fd56..000000000000 --- a/pythonFiles/jedi/evaluate/compiled/fake/datetime.pym +++ /dev/null @@ -1,4 +0,0 @@ -class datetime(): - @staticmethod - def now(): - return datetime() diff --git a/pythonFiles/jedi/evaluate/compiled/fake/io.pym b/pythonFiles/jedi/evaluate/compiled/fake/io.pym deleted file mode 100644 index 87b02eed4321..000000000000 --- a/pythonFiles/jedi/evaluate/compiled/fake/io.pym +++ /dev/null @@ -1,6 +0,0 @@ -class TextIOWrapper(): - def __next__(self): - return str() - - def __iter__(self): - yield str() diff --git a/pythonFiles/jedi/evaluate/compiled/fake/posix.pym b/pythonFiles/jedi/evaluate/compiled/fake/posix.pym deleted file mode 100644 index 4417f7cb0427..000000000000 --- a/pythonFiles/jedi/evaluate/compiled/fake/posix.pym +++ /dev/null @@ -1,5 +0,0 @@ -def getcwd(): - return '' - -def getcwdu(): - return '' diff --git a/pythonFiles/jedi/evaluate/docstrings.py b/pythonFiles/jedi/evaluate/docstrings.py deleted file mode 100644 index 84137de59742..000000000000 --- a/pythonFiles/jedi/evaluate/docstrings.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -Docstrings are another source of information for functions and classes. -:mod:`jedi.evaluate.dynamic` tries to find all executions of functions, while -the docstring parsing is much easier. There are two different types of -docstrings that |jedi| understands: - -- `Sphinx `_ -- `Epydoc `_ - -For example, the sphinx annotation ``:type foo: str`` clearly states that the -type of ``foo`` is ``str``. - -As an addition to parameter searching, this module also provides return -annotations. -""" - -from ast import literal_eval -import re -from itertools import chain -from textwrap import dedent - -from jedi.evaluate.cache import memoize_default -from jedi.parser import Parser, load_grammar -from jedi.common import indent_block -from jedi.evaluate.iterable import Array, FakeSequence, AlreadyEvaluated - - -DOCSTRING_PARAM_PATTERNS = [ - r'\s*:type\s+%s:\s*([^\n]+)', # Sphinx - r'\s*:param\s+(\w+)\s+%s:[^\n]+', # Sphinx param with type - r'\s*@type\s+%s:\s*([^\n]+)', # Epydoc -] - -DOCSTRING_RETURN_PATTERNS = [ - re.compile(r'\s*:rtype:\s*([^\n]+)', re.M), # Sphinx - re.compile(r'\s*@rtype:\s*([^\n]+)', re.M), # Epydoc -] - -REST_ROLE_PATTERN = re.compile(r':[^`]+:`([^`]+)`') - - -try: - from numpydoc.docscrape import NumpyDocString -except ImportError: - def _search_param_in_numpydocstr(docstr, param_str): - return [] -else: - def _search_param_in_numpydocstr(docstr, param_str): - """Search `docstr` (in numpydoc format) for type(-s) of `param_str`.""" - params = NumpyDocString(docstr)._parsed_data['Parameters'] - for p_name, p_type, p_descr in params: - if p_name == param_str: - m = re.match('([^,]+(,[^,]+)*?)(,[ ]*optional)?$', p_type) - if m: - p_type = m.group(1) - - if p_type.startswith('{'): - types = set(type(x).__name__ for x in literal_eval(p_type)) - return list(types) - else: - return [p_type] - return [] - - -def _search_param_in_docstr(docstr, param_str): - """ - Search `docstr` for type(-s) of `param_str`. - - >>> _search_param_in_docstr(':type param: int', 'param') - ['int'] - >>> _search_param_in_docstr('@type param: int', 'param') - ['int'] - >>> _search_param_in_docstr( - ... ':type param: :class:`threading.Thread`', 'param') - ['threading.Thread'] - >>> bool(_search_param_in_docstr('no document', 'param')) - False - >>> _search_param_in_docstr(':param int param: some description', 'param') - ['int'] - - """ - # look at #40 to see definitions of those params - patterns = [re.compile(p % re.escape(param_str)) - for p in DOCSTRING_PARAM_PATTERNS] - for pattern in patterns: - match = pattern.search(docstr) - if match: - return [_strip_rst_role(match.group(1))] - - return (_search_param_in_numpydocstr(docstr, param_str) or - []) - - -def _strip_rst_role(type_str): - """ - Strip off the part looks like a ReST role in `type_str`. - - >>> _strip_rst_role(':class:`ClassName`') # strip off :class: - 'ClassName' - >>> _strip_rst_role(':py:obj:`module.Object`') # works with domain - 'module.Object' - >>> _strip_rst_role('ClassName') # do nothing when not ReST role - 'ClassName' - - See also: - http://sphinx-doc.org/domains.html#cross-referencing-python-objects - - """ - match = REST_ROLE_PATTERN.match(type_str) - if match: - return match.group(1) - else: - return type_str - - -def _evaluate_for_statement_string(evaluator, string, module): - code = dedent(""" - def pseudo_docstring_stuff(): - # Create a pseudo function for docstring statements. - %s - """) - if string is None: - return [] - - for element in re.findall('((?:\w+\.)*\w+)\.', string): - # Try to import module part in dotted name. - # (e.g., 'threading' in 'threading.Thread'). - string = 'import %s\n' % element + string - - # Take the default grammar here, if we load the Python 2.7 grammar here, it - # will be impossible to use `...` (Ellipsis) as a token. Docstring types - # don't need to conform with the current grammar. - p = Parser(load_grammar(), code % indent_block(string)) - try: - pseudo_cls = p.module.subscopes[0] - # First pick suite, then simple_stmt (-2 for DEDENT) and then the node, - # which is also not the last item, because there's a newline. - stmt = pseudo_cls.children[-1].children[-2].children[-2] - except (AttributeError, IndexError): - return [] - - # Use the module of the param. - # TODO this module is not the module of the param in case of a function - # call. In that case it's the module of the function call. - # stuffed with content from a function call. - pseudo_cls.parent = module - return list(_execute_types_in_stmt(evaluator, stmt)) - - -def _execute_types_in_stmt(evaluator, stmt): - """ - Executing all types or general elements that we find in a statement. This - doesn't include tuple, list and dict literals, because the stuff they - contain is executed. (Used as type information). - """ - definitions = evaluator.eval_element(stmt) - return chain.from_iterable(_execute_array_values(evaluator, d) for d in definitions) - - -def _execute_array_values(evaluator, array): - """ - Tuples indicate that there's not just one return value, but the listed - ones. `(str, int)` means that it returns a tuple with both types. - """ - if isinstance(array, Array): - values = [] - for typ in array.values(): - objects = _execute_array_values(evaluator, typ) - values.append(AlreadyEvaluated(objects)) - return [FakeSequence(evaluator, values, array.type)] - else: - return evaluator.execute(array) - - -@memoize_default(None, evaluator_is_first_arg=True) -def follow_param(evaluator, param): - func = param.parent_function - - return [p - for param_str in _search_param_in_docstr(func.raw_doc, - str(param.name)) - for p in _evaluate_for_statement_string(evaluator, param_str, - param.get_parent_until())] - - -@memoize_default(None, evaluator_is_first_arg=True) -def find_return_types(evaluator, func): - def search_return_in_docstr(code): - for p in DOCSTRING_RETURN_PATTERNS: - match = p.search(code) - if match: - return _strip_rst_role(match.group(1)) - - type_str = search_return_in_docstr(func.raw_doc) - return _evaluate_for_statement_string(evaluator, type_str, func.get_parent_until()) diff --git a/pythonFiles/jedi/evaluate/dynamic.py b/pythonFiles/jedi/evaluate/dynamic.py deleted file mode 100644 index 04ed909a1949..000000000000 --- a/pythonFiles/jedi/evaluate/dynamic.py +++ /dev/null @@ -1,146 +0,0 @@ -""" -One of the really important features of |jedi| is to have an option to -understand code like this:: - - def foo(bar): - bar. # completion here - foo(1) - -There's no doubt wheter bar is an ``int`` or not, but if there's also a call -like ``foo('str')``, what would happen? Well, we'll just show both. Because -that's what a human would expect. - -It works as follows: - -- |Jedi| sees a param -- search for function calls named ``foo`` -- execute these calls and check the input. This work with a ``ParamListener``. -""" -from itertools import chain - -from jedi._compatibility import unicode -from jedi.parser import tree -from jedi import settings -from jedi import debug -from jedi.evaluate.cache import memoize_default -from jedi.evaluate import imports - - -class ParamListener(object): - """ - This listener is used to get the params for a function. - """ - def __init__(self): - self.param_possibilities = [] - - def execute(self, params): - self.param_possibilities += params - - -@debug.increase_indent -def search_params(evaluator, param): - """ - A dynamic search for param values. If you try to complete a type: - - >>> def func(foo): - ... foo - >>> func(1) - >>> func("") - - It is not known what the type ``foo`` without analysing the whole code. You - have to look for all calls to ``func`` to find out what ``foo`` possibly - is. - """ - if not settings.dynamic_params: - return [] - - func = param.get_parent_until(tree.Function) - debug.dbg('Dynamic param search for %s in %s.', param, str(func.name)) - # Compare the param names. - names = [n for n in search_function_call(evaluator, func) - if n.value == param.name.value] - # Evaluate the ExecutedParams to types. - result = list(chain.from_iterable(n.parent.eval(evaluator) for n in names)) - debug.dbg('Dynamic param result %s', result) - return result - - -@memoize_default([], evaluator_is_first_arg=True) -def search_function_call(evaluator, func): - """ - Returns a list of param names. - """ - from jedi.evaluate import representation as er - - def get_params_for_module(module): - """ - Returns the values of a param, or an empty array. - """ - @memoize_default([], evaluator_is_first_arg=True) - def get_posibilities(evaluator, module, func_name): - try: - names = module.used_names[func_name] - except KeyError: - return [] - - for name in names: - parent = name.parent - if tree.is_node(parent, 'trailer'): - parent = parent.parent - - trailer = None - if tree.is_node(parent, 'power'): - for t in parent.children[1:]: - if t == '**': - break - if t.start_pos > name.start_pos and t.children[0] == '(': - trailer = t - break - if trailer is not None: - types = evaluator.goto_definition(name) - - # We have to remove decorators, because they are not the - # "original" functions, this way we can easily compare. - # At the same time we also have to remove InstanceElements. - undec = [] - for escope in types: - if escope.isinstance(er.Function, er.Instance) \ - and escope.decorates is not None: - undec.append(escope.decorates) - elif isinstance(escope, er.InstanceElement): - undec.append(escope.var) - else: - undec.append(escope) - - if evaluator.wrap(compare) in undec: - # Only if we have the correct function we execute - # it, otherwise just ignore it. - evaluator.eval_trailer(types, trailer) - return listener.param_possibilities - return get_posibilities(evaluator, module, func_name) - - current_module = func.get_parent_until() - func_name = unicode(func.name) - compare = func - if func_name == '__init__': - cls = func.get_parent_scope() - if isinstance(cls, tree.Class): - func_name = unicode(cls.name) - compare = cls - - # add the listener - listener = ParamListener() - func.listeners.add(listener) - - try: - result = [] - # This is like backtracking: Get the first possible result. - for mod in imports.get_modules_containing_name(evaluator, [current_module], func_name): - result = get_params_for_module(mod) - if result: - break - finally: - # cleanup: remove the listener; important: should not stick. - func.listeners.remove(listener) - - return result diff --git a/pythonFiles/jedi/evaluate/finder.py b/pythonFiles/jedi/evaluate/finder.py deleted file mode 100644 index c112f971b277..000000000000 --- a/pythonFiles/jedi/evaluate/finder.py +++ /dev/null @@ -1,547 +0,0 @@ -""" -Searching for names with given scope and name. This is very central in Jedi and -Python. The name resolution is quite complicated with descripter, -``__getattribute__``, ``__getattr__``, ``global``, etc. - -Flow checks -+++++++++++ - -Flow checks are not really mature. There's only a check for ``isinstance``. It -would check whether a flow has the form of ``if isinstance(a, type_or_tuple)``. -Unfortunately every other thing is being ignored (e.g. a == '' would be easy to -check for -> a is a string). There's big potential in these checks. -""" -from itertools import chain - -from jedi._compatibility import unicode, u -from jedi.parser import tree -from jedi import debug -from jedi import common -from jedi import settings -from jedi.evaluate import representation as er -from jedi.evaluate import dynamic -from jedi.evaluate import compiled -from jedi.evaluate import docstrings -from jedi.evaluate import iterable -from jedi.evaluate import imports -from jedi.evaluate import analysis -from jedi.evaluate import flow_analysis -from jedi.evaluate import param -from jedi.evaluate import helpers -from jedi.evaluate.cache import memoize_default - - -def filter_after_position(names, position): - """ - Removes all names after a certain position. If position is None, just - returns the names list. - """ - if position is None: - return names - - names_new = [] - for n in names: - # Filter positions and also allow list comprehensions and lambdas. - if n.start_pos[0] is not None and n.start_pos < position \ - or isinstance(n.get_definition(), (tree.CompFor, tree.Lambda)): - names_new.append(n) - return names_new - - -def filter_definition_names(names, origin, position=None): - """ - Filter names that are actual definitions in a scope. Names that are just - used will be ignored. - """ - # Just calculate the scope from the first - stmt = names[0].get_definition() - scope = stmt.get_parent_scope() - - if not (isinstance(scope, er.FunctionExecution) - and isinstance(scope.base, er.LambdaWrapper)): - names = filter_after_position(names, position) - names = [name for name in names if name.is_definition()] - - # Private name mangling (compile.c) disallows access on names - # preceeded by two underscores `__` if used outside of the class. Names - # that also end with two underscores (e.g. __id__) are not affected. - for name in list(names): - if name.value.startswith('__') and not name.value.endswith('__'): - if filter_private_variable(scope, origin): - names.remove(name) - return names - - -class NameFinder(object): - def __init__(self, evaluator, scope, name_str, position=None): - self._evaluator = evaluator - # Make sure that it's not just a syntax tree node. - self.scope = evaluator.wrap(scope) - self.name_str = name_str - self.position = position - - @debug.increase_indent - def find(self, scopes, search_global=False): - # TODO rename scopes to names_dicts - names = self.filter_name(scopes) - types = self._names_to_types(names, search_global) - - if not names and not types \ - and not (isinstance(self.name_str, tree.Name) - and isinstance(self.name_str.parent.parent, tree.Param)): - if not isinstance(self.name_str, (str, unicode)): # TODO Remove? - if search_global: - message = ("NameError: name '%s' is not defined." - % self.name_str) - analysis.add(self._evaluator, 'name-error', self.name_str, - message) - else: - analysis.add_attribute_error(self._evaluator, - self.scope, self.name_str) - - debug.dbg('finder._names_to_types: %s -> %s', names, types) - return types - - def scopes(self, search_global=False): - if search_global: - return global_names_dict_generator(self._evaluator, self.scope, self.position) - else: - return ((n, None) for n in self.scope.names_dicts(search_global)) - - def names_dict_lookup(self, names_dict, position): - def get_param(scope, el): - if isinstance(el.get_parent_until(tree.Param), tree.Param): - return scope.param_by_name(str(el)) - return el - - search_str = str(self.name_str) - try: - names = names_dict[search_str] - if not names: # We want names, otherwise stop. - return [] - except KeyError: - return [] - - names = filter_definition_names(names, self.name_str, position) - - name_scope = None - # Only the names defined in the last position are valid definitions. - last_names = [] - for name in reversed(sorted(names, key=lambda name: name.start_pos)): - stmt = name.get_definition() - name_scope = self._evaluator.wrap(stmt.get_parent_scope()) - - if isinstance(self.scope, er.Instance) and not isinstance(name_scope, er.Instance): - # Instances should not be checked for positioning, because we - # don't know in which order the functions are called. - last_names.append(name) - continue - - if isinstance(name_scope, compiled.CompiledObject): - # Let's test this. TODO need comment. shouldn't this be - # filtered before? - last_names.append(name) - continue - - if isinstance(name, compiled.CompiledName) \ - or isinstance(name, er.InstanceName) and isinstance(name._origin_name, compiled.CompiledName): - last_names.append(name) - continue - - if isinstance(self.name_str, tree.Name): - origin_scope = self.name_str.get_parent_until(tree.Scope, reverse=True) - else: - origin_scope = None - if isinstance(stmt.parent, compiled.CompiledObject): - # TODO seriously? this is stupid. - continue - check = flow_analysis.break_check(self._evaluator, name_scope, - stmt, origin_scope) - if check is not flow_analysis.UNREACHABLE: - last_names.append(name) - if check is flow_analysis.REACHABLE: - break - - if isinstance(name_scope, er.FunctionExecution): - # Replace params - return [get_param(name_scope, n) for n in last_names] - return last_names - - def filter_name(self, names_dicts): - """ - Searches names that are defined in a scope (the different - `names_dicts`), until a name fits. - """ - names = [] - for names_dict, position in names_dicts: - names = self.names_dict_lookup(names_dict, position) - if names: - break - - debug.dbg('finder.filter_name "%s" in (%s): %s@%s', self.name_str, - self.scope, u(names), self.position) - return list(self._clean_names(names)) - - def _clean_names(self, names): - """ - ``NameFinder.filter_name`` should only output names with correct - wrapper parents. We don't want to see AST classes out in the - evaluation, so remove them already here! - """ - for n in names: - definition = n.parent - if isinstance(definition, (tree.Function, tree.Class, tree.Module)): - yield self._evaluator.wrap(definition).name - else: - yield n - - def _check_getattr(self, inst): - """Checks for both __getattr__ and __getattribute__ methods""" - result = [] - # str is important, because it shouldn't be `Name`! - name = compiled.create(self._evaluator, str(self.name_str)) - with common.ignored(KeyError): - result = inst.execute_subscope_by_name('__getattr__', name) - if not result: - # this is a little bit special. `__getattribute__` is executed - # before anything else. But: I know no use case, where this - # could be practical and the jedi would return wrong types. If - # you ever have something, let me know! - with common.ignored(KeyError): - result = inst.execute_subscope_by_name('__getattribute__', name) - return result - - def _names_to_types(self, names, search_global): - types = [] - - # Add isinstance and other if/assert knowledge. - if isinstance(self.name_str, tree.Name): - # Ignore FunctionExecution parents for now. - flow_scope = self.name_str - until = flow_scope.get_parent_until(er.FunctionExecution) - while not isinstance(until, er.FunctionExecution): - flow_scope = flow_scope.get_parent_scope(include_flows=True) - if flow_scope is None: - break - # TODO check if result is in scope -> no evaluation necessary - n = check_flow_information(self._evaluator, flow_scope, - self.name_str, self.position) - if n: - return n - - for name in names: - new_types = _name_to_types(self._evaluator, name, self.scope) - if isinstance(self.scope, (er.Class, er.Instance)) and not search_global: - types += self._resolve_descriptors(name, new_types) - else: - types += new_types - if not names and isinstance(self.scope, er.Instance): - # handling __getattr__ / __getattribute__ - types = self._check_getattr(self.scope) - - return types - - def _resolve_descriptors(self, name, types): - # The name must not be in the dictionary, but part of the class - # definition. __get__ is only called if the descriptor is defined in - # the class dictionary. - name_scope = name.get_definition().get_parent_scope() - if not isinstance(name_scope, (er.Instance, tree.Class)): - return types - - result = [] - for r in types: - try: - desc_return = r.get_descriptor_returns - except AttributeError: - result.append(r) - else: - result += desc_return(self.scope) - return result - - -@memoize_default([], evaluator_is_first_arg=True) -def _name_to_types(evaluator, name, scope): - types = [] - typ = name.get_definition() - if typ.isinstance(tree.ForStmt): - for_types = evaluator.eval_element(typ.children[3]) - for_types = iterable.get_iterator_types(for_types) - types += check_tuple_assignments(for_types, name) - elif typ.isinstance(tree.CompFor): - for_types = evaluator.eval_element(typ.children[3]) - for_types = iterable.get_iterator_types(for_types) - types += check_tuple_assignments(for_types, name) - elif isinstance(typ, tree.Param): - types += _eval_param(evaluator, typ, scope) - elif typ.isinstance(tree.ExprStmt): - types += _remove_statements(evaluator, typ, name) - elif typ.isinstance(tree.WithStmt): - types += evaluator.eval_element(typ.node_from_name(name)) - elif isinstance(typ, tree.Import): - types += imports.ImportWrapper(evaluator, name).follow() - elif isinstance(typ, tree.GlobalStmt): - # TODO theoretically we shouldn't be using search_global here, it - # doesn't make sense, because it's a local search (for that name)! - # However, globals are not that important and resolving them doesn't - # guarantee correctness in any way, because we don't check for when - # something is executed. - types += evaluator.find_types(typ.get_parent_scope(), str(name), - search_global=True) - elif isinstance(typ, tree.TryStmt): - # TODO an exception can also be a tuple. Check for those. - # TODO check for types that are not classes and add it to - # the static analysis report. - exceptions = evaluator.eval_element(name.prev_sibling().prev_sibling()) - types = list(chain.from_iterable( - evaluator.execute(t) for t in exceptions)) - else: - if typ.isinstance(er.Function): - typ = typ.get_decorated_func() - types.append(typ) - return types - - -def _remove_statements(evaluator, stmt, name): - """ - This is the part where statements are being stripped. - - Due to lazy evaluation, statements like a = func; b = a; b() have to be - evaluated. - """ - types = [] - # Remove the statement docstr stuff for now, that has to be - # implemented with the evaluator class. - #if stmt.docstr: - #res_new.append(stmt) - - check_instance = None - if isinstance(stmt, er.InstanceElement) and stmt.is_class_var: - check_instance = stmt.instance - stmt = stmt.var - - types += evaluator.eval_statement(stmt, seek_name=name) - - if check_instance is not None: - # class renames - types = [er.get_instance_el(evaluator, check_instance, a, True) - if isinstance(a, (er.Function, tree.Function)) - else a for a in types] - return types - - -def _eval_param(evaluator, param, scope): - res_new = [] - func = param.get_parent_scope() - - cls = func.parent.get_parent_until((tree.Class, tree.Function)) - - from jedi.evaluate.param import ExecutedParam, Arguments - if isinstance(cls, tree.Class) and param.position_nr == 0 \ - and not isinstance(param, ExecutedParam): - # This is where we add self - if it has never been - # instantiated. - if isinstance(scope, er.InstanceElement): - res_new.append(scope.instance) - else: - inst = er.Instance(evaluator, evaluator.wrap(cls), - Arguments(evaluator, ()), is_generated=True) - res_new.append(inst) - return res_new - - # Instances are typically faked, if the instance is not called from - # outside. Here we check it for __init__ functions and return. - if isinstance(func, er.InstanceElement) \ - and func.instance.is_generated and str(func.name) == '__init__': - param = func.var.params[param.position_nr] - - # Add docstring knowledge. - doc_params = docstrings.follow_param(evaluator, param) - if doc_params: - return doc_params - - if isinstance(param, ExecutedParam): - return res_new + param.eval(evaluator) - else: - # Param owns no information itself. - res_new += dynamic.search_params(evaluator, param) - if not res_new: - if param.stars: - t = 'tuple' if param.stars == 1 else 'dict' - typ = evaluator.find_types(compiled.builtin, t)[0] - res_new = evaluator.execute(typ) - if param.default: - res_new += evaluator.eval_element(param.default) - return res_new - - -def check_flow_information(evaluator, flow, search_name, pos): - """ Try to find out the type of a variable just with the information that - is given by the flows: e.g. It is also responsible for assert checks.:: - - if isinstance(k, str): - k. # <- completion here - - ensures that `k` is a string. - """ - if not settings.dynamic_flow_information: - return None - - result = [] - if flow.is_scope(): - # Check for asserts. - try: - names = reversed(flow.names_dict[search_name.value]) - except (KeyError, AttributeError): - names = [] - - for name in names: - ass = name.get_parent_until(tree.AssertStmt) - if isinstance(ass, tree.AssertStmt) and pos is not None and ass.start_pos < pos: - result = _check_isinstance_type(evaluator, ass.assertion(), search_name) - if result: - break - - if isinstance(flow, (tree.IfStmt, tree.WhileStmt)): - element = flow.children[1] - result = _check_isinstance_type(evaluator, element, search_name) - return result - - -def _check_isinstance_type(evaluator, element, search_name): - try: - assert element.type == 'power' - # this might be removed if we analyze and, etc - assert len(element.children) == 2 - first, trailer = element.children - assert isinstance(first, tree.Name) and first.value == 'isinstance' - assert trailer.type == 'trailer' and trailer.children[0] == '(' - assert len(trailer.children) == 3 - - # arglist stuff - arglist = trailer.children[1] - args = param.Arguments(evaluator, arglist, trailer) - lst = list(args.unpack()) - # Disallow keyword arguments - assert len(lst) == 2 and lst[0][0] is None and lst[1][0] is None - name = lst[0][1][0] # first argument, values, first value - # Do a simple get_code comparison. They should just have the same code, - # and everything will be all right. - classes = lst[1][1][0] - call = helpers.call_of_name(search_name) - assert name.get_code() == call.get_code() - except AssertionError: - return [] - - result = [] - for typ in evaluator.eval_element(classes): - for typ in (typ.values() if isinstance(typ, iterable.Array) else [typ]): - result += evaluator.execute(typ) - return result - - -def global_names_dict_generator(evaluator, scope, position): - """ - For global name lookups. Yields tuples of (names_dict, position). If the - position is None, the position does not matter anymore in that scope. - - This function is used to include names from outer scopes. For example, when - the current scope is function: - - >>> from jedi._compatibility import u, no_unicode_pprint - >>> from jedi.parser import Parser, load_grammar - >>> parser = Parser(load_grammar(), u(''' - ... x = ['a', 'b', 'c'] - ... def func(): - ... y = None - ... ''')) - >>> scope = parser.module.subscopes[0] - >>> scope - - - `global_names_dict_generator` is a generator. First it yields names from - most inner scope. - - >>> from jedi.evaluate import Evaluator - >>> evaluator = Evaluator(load_grammar()) - >>> scope = evaluator.wrap(scope) - >>> pairs = list(global_names_dict_generator(evaluator, scope, (4, 0))) - >>> no_unicode_pprint(pairs[0]) - ({'func': [], 'y': []}, (4, 0)) - - Then it yields the names from one level "lower". In this example, this - is the most outer scope. As you can see, the position in the tuple is now - None, because typically the whole module is loaded before the function is - called. - - >>> no_unicode_pprint(pairs[1]) - ({'func': [], 'x': []}, None) - - After that we have a few underscore names that are part of the module. - - >>> sorted(pairs[2][0].keys()) - ['__doc__', '__file__', '__name__', '__package__'] - >>> pairs[3] # global names -> there are none in our example. - ({}, None) - >>> pairs[4] # package modules -> Also none. - ({}, None) - - Finally, it yields names from builtin, if `include_builtin` is - true (default). - - >>> pairs[5][0].values() #doctest: +ELLIPSIS - [[], ...] - """ - in_func = False - while scope is not None: - if not (scope.type == 'classdef' and in_func): - # Names in methods cannot be resolved within the class. - - for names_dict in scope.names_dicts(True): - yield names_dict, position - if scope.type == 'funcdef': - # The position should be reset if the current scope is a function. - in_func = True - position = None - scope = evaluator.wrap(scope.get_parent_scope()) - - # Add builtins to the global scope. - for names_dict in compiled.builtin.names_dicts(True): - yield names_dict, None - - -def check_tuple_assignments(types, name): - """ - Checks if tuples are assigned. - """ - for index in name.assignment_indexes(): - new_types = [] - for r in types: - try: - func = r.get_exact_index_types - except AttributeError: - debug.warning("Invalid tuple lookup #%s of result %s in %s", - index, types, name) - else: - try: - new_types += func(index) - except IndexError: - pass - types = new_types - return types - - -def filter_private_variable(scope, origin_node): - """Check if a variable is defined inside the same class or outside.""" - instance = scope.get_parent_scope() - coming_from = origin_node - while coming_from is not None \ - and not isinstance(coming_from, (tree.Class, compiled.CompiledObject)): - coming_from = coming_from.get_parent_scope() - - # CompiledObjects don't have double underscore attributes, but Jedi abuses - # those for fakes (builtins.pym -> list). - if isinstance(instance, compiled.CompiledObject): - return instance != coming_from - else: - return isinstance(instance, er.Instance) and instance.base.base != coming_from diff --git a/pythonFiles/jedi/evaluate/flow_analysis.py b/pythonFiles/jedi/evaluate/flow_analysis.py deleted file mode 100644 index cd3df554fa7e..000000000000 --- a/pythonFiles/jedi/evaluate/flow_analysis.py +++ /dev/null @@ -1,84 +0,0 @@ -from jedi.parser import tree - - -class Status(object): - lookup_table = {} - - def __init__(self, value, name): - self._value = value - self._name = name - Status.lookup_table[value] = self - - def invert(self): - if self is REACHABLE: - return UNREACHABLE - elif self is UNREACHABLE: - return REACHABLE - else: - return UNSURE - - def __and__(self, other): - if UNSURE in (self, other): - return UNSURE - else: - return REACHABLE if self._value and other._value else UNREACHABLE - - def __repr__(self): - return '<%s: %s>' % (type(self).__name__, self._name) - - -REACHABLE = Status(True, 'reachable') -UNREACHABLE = Status(False, 'unreachable') -UNSURE = Status(None, 'unsure') - - -def break_check(evaluator, base_scope, stmt, origin_scope=None): - element_scope = evaluator.wrap(stmt.get_parent_scope(include_flows=True)) - # Direct parents get resolved, we filter scopes that are separate branches. - # This makes sense for autocompletion and static analysis. For actual - # Python it doesn't matter, because we're talking about potentially - # unreachable code. - # e.g. `if 0:` would cause all name lookup within the flow make - # unaccessible. This is not a "problem" in Python, because the code is - # never called. In Jedi though, we still want to infer types. - while origin_scope is not None: - if element_scope == origin_scope: - return REACHABLE - origin_scope = origin_scope.parent - return _break_check(evaluator, stmt, base_scope, element_scope) - - -def _break_check(evaluator, stmt, base_scope, element_scope): - element_scope = evaluator.wrap(element_scope) - base_scope = evaluator.wrap(base_scope) - - reachable = REACHABLE - if isinstance(element_scope, tree.IfStmt): - if element_scope.node_after_else(stmt): - for check_node in element_scope.check_nodes(): - reachable = _check_if(evaluator, check_node) - if reachable in (REACHABLE, UNSURE): - break - reachable = reachable.invert() - else: - node = element_scope.node_in_which_check_node(stmt) - reachable = _check_if(evaluator, node) - elif isinstance(element_scope, (tree.TryStmt, tree.WhileStmt)): - return UNSURE - - # Only reachable branches need to be examined further. - if reachable in (UNREACHABLE, UNSURE): - return reachable - - if base_scope != element_scope and base_scope != element_scope.parent: - return reachable & _break_check(evaluator, stmt, base_scope, element_scope.parent) - return reachable - - -def _check_if(evaluator, node): - types = evaluator.eval_element(node) - values = set(x.py__bool__() for x in types) - if len(values) == 1: - return Status.lookup_table[values.pop()] - else: - return UNSURE diff --git a/pythonFiles/jedi/evaluate/helpers.py b/pythonFiles/jedi/evaluate/helpers.py deleted file mode 100644 index 4802bee02afe..000000000000 --- a/pythonFiles/jedi/evaluate/helpers.py +++ /dev/null @@ -1,179 +0,0 @@ -import copy -from itertools import chain - -from jedi.parser import tree - - -def deep_ast_copy(obj, parent=None, new_elements=None): - """ - Much, much faster than copy.deepcopy, but just for Parser elements (Doesn't - copy parents). - """ - - if new_elements is None: - new_elements = {} - - def copy_node(obj): - # If it's already in the cache, just return it. - try: - return new_elements[obj] - except KeyError: - # Actually copy and set attributes. - new_obj = copy.copy(obj) - new_elements[obj] = new_obj - - # Copy children - new_children = [] - for child in obj.children: - typ = child.type - if typ in ('whitespace', 'operator', 'keyword', 'number', 'string'): - # At the moment we're not actually copying those primitive - # elements, because there's really no need to. The parents are - # obviously wrong, but that's not an issue. - new_child = child - elif typ == 'name': - new_elements[child] = new_child = copy.copy(child) - new_child.parent = new_obj - else: # Is a BaseNode. - new_child = copy_node(child) - new_child.parent = new_obj - new_children.append(new_child) - new_obj.children = new_children - - # Copy the names_dict (if there is one). - try: - names_dict = obj.names_dict - except AttributeError: - pass - else: - try: - new_obj.names_dict = new_names_dict = {} - except AttributeError: # Impossible to set CompFor.names_dict - pass - else: - for string, names in names_dict.items(): - new_names_dict[string] = [new_elements[n] for n in names] - return new_obj - - if obj.type == 'name': - # Special case of a Name object. - new_elements[obj] = new_obj = copy.copy(obj) - if parent is not None: - new_obj.parent = parent - elif isinstance(obj, tree.BaseNode): - new_obj = copy_node(obj) - if parent is not None: - for child in new_obj.children: - if isinstance(child, (tree.Name, tree.BaseNode)): - child.parent = parent - else: # String literals and so on. - new_obj = obj # Good enough, don't need to copy anything. - return new_obj - - -def call_of_name(name, cut_own_trailer=False): - """ - Creates a "call" node that consist of all ``trailer`` and ``power`` - objects. E.g. if you call it with ``append``:: - - list([]).append(3) or None - - You would get a node with the content ``list([]).append`` back. - - This generates a copy of the original ast node. - """ - par = name - if tree.is_node(par.parent, 'trailer'): - par = par.parent - - power = par.parent - if tree.is_node(power, 'power') and power.children[0] != name \ - and not (power.children[-2] == '**' and - name.start_pos > power.children[-1].start_pos): - par = power - # Now the name must be part of a trailer - index = par.children.index(name.parent) - if index != len(par.children) - 1 or cut_own_trailer: - # Now we have to cut the other trailers away. - par = deep_ast_copy(par) - if not cut_own_trailer: - # Normally we would remove just the stuff after the index, but - # if the option is set remove the index as well. (for goto) - index = index + 1 - par.children[index:] = [] - - return par - - -def get_module_names(module, all_scopes): - """ - Returns a dictionary with name parts as keys and their call paths as - values. - """ - if all_scopes: - dct = module.used_names - else: - dct = module.names_dict - return chain.from_iterable(dct.values()) - - -class FakeImport(tree.ImportName): - def __init__(self, name, parent, level=0): - super(FakeImport, self).__init__([]) - self.parent = parent - self._level = level - self.name = name - - def get_defined_names(self): - return [self.name] - - def aliases(self): - return {} - - @property - def level(self): - return self._level - - @property - def start_pos(self): - return 0, 0 - - def paths(self): - return [[self.name]] - - def is_definition(self): - return True - - -class FakeName(tree.Name): - def __init__(self, name_str, parent=None, start_pos=(0, 0), is_definition=None): - """ - In case is_definition is defined (not None), that bool value will be - returned. - """ - super(FakeName, self).__init__(tree.zero_position_modifier, name_str, start_pos) - self.parent = parent - self._is_definition = is_definition - - def get_definition(self): - return self.parent - - def is_definition(self): - if self._is_definition is None: - return super(FakeName, self).is_definition() - else: - return self._is_definition - - -class LazyName(FakeName): - def __init__(self, name, parent_callback, is_definition=None): - super(LazyName, self).__init__(name, is_definition=is_definition) - self._parent_callback = parent_callback - - @property - def parent(self): - return self._parent_callback() - - @parent.setter - def parent(self, value): - pass # Do nothing, super classes can try to set the parent. diff --git a/pythonFiles/jedi/evaluate/imports.py b/pythonFiles/jedi/evaluate/imports.py deleted file mode 100644 index c14a50c0dce0..000000000000 --- a/pythonFiles/jedi/evaluate/imports.py +++ /dev/null @@ -1,507 +0,0 @@ -""" -:mod:`jedi.evaluate.imports` is here to resolve import statements and return -the modules/classes/functions/whatever, which they stand for. However there's -not any actual importing done. This module is about finding modules in the -filesystem. This can be quite tricky sometimes, because Python imports are not -always that simple. - -This module uses imp for python up to 3.2 and importlib for python 3.3 on; the -correct implementation is delegated to _compatibility. - -This module also supports import autocompletion, which means to complete -statements like ``from datetim`` (curser at the end would return ``datetime``). -""" -import imp -import os -import pkgutil -import sys -from itertools import chain - -from jedi._compatibility import find_module, unicode -from jedi import common -from jedi import debug -from jedi import cache -from jedi.parser import fast -from jedi.parser import tree -from jedi.evaluate import sys_path -from jedi.evaluate import helpers -from jedi import settings -from jedi.common import source_to_unicode -from jedi.evaluate import compiled -from jedi.evaluate import analysis -from jedi.evaluate.cache import memoize_default, NO_DEFAULT - - -def completion_names(evaluator, imp, pos): - name = imp.name_for_position(pos) - module = evaluator.wrap(imp.get_parent_until()) - if name is None: - level = 0 - for node in imp.children: - if node.end_pos <= pos: - if node in ('.', '...'): - level += len(node.value) - import_path = [] - else: - # Completion on an existing name. - - # The import path needs to be reduced by one, because we're completing. - import_path = imp.path_for_name(name)[:-1] - level = imp.level - - importer = Importer(evaluator, tuple(import_path), module, level) - if isinstance(imp, tree.ImportFrom): - c = imp.children - only_modules = c[c.index('import')].start_pos >= pos - else: - only_modules = True - return importer.completion_names(evaluator, only_modules) - - -class ImportWrapper(tree.Base): - def __init__(self, evaluator, name): - self._evaluator = evaluator - self._name = name - - self._import = name.get_parent_until(tree.Import) - self.import_path = self._import.path_for_name(name) - - @memoize_default() - def follow(self, is_goto=False): - if self._evaluator.recursion_detector.push_stmt(self._import): - # check recursion - return [] - - try: - module = self._evaluator.wrap(self._import.get_parent_until()) - import_path = self._import.path_for_name(self._name) - from_import_name = None - try: - from_names = self._import.get_from_names() - except AttributeError: - # Is an import_name - pass - else: - if len(from_names) + 1 == len(import_path): - # We have to fetch the from_names part first and then check - # if from_names exists in the modules. - from_import_name = import_path[-1] - import_path = from_names - - importer = Importer(self._evaluator, tuple(import_path), - module, self._import.level) - - types = importer.follow() - - #if self._import.is_nested() and not self.nested_resolve: - # scopes = [NestedImportModule(module, self._import)] - - if from_import_name is not None: - types = list(chain.from_iterable( - self._evaluator.find_types(t, unicode(from_import_name), - is_goto=is_goto) - for t in types)) - - if not types: - path = import_path + [from_import_name] - importer = Importer(self._evaluator, tuple(path), - module, self._import.level) - types = importer.follow() - # goto only accepts `Name` - if is_goto: - types = [s.name for s in types] - else: - # goto only accepts `Name` - if is_goto: - types = [s.name for s in types] - - debug.dbg('after import: %s', types) - finally: - self._evaluator.recursion_detector.pop_stmt() - return types - - -class NestedImportModule(tree.Module): - """ - TODO while there's no use case for nested import module right now, we might - be able to use them for static analysis checks later on. - """ - def __init__(self, module, nested_import): - self._module = module - self._nested_import = nested_import - - def _get_nested_import_name(self): - """ - Generates an Import statement, that can be used to fake nested imports. - """ - i = self._nested_import - # This is not an existing Import statement. Therefore, set position to - # 0 (0 is not a valid line number). - zero = (0, 0) - names = [unicode(name) for name in i.namespace_names[1:]] - name = helpers.FakeName(names, self._nested_import) - new = tree.Import(i._sub_module, zero, zero, name) - new.parent = self._module - debug.dbg('Generated a nested import: %s', new) - return helpers.FakeName(str(i.namespace_names[1]), new) - - def __getattr__(self, name): - return getattr(self._module, name) - - def __repr__(self): - return "<%s: %s of %s>" % (self.__class__.__name__, self._module, - self._nested_import) - - -def _add_error(evaluator, name, message=None): - if hasattr(name, 'parent'): - # Should be a name, not a string! - analysis.add(evaluator, 'import-error', name, message) - - -def get_init_path(directory_path): - """ - The __init__ file can be searched in a directory. If found return it, else - None. - """ - for suffix, _, _ in imp.get_suffixes(): - path = os.path.join(directory_path, '__init__' + suffix) - if os.path.exists(path): - return path - return None - - -class Importer(object): - def __init__(self, evaluator, import_path, module, level=0): - """ - An implementation similar to ``__import__``. Use `follow` - to actually follow the imports. - - *level* specifies whether to use absolute or relative imports. 0 (the - default) means only perform absolute imports. Positive values for level - indicate the number of parent directories to search relative to the - directory of the module calling ``__import__()`` (see PEP 328 for the - details). - - :param import_path: List of namespaces (strings or Names). - """ - debug.speed('import %s' % (import_path,)) - self._evaluator = evaluator - self.level = level - self.module = module - try: - self.file_path = module.py__file__() - except AttributeError: - # Can be None for certain compiled modules like 'builtins'. - self.file_path = None - - if level: - base = module.py__package__().split('.') - if base == ['']: - base = [] - if level > len(base): - path = module.py__file__() - import_path = list(import_path) - for i in range(level): - path = os.path.dirname(path) - dir_name = os.path.basename(path) - # This is not the proper way to do relative imports. However, since - # Jedi cannot be sure about the entry point, we just calculate an - # absolute path here. - if dir_name: - import_path.insert(0, dir_name) - else: - _add_error(self._evaluator, import_path[-1]) - import_path = [] - # TODO add import error. - debug.warning('Attempted relative import beyond top-level package.') - else: - # Here we basically rewrite the level to 0. - import_path = tuple(base) + import_path - self.import_path = import_path - - @property - def str_import_path(self): - """Returns the import path as pure strings instead of `Name`.""" - return tuple(str(name) for name in self.import_path) - - @memoize_default() - def sys_path_with_modifications(self): - in_path = [] - sys_path_mod = list(sys_path.sys_path_with_modifications(self._evaluator, self.module)) - if self.file_path is not None: - # If you edit e.g. gunicorn, there will be imports like this: - # `from gunicorn import something`. But gunicorn is not in the - # sys.path. Therefore look if gunicorn is a parent directory, #56. - if self.import_path: # TODO is this check really needed? - for path in sys_path.traverse_parents(self.file_path): - if os.path.basename(path) == self.str_import_path[0]: - in_path.append(os.path.dirname(path)) - - # Since we know nothing about the call location of the sys.path, - # it's a possibility that the current directory is the origin of - # the Python execution. - sys_path_mod.insert(0, os.path.dirname(self.file_path)) - - return in_path + sys_path_mod - - @memoize_default(NO_DEFAULT) - def follow(self): - if not self.import_path: - return [] - return self._do_import(self.import_path, self.sys_path_with_modifications()) - - def _do_import(self, import_path, sys_path): - """ - This method is very similar to importlib's `_gcd_import`. - """ - import_parts = [str(i) for i in import_path] - - # Handle "magic" Flask extension imports: - # ``flask.ext.foo`` is really ``flask_foo`` or ``flaskext.foo``. - if len(import_path) > 2 and import_parts[:2] == ['flask', 'ext']: - # New style. - ipath = ('flask_' + str(import_parts[2]),) + import_path[3:] - modules = self._do_import(ipath, sys_path) - if modules: - return modules - else: - # Old style - return self._do_import(('flaskext',) + import_path[2:], sys_path) - - module_name = '.'.join(import_parts) - try: - return [self._evaluator.modules[module_name]] - except KeyError: - pass - - if len(import_path) > 1: - # This is a recursive way of importing that works great with - # the module cache. - bases = self._do_import(import_path[:-1], sys_path) - if not bases: - return [] - # We can take the first element, because only the os special - # case yields multiple modules, which is not important for - # further imports. - base = bases[0] - - # This is a huge exception, we follow a nested import - # ``os.path``, because it's a very important one in Python - # that is being achieved by messing with ``sys.modules`` in - # ``os``. - if [str(i) for i in import_path] == ['os', 'path']: - return self._evaluator.find_types(base, 'path') - - try: - # It's possible that by giving it always the sys path (and not - # the __path__ attribute of the parent, we get wrong results - # and nested namespace packages don't work. But I'm not sure. - paths = base.py__path__(sys_path) - except AttributeError: - # The module is not a package. - _add_error(self._evaluator, import_path[-1]) - return [] - else: - debug.dbg('search_module %s in paths %s', module_name, paths) - for path in paths: - # At the moment we are only using one path. So this is - # not important to be correct. - try: - module_file, module_path, is_pkg = \ - find_module(import_parts[-1], [path]) - break - except ImportError: - module_path = None - if module_path is None: - _add_error(self._evaluator, import_path[-1]) - return [] - else: - try: - debug.dbg('search_module %s in %s', import_parts[-1], self.file_path) - # Override the sys.path. It works only good that way. - # Injecting the path directly into `find_module` did not work. - sys.path, temp = sys_path, sys.path - try: - module_file, module_path, is_pkg = \ - find_module(import_parts[-1]) - finally: - sys.path = temp - except ImportError: - # The module is not a package. - _add_error(self._evaluator, import_path[-1]) - return [] - - source = None - if is_pkg: - # In this case, we don't have a file yet. Search for the - # __init__ file. - module_path = get_init_path(module_path) - elif module_file: - source = module_file.read() - module_file.close() - - if module_file is None and not module_path.endswith('.py'): - module = compiled.load_module(module_path) - else: - module = _load_module(self._evaluator, module_path, source, sys_path) - - self._evaluator.modules[module_name] = module - return [module] - - def _generate_name(self, name): - return helpers.FakeName(name, parent=self.module) - - def _get_module_names(self, search_path=None): - """ - Get the names of all modules in the search_path. This means file names - and not names defined in the files. - """ - - names = [] - # add builtin module names - if search_path is None: - names += [self._generate_name(name) for name in sys.builtin_module_names] - - if search_path is None: - search_path = self.sys_path_with_modifications() - for module_loader, name, is_pkg in pkgutil.iter_modules(search_path): - names.append(self._generate_name(name)) - return names - - def completion_names(self, evaluator, only_modules=False): - """ - :param only_modules: Indicates wheter it's possible to import a - definition that is not defined in a module. - """ - from jedi.evaluate import finder - names = [] - if self.import_path: - # flask - if self.str_import_path == ('flask', 'ext'): - # List Flask extensions like ``flask_foo`` - for mod in self._get_module_names(): - modname = str(mod) - if modname.startswith('flask_'): - extname = modname[len('flask_'):] - names.append(self._generate_name(extname)) - # Now the old style: ``flaskext.foo`` - for dir in self.sys_path_with_modifications(): - flaskext = os.path.join(dir, 'flaskext') - if os.path.isdir(flaskext): - names += self._get_module_names([flaskext]) - - for scope in self.follow(): - # Non-modules are not completable. - if not scope.type == 'file_input': # not a module - continue - - # namespace packages - if isinstance(scope, tree.Module) and scope.path.endswith('__init__.py'): - paths = scope.py__path__(self.sys_path_with_modifications()) - names += self._get_module_names(paths) - - if only_modules: - # In the case of an import like `from x.` we don't need to - # add all the variables. - if ('os',) == self.str_import_path and not self.level: - # os.path is a hardcoded exception, because it's a - # ``sys.modules`` modification. - names.append(self._generate_name('path')) - - continue - - for names_dict in scope.names_dicts(search_global=False): - _names = list(chain.from_iterable(names_dict.values())) - if not _names: - continue - _names = finder.filter_definition_names(_names, scope) - names += _names - else: - # Empty import path=completion after import - if not self.level: - names += self._get_module_names() - - if self.file_path is not None: - path = os.path.abspath(self.file_path) - for i in range(self.level - 1): - path = os.path.dirname(path) - names += self._get_module_names([path]) - - return names - - -def _load_module(evaluator, path=None, source=None, sys_path=None): - def load(source): - dotted_path = path and compiled.dotted_from_fs_path(path, sys_path) - if path is not None and path.endswith('.py') \ - and not dotted_path in settings.auto_import_modules: - if source is None: - with open(path, 'rb') as f: - source = f.read() - else: - return compiled.load_module(path) - p = path - p = fast.FastParser(evaluator.grammar, common.source_to_unicode(source), p) - cache.save_parser(path, p) - return p.module - - cached = cache.load_parser(path) - module = load(source) if cached is None else cached.module - module = evaluator.wrap(module) - return module - - -def add_module(evaluator, module_name, module): - if '.' not in module_name: - # We cannot add paths with dots, because that would collide with - # the sepatator dots for nested packages. Therefore we return - # `__main__` in ModuleWrapper.py__name__(), which is similar to - # Python behavior. - evaluator.modules[module_name] = module - - -def get_modules_containing_name(evaluator, mods, name): - """ - Search a name in the directories of modules. - """ - def check_python_file(path): - try: - return cache.parser_cache[path].parser.module - except KeyError: - try: - return check_fs(path) - except IOError: - return None - - def check_fs(path): - with open(path, 'rb') as f: - source = source_to_unicode(f.read()) - if name in source: - module_name = os.path.basename(path)[:-3] # Remove `.py`. - module = _load_module(evaluator, path, source) - add_module(evaluator, module_name, module) - return module - - # skip non python modules - mods = set(m for m in mods if not isinstance(m, compiled.CompiledObject)) - mod_paths = set() - for m in mods: - mod_paths.add(m.path) - yield m - - if settings.dynamic_params_for_other_modules: - paths = set(settings.additional_dynamic_modules) - for p in mod_paths: - if p is not None: - d = os.path.dirname(p) - for entry in os.listdir(d): - if entry not in mod_paths: - if entry.endswith('.py'): - paths.add(d + os.path.sep + entry) - - for p in sorted(paths): - # make testing easier, sort it - same results on every interpreter - c = check_python_file(p) - if c is not None and c not in mods and not isinstance(c, compiled.CompiledObject): - yield c diff --git a/pythonFiles/jedi/evaluate/iterable.py b/pythonFiles/jedi/evaluate/iterable.py deleted file mode 100644 index d14a4c5fd2fb..000000000000 --- a/pythonFiles/jedi/evaluate/iterable.py +++ /dev/null @@ -1,631 +0,0 @@ -""" -Contains all classes and functions to deal with lists, dicts, generators and -iterators in general. - -Array modifications -******************* - -If the content of an array (``set``/``list``) is requested somewhere, the -current module will be checked for appearances of ``arr.append``, -``arr.insert``, etc. If the ``arr`` name points to an actual array, the -content will be added - -This can be really cpu intensive, as you can imagine. Because |jedi| has to -follow **every** ``append`` and check wheter it's the right array. However this -works pretty good, because in *slow* cases, the recursion detector and other -settings will stop this process. - -It is important to note that: - -1. Array modfications work only in the current module. -2. Jedi only checks Array additions; ``list.pop``, etc are ignored. -""" -from itertools import chain - -from jedi import common -from jedi import debug -from jedi import settings -from jedi._compatibility import use_metaclass, is_py3, unicode -from jedi.parser import tree -from jedi.evaluate import compiled -from jedi.evaluate import helpers -from jedi.evaluate.cache import CachedMetaClass, memoize_default -from jedi.evaluate import analysis - - -def unite(iterable): - """Turns a two dimensional array into a one dimensional.""" - return list(chain.from_iterable(iterable)) - - -class IterableWrapper(tree.Base): - def is_class(self): - return False - - -class GeneratorMixin(object): - @memoize_default() - def names_dicts(self, search_global=False): # is always False - dct = {} - executes_generator = '__next__', 'send', 'next' - for names in compiled.generator_obj.names_dict.values(): - for name in names: - if name.value in executes_generator: - parent = GeneratorMethod(self, name.parent) - dct[name.value] = [helpers.FakeName(name.name, parent, is_definition=True)] - else: - dct[name.value] = [name] - yield dct - - def get_index_types(self, evaluator, index_array): - #debug.warning('Tried to get array access on a generator: %s', self) - analysis.add(self._evaluator, 'type-error-generator', index_array) - return [] - - def get_exact_index_types(self, index): - """ - Exact lookups are used for tuple lookups, which are perfectly fine if - used with generators. - """ - return [self.iter_content()[index]] - - def py__bool__(self): - return True - - -class Generator(use_metaclass(CachedMetaClass, IterableWrapper, GeneratorMixin)): - """Handling of `yield` functions.""" - def __init__(self, evaluator, func, var_args): - super(Generator, self).__init__() - self._evaluator = evaluator - self.func = func - self.var_args = var_args - - def iter_content(self): - """ returns the content of __iter__ """ - # Directly execute it, because with a normal call to py__call__ a - # Generator will be returned. - from jedi.evaluate.representation import FunctionExecution - f = FunctionExecution(self._evaluator, self.func, self.var_args) - return f.get_return_types(check_yields=True) - - def __getattr__(self, name): - if name not in ['start_pos', 'end_pos', 'parent', 'get_imports', - 'doc', 'docstr', 'get_parent_until', - 'get_code', 'subscopes']: - raise AttributeError("Accessing %s of %s is not allowed." - % (self, name)) - return getattr(self.func, name) - - def __repr__(self): - return "<%s of %s>" % (type(self).__name__, self.func) - - -class GeneratorMethod(IterableWrapper): - """``__next__`` and ``send`` methods.""" - def __init__(self, generator, builtin_func): - self._builtin_func = builtin_func - self._generator = generator - - def py__call__(self, evaluator, params): - # TODO add TypeError if params are given. - return self._generator.iter_content() - - def __getattr__(self, name): - return getattr(self._builtin_func, name) - - -class Comprehension(IterableWrapper): - @staticmethod - def from_atom(evaluator, atom): - mapping = { - '(': GeneratorComprehension, - '[': ListComprehension - } - return mapping[atom.children[0]](evaluator, atom) - - def __init__(self, evaluator, atom): - self._evaluator = evaluator - self._atom = atom - - @memoize_default() - def eval_node(self): - """ - The first part `x + 1` of the list comprehension: - - [x + 1 for x in foo] - """ - comprehension = self._atom.children[1] - # For nested comprehensions we need to search the last one. - last = comprehension.children[-1] - last_comp = comprehension.children[1] - while True: - if isinstance(last, tree.CompFor): - last_comp = last - elif not tree.is_node(last, 'comp_if'): - break - last = last.children[-1] - - return helpers.deep_ast_copy(comprehension.children[0], parent=last_comp) - - def get_exact_index_types(self, index): - return [self._evaluator.eval_element(self.eval_node())[index]] - - def __repr__(self): - return "" % (type(self).__name__, self._atom) - - -class ArrayMixin(object): - @memoize_default() - def names_dicts(self, search_global=False): # Always False. - # `array.type` is a string with the type, e.g. 'list'. - scope = self._evaluator.find_types(compiled.builtin, self.type)[0] - # builtins only have one class -> [0] - scope = self._evaluator.execute(scope, (AlreadyEvaluated((self,)),))[0] - return scope.names_dicts(search_global) - - def py__bool__(self): - return None # We don't know the length, because of appends. - - -class ListComprehension(Comprehension, ArrayMixin): - type = 'list' - - def get_index_types(self, evaluator, index): - return self.iter_content() - - def iter_content(self): - return self._evaluator.eval_element(self.eval_node()) - - @property - def name(self): - return FakeSequence(self._evaluator, [], 'list').name - - -class GeneratorComprehension(Comprehension, GeneratorMixin): - def iter_content(self): - return self._evaluator.eval_element(self.eval_node()) - - -class Array(IterableWrapper, ArrayMixin): - mapping = {'(': 'tuple', - '[': 'list', - '{': 'dict'} - - def __init__(self, evaluator, atom): - self._evaluator = evaluator - self.atom = atom - self.type = Array.mapping[atom.children[0]] - """The builtin name of the array (list, set, tuple or dict).""" - - c = self.atom.children - array_node = c[1] - if self.type == 'dict' and array_node != '}' \ - and (not hasattr(array_node, 'children') - or ':' not in array_node.children): - self.type = 'set' - - @property - def name(self): - return helpers.FakeName(self.type, parent=self) - - @memoize_default() - def get_index_types(self, evaluator, index=()): - """ - Get the types of a specific index or all, if not given. - - :param index: A subscriptlist node (or subnode). - """ - indexes = create_indexes_or_slices(evaluator, index) - lookup_done = False - types = [] - for index in indexes: - if isinstance(index, Slice): - types += [self] - lookup_done = True - elif isinstance(index, compiled.CompiledObject) \ - and isinstance(index.obj, (int, str, unicode)): - with common.ignored(KeyError, IndexError, TypeError): - types += self.get_exact_index_types(index.obj) - lookup_done = True - - return types if lookup_done else self.values() - - @memoize_default() - def values(self): - result = unite(self._evaluator.eval_element(v) for v in self._values()) - result += check_array_additions(self._evaluator, self) - return result - - def get_exact_index_types(self, mixed_index): - """ Here the index is an int/str. Raises IndexError/KeyError """ - if self.type == 'dict': - for key, values in self._items(): - # Because we only want the key to be a string. - keys = self._evaluator.eval_element(key) - - for k in keys: - if isinstance(k, compiled.CompiledObject) \ - and mixed_index == k.obj: - for value in values: - return self._evaluator.eval_element(value) - raise KeyError('No key found in dictionary %s.' % self) - - # Can raise an IndexError - return self._evaluator.eval_element(self._items()[mixed_index]) - - def iter_content(self): - return self.values() - - @common.safe_property - def parent(self): - return compiled.builtin - - def get_parent_until(self): - return compiled.builtin - - def __getattr__(self, name): - if name not in ['start_pos', 'get_only_subelement', 'parent', - 'get_parent_until', 'items']: - raise AttributeError('Strange access on %s: %s.' % (self, name)) - return getattr(self.atom, name) - - def _values(self): - """Returns a list of a list of node.""" - if self.type == 'dict': - return list(chain.from_iterable(v for k, v in self._items())) - else: - return self._items() - - def _items(self): - c = self.atom.children - array_node = c[1] - if array_node in (']', '}', ')'): - return [] # Direct closing bracket, doesn't contain items. - - if tree.is_node(array_node, 'testlist_comp'): - return array_node.children[::2] - elif tree.is_node(array_node, 'dictorsetmaker'): - kv = [] - iterator = iter(array_node.children) - for key in iterator: - op = next(iterator, None) - if op is None or op == ',': - kv.append(key) # A set. - elif op == ':': # A dict. - kv.append((key, [next(iterator)])) - next(iterator, None) # Possible comma. - else: - raise NotImplementedError('dict/set comprehensions') - return kv - else: - return [array_node] - - def __iter__(self): - return iter(self._items()) - - def __repr__(self): - return "<%s of %s>" % (type(self).__name__, self.atom) - - -class _FakeArray(Array): - def __init__(self, evaluator, container, type): - self.type = type - self._evaluator = evaluator - self.atom = container - - -class ImplicitTuple(_FakeArray): - def __init__(self, evaluator, testlist): - super(ImplicitTuple, self).__init__(evaluator, testlist, 'tuple') - self._testlist = testlist - - def _items(self): - return self._testlist.children[::2] - - -class FakeSequence(_FakeArray): - def __init__(self, evaluator, sequence_values, type): - super(FakeSequence, self).__init__(evaluator, sequence_values, type) - self._sequence_values = sequence_values - - def _items(self): - return self._sequence_values - - def get_exact_index_types(self, index): - value = self._sequence_values[index] - return self._evaluator.eval_element(value) - - -class AlreadyEvaluated(frozenset): - """A simple container to add already evaluated objects to an array.""" - def get_code(self): - # For debugging purposes. - return str(self) - - -class MergedNodes(frozenset): - pass - - -class FakeDict(_FakeArray): - def __init__(self, evaluator, dct): - super(FakeDict, self).__init__(evaluator, dct, 'dict') - self._dct = dct - - def get_exact_index_types(self, index): - return list(chain.from_iterable(self._evaluator.eval_element(v) - for v in self._dct[index])) - - def _items(self): - return self._dct.items() - - -class MergedArray(_FakeArray): - def __init__(self, evaluator, arrays): - super(MergedArray, self).__init__(evaluator, arrays, arrays[-1].type) - self._arrays = arrays - - def get_exact_index_types(self, mixed_index): - raise IndexError - - def values(self): - return list(chain(*(a.values() for a in self._arrays))) - - def __iter__(self): - for array in self._arrays: - for a in array: - yield a - - def __len__(self): - return sum(len(a) for a in self._arrays) - - -def get_iterator_types(inputs): - """Returns the types of any iterator (arrays, yields, __iter__, etc).""" - iterators = [] - # Take the first statement (for has always only - # one, remember `in`). And follow it. - for it in inputs: - if isinstance(it, (Generator, Array, ArrayInstance, Comprehension)): - iterators.append(it) - else: - if not hasattr(it, 'execute_subscope_by_name'): - debug.warning('iterator/for loop input wrong: %s', it) - continue - try: - iterators += it.execute_subscope_by_name('__iter__') - except KeyError: - debug.warning('iterators: No __iter__ method found.') - - result = [] - from jedi.evaluate.representation import Instance - for it in iterators: - if isinstance(it, Array): - # Array is a little bit special, since this is an internal array, - # but there's also the list builtin, which is another thing. - result += it.values() - elif isinstance(it, Instance): - # __iter__ returned an instance. - name = '__next__' if is_py3 else 'next' - try: - result += it.execute_subscope_by_name(name) - except KeyError: - debug.warning('Instance has no __next__ function in %s.', it) - else: - # TODO this is not correct, __iter__ can return arbitrary input! - # Is a generator. - result += it.iter_content() - return result - - -def check_array_additions(evaluator, array): - """ Just a mapper function for the internal _check_array_additions """ - if array.type not in ('list', 'set'): - # TODO also check for dict updates - return [] - - is_list = array.type == 'list' - try: - current_module = array.atom.get_parent_until() - except AttributeError: - # If there's no get_parent_until, it's a FakeSequence or another Fake - # type. Those fake types are used inside Jedi's engine. No values may - # be added to those after their creation. - return [] - return _check_array_additions(evaluator, array, current_module, is_list) - - -@memoize_default([], evaluator_is_first_arg=True) -def _check_array_additions(evaluator, compare_array, module, is_list): - """ - Checks if a `Array` has "add" (append, insert, extend) statements: - - >>> a = [""] - >>> a.append(1) - """ - if not settings.dynamic_array_additions or isinstance(module, compiled.CompiledObject): - return [] - - def check_additions(arglist, add_name): - params = list(param.Arguments(evaluator, arglist).unpack()) - result = [] - if add_name in ['insert']: - params = params[1:] - if add_name in ['append', 'add', 'insert']: - for key, nodes in params: - result += unite(evaluator.eval_element(node) for node in nodes) - elif add_name in ['extend', 'update']: - for key, nodes in params: - iterators = unite(evaluator.eval_element(node) for node in nodes) - result += get_iterator_types(iterators) - return result - - from jedi.evaluate import representation as er, param - - def get_execution_parent(element): - """ Used to get an Instance/FunctionExecution parent """ - if isinstance(element, Array): - node = element.atom - else: - # Is an Instance with an - # Arguments([AlreadyEvaluated([ArrayInstance])]) inside - # Yeah... I know... It's complicated ;-) - node = list(element.var_args.argument_node[0])[0].var_args.trailer - if isinstance(node, er.InstanceElement): - return node - return node.get_parent_until(er.FunctionExecution) - - temp_param_add, settings.dynamic_params_for_other_modules = \ - settings.dynamic_params_for_other_modules, False - - search_names = ['append', 'extend', 'insert'] if is_list else ['add', 'update'] - comp_arr_parent = get_execution_parent(compare_array) - - added_types = [] - for add_name in search_names: - try: - possible_names = module.used_names[add_name] - except KeyError: - continue - else: - for name in possible_names: - # Check if the original scope is an execution. If it is, one - # can search for the same statement, that is in the module - # dict. Executions are somewhat special in jedi, since they - # literally copy the contents of a function. - if isinstance(comp_arr_parent, er.FunctionExecution): - if comp_arr_parent.start_pos < name.start_pos < comp_arr_parent.end_pos: - name = comp_arr_parent.name_for_position(name.start_pos) - else: - # Don't check definitions that are not defined in the - # same function. This is not "proper" anyway. It also - # improves Jedi's speed for array lookups, since we - # don't have to check the whole source tree anymore. - continue - trailer = name.parent - power = trailer.parent - trailer_pos = power.children.index(trailer) - try: - execution_trailer = power.children[trailer_pos + 1] - except IndexError: - continue - else: - if execution_trailer.type != 'trailer' \ - or execution_trailer.children[0] != '(' \ - or execution_trailer.children[1] == ')': - continue - power = helpers.call_of_name(name, cut_own_trailer=True) - # InstanceElements are special, because they don't get copied, - # but have this wrapper around them. - if isinstance(comp_arr_parent, er.InstanceElement): - power = er.get_instance_el(evaluator, comp_arr_parent.instance, power) - - if evaluator.recursion_detector.push_stmt(power): - # Check for recursion. Possible by using 'extend' in - # combination with function calls. - continue - if compare_array in evaluator.eval_element(power): - # The arrays match. Now add the results - added_types += check_additions(execution_trailer.children[1], add_name) - - evaluator.recursion_detector.pop_stmt() - # reset settings - settings.dynamic_params_for_other_modules = temp_param_add - return added_types - - -def check_array_instances(evaluator, instance): - """Used for set() and list() instances.""" - if not settings.dynamic_array_additions: - return instance.var_args - - ai = ArrayInstance(evaluator, instance) - from jedi.evaluate import param - return param.Arguments(evaluator, [AlreadyEvaluated([ai])]) - - -class ArrayInstance(IterableWrapper): - """ - Used for the usage of set() and list(). - This is definitely a hack, but a good one :-) - It makes it possible to use set/list conversions. - - In contrast to Array, ListComprehension and all other iterable types, this - is something that is only used inside `evaluate/compiled/fake/builtins.py` - and therefore doesn't need `names_dicts`, `py__bool__` and so on, because - we don't use these operations in `builtins.py`. - """ - def __init__(self, evaluator, instance): - self._evaluator = evaluator - self.instance = instance - self.var_args = instance.var_args - - def iter_content(self): - """ - The index is here just ignored, because of all the appends, etc. - lists/sets are too complicated too handle that. - """ - items = [] - for key, nodes in self.var_args.unpack(): - for node in nodes: - for typ in self._evaluator.eval_element(node): - items += get_iterator_types([typ]) - - module = self.var_args.get_parent_until() - is_list = str(self.instance.name) == 'list' - items += _check_array_additions(self._evaluator, self.instance, module, is_list) - return items - - -class Slice(object): - def __init__(self, evaluator, start, stop, step): - self._evaluator = evaluator - # all of them are either a Precedence or None. - self._start = start - self._stop = stop - self._step = step - - @property - def obj(self): - """ - Imitate CompiledObject.obj behavior and return a ``builtin.slice()`` - object. - """ - def get(element): - if element is None: - return None - - result = self._evaluator.eval_element(element) - if len(result) != 1: - # We want slices to be clear defined with just one type. - # Otherwise we will return an empty slice object. - raise IndexError - try: - return result[0].obj - except AttributeError: - return None - - try: - return slice(get(self._start), get(self._stop), get(self._step)) - except IndexError: - return slice(None, None, None) - - -def create_indexes_or_slices(evaluator, index): - if tree.is_node(index, 'subscript'): # subscript is a slice operation. - start, stop, step = None, None, None - result = [] - for el in index.children: - if el == ':': - if not result: - result.append(None) - elif tree.is_node(el, 'sliceop'): - if len(el.children) == 2: - result.append(el.children[1]) - else: - result.append(el) - result += [None] * (3 - len(result)) - - return (Slice(evaluator, *result),) - return evaluator.eval_element(index) diff --git a/pythonFiles/jedi/evaluate/param.py b/pythonFiles/jedi/evaluate/param.py deleted file mode 100644 index 8524bf958582..000000000000 --- a/pythonFiles/jedi/evaluate/param.py +++ /dev/null @@ -1,403 +0,0 @@ -from collections import defaultdict -from itertools import chain - -from jedi._compatibility import unicode, zip_longest -from jedi import debug -from jedi import common -from jedi.parser import tree -from jedi.evaluate import iterable -from jedi.evaluate import analysis -from jedi.evaluate import precedence -from jedi.evaluate.helpers import FakeName -from jedi.cache import underscore_memoization - - -class Arguments(tree.Base): - def __init__(self, evaluator, argument_node, trailer=None): - """ - The argument_node is either a parser node or a list of evaluated - objects. Those evaluated objects may be lists of evaluated objects - themselves (one list for the first argument, one for the second, etc). - - :param argument_node: May be an argument_node or a list of nodes. - """ - self.argument_node = argument_node - self._evaluator = evaluator - self.trailer = trailer # Can be None, e.g. in a class definition. - - def _split(self): - if isinstance(self.argument_node, (tuple, list)): - for el in self.argument_node: - yield 0, el - else: - if not tree.is_node(self.argument_node, 'arglist'): - yield 0, self.argument_node - return - - iterator = iter(self.argument_node.children) - for child in iterator: - if child == ',': - continue - elif child in ('*', '**'): - yield len(child.value), next(iterator) - else: - yield 0, child - - def get_parent_until(self, *args, **kwargs): - if self.trailer is None: - try: - element = self.argument_node[0] - from jedi.evaluate.iterable import AlreadyEvaluated - if isinstance(element, AlreadyEvaluated): - element = self._evaluator.eval_element(element)[0] - except IndexError: - return None - else: - return element.get_parent_until(*args, **kwargs) - else: - return self.trailer.get_parent_until(*args, **kwargs) - - def as_tuple(self): - for stars, argument in self._split(): - if tree.is_node(argument, 'argument'): - argument, default = argument.children[::2] - else: - default = None - yield argument, default, stars - - def unpack(self, func=None): - named_args = [] - for stars, el in self._split(): - if stars == 1: - arrays = self._evaluator.eval_element(el) - iterators = [_iterate_star_args(self._evaluator, a, el, func) - for a in arrays] - iterators = list(iterators) - for values in list(zip_longest(*iterators)): - yield None, [v for v in values if v is not None] - elif stars == 2: - arrays = self._evaluator.eval_element(el) - dicts = [_star_star_dict(self._evaluator, a, el, func) - for a in arrays] - for dct in dicts: - for key, values in dct.items(): - yield key, values - else: - if tree.is_node(el, 'argument'): - c = el.children - if len(c) == 3: # Keyword argument. - named_args.append((c[0].value, (c[2],))) - else: # Generator comprehension. - # Include the brackets with the parent. - comp = iterable.GeneratorComprehension( - self._evaluator, self.argument_node.parent) - yield None, (iterable.AlreadyEvaluated([comp]),) - elif isinstance(el, (list, tuple)): - yield None, el - else: - yield None, (el,) - - # Reordering var_args is necessary, because star args sometimes appear - # after named argument, but in the actual order it's prepended. - for key_arg in named_args: - yield key_arg - - def _reorder_var_args(var_args): - named_index = None - new_args = [] - for i, stmt in enumerate(var_args): - if isinstance(stmt, tree.ExprStmt): - if named_index is None and stmt.assignment_details: - named_index = i - - if named_index is not None: - expression_list = stmt.expression_list() - if expression_list and expression_list[0] == '*': - new_args.insert(named_index, stmt) - named_index += 1 - continue - - new_args.append(stmt) - return new_args - - def eval_argument_clinic(self, arguments): - """Uses a list with argument clinic information (see PEP 436).""" - iterator = self.unpack() - for i, (name, optional, allow_kwargs) in enumerate(arguments): - key, va_values = next(iterator, (None, [])) - if key is not None: - raise NotImplementedError - if not va_values and not optional: - debug.warning('TypeError: %s expected at least %s arguments, got %s', - name, len(arguments), i) - raise ValueError - values = list(chain.from_iterable(self._evaluator.eval_element(el) - for el in va_values)) - if not values and not optional: - # For the stdlib we always want values. If we don't get them, - # that's ok, maybe something is too hard to resolve, however, - # we will not proceed with the evaluation of that function. - debug.warning('argument_clinic "%s" not resolvable.', name) - raise ValueError - yield values - - def scope(self): - # Returns the scope in which the arguments are used. - return (self.trailer or self.argument_node).get_parent_until(tree.IsScope) - - def eval_args(self): - # TODO this method doesn't work with named args and a lot of other - # things. Use unpack. - return [self._evaluator.eval_element(el) for stars, el in self._split()] - - def __repr__(self): - return '<%s: %s>' % (type(self).__name__, self.argument_node) - - def get_calling_var_args(self): - if tree.is_node(self.argument_node, 'arglist', 'argument') \ - or self.argument_node == () and self.trailer is not None: - return _get_calling_var_args(self._evaluator, self) - else: - return None - - -class ExecutedParam(tree.Param): - """Fake a param and give it values.""" - def __init__(self, original_param, var_args, values): - self._original_param = original_param - self.var_args = var_args - self._values = values - - def eval(self, evaluator): - types = [] - for v in self._values: - types += evaluator.eval_element(v) - return types - - @property - def position_nr(self): - # Need to use the original logic here, because it uses the parent. - return self._original_param.position_nr - - @property - @underscore_memoization - def name(self): - return FakeName(str(self._original_param.name), self, self.start_pos) - - def __getattr__(self, name): - return getattr(self._original_param, name) - - -def _get_calling_var_args(evaluator, var_args): - old_var_args = None - while var_args != old_var_args: - old_var_args = var_args - for name, default, stars in reversed(list(var_args.as_tuple())): - if not stars or not isinstance(name, tree.Name): - continue - - names = evaluator.goto(name) - if len(names) != 1: - break - param = names[0].get_definition() - if not isinstance(param, ExecutedParam): - if isinstance(param, tree.Param): - # There is no calling var_args in this case - there's just - # a param without any input. - return None - break - # We never want var_args to be a tuple. This should be enough for - # now, we can change it later, if we need to. - if isinstance(param.var_args, Arguments): - var_args = param.var_args - return var_args.argument_node or var_args.trailer - - -def get_params(evaluator, func, var_args): - param_names = [] - param_dict = {} - for param in func.params: - param_dict[str(param.name)] = param - unpacked_va = list(var_args.unpack(func)) - from jedi.evaluate.representation import InstanceElement - if isinstance(func, InstanceElement): - # Include self at this place. - unpacked_va.insert(0, (None, [iterable.AlreadyEvaluated([func.instance])])) - var_arg_iterator = common.PushBackIterator(iter(unpacked_va)) - - non_matching_keys = defaultdict(lambda: []) - keys_used = {} - keys_only = False - had_multiple_value_error = False - for param in func.params: - # The value and key can both be null. There, the defaults apply. - # args / kwargs will just be empty arrays / dicts, respectively. - # Wrong value count is just ignored. If you try to test cases that are - # not allowed in Python, Jedi will maybe not show any completions. - default = [] if param.default is None else [param.default] - key, va_values = next(var_arg_iterator, (None, default)) - while key is not None: - keys_only = True - k = unicode(key) - try: - key_param = param_dict[unicode(key)] - except KeyError: - non_matching_keys[key] += va_values - else: - param_names.append(ExecutedParam(key_param, var_args, va_values).name) - - if k in keys_used: - had_multiple_value_error = True - m = ("TypeError: %s() got multiple values for keyword argument '%s'." - % (func.name, k)) - calling_va = _get_calling_var_args(evaluator, var_args) - if calling_va is not None: - analysis.add(evaluator, 'type-error-multiple-values', - calling_va, message=m) - else: - try: - keys_used[k] = param_names[-1] - except IndexError: - # TODO this is wrong stupid and whatever. - pass - key, va_values = next(var_arg_iterator, (None, ())) - - values = [] - if param.stars == 1: - # *args param - lst_values = [iterable.MergedNodes(va_values)] if va_values else [] - for key, va_values in var_arg_iterator: - # Iterate until a key argument is found. - if key: - var_arg_iterator.push_back((key, va_values)) - break - if va_values: - lst_values.append(iterable.MergedNodes(va_values)) - seq = iterable.FakeSequence(evaluator, lst_values, 'tuple') - values = [iterable.AlreadyEvaluated([seq])] - elif param.stars == 2: - # **kwargs param - dct = iterable.FakeDict(evaluator, dict(non_matching_keys)) - values = [iterable.AlreadyEvaluated([dct])] - non_matching_keys = {} - else: - # normal param - if va_values: - values = va_values - else: - # No value: Return an empty container - values = [] - if not keys_only: - calling_va = var_args.get_calling_var_args() - if calling_va is not None: - m = _error_argument_count(func, len(unpacked_va)) - analysis.add(evaluator, 'type-error-too-few-arguments', - calling_va, message=m) - - # Now add to result if it's not one of the previously covered cases. - if (not keys_only or param.stars == 2): - param_names.append(ExecutedParam(param, var_args, values).name) - keys_used[unicode(param.name)] = param_names[-1] - - if keys_only: - # All arguments should be handed over to the next function. It's not - # about the values inside, it's about the names. Jedi needs to now that - # there's nothing to find for certain names. - for k in set(param_dict) - set(keys_used): - param = param_dict[k] - values = [] if param.default is None else [param.default] - param_names.append(ExecutedParam(param, var_args, values).name) - - if not (non_matching_keys or had_multiple_value_error - or param.stars or param.default): - # add a warning only if there's not another one. - calling_va = _get_calling_var_args(evaluator, var_args) - if calling_va is not None: - m = _error_argument_count(func, len(unpacked_va)) - analysis.add(evaluator, 'type-error-too-few-arguments', - calling_va, message=m) - - for key, va_values in non_matching_keys.items(): - m = "TypeError: %s() got an unexpected keyword argument '%s'." \ - % (func.name, key) - for value in va_values: - analysis.add(evaluator, 'type-error-keyword-argument', value.parent, message=m) - - remaining_params = list(var_arg_iterator) - if remaining_params: - m = _error_argument_count(func, len(unpacked_va)) - # Just report an error for the first param that is not needed (like - # cPython). - first_key, first_values = remaining_params[0] - for v in first_values: - if first_key is not None: - # Is a keyword argument, return the whole thing instead of just - # the value node. - v = v.parent - try: - non_kw_param = keys_used[first_key] - except KeyError: - pass - else: - origin_args = non_kw_param.parent.var_args.argument_node - # TODO calculate the var_args tree and check if it's in - # the tree (if not continue). - # print('\t\tnonkw', non_kw_param.parent.var_args.argument_node, ) - if origin_args not in [f.parent.parent for f in first_values]: - continue - analysis.add(evaluator, 'type-error-too-many-arguments', - v, message=m) - return param_names - - -def _iterate_star_args(evaluator, array, input_node, func=None): - from jedi.evaluate.representation import Instance - if isinstance(array, iterable.Array): - for field_stmt in array: # yield from plz! - yield field_stmt - elif isinstance(array, iterable.Generator): - for field_stmt in array.iter_content(): - yield iterable.AlreadyEvaluated([field_stmt]) - elif isinstance(array, Instance) and array.name.get_code() == 'tuple': - debug.warning('Ignored a tuple *args input %s' % array) - else: - if func is not None: - m = "TypeError: %s() argument after * must be a sequence, not %s" \ - % (func.name.value, array) - analysis.add(evaluator, 'type-error-star', input_node, message=m) - - -def _star_star_dict(evaluator, array, input_node, func): - dct = defaultdict(lambda: []) - from jedi.evaluate.representation import Instance - if isinstance(array, Instance) and array.name.get_code() == 'dict': - # For now ignore this case. In the future add proper iterators and just - # make one call without crazy isinstance checks. - return {} - - if isinstance(array, iterable.FakeDict): - return array._dct - elif isinstance(array, iterable.Array) and array.type == 'dict': - # TODO bad call to non-public API - for key_node, values in array._items(): - for key in evaluator.eval_element(key_node): - if precedence.is_string(key): - dct[key.obj] += values - - else: - if func is not None: - m = "TypeError: %s argument after ** must be a mapping, not %s" \ - % (func.name.value, array) - analysis.add(evaluator, 'type-error-star-star', input_node, message=m) - return dict(dct) - - -def _error_argument_count(func, actual_count): - default_arguments = sum(1 for p in func.params if p.default or p.stars) - - if default_arguments == 0: - before = 'exactly ' - else: - before = 'from %s to ' % (len(func.params) - default_arguments) - return ('TypeError: %s() takes %s%s arguments (%s given).' - % (func.name, before, len(func.params), actual_count)) diff --git a/pythonFiles/jedi/evaluate/precedence.py b/pythonFiles/jedi/evaluate/precedence.py deleted file mode 100644 index 7a2ee6d2763b..000000000000 --- a/pythonFiles/jedi/evaluate/precedence.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -Handles operator precedence. -""" -import operator - -from jedi._compatibility import unicode -from jedi.parser import tree -from jedi import debug -from jedi.evaluate.compiled import (CompiledObject, create, builtin, - keyword_from_value, true_obj, false_obj) -from jedi.evaluate import analysis - -# Maps Python syntax to the operator module. -COMPARISON_OPERATORS = { - '==': operator.eq, - '!=': operator.ne, - 'is': operator.is_, - 'is not': operator.is_not, - '<': operator.lt, - '<=': operator.le, - '>': operator.gt, - '>=': operator.ge, -} - - -def _literals_to_types(evaluator, result): - # Changes literals ('a', 1, 1.0, etc) to its type instances (str(), - # int(), float(), etc). - for i, r in enumerate(result): - if is_literal(r): - # Literals are only valid as long as the operations are - # correct. Otherwise add a value-free instance. - cls = builtin.get_by_name(r.name.get_code()) - result[i] = evaluator.execute(cls)[0] - return list(set(result)) - - -def calculate_children(evaluator, children): - """ - Calculate a list of children with operators. - """ - iterator = iter(children) - types = evaluator.eval_element(next(iterator)) - for operator in iterator: - right = next(iterator) - if tree.is_node(operator, 'comp_op'): # not in / is not - operator = ' '.join(str(c.value) for c in operator.children) - - # handle lazy evaluation of and/or here. - if operator in ('and', 'or'): - left_bools = set([left.py__bool__() for left in types]) - if left_bools == set([True]): - if operator == 'and': - types = evaluator.eval_element(right) - elif left_bools == set([False]): - if operator != 'and': - types = evaluator.eval_element(right) - # Otherwise continue, because of uncertainty. - else: - types = calculate(evaluator, types, operator, - evaluator.eval_element(right)) - debug.dbg('calculate_children types %s', types) - return types - - -def calculate(evaluator, left_result, operator, right_result): - result = [] - if not left_result or not right_result: - # illegal slices e.g. cause left/right_result to be None - result = (left_result or []) + (right_result or []) - result = _literals_to_types(evaluator, result) - else: - # I don't think there's a reasonable chance that a string - # operation is still correct, once we pass something like six - # objects. - if len(left_result) * len(right_result) > 6: - result = _literals_to_types(evaluator, left_result + right_result) - else: - for left in left_result: - for right in right_result: - result += _element_calculate(evaluator, left, operator, right) - return result - - -def factor_calculate(evaluator, types, operator): - """ - Calculates `+`, `-`, `~` and `not` prefixes. - """ - for typ in types: - if operator == '-': - if _is_number(typ): - yield create(evaluator, -typ.obj) - elif operator == 'not': - value = typ.py__bool__() - if value is None: # Uncertainty. - return - yield keyword_from_value(not value) - else: - yield typ - - -def _is_number(obj): - return isinstance(obj, CompiledObject) \ - and isinstance(obj.obj, (int, float)) - - -def is_string(obj): - return isinstance(obj, CompiledObject) \ - and isinstance(obj.obj, (str, unicode)) - - -def is_literal(obj): - return _is_number(obj) or is_string(obj) - - -def _is_tuple(obj): - from jedi.evaluate import iterable - return isinstance(obj, iterable.Array) and obj.type == 'tuple' - - -def _is_list(obj): - from jedi.evaluate import iterable - return isinstance(obj, iterable.Array) and obj.type == 'list' - - -def _element_calculate(evaluator, left, operator, right): - from jedi.evaluate import iterable, representation as er - l_is_num = _is_number(left) - r_is_num = _is_number(right) - if operator == '*': - # for iterables, ignore * operations - if isinstance(left, iterable.Array) or is_string(left): - return [left] - elif isinstance(right, iterable.Array) or is_string(right): - return [right] - elif operator == '+': - if l_is_num and r_is_num or is_string(left) and is_string(right): - return [create(evaluator, left.obj + right.obj)] - elif _is_tuple(left) and _is_tuple(right) or _is_list(left) and _is_list(right): - return [iterable.MergedArray(evaluator, (left, right))] - elif operator == '-': - if l_is_num and r_is_num: - return [create(evaluator, left.obj - right.obj)] - elif operator == '%': - # With strings and numbers the left type typically remains. Except for - # `int() % float()`. - return [left] - elif operator in COMPARISON_OPERATORS: - operation = COMPARISON_OPERATORS[operator] - if isinstance(left, CompiledObject) and isinstance(right, CompiledObject): - # Possible, because the return is not an option. Just compare. - left = left.obj - right = right.obj - - try: - return [keyword_from_value(operation(left, right))] - except TypeError: - # Could be True or False. - return [true_obj, false_obj] - elif operator == 'in': - return [] - - def check(obj): - """Checks if a Jedi object is either a float or an int.""" - return isinstance(obj, er.Instance) and obj.name.get_code() in ('int', 'float') - - # Static analysis, one is a number, the other one is not. - if operator in ('+', '-') and l_is_num != r_is_num \ - and not (check(left) or check(right)): - message = "TypeError: unsupported operand type(s) for +: %s and %s" - analysis.add(evaluator, 'type-error-operation', operator, - message % (left, right)) - - return [left, right] diff --git a/pythonFiles/jedi/evaluate/recursion.py b/pythonFiles/jedi/evaluate/recursion.py deleted file mode 100644 index a4f5fbc33fc3..000000000000 --- a/pythonFiles/jedi/evaluate/recursion.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -Recursions are the recipe of |jedi| to conquer Python code. However, someone -must stop recursions going mad. Some settings are here to make |jedi| stop at -the right time. You can read more about them :ref:`here `. - -Next to :mod:`jedi.evaluate.cache` this module also makes |jedi| not -thread-safe. Why? ``execution_recursion_decorator`` uses class variables to -count the function calls. -""" -from jedi import debug -from jedi import settings -from jedi.evaluate import compiled -from jedi.evaluate import iterable - - -def recursion_decorator(func): - def run(evaluator, stmt, *args, **kwargs): - rec_detect = evaluator.recursion_detector - # print stmt, len(self.node_statements()) - if rec_detect.push_stmt(stmt): - return [] - else: - result = func(evaluator, stmt, *args, **kwargs) - rec_detect.pop_stmt() - return result - return run - - -class RecursionDetector(object): - """ - A decorator to detect recursions in statements. In a recursion a statement - at the same place, in the same module may not be executed two times. - """ - def __init__(self): - self.top = None - self.current = None - - def push_stmt(self, stmt): - self.current = _RecursionNode(stmt, self.current) - check = self._check_recursion() - if check: - debug.warning('catched stmt recursion: %s against %s @%s', stmt, - check.stmt, stmt.start_pos) - self.pop_stmt() - return True - return False - - def pop_stmt(self): - if self.current is not None: - # I don't know how current can be None, but sometimes it happens - # with Python3. - self.current = self.current.parent - - def _check_recursion(self): - test = self.current - while True: - test = test.parent - if self.current == test: - return test - if not test: - return False - - def node_statements(self): - result = [] - n = self.current - while n: - result.insert(0, n.stmt) - n = n.parent - return result - - -class _RecursionNode(object): - """ A node of the RecursionDecorator. """ - def __init__(self, stmt, parent): - self.script = stmt.get_parent_until() - self.position = stmt.start_pos - self.parent = parent - self.stmt = stmt - - # Don't check param instances, they are not causing recursions - # The same's true for the builtins, because the builtins are really - # simple. - self.is_ignored = self.script == compiled.builtin - - def __eq__(self, other): - if not other: - return None - - return self.script == other.script \ - and self.position == other.position \ - and not self.is_ignored and not other.is_ignored - - -def execution_recursion_decorator(func): - def run(execution, **kwargs): - detector = execution._evaluator.execution_recursion_detector - if detector.push_execution(execution): - result = [] - else: - result = func(execution, **kwargs) - detector.pop_execution() - return result - - return run - - -class ExecutionRecursionDetector(object): - """ - Catches recursions of executions. - It is designed like a Singelton. Only one instance should exist. - """ - def __init__(self): - self.recursion_level = 0 - self.parent_execution_funcs = [] - self.execution_funcs = set() - self.execution_count = 0 - - def __call__(self, execution): - debug.dbg('Execution recursions: %s', execution, self.recursion_level, - self.execution_count, len(self.execution_funcs)) - if self.check_recursion(execution): - result = [] - else: - result = self.func(execution) - self.pop_execution() - return result - - def pop_execution(cls): - cls.parent_execution_funcs.pop() - cls.recursion_level -= 1 - - def push_execution(cls, execution): - in_par_execution_funcs = execution.base in cls.parent_execution_funcs - in_execution_funcs = execution.base in cls.execution_funcs - cls.recursion_level += 1 - cls.execution_count += 1 - cls.execution_funcs.add(execution.base) - cls.parent_execution_funcs.append(execution.base) - - if cls.execution_count > settings.max_executions: - return True - - if isinstance(execution.base, (iterable.Array, iterable.Generator)): - return False - module = execution.get_parent_until() - if module == compiled.builtin: - return False - - if in_par_execution_funcs: - if cls.recursion_level > settings.max_function_recursion_level: - return True - if in_execution_funcs and \ - len(cls.execution_funcs) > settings.max_until_execution_unique: - return True - if cls.execution_count > settings.max_executions_without_builtins: - return True - return False diff --git a/pythonFiles/jedi/evaluate/representation.py b/pythonFiles/jedi/evaluate/representation.py deleted file mode 100644 index 3cfcaa9b5564..000000000000 --- a/pythonFiles/jedi/evaluate/representation.py +++ /dev/null @@ -1,857 +0,0 @@ -""" -Like described in the :mod:`jedi.parser.tree` module, -there's a need for an ast like module to represent the states of parsed -modules. - -But now there are also structures in Python that need a little bit more than -that. An ``Instance`` for example is only a ``Class`` before it is -instantiated. This class represents these cases. - -So, why is there also a ``Class`` class here? Well, there are decorators and -they change classes in Python 3. - -Representation modules also define "magic methods". Those methods look like -``py__foo__`` and are typically mappable to the Python equivalents ``__call__`` -and others. Here's a list: - -====================================== ======================================== -**Method** **Description** --------------------------------------- ---------------------------------------- -py__call__(evaluator, params: Array) On callable objects, returns types. -py__bool__() Returns True/False/None; None means that - there's no certainty. -py__bases__(evaluator) Returns a list of base classes. -py__mro__(evaluator) Returns a list of classes (the mro). -py__getattribute__(evaluator, name) Returns a list of attribute values. The - name can be str or Name. -====================================== ======================================== - -__ -""" -import os -import pkgutil -import imp -import re -from itertools import chain - -from jedi._compatibility import use_metaclass, unicode, Python3Method -from jedi.parser import tree -from jedi import debug -from jedi import common -from jedi.cache import underscore_memoization, cache_star_import -from jedi.evaluate.cache import memoize_default, CachedMetaClass, NO_DEFAULT -from jedi.evaluate import compiled -from jedi.evaluate import recursion -from jedi.evaluate import iterable -from jedi.evaluate import docstrings -from jedi.evaluate import helpers -from jedi.evaluate import param -from jedi.evaluate import flow_analysis -from jedi.evaluate import imports - - -class Executed(tree.Base): - """ - An instance is also an executable - because __init__ is called - :param var_args: The param input array, consist of a parser node or a list. - """ - def __init__(self, evaluator, base, var_args=()): - self._evaluator = evaluator - self.base = base - self.var_args = var_args - - def is_scope(self): - return True - - def get_parent_until(self, *args, **kwargs): - return tree.Base.get_parent_until(self, *args, **kwargs) - - @common.safe_property - def parent(self): - return self.base.parent - - -class Instance(use_metaclass(CachedMetaClass, Executed)): - """ - This class is used to evaluate instances. - """ - def __init__(self, evaluator, base, var_args, is_generated=False): - super(Instance, self).__init__(evaluator, base, var_args) - self.decorates = None - # Generated instances are classes that are just generated by self - # (No var_args) used. - self.is_generated = is_generated - - if base.name.get_code() in ['list', 'set'] \ - and compiled.builtin == base.get_parent_until(): - # compare the module path with the builtin name. - self.var_args = iterable.check_array_instances(evaluator, self) - elif not is_generated: - # Need to execute the __init__ function, because the dynamic param - # searching needs it. - try: - method = self.get_subscope_by_name('__init__') - except KeyError: - pass - else: - evaluator.execute(method, self.var_args) - - @property - def py__call__(self): - def actual(evaluator, params): - return evaluator.execute(method, params) - - try: - method = self.get_subscope_by_name('__call__') - except KeyError: - # Means the Instance is not callable. - raise AttributeError - - return actual - - def py__class__(self, evaluator): - return self.base - - def py__bool__(self): - # Signalize that we don't know about the bool type. - return None - - @memoize_default() - def _get_method_execution(self, func): - func = get_instance_el(self._evaluator, self, func, True) - return FunctionExecution(self._evaluator, func, self.var_args) - - def _get_func_self_name(self, func): - """ - Returns the name of the first param in a class method (which is - normally self. - """ - try: - return str(func.params[0].name) - except IndexError: - return None - - def _self_names_dict(self, add_mro=True): - names = {} - # This loop adds the names of the self object, copies them and removes - # the self. - for sub in self.base.subscopes: - if isinstance(sub, tree.Class): - continue - # Get the self name, if there's one. - self_name = self._get_func_self_name(sub) - if self_name is None: - continue - - if sub.name.value == '__init__' and not self.is_generated: - # ``__init__`` is special because the params need are injected - # this way. Therefore an execution is necessary. - if not sub.get_decorators(): - # __init__ decorators should generally just be ignored, - # because to follow them and their self variables is too - # complicated. - sub = self._get_method_execution(sub) - for name_list in sub.names_dict.values(): - for name in name_list: - if name.value == self_name and name.prev_sibling() is None: - trailer = name.next_sibling() - if tree.is_node(trailer, 'trailer') \ - and len(trailer.children) == 2 \ - and trailer.children[0] == '.': - name = trailer.children[1] # After dot. - if name.is_definition(): - arr = names.setdefault(name.value, []) - arr.append(get_instance_el(self._evaluator, self, name)) - return names - - def get_subscope_by_name(self, name): - sub = self.base.get_subscope_by_name(name) - return get_instance_el(self._evaluator, self, sub, True) - - def execute_subscope_by_name(self, name, *args): - method = self.get_subscope_by_name(name) - return self._evaluator.execute_evaluated(method, *args) - - def get_descriptor_returns(self, obj): - """ Throws a KeyError if there's no method. """ - # Arguments in __get__ descriptors are obj, class. - # `method` is the new parent of the array, don't know if that's good. - args = [obj, obj.base] if isinstance(obj, Instance) else [compiled.none_obj, obj] - try: - return self.execute_subscope_by_name('__get__', *args) - except KeyError: - return [self] - - @memoize_default() - def names_dicts(self, search_global): - yield self._self_names_dict() - - for s in self.base.py__mro__(self._evaluator)[1:]: - if not isinstance(s, compiled.CompiledObject): - # Compiled objects don't have `self.` names. - for inst in self._evaluator.execute(s): - yield inst._self_names_dict(add_mro=False) - - for names_dict in self.base.names_dicts(search_global=False, is_instance=True): - yield LazyInstanceDict(self._evaluator, self, names_dict) - - def get_index_types(self, evaluator, index_array): - indexes = iterable.create_indexes_or_slices(self._evaluator, index_array) - if any([isinstance(i, iterable.Slice) for i in indexes]): - # Slice support in Jedi is very marginal, at the moment, so just - # ignore them in case of __getitem__. - # TODO support slices in a more general way. - indexes = [] - - try: - method = self.get_subscope_by_name('__getitem__') - except KeyError: - debug.warning('No __getitem__, cannot access the array.') - return [] - else: - return self._evaluator.execute(method, [iterable.AlreadyEvaluated(indexes)]) - - @property - @underscore_memoization - def name(self): - name = self.base.name - return helpers.FakeName(unicode(name), self, name.start_pos) - - def __getattr__(self, name): - if name not in ['start_pos', 'end_pos', 'get_imports', 'type', - 'doc', 'raw_doc']: - raise AttributeError("Instance %s: Don't touch this (%s)!" - % (self, name)) - return getattr(self.base, name) - - def __repr__(self): - dec = '' - if self.decorates is not None: - dec = " decorates " + repr(self.decorates) - return "" % (type(self).__name__, self.base, - self.var_args, dec) - - -class LazyInstanceDict(object): - def __init__(self, evaluator, instance, dct): - self._evaluator = evaluator - self._instance = instance - self._dct = dct - - def __getitem__(self, name): - return [get_instance_el(self._evaluator, self._instance, var, True) - for var in self._dct[name]] - - def values(self): - return [self[key] for key in self._dct] - - -class InstanceName(tree.Name): - def __init__(self, origin_name, parent): - super(InstanceName, self).__init__(tree.zero_position_modifier, - origin_name.value, - origin_name.start_pos) - self._origin_name = origin_name - self.parent = parent - - def is_definition(self): - return self._origin_name.is_definition() - - -def get_instance_el(evaluator, instance, var, is_class_var=False): - """ - Returns an InstanceElement if it makes sense, otherwise leaves the object - untouched. - - Basically having an InstanceElement is context information. That is needed - in quite a lot of cases, which includes Nodes like ``power``, that need to - know where a self name comes from for example. - """ - if isinstance(var, tree.Name): - parent = get_instance_el(evaluator, instance, var.parent, is_class_var) - return InstanceName(var, parent) - elif var.type != 'funcdef' \ - and isinstance(var, (Instance, compiled.CompiledObject, tree.Leaf, - tree.Module, FunctionExecution)): - return var - - var = evaluator.wrap(var) - return InstanceElement(evaluator, instance, var, is_class_var) - - -class InstanceElement(use_metaclass(CachedMetaClass, tree.Base)): - """ - InstanceElement is a wrapper for any object, that is used as an instance - variable (e.g. self.variable or class methods). - """ - def __init__(self, evaluator, instance, var, is_class_var): - self._evaluator = evaluator - self.instance = instance - self.var = var - self.is_class_var = is_class_var - - @common.safe_property - @memoize_default() - def parent(self): - par = self.var.parent - if isinstance(par, Class) and par == self.instance.base \ - or isinstance(par, tree.Class) \ - and par == self.instance.base.base: - par = self.instance - else: - par = get_instance_el(self._evaluator, self.instance, par, - self.is_class_var) - return par - - def get_parent_until(self, *args, **kwargs): - return tree.BaseNode.get_parent_until(self, *args, **kwargs) - - def get_definition(self): - return self.get_parent_until((tree.ExprStmt, tree.IsScope, tree.Import)) - - def get_decorated_func(self): - """ Needed because the InstanceElement should not be stripped """ - func = self.var.get_decorated_func() - func = get_instance_el(self._evaluator, self.instance, func) - return func - - def get_rhs(self): - return get_instance_el(self._evaluator, self.instance, - self.var.get_rhs(), self.is_class_var) - - def is_definition(self): - return self.var.is_definition() - - @property - def children(self): - # Copy and modify the array. - return [get_instance_el(self._evaluator, self.instance, command, self.is_class_var) - for command in self.var.children] - - @property - @memoize_default() - def name(self): - name = self.var.name - return helpers.FakeName(unicode(name), self, name.start_pos) - - def __iter__(self): - for el in self.var.__iter__(): - yield get_instance_el(self._evaluator, self.instance, el, - self.is_class_var) - - def __getitem__(self, index): - return get_instance_el(self._evaluator, self.instance, self.var[index], - self.is_class_var) - - def __getattr__(self, name): - return getattr(self.var, name) - - def isinstance(self, *cls): - return isinstance(self.var, cls) - - def is_scope(self): - """ - Since we inherit from Base, it would overwrite the action we want here. - """ - return self.var.is_scope() - - def py__call__(self, evaluator, params): - if isinstance(self.var, compiled.CompiledObject): - # This check is a bit strange, but CompiledObject itself is a bit - # more complicated than we would it actually like to be. - return self.var.py__call__(evaluator, params) - else: - return Function.py__call__(self, evaluator, params) - - def __repr__(self): - return "<%s of %s>" % (type(self).__name__, self.var) - - -class Wrapper(tree.Base): - def is_scope(self): - return True - - def is_class(self): - return False - - def py__bool__(self): - """ - Since Wrapper is a super class for classes, functions and modules, - the return value will always be true. - """ - return True - - @property - @underscore_memoization - def name(self): - name = self.base.name - return helpers.FakeName(unicode(name), self, name.start_pos) - - -class Class(use_metaclass(CachedMetaClass, Wrapper)): - """ - This class is not only important to extend `tree.Class`, it is also a - important for descriptors (if the descriptor methods are evaluated or not). - """ - def __init__(self, evaluator, base): - self._evaluator = evaluator - self.base = base - - @memoize_default(default=()) - def py__mro__(self, evaluator): - def add(cls): - if cls not in mro: - mro.append(cls) - - mro = [self] - # TODO Do a proper mro resolution. Currently we are just listing - # classes. However, it's a complicated algorithm. - for cls in self.py__bases__(self._evaluator): - # TODO detect for TypeError: duplicate base class str, - # e.g. `class X(str, str): pass` - try: - mro_method = cls.py__mro__ - except AttributeError: - # TODO add a TypeError like: - """ - >>> class Y(lambda: test): pass - Traceback (most recent call last): - File "", line 1, in - TypeError: function() argument 1 must be code, not str - >>> class Y(1): pass - Traceback (most recent call last): - File "", line 1, in - TypeError: int() takes at most 2 arguments (3 given) - """ - pass - else: - add(cls) - for cls_new in mro_method(evaluator): - add(cls_new) - return tuple(mro) - - @memoize_default(default=()) - def py__bases__(self, evaluator): - arglist = self.base.get_super_arglist() - if arglist: - args = param.Arguments(self._evaluator, arglist) - return list(chain.from_iterable(args.eval_args())) - else: - return [compiled.object_obj] - - def py__call__(self, evaluator, params): - return [Instance(evaluator, self, params)] - - def py__getattribute__(self, name): - return self._evaluator.find_types(self, name) - - @property - def params(self): - return self.get_subscope_by_name('__init__').params - - def names_dicts(self, search_global, is_instance=False): - if search_global: - yield self.names_dict - else: - for scope in self.py__mro__(self._evaluator): - if isinstance(scope, compiled.CompiledObject): - yield scope.names_dicts(False, is_instance)[0] - else: - yield scope.names_dict - - def is_class(self): - return True - - def get_subscope_by_name(self, name): - for s in self.py__mro__(self._evaluator): - for sub in reversed(s.subscopes): - if sub.name.value == name: - return sub - raise KeyError("Couldn't find subscope.") - - def __getattr__(self, name): - if name not in ['start_pos', 'end_pos', 'parent', 'raw_doc', - 'doc', 'get_imports', 'get_parent_until', 'get_code', - 'subscopes', 'names_dict', 'type']: - raise AttributeError("Don't touch this: %s of %s !" % (name, self)) - return getattr(self.base, name) - - def __repr__(self): - return "" % (type(self).__name__, self.base) - - -class Function(use_metaclass(CachedMetaClass, Wrapper)): - """ - Needed because of decorators. Decorators are evaluated here. - """ - def __init__(self, evaluator, func, is_decorated=False): - """ This should not be called directly """ - self._evaluator = evaluator - self.base = self.base_func = func - self.is_decorated = is_decorated - # A property that is set by the decorator resolution. - self.decorates = None - - @memoize_default() - def get_decorated_func(self): - """ - Returns the function, that should to be executed in the end. - This is also the places where the decorators are processed. - """ - f = self.base_func - decorators = self.base_func.get_decorators() - - if not decorators or self.is_decorated: - return self - - # Only enter it, if has not already been processed. - if not self.is_decorated: - for dec in reversed(decorators): - debug.dbg('decorator: %s %s', dec, f) - dec_results = self._evaluator.eval_element(dec.children[1]) - trailer = dec.children[2:-1] - if trailer: - # Create a trailer and evaluate it. - trailer = tree.Node('trailer', trailer) - trailer.parent = dec - dec_results = self._evaluator.eval_trailer(dec_results, trailer) - - if not len(dec_results): - debug.warning('decorator not found: %s on %s', dec, self.base_func) - return self - decorator = dec_results.pop() - if dec_results: - debug.warning('multiple decorators found %s %s', - self.base_func, dec_results) - - # Create param array. - if isinstance(f, Function): - old_func = f # TODO this is just hacky. change. - else: - old_func = Function(self._evaluator, f, is_decorated=True) - - wrappers = self._evaluator.execute_evaluated(decorator, old_func) - if not len(wrappers): - debug.warning('no wrappers found %s', self.base_func) - return self - if len(wrappers) > 1: - # TODO resolve issue with multiple wrappers -> multiple types - debug.warning('multiple wrappers found %s %s', - self.base_func, wrappers) - f = wrappers[0] - if isinstance(f, (Instance, Function)): - f.decorates = self - - debug.dbg('decorator end %s', f) - return f - - def names_dicts(self, search_global): - if search_global: - yield self.names_dict - else: - for names_dict in compiled.magic_function_class.names_dicts(False): - yield names_dict - - @Python3Method - def py__call__(self, evaluator, params): - if self.base.is_generator(): - return [iterable.Generator(evaluator, self, params)] - else: - return FunctionExecution(evaluator, self, params).get_return_types() - - def __getattr__(self, name): - return getattr(self.base_func, name) - - def __repr__(self): - dec = '' - if self.decorates is not None: - dec = " decorates " + repr(self.decorates) - return "" % (type(self).__name__, self.base_func, dec) - - -class LambdaWrapper(Function): - def get_decorated_func(self): - return self - - -class FunctionExecution(Executed): - """ - This class is used to evaluate functions and their returns. - - This is the most complicated class, because it contains the logic to - transfer parameters. It is even more complicated, because there may be - multiple calls to functions and recursion has to be avoided. But this is - responsibility of the decorators. - """ - type = 'funcdef' - - def __init__(self, evaluator, base, *args, **kwargs): - super(FunctionExecution, self).__init__(evaluator, base, *args, **kwargs) - self._copy_dict = {} - new_func = helpers.deep_ast_copy(base.base_func, self, self._copy_dict) - self.children = new_func.children - self.names_dict = new_func.names_dict - - @memoize_default(default=()) - @recursion.execution_recursion_decorator - def get_return_types(self, check_yields=False): - func = self.base - - if func.isinstance(LambdaWrapper): - return self._evaluator.eval_element(self.children[-1]) - - if func.listeners: - # Feed the listeners, with the params. - for listener in func.listeners: - listener.execute(self._get_params()) - # If we do have listeners, that means that there's not a regular - # execution ongoing. In this case Jedi is interested in the - # inserted params, not in the actual execution of the function. - return [] - - if check_yields: - types = [] - returns = self.yields - else: - returns = self.returns - types = list(docstrings.find_return_types(self._evaluator, func)) - - for r in returns: - check = flow_analysis.break_check(self._evaluator, self, r) - if check is flow_analysis.UNREACHABLE: - debug.dbg('Return unreachable: %s', r) - else: - types += self._evaluator.eval_element(r.children[1]) - if check is flow_analysis.REACHABLE: - debug.dbg('Return reachable: %s', r) - break - return types - - def names_dicts(self, search_global): - yield self.names_dict - - @memoize_default(default=NO_DEFAULT) - def _get_params(self): - """ - This returns the params for an TODO and is injected as a - 'hack' into the tree.Function class. - This needs to be here, because Instance can have __init__ functions, - which act the same way as normal functions. - """ - return param.get_params(self._evaluator, self.base, self.var_args) - - def param_by_name(self, name): - return [n for n in self._get_params() if str(n) == name][0] - - def name_for_position(self, position): - return tree.Function.name_for_position(self, position) - - def _copy_list(self, lst): - """ - Copies a list attribute of a parser Function. Copying is very - expensive, because it is something like `copy.deepcopy`. However, these - copied objects can be used for the executions, as if they were in the - execution. - """ - objects = [] - for element in lst: - self._scope_copy(element.parent) - copied = helpers.deep_ast_copy(element, self._copy_dict) - objects.append(copied) - return objects - - def __getattr__(self, name): - if name not in ['start_pos', 'end_pos', 'imports', 'name', 'type']: - raise AttributeError('Tried to access %s: %s. Why?' % (name, self)) - return getattr(self.base, name) - - def _scope_copy(self, scope): - raise NotImplementedError - """ Copies a scope (e.g. `if foo:`) in an execution """ - if scope != self.base.base_func: - # Just make sure the parents been copied. - self._scope_copy(scope.parent) - helpers.deep_ast_copy(scope, self._copy_dict) - - @common.safe_property - @memoize_default([]) - def returns(self): - return tree.Scope._search_in_scope(self, tree.ReturnStmt) - - @common.safe_property - @memoize_default([]) - def yields(self): - return tree.Scope._search_in_scope(self, tree.YieldExpr) - - @common.safe_property - @memoize_default([]) - def statements(self): - return tree.Scope._search_in_scope(self, tree.ExprStmt) - - @common.safe_property - @memoize_default([]) - def subscopes(self): - return tree.Scope._search_in_scope(self, tree.Scope) - - def __repr__(self): - return "<%s of %s>" % (type(self).__name__, self.base) - - -class GlobalName(helpers.FakeName): - def __init__(self, name): - """ - We need to mark global names somehow. Otherwise they are just normal - names that are not definitions. - """ - super(GlobalName, self).__init__(name.value, name.parent, - name.start_pos, is_definition=True) - - -class ModuleWrapper(use_metaclass(CachedMetaClass, tree.Module, Wrapper)): - def __init__(self, evaluator, module): - self._evaluator = evaluator - self.base = self._module = module - - def names_dicts(self, search_global): - yield self.base.names_dict - yield self._module_attributes_dict() - - for star_module in self.star_imports(): - yield star_module.names_dict - - yield dict((str(n), [GlobalName(n)]) for n in self.base.global_names) - yield self._sub_modules_dict() - - # I'm not sure if the star import cache is really that effective anymore - # with all the other really fast import caches. Recheck. Also we would need - # to push the star imports into Evaluator.modules, if we reenable this. - #@cache_star_import - @memoize_default([]) - def star_imports(self): - modules = [] - for i in self.base.imports: - if i.is_star_import(): - name = i.star_import_name() - new = imports.ImportWrapper(self._evaluator, name).follow() - for module in new: - if isinstance(module, tree.Module): - modules += module.star_imports() - modules += new - return modules - - @memoize_default() - def _module_attributes_dict(self): - def parent_callback(): - return self._evaluator.execute(compiled.create(self._evaluator, str))[0] - - names = ['__file__', '__package__', '__doc__', '__name__'] - # All the additional module attributes are strings. - return dict((n, [helpers.LazyName(n, parent_callback, is_definition=True)]) - for n in names) - - @property - @memoize_default() - def name(self): - return helpers.FakeName(unicode(self.base.name), self, (1, 0)) - - def _get_init_directory(self): - for suffix, _, _ in imp.get_suffixes(): - ending = '__init__' + suffix - if self.py__file__().endswith(ending): - # Remove the ending, including the separator. - return self.py__file__()[:-len(ending) - 1] - return None - - def py__name__(self): - for name, module in self._evaluator.modules.items(): - if module == self: - return name - - return '__main__' - - def py__file__(self): - """ - In contrast to Python's __file__ can be None. - """ - if self._module.path is None: - return None - - return os.path.abspath(self._module.path) - - def py__package__(self): - if self._get_init_directory() is None: - return re.sub(r'\.?[^\.]+$', '', self.py__name__()) - else: - return self.py__name__() - - @property - def py__path__(self): - """ - Not seen here, since it's a property. The callback actually uses a - variable, so use it like:: - - foo.py__path__(sys_path) - - In case of a package, this returns Python's __path__ attribute, which - is a list of paths (strings). - Raises an AttributeError if the module is not a package. - """ - def return_value(search_path): - init_path = self.py__file__() - if os.path.basename(init_path) == '__init__.py': - - with open(init_path, 'rb') as f: - content = common.source_to_unicode(f.read()) - # these are strings that need to be used for namespace packages, - # the first one is ``pkgutil``, the second ``pkg_resources``. - options = ('declare_namespace(__name__)', 'extend_path(__path__') - if options[0] in content or options[1] in content: - # It is a namespace, now try to find the rest of the - # modules on sys_path or whatever the search_path is. - paths = set() - for s in search_path: - other = os.path.join(s, unicode(self.name)) - if os.path.isdir(other): - paths.add(other) - return list(paths) - # Default to this. - return [path] - - path = self._get_init_directory() - - if path is None: - raise AttributeError('Only packages have __path__ attributes.') - else: - return return_value - - @memoize_default() - def _sub_modules_dict(self): - """ - Lists modules in the directory of this module (if this module is a - package). - """ - path = self._module.path - names = {} - if path is not None and path.endswith(os.path.sep + '__init__.py'): - mods = pkgutil.iter_modules([os.path.dirname(path)]) - for module_loader, name, is_pkg in mods: - fake_n = helpers.FakeName(name) - # It's obviously a relative import to the current module. - imp = helpers.FakeImport(fake_n, self, level=1) - fake_n.parent = imp - names[name] = [fake_n] - - # TODO add something like this in the future, its cleaner than the - # import hacks. - # ``os.path`` is a hardcoded exception, because it's a - # ``sys.modules`` modification. - #if str(self.name) == 'os': - # names.append(helpers.FakeName('path', parent=self)) - - return names - - def __getattr__(self, name): - return getattr(self._module, name) - - def __repr__(self): - return "<%s: %s>" % (type(self).__name__, self._module) diff --git a/pythonFiles/jedi/evaluate/stdlib.py b/pythonFiles/jedi/evaluate/stdlib.py deleted file mode 100644 index 20ff5d51aa86..000000000000 --- a/pythonFiles/jedi/evaluate/stdlib.py +++ /dev/null @@ -1,256 +0,0 @@ -""" -Implementations of standard library functions, because it's not possible to -understand them with Jedi. - -To add a new implementation, create a function and add it to the -``_implemented`` dict at the bottom of this module. - -""" -import collections -import re - -from jedi._compatibility import unicode -from jedi.evaluate import compiled -from jedi.evaluate import representation as er -from jedi.evaluate import iterable -from jedi.parser import Parser -from jedi.parser import tree -from jedi import debug -from jedi.evaluate import precedence -from jedi.evaluate import param - - -class NotInStdLib(LookupError): - pass - - -def execute(evaluator, obj, params): - try: - obj_name = str(obj.name) - except AttributeError: - pass - else: - if obj.parent == compiled.builtin: - module_name = 'builtins' - elif isinstance(obj.parent, tree.Module): - module_name = str(obj.parent.name) - else: - module_name = '' - - # for now we just support builtin functions. - try: - return _implemented[module_name][obj_name](evaluator, obj, params) - except KeyError: - pass - raise NotInStdLib() - - -def _follow_param(evaluator, params, index): - try: - key, values = list(params.unpack())[index] - except IndexError: - return [] - else: - return iterable.unite(evaluator.eval_element(v) for v in values) - - -def argument_clinic(string, want_obj=False, want_scope=False): - """ - Works like Argument Clinic (PEP 436), to validate function params. - """ - clinic_args = [] - allow_kwargs = False - optional = False - while string: - # Optional arguments have to begin with a bracket. And should always be - # at the end of the arguments. This is therefore not a proper argument - # clinic implementation. `range()` for exmple allows an optional start - # value at the beginning. - match = re.match('(?:(?:(\[),? ?|, ?|)(\w+)|, ?/)\]*', string) - string = string[len(match.group(0)):] - if not match.group(2): # A slash -> allow named arguments - allow_kwargs = True - continue - optional = optional or bool(match.group(1)) - word = match.group(2) - clinic_args.append((word, optional, allow_kwargs)) - - def f(func): - def wrapper(evaluator, obj, arguments): - try: - lst = list(arguments.eval_argument_clinic(clinic_args)) - except ValueError: - return [] - else: - kwargs = {} - if want_scope: - kwargs['scope'] = arguments.scope() - if want_obj: - kwargs['obj'] = obj - return func(evaluator, *lst, **kwargs) - - return wrapper - return f - - -@argument_clinic('object, name[, default], /') -def builtins_getattr(evaluator, objects, names, defaults=None): - types = [] - # follow the first param - for obj in objects: - if not isinstance(obj, (er.Instance, er.Class, tree.Module, compiled.CompiledObject)): - debug.warning('getattr called without instance') - continue - - for name in names: - if precedence.is_string(name): - return evaluator.find_types(obj, name.obj) - else: - debug.warning('getattr called without str') - continue - return types - - -@argument_clinic('object[, bases, dict], /') -def builtins_type(evaluator, objects, bases, dicts): - if bases or dicts: - # metaclass... maybe someday... - return [] - else: - return [o.base for o in objects if isinstance(o, er.Instance)] - - -class SuperInstance(er.Instance): - """To be used like the object ``super`` returns.""" - def __init__(self, evaluator, cls): - su = cls.py_mro()[1] - super().__init__(evaluator, su and su[0] or self) - - -@argument_clinic('[type[, obj]], /', want_scope=True) -def builtins_super(evaluator, types, objects, scope): - # TODO make this able to detect multiple inheritance super - accept = (tree.Function, er.FunctionExecution) - if scope.isinstance(*accept): - wanted = (tree.Class, er.Instance) - cls = scope.get_parent_until(accept + wanted, - include_current=False) - if isinstance(cls, wanted): - if isinstance(cls, tree.Class): - cls = er.Class(evaluator, cls) - elif isinstance(cls, er.Instance): - cls = cls.base - su = cls.py__bases__(evaluator) - if su: - return evaluator.execute(su[0]) - return [] - - -@argument_clinic('sequence, /', want_obj=True) -def builtins_reversed(evaluator, sequences, obj): - # Unpack the iterator values - objects = tuple(iterable.get_iterator_types(sequences)) - rev = [iterable.AlreadyEvaluated([o]) for o in reversed(objects)] - # Repack iterator values and then run it the normal way. This is - # necessary, because `reversed` is a function and autocompletion - # would fail in certain cases like `reversed(x).__iter__` if we - # just returned the result directly. - rev = iterable.AlreadyEvaluated( - [iterable.FakeSequence(evaluator, rev, 'list')] - ) - return [er.Instance(evaluator, obj, param.Arguments(evaluator, [rev]))] - - -@argument_clinic('obj, type, /') -def builtins_isinstance(evaluator, objects, types): - bool_results = set([]) - for o in objects: - try: - mro_func = o.py__class__(evaluator).py__mro__ - except AttributeError: - # This is temporary. Everything should have a class attribute in - # Python?! Maybe we'll leave it here, because some numpy objects or - # whatever might not. - return [compiled.true_obj, compiled.false_obj] - - mro = mro_func(evaluator) - - for cls_or_tup in types: - if cls_or_tup.is_class(): - bool_results.add(cls_or_tup in mro) - else: - # Check for tuples. - classes = iterable.get_iterator_types([cls_or_tup]) - bool_results.add(any(cls in mro for cls in classes)) - - return [compiled.keyword_from_value(x) for x in bool_results] - - -def collections_namedtuple(evaluator, obj, params): - """ - Implementation of the namedtuple function. - - This has to be done by processing the namedtuple class template and - evaluating the result. - - .. note:: |jedi| only supports namedtuples on Python >2.6. - - """ - # Namedtuples are not supported on Python 2.6 - if not hasattr(collections, '_class_template'): - return [] - - # Process arguments - name = _follow_param(evaluator, params, 0)[0].obj - _fields = _follow_param(evaluator, params, 1)[0] - if isinstance(_fields, compiled.CompiledObject): - fields = _fields.obj.replace(',', ' ').split() - elif isinstance(_fields, iterable.Array): - try: - fields = [v.obj for v in _fields.values()] - except AttributeError: - return [] - else: - return [] - - # Build source - source = collections._class_template.format( - typename=name, - field_names=fields, - num_fields=len(fields), - arg_list=', '.join(fields), - repr_fmt=', '.join(collections._repr_template.format(name=name) for name in fields), - field_defs='\n'.join(collections._field_template.format(index=index, name=name) - for index, name in enumerate(fields)) - ) - - # Parse source - generated_class = Parser(evaluator.grammar, unicode(source)).module.subscopes[0] - return [er.Class(evaluator, generated_class)] - - -@argument_clinic('first, /') -def _return_first_param(evaluator, firsts): - return firsts - - -_implemented = { - 'builtins': { - 'getattr': builtins_getattr, - 'type': builtins_type, - 'super': builtins_super, - 'reversed': builtins_reversed, - 'isinstance': builtins_isinstance, - }, - 'copy': { - 'copy': _return_first_param, - 'deepcopy': _return_first_param, - }, - 'json': { - 'load': lambda *args: [], - 'loads': lambda *args: [], - }, - 'collections': { - 'namedtuple': collections_namedtuple, - }, -} diff --git a/pythonFiles/jedi/evaluate/sys_path.py b/pythonFiles/jedi/evaluate/sys_path.py deleted file mode 100644 index 7cfbd57b06ef..000000000000 --- a/pythonFiles/jedi/evaluate/sys_path.py +++ /dev/null @@ -1,247 +0,0 @@ -import glob -import os -import sys - -from jedi._compatibility import exec_function, unicode -from jedi.parser import tree -from jedi.parser import Parser -from jedi.evaluate.cache import memoize_default -from jedi import debug -from jedi import common -from jedi import cache - - -def get_sys_path(): - def check_virtual_env(sys_path): - """ Add virtualenv's site-packages to the `sys.path`.""" - venv = os.getenv('VIRTUAL_ENV') - if not venv: - return - venv = os.path.abspath(venv) - p = _get_venv_sitepackages(venv) - if p not in sys_path: - sys_path.insert(0, p) - - # Add all egg-links from the virtualenv. - for egg_link in glob.glob(os.path.join(p, '*.egg-link')): - with open(egg_link) as fd: - sys_path.insert(0, fd.readline().rstrip()) - - check_virtual_env(sys.path) - return [p for p in sys.path if p != ""] - - -def _get_venv_sitepackages(venv): - if os.name == 'nt': - p = os.path.join(venv, 'lib', 'site-packages') - else: - p = os.path.join(venv, 'lib', 'python%d.%d' % sys.version_info[:2], - 'site-packages') - return p - - -def _execute_code(module_path, code): - c = "import os; from os.path import *; result=%s" - variables = {'__file__': module_path} - try: - exec_function(c % code, variables) - except Exception: - debug.warning('sys.path manipulation detected, but failed to evaluate.') - else: - try: - res = variables['result'] - if isinstance(res, str): - return [os.path.abspath(res)] - except KeyError: - pass - return [] - - -def _paths_from_assignment(evaluator, expr_stmt): - """ - Extracts the assigned strings from an assignment that looks as follows:: - - >>> sys.path[0:0] = ['module/path', 'another/module/path'] - - This function is in general pretty tolerant (and therefore 'buggy'). - However, it's not a big issue usually to add more paths to Jedi's sys_path, - because it will only affect Jedi in very random situations and by adding - more paths than necessary, it usually benefits the general user. - """ - for assignee, operator in zip(expr_stmt.children[::2], expr_stmt.children[1::2]): - try: - assert operator in ['=', '+='] - assert tree.is_node(assignee, 'power') and len(assignee.children) > 1 - c = assignee.children - assert c[0].type == 'name' and c[0].value == 'sys' - trailer = c[1] - assert trailer.children[0] == '.' and trailer.children[1].value == 'path' - # TODO Essentially we're not checking details on sys.path - # manipulation. Both assigment of the sys.path and changing/adding - # parts of the sys.path are the same: They get added to the current - # sys.path. - """ - execution = c[2] - assert execution.children[0] == '[' - subscript = execution.children[1] - assert subscript.type == 'subscript' - assert ':' in subscript.children - """ - except AssertionError: - continue - - from jedi.evaluate.iterable import get_iterator_types - from jedi.evaluate.precedence import is_string - for val in get_iterator_types(evaluator.eval_statement(expr_stmt)): - if is_string(val): - yield val.obj - - -def _paths_from_list_modifications(module_path, trailer1, trailer2): - """ extract the path from either "sys.path.append" or "sys.path.insert" """ - # Guarantee that both are trailers, the first one a name and the second one - # a function execution with at least one param. - if not (tree.is_node(trailer1, 'trailer') and trailer1.children[0] == '.' - and tree.is_node(trailer2, 'trailer') and trailer2.children[0] == '(' - and len(trailer2.children) == 3): - return [] - - name = trailer1.children[1].value - if name not in ['insert', 'append']: - return [] - - arg = trailer2.children[1] - if name == 'insert' and len(arg.children) in (3, 4): # Possible trailing comma. - arg = arg.children[2] - return _execute_code(module_path, arg.get_code()) - - -def _check_module(evaluator, module): - def get_sys_path_powers(names): - for name in names: - power = name.parent.parent - if tree.is_node(power, 'power'): - c = power.children - if isinstance(c[0], tree.Name) and c[0].value == 'sys' \ - and tree.is_node(c[1], 'trailer'): - n = c[1].children[1] - if isinstance(n, tree.Name) and n.value == 'path': - yield name, power - - sys_path = list(get_sys_path()) # copy - try: - possible_names = module.used_names['path'] - except KeyError: - pass - else: - for name, power in get_sys_path_powers(possible_names): - stmt = name.get_definition() - if len(power.children) >= 4: - sys_path.extend(_paths_from_list_modifications(module.path, *power.children[2:4])) - elif name.get_definition().type == 'expr_stmt': - sys_path.extend(_paths_from_assignment(evaluator, stmt)) - return sys_path - - -@memoize_default(evaluator_is_first_arg=True, default=[]) -def sys_path_with_modifications(evaluator, module): - if module.path is None: - # Support for modules without a path is bad, therefore return the - # normal path. - return list(get_sys_path()) - - curdir = os.path.abspath(os.curdir) - with common.ignored(OSError): - os.chdir(os.path.dirname(module.path)) - - buildout_script_paths = set() - - result = _check_module(evaluator, module) - result += _detect_django_path(module.path) - for buildout_script in _get_buildout_scripts(module.path): - for path in _get_paths_from_buildout_script(evaluator, buildout_script): - buildout_script_paths.add(path) - # cleanup, back to old directory - os.chdir(curdir) - return list(result) + list(buildout_script_paths) - - -def _get_paths_from_buildout_script(evaluator, buildout_script): - def load(buildout_script): - try: - with open(buildout_script, 'rb') as f: - source = common.source_to_unicode(f.read()) - except IOError: - debug.dbg('Error trying to read buildout_script: %s', buildout_script) - return - - p = Parser(evaluator.grammar, source, buildout_script) - cache.save_parser(buildout_script, p) - return p.module - - cached = cache.load_parser(buildout_script) - module = cached and cached.module or load(buildout_script) - if not module: - return - - for path in _check_module(evaluator, module): - yield path - - -def traverse_parents(path): - while True: - new = os.path.dirname(path) - if new == path: - return - path = new - yield path - - -def _get_parent_dir_with_file(path, filename): - for parent in traverse_parents(path): - if os.path.isfile(os.path.join(parent, filename)): - return parent - return None - - -def _detect_django_path(module_path): - """ Detects the path of the very well known Django library (if used) """ - result = [] - - for parent in traverse_parents(module_path): - with common.ignored(IOError): - with open(parent + os.path.sep + 'manage.py'): - debug.dbg('Found django path: %s', module_path) - result.append(parent) - return result - - -def _get_buildout_scripts(module_path): - """ - if there is a 'buildout.cfg' file in one of the parent directories of the - given module it will return a list of all files in the buildout bin - directory that look like python files. - - :param module_path: absolute path to the module. - :type module_path: str - """ - project_root = _get_parent_dir_with_file(module_path, 'buildout.cfg') - if not project_root: - return [] - bin_path = os.path.join(project_root, 'bin') - if not os.path.exists(bin_path): - return [] - extra_module_paths = [] - for filename in os.listdir(bin_path): - try: - filepath = os.path.join(bin_path, filename) - with open(filepath, 'r') as f: - firstline = f.readline() - if firstline.startswith('#!') and 'python' in firstline: - extra_module_paths.append(filepath) - except IOError as e: - # either permission error or race cond. because file got deleted - # ignore - debug.warning(unicode(e)) - continue - return extra_module_paths diff --git a/pythonFiles/jedi/parser/__init__.py b/pythonFiles/jedi/parser/__init__.py deleted file mode 100644 index 3dc982bccc30..000000000000 --- a/pythonFiles/jedi/parser/__init__.py +++ /dev/null @@ -1,393 +0,0 @@ -""" -The ``Parser`` tries to convert the available Python code in an easy to read -format, something like an abstract syntax tree. The classes who represent this -tree, are sitting in the :mod:`jedi.parser.tree` module. - -The Python module ``tokenize`` is a very important part in the ``Parser``, -because it splits the code into different words (tokens). Sometimes it looks a -bit messy. Sorry for that! You might ask now: "Why didn't you use the ``ast`` -module for this? Well, ``ast`` does a very good job understanding proper Python -code, but fails to work as soon as there's a single line of broken code. - -There's one important optimization that needs to be known: Statements are not -being parsed completely. ``Statement`` is just a representation of the tokens -within the statement. This lowers memory usage and cpu time and reduces the -complexity of the ``Parser`` (there's another parser sitting inside -``Statement``, which produces ``Array`` and ``Call``). -""" -import os -import re - -from jedi.parser import tree as pt -from jedi.parser import tokenize -from jedi.parser import token -from jedi.parser.token import (DEDENT, INDENT, ENDMARKER, NEWLINE, NUMBER, - STRING, OP, ERRORTOKEN) -from jedi.parser.pgen2.pgen import generate_grammar -from jedi.parser.pgen2.parse import PgenParser - -OPERATOR_KEYWORDS = 'and', 'for', 'if', 'else', 'in', 'is', 'lambda', 'not', 'or' -# Not used yet. In the future I intend to add something like KeywordStatement -STATEMENT_KEYWORDS = 'assert', 'del', 'global', 'nonlocal', 'raise', \ - 'return', 'yield', 'pass', 'continue', 'break' - - -_loaded_grammars = {} - - -def load_grammar(file='grammar3.4'): - # For now we only support two different Python syntax versions: The latest - # Python 3 and Python 2. This may change. - if file.startswith('grammar3'): - file = 'grammar3.4' - else: - file = 'grammar2.7' - - global _loaded_grammars - path = os.path.join(os.path.dirname(__file__), file) + '.txt' - try: - return _loaded_grammars[path] - except KeyError: - return _loaded_grammars.setdefault(path, generate_grammar(path)) - - -class ErrorStatement(object): - def __init__(self, stack, next_token, position_modifier, next_start_pos): - self.stack = stack - self._position_modifier = position_modifier - self.next_token = next_token - self._next_start_pos = next_start_pos - - @property - def next_start_pos(self): - s = self._next_start_pos - return s[0] + self._position_modifier.line, s[1] - - @property - def first_pos(self): - first_type, nodes = self.stack[0] - return nodes[0].start_pos - - @property - def first_type(self): - first_type, nodes = self.stack[0] - return first_type - - -class ParserSyntaxError(object): - def __init__(self, message, position): - self.message = message - self.position = position - - -class Parser(object): - """ - This class is used to parse a Python file, it then divides them into a - class structure of different scopes. - - :param grammar: The grammar object of pgen2. Loaded by load_grammar. - :param source: The codebase for the parser. Must be unicode. - :param module_path: The path of the module in the file system, may be None. - :type module_path: str - :param top_module: Use this module as a parent instead of `self.module`. - """ - def __init__(self, grammar, source, module_path=None, tokenizer=None): - self._ast_mapping = { - 'expr_stmt': pt.ExprStmt, - 'classdef': pt.Class, - 'funcdef': pt.Function, - 'file_input': pt.Module, - 'import_name': pt.ImportName, - 'import_from': pt.ImportFrom, - 'break_stmt': pt.KeywordStatement, - 'continue_stmt': pt.KeywordStatement, - 'return_stmt': pt.ReturnStmt, - 'raise_stmt': pt.KeywordStatement, - 'yield_expr': pt.YieldExpr, - 'del_stmt': pt.KeywordStatement, - 'pass_stmt': pt.KeywordStatement, - 'global_stmt': pt.GlobalStmt, - 'nonlocal_stmt': pt.KeywordStatement, - 'assert_stmt': pt.AssertStmt, - 'if_stmt': pt.IfStmt, - 'with_stmt': pt.WithStmt, - 'for_stmt': pt.ForStmt, - 'while_stmt': pt.WhileStmt, - 'try_stmt': pt.TryStmt, - 'comp_for': pt.CompFor, - 'decorator': pt.Decorator, - 'lambdef': pt.Lambda, - 'old_lambdef': pt.Lambda, - 'lambdef_nocond': pt.Lambda, - } - - self.syntax_errors = [] - - self._global_names = [] - self._omit_dedent_list = [] - self._indent_counter = 0 - self._last_failed_start_pos = (0, 0) - - # TODO do print absolute import detection here. - #try: - # del python_grammar_no_print_statement.keywords["print"] - #except KeyError: - # pass # Doesn't exist in the Python 3 grammar. - - #if self.options["print_function"]: - # python_grammar = pygram.python_grammar_no_print_statement - #else: - self._used_names = {} - self._scope_names_stack = [{}] - self._error_statement_stacks = [] - - added_newline = False - # The Python grammar needs a newline at the end of each statement. - if not source.endswith('\n'): - source += '\n' - added_newline = True - - # For the fast parser. - self.position_modifier = pt.PositionModifier() - p = PgenParser(grammar, self.convert_node, self.convert_leaf, - self.error_recovery) - tokenizer = tokenizer or tokenize.source_tokens(source) - self.module = p.parse(self._tokenize(tokenizer)) - if self.module.type != 'file_input': - # If there's only one statement, we get back a non-module. That's - # not what we want, we want a module, so we add it here: - self.module = self.convert_node(grammar, - grammar.symbol2number['file_input'], - [self.module]) - - if added_newline: - self.remove_last_newline() - self.module.used_names = self._used_names - self.module.path = module_path - self.module.global_names = self._global_names - self.module.error_statement_stacks = self._error_statement_stacks - - def convert_node(self, grammar, type, children): - """ - Convert raw node information to a Node instance. - - This is passed to the parser driver which calls it whenever a reduction of a - grammar rule produces a new complete node, so that the tree is build - strictly bottom-up. - """ - symbol = grammar.number2symbol[type] - try: - new_node = self._ast_mapping[symbol](children) - except KeyError: - new_node = pt.Node(symbol, children) - - # We need to check raw_node always, because the same node can be - # returned by convert multiple times. - if symbol == 'global_stmt': - self._global_names += new_node.get_global_names() - elif isinstance(new_node, pt.Lambda): - new_node.names_dict = self._scope_names_stack.pop() - elif isinstance(new_node, (pt.ClassOrFunc, pt.Module)) \ - and symbol in ('funcdef', 'classdef', 'file_input'): - # scope_name_stack handling - scope_names = self._scope_names_stack.pop() - if isinstance(new_node, pt.ClassOrFunc): - n = new_node.name - scope_names[n.value].remove(n) - # Set the func name of the current node - arr = self._scope_names_stack[-1].setdefault(n.value, []) - arr.append(n) - new_node.names_dict = scope_names - elif isinstance(new_node, pt.CompFor): - # The name definitions of comprehenions shouldn't be part of the - # current scope. They are part of the comprehension scope. - for n in new_node.get_defined_names(): - self._scope_names_stack[-1][n.value].remove(n) - return new_node - - def convert_leaf(self, grammar, type, value, prefix, start_pos): - #print('leaf', value, pytree.type_repr(type)) - if type == tokenize.NAME: - if value in grammar.keywords: - if value in ('def', 'class', 'lambda'): - self._scope_names_stack.append({}) - - return pt.Keyword(self.position_modifier, value, start_pos, prefix) - else: - name = pt.Name(self.position_modifier, value, start_pos, prefix) - # Keep a listing of all used names - arr = self._used_names.setdefault(name.value, []) - arr.append(name) - arr = self._scope_names_stack[-1].setdefault(name.value, []) - arr.append(name) - return name - elif type == STRING: - return pt.String(self.position_modifier, value, start_pos, prefix) - elif type == NUMBER: - return pt.Number(self.position_modifier, value, start_pos, prefix) - elif type in (NEWLINE, ENDMARKER): - return pt.Whitespace(self.position_modifier, value, start_pos, prefix) - else: - return pt.Operator(self.position_modifier, value, start_pos, prefix) - - def error_recovery(self, grammar, stack, typ, value, start_pos, prefix, - add_token_callback): - """ - This parser is written in a dynamic way, meaning that this parser - allows using different grammars (even non-Python). However, error - recovery is purely written for Python. - """ - def current_suite(stack): - # For now just discard everything that is not a suite or - # file_input, if we detect an error. - for index, (dfa, state, (typ, nodes)) in reversed(list(enumerate(stack))): - # `suite` can sometimes be only simple_stmt, not stmt. - symbol = grammar.number2symbol[typ] - if symbol == 'file_input': - break - elif symbol == 'suite' and len(nodes) > 1: - # suites without an indent in them get discarded. - break - elif symbol == 'simple_stmt' and len(nodes) > 1: - # simple_stmt can just be turned into a Node, if there are - # enough statements. Ignore the rest after that. - break - return index, symbol, nodes - - index, symbol, nodes = current_suite(stack) - if symbol == 'simple_stmt': - index -= 2 - (_, _, (typ, suite_nodes)) = stack[index] - symbol = grammar.number2symbol[typ] - suite_nodes.append(pt.Node(symbol, list(nodes))) - # Remove - nodes[:] = [] - nodes = suite_nodes - stack[index] - - #print('err', token.tok_name[typ], repr(value), start_pos, len(stack), index) - self._stack_removal(grammar, stack, index + 1, value, start_pos) - if typ == INDENT: - # For every deleted INDENT we have to delete a DEDENT as well. - # Otherwise the parser will get into trouble and DEDENT too early. - self._omit_dedent_list.append(self._indent_counter) - - if value in ('import', 'from', 'class', 'def', 'try', 'while', 'return'): - # Those can always be new statements. - add_token_callback(typ, value, prefix, start_pos) - elif typ == DEDENT and symbol == 'suite': - # Close the current suite, with DEDENT. - # Note that this may cause some suites to not contain any - # statements at all. This is contrary to valid Python syntax. We - # keep incomplete suites in Jedi to be able to complete param names - # or `with ... as foo` names. If we want to use this parser for - # syntax checks, we have to check in a separate turn if suites - # contain statements or not. However, a second check is necessary - # anyway (compile.c does that for Python), because Python's grammar - # doesn't stop you from defining `continue` in a module, etc. - add_token_callback(typ, value, prefix, start_pos) - - def _stack_removal(self, grammar, stack, start_index, value, start_pos): - def clear_names(children): - for c in children: - try: - clear_names(c.children) - except AttributeError: - if isinstance(c, pt.Name): - try: - self._scope_names_stack[-1][c.value].remove(c) - self._used_names[c.value].remove(c) - except ValueError: - pass # This may happen with CompFor. - - for dfa, state, node in stack[start_index:]: - clear_names(children=node[1]) - - failed_stack = [] - found = False - for dfa, state, (typ, nodes) in stack[start_index:]: - if nodes: - found = True - if found: - symbol = grammar.number2symbol[typ] - failed_stack.append((symbol, nodes)) - if nodes and nodes[0] in ('def', 'class', 'lambda'): - self._scope_names_stack.pop() - if failed_stack: - err = ErrorStatement(failed_stack, value, self.position_modifier, start_pos) - self._error_statement_stacks.append(err) - - self._last_failed_start_pos = start_pos - - stack[start_index:] = [] - - def _tokenize(self, tokenizer): - for typ, value, start_pos, prefix in tokenizer: - #print(tokenize.tok_name[typ], repr(value), start_pos, repr(prefix)) - if typ == DEDENT: - # We need to count indents, because if we just omit any DEDENT, - # we might omit them in the wrong place. - o = self._omit_dedent_list - if o and o[-1] == self._indent_counter: - o.pop() - continue - - self._indent_counter -= 1 - elif typ == INDENT: - self._indent_counter += 1 - elif typ == ERRORTOKEN: - self._add_syntax_error('Strange token', start_pos) - continue - - if typ == OP: - typ = token.opmap[value] - yield typ, value, prefix, start_pos - - def _add_syntax_error(self, message, position): - self.syntax_errors.append(ParserSyntaxError(message, position)) - - def __repr__(self): - return "<%s: %s>" % (type(self).__name__, self.module) - - def remove_last_newline(self): - """ - In all of this we need to work with _start_pos, because if we worked - with start_pos, we would need to check the position_modifier as well - (which is accounted for in the start_pos property). - """ - endmarker = self.module.children[-1] - # The newline is either in the endmarker as a prefix or the previous - # leaf as a newline token. - if endmarker.prefix.endswith('\n'): - endmarker.prefix = endmarker.prefix[:-1] - last_line = re.sub('.*\n', '', endmarker.prefix) - endmarker._start_pos = endmarker._start_pos[0] - 1, len(last_line) - else: - try: - newline = endmarker.get_previous() - except IndexError: - return # This means that the parser is empty. - while True: - if newline.value == '': - # Must be a DEDENT, just continue. - try: - newline = newline.get_previous() - except IndexError: - # If there's a statement that fails to be parsed, there - # will be no previous leaf. So just ignore it. - break - elif newline.value != '\n': - # This may happen if error correction strikes and removes - # a whole statement including '\n'. - break - else: - newline.value = '' - if self._last_failed_start_pos > newline._start_pos: - # It may be the case that there was a syntax error in a - # function. In that case error correction removes the - # right newline. So we use the previously assigned - # _last_failed_start_pos variable to account for that. - endmarker._start_pos = self._last_failed_start_pos - else: - endmarker._start_pos = newline._start_pos - break diff --git a/pythonFiles/jedi/parser/fast.py b/pythonFiles/jedi/parser/fast.py deleted file mode 100644 index 35bb85556b22..000000000000 --- a/pythonFiles/jedi/parser/fast.py +++ /dev/null @@ -1,580 +0,0 @@ -""" -Basically a parser that is faster, because it tries to parse only parts and if -anything changes, it only reparses the changed parts. But because it's not -finished (and still not working as I want), I won't document it any further. -""" -import re -from itertools import chain - -from jedi._compatibility import use_metaclass -from jedi import settings -from jedi.parser import Parser -from jedi.parser import tree -from jedi import cache -from jedi import debug -from jedi.parser.tokenize import (source_tokens, NEWLINE, - ENDMARKER, INDENT, DEDENT) - -FLOWS = 'if', 'else', 'elif', 'while', 'with', 'try', 'except', 'finally', 'for' - - -class FastModule(tree.Module): - type = 'file_input' - - def __init__(self, module_path): - super(FastModule, self).__init__([]) - self.modules = [] - self.reset_caches() - self.names_dict = {} - self.path = module_path - - def reset_caches(self): - self.modules = [] - try: - del self._used_names # Remove the used names cache. - except AttributeError: - pass # It was never used. - - @property - @cache.underscore_memoization - def used_names(self): - return MergedNamesDict([m.used_names for m in self.modules]) - - @property - def global_names(self): - return [name for m in self.modules for name in m.global_names] - - @property - def error_statement_stacks(self): - return [e for m in self.modules for e in m.error_statement_stacks] - - def __repr__(self): - return "" % (type(self).__name__, self.name, - self.start_pos[0], self.end_pos[0]) - - # To avoid issues with with the `parser.Parser`, we need setters that do - # nothing, because if pickle comes along and sets those values. - @global_names.setter - def global_names(self, value): - pass - - @error_statement_stacks.setter - def error_statement_stacks(self, value): - pass - - @used_names.setter - def used_names(self, value): - pass - - -class MergedNamesDict(object): - def __init__(self, dicts): - self.dicts = dicts - - def __iter__(self): - return iter(set(key for dct in self.dicts for key in dct)) - - def __getitem__(self, value): - return list(chain.from_iterable(dct.get(value, []) for dct in self.dicts)) - - def items(self): - dct = {} - for d in self.dicts: - for key, values in d.items(): - try: - dct_values = dct[key] - dct_values += values - except KeyError: - dct[key] = list(values) - return dct.items() - - def values(self): - lst = [] - for dct in self.dicts: - lst += dct.values() - return lst - - -class CachedFastParser(type): - """ This is a metaclass for caching `FastParser`. """ - def __call__(self, grammar, source, module_path=None): - if not settings.fast_parser: - return Parser(grammar, source, module_path) - - pi = cache.parser_cache.get(module_path, None) - if pi is None or isinstance(pi.parser, Parser): - p = super(CachedFastParser, self).__call__(grammar, source, module_path) - else: - p = pi.parser # pi is a `cache.ParserCacheItem` - p.update(source) - return p - - -class ParserNode(object): - def __init__(self, fast_module, parser, source): - self._fast_module = fast_module - self.parent = None - self._node_children = [] - - self.source = source - self.hash = hash(source) - self.parser = parser - - try: - # With fast_parser we have either 1 subscope or only statements. - self._content_scope = parser.module.subscopes[0] - except IndexError: - self._content_scope = parser.module - else: - self._rewrite_last_newline() - - # We need to be able to reset the original children of a parser. - self._old_children = list(self._content_scope.children) - - def _rewrite_last_newline(self): - """ - The ENDMARKER can contain a newline in the prefix. However this prefix - really belongs to the function - respectively to the next function or - parser node. If we don't rewrite that newline, we end up with a newline - in the wrong position, i.d. at the end of the file instead of in the - middle. - """ - c = self._content_scope.children - if tree.is_node(c[-1], 'suite'): # In a simple_stmt there's no DEDENT. - end_marker = self.parser.module.children[-1] - # Set the DEDENT prefix instead of the ENDMARKER. - c[-1].children[-1].prefix = end_marker.prefix - end_marker.prefix = '' - - def __repr__(self): - module = self.parser.module - try: - return '<%s: %s-%s>' % (type(self).__name__, module.start_pos, module.end_pos) - except IndexError: - # There's no module yet. - return '<%s: empty>' % type(self).__name__ - - def reset_node(self): - """ - Removes changes that were applied in this class. - """ - self._node_children = [] - scope = self._content_scope - scope.children = list(self._old_children) - try: - # This works if it's a MergedNamesDict. - # We are correcting it, because the MergedNamesDicts are artificial - # and can change after closing a node. - scope.names_dict = scope.names_dict.dicts[0] - except AttributeError: - pass - - def close(self): - """ - Closes the current parser node. This means that after this no further - nodes should be added anymore. - """ - # We only need to replace the dict if multiple dictionaries are used: - if self._node_children: - dcts = [n.parser.module.names_dict for n in self._node_children] - # Need to insert the own node as well. - dcts.insert(0, self._content_scope.names_dict) - self._content_scope.names_dict = MergedNamesDict(dcts) - - def parent_until_indent(self, indent=None): - if (indent is None or self._indent >= indent) and self.parent is not None: - self.close() - return self.parent.parent_until_indent(indent) - return self - - @property - def _indent(self): - if not self.parent: - return 0 - - return self.parser.module.children[0].start_pos[1] - - def add_node(self, node, line_offset): - """Adding a node means adding a node that was already added earlier""" - # Changing the line offsets is very important, because if they don't - # fit, all the start_pos values will be wrong. - m = node.parser.module - node.parser.position_modifier.line = line_offset - self._fast_module.modules.append(m) - node.parent = self - - self._node_children.append(node) - - # Insert parser objects into current structure. We only need to set the - # parents and children in a good way. - scope = self._content_scope - for child in m.children: - child.parent = scope - scope.children.append(child) - - return node - - def all_sub_nodes(self): - """ - Returns all nodes including nested ones. - """ - for n in self._node_children: - yield n - for y in n.all_sub_nodes(): - yield y - - @cache.underscore_memoization # Should only happen once! - def remove_last_newline(self): - self.parser.remove_last_newline() - - -class FastParser(use_metaclass(CachedFastParser)): - _FLOWS_NEED_SPACE = 'if', 'elif', 'while', 'with', 'except', 'for' - _FLOWS_NEED_COLON = 'else', 'try', 'except', 'finally' - _keyword_re = re.compile('^[ \t]*(def |class |@|(?:%s)|(?:%s)\s*:)' - % ('|'.join(_FLOWS_NEED_SPACE), - '|'.join(_FLOWS_NEED_COLON))) - - def __init__(self, grammar, source, module_path=None): - # set values like `tree.Module`. - self._grammar = grammar - self.module_path = module_path - self._reset_caches() - self.update(source) - - def _reset_caches(self): - self.module = FastModule(self.module_path) - self.current_node = ParserNode(self.module, self, '') - - def update(self, source): - # For testing purposes: It is important that the number of parsers used - # can be minimized. With these variables we can test against that. - self.number_parsers_used = 0 - self.number_of_splits = 0 - self.number_of_misses = 0 - self.module.reset_caches() - try: - self._parse(source) - except: - # FastParser is cached, be careful with exceptions. - self._reset_caches() - raise - - def _split_parts(self, source): - """ - Split the source code into different parts. This makes it possible to - parse each part seperately and therefore cache parts of the file and - not everything. - """ - def gen_part(): - text = ''.join(current_lines) - del current_lines[:] - self.number_of_splits += 1 - return text - - def just_newlines(current_lines): - for line in current_lines: - line = line.lstrip('\t \n\r') - if line and line[0] != '#': - return False - return True - - # Split only new lines. Distinction between \r\n is the tokenizer's - # job. - # It seems like there's no problem with form feed characters here, - # because we're not counting lines. - self._lines = source.splitlines(True) - current_lines = [] - is_decorator = False - # Use -1, because that indent is always smaller than any other. - indent_list = [-1, 0] - new_indent = False - parentheses_level = 0 - flow_indent = None - previous_line = None - # All things within flows are simply being ignored. - for i, l in enumerate(self._lines): - # Handle backslash newline escaping. - if l.endswith('\\\n') or l.endswith('\\\r\n'): - if previous_line is not None: - previous_line += l - else: - previous_line = l - continue - if previous_line is not None: - l = previous_line + l - previous_line = None - - # check for dedents - s = l.lstrip('\t \n\r') - indent = len(l) - len(s) - if not s or s[0] == '#': - current_lines.append(l) # Just ignore comments and blank lines - continue - - if new_indent: - if indent > indent_list[-2]: - # Set the actual indent, not just the random old indent + 1. - indent_list[-1] = indent - new_indent = False - - while indent <= indent_list[-2]: # -> dedent - indent_list.pop() - # This automatically resets the flow_indent if there was a - # dedent or a flow just on one line (with one simple_stmt). - new_indent = False - if flow_indent is None and current_lines and not parentheses_level: - yield gen_part() - flow_indent = None - - # Check lines for functions/classes and split the code there. - if flow_indent is None: - m = self._keyword_re.match(l) - if m: - # Strip whitespace and colon from flows as a check. - if m.group(1).strip(' \t\r\n:') in FLOWS: - if not parentheses_level: - flow_indent = indent - else: - if not is_decorator and not just_newlines(current_lines): - yield gen_part() - is_decorator = '@' == m.group(1) - if not is_decorator: - parentheses_level = 0 - # The new indent needs to be higher - indent_list.append(indent + 1) - new_indent = True - elif is_decorator: - is_decorator = False - - parentheses_level = \ - max(0, (l.count('(') + l.count('[') + l.count('{') - - l.count(')') - l.count(']') - l.count('}'))) - - current_lines.append(l) - if current_lines: - yield gen_part() - - def _parse(self, source): - """ :type source: str """ - added_newline = False - if not source or source[-1] != '\n': - # To be compatible with Pythons grammar, we need a newline at the - # end. The parser would handle it, but since the fast parser abuses - # the normal parser in various ways, we need to care for this - # ourselves. - source += '\n' - added_newline = True - - next_line_offset = line_offset = 0 - start = 0 - nodes = list(self.current_node.all_sub_nodes()) - # Now we can reset the node, because we have all the old nodes. - self.current_node.reset_node() - last_end_line = 1 - - for code_part in self._split_parts(source): - next_line_offset += code_part.count('\n') - # If the last code part parsed isn't equal to the current end_pos, - # we know that the parser went further (`def` start in a - # docstring). So just parse the next part. - if line_offset + 1 == last_end_line: - self.current_node = self._get_node(code_part, source[start:], - line_offset, nodes) - else: - # Means that some lines where not fully parsed. Parse it now. - # This is a very rare case. Should only happens with very - # strange code bits. - self.number_of_misses += 1 - while last_end_line < next_line_offset + 1: - line_offset = last_end_line - 1 - # We could calculate the src in a more complicated way to - # make caching here possible as well. However, this is - # complicated and error-prone. Since this is not very often - # called - just ignore it. - src = ''.join(self._lines[line_offset:]) - self.current_node = self._get_node(code_part, src, - line_offset, nodes) - last_end_line = self.current_node.parser.module.end_pos[0] - - debug.dbg('While parsing %s, line %s slowed down the fast parser.', - self.module_path, line_offset + 1) - - line_offset = next_line_offset - start += len(code_part) - - last_end_line = self.current_node.parser.module.end_pos[0] - - if added_newline: - self.current_node.remove_last_newline() - - # Now that the for loop is finished, we still want to close all nodes. - self.current_node = self.current_node.parent_until_indent() - self.current_node.close() - - debug.dbg('Parsed %s, with %s parsers in %s splits.' - % (self.module_path, self.number_parsers_used, - self.number_of_splits)) - - def _get_node(self, source, parser_code, line_offset, nodes): - """ - Side effect: Alters the list of nodes. - """ - indent = len(source) - len(source.lstrip('\t ')) - self.current_node = self.current_node.parent_until_indent(indent) - - h = hash(source) - for index, node in enumerate(nodes): - if node.hash == h and node.source == source: - node.reset_node() - nodes.remove(node) - break - else: - tokenizer = FastTokenizer(parser_code) - self.number_parsers_used += 1 - p = Parser(self._grammar, parser_code, self.module_path, tokenizer=tokenizer) - - end = line_offset + p.module.end_pos[0] - used_lines = self._lines[line_offset:end - 1] - code_part_actually_used = ''.join(used_lines) - - node = ParserNode(self.module, p, code_part_actually_used) - - self.current_node.add_node(node, line_offset) - return node - - -class FastTokenizer(object): - """ - Breaks when certain conditions are met, i.e. a new function or class opens. - """ - def __init__(self, source): - self.source = source - self._gen = source_tokens(source) - self._closed = False - - # fast parser options - self.current = self.previous = NEWLINE, '', (0, 0) - self._in_flow = False - self._is_decorator = False - self._first_stmt = True - self._parentheses_level = 0 - self._indent_counter = 0 - self._flow_indent_counter = 0 - self._returned_endmarker = False - self._expect_indent = False - - def __iter__(self): - return self - - def next(self): - """ Python 2 Compatibility """ - return self.__next__() - - def __next__(self): - if self._closed: - return self._finish_dedents() - - typ, value, start_pos, prefix = current = next(self._gen) - if typ == ENDMARKER: - self._closed = True - self._returned_endmarker = True - return current - - self.previous = self.current - self.current = current - - if typ == INDENT: - self._indent_counter += 1 - if not self._expect_indent and not self._first_stmt and not self._in_flow: - # This does not mean that there is an actual flow, it means - # that the INDENT is syntactically wrong. - self._flow_indent_counter = self._indent_counter - 1 - self._in_flow = True - self._expect_indent = False - elif typ == DEDENT: - self._indent_counter -= 1 - if self._in_flow: - if self._indent_counter == self._flow_indent_counter: - self._in_flow = False - else: - self._closed = True - return current - - if value in ('def', 'class') and self._parentheses_level \ - and re.search(r'\n[ \t]*\Z', prefix): - # Account for the fact that an open parentheses before a function - # will reset the parentheses counter, but new lines before will - # still be ignored. So check the prefix. - - # TODO what about flow parentheses counter resets in the tokenizer? - self._parentheses_level = 0 - return self._close() - - # Parentheses ignore the indentation rules. The other three stand for - # new lines. - if self.previous[0] in (NEWLINE, INDENT, DEDENT) \ - and not self._parentheses_level and typ not in (INDENT, DEDENT): - if not self._in_flow: - if value in FLOWS: - self._flow_indent_counter = self._indent_counter - self._first_stmt = False - elif value in ('def', 'class', '@'): - # The values here are exactly the same check as in - # _split_parts, but this time with tokenize and therefore - # precise. - if not self._first_stmt and not self._is_decorator: - return self._close() - - self._is_decorator = '@' == value - if not self._is_decorator: - self._first_stmt = False - self._expect_indent = True - elif self._expect_indent: - return self._close() - else: - self._first_stmt = False - - if value in '([{' and value: - self._parentheses_level += 1 - elif value in ')]}' and value: - # Ignore closing parentheses, because they are all - # irrelevant for the indentation. - self._parentheses_level = max(self._parentheses_level - 1, 0) - return current - - def _close(self): - if self._first_stmt: - # Continue like nothing has happened, because we want to enter - # the first class/function. - if self.current[1] != '@': - self._first_stmt = False - return self.current - else: - self._closed = True - return self._finish_dedents() - - def _finish_dedents(self): - if self._indent_counter: - self._indent_counter -= 1 - return DEDENT, '', self.current[2], '' - elif not self._returned_endmarker: - self._returned_endmarker = True - return ENDMARKER, '', self.current[2], self._get_prefix() - else: - raise StopIteration - - def _get_prefix(self): - """ - We're using the current prefix for the endmarker to not loose any - information. However we care about "lost" lines. The prefix of the - current line (indent) will always be included in the current line. - """ - cur = self.current - while cur[0] == DEDENT: - cur = next(self._gen) - prefix = cur[3] - - # \Z for the end of the string. $ is bugged, because it has the - # same behavior with or without re.MULTILINE. - return re.sub(r'[^\n]+\Z', '', prefix) diff --git a/pythonFiles/jedi/parser/grammar2.7.txt b/pythonFiles/jedi/parser/grammar2.7.txt deleted file mode 100644 index b29501436b5c..000000000000 --- a/pythonFiles/jedi/parser/grammar2.7.txt +++ /dev/null @@ -1,152 +0,0 @@ -# Grammar for 2to3. This grammar supports Python 2.x and 3.x. - -# Note: Changing the grammar specified in this file will most likely -# require corresponding changes in the parser module -# (../Modules/parsermodule.c). If you can't make the changes to -# that module yourself, please co-ordinate the required changes -# with someone who can; ask around on python-dev for help. Fred -# Drake will probably be listening there. - -# NOTE WELL: You should also follow all the steps listed in PEP 306, -# "How to Change Python's Grammar" - - -# Start symbols for the grammar: -# file_input is a module or sequence of commands read from an input file; -# single_input is a single interactive statement; -# eval_input is the input for the eval() and input() functions. -# NB: compound_stmt in single_input is followed by extra NEWLINE! -file_input: (NEWLINE | stmt)* ENDMARKER -single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE -eval_input: testlist NEWLINE* ENDMARKER - -decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE -decorators: decorator+ -decorated: decorators (classdef | funcdef) -funcdef: 'def' NAME parameters ['->' test] ':' suite -parameters: '(' [typedargslist] ')' -typedargslist: ((tfpdef ['=' test] ',')* - ('*' [tname] (',' tname ['=' test])* [',' '**' tname] | '**' tname) - | tfpdef ['=' test] (',' tfpdef ['=' test])* [',']) -tname: NAME [':' test] -tfpdef: tname | '(' tfplist ')' -tfplist: tfpdef (',' tfpdef)* [','] -varargslist: ((vfpdef ['=' test] ',')* - ('*' [vname] (',' vname ['=' test])* [',' '**' vname] | '**' vname) - | vfpdef ['=' test] (',' vfpdef ['=' test])* [',']) -vname: NAME -vfpdef: vname | '(' vfplist ')' -vfplist: vfpdef (',' vfpdef)* [','] - -stmt: simple_stmt | compound_stmt -simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE -small_stmt: (expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt | - import_stmt | global_stmt | exec_stmt | assert_stmt) -expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) | - ('=' (yield_expr|testlist_star_expr))*) -testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [','] -augassign: ('+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | - '<<=' | '>>=' | '**=' | '//=') -# For normal assignments, additional restrictions enforced by the interpreter -print_stmt: 'print' ( [ test (',' test)* [','] ] | - '>>' test [ (',' test)+ [','] ] ) -del_stmt: 'del' exprlist -pass_stmt: 'pass' -flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt -break_stmt: 'break' -continue_stmt: 'continue' -return_stmt: 'return' [testlist] -yield_stmt: yield_expr -raise_stmt: 'raise' [test ['from' test | ',' test [',' test]]] -import_stmt: import_name | import_from -import_name: 'import' dotted_as_names -# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS -import_from: ('from' (('.' | '...')* dotted_name | ('.' | '...')+) - 'import' ('*' | '(' import_as_names ')' | import_as_names)) -import_as_name: NAME ['as' NAME] -dotted_as_name: dotted_name ['as' NAME] -import_as_names: import_as_name (',' import_as_name)* [','] -dotted_as_names: dotted_as_name (',' dotted_as_name)* -dotted_name: NAME ('.' NAME)* -global_stmt: ('global' | 'nonlocal') NAME (',' NAME)* -exec_stmt: 'exec' expr ['in' test [',' test]] -assert_stmt: 'assert' test [',' test] - -compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated -if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite] -while_stmt: 'while' test ':' suite ['else' ':' suite] -for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] -try_stmt: ('try' ':' suite - ((except_clause ':' suite)+ - ['else' ':' suite] - ['finally' ':' suite] | - 'finally' ':' suite)) -with_stmt: 'with' with_item (',' with_item)* ':' suite -with_item: test ['as' expr] -with_var: 'as' expr -# NB compile.c makes sure that the default except clause is last -except_clause: 'except' [test [(',' | 'as') test]] -# Edit by David Halter: The stmt is now optional. This reflects how Jedi allows -# classes and functions to be empty, which is beneficial for autocompletion. -suite: simple_stmt | NEWLINE INDENT stmt* DEDENT - -# Backward compatibility cruft to support: -# [ x for x in lambda: True, lambda: False if x() ] -# even while also allowing: -# lambda x: 5 if x else 2 -# (But not a mix of the two) -testlist_safe: old_test [(',' old_test)+ [',']] -old_test: or_test | old_lambdef -old_lambdef: 'lambda' [varargslist] ':' old_test - -test: or_test ['if' or_test 'else' test] | lambdef -or_test: and_test ('or' and_test)* -and_test: not_test ('and' not_test)* -not_test: 'not' not_test | comparison -comparison: expr (comp_op expr)* -comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not' -star_expr: '*' expr -expr: xor_expr ('|' xor_expr)* -xor_expr: and_expr ('^' and_expr)* -and_expr: shift_expr ('&' shift_expr)* -shift_expr: arith_expr (('<<'|'>>') arith_expr)* -arith_expr: term (('+'|'-') term)* -term: factor (('*'|'/'|'%'|'//') factor)* -factor: ('+'|'-'|'~') factor | power -power: atom trailer* ['**' factor] -atom: ('(' [yield_expr|testlist_comp] ')' | - '[' [testlist_comp] ']' | - '{' [dictorsetmaker] '}' | - '`' testlist1 '`' | - NAME | NUMBER | STRING+ | '.' '.' '.') -# Modification by David Halter, remove `testlist_gexp` and `listmaker` -testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) -lambdef: 'lambda' [varargslist] ':' test -trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME -subscriptlist: subscript (',' subscript)* [','] -subscript: test | [test] ':' [test] [sliceop] -sliceop: ':' [test] -exprlist: (expr|star_expr) (',' (expr|star_expr))* [','] -testlist: test (',' test)* [','] -# Modification by David Halter, dictsetmaker -> dictorsetmaker (so that it's -# the same as in the 3.4 grammar). -dictorsetmaker: ( (test ':' test (comp_for | (',' test ':' test)* [','])) | - (test (comp_for | (',' test)* [','])) ) - -classdef: 'class' NAME ['(' [arglist] ')'] ':' suite - -arglist: (argument ',')* (argument [','] - |'*' test (',' argument)* [',' '**' test] - |'**' test) -argument: test [comp_for] | test '=' test # Really [keyword '='] test - -comp_iter: comp_for | comp_if -comp_for: 'for' exprlist 'in' testlist_safe [comp_iter] -comp_if: 'if' old_test [comp_iter] - -testlist1: test (',' test)* - -# not used in grammar, but may appear in "node" passed from Parser to Compiler -encoding_decl: NAME - -yield_expr: 'yield' [testlist] diff --git a/pythonFiles/jedi/parser/grammar3.4.txt b/pythonFiles/jedi/parser/grammar3.4.txt deleted file mode 100644 index d4a32b8e4ee8..000000000000 --- a/pythonFiles/jedi/parser/grammar3.4.txt +++ /dev/null @@ -1,135 +0,0 @@ -# Grammar for Python - -# Note: Changing the grammar specified in this file will most likely -# require corresponding changes in the parser module -# (../Modules/parsermodule.c). If you can't make the changes to -# that module yourself, please co-ordinate the required changes -# with someone who can; ask around on python-dev for help. Fred -# Drake will probably be listening there. - -# NOTE WELL: You should also follow all the steps listed in PEP 306, -# "How to Change Python's Grammar" - -# Start symbols for the grammar: -# single_input is a single interactive statement; -# file_input is a module or sequence of commands read from an input file; -# eval_input is the input for the eval() functions. -# NB: compound_stmt in single_input is followed by extra NEWLINE! -file_input: (NEWLINE | stmt)* ENDMARKER -single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE -eval_input: testlist NEWLINE* ENDMARKER - -decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE -decorators: decorator+ -decorated: decorators (classdef | funcdef) -funcdef: 'def' NAME parameters ['->' test] ':' suite -parameters: '(' [typedargslist] ')' -typedargslist: (tfpdef ['=' test] (',' tfpdef ['=' test])* [',' - ['*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef]] - | '*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef) -tfpdef: NAME [':' test] -varargslist: (vfpdef ['=' test] (',' vfpdef ['=' test])* [',' - ['*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef]] - | '*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef) -vfpdef: NAME - -stmt: simple_stmt | compound_stmt -simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE -small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt | - import_stmt | global_stmt | nonlocal_stmt | assert_stmt) -expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) | - ('=' (yield_expr|testlist_star_expr))*) -testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [','] -augassign: ('+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | - '<<=' | '>>=' | '**=' | '//=') -# For normal assignments, additional restrictions enforced by the interpreter -del_stmt: 'del' exprlist -pass_stmt: 'pass' -flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt -break_stmt: 'break' -continue_stmt: 'continue' -return_stmt: 'return' [testlist] -yield_stmt: yield_expr -raise_stmt: 'raise' [test ['from' test]] -import_stmt: import_name | import_from -import_name: 'import' dotted_as_names -# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS -import_from: ('from' (('.' | '...')* dotted_name | ('.' | '...')+) - 'import' ('*' | '(' import_as_names ')' | import_as_names)) -import_as_name: NAME ['as' NAME] -dotted_as_name: dotted_name ['as' NAME] -import_as_names: import_as_name (',' import_as_name)* [','] -dotted_as_names: dotted_as_name (',' dotted_as_name)* -dotted_name: NAME ('.' NAME)* -global_stmt: 'global' NAME (',' NAME)* -nonlocal_stmt: 'nonlocal' NAME (',' NAME)* -assert_stmt: 'assert' test [',' test] - -compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated -if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite] -while_stmt: 'while' test ':' suite ['else' ':' suite] -for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] -try_stmt: ('try' ':' suite - ((except_clause ':' suite)+ - ['else' ':' suite] - ['finally' ':' suite] | - 'finally' ':' suite)) -with_stmt: 'with' with_item (',' with_item)* ':' suite -with_item: test ['as' expr] -# NB compile.c makes sure that the default except clause is last -except_clause: 'except' [test ['as' NAME]] -# Edit by David Halter: The stmt is now optional. This reflects how Jedi allows -# classes and functions to be empty, which is beneficial for autocompletion. -suite: simple_stmt | NEWLINE INDENT stmt* DEDENT - -test: or_test ['if' or_test 'else' test] | lambdef -test_nocond: or_test | lambdef_nocond -lambdef: 'lambda' [varargslist] ':' test -lambdef_nocond: 'lambda' [varargslist] ':' test_nocond -or_test: and_test ('or' and_test)* -and_test: not_test ('and' not_test)* -not_test: 'not' not_test | comparison -comparison: expr (comp_op expr)* -# <> isn't actually a valid comparison operator in Python. It's here for the -# sake of a __future__ import described in PEP 401 -comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not' -star_expr: '*' expr -expr: xor_expr ('|' xor_expr)* -xor_expr: and_expr ('^' and_expr)* -and_expr: shift_expr ('&' shift_expr)* -shift_expr: arith_expr (('<<'|'>>') arith_expr)* -arith_expr: term (('+'|'-') term)* -term: factor (('*'|'/'|'%'|'//') factor)* -factor: ('+'|'-'|'~') factor | power -power: atom trailer* ['**' factor] -atom: ('(' [yield_expr|testlist_comp] ')' | - '[' [testlist_comp] ']' | - '{' [dictorsetmaker] '}' | - NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False') -testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] ) -trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME -subscriptlist: subscript (',' subscript)* [','] -subscript: test | [test] ':' [test] [sliceop] -sliceop: ':' [test] -exprlist: (expr|star_expr) (',' (expr|star_expr))* [','] -testlist: test (',' test)* [','] -dictorsetmaker: ( (test ':' test (comp_for | (',' test ':' test)* [','])) | - (test (comp_for | (',' test)* [','])) ) - -classdef: 'class' NAME ['(' [arglist] ')'] ':' suite - -arglist: (argument ',')* (argument [','] - |'*' test (',' argument)* [',' '**' test] - |'**' test) -# The reason that keywords are test nodes instead of NAME is that using NAME -# results in an ambiguity. ast.c makes sure it's a NAME. -argument: test [comp_for] | test '=' test # Really [keyword '='] test -comp_iter: comp_for | comp_if -comp_for: 'for' exprlist 'in' or_test [comp_iter] -comp_if: 'if' test_nocond [comp_iter] - -# not used in grammar, but may appear in "node" passed from Parser to Compiler -encoding_decl: NAME - -yield_expr: 'yield' [yield_arg] -yield_arg: 'from' test | testlist diff --git a/pythonFiles/jedi/parser/pgen2/__init__.py b/pythonFiles/jedi/parser/pgen2/__init__.py deleted file mode 100644 index 1ddae5fea9f7..000000000000 --- a/pythonFiles/jedi/parser/pgen2/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved. -# Licensed to PSF under a Contributor Agreement. - -# Modifications: -# Copyright 2006 Google, Inc. All Rights Reserved. -# Licensed to PSF under a Contributor Agreement. -# Copyright 2014 David Halter. Integration into Jedi. -# Modifications are dual-licensed: MIT and PSF. diff --git a/pythonFiles/jedi/parser/pgen2/grammar.py b/pythonFiles/jedi/parser/pgen2/grammar.py deleted file mode 100644 index 414c0dbe9f01..000000000000 --- a/pythonFiles/jedi/parser/pgen2/grammar.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved. -# Licensed to PSF under a Contributor Agreement. - -# Modifications: -# Copyright 2014 David Halter. Integration into Jedi. -# Modifications are dual-licensed: MIT and PSF. - -"""This module defines the data structures used to represent a grammar. - -These are a bit arcane because they are derived from the data -structures used by Python's 'pgen' parser generator. - -There's also a table here mapping operators to their names in the -token module; the Python tokenize module reports all operators as the -fallback token code OP, but the parser needs the actual token code. - -""" - -# Python imports -import pickle - - -class Grammar(object): - """Pgen parsing tables conversion class. - - Once initialized, this class supplies the grammar tables for the - parsing engine implemented by parse.py. The parsing engine - accesses the instance variables directly. The class here does not - provide initialization of the tables; several subclasses exist to - do this (see the conv and pgen modules). - - The load() method reads the tables from a pickle file, which is - much faster than the other ways offered by subclasses. The pickle - file is written by calling dump() (after loading the grammar - tables using a subclass). The report() method prints a readable - representation of the tables to stdout, for debugging. - - The instance variables are as follows: - - symbol2number -- a dict mapping symbol names to numbers. Symbol - numbers are always 256 or higher, to distinguish - them from token numbers, which are between 0 and - 255 (inclusive). - - number2symbol -- a dict mapping numbers to symbol names; - these two are each other's inverse. - - states -- a list of DFAs, where each DFA is a list of - states, each state is a list of arcs, and each - arc is a (i, j) pair where i is a label and j is - a state number. The DFA number is the index into - this list. (This name is slightly confusing.) - Final states are represented by a special arc of - the form (0, j) where j is its own state number. - - dfas -- a dict mapping symbol numbers to (DFA, first) - pairs, where DFA is an item from the states list - above, and first is a set of tokens that can - begin this grammar rule (represented by a dict - whose values are always 1). - - labels -- a list of (x, y) pairs where x is either a token - number or a symbol number, and y is either None - or a string; the strings are keywords. The label - number is the index in this list; label numbers - are used to mark state transitions (arcs) in the - DFAs. - - start -- the number of the grammar's start symbol. - - keywords -- a dict mapping keyword strings to arc labels. - - tokens -- a dict mapping token numbers to arc labels. - - """ - - def __init__(self): - self.symbol2number = {} - self.number2symbol = {} - self.states = [] - self.dfas = {} - self.labels = [(0, "EMPTY")] - self.keywords = {} - self.tokens = {} - self.symbol2label = {} - self.start = 256 - - def dump(self, filename): - """Dump the grammar tables to a pickle file.""" - with open(filename, "wb") as f: - pickle.dump(self.__dict__, f, 2) - - def load(self, filename): - """Load the grammar tables from a pickle file.""" - with open(filename, "rb") as f: - d = pickle.load(f) - self.__dict__.update(d) - - def copy(self): - """ - Copy the grammar. - """ - new = self.__class__() - for dict_attr in ("symbol2number", "number2symbol", "dfas", "keywords", - "tokens", "symbol2label"): - setattr(new, dict_attr, getattr(self, dict_attr).copy()) - new.labels = self.labels[:] - new.states = self.states[:] - new.start = self.start - return new - - def report(self): - """Dump the grammar tables to standard output, for debugging.""" - from pprint import pprint - print("s2n") - pprint(self.symbol2number) - print("n2s") - pprint(self.number2symbol) - print("states") - pprint(self.states) - print("dfas") - pprint(self.dfas) - print("labels") - pprint(self.labels) - print("start", self.start) diff --git a/pythonFiles/jedi/parser/pgen2/parse.py b/pythonFiles/jedi/parser/pgen2/parse.py deleted file mode 100644 index c8ba70d356be..000000000000 --- a/pythonFiles/jedi/parser/pgen2/parse.py +++ /dev/null @@ -1,205 +0,0 @@ -# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved. -# Licensed to PSF under a Contributor Agreement. - -# Modifications: -# Copyright 2014 David Halter. Integration into Jedi. -# Modifications are dual-licensed: MIT and PSF. - -""" -Parser engine for the grammar tables generated by pgen. - -The grammar table must be loaded first. - -See Parser/parser.c in the Python distribution for additional info on -how this parsing engine works. -""" - -# Local imports -from jedi.parser import tokenize - - -class ParseError(Exception): - """Exception to signal the parser is stuck.""" - - def __init__(self, msg, type, value, start_pos): - Exception.__init__(self, "%s: type=%r, value=%r, start_pos=%r" % - (msg, tokenize.tok_name[type], value, start_pos)) - self.msg = msg - self.type = type - self.value = value - self.start_pos = start_pos - - -class PgenParser(object): - """Parser engine. - - The proper usage sequence is: - - p = Parser(grammar, [converter]) # create instance - p.setup([start]) # prepare for parsing - : - if p.addtoken(...): # parse a token; may raise ParseError - break - root = p.rootnode # root of abstract syntax tree - - A Parser instance may be reused by calling setup() repeatedly. - - A Parser instance contains state pertaining to the current token - sequence, and should not be used concurrently by different threads - to parse separate token sequences. - - See driver.py for how to get input tokens by tokenizing a file or - string. - - Parsing is complete when addtoken() returns True; the root of the - abstract syntax tree can then be retrieved from the rootnode - instance variable. When a syntax error occurs, addtoken() raises - the ParseError exception. There is no error recovery; the parser - cannot be used after a syntax error was reported (but it can be - reinitialized by calling setup()). - - """ - - def __init__(self, grammar, convert_node, convert_leaf, error_recovery): - """Constructor. - - The grammar argument is a grammar.Grammar instance; see the - grammar module for more information. - - The parser is not ready yet for parsing; you must call the - setup() method to get it started. - - The optional convert argument is a function mapping concrete - syntax tree nodes to abstract syntax tree nodes. If not - given, no conversion is done and the syntax tree produced is - the concrete syntax tree. If given, it must be a function of - two arguments, the first being the grammar (a grammar.Grammar - instance), and the second being the concrete syntax tree node - to be converted. The syntax tree is converted from the bottom - up. - - A concrete syntax tree node is a (type, nodes) tuple, where - type is the node type (a token or symbol number) and nodes - is a list of children for symbols, and None for tokens. - - An abstract syntax tree node may be anything; this is entirely - up to the converter function. - - """ - self.grammar = grammar - self.convert_node = convert_node - self.convert_leaf = convert_leaf - - # Prepare for parsing. - start = self.grammar.start - # Each stack entry is a tuple: (dfa, state, node). - # A node is a tuple: (type, children), - # where children is a list of nodes or None - newnode = (start, []) - stackentry = (self.grammar.dfas[start], 0, newnode) - self.stack = [stackentry] - self.rootnode = None - self.error_recovery = error_recovery - - def parse(self, tokenizer): - for type, value, prefix, start_pos in tokenizer: - if self.addtoken(type, value, prefix, start_pos): - break - else: - # We never broke out -- EOF is too soon -- Unfinished statement. - self.error_recovery(self.grammar, self.stack, type, value, - start_pos, prefix, self.addtoken) - # Add the ENDMARKER again. - if not self.addtoken(type, value, prefix, start_pos): - raise ParseError("incomplete input", type, value, start_pos) - return self.rootnode - - def addtoken(self, type, value, prefix, start_pos): - """Add a token; return True if this is the end of the program.""" - # Map from token to label - if type == tokenize.NAME: - # Check for reserved words (keywords) - try: - ilabel = self.grammar.keywords[value] - except KeyError: - ilabel = self.grammar.tokens[type] - else: - ilabel = self.grammar.tokens[type] - - # Loop until the token is shifted; may raise exceptions - while True: - dfa, state, node = self.stack[-1] - states, first = dfa - arcs = states[state] - # Look for a state with this label - for i, newstate in arcs: - t, v = self.grammar.labels[i] - if ilabel == i: - # Look it up in the list of labels - assert t < 256 - # Shift a token; we're done with it - self.shift(type, value, newstate, prefix, start_pos) - # Pop while we are in an accept-only state - state = newstate - while states[state] == [(0, state)]: - self.pop() - if not self.stack: - # Done parsing! - return True - dfa, state, node = self.stack[-1] - states, first = dfa - # Done with this token - return False - elif t >= 256: - # See if it's a symbol and if we're in its first set - itsdfa = self.grammar.dfas[t] - itsstates, itsfirst = itsdfa - if ilabel in itsfirst: - # Push a symbol - self.push(t, itsdfa, newstate) - break # To continue the outer while loop - else: - if (0, state) in arcs: - # An accepting state, pop it and try something else - self.pop() - if not self.stack: - # Done parsing, but another token is input - raise ParseError("too much input", type, value, start_pos) - else: - self.error_recovery(self.grammar, self.stack, type, - value, start_pos, prefix, self.addtoken) - break - - def shift(self, type, value, newstate, prefix, start_pos): - """Shift a token. (Internal)""" - dfa, state, node = self.stack[-1] - newnode = self.convert_leaf(self.grammar, type, value, prefix, start_pos) - node[-1].append(newnode) - self.stack[-1] = (dfa, newstate, node) - - def push(self, type, newdfa, newstate): - """Push a nonterminal. (Internal)""" - dfa, state, node = self.stack[-1] - newnode = (type, []) - self.stack[-1] = (dfa, newstate, node) - self.stack.append((newdfa, 0, newnode)) - - def pop(self): - """Pop a nonterminal. (Internal)""" - popdfa, popstate, (type, children) = self.stack.pop() - # If there's exactly one child, return that child instead of creating a - # new node. We still create expr_stmt and file_input though, because a - # lot of Jedi depends on its logic. - if len(children) == 1: - newnode = children[0] - else: - newnode = self.convert_node(self.grammar, type, children) - - try: - # Equal to: - # dfa, state, node = self.stack[-1] - # symbol, children = node - self.stack[-1][2][1].append(newnode) - except IndexError: - # Stack is empty, set the rootnode. - self.rootnode = newnode diff --git a/pythonFiles/jedi/parser/pgen2/pgen.py b/pythonFiles/jedi/parser/pgen2/pgen.py deleted file mode 100644 index fa2742dd5dc4..000000000000 --- a/pythonFiles/jedi/parser/pgen2/pgen.py +++ /dev/null @@ -1,394 +0,0 @@ -# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved. -# Licensed to PSF under a Contributor Agreement. - -# Modifications: -# Copyright 2014 David Halter. Integration into Jedi. -# Modifications are dual-licensed: MIT and PSF. - -# Pgen imports -from . import grammar -from jedi.parser import token -from jedi.parser import tokenize - - -class ParserGenerator(object): - def __init__(self, filename, stream=None): - close_stream = None - if stream is None: - stream = open(filename) - close_stream = stream.close - self.filename = filename - self.stream = stream - self.generator = tokenize.generate_tokens(stream.readline) - self.gettoken() # Initialize lookahead - self.dfas, self.startsymbol = self.parse() - if close_stream is not None: - close_stream() - self.first = {} # map from symbol name to set of tokens - self.addfirstsets() - - def make_grammar(self): - c = grammar.Grammar() - names = list(self.dfas.keys()) - names.sort() - names.remove(self.startsymbol) - names.insert(0, self.startsymbol) - for name in names: - i = 256 + len(c.symbol2number) - c.symbol2number[name] = i - c.number2symbol[i] = name - for name in names: - dfa = self.dfas[name] - states = [] - for state in dfa: - arcs = [] - for label, next in state.arcs.items(): - arcs.append((self.make_label(c, label), dfa.index(next))) - if state.isfinal: - arcs.append((0, dfa.index(state))) - states.append(arcs) - c.states.append(states) - c.dfas[c.symbol2number[name]] = (states, self.make_first(c, name)) - c.start = c.symbol2number[self.startsymbol] - return c - - def make_first(self, c, name): - rawfirst = self.first[name] - first = {} - for label in rawfirst: - ilabel = self.make_label(c, label) - ##assert ilabel not in first # XXX failed on <> ... != - first[ilabel] = 1 - return first - - def make_label(self, c, label): - # XXX Maybe this should be a method on a subclass of converter? - ilabel = len(c.labels) - if label[0].isalpha(): - # Either a symbol name or a named token - if label in c.symbol2number: - # A symbol name (a non-terminal) - if label in c.symbol2label: - return c.symbol2label[label] - else: - c.labels.append((c.symbol2number[label], None)) - c.symbol2label[label] = ilabel - return ilabel - else: - # A named token (NAME, NUMBER, STRING) - itoken = getattr(token, label, None) - assert isinstance(itoken, int), label - assert itoken in token.tok_name, label - if itoken in c.tokens: - return c.tokens[itoken] - else: - c.labels.append((itoken, None)) - c.tokens[itoken] = ilabel - return ilabel - else: - # Either a keyword or an operator - assert label[0] in ('"', "'"), label - value = eval(label) - if value[0].isalpha(): - # A keyword - if value in c.keywords: - return c.keywords[value] - else: - c.labels.append((token.NAME, value)) - c.keywords[value] = ilabel - return ilabel - else: - # An operator (any non-numeric token) - itoken = token.opmap[value] # Fails if unknown token - if itoken in c.tokens: - return c.tokens[itoken] - else: - c.labels.append((itoken, None)) - c.tokens[itoken] = ilabel - return ilabel - - def addfirstsets(self): - names = list(self.dfas.keys()) - names.sort() - for name in names: - if name not in self.first: - self.calcfirst(name) - #print name, self.first[name].keys() - - def calcfirst(self, name): - dfa = self.dfas[name] - self.first[name] = None # dummy to detect left recursion - state = dfa[0] - totalset = {} - overlapcheck = {} - for label, next in state.arcs.items(): - if label in self.dfas: - if label in self.first: - fset = self.first[label] - if fset is None: - raise ValueError("recursion for rule %r" % name) - else: - self.calcfirst(label) - fset = self.first[label] - totalset.update(fset) - overlapcheck[label] = fset - else: - totalset[label] = 1 - overlapcheck[label] = {label: 1} - inverse = {} - for label, itsfirst in overlapcheck.items(): - for symbol in itsfirst: - if symbol in inverse: - raise ValueError("rule %s is ambiguous; %s is in the" - " first sets of %s as well as %s" % - (name, symbol, label, inverse[symbol])) - inverse[symbol] = label - self.first[name] = totalset - - def parse(self): - dfas = {} - startsymbol = None - # MSTART: (NEWLINE | RULE)* ENDMARKER - while self.type != token.ENDMARKER: - while self.type == token.NEWLINE: - self.gettoken() - # RULE: NAME ':' RHS NEWLINE - name = self.expect(token.NAME) - self.expect(token.OP, ":") - a, z = self.parse_rhs() - self.expect(token.NEWLINE) - #self.dump_nfa(name, a, z) - dfa = self.make_dfa(a, z) - #self.dump_dfa(name, dfa) - # oldlen = len(dfa) - self.simplify_dfa(dfa) - # newlen = len(dfa) - dfas[name] = dfa - #print name, oldlen, newlen - if startsymbol is None: - startsymbol = name - return dfas, startsymbol - - def make_dfa(self, start, finish): - # To turn an NFA into a DFA, we define the states of the DFA - # to correspond to *sets* of states of the NFA. Then do some - # state reduction. Let's represent sets as dicts with 1 for - # values. - assert isinstance(start, NFAState) - assert isinstance(finish, NFAState) - - def closure(state): - base = {} - addclosure(state, base) - return base - - def addclosure(state, base): - assert isinstance(state, NFAState) - if state in base: - return - base[state] = 1 - for label, next in state.arcs: - if label is None: - addclosure(next, base) - - states = [DFAState(closure(start), finish)] - for state in states: # NB states grows while we're iterating - arcs = {} - for nfastate in state.nfaset: - for label, next in nfastate.arcs: - if label is not None: - addclosure(next, arcs.setdefault(label, {})) - for label, nfaset in arcs.items(): - for st in states: - if st.nfaset == nfaset: - break - else: - st = DFAState(nfaset, finish) - states.append(st) - state.addarc(st, label) - return states # List of DFAState instances; first one is start - - def dump_nfa(self, name, start, finish): - print("Dump of NFA for", name) - todo = [start] - for i, state in enumerate(todo): - print(" State", i, state is finish and "(final)" or "") - for label, next in state.arcs: - if next in todo: - j = todo.index(next) - else: - j = len(todo) - todo.append(next) - if label is None: - print(" -> %d" % j) - else: - print(" %s -> %d" % (label, j)) - - def dump_dfa(self, name, dfa): - print("Dump of DFA for", name) - for i, state in enumerate(dfa): - print(" State", i, state.isfinal and "(final)" or "") - for label, next in state.arcs.items(): - print(" %s -> %d" % (label, dfa.index(next))) - - def simplify_dfa(self, dfa): - # This is not theoretically optimal, but works well enough. - # Algorithm: repeatedly look for two states that have the same - # set of arcs (same labels pointing to the same nodes) and - # unify them, until things stop changing. - - # dfa is a list of DFAState instances - changes = True - while changes: - changes = False - for i, state_i in enumerate(dfa): - for j in range(i + 1, len(dfa)): - state_j = dfa[j] - if state_i == state_j: - #print " unify", i, j - del dfa[j] - for state in dfa: - state.unifystate(state_j, state_i) - changes = True - break - - def parse_rhs(self): - # RHS: ALT ('|' ALT)* - a, z = self.parse_alt() - if self.value != "|": - return a, z - else: - aa = NFAState() - zz = NFAState() - aa.addarc(a) - z.addarc(zz) - while self.value == "|": - self.gettoken() - a, z = self.parse_alt() - aa.addarc(a) - z.addarc(zz) - return aa, zz - - def parse_alt(self): - # ALT: ITEM+ - a, b = self.parse_item() - while (self.value in ("(", "[") or - self.type in (token.NAME, token.STRING)): - c, d = self.parse_item() - b.addarc(c) - b = d - return a, b - - def parse_item(self): - # ITEM: '[' RHS ']' | ATOM ['+' | '*'] - if self.value == "[": - self.gettoken() - a, z = self.parse_rhs() - self.expect(token.OP, "]") - a.addarc(z) - return a, z - else: - a, z = self.parse_atom() - value = self.value - if value not in ("+", "*"): - return a, z - self.gettoken() - z.addarc(a) - if value == "+": - return a, z - else: - return a, a - - def parse_atom(self): - # ATOM: '(' RHS ')' | NAME | STRING - if self.value == "(": - self.gettoken() - a, z = self.parse_rhs() - self.expect(token.OP, ")") - return a, z - elif self.type in (token.NAME, token.STRING): - a = NFAState() - z = NFAState() - a.addarc(z, self.value) - self.gettoken() - return a, z - else: - self.raise_error("expected (...) or NAME or STRING, got %s/%s", - self.type, self.value) - - def expect(self, type, value=None): - if self.type != type or (value is not None and self.value != value): - self.raise_error("expected %s/%s, got %s/%s", - type, value, self.type, self.value) - value = self.value - self.gettoken() - return value - - def gettoken(self): - tup = next(self.generator) - while tup[0] in (token.COMMENT, token.NL): - tup = next(self.generator) - self.type, self.value, self.begin, prefix = tup - #print tokenize.tok_name[self.type], repr(self.value) - - def raise_error(self, msg, *args): - if args: - try: - msg = msg % args - except: - msg = " ".join([msg] + list(map(str, args))) - line = open(self.filename).readlines()[self.begin[0]] - raise SyntaxError(msg, (self.filename, self.begin[0], - self.begin[1], line)) - - -class NFAState(object): - def __init__(self): - self.arcs = [] # list of (label, NFAState) pairs - - def addarc(self, next, label=None): - assert label is None or isinstance(label, str) - assert isinstance(next, NFAState) - self.arcs.append((label, next)) - - -class DFAState(object): - def __init__(self, nfaset, final): - assert isinstance(nfaset, dict) - assert isinstance(next(iter(nfaset)), NFAState) - assert isinstance(final, NFAState) - self.nfaset = nfaset - self.isfinal = final in nfaset - self.arcs = {} # map from label to DFAState - - def addarc(self, next, label): - assert isinstance(label, str) - assert label not in self.arcs - assert isinstance(next, DFAState) - self.arcs[label] = next - - def unifystate(self, old, new): - for label, next in self.arcs.items(): - if next is old: - self.arcs[label] = new - - def __eq__(self, other): - # Equality test -- ignore the nfaset instance variable - assert isinstance(other, DFAState) - if self.isfinal != other.isfinal: - return False - # Can't just return self.arcs == other.arcs, because that - # would invoke this method recursively, with cycles... - if len(self.arcs) != len(other.arcs): - return False - for label, next in self.arcs.items(): - if next is not other.arcs.get(label): - return False - return True - - __hash__ = None # For Py3 compatibility. - - -def generate_grammar(filename="Grammar.txt"): - p = ParserGenerator(filename) - return p.make_grammar() diff --git a/pythonFiles/jedi/parser/token.py b/pythonFiles/jedi/parser/token.py deleted file mode 100644 index e9ab3a6272ff..000000000000 --- a/pythonFiles/jedi/parser/token.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import absolute_import - -from jedi._compatibility import is_py3 -from token import * - - -COMMENT = N_TOKENS -tok_name[COMMENT] = 'COMMENT' -N_TOKENS += 1 - -NL = N_TOKENS -tok_name[NL] = 'NL' -N_TOKENS += 1 - -if is_py3: - BACKQUOTE = N_TOKENS - tok_name[BACKQUOTE] = 'BACKQUOTE' - N_TOKENS += 1 -else: - RARROW = N_TOKENS - tok_name[RARROW] = 'RARROW' - N_TOKENS += 1 - ELLIPSIS = N_TOKENS - tok_name[ELLIPSIS] = 'ELLIPSIS' - N_TOKENS += 1 - - - -# Map from operator to number (since tokenize doesn't do this) - -opmap_raw = """\ -( LPAR -) RPAR -[ LSQB -] RSQB -: COLON -, COMMA -; SEMI -+ PLUS -- MINUS -* STAR -/ SLASH -| VBAR -& AMPER -< LESS -> GREATER -= EQUAL -. DOT -% PERCENT -` BACKQUOTE -{ LBRACE -} RBRACE -@ AT -== EQEQUAL -!= NOTEQUAL -<> NOTEQUAL -<= LESSEQUAL ->= GREATEREQUAL -~ TILDE -^ CIRCUMFLEX -<< LEFTSHIFT ->> RIGHTSHIFT -** DOUBLESTAR -+= PLUSEQUAL --= MINEQUAL -*= STAREQUAL -/= SLASHEQUAL -%= PERCENTEQUAL -&= AMPEREQUAL -|= VBAREQUAL -^= CIRCUMFLEXEQUAL -<<= LEFTSHIFTEQUAL ->>= RIGHTSHIFTEQUAL -**= DOUBLESTAREQUAL -// DOUBLESLASH -//= DOUBLESLASHEQUAL --> RARROW -... ELLIPSIS -""" - -opmap = {} -for line in opmap_raw.splitlines(): - op, name = line.split() - opmap[op] = globals()[name] diff --git a/pythonFiles/jedi/parser/tokenize.py b/pythonFiles/jedi/parser/tokenize.py deleted file mode 100644 index b38490468349..000000000000 --- a/pythonFiles/jedi/parser/tokenize.py +++ /dev/null @@ -1,290 +0,0 @@ -# -*- coding: utf-8 -*- -""" -This tokenizer has been copied from the ``tokenize.py`` standard library -tokenizer. The reason was simple: The standard library tokenizer fails -if the indentation is not right. The fast parser of jedi however requires -"wrong" indentation. - -Basically this is a stripped down version of the standard library module, so -you can read the documentation there. Additionally we included some speed and -memory optimizations here. -""" -from __future__ import absolute_import - -import string -import re -from io import StringIO -from jedi.parser.token import (tok_name, N_TOKENS, ENDMARKER, STRING, NUMBER, - NAME, OP, ERRORTOKEN, NEWLINE, INDENT, DEDENT) -from jedi._compatibility import is_py3 - - -cookie_re = re.compile("coding[:=]\s*([-\w.]+)") - - -if is_py3: - # Python 3 has str.isidentifier() to check if a char is a valid identifier - is_identifier = str.isidentifier -else: - namechars = string.ascii_letters + '_' - is_identifier = lambda s: s in namechars - - -COMMENT = N_TOKENS -tok_name[COMMENT] = 'COMMENT' - - -def group(*choices): - return '(' + '|'.join(choices) + ')' - - -def maybe(*choices): - return group(*choices) + '?' - - -# Note: we use unicode matching for names ("\w") but ascii matching for -# number literals. -whitespace = r'[ \f\t]*' -comment = r'#[^\r\n]*' -name = r'\w+' - -hex_number = r'0[xX][0-9a-fA-F]+' -bin_number = r'0[bB][01]+' -oct_number = r'0[oO][0-7]+' -dec_number = r'(?:0+|[1-9][0-9]*)' -int_number = group(hex_number, bin_number, oct_number, dec_number) -exponent = r'[eE][-+]?[0-9]+' -point_float = group(r'[0-9]+\.[0-9]*', r'\.[0-9]+') + maybe(exponent) -Expfloat = r'[0-9]+' + exponent -float_number = group(point_float, Expfloat) -imag_number = group(r'[0-9]+[jJ]', float_number + r'[jJ]') -number = group(imag_number, float_number, int_number) - -# Tail end of ' string. -single = r"[^'\\]*(?:\\.[^'\\]*)*'" -# Tail end of " string. -double = r'[^"\\]*(?:\\.[^"\\]*)*"' -# Tail end of ''' string. -single3 = r"[^'\\]*(?:(?:\\.|'(?!''))[^'\\]*)*'''" -# Tail end of """ string. -double3 = r'[^"\\]*(?:(?:\\.|"(?!""))[^"\\]*)*"""' -triple = group("[uUbB]?[rR]?'''", '[uUbB]?[rR]?"""') -# Single-line ' or " string. - -# Because of leftmost-then-longest match semantics, be sure to put the -# longest operators first (e.g., if = came before ==, == would get -# recognized as two instances of =). -operator = group(r"\*\*=?", r">>=?", r"<<=?", r"!=", - r"//=?", r"->", - r"[+\-*/%&|^=<>]=?", - r"~") - -bracket = '[][(){}]' -special = group(r'\r?\n', r'\.\.\.', r'[:;.,@]') -funny = group(operator, bracket, special) - -# First (or only) line of ' or " string. -cont_str = group(r"[bBuU]?[rR]?'[^\n'\\]*(?:\\.[^\n'\\]*)*" + - group("'", r'\\\r?\n'), - r'[bBuU]?[rR]?"[^\n"\\]*(?:\\.[^\n"\\]*)*' + - group('"', r'\\\r?\n')) -pseudo_extras = group(r'\\\r?\n', comment, triple) -pseudo_token = group(whitespace) + \ - group(pseudo_extras, number, funny, cont_str, name) - - -def _compile(expr): - return re.compile(expr, re.UNICODE) - - -pseudoprog, single3prog, double3prog = map( - _compile, (pseudo_token, single3, double3)) - -endprogs = {"'": _compile(single), '"': _compile(double), - "'''": single3prog, '"""': double3prog, - "r'''": single3prog, 'r"""': double3prog, - "b'''": single3prog, 'b"""': double3prog, - "u'''": single3prog, 'u"""': double3prog, - "R'''": single3prog, 'R"""': double3prog, - "B'''": single3prog, 'B"""': double3prog, - "U'''": single3prog, 'U"""': double3prog, - "br'''": single3prog, 'br"""': double3prog, - "bR'''": single3prog, 'bR"""': double3prog, - "Br'''": single3prog, 'Br"""': double3prog, - "BR'''": single3prog, 'BR"""': double3prog, - "ur'''": single3prog, 'ur"""': double3prog, - "uR'''": single3prog, 'uR"""': double3prog, - "Ur'''": single3prog, 'Ur"""': double3prog, - "UR'''": single3prog, 'UR"""': double3prog, - 'r': None, 'R': None, 'b': None, 'B': None} - -triple_quoted = {} -for t in ("'''", '"""', - "r'''", 'r"""', "R'''", 'R"""', - "b'''", 'b"""', "B'''", 'B"""', - "u'''", 'u"""', "U'''", 'U"""', - "br'''", 'br"""', "Br'''", 'Br"""', - "bR'''", 'bR"""', "BR'''", 'BR"""', - "ur'''", 'ur"""', "Ur'''", 'Ur"""', - "uR'''", 'uR"""', "UR'''", 'UR"""'): - triple_quoted[t] = t -single_quoted = {} -for t in ("'", '"', - "r'", 'r"', "R'", 'R"', - "b'", 'b"', "B'", 'B"', - "u'", 'u"', "U'", 'U"', - "br'", 'br"', "Br'", 'Br"', - "bR'", 'bR"', "BR'", 'BR"', - "ur'", 'ur"', "Ur'", 'Ur"', - "uR'", 'uR"', "UR'", 'UR"'): - single_quoted[t] = t - -del _compile - -tabsize = 8 - -ALWAYS_BREAK_TOKENS = (';', 'import', 'from', 'class', 'def', 'try', 'except', - 'finally', 'while', 'return') - - -def source_tokens(source): - """Generate tokens from a the source code (string).""" - source = source + '\n' # end with \n, because the parser needs it - readline = StringIO(source).readline - return generate_tokens(readline) - - -def generate_tokens(readline): - """ - A heavily modified Python standard library tokenizer. - - Additionally to the default information, yields also the prefix of each - token. This idea comes from lib2to3. The prefix contains all information - that is irrelevant for the parser like newlines in parentheses or comments. - """ - paren_level = 0 # count parentheses - indents = [0] - lnum = 0 - numchars = '0123456789' - contstr = '' - contline = None - # We start with a newline. This makes indent at the first position - # possible. It's not valid Python, but still better than an INDENT in the - # second line (and not in the first). This makes quite a few things in - # Jedi's fast parser possible. - new_line = True - prefix = '' # Should never be required, but here for safety - additional_prefix = '' - while True: # loop over lines in stream - line = readline() # readline returns empty when finished. See StringIO - if not line: - if contstr: - yield ERRORTOKEN, contstr, contstr_start, prefix - break - - lnum += 1 - pos, max = 0, len(line) - - if contstr: # continued string - endmatch = endprog.match(line) - if endmatch: - pos = endmatch.end(0) - yield STRING, contstr + line[:pos], contstr_start, prefix - contstr = '' - contline = None - else: - contstr = contstr + line - contline = contline + line - continue - - while pos < max: - pseudomatch = pseudoprog.match(line, pos) - if not pseudomatch: # scan for tokens - txt = line[pos] - if line[pos] in '"\'': - # If a literal starts but doesn't end the whole rest of the - # line is an error token. - txt = line[pos:] - yield ERRORTOKEN, txt, (lnum, pos), prefix - pos += 1 - continue - - prefix = additional_prefix + pseudomatch.group(1) - additional_prefix = '' - start, pos = pseudomatch.span(2) - spos = (lnum, start) - token, initial = line[start:pos], line[start] - - if new_line and initial not in '\r\n#': - new_line = False - if paren_level == 0: - if start > indents[-1]: - yield INDENT, '', spos, '' - indents.append(start) - while start < indents[-1]: - yield DEDENT, '', spos, '' - indents.pop() - - if (initial in numchars or # ordinary number - (initial == '.' and token != '.' and token != '...')): - yield NUMBER, token, spos, prefix - elif initial in '\r\n': - if not new_line and paren_level == 0: - yield NEWLINE, token, spos, prefix - else: - additional_prefix = prefix + token - new_line = True - elif initial == '#': # Comments - assert not token.endswith("\n") - additional_prefix = prefix + token - elif token in triple_quoted: - endprog = endprogs[token] - endmatch = endprog.match(line, pos) - if endmatch: # all on one line - pos = endmatch.end(0) - token = line[start:pos] - yield STRING, token, spos, prefix - else: - contstr_start = (lnum, start) # multiple lines - contstr = line[start:] - contline = line - break - elif initial in single_quoted or \ - token[:2] in single_quoted or \ - token[:3] in single_quoted: - if token[-1] == '\n': # continued string - contstr_start = lnum, start - endprog = (endprogs.get(initial) or endprogs.get(token[1]) - or endprogs.get(token[2])) - contstr = line[start:] - contline = line - break - else: # ordinary string - yield STRING, token, spos, prefix - elif is_identifier(initial): # ordinary name - if token in ALWAYS_BREAK_TOKENS: - paren_level = 0 - while True: - indent = indents.pop() - if indent > start: - yield DEDENT, '', spos, '' - else: - indents.append(indent) - break - yield NAME, token, spos, prefix - elif initial == '\\' and line[start:] in ('\\\n', '\\\r\n'): # continued stmt - additional_prefix += prefix + line[start:] - break - else: - if token in '([{': - paren_level += 1 - elif token in ')]}': - paren_level -= 1 - yield OP, token, spos, prefix - - end_pos = (lnum, max - 1) - # As the last position we just take the maximally possible position. We - # remove -1 for the last new line. - for indent in indents[1:]: - yield DEDENT, '', end_pos, '' - yield ENDMARKER, '', end_pos, prefix diff --git a/pythonFiles/jedi/parser/tree.py b/pythonFiles/jedi/parser/tree.py deleted file mode 100644 index 079c2d70915d..000000000000 --- a/pythonFiles/jedi/parser/tree.py +++ /dev/null @@ -1,1226 +0,0 @@ -""" -If you know what an abstract syntax tree (AST) is, you'll see that this module -is pretty much that. The classes represent syntax elements like functions and -imports. - -This is the "business logic" part of the parser. There's a lot of logic here -that makes it easier for Jedi (and other libraries to deal with a Python syntax -tree. - -By using `get_code` on a module, you can get back the 1-to-1 representation of -the input given to the parser. This is important if you are using refactoring. - -The easiest way to play with this module is to use :class:`parsing.Parser`. -:attr:`parsing.Parser.module` holds an instance of :class:`Module`: - ->>> from jedi._compatibility import u ->>> from jedi.parser import Parser, load_grammar ->>> parser = Parser(load_grammar(), u('import os'), 'example.py') ->>> submodule = parser.module ->>> submodule - - -Any subclasses of :class:`Scope`, including :class:`Module` has an attribute -:attr:`imports `: - ->>> submodule.imports -[] - -See also :attr:`Scope.subscopes` and :attr:`Scope.statements`. -""" -import os -import re -from inspect import cleandoc -from itertools import chain -import textwrap - -from jedi._compatibility import (Python3Method, encoding, is_py3, utf8_repr, - literal_eval, use_metaclass, unicode) -from jedi import cache - - -def is_node(node, *symbol_names): - try: - type = node.type - except AttributeError: - return False - else: - return type in symbol_names - - -class PositionModifier(object): - """A start_pos modifier for the fast parser.""" - def __init__(self): - self.line = 0 - - -zero_position_modifier = PositionModifier() - - -class DocstringMixin(object): - __slots__ = () - - @property - def raw_doc(self): - """ Returns a cleaned version of the docstring token. """ - if isinstance(self, Module): - node = self.children[0] - elif isinstance(self, ClassOrFunc): - node = self.children[self.children.index(':') + 1] - if is_node(node, 'suite'): # Normally a suite - node = node.children[2] # -> NEWLINE INDENT stmt - else: # ExprStmt - simple_stmt = self.parent - c = simple_stmt.parent.children - index = c.index(simple_stmt) - if not index: - return '' - node = c[index - 1] - - if is_node(node, 'simple_stmt'): - node = node.children[0] - - if node.type == 'string': - # TODO We have to check next leaves until there are no new - # leaves anymore that might be part of the docstring. A - # docstring can also look like this: ``'foo' 'bar' - # Returns a literal cleaned version of the ``Token``. - cleaned = cleandoc(literal_eval(node.value)) - # Since we want the docstr output to be always unicode, just - # force it. - if is_py3 or isinstance(cleaned, unicode): - return cleaned - else: - return unicode(cleaned, 'UTF-8', 'replace') - return '' - - -class Base(object): - """ - This is just here to have an isinstance check, which is also used on - evaluate classes. But since they have sometimes a special type of - delegation, it is important for those classes to override this method. - - I know that there is a chance to do such things with __instancecheck__, but - since Python 2.5 doesn't support it, I decided to do it this way. - """ - __slots__ = () - - def isinstance(self, *cls): - return isinstance(self, cls) - - @Python3Method - def get_parent_until(self, classes=(), reverse=False, - include_current=True): - """ - Searches the parent "chain" until the object is an instance of - classes. If classes is empty return the last parent in the chain - (is without a parent). - """ - if type(classes) not in (tuple, list): - classes = (classes,) - scope = self if include_current else self.parent - while scope.parent is not None: - # TODO why if classes? - if classes and reverse != scope.isinstance(*classes): - break - scope = scope.parent - return scope - - def get_parent_scope(self, include_flows=False): - """ - Returns the underlying scope. - """ - scope = self.parent - while scope is not None: - if include_flows and isinstance(scope, Flow): - return scope - if scope.is_scope(): - break - scope = scope.parent - return scope - - def is_scope(self): - # Default is not being a scope. Just inherit from Scope. - return False - - -class Leaf(Base): - __slots__ = ('position_modifier', 'value', 'parent', '_start_pos', 'prefix') - - def __init__(self, position_modifier, value, start_pos, prefix=''): - self.position_modifier = position_modifier - self.value = value - self._start_pos = start_pos - self.prefix = prefix - self.parent = None - - @property - def start_pos(self): - return self._start_pos[0] + self.position_modifier.line, self._start_pos[1] - - @start_pos.setter - def start_pos(self, value): - self._start_pos = value[0] - self.position_modifier.line, value[1] - - @property - def end_pos(self): - return (self._start_pos[0] + self.position_modifier.line, - self._start_pos[1] + len(self.value)) - - def move(self, line_offset, column_offset): - self._start_pos = (self._start_pos[0] + line_offset, - self._start_pos[1] + column_offset) - - def get_previous(self): - """ - Returns the previous leaf in the parser tree. - """ - node = self - while True: - c = node.parent.children - i = c.index(self) - if i == 0: - node = node.parent - if node.parent is None: - raise IndexError('Cannot access the previous element of the first one.') - else: - node = c[i - 1] - break - - while True: - try: - node = node.children[-1] - except AttributeError: # A Leaf doesn't have children. - return node - - def get_code(self): - return self.prefix + self.value - - def next_sibling(self): - """ - The node immediately following the invocant in their parent's children - list. If the invocant does not have a next sibling, it is None - """ - # Can't use index(); we need to test by identity - for i, child in enumerate(self.parent.children): - if child is self: - try: - return self.parent.children[i + 1] - except IndexError: - return None - - def prev_sibling(self): - """ - The node/leaf immediately preceding the invocant in their parent's - children list. If the invocant does not have a previous sibling, it is - None. - """ - # Can't use index(); we need to test by identity - for i, child in enumerate(self.parent.children): - if child is self: - if i == 0: - return None - return self.parent.children[i - 1] - - @utf8_repr - def __repr__(self): - return "<%s: %s>" % (type(self).__name__, self.value) - - -class LeafWithNewLines(Leaf): - __slots__ = () - - @property - def end_pos(self): - """ - Literals and whitespace end_pos are more complicated than normal - end_pos, because the containing newlines may change the indexes. - """ - end_pos_line, end_pos_col = self.start_pos - lines = self.value.split('\n') - end_pos_line += len(lines) - 1 - # Check for multiline token - if self.start_pos[0] == end_pos_line: - end_pos_col += len(lines[-1]) - else: - end_pos_col = len(lines[-1]) - return end_pos_line, end_pos_col - - - @utf8_repr - def __repr__(self): - return "<%s: %r>" % (type(self).__name__, self.value) - -class Whitespace(LeafWithNewLines): - """Contains NEWLINE and ENDMARKER tokens.""" - __slots__ = () - type = 'whitespace' - - -class Name(Leaf): - """ - A string. Sometimes it is important to know if the string belongs to a name - or not. - """ - type = 'name' - __slots__ = () - - def __str__(self): - return self.value - - def __unicode__(self): - return self.value - - def __repr__(self): - return "<%s: %s@%s,%s>" % (type(self).__name__, self.value, - self.start_pos[0], self.start_pos[1]) - - def get_definition(self): - scope = self - while scope.parent is not None: - parent = scope.parent - if scope.isinstance(Node, Name) and parent.type != 'simple_stmt': - if scope.type == 'testlist_comp': - try: - if isinstance(scope.children[1], CompFor): - return scope.children[1] - except IndexError: - pass - scope = parent - else: - break - return scope - - def is_definition(self): - stmt = self.get_definition() - if stmt.type in ('funcdef', 'classdef', 'file_input', 'param'): - return self == stmt.name - elif stmt.type == 'for_stmt': - return self.start_pos < stmt.children[2].start_pos - elif stmt.type == 'try_stmt': - return self.prev_sibling() == 'as' - else: - return stmt.type in ('expr_stmt', 'import_name', 'import_from', - 'comp_for', 'with_stmt') \ - and self in stmt.get_defined_names() - - def assignment_indexes(self): - """ - Returns an array of ints of the indexes that are used in tuple - assignments. - - For example if the name is ``y`` in the following code:: - - x, (y, z) = 2, '' - - would result in ``[1, 0]``. - """ - indexes = [] - node = self.parent - compare = self - while node is not None: - if is_node(node, 'testlist_comp', 'testlist_star_expr', 'exprlist'): - for i, child in enumerate(node.children): - if child == compare: - indexes.insert(0, int(i / 2)) - break - else: - raise LookupError("Couldn't find the assignment.") - elif isinstance(node, (ExprStmt, CompFor)): - break - - compare = node - node = node.parent - return indexes - - -class Literal(LeafWithNewLines): - __slots__ = () - - def eval(self): - return literal_eval(self.value) - - -class Number(Literal): - type = 'number' - __slots__ = () - - -class String(Literal): - type = 'string' - __slots__ = () - - -class Operator(Leaf): - type = 'operator' - __slots__ = () - - def __str__(self): - return self.value - - def __eq__(self, other): - """ - Make comparisons with strings easy. - Improves the readability of the parser. - """ - if isinstance(other, Operator): - return self is other - else: - return self.value == other - - def __ne__(self, other): - """Python 2 compatibility.""" - return self.value != other - - def __hash__(self): - return hash(self.value) - - -class Keyword(Leaf): - type = 'keyword' - __slots__ = () - - def __eq__(self, other): - """ - Make comparisons with strings easy. - Improves the readability of the parser. - """ - if isinstance(other, Keyword): - return self is other - return self.value == other - - def __ne__(self, other): - """Python 2 compatibility.""" - return not self.__eq__(other) - - def __hash__(self): - return hash(self.value) - - -class BaseNode(Base): - """ - The super class for Scope, Import, Name and Statement. Every object in - the parser tree inherits from this class. - """ - __slots__ = ('children', 'parent') - type = None - - def __init__(self, children): - """ - Initialize :class:`BaseNode`. - - :param children: The module in which this Python object locates. - """ - for c in children: - c.parent = self - self.children = children - self.parent = None - - def move(self, line_offset, column_offset): - """ - Move the Node's start_pos. - """ - for c in self.children: - c.move(line_offset, column_offset) - - @property - def start_pos(self): - return self.children[0].start_pos - - @property - def end_pos(self): - return self.children[-1].end_pos - - def get_code(self): - return "".join(c.get_code() for c in self.children) - - @Python3Method - def name_for_position(self, position): - for c in self.children: - if isinstance(c, Leaf): - if isinstance(c, Name) and c.start_pos <= position <= c.end_pos: - return c - else: - result = c.name_for_position(position) - if result is not None: - return result - return None - - @Python3Method - def get_statement_for_position(self, pos): - for c in self.children: - if c.start_pos <= pos <= c.end_pos: - if c.type not in ('decorated', 'simple_stmt', 'suite') \ - and not isinstance(c, (Flow, ClassOrFunc)): - return c - else: - try: - return c.get_statement_for_position(pos) - except AttributeError: - pass # Must be a non-scope - return None - - def first_leaf(self): - try: - return self.children[0].first_leaf() - except AttributeError: - return self.children[0] - - @utf8_repr - def __repr__(self): - code = self.get_code().replace('\n', ' ') - if not is_py3: - code = code.encode(encoding, 'replace') - return "<%s: %s@%s,%s>" % \ - (type(self).__name__, code, self.start_pos[0], self.start_pos[1]) - - -class Node(BaseNode): - """Concrete implementation for interior nodes.""" - __slots__ = ('type',) - - def __init__(self, type, children): - """ - Initializer. - - Takes a type constant (a symbol number >= 256), a sequence of - child nodes, and an optional context keyword argument. - - As a side effect, the parent pointers of the children are updated. - """ - super(Node, self).__init__(children) - self.type = type - - def __repr__(self): - return "%s(%s, %r)" % (self.__class__.__name__, self.type, self.children) - - -class IsScopeMeta(type): - def __instancecheck__(self, other): - return other.is_scope() - - -class IsScope(use_metaclass(IsScopeMeta)): - pass - - -class Scope(BaseNode, DocstringMixin): - """ - Super class for the parser tree, which represents the state of a python - text file. - A Scope manages and owns its subscopes, which are classes and functions, as - well as variables and imports. It is used to access the structure of python - files. - - :param start_pos: The position (line and column) of the scope. - :type start_pos: tuple(int, int) - """ - __slots__ = ('names_dict',) - - def __init__(self, children): - super(Scope, self).__init__(children) - - @property - def returns(self): - # Needed here for fast_parser, because the fast_parser splits and - # returns will be in "normal" modules. - return self._search_in_scope(ReturnStmt) - - @property - def subscopes(self): - return self._search_in_scope(Scope) - - @property - def flows(self): - return self._search_in_scope(Flow) - - @property - def imports(self): - return self._search_in_scope(Import) - - @Python3Method - def _search_in_scope(self, typ): - def scan(children): - elements = [] - for element in children: - if isinstance(element, typ): - elements.append(element) - if is_node(element, 'suite', 'simple_stmt', 'decorated') \ - or isinstance(element, Flow): - elements += scan(element.children) - return elements - - return scan(self.children) - - @property - def statements(self): - return self._search_in_scope((ExprStmt, KeywordStatement)) - - def is_scope(self): - return True - - def __repr__(self): - try: - name = self.path - except AttributeError: - try: - name = self.name - except AttributeError: - name = self.command - - return "<%s: %s@%s-%s>" % (type(self).__name__, name, - self.start_pos[0], self.end_pos[0]) - - def walk(self): - yield self - for s in self.subscopes: - for scope in s.walk(): - yield scope - - for r in self.statements: - while isinstance(r, Flow): - for scope in r.walk(): - yield scope - r = r.next - - -class Module(Scope): - """ - The top scope, which is always a module. - Depending on the underlying parser this may be a full module or just a part - of a module. - """ - __slots__ = ('path', 'global_names', 'used_names', '_name', - 'error_statement_stacks') - type = 'file_input' - - def __init__(self, children): - """ - Initialize :class:`Module`. - - :type path: str - :arg path: File path to this module. - - .. todo:: Document `top_module`. - """ - super(Module, self).__init__(children) - self.path = None # Set later. - - @property - @cache.underscore_memoization - def name(self): - """ This is used for the goto functions. """ - if self.path is None: - string = '' # no path -> empty name - else: - sep = (re.escape(os.path.sep),) * 2 - r = re.search(r'([^%s]*?)(%s__init__)?(\.py|\.so)?$' % sep, self.path) - # Remove PEP 3149 names - string = re.sub('\.[a-z]+-\d{2}[mud]{0,3}$', '', r.group(1)) - # Positions are not real, but a module starts at (1, 0) - p = (1, 0) - name = Name(zero_position_modifier, string, p) - name.parent = self - return name - - @property - def has_explicit_absolute_import(self): - """ - Checks if imports in this module are explicitly absolute, i.e. there - is a ``__future__`` import. - """ - # TODO this is a strange scan and not fully correct. I think Python's - # parser does it in a different way and scans for the first - # statement/import with a tokenizer (to check for syntax changes like - # the future print statement). - for imp in self.imports: - if imp.type == 'import_from' and imp.level == 0: - for path in imp.paths(): - if [str(name) for name in path] == ['__future__', 'absolute_import']: - return True - return False - - -class Decorator(BaseNode): - type = 'decorator' - __slots__ = () - - -class ClassOrFunc(Scope): - __slots__ = () - - @property - def name(self): - return self.children[1] - - def get_decorators(self): - decorated = self.parent - if is_node(decorated, 'decorated'): - if is_node(decorated.children[0], 'decorators'): - return decorated.children[0].children - else: - return decorated.children[:1] - else: - return [] - - -class Class(ClassOrFunc): - """ - Used to store the parsed contents of a python class. - - :param name: The Class name. - :type name: str - :param supers: The super classes of a Class. - :type supers: list - :param start_pos: The start position (line, column) of the class. - :type start_pos: tuple(int, int) - """ - type = 'classdef' - __slots__ = () - - def __init__(self, children): - super(Class, self).__init__(children) - - def get_super_arglist(self): - if self.children[2] != '(': # Has no parentheses - return None - else: - if self.children[3] == ')': # Empty parentheses - return None - else: - return self.children[3] - - @property - def doc(self): - """ - Return a document string including call signature of __init__. - """ - docstr = self.raw_doc - for sub in self.subscopes: - if str(sub.name) == '__init__': - return '%s\n\n%s' % ( - sub.get_call_signature(func_name=self.name), docstr) - return docstr - - -def _create_params(parent, argslist_list): - """ - `argslist_list` is a list that can contain an argslist as a first item, but - most not. It's basically the items between the parameter brackets (which is - at most one item). - This function modifies the parser structure. It generates `Param` objects - from the normal ast. Those param objects do not exist in a normal ast, but - make the evaluation of the ast tree so much easier. - You could also say that this function replaces the argslist node with a - list of Param objects. - """ - def check_python2_nested_param(node): - """ - Python 2 allows params to look like ``def x(a, (b, c))``, which is - basically a way of unpacking tuples in params. Python 3 has ditched - this behavior. Jedi currently just ignores those constructs. - """ - return node.type == 'tfpdef' and node.children[0] == '(' - - try: - first = argslist_list[0] - except IndexError: - return [] - - if first.type in ('name', 'tfpdef'): - if check_python2_nested_param(first): - return [] - else: - return [Param([first], parent)] - else: # argslist is a `typedargslist` or a `varargslist`. - children = first.children - params = [] - start = 0 - # Start with offset 1, because the end is higher. - for end, child in enumerate(children + [None], 1): - if child is None or child == ',': - new_children = children[start:end] - if new_children: # Could as well be comma and then end. - if check_python2_nested_param(new_children[0]): - continue - params.append(Param(new_children, parent)) - start = end - return params - - -class Function(ClassOrFunc): - """ - Used to store the parsed contents of a python function. - """ - __slots__ = ('listeners',) - type = 'funcdef' - - def __init__(self, children): - super(Function, self).__init__(children) - self.listeners = set() # not used here, but in evaluation. - parameters = self.children[2] # After `def foo` - parameters.children[1:-1] = _create_params(parameters, parameters.children[1:-1]) - - @property - def params(self): - return self.children[2].children[1:-1] - - @property - def name(self): - return self.children[1] # First token after `def` - - @property - def yields(self): - # TODO This is incorrect, yields are also possible in a statement. - return self._search_in_scope(YieldExpr) - - def is_generator(self): - return bool(self.yields) - - def annotation(self): - try: - return self.children[6] # 6th element: def foo(...) -> bar - except IndexError: - return None - - def get_call_signature(self, width=72, func_name=None): - """ - Generate call signature of this function. - - :param width: Fold lines if a line is longer than this value. - :type width: int - :arg func_name: Override function name when given. - :type func_name: str - - :rtype: str - """ - func_name = func_name or self.children[1] - code = unicode(func_name) + self.children[2].get_code() - return '\n'.join(textwrap.wrap(code, width)) - - @property - def doc(self): - """ Return a document string including call signature. """ - docstr = self.raw_doc - return '%s\n\n%s' % (self.get_call_signature(), docstr) - - -class Lambda(Function): - """ - Lambdas are basically trimmed functions, so give it the same interface. - """ - type = 'lambda' - __slots__ = () - - def __init__(self, children): - # We don't want to call the Function constructor, call its parent. - super(Function, self).__init__(children) - self.listeners = set() # not used here, but in evaluation. - lst = self.children[1:-2] # After `def foo` - self.children[1:-2] = _create_params(self, lst) - - @property - def params(self): - return self.children[1:-2] - - def is_generator(self): - return False - - def yields(self): - return [] - - def __repr__(self): - return "<%s@%s>" % (self.__class__.__name__, self.start_pos) - - -class Flow(BaseNode): - __slots__ = () - - -class IfStmt(Flow): - type = 'if_stmt' - __slots__ = () - - def check_nodes(self): - """ - Returns all the `test` nodes that are defined as x, here: - - if x: - pass - elif x: - pass - """ - for i, c in enumerate(self.children): - if c in ('elif', 'if'): - yield self.children[i + 1] - - def node_in_which_check_node(self, node): - for check_node in reversed(list(self.check_nodes())): - if check_node.start_pos < node.start_pos: - return check_node - - def node_after_else(self, node): - """ - Checks if a node is defined after `else`. - """ - for c in self.children: - if c == 'else': - if node.start_pos > c.start_pos: - return True - else: - return False - - -class WhileStmt(Flow): - type = 'while_stmt' - __slots__ = () - - -class ForStmt(Flow): - type = 'for_stmt' - __slots__ = () - - -class TryStmt(Flow): - type = 'try_stmt' - __slots__ = () - - def except_clauses(self): - """ - Returns the ``test`` nodes found in ``except_clause`` nodes. - Returns ``[None]`` for except clauses without an exception given. - """ - for node in self.children: - if node.type == 'except_clause': - yield node.children[1] - elif node == 'except': - yield None - - -class WithStmt(Flow): - type = 'with_stmt' - __slots__ = () - - def get_defined_names(self): - names = [] - for with_item in self.children[1:-2:2]: - # Check with items for 'as' names. - if is_node(with_item, 'with_item'): - names += _defined_names(with_item.children[2]) - return names - - def node_from_name(self, name): - node = name - while True: - node = node.parent - if is_node(node, 'with_item'): - return node.children[0] - - -class Import(BaseNode): - __slots__ = () - - def path_for_name(self, name): - try: - # The name may be an alias. If it is, just map it back to the name. - name = self.aliases()[name] - except KeyError: - pass - - for path in self.paths(): - if name in path: - return path[:path.index(name) + 1] - raise ValueError('Name should be defined in the import itself') - - def is_nested(self): - return False # By default, sub classes may overwrite this behavior - - def is_star_import(self): - return self.children[-1] == '*' - - -class ImportFrom(Import): - type = 'import_from' - __slots__ = () - - def get_defined_names(self): - return [alias or name for name, alias in self._as_name_tuples()] - - def aliases(self): - """Mapping from alias to its corresponding name.""" - return dict((alias, name) for name, alias in self._as_name_tuples() - if alias is not None) - - def get_from_names(self): - for n in self.children[1:]: - if n not in ('.', '...'): - break - if is_node(n, 'dotted_name'): # from x.y import - return n.children[::2] - elif n == 'import': # from . import - return [] - else: # from x import - return [n] - - @property - def level(self): - """The level parameter of ``__import__``.""" - level = 0 - for n in self.children[1:]: - if n in ('.', '...'): - level += len(n.value) - else: - break - return level - - def _as_name_tuples(self): - last = self.children[-1] - if last == ')': - last = self.children[-2] - elif last == '*': - return # No names defined directly. - - if is_node(last, 'import_as_names'): - as_names = last.children[::2] - else: - as_names = [last] - for as_name in as_names: - if as_name.type == 'name': - yield as_name, None - else: - yield as_name.children[::2] # yields x, y -> ``x as y`` - - def star_import_name(self): - """ - The last name defined in a star import. - """ - return self.paths()[-1][-1] - - def paths(self): - """ - The import paths defined in an import statement. Typically an array - like this: ``[, ]``. - """ - dotted = self.get_from_names() - - if self.children[-1] == '*': - return [dotted] - return [dotted + [name] for name, alias in self._as_name_tuples()] - - -class ImportName(Import): - """For ``import_name`` nodes. Covers normal imports without ``from``.""" - type = 'import_name' - __slots__ = () - - def get_defined_names(self): - return [alias or path[0] for path, alias in self._dotted_as_names()] - - @property - def level(self): - """The level parameter of ``__import__``.""" - return 0 # Obviously 0 for imports without from. - - def paths(self): - return [path for path, alias in self._dotted_as_names()] - - def _dotted_as_names(self): - """Generator of (list(path), alias) where alias may be None.""" - dotted_as_names = self.children[1] - if is_node(dotted_as_names, 'dotted_as_names'): - as_names = dotted_as_names.children[::2] - else: - as_names = [dotted_as_names] - - for as_name in as_names: - if is_node(as_name, 'dotted_as_name'): - alias = as_name.children[2] - as_name = as_name.children[0] - else: - alias = None - if as_name.type == 'name': - yield [as_name], alias - else: - # dotted_names - yield as_name.children[::2], alias - - def is_nested(self): - """ - This checks for the special case of nested imports, without aliases and - from statement:: - - import foo.bar - """ - return [1 for path, alias in self._dotted_as_names() - if alias is None and len(path) > 1] - - def aliases(self): - return dict((alias, path[-1]) for path, alias in self._dotted_as_names() - if alias is not None) - - -class KeywordStatement(BaseNode): - """ - For the following statements: `assert`, `del`, `global`, `nonlocal`, - `raise`, `return`, `yield`, `pass`, `continue`, `break`, `return`, `yield`. - """ - __slots__ = () - - @property - def keyword(self): - return self.children[0].value - - -class AssertStmt(KeywordStatement): - type = 'assert_stmt' - __slots__ = () - - def assertion(self): - return self.children[1] - - -class GlobalStmt(KeywordStatement): - type = 'global_stmt' - __slots__ = () - - def get_defined_names(self): - return [] - - def get_global_names(self): - return self.children[1::2] - - -class ReturnStmt(KeywordStatement): - type = 'return_stmt' - __slots__ = () - - -class YieldExpr(BaseNode): - type = 'yield_expr' - __slots__ = () - - -def _defined_names(current): - """ - A helper function to find the defined names in statements, for loops and - list comprehensions. - """ - names = [] - if is_node(current, 'testlist_star_expr', 'testlist_comp', 'exprlist'): - for child in current.children[::2]: - names += _defined_names(child) - elif is_node(current, 'atom'): - names += _defined_names(current.children[1]) - elif is_node(current, 'power'): - if current.children[-2] != '**': # Just if there's no operation - trailer = current.children[-1] - if trailer.children[0] == '.': - names.append(trailer.children[1]) - else: - names.append(current) - return names - - -class ExprStmt(BaseNode, DocstringMixin): - type = 'expr_stmt' - __slots__ = () - - def get_defined_names(self): - return list(chain.from_iterable(_defined_names(self.children[i]) - for i in range(0, len(self.children) - 2, 2) - if '=' in self.children[i + 1].value)) - - def get_rhs(self): - """Returns the right-hand-side of the equals.""" - return self.children[-1] - - def first_operation(self): - """ - Returns `+=`, `=`, etc or None if there is no operation. - """ - try: - return self.children[1] - except IndexError: - return None - - -class Param(BaseNode): - """ - It's a helper class that makes business logic with params much easier. The - Python grammar defines no ``param`` node. It defines it in a different way - that is not really suited to working with parameters. - """ - type = 'param' - - def __init__(self, children, parent): - super(Param, self).__init__(children) - self.parent = parent - for child in children: - child.parent = self - - @property - def stars(self): - first = self.children[0] - if first in ('*', '**'): - return len(first.value) - return 0 - - @property - def default(self): - try: - return self.children[int(self.children[0] in ('*', '**')) + 2] - except IndexError: - return None - - def annotation(self): - # Generate from tfpdef. - raise NotImplementedError - - def _tfpdef(self): - """ - tfpdef: see grammar.txt. - """ - offset = int(self.children[0] in ('*', '**')) - return self.children[offset] - - @property - def name(self): - if is_node(self._tfpdef(), 'tfpdef'): - return self._tfpdef().children[0] - else: - return self._tfpdef() - - @property - def position_nr(self): - return self.parent.children.index(self) - 1 - - @property - def parent_function(self): - return self.get_parent_until(IsScope) - - def __repr__(self): - default = '' if self.default is None else '=%s' % self.default - return '<%s: %s>' % (type(self).__name__, str(self._tfpdef()) + default) - - -class CompFor(BaseNode): - type = 'comp_for' - __slots__ = () - - def is_scope(self): - return True - - @property - def names_dict(self): - dct = {} - for name in self.get_defined_names(): - arr = dct.setdefault(name.value, []) - arr.append(name) - return dct - - def names_dicts(self, search_global): - yield self.names_dict - - def get_defined_names(self): - return _defined_names(self.children[1]) diff --git a/pythonFiles/jedi/parser/user_context.py b/pythonFiles/jedi/parser/user_context.py deleted file mode 100644 index 3cb24a7ceeb5..000000000000 --- a/pythonFiles/jedi/parser/user_context.py +++ /dev/null @@ -1,339 +0,0 @@ -import re -import os -import keyword - -from jedi import cache -from jedi import common -from jedi.parser import tokenize, Parser -from jedi._compatibility import u -from jedi.parser.fast import FastParser -from jedi.parser import tree -from jedi import debug -from jedi.common import PushBackIterator - - -REPLACE_STR = r"[bBuU]?[rR]?" + (r"(?:(')[^\n'\\]*(?:\\.[^\n'\\]*)*(?:'|$)" + - '|' + - r'(")[^\n"\\]*(?:\\.[^\n"\\]*)*(?:"|$))') -REPLACE_STR = re.compile(REPLACE_STR) - - -class UserContext(object): - """ - :param source: The source code of the file. - :param position: The position, the user is currently in. Only important \ - for the main file. - """ - def __init__(self, source, position): - self.source = source - self.position = position - self._line_cache = None - - self._relevant_temp = None - - @cache.underscore_memoization - def get_path_until_cursor(self): - """ Get the path under the cursor. """ - path, self._start_cursor_pos = self._calc_path_until_cursor(self.position) - return path - - def _backwards_line_generator(self, start_pos): - self._line_temp, self._column_temp = start_pos - first_line = self.get_line(start_pos[0])[:self._column_temp] - - self._line_length = self._column_temp - yield first_line[::-1] + '\n' - - while True: - self._line_temp -= 1 - line = self.get_line(self._line_temp) - self._line_length = len(line) - yield line[::-1] + '\n' - - def _get_backwards_tokenizer(self, start_pos, line_gen=None): - if line_gen is None: - line_gen = self._backwards_line_generator(start_pos) - token_gen = tokenize.generate_tokens(lambda: next(line_gen)) - for typ, tok_str, tok_start_pos, prefix in token_gen: - line = self.get_line(self._line_temp) - # Calculate the real start_pos of the token. - if tok_start_pos[0] == 1: - # We are in the first checked line - column = start_pos[1] - tok_start_pos[1] - else: - column = len(line) - tok_start_pos[1] - # Multi-line docstrings must be accounted for. - first_line = common.splitlines(tok_str)[0] - column -= len(first_line) - # Reverse the token again, so that it is in normal order again. - yield typ, tok_str[::-1], (self._line_temp, column), prefix[::-1] - - def _calc_path_until_cursor(self, start_pos): - """ - Something like a reverse tokenizer that tokenizes the reversed strings. - """ - open_brackets = ['(', '[', '{'] - close_brackets = [')', ']', '}'] - - start_cursor = start_pos - gen = PushBackIterator(self._get_backwards_tokenizer(start_pos)) - string = u('') - level = 0 - force_point = False - last_type = None - is_first = True - for tok_type, tok_str, tok_start_pos, prefix in gen: - if is_first: - if prefix: # whitespace is not a path - return u(''), start_cursor - is_first = False - - if last_type == tok_type == tokenize.NAME: - string = ' ' + string - - if level: - if tok_str in close_brackets: - level += 1 - elif tok_str in open_brackets: - level -= 1 - elif tok_str == '.': - force_point = False - elif force_point: - # Reversed tokenizing, therefore a number is recognized as a - # floating point number. - # The same is true for string prefixes -> represented as a - # combination of string and name. - if tok_type == tokenize.NUMBER and tok_str[-1] == '.' \ - or tok_type == tokenize.NAME and last_type == tokenize.STRING \ - and tok_str.lower() in ('b', 'u', 'r', 'br', 'ur'): - force_point = False - else: - break - elif tok_str in close_brackets: - level += 1 - elif tok_type in [tokenize.NAME, tokenize.STRING]: - if keyword.iskeyword(tok_str) and string: - # If there's already something in the string, a keyword - # never adds any meaning to the current statement. - break - force_point = True - elif tok_type == tokenize.NUMBER: - pass - else: - if tok_str == '-': - next_tok = next(gen) - if next_tok[1] == 'e': - gen.push_back(next_tok) - else: - break - else: - break - - start_cursor = tok_start_pos - string = tok_str + prefix + string - last_type = tok_type - - # Don't need whitespace around a statement. - return string.strip(), start_cursor - - def get_path_under_cursor(self): - """ - Return the path under the cursor. If there is a rest of the path left, - it will be added to the stuff before it. - """ - return self.get_path_until_cursor() + self.get_path_after_cursor() - - def get_path_after_cursor(self): - line = self.get_line(self.position[0]) - return re.search("[\w\d]*", line[self.position[1]:]).group(0) - - def get_operator_under_cursor(self): - line = self.get_line(self.position[0]) - after = re.match("[^\w\s]+", line[self.position[1]:]) - before = re.match("[^\w\s]+", line[:self.position[1]][::-1]) - return (before.group(0) if before is not None else '') \ - + (after.group(0) if after is not None else '') - - def call_signature(self): - """ - :return: Tuple of string of the call and the index of the cursor. - """ - def get_line(pos): - def simplify_str(match): - """ - To avoid having strings without end marks (error tokens) and - strings that just screw up all the call signatures, just - simplify everything. - """ - mark = match.group(1) or match.group(2) - return mark + ' ' * (len(match.group(0)) - 2) + mark - - line_gen = self._backwards_line_generator(pos) - for line in line_gen: - # We have to switch the already backwards lines twice, because - # we scan them from start. - line = line[::-1] - modified = re.sub(REPLACE_STR, simplify_str, line) - yield modified[::-1] - - index = 0 - level = 0 - next_must_be_name = False - next_is_key = False - key_name = None - generator = self._get_backwards_tokenizer(self.position, get_line(self.position)) - for tok_type, tok_str, start_pos, prefix in generator: - if tok_str in tokenize.ALWAYS_BREAK_TOKENS: - break - elif next_must_be_name: - if tok_type == tokenize.NUMBER: - # If there's a number at the end of the string, it will be - # tokenized as a number. So add it to the name. - tok_type, t, _, _ = next(generator) - if tok_type == tokenize.NAME: - end_pos = start_pos[0], start_pos[1] + len(tok_str) - call, start_pos = self._calc_path_until_cursor(start_pos=end_pos) - return call, index, key_name, start_pos - index = 0 - next_must_be_name = False - elif next_is_key: - if tok_type == tokenize.NAME: - key_name = tok_str - next_is_key = False - - if tok_str == '(': - level += 1 - if level == 1: - next_must_be_name = True - level = 0 - elif tok_str == ')': - level -= 1 - elif tok_str == ',': - index += 1 - elif tok_str == '=': - next_is_key = True - return None, 0, None, (0, 0) - - def get_context(self, yield_positions=False): - self.get_path_until_cursor() # In case _start_cursor_pos is undefined. - pos = self._start_cursor_pos - while True: - # remove non important white space - line = self.get_line(pos[0]) - while True: - if pos[1] == 0: - line = self.get_line(pos[0] - 1) - if line and line[-1] == '\\': - pos = pos[0] - 1, len(line) - 1 - continue - else: - break - - if line[pos[1] - 1].isspace(): - pos = pos[0], pos[1] - 1 - else: - break - - try: - result, pos = self._calc_path_until_cursor(start_pos=pos) - if yield_positions: - yield pos - else: - yield result - except StopIteration: - if yield_positions: - yield None - else: - yield '' - - def get_line(self, line_nr): - if not self._line_cache: - self._line_cache = common.splitlines(self.source) - - if line_nr == 0: - # This is a fix for the zeroth line. We need a newline there, for - # the backwards parser. - return u('') - if line_nr < 0: - raise StopIteration() - try: - return self._line_cache[line_nr - 1] - except IndexError: - raise StopIteration() - - def get_position_line(self): - return self.get_line(self.position[0])[:self.position[1]] - - -class UserContextParser(object): - def __init__(self, grammar, source, path, position, user_context, - parser_done_callback, use_fast_parser=True): - self._grammar = grammar - self._source = source - self._path = path and os.path.abspath(path) - self._position = position - self._user_context = user_context - self._use_fast_parser = use_fast_parser - self._parser_done_callback = parser_done_callback - - @cache.underscore_memoization - def _parser(self): - cache.invalidate_star_import_cache(self._path) - if self._use_fast_parser: - parser = FastParser(self._grammar, self._source, self._path) - # Don't pickle that module, because the main module is changing quickly - cache.save_parser(self._path, parser, pickling=False) - else: - parser = Parser(self._grammar, self._source, self._path) - self._parser_done_callback(parser) - return parser - - @cache.underscore_memoization - def user_stmt(self): - module = self.module() - debug.speed('parsed') - return module.get_statement_for_position(self._position) - - @cache.underscore_memoization - def user_stmt_with_whitespace(self): - """ - Returns the statement under the cursor even if the statement lies - before the cursor. - """ - user_stmt = self.user_stmt() - - if not user_stmt: - # for statements like `from x import ` (cursor not in statement) - # or `abs( ` where the cursor is out in the whitespace. - if self._user_context.get_path_under_cursor(): - # We really should have a user_stmt, but the parser couldn't - # process it - probably a Syntax Error (or in a comment). - debug.warning('No statement under the cursor.') - return - pos = next(self._user_context.get_context(yield_positions=True)) - user_stmt = self.module().get_statement_for_position(pos) - return user_stmt - - @cache.underscore_memoization - def user_scope(self): - """ - Returns the scope in which the user resides. This includes flows. - """ - user_stmt = self.user_stmt() - if user_stmt is None: - def scan(scope): - for s in scope.children: - if s.start_pos <= self._position <= s.end_pos: - if isinstance(s, (tree.Scope, tree.Flow)): - if isinstance(s, tree.Flow): - return s - return scan(s) or s - elif s.type in ('suite', 'decorated'): - return scan(s) - - return scan(self.module()) or self.module() - else: - return user_stmt.get_parent_scope(include_flows=True) - - def module(self): - return self._parser().module diff --git a/pythonFiles/jedi/refactoring.py b/pythonFiles/jedi/refactoring.py deleted file mode 100644 index a342f08e2b8f..000000000000 --- a/pythonFiles/jedi/refactoring.py +++ /dev/null @@ -1,202 +0,0 @@ -""" -Introduce some basic refactoring functions to |jedi|. This module is still in a -very early development stage and needs much testing and improvement. - -.. warning:: I won't do too much here, but if anyone wants to step in, please - do. Refactoring is none of my priorities - -It uses the |jedi| `API `_ and supports currently the -following functions (sometimes bug-prone): - -- rename -- extract variable -- inline variable -""" -import difflib - -from jedi import common -from jedi.evaluate import helpers -from jedi.parser import tree as pt - - -class Refactoring(object): - def __init__(self, change_dct): - """ - :param change_dct: dict(old_path=(new_path, old_lines, new_lines)) - """ - self.change_dct = change_dct - - def old_files(self): - dct = {} - for old_path, (new_path, old_l, new_l) in self.change_dct.items(): - dct[new_path] = '\n'.join(new_l) - return dct - - def new_files(self): - dct = {} - for old_path, (new_path, old_l, new_l) in self.change_dct.items(): - dct[new_path] = '\n'.join(new_l) - return dct - - def diff(self): - texts = [] - for old_path, (new_path, old_l, new_l) in self.change_dct.items(): - if old_path: - udiff = difflib.unified_diff(old_l, new_l) - else: - udiff = difflib.unified_diff(old_l, new_l, old_path, new_path) - texts.append('\n'.join(udiff)) - return '\n'.join(texts) - - -def rename(script, new_name): - """ The `args` / `kwargs` params are the same as in `api.Script`. - :param operation: The refactoring operation to execute. - :type operation: str - :type source: str - :return: list of changed lines/changed files - """ - return Refactoring(_rename(script.usages(), new_name)) - - -def _rename(names, replace_str): - """ For both rename and inline. """ - order = sorted(names, key=lambda x: (x.module_path, x.line, x.column), - reverse=True) - - def process(path, old_lines, new_lines): - if new_lines is not None: # goto next file, save last - dct[path] = path, old_lines, new_lines - - dct = {} - current_path = object() - new_lines = old_lines = None - for name in order: - if name.in_builtin_module(): - continue - if current_path != name.module_path: - current_path = name.module_path - - process(current_path, old_lines, new_lines) - if current_path is not None: - # None means take the source that is a normal param. - with open(current_path) as f: - source = f.read() - - new_lines = common.splitlines(common.source_to_unicode(source)) - old_lines = new_lines[:] - - nr, indent = name.line, name.column - line = new_lines[nr - 1] - new_lines[nr - 1] = line[:indent] + replace_str + \ - line[indent + len(name.name):] - process(current_path, old_lines, new_lines) - return dct - - -def extract(script, new_name): - """ The `args` / `kwargs` params are the same as in `api.Script`. - :param operation: The refactoring operation to execute. - :type operation: str - :type source: str - :return: list of changed lines/changed files - """ - new_lines = common.splitlines(common.source_to_unicode(script.source)) - old_lines = new_lines[:] - - user_stmt = script._parser.user_stmt() - - # TODO care for multiline extracts - dct = {} - if user_stmt: - pos = script._pos - line_index = pos[0] - 1 - arr, index = helpers.array_for_pos(user_stmt, pos) - if arr is not None: - start_pos = arr[index].start_pos - end_pos = arr[index].end_pos - - # take full line if the start line is different from end line - e = end_pos[1] if end_pos[0] == start_pos[0] else None - start_line = new_lines[start_pos[0] - 1] - text = start_line[start_pos[1]:e] - for l in range(start_pos[0], end_pos[0] - 1): - text += '\n' + l - if e is None: - end_line = new_lines[end_pos[0] - 1] - text += '\n' + end_line[:end_pos[1]] - - # remove code from new lines - t = text.lstrip() - del_start = start_pos[1] + len(text) - len(t) - - text = t.rstrip() - del_end = len(t) - len(text) - if e is None: - new_lines[end_pos[0] - 1] = end_line[end_pos[1] - del_end:] - e = len(start_line) - else: - e = e - del_end - start_line = start_line[:del_start] + new_name + start_line[e:] - new_lines[start_pos[0] - 1] = start_line - new_lines[start_pos[0]:end_pos[0] - 1] = [] - - # add parentheses in multiline case - open_brackets = ['(', '[', '{'] - close_brackets = [')', ']', '}'] - if '\n' in text and not (text[0] in open_brackets and text[-1] == - close_brackets[open_brackets.index(text[0])]): - text = '(%s)' % text - - # add new line before statement - indent = user_stmt.start_pos[1] - new = "%s%s = %s" % (' ' * indent, new_name, text) - new_lines.insert(line_index, new) - dct[script.path] = script.path, old_lines, new_lines - return Refactoring(dct) - - -def inline(script): - """ - :type script: api.Script - """ - new_lines = common.splitlines(common.source_to_unicode(script.source)) - - dct = {} - - definitions = script.goto_assignments() - with common.ignored(AssertionError): - assert len(definitions) == 1 - stmt = definitions[0]._definition - usages = script.usages() - inlines = [r for r in usages - if not stmt.start_pos <= (r.line, r.column) <= stmt.end_pos] - inlines = sorted(inlines, key=lambda x: (x.module_path, x.line, x.column), - reverse=True) - expression_list = stmt.expression_list() - # don't allow multiline refactorings for now. - assert stmt.start_pos[0] == stmt.end_pos[0] - index = stmt.start_pos[0] - 1 - - line = new_lines[index] - replace_str = line[expression_list[0].start_pos[1]:stmt.end_pos[1] + 1] - replace_str = replace_str.strip() - # tuples need parentheses - if expression_list and isinstance(expression_list[0], pr.Array): - arr = expression_list[0] - if replace_str[0] not in ['(', '[', '{'] and len(arr) > 1: - replace_str = '(%s)' % replace_str - - # if it's the only assignment, remove the statement - if len(stmt.get_defined_names()) == 1: - line = line[:stmt.start_pos[1]] + line[stmt.end_pos[1]:] - - dct = _rename(inlines, replace_str) - # remove the empty line - new_lines = dct[script.path][2] - if line.strip(): - new_lines[index] = line - else: - new_lines.pop(index) - - return Refactoring(dct) diff --git a/pythonFiles/jedi/settings.py b/pythonFiles/jedi/settings.py deleted file mode 100644 index fb0b38f55f81..000000000000 --- a/pythonFiles/jedi/settings.py +++ /dev/null @@ -1,239 +0,0 @@ -""" -This module contains variables with global |jedi| settings. To change the -behavior of |jedi|, change the variables defined in :mod:`jedi.settings`. - -Plugins should expose an interface so that the user can adjust the -configuration. - - -Example usage:: - - from jedi import settings - settings.case_insensitive_completion = True - - -Completion output -~~~~~~~~~~~~~~~~~ - -.. autodata:: case_insensitive_completion -.. autodata:: add_dot_after_module -.. autodata:: add_bracket_after_function -.. autodata:: no_completion_duplicates - - -Filesystem cache -~~~~~~~~~~~~~~~~ - -.. autodata:: cache_directory -.. autodata:: use_filesystem_cache - - -Parser -~~~~~~ - -.. autodata:: fast_parser - - -Dynamic stuff -~~~~~~~~~~~~~ - -.. autodata:: dynamic_array_additions -.. autodata:: dynamic_params -.. autodata:: dynamic_params_for_other_modules -.. autodata:: additional_dynamic_modules -.. autodata:: auto_import_modules - - -.. _settings-recursion: - -Recursions -~~~~~~~~~~ - -Recursion settings are important if you don't want extremly -recursive python code to go absolutely crazy. First of there is a -global limit :data:`max_executions`. This limit is important, to set -a maximum amount of time, the completion may use. - -The default values are based on experiments while completing the |jedi| library -itself (inception!). But I don't think there's any other Python library that -uses recursion in a similarly extreme way. These settings make the completion -definitely worse in some cases. But a completion should also be fast. - -.. autodata:: max_until_execution_unique -.. autodata:: max_function_recursion_level -.. autodata:: max_executions_without_builtins -.. autodata:: max_executions -.. autodata:: scale_call_signatures - - -Caching -~~~~~~~ - -.. autodata:: star_import_cache_validity -.. autodata:: call_signatures_validity - - -""" -import os -import platform - -# ---------------- -# completion output settings -# ---------------- - -case_insensitive_completion = True -""" -The completion is by default case insensitive. -""" - -add_dot_after_module = False -""" -Adds a dot after a module, because a module that is not accessed this way is -definitely not the normal case. However, in VIM this doesn't work, that's why -it isn't used at the moment. -""" - -add_bracket_after_function = False -""" -Adds an opening bracket after a function, because that's normal behaviour. -Removed it again, because in VIM that is not very practical. -""" - -no_completion_duplicates = True -""" -If set, completions with the same name don't appear in the output anymore, -but are in the `same_name_completions` attribute. -""" - -# ---------------- -# Filesystem cache -# ---------------- - -use_filesystem_cache = True -""" -Use filesystem cache to save once parsed files with pickle. -""" - -if platform.system().lower() == 'windows': - _cache_directory = os.path.join(os.getenv('APPDATA') or '~', 'Jedi', - 'Jedi') -elif platform.system().lower() == 'darwin': - _cache_directory = os.path.join('~', 'Library', 'Caches', 'Jedi') -else: - _cache_directory = os.path.join(os.getenv('XDG_CACHE_HOME') or '~/.cache', - 'jedi') -cache_directory = os.path.expanduser(_cache_directory) -""" -The path where all the caches can be found. - -On Linux, this defaults to ``~/.cache/jedi/``, on OS X to -``~/Library/Caches/Jedi/`` and on Windows to ``%APPDATA%\\Jedi\\Jedi\\``. -On Linux, if environment variable ``$XDG_CACHE_HOME`` is set, -``$XDG_CACHE_HOME/jedi`` is used instead of the default one. -""" - -# ---------------- -# parser -# ---------------- - -fast_parser = True -""" -Use the fast parser. This means that reparsing is only being done if -something has been changed e.g. to a function. If this happens, only the -function is being reparsed. -""" - -# ---------------- -# dynamic stuff -# ---------------- - -dynamic_array_additions = True -""" -check for `append`, etc. on arrays: [], {}, () as well as list/set calls. -""" - -dynamic_params = True -""" -A dynamic param completion, finds the callees of the function, which define -the params of a function. -""" - -dynamic_params_for_other_modules = True -""" -Do the same for other modules. -""" - -additional_dynamic_modules = [] -""" -Additional modules in which |jedi| checks if statements are to be found. This -is practical for IDEs, that want to administrate their modules themselves. -""" - -dynamic_flow_information = True -""" -Check for `isinstance` and other information to infer a type. -""" - -auto_import_modules = [ - 'hashlib', # setattr -] -""" -Modules that are not analyzed but imported, although they contain Python code. -This improves autocompletion for libraries that use ``setattr`` or -``globals()`` modifications a lot. -""" - -# ---------------- -# recursions -# ---------------- - -max_until_execution_unique = 50 -""" -This limit is probably the most important one, because if this limit is -exceeded, functions can only be one time executed. So new functions will be -executed, complex recursions with the same functions again and again, are -ignored. -""" - -max_function_recursion_level = 5 -""" -`max_function_recursion_level` is more about whether the recursions are -stopped in deepth or in width. The ratio beetween this and -`max_until_execution_unique` is important here. It stops a recursion (after -the number of function calls in the recursion), if it was already used -earlier. -""" - -max_executions_without_builtins = 200 -""" -.. todo:: Document this. -""" - -max_executions = 250 -""" -A maximum amount of time, the completion may use. -""" - -scale_call_signatures = 0.1 -""" -Because call_signatures is normally used on every single key hit, it has -to be faster than a normal completion. This is the factor that is used to -scale `max_executions` and `max_until_execution_unique`: -""" - -# ---------------- -# caching validity (time) -# ---------------- - -star_import_cache_validity = 60.0 -""" -In huge packages like numpy, checking all star imports on every completion -might be slow, therefore we do a star import caching, that lasts a certain -time span (in seconds). -""" - -call_signatures_validity = 3.0 -""" -Finding function calls might be slow (0.1-0.5s). This is not acceptible for -normal writing. Therefore cache it for a short time. -""" diff --git a/pythonFiles/jedi/utils.py b/pythonFiles/jedi/utils.py deleted file mode 100644 index 6b48ef496a5e..000000000000 --- a/pythonFiles/jedi/utils.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -Utilities for end-users. -""" - -from __future__ import absolute_import -import __main__ -from collections import namedtuple -import re -import os -import sys - -from jedi import Interpreter -from jedi.api.helpers import completion_parts -from jedi.parser.user_context import UserContext - - -def setup_readline(namespace_module=__main__): - """ - Install Jedi completer to :mod:`readline`. - - This function setups :mod:`readline` to use Jedi in Python interactive - shell. If you want to use a custom ``PYTHONSTARTUP`` file (typically - ``$HOME/.pythonrc.py``), you can add this piece of code:: - - try: - from jedi.utils import setup_readline - setup_readline() - except ImportError: - # Fallback to the stdlib readline completer if it is installed. - # Taken from http://docs.python.org/2/library/rlcompleter.html - print("Jedi is not installed, falling back to readline") - try: - import readline - import rlcompleter - readline.parse_and_bind("tab: complete") - except ImportError: - print("Readline is not installed either. No tab completion is enabled.") - - This will fallback to the readline completer if Jedi is not installed. - The readline completer will only complete names in the global namespace, - so for example:: - - ran - - will complete to ``range`` - - with both Jedi and readline, but:: - - range(10).cou - - will show complete to ``range(10).count`` only with Jedi. - - You'll also need to add ``export PYTHONSTARTUP=$HOME/.pythonrc.py`` to - your shell profile (usually ``.bash_profile`` or ``.profile`` if you use - bash). - - """ - class JediRL(object): - def complete(self, text, state): - """ - This complete stuff is pretty weird, a generator would make - a lot more sense, but probably due to backwards compatibility - this is still the way how it works. - - The only important part is stuff in the ``state == 0`` flow, - everything else has been copied from the ``rlcompleter`` std. - library module. - """ - if state == 0: - sys.path.insert(0, os.getcwd()) - # Calling python doesn't have a path, so add to sys.path. - try: - interpreter = Interpreter(text, [namespace_module.__dict__]) - - path = UserContext(text, (1, len(text))).get_path_until_cursor() - path, dot, like = completion_parts(path) - before = text[:len(text) - len(like)] - completions = interpreter.completions() - finally: - sys.path.pop(0) - - self.matches = [before + c.name_with_symbols for c in completions] - try: - return self.matches[state] - except IndexError: - return None - - try: - import readline - except ImportError: - print("Module readline not available.") - else: - readline.set_completer(JediRL().complete) - readline.parse_and_bind("tab: complete") - # jedi itself does the case matching - readline.parse_and_bind("set completion-ignore-case on") - # because it's easier to hit the tab just once - readline.parse_and_bind("set show-all-if-unmodified") - readline.parse_and_bind("set show-all-if-ambiguous on") - # don't repeat all the things written in the readline all the time - readline.parse_and_bind("set completion-prefix-display-length 2") - # No delimiters, Jedi handles that. - readline.set_completer_delims('') - - -def version_info(): - """ - Returns a namedtuple of Jedi's version, similar to Python's - ``sys.version_info``. - """ - Version = namedtuple('Version', 'major, minor, micro') - from jedi import __version__ - tupl = re.findall('[a-z]+|\d+', __version__) - return Version(*[x if i == 3 else int(x) for i, x in enumerate(tupl)]) diff --git a/pythonFiles/normalizeForInterpreter.py b/pythonFiles/normalizeForInterpreter.py new file mode 100644 index 000000000000..34b31d56b398 --- /dev/null +++ b/pythonFiles/normalizeForInterpreter.py @@ -0,0 +1,154 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import ast +import io +import operator +import os +import sys +import textwrap +import token +import tokenize + + +class Visitor(ast.NodeVisitor): + def __init__(self, lines): + self._lines = lines + self.line_numbers_with_nodes = set() + self.line_numbers_with_statements = [] + + def generic_visit(self, node): + if ( + hasattr(node, "col_offset") + and hasattr(node, "lineno") + and node.col_offset == 0 + ): + self.line_numbers_with_nodes.add(node.lineno) + if isinstance(node, ast.stmt): + self.line_numbers_with_statements.append(node.lineno) + + ast.NodeVisitor.generic_visit(self, node) + + +def _tokenize(source): + """Tokenize Python source code.""" + # Using an undocumented API as the documented one in Python 2.7 does not work as needed + # cross-version. + if sys.version_info < (3,) and isinstance(source, str): + source = source.decode() + return tokenize.generate_tokens(io.StringIO(source).readline) + + +def _indent_size(line): + for index, char in enumerate(line): + if not char.isspace(): + return index + + +def _get_global_statement_blocks(source, lines): + """Return a list of all global statement blocks. + + The list comprises of 3-item tuples that contain the starting line number, + ending line number and whether the statement is a single line. + + """ + tree = ast.parse(source) + visitor = Visitor(lines) + visitor.visit(tree) + + statement_ranges = [] + for index, line_number in enumerate(visitor.line_numbers_with_statements): + remaining_line_numbers = visitor.line_numbers_with_statements[index + 1 :] + end_line_number = ( + len(lines) + if len(remaining_line_numbers) == 0 + else min(remaining_line_numbers) - 1 + ) + current_statement_is_oneline = line_number == end_line_number + + if len(statement_ranges) == 0: + statement_ranges.append( + (line_number, end_line_number, current_statement_is_oneline) + ) + continue + + previous_statement = statement_ranges[-1] + previous_statement_is_oneline = previous_statement[2] + if previous_statement_is_oneline and current_statement_is_oneline: + statement_ranges[-1] = previous_statement[0], end_line_number, True + else: + statement_ranges.append( + (line_number, end_line_number, current_statement_is_oneline) + ) + + return statement_ranges + + +def normalize_lines(source): + """Normalize blank lines for sending to the terminal. + + Blank lines within a statement block are removed to prevent the REPL + from thinking the block is finished. Newlines are added to separate + top-level statements so that the REPL does not think there is a syntax + error. + + """ + # Ensure to dedent the code (#2837) + lines = textwrap.dedent(source).splitlines(False) + # If we have two blank lines, then add two blank lines. + # Do not trim the spaces, if we have blank lines with spaces, its possible + # we have indented code. + if (len(lines) > 1 and len("".join(lines[-2:])) == 0) or source.endswith( + ("\n\n", "\r\n\r\n") + ): + trailing_newline = "\n" * 2 + # Find out if we have any trailing blank lines + elif len(lines[-1].strip()) == 0 or source.endswith(("\n", "\r\n")): + trailing_newline = "\n" + else: + trailing_newline = "" + + # Step 1: Remove empty lines. + tokens = _tokenize(source) + newlines_indexes_to_remove = ( + spos[0] + for (toknum, tokval, spos, epos, line) in tokens + if len(line.strip()) == 0 + and token.tok_name[toknum] == "NL" + and spos[0] == epos[0] + ) + + for line_number in reversed(list(newlines_indexes_to_remove)): + del lines[line_number - 1] + + # Step 2: Add blank lines between each global statement block. + # A consecutive single lines blocks of code will be treated as a single statement, + # just to ensure we do not unnecessarily add too many blank lines. + source = "\n".join(lines) + tokens = _tokenize(source) + dedent_indexes = ( + spos[0] + for (toknum, tokval, spos, epos, line) in tokens + if toknum == token.DEDENT and _indent_size(line) == 0 + ) + + global_statement_ranges = _get_global_statement_blocks(source, lines) + start_positions = map(operator.itemgetter(0), reversed(global_statement_ranges)) + for line_number in filter(lambda x: x > 1, start_positions): + lines.insert(line_number - 1, "") + + sys.stdout.write("\n".join(lines) + trailing_newline) + sys.stdout.flush() + + +if __name__ == "__main__": + contents = sys.argv[1] + try: + default_encoding = sys.getdefaultencoding() + encoded_contents = contents.encode(default_encoding, "surrogateescape") + contents = encoded_contents.decode(default_encoding, "replace") + except (UnicodeError, LookupError): + pass + if isinstance(contents, bytes): + contents = contents.decode("utf8") + normalize_lines(contents) diff --git a/pythonFiles/printEnvVariables.py b/pythonFiles/printEnvVariables.py new file mode 100644 index 000000000000..353149f237de --- /dev/null +++ b/pythonFiles/printEnvVariables.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import json + +print(json.dumps(dict(os.environ))) diff --git a/pythonFiles/printEnvVariablesToFile.py b/pythonFiles/printEnvVariablesToFile.py new file mode 100644 index 000000000000..be966bcac28c --- /dev/null +++ b/pythonFiles/printEnvVariablesToFile.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import json +import sys + + +# Last argument is the target file into which we'll write the env variables as json. +json_file = sys.argv[-1] + +with open(json_file, "w") as outfile: + json.dump(dict(os.environ), outfile) diff --git a/pythonFiles/pyproject.toml b/pythonFiles/pyproject.toml new file mode 100644 index 000000000000..52c7c96d11e7 --- /dev/null +++ b/pythonFiles/pyproject.toml @@ -0,0 +1,11 @@ +[tool.black] +exclude = ''' + +( + /( + .data + | .vscode + | lib + )/ +) +''' diff --git a/pythonFiles/pyvsc-run-isolated.py b/pythonFiles/pyvsc-run-isolated.py new file mode 100644 index 000000000000..1f6770787490 --- /dev/null +++ b/pythonFiles/pyvsc-run-isolated.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +if __name__ != "__main__": + raise Exception("{} cannot be imported".format(__name__)) + +import os.path +import runpy +import sys + +# We "isolate" the script/module (sys.argv[1]) by +# replacing sys.path[0] with a dummy path and then sending the target +# on to runpy. +sys.path[0] = os.path.join(os.path.dirname(__file__), ".does-not-exist") +del sys.argv[0] +module = sys.argv[0] +if module == "-c": + ns = {} + for code in sys.argv[1:]: + exec(code, ns, ns) +elif module.startswith("-"): + raise NotImplementedError(sys.argv) +elif module.endswith(".py"): + runpy.run_path(module, run_name="__main__") +else: + runpy.run_module(module, run_name="__main__", alter_sys=True) diff --git a/pythonFiles/refactor.py b/pythonFiles/refactor.py new file mode 100644 index 000000000000..9e578906b3c2 --- /dev/null +++ b/pythonFiles/refactor.py @@ -0,0 +1,395 @@ +# Arguments are: +# 1. Working directory. +# 2. Rope folder + +import difflib +import io +import json +import os +import sys +import traceback + +try: + import rope + from rope.base import libutils + from rope.refactor.rename import Rename + from rope.refactor.extract import ExtractMethod, ExtractVariable + import rope.base.project + import rope.base.taskhandle +except: + jsonMessage = { + "error": True, + "message": "Rope not installed", + "traceback": "", + "type": "ModuleNotFoundError", + } + sys.stderr.write(json.dumps(jsonMessage)) + sys.stderr.flush() + +WORKSPACE_ROOT = sys.argv[1] +ROPE_PROJECT_FOLDER = ".vscode/.ropeproject" + + +class RefactorProgress: + """ + Refactor progress information + """ + + def __init__(self, name="Task Name", message=None, percent=0): + self.name = name + self.message = message + self.percent = percent + + +class ChangeType: + """ + Change Type Enum + """ + + EDIT = 0 + NEW = 1 + DELETE = 2 + + +class Change: + """""" + + EDIT = 0 + NEW = 1 + DELETE = 2 + + def __init__(self, filePath, fileMode=ChangeType.EDIT, diff=""): + self.filePath = filePath + self.diff = diff + self.fileMode = fileMode + + +def get_diff(changeset): + """This is a copy of the code form the ChangeSet.get_description method found in Rope.""" + new = changeset.new_contents + old = changeset.old_contents + if old is None: + if changeset.resource.exists(): + old = changeset.resource.read() + else: + old = "" + + # Ensure code has a trailing empty lines, before generating a diff. + # https://github.com/Microsoft/vscode-python/issues/695. + old_lines = old.splitlines(True) + if not old_lines[-1].endswith("\n"): + old_lines[-1] = old_lines[-1] + os.linesep + new = new + os.linesep + + result = difflib.unified_diff( + old_lines, + new.splitlines(True), + "a/" + changeset.resource.path, + "b/" + changeset.resource.path, + ) + return "".join(list(result)) + + +class BaseRefactoring(object): + """ + Base class for refactorings + """ + + def __init__(self, project, resource, name="Refactor", progressCallback=None): + self._progressCallback = progressCallback + self._handle = rope.base.taskhandle.TaskHandle(name) + self._handle.add_observer(self._update_progress) + self.project = project + self.resource = resource + self.changes = [] + + def _update_progress(self): + jobset = self._handle.current_jobset() + if jobset and not self._progressCallback is None: + progress = RefactorProgress() + # getting current job set name + if jobset.get_name() is not None: + progress.name = jobset.get_name() + # getting active job name + if jobset.get_active_job_name() is not None: + progress.message = jobset.get_active_job_name() + # adding done percent + percent = jobset.get_percent_done() + if percent is not None: + progress.percent = percent + if not self._progressCallback is None: + self._progressCallback(progress) + + def stop(self): + self._handle.stop() + + def refactor(self): + try: + self.onRefactor() + except rope.base.exceptions.InterruptedTaskError: + # we can ignore this exception, as user has cancelled refactoring + pass + + def onRefactor(self): + """ + To be implemented by each base class + """ + pass + + +class RenameRefactor(BaseRefactoring): + def __init__( + self, + project, + resource, + name="Rename", + progressCallback=None, + startOffset=None, + newName="new_Name", + ): + BaseRefactoring.__init__(self, project, resource, name, progressCallback) + self._newName = newName + self.startOffset = startOffset + + def onRefactor(self): + renamed = Rename(self.project, self.resource, self.startOffset) + changes = renamed.get_changes(self._newName, task_handle=self._handle) + for item in changes.changes: + if isinstance(item, rope.base.change.ChangeContents): + self.changes.append( + Change(item.resource.real_path, ChangeType.EDIT, get_diff(item)) + ) + else: + raise Exception("Unknown Change") + + +class ExtractVariableRefactor(BaseRefactoring): + def __init__( + self, + project, + resource, + name="Extract Variable", + progressCallback=None, + startOffset=None, + endOffset=None, + newName="new_Name", + similar=False, + global_=False, + ): + BaseRefactoring.__init__(self, project, resource, name, progressCallback) + self._newName = newName + self._startOffset = startOffset + self._endOffset = endOffset + self._similar = similar + self._global = global_ + + def onRefactor(self): + renamed = ExtractVariable( + self.project, self.resource, self._startOffset, self._endOffset + ) + changes = renamed.get_changes(self._newName, self._similar, self._global) + for item in changes.changes: + if isinstance(item, rope.base.change.ChangeContents): + self.changes.append( + Change(item.resource.real_path, ChangeType.EDIT, get_diff(item)) + ) + else: + raise Exception("Unknown Change") + + +class ExtractMethodRefactor(ExtractVariableRefactor): + def __init__( + self, + project, + resource, + name="Extract Method", + progressCallback=None, + startOffset=None, + endOffset=None, + newName="new_Name", + similar=False, + global_=False, + ): + ExtractVariableRefactor.__init__( + self, + project, + resource, + name, + progressCallback, + startOffset=startOffset, + endOffset=endOffset, + newName=newName, + similar=similar, + global_=global_, + ) + + def onRefactor(self): + renamed = ExtractMethod( + self.project, self.resource, self._startOffset, self._endOffset + ) + changes = renamed.get_changes(self._newName, self._similar, self._global) + for item in changes.changes: + if isinstance(item, rope.base.change.ChangeContents): + self.changes.append( + Change(item.resource.real_path, ChangeType.EDIT, get_diff(item)) + ) + else: + raise Exception("Unknown Change") + + +class RopeRefactoring(object): + def __init__(self): + self.default_sys_path = sys.path + self._input = io.open(sys.stdin.fileno(), encoding="utf-8") + + def _rename(self, filePath, start, newName, indent_size): + """ + Renames a variable + """ + project = rope.base.project.Project( + WORKSPACE_ROOT, + ropefolder=ROPE_PROJECT_FOLDER, + save_history=False, + indent_size=indent_size, + ) + resourceToRefactor = libutils.path_to_resource(project, filePath) + refactor = RenameRefactor( + project, resourceToRefactor, startOffset=start, newName=newName + ) + refactor.refactor() + changes = refactor.changes + project.close() + valueToReturn = [] + for change in changes: + valueToReturn.append({"diff": change.diff}) + return valueToReturn + + def _extractVariable(self, filePath, start, end, newName, indent_size): + """ + Extracts a variable + """ + project = rope.base.project.Project( + WORKSPACE_ROOT, + ropefolder=ROPE_PROJECT_FOLDER, + save_history=False, + indent_size=indent_size, + ) + resourceToRefactor = libutils.path_to_resource(project, filePath) + refactor = ExtractVariableRefactor( + project, + resourceToRefactor, + startOffset=start, + endOffset=end, + newName=newName, + similar=True, + ) + refactor.refactor() + changes = refactor.changes + project.close() + valueToReturn = [] + for change in changes: + valueToReturn.append({"diff": change.diff}) + return valueToReturn + + def _extractMethod(self, filePath, start, end, newName, indent_size): + """ + Extracts a method + """ + project = rope.base.project.Project( + WORKSPACE_ROOT, + ropefolder=ROPE_PROJECT_FOLDER, + save_history=False, + indent_size=indent_size, + ) + resourceToRefactor = libutils.path_to_resource(project, filePath) + refactor = ExtractMethodRefactor( + project, + resourceToRefactor, + startOffset=start, + endOffset=end, + newName=newName, + similar=True, + ) + refactor.refactor() + changes = refactor.changes + project.close() + valueToReturn = [] + for change in changes: + valueToReturn.append({"diff": change.diff}) + return valueToReturn + + def _serialize(self, identifier, results): + """ + Serializes the refactor results + """ + return json.dumps({"id": identifier, "results": results}) + + def _deserialize(self, request): + """Deserialize request from VSCode. + + Args: + request: String with raw request from VSCode. + + Returns: + Python dictionary with request data. + """ + return json.loads(request) + + def _process_request(self, request): + """Accept serialized request from VSCode and write response.""" + request = self._deserialize(request) + lookup = request.get("lookup", "") + + if lookup == "": + pass + elif lookup == "rename": + changes = self._rename( + request["file"], + int(request["start"]), + request["name"], + int(request["indent_size"]), + ) + return self._write_response(self._serialize(request["id"], changes)) + elif lookup == "extract_variable": + changes = self._extractVariable( + request["file"], + int(request["start"]), + int(request["end"]), + request["name"], + int(request["indent_size"]), + ) + return self._write_response(self._serialize(request["id"], changes)) + elif lookup == "extract_method": + changes = self._extractMethod( + request["file"], + int(request["start"]), + int(request["end"]), + request["name"], + int(request["indent_size"]), + ) + return self._write_response(self._serialize(request["id"], changes)) + + def _write_response(self, response): + sys.stdout.write(response + "\n") + sys.stdout.flush() + + def watch(self): + self._write_response("STARTED") + while True: + try: + self._process_request(self._input.readline()) + except: + exc_type, exc_value, exc_tb = sys.exc_info() + tb_info = traceback.extract_tb(exc_tb) + jsonMessage = { + "error": True, + "message": str(exc_value), + "traceback": str(tb_info), + "type": str(exc_type), + } + sys.stderr.write(json.dumps(jsonMessage)) + sys.stderr.flush() + + +if __name__ == "__main__": + RopeRefactoring().watch() diff --git a/pythonFiles/shell_exec.py b/pythonFiles/shell_exec.py new file mode 100644 index 000000000000..c521586ca31b --- /dev/null +++ b/pythonFiles/shell_exec.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import sys +import subprocess + +# This is a simple solution to waiting for completion of commands sent to terminal. +# 1. Intercept commands send to a terminal +# 2. Send commands to our script file with an additional argument +# 3. In here create a file that'll log the progress. +# 4. Calling code monitors the contents of the file to determine state of execution. + +# Last argument is a file that's used for synchronizing the actions in the terminal with the calling code in extension. +lock_file = sys.argv[-1] +shell_args = sys.argv[1:-1] + +print("Executing command in shell >> " + " ".join(shell_args)) + +with open(lock_file, "w") as fp: + try: + # Signal start of execution. + fp.write("START\n") + fp.flush() + + subprocess.check_call(shell_args, stdout=sys.stdout, stderr=sys.stderr) + + # Signal start of execution. + fp.write("END\n") + fp.flush() + except Exception: + import traceback + + print(traceback.format_exc()) + # Signal end of execution with failure state. + fp.write("FAIL\n") + fp.flush() + try: + # ALso log the error for use from the other side. + with open(lock_file + ".error", "w") as fpError: + fpError.write(traceback.format_exc()) + except Exception: + pass diff --git a/pythonFiles/sortImports.py b/pythonFiles/sortImports.py index 9099deab0144..070f7883fd66 100644 --- a/pythonFiles/sortImports.py +++ b/pythonFiles/sortImports.py @@ -1,2 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import io +import os +import os.path +import sys + +isort_path = os.path.join(os.path.dirname(__file__), "lib", "python") +sys.path.insert(0, isort_path) + import isort.main -isort.main.main() \ No newline at end of file + +isort.main.main() diff --git a/pythonFiles/symbolProvider.py b/pythonFiles/symbolProvider.py new file mode 100644 index 000000000000..033ce4b99900 --- /dev/null +++ b/pythonFiles/symbolProvider.py @@ -0,0 +1,89 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import ast +import json +import sys + + +class Visitor(ast.NodeVisitor): + def __init__(self): + self.symbols = {"classes": [], "methods": [], "functions": []} + + def visit_Module(self, node): + self.visitChildren(node) + + def visitChildren(self, node, namespace=""): + for child in node.body: + if isinstance(child, ast.FunctionDef): + self.visitDef(child, namespace) + if isinstance(child, ast.ClassDef): + self.visitClassDef(child, namespace) + try: + if isinstance(child, ast.AsyncFunctionDef): + self.visitDef(child, namespace) + except Exception: + pass + + def visitDef(self, node, namespace=""): + end_position = self.getEndPosition(node) + symbol = "functions" if namespace == "" else "methods" + self.symbols[symbol].append(self.getDataObject(node, namespace)) + + def visitClassDef(self, node, namespace=""): + end_position = self.getEndPosition(node) + self.symbols["classes"].append(self.getDataObject(node, namespace)) + + if len(namespace) > 0: + namespace = "{0}::{1}".format(namespace, node.name) + else: + namespace = node.name + self.visitChildren(node, namespace) + + def getDataObject(self, node, namespace=""): + end_position = self.getEndPosition(node) + return { + "namespace": namespace, + "name": node.name, + "range": { + "start": {"line": node.lineno - 1, "character": node.col_offset}, + "end": {"line": end_position[0], "character": end_position[1]}, + }, + } + + def getEndPosition(self, node): + if not hasattr(node, "body") or len(node.body) == 0: + return (node.lineno - 1, node.col_offset) + return self.getEndPosition(node.body[-1]) + + +def provide_symbols(source): + """Provides a list of all symbols in provided code. + + The list comprises of 3-item tuples that contain the starting line number, + ending line number and whether the statement is a single line. + + """ + tree = ast.parse(source) + visitor = Visitor() + visitor.visit(tree) + sys.stdout.write(json.dumps(visitor.symbols)) + sys.stdout.flush() + + +if __name__ == "__main__": + if len(sys.argv) == 3: + contents = sys.argv[2] + else: + with open(sys.argv[1], "r") as source: + contents = source.read() + + try: + default_encoding = sys.getdefaultencoding() + encoded_contents = contents.encode(default_encoding, "surrogateescape") + contents = encoded_contents.decode(default_encoding, "replace") + except (UnicodeError, LookupError): + pass + if isinstance(contents, bytes): + contents = contents.decode("utf8") + provide_symbols(contents) diff --git a/pythonFiles/testing_tools/__init__.py b/pythonFiles/testing_tools/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/pythonFiles/testing_tools/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/pythonFiles/testing_tools/adapter/__init__.py b/pythonFiles/testing_tools/adapter/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/pythonFiles/testing_tools/adapter/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/pythonFiles/testing_tools/adapter/__main__.py b/pythonFiles/testing_tools/adapter/__main__.py new file mode 100644 index 000000000000..5857c63db049 --- /dev/null +++ b/pythonFiles/testing_tools/adapter/__main__.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import absolute_import + +import argparse +import sys + +from . import pytest, report +from .errors import UnsupportedToolError, UnsupportedCommandError + + +TOOLS = { + "pytest": { + "_add_subparser": pytest.add_cli_subparser, + "discover": pytest.discover, + }, +} +REPORTERS = { + "discover": report.report_discovered, +} + + +def parse_args( + # the args to parse + argv=sys.argv[1:], + # the program name + prog=sys.argv[0], +): + """ + Return the subcommand & tool to run, along with its args. + + This defines the standard CLI for the different testing frameworks. + """ + parser = argparse.ArgumentParser( + description="Run Python testing operations.", + prog=prog, + # ... + ) + cmdsubs = parser.add_subparsers(dest="cmd") + + # Add "run" and "debug" subcommands when ready. + for cmdname in ["discover"]: + sub = cmdsubs.add_parser(cmdname) + subsubs = sub.add_subparsers(dest="tool") + for toolname in sorted(TOOLS): + try: + add_subparser = TOOLS[toolname]["_add_subparser"] + except KeyError: + continue + subsub = add_subparser(cmdname, toolname, subsubs) + if cmdname == "discover": + subsub.add_argument("--simple", action="store_true") + subsub.add_argument( + "--no-hide-stdio", dest="hidestdio", action="store_false" + ) + subsub.add_argument("--pretty", action="store_true") + + # Parse the args! + if "--" in argv: + sep_index = argv.index("--") + toolargs = argv[sep_index + 1 :] + argv = argv[:sep_index] + else: + toolargs = [] + args = parser.parse_args(argv) + ns = vars(args) + + cmd = ns.pop("cmd") + if not cmd: + parser.error("missing command") + + tool = ns.pop("tool") + if not tool: + parser.error("missing tool") + + return tool, cmd, ns, toolargs + + +def main( + toolname, + cmdname, + subargs, + toolargs, + # internal args (for testing): + _tools=TOOLS, + _reporters=REPORTERS, +): + try: + tool = _tools[toolname] + except KeyError: + raise UnsupportedToolError(toolname) + + try: + run = tool[cmdname] + report_result = _reporters[cmdname] + except KeyError: + raise UnsupportedCommandError(cmdname) + + parents, result = run(toolargs, **subargs) + report_result(result, parents, **subargs) + + +if __name__ == "__main__": + tool, cmd, subargs, toolargs = parse_args() + main(tool, cmd, subargs, toolargs) diff --git a/pythonFiles/testing_tools/adapter/discovery.py b/pythonFiles/testing_tools/adapter/discovery.py new file mode 100644 index 000000000000..798aea1e93f1 --- /dev/null +++ b/pythonFiles/testing_tools/adapter/discovery.py @@ -0,0 +1,117 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import absolute_import, print_function + +import re + +from .util import fix_fileid, DIRNAME, NORMCASE +from .info import ParentInfo + + +FILE_ID_RE = re.compile( + r""" + ^ + (?: + ( .* [.] (?: py | txt ) \b ) # .txt for doctest files + ( [^.] .* )? + ) + $ + """, + re.VERBOSE, +) + + +def fix_nodeid( + nodeid, + kind, + rootdir=None, + # *, + _fix_fileid=fix_fileid, +): + if not nodeid: + raise ValueError("missing nodeid") + if nodeid == ".": + return nodeid + + fileid = nodeid + remainder = "" + if kind not in ("folder", "file"): + m = FILE_ID_RE.match(nodeid) + if m: + fileid, remainder = m.groups() + elif len(nodeid) > 1: + fileid = nodeid[:2] + remainder = nodeid[2:] + fileid = _fix_fileid(fileid, rootdir) + return fileid + (remainder or "") + + +class DiscoveredTests(object): + """A container for the discovered tests and their parents.""" + + def __init__(self): + self.reset() + + def __len__(self): + return len(self._tests) + + def __getitem__(self, index): + return self._tests[index] + + @property + def parents(self): + return sorted( + self._parents.values(), + # Sort by (name, id). + key=lambda p: (NORMCASE(p.root or p.name), p.id), + ) + + def reset(self): + """Clear out any previously discovered tests.""" + self._parents = {} + self._tests = [] + + def add_test(self, test, parents): + """Add the given test and its parents.""" + parentid = self._ensure_parent(test.path, parents) + # Updating the parent ID and the test ID aren't necessary if the + # provided test and parents (from the test collector) are + # properly generated. However, we play it safe here. + test = test._replace( + # Clean up the ID. + id=fix_nodeid(test.id, "test", test.path.root), + parentid=parentid, + ) + self._tests.append(test) + + def _ensure_parent( + self, + path, + parents, + # *, + _dirname=DIRNAME, + ): + rootdir = path.root + relpath = path.relfile + + _parents = iter(parents) + nodeid, name, kind = next(_parents) + # As in add_test(), the node ID *should* already be correct. + nodeid = fix_nodeid(nodeid, kind, rootdir) + _parentid = nodeid + for parentid, parentname, parentkind in _parents: + # As in add_test(), the parent ID *should* already be correct. + parentid = fix_nodeid(parentid, kind, rootdir) + if kind in ("folder", "file"): + info = ParentInfo(nodeid, kind, name, rootdir, relpath, parentid) + relpath = _dirname(relpath) + else: + info = ParentInfo(nodeid, kind, name, rootdir, None, parentid) + self._parents[(rootdir, nodeid)] = info + nodeid, name, kind = parentid, parentname, parentkind + assert nodeid == "." + info = ParentInfo(nodeid, kind, name=rootdir) + self._parents[(rootdir, nodeid)] = info + + return _parentid diff --git a/pythonFiles/testing_tools/adapter/errors.py b/pythonFiles/testing_tools/adapter/errors.py new file mode 100644 index 000000000000..3e6ae5189cb8 --- /dev/null +++ b/pythonFiles/testing_tools/adapter/errors.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class UnsupportedToolError(ValueError): + def __init__(self, tool): + msg = "unsupported tool {!r}".format(tool) + super(UnsupportedToolError, self).__init__(msg) + self.tool = tool + + +class UnsupportedCommandError(ValueError): + def __init__(self, cmd): + msg = "unsupported cmd {!r}".format(cmd) + super(UnsupportedCommandError, self).__init__(msg) + self.cmd = cmd diff --git a/pythonFiles/testing_tools/adapter/info.py b/pythonFiles/testing_tools/adapter/info.py new file mode 100644 index 000000000000..f99ce0b6f9a2 --- /dev/null +++ b/pythonFiles/testing_tools/adapter/info.py @@ -0,0 +1,120 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from collections import namedtuple + + +class SingleTestPath(namedtuple("TestPath", "root relfile func sub")): + """Where to find a single test.""" + + def __new__(cls, root, relfile, func, sub=None): + self = super(SingleTestPath, cls).__new__( + cls, + str(root) if root else None, + str(relfile) if relfile else None, + str(func) if func else None, + [str(s) for s in sub] if sub else None, + ) + return self + + def __init__(self, *args, **kwargs): + if self.root is None: + raise TypeError("missing id") + if self.relfile is None: + raise TypeError("missing kind") + # self.func may be None (e.g. for doctests). + # self.sub may be None. + + +class ParentInfo(namedtuple("ParentInfo", "id kind name root relpath parentid")): + + KINDS = ("folder", "file", "suite", "function", "subtest") + + def __new__(cls, id, kind, name, root=None, relpath=None, parentid=None): + self = super(ParentInfo, cls).__new__( + cls, + id=str(id) if id else None, + kind=str(kind) if kind else None, + name=str(name) if name else None, + root=str(root) if root else None, + relpath=str(relpath) if relpath else None, + parentid=str(parentid) if parentid else None, + ) + return self + + def __init__(self, *args, **kwargs): + if self.id is None: + raise TypeError("missing id") + if self.kind is None: + raise TypeError("missing kind") + if self.kind not in self.KINDS: + raise ValueError("unsupported kind {!r}".format(self.kind)) + if self.name is None: + raise TypeError("missing name") + if self.root is None: + if self.parentid is not None or self.kind != "folder": + raise TypeError("missing root") + if self.relpath is not None: + raise TypeError("unexpected relpath {}".format(self.relpath)) + elif self.parentid is None: + raise TypeError("missing parentid") + elif self.relpath is None and self.kind in ("folder", "file"): + raise TypeError("missing relpath") + + +class SingleTestInfo( + namedtuple("TestInfo", "id name path source markers parentid kind") +): + """Info for a single test.""" + + MARKERS = ("skip", "skip-if", "expected-failure") + KINDS = ("function", "doctest") + + def __new__(cls, id, name, path, source, markers, parentid, kind="function"): + self = super(SingleTestInfo, cls).__new__( + cls, + str(id) if id else None, + str(name) if name else None, + path or None, + str(source) if source else None, + [str(marker) for marker in markers or ()], + str(parentid) if parentid else None, + str(kind) if kind else None, + ) + return self + + def __init__(self, *args, **kwargs): + if self.id is None: + raise TypeError("missing id") + if self.name is None: + raise TypeError("missing name") + if self.path is None: + raise TypeError("missing path") + if self.source is None: + raise TypeError("missing source") + else: + srcfile, _, lineno = self.source.rpartition(":") + if not srcfile or not lineno or int(lineno) < 0: + raise ValueError("bad source {!r}".format(self.source)) + if self.markers: + badmarkers = [m for m in self.markers if m not in self.MARKERS] + if badmarkers: + raise ValueError("unsupported markers {!r}".format(badmarkers)) + if self.parentid is None: + raise TypeError("missing parentid") + if self.kind is None: + raise TypeError("missing kind") + elif self.kind not in self.KINDS: + raise ValueError("unsupported kind {!r}".format(self.kind)) + + @property + def root(self): + return self.path.root + + @property + def srcfile(self): + return self.source.rpartition(":")[0] + + @property + def lineno(self): + return int(self.source.rpartition(":")[-1]) diff --git a/pythonFiles/testing_tools/adapter/pytest/__init__.py b/pythonFiles/testing_tools/adapter/pytest/__init__.py new file mode 100644 index 000000000000..e894f7bcdb8e --- /dev/null +++ b/pythonFiles/testing_tools/adapter/pytest/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import absolute_import + +from ._cli import add_subparser as add_cli_subparser +from ._discovery import discover diff --git a/pythonFiles/testing_tools/adapter/pytest/_cli.py b/pythonFiles/testing_tools/adapter/pytest/_cli.py new file mode 100644 index 000000000000..3d3eec09a199 --- /dev/null +++ b/pythonFiles/testing_tools/adapter/pytest/_cli.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import absolute_import + +from ..errors import UnsupportedCommandError + + +def add_subparser(cmd, name, parent): + """Add a new subparser to the given parent and add args to it.""" + parser = parent.add_parser(name) + if cmd == "discover": + # For now we don't have any tool-specific CLI options to add. + pass + else: + raise UnsupportedCommandError(cmd) + return parser diff --git a/pythonFiles/testing_tools/adapter/pytest/_discovery.py b/pythonFiles/testing_tools/adapter/pytest/_discovery.py new file mode 100644 index 000000000000..51c94527302d --- /dev/null +++ b/pythonFiles/testing_tools/adapter/pytest/_discovery.py @@ -0,0 +1,109 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import absolute_import, print_function + +import sys + +import pytest + +from .. import util, discovery +from ._pytest_item import parse_item + + +def discover( + pytestargs=None, + hidestdio=False, + # *, + _pytest_main=pytest.main, + _plugin=None, + **_ignored +): + """Return the results of test discovery.""" + if _plugin is None: + _plugin = TestCollector() + + pytestargs = _adjust_pytest_args(pytestargs) + # We use this helper rather than "-pno:terminal" due to possible + # platform-dependent issues. + with (util.hide_stdio() if hidestdio else util.noop_cm()) as stdio: + ec = _pytest_main(pytestargs, [_plugin]) + # See: https://docs.pytest.org/en/latest/usage.html#possible-exit-codes + if ec == 5: + # No tests were discovered. + pass + elif ec != 0: + print( + "equivalent command: {} -m pytest {}".format( + sys.executable, util.shlex_unsplit(pytestargs) + ) + ) + if hidestdio: + print(stdio.getvalue(), file=sys.stderr) + sys.stdout.flush() + raise Exception("pytest discovery failed (exit code {})".format(ec)) + if not _plugin._started: + print( + "equivalent command: {} -m pytest {}".format( + sys.executable, util.shlex_unsplit(pytestargs) + ) + ) + if hidestdio: + print(stdio.getvalue(), file=sys.stderr) + sys.stdout.flush() + raise Exception("pytest discovery did not start") + return ( + _plugin._tests.parents, + list(_plugin._tests), + ) + + +def _adjust_pytest_args(pytestargs): + """Return a corrected copy of the given pytest CLI args.""" + pytestargs = list(pytestargs) if pytestargs else [] + # Duplicate entries should be okay. + pytestargs.insert(0, "--collect-only") + # TODO: pull in code from: + # src/client/testing/pytest/services/discoveryService.ts + # src/client/testing/pytest/services/argsService.ts + return pytestargs + + +class TestCollector(object): + """This is a pytest plugin that collects the discovered tests.""" + + @classmethod + def parse_item(cls, item): + return parse_item(item) + + def __init__(self, tests=None): + if tests is None: + tests = discovery.DiscoveredTests() + self._tests = tests + self._started = False + + # Relevant plugin hooks: + # https://docs.pytest.org/en/latest/reference.html#collection-hooks + + def pytest_collection_modifyitems(self, session, config, items): + self._started = True + self._tests.reset() + for item in items: + test, parents = self.parse_item(item) + if test is not None: + self._tests.add_test(test, parents) + + # This hook is not specified in the docs, so we also provide + # the "modifyitems" hook just in case. + def pytest_collection_finish(self, session): + self._started = True + try: + items = session.items + except AttributeError: + # TODO: Is there an alternative? + return + self._tests.reset() + for item in items: + test, parents = self.parse_item(item) + if test is not None: + self._tests.add_test(test, parents) diff --git a/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py b/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py new file mode 100644 index 000000000000..f9ed1fe0f289 --- /dev/null +++ b/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py @@ -0,0 +1,604 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +""" +During "collection", pytest finds all the tests it supports. These are +called "items". The process is top-down, mostly tracing down through +the file system. Aside from its own machinery, pytest supports hooks +that find tests. Effectively, pytest starts with a set of "collectors"; +objects that can provide a list of tests and sub-collectors. All +collectors in the resulting tree are visited and the tests aggregated. +For the most part, each test's (and collector's) parent is identified +as the collector that collected it. + +Collectors and items are collectively identified as "nodes". The pytest +API relies on collector and item objects providing specific methods and +attributes. In addition to corresponding base classes, pytest provides +a number of concrete implementations. + +The following are the known pytest node types: + + Node + Collector + FSCollector + Session (the top-level collector) + File + Module + Package + DoctestTextfile + DoctestModule + PyCollector + (Module) + (...) + Class + UnitTestCase + Instance + Item + Function + TestCaseFunction + DoctestItem + +Here are the unique attrs for those classes: + + Node + name + nodeid (readonly) + config + session + (parent) - the parent node + (fspath) - the file from which the node was collected + ---- + own_marksers - explicit markers (e.g. with @pytest.mark()) + keywords + extra_keyword_matches + + Item + location - where the actual test source code is: (relfspath, lno, fullname) + user_properties + + PyCollector + module + class + instance + obj + + Function + module + class + instance + obj + function + (callspec) + (fixturenames) + funcargs + originalname - w/o decorations, e.g. [...] for parameterized + + DoctestItem + dtest + obj + +When parsing an item, we make use of the following attributes: + +* name +* nodeid +* __class__ + + __name__ +* fspath +* location +* function + + __name__ + + __code__ + + __closure__ +* own_markers +""" + +from __future__ import absolute_import, print_function + +import sys + +import pytest +import _pytest.doctest +import _pytest.unittest + +from ..info import SingleTestInfo, SingleTestPath +from ..util import fix_fileid, PATH_SEP, NORMCASE + + +def should_never_reach_here(item, **extra): + """Indicates a code path we should never reach.""" + print("The Python extension has run into an unexpected situation") + print("while processing a pytest node during test discovery. Please") + print("Please open an issue at:") + print(" https://github.com/microsoft/vscode-python/issues") + print("and paste the following output there.") + print() + for field, info in _summarize_item(item): + print("{}: {}".format(field, info)) + if extra: + print() + print("extra info:") + for name, info in extra.items(): + print("{:10}".format(name + ":"), end="") + if isinstance(info, str): + print(info) + else: + try: + print(*info) + except TypeError: + print(info) + print() + print("traceback:") + import traceback + + traceback.print_stack() + + msg = "Unexpected pytest node (see printed output)." + exc = NotImplementedError(msg) + exc.item = item + return exc + + +def parse_item( + item, + # *, + _get_item_kind=(lambda *a: _get_item_kind(*a)), + _parse_node_id=(lambda *a: _parse_node_id(*a)), + _split_fspath=(lambda *a: _split_fspath(*a)), + _get_location=(lambda *a: _get_location(*a)), +): + """Return (TestInfo, [suite ID]) for the given item. + + The suite IDs, if any, are in parent order with the item's direct + parent at the beginning. The parent of the last suite ID (or of + the test if there are no suites) is the file ID, which corresponds + to TestInfo.path. + + """ + # _debug_item(item, showsummary=True) + kind, _ = _get_item_kind(item) + # Skip plugin generated tests + if kind is None: + return None, None + (nodeid, parents, fileid, testfunc, parameterized) = _parse_node_id( + item.nodeid, kind + ) + # Note: testfunc does not necessarily match item.function.__name__. + # This can result from importing a test function from another module. + + # Figure out the file. + testroot, relfile = _split_fspath(str(item.fspath), fileid, item) + location, fullname = _get_location(item, testroot, relfile) + if kind == "function": + if testfunc and fullname != testfunc + parameterized: + raise should_never_reach_here( + item, + fullname=fullname, + testfunc=testfunc, + parameterized=parameterized, + # ... + ) + elif kind == "doctest": + if testfunc and fullname != testfunc and fullname != "[doctest] " + testfunc: + raise should_never_reach_here( + item, + fullname=fullname, + testfunc=testfunc, + # ... + ) + testfunc = None + + # Sort out the parent. + if parents: + parentid, _, _ = parents[0] + else: + parentid = None + + # Sort out markers. + # See: https://docs.pytest.org/en/latest/reference.html#marks + markers = set() + for marker in getattr(item, "own_markers", []): + if marker.name == "parameterize": + # We've already covered these. + continue + elif marker.name == "skip": + markers.add("skip") + elif marker.name == "skipif": + markers.add("skip-if") + elif marker.name == "xfail": + markers.add("expected-failure") + # We can add support for other markers as we need them? + + test = SingleTestInfo( + id=nodeid, + name=item.name, + path=SingleTestPath( + root=testroot, + relfile=relfile, + func=testfunc, + sub=[parameterized] if parameterized else None, + ), + source=location, + markers=sorted(markers) if markers else None, + parentid=parentid, + ) + if parents and parents[-1] == (".", None, "folder"): # This should always be true? + parents[-1] = (".", testroot, "folder") + return test, parents + + +def _split_fspath( + fspath, + fileid, + item, + # *, + _normcase=NORMCASE, +): + """Return (testroot, relfile) for the given fspath. + + "relfile" will match "fileid". + """ + # "fileid" comes from nodeid and is always relative to the testroot + # (with a "./" prefix). There are no guarantees about casing, so we + # normcase just be to sure. + relsuffix = fileid[1:] # Drop (only) the "." prefix. + if not _normcase(fspath).endswith(_normcase(relsuffix)): + raise should_never_reach_here( + item, + fspath=fspath, + fileid=fileid, + # ... + ) + testroot = fspath[: -len(fileid) + 1] # Ignore the "./" prefix. + relfile = "." + fspath[-len(fileid) + 1 :] # Keep the pathsep. + return testroot, relfile + + +def _get_location( + item, + testroot, + relfile, + # *, + _matches_relfile=(lambda *a: _matches_relfile(*a)), + _is_legacy_wrapper=(lambda *a: _is_legacy_wrapper(*a)), + _unwrap_decorator=(lambda *a: _unwrap_decorator(*a)), + _pathsep=PATH_SEP, +): + """Return (loc str, fullname) for the given item.""" + # When it comes to normcase, we favor relfile (from item.fspath) + # over item.location in this function. + + srcfile, lineno, fullname = item.location + if _matches_relfile(srcfile, testroot, relfile): + srcfile = relfile + else: + # pytest supports discovery of tests imported from other + # modules. This is reflected by a different filename + # in item.location. + + if _is_legacy_wrapper(srcfile): + srcfile = relfile + unwrapped = _unwrap_decorator(item.function) + if unwrapped is None: + # It was an invalid legacy wrapper so we just say + # "somewhere in relfile". + lineno = None + else: + _srcfile, lineno = unwrapped + if not _matches_relfile(_srcfile, testroot, relfile): + # For legacy wrappers we really expect the wrapped + # function to be in relfile. So here we ignore any + # other file and just say "somewhere in relfile". + lineno = None + elif _matches_relfile(srcfile, testroot, relfile): + srcfile = relfile + # Otherwise we just return the info from item.location as-is. + + if not srcfile.startswith("." + _pathsep): + srcfile = "." + _pathsep + srcfile + + if lineno is None: + lineno = -1 # i.e. "unknown" + + # from pytest, line numbers are 0-based + location = "{}:{}".format(srcfile, int(lineno) + 1) + return location, fullname + + +def _matches_relfile( + srcfile, + testroot, + relfile, + # *, + _normcase=NORMCASE, + _pathsep=PATH_SEP, +): + """Return True if "srcfile" matches the given relfile.""" + testroot = _normcase(testroot) + srcfile = _normcase(srcfile) + relfile = _normcase(relfile) + if srcfile == relfile: + return True + elif srcfile == relfile[len(_pathsep) + 1 :]: + return True + elif srcfile == testroot + relfile[1:]: + return True + else: + return False + + +def _is_legacy_wrapper( + srcfile, + # *, + _pathsep=PATH_SEP, + _pyversion=sys.version_info, +): + """Return True if the test might be wrapped. + + In Python 2 unittest's decorators (e.g. unittest.skip) do not wrap + properly, so we must manually unwrap them. + """ + if _pyversion > (3,): + return False + if (_pathsep + "unittest" + _pathsep + "case.py") not in srcfile: + return False + return True + + +def _unwrap_decorator(func): + """Return (filename, lineno) for the func the given func wraps. + + If the wrapped func cannot be identified then return None. Likewise + for the wrapped filename. "lineno" is None if it cannot be found + but the filename could. + """ + try: + func = func.__closure__[0].cell_contents + except (IndexError, AttributeError): + return None + else: + if not callable(func): + return None + try: + filename = func.__code__.co_filename + except AttributeError: + return None + else: + try: + lineno = func.__code__.co_firstlineno - 1 + except AttributeError: + return (filename, None) + else: + return filename, lineno + + +def _parse_node_id( + testid, + kind, + # *, + _iter_nodes=(lambda *a: _iter_nodes(*a)), +): + """Return the components of the given node ID, in heirarchical order.""" + nodes = iter(_iter_nodes(testid, kind)) + + testid, name, kind = next(nodes) + parents = [] + parameterized = None + if kind == "doctest": + parents = list(nodes) + fileid, _, _ = parents[0] + return testid, parents, fileid, name, parameterized + elif kind is None: + fullname = None + else: + if kind == "subtest": + node = next(nodes) + parents.append(node) + funcid, funcname, _ = node + parameterized = testid[len(funcid) :] + elif kind == "function": + funcname = name + else: + raise should_never_reach_here( + testid, + kind=kind, + # ... + ) + fullname = funcname + + for node in nodes: + parents.append(node) + parentid, name, kind = node + if kind == "file": + fileid = parentid + break + elif fullname is None: + # We don't guess how to interpret the node ID for these tests. + continue + elif kind == "suite": + fullname = name + "." + fullname + else: + raise should_never_reach_here( + testid, + node=node, + # ... + ) + else: + fileid = None + parents.extend(nodes) # Add the rest in as-is. + + return ( + testid, + parents, + fileid, + fullname, + parameterized or "", + ) + + +def _iter_nodes( + testid, + kind, + # *, + _normalize_test_id=(lambda *a: _normalize_test_id(*a)), + _normcase=NORMCASE, + _pathsep=PATH_SEP, +): + """Yield (nodeid, name, kind) for the given node ID and its parents.""" + nodeid, testid = _normalize_test_id(testid, kind) + if len(nodeid) > len(testid): + testid = "." + _pathsep + testid + + if kind == "function" and nodeid.endswith("]"): + funcid, sep, parameterized = nodeid.partition("[") + if not sep: + raise should_never_reach_here( + nodeid, + # ... + ) + yield (nodeid, sep + parameterized, "subtest") + nodeid = funcid + + parentid, _, name = nodeid.rpartition("::") + if not parentid: + if kind is None: + # This assumes that plugins can generate nodes that do not + # have a parent. All the builtin nodes have one. + yield (nodeid, name, kind) + return + # We expect at least a filename and a name. + raise should_never_reach_here( + nodeid, + # ... + ) + yield (nodeid, name, kind) + + # Extract the suites. + while "::" in parentid: + suiteid = parentid + parentid, _, name = parentid.rpartition("::") + yield (suiteid, name, "suite") + + # Extract the file and folders. + fileid = parentid + raw = testid[: len(fileid)] + _parentid, _, filename = _normcase(fileid).rpartition(_pathsep) + parentid = fileid[: len(_parentid)] + raw, name = raw[: len(_parentid)], raw[-len(filename) :] + yield (fileid, name, "file") + # We're guaranteed at least one (the test root). + while _pathsep in _normcase(parentid): + folderid = parentid + _parentid, _, foldername = _normcase(folderid).rpartition(_pathsep) + parentid = folderid[: len(_parentid)] + raw, name = raw[: len(parentid)], raw[-len(foldername) :] + yield (folderid, name, "folder") + # We set the actual test root later at the bottom of parse_item(). + testroot = None + yield (parentid, testroot, "folder") + + +def _normalize_test_id( + testid, + kind, + # *, + _fix_fileid=fix_fileid, + _pathsep=PATH_SEP, +): + """Return the canonical form for the given node ID.""" + while "::()::" in testid: + testid = testid.replace("::()::", "::") + if kind is None: + return testid, testid + orig = testid + + # We need to keep the testid as-is, or else pytest won't recognize + # it when we try to use it later (e.g. to run a test). The only + # exception is that we add a "./" prefix for relative paths. + # Note that pytest always uses "/" as the path separator in IDs. + fileid, sep, remainder = testid.partition("::") + fileid = _fix_fileid(fileid) + if not fileid.startswith("./"): # Absolute "paths" not expected. + raise should_never_reach_here( + testid, + fileid=fileid, + # ... + ) + testid = fileid + sep + remainder + + return testid, orig + + +def _get_item_kind(item): + """Return (kind, isunittest) for the given item.""" + if isinstance(item, _pytest.doctest.DoctestItem): + return "doctest", False + elif isinstance(item, _pytest.unittest.TestCaseFunction): + return "function", True + elif isinstance(item, pytest.Function): + # We *could* be more specific, e.g. "method", "subtest". + return "function", False + else: + return None, False + + +############################# +# useful for debugging + +_FIELDS = [ + "nodeid", + "kind", + "class", + "name", + "fspath", + "location", + "function", + "markers", + "user_properties", + "attrnames", +] + + +def _summarize_item(item): + if not hasattr(item, "nodeid"): + yield "nodeid", item + return + + for field in _FIELDS: + try: + if field == "kind": + yield field, _get_item_kind(item) + elif field == "class": + yield field, item.__class__.__name__ + elif field == "markers": + yield field, item.own_markers + # yield field, list(item.iter_markers()) + elif field == "attrnames": + yield field, dir(item) + else: + yield field, getattr(item, field, "") + except Exception as exc: + yield field, "".format(exc) + + +def _debug_item(item, showsummary=False): + item._debugging = True + try: + summary = dict(_summarize_item(item)) + finally: + item._debugging = False + + if showsummary: + print(item.nodeid) + for key in ( + "kind", + "class", + "name", + "fspath", + "location", + "func", + "markers", + "props", + ): + print(" {:12} {}".format(key, summary[key])) + print() + + return summary diff --git a/pythonFiles/testing_tools/adapter/report.py b/pythonFiles/testing_tools/adapter/report.py new file mode 100644 index 000000000000..bacdef7b9a00 --- /dev/null +++ b/pythonFiles/testing_tools/adapter/report.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import print_function + +import json + + +def report_discovered( + tests, + parents, + # *, + pretty=False, + simple=False, + _send=print, + **_ignored +): + """Serialize the discovered tests and write to stdout.""" + if simple: + data = [ + { + "id": test.id, + "name": test.name, + "testroot": test.path.root, + "relfile": test.path.relfile, + "lineno": test.lineno, + "testfunc": test.path.func, + "subtest": test.path.sub or None, + "markers": test.markers or [], + } + for test in tests + ] + else: + byroot = {} + for parent in parents: + rootdir = parent.name if parent.root is None else parent.root + try: + root = byroot[rootdir] + except KeyError: + root = byroot[rootdir] = { + "id": rootdir, + "parents": [], + "tests": [], + } + if not parent.root: + root["id"] = parent.id + continue + root["parents"].append( + { + # "id" must match what the testing framework recognizes. + "id": parent.id, + "kind": parent.kind, + "name": parent.name, + "parentid": parent.parentid, + } + ) + if parent.relpath is not None: + root["parents"][-1]["relpath"] = parent.relpath + for test in tests: + # We are guaranteed that the parent was added. + root = byroot[test.path.root] + testdata = { + # "id" must match what the testing framework recognizes. + "id": test.id, + "name": test.name, + # TODO: Add a "kind" field + # (e.g. "unittest", "function", "doctest") + "source": test.source, + "markers": test.markers or [], + "parentid": test.parentid, + } + root["tests"].append(testdata) + data = [ + { + "rootid": byroot[root]["id"], + "root": root, + "parents": byroot[root]["parents"], + "tests": byroot[root]["tests"], + } + for root in sorted(byroot) + ] + + kwargs = {} + if pretty: + # human-formatted + kwargs = dict( + sort_keys=True, + indent=4, + separators=(",", ": "), + # ... + ) + serialized = json.dumps(data, **kwargs) + + _send(serialized) diff --git a/pythonFiles/testing_tools/adapter/util.py b/pythonFiles/testing_tools/adapter/util.py new file mode 100644 index 000000000000..77778c5b6126 --- /dev/null +++ b/pythonFiles/testing_tools/adapter/util.py @@ -0,0 +1,287 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import contextlib +import io + +try: + from io import StringIO +except ImportError: + from StringIO import StringIO # 2.7 +import os +import os.path +import sys +import tempfile + + +@contextlib.contextmanager +def noop_cm(): + yield + + +def group_attr_names(attrnames): + grouped = { + "dunder": [], + "private": [], + "constants": [], + "classes": [], + "vars": [], + "other": [], + } + for name in attrnames: + if name.startswith("__") and name.endswith("__"): + group = "dunder" + elif name.startswith("_"): + group = "private" + elif name.isupper(): + group = "constants" + elif name.islower(): + group = "vars" + elif name == name.capitalize(): + group = "classes" + else: + group = "other" + grouped[group].append(name) + return grouped + + +if sys.version_info < (3,): + _str_to_lower = lambda val: val.decode().lower() +else: + _str_to_lower = str.lower + + +############################# +# file paths + +_os_path = os.path +# Uncomment to test Windows behavior on non-windows OS: +# import ntpath as _os_path +PATH_SEP = _os_path.sep +NORMCASE = _os_path.normcase +DIRNAME = _os_path.dirname +BASENAME = _os_path.basename +IS_ABS_PATH = _os_path.isabs +PATH_JOIN = _os_path.join + + +def fix_path( + path, + # *, + _pathsep=PATH_SEP, +): + """Return a platform-appropriate path for the given path.""" + if not path: + return "." + return path.replace("/", _pathsep) + + +def fix_relpath( + path, + # *, + _fix_path=fix_path, + _path_isabs=IS_ABS_PATH, + _pathsep=PATH_SEP, +): + """Return a ./-prefixed, platform-appropriate path for the given path.""" + path = _fix_path(path) + if path in (".", ".."): + return path + if not _path_isabs(path): + if not path.startswith("." + _pathsep): + path = "." + _pathsep + path + return path + + +def _resolve_relpath( + path, + rootdir=None, + # *, + _path_isabs=IS_ABS_PATH, + _normcase=NORMCASE, + _pathsep=PATH_SEP, +): + # "path" is expected to use "/" for its path separator, regardless + # of the provided "_pathsep". + + if path.startswith("./"): + return path[2:] + if not _path_isabs(path): + return path + + # Deal with root-dir-as-fileid. + _, sep, relpath = path.partition("/") + if sep and not relpath.replace("/", ""): + return "" + + if rootdir is None: + return None + rootdir = _normcase(rootdir) + if not rootdir.endswith(_pathsep): + rootdir += _pathsep + + if not _normcase(path).startswith(rootdir): + return None + return path[len(rootdir) :] + + +def fix_fileid( + fileid, + rootdir=None, + # *, + normalize=False, + strictpathsep=None, + _pathsep=PATH_SEP, + **kwargs +): + """Return a pathsep-separated file ID ("./"-prefixed) for the given value. + + The file ID may be absolute. If so and "rootdir" is + provided then make the file ID relative. If absolute but "rootdir" + is not provided then leave it absolute. + """ + if not fileid or fileid == ".": + return fileid + + # We default to "/" (forward slash) as the final path sep, since + # that gives us a consistent, cross-platform result. (Windows does + # actually support "/" as a path separator.) Most notably, node IDs + # from pytest use "/" as the path separator by default. + _fileid = fileid.replace(_pathsep, "/") + + relpath = _resolve_relpath( + _fileid, + rootdir, + _pathsep=_pathsep, + # ... + **kwargs + ) + if relpath: # Note that we treat "" here as an absolute path. + _fileid = "./" + relpath + + if normalize: + if strictpathsep: + raise ValueError("cannot normalize *and* keep strict path separator") + _fileid = _str_to_lower(_fileid) + elif strictpathsep: + # We do not use _normcase since we want to preserve capitalization. + _fileid = _fileid.replace("/", _pathsep) + return _fileid + + +############################# +# stdio + + +@contextlib.contextmanager +def _replace_fd(file, target): + """ + Temporarily replace the file descriptor for `file`, + for which sys.stdout or sys.stderr is passed. + """ + try: + fd = file.fileno() + except (AttributeError, io.UnsupportedOperation): + # `file` does not have fileno() so it's been replaced from the + # default sys.stdout, etc. Return with noop. + yield + return + target_fd = target.fileno() + + # Keep the original FD to be restored in the finally clause. + dup_fd = os.dup(fd) + try: + # Point the FD at the target. + os.dup2(target_fd, fd) + try: + yield + finally: + # Point the FD back at the original. + os.dup2(dup_fd, fd) + finally: + os.close(dup_fd) + + +@contextlib.contextmanager +def _replace_stdout(target): + orig = sys.stdout + sys.stdout = target + try: + yield orig + finally: + sys.stdout = orig + + +@contextlib.contextmanager +def _replace_stderr(target): + orig = sys.stderr + sys.stderr = target + try: + yield orig + finally: + sys.stderr = orig + + +if sys.version_info < (3,): + _coerce_unicode = lambda s: unicode(s) +else: + _coerce_unicode = lambda s: s + + +@contextlib.contextmanager +def _temp_io(): + sio = StringIO() + with tempfile.TemporaryFile("r+") as tmp: + try: + yield sio, tmp + finally: + tmp.seek(0) + buff = tmp.read() + sio.write(_coerce_unicode(buff)) + + +@contextlib.contextmanager +def hide_stdio(): + """Swallow stdout and stderr.""" + with _temp_io() as (sio, fileobj): + with _replace_fd(sys.stdout, fileobj): + with _replace_stdout(fileobj): + with _replace_fd(sys.stderr, fileobj): + with _replace_stderr(fileobj): + yield sio + + +############################# +# shell + + +def shlex_unsplit(argv): + """Return the shell-safe string for the given arguments. + + This effectively the equivalent of reversing shlex.split(). + """ + argv = [_quote_arg(a) for a in argv] + return " ".join(argv) + + +try: + from shlex import quote as _quote_arg +except ImportError: + + def _quote_arg(arg): + parts = None + for i, c in enumerate(arg): + if c.isspace(): + pass + elif c == '"': + pass + elif c == "'": + c = "'\"'\"'" + else: + continue + if parts is None: + parts = list(arg) + parts[i] = c + if parts is not None: + arg = "'" + "".join(parts) + "'" + return arg diff --git a/pythonFiles/testing_tools/run_adapter.py b/pythonFiles/testing_tools/run_adapter.py new file mode 100644 index 000000000000..1eeef194f8f5 --- /dev/null +++ b/pythonFiles/testing_tools/run_adapter.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Replace the "." entry. +import os.path +import sys + +sys.path.insert( + 1, + os.path.dirname( # pythonFiles + os.path.dirname( # pythonFiles/testing_tools + os.path.abspath(__file__) # this file + ) + ), +) + +from testing_tools.adapter.__main__ import parse_args, main + + +if __name__ == "__main__": + tool, cmd, subargs, toolargs = parse_args() + main(tool, cmd, subargs, toolargs) diff --git a/pythonFiles/testlauncher.py b/pythonFiles/testlauncher.py new file mode 100644 index 000000000000..95ca800b9885 --- /dev/null +++ b/pythonFiles/testlauncher.py @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import sys + + +def parse_argv(): + """Parses arguments for use with the test launcher. + Arguments are: + 1. Working directory. + 2. Test runner, `pytest` or `nose` + 3. Rest of the arguments are passed into the test runner. + """ + + return (sys.argv[1], sys.argv[2], sys.argv[3:]) + + +def run(cwd, testRunner, args): + """Runs the test + cwd -- the current directory to be set + testRunner -- test runner to be used `pytest` or `nose` + args -- arguments passed into the test runner + """ + + sys.path[0] = os.getcwd() + os.chdir(cwd) + + try: + if testRunner == "pytest": + import pytest + + pytest.main(args) + else: + import nose + + nose.run(argv=args) + sys.exit(0) + finally: + pass + + +if __name__ == "__main__": + cwd, testRunner, args = parse_argv() + run(cwd, testRunner, args) diff --git a/pythonFiles/tests/__init__.py b/pythonFiles/tests/__init__.py new file mode 100644 index 000000000000..e2e6976acd68 --- /dev/null +++ b/pythonFiles/tests/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os.path + +TEST_ROOT = os.path.dirname(__file__) +SRC_ROOT = os.path.dirname(TEST_ROOT) +PROJECT_ROOT = os.path.dirname(SRC_ROOT) +IPYTHON_ROOT = os.path.join(SRC_ROOT, "ipython") +TESTING_TOOLS_ROOT = os.path.join(SRC_ROOT, "testing_tools") +DEBUG_ADAPTER_ROOT = os.path.join(SRC_ROOT, "debug_adapter") + +PYTHONFILES = os.path.join(SRC_ROOT, "lib", "python") diff --git a/pythonFiles/tests/__main__.py b/pythonFiles/tests/__main__.py new file mode 100644 index 000000000000..14086978c9af --- /dev/null +++ b/pythonFiles/tests/__main__.py @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import sys + +import pytest + +from . import DEBUG_ADAPTER_ROOT, IPYTHON_ROOT, SRC_ROOT, TEST_ROOT, TESTING_TOOLS_ROOT + + +def parse_args(): + parser = argparse.ArgumentParser() + # To mark a test as functional: (decorator) @pytest.mark.functional + parser.add_argument( + "--functional", dest="markers", action="append_const", const="functional" + ) + parser.add_argument( + "--no-functional", dest="markers", action="append_const", const="not functional" + ) + args, remainder = parser.parse_known_args() + + ns = vars(args) + + return ns, remainder + + +def main(pytestargs, markers=None): + sys.path.insert(1, IPYTHON_ROOT) + sys.path.insert(1, TESTING_TOOLS_ROOT) + sys.path.insert(1, DEBUG_ADAPTER_ROOT) + + pytestargs = ["--rootdir", SRC_ROOT, TEST_ROOT] + pytestargs + for marker in reversed(markers or ()): + pytestargs.insert(0, marker) + pytestargs.insert(0, "-m") + + ec = pytest.main(pytestargs) + return ec + + +if __name__ == "__main__": + mainkwargs, pytestargs = parse_args() + ec = main(pytestargs, **mainkwargs) + sys.exit(ec) diff --git a/pythonFiles/tests/debug_adapter/__init__.py b/pythonFiles/tests/debug_adapter/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/pythonFiles/tests/debug_adapter/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/pythonFiles/tests/debug_adapter/test_install_debugpy.py b/pythonFiles/tests/debug_adapter/test_install_debugpy.py new file mode 100644 index 000000000000..19565c19675c --- /dev/null +++ b/pythonFiles/tests/debug_adapter/test_install_debugpy.py @@ -0,0 +1,37 @@ +import os +import pytest +import subprocess +import sys + + +def _check_binaries(dir_path): + expected_endswith = ( + "win_amd64.pyd", + "win32.pyd", + "darwin.so", + "i386-linux-gnu.so", + "x86_64-linux-gnu.so", + ) + + binaries = list(p for p in os.listdir(dir_path) if p.endswith(expected_endswith)) + + assert len(binaries) == len(expected_endswith) + + +@pytest.mark.skipif( + sys.version_info[:2] != (3, 7), + reason="DEBUGPY wheels shipped for Python 3.7 only", +) +def test_install_debugpy(tmpdir): + import install_debugpy + + install_debugpy.main(str(tmpdir)) + dir_path = os.path.join( + str(tmpdir), "debugpy", "_vendored", "pydevd", "_pydevd_bundle" + ) + _check_binaries(dir_path) + + dir_path = os.path.join( + str(tmpdir), "debugpy", "_vendored", "pydevd", "_pydevd_frame_eval" + ) + _check_binaries(dir_path) diff --git a/pythonFiles/tests/ipython/__init__.py b/pythonFiles/tests/ipython/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/pythonFiles/tests/ipython/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/pythonFiles/tests/ipython/getJupyterVariableList.py b/pythonFiles/tests/ipython/getJupyterVariableList.py new file mode 100644 index 000000000000..d9b0778fa2d2 --- /dev/null +++ b/pythonFiles/tests/ipython/getJupyterVariableList.py @@ -0,0 +1,38 @@ +# Query Jupyter server for defined variables list +# Tested on 2.7 and 3.6 +from sys import getsizeof as _VSCODE_getsizeof +import json as _VSCODE_json +from IPython import get_ipython as _VSCODE_get_ipython + +# _VSCode_supportsDataExplorer will contain our list of data explorer supported types +_VSCode_supportsDataExplorer = "['list', 'Series', 'dict', 'ndarray', 'DataFrame']" + +# who_ls is a Jupyter line magic to fetch currently defined vars +_VSCode_JupyterVars = _VSCODE_get_ipython().run_line_magic("who_ls", "") + +_VSCode_output = [] +for _VSCode_var in _VSCode_JupyterVars: + try: + _VSCode_type = type(eval(_VSCode_var)) + _VSCode_output.append( + { + "name": _VSCode_var, + "type": _VSCode_type.__name__, + "size": _VSCODE_getsizeof(_VSCode_var), + "supportsDataExplorer": _VSCode_type.__name__ + in _VSCode_supportsDataExplorer, + } + ) + del _VSCode_type + del _VSCode_var + except: + pass + +print(_VSCODE_json.dumps(_VSCode_output)) + +del _VSCODE_get_ipython +del _VSCode_output +del _VSCode_supportsDataExplorer +del _VSCode_JupyterVars +del _VSCODE_json +del _VSCODE_getsizeof diff --git a/pythonFiles/tests/ipython/getJupyterVariableValue.py b/pythonFiles/tests/ipython/getJupyterVariableValue.py new file mode 100644 index 000000000000..6110d8653dee --- /dev/null +++ b/pythonFiles/tests/ipython/getJupyterVariableValue.py @@ -0,0 +1,475 @@ +import sys as VC_sys +import locale as VC_locale + +VC_IS_PY2 = VC_sys.version_info < (3,) + +# SafeRepr based on the pydevd implementation +# https://github.com/microsoft/ptvsd/blob/master/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_safe_repr.py +class VC_SafeRepr(object): + # Py3 compat - alias unicode to str, and xrange to range + try: + unicode # noqa + except NameError: + unicode = str + try: + xrange # noqa + except NameError: + xrange = range + + # Can be used to override the encoding from locale.getpreferredencoding() + locale_preferred_encoding = None + + # Can be used to override the encoding used for sys.stdout.encoding + sys_stdout_encoding = None + + # String types are truncated to maxstring_outer when at the outer- + # most level, and truncated to maxstring_inner characters inside + # collections. + maxstring_outer = 2 ** 16 + maxstring_inner = 30 + if not VC_IS_PY2: + string_types = (str, bytes) + set_info = (set, "{", "}", False) + frozenset_info = (frozenset, "frozenset({", "})", False) + int_types = (int,) + long_iter_types = (list, tuple, bytearray, range, dict, set, frozenset) + else: + string_types = (str, unicode) + set_info = (set, "set([", "])", False) + frozenset_info = (frozenset, "frozenset([", "])", False) + int_types = (int, long) # noqa + long_iter_types = ( + list, + tuple, + bytearray, + xrange, + dict, + set, + frozenset, + buffer, + ) # noqa + + # Collection types are recursively iterated for each limit in + # maxcollection. + maxcollection = (15, 10) + + # Specifies type, prefix string, suffix string, and whether to include a + # comma if there is only one element. (Using a sequence rather than a + # mapping because we use isinstance() to determine the matching type.) + collection_types = [ + (tuple, "(", ")", True), + (list, "[", "]", False), + frozenset_info, + set_info, + ] + try: + from collections import deque + + collection_types.append((deque, "deque([", "])", False)) + except Exception: + pass + + # type, prefix string, suffix string, item prefix string, + # item key/value separator, item suffix string + dict_types = [(dict, "{", "}", "", ": ", "")] + try: + from collections import OrderedDict + + dict_types.append((OrderedDict, "OrderedDict([", "])", "(", ", ", ")")) + except Exception: + pass + + # All other types are treated identically to strings, but using + # different limits. + maxother_outer = 2 ** 16 + maxother_inner = 30 + + convert_to_hex = False + raw_value = False + + def __call__(self, obj): + try: + if VC_IS_PY2: + return "".join( + (x.encode("utf-8") if isinstance(x, unicode) else x) + for x in self._repr(obj, 0) + ) + else: + return "".join(self._repr(obj, 0)) + except Exception as e: + try: + return "An exception was raised: " + str(e) + except Exception: + return "An exception was raised" + + def _repr(self, obj, level): + """Returns an iterable of the parts in the final repr string.""" + + try: + obj_repr = type(obj).__repr__ + except Exception: + obj_repr = None + + def has_obj_repr(t): + r = t.__repr__ + try: + return obj_repr == r + except Exception: + return obj_repr is r + + for t, prefix, suffix, comma in self.collection_types: + if isinstance(obj, t) and has_obj_repr(t): + return self._repr_iter(obj, level, prefix, suffix, comma) + + for ( + t, + prefix, + suffix, + item_prefix, + item_sep, + item_suffix, + ) in self.dict_types: # noqa + if isinstance(obj, t) and has_obj_repr(t): + return self._repr_dict( + obj, level, prefix, suffix, item_prefix, item_sep, item_suffix + ) + + for t in self.string_types: + if isinstance(obj, t) and has_obj_repr(t): + return self._repr_str(obj, level) + + if self._is_long_iter(obj): + return self._repr_long_iter(obj) + + return self._repr_other(obj, level) + + # Determines whether an iterable exceeds the limits set in + # maxlimits, and is therefore unsafe to repr(). + def _is_long_iter(self, obj, level=0): + try: + # Strings have their own limits (and do not nest). Because + # they don't have __iter__ in 2.x, this check goes before + # the next one. + if isinstance(obj, self.string_types): + return len(obj) > self.maxstring_inner + + # If it's not an iterable (and not a string), it's fine. + if not hasattr(obj, "__iter__"): + return False + + # If it's not an instance of these collection types then it + # is fine. Note: this is a fix for + # https://github.com/Microsoft/ptvsd/issues/406 + if not isinstance(obj, self.long_iter_types): + return False + + # Iterable is its own iterator - this is a one-off iterable + # like generator or enumerate(). We can't really count that, + # but repr() for these should not include any elements anyway, + # so we can treat it the same as non-iterables. + if obj is iter(obj): + return False + + # xrange reprs fine regardless of length. + if isinstance(obj, xrange): + return False + + # numpy and scipy collections (ndarray etc) have + # self-truncating repr, so they're always safe. + try: + module = type(obj).__module__.partition(".")[0] + if module in ("numpy", "scipy"): + return False + except Exception: + pass + + # Iterables that nest too deep are considered long. + if level >= len(self.maxcollection): + return True + + # It is too long if the length exceeds the limit, or any + # of its elements are long iterables. + if hasattr(obj, "__len__"): + try: + size = len(obj) + except Exception: + size = None + if size is not None and size > self.maxcollection[level]: + return True + return any( + (self._is_long_iter(item, level + 1) for item in obj) + ) # noqa + return any( + i > self.maxcollection[level] or self._is_long_iter(item, level + 1) + for i, item in enumerate(obj) + ) # noqa + + except Exception: + # If anything breaks, assume the worst case. + return True + + def _repr_iter(self, obj, level, prefix, suffix, comma_after_single_element=False): + yield prefix + + if level >= len(self.maxcollection): + yield "..." + else: + count = self.maxcollection[level] + yield_comma = False + for item in obj: + if yield_comma: + yield ", " + yield_comma = True + + count -= 1 + if count <= 0: + yield "..." + break + + for p in self._repr(item, 100 if item is obj else level + 1): + yield p + else: + if comma_after_single_element: + if count == self.maxcollection[level] - 1: + yield "," + yield suffix + + def _repr_long_iter(self, obj): + try: + length = hex(len(obj)) if self.convert_to_hex else len(obj) + obj_repr = "<%s, len() = %s>" % (type(obj).__name__, length) + except Exception: + try: + obj_repr = "<" + type(obj).__name__ + ">" + except Exception: + obj_repr = "" + yield obj_repr + + def _repr_dict( + self, obj, level, prefix, suffix, item_prefix, item_sep, item_suffix + ): + if not obj: + yield prefix + suffix + return + if level >= len(self.maxcollection): + yield prefix + "..." + suffix + return + + yield prefix + + count = self.maxcollection[level] + yield_comma = False + + try: + sorted_keys = sorted(obj) + except Exception: + sorted_keys = list(obj) + + for key in sorted_keys: + if yield_comma: + yield ", " + yield_comma = True + + count -= 1 + if count <= 0: + yield "..." + break + + yield item_prefix + for p in self._repr(key, level + 1): + yield p + + yield item_sep + + try: + item = obj[key] + except Exception: + yield "" + else: + for p in self._repr(item, 100 if item is obj else level + 1): + yield p + yield item_suffix + + yield suffix + + def _repr_str(self, obj, level): + return self._repr_obj(obj, level, self.maxstring_inner, self.maxstring_outer) + + def _repr_other(self, obj, level): + return self._repr_obj(obj, level, self.maxother_inner, self.maxother_outer) + + def _repr_obj(self, obj, level, limit_inner, limit_outer): + try: + if self.raw_value: + # For raw value retrieval, ignore all limits. + if isinstance(obj, bytes): + yield obj.decode("latin-1") + return + + try: + mv = memoryview(obj) + except Exception: + yield self._convert_to_unicode_or_bytes_repr(repr(obj)) + return + else: + # Map bytes to Unicode codepoints with same values. + yield mv.tobytes().decode("latin-1") + return + elif self.convert_to_hex and isinstance(obj, self.int_types): + obj_repr = hex(obj) + else: + obj_repr = repr(obj) + except Exception: + try: + obj_repr = object.__repr__(obj) + except Exception: + try: + obj_repr = ( + "" + ) # noqa + except Exception: + obj_repr = "" + + limit = limit_inner if level > 0 else limit_outer + + if limit >= len(obj_repr): + yield self._convert_to_unicode_or_bytes_repr(obj_repr) + return + + # Slightly imprecise calculations - we may end up with a string that is + # up to 3 characters longer than limit. If you need precise formatting, + # you are using the wrong class. + left_count, right_count = ( + max(1, int(2 * limit / 3)), + max(1, int(limit / 3)), + ) # noqa + + if VC_IS_PY2 and isinstance(obj_repr, bytes): + # If we can convert to unicode before slicing, that's better (but don't do + # it if it's not possible as we may be dealing with actual binary data). + + obj_repr = self._bytes_as_unicode_if_possible(obj_repr) + if isinstance(obj_repr, unicode): + # Deal with high-surrogate leftovers on Python 2. + try: + if left_count > 0 and unichr(0xD800) <= obj_repr[ + left_count - 1 + ] <= unichr(0xDBFF): + left_count -= 1 + except ValueError: + # On Jython unichr(0xD800) will throw an error: + # ValueError: unichr() arg is a lone surrogate in range (0xD800, 0xDFFF) (Jython UTF-16 encoding) + # Just ignore it in this case. + pass + + start = obj_repr[:left_count] + + # Note: yielding unicode is fine (it'll be properly converted to utf-8 if needed). + yield start + yield "..." + + # Deal with high-surrogate leftovers on Python 2. + try: + if right_count > 0 and unichr(0xD800) <= obj_repr[ + -right_count - 1 + ] <= unichr(0xDBFF): + right_count -= 1 + except ValueError: + # On Jython unichr(0xD800) will throw an error: + # ValueError: unichr() arg is a lone surrogate in range (0xD800, 0xDFFF) (Jython UTF-16 encoding) + # Just ignore it in this case. + pass + + yield obj_repr[-right_count:] + return + else: + # We can't decode it (binary string). Use repr() of bytes. + obj_repr = repr(obj_repr) + + yield obj_repr[:left_count] + yield "..." + yield obj_repr[-right_count:] + + def _convert_to_unicode_or_bytes_repr(self, obj_repr): + if VC_IS_PY2 and isinstance(obj_repr, bytes): + obj_repr = self._bytes_as_unicode_if_possible(obj_repr) + if isinstance(obj_repr, bytes): + # If we haven't been able to decode it this means it's some binary data + # we can't make sense of, so, we need its repr() -- otherwise json + # encoding may break later on. + obj_repr = repr(obj_repr) + return obj_repr + + def _bytes_as_unicode_if_possible(self, obj_repr): + # We try to decode with 3 possible encoding (sys.stdout.encoding, + # locale.getpreferredencoding() and 'utf-8). If no encoding can decode + # the input, we return the original bytes. + try_encodings = [] + encoding = self.sys_stdout_encoding or getattr(VC_sys.stdout, "encoding", "") + if encoding: + try_encodings.append(encoding.lower()) + + preferred_encoding = ( + self.locale_preferred_encoding or VC_locale.getpreferredencoding() + ) + if preferred_encoding: + preferred_encoding = preferred_encoding.lower() + if preferred_encoding not in try_encodings: + try_encodings.append(preferred_encoding) + + if "utf-8" not in try_encodings: + try_encodings.append("utf-8") + + for encoding in try_encodings: + try: + return obj_repr.decode(encoding) + except UnicodeDecodeError: + pass + + return obj_repr # Return the original version (in bytes) + + +# Query Jupyter server for the value of a variable +import json as _VSCODE_json + +_VSCODE_max_len = 200 +# In IJupyterVariables.getValue this '_VSCode_JupyterTestValue' will be replaced with the json stringified value of the target variable +# Indexes off of _VSCODE_targetVariable need to index types that are part of IJupyterVariable +_VSCODE_targetVariable = _VSCODE_json.loads("""_VSCode_JupyterTestValue""") + +_VSCODE_evalResult = eval(_VSCODE_targetVariable["name"]) + +# Find shape and count if available +if hasattr(_VSCODE_evalResult, "shape"): + try: + # Get a bit more restrictive with exactly what we want to count as a shape, since anything can define it + if isinstance(_VSCODE_evalResult.shape, tuple): + _VSCODE_shapeStr = str(_VSCODE_evalResult.shape) + if ( + len(_VSCODE_shapeStr) >= 3 + and _VSCODE_shapeStr[0] == "(" + and _VSCODE_shapeStr[-1] == ")" + and "," in _VSCODE_shapeStr + ): + _VSCODE_targetVariable["shape"] = _VSCODE_shapeStr + del _VSCODE_shapeStr + except TypeError: + pass + +if hasattr(_VSCODE_evalResult, "__len__"): + try: + _VSCODE_targetVariable["count"] = len(_VSCODE_evalResult) + except TypeError: + pass + +# Use SafeRepr to get our short string value +VC_sr = VC_SafeRepr() +_VSCODE_targetVariable["value"] = VC_sr(_VSCODE_evalResult) + +print(_VSCODE_json.dumps(_VSCODE_targetVariable)) + +del VC_locale +del VC_IS_PY2 +del VC_sys +del VC_SafeRepr +del VC_sr diff --git a/pythonFiles/tests/ipython/random.csv b/pythonFiles/tests/ipython/random.csv new file mode 100644 index 000000000000..dde35fda3850 --- /dev/null +++ b/pythonFiles/tests/ipython/random.csv @@ -0,0 +1,6001 @@ +".Lh~N","~`y","]W|{92","|uHbIo","o}","+h2" +"-O;Xfw ","","KX^K)FHe:","3+4ncqL8=O","py]ZA;","_y>" +"}z#IiI'+c,","^) ""","(SLt4","JJ:2VnAQ/J","ZO>%nl>`EZ","" +"","ji#cufxu2",":7JAHYDdRx","klq?$","tItAtlu-","TxspzZ" +"x9>iX'C+F","","G`[I","p^Y=S)",">NLE@UX|=a","a=XsO" +"`Y""Wc2^tQ","HlZ","N9/@","J","A","Cv" +"","L f'%K","~UDs","mh/A:X","Yz9d:T{2Q|","ee" +"`3A* >","Nu6cv","%F13Kb6p","","8+{hhd","J#","W3_yiE","y)d8?","" +";,.;rnu\K","nfS)Fy=l","",",r~$","jOU[uQYt'","R" +"3$cA$","l4","t!nf","53snc8g","m&,:","OQg\ " +"m>D","74|LkY5eV","/LR","h","","B]" +"60z{e92;","^P","5s-:m","G2YGn*DpK","I","" +"4s2(","n?j1,f~~?d","?","3{j,tax>8r","","Ms_\Z" +",i9sLk3z","D';Tv","R","m1keY","|`DumH","k.2-Mu9P" +"!","Y$","O","r","xj _*%*","#" +"HU","Z_jT0?","!2X>5","@9tD~BA)va","^ Gn=|q4H","eX" +"-","H,m=WD","fE;","YCTw<","""sS/{ow","","D@=iE'j4" +"Tqi1","hTF?RF""g=","","9{","m]?81+hk","1ZHfSk" +"J""oMEj","""*MZ`q; ","{","C","R|}Ws|j)d","s" +"6 k\:$@gl","bo8)dK#Sj","3Z3#","","J*:F>",".$" +"4Fx0(~dQ#U","F('2","YO~phil","/NmyUrJ~E","","" +"Or,B""b@HO.","uV2tX%yAE8","fn HXw","","7R^b","Pq%" +"W8BTO3","Vz{xaSpxd(","7YTQU}D]q[","b]B|5x","?i(kD","" +"","P~O","?m&SG1o{","(",").{1PSm[E","{" +"a","?","k&8)","","FP31","!""P^$!" +"90g=","{XN","FFH;b","r^XHX>@R&1","","4!sz=W#K" +"","##LY$m13","-(","`b","#K","Dd" +"|jA","34 Q","dU-;:OOU\U","nKO","*I","}+IJv" +".>j&","d-Gz","nOe6W""q$Zc","MYyF~","","FzO&u" +"!@>3&","YCyw0y","","","I(,","!fhPR+!=8N","*^tw&-Rt","V","","Sc3S" +"u;","#no","U","|B1`","[^o;Lyp5","" +".Vb[JH9x-s","9?<""y","","","8@1K7Jh","-ySc" +"R9S@g;fJ","ae4b","Q01]VuYAIu","J]W(PXVu`","xUw_LZ!8CB","" +",",":6","CLUFQ[9!","'K9\l^","_(OjmvR?","lT1GR;k" +"""+u$2VaoTD","","","@\|#G","","8I","Nx@R&g&sw","*r-Ltk","","" +"KB/ uy0","YJ6.].gQ-s","Q","RE[3RGSE4H","Q6a_)5p?","","|~O>&Xua","#cD&u","n~nM`]" +"V","-47Q8,k","53S""EIi%`","lDlQVv","A""}c1c='x:",";{4?6\" +"z'","YP5e4(U","l^mTI6qb",")sDm^JM)qV","","lw/yD34~","|uM5","","O6z>P}",":w!CBD" +"GUoP=,Kqc",")0|O","[A99FJC","uM:Fg","O{&","aQ;G_{m" +"D?1m8|8","Ule","CkEK+","Sfi8wyM","c1I~t","2/&bE=QJ:""" +"7XpO#@","I0L(t[`59","E&grAL3,","7","","" +"&tLwX)e","<#R#","&fp","}z@s","Gn","1ln[S!GP" +"k>Kb&YeB","5Qw=$-oq","&^","J15BT'z>~","sBq6&TRf","3FOj0T" +".6r$K4","\6f,.E'","k<","","4l(d 5","J[>x$1D2" +"""fto","lR`(+","?","TJ","","" +"8Nk","H","","]X","m.d%BI#q","; ,Pq?`;,%" +"5^}7sQ`","7>:L","I","3L\-","wA7Nh@rww","9VXVw%)~" +".HS","","","{7?+C","zo17eF|v"," R" +"vH#+T","?_Dk.Y","Gvdh+)-PhU","A]gB^<^e","R%X)","Gna*.n" +"?d","S","J:vQmT_[`^","W_X","`""S<.S}'","IC " +")AFHI(1't","K>]R","Bx","iWTE2fc","1<","m=B;x4n$" +"Zyu","q5C% i""b","$N)""mCi/o","ULa nXr{nv","u9HQx5,_1Z","XbKX" +"OP[s ^wl)6","y",".}2t=:","E,z@aBPR","qC=P3","`p}A63u,3%" +"X[d","Q0tsJ#*","gMR-.6&6:@","H"">M""CfcR","0}BSkSW@/e","R`Wi&`Djr" +"e&gUxU*A","0"," :","","Q>B","E" +"s%>WO5$o3g","D","s{%",")V&x)`@;","5=","1Sw" +"","Ym@","_","M`(*_B>P- " +"K","","2O#D?","%RTu>7Otx","zbN|","I""/" +"-)",".JzD'","bc[y^d#Y,","Tm0ud\","1H@;","n|q< L3i9U" +"&cc-XYJ",">51","0BKHX[","",""":0JR/g","\'\`5l\O" +"","o""L#w?4qY",";X7Qt","ivrB]@","R","% %-6A23" +"E-I[","0TPXa/8);o","T4hBVto0F","Lr[|_:Si","%(/x""","tT" +"","vWq`y[!","fZ{b-)R(","Y^","","bJr$}.<$>r" +"F","","t+r]s,=T#","b]1?U","5Ldjnsq4:N","u,7}qpR" +"","zftO.","3-y","","Y]BLTq3?","-x%#b" +"<[s2YJ&}_","bn","$>Rt7tHW",">!*4Ao","","}}" +"c]-jDpO","!LsvN,/+6","W+w&=l","""@fd`ho","wk+i#'Q","q/:k}" +"IMaj/y","L","g*d_:\:","YT\0","O<[/q@_""#a","UDQzt#5" +"ZFE","xb","oe*1.uQ/.","Xi1fJ8*\I","DXVd!(6>","5sBi}E5" +"8)","Pz","\\ L|","G|e&9","2gd#5)x Z{","gU\Jq]RrA" +"d0A$fck]","`" +"8","Y9ds?,""","^u","GUha]","]MR%qJ",":gq""`lO","8B;N:c?" +"EE"," b:(W9N","|;P""&9","v@qz","}B40.e%Utn","M,ynqkYg" +"e!/]Rz]$","cDQ!NE7","8@=6","d","4#/O","" +"zn","nA^@g!Z","8nt$sxb","t","","m#x{0;V11" +"","{tT5","9QnNs@A","\0URMK","]","j<""gt" +"&0/UGKz'G""","K:j)_","QS>g!7","$`9uc,CJZ,","`8+w,","`" +";'V-7","$p<`=","j^I","r2 ","","jy{" +"","m(hNrbi","h","Z9$V83J","+5]b","I" +":","<|>b[X1jX","c","T","","}r^hq4g" +"(vwf","wQvCnhksM","`","8K,3S'lHv","4o","A!q% P""LX" +"","rf8'y7;","X!Ur","s","?iDO",">h(." +"ui""R&JBr","&'7.u","2Y,l-5","?HB","DL""$","sAh||/l=" +"lXaB7[","1hY","j!%6 ow","Tf","i^r [F","OV9%h5nY}" +"Bp}nh7Ic+M","YH{L","y","o!9","K!<|","@D5" +"OjI/r'r*S4","WI=pe$lRG","'*MB","l","AQapJF4bTE","oBx>" +"WkoM","~H8GI,1p3V","Wu=B7{0j,","","xg)","IZ8l'1+I" +"Q3lB!","Q'N","}xi!#DkY&%","FNv","5a|FsN ","T!98" +"6","h#5{)<+A","#i7|fSA""W_","\u3].o" +"wI*","Rqk|kYv","]d |#lE",")FWt}%O9C","os","i@)=y9" +"","&u[Jq7","MsE24i/b%" +"b","{)\e?09""D","D*?Ybms*","vk6NN?7fJ","~q2Pc|","/vW:v }{J5" +"|@'o"," XH4 haX"," Y^0e>}","A",".]WK""",")bLp%mt(" +"_x&yU!GiV","WTc'5nj","*3NkkGk$'","f?,/V~","Z8g mW4","hW" +"wwE@@","","7Di?""Dt(lB","BU~Gg-","pd:" +";NonN$t5","","4G~X3zC3 ","IHHBOJ.","P}",">r*>W5" +"?x2""L4","sv|)/mN","+""$=1:8","p@~6#C>mM","I0&1Aaq7M@","n" +"NWnM;J","LXjXFA f","iUa;""","|k","662X.1;t"," " +"SBL6tq~B","Is|v9","KjJ[",")mgQ.;iw]","ERR?z","WcWgb#$q6e" +"7~a'`","C?6`","Rr>q&YV6I/","?b6w)G0","'jpc$","c zI9eD " +"_^_4{O&gbY","T^5__","w-[Py`(Qxf","*",":'EKBh#S","ljM" +"","c#sJ","W^2","@2`","\\OuBL""^mo","2wm6WA|x" +"8pkGz","V,/WQq*J7Y","&]2?7Q","EF.T9\@","[k","9YI)(eh:;7" +"$wk~","VkK:0Kz","u0!R~2","xY7#","9#","`j}6&" +"9","~S.E)b" +"Fw4l2A V","o`""Py","{Izy$K","OYsbb8","3]ZDq","8hPS1Pz" +"~"," fQwMd').]","PR&T.;<.\U","""gUK74[","EAn<","JQlWX5","1QI","%AQlZmNPE","2|A?","W^XqWP" +"a#,W(`B@5C","",")","Dz","y \O%","&5'U|J.8",";)SH","q(7","EH&+jZ 1!","HTh" +"bzr]","^;V8$b>B","1:aY5Yyw/","yr+&","noUrd","pkg" +"S(Cp&`e]","",">-c|%.m","","C7Y'u","X" +"ny","dNYQ","FWYe;y!","i?4","1,bY?=","`EjF-M" +"8;;V%","%wY","x~^veI","t" +"kYYbvo8<","C\|",",4g","Ax{sqF-Cv","}^COxg 9","o""" +"UPjp%M","!#:r.8&""Xw","W!/h2","^%^f~","t","oP" +"T{6","","Om=~""9s","u/o","cZ@.F|B09'","LI%e%65gY\" +"5 I","r","'F&o""e","qn{s $lV","*}tz1","" +"M","E74,J","=z3U,)","W[.!,","e:Tr&5<.h""","y$Iwo" +".H""","Qq42V79RQ","^Fn-6A0c{","iZ{yG}nV^1","Kw:)[","0-YkX}/0" +"XZc","","o%Z","","q{,",")J*z=wr" +"cvOCJ&O","x%cJc@","O2bJ2fo","m","","ob]_Tj@*v" +"_!sCApL>_","","qWa6A#$""ww","3'","m4I","L;8( {ye" +" ",")~z0g","YlIA{:Ey","","","k" +"ax#""rfEO","","X(/Pj}","CC?[0","sJI","","fir_*o","8 ShApVE_g","4k6m" +"&>b","c=9","!","I","","$/dYTpip/$" +"'","FGj*S|Z","{N8","eonWJSsY","","U" +"EL|*\!V$1`","Og""","","","lo!:^",">H1&sgt8Z" +"r~:","u","'/Bn?[Qf","nltE-YYzDB","I'ODVU{xr","" +"[I(]cD","<,/","x","","2x/fH!8%","O4a~X" +"})","QoMIAa","%G@","46;pHf*","}3rU @V","+`eU&/gG" +"HD\5","!f","j6OfLC","jX/UU)k" +"","","2@","4bZ+L{","Z|H","""p7u" +"PD'!gTiPcA","","<","0N4x`","n%\E7ol","$duR1IFA2I" +"","d_B","|","aa7;ac","","UviR" +"O@ei*, ","u'Tr","NJo","B*E^/Xo?Y1","BFk""){%:","TS?>s=?= h" +"Lt","f","PYgD c","a4/",">2?+A~+S","IS""" +"xG')3","q","","0k>U)ZI","uHg)U""X+","}uU1?*" +"M!0;Kgz_G","+","1:g}+","b,TC;%rTQ","Z+S,","" +"i=lps","[","$^cP","n2",":0,Wkb","9[URQ8>&" +"@3","UXHc|C","~DLL\QV||[","uQ.8 Q)+q","""","A" +"x2ZrZ7kTQV","","!R<","G%\fT=","E","-qG0{" +"'&)T%oz","{kBu,","OF6vL}FsX","OT'5=[nqH","VdOw+[WU","]SZBjN" +"","rdpPo","Yh4[SDH",".^t","fJttaStU","@%5!;^h\" +"/uT_)[","""{","[up:^Qgi","6GF","5j%$6xE[g","&l" +"&>-Lf7]U","z>","$qPD^>O8","&IzR","e","}" +"}8X%","yDw;c#""b!","Y/}~G","6Oh[ZJIwQ4","km+ _8D~","dTCu58x","sSXv","pBuA>|X;_","eM!):?EMc",".KU>","Vtd`wZQ}" +"y$?{2Ti","}GZcl-","\kQ4","mk_l7O;#.X","KDd[Qd","Gg0pL*" +"sg\dV'=5z","!S""nEgUx~","","QcOn","6","#" +"N#fa","a1|+SwUd5","rC(nF`N0Z>","nnPaaE"," ","yEwZ6DM" +"V","^YU~b","","*bJ3,N","V={@V{","_>#Z0*" +"I-~QAavV\","K6;#0k","o","8(" +"gwut0","","WU^","4","/!][iG,z","""W7+uh" +"j+?WYE2","w]&o4Fr","bBV^SU}Hv","9'C","!Q_7<","" +"J*ok_L.*Gk","$Xl","","_ut","g h7,w-","<1" +"Y>)?WdJ","=","""n","Xn2m[x","Q]mFLF","Sh/Ns" +",/^a5S;","Vxs%'","QivnALbO","","+b8","Sk" +"2","2KNAr""91[<","VJbQ","sZOSuh(5&","","E1+D" +"W""","[V:67mEA","~]c","J5<+pN#!K","%,b})","Y;.>:'SI}e" +"R>flcq%","4e]fB^","^>s}0","8*hE"," a%","lVsOax0'-" +"!l{<+3a?3B","sH","y","~` \hy@+","_'eQ2/o=FS","E" +",","*Y","c%Uz+']<","M28o"," 2o[WuQ0DP","(oWt3($" +"3E","_S""CKJV","x&#@hpdvR","aQ+$(","S","#r%" +"","}!Q#fiHh","I","","RV<[9pS-c2","Ot,1X" +"!u","yn]v5>","'","","Wkf#",")spuPy" +"`","&NgU{7}","zA]&&{kE9o","h,&|5","jWNzw","`[2p""lG2""R" +"9JEw>3","ccc~#;"," ","80UvW9;/","","zebGg>#." +"2{","?wLn]","OXM","8+","LST%bsEJ","," +"V'","OzdoP^ /s=","%*","goIg|b","u","KKQ:va/E-K" +"\xSS0y|2","&","n~S>A",";in)T|","B""V~,z,i\","._w" +"A","""","[e_o","CJhj;~-","3_e2",".%*61w3>\A" +"","f6bOj/M]-","nlZFc7g.",": ""s q~","is/?d)T","c9+sD3bT" +"C0a'R|D","|+","gNvw","","CCZ?F1","N\;C," +"iQTa+b[X8s","""H""X.|#B+Q","AP26-cwO","G","","""EM^D*%%^" +"!ICp*Lw","7[WHV""XH{","[@","U","'1P","RN6lj","F:;'l","*hs%1","" +"",",~&gR.","3fIG","rHMo","&","#" +"JnsY","@?I)p","%37+bN","yS)]_3YAX","\31p!_","n}M(" +"|5W4","@9j-","PB^HQ","{-cCfQ]s","S","m[x64" +"[x","C[zL>vh","h-Ix;N","1-e9 %Nh|","mwdP=M,","X2:d>'0@Ku" +"Mc={-!j","ovo>;Tr","@vC2xP]3","""'Ev","a[x68|<)}<","DX\y)" +"b5U","MDI","aQ,}$v~@7#" +"=""","[[f#7=Nv","mQ","f~\Z^",")""$-)qwJg","I+^]u" +"Q>`tw7+^"," X","[A_qc0FRd""","h_","Swb6$","vRY" +"Pr4","nma)mc9Oq","- TdOChW+i","WQk$A$:N","_IhGX_RS","cO","Ho3@l6","6?","X=_R,8C","GHr",">Yk9{wPy" +"g.XDqYM","J')g@","Nm87","l=?> f","HWQ``Hd",";Oh1""*z" +"tldt ","opHA{-W+)","*PVq*Nnxnk","8",";W7,peh'","-w3kZHa","","+/0|>*.","YCD#$","DowrP" +"m","c4","xd","E6(:>","","_|" +";&d%BsH|i6",",SSk","oZ4" +"24f,,4","bQf/","r\;","zlvdPU","","Vkkp1eZ" +"w@v4m ","","D&","C34$","ZAgUJEP","wfSP*DFUB" +"|\.B[M,|=","y","uV#$""","jS4i","w-Y","~V" +""" LS","{","","al;p","&,ocis]","qj" +"1h,: Pm~^J","tN5zO*QW","<9lU_","uEbd?Ms&$","%bA3O$a","" +"","3P4B","Vh*7lq2","hQuYV;7","0Q","~""3~3yvt,z" +"Aq","{8%N","I*,ssZ(:>u",":68[{yN","","""uu]3" +"I#]O","Gh)NfB##q6",":p","rENjq","o","iBwt(" +"Uw}}wTD","6",",e@rNam","WO","x2tPl","cR9h" +"_o8go","S/","BWo_","z","","t<51fb-j{" +"Xnp#=","(GIR","","*WjHzw","|" +"`ikUg","",",Y","CX+\","U9\HE)1Gz","Wzs0" +"Kt","VO34)\\!*","t~zJ9uYT-X","9`9iN^"," Pr","!F_+Mhf" +"ij+W","jG","","","a@eHP","~D2:" +"","f[fd?","4V","]{cy<>44]R","tagc@me=","Yer" +"=","_ ;Tc","ji36s//v7;y","!Uy.RT$z","","PP~.C,vubY","k3%am","2Gs" +"E7#&8hMZ","-o","^G&","2NCWWjXL_E","","OFIQ.r" +"k@^b","q","wb)etgW2","bE",":F|>=q","K$" +"jg6#j","i","c","^|)2YPk","+<","-4A!5a" +"^:P3{^S5","Z~Q","^.cKSYy","","t*34V2>","t-""Rn]""V:" +"","M?=gzH[v]","[6uvyAC41","d~D","%xe_X\t","I" +"zQkYWrs","(#F |*x-","Sd","h79{!","NQ1LSo)6","=v`fl" +"bp5tK",")YaaJs8","w:rSY4SNecLp" +"vCJ,a","wg;S8js7vt","","0d<@-_O)","{MKV^N","^2" +"d.D`","'""","x.[&","*dcV","{z","6y+0a" +"K{:u","-{u3aN""","Eqr8%O:!X ","5>!p","ZX>0W.~lx","/p/rU" +"taZxgwfN8]","","]P:9M","M30-ptS~]","m4.7\V#'?","dvB?" +"3rq%*Yw","%f4v:4Iu","","5oJgGtPUIb","JI","=D!R*~H" +"gkVe;yG","R\oQ^67)3","2W0|b+ c","/W@0[;]mm`","2<}8I@z","uq]A0=v" +"YI}","}XVL!C",";i","wL#cIdG%","eod 6*",""";[qK" +"r-I6%#","kMM","b<<#4f57Gh","LQ$EPga","kkVPwlN0","Y>%wr D5?(","R[3e,","-j$<","]6aw" +"foY#cWjL","h6{BO4$","c+oVNk","x9UO0","{RIdlhJ","o|E" +"h=ZW!:8Kl","oqW","","R,(5H.piCE","0T","*fAdr" +"e","bQ","","[u;.","/9r","P ^H*X" +"n=ZO@A,S)",":3OyJ];G2","sS","{SZ9?nF","""-0#","p{n" +"8N'p","oe`;S%&MH",";}p","v]@eS,+B",". Sv+p$","BQ1m1HqV-" +"19]4(J","$:+"," 4Pby*H","Gi26IA2<7","AwIo1~z","$_x" +"{,'","g+{!s}g","meNrc+n~",")","WIu19/","FUi!" +"t,k\",")4aJEL<","e3","18","=","aiBKyxK" +"11C/eT-N2","v$","O0Z","u*E9$&#dR","J#{}","p FFaC&$OJ" +"-~J","L:K6 ","?","1","<","vi6G]s?D" +"0-","$[:","H","D&A","JcPwaG,","V" +"9,f","aC3","","mq}","4","{U|@/l2T#{" +"&N""jfZ`","1q","sjM","PrM20;(7.","oA3xzI\","O!H" +"$QX~VR","H/uKon","y-:V","Y\L;'&;q","DfLF5/FDPc","3>MA.8]a" +"Z>E9a.5~","$F?","T~rp{iU","","XWh)V3","H}" +"2","MK8?$x""w|.","","V$F6J.!SO","","8yt" +"_NNof4","QCw-Qh+",".J4xa","jVGk=(+9","G=|q","]1vi>$k+d " +"C9GS_","1>R/V0y.","","!","4pS","E>O6moM~ S" +"OX7-=<(","Zyow$o:,F","Y2Zl'Z","+9Q","xC x9]RbF","jp/RqMEwy+" +"!4BKn>}t","EN","MK","mKD","zTZq'3j " +"b","{v@","QDSA","{--/MV1","F~<#`{9^Q\","bY" +"DBuM4""jm2","e{ho8:","l8S","7","7I~-&S","D#M(n+u""" +"{n~%(z8m","T{o=Q","5","8!>(k1w#","!VaD]",">b~_(^" +"JB^(gmt","kq^,","A#yVx","i.o3ia)kRm","ze'rP","[k" +")#","-P' t~","c/6&","U}","","a{tY","ZoU*EKdP","/X)qa","O[;\RyO{" +"Y","*r4PX<}","PGrVzC+Q","'W;LxII:xo","s&?a2xX","(" +">,E.D|OK","DuqRfMYR/","|=c[rOgPI*","PHCU","E[hAIr#!PZ","" +"|[dE1`>u^","-cCdxXD+>X","","0U(Np","","hfDi}:tI?z" +"!''G","I","X9","a","`4d,?]","B%)("" Q" +"F\l]\","I8)$]<^~","-?v!K7#p","","!cq:k1","/`=" +"","DzvVS","8 @D^BR9Dr","%z","^'GG|oeza","@-" +"gT^9f ","","","Q}q- }%G>\","Eo1<_&+","tf" +"`{2","bbVEi","GcMv`P$]a","bU","v}|B$vu=TTe-H","Za","Vc+M)kP","","Gog!3c)qY","t5gq|Y2" +"'+W5","vs","c9P","/hn)o(B","0/" +"\+EoGy/w",":7QF$]|QF","euVo|G","HNtoa5U{S","g",".-UFU2" +"",":OX+","9B","N-R;C*J}H","/","]" +"/z","/QI.l@","","n","=","Y)tF""sb$" +"%ZL)LVy?","Xkp*,",";K","w","bha""","eMqxFJK" +")mE)Ldb","","{&H?UX#%ZF","T.3zEzOIX","""A2""Q |y","on" +"gt,^[9>As","Vt*","')T%","M","d@I`7n*13","&dBxYQ-" +"*"" uH","^",">u_I;)o","]_f3d.","t#bY7M](","\{Ge" +"["," ?""o~aBm","r2*TV^2","{mP^$:B","","3""" +"*S)","hUS0<*9","x6F8!Yh","f","lr%""dcG;","P`e?\kp`#" +"GqL[_G","X>[G:_i","S5SIm2","HZ","v;gZ=@","lp""V+')i)E" +"'\%","","M@c2","1|_;IfHt!t","Na[b lUC","cV" +"","","N <","n","lDbQ","fOT}A&sROX" +"j#=9J","kGB9l","Krq2omq9","RH+[b","{ MFOQuYOY","1?NGx\~)" +"vsl","[r","z2ZYUr)R5","jLO","=9v9","IJ'S" +"6[J4","=.2dB!","(~qz|F","8S22","3.^bP_2vzx","p" +"x`58Q2","XlP","ATYEQ",":eF_8Ml$RL","","5-?UMGT<\w" +",CB]|vx","rWOb{k>&p-","Bp0M$-kap","yhl5<5f:","E]a","]=/HY]@" +"tn~","`=_829iI{m","V","CI2,(","","y*W-=" +"YQjb{","m","|?U&k598","","OZ5~","uf]|2TREJv" +"\"," >\$^EY(8_","Rjm*""t@","}C","5%1Gah","O" +"(q>Vc5a","=/","Ye3drpl","f? = >>",",,7l""%v","e!%ln^.<'" +"=mnib0@a","sr~n<8i(aD","ky19#u/ApN","u","?gV,i2","'#-,[:" +"B","h(GdA","H&","d1y*j&k","'xT-2c","7JlgbHY" +"9p1k",">","%% iXeV1)","p'z$we`=Pp","Fq","I>Y1" +"ZsJZ","Gji","\","*#0 h~(^-q","=MLU,A8s","dm.m3" +"0.e\O","|nW","6'?L&<","EJa","JU1","s_#Qhx" +"","Eg9","!mOw=-!+ol","*1y,8\=","58 z[","IGE^Q>l?fI" +"Rv(p",".vd~96=I>s","3u%+E","#B[0@","c","bGq" +"SScmE","l Kh","l79JXC3=VZ","k2","$o""~","p" +"uh","ex@7k2p#m","bl","v","c"," $)T[V" +"f:ryalK","j","""|ARTeqi","","K.)4" +"N8","3u~","TD","q_I5L2J3RJ","ye","h8[" +"Ko""$","uu')*69X","3*Bh/P}","\[G%U'6","6%?","5UPL,1RLK`" +"KesVA?FW""","{lIto~*N","_=?g3","qes","8},","NT)Hre" +"S4Pd","}BZ#!&","}g'G*IAs","]=tw}",";1yXn9","90" +"4X+/""{+Q","|gxc6","4B];'[Yj8;"," \5","d]","" +"X\rKIB{V","]b3N","q","5vTkM}T7","lHM2Ax`~[","dhq" +"jjz%c;qcn","%hr\`~2 ","","]nLj","ndwjy{o","e_h" +"Y[$n?3"," ","!","","$KJLM",".zA" +"CX'8`Y,","k>w&T","E!C?Ut:Y ","Zre&","","" +"XgDa","soo-9u:0q","K",":`(Vh9RTCU","OcN[FV","`","@{T!""" +"^\s8:FN'","3f965+","B$nZ8`$;$","N-<","-?e","-Z;cPI" +"3lG1=W]NK","O!E0I","$t>r$/Jwo","E","","Bbp1" +"","J[4","J.W^CyoH",")cD","L","eV&[/" +"9@HrYJ`3Fz",">4"," /o(oz%'Y","lnT+","Vehm>,E*","" +"^qTZ_","%E@5So","~>","_sg)T_IRY","ppXj4","%4'" +"fG","b@D~u","P","0c","T!~%w&anOY","q9?uTURqZ" +"~%v`aU","P",")k","*""","4QXuI;\>","," +"SElfh$",",g5x73","r64`Y","""&Hn[,dk0<","R-c*wcq","""8^O5GKL" +"",".","2/;","=uQO8Q0E)~","?-T0-K'a0_","*rjf%5 n8D" +"kQAL2EM*yH","ABf^~84v7","","*/","&:]'.l","+(@","Nz*QRNB%","T" +"_y$##@","","1N!7#j","D7)DVq","sQr X7r[","L" +"\R*,^A(","yFa=6%z","0#[aK","Grw!S$lY}h","peLP","NO" +"*!w>F","o8EU@","I|rc>2",",U% ]A","0","JS/z " +"p 58","@Py@~HGC%","42B^=^="," &3sM","x#3J}S:k",".}67" +"","m`,v","G@4","s","|t.}""h|.","b$R" +"5%","N0U4","","9F;r1:""H%","","w7^&" +"l\{m'0","{","""""~/","`j34B|","y*CLY4E4","*ny" +"BHw}Fk","V4T9V","","94B","b9'yX""","@q\<_Q" +"b67Vf","1b$8","Z$Ul","pvp0O4x>","3FvrBg?j2i","=M" +"+ItwpkKY,","^ady6^nv","","-wsfp?","LvOcvq","Zw" +"`te",",B=","mK7_A][`w&","!8@;`#!n+","@3o46!S\","" +"Wc)E=FQt3","`lkmDlK","~GO]!%E2Ne","[icn0_w1","sYsucGc","0" +"","0","S2mFv1y","sm""Z :",""," 5$" +"","ktca'+_","esj","","52l=I]5","e,]I8Bn" +"P%=2T[d","/+","63$Ze","^1pKUdv`#","=r!","t>kjX=.|" +"~a|/T","O$S","dW^$pZ","/NJOxu3","m%","S""giH{<4/F" +"sV9w|60?$","'-k@mf^","QJ<","QjO']-D~","EasA","0{8l`h&#" +"3-/ch9Wx,$.","yN<.t&!.","d%IVo2Vzf>","lH'y#fTEa","8d^XPhWmNG" +"l8eVte","{gYR2hLz)","[WxTo","E","NO_8)d=I","b+@ZqL&asM","Et\" +"KW d","|M!MP+&Oas","\z0`","U","g.k","HSMo" +"E],@","7=Kx$""","pA3","CcM","l=a!="," kvcxW" +"`g","Ym6/V+&3z","I3""`1l*Uj","LrxBm:3i","","Rw;*X4CH" +"N","Ag","""F7i_z|&","","Dl1 {N*05i","?N" +"Ox0hNYyJ^;","","2U|-","<|kg=[SCxr","7q?Wj","u>J" +"wrkVHq{","UCs","D","V^[oTJ","C4*K3","yz{Q" +"M}`0+?1","1F-@r?","s","]","ZvOZYk" +"Hc%lPt+i",":#j","7-v20","I@\/{)^","QX",";Fw 8G>~" +"d-k_FKj,3V",";M8Gi""`F(I","m","Co","|.I{w","Ph>NyA" +"9@@?;","t:$x:^`","r?i","e","vK@a&G(","FG3\7oh" +"yxwt\5,o,r","&3s","M?c~oD","'jFO}OY","~t)V~.","(+w1j" +"=pRx|!E","wE>*zq;0Pk","","YAm&%VT&)","j|.i","" +"+Pi","*g","]7>%k;","=.*Ywgzs|","4<\B`$=4","vKY]" +"H'qizT3 u1","|l2#vYI{F","0","C.W@ ;8","=!&&;<","p,2{" +"FMxz{KT~","30}LX[.C@E","\5Q","","pAM=","r+r;eJ" +",","'N!*""","@","miy#0pL]<","mf{","#z0`s]" +"g r&","{",")l@9","P7,j%","0Jt","v" +"&^('>1q9x""","cSE=x4=5","N|MptOz*Z","","\;x/8m:","#T" +"0VJ[","","NE[jM ^X!","""^(Odv AY","","0" +"%7MW hq-","Nfl","?","m:7Mv~}G|","&.x&1","}*+qI'%" +"TLS ","8pCh0is","a","Vdg\ m","cP+n,X1k","/TD/lD7=(~" +",r 3a)o","mOV==","/6","~]eD","","=u""c" +"#-'mX","t/{Bl8","69_z)g\|X","","Ss;Avh}m#6","Zyl+?g" +"3","2:t}71?6j+",":[Q'`K","","))V","c" +"bxL/((`N'","A2","btd)xs" +"`Yo~7*KPX+","x5b!R@#","Z)*pc","VrckVlGQdy","","(g%'C1","n+.l0B6" +"W)#l? 2TgT","7`","~a;L6","I","q`v/|lbQR","$" +"A""Gx{s","dc:vtO;","lx`4,a","V-q w'%","M","|q]" +"*8","K8/|;"," ik<`N1M3,","/","","FJc3JZ m" +";!","M5+%]","+Kt(542;","!V|","pu","]Izuu}" +"+2W/#]R`","",")B^$","","2MK$<""=qX","^dS|" +"z*","","}R~eL)","-]sC?%"">Z","#R82z)vsh","mCRO`$a" +"s2","B&*Y%uUOOE","T|c!","6","*iP@.QXy+F`T(5E","aE=o=f;","<","dd ","NJ{ &axo",";P\cD" +"UEd`F^m","&-gNnM","$28lUuo","","Wl","]#" +"d8M","+",">","7","ijMrGEC6O","p" +"f@_","[","x","b>\f_|8G^","N;,nK","lK,f|Fz^,`" +"frpz","","jI|qqIi","E9N%x","WT","bC(sD" +"t","MJ1> x3","z@IF*y!zP","(T!gW,o","m1rNM","d""A;" +"3Qk.' ","$r_:","O=G!(","","!cFa|h.Q0o","'~u(\eY#" +"r!vO[4","\","0_&w`]mIb{","m","","r " +"V","DDU#eF_f% ","A&]a|2C/","*Sj3@$A0,",">1\41PE","Z(P3aR@" +"","u=Vc3ls","twjqlZX*","J_,H@2","?bgPa","aIj" +"Zgw""MIQQxG","P","?{pK^n]","Nr1 D#xJ","D","iVnJt" +":E&n","_""K4.","%x]","","T@Y\roh~N","LIL9i'8?" +"","~u","nA3 $V>0","!4 {","WLO)I","}$c=1$" +"f}Uc7g3LuX","+2-P","""x'^.","?","y!U!2-Kp8x","~F" +";1r""A>y9","","","=","P","" +"tjT","'","Gvl-"," q/!@N'0'",":XM","e:","jb?BuRT:" +"NR.%",")]5Lti(2~@","U=tm","WrArhRl""","y~","Eo]P" +"_ADu Z*F@","$r>X","mOJUz","g2","g6[@qWo5","C$" +"o(x]p","%f\","aQTSj","","Yh,u,X","5" +"JHSiP","WS@\","",";Y,{QZ`w","+![6/)*","",".X6 ","zx3p","l+6$g","YB7#~+4" +"W(XOUt","","ABY}~","JD-8o`g-XJ","l>ji d","2:Fb&Zl|" +"2h((k","P4]W,)","@zn$ux","P=","+f","#_" +"3`|e#/Rcv","Y",";8c{","zGY5ox",".?M~s11r","'r19)SNG" +"w|x$1#","l","{wT","*jYl-y|,O","$ @Lql",")um" +"Z[Q#",".jV^yJ","ZyyQm","Nr<+","(:1qzgtB(","f" +"f","UWn""Zf","n","7i[","gY=;Jx","" +"","MPH@{e,c*","Wi?~hQzOfX",",~Q/w['","t?x{<#N","CRcD" +"x+%Dvo:E{","8 qo","&+&ZDk$b%x",";\0> &^KZb","y/TR'1t+V","" +"q]\0|","iU>","",":#?A","L0;8&,[yhK","y[C7" +"(.",".et","a","\P@2Y-3{","","""wU\8a@" +"c[=5^}4","&I*P*","aLq5\&","N+/2V","xjE} o""","" +"M[U","gd","-4l_Q12wq","ZZT]L0","","d.kE)8B)" +"4uGc8E%m)E","gh-+X","}i","gZ9@?","Wwni","Xbq~XQ" +";:","O}ojl2i","1","^a~$","X^","$hA" +"GEJ","+`;>:<","VGU","@r#c-aRYm.","J>>","" +"fzi","",";hQZ_+b","F6(=` TY","H","M'AI$0D&" +"+","5!CJ0'!@","4GM}q%#fE[","","^J#mf7&igf","4T" +"f&={4VuYh#","&K%z","BRvl","<~UCW7h7L","-F:ve>eGd",">E" +"HIO","/~1cy9OMg","G/({s","}3%","","/|>]w%j 6" +"""#L6)","+]",":""^","","]B:ZZ","A[" +"uTPB$d$","j(R^","p1(*L?","Hu","","sU(HeC" +"TCD~","h^,q2|^x","!{u",".>^B&" +"b@A","./CXutA","UtN/'|","eg","k{OZ4G$6","`M""v" +"02#SV+,","kL^DY7`r","}g5L!c88`" +"m5myog1s3",";x","x","R!n8H""","6{S+_","Hc" +"h5f}n4~(`U","V","uCIpSU6 0","Zw7WX~uzbr"," Q*c","" +"iqH+rA=5j@","e-","+p:S:"";","Mj","s)L[INYY","U" +"M","<_=_pq","(","""~ge}/<","N","iu",":","Ai7 oxh","Gu" +">#K","4^Of:&-","l n","","""b_","tDp@B6" +")\&e^@yFb","c","- X*o","Iujl,;X","y ea`4","]FP" +"8;;tGHb","5GI(]8%","0B:pg,a3^D","E","~G~I{E" +"&|*YT^u&","UI__:|l(3q","z","euO","vf}'.J\MH","E0" +"^","&d?G{&",":\r","kD","EQE$","E4To~<" +"L","Kc"," ~","0s","mx|qUV","JuTeFhO" +"}N'\|u"," $fu gYq","5C7[","H+i","@3$ K:","tGW" +"#^","!){IZfj[","/$+;P]0l~","5Ga(,V",")a,","j?@Ih86qCz" +",ev%/3Xm:R","","y~Ev]]>","G","`7\S","" +"DbsFI.L/Rq","\&T,H^","n0+9hT,Qo","$,~Iac;i%",", ","rV`?gb4>" +"[)ZC","","a/_X$N","Z3xrK^3",";?<","8RX`jTF" +")'pp1nTL","\81kZO","qX8","#I%","qe|y:^=m1","w-ka#c7" +"]4TlU8kM","lR<(","C","0]O1E7*","","iay" +"G""v3v!1d","o,","\1we7dU","9kc#Mt_""","v&""q{ R t","t}ZAG" +"t","h/l}","_","Imwy%CG>RW","4SbC",",vE" +"t","@3>ml","T>}C ","pL","C]i(%S6e","8W1@e7","26A4vD@|b=","7l#\\X'gjm","ZB","/THj!^C","Kfye*" +"<=e","}*","t}kQ&Q0dg","|0b","0g*Ub!PT","G" +"n)FmG"," $)eUFe:","8$VD1'4X#M","d]yX&)","Rc0VIn","bTK;'R0q^O" +"-E#e\XH","`%4Uv\J9ix","8F|x!t-Ic","K+=!91k","9>","/N" +"RW","?'F@Cqwn%+","+.W","m~zxbMh+}E",">xL'PEk$","o8(Jn3#7hB","4y!@1Bp","D9m;","6-" +")=,","l =,#*#5OB","BNx","hr_:hKS","PS","C(AZ#o+," +"#HGXVPSQ","SQ","`","@LAh1X:","}q~(fo","P3RIUzI" +"fl6A[R","mj(","EJ/3Zpb-j","d","","rTr@^m" +"Cp\c*Vwsx","'","|h[;","G","\\U","Ex\X3Mx6u-" +"st c","","U!Qz6Ebn~","5pR4UjCjVl","i","/j^4N" +"@:","","",",w8)pEm>b;",";","gH*J!c" +"","XBs'!4","0&:kw","X,FD*","0PBc_9","a_" +"d>y&","THAl","","8e{",",3DK" +"Z7>&JO","XFr38","`","Oy1k#^",">iT3$!.","yMSO26>zA" +"kD;q.T","(ft$]",".t~'ecyAu","hd+","|)@,x""$8","X/b}1Z{" +"=@>j ","UnNh0I7(%-","p5F~m","-Na","WP@^","P(#Lp q" +"","(""nXpt:3e}","qF\","ypb;","X$","._r_1Q[G" +"K>Biv","~J~!m:CW%","y`ntR"," rY?_E_c","'u?,qp","x" +"7o(AA",">(;v:","z-z5","n }A","[79)/6.: k","" +"2!4CIt","2W@C","3-8[B","bQ~Ug28","/$Esspv","4?Daz#aje6" +"%[jTK","YeX,wa","9A~|v","D","!xR~X","h: ?smk/G_" +"/","OI>P~Cu@$","v:IQ\ZnkN*","J?Sq^6Y(","}hQ%","(N#" +"^f3M]~zqn:","jU*@%3",";z8`HsL*|","-qq","T2","`p5""" +"x*l-","0F9MDy!]o","JOynuf","9%","~!","5|" +"Dj@zV.","E}- |OB","2p^Z","B{fi:?","eL%>Pi]J'","9C""kwa#" +"LX}","PrLfFS","/}>a0E","("," AzT`6]W"," %e8.>4i" +"&Q.W3","""%5ohk.","'O","|eMU6y5h","?","T" +"A'"," P","'","(9-","","" +">?""_@uqs","<5IOk""7L@",",a@y","NbodJ","359fu`NN",".]g'\9" +"K","3OdL2","h""q_j","Qw","n5&=Er`D","OCU<^G:e","V?83jV","je|?","<",">=I*C.^Xj","Cmev" +"T","j4,}xxcV","cB","q?H","Q.HtJ","YX" +"`'.t8","N\","/Dh","j",":E*@Xj","Qs" +"b;fP""MoAZ|","u[hJ3T&gV","HU","4vT`[z","P2$*","cn%S~" +"?r2u~yr0","#55t","=[CUa#q","sG8kHNl""d}","","SJ9" +"TIUv*","vZh.EU\-","A","U","m","S@@" +"",";n","03Jp","h)","Zz:w72dcO","W2tdn" +"c[&","P%!","OYe#d","TpT)","U.+^`K","M4n~3vq S" +"h}POP'","]E","J4?3^","DGg7DE.'J","5{3fzx)%e ","lct;y9jLf" +"J3fCPT","CU~:jg>TUq","J","r1K&","%yx"";S$Pv","N6vZ1DN(" +"j9.F5","","\kiTSFFk","=Rx&g8","0s~Xi","/0peGo" +"{4(X1","<:{","\=F-!M2[","PHTy$Ot!:'","[DQ","L!f[$]]R" +"","]","gJ~imx(c","T.","'^&=9Z","_+Q#7(g:" +"M4'{","\N^","^","E6_(Ap*","q]{'n]","Ql\HKo" +",E+>j","","F{y*","","{6[XQ","h9" +"?/55z>e'x^","/","","SO`*jK","","*ORgj@QyG" +"!cY","uO8xt7 3T","ld6~9:J","RlQ4c","x","5l=m\R" +"63vLt","DFyH","=mm(pc|q","(","vX",";+W3S>;E" +"""C4-SSl F","=LL/+(","K/Eci","","","" +"WtI7","D;ou.un","R>D?#bym","=.AUe""GD","v'\&dfNfbK","!(""V)|)}" +"y;=n","es[T*o2","~kARTB","@)F""X",".!74L2Kf*G","4" +"xwtD","(.&$","$Z5BJP=rXD","ZTw,""9do","g","V" +"R&g5iIJg","Z","vGP","wG{Do","1Z","KRN$,b" +"+2z","9dX/j,\G","@=","y$T7O<{W","""%`jV|YI0","] ETvdAF" +"a>Qk","m@VB","^","","","U" +"H","""GE&","$Q","O|2-","","@C_l""ct`rB" +"BG3","ie","","{3h6'Q#","u" +"rf","","}QlF.w&","ni&a\","XY>vp","5J%51T9" +"|l","^U","A&ra,.`~","rQ","J6;rt",",{lo?bMh" +"WrYT?","q,K(gK0.","?","","WcEt","qL","`e|Elwp","GnlWNJm","`7\q%5[=A",",""}" +"^vV#","y;.\G&&\@","(URI@6a)jQ","m]~M?{is","rYeF|","]N4" +"B](![19;W*","w%J0@;",">OO~aO!mf","xPmNoX^[","jg8'JU","sYq*" +"itA[","@0lg|dK","LP*y*","}+HNr","?]nzT}","" +"tA?m'$\","rs5I5gp","6i*q","]",") %\L""Ce&$","F>i" +"*~Gi#","?sO","rj4=k[Ke","0G;yy","Ru","*9TU:$$.O" +"]","iS","-W","%#~#yBr","s","&U8r" +"","xRBII/","s$#","kSO","k8yhB?j^","!V&SSwp3" +"K>Qu","o|'PO?*q>b",")(","OcKra)^zy2","j/","yQ}/^08" +"03q4o#@","b0+K*yVO","RBD&zp","d*CN}Q","^ldUmLC%(U","@Sq|;O}iV" +"Klr","+sJlb|It","f","vnJ","",""" D[" +"ysRWT$B","e.)","@&4>1*""FLl","g8","$[d>va9","UbeM" +"","IzL3Ad$","^mGX5TK","rL,cp5:h ","Eh7)","""rDf'L" +"Q(+#c","Oct*:","","a,",";O"" bnX2","RE#" +"v|I5'yz]I{","bf3sNoy","^+N'*+""""[","Bq4@ XS""in",";j-t@Eekg","" +"9","","","J""","QCiU$s","5B/n" +"LpU.F$P`&","ej[p""","W?,0T?.c","}.L$a","#gUoHK;;","&=eHz+" +"Kt(|A","+","SY$ ","P7OfW)R","7|","whiJ?L=" +"w","+\N>_p2","BR^5rgWlJj","y9m+","K O#TO8","U" +"lEvw;WR!q@","[UCCz>,^;e","j!i&;4wcr","*V""8>u/n1","!]cK","dX=39:U6Ys" +"Fyf,o","1y%.`D32q","t|O\&","wb )|","yH{?]R","" +"]-9d","Jj+Rpp+ C;","1v,C{vlO&f","+SAr}""","@+]JtHx7R","4D'" +"~dKoFvL$","iECeIUXM!","q",">Pi+","l9nIl","8$x)(<1t" +"f_,CGQ3Xj","!","5c>N","n@3/","-Rvm3","RyLv" +")","g+{C8ep","=y","t8,//`oTw"," lS.'Z"," " +"G69JD&*",",e=","4dW:q\c?","FBPq","I","1p"";xgMO" +"p;5","","|@]","/0c4",";y2:",":_'AFa=" +"(","","","mJZW~#+VJ","z","P" +"dn}Xw7,:R","r dT","4x" +"R6Q.6r",">j[",",euQm)K2:","(","<","I'#" +"","k-{7,JW,{^","1","V%2#oE","Vh","ehN" +"kH}pCM","FAtz.d%%","m","VR","PU[;xxP6g","j6uUPb*I>" +"DfsKeW","e","eG`","#H","r""O","" +"9","Hzul4","#%Ypyq#p!","@aJ<^?r2N|","c51 ","5u'hyQ%DK?" +"%(WA*EMtVq","3-V",">eA}3lY`;","#A+ FaSrI","v#d","uy?=%?`?_K" +"J""","<}lA~","mW2NMb)lR","2Pa_?'","p[RitFhgG=","MH4m" +"?lnjj|","T1$","","FK","K\'[g","TN" +"g","","","","l=K,2J","o)\j" +"{U","E>w","7 'n$","^o e8&y","^Tx952","" +"w>S;","6kEOp8AOf","`h%Q2C","T6_7","h'%W","M~wi:fy" +"c'",":plRS3G=S","H:]Fm*`","NkOlv.","a8Io)","(~mi" +"r@M!aZ&-tWFh","uo>*L2`9f" +"A","","rI3-`YN","","Gh `>","an!S" +"oj>w\G|47n","c92C/nk","#","Wl2(yA","Y'","/ZS(32\" +"1QcaxxO","h#","L+-5PIN","iFyXP .&=","WN","E^Tl" +"!p~$[yne3Iu","",">tz!p&[*" +"mV4z","4+|A_QZ['",">,HPML","$}j([""V$X;","S3El","$" +"H?","/jz.{^<-","dm","O","gt]","hHrr?" +"tq""5je(\","IAuoD","g",",O>JKBp","SN","nS?" +"0i6B","x8^Mvu6yv","].","vjE_HR","1YG;zP~[","bR" +"i2","h#8zA","","(~W{CAn~[G","8E_|W4c0P'","" +"3$upm","""vAmbyb","msU9 3eI","hg@","2YHfF^Og$","m" +"[i","'UDP0Qw3","NHh!@M.b","rQ5","aWf)~6","bIjX""nh","`e" +"oj<#YqrR5","!Li}D:","deQtMk@?rI#","A.p","7k_h5M1V+" +"","","uVa)@Pa","3GB","-@dPF","%" +"eaI","<_f5","k6Q,!#&","{tH9e&*&bJ","TPV5OCM","8zS)'5" +"6F&dXI`Br7","7JM+7","mB77Uc_7","@74#","`P","M(\#>uZ" +"q17$i","","L^e]","75$","Q","l=m)KS" +"`{1","Z'! r","!R","WY~|l7",".wzMw","%YI=BpO-" +"r81S>","c={*","^0}","kT,r00+ggb@B_S","9_T\4IgTkB","SVj0Z" +">_Q-qN!{","kb}{FwP!I","?[B2\q","E;>IlZDY~","(SoaD","EX" +"!","FR}","H2jx","BQBnG632a","SO;k:""","~5VQu-O" +"E:#","BQ","Q","kI","D_","" +"mw0N?=tPv","]L","W6X@s_*Qg" +"`vX3Nwlq.f","[Y","pm$[;p","?8s.","1kkM^%!34","#5%bI;K!" +"!%M)fm/`z=",")x;NUFk","K2",")","",":3" +"Ndb","NbP","!k*==","8/18eI$","2b9KW}N0:","mhH%8P8" +"s70","","*x=4aA","wv9)+","_|M","9y`&" +"IJyF+;","ek","N","gs+",",","+E].an" +"\.::f.ha\m","* VMjbU ","nGQR?y,.6M","aEq/.?/_~","$qz,","nad""@gc3" +"(-","{nx4?QG","us","*!e`l;&(h.","0","<" +"_mlE.o2",")gX<|oNMpq","pl","",">%`%C7p5T","N^U^='4q" +"^p(R","K+9]i,","p0]EYeR$xl","QoP","db1'-T","48s/ioJ_>" +"S","2=t)T>zBd","","n|%(*~f7","","" +"ux'e","M;[oF5/$^R","'QU","THZ<","VrW[{fEX4V","l,dyipfMz" +"Bd.a","(z-W",";V","""s"," N","Q)B`5@5" +"R$oxUv1v,b","6","V$}W","%Hj""+2HYv|","cySY2U^HKB","x`>=0Z7" +"%","g0~3ZePY","D^q","y}E>U{(","<7a[zAraB","ha" +"^","W9","a","}0,A@","sV","$J" +"H+U!@!7ZM","y6c]I/Z|","eONy}" +"a0r`PJf,","","/0E,","SqW4ygj`","v)|0J-`F 2","" +"M^XO`5G","PErd3","'Q$\","?D","gzG","{1I0N)+.vM" +"A","~.'g","BgT.^fY_#","vow]:PZI9W",".c%T5","KMd~f2e]cT" +"/@-nD@","[/|+Q5tdjG","L","gh+`?g;`Xd","i< I34","[FA-9L&H" +"}7","[HND:;'5","}PumJIN:[-","=**","%o/O","75tbHL1,'""" +"K^!","A5DZQ","a/RLgy-)","_","j eUlA]*p:","" +"LD=hEhN1~W","d}6&S","g&8QM \","V#b=Kf6","J?","8b" +"t.",",3ZS.(l8m","ma-","O2E>V{acu^","YM@3+!K","KV{" +"@]2g?m" +"X ","Oe}36+",":ICO8:""kUe","]pgDU|Jyl." +"}R5%?lQBC","9Bu","[RU%O","V&l;&Ve","B-'","lR" +"%-9e","=.JKh","h/","33","~.*1aLx","b6=M" +"T5h?]s","e\2","*3rS@>o((","+\","/EgP6lI?T","]mM5E]v" +"'nmK)5Hls","MqP","!H{T","= 0","","{>7" +""," ","","","pw1","" +"I311m","IC8jc=","?KMP|","-77n","szOUXL/","kd0" +"8xL|_m\b","_{46""N)-N","=L*""+^~zm","k>A0","3=.Wz,","$>p~" +"u+S","N!D'E)","dkNm{","Q!","z:_q=90","b!" +"'+P\#f9{!k","\","8$8L{_iJQ","LU","z","6z-E!$""" +"m{Rs1E59zR","]#IZc1\","""8tAZOs_","[(","","0eWk^5" +"","0ZV","&a.{m]i,}\","'ppVg","gzn='~cj"," UZ x4T" +"nt pZiE,","uw. |'","0&86o*=QA","WZVisJ6b",":_z(^","YfJv&," +"(Gh)A{l""K","Tyqcqa`,up","0oAqe_","W(fBB" +"","WW%r2{i{X0","","\TN","T &*4:Lp","2H" +"Mgt}l:s[t","&cY%","&F{U","","`_&,o","u75~n","@CEPSu+" +"","-+YVyf1s","-","w:]31S.`","f6",")R" +"F","j1{","%WEq7lkVAx","DE\v5)W","`l",".Uu`_mfb" +"=","","!FbodK","3f$raWo","@xHx+%^","b^yh" +"Dx ayqT","-""`L=R-.SI","R\X{0","FD","4{i","]?MT%v" +">XKzRlz","|,6""=","YO>","^tIDT?d","tN'","];0uW2%" +"%.t ^m@D","F",".t;","","","@" +"=?g0I|Lcp~","Zne{R<","KUP4G~At","x})","""j","6Kn-#TQl" +"B=tY1[f","","HS?Gv2Nub","6^","B'kj","HvEL" +"I8`IH0Ld","E","O=I';",",ja(O""#","8","m4m" +"evO","","@IeR","|*.","l{N","CqA,n}gI&b" +"~MV1A6","@1WXON,|","vmx0 qY","&DD","~L","qK=lQS?v" +"Oqc'J","ZcYAFWVy","{Absei","lx1!nc","w""Y1Qa","O}%qn" +"H(c""pHi:","R><&","","~Ek:","*I[c","!twJ\7ME" +"*T","BMEf?'ZS>H","~A!","BL",">PV|\","" +"GQr)","" +"rVe","(-zfq),","FrU","p]|7)66[","@n*^e4","gex" +"4>a#9","d01H;s,Wh","T8<=e ","8BQo&o","}j>/<","3 i3; 4O~" +":vh{_Q(/L","@vKp4","uN2r)","P0,^]3K'3","x S","n^KQ" +"J4=ySK"," uOPOqA],","Z","=V.}yr#M.","48","f+-4S)G>" +"S;6C/mu","Sg@4g","WybQK","E4+9vR .W%","MZ","" +"e>Xr","GqV","a]-Ksd=;;g","{c')0N!","MCi","n|??" +"VB","y)#2Crk\Ip"," iKqtw",">QWer","}{r5EiC","RYitXl([" +"Q","xq1m","g",":d-","0s5q+#","pkfL6jP" +" ","K$","MI6_b5`","y-KT","`q","vL'=6""r8]z" +"","IN","vK>Y","]Tp3u","rg)v`u#","D MOYY-" +"d@|4/~Qr","t;'pf","V%rz!<","","[n","?=" +"w}O>Ks","c_","WS","gOB&>NhV","p[T6W3A+[-","Y6$m.e00a" +"+4SH=YX3""","i1","'}UM=J%\y9","Ssp%","5ct,?kY","I7O,C^#'q" +"4tl 8<","gi.","","isBc","yG~OR","a:" +"[i1q","!","ly@HXK","30@%vS""fB ","@GA","49'yrjXXpQ" +"!H2KM_vP","G(+","Ta","x%A,;",")P#|?","eQT~" +"""FO.eHhc\g","]x","Jk1vPAwd","","}"",VC","3YY-;" +"Q#:!mN.g7","~-u^Z","","T`;L","7(d","W`#y`" +">","F<","[/6uqy ","MK4*","Z+","U" +"/Y!duX","N[/","Id'k","w","%]0| Y(","W62ma`Sm)+" +"","xh4Y5:b+4d","1g/",",bBH&T","","x6=a6HhO~" +"","qMa","s@pn""'H,","@i<","@U%C","Z. VO]" +",Sb3Gl","t","44","x","~","Zw/P8P" +"N[7","K>nF54""F","|&/@hY","B[2","Zi)","kPy*(\tIEX" +"eH$lI","B","E}Nv,31V" +"u","c^3aLxEs","`2f","$&C0WtH9","9=fzolN*^z" +"zto9m*{","q/UC","$0d,1Z]*^","1x","b@6C","Eg6ld|k.w'" +"fbs$""dTdOr","%OcmCsma","Z","3_@da|z","O=","*x[-T8!'""" +"P ","VI9l8NDsX","!)Mx","m7$O+r^CXA","=3',#i]%","}" +"jZQ26","@}J(","!H_rpoe;",",Yh[","@1O@bc!&3~","Q5|g&+" +"MmaN ;on7,","Vo9","XO","SDyl(P6""D[","zN<,q;UGlt","1vu>QF&Bb#" +"&41sP","1L","N","]d","W.SVV[O&$","O6" +"'bdA","",":'d$DGmA","}","","]8Q9f" +"*/87(Q$N","]?","X","|?^#h"";[,","9^o:X@!<'","O7" +"v@rRy","]","\rhx,pKh9C"," f$UE","BT[1d`)a3","UZ" +"&\~7",".",")No8!{&zA","R>","fBt5rs8<","mX152/" +"&","ZEP""","GV0=on","/wu78_S?Zd","@>NT","pO4-$oRY." +" 9I<#5kPq","g,Ss\~Qg","NU","tHr[./:~","ZDRR1@-","P~~%U,iGb" +"'!","}","kJRwZKuLx2","","'%N=6v^9p"," lf""}u" +"e1[t7Y\B1","PM/2 8+a","%OWHm","foM3$:","Kc)",">Cuc_" +":PUV1nv?W5","*","~l@s_","n?shlB",";=N*P?4h*3","p3jLf39<47" +"9x","hHMPhPA","XW9{b","7w3","","44""N#h" +"59Mkps^F","{*aYUU","","","R!OIZmS7J/","7E3f&g=T" +"89E5}=@","","dDT","UUa","","UOa;`Pa+Xo" +"A{G%`","CYN7g>`^s","IjYb","tY8","","$^\TWH1" +"$9Q%T2G","","{B","()x=2","NY&5H{R@D","8cS""1/`P" +"I9GG=owO","Km^j*x ","<*V.a9","]>;_d]'rP?","&6","" +"(","4lAY[xLS","b9^62@" +"KSq*>`^","tamZjqfI""","4X!&qyR","IAw/)) ","f<","FbEL""" +"xK#${E|&;","v&7h","fr","","e1OJ;5OlD$","c+" +"A}i","FXqz","""|;!E$""/%)","(B","0Iw q+","F =+Oo" +"HWDg|","rj,+v@[","BLQh","\kiJQ1QF","Mw","L" +";\P-}G`6","%&zU~KfM^","d7Q$","o\Rux","4rBx(","2_+hr" +"TqnA3{T/Ch","V","hm","A`'a",":","D/QRkBg" +",t{E","KM"," _","H|fHS0B9","v^)?mI[@","5" +"\6|w/b}'B<","syl,","fz-*FZ#","i}Q4S","","[a" +"KwoiUan","&Csp>0%#Ma","+8A&9Hmg","(C0kVRd%","*2R""","5I3|r]" +"jd$pN",">0wc4","t.","","K","U#7" +"E~fj^N]0:","|[ClslGP","Ek`yZ;^v.n","4|TeYq}'j","QV|~<","bHU|!W" +"D.","@""J2CQ{v","/5l8Z","Rd?$+@","w","0y8|" +"","""-(9m5","""Ib~5AkC^D","-FG~","","M;O*bW" +"@PqS+*h","9{H","CAuT@LL""","1/z!","dg""xFuMP" +"N","[;3M<3/6j","x)s","7$)=^MT",",q","*D0F" +"Y","","X[""+{","X3","N/","~" +"q0xB","^_)f..p=~NV7","m6^c","n`oz5\|er","k["")","x]|Q!","ZB0t x" +"","Ie;$W7Ltq","b?+R m'F","R_@","a(aK@)v4","d^" +":a9LLz5o","Tu2 KmR","jeOLC|" +"Ab.n","A[M","O_B","J{p;","oKo>{","+N}2h}lf8" +"@g'z\","p1[rd","","lp=`""H","&Bey","yD&" +"d-","$6","1,l","m`Z`%Vy","G:JVW""","" +"d+""7Rb=4Wk","",":wc7W|HJWZ","n","o!Ll#k|0","k1" +"\E]|","|7M6Pph1jD","~q","/rhg","S`T^VjZ","]p{i2M""@!q" +"0|s","!.J{G","Q]VQ=QS","!3Ow",")>4","EiRMl" +"W\sSAu","P","hd%L~uL9r3","WBeO@W__","ET","p*7)?" +"dDAp1G7a","~Hf{wk","\>mMvO^M","uJ3","2 Rjl<>","JR" +"K7","R_"," ","d/d:b%k@!'","l","i%ymq|NJq" +"@>(Kd8ee","k6guOgk3","""]","Y ","YC\;=W","*upJ9sf}y3" +"I#q",""";+0X","","e","2J","3#M7`" +"+:e[mW""uD","C#Tw\G","ON2&1","VLK~.?z""","[4""Z","1" +"","~.,38","4sz#)QVo" +"4!,""y}).PV","y|o","2","Uzw[}","_b$oC=!","7Bcv-T" +"SZv/gj","y1Dt (","H}H}","","/D@]","sa*" +"*$,1T","(tUe1&","""{;1EG?","bUB[{9" +"8x""GrfC[:","7KBMvnn]<","$","+]35m","-_","ou" +"(*UT9}Lf","v;","/qBh=","8/pRg2P~%","\]1","Z" +"*Hni\@Q>;(","QD""?BA.","^h>","]t","E;Wkr=3=-h","" +"/i7dns[<","zA0M5#@e",".$","","A3Q+a","{Qt""n" +"a^","A5`G#>!]^8","","9","@Nc22${L","L*R" +"Q","ds?U/","c,","","/","" +"C6qs","MEW:;","","","+,L5,^63""^","Urb^;c" +"on","7!Kw","tee8rRNrJv","^;1:>2","a5"," HJOU" +"#8x_","20.tu","=db}Z_","@jbZ+AZ8","VE@b`uiT=&","J" +"9uz","E&Hqd2glvv","t4[*M","*Ot` [","dXmZtgE","""A{=" +"wIf[XC""8/","sG(U.l)cC","[tg","y","Lb8","yo}wO" +"X3>=RC""@5","(e?WL","_7^]2","#)dfqT""fu","{k[U`$3Jiw","s""gOd66qr" +";w9l9","""DA","gl3","jQ0{'\>","","OF" +"JcnXG\","_u'XbxK4(O","LzQ*","tGoHd%","G","@WO;S#TG=","ss6'6|.p","" +"=$","&I!","^D}PTpl","M;_H","x-PN","OtkNQ" +"\plR Lj8","dGW_","","","lv4a","XW2k" +"_^@=;L]-","k&","%_'V",",Nf.QD","jbaG$"," $)YbQf" +"m^h{B","8#Cy","U","mFb$$fc","1","dz(#3" +"(`5","JZ","e#""","]WN","","L_" +"=KJ1+","VH']{O u","Ttk*,toi","T{c","uO!@`","wJyKwtj" +"""we]c","KpQa","/kc2`G%`lE","Fi","p>1#!/T0*`","""G[" +"[FwV8vyqg","","w;b7!k","4FHmLwB""b","X=","we/",">U","-Bk/t","DuhJ8@ut?" +"ME","|k]d.6 ","bt},%-a_b","YtF6jDg_",",Ztt","[+Pwoc+6U" +" lcL","","0pJp7Q","N>0N)&Xe)g","5.zbIM>[" +"t""&42o","","U","q]RJ(}Z(Qt","'Ahx'","Q" +")","%F(U ","yT^vpM","|@=ah8T5?" +"1`Hy6nb,O","8","e[^}a^","c;mKT<","M-S+]","v/J\1_!" +"rL>ABq|_","+$y}j",":;5-=7","","rrJ'""'xR&F","@<8a" +"5oI.I?*^","Kx(X2UZL^k","nka/xTW~"," Zl8E^","","Q" +"*m4B","%K>3","|a~$0","h,&p","bC-kO6D","Lh=BNr" +"ef] I2K {","Q,V7","[&D||","XbBsPO","9B~A\y;kU","" +"u`R*aS","|eQ3aP0!&","RE(J}","","9ki|rM>d`^","hZA" +"*qXOdYcT","kNIZ]y(>","vkpcWicf'","2j9","d?A","2a81Q-k?~" +"q?","LGP","1P',","Uq_wAke\^",">gpP`o4Y2b","DHvoM6c" +"g.1A","={h]sHeb","g@wRl$","","!!B>T,Q","W2-LEI{" +"","""4VC#@G|r;","A","*rx","wz,ri","f7f" +"n6i","hT.P(gF","g)Z;.\x","?1","E8cg ","}/U" +"y@4O=VkPj","uuw{VS","AqGw(8nA<@","'T+dEO>Yt","foTr%[AicI","+vYcWQv5" +"ce$","","Ve.QW","9E@^","R$P","M{G{QJgv" +"u?$.lVL$M","~pLkO","od","","QXQw'W/Ny~","kJz^@" +"sr'Ny#_M","i/""JVmx\+","&v","2","rIc""p1E4","t1" +"~C-qD+)$=6","#r","*k^u[fuh,9","w2XE","$;""[","n-" +"s","|","","3n","{J7aU~3","Z" +"wOO","","m* Rva+","P","9Ui%S","^I/>c" +"LM","D","TBJWtB10","g*(2","Xs$*ju""H","}Z}Lh]" +"Gt,BulVC","SlY","","c~hjy4$""","DkGCX'/3.{","'' P/s" +"*H;","H%","^>q","hu[oWA$$","pO]#P<","RYe" +"yw A","Q?O8GD9,Z|","U","q;[2","I$mVse","xcT~" +"HMB","qu\L","","%B)g0","Yy5FQOQ>2","B9MY" +"whsJ\","%qi","UV?|f}*","F`hs~zqA","3dCR+Xm~|%","c.QU?J0" +"gvF%|.","3io","v(","Du","2o&KLe","C.V(#POu" +":gb!61a","","=o{BRQ""g","H","*W#d#%<]","oY9" +"p","D#tj|+~/Am","Bu)6suu""K#","","#JRg/sP:-","p+t" +"3m s/1oj+","#V ,aH,~L","j4tk[","","jPXV]8OB?","Z?i:" +"40[","KTvzMw-*g","^zDl9s;","a","","Cq" +"Fnh>|-u'","'","""z$]VqI$a{","_S;nX","ELltURP","<`}","""3d|Qev8C8","cC7k" +"K1F(jZ&)","","[UB7","8|","","IC|T" +";t}Kg5k",".^@F~.c","]Kr}-Zx","wco","c*","F" +"yyr]o6YcAC","dq","f","Eqdywx;","^","&F3q>Z?1/" +"YkaUl*",";","zlH%q","Q}-H7D|q""","t9=E","ud" +"1Po\)x6O","NilQ","O3*Txm/R","F/xp","35","=AgvpHH1","mK$d","\w/","q1" +" -:ofXf5 Z","j/X)","4>]x","JM#uP'UL","""<@cK28\r","*Qb P&QR" +"d","6-Y]Xi0","R9k'b""_","qq","L'R.NCB@a","b0n" +"i)\R9","K4^1","fW~","<{I8]xx>","~TG0p5Dn","y" +"B","!u","XxIC5","M`B]MomG4O","G\jxVH}|","?B,@fn." +"u*Z#&*Qc","<_i++PJPP%","","J-""yeaAz","RV",":zzoTjuSu" +"!)>JqUjQg","1<","j","/Ib5@whC%Z","/","f&a>Zq$xn" +"-oR+V$t2","ZwWXwAPy","\9NU","^E$s7","]obEe)T@+s","T(Hwo(" +"","5R+@",""" ;OsonX6","d9r?/?$E","$3/vxLt*v","@)." +":FV0","E]91l","<3k2Cm","[8n\k","I7TxGjT;[","iNr`KAN" +"$}2Amc#m(W","8[*;","F}:","z","~#k1K4^","6 ","SQLO",");" +"-Qj","a3p","PL~s0","AuEr6","`UcjuA","D","@@/;dD","lqoRUu","ub(]Fqbb""B",";9o-#+)_F" +"w_s_u9E ","88","f,9hAS","""$d6Fv","J""T","ma>Zb/M/6H" +"","f15_?L","*""1'}xa","Rh","L","" +"%2 *","j","~6","n5s|","+>W","","&P","nB1++lumCP" +"^AC=U","aO6~","XlSj_emnf","98[D~","","WQ*^h" +"Ct","djGwZ>","43","mh","O@]v>5""=0","o&l>e;zqef" +"9I+&HzW","UWF",")%eQp*iB","TagAMX{R","EZb","aVEDi(tR{b" +"@|24g{|__b","YqC+","JGy\)","Fcml","","R:q?9,-}" +"""$V'","/B:&9V","1aol7IV","","Z>+o\+","rJph~L e" +"=k0:8Au",":[:!GOXbu_","/u7+Q",">A9Y","i/.W","ex)MfbcL" +"m~r+","2>v","['(MV0","nT","ov8X_^","$vK" +"}nDF?%U$Mu","/Rh","""w","$$FXZ,","A;eI9hF`","mMT]lR" +"","9~v/q",">j","g.g<","P'B [K]V","'@w;Qap" +"u','f:?i","d1-#","cw""""ll~?","ZEdV","""3&","QwFZDt" +"oUGrr5AF","f","p<6]=<)/Y","f2iP","II/>6","mD\'x}E" +"Y1@XExlgX","ux","U79c{,;S\d",""" +!QH(","x+@[S""w<9","6Qu],=" +";C#","\CN[*","h7KuW]CZ","W","+l1Q","|,5;P(w" +"e","D*>{ Gz","q","K0'J","9Mh/N\X5&","$h62qDhC}" +"a\B-MUT7""","&","M]EO!a","QaI@'fs","Ex(e;Hdh","~j8\gD;r" +"Lxh&eC","","/h-!mx","","^3Pau" +"}]7?B","W_","J8>`*r_[.","@","(-N]F`.?:v","" +"Q%","wZ+Untty","|\~\","{z$8I","E'BNN3+4_","w""nV+j%" +"%bTi","aYeKT`","V","","/qriV[","pm72)n#v$N" +"hs(","0",">mUuk","u","C<","$J)!X'PMV" +"1HNGSv07S","})Y\3j\0","QXEKX""%","}6","]1i_ Jmk","fOAUJpG" +"Ngblkh'D","urQ""Pg","","md:X\-A$\","o}A4",":WIG&4" +"FuI6Vf!lgU","EKMu","CHfv",":{N","etRcEw","j" +"[;","Kwy7\;","KSC","0P{/\au","L5","VCwJ#;{S&","{\B7T:S4Q"," ,&.$G[x","$aU5:evb","d""39_Zo&p" +":_,||]b*","","f}]L","%I~","9-^Ggt","TBT}k3]]T" +"^U)""}","n.UUG5X","K9Kmda4)","{U;","jE4`euHi0:","?,myi04?" +"G~@l",")G#JDr5~","I}","#","aE2#up""~","gXM+wstSe" +"H% ","v'Y}","]6","4AgZi`64","0%e^B93>kQ","CO6" +"8K}","2vaPg-P(=",")7.L.>Q","7\9^","fAbU"," 7H\t=W" +"w(6s","7(&Mzxc7k","-kK3`P9-@","9)o","u-lRe`A%","%SfPv" +"NFS","R`6)`","","U1eR","qK#HJ","1G'Q.3]" +"n$","v:DI]","R(ZO!Sd","~)","","m" +"mMJ'|tLiv","S~WBIjy","-!DEg[\R","","0zB>","(X94" +">D^v2","XHhf#","M2""yDE","IakH`!$pw","x~3PK!q","CI4l*," +"e08rc+l(8","wD!9]h!8B","hc= ucF!","~9","}4+6","9a" +"","fr","4?b","R>s>8f\Ov","J","](`zt/,Qx[" +"r_]7{HA\","] #M>","#Z7~;oJ7 ","dQ'r>,)?A3","-pPD}MN_qD","" +"o)&jo","\","O0","[","2&","z" +"VRU_Mlm","*7WP<9","","O,/","s}z[GPm!","=J rfgZ","?G|aX18':","~i'C@" +"O_B~?F5","xTM7","9|f","Kl#""*wns","l]ixW&",">l" +"8uxz,#O","}","w$%O","ds""","Z8|k,","" +"x)","f_9l>2:i","Bx5%","Q[0rm7","r","MMlG??$92" +"X10q","H4t>D","jf/=","l","(L+FY>;","V<\kp9_W","z+","uW$Se7'A" +"DD4+","","e6|M|j","I","Y\j3*","n/V" +"m","P","5}[t","o$3XaB","(1p","Q" +"(l4","+5}If76","]:as","<{jR}[=","","Q;b&" +"&@0RkC2F","KpP#;7}0$","Qi%HZ%VG","d","U","/VC)&1z" +"c1<$OIm%*_","iN'UN!","{)oQmic!","p+^V.$","/rYs{<","Zjf!J]s" +">EI$^+","c@","sj[^","q\Z^v5:","x6","]3Fv+B%G\","!iexxRDer)" +"iz;C","*%]W;-","m9gf.","~EqHR7JI","63L+/7I`h","yq7"".n`>U" +"r4x@vF>x","","OXV3","sF`|wfBnK","","&T" +"e(X,(iYjD7","dGEtB","","s[=!^Ke0fs","ssK[)J?YnS","P!UL" +"","|62l|%?","6C","","","E5""5^xE8.y" +"r*l]6","=","'~omw^2H","y|W<","FJ~+9|",";>+`n^O" +"I","""SAq","( (w oOjfs","lua96Jg~u","L%K","^c" +"Q#f0GDJh_Q","fI,`>1:hbc","yDozSJ/c_","q:Gae{v;:","W)[7""?""Aj-","bJ@+N-AEG." +" &0Bz[a{ <","Tlm09","{ow,)>KiFu",".|z/","orZwLWz","sb]@" +"%#4dn""W6v","",")\ ie6Gq){","&","=oAo","0@Wu1lW=4U" +",}D:9","i>ty;^E","","&L%Otmb","~)Yb","~" +"mKi2","102d}Xj.i","/Z.hw!W","TU{)`","2'ptLd""","*:U ""Z@)`" +"","O?X/H>","*","","05JMV","]es)" +".#d]cs","G]i:-0s","","|","d`h3","?R;" +"{","",";,8Z","UMr3","GU>$C<#=","($" +"6?4^","W=","","YxMcaFi","","QU}RNU:" +"!Xit-N-E","3]I90","(pl&s","Rh","","$1 /|" +"WF0","YybY","o! p;Va0","zUIA","YdGvc|","\pzZF" +""," #","Ds7A","zfW","D","S]]>yZ" +"^zaep.&","[fVOXP","2t8","94#PGqW|r","%d-","'+'L|9T" +"G'+5dSY7","~Bn[ka=V_","[{"":{5","Sd:O=","","PVa" +"ok3J&","<2)R B","3nHmW2Wh","!4v","Xku","&8;%0" +"cLH:","","* m&T/jG`(","CH*","z.Cn3aR","gZ" +"FoUQev","'~!jP>/:=","KeJsGi4`","!@%mY+gI9R","M<^","b4Itg.DHd""" +"v&v``-AQ",">Tv","DfgIrN","+m,>x","nJ%/Q","2+r{-mz`k" +"Gq_,mf","}W|","[WyseU","il","9KG$Nh","n^p" +"FqNa-`P~]","4.z""2M","&UPS","1%`s*5I!","I","D" +"ZO+S|","lP?Oo","G1l","hHJW^)&5F5","!}%-D","zCXD|" +";@^^+;","","Rt!>+'`+v","pXaEZ+8z","Y",";`P0q^J1%" +"o8RLS3iDP","j=w","_,@y<-","Ce{7/88O<","V.#3V","N5\Pt" +")Q/3d(/IV ","j`G:J/","L!,.UynAL","%s ","?Tva%Z","a p" +"y%mogcs!t","&e|qk","WGE7q","","","{\s{k`" +"",")(","?8#e=Y","Usi-","d","o6tiyX" +"tOVe/f'Hh`","W""3?^5IZ","nGKdv4O]","2RW","h@Mk=qU+","R x3" +"pAaqAjus","i|:","x5Kj","}b9","PI","})V.4$" +"h)nUHN","N2c KreCQ","b{","p]T|^soV",">f(^~b*","aX{^8iYc" +"6He>J\zfYN",",-e","]9","1M;;J","7# }ll5a","#","czPm+","Ojt7","dg6d_Gz`K","AQB?y","P%/7M{(_" +"|{nW-c8T-","Lqb9B","^,<","V)&{_3Kp","WCv>rt","DUi!/Psch" +"'","","q%qyhzh'})","4o&","{z0CR","n=#Hjm\H_" +"","'fe","s-B.%$5?l!","&{h;QDK0",">Rj","p.1sET!" +"","n!u8RJ","WG&67~j","x","F","TNaMzW.V" +">","","NobK'eg,y","K|^)gpVP$","prNi3","E$J4o?m2" +"2xfMAc,<)","","6+%u","hoNrez","beSaoe","QJv(r#:O Z" +"3k@}+'i","]pm'|",".cBD","S",".9Ja.)j\un","pl9" +"'w=lq>5Y","O""","o:lgu=","!xO(",")(3*T","" +")","HAKr:]5L","","`wm","q","Bm0" +"fCI\V!Tg(t","yJGBu\mi]","}J 2*cEYd","=>TR","b_Es""","q" +"%cb","","iUqtTIy/","}","[2TGE","(Q" +"c","-H^f%i ","'3\}H#^0","eVHOQ/R^","`0","h" +"#_:i*","YW;","","y@+0klU","D1A@6","W&F^`(O)B" +"p","!t",".G","","-G9&/XyU&","5R0+-T" +"!h","Bl","E","G]wxk","AqC~K","y7eiO" +"XW7@","=+AF?O0/f","","XST","u^4_3","v!N7.=xS" +"&{a$xJYPN","S@Y[O{Rls","+v?09|`,pE","UcPW/=NkJ","GR#HB","o" +"","E3r","$K","P]",";glO","80R>" +"Z-R>","E0>Rdk","","""C8Ipn","Y/jY%(EoO","kTEr4cQir","t","egA3:~4f%","" +"^Uq","","",",gOJ%","ceKP|B" +"K4*#","","EI","yW|q;jg","(}uj","AL7iR8%~" +"6vqU`",")"," 6","2ke/`lW]|e","","Ri:>_NxZ" +"",",5ft","6S","8+$f","cm-FUpy","TN" +"RNU8^2","&ce{.K(1*!","KAe-XRL#Ru","y)$#]JjL","S``.`]","P-)XW","3$5(3<","AwbsP0;&Kx" +"O,mcSHZ@","6@*#","0)","""q:[\`<","QVn:","" +"I","evL","s2o","""ZmT:3O-~+","!|TZ","zP*J3D0hO" +"W9{)%L","y61a","","""n4^L`","3|","kBoX" +";g:{'8q",".","ksK~mJ&","J","tuG","32e][qO%1" +"^M2<","z|s;Da5B5","!Ya?l5/8+_","D4vjM:(""^a","d4t","7" +")_9o","NYe","lZp%Q8tYD",")CxxziJss","p~","oc","L`%H::9J" +"W","(@uS","<~RK=","4yg","qN{VNx=","8,g|hH&" +">DvSB1#hz","y","P","}","#m","4LBUACX" +"#ab","vH4+Kp","(efQH","1X[y*>N|#"," B_n+/*","whb" +"{S[e'`Q.E","l-BUV","~P6&\","kwx@f1'f-X","|li_"," F&^~PH""TY" +"3\SO$TY","a{","\aE6{",";KH","Yj:""G","1H>v/" +"o'T}=N3","","/8","[k.RX","g~","" +"","MF",">p]+B","(","5","zxov" +"VO","fu=&kgYh","CJJQP/3(8",")J ])`9%","f","","s,41i}td" +"","je>}6N9","s:lhwbm","I_w(","M;oC","vx-" +"J]","=N4q","@u0""b","","@^GzUnx","g0YH" +" ~k;hC","R[1}^GMs|","XvW3}F>KR","5>WY(9aBj","DRusg","RYR" +"{k#96oEr","s8Hu=O","","a7","o","""e<" +"","v","gw|#,&N;","nD.v:#A","rM/?3K.O=~","C6rgq" +"A"," Hsm","W!ozP76F&O","T","ZkNlCMF`","gsxD>dk" +"U 4-p","Qtt EWyf","k7'+^c$65","Nj{}j","Xe`I2","QqR}[t" +"","]BIeww)+F4","%$","#o{*","HEY&|w","/{;rp:)fD","","B","o" +"s&Yb|A==","","-&b","{f","oR1","~fymA6\C|<" +"svrbqg&B1","1c","lo","U8/1V!`v" +"J8v","4rWV9","76U9q{KV8P","vv) ae1:.",":QqOMsZ","^#4.;sg" +"","6","AFfoQNEO+","0{?WJ%","`0R/>]","" +"-","h",">","bYKG+T",";vmn0u1us","ls$x-`)" +"aBW1t","m`%l21ta","`.He.""Wk#'","","`p","x3%a4+?|" +"Mr?i8:U__t","aGA-!B&w","L[|Ui ","kKiJ`W!"," Furo","`g(WgAdq" +"01`dNfI","3`}C","}-):!3x",".#MchD","d`","}" +"Jh|,&4Ff","T~.""RDlk","R","*","","9ON^xZEi:A" +"RQno3","[N"," `}5Ek7","(>","Y552I",";AB" +"0=`","&H'4#2}A$","`| L","gF","Y{'E@:H3~M","OCndT't-" +"oe6f&",")7<-lzrZl","8{tT+v","ijM","FZ","n/" +"hyVrW"," ;(tj-q2","6#k!|B)X*","?H==`tp#7","WR0_y>={[","M" +"~N",";>i","l:#","w",")41#l","" +"UB-J^78Ra","""","","]CpRDGNi","t@$]vl%P{","zB6 #e"">U&" +"K:;@?","*","y]r.R<","\< -dU","],a2","F?pwg" +"&p","FSO;|WZ:","@","F1z","Tp{w","5~d~[c4i>g","1/z%p\qK}S","`.G6SG" +",L=RFMN0l","","k/g}x{%Q","u{","y""Jnd$","" +"]^iUl1(","GX~?:q|","","DKto","qyJo","E0BDjFqD-" +"qx","%F_`GWJ2d","FMf4|Bm5","","",": " +"VK>\f=~!","ps4","$UDLr%","","TRgk","i)n\s" +" \b;","7<=J/O~|`q","{","Nz^Ol.",",?","lYzM!A!1" +"A^Bg[7g)","s","aHJ","8JavxhkXHV","$1","%3G" +"4'5]","","(Q5{lpiB*M","C[~b|#t:d","#oa","G,`g?)uR8" +"","%o#&","5YL>'hw2","$-8:K'","6't,&sN[qh",";&lg?" +"%P'nh;&","@hB]=Xe","",",BKq*7","3kpkB","9^wKD[" +"hFj ']>{","""QW","HQ","Z>OM?41\S(","~qaLFh","rim+jN+" +"7{y[D","f]IYmPn#","V$g","1-6S9Pi^t","+","] 5Jrngv:P" +"'c45O{%","@","%#`@4;"," V5[1","0-d2","","","* " +"{","Y W#BO K:F","u","r","Cc oQ!","" +"*&\pHM","","z3qG3:e\X","4","l- ","$=e%|pmXm0" +"Z9*""]g",":","uLto","T0;N q,]U","puT)N4","L^7)" +"Fn}3f.LuPR","OoS6tU}6","AA& XSb","pVbgn;{ub","D","LPYGNNezf" +"uX|X[.","Dl,SdFOy","$9<})M","q","","[Ssf.Rbg" +"L>I","$X.","#$rA$7 #","utSV'RS","#G'daiU~4","xkg " +"dc<%(;I","ObU","""u\cB:\","","pzB+BKW","!@0x" +"","6)uY{","#'7GYK\","^RiXO/|?i+","CS,_#","wY,r(if1,(" +"hm-{","dp","'""E[=LYTk",",X%>K*",",","#TnK7^Bz" +"lFtbeR","%`)XZA]bQ2","HY",";^hM1S;oXC","","""e" +"DQ4CP","7""r(o.h","rf8r1","9oOobh","S/*.jpWQ>4","0` hF|""W:b" +"Y%q6",":x"," ]j5","\pQMmt;u","rTMG[pK","@!|o" +"t>I""7vmd0l","H{3Y8;X+m","","3u,,R#w"," Jme","" +"o","6LZff^3","psjx","2M>?rr",";%2^'BH #"," &e]" +"L=q<","GF;];'U@","","H","@(^!lnU7"," o=z)OOu" +":zHk9:.","cEHow,","KU@T#tZ`}","yz[)3","0","Th>'Nz{>6" +"9","+RS","(/t0601|Yr","","c/aI","C4)'9[s1ym" +"YN3]:B|P","7lVL&%","Cv|+-0sCm",",m>uClMAI","s F=","aQoeb9Sn>^" +"oI\G","]{t",",i\%ol.","3&","JtHBG1:wXO","qd" +"\2-by","","RJQroIL]$","dv.""- <^=","wNd=k:W","1" +"Ii","gkd","VF7uL","b*YK@","f","9;f6|w[0\" +"rqb-","D%","M9^fGFFx","pCsG6-vU%W","U+?gL","S,#4Y\Ba" +" ,&48]A<%*","ZSa -bdk","4AAk4kI","Q&E\","$x\].$",":*=BB%a)" +"Mrg59g|xP","/o8$,.;h_","{|ce|6-7I0","@<@x+byN","KCs-`*J","u_BwRG" +"w/","Qg","hf`",",dvS)","Z[PzR>@>*B","8e1JYN" +"!-]","(5<1uTFg","13*Ly*","V`toYS","7H)@l&!",""")","_B6p","*ndbzfk`,p" +"B#2H+ql","P~TQ^" +"yX ","w","= z","7L0k","","IpP" +"","H|","0",";%/|'g.,e","","o'o]@" +"Vws","tW5eMg P","bN)~cT^-oN","X","""5u,","9HO^<" +"Z.e_QycO","","j8","XV}hHbN&p|","=7}Z","w""!.H" +"2F'i7(]_/","X","zEme:* ?#:","kS&T`","e~9}YX#","4MtIdWXn" +"f:>1N2,4""","bQ!@","y%","qzw* E@=X","L","uK,9" +"G*f\?X\","J\+S=I00u","j","Jl#C","","" +"byTt)","pb!5KU","JWP&XoDbu","f1?Lw","u&Or","a9.MM+nkB" +"v{n","","7VivPC)r","En,","x","ts3nLc'd" +"ML6)","%H>.Yvk7s#","8","na","Kb@%","90B" +",MD","CWKn-SSJ|","F'V\","4=1","g/Lv(e=:b","S4C/,$zn$a" +"[[/*m(r","Qff`","","EQlprgP~;!","","cK(<>5~^bg" +"bdll=T","E%+Ro/\#""p","zU#q?Uh-jN","7r","@S@k/t","`#JX (:GR" +"/","MD;u%4","51i2K~jBD","UVCu`P","J|UDt(5" +"YnX*V_h|~","ql637%&M","=b,S1h","XVhx","LWN/rY","" +"eK+i0z?&3","j+}F","&EWGD_","D~e","FDj(.S\h]b","p3/" +"'K/8/","-pcH""z5#:M","M","c","zQ79","i59V" +"","|}","Rl""e","3+","rqA~S","GGDP {7" +"k5mL\=","!0Y>oy","+L+g#","B","g","" +"k","vJj","v8F|]8WwQ","%W","0W3","a+.3PAanf" +"muC~Tz}5l","","*FQ |/DZD","",":Z2t&N","F|S" +"CMK","aMLU*?^6:","l","_-z[kX","#eJ2""/oEb","i&J" +"PXa<%4s.x",",Lj,ms",";_QAG","\M","+xZ0D1W","" +"Iu","@_$0I","!~I","0475?Pr","","xfX(5fP" +"M","KW","9v","e","G^I0","%Bjba/PTl" +"","Zdj{GY^|3J","EQW2nF`","ioXX7F4(","Fj|)JyCN","5d" +"X<","SX;.v9","8","\=v}Z{i","s","y}m]j-D;L" +"7","C=.9c-V","/_b","rsI%","\9$","OUd'uz|-" +"[vMQV4kkg2","G14a,A@","X`k0;","",":u","x)=N" +"Y","OnMFVMzn\","wr","RT|tXR2","wQ37}","@" +"","n7","^{hYk 2","eOtY\c.1~","`Q>G'G_{_",";XG84" +"%D","dk_#r","dGvR,jI.","Op9+4","=r&2",";c}M(R<" +"M]Y","]","","c","-r"," " +",^]k"," M}","'J9O","p&977\26","h.Sa~7~","" +"VL'yY","Wqb","`","j6\w","YP","Xv`?3" +"v3+V0Ai#a","a-#","X","kHU\&*3F","q/*X8I$_""Q","" +"EH=1D&v3E","/MF","mc","kj\Se(k9k","_cS\*y@e","e !G~Z!Iry" +"fcFbijd9n","e{He`Xh","}aA","g=R$","Kl?-LGQjT","x6T/_`m","g4}OBOqkyg","f}ul$/G" +"","U","4:3","^6zo6?AS","e#A0:]rDP","$` w$S*zE8","pWxS9E(<$","iPO?5}u[" +"T","}yb","","p","?W(V!lmf6B","CF5I" +"h(!])-","k","o2*XV8","3=T`'3M3KL","|U%j&E:\&","" +"KcP>zQ2","m#74#6","m<4]RL","R&@","GukI4z\",")RQ" +";","{6iH",".gs6m:-N","+#i^hU","&t^7j#qS","|uY2QEew","m`$Nbzj`","p" +"e6>.""l","@n0k","^\z)z\g","B{+","","XvYO""" +"9","x?f(","Qq-sF","-0'U","1jVn* X","cFjR" +"rx.];o9ay","","<","""jUbs","C/1LG","MU" +"","|70zmRqt","Fu-C4=Q]","}}","%?E@L0 +,","vgD70ZoCMA" +"%pP~$}mb[B","aDNA_,(SYF","[smlZ;6s","-]hJ+","=z^m=0)gs]","m,(l&N""" +"9hiz","_HXn;}","2R7rS{/am8","A:f5Ah%","u833^tC<","y~ShTK(" +"kx.;?sUL$#","pc","F[r","tj7wK","","" +"_r%n",",[j#|","?8","ek""V{gIoq","P f","|l]:\-l%7" +"","Y""l8rV","wrn]30xDm","VuU","z",".{@CB-h;}" +"\W{j","zTj^qif""k(","}hB","k{.iLU[5Gc9","" +"'=E=+""","~2{j^3}}S","Hsmu`","s#t1","&R","" +"","#T4","XSM_1","","E4","!" +"<`,&","ULnQ/NZW(x","VsSgfCRD","U6L5","a\","&wEO4{_z" +" Jy<(1Pi","V5","';<"," @k+v9N+iw","Yo)e?oE","w%J~Zh" +"^cXyo",";n","","jDl>$7G0'","","u" +"l`D6","5x|+Ab{F7","d","U6K&","NNvkG|","IQe" +"gK )|",")BI4Bt5S/t","","*NGlRJhYW","'}f&","Xi~$Gqls^$" +"a]Y^71","r07hx","@","t5p30","]84p","b" +"","OQ$","HD","TR#qR?u","","`6&_Pz" +"}","b'Cz9w)c","0V","dg'3[Z]rM","G",";2;a" +")Q;","G&'rfX","7 3)E","/3]a|C%","c`}T""eB5e{","[6wA\ " +"!Ts-,k 1",".h*",";xa\AOB","","K/NX}a4S","^" +"0f\zlhVE0","d.#+h7K8r*" +"mdM:)#","V'\z\e3","ruH#0","R|","D{dmoH","?~5z" +",LBC|e","p","Spym-:{L","$eL","5 ","b7s?H0" +"2se""cYG_","={jA-""#)^","6","A$%d","qGEyiD","" +".N","""%-gc","!","Vdn{iRwo$x","[x;PX<)","3" +"E]","1IuA","s\z""#","""~AVs4p","T)A","! C/KT" +"*UX)","wR[%g\","W%JC""f={zW",":u,","4r","<(b`%","" +"~}3Cg","K;RD-" +"jB/?\?(","j%","]rH3*Xfc","[ktC@}","_oWt","" +"NjgH{""1","P f","Qs]/","|do=","qTaAF}0","3Hj(/'*" +"%1X}TH5(","{#KDZ","#?LpF","z\""\u!","ZpuzX*","PT}" +"zJ\","a","'","]","o","/ 0]v~" +"]W","fbB3k,1UE","~(""[w",",k6dS","561{z;E","!{*$vr]" +")C+1H""{","12R-PWsPW+","'","","U","^a&q>d}" +"`ee|l@",";P""&q@K_",")","d-","t`BG}W","qN" +"U","8N?-r`alX","4{b3j^BU","!0AKx'BWH`","feHF-%L","y$|pIK" +"y]@sV","{%AC%Pw9zl","fsm","HbN"," :U}","'P#L0+/bMM" +"]%","","T 6Ut","C!nm/? q(","s!E,>","" +"SLHvW",";8,j","7Yr6U","`EPEw","","z&9" +"%T&mrIT","pt",";A?1cr","5J}(,","&&","J+KG","uf/>BAD\" +"\wc~w[1DI<","R&fF","-.&jh","L","""","J;W" +"" +"Y=UwC%g0","!","ek$6","gOC!qh5:by","m|h","" +"&`0","Ga4AdHN","s-UQ}=c~W(","Qu=#}^","","g" +"9","","","3","*7n#sS","|'" +")c","/DHD$*","G","G-JXVX","yk7+pNhXKi","_zlM) G" +"rL5TJ","|<8$@""[h3#","F8MN4""","j,a0R&yX","[","`:)[qr9" +"X/#k","$=","|% mk#'","V[aF}","=-^" +"","_Pdpz","X[kn8p:","","(jI$B9o","=N]e" +"z$-^Bw","S#7JJ/K9","Uu:y@0aCZH","p;A)M?|~S\","o","?}`jx" +"}q","qF34HrEKOp",";{Y[;","a2M4-E","TKz$qX<@","D/." +"","l","","Kvg"," :","" +"vT","~{<}\zY","%kFh","","]w)","ehPVgF" +"q7U","Nbmx","id""&I8r..","Prv>&R8ch[","s9}!+h0","xX_vpyM" +"xD{G.9X","e9uP;69*","oniE\rs","BuVHm9!Z,f","w[d17[c","\uOusY" +"'SL)kgw@""R","q>","UG`ORA(","","","om Ez" +"%]1y","0/p!.l%","","(mQvxH""0uY","w~thjCIB",")_N>hEh7" +"|E2Xy","u","qYq","I;","<","5sw@nvo%\y" +"nY","RXUPmsxPs","","[]vkY 9p","A/A5\smVx4","" +"TV-jOx","_}","%nKV:c","v+9881","hUo!\x[o","l"";43!AD`" +"h-{MM:kkO","K?*n","92E9t?","a._^!","M q","" +"YoXUv","k+U0 #\/v","{:'8","l","K+A&:Qf","Uou}2BOAZ6" +"d)='u8",")","|",",?E[R-*}u","U","G4,IQ" +"_","m0(F""tT%","f`+","^q]`4n","0y`"," R" +"t==URE","{02:OE@Y}$","lxC`1","5maX","","Ptkh""eFfmn" +"","LN","","H-H(PPSv","l","7J^|vbV-""" +"Dn'^^f3hv","f7o'3","(RzY+#4","%!b""Lh\F-i","'u#'c*","x^Y%tj`" +"e,DQ;K",",!","Q m(","Q""h","6!5","x=0j1Z{" +"*X{&Nlsu","W.^M","+","-","","I}9%q,9net" +"","r)M8K","6[sYyY)vo","d/J(6","t8,@","/`kP(^!Xv" +"ohFtgV","n7v","","","Qg$}9" +"""^#;6","Uy`","X$","e","| bDM_G'}","@c|(" +"B,kz","pI]f/]CXL","uOCUDw","[:5F","v{?","W',]DwWL" +"c`%Uy~ ","U-","dYF%A/X","","g}qn","sLI@$(xU_" +"%Md*L",")W","'Q\)1>o `","BR^m0ddNTE","fk1;G>","`" +"","t?~HTB","&R%QQ1M j","%4H:z","4:UNI-&M8","" +"97","","#6/vD{;-_a$9Ed","I*b2" +"RFJ5","+-k:cb)4&T","5B<","njZ","xzpy""*%/","HA" +"","oMMtv","","VW{","9Kp8","" +"""w,;T\","Y","O%*c%J","","0""-;:{","S" +"k18","","J('r","","k}?Nr","-2M]>" +"W0?|N)|","x/\jF68&Hl","0t","B6K`ebq","=w","" +"s?{>(e","SK+Q'rgO?","`?f@TJ7',9","AD","Xy4-`[J","*OF18" +"fj+1'5","]T<>?YQ","q4V%","A_;,inKs","R;4-q^8ztX","Zv]" +"G@U","D","(dEH\g","m&","eGGS","" +"","#","",",T","]:!8N;","Q;&" +"l","","A.KrWVZi","3m %8S","vlbS","c3E;]" +"D`VPB","b={@p;","","qbyJ;{dK",".`/*C;|","<" +" P@yGH2","","hjmo~kHmo'","D$tqEM3e","","" +"237P ","",")~&1","!(","Tr","y@" +"/""^ME","","Jm9","An","m5","q|=E'" +"","aeOtD","^M*Ty#li","}.{wzRqI(c","p","iK7:" +"m","U2%Bz","1=mETG`x","<31oys^b","O`JXr",")" +"pM9c!7)","g","EES","^)'$=kz%Jj","=Mqr^","+Jf./[[" +"wA7","","~:","x7hpJXRh","MeFJ3-?/" +"T%_AR0","Z2{$?E>","Mf[Ag0ALr","Em[","D@4PY*RjaK","",":.6ZF1K" +"yo|Xqt'","]1P","6}Oi~]O","Y/=2}3$]8","","PG" +"S2#8[^T ","v","Vh","Y%Z?t","Y[gF\xsN","^=4;ke" +"]A/","","l-!_","Ko&","4OczA~cq","}2Xit" +"G&=mBM}","+Dnd10F[","[;8ln!OtqA","CcbEEU?","lsD6","I/tA" +"?","#!6<","","V\w","V","BSs}DSXA" +"_*~[","7F","$R",";$at;w","eqsR;.","" +"Cu\u","-i/g","p","DBLn",",-yhG1Z","$~@o/gR`" +"Z6U4VThU","","Y+c3","%>y","%?|n@&V""","" +"Y<8FAq(+","P","~$",":~K:f5","mu'G:<|p!i","yb&6(j" +"'gxV","nx%J`ZGh","TPK","Ppz;52","W)F3ojnUFz","(8^V" +"M","`SZ","9)3q_D.x$c","3nERar*s+q","","JxSe_" +"T","/jYQ@ZYGT","","Bjo;e5","@HPd#a~D^","G7Q.I" +">qD""2WdM","GA@3","yf+l","\+","Sf|<(2Us","ON$mn&"," 8wT^|,|6","`","u","","8~g" +"NS\","v#Ug+a","%q@UE}V,","mPgZ_V)6YG","TL2q","B*`y" +"e?WdS}d","yPItc(j",")4_X@u&","","f.D5_Zo t-","O,-E" +"{$2/n+","C|",",h",")}q","3=Fr","[.W" +"R9-Y9Pj4","w[ af2","0-H""Qi","9aM4#","jxieH",";/Q" +"riO","=$Ll0jJX:","M.)}|","=","@HLX","zh&J98" +"ppvrE","!RKB.2!z6<","g","on^(1:a","wl! ;c\","Ap^!cCR" +"uX!v:7",";_RIJh6$m","","JRfJ+M\,M'","WTVK","D|" +"sDJ#),","46","t5","dT|X})","i<0zl)","G" +"Isl~1]]FuK","H+ =c(","","",",b,Ay","]hv3eQ;OmU" +"S0C'$1:Y","^c2;","3h$nR|FW@E","T;wtypv",",DhU>","n~l" +"z7","kd\@","","H:0h-t=/","V{/wxTq","%~]13k","X>Zn" +"q. ","N|c>N?hm",">P_(pA.k,@","V","/5","t)mu" +"';~@D1","Vf","Vdg@]L5","","VcTj7K0=gK","zNj{]!d" +"U{Z&ze","S7Q|u5=D@","|9PkGMuNJ","","$","f1" +"lhU!","DX","3A@G~JmFyb","jg","Uv$m","i*8mSCY","Q3jX","<1Q.x!29%e" +"T}44:L)","bR",";(za|X>p","]lz><;o*Oo","2X(OM""","'?W#" +"!Ww",">DQqjyk","!QZ:}E","|)","-Cy8[[-a","AQ(7Jf""(" +"MOb","5l#V","p]3","4Ptqi","T","L" +"/dF ","[*;\47","","<_","tRo" +"kCbR#",":mRk9Y","\^",">?5!:}^Ul","/mEW;/D","oZI.OP{" +"R","}0nO+R",",?X*35","Dw24~&:QXL","5p'wNN","%/>D?\<6!8" +"^qx5?","Fj9#;Z","P","kgqTg@","Y/001LYg~T","237?D_kX*," +")4vqo@z!{3","Rg$","]==NB-'""h","z","~ |xXuwj{","z`1:#]pe" +"","","60$WnV","8T","+","" +"du","A#B8s",";K^;L","n3","LdQCmB","WY(,>EEd","Ob65:3,,O." +"","5" +"2ikxn>$KP",">vS\_dC","9","","_","kc+B':pbMd" +"Y!Hs{8]_-","wE4k","HHvHq","k",",Mdk","" +"0X!8dE","Xn+h*(>","uXtp","iv0TQ/d^","qDI,","`.Vle&Jde" +"5e;QCrWa|","{*","C3","",")","" +"S","3lecX2{!","","c","JU.j#?P\p","9x$ep |e+" +"l4fiS","","",",>^W","siE+#&","" +"Tenrh","?W","sE""J^yG","%{""\GE5","Lwr G/&\","7GZSL38bz" +"foF","~czq","?{,($,s5P","9+8","_\-b","*{bAr^eS" +"b^KtzXA]A","""0.l","5","r#i" +"bN""%*BZ@3","Wz","d3R" +"WE/,5WB4;x","Ld","+Hxy","<9""+","I/e","y=Ya0_$!^" +"Q","SZ,tWif*","","@JqM) ","#CwhIF","HudXtqg," +"","5p|jGX","/Gox+","\?K=PCXa2<","`qOp8\",",,0la/L" +"m?$(Uig","$gqu&n","zOagC{1X","|JA","","" +"c]z",">lL+ig","nF","","N^jYF",")?b}" +"x/Y","K[84zct`l","qC/p>JDl","w_fzR9{BX*","\#RG-j!","Kj" +"]bb:}","j","z}|: mh=i|",".s#N","3|B","cn(2`c(~" +"K?5R5Mw|<","_BO","IN$U x","#n]QP[{","*;M8FHmY7","?e4Nr/]" +"t*x","","Eu1x*$","k","g0},","""ywzOyLS]{","hDLUeAQ^","JdW=1BYC","Ds","tG" +"OWpbf","nM0$\X+(o","rGmj","uT[Y7dX","UP","*.:!P@Q" +"qh.","Jp","=,$TeF]@;D",";","nAxoY0","" +"yc~{ S++","#dhb7","J","284v","zI$=","TA`7" +"5m","-G","","q3w~$^C3","","RkY" +"[ '/hlo!","_","&Tr>O","{#X","O/6RYIP","r'p/b" +">P","?^MJR:XJ+","cWY?","OfOf!i]P:","ahVxyjB;","R" +"G7",",}%~u?Mev","s/wA","+w=#f","^L?VM]K/|","P" +"N","QyT{<'4t|p","","I|j<0'","&R#",":x?Es>H7Gg" +"@lNJ=6%","MBF,dX{K""[","Sw","b.Qn","mIPJ6@DN[k","=xBFyjQ.)" +">mlOo","99b1Sz7@C3","C8.~p","$RTV","/;+OT)zK6_","U+y&%H" +"McKt","C;X$s",",cN`","*5","oQR/EEm*Z@","CsLGK" +"zo(",")wa3O!O?+J","cpGoOi*9G^","9pF,#,Hz","gfh","^B" +"x","eJ","8>E.Rj","","k_","bg{","}YWF\\}","zx""f?&","qjsm5","","W)`[" +"","W","ZX[","A""Oo","Ch",">G" +"z$'{:S","Z$M-Ps$&F","` XG?,YBsv","I/Lx}TU(H?","MntpDE6gq","/_MiJ<`" +"+E,j.GD1","" +"a1!a","ae3n","JQ","M&","Da&&" +"j3pU73",")5x^GV}4H0","Ei:yo&","8``","5jScl^","1,q^9PP@g" +"#eeJtp[v_","6`icv.5pW","","!sMzd$%","G]Rod3<","$v\N%&xu@." +"o.?,K","H*T","5YkVF",">7^Y{","{)","Pro|iLF","4# ]c+fv","Q","s","}Zz5" +"Md{jbT_U5S","7L\P8W","DqXq'M`{ K","h6g","x;9N, ,L","G" +"","kXP:&rZ","Hwo","S'>5)q","F)+E[x","" +"g{`3W%bN;}","#uzo(ZAk","+\","9NL)","*q*Wz" +"e","1m","|Z 5\Uy",";#fY;Z","e=0%$","Xo><","","","+\#%icG" +"hHxS","lo","n,]","e_d:=v","","4~F-7.?<]3O","u+l*arKY3s","7W}i6","7vg\","8@Ww-","}6hLIoe?" +"","","%!=u%q;rq_","f_3","2v","" +"L","pi=%","5kA","c71","=""e$","d&w[gp&,&}" +"j","dR}F 6bi","3","","V8vAC\e_A","L""q(" +"/VZs""u'","N","rl'BIY1","J>zBk?\0+","","a$,6>2T5" +"8","XzW.b""M&","B{ |2w4?CO","IY%s'wH","&el-c","*dDoz]J" +"","/_iI[PQW4",">>{*ib^","n$","NT(osa","1","{]sJ_KV","6 j","" +"k;","th!TO","hNuK~ch\5l","YBWlQfK","e>iAO(HvCs",">" +"","VDps`ft2","s2Qn&dvMd","5w$TRBJ",":Jy-?g?I","O$BuZo^" +"}",",","#)l*P9h&","YJbl//d","q%{Pt9i:/","2law>(SU>" +"","~~Kt+nVe}V",")\",":","c$nRB|r","%}qf" +";x6","szLyj5a","","","Di,,gL#OF'","Lrb"," ?;","9d9.|6_pXO","s" +"S%","1/XcSaMX","I P0","#+9eQzCiCr","","jz9JT7)]Q" +"9b","G|&9Z5e(d+","d","C]d{~@su(","","T&_" +"RzDn,lBMB","XUzS?cHL","&j&M4","l|+C>" +"R",":1~hq","jg]o3","@","&&:*NP&{","-=B:(#","s,6>S]#","DlXI3Dq*U" +"$f""G","DMJWP","YI3ca'c-","~?,nj3R*","8=G'q""Y/u","kB" +"Aw|BH7vy.","","d5#):","zUNcA","=","xjE3@EQq@@" +"","n=j ","F}on@h","d#","+","hNK[=9E" +"U%5g-","","24","9","j%hhQC""d,","9l$yY3Nd}" +"l!XZ[yarA","jr8#Sl]","]Cu","g)]xx","lV4r^0Z^+","" +"","Ad)Zk3a","V`6R99","","","/|","^{" +"*'|z*HBd""","X3D$","&fUFS6","ztR9e_sRRu","%P42.","" +"[)hH2","[EZf<","WA4k@Yx<","","k7","V81M" +"Q","tXq","r","h~dB6","J78cdP |","?~o[x" +"Gt5er","\iCp+&aT|","1","=Qf>","0'HvX",";4YKF$!tA" +"YwMd`","?wx[5",": (WPPT6","",".w!R/0Au;f","" +"D\fl","P@-WeBl","yY","P\g*U0D","qyqWUEnqF%","%" +"s[L","","tkp,eaY^o+","]9*{","","aj","Y2MK","3LJ#L""L+$" +"rShLDbL)#m",")b","""%dH1$/&h","Hgc","`z:^#W","Q" +"y^d=Vq}\","o","%XIIDox4G","N#85#YtP","+K7","" +"m&\C)'wNXT","UO'\eQf","","ueW`","2QxV<33y ","*'k|*(9" +"","a","1zxr","D\+PL," +"n=cv$opPG&","","uU`!pHl","s5*`5","=M}","yvG\uy" +"Vpv9Ul>","G]7*ZK","",".?hm","A","@!" +"J^Wci.d""/","XHS","&","w+GO","^]S,Bj]","'39`" +"","[N","","qd~+Tr","Od~","J~R$S" +">BI]iFU","|","k!w^[P~I","","jj/1K@","7yTz_igXd#" +"><8DK;S","w#","\","avI~D","o]@%","Zo{P" +" G7&H8]=","x""trJt"";","_/ A2","hh)JU+d_-","","bru(" +"M/","hip.$","KjL4+!.c","SeB?Co?GP","","~^Y[K" +"yI@" +"e=4%","z.y","","O17 D^","m_j'\8","Hbu","" +"IA!PiGK","oO6PS>o","""le Z""FC" +"xS_","","U]M",")6WigXr?j","e#?E,Y0","Y,@:^/=r" +"%x;9","r`c`{NcXKu","}S[-1}VI?","~B.tqq)","YKNjCl2rup","" +"[ H","9SLb","{34IkB","Bj","KiP+&$C","P$!)uOB'w" +"","d!","uXz[}","8$=O",";D[@e","JqtW!y)" +"CNS""[!","H=*kR>zy>h","DIWhg",":%@?d*fhfZ","}rW51((","RlI&" +"N%DU","(8BaTyO","gy@o","","W(9","3=9:.V" +"9","!p5W9","X*_?","r","""r,x.pG","ouA!a)t'[" +"Wo^uq3","}_M","W*`45(A1",".Y,oi{T4'","d!G&","|R[-E" +".un.&[p","y","py2tVO_n","I","0b%/0u|a3%","" +"E2G*5u9t","I*Q*ae$q","m","","hQraj5$""","Rb}1`8P" +"`uM85JM","1L$``$","rbN" +"1_.f","AF;6nq3|","|6","yc","LOf7@} ","s:uf>\a:6R" +"/9v%]X}Vh","D","v0vcQ","-o","}MW""{j" +"","","R)WY?>&0\]","}Ask/",".83","Y}L={a/yL>" +"hTeeAb3i7",";","GXtGQ","mnlID","","-5w" +"CS`U","6.~RO!^jp~","z?(Q#5y8v9","i","q8","cFQ$(x-""" +"EC","OscQ1%%","I$=X","4~n0z","h~3","_{@F90" +"a_","J~@@hk","Wc&!^","U=<07L1","#y09@c","_G@$-Mcu" +"K",".HA6","1","(NQI","4mCZ5%)J","[)y" +"","l{dc","oq/","~ZC<;c","\Zj+eE","i!G" +"<(F?","\d2!t 5?","hg""","3}6oV6","8POkuS~\N%","sT" +"&B","G>':PrG3^:","","3'","9i_Z*A","r" +"`;zk<`","shu","Fn4!","([7~5*`Eh|DNs=0","m76;,","Keb!","rG1r","A$BA" +"-","-T","/It:W8O","V","n4~M}","fj+]" +"T44X#+`s>","-y|:m","p V4/v","B[c",":}U*i$1","LoD/cY3F|" +"gaqt",",|NSar4`","&","","yLECc\-","A&0x'&E" +"A{/2e, \?R","C?JX","_QI","TydVEl","$M2Gj%XTIH","k""" +"xT<@*hKs","X.+","/9YEP&q`{","X"":rRb","M","nbD" +"","{|8c#s&G{/","_2Y^v|$D","|z1ngG0q","|=9B/4]RL"," zE69" +"`","","[!@","/|t%;","^g?d","cBR)p@qELI" +"lkuV>","?Xi`~p4r@+","9,","(KmUq3p>","c~jagEu","$qC","}SJ|kW2","E&57LD_VK%","p@<","5Z_f","" +"""","[+Z#>tNb","m;+(lTK6AI","W2e","=E\","""H" +"",",hN9\","","IT$sM0","cwqF","HNXvEc@c" +"U","0h,E","","bl]S,","-9","_5S" +"4Pdl]=rUr","-Ze","@P9sC","*pgRYh","'","!$RkwHb-" +"#*YSiM1#k","c","jv?Q#?>","\_QC","Hrq","v2B|" +"pNW?(uH ","wS\6",",gx$t~M}","/v-$L","ShI`_:]w" +"","4[ \>IgZ=","$d*4dZ""","O<;vP","",">Ore\%\]" +"nu02(","$e*",">K7qZk","\v]e5""(W","**0,T","#7;AG7+5""<","8U']j3 Z","kI4e:","sTFDz^FC","ly>vQ" +"","eNj","n'oQUEw{Q","KIpcS q","ARedi","uh2b" +"Y>sGR","t5'+jf0AJ","JJ:!r2V]e","a)Y|.I$UP\","3S","S*" +"RFO}]Q%","]=fo_","pQ`E+) '","}+|9.LrL/","'h","D'{:" +"u","y!ZATW2m0&","?>%:WKjO~D","_","3","","dR",";e_zZ3(Wxw","" +"MH[]x#(P","W""]Y","*_pWGvjE| ",">1Dm?G,BBN","Z_~V)","W:Y|Y%(k" +"u.5Pq","\r#""A%B","o?X<","IuF","F","WN" +"8$L6Z","HSktb""5@n","f",""">hAPCAI2","","7APO9`" +";Ms8pO","*m+CDBS]","p","","rs\O|rD-","n9?blg?ru>" +"IO,","i9v|sah","-PkL","jyxo8i","aXfPa&"">k4","_l>j)N" +"m8>","eS2",":'Nxvm","F`$|y,uh","#lEQK0%","NDd5" +"o~,Wh1;iMB","JV%9x.@iI",""")VV@7*y","Ao`Kwe5c","/F@","{n!s,()<:a" +">e","{$B.",",Am&ttsU","@a|","8/" +")Q%","S+L;@@4","jr","mO%Cj$9""","6eSm5","@ytq" +"3Ei77[RC=","?","&R""d","j\en>","_M~3p]","[" +"t%({","3>idJ:9|NZ","","4$D<8{hb/","2h{","c$&@JhSY~n" +"DAS8|","Y7K1Px+C","`ak9","Wn&Pv^`r","Aaj~","q16{ir" +"kK*=rW7h" +"lY","`KUR","+4scACgs}O","C","8aBpHDR_","CtsJTZh~" +"x/[ouRM:*","(RA8d+","m(pE]V","b","(PiD_G","'K " +")mQK",";sV(","}N","9*Q$GcT2","SEY,6#y<0+","y!n2" +"|#"," k;' &","1BI>aDR","|q?&{(~$","exH","bq'",">ul""V","@`]mS{YL1H","9tOuZ" +"4N","","G,.","","S'k3(/q","A" +"37&Qa_W_","Nb&" +"/g)tnp_","R","E",".t`Q1t_j'-","Lp78","{)NXoK\[" +"Y","%v3","Z{DGm_Bg","Whih%F<","e^1E[Sf]","~A5Y(X;" +"1l""H/1*A4","2K)(ThDd,","24c","@M_n0W","","=m[O" +"","[SZBy,s","","u'0xNjw","/y:{+5iJ[","M {@OVzA" +"|@gr","^tL&","h4/k+","","E~y","!Jw{h" +"aX;y","Beu43#","rd2|1","i-g'vz1QX%","8|(b|","1:y:" +"tc'&HM(l|D","Lbg","@jB","DU*F0y","X)I.",">5yeR" +"ZBH%T1","""E8t[","bYj[v@","2","gVQ","*CU" +"xcO8,","/","Y[uI6N","9Im","x;&nx0FN8~","M;P" +"|\W",",7Bbo:,D",",'8>zD1","HUv4&>0iI","|q#PA*q3","x]t'{e" +"\","}yL3\(FC_","","SBh` s","VYd&Hw?","X/p:UB" +"\@-","`xd","nyY`D==","","wu","texOMu#V" +"X @K$d3",".0M|0","*\Y~S","E?';1e:","_l?)`",")4hts:5N" +"~\","4qTHu(x-","mmtcM","","5w:mu*oxc","ne;&Ym, " +"Hm""","",",LtXU}%j","7N'qB","A>H","?EaNJ=" +"","32=}pv%Q[s","K?'#o.!5>g","QQ","1m","}s{>DdC." +"",":Wv}3.>k","{}s?X]","p?f't)","s","}" +"QOi","ro# W8n7","|TTh|XNf$Q","/tQ}_!","z^,CF3O5","2X""lf-LWt" +"QE{","C~}JEq*L","","B","8()",",y~R" +"b!?o2:}t","!]gG>","e9.u","3D","A@f8<","" +"taTlE","pz2F/@h","'`]q'","4qYuS","s>M%K:)","'P-" +"V3oj","\[~HoLbl","e6v","9OKN,","'>,l+noQ","?z-&" +"","r5`5~j^d|",";~_!}Ev|Jt","AEP2f","4","j+\v7ANGt" +"5","nX^bBv|","}rzei`L\9m","","8k","qWCG(a2(W","" +"s`KG[",">/TVHz(&Kr","I2^}","R5s","w>C)j84+t","R(O-V b""TU" +"ZV:cFQj","]Y`CxQ ","ei","Tq>,]*%%","O_]6h<)m>","UqDH/bWG" +"-6","L|S<}%=5'","nTq1n>9","8lm","~J@wf&-JL9","&h" +"%Pn|G$=vw","x^oS",";w.^vUG#G>","i","F9","R|Z2NCw" +"R=Z*24#A9.","9$EI\}k]W","V_","","","6a|Y6P9<8" +"9A|]bLC-0i","!oO/!HE~{","","nO","H9P0LCP","q^W3 Q(L~{" +"FJE(","F","ww ","kgb./(X8;=","","YfDF%zWn2" +"p9i#Q","","]*mH}/&C","<","]fO}&7@w4","m(" +"dll*r%n'","RvvqQ_C","yH!z#A7Uo","3P>/,9s ek","'h(8pZ""","lK" +"s%FCq","&cNT","l[","Fk4{z","","Ap8D#" +"","<3IAZ""T}!X","C9d#","ckaS","'<042|APki","UVe$Z+We" +"","%","y/0N:Lr1","?|h&jX","d","]:O" +"(yce","u","'va]","w}5-#{o=","-","" +"%+,","1[","RaW~q92bO","T4","c73-IG~","kAz'a4" +"FHkeUm]","}Y","k""q]vm[","F i","U_evW4[9","<(_~f2E/7" +"^vv","JW Dp%K","B_u{cd.N","g*o~7?","""k6`rYtB","/D%5%~" +"4>","@","","56N","Xo+r`","k0=l" +"R{W&uA","H2","1ZY>L8.<;","LUFGto{","Kg{}=Y","''v!U.j" +"$-let\9n,","V6P","noR>07q2Z","hBpQJ","cD5?Im ","y1`" +"EF\n4td`]","Ly]y1mq","","O","\=WS;","Th""W4" +"2tsjOcJ9L","CXPQ","","F8)YFY",";C","d" +"z9P>1Q9|7","2","}8 l","d[0*cs","","PO<" +"[wV","QYL08T:;l","2$jCA^","(|*0","W%","pY.83=[Eh>" +","",Ps","k)%yr&]43","ZF4","m}\","a","x" +"Ks#'fTc","iK&\","wH2b","",".iu","sN8 b@" +"8F4","`|H?","","~A3""UR`","h?n)~)e","[ZNu" +"v","& vMrdG","`h!","$","W2)W","Xp!lN{" +"iJv","Ab","'+UdYJkQg","F>!","Gq:D","""vSsPS5" +"K","W@lwZ9u","XT>/Mqcek","y","RE","+'_=" +"5hA","X","v*iFb~g","lotbMi0u","","XG<-y.@kbX" +"E+_Mx","$o","o<","v6>`&J","!X7aE","[[mM5hrp" +"__9J'CSS7g","98E;c""","RV5","","","u" +"c0=","&k","k","/6","%~Cg+e5}","W)@|$0" +"Ds0wt;","8S_PZW(","y:;","^i(yU","U!Z-;nCZK","+%km" +"f';w","ubD\o","D[F#","gZ",";k'fx",":n>Xz" +"Q",">hD","bi6LbP@","`\@","/pqRB",")&w6/X,1Z" +"l5f?","vQX@62M|","Pv","][""l[!,U/","'g@s:ai]>D","+/}g#C4n+b" +"","oJ>2fo","wF>.[","WoN1,B","~|[$(L&<","# ]0" +",TJ""^Ej>","8Zr","bY?:J{""R","G~?","BsQ8","b" +"mY[?a","xKG&7""","0hg","O_xb","{nz[JBL- L","" +":","9{R};yB","K_","rpWT-""","8Iz?}" +"3p*FqmL;9","W?)D""H4H(","0$#Ln2)Xb'","(B","*s","*PF}rt[_V" +":wSD","q(4-{","x(&Dk*[^","g>v","YG","f!}e(" +":X_","nE-","EcI""O~a8c",",.=#-C:GQ","","n@o{" +"<#YrY","T","f","","9","mn?EIy" +"Wb1l""9;.*0","""f(","@K39""hkVR","mYW","qt","r" +"9","r$""","U+ Sui%",">z1Y)+k","*""Bvy"," O" +"`!","cPlLL","n?=%p","}bk>Wylnc","0D?L","%z-v$8q" +"T}y=8%qR)L","","nx","","","?h_8&NzF" +"_F}P,","0L","4jRkg&>","`@n","/616","sj7" +"mrXI",",5,\Uy}rY","rZL=","@k1&y6@5","","""@xh""F@" +"m_","]/Fgna9yV$","R)!","k'>Uc\","vW8[Z-","y?" +"NfXHZ+%[",")^$42V+?","kRZ}YY88&|SX","m]""8h","a" +"Z4s,","dA","W7:i;Yge","Prc","N.","" +"O","/o!2uT7#K","m5s~0","D{hZ(",".GG dGfo\m","T8n" +"bM","BVjk@?[J8","","q","=GU","2" +"c","8x'!","","D]~@UL*v","Zh","{-a" +"cE1 ME s","Qc[q ?1#J-","BCP","","X4[7",">^S" +"?","|}4BEnO52c","`nM]B7Y U","C[IvE""","yae2H?Z_&g",".+gjkGE]" +"popKD(}/fo","/4","TU.","&,L=DpA","*(","G7" +"c<4>","r~2(0[dgZr","A g(bG;","JQ\8~.E=&","&:'y{Pf@]","%:y" +"dB mDZ:","_{",",U|!@!c","","3HMwjEF","XE:4[IS" +"hlL","eQ[(^Qf","[ ","N","YTm\","" +"oVv-EAqS`=","=SVJwv2|","^F__;","P3Hc0S{G@","#=CP5","F]b0'u4" +"&$+o+T)","^#/5Y=","c2ruV 1^G","EAb4{Yi",">""uU+","T\8;lR\<" +"3FnR","{","" +";R:}$""e1","#","WY/A\m","q&[a=","lTJ6X$,y","c+oa,N$uX" +"","W=bSGc.sf","0n&w#J9;0","$@/-]e","MQ; ","7(B!" +"ta:Xyh1","noRfo","","L2te`A","U{enx~Y'`","Ou/(^""zO" +"%%|#no","kruMC;","R""4Km}qm2e","0N}3","g[@a(f^""?","a&v#QFjhu" +"EEGgch6YvZ","<=b*3S","","|f","\Qv","jDuf\gU,O" +"RwJC(","(U","8\==?P**","D&|?","Y","O""wP=+oDb" +"?S[Q/l(","wG4&v/s","2D'o{b^^be","aG9w%" +": [^A*Gd","T`k([`*SK9","""cTo5","aI[" +"RvA","H|J@J~qy","","{!m]+zs^GZ","fnO",",o>|(-=" +"pfci","k)./_i","@","z#a%$yw$","","H" +"+H:-}X","=zzwxQx0W%","t0B0[8","!ay""AQ~J*","A'","" +"u[ M61@~Tf","I*}aThg'#)","^n%@","","lB?)","PqPsHyi6" +"Px3lg","tR-B{.=alE","R&sG+SX","",":",":bFJ2Kn" +"K<\h2.6q","5\&==pj","!&%QuWUg%7","qxof?;O",">:=","~zx8BX5&}" +"s->~;Kr","jC~3E@7qfQ","","#=2Vlv,aiI","+FL","" +"","xy\~","","y>Hr4|ski","]$GA>L ",":v\e^dA_" +"","#uG;","XOI(","","!","oe%" +"C1R","w6ZJ.2","/&","iE})_MLSH#","g","AF""^ih>$S-" +"o)K3W:|`}","nT\/8gC^","{_","FSyv~SK","","a|" +";>","","AX%>k","{iq_f_^7\","fr;,$","(0+`-zP.{I" +"`$T3!cO,b","WPk@`Cm@GO","","d","=jx&_hXc[c",";" +"A?","e","0\H_QF","YG","7aiT#","x+PwO<" +"i","5d/X>3)#","1+'(bwSq","}o^V9kuS","fI4\(^Z","ON7C" +"uT&v|2","5","/","","y","r{=I.\" +"N[+v","`","","o=Rz|=\","1cWjDkh","Uljzk!" +"o8_`","nrJk}eqGfS","4fR""},!s?]","S{bGL*;0","","7*$" +"8JB7xr#Hy","06","_{F","tD43B75",".bLTb}i","o?T" +"ixXV+","`","","%$EV=S)-","+<2Rtr","tYq>Ptf" +"c\}3Ru07","n","+P.NZtRn","yn","D@Q|*39L","7P,XcH" +"mPOzk|<29","PdS/l:I","","([X;7@I]$","%QYy:%&Ih","|S" +"utBlI4O#M","c$NTF~-_{","V","4uW","(W@i","rN" +"JU(P.X","4!O>)","RQ+@ AT","jg8qpq|.TD","/u","=XBAnYIg" +"dz","p4*>{YG^","~","D<@_[","Si)?Y",".cL1we_(dD" +"9{(E"," 4","tQ['g8pu","t3_","yO'J|",":6=[k0*)}v" +"9?n{","jY_n:","7","2",".uWtU\","+/" +"jq:j"," sE","oJfe80i<","w4",">0KB","" +"O:",")}#f","Hrm" +"",":.","40!|00","/d#","lHH","k:#}(3" +"VF*b","NuFf("," $","#-Y#ipE","K/T2;YhA","-X9HasVKP" +"/","M08T!","qu]Hf&`","qjpc","d)sug","81ztm03!}" +");`$0o" +"chDz2's@p)","cZcf<","5jeuXv","+hrU,T=","fM&!r4","CMCYBL!m8" +"d]","h+9","o*[]0I","PR:@","_;f`hq_","r ON" +"Ol0d8Gf\(","64c&0IJ]or.W" +"[$+F<","D","fFv$5_n","^m6Z2","t^Gr-:Q7G2","Rt\V|" +"L:R-*","@ts|t|wnA","","`n","","]}w~Y2C#E" +"Q$","691i:","!!>j=#iv","K!]`|9H","dVB0uJ=WL","dXI" +"Xj `y>1=6","72","oJ=@If]N","-D5","LX","" +"aLQsfsMy)g","Lt7E\N5}","SVfn'C<9(d","","{\","h%bOzTB/q" +"Nf8lpr3; ","Z)/W^mihJ","_NamL@2m",">C",":s-Y","?" +")'N2=ZA~(*","86eAXvOnm","L-","6P^ex",";dg:rlWU","^ bn?x%" +"!^","&6zh( >$fK","R","","MV6Mh+'uQ","c >J-TZ." +"P","N","Dbew\d4","An>Z","","TbpiXl" +"e1Ht#}d'&","w[B[","4f","ogT","C","lC^" +"%y|NR:u","}s.bCb=i","iK7#","ok_jxu","+WGctl9","u1Z}" +"=S`U(DaG","M$","M1","e7m@MrM{","$tQ&C(p*","" +"atDF0m","%kYk'6fU<@","SD04Y'#+","g@P$ XA"," 9])Cl2@}e","T." +"V(XjAGfP+I","\GO4""T5,~h","","uo/","tdqu:=f`","?_(N" +"n","lg ",".[7","5:M8I6M.t","Ze#oSj\","ySs2$1VC" +"_R}z>",":","06zjeuTDBk","6VZ96A$","JYVnFi lOn","{/""+P8v5Un" +"{TbI,4'.\P","r!","|wf3?mR","5y5jo4QP","[I~1W2ZKc","SwIeq8" +"t5)Z>G22R,","S$3/?s){&","vMn","lJw+","17\$Iy","a1ZUK]gJ" +"cuh+","J/j\~A","k48~2mf"," >bd]rp~M","Hc"," cO" +"J","poQu+}SHi^","3IID","%4DX","xxoG:m`(","k" +"0`!+","Ux(n","'tAZ*","/&""oOY~","6OJc","nr[ M" +",][3X}O$","YUE\l:1R*:","J2@j","B>","$_k*&rnLo<","" +"iK/RK3ty","(d_o$UB","MN?[:/","nQl9","ox"".u&B0","CZ%Atz" +"",".SMK","8*IRHk/"," -RbUX3[n",",T3;wLh>~","{28FE|" +"?43h","","UK",">XR*(T","SFk\u-a`","p" +"-_","\%v>6Hy$CH","O2B)","Qa5","MT","^7" +"tw:,5E""X","#HmD#}xD","](&6@oI6=","an#@@jR","Y(9","""*0g" +"u?","qDAezvF","p_","zlp","In'ih5A%","cQdNj`D}`" +"V","7","OG","","Lghzp=)bYM",";-z" +"Y}ClD=ME","@AgHhx&kQ","+u","","x)","rD=@PJ+7\" +"sqg","or1l+E ","F>","","XB1ELt!}-","|uW[H" +"","","#oYfF&","y^R ","R4ai48|ee","M{5!Zei" +"zAv$.&X\s","vC;i","AV","n`'","$ht@TVJL(","P.KvkL" +"Lqa","'pjT8W`xP;","%F?7W""","nTW<%Uc","_","uv\@" +"","XA=Fh'1*Z","|/*8","yP8ibO","g@H>K","#" +"*R","`y]5MS3","","*MEFpvqF","1qecJ","F@" +";XU","(?KL$/#","w]LBt" +"*VUHX""1BZ",":,n-RHc4","#kSUF %`P","j4Rf>\`=!","z%^!x","9""H" +"iX8cgE)4G0","d$wp","Ll{JEV}z","*0K]gVSl","S&J","n,-Bg.sF'K","Y","a3^,_(" +"y7&","ine!a+ISD","tfC","]{z~+","WCGt!","" +"SO?y,E\Im","C~qxsE@9","m@/\uv","","WKa","^l,jUMe" +"XR1J9",")&D7uS'","+~`vt",".EH't ","SFup,QV","6rDpJ50+1" +"d09","`Bh{|e|]","","\iEe","T","k4,9^" +"HsTDq","X)L30jRRs","Ww\/$t;""!`","A""8*dn.x%","96+s`q ","'g3nb\l" +"WDW&>'r3;","7z52eH&>F","VSj","s=z=y,iI",")?!M","|wos,_<" +"Z6 c?,_JK","f.!O2TVA","k1IfG","lp","F])=3_V;","-""c" +"pzT>F}","/ee>","[o4l@u","Qox","@","}kjx" +"YK","gE(G""ZWK`","","5""+>,z","&Ocj*","[2HqzK,ojc" +"e4x5YN`@","PWk","vj","$8 ]b?~r/","\ l+7#%","" +"E8U7iAOa","b>N","^","9","""=","=Q#'" +"erD8N;d","","fSq","^GCp","S^D$I#rc6","fm-'r" +"j1","","Vtk?0p4&N","A[QpS5c","C+6mQ","G" +"9H","yUj5","-nd","/I@yQX?qg5","~SR","17;" +"t","B""","a","Zv:f#HPtYx","n","Cdv" +"}6""Slm$B","Yb?f?A","cU)&p","95XWTl*J","6m%i 5?.D","" +"NrJj","0|dA4%","Ht","+>eq","q4PP$","J)k" +"5)#40U}",")O?(","","_bp","gKsIP*l-",">a&U{" +"&)_mx5","p)*i""","'o#FV*y","","[Oy/:2a('","EXjV%=f?8" +"Gvtj","H&m_#67W","5","RH""F#qh3","","RPL" +"","w]'iz?$","7","bv3J","6/B7}7pJAd","SR5os;v)ll" +"A1@""'#)#'","","]Cc","gc+[","iv;t[Z","yQKx" +"`.)","1IV""L19S","Q`(Ssi=]M`","4EcI","1X7nc`Mt","{z'T" +"","b`","8w\UQ","mMX","&EbqkMQ","S","+A6`5" +"|6Dpk","nG/","h]gQ,Ub",".'","?U]HH1vy","%F<" +"A","#r","#+>>(r&w]","","","%g" +"VRtPS~e","}PYc&*^","Y5[SkAW","$?.I9\","lQ3y938VY","K}" +"zrjLc","Z|a#00TBxa","veLdNa 5","","U>M","" +"","t","U","tY!?y J?,","sUJ*Vi[06","Ey;" +"}w",")","B]%Kf",":kF","#'=l","P!","h6'K:","I?K" +"","W:M:7","TdaYc","$*Y","C0l","CU?BrtInc" +"/","d!rMw83I ","da|Urx7","Y~QSxE;","vKE'kfx","A=|:#i)eu/" +"S+HK","tgd7e9D^v{","","R","(dXJ]","WN-""" +"laWe/Rf1)I","e#}B-;,pD","/*A",">f|HY;Qa" +"Owm&fl?X","#B","-X(m#0O","","k?I","<*}+pSOo" +"%F?X","","SPx{$J","Gq7B5\e:","o%V4()F{-'","9|x)V|" +"PzR)}Ej^O","2","""vF4","M0.dz","l5b90uB","*ZryuJ" +"","T5Zt jS@O","5,","E7{KOm$5K","X(h-i%'}","[nk0+pSU'" +"^","Wh,Xn""(AD","cu~$sMt4]t",",Dh@/y","","" +"Nz+a}","}bar?5Q;sJ","T=C[/","J7""lx","","" +"KDJ75","8","yuJ","K,","^;EO~I\",",WZG]/oB8H" +"E;/P)0x{","D*i_F","6`j494Gg4c","jnL=PY9Q","#","7}4G;UIU7=" +"$U","E-F8xTE4","rGH:6Z./","5s>Lh9G","\8|dcK","E" +"vHb+Bt~!","wL<;","/e!J","_fUkr\ aWN","""XQgPj](}","tv" +"L(fKTM","3u$-v/ch&","HR+wc","QgLq$gVh","jI M7L","# &" +"r=M]n)!","A6HZ","fC","Jt7","kq#H$"";B","(Qo" +"e!s","AT\4R","^/gGe","Nc7Q_","7[_M","{ca5uPcSku" +"NS","","?vdq\6A","B","N4:","4Ji" +"""C6mWF9l","@a;kOW","y#c","MQbmn","]Sl/6qN^","" +"!z","z_is","i$aa=r" +"f~O#`95@8`","q","G-","5,R[N/'4xo","8^,a","fe|>B\" +"'}+pY_c","-F]>~-'",":-Y~S","","J.H>&Fi","Rq|V*" +">}","T~meCpyJ^","","Q","R.Qa[.z","k." +"+Nn+IK=S","e","o+{Uo","mp>","No'mgJ4-","" +"J","od]stFv-]","V`YEG>","","#i'",">m","","@@+""","+89" +"0MG#>0","j","S9x","<>","x0ZjM","xa3 " +"","sm^t\A","^0!/|\,cJ","","->","Ky\|" +"^\_Ewh","\e%TC","U|I/@'_l","o@s","AiJ{,",";ro$g&" +"^#`""Xd",",B#","Vsw8%h","""`1{f9bSc","t/y7!a@i","i""5>" +"+s","3JC","AGl*]XG","h","~Q8!FV9@","" +"miuBJ1R-.k","+""-~","OIQ$+Q=(","amu","","u-?" +"lq1}i","%KxCj","K)",":0a$Dr[","@VPNmm24/","" +",R5b.q!|-","5w","#:0(u60S*","Ye]=gb","1HzOtk","w~Hs" +"+#6V","vi#+x.t","z#J>","","g37`~R","/6a+Q" +"Xh(M","wbf`@T:/c",">: /,_","Rlt@","","!c}f>+T" +":C[","#""*x%\f:f","xa7/" +"","f{SEkSGh","(]","8O{","@wX'YTJt","V<\w~Aq" +"?P#{\=(K=","rAr%G","T","9]","","!:""ls=TMxw" +"JdD7w E%fE","fXgmQG""c","n[Xc+YIKD","^`6wj[~H","0","5S^" +"u[|4","Xw2[cskyX","","nWJrL7Zt","""E\(5l]Ph?","" +"Z^8","R",".CdMNd=F;","~x):x","""""DT|c6BpH","" +",W$Z)$qn",")","@FK","G,`?atq","FT{L","}a/%X" +"v,S","{","$uL{$[Ih,B","*G;PES>","NnX;Re","" +"X?}SY:6Xm","BN",".","","qco","!oS" +"7Cn:I","%Ub$","F4MFLH9)K$","","c@,O57KiV","mq\" +"Z)","w8J#gSg ","=l3$,@",";{Urg6","",":u" +"","Mr1Z","'~0","]kH)","]fD","#oZb" +"CJD3",":AI_vex5o",">y4","9,-WtDA&<{","+1\+~/sTc","" +"m4Q","^u`r>","!3]m]","","07TbdX6!H",";t: UFd" +"X","hrm","4J""VK8O:$","({","\(n<=9","T5Z>xd%9I" +"V1d'Jl+#Dn","(AXo]Pa{%s","",":K","_mLV","5U8b/Ozqw" +"d","VV`jA","n_>Zp6V'A","NI8","T;Dl#","Gtw?~" +")a;","@A6J[4`o24","","t!^^N","`c+Rr","p\d2G~L" +"eEG/&!","[OpE4)viNQ","yAw$""f0u","burv6O}","nzVS","ML" +"4S3b#3'j",";01","h.mSm","ZX~uV","MX(2pb&x","_mH4{A" +".eod} *1XH","","*gQ",";@w","8%Mj","wo4@@" +"Ch:AX","b^Y\{","H","Nq%!%A/sim","5#","ZDSE" +"P","CLe","UC5Z`G","","1tf","O8$fq^Y{j" +"&vHc","","jOR|_e[","mti\L>","Y","Iq#gbp0.%" +";E","]0fX","WPi`6wiVgb","='MgYi_I0?","5+/","O)`A>h)&","(""g&ER|1l5" +"*K]YeO","","M1","V","w>x","^.$N" +"?Q]","-","k6<'@N?","","prp%AN","%nU$!" +"-x6D!LgV4","","Iofa78Or","#5g4_lXn$I","PNMU|qU0","Z*os" +"'CLFy*""to","X+<","^kgjm1:.\","E?2_/t","V!?#","nZ*7" +"Zsi!PBf2u","ddU","j@^roIK","~wVcb\Y*","LV","o%`[nK" +"6I1","QF8TxP","v^Bl;o3","b","fWH3[","yw]5Nxe" +",*orfSKWLZ","","jX#k","h","ikwcv ","=4rhFs_xH" +"","tL>j>GJcC","T","uKX^_","a[sdV`Pz","Sm" +"V!S=W-c-4","t`b\ A9","","i9!-","96K\:""+o}{","+:" +"NAJ~","Az6t{m!CJ_","","d>G?X&sTZ","V",".hi&|""qa`" +"a.8#'","(","\'K]A38;O","J*|:_","|D","c" +"G$","","1","Ef","BoZa","[""J""" +"/0,H6]GY","","\F$-vz#K2r","0X@8A1fd","mj}'}a`ctN","u7BcM9:" +"nW|sA?qXC","h(?","!$\Eejf","H!P","9","|7nWBm`[T" +"r^FiB+mP=Z","!",":~J4T","b_uF4tV,(","vTl{=Us","_ -" +"","bth*fd9%","h1r\Ep;8s","TfIOs9yH",",","1xDIv~#G" +"!>Ln","|HlkL","4O~'`M0","","6nNcJxiJ","0" +"o9Q.!!?-","","\^","S","J_xJL%","" +"","^;%&<>8CmP","RdQ\Jj_!%f","]JP","tr{H7","" +"{8j/#N*73","","o|rUz","SDPE98A","sF","FMr" +"V7/Xf!a","3e","E3)|p}\y/","","pr","e;{\S1`=#" +"VsvFpD""u","=#>gL hgKc","5<>,\","H",")vV!m%u ir","#z_" +"oe32&Q^_F","}*","m[>t^2rNO","p;Wu9+sbh2","LIm l99L","O0|jH5" +";","7g","","L","2D~GAd","|6(H7>E%5" +"","9L","xSaL%H)::~","&m,",">Wf_@AllF","" +"p9=MfoJ","zQi#W?","OS(13\j=FB","","ha}u4H","" +"p","ja","l^^qH5k!gL","X~xYC","$?5N2a","gU" +""," y%f","2","%D57;+","#`*>","L>UzOrOn" +"A","((9vRc","PkUtfGU{4","W=","q`f#]vJgm>"," IIsn=" +"L`8""Q}-27%","S:pY+W@M","++:T_C:","A5AVY.!!;","=5""L","" +"]?","3f($cTk","t*4LP5","N","G).Z.=;N(","g" +"b3y)","'[0/","L&_,s>",")","%MO~&Bww","58t\r|X`=H" +"mjk*%&","qWB&j","&v0(JM,","!>*+","d^u&zEIgD","VN" +"*ee_.""","""","\qVrxV%","8>\[|N","$oJ7st","""/J1""k" +",","y7~ohX'F","""p","7|4qALx","^u\i","" +"ivBP]""uM","`8 7^yX","J`_","|?>VGDpGg","s7iMkU""k","RYN@zu~F","Ej, w;j$" +"`5c","","cRu>","{@%.A-""","{&8&J'","6P\cQaO" +"Hb@94rah1h","8~","Th[`7p$Z9G","YznAYYo:","(>>Ve<5","LFd\BlS_jK" +"FD^","","$n#","","/`qc","=<" +"pX1","6-_W","IzOA!H%","v?d*q$Nn","Nv}YrAyiaN","DK+=;w~P" +"/AW","""I{_","6N{7_+","PLsFAs1;","8","tmQ*SdJjEM" +"3AoPWCRh","{cwS","F>Z}y4^%A","?s)V43>&(","{dR,l^ah=","","b:w-" +"1@EhM,(\E","\" +"gN/~84","c3R_GmQ1","svrtdog""","2!}RI^1^lq","JH>J","" +"oJ0RJnr","{Cbo[w>K","EfuJ9\m~:","O_<=qD{'","p<4QQ","}" +"fkg","e\1,L","@|A8IY","Z"")9*","","T56:" +"",",e-\m""g&c","0Zo","FjM","rTPMA","^V-~E(s" +"spko;@X[F?","[s;Q","Xz","%U&[|","e?","Pcb(6;" +"TVF)I","|b:","V6","-","k0aWHpT","J>" +"4?K>ze","sU","Y8x] !",">qh","4W7QZfR9%4","rO?" +"Se6A/1","pwA3Yh","i","1`","W,f|FKz","3={[" +"2","P!SqX6","zm+;@Z","7RB","O%VT","^" +"Gp9XU","?","","","YKZ1@","V56lE`vo" +".""8$","-!","r6g$TZ0mP","KZ_a""^","B[]","#PR?9N#" +"Yqftd$~3> ","JHn","bB=","7","K|vhVdQ","n.7P9" +"s:;}/CAz:G","x/ixsR","6uwMr :Q","@#sK[;<#","Q#;ax1","" +"","","Uozc","s","","bf#Jya%" +"""kz4|ve","}!Lhy","j.jn2","ia","%r'D","c.$H\Sq\" +" Wb]","W7)","/=Lth`&","j;]fTs\N","kL;hCqH","Y1`!yg_LX\" +"f=",";zTu\,^","cyfRp:""","}X;P","$vK=3G08]R","" +"7","H!Z4.Nf","7$6T\B9","=>Dys",";/!X|j","%,S" +"e=u","N~dL$*OcW[","TGJ(FcC ","gt{Q(","pB^oq","","o:=v","C8C!P')&;" +"%s1=xGc}",").","'{cd;9j\","C*%,*&q%",".D","1I5hR;2" +"7/S=z!C","23","38""[KMT2","bwQAR.&","4ZF""]""D","/WnN7\!""a" +"iF``i","""AAOMk","""73","C&?mME|","","5)Y3_gx=" +"","1r~","S y","J6Pn","#kml","N^ZNj(t7" +";+7j-ob","9","y","NUyDn.r)","6B#9U","" +""" l",">P","]Cu""ZOccC","K>+","tc6/Gw(1","-fE1IJ" +"R","dY`74<:V1l","pj","PyD96M,","Au[","[DTO%c=" +"-_jo6[GJVA","dO","mJ&","$T G&","W","" +"0""mgGkS","X,[WN","Wx","-_L+F=R","3^","?*" +"Bp Usg""","|x","p?#^G5G","k","HUl""Gysx","7|E73" +"m$L","uG6_ eF_c","yp9cAvmNC>","j85=9P","=5,@u8",")" +"%:#7e^dBS","**a?X;","7","q","%n(gG-xh","jb" +")k'[*","BjFP+}@O:>","Q+W/\a","_}X6^nE","PG1B?","A6bYaZ" +"%","'5C]n","{","NJX$T*K4P|","YF`YB>","bz" +"h>>h?","IGDJfnfI{Q",")1|""c",":pVm","","f/" +"#`,k~F","|=n'","wEjZ7/0o","hh","1{=|ZvYES","OH2w" +"","./2^y""8r","mI_7SbX","srn:Wcl","","S" +":^bP","x@r)Hz4p","F)|N",",","IeIAn4B;","U=x!?u" +"Hi","PHM$VT" +"2Z`)","","=n,h5)","kPmF[KI""","*(#cx/ig","xR t" +"SJD&A?#?N","t","*","","^HMb/P.","M@t""d$Jt:c" +"`Yi","Y;RS%","","Ua WnnBCs$","","" +"","{t`$Y?","","XUa,dgi","*gV$","(!tG@N6" +"","lr{>Vb","~X","y@I","F59|v","5TUHH|'}i4" +"*I?k",".FF","r","WxZX%t}%","xMJ","7|H'QA" +"9Q","MLE","An","k7","q\dc-Od","""U0O_85","0oH2|T;","" +"P 3","rW&i","e","U",",3/'~D:_D","zaG;Pd2'+q" +"R","6","4\Q","(N","LB/MrcQ;a","=92" +"1\{Mfk{=E","","#/[yR","B$'""=","[k>","p L,dVkp" +"/J","9Ze;,S","43I","%U","BEG:i1","!E>v&-'ii:" +"VjF$e","Y79.j7-.n3","ro=k","!Ksx","Ko}N","jr3N?" +"8""xNF","YT0kE'}f","Y0Qc!T!5","d.ti",".LVo","(R" +"@?FHxP% D~","9{]Cw?'fVM","m;lI2ZG","2,I{D~C'&","wf""qPS~UV","O1f`]u;Z>" +"t?2c_AkD7","{tZ","K'}iy|p]y","Xk4O4}u","Xlu5N","r~Ad3\||}s" +"i","klzafkg:s","SGiS1p",",uPgGo",")W{0x","^m8" +"-F""}&u&::t","H@rJ:8VI","G=o.;2TIRy","z;<","gSt+7 bR","~F.+[" +"*);TRx","JAJ;An<","g","01H",""," 1J" +"~G^q{/Y","l_+7|G(;3",")&1#","f\","QZc,xok `","*R" +";>?ceN","z","ZJMjydz","v,zoi CBk","","@Y*Mf$" +":fGlmr","#(R-" +"T8zDm","k]",".",")5H*#us","3{","Xp`" +"l|p","","z4cst%""8E>","l! 8*,","=c%","" +"vS,eM:","","CUz]","","o","['" +"'%#","'@?m@aL",".sK","","7FC\D_p.0J","nT" +"r","<4&F","""xDnr@}","v.=8%6","dza@r:8yOU","n1ho`Umg" +"","z","xVL`o\QC2h","i~","gV>6tHl6@""","TpQznu5" +"/",";M","Uf]aO)","x(}UO4","","ec" +"2","VnCAM*J-(","uBSU""z\",">hr17&rW","aIObt","" +"|","`R","hBdRwI","=","0y-nAT8","'" +"y","`C1u Fu","U""","*","CHKw&","k" +"ad","u!","k%Kf","l]Ay!ga","","=3)TRTy" +"*{n4VO","""UD%R","{0fFFj","sI5Kj","6mj9}P","Uz=%=PpJ5H" +"R<=e","%taOp5h]Yu","ikx1(ixX","=J!","OtEl;\kgfe","#&v&v:\" +"b>OIF{ ","oT.","[Tm","}C]cL","o:k ","","s=IA^?[T","ZZ/613!rY","L'P`S[_I" +"#","I9","'aC" +"YwZ1~","u|","'$Z-QJ","Hi9ep)o/","t*x","'" +"7$.g#Kh=","80Vo*y[","cuE_sCfIh3","|,I'uK","^Ip^yU","d^jH-Nx7\" +"L&Q^M","yt6o]Ev","f*!]x","@b\6rQj%)","C=d","8GkJTqjBDq" +"P&","`~w_]","i?i","J-;ZeB~?G:","$~u{","sM" +"m","+2to","HE/""!","3","KMvNl&jAQ","Y\" +"EN","Nr}PW7t?+","@K8W","e","lf!WB","mNO" +"#'KQO","nf5'5NQOe","'}\3i#=*X=","<P","w>","k0qmxr" +"","l|g3_S""","cq)g&F","~B+/:'$/","r4?y{$","X" +"!B*hQv+","]L%\h=^ja]","ed'D0#","P*pN\dcC","R@","L[_adlPve/","Dbl9e+t" +"","]`d(0a","G8,","f-<","" +"^.PAa[Jp",")z-","$;","*`","8WnrFEP9","%X,Cs4" +"#%t","d<","M55K,.i","MQ/yp6v","V;9p","`U""I<]9" +"aT_{TI2L1","TL","Lw7A]","B\9cq\","6","]o{o" +"|IG-""","aPBM&u","LBt-jh(#7","oX","6^le'yJ",">!u0" +"oU2$p","nw)HbO","twDSt@.)H","_Sfv","","" +"P,t#+^","=\f~","-[","5RnMDZ","Z","&_GTSd#b" +">jb a","j-m%.)W","}>?ibm$Ej","DU(x=W","","'" +"n","@v$&TYmK","m%","Y{","LkG"," ","|x@#","UbI","G" +"`""","&","fdcRXEN._`","s>O^6/i+","OXg","" +"%A","}G0.i","yk}g~?>!g","UrIhIn,%","do{#$R L","fBC" +"2{Y-","Hl@eJ(}1y","XR!a","J=]_6"," P-d9ev","1?g" +"1R",",8*c","yD>_`Jx","%X9","fbuk=@f","gU.&{%" +":)""WmHE[)E",".uxm07Br&g","_;Kv8","Fuygx_j~`/","jl","E_zs=" +"lwD[3ue","aiGuqd+U""O","{^f|P~&3l7","_","M=H)%-pg","2{8D" +"}FKC;F>","^aBSL*n}","@S[Jwj1c","""VZe58b<","Hk20'I","" +"8J7Mp\5^[","=""q","ibI","Y""Opp0","Q@D|Lj","'.1L$([mMU" +"ad>%ZL","'MX!mvqH(I","!{4>@_","Q'd^","DF?L#*","+" +"","qC3~bhdm","1$w","oI" +"F`",";82`wbuNT","h","ioK\m","r","7@""L$s-." +"GjKg|ra{","\}y^NS|H2V","5R2%[^Y","JkK}k","}}","C#N" +"8#sK","B&Z:gP&F","]","zh#?d","=","`^GUM" +"FlZ","l","_~<","w","Rb'Ad","hX-" +"FkzRi","","T+`eWqcVV*",";[-y","S=&HK\'","-R)sxn`c5" +"Dh$Xh'E!","RJZmu[A|~B","u","s""}4Eg0gZj","]#L,0uR","" +"lbNx|","_Op=%6{s","sC'j_&lpY ","uDHyz","5CXx|C5*","H*0nKRfL" +"",",[>a-pvV<\","k9N--v)<\","]K0}o","4cKze;`N $","(" +"r(>Kj|j[j","-:_","D","([O","*rN","P" +"","o/L&Nkkn","'lt(&","","=FKWv>""T","Wwz" +"0TTj","","V1n6+8ju","5!eo","ll1`*A?","" +"~h\s_]","wt","AgDG","sMsY/TH","jsS",".dq-fJF-." +"X20I{PQy|(","","/l^~Lp34","O`]I","@uUg","\GqBJ" +"","H`>","Hmoa7[z","F8o?`",":","&Mf1>Q" +"z","|2! PXt_","[BJF" +"z3=n>\EO","OZb","J","$.2;_\bQ>y","x^","0`=^B" +"Sd"".dF9B","F]-n#^","","I?q","L","""8L\S+M^" +"BSzoV0)(z""","s","=9,B9","#q","m9","|Zm37ek8" +"/W","",",u'1m>`/P&","",":&ye?\Dt","ZT!u%\hNtn" +"{cx<^U","HDbQ'cO:l",";Lox~w1=Oh","D.qpb4","","9NY@n5" +"~nJ",".yL^O","7","""3` a~q_","_7" +"[","d","Z)Dm","HaW2R@v","}Mr",">}" +"","}I td7R","MVe=@9]*R","?","","3gNpp-" +"au7","$BMx&<","'&YZsIrF'","q]h","A[<","cE_J7" +"$y",".7M,B","u/","","=#'jtlT"," &Sb" +"m","&M?31<.dn","_","^",",@q#>","njFq7VV" +"","1","U=$FJz!","~0L\d","4{L55IaX=","j&JJ9(/:W" +"","pVBa&~3","\:9f3>e; ","B","F>","m5M_.XPAva" +")|unsjx%C2","}O$5D" +"n&{kK#$r","1)","","1""6Q2.","","Z-}'R" +"s","nUN@0pj;8","-B0g%FRB","Ku","s)_Y3@4\X?","eQC~" +"It2V* M","k]~,Bu1mc","HliCCg","4a9+$wt%oW","XOz`0eo!/h","HrME" +"3c6wL:","b@O+4k d}","(","{%0d^I;K0","VlCgF1x","U>7NUa" +"UT`t*>","}f5m@""","W:","YYEb*!KFns","-~,<,","pFwko 8[t" +"H/89c'","","Jn[V","Ae&3`("," 6[!","v" +"F5:%YAMMAW","0","KjMRKB","Z","",",e;" +"~K%o^dDpB0","\Q","_FD","b8WQm/=","","MZl3\LW,=" +":D*;","|IgY`",":*D[NGr=A","X~]oe14","","=~,0NX+6" +"5K=#|,uX","9LNs","[FG","cp\&D0&fk*","+&",">#M]T" +"='oA/","","xq!Q;8","qzNL","`duzWU","j" +";;~/.","U:'v||","","","l1a.2","0LW2P" +"Y","n`@!","J>H!2F","rI3>kN|Ac(","33L","z-R:-iQ80" +"y$k\","6wNF'D","6aW[:j<"":","OQ(e","U7[!L","ES>0]~r" +"p!=","0~X2^}&","{OGGdTA)~p","6 gY-","<*b4!cuh9","2" +"%z^e`M/.r?%I","K","^>$","3x$X","!Kz:0O." +"uK40ZtQyY"," t-lzS","","eF","d,`Z^1.$","Ln" +"","I-a&%.W,","wAo","nuS[|p"">#[","","[x#Bz" +"t[FV}8i","","V~7Me","9c+H`ALto","`H*$=Bgo","" +"}wOU\;>9qF","F#N|;","VN775""G","kH5","&-sO1/N ","^" +"T09","mLW8MMJb-)","NF$i^dy*","","^"",dv""|{/&",")JtR,(" +"5ab`[","),@{]kRm","HfaEwLe[","p","[U*M@J5","x" +"X13=T'&","!p:vnLZ+","Q)6p)ga","eD4IJE($ L","Z6o","" +"PX","_v>`Lg","","xB0^|]","YJ_-|","7koAWFYFO" +"t1@Bm","}mZ*Q}S>)Z","","6(r5","oOFDYO7WZ","nG%b\^aOR" +"'vCU""Y","|FRSC%5p",")c&,","tR2","MbJ_J9~fVf","R" +"(]ZMr}","4O<-EVWg(","","@r","","_Jv-!;$]+" +"","TMmwpgqiVt","{&ao%<","","q#ho6X9b","" +"b","","R|5W1q&","4\WP0{","5<&aQi{R","]+gM&m$o" +"IO","bh4'5O)SXr","""eL","^j8?|OO","zuM""",":Fsb" +"lg","{","x=" +"G=|","","s-b$#I",">\+","'9T\W->Q","Ef@" +"*d%;","=X[v[H>@","!","xa","/\","z" +"","C'Wq$]s","7bNW|5","~8wK","G""IS]HuAKf","bR&0" +"","v4j","u)MP)mUS","M","2%+[ ","1~" +"e","n(""$N-YDo","@~T","}q;*Y","EDJ$","P\N" +"!87kz_!,","rCJ3i","Y","9","gEl","}" +"bA/'","*p%yx","MbR^<","rCz%q*fi","~HH*","40AmOg" +"MOf4thzm5","E&'","TdTyX9","y","K9iC{bnV(","n}#=Ws" +"hO",">IZ3q}ziS","Ld+hkl)B'Z","psm)rYB@TC","OgqG`N","" +"lw9s","A,`~","'A?>fz","","]}?y%As","lJDuGI/" +"EdM""t","RZT}bLx{2","^&ek`[L","^]Drbd","wD}","t!\zOD?J%" +"","""%"," Hpo'@nY;P","1$/","nuHW","I9" +"",":\","B}9lv>Nb","{Gmx(L","/ug-a3Y","w*h Dl].A7" +"^l(w","EoO8^","xosm>5","<,kU0b","","?1`j" +"q&}/sKL0","tI9","+_`=","p","i\C^w]r\;" +")gmS)S"",D","KClkeXt","*S5pM6q","U}L`lkK6d>","m*Y(]O","t|&r)0T|" +"_l","KlpL","X","z6ca}f%@","_-2tQru",") " +"1V,L","%RQ&.tF)ku","`pG_qCul=i","GS_zVA","Zms~)/w","k","%","svQgA)","#","P998]Q>","5+#2az)" +"uV24q","Oz<.w_" +"v' q({d;","+njk}","nn|aa","4B","bHSZ%5m@Q6","V)Qs.Ucb" +"I[","U","r{=z""_PD","*Be7-2^68","}Dv_n<3ouV","" +"y","","K","","M5) |C4M",">1*YIeg4" +"{!h^{;","kFUNy","U.i#Hc","e/","","a<~" +" aB!","f&B%6&",",Lz]nPWGe","*;B9Wi[","","4" +"DKNdP@","3eqY","j","k&1","Toz}","""YO" +"$)'",",U","","M41)6>Q","[v^6_UP","P1y;#<" +"j]ItKmw4","uY*[2.","c,,","t;","PH.Dn?1w","yI(" +"#F}58/Qb","(`_%bj","7d,yx","73g n9b","tIwI~","57" +"`FZD);fk","km","a$cagNV8*~","u#>`","L28cl?","K}E85Y" +"k<~'Z9(nL","","N;5@","4Yx+_@u_zj","5N[","hx&'" +"vYr/45)","-{s(","vjh","^%A3!$mp","V+","GxT6>><" +"/w","z","3:8rKE\I","5!","","ZL~8F+ZE[" +"e!?jMw#.aw","|=e;*I~G~","}KA","6pa","","dK164]/w" +"5f","+|H+W","|@hr","A6%|R/","","HL&q)-" +"$(N8-Y","x:.!9VP(","qUp2@","5g_-W)rW-","","wb[w'qr$a" +"Cbuc","1GH6.7kR","bJ0\*","Ah6XW(o","J}S[8A","EJf0TLCW" +"7(m+/Z","v"," yvy}V3","LWU]",":","" +"rmS}]N","=~p-+]93~f","s","YAT","b","HiX&I" +"","A:d","jZ$","Vex","","iKUth@&" +"","rlSrlQ","","bz0q","frx""8","En" +"J",".@W@","~XP","s","WeKNE^rr","" +"XREU+4`joB","g[P~9[K0","E2`RqB=K","""][_W""~jq$","Rh;D","wO!!e(" +"F]v/",";+4(ovGD","eTB*rFq{","$&J`z%>","OVf`g3<","XGgeuV^j4" +"Ch(W","U{W{","NJWI}kC","<","-pKXGl","6LCYo" +"sz].tI","=;-yVdk5","","UNdl""[u","R*|hnn(|","_""&N>3G","Ajhy_^" +"\Spt","Y@a`U^","\5Me","oVqIa?dMB","H5]}","" +"Vci","%'\","$Ok[9VBm","tM]6OR)FH4",">g|J[bA_&","c" +"","s","_'{j)","%i1p~@f3n_" +"Rtl&xxi'&","2","dv8h`"," goL ","g>s3v","B]S4&\})" +"d;}","Ve3{v","#gp&D.2xn ","CQ","^d:+_btUp^","&)3D1T|rLr" +"-}~!%","|>oZ=O<0h","*%<>j52","","","H4(qHjctO" +")I","D[`XbyE[1h","$ZzJP","Uo","F,wfG0?e8","`.i|SdFdO" +"E/","./""UG+OR","pELt(S<,","ES5y[","h0966","\" +"I?","BQP;7~L","&",">`!#:Nz","","B }zNS" +"2-W/","5(y0[[kyO","","vKan^ge-d","97iHn@IS3{","BL=" +"*q,6","y(G,","yp^NFwy=Bfx]@" +"","frl:","|3","|d","^Lm}K","lUEw8>f$?F" +"","Kc","sI","4I8","tb6",":D" +"0\=7rQa","s","o64b>5o_T1","@xqS>","D|GyACR",")eXD**j$yB""%S","|~/",",@","","gLq@]R" +"4>W","$Wm","","b","rrl","/" +"!zp","T^C?","WH$@","S","3,pD7O3$","NWz$ka[3""5" +"`7,:U","""RS","0|&`2.#rX;","n+","Ku{8`j","uS&_" +"M,#-","","+V;ef{#}bN",":/=Z,a6uI","l_de#$","1YdPz,xkeW" +",M","$""\ 6A:T","","m,|","x","xVJ" +"3:haJG4[v","F","jH_gKcB","z[:V4[g#Y","W","n(" +"a#K","","","-mgzOKJwv2","4- #5<`yKr","Ol[UndAVZa" +"~","[/+AmvQdEA","f<#","R[/n","b&~,6]o+7:","|E,A[7ex" +"["," xV","jy&`U@a`h'","F=5T@fzb`4","v_W>c7wpl","T7%r'|S2]a" +"~{k","ZgX0mS","aV",">/Te]<'xsI"," -oeH","*C.WxeO9" +"l+n2CWN5S","F89[Yc"," ","","","^|d" +"PI WJ*","i","5c_Iq|","t","p2%xeR"";","_C5 ^." +")arZly","B[m","e,os","P`~NLO&","U{ 9H(>","po-^$" +"0VZ&=q@)","s@p:","\","","I}}(1!qb","b74r2Q1p" +"\\4@H>*S","RBDR","]/?","",".tUDA6-]!-","o(""AXMcBZ" +"lt","O""Ty&`A","-:","UjXg Q","q3VAM!G","`" +"D3_|\ShUhS","Ak5/1Fti2","t#5ns","i","ijg]2k"," @x>^zh+%" +"Bt3,Em'&8.","=p","I3\y-%d","m!l2>\","*%""""`_9h","&8" +"|dzx0","","kxY o","","w}a1vo","WP9ke1" +"i0{'h0MW:M","Y","H","6tE[;!,@","","`:,U" +"","O!A8","[Y,","lvl","=_&a7yd=","@" +"?[;V|(aeP","D(J","g\J#","x.~3v""CV","Du:$","NJ" +"s/&hsm","\S))$","","0hiCsodx2","%2c[K^^","H%d""~t[>~" +"DuW_&x-h","hjf","zXCvV","1m!C#","?I","K" +"J:}","u>","l|t%FS","u }J9k[eu5","7#zXM~B;In","{|" +"\>'D26OR","wv{w$2- ","vw-E","Y","""P","f[X" +"dykP","K5vi","C)}","KoV@}y(","jFk'","{#LhXAk/" +"(Pu%j","#","sQ\)^d8","w","aE&","+1K" +"Q@{[S","IBGW>hVcD+","(^-8l","A'u>|09E?",";#4=","e,4/" +"XJ^0%Ud2(p",":L>~","{-:b'B","5*'0IQ2(+","7l","0" +"EsRjSf,-4C",":*XhL@f_:","","7","AjK-d","O" +"vT]S","!*<5Qp","S/.sk`?*","+!" +"Y)?i8{a","uG@Q%$M","W`\=WVL'vH","qa""1\A.Orl","|@Cg?/Vk","W+ KG6[!^" +"d=[QB","Q",";9Hq?$9e(F","5#Z`A@zpCS","_3tO,&","]qd" +"j3U","Nu8","&}za97","H]AA","}x1q","'!" +"@d","","","Vf","O^GD_","q" +"p3","tELnilg#","tk","UeBQCQuMa","oV`a4(","V_RI(>P4Y" +"nEw_Tt","","6Kf","ZC>OE5","DV&rdHVK0,","?do" +"n>#i.Hv","8=","U4E)DJ:","&:*huDRSz]","4Z7[\H-94","xaPCz{H" +"g'w>","vF9Gg?m","oO","""(BV,z*","@a!cc/9\}","[9" +"k{=","DYrnMC","eu%I+_*","/DW_Y\y[yx","t","Cla;}TJK;",";IY4" +"~) &","","Cb+g>!0X","f8Fo2{","w_#","" +"","",">","wWV?>zBh^.","vp","f2",":Km}af$","?{_Y&" +"YBNV","JA!to4|k","^","3-Yk6#4","L)","oi=]" +"J7mlZo\_N","M","&[&^p5/0m",";6U-3S","rR\c]]v","F}C>:L" +".DDrM","6f7!","m{Mx{G","~K","BEhB9","ps" +"}4P,8","","fj4G","0$p{E","/YjE*","nQPn" +"^lY","H)T@","",")XH'","@q%j^(;nC","J_GEG>w'","-pDDH6SQq","g,","N0V4","","" +"I5)","hb","-wc_c~i#\C","wFGA","2rvz","((fntfviO-","(","]l" +"AQ;M%>4","G2-RVh","mwp4p","(3tGM1`\","iSHO?&3","Is" +"rQ","q&n7u4)<<","""%j)^|aJ","~h'(3N","","_=" +"dE7,}.","","qbVH","""RYl8V)]","kbX},","'buNq2" +"n0O+P8eg","aL(W@)w1""H","SF_UkkU'T","OK-QAJ'","Ngz/[","xH+" +"!E :/S@v","Joi~G'Bd;" +"","A8NEc9'","1\(","1GvT@{%","^!#3h""0ra",">Ot6}" +"%$@sxb!n0","mA;B$T'R","RXx" +"W;*?c!L","`2\_z/&","P","A.9t4","W]o.","_r&K" +" -TnzMUJ0.","wKi1","","khI(o","rJGy","oi6k`J" +"sW1d3","|zzD",",7","Qx\","2","" +"d",":QhAS^","ELT$WX","5X|;9t|S8","f_Gw.dGJ","P&@t0e" +"dPFzD-PlTTS2","CL?|U;HSgl" +"]EJ_R1H{eP","-C]|l>q","P","3mR|(","00N{-{4V","I," +"nN ","R&:","czgSf","8","~([","yD}z?T" +"/='",">{^}" +"{.b1 /{Ta","?z5V*l&EFa","L07|;`a,p[","qXG-j.+i","{IC!^EZ","" +"xdMO",">",",5=","L",")y3> l","[#E:xdS" +"4kWy(","q,}\","_1i2!=","v^1*;@u{""","V","vjG/rZ}M" +"1:ogQAJ","","%(","m","0WL&?V","$~" +"^$\","a","fls'o","_",""," %Gxy-TQ" +"r#&H","q","j{9I0i(\D","1gfjbCLR.5","/Wx","#""hc1XM" +"~:&Nv1)K1","a}Rq","3SJ$Q.C62t","Dd{RyK7Q","LVK?*8%(BX","opH","EiM/N@&K.N","G!`[j`","","_tB[Q\","^cCECFz" +"j[]hAoY:g","e06Ox","/][{AV9\k","WAz{",".","Xoa" +"906","cY\VzA","R_D@","Wy","!<8tMxLv$r","UL" +"","z_y","",":}","N|gi","Y\Yb$T" +"rSK).^[K","~(8Y^W%d","pk$O{t","m#|;","","/imFpGE","%Evj" +",PU","TI" +"\Y*CkdMO","u,ZT<$",">h""","V4T{ik((W","b|""TE[",":QN4b5" +"=TE|[IE","g","o@","'#Iwh@|","*iI$","6ZG6es" +"w","",":X'tPQ$U-k","Y&>vD","3","M^33" +"6?@32:u","kzfVjL(x3","E0","de.|Z@gXM","5H","Bs&" +"A$Mx>ml^n","@F7%;","7]cs0/\yk","1,","'|E4Aa:IE","so4kQ|pAT" +"\v>;T'I}+""","","m","n","7!X=o*,[s","b.^X" +"7?}8","","]PrI.","E","HfD`l","+GfQ" +"]0Nq","R[cI >S","JlCzjV",".","}>.Q[pu48%","X'PkPf" +"k>)s$M(b"," &UOTtZ)>","OWS","]","&e8","fMqlS" +".>*6|","1_" +"FNzKCZ","Q6|jGO`","h2\d'BvR","CeW","","2>YZhc#P" +"2","DQ""=7w","]L5&","","~","^Z4|}1Al2." +"T+}Anz","vYY|Y@fw","~5x8","P","2l-","3$.S$SE" +"","~y/","glOL(%XJ56kk8","""wQm6x.D;:o","c^O","HdHyB!@74A","=Xxb;K""","o]eg ,c};" +"z*","jd(Y2~" +"J","|=o.))kn","^+?l8-H","Yve*vf88.","ss.+C~&~U","#wrXq'~p" +"V4} hj6#@x","B*oW","@j","=/WqePy,-","6\T)","5*Ta'" +"gRvpk|7","VOuT","Z","","}/KUa@5","M74<.kAe" +"e:B`o %M~","aNIe{)`|\","deZd","U(An","X8Rcy","V0w%xY}V8" +"^yZ49{:","4K","PeXY/<)$","j[F+pB9","^","&'%xo" +"c/8b","n&@_xw>","I/TN","],r:","@t:ze?~","A!qYD-;" +"QU","!}eU7h","5Y","=7","","" +"O$oG","bBZ","","uN *Hk","3","X)" +"","YC/\APy`","1Cu6?!bNj,","O=2 R !,_#","yvQNQ","hpv2G" +"Iw<","tt?V6b","5?d>","&)b0q%h~7",",r}kR}y","" +"]@\m","p","==IYuOq","<2fMt'hNoc","mu!Idz@","c)0^KEAcd#" +"","q}@35VuN","Ud""]1y=m0e","r7-iV",")gWB=}","" +"e=GzB","R3Hr*o","'.)0/","","nVZ","tU'6{{/" +"1+*kkNF2","TfA","60Q'T~X+[","W)?Tl"," Vr""'/s;^9","9#" +")9qv9H","","hZ XU':S#","x","Zy","nvB+" +"\","xFZ+3X","L-?","aFXd_R[v","fuL@NE;","$p" +"w/`|^e","w)","","N W","fE2q:~k","" +"HD7qI]R:","&coY""","(71","",";F","U" +"``w","E","Zyc],","[gG@,9niw","d8eB>5,t]","3k*gC\Q&09" +"<.2","","""^aQI[",".","Z","+L%=""" +">p","hvAdZ$@2z","9EzRQ",".m _*S=l{","x","" +"cOJ;l*v","f","G_Y.oUz","j","","V" +"~SLIP/6=D)","%d&6/P","d""P","@d","","sOK26@w<" +"*_9Wrj/FGJ",">MBsQ~",">-?Sqt'yQ5","-Z","r8W@\B~C","","Y6tN'VMjw","l&nB" +"scult","","%^T@)","3-<","u:x","cltd" +"","KTFNw","+A","2w","Z&*' e(2e","?" +"+ <%K\","9d0g^,'+","S'-xdB","R0sGmEy11",">","?Y98agC" +"j#|{} ",")","&","HAh""!zA","-cWn#a","Dq" +"","rt>b","R",">~6di",")o{dJ3","zNS" +"mn.%U""xwI","=0&1nde[e","l-g$)oy","8Bu`Bm","KrA!Qtx","eB$3" +")o5","@UC9yU","EqtWT","2Ta!{","v@Pdj+[S&R","5}c)\dzy" +"Etza9|","7ASvQy*;","5Tkm","'}mnxT" +"kY[uj!","",";n>Wxi'}","|9SA","5Q","6emu/~" +">h%i""X","t1b","bz yE","}vkn","@p","""Jht" +"","fGt:A","B","*","\]E\yEe3","k" +"'|?oF:4<-X","gLKCefb""+[","","\lk l","","p" +"g!3u""7","t)RVmT","Iu","?sQ]","`,sYZvf=","g&cCz" +">""{P}W%0","*%h""","'#KZdi7}","-","%","" +"7$fn}5","vHyS,L*-X",";","v[","0cWihVf}/L","g5E;A*j3" +"3uk","8""tb*:","_9KK!","Df^{+hB(","5Z,","G7o5=(H" +":","-e","IJ""UVGy8>",",'a","gBsja]ur:","ciZ" +"@%/d2","GHJQ8","!QQjaYXM:","3[GqC","""QUP/_+j[","d" +"N<""mV+wF","","aviq","cp.7",",","N,|" +"","ZzN*X","4OYi&ci%6A","lX1XP","X/JVo[,","v|H`" +"e2(HK","","6M","D","VN:f;|$u|P","kD+)8rF-Yp" +"$X_]0;,","Q|v","=nYd","BSiPgqC","","8x" +"FuJ[_","Cz","#IdY","u`C.or=9*","=@-2'","e" +"?","GdS?h|\Cjt","$/uH","g0","","F" +"G","@_N[`?29","`l?@@yV\","bn_N","L4","i" +"rcqK(","!Cak\x~Y","}","Yv!CK@","=@c(b=","J;" +"b","*gXQ~","","M=","L{^","YKK,7$L" +"g","X{6@&mP","I?#O,m,","mn!O","i2X_,a,j","_02tx?.v" +" a($K0=wsH","|a.lr ","I,`S[","8""C","&","$YZ~,it)31" +"72""RWvO\\","","&]AQ+s_lMX","6u","j6}$+$U)","" +"s7E>gPd@",";","Nvm%IV9ZT1","3h2eD2yv","Fn5","|UmuEQWXs" +"T/]$,>Te","kOzl3/U8]M","Qwfc\3=!,<","X&.","G","Jv+mpb" +"7","j:C1!).","(]~Mlt,g","#2JA:~w","{","5Toqu||t" +"*.@wA&z}18","`Sp2w=A","f{%9|%*!?","qq$Eh:","BkSFS5L","6" +"i.<7#vE&",">Z}n#w~","yO","QR6)0=S","T","T\D6","" +"+","RX6V,*%f1}","v~lcqf5,}j","k+B7+CRF","","_ci" +"D","I3mFk#","'","V;6f","/D",":s(:" +"cKi/Bi-P","U","1p","4sc/*Lu>s<","@!zM2","ZQM-3" +"KFq+*40b&^","hc&D%j","y^?('~/815","","v%BmJ_","s","P2Jg12LT-{" +"*,","1:=C+b>",">~","G>>f'*N|g"," 0P&4","ghtQ1" +"Y","Qu*vMl!r","@)G_$9eY",">C.a. })K'","93jw%MX","1j&kjXN((" +"d:","q","$nuLrp","Ex(%|-'(E","9bPt","lsg" +"R[Lq$QbQ","","p~","=&B","bf[r","fcPUZ" +"&/!AP'of2[","","gK%S`qxEi","a""HQ#tREJm","V9?",")" +"}jpsIFQFV","6,hLe","9QZK","$(Q}","swAH[!","nEgdsX9Uo" +"DI","33","fKG}8","","^c1","9-" +"N?m8!GSJ9","Y?@@17=F","4","Xwdk-ch","!;{","3BI" +"9lfj","tj#%@Y]t","<","#L","mPk ","2EvG" +"Zsg6}5","3\","W$[$D~|h2","03C*3Y]um","/"," eg.*" +"8wN_e\~Ee",";{ad","Fe'~6ZKS_,","y)C`lnmZ\",",fF?","oW\X-" +"j","o@Y","@","LP>%>=PS","!'b-{_Im7B","{o#3d5x0" +"6H!a0,{ I5","J[|Wj""","*7%\As","l:{}$@","L^!jub>U","1]nw]" +"70(Tco","\pHZ^q6w","","FXwjG","BB{`I","|" +"56%Ia0sCe","6mXvd;","I(hP`p]","/2$","|v@`X","AwZ" +"2jyg&","l;pE5r","\#;v","","","]_4," +"c%v3a&","Svyd","I&\?","o,/Dj1v3","BOjBe]"," Yfi6" +"hb*","*jC3bV","9>>JhZ""zz","# M \","w""^\r","9~.pW 7|e" +"d","I5e]H+",",Mt/6eW","r0o1v","( "," m_\""A" +",,^;%{d",">>","oN","flXdqu'2","n>UK","^1MlQ{vm" +"#a=;De","FOpy)G8R+","|*|W6H-","@?mTf{8?","e2g[E`","4qje|","z","2OwQ)","dp" +"61&<%Ig)",")","1","tu#","","AH&TN" +"y&","c)LM??\","e.]N","yv`opl",":",".wT" +"Fc^2x7","8pA","rudMziKX","rlMu@X{{","~6.vH&~-",";*2~U\P" +"[hQED6","eC?kD?;,~p","Z'c","d[",";.","0(L/" +"'l.u{","Q~S","kuXmX","yi10T]E","M p","jMTMC\uV" +"/","NiN","I.)7*","Er ","","" +"","VhWg\","tl","t6{H+v)cQ4","a","6" +"""W `e<3-=","yOgYfMU","J;8","rL16\vp3a","&gwR(","/V&" +"e\","B=o]=","!CxGh8@","V","uY","^l",";FPO;C!","Y" +"","_Z@S+(f=","O_j|tRLA&}",">&zf","J!St~q","+hdhl^" +"(\CK9","M{<:vy)Ku","n$W5""O]",")a60OGrN","{}FJzjxl","LF|\" +":c","G'","S","D-Rb[_zG","MH~SL|)t","""#[O" +"Yv(","1oFv%eYQ","`#p?q""QuU","cwAXB","aZHY","wyg" +"b}j\+","}oiBQ?]","\","","","HEexFaI7" +"zyP0+pa","Fc"".ZR.ZJ$","3Ahbe`q","Bsg>}W'","8EmimWCt6b","Ei]j+" +"g~){ z","","#'S'","{+S","\FRO:M","E" +"5(""","","SJibvzbm!^","`FO","HS3","B" +"sPI 4*","Y9BInacCg","1!","{z","!'P","(P$oiJ" +"+,","r","*","&P q6","q@4B7!Qvk4","t>`]TaEeF" +"GL?E ]a","INJ>#$","Q_d>WN`a,A","VKIHGE\""^e","N-Vo53h?","AJH<" +"|\ RkL","3H","",".KwI","WD","8LR)4V#ed[" +"/a","R3","8rwy","G(x|;~","b^","[" +":k}>`!R0h",">oj3UxK@7A","9'usR.Tgt","jUMUJW.l","UMEa@","i~:" +"N?!^","3dYd4","_?px""{)",")ovLc`{","gQP$g,h?Kz","7Ekg#|%f" +"8","=#J^-","q)SN","D_*S""U","\=nID:","z" +"3}N(`","*=k","9;\oILK]6r","/+7^-",";B=$","@!lXYjlZF" +"r=Ml5","?.8zt","","eOG.=\","","V113" +"g)IfEngk/lf2","{" +"-[f^","8a","f[Hs:'f$[o","@Z_vg|/","o?j","f~f" +"zv","","q^^1X",">3Wa;" +"m$ \x","a2","9t","G","Z?","f60{W.3" +"+""k/=","2","s]Vp[,PAK1","_>xP","td51 'w","@Rs""fMt\" +"","","x@zdtwY#T]","Ngz{>Gu\","sIX?","Qrc?[}F" +"]z4a+","QTH","J0t\I]m","9","Q!R","_2>cVmG1" +"Pe4}A","t::r+U80k","%n|Gp_^qb","pE$Ks6l!~&","tE","Lt7-Wi" +"DV%W{4_f","%`gF!&x]p_","N}\ 0B }>","dMm)ZkwFq","S)","@a4kn-+" +"'#a","W:}G","zsa:>6u^x~","","]D","Ll`v" +">~jFgP|=c","bVK","eu-F23@",")]!^4","&?TJJl","4a*qB`S" +"c_","v3qX","IEx;E","I,po.>","hG{7}'","Pp&l!`"," nJdM","_:-","%1e'1xr""","W~^","eZ]NcSX\","b25?" +"}^C=ANXiDP","[Kh,&#W8","S'EB7zQH2 ","(_R6","{zI4BO","" +"","X","viLPqKpUq@","","%EMD=kZsl6","y%L8Fks4pT" +"+s","/.","","VAF~l","mgie9Mf;;,",")hH" +"D[]y","{|","Pkw[4I8{8","K53;yZ","/","inMJ,n&#R`" +"yQ9""","`m,8QU""N%v","&c}$x}c","o$;z\S","{>fka","@v06}6~" +"","]kBBjz&X$",",F j{Mnw","9i1*8","ebiSz{/z","|?0o""1W" +"uyP8","<'.i[m","QT:k0","","q$hlrSC","" +"+H@1dlA",">qkRH9rZw","@","h1f","hx+T","QVw=Q?bLDW" +";'Gf<","p?","sbd.""","q>]!T4i:","FGcX!-b","JdVe~6^" +"b+^d0","t6ND","5/T'L7","#&","/CIT","d4M" +"O","D8;iE]N","t51ZNd;l","f+~k;d>@Lo","|qf@sFq-f/","0,!63oA" +"eFwd;a","DBRf","BQcFIUm","MoJU'","O*Mn","rcd gq" +"QV/v=+_4N","o7rj?<","LFK/ !j1","*2<0h","4\P0q" +":rG_2""BjJp","4Skm;Lu","6jzpq","","H","[wK(v?Ncd{" +"!","","ElMY}i","aznu&1","0t#P|I","p4Fj" +"R&-","bU>xS(1`K7","Cm\[dJBHc","^hBulws #p","lI","+p&o74x3" +"/","oX9[Ap$","@","B>tob","b]7mzF+|","6." +"","Y6-<`e","EVd%.W,v'a","F ","CnNAUInAG","o(znE" +"","hiS=(#V","c""[(mdOR}","qY",":Y","n%$OS(huUG" +">2V" +"@iv","V2rq[","P^P","Q=TSzT|.","+ztzwmvN","B4JL4" +"Zh,","Ga(j=#RT","BzC)%f\>","AtSH@","s""&\78bHdJ","z=[" +"{H""ij1","lR\H17|p(D","IwEKh|QvD","vUsY","","Y" +":V_J>1^","!{AW","UEFA_","J1","0","D0" +":no","4WMuG","D4xbJR9","+~.sQ","nR@?","6&Al3b" +"S+u","","LY!66lV.","]&d","`hJZ`2" +"lG","J>>[rKNpR2S""","-'R(","1tQ <&<","z+ZzxTY" +"toN<\'0x\n","k7V","Tr""#l,sy","]f,ENi;","vWKY","" +"g((n","","2,(","","Q\8;+7#Mq","1{b)v" +"I4+","","[( se",".D3%QouV;%","4>W!A",">Uw`E,xJv" +"DwR4m?",")L,u5""","6G$3%$HiG","'|yFywU","DjT","[m1_YNjJ" +"1&U@ow!A","x9TI)M6\J","!x&","Jv)?#z/0","mHkE43(#M]","j>'Y" +"l=","BG!","\L'h+PQO","&[6N7>","","M" +"_;KP -Q","D~)z4A4o_w","kl&","/","B^sY",":" +"[',|w)","Jd","/{r0","""|cJH76/[",">Qja^O;","jfX" +"@;k2`;H","3N_(D}E[","G,3]","=","<","eev~>" +"e","~","q]$Ho~","GfXA$V.ukt","'TA{","/KZt)ess" +"0,mZ.}<","","!t>u={Wi""C","#""[!{",".Z{HZ",">rTnP[{" +"D];g8""z&H~","/.#_","1@L1","","R6L8W'","pcFXOGm'/" +".$%vc{","~yxd~G4*1","y","D","kBgBv""?A]'","" +"bF3""!4%O","p:,ZPA*",">'pL<~","m.LuDB","Oy{,`6M~Ng","W=L=H|ZNH<" +"qWQ3~@4=U","","URuEI)v~Ly","&RV","G.ZZ_XA>o5","-bp" +"$]LZP","C","[!/n","","`?[MQ","l{%B?]" +"","ef8M","QYq","@]mUz~","S![","o>x" +"bBr0fIQ","$`!c","^d1Y-I<","h>ywp""","&%","%" +"W4+f.","mNTCn","S*2qWW","7S","?8kjD84@CO",")T:0" +"HCS&TN4=}","-.cTz8c0!",";c4WW'","","i#{X^-?$m","m?D}JJR" +"^3CAzaBv","W","g|IV=`",",_I","%+%q;b","u ?" +" !YW`Smd","","O?","","{WeO:Dl","G.@0om2D" +"ON_" +"xb!,k<","]@>DX~CAv","u'H>z}XrJ","JEyy6JpO)","*RF[%6q9B_","eAOZ*>+Z" +"Cx_-o","q>J2`u","!Gc>AC","GR","b<+J';BWy*","P" +">Uy:'(0)it","o#]oCMH","$dqJKQ)JjW","B@}iKE","B","kL4]*s5" +"w_k""QA<","N,g$eqH","bJ","Ua\2g","p*","wt-s+p" +"t","O@#1WP!","d-Pq","8%=Vf","ik","F_}E1&a]","nBfw=U" +"^_$j!w","","cYkkG","s|wS3Q","x&Dks&","2A" +"?GmVudUd9","N}.+2","%nRDx-+","|Ag7","aQ<*W1d","!`}k}Vk" +"\0_$9{N","WBV^Mr","x-","","+",")nXV$YDH" +" >/H=BN","TH6{q<*","I5$p,H","Hs0kRJ4","Yf<%+","$`w0J" +",C3q{2h""X","?PH76*>\p0","v=N"," Dc;=f\52'4m","cbN--%" +"u5l\","k[ 6=","xh""qA]J","VX7","E2*0tg","~" +";Vvb($%L)","lI 2D","m]kY~+XT/9","m)5","","AKLMgK.m," +"vBMCW","V[","n*P,af","(>","~XBuxh}","j" +"XT","F}UEA","j#~BK","g7C","@l","d/OcT-+M;" +"P|pB_n~}0","L","O","zY]",";""-B","e>.x" +"$hg@FzVuec","H;,Kv","Hw.XKk2OrZ",":+3z8$!","V*4:FkV*","p}hiB9.&[R" +"","Q!#!6P/","Sb`<","|x}]o","TxB%},jrj","fwGncH" +"By3(O:,lZ","@-","$8=""I5FF","s(","|_QfdY?`}","\saa/eFMT" +"UR2`U","I",";VL<+H","5VK",".V-q6"":","CIh""E!," +"fYfm)!","","dY-|jg`Fl""","","Doq","+" +"3_A","lNi\CV","*#0+*TBm2=Tl} +","9=SI^m" +"EiRk6oQBQo","qj?tVv$a","iX","uftdmnN","v.6","PPn*4.EK{" +")f5S","{","B-oa-:?","w3=!AN;?-@","","N<" +"","""l","VlKy","IZ+ ,5N","9'o.a%H","*.'u1""jC" +"0MT3Rz_","()kd","","GF","Cj""ntU8","$UL~xYW1" +"WkwEEC","","""ASSq","","D^[Do","AoSoUnQt","^rn{c]l*","y|2uS","78" +"P0h}^8tl","@0aZgTN","@fl<","Wm*","9pYy(;","@Lc5s$`1gD" +"iQa45[.c{X","0l3nBo_|Vr","TaYJ","z+V","K","l]C[oEaGbF" +"","mvQrj'dC1o","j",":r\6{o~PQ|","","?W=" +"86fo[","{)[z",".O@","","`fw?D8hUAep","47TW#).","+q|-,|","h3uP" +"^E39i~","M$\I5],!N",".eD:4","@5K","~","KTe3l" +"OHk","/n'*rblt)m","[0.'bT","=zjy%t_Nap","kd","J:%)aC" +"rqU}85x>","It^Y=]","Ul-x4b;!kK",":WufT)[97","&Z F&v[TZf","!FT`8Pv,T" +"","wZ]","z2{k)",";**k3F,7(",":_T0~S""",";V&&;34%R" +"C{d5lt~","u>Z","","\","mB0N.","tIW`k=u U" +";biUHorBT","45k1i<-eD","{VrMw","!qM","Vhr!","Y78_Y" +"7Y9{GsBEw>","q=V","9M[EiZfNy","C","s}Wwyq:{h","lg" +"1Fi]","tx;Xhip","{sR","1Jq","","8CBr?<-" +"v.X","b","","","^Lry9",";K@X" +"0\E`e6ID~\","[","7","cv%M","nAQf{W?q","mm>v),TM8" +"","{1_5hB(DQG","?7$ Xm","z6VTG}]*Gr","vzy","G(6bCkD!g2" +". 6tY","","P2","+4&)2sJRD","G##","&{" +"]*TBo","c","L","g1+J:V","","@WZXyy*3e" +"__c*","T{U%VJ8K","p2!i;r","","","Te7u`K" +"8g","rhp","<03V\k1@",".^UAWt7","vva\CqT'C{",">rdq" +"HvGi","k!P-`%m3","Eg'x","2","xp-W9O]h8","$zKf0Ak(" +"RR-rW","sN2","y|$4)b","-b x7+HDJ_","9QTQ&0u","VM" +"jR@8o+.!eH","$gZfMB>$8A","","*C@:\cM","l/{@}^F?v j.r:","h","""","Zso+" +"jczC*@6S","uXYJLEF","*bS*a+wf",")_-B","*}EY-8","-~g/8" +"$",",k","","[0","$t","[g}\kb&" +"","","Kk%qFAaA{q","B}fQ6p$`","p37","n;!jik" +"w 788?Xp","4KHhNNG{","k/4ndZ='!c","BHG5n\\c","'&vs@V3-" +"","}Qwvm^td@",";o;?Jc"," fFSsL","O$${:;q","`8M[O" +"yHsHt_Z","N%aO$","(SsgbVS","5pP>L","Z<_)%j>*}","xSP" +"hQJ(nz-","pvEO" +"h""H4i","Vx#.4eFSP","P%b","g29/ohJ""~","1""W>c>7,Ff" +";JP;lF=","","sk","2pgI","","" +"PNJCZk`B$b","o_rES_{G'","","c; 5f8X@]\","5501hCLwp{","k-|~QTz" +"5|-'3H","(MBjQ","^L!]ELN!","FtK#er:,w","n~d6_w10","~Pv" +"{^h|}DK90\","\S0dCXF@","~Q6yqV4","0~^eD","p.(?f","" +"O`{32q/6>","gr@D","","[^: ~0+d","b6E","5n3dZ" +")","_F",";PXBX~JYz","","VSgfwg);F","]5(>W@ON" +"i'","_zl|Ly","c#0um=K","AYJz","BG",")rd(,k)MYv" +"7","-[eJkQ","g5DV2","6Q7>","5MWw9Am.","3" +"y 3@hOj{","O~V-",":","%ll#","wn]I;1","bE'1gAdTqv" +"M","J0zmzLj&","X-}8(d%q","&/kHn)lC!","","(" +" ^4JD`-","egb"",]Y","@u,G`b","1)|T1rnI""","M7w}J`-)","WJM6" +",UNTcL1J","M>","1","PxdP?/Lm","6(I+D)7Jur","s>?Y;s" +"\W1%TE","ky[f5PQ","j","!UpdZ","}H^m","I" +"%""","s_y;Z","""6vk8","=v07[5","3=gkHmvH",";2FXcoB=" +"Rkg`vQ","g9wm@_" +"3\|1Tn\","/0X","\a4+\vz","","y}","" +"\a5G67'""",")yr",".J*:vr1*ME","b","AwS&Jo","if~2d" +"`3O","L*f""`","9&8z","6A\","FFJ$[$!reS","I2aP f" +"x^Z>*[_ .","2vg*ZX,","U5z]f","\,BLCRZP","FZ@""","u?s7w5W@B" +"amMNm9WUb","]GXr","Sp.","7;h_i! 4","QW.","N1" +"","aj,$Um\5","Xma","=j","|KQ?tg%8","]4:Fn:}fL" +"(6(i+-Q/,j","XW>","s0y/","O9W'C1zw","fc`,n","B","jk#jL9","lM%Na<","!","fbhYVV" +"Av","7,]T$","",".8o6g","-\8","|xsPPd%e" +"$#p8>D&c4G","xd)%mt71L","""Q","JGoXI9z","J" +"Q Q","OY/ ","~!;5Dm","r zv","]yxPpvQ=1","BYY=U" +"w~vY","(m|@Q8",";8im94=.","g","7k'","@" +"Q>2=V","#","~g","(g6at" +"",">dTi\_95r","*sIaw|g*^","SK","*","rv" +"8S4-5D).3R","cnm","]","!O","nH=""+_'+","" +" &G","""P~[Z"," !Z","CyW0 dkajc","D2I#HdCMkN","~G;4" +" 8.s~6V(0y(" +"Pb","Nh5fY*vmU","F2'","<1UjC.""Og","","=" +"5u$","_ywJ5B","G.8","gS mW:7J","","`ihU-\2}" +""," D`n","Bm","P#(@2","M6^f{=}@PE","[`j3F " +"z"," ","iBrsxGF`","fT_5''>zNJ","Pgv=?DYXDG","eiU}dhlk" +"","rxD","Dq!?)oZ1rj"," g_7Y2J","","Zvq7Ca+*" +"cHDEOx/Jw","4S9!vN","#i](lCG]","El7O""$","OLeNUQ;'O6","mvbL1" +"bR8Tc",">c:!m@AT","","~6#:G'G","T'","RaV:E{6r","Le?u{tcR","XjuH1@" +"<$L{r!",";","GST~*T'a'","","}3}`FQ","I","\" +"=Mb","^EKB["," a4","K2p)_@","_9^0","y6`nJb%lI-" +"GmKxKI>B","2y >!lm","tFF","","1","d/Kyg" +":Lh=","(/i6nm|","%L5>","XiFs""","iWj_0D]>A}"," " +"-XBS+","$ooq'A","wQ% .h5","]",")>8l#m/","9" +"j3","z","","cZBb9}","@N9","Z`@%!" +"{","o\_*]L=*%v","xkK'","A",")FfU_SJW{v","VgtO" +":Ppg","LDT","S`jrrI*","UsvG-","","N(jPK]" +"=qxY","iY","tD,4u","BLm4","nXky\B=>","Y" +"","S'v!jtVN~a","","]_\!)F","o4we",":Q" +"|z#","m E~R3^","_pQd(_QXxD","&Bg?y\q_n","1z$H","k" +"IVtq2euva","CzAvYu:/~","%H~X%" +"ZE6Gx","Zf:","@e$","p","]WO4J",".7k-U0Qsia" +"h$$4}d9)*I","(LYf7","lrw,o","B}","tg3Y~MR","F" +"\SMp./?","XN","qt","M","hy","OYl" +"x'OB","n>/WnKEjRa^","Ej=$RJy" +"FOqxm 3a;","0RAOKOC:","{","pL8g7*","[{","Xq" +"-hL","DZT","qn0{rQ:Z^","","$T_yha&Q","eTI@sO>&s" +"gku;x5wO>o","t","`","ilv-urL$f","" +"8RbT7x7!U","x4[7@C*s","yw^ yT","&BA 3@O","u_gEPsg{","p" +"~~75UkG!>","gf>H","^,id","nwua[ JEL}","/\3jp+2])" +"(iD?","MYP_@bd6""","%!(I+1H","","\&cI","SnzW" +"C+C,FzV","4K`-&p`;~","pIXwz3@>8$","1.","aX+Y","HbA.OA,[Ec" +"kfp#yjnNs~",";eT|","vsjh3%p~",">RI|'&0J=","u","_" +"1\ImZd@$v.","UB","[)#0euLp`","<+/ep","&","/Y!6" +".=>;D>NT",":I_LgJ0xL","|z\_b/8s","HPe,","UIohz$hnd","* z" +"0Ukdlv/Pw","3{","","","","uXk^WOG" +"rw`Z;Bb`ev","B|c3V","M4M","pAe_C:","U9au#*Bp>","[4Fm_]t," +";","h@ZV?","","#./5""K~]","",";8Jtxz" +"BBC4@jng`","u8lD G>]48","OZf>6oj","" +"-Z(}8{",";_)n9h#/lo","","gIvZg","X_On#p4","^","Fhg",".","aJz7p" +"Ke","M#jJVuB1Fb","eB>S","F1]","[/","=tCCB$k'W:" +",,4_","fC=00a","ReK+mxC<",";6Eh",")B","RL" +"Ql4:EHi+4","wG]b!`$","~.Bq","t0:$f\E1","","d:M""" +"J","","L)X{#2","%","Dr0Q=","Fw0@o%1z}" +"DcEB*4\H","'H","4;tb[a","G","(","""4 c:<5p" +"Jp@~_eb","Deg","uXv","","k","6JL-","DU)N?4IGQ" +"AS`q","V+DgaCsEuc","#mMs","","iJkBL",".Q|t O" +">SE","w","[m?","Lkx+uqS""+Z","jEl","*UXJkG]c" +"Xx3","h!9UUKGH,F","","_Isxv)^DD","Z","" +"&UY","[*S","g:`7Dnuwj@","26BfG8{","o%>2eT%h|?","/ue_a9" +"fg\@g,ey","j]%w","dLZ9IVtKbz","Wa/","&o7?!QK%","uDf" +"^wl0(3"," &f>`lwIL ","0>lt9Hg""q","al","","47!k" +"v0","Ksf(m$","\",")","","QU7mR?" +"$5PbDf,i","","?gQb)","V'4)","%0|)fJ^Yx","@+l?0`B}k" +"L=L7","WR#@,\e_","","b","%_.{_$","d" +"6-?Pl.","^P.om""XB^","CVk","AuOs(U h$","^S]9n.3+","[""+" +"I%D/\S*","x#f1%","Qo","FCQjLg=F9","r_iYR?]]N","" +"23Lw3m","qvOa8Ykh","7d{)","","WaHk$","{8L3r]$hy6" +"0h","$1^*q",",!N","Vo~@qe","$-%V)O%","pKuKG%5z.B","%hK:.|" +"[JFR}[mtR'","5$=>oG<","5#Pnk*","x8F\","X%/P\<7dyK","vT?50" +"ulb[Di#mi","a?rd9 i","","j90QD","0j","7c6EH" +"?WE)t4/","Qp,rvIi","H@ybC@V]#","!=",";Jzy)tX","Va" +"t2vrM]",">7f","[B ","gdhbhk","fs=g>`XJ~B","F!`GS","`x[t.t$XY","W;@R~oA7","7eP","StcV/w2C" +"{ksWf8X","L1?","w|Nq","`cGF3Nb\p9","QJ|","K#c$" +"tk]F","dZv'Te9","dkDelv","H3ewbm=}?","","):)" +"AU'i-^v7Qo","2F]2<0{""fa","dO88E7JC","?ep8h""X,","\P76P*2IZS","6." +"k","EzR;3:YRj","gCg","T1k-Nz1","fA-zc","t" +".WJ1+Ro=De","g8e(+","_eJoCv%0la","))""`<)'m+","n_1(","|" +"pgmjK(&;}4","","8?>Kd","sY","","EX(~VIHb&" +"E/4Y6","#%iu]","q!""P'H","o;9","%Kr>6&asm~","q" +"%@S#B{2E<","","?8$0o,8!s"," %o}j J}K","yE+j-","" +"f%(","<`4","g","}c","u>ik)-Q","\?mu" +"\","H","$","WuZg`:d`*A","Y|Q","Q|(P" +"l","N/","YK[&>^`ZC?","rK%MO""0'M","|ZcN|1WS","#\67" +"ds#1M@|","IJzR6F","-t)a","","Etq^","g!" +"J1wK)9G ","H&,?","&S(dJ'{","}z#DC","*T","" +"z Mx&p.|K|","@)#E}}","aC7T","X <,Pm,","8^uTz|o","R" +"(/dW9","","6","N5D`9y","","" +"","]Lc;$$1X]T","C}?mt![t1","NWp\j.","}m\","j-=IYK9/" +"xc#OKD2O2","lyXWx@x",".T\+"," :","0u","'$4" +"jAlzegNuX","+`lqS$!NOv-","^gi1A3","c^GD^:I%G" +"+f87zuY~","\v""","&","S$","JLN#","oP[NpwiQQ" +",=:lri","q0","&mVB&1","f`^c+","IN*+ ]mgJ","]Q3mBc/Ih" +"lUFwZQFYL","|j=T67","R%/Sy","gM@x","^1b, VTi$","" +"y~,","","yfAn]OTpH","7Yz1c?5CS","","=33te]w" +"M(",")~qN","/y-n2yM","","hIcIY~UX.r","LcX3RQz" +"0","xVJkO/'_(?",".[ZT+tUYx","q|""B\O","'V~9Wx1\",".e&&" +"","g8[q}B","","lKhQ$f!p","t)_sK(W","y" +"i:","aFhK95","n]u043a=H","[R0U/h;","f""","`Sc" +"","HA","P>)R","Gr","s","bU4xh""ee5" +"fn8","&suh","|$#o?s0X)","?","!E[V","Jq7""" +"T","gzvDhD","fcjimbwEc","<%0@S-fm#9","8I*}WSQa$","ExQN:7@" +"J","TYJ","","","-%f6 (z!7S","O{EaN" +"so","tH","QvQ4\%f135" +">So;u+9","=#)>(P","h\{TciLlA",">-","4r`Z`_F","-U@E>W~" +"I|","^*MI","x!""v`","=C|""A","TPBJK","^$p_" +".K9j l","i>","KLOR!{iwU","","p","oJ6M38?Xz<" +"1o","AC~E~){=$6","C+%%%RWE.","L A4EQuA","","fb#""'" +"\Uz!","V!.>(&8u>","(1t(*","Egt#B","pd/","dX" +"D&IP7.i"," jGOZ :my","\","","!G2.8`","tSf@" +".","@WP\" +""," ","^ds","C","NM""&","" +"f\>H*l^","3Z_i","*HZDhE","=O2GCEzPS%","5j#4S" +"}'84w","M","i7;/p","T\xbvh(Zvy","m:}j3m","0t:U" +"aFT6","e@N=>[","e","yw{d%zVO}","e@n","MosC" +"Z3>H{%k]","5$j",",Su#fb6_U","eIZX","KH\Q: G)","q4|1UA","J$/Rt$4Q1r","YR~6t;nh.","#)bCw","","l%Ou_Nay" +"Kd*\y,^EL]","0#m&ulCk##","C)`^\y1"," `n""Zr","","Gv&A:/#b*5" +">#KI","cF(0T9""<","$`E^p6$","<","#?","m&oxl*X" +"Q,","RvXUe_|*oP","[(MpJY>","lAa G,K","V)r+","T" +".mIQF.w4","qkP&tVq","L&","HT1JD, ","L2(Gh","rAM32T[s>x" +">","","vZ/[P\6","""n<_,B","","G" +"=","","vKiynaC}","","'go#eVf.u;","!(5O" +"","","b","H~+7Bt6v","\,BXVP+","iT^wB" +"j","!K79","f4DGj(e(",")2 V4WV","wxU#O,>0CeQ)","|Cr'of*","EYGd","z*UoM" +"]]\","E70<","*Vk-)]rsa^","","y?/qYr0","W_y$V" +"5V~X)","g9","Bo;#)","(#&^=ZA","_+d.=LT&,","/" +".y1Gtj","K5A%","\,V>wQj-_c","{x6d","_o","O&" +"D",",[<5","6?","d","+ocf:ay`","*1!3Jb","-","-AMVOE","aY;","`k'%k3S" +".Eeqz;&1","!","",""";ll*","b=YeL*","&" +"[sd?O","^/SIk+","","1L;'XJIDJ"," &v=:1@-Pr","/e61'+9&w" +"dc<(HA;}3>","{_M" +"^~r[FD+","1K","2OvR","@9","?({)i","2?ApV]:h]#" +"t","n]k+","vQv6","p","7+*P|\'","yzL" +"PqnhWiHv|","Dekm","I=","7",";fDW +","w*'T" +"r.A*Ox?Z","`c","3","5:wi\CCCx","J)","! k|~O\s&" +"R!Fy/%l","L_]g9","+","","","lfc5%6%" +"Lxl,n","^t","BBgoMVi!","\@*omRr_","G?","" +"V`?","7PW","i=",">b+D","LX" +"-ZB","{c}8j","oR-*\ylE+",")~rK4}b=","NAD=fT0","VK]" +"G","<","rHso{>","","SaiZc","@" +".T","9p5hU#YP?","w}}R?'e","#}","1.~Qz:","njV" +"\+@}qd'K","1/VGUy'97c","c5a`mH}n","$V_i&","/Ko","x/S}" +"""hfNX>3","n$","p2q%r","f","","" +"""}`&(","?-P}p:N""76","","BpdrX8","2slLCRs`*","VV4HK>|{B)" +"f7","azN\JZH)","t})1)Q8-aW","Ac&","_k@/4[W]q","s:4vUc" +"4_T","^Y0R%","GWETK",")3","*,9PHsEw","3vcB" +"9","MPFmx5A","-$@""`&bY +","?""","L%","((HX" +"9Moc",".dJ","W","yi~k",",9","{nQ" +"jQz:1<@mF)","4E\0i/v","","Kb<","W~ XK/","*\0" +">'XO+#h","-~","qPBJJCN",":W ","z)J","\032y`Bt" +"OB(8MquC","6W{@~^U","<;=Z3-.j","6","reV","" +"53i#0|&[","=ke6u","@i&xsw>","eF;d","eV8h2+|*","FSXt" +"1534*}","7OwHQ0" +"Z}J$y54\*","","7hg(gHYI","{","?D-)",":1n]E" +"c","pnP","'*nz","Xa}D)q9L","7DL","i" +"2$#p`5H!dx","","9F2|","3u","Ccik<\'","Zn&)|l1y)" +"B.~1q} H","MaT8qa'+","N%N)","$cW3""","*Jdd","1-@!nX<-" +"2DPCA':.Ih",")","'U*N>\g(","u_d6^eT","V","" +"xm12.kj","""H'lk","c/L",")|@0","LP.8P#|>","=R4P|" +"","+s","e,T7|HX|tZ",".I"")(:e","",")u-","p+kd:(ES" +"iPx","W",",","059F=a;","Z,h\c)xCS1","" +">TZd","i$b",")_Xm[","e1+'aE:3","d1m%}","?-swZR" +"A>b,RB\C" +"na","","","3k} &-","g{?Otjt","C]'" +"""er!ts","xx","^p","+*c-?H" +"HS]9'"," 8ayV}6""","@'^w#!/","A",">I"" ","%" +"`Rz(n ","./v|","YZP'R]'","hId^$i","Ir","qQ)(" +"']k+}(2VT","a-","f&)e.,","ukO'",",r`ilIjhv7","LR" +")}","d1ts%)ya_","H","Kc","#9Kn#+","h[*=""g;z~y" +"j","r","Ifa]i}Ih","e","","x][0-H]" +"@~H))=r","x]4&1pgD","}.","%{a89N{","~{#+<``","wq" +"1J","M_1}&w>s","{^e,usXNy@","N.ItrH","Sk2%r","_m22me27" +"G%xDjw4{","","%3jx","4~S|","b[gX=XRHOj","(m" +"sB","/E~Pl&A","!","K",";h!aqS+?[F","" +"d_W[=\T","Zo","H`S<","E",".6DT45n0?*","{:f \M" +"!e&`)n\?",":","ZGGCo?&|A","ubd:","","^te}P`1)fu" +"#1m","Y?*%.s$)!i","G","O""Y'b]p" +"aTe~o","f;a@_Ku4","Y[L#A","i6|e","lXM","W$J\:z6" +"36KG#","rX7\","","zM7","",")|]*nHuHDn" +"b)]","@<","I(tNADDwi`","2@sN","&4","Po0" +"nogD'S","","( r!j","33u*(<","?% XWDC[","u""X2h" +"""9AF","kk9m","","","Hosf9>RQ","" +"fo9s","","Ehm#m","A0,{w~n","=|","W[#Xv;^" +"DhDAd0W","","YU+A|/E","zfYD63","AVw(A}S;-U","`(8=" +"","b$r%>","`uG rv","j0.UQM+t","R5c:D.9>>!","#i!7~" +"JJ","X\9^%9J'","~NjY?p+B:9","j","b","-" +"~>K c""O%<","pUW","p/k~}I@Wq","8ztcxiO","s6&;^","#shZ" +"DB6","m","","p_`lCn{","wUNd%3.A","*.NtX-%" +"","s","?CE.","LS+","r'SQ\)","D\kG%P{wr" +"K5&v~""`pS>","8](}+b","""_","",";G","Kbxn.$d"," @0","ehml<","oT7" +"_","5gbAv')$-(","U:21uG-","YC^c","","b!O,^" +"ioUP-;gl","h2`*","nXZ","t|fWgds",".B*","vj." +"]abR:T",";[ae^B'UH","X|""b0q","&t","*","AuF0" +"Rjau8ul&","f","P8","-IU^Y)ZQ]D","5V$_UF^vo0","up" +"","?D[kd%C\K","f","xR","=","o@@6" +"c:Q-6R","m.K8w I","OgX","_Jf~-","F8KGHN","t)L" +"}C??2","r965oHDNz1"," $~","t","qhdH<<","z)gC\" +"FM&]""25","<9(N[#E<}","","'}w.!_U&","[H2Vl$gI""w","F" +"`J+^l\gy","?QiPoL6E","$jj.PL","'a(N}M4","`Q%O8","$UC@Qz" +"ww0]]","MuAX+N>",".i8m-","&=%^z/","6C/kTP.Y","" +"24""","","c%+Rs93Fu8","J:iNcX","BQ*RN" +">8|Jt$","L}","WF/xC/kLH-","55;Vr","yE=0'" +"","","MQs?h","","wO(D,;;","kCs" +"MH","d G%","U","Qg)YB+>>k8","F|2sxVmI9x","I83" +"qwG-ZboHlY","LWjmr+""","(N*","7k6 91","wNP}o@a","eX2f" +"XU[+JRFPX","","O","","8Bg","MUF\'L('" +"J*6L38s<[8","JfI`Mf~T","zH","+","~RGoZ","z_Q" +"~5 e","Uo","V5fAF0-","","@a/q","2" +"x,M*>CiM*+","J~r7*D g","W.Ux","","","_UFyZ"",[:" +"^W8F]#:c",">6","gZHx,+_a/]","Gv","sr","}" +"9'B*:Y","DWDfkr*v1y","E@","P@","","w5*$mR@" +"{""-\([G},","F;gM=0:","[)","+9t","Bs""8(2~","uQgt" +"d#","YZe!vB;?""=","q","L9h\bgDE#r","/J","fFc:AYA3_" +";U,:z)@","","l>^R_S0]",". Rp=","[.R","|[r+Uq{OC" +"N54x","w:7NbeRkO","M?}Ry591","#|@z","'IyWG","" +"h=02i<","7W#es~","","Uh","K\e",":D1" +"7dRh$","","m","VB.$e.D""""","v_",":;T9`7" +"]y){""[t","}.%)/m}","|TD=pZLz","H'|9l","[v_Ew$=","i(HlZOM" +"","=J","@am",".ZIZoUYI7","W@>6OL","~76uy6" +"","`0&3$n*T",".}","oXB*b9FH","R^6"," E[R's" +";^kkE5aup""","jLIj8","N`53;3","_d(/?;/n","h&y<","t" +"kNk;Ke5","tw\&zmY>,","{x)+K#x{jI","","3$b,d#^)","" +"Gp=1(TJ","f","(0u|#u7t't","","*:{<","" +"<","|^R|LYA","*pb" +"#Y","NlW1`","ezT^Jnm","oX@m!:","6","_? =lT" +"",">Rf*u?[","`kzNE.",",KD&,{[jH","V<~.","id84AE " +"lbn","[~11ZBK","8[:yUd","9cSn","","" +"^qgqcc","%:OK$c""","Zm6]SE uuR","","<,P_K?gb,r","AY(lB6" +"RSpH","\|U!:r","{lv)","qxVzjwG(r","X9Kr%g'","" +"d<:","fE4",".Tsv/J>lvN","","#2l~_S.","J9oH" +"OiI0$Ps4",",","Z^7oe","X}B{B~","<8ho.uw#","l !~" +"1V'","","9DDU","GgnZ]T","zECUL[Q","hobJh!h\r7" +"P)L","Y_@eaV|","P:y`#","z","#',vQj!s","kZ@""*eig" +"%%{^~RaO","","R&??QE","=","dT 19e","" +"(j","s6vZr","G","j?CuNK>L<","rHaC2&\#","pZPttv[" +"<%Pplf","s~KZm","-/C""","Fz1k1Y60","K0$","QmEkIPG" +"","IW?",";Ub1ZVWo0","F/1NP#fFC","9`zW>Lc.w","n8""Qq" +"4:?","","j","D+","kyPd#oHVdt","" +"","#","S}=PCb","","oj{C;d1h:","?QV}Fyu%9\" +"wD","",")tjb/+I+","^""O","","XFHQ" +"vK","\","G/",";`/.NU","{z@e""4A","VtA4spU" +"c7,DK05",""," :fpef","LD#/F#R(_D","aVn?_k ","t +eq4Tg8" +"v|8x","s0G|G","8C","<\Q","R0t","%R""C[" +"T_1<","""$E18g3","E?<","6e_x,","|e{9!fL%""","z" +"B","mM\",";oz-\E6d",")","w:8?]N","TNoQ?HOx`" +"prWQ2edO(","@P_=\","",".K@`!hgm$O","Pwn","#]#6" +"","a,@?#=s5\","5eO",")z","'B[9","}'N","ZipE" +"!'.f",">f>\_'Tg","W?EW","h","(p_u:}t","6vYcvmT4P@" +"0","","X","pkZh ","fuem|2w&","|LXXsM}-iG" +"","OR","N0,%vYTx2","7lr9E=c#^}","?i8Et#^<","h2" +"99","qo'*YR","{U3:f","F","x]EJX4","R""Yjuk8U" +",`*[o]UA","G5,tTLZM","","(tQ3>hRT:","d}0M","2WhW[\L" +"5","v4ew","%u{3","fF#ay","u$[","$Nv4S(Ip16" +"oR6WW'y?_","+czdXh)`Fn","CbWLwO","k;.zHc2U#","1)MP3P:yN5","t/+)" +"V53~m""Jc","h3Ld[k","o3OUr0u a1","Z/kzq","B""","&$Q{ .#6FD" +"O",".0y?YQv^n","Of>\Y7~B",")*h72"," ","E3" +"a,V}""x","h(XYXKwEP ","P","I/yDSK61C","Wf,Q84n","" +"ICCY,l4#]","1=^bd*","t44[pF","O0:\n","","7e@1<;^_d,:d'o","+o?""uUt:23" +"x3X<6","shK:t","H&9K","Inud[","9B","t" +"z5","qb%fZ6z[W,","|s","","X","RZH\$P3/G" +"%@0GqM","|?}:mh","MlVt","Zq%U","+i'6","+1mM?J" +"A$&",";B:h%","S6","7J$wz","Z","U}" +"k","M{%U-,,","Fh7_K8Wpk",")hQ9u23t","qX3#0eoq~","h?_EptSeiv" +";Kz3" +"h~xRIUu>Hl","g,","x7I,(-""rs","&.","C3|K}2t6","w[@sG$I)%n" +"",""";[N66I","tJ]T9","RbaC{","_55_z@sA#M","1-UnR3c" +":re""28A_-}","A","~-)S","0","JZQg$\E.","$SWi" +"=3NxWzs","{qJ0[,C" +"7Xm|#","","Md#%,EXZJ","&Y!(AH(","{Etq(|","o_" +")%/yN","K8","|ph9V","J4*","","" +"0!","n","Fh","<-4d}Ao8","t[dx,","_y" +"%d_S!WX-z","""I9&}0;B","-7bHs3i6QG","q/K}[iI4U","1Pqpu",">A;8UR" +"8","m.4J","","!","??FqZ.2H_" +"N]PSgH","|Qf \#M4#","m,c","',v`AY","c","'kqc14EUH|" +"|'","GJ5/)a!N","H","XDjj(x","/C[\jl]^","JKgp,UPF" +"kxxi_A",":f","|hb{u","","-rh3~w)@G","=G" +"a[8","T1","1bqV41","[>=bA","02(" +"qQ`?",":T4M'*","l[j[w]#Df","","kw~E?e]Af)","ew~o x" +"6Abb","Kt<^N`,"," P~","ZrQW*","aoX~NI""","}" +"*tws","~SUh.","OKpR&T<:","M+wo}|1/","","(VN}j" +"`_.Z","5}KX;""ch""","`mKKtX#;","K}K|xiHk?","trVy^tAM^W","","Gc-(" +"[b9^97%-|","lb9<:J*|","J","e","","\yxp3dTh~" +"gel4=","y$","+gqzP, i" +"~TrZ MZ[w","{bQaO@l$}","c ","e",";'IB08L<0","dm83LY3" +"tXv","8t90ODWV","(@4?U+}","h","rE","i>c CwpPM" +"BCe]Kplg","Ge~`3;","S0&kZkPw4U","Id@6lyw ND","`8,Q(9vEb","Z\}_3[|-_{" +"OPC-ov","R","2.c%1f9","%}c}srAJc3","(u""2gWUIc-","0MU/v" +"g,y""mG I","8*3Y*/_","{6F~","@","9R7P%","NUOc" +"U""l<+O%*","tX6(^9HC2","&Kb[","](41~9]cW","v'.sX8O","" +"kl","mn","N9","%*?H@0w","p-C+ci:h:V","" +"WUb","","K{8Xs,","}/7C=","ybRC`","+<#~>$52l","U!2","" +"=tRxwd","","H%*tXfM+[","w\=^7",":w","Min[*8" +"RwFFL0@","NGzjX","6=ml'#O.fU","*] A0hm","1(","*eQXu" +"zl5^$,","__(faA`tT+","`e<\u7","Odd*TmpU8","M","_*""[5aZ6E" +"y+FQiy""","|C/w~d+","","U","(Kb\u","db,@" +"s","9","","R'ksKw8V","wJ:2B7","X@Kzh{" +"2","t[0Jg+io","rlMzm","pslFfg0","9P\J'","~d^""2" +"#0","`","p_MvZzd-","Si","1UhGDN64@F","T%nizDfy" +"xg","PK3:N","rla2(r","Sp ","BY""","FY!~/" +":Hu","P","gYmIGX%","+ehAD","","MpW?q_j" +"&O/!,e","c[tn","aY/5&5Is{","H>h","yrI`#","H6K7" +"3K$W\:%!","&n]|:0^/","aj.","C?5!2(","QO","o6+\G" +";Moc",">=-iF=K","`eA[U[9L","","5S:9:r""","fT}J*06""h}" +"j","*","WP6y^/Jn>*","OS[l8","]<","{Z\Y8d" +"QS","n0tVf","HVa~e","Pun=8#j2<","E-T^=p7","" +"w","^","Z","u.","4J;4+vW!S","9" +"aG","s>sT!SU`","s7i43","@8QR50^CYTX@u","rg" +"$bGY[)5OQ","kHG\B*i",",U&f[GU\Eu","rn3j+8aCR","w\=t#4L;","" +"7^So","`KBSSu8","A krj:k0FB","{","B{g~4&Q","~8Wo4","}I","}","@SgM" +"!5 P@ p^-b","#tKl9","4&","KYYE","\K)PsgY!","q},pFW$E" +"@AB#Y","/GFo","O7t9j^&XDE","mP;C.=W","-p96WVdwRp","oIo'A~#" +"ZGJs","$H?p.R","Fl=*XWGpIP","g|+Q4%x","PSaj","QeMky:3R+" +"e|Qv9rs?D,","|+c","mL*5ZgW&P","K2Q?I^G}","!<","b" +"#;1@G:rM2","","|h+uwN","})bLT","P 8yqc@9","''" +"^A%KXE~nz}","MSop7F8","LKNkYF","]SpTHYF~I","","q/e7;Yw{O" +"&C>""",":!",",e","C;*g~","a","7dJKU)dE&" +"AJ^MCPZ#A","^z","'D","yeEc%yq[g",";1i","y=O-|0Wu" +">V4W","","o%48wC","0_, +~s","",";^GI&","\E~+;|<","??V4%gasz","9z]","u] g3O" +"bcG","JrQzi)}x1","AFf2h","RUxC2","pRa/i1w6]H","" +"$}%?y%",")HpC","N$BR)","h~2NG)8a","+s?-;","f`" +"","4@T=~cC","","fI","c","+]T)vbg" +"B13F@","9C","+52","V:AN cgc~","5'pt]9cXs?","M:R!4V'VT\" +"G..XUS","LWdJS,7,$O","-: #F3o*","tS{i","gqE@o`",":;&,|L! " +"}O aB""","pS`'","0oR5","+-5Mi\8|<""","oA_^y^I[","u|z""" +"gWv","x0>U","","+","x","7}mBmY5Cji" +"Y%*9 YsrJ","kk/$]M2k+n",".LA","0Bi03U""{","aI`6tc0z,=","w&","RPC}:2DM","VZM:V;2aWG" +"~|(Q.5","prY","=>S`]3:oC","Ea1","T@lp2R""Z","C]Se@" +"","j?0 .","qXk?Q^>*"," XNP8","","B!~$jG9Y*t" +"gnO+y3[","QwWawS2H83","7UZ*B;g(2","S_oylo","gjF","le",",5CG9/Pv","M;Clqe" +":#\","bX~q;'l/w","IA)t|"," Jk|ZR%64I","},REL7","a!`naR5N'","cuP`>'""","9mi_=!Zx&","v8BzApTzu""","ZcKt","2{'I""(" +"pE\$l",">{m","V3*","","8R3X.!-gK~","p?ojc" +"5,dlP$","ov","Ta","I-Pz","^Fn""dZ|Zr4","F4SkH87" +"[DF9T{","0w\i/u","Spi8z\B-U+","59{a",")*x_TByJ@","stHZ" +"","45##TM8qs_","J8T:86eR",">hO","'s|",":TJr","rE#x," +"9t*KB","$","","F]~~_","sK=Bjn>" +"|","}","+{~~4GOnrD","Fv4""}",";u?nOmB\6&","" +"","x","Irr_.Cl!","d3 5N^","Z(=","I" +"s3",",>","GTxocT'","","&&a8NV","3#9_{" +"oK","AatU","#",",#24","8","l-+k`M!" +"o,^k;PdU\","C%)","IE]}jm","\&fho""M:m","ackx","K.b'@" +"",".\y5)^E","hoXh","&hR)lR?d#i","{0bG=:OO/","Et" +"o*EP=""y:-","(/5VbK","P?4%+v","p#s","%z[- xOxLs",".;""XyQBC0" +"Vw>C9x>>","Ga","Eb","DhO]","apAP","/uWkaFu" +"D3a1U6oE>","!\sKdBoA2","VR=","h]b:O6","$","S]&K" +"oW""K5?C9","","_","i3oor8-","Xe5acgv","kB6AP4Sv" +"u/D66*Q","s",">x6","L6qtiZ|77","q","u>^y,|_Cs:" +"C3Y","Ff/wII<){","\!A$5~yp","j^d","/>&:dIuNlM",";]8^nMN" +"","P","U~i2\E*w(","hJb 9i","Ds""D","" +"j#{!wo#",".jQ+","yU_ny\3L'T","jBk\","<","BY}:@4" +"","Macm;=hVl<","13rB%Y1","$`","","WKHRPT;fA","EGE_#A","""" +"S2","dMuJ","t t*r[K9","rSDc?&K=u9","DP*","" +"81","","","V_","""p>>6ZXG","A,`^E]K-""&" +"M04JE@o","CTET","zCk)_q","FMpG/\X^","J.YO","N|`" +"XTSkM","HHDT`;","","%1{k]}?","""b=","/ID.v" +"R","","Vm","","qbKULxV" +";qNbq|eqw3","0\o,""eVe[g","vKn\VPp:","=)j|D!z* ","9DmUUl)zf","$^!?" +"<[/" +"|Jb u","!","d^w.","A;g","!R)Xk;","lcF@OX%" +"hB!7","\f0}","tQ;O2a=""","!:}'@o*P?-","Lxq2i","!{","6" +"","3b=3;9c","KYSFO!@>B]",";2V$Zwye","^L>Qn7D","'FE" +"ui""|\","2w","m-5E;K`sdT","I,.s/I","|""nvp","(Y68%" +"8*1ufFM","9{AN ","1LML","","/e|Kj~K2 ","f]hU^" +"X?'|h?D}R{","1;^kx?","","-\","d\wh","`A" +"p""0FPO","6","Y#04%P=4","QZ!r","xZ~E2","b@" +"",":(A5]r~.","1","A>{","tku%%6l","hVOh?4z`r" +"529E=~","P","w}EEc","q7!aVh|2&","","","d.L%" +"y-I'","}DM","x",")d25=Ct=a","(}r(aDiOhg","Lph9K`~0" +".rJD%}","n%)-;76Y","at","u@H'WcG55[","T=DJj4LF9","-J(,U-xV" +"IGn.","H7Z_","4q[&{P""UKm","5=eKS",".{]","1P\Y" +"`E9D","",":","aaP%p@","63'Cc,","8PS" +"]*0VA-SBRD","""a-~i.{FwW","}T(7hu]Z","K_J)qL'","Ca*o""mQM","kPd" +"EZD","Y~",",\BJ .J4","1,q)c","","iY""y6w*#" +"XRj)}YN%","Rq9/","L7SG;6S+T","1)5'{}FPC",";&>?X"""," \" +"5S{*1bF","Of@x=mv}","N|l,9@k-qV","p&OTv=xi\","J*","dhEgs" +"7uLg>s|","5Q4x","r","%z{O","","IG%" +"^","tsI""R}6_J","&-49G[A","_k$(u@\","hzsDI","+yV6P" +"]Y","\iwci:8","H1eJ'YdP0","(uy!*WP@","\'e","""axFFr)7.&" +"0N#02UAe~f","6","d","~ zPu%$L","Rw!nc@~c"," 8pHCf" +"ej-6","N7","2s3tE,","76`ir_8nUu","{pO/rH","" +"|4\@/@n_q","cov","09.*y","o{KG,f;XfG","C{L+Gj*h}","Pw>" +"aZ>gxL;A","PMP94xo","ho7%&#XGW?","I2l","_~@" +"[#THPA@8","j[""","#+}?27v","4`M}1|G",",","?KX" +"8)v\!B~""","4@L",""":gWn"",S","!zzW5q>h$[","","ifUqg" +"!ea","d^'t8=$YLB","$~s","","a$d*1"," 3fM2pq2F\" +"|","awbAJp6%/m","XF","y","*1AXL%VaO","*]@8=R2" +"f'/x*Tz:","BGF8A","`","Ebqk","A;p","o2c)" +"m2y^=:l5","P","]vW","p!","","=?:R~\" +"#;JE4\jOfy","d:E~6<-T^=M40(","`j\","6/L,)}C!","=+r","T;&","!+Q%<" +"Z","!5","fhX6v}3QN",":jm$ff}Ja","'","9[Zf" +"J?e}-","7J1","","\^a",";nIn","" +"4_","FI|im()}","Qn","l))s]<_","&#","Ne$" +"9X./Tl+.*{","&","ori","m)","f:JZ","G6Y&" +"`x)","r=p;""oUw*J","`g","t?|RH","Ef~]HZ:","B=?J7-07" +"/D5}inwij","PGJo mM@t","AU""b6E>-rN","VBGY ivC","BG,r?uVT","p!jn","5" +"X@%qxj4","!?S^bftN",";o7M. t","%1",">Gyd","o" +"V#CL5H","","S7]XF:" +"q}Qx3|ei","+8s)a1\","Z","M(#>nrx>Kl","62IX>$","|Uw+","%enx3fhbT","39zf:" +"0pieC","1C","#e","sFmUE","9","0S|ilar" +"`9K6","OBG\Ir&Fx","#","T,","L~n`T^_","<{k,qdw" +"veC-TrqVK1","C_bZj{BYMe","Q$zE","",". ","" +"Qp;R%","}.B>%#","2Z(,8B0Xj[","\v)","_","k?Ntippq?" +"!","6n""-~>","t66>k",")tv9){i(i9"," f","\AH6oG{" +"5|8","`",":&f9","GeM{R92""e",";wU-","!}" +"=HP7BHD","Zp","oM3K{B&4tz","FhX4.kPgYz","D==bFB+%G%","|W&>Iqsv/" +"_R>R2c=~S","7G/#","NF'q%te","hJL$NG","TV","vi;" +"n","]:(UzIms","3H}tx","82","H","9O`*E" +"He~%vP""","a#","vFFg((a>p","L_b7_7x7M`","`^ #i_[Y","*ts{T%mu" +"K^J1l","4",":P>rI","}6~_AhaBc","f","Atu" +"z+",".2XW^","","]H%x","|Ue","vqw=l" +"vrc^u6T","/","","{""]K=+%w","q$tn*q'","F[#;ex\" +"OY#\h","","$","jkVo","1(","trR;O$<" +"-""C)p!wW+","NRh.HtfUnh","(ni3@","pA5un|[l?9","z]6{Yn","XSAcP UG" +"TU=5@","?AKY-*T","Xq","-","tLAqYc^uwt","""g'z" +"/`'A{","Z(Q","-vxMG)>h}","]K]*L",";/zZV^ZL9k","""0" +"h","","y^mI=R~(0t","9)m","*0zUM","h""" +"","CDyvOC&@Zw","qZHzId(M","$[Vl 6","'h\}Fx5y","B" +"X9AI~>Y","","!:","8aFO*923","m,p","g[" +"}q","","bW0A}a","","(t1q?~+"," h^j!X}VW" +"8!Q1.~ s","Py","c&Lv&\e*","ms_d2-bi3A",")#HGbA-?","" +"9C+gr#uS ","g","G![<","NMCy","D","'9PHH?" +"#.VPb","7=2B37e?4>","nPT7`","llpW","9Aj9Ooe","sJ-nNB" +"b2=S@IxIkm","+BA%Zs","W4%I55M{8'","\'D4,o>F ","=midD","9(J<" +"#bHT49QG?","","ds","BFLy\W+/x#","n","M" +"w_&Vo,","dg3'jV","4I}Y1:k;43","","Y,9FX/8j",">8+t#" +"BXQ6-pUq/?","VY","3MoQp5?","D4\'","|tX2S{\wg%",":A5kWJ8z" +"l","=Wj)t?(H","jw&L6?","(","fUlr6""9]","y'Z2,We-u-" +"U_bx*y7p,",":$]Qs9Q}fH","j|39y{>M.n","AMN6]","^","" +"){uDx:v","T","]","uxCJ","][m4ys*1;","UQ[@" +"^Vp}u}","v>ewvCQ","^mv`P@O","_Q)(I]","]w'e`","?YnBb?a" +"","Hr""x`99Q3c","9'{""WkY\EA","nSh/aN","J1z1,]","4@u" +"_","&","r&","D/rAv(","hM","u0" +"(ra?E","H4bU$8O","9q.w0[T;r{","\R","","XW!8o*k" +"]","}jHz","M-8.SSwe","i_T8m{^","oc&+^zX-@","`4"">U|I%Id" +"D3oIaNg","OV|WXNOJi","VXM1guw","''%}8oT","Q;K@c}HA","E|B1" +"%F[?","","","fOz2","`WQz+x","" +"bYi;G""';i","N=1MX}C>","qxIF{T","+!XJpV'UI","+","e|dR(*E" +"M","""zkip8g","","(OwBj?z#","^Mtv","X" +"x","XFs/K{?6","4Dw&rj|,k9","","G","yZL7" +"","","4nHhv","}++","","OW-mE2" +"q[L^hs'uT=","SR:tN'","Efc\cz","","","" +"yk8ona`{","bUiD1`n2F","B,W[GK6I","x""t9lqZn:F","Sgr~>S-p""","G~@L;" +"ap/""*WB?A]","Y","V(+C1y)","yZS}wh","nRc)","=B?*" +"0w)u2h5","g","9","","@q]uL'","Ifkh" +"ny&#vmzAXQ","0-]2i","oM","oG3I0","n}y","V[Uk" +"B","1(^AJ","d_'","_","s","" +"","6","MZ^b opH[","K}=q","?jzkO=%","@r1_kqb&" +"b7EZF","6SA","qP6]J@(J","P,ti","(Ur"," N_Bv\" +"EZ","g17W","N","#>x'","""\Mld1","FR[zD?W" +">2g.","u","Jo,|#","nd9?u","","" +",9",">Yk/*8CZ","2~T[","NZUF","M$$VVMt","*:U+C" +"XJ","T","Wjk'nBQI26","p","G.""L3>sZ","." +"]3TOkQ","4Sa%..3:[",".fW27ORchA","",";:W~`","1U9W#]W" +"uL&P","Q[7AJg[,%","^/Nd%$u","3vReU;YD","KQK6{-l2^","#i)-H!/n7" +"qrCR/-85?G","g#t="")r2)","","6a1","D$RB%R5\C","?ldH7" +"TE_jpj","sQ\\9:","""I3","\-0Y+","Q\p^","'Rd9wwIk" +"=H~=fV> *G","Et","gf!?`?-=y","tH.m<9^PD4","oB","8>h{1" +"KQ6","","_V >3J6xnk","!^@<9","J{j^","S.S9wm%?#" +"=OD:u","""B^th^`Y&","","^pC_7zY","3zI","^{1j=hG)\C" +"PyD*cT|aeq","0""Ql~","Nt-td@e","n`VXB","s)N[P[dGz","$fvP{" +"~YfBP+D(A","","#]})*A_","kZ""'",",NbG6z0N6","T%+P" +"BbC,h1","&9V","1)","X&tCA","UOL]k""","/0mU" +"U7%=[,M{tf","9OWO*d v","s_?=Q2","","N9fBp","g!N8!Rsoj%" +"+j$","`cm9N","\5.Dl","XFLRQfT.xU","x>","{*)" +"I{N!9","","Ad",".S9Ve!s*e","?pceUt/","tm\6AJ|~" +"R;A:[&>H+,","yzpYi","BZ0U}n!","v-EH","9""e","`\Rt" +"*1V9w",".""C","lm)%C&/|L","yo#tqP+8","LM","" +"","&*Y`","}v&","GeGiWR""z;A","f.","Vy,`u" +"@`fx","]\AP\zF=<(",":Oo%c;gvx","&.","FGkz"".",",f&MXV" +"$5","~5_`2","QKN|%fa7,","","wH","" +"","","+~Q""5t)S.",",g-D ","{",",as+};" +"Es.rh~;.^f","IW]!\Y$~b[","6=x","L1H)]O","aiF#}b","S`_DWAb:U" +"P `D.}l","0","J ","_UR ,","|E-Q3;:s","M\{g|dMjxy" +"","Z|7"," -GH,;0;A","~5'","*KHU{CE5~","AJ.[&1" +"ij*>L#qvt","Ma+5;Lh","","WV$43fpF}t","Ur4H=H","&t" +"3","+""}29D3-","T?XG","r_r*f\:","qOJa{","gm" +"_0M+xc","cp}[","1" +"i","eqCYG","","?","+18E|8W",")W2+R%" +"","6'WT=O[^;/","eF","G(v*xst","pPxN","KCVs3p&saz" +"","MEon","TUgSEKQl","],+-U","6qa^lp!q4x","H;#0{#ujV`" +"lW-","2rhQT","A","qY'8","^","#=}G\6rqI5" +"\sWh","dS","38 H#W","J|UT~" +"O89kTE-","&""E%){","/","KlEuc","eh S_V{jjB","" +"{W.\gK0a","V+u^2$_","@a?","|}","v@7",")~Do,`pi" +"7j'","q15qc1v","\uu","Xc,[#y2C","}FS/|x$","Q+1m.o>7" +"","RCZv]Pr#8","e/nf2"".wy","REeXl%6]","\e<1``YL+","%,/o" +"|UomE~i.","^a","H","GX:t!@WE","QhM>",";" +"=2","9KGu2v},Y","w","$E?","","" +"J/{=tmV%C","#,1-g","}Mx!>b(","JA33G","x\/h!""V","O!6Nzg" +"",">QH0d(a@AK","|=ZIX,*cd","K;[{f.fBBK","^;&p","q5;fYR" +"1k=k","'n'Q50","","","E[AZ:1m?z","|e" +"'L-NSp","~T\@!ELw#)","ad!","Y?$Y","{tcP80","" +"J&F)'""=>*","*~F]YW","q:?G.","","}RX8B""","","DuLj x'~N","","","","E[&66'|" +"2mU*]","9}E,R|P","","pT}TL?ISEt","Ak","? t" +"djk","","IC<<% ","$w!v,SO$","UY9fL|p","F>rAp;v" +"[ >","wl'd{hw",",(]","rmX","j5nl~[t=Np","6q z4:/Q;C" +"6",">XIn-fFF^K","]F{``P","",".,","\e@" +"9n!X","I}","kg|Gj9td","@@(5","~Jf","F^(>;" +"aHR7","Ix7*BleyZ:","M$@]""G'","y1n>7","","\.YP{>" +"ps5Aog_","}","J0$C.M+9yF","z.I96","y*UB","" +"jERgb","","","7b?","","`*v|V-r" +"8t]","4YOUNjYp",":J+/@rP&","","n:xFp","EOjCoPLtS" +"$( ","d9t>{#mFp*","k","{ILy]","[O@=","8Rjf`" +"F","NDNR",".","`CC","","eD" +"(~z}ka&C%","E","wIy","+eT|)0","LtY)W",",sz""w2L" +"r","j AD","WDs""*U~<","h","..s>X]k U","0" +"gO","Hxq""ny","","(%$","c-PS","f7.|XZ:" +"~pYj&)","(bmAm","","Q}BN*)",">E>N@v","9*" +"Ecbpng/","/V:","E/","HLD!''$6="," T/AMh>7","G/=>m86N%" +")gU3*!T<=f","Ug'MUU4","X;""C","lK@","x^~m","IAY" +"G3tzi;?-","-l","-tw","6CAIQKv " +"YjDe","EP*","Rg@VYU|","&","e1=`iJux0*","h" +"W","nQG3wv","","W=","iTh^9 ""#","PJ!\1-/fhl" +"_dimgIOPz","","ruAH^u","5|<>Rt{L","kk","" +"6d"," Xd$r\","_vc;"," A[","","F23Q^Gb`ea" +"","W","_,~G=|7SGB","aUHw","$#,|F","MR[5\4" +"MAnL`","esO8B/r}U","zki+%('aT","K2=","9(E&eG)YV","L9-$#Vslx" +"D","$(","NniaK""E3/l_T","(","EyM;p%T=c","xI" +"*V1`@2oxN","h;X{","YeyVi,",",","T","vG" +"m=`Pl","uj""L`MbeE","KI","netv!9ZB8%","X5+","6\O%9l3250" +"sZMn[@urg?",":TND'","5IC","LYrYg","dn","H""Rod","""a#|","" +"$p,M9((3<","H?16",",F","Uou\`i{/'","7SBk+@","v YR" +"sKEW]|z,","`4B","m>","hO^le-Sp|","!Rxc>f~`","V" +"MRy^niI#^W","","","vA=]Knpq5","7Ax[}",",R" +">^k!YZ+z.","N+-eI","WE~""|-1zjy","/`K/17","{:=""l","" +"ZaF?4uSE\4",":M","rHx&8","&MzX`N"," >aG^L\@t*","|'r@,0" +"#m","""X?B_","X|,%%?","@`z","Qa%PUf","27A" +"0sNK_W$","[{WW#i","","bU"," e0fa\==p","C0$A`@NR5" +"OQGkN[","[gV;73[","","&0.c"," 41z","%)I7m""(kOP" +"sW?0VX*tl","$@t\zH",", 6AxCK-$","]9h_b >","-?ie"," A$sk}c" +"0(iHD","zmv%","-&","oZ][s9","cu3!`P4x4","n, " +"#0EP.mtx","S@]s;","6Nj","b","Q8H|;","" +"-/;\*#","}>","XwPhAK1[6a","0Yn","23","Qx!|lS6" +"2O&T3DWG","#PKZrJG","D W","aQHEw/4/"," b","klBBNEA","8k6",")L<^V","!b;|akP" +"ZPF9Vy-:","SQ%)4Mp","Srdq2/n._" +"<,=((V!","te","V|P$[$/","JgIDE","iXu>=-","1CVf1mrA" +"Dj98#RG _[","n@?HK6dz""@","rHLT","Lk","{[=xY","","D(C,uB","` S{[ti4" +"]0dR&wYZ","utp","y6","","+,p;TJP+1","[!{" +"`}E}!}Q","ej9(5OQe","=T'j:( $v",")sH","9ueJeL","" +"8n8eTE3","#4","\rlM~\","?f=>7g","","\" +"mD`$&]N|f#","k","_xpJ|",">w'u2","rodIU?h","Vp E#M" +"EwK","T","L;v","dUgvgm7","pPYA7krakU",".>}D" +",Q","D5u7~$z,","x+@K;","Sj-!8""U","","'!0*h5QX1." +"}k",".P","td?@Q._","VM""",""""," fpv-" +"Wwh3","m-AS@`,","TxS",")l1","'SSW0cdB@ZZ9","b{UsQjg2$","5\E-=Zf","","FNj8X","#d" +"l-",".'bEHrm","a0td","4=b\O_","~ArL[","V^""Eh#h" +"vn~","'""(PH","","[NuNt3y+","18uv:D","w\=pX""" +"$~,^vwlN&E","t","","TY8%j+L32","lt)NY} ","A/xc'vYrz" +"Y","qRdO","=-IsfX;I","E5+n","b/e-\.DtsC7" +" wj","","scL$","&7+)CQ:","","qNn" +"6ki?`(K:T","S';Ni","P4","B%j,.kDiI","l","fl4&v^" +"dMdiZ4=\","K1'ZZnY`","WsH0","","{#S\`Jb>","$" +"kV((5#","xV*Dc>","TeZ}EC","9!e","i)\X","G" +"YfCZ[CA?","71jR~$h'X","K[+-,C","SvfR","Iaj2","" +"UM>W","B'!""<","eF}/F","qCE0'","R/w6M","]`SK~N]`7b" +"o(:F}o","y+&W$","w ,&y","@=.e=qC","*3/*%(=R","Rj}C748o|!" +"#8ij|OlO","Qb","ldMsGXz\c","{PwuC9ORE","FW||r^|","g" +"[A(","%!3D3qN","eZO","n4Fw.:","G~wdG","" +"<","J}@>Tn","%}b-<","#","X']" +"<(&\\1G","\P.R$.~(","1G.","srP","2o_T-.-","" +":H.5","","","CxnO)2L","VX x0","u~f\*" +"~!f:so",".k[#1t*K","_H.>*^ !V","p)>#h",".0i","2=??m""" +"XZ|","8|dQzJY?",")4bHG?w%","","~TMQv!kTB`","1%$Y@W\%" +"tt$oh=3n","v}","s!]rr[<-3","T38VpO5^","]Pj(%{o'","Dyw@#Wwj" +"]>uE[a","9iF{9AEo","^ogx5","mt","V>","Po+PMC:G" +"HN`{X.","J",",_","d| ",".~","+=X" +"zUW@7","sxVm","1|="," %","g?Hjdb-nQ","f-P" +"=m","]t",",CSO)&1","ACm","e","]{" +"{4%j);c"," ?_7","R","&^g","c{(","-Vju3qlM@," +"zx","dE46kz/","~V2L`[~cH","","iG\",":" +"","J!2Gq","q\0Psz(:?m","H3=kH]=K","A"," " +"FT^g5>","M=Z%YJ!{~","v+","%LNy+#9","4|(S","1/HRtxuU|" +"{o|2oR9k","WAs=}Y","aqWf","C$s","b","$""tQz3sw`r" +"wZ","G$k>'{?q$","","f._!*","D0`S5*Kt","" +"","x#;Sir^TPo","'20#","A @V","FnJxC8@k([","=@h_> 3p" +"74,kgf","","YL~|)K5","PJ11_y",".","Yrv" +"wz:2cC_","MI4Ef","#1Uo","57cax(","73","8" +"8]tv~tau!","~uxDQ","$Duy","ej=Zs/","3KK+","5M" +"I{lsbr","%a>MvkAn|w","L*r!]","kSBkz","\)i=","L" +"5","LUq|_]#Q","8[;V","U","x]Zy\OK","[p'%yX)^q" +"-HuYPT.J[l","1C`smvf","X4PYL","n;N/N","d+l","T_+" +"ZW}ot]","\1^&/_t""" +" ","/Bwvj","<[=8VqI",">+P","Klz/u1ps+~","-xl,V" +"p","X7jx=LGW+",",","_Ua4CYT-","yW7m","w)uU$I{[4R" +"z","Aa","FXX`)%E","","a","b" +"=Kr#bxmmq*","V/w:","","5o oQ$'1","D|]6[","gb<" +"=IRc9A!=w[","0lRRcu6jyZ","#""","","","r}" +"","&K*DQvP`","","]@@1Y","zNvqD{B-.;","rRt1yu6p;'" +"m","jFJ`$Jg","Dci_7]{UF","@; kbb)" +"= 6(gd]zK","%:""rOA","]UNvls):^","bw?<7r W!J" +"tu(R1h0v-","/lc$V;:n]"," uDm$<","s}","*W;+2 [","hT6d.LvZa" +"","~-_2g?","{","m0o","~8","=msEu" +"1< <$","Lg$}N","aj","9:.=mK""_+K","YDJc5csE","V" +"l#","g^p@fb/","/ZMbc0k","xdHXp","G","wAx^t!y#f" +"M)","6x","DfqF","sX(WBL]ch","\W#=iq",">RN'%oxs&," +"=","Bp'e!","^&.Epk","4'n2X9","awF?kV","q|XH.1Wr" +"DzPOe.9N","}wkuS5E","yHGn_<@",">r","/j[","M%" +"","<.t-N+f|>","oU3{}","z","W","7nR4^" +"tlq","r8w","/Vz(6uv","jEkLh","0fM","rkGk9lclrc" +"$u)FlIAqI","]O@c","lSNG.kl4:","U@b-=j2","~" +"0b\,v'Q","z3J-E:h","=9]%E","RWKPcqV","+",",6P[qFFZ$K" +"u3x=dPE","pE"":!k","l`Em3#bX","Tbms}'gTT&",";Vu5b-H","a^UG-t" +"Qd{84Ax","(`Tj;","7","slGG6<>xe","-","O" +"X|M}","LH","7x","",">[ZI2oW","4Uvwa[x2" +"l",""">>'9""4z",">C\Rzdf","594[^oC","W",">" +"*4D4W","Nt~","(_SW","k2Z!F#C>_?","VLDE['9z@}","].G""tu" +"WqZ!&o","j+9{QIU","cKCEHb>3|","d:M\|dLl)B","h{X:g","[WQ{+wf}" +"","+","~Wu$!Ub","N","" +"Q87","v",";x4m","J h,W","kNW","oLlQ" +"tn","#kXw","dM[ ~j;","`*^Ixk","S>LD","0bgS,p\^`" +"bc0VMYJ","a)2A","oSZ2.O","_*Aua{","&i#P5um","u~ F\3?W63" +".hQfq@[","nhkC.q","""dN","op","(""U-MI|F","~!~t-5n{=S" +"y%?","bwxm-=#","T5","`ShN4 pv`e","u","vxQ/]$7^m","#","avu7D$","","|u" +"@Czp'","?""&]s7","q","n!`","vvJV}i0L3m","4!T!" +":IcC","7=;>yv#I","F4TS9","[l","RNj93iz","+np1D" +"3","","]XMt]l","`j=}D","SX:9w8","6t" +"Rx=TAx={8F]","cI","c@H","" +"l@wVdWV=~s","8","&7WIizx","z&n:yA","3hwiuXI*","/S0|=:4GG" +"Zf,5RiHf\;","neFp?",",","B","-qK!6s]","6""wTc*N?" +"|f9","","I`N" +"m","","","WS^3x","gl""""|59","O?a" +"()","\DgIL","7","gwPU|knaA;","H.z3x","RYfn","KQM","p{lpx8","ucbs3""sDU!" +"[","UG`_X","\H}tv6""%","Esp9W_j[k","fD(7\7{b<","m" +"$&XQNcSQ","'MT!bV","DeT7Z&f:V.","/Fap`","Lh{7,?;","M0KIoc:;" +"K6","*?1Q}","",";hf$sE","@V@=JVWo","Opk" +"nVIN","y"," /X","xMWy?80]2G","w","|" +"gf[","|swiB$G%","T" +"l04j)%","&","#""","lA+JV3:e>","SMV: 6","X +pBY" +"|+%ar","Rh;","{","6kzX*",":^*y)4}z","D8m7!-Ef""m" +"PNC","R6A","ZV","87-Z","LKi/wY","rdDn3_KXs" +"do&`C]foe","]0N-jN","trQ'@<]4M","d'2","=a.fbg;S" +"","","p","9sg'uSs","zUAOCiW+5E","95H" +"\bciML","","`i.-#!A0X","|BF@`+jU:U","","+9r/$S ~$N" +"ek+s?bWt","'}","}IEOS.F","","#9WN^rT","" +"9'mAm","G9Y ","XY>t(i6@s|","mXQ4g'`l?M","","yVcse2" +"","","Q`Jn.? Xe","a~53Ff","q$","Z'i" +"","N.BV2m"," 1m*Be#+","w*Cz/%I""'I","+C^Pgd","%m$nS" +"",";","F=[.EpKv","6t`,","@","" +"A","","J@K(","7ap'$mI","{$OTs/Fs0!","g.)Dd" +"6J_","FeHvZ8g","$ZI#Orb","Qp|iRxGDo3","ZVu","G" +"uv)l}""S:","W3kwn3","","3|","WD'3|TVhew","35)v0+cEz" +"+]vFKwQ}J","o\b[(Adk2%","1 @!ug'","n|:zBN(?x","n?u-","r<^IP,2" +"b* hj!pp]7","&Ck","j/O%w","4@3PW#","/*2","Y" +"PhueOrt","ce""[eY","E","","aSlch=b" +"_PRLP:","e~-51",":rM9,OcE5 ","[q23'","09j#","x+^&!kIqSt" +"g! tMLhqk","'fJ[Y9d","xQZ;<","(o;,t(0",".%'$jL","M]h*UY3\d" +";bkMy)","","sAm2",")gFe","u5=bn9a/","kB[_.5n" +"%iZ","L$","cx4gQr","""H'$lNUhY?","HR""""9K YR","q({8?aJ" +"73c"" oBx;;","G~D:vh","WMyJUiqqv","Ld^5K}","W[]58RF","^)A" +"","","`F","y*q.1eV#`","p(","}87" +"B}","qVAxg*","]h@Mxx","#e>4","[h/n>","JQi|mXd3KK" +"]K9"")L|9Kp","f$c_.R","fR""6zAyR","%3qX4","hC'vL","@5tzcN" +"","8u\'FZA5d","WZ'=3","g","+e[;","" +"4Ys!z#9PL",")K+rNIgG""@","e-j5","2qrlai_K","~JP@","'Ig" +"F#_WY?&","4x~X!<~`3'","o,1UP}SaQ","n`y","z8-nh/]J","~tQx~3v:&," +"~~&j=UzU","V'^AT","R#cw0","wra","8@Z","84?l" +"f4xf","5","&10J9*.Z,","u","9$+koqH'3w","*-" +"5M7xn","/j","","<}0%-]","+","rg" +"","8LOd","3T0J_X,$","Jmbh?'F","|m/","eidJC/" +"$H!l{","nlw5.=,",",d.@7^","#M*DR@Uv K" +"}E}*&u>!>l","]>","^Y4p","CTA89eM,D","*6QO","TkZO5Wlu" +"js'r""",":h(","dxt","LD`?`qv""s%","OJ4i","L^qos:}MS@" +"=$s+[bM",")r|7#Va{k7","J","]","7e:{V@%9 ","QaAbt&LL" +"gr}-qu","r$a<",".","","~pv#70T`^","*mf-" +"y)""Ggkm\@t",",2_A","","@d_3Nh","LE ","&_DA" +"MOD~CS:0","8-","@CtqXf2","Q","","WJ>O" +">9+[+m","{ytHorhJ","egj) 6h'tT","|('",",qzG/","=t-" +"X&P","Mx'D_",""," 1Wy4Et","VG6\?\","zux55" +""";gWk","","2","iL,zy",",&","h" +"a","16~*""+58j","c1LEVYi>!","","","=" +":;Nrfu/Vgq","0?A","sQVBo ","Kx","bP8tj*1Ikp","g" +"-W","d","BL7R+\G","","rs","iL;'yJ;" +"$S","i8)E09","V+<*","b","%s+","`Z$P" +"4$kZ|08rBa","P;2W","t3yh73pa","","W/","" +"Gukr5","PPa7|Pi","FJ","","-e>8n,_","iwUlE" +"",",ypj","S;ac","-0j","Jte-2n'd" +"45S@Ts","3p","Cz","jEpI7}znj","*[#Y$","7z)D_+A<" +"f:]*s:[LS)","Z*W24[",";j]G","1_?W$+o_xe","0h(_[","Np?'>","" +"|/m0<_`","h64#f{Y=Sx","lE7=J","Ps#i3Fc$","t\\'\","xuM:" +"fP",",uH*mK<","st25Pb,F\","WoMmepO2>","?+>F&hu0","p" +"{","Q","]4","R3YnS","i(l","" +"+o7&p>yzn","rg","=@b","","W:y*$ahX","0|*Wk-Kl.8" +"""Wc.(/","*Q]!Zy","G+inbj-:p","*Ix&s~sdRp","<)","_8Xk[,r\#)" +"F>P","M_WtMOwU","7S\>`|xB","H Mt&v","""~kk(","OJIxt-" +"6q=b7","I_@4chB(fD","cGr>h","'D ","Ej%PyZ7CtN","X2*/h0MD`D" +">+_[65C#c$","%TB9","ul>z6","?51i","GZb/U","9/]F8o<]]" +"Qf","(R""8","","k\j","%1.Yr$s","Jdf[" +"E","7Q","HpXW","""g","1B","" +"w*$","GXtzoTd","wk&C:zzzL","~K<","/3V","tWyp""5x" +"sFh,3Q$","I","Eq8Ed@)J","O:)uVYJB","9%b23(@{v","|S'g" +"gVbR","Nu}l","Iwx>~","$E(ChS`l","|2J/\#%","5d81" +"","Lw","3o].b0 ","lR=Mr","g3C^","1^^l|&V8" +"9~~fT*","s0"";sZd","W0Z=","%(d`mVK","","hIke-" +"gzx\bI'v}f","S}>L]27","6","dRAY7","","VW#46kzhC" +"IeDL#aL","]?L&>MG:x","1L`{;`r","","hd1Gm}'U","" +"{","``st1&","it0$^'*","V^","\@7a","EXA''" +"D","z|+(^!mY","b","Uv0`m","=5p@IlyjiA","DT'O","\H>|fj" +"?hp6IP",":F?m/ka<1#","6dE","S","M|u9G{*H$T",".a%.6 LPHS" +"&.-zF%@z#]","e4B=`6[!QC",">","en","jx.","uXaNdy{" +"""#@","!JE","wuI.!Gz%","","Q_PfsezC$`","Yq^T5Upwu_" +"@c+3{I7e",">qx","A@pPoF","","U.wYH6","!" +"W>&&Z`O+]^","E`0Vw\","{2*7K>j","x17:oOWA.","EQ&","+u3$U1l" +"qcmz",":v9I","E","3CgNM!Zyuu","J3CnPxlZ1\","3zAoCIzOP~" +"Q.t ","J&9n^+6%","&!:2Reo9","C","n )D=C)7g","F?)`HLn" +"2Cr3G&.?","W7K ""gLu*","/","0%G%bIpe","jV","" +"A-8E","-W;vrHa","%",";","uOHC^6!D","5}+Bo]a!xl" +"<;""","j9v,X?ggD&","Wi^P""M=j:;x|xG","E)h(gfy3:(","V]","K: e9uf=","}#'>Bna","" +"#","r%1]4-/ d","Vo","fjt$4{.","","*Z" +"AI52g","|PLgl\'6""","jIpD","p/GZ~^gb","wHB,vy","\5" +"Es7S_|","!$IlYw""","ShaDcoA67","F5oo\?","c^cVR","'0Mg<4" +"ge@PP","mYHd[",")gT=pKN","J\vE+.D","v?","%Gigv5:N" +"","Ky","r ","i","R'e","v,LJ2|" +"","f,[_@C:lN[","","Q5","*84es9L1@X","sK`" +"&","0(&^\V","Bf\'o8Tde%","r","Zz)L=3YA2","s?_ds=," +"LBq?[","A","?}9Qv",">!BI","z+!}Ha4A","Spg1[B" +",@T ;M","","?3+","nQMla^H","{","sp|" +"Vl6SPJ","SqB6mN&""1","","qaJ'bl","","" +"yv@usc","","?zAaKO5","g(:(FQ)VT","","Q" +"Pr+=+","h","","Rr)C][" +"%c%o,","X,Z>C_%""3","Kj6hh>C?)H","6n:i+G_^e8","","aBEH" +"",".e]Kn'","1ZrSG}#rZB","L69","SR{(","C&ju>,}K\x" +"I-2x""Rrc&"," b&SLKl","\y/+Y.e","3X3","[g","$7j1;du@" +"5_2""FEMGJ"," lS`fl1","4bEh'v^&9F","hzw&u`2%n","#","v|8VT" +"v_E5DH","&!pR4D$lL[","[;:n7j","",".as$S)yF3B","jL~" +"7+#U9evL.",">M{","zgYU(K","FM;M","Z{?,6E9&",">^6hf`P" +"GT/","9dGdsq",":/s+xB>","E@","`","wd" +"C/~nk","_b","F+wu\HU","=cSkL/wU-","^zE","o`2,BG8","OPV+{x;Q","6)<{bwo" +"0","#","K",":,fb){bD","N'p""0RP&=Q","IOc.+N" +"$KWlF+rHT","h[[q91~Q","","dbI<","","P_XIMZ""4L;","&k4[&rC","/nb3" +"D}#QOPR","F=","fZj>^)]\9T","HV!hK","","]Fn" +"@7`&","3/]|@A#aLk","HhR>JY4-","r/8yT3","h?Et1m","cTH7k""_5" +"M$i.O'f","g@M","{Zzd{g?","Gth","","w+~(Zp" +"hbLQD$7\-","B}h0]j +v",":.:#C",";>/7","m98u!M","i*h!V`4`Z0" +"d%^","1/","Yv""Z;-","","{","QCJyMZ^X?" +"X","iz`3{tm*cE","qF",".[","HcG46","." +"991I;pz#'{","","&R%>c","BZ-|tO","tmt?","p" +"Y9o/43","","6GCu}","iwp~f0","=^y+","qK9}a" +".yq#%CcH","8(cQf","TbxTg?+","Jho/nr!9pX","a","t2z>" +"3m","L?cHNvo81P","s'vc Q&)o","","p","zZ" +"a$","{",".z oVLjsq","7","Hg"," qo" +"2?7","X",">|s>%%A","","{~","BE}|E^yt5C" +"IZ|@_)%28u",";","V cCIZU)","","#?E>/}","6YK" +".","x^hsN*2hV","c&ot","9F%[ m$c","P.h?LE","9744" +"hG:ey4lW:*","O i","OowDOrq4","-vRlPVi'B",",","4 wqI" +"eG","uj.{b4","","D]-*3P>uw","GFNY","""" +"9""|HX6JQ","D%","* ","^v(E[Lp","=uTx+0","{r(Tp`" +"H*p!#E]","|!u#]""6'n","{,|(I","ObQ0Iwsz","*2_gQ7B7('","G" +"6I[TcL","ai:MvfPt","*eat2L","nv"," k}5","_^{\?b_.s" +"Ohe[,0[3e ","|PKyw%s^","[s]JWg"">?k","92q. ",">r$3=BQ","+%%L1N" +"}Q5L1 ","JA~yY""","55o=;qh8~*","c51/)E3snj","}XVo?R(V","x" +"hiTVAZD]",")]"," 4km[","2","l?/^bH~b","d|'J2" +"","","p@","OAQ$","/3/!Nf","\4-" +"NOef7RR6",":Pv?U'E:$","U","5eWeM","+t2B}","l^N\GsW" +"","q+c%?","mmg!","?l6GoO","tJzl!" +"k%CwZK!6R)","","['","E1h#]~fx,","0s!_so9,X>","'ZDf$" +"A&0TSD/","","nq4rPnfq","+>G","fe5>H44","jL&\c""6" +"-YV\x|YH","","","I`lz|P",";XA","" +"","6rnX#KJZ","^J>.J{","nT5","qE(4y","#u/u!&>JJ?""",".[C5b"," ##f)J","WS9e/g@","+a)" +"Ih2jwR[?","/:a","4y&!w~","XE;;^#8","N$\]0x*","" +"m","XFD","4QZ","Ny,HT|~P","x""","" +"+iL2","rHxM>^\","IB;L~Qh","","/~D$","ie/pC<~5r^" +"Kwv","vRU","","NBTr","ae=_P22-p5","@*Zo'ar" +"1","~6G""+N","B'ImQomu f","d8R","s","frrai?hD1" +"[:_@","~JC->,KA""","ar*'>",",E#%}u","+d\go|99""","E*r*+/:|5" +"n""j]1)bP","k+y0iZd9q0","5L/m3@e","i8"")(\","K~","%" +"D@9","eG","&3y8","","8(w%","me4$yFW" +"Pzx6heCkcx","gW6t{A4|",")Ul)","26","[e[d3I","i27kE;a" +"t_JN,","cS/2@'DO","22.AdL@R?a","+","%5e2OMLu","h(;96sI" +"p&o{{wgA","","","","X3Jc]tZ1","HhS","!{M-MGO<","h|`<","xrk4[~AeO" +"9-nSy","[i","tp-","]1@Dlg","kU","!j\ho" +"Z{ ':6","MU?ysE","[","Ie0?w ","d`","\R" +"N,&$gmPK","$St\V5T","cml>7k","","F}","('50I_" +"-#3&9K\reC",".2'","\F9#>c0>","QvMG","""=O","bkmKhVqr7" +"0",":#3","!K+j rf","q09E =Ea","%#e^}dyD","2,(M*s" +"e!:^8*[Y^","","/kl[(i:'vj","L()=(UDb+<","qDNH","tQ_7lG_" +"6yBv","","#r","L&1P*#","lT`DMe,9s(","q""QBkRSW2" +";RTJ","xn","}xAZH0Bf-","lZCl+s]!j","=Ex=;7I(|r","0ZbhNU,8=a" +"","uW-3ptELpa","W%TCuF*;","/`~@-C5?""","%?Naa;","nyEf;L7y!" +";f;`t*=Y6","gHn","C?F`};","QRZ1","'DJ][gy","L:=+`_1c]" +"(2G","{,B#0B-qz","zeF7","?^+.hwCe","z#Sa)","*mD =k." +"'L>=","+" +"2H!",":x{dqv","+",";SNYqc","2%","G&^\[" +"%iC9^lY>","~:WA/eT",">","RBi]B,\3|","sZ","#Y:%%a^a" +"JRlT7+1","A`gP","Blsw""Ew:;","CP>.6rQ","LO_","Yk1" +"-t=JvO","!]sr","""","01dx=pc","B""L_","OW" +"%","w1!S0","Z;c;","2i@","5:rX4Z(_j","^bJg" +"O","r{Al/PQ)","Vys AP7M","ri+QE",",","M$k" +"ZouZ3u",".eM>-[|{5","amsJ/U{Chc",".","g","nWS[P" +"*k5=","*f","/Q:`5XIKy","45","h2q" +"","B","","_a1V1","jil*(z[?{","clJeSFGmq<" +"VIMR}!$","","\BEc|J8dx","y*w=a>","f0AMfL","oi]" +"e'X'Nn","ok","N","wD[4","","+12E" +"IcrO","E-3+4I8.\","L$","","Y"," lXL(9" +"Fwf^","jPf",":%(ln%jY","","I{m#","VY.$AV`Sw]" +"W,%H2>","a^UvVH","WY","qqaI^F!Vmn","Q%H~jh4HE","d#" +"J4IvLSF","aG9E""","va","cA","le9%)9","Bwy" +"~dT3A","x$$",")o90{V_6RD","6Joh!wqezL",":1mYh","DYbPZJ" +"tB","MM?fQ=,","Gm&T1","7LYo[1c-","X8Fa>Q|","!=:" +"8b|.iQ 1W",":fQqqc","(srw\Of1","hw>twY?~","*=&","A*" +"|a","C'yLi~}C/P","[,q*4","1$B","~RLxmnvB1C","GMyO95J" +"'","S:4wN","JGp","XOgy,)gEK","(-MJ","r]3PF" +"_^{a>`72","r:lb\ZAYct","3KC$n@A","Jvn","dlA=","Wd)9" +"Jq>x~z#x","N#$-`1f","r",":","{y.","o(\" +"KmK`uJ1bG^","bqj","_aBc!gGk","E5''" +"A<>B","","~$?","qR","jY""","9CkRu hn-n" +"5""VZfg8^^G","CoC","W2.([i&\I","Z{*","}?>;2b0","|g-[","|jT:-J6Xq(""/5&H","","<" +"Y7<}Wu[","O{9c","VB\3!3","BA`R6+N","n}W+_a51+)",".Ii_&2&HO&" +"r","/bu{i/J1zZ","WiK","i9gJ;","sw}?_b~kIU","dL" +"Eg","(q=",".:-","y","i0EgI,","&FlD2" +"v6q8DS7Z","gxeT","f","Y","G!K2V4r#M+","%Q+,_0Cr\D" +"$)*2","<^","z,W4NUHmI#",",9","}^WCH","&LVLV$","vOH(n\>J*&","%'F3,l","m^`8""","AJ","3T" +"*","5Dtb_Y7V","P2+$TT","yYX09a6","k","" +"dB""5","WCsv{tz","T","","","-Cvo\" +">pM/","^H","yjbU-Y","OX","F[(~u7*P","sr," +"fY""","0yF}^yKpO","","",".BKyLg","@MPkL0" +"X","mN","8uq-{","f7}R"":oB","Me",":p" +"","sdxSg","$,_i@U4R","YJ)","J","n6=aK" +"1LmLbC3 p","/(/6r","o(?:kOC","Jj&g2","/ne >S$>EL",",FPYM|" +"","C","9[P#$inlUd","HU}","r3x","sYKa" +"kM|SP<{I]N","F""a=e0A=","X@-s","lB%","~ZN-c","|HR" +"","Ip%P'*8GO@","Qio,hHM ~","l5w*""(:K2&","","qw\P6H:" +"2x2l(eJc#T","{I8G8#L","ycDF","Ro`","9Jca","%FNZS" +"^hNiedR}0","O.w","w'Z3$+@<","n4[I#",".'1/RUvxo","ESm!JU" +"W","","Cn","Es&D","1>|qceZS","c" +"GNCOr/n4",")HhO?yRJX","w","/|3(|qM","|}E`f","" +"fl?!XY-Qye","_5Vi","[,d","u3#6","h6","dC)" +"Glvf!+","O:[K8","C&","$p^;<#::sQ","""X!{","" +"UN2N>5","rVo'{8&u=&",";9I`hl6D(","=4AS3F","])U+","orfTuN7u" +"","J@","Y=","=Ql","@5v;9L :u","`>kyY" +"xum7","","D&#,AP","{mBX$gi","V0Ov","Q" +";,IxZ" +"'*RN~A","L?I!4rw|","@'AeCfvj;Q","Pv","Ha","vnV~?:HV" +"tcl|?eN","","",";*>%","f&i","-U-e4<*hR5" +":iPd","hk","i65t(n!>0","{","n(eP5?l+[","Ch5" +"3>[N26no#r","jOoQ\6*30l","V]0Vk#","%>?","7H=f5_d","'4" +"K[","m{Vo@_esn","M{=4U&$","8","vJe$UbU]","""ts" +"f",">wm=}q%3cp","","i`'""","]Y!E!_lU`","Tnv?TV" +"i/E'","lTkgm/uQ","o","X@{#/","3>A1","/(GDFW" +"!j%5D;","-ON","mcs0(khy","A}ADG58vbA","0|Gy,S\+","3aH%" +"sy1zB8W2L6","~0~0fxi%h","xooZs{@M>","CvLa62","I1M2@f)L","C" +"vNl@!l,","M`q}(W","5p!t%H+X<","+G=$,>sG5","","6" +"@jrzq","hB=","","U&n+Ek7P`(","zj","yZm;" +"","0]","fXQm&p$md","|'""v","\E520s" +"?$6,>","nn1<6OQ","d?5^QPx","@|lQ`5m","PK^(bnW^","PU-'" +"!;""F-","}17PjCBK","VXpm""G-",">9","S,Y.+u,","];=)Tg" +"HUvnz5f","-","","aQ","Vw+>","+mZh" +"H+29W""","W","RF$9Bf$z","&","HhOu,/" +"-L]R_!1^lR","c=:t_","M{%Xk7{f","","","t.hSS`*a" +"W7BF%SamV&","\g42","9on4&:",";6?e{=m* ","r","NChdJ\&P&2" +"c2MLf_","Unr","r>RMGg;*","'wkg0","a]O={K.~5Q","D^I~","`" +"Uy!mkR!}2","Q","gX}\-I[yk5","iZ'ri","{`+KNPP","~[*:J" +"q","+7[2{2*;\R","uzHg\","4_/Ka6_","Yu$","(?n","*]1B","G7=vw`^" +"D;uQX_yoA","#2FwW`To)K","~3KaS[","KH67Rk","{iYo1ge","73/c&5tLxr" +"$oth##sNO","@q",">B5v","`X ","f6","Zk+" +"G:","R,$","8","_\s3*","$/","/d/;|" +"C","o#@;`;g","i)7BJ","Yjh7a","<9ul@i","^[fMq","EF}E(:oD" +"&y","N.u!H","+RN","u","_7\G","pcA8Yl^eZ" +"","P","3P>!","o","~5OWah","|!","Od:pvdD","-U7z7\" +"SbDX8ZJW","S>h3:C_.`","')SLc6!>v"," cg$lg)ob<","","DNwfnm$6-)" +"","Z/&la,}(W","]","DEZHzFk}=}","","&K\" +"+T","=|0","{","jw[dHnc$_","7goKy%X'","@W" +"k H2]6","2""Nc8 SM","","","$X( ","i1""Y," +"EA>g2CW,","1C","}kcAGI<+$","r","*","" +";2M]cbb1","eMs|gMag~"," ,J>PEP","","","QjR5" +"6i\s+","YXbo>+gP^(","=S(]EJFOg","-Qi","RP5{JgjnC"," 5D" +"uc","t-Nn:TOk9l","Got\.Y,Zv",";S$x,u=Wq'","~`w()Q&","x2v" +"a","^ZX<%nFNw8","IY1DK8","{","@hK","u6]5+/>`/9","'" +"V`\RI{8","po","RV4","~gMG7Ez3u-","!8e?gU\ZQ","_8KYZTm;$" +"","FX}hk6{mJ","K=xs","o","OzZau","A5,QMsO" +"/~","B1","&@0k^","d^Hm!%;ORT","^;43gD,_7=","]" +"{&t~V<,t","oOF*0gc3T%",".>","g_{]?","M!@N4hVA\i","p(3=MW(" +"8C""w{[",")$x\Jv)Ao7","cf/F?-ld","Q%>X:","@","SuF:<42" +"Y%x\q","$","/Ag{ H","#vZv|4","+","HNO5Z y$'" +"P-L,JzC\O","-}i'jpM0D","","[z","([","JXP/ {UoF" +"@vF_+Zq2","iaGO$Hz","}d)","","6ydu&","b" +"m.ze\;4_","][OZ","\VF0$e","ME>W4","","7;" +"a~X>i>;E ","=..TSN5IY","9`","o2=GZbZTQ","i0-","_b|QsOSu" +"HjInmRx&","!7LgEXKQ[",".8^bq~pRA","m)t*a}","","rC:{" +"","5+NAC_*h","","X6","z6D]]!","3o6ThM7N" +"CA+PH|CQE","#D","eH`oD0fS|8","","I^6u","wd_n""" +"1X1d<","]/^sZB]=w",",","Wz","fHSf1.twFh","afkl2/" +"]\C3&""6"," ","]","PF","H""@B","9" +"r00KOp|","-","R16F","","X~IGj F3","$c" +"bi>","d7Hv","[:.","TI$##k(mR}",".B|YdVR4;*","JM[=]J" +"Ex z+Eh7","Ja","Uw02 -","]","`]w0bi","" +"qb!1","Ft?B=#","-M","0KoI5[sQ,""","1S$T@%sz)g" +"={","","","e6;|","E""a02(pe","","1v3D}'J" +"/b0","s=Eb?5$1","Jr","t')TV+","B{/G","uQVkmvl" +"Rj","fwyByySt;","@I,S","f_i/\GFb","<3Is_%Y4","#45{aKn-" +"}","LD?8I[31}M","U,anzV&Ir","e43","T~sa:","!gIa=T{5* " +"FZza7*Vp&)","Lr","cPQo.od%80","p8","R","xI`Pj" +"gv","/@z0?z-'IS","1bYF,","v4N]0-uaV:","_R&4","t7'AUt#E" +"q""T","","V=!SO","~YGK)WWC","sm.(.Na","SBH!@5" +"wC2b=g","['mrq;+cO","xV{h","","akQtP5k","T}:cr" +":/J","!",":/Hq""9","","6=Z_kL","M>X3F?" +"'^n/U","%&uJd#SK","6G`xh8MFC","","M%+w|M{~","d>8T" +"Uk","Y#y)})5X","a_","ZMJ(.l-]","/K*","M.G!:","7}}%@I",">_0][,","DT","#\&" +"-)G.Qy","UI,","3b]o0h*<[","q9\","yYr+","-Q|VW" +"WV","62ldDw:","Tg>","|","Ijn4B","rH6EgcQ" +"5\2T/9<","U6wKfM@8","M:@wF-","./|`ZF^^'I","""57yS{3G","B" +"n_jV","?i}C\*>E9 ","JS:;yR" +"M","E@5;_","U","96QR6#!9!-","uCV1e(Y6No","+VQ8)^""" +"E',5Ez","qzbShb",",;0d~M","vv,fb:E",".tAz98n",">S" +"","-BiHqx","6$J.","((v","pih.*F","}IRC""" +"Mr~v","xU`","","k$XN*J","^\w","w." +"","j>J~kiIo$L","gm","NIB","1&UB:9aw-s","F.xAr1nE" +"rz5'y'","4/>AH","~a","g*GAfy","9XLfL!Ern","vW6SQ#lMMW" +"|uu`V1*","In'F7yn","lkaqoN2:Q0","","`","/U""Y7mB9+e" +"R5","x?Z,FT~*H","K~I","80;bt9:5$H","W",")." +">.Lpxd","rWr4`J*&","n,\By$-8+",":J",">\GI","h/zpf%-" +"hwg<1\/S/","d4<","d{D*","M","77fki~8Bi","" +"IY8I]=8p+Z","Ub=.'","$^1a]]R","LI6!","h","'T/&hi" +"qjF?cG};:","\38>Hc","e^+-`jJ","}","K,VXVZ~B1N","oES8" +"#","","P6\MW|~V3\","BokP#","E*'Ej3GO*8","rJ" +"W}su>","rcQcJt{1}N","c=0z@m(y)F","moR^X","0\XD","Rr8$,;6P)" +"::)V6Q|c|B","{v","tU","tH.f4|%cAO",",dA)w!uL*","-fY","_*[~W7T","f" +"QL","",")>X/Y","wgXEuL\","~T","![8uQ" +"","","M$s{#<'~","|d][","LJk","Osf" +"!oH","uS","Z7","QSz/","yXF{X@%","~" +" {","B1N{6swX?","T'}@2q4y","I","k","G!KgP;Y" +"W8D-","L+Fr","~[C,U8K_f`","+wO2","?Yy7","" +"r#,\%Co4@","#[Y0jL?",",D","o^"," !(paQF","" +"","3:d@).{","bmURnX0'i_","-p/*(@"," ","Vo" +"","P6Xv/7 ","&","","ohO38@""","=J):z" +"RX","","!7M?NQ","Bzi"," s;2","Dt~Kf%}" +"","OQV:t","LN=RBNVy H","NOh","3","" +"`uO","\-t)wk/hPR","~uQ+{cEI","rS_+&$a","kMJ7fC!","=G","{kp)Aw","joB","d^/e0YSrl" +"IMv$O","Atc5kK","0nTfQWz","KG41X^" +"yxWd$20qt(","gVgwi","?f","Pa~""","=" +"P&w!JO6","zx`D"," a1B&","h:_(Ie;" +"jk/#.d","&iDBe 8","O8TL8O@","WuBmy;hH[","","HIK4+!I" +"Y[;?j?z[4k","z3","","Ffxe-","4]6d/2Bd","03rugQ" +"PUL9FtNL","yaYi=5","#m","Xi","&0S4i)T9`b","Kp9h@" +"7i]"">++x}]","`%Jm*u1","`{C&6]","#oQ","UXA :F","rQC61<" +"`","e_t,T","","FMjz","K};tdY(yNa","" +"K0tM=E!>#","Q.y","2s;R51p[DV"," 8i","f","!" +"H{""Fw)^j!6","th","|","{lA^QA=US",".IZr=xd+","Tn" +"|>K-[k9pE","Z]n0","K,3s{B","@l","{j3f$r>S","#2ti6" +"l","j.HeasNPH","x&""<","iR2FT4Q>^","jnHi""u(J5=",";X" +"2_,ch`","oW'","DIGCYT{","{w"";' b*","","U<+}?2EG" +"A@lAq","00KZ]<","v[c&[","aT.h"," n!?6",":6" +"7+)wQHw#b","K%ALf1ug&","h7","5","rAJWl-E","I>""]KkD" +"L3}o,!l-A6","~tE.","2W?D","zWahMD","hD3MX{h","<$Z" +"[l8","|h?fPS?H","kGMqLEX","""pOx0","I/'o<8_","\\","^q5y","","X" +"6dnMzlxE~;","3h","&S","RV?wX_","lH)Un$v>","A0q;; Rg:" +"x46Me","KRJ)E6YV","S6","NxF3Xe`","5Kx@+g'","4{rXn7d;" +"jKKh]Nbd","mwvt$)nL_#","]V$n","z7lNQH2","MF""9;",",`k*5o" +"YOq","GCj^va","u@G7Qn","0\7","|","" +"h]","ntAfy7zi","0_y*@Oe~","B9KE","7;","B@\wW" +"M","n^","5%e$m'$)ZU","","Mt?]e,","N|Z1Q" +"{VEwFi).r","bQcI3","m.","pS(<","r.R;yx,5a","[xjE" +"kd(","UF:X(]P","vf(dV0-","UT#","y!Ut{q#","^c}e|3;H" +"$'A+","`","H@D#}2AP^q","\-n","Lc`mb<","|-D2c5V" +"",";^-'&MU","9{o}^(41]","<`","96s.?>L1%5","+?i" +"o~^","g\Mz<)(","Y","Vi_?]4!MFi",",","" +"Knc#xK'G","I_\f)<","xJ!","x*",".w~s$o,","""3P#","/>9b1@Mz","]G|/","e4UsHl$","J^YR2/s1@W","Gls\#$hDF" +"bh0zBn","","G#Nvg","}9%e=a\","","x""T1/q" +"]/0Lp","]5L_ [*","u To^","","{Q@","kyP6.uHY1" +"QUXjd","""1BRv*-5",":b6>",")fDs","~)._.","VSyuG`#1","`4:e!Hr#(i","T%KP",");`%R","t" +"[x>G'","~Y*LU","R\mx;}","?Khr#?yh]","pE+8\k.&#I","." +"!xee","XjR\ w","]Fg(2>D\7","9f[UR","|v","vYdyU7j" +"<;(zk","md@","]K1aK(S","mm","""$3|])p","T>4X.N}e" +"s,]","}i;vBr+","+pr-" +"\Gf" +"T-b","13lg~d@T",">]HMXT","J*bQi.Gp","+","" +"[= ?x]","f9d_.;","~u/WuJ","=B7","8(G';V <\","jJ1X","j.","%sS","St+&}b;qZ" +"XA*","","28@TX`?n","jbJg0Xa#:z","p9%60","zpD" +"","z","vbB1e","?q","F","MjTNN" +"TD;E9uXF","","XI6Qh'\$2b","]jBMoDA","R?Ezs","Qc" +"'",":gj","i","N8$7","}^"," Cd%@R<" +"&SOX7e9u9N","Tv2yH-|B","/[\*B","FH7%T]r|<","'Yl","" +"o{0","C'","*","cv","v}","" +"NuGy+x1!N","","","b*","zB,]5La","" +"",".","","y[<^oc","","`#k#w[" +"Qjb}22","B^D6o*_Loh","fVyvi)@","","~G","3]N_" +"Z~|K|f6|Oa","`WE>(PJuu","B@wzpD;","x","=",";tgXMUNF<" +">|(","J]GI""5{0.3","\wQ[w`D","9079X3NUW","z$|",":lM)w" +"@/c%","93ZJ.+=\mk","5ui_N%/","]","$@o",";!FlH4$" +"xcgXX","20#","PSUlab","Vkf","fW8","0A#>""nd" +"De!^",",}qqc,RB/k","c4\","sn#",";","=bgp" +"1Ryoy","*oUI+","Cd","50{0pm,n6","kt[z","" +"yt**","fy7%","Ie320&NhS0","3DP","Ccg{",">E1" +"-.Z!:^Y6v","M5/B","%~*`Z","C8CyOD","&q]","~" +"'pgL5/0gpI",",m[ r""gGx","f7C/N""","","*3#*;xD)!","B\" +"LKi","x9BW^","\O-9"," !D/zsCKd","|INL>4","o}:y" +"MA~kMkwsRl","p","u}","EJ](7|i!","s'2","Ds1-M`Uj""X" +"qn)dj.","""er","","HAR&x*1","zUzu!tI","QYwB&>-0)" +"y$","ZK_f~&fq(b","+.","%]ha?X","D:a7ypd","GU" +"","","$/U","","4P}f]Pd","`dB2>3XAoP" +"|Xu","Sp|?]l","",":I","c4b<0","!a7-'r,o$" +")","fUNO'@:P-t","@{@)(fl|","\HJ;|m.","3_Px_","Y]WHtd`a" +"@[+k?*#QT","?","Z]zwW: ","+)1F","pc?3Sigt","" +"lk1@:","cd\'",",dEl?","H4~|","YGq~g","" +"LSt3OV","v]1Uu*{","G(y-TmBL>","dtk","LNq","" +"Bi3Id9R{","3l""?\","","C@|_RD/w21","$=BDA8" +"`[ Y.0","z;{","+n",":,z","]lo;zSh","X`!HCUXJ/H" +"vR83Ib","{g","qm->~!","`nBroYF#K","|5",";PF\rfd" +"","}}","R<`,Ri",",","=T"," P-)" +"","b[b89C_I","K^!0","hP","{r8>N","D::C~n" +"hdyQK","""07:jB{",",20G?d","\OEvWr","+u","3L`%dOs$#3" +"qDJ[=Xp",")<+e]u","!_mwrd#^R""","[FvtG","i:","C~" +"EDmN","","@[i|:2",":M'yof 'n","(","T" +". 50`Ia~Qe","@!&<\Q","=","2M|$.7_w","]# fQ""T","ZVf" +"]t","+1& D.u",")!:aE'u~}0","4HA","d#qoS","O}v]3;v" +"C","^3.","BRU1ac.","c#pHg#n!;","lQ","OF^BbQ+nJ" +"OxC%[]V?<","#p8ur9g","f%t.+viSba","u;)K:.1~","a","<2~83g^" +"de:$I","y","+|Y^r8!]c","","9Xv?t{1PN}(","0C","]0T","5}D|v\\","=|K","+5*n","!ElE^e","4.""" +"sT4-'1t","H'l","^y!~O","=Ou5l,h","?","'1r'9p

d","-Qbvc(n:Q","c\gspW^","@ZB3","I[lL6M?xA1","m:6)bH""uJ" +"H*f5VYX","+2Jf6DL0","|Jj","^K.Ui\0>",":Rc","Atc9:hJZy" +"_8)_g9pe","POa)WT","""lL+b_eK","Cgg","Xtb","R" +"<>q-@""w;","\x,%T5]kS" +"M[{:ip(O]V","U# P.AC","44)vfbfO","3","D","1H" +"MG","92Q dmDH","8K4Jn","","B?L","b4rk6X~5GK" +"R!cYx-","S39N|","VKcj3VE","{St8=9w3`w","7Ex^B"," F/XxD.O" +"+S","%","P\k}IW","[ZF(9a]bH","hf","u" +"TnA%F){`j","e2;","-CT|6([;","w","vR8^","w\O~|" +"I.L","","3[`","Bpb","HOI-H","I%" +"Ye4fq]LeS","n^","Gl+>eX+","n5c,w^z q","M+Wg4","MK1y7/s" +"","5","Z l","\R$dt66uB6","","7vtS" +";p","L","r","4:|i[\lk-1","0[?9","L-!c:.Z" +"qL2B","+","pMa6-","-7T?","XrSN$B","~gH:4u~`" +"V/q","Ne)2lTC",".r","~","bo[=","c xcta#zU" +"*s","w'Kd,(6%K","","dTY4","Ud/E[m7d","" +"WF1~Z9ey","q","2W\o\2","<=8R8{f9oo","",";AOU<#Z:~`" +"UAd","J",".b6","8DN^]3dZ=F","rmfNjZ1{","1!iS!.~M" +"1U","phL^S(k2","","R","b(z B","J5C+zG_" +"M6I<]KmFP","#U}l3Ej"," ",",7V]h|lMYE","nk'@+","Vsh2" +"1an/.:d\Aw","w)I1","*s-VWon","o5N","{r","M)n" +"N","@ta","ld(SB?/|",",","].R","MY=WFw.0!T" +"_2_ee","AjD-]U",".}9","""N3_-","Uc)z4Q","" +"`Nh","'YZ_K%","~3v>JbjK*",",aHA2T","vlT~","M01cPO:d" +"%""=/","A,KeHm","-5qe$oa","","&'Q~I","5z5P }'" +"Qq])","","`8","LJ","h","u`" +"@o#NVZ9O","w-","IsM[yR-JC8","mz\XUEe ","","z+EjF""+Rm","h","<-|R9Ni@:","","Dr{/ZUP","t_ m\<}" +"6q~","&5L3","Sp%1t>mw~","","`>/c-","R*Q#cCo#" +"0Y@j8","r26#|uxSC","#o\p]Y","!$A>,]yI","E'?-",")%=Q${7(Z" +"iI","_9IG93yU","Yli=A@gi~","|%17","""B,y",";RoMaqx" +"@wh","W'2N(O0K","m=4FW?K)m/","9W$T","^q-i""","7:r #a" +"[X7aL!X{4",".$","_;/_","|IGc=8A","r#k?},Z|D","v03x4)c" +"h,)","g:y^","1~k","j$","!-wcbhC","!mD'p" +"i]f2,ndU.","UO?yjn0*r9","c8`/1D K","szhv)V.->A","","XSoe>" +"M/%gr",";KW"")5","Oh`x","*ib*","s","Xoz-G(JPa" +"P%*C8Po","{5Z","v","6Vm1mcw'pu","D>F3&8[y","09e*cu" +"9kK@4^wz_","G","> ","HuJ4-","kY]5vhxy","*H?TuXW","PNA6-C7^Q","Ts:J","F" +"G\Sk","","I:","^JfS>YGRD","H","j" +"","#E""","xf^K>eG+Z?","7*&ou","gx>s","c^!" +"D6^j"," oQ","S{",";w;","4KC<(","" +"Lt","&gPpli6}|(","Pr?kT","kNcB3e+Ck","qPnByk[r","]j;v0PutuX" +"X$k","=)T)4!q]Q","'T~rv)","","J'E[um","IPt\'~" +"8{+<4s9","NL\j)l4j","u""69pBva>c","o0o#%:Ad","w","~4","!F" +"z>R`gOw","kD>IZ(](-","B","A","%@8@","oSAp,xJE" +"SwqY ","ts%0H","h","!>","k6W(}X","v" +"yD,Ypjzv","mBs6","v","#f","4T @@H\W6%" +"fZ*1nUi",")xt","nF","D6U:qu[*","R=cLp$r.P]","!cxpw" +"*w_njO","fvK_2F[%","isa|%$X[.","(viUh","Sh; 2W","Y[*^.n" +"","\#I\M:.4a","1np?j","{","","p,2y,Q" +"(_lTsz~=TC","veCy%8zjpZ","Qf""Vld&8}","f$`gMUQFc","&]","I;`#_s" +":BcyN","GjB6dZ{Rd_","M9Uh","XPa","s':G""sw","HT4K^xOLM" +"""G/Q""`","`R3Ibn""","PjJS","!",")","4" +"$_,~","dgvVy","yy}e*","~6`oywRos","wP:)4uW@KV","I","R}NI3m","'Yt;V_0C" +"0vd~wmZ7","","","t_SI;r$","B{G","{E*&^^j<9'" +"F*oK","(To.aG","4""%=g3","Ig46,}ZUN","W0zJL","D,7[&Jy)." +"b7S",">cp{^ ","x}+J","","Y/DwZAg","*" +"","[es\x;1~","3Fi4U!tl","","c{*+S",">" +"uH","?U+$[[Bp`g","'","N5S;'","","""" +"+a$Ms?8ED","]ve~@P+""I","7Yz","_","qI)m u" +"I\?","","<%Wu'","yviC:vV","","n84" +"[fX!\","","L|i7B?(","YO@kbC3","Bn/""","nF#*","","","T","^M%WpBd","C{MA" +"k0^","bgO<*","U%{K:bA?","D","9?l`Y}h","YoP&U}0W" +"uQ",",pI['i^","@,msdH5","/&`pE'","GO@3WP","" +"p,D5dY>F","#l)4:z>Q","4Rlx|(","BV1/5g[w V" +"*|a1DL","{{k&bY@|ho","Mn3-d#"," l%-J^","j","GV/" +"Sl","dka:`a","HS+Px","pF`'?'C\8P","7_6(Hdq-_S","%He1}qQ)" +"!)F","xHu=r","5yCOXn3V","Xb1+Y;>>)I","JU#i","w4-1H" +"cA","-Z",":y6J`t?0J8","Eb","C?f!","qTb^?c)>I>" +"(5};","i%","-TfO?~3x9","G.(~tDOl5","[qc4","J`E^c<%" +"%bj0A",">4F","r%","6^","%7oXb@Ba<","JD?t""Cj_v" +"Q","","d","5{D",",","/e2H1N","=aw?c%","}zx1 ,Z","PY2" +"yNmB/UQq","rf^""\","qya8x","PKVs","x+^ O_G6","cL" +"","ZyRxpw~","PCajf{$D5","","G&","Wqi_" +"`""ik","Dnem6JvoL(","iVBg H","","aJh""|y9","8*","$\68\%zv","S{=(a!R","e" +"cy","","UTl",">;>x+\z]"," %^","k" +"4mt","P{;2),_","^,""","PCo[","Shop","K9Q`z" +"y:X","z:","gvJA$OfYw","9c~GX5OggZ","$0","t-@k>ry" +"","C","H{"".c9","0Wq?9","4n#|","piP^," +"s-oj($O","~HX","c.BSEk=H","mUy-4]{/","",")""BaQ#" +"z","","$7?3j","k~ 6;(""l6b","Im","v" +"7""U\tJf$42","[j","[#x1~u]4#M","j:","z}kk94O",";G1-","Js","mXK<--FDb9","" +")M;#*S","i","&9;@SIX{;","{b)}]X","51","/-bN.MN'" +"""xCY","","'61Mg3y\=","$/j%Q)/","TW&(h\i@GP" +"Du!","""'Oy 'Z","b:%8f8=XlE","T{0""HSm!3","`*jxI=","" +"9","d$$","o}8t#C\3P","mYxb","%Dc9*JE1","Z`""[B,U`","6AzNGtXWt",")2I!G","++'Mb@tux","~H2E.G(\","}Whl"";$-?2" +"2\","cQ","Z27g","ICj]n1" +"IrZ uQ6K.v","x0'#:s_a","vI" +"sP+kU\","[","g""7_y","v\f$x^","!aKiL,H","Zs{Rj" +"snNQ0T",":{rD","&!","k\`TUNSTtW","4f}(7L]G","" +",H273~a","l3vF^S","]ORT(Qz","NmY","4","Q" +"Ty|R#r","X","hJ#f}Lu|","ME","'","" +"=aMV~#~<","N,","-'Csk,J","bWK$tB.%i" +"i4k[NdH`4","U=]v38GLuQ","\Wu9r.iSH>","}cLq","{ '#","whfKl" +"~O/{",":FL""7;","?:\#vur2ZX","R","Zw8zZit","ZE2tfb>" +"=#/Y*S","QrvY@wv_","0M2){","fOAK@@8","B,(K&=8","Y" +"/r aTHPl","?","xv%zL^c<",":4f7e","Ij?hV_?","0z,-z!^>" +"!1mLC&","@9)@{y:","4QF","m?*^F","!G8","8]p^]J/","D'.v|j" +"*o:*y )v@3","#k","2?j/O_m","A:Z6>`%","E.diTT&,","_rohH?","r.'_-Ysi" +"EMJ","24VWJ","I\""6}LQU","*t8e^fNzhN","Kr#w","e7\<\%@`" +"\7GnX}","","7Pd""k(0'^","]","''N""FP)Ck","-F-7dL*z{s" +"uTVk,<t-;/V","k\",".","w7#c.W" +"""+@),:V'Rn","$","*","p&0UB",">C","^2q2i,N.!" +"F|p","l3?ZvpN","DDPW","" +"NP B?[h",",$6Kew","?>>01","","#cn%1p7c","p:" +"i!","Qp+:h}xiL",".]%5""nnP","DZR6|>","-AC6`C6V{7","YZ4Tf" +"I""}eD;{x1A","ICnW&]UU","w/+c/!","L""p'","C","(k" +"G","6n]u@^x","fF","8Z. 7\\Fc","4^YIC6v1"," i:IYq" +"D9k"">J-","'K`]]V$&","","-A'{51I&.f","T!","QmWc" +"go'4Y_","lgije;}C","I+aC","0U^46","X","gwID8[s" +"4","(?'t{4",";Av@^","Ln]","1_&_r=:,","?kKf'=5}?N" +"2W}LdI0G-","W8Ugf","wfi{R","u[mW((Ln@","Zn!~4CT","M/dUo\S)G4" +"%","","|~","w%Q&","c2F/#3M_TQ","0" +"P^","SBM&'OuL","M|6\a:+$","","}","(l1Z75N'" +"3?kD/c_@""B","/Ln:ZH,d",")4ykVY[+z","ZcP","i","{D*L/" +"IdN,Lh\gNA","W<9xisj","Yt<~KVR' >",">*","qL4ELo","" +"M+VcI%+@","u<(2[","" +"V_UoQmTC2","dSLJG|","4BPdqd.Q","iR1t6","fPdd=","oh=7&jn" +";pbA","w_xz=zr","%(rE]","=Xb*O","H%","'8WbZnI>J" +"","/w|L4lA66","","Hr1AQM\","0v""3","@\" +"sR","PMS{*JD^Da","r4f!|S","m$j5:","8,f2e-wo","7D{,mx!" +">h60}P@U_","q1i1","E","2a_","a} ","0V" +"",",eT&F","C",")S8o`^R","kkeF^N^","(s4Q_:]" +"9TlG.| [pe","pv","j1gu",">>","C5v'","gNHt1J" +"fm8 r8]Wv#","dG1wZz00?k","N+m@","a#I","6","07fKI[t" +"",".s","Y",";Iz{","","wzVT*" +"4kD`9^bH","BB639","U","aE^!zs!dPX",":5B#+b","[OK}k=k" +"[6","3r7twC.","\Zg!^""*","GbT<%iC%4","","4" +"T|","0 e?9#rJ","m[+Ud<#[","fJ!n0sQ]","=3?`z0","P""~" +"","\)","r`#-qf:4","+D\5c)uLEp","","" +"""X~i._5'<","=G~e+(L","","!d5Htw","<$w_71","b" +"Zb%ia",";,GBWibWi","""@","`#rmH% g","'faQgw:s,zVCoB" +"M9&A&4x:pY","Es{})","5"," l{hN)" +"o0GC[","","Gu","cq5ud/8>*M","s%SR","#s " +"Xo","-vuY","dFF/J" +"Cire0-RGM*","])'Q2}0K\s","$)8\","","N6Wc0","/u1N/" +"QE|(","rID/$UC{a","INp|dv@pS{","hyoK&%pq","yee>*G%yR,","bBWJw" +" +","e@*:=>`B","woR4","7Q2@NPJi^","_G3","" +"6z""","]~@9","-K+j&+yY6","MeOr=" +"}e:7J","j\WVx_':Rt","xW{","$AJeiS","Z!Q/S","+=m]" +"\",";v|C6`e85""","io<1?-+","!:'N-","A,$1]2","pV","vum=ZU" +"e',.E&EM[B","57`=@kxh","):3:","u0zMA'He)","=]cAT","V`NW","Z&l&~w","ZjiEt","","","jc7" +"*=jgc+fL","","n_l=Y","^PDTwcv~x","u&)$Zp qYU","<>" +";E}","G]CXC","2 e7)$=","""","U9><}j","P","gr","M@5","|." +",Vuxw>S'j","$m[@8y",")4u3[","Ga`x>K/+>","}o","~Ot" +"^U-+TT","FHD","Z7fl","H$","OCj","t$XV" +"5z6P-","","","JS","[7<{T","/L\y=1`Eyf" +"v]K\4fOj","w'oI$G""LM ","uum]Q>&nM","","~A0G)WH","il" +"l","4","mn?","ot#c","VzP}sy65uo","$',u?;Iw" +"8[$mIq*","6{Z","WV[RiMU","Q]2G!","YjZ.W^EQSg","#B,6" +"","DS","W_=O#","^VP&:Ed]'","AB9e-<[","t" +"tt",";","6OXm[","5!d5nS",".:P","60(48","","L2nv96","s-","WM""x:%","xx@","","w@","F I-" +"{Cxh","","5P~ilp1","}\cKj`","hR}?Ae6J","z!" +"*K","|WFI6","YBXwY.bne%","]k9*D@U[5","Y\G","" +"{","am*HEtW","","xXht","g<>;rp|A","2\29#9-o2J" +"c9w$%2S","","H" +"76Bi4:iULa","qX4""J","/TJb","; ","","R" +"","Q\hZ%3CU","Reo^)yTz[\","P1","]wxJP/","" +"!M","KV)","g3O:SL VX","hl+y,aai;","c`","" +"-qR(>""","V;F+!","1","%@vN;%","""p3pUC","4!^}vE" +"c","{(.(`*rjt" +"}I0E:","","cP~&?#","uIXH2i!","H","Bn1]X!" +",lU;8d[","xbSL","y","Q f1XNt ","r*8Dha}","?*[Hg~Uu{" +"tV","Q nm","xm|.","*LI91.F@%","" +"^V3w>9d/E","@X","k'~Pa+","\Wf}","d""NRf*ex","Q2@o,:" +"WSFyMp","r_NG]","^^Z2<&)H)6","a>}""`JE","W ~iTZ","&wqU" +"p,","2jqA","^uhS&~YI_j","Pl ",";#%s9.k","r*" +"^6ny:","","(X-","E]_b7F","9%","" +"ZxI=TB>","wpok","AC# ","*yr","g;+)","" +"z[@;go`=A","+g2","f","W|M""5I","]\m1/aSY6","Wi}b" +" c+^@r","ru6z-c*9;",".Tc29]","~vj36qux%","L_NQ","R-.S" +"xMe:iI8n(","=z}(H~","oUFP<","q~6jat","Fw\a","~&Z$!-&[o" +"o~x0qMT^","Z 5A|",";","YAo[#_'","kJ9-DCg","E=DvK)1;" +"a0aM","%","GJTr4q","gg,SZ^","","i1xA7" +"zdC-M~y;L","jE%&Y","IM#dixX{fB","","sTY1y'!&","v8 GhpP" +"c+*)0","","j`)rF@Attf"," _" +"u1GDnO","0c78Lb","sE","v1+'F-oV`","Kt","+mz,h!$h9N" +"WBp%ZHHxP","n\:j,Q\yb",".7!dK","I*_Q7","'S'~C4(|Z" +"3Q0zA[Qcc[","YmxHr@E0","Ks"," iz[Y{q_H","0","l).1~" +"}{|qK","3","","&y sNI^T]]","F@]U]Bw`A(","-" +"j","1","Hg+`hi","HoV","S""2@+~[c7","" +"sfH","Eh]Rb","uC@}xC5B+_","w@","","8nn" +"'c71","","c","~':E-","D*qwlE\)X","Q&a" +",0","OR","P~loB","QM",")w 0 ""","`n" +"Ef&Y_Pp","^",">]6p""ZSY","m_lE'","g","FnJ" +"KR","wG SN._Nxx","DH4k^&","A:~C","E","uXnx:N8E=" +"M","","IqEa)X6G","{-7^HZ","u$F","G2" +"yBy5","B_u_X8","QWT Nij","rhg","Vv+:""","" +":;w","2v/H","z""`Ie/+9","|Uo*","Po,td",";DT~iBGu," +"`s!(p]1RXw","2iC/#a","ww=K%[aOH","N%Sd","aeyD","K." +"]obQQjN","9;edTJO","^es,C""","n=LqmBbY>i","tKn7","1u`p" +"%","Bh!q.z^@vK","kjVk0GJ","a:5-","","&dQ" +".","[U?pf","S4^\$Z v1","]/hM#`*","=","Q`Td4RP\(" +"d9d_","-ly;SO","0y:bX=Bhb3","RAgf",":8XqzEJ!","oSUDCyH)c" +"Cd<8",">","}8de/[C","MVdTrzCbbW","","kSKS=D1:o}" +"hvw.F","+SBS","\`ZQg","FX,O`EoVf4","4tg^!i","a" +"@Me","w:(Rx","mvlFQv++Dz","","'","p" +"""","94'6Xo ","+'",")*Zj'","g","ZcUj/[S6(" +"9;h2R{tS""!","! P","y","T2k.i*^&s","vM","{s&" +"'KnsN>DO42","coPF","D9ydC>V","E1q ","Vl!x}","q+z}urY6Oh" +"a9>o02>","X%","d34MR","P",":G","h|hod" +"YZz:y","elx2$'d","q","we;K","k*^Flq","" +"W~]","p7X","CagX_M_l",")_@","","u.&" +"","4$C>4_D)","wDh","h%25t2+","RbpE","/pcf""" +"!",""",y","Iox/yx","65","JEflds7","5+" +"M^.rY","Auvh<{_Mq","[`M_/ET0?%","FE""{SR[","Fz2A","z7-/" +"'u","]A","r~&""JH@p~","\UD.","A\}l","Q7%\Mr" +"3%","x[","","h3s","","N@ZFRUsX" +"']|","ERo`>Zs-.","]^wq`iuk","E","CFFBG$","Ek" +"_'","?y4^""Q","B_-","{s","SG","a" +"GCf}0","+E","'x][Y","g$^h3 86n~","t {KfF","","wg,U[7h","=ebag\GxPj","" +"?\i>gbAOe","p/\&$z","INQhg4)=@!","P~:;bf","-","Bn","N4=}:\L3","TSJy{","(L3NVw","hDM'L" +"GP5`LDVx4","h","Vf.","U^|ggLhK=H","py^du{=8","C$w4XM|kg" +")Xo>x07","%~V{","YT!m8g","Syisp","fb@ KU","Z6" +"YAB:ZA_3+","~TPD","Lsd6QH","58~&r","","X^" +"S;{","0I]iC?M","n+U_e","K","P?Nm#K#;Y:",",lUi" +"","KpP","Pt l","}I{bot/ts","=h M","d#dH5eTqf" +"?4c46%$y","#C$Ib5H-","","*~eM'f9","","G+4Ut""9G" +"dL7|",">;","Hj{fQK^a","=}?","zX&<@N" +"t","NGgwnD!","(sKfr","33hqVpX","](]`R","GH_|}x`""O" +"zzzC$T^EO","FLYu","&0Xy~`\Vs","",",;>d0ag66B","d?#" +"~","OwFYcF*%","P[hQK3z","SZ1[9xjnu","v2x{"," 4/de<" +"~gi5\ua ","D`1C)","s_[|[Z[jM-","sAzDoB3","'JZKhhKU","qi" +"/r;?fl""]","wkCxHw/@bp",".","]f_&**_D","Z!`eD","BxmNgUKQ5" +"UBQV","fM0]?","ea+fC)B$","}","\v,]c{]<(W","0\pw/8uy" +"w}duH","y4msg","(D=OtUZc&t","aDYIZ","k""<8S(v","9pOU|","Mi","**""dU]" +"_cdC.`/=","R%na","wW","Z","}UC},.S","L4*L" +":e%"," w>i","a|%7j","=%j}(19g;U","vJen1j`75","qso" +"7X:HuS","hw|Kd","z","","3hz$L0S$T","nayBvnNzl" +"UU",":{5U*TS$V","@D)0I","x$G","^mum/","p" +"?V]J!G4c","t","~]O","wk|\ t)M","Nm7","Y&m" +"OR+TUs","`","l2u:a%?c","'d4f","' w|","{" +"7NS6r[@","m:59(k","4pT[m9i0Y>","","6A<","M!X>*{cQ" +"","uv[<","~!uMx=m;K:","KRK2|","j5","`X," +"","9pY7D-C","/","CsPr5M_$eH","G6XJAu","$0qg'S" +"rzz","$8W[ejr$q","","b\t6>~{~%","z0L","=" +"(""C.,yOR*k","E>T|",")6nK2E>","","Ma)vW%","O4" +":Z#bMS'K4","B1R`+8%,","[zNJlg?","b}","t<.c","=e}6A'ZJM\" +"zeYk'5","-E%??fT","~3NrD","","0`SzOR_","&nF||\P/!*" +"j4X6t qZD","$&","VH,bX/iqx%","Lb#?nJOL","O@r""","~pV;d]=!" +"","/6S}WJ:","qw\D7&}|p(i,9",")Gt6|s","36lpT@","iuQc" +" WpI)^Yas","FeXfJ*","lzljk.t","fD?Cq","g",",(H1Vh](" +"q""","","<'az","M[mi","*%Tjk:fY\","iyDF \L" +"7T*uj","!ih","rqXO","\h3i*i&","lx/cL,Z?H","}Wwn@Ug@y[" +"dDobT>97","it","SUGaTg?4g","[~Rn`Vqq","_?oI`G6@P","fh[" +"i:9oaO:;@L","XFt{%Ll\","?!7|L8BG","l","ZKJf","","O","17MvwUY","efGn","pBx{R-}Lbm" +"$1","t7Ra`","fGX","kVD4n","!C4Kg>5Izk" +"r8=NCPVr","t4@7e,(DRF","wg3a}","bUXZM(Vi","1-S.M","Yz@" +" X%v5j9",";*z","" +"ck7\C","","W--","3Rkn.XS","IOx7>f","#HRJFU(N=e" +"jU""[fymZ$!","","As~1RPB","*SZ#M3","Ea9-#7 m:f",":]fiXKd" +"$`Y8bM","DEnVz=&`","nG;_EWiji`","((jhe` uGB","2[d",">#hx3cxOS" +"C*YAih^ViO","hOF","$Sagt","qC(1!B%","C>>Q*UJq0","@" +"Kz)HE","*Pcj4Us9sx","VXI]f"," I!Otau\x","l9YKUpq?","" +"","CR8`","rb>",">Q\|D7n^;","cv~","fP(&" +"VhMQ","h5E","","\H1r}yR","V*[}N4T","I" +"{8p}*]NO","HZ;","':cu-<*d","KG>gw","/(VFB4d","o$*9>/rQc3" +"aWY","Nf:N&R0pR`","+(1*f4","-uyDp/*Cf","lMF#","B)KTS.ZV\b" +"S5Qh[8ox(]","s","5iG*?U;","uV_3,","YAzid]Cmg",")" +"r6$K+","'J","fR$R","K50uWL~","9~Ps","JKV^m>.9","CA?A]F({",")hw'$gVN","" +"T9'.#Q8LO","u","_65","|6>l*","0_""""","0" +"yEh^`5C-","Vm""D","qO","d/mS+e","r[z" +"v9hk/Qfb","1J","%HSp","","DlO-e;;",".*?48=:Au," +"F=(tb","w(B17O<","kId-""6l","","52","+gbm,z,aX4" +"'=","r&fL2UD","5,","q+.","U0Z6MxY[","}" +"CJ)O-","#E]vp(E-","#y","nN'nfp","?e&6MCC/","U7k","zt>)!","9" +";s","hgL9","","Dj{n","x?|PL.b","yac" +".6dc","Yq^im","|","1J{","","" +"Yja""M6","`UO1w7{pk`","b^h]=1N","2p*cdX68","u]|aq;ol","g^ UX1o-" +" 9V5}q3","Gf","","c","i","*mBe" +"k5Vz",">","d\?!*+5B","-","q\5j4","7 ET'" +"\&""""","\5JE@h`r*]","","!Vf"," ~","wF$+`536^WQ75a","fBF-JG(5U","XeQK!C""d6","D","7JCH" +"tgv((?2U&","x","W8ddA""_}/g","&h*s<","Z-t ","8$" +".VJR=B1","xG=`]s","yH$j","@@[W","R[@P3","( =0]Cc3@" +"O$r=/s|","vt:Fv","","V{i@_1","l.W+-E_","b!" +"5( DQc*","'}AV\",",#i{):#","Q<&fvv@ @","QQCLac%v-","&_pD" +")Uek)9j","I%GZ","r:qC@","QG5S5@","FB(hOn,","#yT." +"","UUN:f9<","","v^7v[IQ=","7","" +"4n","B 9T+;6Op-","U%Ug.:W," +"4%MMt97]3","tf","C-)","3={;CYcQ:C","b#ZrVuwu=","r`/OZ" +"|LCn","LD+S_@C\",";","&qY*","sR-","(i"".`2j" +";I","K;%","a0OMM0","J$A0","R\c+!#]","p@Jaia^8w" +":-fzi","jeDk","^D8`","{g#U*$,C","5Y/3jU2l2","i!w LKb? g" +"g)^","v0\A","dA7n A","DI","@-^4;Ng`1","" +"Q]WZ7ePI5g","oWTo6V|","B>zL!dKZ","9","&'_@SR","KOr" +"AE#:0A^;m/","f","D9","W[l","Ulp}}ysx","e*dsWDA`~" +"0:kJL","2=_JO","i","z[QRG^Z","zzu+W","u?'];_[?,M" +"Qx@z0-}m","wKFe_%","","2Y?S[QE61?j","wbvhFqO","3w_sgCNb;l","LR" +"\~5v","`z","","kEB}","1?]?=CX32","S" +"sj.m","~`22qb}""]","^F","`COA","@fh,UR(zU","5lx6pvQ$M" +"Eth>1lx-","d[/Qd>0","Ust-""z8","w?","[)*vGi","[x`.|," +"v","dC","%""","x1i2K.p'=","E0^&^-","4Xv?9" +"QP{0+*",".'~|$[O-rH","d]Uk0{Y5*]",",F5AhPgNw","s4G:9Z^(5","?8R3V" +"}!&rF","Qd>(EU:K73","I)=","-_EO=>","q4*;","!1" +"48`\","+?D""W","r/Wrjc~3@y","T","*{:t]e","[Q~tiplY" +"jaNna<+@?","< ""!%u","","agI1IM","","D(n8(7" +"","k","Fr\{*J'Z37","qW=[x","QN)","J1" +"S9fMPCZ07?","r40rg3A'","[d2p","1","CJ""i",";3;4(EAn" +"4,=2?;A$_","","kdA",".fl5","Y","c{z}AO#Q" +"S,`)>?r","bg;dqM&^9","TDJ^}OH6!_","&& A","utD.",":Z~GUJZo~" +"[:UMq6%","","Wm6X VpV","YjqrGsJ^","G(@8_","E" +"X","Q?:","Oo:q$S}","I/`OG*7","","e" +"P?G}/zAPqd","pi2$J","stk>'Ec+gZ","RLzg2*","(#t6RJ#v","/NEi`!3*" +"}",";O","","xqEvY,Otf","4VeTY|","Y89/" +",'","\d>y9^o-C","&DTe4","N6kMA@8<5!","T","Sz" +":4lcZ+&T","^v","X6pZOs","!/z",",",":xst" +"<).Z>>$","R","xzY\>","B7","_","o[3:ZZo?" +"","&>=","g?$aElH+","B9[","O!1}+","R;0WgK.","%o/Q9y(" +"Z{F0x|=lT","V>UJ#7r#v","Dh""j*^","`","&Vk))5I2]","'3:t(&" +"/Jb","`xa","1eI89S","","v;mdlg)mh@","" +"","J/'\+","","kN","<","@WF2v_=%rc" +"","|Wm,ob/","TuoB)K|(@E","ZP","|f=xY)/#U","Al(D","+",":iX" +"*n","ug pb[dHu","[aMO*7Zwx2",":Q@U!w","p",")JPzK" +"+vf""8%[T","Gxv}?","/YsX","PD\e\DZ,","","&,`z" +"7G[gJK.1)n","t+YIExD>@","U0>$NA]$","yZ*L9-2r","|t.L9U}q","" +"-,hT>LR9q","|0y","&","=Us7q^E","E""nt=c","XtT~" +"^",",_J:XO9","u-Xl$(+%","tU)W-O","j$,$a%(\i{","G|O" +"Mu","/j`.5H^>","","H`nk!_Hug&","+$U]-}M","MtU$mX^lP" +"(4gk^AxHGK","etSi$+","TG","hs|4I*Iv[)","","|&78" +"","{sjW\O""","V_","W]AlB) C","t>z`]a7H1","=" +"","kT","",";","Rz>_mPR","H]uI@s6fU$" +"j!3tLX}C","\N|(","TJ,nx","y^C","z","6()?O\dBj" +"}1J%XD26","Zm","a","q29b","pTH3","mt]tD.b\XE" +"C`","%$L%.xb\","MT",",","%rz","+SaFh@PJ1" +"n!*5q","","EDMS&""","","T","" +"+","sW`EfAJ#6D","","c&","!RG3mS","}a0qwjc}" +"","4","joB","8BHb]Cn",">`" +"`Op6(CdKN}","NCH!P0k57","a|!O6*mi","U""%yC","""0G~","C}CedO" +"\zz;0","ZJv","-~<}re","","sa4mu","!0" +".=O6.P<","?O$ss","U","65D>X$\{t","F>V","|VNqC" +"l","U","3%lv=e","xNa!ns","[rux","'mX~o" +"!Fmx4V","a.ik","1q","l6HzDDl*)","Y0vi42]`","&Egq" +"Y","r\N","FF","|2[;","ez0H)B","^Fj*:_" +"pI=","",">[9vK","a","","@,_IP=NR\" +"A/x<#","J","O","h,R""zSi@y3","QD:sQdEU,",".W.\V4L[`4" +"M3^F","0""CD8-xd","j!W0","XX;","j-u8OrgGw" +"lt2cth^R","","8>***B4o@'","_4","Ew? ,","3j}qe`" +"~","BL ","f","o""r J","G^f/,",";)^7" +"q-;:D@GkwP","tw8O","|8&","sDgR","w{eHl=Sl","$}" +"hlJFk"," ","\ uI","8" +"J#;Q","\N/h{","u(eznIXEoE","?%g ","myydNO","cd#L !" +"PrJ)T","v","dh' $>","U \-@~m","nG","/yk" +"$L(1;",",-3n}J","a_-d,j","3","]","!mY%jv2" +"&'Ry",".4","",")i]rP-","4","$""C#-QF\E" +"L&AIt","VD]\B{f/","`is95zj","}F8FY","C"," " +"*(]e(1P*O","3VT","P","}l:>s" +"x!?LmJ:sV","^Ce@DW4","5E(3FFNc","tUd)Om","RICDpT2!S","&Th6m)W)","?veTq","DQ;""_}i=b1","'g>" +",miP3t#_","aonhrAdul)","""Mra.","&c","","DZ" +"J)PLS}XL","jf/&)\qNZ","Vh","r=U]GS^","p<[hHDj","6(@>.O0Y" +"k35`p%+~F","Eh(J","7hek_","=l]$?`_","O4Ue~","Ob98qPm0Hk" +"h","~:l;}v","(","Q>!W@","&4wfpNAO?",":hwj6Gb{IZ" +"36jjAw","`[w","(,wN^x;j3B","+#vX3wFS","","d$3/?" +"xiB","T#rzlr}","BSB",")","o_2m2+Bm","~r<4eA""5$'" +"H ,uT(.","f","qdyPW(jE","xXn","","|RI|" +",'K","6.o2eFt","","=?7Qfe6","`zO?","+(=x5dRi;" +"/iON","A$","^X%l4Pj$","E$f","2","aV" +"2-#ki<8(.","/h{4}7joC","gu!2x1","8(Lf4nv","<","8_R%H" +"d}{f)k]dy*","<_)flkg-]1","2/","MIE","=R[C^M","(VG+" +"L2I\> ","Pb\I{c""Lf{","s?z(,PK+_","Dg~,lDuX","Tx-T","fG","bf""H","[Z8I","Y[","+D3^" +".6f","y""-(o","Z#++~<",">9V>`nPz","-nxW^Mzf@o" +"*,_E*0","<7""j&[O 8","#k@)>,o-","Os|","][3",");n" +"","%p?D#5;","]r@r""IDmD","gg=Y","f05fQ&.}#","it(M+o@_B_" +"hgkeED","uHh_ra({",",(","/","$J#.>7","$K~3" +"","=-GsX","kTlfow","&k|!","lbUA","-'We" +"{j##","?2^-2`Z9(d","13|x\B(Lc*","m9*B#","~7hy","l!q5" +"TBnW,\Fe","uaOtsq","^ID7rR%","2;+wvT}c/","6&&h","59CVp:" +"=f|~+H?t","5vPij","~;e","z8UWCu$\T+","G","E>H0" +"1Af","`O&J7","",")_n;iJjz","g=n^hSUF","mk0;3(VU(P" +"UXiAM=A","","@#","xtdXe-aOgf","_Kf._Uiev","FmUtmgr;" +"TePh.P","7AV_jW","","",";8gh<+e2","" +",nO","ZPrOobHD",";bvU(F!qXr","0W;t$Lb","@","D>4K." +"","5","(L","UC7q6""""4b%","C","T54be4|NQ-" +"PVYwa","1H""d;+M8O","","","40GHJg3%","O" +"x5GywI3t","*%","eUJ 9mY","tyO","6lKipQi","_Z>PDO|" +"X*j@","","E","hs","~X_Gz^m",")" +"Ia}P ","{g","-","hzz8[vQ","jB\X>#(+""","hDy" +"Dl_4yHk^+","JPSQ8(cqSq","H!5{sSA]g!","Y,:f*2","cD{NE&Bx'I","" +"","RjIaNtq}^3","Y","","RyAvo","4=0ORc&>4," +"B5","","*(ZW","","J0Yd`","{`zwlvi" +"qEy",".'?MP","""})m""_","""4jy2)ac",">bR,ZDUW\","tP:.z/g" +":F","m8Ema_","",";B\JGV<",";O Nr0?Q1""","4:Z}" +"=|r.\rLwL","0YN|3|gLr6","Xt>K-'L","OC,tw_<3~:","",";dAJ""^QO:c" +"{","]_r;","|",":jqsuf","k7~;V","O" +"od.6I","rK(#","C","$&Z","""R4n 3?vmC","z~R-B.Z" +"ODBbMnT","{",">)LQ","#c,K^7>#","BS9]","eC""" +">","Lf!`","@iL","iVb@EPA","","eu" +"2\*u]JVd3","x9w=xu","_`[7#8XRQL","Yp","~y6P","@W5v`\jRy" +"vh2","I!","A.RYB(",":u~*oxp@2","P!q","[.(F'!R" +"N?;4bJIYx","j6TBnK","","D6CY5","","D" +"| 9${?0","ZnTB2oM","}kq","e9UEGq74A","~","UP^|7" +"*#/)","oW$F,wL$ ","","z^C\N:","","lIX" +")!lW53=$C","!EJL8==","5(","W(TR2o","zsh%","Q!lb. mCQ)" +"\ZV5{gFy3","k#xTOtdqCH","Q2Q.\iu)","NF%x(","M","o)U" +"zeA,5G+","#Vq=K)V","!" +"{>^@j","iTEL|","Jy-_SU/'wH","@lh_","]+l36).","csK0M" +"wQL_","L","Kpef:","Q<","15ho( r","c" +"^(pD","\V)%","uV# H&%)}","H]""#_GU","Z3^A","1""" +"][SBqb2","sm","z","fj]/9","","@J>J?syq" +"$>pc@U","Pc\",";+m6b66ydo","+?jE`.\Q,2","{iQ ","" +"63HSx","'_+eJi","yH",",vWtmMy","Y","xgPWJ:" +"7l39<","","#:SBey#j0:","n1r;{)","B_0C","" +"d","a{eaS _W7","[pZC}4xc","mx^&c","6Xg?#e","" +"Km","x","Pkc*287","60=J","$ciiq>0","aOP","DyL{VAko^2","nCx;^4" +"""AL/","Kvu`","?Ri=~\","Uwo)>>GUx]","T","PUlR=uXl" +",-Z}","NS%s7YSmx","M\~G!8","Fa6!RE!%>","]","AC" +"ITrnuX$z=","Pd ","TN{P]Qx{\R","%""FET","cC)muo","b""7W]?#K" +":XOT","J""mLD\S6&R","]F(XDy%V","V","[72S_fq","SUBd:2#;#e" +"z1e4l4","&:","(Nn","Hfu&s*","g{[kt$5r ","}E" +"","`KT6Md","JA~=","E^[rtkE&","q@(|","&" +"4p:C7+U","(","|P\;TB","/R4I=]i!&","<","[Ioaxib+s[" +" l@i$?","]KeEV{^G","c/O",")xU qf'","JkR","O&\" +".;F9","-0^rp","`n& ","0@`7+!&xS","._fu-y]$N;","i/" +"7:!%75w(","g&W","0n}}h","GecP&'P","y/.WA,","GMh>f[Ub" +"'|sUuHc@","|)JV07o","}>+;p++m3o","1z:],\","[PL","" +"xS6&\2F","vZ0jhcZdOM","$","","C?$@6 lx","cELb,/QZD0" +"6O'^2X6>LY",">kDfL{T|Jq","rq3wv&m#|","GU ","GeD0$","H^" +"$p'v=","","77#","vI+TaW","kU","vn" +"Z3kq+","r(N","ovb:xf","Vk",":5&':7iD",",c}" +"'","@v""","pC9[X#5bLZ","cannP=c9h","6'L0)#KgA1","uFTUYz" +"7y\","Fk'","e~0ucbP_8","}1!ULDjnev",";)J(L","lz" +"eZa6Bj","NY>>E|s=:n","08","G"," [^i)$'","p9wy" +"j'FE"""""" J","","+Y$YLVW","UpmY58Z",",|ot8" +"SY","*:i6","K{RMh8d63'","40SGVof","{~7","|]LrN\)Qf7" +"[>E","s","cG","D6D0nwmqc","""j","QV-H+=!5p" +"T/4SaW","""O9w,|d=","8:5VZYfX.)","8;)H","","""BGhSH70" +"EgC23","&HG","tOT","oohuHfm","","f3Np*4V" +">-wot","pXL}l","%jI!-","q","43SyeR)","&>+V0" +"z","JSqgq","G)7d]o2r%","B ilJ.$","=HVoj""Uuh","ilIlT}^","y!R!}" +"7M+AnR","","V'r","n<(FlA","uG;d","4z," +"CJF|q%}j","<","CxTe)+!!K","rVVSu","-1 ""","t8)A$,G|R&" +".l","L`","ZHr,(","f@DS:",",W))Tu [}","XptY3~qqvx" +"=","sY","tHQ1;~F$",";'|usG<{","/./+","H;PD0x{" +"$T8[+?Hu0","F'RnV#NHX","","+d{I5}N(i","ol?@","s0lmSM","w","54VRh","W?c_" +"~h!,Sh56D","Bl thgiQM","{t","_k[nc ","A9.LZnVV","FIk$lY" +"vmZclCsBW","e=C;6","8{/x0Kzl4",",6fi","v'`ib;v^]","JZ|kZ(o:2" +"HkMT>:p_","c*WOX$i9","?","ZTjO5","J704GZ","Hgh!7q" +">","rk C5&Vp","0BK\]{n","px!PiJT#E","q),`J","0@f8[e=#" +"!$d","y+.<&l","3!]CAr&jU4","","CZ3v'/6;`","N" +"kovwCdyB7S","P","t|6&hRY","@Aw};","^:Izb=vz/n","s1{_" +"]Y!ZV&r","Sebf","x_<","Jk0(","SLK","j" +"NbR]CZ","G}q}}i","y","T","vj","eO" +":Oh,{`W""i","BeRUryiixF","U\]d,YB",";8@j(]zX`" +"?M8=[x1:","z85dvP,SzL","2w2E+mH7S^","W","""\$5qvEF~:","'""2-wPFw0T" +"CG","<2&;!sbo&s","g","D=d(t4O","qb^&CgN8b-","($ea[u" +"","","j&","","#G&+s","FK)","2Wogt","e""(![yggn ","?Po\r[7","s[{f%|W","" +"jpe@V[(;0","{,","~Tjexb","*u|jJd22(/","6v>w",":","","]fzYg" +"yk?t","O)*'%fQPq","nXWAcQzL",",++~/VV8",",*he>{","7znYjvFD(" +"H?","0wH4C%z1be","}\p50","P-xf]^]-/)","XP?[q","ki%@DJC(vW" +""".","HE X)s","{.G8p&","<^yu_9#=g","]b%%-9","QJ2MWO<=F(" +"","|jvCB+f45","'/""cSFn(=""","6HD2","=o=","F`HNwtt" +";%VJ:V",")kd+esu~mf","|N]d4sT[i ","bu-l",";kV_K$c478","98`" +"8Y#]7K;B","#DRFsv9","a","a^TA]L>Le","Y,*Msah:W5","xkKLzd \n" +"@gx}z7%y","6rcx'WpG`=","0/)Fe<","#+A8W","SM<*pl(""","xDg " +"2I?","WU@","+.","3 t","{#_","hP+8D1$L" +"T1Z%)LG","Be","f+jpbEpM$X",":LS","-'N$B1","r$;>h?\" +"","@s","O9:B","'K1{{","oj+19Y;Uc","" +"E}~!z","_!2~K","h\#x^v%","LpxO","Y$","VByN" +"%qty&3IdoX","q","{5xS1+@yC/",")%pPx","r$fHG%Sa","" +"Xf13,Mjp91","(C7-p5%sw","@D[JL","MAmWJ","","[u3?^k4" +"Jv","`F?U","92]","","B:","<+sO|rg" +"\IO9lcAD","IZ*VuA}qxb","2\#zO","F)%d","3D~HCj","bs']M" +"6d6V","""","z%Gc%T?|","^XUF{d","wj}Rrg\Vl|","""/fQE+0J" +"V-()QED""L","1y","r","UDV","3Q>0GM/","","&pR{qC$","=q","Qfu``/","#yA" +"W^R&4","+","_HHxJRL","~+Mn'|gs","9[SQh]","wNzqV}" +"*je*sKQT","R%X^S#C'q","""","-f}^2\6","B!+u%gd""yB","m" +":<","T>hT",")P6Y3","C>","c_w$MySQF]","N" +"?\)H",",!I4G}$e8","rf4|=q'E/Y","\@v=V","cAi=a","3pJa*[d-" +"Ocn@Z$z<+","?k","","wD4MH1xc#","_byG$% t","!C6E=``@*" +"w6$B","p","iP-","]cj","hQ!i<)","erz=2C%,." +"Nw7^`;#3b,","JG/$N","!aAOC","\","~mZn8fb<;l","}UK% 8QL" +"^y!ZvbA:B","Z4mZ4riw","X","fzgiH^~o*6","E","J.&7" +"@6H5",")","d","70CM","y",";xQ@?" +"'","akxgy>NC","UJS","fTSh|`S","""%""isoHd","" +"pT1@0=x","p,i","KBH","w;KA->iE_G","ZYm(","+k=4D}6+H" +"P4Au+$V7Y","o;%>E8","{5",">","\","" +";`""H,v ","","0iN)","qWS7\Hj3R","U:\p}]","0*Su@" +"YBik""tKo","Wv_S4UZ","q?-#[Rf/vR","G","+(","N$ea1" +"Q","^R\Wx","{","`;$$R;}uX","n@nz?6u","b],'zR)P" +"haBm:",",1$","YY","","E71%z","" +"@","Revc","4sScb","fy4m","D=i?10X}P","|rtBrZgcoH" +"8yE4fWPl(","#eq5`.Tj6}","[/)","","","7F*2Q-" +"((K'Rb","hb,c","@i7KT)qJ.!","&WT1$W+","o","=NtVGs/z" +"P?I#!{k","p$","","?Ark)YUq6c","&NTxt","" +"V","W*M","QhXxQ>ZM)V",",Ss^","mwUN","V" +"L%pOM","s&%""e","|.0","","W;kC`a;^""]","v0x2s" +"Q","PHd;","y","p?)ofu BHd","+8","H6q~M" +"f^","9/","[","","9>3MClis","q"")@Xk$^c%" +"PJpB_F","T","","z;8\=:5LY","!)M4N_KS|b","o" +"","-rMJC","A_7U}#H","""cG!NW+jy","x+{A","UiW=|*" +"M","3p","|js&~(W""o","m^o3","K:oWx'}7FF","vKpA?\WR<" +".Z'Toi3.","Kw3M5'","D{)`J|p%","y> <",")Tzjh?/5;""","hkHe2dg/" +"F","","YQ#bjKU","|oyIi.Pn","X%lZJ%G","4p`$gA" +"0<3BcF>","mj#?'cr","?eg","?c\gBxpIL","C^V","4h+/}>2" +"|Gyedrt","&U8j-39",",o{OR|#K&","e*+{TfoLt","7#EHo&yK","-l),i@5@sy" +"( Ru-","8YA_.{ lbA","B","?1","Y?q18G]:","F" +"`"," {@yUR`","N%sfb]._}","2+H","a?(","~488\v,~" +"e%}/?__","1m|&Zwk\H","m;y0J","A?&_bN)","*{YY4g mOv","" +"+o+1~PR^rP","9s:","9[","{","","vSm,]mcX" +"v:Uc","","zmyt","D?hl.Q)_ ","M3`U&^u)","'|b0""I" +"$!Y","`l[rU","P*SX0*7ews","E4tEEV","r4Mc$i?Y","x`-43E" +"XFz","ICV{D","eFh{;F.:","H}L(F8","EFq","UE:7WM7E^" +"","","/",">mK|c?""","fFRcp *!","O" +"","uP4b","","+","WYE+","@*|s}GBT|" +"-1|U","; 0i3FvNS","","U",">Z?`X" +");UmU,5L)U","X0LaKcd","o#"," #f5m]7l",";Vk[u","YD~H3]4 f^" +"@<^43=q|","9J3_Px-_),","(B4s","H'E&","Azb3n","nkLm%@cG","%yh]S\#nbQ" +"yl8%1 Z<","%3+}","|ElVJ","qYMWxz","-O2E","z" +"/~Z}]!Y[","^s>/.","]r*","6G>","W4-","P9%%W28CrK" +"f}hbE",".{w(H.sVl","<","+yR;f$","Z_R","" +"@""^=KMw+&aV=","q>SH7K","X=","YrPU@Qz}" +"R='#.C","%V7VRk","m](y.J",";","],#","/" +"j^;6V","~aY""ZoE","-4~pU""q9 z","Hfy't40","%k$!cb","` t f" +"T,Oyx)dBN","E,","iOWQ`","qUSyf","Gic)PI\","4" +"","b7'-v","mrR""[}68,z","z","1~LW","MEx0E=" +"$7","=","OPyk[tg","T|Jv>","","|}MiE8*\b0" +"xg?ey","r&%L9`&O","E6V]U","Uo^lp_","Jd#","JHC" +"75r","Dm8""","PKDe(jR@","Q|3]{I6,~G","g","x_Z*}" +"+.+",".Q"," H 4&i"," r9x;]?","L$FE#|b`%d","Km%5*Y,=Z{" +"t),H>6^mo","T]v|=?W)","Gq","U6m"",","Iv M","STWnPUuF" +"S_28GTA$/","cy_m(3?.","%Esg;RS^F?7" +"*Lm*BZ;1","","{}:4[j.","","T","w2e:|" +"bn>vI","1wRW=$#hBO","l","aWbEFt.","geWriwB",">" +"[,2""N3N1F","2np3C","F>HPSX","P$NF3,N*\","`","%." +"j*@Vckc","q:a","""5","Au!lI)","","" +""," 2*7g,dY","","zcz5O","Jd4",";Q~U7" +"'r`G","0:-l~C%",";{6bGzD","fzc""w#+2hB","5 .ec",">L*.c+9" +"bLrT9|.","&","BuGS3",";W-;+","","Y8Sg<5AF""" +"t[){H{C","7P)M"," 8J","]G7","uxnCx72","#" +"9Jpg;,S40U","7~,]","pB9I","""F","O","KX<" +"S7D+""Ct?,m","K3y/tj","","hBn'@6;Q","^*,@fi",">I)JxVCGjS" +"",";IC]JIRG","#@","K","!`wp","C#!Lq`ouC" +"\D&{2<7","A)vo{?u","",">","][F","|" +"o,h","VZ,L[K/a?","""%","q'RP_={?","}L]?","/f#W","Sgg`" +"xjzKX","","FgVv","UP7","D'","" +"&","HJot_~M-","Iz","","hl*?wrgh"";","r.j8*" +"cw\34","y%sO=Xx","!'1b!YH","KR","k?daX23=#","R/G" +"&Gd_P","6Uotb;w-","{f,ub","qY0#|=g","Y+@RF3~","yF" +"cyj? /&fo","(o!i","M}W1O","o!sNA[HtT","E/<%Z !Z;","gMGl#t" +"_FWHy","0=|0""<6jb*"," {l7jc,h","","RE<0N5lhD","#msM7=" +"M","h1>y","Rae]'L6"," WxK$",";","WCJu" +"Jn<","Mx/~Gb]cE,_]F" +"$(GZ'.a@|","P*","F[R@~","QC""nTGV[|","Gqx","" +"B$j4S.QyR "," @ub/","Q;-{Rg&","=","mp}cqJ" +"K",".;p-:cf],B",",d.ahp?<","Ax2'E.d2bW","","""N" +"","tmw#$!W4","X wR|2","J","M`py!r9-","u*>" +"Uah","","^vGgUW","_a=","q","?8" +"Kk!","3","-dAP]|WlTd","2daZ 4$M","Sm:%J|J","|4.Y0]FG*=" +"T(zz47nK","rrW!","+m6&","$>O","*b?8/","3Z&0vdO" +"","""1#Wzhd|Q","znf","Ix^_U","B""!<","]" +"YD","j+P6yT","}qELaS","Poi,1DYx5y","9","JAFlfy39F" +"""_A1","!Gz8FKW|Q","gK3xAlVP","h{[J]g","Mp^XsE={",",~""@{O=a9" +"|5}%ou{h","","rEt0m$+","3H}","/agu""#I(","ZA8" +"rTg","C|JOy""K","PAiUKV(>sm","9d%*oE't","k]xpx1C","Mn" +"FIFq\ +","t","","PF?4J","KiV)9rDEWa","&!!a" +"HZF`-Bl","*3T)/2>5ZT","k","4_=2","D*&","T$Reie3o" +"[?N1^","v_qS#k@5F ","","qQ=JSLC","g8J340B:","VpJ" +"#","8!o#?9fD;W8[&%","W5dd*H|","w!+N#2'Kv","","1z" +"A","?U-:+9$Ad","fgG(5$xfYB","be{|1\v+8N","bjh]GeV","U" +"t_q ho","IAN>9OHx ","Nz#i;","","","q3&Ee&4KU" +"k","e","1M@K)D","<%]x:","g@ucMqR","cEU" +"h2{z1","?4/","","","","a<(" +"","l2e6","CT","S7C4osiM!",">h'cl.t/X","yM70tqVeJ" +"5W~n0\","A:&{Ct","Q|~","!j?Q2-","","oZzcS|BOk*" +"","@m9W: 1PY+","d'","T6Z@XI8#","*C","{WY0" +"c|oq^LLWk","c","v~:H","TUjFM2W`#S","+J>E","fRd44" +"","-k_+,H-P+<","j,nLk@","wU16tnz","IB/(#b","],}l<","A(4{hV\S","EU1fDRJDK?","th2M{@VM" +"dd7'7F","u","ez[F\=/","-ND",".%{f|@r@J","}",".r4/b[2+" +"$","|Hl})","mAi^/0%,","","M","42Q1zn2Fj" +"","~kIZCuW|Y","@a;&\W""sVM","""js;V","Ty""-","udU';ci" +"","C@l-Xr=",";4o","FAciX1_.x{","","X" +"Xo=}hYQA","OdLO5~D","GOou_fr","%h|"," iaH)6MCG","Pm?|EKwe=" +"reI:O","rO0","geUs",")","$1'{C)sTH","dF^m,s48/" +"\`A","iCt&`","SQ%x4H>","S","eNJB","F$v=" +"Y4@#","Zx","b;Lzw52FR2","WR","w^21u"" ","c" +"vza hr","h*"""," vr","bu^3R:>tx","'","[" +"","nz(Mu","1Y?<","WY","^","+R.'D7~-V|" +"[","M_0V?}g","4&yc?/.V","v+yw""2N}j=","C","q^G*d<#cOt" +"","[G?YXdCQ8","y","gS2","","GK'ws#@b%","gz_L:!P\R","yjF","K+j5dt","""u*c&z","H" +">ccvc =Y=",",lfu","ob[@GMfb\N","DJYb*-`""","GZ`j?Um~E~","-j/Wl?!}U^" +"TP8ng:k|c","","#Dy[-M[0S","@b'O1rt","fs'","wm;YJd" +"g""vzo=7~j:","#C'.JT","af""<6"")","Uk8u","P00^t:%kI " +"fYg%",";'EvJ)""",",g1~Bn""","HOgS(*iG",",N4][1","`>Qg7'hb" +"j:C^/D&","L}" +"y","3KHns2","yphC]>","g2^","iz","c49k$t>%&j" +"_B5~%W4",">mYNa|)LQ&","|h`O3F","$c" +",B..bVR","PBp%ghU","i","6-n;|w","(?fll%2","v>\w7ZO}>S" +"+}8","+A^","=nEbX;T=kl","/)r.",")7R" +"c(TUPDMq9","S$Y","!{H","ar&s""}X","]n9^ynwJ","KA^XuPC" +"G=@{ |4tp","x","","Q%g'bU*>ko","","8/;_x!&d" +"Eye%U sT","D!vqs,",",/-dIm['","b$6ns","!O4&B","w2-FQ5" +"","vb","Ag","Iz","xAq","Sg/0" +"Ci","Gr_x","rZ.bLnI_n`","IF`&&","<\8x","(Dgo""","%RK>]iM|","G&N" +"g&","MuXViHSk",";sqY..}Qa","]x6","GQfWk/|m",">+{6FTcJ" +"","8eQ9T+","/#c-U[..","6s{","#_'E","56E" +"tJ{)","IazQ","st","}J`& ","pBX6d","7_yYf\kW}" +"r[d|?PuJx","","HHK1 >X","uC",">WJf}[86m","1#" +"FBzo 4PP50D","x","a5=Q|Gp","VMc1PB","0pWSRoYZx" +"#Oy=/-T","EN>S3I^","g`","hKRa]","tX","8" +"Lu&","SI","|>cxM(","B","&HK#%-$a1","Ep" +")V=@P+ ","bNbg","W"," R%w","U-;2Y","" +"kp.~#P{","10C .ckv|v","/rh_7j","vC/","Be6]Ah0s8n","8_s@]","K6EN","JIi,""bWH;g","IpmjQ " +"q3""hoZ-","u{(=+GD!","","","WWs","6t[" +"aL*GReab","g","]xy(zaP","f","=5+kzD","|l8&" +"{}Z`o_","2nro$","mZ Nx},it\",")^~O2Xg<","","=v?" +"]A#4=YjX~","","a7L","Vi6KCa","n-MKd{%^J2","vSCEKmv" +"Qr>","dK",")v`V\","|PyobH;`D","t4","$6","" +"\L}Os","PubF`","7\b?'p","","n19&@B","Q!vM5" +"<&Dl7![","z1*0","/C0;tyxCW","mx~",":lK38^$","w)F" +"=ST:y};;6m","kIja&","$h""nqTG?QX","Mv" +")2M8g\j&","9 ","M.SAW4:""","DX*%","LjMZ]-","V2Y_" +"X|LW.","","ADMXR%rM","+*","}3*Uv ","&" +"`<","r~%h:O(","v--@~@Z_","AWrujA%9ksC","@/",".^p(!" +"7n","[",">bsF","0&i4rnFUn)","\z\$-","5" +"s=zO_X:V","1]X","+2-=","","|$","F({0B9AzL)" +"[l^P","Qsnx:6'","W,R8qFs4X:","XE","B5Hv%u","z#>" +"DgGoEDI|}","#2eDN>0","a","I([""Z" +"O","E",">O","yia.Z","!W","Wst" +"rieJAB)j","Jh>X0'W","0%[Tbb1wU","yu\r!bR&oJ","mL{:Y","" +"g)+9v","\V7G","","i65","u1z""","mwMA0mduA" +"""] Mmz0x","W)Z7Z]I.","Y>Gy?s~","B^VT/[","!1Po*","GRTX)#s8""" +"Qg","D","f","F:;ZMn","","^z" +"m!N+>,G>","rBM)f","S/WddM","*7x~bK?~",":rGV#_=OLS","8*uQ,c" +"NV]byBNum=","]j?#zg&Q""","",",>WC&q","a&ggxHA","6","S","S","vB`v\$X\" +" $I>Qn(","F$vCj}W^""","f,m$(`","Tk&1","g","" +"F""r_833","M","","@:W$gx","rZdtt)y","S1 " +"}I+H?^U","@hvC3b!|L,","O","","nu z9>%#bt}","UnXI/vn-" +"@\6 ","z,fUI78CJ","uqM,A+|[~","~86o","Ttg\v","'W&'.!S|" +"(]+1","DWn>pl0y","5>pp{","pE:","t""n4ovOCp","q9^Nt" +"i","7XNxL.","Y@E2~}Ua?T","NvkqS;","b+XAuT~'tg","{IH" +"","2A)","t?-N)j","uOVY","&-EZa","`" +"E52JNq=h<","iPtlx","Y","","faS","p+Y""o" +"{$]","=S>&n","Q&V","IAYvv","2!_Nzwu8","]fB," +"i2=",">UW","","O{1@""V@nqa","CK","T" +"U%","T","F","-93t\^>","0","Vd" +",m_G&8""\99","-H`NpPZM","!8TxU^","`CKOAeq","T[lX&""4","" +";=t",":l<2DTlM","e7tof`Qgz","pm-xU:m","uffMI!%R","6.R" +"f:^k|)$","8j;c SN""y#","::[^#t%flO",">y[","lYlT2Lh","x\|jiZC=kb" +"$OEjV!'","L","iJbq(/t*P","cn>p<|O\","%c","_&IRa0L" +"""","uq3"," \_u","'+.z+'EcHU","+vjE8","" +"@1Q(^","q!HDi","5 J)22","Z","Hr_;WiMS","6q" +"w$(iH|P","]Q","q86n/","aemHb","S%J3,>3","j\>9/kZw" +";F","*4","[a)2;4","m;*R","B3(pp%","|s6C]" +"N0xX","gK$48$i","" +"LC4Mu","","~MP,U,-g6","0x ({8G^ ","","tL5zhiA|Q" +"drlO","$F\3 95","\","w^n( ^","","uA BKj" +"",".^p+]_D'C>","(r/=:*","(&*#7a.",""," L 2" +"b01`BI&dTL",")$Qd","","YKZw","rs","V""q?W","r]\AR%9eQ","ar2 (X" +"""Q*?lEd","Mr7aZ",">y76","iK","is","wQ" +"_$MJZ","6""%\-","Ea]","$","","" +"\p","vMKAJE(]","Qv"," ;.+w5$","_I~","<.DUo/H7" +">^{","afP:>acaF+","&)Cy","*nyEfBQj;:","EC?=KR}","+7" +"1&vw9Ao","A\C""2!xul(","1|{mLsD","9cSg)y194","WKTG'_!c.",";x!?YBad+" +"","","sN+","","w5w","8J2w6cg*" +"E","6,WQ:}G-H","","1C","5","1" +"cMk","WCj.J{:","f=&4","m^3^","RU","" +"b-~5b^P-O","p%4XTKcK|","od","b5",":",";L!q" +"g\T&","X$vd[#",",PM""H""","a","S Ja]E","$dJ]8" +"<'e#%x","n ","""nfqy","2U","O","yY" +"]S","m","rwU{1YoBts","hf7~qC+,wl","=&P?","<$g" +"K6s#","s}Yz;","aw","S0Gl","Y""IO^C","g[Dj" +"1UuW?- ","lO\","!W)kOeH2OK","","QV!oGl I%","" +"MH$ 2","",">DlLK","[U<-0","","g" +"KZ","","CL&e1nnJG`","e","8&[8\X>a","jJS" +"Z",".3eN\","eW' tBt","+u}:=X","UxJUIs~W^-","TN" +"!mMfsPH","HFNPePB","","Xs^#VQp","K .>g,N","> *_d-" +"{)]y""","(]Z4%","?HL{,#3[NT","","|)-Gm$DVV","" +"O","","vF2@q""","i0","i-=/`k7","yH)-$h0B^" +"F%mKA","O#Cx:]t","J~{,V&","Y^>""V*","v/QKA""","VU7FI0" +"Bt<>","3NNIq","Xb2""\ju7nm","gnKl]","jk3(`l","'\>t vwR" +"ZY","}[J))N%R","","","4Wmf","RK" +"aH&Zd!KIDR",",?ZMK?&v79","A","h1v","K%9~""Arg8=","S&","Lo|8","yeP$IgQ-u"," 2_2}fw","S'CH" +">f{Y","Nk","5q[EGW[","M[n{V\~","_-'b","" +"u/3S\~","m3j7:.JjF0","PH3(yO4@ k","^7?M+m\-z,","K",";<|gC$=Qo" +"\mjYcUz","?uhpF1N=ao","","{F`cP","hzx$EV1h","" +"G%"," OQf{?UJ","%U>>w[[","+|W)E:xR#/","0","QRp" +"d9J;.(VV","G{MC5","^z","}E|p[","H!hX0/",".')Kn#OU#u" +"ym","CnD=D","w;E2:t","g728X.XV'","Od4c:","[uq" +".eDWg","","yw","","H%u","+f!>V&N3 T" +"pA5""(P","t+","","L\36R","*lOLX8[>IY","#&""B[K" +"3\$Ho","f","F!sU!5JE!","baZjc","DVFA","yH&cnw{hJ$" +"""= d(Uu","6.8","QtHv","wltHY}kP","=?F"," /AuX","u$jdzb","sl~BeJgoZy" +"|O>nuG","={.Q>DKh","]o5","x-I\J","]fv F","61Bh7S-" +"<","ZU}""6\",":gR","Y""Fr-tK","Y90yK","w}" +"""EZ[,^dq","u$K]glZ]fX","o""3 b","7,DfS8""EH)",":[XN","rk5~" +"","^:","8.}","&C{,B3)","{ Q^66YOjx","N7|hz+J" +"')Gi(QfwRV","s8|tR&H","C2L>r~'Q","","""g!8WWDydX","_8)v3#QZc," +"UI%*Z+EaC n/T*0","5Qj6q&",",I!9Yf)-","O7""h$","cc};2[W/H ","qi." +"#f&Xc!8_","L/+Sv","-|N-OTo3j",";G]R","9zhHI$[(TA","?cr2"":" +"+(OUct<;TV",".U","imZSB","6bZ3n p{",")w3|,AJ7a","pXq" +"tZZ=2tjg","s:KP]","0\I","hCT9Y","}1k"," OHsW" +"","","ftw40/,","OIT3z2yp&K","","lDAFzM6z" +"","x:1FVv","(-i}","6SZ5`FfS","*$yk","ST%Y" +"B[","6`u{","c","mRb5" +"<}CxG","wdS1l" +"v!K&ctH","9-tk","L8DKsx1E3*","YnI&&(!3!D","dfNpbnp","$Z+TlYK" +"\Jw@%Z","8>Rv?Vamp","&45","0vJlPq","@|-6 '","i:" +",B4E","28","8Ese|pGFa","s","/'up,-",">#In," +"Dcr)4'KN","","5`kb$","'uMK;X",",H.","_K>4}eq-" +"<\r= ,","/MT=6","4Zf5Y","q","X>hcA?n","xL}u%PV" +"C","T2OE","Q'ptg={w$}","dolr5\","hhG}l0O5]","M" +"","P8$","6)Vfr<","1sF)}?=.mf","`7!Zb","" +"0)","h?<)X","|9xwC d","uxU","%CXVw$U","rTFHWR" +".4N(h:8","","ykN5F5bg%}",".@M%t","KP,""ZiX","k?e\b{E:" +"z}zN7v.iJh","+L:BY*'","c","YN=PxD]e^","x[i'NTtO(","8:*zHd" +"xw","o2m,U=","Y","G[Z<>ln","h)d>","/cre+6}" +"xd","OV4.,#3d","KMbT7Z","Jv ","mW|g-/c","fJ\" +"{<|(!P","""uP]z;io",">39","JC","JL*7i%1","qy%n*!" +"Efqv+19gb",",^","(""c(","]9ZhpF+""V}","&lf=cA`~o9","pxnqY!^" +"P","n?","/~QJg`$$6","7XA","j!Al","^7mg" +"y0/Q,pS","/G|FD&=eH","LZQ4oH\{F","i","K!+mU6","u!YAg4" +"=|-f<","=cpf","F[bW&=x?","G1s7w+","0[e7Lqk@N-","Q\p{vW6""O/" +"pG}4J}##","/nU56phs","0%wr%^4,S","""","cJcRi.Sj)","R," +"G","","","(2_l|doqUR","F0FF",",F^" +"[#*'","/rd","^- +HT\!&i","Av?t B","z:jL|{","/" +"bC'W","r98","Fy]","}\1+rzKI_W","y","9T{vO" +"-fk0~4","CV=a2sl+$$","w$~I\T","{vRXai","u","WT" +"5","0hY","jhk}#Ef\","m^tv2*[(","7*","}XR$AM}Io" +"RPk","","yAaG","t1","hNzKKop","{)RG" +"","9c","}EDN^+","%l","2","099=:" +"T!A| }RR","$G^u","a]J:Z9S","A2md{@7b","%>K*afb6O","""'W ~Rtz" +"!j7@sH'S","]Z","`tCQR}","!X'}(2","$`)J^]","A4a)" +"s","eXI!,AOn","=!e:D","96","-4&","B#wJOP" +"S2 ","ae!rLIGc3","_.<","@lkfd9:R","mFw#)s}O",",_","N24NK","^9])","=Fl" +"mA","c-EU","W|v","^dP)","{P59","{n=OSV~" +"h&0zs6","@/,a9]sv","N~","J\R)7B","^A|U","2f~l4w" +"?Z;cu.","1?#^eU","W^otY","A","+JAX","LA;","t" +"pexG4{","m([kDbr#","H)",">S}n/Yp;","h5gRE","gZr3ll8N" +"lEr","5_1fz@AUi","MC","=Wy5u%","~","{aeJEa;0 " +"r","gl-q{~","Ar5","#}1Ks!5'K","w$FC","r5!" +"f","i","<-\[@X4l&","tJ#=","j~SZU86,B,","Cd>9s\~`WG" +"qV[Nvy}RGz","AahvU-I,A6","s\I`;GQ5b","G/mPIa1}","._m","E8aZ!lZvB" +"YZw-> ","{}UK","kzw_9g" +"vuY\7!K!Z5","qE\?","Qi;","Gw6NLB. ;","","b[kw:M*" +"b%","G","xqjT+B>7M","%IR^pRgW4f","J.@V;p{","vhcJ+V" +"h""","K(R=SZJ","^PdEa%^R","@S#oT","T?+""","$C#;o]94;" +"9","n","=fl","P3j#(m","EsdN","60""oa" +"#w*l","CR","GPo""","J""{nZ C;","nKY:P8",";t'2" +"8OU=`","n)9rtw {>","YchZ$dap","","tnnM[8","P" +"Gc""","","","J'Ws","n0DRQ4","FY=Z+" +"_c.bgs","L,m4lr-","t""!zm+z?","","""","RGQ||" +"",",Q@e","k^N@5Q(1z`","","p<9C[",");{<" +"D-gAzD1UEp","b","e.Q6>W","@","UPZ`MTb} ","" +"%A T0[XV""","BK/@?$2;","A.we","mq/uaN~","?=h5L""dLOw",";.n]mr1" +"K","z/HBrSj","j/f","-tp5","dk6]l;tn","vEf2q" +"`8F|_1l07z","!T?","a34HX!4A",")?Lj3Q%,O" +"XbA~","!(0VBdS\a","y2+1","","se","x]" +"j8|","zpuMz","Zx!.^","pE,Q","+","O1}g&" +"-Rj3z&","22","7fgr'b","P*YUXoGzOL","Id","7@6('" +"","~Mn!","zU|U","","mK*rw0","r\Thj" +"'ES^=V",";%~E$fUc","","6/J?","7p8V+'D""tQ","WN?)|9;_sK" +"X?fx""","","","|","[FCyM8","lt1" +"Q:6_NG8"," M","5vO1~","aeL$>K;","({0Y|gn,N{","#[=F0@" +"ws'erE4myY","0spk|3QK","_pIarmCx","nv0""H$wt0T","3v#P","4XJn","a7" +"*+EH4D","8Mkt1D","","y@PpS\D","_>$GB&","5$Tc5" +";","D","|","u}O","NQ0j}4$$","E@]`z9'(" +"Xm^S5rm;%<","PRd","","Fr~-8","cK2P%-> ,v","","1","/Uz" +"(82+{8",">#6f","~%p?+","XGib 7QuID" +"9o:4n[7(+T","fWk~}<|3",">","b","G/E?F","UNpx4Z8" +"I^]W,M5|P","C7","TbKD:N YD","R|'\%","`Ub!1","{*f" +"H",">=","Bu|wuQ?","HegP",".A*&4x<;&f","FLuhcOB" +"e!)","JB$Q`X0F","M","8bg{","_ imFTj~~T","HW:" +"M*x","% +x:p/O","","K[^P&Ux'","O=Oq","k|Y" +"","Cp.a5!","0gg__6G/By","L#(CMmio","",">E""K" +"#`7=j","J","","","L>$py/","R)q3CTl" +"i.","D=","%_>On#]B","" +"","1jfHH","gI/[Y""","#b!k","B|^4#DNcP",":" +"","_wv[3","nC~TVh>G2","YCy=uRO3/","0%++b[v","3dY=AF" +"","bd","D}*vg2CH53","-~","-7i}_U[&","f[O^oGx" +"!/m `(Q4#%","^,ghnAK","ry=md_r<_x","6!+zjh@h",")/","/(" +"(2$S8","=''0]","","","u}+G=f5`","[@-" +"2Xg" +"","+N`#JAwY&","h&","Dz~~8N]7H3","(NCI>jmu","aHy!P9/%Ba" +"&=#","Yq'W","'M6bl","nuUh>K",",M","2K_eT8YS%" +"1""[,8H","jT-)","J9ix(RDM,","}","ky~,'m Jn" +"$","","VVjnJ","$z]j","Bo|Mtn(wGo",".m" +"p","9JK4",")Uo|34ojc","","I\wE{","b\|H1w[iR" +"N.r}Z!","YsgQ","G3cUX","Nby5_",")d0","9([?z" +"UIJO~cs7","[ZHr","l')h/G~","83M","n","f*+YXo","XRXcm\(^+|" +"LS4hCw8","}`uu{ ","O>r v#!",".8","e","RXxFv" +"","4F^nM}n*","'gehCKSk2J","\JY]","q","s53" +"V~F.j,j6t","nq","{@\##Ol","","ij2","'k^t(b" +"0","J/6[Jf%j","?*wS!|_44","","CH,Z","]Y*" +"s~%/!UT","v","6..v1","H,","7zuN""?8","Kmk1D[i","?@j@p~-8Je","p90y9 Iy" +"","Sk6|vQ&lmF","8H(G'&5Ts"," !t","\/Y","MAJGBxi" +"|DkfGX8\","4-o=R*B'L","mp8G4kY","7O^S","i$5MQ=yt","Ab&U","g`etY{;H*)","","Xv","q","6ky6" +"=tEtq&","","DfR`2W","9NE","yF'P*P ","c" +"=B","O[7)3bo","<=7T3eC@&Z","","i","o.B4" +"?#[h/8\a","9f","qaG7p6x]F&","'gSreOQwR","/E }r9","" +"","","F+k4j]F",")O*d","","k7s VlaK)h" +"=d{jlC","B","tJ'^8nb","_""e%6,lU","0:","=" +"^v?bc","~<","c","~$u6I)lB","n","aF," +"s",":WMDU&[","&@""~+G","N","by","g0$LD;?""S+" +"M]",")Zz({Rw","eK*53J","S8vOP=Er","@EkS|8","" +"1x9b","No@","3a","","YQ?]XA#",">zc" +"?p~F]","JG]E}C","Kf '!P`4","(","sI""p=W?)I","5<9T" +"IEj+\]|r9","","+ uB","sxb","}.c","+BJ`!C29Nl" +"6zkl4=z","{\b9Nt[","R\=8S$","=bCc1","/","" +" ","i4m'$T KH\",",-[V1gyE",",3vPcP0","u","ju.sNB*Y" +"j)E/7>]l/n","}R/<8","[","[m90:G","","Mpfb" +"aW,7$7XC","q-","6Cx!yp","j","+kA!7g","l,2(S7/ev4" \ No newline at end of file diff --git a/pythonFiles/tests/ipython/scripts.py b/pythonFiles/tests/ipython/scripts.py new file mode 100644 index 000000000000..4d6bd8803b9c --- /dev/null +++ b/pythonFiles/tests/ipython/scripts.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import re +import os +import json +import sys + + +def check_for_ipython(): + if int(sys.version[0]) >= 3: + try: + from IPython import get_ipython + + return not get_ipython() == None + except ImportError: + pass + return False + + +def execute_script(file, replace_dict=dict([])): + from IPython import get_ipython + + regex = ( + re.compile("|".join(replace_dict.keys())) + if len(replace_dict.keys()) > 0 + else None + ) + + # Open the file. Read all lines into a string + contents = "" + with open(file, "r") as fp: + for line in fp: + # Replace the key value pairs + contents += ( + line + if regex == None + else regex.sub(lambda m: replace_dict[m.group()], line) + ) + + # Execute this script as a cell + result = get_ipython().run_cell(contents) + return result.success + + +def execute_code(code): + # Execute this script as a cell + result = get_ipython().run_cell(code) + return result + + +def get_variables(capsys): + path = os.path.dirname(os.path.abspath(__file__)) + file = os.path.abspath(os.path.join(path, "./getJupyterVariableList.py")) + if execute_script(file): + read_out = capsys.readouterr() + return json.loads(read_out.out) + else: + raise Exception("Getting variables failed.") + + +def find_variable_json(varList, varName): + for sub in varList: + if sub["name"] == varName: + return sub + + +def get_variable_value(variables, name, capsys): + varJson = find_variable_json(variables, name) + path = os.path.dirname(os.path.abspath(__file__)) + file = os.path.abspath(os.path.join(path, "./getJupyterVariableValue.py")) + keys = dict([("_VSCode_JupyterTestValue", json.dumps(varJson))]) + if execute_script(file, keys): + read_out = capsys.readouterr() + return json.loads(read_out.out)["value"] + else: + raise Exception("Getting variable value failed.") + + +def get_data_frame_info(variables, name, capsys): + varJson = find_variable_json(variables, name) + path = os.path.dirname(os.path.abspath(__file__)) + syspath = os.path.abspath( + os.path.join(path, "../../vscode_datascience_helpers/dataframes") + ) + syscode = 'import sys\nsys.path.append("{0}")'.format(syspath.replace("\\", "\\\\")) + importcode = "import vscodeGetDataFrameInfo\nprint(vscodeGetDataFrameInfo._VSCODE_getDataFrameInfo({0}))".format( + name + ) + result = execute_code(syscode) + if not result.success: + result.raise_error() + result = execute_code(importcode) + if result.success: + read_out = capsys.readouterr() + info = json.loads(read_out.out[0:-1]) + varJson.update(info) + return varJson + else: + result.raise_error() + + +def get_data_frame_rows(varJson, start, end, capsys): + path = os.path.dirname(os.path.abspath(__file__)) + syspath = os.path.abspath( + os.path.join(path, "../../vscode_datascience_helpers/dataframes") + ) + syscode = 'import sys\nsys.path.append("{0}")'.format(syspath.replace("\\", "\\\\")) + importcode = "import vscodeGetDataFrameRows\nprint(vscodeGetDataFrameRows._VSCODE_getDataFrameRows({0}, {1}, {2}))".format( + varJson["name"], start, end + ) + result = execute_code(syscode) + if not result.success: + result.raise_error() + result = execute_code(importcode) + if result.success: + read_out = capsys.readouterr() + return json.loads(read_out.out[0:-1]) + else: + result.raise_error() diff --git a/pythonFiles/tests/ipython/test_variables.py b/pythonFiles/tests/ipython/test_variables.py new file mode 100644 index 000000000000..1267f3ed880f --- /dev/null +++ b/pythonFiles/tests/ipython/test_variables.py @@ -0,0 +1,164 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest +import os +from .scripts import ( + get_variable_value, + get_variables, + get_data_frame_info, + get_data_frame_rows, + check_for_ipython, +) + +haveIPython = check_for_ipython() + + +@pytest.mark.skipif( + not haveIPython, reason="Can't run variable tests without IPython console" +) +def test_variable_list(capsys): + from IPython import get_ipython + + # Execute a single cell before we get the variables. + get_ipython().run_cell("x = 3\r\ny = 4\r\nz=5") + vars = get_variables(capsys) + have_x = False + have_y = False + have_z = False + for sub in vars: + have_x |= sub["name"] == "x" + have_y |= sub["name"] == "y" + have_z |= sub["name"] == "z" + assert have_x + assert have_y + assert have_z + + +@pytest.mark.skipif( + not haveIPython, reason="Can't run variable tests without IPython console" +) +def test_variable_value(capsys): + from IPython import get_ipython + + # Execute a single cell before we get the variables. This is the variable we'll look for. + get_ipython().run_cell("x = 3") + vars = get_variables(capsys) + varx_value = get_variable_value(vars, "x", capsys) + assert varx_value + assert varx_value == "3" + + +@pytest.mark.skipif( + not haveIPython, reason="Can't run variable tests without IPython console" +) +def test_dataframe_info(capsys): + from IPython import get_ipython + + # Setup some different types + get_ipython().run_cell( + """ +import pandas as pd +import numpy as np +ls = list([10, 20, 30, 40]) +df = pd.DataFrame(ls) +se = pd.Series(ls) +np1 = np.array(ls) +np2 = np.array([[1, 2, 3], [4, 5, 6]]) +dict1 = {'Name': 'Zara', 'Age': 7, 'Class': 'First'} +obj = {} +col = pd.Series(data=np.random.random_sample((7,))*100) +dfInit = {} +idx = pd.date_range('2007-01-01', periods=7, freq='M') +for i in range(30): + dfInit[i] = col +dfInit['idx'] = idx +df2 = pd.DataFrame(dfInit).set_index('idx') +df3 = df2.iloc[:, [0,1]] +se2 = df2.loc[df2.index[0], :] +""" + ) + vars = get_variables(capsys) + df = get_variable_value(vars, "df", capsys) + se = get_variable_value(vars, "se", capsys) + np = get_variable_value(vars, "np1", capsys) + np2 = get_variable_value(vars, "np2", capsys) + ls = get_variable_value(vars, "ls", capsys) + obj = get_variable_value(vars, "obj", capsys) + df3 = get_variable_value(vars, "df3", capsys) + se2 = get_variable_value(vars, "se2", capsys) + dict1 = get_variable_value(vars, "dict1", capsys) + assert df + assert se + assert np + assert ls + assert obj + assert df3 + assert se2 + assert dict1 + verify_dataframe_info(vars, "df", "index", capsys, True) + verify_dataframe_info(vars, "se", "index", capsys, True) + verify_dataframe_info(vars, "np1", "index", capsys, True) + verify_dataframe_info(vars, "ls", "index", capsys, True) + verify_dataframe_info(vars, "np2", "index", capsys, True) + verify_dataframe_info(vars, "obj", "index", capsys, False) + verify_dataframe_info(vars, "df3", "idx", capsys, True) + verify_dataframe_info(vars, "se2", "index", capsys, True) + verify_dataframe_info(vars, "df2", "idx", capsys, True) + verify_dataframe_info(vars, "dict1", "index", capsys, True) + + +def verify_dataframe_info(vars, name, indexColumn, capsys, hasInfo): + info = get_data_frame_info(vars, name, capsys) + assert info + assert "columns" in info + assert len(info["columns"]) > 0 if hasInfo else True + assert "rowCount" in info + if hasInfo: + assert info["rowCount"] > 0 + assert info["indexColumn"] + assert info["indexColumn"] == indexColumn + + +@pytest.mark.skipif( + not haveIPython, reason="Can't run variable tests without IPython console" +) +def test_dataframe_rows(capsys): + from IPython import get_ipython + + # Setup some different types + path = os.path.dirname(os.path.abspath(__file__)) + file = os.path.abspath(os.path.join(path, "random.csv")) + file = file.replace("\\", "\\\\") + dfstr = "import pandas as pd\r\ndf = pd.read_csv('{}')".format(file) + get_ipython().run_cell(dfstr) + vars = get_variables(capsys) + df = get_variable_value(vars, "df", capsys) + assert df + info = get_data_frame_info(vars, "df", capsys) + assert "rowCount" in info + assert info["rowCount"] == 6000 + rows = get_data_frame_rows(info, 100, 200, capsys) + assert rows + assert rows["data"][0]["+h2"] == "Fy3 W[pMT[" + get_ipython().run_cell( + """ +import pandas as pd +import numpy as np +ls = list([10, 20, 30, 40]) +df = pd.DataFrame(ls) +se = pd.Series(ls) +np1 = np.array(ls) +np2 = np.array([[1, 2, 3], [4, 5, 6]]) +obj = {} +""" + ) + vars = get_variables(capsys) + np2 = get_variable_value(vars, "np2", capsys) + assert np2 + info = get_data_frame_info(vars, "np2", capsys) + assert "rowCount" in info + assert info["rowCount"] == 2 + rows = get_data_frame_rows(info, 0, 2, capsys) + assert rows + assert rows["data"][0] diff --git a/pythonFiles/tests/run_all.py b/pythonFiles/tests/run_all.py new file mode 100644 index 000000000000..ce5a62649962 --- /dev/null +++ b/pythonFiles/tests/run_all.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Replace the "." entry. +import os.path +import sys + +sys.path[0] = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +from tests.__main__ import main, parse_args + + +if __name__ == "__main__": + mainkwargs, pytestargs = parse_args() + ec = main(pytestargs, **mainkwargs) + sys.exit(ec) diff --git a/pythonFiles/tests/test_normalize_for_interpreter.py b/pythonFiles/tests/test_normalize_for_interpreter.py new file mode 100644 index 000000000000..37a2dce5cb7d --- /dev/null +++ b/pythonFiles/tests/test_normalize_for_interpreter.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest +import sys +import textwrap + +import normalizeForInterpreter + + +class TestNormalizationScript(object): + """Basic unit tests for the normalization script.""" + + @pytest.mark.skipif( + sys.version_info.major == 2, + reason="normalizeForInterpreter not working for 2.7, see GH #4805", + ) + def test_basicNormalization(self, capsys): + src = 'print("this is a test")' + normalizeForInterpreter.normalize_lines(src) + captured = capsys.readouterr() + assert captured.out == src + + @pytest.mark.skipif( + sys.version_info.major == 2, + reason="normalizeForInterpreter not working for 2.7, see GH #4805", + ) + def test_moreThanOneLine(self, capsys): + src = textwrap.dedent( + """\ + # Some rando comment + + def show_something(): + print("Something") + """ + ) + normalizeForInterpreter.normalize_lines(src) + captured = capsys.readouterr() + assert captured.out == src + + @pytest.mark.skipif( + sys.version_info.major == 2, + reason="normalizeForInterpreter not working for 2.7, see GH #4805", + ) + def test_withHangingIndent(self, capsys): + src = textwrap.dedent( + """\ + x = 22 + y = 30 + z = -10 + result = x + y + z + + if result == 42: + print("The answer to life, the universe, and everything") + """ + ) + normalizeForInterpreter.normalize_lines(src) + captured = capsys.readouterr() + assert captured.out == src + + @pytest.mark.skipif( + sys.version_info.major == 2, + reason="normalizeForInterpreter not working for 2.7, see GH #4805", + ) + def test_clearOutExtraneousNewlines(self, capsys): + src = textwrap.dedent( + """\ + value_x = 22 + + value_y = 30 + + value_z = -10 + + print(value_x + value_y + value_z) + + """ + ) + expectedResult = textwrap.dedent( + """\ + value_x = 22 + value_y = 30 + value_z = -10 + print(value_x + value_y + value_z) + + """ + ) + normalizeForInterpreter.normalize_lines(src) + result = capsys.readouterr() + assert result.out == expectedResult + + @pytest.mark.skipif( + sys.version_info.major == 2, + reason="normalizeForInterpreter not working for 2.7, see GH #4805", + ) + def test_clearOutExtraLinesAndWhitespace(self, capsys): + src = textwrap.dedent( + """\ + if True: + x = 22 + + y = 30 + + z = -10 + + print(x + y + z) + + """ + ) + expectedResult = textwrap.dedent( + """\ + if True: + x = 22 + y = 30 + z = -10 + + print(x + y + z) + + """ + ) + normalizeForInterpreter.normalize_lines(src) + result = capsys.readouterr() + assert result.out == expectedResult diff --git a/pythonFiles/tests/testing_tools/__init__.py b/pythonFiles/tests/testing_tools/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/pythonFiles/tests/testing_tools/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/pythonFiles/unittest.py b/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/__init__.py similarity index 100% rename from pythonFiles/unittest.py rename to pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/__init__.py diff --git a/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/test_Spam.py b/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/test_Spam.py new file mode 100644 index 000000000000..3501b9e118e5 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/C/test_Spam.py @@ -0,0 +1,3 @@ + +def test_okay(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/A/b/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/NormCase/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md b/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md new file mode 100644 index 000000000000..e30e96142d02 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/README.md @@ -0,0 +1,156 @@ +## Directory Structure + +``` +pythonFiles/tests/testing_tools/adapter/.data/ + tests/ # test root + test_doctest.txt + test_pytest.py + test_unittest.py + test_mixed.py + spam.py # note: no "test_" prefix, but contains tests + test_foo.py + test_42.py + test_42-43.py # note the hyphen + testspam.py + v/ + __init__.py + spam.py + test_eggs.py + test_ham.py + test_spam.py + w/ + # no __init__.py + test_spam.py + test_spam_ex.py + x/y/z/ # each with a __init__.py + test_ham.py + a/ + __init__.py + test_spam.py + b/ + __init__.py + test_spam.py +``` + +## Tests (and Suites) + +basic: + +- `./test_foo.py::test_simple` +- `./test_pytest.py::test_simple` +- `./test_pytest.py::TestSpam::test_simple` +- `./test_pytest.py::TestSpam::TestHam::TestEggs::test_simple` +- `./test_pytest.py::TestEggs::test_simple` +- `./test_pytest.py::TestParam::test_simple` +- `./test_mixed.py::test_top_level` +- `./test_mixed.py::MyTests::test_simple` +- `./test_mixed.py::TestMySuite::test_simple` +- `./test_unittest.py::MyTests::test_simple` +- `./test_unittest.py::OtherTests::test_simple` +- `./x/y/z/test_ham.py::test_simple` +- `./x/y/z/a/test_spam.py::test_simple` +- `./x/y/z/b/test_spam.py::test_simple` + +failures: + +- `./test_pytest.py::test_failure` +- `./test_pytest.py::test_runtime_failed` +- `./test_pytest.py::test_raises` + +skipped: + +- `./test_mixed.py::test_skipped` +- `./test_mixed.py::MyTests::test_skipped` +- `./test_pytest.py::test_runtime_skipped` +- `./test_pytest.py::test_skipped` +- `./test_pytest.py::test_maybe_skipped` +- `./test_pytest.py::SpamTests::test_skipped` +- `./test_pytest.py::test_param_13_markers[???]` +- `./test_pytest.py::test_param_13_skipped[*]` +- `./test_unittest.py::MyTests::test_skipped` +- (`./test_unittest.py::MyTests::test_maybe_skipped`) +- (`./test_unittest.py::MyTests::test_maybe_not_skipped`) + +in namespace package: + +- `./w/test_spam.py::test_simple` +- `./w/test_spam_ex.py::test_simple` + +filename oddities: + +- `./test_42.py::test_simple` +- `./test_42-43.py::test_simple` +- (`./testspam.py::test_simple` not discovered by default) +- (`./spam.py::test_simple` not discovered) + +imports discovered: + +- `./v/test_eggs.py::test_simple` +- `./v/test_eggs.py::TestSimple::test_simple` +- `./v/test_ham.py::test_simple` +- `./v/test_ham.py::test_not_hard` +- `./v/test_spam.py::test_simple` +- `./v/test_spam.py::test_simpler` + +subtests: + +- `./test_pytest.py::test_dynamic_*` +- `./test_pytest.py::test_param_01[]` +- `./test_pytest.py::test_param_11[1]` +- `./test_pytest.py::test_param_13[*]` +- `./test_pytest.py::test_param_13_markers[*]` +- `./test_pytest.py::test_param_13_repeat[*]` +- `./test_pytest.py::test_param_13_skipped[*]` +- `./test_pytest.py::test_param_23_13[*]` +- `./test_pytest.py::test_param_23_raises[*]` +- `./test_pytest.py::test_param_33[*]` +- `./test_pytest.py::test_param_33_ids[*]` +- `./test_pytest.py::TestParam::test_param_13[*]` +- `./test_pytest.py::TestParamAll::test_param_13[*]` +- `./test_pytest.py::TestParamAll::test_spam_13[*]` +- `./test_pytest.py::test_fixture_param[*]` +- `./test_pytest.py::test_param_fixture[*]` +- `./test_pytest_param.py::test_param_13[*]` +- `./test_pytest_param.py::TestParamAll::test_param_13[*]` +- `./test_pytest_param.py::TestParamAll::test_spam_13[*]` +- (`./test_unittest.py::MyTests::test_with_subtests`) +- (`./test_unittest.py::MyTests::test_with_nested_subtests`) +- (`./test_unittest.py::MyTests::test_dynamic_*`) + +For more options for pytests's parametrize(), see +https://docs.pytest.org/en/latest/example/parametrize.html#paramexamples. + +using fixtures: + +- `./test_pytest.py::test_fixture` +- `./test_pytest.py::test_fixture_param[*]` +- `./test_pytest.py::test_param_fixture[*]` +- `./test_pytest.py::test_param_mark_fixture[*]` + +other markers: + +- `./test_pytest.py::test_known_failure` +- `./test_pytest.py::test_param_markers[2]` +- `./test_pytest.py::test_warned` +- `./test_pytest.py::test_custom_marker` +- `./test_pytest.py::test_multiple_markers` +- (`./test_unittest.py::MyTests::test_known_failure`) + +others not discovered: + +- (`./test_pytest.py::TestSpam::TestHam::TestEggs::TestNoop1`) +- (`./test_pytest.py::TestSpam::TestNoop2`) +- (`./test_pytest.py::TestNoop3`) +- (`./test_pytest.py::MyTests::test_simple`) +- (`./test_unittest.py::MyTests::TestSub1`) +- (`./test_unittest.py::MyTests::TestSub2`) +- (`./test_unittest.py::NoTests`) + +doctests: + +- `./test_doctest.txt::test_doctest.txt` +- (`./test_doctest.py::test_doctest.py`) +- (`../mod.py::mod`) +- (`../mod.py::mod.square`) +- (`../mod.py::mod.Spam`) +- (`../mod.py::mod.spam.eggs`) diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/mod.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/mod.py new file mode 100644 index 000000000000..b8c495503895 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/mod.py @@ -0,0 +1,51 @@ +""" + +Examples: + +>>> square(1) +1 +>>> square(2) +4 +>>> square(3) +9 +>>> spam = Spam() +>>> spam.eggs() +42 +""" + + +def square(x): + """ + + Examples: + + >>> square(1) + 1 + >>> square(2) + 4 + >>> square(3) + 9 + """ + return x * x + + +class Spam(object): + """ + + Examples: + + >>> spam = Spam() + >>> spam.eggs() + 42 + """ + + def eggs(self): + """ + + Examples: + + >>> spam = Spam() + >>> spam.eggs() + 42 + """ + return 42 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/spam.py new file mode 100644 index 000000000000..4c4134d75584 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/spam.py @@ -0,0 +1,3 @@ + +def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42-43.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42-43.py new file mode 100644 index 000000000000..4c4134d75584 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42-43.py @@ -0,0 +1,3 @@ + +def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42.py new file mode 100644 index 000000000000..4c4134d75584 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_42.py @@ -0,0 +1,3 @@ + +def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.py new file mode 100644 index 000000000000..27cccbdb77cc --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.py @@ -0,0 +1,6 @@ +""" +Doctests: + +>>> 1 == 1 +True +""" diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.txt b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.txt new file mode 100644 index 000000000000..4b51fde5667e --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_doctest.txt @@ -0,0 +1,15 @@ + +assignment & lookup: + +>>> x = 3 +>>> x +3 + +deletion: + +>>> del x +>>> x +Traceback (most recent call last): + ... +NameError: name 'x' is not defined + diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_foo.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_foo.py new file mode 100644 index 000000000000..e752106f503a --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_foo.py @@ -0,0 +1,4 @@ + + +def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_mixed.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_mixed.py new file mode 100644 index 000000000000..e9c675647f13 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_mixed.py @@ -0,0 +1,27 @@ +import pytest +import unittest + + +def test_top_level(): + assert True + + +@pytest.mark.skip +def test_skipped(): + assert False + + +class TestMySuite(object): + + def test_simple(self): + assert True + + +class MyTests(unittest.TestCase): + + def test_simple(self): + assert True + + @pytest.mark.skip + def test_skipped(self): + assert False diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest.py new file mode 100644 index 000000000000..39d3ece9c0ba --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest.py @@ -0,0 +1,227 @@ +# ... + +import pytest + + +def test_simple(): + assert True + + +def test_failure(): + assert False + + +def test_runtime_skipped(): + pytest.skip('???') + + +def test_runtime_failed(): + pytest.fail('???') + + +def test_raises(): + raise Exception + + +@pytest.mark.skip +def test_skipped(): + assert False + + +@pytest.mark.skipif(True) +def test_maybe_skipped(): + assert False + + +@pytest.mark.xfail +def test_known_failure(): + assert False + + +@pytest.mark.filterwarnings +def test_warned(): + assert False + + +@pytest.mark.spam +def test_custom_marker(): + assert False + + +@pytest.mark.filterwarnings +@pytest.mark.skip +@pytest.mark.xfail +@pytest.mark.skipif(True) +@pytest.mark.skip +@pytest.mark.spam +def test_multiple_markers(): + assert False + + +for i in range(3): + def func(): + assert True + globals()['test_dynamic_{}'.format(i + 1)] = func +del func + + +class TestSpam(object): + + def test_simple(): + assert True + + @pytest.mark.skip + def test_skipped(self): + assert False + + class TestHam(object): + + class TestEggs(object): + + def test_simple(): + assert True + + class TestNoop1(object): + pass + + class TestNoop2(object): + pass + + +class TestEggs(object): + + def test_simple(): + assert True + + +# legend for parameterized test names: +# "test_param_XY[_XY]*" +# X - # params +# Y - # cases +# [_XY]* - extra decorators + +@pytest.mark.parametrize('', [()]) +def test_param_01(): + assert True + + +@pytest.mark.parametrize('x', [(1,)]) +def test_param_11(x): + assert x == 1 + + +@pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) +def test_param_13(x): + assert x == 1 + + +@pytest.mark.parametrize('x', [(1,), (1,), (1,)]) +def test_param_13_repeat(x): + assert x == 1 + + +@pytest.mark.parametrize('x,y,z', [(1, 1, 1), (3, 4, 5), (0, 0, 0)]) +def test_param_33(x, y, z): + assert x*x + y*y == z*z + + +@pytest.mark.parametrize('x,y,z', [(1, 1, 1), (3, 4, 5), (0, 0, 0)], + ids=['v1', 'v2', 'v3']) +def test_param_33_ids(x, y, z): + assert x*x + y*y == z*z + + +@pytest.mark.parametrize('z', [(1,), (5,), (0,)]) +@pytest.mark.parametrize('x,y', [(1, 1), (3, 4), (0, 0)]) +def test_param_23_13(x, y, z): + assert x*x + y*y == z*z + + +@pytest.mark.parametrize('x', [ + (1,), + pytest.param(1.0, marks=[pytest.mark.skip, pytest.mark.spam], id='???'), + pytest.param(2, marks=[pytest.mark.xfail]), + ]) +def test_param_13_markers(x): + assert x == 1 + + +@pytest.mark.skip +@pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) +def test_param_13_skipped(x): + assert x == 1 + + +@pytest.mark.parametrize('x,catch', [(1, None), (1.0, None), (2, pytest.raises(Exception))]) +def test_param_23_raises(x, catch): + if x != 1: + with catch: + raise Exception + + +class TestParam(object): + + def test_simple(): + assert True + + @pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) + def test_param_13(self, x): + assert x == 1 + + +@pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) +class TestParamAll(object): + + def test_param_13(self, x): + assert x == 1 + + def test_spam_13(self, x): + assert x == 1 + + +@pytest.fixture +def spamfix(request): + yield 'spam' + + +@pytest.fixture(params=['spam', 'eggs']) +def paramfix(request): + return request.param + + +def test_fixture(spamfix): + assert spamfix == 'spam' + + +@pytest.mark.usefixtures('spamfix') +def test_mark_fixture(): + assert True + + +@pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) +def test_param_fixture(spamfix, x): + assert spamfix == 'spam' + assert x == 1 + + +@pytest.mark.parametrize('x', [ + (1,), + (1.0,), + pytest.param(1+0j, marks=[pytest.mark.usefixtures('spamfix')]), + ]) +def test_param_mark_fixture(x): + assert x == 1 + + +def test_fixture_param(paramfix): + assert paramfix == 'spam' + + +class TestNoop3(object): + pass + + +class MyTests(object): # does not match default name pattern + + def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest_param.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest_param.py new file mode 100644 index 000000000000..bd22d89f42bd --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_pytest_param.py @@ -0,0 +1,18 @@ +import pytest + + +# module-level parameterization +pytestmark = pytest.mark.parametrize('x', [(1,), (1.0,), (1+0j,)]) + + +def test_param_13(x): + assert x == 1 + + +class TestParamAll(object): + + def test_param_13(self, x): + assert x == 1 + + def test_spam_13(self, x): + assert x == 1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_unittest.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_unittest.py new file mode 100644 index 000000000000..dd3e82535739 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/test_unittest.py @@ -0,0 +1,66 @@ +import unittest + + +class MyTests(unittest.TestCase): + + def test_simple(self): + self.assertTrue(True) + + @unittest.skip('???') + def test_skipped(self): + self.assertTrue(False) + + @unittest.skipIf(True, '???') + def test_maybe_skipped(self): + self.assertTrue(False) + + @unittest.skipUnless(False, '???') + def test_maybe_not_skipped(self): + self.assertTrue(False) + + def test_skipped_inside(self): + raise unittest.SkipTest('???') + + class TestSub1(object): + + def test_simple(self): + self.assertTrue(True) + + class TestSub2(unittest.TestCase): + + def test_simple(self): + self.assertTrue(True) + + def test_failure(self): + raise Exception + + @unittest.expectedFailure + def test_known_failure(self): + raise Exception + + def test_with_subtests(self): + for i in range(3): + with self.subtest(i): # This is invalid under Py2. + self.assertTrue(True) + + def test_with_nested_subtests(self): + for i in range(3): + with self.subtest(i): # This is invalid under Py2. + for j in range(3): + with self.subtest(i): # This is invalid under Py2. + self.assertTrue(True) + + for i in range(3): + def test_dynamic_(self, i=i): + self.assertEqual(True) + test_dynamic_.__name__ += str(i) + + +class OtherTests(unittest.TestCase): + + def test_simple(self): + self.assertTrue(True) + + +class NoTests(unittest.TestCase): + pass diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/testspam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/testspam.py new file mode 100644 index 000000000000..7ec91c783e2c --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/testspam.py @@ -0,0 +1,9 @@ +''' +... +... +... +''' + + +def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/spam.py new file mode 100644 index 000000000000..18c92c09306e --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/spam.py @@ -0,0 +1,9 @@ + +def test_simple(self): + assert True + + +class TestSimple(object): + + def test_simple(self): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_eggs.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_eggs.py new file mode 100644 index 000000000000..f3e7d9517631 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_eggs.py @@ -0,0 +1 @@ +from .spam import * diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_ham.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_ham.py new file mode 100644 index 000000000000..6b6a01f87ec5 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_ham.py @@ -0,0 +1,2 @@ +from .spam import test_simple +from .spam import test_simple as test_not_hard diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_spam.py new file mode 100644 index 000000000000..18cf56f90533 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/v/test_spam.py @@ -0,0 +1,5 @@ +from .spam import test_simple + + +def test_simpler(self): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam.py new file mode 100644 index 000000000000..6a0b60d1d5bd --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam.py @@ -0,0 +1,5 @@ + + + +def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam_ex.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam_ex.py new file mode 100644 index 000000000000..6a0b60d1d5bd --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/w/test_spam_ex.py @@ -0,0 +1,5 @@ + + + +def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/test_spam.py new file mode 100644 index 000000000000..bdb7e4fec3a5 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/a/test_spam.py @@ -0,0 +1,12 @@ +""" +... +""" + + +# ... + +ANSWER = 42 + + +def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/test_spam.py new file mode 100644 index 000000000000..4923c556c29a --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/test_spam.py @@ -0,0 +1,8 @@ + + +# ?!? +CHORUS = 'spamspamspamspamspam...' + + +def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/test_ham.py b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/test_ham.py new file mode 100644 index 000000000000..4c4134d75584 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/test_ham.py @@ -0,0 +1,3 @@ + +def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/notests/tests/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/notests/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/test_spam.py new file mode 100644 index 000000000000..4c4134d75584 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/test_spam.py @@ -0,0 +1,3 @@ + +def test_simple(): + assert True diff --git a/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py b/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/test_spam.py b/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/test_spam.py new file mode 100644 index 000000000000..54d6400a3465 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/test_spam.py @@ -0,0 +1,7 @@ + +def test_simple(): + assert True + + +# A syntax error: +: diff --git a/pythonFiles/tests/testing_tools/adapter/__init__.py b/pythonFiles/tests/testing_tools/adapter/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/pythonFiles/tests/testing_tools/adapter/pytest/__init__.py b/pythonFiles/tests/testing_tools/adapter/pytest/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/pytest/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/pythonFiles/tests/testing_tools/adapter/pytest/test_cli.py b/pythonFiles/tests/testing_tools/adapter/pytest/test_cli.py new file mode 100644 index 000000000000..6f590a31fa56 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/pytest/test_cli.py @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + +from ....util import Stub, StubProxy +from testing_tools.adapter.errors import UnsupportedCommandError +from testing_tools.adapter.pytest._cli import add_subparser + + +class StubSubparsers(StubProxy): + def __init__(self, stub=None, name="subparsers"): + super(StubSubparsers, self).__init__(stub, name) + + def add_parser(self, name): + self.add_call("add_parser", None, {"name": name}) + return self.return_add_parser + + +class StubArgParser(StubProxy): + def __init__(self, stub=None): + super(StubArgParser, self).__init__(stub, "argparser") + + def add_argument(self, *args, **kwargs): + self.add_call("add_argument", args, kwargs) + + +class AddCLISubparserTests(unittest.TestCase): + def test_discover(self): + stub = Stub() + subparsers = StubSubparsers(stub) + parser = StubArgParser(stub) + subparsers.return_add_parser = parser + + add_subparser("discover", "pytest", subparsers) + + self.assertEqual( + stub.calls, + [ + ("subparsers.add_parser", None, {"name": "pytest"}), + ], + ) + + def test_unsupported_command(self): + subparsers = StubSubparsers(name=None) + subparsers.return_add_parser = None + + with self.assertRaises(UnsupportedCommandError): + add_subparser("run", "pytest", subparsers) + with self.assertRaises(UnsupportedCommandError): + add_subparser("debug", "pytest", subparsers) + with self.assertRaises(UnsupportedCommandError): + add_subparser("???", "pytest", subparsers) + self.assertEqual( + subparsers.calls, + [ + ("add_parser", None, {"name": "pytest"}), + ("add_parser", None, {"name": "pytest"}), + ("add_parser", None, {"name": "pytest"}), + ], + ) diff --git a/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py b/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py new file mode 100644 index 000000000000..98b18567253e --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/pytest/test_discovery.py @@ -0,0 +1,1540 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import print_function, unicode_literals + +try: + from io import StringIO +except ImportError: + from StringIO import StringIO # type: ignore (for Pylance) +import os +import sys +import tempfile +import unittest +import warnings + +import pytest +import _pytest.doctest + +from .... import util +from testing_tools.adapter import util as adapter_util +from testing_tools.adapter.pytest import _pytest_item as pytest_item +from testing_tools.adapter import info +from testing_tools.adapter.pytest import _discovery + +# In Python 3.8 __len__ is called twice, which impacts some of the test assertions we do below. +PYTHON_38_OR_LATER = sys.version_info[0] >= 3 and sys.version_info[1] >= 8 + + +class StubPyTest(util.StubProxy): + def __init__(self, stub=None): + super(StubPyTest, self).__init__(stub, "pytest") + self.return_main = 0 + + def main(self, args, plugins): + self.add_call("main", None, {"args": args, "plugins": plugins}) + return self.return_main + + +class StubPlugin(util.StubProxy): + + _started = True + + def __init__(self, stub=None, tests=None): + super(StubPlugin, self).__init__(stub, "plugin") + if tests is None: + tests = StubDiscoveredTests(self.stub) + self._tests = tests + + def __getattr__(self, name): + if not name.startswith("pytest_"): + raise AttributeError(name) + + def func(*args, **kwargs): + self.add_call(name, args or None, kwargs or None) + + return func + + +class StubDiscoveredTests(util.StubProxy): + + NOT_FOUND = object() + + def __init__(self, stub=None): + super(StubDiscoveredTests, self).__init__(stub, "discovered") + self.return_items = [] + self.return_parents = [] + + def __len__(self): + self.add_call("__len__", None, None) + return len(self.return_items) + + def __getitem__(self, index): + self.add_call("__getitem__", (index,), None) + return self.return_items[index] + + @property + def parents(self): + self.add_call("parents", None, None) + return self.return_parents + + def reset(self): + self.add_call("reset", None, None) + + def add_test(self, test, parents): + self.add_call("add_test", None, {"test": test, "parents": parents}) + + +class FakeFunc(object): + def __init__(self, name): + self.__name__ = name + + +class FakeMarker(object): + def __init__(self, name): + self.name = name + + +class StubPytestItem(util.StubProxy): + + _debugging = False + _hasfunc = True + + def __init__(self, stub=None, **attrs): + super(StubPytestItem, self).__init__(stub, "pytest.Item") + if attrs.get("function") is None: + attrs.pop("function", None) + self._hasfunc = False + + attrs.setdefault("user_properties", []) + + self.__dict__.update(attrs) + + if "own_markers" not in attrs: + self.own_markers = () + + def __repr__(self): + return object.__repr__(self) + + def __getattr__(self, name): + if not self._debugging: + self.add_call(name + " (attr)", None, None) + if name == "function": + if not self._hasfunc: + raise AttributeError(name) + + def func(*args, **kwargs): + self.add_call(name, args or None, kwargs or None) + + return func + + +class StubSubtypedItem(StubPytestItem): + def __init__(self, *args, **kwargs): + super(StubSubtypedItem, self).__init__(*args, **kwargs) + if "nodeid" in self.__dict__: + self._nodeid = self.__dict__.pop("nodeid") + + @property + def location(self): + return self.__dict__.get("location") + + +class StubFunctionItem(StubSubtypedItem, pytest.Function): + @property + def function(self): + return self.__dict__.get("function") + + +def create_stub_function_item(*args, **kwargs): + # StubFunctionItem should not be calling __init__(), but instead from_parent(). + # Unfortunately the detangling is massive due to the complexity of the test + # harness, so we are punting in hopes that we rewrite test discovery before + # pytest removes this functionality. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + return StubFunctionItem(*args, **kwargs) + + +class StubDoctestItem(StubSubtypedItem, _pytest.doctest.DoctestItem): + pass + + +def create_stub_doctest_item(*args, **kwargs): + # StubDoctestItem should not be calling __init__(), but instead from_parent(). + # Unfortunately the detangling is massive due to the complexity of the test + # harness, so we are punting in hopes that we rewrite test discovery before + # pytest removes this functionality. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + return StubDoctestItem(*args, **kwargs) + + +class StubPytestSession(util.StubProxy): + def __init__(self, stub=None): + super(StubPytestSession, self).__init__(stub, "pytest.Session") + + def __getattr__(self, name): + self.add_call(name + " (attr)", None, None) + + def func(*args, **kwargs): + self.add_call(name, args or None, kwargs or None) + + return func + + +class StubPytestConfig(util.StubProxy): + def __init__(self, stub=None): + super(StubPytestConfig, self).__init__(stub, "pytest.Config") + + def __getattr__(self, name): + self.add_call(name + " (attr)", None, None) + + def func(*args, **kwargs): + self.add_call(name, args or None, kwargs or None) + + return func + + +def generate_parse_item(pathsep): + if pathsep == "\\": + + def normcase(path): + path = path.lower() + return path.replace("/", "\\") + + else: + raise NotImplementedError + ########## + def _fix_fileid(*args): + return adapter_util.fix_fileid( + *args, + **dict( + _normcase=normcase, + _pathsep=pathsep, + ) + ) + + def _normalize_test_id(*args): + return pytest_item._normalize_test_id( + *args, + **dict( + _fix_fileid=_fix_fileid, + _pathsep=pathsep, + ) + ) + + def _iter_nodes(*args): + return pytest_item._iter_nodes( + *args, + **dict( + _normalize_test_id=_normalize_test_id, + _normcase=normcase, + _pathsep=pathsep, + ) + ) + + def _parse_node_id(*args): + return pytest_item._parse_node_id( + *args, + **dict( + _iter_nodes=_iter_nodes, + ) + ) + + ########## + def _split_fspath(*args): + return pytest_item._split_fspath( + *args, + **dict( + _normcase=normcase, + ) + ) + + ########## + def _matches_relfile(*args): + return pytest_item._matches_relfile( + *args, + **dict( + _normcase=normcase, + _pathsep=pathsep, + ) + ) + + def _is_legacy_wrapper(*args): + return pytest_item._is_legacy_wrapper( + *args, + **dict( + _pathsep=pathsep, + ) + ) + + def _get_location(*args): + return pytest_item._get_location( + *args, + **dict( + _matches_relfile=_matches_relfile, + _is_legacy_wrapper=_is_legacy_wrapper, + _pathsep=pathsep, + ) + ) + + ########## + def _parse_item(item): + return pytest_item.parse_item( + item, + **dict( + _parse_node_id=_parse_node_id, + _split_fspath=_split_fspath, + _get_location=_get_location, + ) + ) + + return _parse_item + + +################################## +# tests + + +def fake_pytest_main(stub, use_fd, pytest_stdout): + def ret(args, plugins): + stub.add_call("pytest.main", None, {"args": args, "plugins": plugins}) + if use_fd: + os.write(sys.stdout.fileno(), pytest_stdout.encode()) + else: + print(pytest_stdout, end="") + return 0 + + return ret + + +class DiscoverTests(unittest.TestCase): + + DEFAULT_ARGS = [ + "--collect-only", + ] + + def test_basic(self): + stub = util.Stub() + stubpytest = StubPyTest(stub) + plugin = StubPlugin(stub) + expected = [] + plugin.discovered = expected + calls = [ + ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), + ("discovered.parents", None, None), + ("discovered.__len__", None, None), + ("discovered.__getitem__", (0,), None), + ] + + # In Python 3.8 __len__ is called twice. + if PYTHON_38_OR_LATER: + calls.insert(3, ("discovered.__len__", None, None)) + + parents, tests = _discovery.discover( + [], _pytest_main=stubpytest.main, _plugin=plugin + ) + + self.assertEqual(parents, []) + self.assertEqual(tests, expected) + self.assertEqual(stub.calls, calls) + + def test_failure(self): + stub = util.Stub() + pytest = StubPyTest(stub) + pytest.return_main = 2 + plugin = StubPlugin(stub) + + with self.assertRaises(Exception): + _discovery.discover([], _pytest_main=pytest.main, _plugin=plugin) + + self.assertEqual( + stub.calls, + [ + ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), + ], + ) + + def test_no_tests_found(self): + stub = util.Stub() + pytest = StubPyTest(stub) + pytest.return_main = 5 + plugin = StubPlugin(stub) + expected = [] + plugin.discovered = expected + calls = [ + ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), + ("discovered.parents", None, None), + ("discovered.__len__", None, None), + ("discovered.__getitem__", (0,), None), + ] + + # In Python 3.8 __len__ is called twice. + if PYTHON_38_OR_LATER: + calls.insert(3, ("discovered.__len__", None, None)) + + parents, tests = _discovery.discover( + [], _pytest_main=pytest.main, _plugin=plugin + ) + + self.assertEqual(parents, []) + self.assertEqual(tests, expected) + self.assertEqual(stub.calls, calls) + + def test_stdio_hidden_file(self): + stub = util.Stub() + + plugin = StubPlugin(stub) + plugin.discovered = [] + calls = [ + ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), + ("discovered.parents", None, None), + ("discovered.__len__", None, None), + ("discovered.__getitem__", (0,), None), + ] + pytest_stdout = "spamspamspamspamspamspamspammityspam" + + # In Python 3.8 __len__ is called twice. + if PYTHON_38_OR_LATER: + calls.insert(3, ("discovered.__len__", None, None)) + + # to simulate stdio behavior in methods like os.dup, + # use actual files (rather than StringIO) + with tempfile.TemporaryFile("r+") as mock: + sys.stdout = mock + try: + _discovery.discover( + [], + hidestdio=True, + _pytest_main=fake_pytest_main(stub, False, pytest_stdout), + _plugin=plugin, + ) + finally: + sys.stdout = sys.__stdout__ + + mock.seek(0) + captured = mock.read() + + self.assertEqual(captured, "") + self.assertEqual(stub.calls, calls) + + def test_stdio_hidden_fd(self): + # simulate cases where stdout comes from the lower layer than sys.stdout + # via file descriptors (e.g., from cython) + stub = util.Stub() + plugin = StubPlugin(stub) + pytest_stdout = "spamspamspamspamspamspamspammityspam" + + # Replace with contextlib.redirect_stdout() once Python 2.7 support is dropped. + sys.stdout = StringIO() + try: + _discovery.discover( + [], + hidestdio=True, + _pytest_main=fake_pytest_main(stub, True, pytest_stdout), + _plugin=plugin, + ) + captured = sys.stdout.read() + self.assertEqual(captured, "") + finally: + sys.stdout = sys.__stdout__ + + def test_stdio_not_hidden_file(self): + stub = util.Stub() + + plugin = StubPlugin(stub) + plugin.discovered = [] + calls = [ + ("pytest.main", None, {"args": self.DEFAULT_ARGS, "plugins": [plugin]}), + ("discovered.parents", None, None), + ("discovered.__len__", None, None), + ("discovered.__getitem__", (0,), None), + ] + pytest_stdout = "spamspamspamspamspamspamspammityspam" + + # In Python 3.8 __len__ is called twice. + if PYTHON_38_OR_LATER: + calls.insert(3, ("discovered.__len__", None, None)) + + buf = StringIO() + + sys.stdout = buf + try: + _discovery.discover( + [], + hidestdio=False, + _pytest_main=fake_pytest_main(stub, False, pytest_stdout), + _plugin=plugin, + ) + finally: + sys.stdout = sys.__stdout__ + captured = buf.getvalue() + + self.assertEqual(captured, pytest_stdout) + self.assertEqual(stub.calls, calls) + + def test_stdio_not_hidden_fd(self): + # simulate cases where stdout comes from the lower layer than sys.stdout + # via file descriptors (e.g., from cython) + stub = util.Stub() + plugin = StubPlugin(stub) + pytest_stdout = "spamspamspamspamspamspamspammityspam" + stub.calls = [] + with tempfile.TemporaryFile("r+") as mock: + sys.stdout = mock + try: + _discovery.discover( + [], + hidestdio=False, + _pytest_main=fake_pytest_main(stub, True, pytest_stdout), + _plugin=plugin, + ) + finally: + mock.seek(0) + captured = sys.stdout.read() + sys.stdout = sys.__stdout__ + self.assertEqual(captured, pytest_stdout) + + +class CollectorTests(unittest.TestCase): + def test_modifyitems(self): + stub = util.Stub() + discovered = StubDiscoveredTests(stub) + session = StubPytestSession(stub) + config = StubPytestConfig(stub) + collector = _discovery.TestCollector(tests=discovered) + + testroot = adapter_util.fix_path("/a/b/c") + relfile1 = adapter_util.fix_path("./test_spam.py") + relfile2 = adapter_util.fix_path("x/y/z/test_eggs.py") + + collector.pytest_collection_modifyitems( + session, + config, + [ + create_stub_function_item( + stub, + nodeid="test_spam.py::SpamTests::test_one", + name="test_one", + location=("test_spam.py", 12, "SpamTests.test_one"), + fspath=adapter_util.PATH_JOIN(testroot, "test_spam.py"), + function=FakeFunc("test_one"), + ), + create_stub_function_item( + stub, + nodeid="test_spam.py::SpamTests::test_other", + name="test_other", + location=("test_spam.py", 19, "SpamTests.test_other"), + fspath=adapter_util.PATH_JOIN(testroot, "test_spam.py"), + function=FakeFunc("test_other"), + ), + create_stub_function_item( + stub, + nodeid="test_spam.py::test_all", + name="test_all", + location=("test_spam.py", 144, "test_all"), + fspath=adapter_util.PATH_JOIN(testroot, "test_spam.py"), + function=FakeFunc("test_all"), + ), + create_stub_function_item( + stub, + nodeid="test_spam.py::test_each[10-10]", + name="test_each[10-10]", + location=("test_spam.py", 273, "test_each[10-10]"), + fspath=adapter_util.PATH_JOIN(testroot, "test_spam.py"), + function=FakeFunc("test_each"), + ), + create_stub_function_item( + stub, + nodeid=relfile2 + "::All::BasicTests::test_first", + name="test_first", + location=(relfile2, 31, "All.BasicTests.test_first"), + fspath=adapter_util.PATH_JOIN(testroot, relfile2), + function=FakeFunc("test_first"), + ), + create_stub_function_item( + stub, + nodeid=relfile2 + "::All::BasicTests::test_each[1+2-3]", + name="test_each[1+2-3]", + location=(relfile2, 62, "All.BasicTests.test_each[1+2-3]"), + fspath=adapter_util.PATH_JOIN(testroot, relfile2), + function=FakeFunc("test_each"), + own_markers=[ + FakeMarker(v) + for v in [ + # supported + "skip", + "skipif", + "xfail", + # duplicate + "skip", + # ignored (pytest-supported) + "parameterize", + "usefixtures", + "filterwarnings", + # ignored (custom) + "timeout", + ] + ], + ), + ], + ) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("discovered.reset", None, None), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./test_spam.py::SpamTests", "SpamTests", "suite"), + ("./test_spam.py", "test_spam.py", "file"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./test_spam.py::SpamTests::test_one", + name="test_one", + path=info.SingleTestPath( + root=testroot, + relfile=relfile1, + func="SpamTests.test_one", + sub=None, + ), + source="{}:{}".format(relfile1, 13), + markers=None, + parentid="./test_spam.py::SpamTests", + ), + ), + ), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./test_spam.py::SpamTests", "SpamTests", "suite"), + ("./test_spam.py", "test_spam.py", "file"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./test_spam.py::SpamTests::test_other", + name="test_other", + path=info.SingleTestPath( + root=testroot, + relfile=relfile1, + func="SpamTests.test_other", + sub=None, + ), + source="{}:{}".format(relfile1, 20), + markers=None, + parentid="./test_spam.py::SpamTests", + ), + ), + ), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./test_spam.py", "test_spam.py", "file"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./test_spam.py::test_all", + name="test_all", + path=info.SingleTestPath( + root=testroot, + relfile=relfile1, + func="test_all", + sub=None, + ), + source="{}:{}".format(relfile1, 145), + markers=None, + parentid="./test_spam.py", + ), + ), + ), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./test_spam.py::test_each", "test_each", "function"), + ("./test_spam.py", "test_spam.py", "file"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./test_spam.py::test_each[10-10]", + name="test_each[10-10]", + path=info.SingleTestPath( + root=testroot, + relfile=relfile1, + func="test_each", + sub=["[10-10]"], + ), + source="{}:{}".format(relfile1, 274), + markers=None, + parentid="./test_spam.py::test_each", + ), + ), + ), + ( + "discovered.add_test", + None, + dict( + parents=[ + ( + "./x/y/z/test_eggs.py::All::BasicTests", + "BasicTests", + "suite", + ), + ("./x/y/z/test_eggs.py::All", "All", "suite"), + ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), + ("./x/y/z", "z", "folder"), + ("./x/y", "y", "folder"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/y/z/test_eggs.py::All::BasicTests::test_first", + name="test_first", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(relfile2), + func="All.BasicTests.test_first", + sub=None, + ), + source="{}:{}".format( + adapter_util.fix_relpath(relfile2), 32 + ), + markers=None, + parentid="./x/y/z/test_eggs.py::All::BasicTests", + ), + ), + ), + ( + "discovered.add_test", + None, + dict( + parents=[ + ( + "./x/y/z/test_eggs.py::All::BasicTests::test_each", + "test_each", + "function", + ), + ( + "./x/y/z/test_eggs.py::All::BasicTests", + "BasicTests", + "suite", + ), + ("./x/y/z/test_eggs.py::All", "All", "suite"), + ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), + ("./x/y/z", "z", "folder"), + ("./x/y", "y", "folder"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/y/z/test_eggs.py::All::BasicTests::test_each[1+2-3]", + name="test_each[1+2-3]", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(relfile2), + func="All.BasicTests.test_each", + sub=["[1+2-3]"], + ), + source="{}:{}".format( + adapter_util.fix_relpath(relfile2), 63 + ), + markers=["expected-failure", "skip", "skip-if"], + parentid="./x/y/z/test_eggs.py::All::BasicTests::test_each", + ), + ), + ), + ], + ) + + def test_finish(self): + stub = util.Stub() + discovered = StubDiscoveredTests(stub) + session = StubPytestSession(stub) + testroot = adapter_util.fix_path("/a/b/c") + relfile = adapter_util.fix_path("x/y/z/test_eggs.py") + session.items = [ + create_stub_function_item( + stub, + nodeid=relfile + "::SpamTests::test_spam", + name="test_spam", + location=(relfile, 12, "SpamTests.test_spam"), + fspath=adapter_util.PATH_JOIN(testroot, relfile), + function=FakeFunc("test_spam"), + ), + ] + collector = _discovery.TestCollector(tests=discovered) + + collector.pytest_collection_finish(session) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("discovered.reset", None, None), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./x/y/z/test_eggs.py::SpamTests", "SpamTests", "suite"), + ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), + ("./x/y/z", "z", "folder"), + ("./x/y", "y", "folder"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/y/z/test_eggs.py::SpamTests::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(relfile), + func="SpamTests.test_spam", + sub=None, + ), + source="{}:{}".format( + adapter_util.fix_relpath(relfile), 13 + ), + markers=None, + parentid="./x/y/z/test_eggs.py::SpamTests", + ), + ), + ), + ], + ) + + def test_doctest(self): + stub = util.Stub() + discovered = StubDiscoveredTests(stub) + session = StubPytestSession(stub) + testroot = adapter_util.fix_path("/a/b/c") + doctestfile = adapter_util.fix_path("x/test_doctest.txt") + relfile = adapter_util.fix_path("x/y/z/test_eggs.py") + session.items = [ + create_stub_doctest_item( + stub, + nodeid=doctestfile + "::test_doctest.txt", + name="test_doctest.txt", + location=(doctestfile, 0, "[doctest] test_doctest.txt"), + fspath=adapter_util.PATH_JOIN(testroot, doctestfile), + ), + # With --doctest-modules + create_stub_doctest_item( + stub, + nodeid=relfile + "::test_eggs", + name="test_eggs", + location=(relfile, 0, "[doctest] test_eggs"), + fspath=adapter_util.PATH_JOIN(testroot, relfile), + ), + create_stub_doctest_item( + stub, + nodeid=relfile + "::test_eggs.TestSpam", + name="test_eggs.TestSpam", + location=(relfile, 12, "[doctest] test_eggs.TestSpam"), + fspath=adapter_util.PATH_JOIN(testroot, relfile), + ), + create_stub_doctest_item( + stub, + nodeid=relfile + "::test_eggs.TestSpam.TestEggs", + name="test_eggs.TestSpam.TestEggs", + location=(relfile, 27, "[doctest] test_eggs.TestSpam.TestEggs"), + fspath=adapter_util.PATH_JOIN(testroot, relfile), + ), + ] + collector = _discovery.TestCollector(tests=discovered) + + collector.pytest_collection_finish(session) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("discovered.reset", None, None), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./x/test_doctest.txt", "test_doctest.txt", "file"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/test_doctest.txt::test_doctest.txt", + name="test_doctest.txt", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(doctestfile), + func=None, + ), + source="{}:{}".format( + adapter_util.fix_relpath(doctestfile), 1 + ), + markers=[], + parentid="./x/test_doctest.txt", + ), + ), + ), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), + ("./x/y/z", "z", "folder"), + ("./x/y", "y", "folder"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/y/z/test_eggs.py::test_eggs", + name="test_eggs", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(relfile), + func=None, + ), + source="{}:{}".format(adapter_util.fix_relpath(relfile), 1), + markers=[], + parentid="./x/y/z/test_eggs.py", + ), + ), + ), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), + ("./x/y/z", "z", "folder"), + ("./x/y", "y", "folder"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/y/z/test_eggs.py::test_eggs.TestSpam", + name="test_eggs.TestSpam", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(relfile), + func=None, + ), + source="{}:{}".format( + adapter_util.fix_relpath(relfile), 13 + ), + markers=[], + parentid="./x/y/z/test_eggs.py", + ), + ), + ), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), + ("./x/y/z", "z", "folder"), + ("./x/y", "y", "folder"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/y/z/test_eggs.py::test_eggs.TestSpam.TestEggs", + name="test_eggs.TestSpam.TestEggs", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(relfile), + func=None, + ), + source="{}:{}".format( + adapter_util.fix_relpath(relfile), 28 + ), + markers=[], + parentid="./x/y/z/test_eggs.py", + ), + ), + ), + ], + ) + + def test_nested_brackets(self): + stub = util.Stub() + discovered = StubDiscoveredTests(stub) + session = StubPytestSession(stub) + testroot = adapter_util.fix_path("/a/b/c") + relfile = adapter_util.fix_path("x/y/z/test_eggs.py") + session.items = [ + create_stub_function_item( + stub, + nodeid=relfile + "::SpamTests::test_spam[a-[b]-c]", + name="test_spam[a-[b]-c]", + location=(relfile, 12, "SpamTests.test_spam[a-[b]-c]"), + fspath=adapter_util.PATH_JOIN(testroot, relfile), + function=FakeFunc("test_spam"), + ), + ] + collector = _discovery.TestCollector(tests=discovered) + + collector.pytest_collection_finish(session) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("discovered.reset", None, None), + ( + "discovered.add_test", + None, + dict( + parents=[ + ( + "./x/y/z/test_eggs.py::SpamTests::test_spam", + "test_spam", + "function", + ), + ("./x/y/z/test_eggs.py::SpamTests", "SpamTests", "suite"), + ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), + ("./x/y/z", "z", "folder"), + ("./x/y", "y", "folder"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/y/z/test_eggs.py::SpamTests::test_spam[a-[b]-c]", + name="test_spam[a-[b]-c]", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(relfile), + func="SpamTests.test_spam", + sub=["[a-[b]-c]"], + ), + source="{}:{}".format( + adapter_util.fix_relpath(relfile), 13 + ), + markers=None, + parentid="./x/y/z/test_eggs.py::SpamTests::test_spam", + ), + ), + ), + ], + ) + + def test_nested_suite(self): + stub = util.Stub() + discovered = StubDiscoveredTests(stub) + session = StubPytestSession(stub) + testroot = adapter_util.fix_path("/a/b/c") + relfile = adapter_util.fix_path("x/y/z/test_eggs.py") + session.items = [ + create_stub_function_item( + stub, + nodeid=relfile + "::SpamTests::Ham::Eggs::test_spam", + name="test_spam", + location=(relfile, 12, "SpamTests.Ham.Eggs.test_spam"), + fspath=adapter_util.PATH_JOIN(testroot, relfile), + function=FakeFunc("test_spam"), + ), + ] + collector = _discovery.TestCollector(tests=discovered) + + collector.pytest_collection_finish(session) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("discovered.reset", None, None), + ( + "discovered.add_test", + None, + dict( + parents=[ + ( + "./x/y/z/test_eggs.py::SpamTests::Ham::Eggs", + "Eggs", + "suite", + ), + ("./x/y/z/test_eggs.py::SpamTests::Ham", "Ham", "suite"), + ("./x/y/z/test_eggs.py::SpamTests", "SpamTests", "suite"), + ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), + ("./x/y/z", "z", "folder"), + ("./x/y", "y", "folder"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/y/z/test_eggs.py::SpamTests::Ham::Eggs::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(relfile), + func="SpamTests.Ham.Eggs.test_spam", + sub=None, + ), + source="{}:{}".format( + adapter_util.fix_relpath(relfile), 13 + ), + markers=None, + parentid="./x/y/z/test_eggs.py::SpamTests::Ham::Eggs", + ), + ), + ), + ], + ) + + def test_windows(self): + stub = util.Stub() + discovered = StubDiscoveredTests(stub) + session = StubPytestSession(stub) + testroot = r"C:\A\B\C" + altroot = testroot.replace("\\", "/") + relfile = r"X\Y\Z\test_Eggs.py" + session.items = [ + # typical: + create_stub_function_item( + stub, + # pytest always uses "/" as the path separator in node IDs: + nodeid="X/Y/Z/test_Eggs.py::SpamTests::test_spam", + name="test_spam", + # normal path separator (contrast with nodeid): + location=(relfile, 12, "SpamTests.test_spam"), + # path separator matches location: + fspath=testroot + "\\" + relfile, + function=FakeFunc("test_spam"), + ), + ] + tests = [ + # permutations of path separators + (r"X/test_a.py", "\\", "\\"), # typical + (r"X/test_b.py", "\\", "/"), + (r"X/test_c.py", "/", "\\"), + (r"X/test_d.py", "/", "/"), + (r"X\test_e.py", "\\", "\\"), + (r"X\test_f.py", "\\", "/"), + (r"X\test_g.py", "/", "\\"), + (r"X\test_h.py", "/", "/"), + ] + for fileid, locfile, fspath in tests: + if locfile == "/": + locfile = fileid.replace("\\", "/") + elif locfile == "\\": + locfile = fileid.replace("/", "\\") + if fspath == "/": + fspath = (testroot + "/" + fileid).replace("\\", "/") + elif fspath == "\\": + fspath = (testroot + "/" + fileid).replace("/", "\\") + session.items.append( + create_stub_function_item( + stub, + nodeid=fileid + "::test_spam", + name="test_spam", + location=(locfile, 12, "test_spam"), + fspath=fspath, + function=FakeFunc("test_spam"), + ) + ) + collector = _discovery.TestCollector(tests=discovered) + if os.name != "nt": + collector.parse_item = generate_parse_item("\\") + + collector.pytest_collection_finish(session) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("discovered.reset", None, None), + ( + "discovered.add_test", + None, + dict( + parents=[ + (r"./X/Y/Z/test_Eggs.py::SpamTests", "SpamTests", "suite"), + (r"./X/Y/Z/test_Eggs.py", "test_Eggs.py", "file"), + (r"./X/Y/Z", "Z", "folder"), + (r"./X/Y", "Y", "folder"), + (r"./X", "X", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id=r"./X/Y/Z/test_Eggs.py::SpamTests::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=testroot, # not normalized + relfile=r".\X\Y\Z\test_Eggs.py", # not normalized + func="SpamTests.test_spam", + sub=None, + ), + source=r".\X\Y\Z\test_Eggs.py:13", # not normalized + markers=None, + parentid=r"./X/Y/Z/test_Eggs.py::SpamTests", + ), + ), + ), + # permutations + # (*all* the IDs use "/") + # (source path separator should match relfile, not location) + # /, \, \ + ( + "discovered.add_test", + None, + dict( + parents=[ + (r"./X/test_a.py", "test_a.py", "file"), + (r"./X", "X", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id=r"./X/test_a.py::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=testroot, + relfile=r".\X\test_a.py", + func="test_spam", + sub=None, + ), + source=r".\X\test_a.py:13", + markers=None, + parentid=r"./X/test_a.py", + ), + ), + ), + # /, \, / + ( + "discovered.add_test", + None, + dict( + parents=[ + (r"./X/test_b.py", "test_b.py", "file"), + (r"./X", "X", "folder"), + (".", altroot, "folder"), + ], + test=info.SingleTestInfo( + id=r"./X/test_b.py::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=altroot, + relfile=r"./X/test_b.py", + func="test_spam", + sub=None, + ), + source=r"./X/test_b.py:13", + markers=None, + parentid=r"./X/test_b.py", + ), + ), + ), + # /, /, \ + ( + "discovered.add_test", + None, + dict( + parents=[ + (r"./X/test_c.py", "test_c.py", "file"), + (r"./X", "X", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id=r"./X/test_c.py::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=testroot, + relfile=r".\X\test_c.py", + func="test_spam", + sub=None, + ), + source=r".\X\test_c.py:13", + markers=None, + parentid=r"./X/test_c.py", + ), + ), + ), + # /, /, / + ( + "discovered.add_test", + None, + dict( + parents=[ + (r"./X/test_d.py", "test_d.py", "file"), + (r"./X", "X", "folder"), + (".", altroot, "folder"), + ], + test=info.SingleTestInfo( + id=r"./X/test_d.py::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=altroot, + relfile=r"./X/test_d.py", + func="test_spam", + sub=None, + ), + source=r"./X/test_d.py:13", + markers=None, + parentid=r"./X/test_d.py", + ), + ), + ), + # \, \, \ + ( + "discovered.add_test", + None, + dict( + parents=[ + (r"./X/test_e.py", "test_e.py", "file"), + (r"./X", "X", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id=r"./X/test_e.py::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=testroot, + relfile=r".\X\test_e.py", + func="test_spam", + sub=None, + ), + source=r".\X\test_e.py:13", + markers=None, + parentid=r"./X/test_e.py", + ), + ), + ), + # \, \, / + ( + "discovered.add_test", + None, + dict( + parents=[ + (r"./X/test_f.py", "test_f.py", "file"), + (r"./X", "X", "folder"), + (".", altroot, "folder"), + ], + test=info.SingleTestInfo( + id=r"./X/test_f.py::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=altroot, + relfile=r"./X/test_f.py", + func="test_spam", + sub=None, + ), + source=r"./X/test_f.py:13", + markers=None, + parentid=r"./X/test_f.py", + ), + ), + ), + # \, /, \ + ( + "discovered.add_test", + None, + dict( + parents=[ + (r"./X/test_g.py", "test_g.py", "file"), + (r"./X", "X", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id=r"./X/test_g.py::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=testroot, + relfile=r".\X\test_g.py", + func="test_spam", + sub=None, + ), + source=r".\X\test_g.py:13", + markers=None, + parentid=r"./X/test_g.py", + ), + ), + ), + # \, /, / + ( + "discovered.add_test", + None, + dict( + parents=[ + (r"./X/test_h.py", "test_h.py", "file"), + (r"./X", "X", "folder"), + (".", altroot, "folder"), + ], + test=info.SingleTestInfo( + id=r"./X/test_h.py::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=altroot, + relfile=r"./X/test_h.py", + func="test_spam", + sub=None, + ), + source=r"./X/test_h.py:13", + markers=None, + parentid=r"./X/test_h.py", + ), + ), + ), + ], + ) + + def test_mysterious_parens(self): + stub = util.Stub() + discovered = StubDiscoveredTests(stub) + session = StubPytestSession(stub) + testroot = adapter_util.fix_path("/a/b/c") + relfile = adapter_util.fix_path("x/y/z/test_eggs.py") + session.items = [ + create_stub_function_item( + stub, + nodeid=relfile + "::SpamTests::()::()::test_spam", + name="test_spam", + location=(relfile, 12, "SpamTests.test_spam"), + fspath=adapter_util.PATH_JOIN(testroot, relfile), + function=FakeFunc("test_spam"), + ), + ] + collector = _discovery.TestCollector(tests=discovered) + + collector.pytest_collection_finish(session) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("discovered.reset", None, None), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./x/y/z/test_eggs.py::SpamTests", "SpamTests", "suite"), + ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), + ("./x/y/z", "z", "folder"), + ("./x/y", "y", "folder"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/y/z/test_eggs.py::SpamTests::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(relfile), + func="SpamTests.test_spam", + sub=[], + ), + source="{}:{}".format( + adapter_util.fix_relpath(relfile), 13 + ), + markers=None, + parentid="./x/y/z/test_eggs.py::SpamTests", + ), + ), + ), + ], + ) + + def test_imported_test(self): + # pytest will even discover tests that were imported from + # another module! + stub = util.Stub() + discovered = StubDiscoveredTests(stub) + session = StubPytestSession(stub) + testroot = adapter_util.fix_path("/a/b/c") + relfile = adapter_util.fix_path("x/y/z/test_eggs.py") + srcfile = adapter_util.fix_path("x/y/z/_extern.py") + session.items = [ + create_stub_function_item( + stub, + nodeid=relfile + "::SpamTests::test_spam", + name="test_spam", + location=(srcfile, 12, "SpamTests.test_spam"), + fspath=adapter_util.PATH_JOIN(testroot, relfile), + function=FakeFunc("test_spam"), + ), + create_stub_function_item( + stub, + nodeid=relfile + "::test_ham", + name="test_ham", + location=(srcfile, 3, "test_ham"), + fspath=adapter_util.PATH_JOIN(testroot, relfile), + function=FakeFunc("test_spam"), + ), + ] + collector = _discovery.TestCollector(tests=discovered) + + collector.pytest_collection_finish(session) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("discovered.reset", None, None), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./x/y/z/test_eggs.py::SpamTests", "SpamTests", "suite"), + ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), + ("./x/y/z", "z", "folder"), + ("./x/y", "y", "folder"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/y/z/test_eggs.py::SpamTests::test_spam", + name="test_spam", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(relfile), + func="SpamTests.test_spam", + sub=None, + ), + source="{}:{}".format( + adapter_util.fix_relpath(srcfile), 13 + ), + markers=None, + parentid="./x/y/z/test_eggs.py::SpamTests", + ), + ), + ), + ( + "discovered.add_test", + None, + dict( + parents=[ + ("./x/y/z/test_eggs.py", "test_eggs.py", "file"), + ("./x/y/z", "z", "folder"), + ("./x/y", "y", "folder"), + ("./x", "x", "folder"), + (".", testroot, "folder"), + ], + test=info.SingleTestInfo( + id="./x/y/z/test_eggs.py::test_ham", + name="test_ham", + path=info.SingleTestPath( + root=testroot, + relfile=adapter_util.fix_relpath(relfile), + func="test_ham", + sub=None, + ), + source="{}:{}".format(adapter_util.fix_relpath(srcfile), 4), + markers=None, + parentid="./x/y/z/test_eggs.py", + ), + ), + ), + ], + ) diff --git a/pythonFiles/tests/testing_tools/adapter/test___main__.py b/pythonFiles/tests/testing_tools/adapter/test___main__.py new file mode 100644 index 000000000000..53500a2f4afe --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/test___main__.py @@ -0,0 +1,210 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + +from ...util import Stub, StubProxy +from testing_tools.adapter.__main__ import ( + parse_args, + main, + UnsupportedToolError, + UnsupportedCommandError, +) + + +class StubTool(StubProxy): + def __init__(self, name, stub=None): + super(StubTool, self).__init__(stub, name) + self.return_discover = None + + def discover(self, args, **kwargs): + self.add_call("discover", (args,), kwargs) + if self.return_discover is None: + raise NotImplementedError + return self.return_discover + + +class StubReporter(StubProxy): + def __init__(self, stub=None): + super(StubReporter, self).__init__(stub, "reporter") + + def report(self, tests, parents, **kwargs): + self.add_call("report", (tests, parents), kwargs or None) + + +################################## +# tests + + +class ParseGeneralTests(unittest.TestCase): + def test_unsupported_command(self): + with self.assertRaises(SystemExit): + parse_args(["run", "pytest"]) + with self.assertRaises(SystemExit): + parse_args(["debug", "pytest"]) + with self.assertRaises(SystemExit): + parse_args(["???", "pytest"]) + + +class ParseDiscoverTests(unittest.TestCase): + def test_pytest_default(self): + tool, cmd, args, toolargs = parse_args( + [ + "discover", + "pytest", + ] + ) + + self.assertEqual(tool, "pytest") + self.assertEqual(cmd, "discover") + self.assertEqual(args, {"pretty": False, "hidestdio": True, "simple": False}) + self.assertEqual(toolargs, []) + + def test_pytest_full(self): + tool, cmd, args, toolargs = parse_args( + [ + "discover", + "pytest", + # no adapter-specific options yet + "--", + "--strict", + "--ignore", + "spam,ham,eggs", + "--pastebin=xyz", + "--no-cov", + "-d", + ] + ) + + self.assertEqual(tool, "pytest") + self.assertEqual(cmd, "discover") + self.assertEqual(args, {"pretty": False, "hidestdio": True, "simple": False}) + self.assertEqual( + toolargs, + [ + "--strict", + "--ignore", + "spam,ham,eggs", + "--pastebin=xyz", + "--no-cov", + "-d", + ], + ) + + def test_pytest_opts(self): + tool, cmd, args, toolargs = parse_args( + [ + "discover", + "pytest", + "--simple", + "--no-hide-stdio", + "--pretty", + ] + ) + + self.assertEqual(tool, "pytest") + self.assertEqual(cmd, "discover") + self.assertEqual(args, {"pretty": True, "hidestdio": False, "simple": True}) + self.assertEqual(toolargs, []) + + def test_unsupported_tool(self): + with self.assertRaises(SystemExit): + parse_args(["discover", "unittest"]) + with self.assertRaises(SystemExit): + parse_args(["discover", "nose"]) + with self.assertRaises(SystemExit): + parse_args(["discover", "???"]) + + +class MainTests(unittest.TestCase): + + # TODO: We could use an integration test for pytest.discover(). + + def test_discover(self): + stub = Stub() + tool = StubTool("spamspamspam", stub) + tests, parents = object(), object() + tool.return_discover = (parents, tests) + reporter = StubReporter(stub) + main( + tool.name, + "discover", + {"spam": "eggs"}, + [], + _tools={ + tool.name: { + "discover": tool.discover, + } + }, + _reporters={ + "discover": reporter.report, + }, + ) + + self.assertEqual( + tool.calls, + [ + ("spamspamspam.discover", ([],), {"spam": "eggs"}), + ("reporter.report", (tests, parents), {"spam": "eggs"}), + ], + ) + + def test_unsupported_tool(self): + with self.assertRaises(UnsupportedToolError): + main( + "unittest", + "discover", + {"spam": "eggs"}, + [], + _tools={"pytest": None}, + _reporters=None, + ) + with self.assertRaises(UnsupportedToolError): + main( + "nose", + "discover", + {"spam": "eggs"}, + [], + _tools={"pytest": None}, + _reporters=None, + ) + with self.assertRaises(UnsupportedToolError): + main( + "???", + "discover", + {"spam": "eggs"}, + [], + _tools={"pytest": None}, + _reporters=None, + ) + + def test_unsupported_command(self): + tool = StubTool("pytest") + with self.assertRaises(UnsupportedCommandError): + main( + "pytest", + "run", + {"spam": "eggs"}, + [], + _tools={"pytest": {"discover": tool.discover}}, + _reporters=None, + ) + with self.assertRaises(UnsupportedCommandError): + main( + "pytest", + "debug", + {"spam": "eggs"}, + [], + _tools={"pytest": {"discover": tool.discover}}, + _reporters=None, + ) + with self.assertRaises(UnsupportedCommandError): + main( + "pytest", + "???", + {"spam": "eggs"}, + [], + _tools={"pytest": {"discover": tool.discover}}, + _reporters=None, + ) + self.assertEqual(tool.calls, []) diff --git a/pythonFiles/tests/testing_tools/adapter/test_discovery.py b/pythonFiles/tests/testing_tools/adapter/test_discovery.py new file mode 100644 index 000000000000..ec3d198b0108 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/test_discovery.py @@ -0,0 +1,675 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import absolute_import, print_function + +import unittest + +from testing_tools.adapter.util import fix_path, fix_relpath +from testing_tools.adapter.info import SingleTestInfo, SingleTestPath, ParentInfo +from testing_tools.adapter.discovery import fix_nodeid, DiscoveredTests + + +def _fix_nodeid(nodeid): + + nodeid = nodeid.replace("\\", "/") + if not nodeid.startswith("./"): + nodeid = "./" + nodeid + return nodeid + + +class DiscoveredTestsTests(unittest.TestCase): + def test_list(self): + testroot = fix_path("/a/b/c") + relfile = fix_path("./test_spam.py") + tests = [ + SingleTestInfo( + # missing "./": + id="test_spam.py::test_each[10-10]", + name="test_each[10-10]", + path=SingleTestPath( + root=testroot, + relfile=relfile, + func="test_each", + sub=["[10-10]"], + ), + source="{}:{}".format(relfile, 10), + markers=None, + # missing "./": + parentid="test_spam.py::test_each", + ), + SingleTestInfo( + id="test_spam.py::All::BasicTests::test_first", + name="test_first", + path=SingleTestPath( + root=testroot, + relfile=relfile, + func="All.BasicTests.test_first", + sub=None, + ), + source="{}:{}".format(relfile, 62), + markers=None, + parentid="test_spam.py::All::BasicTests", + ), + ] + allparents = [ + [ + (fix_path("./test_spam.py::test_each"), "test_each", "function"), + (fix_path("./test_spam.py"), "test_spam.py", "file"), + (".", testroot, "folder"), + ], + [ + (fix_path("./test_spam.py::All::BasicTests"), "BasicTests", "suite"), + (fix_path("./test_spam.py::All"), "All", "suite"), + (fix_path("./test_spam.py"), "test_spam.py", "file"), + (".", testroot, "folder"), + ], + ] + expected = [ + test._replace(id=_fix_nodeid(test.id), parentid=_fix_nodeid(test.parentid)) + for test in tests + ] + discovered = DiscoveredTests() + for test, parents in zip(tests, allparents): + discovered.add_test(test, parents) + size = len(discovered) + items = [discovered[0], discovered[1]] + snapshot = list(discovered) + + self.maxDiff = None + self.assertEqual(size, 2) + self.assertEqual(items, expected) + self.assertEqual(snapshot, expected) + + def test_reset(self): + testroot = fix_path("/a/b/c") + discovered = DiscoveredTests() + discovered.add_test( + SingleTestInfo( + id="./test_spam.py::test_each", + name="test_each", + path=SingleTestPath( + root=testroot, + relfile="test_spam.py", + func="test_each", + ), + source="test_spam.py:11", + markers=[], + parentid="./test_spam.py", + ), + [ + ("./test_spam.py", "test_spam.py", "file"), + (".", testroot, "folder"), + ], + ) + + before = len(discovered), len(discovered.parents) + discovered.reset() + after = len(discovered), len(discovered.parents) + + self.assertEqual(before, (1, 2)) + self.assertEqual(after, (0, 0)) + + def test_parents(self): + testroot = fix_path("/a/b/c") + relfile = fix_path("x/y/z/test_spam.py") + tests = [ + SingleTestInfo( + # missing "./", using pathsep: + id=relfile + "::test_each[10-10]", + name="test_each[10-10]", + path=SingleTestPath( + root=testroot, + relfile=fix_relpath(relfile), + func="test_each", + sub=["[10-10]"], + ), + source="{}:{}".format(relfile, 10), + markers=None, + # missing "./", using pathsep: + parentid=relfile + "::test_each", + ), + SingleTestInfo( + # missing "./", using pathsep: + id=relfile + "::All::BasicTests::test_first", + name="test_first", + path=SingleTestPath( + root=testroot, + relfile=fix_relpath(relfile), + func="All.BasicTests.test_first", + sub=None, + ), + source="{}:{}".format(relfile, 61), + markers=None, + # missing "./", using pathsep: + parentid=relfile + "::All::BasicTests", + ), + ] + allparents = [ + # missing "./", using pathsep: + [ + (relfile + "::test_each", "test_each", "function"), + (relfile, relfile, "file"), + (".", testroot, "folder"), + ], + # missing "./", using pathsep: + [ + (relfile + "::All::BasicTests", "BasicTests", "suite"), + (relfile + "::All", "All", "suite"), + (relfile, "test_spam.py", "file"), + (fix_path("x/y/z"), "z", "folder"), + (fix_path("x/y"), "y", "folder"), + (fix_path("./x"), "x", "folder"), + (".", testroot, "folder"), + ], + ] + discovered = DiscoveredTests() + for test, parents in zip(tests, allparents): + discovered.add_test(test, parents) + + parents = discovered.parents + + self.maxDiff = None + self.assertEqual( + parents, + [ + ParentInfo( + id=".", + kind="folder", + name=testroot, + ), + ParentInfo( + id="./x", + kind="folder", + name="x", + root=testroot, + relpath=fix_path("./x"), + parentid=".", + ), + ParentInfo( + id="./x/y", + kind="folder", + name="y", + root=testroot, + relpath=fix_path("./x/y"), + parentid="./x", + ), + ParentInfo( + id="./x/y/z", + kind="folder", + name="z", + root=testroot, + relpath=fix_path("./x/y/z"), + parentid="./x/y", + ), + ParentInfo( + id="./x/y/z/test_spam.py", + kind="file", + name="test_spam.py", + root=testroot, + relpath=fix_relpath(relfile), + parentid="./x/y/z", + ), + ParentInfo( + id="./x/y/z/test_spam.py::All", + kind="suite", + name="All", + root=testroot, + parentid="./x/y/z/test_spam.py", + ), + ParentInfo( + id="./x/y/z/test_spam.py::All::BasicTests", + kind="suite", + name="BasicTests", + root=testroot, + parentid="./x/y/z/test_spam.py::All", + ), + ParentInfo( + id="./x/y/z/test_spam.py::test_each", + kind="function", + name="test_each", + root=testroot, + parentid="./x/y/z/test_spam.py", + ), + ], + ) + + def test_add_test_simple(self): + testroot = fix_path("/a/b/c") + relfile = "test_spam.py" + test = SingleTestInfo( + # missing "./": + id=relfile + "::test_spam", + name="test_spam", + path=SingleTestPath( + root=testroot, + # missing "./": + relfile=relfile, + func="test_spam", + ), + # missing "./": + source="{}:{}".format(relfile, 11), + markers=[], + # missing "./": + parentid=relfile, + ) + expected = test._replace( + id=_fix_nodeid(test.id), parentid=_fix_nodeid(test.parentid) + ) + discovered = DiscoveredTests() + + before = list(discovered), discovered.parents + discovered.add_test( + test, + [ + (relfile, relfile, "file"), + (".", testroot, "folder"), + ], + ) + after = list(discovered), discovered.parents + + self.maxDiff = None + self.assertEqual(before, ([], [])) + self.assertEqual( + after, + ( + [expected], + [ + ParentInfo( + id=".", + kind="folder", + name=testroot, + ), + ParentInfo( + id="./test_spam.py", + kind="file", + name=relfile, + root=testroot, + relpath=relfile, + parentid=".", + ), + ], + ), + ) + + def test_multiroot(self): + # the first root + testroot1 = fix_path("/a/b/c") + relfile1 = "test_spam.py" + alltests = [ + SingleTestInfo( + # missing "./": + id=relfile1 + "::test_spam", + name="test_spam", + path=SingleTestPath( + root=testroot1, + relfile=fix_relpath(relfile1), + func="test_spam", + ), + source="{}:{}".format(relfile1, 10), + markers=[], + # missing "./": + parentid=relfile1, + ), + ] + allparents = [ + # missing "./": + [ + (relfile1, "test_spam.py", "file"), + (".", testroot1, "folder"), + ], + ] + # the second root + testroot2 = fix_path("/x/y/z") + relfile2 = fix_path("w/test_eggs.py") + alltests.extend( + [ + SingleTestInfo( + id=relfile2 + "::BasicTests::test_first", + name="test_first", + path=SingleTestPath( + root=testroot2, + relfile=fix_relpath(relfile2), + func="BasicTests.test_first", + ), + source="{}:{}".format(relfile2, 61), + markers=[], + parentid=relfile2 + "::BasicTests", + ), + ] + ) + allparents.extend( + [ + # missing "./", using pathsep: + [ + (relfile2 + "::BasicTests", "BasicTests", "suite"), + (relfile2, "test_eggs.py", "file"), + (fix_path("./w"), "w", "folder"), + (".", testroot2, "folder"), + ], + ] + ) + + discovered = DiscoveredTests() + for test, parents in zip(alltests, allparents): + discovered.add_test(test, parents) + tests = list(discovered) + parents = discovered.parents + + self.maxDiff = None + self.assertEqual( + tests, + [ + # the first root + SingleTestInfo( + id="./test_spam.py::test_spam", + name="test_spam", + path=SingleTestPath( + root=testroot1, + relfile=fix_relpath(relfile1), + func="test_spam", + ), + source="{}:{}".format(relfile1, 10), + markers=[], + parentid="./test_spam.py", + ), + # the secondroot + SingleTestInfo( + id="./w/test_eggs.py::BasicTests::test_first", + name="test_first", + path=SingleTestPath( + root=testroot2, + relfile=fix_relpath(relfile2), + func="BasicTests.test_first", + ), + source="{}:{}".format(relfile2, 61), + markers=[], + parentid="./w/test_eggs.py::BasicTests", + ), + ], + ) + self.assertEqual( + parents, + [ + # the first root + ParentInfo( + id=".", + kind="folder", + name=testroot1, + ), + ParentInfo( + id="./test_spam.py", + kind="file", + name="test_spam.py", + root=testroot1, + relpath=fix_relpath(relfile1), + parentid=".", + ), + # the secondroot + ParentInfo( + id=".", + kind="folder", + name=testroot2, + ), + ParentInfo( + id="./w", + kind="folder", + name="w", + root=testroot2, + relpath=fix_path("./w"), + parentid=".", + ), + ParentInfo( + id="./w/test_eggs.py", + kind="file", + name="test_eggs.py", + root=testroot2, + relpath=fix_relpath(relfile2), + parentid="./w", + ), + ParentInfo( + id="./w/test_eggs.py::BasicTests", + kind="suite", + name="BasicTests", + root=testroot2, + parentid="./w/test_eggs.py", + ), + ], + ) + + def test_doctest(self): + testroot = fix_path("/a/b/c") + doctestfile = fix_path("./x/test_doctest.txt") + relfile = fix_path("./x/y/z/test_eggs.py") + alltests = [ + SingleTestInfo( + id=doctestfile + "::test_doctest.txt", + name="test_doctest.txt", + path=SingleTestPath( + root=testroot, + relfile=doctestfile, + func=None, + ), + source="{}:{}".format(doctestfile, 0), + markers=[], + parentid=doctestfile, + ), + # With --doctest-modules + SingleTestInfo( + id=relfile + "::test_eggs", + name="test_eggs", + path=SingleTestPath( + root=testroot, + relfile=relfile, + func=None, + ), + source="{}:{}".format(relfile, 0), + markers=[], + parentid=relfile, + ), + SingleTestInfo( + id=relfile + "::test_eggs.TestSpam", + name="test_eggs.TestSpam", + path=SingleTestPath( + root=testroot, + relfile=relfile, + func=None, + ), + source="{}:{}".format(relfile, 12), + markers=[], + parentid=relfile, + ), + SingleTestInfo( + id=relfile + "::test_eggs.TestSpam.TestEggs", + name="test_eggs.TestSpam.TestEggs", + path=SingleTestPath( + root=testroot, + relfile=relfile, + func=None, + ), + source="{}:{}".format(relfile, 27), + markers=[], + parentid=relfile, + ), + ] + allparents = [ + [ + (doctestfile, "test_doctest.txt", "file"), + (fix_path("./x"), "x", "folder"), + (".", testroot, "folder"), + ], + [ + (relfile, "test_eggs.py", "file"), + (fix_path("./x/y/z"), "z", "folder"), + (fix_path("./x/y"), "y", "folder"), + (fix_path("./x"), "x", "folder"), + (".", testroot, "folder"), + ], + [ + (relfile, "test_eggs.py", "file"), + (fix_path("./x/y/z"), "z", "folder"), + (fix_path("./x/y"), "y", "folder"), + (fix_path("./x"), "x", "folder"), + (".", testroot, "folder"), + ], + [ + (relfile, "test_eggs.py", "file"), + (fix_path("./x/y/z"), "z", "folder"), + (fix_path("./x/y"), "y", "folder"), + (fix_path("./x"), "x", "folder"), + (".", testroot, "folder"), + ], + ] + expected = [ + test._replace(id=_fix_nodeid(test.id), parentid=_fix_nodeid(test.parentid)) + for test in alltests + ] + + discovered = DiscoveredTests() + + for test, parents in zip(alltests, allparents): + discovered.add_test(test, parents) + tests = list(discovered) + parents = discovered.parents + + self.maxDiff = None + self.assertEqual(tests, expected) + self.assertEqual( + parents, + [ + ParentInfo( + id=".", + kind="folder", + name=testroot, + ), + ParentInfo( + id="./x", + kind="folder", + name="x", + root=testroot, + relpath=fix_path("./x"), + parentid=".", + ), + ParentInfo( + id="./x/test_doctest.txt", + kind="file", + name="test_doctest.txt", + root=testroot, + relpath=fix_path(doctestfile), + parentid="./x", + ), + ParentInfo( + id="./x/y", + kind="folder", + name="y", + root=testroot, + relpath=fix_path("./x/y"), + parentid="./x", + ), + ParentInfo( + id="./x/y/z", + kind="folder", + name="z", + root=testroot, + relpath=fix_path("./x/y/z"), + parentid="./x/y", + ), + ParentInfo( + id="./x/y/z/test_eggs.py", + kind="file", + name="test_eggs.py", + root=testroot, + relpath=fix_relpath(relfile), + parentid="./x/y/z", + ), + ], + ) + + def test_nested_suite_simple(self): + testroot = fix_path("/a/b/c") + relfile = fix_path("./test_eggs.py") + alltests = [ + SingleTestInfo( + id=relfile + "::TestOuter::TestInner::test_spam", + name="test_spam", + path=SingleTestPath( + root=testroot, + relfile=relfile, + func="TestOuter.TestInner.test_spam", + ), + source="{}:{}".format(relfile, 10), + markers=None, + parentid=relfile + "::TestOuter::TestInner", + ), + SingleTestInfo( + id=relfile + "::TestOuter::TestInner::test_eggs", + name="test_eggs", + path=SingleTestPath( + root=testroot, + relfile=relfile, + func="TestOuter.TestInner.test_eggs", + ), + source="{}:{}".format(relfile, 21), + markers=None, + parentid=relfile + "::TestOuter::TestInner", + ), + ] + allparents = [ + [ + (relfile + "::TestOuter::TestInner", "TestInner", "suite"), + (relfile + "::TestOuter", "TestOuter", "suite"), + (relfile, "test_eggs.py", "file"), + (".", testroot, "folder"), + ], + [ + (relfile + "::TestOuter::TestInner", "TestInner", "suite"), + (relfile + "::TestOuter", "TestOuter", "suite"), + (relfile, "test_eggs.py", "file"), + (".", testroot, "folder"), + ], + ] + expected = [ + test._replace(id=_fix_nodeid(test.id), parentid=_fix_nodeid(test.parentid)) + for test in alltests + ] + + discovered = DiscoveredTests() + for test, parents in zip(alltests, allparents): + discovered.add_test(test, parents) + tests = list(discovered) + parents = discovered.parents + + self.maxDiff = None + self.assertEqual(tests, expected) + self.assertEqual( + parents, + [ + ParentInfo( + id=".", + kind="folder", + name=testroot, + ), + ParentInfo( + id="./test_eggs.py", + kind="file", + name="test_eggs.py", + root=testroot, + relpath=fix_relpath(relfile), + parentid=".", + ), + ParentInfo( + id="./test_eggs.py::TestOuter", + kind="suite", + name="TestOuter", + root=testroot, + parentid="./test_eggs.py", + ), + ParentInfo( + id="./test_eggs.py::TestOuter::TestInner", + kind="suite", + name="TestInner", + root=testroot, + parentid="./test_eggs.py::TestOuter", + ), + ], + ) diff --git a/pythonFiles/tests/testing_tools/adapter/test_functional.py b/pythonFiles/tests/testing_tools/adapter/test_functional.py new file mode 100644 index 000000000000..bd6c6b200314 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/test_functional.py @@ -0,0 +1,1528 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import absolute_import, unicode_literals + +import json +import os +import os.path +import subprocess +import sys +import unittest + +import pytest + +from ...__main__ import TESTING_TOOLS_ROOT +from testing_tools.adapter.util import fix_path, PATH_SEP + +# Pytest 3.7 and later uses pathlib/pathlib2 for path resolution. +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path # type: ignore (for Pylance) + + +CWD = os.getcwd() +DATA_DIR = os.path.join(os.path.dirname(__file__), ".data") +SCRIPT = os.path.join(TESTING_TOOLS_ROOT, "run_adapter.py") + + +def resolve_testroot(name): + projroot = os.path.join(DATA_DIR, name) + testroot = os.path.join(projroot, "tests") + return str(Path(projroot).resolve()), str(Path(testroot).resolve()) + + +def run_adapter(cmd, tool, *cliargs): + try: + return _run_adapter(cmd, tool, *cliargs) + except subprocess.CalledProcessError as exc: + print(exc.output) + + +def _run_adapter(cmd, tool, *cliargs, **kwargs): + hidestdio = kwargs.pop("hidestdio", True) + assert not kwargs or tuple(kwargs) == ("stderr",) + kwds = kwargs + argv = [sys.executable, SCRIPT, cmd, tool, "--"] + list(cliargs) + if not hidestdio: + argv.insert(4, "--no-hide-stdio") + kwds["stderr"] = subprocess.STDOUT + argv.append("--cache-clear") + print( + "running {!r}".format(" ".join(arg.rpartition(CWD + "/")[-1] for arg in argv)) + ) + output = subprocess.check_output(argv, universal_newlines=True, **kwds) + return output + + +def fix_test_order(tests): + if sys.version_info >= (3, 6): + return tests + fixed = [] + curfile = None + group = [] + for test in tests: + if (curfile or "???") not in test["id"]: + fixed.extend(sorted(group, key=lambda t: t["id"])) + group = [] + curfile = test["id"].partition(".py::")[0] + ".py" + group.append(test) + fixed.extend(sorted(group, key=lambda t: t["id"])) + return fixed + + +def fix_source(tests, testid, srcfile, lineno): + for test in tests: + if test["id"] == testid: + break + else: + raise KeyError("test {!r} not found".format(testid)) + if not srcfile: + srcfile = test["source"].rpartition(":")[0] + test["source"] = fix_path("{}:{}".format(srcfile, lineno)) + + +# Note that these tests are skipped if util.PATH_SEP is not os.path.sep. +# This is because the functional tests should reflect the actual +# operating environment. + + +class PytestTests(unittest.TestCase): + def setUp(self): + if PATH_SEP is not os.path.sep: + raise unittest.SkipTest("functional tests require unmodified env") + super(PytestTests, self).setUp() + + def complex(self, testroot): + results = COMPLEX.copy() + results["root"] = testroot + return [results] + + def test_discover_simple(self): + projroot, testroot = resolve_testroot("simple") + + out = run_adapter("discover", "pytest", "--rootdir", projroot, testroot) + result = json.loads(out) + + self.maxDiff = None + self.assertEqual( + result, + [ + { + "root": projroot, + "rootid": ".", + "parents": [ + { + "id": "./tests", + "kind": "folder", + "name": "tests", + "relpath": fix_path("./tests"), + "parentid": ".", + }, + { + "id": "./tests/test_spam.py", + "kind": "file", + "name": "test_spam.py", + "relpath": fix_path("./tests/test_spam.py"), + "parentid": "./tests", + }, + ], + "tests": [ + { + "id": "./tests/test_spam.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_spam.py:2"), + "markers": [], + "parentid": "./tests/test_spam.py", + }, + ], + } + ], + ) + + def test_discover_complex_default(self): + projroot, testroot = resolve_testroot("complex") + expected = self.complex(projroot) + expected[0]["tests"] = fix_test_order(expected[0]["tests"]) + if sys.version_info < (3,): + decorated = [ + "./tests/test_unittest.py::MyTests::test_skipped", + "./tests/test_unittest.py::MyTests::test_maybe_skipped", + "./tests/test_unittest.py::MyTests::test_maybe_not_skipped", + ] + for testid in decorated: + fix_source(expected[0]["tests"], testid, None, 0) + + out = run_adapter("discover", "pytest", "--rootdir", projroot, testroot) + result = json.loads(out) + result[0]["tests"] = fix_test_order(result[0]["tests"]) + + self.maxDiff = None + self.assertEqual(result, expected) + + def test_discover_complex_doctest(self): + projroot, _ = resolve_testroot("complex") + expected = self.complex(projroot) + # add in doctests from test suite + expected[0]["parents"].insert( + 3, + { + "id": "./tests/test_doctest.py", + "kind": "file", + "name": "test_doctest.py", + "relpath": fix_path("./tests/test_doctest.py"), + "parentid": "./tests", + }, + ) + expected[0]["tests"].insert( + 2, + { + "id": "./tests/test_doctest.py::tests.test_doctest", + "name": "tests.test_doctest", + "source": fix_path("./tests/test_doctest.py:1"), + "markers": [], + "parentid": "./tests/test_doctest.py", + }, + ) + # add in doctests from non-test module + expected[0]["parents"].insert( + 0, + { + "id": "./mod.py", + "kind": "file", + "name": "mod.py", + "relpath": fix_path("./mod.py"), + "parentid": ".", + }, + ) + expected[0]["tests"] = [ + { + "id": "./mod.py::mod", + "name": "mod", + "source": fix_path("./mod.py:1"), + "markers": [], + "parentid": "./mod.py", + }, + { + "id": "./mod.py::mod.Spam", + "name": "mod.Spam", + "source": fix_path("./mod.py:33"), + "markers": [], + "parentid": "./mod.py", + }, + { + "id": "./mod.py::mod.Spam.eggs", + "name": "mod.Spam.eggs", + "source": fix_path("./mod.py:43"), + "markers": [], + "parentid": "./mod.py", + }, + { + "id": "./mod.py::mod.square", + "name": "mod.square", + "source": fix_path("./mod.py:18"), + "markers": [], + "parentid": "./mod.py", + }, + ] + expected[0]["tests"] + expected[0]["tests"] = fix_test_order(expected[0]["tests"]) + if sys.version_info < (3,): + decorated = [ + "./tests/test_unittest.py::MyTests::test_skipped", + "./tests/test_unittest.py::MyTests::test_maybe_skipped", + "./tests/test_unittest.py::MyTests::test_maybe_not_skipped", + ] + for testid in decorated: + fix_source(expected[0]["tests"], testid, None, 0) + + out = run_adapter( + "discover", "pytest", "--rootdir", projroot, "--doctest-modules", projroot + ) + result = json.loads(out) + result[0]["tests"] = fix_test_order(result[0]["tests"]) + + self.maxDiff = None + self.assertEqual(result, expected) + + def test_discover_not_found(self): + projroot, testroot = resolve_testroot("notests") + + out = run_adapter("discover", "pytest", "--rootdir", projroot, testroot) + result = json.loads(out) + + self.maxDiff = None + self.assertEqual(result, []) + # TODO: Expect the following instead? + # self.assertEqual(result, [{ + # 'root': projroot, + # 'rootid': '.', + # 'parents': [], + # 'tests': [], + # }]) + + @unittest.skip("broken in CI") + def test_discover_bad_args(self): + projroot, testroot = resolve_testroot("simple") + + with self.assertRaises(subprocess.CalledProcessError) as cm: + _run_adapter( + "discover", + "pytest", + "--spam", + "--rootdir", + projroot, + testroot, + stderr=subprocess.STDOUT, + ) + self.assertIn("(exit code 4)", cm.exception.output) + + def test_discover_syntax_error(self): + projroot, testroot = resolve_testroot("syntax-error") + + with self.assertRaises(subprocess.CalledProcessError) as cm: + _run_adapter( + "discover", + "pytest", + "--rootdir", + projroot, + testroot, + stderr=subprocess.STDOUT, + ) + self.assertIn("(exit code 2)", cm.exception.output) + + def test_discover_normcase(self): + projroot, testroot = resolve_testroot("NormCase") + + out = run_adapter("discover", "pytest", "--rootdir", projroot, testroot) + result = json.loads(out) + + self.maxDiff = None + self.assertTrue(projroot.endswith("NormCase")) + self.assertEqual( + result, + [ + { + "root": projroot, + "rootid": ".", + "parents": [ + { + "id": "./tests", + "kind": "folder", + "name": "tests", + "relpath": fix_path("./tests"), + "parentid": ".", + }, + { + "id": "./tests/A", + "kind": "folder", + "name": "A", + "relpath": fix_path("./tests/A"), + "parentid": "./tests", + }, + { + "id": "./tests/A/b", + "kind": "folder", + "name": "b", + "relpath": fix_path("./tests/A/b"), + "parentid": "./tests/A", + }, + { + "id": "./tests/A/b/C", + "kind": "folder", + "name": "C", + "relpath": fix_path("./tests/A/b/C"), + "parentid": "./tests/A/b", + }, + { + "id": "./tests/A/b/C/test_Spam.py", + "kind": "file", + "name": "test_Spam.py", + "relpath": fix_path("./tests/A/b/C/test_Spam.py"), + "parentid": "./tests/A/b/C", + }, + ], + "tests": [ + { + "id": "./tests/A/b/C/test_Spam.py::test_okay", + "name": "test_okay", + "source": fix_path("./tests/A/b/C/test_Spam.py:2"), + "markers": [], + "parentid": "./tests/A/b/C/test_Spam.py", + }, + ], + } + ], + ) + + +COMPLEX = { + "root": None, + "rootid": ".", + "parents": [ + # + { + "id": "./tests", + "kind": "folder", + "name": "tests", + "relpath": fix_path("./tests"), + "parentid": ".", + }, + # +++ + { + "id": "./tests/test_42-43.py", + "kind": "file", + "name": "test_42-43.py", + "relpath": fix_path("./tests/test_42-43.py"), + "parentid": "./tests", + }, + # +++ + { + "id": "./tests/test_42.py", + "kind": "file", + "name": "test_42.py", + "relpath": fix_path("./tests/test_42.py"), + "parentid": "./tests", + }, + # +++ + { + "id": "./tests/test_doctest.txt", + "kind": "file", + "name": "test_doctest.txt", + "relpath": fix_path("./tests/test_doctest.txt"), + "parentid": "./tests", + }, + # +++ + { + "id": "./tests/test_foo.py", + "kind": "file", + "name": "test_foo.py", + "relpath": fix_path("./tests/test_foo.py"), + "parentid": "./tests", + }, + # +++ + { + "id": "./tests/test_mixed.py", + "kind": "file", + "name": "test_mixed.py", + "relpath": fix_path("./tests/test_mixed.py"), + "parentid": "./tests", + }, + { + "id": "./tests/test_mixed.py::MyTests", + "kind": "suite", + "name": "MyTests", + "parentid": "./tests/test_mixed.py", + }, + { + "id": "./tests/test_mixed.py::TestMySuite", + "kind": "suite", + "name": "TestMySuite", + "parentid": "./tests/test_mixed.py", + }, + # +++ + { + "id": "./tests/test_pytest.py", + "kind": "file", + "name": "test_pytest.py", + "relpath": fix_path("./tests/test_pytest.py"), + "parentid": "./tests", + }, + { + "id": "./tests/test_pytest.py::TestEggs", + "kind": "suite", + "name": "TestEggs", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::TestParam", + "kind": "suite", + "name": "TestParam", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::TestParam::test_param_13", + "kind": "function", + "name": "test_param_13", + "parentid": "./tests/test_pytest.py::TestParam", + }, + { + "id": "./tests/test_pytest.py::TestParamAll", + "kind": "suite", + "name": "TestParamAll", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::TestParamAll::test_param_13", + "kind": "function", + "name": "test_param_13", + "parentid": "./tests/test_pytest.py::TestParamAll", + }, + { + "id": "./tests/test_pytest.py::TestParamAll::test_spam_13", + "kind": "function", + "name": "test_spam_13", + "parentid": "./tests/test_pytest.py::TestParamAll", + }, + { + "id": "./tests/test_pytest.py::TestSpam", + "kind": "suite", + "name": "TestSpam", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::TestSpam::TestHam", + "kind": "suite", + "name": "TestHam", + "parentid": "./tests/test_pytest.py::TestSpam", + }, + { + "id": "./tests/test_pytest.py::TestSpam::TestHam::TestEggs", + "kind": "suite", + "name": "TestEggs", + "parentid": "./tests/test_pytest.py::TestSpam::TestHam", + }, + { + "id": "./tests/test_pytest.py::test_fixture_param", + "kind": "function", + "name": "test_fixture_param", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_01", + "kind": "function", + "name": "test_param_01", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_11", + "kind": "function", + "name": "test_param_11", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_13", + "kind": "function", + "name": "test_param_13", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_13_markers", + "kind": "function", + "name": "test_param_13_markers", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_13_repeat", + "kind": "function", + "name": "test_param_13_repeat", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_13_skipped", + "kind": "function", + "name": "test_param_13_skipped", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_23_13", + "kind": "function", + "name": "test_param_23_13", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_23_raises", + "kind": "function", + "name": "test_param_23_raises", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_33", + "kind": "function", + "name": "test_param_33", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_33_ids", + "kind": "function", + "name": "test_param_33_ids", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_fixture", + "kind": "function", + "name": "test_param_fixture", + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_mark_fixture", + "kind": "function", + "name": "test_param_mark_fixture", + "parentid": "./tests/test_pytest.py", + }, + # +++ + { + "id": "./tests/test_pytest_param.py", + "kind": "file", + "name": "test_pytest_param.py", + "relpath": fix_path("./tests/test_pytest_param.py"), + "parentid": "./tests", + }, + { + "id": "./tests/test_pytest_param.py::TestParamAll", + "kind": "suite", + "name": "TestParamAll", + "parentid": "./tests/test_pytest_param.py", + }, + { + "id": "./tests/test_pytest_param.py::TestParamAll::test_param_13", + "kind": "function", + "name": "test_param_13", + "parentid": "./tests/test_pytest_param.py::TestParamAll", + }, + { + "id": "./tests/test_pytest_param.py::TestParamAll::test_spam_13", + "kind": "function", + "name": "test_spam_13", + "parentid": "./tests/test_pytest_param.py::TestParamAll", + }, + { + "id": "./tests/test_pytest_param.py::test_param_13", + "kind": "function", + "name": "test_param_13", + "parentid": "./tests/test_pytest_param.py", + }, + # +++ + { + "id": "./tests/test_unittest.py", + "kind": "file", + "name": "test_unittest.py", + "relpath": fix_path("./tests/test_unittest.py"), + "parentid": "./tests", + }, + { + "id": "./tests/test_unittest.py::MyTests", + "kind": "suite", + "name": "MyTests", + "parentid": "./tests/test_unittest.py", + }, + { + "id": "./tests/test_unittest.py::OtherTests", + "kind": "suite", + "name": "OtherTests", + "parentid": "./tests/test_unittest.py", + }, + ## + { + "id": "./tests/v", + "kind": "folder", + "name": "v", + "relpath": fix_path("./tests/v"), + "parentid": "./tests", + }, + ## +++ + { + "id": "./tests/v/test_eggs.py", + "kind": "file", + "name": "test_eggs.py", + "relpath": fix_path("./tests/v/test_eggs.py"), + "parentid": "./tests/v", + }, + { + "id": "./tests/v/test_eggs.py::TestSimple", + "kind": "suite", + "name": "TestSimple", + "parentid": "./tests/v/test_eggs.py", + }, + ## +++ + { + "id": "./tests/v/test_ham.py", + "kind": "file", + "name": "test_ham.py", + "relpath": fix_path("./tests/v/test_ham.py"), + "parentid": "./tests/v", + }, + ## +++ + { + "id": "./tests/v/test_spam.py", + "kind": "file", + "name": "test_spam.py", + "relpath": fix_path("./tests/v/test_spam.py"), + "parentid": "./tests/v", + }, + ## + { + "id": "./tests/w", + "kind": "folder", + "name": "w", + "relpath": fix_path("./tests/w"), + "parentid": "./tests", + }, + ## +++ + { + "id": "./tests/w/test_spam.py", + "kind": "file", + "name": "test_spam.py", + "relpath": fix_path("./tests/w/test_spam.py"), + "parentid": "./tests/w", + }, + ## +++ + { + "id": "./tests/w/test_spam_ex.py", + "kind": "file", + "name": "test_spam_ex.py", + "relpath": fix_path("./tests/w/test_spam_ex.py"), + "parentid": "./tests/w", + }, + ## + { + "id": "./tests/x", + "kind": "folder", + "name": "x", + "relpath": fix_path("./tests/x"), + "parentid": "./tests", + }, + ### + { + "id": "./tests/x/y", + "kind": "folder", + "name": "y", + "relpath": fix_path("./tests/x/y"), + "parentid": "./tests/x", + }, + #### + { + "id": "./tests/x/y/z", + "kind": "folder", + "name": "z", + "relpath": fix_path("./tests/x/y/z"), + "parentid": "./tests/x/y", + }, + ##### + { + "id": "./tests/x/y/z/a", + "kind": "folder", + "name": "a", + "relpath": fix_path("./tests/x/y/z/a"), + "parentid": "./tests/x/y/z", + }, + ##### +++ + { + "id": "./tests/x/y/z/a/test_spam.py", + "kind": "file", + "name": "test_spam.py", + "relpath": fix_path("./tests/x/y/z/a/test_spam.py"), + "parentid": "./tests/x/y/z/a", + }, + ##### + { + "id": "./tests/x/y/z/b", + "kind": "folder", + "name": "b", + "relpath": fix_path("./tests/x/y/z/b"), + "parentid": "./tests/x/y/z", + }, + ##### +++ + { + "id": "./tests/x/y/z/b/test_spam.py", + "kind": "file", + "name": "test_spam.py", + "relpath": fix_path("./tests/x/y/z/b/test_spam.py"), + "parentid": "./tests/x/y/z/b", + }, + #### +++ + { + "id": "./tests/x/y/z/test_ham.py", + "kind": "file", + "name": "test_ham.py", + "relpath": fix_path("./tests/x/y/z/test_ham.py"), + "parentid": "./tests/x/y/z", + }, + ], + "tests": [ + ########## + { + "id": "./tests/test_42-43.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_42-43.py:2"), + "markers": [], + "parentid": "./tests/test_42-43.py", + }, + ##### + { + "id": "./tests/test_42.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_42.py:2"), + "markers": [], + "parentid": "./tests/test_42.py", + }, + ##### + { + "id": "./tests/test_doctest.txt::test_doctest.txt", + "name": "test_doctest.txt", + "source": fix_path("./tests/test_doctest.txt:1"), + "markers": [], + "parentid": "./tests/test_doctest.txt", + }, + ##### + { + "id": "./tests/test_foo.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_foo.py:3"), + "markers": [], + "parentid": "./tests/test_foo.py", + }, + ##### + { + "id": "./tests/test_mixed.py::test_top_level", + "name": "test_top_level", + "source": fix_path("./tests/test_mixed.py:5"), + "markers": [], + "parentid": "./tests/test_mixed.py", + }, + { + "id": "./tests/test_mixed.py::test_skipped", + "name": "test_skipped", + "source": fix_path("./tests/test_mixed.py:9"), + "markers": ["skip"], + "parentid": "./tests/test_mixed.py", + }, + { + "id": "./tests/test_mixed.py::TestMySuite::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_mixed.py:16"), + "markers": [], + "parentid": "./tests/test_mixed.py::TestMySuite", + }, + { + "id": "./tests/test_mixed.py::MyTests::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_mixed.py:22"), + "markers": [], + "parentid": "./tests/test_mixed.py::MyTests", + }, + { + "id": "./tests/test_mixed.py::MyTests::test_skipped", + "name": "test_skipped", + "source": fix_path("./tests/test_mixed.py:25"), + "markers": ["skip"], + "parentid": "./tests/test_mixed.py::MyTests", + }, + ##### + { + "id": "./tests/test_pytest.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_pytest.py:6"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_failure", + "name": "test_failure", + "source": fix_path("./tests/test_pytest.py:10"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_runtime_skipped", + "name": "test_runtime_skipped", + "source": fix_path("./tests/test_pytest.py:14"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_runtime_failed", + "name": "test_runtime_failed", + "source": fix_path("./tests/test_pytest.py:18"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_raises", + "name": "test_raises", + "source": fix_path("./tests/test_pytest.py:22"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_skipped", + "name": "test_skipped", + "source": fix_path("./tests/test_pytest.py:26"), + "markers": ["skip"], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_maybe_skipped", + "name": "test_maybe_skipped", + "source": fix_path("./tests/test_pytest.py:31"), + "markers": ["skip-if"], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_known_failure", + "name": "test_known_failure", + "source": fix_path("./tests/test_pytest.py:36"), + "markers": ["expected-failure"], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_warned", + "name": "test_warned", + "source": fix_path("./tests/test_pytest.py:41"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_custom_marker", + "name": "test_custom_marker", + "source": fix_path("./tests/test_pytest.py:46"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_multiple_markers", + "name": "test_multiple_markers", + "source": fix_path("./tests/test_pytest.py:51"), + "markers": ["expected-failure", "skip", "skip-if"], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_dynamic_1", + "name": "test_dynamic_1", + "source": fix_path("./tests/test_pytest.py:62"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_dynamic_2", + "name": "test_dynamic_2", + "source": fix_path("./tests/test_pytest.py:62"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_dynamic_3", + "name": "test_dynamic_3", + "source": fix_path("./tests/test_pytest.py:62"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::TestSpam::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_pytest.py:70"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestSpam", + }, + { + "id": "./tests/test_pytest.py::TestSpam::test_skipped", + "name": "test_skipped", + "source": fix_path("./tests/test_pytest.py:73"), + "markers": ["skip"], + "parentid": "./tests/test_pytest.py::TestSpam", + }, + { + "id": "./tests/test_pytest.py::TestSpam::TestHam::TestEggs::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_pytest.py:81"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestSpam::TestHam::TestEggs", + }, + { + "id": "./tests/test_pytest.py::TestEggs::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_pytest.py:93"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestEggs", + }, + { + "id": "./tests/test_pytest.py::test_param_01[]", + "name": "test_param_01[]", + "source": fix_path("./tests/test_pytest.py:103"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_01", + }, + { + "id": "./tests/test_pytest.py::test_param_11[x0]", + "name": "test_param_11[x0]", + "source": fix_path("./tests/test_pytest.py:108"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_11", + }, + { + "id": "./tests/test_pytest.py::test_param_13[x0]", + "name": "test_param_13[x0]", + "source": fix_path("./tests/test_pytest.py:113"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_13", + }, + { + "id": "./tests/test_pytest.py::test_param_13[x1]", + "name": "test_param_13[x1]", + "source": fix_path("./tests/test_pytest.py:113"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_13", + }, + { + "id": "./tests/test_pytest.py::test_param_13[x2]", + "name": "test_param_13[x2]", + "source": fix_path("./tests/test_pytest.py:113"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_13", + }, + { + "id": "./tests/test_pytest.py::test_param_13_repeat[x0]", + "name": "test_param_13_repeat[x0]", + "source": fix_path("./tests/test_pytest.py:118"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_13_repeat", + }, + { + "id": "./tests/test_pytest.py::test_param_13_repeat[x1]", + "name": "test_param_13_repeat[x1]", + "source": fix_path("./tests/test_pytest.py:118"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_13_repeat", + }, + { + "id": "./tests/test_pytest.py::test_param_13_repeat[x2]", + "name": "test_param_13_repeat[x2]", + "source": fix_path("./tests/test_pytest.py:118"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_13_repeat", + }, + { + "id": "./tests/test_pytest.py::test_param_33[1-1-1]", + "name": "test_param_33[1-1-1]", + "source": fix_path("./tests/test_pytest.py:123"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_33", + }, + { + "id": "./tests/test_pytest.py::test_param_33[3-4-5]", + "name": "test_param_33[3-4-5]", + "source": fix_path("./tests/test_pytest.py:123"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_33", + }, + { + "id": "./tests/test_pytest.py::test_param_33[0-0-0]", + "name": "test_param_33[0-0-0]", + "source": fix_path("./tests/test_pytest.py:123"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_33", + }, + { + "id": "./tests/test_pytest.py::test_param_33_ids[v1]", + "name": "test_param_33_ids[v1]", + "source": fix_path("./tests/test_pytest.py:128"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_33_ids", + }, + { + "id": "./tests/test_pytest.py::test_param_33_ids[v2]", + "name": "test_param_33_ids[v2]", + "source": fix_path("./tests/test_pytest.py:128"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_33_ids", + }, + { + "id": "./tests/test_pytest.py::test_param_33_ids[v3]", + "name": "test_param_33_ids[v3]", + "source": fix_path("./tests/test_pytest.py:128"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_33_ids", + }, + { + "id": "./tests/test_pytest.py::test_param_23_13[1-1-z0]", + "name": "test_param_23_13[1-1-z0]", + "source": fix_path("./tests/test_pytest.py:134"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_13", + }, + { + "id": "./tests/test_pytest.py::test_param_23_13[1-1-z1]", + "name": "test_param_23_13[1-1-z1]", + "source": fix_path("./tests/test_pytest.py:134"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_13", + }, + { + "id": "./tests/test_pytest.py::test_param_23_13[1-1-z2]", + "name": "test_param_23_13[1-1-z2]", + "source": fix_path("./tests/test_pytest.py:134"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_13", + }, + { + "id": "./tests/test_pytest.py::test_param_23_13[3-4-z0]", + "name": "test_param_23_13[3-4-z0]", + "source": fix_path("./tests/test_pytest.py:134"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_13", + }, + { + "id": "./tests/test_pytest.py::test_param_23_13[3-4-z1]", + "name": "test_param_23_13[3-4-z1]", + "source": fix_path("./tests/test_pytest.py:134"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_13", + }, + { + "id": "./tests/test_pytest.py::test_param_23_13[3-4-z2]", + "name": "test_param_23_13[3-4-z2]", + "source": fix_path("./tests/test_pytest.py:134"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_13", + }, + { + "id": "./tests/test_pytest.py::test_param_23_13[0-0-z0]", + "name": "test_param_23_13[0-0-z0]", + "source": fix_path("./tests/test_pytest.py:134"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_13", + }, + { + "id": "./tests/test_pytest.py::test_param_23_13[0-0-z1]", + "name": "test_param_23_13[0-0-z1]", + "source": fix_path("./tests/test_pytest.py:134"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_13", + }, + { + "id": "./tests/test_pytest.py::test_param_23_13[0-0-z2]", + "name": "test_param_23_13[0-0-z2]", + "source": fix_path("./tests/test_pytest.py:134"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_13", + }, + { + "id": "./tests/test_pytest.py::test_param_13_markers[x0]", + "name": "test_param_13_markers[x0]", + "source": fix_path("./tests/test_pytest.py:140"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_13_markers", + }, + { + "id": "./tests/test_pytest.py::test_param_13_markers[???]", + "name": "test_param_13_markers[???]", + "source": fix_path("./tests/test_pytest.py:140"), + "markers": ["skip"], + "parentid": "./tests/test_pytest.py::test_param_13_markers", + }, + { + "id": "./tests/test_pytest.py::test_param_13_markers[2]", + "name": "test_param_13_markers[2]", + "source": fix_path("./tests/test_pytest.py:140"), + "markers": ["expected-failure"], + "parentid": "./tests/test_pytest.py::test_param_13_markers", + }, + { + "id": "./tests/test_pytest.py::test_param_13_skipped[x0]", + "name": "test_param_13_skipped[x0]", + "source": fix_path("./tests/test_pytest.py:149"), + "markers": ["skip"], + "parentid": "./tests/test_pytest.py::test_param_13_skipped", + }, + { + "id": "./tests/test_pytest.py::test_param_13_skipped[x1]", + "name": "test_param_13_skipped[x1]", + "source": fix_path("./tests/test_pytest.py:149"), + "markers": ["skip"], + "parentid": "./tests/test_pytest.py::test_param_13_skipped", + }, + { + "id": "./tests/test_pytest.py::test_param_13_skipped[x2]", + "name": "test_param_13_skipped[x2]", + "source": fix_path("./tests/test_pytest.py:149"), + "markers": ["skip"], + "parentid": "./tests/test_pytest.py::test_param_13_skipped", + }, + { + "id": "./tests/test_pytest.py::test_param_23_raises[1-None]", + "name": "test_param_23_raises[1-None]", + "source": fix_path("./tests/test_pytest.py:155"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_raises", + }, + { + "id": "./tests/test_pytest.py::test_param_23_raises[1.0-None]", + "name": "test_param_23_raises[1.0-None]", + "source": fix_path("./tests/test_pytest.py:155"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_raises", + }, + { + "id": "./tests/test_pytest.py::test_param_23_raises[2-catch2]", + "name": "test_param_23_raises[2-catch2]", + "source": fix_path("./tests/test_pytest.py:155"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_23_raises", + }, + { + "id": "./tests/test_pytest.py::TestParam::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_pytest.py:164"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestParam", + }, + { + "id": "./tests/test_pytest.py::TestParam::test_param_13[x0]", + "name": "test_param_13[x0]", + "source": fix_path("./tests/test_pytest.py:167"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestParam::test_param_13", + }, + { + "id": "./tests/test_pytest.py::TestParam::test_param_13[x1]", + "name": "test_param_13[x1]", + "source": fix_path("./tests/test_pytest.py:167"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestParam::test_param_13", + }, + { + "id": "./tests/test_pytest.py::TestParam::test_param_13[x2]", + "name": "test_param_13[x2]", + "source": fix_path("./tests/test_pytest.py:167"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestParam::test_param_13", + }, + { + "id": "./tests/test_pytest.py::TestParamAll::test_param_13[x0]", + "name": "test_param_13[x0]", + "source": fix_path("./tests/test_pytest.py:175"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestParamAll::test_param_13", + }, + { + "id": "./tests/test_pytest.py::TestParamAll::test_param_13[x1]", + "name": "test_param_13[x1]", + "source": fix_path("./tests/test_pytest.py:175"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestParamAll::test_param_13", + }, + { + "id": "./tests/test_pytest.py::TestParamAll::test_param_13[x2]", + "name": "test_param_13[x2]", + "source": fix_path("./tests/test_pytest.py:175"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestParamAll::test_param_13", + }, + { + "id": "./tests/test_pytest.py::TestParamAll::test_spam_13[x0]", + "name": "test_spam_13[x0]", + "source": fix_path("./tests/test_pytest.py:178"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestParamAll::test_spam_13", + }, + { + "id": "./tests/test_pytest.py::TestParamAll::test_spam_13[x1]", + "name": "test_spam_13[x1]", + "source": fix_path("./tests/test_pytest.py:178"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestParamAll::test_spam_13", + }, + { + "id": "./tests/test_pytest.py::TestParamAll::test_spam_13[x2]", + "name": "test_spam_13[x2]", + "source": fix_path("./tests/test_pytest.py:178"), + "markers": [], + "parentid": "./tests/test_pytest.py::TestParamAll::test_spam_13", + }, + { + "id": "./tests/test_pytest.py::test_fixture", + "name": "test_fixture", + "source": fix_path("./tests/test_pytest.py:192"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_mark_fixture", + "name": "test_mark_fixture", + "source": fix_path("./tests/test_pytest.py:196"), + "markers": [], + "parentid": "./tests/test_pytest.py", + }, + { + "id": "./tests/test_pytest.py::test_param_fixture[x0]", + "name": "test_param_fixture[x0]", + "source": fix_path("./tests/test_pytest.py:201"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_fixture", + }, + { + "id": "./tests/test_pytest.py::test_param_fixture[x1]", + "name": "test_param_fixture[x1]", + "source": fix_path("./tests/test_pytest.py:201"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_fixture", + }, + { + "id": "./tests/test_pytest.py::test_param_fixture[x2]", + "name": "test_param_fixture[x2]", + "source": fix_path("./tests/test_pytest.py:201"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_fixture", + }, + { + "id": "./tests/test_pytest.py::test_param_mark_fixture[x0]", + "name": "test_param_mark_fixture[x0]", + "source": fix_path("./tests/test_pytest.py:207"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_mark_fixture", + }, + { + "id": "./tests/test_pytest.py::test_param_mark_fixture[x1]", + "name": "test_param_mark_fixture[x1]", + "source": fix_path("./tests/test_pytest.py:207"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_mark_fixture", + }, + { + "id": "./tests/test_pytest.py::test_param_mark_fixture[x2]", + "name": "test_param_mark_fixture[x2]", + "source": fix_path("./tests/test_pytest.py:207"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_param_mark_fixture", + }, + { + "id": "./tests/test_pytest.py::test_fixture_param[spam]", + "name": "test_fixture_param[spam]", + "source": fix_path("./tests/test_pytest.py:216"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_fixture_param", + }, + { + "id": "./tests/test_pytest.py::test_fixture_param[eggs]", + "name": "test_fixture_param[eggs]", + "source": fix_path("./tests/test_pytest.py:216"), + "markers": [], + "parentid": "./tests/test_pytest.py::test_fixture_param", + }, + ###### + { + "id": "./tests/test_pytest_param.py::test_param_13[x0]", + "name": "test_param_13[x0]", + "source": fix_path("./tests/test_pytest_param.py:8"), + "markers": [], + "parentid": "./tests/test_pytest_param.py::test_param_13", + }, + { + "id": "./tests/test_pytest_param.py::test_param_13[x1]", + "name": "test_param_13[x1]", + "source": fix_path("./tests/test_pytest_param.py:8"), + "markers": [], + "parentid": "./tests/test_pytest_param.py::test_param_13", + }, + { + "id": "./tests/test_pytest_param.py::test_param_13[x2]", + "name": "test_param_13[x2]", + "source": fix_path("./tests/test_pytest_param.py:8"), + "markers": [], + "parentid": "./tests/test_pytest_param.py::test_param_13", + }, + { + "id": "./tests/test_pytest_param.py::TestParamAll::test_param_13[x0]", + "name": "test_param_13[x0]", + "source": fix_path("./tests/test_pytest_param.py:14"), + "markers": [], + "parentid": "./tests/test_pytest_param.py::TestParamAll::test_param_13", + }, + { + "id": "./tests/test_pytest_param.py::TestParamAll::test_param_13[x1]", + "name": "test_param_13[x1]", + "source": fix_path("./tests/test_pytest_param.py:14"), + "markers": [], + "parentid": "./tests/test_pytest_param.py::TestParamAll::test_param_13", + }, + { + "id": "./tests/test_pytest_param.py::TestParamAll::test_param_13[x2]", + "name": "test_param_13[x2]", + "source": fix_path("./tests/test_pytest_param.py:14"), + "markers": [], + "parentid": "./tests/test_pytest_param.py::TestParamAll::test_param_13", + }, + { + "id": "./tests/test_pytest_param.py::TestParamAll::test_spam_13[x0]", + "name": "test_spam_13[x0]", + "source": fix_path("./tests/test_pytest_param.py:17"), + "markers": [], + "parentid": "./tests/test_pytest_param.py::TestParamAll::test_spam_13", + }, + { + "id": "./tests/test_pytest_param.py::TestParamAll::test_spam_13[x1]", + "name": "test_spam_13[x1]", + "source": fix_path("./tests/test_pytest_param.py:17"), + "markers": [], + "parentid": "./tests/test_pytest_param.py::TestParamAll::test_spam_13", + }, + { + "id": "./tests/test_pytest_param.py::TestParamAll::test_spam_13[x2]", + "name": "test_spam_13[x2]", + "source": fix_path("./tests/test_pytest_param.py:17"), + "markers": [], + "parentid": "./tests/test_pytest_param.py::TestParamAll::test_spam_13", + }, + ###### + { + "id": "./tests/test_unittest.py::MyTests::test_dynamic_", + "name": "test_dynamic_", + "source": fix_path("./tests/test_unittest.py:54"), + "markers": [], + "parentid": "./tests/test_unittest.py::MyTests", + }, + { + "id": "./tests/test_unittest.py::MyTests::test_failure", + "name": "test_failure", + "source": fix_path("./tests/test_unittest.py:34"), + "markers": [], + "parentid": "./tests/test_unittest.py::MyTests", + }, + { + "id": "./tests/test_unittest.py::MyTests::test_known_failure", + "name": "test_known_failure", + "source": fix_path("./tests/test_unittest.py:37"), + "markers": [], + "parentid": "./tests/test_unittest.py::MyTests", + }, + { + "id": "./tests/test_unittest.py::MyTests::test_maybe_not_skipped", + "name": "test_maybe_not_skipped", + "source": fix_path("./tests/test_unittest.py:17"), + "markers": [], + "parentid": "./tests/test_unittest.py::MyTests", + }, + { + "id": "./tests/test_unittest.py::MyTests::test_maybe_skipped", + "name": "test_maybe_skipped", + "source": fix_path("./tests/test_unittest.py:13"), + "markers": [], + "parentid": "./tests/test_unittest.py::MyTests", + }, + { + "id": "./tests/test_unittest.py::MyTests::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_unittest.py:6"), + "markers": [], + "parentid": "./tests/test_unittest.py::MyTests", + }, + { + "id": "./tests/test_unittest.py::MyTests::test_skipped", + "name": "test_skipped", + "source": fix_path("./tests/test_unittest.py:9"), + "markers": [], + "parentid": "./tests/test_unittest.py::MyTests", + }, + { + "id": "./tests/test_unittest.py::MyTests::test_skipped_inside", + "name": "test_skipped_inside", + "source": fix_path("./tests/test_unittest.py:21"), + "markers": [], + "parentid": "./tests/test_unittest.py::MyTests", + }, + { + "id": "./tests/test_unittest.py::MyTests::test_with_nested_subtests", + "name": "test_with_nested_subtests", + "source": fix_path("./tests/test_unittest.py:46"), + "markers": [], + "parentid": "./tests/test_unittest.py::MyTests", + }, + { + "id": "./tests/test_unittest.py::MyTests::test_with_subtests", + "name": "test_with_subtests", + "source": fix_path("./tests/test_unittest.py:41"), + "markers": [], + "parentid": "./tests/test_unittest.py::MyTests", + }, + { + "id": "./tests/test_unittest.py::OtherTests::test_simple", + "name": "test_simple", + "source": fix_path("./tests/test_unittest.py:61"), + "markers": [], + "parentid": "./tests/test_unittest.py::OtherTests", + }, + ########### + { + "id": "./tests/v/test_eggs.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/v/spam.py:2"), + "markers": [], + "parentid": "./tests/v/test_eggs.py", + }, + { + "id": "./tests/v/test_eggs.py::TestSimple::test_simple", + "name": "test_simple", + "source": fix_path("./tests/v/spam.py:8"), + "markers": [], + "parentid": "./tests/v/test_eggs.py::TestSimple", + }, + ###### + { + "id": "./tests/v/test_ham.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/v/spam.py:2"), + "markers": [], + "parentid": "./tests/v/test_ham.py", + }, + { + "id": "./tests/v/test_ham.py::test_not_hard", + "name": "test_not_hard", + "source": fix_path("./tests/v/spam.py:2"), + "markers": [], + "parentid": "./tests/v/test_ham.py", + }, + ###### + { + "id": "./tests/v/test_spam.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/v/spam.py:2"), + "markers": [], + "parentid": "./tests/v/test_spam.py", + }, + { + "id": "./tests/v/test_spam.py::test_simpler", + "name": "test_simpler", + "source": fix_path("./tests/v/test_spam.py:4"), + "markers": [], + "parentid": "./tests/v/test_spam.py", + }, + ########### + { + "id": "./tests/w/test_spam.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/w/test_spam.py:4"), + "markers": [], + "parentid": "./tests/w/test_spam.py", + }, + { + "id": "./tests/w/test_spam_ex.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/w/test_spam_ex.py:4"), + "markers": [], + "parentid": "./tests/w/test_spam_ex.py", + }, + ########### + { + "id": "./tests/x/y/z/test_ham.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/x/y/z/test_ham.py:2"), + "markers": [], + "parentid": "./tests/x/y/z/test_ham.py", + }, + ###### + { + "id": "./tests/x/y/z/a/test_spam.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/x/y/z/a/test_spam.py:11"), + "markers": [], + "parentid": "./tests/x/y/z/a/test_spam.py", + }, + { + "id": "./tests/x/y/z/b/test_spam.py::test_simple", + "name": "test_simple", + "source": fix_path("./tests/x/y/z/b/test_spam.py:7"), + "markers": [], + "parentid": "./tests/x/y/z/b/test_spam.py", + }, + ], +} diff --git a/pythonFiles/tests/testing_tools/adapter/test_report.py b/pythonFiles/tests/testing_tools/adapter/test_report.py new file mode 100644 index 000000000000..bb68c8a65e79 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/test_report.py @@ -0,0 +1,1179 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import unittest + +from ...util import StubProxy +from testing_tools.adapter.util import fix_path, fix_relpath +from testing_tools.adapter.info import SingleTestInfo, SingleTestPath, ParentInfo +from testing_tools.adapter.report import report_discovered + + +class StubSender(StubProxy): + def send(self, outstr): + self.add_call("send", (json.loads(outstr),), None) + + +################################## +# tests + + +class ReportDiscoveredTests(unittest.TestCase): + def test_basic(self): + stub = StubSender() + testroot = fix_path("/a/b/c") + relfile = "test_spam.py" + relpath = fix_relpath(relfile) + tests = [ + SingleTestInfo( + id="test#1", + name="test_spam", + path=SingleTestPath( + root=testroot, + relfile=relfile, + func="test_spam", + ), + source="{}:{}".format(relfile, 10), + markers=[], + parentid="file#1", + ), + ] + parents = [ + ParentInfo( + id="", + kind="folder", + name=testroot, + ), + ParentInfo( + id="file#1", + kind="file", + name=relfile, + root=testroot, + relpath=relpath, + parentid="", + ), + ] + expected = [ + { + "rootid": "", + "root": testroot, + "parents": [ + { + "id": "file#1", + "kind": "file", + "name": relfile, + "relpath": relpath, + "parentid": "", + }, + ], + "tests": [ + { + "id": "test#1", + "name": "test_spam", + "source": "{}:{}".format(relfile, 10), + "markers": [], + "parentid": "file#1", + } + ], + } + ] + + report_discovered(tests, parents, _send=stub.send) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("send", (expected,), None), + ], + ) + + def test_multiroot(self): + stub = StubSender() + # the first root + testroot1 = fix_path("/a/b/c") + relfileid1 = "./test_spam.py" + relpath1 = fix_path(relfileid1) + relfile1 = relpath1[2:] + tests = [ + SingleTestInfo( + id=relfileid1 + "::test_spam", + name="test_spam", + path=SingleTestPath( + root=testroot1, + relfile=relfile1, + func="test_spam", + ), + source="{}:{}".format(relfile1, 10), + markers=[], + parentid=relfileid1, + ), + ] + parents = [ + ParentInfo( + id=".", + kind="folder", + name=testroot1, + ), + ParentInfo( + id=relfileid1, + kind="file", + name="test_spam.py", + root=testroot1, + relpath=relpath1, + parentid=".", + ), + ] + expected = [ + { + "rootid": ".", + "root": testroot1, + "parents": [ + { + "id": relfileid1, + "kind": "file", + "name": "test_spam.py", + "relpath": relpath1, + "parentid": ".", + }, + ], + "tests": [ + { + "id": relfileid1 + "::test_spam", + "name": "test_spam", + "source": "{}:{}".format(relfile1, 10), + "markers": [], + "parentid": relfileid1, + } + ], + }, + ] + # the second root + testroot2 = fix_path("/x/y/z") + relfileid2 = "./w/test_eggs.py" + relpath2 = fix_path(relfileid2) + relfile2 = relpath2[2:] + tests.extend( + [ + SingleTestInfo( + id=relfileid2 + "::BasicTests::test_first", + name="test_first", + path=SingleTestPath( + root=testroot2, + relfile=relfile2, + func="BasicTests.test_first", + ), + source="{}:{}".format(relfile2, 61), + markers=[], + parentid=relfileid2 + "::BasicTests", + ), + ] + ) + parents.extend( + [ + ParentInfo( + id=".", + kind="folder", + name=testroot2, + ), + ParentInfo( + id="./w", + kind="folder", + name="w", + root=testroot2, + relpath=fix_path("./w"), + parentid=".", + ), + ParentInfo( + id=relfileid2, + kind="file", + name="test_eggs.py", + root=testroot2, + relpath=relpath2, + parentid="./w", + ), + ParentInfo( + id=relfileid2 + "::BasicTests", + kind="suite", + name="BasicTests", + root=testroot2, + parentid=relfileid2, + ), + ] + ) + expected.extend( + [ + { + "rootid": ".", + "root": testroot2, + "parents": [ + { + "id": "./w", + "kind": "folder", + "name": "w", + "relpath": fix_path("./w"), + "parentid": ".", + }, + { + "id": relfileid2, + "kind": "file", + "name": "test_eggs.py", + "relpath": relpath2, + "parentid": "./w", + }, + { + "id": relfileid2 + "::BasicTests", + "kind": "suite", + "name": "BasicTests", + "parentid": relfileid2, + }, + ], + "tests": [ + { + "id": relfileid2 + "::BasicTests::test_first", + "name": "test_first", + "source": "{}:{}".format(relfile2, 61), + "markers": [], + "parentid": relfileid2 + "::BasicTests", + } + ], + }, + ] + ) + + report_discovered(tests, parents, _send=stub.send) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("send", (expected,), None), + ], + ) + + def test_complex(self): + """ + /a/b/c/ + test_ham.py + MySuite + test_x1 + test_x2 + /a/b/e/f/g/ + w/ + test_ham.py + test_ham1 + HamTests + test_uh_oh + test_whoa + MoreHam + test_yay + sub1 + sub2 + sub3 + test_eggs.py + SpamTests + test_okay + x/ + y/ + a/ + test_spam.py + SpamTests + test_okay + b/ + test_spam.py + SpamTests + test_okay + test_spam.py + SpamTests + test_okay + """ + stub = StubSender() + testroot = fix_path("/a/b/c") + relfileid1 = "./test_ham.py" + relfileid2 = "./test_spam.py" + relfileid3 = "./w/test_ham.py" + relfileid4 = "./w/test_eggs.py" + relfileid5 = "./x/y/a/test_spam.py" + relfileid6 = "./x/y/b/test_spam.py" + tests = [ + SingleTestInfo( + id=relfileid1 + "::MySuite::test_x1", + name="test_x1", + path=SingleTestPath( + root=testroot, + relfile=fix_path(relfileid1), + func="MySuite.test_x1", + ), + source="{}:{}".format(fix_path(relfileid1), 10), + markers=None, + parentid=relfileid1 + "::MySuite", + ), + SingleTestInfo( + id=relfileid1 + "::MySuite::test_x2", + name="test_x2", + path=SingleTestPath( + root=testroot, + relfile=fix_path(relfileid1), + func="MySuite.test_x2", + ), + source="{}:{}".format(fix_path(relfileid1), 21), + markers=None, + parentid=relfileid1 + "::MySuite", + ), + SingleTestInfo( + id=relfileid2 + "::SpamTests::test_okay", + name="test_okay", + path=SingleTestPath( + root=testroot, + relfile=fix_path(relfileid2), + func="SpamTests.test_okay", + ), + source="{}:{}".format(fix_path(relfileid2), 17), + markers=None, + parentid=relfileid2 + "::SpamTests", + ), + SingleTestInfo( + id=relfileid3 + "::test_ham1", + name="test_ham1", + path=SingleTestPath( + root=testroot, + relfile=fix_path(relfileid3), + func="test_ham1", + ), + source="{}:{}".format(fix_path(relfileid3), 8), + markers=None, + parentid=relfileid3, + ), + SingleTestInfo( + id=relfileid3 + "::HamTests::test_uh_oh", + name="test_uh_oh", + path=SingleTestPath( + root=testroot, + relfile=fix_path(relfileid3), + func="HamTests.test_uh_oh", + ), + source="{}:{}".format(fix_path(relfileid3), 19), + markers=["expected-failure"], + parentid=relfileid3 + "::HamTests", + ), + SingleTestInfo( + id=relfileid3 + "::HamTests::test_whoa", + name="test_whoa", + path=SingleTestPath( + root=testroot, + relfile=fix_path(relfileid3), + func="HamTests.test_whoa", + ), + source="{}:{}".format(fix_path(relfileid3), 35), + markers=None, + parentid=relfileid3 + "::HamTests", + ), + SingleTestInfo( + id=relfileid3 + "::MoreHam::test_yay[1-2]", + name="test_yay[1-2]", + path=SingleTestPath( + root=testroot, + relfile=fix_path(relfileid3), + func="MoreHam.test_yay", + sub=["[1-2]"], + ), + source="{}:{}".format(fix_path(relfileid3), 57), + markers=None, + parentid=relfileid3 + "::MoreHam::test_yay", + ), + SingleTestInfo( + id=relfileid3 + "::MoreHam::test_yay[1-2][3-4]", + name="test_yay[1-2][3-4]", + path=SingleTestPath( + root=testroot, + relfile=fix_path(relfileid3), + func="MoreHam.test_yay", + sub=["[1-2]", "[3=4]"], + ), + source="{}:{}".format(fix_path(relfileid3), 72), + markers=None, + parentid=relfileid3 + "::MoreHam::test_yay[1-2]", + ), + SingleTestInfo( + id=relfileid4 + "::SpamTests::test_okay", + name="test_okay", + path=SingleTestPath( + root=testroot, + relfile=fix_path(relfileid4), + func="SpamTests.test_okay", + ), + source="{}:{}".format(fix_path(relfileid4), 15), + markers=None, + parentid=relfileid4 + "::SpamTests", + ), + SingleTestInfo( + id=relfileid5 + "::SpamTests::test_okay", + name="test_okay", + path=SingleTestPath( + root=testroot, + relfile=fix_path(relfileid5), + func="SpamTests.test_okay", + ), + source="{}:{}".format(fix_path(relfileid5), 12), + markers=None, + parentid=relfileid5 + "::SpamTests", + ), + SingleTestInfo( + id=relfileid6 + "::SpamTests::test_okay", + name="test_okay", + path=SingleTestPath( + root=testroot, + relfile=fix_path(relfileid6), + func="SpamTests.test_okay", + ), + source="{}:{}".format(fix_path(relfileid6), 27), + markers=None, + parentid=relfileid6 + "::SpamTests", + ), + ] + parents = [ + ParentInfo( + id=".", + kind="folder", + name=testroot, + ), + ParentInfo( + id=relfileid1, + kind="file", + name="test_ham.py", + root=testroot, + relpath=fix_path(relfileid1), + parentid=".", + ), + ParentInfo( + id=relfileid1 + "::MySuite", + kind="suite", + name="MySuite", + root=testroot, + parentid=relfileid1, + ), + ParentInfo( + id=relfileid2, + kind="file", + name="test_spam.py", + root=testroot, + relpath=fix_path(relfileid2), + parentid=".", + ), + ParentInfo( + id=relfileid2 + "::SpamTests", + kind="suite", + name="SpamTests", + root=testroot, + parentid=relfileid2, + ), + ParentInfo( + id="./w", + kind="folder", + name="w", + root=testroot, + relpath=fix_path("./w"), + parentid=".", + ), + ParentInfo( + id=relfileid3, + kind="file", + name="test_ham.py", + root=testroot, + relpath=fix_path(relfileid3), + parentid="./w", + ), + ParentInfo( + id=relfileid3 + "::HamTests", + kind="suite", + name="HamTests", + root=testroot, + parentid=relfileid3, + ), + ParentInfo( + id=relfileid3 + "::MoreHam", + kind="suite", + name="MoreHam", + root=testroot, + parentid=relfileid3, + ), + ParentInfo( + id=relfileid3 + "::MoreHam::test_yay", + kind="function", + name="test_yay", + root=testroot, + parentid=relfileid3 + "::MoreHam", + ), + ParentInfo( + id=relfileid3 + "::MoreHam::test_yay[1-2]", + kind="subtest", + name="test_yay[1-2]", + root=testroot, + parentid=relfileid3 + "::MoreHam::test_yay", + ), + ParentInfo( + id=relfileid4, + kind="file", + name="test_eggs.py", + root=testroot, + relpath=fix_path(relfileid4), + parentid="./w", + ), + ParentInfo( + id=relfileid4 + "::SpamTests", + kind="suite", + name="SpamTests", + root=testroot, + parentid=relfileid4, + ), + ParentInfo( + id="./x", + kind="folder", + name="x", + root=testroot, + relpath=fix_path("./x"), + parentid=".", + ), + ParentInfo( + id="./x/y", + kind="folder", + name="y", + root=testroot, + relpath=fix_path("./x/y"), + parentid="./x", + ), + ParentInfo( + id="./x/y/a", + kind="folder", + name="a", + root=testroot, + relpath=fix_path("./x/y/a"), + parentid="./x/y", + ), + ParentInfo( + id=relfileid5, + kind="file", + name="test_spam.py", + root=testroot, + relpath=fix_path(relfileid5), + parentid="./x/y/a", + ), + ParentInfo( + id=relfileid5 + "::SpamTests", + kind="suite", + name="SpamTests", + root=testroot, + parentid=relfileid5, + ), + ParentInfo( + id="./x/y/b", + kind="folder", + name="b", + root=testroot, + relpath=fix_path("./x/y/b"), + parentid="./x/y", + ), + ParentInfo( + id=relfileid6, + kind="file", + name="test_spam.py", + root=testroot, + relpath=fix_path(relfileid6), + parentid="./x/y/b", + ), + ParentInfo( + id=relfileid6 + "::SpamTests", + kind="suite", + name="SpamTests", + root=testroot, + parentid=relfileid6, + ), + ] + expected = [ + { + "rootid": ".", + "root": testroot, + "parents": [ + { + "id": relfileid1, + "kind": "file", + "name": "test_ham.py", + "relpath": fix_path(relfileid1), + "parentid": ".", + }, + { + "id": relfileid1 + "::MySuite", + "kind": "suite", + "name": "MySuite", + "parentid": relfileid1, + }, + { + "id": relfileid2, + "kind": "file", + "name": "test_spam.py", + "relpath": fix_path(relfileid2), + "parentid": ".", + }, + { + "id": relfileid2 + "::SpamTests", + "kind": "suite", + "name": "SpamTests", + "parentid": relfileid2, + }, + { + "id": "./w", + "kind": "folder", + "name": "w", + "relpath": fix_path("./w"), + "parentid": ".", + }, + { + "id": relfileid3, + "kind": "file", + "name": "test_ham.py", + "relpath": fix_path(relfileid3), + "parentid": "./w", + }, + { + "id": relfileid3 + "::HamTests", + "kind": "suite", + "name": "HamTests", + "parentid": relfileid3, + }, + { + "id": relfileid3 + "::MoreHam", + "kind": "suite", + "name": "MoreHam", + "parentid": relfileid3, + }, + { + "id": relfileid3 + "::MoreHam::test_yay", + "kind": "function", + "name": "test_yay", + "parentid": relfileid3 + "::MoreHam", + }, + { + "id": relfileid3 + "::MoreHam::test_yay[1-2]", + "kind": "subtest", + "name": "test_yay[1-2]", + "parentid": relfileid3 + "::MoreHam::test_yay", + }, + { + "id": relfileid4, + "kind": "file", + "name": "test_eggs.py", + "relpath": fix_path(relfileid4), + "parentid": "./w", + }, + { + "id": relfileid4 + "::SpamTests", + "kind": "suite", + "name": "SpamTests", + "parentid": relfileid4, + }, + { + "id": "./x", + "kind": "folder", + "name": "x", + "relpath": fix_path("./x"), + "parentid": ".", + }, + { + "id": "./x/y", + "kind": "folder", + "name": "y", + "relpath": fix_path("./x/y"), + "parentid": "./x", + }, + { + "id": "./x/y/a", + "kind": "folder", + "name": "a", + "relpath": fix_path("./x/y/a"), + "parentid": "./x/y", + }, + { + "id": relfileid5, + "kind": "file", + "name": "test_spam.py", + "relpath": fix_path(relfileid5), + "parentid": "./x/y/a", + }, + { + "id": relfileid5 + "::SpamTests", + "kind": "suite", + "name": "SpamTests", + "parentid": relfileid5, + }, + { + "id": "./x/y/b", + "kind": "folder", + "name": "b", + "relpath": fix_path("./x/y/b"), + "parentid": "./x/y", + }, + { + "id": relfileid6, + "kind": "file", + "name": "test_spam.py", + "relpath": fix_path(relfileid6), + "parentid": "./x/y/b", + }, + { + "id": relfileid6 + "::SpamTests", + "kind": "suite", + "name": "SpamTests", + "parentid": relfileid6, + }, + ], + "tests": [ + { + "id": relfileid1 + "::MySuite::test_x1", + "name": "test_x1", + "source": "{}:{}".format(fix_path(relfileid1), 10), + "markers": [], + "parentid": relfileid1 + "::MySuite", + }, + { + "id": relfileid1 + "::MySuite::test_x2", + "name": "test_x2", + "source": "{}:{}".format(fix_path(relfileid1), 21), + "markers": [], + "parentid": relfileid1 + "::MySuite", + }, + { + "id": relfileid2 + "::SpamTests::test_okay", + "name": "test_okay", + "source": "{}:{}".format(fix_path(relfileid2), 17), + "markers": [], + "parentid": relfileid2 + "::SpamTests", + }, + { + "id": relfileid3 + "::test_ham1", + "name": "test_ham1", + "source": "{}:{}".format(fix_path(relfileid3), 8), + "markers": [], + "parentid": relfileid3, + }, + { + "id": relfileid3 + "::HamTests::test_uh_oh", + "name": "test_uh_oh", + "source": "{}:{}".format(fix_path(relfileid3), 19), + "markers": ["expected-failure"], + "parentid": relfileid3 + "::HamTests", + }, + { + "id": relfileid3 + "::HamTests::test_whoa", + "name": "test_whoa", + "source": "{}:{}".format(fix_path(relfileid3), 35), + "markers": [], + "parentid": relfileid3 + "::HamTests", + }, + { + "id": relfileid3 + "::MoreHam::test_yay[1-2]", + "name": "test_yay[1-2]", + "source": "{}:{}".format(fix_path(relfileid3), 57), + "markers": [], + "parentid": relfileid3 + "::MoreHam::test_yay", + }, + { + "id": relfileid3 + "::MoreHam::test_yay[1-2][3-4]", + "name": "test_yay[1-2][3-4]", + "source": "{}:{}".format(fix_path(relfileid3), 72), + "markers": [], + "parentid": relfileid3 + "::MoreHam::test_yay[1-2]", + }, + { + "id": relfileid4 + "::SpamTests::test_okay", + "name": "test_okay", + "source": "{}:{}".format(fix_path(relfileid4), 15), + "markers": [], + "parentid": relfileid4 + "::SpamTests", + }, + { + "id": relfileid5 + "::SpamTests::test_okay", + "name": "test_okay", + "source": "{}:{}".format(fix_path(relfileid5), 12), + "markers": [], + "parentid": relfileid5 + "::SpamTests", + }, + { + "id": relfileid6 + "::SpamTests::test_okay", + "name": "test_okay", + "source": "{}:{}".format(fix_path(relfileid6), 27), + "markers": [], + "parentid": relfileid6 + "::SpamTests", + }, + ], + } + ] + + report_discovered(tests, parents, _send=stub.send) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("send", (expected,), None), + ], + ) + + def test_simple_basic(self): + stub = StubSender() + testroot = fix_path("/a/b/c") + relfile = fix_path("x/y/z/test_spam.py") + tests = [ + SingleTestInfo( + id="test#1", + name="test_spam_1", + path=SingleTestPath( + root=testroot, + relfile=relfile, + func="MySuite.test_spam_1", + sub=None, + ), + source="{}:{}".format(relfile, 10), + markers=None, + parentid="suite#1", + ), + ] + parents = None + expected = [ + { + "id": "test#1", + "name": "test_spam_1", + "testroot": testroot, + "relfile": relfile, + "lineno": 10, + "testfunc": "MySuite.test_spam_1", + "subtest": None, + "markers": [], + } + ] + + report_discovered(tests, parents, simple=True, _send=stub.send) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("send", (expected,), None), + ], + ) + + def test_simple_complex(self): + """ + /a/b/c/ + test_ham.py + MySuite + test_x1 + test_x2 + /a/b/e/f/g/ + w/ + test_ham.py + test_ham1 + HamTests + test_uh_oh + test_whoa + MoreHam + test_yay + sub1 + sub2 + sub3 + test_eggs.py + SpamTests + test_okay + x/ + y/ + a/ + test_spam.py + SpamTests + test_okay + b/ + test_spam.py + SpamTests + test_okay + test_spam.py + SpamTests + test_okay + """ + stub = StubSender() + testroot1 = fix_path("/a/b/c") + relfile1 = fix_path("./test_ham.py") + testroot2 = fix_path("/a/b/e/f/g") + relfile2 = fix_path("./test_spam.py") + relfile3 = fix_path("w/test_ham.py") + relfile4 = fix_path("w/test_eggs.py") + relfile5 = fix_path("x/y/a/test_spam.py") + relfile6 = fix_path("x/y/b/test_spam.py") + tests = [ + # under first root folder + SingleTestInfo( + id="test#1", + name="test_x1", + path=SingleTestPath( + root=testroot1, + relfile=relfile1, + func="MySuite.test_x1", + sub=None, + ), + source="{}:{}".format(relfile1, 10), + markers=None, + parentid="suite#1", + ), + SingleTestInfo( + id="test#2", + name="test_x2", + path=SingleTestPath( + root=testroot1, + relfile=relfile1, + func="MySuite.test_x2", + sub=None, + ), + source="{}:{}".format(relfile1, 21), + markers=None, + parentid="suite#1", + ), + # under second root folder + SingleTestInfo( + id="test#3", + name="test_okay", + path=SingleTestPath( + root=testroot2, + relfile=relfile2, + func="SpamTests.test_okay", + sub=None, + ), + source="{}:{}".format(relfile2, 17), + markers=None, + parentid="suite#2", + ), + SingleTestInfo( + id="test#4", + name="test_ham1", + path=SingleTestPath( + root=testroot2, + relfile=relfile3, + func="test_ham1", + sub=None, + ), + source="{}:{}".format(relfile3, 8), + markers=None, + parentid="file#3", + ), + SingleTestInfo( + id="test#5", + name="test_uh_oh", + path=SingleTestPath( + root=testroot2, + relfile=relfile3, + func="HamTests.test_uh_oh", + sub=None, + ), + source="{}:{}".format(relfile3, 19), + markers=["expected-failure"], + parentid="suite#3", + ), + SingleTestInfo( + id="test#6", + name="test_whoa", + path=SingleTestPath( + root=testroot2, + relfile=relfile3, + func="HamTests.test_whoa", + sub=None, + ), + source="{}:{}".format(relfile3, 35), + markers=None, + parentid="suite#3", + ), + SingleTestInfo( + id="test#7", + name="test_yay (sub1)", + path=SingleTestPath( + root=testroot2, + relfile=relfile3, + func="MoreHam.test_yay", + sub=["sub1"], + ), + source="{}:{}".format(relfile3, 57), + markers=None, + parentid="suite#4", + ), + SingleTestInfo( + id="test#8", + name="test_yay (sub2) (sub3)", + path=SingleTestPath( + root=testroot2, + relfile=relfile3, + func="MoreHam.test_yay", + sub=["sub2", "sub3"], + ), + source="{}:{}".format(relfile3, 72), + markers=None, + parentid="suite#3", + ), + SingleTestInfo( + id="test#9", + name="test_okay", + path=SingleTestPath( + root=testroot2, + relfile=relfile4, + func="SpamTests.test_okay", + sub=None, + ), + source="{}:{}".format(relfile4, 15), + markers=None, + parentid="suite#5", + ), + SingleTestInfo( + id="test#10", + name="test_okay", + path=SingleTestPath( + root=testroot2, + relfile=relfile5, + func="SpamTests.test_okay", + sub=None, + ), + source="{}:{}".format(relfile5, 12), + markers=None, + parentid="suite#6", + ), + SingleTestInfo( + id="test#11", + name="test_okay", + path=SingleTestPath( + root=testroot2, + relfile=relfile6, + func="SpamTests.test_okay", + sub=None, + ), + source="{}:{}".format(relfile6, 27), + markers=None, + parentid="suite#7", + ), + ] + expected = [ + { + "id": "test#1", + "name": "test_x1", + "testroot": testroot1, + "relfile": relfile1, + "lineno": 10, + "testfunc": "MySuite.test_x1", + "subtest": None, + "markers": [], + }, + { + "id": "test#2", + "name": "test_x2", + "testroot": testroot1, + "relfile": relfile1, + "lineno": 21, + "testfunc": "MySuite.test_x2", + "subtest": None, + "markers": [], + }, + { + "id": "test#3", + "name": "test_okay", + "testroot": testroot2, + "relfile": relfile2, + "lineno": 17, + "testfunc": "SpamTests.test_okay", + "subtest": None, + "markers": [], + }, + { + "id": "test#4", + "name": "test_ham1", + "testroot": testroot2, + "relfile": relfile3, + "lineno": 8, + "testfunc": "test_ham1", + "subtest": None, + "markers": [], + }, + { + "id": "test#5", + "name": "test_uh_oh", + "testroot": testroot2, + "relfile": relfile3, + "lineno": 19, + "testfunc": "HamTests.test_uh_oh", + "subtest": None, + "markers": ["expected-failure"], + }, + { + "id": "test#6", + "name": "test_whoa", + "testroot": testroot2, + "relfile": relfile3, + "lineno": 35, + "testfunc": "HamTests.test_whoa", + "subtest": None, + "markers": [], + }, + { + "id": "test#7", + "name": "test_yay (sub1)", + "testroot": testroot2, + "relfile": relfile3, + "lineno": 57, + "testfunc": "MoreHam.test_yay", + "subtest": ["sub1"], + "markers": [], + }, + { + "id": "test#8", + "name": "test_yay (sub2) (sub3)", + "testroot": testroot2, + "relfile": relfile3, + "lineno": 72, + "testfunc": "MoreHam.test_yay", + "subtest": ["sub2", "sub3"], + "markers": [], + }, + { + "id": "test#9", + "name": "test_okay", + "testroot": testroot2, + "relfile": relfile4, + "lineno": 15, + "testfunc": "SpamTests.test_okay", + "subtest": None, + "markers": [], + }, + { + "id": "test#10", + "name": "test_okay", + "testroot": testroot2, + "relfile": relfile5, + "lineno": 12, + "testfunc": "SpamTests.test_okay", + "subtest": None, + "markers": [], + }, + { + "id": "test#11", + "name": "test_okay", + "testroot": testroot2, + "relfile": relfile6, + "lineno": 27, + "testfunc": "SpamTests.test_okay", + "subtest": None, + "markers": [], + }, + ] + parents = None + + report_discovered(tests, parents, simple=True, _send=stub.send) + + self.maxDiff = None + self.assertEqual( + stub.calls, + [ + ("send", (expected,), None), + ], + ) diff --git a/pythonFiles/tests/testing_tools/adapter/test_util.py b/pythonFiles/tests/testing_tools/adapter/test_util.py new file mode 100644 index 000000000000..822ba2ed1b22 --- /dev/null +++ b/pythonFiles/tests/testing_tools/adapter/test_util.py @@ -0,0 +1,330 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import absolute_import, print_function + +import ntpath +import os +import os.path +import posixpath +import shlex +import sys +import unittest + +import pytest + +# Pytest 3.7 and later uses pathlib/pathlib2 for path resolution. +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path # type: ignore (for Pylance) + +from testing_tools.adapter.util import ( + fix_path, + fix_relpath, + fix_fileid, + shlex_unsplit, +) + + +@unittest.skipIf(sys.version_info < (3,), "Python 2 does not have subTest") +class FilePathTests(unittest.TestCase): + def test_isolated_imports(self): + import testing_tools.adapter + from testing_tools.adapter import util + from . import test_functional + + ignored = { + str(Path(os.path.abspath(__file__)).resolve()), + str(Path(os.path.abspath(util.__file__)).resolve()), + str(Path(os.path.abspath(test_functional.__file__)).resolve()), + } + adapter = os.path.abspath(os.path.dirname(testing_tools.adapter.__file__)) + tests = os.path.join( + os.path.abspath(os.path.dirname(os.path.dirname(testing_tools.__file__))), + "tests", + "testing_tools", + "adapter", + ) + found = [] + for root in [adapter, tests]: + for dirname, _, files in os.walk(root): + if ".data" in dirname: + continue + for basename in files: + if not basename.endswith(".py"): + continue + filename = os.path.join(dirname, basename) + if filename in ignored: + continue + with open(filename) as srcfile: + for line in srcfile: + if line.strip() == "import os.path": + found.append(filename) + break + + if found: + self.fail( + os.linesep.join( + [ + "", + "Please only use path-related API from testing_tools.adapter.util.", + 'Found use of "os.path" in the following files:', + ] + + [" " + file for file in found] + ) + ) + + def test_fix_path(self): + tests = [ + ("./spam.py", r".\spam.py"), + ("./some-dir", r".\some-dir"), + ("./some-dir/", ".\\some-dir\\"), + ("./some-dir/eggs", r".\some-dir\eggs"), + ("./some-dir/eggs/spam.py", r".\some-dir\eggs\spam.py"), + ("X/y/Z/a.B.c.PY", r"X\y\Z\a.B.c.PY"), + ("/", "\\"), + ("/spam", r"\spam"), + ("C:/spam", r"C:\spam"), + ] + for path, expected in tests: + pathsep = ntpath.sep + with self.subTest(r"fixed for \: {!r}".format(path)): + fixed = fix_path(path, _pathsep=pathsep) + self.assertEqual(fixed, expected) + + pathsep = posixpath.sep + with self.subTest("unchanged for /: {!r}".format(path)): + unchanged = fix_path(path, _pathsep=pathsep) + self.assertEqual(unchanged, path) + + # no path -> "." + for path in ["", None]: + for pathsep in [ntpath.sep, posixpath.sep]: + with self.subTest(r"fixed for {}: {!r}".format(pathsep, path)): + fixed = fix_path(path, _pathsep=pathsep) + self.assertEqual(fixed, ".") + + # no-op paths + paths = [path for _, path in tests] + paths.extend( + [ + ".", + "..", + "some-dir", + "spam.py", + ] + ) + for path in paths: + for pathsep in [ntpath.sep, posixpath.sep]: + with self.subTest(r"unchanged for {}: {!r}".format(pathsep, path)): + unchanged = fix_path(path, _pathsep=pathsep) + self.assertEqual(unchanged, path) + + def test_fix_relpath(self): + tests = [ + ("spam.py", posixpath, "./spam.py"), + ("eggs/spam.py", posixpath, "./eggs/spam.py"), + ("eggs/spam/", posixpath, "./eggs/spam/"), + (r"\spam.py", posixpath, r"./\spam.py"), + ("spam.py", ntpath, r".\spam.py"), + (r"eggs\spam.py", ntpath, r".\eggs\spam.py"), + ("eggs\\spam\\", ntpath, ".\\eggs\\spam\\"), + ("/spam.py", ntpath, r"\spam.py"), # Note the fixed "/". + # absolute + ("/", posixpath, "/"), + ("/spam.py", posixpath, "/spam.py"), + ("\\", ntpath, "\\"), + (r"\spam.py", ntpath, r"\spam.py"), + (r"C:\spam.py", ntpath, r"C:\spam.py"), + # no-op + ("./spam.py", posixpath, "./spam.py"), + (r".\spam.py", ntpath, r".\spam.py"), + ] + # no-op + for path in [".", ".."]: + tests.extend( + [ + (path, posixpath, path), + (path, ntpath, path), + ] + ) + for path, _os_path, expected in tests: + with self.subTest((path, _os_path.sep)): + fixed = fix_relpath( + path, + _fix_path=(lambda p: fix_path(p, _pathsep=_os_path.sep)), + _path_isabs=_os_path.isabs, + _pathsep=_os_path.sep, + ) + self.assertEqual(fixed, expected) + + def test_fix_fileid(self): + common = [ + ("spam.py", "./spam.py"), + ("eggs/spam.py", "./eggs/spam.py"), + ("eggs/spam/", "./eggs/spam/"), + # absolute (no-op) + ("/", "/"), + ("//", "//"), + ("/spam.py", "/spam.py"), + # no-op + (None, None), + ("", ""), + (".", "."), + ("./spam.py", "./spam.py"), + ] + tests = [(p, posixpath, e) for p, e in common] + tests.extend( + (p, posixpath, e) + for p, e in [ + (r"\spam.py", r"./\spam.py"), + ] + ) + tests.extend((p, ntpath, e) for p, e in common) + tests.extend( + (p, ntpath, e) + for p, e in [ + (r"eggs\spam.py", "./eggs/spam.py"), + ("eggs\\spam\\", "./eggs/spam/"), + (r".\spam.py", r"./spam.py"), + # absolute + (r"\spam.py", "/spam.py"), + (r"C:\spam.py", "C:/spam.py"), + ("\\", "/"), + ("\\\\", "//"), + ("C:\\\\", "C://"), + ("C:/", "C:/"), + ("C://", "C://"), + ("C:/spam.py", "C:/spam.py"), + ] + ) + for fileid, _os_path, expected in tests: + pathsep = _os_path.sep + with self.subTest(r"for {}: {!r}".format(pathsep, fileid)): + fixed = fix_fileid( + fileid, + _path_isabs=_os_path.isabs, + _normcase=_os_path.normcase, + _pathsep=pathsep, + ) + self.assertEqual(fixed, expected) + + # with rootdir + common = [ + ("spam.py", "/eggs", "./spam.py"), + ("spam.py", r"\eggs", "./spam.py"), + # absolute + ("/spam.py", "/", "./spam.py"), + ("/eggs/spam.py", "/eggs", "./spam.py"), + ("/eggs/spam.py", "/eggs/", "./spam.py"), + # no-op + ("/spam.py", "/eggs", "/spam.py"), + ("/spam.py", "/eggs/", "/spam.py"), + # root-only (no-op) + ("/", "/", "/"), + ("/", "/spam", "/"), + ("//", "/", "//"), + ("//", "//", "//"), + ("//", "//spam", "//"), + ] + tests = [(p, r, posixpath, e) for p, r, e in common] + tests = [(p, r, ntpath, e) for p, r, e in common] + tests.extend( + (p, r, ntpath, e) + for p, r, e in [ + ("spam.py", r"\eggs", "./spam.py"), + # absolute + (r"\spam.py", "\\", r"./spam.py"), + (r"C:\spam.py", "C:\\", r"./spam.py"), + (r"\eggs\spam.py", r"\eggs", r"./spam.py"), + (r"\eggs\spam.py", "\\eggs\\", r"./spam.py"), + # normcase + (r"C:\spam.py", "c:\\", r"./spam.py"), + (r"\Eggs\Spam.py", "\\eggs", r"./Spam.py"), + (r"\eggs\spam.py", "\\Eggs", r"./spam.py"), + (r"\eggs\Spam.py", "\\Eggs", r"./Spam.py"), + # no-op + (r"\spam.py", r"\eggs", r"/spam.py"), + (r"C:\spam.py", r"C:\eggs", r"C:/spam.py"), + # TODO: Should these be supported. + (r"C:\spam.py", "\\", r"C:/spam.py"), + (r"\spam.py", "C:\\", r"/spam.py"), + # root-only + ("\\", "\\", "/"), + ("\\\\", "\\", "//"), + ("C:\\", "C:\\eggs", "C:/"), + ("C:\\", "C:\\", "C:/"), + (r"C:\spam.py", "D:\\", r"C:/spam.py"), + ] + ) + for fileid, rootdir, _os_path, expected in tests: + pathsep = _os_path.sep + with self.subTest( + r"for {} (with rootdir {!r}): {!r}".format(pathsep, rootdir, fileid) + ): + fixed = fix_fileid( + fileid, + rootdir, + _path_isabs=_os_path.isabs, + _normcase=_os_path.normcase, + _pathsep=pathsep, + ) + self.assertEqual(fixed, expected) + + +class ShlexUnsplitTests(unittest.TestCase): + def test_no_args(self): + argv = [] + joined = shlex_unsplit(argv) + + self.assertEqual(joined, "") + self.assertEqual(shlex.split(joined), argv) + + def test_one_arg(self): + argv = ["spam"] + joined = shlex_unsplit(argv) + + self.assertEqual(joined, "spam") + self.assertEqual(shlex.split(joined), argv) + + def test_multiple_args(self): + argv = [ + "-x", + "X", + "-xyz", + "spam", + "eggs", + ] + joined = shlex_unsplit(argv) + + self.assertEqual(joined, "-x X -xyz spam eggs") + self.assertEqual(shlex.split(joined), argv) + + def test_whitespace(self): + argv = [ + "-x", + "X Y Z", + "spam spam\tspam", + "eggs", + ] + joined = shlex_unsplit(argv) + + self.assertEqual(joined, "-x 'X Y Z' 'spam spam\tspam' eggs") + self.assertEqual(shlex.split(joined), argv) + + def test_quotation_marks(self): + argv = [ + "-x", + "''", + 'spam"spam"spam', + "ham'ham'ham", + "eggs", + ] + joined = shlex_unsplit(argv) + + self.assertEqual( + joined, + "-x ''\"'\"''\"'\"'' 'spam\"spam\"spam' 'ham'\"'\"'ham'\"'\"'ham' eggs", + ) + self.assertEqual(shlex.split(joined), argv) diff --git a/pythonFiles/tests/util.py b/pythonFiles/tests/util.py new file mode 100644 index 000000000000..45c3536145cf --- /dev/null +++ b/pythonFiles/tests/util.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class Stub(object): + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + + +class StubProxy(object): + def __init__(self, stub=None, name=None): + self.name = name + self.stub = stub if stub is not None else Stub() + + @property + def calls(self): + return self.stub.calls + + def add_call(self, funcname, *args, **kwargs): + callname = funcname + if self.name: + callname = "{}.{}".format(self.name, funcname) + return self.stub.add_call(callname, *args, **kwargs) diff --git a/pythonFiles/visualstudio_py_testlauncher.py b/pythonFiles/visualstudio_py_testlauncher.py new file mode 100644 index 000000000000..7731b63b7e65 --- /dev/null +++ b/pythonFiles/visualstudio_py_testlauncher.py @@ -0,0 +1,393 @@ +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. + +__author__ = "Microsoft Corporation " +__version__ = "3.0.0.0" + +import os +import sys +import json +import unittest +import socket +import traceback +from types import CodeType, FunctionType +import signal + +try: + import thread +except: + import _thread as thread + + +class _TestOutput(object): + """file like object which redirects output to the repl window.""" + + errors = "strict" + + def __init__(self, old_out, is_stdout): + self.is_stdout = is_stdout + self.old_out = old_out + if sys.version >= "3." and hasattr(old_out, "buffer"): + self.buffer = _TestOutputBuffer(old_out.buffer, is_stdout) + + def flush(self): + if self.old_out: + self.old_out.flush() + + def writelines(self, lines): + for line in lines: + self.write(line) + + @property + def encoding(self): + return "utf8" + + def write(self, value): + _channel.send_event("stdout" if self.is_stdout else "stderr", content=value) + if self.old_out: + self.old_out.write(value) + # flush immediately, else things go wonky and out of order + self.flush() + + def isatty(self): + return True + + def next(self): + pass + + @property + def name(self): + if self.is_stdout: + return "" + else: + return "" + + def __getattr__(self, name): + return getattr(self.old_out, name) + + +class _TestOutputBuffer(object): + def __init__(self, old_buffer, is_stdout): + self.buffer = old_buffer + self.is_stdout = is_stdout + + def write(self, data): + _channel.send_event("stdout" if self.is_stdout else "stderr", content=data) + self.buffer.write(data) + + def flush(self): + self.buffer.flush() + + def truncate(self, pos=None): + return self.buffer.truncate(pos) + + def tell(self): + return self.buffer.tell() + + def seek(self, pos, whence=0): + return self.buffer.seek(pos, whence) + + +class _IpcChannel(object): + def __init__(self, socket, callback): + self.socket = socket + self.seq = 0 + self.callback = callback + self.lock = thread.allocate_lock() + self._closed = False + # start the testing reader thread loop + self.test_thread_id = thread.start_new_thread(self.readSocket, ()) + + def close(self): + self._closed = True + + def readSocket(self): + try: + data = self.socket.recv(1024) + self.callback() + except OSError: + if not self._closed: + raise + + def receive(self): + pass + + def send_event(self, name, **args): + with self.lock: + body = {"type": "event", "seq": self.seq, "event": name, "body": args} + self.seq += 1 + content = json.dumps(body).encode("utf8") + headers = ("Content-Length: %d\n\n" % (len(content),)).encode("utf8") + self.socket.send(headers) + self.socket.send(content) + + +_channel = None + + +class VsTestResult(unittest.TextTestResult): + def startTest(self, test): + super(VsTestResult, self).startTest(test) + if _channel is not None: + _channel.send_event(name="start", test=test.id()) + + def addError(self, test, err): + super(VsTestResult, self).addError(test, err) + self.sendResult(test, "error", err) + + def addFailure(self, test, err): + super(VsTestResult, self).addFailure(test, err) + self.sendResult(test, "failed", err) + + def addSuccess(self, test): + super(VsTestResult, self).addSuccess(test) + self.sendResult(test, "passed") + + def addSkip(self, test, reason): + super(VsTestResult, self).addSkip(test, reason) + self.sendResult(test, "skipped") + + def addExpectedFailure(self, test, err): + super(VsTestResult, self).addExpectedFailure(test, err) + self.sendResult(test, "failed", err) + + def addUnexpectedSuccess(self, test): + super(VsTestResult, self).addUnexpectedSuccess(test) + self.sendResult(test, "passed") + + def sendResult(self, test, outcome, trace=None): + if _channel is not None: + tb = None + message = None + if trace is not None: + traceback.print_exc() + formatted = traceback.format_exception(*trace) + # Remove the 'Traceback (most recent call last)' + formatted = formatted[1:] + tb = "".join(formatted) + message = str(trace[1]) + _channel.send_event( + name="result", + outcome=outcome, + traceback=tb, + message=message, + test=test.id(), + ) + + +def stopTests(): + try: + os.kill(os.getpid(), signal.SIGUSR1) + except: + try: + os.kill(os.getpid(), signal.SIGTERM) + except: + pass + + +class ExitCommand(Exception): + pass + + +def signal_handler(signal, frame): + raise ExitCommand() + + +def main(): + import os + import sys + import unittest + from optparse import OptionParser + + global _channel + + parser = OptionParser( + prog="visualstudio_py_testlauncher", + usage="Usage: %prog [

+ + ${uris.map((uri) => ``).join('\n')} + + `; + } +} diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts new file mode 100644 index 000000000000..d004ff378364 --- /dev/null +++ b/src/client/common/application/workspace.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import * as path from 'path'; +import { + CancellationToken, + ConfigurationChangeEvent, + Event, + FileSystemWatcher, + GlobPattern, + Uri, + workspace, + WorkspaceConfiguration, + WorkspaceFolder, + WorkspaceFoldersChangeEvent +} from 'vscode'; +import { Resource } from '../types'; +import { getOSType, OSType } from '../utils/platform'; +import { IWorkspaceService } from './types'; + +@injectable() +export class WorkspaceService implements IWorkspaceService { + public get onDidChangeConfiguration(): Event { + return workspace.onDidChangeConfiguration; + } + public get rootPath(): string | undefined { + return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0 + ? workspace.workspaceFolders[0].uri.fsPath + : undefined; + } + public get workspaceFolders(): readonly WorkspaceFolder[] | undefined { + return workspace.workspaceFolders; + } + public get onDidChangeWorkspaceFolders(): Event { + return workspace.onDidChangeWorkspaceFolders; + } + public get hasWorkspaceFolders() { + return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0; + } + public get workspaceFile() { + return workspace.workspaceFile; + } + public getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration { + return workspace.getConfiguration(section, resource || null); + } + public getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined { + return uri ? workspace.getWorkspaceFolder(uri) : undefined; + } + public asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string { + return workspace.asRelativePath(pathOrUri, includeWorkspaceFolder); + } + public createFileSystemWatcher( + globPattern: GlobPattern, + _ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, + ignoreDeleteEvents?: boolean + ): FileSystemWatcher { + return workspace.createFileSystemWatcher( + globPattern, + ignoreChangeEvents, + ignoreChangeEvents, + ignoreDeleteEvents + ); + } + public findFiles( + include: GlobPattern, + exclude?: GlobPattern, + maxResults?: number, + token?: CancellationToken + ): Thenable { + return workspace.findFiles(include, exclude, maxResults, token); + } + public getWorkspaceFolderIdentifier(resource: Resource, defaultValue: string = ''): string { + const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; + return workspaceFolder + ? path.normalize( + getOSType() === OSType.Windows ? workspaceFolder.uri.fsPath.toUpperCase() : workspaceFolder.uri.fsPath + ) + : defaultValue; + } +} diff --git a/src/client/common/asyncDisposableRegistry.ts b/src/client/common/asyncDisposableRegistry.ts new file mode 100644 index 000000000000..10bd9492b31e --- /dev/null +++ b/src/client/common/asyncDisposableRegistry.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { injectable } from 'inversify'; +import { IAsyncDisposable, IAsyncDisposableRegistry, IDisposable } from './types'; + +// List of disposables that need to run a promise. +@injectable() +export class AsyncDisposableRegistry implements IAsyncDisposableRegistry { + private _list: (IDisposable | IAsyncDisposable)[] = []; + + public async dispose(): Promise { + const promises = this._list.map((l) => l.dispose()); + await Promise.all(promises); + this._list = []; + } + + public push(disposable?: IDisposable | IAsyncDisposable) { + if (disposable) { + this._list.push(disposable); + } + } + + public get list(): (IDisposable | IAsyncDisposable)[] { + return this._list; + } +} diff --git a/src/client/common/cancellation.ts b/src/client/common/cancellation.ts new file mode 100644 index 000000000000..601ac933979d --- /dev/null +++ b/src/client/common/cancellation.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { CancellationToken, CancellationTokenSource } from 'vscode'; +import { createDeferred } from './utils/async'; +import * as localize from './utils/localize'; + +/** + * Error type thrown when canceling. + */ +export class CancellationError extends Error { + constructor() { + super(localize.Common.canceled()); + } +} +/** + * Create a promise that will either resolve with a default value or reject when the token is cancelled. + * + * @export + * @template T + * @param {({ defaultValue: T; token: CancellationToken; cancelAction: 'reject' | 'resolve' })} options + * @returns {Promise} + */ +export function createPromiseFromCancellation(options: { + defaultValue: T; + token?: CancellationToken; + cancelAction: 'reject' | 'resolve'; +}): Promise { + return new Promise((resolve, reject) => { + // Never resolve. + if (!options.token) { + return; + } + const complete = () => { + if (options.token!.isCancellationRequested) { + if (options.cancelAction === 'resolve') { + return resolve(options.defaultValue); + } + if (options.cancelAction === 'reject') { + return reject(new CancellationError()); + } + } + }; + + options.token.onCancellationRequested(complete); + }); +} + +/** + * Create a single unified cancellation token that wraps multiple cancellation tokens. + * + * @export + * @param {(...(CancellationToken | undefined)[])} tokens + * @returns {CancellationToken} + */ +export function wrapCancellationTokens(...tokens: (CancellationToken | undefined)[]): CancellationToken { + const wrappedCancellantionToken = new CancellationTokenSource(); + for (const token of tokens) { + if (!token) { + continue; + } + if (token.isCancellationRequested) { + return token; + } + token.onCancellationRequested(() => wrappedCancellantionToken.cancel()); + } + + return wrappedCancellantionToken.token; +} + +export namespace Cancellation { + /** + * Races a promise and cancellation. Promise can take a cancellation token too in order to listen to cancellation. + * @param work function returning a promise to race + * @param token token used for cancellation + */ + export function race(work: (token?: CancellationToken) => Promise, token?: CancellationToken): Promise { + if (token) { + // Use a deferred promise. Resolves when the work finishes + const deferred = createDeferred(); + + // Cancel the deferred promise when the cancellation happens + token.onCancellationRequested(() => { + if (!deferred.completed) { + deferred.reject(new CancellationError()); + } + }); + + // Might already be canceled + if (token.isCancellationRequested) { + // Just start out as rejected + deferred.reject(new CancellationError()); + } else { + // Not canceled yet. When the work finishes + // either resolve our promise or cancel. + work(token) + .then((v) => { + if (!deferred.completed) { + deferred.resolve(v); + } + }) + .catch((e) => { + if (!deferred.completed) { + deferred.reject(e); + } + }); + } + + return deferred.promise; + } else { + // No actual token, just do the original work. + return work(); + } + } + + /** + * isCanceled returns a boolean indicating if the cancel token has been canceled. + * @param cancelToken + */ + export function isCanceled(cancelToken?: CancellationToken): boolean { + return cancelToken ? cancelToken.isCancellationRequested : false; + } + + /** + * throws a CancellationError if the token is canceled. + * @param cancelToken + */ + export function throwIfCanceled(cancelToken?: CancellationToken): void { + if (isCanceled(cancelToken)) { + throw new CancellationError(); + } + } +} diff --git a/src/client/common/childProc.ts b/src/client/common/childProc.ts deleted file mode 100644 index f1a9a1d984c1..000000000000 --- a/src/client/common/childProc.ts +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -import * as path from 'path'; -import * as fs from 'fs'; -import * as child_process from 'child_process'; - -export function sendCommand(commandLine: string, cwd: string, includeErrorAsResponse:boolean = false): Promise { - return new Promise((resolve, reject) => { - - child_process.exec(commandLine, { cwd: cwd }, (error, stdout, stderr) => { - if (includeErrorAsResponse){ - return resolve(stdout + '\n' + stderr); - } - - var hasErrors = (error && error.message.length > 0) || (stderr && stderr.length > 0); - if (hasErrors && (typeof stdout !== "string" || stdout.length === 0)) { - var errorMsg = (error && error.message) ? error.message : (stderr && stderr.length > 0 ? stderr + '' : ""); - return reject(errorMsg); - } - - resolve(stdout + ''); - }); - }); -} diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 94e4ec41a133..7473bcfcd952 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -1,109 +1,744 @@ 'use strict'; -import * as vscode from 'vscode'; +import * as child_process from 'child_process'; import * as path from 'path'; -import * as fs from 'fs'; - -export interface IPythonSettings { - pythonPath: string; - devOptions: any[]; - linting: ILintingSettings; - formatting: IFormattingSettings; - unitTest: IUnitTestSettings; -} -export interface IUnitTestSettings { - nosetestsEnabled: boolean; - nosetestPath: string; - unittestEnabled: boolean; - outputWindow: string; -} -export interface IPylintCategorySeverity { - convention: vscode.DiagnosticSeverity; - refactor: vscode.DiagnosticSeverity; - warning: vscode.DiagnosticSeverity; - error: vscode.DiagnosticSeverity; - fatal: vscode.DiagnosticSeverity; -} -export interface ILintingSettings { - enabled: boolean; - prospectorEnabled: boolean, - pylintEnabled: boolean; - pep8Enabled: boolean; - flake8Enabled: boolean; - pydocstyleEnabled: boolean; - lintOnTextChange: boolean; - lintOnSave: boolean; - maxNumberOfProblems: number; - pylintCategorySeverity: IPylintCategorySeverity; - prospectorPath: string; - prospectorSourcePath: string; - prospectorExtraCommands: string; - pylintPath: string; - pep8Path: string; - flake8Path: string; - pydocStylePath: string; - outputWindow: string; -} -export interface IFormattingSettings { - provider: string; - autopep8Path: string; - yapfPath: string; - formatOnSave: boolean; - outputWindow: string; -} -export interface IAutoCompeteSettings { - extraPaths: string[]; -} +import { + ConfigurationChangeEvent, + ConfigurationTarget, + DiagnosticSeverity, + Disposable, + Event, + EventEmitter, + Uri, + WorkspaceConfiguration +} from 'vscode'; +import { LanguageServerType } from '../activation/types'; +import '../common/extensions'; +import { IInterpreterAutoSeletionProxyService, IInterpreterSecurityService } from '../interpreter/autoSelection/types'; +import { LogLevel } from '../logging/levels'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { sendSettingTelemetry } from '../telemetry/envFileTelemetry'; +import { IWorkspaceService } from './application/types'; +import { WorkspaceService } from './application/workspace'; +import { DEFAULT_INTERPRETER_SETTING, isTestExecution } from './constants'; +import { DeprecatePythonPath } from './experiments/groups'; +import { ExtensionChannels } from './insidersBuild/types'; +import { IS_WINDOWS } from './platform/constants'; +import * as internalPython from './process/internal/python'; +import { + IAnalysisSettings, + IAutoCompleteSettings, + IDataScienceSettings, + IExperiments, + IExperimentsManager, + IFormattingSettings, + IInterpreterPathService, + ILintingSettings, + ILoggingSettings, + IPythonSettings, + ISortImportSettings, + ITerminalSettings, + ITestingSettings, + IWorkspaceSymbolSettings, + LoggingLevelSettingType, + Resource +} from './types'; +import { debounceSync } from './utils/decorators'; +import { SystemVariables } from './variables/systemVariables'; + +// tslint:disable:no-require-imports no-var-requires +const untildify = require('untildify'); + +// tslint:disable-next-line:completed-docs export class PythonSettings implements IPythonSettings { - constructor() { - vscode.workspace.onDidChangeConfiguration(() => { - this.initializeSettings(); - }); + public get onDidChange(): Event { + return this.changed.event; + } + + public get pythonPath(): string { + return this._pythonPath; + } + public set pythonPath(value: string) { + if (this._pythonPath === value) { + return; + } + // Add support for specifying just the directory where the python executable will be located. + // E.g. virtual directory name. + try { + this._pythonPath = this.getPythonExecutable(value); + } catch (ex) { + this._pythonPath = value; + } + } - this.initializeSettings(); + public get defaultInterpreterPath(): string { + return this._defaultInterpreterPath; } - private initializeSettings() { - var pythonSettings = vscode.workspace.getConfiguration("python"); - this.pythonPath = pythonSettings.get("pythonPath"); - this.devOptions = pythonSettings.get("devOptions"); + public set defaultInterpreterPath(value: string) { + if (this._defaultInterpreterPath === value) { + return; + } + // Add support for specifying just the directory where the python executable will be located. + // E.g. virtual directory name. + try { + this._defaultInterpreterPath = this.getPythonExecutable(value); + } catch (ex) { + this._defaultInterpreterPath = value; + } + } + private static pythonSettings: Map = new Map(); + public showStartPage = true; + public downloadLanguageServer = true; + public jediPath = ''; + public jediMemoryLimit = 1024; + public envFile = ''; + public venvPath = ''; + public venvFolders: string[] = []; + public condaPath = ''; + public pipenvPath = ''; + public poetryPath = ''; + public devOptions: string[] = []; + public linting!: ILintingSettings; + public formatting!: IFormattingSettings; + public autoComplete!: IAutoCompleteSettings; + public testing!: ITestingSettings; + public terminal!: ITerminalSettings; + public sortImports!: ISortImportSettings; + public workspaceSymbols!: IWorkspaceSymbolSettings; + public disableInstallationChecks = false; + public globalModuleInstallation = false; + public analysis!: IAnalysisSettings; + public autoUpdateLanguageServer: boolean = true; + public datascience!: IDataScienceSettings; + public insidersChannel!: ExtensionChannels; + public experiments!: IExperiments; + public languageServer: LanguageServerType = LanguageServerType.Microsoft; + public logging: ILoggingSettings = { level: LogLevel.Error }; + + protected readonly changed = new EventEmitter(); + private workspaceRoot: Resource; + private disposables: Disposable[] = []; + // tslint:disable-next-line:variable-name + private _pythonPath = ''; + private _defaultInterpreterPath = ''; + private readonly workspace: IWorkspaceService; + + constructor( + workspaceFolder: Resource, + private readonly interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, + workspace?: IWorkspaceService, + private readonly experimentsManager?: IExperimentsManager, + private readonly interpreterPathService?: IInterpreterPathService, + private readonly interpreterSecurityService?: IInterpreterSecurityService + ) { + this.workspace = workspace || new WorkspaceService(); + this.workspaceRoot = workspaceFolder; + this.initialize(); + } + // tslint:disable-next-line:function-name + public static getInstance( + resource: Uri | undefined, + interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, + workspace?: IWorkspaceService, + experimentsManager?: IExperimentsManager, + interpreterPathService?: IInterpreterPathService, + interpreterSecurityService?: IInterpreterSecurityService + ): PythonSettings { + workspace = workspace || new WorkspaceService(); + const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri; + const workspaceFolderKey = workspaceFolderUri ? workspaceFolderUri.fsPath : ''; + + if (!PythonSettings.pythonSettings.has(workspaceFolderKey)) { + const settings = new PythonSettings( + workspaceFolderUri, + interpreterAutoSelectionService, + workspace, + experimentsManager, + interpreterPathService, + interpreterSecurityService + ); + PythonSettings.pythonSettings.set(workspaceFolderKey, settings); + // Pass null to avoid VSC from complaining about not passing in a value. + // tslint:disable-next-line:no-any + const config = workspace.getConfiguration('editor', resource ? resource : (null as any)); + const formatOnType = config ? config.get('formatOnType', false) : false; + sendTelemetryEvent(EventName.COMPLETION_ADD_BRACKETS, undefined, { + enabled: settings.autoComplete ? settings.autoComplete.addBrackets : false + }); + sendTelemetryEvent(EventName.FORMAT_ON_TYPE, undefined, { enabled: formatOnType }); + } + // tslint:disable-next-line:no-non-null-assertion + return PythonSettings.pythonSettings.get(workspaceFolderKey)!; + } + + // tslint:disable-next-line:type-literal-delimiter + public static getSettingsUriAndTarget( + resource: Uri | undefined, + workspace?: IWorkspaceService + ): { uri: Uri | undefined; target: ConfigurationTarget } { + workspace = workspace || new WorkspaceService(); + const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; + let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined; + + if (!workspaceFolderUri && Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspaceFolderUri = workspace.workspaceFolders[0].uri; + } + + const target = workspaceFolderUri ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Global; + return { uri: workspaceFolderUri, target }; + } + + // tslint:disable-next-line:function-name + public static dispose() { + if (!isTestExecution()) { + throw new Error('Dispose can only be called from unit tests'); + } + // tslint:disable-next-line:no-void-expression + PythonSettings.pythonSettings.forEach((item) => item && item.dispose()); + PythonSettings.pythonSettings.clear(); + } + public dispose() { + // tslint:disable-next-line:no-unsafe-any + this.disposables.forEach((disposable) => disposable && disposable.dispose()); + this.disposables = []; + } + // tslint:disable-next-line:cyclomatic-complexity max-func-body-length + protected update(pythonSettings: WorkspaceConfiguration) { + const workspaceRoot = this.workspaceRoot?.fsPath; + const systemVariables: SystemVariables = new SystemVariables(undefined, workspaceRoot, this.workspace); + + this.pythonPath = this.getPythonPath(pythonSettings, systemVariables, workspaceRoot); + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const defaultInterpreterPath = systemVariables.resolveAny(pythonSettings.get('defaultInterpreterPath')); + this.defaultInterpreterPath = defaultInterpreterPath ? defaultInterpreterPath : DEFAULT_INTERPRETER_SETTING; + this.defaultInterpreterPath = getAbsolutePath(this.defaultInterpreterPath, workspaceRoot); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.venvPath = systemVariables.resolveAny(pythonSettings.get('venvPath'))!; + this.venvFolders = systemVariables.resolveAny(pythonSettings.get('venvFolders'))!; + const condaPath = systemVariables.resolveAny(pythonSettings.get('condaPath'))!; + this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath; + const pipenvPath = systemVariables.resolveAny(pythonSettings.get('pipenvPath'))!; + this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; + const poetryPath = systemVariables.resolveAny(pythonSettings.get('poetryPath'))!; + this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; + + this.downloadLanguageServer = systemVariables.resolveAny( + pythonSettings.get('downloadLanguageServer', true) + )!; + this.autoUpdateLanguageServer = systemVariables.resolveAny( + pythonSettings.get('autoUpdateLanguageServer', true) + )!; + + let ls = pythonSettings.get('languageServer') ?? LanguageServerType.Jedi; + ls = systemVariables.resolveAny(ls); + if (!Object.values(LanguageServerType).includes(ls)) { + ls = LanguageServerType.Jedi; + } + this.languageServer = ls; + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + this.jediPath = systemVariables.resolveAny(pythonSettings.get('jediPath'))!; + if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { + this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); + } else { + this.jediPath = ''; + } + this.jediMemoryLimit = pythonSettings.get('jediMemoryLimit')!; + + const envFileSetting = pythonSettings.get('envFile'); + this.envFile = systemVariables.resolveAny(envFileSetting)!; + sendSettingTelemetry(this.workspace, envFileSetting); + + // tslint:disable-next-line:no-any + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion no-any + this.devOptions = systemVariables.resolveAny(pythonSettings.get('devOptions'))!; this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; - var lintingSettings = pythonSettings.get("linting"); + + // tslint:disable-next-line: no-any + const loggingSettings = systemVariables.resolveAny(pythonSettings.get('logging'))!; + loggingSettings.level = convertSettingTypeToLogLevel(loggingSettings.level); + if (this.logging) { + Object.assign(this.logging, loggingSettings); + } else { + this.logging = loggingSettings; + } + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const lintingSettings = systemVariables.resolveAny(pythonSettings.get('linting'))!; if (this.linting) { Object.assign(this.linting, lintingSettings); - } - else { + } else { this.linting = lintingSettings; } - var formattingSettings = pythonSettings.get("formatting"); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const analysisSettings = systemVariables.resolveAny(pythonSettings.get('analysis'))!; + if (this.analysis) { + Object.assign(this.analysis, analysisSettings); + } else { + this.analysis = analysisSettings; + } + + this.disableInstallationChecks = pythonSettings.get('disableInstallationCheck') === true; + this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; + if (this.sortImports) { + Object.assign(this.sortImports, sortImportSettings); + } else { + this.sortImports = sortImportSettings; + } + // Support for travis. + this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; + // Support for travis. + this.linting = this.linting + ? this.linting + : { + enabled: false, + ignorePatterns: [], + flake8Args: [], + flake8Enabled: false, + flake8Path: 'flake', + lintOnSave: false, + maxNumberOfProblems: 100, + mypyArgs: [], + mypyEnabled: false, + mypyPath: 'mypy', + banditArgs: [], + banditEnabled: false, + banditPath: 'bandit', + pycodestyleArgs: [], + pycodestyleEnabled: false, + pycodestylePath: 'pycodestyle', + pylamaArgs: [], + pylamaEnabled: false, + pylamaPath: 'pylama', + prospectorArgs: [], + prospectorEnabled: false, + prospectorPath: 'prospector', + pydocstyleArgs: [], + pydocstyleEnabled: false, + pydocstylePath: 'pydocstyle', + pylintArgs: [], + pylintEnabled: false, + pylintPath: 'pylint', + pylintCategorySeverity: { + convention: DiagnosticSeverity.Hint, + error: DiagnosticSeverity.Error, + fatal: DiagnosticSeverity.Error, + refactor: DiagnosticSeverity.Hint, + warning: DiagnosticSeverity.Warning + }, + pycodestyleCategorySeverity: { + E: DiagnosticSeverity.Error, + W: DiagnosticSeverity.Warning + }, + flake8CategorySeverity: { + E: DiagnosticSeverity.Error, + W: DiagnosticSeverity.Warning, + // Per http://flake8.pycqa.org/en/latest/glossary.html#term-error-code + // 'F' does not mean 'fatal as in PyLint but rather 'pyflakes' such as + // unused imports, variables, etc. + F: DiagnosticSeverity.Warning + }, + mypyCategorySeverity: { + error: DiagnosticSeverity.Error, + note: DiagnosticSeverity.Hint + }, + pylintUseMinimalCheckers: false + }; + this.linting.pylintPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylintPath), workspaceRoot); + this.linting.flake8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.flake8Path), workspaceRoot); + this.linting.pycodestylePath = getAbsolutePath( + systemVariables.resolveAny(this.linting.pycodestylePath), + workspaceRoot + ); + this.linting.pylamaPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylamaPath), workspaceRoot); + this.linting.prospectorPath = getAbsolutePath( + systemVariables.resolveAny(this.linting.prospectorPath), + workspaceRoot + ); + this.linting.pydocstylePath = getAbsolutePath( + systemVariables.resolveAny(this.linting.pydocstylePath), + workspaceRoot + ); + this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot); + this.linting.banditPath = getAbsolutePath(systemVariables.resolveAny(this.linting.banditPath), workspaceRoot); + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const formattingSettings = systemVariables.resolveAny(pythonSettings.get('formatting'))!; if (this.formatting) { Object.assign(this.formatting, formattingSettings); - } - else { + } else { this.formatting = formattingSettings; } + // Support for travis. + this.formatting = this.formatting + ? this.formatting + : { + autopep8Args: [], + autopep8Path: 'autopep8', + provider: 'autopep8', + blackArgs: [], + blackPath: 'black', + yapfArgs: [], + yapfPath: 'yapf' + }; + this.formatting.autopep8Path = getAbsolutePath( + systemVariables.resolveAny(this.formatting.autopep8Path), + workspaceRoot + ); + this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); + this.formatting.blackPath = getAbsolutePath( + systemVariables.resolveAny(this.formatting.blackPath), + workspaceRoot + ); - var autoCompleteSettings = pythonSettings.get("autoComplete"); + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const autoCompleteSettings = systemVariables.resolveAny( + pythonSettings.get('autoComplete') + )!; if (this.autoComplete) { - Object.assign(this.autoComplete, autoCompleteSettings); - } - else { + Object.assign(this.autoComplete, autoCompleteSettings); + } else { this.autoComplete = autoCompleteSettings; } + // Support for travis. + this.autoComplete = this.autoComplete + ? this.autoComplete + : { + extraPaths: [], + addBrackets: false, + showAdvancedMembers: false, + typeshedPaths: [] + }; + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const workspaceSymbolsSettings = systemVariables.resolveAny( + pythonSettings.get('workspaceSymbols') + )!; + if (this.workspaceSymbols) { + Object.assign( + this.workspaceSymbols, + workspaceSymbolsSettings + ); + } else { + this.workspaceSymbols = workspaceSymbolsSettings; + } + // Support for travis. + this.workspaceSymbols = this.workspaceSymbols + ? this.workspaceSymbols + : { + ctagsPath: 'ctags', + enabled: true, + exclusionPatterns: [], + rebuildOnFileSave: true, + rebuildOnStart: true, + tagFilePath: workspaceRoot ? path.join(workspaceRoot, 'tags') : '' + }; + this.workspaceSymbols.tagFilePath = getAbsolutePath( + systemVariables.resolveAny(this.workspaceSymbols.tagFilePath), + workspaceRoot + ); + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const testSettings = systemVariables.resolveAny(pythonSettings.get('testing'))!; + if (this.testing) { + Object.assign(this.testing, testSettings); + } else { + this.testing = testSettings; + if (isTestExecution() && !this.testing) { + // tslint:disable-next-line:prefer-type-cast + // tslint:disable-next-line:no-object-literal-type-assertion + this.testing = { + nosetestArgs: [], + pytestArgs: [], + unittestArgs: [], + promptToConfigure: true, + debugPort: 3000, + nosetestsEnabled: false, + pytestEnabled: false, + unittestEnabled: false, + nosetestPath: 'nosetests', + pytestPath: 'pytest', + autoTestDiscoverOnSaveEnabled: true + } as ITestingSettings; + } + } + + // Support for travis. + this.testing = this.testing + ? this.testing + : { + promptToConfigure: true, + debugPort: 3000, + nosetestArgs: [], + nosetestPath: 'nosetest', + nosetestsEnabled: false, + pytestArgs: [], + pytestEnabled: false, + pytestPath: 'pytest', + unittestArgs: [], + unittestEnabled: false, + autoTestDiscoverOnSaveEnabled: true + }; + this.testing.pytestPath = getAbsolutePath(systemVariables.resolveAny(this.testing.pytestPath), workspaceRoot); + this.testing.nosetestPath = getAbsolutePath( + systemVariables.resolveAny(this.testing.nosetestPath), + workspaceRoot + ); + if (this.testing.cwd) { + this.testing.cwd = getAbsolutePath(systemVariables.resolveAny(this.testing.cwd), workspaceRoot); + } + + // Resolve any variables found in the test arguments. + this.testing.nosetestArgs = this.testing.nosetestArgs.map((arg) => systemVariables.resolveAny(arg)); + this.testing.pytestArgs = this.testing.pytestArgs.map((arg) => systemVariables.resolveAny(arg)); + this.testing.unittestArgs = this.testing.unittestArgs.map((arg) => systemVariables.resolveAny(arg)); + + // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion + const terminalSettings = systemVariables.resolveAny(pythonSettings.get('terminal'))!; + if (this.terminal) { + Object.assign(this.terminal, terminalSettings); + } else { + this.terminal = terminalSettings; + if (isTestExecution() && !this.terminal) { + // tslint:disable-next-line:prefer-type-cast + // tslint:disable-next-line:no-object-literal-type-assertion + this.terminal = {} as ITerminalSettings; + } + } + // Support for travis. + this.terminal = this.terminal + ? this.terminal + : { + executeInFileDir: true, + launchArgs: [], + activateEnvironment: true, + activateEnvInCurrentTerminal: false + }; + + const experiments = systemVariables.resolveAny(pythonSettings.get('experiments'))!; + if (this.experiments) { + Object.assign(this.experiments, experiments); + } else { + this.experiments = experiments; + } + this.experiments = this.experiments + ? this.experiments + : { + enabled: true, + optInto: [], + optOutFrom: [] + }; - var unitTestSettings = pythonSettings.get("unitTest"); - if (this.unitTest) { - Object.assign(this.unitTest, unitTestSettings); + const dataScienceSettings = systemVariables.resolveAny( + pythonSettings.get('dataScience') + )!; + if (this.datascience) { + Object.assign(this.datascience, dataScienceSettings); + } else { + this.datascience = dataScienceSettings; } - else { - this.unitTest = unitTestSettings; + + const showStartPage = pythonSettings.get('showStartPage'); + if (showStartPage !== undefined) { + this.showStartPage = showStartPage; } + + this.insidersChannel = pythonSettings.get('insidersChannel')!; } - public pythonPath: string; - public devOptions: any[]; - public linting: ILintingSettings; - public formatting: IFormattingSettings; - public autoComplete: IAutoCompeteSettings; - public unitTest: IUnitTestSettings; -} \ No newline at end of file + protected getPythonExecutable(pythonPath: string) { + return getPythonExecutable(pythonPath); + } + protected onWorkspaceFoldersChanged() { + //If an activated workspace folder was removed, delete its key + const workspaceKeys = this.workspace.workspaceFolders!.map((workspaceFolder) => workspaceFolder.uri.fsPath); + const activatedWkspcKeys = Array.from(PythonSettings.pythonSettings.keys()); + const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter((item) => workspaceKeys.indexOf(item) < 0); + if (activatedWkspcFoldersRemoved.length > 0) { + for (const folder of activatedWkspcFoldersRemoved) { + PythonSettings.pythonSettings.delete(folder); + } + } + } + protected initialize(): void { + const onDidChange = () => { + const currentConfig = this.workspace.getConfiguration('python', this.workspaceRoot); + this.update(currentConfig); + + // If workspace config changes, then we could have a cascading effect of on change events. + // Let's defer the change notification. + this.debounceChangeNotification(); + }; + this.disposables.push(this.workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); + this.disposables.push( + this.interpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this)) + ); + if (this.interpreterSecurityService) { + this.disposables.push(this.interpreterSecurityService.onDidChangeSafeInterpreters(onDidChange.bind(this))); + } + this.disposables.push( + this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => { + if (event.affectsConfiguration('python')) { + onDidChange(); + } + }) + ); + if (this.interpreterPathService) { + this.disposables.push(this.interpreterPathService.onDidChange(onDidChange.bind(this))); + } + + const initialConfig = this.workspace.getConfiguration('python', this.workspaceRoot); + if (initialConfig) { + this.update(initialConfig); + } + } + @debounceSync(1) + protected debounceChangeNotification() { + this.changed.fire(); + } + + private getPythonPath( + pythonSettings: WorkspaceConfiguration, + systemVariables: SystemVariables, + workspaceRoot: string | undefined + ) { + /** + * Note that while calling `IExperimentsManager.inExperiment()`, we assume `IExperimentsManager.activate()` is already called. + * That's not true here, as this method is often called in the constructor,which runs before `.activate()` methods. + * But we can still use it here for this particular experiment. Reason being that this experiment only changes + * `pythonPath` setting, and I've checked that `pythonPath` setting is not accessed anywhere in the constructor. + */ + const inExperiment = this.experimentsManager?.inExperiment(DeprecatePythonPath.experiment); + this.experimentsManager?.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + // Use the interpreter path service if in the experiment otherwise use the normal settings + this.pythonPath = systemVariables.resolveAny( + inExperiment && this.interpreterPathService + ? this.interpreterPathService.get(this.workspaceRoot) + : pythonSettings.get('pythonPath') + )!; + if (!process.env.CI_DISABLE_AUTO_SELECTION && (this.pythonPath.length === 0 || this.pythonPath === 'python')) { + const autoSelectedPythonInterpreter = this.interpreterAutoSelectionService.getAutoSelectedInterpreter( + this.workspaceRoot + ); + if (inExperiment && this.interpreterSecurityService) { + if ( + autoSelectedPythonInterpreter && + this.interpreterSecurityService.isSafe(autoSelectedPythonInterpreter) && + this.workspaceRoot + ) { + this.pythonPath = autoSelectedPythonInterpreter.path; + this.interpreterAutoSelectionService + .setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter) + .ignoreErrors(); + } + } else { + if (autoSelectedPythonInterpreter && this.workspaceRoot) { + this.pythonPath = autoSelectedPythonInterpreter.path; + this.interpreterAutoSelectionService + .setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter) + .ignoreErrors(); + } + } + } + if (inExperiment && this.pythonPath === DEFAULT_INTERPRETER_SETTING) { + // If no interpreter is selected, set pythonPath to an empty string. + // This is to ensure that we ask users to select an interpreter in case auto selected interpreter is not safe to select + this.pythonPath = ''; + } + return getAbsolutePath(this.pythonPath, workspaceRoot); + } +} + +function getAbsolutePath(pathToCheck: string, rootDir: string | undefined): string { + if (!rootDir) { + rootDir = __dirname; + } + // tslint:disable-next-line:prefer-type-cast no-unsafe-any + pathToCheck = untildify(pathToCheck) as string; + if (isTestExecution() && !pathToCheck) { + return rootDir; + } + if (pathToCheck.indexOf(path.sep) === -1) { + return pathToCheck; + } + return path.isAbsolute(pathToCheck) ? pathToCheck : path.resolve(rootDir, pathToCheck); +} + +function getPythonExecutable(pythonPath: string): string { + // tslint:disable-next-line:prefer-type-cast no-unsafe-any + pythonPath = untildify(pythonPath) as string; + + // If only 'python'. + if ( + pythonPath === 'python' || + pythonPath.indexOf(path.sep) === -1 || + path.basename(pythonPath) === path.dirname(pythonPath) + ) { + return pythonPath; + } + + if (isValidPythonPath(pythonPath)) { + return pythonPath; + } + // Keep python right on top, for backwards compatibility. + // tslint:disable-next-line:variable-name + const KnownPythonExecutables = ['python', 'python4', 'python3.6', 'python3.5', 'python3', 'python2.7', 'python2']; + + for (let executableName of KnownPythonExecutables) { + // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'. + if (IS_WINDOWS) { + executableName = `${executableName}.exe`; + if (isValidPythonPath(path.join(pythonPath, executableName))) { + return path.join(pythonPath, executableName); + } + if (isValidPythonPath(path.join(pythonPath, 'scripts', executableName))) { + return path.join(pythonPath, 'scripts', executableName); + } + } else { + if (isValidPythonPath(path.join(pythonPath, executableName))) { + return path.join(pythonPath, executableName); + } + if (isValidPythonPath(path.join(pythonPath, 'bin', executableName))) { + return path.join(pythonPath, 'bin', executableName); + } + } + } + + return pythonPath; +} + +function isValidPythonPath(pythonPath: string): boolean { + const [args, parse] = internalPython.isValid(); + try { + const output = child_process.execFileSync(pythonPath, args, { encoding: 'utf8' }); + return parse(output); + } catch (ex) { + return false; + } +} + +function convertSettingTypeToLogLevel(setting: LoggingLevelSettingType | undefined): LogLevel | 'off' { + switch (setting) { + case 'info': { + return LogLevel.Info; + } + case 'warn': { + return LogLevel.Warn; + } + case 'off': { + return 'off'; + } + case 'debug': { + return LogLevel.Debug; + } + default: { + return LogLevel.Error; + } + } +} diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts new file mode 100644 index 000000000000..b922254d8b05 --- /dev/null +++ b/src/client/common/configuration/service.ts @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; +import { + IInterpreterAutoSeletionProxyService, + IInterpreterSecurityService +} from '../../interpreter/autoSelection/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IWorkspaceService } from '../application/types'; +import { PythonSettings } from '../configSettings'; +import { isUnitTestExecution } from '../constants'; +import { DeprecatePythonPath } from '../experiments/groups'; +import { IConfigurationService, IExperimentsManager, IInterpreterPathService, IPythonSettings } from '../types'; + +@injectable() +export class ConfigurationService implements IConfigurationService { + private readonly workspaceService: IWorkspaceService; + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { + this.workspaceService = this.serviceContainer.get(IWorkspaceService); + } + public getSettings(resource?: Uri): IPythonSettings { + const InterpreterAutoSelectionService = this.serviceContainer.get( + IInterpreterAutoSeletionProxyService + ); + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const experiments = this.serviceContainer.get(IExperimentsManager); + const interpreterSecurityService = this.serviceContainer.get( + IInterpreterSecurityService + ); + return PythonSettings.getInstance( + resource, + InterpreterAutoSelectionService, + this.workspaceService, + experiments, + interpreterPathService, + interpreterSecurityService + ); + } + + public async updateSectionSetting( + section: string, + setting: string, + value?: {}, + resource?: Uri, + configTarget?: ConfigurationTarget + ): Promise { + const experiments = this.serviceContainer.get(IExperimentsManager); + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const inExperiment = experiments.inExperiment(DeprecatePythonPath.experiment); + experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + const defaultSetting = { + uri: resource, + target: configTarget || ConfigurationTarget.WorkspaceFolder + }; + let settingsInfo = defaultSetting; + if (section === 'python' && configTarget !== ConfigurationTarget.Global) { + settingsInfo = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService); + } + configTarget = configTarget ? configTarget : settingsInfo.target; + + const configSection = this.workspaceService.getConfiguration(section, settingsInfo.uri); + const currentValue = + inExperiment && section === 'python' && setting === 'pythonPath' + ? interpreterPathService.inspect(settingsInfo.uri) + : configSection.inspect(setting); + + if ( + currentValue !== undefined && + ((configTarget === ConfigurationTarget.Global && currentValue.globalValue === value) || + (configTarget === ConfigurationTarget.Workspace && currentValue.workspaceValue === value) || + (configTarget === ConfigurationTarget.WorkspaceFolder && currentValue.workspaceFolderValue === value)) + ) { + return; + } + if (section === 'python' && setting === 'pythonPath') { + if (inExperiment) { + // tslint:disable-next-line: no-any + await interpreterPathService.update(settingsInfo.uri, configTarget, value as any); + } + } else { + await configSection.update(setting, value, configTarget); + await this.verifySetting(configSection, configTarget, setting, value); + } + } + + public async updateSetting( + setting: string, + value?: {}, + resource?: Uri, + configTarget?: ConfigurationTarget + ): Promise { + return this.updateSectionSetting('python', setting, value, resource, configTarget); + } + + public isTestExecution(): boolean { + return process.env.VSC_PYTHON_CI_TEST === '1'; + } + + private async verifySetting( + configSection: WorkspaceConfiguration, + target: ConfigurationTarget, + settingName: string, + value?: {} + ): Promise { + if (this.isTestExecution() && !isUnitTestExecution()) { + let retries = 0; + do { + const setting = configSection.inspect(settingName); + if (!setting && value === undefined) { + break; // Both are unset + } + if (setting && value !== undefined) { + // Both specified + const actual = + target === ConfigurationTarget.Global + ? setting.globalValue + : target === ConfigurationTarget.Workspace + ? setting.workspaceValue + : setting.workspaceFolderValue; + if (actual === value) { + break; + } + } + // Wait for settings to get refreshed. + await new Promise((resolve) => setTimeout(resolve, 250)); + retries += 1; + } while (retries < 20); + } + } +} diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts new file mode 100644 index 000000000000..6096285fa170 --- /dev/null +++ b/src/client/common/constants.ts @@ -0,0 +1,118 @@ +export const PYTHON_LANGUAGE = 'python'; +export const MARKDOWN_LANGUAGE = 'markdown'; +export const JUPYTER_LANGUAGE = 'jupyter'; + +export const PYTHON_WARNINGS = 'PYTHONWARNINGS'; + +export const NotebookCellScheme = 'vscode-notebook-cell'; +export const PYTHON = [ + { scheme: 'file', language: PYTHON_LANGUAGE }, + { scheme: 'untitled', language: PYTHON_LANGUAGE }, + { scheme: 'vscode-notebook', language: PYTHON_LANGUAGE }, + { scheme: NotebookCellScheme, language: PYTHON_LANGUAGE } +]; +export const PYTHON_ALLFILES = [{ language: PYTHON_LANGUAGE }]; + +export const PVSC_EXTENSION_ID = 'ms-python.python'; +export const CODE_RUNNER_EXTENSION_ID = 'formulahendry.code-runner'; +export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance'; +export const AppinsightsKey = 'AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217'; + +export namespace Commands { + export const Set_Interpreter = 'python.setInterpreter'; + export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; + export const Exec_In_Terminal = 'python.execInTerminal'; + export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; + export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; + export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell'; + export const Tests_View_UI = 'python.viewTestUI'; + export const Tests_Picker_UI = 'python.selectTestToRun'; + export const Tests_Picker_UI_Debug = 'python.selectTestToDebug'; + export const Tests_Configure = 'python.configureTests'; + export const Tests_Discover = 'python.discoverTests'; + export const Tests_Discovering = 'python.discoveringTests'; + export const Tests_Run_Failed = 'python.runFailedTests'; + export const Sort_Imports = 'python.sortImports'; + export const Tests_Run = 'python.runtests'; + export const Tests_Run_Parametrized = 'python.runParametrizedTests'; + export const Tests_Debug = 'python.debugtests'; + export const Tests_Ask_To_Stop_Test = 'python.askToStopTests'; + export const Tests_Ask_To_Stop_Discovery = 'python.askToStopTestDiscovery'; + export const Tests_Stop = 'python.stopTests'; + export const Test_Reveal_Test_Item = 'python.revealTestItem'; + export const ViewOutput = 'python.viewOutput'; + export const Tests_ViewOutput = 'python.viewTestOutput'; + export const Tests_Select_And_Run_Method = 'python.selectAndRunTestMethod'; + export const Tests_Select_And_Debug_Method = 'python.selectAndDebugTestMethod'; + export const Tests_Select_And_Run_File = 'python.selectAndRunTestFile'; + export const Tests_Run_Current_File = 'python.runCurrentTestFile'; + export const Refactor_Extract_Variable = 'python.refactorExtractVariable'; + export const Refactor_Extract_Method = 'python.refactorExtractMethod'; + export const Build_Workspace_Symbols = 'python.buildWorkspaceSymbols'; + export const Start_REPL = 'python.startREPL'; + export const Create_Terminal = 'python.createTerminal'; + export const Set_Linter = 'python.setLinter'; + export const Enable_Linter = 'python.enableLinting'; + export const Run_Linter = 'python.runLinting'; + export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; + export const navigateToTestFunction = 'navigateToTestFunction'; + export const navigateToTestSuite = 'navigateToTestSuite'; + export const navigateToTestFile = 'navigateToTestFile'; + export const openTestNodeInEditor = 'python.openTestNodeInEditor'; + export const runTestNode = 'python.runTestNode'; + export const debugTestNode = 'python.debugTestNode'; + export const SwitchOffInsidersChannel = 'python.switchOffInsidersChannel'; + export const SwitchToInsidersDaily = 'python.switchToDailyChannel'; + export const SwitchToInsidersWeekly = 'python.switchToWeeklyChannel'; + export const PickLocalProcess = 'python.pickLocalProcess'; + export const GetSelectedInterpreterPath = 'python.interpreterPath'; + export const ClearWorkspaceInterpreter = 'python.clearWorkspaceInterpreter'; + export const ResetInterpreterSecurityStorage = 'python.resetInterpreterSecurityStorage'; + export const OpenStartPage = 'python.startPage.open'; +} +export namespace Octicons { + export const Test_Pass = '$(check)'; + export const Test_Fail = '$(alert)'; + export const Test_Error = '$(x)'; + export const Test_Skip = '$(circle-slash)'; + export const Downloading = '$(cloud-download)'; + export const Installing = '$(desktop-download)'; +} + +export const Button_Text_Tests_View_Output = 'View Output'; + +export namespace Text { + export const CodeLensRunUnitTest = 'Run Test'; + export const CodeLensDebugUnitTest = 'Debug Test'; +} +export namespace Delays { + // Max time to wait before aborting the generation of code lenses for unit tests + export const MaxUnitTestCodeLensDelay = 5000; +} + +export const DEFAULT_INTERPRETER_SETTING = 'python'; + +export const STANDARD_OUTPUT_CHANNEL = 'STANDARD_OUTPUT_CHANNEL'; + +export const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; + +export function isTestExecution(): boolean { + return process.env.VSC_PYTHON_CI_TEST === '1' || isUnitTestExecution(); +} + +/** + * Whether we're running unit tests (*.unit.test.ts). + * These tests have a speacial meaning, they run fast. + * @export + * @returns {boolean} + */ +export function isUnitTestExecution(): boolean { + return process.env.VSC_PYTHON_UNIT_TEST === '1'; +} + +// Temporary constant, used to indicate whether we're using custom editor api or not. +export const UseCustomEditorApi = Symbol('USE_CUSTOM_EDITOR'); +export const UseVSCodeNotebookEditorApi = Symbol('USE_NATIVEEDITOR'); +export const UseProposedApi = Symbol('USE_VSC_PROPOSED_API'); + +export * from '../constants'; diff --git a/src/client/common/contextKey.ts b/src/client/common/contextKey.ts new file mode 100644 index 000000000000..96022a3ba3ce --- /dev/null +++ b/src/client/common/contextKey.ts @@ -0,0 +1,18 @@ +import { ICommandManager } from './application/types'; + +export class ContextKey { + public get value(): boolean | undefined { + return this.lastValue; + } + private lastValue?: boolean; + + constructor(private name: string, private commandManager: ICommandManager) {} + + public async set(value: boolean): Promise { + if (this.lastValue === value) { + return; + } + this.lastValue = value; + await this.commandManager.executeCommand('setContext', this.name, this.lastValue); + } +} diff --git a/src/client/common/crypto.ts b/src/client/common/crypto.ts new file mode 100644 index 000000000000..3067ebb4a801 --- /dev/null +++ b/src/client/common/crypto.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// tslint:disable: no-any + +import { createHash } from 'crypto'; +import { injectable } from 'inversify'; +import { traceError } from './logger'; +import { ICryptoUtils, IHashFormat } from './types'; + +/** + * Implements tools related to cryptography + */ +@injectable() +export class CryptoUtils implements ICryptoUtils { + public createHash( + data: string, + hashFormat: E, + algorithm: 'SHA512' | 'SHA256' | 'FNV' = 'FNV' + ): IHashFormat[E] { + let hash: string; + if (algorithm === 'FNV') { + // tslint:disable-next-line:no-require-imports + const fnv = require('@enonic/fnv-plus'); + hash = fnv.fast1a32hex(data) as string; + } else if (algorithm === 'SHA256') { + hash = createHash('sha256').update(data).digest('hex'); + } else { + hash = createHash('sha512').update(data).digest('hex'); + } + if (hashFormat === 'number') { + const result = parseInt(hash, 16); + if (isNaN(result)) { + traceError(`Number hash for data '${data}' is NaN`); + } + return result as any; + } + return hash as any; + } +} diff --git a/src/client/common/dotnet/compatibilityService.ts b/src/client/common/dotnet/compatibilityService.ts new file mode 100644 index 000000000000..421fd7f5dfa9 --- /dev/null +++ b/src/client/common/dotnet/compatibilityService.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { IPlatformService } from '../platform/types'; +import { OSType } from '../utils/platform'; +import { IDotNetCompatibilityService, IOSDotNetCompatibilityService } from './types'; + +/** + * .NET Core 2.1 OS Requirements + * https://github.com/dotnet/core/blob/master/release-notes/2.1/2.1-supported-os.md + * We are using the versions provided in the above .NET 2.1 Core requirements page as minimum required versions. + * Why, cuz getting distros, mapping them to the ones listd on .NET 2.1 Core requirements are entirely accurate. + * Due to the inaccuracy, its easier and safer to just assume futur versions of an OS are also supported. + * We will need to regularly update the requirements over time, when using .NET Core 2.2 or 3, etc. + */ +@injectable() +export class DotNetCompatibilityService implements IDotNetCompatibilityService { + private readonly mappedServices = new Map(); + constructor( + @inject(IOSDotNetCompatibilityService) @named(OSType.Unknown) unknownOsService: IOSDotNetCompatibilityService, + @inject(IOSDotNetCompatibilityService) @named(OSType.OSX) macService: IOSDotNetCompatibilityService, + @inject(IOSDotNetCompatibilityService) @named(OSType.Windows) winService: IOSDotNetCompatibilityService, + @inject(IOSDotNetCompatibilityService) @named(OSType.Linux) linuxService: IOSDotNetCompatibilityService, + @inject(IPlatformService) private readonly platformService: IPlatformService + ) { + this.mappedServices.set(OSType.Unknown, unknownOsService); + this.mappedServices.set(OSType.OSX, macService); + this.mappedServices.set(OSType.Windows, winService); + this.mappedServices.set(OSType.Linux, linuxService); + } + public isSupported() { + return this.mappedServices.get(this.platformService.osType)!.isSupported(); + } +} diff --git a/src/client/common/dotnet/serviceRegistry.ts b/src/client/common/dotnet/serviceRegistry.ts new file mode 100644 index 000000000000..1c1f18bb5845 --- /dev/null +++ b/src/client/common/dotnet/serviceRegistry.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { IServiceManager } from '../../ioc/types'; +import { OSType } from '../utils/platform'; +import { DotNetCompatibilityService } from './compatibilityService'; +import { LinuxDotNetCompatibilityService } from './services/linuxCompatibilityService'; +import { MacDotNetCompatibilityService } from './services/macCompatibilityService'; +import { UnknownOSDotNetCompatibilityService } from './services/unknownOsCompatibilityService'; +import { WindowsDotNetCompatibilityService } from './services/windowsCompatibilityService'; +import { IDotNetCompatibilityService, IOSDotNetCompatibilityService } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IDotNetCompatibilityService, DotNetCompatibilityService); + serviceManager.addSingleton( + IOSDotNetCompatibilityService, + MacDotNetCompatibilityService, + OSType.OSX + ); + serviceManager.addSingleton( + IOSDotNetCompatibilityService, + WindowsDotNetCompatibilityService, + OSType.Windows + ); + serviceManager.addSingleton( + IOSDotNetCompatibilityService, + LinuxDotNetCompatibilityService, + OSType.Linux + ); + serviceManager.addSingleton( + IOSDotNetCompatibilityService, + UnknownOSDotNetCompatibilityService, + OSType.Unknown + ); +} diff --git a/src/client/common/dotnet/services/linuxCompatibilityService.ts b/src/client/common/dotnet/services/linuxCompatibilityService.ts new file mode 100644 index 000000000000..dbd67d221371 --- /dev/null +++ b/src/client/common/dotnet/services/linuxCompatibilityService.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { traceDecorators, traceError } from '../../logger'; +import { IPlatformService } from '../../platform/types'; +import { IOSDotNetCompatibilityService } from '../types'; + +@injectable() +export class LinuxDotNetCompatibilityService implements IOSDotNetCompatibilityService { + constructor(@inject(IPlatformService) private readonly platformService: IPlatformService) {} + @traceDecorators.verbose('Checking support of .NET') + public async isSupported() { + if (!this.platformService.is64bit) { + traceError('.NET is not supported on 32 Bit Linux'); + return false; + } + return true; + } +} diff --git a/src/client/common/dotnet/services/macCompatibilityService.ts b/src/client/common/dotnet/services/macCompatibilityService.ts new file mode 100644 index 000000000000..0fbf44b0fa68 --- /dev/null +++ b/src/client/common/dotnet/services/macCompatibilityService.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IPlatformService } from '../../platform/types'; +import { IOSDotNetCompatibilityService } from '../types'; + +// Min version on https://github.com/dotnet/core/blob/master/release-notes/2.1/2.1-supported-os.md is 10.12. +// On this site https://en.wikipedia.org/wiki/MacOS_Sierra, that maps to 16.0.0. +const minVersion = '16.0.0'; + +@injectable() +export class MacDotNetCompatibilityService implements IOSDotNetCompatibilityService { + constructor(@inject(IPlatformService) private readonly platformService: IPlatformService) {} + public async isSupported() { + const version = await this.platformService.getVersion(); + return version.compare(minVersion) >= 0; + } +} diff --git a/src/client/common/dotnet/services/unknownOsCompatibilityService.ts b/src/client/common/dotnet/services/unknownOsCompatibilityService.ts new file mode 100644 index 000000000000..728a29eacf37 --- /dev/null +++ b/src/client/common/dotnet/services/unknownOsCompatibilityService.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { traceDecorators } from '../../logger'; +import { IOSDotNetCompatibilityService } from '../types'; + +@injectable() +export class UnknownOSDotNetCompatibilityService implements IOSDotNetCompatibilityService { + @traceDecorators.info('Unable to determine compatiblity of DOT.NET with an unknown OS') + public async isSupported() { + return false; + } +} diff --git a/src/client/common/dotnet/services/windowsCompatibilityService.ts b/src/client/common/dotnet/services/windowsCompatibilityService.ts new file mode 100644 index 000000000000..6e616909de3c --- /dev/null +++ b/src/client/common/dotnet/services/windowsCompatibilityService.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { IOSDotNetCompatibilityService } from '../types'; + +@injectable() +export class WindowsDotNetCompatibilityService implements IOSDotNetCompatibilityService { + public async isSupported() { + return true; + } +} diff --git a/src/client/common/dotnet/types.ts b/src/client/common/dotnet/types.ts new file mode 100644 index 000000000000..75db7d6850c3 --- /dev/null +++ b/src/client/common/dotnet/types.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export const IDotNetCompatibilityService = Symbol('IDotNetCompatibilityService'); +export interface IDotNetCompatibilityService { + isSupported(): Promise; +} +export const IOSDotNetCompatibilityService = Symbol('IOSDotNetCompatibilityService'); +export interface IOSDotNetCompatibilityService extends IDotNetCompatibilityService {} diff --git a/src/client/common/editor.ts b/src/client/common/editor.ts new file mode 100644 index 000000000000..2e32e3a81642 --- /dev/null +++ b/src/client/common/editor.ts @@ -0,0 +1,409 @@ +import { Diff, diff_match_patch } from 'diff-match-patch'; +import { injectable } from 'inversify'; +import * as md5 from 'md5'; +import { EOL } from 'os'; +import * as path from 'path'; +import { Position, Range, TextDocument, TextEdit, Uri, WorkspaceEdit } from 'vscode'; +import { IFileSystem } from '../common/platform/types'; +import { WrappedError } from './errors/errorUtils'; +import { traceError } from './logger'; +import { IEditorUtils } from './types'; +import { isNotebookCell } from './utils/misc'; + +// Code borrowed from goFormat.ts (Go Extension for VS Code) +enum EditAction { + Delete, + Insert, + Replace +} + +const NEW_LINE_LENGTH = EOL.length; + +class Patch { + public diffs!: Diff[]; + public start1!: number; + public start2!: number; + public length1!: number; + public length2!: number; +} + +class Edit { + public action: EditAction; + public start: Position; + public end!: Position; + public text: string; + + constructor(action: number, start: Position) { + this.action = action; + this.start = start; + this.text = ''; + } + + public apply(): TextEdit { + switch (this.action) { + case EditAction.Insert: + return TextEdit.insert(this.start, this.text); + case EditAction.Delete: + return TextEdit.delete(new Range(this.start, this.end)); + case EditAction.Replace: + return TextEdit.replace(new Range(this.start, this.end), this.text); + default: + return new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), ''); + } + } +} + +export function getTextEditsFromPatch(before: string, patch: string): TextEdit[] { + if (patch.startsWith('---')) { + // Strip the first two lines + patch = patch.substring(patch.indexOf('@@')); + } + if (patch.length === 0) { + return []; + } + // Remove the text added by unified_diff + // # Work around missing newline (http://bugs.python.org/issue2142). + patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); + // tslint:disable-next-line:no-require-imports + const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); + const d = new dmp.diff_match_patch(); + const patches = patch_fromText.call(d, patch); + if (!Array.isArray(patches) || patches.length === 0) { + throw new Error('Unable to parse Patch string'); + } + const textEdits: TextEdit[] = []; + + // Add line feeds and build the text edits + patches.forEach((p) => { + p.diffs.forEach((diff) => { + diff[1] += EOL; + }); + getTextEditsInternal(before, p.diffs, p.start1).forEach((edit) => textEdits.push(edit.apply())); + }); + + return textEdits; +} +export function getWorkspaceEditsFromPatch( + filePatches: string[], + workspaceRoot: string | undefined, + fs: IFileSystem +): WorkspaceEdit { + const workspaceEdit = new WorkspaceEdit(); + filePatches.forEach((patch) => { + const indexOfAtAt = patch.indexOf('@@'); + if (indexOfAtAt === -1) { + return; + } + const fileNameLines = patch + .substring(0, indexOfAtAt) + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0 && line.toLowerCase().endsWith('.py') && line.indexOf(' a') > 0); + + if (patch.startsWith('---')) { + // Strip the first two lines + patch = patch.substring(indexOfAtAt); + } + if (patch.length === 0) { + return; + } + // We can't find the find name + if (fileNameLines.length === 0) { + return; + } + + let fileName = fileNameLines[0].substring(fileNameLines[0].indexOf(' a') + 3).trim(); + fileName = workspaceRoot && !path.isAbsolute(fileName) ? path.resolve(workspaceRoot, fileName) : fileName; + if (!fs.fileExistsSync(fileName)) { + return; + } + + // Remove the text added by unified_diff + // # Work around missing newline (http://bugs.python.org/issue2142). + patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); + + // tslint:disable-next-line:no-require-imports + const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); + const d = new dmp.diff_match_patch(); + const patches = patch_fromText.call(d, patch); + if (!Array.isArray(patches) || patches.length === 0) { + throw new Error('Unable to parse Patch string'); + } + + const fileSource = fs.readFileSync(fileName); + const fileUri = Uri.file(fileName); + + // Add line feeds and build the text edits + patches.forEach((p) => { + p.diffs.forEach((diff) => { + diff[1] += EOL; + }); + + getTextEditsInternal(fileSource, p.diffs, p.start1).forEach((edit) => { + switch (edit.action) { + case EditAction.Delete: + workspaceEdit.delete(fileUri, new Range(edit.start, edit.end)); + break; + case EditAction.Insert: + workspaceEdit.insert(fileUri, edit.start, edit.text); + break; + case EditAction.Replace: + workspaceEdit.replace(fileUri, new Range(edit.start, edit.end), edit.text); + break; + default: + break; + } + }); + }); + }); + + return workspaceEdit; +} +export function getTextEdits(before: string, after: string): TextEdit[] { + // tslint:disable-next-line:no-require-imports + const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); + const d = new dmp.diff_match_patch(); + const diffs = d.diff_main(before, after); + return getTextEditsInternal(before, diffs).map((edit) => edit.apply()); +} +function getTextEditsInternal(before: string, diffs: [number, string][], startLine: number = 0): Edit[] { + let line = startLine; + let character = 0; + const beforeLines = before.split(/\r?\n/g); + if (line > 0) { + beforeLines.filter((_l, i) => i < line).forEach((l) => (character += l.length + NEW_LINE_LENGTH)); + } + const edits: Edit[] = []; + let edit: Edit | null = null; + let end: Position; + + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < diffs.length; i += 1) { + let start = new Position(line, character); + // Compute the line/character after the diff is applied. + // tslint:disable-next-line:prefer-for-of + for (let curr = 0; curr < diffs[i][1].length; curr += 1) { + if (diffs[i][1][curr] !== '\n') { + character += 1; + } else { + character = 0; + line += 1; + } + } + + // tslint:disable-next-line:no-require-imports + const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); + // tslint:disable-next-line:switch-default + switch (diffs[i][0]) { + case dmp.DIFF_DELETE: + if ( + beforeLines[line - 1].length === 0 && + beforeLines[start.line - 1] && + beforeLines[start.line - 1].length === 0 + ) { + // We're asked to delete an empty line which only contains `/\r?\n/g`. The last line is also empty. + // Delete the `\n` from the last line instead of deleting `\n` from the current line + // This change ensures that the last line in the file, which won't contain `\n` is deleted + start = new Position(start.line - 1, 0); + end = new Position(line - 1, 0); + } else { + end = new Position(line, character); + } + if (edit === null) { + edit = new Edit(EditAction.Delete, start); + } else if (edit.action !== EditAction.Delete) { + throw new Error('cannot format due to an internal error.'); + } + edit.end = end; + break; + + case dmp.DIFF_INSERT: + if (edit === null) { + edit = new Edit(EditAction.Insert, start); + } else if (edit.action === EditAction.Delete) { + edit.action = EditAction.Replace; + } + // insert and replace edits are all relative to the original state + // of the document, so inserts should reset the current line/character + // position to the start. + line = start.line; + character = start.character; + edit.text += diffs[i][1]; + break; + + case dmp.DIFF_EQUAL: + if (edit !== null) { + edits.push(edit); + edit = null; + } + break; + } + } + + if (edit !== null) { + edits.push(edit); + } + + return edits; +} + +export async function getTempFileWithDocumentContents(document: TextDocument, fs: IFileSystem): Promise { + // Don't create file in temp folder since external utilities + // look into configuration files in the workspace and are not + // to find custom rules if file is saved in a random disk location. + // This means temp file has to be created in the same folder + // as the original one and then removed. + // Use a .tmp file extension (instead of the original extension) + // because the language server is watching the file system for Python + // file add/delete/change and we don't want this temp file to trigger it. + + // tslint:disable-next-line:no-require-imports + let fileName = `${document.uri.fsPath}.${md5(document.uri.fsPath)}.tmp`; + try { + // When dealing with untitled notebooks, there's no original physical file, hence create a temp file. + if (isNotebookCell(document.uri) && !(await fs.fileExists(document.uri.fsPath))) { + fileName = (await fs.createTemporaryFile(`${path.basename(document.uri.fsPath)}.tmp`)).filePath; + } + await fs.writeFile(fileName, document.getText()); + } catch (ex) { + traceError('Failed to create a temporary file', ex); + throw new WrappedError(`Failed to create a temporary file, ${ex.message}`, ex); + } + return fileName; +} + +/** + * Parse a textual representation of patches and return a list of Patch objects. + * @param {string} textline Text representation of patches. + * @return {!Array.} Array of Patch objects. + * @throws {!Error} If invalid input. + */ +function patch_fromText(textline: string): Patch[] { + const patches: Patch[] = []; + if (!textline) { + return patches; + } + // Start Modification by Don Jayamanne 24/06/2016 Support for CRLF + const text = textline.split(/[\r\n]/); + // End Modification + let textPointer = 0; + const patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/; + while (textPointer < text.length) { + const m = text[textPointer].match(patchHeader); + if (!m) { + throw new Error(`Invalid patch string: ${text[textPointer]}`); + } + // tslint:disable-next-line:no-any + const patch = new (diff_match_patch).patch_obj(); + patches.push(patch); + patch.start1 = parseInt(m[1], 10); + if (m[2] === '') { + patch.start1 -= 1; + patch.length1 = 1; + } else if (m[2] === '0') { + patch.length1 = 0; + } else { + patch.start1 -= 1; + patch.length1 = parseInt(m[2], 10); + } + + patch.start2 = parseInt(m[3], 10); + if (m[4] === '') { + patch.start2 -= 1; + patch.length2 = 1; + } else if (m[4] === '0') { + patch.length2 = 0; + } else { + patch.start2 -= 1; + patch.length2 = parseInt(m[4], 10); + } + textPointer += 1; + // tslint:disable-next-line:no-require-imports + const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); + + while (textPointer < text.length) { + const sign = text[textPointer].charAt(0); + let line: string; + try { + //var line = decodeURI(text[textPointer].substring(1)); + // For some reason the patch generated by python files don't encode any characters + // And this patch module (code from Google) is expecting the text to be encoded!! + // Temporary solution, disable decoding + // Issue #188 + line = text[textPointer].substring(1); + } catch (ex) { + // Malformed URI sequence. + throw new Error('Illegal escape in patch_fromText'); + } + if (sign === '-') { + // Deletion. + patch.diffs.push([dmp.DIFF_DELETE, line]); + } else if (sign === '+') { + // Insertion. + patch.diffs.push([dmp.DIFF_INSERT, line]); + } else if (sign === ' ') { + // Minor equality. + patch.diffs.push([dmp.DIFF_EQUAL, line]); + } else if (sign === '@') { + // Start of next patch. + break; + } else if (sign === '') { + // Blank line? Whatever. + } else { + // WTF? + throw new Error(`Invalid patch mode '${sign}' in: ${line}`); + } + textPointer += 1; + } + } + return patches; +} + +@injectable() +export class EditorUtils implements IEditorUtils { + public getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit { + const workspaceEdit = new WorkspaceEdit(); + if (patch.startsWith('---')) { + // Strip the first two lines + patch = patch.substring(patch.indexOf('@@')); + } + if (patch.length === 0) { + return workspaceEdit; + } + // Remove the text added by unified_diff + // # Work around missing newline (http://bugs.python.org/issue2142). + patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); + + // tslint:disable-next-line:no-require-imports + const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); + const d = new dmp.diff_match_patch(); + const patches = patch_fromText.call(d, patch); + if (!Array.isArray(patches) || patches.length === 0) { + throw new Error('Unable to parse Patch string'); + } + + // Add line feeds and build the text edits + patches.forEach((p) => { + p.diffs.forEach((diff) => { + diff[1] += EOL; + }); + getTextEditsInternal(originalContents, p.diffs, p.start1).forEach((edit) => { + switch (edit.action) { + case EditAction.Delete: + workspaceEdit.delete(uri, new Range(edit.start, edit.end)); + break; + case EditAction.Insert: + workspaceEdit.insert(uri, edit.start, edit.text); + break; + case EditAction.Replace: + workspaceEdit.replace(uri, new Range(edit.start, edit.end), edit.text); + break; + default: + break; + } + }); + }); + + return workspaceEdit; + } +} diff --git a/src/client/common/errors/errorUtils.ts b/src/client/common/errors/errorUtils.ts new file mode 100644 index 000000000000..9364d3a971df --- /dev/null +++ b/src/client/common/errors/errorUtils.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { EOL } from 'os'; + +// tslint:disable-next-line:no-stateless-class no-unnecessary-class +export class ErrorUtils { + public static outputHasModuleNotInstalledError(moduleName: string, content?: string): boolean { + return content && + (content!.indexOf(`No module named ${moduleName}`) > 0 || + content!.indexOf(`No module named '${moduleName}'`) > 0) + ? true + : false; + } +} + +/** + * Wraps an error with a custom error message, retaining the call stack information. + */ +export class WrappedError extends Error { + constructor(message: string, originalException: Error) { + super(message); + // Retain call stack that trapped the error and rethrows this error. + // Also retain the call stack of the original error. + this.stack = `${new Error('').stack}${EOL}${EOL}${originalException.stack}`; + } +} diff --git a/src/client/common/errors/moduleNotInstalledError.ts b/src/client/common/errors/moduleNotInstalledError.ts new file mode 100644 index 000000000000..944f6dfc3e5d --- /dev/null +++ b/src/client/common/errors/moduleNotInstalledError.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export class ModuleNotInstalledError extends Error { + constructor(moduleName: string) { + super(`Module '${moduleName}' not installed.`); + } +} diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts new file mode 100644 index 000000000000..fe7b5b6ed1f2 --- /dev/null +++ b/src/client/common/experiments/groups.ts @@ -0,0 +1,107 @@ +// Experiment to check whether to always display the test explorer. +export enum AlwaysDisplayTestExplorerGroups { + control = 'AlwaysDisplayTestExplorer - control', + experiment = 'AlwaysDisplayTestExplorer - experiment' +} + +// Experiment to check whether to show "Extension Survey prompt" or not. +export enum ShowExtensionSurveyPrompt { + control = 'ShowExtensionSurveyPrompt - control', + enabled = 'ShowExtensionSurveyPrompt - enabled' +} + +// Experiment to check whether to enable re-load for web apps while debugging. +export enum WebAppReload { + control = 'Reload - control', + experiment = 'Reload - experiment' +} + +// Experiment to use a local ZMQ kernel connection as opposed to starting a Jupyter server locally +export enum LocalZMQKernel { + control = 'LocalZMQKernel - control', + experiment = 'LocalZMQKernel - experiment' +} + +// Experiment for supporting run by line in data science notebooks +export enum RunByLine { + control = 'RunByLine - control', + experiment = 'RunByLine - experiment' +} + +/** + * Experiment to check whether to to use a terminal to generate the environment variables of activated environments. + * + * @export + * @enum {number} + */ +export enum UseTerminalToGetActivatedEnvVars { + control = 'UseTerminalToGetActivatedEnvVars - control', + experiment = 'UseTerminalToGetActivatedEnvVars - experiment' +} + +// Dummy experiment added to validate metrics of A/B testing +export enum ValidateABTesting { + control = 'AA_testing - control', + experiment = 'AA_testing - experiment' +} + +// Collect language server request timings. +export enum CollectLSRequestTiming { + control = 'CollectLSRequestTiming - control', + experiment = 'CollectLSRequestTiming - experiment' +} + +// Collect Node language server request timings. +export enum CollectNodeLSRequestTiming { + control = 'CollectNodeLSRequestTiming - control', + experiment = 'CollectNodeLSRequestTiming - experiment' +} + +// Determine if ipywidgets is enabled or not +export enum EnableIPyWidgets { + control = 'EnableIPyWidgets - control', + experiment = 'EnableIPyWidgets - experiment' +} + +/* + * Experiment to check whether the extension should deprecate `python.pythonPath` setting + */ +export enum DeprecatePythonPath { + control = 'DeprecatePythonPath - control', + experiment = 'DeprecatePythonPath - experiment' +} + +/* + * Experiment to turn on custom editor or VS Code Native Notebook API support. + */ +export enum NotebookEditorSupport { + control = 'CustomEditorSupport - control', + customEditorExperiment = 'CustomEditorSupport - experiment', + nativeNotebookExperiment = 'NativeNotebook - experiment' +} + +// Experiment to remove the Kernel/Server Tooblar in the Interactive Window when running a local Jupyter Server. +// It doesn't make sense to have it there, the user can already change the kernel +// by changing the python interpreter on the status bar. +export enum RemoveKernelToolbarInInteractiveWindow { + experiment = 'RemoveKernelToolbarInInteractiveWindow' +} + +// Experiment to offer switch to Pylance language server +export enum TryPylance { + experiment = 'tryPylance' +} + +// Experiment for the content of the tip being displayed on first extension launch: +// interpreter selection tip, feedback survey or nothing. +export enum SurveyAndInterpreterTipNotification { + tipExperiment = 'pythonTipPromptWording', + surveyExperiment = 'pythonMailingListPromptWording' +} + +// Experiment to show a prompt asking users to join python mailing list. +export enum JoinMailingListPromptVariants { + variant1 = 'pythonJoinMailingListVar1', + variant2 = 'pythonJoinMailingListVar2', + variant3 = 'pythonJoinMailingListVar3' +} diff --git a/src/client/common/experiments/manager.ts b/src/client/common/experiments/manager.ts new file mode 100644 index 000000000000..6dbe80a8d63e --- /dev/null +++ b/src/client/common/experiments/manager.ts @@ -0,0 +1,371 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Refer to A/B testing wiki for more details: https://en.wikipedia.org/wiki/A/B_testing + +'use strict'; + +import { inject, injectable, named, optional } from 'inversify'; +import { parse } from 'jsonc-parser'; +import * as path from 'path'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IApplicationEnvironment } from '../application/types'; +import { EXTENSION_ROOT_DIR, STANDARD_OUTPUT_CHANNEL } from '../constants'; +import { traceDecorators, traceError } from '../logger'; +import { IFileSystem } from '../platform/types'; +import { + ABExperiments, + IConfigurationService, + ICryptoUtils, + IExperimentsManager, + IHttpClient, + IOutputChannel, + IPersistentState, + IPersistentStateFactory, + IPythonSettings +} from '../types'; +import { sleep } from '../utils/async'; +import { swallowExceptions } from '../utils/decorators'; +import { Experiments } from '../utils/localize'; +import { NotebookEditorSupport } from './groups'; + +const EXPIRY_DURATION_MS = 30 * 60 * 1000; +export const isDownloadedStorageValidKey = 'IS_EXPERIMENTS_STORAGE_VALID_KEY'; +export const experimentStorageKey = 'EXPERIMENT_STORAGE_KEY'; +export const downloadedExperimentStorageKey = 'DOWNLOADED_EXPERIMENTS_STORAGE_KEY'; +/** + * Local experiments config file. We have this to ensure that experiments are used in the first session itself, + * as about 40% of the users never come back for the second session. + */ +const configFile = path.join(EXTENSION_ROOT_DIR, 'experiments.json'); +export const configUri = 'https://raw.githubusercontent.com/microsoft/vscode-python/main/experiments.json'; +export const EXPERIMENTS_EFFORT_TIMEOUT_MS = 2000; +// The old experiments which are working fine using the `SHA512` algorithm +export const oldExperimentSalts = ['ShowExtensionSurveyPrompt', 'ShowPlayIcon', 'AlwaysDisplayTestExplorer', 'LS']; + +/** + * Manages and stores experiments, implements the AB testing functionality + */ +@injectable() +export class ExperimentsManager implements IExperimentsManager { + /** + * Keeps track of the list of experiments user is in + */ + public userExperiments: ABExperiments = []; + /** + * Experiments user requested to opt into manually + */ + public _experimentsOptedInto: string[] = []; + /** + * Experiments user requested to opt out from manually + */ + public _experimentsOptedOutFrom: string[] = []; + /** + * Returns `true` if experiments are enabled, else `false`. + */ + public _enabled: boolean = true; + /** + * Keeps track of the experiments to be used in the current session + */ + private experimentStorage: IPersistentState; + /** + * Keeps track of the downloaded experiments in the current session, to be used in the next startup + * Note experiments downloaded in the current session has to be distinguished + * from the experiments download in the previous session (experimentsStorage contains that), reason being the following + * + * THE REASON TO WHY WE NEED TWO STATE STORES USED TO STORE EXPERIMENTS: + * We do not intend to change experiments mid-session. To implement this, we should make sure that we do not replace + * the experiments used in the current session by the newly downloaded experiments. That's why we have a separate + * storage(downloadedExperimentsStorage) to store experiments downloaded in the current session. + * Function updateExperimentStorage() makes sure these are used in the next session. + */ + private downloadedExperimentsStorage: IPersistentState; + /** + * Keeps track if the storage needs updating or not. + * Note this has to be separate from the actual storage as + * download storages by itself should not have an Expiry (so that it can be used in the next session even when download fails in the current session) + */ + private isDownloadedStorageValid: IPersistentState; + private activatedOnce: boolean = false; + private settings!: IPythonSettings; + constructor( + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IHttpClient) private readonly httpClient: IHttpClient, + @inject(ICryptoUtils) private readonly crypto: ICryptoUtils, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly output: IOutputChannel, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @optional() private experimentEffortTimeout: number = EXPERIMENTS_EFFORT_TIMEOUT_MS + ) { + this.isDownloadedStorageValid = this.persistentStateFactory.createGlobalPersistentState( + isDownloadedStorageValidKey, + false, + EXPIRY_DURATION_MS + ); + this.experimentStorage = this.persistentStateFactory.createGlobalPersistentState( + experimentStorageKey, + undefined + ); + this.downloadedExperimentsStorage = this.persistentStateFactory.createGlobalPersistentState< + ABExperiments | undefined + >(downloadedExperimentStorageKey, undefined); + } + + @swallowExceptions('Failed to activate experiments') + public async activate(): Promise { + if (this.activatedOnce) { + return; + } + this.activatedOnce = true; + this.settings = this.configurationService.getSettings(undefined); + this._experimentsOptedInto = this.settings.experiments.optInto; + this._experimentsOptedOutFrom = this.settings.experiments.optOutFrom; + this._enabled = this.settings.experiments.enabled; + if (!this._enabled) { + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_DISABLED); + return; + } + await this.updateExperimentStorage(); + this.populateUserExperiments(); + for (const exp of this.userExperiments || []) { + // We need to know whether an experiment influences the logs we observe in github issues, so log the experiment group + // tslint:disable-next-line: no-console + this.output.appendLine(Experiments.inGroup().format(exp.name)); + } + this.initializeInBackground().ignoreErrors(); + } + + @traceDecorators.error('Failed to identify if user is in experiment') + public inExperiment(experimentName: string): boolean { + if (!this._enabled) { + return false; + } + this.sendTelemetryIfInExperiment(experimentName); + return this.userExperiments.find((exp) => exp.name === experimentName) ? true : false; + } + + /** + * Populates list of experiments user is in + */ + @traceDecorators.error('Failed to populate user experiments') + public populateUserExperiments(): void { + this.cleanUpExperimentsOptList(); + if (Array.isArray(this.experimentStorage.value)) { + const remainingExpriments: ABExperiments = []; + // First process experiments in order of user preference (if they have opted out or opted in). + for (const experiment of this.experimentStorage.value) { + // User cannot belong to NotebookExperiment if they are not using Insiders. + if ( + experiment.name === NotebookEditorSupport.nativeNotebookExperiment && + this.appEnvironment.channel === 'stable' + ) { + continue; + } + // User cannot belong to CustomEditor Experiment if they are not using Insiders. + if ( + experiment.name === NotebookEditorSupport.customEditorExperiment && + this.appEnvironment.channel === 'stable' + ) { + continue; + } + try { + if ( + this._experimentsOptedOutFrom.includes('All') || + this._experimentsOptedOutFrom.includes(experiment.name) + ) { + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT, undefined, { + expNameOptedOutOf: experiment.name + }); + continue; + } + if ( + this._experimentsOptedInto.includes('All') || + this._experimentsOptedInto.includes(experiment.name) + ) { + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT, undefined, { + expNameOptedInto: experiment.name + }); + this.userExperiments.push(experiment); + } else { + remainingExpriments.push(experiment); + } + } catch (ex) { + traceError(`Failed to populate experiment list for experiment '${experiment.name}'`, ex); + } + } + + // Add users (based on algorithm) to experiments they haven't already opted out of or opted into. + remainingExpriments + .filter((experiment) => this.isUserInRange(experiment.min, experiment.max, experiment.salt)) + .filter((experiment) => !this.userExperiments.some((existing) => existing.salt === experiment.salt)) + .forEach((experiment) => this.userExperiments.push(experiment)); + } + } + + @traceDecorators.error('Failed to send telemetry when user is in experiment') + public sendTelemetryIfInExperiment(experimentName: string): void { + if (this.userExperiments.find((exp) => exp.name === experimentName)) { + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS, undefined, { expName: experimentName }); + } + } + + /** + * Downloads experiments and updates downloaded storage for the next session given previously downloaded experiments are no longer valid + */ + @traceDecorators.error('Failed to initialize experiments') + public async initializeInBackground(): Promise { + if (this.isDownloadedStorageValid.value) { + return; + } + await this.downloadAndStoreExperiments(); + } + + /** + * Downloads experiments and updates storage + * @param storage The storage to store the experiments in. By default, downloaded storage for the next session is used. + */ + @traceDecorators.error('Failed to download and store experiments') + public async downloadAndStoreExperiments( + storage: IPersistentState = this.downloadedExperimentsStorage + ): Promise { + const downloadedExperiments = await this.httpClient.getJSON(configUri, false); + if (!this.areExperimentsValid(downloadedExperiments)) { + return; + } + await storage.updateValue(downloadedExperiments); + await this.isDownloadedStorageValid.updateValue(true); + } + + /** + * Checks if user falls between the range of the experiment + * @param min The lower limit + * @param max The upper limit + * @param salt The experiment salt value + */ + public isUserInRange(min: number, max: number, salt: string): boolean { + if (typeof this.appEnvironment.machineId !== 'string') { + throw new Error('Machine ID should be a string'); + } + let hash: number; + if (oldExperimentSalts.find((oldSalt) => oldSalt === salt)) { + hash = this.crypto.createHash(`${this.appEnvironment.machineId}+${salt}`, 'number', 'SHA512'); + } else { + hash = this.crypto.createHash(`${this.appEnvironment.machineId}+${salt}`, 'number', 'FNV'); + } + return hash % 100 >= min && hash % 100 < max; + } + + /** + * Do best effort to populate experiment storage. Attempt to update experiment storage by, + * * Using appropriate local data if available + * * Trying to download fresh experiments within 2 seconds to update storage + * Local data could be: + * * Experiments downloaded in the last session + * - The function makes sure these are used in the current session + * * A default experiments file shipped with the extension + * - Note this file is only used when experiment storage is empty, which is usually the case the first time the extension loads. + * - We have this local file to ensure that experiments are used in the first session itself, + * as about 40% of the users never come back for the second session. + */ + @swallowExceptions('Failed to update experiment storage') + public async updateExperimentStorage(): Promise { + if (!process.env.VSC_PYTHON_LOAD_EXPERIMENTS_FROM_FILE) { + // Step 1. Update experiment storage using downloaded experiments in the last session if any + if (Array.isArray(this.downloadedExperimentsStorage.value)) { + await this.experimentStorage.updateValue(this.downloadedExperimentsStorage.value); + return this.downloadedExperimentsStorage.updateValue(undefined); + } + + if (Array.isArray(this.experimentStorage.value)) { + // Experiment storage already contains latest experiments, do not use the following techniques + return; + } + + // Step 2. Do best effort to download the experiments within timeout and use it in the current session only + if ((await this.doBestEffortToPopulateExperiments()) === true) { + return; + } + } + + // Step 3. Update experiment storage using local experiments file if available + if (await this.fs.fileExists(configFile)) { + const content = await this.fs.readFile(configFile); + try { + const experiments = parse(content, [], { allowTrailingComma: true, disallowComments: false }); + if (!this.areExperimentsValid(experiments)) { + throw new Error('Parsed experiments are not valid'); + } + await this.experimentStorage.updateValue(experiments); + } catch (ex) { + traceError('Failed to parse experiments configuration file to update storage', ex); + } + } + } + + /** + * Checks that experiments are not invalid or incomplete + * @param experiments Local or downloaded experiments + * @returns `true` if type of experiments equals `ABExperiments` type, `false` otherwise + */ + public areExperimentsValid(experiments: ABExperiments): boolean { + if (!Array.isArray(experiments)) { + traceError('Experiments are not of array type'); + return false; + } + for (const exp of experiments) { + if (exp.name === undefined || exp.salt === undefined || exp.min === undefined || exp.max === undefined) { + traceError('Experiments are missing fields from ABExperiments type'); + return false; + } + } + return true; + } + + /** + * Do best effort to download the experiments within timeout and use it in the current session only + */ + public async doBestEffortToPopulateExperiments(): Promise { + try { + const success = await Promise.race([ + // Download and store experiments in the storage for the current session + this.downloadAndStoreExperiments(this.experimentStorage).then(() => true), + sleep(this.experimentEffortTimeout).then(() => false) + ]); + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_DOWNLOAD_SUCCESS_RATE, undefined, { success }); + return success; + } catch (ex) { + sendTelemetryEvent( + EventName.PYTHON_EXPERIMENTS_DOWNLOAD_SUCCESS_RATE, + undefined, + { success: false, error: 'Downloading experiments failed with error' }, + ex + ); + traceError('Effort to download experiments within timeout failed with error', ex); + return false; + } + } + + public _activated(): boolean { + return this.activatedOnce; + } + + /** + * You can only opt in or out of experiment groups, not control groups. So remove requests for control groups. + */ + private cleanUpExperimentsOptList(): void { + for (let i = 0; i < this._experimentsOptedInto.length; i += 1) { + if (this._experimentsOptedInto[i].endsWith('control')) { + this._experimentsOptedInto[i] = ''; + } + } + for (let i = 0; i < this._experimentsOptedOutFrom.length; i += 1) { + if (this._experimentsOptedOutFrom[i].endsWith('control')) { + this._experimentsOptedOutFrom[i] = ''; + } + } + this._experimentsOptedInto = this._experimentsOptedInto.filter((exp) => exp !== ''); + this._experimentsOptedOutFrom = this._experimentsOptedOutFrom.filter((exp) => exp !== ''); + } +} diff --git a/src/client/common/experiments/service.ts b/src/client/common/experiments/service.ts new file mode 100644 index 000000000000..b318732dacde --- /dev/null +++ b/src/client/common/experiments/service.ts @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Memento } from 'vscode'; +import { getExperimentationService, IExperimentationService, TargetPopulation } from 'vscode-tas-client'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IApplicationEnvironment } from '../application/types'; +import { PVSC_EXTENSION_ID, STANDARD_OUTPUT_CHANNEL } from '../constants'; +import { + GLOBAL_MEMENTO, + IConfigurationService, + IExperimentService, + IMemento, + IOutputChannel, + IPythonSettings +} from '../types'; +import { Experiments } from '../utils/localize'; +import { ExperimentationTelemetry } from './telemetry'; + +const EXP_MEMENTO_KEY = 'VSCode.ABExp.FeatureData'; + +@injectable() +export class ExperimentService implements IExperimentService { + /** + * Experiments the user requested to opt into manually. + */ + public _optInto: string[] = []; + /** + * Experiments the user requested to opt out from manually. + */ + public _optOutFrom: string[] = []; + + private readonly experimentationService?: IExperimentationService; + private readonly settings: IPythonSettings; + + constructor( + @inject(IConfigurationService) readonly configurationService: IConfigurationService, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + @inject(IMemento) @named(GLOBAL_MEMENTO) private readonly globalState: Memento, + @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly output: IOutputChannel + ) { + this.settings = configurationService.getSettings(undefined); + + // Users can only opt in or out of experiment groups, not control groups. + const optInto = this.settings.experiments.optInto; + const optOutFrom = this.settings.experiments.optOutFrom; + this._optInto = optInto.filter((exp) => !exp.endsWith('control')); + this._optOutFrom = optOutFrom.filter((exp) => !exp.endsWith('control')); + + // Don't initialize the experiment service if the extension's experiments setting is disabled. + const enabled = this.settings.experiments.enabled; + if (!enabled) { + return; + } + + let targetPopulation: TargetPopulation; + + if (this.appEnvironment.extensionChannel === 'insiders') { + targetPopulation = TargetPopulation.Insiders; + } else { + targetPopulation = TargetPopulation.Public; + } + + const telemetryReporter = new ExperimentationTelemetry(); + + this.experimentationService = getExperimentationService( + PVSC_EXTENSION_ID, + this.appEnvironment.packageJson.version!, + targetPopulation, + telemetryReporter, + this.globalState + ); + + this.logExperiments(); + } + + public async inExperiment(experiment: string): Promise { + if (!this.experimentationService) { + return false; + } + + // Currently the service doesn't support opting in and out of experiments, + // so we need to perform these checks and send the corresponding telemetry manually. + if (this._optOutFrom.includes('All') || this._optOutFrom.includes(experiment)) { + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT, undefined, { + expNameOptedOutOf: experiment + }); + + return false; + } + + if (this._optInto.includes('All') || this._optInto.includes(experiment)) { + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT, undefined, { + expNameOptedInto: experiment + }); + + return true; + } + + return this.experimentationService.isCachedFlightEnabled(experiment); + } + + public async getExperimentValue(experiment: string): Promise { + if (!this.experimentationService || this._optOutFrom.includes('All') || this._optOutFrom.includes(experiment)) { + return; + } + + return this.experimentationService.getTreatmentVariableAsync('vscode', experiment); + } + + private logExperiments() { + const experiments = this.globalState.get<{ features: string[] }>(EXP_MEMENTO_KEY, { features: [] }); + + experiments.features.forEach((exp) => { + // Filter out experiments groups that are not from the Python extension. + if (exp.toLowerCase().startsWith('python')) { + this.output.appendLine(Experiments.inGroup().format(exp)); + } + }); + } +} diff --git a/src/client/common/experiments/telemetry.ts b/src/client/common/experiments/telemetry.ts new file mode 100644 index 000000000000..8e0ace9f7a42 --- /dev/null +++ b/src/client/common/experiments/telemetry.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IExperimentationTelemetry } from 'vscode-tas-client'; +import { sendTelemetryEvent, setSharedProperty } from '../../telemetry'; + +export class ExperimentationTelemetry implements IExperimentationTelemetry { + public setSharedProperty(name: string, value: string): void { + // Add the shared property to all telemetry being sent, not just events being sent by the experimentation package. + // We are not in control of these props, just cast to `any`, i.e. we cannot strongly type these external props. + // tslint:disable-next-line: no-any + setSharedProperty(name as any, value as any); + } + + public postEvent(eventName: string, properties: Map): void { + const formattedProperties: { [key: string]: string } = {}; + properties.forEach((value, key) => { + formattedProperties[key] = value; + }); + + // tslint:disable-next-line: no-any + sendTelemetryEvent(eventName as any, undefined, formattedProperties); + } +} diff --git a/src/client/common/extensions.ts b/src/client/common/extensions.ts new file mode 100644 index 000000000000..a41675749ab9 --- /dev/null +++ b/src/client/common/extensions.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * @typedef {Object} SplitLinesOptions + * @property {boolean} [trim=true] - Whether to trim the lines. + * @property {boolean} [removeEmptyEntries=true] - Whether to remove empty entries. + */ + +// https://stackoverflow.com/questions/39877156/how-to-extend-string-prototype-and-use-it-next-in-typescript +// tslint:disable-next-line:interface-name +declare interface String { + /** + * Split a string using the cr and lf characters and return them as an array. + * By default lines are trimmed and empty lines are removed. + * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. + */ + splitLines(splitOptions?: { trim: boolean; removeEmptyEntries?: boolean }): string[]; + /** + * Appropriately formats a string so it can be used as an argument for a command in a shell. + * E.g. if an argument contains a space, then it will be enclosed within double quotes. + */ + toCommandArgument(): string; + /** + * Appropriately formats a a file path so it can be used as an argument for a command in a shell. + * E.g. if an argument contains a space, then it will be enclosed within double quotes. + */ + fileToCommandArgument(): string; + /** + * String.format() implementation. + * Tokens such as {0}, {1} will be replaced with corresponding positional arguments. + */ + format(...args: string[]): string; + + /** + * String.trimQuotes implementation + * Removes leading and trailing quotes from a string + */ + trimQuotes(): string; +} + +/** + * Split a string using the cr and lf characters and return them as an array. + * By default lines are trimmed and empty lines are removed. + * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. + */ +String.prototype.splitLines = function ( + this: string, + splitOptions: { trim: boolean; removeEmptyEntries: boolean } = { removeEmptyEntries: true, trim: true } +): string[] { + let lines = this.split(/\r?\n/g); + if (splitOptions && splitOptions.trim) { + lines = lines.map((line) => line.trim()); + } + if (splitOptions && splitOptions.removeEmptyEntries) { + lines = lines.filter((line) => line.length > 0); + } + return lines; +}; + +/** + * Appropriately formats a string so it can be used as an argument for a command in a shell. + * E.g. if an argument contains a space, then it will be enclosed within double quotes. + * @param {String} value. + */ +String.prototype.toCommandArgument = function (this: string): string { + if (!this) { + return this; + } + return this.indexOf(' ') >= 0 && !this.startsWith('"') && !this.endsWith('"') ? `"${this}"` : this.toString(); +}; + +/** + * Appropriately formats a a file path so it can be used as an argument for a command in a shell. + * E.g. if an argument contains a space, then it will be enclosed within double quotes. + */ +String.prototype.fileToCommandArgument = function (this: string): string { + if (!this) { + return this; + } + return this.toCommandArgument().replace(/\\/g, '/'); +}; + +/** + * String.trimQuotes implementation + * Removes leading and trailing quotes from a string + */ +String.prototype.trimQuotes = function (this: string): string { + if (!this) { + return this; + } + return this.replace(/(^['"])|(['"]$)/g, ''); +}; + +// tslint:disable-next-line:interface-name +declare interface Promise { + /** + * Catches task error and ignores them. + */ + ignoreErrors(): void; +} + +/** + * Explicitly tells that promise should be run asynchonously. + */ +Promise.prototype.ignoreErrors = function (this: Promise) { + // tslint:disable-next-line:no-empty + this.catch(() => {}); +}; + +if (!String.prototype.format) { + String.prototype.format = function (this: string) { + const args = arguments; + return this.replace(/{(\d+)}/g, (match, number) => (args[number] === undefined ? match : args[number])); + }; +} diff --git a/src/client/common/featureDeprecationManager.ts b/src/client/common/featureDeprecationManager.ts new file mode 100644 index 000000000000..79a4af0cd566 --- /dev/null +++ b/src/client/common/featureDeprecationManager.ts @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Disposable, WorkspaceConfiguration } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from './application/types'; +import { traceVerbose } from './logger'; +import { launch } from './net/browser'; +import { + DeprecatedFeatureInfo, + DeprecatedSettingAndValue, + IFeatureDeprecationManager, + IPersistentStateFactory +} from './types'; + +const deprecatedFeatures: DeprecatedFeatureInfo[] = [ + { + doNotDisplayPromptStateKey: 'SHOW_DEPRECATED_FEATURE_PROMPT_FORMAT_ON_SAVE', + message: "The setting 'python.formatting.formatOnSave' is deprecated, please use 'editor.formatOnSave'.", + moreInfoUrl: 'https://github.com/Microsoft/vscode-python/issues/309', + setting: { setting: 'formatting.formatOnSave', values: ['true', true] } + }, + { + doNotDisplayPromptStateKey: 'SHOW_DEPRECATED_FEATURE_PROMPT_LINT_ON_TEXT_CHANGE', + message: + "The setting 'python.linting.lintOnTextChange' is deprecated, please enable 'python.linting.lintOnSave' and 'files.autoSave'.", + moreInfoUrl: 'https://github.com/Microsoft/vscode-python/issues/313', + setting: { setting: 'linting.lintOnTextChange', values: ['true', true] } + }, + { + doNotDisplayPromptStateKey: 'SHOW_DEPRECATED_FEATURE_PROMPT_FOR_AUTO_COMPLETE_PRELOAD_MODULES', + message: + "The setting 'python.autoComplete.preloadModules' is deprecated, please consider using Pylance Language Server ('python.languageServer' setting).", + moreInfoUrl: 'https://github.com/Microsoft/vscode-python/issues/1704', + setting: { setting: 'autoComplete.preloadModules' } + } +]; + +@injectable() +export class FeatureDeprecationManager implements IFeatureDeprecationManager { + private disposables: Disposable[] = []; + constructor( + @inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory, + @inject(ICommandManager) private cmdMgr: ICommandManager, + @inject(IWorkspaceService) private workspace: IWorkspaceService, + @inject(IApplicationShell) private appShell: IApplicationShell + ) {} + + public dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + + public initialize() { + deprecatedFeatures.forEach(this.registerDeprecation.bind(this)); + } + + public registerDeprecation(deprecatedInfo: DeprecatedFeatureInfo): void { + if (Array.isArray(deprecatedInfo.commands)) { + deprecatedInfo.commands.forEach((cmd) => { + this.disposables.push( + this.cmdMgr.registerCommand(cmd, () => this.notifyDeprecation(deprecatedInfo), this) + ); + }); + } + if (deprecatedInfo.setting) { + this.checkAndNotifyDeprecatedSetting(deprecatedInfo); + } + } + + public async notifyDeprecation(deprecatedInfo: DeprecatedFeatureInfo): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + deprecatedInfo.doNotDisplayPromptStateKey, + true + ); + if (!notificationPromptEnabled.value) { + return; + } + const moreInfo = 'Learn more'; + const doNotShowAgain = 'Never show again'; + const option = await this.appShell.showInformationMessage(deprecatedInfo.message, moreInfo, doNotShowAgain); + if (!option) { + return; + } + switch (option) { + case moreInfo: { + launch(deprecatedInfo.moreInfoUrl); + break; + } + case doNotShowAgain: { + await notificationPromptEnabled.updateValue(false); + break; + } + default: { + throw new Error('Selected option not supported.'); + } + } + return; + } + + public checkAndNotifyDeprecatedSetting(deprecatedInfo: DeprecatedFeatureInfo) { + let notify = false; + if (Array.isArray(this.workspace.workspaceFolders) && this.workspace.workspaceFolders.length > 0) { + this.workspace.workspaceFolders.forEach((workspaceFolder) => { + if (notify) { + return; + } + notify = this.isDeprecatedSettingAndValueUsed( + this.workspace.getConfiguration('python', workspaceFolder.uri), + deprecatedInfo.setting! + ); + }); + } else { + notify = this.isDeprecatedSettingAndValueUsed( + this.workspace.getConfiguration('python'), + deprecatedInfo.setting! + ); + } + + if (notify) { + this.notifyDeprecation(deprecatedInfo).catch((ex) => + traceVerbose('Python Extension: notifyDeprecation', ex) + ); + } + } + + public isDeprecatedSettingAndValueUsed( + pythonConfig: WorkspaceConfiguration, + deprecatedSetting: DeprecatedSettingAndValue + ) { + if (!pythonConfig.has(deprecatedSetting.setting)) { + return false; + } + const configValue = pythonConfig.get(deprecatedSetting.setting); + if (!Array.isArray(deprecatedSetting.values) || deprecatedSetting.values.length === 0) { + if (Array.isArray(configValue)) { + return configValue.length > 0; + } + return true; + } + if (!Array.isArray(deprecatedSetting.values) || deprecatedSetting.values.length === 0) { + if (configValue === undefined) { + return false; + } + if (Array.isArray(configValue)) { + // tslint:disable-next-line:no-any + return (configValue as any[]).length > 0; + } + // If we have a value in the setting, then return. + return true; + } + return deprecatedSetting.values.indexOf(pythonConfig.get<{}>(deprecatedSetting.setting)!) >= 0; + } +} diff --git a/src/client/common/helpers.ts b/src/client/common/helpers.ts new file mode 100644 index 000000000000..2fd69900bd32 --- /dev/null +++ b/src/client/common/helpers.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { isTestExecution } from './constants'; +import { ModuleNotInstalledError } from './errors/moduleNotInstalledError'; + +export function isNotInstalledError(error: Error): boolean { + const isError = typeof error === 'object' && error !== null; + // tslint:disable-next-line:no-any + const errorObj = error; + if (!isError) { + return false; + } + if (error instanceof ModuleNotInstalledError) { + return true; + } + + const isModuleNoInstalledError = error.message.indexOf('No module named') >= 0; + return errorObj.code === 'ENOENT' || errorObj.code === 127 || isModuleNoInstalledError; +} + +export function skipIfTest(isAsyncFunction: boolean) { + // tslint:disable-next-line:no-function-expression no-any + return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value; + // tslint:disable-next-line:no-function-expression no-any + descriptor.value = function (...args: any[]) { + if (isTestExecution()) { + return isAsyncFunction ? Promise.resolve() : undefined; + } + // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any + return originalMethod.apply(this, args); + }; + + return descriptor; + }; +} diff --git a/src/client/common/insidersBuild/downloadChannelRules.ts b/src/client/common/insidersBuild/downloadChannelRules.ts new file mode 100644 index 000000000000..90379a37f99b --- /dev/null +++ b/src/client/common/insidersBuild/downloadChannelRules.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { traceDecorators } from '../logger'; +import { IPersistentStateFactory } from '../types'; +import { IExtensionChannelRule } from './types'; + +export const frequencyForDailyInsidersCheck = 1000 * 60 * 60 * 24; // One day. +export const frequencyForWeeklyInsidersCheck = 1000 * 60 * 60 * 24 * 7; // One week. +export const lastLookUpTimeKey = 'INSIDERS_LAST_LOOK_UP_TIME_KEY'; + +/** + * Determines if we should install insiders when install channel is set of "off". + * "off" setting is defined as a no op, which means we should not be looking for insiders. + * + * @export + * @class ExtensionInsidersOffChannelRule + * @implements {IExtensionChannelRule} + */ +@injectable() +export class ExtensionInsidersOffChannelRule implements IExtensionChannelRule { + public async shouldLookForInsidersBuild(): Promise { + return false; + } +} +@injectable() +export class ExtensionInsidersDailyChannelRule implements IExtensionChannelRule { + constructor(@inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory) {} + @traceDecorators.error('Error in checking if insiders build is to be for daily channel rule') + public async shouldLookForInsidersBuild(isChannelRuleNew: boolean): Promise { + const lastLookUpTime = this.persistentStateFactory.createGlobalPersistentState(lastLookUpTimeKey, -1); + if (isChannelRuleNew) { + // Channel rule has changed to insiders, look for insiders build + await lastLookUpTime.updateValue(Date.now()); + return true; + } + // If we have not looked for it in the last 24 hours, then look. + if (lastLookUpTime.value === -1 || lastLookUpTime.value + frequencyForDailyInsidersCheck < Date.now()) { + await lastLookUpTime.updateValue(Date.now()); + return true; + } + return false; + } +} +@injectable() +export class ExtensionInsidersWeeklyChannelRule implements IExtensionChannelRule { + constructor(@inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory) {} + @traceDecorators.error('Error in checking if insiders build is to be for daily channel rule') + public async shouldLookForInsidersBuild(isChannelRuleNew: boolean): Promise { + const lastLookUpTime = this.persistentStateFactory.createGlobalPersistentState(lastLookUpTimeKey, -1); + if (isChannelRuleNew) { + // Channel rule has changed to insiders, look for insiders build + await lastLookUpTime.updateValue(Date.now()); + return true; + } + // If we have not looked for it in the last week, then look. + if (lastLookUpTime.value === -1 || lastLookUpTime.value + frequencyForWeeklyInsidersCheck < Date.now()) { + await lastLookUpTime.updateValue(Date.now()); + return true; + } + return false; + } +} diff --git a/src/client/common/insidersBuild/downloadChannelService.ts b/src/client/common/insidersBuild/downloadChannelService.ts new file mode 100644 index 000000000000..c0096c080045 --- /dev/null +++ b/src/client/common/insidersBuild/downloadChannelService.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, ConfigurationTarget, Event, EventEmitter } from 'vscode'; +import { IWorkspaceService } from '../application/types'; +import { traceDecorators } from '../logger'; +import { IConfigurationService, IDisposable, IDisposableRegistry, IPythonSettings } from '../types'; +import { ExtensionChannels, IExtensionChannelService } from './types'; + +export const insidersChannelSetting: keyof IPythonSettings = 'insidersChannel'; + +@injectable() +export class ExtensionChannelService implements IExtensionChannelService { + public _onDidChannelChange: EventEmitter = new EventEmitter(); + constructor( + @inject(IConfigurationService) private readonly configService: IConfigurationService, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IDisposableRegistry) disposables: IDisposable[] + ) { + disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + } + public getChannel(): ExtensionChannels { + const settings = this.configService.getSettings(); + return settings.insidersChannel; + } + + public get isChannelUsingDefaultConfiguration(): boolean { + const settings = this.workspaceService + .getConfiguration('python') + .inspect(insidersChannelSetting); + if (!settings) { + throw new Error( + `WorkspaceConfiguration.inspect returns 'undefined' for setting 'python.${insidersChannelSetting}'` + ); + } + return !settings.globalValue; + } + + @traceDecorators.error('Updating channel failed') + public async updateChannel(value: ExtensionChannels): Promise { + await this.configService.updateSetting(insidersChannelSetting, value, undefined, ConfigurationTarget.Global); + } + + public get onDidChannelChange(): Event { + return this._onDidChannelChange.event; + } + + public async onDidChangeConfiguration(event: ConfigurationChangeEvent) { + if (event.affectsConfiguration(`python.${insidersChannelSetting}`)) { + const settings = this.configService.getSettings(); + this._onDidChannelChange.fire(settings.insidersChannel); + } + } +} diff --git a/src/client/common/insidersBuild/insidersExtensionPrompt.ts b/src/client/common/insidersBuild/insidersExtensionPrompt.ts new file mode 100644 index 000000000000..c5fa8200d45b --- /dev/null +++ b/src/client/common/insidersBuild/insidersExtensionPrompt.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IApplicationShell, ICommandManager } from '../application/types'; +import { traceDecorators } from '../logger'; +import { IPersistentState, IPersistentStateFactory } from '../types'; +import { Common, DataScienceSurveyBanner, ExtensionChannels } from '../utils/localize'; +import { noop } from '../utils/misc'; +import { IExtensionChannelService, IInsiderExtensionPrompt } from './types'; + +export const insidersPromptStateKey = 'INSIDERS_PROMPT_STATE_KEY'; + +@injectable() +export class InsidersExtensionPrompt implements IInsiderExtensionPrompt { + public readonly hasUserBeenNotified: IPersistentState; + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IExtensionChannelService) private readonly insidersDownloadChannelService: IExtensionChannelService, + @inject(ICommandManager) private readonly cmdManager: ICommandManager, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory + ) { + this.hasUserBeenNotified = this.persistentStateFactory.createGlobalPersistentState( + insidersPromptStateKey, + false + ); + } + + @traceDecorators.error('Error in prompting to install insiders') + public async promptToInstallInsiders(): Promise { + const prompts = [ + ExtensionChannels.yesWeekly(), + ExtensionChannels.yesDaily(), + DataScienceSurveyBanner.bannerLabelNo() + ]; + const telemetrySelections: ['Yes, weekly', 'Yes, daily', 'No, thanks'] = [ + 'Yes, weekly', + 'Yes, daily', + 'No, thanks' + ]; + const selection = await this.appShell.showInformationMessage(ExtensionChannels.promptMessage(), ...prompts); + + await this.hasUserBeenNotified.updateValue(true); + sendTelemetryEvent(EventName.INSIDERS_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined + }); + + if (!selection) { + return; + } + if (selection === ExtensionChannels.yesWeekly()) { + await this.insidersDownloadChannelService.updateChannel('weekly'); + } else if (selection === ExtensionChannels.yesDaily()) { + await this.insidersDownloadChannelService.updateChannel('daily'); + } + } + + @traceDecorators.error('Error in prompting to reload') + public async promptToReload(): Promise { + const selection = await this.appShell.showInformationMessage( + ExtensionChannels.reloadToUseInsidersMessage(), + Common.reload() + ); + sendTelemetryEvent(EventName.INSIDERS_RELOAD_PROMPT, undefined, { + selection: selection ? 'Reload' : undefined + }); + if (selection === Common.reload()) { + this.cmdManager.executeCommand('workbench.action.reloadWindow').then(noop); + } + } +} diff --git a/src/client/common/insidersBuild/insidersExtensionService.ts b/src/client/common/insidersBuild/insidersExtensionService.ts new file mode 100644 index 000000000000..f7896cd3e4f7 --- /dev/null +++ b/src/client/common/insidersBuild/insidersExtensionService.ts @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import '../extensions'; + +import { inject, injectable, named } from 'inversify'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IApplicationEnvironment, ICommandManager } from '../application/types'; +import { Commands } from '../constants'; +import { IExtensionBuildInstaller, INSIDERS_INSTALLER } from '../installer/types'; +import { traceDecorators } from '../logger'; +import { IDisposable, IDisposableRegistry } from '../types'; +import { ExtensionChannels, IExtensionChannelRule, IExtensionChannelService, IInsiderExtensionPrompt } from './types'; + +@injectable() +export class InsidersExtensionService implements IExtensionSingleActivationService { + constructor( + @inject(IExtensionChannelService) private readonly extensionChannelService: IExtensionChannelService, + @inject(IInsiderExtensionPrompt) private readonly insidersPrompt: IInsiderExtensionPrompt, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + @inject(ICommandManager) private readonly cmdManager: ICommandManager, + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(IExtensionBuildInstaller) + @named(INSIDERS_INSTALLER) + private readonly insidersInstaller: IExtensionBuildInstaller, + @inject(IDisposableRegistry) public readonly disposables: IDisposable[] + ) {} + + public async activate() { + this.registerCommandsAndHandlers(); + await this.initChannel(); + } + + public registerCommandsAndHandlers(): void { + this.disposables.push( + this.extensionChannelService.onDidChannelChange((channel) => { + return this.handleChannel(channel, true); + }) + ); + this.disposables.push( + this.cmdManager.registerCommand(Commands.SwitchOffInsidersChannel, () => + this.extensionChannelService.updateChannel('off') + ) + ); + this.disposables.push( + this.cmdManager.registerCommand(Commands.SwitchToInsidersDaily, () => + this.extensionChannelService.updateChannel('daily') + ) + ); + this.disposables.push( + this.cmdManager.registerCommand(Commands.SwitchToInsidersWeekly, () => + this.extensionChannelService.updateChannel('weekly') + ) + ); + } + + public async initChannel() { + const channel = this.extensionChannelService.getChannel(); + const isDefault = this.extensionChannelService.isChannelUsingDefaultConfiguration; + + const alreadyHandled = await this.handleEdgeCases(channel, isDefault); + if (!alreadyHandled) { + this.handleChannel(channel).ignoreErrors(); + } + } + + // Everything past here is the "channel handler" implementation. + + @traceDecorators.error('Handling channel failed') + public async handleChannel(installChannel: ExtensionChannels, didChannelChange: boolean = false): Promise { + const channelRule = this.serviceContainer.get(IExtensionChannelRule, installChannel); + const shouldInstall = await channelRule.shouldLookForInsidersBuild(didChannelChange); + if (!shouldInstall) { + return; + } + await this.insidersInstaller.install(); + await this.insidersPrompt.promptToReload(); + } + + /** + * Choose what to do in miscellaneous situations + * @returns `true` if install channel is handled in these miscellaneous cases, `false` if install channel needs further handling + */ + public async handleEdgeCases(installChannel: ExtensionChannels, isDefault: boolean): Promise { + // When running UI Tests we might want to disable these prompts. + if (process.env.UITEST_DISABLE_INSIDERS) { + return true; + } else if (await this.promptToInstallInsidersIfApplicable(isDefault)) { + return true; + } else if (await this.setInsidersChannelToOffIfApplicable(installChannel)) { + return true; + } else { + return false; + } + } + + /** + * Only when using VSC insiders and if they have not been notified before (usually the first session), notify to enroll into the insiders program + * @returns `true` if prompt is shown, `false` otherwise + */ + private async promptToInstallInsidersIfApplicable(isDefault: boolean): Promise { + if (this.appEnvironment.channel !== 'insiders') { + return false; + } + if (this.insidersPrompt.hasUserBeenNotified.value) { + return false; + } + if (!isDefault) { + return false; + } + + await this.insidersPrompt.promptToInstallInsiders(); + return true; + } + + /** + * When install channel is not in sync with what is installed, resolve discrepency by setting channel to "off" + * @returns `true` if channel is set to off, `false` otherwise + */ + private async setInsidersChannelToOffIfApplicable(installChannel: ExtensionChannels): Promise { + if (installChannel === 'off') { + return false; + } + if (this.appEnvironment.extensionChannel !== 'stable') { + return false; + } + + // Install channel is set to "weekly" or "daily" but stable version of extension is installed. Switch channel to "off" to use the installed version + await this.extensionChannelService.updateChannel('off'); + return true; + } +} diff --git a/src/client/common/insidersBuild/types.ts b/src/client/common/insidersBuild/types.ts new file mode 100644 index 000000000000..5c47c63ce1ff --- /dev/null +++ b/src/client/common/insidersBuild/types.ts @@ -0,0 +1,43 @@ +import { Event } from 'vscode'; +import { IPersistentState } from '../types'; + +export const IExtensionChannelRule = Symbol('IExtensionChannelRule'); +export interface IExtensionChannelRule { + /** + * Return `true` if insiders build is required to be installed for the channel + * @param isChannelRuleNew Carries boolean `true` if insiders channel just changed to this channel rule + */ + shouldLookForInsidersBuild(isChannelRuleNew?: boolean): Promise; +} + +export const IExtensionChannelService = Symbol('IExtensionChannelService'); +export interface IExtensionChannelService { + readonly onDidChannelChange: Event; + readonly isChannelUsingDefaultConfiguration: boolean; + getChannel(): ExtensionChannels; + updateChannel(value: ExtensionChannels): Promise; +} + +export const IInsiderExtensionPrompt = Symbol('IInsiderExtensionPrompt'); +export interface IInsiderExtensionPrompt { + /** + * Carries boolean `false` for the first session when user has not been notified. + * Gets updated to `true` once user has been prompted to install insiders. + */ + readonly hasUserBeenNotified: IPersistentState; + promptToInstallInsiders(): Promise; + promptToReload(): Promise; +} + +/** + * Note the values in this enum must belong to `ExtensionChannels` type + */ +export enum ExtensionChannel { + /** + * "off" setting is defined as a no op, which means user keeps using the extension they are using + */ + off = 'off', + weekly = 'weekly', + daily = 'daily' +} +export type ExtensionChannels = 'off' | 'weekly' | 'daily'; diff --git a/src/client/common/installer/channelManager.ts b/src/client/common/installer/channelManager.ts new file mode 100644 index 000000000000..14bda4002749 --- /dev/null +++ b/src/client/common/installer/channelManager.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { EnvironmentType } from '../../pythonEnvironments/info'; +import { IApplicationShell } from '../application/types'; +import { IPlatformService } from '../platform/types'; +import { Product } from '../types'; +import { Installer } from '../utils/localize'; +import { isResource } from '../utils/misc'; +import { ProductNames } from './productNames'; +import { IInstallationChannelManager, IModuleInstaller, InterpreterUri } from './types'; + +@injectable() +export class InstallationChannelManager implements IInstallationChannelManager { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + + public async getInstallationChannel( + product: Product, + resource?: InterpreterUri + ): Promise { + const channels = await this.getInstallationChannels(resource); + if (channels.length === 1) { + return channels[0]; + } + + const productName = ProductNames.get(product)!; + const appShell = this.serviceContainer.get(IApplicationShell); + if (channels.length === 0) { + await this.showNoInstallersMessage(isResource(resource) ? resource : undefined); + return; + } + + const placeHolder = `Select an option to install ${productName}`; + const options = channels.map((installer) => { + return { + label: `Install using ${installer.displayName}`, + description: '', + installer + }; + }); + const selection = await appShell.showQuickPick(options, { + matchOnDescription: true, + matchOnDetail: true, + placeHolder + }); + return selection ? selection.installer : undefined; + } + + public async getInstallationChannels(resource?: InterpreterUri): Promise { + const installers = this.serviceContainer.getAll(IModuleInstaller); + const supportedInstallers: IModuleInstaller[] = []; + if (installers.length === 0) { + return []; + } + // group by priority and pick supported from the highest priority + installers.sort((a, b) => b.priority - a.priority); + let currentPri = installers[0].priority; + for (const mi of installers) { + if (mi.priority !== currentPri) { + if (supportedInstallers.length > 0) { + break; // return highest priority supported installers + } + // If none supported, try next priority group + currentPri = mi.priority; + } + if (await mi.isSupported(resource)) { + supportedInstallers.push(mi); + } + } + return supportedInstallers; + } + + public async showNoInstallersMessage(resource?: Uri): Promise { + const interpreters = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreters.getActiveInterpreter(resource); + if (!interpreter) { + return; // Handled in the Python installation check. + } + + const appShell = this.serviceContainer.get(IApplicationShell); + const search = 'Search for help'; + let result: string | undefined; + if (interpreter.envType === EnvironmentType.Conda) { + result = await appShell.showErrorMessage(Installer.noCondaOrPipInstaller(), Installer.searchForHelp()); + } else { + result = await appShell.showErrorMessage(Installer.noPipInstaller(), Installer.searchForHelp()); + } + if (result === search) { + const platform = this.serviceContainer.get(IPlatformService); + const osName = platform.isWindows ? 'Windows' : platform.isMac ? 'MacOS' : 'Linux'; + appShell.openUrl( + `https://www.bing.com/search?q=Install Pip ${osName} ${ + interpreter.envType === EnvironmentType.Conda ? 'Conda' : '' + }` + ); + } + } +} diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts new file mode 100644 index 000000000000..4eb573eed4a3 --- /dev/null +++ b/src/client/common/installer/condaInstaller.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { ICondaService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { ExecutionInfo, IConfigurationService } from '../types'; +import { isResource } from '../utils/misc'; +import { ModuleInstaller } from './moduleInstaller'; +import { InterpreterUri } from './types'; + +/** + * A Python module installer for a conda environment. + */ +@injectable() +export class CondaInstaller extends ModuleInstaller { + public _isCondaAvailable: boolean | undefined; + + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + + public get name(): string { + return 'Conda'; + } + + public get displayName() { + return 'Conda'; + } + + public get priority(): number { + return 0; + } + + /** + * Checks whether we can use Conda as module installer for a given resource. + * We need to perform two checks: + * 1. Ensure we have conda. + * 2. Check if the current environment is a conda environment. + * @param {InterpreterUri} [resource=] Resource used to identify the workspace. + * @returns {Promise} Whether conda is supported as a module installer or not. + */ + public async isSupported(resource?: InterpreterUri): Promise { + if (this._isCondaAvailable === false) { + return false; + } + const condaLocator = this.serviceContainer.get(ICondaService); + this._isCondaAvailable = await condaLocator.isCondaAvailable(); + if (!this._isCondaAvailable) { + return false; + } + // Now we need to check if the current environment is a conda environment or not. + return this.isCurrentEnvironmentACondaEnvironment(resource); + } + + /** + * Return the commandline args needed to install the module. + */ + protected async getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise { + const condaService = this.serviceContainer.get(ICondaService); + const condaFile = await condaService.getCondaFile(); + + const pythonPath = isResource(resource) + ? this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath + : resource.path; + const info = await condaService.getCondaEnvironment(pythonPath); + const args = ['install']; + + if (info && info.name) { + // If we have the name of the conda environment, then use that. + args.push('--name'); + args.push(info.name!.toCommandArgument()); + } else if (info && info.path) { + // Else provide the full path to the environment path. + args.push('--prefix'); + args.push(info.path.fileToCommandArgument()); + } + args.push(moduleName); + args.push('-y'); + return { + args, + execPath: condaFile + }; + } + + /** + * Is the provided interprter a conda environment + */ + private async isCurrentEnvironmentACondaEnvironment(resource?: InterpreterUri): Promise { + const condaService = this.serviceContainer.get(ICondaService); + const pythonPath = isResource(resource) + ? this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath + : resource.path; + return condaService.isCondaEnvironment(pythonPath); + } +} diff --git a/src/client/common/installer/extensionBuildInstaller.ts b/src/client/common/installer/extensionBuildInstaller.ts new file mode 100644 index 000000000000..1e6aee90ced0 --- /dev/null +++ b/src/client/common/installer/extensionBuildInstaller.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../application/types'; +import { Octicons, PVSC_EXTENSION_ID, STANDARD_OUTPUT_CHANNEL } from '../constants'; +import { traceDecorators } from '../logger'; +import { IFileSystem } from '../platform/types'; +import { IFileDownloader, IOutputChannel } from '../types'; +import { ExtensionChannels } from '../utils/localize'; +import { IExtensionBuildInstaller } from './types'; + +export const developmentBuildUri = 'https://pvsc.blob.core.windows.net/extension-builds/ms-python-insiders.vsix'; +export const vsixFileExtension = '.vsix'; + +@injectable() +export class StableBuildInstaller implements IExtensionBuildInstaller { + constructor( + @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly output: IOutputChannel, + @inject(ICommandManager) private readonly cmdManager: ICommandManager, + @inject(IApplicationShell) private readonly appShell: IApplicationShell + ) {} + + @traceDecorators.error('Installing stable build of extension failed') + public async install(): Promise { + this.output.append(ExtensionChannels.installingStableMessage()); + await this.appShell.withProgressCustomIcon(Octicons.Installing, async (progress) => { + progress.report({ message: ExtensionChannels.installingStableMessage() }); + return this.cmdManager.executeCommand('workbench.extensions.installExtension', PVSC_EXTENSION_ID); + }); + this.output.appendLine(ExtensionChannels.installationCompleteMessage()); + } +} + +@injectable() +export class InsidersBuildInstaller implements IExtensionBuildInstaller { + constructor( + @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly output: IOutputChannel, + @inject(IFileDownloader) private readonly fileDownloader: IFileDownloader, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(ICommandManager) private readonly cmdManager: ICommandManager, + @inject(IApplicationShell) private readonly appShell: IApplicationShell + ) {} + + @traceDecorators.error('Installing insiders build of extension failed') + public async install(): Promise { + const vsixFilePath = await this.downloadInsiders(); + this.output.append(ExtensionChannels.installingInsidersMessage()); + await this.appShell.withProgressCustomIcon(Octicons.Installing, async (progress) => { + progress.report({ message: ExtensionChannels.installingInsidersMessage() }); + return this.cmdManager.executeCommand('workbench.extensions.installExtension', Uri.file(vsixFilePath)); + }); + this.output.appendLine(ExtensionChannels.installationCompleteMessage()); + await this.fs.deleteFile(vsixFilePath); + } + + @traceDecorators.error('Downloading insiders build of extension failed') + public async downloadInsiders(): Promise { + this.output.appendLine(ExtensionChannels.startingDownloadOutputMessage()); + const downloadOptions = { + extension: vsixFileExtension, + outputChannel: this.output, + progressMessagePrefix: ExtensionChannels.downloadingInsidersMessage() + }; + return this.fileDownloader.downloadFile(developmentBuildUri, downloadOptions).then((file) => { + this.output.appendLine(ExtensionChannels.downloadCompletedOutputMessage()); + return file; + }); + } +} diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts new file mode 100644 index 000000000000..c227c9e56906 --- /dev/null +++ b/src/client/common/installer/moduleInstaller.ts @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, OutputChannel, ProgressLocation, ProgressOptions } from 'vscode'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { EnvironmentType } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IApplicationShell } from '../application/types'; +import { wrapCancellationTokens } from '../cancellation'; +import { STANDARD_OUTPUT_CHANNEL } from '../constants'; +import { IFileSystem } from '../platform/types'; +import * as internalPython from '../process/internal/python'; +import { ITerminalServiceFactory } from '../terminal/types'; +import { ExecutionInfo, IConfigurationService, IOutputChannel } from '../types'; +import { Products } from '../utils/localize'; +import { isResource } from '../utils/misc'; +import { IModuleInstaller, InterpreterUri } from './types'; + +@injectable() +export abstract class ModuleInstaller implements IModuleInstaller { + public abstract get priority(): number; + public abstract get name(): string; + public abstract get displayName(): string; + + constructor(protected serviceContainer: IServiceContainer) {} + + public async installModule(name: string, resource?: InterpreterUri, cancel?: CancellationToken): Promise { + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { installer: this.displayName }); + const uri = isResource(resource) ? resource : undefined; + const executionInfo = await this.getExecutionInfo(name, resource); + const terminalService = this.serviceContainer + .get(ITerminalServiceFactory) + .getTerminalService(uri); + const install = async (token?: CancellationToken) => { + const executionInfoArgs = await this.processInstallArgs(executionInfo.args, resource); + if (executionInfo.moduleName) { + const configService = this.serviceContainer.get(IConfigurationService); + const settings = configService.getSettings(uri); + + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = isResource(resource) + ? await interpreterService.getActiveInterpreter(resource) + : resource; + const pythonPath = isResource(resource) ? settings.pythonPath : resource.path; + const args = internalPython.execModule(executionInfo.moduleName, executionInfoArgs); + if (!interpreter || interpreter.envType !== EnvironmentType.Unknown) { + await terminalService.sendCommand(pythonPath, args, token); + } else if (settings.globalModuleInstallation) { + const fs = this.serviceContainer.get(IFileSystem); + if (await fs.isDirReadonly(path.dirname(pythonPath)).catch((_err) => true)) { + this.elevatedInstall(pythonPath, args); + } else { + await terminalService.sendCommand(pythonPath, args, token); + } + } else { + await terminalService.sendCommand(pythonPath, args.concat(['--user']), token); + } + } else { + await terminalService.sendCommand(executionInfo.execPath!, executionInfoArgs, token); + } + }; + + // Display progress indicator if we have ability to cancel this operation from calling code. + // This is required as its possible the installation can take a long time. + // (i.e. if installation takes a long time in terminal or like, a progress indicator is necessary to let user know what is being waited on). + if (cancel) { + const shell = this.serviceContainer.get(IApplicationShell); + const options: ProgressOptions = { + location: ProgressLocation.Notification, + cancellable: true, + title: Products.installingModule().format(name) + }; + await shell.withProgress(options, async (_, token: CancellationToken) => + install(wrapCancellationTokens(token, cancel)) + ); + } else { + await install(cancel); + } + } + public abstract isSupported(resource?: InterpreterUri): Promise; + + protected elevatedInstall(execPath: string, args: string[]) { + const options = { + name: 'VS Code Python' + }; + const outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + const command = `"${execPath.replace(/\\/g, '/')}" ${args.join(' ')}`; + + outputChannel.appendLine(''); + outputChannel.appendLine(`[Elevated] ${command}`); + // tslint:disable-next-line:no-require-imports no-var-requires + const sudo = require('sudo-prompt'); + + sudo.exec(command, options, async (error: string, stdout: string, stderr: string) => { + if (error) { + const shell = this.serviceContainer.get(IApplicationShell); + await shell.showErrorMessage(error); + } else { + outputChannel.show(); + if (stdout) { + outputChannel.appendLine(''); + outputChannel.append(stdout); + } + if (stderr) { + outputChannel.appendLine(''); + outputChannel.append(`Warning: ${stderr}`); + } + } + }); + } + protected abstract getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise; + private async processInstallArgs(args: string[], resource?: InterpreterUri): Promise { + const indexOfPylint = args.findIndex((arg) => arg.toUpperCase() === 'PYLINT'); + if (indexOfPylint === -1) { + return args; + } + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = isResource(resource) ? await interpreterService.getActiveInterpreter(resource) : resource; + // If installing pylint on python 2.x, then use pylint~=1.9.0 + if (interpreter && interpreter.version && interpreter.version.major === 2) { + const newArgs = [...args]; + // This command could be sent to the terminal, hence '<' needs to be escaped for UNIX. + newArgs[indexOfPylint] = '"pylint<2.0.0"'; + return newArgs; + } + return args; + } +} diff --git a/src/client/common/installer/pipEnvInstaller.ts b/src/client/common/installer/pipEnvInstaller.ts new file mode 100644 index 000000000000..ea4ed5be906f --- /dev/null +++ b/src/client/common/installer/pipEnvInstaller.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IInterpreterLocatorService, PIPENV_SERVICE } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { EnvironmentType } from '../../pythonEnvironments/info'; +import { ExecutionInfo } from '../types'; +import { isResource } from '../utils/misc'; +import { ModuleInstaller } from './moduleInstaller'; +import { InterpreterUri } from './types'; + +export const pipenvName = 'pipenv'; + +@injectable() +export class PipEnvInstaller extends ModuleInstaller { + private readonly pipenv: IInterpreterLocatorService; + + public get name(): string { + return 'pipenv'; + } + + public get displayName() { + return pipenvName; + } + public get priority(): number { + return 10; + } + + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + this.pipenv = this.serviceContainer.get(IInterpreterLocatorService, PIPENV_SERVICE); + } + public async isSupported(resource?: InterpreterUri): Promise { + if (isResource(resource)) { + const interpreters = await this.pipenv.getInterpreters(resource); + return interpreters.length > 0; + } else { + return resource.envType === EnvironmentType.Pipenv; + } + } + protected async getExecutionInfo(moduleName: string, _resource?: InterpreterUri): Promise { + const args = ['install', moduleName, '--dev']; + if (moduleName === 'black') { + args.push('--pre'); + } + return { + args: args, + execPath: pipenvName + }; + } +} diff --git a/src/client/common/installer/pipInstaller.ts b/src/client/common/installer/pipInstaller.ts new file mode 100644 index 000000000000..9b0afd3abbc2 --- /dev/null +++ b/src/client/common/installer/pipInstaller.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IServiceContainer } from '../../ioc/types'; +import { IWorkspaceService } from '../application/types'; +import { IPythonExecutionFactory } from '../process/types'; +import { ExecutionInfo } from '../types'; +import { isResource } from '../utils/misc'; +import { ModuleInstaller } from './moduleInstaller'; +import { InterpreterUri } from './types'; + +@injectable() +export class PipInstaller extends ModuleInstaller { + public get name(): string { + return 'Pip'; + } + + public get displayName() { + return 'Pip'; + } + public get priority(): number { + return 0; + } + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public isSupported(resource?: InterpreterUri): Promise { + return this.isPipAvailable(resource); + } + protected async getExecutionInfo(moduleName: string, _resource?: InterpreterUri): Promise { + const proxyArgs: string[] = []; + const workspaceService = this.serviceContainer.get(IWorkspaceService); + const proxy = workspaceService.getConfiguration('http').get('proxy', ''); + if (proxy.length > 0) { + proxyArgs.push('--proxy'); + proxyArgs.push(proxy); + } + return { + args: [...proxyArgs, 'install', '-U', moduleName], + moduleName: 'pip' + }; + } + private isPipAvailable(info?: InterpreterUri): Promise { + const pythonExecutionFactory = this.serviceContainer.get(IPythonExecutionFactory); + const resource = isResource(info) ? info : undefined; + const pythonPath = isResource(info) ? undefined : info.path; + return pythonExecutionFactory + .create({ resource, pythonPath }) + .then((proc) => proc.isModuleInstalled('pip')) + .catch(() => false); + } +} diff --git a/src/client/common/installer/poetryInstaller.ts b/src/client/common/installer/poetryInstaller.ts new file mode 100644 index 000000000000..68f759e559b8 --- /dev/null +++ b/src/client/common/installer/poetryInstaller.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; +import { IWorkspaceService } from '../application/types'; +import { traceError } from '../logger'; +import { IFileSystem } from '../platform/types'; +import { IProcessServiceFactory } from '../process/types'; +import { ExecutionInfo, IConfigurationService } from '../types'; +import { isResource } from '../utils/misc'; +import { ModuleInstaller } from './moduleInstaller'; +import { InterpreterUri } from './types'; +export const poetryName = 'poetry'; +const poetryFile = 'poetry.lock'; + +@injectable() +export class PoetryInstaller extends ModuleInstaller { + public get name(): string { + return 'poetry'; + } + + public get displayName() { + return poetryName; + } + public get priority(): number { + return 10; + } + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IProcessServiceFactory) private readonly processFactory: IProcessServiceFactory + ) { + super(serviceContainer); + } + public async isSupported(resource?: InterpreterUri): Promise { + if (!resource) { + return false; + } + const workspaceFolder = this.workspaceService.getWorkspaceFolder(isResource(resource) ? resource : undefined); + if (!workspaceFolder) { + return false; + } + if (!(await this.fs.fileExists(path.join(workspaceFolder.uri.fsPath, poetryFile)))) { + return false; + } + return this.isPoetryAvailable(workspaceFolder.uri); + } + protected async isPoetryAvailable(workfolder: Uri) { + try { + const processService = await this.processFactory.create(workfolder); + const execPath = this.configurationService.getSettings(workfolder).poetryPath; + const result = await processService.exec(execPath, ['list'], { cwd: workfolder.fsPath }); + return result && (result.stderr || '').trim().length === 0; + } catch (error) { + traceError(`${poetryFile} exists but Poetry not found`, error); + return false; + } + } + protected async getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise { + const execPath = this.configurationService.getSettings(isResource(resource) ? resource : undefined).poetryPath; + const args = ['add', '--dev', moduleName]; + if (moduleName === 'black') { + args.push('--allow-prereleases'); + } + return { + args, + execPath + }; + } +} diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts new file mode 100644 index 000000000000..795f0fd84994 --- /dev/null +++ b/src/client/common/installer/productInstaller.ts @@ -0,0 +1,542 @@ +// tslint:disable:max-classes-per-file max-classes-per-file + +import { inject, injectable, named } from 'inversify'; +import * as os from 'os'; +import { CancellationToken, OutputChannel, Uri } from 'vscode'; +import '../../common/extensions'; +import * as localize from '../../common/utils/localize'; +import { Telemetry } from '../../datascience/constants'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { LinterId } from '../../linters/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../application/types'; +import { Commands, STANDARD_OUTPUT_CHANNEL } from '../constants'; +import { traceError } from '../logger'; +import { IPlatformService } from '../platform/types'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types'; +import { ITerminalServiceFactory } from '../terminal/types'; +import { + IConfigurationService, + IInstaller, + InstallerResponse, + IOutputChannel, + IPersistentStateFactory, + ModuleNamePurpose, + Product, + ProductType +} from '../types'; +import { isResource, noop } from '../utils/misc'; +import { StopWatch } from '../utils/stopWatch'; +import { ProductNames } from './productNames'; +import { + IInstallationChannelManager, + IModuleInstaller, + InterpreterUri, + IProductPathService, + IProductService +} from './types'; + +export { Product } from '../types'; + +export const CTagsInsllationScript = + os.platform() === 'darwin' ? 'brew install ctags' : 'sudo apt-get install exuberant-ctags'; + +export abstract class BaseInstaller { + private static readonly PromptPromises = new Map>(); + protected readonly appShell: IApplicationShell; + protected readonly configService: IConfigurationService; + private readonly workspaceService: IWorkspaceService; + private readonly productService: IProductService; + + constructor(protected serviceContainer: IServiceContainer, protected outputChannel: OutputChannel) { + this.appShell = serviceContainer.get(IApplicationShell); + this.configService = serviceContainer.get(IConfigurationService); + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.productService = serviceContainer.get(IProductService); + } + + public promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken + ): Promise { + // If this method gets called twice, while previous promise has not been resolved, then return that same promise. + // E.g. previous promise is not resolved as a message has been displayed to the user, so no point displaying + // another message. + const workspaceFolder = + resource && isResource(resource) ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + const key = `${product}${workspaceFolder ? workspaceFolder.uri.fsPath : ''}`; + if (BaseInstaller.PromptPromises.has(key)) { + return BaseInstaller.PromptPromises.get(key)!; + } + const promise = this.promptToInstallImplementation(product, resource, cancel); + BaseInstaller.PromptPromises.set(key, promise); + promise.then(() => BaseInstaller.PromptPromises.delete(key)).ignoreErrors(); + promise.catch(() => BaseInstaller.PromptPromises.delete(key)).ignoreErrors(); + + return promise; + } + + public async install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken + ): Promise { + if (product === Product.unittest) { + return InstallerResponse.Installed; + } + + const channels = this.serviceContainer.get(IInstallationChannelManager); + const installer = await channels.getInstallationChannel(product, resource); + if (!installer) { + return InstallerResponse.Ignore; + } + + const moduleName = translateProductToModule(product, ModuleNamePurpose.install); + await installer + .installModule(moduleName, resource, cancel) + .catch((ex) => traceError(`Error in installing the module '${moduleName}', ${ex}`)); + + return this.isInstalled(product, resource).then((isInstalled) => + isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore + ); + } + + public async isInstalled(product: Product, resource?: InterpreterUri): Promise { + if (product === Product.unittest) { + return true; + } + // User may have customized the module name or provided the fully qualified path. + const interpreter = isResource(resource) ? undefined : resource; + const uri = isResource(resource) ? resource : undefined; + const executableName = this.getExecutableNameFromSettings(product, uri); + + const isModule = this.isExecutableAModule(product, uri); + if (isModule) { + const pythonProcess = await this.serviceContainer + .get(IPythonExecutionFactory) + .createActivatedEnvironment({ resource: uri, interpreter, allowEnvironmentFetchExceptions: true }); + return pythonProcess.isModuleInstalled(executableName); + } else { + const process = await this.serviceContainer.get(IProcessServiceFactory).create(uri); + return process + .exec(executableName, ['--version'], { mergeStdOutErr: true }) + .then(() => true) + .catch(() => false); + } + } + + protected abstract promptToInstallImplementation( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken + ): Promise; + protected getExecutableNameFromSettings(product: Product, resource?: Uri): string { + const productType = this.productService.getProductType(product); + const productPathService = this.serviceContainer.get(IProductPathService, productType); + return productPathService.getExecutableNameFromSettings(product, resource); + } + protected isExecutableAModule(product: Product, resource?: Uri): Boolean { + const productType = this.productService.getProductType(product); + const productPathService = this.serviceContainer.get(IProductPathService, productType); + return productPathService.isExecutableAModule(product, resource); + } +} + +export class CTagsInstaller extends BaseInstaller { + constructor(serviceContainer: IServiceContainer, outputChannel: OutputChannel) { + super(serviceContainer, outputChannel); + } + + public async install(_product: Product, resource?: Uri): Promise { + if (this.serviceContainer.get(IPlatformService).isWindows) { + this.outputChannel.appendLine('Install Universal Ctags Win32 to enable support for Workspace Symbols'); + this.outputChannel.appendLine('Download the CTags binary from the Universal CTags site.'); + this.outputChannel.appendLine( + 'Option 1: Extract ctags.exe from the downloaded zip to any folder within your PATH so that Visual Studio Code can run it.' + ); + this.outputChannel.appendLine( + 'Option 2: Extract to any folder and add the path to this folder to the command setting.' + ); + this.outputChannel.appendLine( + 'Option 3: Extract to any folder and define that path in the python.workspaceSymbols.ctagsPath setting of your user settings file (settings.json).' + ); + this.outputChannel.show(); + } else { + const terminalService = this.serviceContainer + .get(ITerminalServiceFactory) + .getTerminalService(resource); + terminalService + .sendCommand(CTagsInsllationScript, []) + .catch((ex) => traceError(`Failed to install ctags. Script sent '${CTagsInsllationScript}', ${ex}`)); + } + return InstallerResponse.Ignore; + } + protected async promptToInstallImplementation( + product: Product, + resource?: Uri, + _cancel?: CancellationToken + ): Promise { + const item = await this.appShell.showErrorMessage( + 'Install CTags to enable Python workspace symbols?', + 'Yes', + 'No' + ); + return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; + } +} + +export class FormatterInstaller extends BaseInstaller { + protected async promptToInstallImplementation( + product: Product, + resource?: Uri, + cancel?: CancellationToken + ): Promise { + // Hard-coded on purpose because the UI won't necessarily work having + // another formatter. + const formatters = [Product.autopep8, Product.black, Product.yapf]; + const formatterNames = formatters.map((formatter) => ProductNames.get(formatter)!); + const productName = ProductNames.get(product)!; + formatterNames.splice(formatterNames.indexOf(productName), 1); + const useOptions = formatterNames.map((name) => `Use ${name}`); + const yesChoice = 'Yes'; + + const options = [...useOptions]; + let message = `Formatter ${productName} is not installed. Install?`; + if (this.isExecutableAModule(product, resource)) { + options.splice(0, 0, yesChoice); + } else { + const executable = this.getExecutableNameFromSettings(product, resource); + message = `Path to the ${productName} formatter is invalid (${executable})`; + } + + const item = await this.appShell.showErrorMessage(message, ...options); + if (item === yesChoice) { + return this.install(product, resource, cancel); + } else if (typeof item === 'string') { + for (const formatter of formatters) { + const formatterName = ProductNames.get(formatter)!; + + if (item.endsWith(formatterName)) { + await this.configService.updateSetting('formatting.provider', formatterName, resource); + return this.install(formatter, resource, cancel); + } + } + } + + return InstallerResponse.Ignore; + } +} + +export class LinterInstaller extends BaseInstaller { + protected async promptToInstallImplementation( + product: Product, + resource?: Uri, + cancel?: CancellationToken + ): Promise { + const isPylint = product === Product.pylint; + + const productName = ProductNames.get(product)!; + const install = 'Install'; + const disableInstallPrompt = 'Do not show again'; + const disableLinterInstallPromptKey = `${productName}_DisableLinterInstallPrompt`; + const selectLinter = 'Select Linter'; + + if (isPylint && this.getStoredResponse(disableLinterInstallPromptKey) === true) { + return InstallerResponse.Ignore; + } + + const options = isPylint ? [selectLinter, disableInstallPrompt] : [selectLinter]; + + let message = `Linter ${productName} is not installed.`; + if (this.isExecutableAModule(product, resource)) { + options.splice(0, 0, install); + } else { + const executable = this.getExecutableNameFromSettings(product, resource); + message = `Path to the ${productName} linter is invalid (${executable})`; + } + const response = await this.appShell.showErrorMessage(message, ...options); + if (response === install) { + sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { + tool: productName as LinterId, + action: 'install' + }); + return this.install(product, resource, cancel); + } else if (response === disableInstallPrompt) { + await this.setStoredResponse(disableLinterInstallPromptKey, true); + sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { + tool: productName as LinterId, + action: 'disablePrompt' + }); + return InstallerResponse.Ignore; + } + + if (response === selectLinter) { + sendTelemetryEvent(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select' }); + const commandManager = this.serviceContainer.get(ICommandManager); + await commandManager.executeCommand(Commands.Set_Linter); + } + return InstallerResponse.Ignore; + } + + /** + * For installers that want to avoid prompting the user over and over, they can make use of a + * persisted true/false value representing user responses to 'stop showing this prompt'. This method + * gets the persisted value given the installer-defined key. + * + * @param key Key to use to get a persisted response value, each installer must define this for themselves. + * @returns Boolean: The current state of the stored response key given. + */ + protected getStoredResponse(key: string): boolean { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const state = factory.createGlobalPersistentState(key, undefined); + return state.value === true; + } + + /** + * For installers that want to avoid prompting the user over and over, they can make use of a + * persisted true/false value representing user responses to 'stop showing this prompt'. This + * method will set that persisted value given the installer-defined key. + * + * @param key Key to use to get a persisted response value, each installer must define this for themselves. + * @param value Boolean value to store for the user - if they choose to not be prompted again for instance. + * @returns Boolean: The current state of the stored response key given. + */ + private async setStoredResponse(key: string, value: boolean): Promise { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const state = factory.createGlobalPersistentState(key, undefined); + if (state && state.value !== value) { + await state.updateValue(value); + } + } +} + +export class TestFrameworkInstaller extends BaseInstaller { + protected async promptToInstallImplementation( + product: Product, + resource?: Uri, + cancel?: CancellationToken + ): Promise { + const productName = ProductNames.get(product)!; + + const options: string[] = []; + let message = `Test framework ${productName} is not installed. Install?`; + if (this.isExecutableAModule(product, resource)) { + options.push(...['Yes', 'No']); + } else { + const executable = this.getExecutableNameFromSettings(product, resource); + message = `Path to the ${productName} test framework is invalid (${executable})`; + } + + const item = await this.appShell.showErrorMessage(message, ...options); + return item === 'Yes' ? this.install(product, resource, cancel) : InstallerResponse.Ignore; + } +} + +export class RefactoringLibraryInstaller extends BaseInstaller { + protected async promptToInstallImplementation( + product: Product, + resource?: Uri, + cancel?: CancellationToken + ): Promise { + const productName = ProductNames.get(product)!; + const item = await this.appShell.showErrorMessage( + `Refactoring library ${productName} is not installed. Install?`, + 'Yes', + 'No' + ); + return item === 'Yes' ? this.install(product, resource, cancel) : InstallerResponse.Ignore; + } +} + +export class DataScienceInstaller extends BaseInstaller { + // Override base installer to support a more DS-friendly streamlined installation. + public async install( + product: Product, + interpreterUri?: InterpreterUri, + cancel?: CancellationToken + ): Promise { + // Precondition + if (isResource(interpreterUri)) { + throw new Error('All data science packages require an interpreter be passed in'); + } + + // At this point we know that `interpreterUri` is of type PythonInterpreter + const interpreter = interpreterUri as PythonEnvironment; + + // Get a list of known installation channels, pip, conda, etc. + const channels: IModuleInstaller[] = await this.serviceContainer + .get(IInstallationChannelManager) + .getInstallationChannels(); + + // Pick an installerModule based on whether the interpreter is conda or not. Default is pip. + let installerModule; + if (interpreter.envType === 'Conda') { + installerModule = channels.find((v) => v.name === 'Conda'); + } else { + installerModule = channels.find((v) => v.name === 'Pip'); + } + + const moduleName = translateProductToModule(product, ModuleNamePurpose.install); + if (!installerModule) { + this.appShell + .showErrorMessage(localize.DataScience.couldNotInstallLibrary().format(moduleName)) + .then(noop, noop); + return InstallerResponse.Ignore; + } + + await installerModule + .installModule(moduleName, interpreter, cancel) + .catch((ex) => traceError(`Error in installing the module '${moduleName}', ${ex}`)); + + return this.isInstalled(product, interpreter).then((isInstalled) => + isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore + ); + } + protected async promptToInstallImplementation( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken + ): Promise { + const productName = ProductNames.get(product)!; + const item = await this.appShell.showErrorMessage( + localize.DataScience.libraryNotInstalled().format(productName), + 'Yes', + 'No' + ); + if (item === 'Yes') { + const stopWatch = new StopWatch(); + try { + const response = await this.install(product, resource, cancel); + const event = + product === Product.jupyter ? Telemetry.UserInstalledJupyter : Telemetry.UserInstalledModule; + sendTelemetryEvent(event, stopWatch.elapsedTime, { product: productName }); + return response; + } catch (e) { + if (product === Product.jupyter) { + sendTelemetryEvent(Telemetry.JupyterInstallFailed); + } + throw e; + } + } + return InstallerResponse.Ignore; + } +} + +@injectable() +export class ProductInstaller implements IInstaller { + private readonly productService: IProductService; + private interpreterService: IInterpreterService; + + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private outputChannel: OutputChannel + ) { + this.productService = serviceContainer.get(IProductService); + this.interpreterService = this.serviceContainer.get(IInterpreterService); + } + + // tslint:disable-next-line:no-empty + public dispose() {} + public async promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken + ): Promise { + const currentInterpreter = isResource(resource) + ? await this.interpreterService.getActiveInterpreter(resource) + : resource; + if (!currentInterpreter) { + return InstallerResponse.Ignore; + } + return this.createInstaller(product).promptToInstall(product, resource, cancel); + } + public async install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken + ): Promise { + return this.createInstaller(product).install(product, resource, cancel); + } + public async isInstalled(product: Product, resource?: InterpreterUri): Promise { + return this.createInstaller(product).isInstalled(product, resource); + } + public translateProductToModuleName(product: Product, purpose: ModuleNamePurpose): string { + return translateProductToModule(product, purpose); + } + private createInstaller(product: Product): BaseInstaller { + const productType = this.productService.getProductType(product); + switch (productType) { + case ProductType.Formatter: + return new FormatterInstaller(this.serviceContainer, this.outputChannel); + case ProductType.Linter: + return new LinterInstaller(this.serviceContainer, this.outputChannel); + case ProductType.WorkspaceSymbols: + return new CTagsInstaller(this.serviceContainer, this.outputChannel); + case ProductType.TestFramework: + return new TestFrameworkInstaller(this.serviceContainer, this.outputChannel); + case ProductType.RefactoringLibrary: + return new RefactoringLibraryInstaller(this.serviceContainer, this.outputChannel); + case ProductType.DataScience: + return new DataScienceInstaller(this.serviceContainer, this.outputChannel); + default: + break; + } + throw new Error(`Unknown product ${product}`); + } +} + +// tslint:disable-next-line: cyclomatic-complexity +function translateProductToModule(product: Product, purpose: ModuleNamePurpose): string { + switch (product) { + case Product.mypy: + return 'mypy'; + case Product.nosetest: { + return purpose === ModuleNamePurpose.install ? 'nose' : 'nosetests'; + } + case Product.pylama: + return 'pylama'; + case Product.prospector: + return 'prospector'; + case Product.pylint: + return 'pylint'; + case Product.pytest: + return 'pytest'; + case Product.autopep8: + return 'autopep8'; + case Product.black: + return 'black'; + case Product.pycodestyle: + return 'pycodestyle'; + case Product.pydocstyle: + return 'pydocstyle'; + case Product.yapf: + return 'yapf'; + case Product.flake8: + return 'flake8'; + case Product.unittest: + return 'unittest'; + case Product.rope: + return 'rope'; + case Product.bandit: + return 'bandit'; + case Product.jupyter: + return 'jupyter'; + case Product.notebook: + return 'notebook'; + case Product.pandas: + return 'pandas'; + case Product.ipykernel: + return 'ipykernel'; + case Product.nbconvert: + return 'nbconvert'; + case Product.kernelspec: + return 'kernelspec'; + default: { + throw new Error(`Product ${product} cannot be installed as a Python Module.`); + } + } +} diff --git a/src/client/common/installer/productNames.ts b/src/client/common/installer/productNames.ts new file mode 100644 index 000000000000..54d478f81d89 --- /dev/null +++ b/src/client/common/installer/productNames.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Product } from '../types'; + +// tslint:disable-next-line:variable-name +export const ProductNames = new Map(); +ProductNames.set(Product.autopep8, 'autopep8'); +ProductNames.set(Product.bandit, 'bandit'); +ProductNames.set(Product.black, 'black'); +ProductNames.set(Product.flake8, 'flake8'); +ProductNames.set(Product.mypy, 'mypy'); +ProductNames.set(Product.nosetest, 'nosetest'); +ProductNames.set(Product.pycodestyle, 'pycodestyle'); +ProductNames.set(Product.pylama, 'pylama'); +ProductNames.set(Product.prospector, 'prospector'); +ProductNames.set(Product.pydocstyle, 'pydocstyle'); +ProductNames.set(Product.pylint, 'pylint'); +ProductNames.set(Product.pytest, 'pytest'); +ProductNames.set(Product.yapf, 'yapf'); +ProductNames.set(Product.rope, 'rope'); +ProductNames.set(Product.jupyter, 'jupyter'); +ProductNames.set(Product.notebook, 'notebook'); +ProductNames.set(Product.ipykernel, 'ipykernel'); +ProductNames.set(Product.nbconvert, 'nbconvert'); +ProductNames.set(Product.kernelspec, 'kernelspec'); +ProductNames.set(Product.pandas, 'pandas'); diff --git a/src/client/common/installer/productPath.ts b/src/client/common/installer/productPath.ts new file mode 100644 index 000000000000..7ef9bd801d5d --- /dev/null +++ b/src/client/common/installer/productPath.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-classes-per-file + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IFormatterHelper } from '../../formatters/types'; +import { IServiceContainer } from '../../ioc/types'; +import { ILinterManager } from '../../linters/types'; +import { ITestsHelper } from '../../testing/common/types'; +import { IConfigurationService, IInstaller, ModuleNamePurpose, Product } from '../types'; +import { IProductPathService } from './types'; + +@injectable() +export abstract class BaseProductPathsService implements IProductPathService { + protected readonly configService: IConfigurationService; + protected readonly productInstaller: IInstaller; + constructor(@inject(IServiceContainer) protected serviceContainer: IServiceContainer) { + this.configService = serviceContainer.get(IConfigurationService); + this.productInstaller = serviceContainer.get(IInstaller); + } + public abstract getExecutableNameFromSettings(product: Product, resource?: Uri): string; + public isExecutableAModule(product: Product, resource?: Uri): Boolean { + if (product === Product.kernelspec) { + return false; + } + let moduleName: string | undefined; + try { + moduleName = this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); + // tslint:disable-next-line:no-empty + } catch {} + + // User may have customized the module name or provided the fully qualifieid path. + const executableName = this.getExecutableNameFromSettings(product, resource); + + return ( + typeof moduleName === 'string' && moduleName.length > 0 && path.basename(executableName) === executableName + ); + } +} + +@injectable() +export class CTagsProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(_: Product, resource?: Uri): string { + const settings = this.configService.getSettings(resource); + return settings.workspaceSymbols.ctagsPath; + } +} + +@injectable() +export class FormatterProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(product: Product, resource?: Uri): string { + const settings = this.configService.getSettings(resource); + const formatHelper = this.serviceContainer.get(IFormatterHelper); + const settingsPropNames = formatHelper.getSettingsPropertyNames(product); + return settings.formatting[settingsPropNames.pathName] as string; + } +} + +@injectable() +export class LinterProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(product: Product, resource?: Uri): string { + const linterManager = this.serviceContainer.get(ILinterManager); + return linterManager.getLinterInfo(product).pathName(resource); + } +} + +@injectable() +export class TestFrameworkProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(product: Product, resource?: Uri): string { + const testHelper = this.serviceContainer.get(ITestsHelper); + const settingsPropNames = testHelper.getSettingsPropertyNames(product); + if (!settingsPropNames.pathName) { + // E.g. in the case of UnitTests we don't allow customizing the paths. + return this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); + } + const settings = this.configService.getSettings(resource); + return settings.testing[settingsPropNames.pathName] as string; + } +} + +@injectable() +export class RefactoringLibraryProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(product: Product, _?: Uri): string { + return this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); + } +} + +@injectable() +export class DataScienceProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(product: Product, _?: Uri): string { + return this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); + } +} diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts new file mode 100644 index 000000000000..9687529670a0 --- /dev/null +++ b/src/client/common/installer/productService.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { Product, ProductType } from '../types'; +import { IProductService } from './types'; + +@injectable() +export class ProductService implements IProductService { + private ProductTypes = new Map(); + + constructor() { + this.ProductTypes.set(Product.bandit, ProductType.Linter); + this.ProductTypes.set(Product.flake8, ProductType.Linter); + this.ProductTypes.set(Product.mypy, ProductType.Linter); + this.ProductTypes.set(Product.pycodestyle, ProductType.Linter); + this.ProductTypes.set(Product.prospector, ProductType.Linter); + this.ProductTypes.set(Product.pydocstyle, ProductType.Linter); + this.ProductTypes.set(Product.pylama, ProductType.Linter); + this.ProductTypes.set(Product.pylint, ProductType.Linter); + this.ProductTypes.set(Product.ctags, ProductType.WorkspaceSymbols); + this.ProductTypes.set(Product.nosetest, ProductType.TestFramework); + this.ProductTypes.set(Product.pytest, ProductType.TestFramework); + this.ProductTypes.set(Product.unittest, ProductType.TestFramework); + this.ProductTypes.set(Product.autopep8, ProductType.Formatter); + this.ProductTypes.set(Product.black, ProductType.Formatter); + this.ProductTypes.set(Product.yapf, ProductType.Formatter); + this.ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); + this.ProductTypes.set(Product.jupyter, ProductType.DataScience); + this.ProductTypes.set(Product.notebook, ProductType.DataScience); + this.ProductTypes.set(Product.ipykernel, ProductType.DataScience); + this.ProductTypes.set(Product.nbconvert, ProductType.DataScience); + this.ProductTypes.set(Product.kernelspec, ProductType.DataScience); + this.ProductTypes.set(Product.pandas, ProductType.DataScience); + } + public getProductType(product: Product): ProductType { + return this.ProductTypes.get(product)!; + } +} diff --git a/src/client/common/installer/serviceRegistry.ts b/src/client/common/installer/serviceRegistry.ts new file mode 100644 index 000000000000..9c3d4680245e --- /dev/null +++ b/src/client/common/installer/serviceRegistry.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { IServiceManager } from '../../ioc/types'; +import { IWebviewPanelProvider } from '../application/types'; +import { WebviewPanelProvider } from '../application/webviewPanels/webviewPanelProvider'; +import { ProductType } from '../types'; +import { InstallationChannelManager } from './channelManager'; +import { CondaInstaller } from './condaInstaller'; +import { InsidersBuildInstaller, StableBuildInstaller } from './extensionBuildInstaller'; +import { PipEnvInstaller } from './pipEnvInstaller'; +import { PipInstaller } from './pipInstaller'; +import { PoetryInstaller } from './poetryInstaller'; +import { + CTagsProductPathService, + DataScienceProductPathService, + FormatterProductPathService, + LinterProductPathService, + RefactoringLibraryProductPathService, + TestFrameworkProductPathService +} from './productPath'; +import { ProductService } from './productService'; +import { + IExtensionBuildInstaller, + IInstallationChannelManager, + IModuleInstaller, + INSIDERS_INSTALLER, + IProductPathService, + IProductService, + STABLE_INSTALLER +} from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IModuleInstaller, CondaInstaller); + serviceManager.addSingleton(IModuleInstaller, PipInstaller); + serviceManager.addSingleton(IModuleInstaller, PipEnvInstaller); + serviceManager.addSingleton(IModuleInstaller, PoetryInstaller); + serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); + serviceManager.addSingleton( + IExtensionBuildInstaller, + StableBuildInstaller, + STABLE_INSTALLER + ); + serviceManager.addSingleton( + IExtensionBuildInstaller, + InsidersBuildInstaller, + INSIDERS_INSTALLER + ); + + serviceManager.addSingleton(IProductService, ProductService); + serviceManager.addSingleton( + IProductPathService, + CTagsProductPathService, + ProductType.WorkspaceSymbols + ); + serviceManager.addSingleton( + IProductPathService, + FormatterProductPathService, + ProductType.Formatter + ); + serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); + serviceManager.addSingleton( + IProductPathService, + TestFrameworkProductPathService, + ProductType.TestFramework + ); + serviceManager.addSingleton( + IProductPathService, + RefactoringLibraryProductPathService, + ProductType.RefactoringLibrary + ); + serviceManager.addSingleton( + IProductPathService, + DataScienceProductPathService, + ProductType.DataScience + ); + serviceManager.addSingleton(IWebviewPanelProvider, WebviewPanelProvider); +} diff --git a/src/client/common/installer/types.ts b/src/client/common/installer/types.ts new file mode 100644 index 000000000000..4b1bb9ad146d --- /dev/null +++ b/src/client/common/installer/types.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Uri } from 'vscode'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { Product, ProductType, Resource } from '../types'; + +export type InterpreterUri = Resource | PythonEnvironment; + +export const IModuleInstaller = Symbol('IModuleInstaller'); +export interface IModuleInstaller { + readonly name: string; + readonly displayName: string; + readonly priority: number; + /** + * Installs a module + * If a cancellation token is provided, then a cancellable progress message is dispalyed. + * At this point, this method would resolve only after the module has been successfully installed. + * If cancellation token is not provided, its not guaranteed that module installation has completed. + * @param {string} name + * @param {InterpreterUri} [resource] + * @param {CancellationToken} [cancel] + * @returns {Promise} + * @memberof IModuleInstaller + */ + installModule(name: string, resource?: InterpreterUri, cancel?: CancellationToken): Promise; + isSupported(resource?: InterpreterUri): Promise; +} + +export const IPythonInstallation = Symbol('IPythonInstallation'); +export interface IPythonInstallation { + checkInstallation(): Promise; +} + +export const IInstallationChannelManager = Symbol('IInstallationChannelManager'); +export interface IInstallationChannelManager { + getInstallationChannel(product: Product, resource?: InterpreterUri): Promise; + getInstallationChannels(resource?: InterpreterUri): Promise; + showNoInstallersMessage(): void; +} +export const IProductService = Symbol('IProductService'); +export interface IProductService { + getProductType(product: Product): ProductType; +} +export const IProductPathService = Symbol('IProductPathService'); +export interface IProductPathService { + getExecutableNameFromSettings(product: Product, resource?: Uri): string; + isExecutableAModule(product: Product, resource?: Uri): Boolean; +} + +export const INSIDERS_INSTALLER = 'INSIDERS_INSTALLER'; +export const STABLE_INSTALLER = 'STABLE_INSTALLER'; +export const IExtensionBuildInstaller = Symbol('IExtensionBuildInstaller'); +export interface IExtensionBuildInstaller { + install(): Promise; +} diff --git a/src/client/common/interpreterPathService.ts b/src/client/common/interpreterPathService.ts new file mode 100644 index 000000000000..a5bbc17108b5 --- /dev/null +++ b/src/client/common/interpreterPathService.ts @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fs from 'fs-extra'; +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, ConfigurationTarget, Event, EventEmitter, Uri } from 'vscode'; +import { IWorkspaceService } from './application/types'; +import { PythonSettings } from './configSettings'; +import { isTestExecution } from './constants'; +import { traceError } from './logger'; +import { FileSystemPaths } from './platform/fs-paths'; +import { + IDisposable, + IDisposableRegistry, + IInterpreterPathService, + InspectInterpreterSettingType, + InterpreterConfigurationScope, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, + Resource +} from './types'; + +export const workspaceKeysForWhichTheCopyIsDone_Key = 'workspaceKeysForWhichTheCopyIsDone_Key'; +export const workspaceFolderKeysForWhichTheCopyIsDone_Key = 'workspaceFolderKeysForWhichTheCopyIsDone_Key'; +export const isGlobalSettingCopiedKey = 'isGlobalSettingCopiedKey'; +export const defaultInterpreterPathSetting: keyof IPythonSettings = 'defaultInterpreterPath'; +const CI_PYTHON_PATH = getCIPythonPath(); + +export function getCIPythonPath(): string { + if (process.env.CI_PYTHON_PATH && fs.existsSync(process.env.CI_PYTHON_PATH)) { + return process.env.CI_PYTHON_PATH; + } + return 'python'; +} +@injectable() +export class InterpreterPathService implements IInterpreterPathService { + public get onDidChange(): Event { + return this._didChangeInterpreterEmitter.event; + } + public _didChangeInterpreterEmitter = new EventEmitter(); + private fileSystemPaths: FileSystemPaths; + constructor( + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IDisposableRegistry) disposables: IDisposable[] + ) { + disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + this.fileSystemPaths = FileSystemPaths.withDefaults(); + } + + public async onDidChangeConfiguration(event: ConfigurationChangeEvent) { + if (event.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) { + this._didChangeInterpreterEmitter.fire({ uri: undefined, configTarget: ConfigurationTarget.Global }); + } + } + + public inspect(resource: Resource): InspectInterpreterSettingType { + resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; + let workspaceFolderSetting: IPersistentState | undefined; + let workspaceSetting: IPersistentState | undefined; + if (resource) { + workspaceFolderSetting = this.persistentStateFactory.createGlobalPersistentState( + this.getSettingKey(resource, ConfigurationTarget.WorkspaceFolder), + undefined + ); + workspaceSetting = this.persistentStateFactory.createGlobalPersistentState( + this.getSettingKey(resource, ConfigurationTarget.Workspace), + undefined + ); + } + const globalValue = this.workspaceService.getConfiguration('python')!.inspect('defaultInterpreterPath')! + .globalValue; + return { + globalValue, + workspaceFolderValue: workspaceFolderSetting?.value, + workspaceValue: workspaceSetting?.value + }; + } + + public get(resource: Resource): string { + const settings = this.inspect(resource); + return ( + settings.workspaceFolderValue || + settings.workspaceValue || + settings.globalValue || + (isTestExecution() ? CI_PYTHON_PATH : 'python') + ); + } + + public async update( + resource: Resource, + configTarget: ConfigurationTarget, + pythonPath: string | undefined + ): Promise { + resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; + if (configTarget === ConfigurationTarget.Global) { + const pythonConfig = this.workspaceService.getConfiguration('python'); + const globalValue = pythonConfig.inspect('defaultInterpreterPath')!.globalValue; + if (globalValue !== pythonPath) { + await pythonConfig.update('defaultInterpreterPath', pythonPath, true); + this._didChangeInterpreterEmitter.fire({ uri: undefined, configTarget }); + } + return; + } + if (!resource) { + traceError('Cannot update workspace settings as no workspace is opened'); + return; + } + const settingKey = this.getSettingKey(resource, configTarget); + const persistentSetting = this.persistentStateFactory.createGlobalPersistentState( + settingKey, + undefined + ); + if (persistentSetting.value !== pythonPath) { + await persistentSetting.updateValue(pythonPath); + this._didChangeInterpreterEmitter.fire({ uri: resource, configTarget }); + } + } + + public getSettingKey( + resource: Uri, + configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder + ): string { + let settingKey: string; + const folderKey = this.workspaceService.getWorkspaceFolderIdentifier(resource); + if (configTarget === ConfigurationTarget.WorkspaceFolder) { + settingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${folderKey}`; + } else { + settingKey = this.workspaceService.workspaceFile + ? `WORKSPACE_INTERPRETER_PATH_${this.fileSystemPaths.normCase( + this.workspaceService.workspaceFile.fsPath + )}` + : // Only a single folder is opened, use fsPath of the folder as key + `WORKSPACE_FOLDER_INTERPRETER_PATH_${folderKey}`; + } + return settingKey; + } + + public async copyOldInterpreterStorageValuesToNew(resource: Resource): Promise { + resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; + const oldSettings = this.workspaceService.getConfiguration('python', resource).inspect('pythonPath')!; + await Promise.all([ + this._copyWorkspaceFolderValueToNewStorage(resource, oldSettings.workspaceFolderValue), + this._copyWorkspaceValueToNewStorage(resource, oldSettings.workspaceValue), + this._moveGlobalSettingValueToNewStorage(oldSettings.globalValue) + ]); + } + + public async _copyWorkspaceFolderValueToNewStorage(resource: Resource, value: string | undefined): Promise { + // Copy workspace folder setting into the new storage if it hasn't been copied already + const workspaceFolderKey = this.workspaceService.getWorkspaceFolderIdentifier(resource, ''); + if (workspaceFolderKey === '') { + // No workspace folder is opened, simply return. + return; + } + const flaggedWorkspaceFolderKeysStorage = this.persistentStateFactory.createGlobalPersistentState( + workspaceFolderKeysForWhichTheCopyIsDone_Key, + [] + ); + const flaggedWorkspaceFolderKeys = flaggedWorkspaceFolderKeysStorage.value; + const shouldUpdateWorkspaceFolderSetting = !flaggedWorkspaceFolderKeys.includes(workspaceFolderKey); + if (shouldUpdateWorkspaceFolderSetting) { + await this.update(resource, ConfigurationTarget.WorkspaceFolder, value); + await flaggedWorkspaceFolderKeysStorage.updateValue([workspaceFolderKey, ...flaggedWorkspaceFolderKeys]); + } + } + + public async _copyWorkspaceValueToNewStorage(resource: Resource, value: string | undefined): Promise { + // Copy workspace setting into the new storage if it hasn't been copied already + const workspaceKey = this.workspaceService.workspaceFile + ? this.fileSystemPaths.normCase(this.workspaceService.workspaceFile.fsPath) + : undefined; + if (!workspaceKey) { + return; + } + const flaggedWorkspaceKeysStorage = this.persistentStateFactory.createGlobalPersistentState( + workspaceKeysForWhichTheCopyIsDone_Key, + [] + ); + const flaggedWorkspaceKeys = flaggedWorkspaceKeysStorage.value; + const shouldUpdateWorkspaceSetting = !flaggedWorkspaceKeys.includes(workspaceKey); + if (shouldUpdateWorkspaceSetting) { + await this.update(resource, ConfigurationTarget.Workspace, value); + await flaggedWorkspaceKeysStorage.updateValue([workspaceKey, ...flaggedWorkspaceKeys]); + } + } + + public async _moveGlobalSettingValueToNewStorage(value: string | undefined) { + // Move global setting into the new storage if it hasn't been moved already + const isGlobalSettingCopiedStorage = this.persistentStateFactory.createGlobalPersistentState( + isGlobalSettingCopiedKey, + false + ); + const shouldUpdateGlobalSetting = !isGlobalSettingCopiedStorage.value; + if (shouldUpdateGlobalSetting) { + await this.update(undefined, ConfigurationTarget.Global, value); + // Make sure to delete the original setting after copying it + await this.workspaceService + .getConfiguration('python') + .update('pythonPath', undefined, ConfigurationTarget.Global); + await isGlobalSettingCopiedStorage.updateValue(true); + } + } +} diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts index df8451440fe9..d332dd6028e5 100644 --- a/src/client/common/logger.ts +++ b/src/client/common/logger.ts @@ -1,40 +1,10 @@ -import * as vscode from 'vscode'; -import * as settings from './configSettings' - -let outChannel: vscode.OutputChannel; -let pythonSettings: settings.IPythonSettings; - -class Logger { - static initializeChannel() { - if (pythonSettings) return; - pythonSettings = new settings.PythonSettings(); - if (pythonSettings.devOptions.indexOf("DEBUG") >= 0) { - outChannel = vscode.window.createOutputChannel('PythonExtLog'); - } - } - - static write(category: string = "log", title: string = "", message: any) { - Logger.initializeChannel(); - if (title.length > 0) { - Logger.writeLine(category, "---------------------------"); - Logger.writeLine(category, title); - } - - Logger.writeLine(category, message); - } - static writeLine(category: string = "log", line: any) { - console[category](line); - if (outChannel) { - outChannel.appendLine(line); - } - } -} -export function error(title: string = "", message: any) { - Logger.write.apply(Logger, ["error", title, message]); -} -export function warn(title: string = "", message: any) { - Logger.write.apply(Logger, ["warn", title, message]); -} -export function log(title: string = "", message: any) { - Logger.write.apply(Logger, ["log", title, message]); -} +// These are all just temporary aliases, for backward compatibility +// and to avoid churn. +export { + traceDecorators, + logError as traceError, + logInfo as traceInfo, + logVerbose as traceVerbose, + logWarning as traceWarning +} from '../logging'; +export { TraceOptions as LogOptions } from '../logging/trace'; diff --git a/src/client/common/markdown/restTextConverter.ts b/src/client/common/markdown/restTextConverter.ts new file mode 100644 index 000000000000..0881b37f3cfb --- /dev/null +++ b/src/client/common/markdown/restTextConverter.ts @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { EOL } from 'os'; +// tslint:disable-next-line:import-name +import Char from 'typescript-char'; +import { isDecimal, isWhiteSpace } from '../../language/characters'; + +enum State { + Default, + Preformatted, + Code +} + +export class RestTextConverter { + private state: State = State.Default; + private md: string[] = []; + + // tslint:disable-next-line:cyclomatic-complexity + public toMarkdown(docstring: string): string { + // Translates reStructruredText (Python doc syntax) to markdown. + // It only translates as much as needed to display tooltips + // and documentation in the completion list. + // See https://en.wikipedia.org/wiki/ReStructuredText + + const result = this.transformLines(docstring); + this.state = State.Default; + this.md = []; + + return result; + } + + public escapeMarkdown(text: string): string { + // Not complete escape list so it does not interfere + // with subsequent code highlighting (see above). + return text.replace(/\#/g, '\\#').replace(/\*/g, '\\*').replace(/\ _/g, ' \\_').replace(/^_/, '\\_'); + } + + private transformLines(docstring: string): string { + const lines = docstring.split(/\r?\n/); + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + // Avoid leading empty lines + if (this.md.length === 0 && line.length === 0) { + continue; + } + + switch (this.state) { + case State.Default: + i += this.inDefaultState(lines, i); + break; + case State.Preformatted: + i += this.inPreformattedState(lines, i); + break; + case State.Code: + this.inCodeState(line); + break; + default: + break; + } + } + + this.endCodeBlock(); + this.endPreformattedBlock(); + + return this.md.join(EOL).trim(); + } + + private inDefaultState(lines: string[], i: number): number { + let line = lines[i]; + if (line.startsWith('```')) { + this.startCodeBlock(); + return 0; + } + + if (line.startsWith('===') || line.startsWith('---')) { + return 0; // Eat standalone === or --- lines. + } + if (this.handleDoubleColon(line)) { + return 0; + } + if (this.isIgnorable(line)) { + return 0; + } + + if (this.handleSectionHeader(lines, i)) { + return 1; // Eat line with === or --- + } + + const result = this.checkPreContent(lines, i); + if (this.state !== State.Default) { + return result; // Handle line in the new state + } + + line = this.cleanup(line); + line = line.replace(/``/g, '`'); // Convert double backticks to single. + line = this.escapeMarkdown(line); + this.md.push(line); + + return 0; + } + + private inPreformattedState(lines: string[], i: number): number { + let line = lines[i]; + if (this.isIgnorable(line)) { + return 0; + } + // Preformatted block terminates by a line without leading whitespace. + if (line.length > 0 && !isWhiteSpace(line.charCodeAt(0)) && !this.isListItem(line)) { + this.endPreformattedBlock(); + return -1; + } + + const prevLine = this.md.length > 0 ? this.md[this.md.length - 1] : undefined; + if (line.length === 0 && prevLine && (prevLine.length === 0 || prevLine.startsWith('```'))) { + return 0; // Avoid more than one empty line in a row. + } + + // Since we use HTML blocks as preformatted text + // make sure we drop angle brackets since otherwise + // they will render as tags and attributes + line = line.replace(//g, ' '); + line = line.replace(/``/g, '`'); // Convert double backticks to single. + // Keep hard line breaks for the preformatted content + this.md.push(`${line} `); + return 0; + } + + private inCodeState(line: string): void { + const prevLine = this.md.length > 0 ? this.md[this.md.length - 1] : undefined; + if (line.length === 0 && prevLine && (prevLine.length === 0 || prevLine.startsWith('```'))) { + return; // Avoid more than one empty line in a row. + } + + if (line.startsWith('```')) { + this.endCodeBlock(); + } else { + this.md.push(line); + } + } + + private isIgnorable(line: string): boolean { + if (line.indexOf('generated/') >= 0) { + return true; // Drop generated content. + } + const trimmed = line.trim(); + if (trimmed.startsWith('..') && trimmed.indexOf('::') > 0) { + // Ignore lines likes .. sectionauthor:: John Doe. + return true; + } + return false; + } + + private checkPreContent(lines: string[], i: number): number { + const line = lines[i]; + if (i === 0 || line.trim().length === 0) { + return 0; + } + + if (!isWhiteSpace(line.charCodeAt(0)) && !this.isListItem(line)) { + return 0; // regular line, nothing to do here. + } + // Indented content is considered to be preformatted. + this.startPreformattedBlock(); + return -1; + } + + private handleSectionHeader(lines: string[], i: number): boolean { + const line = lines[i]; + if (i < lines.length - 1 && lines[i + 1].startsWith('===')) { + // Section title -> heading level 3. + this.md.push(`### ${this.cleanup(line)}`); + return true; + } + if (i < lines.length - 1 && lines[i + 1].startsWith('---')) { + // Subsection title -> heading level 4. + this.md.push(`#### ${this.cleanup(line)}`); + return true; + } + return false; + } + + private handleDoubleColon(line: string): boolean { + if (!line.endsWith('::')) { + return false; + } + // Literal blocks begin with `::`. Such as sequence like + // '... as shown below::' that is followed by a preformatted text. + if (line.length > 2 && !line.startsWith('..')) { + // Ignore lines likes .. autosummary:: John Doe. + // Trim trailing : so :: turns into :. + this.md.push(line.substring(0, line.length - 1)); + } + + this.startPreformattedBlock(); + return true; + } + + private startPreformattedBlock(): void { + // Remove previous empty line so we avoid double empties. + this.tryRemovePrecedingEmptyLines(); + // Lie about the language since we don't want preformatted text + // to be colorized as Python. HTML is more 'appropriate' as it does + // not colorize -- or + or keywords like 'from'. + this.md.push('```html'); + this.state = State.Preformatted; + } + + private endPreformattedBlock(): void { + if (this.state === State.Preformatted) { + this.tryRemovePrecedingEmptyLines(); + this.md.push('```'); + this.state = State.Default; + } + } + + private startCodeBlock(): void { + // Remove previous empty line so we avoid double empties. + this.tryRemovePrecedingEmptyLines(); + this.md.push('```python'); + this.state = State.Code; + } + + private endCodeBlock(): void { + if (this.state === State.Code) { + this.tryRemovePrecedingEmptyLines(); + this.md.push('```'); + this.state = State.Default; + } + } + + private tryRemovePrecedingEmptyLines(): void { + while (this.md.length > 0 && this.md[this.md.length - 1].trim().length === 0) { + this.md.pop(); + } + } + + private isListItem(line: string): boolean { + const trimmed = line.trim(); + const ch = trimmed.length > 0 ? trimmed.charCodeAt(0) : 0; + return ch === Char.Asterisk || ch === Char.Hyphen || isDecimal(ch); + } + + private cleanup(line: string): string { + return line.replace(/:mod:/g, 'module:'); + } +} diff --git a/src/client/common/net/browser.ts b/src/client/common/net/browser.ts new file mode 100644 index 000000000000..74c629c7d592 --- /dev/null +++ b/src/client/common/net/browser.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-var-requires + +import { injectable } from 'inversify'; +import { env, Uri } from 'vscode'; +import { IBrowserService } from '../types'; + +export function launch(url: string) { + env.openExternal(Uri.parse(url)); +} + +@injectable() +export class BrowserService implements IBrowserService { + public launch(url: string): void { + launch(url); + } +} diff --git a/src/client/common/net/fileDownloader.ts b/src/client/common/net/fileDownloader.ts new file mode 100644 index 000000000000..61499d2c28b7 --- /dev/null +++ b/src/client/common/net/fileDownloader.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as requestTypes from 'request'; +import { Progress } from 'vscode'; +import { IApplicationShell } from '../application/types'; +import { Octicons } from '../constants'; +import { IFileSystem, WriteStream } from '../platform/types'; +import { DownloadOptions, IFileDownloader, IHttpClient } from '../types'; +import { Http } from '../utils/localize'; +import { noop } from '../utils/misc'; + +@injectable() +export class FileDownloader implements IFileDownloader { + constructor( + @inject(IHttpClient) private readonly httpClient: IHttpClient, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IApplicationShell) private readonly appShell: IApplicationShell + ) {} + public async downloadFile(uri: string, options: DownloadOptions): Promise { + if (options.outputChannel) { + options.outputChannel.appendLine(Http.downloadingFile().format(uri)); + } + const tempFile = await this.fs.createTemporaryFile(options.extension); + + await this.downloadFileWithStatusBarProgress(uri, options.progressMessagePrefix, tempFile.filePath).then( + noop, + (ex) => { + tempFile.dispose(); + return Promise.reject(ex); + } + ); + + return tempFile.filePath; + } + public async downloadFileWithStatusBarProgress( + uri: string, + progressMessage: string, + tmpFilePath: string + ): Promise { + await this.appShell.withProgressCustomIcon(Octicons.Downloading, async (progress) => { + const req = await this.httpClient.downloadFile(uri); + const fileStream = this.fs.createWriteStream(tmpFilePath); + return this.displayDownloadProgress(uri, progress, req, fileStream, progressMessage); + }); + } + + public async displayDownloadProgress( + uri: string, + progress: Progress<{ message?: string; increment?: number }>, + request: requestTypes.Request, + fileStream: WriteStream, + progressMessagePrefix: string + ): Promise { + return new Promise((resolve, reject) => { + request.on('response', (response) => { + if (response.statusCode !== 200) { + reject( + new Error(`Failed with status ${response.statusCode}, ${response.statusMessage}, Uri ${uri}`) + ); + } + }); + // tslint:disable-next-line: no-require-imports + const requestProgress = require('request-progress'); + requestProgress(request) + .on('progress', (state: RequestProgressState) => { + const message = formatProgressMessageWithState(progressMessagePrefix, state); + progress.report({ message }); + }) + // Handle errors from download. + .on('error', reject) + .pipe(fileStream) + // Handle error in writing to fs. + .on('error', reject) + .on('close', resolve); + }); + } +} + +type RequestProgressState = { + percent: number; + speed: number; + size: { + total: number; + transferred: number; + }; + time: { + elapsed: number; + remaining: number; + }; +}; + +function formatProgressMessageWithState(progressMessagePrefix: string, state: RequestProgressState): string { + const received = Math.round(state.size.transferred / 1024); + const total = Math.round(state.size.total / 1024); + const percentage = Math.round(100 * state.percent); + + return Http.downloadingFileProgress().format( + progressMessagePrefix, + received.toString(), + total.toString(), + percentage.toString() + ); +} diff --git a/src/client/common/net/httpClient.ts b/src/client/common/net/httpClient.ts new file mode 100644 index 000000000000..adceb9b1e8a3 --- /dev/null +++ b/src/client/common/net/httpClient.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { parse, ParseError } from 'jsonc-parser'; +import type * as requestTypes from 'request'; +import { IHttpClient } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IWorkspaceService } from '../application/types'; +import { traceError } from '../logger'; + +@injectable() +export class HttpClient implements IHttpClient { + public readonly requestOptions: requestTypes.CoreOptions; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + const workspaceService = serviceContainer.get(IWorkspaceService); + this.requestOptions = { proxy: workspaceService.getConfiguration('http').get('proxy', '') }; + } + + public async downloadFile(uri: string): Promise { + // tslint:disable-next-line:no-any + const request = ((await import('request')) as any) as typeof requestTypes; + return request(uri, this.requestOptions); + } + + public async getJSON(uri: string, strict: boolean = true): Promise { + const body = await this.getContents(uri); + return this.parseBodyToJSON(body, strict); + } + + public async parseBodyToJSON(body: string, strict: boolean): Promise { + if (strict) { + return JSON.parse(body); + } else { + // tslint:disable-next-line: prefer-const + let errors: ParseError[] = []; + const content = parse(body, errors, { allowTrailingComma: true, disallowComments: false }) as T; + if (errors.length > 0) { + traceError('JSONC parser returned ParseError codes', errors); + } + return content; + } + } + + public async exists(uri: string): Promise { + // tslint:disable-next-line:no-require-imports + const request = require('request') as typeof requestTypes; + return new Promise((resolve) => { + try { + request + .get(uri, this.requestOptions) + .on('response', (response) => resolve(response.statusCode === 200)) + .on('error', () => resolve(false)); + } catch { + resolve(false); + } + }); + } + private async getContents(uri: string): Promise { + // tslint:disable-next-line:no-require-imports + const request = require('request') as typeof requestTypes; + return new Promise((resolve, reject) => { + request(uri, this.requestOptions, (ex, response, body) => { + if (ex) { + return reject(ex); + } + if (response.statusCode !== 200) { + return reject( + new Error(`Failed with status ${response.statusCode}, ${response.statusMessage}, Uri ${uri}`) + ); + } + resolve(body); + }); + }); + } +} diff --git a/src/client/common/net/socket/SocketStream.ts b/src/client/common/net/socket/SocketStream.ts new file mode 100644 index 000000000000..0576d3026fb5 --- /dev/null +++ b/src/client/common/net/socket/SocketStream.ts @@ -0,0 +1,238 @@ +'use strict'; + +import * as net from 'net'; +// tslint:disable:no-var-requires no-require-imports member-ordering no-any +const uint64be = require('uint64be'); + +enum DataType { + string, + int32, + int64 +} + +export class SocketStream { + constructor(socket: net.Socket, buffer: Buffer) { + this.buffer = buffer; + this.socket = socket; + } + + private socket: net.Socket; + public WriteInt32(num: number) { + this.WriteInt64(num); + } + + public WriteInt64(num: number) { + const buffer = uint64be.encode(num); + this.socket.write(buffer); + } + public WriteString(value: string) { + const stringBuffer = new Buffer(value, 'utf-8'); + this.WriteInt32(stringBuffer.length); + if (stringBuffer.length > 0) { + this.socket.write(stringBuffer); + } + } + public Write(buffer: Buffer) { + this.socket.write(buffer); + } + + private buffer: Buffer; + private isInTransaction!: boolean; + private bytesRead: number = 0; + public get Buffer(): Buffer { + return this.buffer; + } + public BeginTransaction() { + this.isInTransaction = true; + this.bytesRead = 0; + this.ClearErrors(); + } + public EndTransaction() { + this.isInTransaction = false; + this.buffer = this.buffer.slice(this.bytesRead); + this.bytesRead = 0; + this.ClearErrors(); + } + public RollBackTransaction() { + this.isInTransaction = false; + this.bytesRead = 0; + this.ClearErrors(); + } + + public ClearErrors() { + this.hasInsufficientDataForReading = false; + } + + private hasInsufficientDataForReading: boolean = false; + public get HasInsufficientDataForReading(): boolean { + return this.hasInsufficientDataForReading; + } + + public toString(): string { + return this.buffer.toString(); + } + + public get Length(): number { + return this.buffer.length; + } + + public Append(additionalData: Buffer) { + if (this.buffer.length === 0) { + this.buffer = additionalData; + return; + } + const newBuffer = new Buffer(this.buffer.length + additionalData.length); + this.buffer.copy(newBuffer); + additionalData.copy(newBuffer, this.buffer.length); + this.buffer = newBuffer; + } + + private isSufficientDataAvailable(length: number): boolean { + if (this.buffer.length < this.bytesRead + length) { + this.hasInsufficientDataForReading = true; + } + + return !this.hasInsufficientDataForReading; + } + + public ReadByte(): number { + if (!this.isSufficientDataAvailable(1)) { + return null as any; + } + + const value = this.buffer.slice(this.bytesRead, this.bytesRead + 1)[0]; + if (this.isInTransaction) { + this.bytesRead += 1; + } else { + this.buffer = this.buffer.slice(1); + } + return value; + } + + public ReadString(): string { + const byteRead = this.ReadByte(); + if (this.HasInsufficientDataForReading) { + return null as any; + } + + if (byteRead < 0) { + throw new Error('IOException() - Socket.ReadString failed to read string type;'); + } + + const type = new Buffer([byteRead]).toString(); + let isUnicode = false; + switch (type) { + case 'N': // null string + return null as any; + case 'U': + isUnicode = true; + break; + case 'A': { + isUnicode = false; + break; + } + default: { + throw new Error(`IOException(); Socket.ReadString failed to parse unknown string type ${type}`); + } + } + + const len = this.ReadInt32(); + if (this.HasInsufficientDataForReading) { + return null as any; + } + + if (!this.isSufficientDataAvailable(len)) { + return null as any; + } + + const stringBuffer = this.buffer.slice(this.bytesRead, this.bytesRead + len); + if (this.isInTransaction) { + this.bytesRead = this.bytesRead + len; + } else { + this.buffer = this.buffer.slice(len); + } + + return isUnicode ? stringBuffer.toString('utf-8') : stringBuffer.toString(); + } + + public ReadInt32(): number { + return this.ReadInt64(); + } + + public ReadInt64(): number { + if (!this.isSufficientDataAvailable(8)) { + return null as any; + } + + const buf = this.buffer.slice(this.bytesRead, this.bytesRead + 8); + + if (this.isInTransaction) { + this.bytesRead = this.bytesRead + 8; + } else { + this.buffer = this.buffer.slice(8); + } + + return uint64be.decode(buf); + } + + public ReadAsciiString(length: number): string { + if (!this.isSufficientDataAvailable(length)) { + return null as any; + } + + const stringBuffer = this.buffer.slice(this.bytesRead, this.bytesRead + length); + if (this.isInTransaction) { + this.bytesRead = this.bytesRead + length; + } else { + this.buffer = this.buffer.slice(length); + } + return stringBuffer.toString('ascii'); + } + + private readValueInTransaction(dataType: DataType): T { + let startedTransaction = false; + if (!this.isInTransaction) { + this.BeginTransaction(); + startedTransaction = true; + } + let data: any; + switch (dataType) { + case DataType.string: { + data = this.ReadString(); + break; + } + case DataType.int32: { + data = this.ReadInt32(); + break; + } + case DataType.int64: { + data = this.ReadInt64(); + break; + } + default: { + break; + } + } + if (this.HasInsufficientDataForReading) { + if (startedTransaction) { + this.RollBackTransaction(); + } + return undefined as any; + } + if (startedTransaction) { + this.EndTransaction(); + } + return data; + } + public readStringInTransaction(): string { + return this.readValueInTransaction(DataType.string); + } + + public readInt32InTransaction(): number { + return this.readValueInTransaction(DataType.int32); + } + + public readInt64InTransaction(): number { + return this.readValueInTransaction(DataType.int64); + } +} diff --git a/src/client/common/net/socket/socketCallbackHandler.ts b/src/client/common/net/socket/socketCallbackHandler.ts new file mode 100644 index 000000000000..55bf49e7b27d --- /dev/null +++ b/src/client/common/net/socket/socketCallbackHandler.ts @@ -0,0 +1,93 @@ +// tslint:disable:quotemark ordered-imports member-ordering one-line prefer-const + +'use strict'; + +import * as net from 'net'; +import { EventEmitter } from 'events'; +import { SocketStream } from './SocketStream'; +import { SocketServer } from './socketServer'; + +export abstract class SocketCallbackHandler extends EventEmitter { + private _stream!: SocketStream; + private commandHandlers: Map; + private handeshakeDone!: boolean; + + constructor(socketServer: SocketServer) { + super(); + this.commandHandlers = new Map(); + socketServer.on('data', this.onData.bind(this)); + } + private disposed!: boolean; + public dispose() { + this.disposed = true; + this.commandHandlers.clear(); + } + private onData(socketClient: net.Socket, data: Buffer) { + if (this.disposed) { + return; + } + this.HandleIncomingData(data, socketClient); + } + + protected get stream(): SocketStream { + return this._stream; + } + + protected SendRawCommand(commandId: Buffer) { + this.stream.Write(commandId); + } + + protected registerCommandHandler(commandId: string, handler: Function) { + this.commandHandlers.set(commandId, handler); + } + + protected abstract handleHandshake(): boolean; + + private HandleIncomingData(buffer: Buffer, socket: net.Socket): boolean | undefined { + if (!this._stream) { + this._stream = new SocketStream(socket, buffer); + } else { + this._stream.Append(buffer); + } + + if (!this.handeshakeDone && !this.handleHandshake()) { + return; + } + + this.handeshakeDone = true; + + this.HandleIncomingDataFromStream(); + return true; + } + + private HandleIncomingDataFromStream() { + if (this.stream.Length === 0) { + return; + } + this.stream.BeginTransaction(); + + let cmd = this.stream.ReadAsciiString(4); + if (this.stream.HasInsufficientDataForReading) { + this.stream.RollBackTransaction(); + return; + } + + if (this.commandHandlers.has(cmd)) { + const handler = this.commandHandlers.get(cmd)!; + handler(); + } else { + this.emit('error', `Unhandled command '${cmd}'`); + } + + if (this.stream.HasInsufficientDataForReading) { + // Most possibly due to insufficient data + this.stream.RollBackTransaction(); + return; + } + + this.stream.EndTransaction(); + if (this.stream.Length > 0) { + this.HandleIncomingDataFromStream(); + } + } +} diff --git a/src/client/common/net/socket/socketServer.ts b/src/client/common/net/socket/socketServer.ts new file mode 100644 index 000000000000..ed0b37367f42 --- /dev/null +++ b/src/client/common/net/socket/socketServer.ts @@ -0,0 +1,67 @@ +import { EventEmitter } from 'events'; +import { injectable } from 'inversify'; +import * as net from 'net'; +import { ISocketServer } from '../../types'; +import { createDeferred, Deferred } from '../../utils/async'; +import { noop } from '../../utils/misc'; + +@injectable() +export class SocketServer extends EventEmitter implements ISocketServer { + private socketServer: net.Server | undefined; + private clientSocket: Deferred; + public get client(): Promise { + return this.clientSocket.promise; + } + constructor() { + super(); + this.clientSocket = createDeferred(); + } + public dispose() { + this.Stop(); + } + public Stop() { + if (!this.socketServer) { + return; + } + try { + this.socketServer.close(); + // tslint:disable-next-line:no-empty + } catch (ex) {} + this.socketServer = undefined; + } + + public Start(options: { port?: number; host?: string } = {}): Promise { + const def = createDeferred(); + this.socketServer = net.createServer(this.connectionListener.bind(this)); + + const port = typeof options.port === 'number' ? options.port! : 0; + const host = typeof options.host === 'string' ? options.host! : 'localhost'; + this.socketServer!.on('error', (ex) => { + const msg = `Failed to start the socket server. (Error: ${ex.message})`; + + def.reject(msg); + }); + this.socketServer!.listen({ port, host }, () => { + def.resolve((this.socketServer!.address() as net.AddressInfo).port); + }); + + return def.promise; + } + + private connectionListener(client: net.Socket) { + if (!this.clientSocket.completed) { + this.clientSocket.resolve(client); + } + client.on('close', () => { + this.emit('close', client); + }); + client.on('data', (data: Buffer) => { + this.emit('data', client, data); + }); + client.on('error', () => noop); + + client.on('timeout', () => { + // let msg = "Debugger client timedout, " + d; + }); + } +} diff --git a/src/client/common/nuget/azureBlobStoreNugetRepository.ts b/src/client/common/nuget/azureBlobStoreNugetRepository.ts new file mode 100644 index 000000000000..ae19c65e81be --- /dev/null +++ b/src/client/common/nuget/azureBlobStoreNugetRepository.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable, unmanaged } from 'inversify'; +import { IServiceContainer } from '../../ioc/types'; +import { captureTelemetry } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IWorkspaceService } from '../application/types'; +import { traceDecorators } from '../logger'; +import { Resource } from '../types'; +import { INugetRepository, INugetService, NugetPackage } from './types'; + +@injectable() +export class AzureBlobStoreNugetRepository implements INugetRepository { + constructor( + @unmanaged() private readonly serviceContainer: IServiceContainer, + @unmanaged() protected readonly azureBlobStorageAccount: string, + @unmanaged() protected readonly azureBlobStorageContainer: string, + @unmanaged() protected readonly azureCDNBlobStorageAccount: string, + private getBlobStore: (uri: string) => Promise = _getAZBlobStore + ) {} + + public async getPackages(packageName: string, resource: Resource): Promise { + return this.listPackages( + this.azureBlobStorageAccount, + this.azureBlobStorageContainer, + packageName, + this.azureCDNBlobStorageAccount, + resource + ); + } + + @captureTelemetry(EventName.PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES) + @traceDecorators.verbose('Listing Nuget Packages') + protected async listPackages( + azureBlobStorageAccount: string, + azureBlobStorageContainer: string, + packageName: string, + azureCDNBlobStorageAccount: string, + resource: Resource + ) { + const results = await this.listBlobStoreCatalog( + this.fixBlobStoreURI(azureBlobStorageAccount, resource), + azureBlobStorageContainer, + packageName + ); + const nugetService = this.serviceContainer.get(INugetService); + return results.map((item) => { + return { + package: item.name, + uri: `${azureCDNBlobStorageAccount}/${azureBlobStorageContainer}/${item.name}`, + version: nugetService.getVersionFromPackageFileName(item.name) + }; + }); + } + + private async listBlobStoreCatalog( + azureBlobStorageAccount: string, + azureBlobStorageContainer: string, + packageName: string + ): Promise { + const blobStore = await this.getBlobStore(azureBlobStorageAccount); + return new Promise((resolve, reject) => { + // We must pass undefined according to docs, but type definition doesn't all it to be undefined or null!!! + // tslint:disable-next-line:no-any + const token = undefined as any; + blobStore.listBlobsSegmentedWithPrefix(azureBlobStorageContainer, packageName, token, (error, result) => { + if (error) { + return reject(error); + } + resolve(result.entries); + }); + }); + } + private fixBlobStoreURI(uri: string, resource: Resource) { + if (!uri.startsWith('https:')) { + return uri; + } + + const workspace = this.serviceContainer.get(IWorkspaceService); + const cfg = workspace.getConfiguration('http', resource); + if (cfg.get('proxyStrictSSL', true)) { + return uri; + } + + // tslint:disable-next-line:no-http-string + return uri.replace(/^https:/, 'http:'); + } +} + +// The "azure-storage" package is large enough that importing it has +// a significant impact on extension startup time. So we import it +// lazily and deal with the consequences below. + +interface IBlobResult { + name: string; +} + +interface IBlobResults { + entries: IBlobResult[]; +} + +type ErrorOrResult = (error: Error, result: TResult) => void; + +interface IAZBlobStore { + listBlobsSegmentedWithPrefix( + container: string, + prefix: string, + // tslint:disable-next-line:no-any + currentToken: any, + callback: ErrorOrResult + ): void; +} + +async function _getAZBlobStore(uri: string): Promise { + // tslint:disable-next-line:no-require-imports + const az = (await import('azure-storage')) as typeof import('azure-storage'); + return az.createBlobServiceAnonymous(uri); +} diff --git a/src/client/common/nuget/nugetRepository.ts b/src/client/common/nuget/nugetRepository.ts new file mode 100644 index 000000000000..08524d7c3068 --- /dev/null +++ b/src/client/common/nuget/nugetRepository.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { parse, SemVer } from 'semver'; +import { IHttpClient } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { INugetRepository, NugetPackage } from './types'; + +const nugetPackageBaseAddress = + 'https://dotnetmyget.blob.core.windows.net/artifacts/dotnet-core-svc/nuget/v3/flatcontainer'; + +@injectable() +export class NugetRepository implements INugetRepository { + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} + public async getPackages(packageName: string): Promise { + const versions = await this.getVersions(nugetPackageBaseAddress, packageName); + return versions.map((version) => { + const uri = this.getNugetPackageUri(nugetPackageBaseAddress, packageName, version); + return { version, uri, package: packageName }; + }); + } + public async getVersions(packageBaseAddress: string, packageName: string): Promise { + const uri = `${packageBaseAddress}/${packageName.toLowerCase().trim()}/index.json`; + const httpClient = this.serviceContainer.get(IHttpClient); + const result = await httpClient.getJSON<{ versions: string[] }>(uri); + return result.versions.map((v) => parse(v, true) || new SemVer('0.0.0')); + } + public getNugetPackageUri(packageBaseAddress: string, packageName: string, version: SemVer): string { + return `${packageBaseAddress}/${packageName}/${version.raw}/${packageName}.${version.raw}.nupkg`; + } +} diff --git a/src/client/common/nuget/nugetService.ts b/src/client/common/nuget/nugetService.ts new file mode 100644 index 000000000000..a0a974dfd64d --- /dev/null +++ b/src/client/common/nuget/nugetService.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import * as path from 'path'; +import { parse, SemVer } from 'semver'; +import { INugetService } from './types'; + +@injectable() +export class NugetService implements INugetService { + public isReleaseVersion(version: SemVer): boolean { + return version.prerelease.length === 0; + } + + public getVersionFromPackageFileName(packageName: string): SemVer { + const ext = path.extname(packageName); + const versionWithExt = packageName.substring(packageName.indexOf('.') + 1); + const version = versionWithExt.substring(0, versionWithExt.length - ext.length); + // Take only the first 3 parts. + const parts = version.split('.'); + const semverParts = parts.filter((_, index) => index <= 2).join('.'); + const lastParts = parts.filter((_, index) => index === 3).join('.'); + const suffix = lastParts.length === 0 ? '' : `-${lastParts}`; + const fixedVersion = `${semverParts}${suffix}`; + return parse(fixedVersion, true) || new SemVer('0.0.0'); + } +} diff --git a/src/client/common/nuget/types.ts b/src/client/common/nuget/types.ts new file mode 100644 index 000000000000..dcde51025138 --- /dev/null +++ b/src/client/common/nuget/types.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { SemVer } from 'semver'; +import { Resource } from '../types'; +export type NugetPackage = { package: string; version: SemVer; uri: string }; + +export const INugetService = Symbol('INugetService'); +export interface INugetService { + isReleaseVersion(version: SemVer): boolean; + getVersionFromPackageFileName(packageName: string): SemVer; +} + +export const INugetRepository = Symbol('INugetRepository'); +export interface INugetRepository { + getPackages(packageName: string, resource: Resource): Promise; +} diff --git a/src/client/common/open.ts b/src/client/common/open.ts index 8f215c4c11ef..648cfa8029a9 100644 --- a/src/client/common/open.ts +++ b/src/client/common/open.ts @@ -3,23 +3,23 @@ //https://github.com/sindresorhus/opn/blob/master/index.js //Modified as this uses target as an argument -import * as path from 'path'; import * as childProcess from 'child_process'; +// tslint:disable:no-any no-function-expression prefer-template export function open(opts: any): Promise { - //opts = objectAssign({wait: true}, opts); - if (!opts.hasOwnProperty("wait")) { + // opts = objectAssign({wait: true}, opts); + if (!opts.hasOwnProperty('wait')) { (opts).wait = true; } - var cmd; - var appArgs = []; - var args = []; - var cpOpts: any = {}; - if (opts.cwd) { + let cmd; + let appArgs = []; + let args: string[] = []; + const cpOpts: any = {}; + if (opts.cwd && typeof opts.cwd === 'string' && opts.cwd.length > 0) { cpOpts.cwd = opts.cwd; } - if (opts.env) { + if (opts.env && Object.keys(opts.env).length > 0) { cpOpts.env = opts.env; } @@ -29,11 +29,18 @@ export function open(opts: any): Promise { } if (process.platform === 'darwin') { + const sudoPrefix = opts.sudo === true ? 'sudo ' : ''; cmd = 'osascript'; - args = [ '-e', 'tell application "terminal"', - '-e', 'do script "' + [opts.app].concat(appArgs).join(" ") + '"', - '-e', 'end tell' ]; - + args = [ + '-e', + 'tell application "terminal"', + '-e', + 'activate', + '-e', + 'do script "' + sudoPrefix + [opts.app].concat(appArgs).join(' ') + '"', + '-e', + 'end tell' + ]; } else if (process.platform === 'win32') { cmd = 'cmd'; args.push('/c', 'start'); @@ -50,32 +57,20 @@ export function open(opts: any): Promise { args = args.concat(appArgs); } } else { - if (opts.app) { - cmd = opts.app; - } else { - cmd = path.join(__dirname, 'xdg-open'); - } - - if (appArgs.length > 0) { - args = args.concat(appArgs); - } - - if (!opts.wait) { - // xdg-open will block the process unless - // stdio is ignored even if it's unref'd - cpOpts.stdio = 'ignore'; - } + cmd = 'gnome-terminal'; + const sudoPrefix = opts.sudo === true ? 'sudo ' : ''; + args = ['-x', 'sh', '-c', `"${sudoPrefix}${opts.app}" ${appArgs.join(' ')}`]; } - var cp = childProcess.spawn(cmd, args, cpOpts); + const cp = childProcess.spawn(cmd, args, cpOpts); if (opts.wait) { - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { cp.once('error', reject); - cp.once('close', function(code) { + cp.once('close', function (code) { if (code > 0) { - reject(new Error('Exited with code ' + code)); + reject(new Error(`Exited with code ${code}`)); return; } @@ -87,4 +82,4 @@ export function open(opts: any): Promise { cp.unref(); return Promise.resolve(cp); -}; \ No newline at end of file +} diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts new file mode 100644 index 000000000000..ed03b60ddb77 --- /dev/null +++ b/src/client/common/persistentState.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Memento } from 'vscode'; +import { GLOBAL_MEMENTO, IMemento, IPersistentState, IPersistentStateFactory, WORKSPACE_MEMENTO } from './types'; + +export class PersistentState implements IPersistentState { + constructor( + private storage: Memento, + private key: string, + private defaultValue?: T, + private expiryDurationMs?: number + ) {} + + public get value(): T { + if (this.expiryDurationMs) { + const cachedData = this.storage.get<{ data?: T; expiry?: number }>(this.key, { data: this.defaultValue! }); + if (!cachedData || !cachedData.expiry || cachedData.expiry < Date.now()) { + return this.defaultValue!; + } else { + return cachedData.data!; + } + } else { + return this.storage.get(this.key, this.defaultValue!); + } + } + + public async updateValue(newValue: T): Promise { + if (this.expiryDurationMs) { + await this.storage.update(this.key, { data: newValue, expiry: Date.now() + this.expiryDurationMs }); + } else { + await this.storage.update(this.key, newValue); + } + } +} + +@injectable() +export class PersistentStateFactory implements IPersistentStateFactory { + constructor( + @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, + @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento + ) {} + public createGlobalPersistentState( + key: string, + defaultValue?: T, + expiryDurationMs?: number + ): IPersistentState { + return new PersistentState(this.globalState, key, defaultValue, expiryDurationMs); + } + public createWorkspacePersistentState( + key: string, + defaultValue?: T, + expiryDurationMs?: number + ): IPersistentState { + return new PersistentState(this.workspaceState, key, defaultValue, expiryDurationMs); + } +} diff --git a/src/client/common/platform/constants.ts b/src/client/common/platform/constants.ts new file mode 100644 index 000000000000..2c4a65f80251 --- /dev/null +++ b/src/client/common/platform/constants.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable-next-line:no-suspicious-comment +// TODO : Drop all these in favor of IPlatformService. +// See https://github.com/microsoft/vscode-python/issues/8542. + +export const WINDOWS_PATH_VARIABLE_NAME = 'Path'; +export const NON_WINDOWS_PATH_VARIABLE_NAME = 'PATH'; +export const IS_WINDOWS = /^win/.test(process.platform); diff --git a/src/client/common/platform/errors.ts b/src/client/common/platform/errors.ts new file mode 100644 index 000000000000..8d3fe1b6bbe3 --- /dev/null +++ b/src/client/common/platform/errors.ts @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as vscode from 'vscode'; + +/* +See: + + https://nodejs.org/api/errors.html + + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + + node_modules/@types/node/globals.d.ts + */ + +interface IError { + name: string; + message: string; + + toString(): string; +} + +interface INodeJSError extends IError { + code: string; + stack?: string; + stackTraceLimit: number; + + captureStackTrace(): void; +} + +//================================ +// "system" errors + +namespace vscErrors { + const FILE_NOT_FOUND = vscode.FileSystemError.FileNotFound().name; + const FILE_EXISTS = vscode.FileSystemError.FileExists().name; + const IS_DIR = vscode.FileSystemError.FileIsADirectory().name; + const NOT_DIR = vscode.FileSystemError.FileNotADirectory().name; + const NO_PERM = vscode.FileSystemError.NoPermissions().name; + const known = [ + // (order does not matter) + FILE_NOT_FOUND, + FILE_EXISTS, + IS_DIR, + NOT_DIR, + NO_PERM + ]; + function errorMatches(err: Error, expectedName: string): boolean | undefined { + if (!known.includes(err.name)) { + return undefined; + } + return err.name === expectedName; + } + + export function isFileNotFound(err: Error): boolean | undefined { + return errorMatches(err, FILE_NOT_FOUND); + } + export function isFileExists(err: Error): boolean | undefined { + return errorMatches(err, FILE_EXISTS); + } + export function isFileIsDir(err: Error): boolean | undefined { + return errorMatches(err, IS_DIR); + } + export function isNotDir(err: Error): boolean | undefined { + return errorMatches(err, NOT_DIR); + } + export function isNoPermissions(err: Error): boolean | undefined { + return errorMatches(err, NO_PERM); + } +} + +interface ISystemError extends INodeJSError { + errno: number; + syscall: string; + info?: string; + path?: string; + address?: string; + dest?: string; + port?: string; +} + +// Return a new error for errno ENOTEMPTY. +export function createDirNotEmptyError(dirname: string): ISystemError { + const err = new Error(`directory "${dirname}" not empty`) as ISystemError; + err.name = 'SystemError'; + err.code = 'ENOTEMPTY'; + err.path = dirname; + err.syscall = 'rmdir'; + return err; +} + +function isSystemError(err: Error, expectedCode: string): boolean | undefined { + const code = (err as ISystemError).code; + if (!code) { + return undefined; + } + return code === expectedCode; +} + +// Return true if the given error is ENOENT. +export function isFileNotFoundError(err: Error): boolean | undefined { + const matched = vscErrors.isFileNotFound(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'ENOENT'); +} + +// Return true if the given error is EEXIST. +export function isFileExistsError(err: Error): boolean | undefined { + const matched = vscErrors.isFileExists(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'EEXIST'); +} + +// Return true if the given error is EISDIR. +export function isFileIsDirError(err: Error): boolean | undefined { + const matched = vscErrors.isFileIsDir(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'EISDIR'); +} + +// Return true if the given error is ENOTDIR. +export function isNotDirError(err: Error): boolean | undefined { + const matched = vscErrors.isNotDir(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'ENOTDIR'); +} + +// Return true if the given error is EACCES. +export function isNoPermissionsError(err: Error): boolean | undefined { + const matched = vscErrors.isNoPermissions(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'EACCES'); +} + +// Return true if the given error is ENOTEMPTY. +export function isDirNotEmptyError(err: Error): boolean | undefined { + return isSystemError(err, 'ENOTEMPTY'); +} diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts new file mode 100644 index 000000000000..c79075788416 --- /dev/null +++ b/src/client/common/platform/fileSystem.ts @@ -0,0 +1,588 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// tslint:disable:no-suspicious-comment + +import { createHash } from 'crypto'; +import * as fs from 'fs-extra'; +import * as glob from 'glob'; +import { injectable } from 'inversify'; +import { promisify } from 'util'; +import * as vscode from 'vscode'; +import '../../common/extensions'; +import { traceError } from '../logger'; +import { createDirNotEmptyError, isFileExistsError, isFileNotFoundError, isNoPermissionsError } from './errors'; +import { FileSystemPaths, FileSystemPathUtils } from './fs-paths'; +import { TemporaryFileSystem } from './fs-temp'; +import { + FileStat, + FileType, + IFileSystem, + IFileSystemPaths, + IFileSystemPathUtils, + IFileSystemUtils, + IRawFileSystem, + ITempFileSystem, + ReadStream, + TemporaryFile, + WriteStream +} from './types'; + +const ENCODING: string = 'utf8'; + +// This helper function determines the file type of the given stats +// object. The type follows the convention of node's fs module, where +// a file has exactly one type. Symlinks are not resolved. +export function convertFileType(stat: fs.Stats): FileType { + if (stat.isFile()) { + return FileType.File; + } else if (stat.isDirectory()) { + return FileType.Directory; + } else if (stat.isSymbolicLink()) { + // The caller is responsible for combining this ("logical or") + // with File or Directory as necessary. + return FileType.SymbolicLink; + } else { + return FileType.Unknown; + } +} + +export function convertStat(old: fs.Stats, filetype: FileType): FileStat { + return { + type: filetype, + size: old.size, + // FileStat.ctime and FileStat.mtime only have 1-millisecond + // resolution, while node provides nanosecond resolution. So + // for now we round to the nearest integer. + // See: https://github.com/microsoft/vscode/issues/84526 + ctime: Math.round(old.ctimeMs), + mtime: Math.round(old.mtimeMs) + }; +} + +function filterByFileType( + files: [string, FileType][], // the files to filter + fileType: FileType // the file type to look for +): [string, FileType][] { + // We preserve the pre-existing behavior of following symlinks. + if (fileType === FileType.Unknown) { + // FileType.Unknown == 0 so we can't just use bitwise + // operations blindly here. + return files.filter(([_file, ft]) => { + return ft === FileType.Unknown || ft === (FileType.SymbolicLink & FileType.Unknown); + }); + } else { + return files.filter(([_file, ft]) => (ft & fileType) > 0); + } +} + +//========================================== +// "raw" filesystem + +// This is the parts of the vscode.workspace.fs API that we use here. +// See: https://code.visualstudio.com/api/references/vscode-api#FileSystem +// Note that we have used all the API functions *except* "rename()". +interface IVSCodeFileSystemAPI { + copy(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable; + createDirectory(uri: vscode.Uri): Thenable; + delete(uri: vscode.Uri, options?: { recursive: boolean; useTrash: boolean }): Thenable; + readDirectory(uri: vscode.Uri): Thenable<[string, FileType][]>; + readFile(uri: vscode.Uri): Thenable; + rename(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable; + stat(uri: vscode.Uri): Thenable; + writeFile(uri: vscode.Uri, content: Uint8Array): Thenable; +} + +// This is the parts of the 'fs-extra' module that we use in RawFileSystem. +interface IRawFSExtra { + lstat(filename: string): Promise; + chmod(filePath: string, mode: string | number): Promise; + appendFile(filename: string, data: {}): Promise; + + // non-async + lstatSync(filename: string): fs.Stats; + statSync(filename: string): fs.Stats; + readFileSync(path: string, encoding: string): string; + createReadStream(filename: string): ReadStream; + createWriteStream(filename: string): WriteStream; +} + +interface IRawPath { + dirname(path: string): string; + join(...paths: string[]): string; +} + +// Later we will drop "FileSystem", switching usage to +// "FileSystemUtils" and then rename "RawFileSystem" to "FileSystem". + +// The low-level filesystem operations used by the extension. +export class RawFileSystem implements IRawFileSystem { + constructor( + // the low-level FS path operations to use + protected readonly paths: IRawPath, + // the VS Code FS API to use + protected readonly vscfs: IVSCodeFileSystemAPI, + // the node FS API to use + protected readonly fsExtra: IRawFSExtra + ) {} + + // Create a new object using common-case default values. + public static withDefaults( + paths?: IRawPath, // default: a new FileSystemPaths object (using defaults) + vscfs?: IVSCodeFileSystemAPI, // default: the actual "vscode.workspace.fs" namespace + fsExtra?: IRawFSExtra // default: the "fs-extra" module + ): RawFileSystem { + return new RawFileSystem( + paths || FileSystemPaths.withDefaults(), + vscfs || vscode.workspace.fs, + // The "fs-extra" module is effectively equivalent to node's "fs" + // module (but is a bit more async-friendly). So we use that + // instead of "fs". + fsExtra || fs + ); + } + + public async stat(filename: string): Promise { + // Note that, prior to the November release of VS Code, + // stat.ctime was always 0. + // See: https://github.com/microsoft/vscode/issues/84525 + const uri = vscode.Uri.file(filename); + return this.vscfs.stat(uri); + } + + public async lstat(filename: string): Promise { + // TODO https://github.com/microsoft/vscode/issues/71204 (84514)): + // This functionality has been requested for the VS Code API. + const stat = await this.fsExtra.lstat(filename); + // Note that, unlike stat(), lstat() does not include the type + // of the symlink's target. + const fileType = convertFileType(stat); + return convertStat(stat, fileType); + } + + public async chmod(filename: string, mode: string | number): Promise { + // TODO (https://github.com/microsoft/vscode/issues/73122 (84513)): + // This functionality has been requested for the VS Code API. + return this.fsExtra.chmod(filename, mode); + } + + public async move(src: string, tgt: string): Promise { + const srcUri = vscode.Uri.file(src); + const tgtUri = vscode.Uri.file(tgt); + // The VS Code API will automatically create the target parent + // directory if it does not exist (even though the docs imply + // otherwise). So we have to manually stat, just to be sure. + // Note that this behavior was reported, but won't be changing. + // See: https://github.com/microsoft/vscode/issues/84177 + await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(tgt))); + // We stick with the pre-existing behavior where files are + // overwritten and directories are not. + const options = { overwrite: false }; + try { + await this.vscfs.rename(srcUri, tgtUri, options); + } catch (err) { + if (!isFileExistsError(err)) { + throw err; // re-throw + } + const stat = await this.vscfs.stat(tgtUri); + if (stat.type === FileType.Directory) { + throw err; // re-throw + } + options.overwrite = true; + await this.vscfs.rename(srcUri, tgtUri, options); + } + } + + public async readData(filename: string): Promise { + const uri = vscode.Uri.file(filename); + const data = await this.vscfs.readFile(uri); + return Buffer.from(data); + } + + public async readText(filename: string): Promise { + const uri = vscode.Uri.file(filename); + const result = await this.vscfs.readFile(uri); + const data = Buffer.from(result); + return data.toString(ENCODING); + } + + public async writeText(filename: string, text: string): Promise { + const uri = vscode.Uri.file(filename); + const data = Buffer.from(text); + await this.vscfs.writeFile(uri, data); + } + + public async appendText(filename: string, text: string): Promise { + // TODO: We *could* use the new API for this. + // See https://github.com/microsoft/vscode-python/issues/9900 + return this.fsExtra.appendFile(filename, text); + } + + public async copyFile(src: string, dest: string): Promise { + const srcURI = vscode.Uri.file(src); + const destURI = vscode.Uri.file(dest); + // The VS Code API will automatically create the target parent + // directory if it does not exist (even though the docs imply + // otherwise). So we have to manually stat, just to be sure. + // Note that this behavior was reported, but won't be changing. + // See: https://github.com/microsoft/vscode/issues/84177 + await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(dest))); + await this.vscfs.copy(srcURI, destURI, { + overwrite: true + }); + } + + public async rmfile(filename: string): Promise { + const uri = vscode.Uri.file(filename); + return this.vscfs.delete(uri, { + recursive: false, + useTrash: false + }); + } + + public async rmdir(dirname: string): Promise { + const uri = vscode.Uri.file(dirname); + // The "recursive" option disallows directories, even if they + // are empty. So we have to deal with this ourselves. + const files = await this.vscfs.readDirectory(uri); + if (files && files.length > 0) { + throw createDirNotEmptyError(dirname); + } + return this.vscfs.delete(uri, { + recursive: true, + useTrash: false + }); + } + + public async rmtree(dirname: string): Promise { + const uri = vscode.Uri.file(dirname); + // TODO (https://github.com/microsoft/vscode/issues/84177): + // The docs say "throws - FileNotFound when uri doesn't exist". + // However, it happily does nothing. So for now we have to + // manually stat, just to be sure. + await this.vscfs.stat(uri); + return this.vscfs.delete(uri, { + recursive: true, + useTrash: false + }); + } + + public async mkdirp(dirname: string): Promise { + const uri = vscode.Uri.file(dirname); + await this.vscfs.createDirectory(uri); + } + + public async listdir(dirname: string): Promise<[string, FileType][]> { + const uri = vscode.Uri.file(dirname); + const files = await this.vscfs.readDirectory(uri); + return files.map(([basename, filetype]) => { + const filename = this.paths.join(dirname, basename); + return [filename, filetype] as [string, FileType]; + }); + } + + //**************************** + // non-async + + // VS Code has decided to never support any sync functions (aside + // from perhaps create*Stream()). + // See: https://github.com/microsoft/vscode/issues/84518 + + public statSync(filename: string): FileStat { + // We follow the filetype behavior of the VS Code API, by + // acknowledging symlinks. + let stat = this.fsExtra.lstatSync(filename); + let filetype = FileType.Unknown; + if (stat.isSymbolicLink()) { + filetype = FileType.SymbolicLink; + stat = this.fsExtra.statSync(filename); + } + filetype |= convertFileType(stat); + return convertStat(stat, filetype); + } + + public readTextSync(filename: string): string { + return this.fsExtra.readFileSync(filename, ENCODING); + } + + public createReadStream(filename: string): ReadStream { + // TODO (https://github.com/microsoft/vscode/issues/84515): + // This functionality has been requested for the VS Code API. + return this.fsExtra.createReadStream(filename); + } + + public createWriteStream(filename: string): WriteStream { + // TODO (https://github.com/microsoft/vscode/issues/84515): + // This functionality has been requested for the VS Code API. + return this.fsExtra.createWriteStream(filename); + } +} + +//========================================== +// filesystem "utils" + +// High-level filesystem operations used by the extension. +export class FileSystemUtils implements IFileSystemUtils { + constructor( + public readonly raw: IRawFileSystem, + public readonly pathUtils: IFileSystemPathUtils, + public readonly paths: IFileSystemPaths, + public readonly tmp: ITempFileSystem, + private readonly getHash: (data: string) => string, + private readonly globFiles: (pat: string, options?: { cwd: string; dot?: boolean }) => Promise + ) {} + // Create a new object using common-case default values. + public static withDefaults( + raw?: IRawFileSystem, + pathUtils?: IFileSystemPathUtils, + tmp?: ITempFileSystem, + getHash?: (data: string) => string, + globFiles?: (pat: string, options?: { cwd: string }) => Promise + ): FileSystemUtils { + pathUtils = pathUtils || FileSystemPathUtils.withDefaults(); + return new FileSystemUtils( + raw || RawFileSystem.withDefaults(pathUtils.paths), + pathUtils, + pathUtils.paths, + tmp || TemporaryFileSystem.withDefaults(), + getHash || getHashString, + globFiles || promisify(glob) + ); + } + + //**************************** + // aliases + + public async createDirectory(directoryPath: string): Promise { + return this.raw.mkdirp(directoryPath); + } + + public async deleteDirectory(directoryPath: string): Promise { + return this.raw.rmdir(directoryPath); + } + + public async deleteFile(filename: string): Promise { + return this.raw.rmfile(filename); + } + + //**************************** + // helpers + + public async pathExists( + // the "file" to look for + filename: string, + // the file type to expect; if not provided then any file type + // matches; otherwise a mismatch results in a "false" value + fileType?: FileType + ): Promise { + let stat: FileStat; + try { + // Note that we are using stat() rather than lstat(). This + // means that any symlinks are getting resolved. + stat = await this.raw.stat(filename); + } catch (err) { + if (isFileNotFoundError(err)) { + return false; + } + traceError(`stat() failed for "${filename}"`, err); + return false; + } + + if (fileType === undefined) { + return true; + } + if (fileType === FileType.Unknown) { + // FileType.Unknown == 0, hence do not use bitwise operations. + return stat.type === FileType.Unknown; + } + return (stat.type & fileType) === fileType; + } + public async fileExists(filename: string): Promise { + return this.pathExists(filename, FileType.File); + } + public async directoryExists(dirname: string): Promise { + return this.pathExists(dirname, FileType.Directory); + } + + public async listdir(dirname: string): Promise<[string, FileType][]> { + try { + return await this.raw.listdir(dirname); + } catch (err) { + // We're only preserving pre-existng behavior here... + if (!(await this.pathExists(dirname))) { + return []; + } + throw err; // re-throw + } + } + public async getSubDirectories(dirname: string): Promise { + const files = await this.listdir(dirname); + const filtered = filterByFileType(files, FileType.Directory); + return filtered.map(([filename, _fileType]) => filename); + } + public async getFiles(dirname: string): Promise { + // Note that only "regular" files are returned. + const files = await this.listdir(dirname); + const filtered = filterByFileType(files, FileType.File); + return filtered.map(([filename, _fileType]) => filename); + } + + public async isDirReadonly(dirname: string): Promise { + const filePath = `${dirname}${this.paths.sep}___vscpTest___`; + try { + await this.raw.stat(dirname); + await this.raw.writeText(filePath, ''); + } catch (err) { + if (isNoPermissionsError(err)) { + return true; + } + throw err; // re-throw + } + this.raw + .rmfile(filePath) + // Clean resources in the background. + .ignoreErrors(); + return false; + } + + public async getFileHash(filename: string): Promise { + // The reason for lstat rather than stat is not clear... + const stat = await this.raw.lstat(filename); + const data = `${stat.ctime}-${stat.mtime}`; + return this.getHash(data); + } + + public async search(globPattern: string, cwd?: string, dot?: boolean): Promise { + // tslint:disable-next-line: no-any + let options: any; + if (cwd) { + options = { ...options, cwd }; + } + if (dot) { + options = { ...options, dot }; + } + + const found = await this.globFiles(globPattern, options); + return Array.isArray(found) ? found : []; + } + + //**************************** + // helpers (non-async) + + public fileExistsSync(filePath: string): boolean { + try { + this.raw.statSync(filePath); + } catch (err) { + if (isFileNotFoundError(err)) { + return false; + } + throw err; // re-throw + } + return true; + } +} + +// We *could* use ICryptoUtils, but it's a bit overkill, issue tracked +// in https://github.com/microsoft/vscode-python/issues/8438. +export function getHashString(data: string): string { + const hash = createHash('sha512'); + hash.update(data); + return hash.digest('hex'); +} + +//========================================== +// legacy filesystem API + +// more aliases (to cause less churn) +@injectable() +export class FileSystem implements IFileSystem { + // We expose this for the sake of functional tests that do not have + // access to the actual "vscode" namespace. + protected utils: FileSystemUtils; + constructor() { + this.utils = FileSystemUtils.withDefaults(); + } + + public get directorySeparatorChar(): string { + return this.utils.paths.sep; + } + public arePathsSame(path1: string, path2: string): boolean { + return this.utils.pathUtils.arePathsSame(path1, path2); + } + public getDisplayName(path: string): string { + return this.utils.pathUtils.getDisplayName(path); + } + public async stat(filename: string): Promise { + return this.utils.raw.stat(filename); + } + public async createDirectory(dirname: string): Promise { + return this.utils.createDirectory(dirname); + } + public async deleteDirectory(dirname: string): Promise { + return this.utils.deleteDirectory(dirname); + } + public async listdir(dirname: string): Promise<[string, FileType][]> { + return this.utils.listdir(dirname); + } + public async readFile(filePath: string): Promise { + return this.utils.raw.readText(filePath); + } + public async readData(filePath: string): Promise { + return this.utils.raw.readData(filePath); + } + public async writeFile(filename: string, data: {}): Promise { + return this.utils.raw.writeText(filename, data); + } + public async appendFile(filename: string, text: string): Promise { + return this.utils.raw.appendText(filename, text); + } + public async copyFile(src: string, dest: string): Promise { + return this.utils.raw.copyFile(src, dest); + } + public async deleteFile(filename: string): Promise { + return this.utils.deleteFile(filename); + } + public async chmod(filename: string, mode: string): Promise { + return this.utils.raw.chmod(filename, mode); + } + public async move(src: string, tgt: string) { + await this.utils.raw.move(src, tgt); + } + public readFileSync(filePath: string): string { + return this.utils.raw.readTextSync(filePath); + } + public createReadStream(filePath: string): ReadStream { + return this.utils.raw.createReadStream(filePath); + } + public createWriteStream(filePath: string): WriteStream { + return this.utils.raw.createWriteStream(filePath); + } + public async fileExists(filename: string): Promise { + return this.utils.fileExists(filename); + } + public fileExistsSync(filename: string): boolean { + return this.utils.fileExistsSync(filename); + } + public async directoryExists(dirname: string): Promise { + return this.utils.directoryExists(dirname); + } + public async getSubDirectories(dirname: string): Promise { + return this.utils.getSubDirectories(dirname); + } + public async getFiles(dirname: string): Promise { + return this.utils.getFiles(dirname); + } + public async getFileHash(filename: string): Promise { + return this.utils.getFileHash(filename); + } + public async search(globPattern: string, cwd?: string, dot?: boolean): Promise { + return this.utils.search(globPattern, cwd, dot); + } + public async createTemporaryFile(suffix: string, mode?: number): Promise { + return this.utils.tmp.createFile(suffix, mode); + } + public async isDirReadonly(dirname: string): Promise { + return this.utils.isDirReadonly(dirname); + } +} diff --git a/src/client/common/platform/fs-paths.ts b/src/client/common/platform/fs-paths.ts new file mode 100644 index 000000000000..c2d953dcca87 --- /dev/null +++ b/src/client/common/platform/fs-paths.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as nodepath from 'path'; +import { getOSType, OSType } from '../utils/platform'; +import { IExecutables, IFileSystemPaths, IFileSystemPathUtils } from './types'; +// tslint:disable-next-line:no-var-requires no-require-imports +const untildify = require('untildify'); + +// The parts of node's 'path' module used by FileSystemPaths. +interface INodePath { + sep: string; + join(...filenames: string[]): string; + dirname(filename: string): string; + basename(filename: string, ext?: string): string; + normalize(filename: string): string; +} + +export class FileSystemPaths implements IFileSystemPaths { + constructor( + // "true" if targeting a case-insensitive host (like Windows) + private readonly isCaseInsensitive: boolean, + // (effectively) the node "path" module to use + private readonly raw: INodePath + ) {} + // Create a new object using common-case default values. + // We do not use an alternate constructor because defaults in the + // constructor runs counter to our typical approach. + public static withDefaults( + // default: use "isWindows" + isCaseInsensitive?: boolean + ): FileSystemPaths { + if (isCaseInsensitive === undefined) { + isCaseInsensitive = getOSType() === OSType.Windows; + } + return new FileSystemPaths( + isCaseInsensitive, + // Use the actual node "path" module. + nodepath + ); + } + + public get sep(): string { + return this.raw.sep; + } + + public join(...filenames: string[]): string { + return this.raw.join(...filenames); + } + + public dirname(filename: string): string { + return this.raw.dirname(filename); + } + + public basename(filename: string, suffix?: string): string { + return this.raw.basename(filename, suffix); + } + + public normalize(filename: string): string { + return this.raw.normalize(filename); + } + + public normCase(filename: string): string { + filename = this.raw.normalize(filename); + return this.isCaseInsensitive ? filename.toUpperCase() : filename; + } +} + +export class Executables { + constructor( + // the $PATH delimiter to use + public readonly delimiter: string, + // the OS type to target + private readonly osType: OSType + ) {} + // Create a new object using common-case default values. + // We do not use an alternate constructor because defaults in the + // constructor runs counter to our typical approach. + public static withDefaults(): Executables { + return new Executables( + // Use node's value. + nodepath.delimiter, + // Use the current OS. + getOSType() + ); + } + + public get envVar(): string { + return this.osType === OSType.Windows ? 'Path' : 'PATH'; + } +} + +// The dependencies FileSystemPathUtils has on node's path module. +interface IRawPaths { + relative(relpath: string, rootpath: string): string; +} + +export class FileSystemPathUtils implements IFileSystemPathUtils { + constructor( + // the user home directory to use (and expose) + public readonly home: string, + // the low-level FS path operations to use (and expose) + public readonly paths: IFileSystemPaths, + // the low-level OS "executables" to use (and expose) + public readonly executables: IExecutables, + // other low-level FS path operations to use + private readonly raw: IRawPaths + ) {} + // Create a new object using common-case default values. + // We do not use an alternate constructor because defaults in the + // constructor runs counter to our typical approach. + public static withDefaults( + // default: a new FileSystemPaths object (using defaults) + paths?: IFileSystemPaths + ): FileSystemPathUtils { + if (paths === undefined) { + paths = FileSystemPaths.withDefaults(); + } + return new FileSystemPathUtils( + // Use the current user's home directory. + untildify('~'), + paths, + Executables.withDefaults(), + // Use the actual node "path" module. + nodepath + ); + } + + public arePathsSame(path1: string, path2: string): boolean { + path1 = this.paths.normCase(path1); + path2 = this.paths.normCase(path2); + return path1 === path2; + } + + public getDisplayName(filename: string, cwd?: string): string { + if (cwd && filename.startsWith(cwd)) { + return `.${this.paths.sep}${this.raw.relative(cwd, filename)}`; + } else if (filename.startsWith(this.home)) { + return `~${this.paths.sep}${this.raw.relative(this.home, filename)}`; + } else { + return filename; + } + } +} diff --git a/src/client/common/platform/fs-temp.ts b/src/client/common/platform/fs-temp.ts new file mode 100644 index 000000000000..64d2870a47e4 --- /dev/null +++ b/src/client/common/platform/fs-temp.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as tmp from 'tmp'; +import { ITempFileSystem, TemporaryFile } from './types'; + +interface IRawTempFS { + // tslint:disable-next-line:no-suspicious-comment + // TODO (https://github.com/microsoft/vscode/issues/84517) + // This functionality has been requested for the + // VS Code FS API (vscode.workspace.fs.*). + file( + config: tmp.Options, + // tslint:disable-next-line:no-any + callback?: (err: any, path: string, fd: number, cleanupCallback: () => void) => void + ): void; +} + +// Operations related to temporary files and directories. +export class TemporaryFileSystem implements ITempFileSystem { + constructor( + // (effectively) the third-party "tmp" module to use + private readonly raw: IRawTempFS + ) {} + public static withDefaults(): TemporaryFileSystem { + return new TemporaryFileSystem( + // Use the actual "tmp" module. + tmp + ); + } + + // Create a new temp file with the given filename suffix. + public createFile(suffix: string, mode?: number): Promise { + const opts = { + postfix: suffix, + mode + }; + return new Promise((resolve, reject) => { + this.raw.file(opts, (err, filename, _fd, cleanUp) => { + if (err) { + return reject(err); + } + resolve({ + filePath: filename, + dispose: cleanUp + }); + }); + }); + } +} diff --git a/src/client/common/platform/pathUtils.ts b/src/client/common/platform/pathUtils.ts new file mode 100644 index 000000000000..6d3a5522db8e --- /dev/null +++ b/src/client/common/platform/pathUtils.ts @@ -0,0 +1,57 @@ +// tslint:disable-next-line:no-suspicious-comment +// TODO: Drop this file. +// See https://github.com/microsoft/vscode-python/issues/8542. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { IPathUtils, IsWindows } from '../types'; +import { OSType } from '../utils/platform'; +import { Executables, FileSystemPaths, FileSystemPathUtils } from './fs-paths'; +// tslint:disable-next-line:no-var-requires no-require-imports +const untildify = require('untildify'); + +@injectable() +export class PathUtils implements IPathUtils { + private readonly utils: FileSystemPathUtils; + constructor( + // "true" if targeting a Windows host. + @inject(IsWindows) isWindows: boolean + ) { + const osType = isWindows ? OSType.Windows : OSType.Unknown; + // We cannot just use FileSystemPathUtils.withDefaults() because + // of the isWindows arg. + this.utils = new FileSystemPathUtils( + untildify('~'), + FileSystemPaths.withDefaults(), + new Executables(path.delimiter, osType), + path + ); + } + + public get home(): string { + return this.utils.home; + } + + public get delimiter(): string { + return this.utils.executables.delimiter; + } + + public get separator(): string { + return this.utils.paths.sep; + } + + // tslint:disable-next-line:no-suspicious-comment + // TODO: Deprecate in favor of IPlatformService? + public getPathVariableName(): 'Path' | 'PATH' { + // tslint:disable-next-line:no-any + return this.utils.executables.envVar as any; + } + + public getDisplayName(pathValue: string, cwd?: string): string { + return this.utils.getDisplayName(pathValue, cwd); + } + + public basename(pathValue: string, ext?: string): string { + return this.utils.paths.basename(pathValue, ext); + } +} diff --git a/src/client/common/platform/platformService.ts b/src/client/common/platform/platformService.ts new file mode 100644 index 000000000000..81706960e6a6 --- /dev/null +++ b/src/client/common/platform/platformService.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { injectable } from 'inversify'; +import * as os from 'os'; +import { coerce, SemVer } from 'semver'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName, PlatformErrors } from '../../telemetry/constants'; +import { getOSType, OSType } from '../utils/platform'; +import { parseVersion } from '../utils/version'; +import { NON_WINDOWS_PATH_VARIABLE_NAME, WINDOWS_PATH_VARIABLE_NAME } from './constants'; +import { IPlatformService } from './types'; + +@injectable() +export class PlatformService implements IPlatformService { + public readonly osType: OSType = getOSType(); + public version?: SemVer; + constructor() { + if (this.osType === OSType.Unknown) { + sendTelemetryEvent(EventName.PLATFORM_INFO, undefined, { + failureType: PlatformErrors.FailedToDetermineOS + }); + } + } + public get pathVariableName() { + return this.isWindows ? WINDOWS_PATH_VARIABLE_NAME : NON_WINDOWS_PATH_VARIABLE_NAME; + } + public get virtualEnvBinName() { + return this.isWindows ? 'Scripts' : 'bin'; + } + public async getVersion(): Promise { + if (this.version) { + return this.version; + } + switch (this.osType) { + case OSType.Windows: + case OSType.OSX: + // Release section of https://en.wikipedia.org/wiki/MacOS_Sierra. + // Version 10.12 maps to Darwin 16.0.0. + // Using os.relase() we get the darwin release #. + try { + const ver = coerce(os.release()); + if (ver) { + sendTelemetryEvent(EventName.PLATFORM_INFO, undefined, { + osVersion: `${ver.major}.${ver.minor}.${ver.patch}` + }); + return (this.version = ver); + } + throw new Error('Unable to parse version'); + } catch (ex) { + sendTelemetryEvent(EventName.PLATFORM_INFO, undefined, { + failureType: PlatformErrors.FailedToParseVersion + }); + return parseVersion(os.release()); + } + default: + throw new Error('Not Supported'); + } + } + + public get isWindows(): boolean { + return this.osType === OSType.Windows; + } + public get isMac(): boolean { + return this.osType === OSType.OSX; + } + public get isLinux(): boolean { + return this.osType === OSType.Linux; + } + public get osRelease(): string { + return os.release(); + } + public get is64bit(): boolean { + // tslint:disable-next-line:no-require-imports + const arch = require('arch'); + return arch() === 'x64'; + } +} diff --git a/src/client/common/platform/registry.ts b/src/client/common/platform/registry.ts new file mode 100644 index 000000000000..3e920f03c30c --- /dev/null +++ b/src/client/common/platform/registry.ts @@ -0,0 +1,88 @@ +import { injectable } from 'inversify'; +import { Options } from 'winreg'; +import { traceError } from '../logger'; +import { Architecture } from '../utils/platform'; +import { IRegistry, RegistryHive } from './types'; + +enum RegistryArchitectures { + x86 = 'x86', + x64 = 'x64' +} + +@injectable() +export class RegistryImplementation implements IRegistry { + public async getKeys(key: string, hive: RegistryHive, arch?: Architecture) { + return getRegistryKeys({ hive: translateHive(hive)!, arch: translateArchitecture(arch), key }).catch((ex) => { + traceError('Fetching keys from windows registry resulted in an error', ex); + return []; + }); + } + public async getValue(key: string, hive: RegistryHive, arch?: Architecture, name: string = '') { + return getRegistryValue({ hive: translateHive(hive)!, arch: translateArchitecture(arch), key }, name).catch( + (ex) => { + traceError('Fetching key value from windows registry resulted in an error', ex); + return undefined; + } + ); + } +} + +export function getArchitectureDisplayName(arch?: Architecture) { + switch (arch) { + case Architecture.x64: + return '64-bit'; + case Architecture.x86: + return '32-bit'; + default: + return ''; + } +} + +async function getRegistryValue(options: Options, name: string = '') { + // tslint:disable-next-line:no-require-imports + const Registry = require('winreg') as typeof import('winreg'); + return new Promise((resolve) => { + new Registry(options).get(name, (error, result) => { + if (error || !result || typeof result.value !== 'string') { + return resolve(undefined); + } + resolve(result.value); + }); + }); +} + +async function getRegistryKeys(options: Options): Promise { + // tslint:disable-next-line:no-require-imports + const Registry = require('winreg') as typeof import('winreg'); + // https://github.com/python/peps/blob/master/pep-0514.txt#L85 + return new Promise((resolve) => { + new Registry(options).keys((error, result) => { + if (error || !Array.isArray(result)) { + return resolve([]); + } + resolve(result.filter((item) => typeof item.key === 'string').map((item) => item.key)); + }); + }); +} +function translateArchitecture(arch?: Architecture): RegistryArchitectures | undefined { + switch (arch) { + case Architecture.x86: + return RegistryArchitectures.x86; + case Architecture.x64: + return RegistryArchitectures.x64; + default: + return; + } +} +function translateHive(hive: RegistryHive): string | undefined { + // tslint:disable-next-line:no-require-imports + const Registry = require('winreg') as typeof import('winreg'); + switch (hive) { + case RegistryHive.HKCU: + return Registry.HKCU; + case RegistryHive.HKLM: + return Registry.HKLM; + default: + return; + } +} diff --git a/src/client/common/platform/serviceRegistry.ts b/src/client/common/platform/serviceRegistry.ts new file mode 100644 index 000000000000..d15edf5fc388 --- /dev/null +++ b/src/client/common/platform/serviceRegistry.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { IServiceManager } from '../../ioc/types'; +import { FileSystem } from './fileSystem'; +import { PlatformService } from './platformService'; +import { RegistryImplementation } from './registry'; +import { IFileSystem, IPlatformService, IRegistry } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IPlatformService, PlatformService); + serviceManager.addSingleton(IFileSystem, FileSystem); + serviceManager.addSingleton(IRegistry, RegistryImplementation); +} diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts new file mode 100644 index 000000000000..1f533a0f80c2 --- /dev/null +++ b/src/client/common/platform/types.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs'; +import * as fsextra from 'fs-extra'; +import { SemVer } from 'semver'; +import * as vscode from 'vscode'; +import { Architecture, OSType } from '../utils/platform'; + +//=========================== +// registry + +export enum RegistryHive { + HKCU, + HKLM +} + +export const IRegistry = Symbol('IRegistry'); +export interface IRegistry { + getKeys(key: string, hive: RegistryHive, arch?: Architecture): Promise; + getValue(key: string, hive: RegistryHive, arch?: Architecture, name?: string): Promise; +} + +//=========================== +// platform + +export const IsWindows = Symbol('IS_WINDOWS'); + +export const IPlatformService = Symbol('IPlatformService'); +export interface IPlatformService { + readonly osType: OSType; + osRelease: string; + readonly pathVariableName: 'Path' | 'PATH'; + readonly virtualEnvBinName: 'bin' | 'Scripts'; + + // convenience methods + readonly isWindows: boolean; + readonly isMac: boolean; + readonly isLinux: boolean; + readonly is64bit: boolean; + getVersion(): Promise; +} + +//=========================== +// temp FS + +export type TemporaryFile = { filePath: string } & vscode.Disposable; +export type TemporaryDirectory = { path: string } & vscode.Disposable; + +export interface ITempFileSystem { + createFile(suffix: string, mode?: number): Promise; +} + +//=========================== +// FS paths + +// The low-level file path operations used by the extension. +export interface IFileSystemPaths { + readonly sep: string; + join(...filenames: string[]): string; + dirname(filename: string): string; + basename(filename: string, suffix?: string): string; + normalize(filename: string): string; + normCase(filename: string): string; +} + +// Where to fine executables. +// +// In particular this class provides all the tools needed to find +// executables, including through an environment variable. +export interface IExecutables { + delimiter: string; + envVar: string; +} + +export const IFileSystemPathUtils = Symbol('IFileSystemPathUtils'); +// A collection of high-level utilities related to filesystem paths. +export interface IFileSystemPathUtils { + readonly paths: IFileSystemPaths; + readonly executables: IExecutables; + readonly home: string; + // Return true if the two paths are equivalent on the current + // filesystem and false otherwise. On Windows this is significant. + // On non-Windows the filenames must always be exactly the same. + arePathsSame(path1: string, path2: string): boolean; + // Return the clean (displayable) form of the given filename. + getDisplayName(pathValue: string, cwd?: string): string; +} + +//=========================== +// filesystem operations + +export import FileType = vscode.FileType; +export import FileStat = vscode.FileStat; +export type ReadStream = fs.ReadStream; +export type WriteStream = fs.WriteStream; + +// The low-level filesystem operations on which the extension depends. +export interface IRawFileSystem { + // Get information about a file (resolve symlinks). + stat(filename: string): Promise; + // Get information about a file (do not resolve synlinks). + lstat(filename: string): Promise; + // Change a file's permissions. + chmod(filename: string, mode: string | number): Promise; + // Move the file to a different location (and/or rename it). + move(src: string, tgt: string): Promise; + + //*********************** + // files + + // Return the raw bytes of the given file. + readData(filename: string): Promise; + // Return the text of the given file (decoded from UTF-8). + readText(filename: string): Promise; + // Write the given text to the file (UTF-8 encoded). + writeText(filename: string, data: {}): Promise; + // Write the given text to the end of the file (UTF-8 encoded). + appendText(filename: string, text: string): Promise; + // Copy a file. + copyFile(src: string, dest: string): Promise; + // Delete a file. + rmfile(filename: string): Promise; + + //*********************** + // directories + + // Create the directory and any missing parent directories. + mkdirp(dirname: string): Promise; + // Delete the directory if empty. + rmdir(dirname: string): Promise; + // Delete the directory and everything in it. + rmtree(dirname: string): Promise; + // Return the contents of the directory. + listdir(dirname: string): Promise<[string, FileType][]>; + + //*********************** + // not async + + // Get information about a file (resolve symlinks). + statSync(filename: string): FileStat; + // Return the text of the given file (decoded from UTF-8). + readTextSync(filename: string): string; + // Create a streaming wrappr around an open file (for reading). + createReadStream(filename: string): ReadStream; + // Create a streaming wrappr around an open file (for writing). + createWriteStream(filename: string): WriteStream; +} + +// High-level filesystem operations used by the extension. +export interface IFileSystemUtils { + readonly raw: IRawFileSystem; + readonly paths: IFileSystemPaths; + readonly pathUtils: IFileSystemPathUtils; + readonly tmp: ITempFileSystem; + + //*********************** + // aliases + + createDirectory(dirname: string): Promise; + deleteDirectory(dirname: string): Promise; + deleteFile(filename: string): Promise; + + //*********************** + // helpers + + // Determine if the file exists, optionally requiring the type. + pathExists(filename: string, fileType?: FileType): Promise; + // Determine if the regular file exists. + fileExists(filename: string): Promise; + // Determine if the directory exists. + directoryExists(dirname: string): Promise; + // Get all the directory's entries. + listdir(dirname: string): Promise<[string, FileType][]>; + // Get the paths of all immediate subdirectories. + getSubDirectories(dirname: string): Promise; + // Get the paths of all immediately contained files. + getFiles(dirname: string): Promise; + // Determine if the directory is read-only. + isDirReadonly(dirname: string): Promise; + // Generate the sha512 hash for the file (based on timestamps). + getFileHash(filename: string): Promise; + // Get the paths of all files matching the pattern. + search(globPattern: string): Promise; + + //*********************** + // helpers (non-async) + + fileExistsSync(path: string): boolean; +} + +// tslint:disable-next-line:no-suspicious-comment +// TODO: Later we will drop IFileSystem, switching usage to IFileSystemUtils. +// See https://github.com/microsoft/vscode-python/issues/8542. + +export const IFileSystem = Symbol('IFileSystem'); +export interface IFileSystem { + // path-related + directorySeparatorChar: string; + arePathsSame(path1: string, path2: string): boolean; + getDisplayName(path: string): string; + + // "raw" operations + stat(filePath: string): Promise; + createDirectory(path: string): Promise; + deleteDirectory(path: string): Promise; + listdir(dirname: string): Promise<[string, FileType][]>; + readFile(filePath: string): Promise; + readData(filePath: string): Promise; + writeFile(filePath: string, text: string | Buffer, options?: string | fsextra.WriteFileOptions): Promise; + appendFile(filename: string, text: string | Buffer): Promise; + copyFile(src: string, dest: string): Promise; + deleteFile(filename: string): Promise; + chmod(path: string, mode: string | number): Promise; + move(src: string, tgt: string): Promise; + // sync + readFileSync(filename: string): string; + createReadStream(path: string): fs.ReadStream; + createWriteStream(path: string): fs.WriteStream; + + // utils + fileExists(path: string): Promise; + fileExistsSync(path: string): boolean; + directoryExists(path: string): Promise; + getSubDirectories(rootDir: string): Promise; + getFiles(rootDir: string): Promise; + getFileHash(filePath: string): Promise; + search(globPattern: string, cwd?: string, dot?: boolean): Promise; + createTemporaryFile(extension: string, mode?: number): Promise; + isDirReadonly(dirname: string): Promise; +} diff --git a/src/client/common/process/baseDaemon.ts b/src/client/common/process/baseDaemon.ts new file mode 100644 index 000000000000..abd93668115d --- /dev/null +++ b/src/client/common/process/baseDaemon.ts @@ -0,0 +1,377 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ChildProcess } from 'child_process'; +import * as os from 'os'; +import { Subject } from 'rxjs/Subject'; +import * as util from 'util'; +import { MessageConnection, NotificationType, RequestType, RequestType0 } from 'vscode-jsonrpc'; +import { IPlatformService } from '../../common/platform/types'; +import { traceError, traceInfo, traceVerbose, traceWarning } from '../logger'; +import { IDisposable } from '../types'; +import { createDeferred, Deferred } from '../utils/async'; +import { noop } from '../utils/misc'; +import { + ExecutionResult, + IPythonExecutionService, + ObservableExecutionResult, + Output, + SpawnOptions, + StdErrError +} from './types'; + +export type ErrorResponse = { error?: string }; +export type ExecResponse = ErrorResponse & { stdout: string; stderr?: string }; +export class ConnectionClosedError extends Error { + constructor(public readonly message: string) { + super(); + } +} + +export class DaemonError extends Error { + constructor(public readonly message: string) { + super(); + } +} +export abstract class BasePythonDaemon { + public get isAlive(): boolean { + return this.connectionClosedMessage === ''; + } + protected outputObservale = new Subject>(); + private connectionClosedMessage: string = ''; + protected get closed() { + return this.connectionClosedDeferred.promise; + } + // tslint:disable-next-line: no-any + private readonly connectionClosedDeferred: Deferred; + private disposables: IDisposable[] = []; + private disposed = false; + constructor( + protected readonly pythonExecutionService: IPythonExecutionService, + protected readonly platformService: IPlatformService, + protected readonly pythonPath: string, + public readonly proc: ChildProcess, + public readonly connection: MessageConnection + ) { + // tslint:disable-next-line: no-any + this.connectionClosedDeferred = createDeferred(); + // This promise gets used conditionally, if it doesn't get used, and the promise is rejected, + // then node logs errors. We don't want that, hence add a dummy error handler. + this.connectionClosedDeferred.promise.catch(noop); + this.monitorConnection(); + } + public dispose() { + // Make sure that we only dispose once so we are not sending multiple kill signals or notifications + // This daemon can be held by multiple disposes such as a jupyter server daemon process which can + // be disposed by both the connection and the main async disposable + if (!this.disposed) { + try { + this.disposed = true; + + // Proc.kill uses a 'SIGTERM' signal by default to kill. This was failing to kill the process + // sometimes on Mac and Linux. Changing this over to a 'SIGKILL' to fully kill the process. + // Windows closes with a different non-signal message, so keep that the same + // See kill_kernel message of kernel_launcher_daemon.py for and example of this. + if (this.platformService.isWindows) { + this.proc.kill(); + } else { + this.proc.kill('SIGKILL'); + } + } catch { + noop(); + } + this.disposables.forEach((item) => item.dispose()); + } + } + public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult { + if (this.isAlive && this.canExecFileUsingDaemon(args, options)) { + try { + return this.execAsObservable({ fileName: args[0] }, args.slice(1), options); + } catch (ex) { + if (ex instanceof DaemonError || ex instanceof ConnectionClosedError) { + traceWarning('Falling back to Python Execution Service due to failure in daemon', ex); + return this.pythonExecutionService.execObservable(args, options); + } else { + throw ex; + } + } + } else { + return this.pythonExecutionService.execObservable(args, options); + } + } + public execModuleObservable( + moduleName: string, + args: string[], + options: SpawnOptions + ): ObservableExecutionResult { + if (this.isAlive && this.canExecModuleUsingDaemon(moduleName, args, options)) { + try { + return this.execAsObservable({ moduleName }, args, options); + } catch (ex) { + if (ex instanceof DaemonError || ex instanceof ConnectionClosedError) { + traceWarning('Falling back to Python Execution Service due to failure in daemon', ex); + return this.pythonExecutionService.execModuleObservable(moduleName, args, options); + } else { + throw ex; + } + } + } else { + return this.pythonExecutionService.execModuleObservable(moduleName, args, options); + } + } + public async exec(args: string[], options: SpawnOptions): Promise> { + if (this.isAlive && this.canExecFileUsingDaemon(args, options)) { + try { + return await this.execFileWithDaemon(args[0], args.slice(1), options); + } catch (ex) { + if (ex instanceof DaemonError || ex instanceof ConnectionClosedError) { + traceWarning('Falling back to Python Execution Service due to failure in daemon', ex); + return this.pythonExecutionService.exec(args, options); + } else { + throw ex; + } + } + } else { + return this.pythonExecutionService.exec(args, options); + } + } + public async execModule( + moduleName: string, + args: string[], + options: SpawnOptions + ): Promise> { + if (this.isAlive && this.canExecModuleUsingDaemon(moduleName, args, options)) { + try { + return await this.execModuleWithDaemon(moduleName, args, options); + } catch (ex) { + if (ex instanceof DaemonError || ex instanceof ConnectionClosedError) { + traceWarning('Falling back to Python Execution Service due to failure in daemon', ex); + return this.pythonExecutionService.execModule(moduleName, args, options); + } else { + throw ex; + } + } + } else { + return this.pythonExecutionService.execModule(moduleName, args, options); + } + } + protected canExecFileUsingDaemon(args: string[], options: SpawnOptions): boolean { + return args[0].toLowerCase().endsWith('.py') && this.areOptionsSupported(options); + } + protected canExecModuleUsingDaemon(_moduleName: string, _args: string[], options: SpawnOptions): boolean { + return this.areOptionsSupported(options); + } + protected areOptionsSupported(options: SpawnOptions): boolean { + const daemonSupportedSpawnOptions: (keyof SpawnOptions)[] = [ + 'cwd', + 'env', + 'throwOnStdErr', + 'token', + 'encoding', + 'mergeStdOutErr', + 'extraVariables' + ]; + // tslint:disable-next-line: no-any + return Object.keys(options).every((item) => daemonSupportedSpawnOptions.indexOf(item as any) >= 0); + } + protected sendRequestWithoutArgs(type: RequestType0): Thenable { + return Promise.race([this.connection.sendRequest(type), this.connectionClosedDeferred.promise]); + } + protected sendRequest(type: RequestType, params?: P): Thenable { + if (!this.isAlive) { + traceError('Daemon is handling a request after death.'); + } + // Throw an error if the connection has been closed. + return Promise.race([this.connection.sendRequest(type, params), this.connectionClosedDeferred.promise]); + } + protected throwIfRPCConnectionIsDead() { + if (!this.isAlive) { + throw new ConnectionClosedError(this.connectionClosedMessage); + } + } + protected execAsObservable( + moduleOrFile: { moduleName: string } | { fileName: string }, + args: string[], + options: SpawnOptions + ): ObservableExecutionResult { + const subject = new Subject>(); + const start = async () => { + let response: ExecResponse; + if ('fileName' in moduleOrFile) { + const request = new RequestType< + // tslint:disable-next-line: no-any + { file_name: string; args: string[]; cwd?: string; env?: any }, + ExecResponse, + void, + void + >('exec_file_observable'); + response = await this.sendRequest(request, { + file_name: moduleOrFile.fileName, + args, + cwd: options.cwd, + env: options.env + }); + } else { + const request = new RequestType< + // tslint:disable-next-line: no-any + { module_name: string; args: string[]; cwd?: string; env?: any }, + ExecResponse, + void, + void + >('exec_module_observable'); + response = await this.sendRequest(request, { + module_name: moduleOrFile.moduleName, + args, + cwd: options.cwd, + env: options.env + }); + } + // Might not get a response object back, as its observable. + if (response && response.error) { + throw new DaemonError(response.error); + } + }; + let stdErr = ''; + this.proc.stderr.on('data', (output: string | Buffer) => (stdErr += output.toString())); + // Wire up stdout/stderr. + const subscription = this.outputObservale.subscribe((out) => { + if (out.source === 'stderr' && options.throwOnStdErr) { + subject.error(new StdErrError(out.out)); + } else if (out.source === 'stderr' && options.mergeStdOutErr) { + subject.next({ source: 'stdout', out: out.out }); + } else { + subject.next(out); + } + }); + start() + .catch((ex) => { + const errorMsg = `Failed to run ${ + 'fileName' in moduleOrFile ? moduleOrFile.fileName : moduleOrFile.moduleName + } as observable with args ${args.join(' ')}`; + traceError(errorMsg, ex); + subject.next({ source: 'stderr', out: `${errorMsg}\n${stdErr}` }); + subject.error(ex); + }) + .finally(() => { + // Wait until all messages are received. + setTimeout(() => { + subscription.unsubscribe(); + subject.complete(); + }, 100); + }) + .ignoreErrors(); + + return { + proc: this.proc, + dispose: () => this.dispose(), + out: subject + }; + } + /** + * Process the response. + * + * @private + * @param {{ error?: string | undefined; stdout: string; stderr?: string }} response + * @param {SpawnOptions} options + * @memberof PythonDaemonExecutionService + */ + private processResponse( + response: { error?: string | undefined; stdout: string; stderr?: string }, + options: SpawnOptions + ) { + if (response.error) { + throw new DaemonError(`Failed to execute using the daemon, ${response.error}`); + } + // Throw an error if configured to do so if there's any output in stderr. + if (response.stderr && options.throwOnStdErr) { + throw new StdErrError(response.stderr); + } + // Merge stdout and stderr into on if configured to do so. + if (response.stderr && options.mergeStdOutErr) { + response.stdout = `${response.stdout || ''}${os.EOL}${response.stderr}`; + } + } + private async execFileWithDaemon( + fileName: string, + args: string[], + options: SpawnOptions + ): Promise> { + const request = new RequestType< + // tslint:disable-next-line: no-any + { file_name: string; args: string[]; cwd?: string; env?: any }, + ExecResponse, + void, + void + >('exec_file'); + const response = await this.sendRequest(request, { + file_name: fileName, + args, + cwd: options.cwd, + env: options.env + }); + this.processResponse(response, options); + return response; + } + private async execModuleWithDaemon( + moduleName: string, + args: string[], + options: SpawnOptions + ): Promise> { + const request = new RequestType< + // tslint:disable-next-line: no-any + { module_name: string; args: string[]; cwd?: string; env?: any }, + ExecResponse, + void, + void + >('exec_module'); + const response = await this.sendRequest(request, { + module_name: moduleName, + args, + cwd: options.cwd, + env: options.env + }); + this.processResponse(response, options); + return response; + } + private monitorConnection() { + // tslint:disable-next-line: no-any + const logConnectionStatus = (msg: string, ex?: any) => { + if (!this.disposed) { + this.connectionClosedMessage += msg + (ex ? `, With Error: ${util.format(ex)}` : ''); + this.connectionClosedDeferred.reject(new ConnectionClosedError(this.connectionClosedMessage)); + traceWarning(msg); + if (ex) { + traceError('Connection errored', ex); + } + } + }; + this.disposables.push(this.connection.onClose(() => logConnectionStatus('Daemon Connection Closed'))); + this.disposables.push(this.connection.onDispose(() => logConnectionStatus('Daemon Connection disposed'))); + this.disposables.push(this.connection.onError((ex) => logConnectionStatus('Daemon Connection errored', ex))); + // this.proc.on('error', error => logConnectionStatus('Daemon Processed died with error', error)); + this.proc.on('exit', (code) => logConnectionStatus('Daemon Processed died with exit code', code)); + // Wire up stdout/stderr. + const OuputNotification = new NotificationType, void>('output'); + this.connection.onNotification(OuputNotification, (output) => this.outputObservale.next(output)); + const logNotification = new NotificationType< + { level: 'WARN' | 'WARNING' | 'INFO' | 'DEBUG' | 'NOTSET'; msg: string; pid?: string }, + void + >('log'); + this.connection.onNotification(logNotification, (output) => { + const pid = output.pid ? ` (pid: ${output.pid})` : ''; + const msg = `Python Daemon${pid}: ${output.msg}`; + if (output.level === 'DEBUG' || output.level === 'NOTSET') { + traceVerbose(msg); + } else if (output.level === 'INFO') { + traceInfo(msg); + } else if (output.level === 'WARN' || output.level === 'WARNING') { + traceWarning(msg); + } else { + traceError(msg); + } + }); + this.connection.onUnhandledNotification(traceError); + } +} diff --git a/src/client/common/process/constants.ts b/src/client/common/process/constants.ts new file mode 100644 index 000000000000..ccf8e6c6850d --- /dev/null +++ b/src/client/common/process/constants.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export const DEFAULT_ENCODING = 'utf8'; diff --git a/src/client/common/process/currentProcess.ts b/src/client/common/process/currentProcess.ts new file mode 100644 index 000000000000..8d918f83f26c --- /dev/null +++ b/src/client/common/process/currentProcess.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// tslint:disable:no-any + +import { injectable } from 'inversify'; +import { ICurrentProcess } from '../types'; +import { EnvironmentVariables } from '../variables/types'; + +@injectable() +export class CurrentProcess implements ICurrentProcess { + public on = (event: string | symbol, listener: Function): this => { + process.on(event as any, listener as any); + return process as any; + }; + public get env(): EnvironmentVariables { + return (process.env as any) as EnvironmentVariables; + } + public get argv(): string[] { + return process.argv; + } + public get stdout(): NodeJS.WriteStream { + return process.stdout; + } + public get stdin(): NodeJS.ReadStream { + return process.stdin; + } + + public get execPath(): string { + return process.execPath; + } +} diff --git a/src/client/common/process/decoder.ts b/src/client/common/process/decoder.ts new file mode 100644 index 000000000000..4e03b48501d0 --- /dev/null +++ b/src/client/common/process/decoder.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as iconv from 'iconv-lite'; +import { injectable } from 'inversify'; +import { DEFAULT_ENCODING } from './constants'; +import { IBufferDecoder } from './types'; + +@injectable() +export class BufferDecoder implements IBufferDecoder { + public decode(buffers: Buffer[], encoding: string = DEFAULT_ENCODING): string { + encoding = iconv.encodingExists(encoding) ? encoding : DEFAULT_ENCODING; + return iconv.decode(Buffer.concat(buffers), encoding); + } +} diff --git a/src/client/common/process/internal/python.ts b/src/client/common/process/internal/python.ts new file mode 100644 index 000000000000..d553e54293c1 --- /dev/null +++ b/src/client/common/process/internal/python.ts @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { _ISOLATED as ISOLATED } from './scripts'; + +// "python" contains functions corresponding to the various ways that +// the extension invokes a Python executable internally. Each function +// takes arguments relevant to the specific use case. However, each +// always *returns* a list of strings for the commandline arguments that +// should be used when invoking the Python executable for the specific +// use case, whether through spawn/exec or a terminal. +// +// Where relevant (nearly always), the function also returns a "parse" +// function that may be used to deserialize the stdout of the command +// into the corresponding object or objects. "parse()" takes a single +// string as the stdout text and returns the relevant data. + +export function execCode(code: string, isolated = true): string[] { + const args = ['-c', code]; + if (isolated) { + args.splice(0, 0, ISOLATED); + } + // "code" isn't specific enough to know how to parse it, + // so we only return the args. + return args; +} + +export function execModule(name: string, moduleArgs: string[], isolated = true): string[] { + const args = ['-m', name, ...moduleArgs]; + if (isolated) { + args[0] = ISOLATED.fileToCommandArgument(); + } + // "code" isn't specific enough to know how to parse it, + // so we only return the args. + return args; +} + +export function getVersion(): [string[], (out: string) => string] { + // There is no need to isolate this. + const args = ['--version']; + + function parse(out: string): string { + return out.trim(); + } + + return [args, parse]; +} + +export function getSysPrefix(): [string[], (out: string) => string] { + const args = [ISOLATED, '-c', 'import sys;print(sys.prefix)']; + + function parse(out: string): string { + return out.trim(); + } + + return [args, parse]; +} + +export function getExecutable(): [string[], (out: string) => string] { + const args = [ISOLATED, '-c', 'import sys;print(sys.executable)']; + + function parse(out: string): string { + return out.trim(); + } + + return [args, parse]; +} + +export function getSitePackages(): [string[], (out: string) => string] { + const args = [ + ISOLATED, + '-c', + // On windows we also need the libs path (second item will + // return c:\xxx\lib\site-packages). This is returned by + // the following: + 'from distutils.sysconfig import get_python_lib; print(get_python_lib())' + ]; + + function parse(out: string): string { + return out.trim(); + } + + return [args, parse]; +} + +export function getUserSitePackages(): [string[], (out: string) => string] { + const args = [ISOLATED, 'site', '--user-site']; + + function parse(out: string): string { + return out.trim(); + } + + return [args, parse]; +} + +export function isValid(): [string[], (out: string) => boolean] { + // There is no need to isolate this. + const args = ['-c', 'print(1234)']; + + function parse(out: string): boolean { + return out.startsWith('1234'); + } + + return [args, parse]; +} + +export function isModuleInstalled(name: string): [string[], (out: string) => boolean] { + const args = [ISOLATED, '-c', `import ${name}`]; + + function parse(_out: string): boolean { + // If the command did not fail then the module is installed. + return true; + } + + return [args, parse]; +} + +export function getModuleVersion(name: string): [string[], (out: string) => string] { + const args = [ISOLATED, name, '--version']; + + function parse(out: string): string { + return out.trim(); + } + + return [args, parse]; +} diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts new file mode 100644 index 000000000000..74408e81e1e6 --- /dev/null +++ b/src/client/common/process/internal/scripts/index.ts @@ -0,0 +1,335 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; + +// It is simpler to hard-code it instead of using vscode.ExtensionContext.extensionPath. +export const _SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'pythonFiles'); +const SCRIPTS_DIR = _SCRIPTS_DIR; +export const _ISOLATED = path.join(_SCRIPTS_DIR, 'pyvsc-run-isolated.py'); +const ISOLATED = _ISOLATED; + +// "scripts" contains everything relevant to the scripts found under +// the top-level "pythonFiles" directory. Each of those scripts has +// a function in this module which matches the script's filename. +// Each function provides the commandline arguments that should be +// used when invoking a Python executable, whether through spawn/exec +// or a terminal. +// +// Where relevant (nearly always), the function also returns a "parse" +// function that may be used to deserialize the stdout of the script +// into the corresponding object or objects. "parse()" takes a single +// string as the stdout text and returns the relevant data. +// +// Some of the scripts are located in subdirectories of "pythonFiles". +// For each of those subdirectories there is a sub-module where +// those scripts' functions may be found. +// +// In some cases one or more types related to a script are exported +// from the same module in which the script's function is located. +// These types typically relate to the return type of "parse()". +// +// ignored scripts: +// * install_debugpy.py (used only for extension development) + +export * as testing_tools from './testing_tools'; +export * as vscode_datascience_helpers from './vscode_datascience_helpers'; + +//============================ +// interpreterInfo.py + +type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; +type PythonVersionInfo = [number, number, number, ReleaseLevel, number]; +export type PythonEnvInfo = { + versionInfo: PythonVersionInfo; + sysPrefix: string; + sysVersion: string; + is64Bit: boolean; +}; + +export function interpreterInfo(): [string[], (out: string) => PythonEnvInfo] { + const script = path.join(SCRIPTS_DIR, 'interpreterInfo.py'); + const args = [ISOLATED, script]; + + function parse(out: string): PythonEnvInfo { + let json: PythonEnvInfo; + try { + json = JSON.parse(out); + } catch (ex) { + throw Error(`python ${args} returned bad JSON (${out}) (${ex})`); + } + return json; + } + + return [args, parse]; +} + +//============================ +// completion.py + +namespace _completion { + export type Response = (_Response1 | _Response2) & { + id: number; + }; + type _Response1 = { + // tslint:disable-next-line:no-any no-banned-terms + arguments: any[]; + }; + type _Response2 = + | CompletionResponse + | HoverResponse + | DefinitionResponse + | ReferenceResponse + | SymbolResponse + | ArgumentsResponse; + + type CompletionResponse = { + results: AutoCompleteItem[]; + }; + type HoverResponse = { + results: HoverItem[]; + }; + type DefinitionResponse = { + results: Definition[]; + }; + type ReferenceResponse = { + results: Reference[]; + }; + type SymbolResponse = { + results: Definition[]; + }; + type ArgumentsResponse = { + results: Signature[]; + }; + + type Signature = { + name: string; + docstring: string; + description: string; + paramindex: number; + params: Argument[]; + }; + type Argument = { + name: string; + value: string; + docstring: string; + description: string; + }; + + type Reference = { + name: string; + fileName: string; + columnIndex: number; + lineIndex: number; + moduleName: string; + }; + + type AutoCompleteItem = { + type: string; + kind: string; + text: string; + description: string; + raw_docstring: string; + rightLabel: string; + }; + + type DefinitionRange = { + startLine: number; + startColumn: number; + endLine: number; + endColumn: number; + }; + type Definition = { + type: string; + kind: string; + text: string; + fileName: string; + container: string; + range: DefinitionRange; + }; + + type HoverItem = { + kind: string; + text: string; + description: string; + docstring: string; + signature: string; + }; +} + +export function completion(jediPath?: string): [string[], (out: string) => _completion.Response[]] { + const script = path.join(SCRIPTS_DIR, 'completion.py'); + const args = [ISOLATED, script]; + if (jediPath) { + args.push('custom'); + args.push(jediPath); + } + + function parse(out: string): _completion.Response[] { + return out.splitLines().map((resp) => JSON.parse(resp)); + } + + return [args, parse]; +} + +//============================ +// sortImports.py + +export function sortImports(filename: string, sortArgs?: string[]): [string[], (out: string) => string] { + const script = path.join(SCRIPTS_DIR, 'sortImports.py'); + const args = [ISOLATED, script, filename, '--diff']; + if (sortArgs) { + args.push(...sortArgs); + } + + function parse(out: string) { + // It should just be a diff that the extension will use directly. + return out; + } + + return [args, parse]; +} + +//============================ +// refactor.py + +export function refactor(root: string): [string[], (out: string) => object[]] { + const script = path.join(SCRIPTS_DIR, 'refactor.py'); + const args = [ISOLATED, script, root]; + + // tslint:disable-next-line:no-suspicious-comment + // TODO: Make the return type more specific, like we did + // with completion(). + function parse(out: string): object[] { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Also handle "STARTED"? + return out + .split(/\r?\n/g) + .filter((line) => line.length > 0) + .map((resp) => JSON.parse(resp)); + } + + return [args, parse]; +} + +//============================ +// normalizeForInterpreter.py + +export function normalizeForInterpreter(code: string): [string[], (out: string) => string] { + const script = path.join(SCRIPTS_DIR, 'normalizeForInterpreter.py'); + const args = [ISOLATED, script, code]; + + function parse(out: string) { + // The text will be used as-is. + return out; + } + + return [args, parse]; +} + +//============================ +// symbolProvider.py + +namespace _symbolProvider { + type Position = { + line: number; + character: number; + }; + type RawSymbol = { + // If no namespace then ''. + namespace: string; + name: string; + range: { + start: Position; + end: Position; + }; + }; + export type Symbols = { + classes: RawSymbol[]; + methods: RawSymbol[]; + functions: RawSymbol[]; + }; +} + +export function symbolProvider( + filename: string, + // If "text" is provided then it gets passed to the script as-is. + text?: string +): [string[], (out: string) => _symbolProvider.Symbols] { + const script = path.join(SCRIPTS_DIR, 'symbolProvider.py'); + const args = [ISOLATED, script, filename]; + if (text) { + args.push(text); + } + + function parse(out: string): _symbolProvider.Symbols { + return JSON.parse(out); + } + + return [args, parse]; +} + +//============================ +// printEnvVariables.py + +export function printEnvVariables(): [string[], (out: string) => NodeJS.ProcessEnv] { + const script = path.join(SCRIPTS_DIR, 'printEnvVariables.py').fileToCommandArgument(); + const args = [ISOLATED, script]; + + function parse(out: string): NodeJS.ProcessEnv { + return JSON.parse(out); + } + + return [args, parse]; +} + +//============================ +// printEnvVariablesToFile.py + +export function printEnvVariablesToFile(filename: string): [string[], (out: string) => NodeJS.ProcessEnv] { + const script = path.join(SCRIPTS_DIR, 'printEnvVariablesToFile.py'); + const args = [ISOLATED, script, filename.fileToCommandArgument()]; + + function parse(out: string): NodeJS.ProcessEnv { + return JSON.parse(out); + } + + return [args, parse]; +} + +//============================ +// shell_exec.py + +export function shell_exec(command: string, lockfile: string, shellArgs: string[]): string[] { + const script = path.join(SCRIPTS_DIR, 'shell_exec.py'); + // We don't bother with a "parse" function since the output + // could be anything. + return [ + ISOLATED.fileToCommandArgument(), + script, + command.fileToCommandArgument(), + // The shell args must come after the command + // but before the lockfile. + ...shellArgs, + lockfile.fileToCommandArgument() + ]; +} + +//============================ +// testlauncher.py + +export function testlauncher(testArgs: string[]): string[] { + const script = path.join(SCRIPTS_DIR, 'testlauncher.py'); + // There is no output to parse, so we do not return a function. + return [ISOLATED, script, ...testArgs]; +} + +//============================ +// visualstudio_py_testlauncher.py + +export function visualstudio_py_testlauncher(testArgs: string[]): string[] { + const script = path.join(SCRIPTS_DIR, 'visualstudio_py_testlauncher.py'); + // There is no output to parse, so we do not return a function. + return [script, ...testArgs]; +} diff --git a/src/client/common/process/internal/scripts/testing_tools.ts b/src/client/common/process/internal/scripts/testing_tools.ts new file mode 100644 index 000000000000..d2047784622e --- /dev/null +++ b/src/client/common/process/internal/scripts/testing_tools.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { _SCRIPTS_DIR } from './index'; + +const SCRIPTS_DIR = path.join(_SCRIPTS_DIR, 'testing_tools'); + +//============================ +// run_adapter.py + +type TestNode = { + id: string; + name: string; + parentid: string; +}; +type TestParent = TestNode & { + kind: 'folder' | 'file' | 'suite' | 'function'; +}; +type TestFSNode = TestParent & { + kind: 'folder' | 'file'; + relpath: string; +}; + +export type TestFolder = TestFSNode & { + kind: 'folder'; +}; +export type TestFile = TestFSNode & { + kind: 'file'; +}; +export type TestSuite = TestParent & { + kind: 'suite'; +}; +// function-as-a-container is for parameterized ("sub") tests. +export type TestFunction = TestParent & { + kind: 'function'; +}; +export type Test = TestNode & { + source: string; +}; +export type DiscoveredTests = { + rootid: string; + root: string; + parents: TestParent[]; + tests: Test[]; +}; + +export function run_adapter(adapterArgs: string[]): [string[], (out: string) => DiscoveredTests[]] { + const script = path.join(SCRIPTS_DIR, 'run_adapter.py'); + // Note that we for now we do not run this "isolated". The + // script relies on some magic that conflicts with the + // isolated script. + const args = [script, ...adapterArgs]; + + function parse(out: string): DiscoveredTests[] { + return JSON.parse(out); + } + + return [args, parse]; +} diff --git a/src/client/common/process/internal/scripts/vscode_datascience_helpers.ts b/src/client/common/process/internal/scripts/vscode_datascience_helpers.ts new file mode 100644 index 000000000000..880d46bd6738 --- /dev/null +++ b/src/client/common/process/internal/scripts/vscode_datascience_helpers.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { _ISOLATED as ISOLATED, _SCRIPTS_DIR } from './index'; + +const SCRIPTS_DIR = path.join(_SCRIPTS_DIR, 'vscode_datascience_helpers'); + +//============================ +// getServerInfo.py + +type JupyterServerInfo = { + base_url: string; + notebook_dir: string; + hostname: string; + password: boolean; + pid: number; + port: number; + secure: boolean; + token: string; + url: string; +}; + +export function getServerInfo(): [string[], (out: string) => JupyterServerInfo[]] { + const script = path.join(SCRIPTS_DIR, 'getServerInfo.py'); + const args = [ISOLATED, script]; + + function parse(out: string): JupyterServerInfo[] { + return JSON.parse(out.trim()); + } + + return [args, parse]; +} + +//============================ +// getJupyterKernels.py + +export function getJupyterKernels(): string[] { + const script = path.join(SCRIPTS_DIR, 'getJupyterKernels.py'); + // There is no script-specific output to parse, so we do not return a function. + return [ISOLATED, script]; +} + +//============================ +// getJupyterKernelspecVersion.py + +export function getJupyterKernelspecVersion(): string[] { + const script = path.join(SCRIPTS_DIR, 'getJupyterKernelspecVersion.py'); + // For now we do not worry about parsing the output here. + return [ISOLATED, script]; +} + +//============================ +// jupyter_nbInstalled.py + +export function jupyter_nbInstalled(): [string[], (out: string) => boolean] { + const script = path.join(SCRIPTS_DIR, 'jupyter_nbInstalled.py'); + const args = [ISOLATED, script]; + + function parse(out: string): boolean { + return out.toLowerCase().includes('available'); + } + + return [args, parse]; +} diff --git a/src/client/common/process/logger.ts b/src/client/common/process/logger.ts new file mode 100644 index 000000000000..a17896fdd467 --- /dev/null +++ b/src/client/common/process/logger.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { isCI, isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../constants'; +import { traceInfo } from '../logger'; +import { IOutputChannel, IPathUtils } from '../types'; +import { Logging } from '../utils/localize'; +import { IProcessLogger, SpawnOptions } from './types'; + +@injectable() +export class ProcessLogger implements IProcessLogger { + constructor( + @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel, + @inject(IPathUtils) private readonly pathUtils: IPathUtils + ) {} + + public logProcess(file: string, args: string[], options?: SpawnOptions) { + if (!isTestExecution() && isCI && process.env.UITEST_DISABLE_PROCESS_LOGGING) { + // Added to disable logging of process execution commands during UI Tests. + // Used only during UI Tests (hence this setting need not be exposed as a valid setting). + return; + } + const argsList = args.reduce((accumulator, current, index) => { + let formattedArg = this.pathUtils.getDisplayName(current).toCommandArgument(); + if (current[0] === "'" || current[0] === '"') { + formattedArg = `${current[0]}${this.pathUtils.getDisplayName(current.substr(1))}`; + } + + return index === 0 ? formattedArg : `${accumulator} ${formattedArg}`; + }, ''); + + const info = [`> ${this.pathUtils.getDisplayName(file)} ${argsList}`]; + if (options && options.cwd) { + info.push(`${Logging.currentWorkingDirectory()} ${this.pathUtils.getDisplayName(options.cwd)}`); + } + + info.forEach((line) => { + traceInfo(line); + this.outputChannel.appendLine(line); + }); + } +} diff --git a/src/client/common/process/proc.ts b/src/client/common/process/proc.ts new file mode 100644 index 000000000000..54cf2f53bf43 --- /dev/null +++ b/src/client/common/process/proc.ts @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { exec, execSync, spawn } from 'child_process'; +import { EventEmitter } from 'events'; +import { Observable } from 'rxjs/Observable'; + +import { IDisposable } from '../types'; +import { createDeferred } from '../utils/async'; +import { EnvironmentVariables } from '../variables/types'; +import { DEFAULT_ENCODING } from './constants'; +import { + ExecutionResult, + IBufferDecoder, + IProcessService, + ObservableExecutionResult, + Output, + ShellOptions, + SpawnOptions, + StdErrError +} from './types'; + +// tslint:disable:no-any +export class ProcessService extends EventEmitter implements IProcessService { + private processesToKill = new Set(); + constructor(private readonly decoder: IBufferDecoder, private readonly env?: EnvironmentVariables) { + super(); + } + public static isAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } + } + public static kill(pid: number): void { + try { + if (process.platform === 'win32') { + // Windows doesn't support SIGTERM, so execute taskkill to kill the process + execSync(`taskkill /pid ${pid} /T /F`); + } else { + process.kill(pid); + } + } catch { + // Ignore. + } + } + public dispose() { + this.removeAllListeners(); + this.processesToKill.forEach((p) => { + try { + p.dispose(); + } catch { + // ignore. + } + }); + } + + public execObservable(file: string, args: string[], options: SpawnOptions = {}): ObservableExecutionResult { + const spawnOptions = this.getDefaultOptions(options); + const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + const proc = spawn(file, args, spawnOptions); + let procExited = false; + const disposable: IDisposable = { + // tslint:disable-next-line: no-function-expression + dispose: function () { + if (proc && !proc.killed && !procExited) { + ProcessService.kill(proc.pid); + } + if (proc) { + proc.unref(); + } + } + }; + this.processesToKill.add(disposable); + + const output = new Observable>((subscriber) => { + const disposables: IDisposable[] = []; + + const on = (ee: NodeJS.EventEmitter, name: string, fn: Function) => { + ee.on(name, fn as any); + disposables.push({ dispose: () => ee.removeListener(name, fn as any) as any }); + }; + + if (options.token) { + disposables.push( + options.token.onCancellationRequested(() => { + if (!procExited && !proc.killed) { + proc.kill(); + procExited = true; + } + }) + ); + } + + const sendOutput = (source: 'stdout' | 'stderr', data: Buffer) => { + const out = this.decoder.decode([data], encoding); + if (source === 'stderr' && options.throwOnStdErr) { + subscriber.error(new StdErrError(out)); + } else { + subscriber.next({ source, out: out }); + } + }; + + on(proc.stdout, 'data', (data: Buffer) => sendOutput('stdout', data)); + on(proc.stderr, 'data', (data: Buffer) => sendOutput('stderr', data)); + + proc.once('close', () => { + procExited = true; + subscriber.complete(); + disposables.forEach((d) => d.dispose()); + }); + proc.once('exit', () => { + procExited = true; + subscriber.complete(); + disposables.forEach((d) => d.dispose()); + }); + proc.once('error', (ex) => { + procExited = true; + subscriber.error(ex); + disposables.forEach((d) => d.dispose()); + }); + }); + + this.emit('exec', file, args, options); + + return { + proc, + out: output, + dispose: disposable.dispose + }; + } + public exec(file: string, args: string[], options: SpawnOptions = {}): Promise> { + const spawnOptions = this.getDefaultOptions(options); + const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + const proc = spawn(file, args, spawnOptions); + const deferred = createDeferred>(); + const disposable: IDisposable = { + dispose: () => { + if (!proc.killed && !deferred.completed) { + proc.kill(); + } + } + }; + this.processesToKill.add(disposable); + const disposables: IDisposable[] = []; + + const on = (ee: NodeJS.EventEmitter, name: string, fn: Function) => { + ee.on(name, fn as any); + disposables.push({ dispose: () => ee.removeListener(name, fn as any) as any }); + }; + + if (options.token) { + disposables.push(options.token.onCancellationRequested(disposable.dispose)); + } + + const stdoutBuffers: Buffer[] = []; + on(proc.stdout, 'data', (data: Buffer) => stdoutBuffers.push(data)); + const stderrBuffers: Buffer[] = []; + on(proc.stderr, 'data', (data: Buffer) => { + if (options.mergeStdOutErr) { + stdoutBuffers.push(data); + stderrBuffers.push(data); + } else { + stderrBuffers.push(data); + } + }); + + proc.once('close', () => { + if (deferred.completed) { + return; + } + const stderr: string | undefined = + stderrBuffers.length === 0 ? undefined : this.decoder.decode(stderrBuffers, encoding); + if (stderr && stderr.length > 0 && options.throwOnStdErr) { + deferred.reject(new StdErrError(stderr)); + } else { + const stdout = this.decoder.decode(stdoutBuffers, encoding); + deferred.resolve({ stdout, stderr }); + } + disposables.forEach((d) => d.dispose()); + }); + proc.once('error', (ex) => { + deferred.reject(ex); + disposables.forEach((d) => d.dispose()); + }); + + this.emit('exec', file, args, options); + + return deferred.promise; + } + + public shellExec(command: string, options: ShellOptions = {}): Promise> { + const shellOptions = this.getDefaultOptions(options); + return new Promise((resolve, reject) => { + const proc = exec(command, shellOptions, (e, stdout, stderr) => { + if (e && e !== null) { + reject(e); + } else if (shellOptions.throwOnStdErr && stderr && stderr.length) { + reject(new Error(stderr)); + } else { + // Make sure stderr is undefined if we actually had none. This is checked + // elsewhere because that's how exec behaves. + resolve({ stderr: stderr && stderr.length > 0 ? stderr : undefined, stdout: stdout }); + } + }); + const disposable: IDisposable = { + dispose: () => { + if (!proc.killed) { + proc.kill(); + } + } + }; + this.processesToKill.add(disposable); + }); + } + + private getDefaultOptions(options: T): T { + const defaultOptions = { ...options }; + const execOptions = defaultOptions as SpawnOptions; + if (execOptions) { + const encoding = (execOptions.encoding = + typeof execOptions.encoding === 'string' && execOptions.encoding.length > 0 + ? execOptions.encoding + : DEFAULT_ENCODING); + delete execOptions.encoding; + execOptions.encoding = encoding; + } + if (!defaultOptions.env || Object.keys(defaultOptions.env).length === 0) { + const env = this.env ? this.env : process.env; + defaultOptions.env = { ...env }; + } else { + defaultOptions.env = { ...defaultOptions.env }; + } + + if (execOptions && execOptions.extraVariables) { + defaultOptions.env = { ...defaultOptions.env, ...execOptions.extraVariables }; + } + + // Always ensure we have unbuffered output. + defaultOptions.env.PYTHONUNBUFFERED = '1'; + if (!defaultOptions.env.PYTHONIOENCODING) { + defaultOptions.env.PYTHONIOENCODING = 'utf-8'; + } + + return defaultOptions; + } +} diff --git a/src/client/common/process/processFactory.ts b/src/client/common/process/processFactory.ts new file mode 100644 index 000000000000..d6da78721138 --- /dev/null +++ b/src/client/common/process/processFactory.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IDisposableRegistry } from '../types'; +import { IEnvironmentVariablesProvider } from '../variables/types'; +import { ProcessService } from './proc'; +import { IBufferDecoder, IProcessLogger, IProcessService, IProcessServiceFactory } from './types'; + +@injectable() +export class ProcessServiceFactory implements IProcessServiceFactory { + constructor( + @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, + @inject(IProcessLogger) private readonly processLogger: IProcessLogger, + @inject(IBufferDecoder) private readonly decoder: IBufferDecoder, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry + ) {} + public async create(resource?: Uri): Promise { + const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); + const proc: IProcessService = new ProcessService(this.decoder, customEnvVars); + this.disposableRegistry.push(proc); + return proc.on('exec', this.processLogger.logProcess.bind(this.processLogger)); + } +} diff --git a/src/client/common/process/pythonDaemon.ts b/src/client/common/process/pythonDaemon.ts new file mode 100644 index 000000000000..b588af7f8053 --- /dev/null +++ b/src/client/common/process/pythonDaemon.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ChildProcess } from 'child_process'; +import { MessageConnection, RequestType, RequestType0 } from 'vscode-jsonrpc'; +import { PythonExecInfo } from '../../pythonEnvironments/exec'; +import { InterpreterInformation } from '../../pythonEnvironments/info'; +import { extractInterpreterInfo } from '../../pythonEnvironments/info/interpreter'; +import { traceWarning } from '../logger'; +import { IPlatformService } from '../platform/types'; +import { BasePythonDaemon } from './baseDaemon'; +import { PythonEnvInfo } from './internal/scripts'; +import { + IPythonDaemonExecutionService, + IPythonExecutionService, + ObservableExecutionResult, + SpawnOptions +} from './types'; + +type ErrorResponse = { error?: string }; + +export class ConnectionClosedError extends Error { + constructor(public readonly message: string) { + super(); + } +} + +export class DaemonError extends Error { + constructor(public readonly message: string) { + super(); + } +} +export class PythonDaemonExecutionService extends BasePythonDaemon implements IPythonDaemonExecutionService { + constructor( + pythonExecutionService: IPythonExecutionService, + platformService: IPlatformService, + pythonPath: string, + proc: ChildProcess, + connection: MessageConnection + ) { + super(pythonExecutionService, platformService, pythonPath, proc, connection); + } + public async getInterpreterInformation(): Promise { + try { + this.throwIfRPCConnectionIsDead(); + const request = new RequestType0('get_interpreter_information'); + const response = await this.sendRequestWithoutArgs(request); + if (response.error) { + throw Error(response.error); + } + return extractInterpreterInfo(this.pythonPath, response); + } catch (ex) { + traceWarning('Falling back to Python Execution Service due to failure in daemon', ex); + return this.pythonExecutionService.getInterpreterInformation(); + } + } + public async getExecutablePath(): Promise { + try { + this.throwIfRPCConnectionIsDead(); + type ExecutablePathResponse = ErrorResponse & { path: string }; + const request = new RequestType0('get_executable'); + const response = await this.sendRequestWithoutArgs(request); + if (response.error) { + throw new DaemonError(response.error); + } + return response.path; + } catch (ex) { + traceWarning('Falling back to Python Execution Service due to failure in daemon', ex); + return this.pythonExecutionService.getExecutablePath(); + } + } + public getExecutionInfo(pythonArgs?: string[]): PythonExecInfo { + return this.pythonExecutionService.getExecutionInfo(pythonArgs); + } + public async isModuleInstalled(moduleName: string): Promise { + try { + this.throwIfRPCConnectionIsDead(); + type ModuleInstalledResponse = ErrorResponse & { exists: boolean }; + const request = new RequestType<{ module_name: string }, ModuleInstalledResponse, void, void>( + 'is_module_installed' + ); + const response = await this.sendRequest(request, { module_name: moduleName }); + if (response.error) { + throw new DaemonError(response.error); + } + return response.exists; + } catch (ex) { + traceWarning('Falling back to Python Execution Service due to failure in daemon', ex); + return this.pythonExecutionService.isModuleInstalled(moduleName); + } + } + public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult { + if (this.isAlive && this.canExecFileUsingDaemon(args, options)) { + try { + return this.execAsObservable({ fileName: args[0] }, args.slice(1), options); + } catch (ex) { + if (ex instanceof DaemonError || ex instanceof ConnectionClosedError) { + traceWarning('Falling back to Python Execution Service due to failure in daemon', ex); + return this.pythonExecutionService.execObservable(args, options); + } else { + throw ex; + } + } + } else { + return this.pythonExecutionService.execObservable(args, options); + } + } + public execModuleObservable( + moduleName: string, + args: string[], + options: SpawnOptions + ): ObservableExecutionResult { + if (this.isAlive && this.canExecModuleUsingDaemon(moduleName, args, options)) { + try { + return this.execAsObservable({ moduleName }, args, options); + } catch (ex) { + if (ex instanceof DaemonError || ex instanceof ConnectionClosedError) { + traceWarning('Falling back to Python Execution Service due to failure in daemon', ex); + return this.pythonExecutionService.execModuleObservable(moduleName, args, options); + } else { + throw ex; + } + } + } else { + return this.pythonExecutionService.execModuleObservable(moduleName, args, options); + } + } +} diff --git a/src/client/common/process/pythonDaemonFactory.ts b/src/client/common/process/pythonDaemonFactory.ts new file mode 100644 index 000000000000..a4a450b95539 --- /dev/null +++ b/src/client/common/process/pythonDaemonFactory.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { ChildProcess } from 'child_process'; +import * as path from 'path'; +import { + createMessageConnection, + MessageConnection, + RequestType, + StreamMessageReader, + StreamMessageWriter +} from 'vscode-jsonrpc/node'; + +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { PYTHON_WARNINGS } from '../constants'; +import { traceDecorators, traceError } from '../logger'; +import { IPlatformService } from '../platform/types'; +import { IDisposable, IDisposableRegistry } from '../types'; +import { createDeferred } from '../utils/async'; +import { BasePythonDaemon } from './baseDaemon'; +import { PythonDaemonExecutionService } from './pythonDaemon'; +import { DaemonExecutionFactoryCreationOptions, IPythonDaemonExecutionService, IPythonExecutionService } from './types'; + +export class PythonDaemonFactory { + protected readonly envVariables: NodeJS.ProcessEnv; + protected readonly pythonPath: string; + constructor( + protected readonly disposables: IDisposableRegistry, + protected readonly options: DaemonExecutionFactoryCreationOptions, + protected readonly pythonExecutionService: IPythonExecutionService, + protected readonly platformService: IPlatformService, + protected readonly activatedEnvVariables?: NodeJS.ProcessEnv + ) { + if (!options.pythonPath) { + throw new Error('options.pythonPath is empty when it shoud not be'); + } + this.pythonPath = options.pythonPath; + // Setup environment variables for the daemon. + // The daemon must have access to the Python Module that'll run the daemon + // & also access to a Python package used for the JSON rpc comms. + const envPythonPath = `${path.join(EXTENSION_ROOT_DIR, 'pythonFiles')}${path.delimiter}${path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'lib', + 'python' + )}`; + this.envVariables = this.activatedEnvVariables ? { ...this.activatedEnvVariables } : { ...process.env }; + this.envVariables.PYTHONPATH = this.envVariables.PYTHONPATH + ? `${this.envVariables.PYTHONPATH}${path.delimiter}${envPythonPath}` + : envPythonPath; + this.envVariables.PYTHONUNBUFFERED = '1'; + + // Always ignore warnings as the user should never see the output of the daemon running + this.envVariables[PYTHON_WARNINGS] = 'ignore'; + } + @traceDecorators.error('Failed to create daemon') + public async createDaemonService(): Promise { + // Add '--log-file=/Users/donjayamanne/Desktop/Development/vsc/pythonVSCode/daaemon.log' to log to a file. + const loggingArgs: string[] = ['-v']; // Log information messages or greater (see daemon.__main__.py for options). + + const args = (this.options.daemonModule ? [`--daemon-module=${this.options.daemonModule}`] : []).concat( + loggingArgs + ); + const env = this.envVariables; + const daemonProc = this.pythonExecutionService!.execModuleObservable( + 'vscode_datascience_helpers.daemon', + args, + { env } + ); + if (!daemonProc.proc) { + throw new Error('Failed to create Daemon Proc'); + } + const connection = this.createConnection(daemonProc.proc); + + connection.listen(); + let stdError = ''; + let procEndEx: Error | undefined; + daemonProc.proc.stderr.on('data', (data: string | Buffer) => { + data = typeof data === 'string' ? data : data.toString('utf8'); + stdError += data; + }); + daemonProc.proc.on('error', (ex) => (procEndEx = ex)); + + try { + await this.testDaemon(connection); + + const cls = this.options.daemonClass ?? PythonDaemonExecutionService; + const instance = new cls( + this.pythonExecutionService, + this.platformService, + this.pythonPath, + daemonProc.proc, + connection + ); + if (instance instanceof BasePythonDaemon) { + this.disposables.push(instance); + return (instance as unknown) as T; + } + throw new Error(`Daemon class ${cls.name} must inherit BasePythonDaemon.`); + } catch (ex) { + traceError('Failed to start the Daemon, StdErr: ', stdError); + traceError('Failed to start the Daemon, ProcEndEx', procEndEx || ex); + traceError('Failed to start the Daemon, Ex', ex); + throw ex; + } + } + /** + * Protected so we can override for testing purposes. + */ + protected createConnection(proc: ChildProcess) { + return createMessageConnection(new StreamMessageReader(proc.stdout), new StreamMessageWriter(proc.stdin)); + } + /** + * Tests whether a daemon is usable or not by checking whether it responds to a simple ping. + * If a daemon doesn't reply to a ping in 5s, then its deemed to be dead/not usable. + * + * @private + * @param {MessageConnection} connection + * @memberof PythonDaemonExecutionServicePool + */ + @traceDecorators.error('Pinging Daemon Failed') + protected async testDaemon(connection: MessageConnection) { + // If we don't get a reply to the ping in 5 seconds assume it will never work. Bomb out. + // At this point there should be some information logged in stderr of the daemon process. + const fail = createDeferred<{ pong: string }>(); + const timer = setTimeout(() => fail.reject(new Error('Timeout waiting for daemon to start')), 5_000); + const request = new RequestType<{ data: string }, { pong: string }, void, void>('ping'); + // Check whether the daemon has started correctly, by sending a ping. + const result = await Promise.race([fail.promise, connection.sendRequest(request, { data: 'hello' })]); + clearTimeout(timer); + if (result.pong !== 'hello') { + throw new Error(`Daemon did not reply to the ping, received: ${result.pong}`); + } + } +} diff --git a/src/client/common/process/pythonDaemonPool.ts b/src/client/common/process/pythonDaemonPool.ts new file mode 100644 index 000000000000..e82d52f4828d --- /dev/null +++ b/src/client/common/process/pythonDaemonPool.ts @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IPlatformService } from '../../common/platform/types'; +import { PythonExecInfo } from '../../pythonEnvironments/exec'; +import { InterpreterInformation } from '../../pythonEnvironments/info'; +import { IDisposableRegistry } from '../types'; +import { sleep } from '../utils/async'; +import { noop } from '../utils/misc'; +import { StopWatch } from '../utils/stopWatch'; +import { ProcessService } from './proc'; +import { PythonDaemonExecutionService } from './pythonDaemon'; +import { PythonDaemonFactory } from './pythonDaemonFactory'; +import { + ExecutionResult, + IProcessLogger, + IPythonDaemonExecutionService, + IPythonExecutionService, + isDaemonPoolCreationOption, + ObservableExecutionResult, + PooledDaemonExecutionFactoryCreationOptions, + SpawnOptions +} from './types'; + +type DaemonType = 'StandardDaemon' | 'ObservableDaemon'; + +export class PythonDaemonExecutionServicePool extends PythonDaemonFactory implements IPythonDaemonExecutionService { + private readonly daemons: IPythonDaemonExecutionService[] = []; + private readonly observableDaemons: IPythonDaemonExecutionService[] = []; + private _disposed = false; + constructor( + private readonly logger: IProcessLogger, + disposables: IDisposableRegistry, + options: PooledDaemonExecutionFactoryCreationOptions, + pythonExecutionService: IPythonExecutionService, + platformService: IPlatformService, + activatedEnvVariables?: NodeJS.ProcessEnv, + private readonly timeoutWaitingForDaemon: number = 1_000 + ) { + super(disposables, options, pythonExecutionService, platformService, activatedEnvVariables); + this.disposables.push(this); + } + public async initialize() { + if (!isDaemonPoolCreationOption(this.options)) { + return; + } + const promises = Promise.all( + [ + // tslint:disable-next-line: prefer-array-literal + ...new Array(this.options.daemonCount ?? 2).keys() + ].map(() => this.addDaemonService('StandardDaemon')) + ); + const promises2 = Promise.all( + [ + // tslint:disable-next-line: prefer-array-literal + ...new Array(this.options.observableDaemonCount ?? 1).keys() + ].map(() => this.addDaemonService('ObservableDaemon')) + ); + + await Promise.all([promises, promises2]); + } + public dispose() { + this._disposed = true; + } + public async getInterpreterInformation(): Promise { + const msg = { args: ['GetPythonVersion'] }; + return this.wrapCall((daemon) => daemon.getInterpreterInformation(), msg); + } + public async getExecutablePath(): Promise { + const msg = { args: ['getExecutablePath'] }; + return this.wrapCall((daemon) => daemon.getExecutablePath(), msg); + } + public getExecutionInfo(pythonArgs?: string[]): PythonExecInfo { + return this.pythonExecutionService.getExecutionInfo(pythonArgs); + } + public async isModuleInstalled(moduleName: string): Promise { + const msg = { args: ['-m', moduleName] }; + return this.wrapCall((daemon) => daemon.isModuleInstalled(moduleName), msg); + } + public async exec(args: string[], options: SpawnOptions): Promise> { + const msg = { args, options }; + return this.wrapCall((daemon) => daemon.exec(args, options), msg); + } + public async execModule( + moduleName: string, + args: string[], + options: SpawnOptions + ): Promise> { + const msg = { args: ['-m', moduleName].concat(args), options }; + return this.wrapCall((daemon) => daemon.execModule(moduleName, args, options), msg); + } + public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult { + const msg = { args, options }; + return this.wrapObservableCall((daemon) => daemon.execObservable(args, options), msg); + } + public execModuleObservable( + moduleName: string, + args: string[], + options: SpawnOptions + ): ObservableExecutionResult { + const msg = { args: ['-m', moduleName].concat(args), options }; + return this.wrapObservableCall((daemon) => daemon.execModuleObservable(moduleName, args, options), msg); + } + /** + * Wrapper for all promise operations to be performed on a daemon. + * Gets a daemon from the pool, executes the required code, then returns the daemon back into the pool. + * + * @private + * @template T + * @param {(daemon: IPythonExecutionService) => Promise} cb + * @param daemonLogMessage + * @returns {Promise} + * @memberof PythonDaemonExecutionServicePool + */ + private async wrapCall( + cb: (daemon: IPythonExecutionService) => Promise, + daemonLogMessage: { args: string[]; options?: SpawnOptions } + ): Promise { + const daemon = await this.popDaemonFromPool(); + try { + // When using the daemon, log the message ourselves. + if (daemon instanceof PythonDaemonExecutionService) { + this.logger.logProcess(`${this.pythonPath} (daemon)`, daemonLogMessage.args, daemonLogMessage.options); + } + return await cb(daemon); + } finally { + this.pushDaemonIntoPool('StandardDaemon', daemon); + } + } + /** + * Wrapper for all observable operations to be performed on a daemon. + * Gets a daemon from the pool, executes the required code, then returns the daemon back into the pool. + * + * @private + * @param {(daemon: IPythonExecutionService) => ObservableExecutionResult} cb + * @param daemonLogMessage + * @returns {ObservableExecutionResult} + * @memberof PythonDaemonExecutionServicePool + */ + private wrapObservableCall( + cb: (daemon: IPythonExecutionService) => ObservableExecutionResult, + daemonLogMessage: { args: string[]; options?: SpawnOptions } + ): ObservableExecutionResult { + const execService = this.popDaemonFromObservablePool(); + // Possible the daemon returned is a standard python execution service. + const daemonProc = execService instanceof PythonDaemonExecutionService ? execService.proc : undefined; + + // When using the daemon, log the message ourselves. + if (daemonProc) { + this.logger.logProcess(`${this.pythonPath} (daemon)`, daemonLogMessage.args, daemonLogMessage.options); + } + const result = cb(execService); + let completed = false; + const completeHandler = () => { + if (completed) { + return; + } + completed = true; + if (!daemonProc || (!daemonProc.killed && ProcessService.isAlive(daemonProc.pid))) { + this.pushDaemonIntoPool('ObservableDaemon', execService); + } else if (!this._disposed) { + // Possible daemon is dead (explicitly killed or died due to some error). + this.addDaemonService('ObservableDaemon').ignoreErrors(); + } + }; + + if (daemonProc) { + daemonProc.on('exit', completeHandler); + daemonProc.on('close', completeHandler); + } + result.out.subscribe(noop, completeHandler, completeHandler); + + return result; + } + /** + * Adds a daemon into a pool. + * + * @private + * @param {DaemonType} type + * @memberof PythonDaemonExecutionServicePool + */ + private async addDaemonService(type: DaemonType) { + const daemon = await this.createDaemonService(); + const pool = type === 'StandardDaemon' ? this.daemons : this.observableDaemons; + pool.push(daemon); + } + /** + * Gets a daemon from a pool. + * If we're unable to get a daemon from a pool within 1s, then return the standard `PythonExecutionService`. + * The `PythonExecutionService` will spanw the required python process and do the needful. + * + * @private + * @returns {Promise} + * @memberof PythonDaemonExecutionServicePool + */ + private async popDaemonFromPool(): Promise { + const stopWatch = new StopWatch(); + while (this.daemons.length === 0 && stopWatch.elapsedTime <= this.timeoutWaitingForDaemon) { + await sleep(50); + } + return this.daemons.shift() ?? this.pythonExecutionService; + } + /** + * Gets a daemon from a pool for observable operations. + * If we're unable to get a daemon from a pool, then return the standard `PythonExecutionService`. + * The `PythonExecutionService` will spanw the required python process and do the needful. + * + * @private + * @returns {IPythonExecutionService} + * @memberof PythonDaemonExecutionServicePool + */ + private popDaemonFromObservablePool(): IPythonExecutionService { + if (this.observableDaemons.length > 0) { + return this.observableDaemons.shift()!; + } + return this.pythonExecutionService; + } + /** + * Pushes a daemon back into the pool. + * Before doing this, check whether the daemon is usable or not. + * If not, then create a new daemon and add it into the pool. + * + * @private + * @param {DaemonType} type + * @param {IPythonExecutionService} daemon + * @returns + * @memberof PythonDaemonExecutionServicePool + */ + private pushDaemonIntoPool(type: DaemonType, daemon: IPythonExecutionService) { + if (daemon === this.pythonExecutionService) { + return; + } + // Ensure we test the daemon before we push it back into the pool. + // Possible it is dead. + const testAndPushIntoPool = async () => { + const daemonService = daemon as PythonDaemonExecutionService; + let procIsDead = false; + if ( + !daemonService.isAlive || + daemonService.proc.killed || + !ProcessService.isAlive(daemonService.proc.pid) + ) { + procIsDead = true; + } else { + // Test sending a ping. + procIsDead = await this.testDaemon(daemonService.connection) + .then(() => false) + .catch(() => true); + } + if (procIsDead) { + // The process is dead, create a new daemon. + await this.addDaemonService(type); + try { + daemonService.dispose(); + } catch { + noop(); + } + } else { + const pool = type === 'StandardDaemon' ? this.daemons : this.observableDaemons; + pool.push(daemon as IPythonDaemonExecutionService); + } + }; + + testAndPushIntoPool().ignoreErrors(); + } +} diff --git a/src/client/common/process/pythonEnvironment.ts b/src/client/common/process/pythonEnvironment.ts new file mode 100644 index 000000000000..2f3a86c953cb --- /dev/null +++ b/src/client/common/process/pythonEnvironment.ts @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CondaEnvironmentInfo } from '../../pythonEnvironments/discovery/locators/services/conda'; +import { buildPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/exec'; +import { InterpreterInformation } from '../../pythonEnvironments/info'; +import { getExecutablePath } from '../../pythonEnvironments/info/executable'; +import { getInterpreterInfo } from '../../pythonEnvironments/info/interpreter'; +import { traceError, traceInfo } from '../logger'; +import { IFileSystem } from '../platform/types'; +import * as internalPython from './internal/python'; +import { ExecutionResult, IProcessService, ShellOptions, SpawnOptions } from './types'; + +class PythonEnvironment { + private cachedInterpreterInformation: InterpreterInformation | undefined | null = null; + + constructor( + protected readonly pythonPath: string, + // "deps" is the externally defined functionality used by the class. + protected readonly deps: { + getPythonArgv(python: string): string[]; + getObservablePythonArgv(python: string): string[]; + isValidExecutable(python: string): Promise; + // from ProcessService: + exec(file: string, args: string[]): Promise>; + shellExec(command: string, timeout: number): Promise>; + } + ) {} + + public getExecutionInfo(pythonArgs: string[] = []): PythonExecInfo { + const python = this.deps.getPythonArgv(this.pythonPath); + return buildPythonExecInfo(python, pythonArgs); + } + public getExecutionObservableInfo(pythonArgs: string[] = []): PythonExecInfo { + const python = this.deps.getObservablePythonArgv(this.pythonPath); + return buildPythonExecInfo(python, pythonArgs); + } + + public async getInterpreterInformation(): Promise { + if (this.cachedInterpreterInformation === null) { + this.cachedInterpreterInformation = await this.getInterpreterInformationImpl(); + } + return this.cachedInterpreterInformation; + } + + public async getExecutablePath(): Promise { + // If we've passed the python file, then return the file. + // This is because on mac if using the interpreter /usr/bin/python2.7 we can get a different value for the path + if (await this.deps.isValidExecutable(this.pythonPath)) { + return this.pythonPath; + } + const python = this.getExecutionInfo(); + return getExecutablePath(python, this.deps.exec); + } + + public async isModuleInstalled(moduleName: string): Promise { + // prettier-ignore + const [args,] = internalPython.isModuleInstalled(moduleName); + const info = this.getExecutionInfo(args); + try { + await this.deps.exec(info.command, info.args); + } catch { + return false; + } + return true; + } + + private async getInterpreterInformationImpl(): Promise { + try { + const python = this.getExecutionInfo(); + return await getInterpreterInfo(python, this.deps.shellExec, { info: traceInfo, error: traceError }); + } catch (ex) { + traceError(`Failed to get interpreter information for '${this.pythonPath}'`, ex); + } + } +} + +function createDeps( + isValidExecutable: (filename: string) => Promise, + pythonArgv: string[] | undefined, + observablePythonArgv: string[] | undefined, + // from ProcessService: + exec: (file: string, args: string[], options?: SpawnOptions) => Promise>, + shellExec: (command: string, options?: ShellOptions) => Promise> +) { + return { + getPythonArgv: (python: string) => pythonArgv || [python], + getObservablePythonArgv: (python: string) => observablePythonArgv || [python], + isValidExecutable, + exec: async (cmd: string, args: string[]) => exec(cmd, args, { throwOnStdErr: true }), + shellExec: async (text: string, timeout: number) => shellExec(text, { timeout }) + }; +} + +export function createPythonEnv( + pythonPath: string, + // These are used to generate the deps. + procs: IProcessService, + fs: IFileSystem +): PythonEnvironment { + const deps = createDeps( + async (filename) => fs.fileExists(filename), + // We use the default: [pythonPath]. + undefined, + undefined, + (file, args, opts) => procs.exec(file, args, opts), + (command, opts) => procs.shellExec(command, opts) + ); + return new PythonEnvironment(pythonPath, deps); +} + +export function createCondaEnv( + condaFile: string, + condaInfo: CondaEnvironmentInfo, + pythonPath: string, + // These are used to generate the deps. + procs: IProcessService, + fs: IFileSystem +): PythonEnvironment { + const runArgs = ['run']; + if (condaInfo.name === '') { + runArgs.push('-p', condaInfo.path); + } else { + runArgs.push('-n', condaInfo.name); + } + const pythonArgv = [condaFile, ...runArgs, 'python']; + const deps = createDeps( + async (filename) => fs.fileExists(filename), + pythonArgv, + // tslint:disable-next-line:no-suspicious-comment + // TODO: Use pythonArgv here once 'conda run' can be + // run without buffering output. + // See https://github.com/microsoft/vscode-python/issues/8473. + undefined, + (file, args, opts) => procs.exec(file, args, opts), + (command, opts) => procs.shellExec(command, opts) + ); + return new PythonEnvironment(pythonPath, deps); +} + +export function createWindowsStoreEnv( + pythonPath: string, + // These are used to generate the deps. + procs: IProcessService +): PythonEnvironment { + const deps = createDeps( + /** + * With windows store python apps, we have generally use the + * symlinked python executable. The actual file is not accessible + * by the user due to permission issues (& rest of exension fails + * when using that executable). Hence lets not resolve the + * executable using sys.executable for windows store python + * interpreters. + */ + async (_f: string) => true, + // We use the default: [pythonPath]. + undefined, + undefined, + (file, args, opts) => procs.exec(file, args, opts), + (command, opts) => procs.shellExec(command, opts) + ); + return new PythonEnvironment(pythonPath, deps); +} diff --git a/src/client/common/process/pythonExecutionFactory.ts b/src/client/common/process/pythonExecutionFactory.ts new file mode 100644 index 000000000000..f6432785bf71 --- /dev/null +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { inject, injectable } from 'inversify'; +import { gte } from 'semver'; + +import { Uri } from 'vscode'; +import { IPlatformService } from '../../common/platform/types'; +import { IEnvironmentActivationService } from '../../interpreter/activation/types'; +import { ICondaService, IInterpreterService } from '../../interpreter/contracts'; +import { IWindowsStoreInterpreter } from '../../interpreter/locators/types'; +import { IServiceContainer } from '../../ioc/types'; +import { CondaEnvironmentInfo } from '../../pythonEnvironments/discovery/locators/services/conda'; +import { WindowsStoreInterpreter } from '../../pythonEnvironments/discovery/locators/services/windowsStoreInterpreter'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { traceError } from '../logger'; +import { IFileSystem } from '../platform/types'; +import { IConfigurationService, IDisposable, IDisposableRegistry } from '../types'; +import { ProcessService } from './proc'; +import { PythonDaemonFactory } from './pythonDaemonFactory'; +import { PythonDaemonExecutionServicePool } from './pythonDaemonPool'; +import { createCondaEnv, createPythonEnv, createWindowsStoreEnv } from './pythonEnvironment'; +import { createPythonProcessService } from './pythonProcess'; +import { + DaemonExecutionFactoryCreationOptions, + ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionFactoryCreationOptions, + IBufferDecoder, + IProcessLogger, + IProcessService, + IProcessServiceFactory, + IPythonDaemonExecutionService, + IPythonExecutionFactory, + IPythonExecutionService, + isDaemonPoolCreationOption +} from './types'; + +// Minimum version number of conda required to be able to use 'conda run' +export const CONDA_RUN_VERSION = '4.6.0'; + +@injectable() +export class PythonExecutionFactory implements IPythonExecutionFactory { + private readonly daemonsPerPythonService = new Map>(); + private readonly disposables: IDisposableRegistry; + private readonly logger: IProcessLogger; + private readonly fileSystem: IFileSystem; + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IEnvironmentActivationService) private readonly activationHelper: IEnvironmentActivationService, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + @inject(IConfigurationService) private readonly configService: IConfigurationService, + @inject(ICondaService) private readonly condaService: ICondaService, + @inject(IBufferDecoder) private readonly decoder: IBufferDecoder, + @inject(WindowsStoreInterpreter) private readonly windowsStoreInterpreter: IWindowsStoreInterpreter, + @inject(IPlatformService) private readonly platformService: IPlatformService + ) { + // Acquire other objects here so that if we are called during dispose they are available. + this.disposables = this.serviceContainer.get(IDisposableRegistry); + this.logger = this.serviceContainer.get(IProcessLogger); + this.fileSystem = this.serviceContainer.get(IFileSystem); + } + public async create(options: ExecutionFactoryCreationOptions): Promise { + const pythonPath = options.pythonPath + ? options.pythonPath + : this.configService.getSettings(options.resource).pythonPath; + const processService: IProcessService = await this.processServiceFactory.create(options.resource); + processService.on('exec', this.logger.logProcess.bind(this.logger)); + + return createPythonService( + pythonPath, + processService, + this.fileSystem, + undefined, + this.windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath) + ); + } + + public async createDaemon( + options: DaemonExecutionFactoryCreationOptions + ): Promise { + const pythonPath = options.pythonPath + ? options.pythonPath + : this.configService.getSettings(options.resource).pythonPath; + const daemonPoolKey = `${pythonPath}#${options.daemonClass || ''}#${options.daemonModule || ''}`; + const interpreterService = this.serviceContainer.tryGet(IInterpreterService); + const interpreter = interpreterService + ? await interpreterService.getInterpreterDetails(pythonPath, options.resource) + : undefined; + const activatedProcPromise = this.createActivatedEnvironment({ + allowEnvironmentFetchExceptions: true, + interpreter: interpreter, + resource: options.resource, + bypassCondaExecution: true + }); + // No daemon support in Python 2.7 or during shutdown + if (!interpreterService || (interpreter?.version && interpreter.version.major < 3)) { + return activatedProcPromise; + } + + // Ensure we do not start multiple daemons for the same interpreter. + // Cache the promise. + const start = async (): Promise => { + const [activatedProc, activatedEnvVars] = await Promise.all([ + activatedProcPromise, + this.activationHelper.getActivatedEnvironmentVariables(options.resource, interpreter, true) + ]); + + if (isDaemonPoolCreationOption(options)) { + const daemon = new PythonDaemonExecutionServicePool( + this.logger, + this.disposables, + { ...options, pythonPath }, + activatedProc!, + this.platformService, + activatedEnvVars + ); + await daemon.initialize(); + this.disposables.push(daemon); + return (daemon as unknown) as T; + } else { + const factory = new PythonDaemonFactory( + this.disposables, + { ...options, pythonPath }, + activatedProc!, + this.platformService, + activatedEnvVars + ); + return factory.createDaemonService(); + } + }; + + let promise: Promise; + + if (isDaemonPoolCreationOption(options)) { + // Ensure we do not create multiple daemon pools for the same python interpreter. + promise = (this.daemonsPerPythonService.get(daemonPoolKey) as unknown) as Promise; + if (!promise) { + promise = start(); + this.daemonsPerPythonService.set(daemonPoolKey, promise as Promise); + } + } else { + promise = start(); + } + return promise.catch((ex) => { + // Ok, we failed to create the daemon (or failed to start). + // What ever the cause, we need to log this & give a standard IPythonExecutionService + traceError('Failed to create the daemon service, defaulting to activated environment', ex); + this.daemonsPerPythonService.delete(daemonPoolKey); + return (activatedProcPromise as unknown) as T; + }); + } + public async createActivatedEnvironment( + options: ExecutionFactoryCreateWithEnvironmentOptions + ): Promise { + const envVars = await this.activationHelper.getActivatedEnvironmentVariables( + options.resource, + options.interpreter, + options.allowEnvironmentFetchExceptions + ); + const hasEnvVars = envVars && Object.keys(envVars).length > 0; + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, undefined, { hasEnvVars }); + if (!hasEnvVars) { + return this.create({ + resource: options.resource, + pythonPath: options.interpreter ? options.interpreter.path : undefined + }); + } + const pythonPath = options.interpreter + ? options.interpreter.path + : this.configService.getSettings(options.resource).pythonPath; + const processService: IProcessService = new ProcessService(this.decoder, { ...envVars }); + processService.on('exec', this.logger.logProcess.bind(this.logger)); + this.disposables.push(processService); + + return createPythonService(pythonPath, processService, this.fileSystem); + } + // Not using this function for now because there are breaking issues with conda run (conda 4.8, PVSC 2020.1). + // See https://github.com/microsoft/vscode-python/issues/9490 + public async createCondaExecutionService( + pythonPath: string, + processService?: IProcessService, + resource?: Uri + ): Promise { + const processServicePromise = processService + ? Promise.resolve(processService) + : this.processServiceFactory.create(resource); + const [condaVersion, condaEnvironment, condaFile, procService] = await Promise.all([ + this.condaService.getCondaVersion(), + this.condaService.getCondaEnvironment(pythonPath), + this.condaService.getCondaFile(), + processServicePromise + ]); + + if (condaVersion && gte(condaVersion, CONDA_RUN_VERSION) && condaEnvironment && condaFile && procService) { + // Add logging to the newly created process service + if (!processService) { + procService.on('exec', this.logger.logProcess.bind(this.logger)); + this.disposables.push(procService); + } + return createPythonService( + pythonPath, + procService, + this.fileSystem, + // This is what causes a CondaEnvironment to be returned: + [condaFile, condaEnvironment] + ); + } + + return Promise.resolve(undefined); + } +} + +function createPythonService( + pythonPath: string, + procService: IProcessService, + fs: IFileSystem, + conda?: [string, CondaEnvironmentInfo], + isWindowsStore?: boolean +): IPythonExecutionService { + let env = createPythonEnv(pythonPath, procService, fs); + if (conda) { + const [condaPath, condaInfo] = conda; + env = createCondaEnv(condaPath, condaInfo, pythonPath, procService, fs); + } else if (isWindowsStore) { + env = createWindowsStoreEnv(pythonPath, procService); + } + const procs = createPythonProcessService(procService, env); + return { + getInterpreterInformation: () => env.getInterpreterInformation(), + getExecutablePath: () => env.getExecutablePath(), + isModuleInstalled: (m) => env.isModuleInstalled(m), + getExecutionInfo: (a) => env.getExecutionInfo(a), + execObservable: (a, o) => procs.execObservable(a, o), + execModuleObservable: (m, a, o) => procs.execModuleObservable(m, a, o), + exec: (a, o) => procs.exec(a, o), + execModule: (m, a, o) => procs.execModule(m, a, o) + }; +} diff --git a/src/client/common/process/pythonProcess.ts b/src/client/common/process/pythonProcess.ts new file mode 100644 index 000000000000..b6ce351ee318 --- /dev/null +++ b/src/client/common/process/pythonProcess.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonExecInfo } from '../../pythonEnvironments/exec'; +import { ErrorUtils } from '../errors/errorUtils'; +import { ModuleNotInstalledError } from '../errors/moduleNotInstalledError'; +import * as internalPython from './internal/python'; +import { ExecutionResult, IProcessService, ObservableExecutionResult, SpawnOptions } from './types'; + +class PythonProcessService { + constructor( + // This is the externally defined functionality used by the class. + private readonly deps: { + // from PythonEnvironment: + isModuleInstalled(moduleName: string): Promise; + getExecutionInfo(pythonArgs?: string[]): PythonExecInfo; + getExecutionObservableInfo(pythonArgs?: string[]): PythonExecInfo; + // from ProcessService: + exec(file: string, args: string[], options: SpawnOptions): Promise>; + execObservable(file: string, args: string[], options: SpawnOptions): ObservableExecutionResult; + } + ) {} + + public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult { + const opts: SpawnOptions = { ...options }; + const executable = this.deps.getExecutionObservableInfo(args); + return this.deps.execObservable(executable.command, executable.args, opts); + } + + public execModuleObservable( + moduleName: string, + moduleArgs: string[], + options: SpawnOptions + ): ObservableExecutionResult { + const args = internalPython.execModule(moduleName, moduleArgs); + const opts: SpawnOptions = { ...options }; + const executable = this.deps.getExecutionObservableInfo(args); + return this.deps.execObservable(executable.command, executable.args, opts); + } + + public async exec(args: string[], options: SpawnOptions): Promise> { + const opts: SpawnOptions = { ...options }; + const executable = this.deps.getExecutionInfo(args); + return this.deps.exec(executable.command, executable.args, opts); + } + + public async execModule( + moduleName: string, + moduleArgs: string[], + options: SpawnOptions + ): Promise> { + const args = internalPython.execModule(moduleName, moduleArgs); + const opts: SpawnOptions = { ...options }; + const executable = this.deps.getExecutionInfo(args); + const result = await this.deps.exec(executable.command, executable.args, opts); + + // If a module is not installed we'll have something in stderr. + if (moduleName && ErrorUtils.outputHasModuleNotInstalledError(moduleName, result.stderr)) { + const isInstalled = await this.deps.isModuleInstalled(moduleName); + if (!isInstalled) { + throw new ModuleNotInstalledError(moduleName); + } + } + + return result; + } +} + +export function createPythonProcessService( + procs: IProcessService, + // from PythonEnvironment: + env: { + getExecutionInfo(pythonArgs?: string[]): PythonExecInfo; + getExecutionObservableInfo(pythonArgs?: string[]): PythonExecInfo; + isModuleInstalled(moduleName: string): Promise; + } +) { + const deps = { + // from PythonService: + isModuleInstalled: async (m: string) => env.isModuleInstalled(m), + getExecutionInfo: (a?: string[]) => env.getExecutionInfo(a), + getExecutionObservableInfo: (a?: string[]) => env.getExecutionObservableInfo(a), + // from ProcessService: + exec: async (f: string, a: string[], o: SpawnOptions) => procs.exec(f, a, o), + execObservable: (f: string, a: string[], o: SpawnOptions) => procs.execObservable(f, a, o) + }; + return new PythonProcessService(deps); +} diff --git a/src/client/common/process/pythonToolService.ts b/src/client/common/process/pythonToolService.ts new file mode 100644 index 000000000000..cb1068651c82 --- /dev/null +++ b/src/client/common/process/pythonToolService.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; +import { ExecutionInfo } from '../types'; +import { + ExecutionResult, + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonToolExecutionService, + ObservableExecutionResult, + SpawnOptions +} from './types'; + +@injectable() +export class PythonToolExecutionService implements IPythonToolExecutionService { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + public async execObservable( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri + ): Promise> { + if (options.env) { + throw new Error('Environment variables are not supported'); + } + if (executionInfo.moduleName && executionInfo.moduleName.length > 0) { + const pythonExecutionService = await this.serviceContainer + .get(IPythonExecutionFactory) + .create({ resource }); + return pythonExecutionService.execModuleObservable(executionInfo.moduleName, executionInfo.args, options); + } else { + const processService = await this.serviceContainer + .get(IProcessServiceFactory) + .create(resource); + return processService.execObservable(executionInfo.execPath!, executionInfo.args, { ...options }); + } + } + public async exec( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri + ): Promise> { + if (options.env) { + throw new Error('Environment variables are not supported'); + } + if (executionInfo.moduleName && executionInfo.moduleName.length > 0) { + const pythonExecutionService = await this.serviceContainer + .get(IPythonExecutionFactory) + .create({ resource }); + return pythonExecutionService.execModule(executionInfo.moduleName!, executionInfo.args, options); + } else { + const processService = await this.serviceContainer + .get(IProcessServiceFactory) + .create(resource); + return processService.exec(executionInfo.execPath!, executionInfo.args, { ...options }); + } + } +} diff --git a/src/client/common/process/serviceRegistry.ts b/src/client/common/process/serviceRegistry.ts new file mode 100644 index 000000000000..27684a20cc32 --- /dev/null +++ b/src/client/common/process/serviceRegistry.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceManager } from '../../ioc/types'; +import { BufferDecoder } from './decoder'; +import { ProcessServiceFactory } from './processFactory'; +import { PythonExecutionFactory } from './pythonExecutionFactory'; +import { PythonToolExecutionService } from './pythonToolService'; +import { IBufferDecoder, IProcessServiceFactory, IPythonExecutionFactory, IPythonToolExecutionService } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IBufferDecoder, BufferDecoder); + serviceManager.addSingleton(IProcessServiceFactory, ProcessServiceFactory); + serviceManager.addSingleton(IPythonExecutionFactory, PythonExecutionFactory); + serviceManager.addSingleton(IPythonToolExecutionService, PythonToolExecutionService); +} diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts new file mode 100644 index 000000000000..df1a030d48c3 --- /dev/null +++ b/src/client/common/process/types.ts @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ChildProcess, ExecOptions, SpawnOptions as ChildProcessSpawnOptions } from 'child_process'; +import { Observable } from 'rxjs/Observable'; +import { CancellationToken, Uri } from 'vscode'; + +import { Newable } from '../../ioc/types'; +import { PythonExecInfo } from '../../pythonEnvironments/exec'; +import { InterpreterInformation, PythonEnvironment } from '../../pythonEnvironments/info'; +import { ExecutionInfo, IDisposable } from '../types'; +import { EnvironmentVariables } from '../variables/types'; + +export const IBufferDecoder = Symbol('IBufferDecoder'); +export interface IBufferDecoder { + decode(buffers: Buffer[], encoding: string): string; +} + +export type Output = { + source: 'stdout' | 'stderr'; + out: T; +}; +export type ObservableExecutionResult = { + proc: ChildProcess | undefined; + out: Observable>; + dispose(): void; +}; + +// tslint:disable-next-line:interface-name +export type SpawnOptions = ChildProcessSpawnOptions & { + encoding?: string; + token?: CancellationToken; + mergeStdOutErr?: boolean; + throwOnStdErr?: boolean; + extraVariables?: NodeJS.ProcessEnv; +}; + +// tslint:disable-next-line:interface-name +export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; + +export type ExecutionResult = { + stdout: T; + stderr?: T; +}; + +export const IProcessLogger = Symbol('IProcessLogger'); +export interface IProcessLogger { + logProcess(file: string, ars: string[], options?: SpawnOptions): void; +} + +export interface IProcessService extends IDisposable { + execObservable(file: string, args: string[], options?: SpawnOptions): ObservableExecutionResult; + exec(file: string, args: string[], options?: SpawnOptions): Promise>; + shellExec(command: string, options?: ShellOptions): Promise>; + on(event: 'exec', listener: (file: string, args: string[], options?: SpawnOptions) => void): this; +} + +export const IProcessServiceFactory = Symbol('IProcessServiceFactory'); + +export interface IProcessServiceFactory { + create(resource?: Uri): Promise; +} + +export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory'); +export type ExecutionFactoryCreationOptions = { + resource?: Uri; + pythonPath?: string; +}; +export function isDaemonPoolCreationOption( + options: PooledDaemonExecutionFactoryCreationOptions | DedicatedDaemonExecutionFactoryCreationOptions +): options is PooledDaemonExecutionFactoryCreationOptions { + if ('dedicated' in options && options.dedicated === true) { + return false; + } else { + return true; + } +} + +// This daemon will belong to a daemon pool (i.e it goes back into a pool for re-use). +export type PooledDaemonExecutionFactoryCreationOptions = ExecutionFactoryCreationOptions & { + /** + * Python file that implements the daemon. + * + * @type {string} + */ + daemonModule?: string; + /** + * Typescript Daemon class (client) that maps to the Python daemon. + * Defaults to `PythonDaemonExecutionService`. + * Any other class provided must extend `PythonDaemonExecutionService`. + * + * @type {Newable} + */ + daemonClass?: Newable; + /** + * Number of daemons to be created for standard synchronous operations such as + * checking if a module is installed, running a module, running a python file, etc. + * Defaults to `2`. + * + */ + daemonCount?: number; + /** + * Number of daemons to be created for operations such as execObservable, execModuleObservale. + * These operations are considered to be long running compared to checking if a module is installed. + * Hence a separate daemon will be created for this. + * Defaults to `1`. + * + */ + observableDaemonCount?: number; +}; +// This daemon will not belong to a daemon pool (i.e its a dedicated daemon and cannot be re-used). +export type DedicatedDaemonExecutionFactoryCreationOptions = ExecutionFactoryCreationOptions & { + /** + * Python file that implements the daemon. + */ + daemonModule?: string; + /** + * Typescript Daemon class (client) that maps to the Python daemon. + * Defaults to `PythonDaemonExecutionService`. + * Any other class provided must extend `PythonDaemonExecutionService`. + */ + daemonClass?: Newable; + /** + * This flag indicates it is a dedicated daemon. + */ + dedicated: true; +}; +export type DaemonExecutionFactoryCreationOptions = + | PooledDaemonExecutionFactoryCreationOptions + | DedicatedDaemonExecutionFactoryCreationOptions; +export type ExecutionFactoryCreateWithEnvironmentOptions = { + resource?: Uri; + interpreter?: PythonEnvironment; + allowEnvironmentFetchExceptions?: boolean; + /** + * Ignore running `conda run` when running code. + * It is known to fail in certain scenarios. Where necessary we might want to bypass this. + * + * @type {boolean} + */ + bypassCondaExecution?: boolean; +}; +export interface IPythonExecutionFactory { + create(options: ExecutionFactoryCreationOptions): Promise; + /** + * Creates a daemon Python Process. + * On windows it's cheaper to create a daemon and use that than spin up Python Processes everytime. + * If something cannot be executed within the daemon, it will resort to using the standard IPythonExecutionService. + * Note: The returned execution service is always using an activated environment. + * + * @param {ExecutionFactoryCreationOptions} options + * @returns {(Promise)} + * @memberof IPythonExecutionFactory + */ + createDaemon( + options: DaemonExecutionFactoryCreationOptions + ): Promise; + createActivatedEnvironment(options: ExecutionFactoryCreateWithEnvironmentOptions): Promise; + createCondaExecutionService( + pythonPath: string, + processService?: IProcessService, + resource?: Uri + ): Promise; +} +export const IPythonExecutionService = Symbol('IPythonExecutionService'); + +export interface IPythonExecutionService { + getInterpreterInformation(): Promise; + getExecutablePath(): Promise; + isModuleInstalled(moduleName: string): Promise; + getExecutionInfo(pythonArgs?: string[]): PythonExecInfo; + + execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult; + execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult; + + exec(args: string[], options: SpawnOptions): Promise>; + execModule(moduleName: string, args: string[], options: SpawnOptions): Promise>; +} + +/** + * Identical to the PythonExecutionService, but with a `dispose` method. + * This is a daemon process that lives on until it is disposed, hence the `IDisposable`. + * + * @export + * @interface IPythonDaemonExecutionService + * @extends {IPythonExecutionService} + * @extends {IDisposable} + */ +export interface IPythonDaemonExecutionService extends IPythonExecutionService, IDisposable {} + +export class StdErrError extends Error { + constructor(message: string) { + super(message); + } +} + +export interface IExecutionEnvironmentVariablesService { + getEnvironmentVariables(resource?: Uri): Promise; +} + +export const IPythonToolExecutionService = Symbol('IPythonToolRunnerService'); + +export interface IPythonToolExecutionService { + execObservable( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri + ): Promise>; + exec(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise>; +} diff --git a/src/client/common/refBool.ts b/src/client/common/refBool.ts new file mode 100644 index 000000000000..c0ef43a20777 --- /dev/null +++ b/src/client/common/refBool.ts @@ -0,0 +1,11 @@ +export class RefBool { + constructor(private val: boolean) {} + + public get value(): boolean { + return this.val; + } + + public update(newVal: boolean) { + this.val = newVal; + } +} diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts new file mode 100644 index 000000000000..444b666c70e3 --- /dev/null +++ b/src/client/common/serviceRegistry.ts @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { IExtensionSingleActivationService } from '../activation/types'; +import { IExperimentService, IFileDownloader, IHttpClient, IInterpreterPathService } from '../common/types'; +import { LiveShareApi } from '../datascience/liveshare/liveshare'; +import { INotebookExecutionLogger } from '../datascience/types'; +import { IServiceManager } from '../ioc/types'; +import { ImportTracker } from '../telemetry/importTracker'; +import { IImportTracker } from '../telemetry/types'; +import { ActiveResourceService } from './application/activeResource'; +import { ApplicationEnvironment } from './application/applicationEnvironment'; +import { ApplicationShell } from './application/applicationShell'; +import { ClipboardService } from './application/clipboard'; +import { CommandManager } from './application/commandManager'; +import { ReloadVSCodeCommandHandler } from './application/commands/reloadCommand'; +import { CustomEditorService } from './application/customEditorService'; +import { DebugService } from './application/debugService'; +import { DebugSessionTelemetry } from './application/debugSessionTelemetry'; +import { DocumentManager } from './application/documentManager'; +import { Extensions } from './application/extensions'; +import { LanguageService } from './application/languageService'; +import { VSCodeNotebook } from './application/notebook'; +import { TerminalManager } from './application/terminalManager'; +import { + IActiveResourceService, + IApplicationEnvironment, + IApplicationShell, + IClipboard, + ICommandManager, + ICustomEditorService, + IDebugService, + IDocumentManager, + ILanguageService, + ILiveShareApi, + ITerminalManager, + IVSCodeNotebook, + IWorkspaceService +} from './application/types'; +import { WorkspaceService } from './application/workspace'; +import { AsyncDisposableRegistry } from './asyncDisposableRegistry'; +import { ConfigurationService } from './configuration/service'; +import { CryptoUtils } from './crypto'; +import { EditorUtils } from './editor'; +import { ExperimentsManager } from './experiments/manager'; +import { ExperimentService } from './experiments/service'; +import { FeatureDeprecationManager } from './featureDeprecationManager'; +import { + ExtensionInsidersDailyChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionInsidersWeeklyChannelRule +} from './insidersBuild/downloadChannelRules'; +import { ExtensionChannelService } from './insidersBuild/downloadChannelService'; +import { InsidersExtensionPrompt } from './insidersBuild/insidersExtensionPrompt'; +import { InsidersExtensionService } from './insidersBuild/insidersExtensionService'; +import { + ExtensionChannel, + IExtensionChannelRule, + IExtensionChannelService, + IInsiderExtensionPrompt +} from './insidersBuild/types'; +import { ProductInstaller } from './installer/productInstaller'; +import { InterpreterPathService } from './interpreterPathService'; +import { BrowserService } from './net/browser'; +import { FileDownloader } from './net/fileDownloader'; +import { HttpClient } from './net/httpClient'; +import { NugetService } from './nuget/nugetService'; +import { INugetService } from './nuget/types'; +import { PersistentStateFactory } from './persistentState'; +import { IS_WINDOWS } from './platform/constants'; +import { PathUtils } from './platform/pathUtils'; +import { CurrentProcess } from './process/currentProcess'; +import { ProcessLogger } from './process/logger'; +import { IProcessLogger } from './process/types'; +import { TerminalActivator } from './terminal/activator'; +import { PowershellTerminalActivationFailedHandler } from './terminal/activator/powershellFailedHandler'; +import { Bash } from './terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from './terminal/environmentActivationProviders/commandPrompt'; +import { CondaActivationCommandProvider } from './terminal/environmentActivationProviders/condaActivationProvider'; +import { PipEnvActivationCommandProvider } from './terminal/environmentActivationProviders/pipEnvActivationProvider'; +import { PyEnvActivationCommandProvider } from './terminal/environmentActivationProviders/pyenvActivationProvider'; +import { TerminalServiceFactory } from './terminal/factory'; +import { TerminalHelper } from './terminal/helper'; +import { SettingsShellDetector } from './terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from './terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from './terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from './terminal/shellDetectors/vscEnvironmentShellDetector'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalActivationHandler, + ITerminalActivator, + ITerminalHelper, + ITerminalServiceFactory, + TerminalActivationProviders +} from './terminal/types'; +import { + IAsyncDisposableRegistry, + IBrowserService, + IConfigurationService, + ICryptoUtils, + ICurrentProcess, + IEditorUtils, + IExperimentsManager, + IExtensions, + IFeatureDeprecationManager, + IInstaller, + IPathUtils, + IPersistentStateFactory, + IRandom, + IsWindows +} from './types'; +import { IMultiStepInputFactory, MultiStepInputFactory } from './utils/multiStepInput'; +import { Random } from './utils/random'; + +// tslint:disable-next-line: max-func-body-length +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); + + serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); + serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); + serviceManager.addSingleton(IExtensions, Extensions); + serviceManager.addSingleton(IRandom, Random); + serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); + serviceManager.addSingleton(ITerminalServiceFactory, TerminalServiceFactory); + serviceManager.addSingleton(IPathUtils, PathUtils); + serviceManager.addSingleton(IApplicationShell, ApplicationShell); + serviceManager.addSingleton(IVSCodeNotebook, VSCodeNotebook); + serviceManager.addSingleton(IClipboard, ClipboardService); + serviceManager.addSingleton(ICurrentProcess, CurrentProcess); + serviceManager.addSingleton(IInstaller, ProductInstaller); + serviceManager.addSingleton(ICommandManager, CommandManager); + serviceManager.addSingleton(IConfigurationService, ConfigurationService); + serviceManager.addSingleton(IWorkspaceService, WorkspaceService); + serviceManager.addSingleton(IProcessLogger, ProcessLogger); + serviceManager.addSingleton(IDocumentManager, DocumentManager); + serviceManager.addSingleton(ITerminalManager, TerminalManager); + serviceManager.addSingleton(IDebugService, DebugService); + serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); + serviceManager.addSingleton(ILanguageService, LanguageService); + serviceManager.addSingleton(IBrowserService, BrowserService); + serviceManager.addSingleton(IHttpClient, HttpClient); + serviceManager.addSingleton(IFileDownloader, FileDownloader); + serviceManager.addSingleton(IEditorUtils, EditorUtils); + serviceManager.addSingleton(INugetService, NugetService); + serviceManager.addSingleton(ITerminalActivator, TerminalActivator); + serviceManager.addSingleton( + ITerminalActivationHandler, + PowershellTerminalActivationFailedHandler + ); + serviceManager.addSingleton(ILiveShareApi, LiveShareApi); + serviceManager.addSingleton(ICryptoUtils, CryptoUtils); + serviceManager.addSingleton(IExperimentsManager, ExperimentsManager); + serviceManager.addSingleton(IExperimentService, ExperimentService); + + serviceManager.addSingleton(ITerminalHelper, TerminalHelper); + serviceManager.addSingleton( + ITerminalActivationCommandProvider, + Bash, + TerminalActivationProviders.bashCShellFish + ); + serviceManager.addSingleton( + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell + ); + serviceManager.addSingleton( + ITerminalActivationCommandProvider, + PyEnvActivationCommandProvider, + TerminalActivationProviders.pyenv + ); + serviceManager.addSingleton( + ITerminalActivationCommandProvider, + CondaActivationCommandProvider, + TerminalActivationProviders.conda + ); + serviceManager.addSingleton( + ITerminalActivationCommandProvider, + PipEnvActivationCommandProvider, + TerminalActivationProviders.pipenv + ); + serviceManager.addSingleton(IFeatureDeprecationManager, FeatureDeprecationManager); + + serviceManager.addSingleton(IAsyncDisposableRegistry, AsyncDisposableRegistry); + serviceManager.addSingleton(IMultiStepInputFactory, MultiStepInputFactory); + serviceManager.addSingleton(IImportTracker, ImportTracker); + serviceManager.addBinding(IImportTracker, IExtensionSingleActivationService); + serviceManager.addBinding(IImportTracker, INotebookExecutionLogger); + serviceManager.addSingleton(IShellDetector, TerminalNameShellDetector); + serviceManager.addSingleton(IShellDetector, SettingsShellDetector); + serviceManager.addSingleton(IShellDetector, UserEnvironmentShellDetector); + serviceManager.addSingleton(IShellDetector, VSCEnvironmentShellDetector); + serviceManager.addSingleton(IInsiderExtensionPrompt, InsidersExtensionPrompt); + serviceManager.addSingleton( + IExtensionSingleActivationService, + InsidersExtensionService + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + ReloadVSCodeCommandHandler + ); + serviceManager.addSingleton(IExtensionChannelService, ExtensionChannelService); + serviceManager.addSingleton( + IExtensionChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionChannel.off + ); + serviceManager.addSingleton( + IExtensionChannelRule, + ExtensionInsidersDailyChannelRule, + ExtensionChannel.daily + ); + serviceManager.addSingleton( + IExtensionChannelRule, + ExtensionInsidersWeeklyChannelRule, + ExtensionChannel.weekly + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugSessionTelemetry + ); + serviceManager.addSingleton(ICustomEditorService, CustomEditorService); +} diff --git a/src/client/common/startPage/startPage.ts b/src/client/common/startPage/startPage.ts new file mode 100644 index 000000000000..83a1fb6defab --- /dev/null +++ b/src/client/common/startPage/startPage.ts @@ -0,0 +1,307 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { ConfigurationTarget, EventEmitter, Uri, ViewColumn } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { Commands, Telemetry } from '../../datascience/constants'; +import { ICodeCssGenerator, INotebookEditorProvider, IThemeFinder } from '../../datascience/types'; +import { WebviewPanelHost } from '../../datascience/webviews/webviewPanelHost'; +import { sendTelemetryEvent } from '../../telemetry'; +import { + IApplicationEnvironment, + IApplicationShell, + ICommandManager, + IDocumentManager, + IWebviewPanelProvider, + IWorkspaceService +} from '../application/types'; +import { IFileSystem } from '../platform/types'; +import { IConfigurationService, IExtensionContext, Resource } from '../types'; +import * as localize from '../utils/localize'; +import { StopWatch } from '../utils/stopWatch'; +import { StartPageMessageListener } from './startPageMessageListener'; +import { IStartPage, IStartPageMapping, StartPageMessages } from './types'; + +const startPageDir = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'viewers'); + +// Class that opens, disposes and handles messages and actions for the Python Extension Start Page. +// It also runs when the extension activates. +@injectable() +export class StartPage extends WebviewPanelHost + implements IStartPage, IExtensionSingleActivationService { + protected closedEvent: EventEmitter = new EventEmitter(); + private timer: StopWatch; + private actionTaken = false; + private actionTakenOnFirstTime = false; + private firstTime = false; + private webviewDidLoad = false; + constructor( + @inject(IWebviewPanelProvider) provider: IWebviewPanelProvider, + @inject(ICodeCssGenerator) cssGenerator: ICodeCssGenerator, + @inject(IThemeFinder) themeFinder: IThemeFinder, + @inject(IConfigurationService) protected configuration: IConfigurationService, + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IFileSystem) private file: IFileSystem, + @inject(INotebookEditorProvider) private notebookEditorProvider: INotebookEditorProvider, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDocumentManager) private readonly documentManager: IDocumentManager, + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IExtensionContext) private readonly context: IExtensionContext, + @inject(IApplicationEnvironment) private appEnvironment: IApplicationEnvironment + ) { + super( + configuration, + provider, + cssGenerator, + themeFinder, + workspaceService, + (c, v, d) => new StartPageMessageListener(c, v, d), + startPageDir, + [path.join(startPageDir, 'commons.initial.bundle.js'), path.join(startPageDir, 'startPage.js')], + localize.StartPage.getStarted(), + ViewColumn.One, + false, + false, + Promise.resolve(false) + ); + this.timer = new StopWatch(); + } + + public async activate(): Promise { + this.activateBackground().ignoreErrors(); + } + + public dispose(): Promise { + if (!this.isDisposed) { + super.dispose(); + } + return this.close(); + } + + public async open(): Promise { + sendTelemetryEvent(Telemetry.StartPageViewed); + setTimeout(async () => { + await this.loadWebPanel(process.cwd()); + // open webview + await super.show(true); + + setTimeout(() => { + if (!this.webviewDidLoad) { + sendTelemetryEvent(Telemetry.StartPageWebViewError); + } + }, 5000); + }, 3000); + } + + public get owningResource(): Resource { + return undefined; + } + + public async close(): Promise { + if (!this.actionTaken) { + sendTelemetryEvent(Telemetry.StartPageClosedWithoutAction); + } + if (this.actionTakenOnFirstTime) { + sendTelemetryEvent(Telemetry.StartPageUsedAnActionOnFirstTime); + } + sendTelemetryEvent(Telemetry.StartPageTime, this.timer.elapsedTime); + // Fire our event + this.closedEvent.fire(this); + } + + // tslint:disable-next-line: no-any + public async onMessage(message: string, payload: any) { + switch (message) { + case StartPageMessages.Started: + this.webviewDidLoad = true; + break; + case StartPageMessages.RequestShowAgainSetting: + const settings = this.configuration.getSettings(); + await this.postMessage(StartPageMessages.SendSetting, { + showAgainSetting: settings.showStartPage + }); + break; + case StartPageMessages.OpenBlankNotebook: + sendTelemetryEvent(Telemetry.StartPageOpenBlankNotebook); + this.setTelemetryFlags(); + + const savedVersion: string | undefined = this.context.globalState.get('extensionVersion'); + + if (savedVersion) { + await this.notebookEditorProvider.createNew(); + } else { + this.openSampleNotebook().ignoreErrors(); + } + break; + case StartPageMessages.OpenBlankPythonFile: + sendTelemetryEvent(Telemetry.StartPageOpenBlankPythonFile); + this.setTelemetryFlags(); + + const doc = await this.documentManager.openTextDocument({ + language: 'python', + content: `print("${localize.StartPage.helloWorld()}")` + }); + await this.documentManager.showTextDocument(doc, 1, true); + break; + case StartPageMessages.OpenInteractiveWindow: + sendTelemetryEvent(Telemetry.StartPageOpenInteractiveWindow); + this.setTelemetryFlags(); + + const doc2 = await this.documentManager.openTextDocument({ + language: 'python', + content: `#%%\nprint("${localize.StartPage.helloWorld()}")` + }); + await this.documentManager.showTextDocument(doc2, 1, true); + await this.commandManager.executeCommand(Commands.RunAllCells, Uri.parse('')); + break; + case StartPageMessages.OpenCommandPalette: + sendTelemetryEvent(Telemetry.StartPageOpenCommandPalette); + this.setTelemetryFlags(); + + await this.commandManager.executeCommand('workbench.action.showCommands'); + break; + case StartPageMessages.OpenCommandPaletteWithOpenNBSelected: + sendTelemetryEvent(Telemetry.StartPageOpenCommandPaletteWithOpenNBSelected); + this.setTelemetryFlags(); + + await this.commandManager.executeCommand( + 'workbench.action.quickOpen', + '>Create New Blank Jupyter Notebook' + ); + break; + case StartPageMessages.OpenSampleNotebook: + sendTelemetryEvent(Telemetry.StartPageOpenSampleNotebook); + this.setTelemetryFlags(); + + this.openSampleNotebook().ignoreErrors(); + break; + case StartPageMessages.OpenFileBrowser: + sendTelemetryEvent(Telemetry.StartPageOpenFileBrowser); + this.setTelemetryFlags(); + + const uri = await this.appShell.showOpenDialog({ + filters: { + Python: ['py', 'ipynb'] + }, + canSelectMany: false + }); + if (uri) { + const doc3 = await this.documentManager.openTextDocument(uri[0]); + await this.documentManager.showTextDocument(doc3); + } + break; + case StartPageMessages.OpenFolder: + sendTelemetryEvent(Telemetry.StartPageOpenFolder); + this.setTelemetryFlags(); + this.commandManager.executeCommand('workbench.action.files.openFolder'); + break; + case StartPageMessages.OpenWorkspace: + sendTelemetryEvent(Telemetry.StartPageOpenWorkspace); + this.setTelemetryFlags(); + this.commandManager.executeCommand('workbench.action.openWorkspace'); + break; + case StartPageMessages.UpdateSettings: + if (payload === false) { + sendTelemetryEvent(Telemetry.StartPageClickedDontShowAgain); + } + await this.configuration.updateSetting('showStartPage', payload, undefined, ConfigurationTarget.Global); + break; + default: + break; + } + + super.onMessage(message, payload); + } + + // Public for testing + public async extensionVersionChanged(): Promise { + const savedVersion: string | undefined = this.context.globalState.get('extensionVersion'); + const version: string = this.appEnvironment.packageJson.version; + let shouldShowStartPage: boolean; + + if (savedVersion) { + if (savedVersion === version || this.savedVersionisOlder(savedVersion, version)) { + // There has not been an update + shouldShowStartPage = false; + } else { + sendTelemetryEvent(Telemetry.StartPageOpenedFromNewUpdate); + shouldShowStartPage = true; + } + } else { + sendTelemetryEvent(Telemetry.StartPageOpenedFromNewInstall); + shouldShowStartPage = true; + } + + // savedVersion being undefined means this is the first time the user activates the extension. + // if savedVersion != version, there was an update + await this.context.globalState.update('extensionVersion', version); + return shouldShowStartPage; + } + + private async activateBackground(): Promise { + const settings = this.configuration.getSettings(); + + if (settings.showStartPage && this.appEnvironment.extensionChannel === 'stable') { + // extesionVersionChanged() reads CHANGELOG.md + // So we use separate if's to try and avoid reading a file every time + const firstTimeOrUpdate = await this.extensionVersionChanged(); + + if (firstTimeOrUpdate) { + this.firstTime = true; + this.open().ignoreErrors(); + } + } + } + + private savedVersionisOlder(savedVersion: string, actualVersion: string): boolean { + const saved = savedVersion.split('.'); + const actual = actualVersion.split('.'); + + switch (true) { + case Number(actual[0]) > Number(saved[0]): + return false; + case Number(actual[0]) < Number(saved[0]): + return true; + case Number(actual[1]) > Number(saved[1]): + return false; + case Number(actual[1]) < Number(saved[1]): + return true; + case Number(actual[2][0]) > Number(saved[2][0]): + return false; + case Number(actual[2][0]) < Number(saved[2][0]): + return true; + default: + return false; + } + } + + private async openSampleNotebook(): Promise { + const ipynb = '.ipynb'; + const localizedFilePath = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + localize.StartPage.sampleNotebook() + ipynb + ); + let sampleNotebookPath: string; + + if (await this.file.fileExists(localizedFilePath)) { + sampleNotebookPath = localizedFilePath; + } else { + sampleNotebookPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'Notebooks intro.ipynb'); + } + + const content = await this.file.readFile(sampleNotebookPath); + await this.notebookEditorProvider.createNew(content, localize.StartPage.sampleNotebook()); + } + + private setTelemetryFlags() { + if (this.firstTime) { + this.actionTakenOnFirstTime = true; + } + this.actionTaken = true; + } +} diff --git a/src/client/common/startPage/startPageMessageListener.ts b/src/client/common/startPage/startPageMessageListener.ts new file mode 100644 index 000000000000..642bfb609674 --- /dev/null +++ b/src/client/common/startPage/startPageMessageListener.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { IWebviewPanel, IWebviewPanelMessageListener } from '../application/types'; +import '../extensions'; + +// tslint:disable:no-any +// This class listens to messages that come from the local Python Interactive window +export class StartPageMessageListener implements IWebviewPanelMessageListener { + private disposedCallback: () => void; + private callback: (message: string, payload: any) => void; + private viewChanged: (panel: IWebviewPanel) => void; + + constructor( + callback: (message: string, payload: any) => void, + viewChanged: (panel: IWebviewPanel) => void, + disposed: () => void + ) { + // Save our dispose callback so we remove our interactive window + this.disposedCallback = disposed; + + // Save our local callback so we can handle the non broadcast case(s) + this.callback = callback; + + // Save view changed so we can forward view change events. + this.viewChanged = viewChanged; + } + + public async dispose() { + this.disposedCallback(); + } + + public onMessage(message: string, payload: any) { + // Send to just our local callback. + this.callback(message, payload); + } + + public onChangeViewState(panel: IWebviewPanel) { + // Forward this onto our callback + this.viewChanged(panel); + } +} diff --git a/src/client/common/startPage/types.ts b/src/client/common/startPage/types.ts new file mode 100644 index 000000000000..a147085fe426 --- /dev/null +++ b/src/client/common/startPage/types.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { SharedMessages } from '../../datascience/messages'; + +export const IStartPage = Symbol('IStartPage'); +export interface IStartPage { + open(): Promise; + extensionVersionChanged(): Promise; +} + +export interface ISettingPackage { + showAgainSetting: boolean; +} + +export namespace StartPageMessages { + export const Started = SharedMessages.Started; + export const UpdateSettings = SharedMessages.UpdateSettings; + export const RequestShowAgainSetting = 'RequestShowAgainSetting'; + export const SendSetting = 'SendSetting'; + export const OpenBlankNotebook = 'OpenBlankNotebook'; + export const OpenBlankPythonFile = 'OpenBlankPythonFile'; + export const OpenInteractiveWindow = 'OpenInteractiveWindow'; + export const OpenCommandPalette = 'OpenCommandPalette'; + export const OpenCommandPaletteWithOpenNBSelected = 'OpenCommandPaletteWithOpenNBSelected'; + export const OpenSampleNotebook = 'OpenSampleNotebook'; + export const OpenFileBrowser = 'OpenFileBrowser'; + export const OpenFolder = 'OpenFolder'; + export const OpenWorkspace = 'OpenWorkspace'; +} + +export class IStartPageMapping { + public [StartPageMessages.RequestShowAgainSetting]: ISettingPackage; + public [StartPageMessages.SendSetting]: ISettingPackage; + public [StartPageMessages.Started]: never | undefined; + public [StartPageMessages.UpdateSettings]: boolean; + public [StartPageMessages.OpenBlankNotebook]: never | undefined; + public [StartPageMessages.OpenBlankPythonFile]: never | undefined; + public [StartPageMessages.OpenInteractiveWindow]: never | undefined; + public [StartPageMessages.OpenCommandPalette]: never | undefined; + public [StartPageMessages.OpenCommandPaletteWithOpenNBSelected]: never | undefined; + public [StartPageMessages.OpenSampleNotebook]: never | undefined; + public [StartPageMessages.OpenFileBrowser]: never | undefined; + public [StartPageMessages.OpenFolder]: never | undefined; + public [StartPageMessages.OpenWorkspace]: never | undefined; +} diff --git a/src/client/common/terminal/activator/base.ts b/src/client/common/terminal/activator/base.ts new file mode 100644 index 000000000000..27ad3d6ad5d5 --- /dev/null +++ b/src/client/common/terminal/activator/base.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Terminal } from 'vscode'; +import { createDeferred, sleep } from '../../utils/async'; +import { ITerminalActivator, ITerminalHelper, TerminalActivationOptions, TerminalShellType } from '../types'; + +export class BaseTerminalActivator implements ITerminalActivator { + private readonly activatedTerminals: Map> = new Map>(); + constructor(private readonly helper: ITerminalHelper) {} + public async activateEnvironmentInTerminal( + terminal: Terminal, + options?: TerminalActivationOptions + ): Promise { + if (this.activatedTerminals.has(terminal)) { + return this.activatedTerminals.get(terminal)!; + } + const deferred = createDeferred(); + this.activatedTerminals.set(terminal, deferred.promise); + const terminalShellType = this.helper.identifyTerminalShell(terminal); + + const activationCommands = await this.helper.getEnvironmentActivationCommands( + terminalShellType, + options?.resource, + options?.interpreter + ); + let activated = false; + if (activationCommands) { + for (const command of activationCommands!) { + terminal.show(options?.preserveFocus); + terminal.sendText(command); + await this.waitForCommandToProcess(terminalShellType); + activated = true; + } + } + deferred.resolve(activated); + return activated; + } + protected async waitForCommandToProcess(_shell: TerminalShellType) { + // Give the command some time to complete. + // Its been observed that sending commands too early will strip some text off in VS Code Terminal. + await sleep(500); + } +} diff --git a/src/client/common/terminal/activator/index.ts b/src/client/common/terminal/activator/index.ts new file mode 100644 index 000000000000..b7b8beb147a6 --- /dev/null +++ b/src/client/common/terminal/activator/index.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, multiInject } from 'inversify'; +import { Terminal } from 'vscode'; +import { IConfigurationService } from '../../types'; +import { ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, TerminalActivationOptions } from '../types'; +import { BaseTerminalActivator } from './base'; + +@injectable() +export class TerminalActivator implements ITerminalActivator { + protected baseActivator!: ITerminalActivator; + constructor( + @inject(ITerminalHelper) readonly helper: ITerminalHelper, + @multiInject(ITerminalActivationHandler) private readonly handlers: ITerminalActivationHandler[], + @inject(IConfigurationService) private readonly configurationService: IConfigurationService + ) { + this.initialize(); + } + public async activateEnvironmentInTerminal( + terminal: Terminal, + options?: TerminalActivationOptions + ): Promise { + const settings = this.configurationService.getSettings(options?.resource); + const activateEnvironment = settings.terminal.activateEnvironment; + if (!activateEnvironment || options?.hideFromUser) { + return false; + } + + const activated = await this.baseActivator.activateEnvironmentInTerminal(terminal, options); + this.handlers.forEach((handler) => + handler + .handleActivation(terminal, options?.resource, options?.preserveFocus === true, activated) + .ignoreErrors() + ); + return activated; + } + protected initialize() { + this.baseActivator = new BaseTerminalActivator(this.helper); + } +} diff --git a/src/client/common/terminal/activator/powershellFailedHandler.ts b/src/client/common/terminal/activator/powershellFailedHandler.ts new file mode 100644 index 000000000000..cdb758ebea54 --- /dev/null +++ b/src/client/common/terminal/activator/powershellFailedHandler.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Terminal } from 'vscode'; +import { + PowerShellActivationHackDiagnosticsServiceId, + PowershellActivationNotAvailableDiagnostic +} from '../../../application/diagnostics/checks/powerShellActivation'; +import { IDiagnosticsService } from '../../../application/diagnostics/types'; +import { IPlatformService } from '../../platform/types'; +import { Resource } from '../../types'; +import { ITerminalActivationHandler, ITerminalHelper, TerminalShellType } from '../types'; + +@injectable() +export class PowershellTerminalActivationFailedHandler implements ITerminalActivationHandler { + constructor( + @inject(ITerminalHelper) private readonly helper: ITerminalHelper, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IDiagnosticsService) + @named(PowerShellActivationHackDiagnosticsServiceId) + private readonly diagnosticService: IDiagnosticsService + ) {} + public async handleActivation(terminal: Terminal, resource: Resource, _preserveFocus: boolean, activated: boolean) { + if (activated || !this.platformService.isWindows) { + return; + } + const shell = this.helper.identifyTerminalShell(terminal); + if (shell !== TerminalShellType.powershell && shell !== TerminalShellType.powershellCore) { + return; + } + // Check if we can activate in Command Prompt. + const activationCommands = await this.helper.getEnvironmentActivationCommands( + TerminalShellType.commandPrompt, + resource + ); + if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { + return; + } + this.diagnosticService.handle([new PowershellActivationNotAvailableDiagnostic(resource)]).ignoreErrors(); + } +} diff --git a/src/client/common/terminal/commandPrompt.ts b/src/client/common/terminal/commandPrompt.ts new file mode 100644 index 000000000000..3d83e854e116 --- /dev/null +++ b/src/client/common/terminal/commandPrompt.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { ConfigurationTarget } from 'vscode'; +import { IConfigurationService, ICurrentProcess } from '../types'; + +export function getCommandPromptLocation(currentProcess: ICurrentProcess) { + // https://github.com/Microsoft/vscode/blob/master/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts#L218 + // Determine the correct System32 path. We want to point to Sysnative + // when the 32-bit version of VS Code is running on a 64-bit machine. + // The reason for this is because PowerShell's important PSReadline + // module doesn't work if this is not the case. See #27915. + const is32ProcessOn64Windows = currentProcess.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + const system32Path = path.join(currentProcess.env.windir!, is32ProcessOn64Windows ? 'Sysnative' : 'System32'); + return path.join(system32Path, 'cmd.exe'); +} +export async function useCommandPromptAsDefaultShell( + currentProcess: ICurrentProcess, + configService: IConfigurationService +) { + const cmdPromptLocation = getCommandPromptLocation(currentProcess); + await configService.updateSectionSetting( + 'terminal', + 'integrated.shell.windows', + cmdPromptLocation, + undefined, + ConfigurationTarget.Global + ); +} diff --git a/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts new file mode 100644 index 000000000000..215e114f00eb --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IServiceContainer } from '../../../ioc/types'; +import { getVenvExecutableFinder } from '../../../pythonEnvironments/discovery/subenv'; +import { IFileSystem } from '../../platform/types'; +import { IConfigurationService } from '../../types'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; + +@injectable() +export abstract class BaseActivationCommandProvider implements ITerminalActivationCommandProvider { + constructor(@inject(IServiceContainer) protected readonly serviceContainer: IServiceContainer) {} + + public abstract isShellSupported(targetShell: TerminalShellType): boolean; + public getActivationCommands( + resource: Uri | undefined, + targetShell: TerminalShellType + ): Promise { + const pythonPath = this.serviceContainer.get(IConfigurationService).getSettings(resource) + .pythonPath; + return this.getActivationCommandsForInterpreter(pythonPath, targetShell); + } + public abstract getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType + ): Promise; +} + +export type ActivationScripts = Record; + +export abstract class VenvBaseActivationCommandProvider extends BaseActivationCommandProvider { + public isShellSupported(targetShell: TerminalShellType): boolean { + return this.scripts[targetShell] !== undefined; + } + + protected abstract get scripts(): ActivationScripts; + + protected async findScriptFile(pythonPath: string, targetShell: TerminalShellType): Promise { + const fs = this.serviceContainer.get(IFileSystem); + const candidates = this.scripts[targetShell]; + if (!candidates) { + return undefined; + } + const findScript = getVenvExecutableFinder( + candidates, + path.dirname, + path.join, + // Bind "this"! + (n: string) => fs.fileExists(n) + ); + return findScript(pythonPath); + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/bash.ts b/src/client/common/terminal/environmentActivationProviders/bash.ts new file mode 100644 index 000000000000..b0379ae97c7e --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/bash.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import '../../extensions'; +import { TerminalShellType } from '../types'; +import { ActivationScripts, VenvBaseActivationCommandProvider } from './baseActivationProvider'; + +// For a given shell the scripts are in order of precedence. +const SCRIPTS: ActivationScripts = ({ + // Group 1 + [TerminalShellType.wsl]: ['activate.sh', 'activate'], + [TerminalShellType.ksh]: ['activate.sh', 'activate'], + [TerminalShellType.zsh]: ['activate.sh', 'activate'], + [TerminalShellType.gitbash]: ['activate.sh', 'activate'], + [TerminalShellType.bash]: ['activate.sh', 'activate'], + // Group 2 + [TerminalShellType.tcshell]: ['activate.csh'], + [TerminalShellType.cshell]: ['activate.csh'], + // Group 3 + [TerminalShellType.fish]: ['activate.fish'] +} as unknown) as ActivationScripts; + +export function getAllScripts(): string[] { + const scripts: string[] = []; + for (const key of Object.keys(SCRIPTS)) { + const shell = key as TerminalShellType; + for (const name of SCRIPTS[shell]) { + if (!scripts.includes(name)) { + scripts.push(name); + } + } + } + return scripts; +} + +@injectable() +export class Bash extends VenvBaseActivationCommandProvider { + protected readonly scripts = SCRIPTS; + + public async getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType + ): Promise { + const scriptFile = await this.findScriptFile(pythonPath, targetShell); + if (!scriptFile) { + return; + } + return [`source ${scriptFile.fileToCommandArgument()}`]; + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts new file mode 100644 index 000000000000..dc8f51ba8aa6 --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { IServiceContainer } from '../../../ioc/types'; +import '../../extensions'; +import { TerminalShellType } from '../types'; +import { ActivationScripts, VenvBaseActivationCommandProvider } from './baseActivationProvider'; + +// For a given shell the scripts are in order of precedence. +const SCRIPTS: ActivationScripts = ({ + // Group 1 + [TerminalShellType.commandPrompt]: ['activate.bat', 'Activate.ps1'], + // Group 2 + [TerminalShellType.powershell]: ['Activate.ps1', 'activate.bat'], + [TerminalShellType.powershellCore]: ['Activate.ps1', 'activate.bat'] +} as unknown) as ActivationScripts; + +export function getAllScripts(pathJoin: (...p: string[]) => string): string[] { + const scripts: string[] = []; + for (const key of Object.keys(SCRIPTS)) { + const shell = key as TerminalShellType; + for (const name of SCRIPTS[shell]) { + if (!scripts.includes(name)) { + scripts.push( + name, + // We also add scripts in subdirs. + pathJoin('Scripts', name), + pathJoin('scripts', name) + ); + } + } + } + return scripts; +} + +@injectable() +export class CommandPromptAndPowerShell extends VenvBaseActivationCommandProvider { + protected readonly scripts: ActivationScripts; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + this.scripts = ({} as unknown) as ActivationScripts; + for (const key of Object.keys(SCRIPTS)) { + const shell = key as TerminalShellType; + const scripts: string[] = []; + for (const name of SCRIPTS[shell]) { + scripts.push( + name, + // We also add scripts in subdirs. + path.join('Scripts', name), + path.join('scripts', name) + ); + } + this.scripts[shell] = scripts; + } + } + + public async getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType + ): Promise { + const scriptFile = await this.findScriptFile(pythonPath, targetShell); + if (!scriptFile) { + return; + } + + if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('activate.bat')) { + return [scriptFile.fileToCommandArgument()]; + } else if ( + (targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore) && + scriptFile.endsWith('Activate.ps1') + ) { + return [`& ${scriptFile.fileToCommandArgument()}`]; + } else if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('Activate.ps1')) { + // lets not try to run the powershell file from command prompt (user may not have powershell) + return []; + } else { + return; + } + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts new file mode 100644 index 000000000000..3f4b5cfaa809 --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../extensions'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; + +import { ICondaService } from '../../../interpreter/contracts'; +import { IPlatformService } from '../../platform/types'; +import { IConfigurationService } from '../../types'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; + +// Version number of conda that requires we call activate with 'conda activate' instead of just 'activate' +const CondaRequiredMajor = 4; +const CondaRequiredMinor = 4; +const CondaRequiredMinorForPowerShell = 6; + +/** + * Support conda env activation (in the terminal). + */ +@injectable() +export class CondaActivationCommandProvider implements ITerminalActivationCommandProvider { + constructor( + @inject(ICondaService) private readonly condaService: ICondaService, + @inject(IPlatformService) private platform: IPlatformService, + @inject(IConfigurationService) private configService: IConfigurationService + ) {} + + /** + * Is the given shell supported for activating a conda env? + */ + public isShellSupported(_targetShell: TerminalShellType): boolean { + return true; + } + + /** + * Return the command needed to activate the conda env. + */ + public getActivationCommands( + resource: Uri | undefined, + targetShell: TerminalShellType + ): Promise { + const pythonPath = this.configService.getSettings(resource).pythonPath; + return this.getActivationCommandsForInterpreter(pythonPath, targetShell); + } + + /** + * Return the command needed to activate the conda env. + * + */ + public async getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType + ): Promise { + const envInfo = await this.condaService.getCondaEnvironment(pythonPath); + if (!envInfo) { + return; + } + + const condaEnv = envInfo.name.length > 0 ? envInfo.name : envInfo.path; + + // Algorithm differs based on version + // Old version, just call activate directly. + // New version, call activate from the same path as our python path, then call it again to activate our environment. + // -- note that the 'default' conda location won't allow activate to work for the environment sometimes. + const versionInfo = await this.condaService.getCondaVersion(); + if (versionInfo && versionInfo.major >= CondaRequiredMajor) { + // Conda added support for powershell in 4.6. + if ( + versionInfo.minor >= CondaRequiredMinorForPowerShell && + (targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore) + ) { + return this.getPowershellCommands(condaEnv); + } + if (versionInfo.minor >= CondaRequiredMinor) { + // New version. + const interpreterPath = await this.condaService.getCondaFileFromInterpreter(pythonPath, envInfo.name); + if (interpreterPath) { + const activatePath = path.join(path.dirname(interpreterPath), 'activate').fileToCommandArgument(); + const firstActivate = this.platform.isWindows ? activatePath : `source ${activatePath}`; + return [firstActivate, `conda activate ${condaEnv.toCommandArgument()}`]; + } + } + } + + switch (targetShell) { + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return this.getPowershellCommands(condaEnv); + + // tslint:disable-next-line:no-suspicious-comment + // TODO: Do we really special-case fish on Windows? + case TerminalShellType.fish: + return this.getFishCommands(condaEnv, await this.condaService.getCondaFile()); + + default: + if (this.platform.isWindows) { + return this.getWindowsCommands(condaEnv); + } else { + return this.getUnixCommands(condaEnv, await this.condaService.getCondaFile()); + } + } + } + + public async getWindowsActivateCommand(): Promise { + let activateCmd: string = 'activate'; + + const condaExePath = await this.condaService.getCondaFile(); + + if (condaExePath && path.basename(condaExePath) !== condaExePath) { + const condaScriptsPath: string = path.dirname(condaExePath); + // prefix the cmd with the found path, and ensure it's quoted properly + activateCmd = path.join(condaScriptsPath, activateCmd); + activateCmd = activateCmd.toCommandArgument(); + } + + return activateCmd; + } + + public async getWindowsCommands(condaEnv: string): Promise { + const activate = await this.getWindowsActivateCommand(); + return [`${activate} ${condaEnv.toCommandArgument()}`]; + } + /** + * The expectation is for the user to configure Powershell for Conda. + * Hence we just send the command `conda activate ...`. + * This configuration is documented on Conda. + * Extension will not attempt to work around issues by trying to setup shell for user. + * + * @param {string} condaEnv + * @returns {(Promise)} + * @memberof CondaActivationCommandProvider + */ + public async getPowershellCommands(condaEnv: string): Promise { + return [`conda activate ${condaEnv.toCommandArgument()}`]; + } + + public async getFishCommands(condaEnv: string, condaFile: string): Promise { + // https://github.com/conda/conda/blob/be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe/shell/etc/fish/conf.d/conda.fish#L18-L28 + return [`${condaFile.fileToCommandArgument()} activate ${condaEnv.toCommandArgument()}`]; + } + + public async getUnixCommands(condaEnv: string, condaFile: string): Promise { + const condaDir = path.dirname(condaFile); + const activateFile = path.join(condaDir, 'activate'); + return [`source ${activateFile.fileToCommandArgument()} ${condaEnv.toCommandArgument()}`]; + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts new file mode 100644 index 000000000000..3e7a90ce7603 --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Uri } from 'vscode'; +import '../../../common/extensions'; +import { + IInterpreterLocatorService, + IInterpreterService, + IPipEnvService, + PIPENV_SERVICE +} from '../../../interpreter/contracts'; +import { EnvironmentType } from '../../../pythonEnvironments/info'; +import { IWorkspaceService } from '../../application/types'; +import { IFileSystem } from '../../platform/types'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; + +@injectable() +export class PipEnvActivationCommandProvider implements ITerminalActivationCommandProvider { + constructor( + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IInterpreterLocatorService) + @named(PIPENV_SERVICE) + private readonly pipenvService: IPipEnvService, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IFileSystem) private readonly fs: IFileSystem + ) {} + + public isShellSupported(_targetShell: TerminalShellType): boolean { + return false; + } + + public async getActivationCommands(resource: Uri | undefined, _: TerminalShellType): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter || interpreter.envType !== EnvironmentType.Pipenv) { + return; + } + // Activate using `pipenv shell` only if the current folder relates pipenv environment. + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + if ( + workspaceFolder && + interpreter.pipEnvWorkspaceFolder && + !this.fs.arePathsSame(workspaceFolder.uri.fsPath, interpreter.pipEnvWorkspaceFolder) + ) { + return; + } + const execName = this.pipenvService.executable; + return [`${execName.fileToCommandArgument()} shell`]; + } + + public async getActivationCommandsForInterpreter( + pythonPath: string, + _targetShell: TerminalShellType + ): Promise { + const interpreter = await this.interpreterService.getInterpreterDetails(pythonPath); + if (!interpreter || interpreter.envType !== EnvironmentType.Pipenv) { + return; + } + + const execName = this.pipenvService.executable; + return [`${execName.fileToCommandArgument()} shell`]; + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts new file mode 100644 index 000000000000..d458a10938b6 --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import { EnvironmentType } from '../../../pythonEnvironments/info'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; + +@injectable() +export class PyEnvActivationCommandProvider implements ITerminalActivationCommandProvider { + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} + + public isShellSupported(_targetShell: TerminalShellType): boolean { + return true; + } + + public async getActivationCommands(resource: Uri | undefined, _: TerminalShellType): Promise { + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getActiveInterpreter(resource); + if (!interpreter || interpreter.envType !== EnvironmentType.Pyenv || !interpreter.envName) { + return; + } + + return [`pyenv shell ${interpreter.envName.toCommandArgument()}`]; + } + + public async getActivationCommandsForInterpreter( + pythonPath: string, + _targetShell: TerminalShellType + ): Promise { + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getInterpreterDetails(pythonPath); + if (!interpreter || interpreter.envType !== EnvironmentType.Pyenv || !interpreter.envName) { + return; + } + + return [`pyenv shell ${interpreter.envName.toCommandArgument()}`]; + } +} diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts new file mode 100644 index 000000000000..2a9e74f624ab --- /dev/null +++ b/src/client/common/terminal/factory.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { IWorkspaceService } from '../application/types'; +import { IFileSystem } from '../platform/types'; +import { isUri } from '../utils/misc'; +import { TerminalService } from './service'; +import { SynchronousTerminalService } from './syncTerminalService'; +import { ITerminalService, ITerminalServiceFactory, TerminalCreationOptions } from './types'; + +@injectable() +export class TerminalServiceFactory implements ITerminalServiceFactory { + private terminalServices: Map; + + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IFileSystem) private fs: IFileSystem, + @inject(IInterpreterService) private interpreterService: IInterpreterService + ) { + this.terminalServices = new Map(); + } + public getTerminalService(options?: TerminalCreationOptions): ITerminalService; + public getTerminalService(resource?: Uri, title?: string): ITerminalService; + public getTerminalService(arg1?: Uri | TerminalCreationOptions, arg2?: string): ITerminalService { + const resource = isUri(arg1) ? arg1 : undefined; + const title = isUri(arg1) ? undefined : arg1?.title || arg2; + const terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; + const interpreter = isUri(arg1) ? undefined : arg1?.interpreter; + const hideFromUser = isUri(arg1) ? false : arg1?.hideFromUser === true; + const env = isUri(arg1) ? undefined : arg1?.env; + + const options: TerminalCreationOptions = { + env, + hideFromUser, + interpreter, + resource, + title + }; + const id = this.getTerminalId(terminalTitle, resource, interpreter); + if (!this.terminalServices.has(id)) { + const terminalService = new TerminalService(this.serviceContainer, options); + this.terminalServices.set(id, terminalService); + } + + // Decorate terminal service with the synchronous service. + return new SynchronousTerminalService( + this.fs, + this.interpreterService, + this.terminalServices.get(id)!, + interpreter + ); + } + public createTerminalService(resource?: Uri, title?: string): ITerminalService { + title = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; + return new TerminalService(this.serviceContainer, { resource, title }); + } + private getTerminalId(title: string, resource?: Uri, interpreter?: PythonEnvironment): string { + if (!resource && !interpreter) { + return title; + } + const workspaceFolder = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolder(resource || undefined); + return `${title}:${workspaceFolder?.uri.fsPath || ''}:${interpreter?.path}`; + } +} diff --git a/src/client/common/terminal/helper.ts b/src/client/common/terminal/helper.ts new file mode 100644 index 000000000000..7d22bf75b8a5 --- /dev/null +++ b/src/client/common/terminal/helper.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, multiInject, named } from 'inversify'; +import { Terminal, Uri } from 'vscode'; +import { ICondaService, IInterpreterService } from '../../interpreter/contracts'; +import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { ITerminalManager } from '../application/types'; +import '../extensions'; +import { traceDecorators, traceError } from '../logger'; +import { IPlatformService } from '../platform/types'; +import { IConfigurationService, Resource } from '../types'; +import { OSType } from '../utils/platform'; +import { ShellDetector } from './shellDetector'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalHelper, + TerminalActivationProviders, + TerminalShellType +} from './types'; + +@injectable() +export class TerminalHelper implements ITerminalHelper { + private readonly shellDetector: ShellDetector; + constructor( + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(ICondaService) private readonly condaService: ICondaService, + @inject(IInterpreterService) readonly interpreterService: IInterpreterService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.conda) + private readonly conda: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.bashCShellFish) + private readonly bashCShellFish: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.commandPromptAndPowerShell) + private readonly commandPromptAndPowerShell: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.pyenv) + private readonly pyenv: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.pipenv) + private readonly pipenv: ITerminalActivationCommandProvider, + @multiInject(IShellDetector) shellDetectors: IShellDetector[] + ) { + this.shellDetector = new ShellDetector(this.platform, shellDetectors); + } + public createTerminal(title?: string): Terminal { + return this.terminalManager.createTerminal({ name: title }); + } + public identifyTerminalShell(terminal?: Terminal): TerminalShellType { + return this.shellDetector.identifyTerminalShell(terminal); + } + + public buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]) { + const isPowershell = + terminalShellType === TerminalShellType.powershell || + terminalShellType === TerminalShellType.powershellCore; + const commandPrefix = isPowershell ? '& ' : ''; + const formattedArgs = args.map((a) => a.toCommandArgument()); + + return `${commandPrefix}${command.fileToCommandArgument()} ${formattedArgs.join(' ')}`.trim(); + } + public async getEnvironmentActivationCommands( + terminalShellType: TerminalShellType, + resource?: Uri, + interpreter?: PythonEnvironment + ): Promise { + const providers = [this.pipenv, this.pyenv, this.bashCShellFish, this.commandPromptAndPowerShell]; + const promise = this.getActivationCommands(resource || undefined, interpreter, terminalShellType, providers); + this.sendTelemetry( + terminalShellType, + EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL, + interpreter, + promise + ).ignoreErrors(); + return promise; + } + public async getEnvironmentActivationShellCommands( + resource: Resource, + shell: TerminalShellType, + interpreter?: PythonEnvironment + ): Promise { + if (this.platform.osType === OSType.Unknown) { + return; + } + const providers = [this.bashCShellFish, this.commandPromptAndPowerShell]; + const promise = this.getActivationCommands(resource, interpreter, shell, providers); + this.sendTelemetry( + shell, + EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE, + interpreter, + promise + ).ignoreErrors(); + return promise; + } + @traceDecorators.error('Failed to capture telemetry') + protected async sendTelemetry( + terminalShellType: TerminalShellType, + eventName: EventName, + interpreter: PythonEnvironment | undefined, + promise: Promise + ): Promise { + let hasCommands = false; + let failed = false; + try { + const cmds = await promise; + hasCommands = Array.isArray(cmds) && cmds.length > 0; + } catch (ex) { + failed = true; + traceError('Failed to get activation commands', ex); + } + + const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; + const interpreterType = interpreter ? interpreter.envType : EnvironmentType.Unknown; + const data = { failed, hasCommands, interpreterType, terminal: terminalShellType, pythonVersion }; + sendTelemetryEvent(eventName, undefined, data); + } + protected async getActivationCommands( + resource: Resource, + interpreter: PythonEnvironment | undefined, + terminalShellType: TerminalShellType, + providers: ITerminalActivationCommandProvider[] + ): Promise { + const settings = this.configurationService.getSettings(resource); + + // If we have a conda environment, then use that. + const isCondaEnvironment = interpreter + ? interpreter.envType === EnvironmentType.Conda + : await this.condaService.isCondaEnvironment(settings.pythonPath); + if (isCondaEnvironment) { + const activationCommands = interpreter + ? await this.conda.getActivationCommandsForInterpreter(interpreter.path, terminalShellType) + : await this.conda.getActivationCommands(resource, terminalShellType); + + if (Array.isArray(activationCommands)) { + return activationCommands; + } + } + + // Search from the list of providers. + const supportedProviders = providers.filter((provider) => provider.isShellSupported(terminalShellType)); + + for (const provider of supportedProviders) { + const activationCommands = interpreter + ? await provider.getActivationCommandsForInterpreter(interpreter.path, terminalShellType) + : await provider.getActivationCommands(resource, terminalShellType); + + if (Array.isArray(activationCommands) && activationCommands.length > 0) { + return activationCommands; + } + } + } +} diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts new file mode 100644 index 000000000000..dd9bb04074e3 --- /dev/null +++ b/src/client/common/terminal/service.ts @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { CancellationToken, Disposable, Event, EventEmitter, Terminal } from 'vscode'; +import '../../common/extensions'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { captureTelemetry } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { ITerminalManager } from '../application/types'; +import { IConfigurationService, IDisposableRegistry } from '../types'; +import { + ITerminalActivator, + ITerminalHelper, + ITerminalService, + TerminalCreationOptions, + TerminalShellType +} from './types'; + +@injectable() +export class TerminalService implements ITerminalService, Disposable { + private terminal?: Terminal; + private terminalShellType!: TerminalShellType; + private terminalClosed = new EventEmitter(); + private terminalManager: ITerminalManager; + private terminalHelper: ITerminalHelper; + private terminalActivator: ITerminalActivator; + public get onDidCloseTerminal(): Event { + return this.terminalClosed.event.bind(this.terminalClosed); + } + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + private readonly options?: TerminalCreationOptions + ) { + const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); + disposableRegistry.push(this); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + this.terminalManager = this.serviceContainer.get(ITerminalManager); + this.terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry); + this.terminalActivator = this.serviceContainer.get(ITerminalActivator); + } + public dispose() { + if (this.terminal) { + this.terminal.dispose(); + } + } + public async sendCommand(command: string, args: string[], _?: CancellationToken): Promise { + await this.ensureTerminal(); + const text = this.terminalHelper.buildCommandForTerminal(this.terminalShellType, command, args); + if (!this.options?.hideFromUser) { + this.terminal!.show(true); + } + this.terminal!.sendText(text, true); + } + public async sendText(text: string): Promise { + await this.ensureTerminal(); + if (!this.options?.hideFromUser) { + this.terminal!.show(true); + } + this.terminal!.sendText(text); + } + public async show(preserveFocus: boolean = true): Promise { + await this.ensureTerminal(preserveFocus); + if (!this.options?.hideFromUser) { + this.terminal!.show(preserveFocus); + } + } + private async ensureTerminal(preserveFocus: boolean = true): Promise { + if (this.terminal) { + return; + } + this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); + this.terminal = this.terminalManager.createTerminal({ + name: this.options?.title || 'Python', + env: this.options?.env, + hideFromUser: this.options?.hideFromUser + }); + + // Sometimes the terminal takes some time to start up before it can start accepting input. + await new Promise((resolve) => setTimeout(resolve, 100)); + + await this.terminalActivator.activateEnvironmentInTerminal(this.terminal!, { + resource: this.options?.resource, + preserveFocus, + interpreter: this.options?.interpreter, + hideFromUser: this.options?.hideFromUser + }); + + if (!this.options?.hideFromUser) { + this.terminal!.show(preserveFocus); + } + + this.sendTelemetry().ignoreErrors(); + } + private terminalCloseHandler(terminal: Terminal) { + if (terminal === this.terminal) { + this.terminalClosed.fire(); + this.terminal = undefined; + } + } + + private async sendTelemetry() { + const pythonPath = this.serviceContainer + .get(IConfigurationService) + .getSettings(this.options?.resource).pythonPath; + const interpreterInfo = + this.options?.interpreter || + (await this.serviceContainer + .get(IInterpreterService) + .getInterpreterDetails(pythonPath)); + const pythonVersion = interpreterInfo && interpreterInfo.version ? interpreterInfo.version.raw : undefined; + const interpreterType = interpreterInfo ? interpreterInfo.envType : undefined; + captureTelemetry(EventName.TERMINAL_CREATE, { + terminal: this.terminalShellType, + pythonVersion, + interpreterType + }); + } +} diff --git a/src/client/common/terminal/shellDetector.ts b/src/client/common/terminal/shellDetector.ts new file mode 100644 index 000000000000..9a911a26bc0b --- /dev/null +++ b/src/client/common/terminal/shellDetector.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, multiInject } from 'inversify'; +import { Terminal } from 'vscode'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import '../extensions'; +import { traceVerbose } from '../logger'; +import { IPlatformService } from '../platform/types'; +import { OSType } from '../utils/platform'; +import { IShellDetector, ShellIdentificationTelemetry, TerminalShellType } from './types'; + +const defaultOSShells = { + [OSType.Linux]: TerminalShellType.bash, + [OSType.OSX]: TerminalShellType.bash, + [OSType.Windows]: TerminalShellType.commandPrompt, + [OSType.Unknown]: TerminalShellType.other +}; + +@injectable() +export class ShellDetector { + constructor( + @inject(IPlatformService) private readonly platform: IPlatformService, + @multiInject(IShellDetector) private readonly shellDetectors: IShellDetector[] + ) {} + /** + * Logic is as follows: + * 1. Try to identify the type of the shell based on the name of the terminal. + * 2. Try to identify the type of the shell based on the usettigs in VSC. + * 3. Try to identify the type of the shell based on the user environment (OS). + * 4. If all else fail, use defaults hardcoded (cmd for windows, bash for linux & mac). + * More information here See solution here https://github.com/microsoft/vscode/issues/74233#issuecomment-497527337 + * + * @param {Terminal} [terminal] + * @returns {TerminalShellType} + * @memberof TerminalHelper + */ + public identifyTerminalShell(terminal?: Terminal): TerminalShellType { + let shell: TerminalShellType | undefined; + const telemetryProperties: ShellIdentificationTelemetry = { + failed: true, + shellIdentificationSource: 'default', + terminalProvided: !!terminal, + hasCustomShell: undefined, + hasShellInEnv: undefined + }; + + // Sort in order of priority and then identify the shell. + const shellDetectors = this.shellDetectors.slice().sort((a, b) => b.priority - a.priority); + + for (const detector of shellDetectors) { + shell = detector.identify(telemetryProperties, terminal); + traceVerbose( + `${detector}. Shell identified as ${shell} ${terminal ? `(Terminal name is ${terminal.name})` : ''}` + ); + if (shell && shell !== TerminalShellType.other) { + break; + } + } + + // This information is useful in determining how well we identify shells on users machines. + // This impacts executing code in terminals and activation of environments in terminal. + // So, the better this works, the better it is for the user. + sendTelemetryEvent(EventName.TERMINAL_SHELL_IDENTIFICATION, undefined, telemetryProperties); + traceVerbose(`Shell identified as '${shell}'`); + + // If we could not identify the shell, use the defaults. + if (shell === undefined || shell === TerminalShellType.other) { + traceVerbose('Using default OS shell'); + shell = defaultOSShells[this.platform.osType]; + } + return shell; + } +} diff --git a/src/client/common/terminal/shellDetectors/baseShellDetector.ts b/src/client/common/terminal/shellDetectors/baseShellDetector.ts new file mode 100644 index 000000000000..04c534dedf06 --- /dev/null +++ b/src/client/common/terminal/shellDetectors/baseShellDetector.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable, unmanaged } from 'inversify'; +import { Terminal } from 'vscode'; +import { traceVerbose } from '../../logger'; +import { IShellDetector, ShellIdentificationTelemetry, TerminalShellType } from '../types'; + +// tslint:disable: max-classes-per-file + +/* +When identifying the shell use the following algorithm: +* 1. Identify shell based on the name of the terminal (if there is one already opened and used). +* 2. Identify shell based on the api provided by VSC. +* 2. Identify shell based on the settings in VSC. +* 3. Identify shell based on users environment variables. +* 4. Use default shells (bash for mac and linux, cmd for windows). +*/ + +// Types of shells can be found here: +// 1. https://wiki.ubuntu.com/ChangingShells +const IS_GITBASH = /(gitbash.exe$)/i; +const IS_BASH = /(bash.exe$|bash$)/i; +const IS_WSL = /(wsl.exe$)/i; +const IS_ZSH = /(zsh$)/i; +const IS_KSH = /(ksh$)/i; +const IS_COMMAND = /(cmd.exe$|cmd$)/i; +const IS_POWERSHELL = /(powershell.exe$|powershell$)/i; +const IS_POWERSHELL_CORE = /(pwsh.exe$|pwsh$)/i; +const IS_FISH = /(fish$)/i; +const IS_CSHELL = /(csh$)/i; +const IS_TCSHELL = /(tcsh$)/i; +const IS_XONSH = /(xonsh$)/i; + +const detectableShells = new Map(); +detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL); +detectableShells.set(TerminalShellType.gitbash, IS_GITBASH); +detectableShells.set(TerminalShellType.bash, IS_BASH); +detectableShells.set(TerminalShellType.wsl, IS_WSL); +detectableShells.set(TerminalShellType.zsh, IS_ZSH); +detectableShells.set(TerminalShellType.ksh, IS_KSH); +detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND); +detectableShells.set(TerminalShellType.fish, IS_FISH); +detectableShells.set(TerminalShellType.tcshell, IS_TCSHELL); +detectableShells.set(TerminalShellType.cshell, IS_CSHELL); +detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE); +detectableShells.set(TerminalShellType.xonsh, IS_XONSH); + +@injectable() +export abstract class BaseShellDetector implements IShellDetector { + constructor(@unmanaged() public readonly priority: number) {} + public abstract identify( + telemetryProperties: ShellIdentificationTelemetry, + terminal?: Terminal + ): TerminalShellType | undefined; + public identifyShellFromShellPath(shellPath: string): TerminalShellType { + const shell = Array.from(detectableShells.keys()).reduce((matchedShell, shellToDetect) => { + if (matchedShell === TerminalShellType.other) { + const pat = detectableShells.get(shellToDetect); + if (pat && pat.test(shellPath)) { + return shellToDetect; + } + } + return matchedShell; + }, TerminalShellType.other); + + traceVerbose(`Shell path '${shellPath}'`); + traceVerbose(`Shell path identified as shell '${shell}'`); + return shell; + } +} diff --git a/src/client/common/terminal/shellDetectors/settingsShellDetector.ts b/src/client/common/terminal/shellDetectors/settingsShellDetector.ts new file mode 100644 index 000000000000..5fb315cc1c2c --- /dev/null +++ b/src/client/common/terminal/shellDetectors/settingsShellDetector.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Terminal } from 'vscode'; +import { IWorkspaceService } from '../../application/types'; +import { traceVerbose } from '../../logger'; +import { IPlatformService } from '../../platform/types'; +import { OSType } from '../../utils/platform'; +import { ShellIdentificationTelemetry, TerminalShellType } from '../types'; +import { BaseShellDetector } from './baseShellDetector'; + +/** + * Identifies the shell based on the user settings. + * + * @export + * @class SettingsShellDetector + * @extends {BaseShellDetector} + */ +@injectable() +export class SettingsShellDetector extends BaseShellDetector { + constructor( + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IPlatformService) private readonly platform: IPlatformService + ) { + super(2); + } + public getTerminalShellPath(): string | undefined { + const shellConfig = this.workspace.getConfiguration('terminal.integrated.shell'); + let osSection = ''; + switch (this.platform.osType) { + case OSType.Windows: { + osSection = 'windows'; + break; + } + case OSType.OSX: { + osSection = 'osx'; + break; + } + case OSType.Linux: { + osSection = 'linux'; + break; + } + default: { + return ''; + } + } + return shellConfig.get(osSection)!; + } + public identify( + telemetryProperties: ShellIdentificationTelemetry, + _terminal?: Terminal + ): TerminalShellType | undefined { + const shellPath = this.getTerminalShellPath(); + telemetryProperties.hasCustomShell = !!shellPath; + const shell = shellPath ? this.identifyShellFromShellPath(shellPath) : TerminalShellType.other; + + if (shell !== TerminalShellType.other) { + telemetryProperties.shellIdentificationSource = 'environment'; + } + telemetryProperties.shellIdentificationSource = 'settings'; + traceVerbose(`Shell path from user settings '${shellPath}'`); + return shell; + } +} diff --git a/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts b/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts new file mode 100644 index 000000000000..34ca634d3936 --- /dev/null +++ b/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { Terminal } from 'vscode'; +import { traceVerbose } from '../../logger'; +import { ShellIdentificationTelemetry, TerminalShellType } from '../types'; +import { BaseShellDetector } from './baseShellDetector'; + +/** + * Identifies the shell, based on the display name of the terminal. + * + * @export + * @class TerminalNameShellDetector + * @extends {BaseShellDetector} + */ +@injectable() +export class TerminalNameShellDetector extends BaseShellDetector { + constructor() { + super(4); + } + public identify( + telemetryProperties: ShellIdentificationTelemetry, + terminal?: Terminal + ): TerminalShellType | undefined { + if (!terminal) { + return; + } + const shell = this.identifyShellFromShellPath(terminal.name); + traceVerbose(`Terminal name '${terminal.name}' identified as shell '${shell}'`); + telemetryProperties.shellIdentificationSource = + shell === TerminalShellType.other ? telemetryProperties.shellIdentificationSource : 'terminalName'; + return shell; + } +} diff --git a/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts b/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts new file mode 100644 index 000000000000..3f655da212ca --- /dev/null +++ b/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Terminal } from 'vscode'; +import { traceVerbose } from '../../logger'; +import { IPlatformService } from '../../platform/types'; +import { ICurrentProcess } from '../../types'; +import { OSType } from '../../utils/platform'; +import { ShellIdentificationTelemetry, TerminalShellType } from '../types'; +import { BaseShellDetector } from './baseShellDetector'; + +/** + * Identifies the shell based on the users environment (env variables). + * + * @export + * @class UserEnvironmentShellDetector + * @extends {BaseShellDetector} + */ +@injectable() +export class UserEnvironmentShellDetector extends BaseShellDetector { + constructor( + @inject(ICurrentProcess) private readonly currentProcess: ICurrentProcess, + @inject(IPlatformService) private readonly platform: IPlatformService + ) { + super(1); + } + public getDefaultPlatformShell(): string { + return getDefaultShell(this.platform, this.currentProcess); + } + public identify( + telemetryProperties: ShellIdentificationTelemetry, + _terminal?: Terminal + ): TerminalShellType | undefined { + const shellPath = this.getDefaultPlatformShell(); + telemetryProperties.hasShellInEnv = !!shellPath; + const shell = this.identifyShellFromShellPath(shellPath); + + if (shell !== TerminalShellType.other) { + telemetryProperties.shellIdentificationSource = 'environment'; + } + traceVerbose(`Shell path from user env '${shellPath}'`); + return shell; + } +} + +/* + The following code is based on VS Code from https://github.com/microsoft/vscode/blob/5c65d9bfa4c56538150d7f3066318e0db2c6151f/src/vs/workbench/contrib/terminal/node/terminal.ts#L12-L55 + This is only a fall back to identify the default shell used by VSC. + On Windows, determine the default shell. + On others, default to bash. +*/ +function getDefaultShell(platform: IPlatformService, currentProcess: ICurrentProcess): string { + if (platform.osType === OSType.Windows) { + return getTerminalDefaultShellWindows(platform, currentProcess); + } + + return currentProcess.env.SHELL && currentProcess.env.SHELL !== '/bin/false' + ? currentProcess.env.SHELL + : '/bin/bash'; +} +function getTerminalDefaultShellWindows(platform: IPlatformService, currentProcess: ICurrentProcess): string { + const isAtLeastWindows10 = parseFloat(platform.osRelease) >= 10; + const is32ProcessOn64Windows = currentProcess.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + const powerShellPath = `${currentProcess.env.windir}\\${ + is32ProcessOn64Windows ? 'Sysnative' : 'System32' + }\\WindowsPowerShell\\v1.0\\powershell.exe`; + return isAtLeastWindows10 ? powerShellPath : getWindowsShell(currentProcess); +} + +function getWindowsShell(currentProcess: ICurrentProcess): string { + return currentProcess.env.comspec || 'cmd.exe'; +} diff --git a/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts b/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts new file mode 100644 index 000000000000..e39197fbea7b --- /dev/null +++ b/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject } from 'inversify'; +import { Terminal } from 'vscode'; +import { IApplicationEnvironment } from '../../application/types'; +import { traceVerbose } from '../../logger'; +import { ShellIdentificationTelemetry, TerminalShellType } from '../types'; +import { BaseShellDetector } from './baseShellDetector'; + +/** + * Identifies the shell, based on the VSC Environment API. + * + * @export + * @class VSCEnvironmentShellDetector + * @extends {BaseShellDetector} + */ +export class VSCEnvironmentShellDetector extends BaseShellDetector { + constructor(@inject(IApplicationEnvironment) private readonly appEnv: IApplicationEnvironment) { + super(3); + } + public identify( + telemetryProperties: ShellIdentificationTelemetry, + _terminal?: Terminal + ): TerminalShellType | undefined { + if (!this.appEnv.shell) { + return; + } + const shell = this.identifyShellFromShellPath(this.appEnv.shell); + traceVerbose(`Terminal shell path '${this.appEnv.shell}' identified as shell '${shell}'`); + telemetryProperties.shellIdentificationSource = + shell === TerminalShellType.other ? telemetryProperties.shellIdentificationSource : 'vscode'; + return shell; + } +} diff --git a/src/client/common/terminal/syncTerminalService.ts b/src/client/common/terminal/syncTerminalService.ts new file mode 100644 index 000000000000..53e6231be95d --- /dev/null +++ b/src/client/common/terminal/syncTerminalService.ts @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject } from 'inversify'; +import { CancellationToken, Disposable, Event } from 'vscode'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { Cancellation } from '../cancellation'; +import { traceVerbose } from '../logger'; +import { IFileSystem, TemporaryFile } from '../platform/types'; +import * as internalScripts from '../process/internal/scripts'; +import { createDeferred, Deferred } from '../utils/async'; +import { noop } from '../utils/misc'; +import { TerminalService } from './service'; +import { ITerminalService } from './types'; + +enum State { + notStarted = 0, + started = 1, + completed = 2, + errored = 4 +} + +class ExecutionState implements Disposable { + public state: State = State.notStarted; + private _completed: Deferred = createDeferred(); + private disposable?: Disposable; + constructor( + public readonly lockFile: string, + private readonly fs: IFileSystem, + private readonly command: string[] + ) { + this.registerStateUpdate(); + this._completed.promise.finally(() => this.dispose()).ignoreErrors(); + } + public get completed(): Promise { + return this._completed.promise; + } + public dispose() { + if (this.disposable) { + this.disposable.dispose(); + this.disposable = undefined; + } + } + private registerStateUpdate() { + const timeout = setInterval(async () => { + const state = await this.getLockFileState(this.lockFile); + if (state !== this.state) { + traceVerbose(`Command state changed to ${state}. ${this.command.join(' ')}`); + } + this.state = state; + if (state & State.errored) { + const errorContents = await this.fs.readFile(`${this.lockFile}.error`).catch(() => ''); + this._completed.reject( + new Error( + `Command failed with errors, check the terminal for details. Command: ${this.command.join( + ' ' + )}\n${errorContents}` + ) + ); + } else if (state & State.completed) { + this._completed.resolve(); + } + }, 100); + + this.disposable = { + // tslint:disable-next-line: no-any + dispose: () => clearInterval(timeout as any) + }; + } + private async getLockFileState(file: string): Promise { + const source = await this.fs.readFile(file); + let state: State = State.notStarted; + if (source.includes('START')) { + state |= State.started; + } + if (source.includes('END')) { + state |= State.completed; + } + if (source.includes('FAIL')) { + state |= State.completed | State.errored; + } + return state; + } +} + +/** + * This is a decorator class that ensures commands send to a terminal are completed and then execution is returned back to calling code. + * The tecnique used is simple: + * - Instead of sending actual text to a terminal, + * - Send text to a terminal that executes our python file, passing in the original text as args + * - The pthon file will execute the commands as a subprocess + * - At the end of the execution a file is created to singal completion. + * + * @export + * @class SynchronousTerminalService + * @implements {ITerminalService} + * @implements {Disposable} + */ +export class SynchronousTerminalService implements ITerminalService, Disposable { + private readonly disposables: Disposable[] = []; + public get onDidCloseTerminal(): Event { + return this.terminalService.onDidCloseTerminal; + } + constructor( + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IInterpreterService) private readonly interpreter: IInterpreterService, + public readonly terminalService: TerminalService, + private readonly pythonInterpreter?: PythonEnvironment + ) {} + public dispose() { + this.terminalService.dispose(); + while (this.disposables.length) { + const disposable = this.disposables.shift(); + if (disposable) { + try { + disposable.dispose(); + } catch { + noop(); + } + } else { + break; + } + } + } + public async sendCommand( + command: string, + args: string[], + cancel?: CancellationToken, + swallowExceptions: boolean = true + ): Promise { + if (!cancel) { + return this.terminalService.sendCommand(command, args); + } + const lockFile = await this.createLockFile(); + const state = new ExecutionState(lockFile.filePath, this.fs, [command, ...args]); + try { + const pythonExec = this.pythonInterpreter || (await this.interpreter.getActiveInterpreter(undefined)); + const sendArgs = internalScripts.shell_exec(command, lockFile.filePath, args); + await this.terminalService.sendCommand(pythonExec?.path || 'python', sendArgs); + const promise = swallowExceptions ? state.completed : state.completed.catch(noop); + await Cancellation.race(() => promise, cancel); + } finally { + state.dispose(); + lockFile.dispose(); + } + } + public sendText(text: string): Promise { + return this.terminalService.sendText(text); + } + public show(preserveFocus?: boolean | undefined): Promise { + return this.terminalService.show(preserveFocus); + } + + private createLockFile(): Promise { + return this.fs.createTemporaryFile('.log').then((l) => { + this.disposables.push(l); + return l; + }); + } +} diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts new file mode 100644 index 000000000000..487827ffb4db --- /dev/null +++ b/src/client/common/terminal/types.ts @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { CancellationToken, Event, Terminal, Uri } from 'vscode'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { IEventNamePropertyMapping } from '../../telemetry/index'; +import { IDisposable, Resource } from '../types'; + +export enum TerminalActivationProviders { + bashCShellFish = 'bashCShellFish', + commandPromptAndPowerShell = 'commandPromptAndPowerShell', + pyenv = 'pyenv', + conda = 'conda', + pipenv = 'pipenv' +} +export enum TerminalShellType { + powershell = 'powershell', + powershellCore = 'powershellCore', + commandPrompt = 'commandPrompt', + gitbash = 'gitbash', + bash = 'bash', + zsh = 'zsh', + ksh = 'ksh', + fish = 'fish', + cshell = 'cshell', + tcshell = 'tshell', + wsl = 'wsl', + xonsh = 'xonsh', + other = 'other' +} + +export interface ITerminalService extends IDisposable { + readonly onDidCloseTerminal: Event; + /** + * Sends a command to the terminal. + * + * @param {string} command + * @param {string[]} args + * @param {CancellationToken} [cancel] If provided, then wait till the command is executed in the terminal. + * @param {boolean} [swallowExceptions] Whether to swallow exceptions raised as a result of the execution of the command. Defaults to `true`. + * @returns {Promise} + * @memberof ITerminalService + */ + sendCommand( + command: string, + args: string[], + cancel?: CancellationToken, + swallowExceptions?: boolean + ): Promise; + sendText(text: string): Promise; + show(preserveFocus?: boolean): Promise; +} + +export const ITerminalServiceFactory = Symbol('ITerminalServiceFactory'); + +export type TerminalCreationOptions = { + /** + * Object with environment variables that will be added to the Terminal. + */ + env?: { [key: string]: string | null }; + /** + * Resource identifier. E.g. used to determine python interpreter that needs to be used or environment variables or the like. + * + * @type {Uri} + */ + resource?: Uri; + /** + * Title. + * + * @type {string} + */ + title?: string; + /** + * Associated Python Interpreter. + * + * @type {PythonEnvironment} + */ + interpreter?: PythonEnvironment; + /** + * Whether hidden. + * + * @type {boolean} + */ + hideFromUser?: boolean; +}; + +export interface ITerminalServiceFactory { + /** + * Gets a terminal service with a specific title. + * If one exists, its returned else a new one is created. + * @param {Uri} resource + * @param {string} title + * @returns {ITerminalService} + * @memberof ITerminalServiceFactory + */ + getTerminalService(resource?: Uri, title?: string): ITerminalService; + /** + * Gets a terminal service. + * If one exists with the same information, that is returned else a new one is created. + * + * @param {TerminalCreationOptions} [options] + * @returns {ITerminalService} + * @memberof ITerminalServiceFactory + */ + getTerminalService(options?: TerminalCreationOptions): ITerminalService; + createTerminalService(resource?: Uri, title?: string): ITerminalService; +} + +export const ITerminalHelper = Symbol('ITerminalHelper'); + +export interface ITerminalHelper { + createTerminal(title?: string): Terminal; + identifyTerminalShell(terminal?: Terminal): TerminalShellType; + buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]): string; + getEnvironmentActivationCommands( + terminalShellType: TerminalShellType, + resource?: Uri, + interpreter?: PythonEnvironment + ): Promise; + getEnvironmentActivationShellCommands( + resource: Resource, + shell: TerminalShellType, + interpreter?: PythonEnvironment + ): Promise; +} + +export const ITerminalActivator = Symbol('ITerminalActivator'); +export type TerminalActivationOptions = { + resource?: Resource; + preserveFocus?: boolean; + interpreter?: PythonEnvironment; + /** + * When sending commands to the terminal, do not display the terminal. + * + * @type {boolean} + */ + hideFromUser?: boolean; +}; +export interface ITerminalActivator { + activateEnvironmentInTerminal(terminal: Terminal, options?: TerminalActivationOptions): Promise; +} + +export const ITerminalActivationCommandProvider = Symbol('ITerminalActivationCommandProvider'); + +export interface ITerminalActivationCommandProvider { + isShellSupported(targetShell: TerminalShellType): boolean; + getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise; + getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType + ): Promise; +} + +export const ITerminalActivationHandler = Symbol('ITerminalActivationHandler'); +export interface ITerminalActivationHandler { + handleActivation( + terminal: Terminal, + resource: Uri | undefined, + preserveFocus: boolean, + activated: boolean + ): Promise; +} + +export type ShellIdentificationTelemetry = IEventNamePropertyMapping['TERMINAL_SHELL_IDENTIFICATION']; + +export const IShellDetector = Symbol('IShellDetector'); +/** + * Used to identify a shell. + * Each implemenetion will provide a unique way of identifying the shell. + * + * @export + * @interface IShellDetector + */ +export interface IShellDetector { + /** + * Classes with higher priorities will be used first when identifying the shell. + * + * @type {number} + * @memberof IShellDetector + */ + readonly priority: number; + identify(telemetryProperties: ShellIdentificationTelemetry, terminal?: Terminal): TerminalShellType | undefined; +} diff --git a/src/client/common/types.ts b/src/client/common/types.ts new file mode 100644 index 000000000000..31f4affc6685 --- /dev/null +++ b/src/client/common/types.ts @@ -0,0 +1,659 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { Socket } from 'net'; +import { Request as RequestResult } from 'request'; +import { + CancellationToken, + ConfigurationTarget, + DiagnosticSeverity, + Disposable, + DocumentSymbolProvider, + Event, + Extension, + ExtensionContext, + OutputChannel, + Uri, + WorkspaceEdit +} from 'vscode'; +import { LanguageServerType } from '../activation/types'; +import { LogLevel } from '../logging/levels'; +import { CommandsWithoutArgs } from './application/commands'; +import { ExtensionChannels } from './insidersBuild/types'; +import { InterpreterUri } from './installer/types'; +import { EnvironmentVariables } from './variables/types'; +export const IOutputChannel = Symbol('IOutputChannel'); +export interface IOutputChannel extends OutputChannel {} +export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); +export interface IDocumentSymbolProvider extends DocumentSymbolProvider {} +export const IsWindows = Symbol('IS_WINDOWS'); +export const IDisposableRegistry = Symbol('IDisposableRegistry'); +export type IDisposableRegistry = Disposable[]; +export const IMemento = Symbol('IGlobalMemento'); +export const GLOBAL_MEMENTO = Symbol('IGlobalMemento'); +export const WORKSPACE_MEMENTO = Symbol('IWorkspaceMemento'); + +export type Resource = Uri | undefined; +export interface IPersistentState { + readonly value: T; + updateValue(value: T): Promise; +} +export type Version = { + raw: string; + major: number; + minor: number; + patch: number; + build: string[]; + prerelease: string[]; +}; + +export type ReadWrite = { + -readonly [P in keyof T]: T[P]; +}; + +export const IPersistentStateFactory = Symbol('IPersistentStateFactory'); + +export interface IPersistentStateFactory { + createGlobalPersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState; + createWorkspacePersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState; +} + +export type ExecutionInfo = { + execPath?: string; + moduleName?: string; + args: string[]; + product?: Product; +}; + +export enum InstallerResponse { + Installed, + Disabled, + Ignore +} + +export enum ProductType { + Linter = 'Linter', + Formatter = 'Formatter', + TestFramework = 'TestFramework', + RefactoringLibrary = 'RefactoringLibrary', + WorkspaceSymbols = 'WorkspaceSymbols', + DataScience = 'DataScience' +} + +export enum Product { + pytest = 1, + nosetest = 2, + pylint = 3, + flake8 = 4, + pycodestyle = 5, + pylama = 6, + prospector = 7, + pydocstyle = 8, + yapf = 9, + autopep8 = 10, + mypy = 11, + unittest = 12, + ctags = 13, + rope = 14, + isort = 15, + black = 16, + bandit = 17, + jupyter = 18, + ipykernel = 19, + notebook = 20, + kernelspec = 21, + nbconvert = 22, + pandas = 23 +} + +export enum ModuleNamePurpose { + install = 1, + run = 2 +} + +export const IInstaller = Symbol('IInstaller'); + +export interface IInstaller { + promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken + ): Promise; + install(product: Product, resource?: InterpreterUri, cancel?: CancellationToken): Promise; + isInstalled(product: Product, resource?: InterpreterUri): Promise; + translateProductToModuleName(product: Product, purpose: ModuleNamePurpose): string; +} + +// tslint:disable-next-line:no-suspicious-comment +// TODO: Drop IPathUtils in favor of IFileSystemPathUtils. +// See https://github.com/microsoft/vscode-python/issues/8542. +export const IPathUtils = Symbol('IPathUtils'); +export interface IPathUtils { + readonly delimiter: string; + readonly home: string; + /** + * The platform-specific file separator. '\\' or '/'. + * @type {string} + * @memberof IPathUtils + */ + readonly separator: string; + getPathVariableName(): 'Path' | 'PATH'; + basename(pathValue: string, ext?: string): string; + getDisplayName(pathValue: string, cwd?: string): string; +} + +export const IRandom = Symbol('IRandom'); +export interface IRandom { + getRandomInt(min?: number, max?: number): number; +} + +export const ICurrentProcess = Symbol('ICurrentProcess'); +export interface ICurrentProcess { + readonly env: EnvironmentVariables; + readonly argv: string[]; + readonly stdout: NodeJS.WriteStream; + readonly stdin: NodeJS.ReadStream; + readonly execPath: string; + on(event: string | symbol, listener: Function): this; +} + +export interface IPythonSettings { + readonly pythonPath: string; + readonly venvPath: string; + readonly venvFolders: string[]; + readonly condaPath: string; + readonly pipenvPath: string; + readonly poetryPath: string; + readonly insidersChannel: ExtensionChannels; + readonly downloadLanguageServer: boolean; + readonly showStartPage: boolean; + readonly jediPath: string; + readonly jediMemoryLimit: number; + readonly devOptions: string[]; + readonly linting: ILintingSettings; + readonly formatting: IFormattingSettings; + readonly testing: ITestingSettings; + readonly autoComplete: IAutoCompleteSettings; + readonly terminal: ITerminalSettings; + readonly sortImports: ISortImportSettings; + readonly workspaceSymbols: IWorkspaceSymbolSettings; + readonly envFile: string; + readonly disableInstallationChecks: boolean; + readonly globalModuleInstallation: boolean; + readonly analysis: IAnalysisSettings; + readonly autoUpdateLanguageServer: boolean; + readonly datascience: IDataScienceSettings; + readonly onDidChange: Event; + readonly experiments: IExperiments; + readonly languageServer: LanguageServerType; + readonly defaultInterpreterPath: string; + readonly logging: ILoggingSettings; +} +export interface ISortImportSettings { + readonly path: string; + readonly args: string[]; +} + +export interface ITestingSettings { + readonly promptToConfigure: boolean; + readonly debugPort: number; + readonly nosetestsEnabled: boolean; + nosetestPath: string; + nosetestArgs: string[]; + readonly pytestEnabled: boolean; + pytestPath: string; + pytestArgs: string[]; + readonly unittestEnabled: boolean; + unittestArgs: string[]; + cwd?: string; + readonly autoTestDiscoverOnSaveEnabled: boolean; +} +export interface IPylintCategorySeverity { + readonly convention: DiagnosticSeverity; + readonly refactor: DiagnosticSeverity; + readonly warning: DiagnosticSeverity; + readonly error: DiagnosticSeverity; + readonly fatal: DiagnosticSeverity; +} +export interface IPycodestyleCategorySeverity { + readonly W: DiagnosticSeverity; + readonly E: DiagnosticSeverity; +} +// tslint:disable-next-line:interface-name +export interface Flake8CategorySeverity { + readonly F: DiagnosticSeverity; + readonly E: DiagnosticSeverity; + readonly W: DiagnosticSeverity; +} +export interface IMypyCategorySeverity { + readonly error: DiagnosticSeverity; + readonly note: DiagnosticSeverity; +} + +export type LoggingLevelSettingType = 'off' | 'error' | 'warn' | 'info' | 'debug'; + +export interface ILoggingSettings { + readonly level: LogLevel | 'off'; +} +export interface ILintingSettings { + readonly enabled: boolean; + readonly ignorePatterns: string[]; + readonly prospectorEnabled: boolean; + readonly prospectorArgs: string[]; + readonly pylintEnabled: boolean; + readonly pylintArgs: string[]; + readonly pycodestyleEnabled: boolean; + readonly pycodestyleArgs: string[]; + readonly pylamaEnabled: boolean; + readonly pylamaArgs: string[]; + readonly flake8Enabled: boolean; + readonly flake8Args: string[]; + readonly pydocstyleEnabled: boolean; + readonly pydocstyleArgs: string[]; + readonly lintOnSave: boolean; + readonly maxNumberOfProblems: number; + readonly pylintCategorySeverity: IPylintCategorySeverity; + readonly pycodestyleCategorySeverity: IPycodestyleCategorySeverity; + readonly flake8CategorySeverity: Flake8CategorySeverity; + readonly mypyCategorySeverity: IMypyCategorySeverity; + prospectorPath: string; + pylintPath: string; + pycodestylePath: string; + pylamaPath: string; + flake8Path: string; + pydocstylePath: string; + mypyEnabled: boolean; + mypyArgs: string[]; + mypyPath: string; + banditEnabled: boolean; + banditArgs: string[]; + banditPath: string; + readonly pylintUseMinimalCheckers: boolean; +} +export interface IFormattingSettings { + readonly provider: string; + autopep8Path: string; + readonly autopep8Args: string[]; + blackPath: string; + readonly blackArgs: string[]; + yapfPath: string; + readonly yapfArgs: string[]; +} +export interface IAutoCompleteSettings { + readonly addBrackets: boolean; + readonly extraPaths: string[]; + readonly showAdvancedMembers: boolean; + readonly typeshedPaths: string[]; +} +export interface IWorkspaceSymbolSettings { + readonly enabled: boolean; + tagFilePath: string; + readonly rebuildOnStart: boolean; + readonly rebuildOnFileSave: boolean; + readonly ctagsPath: string; + readonly exclusionPatterns: string[]; +} +export interface ITerminalSettings { + readonly executeInFileDir: boolean; + readonly launchArgs: string[]; + readonly activateEnvironment: boolean; + readonly activateEnvInCurrentTerminal: boolean; +} + +export interface IExperiments { + /** + * Return `true` if experiments are enabled, else `false`. + */ + readonly enabled: boolean; + /** + * Experiments user requested to opt into manually + */ + readonly optInto: string[]; + /** + * Experiments user requested to opt out from manually + */ + readonly optOutFrom: string[]; +} + +export enum AnalysisSettingsLogLevel { + Information = 'Information', + Error = 'Error', + Warning = 'Warning' +} + +export type LanguageServerDownloadChannels = 'stable' | 'beta' | 'daily'; +export interface IAnalysisSettings { + readonly downloadChannel?: LanguageServerDownloadChannels; + readonly typeshedPaths: string[]; + readonly cacheFolderPath: string | null; + readonly errors: string[]; + readonly warnings: string[]; + readonly information: string[]; + readonly disabled: string[]; + readonly traceLogging: boolean; + readonly logLevel: AnalysisSettingsLogLevel; +} + +export interface IVariableQuery { + language: string; + query: string; + parseExpr: string; +} + +export interface IDataScienceSettings { + allowImportFromNotebook: boolean; + alwaysTrustNotebooks: boolean; + enabled: boolean; + jupyterInterruptTimeout: number; + jupyterLaunchTimeout: number; + jupyterLaunchRetries: number; + jupyterServerURI: string; + notebookFileRoot: string; + changeDirOnImportExport: boolean; + useDefaultConfigForJupyter: boolean; + searchForJupyter: boolean; + allowInput: boolean; + showCellInputCode: boolean; + collapseCellInputCodeByDefault: boolean; + maxOutputSize: number; + enableScrollingForCellOutputs: boolean; + gatherToScript?: boolean; + gatherSpecPath?: string; + sendSelectionToInteractiveWindow: boolean; + markdownRegularExpression: string; + codeRegularExpression: string; + allowLiveShare?: boolean; + errorBackgroundColor: string; + ignoreVscodeTheme?: boolean; + variableExplorerExclude?: string; + liveShareConnectionTimeout?: number; + decorateCells?: boolean; + enableCellCodeLens?: boolean; + askForLargeDataFrames?: boolean; + enableAutoMoveToNextCell?: boolean; + allowUnauthorizedRemoteConnection?: boolean; + askForKernelRestart?: boolean; + enablePlotViewer?: boolean; + codeLenses?: string; + debugCodeLenses?: string; + debugpyDistPath?: string; + stopOnFirstLineWhileDebugging?: boolean; + textOutputLimit?: number; + magicCommandsAsComments?: boolean; + stopOnError?: boolean; + remoteDebuggerPort?: number; + colorizeInputBox?: boolean; + addGotoCodeLenses?: boolean; + useNotebookEditor?: boolean; + runMagicCommands?: string; + runStartupCommands: string | string[]; + debugJustMyCode: boolean; + defaultCellMarker?: string; + verboseLogging?: boolean; + themeMatplotlibPlots?: boolean; + useWebViewServer?: boolean; + variableQueries: IVariableQuery[]; + disableJupyterAutoStart?: boolean; + jupyterCommandLineArguments: string[]; + widgetScriptSources: WidgetCDNs[]; + alwaysScrollOnNewCell?: boolean; + showKernelSelectionOnInteractiveWindow?: boolean; + interactiveWindowMode: InteractiveWindowMode; +} + +export type InteractiveWindowMode = 'perFile' | 'single' | 'multiple'; + +export type WidgetCDNs = 'unpkg.com' | 'jsdelivr.com'; + +export const IConfigurationService = Symbol('IConfigurationService'); +export interface IConfigurationService { + getSettings(resource?: Uri): IPythonSettings; + isTestExecution(): boolean; + updateSetting(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise; + updateSectionSetting( + section: string, + setting: string, + value?: {}, + resource?: Uri, + configTarget?: ConfigurationTarget + ): Promise; +} + +export const ISocketServer = Symbol('ISocketServer'); +export interface ISocketServer extends Disposable { + readonly client: Promise; + Start(options?: { port?: number; host?: string }): Promise; +} + +export type DownloadOptions = { + /** + * Prefix for progress messages displayed. + * + * @type {('Downloading ... ' | string)} + */ + progressMessagePrefix: 'Downloading ... ' | string; + /** + * Output panel into which progress information is written. + * + * @type {IOutputChannel} + */ + outputChannel?: IOutputChannel; + /** + * Extension of file that'll be created when downloading the file. + * + * @type {('tmp' | string)} + */ + extension: 'tmp' | string; +}; + +export const IFileDownloader = Symbol('IFileDownloader'); +/** + * File downloader, that'll display progress in the status bar. + * + * @export + * @interface IFileDownloader + */ +export interface IFileDownloader { + /** + * Download file and display progress in statusbar. + * Optionnally display progress in the provided output channel. + * + * @param {string} uri + * @param {DownloadOptions} options + * @returns {Promise} + * @memberof IFileDownloader + */ + downloadFile(uri: string, options: DownloadOptions): Promise; +} + +export const IHttpClient = Symbol('IHttpClient'); +export interface IHttpClient { + downloadFile(uri: string): Promise; + /** + * Downloads file from uri as string and parses them into JSON objects + * @param uri The uri to download the JSON from + * @param strict Set `false` to allow trailing comma and comments in the JSON, defaults to `true` + */ + getJSON(uri: string, strict?: boolean): Promise; + /** + * Returns the url is valid (i.e. return status code of 200). + */ + exists(uri: string): Promise; +} + +export const IExtensionContext = Symbol('ExtensionContext'); +export interface IExtensionContext extends ExtensionContext {} + +export const IExtensions = Symbol('IExtensions'); +export interface IExtensions { + /** + * All extensions currently known to the system. + */ + // tslint:disable-next-line:no-any + readonly all: readonly Extension[]; + + /** + * An event which fires when `extensions.all` changes. This can happen when extensions are + * installed, uninstalled, enabled or disabled. + */ + readonly onDidChange: Event; + + /** + * Get an extension by its full identifier in the form of: `publisher.name`. + * + * @param extensionId An extension identifier. + * @return An extension or `undefined`. + */ + // tslint:disable-next-line:no-any + getExtension(extensionId: string): Extension | undefined; + + /** + * Get an extension its full identifier in the form of: `publisher.name`. + * + * @param extensionId An extension identifier. + * @return An extension or `undefined`. + */ + getExtension(extensionId: string): Extension | undefined; +} + +export const IBrowserService = Symbol('IBrowserService'); +export interface IBrowserService { + launch(url: string): void; +} + +export const IPythonExtensionBanner = Symbol('IPythonExtensionBanner'); +export interface IPythonExtensionBanner { + readonly enabled: boolean; + showBanner(): Promise; +} +export const BANNER_NAME_PROPOSE_LS: string = 'ProposePylance'; +export const BANNER_NAME_DS_SURVEY: string = 'DSSurveyBanner'; +export const BANNER_NAME_INTERACTIVE_SHIFTENTER: string = 'InteractiveShiftEnterBanner'; + +export type DeprecatedSettingAndValue = { + setting: string; + values?: {}[]; +}; + +export type DeprecatedFeatureInfo = { + doNotDisplayPromptStateKey: string; + message: string; + moreInfoUrl: string; + commands?: CommandsWithoutArgs[]; + setting?: DeprecatedSettingAndValue; +}; + +export const IFeatureDeprecationManager = Symbol('IFeatureDeprecationManager'); + +export interface IFeatureDeprecationManager extends Disposable { + initialize(): void; + registerDeprecation(deprecatedInfo: DeprecatedFeatureInfo): void; +} + +export const IEditorUtils = Symbol('IEditorUtils'); +export interface IEditorUtils { + getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit; +} + +export interface IDisposable { + dispose(): void | undefined; +} +export interface IAsyncDisposable { + dispose(): Promise; +} + +/** + * Stores hash formats + */ +export interface IHashFormat { + number: number; // If hash format is a number + string: string; // If hash format is a string +} + +/** + * Interface used to implement cryptography tools + */ +export const ICryptoUtils = Symbol('ICryptoUtils'); +export interface ICryptoUtils { + /** + * Creates hash using the data and encoding specified + * @returns hash as number, or string + * @param data The string to hash + * @param hashFormat Return format of the hash, number or string + * @param [algorithm] + */ + createHash( + data: string, + hashFormat: E, + algorithm?: 'SHA512' | 'SHA256' | 'FNV' + ): IHashFormat[E]; +} + +export const IAsyncDisposableRegistry = Symbol('IAsyncDisposableRegistry'); +export interface IAsyncDisposableRegistry extends IAsyncDisposable { + push(disposable: IDisposable | IAsyncDisposable): void; +} + +/* ABExperiments field carries the identity, and the range of the experiment, + where the experiment is valid for users falling between the number 'min' and 'max' + More details: https://en.wikipedia.org/wiki/A/B_testing +*/ +export type ABExperiments = { + name: string; // Name of the experiment + salt: string; // Salt string for the experiment + min: number; // Lower limit for the experiment + max: number; // Upper limit for the experiment +}[]; + +/** + * Interface used to implement AB testing + */ +export const IExperimentsManager = Symbol('IExperimentsManager'); +export interface IExperimentsManager { + /** + * Checks if experiments are enabled, sets required environment to be used for the experiments, logs experiment groups + */ + activate(): Promise; + + /** + * Checks if user is in experiment or not + * @param experimentName Name of the experiment + * @returns `true` if user is in experiment, `false` if user is not in experiment + */ + inExperiment(experimentName: string): boolean; + + /** + * Sends experiment telemetry if user is in experiment + * @param experimentName Name of the experiment + */ + sendTelemetryIfInExperiment(experimentName: string): void; +} + +/** + * Experiment service leveraging VS Code's experiment framework. + */ +export const IExperimentService = Symbol('IExperimentService'); +export interface IExperimentService { + inExperiment(experimentName: string): Promise; + getExperimentValue(experimentName: string): Promise; +} + +export type InterpreterConfigurationScope = { uri: Resource; configTarget: ConfigurationTarget }; +export type InspectInterpreterSettingType = { + globalValue?: string; + workspaceValue?: string; + workspaceFolderValue?: string; +}; + +/** + * Interface used to access current Interpreter Path + */ +export const IInterpreterPathService = Symbol('IInterpreterPathService'); +export interface IInterpreterPathService { + onDidChange: Event; + get(resource: Resource): string; + inspect(resource: Resource): InspectInterpreterSettingType; + update(resource: Resource, configTarget: ConfigurationTarget, value: string | undefined): Promise; + copyOldInterpreterStorageValuesToNew(resource: Uri | undefined): Promise; +} diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts new file mode 100644 index 000000000000..a329f6c6465a --- /dev/null +++ b/src/client/common/utils/async.ts @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export async function sleep(timeout: number): Promise { + return new Promise((resolve) => { + setTimeout(() => resolve(timeout), timeout); + }); +} + +export async function waitForPromise(promise: Promise, timeout: number): Promise { + // Set a timer that will resolve with null + return new Promise((resolve, reject) => { + const timer = setTimeout(() => resolve(null), timeout); + promise + .then((result) => { + // When the promise resolves, make sure to clear the timer or + // the timer may stick around causing tests to wait + clearTimeout(timer); + resolve(result); + }) + .catch((e) => { + clearTimeout(timer); + reject(e); + }); + }); +} + +// tslint:disable-next-line: no-any +export function isThenable(v: any): v is Thenable { + return typeof v?.then === 'function'; +} + +// tslint:disable-next-line: no-any +export function isPromise(v: any): v is Promise { + return typeof v?.then === 'function' && typeof v?.catch === 'function'; +} + +//====================== +// Deferred + +// tslint:disable-next-line:interface-name +export interface Deferred { + readonly promise: Promise; + readonly resolved: boolean; + readonly rejected: boolean; + readonly completed: boolean; + resolve(value?: T | PromiseLike): void; + // tslint:disable-next-line:no-any + reject(reason?: any): void; +} + +class DeferredImpl implements Deferred { + private _resolve!: (value?: T | PromiseLike) => void; + // tslint:disable-next-line:no-any + private _reject!: (reason?: any) => void; + private _resolved: boolean = false; + private _rejected: boolean = false; + private _promise: Promise; + // tslint:disable-next-line:no-any + constructor(private scope: any = null) { + // tslint:disable-next-line:promise-must-complete + this._promise = new Promise((res, rej) => { + this._resolve = res; + this._reject = rej; + }); + } + public resolve(_value?: T | PromiseLike) { + // tslint:disable-next-line:no-any + this._resolve.apply(this.scope ? this.scope : this, arguments as any); + this._resolved = true; + } + // tslint:disable-next-line:no-any + public reject(_reason?: any) { + // tslint:disable-next-line:no-any + this._reject.apply(this.scope ? this.scope : this, arguments as any); + this._rejected = true; + } + get promise(): Promise { + return this._promise; + } + get resolved(): boolean { + return this._resolved; + } + get rejected(): boolean { + return this._rejected; + } + get completed(): boolean { + return this._rejected || this._resolved; + } +} +// tslint:disable-next-line:no-any +export function createDeferred(scope: any = null): Deferred { + return new DeferredImpl(scope); +} + +export function createDeferredFrom(...promises: Promise[]): Deferred { + const deferred = createDeferred(); + Promise.all(promises) + // tslint:disable-next-line:no-any + .then(deferred.resolve.bind(deferred) as any) + // tslint:disable-next-line:no-any + .catch(deferred.reject.bind(deferred) as any); + + return deferred; +} +export function createDeferredFromPromise(promise: Promise): Deferred { + const deferred = createDeferred(); + promise.then(deferred.resolve.bind(deferred)).catch(deferred.reject.bind(deferred)); + return deferred; +} + +//================================ +// iterators + +/** + * An iterator that yields nothing. + */ +export function iterEmpty(): AsyncIterator { + // tslint:disable-next-line:no-empty + return ((async function* () {})() as unknown) as AsyncIterator; +} + +type NextResult = { index: number } & ( + | { result: IteratorResult; err: null } + | { result: null; err: Error } +); +async function getNext(it: AsyncIterator, indexMaybe?: number): Promise> { + const index = indexMaybe === undefined ? -1 : indexMaybe; + try { + const result = await it.next(); + return { index, result, err: null }; + } catch (err) { + return { index, err, result: null }; + } +} + +// tslint:disable-next-line:promise-must-complete no-empty +const NEVER: Promise = new Promise(() => {}); + +/** + * Yield everything produced by the given iterators as soon as each is ready. + * + * When one of the iterators has something to yield then it gets yielded + * right away, regardless of where the iterator is located in the array + * of iterators. + * + * @param iterators - the async iterators from which to yield items + * @param onError - called/awaited once for each iterator that fails + */ +export async function* chain( + iterators: AsyncIterator[], + onError?: (err: Error, index: number) => Promise + // Ultimately we may also want to support cancellation. +): AsyncIterator { + const promises = iterators.map(getNext); + let numRunning = iterators.length; + while (numRunning > 0) { + const { index, result, err } = await Promise.race(promises); + if (err !== null) { + promises[index] = NEVER as Promise>; + numRunning -= 1; + if (onError !== undefined) { + await onError(err, index); + } + // XXX Log the error. + } else if (result!.done) { + promises[index] = NEVER as Promise>; + numRunning -= 1; + // If R is void then result.value will be undefined. + if (result!.value !== undefined) { + yield result!.value; + } + } else { + promises[index] = getNext(iterators[index], index); + // Only the "return" result can be undefined (void), + // so we're okay here. + yield result!.value as T; + } + } +} + +/** + * Get everything yielded by the iterator. + */ +export async function flattenIterator(iterator: AsyncIterator): Promise { + const results: T[] = []; + // We are dealing with an iterator, not an iterable, so we have + // to iterate manually rather than with a for-await loop. + let result = await iterator.next(); + while (!result.done) { + results.push(result.value); + result = await iterator.next(); + } + return results; +} diff --git a/src/client/common/utils/cacheUtils.ts b/src/client/common/utils/cacheUtils.ts new file mode 100644 index 000000000000..2f6895b4e5a8 --- /dev/null +++ b/src/client/common/utils/cacheUtils.ts @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-require-imports + +import { Uri } from 'vscode'; +import '../../common/extensions'; +import { IServiceContainer } from '../../ioc/types'; +import { DEFAULT_INTERPRETER_SETTING } from '../constants'; +import { DeprecatePythonPath } from '../experiments/groups'; +import { IExperimentsManager, IInterpreterPathService, Resource } from '../types'; + +type VSCodeType = typeof import('vscode'); +type CacheData = { + value: unknown; + expiry: number; +}; +const resourceSpecificCacheStores = new Map>(); + +/** + * Get a cache key specific to a resource (i.e. workspace) + * This key will be used to cache interpreter related data, hence the Python Path + * used in a workspace will affect the cache key. + * @param {Resource} resource + * @param {VSCodeType} [vscode=require('vscode')] + * @param serviceContainer + * @returns + */ +function getCacheKey( + resource: Resource, + vscode: VSCodeType = require('vscode'), + serviceContainer: IServiceContainer | undefined +) { + const section = vscode.workspace.getConfiguration('python', vscode.Uri.file(__filename)); + if (!section) { + return 'python'; + } + let interpreterPathService: IInterpreterPathService | undefined; + let inExperiment: boolean | undefined; + if (serviceContainer) { + interpreterPathService = serviceContainer.get(IInterpreterPathService); + const abExperiments = serviceContainer.get(IExperimentsManager); + inExperiment = abExperiments.inExperiment(DeprecatePythonPath.experiment); + abExperiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + } + const globalPythonPath = + inExperiment && interpreterPathService + ? interpreterPathService.inspect(vscode.Uri.file(__filename)).globalValue || DEFAULT_INTERPRETER_SETTING + : section.inspect('pythonPath')!.globalValue || DEFAULT_INTERPRETER_SETTING; + // Get the workspace related to this resource. + if ( + !resource || + !Array.isArray(vscode.workspace.workspaceFolders) || + vscode.workspace.workspaceFolders.length === 0 + ) { + return globalPythonPath; + } + const folder = resource ? vscode.workspace.getWorkspaceFolder(resource) : vscode.workspace.workspaceFolders[0]; + if (!folder) { + return globalPythonPath; + } + const workspacePythonPath = + inExperiment && interpreterPathService + ? interpreterPathService.get(resource) + : vscode.workspace.getConfiguration('python', resource).get('pythonPath') || + DEFAULT_INTERPRETER_SETTING; + return `${folder.uri.fsPath}-${workspacePythonPath}`; +} +/** + * Gets the cache store for a resource that's specific to the interpreter. + * @param {Resource} resource + * @param {VSCodeType} [vscode=require('vscode')] + * @param serviceContainer + * @returns + */ +function getCacheStore( + resource: Resource, + vscode: VSCodeType = require('vscode'), + serviceContainer: IServiceContainer | undefined +) { + const key = getCacheKey(resource, vscode, serviceContainer); + if (!resourceSpecificCacheStores.has(key)) { + resourceSpecificCacheStores.set(key, new Map()); + } + return resourceSpecificCacheStores.get(key)!; +} + +const globalCacheStore = new Map(); + +/** + * Gets a cache store to be used to store return values of methods or any other. + * + * @returns + */ +export function getGlobalCacheStore() { + return globalCacheStore; +} + +export function getCacheKeyFromFunctionArgs(keyPrefix: string, fnArgs: any[]): string { + const argsKey = fnArgs.map((arg) => `${JSON.stringify(arg)}`).join('-Arg-Separator-'); + return `KeyPrefix=${keyPrefix}-Args=${argsKey}`; +} + +export function clearCache() { + globalCacheStore.clear(); + resourceSpecificCacheStores.clear(); +} + +export class InMemoryCache { + private readonly _store = new Map(); + protected get store(): Map { + return this._store; + } + constructor(protected readonly expiryDurationMs: number, protected readonly cacheKey: string = '') {} + public get hasData() { + if (!this.store.get(this.cacheKey) || this.hasExpired(this.store.get(this.cacheKey)!.expiry)) { + this.store.delete(this.cacheKey); + return false; + } + return true; + } + /** + * Returns undefined if there is no data. + * Uses `hasData` to determine whether any cached data exists. + * + * @readonly + * @type {(T | undefined)} + * @memberof InMemoryCache + */ + public get data(): T | undefined { + if (!this.hasData || !this.store.has(this.cacheKey)) { + return; + } + return this.store.get(this.cacheKey)?.value as T; + } + public set data(value: T | undefined) { + this.store.set(this.cacheKey, { + expiry: this.calculateExpiry(), + value + }); + } + public clear() { + this.store.clear(); + } + + /** + * Has this data expired? + * (protected class member to allow for reliable non-data-time-based testing) + * + * @param expiry The date to be tested for expiry. + * @returns true if the data expired, false otherwise. + */ + protected hasExpired(expiry: number): boolean { + return expiry <= Date.now(); + } + + /** + * When should this data item expire? + * (protected class method to allow for reliable non-data-time-based testing) + * + * @returns number representing the expiry time for this item. + */ + protected calculateExpiry(): number { + return Date.now() + this.expiryDurationMs; + } +} + +export class InMemoryInterpreterSpecificCache extends InMemoryCache { + private readonly resource: Resource; + protected get store() { + return getCacheStore(this.resource, this.vscode, this.serviceContainer); + } + constructor( + keyPrefix: string, + expiryDurationMs: number, + args: [Uri | undefined, ...any[]], + private readonly serviceContainer: IServiceContainer | undefined, + private readonly vscode: VSCodeType = require('vscode') + ) { + super(expiryDurationMs, getCacheKeyFromFunctionArgs(keyPrefix, args.slice(1))); + this.resource = args[0]; + } +} diff --git a/src/client/common/utils/decorators.ts b/src/client/common/utils/decorators.ts new file mode 100644 index 000000000000..512f31e65e19 --- /dev/null +++ b/src/client/common/utils/decorators.ts @@ -0,0 +1,262 @@ +// tslint:disable:no-any no-require-imports no-function-expression no-invalid-this + +import { ProgressLocation, ProgressOptions, window } from 'vscode'; +import '../../common/extensions'; +import { IServiceContainer } from '../../ioc/types'; +import { isTestExecution } from '../constants'; +import { traceError, traceVerbose } from '../logger'; +import { Resource } from '../types'; +import { createDeferred, Deferred } from './async'; +import { getCacheKeyFromFunctionArgs, getGlobalCacheStore, InMemoryInterpreterSpecificCache } from './cacheUtils'; +import { TraceInfo, tracing } from './misc'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const _debounce = require('lodash/debounce') as typeof import('lodash/debounce'); + +type VoidFunction = () => any; +type AsyncVoidFunction = () => Promise; + +/** + * Combine multiple sequential calls to the decorated function into one. + * @export + * @param {number} [wait] Wait time (milliseconds). + * @returns void + * + * The point is to ensure that successive calls to the function result + * only in a single actual call. Following the most recent call to + * the debounced function, debouncing resets after the "wait" interval + * has elapsed. + */ +export function debounceSync(wait?: number) { + if (isTestExecution()) { + // If running tests, lets debounce until the next cycle in the event loop. + // Same as `setTimeout(()=> {}, 0);` with a value of `0`. + wait = undefined; + } + return makeDebounceDecorator(wait); +} + +/** + * Combine multiple sequential calls to the decorated async function into one. + * @export + * @param {number} [wait] Wait time (milliseconds). + * @returns void + * + * The point is to ensure that successive calls to the function result + * only in a single actual call. Following the most recent call to + * the debounced function, debouncing resets after the "wait" interval + * has elapsed. + */ +export function debounceAsync(wait?: number) { + if (isTestExecution()) { + // If running tests, lets debounce until the next cycle in the event loop. + // Same as `setTimeout(()=> {}, 0);` with a value of `0`. + wait = undefined; + } + return makeDebounceAsyncDecorator(wait); +} + +export function makeDebounceDecorator(wait?: number) { + // tslint:disable-next-line:no-any no-function-expression + return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { + // We could also make use of _debounce() options. For instance, + // the following causes the original method to be called + // immediately: + // + // {leading: true, trailing: false} + // + // The default is: + // + // {leading: false, trailing: true} + // + // See https://lodash.com/docs/#debounce. + const options = {}; + const originalMethod = descriptor.value!; + const debounced = _debounce( + function (this: any) { + return originalMethod.apply(this, arguments as any); + }, + wait, + options + ); + (descriptor as any).value = debounced; + }; +} + +export function makeDebounceAsyncDecorator(wait?: number) { + // tslint:disable-next-line:no-any no-function-expression + return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { + type StateInformation = { + started: boolean; + deferred: Deferred | undefined; + timer: NodeJS.Timer | number | undefined; + }; + const originalMethod = descriptor.value!; + const state: StateInformation = { started: false, deferred: undefined, timer: undefined }; + + // Lets defer execution using a setTimeout for the given time. + (descriptor as any).value = function (this: any) { + const existingDeferred: Deferred | undefined = state.deferred; + if (existingDeferred && state.started) { + return existingDeferred.promise; + } + + // Clear previous timer. + const existingDeferredCompleted = existingDeferred && existingDeferred.completed; + const deferred = (state.deferred = + !existingDeferred || existingDeferredCompleted ? createDeferred() : existingDeferred); + if (state.timer) { + clearTimeout(state.timer as any); + } + + state.timer = setTimeout(async () => { + state.started = true; + originalMethod + .apply(this) + .then((r) => { + state.started = false; + deferred.resolve(r); + }) + .catch((ex) => { + state.started = false; + deferred.reject(ex); + }); + }, wait || 0); + return deferred.promise; + }; + }; +} + +type VSCodeType = typeof import('vscode'); + +export function clearCachedResourceSpecificIngterpreterData( + key: string, + resource: Resource, + serviceContainer: IServiceContainer, + vscode: VSCodeType = require('vscode') +) { + const cacheStore = new InMemoryInterpreterSpecificCache(key, 0, [resource], serviceContainer, vscode); + cacheStore.clear(); +} + +type PromiseFunctionWithAnyArgs = (...any: any) => Promise; +const cacheStoreForMethods = getGlobalCacheStore(); +export function cache(expiryDurationMs: number) { + return function ( + target: Object, + propertyName: string, + descriptor: TypedPropertyDescriptor + ) { + const originalMethod = descriptor.value!; + const className = 'constructor' in target && target.constructor.name ? target.constructor.name : ''; + const keyPrefix = `Cache_Method_Output_${className}.${propertyName}`; + descriptor.value = async function (...args: any) { + if (isTestExecution()) { + return originalMethod.apply(this, args) as Promise; + } + const key = getCacheKeyFromFunctionArgs(keyPrefix, args); + const cachedItem = cacheStoreForMethods.get(key); + if (cachedItem && cachedItem.expiry > Date.now()) { + traceVerbose(`Cached data exists ${key}`); + return Promise.resolve(cachedItem.data); + } + const promise = originalMethod.apply(this, args) as Promise; + promise + .then((result) => + cacheStoreForMethods.set(key, { data: result, expiry: Date.now() + expiryDurationMs }) + ) + .ignoreErrors(); + return promise; + }; + }; +} + +/** + * Swallows exceptions thrown by a function. Function must return either a void or a promise that resolves to a void. + * When exceptions (including in promises) are caught, this will return `undefined` to calling code. + * @export + * @param {string} [scopeName] Scope for the error message to be logged along with the error. + * @returns void + */ +export function swallowExceptions(scopeName: string) { + // tslint:disable-next-line:no-any no-function-expression + return function (_target: any, propertyName: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value!; + const errorMessage = `Python Extension (Error in ${scopeName}, method:${propertyName}):`; + // tslint:disable-next-line:no-any no-function-expression + descriptor.value = function (...args: any[]) { + try { + // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any + const result = originalMethod.apply(this, args); + + // If method being wrapped returns a promise then wait and swallow errors. + if (result && typeof result.then === 'function' && typeof result.catch === 'function') { + return (result as Promise).catch((error) => { + if (isTestExecution()) { + return; + } + traceError(errorMessage, error); + }); + } + } catch (error) { + if (isTestExecution()) { + return; + } + traceError(errorMessage, error); + } + }; + }; +} + +// tslint:disable-next-line:no-any +type PromiseFunction = (...any: any[]) => Promise; + +export function displayProgress(title: string, location = ProgressLocation.Window) { + return function (_target: Object, _propertyName: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value!; + // tslint:disable-next-line:no-any no-function-expression + descriptor.value = async function (...args: any[]) { + const progressOptions: ProgressOptions = { location, title }; + // tslint:disable-next-line:no-invalid-this + const promise = originalMethod.apply(this, args); + if (!isTestExecution()) { + window.withProgress(progressOptions, () => promise); + } + return promise; + }; + }; +} + +// Information about a function/method call. +export type CallInfo = { + kind: string; // "Class", etc. + name: string; + // tslint:disable-next-line:no-any + args: any[]; +}; + +// Return a decorator that traces the decorated function. +export function trace(log: (c: CallInfo, t: TraceInfo) => void) { + // tslint:disable-next-line:no-function-expression no-any + return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value; + // tslint:disable-next-line:no-function-expression no-any + descriptor.value = function (...args: any[]) { + const call = { + kind: 'Class', + name: _ && _.constructor ? _.constructor.name : '', + args + }; + // tslint:disable-next-line:no-this-assignment no-invalid-this + const scope = this; + return tracing( + // "log()" + (t) => log(call, t), + // "run()" + () => originalMethod.apply(scope, args) + ); + }; + + return descriptor; + }; +} diff --git a/src/client/common/utils/enum.ts b/src/client/common/utils/enum.ts new file mode 100644 index 000000000000..2d6a53fdb505 --- /dev/null +++ b/src/client/common/utils/enum.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +export function getNamesAndValues(e: any): { name: string; value: T }[] { + return getNames(e).map((n) => ({ name: n, value: e[n] })); +} + +export function getNames(e: any) { + return getObjValues(e).filter((v) => typeof v === 'string') as string[]; +} + +export function getValues(e: any) { + return (getObjValues(e).filter((v) => typeof v === 'number') as any) as T[]; +} + +function getObjValues(e: any): (number | string)[] { + return Object.keys(e).map((k) => e[k]); +} diff --git a/src/client/common/utils/icons.ts b/src/client/common/utils/icons.ts new file mode 100644 index 000000000000..3d312818e058 --- /dev/null +++ b/src/client/common/utils/icons.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { Uri } from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../constants'; + +const darkIconsPath = path.join(EXTENSION_ROOT_DIR, 'resources', 'dark'); +const lightIconsPath = path.join(EXTENSION_ROOT_DIR, 'resources', 'light'); + +export function getIcon(fileName: string): { light: string | Uri; dark: string | Uri } { + return { + dark: path.join(darkIconsPath, fileName), + light: path.join(lightIconsPath, fileName) + }; +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts new file mode 100644 index 000000000000..d22795bb33e9 --- /dev/null +++ b/src/client/common/utils/localize.ts @@ -0,0 +1,1390 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { FileSystem } from '../platform/fileSystem'; + +// External callers of localize use these tables to retrieve localized values. +export namespace Diagnostics { + export const warnSourceMaps = localize( + 'diagnostics.warnSourceMaps', + 'Source map support is enabled in the Python Extension, this will adversely impact performance of the extension.' + ); + export const disableSourceMaps = localize('diagnostics.disableSourceMaps', 'Disable Source Map Support'); + export const warnBeforeEnablingSourceMaps = localize( + 'diagnostics.warnBeforeEnablingSourceMaps', + 'Enabling source map support in the Python Extension will adversely impact performance of the extension.' + ); + export const enableSourceMapsAndReloadVSC = localize( + 'diagnostics.enableSourceMapsAndReloadVSC', + 'Enable and reload Window.' + ); + export const lsNotSupported = localize( + 'diagnostics.lsNotSupported', + 'Your operating system does not meet the minimum requirements of the Python Language Server. Reverting to the alternative autocompletion provider, Jedi.' + ); + export const upgradeCodeRunner = localize( + 'diagnostics.upgradeCodeRunner', + 'Please update the Code Runner extension for it to be compatible with the Python extension.' + ); + export const removedPythonPathFromSettings = localize( + 'diagnostics.removedPythonPathFromSettings', + 'We removed the "python.pythonPath" setting from your settings.json file as the setting is no longer used by the Python extension. You can get the path of your selected interpreter in the Python output channel. [Learn more](https://aka.ms/AA7jfor).' + ); + export const invalidPythonPathInDebuggerSettings = localize( + 'diagnostics.invalidPythonPathInDebuggerSettings', + 'You need to select a Python interpreter before you start debugging.\n\nTip: click on "Select Python Interpreter" in the status bar.' + ); + export const invalidPythonPathInDebuggerLaunch = localize( + 'diagnostics.invalidPythonPathInDebuggerLaunch', + 'The Python path in your debug configuration is invalid.' + ); + export const invalidDebuggerTypeDiagnostic = localize( + 'diagnostics.invalidDebuggerTypeDiagnostic', + 'Your launch.json file needs to be updated to change the "pythonExperimental" debug configurations to use the "python" debugger type, otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?' + ); + export const consoleTypeDiagnostic = localize( + 'diagnostics.consoleTypeDiagnostic', + 'Your launch.json file needs to be updated to change the console type string from "none" to "internalConsole", otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?' + ); + export const justMyCodeDiagnostic = localize( + 'diagnostics.justMyCodeDiagnostic', + 'Configuration "debugStdLib" in launch.json is no longer supported. It\'s recommended to replace it with "justMyCode", which is the exact opposite of using "debugStdLib". Would you like to automatically update your launch.json file to do that?' + ); + export const yesUpdateLaunch = localize('diagnostics.yesUpdateLaunch', 'Yes, update launch.json'); + export const invalidTestSettings = localize( + 'diagnostics.invalidTestSettings', + 'Your settings needs to be updated to change the setting "python.unitTest." to "python.testing.", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?' + ); + export const updateSettings = localize('diagnostics.updateSettings', 'Yes, update settings'); + export const checkIsort5UpgradeGuide = localize( + 'diagnostics.checkIsort5UpgradeGuide', + 'We found outdated configuration for sorting imports in this workspace. Check the [isort upgrade guide](https://aka.ms/AA9j5x4) to update your settings.' + ); +} + +export namespace Common { + export const bannerLabelYes = localize('Common.bannerLabelYes', 'Yes'); + export const bannerLabelNo = localize('Common.bannerLabelNo', 'No'); + export const yesPlease = localize('Common.yesPlease', 'Yes, please'); + export const canceled = localize('Common.canceled', 'Canceled'); + export const cancel = localize('Common.cancel', 'Cancel'); + export const ok = localize('Common.ok', 'Ok'); + export const gotIt = localize('Common.gotIt', 'Got it!'); + export const install = localize('Common.install', 'Install'); + export const loadingExtension = localize('Common.loadingPythonExtension', 'Python extension loading...'); + export const openOutputPanel = localize('Common.openOutputPanel', 'Show output'); + export const noIWillDoItLater = localize('Common.noIWillDoItLater', 'No, I will do it later'); + export const notNow = localize('Common.notNow', 'Not now'); + export const doNotShowAgain = localize('Common.doNotShowAgain', 'Do not show again'); + export const reload = localize('Common.reload', 'Reload'); + export const moreInfo = localize('Common.moreInfo', 'More Info'); + export const learnMore = localize('Common.learnMore', 'Learn more'); + export const and = localize('Common.and', 'and'); + export const reportThisIssue = localize('Common.reportThisIssue', 'Report this issue'); +} + +export namespace CommonSurvey { + export const remindMeLaterLabel = localize('CommonSurvey.remindMeLaterLabel', 'Remind me later'); + export const yesLabel = localize('CommonSurvey.yesLabel', 'Yes, take survey now'); + export const noLabel = localize('CommonSurvey.noLabel', 'No, thanks'); +} + +export namespace AttachProcess { + export const unsupportedOS = localize('AttachProcess.unsupportedOS', "Operating system '{0}' not supported."); + export const attachTitle = localize('AttachProcess.attachTitle', 'Attach to process'); + export const selectProcessPlaceholder = localize( + 'AttachProcess.selectProcessPlaceholder', + 'Select the process to attach to' + ); + export const noProcessSelected = localize('AttachProcess.noProcessSelected', 'No process selected'); + export const refreshList = localize('AttachProcess.refreshList', 'Refresh process list'); +} + +export namespace Pylance { + export const proposePylanceMessage = localize( + 'Pylance.proposePylanceMessage', + 'Try out a new faster, feature-rich language server for Python by Microsoft, Pylance! Install the extension now.' + ); + export const tryItNow = localize('Pylance.tryItNow', 'Try it now'); + export const remindMeLater = localize('Pylance.remindMeLater', 'Remind me later'); + + export const installPylanceMessage = localize( + 'Pylance.installPylanceMessage', + 'Pylance extension is not installed. Click Yes to open Pylance installation page.' + ); + export const pylanceNotInstalledMessage = localize( + 'Pylance.pylanceNotInstalledMessage', + 'Pylance extension is not installed.' + ); + export const pylanceInstalledReloadPromptMessage = localize( + 'Pylance.pylanceInstalledReloadPromptMessage', + 'Pylance extension is now installed. Reload window to activate?' + ); +} + +export namespace LanguageService { + export const startingJedi = localize('LanguageService.startingJedi', 'Starting Jedi Python language engine.'); + export const startingMicrosoft = localize( + 'LanguageService.startingMicrosoft', + 'Starting Microsoft Python language server.' + ); + export const startingPylance = localize('LanguageService.startingPylance', 'Starting Pylance language server.'); + export const startingNone = localize( + 'LanguageService.startingNone', + 'Editor support is inactive since language server is set to None.' + ); + + export const reloadAfterLanguageServerChange = localize( + 'LanguageService.reloadAfterLanguageServerChange', + 'Please reload the window switching between language servers.' + ); + + export const lsFailedToStart = localize( + 'LanguageService.lsFailedToStart', + 'We encountered an issue starting the language server. Reverting to Jedi language engine. Check the Python output panel for details.' + ); + export const lsFailedToDownload = localize( + 'LanguageService.lsFailedToDownload', + 'We encountered an issue downloading the language server. Reverting to Jedi language engine. Check the Python output panel for details.' + ); + export const lsFailedToExtract = localize( + 'LanguageService.lsFailedToExtract', + 'We encountered an issue extracting the language server. Reverting to Jedi language engine. Check the Python output panel for details.' + ); + export const downloadFailedOutputMessage = localize( + 'LanguageService.downloadFailedOutputMessage', + 'Language server download failed.' + ); + export const extractionFailedOutputMessage = localize( + 'LanguageService.extractionFailedOutputMessage', + 'Language server extraction failed.' + ); + export const extractionCompletedOutputMessage = localize( + 'LanguageService.extractionCompletedOutputMessage', + 'Language server download complete.' + ); + export const extractionDoneOutputMessage = localize('LanguageService.extractionDoneOutputMessage', 'done.'); + export const reloadVSCodeIfSeachPathHasChanged = localize( + 'LanguageService.reloadVSCodeIfSeachPathHasChanged', + 'Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.' + ); +} + +export namespace Http { + export const downloadingFile = localize('downloading.file', 'Downloading {0}...'); + export const downloadingFileProgress = localize('downloading.file.progress', '{0}{1} of {2} KB ({3}%)'); +} +export namespace Experiments { + export const inGroup = localize('Experiments.inGroup', "User belongs to experiment group '{0}'"); +} +export namespace Interpreters { + export const loading = localize('Interpreters.LoadingInterpreters', 'Loading Python Interpreters'); + export const refreshing = localize('Interpreters.RefreshingInterpreters', 'Refreshing Python Interpreters'); + export const condaInheritEnvMessage = localize( + 'Interpreters.condaInheritEnvMessage', + 'We noticed you\'re using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings.' + ); + export const unsafeInterpreterMessage = localize( + 'Interpreters.unsafeInterpreterMessage', + 'We found a Python environment in this workspace. Do you want to select it to start up the features in the Python extension? Only accept if you trust this environment.' + ); + export const environmentPromptMessage = localize( + 'Interpreters.environmentPromptMessage', + 'We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?' + ); + export const entireWorkspace = localize('Interpreters.entireWorkspace', 'Entire workspace'); + export const selectInterpreterTip = localize( + 'Interpreters.selectInterpreterTip', + 'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar' + ); + export const pythonInterpreterPath = localize('Interpreters.pythonInterpreterPath', 'Python interpreter path: {0}'); +} + +export namespace InterpreterQuickPickList { + export const quickPickListPlaceholder = localize( + 'InterpreterQuickPickList.quickPickListPlaceholder', + 'Current: {0}' + ); + export const enterPath = { + detail: localize('InterpreterQuickPickList.enterPath.detail', 'Enter path or find an existing interpreter'), + label: localize('InterpreterQuickPickList.enterPath.label', 'Enter interpreter path...'), + placeholder: localize('InterpreterQuickPickList.enterPath.placeholder', 'Enter path to a Python interpreter.') + }; + export const browsePath = { + label: localize('InterpreterQuickPickList.browsePath.label', 'Find...'), + detail: localize( + 'InterpreterQuickPickList.browsePath.detail', + 'Browse your file system to find a Python interpreter.' + ), + openButtonLabel: localize('python.command.python.setInterpreter.title', 'Select Interpreter'), + title: localize('InterpreterQuickPickList.browsePath.title', 'Select Python interpreter') + }; +} +export namespace ExtensionChannels { + export const yesWeekly = localize('ExtensionChannels.yesWeekly', 'Yes, weekly'); + export const yesDaily = localize('ExtensionChannels.yesDaily', 'Yes, daily'); + export const promptMessage = localize( + 'ExtensionChannels.promptMessage', + 'We noticed you are using Visual Studio Code Insiders. Would you like to use the Insiders build of the Python extension?' + ); + export const reloadToUseInsidersMessage = localize( + 'ExtensionChannels.reloadToUseInsidersMessage', + 'Please reload Visual Studio Code to use the insiders build of the Python extension.' + ); + export const downloadCompletedOutputMessage = localize( + 'ExtensionChannels.downloadCompletedOutputMessage', + 'Insiders build download complete.' + ); + export const startingDownloadOutputMessage = localize( + 'ExtensionChannels.startingDownloadOutputMessage', + 'Starting download for Insiders build.' + ); + export const downloadingInsidersMessage = localize( + 'ExtensionChannels.downloadingInsidersMessage', + 'Downloading Insiders Extension... ' + ); + export const installingInsidersMessage = localize( + 'ExtensionChannels.installingInsidersMessage', + 'Installing Insiders build of extension... ' + ); + export const installingStableMessage = localize( + 'ExtensionChannels.installingStableMessage', + 'Installing Stable build of extension... ' + ); + export const installationCompleteMessage = localize('ExtensionChannels.installationCompleteMessage', 'complete.'); +} +export namespace OutputChannelNames { + export const languageServer = localize('OutputChannelNames.languageServer', 'Python Language Server'); + export const python = localize('OutputChannelNames.python', 'Python'); + export const pythonTest = localize('OutputChannelNames.pythonTest', 'Python Test Log'); + export const jupyter = localize('OutputChannelNames.jupyter', 'Jupyter'); +} + +export namespace Logging { + export const currentWorkingDirectory = localize('Logging.CurrentWorkingDirectory', 'cwd:'); +} + +export namespace Linters { + export const enableLinter = localize('Linter.enableLinter', 'Enable {0}'); + export const enablePylint = localize( + 'Linter.enablePylint', + 'You have a pylintrc file in your workspace. Do you want to enable pylint?' + ); + export const replaceWithSelectedLinter = localize( + 'Linter.replaceWithSelectedLinter', + "Multiple linters are enabled in settings. Replace with '{0}'?" + ); +} + +export namespace InteractiveShiftEnterBanner { + export const bannerMessage = localize( + 'InteractiveShiftEnterBanner.bannerMessage', + 'Would you like shift-enter to send code to the new Interactive Window experience?' + ); +} + +export namespace DataScienceSurveyBanner { + export const bannerMessage = localize( + 'DataScienceSurveyBanner.bannerMessage', + 'Can you please take 2 minutes to tell us how the Python Data Science features are working for you?' + ); + export const bannerLabelYes = localize('DataScienceSurveyBanner.bannerLabelYes', 'Yes, take survey now'); + export const bannerLabelNo = localize('DataScienceSurveyBanner.bannerLabelNo', 'No, thanks'); +} +export namespace DataScienceRendererExtension { + export const installingExtension = localize( + 'DataScienceRendererExtension.installingExtension', + 'Installing Notebook Renderers extension... ' + ); + export const installationCompleteMessage = localize( + 'DataScienceRendererExtension.installationCompleteMessage', + 'complete.' + ); + export const startingDownloadOutputMessage = localize( + 'DataScienceRendererExtension.startingDownloadOutputMessage', + 'Starting download of Notebook Renderers extension.' + ); + export const downloadingMessage = localize( + 'DataScienceRendererExtension.downloadingMessage', + 'Downloading Notebook Renderers Extension... ' + ); + export const downloadCompletedOutputMessage = localize( + 'DataScienceRendererExtension.downloadCompletedOutputMessage', + 'Notebook Renderers extension download complete.' + ); +} +export namespace DataScienceNotebookSurveyBanner { + export const bannerMessage = localize( + 'DataScienceNotebookSurveyBanner.bannerMessage', + 'Can you please take 2 minutes to tell us how the Preview Notebook Editor is working for you?' + ); +} + +export namespace Installer { + export const noCondaOrPipInstaller = localize( + 'Installer.noCondaOrPipInstaller', + 'There is no Conda or Pip installer available in the selected environment.' + ); + export const noPipInstaller = localize( + 'Installer.noPipInstaller', + 'There is no Pip installer available in the selected environment.' + ); + export const searchForHelp = localize('Installer.searchForHelp', 'Search for help'); +} + +export namespace ExtensionSurveyBanner { + export const bannerMessage = localize( + 'ExtensionSurveyBanner.bannerMessage', + 'Can you please take 2 minutes to tell us how the Python extension is working for you?' + ); + export const bannerLabelYes = localize('ExtensionSurveyBanner.bannerLabelYes', 'Yes, take survey now'); + export const bannerLabelNo = localize('ExtensionSurveyBanner.bannerLabelNo', 'No, thanks'); + export const maybeLater = localize('ExtensionSurveyBanner.maybeLater', 'Maybe later'); +} + +export namespace Products { + export const installingModule = localize('products.installingModule', 'Installing {0}'); +} + +export namespace DataScience { + export const unknownServerUri = localize( + 'DataScience.unknownServerUri', + 'Server URI cannot be used. Did you uninstall an extension that provided a Jupyter server connection?' + ); + export const uriProviderDescriptionFormat = localize( + 'DataScience.uriProviderDescriptionFormat', + '{0} (From {1} extension)' + ); + export const unknownPackage = localize('DataScience.unknownPackage', 'unknown'); + export const interactiveWindowTitle = localize('DataScience.interactiveWindowTitle', 'Python Interactive'); + export const interactiveWindowTitleFormat = localize( + 'DataScience.interactiveWindowTitleFormat', + 'Python Interactive - {0}' + ); + + export const interactiveWindowModeBannerTitle = localize( + 'DataScience.interactiveWindowModeBannerTitle', + 'Do you want to open a new Python Interactive window for this file? [More Information](command:workbench.action.openSettings?%5B%22python.dataScience.interactiveWindowMode%22%5D).' + ); + + export const interactiveWindowModeBannerSwitchYes = localize( + 'DataScience.interactiveWindowModeBannerSwitchYes', + 'Yes' + ); + export const interactiveWindowModeBannerSwitchAlways = localize( + 'DataScience.interactiveWindowModeBannerSwitchAlways', + 'Always' + ); + export const interactiveWindowModeBannerSwitchNo = localize( + 'DataScience.interactiveWindowModeBannerSwitchNo', + 'No' + ); + + export const dataExplorerTitle = localize('DataScience.dataExplorerTitle', 'Data Viewer'); + export const badWebPanelFormatString = localize( + 'DataScience.badWebPanelFormatString', + '

{0} is not a valid file name

' + ); + export const checkingIfImportIsSupported = localize( + 'DataScience.checkingIfImportIsSupported', + 'Checking if import is supported' + ); + export const installingMissingDependencies = localize( + 'DataScience.installingMissingDependencies', + 'Installing missing dependencies' + ); + export const performingExport = localize('DataScience.performingExport', 'Performing Export'); + export const convertingToPDF = localize('DataScience.convertingToPDF', 'Converting to PDF'); + export const exportNotebookToPython = localize( + 'DataScience.exportNotebookToPython', + 'Exporting Notebook to Python' + ); + export const sessionDisposed = localize( + 'DataScience.sessionDisposed', + 'Cannot execute code, session has been disposed.' + ); + export const passwordFailure = localize( + 'DataScience.passwordFailure', + 'Failed to connect to password protected server. Check that password is correct.' + ); + export const rawKernelProcessNotStarted = localize( + 'DataScience.rawKernelProcessNotStarted', + 'Raw kernel process was not able to start.' + ); + export const rawKernelProcessExitBeforeConnect = localize( + 'DataScience.rawKernelProcessExitBeforeConnect', + 'Raw kernel process exited before connecting.' + ); + export const unknownMimeTypeFormat = localize( + 'DataScience.unknownMimeTypeFormat', + 'Mime type {0} is not currently supported' + ); + export const exportDialogTitle = localize('DataScience.exportDialogTitle', 'Export to Jupyter Notebook'); + export const exportDialogFilter = localize('DataScience.exportDialogFilter', 'Jupyter Notebooks'); + export const exportDialogComplete = localize('DataScience.exportDialogComplete', 'Notebook written to {0}'); + export const exportDialogFailed = localize('DataScience.exportDialogFailed', 'Failed to export notebook. {0}'); + export const exportOpenQuestion = localize('DataScience.exportOpenQuestion', 'Open in browser'); + export const exportOpenQuestion1 = localize('DataScience.exportOpenQuestion1', 'Open in editor'); + export const runCellLensCommandTitle = localize('python.command.python.datascience.runcell.title', 'Run cell'); + export const importDialogTitle = localize('DataScience.importDialogTitle', 'Import Jupyter Notebook'); + export const importDialogFilter = localize('DataScience.importDialogFilter', 'Jupyter Notebooks'); + export const notebookCheckForImportTitle = localize( + 'DataScience.notebookCheckForImportTitle', + 'Do you want to import the Jupyter Notebook into Python code?' + ); + export const notebookCheckForImportYes = localize('DataScience.notebookCheckForImportYes', 'Import'); + export const notebookCheckForImportNo = localize('DataScience.notebookCheckForImportNo', 'Later'); + export const notebookCheckForImportDontAskAgain = localize( + 'DataScience.notebookCheckForImportDontAskAgain', + "Don't Ask Again" + ); + export const libraryNotInstalled = localize( + 'DataScience.libraryNotInstalled', + 'Data Science library {0} is not installed. Install?' + ); + export const couldNotInstallLibrary = localize( + 'DataScience.couldNotInstallLibrary', + 'Could not install {0}. If pip is not available, please use the package manager of your choice to manually install this library into your Python environment.' + ); + export const libraryRequiredToLaunchJupyterNotInstalled = localize( + 'DataScience.libraryRequiredToLaunchJupyterNotInstalled', + 'Data Science library {0} is not installed.' + ); + export const librariesRequiredToLaunchJupyterNotInstalled = localize( + 'DataScience.librariesRequiredToLaunchJupyterNotInstalled', + 'Data Science libraries {0} are not installed.' + ); + export const libraryRequiredToLaunchJupyterNotInstalledInterpreter = localize( + 'DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter', + '{0} requires {1} to be installed.' + ); + export const libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter = localize( + 'DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter', + '{0} requires {1} to be installed.' + ); + export const librariesRequiredToLaunchJupyterNotInstalledInterpreter = localize( + 'DataScience.librariesRequiredToLaunchJupyterNotInstalledInterpreter', + '{0} requires {1} to be installed.' + ); + export const selectJupyterInterpreter = localize( + 'DataScience.selectJupyterInterpreter', + 'Select an Interpreter to start Jupyter' + ); + export const jupyterInstall = localize('DataScience.jupyterInstall', 'Install'); + export const currentlySelectedJupyterInterpreterForPlaceholder = localize( + 'Datascience.currentlySelectedJupyterInterpreterForPlaceholder', + 'current: {0}' + ); + export const jupyterNotSupported = localize( + 'DataScience.jupyterNotSupported', + 'Jupyter cannot be started. Error attempting to locate jupyter: {0}' + ); + export const jupyterNotSupportedBecauseOfEnvironment = localize( + 'DataScience.jupyterNotSupportedBecauseOfEnvironment', + 'Activating {0} to run Jupyter failed with {1}' + ); + export const jupyterNbConvertNotSupported = localize( + 'DataScience.jupyterNbConvertNotSupported', + 'Jupyter nbconvert is not installed' + ); + export const jupyterLaunchTimedOut = localize( + 'DataScience.jupyterLaunchTimedOut', + 'The Jupyter notebook server failed to launch in time' + ); + export const jupyterLaunchNoURL = localize( + 'DataScience.jupyterLaunchNoURL', + 'Failed to find the URL of the launched Jupyter notebook server' + ); + export const jupyterSelfCertFail = localize( + 'DataScience.jupyterSelfCertFail', + 'The security certificate used by server {0} was not issued by a trusted certificate authority.\r\nThis may indicate an attempt to steal your information.\r\nDo you want to enable the Allow Unauthorized Remote Connection setting for this workspace to allow you to connect?' + ); + export const jupyterSelfCertEnable = localize('DataScience.jupyterSelfCertEnable', 'Yes, connect anyways'); + export const jupyterSelfCertClose = localize('DataScience.jupyterSelfCertClose', 'No, close the connection'); + export const pythonInteractiveHelpLink = localize( + 'DataScience.pythonInteractiveHelpLink', + 'See [https://aka.ms/pyaiinstall] for help on installing jupyter.' + ); + export const markdownHelpInstallingMissingDependencies = localize( + 'DataScience.markdownHelpInstallingMissingDependencies', + 'See [https://aka.ms/pyaiinstall](https://aka.ms/pyaiinstall) for help on installing Jupyter and related dependencies.' + ); + export const importingFormat = localize('DataScience.importingFormat', 'Importing {0}'); + export const startingJupyter = localize('DataScience.startingJupyter', 'Starting Jupyter server'); + export const connectingIPyKernel = localize('DataScience.connectingToIPyKernel', 'Connecting to IPython kernel'); + export const connectedToIPyKernel = localize('DataScience.connectedToIPyKernel', 'Connected.'); + export const connectingToJupyter = localize('DataScience.connectingToJupyter', 'Connecting to Jupyter server'); + export const exportingFormat = localize('DataScience.exportingFormat', 'Exporting {0}'); + export const runAllCellsLensCommandTitle = localize( + 'python.command.python.datascience.runallcells.title', + 'Run all cells' + ); + export const runAllCellsAboveLensCommandTitle = localize( + 'python.command.python.datascience.runallcellsabove.title', + 'Run above' + ); + export const runCellAndAllBelowLensCommandTitle = localize( + 'python.command.python.datascience.runcellandallbelow.title', + 'Run Below' + ); + export const importChangeDirectoryComment = localize( + 'DataScience.importChangeDirectoryComment', + '{0} Change working directory from the workspace root to the ipynb file location. Turn this addition off with the DataScience.changeDirOnImportExport setting' + ); + export const exportChangeDirectoryComment = localize( + 'DataScience.exportChangeDirectoryComment', + '# Change directory to VSCode workspace root so that relative path loads work correctly. Turn this addition off with the DataScience.changeDirOnImportExport setting' + ); + + export const restartKernelMessage = localize( + 'DataScience.restartKernelMessage', + 'Do you want to restart the Jupter kernel? All variables will be lost.' + ); + export const restartKernelMessageYes = localize('DataScience.restartKernelMessageYes', 'Restart'); + export const restartKernelMessageDontAskAgain = localize( + 'DataScience.restartKernelMessageDontAskAgain', + "Don't Ask Again" + ); + export const restartKernelMessageNo = localize('DataScience.restartKernelMessageNo', 'Cancel'); + export const restartingKernelStatus = localize('DataScience.restartingKernelStatus', 'Restarting IPython Kernel'); + export const restartingKernelFailed = localize( + 'DataScience.restartingKernelFailed', + 'Kernel restart failed. Jupyter server is hung. Please reload VS code.' + ); + export const interruptingKernelFailed = localize( + 'DataScience.interruptingKernelFailed', + 'Kernel interrupt failed. Jupyter server is hung. Please reload VS code.' + ); + export const sessionStartFailedWithKernel = localize( + 'DataScience.sessionStartFailedWithKernel', + "Failed to start a session for the Kernel '{0}'. \nView Jupyter [log](command:{1}) for further details." + ); + export const executingCode = localize('DataScience.executingCode', 'Executing Cell'); + export const collapseAll = localize('DataScience.collapseAll', 'Collapse all cell inputs'); + export const expandAll = localize('DataScience.expandAll', 'Expand all cell inputs'); + export const collapseSingle = localize('DataScience.collapseSingle', 'Collapse'); + export const expandSingle = localize('DataScience.expandSingle', 'Expand'); + export const exportKey = localize('DataScience.export', 'Export as Jupyter notebook'); + export const restartServer = localize('DataScience.restartServer', 'Restart IPython Kernel'); + export const undo = localize('DataScience.undo', 'Undo'); + export const redo = localize('DataScience.redo', 'Redo'); + export const save = localize('DataScience.save', 'Save file'); + export const clearAll = localize('DataScience.clearAll', 'Remove all cells'); + export const reloadRequired = localize( + 'DataScience.reloadRequired', + 'Please reload the window for new settings to take effect.' + ); + export const pythonVersionHeader = localize('DataScience.pythonVersionHeader', 'Python Version:'); + export const pythonRestartHeader = localize('DataScience.pythonRestartHeader', 'Restarted Kernel:'); + export const pythonNewHeader = localize('DataScience.pythonNewHeader', 'Started new kernel:'); + export const pythonConnectHeader = localize('DataScience.pythonConnectHeader', 'Connected to kernel:'); + + export const jupyterSelectURIPrompt = localize( + 'DataScience.jupyterSelectURIPrompt', + 'Enter the URI of the running Jupyter server' + ); + export const jupyterSelectURIQuickPickTitle = localize( + 'DataScience.jupyterSelectURIQuickPickTitle', + 'Pick how to connect to Jupyter' + ); + export const jupyterSelectURIQuickPickPlaceholder = localize( + 'DataScience.jupyterSelectURIQuickPickPlaceholder', + 'Choose an option' + ); + export const jupyterSelectURILocalLabel = localize('DataScience.jupyterSelectURILocalLabel', 'Default'); + export const jupyterSelectURILocalDetail = localize( + 'DataScience.jupyterSelectURILocalDetail', + 'VS Code will automatically start a server for you on the localhost' + ); + export const jupyterSelectURIMRUDetail = localize('DataScience.jupyterSelectURIMRUDetail', 'Last Connection: {0}'); + export const jupyterSelectURINewLabel = localize('DataScience.jupyterSelectURINewLabel', 'Existing'); + export const jupyterSelectURINewDetail = localize( + 'DataScience.jupyterSelectURINewDetail', + 'Specify the URI of an existing server' + ); + export const jupyterSelectURIInvalidURI = localize( + 'DataScience.jupyterSelectURIInvalidURI', + 'Invalid URI specified' + ); + export const jupyterSelectURIRunningDetailFormat = localize( + 'DataScience.jupyterSelectURIRunningDetailFormat', + 'Last activity {0}. {1} existing connections.' + ); + export const jupyterSelectURINotRunningDetail = localize( + 'DataScience.jupyterSelectURINotRunningDetail', + 'Cannot connect at this time. Status unknown.' + ); + export const jupyterSelectUserAndPasswordTitle = localize( + 'DataScience.jupyterSelectUserAndPasswordTitle', + 'Enter your user name and password to connect to Jupyter Hub' + ); + export const jupyterSelectUserPrompt = localize('DataScience.jupyterSelectUserPrompt', 'Enter your user name'); + export const jupyterSelectPasswordPrompt = localize( + 'DataScience.jupyterSelectPasswordPrompt', + 'Enter your password' + ); + export const jupyterNotebookFailure = localize( + 'DataScience.jupyterNotebookFailure', + 'Jupyter notebook failed to launch. \r\n{0}' + ); + export const jupyterNotebookConnectFailed = localize( + 'DataScience.jupyterNotebookConnectFailed', + 'Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}' + ); + export const reloadAfterChangingJupyterServerConnection = localize( + 'DataScience.reloadAfterChangingJupyterServerConnection', + 'Please reload VS Code when changing the Jupyter Server connection.' + ); + export const jupyterNotebookRemoteConnectFailed = localize( + 'DataScience.jupyterNotebookRemoteConnectFailed', + 'Failed to connect to remote Jupyter notebook.\r\nCheck that the Jupyter Server URI setting has a valid running server specified.\r\n{0}\r\n{1}' + ); + export const jupyterNotebookRemoteConnectSelfCertsFailed = localize( + 'DataScience.jupyterNotebookRemoteConnectSelfCertsFailed', + 'Failed to connect to remote Jupyter notebook.\r\nSpecified server is using self signed certs. Enable Allow Unauthorized Remote Connection setting to connect anyways\r\n{0}\r\n{1}' + ); + export const rawConnectionDisplayName = localize( + 'DataScience.rawConnectionDisplayName', + 'Direct kernel connection' + ); + export const rawConnectionBrokenError = localize( + 'DataScience.rawConnectionBrokenError', + 'Direct kernel connection broken' + ); + export const jupyterServerCrashed = localize( + 'DataScience.jupyterServerCrashed', + 'Jupyter server crashed. Unable to connect. \r\nError code from jupyter: {0}' + ); + export const notebookVersionFormat = localize('DataScience.notebookVersionFormat', 'Jupyter Notebook Version: {0}'); + export const jupyterKernelSpecNotFound = localize( + 'DataScience.jupyterKernelSpecNotFound', + 'Cannot create a IPython kernel spec and none are available for use' + ); + export const jupyterKernelSpecModuleNotFound = localize( + 'DataScience.jupyterKernelSpecModuleNotFound', + "'Kernelspec' module not installed in the selected interpreter ({0}).\n Please re-install or update 'jupyter'." + ); + export const interruptKernel = localize('DataScience.interruptKernel', 'Interrupt IPython Kernel'); + export const clearAllOutput = localize('DataScience.clearAllOutput', 'Clear All Output'); + export const interruptKernelStatus = localize('DataScience.interruptKernelStatus', 'Interrupting IPython Kernel'); + export const exportCancel = localize('DataScience.exportCancel', 'Cancel'); + export const exportPythonQuickPickLabel = localize('DataScience.exportPythonQuickPickLabel', 'Python Script'); + export const exportHTMLQuickPickLabel = localize('DataScience.exportHTMLQuickPickLabel', 'HTML'); + export const exportPDFQuickPickLabel = localize('DataScience.exportPDFQuickPickLabel', 'PDF'); + export const restartKernelAfterInterruptMessage = localize( + 'DataScience.restartKernelAfterInterruptMessage', + 'Interrupting the kernel timed out. Do you want to restart the kernel instead? All variables will be lost.' + ); + export const pythonInterruptFailedHeader = localize( + 'DataScience.pythonInterruptFailedHeader', + 'Keyboard interrupt crashed the kernel. Kernel restarted.' + ); + export const sysInfoURILabel = localize('DataScience.sysInfoURILabel', 'Jupyter Server URI: '); + export const executingCodeFailure = localize('DataScience.executingCodeFailure', 'Executing code failed : {0}'); + export const inputWatermark = localize('DataScience.inputWatermark', 'Type code here and press shift-enter to run'); + export const liveShareConnectFailure = localize( + 'DataScience.liveShareConnectFailure', + 'Cannot connect to host jupyter session. URI not found.' + ); + export const liveShareCannotSpawnNotebooks = localize( + 'DataScience.liveShareCannotSpawnNotebooks', + 'Spawning jupyter notebooks is not supported over a live share connection' + ); + export const liveShareCannotImportNotebooks = localize( + 'DataScience.liveShareCannotImportNotebooks', + 'Importing notebooks is not currently supported over a live share connection' + ); + export const liveShareHostFormat = localize('DataScience.liveShareHostFormat', '{0} Jupyter Server'); + export const liveShareSyncFailure = localize( + 'DataScience.liveShareSyncFailure', + 'Synchronization failure during live share startup.' + ); + export const liveShareServiceFailure = localize( + 'DataScience.liveShareServiceFailure', + "Failure starting '{0}' service during live share connection." + ); + export const documentMismatch = localize( + 'DataScience.documentMismatch', + 'Cannot run cells, duplicate documents for {0} found.' + ); + export const jupyterGetVariablesBadResults = localize( + 'DataScience.jupyterGetVariablesBadResults', + 'Failed to fetch variable info from the Jupyter server.' + ); + export const dataExplorerInvalidVariableFormat = localize( + 'DataScience.dataExplorerInvalidVariableFormat', + "'{0}' is not an active variable." + ); + export const pythonInteractiveCreateFailed = localize( + 'DataScience.pythonInteractiveCreateFailed', + "Failure to create a 'Python Interactive' window. Try reinstalling the Python extension." + ); + export const jupyterGetVariablesExecutionError = localize( + 'DataScience.jupyterGetVariablesExecutionError', + 'Failure during variable extraction: \r\n{0}' + ); + export const loadingMessage = localize('DataScience.loadingMessage', 'loading ...'); + export const fetchingDataViewer = localize('DataScience.fetchingDataViewer', 'Fetching data ...'); + export const noRowsInDataViewer = localize('DataScience.noRowsInDataViewer', 'No rows match current filter'); + export const jupyterServer = localize('DataScience.jupyterServer', 'Jupyter Server'); + export const notebookIsTrusted = localize('DataScience.notebookIsTrusted', 'Trusted'); + export const notebookIsNotTrusted = localize('DataScience.notebookIsNotTrusted', 'Not Trusted'); + export const noKernel = localize('DataScience.noKernel', 'No Kernel'); + export const serverNotStarted = localize('DataScience.serverNotStarted', 'Not Started'); + export const selectKernel = localize('DataScience.selectKernel', 'Select a Kernel'); + export const selectDifferentKernel = localize('DataScience.selectDifferentKernel', 'Select a different Kernel'); + export const selectDifferentJupyterInterpreter = localize( + 'DataScience.selectDifferentJupyterInterpreter', + 'Select a different Interpreter' + ); + export const localJupyterServer = localize('DataScience.localJupyterServer', 'local'); + export const pandasTooOldForViewingFormat = localize( + 'DataScience.pandasTooOldForViewingFormat', + "Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data." + ); + export const pandasRequiredForViewing = localize( + 'DataScience.pandasRequiredForViewing', + "Python package 'pandas' is required for viewing data." + ); + export const valuesColumn = localize('DataScience.valuesColumn', 'values'); + export const liveShareInvalid = localize( + 'DataScience.liveShareInvalid', + 'One or more guests in the session do not have the Python Extension installed. Live share session cannot continue.' + ); + export const tooManyColumnsMessage = localize( + 'DataScience.tooManyColumnsMessage', + 'Variables with over a 1000 columns may take a long time to display. Are you sure you wish to continue?' + ); + export const tooManyColumnsYes = localize('DataScience.tooManyColumnsYes', 'Yes'); + export const tooManyColumnsNo = localize('DataScience.tooManyColumnsNo', 'No'); + export const tooManyColumnsDontAskAgain = localize('DataScience.tooManyColumnsDontAskAgain', "Don't Ask Again"); + export const filterRowsButton = localize('DataScience.filterRowsButton', 'Filter Rows'); + export const filterRowsTooltip = localize( + 'DataScience.filterRowsTooltip', + 'Allows filtering multiple rows. Use =, >, or < signs to filter numeric values.' + ); + export const previewHeader = localize('DataScience.previewHeader', '--- Begin preview of {0} ---'); + export const previewFooter = localize('DataScience.previewFooter', '--- End preview of {0} ---'); + export const previewStatusMessage = localize('DataScience.previewStatusMessage', 'Generating preview of {0}'); + export const plotViewerTitle = localize('DataScience.plotViewerTitle', 'Plots'); + export const exportPlotTitle = localize('DataScience.exportPlotTitle', 'Save plot image'); + export const pdfFilter = localize('DataScience.pdfFilter', 'PDF'); + export const pngFilter = localize('DataScience.pngFilter', 'PNG'); + export const svgFilter = localize('DataScience.svgFilter', 'SVG'); + export const previousPlot = localize('DataScience.previousPlot', 'Previous'); + export const nextPlot = localize('DataScience.nextPlot', 'Next'); + export const panPlot = localize('DataScience.panPlot', 'Pan'); + export const zoomInPlot = localize('DataScience.zoomInPlot', 'Zoom in'); + export const zoomOutPlot = localize('DataScience.zoomOutPlot', 'Zoom out'); + export const exportPlot = localize('DataScience.exportPlot', 'Export to different formats'); + export const deletePlot = localize('DataScience.deletePlot', 'Remove'); + export const editSection = localize('DataScience.editSection', 'Input new cells here.'); + export const selectedImageListLabel = localize('DataScience.selectedImageListLabel', 'Selected Image'); + export const imageListLabel = localize('DataScience.imageListLabel', 'Image'); + export const exportImageFailed = localize('DataScience.exportImageFailed', 'Error exporting image: {0}'); + export const jupyterDataRateExceeded = localize( + 'DataScience.jupyterDataRateExceeded', + 'Cannot view variable because data rate exceeded. Please restart your server with a higher data rate limit. For example, --NotebookApp.iopub_data_rate_limit=10000000000.0' + ); + export const addCellBelowCommandTitle = localize('DataScience.addCellBelowCommandTitle', 'Add cell'); + export const debugCellCommandTitle = localize('DataScience.debugCellCommandTitle', 'Debug Cell'); + export const debugStepOverCommandTitle = localize('DataScience.debugStepOverCommandTitle', 'Step over'); + export const debugContinueCommandTitle = localize('DataScience.debugContinueCommandTitle', 'Continue'); + export const debugStopCommandTitle = localize('DataScience.debugStopCommandTitle', 'Stop'); + export const runCurrentCellAndAddBelow = localize( + 'DataScience.runCurrentCellAndAddBelow', + 'Run current and add cell below' + ); + export const variableExplorerDisabledDuringDebugging = localize( + 'DataScience.variableExplorerDisabledDuringDebugging', + "Please see the Debug Side Bar's VARIABLES section." + ); + export const jupyterDebuggerNotInstalledError = localize( + 'DataScience.jupyterDebuggerNotInstalledError', + 'Pip module {0} is required for debugging cells. You will need to install it to debug cells.' + ); + export const jupyterDebuggerOutputParseError = localize( + 'DataScience.jupyterDebuggerOutputParseError', + 'Unable to parse {0} output, please log an issue with https://github.com/microsoft/vscode-python' + ); + export const jupyterDebuggerPortNotAvailableError = localize( + 'DataScience.jupyterDebuggerPortNotAvailableError', + 'Port {0} cannot be opened for debugging. Please specify a different port in the remoteDebuggerPort setting.' + ); + export const jupyterDebuggerPortBlockedError = localize( + 'DataScience.jupyterDebuggerPortBlockedError', + 'Port {0} cannot be connected to for debugging. Please let port {0} through your firewall.' + ); + export const jupyterDebuggerPortNotAvailableSearchError = localize( + 'DataScience.jupyterDebuggerPortNotAvailableSearchError', + 'Ports in the range {0}-{1} cannot be found for debugging. Please specify a port in the remoteDebuggerPort setting.' + ); + export const jupyterDebuggerPortBlockedSearchError = localize( + 'DataScience.jupyterDebuggerPortBlockedSearchError', + 'A port cannot be connected to for debugging. Please let ports {0}-{1} through your firewall.' + ); + export const jupyterDebuggerInstallNew = localize( + 'DataScience.jupyterDebuggerInstallNew', + 'Pip module {0} is required for debugging cells. Install {0} and continue to debug cell?' + ); + export const jupyterDebuggerInstallNewRunByLine = localize( + 'DataScience.jupyterDebuggerInstallNewRunByLine', + 'Pip module {0} is required for running by line. Install {0} and continue to run by line?' + ); + export const jupyterDebuggerInstallUpdate = localize( + 'DataScience.jupyterDebuggerInstallUpdate', + 'The version of {0} installed does not support debugging cells. Update {0} to newest version and continue to debug cell?' + ); + export const jupyterDebuggerInstallUpdateRunByLine = localize( + 'DataScience.jupyterDebuggerInstallUpdateRunByLine', + 'The version of {0} installed does not support running by line. Update {0} to newest version and continue to run by line?' + ); + export const jupyterDebuggerInstallYes = localize('DataScience.jupyterDebuggerInstallYes', 'Yes'); + export const jupyterDebuggerInstallNo = localize('DataScience.jupyterDebuggerInstallNo', 'No'); + export const cellStopOnErrorFormatMessage = localize( + 'DataScience.cellStopOnErrorFormatMessage', + '{0} cells were canceled due to an error in the previous cell.' + ); + export const scrollToCellTitleFormatMessage = localize('DataScience.scrollToCellTitleFormatMessage', 'Go to [{0}]'); + export const instructionComments = localize( + 'DataScience.instructionComments', + '# To add a new cell, type "{0}"\n# To add a new markdown cell, type "{0} [markdown]"\n' + ); + export const invalidNotebookFileError = localize( + 'DataScience.invalidNotebookFileError', + 'Notebook is not in the correct format. Check the file for correct json.' + ); + export const invalidNotebookFileErrorFormat = localize( + 'DataScience.invalidNotebookFileError', + '{0} is not a valid notebook file. Check the file for correct json.' + ); + export const nativeEditorTitle = localize('DataScience.nativeEditorTitle', 'Notebook Editor'); + export const untitledNotebookFileName = localize('DataScience.untitledNotebookFileName', 'Untitled'); + export const dirtyNotebookMessage1 = localize( + 'DataScience.dirtyNotebookMessage1', + 'Do you want to save the changes you made to {0}?' + ); + export const dirtyNotebookMessage2 = localize( + 'DataScience.dirtyNotebookMessage2', + "Your changes will be lost if you don't save them." + ); + export const dirtyNotebookYes = localize('DataScience.dirtyNotebookYes', 'Save'); + export const dirtyNotebookNo = localize('DataScience.dirtyNotebookNo', "Don't Save"); + export const dirtyNotebookCancel = localize('DataScience.dirtyNotebookCancel', 'Cancel'); + export const dirtyNotebookDialogTitle = localize('DataScience.dirtyNotebookDialogTitle', 'Save'); + export const dirtyNotebookDialogFilter = localize('DataScience.dirtyNotebookDialogFilter', 'Jupyter Notebooks'); + export const remoteDebuggerNotSupported = localize( + 'DataScience.remoteDebuggerNotSupported', + 'Debugging while attached to a remote server is not currently supported.' + ); + export const notebookExportAs = localize('DataScience.notebookExportAs', 'Export As'); + export const exportAsPythonFileTitle = localize('DataScience.exportAsPythonFileTitle', 'Save As Python File'); + export const exportAsQuickPickPlaceholder = localize('DataScience.exportAsQuickPickPlaceholder', 'Export As...'); + export const openExportedFileMessage = localize( + 'DataScience.openExportedFileMessage', + 'Would you like to open the exported file?' + ); + export const openExportFileYes = localize('DataScience.openExportFileYes', 'Yes'); + export const openExportFileNo = localize('DataScience.openExportFileNo', 'No'); + export const exportToPDFDependencyMessage = localize( + 'DataScience.exportToPDFDependencyMessage', + 'If you have not installed xelatex (TeX) you will need to do so before you can export to PDF, for further instructions please look [here](https://nbconvert.readthedocs.io/en/latest/install.html#installing-tex). \r\nTo avoid installing xelatex (TeX) you might want to try exporting to HTML and using your browsers "Print to PDF" feature.' + ); + export const failedExportMessage = localize('DataScience.failedExportMessage', 'Export failed.'); + export const runCell = localize('DataScience.runCell', 'Run cell'); + export const deleteCell = localize('DataScience.deleteCell', 'Delete cell'); + export const moveCellUp = localize('DataScience.moveCellUp', 'Move cell up'); + export const moveCellDown = localize('DataScience.moveCellDown', 'Move cell down'); + export const moveSelectedCellUp = localize('DataScience.moveSelectedCellUp', 'Move selected cell up'); + export const moveSelectedCellDown = localize('DataScience.deleteCell', 'Move selected cell down'); + export const insertBelow = localize('DataScience.insertBelow', 'Insert cell below'); + export const insertAbove = localize('DataScience.insertAbove', 'Insert cell above'); + export const addCell = localize('DataScience.addCell', 'Add cell'); + export const runAll = localize('DataScience.runAll', 'Insert cell'); + export const convertingToPythonFile = localize( + 'DataScience.convertingToPythonFile', + 'Converting ipynb to python file' + ); + export const noInterpreter = localize('DataScience.noInterpreter', 'No python selected'); + export const notebookNotFound = localize( + 'DataScience.notebookNotFound', + 'python -m jupyter notebook --version is not running' + ); + export const findJupyterCommandProgress = localize( + 'DataScience.findJupyterCommandProgress', + 'Active interpreter does not support {0}. Searching for the best available interpreter.' + ); + export const findJupyterCommandProgressCheckInterpreter = localize( + 'DataScience.findJupyterCommandProgressCheckInterpreter', + 'Checking {0}.' + ); + export const findJupyterCommandProgressSearchCurrentPath = localize( + 'DataScience.findJupyterCommandProgressSearchCurrentPath', + 'Searching current path.' + ); + export const gatherError = localize('DataScience.gatherError', 'Gather internal error'); + export const gatheredScriptDescription = localize( + 'DataScience.gatheredScriptDescription', + '# This file was generated by the Gather Extension.\n# It requires version 2020.7.94776 (or newer) of the Python Extension.\n#\n# The intent is that it contains only the code required to produce\n# the same results as the cell originally selected for gathering.\n# Please note that the Python analysis is quite conservative, so if\n# it is unsure whether a line of code is necessary for execution, it\n# will err on the side of including it.\n#\n# Please let us know if you are satisfied with what was gathered here:\n# https://aka.ms/gatherfeedback\n\n' + ); + export const gatheredNotebookDescriptionInMarkdown = localize( + 'DataScience.gatheredNotebookDescriptionInMarkdown', + '# Gathered Notebook\nGathered from ```{0}```\n\n| | |\n|---|---|\n|   |This notebook was generated by the Gather Extension. It requires version 2020.7.94776 (or newer) of the Python Extension, please update [here](https://command:python.datascience.latestExtension). The intent is that it contains only the code and cells required to produce the same results as the cell originally selected for gathering. Please note that the Python analysis is quite conservative, so if it is unsure whether a line of code is necessary for execution, it will err on the side of including it.|\n\n**Are you satisfied with the code that was gathered?**\n\n[Yes](https://command:python.datascience.gatherquality?yes) [No](https://command:python.datascience.gatherquality?no)' + ); + export const savePngTitle = localize('DataScience.savePngTitle', 'Save Image'); + export const fallbackToUseActiveInterpreterAsKernel = localize( + 'DataScience.fallbackToUseActiveInterpeterAsKernel', + "Couldn't find kernel '{0}' that the notebook was created with. Using the current interpreter." + ); + export const fallBackToRegisterAndUseActiveInterpeterAsKernel = localize( + 'DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel', + "Couldn't find kernel '{0}' that the notebook was created with. Registering a new kernel using the current interpreter." + ); + export const fallBackToPromptToUseActiveInterpreterOrSelectAKernel = localize( + 'DataScience.fallBackToPromptToUseActiveInterpreterOrSelectAKernel', + "Couldn't find kernel '{0}' that the notebook was created with." + ); + export const startingJupyterLogMessage = localize( + 'DataScience.startingJupyterLogMessage', + 'Starting Jupyter from {0}' + ); + export const jupyterStartTimedout = localize( + 'DataScience.jupyterStartTimedout', + "Starting Jupyter has timedout. Please check the 'Jupyter' output panel for further details." + ); + export const switchingKernelProgress = localize('DataScience.switchingKernelProgress', "Switching Kernel to '{0}'"); + export const waitingForJupyterSessionToBeIdle = localize( + 'DataScience.waitingForJupyterSessionToBeIdle', + 'Waiting for Jupyter Session to be idle' + ); + export const gettingListOfKernelsForLocalConnection = localize( + 'DataScience.gettingListOfKernelsForLocalConnection', + 'Fetching Kernels' + ); + export const gettingListOfKernelsForRemoteConnection = localize( + 'DataScience.gettingListOfKernelsForRemoteConnection', + 'Fetching Kernels' + ); + export const gettingListOfKernelSpecs = localize('DataScience.gettingListOfKernelSpecs', 'Fetching Kernel specs'); + export const startingJupyterNotebook = localize('DataScience.startingJupyterNotebook', 'Starting Jupyter Notebook'); + export const registeringKernel = localize('DataScience.registeringKernel', 'Registering Kernel'); + export const trimmedOutput = localize( + 'DataScience.trimmedOutput', + 'Output was trimmed for performance reasons.\nTo see the full output set the setting "python.dataScience.textOutputLimit" to 0.' + ); + export const jupyterCommandLineDefaultLabel = localize('DataScience.jupyterCommandLineDefaultLabel', 'Default'); + export const jupyterCommandLineDefaultDetail = localize( + 'DataScience.jupyterCommandLineDefaultDetail', + 'The Python extension will determine the appropriate command line for Jupyter' + ); + export const jupyterCommandLineCustomLabel = localize('DataScience.jupyterCommandLineCustomLabel', 'Custom'); + export const jupyterCommandLineCustomDetail = localize( + 'DataScience.jupyterCommandLineCustomDetail', + 'Customize the command line passed to Jupyter on startup' + ); + export const jupyterCommandLineReloadQuestion = localize( + 'DataScience.jupyterCommandLineReloadQuestion', + 'Please reload the window when changing the Jupyter command line.' + ); + export const jupyterCommandLineReloadAnswer = localize('DataScience.jupyterCommandLineReloadAnswer', 'Reload'); + export const jupyterCommandLineQuickPickPlaceholder = localize( + 'DataScience.jupyterCommandLineQuickPickPlaceholder', + 'Choose an option' + ); + export const jupyterCommandLineQuickPickTitle = localize( + 'DataScience.jupyterCommandLineQuickPickTitle', + 'Pick command line for Jupyter' + ); + export const jupyterCommandLinePrompt = localize( + 'DataScience.jupyterCommandLinePrompt', + 'Enter your custom command line for Jupyter' + ); + + export const connectingToJupyterUri = localize( + 'DataScience.connectingToJupyterUri', + 'Connecting to Jupyter server at {0}' + ); + export const createdNewNotebook = localize('DataScience.createdNewNotebook', '{0}: Creating new notebook '); + + export const createdNewKernel = localize('DataScience.createdNewKernel', '{0}: Kernel started: {1}'); + export const kernelInvalid = localize( + 'DataScience.kernelInvalid', + 'Kernel {0} is not usable. Check the Jupyter output tab for more information.' + ); + + export const nativeDependencyFail = localize( + 'DataScience.nativeDependencyFail', + '{0}. We cannot launch a jupyter server for you because your OS is not supported. Select an already running server if you wish to continue.' + ); + + export const selectNewServer = localize('DataScience.selectNewServer', 'Pick Running Server'); + export const jupyterSelectURIRemoteLabel = localize('DataScience.jupyterSelectURIRemoteLabel', 'Existing'); + export const jupyterSelectURIQuickPickTitleRemoteOnly = localize( + 'DataScience.jupyterSelectURIQuickPickTitleRemoteOnly', + 'Pick an already running jupyter server' + ); + export const jupyterSelectURIRemoteDetail = localize( + 'DataScience.jupyterSelectURIRemoteDetail', + 'Specify the URI of an existing server' + ); + + export const loadClassFailedWithNoInternet = localize( + 'DataScience.loadClassFailedWithNoInternet', + 'Error loading {0}:{1}. Internet connection required for loading 3rd party widgets.' + ); + export const loadThirdPartyWidgetScriptsPostEnabled = localize( + 'DataScience.loadThirdPartyWidgetScriptsPostEnabled', + "Please restart the Kernel when changing the setting 'python.dataScience.widgetScriptSources'." + ); + export const useCDNForWidgets = localize( + 'DataScience.useCDNForWidgets', + 'Widgets require us to download supporting files from a 3rd party website. Click [here](https://aka.ms/PVSCIPyWidgets) for more information.' + ); + export const enableCDNForWidgetsSetting = localize( + 'DataScience.enableCDNForWidgetsSetting', + "Widgets require us to download supporting files from a 3rd party website. Click
here to enable this or click here for more information. (Error loading {0}:{1})." + ); + + export const unhandledMessage = localize( + 'DataScience.unhandledMessage', + 'Unhandled kernel message from a widget: {0} : {1}' + ); + + export const widgetScriptNotFoundOnCDNWidgetMightNotWork = localize( + 'DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork', + "Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected." + ); + export const qgridWidgetScriptVersionCompatibilityWarning = localize( + 'DataScience.qgridWidgetScriptVersionCompatibilityWarning', + "Unable to load a compatible version of the widget 'qgrid'. Consider downgrading to version 1.1.1." + ); + + export const kernelStarted = localize('DataScience.kernelStarted', 'Started kernel {0}.'); + export const runByLine = localize('DataScience.runByLine', 'Run by line (F10)'); + export const step = localize('DataScience.step', 'Run next line (F10)'); + export const stopRunByLine = localize('DataScience.stopRunByLine', 'Stop'); + export const rawKernelSessionFailed = localize( + 'DataScience.rawKernelSessionFailed', + 'Unable to start session for kernel {0}. Select another kernel to launch with.' + ); + export const rawKernelConnectingSession = localize( + 'DataScience.rawKernelConnectingSession', + 'Connecting to kernel.' + ); + + export const reloadCustomEditor = localize( + 'DataScience.reloadCustomEditor', + 'Please reload VS Code to use the custom editor API' + ); + export const reloadVSCodeNotebookEditor = localize( + 'DataScience.reloadVSCodeNotebookEditor', + 'Please reload VS Code to use the Notebook Editor' + ); + export const usingPreviewNotebookWithOtherNotebookWarning = localize( + 'DataScience.usingPreviewNotebookWithOtherNotebookWarning', + 'Opening the same file in the Preview Notebook Editor and stable Notebook Editor is not recommended. Doing so could result in data loss or corruption of notebooks.' + ); + export const launchNotebookTrustPrompt = localize( + 'DataScience.launchNotebookTrustPrompt', + 'A notebook could execute harmful code when opened. Some outputs have been hidden. Do you trust this notebook? [Learn more.](https://aka.ms/trusted-notebooks)' + ); + export const trustNotebook = localize('DataScience.launchNotebookTrustPrompt.yes', 'Trust'); + export const doNotTrustNotebook = localize('DataScience.launchNotebookTrustPrompt.no', 'Do not trust'); + export const trustAllNotebooks = localize( + 'DataScience.launchNotebookTrustPrompt.trustAllNotebooks', + 'Trust all notebooks' + ); + export const insecureSessionMessage = localize( + 'DataScience.insecureSessionMessage', + 'Connecting over HTTP without a token may be an insecure connection. Do you want to connect to a possibly insecure server?' + ); + export const insecureSessionDenied = localize( + 'DataScience.insecureSessionDenied', + 'Denied connection to insecure server.' + ); + export const previewNotebookOnlySupportedInVSCInsiders = localize( + 'DataScience.previewNotebookOnlySupportedInVSCInsiders', + 'The Preview Notebook Editor is supported only in the Insiders version of Visual Studio Code.' + ); + export const connected = localize('DataScience.connected', 'Connected'); + export const disconnected = localize('DataScience.disconnected', 'Disconnected'); +} + +export namespace StartPage { + export const getStarted = localize('StartPage.getStarted', 'Python - Get Started'); + export const pythonExtensionTitle = localize('StartPage.pythonExtensionTitle', 'Python Extension'); + export const createJupyterNotebook = localize('StartPage.createJupyterNotebook', 'Create a Jupyter Notebook'); + export const notebookDescription = localize( + 'StartPage.notebookDescription', + '- Run "" in the Command Palette (
Shift + Command + P
)
- Explore our to learn about notebook features' + ); + export const createAPythonFile = localize('StartPage.createAPythonFile', 'Create a Python File'); + export const pythonFileDescription = localize( + 'StartPage.pythonFileDescription', + '- Create a with a .py extension' + ); + export const openInteractiveWindow = localize( + 'StartPage.openInteractiveWindow', + 'Use the Interactive Window to develop Python Scripts' + ); + export const interactiveWindowDesc = localize( + 'StartPage.interactiveWindowDesc', + '- You can create cells on a Python file by typing "#%%"
- Use "
Shift + Enter
" to run a cell, the output will be shown in the interactive window' + ); + + export const releaseNotes = localize( + 'StartPage.releaseNotes', + 'Take a look at our Release Notes to learn more about the latest features.' + ); + export const tutorialAndDoc = localize( + 'StartPage.tutorialAndDoc', + 'Explore more features in our Tutorials or check Documentation for tips and troubleshooting.' + ); + export const dontShowAgain = localize('StartPage.dontShowAgain', "Don't show this page again"); + export const helloWorld = localize('StartPage.helloWorld', 'Hello world'); + // When localizing sampleNotebook, the translated notebook must also be included in + // pythonFiles\* + export const sampleNotebook = localize('StartPage.sampleNotebook', 'Notebooks intro'); + export const openFolder = localize('StartPage.openFolder', 'Open a Folder or Workspace'); + export const folderDesc = localize( + 'StartPage.folderDesc', + '- Open a
- Open a ' + ); +} + +export namespace DebugConfigStrings { + export const selectConfiguration = { + title: localize('debug.selectConfigurationTitle'), + placeholder: localize('debug.selectConfigurationPlaceholder') + }; + export const launchJsonCompletions = { + label: localize('debug.launchJsonConfigurationsCompletionLabel'), + description: localize('debug.launchJsonConfigurationsCompletionDescription') + }; + + export namespace file { + export const snippet = { + name: localize('python.snippet.launch.standard.label') + }; + // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { + label: localize('debug.debugFileConfigurationLabel'), + description: localize('debug.debugFileConfigurationDescription') + }; + } + export namespace module { + export const snippet = { + name: localize('python.snippet.launch.module.label'), + default: localize('python.snippet.launch.module.default') + }; + // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { + label: localize('debug.debugModuleConfigurationLabel'), + description: localize('debug.debugModuleConfigurationDescription') + }; + export const enterModule = { + title: localize('debug.moduleEnterModuleTitle'), + prompt: localize('debug.moduleEnterModulePrompt'), + default: localize('debug.moduleEnterModuleDefault'), + invalid: localize('debug.moduleEnterModuleInvalidNameError') + }; + } + export namespace attach { + export const snippet = { + name: localize('python.snippet.launch.attach.label') + }; + // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { + label: localize('debug.remoteAttachConfigurationLabel'), + description: localize('debug.remoteAttachConfigurationDescription') + }; + export const enterRemoteHost = { + title: localize('debug.attachRemoteHostTitle'), + prompt: localize('debug.attachRemoteHostPrompt'), + invalid: localize('debug.attachRemoteHostValidationError') + }; + export const enterRemotePort = { + title: localize('debug.attachRemotePortTitle'), + prompt: localize('debug.attachRemotePortPrompt'), + invalid: localize('debug.attachRemotePortValidationError') + }; + } + export namespace attachPid { + export const snippet = { + name: localize('python.snippet.launch.attachpid.label') + }; + // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { + label: localize('debug.attachPidConfigurationLabel'), + description: localize('debug.attachPidConfigurationDescription') + }; + } + export namespace django { + export const snippet = { + name: localize('python.snippet.launch.django.label') + }; + // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { + label: localize('debug.debugDjangoConfigurationLabel'), + description: localize('debug.debugDjangoConfigurationDescription') + }; + export const enterManagePyPath = { + title: localize('debug.djangoEnterManagePyPathTitle'), + prompt: localize('debug.djangoEnterManagePyPathPrompt'), + invalid: localize('debug.djangoEnterManagePyPathInvalidFilePathError') + }; + } + export namespace flask { + export const snippet = { + name: localize('python.snippet.launch.flask.label') + }; + // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { + label: localize('debug.debugFlaskConfigurationLabel'), + description: localize('debug.debugFlaskConfigurationDescription') + }; + export const enterAppPathOrNamePath = { + title: localize('debug.flaskEnterAppPathOrNamePathTitle'), + prompt: localize('debug.flaskEnterAppPathOrNamePathPrompt'), + invalid: localize('debug.flaskEnterAppPathOrNamePathInvalidNameError') + }; + } + export namespace pyramid { + export const snippet = { + name: localize('python.snippet.launch.pyramid.label') + }; + // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { + label: localize('debug.debugPyramidConfigurationLabel'), + description: localize('debug.debugPyramidConfigurationDescription') + }; + export const enterDevelopmentIniPath = { + title: localize('debug.pyramidEnterDevelopmentIniPathTitle'), + prompt: localize('debug.pyramidEnterDevelopmentIniPathPrompt'), + invalid: localize('debug.pyramidEnterDevelopmentIniPathInvalidFilePathError') + }; + } +} + +export namespace Testing { + export const testErrorDiagnosticMessage = localize('Testing.testErrorDiagnosticMessage', 'Error'); + export const testFailDiagnosticMessage = localize('Testing.testFailDiagnosticMessage', 'Fail'); + export const testSkippedDiagnosticMessage = localize('Testing.testSkippedDiagnosticMessage', 'Skipped'); + export const configureTests = localize('Testing.configureTests', 'Configure Test Framework'); + export const disableTests = localize('Testing.disableTests', 'Disable Tests'); +} + +export namespace OutdatedDebugger { + export const outdatedDebuggerMessage = localize( + 'OutdatedDebugger.updateDebuggerMessage', + 'We noticed you are attaching to ptvsd (Python debugger), which was deprecated on May 1st, 2020. Please switch to [debugpy](https://aka.ms/migrateToDebugpy).' + ); +} + +// Skip using vscode-nls and instead just compute our strings based on key values. Key values +// can be loaded out of the nls..json files +let loadedCollection: Record | undefined; +let defaultCollection: Record | undefined; +let askedForCollection: Record = {}; +let loadedLocale: string; + +// This is exported only for testing purposes. +export function _resetCollections() { + loadedLocale = ''; + loadedCollection = undefined; + askedForCollection = {}; +} + +// This is exported only for testing purposes. +export function _getAskedForCollection() { + return askedForCollection; +} + +// Return the effective set of all localization strings, by key. +// +// This should not be used for direct lookup. +export function getCollectionJSON(): string { + // Load the current collection + if (!loadedCollection || parseLocale() !== loadedLocale) { + load(); + } + + // Combine the default and loaded collections + return JSON.stringify({ ...defaultCollection, ...loadedCollection }); +} + +// tslint:disable-next-line:no-suspicious-comment +export function localize(key: string, defValue?: string) { + // Return a pointer to function so that we refetch it on each call. + return () => { + return getString(key, defValue); + }; +} + +function parseLocale(): string { + // Attempt to load from the vscode locale. If not there, use english + const vscodeConfigString = process.env.VSCODE_NLS_CONFIG; + return vscodeConfigString ? JSON.parse(vscodeConfigString).locale : 'en-us'; +} + +function getString(key: string, defValue?: string) { + // Load the current collection + if (!loadedCollection || parseLocale() !== loadedLocale) { + load(); + } + + // The default collection (package.nls.json) is the fallback. + // Note that we are guaranteed the following (during shipping) + // 1. defaultCollection was initialized by the load() call above + // 2. defaultCollection has the key (see the "keys exist" test) + let collection = defaultCollection!; + + // Use the current locale if the key is defined there. + if (loadedCollection && loadedCollection.hasOwnProperty(key)) { + collection = loadedCollection; + } + let result = collection[key]; + if (!result && defValue) { + // This can happen during development if you haven't fixed up the nls file yet or + // if for some reason somebody broke the functional test. + result = defValue; + } + askedForCollection[key] = result; + + return result; +} + +function load() { + const fs = new FileSystem(); + + // Figure out our current locale. + loadedLocale = parseLocale(); + + // Find the nls file that matches (if there is one) + const nlsFile = path.join(EXTENSION_ROOT_DIR, `package.nls.${loadedLocale}.json`); + if (fs.fileExistsSync(nlsFile)) { + const contents = fs.readFileSync(nlsFile); + loadedCollection = JSON.parse(contents); + } else { + // If there isn't one, at least remember that we looked so we don't try to load a second time + loadedCollection = {}; + } + + // Get the default collection if necessary. Strings may be in the default or the locale json + if (!defaultCollection) { + const defaultNlsFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); + if (fs.fileExistsSync(defaultNlsFile)) { + const contents = fs.readFileSync(defaultNlsFile); + defaultCollection = JSON.parse(contents); + } else { + defaultCollection = {}; + } + } +} + +// Default to loading the current locale +load(); diff --git a/src/client/common/utils/logging.ts b/src/client/common/utils/logging.ts new file mode 100644 index 000000000000..c9c2f756c094 --- /dev/null +++ b/src/client/common/utils/logging.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export function formatErrorForLogging(error: Error | string): string { + let message: string = ''; + if (typeof error === 'string') { + message = error; + } else { + if (error.message) { + message = `Error Message: ${error.message}`; + } + if (error.name && error.message.indexOf(error.name) === -1) { + message += `, (${error.name})`; + } + // tslint:disable-next-line:no-any + const innerException = (error as any).innerException; + if (innerException && (innerException.message || innerException.name)) { + if (innerException.message) { + message += `, Inner Error Message: ${innerException.message}`; + } + if (innerException.name && innerException.message.indexOf(innerException.name) === -1) { + message += `, (${innerException.name})`; + } + } + } + return message; +} diff --git a/src/client/common/utils/misc.ts b/src/client/common/utils/misc.ts new file mode 100644 index 000000000000..8c6783b78c9f --- /dev/null +++ b/src/client/common/utils/misc.ts @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { TextDocument, Uri } from 'vscode'; +import { NotebookCellScheme } from '../constants'; +import { InterpreterUri } from '../installer/types'; +import { IAsyncDisposable, IDisposable, Resource } from '../types'; +import { isPromise } from './async'; +import { StopWatch } from './stopWatch'; + +// tslint:disable-next-line:no-empty +export function noop() {} + +/** + * Execute a block of code ignoring any exceptions. + */ +export function swallowExceptions(cb: Function) { + try { + cb(); + } catch { + // Ignore errors. + } +} + +export function using(disposable: T, func: (obj: T) => void) { + try { + func(disposable); + } finally { + disposable.dispose(); + } +} + +export async function usingAsync( + disposable: T, + func: (obj: T) => Promise +): Promise { + try { + return await func(disposable); + } finally { + await disposable.dispose(); + } +} + +/** + * Like `Readonly<>`, but recursive. + * + * See https://github.com/Microsoft/TypeScript/pull/21316. + */ +// tslint:disable-next-line:no-any +export type DeepReadonly = T extends any[] ? IDeepReadonlyArray : DeepReadonlyNonArray; +type DeepReadonlyNonArray = T extends object ? DeepReadonlyObject : T; +interface IDeepReadonlyArray extends ReadonlyArray> {} +type DeepReadonlyObject = { + readonly [P in NonFunctionPropertyNames]: DeepReadonly; +}; +type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]; + +// Information about a traced function/method call. +export type TraceInfo = { + elapsed: number; // milliseconds + // Either returnValue or err will be set. + // tslint:disable-next-line:no-any + returnValue?: any; + err?: Error; +}; + +// Call run(), call log() with the trace info, and return the result. +export function tracing(log: (t: TraceInfo) => void, run: () => T): T { + const timer = new StopWatch(); + try { + // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any + const result = run(); + + // If method being wrapped returns a promise then wait for it. + if (isPromise(result)) { + // tslint:disable-next-line:prefer-type-cast + (result as Promise) + .then((data) => { + log({ elapsed: timer.elapsedTime, returnValue: data }); + return data; + }) + .catch((ex) => { + log({ elapsed: timer.elapsedTime, err: ex }); + // tslint:disable-next-line:no-suspicious-comment + // TODO(GH-11645) Re-throw the error like we do + // in the non-Promise case. + }); + } else { + log({ elapsed: timer.elapsedTime, returnValue: result }); + } + return result; + } catch (ex) { + log({ elapsed: timer.elapsedTime, err: ex }); + throw ex; + } +} + +/** + * Checking whether something is a Resource (Uri/undefined). + * Using `instanceof Uri` doesn't always work as the object is not an instance of Uri (at least not in tests). + * That's why VSC too has a helper method `URI.isUri` (though not public). + * + * @export + * @param {InterpreterUri} [resource] + * @returns {resource is Resource} + */ +export function isResource(resource?: InterpreterUri): resource is Resource { + if (!resource) { + return true; + } + const uri = resource as Uri; + return typeof uri.path === 'string' && typeof uri.scheme === 'string'; +} + +/** + * Checking whether something is a Uri. + * Using `instanceof Uri` doesn't always work as the object is not an instance of Uri (at least not in tests). + * That's why VSC too has a helper method `URI.isUri` (though not public). + * + * @export + * @param {InterpreterUri} [resource] + * @returns {resource is Uri} + */ +// tslint:disable-next-line: no-any +export function isUri(resource?: Uri | any): resource is Uri { + if (!resource) { + return false; + } + const uri = resource as Uri; + return typeof uri.path === 'string' && typeof uri.scheme === 'string'; +} + +export function isNotebookCell(documentOrUri: TextDocument | Uri): boolean { + const uri = isUri(documentOrUri) ? documentOrUri : documentOrUri.uri; + return uri.scheme.includes(NotebookCellScheme); +} + +export function isUntitledFile(file?: Uri) { + return file?.scheme === 'untitled'; +} diff --git a/src/client/common/utils/multiStepInput.ts b/src/client/common/utils/multiStepInput.ts new file mode 100644 index 000000000000..03e68155e86a --- /dev/null +++ b/src/client/common/utils/multiStepInput.ts @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any no-unnecessary-class + +import { inject, injectable } from 'inversify'; +import { Disposable, QuickInput, QuickInputButton, QuickInputButtons, QuickPickItem } from 'vscode'; +import { IApplicationShell } from '../application/types'; + +// Borrowed from https://github.com/Microsoft/vscode-extension-samples/blob/master/quickinput-sample/src/multiStepInput.ts +// Why re-invent the wheel :) + +export class InputFlowAction { + public static back = new InputFlowAction(); + public static cancel = new InputFlowAction(); + public static resume = new InputFlowAction(); + private constructor() {} +} + +export type InputStep = (input: MultiStepInput, state: T) => Promise | void>; + +export interface IQuickPickParameters { + title?: string; + step?: number; + totalSteps?: number; + canGoBack?: boolean; + items: T[]; + activeItem?: T; + placeholder: string; + buttons?: QuickInputButton[]; + matchOnDescription?: boolean; + matchOnDetail?: boolean; + acceptFilterBoxTextAsSelection?: boolean; + shouldResume?(): Promise; +} + +// tslint:disable-next-line: interface-name +export interface InputBoxParameters { + title: string; + password?: boolean; + step?: number; + totalSteps?: number; + value: string; + prompt: string; + buttons?: QuickInputButton[]; + validate(value: string): Promise; + shouldResume?(): Promise; +} + +type MultiStepInputQuickPicResponseType = T | (P extends { buttons: (infer I)[] } ? I : never); +type MultiStepInputInputBoxResponseType

= string | (P extends { buttons: (infer I)[] } ? I : never); +export interface IMultiStepInput { + run(start: InputStep, state: S): Promise; + showQuickPick>({ + title, + step, + totalSteps, + items, + activeItem, + placeholder, + buttons, + shouldResume + }: P): Promise>; + showInputBox

({ + title, + step, + totalSteps, + value, + prompt, + validate, + buttons, + shouldResume + }: P): Promise>; +} + +export class MultiStepInput implements IMultiStepInput { + private current?: QuickInput; + private steps: InputStep[] = []; + constructor(private readonly shell: IApplicationShell) {} + public run(start: InputStep, state: S) { + return this.stepThrough(start, state); + } + + public async showQuickPick>({ + title, + step, + totalSteps, + items, + activeItem, + placeholder, + buttons, + shouldResume, + matchOnDescription, + matchOnDetail, + acceptFilterBoxTextAsSelection + }: P): Promise> { + const disposables: Disposable[] = []; + try { + return await new Promise>((resolve, reject) => { + const input = this.shell.createQuickPick(); + input.title = title; + input.step = step; + input.totalSteps = totalSteps; + input.placeholder = placeholder; + input.ignoreFocusOut = true; + input.items = items; + input.matchOnDescription = matchOnDescription || false; + input.matchOnDetail = matchOnDetail || false; + if (activeItem) { + input.activeItems = [activeItem]; + } else { + input.activeItems = []; + } + input.buttons = [...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons || [])]; + disposables.push( + input.onDidTriggerButton((item) => { + if (item === QuickInputButtons.Back) { + reject(InputFlowAction.back); + } else { + resolve(item); + } + }), + input.onDidChangeSelection((selectedItems) => resolve(selectedItems[0])), + input.onDidHide(() => { + (async () => { + reject( + shouldResume && (await shouldResume()) ? InputFlowAction.resume : InputFlowAction.cancel + ); + })().catch(reject); + }) + ); + if (acceptFilterBoxTextAsSelection) { + disposables.push( + input.onDidAccept(() => { + resolve(input.value); + }) + ); + } + if (this.current) { + this.current.dispose(); + } + this.current = input; + this.current.show(); + }); + } finally { + disposables.forEach((d) => d.dispose()); + } + } + + public async showInputBox

({ + title, + step, + totalSteps, + value, + prompt, + validate, + password, + buttons, + shouldResume + }: P): Promise> { + const disposables: Disposable[] = []; + try { + return await new Promise>((resolve, reject) => { + const input = this.shell.createInputBox(); + input.title = title; + input.step = step; + input.totalSteps = totalSteps; + input.password = password ? true : false; + input.value = value || ''; + input.prompt = prompt; + input.ignoreFocusOut = true; + input.buttons = [...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons || [])]; + let validating = validate(''); + disposables.push( + input.onDidTriggerButton((item) => { + if (item === QuickInputButtons.Back) { + reject(InputFlowAction.back); + } else { + resolve(item); + } + }), + input.onDidAccept(async () => { + const inputValue = input.value; + input.enabled = false; + input.busy = true; + if (!(await validate(inputValue))) { + resolve(inputValue); + } + input.enabled = true; + input.busy = false; + }), + input.onDidChangeValue(async (text) => { + const current = validate(text); + validating = current; + const validationMessage = await current; + if (current === validating) { + input.validationMessage = validationMessage; + } + }), + input.onDidHide(() => { + (async () => { + reject( + shouldResume && (await shouldResume()) ? InputFlowAction.resume : InputFlowAction.cancel + ); + })().catch(reject); + }) + ); + if (this.current) { + this.current.dispose(); + } + this.current = input; + this.current.show(); + }); + } finally { + disposables.forEach((d) => d.dispose()); + } + } + + private async stepThrough(start: InputStep, state: S) { + let step: InputStep | void = start; + while (step) { + this.steps.push(step); + if (this.current) { + this.current.enabled = false; + this.current.busy = true; + } + try { + step = await step(this, state); + } catch (err) { + if (err === InputFlowAction.back) { + this.steps.pop(); + step = this.steps.pop(); + } else if (err === InputFlowAction.resume) { + step = this.steps.pop(); + } else if (err === InputFlowAction.cancel) { + step = undefined; + } else { + throw err; + } + } + } + if (this.current) { + this.current.dispose(); + } + } +} +export const IMultiStepInputFactory = Symbol('IMultiStepInputFactory'); +export interface IMultiStepInputFactory { + create(): IMultiStepInput; +} +@injectable() +export class MultiStepInputFactory { + constructor(@inject(IApplicationShell) private readonly shell: IApplicationShell) {} + public create(): IMultiStepInput { + return new MultiStepInput(this.shell); + } +} diff --git a/src/client/common/utils/platform.ts b/src/client/common/utils/platform.ts new file mode 100644 index 000000000000..20476b5944d4 --- /dev/null +++ b/src/client/common/utils/platform.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { EnvironmentVariables } from '../variables/types'; + +export enum Architecture { + Unknown = 1, + x86 = 2, + x64 = 3 +} +export enum OSType { + Unknown = 'Unknown', + Windows = 'Windows', + OSX = 'OSX', + Linux = 'Linux' +} + +// Return the OS type for the given platform string. +export function getOSType(platform: string = process.platform): OSType { + if (/^win/.test(platform)) { + return OSType.Windows; + } else if (/^darwin/.test(platform)) { + return OSType.OSX; + } else if (/^linux/.test(platform)) { + return OSType.Linux; + } else { + return OSType.Unknown; + } +} + +export function getEnvironmentVariable(key: string): string | undefined { + // tslint:disable-next-line: no-any + return ((process.env as any) as EnvironmentVariables)[key]; +} + +export function getPathEnvironmentVariable(): string | undefined { + return getEnvironmentVariable('Path') || getEnvironmentVariable('PATH'); +} + +export function getUserHomeDir(): string | undefined { + if (getOSType() === OSType.Windows) { + return getEnvironmentVariable('USERPROFILE'); + } + return getEnvironmentVariable('HOME') || getEnvironmentVariable('HOMEPATH'); +} diff --git a/src/client/common/utils/random.ts b/src/client/common/utils/random.ts new file mode 100644 index 000000000000..872766274ff2 --- /dev/null +++ b/src/client/common/utils/random.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as crypto from 'crypto'; +import { injectable } from 'inversify'; +import { IRandom } from '../types'; + +function getRandom(): number { + let num: number = 0; + + const buf: Buffer = crypto.randomBytes(2); + num = (buf.readUInt8(0) << 8) + buf.readUInt8(1); + + const maxValue: number = Math.pow(16, 4) - 1; + return num / maxValue; +} + +export function getRandomBetween(min: number = 0, max: number = 10): number { + const randomVal: number = getRandom(); + return min + randomVal * (max - min); +} + +@injectable() +export class Random implements IRandom { + public getRandomInt(min: number = 0, max: number = 10): number { + return getRandomBetween(min, max); + } +} diff --git a/src/client/common/utils/regexp.ts b/src/client/common/utils/regexp.ts new file mode 100644 index 000000000000..d05d7fc60204 --- /dev/null +++ b/src/client/common/utils/regexp.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/* Generate a RegExp from a "verbose" pattern. + * + * All whitespace in the pattern is removed, including newlines. This + * allows the pattern to be much more readable by allowing it to span + * multiple lines and to separate tokens with insignificant whitespace. + * The functionality is similar to the VERBOSE ("x") flag in Python's + * regular expressions. + * + * Note that significant whitespace in the pattern must be explicitly + * indicated by "\s". Also, unlike with regular expression literals, + * backslashes must be escaped. Conversely, forward slashes do not + * need to be escaped. + * + * Line comments are also removed. A comment is two spaces followed + * by `#` followed by a space and then the rest of the text to the + * end of the line. + */ +export function verboseRegExp(pattern: string, flags?: string): RegExp { + pattern = pattern.replace(/(^| {2})# .*$/gm, ''); + pattern = pattern.replace(/\s+?/g, ''); + return RegExp(pattern, flags); +} diff --git a/src/client/common/utils/serializers.ts b/src/client/common/utils/serializers.ts new file mode 100644 index 000000000000..7626c0780264 --- /dev/null +++ b/src/client/common/utils/serializers.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/** + * Serialize ArraBuffer and ArrayBufferView into a fomat such that they are json serializable. + * + * @export + * @param {(undefined | (ArrayBuffer | ArrayBufferView)[])} buffers + * @returns + */ +export function serializeDataViews(buffers: undefined | (ArrayBuffer | ArrayBufferView)[]) { + if (!buffers || !Array.isArray(buffers) || buffers.length === 0) { + return; + } + // tslint:disable-next-line: no-any + const newBufferView: any[] = []; + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < buffers.length; i += 1) { + const item = buffers[i]; + if ('buffer' in item && 'byteOffset' in item) { + // It is an ArrayBufferView + // tslint:disable-next-line: no-any + const buffer = Array.apply(null, new Uint8Array(item.buffer as any) as any); + newBufferView.push({ + ...item, + byteLength: item.byteLength, + byteOffset: item.byteOffset, + buffer + // tslint:disable-next-line: no-any + } as any); + } else { + // Do not use `Array.apply`, it will not work for large arrays. + // Nodejs will throw `stackoverflow` exceptions. + // Else following ipynb fails https://github.com/K3D-tools/K3D-jupyter/blob/821a59ed88579afaafababd6291e8692d70eb088/examples/camera_manipulation.ipynb + // Yet another case where 99% can work, but 1% can fail when testing. + // tslint:disable-next-line: no-any + newBufferView.push([...new Uint8Array(item as any)]); + } + } + + // tslint:disable-next-line: no-any + return newBufferView; +} + +/** + * Deserializes ArrayBuffer and ArrayBufferView from a format that was json serializable into actual ArrayBuffer and ArrayBufferViews. + * + * @export + * @param {(undefined | (ArrayBuffer | ArrayBufferView)[])} buffers + * @returns + */ +export function deserializeDataViews(buffers: undefined | (ArrayBuffer | ArrayBufferView)[]) { + if (!Array.isArray(buffers) || buffers.length === 0) { + return buffers; + } + const newBufferView: (ArrayBuffer | ArrayBufferView)[] = []; + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < buffers.length; i += 1) { + const item = buffers[i]; + if ('buffer' in item && 'byteOffset' in item) { + const buffer = new Uint8Array(item.buffer).buffer; + // It is an ArrayBufferView + // tslint:disable-next-line: no-any + const bufferView = new DataView(buffer, item.byteOffset, item.byteLength); + newBufferView.push(bufferView); + } else { + const buffer = new Uint8Array(item).buffer; + // tslint:disable-next-line: no-any + newBufferView.push(buffer); + } + } + return newBufferView; +} diff --git a/src/client/common/utils/stopWatch.ts b/src/client/common/utils/stopWatch.ts new file mode 100644 index 000000000000..c78c763f7d2c --- /dev/null +++ b/src/client/common/utils/stopWatch.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export class StopWatch { + private started = new Date().getTime(); + public get elapsedTime() { + return new Date().getTime() - this.started; + } + public reset() { + this.started = new Date().getTime(); + } +} diff --git a/src/client/common/utils/sysTypes.ts b/src/client/common/utils/sysTypes.ts new file mode 100644 index 000000000000..ce0bce0af963 --- /dev/null +++ b/src/client/common/utils/sysTypes.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +// tslint:disable:rule1 no-any no-unnecessary-callback-wrapper jsdoc-format no-for-in prefer-const no-increment-decrement + +const _typeof = { + number: 'number', + string: 'string', + undefined: 'undefined', + object: 'object', + function: 'function' +}; + +/** + * @returns whether the provided parameter is a JavaScript Array or not. + */ +export function isArray(array: any): array is any[] { + if (Array.isArray) { + return Array.isArray(array); + } + + if (array && typeof array.length === _typeof.number && array.constructor === Array) { + return true; + } + + return false; +} + +/** + * @returns whether the provided parameter is a JavaScript String or not. + */ +export function isString(str: any): str is string { + if (typeof str === _typeof.string || str instanceof String) { + return true; + } + + return false; +} + +/** + * @returns whether the provided parameter is a JavaScript Array and each element in the array is a string. + */ +export function isStringArray(value: any): value is string[] { + return isArray(value) && (value).every((elem) => isString(elem)); +} + +/** + * + * @returns whether the provided parameter is of type `object` but **not** + * `null`, an `array`, a `regexp`, nor a `date`. + */ +export function isObject(obj: any): obj is any { + return ( + typeof obj === _typeof.object && + obj !== null && + !Array.isArray(obj) && + !(obj instanceof RegExp) && + !(obj instanceof Date) + ); +} + +/** + * In **contrast** to just checking `typeof` this will return `false` for `NaN`. + * @returns whether the provided parameter is a JavaScript Number or not. + */ +export function isNumber(obj: any): obj is number { + if ((typeof obj === _typeof.number || obj instanceof Number) && !isNaN(obj)) { + return true; + } + + return false; +} + +/** + * @returns whether the provided parameter is a JavaScript Boolean or not. + */ +export function isBoolean(obj: any): obj is boolean { + return obj === true || obj === false; +} + +/** + * @returns whether the provided parameter is undefined. + */ +export function isUndefined(obj: any): boolean { + return typeof obj === _typeof.undefined; +} + +/** + * @returns whether the provided parameter is undefined or null. + */ +export function isUndefinedOrNull(obj: any): boolean { + return isUndefined(obj) || obj === null; +} + +const hasOwnProperty = Object.prototype.hasOwnProperty; + +/** + * @returns whether the provided parameter is an empty JavaScript Object or not. + */ +export function isEmptyObject(obj: any): obj is any { + if (!isObject(obj)) { + return false; + } + + for (let key in obj) { + if (hasOwnProperty.call(obj, key)) { + return false; + } + } + + return true; +} + +/** + * @returns whether the provided parameter is a JavaScript Function or not. + */ +export function isFunction(obj: any): obj is Function { + return typeof obj === _typeof.function; +} + +/** + * @returns whether the provided parameters is are JavaScript Function or not. + */ +export function areFunctions(...objects: any[]): boolean { + return objects && objects.length > 0 && objects.every(isFunction); +} diff --git a/src/client/common/utils/text.ts b/src/client/common/utils/text.ts new file mode 100644 index 000000000000..59359966db47 --- /dev/null +++ b/src/client/common/utils/text.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Position, Range, TextDocument } from 'vscode'; +import { isNumber } from './sysTypes'; + +export function getWindowsLineEndingCount(document: TextDocument, offset: Number) { + //const eolPattern = new RegExp('\r\n', 'g'); + const eolPattern = /\r\n/g; + const readBlock = 1024; + let count = 0; + let offsetDiff = offset.valueOf(); + + // In order to prevent the one-time loading of large files from taking up too much memory + for (let pos = 0; pos < offset; pos += readBlock) { + const startAt = document.positionAt(pos); + + let endAt: Position; + if (offsetDiff >= readBlock) { + endAt = document.positionAt(pos + readBlock); + offsetDiff = offsetDiff - readBlock; + } else { + endAt = document.positionAt(pos + offsetDiff); + } + + const text = document.getText(new Range(startAt, endAt!)); + const cr = text.match(eolPattern); + + count += cr ? cr.length : 0; + } + return count; +} + +/** + * Return the range represented by the given string. + * + * If a number is provided then it is used as both lines and the + * character are set to 0. + * + * Examples: + * '1:5-3:5' -> Range(1, 5, 3, 5) + * '1-3' -> Range(1, 0, 3, 0) + * '1:3-1:5' -> Range(1, 3, 1, 5) + * '1-1' -> Range(1, 0, 1, 0) + * '1' -> Range(1, 0, 1, 0) + * '1:3-' -> Range(1, 3, 1, 0) + * '1:3' -> Range(1, 3, 1, 0) + * '' -> Range(0, 0, 0, 0) + * '3-1' -> Range(1, 0, 3, 0) + */ +export function parseRange(raw: string | number): Range { + if (isNumber(raw)) { + return new Range(raw, 0, raw, 0); + } + if (raw === '') { + return new Range(0, 0, 0, 0); + } + + const parts = raw.split('-'); + if (parts.length > 2) { + throw new Error(`invalid range ${raw}`); + } + + const start = parsePosition(parts[0]); + let end = start; + if (parts.length === 2) { + end = parsePosition(parts[1]); + } + return new Range(start, end); +} + +/** + * Return the line/column represented by the given string. + * + * If a number is provided then it is used as the line and the character + * is set to 0. + * + * Examples: + * '1:5' -> Position(1, 5) + * '1' -> Position(1, 0) + * '' -> Position(0, 0) + */ +export function parsePosition(raw: string | number): Position { + if (isNumber(raw)) { + return new Position(raw, 0); + } + if (raw === '') { + return new Position(0, 0); + } + + const parts = raw.split(':'); + if (parts.length > 2) { + throw new Error(`invalid position ${raw}`); + } + + let line = 0; + if (parts[0] !== '') { + if (!/^\d+$/.test(parts[0])) { + throw new Error(`invalid position ${raw}`); + } + line = +parts[0]; + } + let col = 0; + if (parts.length === 2 && parts[1] !== '') { + if (!/^\d+$/.test(parts[1])) { + throw new Error(`invalid position ${raw}`); + } + col = +parts[1]; + } + return new Position(line, col); +} diff --git a/src/client/common/utils/version.ts b/src/client/common/utils/version.ts new file mode 100644 index 000000000000..830ebc668f78 --- /dev/null +++ b/src/client/common/utils/version.ts @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-multiline-string + +import * as semver from 'semver'; +import { verboseRegExp } from './regexp'; + +//=========================== +// basic version info + +/** + * basic version information + * + * A normalized object will only have non-negative numbers, or `-1`, + * in its properties. A `-1` value is an indicator that the property + * is not set. Lower properties will not be set if a higher property + * is not. + * + * Note that any object can be forced to look like a VersionInfo and + * any of the properties may be forced to hold a non-number value. + * To resolve this situation, pass the object through + * `normalizeVersionInfo()` and then `validateVersionInfo()`. + */ +export type BasicVersionInfo = { + major: number; + minor: number; + micro: number; + // There is also a hidden `unnormalized` property. +}; + +type ErrorMsg = string; + +function normalizeVersionPart(part: unknown): [number, ErrorMsg] { + // Any -1 values where the original is not a number are handled in validation. + if (typeof part === 'number') { + if (isNaN(part)) { + return [-1, 'missing']; + } + if (part < 0) { + // We leave this as a marker. + return [-1, '']; + } + return [part, '']; + } + if (typeof part === 'string') { + const parsed = parseInt(part, 10); + if (isNaN(parsed)) { + return [-1, 'string not numeric']; + } + if (parsed < 0) { + return [-1, '']; + } + return [parsed, '']; + } + if (part === undefined || part === null) { + return [-1, 'missing']; + } + return [-1, 'unsupported type']; +} + +type RawBasicVersionInfo = BasicVersionInfo & { + unnormalized?: { + major?: ErrorMsg; + minor?: ErrorMsg; + micro?: ErrorMsg; + }; +}; + +export const EMPTY_VERSION: RawBasicVersionInfo = { + major: -1, + minor: -1, + micro: -1, + unnormalized: { + major: undefined, + minor: undefined, + micro: undefined + } +}; + +/** + * Make a copy and set all the properties properly. + * + * Only the "basic" version info will be normalized. The caller + * is responsible for any other properties beyond that. + */ +export function normalizeBasicVersionInfo(info: T | undefined): T { + if (!info) { + return EMPTY_VERSION as T; + } + const norm: T = { ...info }; + const raw = (norm as unknown) as RawBasicVersionInfo; + // Do not normalize if it has already been normalized. + if (raw.unnormalized === undefined) { + raw.unnormalized = {}; + [norm.major, raw.unnormalized.major] = normalizeVersionPart(norm.major); + [norm.minor, raw.unnormalized.minor] = normalizeVersionPart(norm.minor); + [norm.micro, raw.unnormalized.micro] = normalizeVersionPart(norm.micro); + } + return norm; +} + +function validateVersionPart(prop: string, part: number, unnormalized?: ErrorMsg) { + // We expect a normalized version part here, so there's no need + // to check for NaN or non-numbers here. + if (part === 0 || part > 0) { + return; + } + if (!unnormalized || unnormalized === '') { + return; + } + throw Error(`invalid ${prop} version (failed to normalize; ${unnormalized})`); +} + +/** + * Fail if any properties are not set properly. + * + * The info is expected to be normalized already. + * + * Only the "basic" version info will be validated. The caller + * is responsible for any other properties beyond that. + */ +export function validateBasicVersionInfo(info: T) { + const raw = (info as unknown) as RawBasicVersionInfo; + validateVersionPart('major', info.major, raw.unnormalized?.major); + validateVersionPart('minor', info.minor, raw.unnormalized?.minor); + validateVersionPart('micro', info.micro, raw.unnormalized?.micro); + if (info.major < 0) { + throw Error('missing major version'); + } + if (info.minor < 0) { + if (info.micro === 0 || info.micro > 0) { + throw Error('missing minor version'); + } + } +} + +/** + * Convert the info to a simple string. + * + * Any negative parts are ignored. + * + * The object is expected to be normalized. + */ +export function getVersionString(info: T): string { + if (info.major < 0) { + return ''; + } else if (info.minor < 0) { + return `${info.major}`; + } else if (info.micro < 0) { + return `${info.major}.${info.minor}`; + } + return `${info.major}.${info.minor}.${info.micro}`; +} + +export type ParseResult = { + version: T; + before: string; + after: string; +}; + +const basicVersionPattern = ` + ^ + (.*?) # + (\\d+) # + (?: + [.] + (\\d+) # + (?: + [.] + (\\d+) # + )? + )? + ([^\\d].*)? # + $ +`; +const basicVersionRegexp = verboseRegExp(basicVersionPattern, 's'); + +/** + * Extract a version from the given text. + * + * If the version is surrounded by other text then that is provided + * as well. + */ +export function parseBasicVersionInfo(verStr: string): ParseResult | undefined { + const match = verStr.match(basicVersionRegexp); + if (!match) { + return undefined; + } + // Ignore the first element (the full match). + const [, before, majorStr, minorStr, microStr, after] = match; + if (before && before.endsWith('.')) { + return undefined; + } + + if (after && after !== '') { + if (after === '.') { + return undefined; + } + // Disallow a plain version with trailing text if it isn't complete + if (!before || before === '') { + if (!microStr || microStr === '') { + return undefined; + } + } + } + const major = parseInt(majorStr, 10); + const minor = minorStr ? parseInt(minorStr, 10) : -1; + const micro = microStr ? parseInt(microStr, 10) : -1; + return { + // This is effectively normalized. + version: ({ major, minor, micro } as unknown) as T, + before: before || '', + after: after || '' + }; +} + +/** + * Returns true if the given version appears to be not set. + * + * The object is expected to already be normalized. + */ +export function isVersionInfoEmpty(info: T): boolean { + if (!info) { + return false; + } + if (typeof info.major !== 'number' || typeof info.minor !== 'number' || typeof info.micro !== 'number') { + return false; + } + return info.major < 0 && info.minor < 0 && info.micro < 0; +} + +//=========================== +// base version info + +/** + * basic version information + * + * @prop raw - the unparsed version string, if any + */ +export type VersionInfo = BasicVersionInfo & { + raw?: string; +}; + +/** + * Make a copy and set all the properties properly. + */ +export function normalizeVersionInfo(info: T): T { + const basic = normalizeBasicVersionInfo(info); + if (!info) { + basic.raw = ''; + return basic; + } + const norm = { ...info, ...basic }; + if (!norm.raw) { + norm.raw = ''; + } + return norm; +} + +/** + * Fail if any properties are not set properly. + * + * Optional properties that are not set are ignored. + * + * This assumes that the info has already been normalized. + */ +export function validateVersionInfo(info: T) { + validateBasicVersionInfo(info); + // `info.raw` can be anything. +} + +/** + * Extract a version from the given text. + * + * If the version is surrounded by other text then that is provided + * as well. + */ +export function parseVersionInfo(verStr: string): ParseResult | undefined { + const result = parseBasicVersionInfo(verStr); + if (result === undefined) { + return undefined; + } + result.version.raw = verStr; + return result; +} + +//=========================== +// semver + +export function parseVersion(raw: string): semver.SemVer { + raw = raw.replace(/\.00*(?=[1-9]|0\.)/, '.'); + const ver = semver.coerce(raw); + if (ver === null || !semver.valid(ver)) { + // tslint:disable-next-line: no-suspicious-comment + // TODO: Raise an exception instead? + return new semver.SemVer('0.0.0'); + } + return ver; +} diff --git a/src/client/common/utils/workerPool.ts b/src/client/common/utils/workerPool.ts new file mode 100644 index 000000000000..12bc2d322045 --- /dev/null +++ b/src/client/common/utils/workerPool.ts @@ -0,0 +1,250 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { traceError } from '../logger'; +import { createDeferred, Deferred } from './async'; + +interface IWorker { + /** + * Start processing of items. + * @method stop + */ + start(): void; + /** + * Stops any further processing of items. + * @method stop + */ + stop(): void; +} + +type NextFunc = () => Promise; +type WorkFunc = (item: T) => Promise; +type PostResult = (item: T, result?: R, err?: Error) => void; + +interface IWorkItem { + item: T; +} + +export enum QueuePosition { + Back, + Front +} + +export interface IWorkerPool extends IWorker { + /** + * Add items to be processed to a queue. + * @method addToQueue + * @param {T} item: Item to process + * @param {QueuePosition} position: Add items to the front or back of the queue. + * @returns A promise that when resolved gets the result from running the worker function. + */ + addToQueue(item: T, position?: QueuePosition): Promise; +} + +class Worker implements IWorker { + private stopProcessing: boolean = false; + public constructor( + private readonly next: NextFunc, + private readonly workFunc: WorkFunc, + private readonly postResult: PostResult, + private readonly name: string + ) {} + public stop() { + this.stopProcessing = true; + } + + public async start() { + while (!this.stopProcessing) { + try { + const workItem = await this.next(); + try { + const result = await this.workFunc(workItem); + this.postResult(workItem, result); + } catch (ex) { + this.postResult(workItem, undefined, ex); + } + } catch (ex) { + // Next got rejected. Likely worker pool is shutting down. + // continue here and worker will exit if the worker pool is shutting down. + traceError(`Error while running worker[${this.name}].`, ex); + continue; + } + } + } +} + +class WorkQueue { + private readonly items: IWorkItem[] = []; + private readonly results: Map, Deferred> = new Map(); + public add(item: T, position?: QueuePosition): Promise { + // Wrap the user provided item in a wrapper object. This will allow us to track multiple + // submissions of the same item. For example, addToQueue(2), addToQueue(2). If we did not + // wrap this, then from the map both submissions will look the same. Since this is a generic + // worker pool, we do not know if we can resolve both using the same promise. So, a better + // approach is to ensure each gets a unique promise, and let the worker function figure out + // how to handle repeat submissions. + const workItem: IWorkItem = { item }; + if (position === QueuePosition.Front) { + this.items.unshift(workItem); + } else { + this.items.push(workItem); + } + + // This is the promise that will be resolved when the work + // item is complete. We save this in a map to resolve when + // the worker finishes and posts the result. + const deferred = createDeferred(); + this.results.set(workItem, deferred); + + return deferred.promise; + } + + public completed(workItem: IWorkItem, result?: R, error?: Error): void { + const deferred = this.results.get(workItem); + if (deferred !== undefined) { + this.results.delete(workItem); + if (error !== undefined) { + deferred.reject(error); + } + deferred.resolve(result); + } + } + + public next(): IWorkItem | undefined { + return this.items.shift(); + } + + public clear(): void { + this.results.forEach((v: Deferred, k: IWorkItem, map: Map, Deferred>) => { + v.reject(Error('Queue stopped processing')); + map.delete(k); + }); + } +} + +class WorkerPool implements IWorkerPool { + // This collection tracks the full set of workers. + private readonly workers: IWorker[] = []; + + // A collections that holds unblock callback for each worker waiting + // for a work item when the queue is empty + private readonly waitingWorkersUnblockQueue: { unblock(w: IWorkItem): void; stop(): void }[] = []; + + // A collection that manages the work items. + private readonly queue = new WorkQueue(); + + // State of the pool manages via stop(), start() + private stopProcessing = false; + + public constructor( + private readonly workerFunc: WorkFunc, + private readonly numWorkers: number = 2, + private readonly name: string = 'Worker' + ) {} + + public addToQueue(item: T, position?: QueuePosition): Promise { + if (this.stopProcessing) { + throw Error('Queue is stopped'); + } + + // This promise when resolved should return the processed result of the item + // being added to the queue. + const deferred = this.queue.add(item, position); + + const worker = this.waitingWorkersUnblockQueue.shift(); + if (worker) { + const workItem = this.queue.next(); + if (workItem !== undefined) { + // If we are here it means there were no items to process in the queue. + // At least one worker is free and waiting for a work item. Call 'unblock' + // and give the worker the newly added item. + worker.unblock(workItem); + } else { + // Something is wrong, we should not be here. we just added an item to + // the queue. It should not be empty. + traceError('Work queue was empty immediately after adding item.'); + } + } + + return deferred; + } + + public start() { + this.stopProcessing = false; + let num = this.numWorkers; + while (num > 0) { + this.workers.push( + new Worker, R>( + () => this.nextWorkItem(), + (workItem: IWorkItem) => this.workerFunc(workItem.item), + (workItem: IWorkItem, result?: R, error?: Error) => + this.queue.completed(workItem, result, error), + `${this.name} ${num}` + ) + ); + num = num - 1; + } + this.workers.forEach(async (w) => w.start()); + } + + public stop(): void { + this.stopProcessing = true; + + // Signal all registered workers with this worker pool to stop processing. + // Workers should complete the task they are currently doing. + let worker = this.workers.shift(); + while (worker) { + worker.stop(); + worker = this.workers.shift(); + } + + // Remove items from queue. + this.queue.clear(); + + // This is necessary to exit any worker that is waiting for an item. + // If we don't unblock here then the worker just remains blocked + // forever. + let blockedWorker = this.waitingWorkersUnblockQueue.shift(); + while (blockedWorker) { + blockedWorker.stop(); + blockedWorker = this.waitingWorkersUnblockQueue.shift(); + } + } + + public nextWorkItem(): Promise> { + // Note that next() will return `undefined` if the queue is empty. + const nextWorkItem = this.queue.next(); + if (nextWorkItem !== undefined) { + return Promise.resolve(nextWorkItem); + } + + // Queue is Empty, so return a promise that will be resolved when + // new items are added to the queue. + return new Promise>((resolve, reject) => { + this.waitingWorkersUnblockQueue.push({ + unblock: (workItem?: IWorkItem) => { + // This will be called to unblock any worker waiting for items. + if (this.stopProcessing) { + // We should reject here since the processing should be stopped. + reject(); + } + // If we are here, the queue received a new work item. Resolve with that item. + resolve(workItem); + }, + stop: () => { + reject(); + } + }); + }); + } +} + +export function createWorkerPool( + workerFunc: WorkFunc, + numWorkers: number = 2, + name: string = 'Worker' +): IWorkerPool { + const pool = new WorkerPool(workerFunc, numWorkers, name); + pool.start(); + return pool; +} diff --git a/src/client/common/variables/environment.ts b/src/client/common/variables/environment.ts new file mode 100644 index 000000000000..63f7896d5423 --- /dev/null +++ b/src/client/common/variables/environment.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IFileSystem } from '../platform/types'; +import { IPathUtils } from '../types'; +import { EnvironmentVariables, IEnvironmentVariablesService } from './types'; + +@injectable() +export class EnvironmentVariablesService implements IEnvironmentVariablesService { + private _pathVariable?: 'Path' | 'PATH'; + constructor( + // We only use a small portion of either of these interfaces. + @inject(IPathUtils) private readonly pathUtils: IPathUtils, + @inject(IFileSystem) private readonly fs: IFileSystem + ) {} + + public async parseFile( + filePath?: string, + baseVars?: EnvironmentVariables + ): Promise { + if (!filePath || !(await this.fs.fileExists(filePath))) { + return; + } + return parseEnvFile(await this.fs.readFile(filePath), baseVars); + } + + public mergeVariables(source: EnvironmentVariables, target: EnvironmentVariables) { + if (!target) { + return; + } + const settingsNotToMerge = ['PYTHONPATH', this.pathVariable]; + Object.keys(source).forEach((setting) => { + if (settingsNotToMerge.indexOf(setting) >= 0) { + return; + } + if (target[setting] === undefined) { + target[setting] = source[setting]; + } + }); + } + + public appendPythonPath(vars: EnvironmentVariables, ...pythonPaths: string[]) { + return this.appendPaths(vars, 'PYTHONPATH', ...pythonPaths); + } + + public appendPath(vars: EnvironmentVariables, ...paths: string[]) { + return this.appendPaths(vars, this.pathVariable, ...paths); + } + + private get pathVariable(): 'Path' | 'PATH' { + if (!this._pathVariable) { + this._pathVariable = this.pathUtils.getPathVariableName(); + } + return this._pathVariable!; + } + + private appendPaths( + vars: EnvironmentVariables, + variableName: 'PATH' | 'Path' | 'PYTHONPATH', + ...pathsToAppend: string[] + ) { + const valueToAppend = pathsToAppend + .filter((item) => typeof item === 'string' && item.trim().length > 0) + .map((item) => item.trim()) + .join(path.delimiter); + if (valueToAppend.length === 0) { + return vars; + } + + const variable = vars ? vars[variableName] : undefined; + if (variable && typeof variable === 'string' && variable.length > 0) { + vars[variableName] = variable + path.delimiter + valueToAppend; + } else { + vars[variableName] = valueToAppend; + } + return vars; + } +} + +export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVariables): EnvironmentVariables { + const globalVars = baseVars ? baseVars : {}; + const vars: EnvironmentVariables = {}; + lines + .toString() + .split('\n') + .forEach((line, _idx) => { + const [name, value] = parseEnvLine(line); + if (name === '') { + return; + } + vars[name] = substituteEnvVars(value, vars, globalVars); + }); + return vars; +} + +function parseEnvLine(line: string): [string, string] { + // Most of the following is an adaptation of the dotenv code: + // https://github.com/motdotla/dotenv/blob/master/lib/main.js#L32 + // We don't use dotenv here because it loses ordering, which is + // significant for substitution. + const match = line.match(/^\s*([a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/); + if (!match) { + return ['', '']; + } + + const name = match[1]; + let value = match[2]; + if (value && value !== '') { + if (value[0] === "'" && value[value.length - 1] === "'") { + value = value.substring(1, value.length - 1); + value = value.replace(/\\n/gm, '\n'); + } else if (value[0] === '"' && value[value.length - 1] === '"') { + value = value.substring(1, value.length - 1); + value = value.replace(/\\n/gm, '\n'); + } + } else { + value = ''; + } + + return [name, value]; +} + +const SUBST_REGEX = /\${([a-zA-Z]\w*)?([^}\w].*)?}/g; + +function substituteEnvVars( + value: string, + localVars: EnvironmentVariables, + globalVars: EnvironmentVariables, + missing = '' +): string { + // Substitution here is inspired a little by dotenv-expand: + // https://github.com/motdotla/dotenv-expand/blob/master/lib/main.js + + let invalid = false; + let replacement = value; + replacement = replacement.replace(SUBST_REGEX, (match, substName, bogus, offset, orig) => { + if (offset > 0 && orig[offset - 1] === '\\') { + return match; + } + if ((bogus && bogus !== '') || !substName || substName === '') { + invalid = true; + return match; + } + return localVars[substName] || globalVars[substName] || missing; + }); + if (!invalid && replacement !== value) { + value = replacement; + sendTelemetryEvent(EventName.ENVFILE_VARIABLE_SUBSTITUTION); + } + + return value.replace(/\\\$/g, '$'); +} diff --git a/src/client/common/variables/environmentVariablesProvider.ts b/src/client/common/variables/environmentVariablesProvider.ts new file mode 100644 index 000000000000..fd083b10e88f --- /dev/null +++ b/src/client/common/variables/environmentVariablesProvider.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, optional } from 'inversify'; +import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, FileSystemWatcher, Uri } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; +import { sendFileCreationTelemetry } from '../../telemetry/envFileTelemetry'; +import { IWorkspaceService } from '../application/types'; +import { traceVerbose } from '../logger'; +import { IPlatformService } from '../platform/types'; +import { IConfigurationService, ICurrentProcess, IDisposableRegistry } from '../types'; +import { InMemoryInterpreterSpecificCache } from '../utils/cacheUtils'; +import { clearCachedResourceSpecificIngterpreterData } from '../utils/decorators'; +import { EnvironmentVariables, IEnvironmentVariablesProvider, IEnvironmentVariablesService } from './types'; + +const CACHE_DURATION = 60 * 60 * 1000; +@injectable() +export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvider, Disposable { + public trackedWorkspaceFolders = new Set(); + private fileWatchers = new Map(); + private disposables: Disposable[] = []; + private changeEventEmitter: EventEmitter; + constructor( + @inject(IEnvironmentVariablesService) private envVarsService: IEnvironmentVariablesService, + @inject(IDisposableRegistry) disposableRegistry: Disposable[], + @inject(IPlatformService) private platformService: IPlatformService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(ICurrentProcess) private process: ICurrentProcess, + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @optional() private cacheDuration: number = CACHE_DURATION + ) { + disposableRegistry.push(this); + this.changeEventEmitter = new EventEmitter(); + const disposable = this.workspaceService.onDidChangeConfiguration(this.configurationChanged, this); + this.disposables.push(disposable); + } + + public get onDidEnvironmentVariablesChange(): Event { + return this.changeEventEmitter.event; + } + + public dispose() { + this.changeEventEmitter.dispose(); + this.fileWatchers.forEach((watcher) => { + if (watcher) { + watcher.dispose(); + } + }); + } + + public async getEnvironmentVariables(resource?: Uri): Promise { + // Cache resource specific interpreter data + const cacheStore = new InMemoryInterpreterSpecificCache( + 'getEnvironmentVariables', + this.cacheDuration, + [resource], + this.serviceContainer + ); + if (cacheStore.hasData) { + traceVerbose(`Cached data exists getEnvironmentVariables, ${resource ? resource.fsPath : ''}`); + return Promise.resolve(cacheStore.data) as Promise; + } + const promise = this._getEnvironmentVariables(resource); + promise.then((result) => (cacheStore.data = result)).ignoreErrors(); + return promise; + } + public async _getEnvironmentVariables(resource?: Uri): Promise { + let mergedVars = await this.getCustomEnvironmentVariables(resource); + if (!mergedVars) { + mergedVars = {}; + } + this.envVarsService.mergeVariables(this.process.env, mergedVars!); + const pathVariable = this.platformService.pathVariableName; + const pathValue = this.process.env[pathVariable]; + if (pathValue) { + this.envVarsService.appendPath(mergedVars!, pathValue); + } + if (this.process.env.PYTHONPATH) { + this.envVarsService.appendPythonPath(mergedVars!, this.process.env.PYTHONPATH); + } + return mergedVars; + } + public async getCustomEnvironmentVariables(resource?: Uri): Promise { + const settings = this.configurationService.getSettings(resource); + const workspaceFolderUri = this.getWorkspaceFolderUri(resource); + this.trackedWorkspaceFolders.add(workspaceFolderUri ? workspaceFolderUri.fsPath : ''); + this.createFileWatcher(settings.envFile, workspaceFolderUri); + return this.envVarsService.parseFile(settings.envFile, this.process.env); + } + public configurationChanged(e: ConfigurationChangeEvent) { + this.trackedWorkspaceFolders.forEach((item) => { + const uri = item && item.length > 0 ? Uri.file(item) : undefined; + if (e.affectsConfiguration('python.envFile', uri)) { + this.onEnvironmentFileChanged(uri); + } + }); + } + public createFileWatcher(envFile: string, workspaceFolderUri?: Uri) { + if (this.fileWatchers.has(envFile)) { + return; + } + const envFileWatcher = this.workspaceService.createFileSystemWatcher(envFile); + this.fileWatchers.set(envFile, envFileWatcher); + if (envFileWatcher) { + this.disposables.push(envFileWatcher.onDidChange(() => this.onEnvironmentFileChanged(workspaceFolderUri))); + this.disposables.push(envFileWatcher.onDidCreate(() => this.onEnvironmentFileCreated(workspaceFolderUri))); + this.disposables.push(envFileWatcher.onDidDelete(() => this.onEnvironmentFileChanged(workspaceFolderUri))); + } + } + private getWorkspaceFolderUri(resource?: Uri): Uri | undefined { + if (!resource) { + return; + } + const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource!); + return workspaceFolder ? workspaceFolder.uri : undefined; + } + + private onEnvironmentFileCreated(workspaceFolderUri?: Uri) { + this.onEnvironmentFileChanged(workspaceFolderUri); + sendFileCreationTelemetry(); + } + + private onEnvironmentFileChanged(workspaceFolderUri?: Uri) { + clearCachedResourceSpecificIngterpreterData( + 'getEnvironmentVariables', + workspaceFolderUri, + this.serviceContainer + ); + clearCachedResourceSpecificIngterpreterData( + 'CustomEnvironmentVariables', + workspaceFolderUri, + this.serviceContainer + ); + this.changeEventEmitter.fire(workspaceFolderUri); + } +} diff --git a/src/client/common/variables/serviceRegistry.ts b/src/client/common/variables/serviceRegistry.ts new file mode 100644 index 000000000000..3a98c3cbfea4 --- /dev/null +++ b/src/client/common/variables/serviceRegistry.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceManager } from '../../ioc/types'; +import { EnvironmentVariablesService } from './environment'; +import { EnvironmentVariablesProvider } from './environmentVariablesProvider'; +import { IEnvironmentVariablesProvider, IEnvironmentVariablesService } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton( + IEnvironmentVariablesService, + EnvironmentVariablesService + ); + serviceManager.addSingleton( + IEnvironmentVariablesProvider, + EnvironmentVariablesProvider + ); +} diff --git a/src/client/common/variables/sysTypes.ts b/src/client/common/variables/sysTypes.ts new file mode 100644 index 000000000000..10bd2b776b17 --- /dev/null +++ b/src/client/common/variables/sysTypes.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +// tslint:disable:no-any no-increment-decrement + +import { isFunction, isString } from '../utils/sysTypes'; + +export type TypeConstraint = string | Function; + +export function validateConstraints(args: any[], constraints: TypeConstraint[]): void { + const len = Math.min(args.length, constraints.length); + for (let i = 0; i < len; i++) { + validateConstraint(args[i], constraints[i]); + } +} + +export function validateConstraint(arg: any, constraint: TypeConstraint): void { + if (isString(constraint)) { + if (typeof arg !== constraint) { + throw new Error(`argument does not match constraint: typeof ${constraint}`); + } + } else if (isFunction(constraint)) { + if (arg instanceof constraint) { + return; + } + if (arg && arg.constructor === constraint) { + return; + } + if (constraint.length === 1 && constraint.call(undefined, arg) === true) { + return; + } + throw new Error( + 'argument does not match one of these constraints: arg instanceof constraint, arg.constructor === constraint, nor constraint(arg) === true' + ); + } +} diff --git a/src/client/common/variables/systemVariables.ts b/src/client/common/variables/systemVariables.ts new file mode 100644 index 000000000000..74b307dafb78 --- /dev/null +++ b/src/client/common/variables/systemVariables.ts @@ -0,0 +1,192 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +import * as Path from 'path'; +import { Range, Uri } from 'vscode'; + +import { IDocumentManager, IWorkspaceService } from '../application/types'; +import * as Types from '../utils/sysTypes'; +import { IStringDictionary, ISystemVariables } from './types'; + +/* tslint:disable:rule1 no-any no-unnecessary-callback-wrapper jsdoc-format no-for-in prefer-const no-increment-decrement */ + +abstract class AbstractSystemVariables implements ISystemVariables { + public resolve(value: string): string; + public resolve(value: string[]): string[]; + public resolve(value: IStringDictionary): IStringDictionary; + public resolve(value: IStringDictionary): IStringDictionary; + public resolve(value: IStringDictionary>): IStringDictionary>; + // tslint:disable-next-line:no-any + public resolve(value: any): any { + if (Types.isString(value)) { + return this.__resolveString(value); + } else if (Types.isArray(value)) { + return this.__resolveArray(value); + } else if (Types.isObject(value)) { + return this.__resolveLiteral(value); + } + + return value; + } + + public resolveAny(value: T): T; + // tslint:disable-next-line:no-any + public resolveAny(value: any): any { + if (Types.isString(value)) { + return this.__resolveString(value); + } else if (Types.isArray(value)) { + return this.__resolveAnyArray(value); + } else if (Types.isObject(value)) { + return this.__resolveAnyLiteral(value); + } + + return value; + } + + private __resolveString(value: string): string { + const regexp = /\$\{(.*?)\}/g; + return value.replace(regexp, (match: string, name: string) => { + // tslint:disable-next-line:no-any + const newValue = (this)[name]; + if (Types.isString(newValue)) { + return newValue; + } else { + return match && (match.indexOf('env.') > 0 || match.indexOf('env:') > 0) ? '' : match; + } + }); + } + + private __resolveLiteral( + values: IStringDictionary | string[]> + ): IStringDictionary | string[]> { + const result: IStringDictionary | string[]> = Object.create(null); + Object.keys(values).forEach((key) => { + const value = values[key]; + // tslint:disable-next-line:no-any + result[key] = this.resolve(value); + }); + return result; + } + + private __resolveAnyLiteral(values: T): T; + // tslint:disable-next-line:no-any + private __resolveAnyLiteral(values: any): any { + const result: IStringDictionary | string[]> = Object.create(null); + Object.keys(values).forEach((key) => { + const value = values[key]; + // tslint:disable-next-line:no-any + result[key] = this.resolveAny(value); + }); + return result; + } + + private __resolveArray(value: string[]): string[] { + return value.map((s) => this.__resolveString(s)); + } + + private __resolveAnyArray(value: T[]): T[]; + // tslint:disable-next-line:no-any + private __resolveAnyArray(value: any[]): any[] { + return value.map((s) => this.resolveAny(s)); + } +} + +export class SystemVariables extends AbstractSystemVariables { + private _workspaceFolder: string; + private _workspaceFolderName: string; + private _filePath: string | undefined; + private _lineNumber: number | undefined; + private _selectedText: string | undefined; + private _execPath: string; + + constructor( + file: Uri | undefined, + rootFolder: string | undefined, + workspace?: IWorkspaceService, + documentManager?: IDocumentManager + ) { + super(); + const workspaceFolder = workspace && file ? workspace.getWorkspaceFolder(file) : undefined; + this._workspaceFolder = workspaceFolder ? workspaceFolder.uri.fsPath : rootFolder || __dirname; + this._workspaceFolderName = Path.basename(this._workspaceFolder); + this._filePath = file ? file.fsPath : undefined; + if (documentManager && documentManager.activeTextEditor) { + this._lineNumber = documentManager.activeTextEditor.selection.anchor.line + 1; + this._selectedText = documentManager.activeTextEditor.document.getText( + new Range( + documentManager.activeTextEditor.selection.start, + documentManager.activeTextEditor.selection.end + ) + ); + } + this._execPath = process.execPath; + Object.keys(process.env).forEach((key) => { + ((this as any) as Record)[`env:${key}`] = ((this as any) as Record< + string, + string | undefined + >)[`env.${key}`] = process.env[key]; + }); + } + + public get cwd(): string { + return this.workspaceFolder; + } + + public get workspaceRoot(): string { + return this._workspaceFolder; + } + + public get workspaceFolder(): string { + return this._workspaceFolder; + } + + public get workspaceRootFolderName(): string { + return this._workspaceFolderName; + } + + public get workspaceFolderBasename(): string { + return this._workspaceFolderName; + } + + public get file(): string | undefined { + return this._filePath; + } + + public get relativeFile(): string | undefined { + return this.file ? Path.relative(this._workspaceFolder, this.file) : undefined; + } + + public get relativeFileDirname(): string | undefined { + return this.relativeFile ? Path.dirname(this.relativeFile) : undefined; + } + + public get fileBasename(): string | undefined { + return this.file ? Path.basename(this.file) : undefined; + } + + public get fileBasenameNoExtension(): string | undefined { + return this.file ? Path.parse(this.file).name : undefined; + } + + public get fileDirname(): string | undefined { + return this.file ? Path.dirname(this.file) : undefined; + } + + public get fileExtname(): string | undefined { + return this.file ? Path.extname(this.file) : undefined; + } + + public get lineNumber(): number | undefined { + return this._lineNumber; + } + + public get selectedText(): string | undefined { + return this._selectedText; + } + + public get execPath(): string { + return this._execPath; + } +} diff --git a/src/client/common/variables/types.ts b/src/client/common/variables/types.ts new file mode 100644 index 000000000000..c1b844451021 --- /dev/null +++ b/src/client/common/variables/types.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event, Uri } from 'vscode'; + +export type EnvironmentVariables = Object & Record; + +export const IEnvironmentVariablesService = Symbol('IEnvironmentVariablesService'); + +export interface IEnvironmentVariablesService { + parseFile(filePath?: string, baseVars?: EnvironmentVariables): Promise; + mergeVariables(source: EnvironmentVariables, target: EnvironmentVariables): void; + appendPythonPath(vars: EnvironmentVariables, ...pythonPaths: string[]): void; + appendPath(vars: EnvironmentVariables, ...paths: string[]): void; +} + +/** + * An interface for a JavaScript object that + * acts as a dictionary. The keys are strings. + */ +export interface IStringDictionary { + [name: string]: V; +} + +export interface ISystemVariables { + resolve(value: string): string; + resolve(value: string[]): string[]; + resolve(value: IStringDictionary): IStringDictionary; + resolve(value: IStringDictionary): IStringDictionary; + resolve(value: IStringDictionary>): IStringDictionary>; + resolveAny(value: T): T; + // tslint:disable-next-line:no-any + [key: string]: any; +} + +export const IEnvironmentVariablesProvider = Symbol('IEnvironmentVariablesProvider'); + +export interface IEnvironmentVariablesProvider { + onDidEnvironmentVariablesChange: Event; + getEnvironmentVariables(resource?: Uri): Promise; + getCustomEnvironmentVariables(resource?: Uri): Promise; +} diff --git a/src/client/constants.ts b/src/client/constants.ts new file mode 100644 index 000000000000..55e22eb0e4e3 --- /dev/null +++ b/src/client/constants.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; + +// This file is also used by the debug adapter. +// When bundling, the bundle file for the debug adapter ends up elsewhere. +const folderName = path.basename(__dirname); +export const EXTENSION_ROOT_DIR = + folderName === 'client' ? path.join(__dirname, '..', '..') : path.join(__dirname, '..', '..', '..', '..'); + +export const HiddenFileFormatString = '_HiddenFile_{0}.py'; +export const HiddenFilePrefix = '_HiddenFile_'; + +export const MillisecondsInADay = 24 * 60 * 60 * 1_000; diff --git a/src/client/datascience/activation.ts b/src/client/datascience/activation.ts new file mode 100644 index 000000000000..ccf319aa9b5c --- /dev/null +++ b/src/client/datascience/activation.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../activation/types'; +import '../common/extensions'; +import { IPythonDaemonExecutionService, IPythonExecutionFactory } from '../common/process/types'; +import { IDisposableRegistry } from '../common/types'; +import { debounceAsync, swallowExceptions } from '../common/utils/decorators'; +import { sendTelemetryEvent, setSharedProperty } from '../telemetry'; +import { JupyterDaemonModule, Telemetry } from './constants'; +import { ActiveEditorContextService } from './context/activeEditorContext'; +import { JupyterInterpreterService } from './jupyter/interpreter/jupyterInterpreterService'; +import { KernelDaemonPreWarmer } from './kernel-launcher/kernelDaemonPreWarmer'; +import { INotebookAndInteractiveWindowUsageTracker, INotebookEditor, INotebookEditorProvider } from './types'; + +@injectable() +export class Activation implements IExtensionSingleActivationService { + private notebookOpened = false; + constructor( + @inject(INotebookEditorProvider) private readonly notebookEditorProvider: INotebookEditorProvider, + @inject(JupyterInterpreterService) private readonly jupyterInterpreterService: JupyterInterpreterService, + @inject(IPythonExecutionFactory) private readonly factory: IPythonExecutionFactory, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(ActiveEditorContextService) private readonly contextService: ActiveEditorContextService, + @inject(KernelDaemonPreWarmer) private readonly daemonPoolPrewarmer: KernelDaemonPreWarmer, + @inject(INotebookAndInteractiveWindowUsageTracker) + private readonly tracker: INotebookAndInteractiveWindowUsageTracker + ) {} + public async activate(): Promise { + this.disposables.push(this.notebookEditorProvider.onDidOpenNotebookEditor(this.onDidOpenNotebookEditor, this)); + this.disposables.push(this.jupyterInterpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter, this)); + this.contextService.activate().ignoreErrors(); + this.daemonPoolPrewarmer.activate(undefined).ignoreErrors(); + this.tracker.startTracking(); + } + + private onDidOpenNotebookEditor(e: INotebookEditor) { + this.notebookOpened = true; + this.PreWarmDaemonPool().ignoreErrors(); + setSharedProperty('ds_notebookeditor', e.type); + sendTelemetryEvent(Telemetry.OpenNotebookAll); + // Warm up our selected interpreter for the extension + this.jupyterInterpreterService.setInitialInterpreter().ignoreErrors(); + } + + private onDidChangeInterpreter() { + if (this.notebookOpened) { + // Warm up our selected interpreter for the extension + this.jupyterInterpreterService.setInitialInterpreter().ignoreErrors(); + this.PreWarmDaemonPool().ignoreErrors(); + } + } + + @debounceAsync(500) + @swallowExceptions('Failed to pre-warm daemon pool') + private async PreWarmDaemonPool() { + const interpreter = await this.jupyterInterpreterService.getSelectedInterpreter(); + if (!interpreter) { + return; + } + await this.factory.createDaemon({ + daemonModule: JupyterDaemonModule, + pythonPath: interpreter.path + }); + } +} diff --git a/src/client/datascience/baseJupyterSession.ts b/src/client/datascience/baseJupyterSession.ts new file mode 100644 index 000000000000..2b3618354a1b --- /dev/null +++ b/src/client/datascience/baseJupyterSession.ts @@ -0,0 +1,523 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { Kernel, KernelMessage, Session } from '@jupyterlab/services'; +import type { JSONObject } from '@phosphor/coreutils'; +import type { Slot } from '@phosphor/signaling'; +import { Observable } from 'rxjs/Observable'; +import { ReplaySubject } from 'rxjs/ReplaySubject'; +import { Event, EventEmitter } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { ServerStatus } from '../../datascience-ui/interactive-common/mainState'; +import { traceError, traceInfo, traceWarning } from '../common/logger'; +import { sleep, waitForPromise } from '../common/utils/async'; +import * as localize from '../common/utils/localize'; +import { noop } from '../common/utils/misc'; +import { sendTelemetryEvent } from '../telemetry'; +import { Identifiers, Telemetry } from './constants'; +import { JupyterInvalidKernelError } from './jupyter/jupyterInvalidKernelError'; +import { JupyterWaitForIdleError } from './jupyter/jupyterWaitForIdleError'; +import { kernelConnectionMetadataHasKernelSpec } from './jupyter/kernels/helpers'; +import { JupyterKernelPromiseFailedError } from './jupyter/kernels/jupyterKernelPromiseFailedError'; +import { KernelConnectionMetadata } from './jupyter/kernels/types'; +import { suppressShutdownErrors } from './raw-kernel/rawKernel'; +import { IJupyterSession, ISessionWithSocket, KernelSocketInformation } from './types'; + +/** + * Exception raised when starting a Jupyter Session fails. + * + * @export + * @class JupyterSessionStartError + * @extends {Error} + */ +export class JupyterSessionStartError extends Error { + constructor(originalException: Error) { + super(originalException.message); + this.stack = originalException.stack; + sendTelemetryEvent(Telemetry.StartSessionFailedJupyter); + } +} + +export abstract class BaseJupyterSession implements IJupyterSession { + protected get session(): ISessionWithSocket | undefined { + return this._session; + } + protected kernelConnectionMetadata?: KernelConnectionMetadata; + public get kernelSocket(): Observable { + return this._kernelSocket; + } + private get jupyterLab(): undefined | typeof import('@jupyterlab/services') { + if (!this._jupyterLab) { + // tslint:disable-next-line:no-require-imports + this._jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); // NOSONAR + } + return this._jupyterLab; + } + + public get onSessionStatusChanged(): Event { + if (!this.onStatusChangedEvent) { + this.onStatusChangedEvent = new EventEmitter(); + } + return this.onStatusChangedEvent.event; + } + + public get status(): ServerStatus { + return this.getServerStatus(); + } + + public get isConnected(): boolean { + return this.connected; + } + public get onIoPubMessage() { + return this.ioPubEventEmitter.event; + } + protected onStatusChangedEvent: EventEmitter = new EventEmitter(); + protected statusHandler: Slot; + protected connected: boolean = false; + protected restartSessionPromise: Promise | undefined; + private _session: ISessionWithSocket | undefined; + private _kernelSocket = new ReplaySubject(); + private _jupyterLab?: typeof import('@jupyterlab/services'); + private ioPubEventEmitter = new EventEmitter(); + private ioPubHandler: Slot; + + constructor(private restartSessionUsed: (id: Kernel.IKernelConnection) => void, public workingDirectory: string) { + this.statusHandler = this.onStatusChanged.bind(this); + this.ioPubHandler = (_s, m) => this.ioPubEventEmitter.fire(m); + } + public dispose(): Promise { + return this.shutdown(); + } + // Abstracts for each Session type to implement + public abstract async waitForIdle(timeout: number): Promise; + + public async shutdown(): Promise { + if (this.session) { + try { + traceInfo('Shutdown session - current session'); + await this.shutdownSession(this.session, this.statusHandler); + traceInfo('Shutdown session - get restart session'); + if (this.restartSessionPromise) { + const restartSession = await this.restartSessionPromise; + traceInfo('Shutdown session - shutdown restart session'); + await this.shutdownSession(restartSession, undefined); + } + } catch { + noop(); + } + this.setSession(undefined); + this.restartSessionPromise = undefined; + } + if (this.onStatusChangedEvent) { + this.onStatusChangedEvent.dispose(); + } + traceInfo('Shutdown session -- complete'); + } + public async interrupt(timeout: number): Promise { + if (this.session && this.session.kernel) { + // Listen for session status changes + this.session.statusChanged.connect(this.statusHandler); + + await this.waitForKernelPromise( + this.session.kernel.interrupt(), + timeout, + localize.DataScience.interruptingKernelFailed() + ); + } + } + + public async changeKernel(kernelConnection: KernelConnectionMetadata, timeoutMS: number): Promise { + let newSession: ISessionWithSocket | undefined; + + // If we are already using this kernel in an active session just return back + const currentKernelSpec = + this.kernelConnectionMetadata && kernelConnectionMetadataHasKernelSpec(this.kernelConnectionMetadata) + ? this.kernelConnectionMetadata.kernelSpec + : undefined; + const kernelSpecToUse = kernelConnectionMetadataHasKernelSpec(kernelConnection) + ? kernelConnection.kernelSpec + : undefined; + if (this.session && currentKernelSpec && kernelSpecToUse) { + // Name and id have to match (id is only for active sessions) + if (currentKernelSpec.name === kernelSpecToUse.name && currentKernelSpec.id === kernelSpecToUse.id) { + return; + } + } + + newSession = await this.createNewKernelSession(kernelConnection, timeoutMS); + + // This is just like doing a restart, kill the old session (and the old restart session), and start new ones + if (this.session) { + this.shutdownSession(this.session, this.statusHandler).ignoreErrors(); + this.restartSessionPromise?.then((r) => this.shutdownSession(r, undefined)).ignoreErrors(); // NOSONAR + } + + // Update our kernel connection metadata. + this.kernelConnectionMetadata = kernelConnection; + + // Save the new session + this.setSession(newSession); + + // Listen for session status changes + this.session?.statusChanged.connect(this.statusHandler); // NOSONAR + + // Start the restart session promise too. + this.restartSessionPromise = this.createRestartSession(kernelConnection, newSession); + } + + public async restart(_timeout: number): Promise { + if (this.session?.isRemoteSession) { + await this.session.kernel.restart(); + return; + } + + // Start the restart session now in case it wasn't started + if (!this.restartSessionPromise) { + this.startRestartSession(); + } + + // Just kill the current session and switch to the other + if (this.restartSessionPromise && this.session) { + traceInfo(`Restarting ${this.session.kernel.id}`); + + // Save old state for shutdown + const oldSession = this.session; + const oldStatusHandler = this.statusHandler; + + // Just switch to the other session. It should already be ready + this.setSession(await this.restartSessionPromise); + if (!this.session) { + throw new Error(localize.DataScience.sessionDisposed()); + } + this.restartSessionUsed(this.session.kernel); + traceInfo(`Got new session ${this.session.kernel.id}`); + + // Rewire our status changed event. + this.session.statusChanged.connect(this.statusHandler); + + // After switching, start another in case we restart again. + this.restartSessionPromise = this.createRestartSession(this.kernelConnectionMetadata, oldSession); + traceInfo('Started new restart session'); + if (oldStatusHandler) { + oldSession.statusChanged.disconnect(oldStatusHandler); + } + this.shutdownSession(oldSession, undefined).ignoreErrors(); + } else { + throw new Error(localize.DataScience.sessionDisposed()); + } + } + + public requestExecute( + content: KernelMessage.IExecuteRequestMsg['content'], + disposeOnDone?: boolean, + metadata?: JSONObject + ): Kernel.IShellFuture | undefined { + const promise = + this.session && this.session.kernel + ? this.session.kernel.requestExecute(content, disposeOnDone, metadata) + : undefined; + + // It has been observed that starting the restart session slows down first time to execute a cell. + // Solution is to start the restart session after the first execution of user code. + if (promise) { + promise.done.finally(() => this.startRestartSession()).catch(noop); + } + return promise; + } + + public requestInspect( + content: KernelMessage.IInspectRequestMsg['content'] + ): Promise { + return this.session && this.session.kernel + ? this.session.kernel.requestInspect(content) + : Promise.resolve(undefined); + } + + public requestComplete( + content: KernelMessage.ICompleteRequestMsg['content'] + ): Promise { + return this.session && this.session.kernel + ? this.session.kernel.requestComplete(content) + : Promise.resolve(undefined); + } + + public sendInputReply(content: string) { + if (this.session && this.session.kernel) { + // tslint:disable-next-line: no-any + this.session.kernel.sendInputReply({ value: content, status: 'ok' }); + } + } + + public registerCommTarget( + targetName: string, + callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike + ) { + if (this.session && this.session.kernel) { + this.session.kernel.registerCommTarget(targetName, callback); + } else { + throw new Error(localize.DataScience.sessionDisposed()); + } + } + + public sendCommMessage( + buffers: (ArrayBuffer | ArrayBufferView)[], + content: { comm_id: string; data: JSONObject; target_name: string | undefined }, + // tslint:disable-next-line: no-any + metadata: any, + // tslint:disable-next-line: no-any + msgId: any + ): Kernel.IShellFuture< + KernelMessage.IShellMessage<'comm_msg'>, + KernelMessage.IShellMessage + > { + if (this.session && this.session.kernel && this.jupyterLab) { + const shellMessage = this.jupyterLab.KernelMessage.createMessage>({ + // tslint:disable-next-line: no-any + msgType: 'comm_msg', + channel: 'shell', + buffers, + content, + metadata, + msgId, + session: this.session.kernel.clientId, + username: this.session.kernel.username + }); + + return this.session.kernel.sendShellMessage(shellMessage, false, true); + } else { + throw new Error(localize.DataScience.sessionDisposed()); + } + } + + public requestCommInfo( + content: KernelMessage.ICommInfoRequestMsg['content'] + ): Promise { + if (this.session?.kernel) { + return this.session.kernel.requestCommInfo(content); + } else { + throw new Error(localize.DataScience.sessionDisposed()); + } + } + public registerMessageHook( + msgId: string, + hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void { + if (this.session?.kernel) { + return this.session.kernel.registerMessageHook(msgId, hook); + } else { + throw new Error(localize.DataScience.sessionDisposed()); + } + } + public removeMessageHook( + msgId: string, + hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void { + if (this.session?.kernel) { + return this.session.kernel.removeMessageHook(msgId, hook); + } else { + throw new Error(localize.DataScience.sessionDisposed()); + } + } + + // Sub classes need to implement their own restarting specific code + protected abstract startRestartSession(): void; + protected abstract async createRestartSession( + kernelConnection: KernelConnectionMetadata | undefined, + session: ISessionWithSocket, + cancelToken?: CancellationToken + ): Promise; + + // Sub classes need to implement their own kernel change specific code + protected abstract createNewKernelSession( + kernelConnection: KernelConnectionMetadata, + timeoutMS: number + ): Promise; + + protected async waitForIdleOnSession(session: ISessionWithSocket | undefined, timeout: number): Promise { + if (session && session.kernel) { + traceInfo(`Waiting for idle on (kernel): ${session.kernel.id} -> ${session.kernel.status}`); + // tslint:disable-next-line: no-any + const statusHandler = (resolve: () => void, reject: (exc: any) => void, e: Kernel.Status | undefined) => { + if (e === 'idle') { + resolve(); + } else if (e === 'dead') { + traceError('Kernel died while waiting for idle'); + // If we throw an exception, make sure to shutdown the session as it's not usable anymore + this.shutdownSession(session, this.statusHandler).ignoreErrors(); + const kernelModel = { + ...session.kernel, + lastActivityTime: new Date(), + numberOfConnections: 0, + session: session.model + }; + reject( + new JupyterInvalidKernelError({ + kernelModel, + kind: 'connectToLiveKernel' + }) + ); + } + }; + + let statusChangeHandler: Slot | undefined; + const kernelStatusChangedPromise = new Promise((resolve, reject) => { + statusChangeHandler = (_: ISessionWithSocket, e: Kernel.Status) => statusHandler(resolve, reject, e); + session.statusChanged.connect(statusChangeHandler); + }); + let kernelChangedHandler: Slot | undefined; + const statusChangedPromise = new Promise((resolve, reject) => { + kernelChangedHandler = (_: ISessionWithSocket, e: Session.IKernelChangedArgs) => + statusHandler(resolve, reject, e.newValue?.status); + session.kernelChanged.connect(kernelChangedHandler); + }); + const checkStatusPromise = new Promise(async (resolve) => { + // This function seems to cause CI builds to timeout randomly on + // different tests. Waiting for status to go idle doesn't seem to work and + // in the past, waiting on the ready promise doesn't work either. Check status with a maximum of 5 seconds + const startTime = Date.now(); + while ( + session && + session.kernel && + session.kernel.status !== 'idle' && + Date.now() - startTime < timeout + ) { + await sleep(100); + } + resolve(); + }); + await Promise.race([kernelStatusChangedPromise, statusChangedPromise, checkStatusPromise]); + traceInfo(`Finished waiting for idle on (kernel): ${session.kernel.id} -> ${session.kernel.status}`); + + if (statusChangeHandler && session && session.statusChanged) { + session.statusChanged.disconnect(statusChangeHandler); + } + if (kernelChangedHandler && session && session.kernelChanged) { + session.kernelChanged.disconnect(kernelChangedHandler); + } + + // If we didn't make it out in ten seconds, indicate an error + if (session.kernel && session.kernel.status === 'idle') { + // So that we don't have problems with ipywidgets, always register the default ipywidgets comm target. + // Restart sessions and retries might make this hard to do correctly otherwise. + session.kernel.registerCommTarget(Identifiers.DefaultCommTarget, noop); + + return; + } + + // If we throw an exception, make sure to shutdown the session as it's not usable anymore + this.shutdownSession(session, this.statusHandler).ignoreErrors(); + throw new JupyterWaitForIdleError(localize.DataScience.jupyterLaunchTimedOut()); + } + } + + // Changes the current session. + protected setSession(session: ISessionWithSocket | undefined) { + const oldSession = this._session; + if (this.ioPubHandler && oldSession) { + oldSession.iopubMessage.disconnect(this.ioPubHandler); + } + this._session = session; + if (session) { + session.iopubMessage.connect(this.ioPubHandler); + } + + // If we have a new session, then emit the new kernel connection information. + if (session && oldSession !== session) { + if (!session.kernelSocketInformation) { + traceError(`Unable to find WebSocket connection associated with kernel ${session.kernel.id}`); + this._kernelSocket.next(undefined); + } else { + this._kernelSocket.next({ + options: { + clientId: session.kernel.clientId, + id: session.kernel.id, + model: { ...session.kernel.model }, + userName: session.kernel.username + }, + socket: session.kernelSocketInformation.socket + }); + } + } + } + protected async shutdownSession( + session: ISessionWithSocket | undefined, + statusHandler: Slot | undefined + ): Promise { + if (session && session.kernel) { + const kernelId = session.kernel.id; + traceInfo(`shutdownSession ${kernelId} - start`); + try { + if (statusHandler) { + session.statusChanged.disconnect(statusHandler); + } + // Do not shutdown remote sessions. + if (session.isRemoteSession) { + session.dispose(); + return; + } + try { + suppressShutdownErrors(session.kernel); + // Shutdown may fail if the process has been killed + if (!session.isDisposed) { + await waitForPromise(session.shutdown(), 1000); + } + } catch { + noop(); + } + if (session && !session.isDisposed) { + session.dispose(); + } + } catch (e) { + // Ignore, just trace. + traceWarning(e); + } + traceInfo(`shutdownSession ${kernelId} - shutdown complete`); + } + } + private getServerStatus(): ServerStatus { + if (this.session) { + switch (this.session.kernel.status) { + case 'busy': + return ServerStatus.Busy; + case 'dead': + return ServerStatus.Dead; + case 'idle': + case 'connected': + return ServerStatus.Idle; + case 'restarting': + case 'autorestarting': + case 'reconnecting': + return ServerStatus.Restarting; + case 'starting': + return ServerStatus.Starting; + default: + return ServerStatus.NotStarted; + } + } + + return ServerStatus.NotStarted; + } + + private async waitForKernelPromise( + kernelPromise: Promise, + timeout: number, + errorMessage: string + ): Promise { + // Wait for this kernel promise to happen + try { + await waitForPromise(kernelPromise, timeout); + } catch (e) { + if (!e) { + // We timed out. Throw a specific exception + throw new JupyterKernelPromiseFailedError(errorMessage); + } + throw e; + } + } + + private onStatusChanged(_s: Session.ISession) { + if (this.onStatusChangedEvent) { + this.onStatusChangedEvent.fire(this.getServerStatus()); + } + } +} diff --git a/src/client/datascience/cellFactory.ts b/src/client/datascience/cellFactory.ts new file mode 100644 index 000000000000..b8df01e41317 --- /dev/null +++ b/src/client/datascience/cellFactory.ts @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../common/extensions'; + +import * as uuid from 'uuid/v4'; +import { Range, TextDocument, Uri } from 'vscode'; + +import { parseForComments } from '../../datascience-ui/common'; +import { createCodeCell, createMarkdownCell } from '../../datascience-ui/common/cellFactory'; +import { IDataScienceSettings, Resource } from '../common/types'; +import { noop } from '../common/utils/misc'; +import { CellMatcher } from './cellMatcher'; +import { Identifiers } from './constants'; +import { CellState, ICell, ICellRange } from './types'; + +function generateCodeCell( + code: string[], + file: string, + line: number, + id: string, + magicCommandsAsComments: boolean +): ICell { + // Code cells start out with just source and no outputs. + return { + data: createCodeCell(code, magicCommandsAsComments), + id: id, + file: file, + line: line, + state: CellState.init + }; +} + +function generateMarkdownCell(code: string[], file: string, line: number, id: string): ICell { + return { + id: id, + file: file, + line: line, + state: CellState.finished, + data: createMarkdownCell(code) + }; +} + +export function getCellResource(cell: ICell): Resource { + if (cell.file !== Identifiers.EmptyFileName) { + return Uri.file(cell.file); + } + return undefined; +} + +export function generateCells( + settings: IDataScienceSettings | undefined, + code: string, + file: string, + line: number, + splitMarkdown: boolean, + id: string +): ICell[] { + // Determine if we have a markdown cell/ markdown and code cell combined/ or just a code cell + const split = code.splitLines({ trim: false }); + const firstLine = split[0]; + const matcher = new CellMatcher(settings); + const { magicCommandsAsComments = false } = settings || {}; + if (matcher.isMarkdown(firstLine)) { + // We have at least one markdown. We might have to split it if there any lines that don't begin + // with # or are inside a multiline comment + let firstNonMarkdown = -1; + parseForComments( + split, + (_s, _i) => noop(), + (s, i) => { + // Make sure there's actually some code. + if (s && s.length > 0 && firstNonMarkdown === -1) { + firstNonMarkdown = splitMarkdown ? i : -1; + } + } + ); + if (firstNonMarkdown >= 0) { + // Make sure if we split, the second cell has a new id. It's a new submission. + return [ + generateMarkdownCell(split.slice(0, firstNonMarkdown), file, line, id), + generateCodeCell( + split.slice(firstNonMarkdown), + file, + line + firstNonMarkdown, + uuid(), + magicCommandsAsComments + ) + ]; + } else { + // Just a single markdown cell + return [generateMarkdownCell(split, file, line, id)]; + } + } else { + // Just code + return [generateCodeCell(split, file, line, id, magicCommandsAsComments)]; + } +} + +export function hasCells(document: TextDocument, settings?: IDataScienceSettings): boolean { + const matcher = new CellMatcher(settings); + for (let index = 0; index < document.lineCount; index += 1) { + const line = document.lineAt(index); + if (matcher.isCell(line.text)) { + return true; + } + } + + return false; +} + +export function generateCellsFromString(source: string, settings?: IDataScienceSettings): ICell[] { + const lines: string[] = source.splitLines({ trim: false, removeEmptyEntries: false }); + + // Find all the lines that start a cell + const matcher = new CellMatcher(settings); + const starts: { startLine: number; title: string; code: string; cell_type: string }[] = []; + let currentCode: string | undefined; + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + if (matcher.isCell(line)) { + if (starts.length > 0 && currentCode) { + const previousCell = starts[starts.length - 1]; + previousCell.code = currentCode; + } + const results = matcher.exec(line); + if (results !== undefined) { + starts.push({ + startLine: index + 1, + title: results, + cell_type: matcher.getCellType(line), + code: '' + }); + } + currentCode = undefined; + } + currentCode = currentCode ? `${currentCode}\n${line}` : line; + } + + if (starts.length >= 1 && currentCode) { + const previousCell = starts[starts.length - 1]; + previousCell.code = currentCode; + } + + // For each one, get its text and turn it into a cell + return Array.prototype.concat( + ...starts.map((s) => { + return generateCells(settings, s.code, '', s.startLine, false, uuid()); + }) + ); +} + +export function generateCellRangesFromDocument(document: TextDocument, settings?: IDataScienceSettings): ICellRange[] { + // Implmentation of getCells here based on Don's Jupyter extension work + const matcher = new CellMatcher(settings); + const cells: ICellRange[] = []; + for (let index = 0; index < document.lineCount; index += 1) { + const line = document.lineAt(index); + if (matcher.isCell(line.text)) { + if (cells.length > 0) { + const previousCell = cells[cells.length - 1]; + previousCell.range = new Range(previousCell.range.start, document.lineAt(index - 1).range.end); + } + + const results = matcher.exec(line.text); + if (results !== undefined) { + cells.push({ + range: line.range, + title: results, + cell_type: matcher.getCellType(line.text) + }); + } + } + } + + if (cells.length >= 1) { + const line = document.lineAt(document.lineCount - 1); + const previousCell = cells[cells.length - 1]; + previousCell.range = new Range(previousCell.range.start, line.range.end); + } + + return cells; +} + +export function generateCellsFromDocument(document: TextDocument, settings?: IDataScienceSettings): ICell[] { + const ranges = generateCellRangesFromDocument(document, settings); + + // For each one, get its text and turn it into a cell + return Array.prototype.concat( + ...ranges.map((cr) => { + const code = document.getText(cr.range); + return generateCells(settings, code, '', cr.range.start.line, false, uuid()); + }) + ); +} diff --git a/src/client/datascience/cellMatcher.ts b/src/client/datascience/cellMatcher.ts new file mode 100644 index 000000000000..1a6a61848983 --- /dev/null +++ b/src/client/datascience/cellMatcher.ts @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../common/extensions'; + +import { IDataScienceSettings } from '../common/types'; +import { noop } from '../common/utils/misc'; +import { RegExpValues } from './constants'; + +export class CellMatcher { + public codeExecRegEx: RegExp; + public markdownExecRegEx: RegExp; + + private codeMatchRegEx: RegExp; + private markdownMatchRegEx: RegExp; + private defaultCellMarker: string; + private defaultCellMarkerExec: RegExp; + + constructor(settings?: IDataScienceSettings) { + this.codeMatchRegEx = this.createRegExp( + settings ? settings.codeRegularExpression : undefined, + RegExpValues.PythonCellMarker + ); + this.markdownMatchRegEx = this.createRegExp( + settings ? settings.markdownRegularExpression : undefined, + RegExpValues.PythonMarkdownCellMarker + ); + this.codeExecRegEx = new RegExp(`${this.codeMatchRegEx.source}(.*)`); + this.markdownExecRegEx = new RegExp(`${this.markdownMatchRegEx.source}(.*)`); + this.defaultCellMarker = settings?.defaultCellMarker ? settings.defaultCellMarker : '# %%'; + this.defaultCellMarkerExec = this.createRegExp(`${this.defaultCellMarker}(.*)`, /# %%(.*)/); + } + + public isCell(code: string): boolean { + return this.isCode(code) || this.isMarkdown(code); + } + + public isMarkdown(code: string): boolean { + return this.markdownMatchRegEx.test(code); + } + + public isCode(code: string): boolean { + return this.codeMatchRegEx.test(code) || code.trim() === this.defaultCellMarker; + } + + public getCellType(code: string): string { + return this.isMarkdown(code) ? 'markdown' : 'code'; + } + + public stripFirstMarker(code: string): string { + const lines = code.splitLines({ trim: false, removeEmptyEntries: false }); + + // Only strip this off the first line. Otherwise we want the markers in the code. + if (lines.length > 0 && (this.isCode(lines[0]) || this.isMarkdown(lines[0]))) { + return lines.slice(1).join('\n'); + } + return code; + } + + public exec(code: string): string | undefined { + let result: RegExpExecArray | null = null; + if (this.defaultCellMarkerExec.test(code)) { + this.defaultCellMarkerExec.lastIndex = -1; + result = this.defaultCellMarkerExec.exec(code); + } else if (this.codeMatchRegEx.test(code)) { + this.codeExecRegEx.lastIndex = -1; + result = this.codeExecRegEx.exec(code); + } else if (this.markdownMatchRegEx.test(code)) { + this.markdownExecRegEx.lastIndex = -1; + result = this.markdownExecRegEx.exec(code); + } + if (result) { + return result.length > 1 ? result[result.length - 1].trim() : ''; + } + return undefined; + } + + private createRegExp(potential: string | undefined, backup: RegExp): RegExp { + try { + if (potential) { + return new RegExp(potential); + } + } catch { + noop(); + } + + return backup; + } +} diff --git a/src/client/datascience/codeCssGenerator.ts b/src/client/datascience/codeCssGenerator.ts new file mode 100644 index 000000000000..c92380695dbb --- /dev/null +++ b/src/client/datascience/codeCssGenerator.ts @@ -0,0 +1,518 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { JSONArray, JSONObject } from '@phosphor/coreutils'; +import { inject, injectable } from 'inversify'; +import { parse } from 'jsonc-parser'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as path from 'path'; +import { IWorkspaceService } from '../common/application/types'; +import { traceError, traceInfo, traceWarning } from '../common/logger'; + +import { IConfigurationService, Resource } from '../common/types'; +import { DefaultTheme } from './constants'; +import { ICodeCssGenerator, IDataScienceFileSystem, IThemeFinder } from './types'; + +// tslint:disable:no-any +const DarkTheme = 'dark'; +const LightTheme = 'light'; + +const MonacoColorRegEx = /^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/; +const ThreeColorRegEx = /^#?([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])$/; + +// These are based on the colors generated by 'Default Light+' and are only set when we +// are ignoring themes. +//tslint:disable:no-multiline-string object-literal-key-quotes +const DefaultCssVars: { [key: string]: string } = { + light: ` + :root { + --override-widget-background: #f3f3f3; + --override-foreground: #000000; + --override-background: #FFFFFF; + --override-selection-background: #add6ff; + --override-watermark-color: rgba(66, 66, 66, 0.75); + --override-tabs-background: #f3f3f3; + --override-progress-background: #0066bf; + --override-badge-background: #c4c4c4; + --override-lineHighlightBorder: #eeeeee; + --override-peek-background: #f2f8fc; + } +`, + dark: ` + :root { + --override-widget-background: #1e1e1e; + --override-foreground: #d4d4d4; + --override-background: #1e1e1e; + --override-selection-background: #264f78; + --override-watermark-color: rgba(231, 231, 231, 0.6); + --override-tabs-background: #252526; + --override-progress-background: #0066bf; + --override-badge-background: #4d4d4d; + --override-lineHighlightBorder: #282828; + --override-peek-background: #001f33; + } +` +}; + +// These colors below should match colors that come from either the Default Light+ theme or the Default Dark+ theme. +// They are used when we can't find a theme json file. +const DefaultColors: { [key: string]: string } = { + 'light.comment': '#008000', + 'light.constant.numeric': '#09885a', + 'light.string': '#a31515', + 'light.keyword.control': '#AF00DB', + 'light.keyword.operator': '#000000', + 'light.variable': '#001080', + 'light.entity.name.type': '#267f99', + 'light.support.function': '#795E26', + 'light.punctuation': '#000000', + 'dark.comment': '#6A9955', + 'dark.constant.numeric': '#b5cea8', + 'dark.string': '#ce9178', + 'dark.keyword.control': '#C586C0', + 'dark.keyword.operator': '#d4d4d4', + 'dark.variable': '#9CDCFE', + 'dark.entity.name.type': '#4EC9B0', + 'dark.support.function': '#DCDCAA', + 'dark.punctuation': '#1e1e1e' +}; + +interface IApplyThemeArgs { + tokenColors?: JSONArray | null; + baseColors?: JSONObject | null; + fontFamily: string; + fontSize: number; + isDark: boolean; + defaultStyle: string | undefined; +} + +// This class generates css using the current theme in order to colorize code. +// +// NOTE: This is all a big hack. It's relying on the theme json files to have a certain format +// in order for this to work. +// See this vscode issue for the real way we think this should happen: +// https://github.com/Microsoft/vscode/issues/32813 +@injectable() +export class CodeCssGenerator implements ICodeCssGenerator { + constructor( + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IThemeFinder) private themeFinder: IThemeFinder, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem + ) {} + + public generateThemeCss(resource: Resource, isDark: boolean, theme: string): Promise { + return this.applyThemeData(resource, isDark, theme, '', this.generateCss.bind(this)); + } + + public generateMonacoTheme(resource: Resource, isDark: boolean, theme: string): Promise { + return this.applyThemeData(resource, isDark, theme, {} as any, this.generateMonacoThemeObject.bind(this)); + } + + private async applyThemeData( + resource: Resource, + isDark: boolean, + theme: string, + defaultT: T, + applier: (args: IApplyThemeArgs) => T + ): Promise { + let result = defaultT; + try { + // First compute our current theme. + const ignoreTheme = this.configService.getSettings(resource).datascience.ignoreVscodeTheme ? true : false; + theme = ignoreTheme ? DefaultTheme : theme; + const editor = this.workspaceService.getConfiguration('editor', undefined); + const fontFamily = editor + ? editor.get('fontFamily', "Consolas, 'Courier New', monospace") + : "Consolas, 'Courier New', monospace"; + const fontSize = editor ? editor.get('fontSize', 14) : 14; + const isDarkUpdated = ignoreTheme ? false : isDark; + + // Then we have to find where the theme resources are loaded from + if (theme) { + traceInfo('Searching for token colors ...'); + const tokenColors = await this.findTokenColors(theme); + const baseColors = await this.findBaseColors(theme); + + // The tokens object then contains the necessary data to generate our css + if (tokenColors && fontFamily && fontSize) { + traceInfo('Using colors to generate CSS ...'); + result = applier({ + tokenColors, + baseColors, + fontFamily, + fontSize, + isDark: isDarkUpdated, + defaultStyle: ignoreTheme ? LightTheme : undefined + }); + } else if (tokenColors === null && fontFamily && fontSize) { + // No colors found. See if we can figure out what type of theme we have + const style = isDark ? DarkTheme : LightTheme; + result = applier({ fontFamily, fontSize, isDark: isDarkUpdated, defaultStyle: style }); + } + } + } catch (err) { + // On error don't fail, just log + traceError(err); + } + + return result; + } + + private getScopes(entry: any): JSONArray { + if (entry && entry.scope) { + return Array.isArray(entry.scope) ? (entry.scope as JSONArray) : entry.scope.toString().split(','); + } + return []; + } + + private matchTokenColor(tokenColors: JSONArray, scope: string): number { + return tokenColors.findIndex((entry: any) => { + const scopeArray = this.getScopes(entry); + if (scopeArray.find((v) => v !== null && v !== undefined && v.toString().trim() === scope)) { + return true; + } + return false; + }); + } + + private getScopeStyle = ( + tokenColors: JSONArray | null | undefined, + scope: string, + secondary: string, + defaultStyle: string | undefined + ): { color: string; fontStyle: string } => { + // Search through the scopes on the json object + if (tokenColors) { + let match = this.matchTokenColor(tokenColors, scope); + if (match < 0 && secondary) { + match = this.matchTokenColor(tokenColors, secondary); + } + const found = match >= 0 ? (tokenColors[match] as any) : null; + if (found !== null) { + const settings = found.settings; + if (settings && settings !== null) { + const fontStyle = settings.fontStyle ? settings.fontStyle : 'normal'; + const foreground = settings.foreground ? settings.foreground : 'var(--vscode-editor-foreground)'; + + return { fontStyle, color: foreground }; + } + } + } + + // Default to editor foreground + return { color: this.getDefaultColor(defaultStyle, scope), fontStyle: 'normal' }; + }; + + private getDefaultColor(style: string | undefined, scope: string): string { + return style + ? DefaultColors[`${style}.${scope}`] + : 'var(--override-foreground, var(--vscode-editor-foreground))'; + } + + // tslint:disable-next-line:max-func-body-length + private generateCss(args: IApplyThemeArgs): string { + // There's a set of values that need to be found + const commentStyle = this.getScopeStyle(args.tokenColors, 'comment', 'comment', args.defaultStyle); + const numericStyle = this.getScopeStyle(args.tokenColors, 'constant.numeric', 'constant', args.defaultStyle); + const stringStyle = this.getScopeStyle(args.tokenColors, 'string', 'string', args.defaultStyle); + const variableStyle = this.getScopeStyle(args.tokenColors, 'variable', 'variable', args.defaultStyle); + const entityTypeStyle = this.getScopeStyle( + args.tokenColors, + 'entity.name.type', + 'entity.name.type', + args.defaultStyle + ); + + // Use these values to fill in our format string + return ` +:root { + --code-comment-color: ${commentStyle.color}; + --code-numeric-color: ${numericStyle.color}; + --code-string-color: ${stringStyle.color}; + --code-variable-color: ${variableStyle.color}; + --code-type-color: ${entityTypeStyle.color}; + --code-font-family: ${args.fontFamily}; + --code-font-size: ${args.fontSize}px; +} + +${args.defaultStyle ? DefaultCssVars[args.defaultStyle] : ''} +`; + } + + // Based on this data here: + // https://github.com/Microsoft/vscode/blob/master/src/vs/editor/standalone/common/themes.ts#L13 + // tslint:disable: max-func-body-length + private generateMonacoThemeObject(args: IApplyThemeArgs): monacoEditor.editor.IStandaloneThemeData { + const result: monacoEditor.editor.IStandaloneThemeData = { + base: args.isDark ? 'vs-dark' : 'vs', + inherit: false, + rules: [], + colors: {} + }; + // If we have token colors enumerate them and add them into the rules + if (args.tokenColors && args.tokenColors.length) { + const tokenSet = new Set(); + args.tokenColors.forEach((t: any) => { + const scopes = this.getScopes(t); + const settings = t && t.settings ? t.settings : undefined; + if (scopes && settings) { + scopes.forEach((s) => { + const token = s ? s.toString() : ''; + if (!tokenSet.has(token)) { + tokenSet.add(token); + + if (settings.foreground) { + // Make sure matches the monaco requirements of having 6 values + if (!MonacoColorRegEx.test(settings.foreground)) { + const match = ThreeColorRegEx.exec(settings.foreground); + if (match && match.length > 3) { + settings.foreground = `#${match[1]}${match[1]}${match[2]}${match[2]}${match[3]}${match[3]}`; + } else { + settings.foreground = undefined; + } + } + } + + if (settings.foreground) { + result.rules.push({ + token, + foreground: settings.foreground, + background: settings.background, + fontStyle: settings.fontStyle + }); + } else { + result.rules.push({ + token, + background: settings.background, + fontStyle: settings.fontStyle + }); + } + + // Special case some items. punctuation.definition.comment doesn't seem to + // be listed anywhere. Add it manually when we find a 'comment' + // tslint:disable-next-line: possible-timing-attack + if (token === 'comment') { + result.rules.push({ + token: 'punctuation.definition.comment', + foreground: settings.foreground, + background: settings.background, + fontStyle: settings.fontStyle + }); + } + + // Same for string + // tslint:disable-next-line: possible-timing-attack + if (token === 'string') { + result.rules.push({ + token: 'punctuation.definition.string', + foreground: settings.foreground, + background: settings.background, + fontStyle: settings.fontStyle + }); + } + } + }); + } + }); + + result.rules = result.rules.sort( + (a: monacoEditor.editor.ITokenThemeRule, b: monacoEditor.editor.ITokenThemeRule) => { + return a.token.localeCompare(b.token); + } + ); + } else { + // Otherwise use our default values. + result.base = args.defaultStyle === DarkTheme ? 'vs-dark' : 'vs'; + result.inherit = true; + + if (args.defaultStyle) { + // Special case. We need rules for the comment beginning and the string beginning + result.rules.push({ + token: 'punctuation.definition.comment', + foreground: DefaultColors[`${args.defaultStyle}.comment`] + }); + result.rules.push({ + token: 'punctuation.definition.string', + foreground: DefaultColors[`${args.defaultStyle}.string`] + }); + } + } + // If we have base colors enumerate them and add them to the colors + if (args.baseColors) { + const keys = Object.keys(args.baseColors); + keys.forEach((k) => { + const color = args.baseColors && args.baseColors[k] ? args.baseColors[k] : '#000000'; + result.colors[k] = color ? color.toString() : '#000000'; + }); + } // The else case here should end up inheriting. + return result; + } + + private mergeColors = (colors1: JSONArray, colors2: JSONArray): JSONArray => { + return [...colors1, ...colors2]; + }; + + private mergeBaseColors = (colors1: JSONObject, colors2: JSONObject): JSONObject => { + return { ...colors1, ...colors2 }; + }; + + private readTokenColors = async (themeFile: string): Promise => { + try { + const tokenContent = await this.fs.readLocalFile(themeFile); + const theme = parse(tokenContent); + let tokenColors: JSONArray = []; + + if (typeof theme.tokenColors === 'string') { + const style = await this.fs.readLocalFile(theme.tokenColors); + tokenColors = JSON.parse(style); + } else { + tokenColors = theme.tokenColors as JSONArray; + } + + if (tokenColors && tokenColors.length > 0) { + // This theme may include others. If so we need to combine the two together + const include = theme ? theme.include : undefined; + if (include) { + const includePath = path.join(path.dirname(themeFile), include.toString()); + const includedColors = await this.readTokenColors(includePath); + return this.mergeColors(tokenColors, includedColors); + } + + // Theme is a root, don't need to include others + return tokenColors; + } + + // Might also have a 'settings' object that equates to token colors + const settings = theme.settings as JSONArray; + if (settings && settings.length > 0) { + return settings; + } + + return []; + } catch (e) { + traceError('Python Extension: Error reading custom theme', e); + return []; + } + }; + + private readBaseColors = async (themeFile: string): Promise => { + const tokenContent = await this.fs.readLocalFile(themeFile); + const theme = parse(tokenContent); + const colors = theme.colors as JSONObject; + + // This theme may include others. If so we need to combine the two together + const include = theme ? theme.include : undefined; + if (include) { + const includePath = path.join(path.dirname(themeFile), include.toString()); + const includedColors = await this.readBaseColors(includePath); + return this.mergeBaseColors(colors, includedColors); + } + + // Theme is a root, don't need to include others + return colors; + }; + + private findTokenColors = async (theme: string): Promise => { + try { + traceInfo('Attempting search for colors ...'); + const themeRoot = await this.themeFinder.findThemeRootJson(theme); + + // Use the first result if we have one + if (themeRoot) { + traceInfo(`Loading colors from ${themeRoot} ...`); + + // This should be the path to the file. Load it as a json object + const contents = await this.fs.readLocalFile(themeRoot); + const json = parse(contents); + + // There should be a theme colors section + const contributes = json.contributes as JSONObject; + + // If no contributes section, see if we have a tokenColors section. This means + // this is a direct token colors file + if (!contributes) { + const tokenColors = json.tokenColors as JSONObject; + if (tokenColors) { + return await this.readTokenColors(themeRoot); + } + } + + // This should have a themes section + const themes = contributes.themes as JSONArray; + + // One of these (it's an array), should have our matching theme entry + const index = themes.findIndex((e: any) => { + return e !== null && (e.id === theme || e.name === theme); + }); + + const found = index >= 0 ? (themes[index] as any) : null; + if (found !== null) { + // Then the path entry should contain a relative path to the json file with + // the tokens in it + const themeFile = path.join(path.dirname(themeRoot), found.path); + traceInfo(`Reading colors from ${themeFile}`); + return await this.readTokenColors(themeFile); + } + } else { + traceWarning(`Color theme ${theme} not found. Using default colors.`); + } + } catch (err) { + // Swallow any exceptions with searching or parsing + traceError(err); + } + + // Force the colors to the defaults + return null; + }; + + private findBaseColors = async (theme: string): Promise => { + try { + traceInfo('Attempting search for colors ...'); + const themeRoot = await this.themeFinder.findThemeRootJson(theme); + + // Use the first result if we have one + if (themeRoot) { + traceInfo(`Loading base colors from ${themeRoot} ...`); + + // This should be the path to the file. Load it as a json object + const contents = await this.fs.readLocalFile(themeRoot); + const json = parse(contents); + + // There should be a theme colors section + const contributes = json.contributes as JSONObject; + + // If no contributes section, see if we have a tokenColors section. This means + // this is a direct token colors file + if (!contributes) { + return await this.readBaseColors(themeRoot); + } + + // This should have a themes section + const themes = contributes.themes as JSONArray; + + // One of these (it's an array), should have our matching theme entry + const index = themes.findIndex((e: any) => { + return e !== null && (e.id === theme || e.name === theme); + }); + + const found = index >= 0 ? (themes[index] as any) : null; + if (found !== null) { + // Then the path entry should contain a relative path to the json file with + // the tokens in it + const themeFile = path.join(path.dirname(themeRoot), found.path); + traceInfo(`Reading base colors from ${themeFile}`); + return await this.readBaseColors(themeFile); + } + } else { + traceWarning(`Color theme ${theme} not found. Using default colors.`); + } + } catch (err) { + // Swallow any exceptions with searching or parsing + traceError(err); + } + + // Force the colors to the defaults + return null; + }; +} diff --git a/src/client/datascience/commands/commandLineSelector.ts b/src/client/datascience/commands/commandLineSelector.ts new file mode 100644 index 000000000000..0ea635d5c83d --- /dev/null +++ b/src/client/datascience/commands/commandLineSelector.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ICommandManager } from '../../common/application/types'; +import { IDisposable } from '../../common/types'; +import { Commands } from '../constants'; +import { JupyterCommandLineSelector } from '../jupyter/commandLineSelector'; + +@injectable() +export class JupyterCommandLineSelectorCommand implements IDisposable { + private readonly disposables: IDisposable[] = []; + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(JupyterCommandLineSelector) private readonly commandSelector: JupyterCommandLineSelector + ) {} + public register() { + this.disposables.push( + this.commandManager.registerCommand( + Commands.SelectJupyterCommandLine, + this.commandSelector.selectJupyterCommandLine, + this.commandSelector + ) + ); + } + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } +} diff --git a/src/client/datascience/commands/commandRegistry.ts b/src/client/datascience/commands/commandRegistry.ts new file mode 100644 index 000000000000..ff9c64025dd8 --- /dev/null +++ b/src/client/datascience/commands/commandRegistry.ts @@ -0,0 +1,469 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, multiInject, named, optional } from 'inversify'; +import { CodeLens, ConfigurationTarget, env, Range, Uri } from 'vscode'; +import { ICommandNameArgumentTypeMapping } from '../../common/application/commands'; +import { IApplicationShell, ICommandManager, IDebugService, IDocumentManager } from '../../common/application/types'; +import { Commands as coreCommands } from '../../common/constants'; + +import { IStartPage } from '../../common/startPage/types'; +import { IConfigurationService, IDisposable, IOutputChannel } from '../../common/types'; +import { DataScience } from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { Commands, JUPYTER_OUTPUT_CHANNEL, Telemetry } from '../constants'; +import { + ICodeWatcher, + IDataScienceCodeLensProvider, + IDataScienceCommandListener, + IDataScienceFileSystem, + INotebookEditorProvider +} from '../types'; +import { JupyterCommandLineSelectorCommand } from './commandLineSelector'; +import { ExportCommands } from './exportCommands'; +import { NotebookCommands } from './notebookCommands'; +import { JupyterServerSelectorCommand } from './serverSelector'; + +@injectable() +export class CommandRegistry implements IDisposable { + private readonly disposables: IDisposable[] = []; + constructor( + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IDataScienceCodeLensProvider) private dataScienceCodeLensProvider: IDataScienceCodeLensProvider, + @multiInject(IDataScienceCommandListener) + @optional() + private commandListeners: IDataScienceCommandListener[] | undefined, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(JupyterServerSelectorCommand) private readonly serverSelectedCommand: JupyterServerSelectorCommand, + @inject(NotebookCommands) private readonly notebookCommands: NotebookCommands, + @inject(JupyterCommandLineSelectorCommand) + private readonly commandLineCommand: JupyterCommandLineSelectorCommand, + @inject(INotebookEditorProvider) private notebookEditorProvider: INotebookEditorProvider, + @inject(IDebugService) private debugService: IDebugService, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private jupyterOutput: IOutputChannel, + @inject(IStartPage) private startPage: IStartPage, + @inject(ExportCommands) private readonly exportCommand: ExportCommands, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem + ) { + this.disposables.push(this.serverSelectedCommand); + this.disposables.push(this.notebookCommands); + } + public register() { + this.commandLineCommand.register(); + this.serverSelectedCommand.register(); + this.notebookCommands.register(); + this.exportCommand.register(); + this.registerCommand(Commands.RunAllCells, this.runAllCells); + this.registerCommand(Commands.RunCell, this.runCell); + this.registerCommand(Commands.RunCurrentCell, this.runCurrentCell); + this.registerCommand(Commands.RunCurrentCellAdvance, this.runCurrentCellAndAdvance); + this.registerCommand(Commands.ExecSelectionInInteractiveWindow, this.runSelectionOrLine); + this.registerCommand(Commands.RunAllCellsAbove, this.runAllCellsAbove); + this.registerCommand(Commands.RunCellAndAllBelow, this.runCellAndAllBelow); + this.registerCommand(Commands.InsertCellBelowPosition, this.insertCellBelowPosition); + this.registerCommand(Commands.InsertCellBelow, this.insertCellBelow); + this.registerCommand(Commands.InsertCellAbove, this.insertCellAbove); + this.registerCommand(Commands.DeleteCells, this.deleteCells); + this.registerCommand(Commands.SelectCell, this.selectCell); + this.registerCommand(Commands.SelectCellContents, this.selectCellContents); + this.registerCommand(Commands.ExtendSelectionByCellAbove, this.extendSelectionByCellAbove); + this.registerCommand(Commands.ExtendSelectionByCellBelow, this.extendSelectionByCellBelow); + this.registerCommand(Commands.MoveCellsUp, this.moveCellsUp); + this.registerCommand(Commands.MoveCellsDown, this.moveCellsDown); + this.registerCommand(Commands.ChangeCellToMarkdown, this.changeCellToMarkdown); + this.registerCommand(Commands.ChangeCellToCode, this.changeCellToCode); + this.registerCommand(Commands.GotoNextCellInFile, this.gotoNextCellInFile); + this.registerCommand(Commands.GotoPrevCellInFile, this.gotoPrevCellInFile); + this.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor); + this.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor); + this.registerCommand(Commands.RunToLine, this.runToLine); + this.registerCommand(Commands.RunFromLine, this.runFromLine); + this.registerCommand(Commands.RunFileInInteractiveWindows, this.runFileInteractive); + this.registerCommand(Commands.DebugFileInInteractiveWindows, this.debugFileInteractive); + this.registerCommand(Commands.AddCellBelow, this.addCellBelow); + this.registerCommand(Commands.RunCurrentCellAndAddBelow, this.runCurrentCellAndAddBelow); + this.registerCommand(Commands.DebugCell, this.debugCell); + this.registerCommand(Commands.DebugStepOver, this.debugStepOver); + this.registerCommand(Commands.DebugContinue, this.debugContinue); + this.registerCommand(Commands.DebugStop, this.debugStop); + this.registerCommand(Commands.DebugCurrentCellPalette, this.debugCurrentCellFromCursor); + this.registerCommand(Commands.CreateNewNotebook, this.createNewNotebook); + this.registerCommand(Commands.ViewJupyterOutput, this.viewJupyterOutput); + this.registerCommand(Commands.GatherQuality, this.reportGatherQuality); + this.registerCommand(Commands.LatestExtension, this.openPythonExtensionPage); + this.registerCommand( + Commands.EnableLoadingWidgetsFrom3rdPartySource, + this.enableLoadingWidgetScriptsFromThirdParty + ); + this.registerCommand(coreCommands.OpenStartPage, this.openStartPage); + if (this.commandListeners) { + this.commandListeners.forEach((listener: IDataScienceCommandListener) => { + listener.register(this.commandManager); + }); + } + } + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + private registerCommand< + E extends keyof ICommandNameArgumentTypeMapping, + U extends ICommandNameArgumentTypeMapping[E] + // tslint:disable-next-line: no-any + >(command: E, callback: (...args: U) => any) { + const disposable = this.commandManager.registerCommand(command, callback, this); + this.disposables.push(disposable); + } + + private getCodeWatcher(file: Uri | undefined): ICodeWatcher | undefined { + if (file) { + const possibleDocuments = this.documentManager.textDocuments.filter((d) => + this.fs.arePathsSame(d.uri, file) + ); + if (possibleDocuments && possibleDocuments.length === 1) { + return this.dataScienceCodeLensProvider.getCodeWatcher(possibleDocuments[0]); + } else if (possibleDocuments && possibleDocuments.length > 1) { + throw new Error(DataScience.documentMismatch().format(file.fsPath)); + } + } + + return undefined; + } + + private enableLoadingWidgetScriptsFromThirdParty(): void { + if (this.configService.getSettings(undefined).datascience.widgetScriptSources.length > 0) { + return; + } + // Update the setting and once updated, notify user to restart kernel. + this.configService + .updateSetting( + 'dataScience.widgetScriptSources', + ['jsdelivr.com', 'unpkg.com'], + undefined, + ConfigurationTarget.Global + ) + .then(() => { + // Let user know they'll need to restart the kernel. + this.appShell + .showInformationMessage(DataScience.loadThirdPartyWidgetScriptsPostEnabled()) + .then(noop, noop); + }) + .catch(noop); + } + + private async runAllCells(file: Uri | undefined): Promise { + let codeWatcher = this.getCodeWatcher(file); + if (!codeWatcher) { + codeWatcher = this.getCurrentCodeWatcher(); + } + if (codeWatcher) { + return codeWatcher.runAllCells(); + } else { + return; + } + } + + private async runFileInteractive(file: Uri): Promise { + let codeWatcher = this.getCodeWatcher(file); + if (!codeWatcher) { + codeWatcher = this.getCurrentCodeWatcher(); + } + if (codeWatcher) { + return codeWatcher.runFileInteractive(); + } else { + return; + } + } + + private async debugFileInteractive(file: Uri): Promise { + let codeWatcher = this.getCodeWatcher(file); + if (!codeWatcher) { + codeWatcher = this.getCurrentCodeWatcher(); + } + if (codeWatcher) { + return codeWatcher.debugFileInteractive(); + } else { + return; + } + } + + // Note: see codewatcher.ts where the runcell command args are attached. The reason we don't have any + // objects for parameters is because they can't be recreated when passing them through the LiveShare API + private async runCell( + file: Uri, + startLine: number, + startChar: number, + endLine: number, + endChar: number + ): Promise { + const codeWatcher = this.getCodeWatcher(file); + if (codeWatcher) { + return codeWatcher.runCell(new Range(startLine, startChar, endLine, endChar)); + } + } + + private async runAllCellsAbove(file: Uri, stopLine: number, stopCharacter: number): Promise { + if (file) { + const codeWatcher = this.getCodeWatcher(file); + + if (codeWatcher) { + return codeWatcher.runAllCellsAbove(stopLine, stopCharacter); + } + } + } + + private async runCellAndAllBelow(file: Uri | undefined, startLine: number, startCharacter: number): Promise { + if (file) { + const codeWatcher = this.getCodeWatcher(file); + + if (codeWatcher) { + return codeWatcher.runCellAndAllBelow(startLine, startCharacter); + } + } + } + + private async runToLine(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + const textEditor = this.documentManager.activeTextEditor; + + if (activeCodeWatcher && textEditor && textEditor.selection) { + return activeCodeWatcher.runToLine(textEditor.selection.start.line); + } + } + + private async runFromLine(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + const textEditor = this.documentManager.activeTextEditor; + + if (activeCodeWatcher && textEditor && textEditor.selection) { + return activeCodeWatcher.runFromLine(textEditor.selection.start.line); + } + } + + private async runCurrentCell(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.runCurrentCell(); + } else { + return; + } + } + + private async runCurrentCellAndAdvance(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.runCurrentCellAndAdvance(); + } else { + return; + } + } + + private async runSelectionOrLine(): Promise { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.runSelectionOrLine(this.documentManager.activeTextEditor); + } else { + return; + } + } + + private async debugCell( + file: Uri, + startLine: number, + startChar: number, + endLine: number, + endChar: number + ): Promise { + if (file) { + const codeWatcher = this.getCodeWatcher(file); + + if (codeWatcher) { + return codeWatcher.debugCell(new Range(startLine, startChar, endLine, endChar)); + } + } + } + + @captureTelemetry(Telemetry.DebugStepOver) + private async debugStepOver(): Promise { + // Make sure that we are in debug mode + if (this.debugService.activeDebugSession) { + this.commandManager.executeCommand('workbench.action.debug.stepOver'); + } + } + + @captureTelemetry(Telemetry.DebugStop) + private async debugStop(): Promise { + // Make sure that we are in debug mode + if (this.debugService.activeDebugSession) { + this.commandManager.executeCommand('workbench.action.debug.stop'); + } + } + + @captureTelemetry(Telemetry.DebugContinue) + private async debugContinue(): Promise { + // Make sure that we are in debug mode + if (this.debugService.activeDebugSession) { + this.commandManager.executeCommand('workbench.action.debug.continue'); + } + } + + @captureTelemetry(Telemetry.AddCellBelow) + private async addCellBelow(): Promise { + await this.getCurrentCodeWatcher()?.addEmptyCellToBottom(); + } + + private async runCurrentCellAndAddBelow(): Promise { + this.getCurrentCodeWatcher()?.runCurrentCellAndAddBelow(); + } + + private async insertCellBelowPosition(): Promise { + this.getCurrentCodeWatcher()?.insertCellBelowPosition(); + } + + private async insertCellBelow(): Promise { + this.getCurrentCodeWatcher()?.insertCellBelow(); + } + + private async insertCellAbove(): Promise { + this.getCurrentCodeWatcher()?.insertCellAbove(); + } + + private async deleteCells(): Promise { + this.getCurrentCodeWatcher()?.deleteCells(); + } + + private async selectCell(): Promise { + this.getCurrentCodeWatcher()?.selectCell(); + } + + private async selectCellContents(): Promise { + this.getCurrentCodeWatcher()?.selectCellContents(); + } + + private async extendSelectionByCellAbove(): Promise { + this.getCurrentCodeWatcher()?.extendSelectionByCellAbove(); + } + + private async extendSelectionByCellBelow(): Promise { + this.getCurrentCodeWatcher()?.extendSelectionByCellBelow(); + } + + private async moveCellsUp(): Promise { + this.getCurrentCodeWatcher()?.moveCellsUp(); + } + + private async moveCellsDown(): Promise { + this.getCurrentCodeWatcher()?.moveCellsDown(); + } + + private async changeCellToMarkdown(): Promise { + this.getCurrentCodeWatcher()?.changeCellToMarkdown(); + } + + private async changeCellToCode(): Promise { + this.getCurrentCodeWatcher()?.changeCellToCode(); + } + + private async gotoNextCellInFile(): Promise { + this.getCurrentCodeWatcher()?.gotoNextCell(); + } + + private async gotoPrevCellInFile(): Promise { + this.getCurrentCodeWatcher()?.gotoPreviousCell(); + } + + private async runAllCellsAboveFromCursor(): Promise { + const currentCodeLens = this.getCurrentCodeLens(); + if (currentCodeLens) { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.runAllCellsAbove( + currentCodeLens.range.start.line, + currentCodeLens.range.start.character + ); + } + } else { + return; + } + } + + private async runCellAndAllBelowFromCursor(): Promise { + const currentCodeLens = this.getCurrentCodeLens(); + if (currentCodeLens) { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.runCellAndAllBelow( + currentCodeLens.range.start.line, + currentCodeLens.range.start.character + ); + } + } else { + return; + } + } + + private async debugCurrentCellFromCursor(): Promise { + const currentCodeLens = this.getCurrentCodeLens(); + if (currentCodeLens) { + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeCodeWatcher) { + return activeCodeWatcher.debugCurrentCell(); + } + } else { + return; + } + } + + private async createNewNotebook(): Promise { + await this.notebookEditorProvider.createNew(); + } + + private async openStartPage(): Promise { + sendTelemetryEvent(Telemetry.StartPageOpenedFromCommandPalette); + return this.startPage.open(); + } + + private viewJupyterOutput() { + this.jupyterOutput.show(true); + } + + private getCurrentCodeLens(): CodeLens | undefined { + const activeEditor = this.documentManager.activeTextEditor; + const activeCodeWatcher = this.getCurrentCodeWatcher(); + if (activeEditor && activeCodeWatcher) { + // Find the cell that matches + return activeCodeWatcher.getCodeLenses().find((c: CodeLens) => { + if ( + c.range.end.line >= activeEditor.selection.anchor.line && + c.range.start.line <= activeEditor.selection.anchor.line + ) { + return true; + } + return false; + }); + } + } + // Get our matching code watcher for the active document + private getCurrentCodeWatcher(): ICodeWatcher | undefined { + const activeEditor = this.documentManager.activeTextEditor; + if (!activeEditor || !activeEditor.document) { + return undefined; + } + + // Ask our code lens provider to find the matching code watcher for the current document + return this.dataScienceCodeLensProvider.getCodeWatcher(activeEditor.document); + } + + private reportGatherQuality(val: string) { + sendTelemetryEvent(Telemetry.GatherQualityReport, undefined, { result: val[0] === 'no' ? 'no' : 'yes' }); + env.openExternal(Uri.parse(`https://aka.ms/gatherfeedback?succeed=${val[0]}`)); + } + + private openPythonExtensionPage() { + env.openExternal(Uri.parse(`https://marketplace.visualstudio.com/items?itemName=ms-python.python`)); + } +} diff --git a/src/client/datascience/commands/exportCommands.ts b/src/client/datascience/commands/exportCommands.ts new file mode 100644 index 000000000000..b8be186195e0 --- /dev/null +++ b/src/client/datascience/commands/exportCommands.ts @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { QuickPickItem, QuickPickOptions, Uri } from 'vscode'; +import { getLocString } from '../../../datascience-ui/react-common/locReactSide'; +import { ICommandNameArgumentTypeMapping } from '../../common/application/commands'; +import { IApplicationShell, ICommandManager } from '../../common/application/types'; + +import { IDisposable } from '../../common/types'; +import { DataScience } from '../../common/utils/localize'; +import { isUri } from '../../common/utils/misc'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Commands, Telemetry } from '../constants'; +import { ExportManager } from '../export/exportManager'; +import { ExportFormat, IExportManager } from '../export/types'; +import { IDataScienceFileSystem, INotebookEditorProvider, INotebookModel } from '../types'; + +interface IExportQuickPickItem extends QuickPickItem { + handler(): void; +} + +@injectable() +export class ExportCommands implements IDisposable { + private readonly disposables: IDisposable[] = []; + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IExportManager) private exportManager: ExportManager, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(INotebookEditorProvider) private readonly notebookProvider: INotebookEditorProvider, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem + ) {} + public register() { + this.registerCommand(Commands.ExportAsPythonScript, (model) => this.export(model, ExportFormat.python)); + this.registerCommand(Commands.ExportToHTML, (model, defaultFileName?) => + this.export(model, ExportFormat.html, defaultFileName) + ); + this.registerCommand(Commands.ExportToPDF, (model, defaultFileName?) => + this.export(model, ExportFormat.pdf, defaultFileName) + ); + this.registerCommand(Commands.Export, (model, defaultFileName?) => + this.export(model, undefined, defaultFileName) + ); + } + + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + + private registerCommand< + E extends keyof ICommandNameArgumentTypeMapping, + U extends ICommandNameArgumentTypeMapping[E] + // tslint:disable-next-line: no-any + >(command: E, callback: (...args: U) => any) { + const disposable = this.commandManager.registerCommand(command, callback, this); + this.disposables.push(disposable); + } + + private async export(modelOrUri: Uri | INotebookModel, exportMethod?: ExportFormat, defaultFileName?: string) { + defaultFileName = typeof defaultFileName === 'string' ? defaultFileName : undefined; + let model: INotebookModel | undefined; + if (modelOrUri && isUri(modelOrUri)) { + const uri = modelOrUri; + const editor = this.notebookProvider.editors.find((item) => this.fs.arePathsSame(item.file, uri)); + if (editor && editor.model) { + model = editor.model; + } + } else { + model = modelOrUri; + } + if (!model) { + // if no model was passed then this was called from the command palette, + // so we need to get the active editor + const activeEditor = this.notebookProvider.activeEditor; + if (!activeEditor || !activeEditor.model) { + return; + } + model = activeEditor.model; + + if (exportMethod) { + sendTelemetryEvent(Telemetry.ExportNotebookAsCommand, undefined, { format: exportMethod }); + } + } + + if (exportMethod) { + await this.exportManager.export(exportMethod, model, defaultFileName); + } else { + // if we don't have an export method we need to ask for one and display the + // quickpick menu + const pickedItem = await this.showExportQuickPickMenu(model, defaultFileName).then((item) => item); + if (pickedItem !== undefined) { + pickedItem.handler(); + } else { + sendTelemetryEvent(Telemetry.ClickedExportNotebookAsQuickPick); + } + } + } + + private getExportQuickPickItems(model: INotebookModel, defaultFileName?: string): IExportQuickPickItem[] { + return [ + { + label: DataScience.exportPythonQuickPickLabel(), + picked: true, + handler: () => { + sendTelemetryEvent(Telemetry.ClickedExportNotebookAsQuickPick, undefined, { + format: ExportFormat.python + }); + this.commandManager.executeCommand(Commands.ExportAsPythonScript, model); + } + }, + { + label: DataScience.exportHTMLQuickPickLabel(), + picked: false, + handler: () => { + sendTelemetryEvent(Telemetry.ClickedExportNotebookAsQuickPick, undefined, { + format: ExportFormat.html + }); + this.commandManager.executeCommand(Commands.ExportToHTML, model, defaultFileName); + } + }, + { + label: DataScience.exportPDFQuickPickLabel(), + picked: false, + handler: () => { + sendTelemetryEvent(Telemetry.ClickedExportNotebookAsQuickPick, undefined, { + format: ExportFormat.pdf + }); + this.commandManager.executeCommand(Commands.ExportToPDF, model, defaultFileName); + } + } + ]; + } + + private async showExportQuickPickMenu( + model: INotebookModel, + defaultFileName?: string + ): Promise { + const items = this.getExportQuickPickItems(model, defaultFileName); + + const options: QuickPickOptions = { + ignoreFocusOut: false, + matchOnDescription: true, + matchOnDetail: true, + placeHolder: getLocString('DataScience.exportAsQuickPickPlaceholder', 'Export As...') + }; + + return this.applicationShell.showQuickPick(items, options); + } +} diff --git a/src/client/datascience/commands/notebookCommands.ts b/src/client/datascience/commands/notebookCommands.ts new file mode 100644 index 000000000000..7c96bd7590a7 --- /dev/null +++ b/src/client/datascience/commands/notebookCommands.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { ICommandManager } from '../../common/application/types'; +import { IDisposable } from '../../common/types'; +import { Commands } from '../constants'; +import { + getDisplayNameOrNameOfKernelConnection, + kernelConnectionMetadataHasKernelModel +} from '../jupyter/kernels/helpers'; +import { KernelSelector } from '../jupyter/kernels/kernelSelector'; +import { KernelSwitcher } from '../jupyter/kernels/kernelSwitcher'; +import { KernelConnectionMetadata } from '../jupyter/kernels/types'; +import { IInteractiveWindowProvider, INotebookEditorProvider, INotebookProvider, ISwitchKernelOptions } from '../types'; + +@injectable() +export class NotebookCommands implements IDisposable { + private readonly disposables: IDisposable[] = []; + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(INotebookEditorProvider) private notebookEditorProvider: INotebookEditorProvider, + @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, + @inject(INotebookProvider) private readonly notebookProvider: INotebookProvider, + @inject(KernelSelector) private readonly kernelSelector: KernelSelector, + @inject(KernelSwitcher) private readonly kernelSwitcher: KernelSwitcher + ) {} + public register() { + this.disposables.push( + this.commandManager.registerCommand(Commands.SwitchJupyterKernel, this.switchKernel, this), + this.commandManager.registerCommand(Commands.SetJupyterKernel, this.setKernel, this), + this.commandManager.registerCommand(Commands.NotebookEditorCollapseAllCells, this.collapseAll, this), + this.commandManager.registerCommand(Commands.NotebookEditorExpandAllCells, this.expandAll, this) + ); + } + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + + private collapseAll() { + if (this.notebookEditorProvider.activeEditor) { + this.notebookEditorProvider.activeEditor.collapseAllCells(); + } + } + + private expandAll() { + if (this.notebookEditorProvider.activeEditor) { + this.notebookEditorProvider.activeEditor.expandAllCells(); + } + } + + private async switchKernel(options: ISwitchKernelOptions | undefined) { + // If no identity, spec, or resource, look at the active editor or interactive window. + // Only one is possible to be active at any point in time + if (!options) { + options = this.notebookEditorProvider.activeEditor + ? { + identity: this.notebookEditorProvider.activeEditor.file, + resource: this.notebookEditorProvider.activeEditor.file, + currentKernelDisplayName: + this.notebookEditorProvider.activeEditor.model.metadata?.kernelspec?.display_name || + this.notebookEditorProvider.activeEditor.model.metadata?.kernelspec?.name + } + : { + identity: this.interactiveWindowProvider.activeWindow?.identity, + resource: this.interactiveWindowProvider.activeWindow?.owner, + currentKernelDisplayName: getDisplayNameOrNameOfKernelConnection( + this.interactiveWindowProvider.activeWindow?.notebook?.getKernelConnection() + ) + }; + } + if (options.identity) { + // Make sure we have a connection or we can't get remote kernels. + const connection = await this.notebookProvider.connect({ getOnly: false, disableUI: false }); + + // Select a new kernel using the connection information + const kernel = await this.kernelSelector.selectJupyterKernel( + options.identity, + connection, + connection?.type || this.notebookProvider.type, + options.currentKernelDisplayName + ); + if (kernel) { + await this.setKernel(kernel, options.identity, options.resource); + } + } + } + + private async setKernel(kernel: KernelConnectionMetadata, identity: Uri, resource: Uri | undefined) { + const specOrModel = kernelConnectionMetadataHasKernelModel(kernel) ? kernel.kernelModel : kernel.kernelSpec; + if (specOrModel) { + const notebook = await this.notebookProvider.getOrCreateNotebook({ + resource, + identity, + getOnly: true + }); + + // If we have a notebook, change its kernel now + if (notebook) { + return this.kernelSwitcher.switchKernelWithRetry(notebook, kernel); + } else { + this.notebookProvider.firePotentialKernelChanged(identity, kernel); + } + } + } +} diff --git a/src/client/datascience/commands/serverSelector.ts b/src/client/datascience/commands/serverSelector.ts new file mode 100644 index 000000000000..ea2956f0d331 --- /dev/null +++ b/src/client/datascience/commands/serverSelector.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ICommandManager } from '../../common/application/types'; +import { IDisposable } from '../../common/types'; +import { Commands } from '../constants'; +import { JupyterServerSelector } from '../jupyter/serverSelector'; + +@injectable() +export class JupyterServerSelectorCommand implements IDisposable { + private readonly disposables: IDisposable[] = []; + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(JupyterServerSelector) private readonly serverSelector: JupyterServerSelector + ) {} + public register() { + this.disposables.push( + this.commandManager.registerCommand( + Commands.SelectJupyterURI, + () => this.serverSelector.selectJupyterURI(true), + this.serverSelector + ) + ); + } + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } +} diff --git a/src/client/datascience/common.ts b/src/client/datascience/common.ts new file mode 100644 index 000000000000..65c58102ab64 --- /dev/null +++ b/src/client/datascience/common.ts @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { nbformat } from '@jupyterlab/coreutils'; +import * as os from 'os'; +import { Memento, Uri } from 'vscode'; +import { splitMultilineString } from '../../datascience-ui/common'; +import { traceError, traceInfo } from '../common/logger'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { DataScience } from '../common/utils/localize'; +import { noop } from '../common/utils/misc'; +import { Settings } from './constants'; +import { ICell, IDataScienceFileSystem } from './types'; + +// Can't figure out a better way to do this. Enumerate +// the allowed keys of different output formats. +const dummyStreamObj: nbformat.IStream = { + output_type: 'stream', + name: 'stdout', + text: '' +}; +const dummyErrorObj: nbformat.IError = { + output_type: 'error', + ename: '', + evalue: '', + traceback: [''] +}; +const dummyDisplayObj: nbformat.IDisplayData = { + output_type: 'display_data', + data: {}, + metadata: {} +}; +const dummyExecuteResultObj: nbformat.IExecuteResult = { + output_type: 'execute_result', + name: '', + execution_count: 0, + data: {}, + metadata: {} +}; +const AllowedKeys = { + ['stream']: new Set(Object.keys(dummyStreamObj)), + ['error']: new Set(Object.keys(dummyErrorObj)), + ['display_data']: new Set(Object.keys(dummyDisplayObj)), + ['execute_result']: new Set(Object.keys(dummyExecuteResultObj)) +}; + +export function getSavedUriList(globalState: Memento): { uri: string; time: number; displayName?: string }[] { + const uriList = globalState.get<{ uri: string; time: number; displayName?: string }[]>( + Settings.JupyterServerUriList + ); + return uriList + ? uriList.sort((a, b) => { + return b.time - a.time; + }) + : []; +} +export function addToUriList(globalState: Memento, uri: string, time: number, displayName: string) { + const uriList = getSavedUriList(globalState); + + const editList = uriList.filter((f, i) => { + return f.uri !== uri && i < Settings.JupyterServerUriListMax - 1; + }); + editList.splice(0, 0, { uri, time, displayName }); + + globalState.update(Settings.JupyterServerUriList, editList).then(noop, noop); +} + +function fixupOutput(output: nbformat.IOutput): nbformat.IOutput { + let allowedKeys: Set; + switch (output.output_type) { + case 'stream': + case 'error': + case 'execute_result': + case 'display_data': + allowedKeys = AllowedKeys[output.output_type]; + break; + default: + return output; + } + const result = { ...output }; + for (const k of Object.keys(output)) { + if (!allowedKeys.has(k)) { + delete result[k]; + } + } + return result; +} + +export function pruneCell(cell: nbformat.ICell): nbformat.ICell { + // Source is usually a single string on input. Convert back to an array + const result = ({ + ...cell, + source: splitMultilineString(cell.source) + // tslint:disable-next-line: no-any + } as any) as nbformat.ICell; // nyc (code coverage) barfs on this so just trick it. + + // Remove outputs and execution_count from non code cells + if (result.cell_type !== 'code') { + delete result.outputs; + delete result.execution_count; + } else { + // Clean outputs from code cells + result.outputs = result.outputs ? (result.outputs as nbformat.IOutput[]).map(fixupOutput) : []; + } + + return result; +} + +export function traceCellResults(prefix: string, results: ICell[]) { + if (results.length > 0 && results[0].data.cell_type === 'code') { + const cell = results[0].data as nbformat.ICodeCell; + const error = cell.outputs && cell.outputs[0] ? cell.outputs[0].evalue : undefined; + if (error) { + traceError(`${prefix} Error : ${error}`); + } else if (cell.outputs && cell.outputs[0]) { + if (cell.outputs[0].output_type.includes('image')) { + traceInfo(`${prefix} Output: image`); + } else { + const data = cell.outputs[0].data; + const text = cell.outputs[0].text; + traceInfo(`${prefix} Output: ${text || JSON.stringify(data)}`); + } + } + } else { + traceInfo(`${prefix} no output.`); + } +} + +export function translateKernelLanguageToMonaco(kernelLanguage: string): string { + // The only known translation is C# to csharp at the moment + if (kernelLanguage === 'C#' || kernelLanguage === 'c#') { + return 'csharp'; + } + return kernelLanguage.toLowerCase(); +} + +export function generateNewNotebookUri( + counter: number, + rootFolder: string | undefined, + title?: string, + forVSCodeNotebooks?: boolean +): Uri { + // However if there are files already on disk, we should be able to overwrite them because + // they will only ever be used by 'open' editors. So just use the current counter for our untitled count. + const fileName = title ? `${title}-${counter}.ipynb` : `${DataScience.untitledNotebookFileName()}-${counter}.ipynb`; + // Turn this back into an untitled + if (forVSCodeNotebooks) { + return Uri.file(fileName).with({ scheme: 'untitled', path: fileName }); + } else { + return Uri.joinPath(rootFolder ? Uri.file(rootFolder) : Uri.file(os.tmpdir()), fileName).with({ + scheme: 'untitled' + }); + } +} + +export async function getRealPath( + fs: IDataScienceFileSystem, + execFactory: IPythonExecutionFactory, + pythonPath: string, + expectedPath: string +): Promise { + if (await fs.localDirectoryExists(expectedPath)) { + return expectedPath; + } + if (await fs.localFileExists(expectedPath)) { + return expectedPath; + } + + // If can't find the path, try turning it into a real path. + const pythonRunner = await execFactory.create({ pythonPath }); + const result = await pythonRunner.exec( + ['-c', `import os;print(os.path.realpath("${expectedPath.replace(/\\/g, '\\\\')}"))`], + { + throwOnStdErr: false, + encoding: 'utf-8' + } + ); + if (result && result.stdout) { + const trimmed = result.stdout.trim(); + if (await fs.localDirectoryExists(trimmed)) { + return trimmed; + } + if (await fs.localFileExists(trimmed)) { + return trimmed; + } + } +} diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts new file mode 100644 index 000000000000..f168c60438e5 --- /dev/null +++ b/src/client/datascience/constants.ts @@ -0,0 +1,594 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as path from 'path'; +import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../common/constants'; +import { IS_WINDOWS } from '../common/platform/constants'; +import { IVariableQuery } from '../common/types'; + +export const DefaultTheme = 'Default Light+'; +// Identifier for the output panel that will display the output from the Jupyter Server. +export const JUPYTER_OUTPUT_CHANNEL = 'JUPYTER_OUTPUT_CHANNEL'; + +// Python Module to be used when instantiating the Python Daemon. +export const JupyterDaemonModule = 'vscode_datascience_helpers.jupyter_daemon'; +export const KernelLauncherDaemonModule = 'vscode_datascience_helpers.kernel_launcher_daemon'; + +export const GatherExtension = 'ms-python.gather'; + +// List of 'language' names that we know about. All should be lower case as that's how we compare. +export const KnownNotebookLanguages: string[] = [ + 'python', + 'r', + 'julia', + 'c++', + 'c#', + 'f#', + 'scala', + 'haskell', + 'bash', + 'cling', + 'sas' +]; + +export namespace Commands { + export const RunAllCells = 'python.datascience.runallcells'; + export const RunAllCellsAbove = 'python.datascience.runallcellsabove'; + export const RunCellAndAllBelow = 'python.datascience.runcellandallbelow'; + export const SetJupyterKernel = 'python.datascience.setKernel'; + export const SwitchJupyterKernel = 'python.datascience.switchKernel'; + export const RunAllCellsAbovePalette = 'python.datascience.runallcellsabove.palette'; + export const RunCellAndAllBelowPalette = 'python.datascience.runcurrentcellandallbelow.palette'; + export const RunToLine = 'python.datascience.runtoline'; + export const RunFromLine = 'python.datascience.runfromline'; + export const RunCell = 'python.datascience.runcell'; + export const RunCurrentCell = 'python.datascience.runcurrentcell'; + export const RunCurrentCellAdvance = 'python.datascience.runcurrentcelladvance'; + export const CreateNewInteractive = 'python.datascience.createnewinteractive'; + export const ImportNotebook = 'python.datascience.importnotebook'; + export const ImportNotebookFile = 'python.datascience.importnotebookfile'; + export const OpenNotebook = 'python.datascience.opennotebook'; + export const OpenNotebookInPreviewEditor = 'python.datascience.opennotebookInPreviewEditor'; + export const SelectJupyterURI = 'python.datascience.selectjupyteruri'; + export const SelectJupyterCommandLine = 'python.datascience.selectjupytercommandline'; + export const ExportFileAsNotebook = 'python.datascience.exportfileasnotebook'; + export const ExportFileAndOutputAsNotebook = 'python.datascience.exportfileandoutputasnotebook'; + export const UndoCells = 'python.datascience.undocells'; + export const RedoCells = 'python.datascience.redocells'; + export const RemoveAllCells = 'python.datascience.removeallcells'; + export const InterruptKernel = 'python.datascience.interruptkernel'; + export const RestartKernel = 'python.datascience.restartkernel'; + export const NotebookEditorUndoCells = 'python.datascience.notebookeditor.undocells'; + export const NotebookEditorRedoCells = 'python.datascience.notebookeditor.redocells'; + export const NotebookEditorRemoveAllCells = 'python.datascience.notebookeditor.removeallcells'; + export const NotebookEditorInterruptKernel = 'python.datascience.notebookeditor.interruptkernel'; + export const NotebookEditorRestartKernel = 'python.datascience.notebookeditor.restartkernel'; + export const NotebookEditorRunAllCells = 'python.datascience.notebookeditor.runallcells'; + export const NotebookEditorRunSelectedCell = 'python.datascience.notebookeditor.runselectedcell'; + export const NotebookEditorAddCellBelow = 'python.datascience.notebookeditor.addcellbelow'; + export const ExpandAllCells = 'python.datascience.expandallcells'; + export const CollapseAllCells = 'python.datascience.collapseallcells'; + export const ExportOutputAsNotebook = 'python.datascience.exportoutputasnotebook'; + export const ExecSelectionInInteractiveWindow = 'python.datascience.execSelectionInteractive'; + export const RunFileInInteractiveWindows = 'python.datascience.runFileInteractive'; + export const DebugFileInInteractiveWindows = 'python.datascience.debugFileInteractive'; + export const AddCellBelow = 'python.datascience.addcellbelow'; + export const DebugCurrentCellPalette = 'python.datascience.debugcurrentcell.palette'; + export const DebugCell = 'python.datascience.debugcell'; + export const DebugStepOver = 'python.datascience.debugstepover'; + export const DebugContinue = 'python.datascience.debugcontinue'; + export const DebugStop = 'python.datascience.debugstop'; + export const RunCurrentCellAndAddBelow = 'python.datascience.runcurrentcellandaddbelow'; + export const InsertCellBelowPosition = 'python.datascience.insertCellBelowPosition'; + export const InsertCellBelow = 'python.datascience.insertCellBelow'; + export const InsertCellAbove = 'python.datascience.insertCellAbove'; + export const DeleteCells = 'python.datascience.deleteCells'; + export const SelectCell = 'python.datascience.selectCell'; + export const SelectCellContents = 'python.datascience.selectCellContents'; + export const ExtendSelectionByCellAbove = 'python.datascience.extendSelectionByCellAbove'; + export const ExtendSelectionByCellBelow = 'python.datascience.extendSelectionByCellBelow'; + export const MoveCellsUp = 'python.datascience.moveCellsUp'; + export const MoveCellsDown = 'python.datascience.moveCellsDown'; + export const ChangeCellToMarkdown = 'python.datascience.changeCellToMarkdown'; + export const ChangeCellToCode = 'python.datascience.changeCellToCode'; + export const GotoNextCellInFile = 'python.datascience.gotoNextCellInFile'; + export const GotoPrevCellInFile = 'python.datascience.gotoPrevCellInFile'; + export const ScrollToCell = 'python.datascience.scrolltocell'; + export const CreateNewNotebook = 'python.datascience.createnewnotebook'; + export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput'; + export const ExportAsPythonScript = 'python.datascience.exportAsPythonScript'; + export const ExportToHTML = 'python.datascience.exportToHTML'; + export const ExportToPDF = 'python.datascience.exportToPDF'; + export const Export = 'python.datascience.export'; + export const SaveNotebookNonCustomEditor = 'python.datascience.notebookeditor.save'; + export const SaveAsNotebookNonCustomEditor = 'python.datascience.notebookeditor.saveAs'; + export const OpenNotebookNonCustomEditor = 'python.datascience.notebookeditor.open'; + export const GatherQuality = 'python.datascience.gatherquality'; + export const LatestExtension = 'python.datascience.latestExtension'; + export const TrustNotebook = 'python.datascience.notebookeditor.trust'; + export const EnableLoadingWidgetsFrom3rdPartySource = + 'python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'; + export const NotebookEditorExpandAllCells = 'python.datascience.notebookeditor.expandallcells'; + export const NotebookEditorCollapseAllCells = 'python.datascience.notebookeditor.collapseallcells'; +} + +export namespace CodeLensCommands { + // If not specified in the options this is the default set of commands in our design time code lenses + export const DefaultDesignLenses = [Commands.RunCurrentCell, Commands.RunAllCellsAbove, Commands.DebugCell]; + // If not specified in the options this is the default set of commands in our debug time code lenses + export const DefaultDebuggingLenses = [Commands.DebugContinue, Commands.DebugStop, Commands.DebugStepOver]; + // These are the commands that are allowed at debug time + export const DebuggerCommands = [Commands.DebugContinue, Commands.DebugStop, Commands.DebugStepOver]; +} + +export namespace EditorContexts { + export const HasCodeCells = 'python.datascience.hascodecells'; + export const DataScienceEnabled = 'python.datascience.featureenabled'; + export const HaveInteractiveCells = 'python.datascience.haveinteractivecells'; + export const HaveRedoableCells = 'python.datascience.haveredoablecells'; + export const HaveInteractive = 'python.datascience.haveinteractive'; + export const IsInteractiveActive = 'python.datascience.isinteractiveactive'; + export const OwnsSelection = 'python.datascience.ownsSelection'; + export const HaveNativeCells = 'python.datascience.havenativecells'; + export const HaveNativeRedoableCells = 'python.datascience.havenativeredoablecells'; + export const HaveNative = 'python.datascience.havenative'; + export const IsNativeActive = 'python.datascience.isnativeactive'; + export const IsInteractiveOrNativeActive = 'python.datascience.isinteractiveornativeactive'; + export const IsPythonOrNativeActive = 'python.datascience.ispythonornativeactive'; + export const IsPythonOrInteractiveActive = 'python.datascience.ispythonorinteractiveeactive'; + export const IsPythonOrInteractiveOrNativeActive = 'python.datascience.ispythonorinteractiveornativeeactive'; + export const HaveCellSelected = 'python.datascience.havecellselected'; + export const IsNotebookTrusted = 'python.datascience.isnotebooktrusted'; + export const CanRestartNotebookKernel = 'python.datascience.notebookeditor.canrestartNotebookkernel'; +} + +export namespace RegExpValues { + export const PythonCellMarker = /^(#\s*%%|#\s*\|#\s*In\[\d*?\]|#\s*In\[ \])/; + export const PythonMarkdownCellMarker = /^(#\s*%%\s*\[markdown\]|#\s*\)/; + export const CheckJupyterRegEx = IS_WINDOWS ? /^jupyter?\.exe$/ : /^jupyter?$/; + export const PyKernelOutputRegEx = /.*\s+(.+)$/m; + export const KernelSpecOutputRegEx = /^\s*(\S+)\s+(\S+)$/; + // This next one has to be a string because uglifyJS isn't handling the groups. We use named-js-regexp to parse it + // instead. + export const UrlPatternRegEx = + '(?https?:\\/\\/)((\\(.+\\s+or\\s+(?.+)\\))|(?[^\\s]+))(?:.+)'; + export interface IUrlPatternGroupType { + LOCAL: string | undefined; + PREFIX: string | undefined; + REST: string | undefined; + IP: string | undefined; + } + export const HttpPattern = /https?:\/\//; + export const ExtractPortRegex = /https?:\/\/[^\s]+:(\d+)[^\s]+/; + export const ConvertToRemoteUri = /(https?:\/\/)([^\s])+(:\d+[^\s]*)/; + export const ParamsExractorRegEx = /\S+\((.*)\)\s*{/; + export const ArgsSplitterRegEx = /([^\s,]+)/; + export const ShapeSplitterRegEx = /.*,\s*(\d+).*/; + export const SvgHeightRegex = /(\/m; +} + +export enum Telemetry { + ImportNotebook = 'DATASCIENCE.IMPORT_NOTEBOOK', + RunCell = 'DATASCIENCE.RUN_CELL', + RunCurrentCell = 'DATASCIENCE.RUN_CURRENT_CELL', + RunCurrentCellAndAdvance = 'DATASCIENCE.RUN_CURRENT_CELL_AND_ADVANCE', + RunAllCells = 'DATASCIENCE.RUN_ALL_CELLS', + RunAllCellsAbove = 'DATASCIENCE.RUN_ALL_CELLS_ABOVE', + RunCellAndAllBelow = 'DATASCIENCE.RUN_CELL_AND_ALL_BELOW', + AddEmptyCellToBottom = 'DATASCIENCE.RUN_ADD_EMPTY_CELL_TO_BOTTOM', + RunCurrentCellAndAddBelow = 'DATASCIENCE.RUN_CURRENT_CELL_AND_ADD_BELOW', + InsertCellBelowPosition = 'DATASCIENCE.RUN_INSERT_CELL_BELOW_POSITION', + InsertCellBelow = 'DATASCIENCE.RUN_INSERT_CELL_BELOW', + InsertCellAbove = 'DATASCIENCE.RUN_INSERT_CELL_ABOVE', + DeleteCells = 'DATASCIENCE.RUN_DELETE_CELLS', + SelectCell = 'DATASCIENCE.RUN_SELECT_CELL', + SelectCellContents = 'DATASCIENCE.RUN_SELECT_CELL_CONTENTS', + ExtendSelectionByCellAbove = 'DATASCIENCE.RUN_EXTEND_SELECTION_BY_CELL_ABOVE', + ExtendSelectionByCellBelow = 'DATASCIENCE.RUN_EXTEND_SELECTION_BY_CELL_BELOW', + MoveCellsUp = 'DATASCIENCE.RUN_MOVE_CELLS_UP', + MoveCellsDown = 'DATASCIENCE.RUN_MOVE_CELLS_DOWN', + ChangeCellToMarkdown = 'DATASCIENCE.RUN_CHANGE_CELL_TO_MARKDOWN', + ChangeCellToCode = 'DATASCIENCE.RUN_CHANGE_CELL_TO_CODE', + GotoNextCellInFile = 'DATASCIENCE.GOTO_NEXT_CELL_IN_FILE', + GotoPrevCellInFile = 'DATASCIENCE.GOTO_PREV_CELL_IN_FILE', + RunSelectionOrLine = 'DATASCIENCE.RUN_SELECTION_OR_LINE', + RunToLine = 'DATASCIENCE.RUN_TO_LINE', + RunFromLine = 'DATASCIENCE.RUN_FROM_LINE', + DeleteAllCells = 'DATASCIENCE.DELETE_ALL_CELLS', + DeleteCell = 'DATASCIENCE.DELETE_CELL', + GotoSourceCode = 'DATASCIENCE.GOTO_SOURCE', + CopySourceCode = 'DATASCIENCE.COPY_SOURCE', + RestartKernel = 'DS_INTERNAL.RESTART_KERNEL', + RestartKernelCommand = 'DATASCIENCE.RESTART_KERNEL_COMMAND', + ExportNotebookInteractive = 'DATASCIENCE.EXPORT_NOTEBOOK', + Undo = 'DATASCIENCE.UNDO', + Redo = 'DATASCIENCE.REDO', + /** + * Saving a notebook + */ + Save = 'DATASCIENCE.SAVE', + CellCount = 'DS_INTERNAL.CELL_COUNT', + /** + * Whether auto save feature in VS Code is enabled or not. + */ + CreateNewInteractive = 'DATASCIENCE.CREATE_NEW_INTERACTIVE', + ExpandAll = 'DATASCIENCE.EXPAND_ALL', + CollapseAll = 'DATASCIENCE.COLLAPSE_ALL', + SelectJupyterURI = 'DATASCIENCE.SELECT_JUPYTER_URI', + SelectLocalJupyterKernel = 'DATASCIENCE.SELECT_LOCAL_JUPYTER_KERNEL', + SelectRemoteJupyterKernel = 'DATASCIENCE.SELECT_REMOTE_JUPYTER_KERNEL', + SetJupyterURIToLocal = 'DATASCIENCE.SET_JUPYTER_URI_LOCAL', + SetJupyterURIToUserSpecified = 'DATASCIENCE.SET_JUPYTER_URI_USER_SPECIFIED', + Interrupt = 'DATASCIENCE.INTERRUPT', + /** + * Exporting from the interactive window + */ + ExportPythonFileInteractive = 'DATASCIENCE.EXPORT_PYTHON_FILE', + ExportPythonFileAndOutputInteractive = 'DATASCIENCE.EXPORT_PYTHON_FILE_AND_OUTPUT', + /** + * User clicked export as quick pick button + */ + ClickedExportNotebookAsQuickPick = 'DATASCIENCE.CLICKED_EXPORT_NOTEBOOK_AS_QUICK_PICK', + /** + * exported a notebook + */ + ExportNotebookAs = 'DATASCIENCE.EXPORT_NOTEBOOK_AS', + /** + * User invokes export as format from command pallet + */ + ExportNotebookAsCommand = 'DATASCIENCE.EXPORT_NOTEBOOK_AS_COMMAND', + /** + * An export to a specific format failed + */ + ExportNotebookAsFailed = 'DATASCIENCE.EXPORT_NOTEBOOK_AS_FAILED', + + StartJupyter = 'DS_INTERNAL.JUPYTERSTARTUPCOST', + SubmitCellThroughInput = 'DATASCIENCE.SUBMITCELLFROMREPL', + ConnectLocalJupyter = 'DS_INTERNAL.CONNECTLOCALJUPYTER', + ConnectRemoteJupyter = 'DS_INTERNAL.CONNECTREMOTEJUPYTER', + ConnectRemoteJupyterViaLocalHost = 'DS_INTERNAL.CONNECTREMOTEJUPYTER_VIA_LOCALHOST', + ConnectFailedJupyter = 'DS_INTERNAL.CONNECTFAILEDJUPYTER', + ConnectRemoteFailedJupyter = 'DS_INTERNAL.CONNECTREMOTEFAILEDJUPYTER', + StartSessionFailedJupyter = 'DS_INTERNAL.START_SESSION_FAILED_JUPYTER', + ConnectRemoteSelfCertFailedJupyter = 'DS_INTERNAL.CONNECTREMOTESELFCERTFAILEDJUPYTER', + RegisterAndUseInterpreterAsKernel = 'DS_INTERNAL.REGISTER_AND_USE_INTERPRETER_AS_KERNEL', + UseInterpreterAsKernel = 'DS_INTERNAL.USE_INTERPRETER_AS_KERNEL', + UseExistingKernel = 'DS_INTERNAL.USE_EXISTING_KERNEL', + SwitchToInterpreterAsKernel = 'DS_INTERNAL.SWITCH_TO_INTERPRETER_AS_KERNEL', + SwitchToExistingKernel = 'DS_INTERNAL.SWITCH_TO_EXISTING_KERNEL', + SelfCertsMessageEnabled = 'DATASCIENCE.SELFCERTSMESSAGEENABLED', + SelfCertsMessageClose = 'DATASCIENCE.SELFCERTSMESSAGECLOSE', + RemoteAddCode = 'DATASCIENCE.LIVESHARE.ADDCODE', + RemoteReexecuteCode = 'DATASCIENCE.LIVESHARE.REEXECUTECODE', + ShiftEnterBannerShown = 'DS_INTERNAL.SHIFTENTER_BANNER_SHOWN', + EnableInteractiveShiftEnter = 'DATASCIENCE.ENABLE_INTERACTIVE_SHIFT_ENTER', + DisableInteractiveShiftEnter = 'DATASCIENCE.DISABLE_INTERACTIVE_SHIFT_ENTER', + ShowDataViewer = 'DATASCIENCE.SHOW_DATA_EXPLORER', + RunFileInteractive = 'DATASCIENCE.RUN_FILE_INTERACTIVE', + DebugFileInteractive = 'DATASCIENCE.DEBUG_FILE_INTERACTIVE', + PandasNotInstalled = 'DS_INTERNAL.SHOW_DATA_NO_PANDAS', + PandasTooOld = 'DS_INTERNAL.SHOW_DATA_PANDAS_TOO_OLD', + DataScienceSettings = 'DS_INTERNAL.SETTINGS', + VariableExplorerToggled = 'DATASCIENCE.VARIABLE_EXPLORER_TOGGLE', + VariableExplorerVariableCount = 'DS_INTERNAL.VARIABLE_EXPLORER_VARIABLE_COUNT', + AddCellBelow = 'DATASCIENCE.ADD_CELL_BELOW', + GetPasswordAttempt = 'DATASCIENCE.GET_PASSWORD_ATTEMPT', + GetPasswordFailure = 'DS_INTERNAL.GET_PASSWORD_FAILURE', + GetPasswordSuccess = 'DS_INTERNAL.GET_PASSWORD_SUCCESS', + OpenPlotViewer = 'DATASCIENCE.OPEN_PLOT_VIEWER', + DebugCurrentCell = 'DATASCIENCE.DEBUG_CURRENT_CELL', + CodeLensAverageAcquisitionTime = 'DS_INTERNAL.CODE_LENS_ACQ_TIME', + FindJupyterCommand = 'DS_INTERNAL.FIND_JUPYTER_COMMAND', + /** + * Telemetry sent when user selects an interpreter to be used for starting of Jupyter server. + */ + SelectJupyterInterpreter = 'DS_INTERNAL.SELECT_JUPYTER_INTERPRETER', + /** + * User used command to select an intrepreter for the jupyter server. + */ + SelectJupyterInterpreterCommand = 'DATASCIENCE.SELECT_JUPYTER_INTERPRETER_Command', + StartJupyterProcess = 'DS_INTERNAL.START_JUPYTER_PROCESS', + WaitForIdleJupyter = 'DS_INTERNAL.WAIT_FOR_IDLE_JUPYTER', + HiddenCellTime = 'DS_INTERNAL.HIDDEN_EXECUTION_TIME', + RestartJupyterTime = 'DS_INTERNAL.RESTART_JUPYTER_TIME', + InterruptJupyterTime = 'DS_INTERNAL.INTERRUPT_JUPYTER_TIME', + ExecuteCell = 'DATASCIENCE.EXECUTE_CELL_TIME', + ExecuteCellPerceivedCold = 'DS_INTERNAL.EXECUTE_CELL_PERCEIVED_COLD', + ExecuteCellPerceivedWarm = 'DS_INTERNAL.EXECUTE_CELL_PERCEIVED_WARM', + PerceivedJupyterStartupNotebook = 'DS_INTERNAL.PERCEIVED_JUPYTER_STARTUP_NOTEBOOK', + StartExecuteNotebookCellPerceivedCold = 'DS_INTERNAL.START_EXECUTE_NOTEBOOK_CELL_PERCEIVED_COLD', + WebviewStartup = 'DS_INTERNAL.WEBVIEW_STARTUP', + VariableExplorerFetchTime = 'DS_INTERNAL.VARIABLE_EXPLORER_FETCH_TIME', + WebviewStyleUpdate = 'DS_INTERNAL.WEBVIEW_STYLE_UPDATE', + WebviewMonacoStyleUpdate = 'DS_INTERNAL.WEBVIEW_MONACO_STYLE_UPDATE', + FindJupyterKernelSpec = 'DS_INTERNAL.FIND_JUPYTER_KERNEL_SPEC', + HashedCellOutputMimeType = 'DS_INTERNAL.HASHED_OUTPUT_MIME_TYPE', + HashedCellOutputMimeTypePerf = 'DS_INTERNAL.HASHED_OUTPUT_MIME_TYPE_PERF', + HashedNotebookCellOutputMimeTypePerf = 'DS_INTERNAL.HASHED_NOTEBOOK_OUTPUT_MIME_TYPE_PERF', + JupyterInstalledButNotKernelSpecModule = 'DS_INTERNAL.JUPYTER_INTALLED_BUT_NO_KERNELSPEC_MODULE', + DebugpyPromptToInstall = 'DATASCIENCE.DEBUGPY_PROMPT_TO_INSTALL', + DebugpySuccessfullyInstalled = 'DATASCIENCE.DEBUGPY_SUCCESSFULLY_INSTALLED', + DebugpyInstallFailed = 'DATASCIENCE.DEBUGPY_INSTALL_FAILED', + DebugpyInstallCancelled = 'DATASCIENCE.DEBUGPY_INSTALL_CANCELLED', + ScrolledToCell = 'DATASCIENCE.SCROLLED_TO_CELL', + ExecuteNativeCell = 'DATASCIENCE.NATIVE.EXECUTE_NATIVE_CELL', + CreateNewNotebook = 'DATASCIENCE.NATIVE.CREATE_NEW_NOTEBOOK', + DebugStepOver = 'DATASCIENCE.DEBUG_STEP_OVER', + DebugContinue = 'DATASCIENCE.DEBUG_CONTINUE', + DebugStop = 'DATASCIENCE.DEBUG_STOP', + OpenNotebook = 'DATASCIENCE.NATIVE.OPEN_NOTEBOOK', + OpenNotebookAll = 'DATASCIENCE.NATIVE.OPEN_NOTEBOOK_ALL', + ConvertToPythonFile = 'DATASCIENCE.NATIVE.CONVERT_NOTEBOOK_TO_PYTHON', + NotebookWorkspaceCount = 'DS_INTERNAL.NATIVE.WORKSPACE_NOTEBOOK_COUNT', + NotebookRunCount = 'DS_INTERNAL.NATIVE.NOTEBOOK_RUN_COUNT', + NotebookOpenCount = 'DS_INTERNAL.NATIVE.NOTEBOOK_OPEN_COUNT', + NotebookOpenTime = 'DS_INTERNAL.NATIVE.NOTEBOOK_OPEN_TIME', + SessionIdleTimeout = 'DS_INTERNAL.JUPYTER_IDLE_TIMEOUT', + JupyterStartTimeout = 'DS_INTERNAL.JUPYTER_START_TIMEOUT', + JupyterNotInstalledErrorShown = 'DATASCIENCE.JUPYTER_NOT_INSTALLED_ERROR_SHOWN', + JupyterCommandSearch = 'DATASCIENCE.JUPYTER_COMMAND_SEARCH', + RegisterInterpreterAsKernel = 'DS_INTERNAL.JUPYTER_REGISTER_INTERPRETER_AS_KERNEL', + UserInstalledJupyter = 'DATASCIENCE.USER_INSTALLED_JUPYTER', + UserInstalledPandas = 'DATASCIENCE.USER_INSTALLED_PANDAS', + UserDidNotInstallJupyter = 'DATASCIENCE.USER_DID_NOT_INSTALL_JUPYTER', + UserDidNotInstallPandas = 'DATASCIENCE.USER_DID_NOT_INSTALL_PANDAS', + OpenedInteractiveWindow = 'DATASCIENCE.OPENED_INTERACTIVE', + OpenNotebookFailure = 'DS_INTERNAL.NATIVE.OPEN_NOTEBOOK_FAILURE', + FindKernelForLocalConnection = 'DS_INTERNAL.FIND_KERNEL_FOR_LOCAL_CONNECTION', + CompletionTimeFromLS = 'DS_INTERNAL.COMPLETION_TIME_FROM_LS', + CompletionTimeFromJupyter = 'DS_INTERNAL.COMPLETION_TIME_FROM_JUPYTER', + NotebookLanguage = 'DATASCIENCE.NOTEBOOK_LANGUAGE', + KernelSpecNotFound = 'DS_INTERNAL.KERNEL_SPEC_NOT_FOUND', + KernelRegisterFailed = 'DS_INTERNAL.KERNEL_REGISTER_FAILED', + KernelEnumeration = 'DS_INTERNAL.KERNEL_ENUMERATION', + KernelLauncherPerf = 'DS_INTERNAL.KERNEL_LAUNCHER_PERF', + KernelFinderPerf = 'DS_INTERNAL.KERNEL_FINDER_PERF', + JupyterInstallFailed = 'DS_INTERNAL.JUPYTER_INSTALL_FAILED', + UserInstalledModule = 'DATASCIENCE.USER_INSTALLED_MODULE', + JupyterCommandLineNonDefault = 'DS_INTERNAL.JUPYTER_CUSTOM_COMMAND_LINE', + NewFileForInteractiveWindow = 'DS_INTERNAL.NEW_FILE_USED_IN_INTERACTIVE', + KernelInvalid = 'DS_INTERNAL.INVALID_KERNEL_USED', + GatherIsInstalled = 'DS_INTERNAL.GATHER_IS_INSTALLED', + GatherCompleted = 'DATASCIENCE.GATHER_COMPLETED', + GatherStats = 'DS_INTERNAL.GATHER_STATS', + GatherException = 'DS_INTERNAL.GATHER_EXCEPTION', + GatheredNotebookSaved = 'DATASCIENCE.GATHERED_NOTEBOOK_SAVED', + GatherQualityReport = 'DS_INTERNAL.GATHER_QUALITY_REPORT', + ZMQSupported = 'DS_INTERNAL.ZMQ_NATIVE_BINARIES_LOADING', + ZMQNotSupported = 'DS_INTERNAL.ZMQ_NATIVE_BINARIES_NOT_LOADING', + IPyWidgetLoadSuccess = 'DS_INTERNAL.IPYWIDGET_LOAD_SUCCESS', + IPyWidgetLoadFailure = 'DS_INTERNAL.IPYWIDGET_LOAD_FAILURE', + IPyWidgetWidgetVersionNotSupportedLoadFailure = 'DS_INTERNAL.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED_LOAD_FAILURE', + IPyWidgetLoadDisabled = 'DS_INTERNAL.IPYWIDGET_LOAD_DISABLED', + HashedIPyWidgetNameUsed = 'DS_INTERNAL.IPYWIDGET_USED_BY_USER', + VSCNotebookCellTranslationFailed = 'DS_INTERNAL.VSCNOTEBOOK_CELL_TRANSLATION_FAILED', + HashedIPyWidgetNameDiscovered = 'DS_INTERNAL.IPYWIDGET_DISCOVERED', + HashedIPyWidgetScriptDiscoveryError = 'DS_INTERNAL.IPYWIDGET_DISCOVERY_ERRORED', + DiscoverIPyWidgetNamesLocalPerf = 'DS_INTERNAL.IPYWIDGET_TEST_AVAILABILITY_ON_LOCAL', + DiscoverIPyWidgetNamesCDNPerf = 'DS_INTERNAL.IPYWIDGET_TEST_AVAILABILITY_ON_CDN', + IPyWidgetPromptToUseCDN = 'DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN', + IPyWidgetPromptToUseCDNSelection = 'DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN_SELECTION', + IPyWidgetOverhead = 'DS_INTERNAL.IPYWIDGET_OVERHEAD', + IPyWidgetRenderFailure = 'DS_INTERNAL.IPYWIDGET_RENDER_FAILURE', + IPyWidgetUnhandledMessage = 'DS_INTERNAL.IPYWIDGET_UNHANDLED_MESSAGE', + RawKernelCreatingNotebook = 'DS_INTERNAL.RAWKERNEL_CREATING_NOTEBOOK', + JupyterCreatingNotebook = 'DS_INTERNAL.JUPYTER_CREATING_NOTEBOOK', + RawKernelSessionConnect = 'DS_INTERNAL.RAWKERNEL_SESSION_CONNECT', + RawKernelStartRawSession = 'DS_INTERNAL.RAWKERNEL_START_RAW_SESSION', + RawKernelSessionStartSuccess = 'DS_INTERNAL.RAWKERNEL_SESSION_START_SUCCESS', + RawKernelSessionStartUserCancel = 'DS_INTERNAL.RAWKERNEL_SESSION_START_USER_CANCEL', + RawKernelSessionStartTimeout = 'DS_INTERNAL.RAWKERNEL_SESSION_START_TIMEOUT', + RawKernelSessionStartException = 'DS_INTERNAL.RAWKERNEL_SESSION_START_EXCEPTION', + RawKernelProcessLaunch = 'DS_INTERNAL.RAWKERNEL_PROCESS_LAUNCH', + StartPageViewed = 'DS_INTERNAL.STARTPAGE_VIEWED', + StartPageOpenedFromCommandPalette = 'DS_INTERNAL.STARTPAGE_OPENED_FROM_COMMAND_PALETTE', + StartPageOpenedFromNewInstall = 'DS_INTERNAL.STARTPAGE_OPENED_FROM_NEW_INSTALL', + StartPageOpenedFromNewUpdate = 'DS_INTERNAL.STARTPAGE_OPENED_FROM_NEW_UPDATE', + StartPageWebViewError = 'DS_INTERNAL.STARTPAGE_WEBVIEWERROR', + StartPageTime = 'DS_INTERNAL.STARTPAGE_TIME', + StartPageClickedDontShowAgain = 'DATASCIENCE.STARTPAGE_DONT_SHOW_AGAIN', + StartPageClosedWithoutAction = 'DATASCIENCE.STARTPAGE_CLOSED_WITHOUT_ACTION', + StartPageUsedAnActionOnFirstTime = 'DATASCIENCE.STARTPAGE_USED_ACTION_ON_FIRST_TIME', + StartPageOpenBlankNotebook = 'DATASCIENCE.STARTPAGE_OPEN_BLANK_NOTEBOOK', + StartPageOpenBlankPythonFile = 'DATASCIENCE.STARTPAGE_OPEN_BLANK_PYTHON_FILE', + StartPageOpenInteractiveWindow = 'DATASCIENCE.STARTPAGE_OPEN_INTERACTIVE_WINDOW', + StartPageOpenCommandPalette = 'DATASCIENCE.STARTPAGE_OPEN_COMMAND_PALETTE', + StartPageOpenCommandPaletteWithOpenNBSelected = 'DATASCIENCE.STARTPAGE_OPEN_COMMAND_PALETTE_WITH_OPENNBSELECTED', + StartPageOpenSampleNotebook = 'DATASCIENCE.STARTPAGE_OPEN_SAMPLE_NOTEBOOK', + StartPageOpenFileBrowser = 'DATASCIENCE.STARTPAGE_OPEN_FILE_BROWSER', + StartPageOpenFolder = 'DATASCIENCE.STARTPAGE_OPEN_FOLDER', + StartPageOpenWorkspace = 'DATASCIENCE.STARTPAGE_OPEN_WORKSPACE', + RunByLineStart = 'DATASCIENCE.RUN_BY_LINE', + RunByLineStep = 'DATASCIENCE.RUN_BY_LINE_STEP', + RunByLineStop = 'DATASCIENCE.RUN_BY_LINE_STOP', + RunByLineVariableHover = 'DATASCIENCE.RUN_BY_LINE_VARIABLE_HOVER', + TrustAllNotebooks = 'DATASCIENCE.TRUST_ALL_NOTEBOOKS', + TrustNotebook = 'DATASCIENCE.TRUST_NOTEBOOK', + DoNotTrustNotebook = 'DATASCIENCE.DO_NOT_TRUST_NOTEBOOK', + NotebookTrustPromptShown = 'DATASCIENCE.NOTEBOOK_TRUST_PROMPT_SHOWN' +} + +export enum NativeKeyboardCommandTelemetry { + ArrowDown = 'DATASCIENCE.NATIVE.KEYBOARD.ARROW_DOWN', + ArrowUp = 'DATASCIENCE.NATIVE.KEYBOARD.ARROW_UP', + ChangeToCode = 'DATASCIENCE.NATIVE.KEYBOARD.CHANGE_TO_CODE', + ChangeToMarkdown = 'DATASCIENCE.NATIVE.KEYBOARD.CHANGE_TO_MARKDOWN', + DeleteCell = 'DATASCIENCE.NATIVE.KEYBOARD.DELETE_CELL', + InsertAbove = 'DATASCIENCE.NATIVE.KEYBOARD.INSERT_ABOVE', + InsertBelow = 'DATASCIENCE.NATIVE.KEYBOARD.INSERT_BELOW', + Redo = 'DATASCIENCE.NATIVE.KEYBOARD.REDO', + Run = 'DATASCIENCE.NATIVE.KEYBOARD.RUN', + Save = 'DATASCIENCE.NATIVE.KEYBOARD.SAVE', + RunAndAdd = 'DATASCIENCE.NATIVE.KEYBOARD.RUN_AND_ADD', + RunAndMove = 'DATASCIENCE.NATIVE.KEYBOARD.RUN_AND_MOVE', + ToggleLineNumbers = 'DATASCIENCE.NATIVE.KEYBOARD.TOGGLE_LINE_NUMBERS', + ToggleOutput = 'DATASCIENCE.NATIVE.KEYBOARD.TOGGLE_OUTPUT', + Undo = 'DATASCIENCE.NATIVE.KEYBOARD.UNDO', + Unfocus = 'DATASCIENCE.NATIVE.KEYBOARD.UNFOCUS' +} + +export enum NativeMouseCommandTelemetry { + AddToEnd = 'DATASCIENCE.NATIVE.MOUSE.ADD_TO_END', + ChangeToCode = 'DATASCIENCE.NATIVE.MOUSE.CHANGE_TO_CODE', + ChangeToMarkdown = 'DATASCIENCE.NATIVE.MOUSE.CHANGE_TO_MARKDOWN', + DeleteCell = 'DATASCIENCE.NATIVE.MOUSE.DELETE_CELL', + InsertBelow = 'DATASCIENCE.NATIVE.MOUSE.INSERT_BELOW', + MoveCellDown = 'DATASCIENCE.NATIVE.MOUSE.MOVE_CELL_DOWN', + MoveCellUp = 'DATASCIENCE.NATIVE.MOUSE.MOVE_CELL_UP', + Run = 'DATASCIENCE.NATIVE.MOUSE.RUN', + RunAbove = 'DATASCIENCE.NATIVE.MOUSE.RUN_ABOVE', + RunAll = 'DATASCIENCE.NATIVE.MOUSE.RUN_ALL', + RunBelow = 'DATASCIENCE.NATIVE.MOUSE.RUN_BELOW', + SelectKernel = 'DATASCIENCE.NATIVE.MOUSE.SELECT_KERNEL', + SelectServer = 'DATASCIENCE.NATIVE.MOUSE.SELECT_SERVER', + Save = 'DATASCIENCE.NATIVE.MOUSE.SAVE', + ToggleVariableExplorer = 'DATASCIENCE.NATIVE.MOUSE.TOGGLE_VARIABLE_EXPLORER' +} + +/** + * Notebook editing in VS Code Notebooks is handled by VSC. + * There's no way for us to know whether user added a cell using keyboard or not. + * Similarly a cell could have been added as part of an undo operation. + * All we know is previously user had n # of cells and now they have m # of cells. + */ +export enum VSCodeNativeTelemetry { + AddCell = 'DATASCIENCE.VSCODE_NATIVE.INSERT_CELL', + RunAllCells = 'DATASCIENCE.VSCODE_NATIVE.RUN_ALL', + DeleteCell = 'DATASCIENCE.VSCODE_NATIVE.DELETE_CELL', + MoveCell = 'DATASCIENCE.VSCODE_NATIVE.MOVE_CELL', + ChangeToCode = 'DATASCIENCE.VSCODE_NATIVE.CHANGE_TO_CODE', // Not guaranteed to work see, https://github.com/microsoft/vscode/issues/100042 + ChangeToMarkdown = 'DATASCIENCE.VSCODE_NATIVE.CHANGE_TO_MARKDOWN' // Not guaranteed to work see, https://github.com/microsoft/vscode/issues/100042 +} + +export namespace HelpLinks { + export const PythonInteractiveHelpLink = 'https://aka.ms/pyaiinstall'; + export const JupyterDataRateHelpLink = 'https://aka.ms/AA5ggm0'; // This redirects here: https://jupyter-notebook.readthedocs.io/en/stable/config.html +} + +export namespace Settings { + export const JupyterServerLocalLaunch = 'local'; + export const JupyterServerUriList = 'python.dataScience.jupyterServer.uriList'; + export const JupyterServerUriListMax = 10; + // If this timeout expires, ignore the completion request sent to Jupyter. + export const IntellisenseTimeout = 500; + // If this timeout expires, ignore the completions requests. (don't wait for it to complete). + export const MaxIntellisenseTimeout = 30_000; + export const RemoteDebuggerPortBegin = 8889; + export const RemoteDebuggerPortEnd = 9000; + export const DefaultVariableQuery: IVariableQuery = { + language: PYTHON_LANGUAGE, + query: '_rwho_ls = %who_ls\nprint(_rwho_ls)', + parseExpr: "'(\\w+)'" + }; +} + +export namespace DataFrameLoading { + export const SysPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'vscode_datascience_helpers', 'dataframes'); + export const DataFrameSysImport = `import sys\nsys.path.append("${SysPath.replace(/\\/g, '\\\\')}")`; + export const DataFrameInfoImportName = '_VSCODE_InfoImport'; + export const DataFrameInfoImport = `import vscodeGetDataFrameInfo as ${DataFrameInfoImportName}`; + export const DataFrameInfoFunc = `${DataFrameInfoImportName}._VSCODE_getDataFrameInfo`; + export const DataFrameRowImportName = '_VSCODE_RowImport'; + export const DataFrameRowImport = `import vscodeGetDataFrameRows as ${DataFrameRowImportName}`; + export const DataFrameRowFunc = `${DataFrameRowImportName}._VSCODE_getDataFrameRows`; + export const VariableInfoImportName = '_VSCODE_VariableImport'; + export const VariableInfoImport = `import vscodeGetVariableInfo as ${VariableInfoImportName}`; + export const VariableInfoFunc = `${VariableInfoImportName}._VSCODE_getVariableInfo`; +} + +export namespace Identifiers { + export const EmptyFileName = '2DB9B899-6519-4E1B-88B0-FA728A274115'; + export const GeneratedThemeName = 'ipython-theme'; // This needs to be all lower class and a valid class name. + export const HistoryPurpose = 'history'; + export const RawPurpose = 'raw'; + export const PingPurpose = 'ping'; + export const MatplotLibDefaultParams = '_VSCode_defaultMatplotlib_Params'; + export const EditCellId = '3D3AB152-ADC1-4501-B813-4B83B49B0C10'; + export const SvgSizeTag = 'sizeTag={{0}, {1}}'; + export const InteractiveWindowIdentityScheme = 'history'; + export const DefaultCodeCellMarker = '# %%'; + export const DefaultCommTarget = 'jupyter.widget'; + export const ALL_VARIABLES = 'ALL_VARIABLES'; + export const OLD_VARIABLES = 'OLD_VARIABLES'; + export const KERNEL_VARIABLES = 'KERNEL_VARIABLES'; + export const DEBUGGER_VARIABLES = 'DEBUGGER_VARIABLES'; + export const MULTIPLEXING_DEBUGSERVICE = 'MULTIPLEXING_DEBUGSERVICE'; + export const RUN_BY_LINE_DEBUGSERVICE = 'RUN_BY_LINE_DEBUGSERVICE'; + export const REMOTE_URI = 'https://remote/'; + export const REMOTE_URI_ID_PARAM = 'id'; + export const REMOTE_URI_HANDLE_PARAM = 'uriHandle'; +} + +export namespace CodeSnippets { + export const ChangeDirectory = [ + '{0}', + '{1}', + 'import os', + 'try:', + "\tos.chdir(os.path.join(os.getcwd(), '{2}'))", + '\tprint(os.getcwd())', + 'except:', + '\tpass', + '' + ]; + export const ChangeDirectoryCommentIdentifier = '# ms-python.python added'; // Not translated so can compare. + export const ImportIPython = '{0}\nfrom IPython import get_ipython\n\n{1}'; + export const MatplotLibInitSvg = `import matplotlib\n%matplotlib inline\n${Identifiers.MatplotLibDefaultParams} = dict(matplotlib.rcParams)\n%config InlineBackend.figure_formats = {'svg', 'png'}`; + export const MatplotLibInitPng = `import matplotlib\n%matplotlib inline\n${Identifiers.MatplotLibDefaultParams} = dict(matplotlib.rcParams)\n%config InlineBackend.figure_formats = {'png'}`; + export const ConfigSvg = `%config InlineBackend.figure_formats = {'svg', 'png'}`; + export const ConfigPng = `%config InlineBackend.figure_formats = {'png'}`; + export const UpdateCWDAndPath = + 'import os\nimport sys\n%cd "{0}"\nif os.getcwd() not in sys.path:\n sys.path.insert(0, os.getcwd())'; + export const disableJedi = '%config Completer.use_jedi = False'; +} + +export enum JupyterCommands { + NotebookCommand = 'notebook', + ConvertCommand = 'nbconvert', + KernelSpecCommand = 'kernelspec' +} + +export namespace LiveShare { + export const JupyterExecutionService = 'jupyterExecutionService'; + export const JupyterServerSharedService = 'jupyterServerSharedService'; + export const JupyterNotebookSharedService = 'jupyterNotebookSharedService'; + export const CommandBrokerService = 'commmandBrokerService'; + export const WebPanelMessageService = 'webPanelMessageService'; + export const InteractiveWindowProviderService = 'interactiveWindowProviderService'; + export const GuestCheckerService = 'guestCheckerService'; + export const LiveShareBroadcastRequest = 'broadcastRequest'; + export const RawNotebookProviderService = 'rawNotebookProviderSharedService'; + export const ResponseLifetime = 15000; + export const ResponseRange = 1000; // Range of time alloted to check if a response matches or not + export const InterruptDefaultTimeout = 10000; +} + +export namespace LiveShareCommands { + export const isNotebookSupported = 'isNotebookSupported'; + export const isImportSupported = 'isImportSupported'; + export const connectToNotebookServer = 'connectToNotebookServer'; + export const getUsableJupyterPython = 'getUsableJupyterPython'; + export const executeObservable = 'executeObservable'; + export const getSysInfo = 'getSysInfo'; + export const serverResponse = 'serverResponse'; + export const catchupRequest = 'catchupRequest'; + export const syncRequest = 'synchRequest'; + export const restart = 'restart'; + export const interrupt = 'interrupt'; + export const interactiveWindowCreate = 'interactiveWindowCreate'; + export const interactiveWindowCreateSync = 'interactiveWindowCreateSync'; + export const disposeServer = 'disposeServer'; + export const guestCheck = 'guestCheck'; + export const createNotebook = 'createNotebook'; + export const inspect = 'inspect'; + export const rawKernelSupported = 'rawKernelSupported'; + export const createRawNotebook = 'createRawNotebook'; +} + +export const VSCodeNotebookProvider = 'VSCodeNotebookProvider'; +export const OurNotebookProvider = 'OurNotebookProvider'; +export const DataScienceStartupTime = Symbol('DataScienceStartupTime'); diff --git a/src/client/datascience/context/activeEditorContext.ts b/src/client/datascience/context/activeEditorContext.ts new file mode 100644 index 000000000000..103021f29481 --- /dev/null +++ b/src/client/datascience/context/activeEditorContext.ts @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { TextEditor } from 'vscode'; +import { ServerStatus } from '../../../datascience-ui/interactive-common/mainState'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { ICommandManager, IDocumentManager } from '../../common/application/types'; +import { PYTHON_LANGUAGE } from '../../common/constants'; +import { ContextKey } from '../../common/contextKey'; +import { NotebookEditorSupport } from '../../common/experiments/groups'; +import { traceError } from '../../common/logger'; +import { IDisposable, IDisposableRegistry, IExperimentsManager } from '../../common/types'; +import { setSharedProperty } from '../../telemetry'; +import { EditorContexts } from '../constants'; +import { + IInteractiveWindow, + IInteractiveWindowProvider, + INotebook, + INotebookEditor, + INotebookEditorProvider, + INotebookProvider, + ITrustService +} from '../types'; + +@injectable() +export class ActiveEditorContextService implements IExtensionSingleActivationService, IDisposable { + private readonly disposables: IDisposable[] = []; + private nativeContext: ContextKey; + private interactiveContext: ContextKey; + private interactiveOrNativeContext: ContextKey; + private pythonOrInteractiveContext: ContextKey; + private pythonOrNativeContext: ContextKey; + private pythonOrInteractiveOrNativeContext: ContextKey; + private canRestartNotebookKernelContext: ContextKey; + private hasNativeNotebookCells: ContextKey; + private isNotebookTrusted: ContextKey; + private isPythonFileActive: boolean = false; + constructor( + @inject(IInteractiveWindowProvider) private readonly interactiveProvider: IInteractiveWindowProvider, + @inject(INotebookEditorProvider) private readonly notebookEditorProvider: INotebookEditorProvider, + @inject(IDocumentManager) private readonly docManager: IDocumentManager, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(IExperimentsManager) private readonly experiments: IExperimentsManager, + @inject(INotebookProvider) private readonly notebookProvider: INotebookProvider, + @inject(ITrustService) private readonly trustService: ITrustService + ) { + disposables.push(this); + this.nativeContext = new ContextKey(EditorContexts.IsNativeActive, this.commandManager); + this.canRestartNotebookKernelContext = new ContextKey( + EditorContexts.CanRestartNotebookKernel, + this.commandManager + ); + this.interactiveContext = new ContextKey(EditorContexts.IsInteractiveActive, this.commandManager); + this.interactiveOrNativeContext = new ContextKey( + EditorContexts.IsInteractiveOrNativeActive, + this.commandManager + ); + this.pythonOrNativeContext = new ContextKey(EditorContexts.IsPythonOrNativeActive, this.commandManager); + this.pythonOrInteractiveContext = new ContextKey( + EditorContexts.IsPythonOrInteractiveActive, + this.commandManager + ); + this.pythonOrInteractiveOrNativeContext = new ContextKey( + EditorContexts.IsPythonOrInteractiveOrNativeActive, + this.commandManager + ); + this.hasNativeNotebookCells = new ContextKey(EditorContexts.HaveNativeCells, this.commandManager); + this.isNotebookTrusted = new ContextKey(EditorContexts.IsNotebookTrusted, this.commandManager); + } + public dispose() { + this.disposables.forEach((item) => item.dispose()); + } + public async activate(): Promise { + this.docManager.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this.disposables); + this.notebookProvider.onSessionStatusChanged(this.onDidKernelStatusChange, this, this.disposables); + this.interactiveProvider.onDidChangeActiveInteractiveWindow( + this.onDidChangeActiveInteractiveWindow, + this, + this.disposables + ); + this.notebookEditorProvider.onDidChangeActiveNotebookEditor( + this.onDidChangeActiveNotebookEditor, + this, + this.disposables + ); + this.trustService.onDidSetNotebookTrust(this.onDidSetNotebookTrust, this, this.disposables); + + // Do we already have python file opened. + if (this.docManager.activeTextEditor?.document.languageId === PYTHON_LANGUAGE) { + this.onDidChangeActiveTextEditor(this.docManager.activeTextEditor); + } + } + + private updateNativeNotebookCellContext() { + if (!this.experiments.inExperiment(NotebookEditorSupport.nativeNotebookExperiment)) { + return; + } + this.hasNativeNotebookCells + .set((this.notebookEditorProvider.activeEditor?.model?.cells?.length || 0) > 0) + .ignoreErrors(); + } + private onDidChangeActiveInteractiveWindow(e?: IInteractiveWindow) { + this.interactiveContext.set(!!e).ignoreErrors(); + this.updateMergedContexts(); + } + private onDidChangeActiveNotebookEditor(e?: INotebookEditor) { + // This will ensure all subsequent telemetry will get the context of whether it is a custom/native/old notebook editor. + // This is temporary, and once we ship native editor this needs to be removed. + setSharedProperty('ds_notebookeditor', e?.type); + this.nativeContext.set(!!e).ignoreErrors(); + this.isNotebookTrusted.set(e?.model?.isTrusted === true).ignoreErrors(); + this.updateMergedContexts(); + this.updateContextOfActiveNotebookKernel(e); + } + private updateContextOfActiveNotebookKernel(activeEditor?: INotebookEditor) { + if (activeEditor) { + this.notebookProvider + .getOrCreateNotebook({ identity: activeEditor.file, getOnly: true }) + .then((nb) => { + if (activeEditor === this.notebookEditorProvider.activeEditor) { + const canStart = nb && nb.status !== ServerStatus.NotStarted && activeEditor.model?.isTrusted; + this.canRestartNotebookKernelContext.set(!!canStart).ignoreErrors(); + } + }) + .catch( + traceError.bind(undefined, 'Failed to determine if a notebook is active for the current editor') + ); + } else { + this.canRestartNotebookKernelContext.set(false).ignoreErrors(); + } + } + private onDidKernelStatusChange({ notebook }: { status: ServerStatus; notebook: INotebook }) { + // Ok, kernel status has changed. + const activeEditor = this.notebookEditorProvider.activeEditor; + if (!activeEditor) { + return; + } + if (activeEditor.file.toString() !== notebook.identity.toString()) { + // Status of a notebook thats not related to active editor has changed. + // We can ignore that. + return; + } + this.updateContextOfActiveNotebookKernel(activeEditor); + } + private onDidChangeActiveTextEditor(e?: TextEditor) { + this.isPythonFileActive = + e?.document.languageId === PYTHON_LANGUAGE && !this.notebookEditorProvider.activeEditor; + this.updateNativeNotebookCellContext(); + this.updateMergedContexts(); + } + // When trust service says trust has changed, update context with whether the currently active notebook is trusted + private onDidSetNotebookTrust() { + if (this.notebookEditorProvider.activeEditor?.model !== undefined) { + this.isNotebookTrusted.set(this.notebookEditorProvider.activeEditor?.model?.isTrusted).ignoreErrors(); + } + } + private updateMergedContexts() { + this.interactiveOrNativeContext + .set(this.nativeContext.value === true && this.interactiveContext.value === true) + .ignoreErrors(); + this.pythonOrNativeContext + .set(this.nativeContext.value === true || this.isPythonFileActive === true) + .ignoreErrors(); + this.pythonOrInteractiveContext + .set(this.interactiveContext.value === true || this.isPythonFileActive === true) + .ignoreErrors(); + this.pythonOrInteractiveOrNativeContext + .set( + this.nativeContext.value === true || + (this.interactiveContext.value === true && this.isPythonFileActive === true) + ) + .ignoreErrors(); + } +} diff --git a/src/client/datascience/crossProcessLock.ts b/src/client/datascience/crossProcessLock.ts new file mode 100644 index 000000000000..e738ec9b9186 --- /dev/null +++ b/src/client/datascience/crossProcessLock.ts @@ -0,0 +1,66 @@ +import { promises } from 'fs'; +import { tmpdir } from 'os'; +import * as path from 'path'; +import { traceError } from '../common/logger'; +import { sleep } from '../common/utils/async'; + +export class CrossProcessLock { + private lockFilePath: string; + private acquired: boolean = false; + + constructor(mutexName: string) { + this.lockFilePath = path.join(tmpdir(), `${mutexName}.tmp`); + } + + public async lock(): Promise { + const maxTries = 50; + let tries = 0; + while (!this.acquired && tries < maxTries) { + try { + await this.acquire(); + if (this.acquired) { + return true; + } + await sleep(100); + } catch (err) { + // Swallow the error and retry + traceError(err); + } + tries += 1; + } + return false; + } + + public async unlock() { + // Does nothing if the lock is not currently held + if (this.acquired) { + try { + // Delete the lockfile + await promises.unlink(this.lockFilePath); + this.acquired = false; + } catch (err) { + traceError(err); + } + } else { + throw new Error('Current process attempted to release a lock it does not hold'); + } + } + + /* + One of the few atomicity guarantees that the node fs module appears to provide + is with fs.open(). With the 'wx' option flags, open() will error if the + file already exists, which tells us if it was already created in another process. + Hence we can use the existence of the file as a flag indicating whether we have + successfully acquired the right to create the keyfile. + */ + private async acquire() { + try { + await promises.open(this.lockFilePath, 'wx'); + this.acquired = true; + } catch (err) { + if (err.code !== 'EEXIST') { + throw err; + } + } + } +} diff --git a/src/client/datascience/data-viewing/dataViewer.ts b/src/client/datascience/data-viewing/dataViewer.ts new file mode 100644 index 000000000000..21542cc774ee --- /dev/null +++ b/src/client/datascience/data-viewing/dataViewer.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { ViewColumn } from 'vscode'; + +import { IApplicationShell, IWebviewPanelProvider, IWorkspaceService } from '../../common/application/types'; +import { EXTENSION_ROOT_DIR, UseCustomEditorApi } from '../../common/constants'; +import { traceError } from '../../common/logger'; +import { IConfigurationService, IDisposable, Resource } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { sendTelemetryEvent } from '../../telemetry'; +import { HelpLinks, Telemetry } from '../constants'; +import { JupyterDataRateLimitError } from '../jupyter/jupyterDataRateLimitError'; +import { ICodeCssGenerator, IThemeFinder } from '../types'; +import { WebviewPanelHost } from '../webviews/webviewPanelHost'; +import { DataViewerMessageListener } from './dataViewerMessageListener'; +import { + DataViewerMessages, + IDataFrameInfo, + IDataViewer, + IDataViewerDataProvider, + IDataViewerMapping, + IGetRowsRequest +} from './types'; + +const dataExplorereDir = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'viewers'); +@injectable() +export class DataViewer extends WebviewPanelHost implements IDataViewer, IDisposable { + private dataProvider: IDataViewerDataProvider | undefined; + private rowsTimer: StopWatch | undefined; + private pendingRowsCount: number = 0; + private dataFrameInfoPromise: Promise | undefined; + + constructor( + @inject(IWebviewPanelProvider) provider: IWebviewPanelProvider, + @inject(IConfigurationService) configuration: IConfigurationService, + @inject(ICodeCssGenerator) cssGenerator: ICodeCssGenerator, + @inject(IThemeFinder) themeFinder: IThemeFinder, + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IApplicationShell) private applicationShell: IApplicationShell, + @inject(UseCustomEditorApi) useCustomEditorApi: boolean + ) { + super( + configuration, + provider, + cssGenerator, + themeFinder, + workspaceService, + (c, v, d) => new DataViewerMessageListener(c, v, d), + dataExplorereDir, + [path.join(dataExplorereDir, 'commons.initial.bundle.js'), path.join(dataExplorereDir, 'dataExplorer.js')], + localize.DataScience.dataExplorerTitle(), + ViewColumn.One, + useCustomEditorApi, + false, + Promise.resolve(false) + ); + } + + public async showData(dataProvider: IDataViewerDataProvider, title: string): Promise { + if (!this.isDisposed) { + // Save the data provider + this.dataProvider = dataProvider; + + // Load the web panel using our current directory as we don't expect to load any other files + await super.loadWebPanel(process.cwd()).catch(traceError); + + super.setTitle(title); + + // Then show our web panel. Eventually we need to consume the data + await super.show(true); + + const dataFrameInfo = await this.prepDataFrameInfo(); + + // Send a message with our data + this.postMessage(DataViewerMessages.InitializeData, dataFrameInfo).ignoreErrors(); + } + } + + public dispose(): void { + super.dispose(); + + if (this.dataProvider) { + // Call dispose on the data provider + this.dataProvider.dispose(); + this.dataProvider = undefined; + } + } + + protected get owningResource(): Resource { + return undefined; + } + + //tslint:disable-next-line:no-any + protected onMessage(message: string, payload: any) { + switch (message) { + case DataViewerMessages.GetAllRowsRequest: + this.getAllRows().ignoreErrors(); + break; + + case DataViewerMessages.GetRowsRequest: + this.getRowChunk(payload as IGetRowsRequest).ignoreErrors(); + break; + + default: + break; + } + + super.onMessage(message, payload); + } + + private getDataFrameInfo(): Promise { + if (!this.dataFrameInfoPromise) { + this.dataFrameInfoPromise = this.dataProvider ? this.dataProvider.getDataFrameInfo() : Promise.resolve({}); + } + return this.dataFrameInfoPromise; + } + + private async prepDataFrameInfo(): Promise { + this.rowsTimer = new StopWatch(); + const output = await this.getDataFrameInfo(); + + // Log telemetry about number of rows + try { + sendTelemetryEvent(Telemetry.ShowDataViewer, 0, { + rows: output.rowCount ? output.rowCount : 0, + columns: output.columns ? output.columns.length : 0 + }); + + // Count number of rows to fetch so can send telemetry on how long it took. + this.pendingRowsCount = output.rowCount ? output.rowCount : 0; + } catch { + noop(); + } + + return output; + } + + private async getAllRows() { + return this.wrapRequest(async () => { + if (this.dataProvider) { + const allRows = await this.dataProvider.getAllRows(); + this.pendingRowsCount = 0; + return this.postMessage(DataViewerMessages.GetAllRowsResponse, allRows); + } + }); + } + + private getRowChunk(request: IGetRowsRequest) { + return this.wrapRequest(async () => { + if (this.dataProvider) { + const dataFrameInfo = await this.getDataFrameInfo(); + const rows = await this.dataProvider.getRows( + request.start, + Math.min(request.end, dataFrameInfo.rowCount ? dataFrameInfo.rowCount : 0) + ); + return this.postMessage(DataViewerMessages.GetRowsResponse, { + rows, + start: request.start, + end: request.end + }); + } + }); + } + + private async wrapRequest(func: () => Promise) { + try { + return await func(); + } catch (e) { + if (e instanceof JupyterDataRateLimitError) { + traceError(e); + const actionTitle = localize.DataScience.pythonInteractiveHelpLink(); + this.applicationShell.showErrorMessage(e.toString(), actionTitle).then((v) => { + // User clicked on the link, open it. + if (v === actionTitle) { + this.applicationShell.openUrl(HelpLinks.JupyterDataRateHelpLink); + } + }); + this.dispose(); + } + traceError(e); + this.applicationShell.showErrorMessage(e); + } finally { + this.sendElapsedTimeTelemetry(); + } + } + + private sendElapsedTimeTelemetry() { + if (this.rowsTimer && this.pendingRowsCount === 0) { + sendTelemetryEvent(Telemetry.ShowDataViewer, this.rowsTimer.elapsedTime); + } + } +} diff --git a/src/client/datascience/data-viewing/dataViewerDependencyService.ts b/src/client/datascience/data-viewing/dataViewerDependencyService.ts new file mode 100644 index 000000000000..90bf35756a81 --- /dev/null +++ b/src/client/datascience/data-viewing/dataViewerDependencyService.ts @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { parse, SemVer } from 'semver'; +import { CancellationToken } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { Cancellation, createPromiseFromCancellation, wrapCancellationTokens } from '../../common/cancellation'; +import { traceWarning } from '../../common/logger'; +import { IPythonExecutionFactory } from '../../common/process/types'; +import { IInstaller, InstallerResponse, Product } from '../../common/types'; +import { Common, DataScience } from '../../common/utils/localize'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; + +const minimumSupportedPandaVersion = '0.20.0'; + +function isVersionOfPandasSupported(version: SemVer) { + return version.compare(minimumSupportedPandaVersion) > 0; +} + +/** + * Responsible for managing dependencies of a Data Viewer. + */ +@injectable() +export class DataViewerDependencyService { + constructor( + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(IInstaller) private readonly installer: IInstaller, + @inject(IPythonExecutionFactory) private pythonFactory: IPythonExecutionFactory + ) {} + + public async checkAndInstallMissingDependencies( + interpreter?: PythonEnvironment, + token?: CancellationToken + ): Promise { + const pandasVersion = await this.getVersionOfPandas(interpreter, token); + if (Cancellation.isCanceled(token)) { + return; + } + + if (pandasVersion) { + if (isVersionOfPandasSupported(pandasVersion)) { + return; + } + sendTelemetryEvent(Telemetry.PandasTooOld); + // Warn user that we cannot start because pandas is too old. + const versionStr = `${pandasVersion.major}.${pandasVersion.minor}.${pandasVersion.build}`; + throw new Error(DataScience.pandasTooOldForViewingFormat().format(versionStr)); + } + + sendTelemetryEvent(Telemetry.PandasNotInstalled); + await this.installMissingDependencies(interpreter, token); + } + + private async installMissingDependencies( + interpreter?: PythonEnvironment, + token?: CancellationToken + ): Promise { + const selection = await this.applicationShell.showErrorMessage( + DataScience.pandasRequiredForViewing(), + Common.install() + ); + + if (Cancellation.isCanceled(token)) { + return; + } + + if (selection === Common.install()) { + const cancellatonPromise = createPromiseFromCancellation({ + cancelAction: 'resolve', + defaultValue: InstallerResponse.Ignore, + token + }); + // Always pass a cancellation token to `install`, to ensure it waits until the module is installed. + const response = await Promise.race([ + this.installer.install(Product.pandas, interpreter, wrapCancellationTokens(token)), + cancellatonPromise + ]); + if (response === InstallerResponse.Installed) { + sendTelemetryEvent(Telemetry.UserInstalledPandas); + } + } else { + sendTelemetryEvent(Telemetry.UserDidNotInstallPandas); + throw new Error(DataScience.pandasRequiredForViewing()); + } + } + + private async getVersionOfPandas( + interpreter?: PythonEnvironment, + token?: CancellationToken + ): Promise { + const launcher = await this.pythonFactory.createActivatedEnvironment({ + resource: undefined, + interpreter, + allowEnvironmentFetchExceptions: true, + bypassCondaExecution: true + }); + try { + const result = await launcher.exec(['-c', 'import pandas;print(pandas.__version__)'], { + throwOnStdErr: true, + token + }); + const versionMatch = /^\s*(\d+)\.(\d+)\.(.+)\s*$/.exec(result.stdout); + if (versionMatch && versionMatch.length > 2) { + const major = parseInt(versionMatch[1], 10); + const minor = parseInt(versionMatch[2], 10); + const build = parseInt(versionMatch[3], 10); + return parse(`${major}.${minor}.${build}`, true) ?? undefined; + } + } catch (ex) { + traceWarning('Failed to get version of Pandas to use Data Viewer', ex); + return; + } + } +} diff --git a/src/client/datascience/data-viewing/dataViewerFactory.ts b/src/client/datascience/data-viewing/dataViewerFactory.ts new file mode 100644 index 000000000000..ce64481e6c41 --- /dev/null +++ b/src/client/datascience/data-viewing/dataViewerFactory.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; + +import { IAsyncDisposable, IAsyncDisposableRegistry } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IDataViewer, IDataViewerDataProvider, IDataViewerFactory } from './types'; + +@injectable() +export class DataViewerFactory implements IDataViewerFactory, IAsyncDisposable { + private activeExplorers: IDataViewer[] = []; + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry + ) { + asyncRegistry.push(this); + } + + public async dispose() { + await Promise.all(this.activeExplorers.map((d) => d.dispose())); + } + + public async create(dataProvider: IDataViewerDataProvider, title: string): Promise { + let result: IDataViewer | undefined; + + // Create the data explorer + const dataExplorer = this.serviceContainer.get(IDataViewer); + try { + // Then load the data. + this.activeExplorers.push(dataExplorer); + + // Show the window and the data + await dataExplorer.showData(dataProvider, title); + result = dataExplorer; + } finally { + if (!result) { + // If throw any errors, close the window we opened. + dataExplorer.dispose(); + } + } + return result; + } +} diff --git a/src/client/datascience/data-viewing/dataViewerMessageListener.ts b/src/client/datascience/data-viewing/dataViewerMessageListener.ts new file mode 100644 index 000000000000..7d9cb03526a9 --- /dev/null +++ b/src/client/datascience/data-viewing/dataViewerMessageListener.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { IWebviewPanel, IWebviewPanelMessageListener } from '../../common/application/types'; + +// tslint:disable:no-any + +// This class listens to messages that come from the local Data Explorer window +export class DataViewerMessageListener implements IWebviewPanelMessageListener { + private disposedCallback: () => void; + private callback: (message: string, payload: any) => void; + private viewChanged: (panel: IWebviewPanel) => void; + + constructor( + callback: (message: string, payload: any) => void, + viewChanged: (panel: IWebviewPanel) => void, + disposed: () => void + ) { + // Save our dispose callback so we remove our interactive window + this.disposedCallback = disposed; + + // Save our local callback so we can handle the non broadcast case(s) + this.callback = callback; + + // Save view changed so we can forward view change events. + this.viewChanged = viewChanged; + } + + public async dispose() { + this.disposedCallback(); + } + + public onMessage(message: string, payload: any) { + // Send to just our local callback. + this.callback(message, payload); + } + + public onChangeViewState(panel: IWebviewPanel) { + // Forward this onto our callback + this.viewChanged(panel); + } +} diff --git a/src/client/datascience/data-viewing/jupyterVariableDataProvider.ts b/src/client/datascience/data-viewing/jupyterVariableDataProvider.ts new file mode 100644 index 000000000000..58a4982edea9 --- /dev/null +++ b/src/client/datascience/data-viewing/jupyterVariableDataProvider.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable, named } from 'inversify'; + +import { Identifiers } from '../constants'; +import { IJupyterVariable, IJupyterVariableDataProvider, IJupyterVariables, INotebook } from '../types'; +import { DataViewerDependencyService } from './dataViewerDependencyService'; +import { ColumnType, IDataFrameInfo, IRowsResponse } from './types'; + +@injectable() +export class JupyterVariableDataProvider implements IJupyterVariableDataProvider { + private initialized: boolean = false; + private notebook: INotebook | undefined; + private variable: IJupyterVariable | undefined; + + constructor( + @inject(IJupyterVariables) @named(Identifiers.ALL_VARIABLES) private variableManager: IJupyterVariables, + @inject(DataViewerDependencyService) private dependencyService: DataViewerDependencyService + ) {} + + /** + * Normalizes column types to the types the UI component understands. + * Defaults to 'string'. + * @param columns + * @returns Array of columns with normalized type + */ + private static getNormalizedColumns(columns: { key: string; type: string }[]): { key: string; type: ColumnType }[] { + return columns.map((column: { key: string; type: string }) => { + let normalizedType: ColumnType; + switch (column.type) { + case 'bool': + normalizedType = ColumnType.Bool; + break; + case 'integer': + case 'int32': + case 'int64': + case 'float': + case 'float32': + case 'float64': + case 'number': + normalizedType = ColumnType.Number; + break; + default: + normalizedType = ColumnType.String; + } + return { + key: column.key, + type: normalizedType + }; + }); + } + + public dispose(): void { + return; + } + + public setDependencies(variable: IJupyterVariable, notebook: INotebook): void { + this.notebook = notebook; + this.variable = variable; + } + + public async getDataFrameInfo(): Promise { + let dataFrameInfo: IDataFrameInfo = {}; + await this.ensureInitialized(); + if (this.variable && this.notebook) { + dataFrameInfo = { + columns: this.variable.columns + ? JupyterVariableDataProvider.getNormalizedColumns(this.variable.columns) + : this.variable.columns, + indexColumn: this.variable.indexColumn, + rowCount: this.variable.rowCount + }; + } + return dataFrameInfo; + } + + public async getAllRows() { + let allRows: IRowsResponse = []; + await this.ensureInitialized(); + if (this.variable && this.variable.rowCount && this.notebook) { + const dataFrameRows = await this.variableManager.getDataFrameRows( + this.variable, + this.notebook, + 0, + this.variable.rowCount + ); + allRows = dataFrameRows && dataFrameRows.data ? (dataFrameRows.data as IRowsResponse) : []; + } + return allRows; + } + + public async getRows(start: number, end: number) { + let rows: IRowsResponse = []; + await this.ensureInitialized(); + if (this.variable && this.variable.rowCount && this.notebook) { + const dataFrameRows = await this.variableManager.getDataFrameRows(this.variable, this.notebook, start, end); + rows = dataFrameRows && dataFrameRows.data ? (dataFrameRows.data as IRowsResponse) : []; + } + return rows; + } + + private async ensureInitialized(): Promise { + // Postpone pre-req and variable initialization until data is requested. + if (!this.initialized && this.variable && this.notebook) { + this.initialized = true; + await this.dependencyService.checkAndInstallMissingDependencies(this.notebook.getMatchingInterpreter()); + this.variable = await this.variableManager.getDataFrameInfo(this.variable, this.notebook); + } + } +} diff --git a/src/client/datascience/data-viewing/jupyterVariableDataProviderFactory.ts b/src/client/datascience/data-viewing/jupyterVariableDataProviderFactory.ts new file mode 100644 index 000000000000..b8b48b45a4b3 --- /dev/null +++ b/src/client/datascience/data-viewing/jupyterVariableDataProviderFactory.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; + +import { IServiceContainer } from '../../ioc/types'; +import { + IJupyterVariable, + IJupyterVariableDataProvider, + IJupyterVariableDataProviderFactory, + INotebook +} from '../types'; + +@injectable() +export class JupyterVariableDataProviderFactory implements IJupyterVariableDataProviderFactory { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + + public async create(variable: IJupyterVariable, notebook: INotebook): Promise { + const jupyterVariableDataProvider = this.serviceContainer.get( + IJupyterVariableDataProvider + ); + jupyterVariableDataProvider.setDependencies(variable, notebook); + return jupyterVariableDataProvider; + } +} diff --git a/src/client/datascience/data-viewing/types.ts b/src/client/datascience/data-viewing/types.ts new file mode 100644 index 000000000000..df3c97bc5cd5 --- /dev/null +++ b/src/client/datascience/data-viewing/types.ts @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { IDisposable } from '../../common/types'; +import { SharedMessages } from '../messages'; + +export const CellFetchAllLimit = 100000; +export const CellFetchSizeFirst = 100000; +export const CellFetchSizeSubsequent = 1000000; +export const MaxStringCompare = 200; +export const ColumnWarningSize = 1000; // Anything over this takes too long to load + +export namespace DataViewerRowStates { + export const Fetching = 'fetching'; + export const Skipped = 'skipped'; +} + +export namespace DataViewerMessages { + export const Started = SharedMessages.Started; + export const UpdateSettings = SharedMessages.UpdateSettings; + export const InitializeData = 'init'; + export const GetAllRowsRequest = 'get_all_rows_request'; + export const GetAllRowsResponse = 'get_all_rows_response'; + export const GetRowsRequest = 'get_rows_request'; + export const GetRowsResponse = 'get_rows_response'; + export const CompletedData = 'complete'; +} + +export interface IGetRowsRequest { + start: number; + end: number; +} + +export interface IGetRowsResponse { + rows: IRowsResponse; + start: number; + end: number; +} + +// Map all messages to specific payloads +export type IDataViewerMapping = { + [DataViewerMessages.Started]: never | undefined; + [DataViewerMessages.UpdateSettings]: string; + [DataViewerMessages.InitializeData]: IDataFrameInfo; + [DataViewerMessages.GetAllRowsRequest]: never | undefined; + [DataViewerMessages.GetAllRowsResponse]: IRowsResponse; + [DataViewerMessages.GetRowsRequest]: IGetRowsRequest; + [DataViewerMessages.GetRowsResponse]: IGetRowsResponse; + [DataViewerMessages.CompletedData]: never | undefined; +}; + +export interface IDataFrameInfo { + columns?: { key: string; type: ColumnType }[]; + indexColumn?: string; + rowCount?: number; +} + +export interface IDataViewerDataProvider { + dispose(): void; + getDataFrameInfo(): Promise; + getAllRows(): Promise; + getRows(start: number, end: number): Promise; +} + +export enum ColumnType { + String = 'string', + Number = 'number', + Bool = 'bool' +} + +// tslint:disable-next-line: no-any +export type IRowsResponse = any[]; + +export const IDataViewerFactory = Symbol('IDataViewerFactory'); +export interface IDataViewerFactory { + create(dataProvider: IDataViewerDataProvider, title: string): Promise; +} + +export const IDataViewer = Symbol('IDataViewer'); +export interface IDataViewer extends IDisposable { + showData(dataProvider: IDataViewerDataProvider, title: string): Promise; +} diff --git a/src/client/datascience/dataScienceFileSystem.ts b/src/client/datascience/dataScienceFileSystem.ts new file mode 100644 index 000000000000..47eb20fb5104 --- /dev/null +++ b/src/client/datascience/dataScienceFileSystem.ts @@ -0,0 +1,218 @@ +import * as fs from 'fs-extra'; +import * as glob from 'glob'; +import { injectable } from 'inversify'; +import * as tmp from 'tmp'; +import { promisify } from 'util'; +import { FileStat, FileSystem, Uri, workspace } from 'vscode'; +import { traceError } from '../common/logger'; +import { createDirNotEmptyError, isFileNotFoundError } from '../common/platform/errors'; +import { convertFileType, convertStat, getHashString } from '../common/platform/fileSystem'; +import { FileSystemPathUtils } from '../common/platform/fs-paths'; +import { FileType, IFileSystemPathUtils, TemporaryFile } from '../common/platform/types'; +import { IDataScienceFileSystem } from './types'; + +const ENCODING = 'utf8'; + +/** + * File system abstraction which wraps the VS Code API. + */ +@injectable() +export class DataScienceFileSystem implements IDataScienceFileSystem { + protected vscfs: FileSystem; + private globFiles: (pat: string, options?: { cwd: string; dot?: boolean }) => Promise; + private fsPathUtils: IFileSystemPathUtils; + constructor() { + this.globFiles = promisify(glob); + this.fsPathUtils = FileSystemPathUtils.withDefaults(); + this.vscfs = workspace.fs; + } + + public async appendLocalFile(path: string, text: string): Promise { + return fs.appendFile(path, text); + } + + public areLocalPathsSame(path1: string, path2: string): boolean { + return this.fsPathUtils.arePathsSame(path1, path2); + } + + public async createLocalDirectory(path: string): Promise { + await this.createDirectory(Uri.file(path)); + } + + public createLocalWriteStream(path: string): fs.WriteStream { + return fs.createWriteStream(path); + } + + public async copyLocal(source: string, destination: string): Promise { + const srcUri = Uri.file(source); + const dstUri = Uri.file(destination); + await this.vscfs.copy(srcUri, dstUri, { overwrite: true }); + } + + public async createTemporaryLocalFile(suffix: string, mode?: number): Promise { + const opts = { + postfix: suffix, + mode + }; + return new Promise((resolve, reject) => { + tmp.file(opts, (err, filename, _fd, cleanUp) => { + if (err) { + return reject(err); + } + resolve({ + filePath: filename, + dispose: cleanUp + }); + }); + }); + } + + public async deleteLocalDirectory(dirname: string) { + const uri = Uri.file(dirname); + // The "recursive" option disallows directories, even if they + // are empty. So we have to deal with this ourselves. + const files = await this.vscfs.readDirectory(uri); + if (files && files.length > 0) { + throw createDirNotEmptyError(dirname); + } + return this.vscfs.delete(uri, { + recursive: true, + useTrash: false + }); + } + + public async deleteLocalFile(path: string): Promise { + const uri = Uri.file(path); + return this.vscfs.delete(uri, { + recursive: false, + useTrash: false + }); + } + + public getDisplayName(filename: string, cwd?: string): string { + return this.fsPathUtils.getDisplayName(filename, cwd); + } + + public async getFileHash(filename: string): Promise { + // The reason for lstat rather than stat is not clear... + const stat = await this.lstat(filename); + const data = `${stat.ctime}-${stat.mtime}`; + return getHashString(data); + } + + public async localDirectoryExists(dirname: string): Promise { + return this.localPathExists(dirname, FileType.Directory); + } + + public async localFileExists(filename: string): Promise { + return this.localPathExists(filename, FileType.File); + } + + public async readLocalData(filename: string): Promise { + const uri = Uri.file(filename); + const data = await this.vscfs.readFile(uri); + return Buffer.from(data); + } + + public async readLocalFile(filename: string): Promise { + const uri = Uri.file(filename); + return this.readFile(uri); + } + + public async searchLocal(globPattern: string, cwd?: string, dot?: boolean): Promise { + // tslint:disable-next-line: no-any + let options: any; + if (cwd) { + options = { ...options, cwd }; + } + if (dot) { + options = { ...options, dot }; + } + + const found = await this.globFiles(globPattern, options); + return Array.isArray(found) ? found : []; + } + + public async writeLocalFile(filename: string, text: string | Buffer): Promise { + const uri = Uri.file(filename); + return this.writeFile(uri, text); + } + + // URI-based filesystem functions for interacting with files provided by VS Code + public arePathsSame(path1: Uri, path2: Uri): boolean { + if (path1.scheme === 'file' && path1.scheme === path2.scheme) { + return this.areLocalPathsSame(path1.fsPath, path2.fsPath); + } else { + return path1.toString() === path2.toString(); + } + } + + public async copy(source: Uri, destination: Uri): Promise { + await this.vscfs.copy(source, destination); + } + + public async createDirectory(uri: Uri): Promise { + await this.vscfs.createDirectory(uri); + } + + public async delete(uri: Uri): Promise { + await this.vscfs.delete(uri); + } + + public async readFile(uri: Uri): Promise { + const result = await this.vscfs.readFile(uri); + const data = Buffer.from(result); + return data.toString(ENCODING); + } + + public async stat(uri: Uri): Promise { + return this.vscfs.stat(uri); + } + + public async writeFile(uri: Uri, text: string | Buffer): Promise { + const data = typeof text === 'string' ? Buffer.from(text) : text; + return this.vscfs.writeFile(uri, data); + } + + private async lstat(filename: string): Promise { + // tslint:disable-next-line: no-suspicious-comment + // TODO https://github.com/microsoft/vscode/issues/71204 (84514)): + // This functionality has been requested for the VS Code API. + const stat = await fs.lstat(filename); + // Note that, unlike stat(), lstat() does not include the type + // of the symlink's target. + const fileType = convertFileType(stat); + return convertStat(stat, fileType); + } + + private async localPathExists( + // the "file" to look for + filename: string, + // the file type to expect; if not provided then any file type + // matches; otherwise a mismatch results in a "false" value + fileType?: FileType + ): Promise { + let stat: FileStat; + try { + // Note that we are using stat() rather than lstat(). This + // means that any symlinks are getting resolved. + const uri = Uri.file(filename); + stat = await this.stat(uri); + } catch (err) { + if (isFileNotFoundError(err)) { + return false; + } + traceError(`stat() failed for "${filename}"`, err); + return false; + } + + if (fileType === undefined) { + return true; + } + if (fileType === FileType.Unknown) { + // FileType.Unknown == 0, hence do not use bitwise operations. + return stat.type === FileType.Unknown; + } + return (stat.type & fileType) === fileType; + } +} diff --git a/src/client/datascience/dataScienceSurveyBanner.ts b/src/client/datascience/dataScienceSurveyBanner.ts new file mode 100644 index 000000000000..cd63823c1778 --- /dev/null +++ b/src/client/datascience/dataScienceSurveyBanner.ts @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Event, EventEmitter } from 'vscode'; +import { IApplicationShell } from '../common/application/types'; +import '../common/extensions'; +import { + BANNER_NAME_DS_SURVEY, + IBrowserService, + IPersistentStateFactory, + IPythonExtensionBanner +} from '../common/types'; +import * as localize from '../common/utils/localize'; +import { noop } from '../common/utils/misc'; +import { InteractiveWindowMessages, IReExecuteCells } from './interactive-common/interactiveWindowTypes'; +import { IInteractiveWindowListener, INotebookEditorProvider } from './types'; + +export enum DSSurveyStateKeys { + ShowBanner = 'ShowDSSurveyBanner', + OpenNotebookCount = 'DS_OpenNotebookCount', + ExecutionCount = 'DS_ExecutionCount' +} + +enum DSSurveyLabelIndex { + Yes, + No +} + +const NotebookOpenThreshold = 5; +const NotebookExecutionThreshold = 100; + +@injectable() +export class DataScienceSurveyBannerLogger implements IInteractiveWindowListener { + // tslint:disable-next-line: no-any + private postEmitter = new EventEmitter<{ message: string; payload: any }>(); + constructor( + @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, + @inject(IPythonExtensionBanner) + @named(BANNER_NAME_DS_SURVEY) + private readonly dataScienceSurveyBanner: IPythonExtensionBanner + ) {} + // tslint:disable-next-line: no-any + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + // tslint:disable-next-line: no-any + public onMessage(message: string, payload?: any): void { + if (message === InteractiveWindowMessages.ReExecuteCells) { + const args = payload as IReExecuteCells; + if (args && args.cellIds.length) { + const state = this.persistentState.createGlobalPersistentState( + DSSurveyStateKeys.ExecutionCount, + 0 + ); + state + .updateValue(state.value + args.cellIds.length) + .then(() => { + // On every update try to show the banner. + return this.dataScienceSurveyBanner.showBanner(); + }) + .ignoreErrors(); + } + } + } + public dispose(): void | undefined { + noop(); + } +} + +@injectable() +export class DataScienceSurveyBanner implements IPythonExtensionBanner { + private disabledInCurrentSession: boolean = false; + private isInitialized: boolean = false; + private bannerMessage: string = localize.DataScienceSurveyBanner.bannerMessage(); + private bannerLabels: string[] = [ + localize.DataScienceSurveyBanner.bannerLabelYes(), + localize.DataScienceSurveyBanner.bannerLabelNo() + ]; + private readonly surveyLink: string; + + constructor( + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, + @inject(IBrowserService) private browserService: IBrowserService, + @inject(INotebookEditorProvider) editorProvider: INotebookEditorProvider, + surveyLink: string = 'https://aka.ms/pyaisurvey' + ) { + this.surveyLink = surveyLink; + this.initialize(); + editorProvider.onDidOpenNotebookEditor(this.openedNotebook.bind(this)); + } + + public initialize(): void { + if (this.isInitialized) { + return; + } + this.isInitialized = true; + } + public get enabled(): boolean { + return this.persistentState.createGlobalPersistentState(DSSurveyStateKeys.ShowBanner, true).value; + } + + public async showBanner(): Promise { + if (!this.enabled || this.disabledInCurrentSession) { + return; + } + + const executionCount: number = this.getExecutionCount(); + const notebookCount: number = this.getOpenNotebookCount(); + const show = await this.shouldShowBanner(executionCount, notebookCount); + if (!show) { + return; + } + + const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels); + switch (response) { + case this.bannerLabels[DSSurveyLabelIndex.Yes]: { + await this.launchSurvey(); + await this.disable(); + break; + } + case this.bannerLabels[DSSurveyLabelIndex.No]: { + await this.disable(); + break; + } + default: { + // Disable for the current session. + this.disabledInCurrentSession = true; + } + } + } + + public async shouldShowBanner(executionCount: number, notebookOpenCount: number): Promise { + if (!this.enabled || this.disabledInCurrentSession) { + return false; + } + + return executionCount >= NotebookExecutionThreshold || notebookOpenCount > NotebookOpenThreshold; + } + + public async disable(): Promise { + await this.persistentState + .createGlobalPersistentState(DSSurveyStateKeys.ShowBanner, false) + .updateValue(false); + } + + public async launchSurvey(): Promise { + this.browserService.launch(this.surveyLink); + } + + private getOpenNotebookCount(): number { + const state = this.persistentState.createGlobalPersistentState(DSSurveyStateKeys.OpenNotebookCount, 0); + return state.value; + } + + private getExecutionCount(): number { + const state = this.persistentState.createGlobalPersistentState(DSSurveyStateKeys.ExecutionCount, 0); + return state.value; + } + + private async openedNotebook() { + const state = this.persistentState.createGlobalPersistentState(DSSurveyStateKeys.OpenNotebookCount, 0); + await state.updateValue(state.value + 1); + return this.showBanner(); + } +} diff --git a/src/client/datascience/datascience.ts b/src/client/datascience/datascience.ts new file mode 100644 index 000000000000..9109ca732d94 --- /dev/null +++ b/src/client/datascience/datascience.ts @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { JSONObject } from '@phosphor/coreutils'; +import { inject, injectable } from 'inversify'; +import * as vscode from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { PYTHON_ALLFILES, PYTHON_LANGUAGE } from '../common/constants'; +import { ContextKey } from '../common/contextKey'; +import '../common/extensions'; +import { IConfigurationService, IDisposable, IDisposableRegistry, IExtensionContext } from '../common/types'; +import { debounceAsync, swallowExceptions } from '../common/utils/decorators'; +import { sendTelemetryEvent } from '../telemetry'; +import { hasCells } from './cellFactory'; +import { CommandRegistry } from './commands/commandRegistry'; +import { EditorContexts, Telemetry } from './constants'; +import { IDataScience, IDataScienceCodeLensProvider } from './types'; + +@injectable() +export class DataScience implements IDataScience { + public isDisposed: boolean = false; + private changeHandler: IDisposable | undefined; + private startTime: number = Date.now(); + constructor( + @inject(ICommandManager) private commandManager: ICommandManager, + @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, + @inject(IExtensionContext) private extensionContext: IExtensionContext, + @inject(IDataScienceCodeLensProvider) private dataScienceCodeLensProvider: IDataScienceCodeLensProvider, + @inject(IConfigurationService) private configuration: IConfigurationService, + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IWorkspaceService) private workspace: IWorkspaceService, + @inject(CommandRegistry) private commandRegistry: CommandRegistry + ) { + this.disposableRegistry.push(this.commandRegistry); + } + + public get activationStartTime(): number { + return this.startTime; + } + + public async activate(): Promise { + this.commandRegistry.register(); + + this.extensionContext.subscriptions.push( + vscode.languages.registerCodeLensProvider(PYTHON_ALLFILES, this.dataScienceCodeLensProvider) + ); + + // Set our initial settings and sign up for changes + this.onSettingsChanged(); + this.changeHandler = this.configuration.getSettings(undefined).onDidChange(this.onSettingsChanged.bind(this)); + this.disposableRegistry.push(this); + + // Listen for active editor changes so we can detect have code cells or not + this.disposableRegistry.push( + this.documentManager.onDidChangeActiveTextEditor(() => this.onChangedActiveTextEditor()) + ); + this.onChangedActiveTextEditor(); + + // Send telemetry for all of our settings + this.sendSettingsTelemetry().ignoreErrors(); + } + + public async dispose() { + if (this.changeHandler) { + this.changeHandler.dispose(); + this.changeHandler = undefined; + } + } + + private onSettingsChanged = () => { + const settings = this.configuration.getSettings(undefined); + const enabled = settings.datascience.enabled; + let editorContext = new ContextKey(EditorContexts.DataScienceEnabled, this.commandManager); + editorContext.set(enabled).catch(); + const ownsSelection = settings.datascience.sendSelectionToInteractiveWindow; + editorContext = new ContextKey(EditorContexts.OwnsSelection, this.commandManager); + editorContext.set(ownsSelection && enabled).catch(); + }; + + private onChangedActiveTextEditor() { + // Setup the editor context for the cells + const editorContext = new ContextKey(EditorContexts.HasCodeCells, this.commandManager); + const activeEditor = this.documentManager.activeTextEditor; + + if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { + // Inform the editor context that we have cells, fire and forget is ok on the promise here + // as we don't care to wait for this context to be set and we can't do anything if it fails + editorContext.set(hasCells(activeEditor.document, this.configuration.getSettings().datascience)).catch(); + } else { + editorContext.set(false).catch(); + } + } + + @debounceAsync(1) + @swallowExceptions('Sending DataScience Settings Telemetry failed') + private async sendSettingsTelemetry(): Promise { + // Get our current settings. This is what we want to send. + // tslint:disable-next-line:no-any + const settings = this.configuration.getSettings().datascience as any; + + // Translate all of the 'string' based settings into known values or not. + const pythonConfig = this.workspace.getConfiguration('python'); + if (pythonConfig) { + const keys = Object.keys(settings); + const resultSettings: JSONObject = {}; + for (const k of keys) { + const currentValue = settings[k]; + if (typeof currentValue === 'string' && k !== 'interactiveWindowMode') { + const inspectResult = pythonConfig.inspect(`dataScience.${k}`); + if (inspectResult && inspectResult.defaultValue !== currentValue) { + resultSettings[k] = 'non-default'; + } else { + resultSettings[k] = 'default'; + } + } else { + resultSettings[k] = currentValue; + } + } + sendTelemetryEvent(Telemetry.DataScienceSettings, 0, resultSettings); + } + } +} diff --git a/src/client/datascience/debugLocationTracker.ts b/src/client/datascience/debugLocationTracker.ts new file mode 100644 index 000000000000..6642b3f44652 --- /dev/null +++ b/src/client/datascience/debugLocationTracker.ts @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { DebugAdapterTracker, Event, EventEmitter } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +import { IDebugLocation } from './types'; + +// When a python debugging session is active keep track of the current debug location +export class DebugLocationTracker implements DebugAdapterTracker { + private waitingForStackTrace: boolean = false; + private _debugLocation: IDebugLocation | undefined; + private debugLocationUpdatedEvent: EventEmitter = new EventEmitter(); + private sessionEndedEmitter: EventEmitter = new EventEmitter(); + + constructor(private _sessionId: string) { + this.DebugLocation = undefined; + } + + public get sessionId() { + return this._sessionId; + } + + public get sessionEnded(): Event { + return this.sessionEndedEmitter.event; + } + + public get debugLocationUpdated(): Event { + return this.debugLocationUpdatedEvent.event; + } + + public get debugLocation(): IDebugLocation | undefined { + return this._debugLocation; + } + + // tslint:disable-next-line:no-any + public onDidSendMessage(message: DebugProtocol.ProtocolMessage) { + if (this.isStopEvent(message)) { + // Some type of stop, wait to see our next stack trace to find our location + this.waitingForStackTrace = true; + } + + if (this.isContinueEvent(message)) { + // Running, clear the location + this.DebugLocation = undefined; + this.waitingForStackTrace = false; + } + + if (this.waitingForStackTrace) { + // If we are waiting for a stack track, check our messages for one + const debugLoc = this.getStackTrace(message); + if (debugLoc) { + this.DebugLocation = debugLoc; + this.waitingForStackTrace = false; + } + } + } + + public onWillStopSession() { + this.sessionEndedEmitter.fire(this); + } + + // Set our new location and fire our debug event + private set DebugLocation(newLocation: IDebugLocation | undefined) { + const oldLocation = this._debugLocation; + this._debugLocation = newLocation; + + if (this._debugLocation !== oldLocation) { + this.debugLocationUpdatedEvent.fire(); + } + } + + // tslint:disable-next-line:no-any + private isStopEvent(message: DebugProtocol.ProtocolMessage) { + if (message.type === 'event') { + const eventMessage = message as DebugProtocol.Event; + if (eventMessage.event === 'stopped') { + return true; + } + } + + return false; + } + + // tslint:disable-next-line:no-any + private getStackTrace(message: DebugProtocol.ProtocolMessage): IDebugLocation | undefined { + if (message.type === 'response') { + const responseMessage = message as DebugProtocol.Response; + if (responseMessage.command === 'stackTrace') { + const messageBody = responseMessage.body; + if (messageBody.stackFrames.length > 0) { + const lineNumber = messageBody.stackFrames[0].line; + const fileName = this.normalizeFilePath(messageBody.stackFrames[0].source.path); + const column = messageBody.stackFrames[0].column; + return { lineNumber, fileName, column }; + } + } + } + + return undefined; + } + + private normalizeFilePath(path: string): string { + // Make the path match the os. Debugger seems to return + // invalid path chars on linux/darwin + if (process.platform !== 'win32') { + return path.replace(/\\/g, '/'); + } + return path; + } + + // tslint:disable-next-line:no-any + private isContinueEvent(message: DebugProtocol.ProtocolMessage): boolean { + if (message.type === 'event') { + const eventMessage = message as DebugProtocol.Event; + if (eventMessage.event === 'continue') { + return true; + } + } else if (message.type === 'response') { + const responseMessage = message as DebugProtocol.Response; + if (responseMessage.command === 'continue') { + return true; + } + } + + return false; + } +} diff --git a/src/client/datascience/debugLocationTrackerFactory.ts b/src/client/datascience/debugLocationTrackerFactory.ts new file mode 100644 index 000000000000..d31287410c1a --- /dev/null +++ b/src/client/datascience/debugLocationTrackerFactory.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { + DebugAdapterTracker, + DebugAdapterTrackerFactory, + DebugSession, + Event, + EventEmitter, + ProviderResult +} from 'vscode'; + +import { IDebugService } from '../common/application/types'; +import { IDisposableRegistry } from '../common/types'; +import { DebugLocationTracker } from './debugLocationTracker'; +import { IDebugLocationTracker } from './types'; + +// Hook up our IDebugLocationTracker to python debugging sessions +@injectable() +export class DebugLocationTrackerFactory implements IDebugLocationTracker, DebugAdapterTrackerFactory { + private activeTrackers: Map = new Map(); + private updatedEmitter: EventEmitter = new EventEmitter(); + + constructor( + @inject(IDebugService) debugService: IDebugService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry + ) { + disposableRegistry.push(debugService.registerDebugAdapterTrackerFactory('python', this)); + } + + public createDebugAdapterTracker(session: DebugSession): ProviderResult { + const result = new DebugLocationTracker(session.id); + this.activeTrackers.set(session.id, result); + result.sessionEnded(this.onSessionEnd.bind(this)); + result.debugLocationUpdated(this.onLocationUpdated.bind(this)); + this.onLocationUpdated(); + return result; + } + + public get updated(): Event { + return this.updatedEmitter.event; + } + + public getLocation(session: DebugSession) { + const tracker = this.activeTrackers.get(session.id); + if (tracker) { + return tracker.debugLocation; + } + } + + private onSessionEnd(locationTracker: DebugLocationTracker) { + this.activeTrackers.delete(locationTracker.sessionId); + } + + private onLocationUpdated() { + this.updatedEmitter.fire(); + } +} diff --git a/src/client/datascience/editor-integration/cellhashprovider.ts b/src/client/datascience/editor-integration/cellhashprovider.ts new file mode 100644 index 000000000000..7f9f14a9de8e --- /dev/null +++ b/src/client/datascience/editor-integration/cellhashprovider.ts @@ -0,0 +1,443 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { KernelMessage } from '@jupyterlab/services'; +import * as hashjs from 'hash.js'; +import { inject, injectable, multiInject, optional } from 'inversify'; +import stripAnsi from 'strip-ansi'; +import { Event, EventEmitter, Position, Range, TextDocumentChangeEvent, TextDocumentContentChangeEvent } from 'vscode'; + +import { splitMultilineString } from '../../../datascience-ui/common'; +import { IDebugService, IDocumentManager } from '../../common/application/types'; +import { traceError, traceInfo } from '../../common/logger'; + +import { IConfigurationService } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { getCellResource } from '../cellFactory'; +import { CellMatcher } from '../cellMatcher'; +import { Identifiers } from '../constants'; +import { + ICell, + ICellHash, + ICellHashListener, + ICellHashProvider, + IDataScienceFileSystem, + IFileHashes, + INotebook, + INotebookExecutionLogger +} from '../types'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const _escapeRegExp = require('lodash/escapeRegExp') as typeof import('lodash/escapeRegExp'); // NOSONAR +const LineNumberMatchRegex = /(;32m[ ->]*?)(\d+)/g; + +interface IRangedCellHash extends ICellHash { + code: string; + startOffset: number; + endOffset: number; + deleted: boolean; + realCode: string; + trimmedRightCode: string; + firstNonBlankLineIndex: number; // zero based. First non blank line of the real code. +} + +// This class provides hashes for debugging jupyter cells. Call getHashes just before starting debugging to compute all of the +// hashes for cells. +@injectable() +export class CellHashProvider implements ICellHashProvider, INotebookExecutionLogger { + // tslint:disable-next-line: no-any + private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ + message: string; + // tslint:disable-next-line: no-any + payload: any; + }>(); + // Map of file to Map of start line to actual hash + private executionCount: number = 0; + private hashes: Map = new Map(); + private updateEventEmitter: EventEmitter = new EventEmitter(); + private traceBackRegexes = new Map(); + + constructor( + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IDebugService) private debugService: IDebugService, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @multiInject(ICellHashListener) @optional() private listeners: ICellHashListener[] | undefined + ) { + // Watch document changes so we can update our hashes + this.documentManager.onDidChangeTextDocument(this.onChangedDocument.bind(this)); + } + + public dispose() { + this.hashes.clear(); + this.traceBackRegexes.clear(); + } + + public get updated(): Event { + return this.updateEventEmitter.event; + } + + // tslint:disable-next-line: no-any + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + + public getHashes(): IFileHashes[] { + return [...this.hashes.entries()] + .map((e) => { + return { + file: e[0], + hashes: e[1].filter((h) => !h.deleted) + }; + }) + .filter((e) => e.hashes.length > 0); + } + + public onKernelRestarted() { + this.hashes.clear(); + this.traceBackRegexes.clear(); + this.executionCount = 0; + this.updateEventEmitter.fire(); + } + + public async preExecute(cell: ICell, silent: boolean): Promise { + try { + if (!silent) { + // Don't log empty cells + const stripped = this.extractExecutableLines(cell); + if (stripped.length > 0 && stripped.find((s) => s.trim().length > 0)) { + // When the user adds new code, we know the execution count is increasing + this.executionCount += 1; + + // Skip hash on unknown file though + if (cell.file !== Identifiers.EmptyFileName) { + await this.addCellHash(cell, this.executionCount); + } + } + } + } catch (exc) { + // Don't let exceptions in a preExecute mess up normal operation + traceError(exc); + } + } + + public async postExecute(_cell: ICell, _silent: boolean): Promise { + noop(); + } + + public preHandleIOPub(msg: KernelMessage.IIOPubMessage): KernelMessage.IIOPubMessage { + // When an error message comes, rewrite the traceback so we can jump back to the correct + // cell. For now this only works with the interactive window + if (msg.header.msg_type === 'error') { + return { + ...msg, + content: { + ...msg.content, + traceback: this.modifyTraceback(msg as KernelMessage.IErrorMsg) // NOSONAR + } + }; + } + return msg; + } + + public extractExecutableLines(cell: ICell): string[] { + const cellMatcher = new CellMatcher(this.configService.getSettings(getCellResource(cell)).datascience); + const lines = splitMultilineString(cell.data.source); + // Only strip this off the first line. Otherwise we want the markers in the code. + if (lines.length > 0 && (cellMatcher.isCode(lines[0]) || cellMatcher.isMarkdown(lines[0]))) { + return lines.slice(1); + } + return lines; + } + + public generateHashFileName(cell: ICell, expectedCount: number): string { + // First get the true lines from the cell + const { stripped } = this.extractStrippedLines(cell); + + // Then use that to make a hash value + const hashedCode = stripped.join(''); + const hash = hashjs.sha1().update(hashedCode).digest('hex').substr(0, 12); + return ``; + } + + // tslint:disable-next-line: cyclomatic-complexity + public async addCellHash(cell: ICell, expectedCount: number): Promise { + // Find the text document that matches. We need more information than + // the add code gives us + const doc = this.documentManager.textDocuments.find((d) => this.fs.areLocalPathsSame(d.fileName, cell.file)); + if (doc) { + // Compute the code that will really be sent to jupyter + const { stripped, trueStartLine } = this.extractStrippedLines(cell); + + const line = doc.lineAt(trueStartLine); + const endLine = doc.lineAt(Math.min(trueStartLine + stripped.length - 1, doc.lineCount - 1)); + + // Find the first non blank line + let firstNonBlankIndex = 0; + while (firstNonBlankIndex < stripped.length && stripped[firstNonBlankIndex].trim().length === 0) { + firstNonBlankIndex += 1; + } + + // Use the original values however to track edits. This is what we need + // to move around + const startOffset = doc.offsetAt(new Position(cell.line, 0)); + const endOffset = doc.offsetAt(endLine.rangeIncludingLineBreak.end); + + // Compute the runtime line and adjust our cell/stripped source for debugging + const runtimeLine = this.adjustRuntimeForDebugging(cell, stripped, startOffset, endOffset); + const hashedCode = stripped.join(''); + const realCode = doc.getText(new Range(new Position(cell.line, 0), endLine.rangeIncludingLineBreak.end)); + + const hash: IRangedCellHash = { + hash: hashjs.sha1().update(hashedCode).digest('hex').substr(0, 12), + line: line ? line.lineNumber + 1 : 1, + endLine: endLine ? endLine.lineNumber + 1 : 1, + firstNonBlankLineIndex: firstNonBlankIndex + trueStartLine, + executionCount: expectedCount, + startOffset, + endOffset, + deleted: false, + code: hashedCode, + trimmedRightCode: stripped.map((s) => s.replace(/[ \t\r]+\n$/g, '\n')).join(''), + realCode, + runtimeLine, + id: cell.id, + timestamp: Date.now() + }; + + traceInfo(`Adding hash for ${expectedCount} = ${hash.hash} with ${stripped.length} lines`); + + let list = this.hashes.get(cell.file); + if (!list) { + list = []; + } + + // Figure out where to put the item in the list + let inserted = false; + for (let i = 0; i < list.length && !inserted; i += 1) { + const pos = list[i]; + if (hash.line >= pos.line && hash.line <= pos.endLine) { + // Stick right here. This is either the same cell or a cell that overwrote where + // we were. + list.splice(i, 1, hash); + inserted = true; + } else if (pos.line > hash.line) { + // This item comes just after the cell we're inserting. + list.splice(i, 0, hash); + inserted = true; + } + } + if (!inserted) { + list.push(hash); + } + this.hashes.set(cell.file, list); + + // Save a regex to find this file later when looking for + // exceptions in output + if (!this.traceBackRegexes.has(cell.file)) { + const fileDisplayName = this.fs.getDisplayName(cell.file); + const escaped = _escapeRegExp(fileDisplayName); + const fileMatchRegex = new RegExp(`\\[.*?;32m${escaped}`); + this.traceBackRegexes.set(cell.file, fileMatchRegex); + } + + // Tell listeners we have new hashes. + if (this.listeners) { + const hashes = this.getHashes(); + await Promise.all(this.listeners.map((l) => l.hashesUpdated(hashes))); + + // Then fire our event + this.updateEventEmitter.fire(); + } + } + } + + public getExecutionCount(): number { + return this.executionCount; + } + + public incExecutionCount(): void { + this.executionCount += 1; + } + + private onChangedDocument(e: TextDocumentChangeEvent) { + // See if the document is in our list of docs to watch + const perFile = this.hashes.get(e.document.fileName); + if (perFile) { + // Apply the content changes to the file's cells. + const docText = e.document.getText(); + e.contentChanges.forEach((c) => { + this.handleContentChange(docText, c, perFile); + }); + } + } + + private extractStrippedLines(cell: ICell): { stripped: string[]; trueStartLine: number } { + // Compute the code that will really be sent to jupyter + const lines = splitMultilineString(cell.data.source); + const stripped = this.extractExecutableLines(cell); + + // Figure out our true 'start' line. This is what we need to tell the debugger is + // actually the start of the code as that's what Jupyter will be getting. + let trueStartLine = cell.line; + for (let i = 0; i < stripped.length; i += 1) { + if (stripped[i] !== lines[i]) { + trueStartLine += i + 1; + break; + } + } + + // Find the first non blank line + let firstNonBlankIndex = 0; + while (firstNonBlankIndex < stripped.length && stripped[firstNonBlankIndex].trim().length === 0) { + firstNonBlankIndex += 1; + } + + // Jupyter also removes blank lines at the end. Make sure only one + let lastLinePos = stripped.length - 1; + let nextToLastLinePos = stripped.length - 2; + while (nextToLastLinePos > 0) { + const lastLine = stripped[lastLinePos]; + const nextToLastLine = stripped[nextToLastLinePos]; + if ( + (lastLine.length === 0 || lastLine === '\n') && + (nextToLastLine.length === 0 || nextToLastLine === '\n') + ) { + stripped.splice(lastLinePos, 1); + lastLinePos -= 1; + nextToLastLinePos -= 1; + } else { + break; + } + } + // Make sure the last line with actual content ends with a linefeed + if (!stripped[lastLinePos].endsWith('\n') && stripped[lastLinePos].length > 0) { + stripped[lastLinePos] = `${stripped[lastLinePos]}\n`; + } + + return { stripped, trueStartLine }; + } + + private handleContentChange(docText: string, c: TextDocumentContentChangeEvent, hashes: IRangedCellHash[]) { + // First compute the number of lines that changed + const lineDiff = c.range.start.line - c.range.end.line + c.text.split('\n').length - 1; + const offsetDiff = c.text.length - c.rangeLength; + + // Compute the inclusive offset that is changed by the cell. + const endChangedOffset = c.rangeLength <= 0 ? c.rangeOffset : c.rangeOffset + c.rangeLength - 1; + + hashes.forEach((h) => { + // See how this existing cell compares to the change + if (h.endOffset < c.rangeOffset) { + // No change. This cell is entirely before the change + } else if (h.startOffset > endChangedOffset) { + // This cell is after the text that got replaced. Adjust its start/end lines + h.line += lineDiff; + h.endLine += lineDiff; + h.startOffset += offsetDiff; + h.endOffset += offsetDiff; + } else if (h.startOffset === endChangedOffset) { + // Cell intersects but exactly, might be a replacement or an insertion + if (h.deleted || c.rangeLength > 0 || lineDiff === 0) { + // Replacement + h.deleted = docText.substr(h.startOffset, h.endOffset - h.startOffset) !== h.realCode; + } else { + // Insertion + h.line += lineDiff; + h.endLine += lineDiff; + h.startOffset += offsetDiff; + h.endOffset += offsetDiff; + } + } else { + // Intersection, delete if necessary + h.deleted = docText.substr(h.startOffset, h.endOffset - h.startOffset) !== h.realCode; + } + }); + } + + private adjustRuntimeForDebugging( + cell: ICell, + source: string[], + _cellStartOffset: number, + _cellEndOffset: number + ): number { + if ( + this.debugService.activeDebugSession && + this.configService.getSettings(getCellResource(cell)).datascience.stopOnFirstLineWhileDebugging + ) { + // Inject the breakpoint line + source.splice(0, 0, 'breakpoint()\n'); + cell.data.source = source; + cell.extraLines = [0]; + + // Start on the second line + return 2; + } + // No breakpoint necessary, start on the first line + return 1; + } + + // This function will modify a traceback from an error message. + // Tracebacks take a form like so: + // "---------------------------------------------------------------------------" + // "ZeroDivisionError Traceback (most recent call last)" + // "d:\Training\SnakePython\foo.py in \n 1 print('some more')\n ----> 2 cause_error()\n " + // "d:\Training\SnakePython\foo.py in cause_error()\n 3 print('error')\n  4 print('now')\n ----> 5 print( 1 / 0)\n " + // "ZeroDivisionError: division by zero" + // Each item in the array being a stack frame. + private modifyTraceback(msg: KernelMessage.IErrorMsg): string[] { + // Do one frame at a time. + return msg.content.traceback ? msg.content.traceback.map(this.modifyTracebackFrame.bind(this)) : []; + } + + private findCellOffset(hashes: IRangedCellHash[] | undefined, codeLines: string): number | undefined { + if (hashes) { + // Go through all cell code looking for these code lines exactly + // (although with right side trimmed as that's what a stack trace does) + for (const hash of hashes) { + const index = hash.trimmedRightCode.indexOf(codeLines); + if (index >= 0) { + // Jupyter isn't counting blank lines at the top so use our + // first non blank line + return hash.firstNonBlankLineIndex; + } + } + } + // No hash found + return undefined; + } + + private modifyTracebackFrame(traceFrame: string): string { + // See if this item matches any of our cell files + const regexes = [...this.traceBackRegexes.entries()]; + const match = regexes.find((e) => e[1].test(traceFrame)); + if (match) { + // We have a match, pull out the source lines + let sourceLines = ''; + const regex = /(;32m[ ->]*?)(\d+)(.*)/g; + for (let l = regex.exec(traceFrame); l && l.length > 3; l = regex.exec(traceFrame)) { + const newLine = stripAnsi(l[3]).substr(1); // Seem to have a space on the front + sourceLines = `${sourceLines}${newLine}\n`; + } + + // Now attempt to find a cell that matches these source lines + const offset = this.findCellOffset(this.hashes.get(match[0]), sourceLines); + if (offset !== undefined) { + return traceFrame.replace(LineNumberMatchRegex, (_s, prefix, num) => { + const n = parseInt(num, 10); + const newLine = offset + n - 1; + return `${prefix}${newLine + 1}`; + }); + } + } + return traceFrame; + } +} + +export function getCellHashProvider(notebook: INotebook): ICellHashProvider | undefined { + const logger = notebook.getLoggers().find((f) => f instanceof CellHashProvider); + if (logger) { + // tslint:disable-next-line: no-any + return (logger as any) as ICellHashProvider; + } +} diff --git a/src/client/datascience/editor-integration/codeLensFactory.ts b/src/client/datascience/editor-integration/codeLensFactory.ts new file mode 100644 index 000000000000..a3ced5f49440 --- /dev/null +++ b/src/client/datascience/editor-integration/codeLensFactory.ts @@ -0,0 +1,497 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { CodeLens, Command, Event, EventEmitter, Range, TextDocument, Uri } from 'vscode'; + +import { IDocumentManager } from '../../common/application/types'; +import { traceWarning } from '../../common/logger'; + +import { IConfigurationService, Resource } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { generateCellRangesFromDocument } from '../cellFactory'; +import { CodeLensCommands, Commands, Identifiers } from '../constants'; +import { InteractiveWindowMessages, SysInfoReason } from '../interactive-common/interactiveWindowTypes'; +import { + ICell, + ICellHashProvider, + ICellRange, + ICodeLensFactory, + IDataScienceFileSystem, + IFileHashes, + IInteractiveWindowListener, + INotebook, + INotebookProvider +} from '../types'; +import { getCellHashProvider } from './cellhashprovider'; + +type CodeLensCacheData = { + cachedDocumentVersion: number | undefined; + cachedExecutionCounts: Set; + documentLenses: CodeLens[]; + cellRanges: ICellRange[]; + gotoCellLens: CodeLens[]; +}; + +type PerNotebookData = { + cellExecutionCounts: Map; + documentExecutionCounts: Map; + hashProvider: ICellHashProvider | undefined; +}; + +/** + * This class is a singleton that generates code lenses for any document the user opens. It listens + * to cells being execute so it can add 'goto' lenses on cells that have already been run. + */ +@injectable() +export class CodeLensFactory implements ICodeLensFactory, IInteractiveWindowListener { + private updateEvent: EventEmitter = new EventEmitter(); + // tslint:disable-next-line: no-any + private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ + message: string; + // tslint:disable-next-line: no-any + payload: any; + }>(); + private notebookData = new Map(); + private codeLensCache = new Map(); + constructor( + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(INotebookProvider) private notebookProvider: INotebookProvider, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(IDocumentManager) private documentManager: IDocumentManager + ) { + this.documentManager.onDidCloseTextDocument(this.onClosedDocument.bind(this)); + this.configService.getSettings(undefined).onDidChange(this.onChangedSettings.bind(this)); + this.notebookProvider.onNotebookCreated(this.onNotebookCreated.bind(this)); + } + + public dispose(): void { + noop(); + } + + // tslint:disable-next-line: no-any + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + + // tslint:disable-next-line: no-any + public onMessage(message: string, payload?: any) { + switch (message) { + case InteractiveWindowMessages.NotebookIdentity: + if (payload.type === 'interactive') { + this.trackNotebook(payload.resource); + } + break; + + case InteractiveWindowMessages.NotebookClose: + if (payload.type === 'interactive') { + this.untrackNotebook(payload.resource); + } + break; + + case InteractiveWindowMessages.AddedSysInfo: + if (payload && payload.type) { + const reason = payload.type as SysInfoReason; + if (reason !== SysInfoReason.Interrupt) { + this.clearExecutionCounts(payload.notebookIdentity); + } + } + break; + + case InteractiveWindowMessages.FinishCell: + const cell = payload.cell as ICell; + if (cell && cell.data && cell.data.execution_count) { + if (cell.file && cell.file !== Identifiers.EmptyFileName) { + this.updateExecutionCounts(payload.notebookIdentity, cell); + } + } + break; + + default: + break; + } + } + + public get updateRequired(): Event { + return this.updateEvent.event; + } + + public createCodeLenses(document: TextDocument): CodeLens[] { + const cache = this.getCodeLensCacheData(document); + return [...cache.documentLenses, ...cache.gotoCellLens]; + } + + public getCellRanges(document: TextDocument): ICellRange[] { + const cache = this.getCodeLensCacheData(document); + return cache.cellRanges; + } + + private getCodeLensCacheData(document: TextDocument): CodeLensCacheData { + // See if we have a cached version of the code lenses for this document + const key = document.fileName.toLocaleLowerCase(); + let cache = this.codeLensCache.get(key); + let needUpdate = false; + + // If we don't have one, generate one + if (!cache) { + cache = { + cachedDocumentVersion: undefined, + cachedExecutionCounts: new Set(), + documentLenses: [], + cellRanges: [], + gotoCellLens: [] + }; + needUpdate = true; + this.codeLensCache.set(key, cache); + } + + // If the document version doesn't match, our cell ranges are out of date + if (cache.cachedDocumentVersion !== document.version) { + cache.cellRanges = generateCellRangesFromDocument( + document, + this.configService.getSettings(document.uri).datascience + ); + + // Because we have all new ranges, we need to recompute ALL of our code lenses. + cache.documentLenses = []; + cache.gotoCellLens = []; + cache.cachedDocumentVersion = document.version; + needUpdate = true; + } + + // If the document execution count doesn't match, then our goto cell lenses are out of date + const documentCounts = this.getDocumentExecutionCounts(key); + if ( + documentCounts.length !== cache.cachedExecutionCounts.size || + documentCounts.find((n) => !cache?.cachedExecutionCounts.has(n)) + ) { + cache.gotoCellLens = []; + cache.cachedExecutionCounts = new Set(documentCounts); + needUpdate = true; + } + + // Generate our code lenses if necessary + if (cache.documentLenses.length === 0 && needUpdate && cache.cellRanges.length) { + // Enumerate the possible commands for the document based code lenses + const commands = needUpdate ? this.enumerateCommands(document.uri) : []; + + // Then iterate over all of the cell ranges and generate code lenses for each possible + // commands + let firstCell = true; + cache.cellRanges.forEach((r) => { + commands.forEach((c) => { + const codeLens = this.createCodeLens(document, r, c, firstCell); + if (codeLens) { + cache?.documentLenses.push(codeLens); // NOSONAR + } + }); + firstCell = false; + }); + } + + // Generate the goto cell lenses if necessary + if ( + needUpdate && + cache.gotoCellLens.length === 0 && + cache.cellRanges.length && + this.configService.getSettings(document.uri).datascience.addGotoCodeLenses + ) { + const hashes = this.getHashes(); + if (hashes && hashes.length) { + cache.cellRanges.forEach((r) => { + const codeLens = this.createExecutionLens(document, r.range, hashes); + if (codeLens) { + cache?.gotoCellLens.push(codeLens); // NOSONAR + } + }); + } + } + return cache; + } + + private trackNotebook(identity: Uri) { + // Setup our per notebook data if not already tracked. + if (!this.notebookData.has(identity.toString())) { + this.notebookData.set(identity.toString(), this.createNotebookData()); + } + } + + private createNotebookData(): PerNotebookData { + return { + cellExecutionCounts: new Map(), + documentExecutionCounts: new Map(), + hashProvider: undefined + }; + } + + private untrackNotebook(identity: Uri) { + this.notebookData.delete(identity.toString()); + this.updateEvent.fire(); + } + + private clearExecutionCounts(identity: Uri) { + const data = this.notebookData.get(identity.toString()); + if (data) { + data.cellExecutionCounts.clear(); + data.documentExecutionCounts.clear(); + this.updateEvent.fire(); + } + } + + private getDocumentExecutionCounts(key: string): number[] { + return [...this.notebookData.values()] + .map((d) => d.documentExecutionCounts.get(key)) + .filter((n) => n !== undefined) as number[]; + } + + private updateExecutionCounts(identity: Uri, cell: ICell) { + let data = this.notebookData.get(identity.toString()); + if (!data) { + data = this.createNotebookData(); + } + if (data && cell.data.execution_count) { + data.cellExecutionCounts.set(cell.id, cell.data.execution_count?.toString()); + data.documentExecutionCounts.set( + cell.file.toLowerCase(), + parseInt(cell.data.execution_count.toString(), 10) + ); + this.updateEvent.fire(); + } + } + + private onNotebookCreated(args: { identity: Uri; notebook: INotebook }) { + const key = args.identity.toString(); + let data = this.notebookData.get(key); + if (!data) { + data = this.createNotebookData(); + this.notebookData.set(key, data); + } + if (data) { + data.hashProvider = getCellHashProvider(args.notebook); + } + args.notebook.onDisposed(() => { + this.notebookData.delete(key); + }); + } + + private getHashProviders(): ICellHashProvider[] { + return [...this.notebookData.values()].filter((v) => v.hashProvider).map((v) => v.hashProvider!); + } + + private getHashes(): IFileHashes[] { + // Get all of the hash providers and get all of their hashes + const providers = this.getHashProviders(); + + // Combine them together into one big array + return providers && providers.length ? providers.map((p) => p!.getHashes()).reduce((p, c) => [...p, ...c]) : []; + } + + private onClosedDocument(doc: TextDocument) { + this.codeLensCache.delete(doc.fileName.toLocaleLowerCase()); + + // Don't delete the document execution count, we need to keep track + // of it past the closing of a doc if the notebook or interactive window is still open. + } + + private onChangedSettings() { + // When config settings change, refresh our code lenses. + this.codeLensCache.clear(); + + // Force an update so that code lenses are recomputed now and not during execution. + this.updateEvent.fire(); + } + + private enumerateCommands(resource: Resource): string[] { + let fullCommandList: string[]; + // Add our non-debug commands + const commands = this.configService.getSettings(resource).datascience.codeLenses; + if (commands) { + fullCommandList = commands.split(',').map((s) => s.trim()); + } else { + fullCommandList = CodeLensCommands.DefaultDesignLenses; + } + + // Add our debug commands + const debugCommands = this.configService.getSettings(resource).datascience.debugCodeLenses; + if (debugCommands) { + fullCommandList = fullCommandList.concat(debugCommands.split(',').map((s) => s.trim())); + } else { + fullCommandList = fullCommandList.concat(CodeLensCommands.DefaultDebuggingLenses); + } + + return fullCommandList; + } + + // tslint:disable-next-line: max-func-body-length + private createCodeLens( + document: TextDocument, + cellRange: { range: Range; cell_type: string }, + commandName: string, + isFirst: boolean + ): CodeLens | undefined { + // We only support specific commands + // Be careful here. These arguments will be serialized during liveshare sessions + // and so shouldn't reference local objects. + const { range, cell_type } = cellRange; + switch (commandName) { + case Commands.RunCurrentCellAndAddBelow: + return this.generateCodeLens( + range, + Commands.RunCurrentCellAndAddBelow, + localize.DataScience.runCurrentCellAndAddBelow() + ); + case Commands.AddCellBelow: + return this.generateCodeLens( + range, + Commands.AddCellBelow, + localize.DataScience.addCellBelowCommandTitle(), + [document.uri, range.start.line] + ); + case Commands.DebugCurrentCellPalette: + return this.generateCodeLens( + range, + Commands.DebugCurrentCellPalette, + localize.DataScience.debugCellCommandTitle() + ); + + case Commands.DebugCell: + // If it's not a code cell (e.g. markdown), don't add the "Debug cell" action. + if (cell_type !== 'code') { + break; + } + return this.generateCodeLens(range, Commands.DebugCell, localize.DataScience.debugCellCommandTitle(), [ + document.uri, + range.start.line, + range.start.character, + range.end.line, + range.end.character + ]); + + case Commands.DebugStepOver: + // Only code cells get debug actions + if (cell_type !== 'code') { + break; + } + return this.generateCodeLens( + range, + Commands.DebugStepOver, + localize.DataScience.debugStepOverCommandTitle() + ); + + case Commands.DebugContinue: + // Only code cells get debug actions + if (cell_type !== 'code') { + break; + } + return this.generateCodeLens( + range, + Commands.DebugContinue, + localize.DataScience.debugContinueCommandTitle() + ); + + case Commands.DebugStop: + // Only code cells get debug actions + if (cell_type !== 'code') { + break; + } + return this.generateCodeLens(range, Commands.DebugStop, localize.DataScience.debugStopCommandTitle()); + + case Commands.RunCurrentCell: + case Commands.RunCell: + return this.generateCodeLens(range, Commands.RunCell, localize.DataScience.runCellLensCommandTitle(), [ + document.uri, + range.start.line, + range.start.character, + range.end.line, + range.end.character + ]); + + case Commands.RunAllCells: + return this.generateCodeLens( + range, + Commands.RunAllCells, + localize.DataScience.runAllCellsLensCommandTitle(), + [document.uri, range.start.line, range.start.character] + ); + + case Commands.RunAllCellsAbovePalette: + case Commands.RunAllCellsAbove: + if (!isFirst) { + return this.generateCodeLens( + range, + Commands.RunAllCellsAbove, + localize.DataScience.runAllCellsAboveLensCommandTitle(), + [document.uri, range.start.line, range.start.character] + ); + } else { + return this.generateCodeLens( + range, + Commands.RunCellAndAllBelow, + localize.DataScience.runCellAndAllBelowLensCommandTitle(), + [document.uri, range.start.line, range.start.character] + ); + } + break; + case Commands.RunCellAndAllBelowPalette: + case Commands.RunCellAndAllBelow: + return this.generateCodeLens( + range, + Commands.RunCellAndAllBelow, + localize.DataScience.runCellAndAllBelowLensCommandTitle(), + [document.uri, range.start.line, range.start.character] + ); + + default: + traceWarning(`Invalid command for code lens ${commandName}`); + break; + } + + return undefined; + } + + private findMatchingCellExecutionCount(cellId: string) { + // Cell ids on interactive window are generated on the fly so there shouldn't be dupes + const data = [...this.notebookData.values()].find((d) => d.cellExecutionCounts.get(cellId)); + return data?.cellExecutionCounts.get(cellId); + } + + private createExecutionLens(document: TextDocument, range: Range, hashes: IFileHashes[]) { + const list = hashes + .filter((h) => this.fs.areLocalPathsSame(h.file, document.fileName)) + .map((f) => f.hashes) + .flat(); + if (list) { + // Match just the start of the range. Should be - 2 (1 for 1 based numbers and 1 for skipping the comment at the top) + const rangeMatches = list + .filter((h) => h.line - 2 === range.start.line) + .sort((a, b) => a.timestamp - b.timestamp); + if (rangeMatches && rangeMatches.length) { + const rangeMatch = rangeMatches[rangeMatches.length - 1]; + const matchingExecutionCount = this.findMatchingCellExecutionCount(rangeMatch.id); + if (matchingExecutionCount !== undefined) { + return this.generateCodeLens( + range, + Commands.ScrollToCell, + localize.DataScience.scrollToCellTitleFormatMessage().format(matchingExecutionCount), + [document.uri, rangeMatch.id] + ); + } + } + } + } + + // tslint:disable-next-line: no-any + private generateCodeLens(range: Range, commandName: string, title: string, args?: any[]): CodeLens { + return new CodeLens(range, this.generateCommand(commandName, title, args)); + } + + // tslint:disable-next-line: no-any + private generateCommand(commandName: string, title: string, args?: any[]): Command { + return { + arguments: args, + title, + command: commandName + }; + } +} diff --git a/src/client/datascience/editor-integration/codelensprovider.ts b/src/client/datascience/editor-integration/codelensprovider.ts new file mode 100644 index 000000000000..fcf233ca91e6 --- /dev/null +++ b/src/client/datascience/editor-integration/codelensprovider.ts @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import * as vscode from 'vscode'; + +import { ICommandManager, IDebugService, IDocumentManager, IVSCodeNotebook } from '../../common/application/types'; +import { ContextKey } from '../../common/contextKey'; + +import { IConfigurationService, IDataScienceSettings, IDisposable, IDisposableRegistry } from '../../common/types'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { IServiceContainer } from '../../ioc/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { CodeLensCommands, EditorContexts, Telemetry } from '../constants'; +import { ICodeWatcher, IDataScienceCodeLensProvider, IDataScienceFileSystem, IDebugLocationTracker } from '../types'; + +@injectable() +export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider, IDisposable { + private totalExecutionTimeInMs: number = 0; + private totalGetCodeLensCalls: number = 0; + private activeCodeWatchers: ICodeWatcher[] = []; + private didChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IDebugLocationTracker) private debugLocationTracker: IDebugLocationTracker, + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IConfigurationService) private configuration: IConfigurationService, + @inject(ICommandManager) private commandManager: ICommandManager, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IDebugService) private debugService: IDebugService, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(IVSCodeNotebook) private readonly vsCodeNotebook: IVSCodeNotebook + ) { + disposableRegistry.push(this); + disposableRegistry.push(this.debugService.onDidChangeActiveDebugSession(this.onChangeDebugSession.bind(this))); + disposableRegistry.push(this.documentManager.onDidCloseTextDocument(this.onDidCloseTextDocument.bind(this))); + disposableRegistry.push(this.debugLocationTracker.updated(this.onDebugLocationUpdated.bind(this))); + } + + public dispose() { + // On shutdown send how long on average we spent parsing code lens + if (this.totalGetCodeLensCalls > 0) { + sendTelemetryEvent( + Telemetry.CodeLensAverageAcquisitionTime, + this.totalExecutionTimeInMs / this.totalGetCodeLensCalls + ); + } + } + + public get onDidChangeCodeLenses(): vscode.Event { + return this.didChangeCodeLenses.event; + } + + // CodeLensProvider interface + // Some implementation based on DonJayamanne's jupyter extension work + public provideCodeLenses(document: vscode.TextDocument, _token: vscode.CancellationToken): vscode.CodeLens[] { + if (this.vsCodeNotebook.activeNotebookEditor) { + return []; + } + // Get the list of code lens for this document. + return this.getCodeLensTimed(document); + } + + // IDataScienceCodeLensProvider interface + public getCodeWatcher(document: vscode.TextDocument): ICodeWatcher | undefined { + return this.matchWatcher( + document.fileName, + document.version, + this.configuration.getSettings(document.uri).datascience + ); + } + + private onDebugLocationUpdated() { + this.didChangeCodeLenses.fire(); + } + + private onChangeDebugSession(_e: vscode.DebugSession | undefined) { + this.didChangeCodeLenses.fire(); + } + + private onDidCloseTextDocument(e: vscode.TextDocument) { + const index = this.activeCodeWatchers.findIndex( + (item) => item.uri && this.fs.areLocalPathsSame(item.uri.fsPath, e.fileName) + ); + if (index >= 0) { + this.activeCodeWatchers.splice(index, 1); + } + } + + private getCodeLensTimed(document: vscode.TextDocument): vscode.CodeLens[] { + const stopWatch = new StopWatch(); + const result = this.getCodeLens(document); + this.totalExecutionTimeInMs += stopWatch.elapsedTime; + this.totalGetCodeLensCalls += 1; + + // Update the hasCodeCells context at the same time we are asked for codelens as VS code will + // ask whenever a change occurs. Do this regardless of if we have code lens turned on or not as + // shift+enter relies on this code context. + const editorContext = new ContextKey(EditorContexts.HasCodeCells, this.commandManager); + editorContext.set(result && result.length > 0).catch(); + + // Don't provide any code lenses if we have not enabled data science + const settings = this.configuration.getSettings(document.uri); + if (!settings.datascience.enabled || !settings.datascience.enableCellCodeLens) { + // Clear out any existing code watchers, providecodelenses is called on settings change + // so we don't need to watch the settings change specifically here + if (this.activeCodeWatchers.length > 0) { + this.activeCodeWatchers = []; + } + return []; + } + + return this.adjustDebuggingLenses(document, result); + } + + // Adjust what code lenses are visible or not given debug mode and debug context location + private adjustDebuggingLenses(document: vscode.TextDocument, lenses: vscode.CodeLens[]): vscode.CodeLens[] { + const debugCellList = CodeLensCommands.DebuggerCommands; + + if (this.debugService.activeDebugSession) { + const debugLocation = this.debugLocationTracker.getLocation(this.debugService.activeDebugSession); + + if (debugLocation && this.fs.areLocalPathsSame(debugLocation.fileName, document.uri.fsPath)) { + // We are in the given debug file, so only return the code lens that contains the given line + const activeLenses = lenses.filter((lens) => { + // -1 for difference between file system one based and debugger zero based + const pos = new vscode.Position(debugLocation.lineNumber - 1, debugLocation.column - 1); + return lens.range.contains(pos); + }); + + return activeLenses.filter((lens) => { + if (lens.command) { + return debugCellList.includes(lens.command.command); + } + return false; + }); + } + } else { + return lenses.filter((lens) => { + if (lens.command) { + return !debugCellList.includes(lens.command.command); + } + return false; + }); + } + + // Fall through case to return nothing + return []; + } + + private getCodeLens(document: vscode.TextDocument): vscode.CodeLens[] { + // See if we already have a watcher for this file and version + const codeWatcher: ICodeWatcher | undefined = this.matchWatcher( + document.fileName, + document.version, + this.configuration.getSettings(document.uri).datascience + ); + if (codeWatcher) { + return codeWatcher.getCodeLenses(); + } + + // Create a new watcher for this file + const newCodeWatcher = this.createNewCodeWatcher(document); + return newCodeWatcher.getCodeLenses(); + } + + private matchWatcher(fileName: string, version: number, settings: IDataScienceSettings): ICodeWatcher | undefined { + const index = this.activeCodeWatchers.findIndex( + (item) => item.uri && this.fs.areLocalPathsSame(item.uri.fsPath, fileName) + ); + if (index >= 0) { + const item = this.activeCodeWatchers[index]; + if (item.getVersion() === version) { + // Also make sure the cached settings are the same. Otherwise these code lenses + // were created with old settings + const settingsStr = JSON.stringify(settings); + const itemSettings = JSON.stringify(item.getCachedSettings()); + if (settingsStr === itemSettings) { + return item; + } + } + // If we have an old version remove it from the active list + this.activeCodeWatchers.splice(index, 1); + } + + // Create a new watcher for this file if we can find a matching document + const possibleDocuments = this.documentManager.textDocuments.filter((d) => d.fileName === fileName); + if (possibleDocuments && possibleDocuments.length > 0) { + return this.createNewCodeWatcher(possibleDocuments[0]); + } + + return undefined; + } + + private createNewCodeWatcher(document: vscode.TextDocument): ICodeWatcher { + const newCodeWatcher = this.serviceContainer.get(ICodeWatcher); + newCodeWatcher.setDocument(document); + newCodeWatcher.codeLensUpdated(this.onWatcherUpdated.bind(this)); + this.activeCodeWatchers.push(newCodeWatcher); + return newCodeWatcher; + } + + private onWatcherUpdated(): void { + this.didChangeCodeLenses.fire(); + } +} diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts new file mode 100644 index 000000000000..ce00d51c830b --- /dev/null +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -0,0 +1,1128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { nbformat } from '@jupyterlab/coreutils'; +import { inject, injectable } from 'inversify'; +import { + CodeLens, + commands, + Event, + EventEmitter, + Position, + Range, + Selection, + TextDocument, + TextEditor, + TextEditorRevealType, + Uri +} from 'vscode'; + +import { IDocumentManager } from '../../common/application/types'; + +import { IConfigurationService, IDataScienceSettings, IDisposable, Resource } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { ICodeExecutionHelper } from '../../terminals/types'; +import { CellMatcher } from '../cellMatcher'; +import { Commands, Identifiers, Telemetry } from '../constants'; +import { + ICellRange, + ICodeLensFactory, + ICodeWatcher, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IInteractiveWindowProvider +} from '../types'; + +function getIndex(index: number, length: number): number { + // return index within the length range with negative indexing + if (length <= 0) { + throw new RangeError(`Length must be > 0 not ${length}`); + } + // negative index count back from length + if (index < 0) { + index += length; + } + // bounded index + if (index < 0) { + return 0; + } else if (index >= length) { + return length - 1; + } else { + return index; + } +} + +@injectable() +export class CodeWatcher implements ICodeWatcher { + private static sentExecuteCellTelemetry: boolean = false; + private document?: TextDocument; + private version: number = -1; + private codeLenses: CodeLens[] = []; + private cells: ICellRange[] = []; + private cachedSettings: IDataScienceSettings | undefined; + private codeLensUpdatedEvent: EventEmitter = new EventEmitter(); + private updateRequiredDisposable: IDisposable | undefined; + private closeDocumentDisposable: IDisposable | undefined; + + constructor( + @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(ICodeExecutionHelper) private executionHelper: ICodeExecutionHelper, + @inject(IDataScienceErrorHandler) protected dataScienceErrorHandler: IDataScienceErrorHandler, + @inject(ICodeLensFactory) private codeLensFactory: ICodeLensFactory + ) {} + + public setDocument(document: TextDocument) { + this.document = document; + + // Cache the version, we don't want to pull an old version if the document is updated + this.version = document.version; + + // Get document cells here. Make a copy of our settings. + this.cachedSettings = JSON.parse(JSON.stringify(this.configService.getSettings(document.uri).datascience)); + + // Use the factory to generate our new code lenses. + this.codeLenses = this.codeLensFactory.createCodeLenses(document); + this.cells = this.codeLensFactory.getCellRanges(document); + + // Listen for changes + this.updateRequiredDisposable = this.codeLensFactory.updateRequired(this.onCodeLensFactoryUpdated.bind(this)); + + // Make sure to stop listening for changes when this document closes. + this.closeDocumentDisposable = this.documentManager.onDidCloseTextDocument(this.onDocumentClosed.bind(this)); + } + + public get codeLensUpdated(): Event { + return this.codeLensUpdatedEvent.event; + } + + public get uri() { + return this.document?.uri; + } + + public getVersion() { + return this.version; + } + + public getCachedSettings(): IDataScienceSettings | undefined { + return this.cachedSettings; + } + + public getCodeLenses() { + return this.codeLenses; + } + + @captureTelemetry(Telemetry.DebugCurrentCell) + public async debugCurrentCell() { + if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { + return; + } + + // Run the cell that matches the current cursor position. + return this.runMatchingCell(this.documentManager.activeTextEditor.selection, false, true); + } + + @captureTelemetry(Telemetry.RunAllCells) + public async runAllCells() { + const runCellCommands = this.codeLenses.filter( + (c) => + c.command && + c.command.command === Commands.RunCell && + c.command.arguments && + c.command.arguments.length >= 5 + ); + let leftCount = runCellCommands.length; + + // Run all of our code lenses, they should always be ordered in the file so we can just + // run them one by one + for (const lens of runCellCommands) { + // Make sure that we have the correct command (RunCell) lenses + let range: Range = new Range( + lens.command!.arguments![1], + lens.command!.arguments![2], + lens.command!.arguments![3], + lens.command!.arguments![4] + ); + if (this.document) { + // Special case, if this is the first, expand our range to always include the top. + if (leftCount === runCellCommands.length) { + range = new Range(new Position(0, 0), range.end); + } + + const code = this.document.getText(range); + leftCount -= 1; + + // Note: We do a get or create active before all addCode commands to make sure that we either have a history up already + // or if we do not we need to start it up as these commands are all expected to start a new history if needed + const success = await this.addCode(code, this.document.uri, range.start.line); + if (!success) { + await this.addErrorMessage(this.document.uri, leftCount); + break; + } + } + } + + // If there are no codelenses, just run all of the code as a single cell + if (runCellCommands.length === 0) { + return this.runFileInteractiveInternal(false); + } + } + + @captureTelemetry(Telemetry.RunFileInteractive) + public async runFileInteractive() { + return this.runFileInteractiveInternal(false); + } + + @captureTelemetry(Telemetry.DebugFileInteractive) + public async debugFileInteractive() { + return this.runFileInteractiveInternal(true); + } + + // Run all cells up to the cell containing this start line and character + @captureTelemetry(Telemetry.RunAllCellsAbove) + public async runAllCellsAbove(stopLine: number, stopCharacter: number) { + const runCellCommands = this.codeLenses.filter((c) => c.command && c.command.command === Commands.RunCell); + let leftCount = runCellCommands.findIndex( + (c) => c.range.start.line >= stopLine && c.range.start.character >= stopCharacter + ); + if (leftCount < 0) { + leftCount = runCellCommands.length; + } + const startCount = leftCount; + + // Run our code lenses up to this point, lenses are created in order on document load + // so we can rely on them being in linear order for this + for (const lens of runCellCommands) { + // Make sure we are dealing with run cell based code lenses in case more types are added later + if (leftCount > 0 && this.document) { + let range: Range = new Range(lens.range.start, lens.range.end); + + // If this is the first, make sure it extends to the top + if (leftCount === startCount) { + range = new Range(new Position(0, 0), range.end); + } + + // We have a cell and we are not past or at the stop point + leftCount -= 1; + const code = this.document.getText(range); + const success = await this.addCode(code, this.document.uri, lens.range.start.line); + if (!success) { + await this.addErrorMessage(this.document.uri, leftCount); + break; + } + } else { + // If we get a cell past or at the stop point stop + break; + } + } + } + + @captureTelemetry(Telemetry.RunCellAndAllBelow) + public async runCellAndAllBelow(startLine: number, startCharacter: number) { + const runCellCommands = this.codeLenses.filter((c) => c.command && c.command.command === Commands.RunCell); + const index = runCellCommands.findIndex( + (c) => c.range.start.line >= startLine && c.range.start.character >= startCharacter + ); + let leftCount = index > 0 ? runCellCommands.length - index : runCellCommands.length; + + // Run our code lenses from this point to the end, lenses are created in order on document load + // so we can rely on them being in linear order for this + for (let pos = index; pos >= 0 && pos < runCellCommands.length; pos += 1) { + if (leftCount > 0 && this.document) { + const lens = runCellCommands[pos]; + // We have a cell and we are not past or at the stop point + leftCount -= 1; + const code = this.document.getText(lens.range); + const success = await this.addCode(code, this.document.uri, lens.range.start.line); + if (!success) { + await this.addErrorMessage(this.document.uri, leftCount); + break; + } + } + } + } + + @captureTelemetry(Telemetry.RunSelectionOrLine) + public async runSelectionOrLine(activeEditor: TextEditor | undefined) { + if (this.document && activeEditor && this.fs.arePathsSame(activeEditor.document.uri, this.document.uri)) { + // Get just the text of the selection or the current line if none + const codeToExecute = await this.executionHelper.getSelectedTextToExecute(activeEditor); + if (!codeToExecute) { + return; + } + const normalizedCode = await this.executionHelper.normalizeLines(codeToExecute!); + if (!normalizedCode || normalizedCode.trim().length === 0) { + return; + } + await this.addCode(normalizedCode, this.document.uri, activeEditor.selection.start.line, activeEditor); + } + } + + @captureTelemetry(Telemetry.RunToLine) + public async runToLine(targetLine: number) { + if (this.document && targetLine > 0) { + const previousLine = this.document.lineAt(targetLine - 1); + const code = this.document.getText( + new Range(0, 0, previousLine.range.end.line, previousLine.range.end.character) + ); + + if (code && code.trim().length) { + await this.addCode(code, this.document.uri, 0); + } + } + } + + @captureTelemetry(Telemetry.RunFromLine) + public async runFromLine(targetLine: number) { + if (this.document && targetLine < this.document.lineCount) { + const lastLine = this.document.lineAt(this.document.lineCount - 1); + const code = this.document.getText( + new Range(targetLine, 0, lastLine.range.end.line, lastLine.range.end.character) + ); + + if (code && code.trim().length) { + await this.addCode(code, this.document.uri, targetLine); + } + } + } + + @captureTelemetry(Telemetry.RunCell) + public async runCell(range: Range): Promise { + if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { + return; + } + + // Run the cell clicked. Advance if the cursor is inside this cell and we're allowed to + const advance = + range.contains(this.documentManager.activeTextEditor.selection.start) && + this.configService.getSettings(this.documentManager.activeTextEditor.document.uri).datascience + .enableAutoMoveToNextCell; + return this.runMatchingCell(range, advance); + } + + @captureTelemetry(Telemetry.DebugCurrentCell) + public async debugCell(range: Range): Promise { + if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { + return; + } + + // Debug the cell clicked. + return this.runMatchingCell(range, false, true); + } + + @captureTelemetry(Telemetry.RunCurrentCell) + public async runCurrentCell(): Promise { + if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { + return; + } + + // Run the cell that matches the current cursor position. + return this.runMatchingCell(this.documentManager.activeTextEditor.selection, false); + } + + @captureTelemetry(Telemetry.RunCurrentCellAndAdvance) + public async runCurrentCellAndAdvance() { + if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { + return; + } + + // Run the cell that matches the current cursor position. Always advance + return this.runMatchingCell(this.documentManager.activeTextEditor.selection, true); + } + + // telemetry captured on CommandRegistry + public async addEmptyCellToBottom(): Promise { + const editor = this.documentManager.activeTextEditor; + if (editor) { + this.insertCell(editor, editor.document.lineCount + 1); + } + } + + @captureTelemetry(Telemetry.RunCurrentCellAndAddBelow) + public async runCurrentCellAndAddBelow(): Promise { + if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { + return; + } + + const editor = this.documentManager.activeTextEditor; + const cellMatcher = new CellMatcher(); + let index = 0; + const cellDelineator = this.getDefaultCellMarker(editor.document.uri); + + if (editor) { + editor.edit((editBuilder) => { + let lastCell = true; + + for (let i = editor.selection.end.line + 1; i < editor.document.lineCount; i += 1) { + if (cellMatcher.isCell(editor.document.lineAt(i).text)) { + lastCell = false; + index = i; + editBuilder.insert(new Position(i, 0), `${cellDelineator}\n\n`); + break; + } + } + + if (lastCell) { + index = editor.document.lineCount; + editBuilder.insert(new Position(editor.document.lineCount, 0), `\n${cellDelineator}\n`); + } + }); + } + + // Run the cell that matches the current cursor position, and then advance to the new cell + const newPosition = new Position(index + 1, 0); + return this.runMatchingCell(editor.selection, false).then(() => + this.advanceToRange(new Range(newPosition, newPosition)) + ); + } + + @captureTelemetry(Telemetry.InsertCellBelowPosition) + public insertCellBelowPosition() { + const editor = this.documentManager.activeTextEditor; + if (editor && editor.selection) { + this.insertCell(editor, editor.selection.end.line + 1); + } + } + + @captureTelemetry(Telemetry.InsertCellBelow) + public insertCellBelow() { + const editor = this.documentManager.activeTextEditor; + if (editor && editor.selection) { + const cell = this.getCellFromPosition(editor.selection.end); + if (cell) { + this.insertCell(editor, cell.range.end.line + 1); + } else { + this.insertCell(editor, editor.selection.end.line + 1); + } + } + } + + @captureTelemetry(Telemetry.InsertCellAbove) + public insertCellAbove() { + const editor = this.documentManager.activeTextEditor; + if (editor && editor.selection) { + const cell = this.getCellFromPosition(editor.selection.start); + if (cell) { + this.insertCell(editor, cell.range.start.line); + } else { + this.insertCell(editor, editor.selection.start.line); + } + } + } + + @captureTelemetry(Telemetry.DeleteCells) + public deleteCells() { + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return; + } + + const firstLastCells = this.getStartEndCells(editor.selection); + if (!firstLastCells) { + return; + } + const startCell = firstLastCells[0]; + const endCell = firstLastCells[1]; + + // Start of the document should start at position 0, 0 and end one line ahead. + let startLineNumber = 0; + let startCharacterNumber = 0; + let endLineNumber = endCell.range.end.line + 1; + let endCharacterNumber = 0; + // Anywhere else in the document should start at the end of line before the + // cell and end at the last character of the cell. + if (startCell.range.start.line > 0) { + startLineNumber = startCell.range.start.line - 1; + startCharacterNumber = editor.document.lineAt(startLineNumber).range.end.character; + endLineNumber = endCell.range.end.line; + endCharacterNumber = endCell.range.end.character; + } + const cellExtendedRange = new Range( + new Position(startLineNumber, startCharacterNumber), + new Position(endLineNumber, endCharacterNumber) + ); + editor.edit((editBuilder) => { + editBuilder.replace(cellExtendedRange, ''); + this.codeLensUpdatedEvent.fire(); + }); + } + + @captureTelemetry(Telemetry.SelectCell) + public selectCell() { + const editor = this.documentManager.activeTextEditor; + if (editor && editor.selection) { + const startEndCells = this.getStartEndCells(editor.selection); + if (startEndCells) { + const startCell = startEndCells[0]; + const endCell = startEndCells[1]; + if (editor.selection.anchor.isBeforeOrEqual(editor.selection.active)) { + editor.selection = new Selection(startCell.range.start, endCell.range.end); + } else { + editor.selection = new Selection(endCell.range.end, startCell.range.start); + } + } + } + } + + @captureTelemetry(Telemetry.SelectCellContents) + public selectCellContents() { + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return; + } + const startEndCellIndex = this.getStartEndCellIndex(editor.selection); + if (!startEndCellIndex) { + return; + } + const startCellIndex = startEndCellIndex[0]; + const endCellIndex = startEndCellIndex[1]; + const isAnchorLessEqualActive = editor.selection.anchor.isBeforeOrEqual(editor.selection.active); + + const cells = this.cells; + const selections: Selection[] = []; + for (let i = startCellIndex; i <= endCellIndex; i += 1) { + const cell = cells[i]; + let anchorLine = cell.range.start.line + 1; + let achorCharacter = 0; + let activeLine = cell.range.end.line; + let activeCharacter = cell.range.end.character; + // if cell is only one line long, select the end of that line + if (cell.range.start.line === cell.range.end.line) { + anchorLine = cell.range.start.line; + achorCharacter = editor.document.lineAt(anchorLine).range.end.character; + activeLine = anchorLine; + activeCharacter = achorCharacter; + } + if (isAnchorLessEqualActive) { + selections.push(new Selection(anchorLine, achorCharacter, activeLine, activeCharacter)); + } else { + selections.push(new Selection(activeLine, activeCharacter, anchorLine, achorCharacter)); + } + } + editor.selections = selections; + } + + @captureTelemetry(Telemetry.ExtendSelectionByCellAbove) + public extendSelectionByCellAbove() { + // This behaves similarly to excel "Extend Selection by One Cell Above". + // The direction of the selection matters (i.e. where the active cursor) + // position is. First, it ensures that complete cells are selection. + // If so, then if active cursor is in cells below it contracts the + // selection range. If the active cursor is above, it expands the + // selection range. + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return; + } + const currentSelection = editor.selection; + const startEndCellIndex = this.getStartEndCellIndex(editor.selection); + if (!startEndCellIndex) { + return; + } + + const isAnchorLessThanActive = editor.selection.anchor.isBefore(editor.selection.active); + + const cells = this.cells; + const startCellIndex = startEndCellIndex[0]; + const endCellIndex = startEndCellIndex[1]; + const startCell = cells[startCellIndex]; + const endCell = cells[endCellIndex]; + + if ( + !startCell.range.start.isEqual(currentSelection.start) || + !endCell.range.end.isEqual(currentSelection.end) + ) { + // full cell range not selected, first select a full cell range. + let selection: Selection; + if (isAnchorLessThanActive) { + if (startCellIndex < endCellIndex) { + // active at end of cell before endCell + selection = new Selection(startCell.range.start, cells[endCellIndex - 1].range.end); + } else { + // active at end of startCell + selection = new Selection(startCell.range.end, startCell.range.start); + } + } else { + // active at start of start cell. + selection = new Selection(endCell.range.end, startCell.range.start); + } + editor.selection = selection; + } else { + let newCell: ICellRange | undefined; + // full cell range is selected now decide if expanding or contracting? + if (isAnchorLessThanActive && startCellIndex < endCellIndex) { + // anchor is above active, contract selection by cell below. + newCell = cells[endCellIndex - 1]; + editor.selection = new Selection(startCell.range.start, newCell.range.end); + } else { + // anchor is below active, expand selection by cell above. + if (startCellIndex > 0) { + newCell = cells[startCellIndex - 1]; + editor.selection = new Selection(endCell.range.end, newCell.range.start); + } + } + + if (newCell) { + editor.revealRange(newCell.range, TextEditorRevealType.Default); + } + } + } + + @captureTelemetry(Telemetry.ExtendSelectionByCellBelow) + public extendSelectionByCellBelow() { + // This behaves similarly to excel "Extend Selection by One Cell Above". + // The direction of the selection matters (i.e. where the active cursor) + // position is. First, it ensures that complete cells are selection. + // If so, then if active cursor is in cells below it expands the + // selection range. If the active cursor is above, it contracts the + // selection range. + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return; + } + const currentSelection = editor.selection; + const startEndCellIndex = this.getStartEndCellIndex(editor.selection); + if (!startEndCellIndex) { + return; + } + + const isAnchorLessEqualActive = editor.selection.anchor.isBeforeOrEqual(editor.selection.active); + + const cells = this.cells; + const startCellIndex = startEndCellIndex[0]; + const endCellIndex = startEndCellIndex[1]; + const startCell = cells[startCellIndex]; + const endCell = cells[endCellIndex]; + + if ( + !startCell.range.start.isEqual(currentSelection.start) || + !endCell.range.end.isEqual(currentSelection.end) + ) { + // full cell range not selected, first select a full cell range. + let selection: Selection; + if (isAnchorLessEqualActive) { + // active at start of start cell. + selection = new Selection(startCell.range.start, endCell.range.end); + } else { + if (startCellIndex < endCellIndex) { + // active at end of cell before endCell + selection = new Selection(cells[startCellIndex + 1].range.start, endCell.range.end); + } else { + // active at end of startCell + selection = new Selection(endCell.range.start, endCell.range.end); + } + } + editor.selection = selection; + } else { + let newCell: ICellRange | undefined; + // full cell range is selected now decide if expanding or contracting? + if (isAnchorLessEqualActive || startCellIndex === endCellIndex) { + // anchor is above active, expand selection by cell below. + if (endCellIndex < cells.length - 1) { + newCell = cells[endCellIndex + 1]; + editor.selection = new Selection(startCell.range.start, newCell.range.end); + } + } else { + // anchor is below active, contract selection by cell above. + if (startCellIndex < endCellIndex) { + newCell = cells[startCellIndex + 1]; + editor.selection = new Selection(endCell.range.end, newCell.range.start); + } + } + + if (newCell) { + editor.revealRange(newCell.range, TextEditorRevealType.Default); + } + } + } + + @captureTelemetry(Telemetry.MoveCellsUp) + public async moveCellsUp(): Promise { + await this.moveCellsDirection(true); + } + + @captureTelemetry(Telemetry.MoveCellsDown) + public async moveCellsDown(): Promise { + await this.moveCellsDirection(false); + } + + @captureTelemetry(Telemetry.ChangeCellToMarkdown) + public changeCellToMarkdown() { + this.applyToCells((editor, cell, _) => { + return this.changeCellTo(editor, cell, 'markdown'); + }); + } + + @captureTelemetry(Telemetry.ChangeCellToCode) + public changeCellToCode() { + this.applyToCells((editor, cell, _) => { + return this.changeCellTo(editor, cell, 'code'); + }); + } + + @captureTelemetry(Telemetry.GotoNextCellInFile) + public gotoNextCell() { + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return; + } + + const currentSelection = editor.selection; + + const currentRunCellLens = this.getCurrentCellLens(currentSelection.start); + const nextRunCellLens = this.getNextCellLens(currentSelection.start); + + if (currentRunCellLens && nextRunCellLens) { + this.advanceToRange(nextRunCellLens.range); + } + } + + @captureTelemetry(Telemetry.GotoPrevCellInFile) + public gotoPreviousCell() { + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return; + } + + const currentSelection = editor.selection; + + const currentRunCellLens = this.getCurrentCellLens(currentSelection.start); + const prevRunCellLens = this.getPreviousCellLens(currentSelection.start); + + if (currentRunCellLens && prevRunCellLens) { + this.advanceToRange(prevRunCellLens.range); + } + } + + private applyToCells(callback: (editor: TextEditor, cell: ICellRange, cellIndex: number) => void) { + const editor = this.documentManager.activeTextEditor; + const startEndCellIndex = this.getStartEndCellIndex(editor?.selection); + if (!editor || !startEndCellIndex) { + return; + } + const cells = this.cells; + const startIndex = startEndCellIndex[0]; + const endIndex = startEndCellIndex[1]; + for (let cellIndex = startIndex; cellIndex <= endIndex; cellIndex += 1) { + callback(editor, cells[cellIndex], cellIndex); + } + } + + private changeCellTo(editor: TextEditor, cell: ICellRange, toCellType: nbformat.CellType) { + // change cell from code -> markdown or markdown -> code + if (toCellType === 'raw') { + throw Error('Cell Type raw not implemented'); + } + + // don't change cell type if already that type + if (cell.cell_type === toCellType) { + return; + } + const cellMatcher = new CellMatcher(this.configService.getSettings(editor.document.uri).datascience); + const definitionLine = editor.document.lineAt(cell.range.start.line); + const definitionText = editor.document.getText(definitionLine.range); + + // new definition text + const cellMarker = this.getDefaultCellMarker(editor.document.uri); + const definitionMatch = + toCellType === 'markdown' + ? cellMatcher.codeExecRegEx.exec(definitionText) // code -> markdown + : cellMatcher.markdownExecRegEx.exec(definitionText); // markdown -> code + if (!definitionMatch) { + return; + } + const definitionExtra = definitionMatch[definitionMatch.length - 1]; + const newDefinitionText = + toCellType === 'markdown' + ? `${cellMarker} [markdown]${definitionExtra}` // code -> markdown + : `${cellMarker}${definitionExtra}`; // markdown -> code + + editor.edit(async (editBuilder) => { + editBuilder.replace(definitionLine.range, newDefinitionText); + cell.cell_type = toCellType; + if (cell.range.start.line < cell.range.end.line) { + editor.selection = new Selection( + cell.range.start.line + 1, + 0, + cell.range.end.line, + cell.range.end.character + ); + // ensure all lines in markdown cell have a comment. + // these are not included in the test because it's unclear + // how TypeMoq works with them. + commands.executeCommand('editor.action.removeCommentLine'); + if (toCellType === 'markdown') { + commands.executeCommand('editor.action.addCommentLine'); + } + } + }); + } + + private async moveCellsDirection(directionUp: boolean): Promise { + const editor = this.documentManager.activeTextEditor; + if (!editor || !editor.selection) { + return false; + } + const startEndCellIndex = this.getStartEndCellIndex(editor.selection); + if (!startEndCellIndex) { + return false; + } + const startCellIndex = startEndCellIndex[0]; + const endCellIndex = startEndCellIndex[1]; + const cells = this.cells; + const startCell = cells[startCellIndex]; + const endCell = cells[endCellIndex]; + if (!startCell || !endCell) { + return false; + } + const currentRange = new Range(startCell.range.start, endCell.range.end); + const relativeSelectionRange = new Range( + editor.selection.start.line - currentRange.start.line, + editor.selection.start.character, + editor.selection.end.line - currentRange.start.line, + editor.selection.end.character + ); + const isActiveBeforeAnchor = editor.selection.active.isBefore(editor.selection.anchor); + let thenSetSelection: Thenable; + if (directionUp) { + if (startCellIndex === 0) { + return false; + } else { + const aboveCell = cells[startCellIndex - 1]; + const thenExchangeTextLines = this.exchangeTextLines(editor, aboveCell.range, currentRange); + thenSetSelection = thenExchangeTextLines.then((isEditSuccessful) => { + if (isEditSuccessful) { + editor.selection = new Selection( + aboveCell.range.start.line + relativeSelectionRange.start.line, + relativeSelectionRange.start.character, + aboveCell.range.start.line + relativeSelectionRange.end.line, + relativeSelectionRange.end.character + ); + } + return isEditSuccessful; + }); + } + } else { + if (endCellIndex === cells.length - 1) { + return false; + } else { + const belowCell = cells[endCellIndex + 1]; + const thenExchangeTextLines = this.exchangeTextLines(editor, currentRange, belowCell.range); + const belowCellLineLength = belowCell.range.end.line - belowCell.range.start.line; + const aboveCellLineLength = currentRange.end.line - currentRange.start.line; + const diffCellLineLength = belowCellLineLength - aboveCellLineLength; + thenSetSelection = thenExchangeTextLines.then((isEditSuccessful) => { + if (isEditSuccessful) { + editor.selection = new Selection( + belowCell.range.start.line + diffCellLineLength + relativeSelectionRange.start.line, + relativeSelectionRange.start.character, + belowCell.range.start.line + diffCellLineLength + relativeSelectionRange.end.line, + relativeSelectionRange.end.character + ); + } + return isEditSuccessful; + }); + } + } + return thenSetSelection.then((isEditSuccessful) => { + if (isEditSuccessful && isActiveBeforeAnchor) { + editor.selection = new Selection(editor.selection.active, editor.selection.anchor); + } + return true; + }); + } + + private exchangeTextLines(editor: TextEditor, aboveRange: Range, belowRange: Range): Thenable { + const aboveStartLine = aboveRange.start.line; + const aboveEndLine = aboveRange.end.line; + const belowStartLine = belowRange.start.line; + const belowEndLine = belowRange.end.line; + + if (aboveEndLine >= belowStartLine) { + throw RangeError(`Above lines must be fully above not ${aboveEndLine} <= ${belowStartLine}`); + } + + const above = new Range( + aboveStartLine, + 0, + aboveEndLine, + editor.document.lineAt(aboveEndLine).range.end.character + ); + const aboveText = editor.document.getText(above); + + const below = new Range( + belowStartLine, + 0, + belowEndLine, + editor.document.lineAt(belowEndLine).range.end.character + ); + const belowText = editor.document.getText(below); + + let betweenText = ''; + if (aboveEndLine + 1 < belowStartLine) { + const betweenStatLine = aboveEndLine + 1; + const betweenEndLine = belowStartLine - 1; + const between = new Range( + betweenStatLine, + 0, + betweenEndLine, + editor.document.lineAt(betweenEndLine).range.end.character + ); + betweenText = `${editor.document.getText(between)}\n`; + } + + const newText = `${belowText}\n${betweenText}${aboveText}`; + const newRange = new Range(above.start, below.end); + return editor.edit((editBuilder) => { + editBuilder.replace(newRange, newText); + this.codeLensUpdatedEvent.fire(); + }); + } + + private getStartEndCells(selection: Selection): ICellRange[] | undefined { + const startEndCellIndex = this.getStartEndCellIndex(selection); + if (startEndCellIndex) { + const startCell = this.getCellFromIndex(startEndCellIndex[0]); + const endCell = this.getCellFromIndex(startEndCellIndex[1]); + return [startCell, endCell]; + } + } + + private getStartEndCellIndex(selection?: Selection): number[] | undefined { + if (!selection) { + return undefined; + } + let startCellIndex = this.getCellIndex(selection.start); + let endCellIndex = startCellIndex; + // handle if the selection is the same line, hence same cell + if (selection.start.line !== selection.end.line) { + endCellIndex = this.getCellIndex(selection.end); + } + // handle when selection is above the top most cell + if (startCellIndex === -1) { + if (endCellIndex === -1) { + return undefined; + } else { + // selected a range above the first cell. + startCellIndex = 0; + const startCell = this.getCellFromIndex(0); + if (selection.start.line > startCell.range.start.line) { + throw RangeError( + `Should not be able to pick a range with an end in a cell and start after a cell. ${selection.start.line} > ${startCell.range.end.line}` + ); + } + } + } + if (startCellIndex >= 0 && endCellIndex >= 0) { + return [startCellIndex, endCellIndex]; + } + } + + private insertCell(editor: TextEditor, line: number) { + // insertCell + // + // Inserts a cell at current line defined as two new lines and then + // moves cursor to within the cell. + // ``` + // # %% + // + // ``` + // + const cellDelineator = this.getDefaultCellMarker(editor.document.uri); + let newCell = `${cellDelineator}\n\n`; + if (line >= editor.document.lineCount) { + newCell = `\n${cellDelineator}\n`; + } + + const cellStartPosition = new Position(line, 0); + const newCursorPosition = new Position(line + 1, 0); + + editor.edit((editBuilder) => { + editBuilder.insert(cellStartPosition, newCell); + this.codeLensUpdatedEvent.fire(); + }); + + editor.selection = new Selection(newCursorPosition, newCursorPosition); + } + + private getDefaultCellMarker(resource: Resource): string { + return ( + this.configService.getSettings(resource).datascience.defaultCellMarker || Identifiers.DefaultCodeCellMarker + ); + } + + private onCodeLensFactoryUpdated(): void { + // Update our code lenses. + if (this.document) { + this.codeLenses = this.codeLensFactory.createCodeLenses(this.document); + this.cells = this.codeLensFactory.getCellRanges(this.document); + } + this.codeLensUpdatedEvent.fire(); + } + + private onDocumentClosed(doc: TextDocument): void { + if (this.document && this.fs.arePathsSame(doc.uri, this.document.uri)) { + this.codeLensUpdatedEvent.dispose(); + this.closeDocumentDisposable?.dispose(); // NOSONAR + this.updateRequiredDisposable?.dispose(); // NOSONAR + } + } + + private async addCode( + code: string, + file: Uri, + line: number, + editor?: TextEditor, + debug?: boolean + ): Promise { + let result = false; + try { + const stopWatch = new StopWatch(); + const activeInteractiveWindow = await this.interactiveWindowProvider.getOrCreate(file); + if (debug) { + result = await activeInteractiveWindow.debugCode(code, file, line, editor); + } else { + result = await activeInteractiveWindow.addCode(code, file, line, editor); + } + this.sendPerceivedCellExecute(stopWatch); + } catch (err) { + await this.dataScienceErrorHandler.handleError(err); + } + + return result; + } + + private async addErrorMessage(file: Uri, leftCount: number): Promise { + // Only show an error message if any left + if (leftCount > 0) { + const message = localize.DataScience.cellStopOnErrorFormatMessage().format(leftCount.toString()); + try { + const activeInteractiveWindow = await this.interactiveWindowProvider.getOrCreate(file); + return activeInteractiveWindow.addMessage(message); + } catch (err) { + await this.dataScienceErrorHandler.handleError(err); + } + } + } + + private sendPerceivedCellExecute(runningStopWatch?: StopWatch) { + if (runningStopWatch) { + if (!CodeWatcher.sentExecuteCellTelemetry) { + CodeWatcher.sentExecuteCellTelemetry = true; + sendTelemetryEvent(Telemetry.ExecuteCellPerceivedCold, runningStopWatch.elapsedTime); + } else { + sendTelemetryEvent(Telemetry.ExecuteCellPerceivedWarm, runningStopWatch.elapsedTime); + } + } + } + + private async runMatchingCell(range: Range, advance?: boolean, debug?: boolean) { + const currentRunCellLens = this.getCurrentCellLens(range.start); + const nextRunCellLens = this.getNextCellLens(range.start); + + if (currentRunCellLens) { + // Move the next cell if allowed. + if (advance) { + if (nextRunCellLens) { + this.advanceToRange(nextRunCellLens.range); + } else { + // insert new cell at bottom after current + const editor = this.documentManager.activeTextEditor; + if (editor) { + this.insertCell(editor, currentRunCellLens.range.end.line + 1); + } + } + } + + // Run the cell after moving the selection + if (this.document) { + // Use that to get our code. + const code = this.document.getText(currentRunCellLens.range); + await this.addCode( + code, + this.document.uri, + currentRunCellLens.range.start.line, + this.documentManager.activeTextEditor, + debug + ); + } + } + } + + private getCellIndex(position: Position): number { + return this.cells.findIndex((cell) => position && cell.range.contains(position)); + } + + private getCellFromIndex(index: number): ICellRange { + const cells = this.cells; + const indexBounded = getIndex(index, cells.length); + return cells[indexBounded]; + } + + private getCellFromPosition(position?: Position): ICellRange | undefined { + if (!position) { + const editor = this.documentManager.activeTextEditor; + if (editor && editor.selection) { + position = editor.selection.active; + } + } + if (position) { + const index = this.getCellIndex(position); + if (index >= 0) { + return this.cells[index]; + } + } + } + + private getCurrentCellLens(pos: Position): CodeLens | undefined { + return this.codeLenses.find( + (l) => l.range.contains(pos) && l.command !== undefined && l.command.command === Commands.RunCell + ); + } + + private getNextCellLens(pos: Position): CodeLens | undefined { + const currentIndex = this.codeLenses.findIndex( + (l) => l.range.contains(pos) && l.command !== undefined && l.command.command === Commands.RunCell + ); + if (currentIndex >= 0) { + return this.codeLenses.find( + (l: CodeLens, i: number) => + l.command !== undefined && l.command.command === Commands.RunCell && i > currentIndex + ); + } + return undefined; + } + + private getPreviousCellLens(pos: Position): CodeLens | undefined { + const currentIndex = this.codeLenses.findIndex( + (l) => l.range.contains(pos) && l.command !== undefined && l.command.command === Commands.RunCell + ); + if (currentIndex >= 1) { + return this.codeLenses.find( + (l: CodeLens, i: number) => l.command !== undefined && i < currentIndex && i + 1 === currentIndex + ); + } + return undefined; + } + + private async runFileInteractiveInternal(debug: boolean) { + if (this.document) { + const code = this.document.getText(); + await this.addCode(code, this.document.uri, 0, undefined, debug); + } + } + + // Advance the cursor to the selected range + private advanceToRange(targetRange: Range) { + const editor = this.documentManager.activeTextEditor; + const newSelection = new Selection(targetRange.start, targetRange.start); + if (editor) { + editor.selection = newSelection; + editor.revealRange(targetRange, TextEditorRevealType.Default); + } + } +} diff --git a/src/client/datascience/editor-integration/decorator.ts b/src/client/datascience/editor-integration/decorator.ts new file mode 100644 index 000000000000..f85e2723ed7d --- /dev/null +++ b/src/client/datascience/editor-integration/decorator.ts @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import * as vscode from 'vscode'; + +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IDocumentManager, IVSCodeNotebook } from '../../common/application/types'; +import { PYTHON_LANGUAGE } from '../../common/constants'; +import { IConfigurationService, IDisposable, IDisposableRegistry } from '../../common/types'; +import { generateCellRangesFromDocument } from '../cellFactory'; + +@injectable() +export class Decorator implements IExtensionSingleActivationService, IDisposable { + private activeCellTop: vscode.TextEditorDecorationType | undefined; + private activeCellBottom: vscode.TextEditorDecorationType | undefined; + private cellSeparatorType: vscode.TextEditorDecorationType | undefined; + private timer: NodeJS.Timer | undefined | number; + + constructor( + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(IConfigurationService) private configuration: IConfigurationService, + @inject(IVSCodeNotebook) private vsCodeNotebook: IVSCodeNotebook + ) { + this.computeDecorations(); + disposables.push(this); + disposables.push(this.configuration.getSettings(undefined).onDidChange(this.settingsChanged, this)); + disposables.push(this.documentManager.onDidChangeActiveTextEditor(this.changedEditor, this)); + disposables.push(this.documentManager.onDidChangeTextEditorSelection(this.changedSelection, this)); + disposables.push(this.documentManager.onDidChangeTextDocument(this.changedDocument, this)); + this.settingsChanged(); + } + + public activate(): Promise { + // We don't need to do anything here as we already did all of our work in the + // constructor. + return Promise.resolve(); + } + + public dispose() { + if (this.timer) { + // tslint:disable-next-line: no-any + clearTimeout(this.timer as any); + } + } + + private settingsChanged() { + if (this.documentManager.activeTextEditor) { + this.triggerUpdate(this.documentManager.activeTextEditor); + } + } + + private changedEditor(editor: vscode.TextEditor | undefined) { + this.triggerUpdate(editor); + } + + private changedDocument(e: vscode.TextDocumentChangeEvent) { + if (this.documentManager.activeTextEditor && e.document === this.documentManager.activeTextEditor.document) { + this.triggerUpdate(this.documentManager.activeTextEditor); + } + } + + private changedSelection(e: vscode.TextEditorSelectionChangeEvent) { + if (e.textEditor && e.textEditor.selection.anchor) { + this.triggerUpdate(e.textEditor); + } + } + + private triggerUpdate(editor: vscode.TextEditor | undefined) { + if (this.timer) { + // tslint:disable-next-line: no-any + clearTimeout(this.timer as any); + } + this.timer = setTimeout(() => this.update(editor), 100); + } + + private computeDecorations() { + this.activeCellTop = this.documentManager.createTextEditorDecorationType({ + borderColor: new vscode.ThemeColor('peekView.border'), + borderWidth: '2px 0px 0px 0px', + borderStyle: 'solid', + isWholeLine: true + }); + this.activeCellBottom = this.documentManager.createTextEditorDecorationType({ + borderColor: new vscode.ThemeColor('peekView.border'), + borderWidth: '0px 0px 1px 0px', + borderStyle: 'solid', + isWholeLine: true + }); + this.cellSeparatorType = this.documentManager.createTextEditorDecorationType({ + borderColor: new vscode.ThemeColor('editor.lineHighlightBorder'), + borderWidth: '1px 0px 0px 0px', + borderStyle: 'solid', + isWholeLine: true + }); + } + + private update(editor: vscode.TextEditor | undefined) { + if ( + editor && + editor.document && + editor.document.languageId === PYTHON_LANGUAGE && + !this.vsCodeNotebook.activeNotebookEditor && + this.activeCellTop && + this.cellSeparatorType && + this.activeCellBottom + ) { + const settings = this.configuration.getSettings(editor.document.uri).datascience; + if (settings.decorateCells && settings.enabled) { + // Find all of the cells + const cells = generateCellRangesFromDocument(editor.document, settings); + + // Find the range for our active cell. + const currentRange = cells.map((c) => c.range).filter((r) => r.contains(editor.selection.anchor)); + const rangeTop = + currentRange.length > 0 ? [new vscode.Range(currentRange[0].start, currentRange[0].start)] : []; + const rangeBottom = + currentRange.length > 0 ? [new vscode.Range(currentRange[0].end, currentRange[0].end)] : []; + editor.setDecorations(this.activeCellTop, rangeTop); + editor.setDecorations(this.activeCellBottom, rangeBottom); + + // Find the start range for the rest + const startRanges = cells.map((c) => new vscode.Range(c.range.start, c.range.start)); + editor.setDecorations(this.cellSeparatorType, startRanges); + } else { + editor.setDecorations(this.activeCellTop, []); + editor.setDecorations(this.activeCellBottom, []); + editor.setDecorations(this.cellSeparatorType, []); + } + } + } +} diff --git a/src/client/datascience/editor-integration/hoverProvider.ts b/src/client/datascience/editor-integration/hoverProvider.ts new file mode 100644 index 000000000000..23723262de08 --- /dev/null +++ b/src/client/datascience/editor-integration/hoverProvider.ts @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable, named } from 'inversify'; + +import * as vscode from 'vscode'; +import { Cancellation } from '../../common/cancellation'; +import { PYTHON } from '../../common/constants'; +import { RunByLine } from '../../common/experiments/groups'; +import { traceError } from '../../common/logger'; + +import { IExperimentsManager } from '../../common/types'; +import { sleep } from '../../common/utils/async'; +import { noop } from '../../common/utils/misc'; +import { Identifiers } from '../constants'; +import { + ICell, + IDataScienceFileSystem, + IInteractiveWindowProvider, + IJupyterVariables, + INotebook, + INotebookExecutionLogger +} from '../types'; + +// This class provides hashes for debugging jupyter cells. Call getHashes just before starting debugging to compute all of the +// hashes for cells. +@injectable() +export class HoverProvider implements INotebookExecutionLogger, vscode.HoverProvider { + private runFiles = new Set(); + private enabled = false; + private hoverProviderRegistration: vscode.Disposable | undefined; + + constructor( + @inject(IExperimentsManager) experimentsManager: IExperimentsManager, + @inject(IJupyterVariables) @named(Identifiers.KERNEL_VARIABLES) private variableProvider: IJupyterVariables, + @inject(IInteractiveWindowProvider) private interactiveProvider: IInteractiveWindowProvider, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem + ) { + this.enabled = experimentsManager.inExperiment(RunByLine.experiment); + } + + public dispose() { + if (this.hoverProviderRegistration) { + this.hoverProviderRegistration.dispose(); + } + } + + // tslint:disable-next-line: no-any + public onKernelRestarted() { + this.runFiles.clear(); + } + + public async preExecute(cell: ICell, silent: boolean): Promise { + try { + if (!silent && cell.file && cell.file !== Identifiers.EmptyFileName) { + const size = this.runFiles.size; + this.runFiles.add(cell.file.toLocaleLowerCase()); + if (size !== this.runFiles.size) { + this.initializeHoverProvider(); + } + } + } catch (exc) { + // Don't let exceptions in a preExecute mess up normal operation + traceError(exc); + } + } + + public async postExecute(_cell: ICell, _silent: boolean): Promise { + noop(); + } + + public provideHover( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): vscode.ProviderResult { + const timeoutHandler = async () => { + await sleep(100); + return null; + }; + return Promise.race([timeoutHandler(), this.getVariableHover(document, position, token)]); + } + + private initializeHoverProvider() { + if (!this.hoverProviderRegistration) { + if (this.enabled) { + this.hoverProviderRegistration = vscode.languages.registerHoverProvider(PYTHON, this); + } else { + this.hoverProviderRegistration = { + dispose: noop + }; + } + } + } + + private getVariableHover( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): Promise { + // Make sure to fail as soon as the cancel token is signaled + return Cancellation.race(async (t) => { + const range = document.getWordRangeAtPosition(position); + if (range) { + const word = document.getText(range); + if (word) { + // See if we have any matching notebooks + const notebooks = this.getMatchingNotebooks(document); + if (notebooks && notebooks.length) { + // Just use the first one to reply if more than one. + const match = await Promise.race( + notebooks.map((n) => this.variableProvider.getMatchingVariable(n, word, t)) + ); + if (match) { + return { + contents: [`${word} = ${match.value}`] + }; + } + } + } + } + return null; + }, token); + } + + private getMatchingNotebooks(document: vscode.TextDocument): INotebook[] { + // First see if we have an interactive window who's owner is this document + let result = this.interactiveProvider.windows + .filter((w) => w.notebook && w.owner && this.fs.arePathsSame(w.owner, document.uri)) + .map((w) => w.notebook!); + if (!result || result.length === 0) { + // Not a match on the owner, find all that were submitters? Might be a bit risky + result = this.interactiveProvider.windows + .filter((w) => w.notebook && w.submitters.find((s) => this.fs.arePathsSame(s, document.uri))) + .map((w) => w.notebook!); + } + return result; + } +} diff --git a/src/client/datascience/errorHandler/errorHandler.ts b/src/client/datascience/errorHandler/errorHandler.ts new file mode 100644 index 000000000000..f0fa2f29cd95 --- /dev/null +++ b/src/client/datascience/errorHandler/errorHandler.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { inject, injectable } from 'inversify'; +import { IApplicationShell } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { JupyterInstallError } from '../jupyter/jupyterInstallError'; +import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; +import { JupyterZMQBinariesNotFoundError } from '../jupyter/jupyterZMQBinariesNotFoundError'; +import { JupyterServerSelector } from '../jupyter/serverSelector'; +import { IDataScienceErrorHandler, IJupyterInterpreterDependencyManager } from '../types'; +@injectable() +export class DataScienceErrorHandler implements IDataScienceErrorHandler { + constructor( + @inject(IApplicationShell) private applicationShell: IApplicationShell, + @inject(IJupyterInterpreterDependencyManager) protected dependencyManager: IJupyterInterpreterDependencyManager, + @inject(JupyterServerSelector) private serverSelector: JupyterServerSelector + ) {} + + public async handleError(err: Error): Promise { + if (err instanceof JupyterInstallError) { + await this.dependencyManager.installMissingDependencies(err); + } else if (err instanceof JupyterZMQBinariesNotFoundError) { + await this.showZMQError(err); + } else if (err instanceof JupyterSelfCertsError) { + // Don't show the message for self cert errors + noop(); + } else if (err.message) { + this.applicationShell.showErrorMessage(err.message); + } else { + this.applicationShell.showErrorMessage(err.toString()); + } + traceError('DataScience Error', err); + } + + private async showZMQError(err: JupyterZMQBinariesNotFoundError) { + // Ask the user to always pick remote as this is their only option + const selectNewServer = localize.DataScience.selectNewServer(); + this.applicationShell + .showErrorMessage(localize.DataScience.nativeDependencyFail().format(err.toString()), selectNewServer) + .then((selection) => { + if (selection === selectNewServer) { + this.serverSelector.selectJupyterURI(false).ignoreErrors(); + } + }); + } +} diff --git a/src/client/datascience/export/README.md b/src/client/datascience/export/README.md new file mode 100644 index 000000000000..0e1b31b9adf4 --- /dev/null +++ b/src/client/datascience/export/README.md @@ -0,0 +1,13 @@ + +## TO ADD A NEW EXPORT METHOD +1. Create a new command in src/client/datascience/constants +2. Register the command in this file +3. Add an item to the quick pick menu for your new export method from inside the getExportQuickPickItems() method (in this file). +4. Add a new command to the command pallete in package.json (optional) +5. Declare and add your file extensions inside exportManagerFilePicker +6. Declare and add your export method inside exportManager +7. Create an injectable class that implements IExport and register it in src/client/datascience/serviceRegistry +8. Implement the export method on your new class +9. Inject the class inside exportManager +10. Add a case for your new export method and call the export method of your new class with the appropriate arguments +11. Add telementry and status messages diff --git a/src/client/datascience/export/exportBase.ts b/src/client/datascience/export/exportBase.ts new file mode 100644 index 000000000000..1d54a7ba5284 --- /dev/null +++ b/src/client/datascience/export/exportBase.ts @@ -0,0 +1,86 @@ +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, Uri } from 'vscode'; + +import { IPythonExecutionFactory, IPythonExecutionService } from '../../common/process/types'; +import { reportAction } from '../progress/decorator'; +import { ReportableAction } from '../progress/types'; +import { IDataScienceFileSystem, IJupyterSubCommandExecutionService, INotebookImporter } from '../types'; +import { ExportFormat, IExport } from './types'; + +@injectable() +export class ExportBase implements IExport { + constructor( + @inject(IPythonExecutionFactory) protected readonly pythonExecutionFactory: IPythonExecutionFactory, + @inject(IJupyterSubCommandExecutionService) + protected jupyterService: IJupyterSubCommandExecutionService, + @inject(IDataScienceFileSystem) protected readonly fs: IDataScienceFileSystem, + @inject(INotebookImporter) protected readonly importer: INotebookImporter + ) {} + + // tslint:disable-next-line: no-empty + public async export(_source: Uri, _target: Uri, _token: CancellationToken): Promise {} + + @reportAction(ReportableAction.PerformingExport) + public async executeCommand( + source: Uri, + target: Uri, + format: ExportFormat, + token: CancellationToken + ): Promise { + if (token.isCancellationRequested) { + return; + } + + const service = await this.getExecutionService(source); + if (!service) { + return; + } + + if (token.isCancellationRequested) { + return; + } + + const tempTarget = await this.fs.createTemporaryLocalFile(path.extname(target.fsPath)); + const args = [ + source.fsPath, + '--to', + format, + '--output', + path.basename(tempTarget.filePath), + '--output-dir', + path.dirname(tempTarget.filePath) + ]; + const result = await service.execModule('jupyter', ['nbconvert'].concat(args), { + throwOnStdErr: false, + encoding: 'utf8', + token: token + }); + + if (token.isCancellationRequested) { + tempTarget.dispose(); + return; + } + + try { + await this.fs.copyLocal(tempTarget.filePath, target.fsPath); + } catch { + throw new Error(result.stderr); + } finally { + tempTarget.dispose(); + } + } + + protected async getExecutionService(source: Uri): Promise { + const interpreter = await this.jupyterService.getSelectedInterpreter(); + if (!interpreter) { + return; + } + return this.pythonExecutionFactory.createActivatedEnvironment({ + resource: source, + interpreter, + allowEnvironmentFetchExceptions: false, + bypassCondaExecution: true + }); + } +} diff --git a/src/client/datascience/export/exportDependencyChecker.ts b/src/client/datascience/export/exportDependencyChecker.ts new file mode 100644 index 000000000000..0ec028999e51 --- /dev/null +++ b/src/client/datascience/export/exportDependencyChecker.ts @@ -0,0 +1,30 @@ +import { inject, injectable } from 'inversify'; +import * as localize from '../../common/utils/localize'; +import { ProgressReporter } from '../progress/progressReporter'; +import { IJupyterExecution, IJupyterInterpreterDependencyManager } from '../types'; +import { ExportFormat } from './types'; + +@injectable() +export class ExportDependencyChecker { + constructor( + @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, + @inject(IJupyterInterpreterDependencyManager) + private readonly dependencyManager: IJupyterInterpreterDependencyManager, + @inject(ProgressReporter) private readonly progressReporter: ProgressReporter + ) {} + + public async checkDependencies(format: ExportFormat) { + // Before we try the import, see if we don't support it, if we don't give a chance to install dependencies + const reporter = this.progressReporter.createProgressIndicator(`Exporting to ${format}`); + try { + if (!(await this.jupyterExecution.isImportSupported())) { + await this.dependencyManager.installMissingDependencies(); + if (!(await this.jupyterExecution.isImportSupported())) { + throw new Error(localize.DataScience.jupyterNbConvertNotSupported()); + } + } + } finally { + reporter.dispose(); + } + } +} diff --git a/src/client/datascience/export/exportFileOpener.ts b/src/client/datascience/export/exportFileOpener.ts new file mode 100644 index 000000000000..d2d1e086efdc --- /dev/null +++ b/src/client/datascience/export/exportFileOpener.ts @@ -0,0 +1,66 @@ +import { inject, injectable } from 'inversify'; +import { Position, Uri } from 'vscode'; +import { IApplicationShell, IDocumentManager } from '../../common/application/types'; +import { PYTHON_LANGUAGE } from '../../common/constants'; + +import { IBrowserService } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { IDataScienceFileSystem } from '../types'; +import { ExportFormat } from './types'; + +@injectable() +export class ExportFileOpener { + constructor( + @inject(IDocumentManager) protected readonly documentManager: IDocumentManager, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(IBrowserService) private readonly browserService: IBrowserService + ) {} + + public async openFile(format: ExportFormat, uri: Uri) { + if (format === ExportFormat.python) { + await this.openPythonFile(uri); + sendTelemetryEvent(Telemetry.ExportNotebookAs, undefined, { + format: format, + successful: true, + opened: true + }); + } else { + const opened = await this.askOpenFile(uri); + sendTelemetryEvent(Telemetry.ExportNotebookAs, undefined, { + format: format, + successful: true, + opened: opened + }); + } + } + + private async openPythonFile(uri: Uri): Promise { + const contents = await this.fs.readFile(uri); + await this.fs.delete(uri); + const doc = await this.documentManager.openTextDocument({ language: PYTHON_LANGUAGE, content: contents }); + const editor = await this.documentManager.showTextDocument(doc); + // Edit the document so that it is dirty (add a space at the end) + editor.edit((editBuilder) => { + editBuilder.insert(new Position(editor.document.lineCount, 0), '\n'); + }); + } + + private async askOpenFile(uri: Uri): Promise { + const yes = localize.DataScience.openExportFileYes(); + const no = localize.DataScience.openExportFileNo(); + const items = [yes, no]; + + const selected = await this.applicationShell + .showInformationMessage(localize.DataScience.openExportedFileMessage(), ...items) + .then((item) => item); + + if (selected === yes) { + this.browserService.launch(uri.toString()); + return true; + } + return false; + } +} diff --git a/src/client/datascience/export/exportManager.ts b/src/client/datascience/export/exportManager.ts new file mode 100644 index 000000000000..e6457ed8e297 --- /dev/null +++ b/src/client/datascience/export/exportManager.ts @@ -0,0 +1,131 @@ +import { inject, injectable, named } from 'inversify'; +import { CancellationToken } from 'monaco-editor'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import { TemporaryDirectory } from '../../common/platform/types'; +import * as localize from '../../common/utils/localize'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { ProgressReporter } from '../progress/progressReporter'; +import { IDataScienceFileSystem, INotebookModel } from '../types'; +import { ExportDependencyChecker } from './exportDependencyChecker'; +import { ExportFileOpener } from './exportFileOpener'; +import { ExportUtil } from './exportUtil'; +import { ExportFormat, IExport, IExportManager, IExportManagerFilePicker } from './types'; + +@injectable() +export class ExportManager implements IExportManager { + constructor( + @inject(IExport) @named(ExportFormat.pdf) private readonly exportToPDF: IExport, + @inject(IExport) @named(ExportFormat.html) private readonly exportToHTML: IExport, + @inject(IExport) @named(ExportFormat.python) private readonly exportToPython: IExport, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(IExportManagerFilePicker) private readonly filePicker: IExportManagerFilePicker, + @inject(ProgressReporter) private readonly progressReporter: ProgressReporter, + @inject(ExportUtil) private readonly exportUtil: ExportUtil, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(ExportFileOpener) private readonly exportFileOpener: ExportFileOpener, + @inject(ExportDependencyChecker) private exportDepedencyChecker: ExportDependencyChecker + ) {} + + public async export(format: ExportFormat, model: INotebookModel, defaultFileName?: string): Promise { + let target; + try { + await this.exportDepedencyChecker.checkDependencies(format); + target = await this.getTargetFile(format, model, defaultFileName); + if (!target) { + return; + } + await this.performExport(format, model, target); + } catch (e) { + let msg = e; + traceError('Export failed', e); + sendTelemetryEvent(Telemetry.ExportNotebookAsFailed, undefined, { format: format }); + + if (format === ExportFormat.pdf) { + msg = localize.DataScience.exportToPDFDependencyMessage(); + } + + this.showExportFailed(msg); + } + } + + private async performExport(format: ExportFormat, model: INotebookModel, target: Uri) { + /* Need to make a temp directory here, instead of just a temp file. This is because + we need to store the contents of the notebook in a file that is named the same + as what we want the title of the exported file to be. To ensure this file path will be unique + we store it in a temp directory. The name of the file matters because when + exporting to certain formats the filename is used within the exported document as the title. */ + const tempDir = await this.exportUtil.generateTempDir(); + const source = await this.makeSourceFile(target, model, tempDir); + + const reporter = this.progressReporter.createProgressIndicator(`Exporting to ${format}`, true); + try { + await this.exportToFormat(source, target, format, reporter.token); + } finally { + tempDir.dispose(); + reporter.dispose(); + } + + if (reporter.token.isCancellationRequested) { + sendTelemetryEvent(Telemetry.ExportNotebookAs, undefined, { format: format, cancelled: true }); + return; + } + await this.exportFileOpener.openFile(format, target); + } + + private async getTargetFile( + format: ExportFormat, + model: INotebookModel, + defaultFileName?: string + ): Promise { + let target; + + if (format !== ExportFormat.python) { + target = await this.filePicker.getExportFileLocation(format, model.file, defaultFileName); + } else { + target = Uri.file((await this.fs.createTemporaryLocalFile('.py')).filePath); + } + + return target; + } + + private async makeSourceFile(target: Uri, model: INotebookModel, tempDir: TemporaryDirectory): Promise { + // Creates a temporary file with the same base name as the target file + const fileName = path.basename(target.fsPath, path.extname(target.fsPath)); + const sourceFilePath = await this.exportUtil.makeFileInDirectory(model, `${fileName}.ipynb`, tempDir.path); + return Uri.file(sourceFilePath); + } + + private showExportFailed(msg: string) { + // tslint:disable-next-line: messages-must-be-localized + this.applicationShell.showErrorMessage(`${localize.DataScience.failedExportMessage()} ${msg}`).then(); + } + + private async exportToFormat(source: Uri, target: Uri, format: ExportFormat, cancelToken: CancellationToken) { + if (format === ExportFormat.pdf) { + // When exporting to PDF we need to remove any SVG output. This is due to an error + // with nbconvert and a dependency of its called InkScape. + await this.exportUtil.removeSvgs(source); + } + + switch (format) { + case ExportFormat.python: + await this.exportToPython.export(source, target, cancelToken); + break; + + case ExportFormat.pdf: + await this.exportToPDF.export(source, target, cancelToken); + break; + + case ExportFormat.html: + await this.exportToHTML.export(source, target, cancelToken); + break; + + default: + break; + } + } +} diff --git a/src/client/datascience/export/exportManagerFilePicker.ts b/src/client/datascience/export/exportManagerFilePicker.ts new file mode 100644 index 000000000000..c92436c295dd --- /dev/null +++ b/src/client/datascience/export/exportManagerFilePicker.ts @@ -0,0 +1,83 @@ +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +import { Memento, SaveDialogOptions, Uri } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { IMemento, WORKSPACE_MEMENTO } from '../../common/types'; +import { ExportNotebookSettings } from '../interactive-common/interactiveWindowTypes'; +import { ExportFormat, IExportManagerFilePicker } from './types'; + +// File extensions for each export method +export const PDFExtensions = { PDF: ['pdf'] }; +export const HTMLExtensions = { HTML: ['html', 'htm'] }; +export const PythonExtensions = { Python: ['py'] }; + +@injectable() +export class ExportManagerFilePicker implements IExportManagerFilePicker { + private readonly defaultExportSaveLocation = ''; // set default save location + + constructor( + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceStorage: Memento + ) {} + + public async getExportFileLocation( + format: ExportFormat, + source: Uri, + defaultFileName?: string + ): Promise { + // map each export method to a set of file extensions + let fileExtensions; + let extension: string | undefined; + switch (format) { + case ExportFormat.python: + fileExtensions = PythonExtensions; + extension = '.py'; + break; + + case ExportFormat.pdf: + extension = '.pdf'; + fileExtensions = PDFExtensions; + break; + + case ExportFormat.html: + extension = '.html'; + fileExtensions = HTMLExtensions; + break; + + default: + return; + } + + const targetFileName = defaultFileName + ? defaultFileName + : `${path.basename(source.fsPath, path.extname(source.fsPath))}${extension}`; + + const dialogUri = Uri.file(path.join(this.getLastFileSaveLocation().fsPath, targetFileName)); + const options: SaveDialogOptions = { + defaultUri: dialogUri, + saveLabel: 'Export', + filters: fileExtensions + }; + + const uri = await this.applicationShell.showSaveDialog(options); + if (uri) { + await this.updateFileSaveLocation(uri); + } + + return uri; + } + + private getLastFileSaveLocation(): Uri { + const filePath = this.workspaceStorage.get( + ExportNotebookSettings.lastSaveLocation, + this.defaultExportSaveLocation + ); + + return Uri.file(filePath); + } + + private async updateFileSaveLocation(value: Uri) { + const location = path.dirname(value.fsPath); + await this.workspaceStorage.update(ExportNotebookSettings.lastSaveLocation, location); + } +} diff --git a/src/client/datascience/export/exportToHTML.ts b/src/client/datascience/export/exportToHTML.ts new file mode 100644 index 000000000000..d12c92502402 --- /dev/null +++ b/src/client/datascience/export/exportToHTML.ts @@ -0,0 +1,11 @@ +import { injectable } from 'inversify'; +import { CancellationToken, Uri } from 'vscode'; +import { ExportBase } from './exportBase'; +import { ExportFormat } from './types'; + +@injectable() +export class ExportToHTML extends ExportBase { + public async export(source: Uri, target: Uri, token: CancellationToken): Promise { + await this.executeCommand(source, target, ExportFormat.html, token); + } +} diff --git a/src/client/datascience/export/exportToPDF.ts b/src/client/datascience/export/exportToPDF.ts new file mode 100644 index 000000000000..0b4b6afba335 --- /dev/null +++ b/src/client/datascience/export/exportToPDF.ts @@ -0,0 +1,11 @@ +import { injectable } from 'inversify'; +import { CancellationToken, Uri } from 'vscode'; +import { ExportBase } from './exportBase'; +import { ExportFormat } from './types'; + +@injectable() +export class ExportToPDF extends ExportBase { + public async export(source: Uri, target: Uri, token: CancellationToken): Promise { + await this.executeCommand(source, target, ExportFormat.pdf, token); + } +} diff --git a/src/client/datascience/export/exportToPython.ts b/src/client/datascience/export/exportToPython.ts new file mode 100644 index 000000000000..b72168886123 --- /dev/null +++ b/src/client/datascience/export/exportToPython.ts @@ -0,0 +1,14 @@ +import { injectable } from 'inversify'; +import { CancellationToken, Uri } from 'vscode'; +import { ExportBase } from './exportBase'; + +@injectable() +export class ExportToPython extends ExportBase { + public async export(source: Uri, target: Uri, token: CancellationToken): Promise { + if (token.isCancellationRequested) { + return; + } + const contents = await this.importer.importFromFile(source); + await this.fs.writeFile(target, contents); + } +} diff --git a/src/client/datascience/export/exportUtil.ts b/src/client/datascience/export/exportUtil.ts new file mode 100644 index 000000000000..237b93ff4ca1 --- /dev/null +++ b/src/client/datascience/export/exportUtil.ts @@ -0,0 +1,107 @@ +import { nbformat } from '@jupyterlab/coreutils'; +import { inject, injectable } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { CancellationTokenSource, Uri } from 'vscode'; +import { TemporaryDirectory } from '../../common/platform/types'; +import { sleep } from '../../common/utils/async'; +import { ICell, IDataScienceFileSystem, INotebookExporter, INotebookModel, INotebookStorage } from '../types'; + +@injectable() +export class ExportUtil { + constructor( + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(INotebookStorage) private notebookStorage: INotebookStorage, + @inject(INotebookExporter) private jupyterExporter: INotebookExporter + ) {} + + public async generateTempDir(): Promise { + const resultDir = path.join(os.tmpdir(), uuid()); + await this.fs.createLocalDirectory(resultDir); + + return { + path: resultDir, + dispose: async () => { + // Try ten times. Process may still be up and running. + // We don't want to do async as async dispose means it may never finish and then we don't + // delete + let count = 0; + while (count < 10) { + try { + await this.fs.deleteLocalDirectory(resultDir); + count = 10; + } catch { + await sleep(3000); + count += 1; + } + } + } + }; + } + + public async makeFileInDirectory(model: INotebookModel, fileName: string, dirPath: string): Promise { + const newFilePath = path.join(dirPath, fileName); + + await this.fs.writeLocalFile(newFilePath, model.getContent()); + + return newFilePath; + } + + public async getModelFromCells(cells: ICell[]): Promise { + const tempDir = await this.generateTempDir(); + const tempFile = await this.fs.createTemporaryLocalFile('.ipynb'); + let model: INotebookModel; + + try { + await this.jupyterExporter.exportToFile(cells, tempFile.filePath, false); + const newPath = path.join(tempDir.path, '.ipynb'); + await this.fs.copyLocal(tempFile.filePath, newPath); + model = await this.notebookStorage.getOrCreateModel(Uri.file(newPath)); + } finally { + tempFile.dispose(); + tempDir.dispose(); + } + + return model; + } + + public async removeSvgs(source: Uri) { + const model = await this.notebookStorage.getOrCreateModel(source); + + const newCells: ICell[] = []; + for (const cell of model.cells) { + const outputs = cell.data.outputs; + if (outputs as nbformat.IOutput[]) { + this.removeSvgFromOutputs(outputs as nbformat.IOutput[]); + } + newCells.push(cell); + } + model.update({ + kind: 'modify', + newCells: newCells, + oldCells: model.cells as ICell[], + oldDirty: false, + newDirty: false, + source: 'user' + }); + await this.notebookStorage.save(model, new CancellationTokenSource().token); + } + + private removeSvgFromOutputs(outputs: nbformat.IOutput[]) { + const SVG = 'image/svg+xml'; + const PNG = 'image/png'; + for (const output of outputs as nbformat.IOutput[]) { + if (output.data as nbformat.IMimeBundle) { + const data = output.data as nbformat.IMimeBundle; + // only remove the svg if there is a png available + if (!(SVG in data)) { + continue; + } + if (PNG in data) { + delete data[SVG]; + } + } + } + } +} diff --git a/src/client/datascience/export/types.ts b/src/client/datascience/export/types.ts new file mode 100644 index 000000000000..d1ec1dc8e484 --- /dev/null +++ b/src/client/datascience/export/types.ts @@ -0,0 +1,23 @@ +import { CancellationToken, Uri } from 'vscode'; +import { INotebookModel } from '../types'; + +export enum ExportFormat { + pdf = 'pdf', + html = 'html', + python = 'python' +} + +export const IExportManager = Symbol('IExportManager'); +export interface IExportManager { + export(format: ExportFormat, model: INotebookModel, defaultFileName?: string): Promise; +} + +export const IExport = Symbol('IExport'); +export interface IExport { + export(source: Uri, target: Uri, token: CancellationToken): Promise; +} + +export const IExportManagerFilePicker = Symbol('IExportManagerFilePicker'); +export interface IExportManagerFilePicker { + getExportFileLocation(format: ExportFormat, source: Uri, defaultFileName?: string): Promise; +} diff --git a/src/client/datascience/gather/gatherListener.ts b/src/client/datascience/gather/gatherListener.ts new file mode 100644 index 000000000000..11e71948c613 --- /dev/null +++ b/src/client/datascience/gather/gatherListener.ts @@ -0,0 +1,326 @@ +import { inject, injectable } from 'inversify'; +import { IDisposable } from 'monaco-editor'; +import * as uuid from 'uuid/v4'; +import { Event, EventEmitter, Position, Uri, ViewColumn } from 'vscode'; +import { createMarkdownCell } from '../../../datascience-ui/common/cellFactory'; +import { IApplicationShell, IDocumentManager } from '../../common/application/types'; +import { PYTHON_LANGUAGE } from '../../common/constants'; +import { traceError } from '../../common/logger'; + +import type { nbformat } from '@jupyterlab/coreutils'; +import { IConfigurationService, Resource } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { sendTelemetryEvent } from '../../telemetry'; +import { generateCellsFromString } from '../cellFactory'; +import { Identifiers, Telemetry } from '../constants'; +import { + IInteractiveWindowMapping, + INotebookIdentity, + InteractiveWindowMessages +} from '../interactive-common/interactiveWindowTypes'; +import { + ICell, + IDataScienceFileSystem, + IGatherLogger, + IGatherProvider, + IInteractiveWindowListener, + INotebook, + INotebookEditorProvider, + INotebookExecutionLogger, + INotebookExporter, + INotebookProvider +} from '../types'; + +@injectable() +export class GatherListener implements IInteractiveWindowListener { + // tslint:disable-next-line: no-any + private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ + message: string; + // tslint:disable-next-line: no-any + payload: any; + }>(); + private notebookUri: Uri | undefined; + private gatherProvider: IGatherProvider | undefined; + private gatherTimer: StopWatch | undefined; + private linesSubmitted: number = 0; + private cellsSubmitted: number = 0; + + constructor( + @inject(IApplicationShell) private applicationShell: IApplicationShell, + @inject(INotebookExporter) private jupyterExporter: INotebookExporter, + @inject(INotebookEditorProvider) private ipynbProvider: INotebookEditorProvider, + @inject(INotebookProvider) private notebookProvider: INotebookProvider, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem + ) {} + + public dispose() { + noop(); + } + + // tslint:disable-next-line: no-any + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + + // tslint:disable-next-line: no-any + public onMessage(message: string, payload?: any): void { + switch (message) { + case InteractiveWindowMessages.NotebookExecutionActivated: + this.handleMessage(message, payload, this.doInitGather); + break; + + case InteractiveWindowMessages.GatherCode: + this.postEmitter.fire({ + message: InteractiveWindowMessages.Gathering, + payload: { cellId: payload.id, gathering: true } + }); + this.handleMessage(message, payload, this.doGather); + break; + + case InteractiveWindowMessages.GatherCodeToScript: + this.postEmitter.fire({ + message: InteractiveWindowMessages.Gathering, + payload: { cellId: payload.id, gathering: true } + }); + this.handleMessage(message, payload, this.doGatherToScript); + break; + + case InteractiveWindowMessages.RestartKernel: + this.linesSubmitted = 0; + this.cellsSubmitted = 0; + if (this.gatherProvider) { + try { + this.gatherProvider.resetLog(); + } catch (e) { + traceError('Gather: Exception at Reset Log', e); + sendTelemetryEvent(Telemetry.GatherException, undefined, { exceptionType: 'reset' }); + } + } + break; + + case InteractiveWindowMessages.FinishCell: + const cell = payload.cell as ICell; + if (cell && cell.data && cell.data.source) { + const lineCount: number = cell.data.source.length as number; + this.linesSubmitted += lineCount; + this.cellsSubmitted += 1; + } + break; + + default: + break; + } + } + + private handleMessage( + _message: T, + // tslint:disable:no-any + payload: any, + handler: (args: M[T]) => void + ) { + const args = payload as M[T]; + handler.bind(this)(args); + } + + private doInitGather(payload: INotebookIdentity & { owningResource: Resource }): void { + this.initGather(payload).ignoreErrors(); + } + + private async initGather(identity: INotebookIdentity & { owningResource: Resource }) { + this.notebookUri = identity.resource; + + const nb = await this.notebookProvider.getOrCreateNotebook({ identity: this.notebookUri, getOnly: true }); + // If we have an executing notebook, get its gather execution service. + if (nb) { + this.gatherProvider = this.getGatherProvider(nb); + } + } + + private getGatherProvider(nb: INotebook): any | undefined { + const gatherLogger = ( + nb.getLoggers().find((logger: INotebookExecutionLogger) => (logger).getGatherProvider) + ); + + if (gatherLogger) { + return gatherLogger.getGatherProvider(); + } + } + + private doGather(payload: ICell): Promise { + return this.gatherCodeInternal(payload) + .catch((err) => { + traceError(`Gather to Notebook error: ${err}`); + this.applicationShell.showErrorMessage(err); + }) + .finally(() => + this.postEmitter.fire({ + message: InteractiveWindowMessages.Gathering, + payload: { cellId: payload.id, gathering: false } + }) + ); + } + + private doGatherToScript(payload: ICell): Promise { + return this.gatherCodeInternal(payload, true) + .catch((err) => { + traceError(`Gather to Script error: ${err}`); + this.applicationShell.showErrorMessage(err); + }) + .finally(() => + this.postEmitter.fire({ + message: InteractiveWindowMessages.Gathering, + payload: { cellId: payload.id, gathering: false } + }) + ); + } + + private gatherCodeInternal = async (cell: ICell, toScript: boolean = false) => { + this.gatherTimer = new StopWatch(); + let slicedProgram: string | undefined; + + try { + slicedProgram = this.gatherProvider + ? this.gatherProvider.gatherCode(cell) + : localize.DataScience.gatherError(); + } catch (e) { + traceError('Gather: Exception at gatherCode', e); + sendTelemetryEvent(Telemetry.GatherException, undefined, { exceptionType: 'gather' }); + const newline = '\n'; + const defaultCellMarker = + this.configService.getSettings().datascience.defaultCellMarker || Identifiers.DefaultCodeCellMarker; + slicedProgram = defaultCellMarker + newline + localize.DataScience.gatherError() + newline + (e as string); + } + + if (!slicedProgram) { + sendTelemetryEvent(Telemetry.GatherCompleted, this.gatherTimer?.elapsedTime, { result: 'err' }); + } else { + const gatherToScript: boolean = this.configService.getSettings().datascience.gatherToScript || toScript; + + if (gatherToScript) { + await this.showFile(slicedProgram, cell.file); + sendTelemetryEvent(Telemetry.GatherCompleted, this.gatherTimer?.elapsedTime, { result: 'script' }); + } else { + await this.showNotebook(slicedProgram, cell); + sendTelemetryEvent(Telemetry.GatherCompleted, this.gatherTimer?.elapsedTime, { + result: 'notebook' + }); + } + + sendTelemetryEvent(Telemetry.GatherStats, undefined, { + linesSubmitted: this.linesSubmitted, + cellsSubmitted: this.cellsSubmitted, + linesGathered: slicedProgram.trim().splitLines().length, + cellsGathered: generateCellsFromString(slicedProgram).length + }); + } + }; + + private async showNotebook(slicedProgram: string, cell: ICell) { + if (slicedProgram) { + const file = + cell.file === Identifiers.EmptyFileName && this.notebookUri ? this.notebookUri.fsPath : cell.file; + + let cells: ICell[] = [ + { + id: uuid(), + file: '', + line: 0, + state: 0, + data: createMarkdownCell(localize.DataScience.gatheredNotebookDescriptionInMarkdown().format(file)) + } + ]; + + // Create new notebook with the returned program and open it. + cells = cells.concat(generateCellsFromString(slicedProgram)); + + // Try to get a kernelspec + let kernelspec: nbformat.IKernelspecMetadata | undefined; + try { + const text = await this.fs.readLocalFile(file); + const json = JSON.parse(text); + kernelspec = json.metadata.kernelspec; + } catch (e) { + traceError('Gather: No kernelspec found', e); + } + + const notebook = await this.jupyterExporter.translateToNotebook(cells, undefined, kernelspec); + if (notebook) { + const contents = JSON.stringify(notebook); + const editor = await this.ipynbProvider.createNew(contents); + + let disposableNotebookSaved: IDisposable; + let disposableNotebookClosed: IDisposable; + + const savedHandler = () => { + sendTelemetryEvent(Telemetry.GatheredNotebookSaved); + if (disposableNotebookSaved) { + disposableNotebookSaved.dispose(); + } + if (disposableNotebookClosed) { + disposableNotebookClosed.dispose(); + } + }; + + const closedHandler = () => { + if (disposableNotebookSaved) { + disposableNotebookSaved.dispose(); + } + if (disposableNotebookClosed) { + disposableNotebookClosed.dispose(); + } + }; + + disposableNotebookSaved = editor.saved(savedHandler); + disposableNotebookClosed = editor.closed(closedHandler); + } + } + } + + private async showFile(slicedProgram: string, filename: string) { + const defaultCellMarker = + this.configService.getSettings().datascience.defaultCellMarker || Identifiers.DefaultCodeCellMarker; + + if (slicedProgram) { + // Remove all cell definitions and newlines + const re = new RegExp(`^(${defaultCellMarker}.*|\\s*)\n`, 'gm'); + slicedProgram = slicedProgram.replace(re, ''); + } + + const annotatedScript = `${localize.DataScience.gatheredScriptDescription()}${defaultCellMarker}\n${slicedProgram}`; + + // Don't want to open the gathered code on top of the interactive window + let viewColumn: ViewColumn | undefined; + const fileNameMatch = this.documentManager.visibleTextEditors.filter((textEditor) => + this.fs.areLocalPathsSame(textEditor.document.fileName, filename) + ); + const definedVisibleEditors = this.documentManager.visibleTextEditors.filter( + (textEditor) => textEditor.viewColumn !== undefined + ); + if (this.documentManager.visibleTextEditors.length > 0 && fileNameMatch.length > 0) { + // Original file is visible + viewColumn = fileNameMatch[0].viewColumn; + } else if (this.documentManager.visibleTextEditors.length > 0 && definedVisibleEditors.length > 0) { + // There is a visible text editor, just not the original file. Make sure viewColumn isn't undefined + viewColumn = definedVisibleEditors[0].viewColumn; + } else { + // Only one panel open and interactive window is occupying it, or original file is open but hidden + viewColumn = ViewColumn.Beside; + } + + // Create a new open editor with the returned program in the right panel + const doc = await this.documentManager.openTextDocument({ + content: annotatedScript, + language: PYTHON_LANGUAGE + }); + const editor = await this.documentManager.showTextDocument(doc, viewColumn); + + // Edit the document so that it is dirty (add a space at the end) + editor.edit((editBuilder) => { + editBuilder.insert(new Position(editor.document.lineCount, 0), '\n'); + }); + } +} diff --git a/src/client/datascience/gather/gatherLogger.ts b/src/client/datascience/gather/gatherLogger.ts new file mode 100644 index 000000000000..e5b625d26a2f --- /dev/null +++ b/src/client/datascience/gather/gatherLogger.ts @@ -0,0 +1,79 @@ +import { inject, injectable } from 'inversify'; +// tslint:disable-next-line: no-require-imports +import cloneDeep = require('lodash/cloneDeep'); +import { extensions } from 'vscode'; +import { concatMultilineString } from '../../../datascience-ui/common'; +import { traceError, traceInfo } from '../../common/logger'; +import { IConfigurationService } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { sendTelemetryEvent } from '../../telemetry'; +import { CellMatcher } from '../cellMatcher'; +import { GatherExtension, Telemetry } from '../constants'; +import { ICell as IVscCell, IGatherLogger, IGatherProvider } from '../types'; + +@injectable() +export class GatherLogger implements IGatherLogger { + private gather: IGatherProvider | undefined; + constructor(@inject(IConfigurationService) private configService: IConfigurationService) { + this.initGatherExtension().ignoreErrors(); + } + + public dispose() { + noop(); + } + public onKernelRestarted() { + noop(); + } + + public async preExecute(_vscCell: IVscCell, _silent: boolean): Promise { + // This function is just implemented here for compliance with the INotebookExecutionLogger interface + noop(); + } + + public async postExecute(vscCell: IVscCell, _silent: boolean): Promise { + if (this.gather) { + // Don't log if vscCell.data.source is an empty string or if it was + // silently executed. Original Jupyter extension also does this. + if (vscCell.data.source !== '' && !_silent) { + // First make a copy of this cell, as we are going to modify it + const cloneCell: IVscCell = cloneDeep(vscCell); + + // Strip first line marker. We can't do this at JupyterServer.executeCodeObservable because it messes up hashing + const cellMatcher = new CellMatcher(this.configService.getSettings().datascience); + cloneCell.data.source = cellMatcher.stripFirstMarker(concatMultilineString(vscCell.data.source)); + + try { + this.gather.logExecution(cloneCell); + } catch (e) { + traceError('Gather: Exception at Log Execution', e); + sendTelemetryEvent(Telemetry.GatherException, undefined, { exceptionType: 'log' }); + } + } + } + } + + public getGatherProvider(): IGatherProvider | undefined { + return this.gather; + } + + private async initGatherExtension() { + const ext = extensions.getExtension(GatherExtension); + if (ext) { + sendTelemetryEvent(Telemetry.GatherIsInstalled); + if (!ext.isActive) { + try { + await ext.activate(); + } catch (e) { + traceError('Gather: Exception at Activate', e); + sendTelemetryEvent(Telemetry.GatherException, undefined, { exceptionType: 'activate' }); + } + } + const api = ext.exports; + try { + this.gather = api.getGatherProvider(); + } catch { + traceInfo(`Gather not installed`); + } + } + } +} diff --git a/src/client/datascience/interactive-common/debugListener.ts b/src/client/datascience/interactive-common/debugListener.ts new file mode 100644 index 000000000000..c50fb893ced5 --- /dev/null +++ b/src/client/datascience/interactive-common/debugListener.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable, named } from 'inversify'; +import { DebugSession, Event, EventEmitter } from 'vscode'; + +import { noop } from '../../common/utils/misc'; +import { Identifiers } from '../constants'; +import { IInteractiveWindowListener, IJupyterDebugService } from '../types'; +import { InteractiveWindowMessages } from './interactiveWindowTypes'; + +// tslint:disable: no-any +@injectable() +export class DebugListener implements IInteractiveWindowListener { + private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ + message: string; + payload: any; + }>(); + constructor( + @inject(IJupyterDebugService) + @named(Identifiers.MULTIPLEXING_DEBUGSERVICE) + private debugService: IJupyterDebugService + ) { + this.debugService.onDidChangeActiveDebugSession(this.onChangeDebugSession.bind(this)); + } + + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + + public onMessage(message: string, _payload?: any): void { + switch (message) { + default: + break; + } + } + public dispose(): void | undefined { + noop(); + } + + private onChangeDebugSession(e: DebugSession | undefined) { + if (e) { + this.postEmitter.fire({ message: InteractiveWindowMessages.StartDebugging, payload: undefined }); + } else { + this.postEmitter.fire({ message: InteractiveWindowMessages.StopDebugging, payload: undefined }); + } + } +} diff --git a/src/client/datascience/interactive-common/intellisense/conversion.ts b/src/client/datascience/interactive-common/intellisense/conversion.ts new file mode 100644 index 000000000000..6dd99b2b41fc --- /dev/null +++ b/src/client/datascience/interactive-common/intellisense/conversion.ts @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../../common/extensions'; + +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as vscode from 'vscode'; +import * as vscodeLanguageClient from 'vscode-languageclient/node'; + +// See the comment on convertCompletionItemKind below +// Here's the monaco enum: +enum monacoCompletionItemKind { + Method = 0, + Function = 1, + Constructor = 2, + Field = 3, + Variable = 4, + Class = 5, + Struct = 6, + Interface = 7, + Module = 8, + Property = 9, + Event = 10, + Operator = 11, + Unit = 12, + Value = 13, + Constant = 14, + Enum = 15, + EnumMember = 16, + Keyword = 17, + Text = 18, + Color = 19, + File = 20, + Reference = 21, + Customcolor = 22, + Folder = 23, + TypeParameter = 24, + Snippet = 25 +} +// + +// Left side is the vscode value. +const mapCompletionItemKind: Map = new Map([ + [vscode.CompletionItemKind.Text, monacoCompletionItemKind.Text], // Text + [vscode.CompletionItemKind.Method, monacoCompletionItemKind.Method], // Method + [vscode.CompletionItemKind.Function, monacoCompletionItemKind.Function], // Function + [vscode.CompletionItemKind.Constructor, monacoCompletionItemKind.Constructor], // Constructor + [vscode.CompletionItemKind.Field, monacoCompletionItemKind.Field], // Field + [vscode.CompletionItemKind.Variable, monacoCompletionItemKind.Variable], // Variable + [vscode.CompletionItemKind.Class, monacoCompletionItemKind.Class], // Class + [vscode.CompletionItemKind.Interface, monacoCompletionItemKind.Interface], // Interface + [vscode.CompletionItemKind.Module, monacoCompletionItemKind.Module], // Module + [vscode.CompletionItemKind.Property, monacoCompletionItemKind.Property], // Property + [vscode.CompletionItemKind.Unit, monacoCompletionItemKind.Unit], // Unit + [vscode.CompletionItemKind.Value, monacoCompletionItemKind.Value], // Value + [vscode.CompletionItemKind.Enum, monacoCompletionItemKind.Enum], // Enum + [vscode.CompletionItemKind.Keyword, monacoCompletionItemKind.Keyword], // Keyword + [vscode.CompletionItemKind.Snippet, monacoCompletionItemKind.Snippet], // Snippet + [vscode.CompletionItemKind.Color, monacoCompletionItemKind.Color], // Color + [vscode.CompletionItemKind.File, monacoCompletionItemKind.File], // File + [vscode.CompletionItemKind.Reference, monacoCompletionItemKind.Reference], // Reference + [vscode.CompletionItemKind.Folder, monacoCompletionItemKind.Folder], // Folder + [vscode.CompletionItemKind.EnumMember, monacoCompletionItemKind.EnumMember], // EnumMember + [vscode.CompletionItemKind.Constant, monacoCompletionItemKind.Constant], // Constant + [vscode.CompletionItemKind.Struct, monacoCompletionItemKind.Struct], // Struct + [vscode.CompletionItemKind.Event, monacoCompletionItemKind.Event], // Event + [vscode.CompletionItemKind.Operator, monacoCompletionItemKind.Operator], // Operator + [vscode.CompletionItemKind.TypeParameter, monacoCompletionItemKind.TypeParameter] // TypeParameter +]); + +// Left side is the monaco value. +const reverseMapCompletionItemKind: Map = new Map( + [ + [monacoCompletionItemKind.Text, vscode.CompletionItemKind.Text], // Text + [monacoCompletionItemKind.Method, vscode.CompletionItemKind.Method], // Method + [monacoCompletionItemKind.Function, vscode.CompletionItemKind.Function], // Function + [monacoCompletionItemKind.Constructor, vscode.CompletionItemKind.Constructor], // Constructor + [monacoCompletionItemKind.Field, vscode.CompletionItemKind.Field], // Field + [monacoCompletionItemKind.Variable, vscode.CompletionItemKind.Variable], // Variable + [monacoCompletionItemKind.Class, vscode.CompletionItemKind.Class], // Class + [monacoCompletionItemKind.Interface, vscode.CompletionItemKind.Interface], // Interface + [monacoCompletionItemKind.Module, vscode.CompletionItemKind.Module], // Module + [monacoCompletionItemKind.Property, vscode.CompletionItemKind.Property], // Property + [monacoCompletionItemKind.Unit, vscode.CompletionItemKind.Unit], // Unit + [monacoCompletionItemKind.Value, vscode.CompletionItemKind.Value], // Value + [monacoCompletionItemKind.Enum, vscode.CompletionItemKind.Enum], // Enum + [monacoCompletionItemKind.Keyword, vscode.CompletionItemKind.Keyword], // Keyword + [monacoCompletionItemKind.Snippet, vscode.CompletionItemKind.Snippet], // Snippet + [monacoCompletionItemKind.Color, vscode.CompletionItemKind.Color], // Color + [monacoCompletionItemKind.File, vscode.CompletionItemKind.File], // File + [monacoCompletionItemKind.Reference, vscode.CompletionItemKind.Reference], // Reference + [monacoCompletionItemKind.Folder, vscode.CompletionItemKind.Folder], // Folder + [monacoCompletionItemKind.EnumMember, vscode.CompletionItemKind.EnumMember], // EnumMember + [monacoCompletionItemKind.Constant, vscode.CompletionItemKind.Constant], // Constant + [monacoCompletionItemKind.Struct, vscode.CompletionItemKind.Struct], // Struct + [monacoCompletionItemKind.Event, vscode.CompletionItemKind.Event], // Event + [monacoCompletionItemKind.Operator, vscode.CompletionItemKind.Operator], // Operator + [monacoCompletionItemKind.TypeParameter, vscode.CompletionItemKind.TypeParameter] // TypeParameter + ] +); + +const mapJupyterKind: Map = new Map([ + ['method', monacoCompletionItemKind.Method], + ['function', monacoCompletionItemKind.Function], + ['constructor', monacoCompletionItemKind.Constructor], + ['field', monacoCompletionItemKind.Field], + ['variable', monacoCompletionItemKind.Variable], + ['class', monacoCompletionItemKind.Class], + ['struct', monacoCompletionItemKind.Struct], + ['interface', monacoCompletionItemKind.Interface], + ['module', monacoCompletionItemKind.Module], + ['property', monacoCompletionItemKind.Property], + ['event', monacoCompletionItemKind.Event], + ['operator', monacoCompletionItemKind.Operator], + ['unit', monacoCompletionItemKind.Unit], + ['value', monacoCompletionItemKind.Value], + ['constant', monacoCompletionItemKind.Constant], + ['enum', monacoCompletionItemKind.Enum], + ['enumMember', monacoCompletionItemKind.EnumMember], + ['keyword', monacoCompletionItemKind.Keyword], + ['text', monacoCompletionItemKind.Text], + ['color', monacoCompletionItemKind.Color], + ['file', monacoCompletionItemKind.File], + ['reference', monacoCompletionItemKind.Reference], + ['customcolor', monacoCompletionItemKind.Customcolor], + ['folder', monacoCompletionItemKind.Folder], + ['typeParameter', monacoCompletionItemKind.TypeParameter], + ['snippet', monacoCompletionItemKind.Snippet], + ['', monacoCompletionItemKind.Field] +]); + +function convertToMonacoRange(range: vscodeLanguageClient.Range | undefined): monacoEditor.IRange | undefined { + if (range) { + return { + startLineNumber: range.start.line + 1, + startColumn: range.start.character + 1, + endLineNumber: range.end.line + 1, + endColumn: range.end.character + 1 + }; + } +} + +function convertToVSCodeRange(range: monacoEditor.IRange | undefined): vscode.Range | undefined { + if (range) { + return new vscode.Range( + new vscode.Position(range.startLineNumber - 1, range.startColumn - 1), + new vscode.Position(range.endLineNumber - 1, range.endColumn - 1) + ); + } +} + +// Something very fishy. If the monacoEditor.languages.CompletionItemKind is included here, we get this error on startup +// Activating extension `ms-python.python` failed: Unexpected token { +// extensionHostProcess.js:457 +// Here is the error stack: f:\vscode-python\node_modules\monaco-editor\esm\vs\editor\editor.api.js:5 +// import { EDITOR_DEFAULTS } from './common/config/editorOptions.js'; +// Instead just use a map +function convertToMonacoCompletionItemKind(kind?: number): number { + const value = kind ? mapCompletionItemKind.get(kind) : monacoCompletionItemKind.Property; // Property is 9 + if (value) { + return value; + } + return monacoCompletionItemKind.Property; +} + +function convertToVSCodeCompletionItemKind(kind?: number): vscode.CompletionItemKind { + const value = kind ? reverseMapCompletionItemKind.get(kind) : vscode.CompletionItemKind.Property; + if (value) { + return value; + } + return vscode.CompletionItemKind.Property; +} + +const SnippetEscape = 4; + +export function convertToMonacoCompletionItem( + item: vscodeLanguageClient.CompletionItem, + requiresKindConversion: boolean +): monacoEditor.languages.CompletionItem { + // They should be pretty much identical? Except for ranges. + // tslint:disable-next-line: no-object-literal-type-assertion no-any + const result = ({ ...item } as any) as monacoEditor.languages.CompletionItem; + if (requiresKindConversion) { + result.kind = convertToMonacoCompletionItemKind(item.kind); + } + + // Make sure we have insert text, otherwise the monaco editor will crash on trying to hit tab or enter on the text + if (!result.insertText && result.label) { + result.insertText = result.label; + } + + // tslint:disable-next-line: no-any + const snippet = (result.insertText as any) as vscode.SnippetString; + if (snippet.value) { + result.insertTextRules = SnippetEscape; + // Monaco can't handle the snippetText value, so rewrite it. + result.insertText = snippet.value; + } + + // Make sure we don't have _documentPosition. It holds onto a huge tree of information + // tslint:disable-next-line: no-any + const resultAny = result as any; + if (resultAny._documentPosition) { + delete resultAny._documentPosition; + } + + return result; +} + +export function convertToVSCodeCompletionItem(item: monacoEditor.languages.CompletionItem): vscode.CompletionItem { + // tslint:disable-next-line: no-object-literal-type-assertion no-any + const result = ({ ...item } as any) as vscode.CompletionItem; + + if (item.kind && result.kind) { + result.kind = convertToVSCodeCompletionItemKind(item.kind); + } + + if (item.range && result.range) { + result.range = convertToVSCodeRange(item.range); + } + + return result; +} + +export function convertToMonacoCompletionList( + result: + | vscodeLanguageClient.CompletionList + | vscodeLanguageClient.CompletionItem[] + | vscode.CompletionItem[] + | vscode.CompletionList + | null, + requiresKindConversion: boolean +): monacoEditor.languages.CompletionList { + if (result) { + if (result.hasOwnProperty('items')) { + const list = result as vscodeLanguageClient.CompletionList; + return { + suggestions: list.items.map((l) => convertToMonacoCompletionItem(l, requiresKindConversion)), + incomplete: list.isIncomplete + }; + } else { + // Must be one of the two array types since there's no items property. + const array = result as vscodeLanguageClient.CompletionItem[]; + return { + suggestions: array.map((l) => convertToMonacoCompletionItem(l, requiresKindConversion)), + incomplete: false + }; + } + } + + return { + suggestions: [], + incomplete: false + }; +} + +function convertToMonacoMarkdown( + strings: + | vscodeLanguageClient.MarkupContent + | vscodeLanguageClient.MarkedString + | vscodeLanguageClient.MarkedString[] + | vscode.MarkedString + | vscode.MarkedString[] +): monacoEditor.IMarkdownString[] { + if (strings.hasOwnProperty('kind')) { + const content = strings as vscodeLanguageClient.MarkupContent; + return [ + { + value: content.value + } + ]; + } else if (strings.hasOwnProperty('value')) { + // tslint:disable-next-line: no-any + const content = strings as any; + return [ + { + value: content.value + } + ]; + } else if (typeof strings === 'string') { + return [ + { + value: strings.toString() + } + ]; + } else if (Array.isArray(strings)) { + const array = strings as vscodeLanguageClient.MarkedString[]; + return array.map((a) => convertToMonacoMarkdown(a)[0]); + } + + return []; +} + +export function convertToMonacoHover( + result: vscodeLanguageClient.Hover | vscode.Hover | null | undefined +): monacoEditor.languages.Hover { + if (result) { + return { + contents: convertToMonacoMarkdown(result.contents), + range: convertToMonacoRange(result.range) + }; + } + + return { + contents: [] + }; +} + +export function convertStringsToSuggestions( + strings: ReadonlyArray, + range: monacoEditor.IRange, + // tslint:disable-next-line: no-any + metadata: any +): monacoEditor.languages.CompletionItem[] { + // Try to compute kind from the metadata. + let kinds: number[]; + if (metadata && metadata._jupyter_types_experimental) { + // tslint:disable-next-line: no-any + kinds = metadata._jupyter_types_experimental.map((e: any) => { + const result = mapJupyterKind.get(e.type); + return result ? result : 3; // If not found use Field = 3 + }); + } + + return strings.map((s: string, i: number) => { + return { + label: s, + insertText: s, + sortText: s, + kind: kinds ? kinds[i] : 3, // Note: importing the monacoEditor.languages.CompletionItemKind causes a failure in loading the extension. So we use numbers. + range + }; + }); +} + +export function convertToMonacoSignatureHelp( + result: vscodeLanguageClient.SignatureHelp | vscode.SignatureHelp | null +): monacoEditor.languages.SignatureHelp { + if (result) { + return result as monacoEditor.languages.SignatureHelp; + } + + return { + signatures: [], + activeParameter: 0, + activeSignature: 0 + }; +} diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseDocument.ts b/src/client/datascience/interactive-common/intellisense/intellisenseDocument.ts new file mode 100644 index 000000000000..52bbc2afbf5e --- /dev/null +++ b/src/client/datascience/interactive-common/intellisense/intellisenseDocument.ts @@ -0,0 +1,693 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../../common/extensions'; + +import { EndOfLine, Position, Range, TextDocument, TextDocumentContentChangeEvent, TextLine, Uri } from 'vscode'; +import * as vscodeLanguageClient from 'vscode-languageclient/node'; + +import { PYTHON_LANGUAGE } from '../../../common/constants'; +import { Identifiers } from '../../constants'; +import { IEditorContentChange } from '../interactiveWindowTypes'; +import { DefaultWordPattern, ensureValidWordDefinition, getWordAtText, regExpLeadsToEndlessLoop } from './wordHelper'; + +class IntellisenseLine implements TextLine { + private _range: Range; + private _rangeWithLineBreak: Range; + private _firstNonWhitespaceIndex: number | undefined; + private _isEmpty: boolean | undefined; + + constructor(private _contents: string, private _line: number, private _offset: number) { + this._range = new Range(new Position(_line, 0), new Position(_line, _contents.length)); + this._rangeWithLineBreak = new Range(this.range.start, new Position(_line, _contents.length + 1)); + } + + public get offset(): number { + return this._offset; + } + public get lineNumber(): number { + return this._line; + } + public get text(): string { + return this._contents; + } + public get range(): Range { + return this._range; + } + public get rangeIncludingLineBreak(): Range { + return this._rangeWithLineBreak; + } + public get firstNonWhitespaceCharacterIndex(): number { + if (this._firstNonWhitespaceIndex === undefined) { + this._firstNonWhitespaceIndex = this._contents.trimLeft().length - this._contents.length; + } + return this._firstNonWhitespaceIndex; + } + public get isEmptyOrWhitespace(): boolean { + if (this._isEmpty === undefined) { + this._isEmpty = this._contents.length === 0 || this._contents.trim().length === 0; + } + return this._isEmpty; + } +} + +interface ICellRange { + id: string; + start: number; + fullEnd: number; + currentEnd: number; +} + +export interface ICellData { + text: string; + offset: number; +} + +export class IntellisenseDocument implements TextDocument { + private _uri: Uri; + private _version: number = 1; + private _lines: IntellisenseLine[] = []; + private _contents: string = ''; + private _cellRanges: ICellRange[] = []; + private inEditMode: boolean = false; + + constructor(fileName: string) { + // The file passed in is the base Uri for where we're basing this + // document. + // + // What about liveshare? + this._uri = Uri.file(fileName); + + // We should start our edit offset at 0. Each cell should end with a '/n' + this._cellRanges.push({ id: Identifiers.EditCellId, start: 0, fullEnd: 0, currentEnd: 0 }); + } + + public get uri(): Uri { + return this._uri; + } + public get fileName(): string { + return this._uri.fsPath; + } + + public get isUntitled(): boolean { + return true; + } + + public get isReadOnly(): boolean { + return !this.inEditMode; + } + public get languageId(): string { + return PYTHON_LANGUAGE; + } + public get version(): number { + return this._version; + } + public get isDirty(): boolean { + return true; + } + public get isClosed(): boolean { + return false; + } + public save(): Thenable { + return Promise.resolve(true); + } + public get eol(): EndOfLine { + return EndOfLine.LF; + } + public get lineCount(): number { + return this._lines.length; + } + + public lineAt(position: Position | number): TextLine { + if (typeof position === 'number') { + return this._lines[position as number]; + } else { + return this._lines[position.line]; + } + } + public offsetAt(position: Position): number { + return this.convertToOffset(position); + } + public positionAt(offset: number): Position { + let line = 0; + let ch = 0; + while (line + 1 < this._lines.length && this._lines[line + 1].offset <= offset) { + line += 1; + } + if (line < this._lines.length) { + ch = offset - this._lines[line].offset; + } + return new Position(line, ch); + } + public getText(range?: Range | undefined): string { + if (!range) { + return this._contents; + } else { + const startOffset = this.convertToOffset(range.start); + const endOffset = this.convertToOffset(range.end); + return this._contents.substr(startOffset, endOffset - startOffset); + } + } + + public getFullContentChanges(): TextDocumentContentChangeEvent[] { + return [ + { + range: this.createSerializableRange(new Position(0, 0), new Position(0, 0)), + rangeOffset: 0, + rangeLength: 0, // Adds are always zero + text: this._contents + } + ]; + } + + public getWordRangeAtPosition(position: Position, regexp?: RegExp | undefined): Range | undefined { + if (!regexp) { + // use default when custom-regexp isn't provided + regexp = DefaultWordPattern; + } else if (regExpLeadsToEndlessLoop(regexp)) { + // use default when custom-regexp is bad + console.warn( + `[getWordRangeAtPosition]: ignoring custom regexp '${regexp.source}' because it matches the empty string.` + ); + regexp = DefaultWordPattern; + } + + const wordAtText = getWordAtText( + position.character + 1, + ensureValidWordDefinition(regexp), + this._lines[position.line].text, + 0 + ); + + if (wordAtText) { + return new Range(position.line, wordAtText.startColumn - 1, position.line, wordAtText.endColumn - 1); + } + return undefined; + } + public validateRange(range: Range): Range { + return range; + } + public validatePosition(position: Position): Position { + return position; + } + + public get textDocumentItem(): vscodeLanguageClient.TextDocumentItem { + return { + uri: this._uri.toString(), + languageId: this.languageId, + version: this.version, + text: this.getText() + }; + } + + public get textDocumentId(): vscodeLanguageClient.VersionedTextDocumentIdentifier { + return { + uri: this._uri.toString(), + version: this.version + }; + } + + public loadAllCells( + cells: { code: string; id: string }[], + notebookType: 'interactive' | 'native' + ): TextDocumentContentChangeEvent[] { + if (!this.inEditMode && notebookType === 'native') { + this.inEditMode = true; + return this.reloadCells(cells); + } + return []; + } + + public reloadCells(cells: { code: string; id: string }[]): TextDocumentContentChangeEvent[] { + this._version += 1; + + // Normalize all of the cells, removing \r and separating each + // with a newline + const normalized = cells.map((c) => { + return { + id: c.id, + code: `${c.code.replace(/\r/g, '')}\n` + }; + }); + + // Contents are easy, just load all of the code in a row + this._contents = + normalized && normalized.length + ? normalized + .map((c) => c.code) + .reduce((p, c) => { + return `${p}${c}`; + }) + : ''; + + // Cell ranges are slightly more complicated + let prev: number = 0; + this._cellRanges = normalized.map((c) => { + const result = { + id: c.id, + start: prev, + fullEnd: prev + c.code.length, + currentEnd: prev + c.code.length + }; + prev += c.code.length; + return result; + }); + + // Then create the lines. + this._lines = this.createLines(); + + // Return our changes + return [ + { + range: this.createSerializableRange(new Position(0, 0), new Position(0, 0)), + rangeOffset: 0, + rangeLength: 0, // Adds are always zero + text: this._contents + } + ]; + } + + public addCell(fullCode: string, currentCode: string, id: string): TextDocumentContentChangeEvent[] { + // This should only happen once for each cell. + this._version += 1; + + // Get rid of windows line endings. We're normalizing on linux + const normalized = fullCode.replace(/\r/g, ''); + const normalizedCurrent = currentCode.replace(/\r/g, ''); + + // This item should go just before the edit cell + + // Make sure to put a newline between this code and the next code + const newCode = `${normalized}\n`; + const newCurrentCode = `${normalizedCurrent}\n`; + + // We should start just before the last cell. + const fromOffset = this.getEditCellOffset(); + + // Split our text between the edit text and the cells above + const before = this._contents.substr(0, fromOffset); + const after = this._contents.substr(fromOffset); + const fromPosition = this.positionAt(fromOffset); + + // Save the range for this cell () + this._cellRanges.splice(this._cellRanges.length - 1, 0, { + id, + start: fromOffset, + fullEnd: fromOffset + newCode.length, + currentEnd: fromOffset + newCurrentCode.length + }); + + // Update our entire contents and recompute our lines + this._contents = `${before}${newCode}${after}`; + this._lines = this.createLines(); + this._cellRanges[this._cellRanges.length - 1].start += newCode.length; + this._cellRanges[this._cellRanges.length - 1].fullEnd += newCode.length; + this._cellRanges[this._cellRanges.length - 1].currentEnd += newCode.length; + + return [ + { + range: this.createSerializableRange(fromPosition, fromPosition), + rangeOffset: fromOffset, + rangeLength: 0, // Adds are always zero + text: newCode + } + ]; + } + + public reloadCell(id: string, code: string): TextDocumentContentChangeEvent[] { + this._version += 1; + + // Make sure to put a newline between this code and the next code + const newCode = `${code.replace(/\r/g, '')}\n`; + + // Figure where this goes + const index = this._cellRanges.findIndex((r) => r.id === id); + if (index >= 0) { + const start = this.positionAt(this._cellRanges[index].start); + const end = this.positionAt(this._cellRanges[index].currentEnd); + return this.removeRange(newCode, start, end, index); + } + + return []; + } + + public insertCell( + id: string, + code: string, + codeCellAboveOrIndex: string | undefined | number + ): TextDocumentContentChangeEvent[] { + // This should only happen once for each cell. + this._version += 1; + + // Make sure to put a newline between this code and the next code + const newCode = `${code.replace(/\r/g, '')}\n`; + + // Figure where this goes + const aboveIndex = this._cellRanges.findIndex((r) => r.id === codeCellAboveOrIndex); + const insertIndex = typeof codeCellAboveOrIndex === 'number' ? codeCellAboveOrIndex : aboveIndex + 1; + + // Compute where we start from. + const fromOffset = + insertIndex < this._cellRanges.length ? this._cellRanges[insertIndex].start : this._contents.length; + + // Split our text between the text and the cells above + const before = this._contents.substr(0, fromOffset); + const after = this._contents.substr(fromOffset); + const fromPosition = this.positionAt(fromOffset); + + // Update our entire contents and recompute our lines + this._contents = `${before}${newCode}${after}`; + this._lines = this.createLines(); + + // Move all the other cell ranges down + for (let i = insertIndex; i <= this._cellRanges.length - 1; i += 1) { + this._cellRanges[i].start += newCode.length; + this._cellRanges[i].fullEnd += newCode.length; + this._cellRanges[i].currentEnd += newCode.length; + } + this._cellRanges.splice(insertIndex, 0, { + id, + start: fromOffset, + fullEnd: fromOffset + newCode.length, + currentEnd: fromOffset + newCode.length + }); + + return [ + { + range: this.createSerializableRange(fromPosition, fromPosition), + rangeOffset: fromOffset, + rangeLength: 0, // Adds are always zero + text: newCode + } + ]; + } + + public removeAllCells(): TextDocumentContentChangeEvent[] { + // Remove everything + if (this.inEditMode) { + this._version += 1; + + // Compute the offset for the edit cell + const toOffset = this._cellRanges.length > 0 ? this._cellRanges[this._cellRanges.length - 1].fullEnd : 0; + const from = this.positionAt(0); + const to = this.positionAt(toOffset); + + // Remove the entire range. + const result = this.removeRange('', from, to, 0); + + // Update our cell range + this._cellRanges = []; + + return result; + } + + return []; + } + + public editCell(editorChanges: IEditorContentChange[], id: string): TextDocumentContentChangeEvent[] { + this._version += 1; + + // Convert the range to local (and remove 1 based) + if (editorChanges && editorChanges.length) { + const normalized = editorChanges[0].text.replace(/\r/g, ''); + + // Figure out which cell we're editing. + const cellIndex = this._cellRanges.findIndex((c) => c.id === id); + if (cellIndex >= 0 && (id === Identifiers.EditCellId || this.inEditMode)) { + // This is an actual edit. + // Line/column are within this cell. Use its offset to compute the real position + const editPos = this.positionAt(this._cellRanges[cellIndex].start); + const from = new Position( + editPos.line + editorChanges[0].range.startLineNumber - 1, + editorChanges[0].range.startColumn - 1 + ); + const to = new Position( + editPos.line + editorChanges[0].range.endLineNumber - 1, + editorChanges[0].range.endColumn - 1 + ); + + // Remove this range from the contents and return the change. + return this.removeRange(normalized, from, to, cellIndex); + } else if (cellIndex >= 0) { + // This is an edit of a read only cell. Just replace our currentEnd position + const newCode = `${normalized}\n`; + this._cellRanges[cellIndex].currentEnd = this._cellRanges[cellIndex].start + newCode.length; + } + } + + return []; + } + + public remove(id: string): TextDocumentContentChangeEvent[] { + let change: TextDocumentContentChangeEvent[] = []; + + const index = this._cellRanges.findIndex((c) => c.id === id); + // Ignore unless in edit mode. For non edit mode, cells are still there. + if (index >= 0 && this.inEditMode) { + this._version += 1; + + const found = this._cellRanges[index]; + const foundLength = found.currentEnd - found.start; + const from = new Position(this.getLineFromOffset(found.start), 0); + const to = this.positionAt(found.currentEnd); + + // Remove from the cell ranges. + for (let i = index + 1; i <= this._cellRanges.length - 1; i += 1) { + this._cellRanges[i].start -= foundLength; + this._cellRanges[i].fullEnd -= foundLength; + this._cellRanges[i].currentEnd -= foundLength; + } + this._cellRanges.splice(index, 1); + + // Recreate the contents + const before = this._contents.substr(0, found.start); + const after = this._contents.substr(found.currentEnd); + this._contents = `${before}${after}`; + this._lines = this.createLines(); + + change = [ + { + range: this.createSerializableRange(from, to), + rangeOffset: found.start, + rangeLength: foundLength, + text: '' + } + ]; + } + + return change; + } + + public swap(first: string, second: string): TextDocumentContentChangeEvent[] { + let change: TextDocumentContentChangeEvent[] = []; + + const firstIndex = this._cellRanges.findIndex((c) => c.id === first); + const secondIndex = this._cellRanges.findIndex((c) => c.id === second); + if (firstIndex >= 0 && secondIndex >= 0 && firstIndex !== secondIndex && this.inEditMode) { + this._version += 1; + + const topIndex = firstIndex < secondIndex ? firstIndex : secondIndex; + const bottomIndex = firstIndex > secondIndex ? firstIndex : secondIndex; + const top = { ...this._cellRanges[topIndex] }; + const bottom = { ...this._cellRanges[bottomIndex] }; + + const from = new Position(this.getLineFromOffset(top.start), 0); + const to = this.positionAt(bottom.currentEnd); + + // Swap everything + this._cellRanges[topIndex].id = bottom.id; + this._cellRanges[topIndex].fullEnd = top.start + (bottom.fullEnd - bottom.start); + this._cellRanges[topIndex].currentEnd = top.start + (bottom.currentEnd - bottom.start); + this._cellRanges[bottomIndex].id = top.id; + this._cellRanges[bottomIndex].start = this._cellRanges[topIndex].fullEnd; + this._cellRanges[bottomIndex].fullEnd = this._cellRanges[topIndex].fullEnd + (top.fullEnd - top.start); + this._cellRanges[bottomIndex].currentEnd = + this._cellRanges[topIndex].fullEnd + (top.currentEnd - top.start); + + const fromOffset = this.convertToOffset(from); + const toOffset = this.convertToOffset(to); + + // Recreate our contents, and then recompute all of our lines + const before = this._contents.substr(0, fromOffset); + const topText = this._contents.substr(top.start, top.fullEnd - top.start); + const bottomText = this._contents.substr(bottom.start, bottom.fullEnd - bottom.start); + const after = this._contents.substr(toOffset); + const replacement = `${bottomText}${topText}`; + this._contents = `${before}${replacement}${after}`; + this._lines = this.createLines(); + + // Change is a full replacement + change = [ + { + range: this.createSerializableRange(from, to), + rangeOffset: fromOffset, + rangeLength: toOffset - fromOffset, + text: replacement + } + ]; + } + + return change; + } + + public removeAll(): TextDocumentContentChangeEvent[] { + let change: TextDocumentContentChangeEvent[] = []; + // Ignore unless in edit mode. + if (this._lines.length > 0 && this.inEditMode) { + this._version += 1; + + const from = this._lines[0].range.start; + const to = this._lines[this._lines.length - 1].rangeIncludingLineBreak.end; + const length = this._contents.length; + this._cellRanges = []; + this._contents = ''; + this._lines = []; + + change = [ + { + range: this.createSerializableRange(from, to), + rangeOffset: 0, + rangeLength: length, + text: '' + } + ]; + } + + return change; + } + + public convertToDocumentPosition(id: string, line: number, ch: number): Position { + // Monaco is 1 based, and we need to add in our cell offset. + const cellIndex = this._cellRanges.findIndex((c) => c.id === id); + if (cellIndex >= 0) { + // Line/column are within this cell. Use its offset to compute the real position + const editLine = this.positionAt(this._cellRanges[cellIndex].start); + const docLine = line - 1 + editLine.line; + const docCh = ch - 1; + return new Position(docLine, docCh); + } + + // We can't find a cell that matches. Just remove the 1 based + return new Position(line - 1, ch - 1); + } + + public getCellData(cellId: string) { + const range = this._cellRanges.find((cellRange) => cellRange.id === cellId); + if (range) { + return { + offset: range.start, + text: this._contents.substring(range.start, range.currentEnd) + }; + } + } + + public getEditCellContent() { + return this._contents.substr(this.getEditCellOffset()); + } + + public getEditCellOffset(cellId?: string) { + // in native editor + if (this.inEditMode && cellId) { + const cell = this._cellRanges.find((c) => c.id === cellId); + + if (cell) { + return cell.start; + } + } + + // in interactive window + return this._cellRanges && this._cellRanges.length > 0 + ? this._cellRanges[this._cellRanges.length - 1].start + : 0; + } + + private getLineFromOffset(offset: number) { + let lineCounter = 0; + + for (let i = 0; i < offset; i += 1) { + if (this._contents[i] === '\n') { + lineCounter += 1; + } + } + + return lineCounter; + } + + private removeRange( + newText: string, + from: Position, + to: Position, + cellIndex: number + ): TextDocumentContentChangeEvent[] { + const fromOffset = this.convertToOffset(from); + const toOffset = this.convertToOffset(to); + + // Recreate our contents, and then recompute all of our lines + const before = this._contents.substr(0, fromOffset); + const after = this._contents.substr(toOffset); + this._contents = `${before}${newText}${after}`; + this._lines = this.createLines(); + + // Update ranges after this. All should move by the diff in length, although the current one + // should stay at the same start point. + const lengthDiff = newText.length - (toOffset - fromOffset); + for (let i = cellIndex; i < this._cellRanges.length; i += 1) { + if (i !== cellIndex) { + this._cellRanges[i].start += lengthDiff; + } + this._cellRanges[i].fullEnd += lengthDiff; + this._cellRanges[i].currentEnd += lengthDiff; + } + + return [ + { + range: this.createSerializableRange(from, to), + rangeOffset: fromOffset, + rangeLength: toOffset - fromOffset, + text: newText + } + ]; + } + + private createLines(): IntellisenseLine[] { + const split = this._contents.splitLines({ trim: false, removeEmptyEntries: false }); + let prevLine: IntellisenseLine | undefined; + return split.map((s, i) => { + const nextLine = this.createTextLine(s, i, prevLine); + prevLine = nextLine; + return nextLine; + }); + } + + private createTextLine(line: string, index: number, prevLine: IntellisenseLine | undefined): IntellisenseLine { + return new IntellisenseLine( + line, + index, + prevLine ? prevLine.offset + prevLine.rangeIncludingLineBreak.end.character : 0 + ); + } + + private convertToOffset(pos: Position): number { + if (pos.line < this._lines.length) { + return this._lines[pos.line].offset + pos.character; + } + return this._contents.length; + } + + private createSerializableRange(start: Position, end: Position): Range { + // This funciton is necessary so that the Range can be passed back + // over a remote connection without including all of the extra fields that + // VS code puts into a Range object. + const result = { + start: { + line: start.line, + character: start.character + }, + end: { + line: end.line, + character: end.character + } + }; + return result as Range; + } +} diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseLine.ts b/src/client/datascience/interactive-common/intellisense/intellisenseLine.ts new file mode 100644 index 000000000000..54dc5a7e5c2e --- /dev/null +++ b/src/client/datascience/interactive-common/intellisense/intellisenseLine.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../../common/extensions'; + +import { Position, Range, TextLine } from 'vscode'; + +export class IntellisenseLine implements TextLine { + private _range: Range; + private _rangeWithLineBreak: Range; + private _firstNonWhitespaceIndex: number | undefined; + private _isEmpty: boolean | undefined; + + constructor(private _contents: string, private _line: number, private _offset: number) { + this._range = new Range(new Position(_line, 0), new Position(_line, _contents.length)); + this._rangeWithLineBreak = new Range(this.range.start, new Position(_line, _contents.length + 1)); + } + + public get offset(): number { + return this._offset; + } + public get lineNumber(): number { + return this._line; + } + public get text(): string { + return this._contents; + } + public get range(): Range { + return this._range; + } + public get rangeIncludingLineBreak(): Range { + return this._rangeWithLineBreak; + } + public get firstNonWhitespaceCharacterIndex(): number { + if (this._firstNonWhitespaceIndex === undefined) { + this._firstNonWhitespaceIndex = this._contents.trimLeft().length - this._contents.length; + } + return this._firstNonWhitespaceIndex; + } + public get isEmptyOrWhitespace(): boolean { + if (this._isEmpty === undefined) { + this._isEmpty = this._contents.length === 0 || this._contents.trim().length === 0; + } + return this._isEmpty; + } +} diff --git a/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts new file mode 100644 index 000000000000..64909c7e06a5 --- /dev/null +++ b/src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts @@ -0,0 +1,948 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../../common/extensions'; + +import { inject, injectable, named } from 'inversify'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { + CancellationTokenSource, + CompletionItem, + Event, + EventEmitter, + Hover, + MarkdownString, + SignatureHelpContext, + SignatureInformation, + TextDocumentContentChangeEvent, + Uri +} from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import * as vscodeLanguageClient from 'vscode-languageclient/node'; +import { concatMultilineString } from '../../../../datascience-ui/common'; +import { ILanguageServer, ILanguageServerCache } from '../../../activation/types'; +import { IWorkspaceService } from '../../../common/application/types'; +import { CancellationError } from '../../../common/cancellation'; +import { traceError, traceWarning } from '../../../common/logger'; +import { TemporaryFile } from '../../../common/platform/types'; +import { Resource } from '../../../common/types'; +import { createDeferred, Deferred, sleep, waitForPromise } from '../../../common/utils/async'; +import { noop } from '../../../common/utils/misc'; +import { HiddenFileFormatString } from '../../../constants'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { sendTelemetryWhenDone } from '../../../telemetry'; +import { Identifiers, Settings, Telemetry } from '../../constants'; +import { + ICell, + IDataScienceFileSystem, + IInteractiveWindowListener, + IJupyterVariables, + INotebook, + INotebookCompletion, + INotebookProvider +} from '../../types'; +import { + ICancelIntellisenseRequest, + IInteractiveWindowMapping, + ILoadAllCells, + INotebookIdentity, + InteractiveWindowMessages, + IProvideCompletionItemsRequest, + IProvideHoverRequest, + IProvideSignatureHelpRequest, + IResolveCompletionItemRequest, + NotebookModelChange +} from '../interactiveWindowTypes'; +import { + convertStringsToSuggestions, + convertToMonacoCompletionItem, + convertToMonacoCompletionList, + convertToMonacoHover, + convertToMonacoSignatureHelp, + convertToVSCodeCompletionItem +} from './conversion'; +import { IntellisenseDocument } from './intellisenseDocument'; + +// These regexes are used to get the text from jupyter output by recognizing escape charactor \x1b +const DocStringRegex = /\x1b\[1;31mDocstring:\x1b\[0m\s+([\s\S]*?)\r?\n\x1b\[1;31m/; +const SignatureTextRegex = /\x1b\[1;31mSignature:\x1b\[0m\s+([\s\S]*?)\r?\n\x1b\[1;31m/; +const TypeRegex = /\x1b\[1;31mType:\x1b\[0m\s+(.*)/; + +// This regex is to parse the name and the signature in the signature text from Jupyter, +// Example string: some_func(param1=1, param2=2) -> int +// match[1]: some_func +// match[2]: (param1=1, param2=2) -> int +const SignatureRegex = /(.+?)(\(([\s\S]*)\)(\s*->[\s\S]*)?)/; +const GeneralCallableSignature = '(*args, **kwargs)'; +// This regex is to detect whether a markdown provided by the language server is a callable and get its signature. +// Example string: ```python\n(function) some_func: (*args, **kwargs) -> None\n``` +// match[1]: (*args, **kwargs) +// If the string is not a callable, no match will be found. +// Example string: ```python\n(variable) some_var: Any\n``` +const CallableRegex = /python\n\(.+?\) \S+?: (\([\s\S]+?\))/; + +// tslint:disable:no-any +@injectable() +export class IntellisenseProvider implements IInteractiveWindowListener { + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + private documentPromise: Deferred | undefined; + private temporaryFile: TemporaryFile | undefined; + private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ + message: string; + payload: any; + }>(); + private cancellationSources: Map = new Map(); + private notebookIdentity: Uri | undefined; + private notebookType: 'interactive' | 'native' = 'interactive'; + private potentialResource: Uri | undefined; + private sentOpenDocument: boolean = false; + private languageServer: ILanguageServer | undefined; + private resource: Resource; + private interpreter: PythonEnvironment | undefined; + + constructor( + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(INotebookProvider) private notebookProvider: INotebookProvider, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(ILanguageServerCache) private languageServerCache: ILanguageServerCache, + @inject(IJupyterVariables) @named(Identifiers.ALL_VARIABLES) private variableProvider: IJupyterVariables + ) {} + + public dispose() { + if (this.temporaryFile) { + this.temporaryFile.dispose(); + } + if (this.languageServer) { + this.languageServer.dispose(); + this.languageServer = undefined; + } + } + + public onMessage(message: string, payload?: any) { + switch (message) { + case InteractiveWindowMessages.CancelCompletionItemsRequest: + case InteractiveWindowMessages.CancelHoverRequest: + this.dispatchMessage(message, payload, this.handleCancel); + break; + + case InteractiveWindowMessages.ProvideCompletionItemsRequest: + this.dispatchMessage(message, payload, this.handleCompletionItemsRequest); + break; + + case InteractiveWindowMessages.ProvideHoverRequest: + this.dispatchMessage(message, payload, this.handleHoverRequest); + break; + + case InteractiveWindowMessages.ProvideSignatureHelpRequest: + this.dispatchMessage(message, payload, this.handleSignatureHelpRequest); + break; + + case InteractiveWindowMessages.ResolveCompletionItemRequest: + this.dispatchMessage(message, payload, this.handleResolveCompletionItemRequest); + break; + + case InteractiveWindowMessages.UpdateModel: + this.dispatchMessage(message, payload, this.update); + break; + + case InteractiveWindowMessages.RestartKernel: + this.dispatchMessage(message, payload, this.restartKernel); + break; + + case InteractiveWindowMessages.NotebookIdentity: + this.dispatchMessage(message, payload, this.setIdentity); + break; + + case InteractiveWindowMessages.NotebookExecutionActivated: + this.dispatchMessage(message, payload, this.updateIdentity); + break; + + case InteractiveWindowMessages.LoadAllCellsComplete: + this.dispatchMessage(message, payload, this.loadAllCells); + break; + + default: + break; + } + } + + public getDocument(resource?: Uri): Promise { + if (!this.documentPromise) { + this.documentPromise = createDeferred(); + + // Create our dummy document. Compute a file path for it. + if (this.workspaceService.rootPath || resource) { + const dir = resource ? path.dirname(resource.fsPath) : this.workspaceService.rootPath!; + const dummyFilePath = path.join(dir, HiddenFileFormatString.format(uuid().replace(/-/g, ''))); + this.documentPromise.resolve(new IntellisenseDocument(dummyFilePath)); + } else { + this.fs + .createTemporaryLocalFile('.py') + .then((t) => { + this.temporaryFile = t; + const dummyFilePath = this.temporaryFile.filePath; + this.documentPromise!.resolve(new IntellisenseDocument(dummyFilePath)); + }) + .catch((e) => { + this.documentPromise!.reject(e); + }); + } + } + + return this.documentPromise.promise; + } + + protected async getLanguageServer(token: CancellationToken): Promise { + // Resource should be our potential resource if its set. Otherwise workspace root + const resource = + this.potentialResource || + (this.workspaceService.rootPath ? Uri.parse(this.workspaceService.rootPath) : undefined); + + // Interpreter should be the interpreter currently active in the notebook + const activeNotebook = await this.getNotebook(token); + const interpreter = activeNotebook + ? activeNotebook.getMatchingInterpreter() + : await this.interpreterService.getActiveInterpreter(resource); + + const newPath = resource; + const oldPath = this.resource; + + // See if the resource or the interpreter are different + if ( + (newPath && !oldPath) || + (newPath && oldPath && !this.fs.arePathsSame(newPath, oldPath)) || + interpreter?.path !== this.interpreter?.path || + this.languageServer === undefined + ) { + this.resource = resource; + this.interpreter = interpreter; + + // Get an instance of the language server (so we ref count it ) + try { + const languageServer = await this.languageServerCache.get(resource, interpreter); + + // Dispose of our old language service + this.languageServer?.dispose(); + + // This new language server does not know about our document, so tell it. + const document = await this.getDocument(); + if (document && languageServer.handleOpen && languageServer.handleChanges) { + // If we already sent an open document, that means we need to send both the open and + // the new changes + if (this.sentOpenDocument) { + languageServer.handleOpen(document); + languageServer.handleChanges(document, document.getFullContentChanges()); + } else { + this.sentOpenDocument = true; + languageServer.handleOpen(document); + } + } + + // Save the ref. + this.languageServer = languageServer; + } catch (e) { + traceError(e); + } + } + return this.languageServer; + } + + protected async provideCompletionItems( + position: monacoEditor.Position, + context: monacoEditor.languages.CompletionContext, + cellId: string, + token: CancellationToken + ): Promise { + const [languageServer, document] = await Promise.all([this.getLanguageServer(token), this.getDocument()]); + if (languageServer && document) { + const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); + const result = await languageServer.provideCompletionItems(document, docPos, token, context); + if (result) { + return convertToMonacoCompletionList(result, true); + } + } + + return { + suggestions: [], + incomplete: false + }; + } + + protected async provideHover( + position: monacoEditor.Position, + wordAtPosition: string | undefined, + cellId: string, + token: CancellationToken + ): Promise { + const [languageServer, document, variableHover] = await Promise.all([ + this.getLanguageServer(token), + this.getDocument(), + this.getVariableHover(wordAtPosition, token) + ]); + if (!variableHover && languageServer && document) { + const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); + const [lsResult, jupyterResult] = await Promise.all([ + languageServer.provideHover(document, docPos, token), + Promise.race([ + this.provideJupyterHover(position, cellId, token), + sleep(Settings.IntellisenseTimeout).then(() => undefined) + ]) + ]); + const jupyterHover = jupyterResult ? convertToMonacoHover(jupyterResult) : undefined; + const lsHover = lsResult ? convertToMonacoHover(lsResult) : undefined; + // If lsHover is not valid or it is not a callable with hints, + // while the jupyter hover is a callable with hint, + // we prefer to use jupyterHover which provides better callable hints from jupyter kernel. + const preferJupyterHover = + jupyterHover && + jupyterHover.contents[0] && + this.isCallableWithGoodHint(jupyterHover.contents[0].value) && + (!lsHover || !lsHover.contents[0] || !this.isCallableWithGoodHint(lsHover.contents[0].value)); + if (preferJupyterHover && jupyterHover) { + return jupyterHover; + } else if (lsHover) { + return lsHover; + } + } else if (variableHover) { + return convertToMonacoHover(variableHover); + } + + return { + contents: [] + }; + } + + protected async provideSignatureHelp( + position: monacoEditor.Position, + context: monacoEditor.languages.SignatureHelpContext, + cellId: string, + token: CancellationToken + ): Promise { + const [languageServer, document] = await Promise.all([this.getLanguageServer(token), this.getDocument()]); + if (languageServer && document) { + const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); + const result = await languageServer.provideSignatureHelp( + document, + docPos, + token, + context as SignatureHelpContext + ); + if (result) { + return convertToMonacoSignatureHelp(result); + } + } + + return { + signatures: [], + activeParameter: 0, + activeSignature: 0 + }; + } + + protected async resolveCompletionItem( + position: monacoEditor.Position, + item: monacoEditor.languages.CompletionItem, + cellId: string, + token: CancellationToken + ): Promise { + const [languageServer, document] = await Promise.all([this.getLanguageServer(token), this.getDocument()]); + if (languageServer && languageServer.resolveCompletionItem && document) { + const vscodeCompItem: CompletionItem = convertToVSCodeCompletionItem(item); + + // Needed by Jedi in completionSource.ts to resolve the item + const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); + (vscodeCompItem as any)._documentPosition = { document, position: docPos }; + + const result = await languageServer.resolveCompletionItem(vscodeCompItem, token); + if (result) { + // Convert expects vclc completion item, but takes both vclc and vscode items so just cast here + return convertToMonacoCompletionItem(result as vscodeLanguageClient.CompletionItem, true); + } + } + + // If we can't fill in the extra info, just return the item + return item; + } + + protected async handleChanges( + document: IntellisenseDocument, + changes: TextDocumentContentChangeEvent[] + ): Promise { + // For the dot net language server, we have to send extra data to the language server + if (document) { + // Broadcast an update to the language server + const languageServer = await this.getLanguageServer(CancellationToken.None); + if (languageServer && languageServer.handleChanges && languageServer.handleOpen) { + if (!this.sentOpenDocument) { + this.sentOpenDocument = true; + return languageServer.handleOpen(document); + } else { + return languageServer.handleChanges(document, changes); + } + } + } + } + + private isCallableWithGoodHint(markdown: string): boolean { + // Check whether the markdown is a callable with the hint that is not (*args, **kwargs) + const match = CallableRegex.exec(markdown); + return match !== null && match[1] !== GeneralCallableSignature; + } + + private convertDocMarkDown(doc: string): string { + // For the argument definitions (Starts with :param/:type/:return), to make markdown works well, we need to: + // 1. Add one more line break; + // 2. Replace '_' with '\_'; + const docLines = doc.splitLines({ trim: false, removeEmptyEntries: false }); + return docLines.map((line) => (line.startsWith(':') ? `\n${line.replace(/_/g, '\\_')}` : line)).join('\n'); + } + + private async provideJupyterHover( + position: monacoEditor.Position, + cellId: string, + token: CancellationToken + ): Promise { + // Currently we only get the callable information from jupyter, + // this aims to handle the case that language server cannot well recognize the dynamically created callables. + const callable = await this.getJupyterCallableInspectResult(position, cellId, token); + if (callable) { + const signatureMarkdown = `\`\`\`python\n(${callable.type}) ${callable.name}: ${callable.signature}\n\`\`\``; + const docMarkdown = this.convertDocMarkDown(callable.doc); + const result = new MarkdownString(`${signatureMarkdown}\n\n${docMarkdown}`); + return { contents: [result] }; + } + return undefined; + } + + private dispatchMessage( + _message: T, + payload: any, + handler: (args: M[T]) => void + ) { + const args = payload as M[T]; + handler.bind(this)(args); + } + + private postResponse(type: T, payload?: M[T]): void { + const response = payload as any; + if (response && response.id) { + const cancelSource = this.cancellationSources.get(response.id); + if (cancelSource) { + cancelSource.dispose(); + this.cancellationSources.delete(response.id); + } + } + this.postEmitter.fire({ message: type.toString(), payload }); + } + + private handleCancel(request: ICancelIntellisenseRequest) { + const cancelSource = this.cancellationSources.get(request.requestId); + if (cancelSource) { + cancelSource.cancel(); + cancelSource.dispose(); + this.cancellationSources.delete(request.requestId); + } + } + + private handleCompletionItemsRequest(request: IProvideCompletionItemsRequest) { + // Create a cancellation source. We'll use this for our sub class request and a jupyter one + const cancelSource = new CancellationTokenSource(); + this.cancellationSources.set(request.requestId, cancelSource); + + const getCompletions = async (): Promise => { + const emptyList: monacoEditor.languages.CompletionList = { + dispose: noop, + incomplete: false, + suggestions: [] + }; + + const lsCompletions = this.provideCompletionItems( + request.position, + request.context, + request.cellId, + cancelSource.token + ); + + const jupyterCompletions = this.provideJupyterCompletionItems( + request.position, + request.context, + request.cellId, + cancelSource.token + ); + + // Capture telemetry for each of the two providers. + // Telemetry will be used to improve how we handle intellisense to improve response times for code completion. + // NOTE: If this code is around after a few months, telemetry isn't used, or we don't need it anymore. + // I.e. delete this code. + sendTelemetryWhenDone(Telemetry.CompletionTimeFromLS, lsCompletions); + sendTelemetryWhenDone(Telemetry.CompletionTimeFromJupyter, jupyterCompletions); + + return this.combineCompletions( + await Promise.all([ + // Ensure we wait for a result from language server (assumption is LS is faster). + // Telemetry will prove/disprove this assumption and we'll change this code accordingly. + lsCompletions, + // Wait for a max of n ms before ignoring results from jupyter (jupyter completion is generally slower). + Promise.race([jupyterCompletions, sleep(Settings.IntellisenseTimeout).then(() => emptyList)]) + ]) + ); + }; + + // Combine all of the results together. + this.postTimedResponse([getCompletions()], InteractiveWindowMessages.ProvideCompletionItemsResponse, (c) => { + const list = this.combineCompletions(c); + return { list, requestId: request.requestId }; + }); + } + + private handleResolveCompletionItemRequest(request: IResolveCompletionItemRequest) { + // Create a cancellation source. We'll use this for our sub class request and a jupyter one + const cancelSource = new CancellationTokenSource(); + this.cancellationSources.set(request.requestId, cancelSource); + + // Combine all of the results together. + this.postTimedResponse( + [this.resolveCompletionItem(request.position, request.item, request.cellId, cancelSource.token)], + InteractiveWindowMessages.ResolveCompletionItemResponse, + (c) => { + if (c && c[0]) { + return { item: c[0], requestId: request.requestId }; + } else { + return { item: request.item, requestId: request.requestId }; + } + } + ); + } + + private handleHoverRequest(request: IProvideHoverRequest) { + const cancelSource = new CancellationTokenSource(); + this.cancellationSources.set(request.requestId, cancelSource); + this.postTimedResponse( + [this.provideHover(request.position, request.wordAtPosition, request.cellId, cancelSource.token)], + InteractiveWindowMessages.ProvideHoverResponse, + (h) => { + if (h && h[0]) { + return { hover: h[0]!, requestId: request.requestId }; + } else { + return { hover: { contents: [] }, requestId: request.requestId }; + } + } + ); + } + + private convertCallableInspectResult(text: string) { + // This method will parse the inspect result from jupyter and get the following values of a callable: + // Name, Type (function or method), Signature, Documentation + + const docMatch = DocStringRegex.exec(text); + // Variable type will be used in hover result, it could be function/method + const typeMatch = TypeRegex.exec(text); + + const signatureTextMatch = SignatureTextRegex.exec(text); + // The signature text returned by jupyter contains escape sequences, we need to remove them. + // See https://en.wikipedia.org/wiki/ANSI_escape_code#Escape_sequences + const signatureText = signatureTextMatch ? signatureTextMatch[1].replace(/\x1b\[[;\d]+m/g, '') : ''; + // Use this to get different parts of the signature: 1: Callable name, 2: Callable signature + const signatureMatch = SignatureRegex.exec(signatureText); + + if (docMatch && typeMatch && signatureMatch) { + return { + name: signatureMatch[1], + type: typeMatch[1], + signature: signatureMatch[2], + doc: docMatch[1] + }; + } + return undefined; + } + + private async getJupyterCallableInspectResult( + position: monacoEditor.Position, + cellId: string, + cancelToken: CancellationToken + ) { + try { + const [activeNotebook, document] = await Promise.all([this.getNotebook(cancelToken), this.getDocument()]); + if (activeNotebook && document) { + const data = document.getCellData(cellId); + if (data) { + const offsetInCode = this.getOffsetInCode(data.text, position); + const jupyterResults = await activeNotebook.inspect(data.text, offsetInCode, cancelToken); + if (jupyterResults && jupyterResults.hasOwnProperty('text/plain')) { + return this.convertCallableInspectResult((jupyterResults as any)['text/plain'].toString()); + } + } + } + } catch (e) { + if (!(e instanceof CancellationError)) { + traceWarning(e); + } + } + return undefined; + } + + private async provideJupyterSignatureHelp( + position: monacoEditor.Position, + cellId: string, + cancelToken: CancellationToken + ): Promise { + const callable = await this.getJupyterCallableInspectResult(position, cellId, cancelToken); + let signatures: SignatureInformation[] = []; + if (callable) { + const signatureInfo: SignatureInformation = { + label: callable.signature, + documentation: callable.doc, + parameters: [] + }; + signatures = [signatureInfo]; + } + return { + signatures: signatures, + activeParameter: 0, + activeSignature: 0 + }; + } + + private getOffsetInCode(text: string, position: monacoEditor.Position) { + const lines = text.splitLines({ trim: false, removeEmptyEntries: false }); + return lines.reduce((a: number, c: string, i: number) => { + if (i < position.lineNumber - 1) { + return a + c.length + 1; + } else if (i === position.lineNumber - 1) { + return a + position.column - 1; + } else { + return a; + } + }, 0); + } + + private async provideJupyterCompletionItems( + position: monacoEditor.Position, + _context: monacoEditor.languages.CompletionContext, + cellId: string, + cancelToken: CancellationToken + ): Promise { + try { + const [activeNotebook, document] = await Promise.all([this.getNotebook(cancelToken), this.getDocument()]); + if (activeNotebook && document) { + const data = document.getCellData(cellId); + + if (data) { + const offsetInCode = this.getOffsetInCode(data.text, position); + const jupyterResults = await activeNotebook.getCompletion(data.text, offsetInCode, cancelToken); + if (jupyterResults && jupyterResults.matches) { + const filteredMatches = this.filterJupyterMatches(document, jupyterResults, cellId, position); + + const baseOffset = data.offset; + const basePosition = document.positionAt(baseOffset); + const startPosition = document.positionAt(jupyterResults.cursor.start + baseOffset); + const endPosition = document.positionAt(jupyterResults.cursor.end + baseOffset); + const range: monacoEditor.IRange = { + startLineNumber: startPosition.line + 1 - basePosition.line, // monaco is 1 based + startColumn: startPosition.character + 1, + endLineNumber: endPosition.line + 1 - basePosition.line, + endColumn: endPosition.character + 1 + }; + return { + suggestions: convertStringsToSuggestions(filteredMatches, range, jupyterResults.metadata), + incomplete: false + }; + } + } + } + } catch (e) { + if (!(e instanceof CancellationError)) { + traceWarning(e); + } + } + + return { + suggestions: [], + incomplete: false + }; + } + + // The suggestions that the kernel is giving always include magic commands. That is confusing to the user. + // This function is called by provideJupyterCompletionItems to filter those magic commands when not in an empty line of code. + private filterJupyterMatches( + document: IntellisenseDocument, + jupyterResults: INotebookCompletion, + cellId: string, + position: monacoEditor.Position + ) { + // If the line we're analyzing is empty or a whitespace, we filter out the magic commands + // as its confusing to see them appear after a . or inside (). + const pos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); + const line = document.lineAt(pos); + return line.isEmptyOrWhitespace + ? jupyterResults.matches + : jupyterResults.matches.filter((match) => !match.startsWith('%')); + } + + private postTimedResponse( + promises: Promise[], + message: T, + formatResponse: (val: (R | null)[]) => M[T] + ) { + // Time all of the promises to make sure they don't take too long. + // Even if LS or Jupyter doesn't complete within e.g. 30s, then we should return an empty response (no point waiting that long). + const timed = promises.map((p) => waitForPromise(p, Settings.MaxIntellisenseTimeout)); + + // Wait for all of of the timings. + const all = Promise.all(timed); + all.then((r) => { + this.postResponse(message, formatResponse(r)); + }).catch((_e) => { + this.postResponse(message, formatResponse([null])); + }); + } + + private combineCompletions( + list: (monacoEditor.languages.CompletionList | null)[] + ): monacoEditor.languages.CompletionList { + // Note to self. We're eliminating duplicates ourselves. The alternative would be to + // have more than one intellisense provider at the monaco editor level and return jupyter + // results independently. Maybe we switch to this when jupyter resides on the react side. + const uniqueSuggestions: Map = new Map< + string, + monacoEditor.languages.CompletionItem + >(); + list.forEach((c) => { + if (c) { + c.suggestions.forEach((s) => { + if (!uniqueSuggestions.has(s.insertText)) { + uniqueSuggestions.set(s.insertText, s); + } + }); + } + }); + + return { + suggestions: Array.from(uniqueSuggestions.values()), + incomplete: false + }; + } + + private handleSignatureHelpRequest(request: IProvideSignatureHelpRequest) { + const cancelSource = new CancellationTokenSource(); + this.cancellationSources.set(request.requestId, cancelSource); + + const getSignatureHelp = async (): Promise => { + const jupyterSignatureHelp = this.provideJupyterSignatureHelp( + request.position, + request.cellId, + cancelSource.token + ); + + const lsSignatureHelp = this.provideSignatureHelp( + request.position, + request.context, + request.cellId, + cancelSource.token + ); + + const defaultHelp = { + signatures: [], + activeParameter: 0, + activeSignature: 0 + }; + + const [lsHelp, jupyterHelp] = await Promise.all([ + lsSignatureHelp, + Promise.race([jupyterSignatureHelp, sleep(Settings.IntellisenseTimeout).then(() => defaultHelp)]) + ]); + // Only when language server result is not valid or the signature is (*args, **kwargs) , we prefer to use the result from jupyter. + const preferJupyterHelp = + (!lsHelp.signatures[0] || lsHelp.signatures[0].label.startsWith(GeneralCallableSignature)) && + jupyterHelp.signatures[0]; + return preferJupyterHelp ? jupyterHelp : lsHelp; + }; + + this.postTimedResponse([getSignatureHelp()], InteractiveWindowMessages.ProvideSignatureHelpResponse, (s) => { + if (s && s[0]) { + return { signatureHelp: s[0]!, requestId: request.requestId }; + } else { + return { + signatureHelp: { signatures: [], activeParameter: 0, activeSignature: 0 }, + requestId: request.requestId + }; + } + }); + } + + private async update(request: NotebookModelChange): Promise { + // See where this request is coming from + switch (request.source) { + case 'redo': + case 'user': + return this.handleRedo(request); + case 'undo': + return this.handleUndo(request); + default: + break; + } + } + + private convertToDocCells(cells: ICell[]): { code: string; id: string }[] { + return cells + .filter((c) => c.data.cell_type === 'code') + .map((c) => { + return { code: concatMultilineString(c.data.source), id: c.id }; + }); + } + + private async handleUndo(request: NotebookModelChange): Promise { + const document = await this.getDocument(); + let changes: TextDocumentContentChangeEvent[] = []; + switch (request.kind) { + case 'clear': + // This one can be ignored, it only clears outputs + break; + case 'edit': + changes = document.editCell(request.reverse, request.id); + break; + case 'add': + case 'insert': + changes = document.remove(request.cell.id); + break; + case 'modify': + // This one can be ignored. it's only used for updating cell finished state. + break; + case 'remove': + changes = document.insertCell( + request.cell.id, + concatMultilineString(request.cell.data.source), + request.index + ); + break; + case 'remove_all': + changes = document.reloadCells(this.convertToDocCells(request.oldCells)); + break; + case 'swap': + changes = document.swap(request.secondCellId, request.firstCellId); + break; + case 'version': + // Also ignored. updates version which we don't keep track of. + break; + default: + break; + } + + return this.handleChanges(document, changes); + } + + private async handleRedo(request: NotebookModelChange): Promise { + const document = await this.getDocument(); + let changes: TextDocumentContentChangeEvent[] = []; + switch (request.kind) { + case 'clear': + // This one can be ignored, it only clears outputs + break; + case 'edit': + changes = document.editCell(request.forward, request.id); + break; + case 'add': + changes = document.addCell(request.fullText, request.currentText, request.cell.id); + break; + case 'insert': + changes = document.insertCell( + request.cell.id, + concatMultilineString(request.cell.data.source), + request.codeCellAboveId || request.index + ); + break; + case 'modify': + // This one can be ignored. it's only used for updating cell finished state. + break; + case 'remove': + changes = document.remove(request.cell.id); + break; + case 'remove_all': + changes = document.removeAll(); + break; + case 'swap': + changes = document.swap(request.firstCellId, request.secondCellId); + break; + case 'version': + // Also ignored. updates version which we don't keep track of. + break; + default: + break; + } + + return this.handleChanges(document, changes); + } + + private async loadAllCells(payload: ILoadAllCells) { + const document = await this.getDocument(); + if (document) { + const changes = document.loadAllCells( + payload.cells + .filter((c) => c.data.cell_type === 'code') + .map((cell) => { + return { + code: concatMultilineString(cell.data.source), + id: cell.id + }; + }), + this.notebookType + ); + + await this.handleChanges(document, changes); + } + } + + private async restartKernel(): Promise { + // This is the one that acts like a reset if this is the interactive window + const document = await this.getDocument(); + if (document && document.isReadOnly) { + this.sentOpenDocument = false; + const changes = document.removeAllCells(); + return this.handleChanges(document, changes); + } + } + + private setIdentity(identity: INotebookIdentity) { + this.notebookIdentity = identity.resource; + this.potentialResource = + identity.resource.scheme !== Identifiers.HistoryPurpose ? identity.resource : undefined; + this.notebookType = identity.type; + } + + private updateIdentity(identity: INotebookIdentity & { owningResource: Resource }) { + this.potentialResource = identity.owningResource ? identity.owningResource : this.potentialResource; + } + + private async getNotebook(token: CancellationToken): Promise { + return this.notebookIdentity + ? this.notebookProvider.getOrCreateNotebook({ identity: this.notebookIdentity, getOnly: true, token }) + : undefined; + } + + private async getVariableHover( + wordAtPosition: string | undefined, + token: CancellationToken + ): Promise { + if (wordAtPosition) { + const notebook = await this.getNotebook(token); + if (notebook) { + try { + const value = await this.variableProvider.getMatchingVariable(notebook, wordAtPosition, token); + if (value) { + return { + contents: [`${wordAtPosition}: ${value.type} = ${value.value}`] + }; + } + } catch (exc) { + traceError(`Exception attempting to retrieve hover for variables`, exc); + } + } + } + } +} diff --git a/src/client/datascience/interactive-common/intellisense/wordHelper.ts b/src/client/datascience/interactive-common/intellisense/wordHelper.ts new file mode 100644 index 000000000000..e8ae47b1bd06 --- /dev/null +++ b/src/client/datascience/interactive-common/intellisense/wordHelper.ts @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Borrowed this from the vscode source. From here: +// src\vs\editor\common\model\wordHelper.ts + +export interface IWordAtPosition { + readonly word: string; + readonly startColumn: number; + readonly endColumn: number; +} + +export const USUAL_WORD_SEPARATORS = '`~!@#$%^&*()-=+[{]}\\|;:\'",.<>/?'; + +/** + * Create a word definition regular expression based on default word separators. + * Optionally provide allowed separators that should be included in words. + * + * The default would look like this: + * /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g + */ +function createWordRegExp(allowInWords: string = ''): RegExp { + let source = '(-?\\d*\\.\\d\\w*)|([^'; + for (const sep of USUAL_WORD_SEPARATORS) { + if (allowInWords.indexOf(sep) >= 0) { + continue; + } + source += `\\${sep}`; + } + source += '\\s]+)'; + return new RegExp(source, 'g'); +} + +// catches numbers (including floating numbers) in the first group, and alphanum in the second +export const DEFAULT_WORD_REGEXP = createWordRegExp(); + +export function ensureValidWordDefinition(wordDefinition?: RegExp | null): RegExp { + let result: RegExp = DEFAULT_WORD_REGEXP; + + if (wordDefinition && wordDefinition instanceof RegExp) { + if (!wordDefinition.global) { + let flags = 'g'; + if (wordDefinition.ignoreCase) { + flags += 'i'; + } + if (wordDefinition.multiline) { + flags += 'm'; + } + // tslint:disable-next-line: no-any + if ((wordDefinition as any).unicode) { + flags += 'u'; + } + result = new RegExp(wordDefinition.source, flags); + } else { + result = wordDefinition; + } + } + + result.lastIndex = 0; + + return result; +} + +function getWordAtPosFast( + column: number, + wordDefinition: RegExp, + text: string, + textOffset: number +): IWordAtPosition | null { + // find whitespace enclosed text around column and match from there + + const pos = column - 1 - textOffset; + const start = text.lastIndexOf(' ', pos - 1) + 1; + + wordDefinition.lastIndex = start; + let match: RegExpMatchArray | null = wordDefinition.exec(text); + while (match) { + const matchIndex = match.index || 0; + if (matchIndex <= pos && wordDefinition.lastIndex >= pos) { + return { + word: match[0], + startColumn: textOffset + 1 + matchIndex, + endColumn: textOffset + 1 + wordDefinition.lastIndex + }; + } + match = wordDefinition.exec(text); + } + + return null; +} + +function getWordAtPosSlow( + column: number, + wordDefinition: RegExp, + text: string, + textOffset: number +): IWordAtPosition | null { + // matches all words starting at the beginning + // of the input until it finds a match that encloses + // the desired column. slow but correct + + const pos = column - 1 - textOffset; + wordDefinition.lastIndex = 0; + + let match: RegExpMatchArray | null = wordDefinition.exec(text); + while (match) { + const matchIndex = match.index || 0; + if (matchIndex > pos) { + // |nW -> matched only after the pos + return null; + } else if (wordDefinition.lastIndex >= pos) { + // W|W -> match encloses pos + return { + word: match[0], + startColumn: textOffset + 1 + matchIndex, + endColumn: textOffset + 1 + wordDefinition.lastIndex + }; + } + match = wordDefinition.exec(text); + } + + return null; +} + +export function getWordAtText( + column: number, + wordDefinition: RegExp, + text: string, + textOffset: number +): IWordAtPosition | null { + // if `words` can contain whitespace character we have to use the slow variant + // otherwise we use the fast variant of finding a word + wordDefinition.lastIndex = 0; + const match = wordDefinition.exec(text); + if (!match) { + return null; + } + // todo@joh the `match` could already be the (first) word + const ret = + match[0].indexOf(' ') >= 0 + ? // did match a word which contains a space character -> use slow word find + getWordAtPosSlow(column, wordDefinition, text, textOffset) + : // sane word definition -> use fast word find + getWordAtPosFast(column, wordDefinition, text, textOffset); + + // both (getWordAtPosFast and getWordAtPosSlow) leave the wordDefinition-RegExp + // in an undefined state and to not confuse other users of the wordDefinition + // we reset the lastIndex + wordDefinition.lastIndex = 0; + + return ret; +} + +export function regExpLeadsToEndlessLoop(regexp: RegExp): boolean { + // Exit early if it's one of these special cases which are meant to match + // against an empty string + if (regexp.source === '^' || regexp.source === '^$' || regexp.source === '$' || regexp.source === '^\\s*$') { + return false; + } + + // We check against an empty string. If the regular expression doesn't advance + // (e.g. ends in an endless loop) it will match an empty string. + const match = regexp.exec(''); + // tslint:disable-next-line: no-any + return !!(match && regexp.lastIndex === 0); +} + +export const DefaultWordPattern = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g; diff --git a/src/client/datascience/interactive-common/interactiveBase.ts b/src/client/datascience/interactive-common/interactiveBase.ts new file mode 100644 index 000000000000..d9d1d5052742 --- /dev/null +++ b/src/client/datascience/interactive-common/interactiveBase.ts @@ -0,0 +1,1557 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import type { nbformat } from '@jupyterlab/coreutils'; +import type { KernelMessage } from '@jupyterlab/services'; +import * as os from 'os'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { + CancellationToken, + commands, + ConfigurationTarget, + Event, + EventEmitter, + Memento, + Position, + Range, + Selection, + TextEditor, + Uri, + ViewColumn +} from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; +import { ServerStatus } from '../../../datascience-ui/interactive-common/mainState'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + ILiveShareApi, + IWebviewPanelProvider, + IWorkspaceService +} from '../../common/application/types'; +import { CancellationError } from '../../common/cancellation'; +import { EXTENSION_ROOT_DIR, isTestExecution, PYTHON_LANGUAGE } from '../../common/constants'; +import { RemoveKernelToolbarInInteractiveWindow, RunByLine } from '../../common/experiments/groups'; +import { traceError, traceInfo, traceWarning } from '../../common/logger'; + +import { isNil } from 'lodash'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IExperimentsManager +} from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import * as localize from '../../common/utils/localize'; +import { isUntitledFile, noop } from '../../common/utils/misc'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { generateCellRangesFromDocument } from '../cellFactory'; +import { CellMatcher } from '../cellMatcher'; +import { addToUriList, translateKernelLanguageToMonaco } from '../common'; +import { Commands, Identifiers, Telemetry } from '../constants'; +import { ColumnWarningSize, IDataViewerFactory } from '../data-viewing/types'; +import { + IAddedSysInfo, + ICopyCode, + IGotoCode, + IInteractiveWindowMapping, + INotebookIdentity, + InteractiveWindowMessages, + IReExecuteCells, + IRemoteAddCode, + IRemoteReexecuteCode, + IShowDataViewer, + ISubmitNewCell, + SysInfoReason, + VariableExplorerStateKeys +} from '../interactive-common/interactiveWindowTypes'; +import { JupyterInvalidKernelError } from '../jupyter/jupyterInvalidKernelError'; +import { + getDisplayNameOrNameOfKernelConnection, + getKernelConnectionLanguage, + kernelConnectionMetadataHasKernelModel, + kernelConnectionMetadataHasKernelSpec +} from '../jupyter/kernels/helpers'; +import { JupyterKernelPromiseFailedError } from '../jupyter/kernels/jupyterKernelPromiseFailedError'; +import { KernelSelector } from '../jupyter/kernels/kernelSelector'; +import { KernelConnectionMetadata } from '../jupyter/kernels/types'; +import { CssMessages, SharedMessages } from '../messages'; +import { + CellState, + ICell, + ICodeCssGenerator, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IInteractiveBase, + IInteractiveWindowInfo, + IInteractiveWindowListener, + IJupyterDebugger, + IJupyterVariableDataProviderFactory, + IJupyterVariables, + IJupyterVariablesRequest, + IJupyterVariablesResponse, + IMessageCell, + INotebook, + INotebookExporter, + INotebookMetadataLive, + INotebookProvider, + INotebookProviderConnection, + InterruptResult, + IStatusProvider, + IThemeFinder, + WebViewViewChangeEventArgs +} from '../types'; +import { WebviewPanelHost } from '../webviews/webviewPanelHost'; +import { InteractiveWindowMessageListener } from './interactiveWindowMessageListener'; +import { serializeLanguageConfiguration } from './serialization'; + +export abstract class InteractiveBase extends WebviewPanelHost implements IInteractiveBase { + public get notebook(): INotebook | undefined { + return this._notebook; + } + + public get id(): string { + return this._id; + } + + public get onExecutedCode(): Event { + return this.executeEvent.event; + } + public get ready(): Event { + return this.readyEvent.event; + } + + protected abstract get notebookMetadata(): INotebookMetadataLive | undefined; + + protected abstract get notebookIdentity(): INotebookIdentity; + + private unfinishedCells: ICell[] = []; + private restartingKernel: boolean = false; + private perceivedJupyterStartupTelemetryCaptured: boolean = false; + private potentiallyUnfinishedStatus: Disposable[] = []; + private addSysInfoPromise: Deferred | undefined; + private _notebook: INotebook | undefined; + private _id: string; + private executeEvent: EventEmitter = new EventEmitter(); + private connectionAndNotebookPromise: Promise | undefined; + private notebookPromise: Promise | undefined; + private setDarkPromise: Deferred | undefined; + private readyEvent = new EventEmitter(); + + constructor( + private readonly listeners: IInteractiveWindowListener[], + liveShare: ILiveShareApi, + protected applicationShell: IApplicationShell, + protected documentManager: IDocumentManager, + provider: IWebviewPanelProvider, + private disposables: IDisposableRegistry, + cssGenerator: ICodeCssGenerator, + themeFinder: IThemeFinder, + private statusProvider: IStatusProvider, + protected fs: IDataScienceFileSystem, + protected configuration: IConfigurationService, + protected jupyterExporter: INotebookExporter, + workspaceService: IWorkspaceService, + private dataExplorerFactory: IDataViewerFactory, + private jupyterVariableDataProviderFactory: IJupyterVariableDataProviderFactory, + private jupyterVariables: IJupyterVariables, + private jupyterDebugger: IJupyterDebugger, + protected errorHandler: IDataScienceErrorHandler, + protected readonly commandManager: ICommandManager, + protected globalStorage: Memento, + protected workspaceStorage: Memento, + rootPath: string, + scripts: string[], + title: string, + viewColumn: ViewColumn, + experimentsManager: IExperimentsManager, + private readonly notebookProvider: INotebookProvider, + useCustomEditorApi: boolean, + expService: IExperimentService, + private selector: KernelSelector + ) { + super( + configuration, + provider, + cssGenerator, + themeFinder, + workspaceService, + (c, v, d) => new InteractiveWindowMessageListener(liveShare, c, v, d), + rootPath, + scripts, + title, + viewColumn, + useCustomEditorApi, + experimentsManager.inExperiment(RunByLine.experiment), + expService.inExperiment(RemoveKernelToolbarInInteractiveWindow.experiment) + ); + + // Create our unique id. We use this to skip messages we send to other interactive windows + this._id = uuid(); + + // Listen for active text editor changes. This is the only way we can tell that we might be needing to gain focus + const handler = this.documentManager.onDidChangeActiveTextEditor(() => this.activating()); + this.disposables.push(handler); + + // For each listener sign up for their post events + this.listeners.forEach((l) => l.postMessage((e) => this.postMessageInternal(e.message, e.payload))); + // Channel for listeners to send messages to the interactive base. + this.listeners.forEach((l) => { + if (l.postInternalMessage) { + l.postInternalMessage((e) => this.onMessage(e.message, e.payload)); + } + }); + + // Tell each listener our identity. Can't do it here though as were in the constructor for the base class + setTimeout(() => { + this.listeners.forEach((l) => + l.onMessage(InteractiveWindowMessages.NotebookIdentity, this.notebookIdentity) + ); + }, 0); + + // When a notebook provider first makes its connection check it to see if we should create a notebook + this.disposables.push( + notebookProvider.onConnectionMade(this.createNotebookIfProviderConnectionExists.bind(this)) + ); + + // When a notebook provider indicates a kernel change, change our UI + this.disposables.push(notebookProvider.onPotentialKernelChanged(this.potentialKernelChanged.bind(this))); + + // When the variable service requests a refresh, refresh our variable list + this.disposables.push(this.jupyterVariables.refreshRequired(this.refreshVariables.bind(this))); + } + + public async show(preserveFocus: boolean = true): Promise { + // Verify a server that matches us hasn't started already + this.createNotebookIfProviderConnectionExists().ignoreErrors(); + + // Show our web panel. + return super.show(preserveFocus); + } + + // tslint:disable-next-line: no-any no-empty cyclomatic-complexity max-func-body-length + public onMessage(message: string, payload: any) { + switch (message) { + case InteractiveWindowMessages.ConvertUriForUseInWebViewRequest: + const request = payload as Uri; + const response = { request, response: this.asWebviewUri(request) }; + this.postMessageToListeners(InteractiveWindowMessages.ConvertUriForUseInWebViewResponse, response); + break; + + case InteractiveWindowMessages.Started: + // Send the first settings message + this.onDataScienceSettingsChanged().ignoreErrors(); + + // Send the loc strings (skip during testing as it takes up a lot of memory) + const locStrings = isTestExecution() ? '{}' : localize.getCollectionJSON(); + this.postMessageInternal(SharedMessages.LocInit, locStrings).ignoreErrors(); + this.variableExplorerHeightRequest() + .then((data) => + this.postMessageInternal( + InteractiveWindowMessages.VariableExplorerHeightResponse, + data + ).ignoreErrors() + ) + .catch(); // do nothing + break; + + case InteractiveWindowMessages.GotoCodeCell: + this.handleMessage(message, payload, this.gotoCode); + break; + + case InteractiveWindowMessages.CopyCodeCell: + this.handleMessage(message, payload, this.copyCode); + break; + + case InteractiveWindowMessages.RestartKernel: + this.restartKernel().ignoreErrors(); + break; + + case InteractiveWindowMessages.Interrupt: + this.interruptKernel().ignoreErrors(); + break; + + case InteractiveWindowMessages.SendInfo: + this.handleMessage(message, payload, this.updateContexts); + break; + + case InteractiveWindowMessages.SubmitNewCell: + this.handleMessage(message, payload, this.submitNewCell); + break; + + case InteractiveWindowMessages.ReExecuteCells: + this.handleMessage(message, payload, this.reexecuteCells); + break; + + case InteractiveWindowMessages.Undo: + this.logTelemetry(Telemetry.Undo); + break; + + case InteractiveWindowMessages.Redo: + this.logTelemetry(Telemetry.Redo); + break; + + case InteractiveWindowMessages.ExpandAll: + this.logTelemetry(Telemetry.ExpandAll); + break; + + case InteractiveWindowMessages.CollapseAll: + this.logTelemetry(Telemetry.CollapseAll); + break; + + case InteractiveWindowMessages.VariableExplorerToggle: + this.variableExplorerToggle(payload); + break; + + case InteractiveWindowMessages.SetVariableExplorerHeight: + this.setVariableExplorerHeight(payload).ignoreErrors(); + break; + + case InteractiveWindowMessages.AddedSysInfo: + this.handleMessage(message, payload, this.onAddedSysInfo); + break; + + case InteractiveWindowMessages.RemoteAddCode: + this.handleMessage(message, payload, this.onRemoteAddedCode); + break; + + case InteractiveWindowMessages.RemoteReexecuteCode: + this.handleMessage(message, payload, this.onRemoteReexecuteCode); + break; + + case InteractiveWindowMessages.ShowDataViewer: + this.handleMessage(message, payload, this.showDataViewer); + break; + + case InteractiveWindowMessages.GetVariablesRequest: + this.handleMessage(message, payload, this.requestVariables); + break; + + case InteractiveWindowMessages.LoadTmLanguageRequest: + this.handleMessage(message, payload, this.requestTmLanguage); + break; + + case InteractiveWindowMessages.LoadOnigasmAssemblyRequest: + this.handleMessage(message, payload, this.requestOnigasm); + break; + + case InteractiveWindowMessages.SelectKernel: + this.handleMessage(message, payload, this.selectNewKernel); + break; + + case InteractiveWindowMessages.SelectJupyterServer: + this.handleMessage(message, payload, this.selectServer); + break; + + case InteractiveWindowMessages.OpenSettings: + this.handleMessage(message, payload, this.openSettings); + break; + + case InteractiveWindowMessages.MonacoReady: + this.readyEvent.fire(); + break; + + default: + break; + } + + // Let our listeners handle the message too + this.postMessageToListeners(message, payload); + + // Pass onto our base class. + super.onMessage(message, payload); + + // After our base class handles some stuff, handle it ourselves too. + switch (message) { + case CssMessages.GetCssRequest: + // Update the notebook if we have one: + if (this._notebook) { + this.isDark() + .then((d) => (this._notebook ? this._notebook.setMatplotLibStyle(d) : Promise.resolve())) + .ignoreErrors(); + } + break; + + default: + break; + } + } + + public dispose() { + // Fire ready event in case anything is waiting on it. + this.readyEvent.fire(); + + // Dispose of the web panel. + super.dispose(); + // Tell listeners we're closing. They can decide if they should dispose themselves or not. + this.listeners.forEach((l) => l.onMessage(InteractiveWindowMessages.NotebookClose, this.notebookIdentity)); + this.updateContexts(undefined); + } + + public startProgress() { + this.postMessage(InteractiveWindowMessages.StartProgress).ignoreErrors(); + } + + public stopProgress() { + this.postMessage(InteractiveWindowMessages.StopProgress).ignoreErrors(); + } + + @captureTelemetry(Telemetry.Undo) + public undoCells() { + this.postMessage(InteractiveWindowMessages.Undo).ignoreErrors(); + } + + @captureTelemetry(Telemetry.Redo) + public redoCells() { + this.postMessage(InteractiveWindowMessages.Redo).ignoreErrors(); + } + + @captureTelemetry(Telemetry.DeleteAllCells) + public removeAllCells() { + this.postMessage(InteractiveWindowMessages.DeleteAllCells).ignoreErrors(); + } + + @captureTelemetry(Telemetry.RestartKernel) + public async restartKernel(internal: boolean = false): Promise { + // Only log this if it's user requested restart + if (!internal) { + this.logTelemetry(Telemetry.RestartKernelCommand); + } + + if (this._notebook && !this.restartingKernel) { + this.restartingKernel = true; + this.startProgress(); + + try { + if (await this.shouldAskForRestart()) { + // Ask the user if they want us to restart or not. + const message = localize.DataScience.restartKernelMessage(); + const yes = localize.DataScience.restartKernelMessageYes(); + const dontAskAgain = localize.DataScience.restartKernelMessageDontAskAgain(); + const no = localize.DataScience.restartKernelMessageNo(); + + const v = await this.applicationShell.showInformationMessage(message, yes, dontAskAgain, no); + if (v === dontAskAgain) { + await this.disableAskForRestart(); + await this.restartKernelInternal(); + } else if (v === yes) { + await this.restartKernelInternal(); + } + } else { + await this.restartKernelInternal(); + } + } finally { + this.restartingKernel = false; + this.stopProgress(); + } + } + } + + @captureTelemetry(Telemetry.Interrupt) + public async interruptKernel(): Promise { + if (this._notebook && !this.restartingKernel) { + const status = this.statusProvider.set( + localize.DataScience.interruptKernelStatus(), + true, + undefined, + undefined, + this + ); + + try { + const settings = this.configuration.getSettings(this.owningResource); + const interruptTimeout = settings.datascience.jupyterInterruptTimeout; + + const result = await this._notebook.interruptKernel(interruptTimeout); + status.dispose(); + + // We timed out, ask the user if they want to restart instead. + if (result === InterruptResult.TimedOut && !this.restartingKernel) { + const message = localize.DataScience.restartKernelAfterInterruptMessage(); + const yes = localize.DataScience.restartKernelMessageYes(); + const no = localize.DataScience.restartKernelMessageNo(); + const v = await this.applicationShell.showInformationMessage(message, yes, no); + if (v === yes) { + await this.restartKernelInternal(); + } + } else if (result === InterruptResult.Restarted) { + // Uh-oh, keyboard interrupt crashed the kernel. + this.addSysInfo(SysInfoReason.Interrupt).ignoreErrors(); + } + } catch (err) { + status.dispose(); + traceError(err); + this.applicationShell.showErrorMessage(err); + } + } + } + + @captureTelemetry(Telemetry.CopySourceCode, undefined, false) + public copyCode(args: ICopyCode) { + return this.copyCodeInternal(args.source).catch((err) => { + this.applicationShell.showErrorMessage(err); + }); + } + + public abstract hasCell(id: string): Promise; + + protected onViewStateChanged(args: WebViewViewChangeEventArgs) { + // Only activate if the active editor is empty. This means that + // vscode thinks we are actually supposed to have focus. It would be + // nice if they would more accurately tell us this, but this works for now. + // Essentially the problem is the webPanel.active state doesn't track + // if the focus is supposed to be in the webPanel or not. It only tracks if + // it's been activated. However if there's no active text editor and we're active, we + // can safely attempt to give ourselves focus. This won't actually give us focus if we aren't + // allowed to have it. + if (args.current.active && !args.previous.active) { + this.activating().ignoreErrors(); + } + + // Tell our listeners, they may need to know too + this.listeners.forEach((l) => (l.onViewStateChanged ? l.onViewStateChanged(args) : noop())); + } + + protected async activating() { + // Only activate if the active editor is empty. This means that + // vscode thinks we are actually supposed to have focus. It would be + // nice if they would more accurately tell us this, but this works for now. + // Essentially the problem is the webPanel.active state doesn't track + // if the focus is supposed to be in the webPanel or not. It only tracks if + // it's been activated. However if there's no active text editor and we're active, we + // can safely attempt to give ourselves focus. This won't actually give us focus if we aren't + // allowed to have it. + if (this.viewState.active && !this.documentManager.activeTextEditor) { + // Force the webpanel to reveal and take focus. + await super.show(false); + + // Send this to the react control + await this.postMessage(InteractiveWindowMessages.Activate); + } + } + + // Submits a new cell to the window + protected abstract submitNewCell(info: ISubmitNewCell): void; + + // Re-executes cells already in the window + protected reexecuteCells(_info: IReExecuteCells): void { + // Default is not to do anything. This only works in the native editor + } + + protected abstract updateContexts(info: IInteractiveWindowInfo | undefined): void; + + protected abstract closeBecauseOfFailure(exc: Error): Promise; + + protected abstract updateNotebookOptions(kernelConnection: KernelConnectionMetadata): Promise; + + protected async clearResult(id: string): Promise { + await this.ensureConnectionAndNotebook(); + if (this._notebook) { + this._notebook.clear(id); + } + } + + protected async setLaunchingFile(file: string): Promise { + if (file !== Identifiers.EmptyFileName && this._notebook) { + await this._notebook.setLaunchingFile(file); + } + } + + protected getNotebook(): INotebook | undefined { + return this._notebook; + } + + // tslint:disable-next-line: max-func-body-length + protected async submitCode( + code: string, + file: string, + line: number, + id?: string, + data?: nbformat.ICodeCell | nbformat.IRawCell | nbformat.IMarkdownCell, + debugInfo?: { runByLine: boolean; hashFileName?: string }, + cancelToken?: CancellationToken + ): Promise { + traceInfo(`Submitting code for ${this.id}`); + const stopWatch = + this._notebook && !this.perceivedJupyterStartupTelemetryCaptured ? new StopWatch() : undefined; + let result = true; + // Do not execute or render empty code cells + const cellMatcher = new CellMatcher(this.configService.getSettings(this.owningResource).datascience); + if (cellMatcher.stripFirstMarker(code).length === 0) { + return result; + } + + // Start a status item + const status = this.setStatus(localize.DataScience.executingCode(), false); + + // Transmit this submission to all other listeners (in a live share session) + if (!id) { + id = uuid(); + this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { + code, + file, + line, + id, + originator: this.id, + debug: debugInfo !== undefined ? true : false + }); + } + + // Create a deferred object that will wait until the status is disposed + const finishedAddingCode = createDeferred(); + const actualDispose = status.dispose.bind(status); + status.dispose = () => { + finishedAddingCode.resolve(); + actualDispose(); + }; + + try { + // Make sure we're loaded first. + await this.ensureConnectionAndNotebook(); + + // Make sure we set the dark setting + await this.ensureDarkSet(); + + // Then show our webpanel + await this.show(); + + // Add our sys info if necessary + if (file !== Identifiers.EmptyFileName) { + await this.addSysInfo(SysInfoReason.Start); + } + + if (this._notebook) { + // Before we try to execute code make sure that we have an initial directory set + // Normally set via the workspace, but we might not have one here if loading a single loose file + await this.setLaunchingFile(file); + + if (debugInfo) { + // Attach our debugger based on run by line setting + if (debugInfo.runByLine && debugInfo.hashFileName) { + await this.jupyterDebugger.startRunByLine(this._notebook, debugInfo.hashFileName); + } else if (!debugInfo.runByLine) { + await this.jupyterDebugger.startDebugging(this._notebook); + } else { + throw Error('Missing hash file name when running by line'); + } + } + + // If the file isn't unknown, set the active kernel's __file__ variable to point to that same file. + if (file !== Identifiers.EmptyFileName) { + await this._notebook.execute( + `__file__ = '${file.replace(/\\/g, '\\\\')}'`, + file, + line, + uuid(), + cancelToken, + true + ); + } + if (stopWatch && !this.perceivedJupyterStartupTelemetryCaptured) { + this.perceivedJupyterStartupTelemetryCaptured = true; + sendTelemetryEvent(Telemetry.PerceivedJupyterStartupNotebook, stopWatch?.elapsedTime); + const disposable = this._notebook.onSessionStatusChanged((e) => { + if (e === ServerStatus.Busy) { + sendTelemetryEvent(Telemetry.StartExecuteNotebookCellPerceivedCold, stopWatch?.elapsedTime); + disposable.dispose(); + } + }); + } + const owningResource = this.owningResource; + const observable = this._notebook.executeObservable(code, file, line, id, false); + + // Indicate we executed some code + this.executeEvent.fire(code); + + // Sign up for cell changes + observable.subscribe( + (cells: ICell[]) => { + // Combine the cell data with the possible input data (so we don't lose anything that might have already been in the cells) + const combined = cells.map(this.combineData.bind(undefined, data)); + + // Then send the combined output to the UI + this.sendCellsToWebView(combined); + + // Any errors will move our result to false (if allowed) + if (this.configuration.getSettings(owningResource).datascience.stopOnError) { + result = result && cells.find((c) => c.state === CellState.error) === undefined; + } + }, + (error) => { + traceError(`Error executing a cell: `, error); + status.dispose(); + if (!(error instanceof CancellationError)) { + this.applicationShell.showErrorMessage(error.toString()); + } + }, + () => { + // Indicate executing until this cell is done. + status.dispose(); + } + ); + + // Wait for the cell to finish + await finishedAddingCode.promise; + traceInfo(`Finished execution for ${id}`); + } + } finally { + status.dispose(); + + if (debugInfo) { + if (this._notebook) { + await this.jupyterDebugger.stopDebugging(this._notebook); + } + } + } + + return result; + } + + protected addMessageImpl(message: string): void { + const cell: ICell = { + id: uuid(), + file: Identifiers.EmptyFileName, + line: 0, + state: CellState.finished, + data: { + cell_type: 'messages', + messages: [message], + source: [], + metadata: {} + } + }; + + // Do the same thing that happens when new code is added. + this.sendCellsToWebView([cell]); + } + + protected sendCellsToWebView(cells: ICell[]) { + // Send each cell to the other side + cells.forEach((cell: ICell) => { + switch (cell.state) { + case CellState.init: + // Tell the react controls we have a new cell + this.postMessage(InteractiveWindowMessages.StartCell, cell).ignoreErrors(); + + // Keep track of this unfinished cell so if we restart we can finish right away. + this.unfinishedCells.push(cell); + break; + + case CellState.executing: + // Tell the react controls we have an update + this.postMessage(InteractiveWindowMessages.UpdateCellWithExecutionResults, cell).ignoreErrors(); + break; + + case CellState.error: + case CellState.finished: + // Tell the react controls we're done + this.postMessage(InteractiveWindowMessages.FinishCell, { + cell, + notebookIdentity: this.notebookIdentity.resource + }).ignoreErrors(); + + // Remove from the list of unfinished cells + this.unfinishedCells = this.unfinishedCells.filter((c) => c.id !== cell.id); + break; + + default: + break; // might want to do a progress bar or something + } + }); + } + + protected postMessage( + type: T, + payload?: M[T] + ): Promise { + // First send to our listeners + this.postMessageToListeners(type.toString(), payload); + + // Then send it to the webview + return super.postMessage(type, payload); + } + + protected handleMessage( + _message: T, + // tslint:disable-next-line:no-any + payload: any, + handler: (args: M[T]) => void + ) { + const args = payload as M[T]; + handler.bind(this)(args); + } + + protected setStatus = (message: string, showInWebView: boolean): Disposable => { + const result = this.statusProvider.set(message, showInWebView, undefined, undefined, this); + this.potentiallyUnfinishedStatus.push(result); + return result; + }; + + protected async addSysInfo(reason: SysInfoReason): Promise { + if (!this.addSysInfoPromise || reason !== SysInfoReason.Start) { + traceInfo(`Adding sys info for ${this.id} ${reason}`); + const deferred = createDeferred(); + this.addSysInfoPromise = deferred; + + // Generate a new sys info cell and send it to the web panel. + const sysInfo = await this.generateSysInfoCell(reason); + if (sysInfo) { + this.sendCellsToWebView([sysInfo]); + } + + // For anything but start, tell the other sides of a live share session + if (reason !== SysInfoReason.Start && sysInfo) { + this.shareMessage(InteractiveWindowMessages.AddedSysInfo, { + type: reason, + sysInfoCell: sysInfo, + id: this.id, + notebookIdentity: this.notebookIdentity.resource + }); + } + + // For a restart, tell our window to reset + if (reason === SysInfoReason.Restart || reason === SysInfoReason.New) { + this.postMessage(InteractiveWindowMessages.RestartKernel).ignoreErrors(); + } + + traceInfo(`Sys info for ${this.id} ${reason} complete`); + deferred.resolve(true); + } else if (this.addSysInfoPromise) { + traceInfo(`Wait for sys info for ${this.id} ${reason}`); + await this.addSysInfoPromise.promise; + } + } + + protected async ensureConnectionAndNotebook(): Promise { + // Start over if we somehow end up with a disposed notebook. + if (this._notebook && this._notebook.disposed) { + this._notebook = undefined; + this.notebookPromise = undefined; + this.connectionAndNotebookPromise = undefined; + } + if (!this.connectionAndNotebookPromise) { + this.connectionAndNotebookPromise = this.ensureConnectionAndNotebookImpl(); + } + try { + await this.connectionAndNotebookPromise; + } catch (e) { + // Reset the load promise. Don't want to keep hitting the same error + this.connectionAndNotebookPromise = undefined; + throw e; + } + } + + // ensureNotebook can be called apart from ensureNotebookAndServer and it needs + // the same protection to not be called twice + // tslint:disable-next-line: member-ordering + protected async ensureNotebook(serverConnection: INotebookProviderConnection): Promise { + if (!this.notebookPromise) { + this.notebookPromise = this.ensureNotebookImpl(serverConnection); + } + try { + await this.notebookPromise; + } catch (e) { + // Reset the load promise. Don't want to keep hitting the same error + this.notebookPromise = undefined; + + throw e; + } + } + + protected async createNotebookIfProviderConnectionExists(): Promise { + // Check to see if we are already connected to our provider + const providerConnection = await this.notebookProvider.connect({ getOnly: true }); + + if (providerConnection) { + try { + await this.ensureNotebook(providerConnection); + } catch (e) { + this.errorHandler.handleError(e).ignoreErrors(); + } + } + } + + private combineData( + oldData: nbformat.ICodeCell | nbformat.IRawCell | nbformat.IMarkdownCell | undefined, + cell: ICell + ): ICell { + if (oldData) { + const result = { + ...cell, + data: { + ...oldData, + ...cell.data, + metadata: { + ...oldData.metadata, + ...cell.data.metadata + } + } + }; + // Workaround the nyc compiler problem. + // tslint:disable-next-line: no-any + return (result as any) as ICell; + } + // tslint:disable-next-line: no-any + return (cell as any) as ICell; + } + + private async ensureConnectionAndNotebookImpl(): Promise { + // Make sure we're loaded first. + try { + traceInfo('Waiting for jupyter server and web panel ...'); + const serverConnection = await this.notebookProvider.connect({ getOnly: false, disableUI: false }); + if (serverConnection) { + await this.ensureNotebook(serverConnection); + } + } catch (exc) { + // We should dispose ourselves if the load fails. Othewise the user + // updates their install and we just fail again because the load promise is the same. + await this.closeBecauseOfFailure(exc); + + // Finally throw the exception so the user can do something about it. + throw exc; + } + } + + // tslint:disable-next-line: no-any + private postMessageToListeners(message: string, payload: any) { + if (this.listeners) { + this.listeners.forEach((l) => l.onMessage(message, payload)); + } + } + + private async shouldAskForRestart(): Promise { + const settings = this.configuration.getSettings(this.owningResource); + return settings && settings.datascience && settings.datascience.askForKernelRestart === true; + } + + private async disableAskForRestart(): Promise { + const settings = this.configuration.getSettings(this.owningResource); + if (settings && settings.datascience) { + settings.datascience.askForKernelRestart = false; + this.configuration + .updateSetting('dataScience.askForKernelRestart', false, undefined, ConfigurationTarget.Global) + .ignoreErrors(); + } + } + + private async shouldAskForLargeData(): Promise { + const settings = this.configuration.getSettings(this.owningResource); + return settings && settings.datascience && settings.datascience.askForLargeDataFrames === true; + } + + private async disableAskForLargeData(): Promise { + const settings = this.configuration.getSettings(this.owningResource); + if (settings && settings.datascience) { + settings.datascience.askForLargeDataFrames = false; + this.configuration + .updateSetting('dataScience.askForLargeDataFrames', false, undefined, ConfigurationTarget.Global) + .ignoreErrors(); + } + } + + private async checkColumnSize(columnSize: number): Promise { + if (columnSize > ColumnWarningSize && (await this.shouldAskForLargeData())) { + const message = localize.DataScience.tooManyColumnsMessage(); + const yes = localize.DataScience.tooManyColumnsYes(); + const no = localize.DataScience.tooManyColumnsNo(); + const dontAskAgain = localize.DataScience.tooManyColumnsDontAskAgain(); + + const result = await this.applicationShell.showWarningMessage(message, yes, no, dontAskAgain); + if (result === dontAskAgain) { + await this.disableAskForLargeData(); + } + return result === yes; + } + return true; + } + + private async showDataViewer(request: IShowDataViewer): Promise { + try { + if (await this.checkColumnSize(request.columnSize)) { + const jupyterVariableDataProvider = await this.jupyterVariableDataProviderFactory.create( + request.variable, + this._notebook! + ); + const title: string = `${localize.DataScience.dataExplorerTitle()} - ${request.variable.name}`; + await this.dataExplorerFactory.create(jupyterVariableDataProvider, title); + } + } catch (e) { + this.applicationShell.showErrorMessage(e.toString()); + } + } + + private onAddedSysInfo(sysInfo: IAddedSysInfo) { + // See if this is from us or not. + if (sysInfo.id !== this.id) { + // Not from us, must come from a different interactive window. Add to our + // own to keep in sync + if (sysInfo.sysInfoCell) { + this.sendCellsToWebView([sysInfo.sysInfoCell]); + } + } + } + + private async onRemoteReexecuteCode(args: IRemoteReexecuteCode) { + // Make sure this is valid + if (args && args.id && args.file && args.originator !== this.id) { + try { + // On a reexecute clear the previous execution + if (this._notebook) { + this._notebook.clear(args.id); + } + + // Indicate this in our telemetry. + // Add new telemetry type + sendTelemetryEvent(Telemetry.RemoteReexecuteCode); + + // Submit this item as new code. + this.submitCode( + args.code, + args.file, + args.line, + args.id, + undefined, + args.debug ? { runByLine: false } : undefined + ).ignoreErrors(); + } catch (exc) { + this.errorHandler.handleError(exc).ignoreErrors(); + } + } + } + + private async onRemoteAddedCode(args: IRemoteAddCode) { + // Make sure this is valid + if (args && args.id && args.file && args.originator !== this.id) { + try { + // Indicate this in our telemetry. + sendTelemetryEvent(Telemetry.RemoteAddCode); + + // Submit this item as new code. + await this.submitCode( + args.code, + args.file, + args.line, + args.id, + undefined, + args.debug ? { runByLine: false } : undefined + ); + } catch (exc) { + this.errorHandler.handleError(exc).ignoreErrors(); + } + } + } + + private finishOutstandingCells() { + this.unfinishedCells.forEach((c) => { + c.state = CellState.error; + this.postMessage(InteractiveWindowMessages.FinishCell, { + cell: c, + notebookIdentity: this.notebookIdentity.resource + }).ignoreErrors(); + }); + this.unfinishedCells = []; + this.potentiallyUnfinishedStatus.forEach((s) => s.dispose()); + this.potentiallyUnfinishedStatus = []; + } + + private async restartKernelInternal(): Promise { + this.restartingKernel = true; + + // First we need to finish all outstanding cells. + this.finishOutstandingCells(); + + // Set our status + const status = this.statusProvider.set( + localize.DataScience.restartingKernelStatus(), + true, + undefined, + undefined, + this + ); + + try { + if (this._notebook) { + await this._notebook.restartKernel( + (await this.generateDataScienceExtraSettings()).jupyterInterruptTimeout + ); + await this.addSysInfo(SysInfoReason.Restart); + + // Compute if dark or not. + const knownDark = await this.isDark(); + + // Before we run any cells, update the dark setting + await this._notebook.setMatplotLibStyle(knownDark); + } + } catch (exc) { + // If we get a kernel promise failure, then restarting timed out. Just shutdown and restart the entire server + if (exc instanceof JupyterKernelPromiseFailedError && this._notebook) { + await this._notebook.dispose(); + await this.ensureConnectionAndNotebook(); + await this.addSysInfo(SysInfoReason.Restart); + } else { + // Show the error message + this.applicationShell.showErrorMessage(exc); + traceError(exc); + } + } finally { + status.dispose(); + this.restartingKernel = false; + } + } + + private logTelemetry = (event: Telemetry) => { + sendTelemetryEvent(event); + }; + + private selectNewKernel() { + // This is handled by a command. + this.commandManager.executeCommand(Commands.SwitchJupyterKernel, { + identity: this.notebookIdentity.resource, + resource: this.owningResource, + currentKernelDisplayName: + this.notebookMetadata?.kernelspec?.display_name || + this.notebookMetadata?.kernelspec?.name || + getDisplayNameOrNameOfKernelConnection(this._notebook?.getKernelConnection()) + }); + } + + private async createNotebook(serverConnection: INotebookProviderConnection): Promise { + let notebook: INotebook | undefined; + while (!notebook) { + try { + notebook = await this.notebookProvider.getOrCreateNotebook({ + identity: this.notebookIdentity.resource, + resource: this.owningResource, + metadata: this.notebookMetadata + }); + if (notebook) { + const executionActivation = { ...this.notebookIdentity, owningResource: this.owningResource }; + this.postMessageToListeners( + InteractiveWindowMessages.NotebookExecutionActivated, + executionActivation + ); + } + } catch (e) { + // If we get an invalid kernel error, make sure to ask the user to switch + if (e instanceof JupyterInvalidKernelError && serverConnection && serverConnection.localLaunch) { + // Ask the user for a new local kernel + const newKernel = await this.selector.askForLocalKernel( + this.owningResource, + serverConnection.type, + e.kernelConnectionMetadata + ); + if (newKernel && kernelConnectionMetadataHasKernelSpec(newKernel) && newKernel.kernelSpec) { + this.commandManager.executeCommand( + Commands.SetJupyterKernel, + newKernel, + this.notebookIdentity.resource, + this.owningResource + ); + } + } else { + throw e; + } + } + } + return notebook; + } + + private getServerUri(serverConnection: INotebookProviderConnection | undefined): string { + let localizedUri = ''; + + if (serverConnection) { + // Determine the connection URI of the connected server to display + if (serverConnection.localLaunch) { + localizedUri = localize.DataScience.localJupyterServer(); + } else { + // Log this remote URI into our MRU list + addToUriList( + this.globalStorage, + !isNil(serverConnection.url) ? serverConnection.url : serverConnection.displayName, + Date.now(), + serverConnection.displayName + ); + } + } + + return localizedUri; + } + + private async listenToNotebookEvents(notebook: INotebook): Promise { + const statusChangeHandler = async (status: ServerStatus) => { + const connectionMetadata = notebook.getKernelConnection(); + const name = getDisplayNameOrNameOfKernelConnection(connectionMetadata); + + await this.postMessage(InteractiveWindowMessages.UpdateKernel, { + jupyterServerStatus: status, + localizedUri: this.getServerUri(notebook.connection), + displayName: name, + language: translateKernelLanguageToMonaco( + getKernelConnectionLanguage(connectionMetadata) || PYTHON_LANGUAGE + ) + }); + }; + notebook.onSessionStatusChanged(statusChangeHandler); + this.disposables.push(notebook.onKernelChanged(this.kernelChangeHandler.bind(this))); + + // Fire the status changed handler at least once (might have already been running and so won't show a status update) + statusChangeHandler(notebook.status).ignoreErrors(); + + // Also listen to iopub messages so we can update other cells on update_display_data + notebook.registerIOPubListener(this.handleKernelMessage.bind(this)); + } + + private async ensureNotebookImpl(serverConnection: INotebookProviderConnection): Promise { + // Create a new notebook if we need to. + if (!this._notebook) { + // While waiting make the notebook look busy + this.postMessage(InteractiveWindowMessages.UpdateKernel, { + jupyterServerStatus: ServerStatus.Busy, + localizedUri: this.getServerUri(serverConnection), + displayName: '', + language: PYTHON_LANGUAGE + }).ignoreErrors(); + + this._notebook = await this.createNotebook(serverConnection); + + // If that works notify the UI and listen to status changes. + if (this._notebook && this._notebook.identity) { + return this.listenToNotebookEvents(this._notebook); + } + } + } + + private refreshVariables() { + this.postMessage(InteractiveWindowMessages.ForceVariableRefresh).ignoreErrors(); + } + + private async potentialKernelChanged(data: { + identity: Uri; + kernelConnection: KernelConnectionMetadata; + }): Promise { + const specOrModel = kernelConnectionMetadataHasKernelModel(data.kernelConnection) + ? data.kernelConnection.kernelModel + : data.kernelConnection.kernelSpec; + if (!this._notebook && specOrModel && this.notebookIdentity.resource.toString() === data.identity.toString()) { + // No notebook, send update to UI anyway + this.postMessage(InteractiveWindowMessages.UpdateKernel, { + jupyterServerStatus: ServerStatus.NotStarted, + localizedUri: '', + displayName: getDisplayNameOrNameOfKernelConnection(data.kernelConnection), + language: translateKernelLanguageToMonaco( + getKernelConnectionLanguage(data.kernelConnection) || PYTHON_LANGUAGE + ) + }).ignoreErrors(); + + // Update our model + this.updateNotebookOptions(data.kernelConnection).ignoreErrors(); + } + } + + @captureTelemetry(Telemetry.GotoSourceCode, undefined, false) + private gotoCode(args: IGotoCode) { + this.gotoCodeInternal(args.file, args.line).catch((err) => { + this.applicationShell.showErrorMessage(err); + }); + } + + private async gotoCodeInternal(file: string, line: number) { + let editor: TextEditor | undefined; + + if (await this.fs.localFileExists(file)) { + editor = await this.documentManager.showTextDocument(Uri.file(file), { viewColumn: ViewColumn.One }); + } else { + // File URI isn't going to work. Look through the active text documents + editor = this.documentManager.visibleTextEditors.find((te) => te.document.fileName === file); + if (editor) { + editor.show(); + } + } + + // If we found the editor change its selection + if (editor) { + editor.revealRange(new Range(line, 0, line, 0)); + editor.selection = new Selection(new Position(line, 0), new Position(line, 0)); + } + } + + private async copyCodeInternal(source: string) { + let editor = this.documentManager.activeTextEditor; + if (!editor || editor.document.languageId !== PYTHON_LANGUAGE) { + // Find the first visible python editor + const pythonEditors = this.documentManager.visibleTextEditors.filter( + (e) => e.document.languageId === PYTHON_LANGUAGE || e.document.isUntitled + ); + + if (pythonEditors.length > 0) { + editor = pythonEditors[0]; + } + } + if (editor && (editor.document.languageId === PYTHON_LANGUAGE || editor.document.isUntitled)) { + // Figure out if any cells in this document already. + const ranges = generateCellRangesFromDocument( + editor.document, + await this.generateDataScienceExtraSettings() + ); + const hasCellsAlready = ranges.length > 0; + const line = editor.selection.start.line; + const revealLine = line + 1; + const defaultCellMarker = + this.configService.getSettings(this.owningResource).datascience.defaultCellMarker || + Identifiers.DefaultCodeCellMarker; + let newCode = `${source}${os.EOL}`; + if (hasCellsAlready) { + // See if inside of a range or not. + const matchingRange = ranges.find((r) => r.range.start.line <= line && r.range.end.line >= line); + + // If in the middle, wrap the new code + if (matchingRange && matchingRange.range.start.line < line && line < editor.document.lineCount - 1) { + newCode = `${defaultCellMarker}${os.EOL}${source}${os.EOL}${defaultCellMarker}${os.EOL}`; + } else { + newCode = `${defaultCellMarker}${os.EOL}${source}${os.EOL}`; + } + } else if (editor.document.lineCount <= 0 || editor.document.isUntitled) { + // No lines in the document at all, just insert new code + newCode = `${defaultCellMarker}${os.EOL}${source}${os.EOL}`; + } + + await editor.edit((editBuilder) => { + editBuilder.insert(new Position(line, 0), newCode); + }); + editor.revealRange(new Range(revealLine, 0, revealLine + source.split('\n').length + 3, 0)); + + // Move selection to just beyond the text we input so that the next + // paste will be right after + const selectionLine = line + newCode.split('\n').length - 1; + editor.selection = new Selection(new Position(selectionLine, 0), new Position(selectionLine, 0)); + } + } + + private async ensureDarkSet(): Promise { + if (!this.setDarkPromise) { + this.setDarkPromise = createDeferred(); + + // Wait for the web panel to get the isDark setting + const knownDark = await this.isDark(); + + // Before we run any cells, update the dark setting + if (this._notebook) { + await this._notebook.setMatplotLibStyle(knownDark); + } + + this.setDarkPromise.resolve(true); + } else { + await this.setDarkPromise.promise; + } + } + + private generateSysInfoCell = async (reason: SysInfoReason): Promise => { + // Execute the code 'import sys\r\nsys.version' and 'import sys\r\nsys.executable' to get our + // version and executable + if (this._notebook) { + const message = await this.generateSysInfoMessage(reason); + + // The server handles getting this data. + const sysInfo = await this._notebook.getSysInfo(); + if (sysInfo) { + // Connection string only for our initial start, not restart or interrupt + let connectionString: string = ''; + if (reason === SysInfoReason.Start) { + connectionString = this.generateConnectionInfoString(this._notebook.connection); + } + + // Update our sys info with our locally applied data. + const cell = sysInfo.data as IMessageCell; + if (cell) { + cell.messages.unshift(message); + if (connectionString && connectionString.length) { + cell.messages.unshift(connectionString); + } + } + + return sysInfo; + } + } + }; + + private async generateSysInfoMessage(reason: SysInfoReason): Promise { + switch (reason) { + case SysInfoReason.Start: + return localize.DataScience.pythonVersionHeader(); + break; + case SysInfoReason.Restart: + return localize.DataScience.pythonRestartHeader(); + break; + case SysInfoReason.Interrupt: + return localize.DataScience.pythonInterruptFailedHeader(); + break; + case SysInfoReason.New: + return localize.DataScience.pythonNewHeader(); + break; + case SysInfoReason.Connect: + return localize.DataScience.pythonConnectHeader(); + break; + default: + traceError('Invalid SysInfoReason'); + return ''; + break; + } + } + + private generateConnectionInfoString(connInfo: INotebookProviderConnection | undefined): string { + return connInfo?.displayName || ''; + } + + private async requestVariables(args: IJupyterVariablesRequest): Promise { + // Request our new list of variables + const response: IJupyterVariablesResponse = this._notebook + ? await this.jupyterVariables.getVariables(this._notebook, args) + : { + totalCount: 0, + pageResponse: [], + pageStartIndex: args?.startIndex, + executionCount: args?.executionCount, + refreshCount: args?.refreshCount || 0 + }; + + this.postMessage(InteractiveWindowMessages.GetVariablesResponse, response).ignoreErrors(); + sendTelemetryEvent(Telemetry.VariableExplorerVariableCount, undefined, { variableCount: response.totalCount }); + } + + // tslint:disable-next-line: no-any + private variableExplorerToggle = (payload?: any) => { + // Direct undefined check as false boolean will skip code + if (payload !== undefined) { + const openValue = payload as boolean; + + // Log the state in our Telemetry + sendTelemetryEvent(Telemetry.VariableExplorerToggled, undefined, { + open: openValue, + runByLine: this.jupyterDebugger.isRunningByLine + }); + } + }; + + // tslint:disable-next-line: no-any + private async setVariableExplorerHeight(payload?: any) { + // Store variable explorer height based on file name in workspace storage + if (payload !== undefined) { + const updatedHeights = payload as { containerHeight: number; gridHeight: number }; + const uri = this.owningResource; // Get file name + + if (!uri) { + return; + } + // Storing an object that looks like + // { "fully qualified Path to 1.ipynb": 1234, + // "fully qualified path to 2.ipynb": 1234 } + + // tslint:disable-next-line: no-any + const value = this.workspaceStorage.get(VariableExplorerStateKeys.height, {} as any); + value[uri.toString()] = updatedHeights; + this.workspaceStorage.update(VariableExplorerStateKeys.height, value); + } + } + + private async variableExplorerHeightRequest(): Promise< + { containerHeight: number; gridHeight: number } | undefined + > { + const uri = this.owningResource; // Get file name + + if (!uri || isUntitledFile(uri)) { + return; // don't restore height of untitled notebooks + } + + // tslint:disable-next-line: no-any + const value = this.workspaceStorage.get(VariableExplorerStateKeys.height, {} as any); + const uriString = uri.toString(); + if (uriString in value) { + return value[uriString]; + } + } + + private async requestTmLanguage(languageId: string) { + // Get the contents of the appropriate tmLanguage file. + traceInfo('Request for tmlanguage file.'); + const languageJson = await this.themeFinder.findTmLanguage(languageId); + const languageConfiguration = serializeLanguageConfiguration( + await this.themeFinder.findLanguageConfiguration(languageId) + ); + const extensions = languageId === PYTHON_LANGUAGE ? ['.py'] : []; + const scopeName = `scope.${languageId}`; // This works for python, not sure about c# etc. + this.postMessage(InteractiveWindowMessages.LoadTmLanguageResponse, { + languageJSON: languageJson ?? '', + languageConfiguration, + extensions, + scopeName, + languageId + }).ignoreErrors(); + } + + private async requestOnigasm(): Promise { + // Look for the file next or our current file (this is where it's installed in the vsix) + let filePath = path.join(__dirname, 'node_modules', 'onigasm', 'lib', 'onigasm.wasm'); + traceInfo(`Request for onigasm file at ${filePath}`); + if (this.fs) { + if (await this.fs.localFileExists(filePath)) { + const contents = await this.fs.readLocalData(filePath); + this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse, contents).ignoreErrors(); + } else { + // During development it's actually in the node_modules folder + filePath = path.join(EXTENSION_ROOT_DIR, 'node_modules', 'onigasm', 'lib', 'onigasm.wasm'); + traceInfo(`Backup request for onigasm file at ${filePath}`); + if (await this.fs.localFileExists(filePath)) { + const contents = await this.fs.readLocalData(filePath); + this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse, contents).ignoreErrors(); + } else { + traceWarning('Onigasm file not found. Colorization will not be available.'); + this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse).ignoreErrors(); + } + } + } else { + // This happens during testing. Onigasm not needed as we're not testing colorization. + traceWarning('File system not found. Colorization will not be available.'); + this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse).ignoreErrors(); + } + } + + private async selectServer() { + await this.commandManager.executeCommand(Commands.SelectJupyterURI); + } + private async kernelChangeHandler(kernelConnection: KernelConnectionMetadata) { + // Check if we are changing to LiveKernelModel + if (kernelConnection.kind === 'connectToLiveKernel') { + await this.addSysInfo(SysInfoReason.Connect); + } else { + await this.addSysInfo(SysInfoReason.New); + } + return this.updateNotebookOptions(kernelConnection); + } + + private openSettings(setting: string | undefined) { + if (setting) { + commands.executeCommand('workbench.action.openSettings', setting); + } else { + commands.executeCommand('workbench.action.openSettings'); + } + } + + private handleKernelMessage(msg: KernelMessage.IIOPubMessage, _requestId: string) { + // Only care about one sort of message, UpdateDisplayData + // tslint:disable-next-line: no-require-imports + const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); // NOSONAR + if (jupyterLab.KernelMessage.isUpdateDisplayDataMsg(msg)) { + this.handleUpdateDisplayData(msg as KernelMessage.IUpdateDisplayDataMsg); + } + } + + private handleUpdateDisplayData(msg: KernelMessage.IUpdateDisplayDataMsg) { + // Send to the UI to handle + this.postMessage(InteractiveWindowMessages.UpdateDisplayData, msg).ignoreErrors(); + } +} diff --git a/src/client/datascience/interactive-common/interactiveWindowMessageListener.ts b/src/client/datascience/interactive-common/interactiveWindowMessageListener.ts new file mode 100644 index 000000000000..df691ea0497f --- /dev/null +++ b/src/client/datascience/interactive-common/interactiveWindowMessageListener.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import * as vscode from 'vscode'; +import * as vsls from 'vsls/vscode'; + +import { ILiveShareApi, IWebviewPanel, IWebviewPanelMessageListener } from '../../common/application/types'; +import { Identifiers, LiveShare } from '../constants'; +import { PostOffice } from '../liveshare/postOffice'; +import { InteractiveWindowMessages, InteractiveWindowRemoteMessages } from './interactiveWindowTypes'; + +// tslint:disable:no-any + +// This class listens to messages that come from the local Python Interactive window +export class InteractiveWindowMessageListener implements IWebviewPanelMessageListener { + private postOffice: PostOffice; + private disposedCallback: () => void; + private callback: (message: string, payload: any) => void; + private viewChanged: (panel: IWebviewPanel) => void; + private interactiveWindowMessages: string[] = []; + + constructor( + liveShare: ILiveShareApi, + callback: (message: string, payload: any) => void, + viewChanged: (panel: IWebviewPanel) => void, + disposed: () => void + ) { + this.postOffice = new PostOffice(LiveShare.WebPanelMessageService, liveShare, (api, _command, role, args) => + this.translateHostArgs(api, role, args) + ); + + // Save our dispose callback so we remove our interactive window + this.disposedCallback = disposed; + + // Save our local callback so we can handle the non broadcast case(s) + this.callback = callback; + + // Save view changed so we can forward view change events. + this.viewChanged = viewChanged; + + // Remember the list of interactive window messages we registered for + this.interactiveWindowMessages = this.getInteractiveWindowMessages(); + + // We need to register callbacks for all interactive window messages. + this.interactiveWindowMessages.forEach((m) => { + this.postOffice.registerCallback(m, (a) => callback(m, a)).ignoreErrors(); + }); + } + + public async dispose() { + await this.postOffice.dispose(); + this.disposedCallback(); + } + + public onMessage(message: string, payload: any) { + // We received a message from the local webview. Broadcast it to everybody if it's a remote message + if (InteractiveWindowRemoteMessages.indexOf(message) >= 0) { + this.postOffice.postCommand(message, payload).ignoreErrors(); + } else { + // Send to just our local callback. + this.callback(message, payload); + } + } + + public onChangeViewState(panel: IWebviewPanel) { + // Forward this onto our callback + this.viewChanged(panel); + } + + private getInteractiveWindowMessages(): string[] { + return Object.keys(InteractiveWindowMessages).map((k) => (InteractiveWindowMessages as any)[k].toString()); + } + + private translateHostArgs(api: vsls.LiveShare | null, role: vsls.Role, args: any[]) { + // Figure out the true type of the args + if (api && args && args.length > 0) { + const trueArg = args[0]; + + // See if the trueArg has a 'file' name or not + if (trueArg) { + const keys = Object.keys(trueArg); + keys.forEach((k) => { + if (k.includes('file')) { + if (typeof trueArg[k] === 'string') { + // Pull out the string. We need to convert it to a file or vsls uri based on our role + const file = trueArg[k].toString(); + + // Skip the empty file + if (file !== Identifiers.EmptyFileName) { + const uri = + role === vsls.Role.Host ? vscode.Uri.file(file) : vscode.Uri.parse(`vsls:${file}`); + + // Translate this into the other side. + trueArg[k] = + role === vsls.Role.Host + ? api.convertLocalUriToShared(uri).fsPath + : api.convertSharedUriToLocal(uri).fsPath; + } + } + } + }); + } + } + } +} diff --git a/src/client/datascience/interactive-common/interactiveWindowTypes.ts b/src/client/datascience/interactive-common/interactiveWindowTypes.ts new file mode 100644 index 000000000000..86921d56c919 --- /dev/null +++ b/src/client/datascience/interactive-common/interactiveWindowTypes.ts @@ -0,0 +1,684 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { Uri } from 'vscode'; +import { DebugState, IServerState } from '../../../datascience-ui/interactive-common/mainState'; + +import type { KernelMessage } from '@jupyterlab/services'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { + CommonActionType, + IAddCellAction, + IChangeGatherStatus, + ILoadIPyWidgetClassFailureAction, + IVariableExplorerHeight, + LoadIPyWidgetClassLoadAction, + NotifyIPyWidgeWidgetVersionNotSupportedAction +} from '../../../datascience-ui/interactive-common/redux/reducers/types'; +import { Resource } from '../../common/types'; +import { NativeKeyboardCommandTelemetry, NativeMouseCommandTelemetry } from '../constants'; +import { WidgetScriptSource } from '../ipywidgets/types'; +import { KernelConnectionMetadata } from '../jupyter/kernels/types'; +import { CssMessages, IGetCssRequest, IGetCssResponse, IGetMonacoThemeRequest, SharedMessages } from '../messages'; +import { IGetMonacoThemeResponse } from '../monacoMessages'; +import { + ICell, + IInteractiveWindowInfo, + IJupyterVariable, + IJupyterVariablesRequest, + IJupyterVariablesResponse, + INotebookModel, + KernelSocketOptions +} from '../types'; +import { ILanguageConfigurationDto } from './serialization'; +import { BaseReduxActionPayload } from './types'; + +export enum InteractiveWindowMessages { + StartCell = 'start_cell', + FinishCell = 'finish_cell', + UpdateCellWithExecutionResults = 'UpdateCellWithExecutionResults', + GotoCodeCell = 'gotocell_code', + CopyCodeCell = 'copycell_code', + NotebookExecutionActivated = 'notebook_execution_activated', + RestartKernel = 'restart_kernel', + Export = 'export_to_ipynb', + ExportNotebookAs = 'export_as_menu', + GetAllCells = 'get_all_cells', + ReturnAllCells = 'return_all_cells', + DeleteAllCells = 'delete_all_cells', + Undo = 'undo', + Redo = 'redo', + ExpandAll = 'expand_all', + CollapseAll = 'collapse_all', + StartProgress = 'start_progress', + StopProgress = 'stop_progress', + Interrupt = 'interrupt', + SubmitNewCell = 'submit_new_cell', + SettingsUpdated = 'settings_updated', + // Message sent to React component from extension asking it to save the notebook. + DoSave = 'DoSave', + SendInfo = 'send_info', + Started = 'started', + ConvertUriForUseInWebViewRequest = 'ConvertUriForUseInWebViewRequest', + ConvertUriForUseInWebViewResponse = 'ConvertUriForUseInWebViewResponse', + AddedSysInfo = 'added_sys_info', + RemoteAddCode = 'remote_add_code', + RemoteReexecuteCode = 'remote_reexecute_code', + Activate = 'activate', + ShowDataViewer = 'show_data_explorer', + GetVariablesRequest = 'get_variables_request', + GetVariablesResponse = 'get_variables_response', + VariableExplorerToggle = 'variable_explorer_toggle', + SetVariableExplorerHeight = 'set_variable_explorer_height', + VariableExplorerHeightResponse = 'variable_explorer_height_response', + ForceVariableRefresh = 'force_variable_refresh', + ProvideCompletionItemsRequest = 'provide_completion_items_request', + CancelCompletionItemsRequest = 'cancel_completion_items_request', + ProvideCompletionItemsResponse = 'provide_completion_items_response', + ProvideHoverRequest = 'provide_hover_request', + CancelHoverRequest = 'cancel_hover_request', + ProvideHoverResponse = 'provide_hover_response', + ProvideSignatureHelpRequest = 'provide_signature_help_request', + CancelSignatureHelpRequest = 'cancel_signature_help_request', + ProvideSignatureHelpResponse = 'provide_signature_help_response', + ResolveCompletionItemRequest = 'resolve_completion_item_request', + CancelResolveCompletionItemRequest = 'cancel_resolve_completion_item_request', + ResolveCompletionItemResponse = 'resolve_completion_item_response', + Sync = 'sync_message_used_to_broadcast_and_sync_editors', + LoadOnigasmAssemblyRequest = 'load_onigasm_assembly_request', + LoadOnigasmAssemblyResponse = 'load_onigasm_assembly_response', + LoadTmLanguageRequest = 'load_tmlanguage_request', + LoadTmLanguageResponse = 'load_tmlanguage_response', + OpenLink = 'open_link', + ShowPlot = 'show_plot', + SavePng = 'save_png', + StartDebugging = 'start_debugging', + StopDebugging = 'stop_debugging', + GatherCode = 'gather_code', + GatherCodeToScript = 'gather_code_to_script', + Gathering = 'gathering', + LaunchNotebookTrustPrompt = 'launch_notebook_trust_prompt', + TrustNotebookComplete = 'trust_notebook_complete', + LoadAllCells = 'load_all_cells', + LoadAllCellsComplete = 'load_all_cells_complete', + ScrollToCell = 'scroll_to_cell', + ReExecuteCells = 'reexecute_cells', + NotebookIdentity = 'identity', + NotebookClose = 'close', + NotebookDirty = 'dirty', + NotebookClean = 'clean', + SaveAll = 'save_all', + NativeCommand = 'native_command', + VariablesComplete = 'variables_complete', + NotebookRunAllCells = 'notebook_run_all_cells', + NotebookRunSelectedCell = 'notebook_run_selected_cell', + NotebookAddCellBelow = 'notebook_add_cell_below', + ExecutionRendered = 'rendered_execution', + FocusedCellEditor = 'focused_cell_editor', + SelectedCell = 'selected_cell', + OutputToggled = 'output_toggled', + UnfocusedCellEditor = 'unfocused_cell_editor', + MonacoReady = 'monaco_ready', + ClearAllOutputs = 'clear_all_outputs', + SelectKernel = 'select_kernel', + UpdateKernel = 'update_kernel', + SelectJupyterServer = 'select_jupyter_server', + UpdateModel = 'update_model', + ReceivedUpdateModel = 'received_update_model', + OpenSettings = 'open_settings', + UpdateDisplayData = 'update_display_data', + IPyWidgetLoadSuccess = 'ipywidget_load_success', + IPyWidgetLoadFailure = 'ipywidget_load_failure', + IPyWidgetRenderFailure = 'ipywidget_render_failure', + IPyWidgetUnhandledKernelMessage = 'ipywidget_unhandled_kernel_message', + IPyWidgetWidgetVersionNotSupported = 'ipywidget_widget_version_not_supported', + RunByLine = 'run_by_line', + Step = 'step', + Continue = 'continue', + ShowContinue = 'show_continue', + ShowBreak = 'show_break', + ShowingIp = 'showing_ip', + DebugStateChange = 'debug_state_change', + KernelIdle = 'kernel_idle', + HasCell = 'has_cell', + HasCellResponse = 'has_cell_response' +} + +export enum IPyWidgetMessages { + IPyWidgets_Ready = 'IPyWidgets_Ready', + IPyWidgets_onRestartKernel = 'IPyWidgets_onRestartKernel', + IPyWidgets_onKernelChanged = 'IPyWidgets_onKernelChanged', + IPyWidgets_updateRequireConfig = 'IPyWidgets_updateRequireConfig', + /** + * UI sends a request to extension to determine whether we have the source for any of the widgets. + */ + IPyWidgets_WidgetScriptSourceRequest = 'IPyWidgets_WidgetScriptSourceRequest', + /** + * Extension sends response to the request with yes/no. + */ + IPyWidgets_WidgetScriptSourceResponse = 'IPyWidgets_WidgetScriptSourceResponse', + IPyWidgets_msg = 'IPyWidgets_msg', + IPyWidgets_binary_msg = 'IPyWidgets_binary_msg', + // Message was received by the widget kernel and added to the msgChain queue for processing + IPyWidgets_msg_received = 'IPyWidgets_msg_received', + // IOPub message was fully handled by the widget kernel + IPyWidgets_iopub_msg_handled = 'IPyWidgets_iopub_msg_handled', + IPyWidgets_kernelOptions = 'IPyWidgets_kernelOptions', + IPyWidgets_registerCommTarget = 'IPyWidgets_registerCommTarget', + IPyWidgets_RegisterMessageHook = 'IPyWidgets_RegisterMessageHook', + // Message sent when the extension has finished an operation requested by the kernel UI for processing a message + IPyWidgets_ExtensionOperationHandled = 'IPyWidgets_ExtensionOperationHandled', + IPyWidgets_RemoveMessageHook = 'IPyWidgets_RemoveMessageHook', + IPyWidgets_MessageHookCall = 'IPyWidgets_MessageHookCall', + IPyWidgets_MessageHookResult = 'IPyWidgets_MessageHookResult', + IPyWidgets_mirror_execute = 'IPyWidgets_mirror_execute' +} + +// These are the messages that will mirror'd to guest/hosts in +// a live share session +export const InteractiveWindowRemoteMessages: string[] = [ + InteractiveWindowMessages.AddedSysInfo.toString(), + InteractiveWindowMessages.RemoteAddCode.toString(), + InteractiveWindowMessages.RemoteReexecuteCode.toString() +]; + +export interface IGotoCode { + file: string; + line: number; +} + +export interface ICopyCode { + source: string; +} + +export enum VariableExplorerStateKeys { + height = 'NBVariableHeights' +} + +export enum ExportNotebookSettings { + lastSaveLocation = 'NBExportSaveLocation' +} + +export enum SysInfoReason { + Start, + Restart, + Interrupt, + New, + Connect +} + +export interface IAddedSysInfo { + type: SysInfoReason; + id: string; + sysInfoCell: ICell; + notebookIdentity: Uri; +} + +export interface IFinishCell { + cell: ICell; + notebookIdentity: Uri; +} + +export interface IExecuteInfo { + code: string; + id: string; + file: string; + line: number; + debug: boolean; +} + +export interface IRemoteAddCode extends IExecuteInfo { + originator: string; +} + +export interface IRemoteReexecuteCode extends IExecuteInfo { + originator: string; +} + +export interface ISubmitNewCell { + code: string; + id: string; +} + +export interface IReExecuteCells { + cellIds: string[]; +} + +export interface IProvideCompletionItemsRequest { + position: monacoEditor.Position; + context: monacoEditor.languages.CompletionContext; + requestId: string; + cellId: string; +} + +export interface IProvideHoverRequest { + position: monacoEditor.Position; + requestId: string; + cellId: string; + wordAtPosition: string | undefined; +} + +export interface IProvideSignatureHelpRequest { + position: monacoEditor.Position; + context: monacoEditor.languages.SignatureHelpContext; + requestId: string; + cellId: string; +} + +export interface ICancelIntellisenseRequest { + requestId: string; +} + +export interface IResolveCompletionItemRequest { + position: monacoEditor.Position; + item: monacoEditor.languages.CompletionItem; + requestId: string; + cellId: string; +} + +export interface IProvideCompletionItemsResponse { + list: monacoEditor.languages.CompletionList; + requestId: string; +} + +export interface IProvideHoverResponse { + hover: monacoEditor.languages.Hover; + requestId: string; +} + +export interface IProvideSignatureHelpResponse { + signatureHelp: monacoEditor.languages.SignatureHelp; + requestId: string; +} + +export interface IResolveCompletionItemResponse { + item: monacoEditor.languages.CompletionItem; + requestId: string; +} + +export interface IPosition { + line: number; + ch: number; +} + +export interface IEditCell { + changes: monacoEditor.editor.IModelContentChange[]; + id: string; +} + +export interface IAddCell { + fullText: string; + currentText: string; + cell: ICell; +} + +export interface IRemoveCell { + id: string; +} + +export interface ISwapCells { + firstCellId: string; + secondCellId: string; +} + +export interface IInsertCell { + cell: ICell; + code: string; + index: number; + codeCellAboveId: string | undefined; +} + +export interface IShowDataViewer { + variable: IJupyterVariable; + columnSize: number; +} + +export interface IRefreshVariablesRequest { + newExecutionCount?: number; +} + +export interface ILoadAllCells { + cells: ICell[]; + isNotebookTrusted?: boolean; +} + +export interface IScrollToCell { + id: string; +} + +export interface INotebookIdentity { + resource: Uri; + type: 'interactive' | 'native'; +} + +export interface ISaveAll { + cells: ICell[]; +} + +export interface INativeCommand { + command: NativeKeyboardCommandTelemetry | NativeMouseCommandTelemetry; +} + +export interface IRenderComplete { + ids: string[]; +} + +export interface IDebugStateChange { + oldState: DebugState; + newState: DebugState; +} + +export interface IFocusedCellEditor { + cellId: string; +} + +export interface INotebookModelChange { + oldDirty: boolean; + newDirty: boolean; + source: 'undo' | 'user' | 'redo'; + model?: INotebookModel; +} + +export interface INotebookModelSaved extends INotebookModelChange { + kind: 'save'; +} +export interface INotebookModelSavedAs extends INotebookModelChange { + kind: 'saveAs'; + target: Uri; + sourceUri: Uri; +} + +export interface INotebookModelRemoveAllChange extends INotebookModelChange { + kind: 'remove_all'; + oldCells: ICell[]; + newCellId: string; +} +export interface INotebookModelModifyChange extends INotebookModelChange { + kind: 'modify'; + newCells: ICell[]; + oldCells: ICell[]; +} +export interface INotebookModelCellExecutionCountChange extends INotebookModelChange { + kind: 'updateCellExecutionCount'; + cellId: string; + executionCount?: number; +} + +export interface INotebookModelClearChange extends INotebookModelChange { + kind: 'clear'; + oldCells: ICell[]; +} + +export interface INotebookModelSwapChange extends INotebookModelChange { + kind: 'swap'; + firstCellId: string; + secondCellId: string; +} + +export interface INotebookModelRemoveChange extends INotebookModelChange { + kind: 'remove'; + cell: ICell; + index: number; +} + +export interface INotebookModelInsertChange extends INotebookModelChange { + kind: 'insert'; + cell: ICell; + index: number; + codeCellAboveId?: string; +} + +export interface INotebookModelAddChange extends INotebookModelChange { + kind: 'add'; + cell: ICell; + fullText: string; + currentText: string; +} + +export interface INotebookModelChangeTypeChange extends INotebookModelChange { + kind: 'changeCellType'; + cell: ICell; +} + +export interface IEditorPosition { + /** + * line number (starts at 1) + */ + readonly lineNumber: number; + /** + * column (the first character in a line is between column 1 and column 2) + */ + readonly column: number; +} + +export interface IEditorRange { + /** + * Line number on which the range starts (starts at 1). + */ + readonly startLineNumber: number; + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + readonly startColumn: number; + /** + * Line number on which the range ends. + */ + readonly endLineNumber: number; + /** + * Column on which the range ends in line `endLineNumber`. + */ + readonly endColumn: number; +} + +export interface IEditorContentChange { + /** + * The range that got replaced. + */ + readonly range: IEditorRange; + /** + * The offset of the range that got replaced. + */ + readonly rangeOffset: number; + /** + * The length of the range that got replaced. + */ + readonly rangeLength: number; + /** + * The new text for the range. + */ + readonly text: string; + /** + * The cursor position to be set after the change + */ + readonly position: IEditorPosition; +} + +export interface INotebookModelEditChange extends INotebookModelChange { + kind: 'edit'; + forward: IEditorContentChange[]; + reverse: IEditorContentChange[]; + id: string; +} + +export interface INotebookModelVersionChange extends INotebookModelChange { + kind: 'version'; + kernelConnection?: KernelConnectionMetadata; +} + +export type NotebookModelChange = + | INotebookModelSaved + | INotebookModelSavedAs + | INotebookModelModifyChange + | INotebookModelRemoveAllChange + | INotebookModelClearChange + | INotebookModelSwapChange + | INotebookModelRemoveChange + | INotebookModelInsertChange + | INotebookModelAddChange + | INotebookModelEditChange + | INotebookModelVersionChange + | INotebookModelChangeTypeChange + | INotebookModelCellExecutionCountChange; + +export interface IRunByLine { + cell: ICell; + expectedExecutionCount: number; +} + +export interface ILoadTmLanguageResponse { + languageId: string; + scopeName: string; // Name in the tmlanguage scope file (scope.python instead of python) + // tslint:disable-next-line: no-any + languageConfiguration: ILanguageConfigurationDto; + languageJSON: string; // Contents of the tmLanguage.json file + extensions: string[]; // Array of file extensions that map to this language +} + +// Map all messages to specific payloads +export class IInteractiveWindowMapping { + public [IPyWidgetMessages.IPyWidgets_kernelOptions]: KernelSocketOptions; + public [IPyWidgetMessages.IPyWidgets_WidgetScriptSourceRequest]: { moduleName: string; moduleVersion: string }; + public [IPyWidgetMessages.IPyWidgets_WidgetScriptSourceResponse]: WidgetScriptSource; + public [IPyWidgetMessages.IPyWidgets_Ready]: never | undefined; + public [IPyWidgetMessages.IPyWidgets_onRestartKernel]: never | undefined; + public [IPyWidgetMessages.IPyWidgets_onKernelChanged]: never | undefined; + public [IPyWidgetMessages.IPyWidgets_registerCommTarget]: string; + // tslint:disable-next-line: no-any + public [IPyWidgetMessages.IPyWidgets_binary_msg]: { id: string; data: any }; + public [IPyWidgetMessages.IPyWidgets_msg]: { id: string; data: string }; + public [IPyWidgetMessages.IPyWidgets_msg_received]: { id: string }; + public [IPyWidgetMessages.IPyWidgets_iopub_msg_handled]: { id: string }; + public [IPyWidgetMessages.IPyWidgets_RegisterMessageHook]: string; + public [IPyWidgetMessages.IPyWidgets_ExtensionOperationHandled]: { id: string; type: IPyWidgetMessages }; + public [IPyWidgetMessages.IPyWidgets_RemoveMessageHook]: { hookMsgId: string; lastHookedMsgId: string | undefined }; + public [IPyWidgetMessages.IPyWidgets_MessageHookCall]: { + requestId: string; + parentId: string; + msg: KernelMessage.IIOPubMessage; + }; + public [IPyWidgetMessages.IPyWidgets_MessageHookResult]: { + requestId: string; + parentId: string; + msgType: string; + result: boolean; + }; + public [IPyWidgetMessages.IPyWidgets_mirror_execute]: { id: string; msg: KernelMessage.IExecuteRequestMsg }; + public [InteractiveWindowMessages.StartCell]: ICell; + public [InteractiveWindowMessages.ForceVariableRefresh]: never | undefined; + public [InteractiveWindowMessages.FinishCell]: IFinishCell; + public [InteractiveWindowMessages.UpdateCellWithExecutionResults]: ICell; + public [InteractiveWindowMessages.GotoCodeCell]: IGotoCode; + public [InteractiveWindowMessages.CopyCodeCell]: ICopyCode; + public [InteractiveWindowMessages.NotebookExecutionActivated]: INotebookIdentity & { owningResource: Resource }; + public [InteractiveWindowMessages.RestartKernel]: never | undefined; + public [InteractiveWindowMessages.SelectKernel]: IServerState | undefined; + public [InteractiveWindowMessages.SelectJupyterServer]: never | undefined; + public [InteractiveWindowMessages.OpenSettings]: string | undefined; + public [InteractiveWindowMessages.Export]: ICell[]; + public [InteractiveWindowMessages.ExportNotebookAs]: ICell[]; + public [InteractiveWindowMessages.GetAllCells]: never | undefined; + public [InteractiveWindowMessages.ReturnAllCells]: ICell[]; + public [InteractiveWindowMessages.DeleteAllCells]: IAddCellAction; + public [InteractiveWindowMessages.Undo]: never | undefined; + public [InteractiveWindowMessages.Redo]: never | undefined; + public [InteractiveWindowMessages.ExpandAll]: never | undefined; + public [InteractiveWindowMessages.CollapseAll]: never | undefined; + public [InteractiveWindowMessages.StartProgress]: never | undefined; + public [InteractiveWindowMessages.StopProgress]: never | undefined; + public [InteractiveWindowMessages.Interrupt]: never | undefined; + public [InteractiveWindowMessages.SettingsUpdated]: string; + public [InteractiveWindowMessages.SubmitNewCell]: ISubmitNewCell; + public [InteractiveWindowMessages.SendInfo]: IInteractiveWindowInfo; + public [InteractiveWindowMessages.Started]: never | undefined; + public [InteractiveWindowMessages.AddedSysInfo]: IAddedSysInfo; + public [InteractiveWindowMessages.RemoteAddCode]: IRemoteAddCode; + public [InteractiveWindowMessages.RemoteReexecuteCode]: IRemoteReexecuteCode; + public [InteractiveWindowMessages.Activate]: never | undefined; + public [InteractiveWindowMessages.ShowDataViewer]: IShowDataViewer; + public [InteractiveWindowMessages.GetVariablesRequest]: IJupyterVariablesRequest; + public [InteractiveWindowMessages.GetVariablesResponse]: IJupyterVariablesResponse; + public [InteractiveWindowMessages.VariableExplorerToggle]: boolean; + public [InteractiveWindowMessages.SetVariableExplorerHeight]: IVariableExplorerHeight; + public [InteractiveWindowMessages.VariableExplorerHeightResponse]: IVariableExplorerHeight; + public [CssMessages.GetCssRequest]: IGetCssRequest; + public [CssMessages.GetCssResponse]: IGetCssResponse; + public [CssMessages.GetMonacoThemeRequest]: IGetMonacoThemeRequest; + public [CssMessages.GetMonacoThemeResponse]: IGetMonacoThemeResponse; + public [InteractiveWindowMessages.ProvideCompletionItemsRequest]: IProvideCompletionItemsRequest; + public [InteractiveWindowMessages.CancelCompletionItemsRequest]: ICancelIntellisenseRequest; + public [InteractiveWindowMessages.ProvideCompletionItemsResponse]: IProvideCompletionItemsResponse; + public [InteractiveWindowMessages.ProvideHoverRequest]: IProvideHoverRequest; + public [InteractiveWindowMessages.CancelHoverRequest]: ICancelIntellisenseRequest; + public [InteractiveWindowMessages.ProvideHoverResponse]: IProvideHoverResponse; + public [InteractiveWindowMessages.ProvideSignatureHelpRequest]: IProvideSignatureHelpRequest; + public [InteractiveWindowMessages.CancelSignatureHelpRequest]: ICancelIntellisenseRequest; + public [InteractiveWindowMessages.ProvideSignatureHelpResponse]: IProvideSignatureHelpResponse; + public [InteractiveWindowMessages.ResolveCompletionItemRequest]: IResolveCompletionItemRequest; + public [InteractiveWindowMessages.CancelResolveCompletionItemRequest]: ICancelIntellisenseRequest; + public [InteractiveWindowMessages.ResolveCompletionItemResponse]: IResolveCompletionItemResponse; + public [InteractiveWindowMessages.LoadOnigasmAssemblyRequest]: never | undefined; + public [InteractiveWindowMessages.LoadOnigasmAssemblyResponse]: Buffer; + public [InteractiveWindowMessages.LoadTmLanguageRequest]: string; + public [InteractiveWindowMessages.LoadTmLanguageResponse]: ILoadTmLanguageResponse; + public [InteractiveWindowMessages.OpenLink]: string | undefined; + public [InteractiveWindowMessages.ShowPlot]: string | undefined; + public [InteractiveWindowMessages.SavePng]: string | undefined; + public [InteractiveWindowMessages.StartDebugging]: never | undefined; + public [InteractiveWindowMessages.StopDebugging]: never | undefined; + public [InteractiveWindowMessages.GatherCode]: ICell; + public [InteractiveWindowMessages.GatherCodeToScript]: ICell; + public [InteractiveWindowMessages.Gathering]: IChangeGatherStatus; + public [InteractiveWindowMessages.LaunchNotebookTrustPrompt]: never | undefined; + public [InteractiveWindowMessages.TrustNotebookComplete]: never | undefined; + public [InteractiveWindowMessages.LoadAllCells]: ILoadAllCells; + public [InteractiveWindowMessages.LoadAllCellsComplete]: ILoadAllCells; + public [InteractiveWindowMessages.ScrollToCell]: IScrollToCell; + public [InteractiveWindowMessages.ReExecuteCells]: IReExecuteCells; + public [InteractiveWindowMessages.NotebookIdentity]: INotebookIdentity; + public [InteractiveWindowMessages.NotebookClose]: INotebookIdentity; + public [InteractiveWindowMessages.NotebookDirty]: never | undefined; + public [InteractiveWindowMessages.NotebookClean]: never | undefined; + public [InteractiveWindowMessages.SaveAll]: ISaveAll; + public [InteractiveWindowMessages.Sync]: { + type: InteractiveWindowMessages | SharedMessages | CommonActionType; + // tslint:disable-next-line: no-any + payload: BaseReduxActionPayload; + }; + public [InteractiveWindowMessages.NativeCommand]: INativeCommand; + public [InteractiveWindowMessages.VariablesComplete]: never | undefined; + public [InteractiveWindowMessages.NotebookRunAllCells]: never | undefined; + public [InteractiveWindowMessages.NotebookRunSelectedCell]: never | undefined; + public [InteractiveWindowMessages.NotebookAddCellBelow]: IAddCellAction; + public [InteractiveWindowMessages.DoSave]: never | undefined; + public [InteractiveWindowMessages.ExecutionRendered]: never | undefined; + public [InteractiveWindowMessages.FocusedCellEditor]: IFocusedCellEditor; + public [InteractiveWindowMessages.SelectedCell]: IFocusedCellEditor; + public [InteractiveWindowMessages.OutputToggled]: never | undefined; + public [InteractiveWindowMessages.UnfocusedCellEditor]: never | undefined; + public [InteractiveWindowMessages.MonacoReady]: never | undefined; + public [InteractiveWindowMessages.ClearAllOutputs]: never | undefined; + public [InteractiveWindowMessages.UpdateKernel]: IServerState | undefined; + public [InteractiveWindowMessages.UpdateModel]: NotebookModelChange; + public [InteractiveWindowMessages.ReceivedUpdateModel]: never | undefined; + public [SharedMessages.UpdateSettings]: string; + public [SharedMessages.LocInit]: string; + public [InteractiveWindowMessages.UpdateDisplayData]: KernelMessage.IUpdateDisplayDataMsg; + public [InteractiveWindowMessages.IPyWidgetLoadSuccess]: LoadIPyWidgetClassLoadAction; + public [InteractiveWindowMessages.IPyWidgetLoadFailure]: ILoadIPyWidgetClassFailureAction; + public [InteractiveWindowMessages.IPyWidgetWidgetVersionNotSupported]: NotifyIPyWidgeWidgetVersionNotSupportedAction; + public [InteractiveWindowMessages.ConvertUriForUseInWebViewRequest]: Uri; + public [InteractiveWindowMessages.ConvertUriForUseInWebViewResponse]: { request: Uri; response: Uri }; + public [InteractiveWindowMessages.IPyWidgetRenderFailure]: Error; + public [InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage]: KernelMessage.IMessage; + public [InteractiveWindowMessages.RunByLine]: IRunByLine; + public [InteractiveWindowMessages.Continue]: never | undefined; + public [InteractiveWindowMessages.ShowBreak]: { frames: DebugProtocol.StackFrame[]; cell: ICell }; + public [InteractiveWindowMessages.ShowContinue]: ICell; + public [InteractiveWindowMessages.Step]: never | undefined; + public [InteractiveWindowMessages.ShowingIp]: never | undefined; + public [InteractiveWindowMessages.KernelIdle]: never | undefined; + public [InteractiveWindowMessages.DebugStateChange]: IDebugStateChange; + public [InteractiveWindowMessages.HasCell]: string; + public [InteractiveWindowMessages.HasCellResponse]: { id: string; result: boolean }; +} diff --git a/src/client/datascience/interactive-common/linkProvider.ts b/src/client/datascience/interactive-common/linkProvider.ts new file mode 100644 index 000000000000..ac14dd430fae --- /dev/null +++ b/src/client/datascience/interactive-common/linkProvider.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; +import { commands, Event, EventEmitter, Position, Range, Selection, TextEditorRevealType, Uri } from 'vscode'; + +import { IApplicationShell, ICommandManager, IDocumentManager } from '../../common/application/types'; + +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { IDataScienceFileSystem, IInteractiveWindowListener } from '../types'; +import { InteractiveWindowMessages } from './interactiveWindowTypes'; + +const LineQueryRegex = /line=(\d+)/; + +// The following list of commands represent those that can be executed +// in a markdown cell using the syntax: https://command:[my.vscode.command]. +const linkCommandAllowList = [ + 'python.datascience.gatherquality', + 'python.datascience.latestExtension', + 'python.datascience.enableLoadingWidgetScriptsFromThirdPartySource' +]; + +// tslint:disable: no-any +@injectable() +export class LinkProvider implements IInteractiveWindowListener { + private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ + message: string; + payload: any; + }>(); + constructor( + @inject(IApplicationShell) private applicationShell: IApplicationShell, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(ICommandManager) private commandManager: ICommandManager + ) { + noop(); + } + + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + + public onMessage(message: string, payload?: any): void { + switch (message) { + case InteractiveWindowMessages.OpenLink: + if (payload) { + // Special case file URIs + const href: string = payload.toString(); + if (href.startsWith('file')) { + this.openFile(href); + } else if (href.startsWith('https://command:')) { + const temp: string = href.split(':')[2]; + const params: string[] = temp.includes('/?') ? temp.split('/?')[1].split(',') : []; + let command = temp.split('/?')[0]; + if (command.endsWith('/')) { + command = command.substring(0, command.length - 1); + } + if (linkCommandAllowList.includes(command)) { + commands.executeCommand(command, params); + } + } else { + this.applicationShell.openUrl(href); + } + } + break; + case InteractiveWindowMessages.SavePng: + if (payload) { + // Payload should contain the base 64 encoded string. Ask the user to save the file + const filtersObject: Record = {}; + filtersObject[localize.DataScience.pngFilter()] = ['png']; + + // Ask the user what file to save to + this.applicationShell + .showSaveDialog({ + saveLabel: localize.DataScience.savePngTitle(), + filters: filtersObject + }) + .then((f) => { + if (f) { + const buffer = new Buffer(payload.replace('data:image/png;base64', ''), 'base64'); + this.fs.writeFile(f, buffer).ignoreErrors(); + } + }); + } + break; + default: + break; + } + } + public dispose(): void | undefined { + noop(); + } + + private openFile(fileUri: string) { + const uri = Uri.parse(fileUri); + let selection: Range = new Range(new Position(0, 0), new Position(0, 0)); + if (uri.query) { + // Might have a line number query on the file name + const lineMatch = LineQueryRegex.exec(uri.query); + if (lineMatch) { + const lineNumber = parseInt(lineMatch[1], 10); + selection = new Range(new Position(lineNumber, 0), new Position(lineNumber, 0)); + } + } + + // Show the matching editor if there is one + let editor = this.documentManager.visibleTextEditors.find((e) => this.fs.arePathsSame(e.document.uri, uri)); + if (editor) { + this.documentManager + .showTextDocument(editor.document, { selection, viewColumn: editor.viewColumn }) + .then((e) => { + e.revealRange(selection, TextEditorRevealType.InCenter); + }); + } else { + // Not a visible editor, try opening otherwise + this.commandManager.executeCommand('vscode.open', uri).then(() => { + // See if that opened a text document + editor = this.documentManager.visibleTextEditors.find((e) => this.fs.arePathsSame(e.document.uri, uri)); + if (editor) { + // Force the selection to change + editor.revealRange(selection); + editor.selection = new Selection(selection.start, selection.start); + } + }); + } + } +} diff --git a/src/client/datascience/interactive-common/notebookProvider.ts b/src/client/datascience/interactive-common/notebookProvider.ts new file mode 100644 index 000000000000..23a8cda190cb --- /dev/null +++ b/src/client/datascience/interactive-common/notebookProvider.ts @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { EventEmitter, Uri } from 'vscode'; +import { ServerStatus } from '../../../datascience-ui/interactive-common/mainState'; +import { IWorkspaceService } from '../../common/application/types'; +import { traceWarning } from '../../common/logger'; +import { IDisposableRegistry, Resource } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { Identifiers } from '../constants'; +import { KernelConnectionMetadata } from '../jupyter/kernels/types'; +import { + ConnectNotebookProviderOptions, + GetNotebookOptions, + IJupyterNotebookProvider, + INotebook, + INotebookProvider, + INotebookProviderConnection, + IRawNotebookProvider +} from '../types'; + +@injectable() +export class NotebookProvider implements INotebookProvider { + private readonly notebooks = new Map>(); + private _notebookCreated = new EventEmitter<{ identity: Uri; notebook: INotebook }>(); + private readonly _onSessionStatusChanged = new EventEmitter<{ status: ServerStatus; notebook: INotebook }>(); + private _connectionMade = new EventEmitter(); + private _potentialKernelChanged = new EventEmitter<{ identity: Uri; kernelConnection: KernelConnectionMetadata }>(); + private _type: 'jupyter' | 'raw' = 'jupyter'; + public get activeNotebooks() { + return [...this.notebooks.values()]; + } + public get onSessionStatusChanged() { + return this._onSessionStatusChanged.event; + } + public get onPotentialKernelChanged() { + return this._potentialKernelChanged.event; + } + constructor( + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IRawNotebookProvider) private readonly rawNotebookProvider: IRawNotebookProvider, + @inject(IJupyterNotebookProvider) private readonly jupyterNotebookProvider: IJupyterNotebookProvider, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService + ) { + this.rawNotebookProvider + .supported() + .then((b) => (this._type = b ? 'raw' : 'jupyter')) + .ignoreErrors(); + } + public get onNotebookCreated() { + return this._notebookCreated.event; + } + + public get onConnectionMade() { + return this._connectionMade.event; + } + + public get type(): 'jupyter' | 'raw' { + return this._type; + } + + // Disconnect from the specified provider + public async disconnect(options: ConnectNotebookProviderOptions): Promise { + // Only need to disconnect from actual jupyter servers + if (!(await this.rawNotebookProvider.supported())) { + return this.jupyterNotebookProvider.disconnect(options); + } + } + + // Attempt to connect to our server provider, and if we do, return the connection info + public async connect(options: ConnectNotebookProviderOptions): Promise { + // Connect to either a jupyter server or a stubbed out raw notebook "connection" + if (await this.rawNotebookProvider.supported()) { + return this.rawNotebookProvider.connect({ + ...options, + onConnectionMade: this.fireConnectionMade.bind(this) + }); + } else { + return this.jupyterNotebookProvider.connect({ + ...options, + onConnectionMade: this.fireConnectionMade.bind(this) + }); + } + } + public disposeAssociatedNotebook(options: { identity: Uri }) { + const nbPromise = this.notebooks.get(options.identity.toString()); + if (!nbPromise) { + return; + } + this.notebooks.delete(options.identity.toString()); + nbPromise + .then((nb) => nb.dispose()) + .catch((ex) => traceWarning('Failed to dispose notebook in disposeAssociatedNotebook', ex)); + } + public async getOrCreateNotebook(options: GetNotebookOptions): Promise { + const rawKernel = await this.rawNotebookProvider.supported(); + + // Check our own promise cache + if (this.notebooks.get(options.identity.toString())) { + return this.notebooks.get(options.identity.toString())!!; + } + + // Check to see if our provider already has this notebook + const notebook = rawKernel + ? await this.rawNotebookProvider.getNotebook(options.identity, options.token) + : await this.jupyterNotebookProvider.getNotebook(options); + if (notebook) { + this.cacheNotebookPromise(options.identity, Promise.resolve(notebook)); + return notebook; + } + + // If get only, don't create a notebook + if (options.getOnly) { + return undefined; + } + + // We want to cache a Promise from the create functions + // but jupyterNotebookProvider.createNotebook can be undefined if the server is not available + // so check for our connection here first + if (!rawKernel) { + if (!(await this.jupyterNotebookProvider.connect(options))) { + return undefined; + } + } + + // Finally create if needed + let resource: Resource = options.resource; + if (options.identity.scheme === Identifiers.HistoryPurpose && !resource) { + // If we have any workspaces, then use the first available workspace. + // This is required, else using `undefined` as a resource when we have worksapce folders is a different meaning. + // This means interactive window doesn't properly support mult-root workspaces as we pick first workspace. + // Ideally we need to pick the resource of the corresponding Python file. + resource = this.workspaceService.hasWorkspaceFolders + ? this.workspaceService.workspaceFolders![0]!.uri + : undefined; + } + const promise = rawKernel + ? this.rawNotebookProvider.createNotebook( + options.identity, + resource, + options.disableUI, + options.metadata, + options.token + ) + : this.jupyterNotebookProvider.createNotebook(options); + + this.cacheNotebookPromise(options.identity, promise); + + return promise; + } + + // This method is here so that the kernel selector can pick a kernel and not have + // to know about any of the UI that's active. + public firePotentialKernelChanged(identity: Uri, kernel: KernelConnectionMetadata) { + this._potentialKernelChanged.fire({ identity, kernelConnection: kernel }); + } + + private fireConnectionMade() { + this._connectionMade.fire(); + } + + // Cache the promise that will return a notebook + private cacheNotebookPromise(identity: Uri, promise: Promise) { + this.notebooks.set(identity.toString(), promise); + + // Remove promise from cache if the same promise still exists. + const removeFromCache = () => { + const cachedPromise = this.notebooks.get(identity.toString()); + if (cachedPromise === promise) { + this.notebooks.delete(identity.toString()); + } + }; + + promise + .then((nb) => { + // If the notebook is disposed, remove from cache. + nb.onDisposed(removeFromCache); + nb.onSessionStatusChanged( + (e) => this._onSessionStatusChanged.fire({ status: e, notebook: nb }), + this, + this.disposables + ); + this._notebookCreated.fire({ identity: identity, notebook: nb }); + }) + .catch(noop); + + // If promise fails, then remove the promise from cache. + promise.catch(removeFromCache); + } +} diff --git a/src/client/datascience/interactive-common/notebookServerProvider.ts b/src/client/datascience/interactive-common/notebookServerProvider.ts new file mode 100644 index 000000000000..c2f50a9ae286 --- /dev/null +++ b/src/client/datascience/interactive-common/notebookServerProvider.ts @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationToken, ConfigurationTarget, EventEmitter, Uri } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { CancellationError, wrapCancellationTokens } from '../../common/cancellation'; +import { traceInfo } from '../../common/logger'; +import { IConfigurationService } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Identifiers, Settings, Telemetry } from '../constants'; +import { JupyterInstallError } from '../jupyter/jupyterInstallError'; +import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; +import { JupyterZMQBinariesNotFoundError } from '../jupyter/jupyterZMQBinariesNotFoundError'; +import { ProgressReporter } from '../progress/progressReporter'; +import { + GetServerOptions, + IJupyterExecution, + IJupyterServerProvider, + INotebook, + INotebookServer, + INotebookServerOptions +} from '../types'; + +@injectable() +export class NotebookServerProvider implements IJupyterServerProvider { + private serverPromise: Promise | undefined; + private allowingUI = false; + private _notebookCreated = new EventEmitter<{ identity: Uri; notebook: INotebook }>(); + constructor( + @inject(ProgressReporter) private readonly progressReporter: ProgressReporter, + @inject(IConfigurationService) private readonly configuration: IConfigurationService, + @inject(IJupyterExecution) private readonly jupyterExecution: IJupyterExecution, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService + ) {} + public get onNotebookCreated() { + return this._notebookCreated.event; + } + + public async getOrCreateServer( + options: GetServerOptions, + token?: CancellationToken + ): Promise { + const serverOptions = this.getNotebookServerOptions(); + + // If we are just fetching or only want to create for local, see if exists + if (options.getOnly || (options.localOnly && serverOptions.uri)) { + return this.jupyterExecution.getServer(serverOptions); + } else { + // Otherwise create a new server + return this.createServer(options, token).then((val) => { + // If we created a new server notify of our first time provider connection + if (val && options.onConnectionMade) { + options.onConnectionMade(); + } + + return val; + }); + } + } + + private async createServer( + options: GetServerOptions, + token?: CancellationToken + ): Promise { + // When we finally try to create a server, update our flag indicating if we're going to allow UI or not. This + // allows the server to be attempted without a UI, but a future request can come in and use the same startup + this.allowingUI = options.disableUI ? this.allowingUI : true; + + if (!this.serverPromise) { + // Start a server + this.serverPromise = this.startServer(token); + } + try { + return await this.serverPromise; + } catch (e) { + // Don't cache the error + this.serverPromise = undefined; + throw e; + } + } + + private async startServer(token?: CancellationToken): Promise { + const serverOptions = this.getNotebookServerOptions(); + + traceInfo(`Checking for server existence.`); + + // Status depends upon if we're about to connect to existing server or not. + const progressReporter = this.allowingUI + ? (await this.jupyterExecution.getServer(serverOptions)) + ? this.progressReporter.createProgressIndicator(localize.DataScience.connectingToJupyter()) + : this.progressReporter.createProgressIndicator(localize.DataScience.startingJupyter()) + : undefined; + + // Check to see if we support ipykernel or not + try { + traceInfo(`Checking for server usability.`); + + const usable = await this.checkUsable(serverOptions); + if (!usable) { + traceInfo('Server not usable (should ask for install now)'); + // Indicate failing. + throw new JupyterInstallError( + localize.DataScience.jupyterNotSupported().format(await this.jupyterExecution.getNotebookError()), + localize.DataScience.pythonInteractiveHelpLink() + ); + } + // Then actually start the server + traceInfo(`Starting notebook server.`); + const result = await this.jupyterExecution.connectToNotebookServer( + serverOptions, + wrapCancellationTokens(progressReporter?.token, token) + ); + traceInfo(`Server started.`); + return result; + } catch (e) { + progressReporter?.dispose(); // NOSONAR + // If user cancelled, then do nothing. + if (progressReporter && progressReporter.token.isCancellationRequested && e instanceof CancellationError) { + return; + } + + // Also tell jupyter execution to reset its search. Otherwise we've just cached + // the failure there + await this.jupyterExecution.refreshCommands(); + + if (e instanceof JupyterSelfCertsError) { + // On a self cert error, warn the user and ask if they want to change the setting + const enableOption: string = localize.DataScience.jupyterSelfCertEnable(); + const closeOption: string = localize.DataScience.jupyterSelfCertClose(); + this.applicationShell + .showErrorMessage( + localize.DataScience.jupyterSelfCertFail().format(e.message), + enableOption, + closeOption + ) + .then((value) => { + if (value === enableOption) { + sendTelemetryEvent(Telemetry.SelfCertsMessageEnabled); + this.configuration + .updateSetting( + 'dataScience.allowUnauthorizedRemoteConnection', + true, + undefined, + ConfigurationTarget.Workspace + ) + .ignoreErrors(); + } else if (value === closeOption) { + sendTelemetryEvent(Telemetry.SelfCertsMessageClose); + } + }); + throw e; + } else { + throw e; + } + } finally { + progressReporter?.dispose(); // NOSONAR + } + } + + private async checkUsable(options: INotebookServerOptions): Promise { + try { + if (options && !options.uri) { + const usableInterpreter = await this.jupyterExecution.getUsableJupyterPython(); + return usableInterpreter ? true : false; + } else { + return true; + } + } catch (e) { + if (e instanceof JupyterZMQBinariesNotFoundError) { + throw e; + } + const activeInterpreter = await this.interpreterService.getActiveInterpreter(undefined); + // Can't find a usable interpreter, show the error. + if (activeInterpreter) { + const displayName = activeInterpreter.displayName + ? activeInterpreter.displayName + : activeInterpreter.path; + throw new Error( + localize.DataScience.jupyterNotSupportedBecauseOfEnvironment().format(displayName, e.toString()) + ); + } else { + throw new JupyterInstallError( + localize.DataScience.jupyterNotSupported().format(await this.jupyterExecution.getNotebookError()), + localize.DataScience.pythonInteractiveHelpLink() + ); + } + } + } + + private getNotebookServerOptions(): INotebookServerOptions { + // Since there's one server per session, don't use a resource to figure out these settings + const settings = this.configuration.getSettings(undefined); + let serverURI: string | undefined = settings.datascience.jupyterServerURI; + const useDefaultConfig: boolean | undefined = settings.datascience.useDefaultConfigForJupyter; + + // For the local case pass in our URI as undefined, that way connect doesn't have to check the setting + if (serverURI.toLowerCase() === Settings.JupyterServerLocalLaunch) { + serverURI = undefined; + } + + return { + uri: serverURI, + skipUsingDefaultConfig: !useDefaultConfig, + purpose: Identifiers.HistoryPurpose, + allowUI: this.allowUI.bind(this) + }; + } + + private allowUI(): boolean { + return this.allowingUI; + } +} diff --git a/src/client/datascience/interactive-common/notebookUsageTracker.ts b/src/client/datascience/interactive-common/notebookUsageTracker.ts new file mode 100644 index 000000000000..cef91dcddaa7 --- /dev/null +++ b/src/client/datascience/interactive-common/notebookUsageTracker.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { EventEmitter } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IWorkspaceService } from '../../common/application/types'; +import { IDisposableRegistry } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { INotebookEditor, INotebookEditorProvider } from '../types'; + +/** + * This class tracks opened notebooks, # of notebooks in workspace & # of executed notebooks. + */ +@injectable() +export class NotebookUsageTracker implements IExtensionSingleActivationService { + protected readonly _onDidChangeActiveNotebookEditor = new EventEmitter(); + protected readonly _onDidOpenNotebookEditor = new EventEmitter(); + private readonly executedEditors = new Set(); + private notebookCount: number = 0; + private openedNotebookCount: number = 0; + constructor( + @inject(INotebookEditorProvider) private readonly editorProvider: INotebookEditorProvider, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry + ) {} + + public async activate(): Promise { + // Look through the file system for ipynb files to see how many we have in the workspace. Don't wait + // on this though. + const findFilesPromise = this.workspace.findFiles('**/*.ipynb'); + if (findFilesPromise && findFilesPromise.then) { + findFilesPromise.then((r) => (this.notebookCount += r.length)); + } + this.editorProvider.onDidOpenNotebookEditor(this.onEditorOpened, this, this.disposables); + } + public dispose() { + // Send a bunch of telemetry + if (this.openedNotebookCount) { + sendTelemetryEvent(Telemetry.NotebookOpenCount, undefined, { count: this.openedNotebookCount }); + } + if (this.executedEditors.size) { + sendTelemetryEvent(Telemetry.NotebookRunCount, undefined, { count: this.executedEditors.size }); + } + if (this.notebookCount) { + sendTelemetryEvent(Telemetry.NotebookWorkspaceCount, undefined, { count: this.notebookCount }); + } + } + private onEditorOpened(editor: INotebookEditor): void { + this.openedNotebookCount += 1; + if (editor.model?.isUntitled) { + this.notebookCount += 1; + } + if (!this.executedEditors.has(editor)) { + editor.executed((e) => this.executedEditors.add(e), this, this.disposables); + } + } +} diff --git a/src/client/datascience/interactive-common/serialization.ts b/src/client/datascience/interactive-common/serialization.ts new file mode 100644 index 000000000000..da853c44f6ea --- /dev/null +++ b/src/client/datascience/interactive-common/serialization.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CharacterPair, CommentRule, EnterAction, IndentationRule, LanguageConfiguration, OnEnterRule } from 'vscode'; + +// tslint:disable: no-any + +export interface IRegExpDto { + pattern: string; + flags?: string; +} +export interface IIndentationRuleDto { + decreaseIndentPattern: IRegExpDto; + increaseIndentPattern: IRegExpDto; + indentNextLinePattern?: IRegExpDto; + unIndentedLinePattern?: IRegExpDto; +} +export interface IOnEnterRuleDto { + beforeText: IRegExpDto; + afterText?: IRegExpDto; + oneLineAboveText?: IRegExpDto; + action: EnterAction; +} +export interface ILanguageConfigurationDto { + comments?: CommentRule; + brackets?: CharacterPair[]; + wordPattern?: IRegExpDto; + indentationRules?: IIndentationRuleDto; + onEnterRules?: IOnEnterRuleDto[]; + __electricCharacterSupport?: { + brackets?: any; + docComment?: { + scope: string; + open: string; + lineStart: string; + close?: string; + }; + }; + __characterPairSupport?: { + autoClosingPairs: { + open: string; + close: string; + notIn?: string[]; + }[]; + }; +} +// Copied most of this from VS code directly. + +function regExpFlags(regexp: RegExp): string { + return ( + (regexp.global ? 'g' : '') + + (regexp.ignoreCase ? 'i' : '') + + (regexp.multiline ? 'm' : '') + + ((regexp as any) /* standalone editor compilation */.unicode ? 'u' : '') + ); +} + +function _serializeRegExp(regExp: RegExp): IRegExpDto { + return { + pattern: regExp.source, + flags: regExpFlags(regExp) + }; +} + +function _serializeIndentationRule(indentationRule: IndentationRule): IIndentationRuleDto { + return { + decreaseIndentPattern: _serializeRegExp(indentationRule.decreaseIndentPattern), + increaseIndentPattern: _serializeRegExp(indentationRule.increaseIndentPattern), + indentNextLinePattern: indentationRule.indentNextLinePattern + ? _serializeRegExp(indentationRule.indentNextLinePattern) + : undefined, + unIndentedLinePattern: indentationRule.unIndentedLinePattern + ? _serializeRegExp(indentationRule.unIndentedLinePattern) + : undefined + }; +} + +function _serializeOnEnterRule(onEnterRule: OnEnterRule): IOnEnterRuleDto { + return { + beforeText: _serializeRegExp(onEnterRule.beforeText), + afterText: onEnterRule.afterText ? _serializeRegExp(onEnterRule.afterText) : undefined, + oneLineAboveText: (onEnterRule as any).oneLineAboveText + ? _serializeRegExp((onEnterRule as any).oneLineAboveText) + : undefined, + action: onEnterRule.action + }; +} + +function _serializeOnEnterRules(onEnterRules: OnEnterRule[]): IOnEnterRuleDto[] { + return onEnterRules.map(_serializeOnEnterRule); +} + +function _reviveRegExp(regExp: IRegExpDto): RegExp { + return new RegExp(regExp.pattern, regExp.flags); +} + +function _reviveIndentationRule(indentationRule: IIndentationRuleDto): IndentationRule { + return { + decreaseIndentPattern: _reviveRegExp(indentationRule.decreaseIndentPattern), + increaseIndentPattern: _reviveRegExp(indentationRule.increaseIndentPattern), + indentNextLinePattern: indentationRule.indentNextLinePattern + ? _reviveRegExp(indentationRule.indentNextLinePattern) + : undefined, + unIndentedLinePattern: indentationRule.unIndentedLinePattern + ? _reviveRegExp(indentationRule.unIndentedLinePattern) + : undefined + }; +} + +function _reviveOnEnterRule(onEnterRule: IOnEnterRuleDto): OnEnterRule { + return { + beforeText: _reviveRegExp(onEnterRule.beforeText), + afterText: onEnterRule.afterText ? _reviveRegExp(onEnterRule.afterText) : undefined, + oneLineAboveText: onEnterRule.oneLineAboveText ? _reviveRegExp(onEnterRule.oneLineAboveText) : undefined, + action: onEnterRule.action + } as any; +} + +function _reviveOnEnterRules(onEnterRules: IOnEnterRuleDto[]): OnEnterRule[] { + return onEnterRules.map(_reviveOnEnterRule); +} + +export function serializeLanguageConfiguration( + configuration: LanguageConfiguration | undefined +): ILanguageConfigurationDto { + return { + ...configuration, + wordPattern: configuration?.wordPattern ? _serializeRegExp(configuration.wordPattern) : undefined, + indentationRules: configuration?.indentationRules + ? _serializeIndentationRule(configuration.indentationRules) + : undefined, + onEnterRules: configuration?.onEnterRules ? _serializeOnEnterRules(configuration.onEnterRules) : undefined + }; +} + +export function deserializeLanguageConfiguration(configuration: ILanguageConfigurationDto): LanguageConfiguration { + return { + ...configuration, + wordPattern: configuration.wordPattern ? _reviveRegExp(configuration.wordPattern) : undefined, + indentationRules: configuration.indentationRules + ? _reviveIndentationRule(configuration.indentationRules) + : undefined, + onEnterRules: configuration.onEnterRules ? _reviveOnEnterRules(configuration.onEnterRules) : undefined + }; +} diff --git a/src/client/datascience/interactive-common/showPlotListener.ts b/src/client/datascience/interactive-common/showPlotListener.ts new file mode 100644 index 000000000000..7151c9ffd108 --- /dev/null +++ b/src/client/datascience/interactive-common/showPlotListener.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter } from 'vscode'; + +import { noop } from '../../common/utils/misc'; +import { IInteractiveWindowListener, IPlotViewerProvider } from '../types'; +import { InteractiveWindowMessages } from './interactiveWindowTypes'; + +// tslint:disable: no-any +@injectable() +export class ShowPlotListener implements IInteractiveWindowListener { + private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ + message: string; + payload: any; + }>(); + constructor(@inject(IPlotViewerProvider) private provider: IPlotViewerProvider) { + noop(); + } + + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + + public onMessage(message: string, payload?: any): void { + switch (message) { + case InteractiveWindowMessages.ShowPlot: + if (payload) { + this.provider.showPlot(payload).ignoreErrors(); + break; + } + break; + default: + break; + } + } + public dispose(): void | undefined { + noop(); + } +} diff --git a/src/client/datascience/interactive-common/synchronization.ts b/src/client/datascience/interactive-common/synchronization.ts new file mode 100644 index 000000000000..7a0fd4394ae2 --- /dev/null +++ b/src/client/datascience/interactive-common/synchronization.ts @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { + CommonActionType, + CommonActionTypeMapping +} from '../../../datascience-ui/interactive-common/redux/reducers/types'; +import { CssMessages, SharedMessages } from '../messages'; +import { + IInteractiveWindowMapping, + InteractiveWindowMessages, + IPyWidgetMessages, + NotebookModelChange +} from './interactiveWindowTypes'; + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export enum MessageType { + /** + * Action dispatched as result of some user action. + */ + other = 0, + /** + * Action dispatched to re-broadcast a message across other editors of the same file in the same session. + */ + syncAcrossSameNotebooks = 1 << 0, + /** + * Action dispatched to re-broadcast a message across other sessions (live share). + */ + syncWithLiveShare = 1 << 1, + noIdea = 1 << 2 +} + +// tslint:disable-next-line: no-any +type MessageAction = (payload: any) => boolean; + +type MessageMapping = { + [P in keyof T]: MessageType | MessageAction; +}; + +export type IInteractiveActionMapping = MessageMapping; + +// Do not change to a dictionary or a record. +// The current structure ensures all new enums added will be categorized. +// This way, if a new message is added, we'll make the decision early on whether it needs to be synchronized and how. +// Rather than waiting for users to report issues related to new messages. +const messageWithMessageTypes: MessageMapping & MessageMapping = { + [CommonActionType.ADD_AND_FOCUS_NEW_CELL]: MessageType.other, + [CommonActionType.ADD_NEW_CELL]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [CommonActionType.ARROW_DOWN]: MessageType.syncWithLiveShare, + [CommonActionType.ARROW_UP]: MessageType.syncWithLiveShare, + [CommonActionType.CHANGE_CELL_TYPE]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [CommonActionType.CLICK_CELL]: MessageType.syncWithLiveShare, + [CommonActionType.CONTINUE]: MessageType.other, + [CommonActionType.DELETE_CELL]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [CommonActionType.CODE_CREATED]: MessageType.noIdea, + [CommonActionType.COPY_CELL_CODE]: MessageType.other, + [CommonActionType.EDITOR_LOADED]: MessageType.other, + [CommonActionType.EDIT_CELL]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [CommonActionType.EXECUTE_CELL_AND_ADVANCE]: MessageType.other, + [CommonActionType.EXECUTE_ABOVE]: MessageType.other, + [CommonActionType.EXECUTE_ALL_CELLS]: MessageType.other, + [CommonActionType.EXECUTE_CELL]: MessageType.other, + [CommonActionType.EXECUTE_CELL_AND_BELOW]: MessageType.other, + [CommonActionType.EXPORT]: MessageType.other, + [CommonActionType.EXPORT_NOTEBOOK_AS]: MessageType.other, + [CommonActionType.FOCUS_CELL]: MessageType.syncWithLiveShare, + [CommonActionType.GATHER_CELL]: MessageType.other, + [CommonActionType.GATHER_CELL_TO_SCRIPT]: MessageType.other, + [CommonActionType.GET_VARIABLE_DATA]: MessageType.other, + [CommonActionType.GOTO_CELL]: MessageType.syncWithLiveShare, + [CommonActionType.INSERT_ABOVE_AND_FOCUS_NEW_CELL]: MessageType.other, + [CommonActionType.INSERT_ABOVE]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [CommonActionType.INSERT_ABOVE_FIRST_AND_FOCUS_NEW_CELL]: MessageType.other, + [CommonActionType.INSERT_ABOVE_FIRST]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [CommonActionType.INSERT_BELOW]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [CommonActionType.INSERT_BELOW_AND_FOCUS_NEW_CELL]: MessageType.other, + [CommonActionType.INTERRUPT_KERNEL]: MessageType.other, + [CommonActionType.LAUNCH_NOTEBOOK_TRUST_PROMPT]: MessageType.other, + [CommonActionType.LOADED_ALL_CELLS]: MessageType.other, + [CommonActionType.LINK_CLICK]: MessageType.other, + [CommonActionType.MOVE_CELL_DOWN]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [CommonActionType.MOVE_CELL_UP]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [CommonActionType.OPEN_SETTINGS]: MessageType.other, + [CommonActionType.RESTART_KERNEL]: MessageType.other, + [CommonActionType.RUN_BY_LINE]: MessageType.other, + [CommonActionType.SAVE]: MessageType.other, + [CommonActionType.SCROLL]: MessageType.syncWithLiveShare, + [CommonActionType.SELECT_CELL]: MessageType.syncWithLiveShare, + [CommonActionType.SELECT_SERVER]: MessageType.other, + [CommonActionType.SEND_COMMAND]: MessageType.other, + [CommonActionType.SHOW_DATA_VIEWER]: MessageType.other, + [CommonActionType.STEP]: MessageType.other, + [CommonActionType.SUBMIT_INPUT]: MessageType.other, + [CommonActionType.TOGGLE_INPUT_BLOCK]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [CommonActionType.TOGGLE_LINE_NUMBERS]: MessageType.syncWithLiveShare, + [CommonActionType.TOGGLE_OUTPUT]: MessageType.syncWithLiveShare, + [CommonActionType.TOGGLE_VARIABLE_EXPLORER]: MessageType.syncWithLiveShare, + [CommonActionType.SET_VARIABLE_EXPLORER_HEIGHT]: MessageType.other, + [CommonActionType.UNFOCUS_CELL]: MessageType.syncWithLiveShare, + [CommonActionType.UNMOUNT]: MessageType.other, + [CommonActionType.PostOutgoingMessage]: MessageType.other, + [CommonActionType.REFRESH_VARIABLES]: MessageType.other, + [CommonActionType.FOCUS_INPUT]: MessageType.other, + [CommonActionType.LOAD_IPYWIDGET_CLASS_SUCCESS]: MessageType.other, + [CommonActionType.LOAD_IPYWIDGET_CLASS_FAILURE]: MessageType.other, + [CommonActionType.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED]: MessageType.other, + [CommonActionType.IPYWIDGET_RENDER_FAILURE]: MessageType.other, + + // Types from InteractiveWindowMessages + [InteractiveWindowMessages.Activate]: MessageType.other, + [InteractiveWindowMessages.AddedSysInfo]: MessageType.other, + [InteractiveWindowMessages.CancelCompletionItemsRequest]: MessageType.other, + [InteractiveWindowMessages.CancelHoverRequest]: MessageType.other, + [InteractiveWindowMessages.CancelResolveCompletionItemRequest]: MessageType.other, + [InteractiveWindowMessages.CancelSignatureHelpRequest]: MessageType.other, + [InteractiveWindowMessages.ClearAllOutputs]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [InteractiveWindowMessages.CollapseAll]: MessageType.syncWithLiveShare, + [InteractiveWindowMessages.Continue]: MessageType.other, + [InteractiveWindowMessages.CopyCodeCell]: MessageType.other, + [InteractiveWindowMessages.DebugStateChange]: MessageType.other, + [InteractiveWindowMessages.DeleteAllCells]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [InteractiveWindowMessages.DoSave]: MessageType.other, + [InteractiveWindowMessages.ExecutionRendered]: MessageType.other, + [InteractiveWindowMessages.ExpandAll]: MessageType.syncWithLiveShare, + [InteractiveWindowMessages.Export]: MessageType.other, + [InteractiveWindowMessages.ExportNotebookAs]: MessageType.other, + [InteractiveWindowMessages.FinishCell]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [InteractiveWindowMessages.FocusedCellEditor]: MessageType.syncWithLiveShare, + [InteractiveWindowMessages.GatherCode]: MessageType.other, + [InteractiveWindowMessages.GatherCodeToScript]: MessageType.other, + [InteractiveWindowMessages.Gathering]: MessageType.other, + [InteractiveWindowMessages.GetAllCells]: MessageType.other, + [InteractiveWindowMessages.ForceVariableRefresh]: MessageType.other, + [InteractiveWindowMessages.GetVariablesRequest]: MessageType.other, + [InteractiveWindowMessages.GetVariablesResponse]: MessageType.other, + [InteractiveWindowMessages.GotoCodeCell]: MessageType.syncWithLiveShare, + [InteractiveWindowMessages.GotoCodeCell]: MessageType.syncWithLiveShare, + [InteractiveWindowMessages.HasCell]: MessageType.other, + [InteractiveWindowMessages.HasCellResponse]: MessageType.other, + [InteractiveWindowMessages.Interrupt]: MessageType.other, + [InteractiveWindowMessages.IPyWidgetLoadSuccess]: MessageType.other, + [InteractiveWindowMessages.IPyWidgetLoadFailure]: MessageType.other, + [InteractiveWindowMessages.IPyWidgetRenderFailure]: MessageType.other, + [InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage]: MessageType.other, + [InteractiveWindowMessages.IPyWidgetWidgetVersionNotSupported]: MessageType.other, + [InteractiveWindowMessages.KernelIdle]: MessageType.other, + [InteractiveWindowMessages.LaunchNotebookTrustPrompt]: MessageType.other, + [InteractiveWindowMessages.TrustNotebookComplete]: MessageType.other, + [InteractiveWindowMessages.LoadAllCells]: MessageType.other, + [InteractiveWindowMessages.LoadAllCellsComplete]: MessageType.other, + [InteractiveWindowMessages.LoadOnigasmAssemblyRequest]: MessageType.other, + [InteractiveWindowMessages.LoadOnigasmAssemblyResponse]: MessageType.other, + [InteractiveWindowMessages.LoadTmLanguageRequest]: MessageType.other, + [InteractiveWindowMessages.LoadTmLanguageResponse]: MessageType.other, + [InteractiveWindowMessages.MonacoReady]: MessageType.other, + [InteractiveWindowMessages.NativeCommand]: MessageType.other, + [InteractiveWindowMessages.NotebookAddCellBelow]: + MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [InteractiveWindowMessages.NotebookClean]: MessageType.other, + [InteractiveWindowMessages.NotebookDirty]: MessageType.other, + [InteractiveWindowMessages.NotebookExecutionActivated]: MessageType.other, + [InteractiveWindowMessages.NotebookIdentity]: MessageType.other, + [InteractiveWindowMessages.NotebookClose]: MessageType.other, + [InteractiveWindowMessages.NotebookRunAllCells]: MessageType.other, + [InteractiveWindowMessages.NotebookRunSelectedCell]: MessageType.other, + [InteractiveWindowMessages.OpenLink]: MessageType.other, + [InteractiveWindowMessages.OpenSettings]: MessageType.other, + [InteractiveWindowMessages.OutputToggled]: MessageType.other, + [InteractiveWindowMessages.ProvideCompletionItemsRequest]: MessageType.other, + [InteractiveWindowMessages.ProvideCompletionItemsResponse]: MessageType.other, + [InteractiveWindowMessages.ProvideHoverRequest]: MessageType.other, + [InteractiveWindowMessages.ProvideHoverResponse]: MessageType.other, + [InteractiveWindowMessages.ProvideSignatureHelpRequest]: MessageType.other, + [InteractiveWindowMessages.ProvideSignatureHelpResponse]: MessageType.other, + [InteractiveWindowMessages.ReExecuteCells]: MessageType.other, + [InteractiveWindowMessages.Redo]: MessageType.other, + [InteractiveWindowMessages.RemoteAddCode]: MessageType.other, + [InteractiveWindowMessages.ReceivedUpdateModel]: MessageType.other, + [InteractiveWindowMessages.RemoteReexecuteCode]: MessageType.other, + [InteractiveWindowMessages.ResolveCompletionItemRequest]: MessageType.other, + [InteractiveWindowMessages.ResolveCompletionItemResponse]: MessageType.other, + [InteractiveWindowMessages.RestartKernel]: MessageType.other, + [InteractiveWindowMessages.ReturnAllCells]: MessageType.other, + [InteractiveWindowMessages.RunByLine]: MessageType.other, + [InteractiveWindowMessages.SaveAll]: MessageType.other, + [InteractiveWindowMessages.SavePng]: MessageType.other, + [InteractiveWindowMessages.ScrollToCell]: MessageType.syncWithLiveShare, + [InteractiveWindowMessages.SelectedCell]: MessageType.other, + [InteractiveWindowMessages.SelectJupyterServer]: MessageType.other, + [InteractiveWindowMessages.SelectKernel]: MessageType.other, + [InteractiveWindowMessages.SendInfo]: MessageType.other, + [InteractiveWindowMessages.SettingsUpdated]: MessageType.other, + [InteractiveWindowMessages.ShowBreak]: MessageType.other, + [InteractiveWindowMessages.ShowingIp]: MessageType.other, + [InteractiveWindowMessages.ShowContinue]: MessageType.other, + [InteractiveWindowMessages.ShowDataViewer]: MessageType.other, + [InteractiveWindowMessages.ShowPlot]: MessageType.other, + [InteractiveWindowMessages.StartCell]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [InteractiveWindowMessages.StartDebugging]: MessageType.other, + [InteractiveWindowMessages.StartProgress]: MessageType.other, + [InteractiveWindowMessages.Started]: MessageType.other, + [InteractiveWindowMessages.Step]: MessageType.other, + [InteractiveWindowMessages.StopDebugging]: MessageType.other, + [InteractiveWindowMessages.StopProgress]: MessageType.other, + [InteractiveWindowMessages.SubmitNewCell]: MessageType.other, + [InteractiveWindowMessages.Sync]: MessageType.other, + [InteractiveWindowMessages.Undo]: MessageType.other, + [InteractiveWindowMessages.UnfocusedCellEditor]: MessageType.syncWithLiveShare, + [InteractiveWindowMessages.UpdateCellWithExecutionResults]: + MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [InteractiveWindowMessages.UpdateModel]: checkSyncUpdateModel, + [InteractiveWindowMessages.UpdateKernel]: MessageType.syncAcrossSameNotebooks | MessageType.syncWithLiveShare, + [InteractiveWindowMessages.UpdateDisplayData]: MessageType.syncWithLiveShare, + [InteractiveWindowMessages.VariableExplorerToggle]: MessageType.other, + [InteractiveWindowMessages.SetVariableExplorerHeight]: MessageType.other, + [InteractiveWindowMessages.VariableExplorerHeightResponse]: MessageType.other, + [InteractiveWindowMessages.VariablesComplete]: MessageType.other, + [InteractiveWindowMessages.ConvertUriForUseInWebViewRequest]: MessageType.other, + [InteractiveWindowMessages.ConvertUriForUseInWebViewResponse]: MessageType.other, + // Types from CssMessages + [CssMessages.GetCssRequest]: MessageType.other, + [CssMessages.GetCssResponse]: MessageType.other, + [CssMessages.GetMonacoThemeRequest]: MessageType.other, + [CssMessages.GetMonacoThemeResponse]: MessageType.other, + // Types from Shared Messages + [SharedMessages.LocInit]: MessageType.other, + [SharedMessages.Started]: MessageType.other, + [SharedMessages.UpdateSettings]: MessageType.other, + // IpyWidgets + [IPyWidgetMessages.IPyWidgets_kernelOptions]: MessageType.syncAcrossSameNotebooks, + [IPyWidgetMessages.IPyWidgets_Ready]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_WidgetScriptSourceRequest]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_WidgetScriptSourceResponse]: MessageType.syncAcrossSameNotebooks, + [IPyWidgetMessages.IPyWidgets_onKernelChanged]: MessageType.syncAcrossSameNotebooks, + [IPyWidgetMessages.IPyWidgets_onRestartKernel]: MessageType.syncAcrossSameNotebooks, + [IPyWidgetMessages.IPyWidgets_msg]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_binary_msg]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_msg_received]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_iopub_msg_handled]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_registerCommTarget]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_MessageHookCall]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_MessageHookResult]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_RegisterMessageHook]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_ExtensionOperationHandled]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_RemoveMessageHook]: MessageType.noIdea, + [IPyWidgetMessages.IPyWidgets_mirror_execute]: MessageType.noIdea +}; + +/** + * Function to check if a NotebookModelChange should be sync'd across editors or not + */ +function checkSyncUpdateModel(payload: NotebookModelChange): boolean { + // Only sync user changes + return payload.source === 'user'; +} + +/** + * If the original message was a sync message, then do not send messages to extension. + * We allow messages to be sent to extension ONLY when the original message was triggered by the user. + * + * @export + * @param {MessageType} [messageType] + * @returns + */ +export function checkToPostBasedOnOriginalMessageType(messageType?: MessageType): boolean { + if (!messageType) { + return true; + } + if ( + (messageType & MessageType.syncAcrossSameNotebooks) === MessageType.syncAcrossSameNotebooks || + (messageType & MessageType.syncWithLiveShare) === MessageType.syncWithLiveShare + ) { + return false; + } + + return true; +} + +// tslint:disable-next-line: no-any +export function shouldRebroadcast(message: keyof IInteractiveWindowMapping, payload: any): [boolean, MessageType] { + // Get the configured type for this message (whether it should be re-broadcasted or not). + const messageTypeOrFunc: MessageType | undefined | MessageAction = messageWithMessageTypes[message]; + const messageType = + typeof messageTypeOrFunc !== 'function' ? (messageTypeOrFunc as number) : MessageType.syncAcrossSameNotebooks; + // Support for liveshare is turned off for now, we can enable that later. + // I.e. we only support synchronizing across editors in the same session. + if ( + messageType === undefined || + (messageType & MessageType.syncAcrossSameNotebooks) !== MessageType.syncAcrossSameNotebooks + ) { + return [false, MessageType.other]; + } + + if (typeof messageTypeOrFunc === 'function') { + return [messageTypeOrFunc(payload), messageType]; + } + + return [ + (messageType & MessageType.syncAcrossSameNotebooks) > 0 || (messageType & MessageType.syncWithLiveShare) > 0, + messageType + ]; +} diff --git a/src/client/datascience/interactive-common/types.ts b/src/client/datascience/interactive-common/types.ts new file mode 100644 index 000000000000..3c22935868a2 --- /dev/null +++ b/src/client/datascience/interactive-common/types.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { CommonActionType } from '../../../datascience-ui/interactive-common/redux/reducers/types'; +import { CssMessages, SharedMessages } from '../messages'; +import { InteractiveWindowMessages } from './interactiveWindowTypes'; +import { MessageType } from './synchronization'; + +// Stuff common to React and Extensions. + +type BaseData = { + messageType?: MessageType; + /** + * Tells us whether this message is incoming for reducer use or + * whether this is a message that needs to be sent out to extension (from reducer). + */ + messageDirection?: 'incoming' | 'outgoing'; +}; + +type BaseDataWithPayload = { + messageType?: MessageType; + /** + * Tells us whether this message is incoming for reducer use or + * whether this is a message that needs to be sent out to extension (from reducer). + */ + messageDirection?: 'incoming' | 'outgoing'; + data: T; +}; + +// This forms the base content of every payload in all dispatchers. +export type BaseReduxActionPayload = T extends never + ? T extends undefined + ? BaseData + : BaseDataWithPayload + : BaseDataWithPayload; +export type SyncPayload = { + type: InteractiveWindowMessages | SharedMessages | CommonActionType | CssMessages; + // tslint:disable-next-line: no-any + payload: BaseReduxActionPayload; +}; diff --git a/src/client/datascience/interactive-ipynb/autoSaveService.ts b/src/client/datascience/interactive-ipynb/autoSaveService.ts new file mode 100644 index 000000000000..6920c74a21f1 --- /dev/null +++ b/src/client/datascience/interactive-ipynb/autoSaveService.ts @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, Event, EventEmitter, TextEditor, Uri, WindowState } from 'vscode'; +import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import '../../common/extensions'; + +import { IDisposable } from '../../common/types'; +import { INotebookIdentity, InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes'; +import { + FileSettings, + IDataScienceFileSystem, + IInteractiveWindowListener, + INotebookEditor, + INotebookEditorProvider, + WebViewViewChangeEventArgs +} from '../types'; + +// tslint:disable: no-any + +/** + * Sends notifications to Notebooks to save the notebook. + * Based on auto save settings, this class will regularly check for changes and send a save requet. + * If window state changes or active editor changes, then notify notebooks (if auto save is configured to do so). + * Monitor save and modified events on editor to determine its current dirty state. + * + * @export + * @class AutoSaveService + * @implements {IInteractiveWindowListener} + */ +@injectable() +export class AutoSaveService implements IInteractiveWindowListener { + private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ + message: string; + payload: any; + }>(); + private disposables: IDisposable[] = []; + private notebookUri?: Uri; + private timeout?: ReturnType; + private visible: boolean | undefined; + private active: boolean | undefined; + constructor( + @inject(IApplicationShell) appShell: IApplicationShell, + @inject(IDocumentManager) documentManager: IDocumentManager, + @inject(INotebookEditorProvider) private readonly notebookEditorProvider: INotebookEditorProvider, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService + ) { + this.workspace.onDidChangeConfiguration(this.onSettingsChanded.bind(this), this, this.disposables); + this.disposables.push(appShell.onDidChangeWindowState(this.onDidChangeWindowState.bind(this))); + this.disposables.push(documentManager.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor.bind(this))); + } + + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + + public onMessage(message: string, payload?: any): void { + if (message === InteractiveWindowMessages.NotebookIdentity) { + this.notebookUri = (payload as INotebookIdentity).resource; + } else if (message === InteractiveWindowMessages.NotebookClose) { + this.dispose(); + } else if (message === InteractiveWindowMessages.LoadAllCellsComplete) { + const notebook = this.getNotebook(); + if (!notebook) { + return; + } + this.disposables.push(notebook.modified(this.onNotebookModified, this, this.disposables)); + this.disposables.push(notebook.saved(this.onNotebookSaved, this, this.disposables)); + } + } + public onViewStateChanged(args: WebViewViewChangeEventArgs) { + let changed = false; + if (this.visible !== args.current.visible) { + this.visible = args.current.visible; + changed = true; + } + if (this.active !== args.current.active) { + this.active = args.current.active; + changed = true; + } + if (changed) { + const settings = this.getAutoSaveSettings(); + if (settings && settings.autoSave === 'onFocusChange') { + this.save(); + } + } + } + public dispose(): void | undefined { + this.disposables.filter((item) => !!item).forEach((item) => item.dispose()); + this.clearTimeout(); + } + private onNotebookModified(_: INotebookEditor) { + // If we haven't started a timer, then start if necessary. + if (!this.timeout) { + this.setTimer(); + } + } + private onNotebookSaved(_: INotebookEditor) { + // If we haven't started a timer, then start if necessary. + if (!this.timeout) { + this.setTimer(); + } + } + private getNotebook(): INotebookEditor | undefined { + const uri = this.notebookUri; + if (!uri) { + return; + } + return this.notebookEditorProvider.editors.find((item) => + this.fs.areLocalPathsSame(item.file.fsPath, uri.fsPath) + ); + } + private getAutoSaveSettings(): FileSettings { + const filesConfig = this.workspace.getConfiguration('files', this.notebookUri); + return { + autoSave: filesConfig.get('autoSave', 'off'), + autoSaveDelay: filesConfig.get('autoSaveDelay', 1000) + }; + } + private onSettingsChanded(e: ConfigurationChangeEvent) { + if ( + e.affectsConfiguration('files.autoSave', this.notebookUri) || + e.affectsConfiguration('files.autoSaveDelay', this.notebookUri) + ) { + // Reset the timer, as we may have increased it, turned it off or other. + this.clearTimeout(); + this.setTimer(); + } + } + private setTimer() { + const settings = this.getAutoSaveSettings(); + if (!settings || settings.autoSave === 'off') { + return; + } + if (settings && settings.autoSave === 'afterDelay') { + // Add a timeout to save after n milli seconds. + // Do not use setInterval, as that will cause all handlers to queue up. + this.timeout = setTimeout(() => { + this.save(); + }, settings.autoSaveDelay); + } + } + private clearTimeout() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } + } + private save() { + this.clearTimeout(); + const notebook = this.getNotebook(); + if (notebook && notebook.isDirty && !notebook.isUntitled) { + // Notify webview to perform a save. + this.postEmitter.fire({ message: InteractiveWindowMessages.DoSave, payload: undefined }); + } else { + this.setTimer(); + } + } + private onDidChangeWindowState(_state: WindowState) { + const settings = this.getAutoSaveSettings(); + if (settings && (settings.autoSave === 'onWindowChange' || settings.autoSave === 'onFocusChange')) { + this.save(); + } + } + private onDidChangeActiveTextEditor(_e?: TextEditor) { + const settings = this.getAutoSaveSettings(); + if (settings && settings.autoSave === 'onFocusChange') { + this.save(); + } + } +} diff --git a/src/client/datascience/interactive-ipynb/digestStorage.ts b/src/client/datascience/interactive-ipynb/digestStorage.ts new file mode 100644 index 000000000000..7d0def08b1fa --- /dev/null +++ b/src/client/datascience/interactive-ipynb/digestStorage.ts @@ -0,0 +1,108 @@ +import { createHash, randomBytes } from 'crypto'; +import { inject, injectable } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { traceError, traceInfo } from '../../common/logger'; +import { isFileNotFoundError } from '../../common/platform/errors'; +import { IExtensionContext } from '../../common/types'; +import { IDataScienceFileSystem, IDigestStorage } from '../types'; + +@injectable() +export class DigestStorage implements IDigestStorage { + public readonly key: Promise; + private digestDir: Promise; + private loggedFileLocations = new Set(); + + constructor( + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(IExtensionContext) private extensionContext: IExtensionContext + ) { + this.key = this.initKey(); + this.digestDir = this.initDir(); + } + + public async saveDigest(uri: Uri, signature: string) { + const fileLocation = await this.getFileLocation(uri); + // Since the signature is a hex digest, the character 'z' is being used to delimit the start and end of a single digest + try { + await this.saveDigestInner(uri, fileLocation, signature); + } catch (err) { + // The nbsignatures dir is only initialized on extension activation. + // If the user deletes it to reset trust, the next attempt to trust + // an untrusted notebook in the same session will fail because the parent + // directory does not exist. + if (isFileNotFoundError(err)) { + // Gracefully recover from such errors by reinitializing directory and retrying + await this.initDir(); + await this.saveDigestInner(uri, fileLocation, signature); + } else { + traceError(err); + } + } + } + + public async containsDigest(uri: Uri, signature: string) { + const fileLocation = await this.getFileLocation(uri); + try { + const digests = await this.fs.readLocalFile(fileLocation); + return digests.indexOf(`z${signature}z`) >= 0; + } catch (err) { + if (!isFileNotFoundError(err)) { + traceError(err); // Don't log the error if the file simply doesn't exist + } + return false; + } + } + + private async saveDigestInner(uri: Uri, fileLocation: string, signature: string) { + await this.fs.appendLocalFile(fileLocation, `z${signature}z\n`); + if (!this.loggedFileLocations.has(fileLocation)) { + traceInfo(`Wrote trust for ${uri.toString()} to ${fileLocation}`); + this.loggedFileLocations.add(fileLocation); + } + } + + private async getFileLocation(uri: Uri): Promise { + const normalizedName = os.platform() === 'win32' ? uri.fsPath.toLowerCase() : uri.fsPath; + const hashedName = createHash('sha256').update(normalizedName).digest('hex'); + return path.join(await this.digestDir, hashedName); + } + + private async initDir(): Promise { + const defaultDigestDirLocation = this.getDefaultLocation('nbsignatures'); + if (!(await this.fs.localDirectoryExists(defaultDigestDirLocation))) { + await this.fs.createLocalDirectory(defaultDigestDirLocation); + } + return defaultDigestDirLocation; + } + + /** + * Get or create a local secret key, used in computing HMAC hashes of trusted + * checkpoints in the notebook's execution history + */ + private async initKey(): Promise { + const defaultKeyFileLocation = this.getDefaultLocation('nbsecret'); + + if (await this.fs.localFileExists(defaultKeyFileLocation)) { + // if the keyfile already exists, bail out + return this.fs.readLocalFile(defaultKeyFileLocation); + } else { + // If it doesn't exist, create one + // Key must be generated from a cryptographically secure pseudorandom function: + // https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback + // No callback is provided so random bytes will be generated synchronously + const key = randomBytes(1024).toString('hex'); + await this.fs.writeLocalFile(defaultKeyFileLocation, key); + return key; + } + } + + private getDefaultLocation(fileName: string) { + const dir = this.extensionContext.globalStoragePath; + if (dir) { + return path.join(dir, fileName); + } + throw new Error('Unable to locate extension global storage path for trusted digest storage'); + } +} diff --git a/src/client/datascience/interactive-ipynb/nativeEditor.ts b/src/client/datascience/interactive-ipynb/nativeEditor.ts new file mode 100644 index 000000000000..539848a9213b --- /dev/null +++ b/src/client/datascience/interactive-ipynb/nativeEditor.ts @@ -0,0 +1,825 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as path from 'path'; +import { + CancellationToken, + CancellationTokenSource, + Event, + EventEmitter, + Memento, + Uri, + ViewColumn, + WebviewPanel +} from 'vscode'; +import '../../common/extensions'; + +import * as uuid from 'uuid/v4'; +import { createErrorOutput } from '../../../datascience-ui/common/cellFactory'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + ILiveShareApi, + IWebviewPanelProvider, + IWorkspaceService +} from '../../common/application/types'; +import { ContextKey } from '../../common/contextKey'; +import { traceError, traceInfo } from '../../common/logger'; + +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IExperimentsManager, + Resource +} from '../../common/types'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { Commands, EditorContexts, Identifiers, Telemetry } from '../constants'; +import { InteractiveBase } from '../interactive-common/interactiveBase'; +import { + INativeCommand, + INotebookIdentity, + InteractiveWindowMessages, + IReExecuteCells, + IRunByLine, + ISubmitNewCell, + NotebookModelChange, + SysInfoReason, + VariableExplorerStateKeys +} from '../interactive-common/interactiveWindowTypes'; +import { + CellState, + ICell, + ICodeCssGenerator, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IInteractiveWindowInfo, + IInteractiveWindowListener, + IJupyterDebugger, + IJupyterVariableDataProviderFactory, + IJupyterVariables, + INotebookEditor, + INotebookEditorProvider, + INotebookExporter, + INotebookImporter, + INotebookMetadataLive, + INotebookModel, + INotebookProvider, + IStatusProvider, + IThemeFinder, + ITrustService, + WebViewViewChangeEventArgs +} from '../types'; +import { NativeEditorSynchronizer } from './nativeEditorSynchronizer'; + +import type { nbformat } from '@jupyterlab/coreutils'; +// tslint:disable-next-line: no-require-imports +import cloneDeep = require('lodash/cloneDeep'); +import { concatMultilineString, splitMultilineString } from '../../../datascience-ui/common'; +import { ServerStatus } from '../../../datascience-ui/interactive-common/mainState'; +import { isTestExecution, PYTHON_LANGUAGE } from '../../common/constants'; +import { translateKernelLanguageToMonaco } from '../common'; +import { IDataViewerFactory } from '../data-viewing/types'; +import { getCellHashProvider } from '../editor-integration/cellhashprovider'; +import { KernelSelector } from '../jupyter/kernels/kernelSelector'; +import { KernelConnectionMetadata } from '../jupyter/kernels/types'; + +const nativeEditorDir = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'notebook'); +export class NativeEditor extends InteractiveBase implements INotebookEditor { + public get onDidChangeViewState(): Event { + return this._onDidChangeViewState.event; + } + + public get visible(): boolean { + return this.viewState.visible; + } + + public get active(): boolean { + return this.viewState.active; + } + + public get file(): Uri { + if (this.model) { + return this.model.file; + } + return Uri.file(''); + } + + public get isUntitled(): boolean { + return this.model ? this.model.isUntitled : false; + } + + public get closed(): Event { + return this.closedEvent.event; + } + + public get executed(): Event { + return this.executedEvent.event; + } + + public get modified(): Event { + return this.modifiedEvent.event; + } + public get saved(): Event { + return this.savedEvent.event; + } + + public get isDirty(): boolean { + return this.model ? this.model.isDirty : false; + } + public get model(): Readonly { + return this._model; + } + public readonly type: 'old' | 'custom' = 'custom'; + protected savedEvent: EventEmitter = new EventEmitter(); + protected closedEvent: EventEmitter = new EventEmitter(); + protected modifiedEvent: EventEmitter = new EventEmitter(); + + private sentExecuteCellTelemetry: boolean = false; + private _onDidChangeViewState = new EventEmitter(); + private executedEvent: EventEmitter = new EventEmitter(); + private startupTimer: StopWatch = new StopWatch(); + private loadedAllCells: boolean = false; + private executeCancelTokens = new Set(); + private loadPromise: Promise; + private previouslyNotTrusted: boolean = false; + + constructor( + listeners: IInteractiveWindowListener[], + liveShare: ILiveShareApi, + applicationShell: IApplicationShell, + documentManager: IDocumentManager, + provider: IWebviewPanelProvider, + disposables: IDisposableRegistry, + cssGenerator: ICodeCssGenerator, + themeFinder: IThemeFinder, + statusProvider: IStatusProvider, + fs: IDataScienceFileSystem, + configuration: IConfigurationService, + commandManager: ICommandManager, + jupyterExporter: INotebookExporter, + workspaceService: IWorkspaceService, + private readonly synchronizer: NativeEditorSynchronizer, + private editorProvider: INotebookEditorProvider, + dataExplorerFactory: IDataViewerFactory, + jupyterVariableDataProviderFactory: IJupyterVariableDataProviderFactory, + jupyterVariables: IJupyterVariables, + jupyterDebugger: IJupyterDebugger, + protected readonly importer: INotebookImporter, + errorHandler: IDataScienceErrorHandler, + globalStorage: Memento, + workspaceStorage: Memento, + experimentsManager: IExperimentsManager, + asyncRegistry: IAsyncDisposableRegistry, + notebookProvider: INotebookProvider, + useCustomEditorApi: boolean, + private trustService: ITrustService, + expService: IExperimentService, + private _model: INotebookModel, + webviewPanel: WebviewPanel | undefined, + selector: KernelSelector + ) { + super( + listeners, + liveShare, + applicationShell, + documentManager, + provider, + disposables, + cssGenerator, + themeFinder, + statusProvider, + fs, + configuration, + jupyterExporter, + workspaceService, + dataExplorerFactory, + jupyterVariableDataProviderFactory, + jupyterVariables, + jupyterDebugger, + errorHandler, + commandManager, + globalStorage, + workspaceStorage, + nativeEditorDir, + [ + path.join(nativeEditorDir, 'require.js'), + path.join(nativeEditorDir, 'ipywidgets.js'), + path.join(nativeEditorDir, 'monaco.bundle.js'), + path.join(nativeEditorDir, 'commons.initial.bundle.js'), + path.join(nativeEditorDir, 'nativeEditor.js') + ], + path.basename(_model.file.fsPath), + ViewColumn.Active, + experimentsManager, + notebookProvider, + useCustomEditorApi, + expService, + selector + ); + asyncRegistry.push(this); + asyncRegistry.push(this.trustService.onDidSetNotebookTrust(this.monitorChangesToTrust, this)); + this.synchronizer.subscribeToUserActions(this, this.postMessage.bind(this)); + + traceInfo(`Loading web panel for ${this.model.file}`); + + // Load the web panel using our file path so it can find + // relative files next to the notebook. + this.loadPromise = super + .loadWebPanel(path.dirname(this.file.fsPath), webviewPanel) + .catch((e) => this.errorHandler.handleError(e)); + + // Sign up for dirty events + this._model.changed(this.modelChanged.bind(this)); + this.previouslyNotTrusted = !this._model.isTrusted; + } + + public async show(preserveFocus?: boolean) { + await this.loadPromise; + return super.show(preserveFocus); + } + public dispose(): Promise { + super.dispose(); + this.model?.dispose(); // NOSONAR + return this.close(); + } + + // tslint:disable-next-line: no-any + public onMessage(message: string, payload: any) { + super.onMessage(message, payload); + switch (message) { + case InteractiveWindowMessages.Started: + if (this.model) { + // Load our cells, but don't wait for this to finish, otherwise the window won't load. + this.sendInitialCellsToWebView([...this.model.cells], this.model.isTrusted) + .then(() => { + // May alread be dirty, if so send a message + if (this.model?.isDirty) { + this.postMessage(InteractiveWindowMessages.NotebookDirty).ignoreErrors(); + } + }) + .catch((exc) => traceError('Error loading cells: ', exc)); + } + break; + case InteractiveWindowMessages.Sync: + this.synchronizer.notifyUserAction(payload, this); + break; + + case InteractiveWindowMessages.ReExecuteCells: + this.executedEvent.fire(this); + break; + + case InteractiveWindowMessages.SaveAll: + this.handleMessage(message, payload, this.saveAll); + break; + + case InteractiveWindowMessages.ExportNotebookAs: + this.handleMessage(message, payload, this.exportAs); + break; + + case InteractiveWindowMessages.UpdateModel: + this.handleMessage(message, payload, this.updateModel); + break; + + case InteractiveWindowMessages.NativeCommand: + this.handleMessage(message, payload, this.logNativeCommand); + break; + + // call this to update the whole document for intellisense + case InteractiveWindowMessages.LoadAllCellsComplete: + this.handleMessage(message, payload, this.loadCellsComplete); + break; + + case InteractiveWindowMessages.LaunchNotebookTrustPrompt: + this.handleMessage(message, payload, this.launchNotebookTrustPrompt); + break; + + case InteractiveWindowMessages.RestartKernel: + this.interruptExecution(); + break; + + case InteractiveWindowMessages.Interrupt: + this.interruptExecution(); + break; + + case InteractiveWindowMessages.RunByLine: + this.handleMessage(message, payload, this.handleRunByLine); + break; + + default: + break; + } + } + + public get notebookMetadata(): INotebookMetadataLive | undefined { + return this.model.metadata; + } + + public async updateNotebookOptions(kernelConnection: KernelConnectionMetadata): Promise { + if (this.model) { + const change: NotebookModelChange = { + kind: 'version', + kernelConnection, + oldDirty: this.model.isDirty, + newDirty: true, + source: 'user' + }; + this.updateModel(change); + } + } + + public async hasCell(id: string): Promise { + if (this.model && this.model.cells.find((c) => c.id === id)) { + return true; + } + return false; + } + + public runAllCells() { + this.postMessage(InteractiveWindowMessages.NotebookRunAllCells).ignoreErrors(); + } + + public runSelectedCell() { + this.postMessage(InteractiveWindowMessages.NotebookRunSelectedCell).ignoreErrors(); + } + + public addCellBelow() { + this.postMessage(InteractiveWindowMessages.NotebookAddCellBelow, { newCellId: uuid() }).ignoreErrors(); + } + + public get owningResource(): Resource { + // Resource to use for loading and our identity are the same. + return this.notebookIdentity.resource; + } + + public expandAllCells(): void { + throw Error('Not implemented Exception'); + } + public collapseAllCells(): void { + throw Error('Not implemented Exception'); + } + + protected addSysInfo(reason: SysInfoReason): Promise { + // We need to send a message when restarting + if (reason === SysInfoReason.Restart || reason === SysInfoReason.New) { + this.postMessage(InteractiveWindowMessages.RestartKernel).ignoreErrors(); + } + + // These are not supported. + return Promise.resolve(); + } + + protected async createNotebookIfProviderConnectionExists() { + if (this._model.isTrusted) { + await super.createNotebookIfProviderConnectionExists(); + } + } + + protected submitCode( + code: string, + file: string, + line: number, + id?: string, + data?: nbformat.ICodeCell | nbformat.IRawCell | nbformat.IMarkdownCell, + debugOptions?: { runByLine: boolean; hashFileName?: string }, + cancelToken?: CancellationToken + ): Promise { + const stopWatch = new StopWatch(); + return super + .submitCode(code, file, line, id, data, debugOptions, cancelToken) + .finally(() => this.sendPerceivedCellExecute(stopWatch)); + } + + @captureTelemetry(Telemetry.SubmitCellThroughInput, undefined, false) + // tslint:disable-next-line:no-any + protected submitNewCell(info: ISubmitNewCell) { + // If there's any payload, it has the code and the id + if (info && info.code && info.id) { + try { + // Activate the other side, and send as if came from a file + this.editorProvider + .show(this.file) + .then((_v) => { + this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { + code: info.code, + file: Identifiers.EmptyFileName, + line: 0, + id: info.id, + originator: this.id, + debug: false + }); + }) + .ignoreErrors(); + // Send to ourselves. + this.submitCode(info.code, Identifiers.EmptyFileName, 0, info.id).ignoreErrors(); + } catch (exc) { + this.errorHandler.handleError(exc).ignoreErrors(); + } + } + } + + @captureTelemetry(Telemetry.ExecuteNativeCell, undefined, true) + // tslint:disable-next-line:no-any + protected async reexecuteCells(info: IReExecuteCells): Promise { + // This is here for existing functional tests that somehow pass undefined into this method. + if (!this.model || !info || !Array.isArray(info.cellIds)) { + return; + } + const tokenSource = new CancellationTokenSource(); + this.executeCancelTokens.add(tokenSource); + const cellsExecuting = new Set(); + try { + for (let i = 0; i < info.cellIds.length && !tokenSource.token.isCancellationRequested; i += 1) { + const cell = this.model.cells.find((item) => item.id === info.cellIds[i]); + if (!cell) { + continue; + } + cellsExecuting.add(cell); + await this.reexecuteCell(cell, tokenSource.token); + cellsExecuting.delete(cell); + + // Check the new state of our cell + const resultCell = this.model.cells.find((item) => item.id === cell.id); + + // Bail on the rest of our cells if one comes back with an error + if ( + this.configuration.getSettings(this.owningResource).datascience.stopOnError && + resultCell && + resultCell.state === CellState.error + ) { + // Set the remaining cells as finished and break out + if (i < info.cellIds.length) { + const unExecutedCellIds = info.cellIds.slice(i + 1, info.cellIds.length); + unExecutedCellIds.forEach((cellId) => { + const unexecutedCell = this.model.cells.find((item) => item.id === cellId); + if (unexecutedCell) { + this.finishCell(unexecutedCell); + } + }); + } + break; + } + } + } catch (exc) { + // Tell the other side we restarted the kernel. This will stop all executions + this.postMessage(InteractiveWindowMessages.RestartKernel).ignoreErrors(); + + // Handle an error + await this.errorHandler.handleError(exc); + } finally { + this.executeCancelTokens.delete(tokenSource); + + // Make sure everything is marked as finished or error after the final finished + cellsExecuting.forEach((cell) => this.finishCell(cell)); + } + } + + protected get notebookIdentity(): INotebookIdentity { + return { + resource: this.file, + type: 'native' + }; + } + + protected async setLaunchingFile(_file: string): Promise { + // For the native editor, use our own file as the path + const notebook = this.getNotebook(); + if (notebook) { + await notebook.setLaunchingFile(this.file.fsPath); + } + } + + protected sendCellsToWebView(cells: ICell[]) { + // Filter out sysinfo messages. Don't want to show those + const filtered = cells.filter((c) => c.data.cell_type !== 'messages'); + + // Update these cells in our storage only when cells are finished + const modified = filtered.filter((c) => c.state === CellState.finished || c.state === CellState.error); + const unmodified = this.model?.cells.filter((c) => modified.find((m) => m.id === c.id)); + if (modified.length > 0 && unmodified && this.model) { + // As this point, we're updating the model because of changes to the cell as a result of execution. + // The output and execution count change, however we're just going to update everything. + // But, we should not update the `source`. The only time source can change is when a request comes from the UI. + // Perhaps we need a finer grained update (update only output and execution count along with `source=execution`). + // For now retain source from previous model. + // E.g. user executes a cell, in the mean time they update the text. Now model contains new value. + // However once execution has completed, this code will update the model with results from previous execution (prior to edit). + // We now need to give preference to the text in the model, over the old one that was executed. + modified.forEach((modifiedCell) => { + const originalCell = unmodified.find((unmodifiedCell) => unmodifiedCell.id === modifiedCell.id); + if (originalCell) { + modifiedCell.data.source = originalCell.data.source; + } + }); + this.model.update({ + source: 'user', + kind: 'modify', + newCells: modified, + oldCells: cloneDeep(unmodified), + oldDirty: this.model.isDirty, + newDirty: true + }); + } + + // Tell storage about our notebook object + const notebook = this.getNotebook(); + if (notebook && this.model) { + const kernelConnection = notebook.getKernelConnection(); + this.model.update({ + source: 'user', + kind: 'version', + oldDirty: this.model.isDirty, + newDirty: this.model.isDirty, + kernelConnection + }); + } + + // Send onto the webview. + super.sendCellsToWebView(filtered); + } + + protected updateContexts(info: IInteractiveWindowInfo | undefined) { + // This should be called by the python interactive window every + // time state changes. We use this opportunity to update our + // extension contexts + if (this.commandManager && this.commandManager.executeCommand) { + const nativeContext = new ContextKey(EditorContexts.HaveNative, this.commandManager); + nativeContext.set(!this.isDisposed).catch(); + const interactiveCellsContext = new ContextKey(EditorContexts.HaveNativeCells, this.commandManager); + const redoableContext = new ContextKey(EditorContexts.HaveNativeRedoableCells, this.commandManager); + const hasCellSelectedContext = new ContextKey(EditorContexts.HaveCellSelected, this.commandManager); + if (info) { + interactiveCellsContext.set(info.cellCount > 0).catch(); + redoableContext.set(info.redoCount > 0).catch(); + hasCellSelectedContext.set(info.selectedCell ? true : false).catch(); + } else { + hasCellSelectedContext.set(false).catch(); + interactiveCellsContext.set(false).catch(); + redoableContext.set(false).catch(); + } + } + } + + protected async onViewStateChanged(args: WebViewViewChangeEventArgs) { + super.onViewStateChanged(args); + + // Update our contexts + const nativeContext = new ContextKey(EditorContexts.HaveNative, this.commandManager); + nativeContext.set(args.current.visible && args.current.active).catch(); + this._onDidChangeViewState.fire(); + } + + protected async closeBecauseOfFailure(_exc: Error): Promise { + // Actually don't close, just let the error bubble out + } + + protected async close(): Promise { + // Fire our event + this.closedEvent.fire(this); + } + + protected saveAll() { + this.commandManager.executeCommand('workbench.action.files.save', this.file); + } + + private async modelChanged(change: NotebookModelChange) { + if (change.source !== 'user') { + // VS code is telling us to broadcast this to our UI. Tell the UI about the new change. Remove the + // the model so this doesn't have to be stringified + await this.postMessage(InteractiveWindowMessages.UpdateModel, { ...change, model: undefined }); + } + if (change.kind === 'saveAs' && change.model) { + const newFileName = change.model.file.toString(); + const oldFileName = change.sourceUri.toString(); + + if (newFileName !== oldFileName) { + // If the filename has changed + this.renameVariableExplorerHeights(oldFileName, newFileName); + } + } + + // Use the current state of the model to indicate dirty (not the message itself) + if (this.model && change.newDirty !== change.oldDirty) { + this.modifiedEvent.fire(this); + if (this.model.isDirty) { + await this.postMessage(InteractiveWindowMessages.NotebookDirty); + } else { + // Then tell the UI + await this.postMessage(InteractiveWindowMessages.NotebookClean); + } + } + } + private async monitorChangesToTrust() { + if (this.previouslyNotTrusted && this.model?.isTrusted) { + this.previouslyNotTrusted = false; + // Tell UI to update main state + this.postMessage(InteractiveWindowMessages.TrustNotebookComplete).ignoreErrors(); + } + } + private renameVariableExplorerHeights(name: string, updatedName: string) { + // Updates the workspace storage to reflect the updated name of the notebook + // should be called if the name of the notebook changes + // tslint:disable-next-line: no-any + const value = this.workspaceStorage.get(VariableExplorerStateKeys.height, {} as any); + if (!(name in value)) { + return; // Nothing to update + } + + value[updatedName] = value[name]; + delete value[name]; + this.workspaceStorage.update(VariableExplorerStateKeys.height, value); + } + + private async launchNotebookTrustPrompt() { + if (this.model && !this.model.isTrusted) { + await this.commandManager.executeCommand(Commands.TrustNotebook, this.model.file); + } + } + + private interruptExecution() { + this.executeCancelTokens.forEach((t) => t.cancel()); + } + + private finishCell(cell: ICell) { + this.sendCellsToWebView([ + { + ...cell, + state: CellState.finished + } + ]); + } + + private async reexecuteCell(cell: ICell, cancelToken: CancellationToken): Promise { + try { + // If there's any payload, it has the code and the id + if (cell.id && cell.data.cell_type !== 'messages') { + traceInfo(`Executing cell ${cell.id}`); + + // Clear the result if we've run before + await this.clearResult(cell.id); + + // Clear 'per run' data passed to WebView before execution + if (cell.data.metadata.tags !== undefined) { + cell.data.metadata.tags = cell.data.metadata.tags.filter((t) => t !== 'outputPrepend'); + } + + const code = concatMultilineString(cell.data.source); + // Send to ourselves. + await this.submitCode(code, Identifiers.EmptyFileName, 0, cell.id, cell.data, undefined, cancelToken); + } + } catch (exc) { + traceInfo(`Exception executing cell ${cell.id}: `, exc); + + // Make this error our cell output + this.sendCellsToWebView([ + { + // tslint:disable-next-line: no-any + data: { ...cell.data, outputs: [createErrorOutput(exc)] } as any, // nyc compiler issue + id: cell.id, + file: Identifiers.EmptyFileName, + line: 0, + state: CellState.error + } + ]); + + throw exc; + } finally { + if (cell && cell.id) { + traceInfo(`Finished executing cell ${cell.id}`); + } + } + } + + private sendPerceivedCellExecute(runningStopWatch?: StopWatch) { + if (runningStopWatch) { + const props = { notebook: true }; + if (!this.sentExecuteCellTelemetry) { + this.sentExecuteCellTelemetry = true; + sendTelemetryEvent(Telemetry.ExecuteCellPerceivedCold, runningStopWatch.elapsedTime, props); + } else { + sendTelemetryEvent(Telemetry.ExecuteCellPerceivedWarm, runningStopWatch.elapsedTime, props); + } + } + } + + private updateModel(change: NotebookModelChange) { + // Send to our model using a command. User has done something that changes the model + if (change.source === 'user' && this.model) { + // Note, originally this was posted with a command but sometimes had problems + // with commands being handled out of order. + this.model.update(change); + } + } + + private async sendInitialCellsToWebView(cells: ICell[], isNotebookTrusted: boolean): Promise { + sendTelemetryEvent(Telemetry.CellCount, undefined, { count: cells.length }); + + return this.postMessage(InteractiveWindowMessages.LoadAllCells, { + cells, + isNotebookTrusted + }); + } + + private async exportAs(): Promise { + const activeEditor = this.editorProvider.activeEditor; + if (!activeEditor || !activeEditor.model) { + return; + } + this.commandManager.executeCommand(Commands.Export, activeEditor.model, undefined); + } + + private logNativeCommand(args: INativeCommand) { + sendTelemetryEvent(args.command); + } + + private async loadCellsComplete() { + if (!this.loadedAllCells) { + this.loadedAllCells = true; + sendTelemetryEvent(Telemetry.NotebookOpenTime, this.startupTimer.elapsedTime); + } + + // If we don't have a server right now, at least show our kernel name (this seems to slow down tests + // too much though) + if (!isTestExecution()) { + const metadata = this.notebookMetadata; + if (!this.notebook && metadata?.kernelspec) { + this.postMessage(InteractiveWindowMessages.UpdateKernel, { + jupyterServerStatus: ServerStatus.NotStarted, + localizedUri: '', + displayName: metadata.kernelspec.display_name ?? metadata.kernelspec.name, + language: translateKernelLanguageToMonaco( + (metadata.kernelspec.language as string) ?? PYTHON_LANGUAGE + ) + }).ignoreErrors(); + } + } + } + + @captureTelemetry(Telemetry.RunByLineStart) + private async handleRunByLine(runByLine: IRunByLine) { + try { + // If there's any payload, it has the code and the id + if (runByLine.cell.id && runByLine.cell.data.cell_type === 'code') { + traceInfo(`Running by line cell ${runByLine.cell.id}`); + + // Clear the result if we've run before + await this.clearResult(runByLine.cell.id); + + // Generate a hash file name for this cell. + const notebook = this.getNotebook(); + if (notebook) { + const hashProvider = getCellHashProvider(notebook); + if (hashProvider) { + // Add the breakpoint to the first line of the cell so we actually stop + // on the first line. + const newSource = splitMultilineString(runByLine.cell.data.source); + newSource.splice(0, -1, 'breakpoint()\n'); + runByLine.cell.data.source = newSource; + + const hashFileName = hashProvider.generateHashFileName( + runByLine.cell, + runByLine.expectedExecutionCount + ); + const code = concatMultilineString(runByLine.cell.data.source); + // Send to ourselves. + await this.submitCode( + code, + Identifiers.EmptyFileName, + 0, + runByLine.cell.id, + runByLine.cell.data, + { + runByLine: true, + hashFileName + } + ); + } + } + } else { + throw new Error('Run by line started with an invalid cell'); + } + } catch (exc) { + // Make this error our cell output + this.sendCellsToWebView([ + { + // tslint:disable-next-line: no-any + data: { ...runByLine.cell.data, outputs: [createErrorOutput(exc)] } as any, // nyc compiler issue + id: runByLine.cell.id, + file: Identifiers.EmptyFileName, + line: 0, + state: CellState.error + } + ]); + + throw exc; + } finally { + if (runByLine.cell && runByLine.cell.id) { + traceInfo(`Finished run by line on cell ${runByLine.cell.id}`); + } + } + } +} diff --git a/src/client/datascience/interactive-ipynb/nativeEditorCommandListener.ts b/src/client/datascience/interactive-ipynb/nativeEditorCommandListener.ts new file mode 100644 index 000000000000..20629faff4ae --- /dev/null +++ b/src/client/datascience/interactive-ipynb/nativeEditorCommandListener.ts @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; + +import { ICommandManager } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import { IDisposableRegistry } from '../../common/types'; +import { captureTelemetry } from '../../telemetry'; +import { CommandSource } from '../../testing/common/constants'; +import { Commands, Telemetry } from '../constants'; +import { IDataScienceCommandListener, IDataScienceErrorHandler, INotebookEditorProvider } from '../types'; + +@injectable() +export class NativeEditorCommandListener implements IDataScienceCommandListener { + constructor( + @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, + @inject(INotebookEditorProvider) private provider: INotebookEditorProvider, + @inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler + ) {} + + public register(commandManager: ICommandManager): void { + this.disposableRegistry.push( + commandManager.registerCommand(Commands.NotebookEditorUndoCells, () => this.undoCells()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.NotebookEditorRedoCells, () => this.redoCells()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.NotebookEditorRemoveAllCells, () => this.removeAllCells()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.NotebookEditorInterruptKernel, () => this.interruptKernel()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.NotebookEditorRestartKernel, () => this.restartKernel()) + ); + this.disposableRegistry.push( + commandManager.registerCommand( + Commands.OpenNotebook, + (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => this.openNotebook(file) + ) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.NotebookEditorRunAllCells, () => this.runAllCells()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.NotebookEditorRunSelectedCell, () => this.runSelectedCell()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.NotebookEditorAddCellBelow, () => this.addCellBelow()) + ); + } + + private runAllCells() { + const activeEditor = this.provider.activeEditor; + if (activeEditor) { + activeEditor.runAllCells(); + } + } + + private runSelectedCell() { + const activeEditor = this.provider.activeEditor; + if (activeEditor) { + activeEditor.runSelectedCell(); + } + } + + private addCellBelow() { + const activeEditor = this.provider.activeEditor; + if (activeEditor) { + activeEditor.addCellBelow(); + } + } + + private undoCells() { + const activeEditor = this.provider.activeEditor; + if (activeEditor) { + activeEditor.undoCells(); + } + } + + private redoCells() { + const activeEditor = this.provider.activeEditor; + if (activeEditor) { + activeEditor.redoCells(); + } + } + + private removeAllCells() { + const activeEditor = this.provider.activeEditor; + if (activeEditor) { + activeEditor.removeAllCells(); + } + } + + private interruptKernel() { + const activeEditor = this.provider.activeEditor; + if (activeEditor) { + activeEditor.interruptKernel().ignoreErrors(); + } + } + + private async restartKernel() { + const activeEditor = this.provider.activeEditor; + if (activeEditor) { + await activeEditor.restartKernel().catch(traceError.bind('Failed to restart kernel')); + } + } + + @captureTelemetry(Telemetry.OpenNotebook, { scope: 'command' }, false) + private async openNotebook(file?: Uri): Promise { + if (file && path.extname(file.fsPath).toLocaleLowerCase() === '.ipynb') { + try { + // Then take the contents and load it. + await this.provider.open(file); + } catch (e) { + return this.dataScienceErrorHandler.handleError(e); + } + } + } +} diff --git a/src/client/datascience/interactive-ipynb/nativeEditorOldWebView.ts b/src/client/datascience/interactive-ipynb/nativeEditorOldWebView.ts new file mode 100644 index 000000000000..f179f0646dd6 --- /dev/null +++ b/src/client/datascience/interactive-ipynb/nativeEditorOldWebView.ts @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import * as path from 'path'; +import { CancellationTokenSource, Memento, Uri, WebviewPanel } from 'vscode'; + +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + ILiveShareApi, + IWebviewPanelProvider, + IWorkspaceService +} from '../../common/application/types'; +import { traceError } from '../../common/logger'; + +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IExperimentsManager +} from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { captureTelemetry } from '../../telemetry'; +import { Commands, Telemetry } from '../constants'; +import { IDataViewerFactory } from '../data-viewing/types'; +import { InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes'; +import { KernelSelector } from '../jupyter/kernels/kernelSelector'; +import { INotebookStorageProvider } from '../notebookStorage/notebookStorageProvider'; +import { + ICodeCssGenerator, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IInteractiveWindowListener, + IJupyterDebugger, + IJupyterVariableDataProviderFactory, + IJupyterVariables, + INotebookEditorProvider, + INotebookExporter, + INotebookImporter, + INotebookModel, + INotebookProvider, + IStatusProvider, + IThemeFinder, + ITrustService +} from '../types'; +import { NativeEditor } from './nativeEditor'; +import { NativeEditorSynchronizer } from './nativeEditorSynchronizer'; + +enum AskForSaveResult { + Yes, + No, + Cancel +} + +export class NativeEditorOldWebView extends NativeEditor { + public readonly type = 'old'; + public get visible(): boolean { + return this.viewState.visible; + } + public get active(): boolean { + return this.viewState.active; + } + + private isPromptingToSaveToDisc: boolean = false; + + constructor( + listeners: IInteractiveWindowListener[], + liveShare: ILiveShareApi, + applicationShell: IApplicationShell, + documentManager: IDocumentManager, + provider: IWebviewPanelProvider, + disposables: IDisposableRegistry, + cssGenerator: ICodeCssGenerator, + themeFinder: IThemeFinder, + statusProvider: IStatusProvider, + fs: IDataScienceFileSystem, + configuration: IConfigurationService, + commandManager: ICommandManager, + jupyterExporter: INotebookExporter, + workspaceService: IWorkspaceService, + synchronizer: NativeEditorSynchronizer, + editorProvider: INotebookEditorProvider, + dataExplorerFactory: IDataViewerFactory, + + jupyterVariableDataProviderFactory: IJupyterVariableDataProviderFactory, + jupyterVariables: IJupyterVariables, + jupyterDebugger: IJupyterDebugger, + importer: INotebookImporter, + errorHandler: IDataScienceErrorHandler, + globalStorage: Memento, + workspaceStorage: Memento, + experimentsManager: IExperimentsManager, + asyncRegistry: IAsyncDisposableRegistry, + notebookProvider: INotebookProvider, + useCustomEditorApi: boolean, + private readonly storage: INotebookStorageProvider, + trustService: ITrustService, + expService: IExperimentService, + model: INotebookModel, + webviewPanel: WebviewPanel | undefined, + selector: KernelSelector + ) { + super( + listeners, + liveShare, + applicationShell, + documentManager, + provider, + disposables, + cssGenerator, + themeFinder, + statusProvider, + fs, + configuration, + commandManager, + jupyterExporter, + workspaceService, + synchronizer, + editorProvider, + dataExplorerFactory, + jupyterVariableDataProviderFactory, + jupyterVariables, + jupyterDebugger, + importer, + errorHandler, + globalStorage, + workspaceStorage, + experimentsManager, + asyncRegistry, + notebookProvider, + useCustomEditorApi, + trustService, + expService, + model, + webviewPanel, + selector + ); + asyncRegistry.push(this); + // No ui syncing in old notebooks. + synchronizer.disable(); + + // Update our title to match + this.setTitle(path.basename(model.file.fsPath)); + + // Update dirty if model started out that way + if (this.model?.isDirty) { + this.setDirty().ignoreErrors(); + } + + this.model?.changed(() => { + if (this.model?.isDirty) { + this.setDirty().ignoreErrors(); + } else { + this.setClean().ignoreErrors(); + } + }); + } + + protected async close(): Promise { + // Ask user if they want to save. It seems hotExit has no bearing on + // whether or not we should ask + if (this.isDirty) { + const askResult = await this.askForSave(); + switch (askResult) { + case AskForSaveResult.Yes: + // Save the file + await this.saveToDisk(); + + // Close it + await super.close(); + break; + + case AskForSaveResult.No: + // If there were changes, delete them + if (this.model) { + await this.storage.deleteBackup(this.model); + } + // Close it + await super.close(); + break; + + default: { + await super.close(); + await this.reopen(); + break; + } + } + } else { + // Not dirty, just close normally. + await super.close(); + } + } + + protected saveAll() { + this.saveToDisk().ignoreErrors(); + } + + /** + * Used closed notebook with unsaved changes, then when prompted they clicked cancel. + * Clicking cancel means we need to keep the nb open. + * Hack is to re-open nb with old changes. + */ + private async reopen(): Promise { + if (this.model) { + // Skip doing this if auto save is enabled. + const filesConfig = this.workspaceService.getConfiguration('files', this.file); + const autoSave = filesConfig.get('autoSave', 'off'); + if (autoSave === 'off' || this.isUntitled) { + await this.storage.backup(this.model, new CancellationTokenSource().token); + } + this.commandManager.executeCommand(Commands.OpenNotebookNonCustomEditor, this.model.file).then(noop, noop); + } + } + + private async askForSave(): Promise { + const message1 = localize.DataScience.dirtyNotebookMessage1().format(`${path.basename(this.file.fsPath)}`); + const message2 = localize.DataScience.dirtyNotebookMessage2(); + const yes = localize.DataScience.dirtyNotebookYes(); + const no = localize.DataScience.dirtyNotebookNo(); + const result = await this.applicationShell.showInformationMessage( + // tslint:disable-next-line: messages-must-be-localized + `${message1}\n${message2}`, + { modal: true }, + yes, + no + ); + switch (result) { + case yes: + return AskForSaveResult.Yes; + + case no: + return AskForSaveResult.No; + + default: + return AskForSaveResult.Cancel; + } + } + private async setDirty(): Promise { + // Then update dirty flag. + if (this.isDirty) { + this.setTitle(`${path.basename(this.file.fsPath)}*`); + + // Tell the webview we're dirty + await this.postMessage(InteractiveWindowMessages.NotebookDirty); + + // Tell listeners we're dirty + this.modifiedEvent.fire(this); + } + } + + private async setClean(): Promise { + if (!this.isDirty) { + this.setTitle(`${path.basename(this.file.fsPath)}`); + await this.postMessage(InteractiveWindowMessages.NotebookClean); + } + } + + @captureTelemetry(Telemetry.Save, undefined, true) + private async saveToDisk(): Promise { + // If we're already in the middle of prompting the user to save, then get out of here. + // We could add a debounce decorator, unfortunately that slows saving (by waiting for no more save events to get sent). + if ((this.isPromptingToSaveToDisc && this.isUntitled) || !this.model) { + return; + } + try { + if (!this.isUntitled) { + await this.commandManager.executeCommand(Commands.SaveNotebookNonCustomEditor, this.model); + this.savedEvent.fire(this); + return; + } + // Ask user for a save as dialog if no title + let fileToSaveTo: Uri | undefined = this.file; + + this.isPromptingToSaveToDisc = true; + const filtersKey = localize.DataScience.dirtyNotebookDialogFilter(); + const filtersObject: { [name: string]: string[] } = {}; + filtersObject[filtersKey] = ['ipynb']; + + const defaultUri = + Array.isArray(this.workspaceService.workspaceFolders) && + this.workspaceService.workspaceFolders.length > 0 + ? this.workspaceService.workspaceFolders[0].uri + : undefined; + fileToSaveTo = await this.applicationShell.showSaveDialog({ + saveLabel: localize.DataScience.dirtyNotebookDialogTitle(), + filters: filtersObject, + defaultUri + }); + + if (fileToSaveTo) { + await this.commandManager.executeCommand( + Commands.SaveAsNotebookNonCustomEditor, + this.model, + fileToSaveTo + ); + this.savedEvent.fire(this); + } + } catch (e) { + traceError('Failed to Save nb', e); + } finally { + this.isPromptingToSaveToDisc = false; + } + } +} diff --git a/src/client/datascience/interactive-ipynb/nativeEditorProviderOld.ts b/src/client/datascience/interactive-ipynb/nativeEditorProviderOld.ts new file mode 100644 index 000000000000..d873a9773050 --- /dev/null +++ b/src/client/datascience/interactive-ipynb/nativeEditorProviderOld.ts @@ -0,0 +1,413 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { CancellationTokenSource, Memento, TextDocument, TextEditor, Uri, WebviewPanel } from 'vscode'; + +import { CancellationToken } from 'vscode-jsonrpc'; +import { + IApplicationShell, + ICommandManager, + ICustomEditorService, + IDocumentManager, + ILiveShareApi, + IWebviewPanelProvider, + IWorkspaceService +} from '../../common/application/types'; +import { JUPYTER_LANGUAGE, UseCustomEditorApi } from '../../common/constants'; + +import { + GLOBAL_MEMENTO, + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IExperimentsManager, + IMemento, + WORKSPACE_MEMENTO +} from '../../common/types'; +import { createDeferred } from '../../common/utils/async'; +import { isNotebookCell, noop } from '../../common/utils/misc'; +import { IServiceContainer } from '../../ioc/types'; +import { Commands, Identifiers } from '../constants'; +import { IDataViewerFactory } from '../data-viewing/types'; +import { NotebookModelChange } from '../interactive-common/interactiveWindowTypes'; +import { KernelSelector } from '../jupyter/kernels/kernelSelector'; +import { NativeEditorProvider } from '../notebookStorage/nativeEditorProvider'; +import { INotebookStorageProvider } from '../notebookStorage/notebookStorageProvider'; +import { VSCodeNotebookModel } from '../notebookStorage/vscNotebookModel'; +import { + ICodeCssGenerator, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IInteractiveWindowListener, + IJupyterDebugger, + IJupyterVariableDataProviderFactory, + IJupyterVariables, + INotebookEditor, + INotebookEditorProvider, + INotebookExporter, + INotebookImporter, + INotebookModel, + INotebookProvider, + IStatusProvider, + IThemeFinder, + ITrustService +} from '../types'; +import { NativeEditor } from './nativeEditor'; +import { NativeEditorOldWebView } from './nativeEditorOldWebView'; +import { NativeEditorSynchronizer } from './nativeEditorSynchronizer'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const debounce = require('lodash/debounce') as typeof import('lodash/debounce'); + +@injectable() +export class NativeEditorProviderOld extends NativeEditorProvider { + public get activeEditor(): INotebookEditor | undefined { + const active = [...this.activeEditors.entries()].find((e) => e[1].active); + if (active) { + return active[1]; + } + } + + public get editors(): INotebookEditor[] { + return [...this.activeEditors.values()]; + } + private activeEditors: Map = new Map(); + private readonly _autoSaveNotebookInHotExitFile = new WeakMap(); + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(IWorkspaceService) workspace: IWorkspaceService, + @inject(IConfigurationService) configuration: IConfigurationService, + @inject(ICustomEditorService) customEditorService: ICustomEditorService, + @inject(IDataScienceFileSystem) fs: IDataScienceFileSystem, + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(ICommandManager) private readonly cmdManager: ICommandManager, + @inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler, + @inject(INotebookStorageProvider) storage: INotebookStorageProvider, + @inject(INotebookProvider) notebookProvider: INotebookProvider + ) { + super( + serviceContainer, + asyncRegistry, + disposables, + workspace, + configuration, + customEditorService, + storage, + notebookProvider, + fs + ); + + // No live share sync required as open document from vscode will give us our contents. + + this.disposables.push( + this.documentManager.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditorHandler.bind(this)) + ); + this.disposables.push( + this.cmdManager.registerCommand(Commands.SaveNotebookNonCustomEditor, async (model: INotebookModel) => { + await this.storage.save(model, new CancellationTokenSource().token); + }) + ); + this.disposables.push( + this.cmdManager.registerCommand( + Commands.SaveAsNotebookNonCustomEditor, + async (model: INotebookModel, targetResource: Uri) => { + await this.storage.saveAs(model, targetResource); + const customDocument = this.customDocuments.get(model.file.fsPath); + if (customDocument) { + this.customDocuments.delete(model.file.fsPath); + this.customDocuments.set(targetResource.fsPath, { ...customDocument, uri: targetResource }); + } + } + ) + ); + + this.disposables.push( + this.cmdManager.registerCommand(Commands.OpenNotebookNonCustomEditor, async (resource: Uri) => { + await this.open(resource); + }) + ); + + // Since we may have activated after a document was opened, also run open document for all documents. + // This needs to be async though. Iterating over all of these in the .ctor is crashing the extension + // host, so postpone till after the ctor is finished. + setTimeout(() => { + if (this.documentManager.textDocuments && this.documentManager.textDocuments.forEach) { + this.documentManager.textDocuments.forEach((doc) => this.openNotebookAndCloseEditor(doc, false)); + } + }, 0); + } + + public async open(file: Uri): Promise { + // Save a custom document as we use it to search for the object later. + if (!this.customDocuments.has(file.fsPath)) { + // Required for old editor + this.customDocuments.set(file.fsPath, { + uri: file, + dispose: noop + }); + } + + // See if this file is open or not already + let editor = this.activeEditors.get(file.fsPath); + if (!editor) { + // Note: create will fire the open event. + editor = await this.create(file); + } else { + await this.showEditor(editor); + } + return editor; + } + + public async show(file: Uri): Promise { + // See if this file is open or not already + const editor = this.activeEditors.get(file.fsPath); + if (editor) { + await this.showEditor(editor); + } + return editor; + } + + protected openedEditor(e: INotebookEditor) { + super.openedEditor(e); + this.activeEditors.set(e.file.fsPath, e); + this.disposables.push(e.saved(this.onSavedEditor.bind(this, e.file.fsPath))); + this._onDidChangeActiveNotebookEditor.fire(this.activeEditor); + } + + protected async modelEdited(model: INotebookModel, e: NotebookModelChange) { + const actualModel = e.model || model; // Test mocks can screw up bound values. + if (actualModel && e.kind !== 'save' && e.kind !== 'saveAs' && e.source === 'user') { + // This isn't necessary with the custom editor api because the custom editor will + // cause backup to be called appropriately. + let debounceFunc = this._autoSaveNotebookInHotExitFile.get(actualModel); + if (!debounceFunc) { + debounceFunc = debounce(this.autoSaveNotebookInHotExitFile.bind(this, actualModel), 250); + this._autoSaveNotebookInHotExitFile.set(actualModel, debounceFunc); + } + debounceFunc(); + } + } + + protected createNotebookEditor(model: INotebookModel, panel?: WebviewPanel): NativeEditor { + const editor = new NativeEditorOldWebView( + this.serviceContainer.getAll(IInteractiveWindowListener), + this.serviceContainer.get(ILiveShareApi), + this.serviceContainer.get(IApplicationShell), + this.serviceContainer.get(IDocumentManager), + this.serviceContainer.get(IWebviewPanelProvider), + this.serviceContainer.get(IDisposableRegistry), + this.serviceContainer.get(ICodeCssGenerator), + this.serviceContainer.get(IThemeFinder), + this.serviceContainer.get(IStatusProvider), + this.serviceContainer.get(IDataScienceFileSystem), + this.serviceContainer.get(IConfigurationService), + this.serviceContainer.get(ICommandManager), + this.serviceContainer.get(INotebookExporter), + this.serviceContainer.get(IWorkspaceService), + this.serviceContainer.get(NativeEditorSynchronizer), + this.serviceContainer.get(INotebookEditorProvider), + this.serviceContainer.get(IDataViewerFactory), + this.serviceContainer.get(IJupyterVariableDataProviderFactory), + this.serviceContainer.get(IJupyterVariables, Identifiers.ALL_VARIABLES), + this.serviceContainer.get(IJupyterDebugger), + this.serviceContainer.get(INotebookImporter), + this.serviceContainer.get(IDataScienceErrorHandler), + this.serviceContainer.get(IMemento, GLOBAL_MEMENTO), + this.serviceContainer.get(IMemento, WORKSPACE_MEMENTO), + this.serviceContainer.get(IExperimentsManager), + this.serviceContainer.get(IAsyncDisposableRegistry), + this.serviceContainer.get(INotebookProvider), + this.serviceContainer.get(UseCustomEditorApi), + this.serviceContainer.get(INotebookStorageProvider), + this.serviceContainer.get(ITrustService), + this.serviceContainer.get(IExperimentService), + model, + panel, + this.serviceContainer.get(KernelSelector) + ); + this.activeEditors.set(model.file.fsPath, editor); + this.disposables.push(editor.closed(this.onClosedEditor.bind(this))); + this.openedEditor(editor); + return editor; + } + + protected async loadNotebookEditor(resource: Uri, panel?: WebviewPanel) { + const result = await super.loadNotebookEditor(resource, panel); + + // Wait for monaco ready (it's not really useable until it has a language) + const readyPromise = createDeferred(); + const disposable = result.ready(() => readyPromise.resolve()); + await result.show(); + await readyPromise.promise; + disposable.dispose(); + + return result; + } + + private autoSaveNotebookInHotExitFile(model: INotebookModel) { + // Refetch settings each time as they can change before the debounce can happen + const fileSettings = this.workspace.getConfiguration('files', model.file); + // We need to backup, only if auto save if turned off and not an untitled file. + if (fileSettings.get('autoSave', 'off') !== 'off' && !model.isUntitled) { + return; + } + this.storage.backup(model, CancellationToken.None).ignoreErrors(); + } + + /** + * Open ipynb files when user opens an ipynb file. + * + * @private + * @memberof NativeEditorProvider + */ + private onDidChangeActiveTextEditorHandler(editor?: TextEditor) { + // I we're a source control diff view, then ignore this editor. + if (!editor || this.isEditorPartOfDiffView(editor)) { + return; + } + this.openNotebookAndCloseEditor(editor.document, true).ignoreErrors(); + } + + private async showEditor(editor: INotebookEditor) { + await editor.show(); + this._onDidChangeActiveNotebookEditor.fire(this.activeEditor); + } + + private async create(file: Uri): Promise { + let editor = this.activeEditors.get(file.fsPath); + if (!editor) { + editor = await this.loadNotebookEditor(file); + await this.showEditor(editor); + } + return editor; + } + + private onClosedEditor(e: INotebookEditor) { + this.activeEditors.delete(e.file.fsPath); + this._onDidChangeActiveNotebookEditor.fire(this.activeEditor); + } + private onSavedEditor(oldPath: string, e: INotebookEditor) { + // Switch our key for this editor + if (this.activeEditors.has(oldPath)) { + this.activeEditors.delete(oldPath); + } + this.activeEditors.set(e.file.fsPath, e); + + // Remove backup storage + this.loadModel(Uri.file(oldPath)) + .then((m) => this.storage.deleteBackup(m)) + .ignoreErrors(); + } + + private openNotebookAndCloseEditor = async ( + document: TextDocument, + closeDocumentBeforeOpeningNotebook: boolean + ) => { + // See if this is an ipynb file + if (this.isNotebook(document) && this.configuration.getSettings(document.uri).datascience.useNotebookEditor) { + if (await this.isDocumentOpenedInVSCodeNotebook(document)) { + return; + } + + const closeActiveEditorCommand = 'workbench.action.closeActiveEditor'; + try { + const uri = document.uri; + + if (closeDocumentBeforeOpeningNotebook) { + if ( + !this.documentManager.activeTextEditor || + this.documentManager.activeTextEditor.document !== document + ) { + await this.documentManager.showTextDocument(document); + } + await this.cmdManager.executeCommand(closeActiveEditorCommand); + } + + // Open our own editor. + await this.open(uri); + + if (!closeDocumentBeforeOpeningNotebook) { + // Then switch back to the ipynb and close it. + // If we don't do it in this order, the close will switch to the wrong item + await this.documentManager.showTextDocument(document); + await this.cmdManager.executeCommand(closeActiveEditorCommand); + } + } catch (e) { + return this.dataScienceErrorHandler.handleError(e); + } + } + }; + /** + * If the INotebookModel associated with a Notebook is of type VSCodeNotebookModel, then its used with a VSC Notebook. + * I.e. document is already opened in a VSC Notebook. + */ + private async isDocumentOpenedInVSCodeNotebook(document: TextDocument): Promise { + const model = await this.loadModel(document.uri); + // This is temporary code. + return model instanceof VSCodeNotebookModel; + } + /** + * Check if user is attempting to compare two ipynb files. + * If yes, then return `true`, else `false`. + * + * @private + * @param {TextEditor} editor + * @memberof NativeEditorProvider + */ + private isEditorPartOfDiffView(editor?: TextEditor) { + if (!editor) { + return false; + } + // There's no easy way to determine if the user is openeing a diff view. + // One simple way is to check if there are 2 editor opened, and if both editors point to the same file + // One file with the `file` scheme and the other with the `git` scheme. + if (this.documentManager.visibleTextEditors.length <= 1) { + return false; + } + + // If we have both `git` & `file`/`git` schemes for the same file, then we're most likely looking at a diff view. + // Also ensure both editors are in the same view column. + // Possible we have a git diff view (with two editors git and file scheme), and we open the file view + // on the side (different view column). + const gitSchemeEditor = this.documentManager.visibleTextEditors.find( + (editorUri) => + editorUri.document && + editorUri.document.uri.scheme === 'git' && + this.fs.arePathsSame(editorUri.document.uri, editor.document.uri) + ); + + if (!gitSchemeEditor) { + return false; + } + + // Look for other editors with the same file name that have a scheme of file/git and same viewcolumn. + const fileSchemeEditor = this.documentManager.visibleTextEditors.find( + (editorUri) => + editorUri !== gitSchemeEditor && + this.fs.arePathsSame(editorUri.document.uri, editor.document.uri) && + editorUri.viewColumn === gitSchemeEditor.viewColumn + ); + if (!fileSchemeEditor) { + return false; + } + + // Also confirm the document we have passed in, belongs to one of the editors. + // If its not, then its another document (that is not in the diff view). + return gitSchemeEditor === editor || fileSchemeEditor === editor; + } + private isNotebook(document: TextDocument) { + // Skip opening anything from git as we should use the git viewer. + const validUriScheme = document.uri.scheme !== 'git'; + return ( + validUriScheme && + !isNotebookCell(document) && + (document.languageId === JUPYTER_LANGUAGE || + path.extname(document.fileName).toLocaleLowerCase() === '.ipynb') + ); + } +} diff --git a/src/client/datascience/interactive-ipynb/nativeEditorRunByLineListener.ts b/src/client/datascience/interactive-ipynb/nativeEditorRunByLineListener.ts new file mode 100644 index 000000000000..fc2860cae74f --- /dev/null +++ b/src/client/datascience/interactive-ipynb/nativeEditorRunByLineListener.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable, named } from 'inversify'; +import { + DebugAdapterTracker, + DebugAdapterTrackerFactory, + DebugSession, + Event, + EventEmitter, + ProviderResult +} from 'vscode'; + +import { PYTHON_LANGUAGE } from '../../common/constants'; +import { traceInfo } from '../../common/logger'; +import { noop } from '../../common/utils/misc'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { Identifiers, Telemetry } from '../constants'; +import { InteractiveWindowMessages, IRunByLine } from '../interactive-common/interactiveWindowTypes'; +import { ICell, IInteractiveWindowListener, IJupyterDebugService } from '../types'; + +// tslint:disable: no-any +/** + * Native editor listener that responds to run by line commands from the UI and uses + * those commands to control a debug client. + */ +@injectable() +export class NativeEditorRunByLineListener + implements IInteractiveWindowListener, DebugAdapterTrackerFactory, DebugAdapterTracker { + private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ + message: string; + payload: any; + }>(); + private currentCellBeingRun: ICell | undefined; + + constructor( + @inject(IJupyterDebugService) + @named(Identifiers.RUN_BY_LINE_DEBUGSERVICE) + private debugService: IJupyterDebugService + ) { + debugService.registerDebugAdapterTrackerFactory(PYTHON_LANGUAGE, this); + } + + public createDebugAdapterTracker(_session: DebugSession): ProviderResult { + return this; + } + + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + + public onExit() { + this.currentCellBeingRun = undefined; + } + + public onDidSendMessage?(message: any): void { + if (message.type === 'event' && message.event === 'stopped') { + // We've stopped at breakpoint. Get the top most stack frame to figure out our IP + this.handleBreakEvent().ignoreErrors(); + } else if (message.type === 'event' && (message.command === 'next' || message.command === 'continue')) { + this.handleContinueEvent().ignoreErrors(); + } + } + + public onMessage(message: string, payload?: any): void { + switch (message) { + case InteractiveWindowMessages.Interrupt: + if (this.debugService.activeDebugSession && this.currentCellBeingRun) { + sendTelemetryEvent(Telemetry.RunByLineStop); + } + break; + + case InteractiveWindowMessages.Step: + this.handleStep().ignoreErrors(); + break; + + case InteractiveWindowMessages.Continue: + this.handleContinue().ignoreErrors(); + break; + + case InteractiveWindowMessages.RunByLine: + this.saveCell(payload); + break; + + default: + break; + } + } + public dispose(): void | undefined { + noop(); + } + + private saveCell(runByLine: IRunByLine) { + this.currentCellBeingRun = { ...runByLine.cell }; + } + + private async handleBreakEvent() { + // First get the stack + const frames = await this.debugService.getStack(); + + // Then force a variable refresh + await this.debugService.requestVariables(); + + // If we got frames, tell the UI + if (frames && frames.length > 0) { + traceInfo(`Broke into ${frames[0].source?.path}:${frames[0].line}`); + // Tell the UI to move to a new location + this.postEmitter.fire({ + message: InteractiveWindowMessages.ShowBreak, + payload: { frames, cell: this.currentCellBeingRun } + }); + } + } + + @captureTelemetry(Telemetry.RunByLineStep) + private async handleStep() { + // User issued a step command. + this.postEmitter.fire({ message: InteractiveWindowMessages.ShowContinue, payload: this.currentCellBeingRun }); + return this.debugService.step(); + } + + private async handleContinue() { + // User issued a continue command + this.postEmitter.fire({ message: InteractiveWindowMessages.ShowContinue, payload: this.currentCellBeingRun }); + return this.debugService.continue(); + } + + private async handleContinueEvent() { + // Tell the ui to erase the current IP + this.postEmitter.fire({ message: InteractiveWindowMessages.ShowContinue, payload: this.currentCellBeingRun }); + } +} diff --git a/src/client/datascience/interactive-ipynb/nativeEditorSynchronizer.ts b/src/client/datascience/interactive-ipynb/nativeEditorSynchronizer.ts new file mode 100644 index 000000000000..74d7ecd1c734 --- /dev/null +++ b/src/client/datascience/interactive-ipynb/nativeEditorSynchronizer.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; + +import { IInteractiveWindowMapping, InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes'; +import { SyncPayload } from '../interactive-common/types'; +import { IDataScienceFileSystem, INotebookEditor } from '../types'; + +// tslint:disable: no-any + +type UserActionNotificationCallback = ( + type: T, + payload?: M[T] +) => void; + +@injectable() +export class NativeEditorSynchronizer { + private registeredNotebooks = new Map(); + private enabled = true; + constructor(@inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem) {} + public notifyUserAction(message: SyncPayload, editor: INotebookEditor) { + if (!this.enabled) { + return; + } + this.registeredNotebooks.forEach((cb, item) => { + if (item !== editor && this.fs.arePathsSame(item.file, editor.file)) { + cb(InteractiveWindowMessages.Sync, message as any); + } + }); + } + public subscribeToUserActions(editor: INotebookEditor, cb: UserActionNotificationCallback) { + this.registeredNotebooks.set(editor, cb); + } + public disable() { + this.enabled = false; + this.registeredNotebooks.clear(); + } +} diff --git a/src/client/datascience/interactive-ipynb/nativeEditorViewTracker.ts b/src/client/datascience/interactive-ipynb/nativeEditorViewTracker.ts new file mode 100644 index 000000000000..bdf71a4cdb6c --- /dev/null +++ b/src/client/datascience/interactive-ipynb/nativeEditorViewTracker.ts @@ -0,0 +1,77 @@ +import { inject, injectable, named } from 'inversify'; +import { Memento, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { UseCustomEditorApi } from '../../common/constants'; +import { IDisposableRegistry, IMemento, WORKSPACE_MEMENTO } from '../../common/types'; +import { INotebookEditor, INotebookEditorProvider } from '../types'; + +const MEMENTO_KEY = 'nativeEditorViewTracking'; +/** + * This class tracks opened notebooks and stores the list of files in a memento. On next activation + * this list of files is then opened. + * Untitled files are tracked too, but they should only open if they're dirty. + */ +@injectable() +export class NativeEditorViewTracker implements IExtensionSingleActivationService { + constructor( + @inject(INotebookEditorProvider) private readonly editorProvider: INotebookEditorProvider, + @inject(IMemento) @named(WORKSPACE_MEMENTO) private readonly workspaceMemento: Memento, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(UseCustomEditorApi) private readonly useCustomEditorApi: boolean + ) { + if (!useCustomEditorApi) { + disposableRegistry.push(editorProvider.onDidOpenNotebookEditor(this.onOpenedEditor.bind(this))); + disposableRegistry.push(editorProvider.onDidCloseNotebookEditor(this.onClosedEditor.bind(this))); + } + } + + public async activate(): Promise { + // On activate get the list and eliminate any dupes that might have snuck in. + const set = new Set(this.workspaceMemento.get(MEMENTO_KEY) || []); + await this.workspaceMemento.update(MEMENTO_KEY, undefined); + + // Then open each one if not using the custom editor api + if (!this.useCustomEditorApi) { + set.forEach((l) => { + const uri = Uri.parse(l); + if (uri) { + this.editorProvider.open(uri).ignoreErrors(); + } + }); + } + } + + private onOpenedEditor(editor: INotebookEditor) { + // Save this as a file that should be reopened in this workspace + const list = this.workspaceMemento.get(MEMENTO_KEY) || []; + const fileKey = editor.file.toString(); + + // Skip untitled files. They have to be changed first. + if (!list.includes(fileKey) && (!editor.model.isUntitled || editor.isDirty)) { + this.workspaceMemento.update(MEMENTO_KEY, [...list, fileKey]); + } else if (editor.model.isUntitled && editor.model) { + editor.model.changed(this.onUntitledChanged.bind(this, editor.file)); + } + } + + private onUntitledChanged(file: Uri) { + const list = this.workspaceMemento.get(MEMENTO_KEY) || []; + const fileKey = file.toString(); + if (!list.includes(fileKey)) { + this.workspaceMemento.update(MEMENTO_KEY, [...list, fileKey]); + } + } + + private onClosedEditor(editor: INotebookEditor) { + // Save this as a file that should not be reopened in this workspace if this is the + // last editor for this file + const fileKey = editor.file.toString(); + if (!this.editorProvider.editors.find((e) => e.file.toString() === fileKey && e !== editor)) { + const list = this.workspaceMemento.get(MEMENTO_KEY) || []; + this.workspaceMemento.update( + MEMENTO_KEY, + list.filter((e) => e !== fileKey) + ); + } + } +} diff --git a/src/client/datascience/interactive-ipynb/trustCommandHandler.ts b/src/client/datascience/interactive-ipynb/trustCommandHandler.ts new file mode 100644 index 000000000000..3718379cc804 --- /dev/null +++ b/src/client/datascience/interactive-ipynb/trustCommandHandler.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { commands, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IApplicationShell, ICommandManager } from '../../common/application/types'; +import { ContextKey } from '../../common/contextKey'; +import '../../common/extensions'; +import { IDisposableRegistry } from '../../common/types'; +import { swallowExceptions } from '../../common/utils/decorators'; +import { DataScience } from '../../common/utils/localize'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Commands, Telemetry } from '../constants'; +import { INotebookStorageProvider } from '../notebookStorage/notebookStorageProvider'; +import { INotebookEditorProvider, ITrustService } from '../types'; + +@injectable() +export class TrustCommandHandler implements IExtensionSingleActivationService { + constructor( + @inject(ITrustService) private readonly trustService: ITrustService, + @inject(INotebookEditorProvider) private readonly editorProvider: INotebookEditorProvider, + @inject(INotebookStorageProvider) private readonly storageProvider: INotebookStorageProvider, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry + ) {} + public async activate(): Promise { + this.activateInBackground().ignoreErrors(); + } + public async activateInBackground(): Promise { + const context = new ContextKey('python.datascience.trustfeatureenabled', this.commandManager); + context.set(true).ignoreErrors(); + this.disposables.push(this.commandManager.registerCommand(Commands.TrustNotebook, this.onTrustNotebook, this)); + } + @swallowExceptions('Trusting notebook') + private async onTrustNotebook(uri?: Uri) { + uri = uri ?? this.editorProvider.activeEditor?.file; + if (!uri) { + return; + } + + const model = await this.storageProvider.getOrCreateModel(uri); + if (model.isTrusted) { + return; + } + + const selection = await this.applicationShell.showErrorMessage( + DataScience.launchNotebookTrustPrompt(), + DataScience.trustNotebook(), + DataScience.doNotTrustNotebook(), + DataScience.trustAllNotebooks() + ); + sendTelemetryEvent(Telemetry.NotebookTrustPromptShown); + + switch (selection) { + case DataScience.trustAllNotebooks(): + commands.executeCommand('workbench.action.openSettings', 'python.dataScience.alwaysTrustNotebooks'); + sendTelemetryEvent(Telemetry.TrustAllNotebooks); + break; + case DataScience.trustNotebook(): + // Update model trust + model.trust(); + const contents = model.getContent(); + await this.trustService.trustNotebook(model.file, contents); + sendTelemetryEvent(Telemetry.TrustNotebook); + break; + case DataScience.doNotTrustNotebook(): + sendTelemetryEvent(Telemetry.DoNotTrustNotebook); + break; + default: + break; + } + } +} diff --git a/src/client/datascience/interactive-ipynb/trustService.ts b/src/client/datascience/interactive-ipynb/trustService.ts new file mode 100644 index 000000000000..3b5516cb2e8f --- /dev/null +++ b/src/client/datascience/interactive-ipynb/trustService.ts @@ -0,0 +1,56 @@ +import { createHmac } from 'crypto'; +import { inject, injectable } from 'inversify'; +import { EventEmitter, Uri } from 'vscode'; +import { IConfigurationService } from '../../common/types'; +import { IDigestStorage, ITrustService } from '../types'; + +@injectable() +export class TrustService implements ITrustService { + public get onDidSetNotebookTrust() { + return this._onDidSetNotebookTrust.event; + } + private get alwaysTrustNotebooks() { + return this.configService.getSettings().datascience.alwaysTrustNotebooks; + } + protected readonly _onDidSetNotebookTrust = new EventEmitter(); + constructor( + @inject(IDigestStorage) private readonly digestStorage: IDigestStorage, + @inject(IConfigurationService) private configService: IConfigurationService + ) {} + + /** + * When a notebook is opened, we check the database to see if a trusted checkpoint + * for this notebook exists by computing and looking up its digest. + * If the digest does not exist, the notebook is marked untrusted. + * Once a notebook is loaded in an untrusted state, no code will be executed and no + * markdown will be rendered until notebook as a whole is marked trusted + */ + public async isNotebookTrusted(uri: Uri, notebookContents: string) { + if (this.alwaysTrustNotebooks) { + return true; // Skip check if user manually overrode our trust checking + } + // Compute digest and see if notebook is trusted + const digest = await this.computeDigest(notebookContents); + return this.digestStorage.containsDigest(uri, digest); + } + + /** + * Call this method on a notebook save + * It will add a new trusted checkpoint to the local database if it's safe to do so + * I.e. if the notebook has already been trusted by the user + */ + public async trustNotebook(uri: Uri, notebookContents: string) { + if (!this.alwaysTrustNotebooks) { + // Only update digest store if the user wants us to check trust + const digest = await this.computeDigest(notebookContents); + await this.digestStorage.saveDigest(uri, digest); + this._onDidSetNotebookTrust.fire(); + } + } + + private async computeDigest(notebookContents: string) { + const hmac = createHmac('sha256', await this.digestStorage.key); + hmac.update(notebookContents); + return hmac.digest('hex'); + } +} diff --git a/src/client/datascience/interactive-window/identity.ts b/src/client/datascience/interactive-window/identity.ts new file mode 100644 index 000000000000..745630a85ae1 --- /dev/null +++ b/src/client/datascience/interactive-window/identity.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { Uri } from 'vscode'; +import '../../common/extensions'; +import * as localize from '../../common/utils/localize'; + +let identities: string[] = []; +let createCount = 0; + +export function getDefaultInteractiveIdentity(): Uri { + // Always return the first one + if (identities.length <= 0) { + identities.push(uuid()); + } + return Uri.parse(`history://${identities[0]}`); +} + +// Between test runs reset our identity +export function resetIdentity() { + createCount = 0; + identities = []; +} + +export function createInteractiveIdentity(): Uri { + if (createCount > 0 || identities.length <= 0) { + identities.push(uuid()); + } + createCount += 1; + return Uri.parse(`history://${identities[identities.length - 1]}`); +} + +export function createExportInteractiveIdentity(): Uri { + return Uri.parse(`history://${uuid()}`); +} + +export function getInteractiveWindowTitle(owner: Uri): string { + return localize.DataScience.interactiveWindowTitleFormat().format(path.basename(owner.fsPath)); +} diff --git a/src/client/datascience/interactive-window/interactiveWindow.ts b/src/client/datascience/interactive-window/interactiveWindow.ts new file mode 100644 index 000000000000..613f2625c110 --- /dev/null +++ b/src/client/datascience/interactive-window/interactiveWindow.ts @@ -0,0 +1,537 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import type { nbformat } from '@jupyterlab/coreutils'; +import * as path from 'path'; +import { Event, EventEmitter, Memento, Uri, ViewColumn } from 'vscode'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + ILiveShareApi, + IWebviewPanelProvider, + IWorkspaceService +} from '../../common/application/types'; +import { ContextKey } from '../../common/contextKey'; +import '../../common/extensions'; +import { traceError } from '../../common/logger'; + +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IExperimentsManager, + InteractiveWindowMode, + IPersistentStateFactory, + Resource +} from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import * as localize from '../../common/utils/localize'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { Commands, EditorContexts, Identifiers, Telemetry } from '../constants'; +import { IDataViewerFactory } from '../data-viewing/types'; +import { ExportUtil } from '../export/exportUtil'; +import { InteractiveBase } from '../interactive-common/interactiveBase'; +import { + INotebookIdentity, + InteractiveWindowMessages, + ISubmitNewCell, + NotebookModelChange, + SysInfoReason +} from '../interactive-common/interactiveWindowTypes'; +import { KernelSelector } from '../jupyter/kernels/kernelSelector'; +import { KernelConnectionMetadata } from '../jupyter/kernels/types'; +import { + ICell, + ICodeCssGenerator, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IInteractiveWindow, + IInteractiveWindowInfo, + IInteractiveWindowListener, + IInteractiveWindowLoadable, + IInteractiveWindowProvider, + IJupyterDebugger, + IJupyterVariableDataProviderFactory, + IJupyterVariables, + INotebookExporter, + INotebookModel, + INotebookProvider, + IStatusProvider, + IThemeFinder, + WebViewViewChangeEventArgs +} from '../types'; +import { createInteractiveIdentity, getInteractiveWindowTitle } from './identity'; + +const historyReactDir = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'notebook'); + +export class InteractiveWindow extends InteractiveBase implements IInteractiveWindowLoadable { + public get onDidChangeViewState(): Event { + return this._onDidChangeViewState.event; + } + public get visible(): boolean { + return this.viewState.visible; + } + public get active(): boolean { + return this.viewState.active; + } + + public get closed(): Event { + return this.closedEvent.event; + } + public get owner(): Resource { + return this._owner; + } + public get submitters(): Uri[] { + return this._submitters; + } + public get identity(): Uri { + return this._identity; + } + private _onDidChangeViewState = new EventEmitter(); + private closedEvent: EventEmitter = new EventEmitter(); + private waitingForExportCells: boolean = false; + private trackedJupyterStart: boolean = false; + private _owner: Uri | undefined; + private _identity: Uri = createInteractiveIdentity(); + private _submitters: Uri[] = []; + private pendingHasCell = new Map>(); + private mode: InteractiveWindowMode = 'multiple'; + private loadPromise: Promise; + constructor( + listeners: IInteractiveWindowListener[], + liveShare: ILiveShareApi, + applicationShell: IApplicationShell, + documentManager: IDocumentManager, + statusProvider: IStatusProvider, + provider: IWebviewPanelProvider, + disposables: IDisposableRegistry, + cssGenerator: ICodeCssGenerator, + themeFinder: IThemeFinder, + fs: IDataScienceFileSystem, + configuration: IConfigurationService, + commandManager: ICommandManager, + jupyterExporter: INotebookExporter, + workspaceService: IWorkspaceService, + private interactiveWindowProvider: IInteractiveWindowProvider, + dataExplorerFactory: IDataViewerFactory, + jupyterVariableDataProviderFactory: IJupyterVariableDataProviderFactory, + jupyterVariables: IJupyterVariables, + jupyterDebugger: IJupyterDebugger, + errorHandler: IDataScienceErrorHandler, + private readonly stateFactory: IPersistentStateFactory, + globalStorage: Memento, + workspaceStorage: Memento, + experimentsManager: IExperimentsManager, + notebookProvider: INotebookProvider, + useCustomEditorApi: boolean, + expService: IExperimentService, + private exportUtil: ExportUtil, + owner: Resource, + mode: InteractiveWindowMode, + title: string | undefined, + selector: KernelSelector + ) { + super( + listeners, + liveShare, + applicationShell, + documentManager, + provider, + disposables, + cssGenerator, + themeFinder, + statusProvider, + fs, + configuration, + jupyterExporter, + workspaceService, + dataExplorerFactory, + jupyterVariableDataProviderFactory, + jupyterVariables, + jupyterDebugger, + errorHandler, + commandManager, + globalStorage, + workspaceStorage, + historyReactDir, + [ + path.join(historyReactDir, 'require.js'), + path.join(historyReactDir, 'ipywidgets.js'), + path.join(historyReactDir, 'monaco.bundle.js'), + path.join(historyReactDir, 'commons.initial.bundle.js'), + path.join(historyReactDir, 'interactiveWindow.js') + ], + localize.DataScience.interactiveWindowTitle(), + ViewColumn.Two, + experimentsManager, + notebookProvider, + useCustomEditorApi, + expService, + selector + ); + + // Send a telemetry event to indicate window is opening + sendTelemetryEvent(Telemetry.OpenedInteractiveWindow); + + // Set our owner and first submitter + this._owner = owner; + this.mode = mode; + if (owner) { + this._submitters.push(owner); + } + + // When opening we have to load the web panel. + this.loadPromise = this.loadWebPanel(this.owner ? path.dirname(this.owner.fsPath) : process.cwd()) + .then(async () => { + // Always load our notebook. + await this.ensureConnectionAndNotebook(); + + // Then the initial sys info + await this.addSysInfo(SysInfoReason.Start); + }) + .catch((e) => this.errorHandler.handleError(e)); + + // Update the title if possible + if (this.owner && mode === 'perFile') { + this.setTitle(getInteractiveWindowTitle(this.owner)); + } else if (title) { + this.setTitle(title); + } + } + + public async show(preserveFocus?: boolean): Promise { + await this.loadPromise; + return super.show(preserveFocus); + } + + public dispose() { + super.dispose(); + if (this.notebook) { + this.notebook.dispose().ignoreErrors(); + } + if (this.closedEvent) { + this.closedEvent.fire(this); + } + } + + public addMessage(message: string): Promise { + this.addMessageImpl(message); + return Promise.resolve(); + } + + public changeMode(mode: InteractiveWindowMode): void { + if (this.mode !== mode) { + this.mode = mode; + if (this.owner && mode === 'perFile') { + this.setTitle(getInteractiveWindowTitle(this.owner)); + } + } + } + + public async addCode(code: string, file: Uri, line: number): Promise { + return this.addOrDebugCode(code, file, line, false); + } + + public exportCells() { + // First ask for all cells. Set state to indicate waiting for result + this.waitingForExportCells = true; + + // Telemetry will fire when the export function is called. + this.postMessage(InteractiveWindowMessages.GetAllCells).ignoreErrors(); + } + + // tslint:disable-next-line: no-any + public onMessage(message: string, payload: any) { + super.onMessage(message, payload); + + switch (message) { + case InteractiveWindowMessages.Export: + this.handleMessage(message, payload, this.export); + break; + + case InteractiveWindowMessages.ReturnAllCells: + this.handleMessage(message, payload, this.handleReturnAllCells); + break; + + case InteractiveWindowMessages.UpdateModel: + this.handleMessage(message, payload, this.handleModelChange); + break; + + case InteractiveWindowMessages.ExportNotebookAs: + this.handleMessage(message, payload, this.exportAs); + break; + + case InteractiveWindowMessages.HasCellResponse: + this.handleMessage(message, payload, this.handleHasCellResponse); + break; + + default: + break; + } + } + + public async debugCode(code: string, file: Uri, line: number): Promise { + let saved = true; + // Make sure the file is saved before debugging + const doc = this.documentManager.textDocuments.find((d) => this.fs.areLocalPathsSame(d.fileName, file.fsPath)); + if (doc && doc.isUntitled) { + // Before we start, get the list of documents + const beforeSave = [...this.documentManager.textDocuments]; + + saved = await doc.save(); + + // If that worked, we have to open the new document. It should be + // the new entry in the list + if (saved) { + const diff = this.documentManager.textDocuments.filter((f) => beforeSave.indexOf(f) === -1); + if (diff && diff.length > 0) { + file = diff[0].uri; + + // Open the new document + await this.documentManager.openTextDocument(file); + } + } + } + + // Call the internal method if we were able to save + if (saved) { + return this.addOrDebugCode(code, file, line, true); + } + + return false; + } + + @captureTelemetry(Telemetry.ExpandAll) + public expandAllCells() { + this.postMessage(InteractiveWindowMessages.ExpandAll).ignoreErrors(); + } + + @captureTelemetry(Telemetry.CollapseAll) + public collapseAllCells() { + this.postMessage(InteractiveWindowMessages.CollapseAll).ignoreErrors(); + } + + @captureTelemetry(Telemetry.ScrolledToCell) + public scrollToCell(id: string): void { + this.show(false) + .then(() => { + return this.postMessage(InteractiveWindowMessages.ScrollToCell, { id }); + }) + .ignoreErrors(); + } + + public hasCell(id: string): Promise { + let deferred = this.pendingHasCell.get(id); + if (!deferred) { + deferred = createDeferred(); + this.pendingHasCell.set(id, deferred); + this.postMessage(InteractiveWindowMessages.HasCell, id).ignoreErrors(); + } + return deferred.promise; + } + + public get owningResource(): Resource { + if (this.owner) { + return this.owner; + } + const root = this.workspaceService.rootPath; + if (root) { + return Uri.file(root); + } + return undefined; + } + protected async addSysInfo(reason: SysInfoReason): Promise { + await super.addSysInfo(reason); + + // If `reason == Start`, then this means UI has been updated with the last + // pience of informaiotn (which was sys info), and now UI can be deemed as having been loaded. + // Marking a UI as having been loaded is done by sending a message `LoadAllCells`, even though we're not loading any cells. + // We're merely using existing messages (from NativeEditor). + if (reason === SysInfoReason.Start) { + this.postMessage(InteractiveWindowMessages.LoadAllCells, { cells: [] }).ignoreErrors(); + } + } + protected async onViewStateChanged(args: WebViewViewChangeEventArgs) { + super.onViewStateChanged(args); + this._onDidChangeViewState.fire(); + } + + @captureTelemetry(Telemetry.SubmitCellThroughInput, undefined, false) + // tslint:disable-next-line:no-any + protected submitNewCell(info: ISubmitNewCell) { + // If there's any payload, it has the code and the id + if (info && info.code && info.id) { + // Send to ourselves. + this.submitCode(info.code, Identifiers.EmptyFileName, 0, info.id).ignoreErrors(); + + // Activate the other side, and send as if came from a file + this.interactiveWindowProvider + .synchronize(this) + .then((_v) => { + this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { + code: info.code, + file: Identifiers.EmptyFileName, + line: 0, + id: info.id, + originator: this.id, + debug: false + }); + }) + .ignoreErrors(); + } + } + + protected get notebookMetadata(): nbformat.INotebookMetadata | undefined { + return undefined; + } + + protected async updateNotebookOptions(_kernelConnection: KernelConnectionMetadata): Promise { + // Do nothing as this data isn't stored in our options. + } + + protected get notebookIdentity(): INotebookIdentity { + // Use this identity for the lifetime of the notebook + return { + resource: this._identity, + type: 'interactive' + }; + } + + protected updateContexts(info: IInteractiveWindowInfo | undefined) { + // This should be called by the python interactive window every + // time state changes. We use this opportunity to update our + // extension contexts + if (this.commandManager && this.commandManager.executeCommand) { + const interactiveContext = new ContextKey(EditorContexts.HaveInteractive, this.commandManager); + interactiveContext.set(!this.isDisposed).catch(); + const interactiveCellsContext = new ContextKey(EditorContexts.HaveInteractiveCells, this.commandManager); + const redoableContext = new ContextKey(EditorContexts.HaveRedoableCells, this.commandManager); + const hasCellSelectedContext = new ContextKey(EditorContexts.HaveCellSelected, this.commandManager); + if (info) { + interactiveCellsContext.set(info.cellCount > 0).catch(); + redoableContext.set(info.redoCount > 0).catch(); + hasCellSelectedContext.set(info.selectedCell ? true : false).catch(); + } else { + interactiveCellsContext.set(false).catch(); + redoableContext.set(false).catch(); + hasCellSelectedContext.set(false).catch(); + } + } + } + + protected async closeBecauseOfFailure(_exc: Error): Promise { + this.dispose(); + } + protected ensureConnectionAndNotebook(): Promise { + // Keep track of users who have used interactive window in a worksapce folder. + // To be used if/when changing workflows related to startup of jupyter. + if (!this.trackedJupyterStart) { + this.trackedJupyterStart = true; + const store = this.stateFactory.createGlobalPersistentState('INTERACTIVE_WINDOW_USED', false); + store.updateValue(true).ignoreErrors(); + } + return super.ensureConnectionAndNotebook(); + } + + private async addOrDebugCode(code: string, file: Uri, line: number, debug: boolean): Promise { + if (this.owner && !this.fs.areLocalPathsSame(file.fsPath, this.owner.fsPath)) { + sendTelemetryEvent(Telemetry.NewFileForInteractiveWindow); + } + // Update the owner for this window if not already set + if (!this._owner) { + this._owner = file; + + // Update the title if we're in per file mode + if (this.mode === 'perFile') { + this.setTitle(getInteractiveWindowTitle(file)); + } + } + + // Add to the list of 'submitters' for this window. + if (!this._submitters.find((s) => this.fs.areLocalPathsSame(s.fsPath, file.fsPath))) { + this._submitters.push(file); + } + + // Make sure our web panel opens. + await this.show(); + + // Tell the webpanel about the new directory. + this.updateCwd(path.dirname(file.fsPath)); + + // Call the internal method. + return this.submitCode(code, file.fsPath, line, undefined, undefined, debug ? { runByLine: false } : undefined); + } + + @captureTelemetry(Telemetry.ExportNotebookInteractive, undefined, false) + // tslint:disable-next-line: no-any no-empty + private async export(cells: ICell[]) { + // Should be an array of cells + if (cells && this.applicationShell) { + // Indicate busy + this.startProgress(); + try { + const filtersKey = localize.DataScience.exportDialogFilter(); + const filtersObject: Record = {}; + filtersObject[filtersKey] = ['ipynb']; + + // Bring up the open file dialog box + const uri = await this.applicationShell.showSaveDialog({ + saveLabel: localize.DataScience.exportDialogTitle(), + filters: filtersObject + }); + if (uri) { + await this.jupyterExporter.exportToFile(cells, uri.fsPath); + } + } finally { + this.stopProgress(); + } + } + } + + private async exportAs(cells: ICell[]) { + let model: INotebookModel; + + this.startProgress(); + try { + model = await this.exportUtil.getModelFromCells(cells); + } finally { + this.stopProgress(); + } + if (model) { + let defaultFileName; + if (this.submitters && this.submitters.length) { + const lastSubmitter = this.submitters[this.submitters.length - 1]; + defaultFileName = path.basename(lastSubmitter.fsPath, path.extname(lastSubmitter.fsPath)); + } + this.commandManager.executeCommand(Commands.Export, model, defaultFileName); + } + } + + private handleModelChange(update: NotebookModelChange) { + // Send telemetry for delete and delete all. We don't send telemetry for the other updates yet + if (update.source === 'user') { + if (update.kind === 'remove_all') { + sendTelemetryEvent(Telemetry.DeleteAllCells); + } else if (update.kind === 'remove') { + sendTelemetryEvent(Telemetry.DeleteCell); + } + } + } + + // tslint:disable-next-line:no-any + private handleReturnAllCells(cells: ICell[]) { + // See what we're waiting for. + if (this.waitingForExportCells) { + this.export(cells).catch((ex) => traceError('Error exporting:', ex)); + } + } + + private handleHasCellResponse(response: { id: string; result: boolean }) { + const deferred = this.pendingHasCell.get(response.id); + if (deferred) { + deferred.resolve(response.result); + this.pendingHasCell.delete(response.id); + } + } +} diff --git a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts new file mode 100644 index 000000000000..f68d89c200c1 --- /dev/null +++ b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts @@ -0,0 +1,499 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; +import * as uuid from 'uuid/v4'; +import { Range, TextDocument, Uri } from 'vscode'; +import { CancellationToken, CancellationTokenSource } from 'vscode-jsonrpc'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../../common/application/types'; +import { CancellationError } from '../../common/cancellation'; +import { PYTHON_LANGUAGE } from '../../common/constants'; +import { traceError, traceInfo } from '../../common/logger'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { captureTelemetry } from '../../telemetry'; +import { CommandSource } from '../../testing/common/constants'; +import { generateCellRangesFromDocument, generateCellsFromDocument } from '../cellFactory'; +import { Commands, Telemetry } from '../constants'; +import { ExportFormat, IExportManager } from '../export/types'; +import { JupyterInstallError } from '../jupyter/jupyterInstallError'; +import { INotebookStorageProvider } from '../notebookStorage/notebookStorageProvider'; +import { + IDataScienceCommandListener, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IInteractiveBase, + IInteractiveWindowProvider, + IJupyterExecution, + INotebook, + INotebookEditorProvider, + INotebookExporter, + INotebookProvider, + IStatusProvider +} from '../types'; +import { createExportInteractiveIdentity } from './identity'; + +@injectable() +export class InteractiveWindowCommandListener implements IDataScienceCommandListener { + constructor( + @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, + @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, + @inject(INotebookExporter) private jupyterExporter: INotebookExporter, + @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, + @inject(INotebookProvider) private notebookProvider: INotebookProvider, + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IApplicationShell) private applicationShell: IApplicationShell, + @inject(IDataScienceFileSystem) private fileSystem: IDataScienceFileSystem, + @inject(IConfigurationService) private configuration: IConfigurationService, + @inject(IStatusProvider) private statusProvider: IStatusProvider, + @inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler, + @inject(INotebookEditorProvider) protected ipynbProvider: INotebookEditorProvider, + @inject(IExportManager) private exportManager: IExportManager, + @inject(INotebookStorageProvider) private notebookStorageProvider: INotebookStorageProvider + ) {} + + public register(commandManager: ICommandManager): void { + let disposable = commandManager.registerCommand(Commands.CreateNewInteractive, () => + this.createNewInteractiveWindow() + ); + this.disposableRegistry.push(disposable); + disposable = commandManager.registerCommand( + Commands.ImportNotebook, + (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { + return this.listenForErrors(() => { + if (file) { + return this.importNotebookOnFile(file); + } else { + return this.importNotebook(); + } + }); + } + ); + this.disposableRegistry.push(disposable); + disposable = commandManager.registerCommand( + Commands.ImportNotebookFile, + (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { + return this.listenForErrors(() => { + if (file) { + return this.importNotebookOnFile(file); + } else { + return this.importNotebook(); + } + }); + } + ); + this.disposableRegistry.push(disposable); + disposable = commandManager.registerCommand( + Commands.ExportFileAsNotebook, + (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { + return this.listenForErrors(() => { + if (file) { + return this.exportFile(file); + } else { + const activeEditor = this.documentManager.activeTextEditor; + if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { + return this.exportFile(activeEditor.document.uri); + } + } + + return Promise.resolve(); + }); + } + ); + this.disposableRegistry.push(disposable); + disposable = commandManager.registerCommand( + Commands.ExportFileAndOutputAsNotebook, + (file: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { + return this.listenForErrors(() => { + if (file) { + return this.exportFileAndOutput(file); + } else { + const activeEditor = this.documentManager.activeTextEditor; + if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { + return this.exportFileAndOutput(activeEditor.document.uri); + } + } + return Promise.resolve(); + }); + } + ); + this.disposableRegistry.push(disposable); + this.disposableRegistry.push(commandManager.registerCommand(Commands.UndoCells, () => this.undoCells())); + this.disposableRegistry.push(commandManager.registerCommand(Commands.RedoCells, () => this.redoCells())); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.RemoveAllCells, () => this.removeAllCells()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.InterruptKernel, () => this.interruptKernel()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.RestartKernel, () => this.restartKernel()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.ExpandAllCells, () => this.expandAllCells()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.CollapseAllCells, () => this.collapseAllCells()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.ExportOutputAsNotebook, () => this.exportCells()) + ); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.ScrollToCell, (file: Uri, id: string) => + this.scrollToCell(file, id) + ) + ); + } + + // tslint:disable:no-any + private async listenForErrors(promise: () => Promise): Promise { + let result: any; + try { + result = await promise(); + return result; + } catch (err) { + if (!(err instanceof CancellationError)) { + if (err.message) { + traceError(err.message); + this.applicationShell.showErrorMessage(err.message); + } else { + traceError(err.toString()); + this.applicationShell.showErrorMessage(err.toString()); + } + } else { + traceInfo('Canceled'); + } + } + return result; + } + + private showInformationMessage(message: string, question?: string): Thenable { + if (question) { + return this.applicationShell.showInformationMessage(message, question); + } else { + return this.applicationShell.showInformationMessage(message); + } + } + + @captureTelemetry(Telemetry.ExportPythonFileInteractive, undefined, false) + private async exportFile(file: Uri): Promise { + if (file && file.fsPath && file.fsPath.length > 0) { + // If the current file is the active editor, then generate cells from the document. + const activeEditor = this.documentManager.activeTextEditor; + if (activeEditor && this.fileSystem.arePathsSame(activeEditor.document.uri, file)) { + const cells = generateCellsFromDocument( + activeEditor.document, + this.configuration.getSettings(activeEditor.document.uri).datascience + ); + if (cells) { + const filtersKey = localize.DataScience.exportDialogFilter(); + const filtersObject: { [name: string]: string[] } = {}; + filtersObject[filtersKey] = ['ipynb']; + + // Bring up the save file dialog box + const uri = await this.applicationShell.showSaveDialog({ + saveLabel: localize.DataScience.exportDialogTitle(), + filters: filtersObject + }); + await this.waitForStatus( + async () => { + if (uri) { + let directoryChange; + const settings = this.configuration.getSettings(activeEditor.document.uri); + if (settings.datascience.changeDirOnImportExport) { + directoryChange = uri; + } + + const notebook = await this.jupyterExporter.translateToNotebook( + cells, + directoryChange?.fsPath + ); + await this.fileSystem.writeFile(uri, JSON.stringify(notebook)); + } + }, + localize.DataScience.exportingFormat(), + file.fsPath + ); + // When all done, show a notice that it completed. + if (uri && uri.fsPath) { + const openQuestion1 = localize.DataScience.exportOpenQuestion1(); + const openQuestion2 = (await this.jupyterExecution.isSpawnSupported()) + ? localize.DataScience.exportOpenQuestion() + : undefined; + const questions = [openQuestion1, ...(openQuestion2 ? [openQuestion2] : [])]; + const selection = await this.applicationShell.showInformationMessage( + localize.DataScience.exportDialogComplete().format(uri.fsPath), + ...questions + ); + if (selection === openQuestion1) { + await this.ipynbProvider.open(uri); + } + if (selection === openQuestion2) { + // If the user wants to, open the notebook they just generated. + this.jupyterExecution.spawnNotebook(uri.fsPath).ignoreErrors(); + } + } + } + } + } + } + + @captureTelemetry(Telemetry.ExportPythonFileAndOutputInteractive, undefined, false) + private async exportFileAndOutput(file: Uri): Promise { + if (file && file.fsPath && file.fsPath.length > 0 && (await this.jupyterExecution.isNotebookSupported())) { + // If the current file is the active editor, then generate cells from the document. + const activeEditor = this.documentManager.activeTextEditor; + if ( + activeEditor && + activeEditor.document && + this.fileSystem.arePathsSame(activeEditor.document.uri, file) + ) { + const ranges = generateCellRangesFromDocument(activeEditor.document); + if (ranges.length > 0) { + // Ask user for path + const output = await this.showExportDialog(); + + // If that worked, we need to start a jupyter server to get our output values. + // In the future we could potentially only update changed cells. + if (output) { + // Create a cancellation source so we can cancel starting the jupyter server if necessary + const cancelSource = new CancellationTokenSource(); + + // Then wait with status that lets the user cancel + await this.waitForStatus( + () => { + try { + return this.exportCellsWithOutput( + ranges, + activeEditor.document, + output, + cancelSource.token + ); + } catch (err) { + if (!(err instanceof CancellationError)) { + this.showInformationMessage( + localize.DataScience.exportDialogFailed().format(err) + ); + } + } + return Promise.resolve(); + }, + localize.DataScience.exportingFormat(), + file.fsPath, + () => { + cancelSource.cancel(); + } + ); + + // When all done, show a notice that it completed. + const openQuestion1 = localize.DataScience.exportOpenQuestion1(); + const openQuestion2 = (await this.jupyterExecution.isSpawnSupported()) + ? localize.DataScience.exportOpenQuestion() + : undefined; + const questions = [openQuestion1, ...(openQuestion2 ? [openQuestion2] : [])]; + const selection = await this.applicationShell.showInformationMessage( + localize.DataScience.exportDialogComplete().format(output.fsPath), + ...questions + ); + if (selection === openQuestion1) { + await this.ipynbProvider.open(output); + } + if (selection === openQuestion2) { + // If the user wants to, open the notebook they just generated. + this.jupyterExecution.spawnNotebook(output.fsPath).ignoreErrors(); + } + return output; + } + } + } + } else { + await this.dataScienceErrorHandler.handleError( + new JupyterInstallError( + localize.DataScience.jupyterNotSupported().format(await this.jupyterExecution.getNotebookError()), + localize.DataScience.pythonInteractiveHelpLink() + ) + ); + } + } + + private async exportCellsWithOutput( + ranges: { range: Range; title: string }[], + document: TextDocument, + file: Uri, + cancelToken: CancellationToken + ): Promise { + let notebook: INotebook | undefined; + try { + const settings = this.configuration.getSettings(document.uri); + // Create a new notebook + notebook = await this.notebookProvider.getOrCreateNotebook({ identity: createExportInteractiveIdentity() }); + // If that works, then execute all of the cells. + const cells = Array.prototype.concat( + ...(await Promise.all( + ranges.map((r) => { + const code = document.getText(r.range); + return notebook + ? notebook.execute(code, document.fileName, r.range.start.line, uuid(), cancelToken) + : []; + }) + )) + ); + // Then save them to the file + let directoryChange; + if (settings.datascience.changeDirOnImportExport) { + directoryChange = file; + } + const notebookJson = await this.jupyterExporter.translateToNotebook(cells, directoryChange?.fsPath); + await this.fileSystem.writeFile(file, JSON.stringify(notebookJson)); + } finally { + if (notebook) { + await notebook.dispose(); + } + } + } + + private async showExportDialog(): Promise { + const filtersKey = localize.DataScience.exportDialogFilter(); + const filtersObject: { [name: string]: string[] } = {}; + filtersObject[filtersKey] = ['ipynb']; + + // Bring up the save file dialog box + return this.applicationShell.showSaveDialog({ + saveLabel: localize.DataScience.exportDialogTitle(), + filters: filtersObject + }); + } + + private undoCells() { + const interactiveWindow = this.interactiveWindowProvider.activeWindow; + if (interactiveWindow) { + interactiveWindow.undoCells(); + } + } + + private redoCells() { + const interactiveWindow = this.interactiveWindowProvider.activeWindow; + if (interactiveWindow) { + interactiveWindow.redoCells(); + } + } + + private removeAllCells() { + const interactiveWindow = this.interactiveWindowProvider.activeWindow; + if (interactiveWindow) { + interactiveWindow.removeAllCells(); + } + } + + private interruptKernel() { + const interactiveWindow = this.interactiveWindowProvider.activeWindow; + if (interactiveWindow) { + interactiveWindow.interruptKernel().ignoreErrors(); + } + } + + private restartKernel() { + const interactiveWindow = this.interactiveWindowProvider.activeWindow; + if (interactiveWindow) { + interactiveWindow.restartKernel().ignoreErrors(); + } + } + + private expandAllCells() { + const interactiveWindow = this.interactiveWindowProvider.activeWindow; + if (interactiveWindow) { + interactiveWindow.expandAllCells(); + } + } + + private collapseAllCells() { + const interactiveWindow = this.interactiveWindowProvider.activeWindow; + if (interactiveWindow) { + interactiveWindow.collapseAllCells(); + } + } + + private exportCells() { + const interactiveWindow = this.interactiveWindowProvider.activeWindow; + if (interactiveWindow) { + interactiveWindow.exportCells(); + } + } + + @captureTelemetry(Telemetry.CreateNewInteractive, undefined, false) + private async createNewInteractiveWindow(): Promise { + await this.interactiveWindowProvider.getOrCreate(undefined); + } + + private waitForStatus( + promise: () => Promise, + format: string, + file?: string, + canceled?: () => void, + interactiveWindow?: IInteractiveBase + ): Promise { + const message = file ? format.format(file) : format; + return this.statusProvider.waitWithStatus(promise, message, true, undefined, canceled, interactiveWindow); + } + + @captureTelemetry(Telemetry.ImportNotebook, { scope: 'command' }, false) + private async importNotebook(): Promise { + const filtersKey = localize.DataScience.importDialogFilter(); + const filtersObject: { [name: string]: string[] } = {}; + filtersObject[filtersKey] = ['ipynb']; + + const uris = await this.applicationShell.showOpenDialog({ + openLabel: localize.DataScience.importDialogTitle(), + filters: filtersObject + }); + + if (uris && uris.length > 0) { + // Don't call the other overload as we'll end up with double telemetry. + await this.waitForStatus( + async () => { + const contents = await this.fileSystem.readFile(uris[0]); + const model = await this.notebookStorageProvider.createNew(contents); + await this.exportManager.export(ExportFormat.python, model); + }, + localize.DataScience.importingFormat(), + uris[0].fsPath + ); + } + } + + @captureTelemetry(Telemetry.ImportNotebook, { scope: 'file' }, false) + private async importNotebookOnFile(file: Uri): Promise { + if (file.fsPath && file.fsPath.length > 0) { + await this.waitForStatus( + async () => { + const contents = await this.fileSystem.readFile(file); + const model = await this.notebookStorageProvider.createNew(contents); + await this.exportManager.export(ExportFormat.python, model); + }, + localize.DataScience.importingFormat(), + file.fsPath + ); + } + } + + private async scrollToCell(file: Uri, id: string): Promise { + if (id && file) { + // Find the interactive windows that have this file as a submitter + const possibles = this.interactiveWindowProvider.windows.filter( + (w) => w.submitters.findIndex((s) => this.fileSystem.areLocalPathsSame(s.fsPath, file.fsPath)) >= 0 + ); + + // Scroll to cell in the one that has the cell. We need this so + // we don't activate all of them. + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < possibles.length; i += 1) { + if (await possibles[i].hasCell(id)) { + possibles[i].scrollToCell(id); + break; + } + } + } + } +} diff --git a/src/client/datascience/interactive-window/interactiveWindowProvider.ts b/src/client/datascience/interactive-window/interactiveWindowProvider.ts new file mode 100644 index 000000000000..b645b1b4f141 --- /dev/null +++ b/src/client/datascience/interactive-window/interactiveWindowProvider.ts @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable, named } from 'inversify'; +import * as uuid from 'uuid/v4'; +import { ConfigurationTarget, Event, EventEmitter, Memento, Uri } from 'vscode'; +import * as vsls from 'vsls/vscode'; + +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + ILiveShareApi, + IWebviewPanelProvider, + IWorkspaceService +} from '../../common/application/types'; +import { UseCustomEditorApi } from '../../common/constants'; + +import { + GLOBAL_MEMENTO, + IAsyncDisposable, + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IExperimentsManager, + IMemento, + InteractiveWindowMode, + IPersistentStateFactory, + Resource, + WORKSPACE_MEMENTO +} from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import * as localize from '../../common/utils/localize'; +import { IServiceContainer } from '../../ioc/types'; +import { Identifiers, LiveShare, LiveShareCommands } from '../constants'; +import { IDataViewerFactory } from '../data-viewing/types'; +import { ExportUtil } from '../export/exportUtil'; +import { KernelSelector } from '../jupyter/kernels/kernelSelector'; +import { PostOffice } from '../liveshare/postOffice'; +import { + ICodeCssGenerator, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IInteractiveWindow, + IInteractiveWindowListener, + IInteractiveWindowLoadable, + IInteractiveWindowProvider, + IJupyterDebugger, + IJupyterVariableDataProviderFactory, + IJupyterVariables, + INotebookExporter, + INotebookProvider, + IStatusProvider, + IThemeFinder +} from '../types'; +import { InteractiveWindow } from './interactiveWindow'; + +interface ISyncData { + count: number; + waitable: Deferred; +} + +// Export for testing +export const AskedForPerFileSettingKey = 'ds_asked_per_file_interactive'; + +@injectable() +export class InteractiveWindowProvider implements IInteractiveWindowProvider, IAsyncDisposable { + public get onDidChangeActiveInteractiveWindow(): Event { + return this._onDidChangeActiveInteractiveWindow.event; + } + public get activeWindow(): IInteractiveWindow | undefined { + return this._windows.find((w) => w.active && w.visible); + } + public get windows(): ReadonlyArray { + return this._windows; + } + private readonly _onDidChangeActiveInteractiveWindow = new EventEmitter(); + private lastActiveInteractiveWindow: IInteractiveWindow | undefined; + private postOffice: PostOffice; + private id: string; + private pendingSyncs: Map = new Map(); + private _windows: IInteractiveWindowLoadable[] = []; + constructor( + @inject(ILiveShareApi) liveShare: ILiveShareApi, + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(IConfigurationService) private readonly configService: IConfigurationService, + @inject(IMemento) @named(GLOBAL_MEMENTO) private readonly globalMemento: Memento, + @inject(IApplicationShell) private readonly appShell: IApplicationShell + ) { + asyncRegistry.push(this); + + // Create a post office so we can make sure interactive windows are created at the same time + // on both sides. + this.postOffice = new PostOffice(LiveShare.InteractiveWindowProviderService, liveShare); + + // Listen for peer changes + this.postOffice.peerCountChanged((n) => this.onPeerCountChanged(n)); + + // Listen for messages so we force a create on both sides. + this.postOffice + .registerCallback(LiveShareCommands.interactiveWindowCreate, this.onRemoteCreate, this) + .ignoreErrors(); + this.postOffice + .registerCallback(LiveShareCommands.interactiveWindowCreateSync, this.onRemoteSync, this) + .ignoreErrors(); + + // Make a unique id so we can tell who sends a message + this.id = uuid(); + } + + public async getOrCreate(resource: Resource): Promise { + // Ask for a configuration change if appropriate + const mode = await this.getInteractiveMode(resource); + + // See if we already have a match + let result = this.get(resource, mode) as InteractiveWindow; + if (!result) { + // No match. Create a new item. + result = this.create(resource, mode); + + // Wait for monaco ready (it's not really useable until it has a language) + const readyPromise = createDeferred(); + const disposable = result.ready(() => readyPromise.resolve()); + + // Wait for monaco ready + await readyPromise.promise; + disposable.dispose(); + } + + // Wait for synchronization in liveshare + await this.synchronize(result); + + return result; + } + + public dispose(): Promise { + return this.postOffice.dispose(); + } + + public async synchronize(window: IInteractiveWindow): Promise { + // Create a new pending wait if necessary + if (this.postOffice.peerCount > 0 || this.postOffice.role === vsls.Role.Guest) { + const key = window.identity.toString(); + const owner = window.owner?.toString(); + const waitable = createDeferred(); + this.pendingSyncs.set(key, { count: this.postOffice.peerCount, waitable }); + + // Make sure all providers have an active interactive window + await this.postOffice.postCommand(LiveShareCommands.interactiveWindowCreate, this.id, key, owner); + + // Wait for the waitable to be signaled or the peer count on the post office to change + await waitable.promise; + } + } + + protected create(resource: Resource, mode: InteractiveWindowMode): InteractiveWindow { + const title = + mode === 'multiple' || (mode === 'perFile' && !resource) + ? localize.DataScience.interactiveWindowTitleFormat().format(`#${this._windows.length + 1}`) + : undefined; + + // Set it as soon as we create it. The .ctor for the interactive window + // may cause a subclass to talk to the IInteractiveWindowProvider to get the active interactive window. + const result = new InteractiveWindow( + this.serviceContainer.getAll(IInteractiveWindowListener), + this.serviceContainer.get(ILiveShareApi), + this.serviceContainer.get(IApplicationShell), + this.serviceContainer.get(IDocumentManager), + this.serviceContainer.get(IStatusProvider), + this.serviceContainer.get(IWebviewPanelProvider), + this.serviceContainer.get(IDisposableRegistry), + this.serviceContainer.get(ICodeCssGenerator), + this.serviceContainer.get(IThemeFinder), + this.serviceContainer.get(IDataScienceFileSystem), + this.serviceContainer.get(IConfigurationService), + this.serviceContainer.get(ICommandManager), + this.serviceContainer.get(INotebookExporter), + this.serviceContainer.get(IWorkspaceService), + this, + this.serviceContainer.get(IDataViewerFactory), + this.serviceContainer.get(IJupyterVariableDataProviderFactory), + this.serviceContainer.get(IJupyterVariables, Identifiers.ALL_VARIABLES), + this.serviceContainer.get(IJupyterDebugger), + this.serviceContainer.get(IDataScienceErrorHandler), + this.serviceContainer.get(IPersistentStateFactory), + this.serviceContainer.get(IMemento, GLOBAL_MEMENTO), + this.serviceContainer.get(IMemento, WORKSPACE_MEMENTO), + this.serviceContainer.get(IExperimentsManager), + this.serviceContainer.get(INotebookProvider), + this.serviceContainer.get(UseCustomEditorApi), + this.serviceContainer.get(IExperimentService), + this.serviceContainer.get(ExportUtil), + resource, + mode, + title, + this.serviceContainer.get(KernelSelector) + ); + this._windows.push(result); + + // This is the last interactive window at the moment (as we're about to create it) + this.lastActiveInteractiveWindow = result; + + // When shutting down, we fire an event + const handler = result.closed(this.onInteractiveWindowClosed); + this.disposables.push(result); + this.disposables.push(handler); + this.disposables.push(result.onDidChangeViewState(this.raiseOnDidChangeActiveInteractiveWindow.bind(this))); + + // Show in the background + result.show().ignoreErrors(); + + return result; + } + + private async getInteractiveMode(resource: Resource): Promise { + let result = this.configService.getSettings(resource).datascience.interactiveWindowMode; + + // Ask user if still at default value and they're opening a second file. + if ( + result === 'multiple' && + resource && + !this.globalMemento.get(AskedForPerFileSettingKey) && + this._windows.length === 1 + ) { + // See if the first window was tied to a file or not. + const firstWindow = this._windows.find((w) => w.owner); + if (firstWindow) { + this.globalMemento.update(AskedForPerFileSettingKey, true); + const questions = [ + localize.DataScience.interactiveWindowModeBannerSwitchYes(), + localize.DataScience.interactiveWindowModeBannerSwitchNo() + ]; + // Ask user if they'd like to switch to per file or not. + const response = await this.appShell.showInformationMessage( + localize.DataScience.interactiveWindowModeBannerTitle(), + ...questions + ); + if (response === questions[0]) { + result = 'perFile'; + firstWindow.changeMode(result); + await this.configService.updateSetting( + 'dataScience.interactiveWindowMode', + result, + resource, + ConfigurationTarget.Global + ); + } + } + } + return result; + } + + private get(owner: Resource, interactiveMode: InteractiveWindowMode): IInteractiveWindow | undefined { + // Single mode means there's only ever one. + if (interactiveMode === 'single') { + return this._windows.length > 0 ? this._windows[0] : undefined; + } + + // Multiple means use last active window or create a new one + // if not owned. + if (interactiveMode === 'multiple') { + // Owner being undefined means create a new window, othewise use + // the last active window. + return owner ? this.activeWindow || this.lastActiveInteractiveWindow || this._windows[0] : undefined; + } + + // Otherwise match the owner. + return this._windows.find((w) => { + if (!owner && !w.owner) { + return true; + } + if (owner && w.owner && this.fs.areLocalPathsSame(owner.fsPath, w.owner.fsPath)) { + return true; + } + return false; + }); + } + + private raiseOnDidChangeActiveInteractiveWindow() { + // Update last active window (remember changes to the active window) + this.lastActiveInteractiveWindow = this.activeWindow ? this.activeWindow : this.lastActiveInteractiveWindow; + this._onDidChangeActiveInteractiveWindow.fire(this.activeWindow); + } + private onPeerCountChanged(newCount: number) { + // If we're losing peers, resolve all syncs + if (newCount < this.postOffice.peerCount) { + this.pendingSyncs.forEach((v) => v.waitable.resolve()); + this.pendingSyncs.clear(); + } + } + + // tslint:disable-next-line:no-any + private async onRemoteCreate(...args: any[]) { + // Should be 3 args, the originator of the create, the key, and the owner. Key isn't used here + // but it is passed through to the response. + if (args.length > 1 && args[0].toString() !== this.id) { + // The other side is creating a interactive window. Create on this side. We don't need to show + // it as the running of new code should do that. + const owner = args[2] ? Uri.parse(args[2].toString()) : undefined; + const mode = await this.getInteractiveMode(owner); + if (!this.get(owner, mode)) { + this.create(owner, mode); + } + + // Tell the requestor that we got its message (it should be waiting for all peers to sync) + this.postOffice.postCommand(LiveShareCommands.interactiveWindowCreateSync, ...args).ignoreErrors(); + } + } + + // tslint:disable-next-line:no-any + private onRemoteSync(...args: any[]) { + // Should be 3 args, the originator of the create, the key, and the owner (owner used on other call) + if (args.length > 1 && args[0].toString() === this.id) { + // Update our pending wait count on the matching pending sync + const key = args[1].toString(); + const sync = this.pendingSyncs.get(key); + if (sync) { + sync.count -= 1; + if (sync.count <= 0) { + sync.waitable.resolve(); + this.pendingSyncs.delete(key); + } + } + } + } + + private onInteractiveWindowClosed = (interactiveWindow: IInteractiveWindow) => { + this._windows = this._windows.filter((w) => w !== interactiveWindow); + if (this.lastActiveInteractiveWindow === interactiveWindow) { + this.lastActiveInteractiveWindow = this._windows[0]; + } + this.raiseOnDidChangeActiveInteractiveWindow(); + }; +} diff --git a/src/client/datascience/ipywidgets/cdnWidgetScriptSourceProvider.ts b/src/client/datascience/ipywidgets/cdnWidgetScriptSourceProvider.ts new file mode 100644 index 000000000000..47937d0970e6 --- /dev/null +++ b/src/client/datascience/ipywidgets/cdnWidgetScriptSourceProvider.ts @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { sha256 } from 'hash.js'; +import * as path from 'path'; +import request from 'request'; +import { Uri } from 'vscode'; +import { traceError, traceInfo } from '../../common/logger'; +import { TemporaryFile } from '../../common/platform/types'; +import { IConfigurationService, IHttpClient, WidgetCDNs } from '../../common/types'; +import { createDeferred, sleep } from '../../common/utils/async'; +import { IDataScienceFileSystem, ILocalResourceUriConverter } from '../types'; +import { IWidgetScriptSourceProvider, WidgetScriptSource } from './types'; + +// Source borrowed from https://github.com/jupyter-widgets/ipywidgets/blob/54941b7a4b54036d089652d91b39f937bde6b6cd/packages/html-manager/src/libembed-amd.ts#L33 +const unpgkUrl = 'https://unpkg.com/'; +const jsdelivrUrl = 'https://cdn.jsdelivr.net/npm/'; + +// tslint:disable: no-var-requires no-require-imports +const sanitize = require('sanitize-filename'); + +function moduleNameToCDNUrl(cdn: string, moduleName: string, moduleVersion: string) { + let packageName = moduleName; + let fileName = 'index'; // default filename + // if a '/' is present, like 'foo/bar', packageName is changed to 'foo', and path to 'bar' + // We first find the first '/' + let index = moduleName.indexOf('/'); + if (index !== -1 && moduleName[0] === '@') { + // if we have a namespace, it's a different story + // @foo/bar/baz should translate to @foo/bar and baz + // so we find the 2nd '/' + index = moduleName.indexOf('/', index + 1); + } + if (index !== -1) { + fileName = moduleName.substr(index + 1); + packageName = moduleName.substr(0, index); + } + if (cdn === jsdelivrUrl) { + // Js Delivr doesn't support ^ in the version. It needs an exact version + if (moduleVersion.startsWith('^')) { + moduleVersion = moduleVersion.slice(1); + } + // Js Delivr also needs the .js file on the end. + if (!fileName.endsWith('.js')) { + fileName = fileName.concat('.js'); + } + } + return `${cdn}${packageName}@${moduleVersion}/dist/${fileName}`; +} + +function getCDNPrefix(cdn?: WidgetCDNs): string | undefined { + switch (cdn) { + case 'unpkg.com': + return unpgkUrl; + case 'jsdelivr.com': + return jsdelivrUrl; + default: + break; + } +} +/** + * Widget scripts are found in CDN. + * Given an widget module name & version, this will attempt to find the Url on a CDN. + * We'll need to stick to the order of preference prescribed by the user. + */ +export class CDNWidgetScriptSourceProvider implements IWidgetScriptSourceProvider { + private get cdnProviders(): readonly WidgetCDNs[] { + const settings = this.configurationSettings.getSettings(undefined); + return settings.datascience.widgetScriptSources; + } + private cache = new Map(); + constructor( + private readonly configurationSettings: IConfigurationService, + private readonly httpClient: IHttpClient, + private readonly localResourceUriConverter: ILocalResourceUriConverter, + private readonly fs: IDataScienceFileSystem + ) {} + public dispose() { + this.cache.clear(); + } + public async getWidgetScriptSource(moduleName: string, moduleVersion: string): Promise { + // First see if we already have it downloaded. + const key = this.getModuleKey(moduleName, moduleVersion); + const diskPath = path.join(this.localResourceUriConverter.rootScriptFolder.fsPath, key, 'index.js'); + let cached = this.cache.get(key); + let tempFile: TemporaryFile | undefined; + + // Might be on disk, try there first. + if (!cached) { + if (diskPath && (await this.fs.localFileExists(diskPath))) { + const scriptUri = (await this.localResourceUriConverter.asWebviewUri(Uri.file(diskPath))).toString(); + cached = { moduleName, scriptUri, source: 'cdn' }; + this.cache.set(key, cached); + } + } + + // If still not found, download it. + if (!cached) { + try { + // Make sure the disk path directory exists. We'll be downloading it to there. + await this.fs.createLocalDirectory(path.dirname(diskPath)); + + // Then get the first one that returns. + tempFile = await this.downloadFastestCDN(moduleName, moduleVersion); + if (tempFile) { + // Need to copy from the temporary file to our real file (note: VSC filesystem fails to copy so just use straight file system) + await this.fs.copyLocal(tempFile.filePath, diskPath); + + // Now we can generate the script URI so the local converter doesn't try to copy it. + const scriptUri = ( + await this.localResourceUriConverter.asWebviewUri(Uri.file(diskPath)) + ).toString(); + cached = { moduleName, scriptUri, source: 'cdn' }; + } else { + cached = { moduleName }; + } + } catch (exc) { + traceError('Error downloading from CDN: ', exc); + cached = { moduleName }; + } finally { + if (tempFile) { + tempFile.dispose(); + } + } + this.cache.set(key, cached); + } + + return cached; + } + + private async downloadFastestCDN(moduleName: string, moduleVersion: string) { + const deferred = createDeferred(); + Promise.all( + // For each CDN, try to download it. + this.cdnProviders.map((cdn) => + this.downloadFromCDN(moduleName, moduleVersion, cdn).then((t) => { + // First one to get here wins. Meaning the first one that + // returns a valid temporary file. If a request doesn't download it will + // return undefined. + if (!deferred.resolved && t) { + deferred.resolve(t); + } + }) + ) + ) + .then((_a) => { + // If after running all requests, we're still not resolved, then return empty. + // This would happen if both unpkg.com and jsdelivr failed. + if (!deferred.resolved) { + deferred.resolve(undefined); + } + }) + .ignoreErrors(); + + // Note, we only wait until one download finishes. We don't need to wait + // for everybody (hence the use of the deferred) + return deferred.promise; + } + + private async downloadFromCDN( + moduleName: string, + moduleVersion: string, + cdn: WidgetCDNs + ): Promise { + // First validate CDN + const downloadUrl = await this.generateDownloadUri(moduleName, moduleVersion, cdn); + if (downloadUrl) { + // Then see if we can download the file. + try { + return await this.downloadFile(downloadUrl); + } catch (exc) { + // Something goes wrong, just fail + } + } + } + + private async generateDownloadUri( + moduleName: string, + moduleVersion: string, + cdn: WidgetCDNs + ): Promise { + const cdnBaseUrl = getCDNPrefix(cdn); + if (cdnBaseUrl) { + return moduleNameToCDNUrl(cdnBaseUrl, moduleName, moduleVersion); + } + return undefined; + } + + private getModuleKey(moduleName: string, moduleVersion: string) { + return sanitize(sha256().update(`${moduleName}${moduleVersion}`).digest('hex')); + } + + private handleResponse(req: request.Request, filePath: string): Promise { + const deferred = createDeferred(); + // tslint:disable-next-line: no-any + const errorHandler = (e: any) => { + traceError('Error downloading from CDN', e); + deferred.resolve(false); + }; + req.on('response', (r) => { + if (r.statusCode === 200) { + const ws = this.fs.createLocalWriteStream(filePath); + r.on('error', errorHandler) + .pipe(ws) + .on('close', () => deferred.resolve(true)); + } else if (r.statusCode === 429) { + // Special case busy. Sleep for 500 milliseconds + sleep(500) + .then(() => deferred.resolve(false)) + .ignoreErrors(); + } else { + deferred.resolve(false); + } + }).on('error', errorHandler); + return deferred.promise; + } + + private async downloadFile(downloadUrl: string): Promise { + // Create a temp file to download the results to + const tempFile = await this.fs.createTemporaryLocalFile('.js'); + + // Otherwise do an http get on the url. Retry at least 5 times + let retryCount = 5; + let success = false; + while (retryCount > 0 && !success) { + let req: request.Request; + try { + req = await this.httpClient.downloadFile(downloadUrl); + success = await this.handleResponse(req, tempFile.filePath); + } catch (exc) { + traceInfo(`Error downloading from ${downloadUrl}: `, exc); + } finally { + retryCount -= 1; + } + } + + // Once we make it out, return result + if (success) { + return tempFile; + } else { + tempFile.dispose(); + } + } +} diff --git a/src/client/datascience/ipywidgets/constants.ts b/src/client/datascience/ipywidgets/constants.ts new file mode 100644 index 000000000000..a413d7ec7490 --- /dev/null +++ b/src/client/datascience/ipywidgets/constants.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export const WIDGET_MIMETYPE = 'application/vnd.jupyter.widget-view+json'; diff --git a/src/client/datascience/ipywidgets/ipyWidgetMessageDispatcher.ts b/src/client/datascience/ipywidgets/ipyWidgetMessageDispatcher.ts new file mode 100644 index 000000000000..71d7bbd61d76 --- /dev/null +++ b/src/client/datascience/ipywidgets/ipyWidgetMessageDispatcher.ts @@ -0,0 +1,504 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import type { KernelMessage } from '@jupyterlab/services'; +import * as util from 'util'; +import * as uuid from 'uuid/v4'; +import { Event, EventEmitter, Uri } from 'vscode'; +import type { Data as WebSocketData } from 'ws'; +import { traceError, traceInfo } from '../../common/logger'; +import { IDisposable } from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { noop } from '../../common/utils/misc'; +import { deserializeDataViews, serializeDataViews } from '../../common/utils/serializers'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Identifiers, Telemetry } from '../constants'; +import { IInteractiveWindowMapping, IPyWidgetMessages } from '../interactive-common/interactiveWindowTypes'; +import { INotebook, INotebookProvider, KernelSocketInformation } from '../types'; +import { WIDGET_MIMETYPE } from './constants'; +import { IIPyWidgetMessageDispatcher, IPyWidgetMessage } from './types'; + +type PendingMessage = { + resultPromise: Deferred; + startTime: number; +}; + +// tslint:disable: no-any +/** + * This class maps between messages from the react code and talking to a real kernel. + */ +export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher { + public get postMessage(): Event { + return this._postMessageEmitter.event; + } + private readonly commTargetsRegistered = new Set(); + private jupyterLab?: typeof import('@jupyterlab/services'); + private pendingTargetNames = new Set(); + private notebook?: INotebook; + private _postMessageEmitter = new EventEmitter(); + private messageHooks = new Map boolean | PromiseLike>(); + private pendingHookRemovals = new Map(); + private messageHookRequests = new Map>(); + + private readonly disposables: IDisposable[] = []; + private kernelRestartHandlerAttached?: boolean; + private kernelSocketInfo?: KernelSocketInformation; + private sentKernelOptions = false; + private kernelWasConnectedAtleastOnce?: boolean; + private disposed = false; + private pendingMessages: string[] = []; + private subscribedToKernelSocket: boolean = false; + private waitingMessageIds = new Map(); + private totalWaitTime: number = 0; + private totalWaitedMessages: number = 0; + private hookCount: number = 0; + private fullHandleMessage?: { id: string; promise: Deferred }; + /** + * This will be true if user has executed something that has resulted in the use of ipywidgets. + * We make this determinination based on whether we see messages coming from backend kernel of a specific shape. + * E.g. if it contains ipywidget mime type, then widgets are in use. + */ + private isUsingIPyWidgets?: boolean; + private readonly deserialize: (data: string | ArrayBuffer) => KernelMessage.IMessage; + + constructor(private readonly notebookProvider: INotebookProvider, public readonly notebookIdentity: Uri) { + // Always register this comm target. + // Possible auto start is disabled, and when cell is executed with widget stuff, this comm target will not have + // been reigstered, in which case kaboom. As we know this is always required, pre-register this. + this.pendingTargetNames.add('jupyter.widget'); + notebookProvider.onNotebookCreated( + (e) => { + if (e.identity.toString() === notebookIdentity.toString()) { + this.initialize().ignoreErrors(); + } + }, + this, + this.disposables + ); + this.mirrorSend = this.mirrorSend.bind(this); + this.onKernelSocketMessage = this.onKernelSocketMessage.bind(this); + // tslint:disable-next-line: no-require-imports + const jupyterLabSerialize = require('@jupyterlab/services/lib/kernel/serialize') as typeof import('@jupyterlab/services/lib/kernel/serialize'); // NOSONAR + this.deserialize = jupyterLabSerialize.deserialize; + } + public dispose() { + // Send overhead telemetry for our message hooking + this.sendOverheadTelemetry(); + this.disposed = true; + while (this.disposables.length) { + const disposable = this.disposables.shift(); + disposable?.dispose(); // NOSONAR + } + } + + public receiveMessage(message: IPyWidgetMessage): void { + if (process.env.VSC_PYTHON_LOG_IPYWIDGETS && message.message.includes('IPyWidgets_')) { + traceInfo(`IPyWidgetMessage: ${util.inspect(message)}`); + } + switch (message.message) { + case IPyWidgetMessages.IPyWidgets_Ready: + this.sendKernelOptions(); + this.initialize().ignoreErrors(); + break; + case IPyWidgetMessages.IPyWidgets_msg: + this.sendRawPayloadToKernelSocket(message.payload); + break; + case IPyWidgetMessages.IPyWidgets_binary_msg: + this.sendRawPayloadToKernelSocket(deserializeDataViews(message.payload)![0]); + break; + + case IPyWidgetMessages.IPyWidgets_msg_received: + this.onKernelSocketResponse(message.payload); + break; + + case IPyWidgetMessages.IPyWidgets_registerCommTarget: + this.registerCommTarget(message.payload).ignoreErrors(); + break; + + case IPyWidgetMessages.IPyWidgets_RegisterMessageHook: + this.registerMessageHook(message.payload); + break; + + case IPyWidgetMessages.IPyWidgets_RemoveMessageHook: + this.possiblyRemoveMessageHook(message.payload); + break; + + case IPyWidgetMessages.IPyWidgets_MessageHookResult: + this.handleMessageHookResponse(message.payload); + break; + + case IPyWidgetMessages.IPyWidgets_iopub_msg_handled: + this.iopubMessageHandled(message.payload); + break; + + default: + break; + } + } + public sendRawPayloadToKernelSocket(payload?: any) { + this.pendingMessages.push(payload); + this.sendPendingMessages(); + } + public async registerCommTarget(targetName: string) { + this.pendingTargetNames.add(targetName); + await this.initialize(); + } + + public async initialize() { + if (!this.jupyterLab) { + // Lazy load jupyter lab for faster extension loading. + // tslint:disable-next-line:no-require-imports + this.jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); // NOSONAR + } + + // If we have any pending targets, register them now + const notebook = await this.getNotebook(); + if (notebook) { + this.subscribeToKernelSocket(notebook); + this.registerCommTargets(notebook); + } + } + protected raisePostMessage( + message: IPyWidgetMessages, + payload: M[T] + ) { + this._postMessageEmitter.fire({ message, payload }); + } + private subscribeToKernelSocket(notebook: INotebook) { + if (this.subscribedToKernelSocket) { + return; + } + this.subscribedToKernelSocket = true; + // Listen to changes to kernel socket (e.g. restarts or changes to kernel). + notebook.kernelSocket.subscribe((info) => { + // Remove old handlers. + this.kernelSocketInfo?.socket?.removeReceiveHook(this.onKernelSocketMessage); // NOSONAR + this.kernelSocketInfo?.socket?.removeSendHook(this.mirrorSend); // NOSONAR + + if (this.kernelWasConnectedAtleastOnce) { + // this means we restarted the kernel and we now have new information. + // Discard all of the messages upto this point. + while (this.pendingMessages.length) { + this.pendingMessages.shift(); + } + this.sentKernelOptions = false; + this.waitingMessageIds.forEach((d) => d.resultPromise.resolve()); + this.waitingMessageIds.clear(); + this.messageHookRequests.forEach((m) => m.resolve(false)); + this.messageHookRequests.clear(); + this.messageHooks.clear(); + this.sendRestartKernel(); + } + if (!info || !info.socket) { + // No kernel socket information, hence nothing much we can do. + this.kernelSocketInfo = undefined; + return; + } + + this.kernelWasConnectedAtleastOnce = true; + this.kernelSocketInfo = info; + this.kernelSocketInfo.socket?.addReceiveHook(this.onKernelSocketMessage); // NOSONAR + this.kernelSocketInfo.socket?.addSendHook(this.mirrorSend); // NOSONAR + this.sendKernelOptions(); + // Since we have connected to a kernel, send any pending messages. + this.registerCommTargets(notebook); + this.sendPendingMessages(); + }); + } + /** + * Pass this information to UI layer so it can create a dummy kernel with same information. + * Information includes kernel connection info (client id, user name, model, etc). + */ + private sendKernelOptions() { + if (!this.kernelSocketInfo) { + return; + } + if (!this.sentKernelOptions) { + this.sentKernelOptions = true; + this.raisePostMessage(IPyWidgetMessages.IPyWidgets_kernelOptions, this.kernelSocketInfo.options); + } + } + private async mirrorSend(data: any, _cb?: (err?: Error) => void): Promise { + // If this is shell control message, mirror to the other side. This is how + // we get the kernel in the UI to have the same set of futures we have on this side + if (typeof data === 'string') { + const startTime = Date.now(); + // tslint:disable-next-line: no-require-imports + const msg = this.deserialize(data); + if (msg.channel === 'shell' && msg.header.msg_type === 'execute_request') { + const promise = this.mirrorExecuteRequest(msg as KernelMessage.IExecuteRequestMsg); // NOSONAR + // If there are no ipywidgets thusfar in the notebook, then no need to synchronize messages. + if (this.isUsingIPyWidgets) { + await promise; + } + this.totalWaitTime = Date.now() - startTime; + this.totalWaitedMessages += 1; + } + } + } + + private sendRestartKernel() { + this.raisePostMessage(IPyWidgetMessages.IPyWidgets_onRestartKernel, undefined); + } + + private mirrorExecuteRequest(msg: KernelMessage.IExecuteRequestMsg) { + const promise = createDeferred(); + this.waitingMessageIds.set(msg.header.msg_id, { startTime: Date.now(), resultPromise: promise }); + this.raisePostMessage(IPyWidgetMessages.IPyWidgets_mirror_execute, { id: msg.header.msg_id, msg }); + return promise.promise; + } + + // Determine if a message can just be added into the message queue or if we need to wait for it to be + // fully handled on both the UI and extension side before we process the next message incoming + private messageNeedsFullHandle(message: any) { + // We only get a handled callback for iopub messages, so this channel must be iopub + if (message.channel === 'iopub') { + if (message.header?.msg_type === 'comm_msg') { + // IOPub comm messages need to be fully handled + return true; + } + } + + return false; + } + + // Callback from the UI kernel when an iopubMessage has been fully handled + private iopubMessageHandled(payload: any) { + const msgId = payload.id; + // We don't fully handle all iopub messages, so check our id here + if (this.fullHandleMessage && this.fullHandleMessage.id === msgId) { + this.fullHandleMessage.promise.resolve(); + this.fullHandleMessage = undefined; + } + } + + private async onKernelSocketMessage(data: WebSocketData): Promise { + // Hooks expect serialized data as this normally comes from a WebSocket + let message; + + if (!this.isUsingIPyWidgets) { + if (!message) { + message = this.deserialize(data as any) as any; + } + + // Check for hints that would indicate whether ipywidgest are used in outputs. + if ( + message.content && + message.content.data && + (message.content.data[WIDGET_MIMETYPE] || message.content.target_name === Identifiers.DefaultCommTarget) + ) { + this.isUsingIPyWidgets = true; + } + } + + const msgUuid = uuid(); + const promise = createDeferred(); + this.waitingMessageIds.set(msgUuid, { startTime: Date.now(), resultPromise: promise }); + + // Check if we need to fully handle this message on UI and Extension side before we move to the next + if (this.isUsingIPyWidgets) { + if (!message) { + message = this.deserialize(data as any) as any; + } + if (this.messageNeedsFullHandle(message)) { + this.fullHandleMessage = { id: message.header.msg_id, promise: createDeferred() }; + } + } + + if (typeof data === 'string') { + this.raisePostMessage(IPyWidgetMessages.IPyWidgets_msg, { id: msgUuid, data }); + } else { + this.raisePostMessage(IPyWidgetMessages.IPyWidgets_binary_msg, { + id: msgUuid, + data: serializeDataViews([data as any]) + }); + } + + // There are three handling states that we have for messages here + // 1. If we have not detected ipywidget usage at all, we just forward messages to the kernel + // 2. If we have detected ipywidget usage. We wait on our message to be received, but not + // possibly processed yet by the UI kernel. This make sure our ordering is in sync + // 3. For iopub comm messages we wait for them to be fully handled by the UI kernel + // and the Extension kernel as they may be required to do things like + // register message hooks on both sides before we process the nextExtension message + + // If there are no ipywidgets thusfar in the notebook, then no need to synchronize messages. + if (this.isUsingIPyWidgets) { + await promise.promise; + + // Comm specific iopub messages we need to wait until they are full handled + // by both the UI and extension side before we move forward + if (this.fullHandleMessage) { + await this.fullHandleMessage.promise.promise; + this.fullHandleMessage = undefined; + } + } + } + private onKernelSocketResponse(payload: { id: string }) { + const pending = this.waitingMessageIds.get(payload.id); + if (pending) { + this.waitingMessageIds.delete(payload.id); + this.totalWaitTime += Date.now() - pending.startTime; + this.totalWaitedMessages += 1; + pending.resultPromise.resolve(); + } + } + private sendPendingMessages() { + if (!this.notebook || !this.kernelSocketInfo) { + return; + } + while (this.pendingMessages.length) { + try { + this.kernelSocketInfo.socket?.sendToRealKernel(this.pendingMessages[0]); // NOSONAR + this.pendingMessages.shift(); + } catch (ex) { + traceError('Failed to send message to Kernel', ex); + return; + } + } + } + + private registerCommTargets(notebook: INotebook) { + while (this.pendingTargetNames.size > 0) { + const targetNames = Array.from([...this.pendingTargetNames.values()]); + const targetName = targetNames.shift(); + if (!targetName) { + continue; + } + if (this.commTargetsRegistered.has(targetName)) { + // Already registered. + return; + } + + traceInfo(`Registering commtarget ${targetName}`); + this.commTargetsRegistered.add(targetName); + this.pendingTargetNames.delete(targetName); + + // Skip the predefined target. It should have been registered + // inside the kernel on startup. However we + // still need to track it here. + if (targetName !== Identifiers.DefaultCommTarget) { + notebook.registerCommTarget(targetName, noop); + } + } + } + + private async getNotebook(): Promise { + if (this.notebookIdentity && !this.notebook) { + this.notebook = await this.notebookProvider.getOrCreateNotebook({ + identity: this.notebookIdentity, + getOnly: true + }); + } + if (this.notebook && !this.kernelRestartHandlerAttached) { + this.kernelRestartHandlerAttached = true; + this.disposables.push(this.notebook.onKernelRestarted(this.handleKernelRestarts, this)); + } + return this.notebook; + } + /** + * When a kernel restarts, we need to ensure the comm targets are re-registered. + * This must happen before anything else is processed. + */ + private async handleKernelRestarts() { + if (this.disposed || this.commTargetsRegistered.size === 0 || !this.notebook) { + return; + } + // Ensure we re-register the comm targets. + Array.from(this.commTargetsRegistered.keys()).forEach((targetName) => { + this.commTargetsRegistered.delete(targetName); + this.pendingTargetNames.add(targetName); + }); + + this.subscribeToKernelSocket(this.notebook); + this.registerCommTargets(this.notebook); + } + + private registerMessageHook(msgId: string) { + try { + if (this.notebook && !this.messageHooks.has(msgId)) { + this.hookCount += 1; + const callback = this.messageHookCallback.bind(this); + this.messageHooks.set(msgId, callback); + this.notebook.registerMessageHook(msgId, callback); + } + } finally { + // Regardless of if we registered successfully or not, send back a message to the UI + // that we are done with extension side handling of this message + this.raisePostMessage(IPyWidgetMessages.IPyWidgets_ExtensionOperationHandled, { + id: msgId, + type: IPyWidgetMessages.IPyWidgets_RegisterMessageHook + }); + } + } + + private possiblyRemoveMessageHook(args: { hookMsgId: string; lastHookedMsgId: string | undefined }) { + // Message hooks might need to be removed after a certain message is processed. + try { + if (args.lastHookedMsgId) { + this.pendingHookRemovals.set(args.lastHookedMsgId, args.hookMsgId); + } else { + this.removeMessageHook(args.hookMsgId); + } + } finally { + // Regardless of if we removed the hook, added to pending removals or just failed, send back a message to the UI + // that we are done with extension side handling of this message + this.raisePostMessage(IPyWidgetMessages.IPyWidgets_ExtensionOperationHandled, { + id: args.hookMsgId, + type: IPyWidgetMessages.IPyWidgets_RemoveMessageHook + }); + } + } + + private removeMessageHook(msgId: string) { + if (this.notebook && this.messageHooks.has(msgId)) { + const callback = this.messageHooks.get(msgId); + this.messageHooks.delete(msgId); + this.notebook.removeMessageHook(msgId, callback!); + } + } + + private async messageHookCallback(msg: KernelMessage.IIOPubMessage): Promise { + const promise = createDeferred(); + const requestId = uuid(); + // tslint:disable-next-line: no-any + const parentId = (msg.parent_header as any).msg_id; + if (this.messageHooks.has(parentId)) { + this.messageHookRequests.set(requestId, promise); + this.raisePostMessage(IPyWidgetMessages.IPyWidgets_MessageHookCall, { requestId, parentId, msg }); + } else { + promise.resolve(true); + } + + // Might have a pending removal. We may have delayed removing a message hook until a message was actually + // processed. + if (this.pendingHookRemovals.has(msg.header.msg_id)) { + const hookId = this.pendingHookRemovals.get(msg.header.msg_id); + this.pendingHookRemovals.delete(msg.header.msg_id); + this.removeMessageHook(hookId!); + } + + return promise.promise; + } + + private handleMessageHookResponse(args: { requestId: string; parentId: string; msgType: string; result: boolean }) { + const promise = this.messageHookRequests.get(args.requestId); + if (promise) { + this.messageHookRequests.delete(args.requestId); + + // During a comm message, make sure all messages come out. + promise.resolve(args.msgType.includes('comm') ? true : args.result); + } + } + + private sendOverheadTelemetry() { + sendTelemetryEvent(Telemetry.IPyWidgetOverhead, 0, { + totalOverheadInMs: this.totalWaitTime, + numberOfMessagesWaitedOn: this.totalWaitedMessages, + averageWaitTime: this.totalWaitTime / this.totalWaitedMessages, + numberOfRegisteredHooks: this.hookCount + }); + } +} diff --git a/src/client/datascience/ipywidgets/ipyWidgetMessageDispatcherFactory.ts b/src/client/datascience/ipywidgets/ipyWidgetMessageDispatcherFactory.ts new file mode 100644 index 000000000000..5519cfad3439 --- /dev/null +++ b/src/client/datascience/ipywidgets/ipyWidgetMessageDispatcherFactory.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { IDisposable, IDisposableRegistry } from '../../common/types'; +import { INotebook, INotebookProvider } from '../types'; +import { IPyWidgetMessageDispatcher } from './ipyWidgetMessageDispatcher'; +import { IIPyWidgetMessageDispatcher, IPyWidgetMessage } from './types'; + +/** + * This just wraps the iPyWidgetMessageDispatcher class. + * When raising events for arrived messages, this class will first raise events for + * all messages that arrived before this class was contructed. + */ +class IPyWidgetMessageDispatcherWithOldMessages implements IIPyWidgetMessageDispatcher { + public get postMessage(): Event { + return this._postMessageEmitter.event; + } + private _postMessageEmitter = new EventEmitter(); + private readonly disposables: IDisposable[] = []; + constructor( + private readonly baseMulticaster: IPyWidgetMessageDispatcher, + private oldMessages: ReadonlyArray + ) { + baseMulticaster.postMessage(this.raisePostMessage, this, this.disposables); + } + + public dispose() { + while (this.disposables.length) { + const disposable = this.disposables.shift(); + disposable?.dispose(); // NOSONAR + } + } + public async initialize() { + return this.baseMulticaster.initialize(); + } + + public receiveMessage(message: IPyWidgetMessage) { + this.baseMulticaster.receiveMessage(message); + } + private raisePostMessage(message: IPyWidgetMessage) { + // Send all of the old messages the notebook may not have received. + // Also send them in the same order. + this.oldMessages.forEach((oldMessage) => { + this._postMessageEmitter.fire(oldMessage); + }); + this.oldMessages = []; + this._postMessageEmitter.fire(message); + } +} + +/** + * Creates the dispatcher responsible for sending the ipywidget messages to notebooks. + * The way ipywidgets work are as follows: + * - IpyWidget framework registers with kernel (registerCommTarget). + * - IpyWidgets listen to messages from kernel (iopub). + * - IpyWidgets maintain their own state. + * - IpyWidgets build their state slowly based on messages arriving/being sent from iopub. + * - When kernel finally sends a message `display xyz`, ipywidgets looks for data related `xyz` and displays it. + * I.e. by now, ipywidgets has all of the data related to `xyz`. `xyz` is merely an id. + * I.e. kernel merely sends a message saying `ipywidgets please display the UI related to id xyz`. + * The terminoloy used by ipywidgest for the identifier is the `model id`. + * + * Now, if we have another UI opened for the same notebook, e.g. multiple notebooks, we need all of this informaiton. + * I.e. ipywidgets needs all of the information prior to the `display xyz command` form kernel. + * For this to happen, ipywidgets needs to be sent all of the messages from the time it reigstered for a comm target in the original notebook. + * + * Solution: + * - Save all of the messages sent to ipywidgets. + * - When we open a new notebook, then re-send all of these messages to this new ipywidgets manager in the second notebook. + * - Now, both ipywidget managers in both notebooks have the same data, hence are able to render the same controls. + */ +@injectable() +export class IPyWidgetMessageDispatcherFactory implements IDisposable { + private readonly messageDispatchers = new Map(); + private readonly messages: IPyWidgetMessage[] = []; + private disposed = false; + private disposables: IDisposable[] = []; + constructor( + @inject(INotebookProvider) private notebookProvider: INotebookProvider, + @inject(IDisposableRegistry) disposables: IDisposableRegistry + ) { + disposables.push(this); + notebookProvider.onNotebookCreated((e) => this.trackDisposingOfNotebook(e.notebook), this, this.disposables); + + notebookProvider.activeNotebooks.forEach((nbPromise) => + nbPromise.then((notebook) => this.trackDisposingOfNotebook(notebook)).ignoreErrors() + ); + } + + public dispose() { + this.disposed = true; + while (this.disposables.length) { + this.disposables.shift()?.dispose(); // NOSONAR + } + } + public create(identity: Uri): IIPyWidgetMessageDispatcher { + let baseDispatcher = this.messageDispatchers.get(identity.fsPath); + if (!baseDispatcher) { + baseDispatcher = new IPyWidgetMessageDispatcher(this.notebookProvider, identity); + this.messageDispatchers.set(identity.fsPath, baseDispatcher); + + // Capture all messages so we can re-play messages that others missed. + this.disposables.push(baseDispatcher.postMessage(this.onMessage, this)); + } + + // If we have messages upto this point, then capture those messages, + // & pass to the dispatcher so it can re-broadcast those old messages. + // If there are no old messages, even then return a new instance of the class. + // This way, the reference to that will be controlled by calling code. + const dispatcher = new IPyWidgetMessageDispatcherWithOldMessages( + baseDispatcher, + this.messages as ReadonlyArray + ); + this.disposables.push(dispatcher); + return dispatcher; + } + private trackDisposingOfNotebook(notebook: INotebook) { + if (this.disposed) { + return; + } + notebook.onDisposed( + () => { + const item = this.messageDispatchers.get(notebook.identity.fsPath); + this.messageDispatchers.delete(notebook.identity.fsPath); + item?.dispose(); // NOSONAR + }, + this, + this.disposables + ); + } + + private onMessage(_message: IPyWidgetMessage) { + // Disabled for now, as this has the potential to consume a lot of resources (memory). + // One solution - store n messages in array, then use file as storage. + // Next problem, data at rest is not encrypted, now we need to encrypt. + // Till we decide, lets disable this. + //this.messages.push(message); + } +} diff --git a/src/client/datascience/ipywidgets/ipyWidgetScriptSource.ts b/src/client/datascience/ipywidgets/ipyWidgetScriptSource.ts new file mode 100644 index 000000000000..e99730484a12 --- /dev/null +++ b/src/client/datascience/ipywidgets/ipyWidgetScriptSource.ts @@ -0,0 +1,310 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import type * as jupyterlabService from '@jupyterlab/services'; +import { sha256 } from 'hash.js'; +import { inject, injectable } from 'inversify'; +import { IDisposable } from 'monaco-editor'; +import * as path from 'path'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { traceError, traceInfo } from '../../common/logger'; + +import { + IConfigurationService, + IDisposableRegistry, + IExtensionContext, + IHttpClient, + IPersistentStateFactory +} from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { getOSType, OSType } from '../../common/utils/platform'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { + INotebookIdentity, + InteractiveWindowMessages, + IPyWidgetMessages +} from '../interactive-common/interactiveWindowTypes'; +import { + IDataScienceFileSystem, + IInteractiveWindowListener, + ILocalResourceUriConverter, + INotebook, + INotebookProvider +} from '../types'; +import { IPyWidgetScriptSourceProvider } from './ipyWidgetScriptSourceProvider'; +import { WidgetScriptSource } from './types'; +// tslint:disable: no-var-requires no-require-imports +const sanitize = require('sanitize-filename'); + +@injectable() +export class IPyWidgetScriptSource implements IInteractiveWindowListener, ILocalResourceUriConverter { + // tslint:disable-next-line: no-any + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + // tslint:disable-next-line: no-any + public get postInternalMessage(): Event<{ message: string; payload: any }> { + return this.postInternalMessageEmitter.event; + } + private readonly resourcesMappedToExtensionFolder = new Map>(); + private notebookIdentity?: Uri; + private postEmitter = new EventEmitter<{ + message: string; + // tslint:disable-next-line: no-any + payload: any; + }>(); + private postInternalMessageEmitter = new EventEmitter<{ + message: string; + // tslint:disable-next-line: no-any + payload: any; + }>(); + private notebook?: INotebook; + private jupyterLab?: typeof jupyterlabService; + private scriptProvider?: IPyWidgetScriptSourceProvider; + private disposables: IDisposable[] = []; + private interpreterForWhichWidgetSourcesWereFetched?: PythonEnvironment; + /** + * Key value pair of widget modules along with the version that needs to be loaded. + */ + private pendingModuleRequests = new Map(); + private readonly uriConversionPromises = new Map>(); + private readonly targetWidgetScriptsFolder: string; + private readonly _rootScriptFolder: string; + private readonly createTargetWidgetScriptsFolder: Promise; + constructor( + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(INotebookProvider) private readonly notebookProvider: INotebookProvider, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IConfigurationService) private readonly configurationSettings: IConfigurationService, + @inject(IHttpClient) private readonly httpClient: IHttpClient, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IPersistentStateFactory) private readonly stateFactory: IPersistentStateFactory, + @inject(IExtensionContext) extensionContext: IExtensionContext + ) { + this._rootScriptFolder = path.join(extensionContext.extensionPath, 'tmp', 'scripts'); + this.targetWidgetScriptsFolder = path.join(this._rootScriptFolder, 'nbextensions'); + this.createTargetWidgetScriptsFolder = this.fs + .localDirectoryExists(this.targetWidgetScriptsFolder) + .then(async (exists) => { + if (!exists) { + await this.fs.createLocalDirectory(this.targetWidgetScriptsFolder); + } + return this.targetWidgetScriptsFolder; + }); + disposables.push(this); + this.notebookProvider.onNotebookCreated( + (e) => { + if (e.identity.toString() === this.notebookIdentity?.toString()) { + this.initialize().catch(traceError.bind('Failed to initialize')); + } + }, + this, + this.disposables + ); + } + /** + * This method is called to convert a Uri to a format such that it can be used in a webview. + * WebViews only allow files that are part of extension and the same directory where notebook lives. + * To ensure widgets can find the js files, we copy the script file to a into the extensionr folder `tmp/nbextensions`. + * (storing files in `tmp/nbextensions` is relatively safe as this folder gets deleted when ever a user updates to a new version of VSC). + * Hence we need to copy for every version of the extension. + * Copying into global workspace folder would also work, but over time this folder size could grow (in an unmanaged way). + */ + public async asWebviewUri(localResource: Uri): Promise { + // Make a copy of the local file if not already in the correct location + if (!this.isInScriptPath(localResource.fsPath)) { + if (this.notebookIdentity && !this.resourcesMappedToExtensionFolder.has(localResource.fsPath)) { + const deferred = createDeferred(); + this.resourcesMappedToExtensionFolder.set(localResource.fsPath, deferred.promise); + try { + // Create a file name such that it will be unique and consistent across VSC reloads. + // Only if original file has been modified should we create a new copy of the sam file. + const fileHash: string = await this.fs.getFileHash(localResource.fsPath); + const uniqueFileName = sanitize( + sha256().update(`${localResource.fsPath}${fileHash}`).digest('hex') + ); + const targetFolder = await this.createTargetWidgetScriptsFolder; + const mappedResource = Uri.file( + path.join(targetFolder, `${uniqueFileName}${path.basename(localResource.fsPath)}`) + ); + if (!(await this.fs.localFileExists(mappedResource.fsPath))) { + await this.fs.copyLocal(localResource.fsPath, mappedResource.fsPath); + } + traceInfo(`Widget Script file ${localResource.fsPath} mapped to ${mappedResource.fsPath}`); + deferred.resolve(mappedResource); + } catch (ex) { + traceError(`Failed to map widget Script file ${localResource.fsPath}`); + deferred.reject(ex); + } + } + localResource = await this.resourcesMappedToExtensionFolder.get(localResource.fsPath)!; + } + const key = localResource.toString(); + if (!this.uriConversionPromises.has(key)) { + this.uriConversionPromises.set(key, createDeferred()); + // Send a request for the translation. + this.postInternalMessageEmitter.fire({ + message: InteractiveWindowMessages.ConvertUriForUseInWebViewRequest, + payload: localResource + }); + } + return this.uriConversionPromises.get(key)!.promise; + } + + public get rootScriptFolder(): Uri { + return Uri.file(this._rootScriptFolder); + } + + public dispose() { + while (this.disposables.length) { + this.disposables.shift()?.dispose(); // NOSONAR + } + } + + // tslint:disable-next-line: no-any + public onMessage(message: string, payload?: any): void { + if (message === InteractiveWindowMessages.NotebookIdentity) { + this.saveIdentity(payload).catch((ex) => + traceError(`Failed to initialize ${(this as Object).constructor.name}`, ex) + ); + } else if (message === InteractiveWindowMessages.NotebookClose) { + this.dispose(); + } else if (message === InteractiveWindowMessages.ConvertUriForUseInWebViewResponse) { + const response: undefined | { request: Uri; response: Uri } = payload; + if (response && this.uriConversionPromises.get(response.request.toString())) { + this.uriConversionPromises.get(response.request.toString())!.resolve(response.response); + } + } else if (message === IPyWidgetMessages.IPyWidgets_WidgetScriptSourceRequest) { + if (payload) { + const { moduleName, moduleVersion } = payload as { moduleName: string; moduleVersion: string }; + this.sendWidgetSource(moduleName, moduleVersion).catch( + traceError.bind('Failed to send widget sources upon ready') + ); + } + } + } + + /** + * Send the widget script source for a specific widget module & version. + * This is a request made when a widget is certainly used in a notebook. + */ + private async sendWidgetSource(moduleName?: string, moduleVersion: string = '*') { + // Standard widgets area already available, hence no need to look for them. + if (!moduleName || moduleName.startsWith('@jupyter')) { + return; + } + if (!this.notebook || !this.scriptProvider) { + this.pendingModuleRequests.set(moduleName, moduleVersion); + return; + } + + let widgetSource: WidgetScriptSource = { moduleName }; + try { + widgetSource = await this.scriptProvider.getWidgetScriptSource(moduleName, moduleVersion); + } catch (ex) { + traceError('Failed to get widget source due to an error', ex); + sendTelemetryEvent(Telemetry.HashedIPyWidgetScriptDiscoveryError); + } finally { + // Send to UI (even if there's an error) continues instead of hanging while waiting for a response. + this.postEmitter.fire({ + message: IPyWidgetMessages.IPyWidgets_WidgetScriptSourceResponse, + payload: widgetSource + }); + } + } + private async saveIdentity(args: INotebookIdentity) { + this.notebookIdentity = args.resource; + await this.initialize(); + } + + private async initialize() { + if (!this.jupyterLab) { + // Lazy load jupyter lab for faster extension loading. + // tslint:disable-next-line:no-require-imports + this.jupyterLab = require('@jupyterlab/services') as typeof jupyterlabService; // NOSONAR + } + + if (!this.notebookIdentity) { + return; + } + if (!this.notebook) { + this.notebook = await this.notebookProvider.getOrCreateNotebook({ + identity: this.notebookIdentity, + disableUI: true, + getOnly: true + }); + } + if (!this.notebook) { + return; + } + if (this.scriptProvider) { + return; + } + this.scriptProvider = new IPyWidgetScriptSourceProvider( + this.notebook, + this, + this.fs, + this.interpreterService, + this.appShell, + this.configurationSettings, + this.workspaceService, + this.stateFactory, + this.httpClient + ); + await this.initializeNotebook(); + } + private async initializeNotebook() { + if (!this.notebook) { + return; + } + this.notebook.onDisposed(() => this.dispose()); + // When changing a kernel, we might have a new interpreter. + this.notebook.onKernelChanged( + () => { + // If underlying interpreter has changed, then refresh list of widget sources. + // After all, different kernels have different widgets. + if ( + this.notebook?.getMatchingInterpreter() && + this.notebook?.getMatchingInterpreter() === this.interpreterForWhichWidgetSourcesWereFetched + ) { + return; + } + // Let UI know that kernel has changed. + this.postEmitter.fire({ message: IPyWidgetMessages.IPyWidgets_onKernelChanged, payload: undefined }); + }, + this, + this.disposables + ); + this.handlePendingRequests(); + } + private handlePendingRequests() { + const pendingModuleNames = Array.from(this.pendingModuleRequests.keys()); + while (pendingModuleNames.length) { + const moduleName = pendingModuleNames.shift(); + if (moduleName) { + const moduleVersion = this.pendingModuleRequests.get(moduleName)!; + this.pendingModuleRequests.delete(moduleName); + this.sendWidgetSource(moduleName, moduleVersion).catch( + traceError.bind(`Failed to send WidgetScript for ${moduleName}`) + ); + } + } + } + + private isInScriptPath(filePath: string) { + const scriptPath = path.normalize(this._rootScriptFolder); + filePath = path.normalize(filePath); + if (getOSType() === OSType.Windows) { + return filePath.toUpperCase().startsWith(scriptPath.toUpperCase()); + } else { + return filePath.startsWith(scriptPath); + } + } +} diff --git a/src/client/datascience/ipywidgets/ipyWidgetScriptSourceProvider.ts b/src/client/datascience/ipywidgets/ipyWidgetScriptSourceProvider.ts new file mode 100644 index 000000000000..13b6c3fb8871 --- /dev/null +++ b/src/client/datascience/ipywidgets/ipyWidgetScriptSourceProvider.ts @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { sha256 } from 'hash.js'; +import { ConfigurationChangeEvent, ConfigurationTarget } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import '../../common/extensions'; +import { traceError } from '../../common/logger'; + +import { + IConfigurationService, + IHttpClient, + IPersistentState, + IPersistentStateFactory, + WidgetCDNs +} from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { Common, DataScience } from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { IDataScienceFileSystem, ILocalResourceUriConverter, INotebook } from '../types'; +import { CDNWidgetScriptSourceProvider } from './cdnWidgetScriptSourceProvider'; +import { LocalWidgetScriptSourceProvider } from './localWidgetScriptSourceProvider'; +import { RemoteWidgetScriptSourceProvider } from './remoteWidgetScriptSourceProvider'; +import { IWidgetScriptSourceProvider, WidgetScriptSource } from './types'; + +const GlobalStateKeyToTrackIfUserConfiguredCDNAtLeastOnce = 'IPYWidgetCDNConfigured'; +const GlobalStateKeyToNeverWarnAboutScriptsNotFoundOnCDN = 'IPYWidgetNotFoundOnCDN'; + +/** + * This class decides where to get widget scripts from. + * Whether its cdn or local or other, and also controls the order/priority. + * If user changes the order, this will react to those configuration setting changes. + * If user has not configured antying, user will be presented with a prompt. + */ +export class IPyWidgetScriptSourceProvider implements IWidgetScriptSourceProvider { + private readonly notifiedUserAboutWidgetScriptNotFound = new Set(); + private scriptProviders?: IWidgetScriptSourceProvider[]; + private configurationPromise?: Deferred; + private get configuredScriptSources(): readonly WidgetCDNs[] { + const settings = this.configurationSettings.getSettings(undefined); + return settings.datascience.widgetScriptSources; + } + private readonly userConfiguredCDNAtLeastOnce: IPersistentState; + private readonly neverWarnAboutScriptsNotFoundOnCDN: IPersistentState; + constructor( + private readonly notebook: INotebook, + private readonly localResourceUriConverter: ILocalResourceUriConverter, + private readonly fs: IDataScienceFileSystem, + private readonly interpreterService: IInterpreterService, + private readonly appShell: IApplicationShell, + private readonly configurationSettings: IConfigurationService, + private readonly workspaceService: IWorkspaceService, + private readonly stateFactory: IPersistentStateFactory, + private readonly httpClient: IHttpClient + ) { + this.userConfiguredCDNAtLeastOnce = this.stateFactory.createGlobalPersistentState( + GlobalStateKeyToTrackIfUserConfiguredCDNAtLeastOnce, + false + ); + this.neverWarnAboutScriptsNotFoundOnCDN = this.stateFactory.createGlobalPersistentState( + GlobalStateKeyToNeverWarnAboutScriptsNotFoundOnCDN, + false + ); + } + public initialize() { + this.workspaceService.onDidChangeConfiguration(this.onSettingsChagned.bind(this)); + } + public dispose() { + this.disposeScriptProviders(); + } + /** + * We know widgets are being used, at this point prompt user if required. + */ + public async getWidgetScriptSource( + moduleName: string, + moduleVersion: string + ): Promise> { + await this.configureWidgets(); + if (!this.scriptProviders) { + this.rebuildProviders(); + } + + // Get script sources in order, if one works, then get out. + const scriptSourceProviders = (this.scriptProviders || []).slice(); + let found: WidgetScriptSource = { moduleName }; + while (scriptSourceProviders.length) { + const scriptProvider = scriptSourceProviders.shift(); + if (!scriptProvider) { + continue; + } + const source = await scriptProvider.getWidgetScriptSource(moduleName, moduleVersion); + // If we found the script source, then use that. + if (source.scriptUri) { + found = source; + break; + } + } + + sendTelemetryEvent(Telemetry.HashedIPyWidgetNameUsed, undefined, { + hashedName: sha256().update(found.moduleName).digest('hex'), + source: found.source, + cdnSearched: this.configuredScriptSources.length > 0 + }); + + if (!found.scriptUri) { + traceError(`Script source for Widget ${moduleName}@${moduleVersion} not found`); + } + this.handleWidgetSourceNotFoundOnCDN(found).ignoreErrors(); + return found; + } + private async handleWidgetSourceNotFoundOnCDN(widgetSource: WidgetScriptSource) { + // if widget exists nothing to do. + if (widgetSource.source === 'cdn' || this.neverWarnAboutScriptsNotFoundOnCDN.value === true) { + return; + } + if ( + this.notifiedUserAboutWidgetScriptNotFound.has(widgetSource.moduleName) || + this.configuredScriptSources.length === 0 + ) { + return; + } + this.notifiedUserAboutWidgetScriptNotFound.add(widgetSource.moduleName); + const selection = await this.appShell.showWarningMessage( + DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork().format(widgetSource.moduleName), + Common.ok(), + Common.doNotShowAgain(), + Common.reportThisIssue() + ); + switch (selection) { + case Common.doNotShowAgain(): + return this.neverWarnAboutScriptsNotFoundOnCDN.updateValue(true); + case Common.reportThisIssue(): + return this.appShell.openUrl('https://aka.ms/CreatePVSCDataScienceIssue'); + default: + noop(); + } + } + + private onSettingsChagned(e: ConfigurationChangeEvent) { + if (e.affectsConfiguration('python.dataScience.widgetScriptSources')) { + this.rebuildProviders(); + } + } + private disposeScriptProviders() { + while (this.scriptProviders && this.scriptProviders.length) { + const item = this.scriptProviders.shift(); + if (item) { + item.dispose(); + } + } + } + private rebuildProviders() { + this.disposeScriptProviders(); + + const scriptProviders: IWidgetScriptSourceProvider[] = []; + + // If we're allowed to use CDN providers, then use them, and use in order of preference. + if (this.configuredScriptSources.length > 0) { + scriptProviders.push( + new CDNWidgetScriptSourceProvider( + this.configurationSettings, + this.httpClient, + this.localResourceUriConverter, + this.fs + ) + ); + } + if (this.notebook.connection && this.notebook.connection.localLaunch) { + scriptProviders.push( + new LocalWidgetScriptSourceProvider( + this.notebook, + this.localResourceUriConverter, + this.fs, + this.interpreterService + ) + ); + } else { + if (this.notebook.connection) { + scriptProviders.push(new RemoteWidgetScriptSourceProvider(this.notebook.connection, this.httpClient)); + } + } + + this.scriptProviders = scriptProviders; + } + + private async configureWidgets(): Promise { + if (this.configuredScriptSources.length !== 0) { + return; + } + + if (this.userConfiguredCDNAtLeastOnce.value) { + return; + } + + if (this.configurationPromise) { + return this.configurationPromise.promise; + } + this.configurationPromise = createDeferred(); + sendTelemetryEvent(Telemetry.IPyWidgetPromptToUseCDN); + const selection = await this.appShell.showInformationMessage( + DataScience.useCDNForWidgets(), + Common.ok(), + Common.cancel(), + Common.doNotShowAgain() + ); + + let selectionForTelemetry: 'ok' | 'cancel' | 'dismissed' | 'doNotShowAgain' = 'dismissed'; + switch (selection) { + case Common.ok(): { + selectionForTelemetry = 'ok'; + // always search local interpreter or attempt to fetch scripts from remote jupyter server as backups. + await Promise.all([ + this.updateScriptSources(['jsdelivr.com', 'unpkg.com']), + this.userConfiguredCDNAtLeastOnce.updateValue(true) + ]); + break; + } + case Common.doNotShowAgain(): { + selectionForTelemetry = 'doNotShowAgain'; + // At a minimum search local interpreter or attempt to fetch scripts from remote jupyter server. + await Promise.all([this.updateScriptSources([]), this.userConfiguredCDNAtLeastOnce.updateValue(true)]); + break; + } + default: + selectionForTelemetry = selection === Common.cancel() ? 'cancel' : 'dismissed'; + break; + } + + sendTelemetryEvent(Telemetry.IPyWidgetPromptToUseCDNSelection, undefined, { selection: selectionForTelemetry }); + this.configurationPromise.resolve(); + } + private async updateScriptSources(scriptSources: WidgetCDNs[]) { + const targetSetting = 'dataScience.widgetScriptSources'; + await this.configurationSettings.updateSetting( + targetSetting, + scriptSources, + undefined, + ConfigurationTarget.Global + ); + } +} diff --git a/src/client/datascience/ipywidgets/ipywidgetHandler.ts b/src/client/datascience/ipywidgets/ipywidgetHandler.ts new file mode 100644 index 000000000000..c2f569070d80 --- /dev/null +++ b/src/client/datascience/ipywidgets/ipywidgetHandler.ts @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import type { KernelMessage } from '@jupyterlab/services'; +import { inject, injectable, named } from 'inversify'; +import stripAnsi from 'strip-ansi'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { + ILoadIPyWidgetClassFailureAction, + LoadIPyWidgetClassLoadAction, + NotifyIPyWidgeWidgetVersionNotSupportedAction +} from '../../../datascience-ui/interactive-common/redux/reducers/types'; +import { EnableIPyWidgets } from '../../common/experiments/groups'; +import { traceError, traceInfo } from '../../common/logger'; +import { IDisposableRegistry, IExperimentsManager, IOutputChannel } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { sendTelemetryEvent } from '../../telemetry'; +import { JUPYTER_OUTPUT_CHANNEL, Telemetry } from '../constants'; +import { INotebookIdentity, InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes'; +import { IInteractiveWindowListener, INotebookProvider } from '../types'; +import { IPyWidgetMessageDispatcherFactory } from './ipyWidgetMessageDispatcherFactory'; +import { IIPyWidgetMessageDispatcher } from './types'; + +/** + * This class handles all of the ipywidgets communication with the notebook + */ +@injectable() +// +export class IPyWidgetHandler implements IInteractiveWindowListener { + // tslint:disable-next-line: no-any + public get postMessage(): Event<{ message: string; payload: any }> { + return this.postEmitter.event; + } + private ipyWidgetMessageDispatcher?: IIPyWidgetMessageDispatcher; + private notebookIdentity: Uri | undefined; + // tslint:disable-next-line: no-any + private postEmitter: EventEmitter<{ message: string; payload: any }> = new EventEmitter<{ + message: string; + // tslint:disable-next-line: no-any + payload: any; + }>(); + // tslint:disable-next-line: no-require-imports + private hashFn = require('hash.js').sha256; + private enabled = false; + + constructor( + @inject(INotebookProvider) notebookProvider: INotebookProvider, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IPyWidgetMessageDispatcherFactory) + private readonly widgetMessageDispatcherFactory: IPyWidgetMessageDispatcherFactory, + @inject(IExperimentsManager) readonly experimentsManager: IExperimentsManager, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private jupyterOutput: IOutputChannel + ) { + disposables.push( + notebookProvider.onNotebookCreated(async (e) => { + if (e.identity.toString() === this.notebookIdentity?.toString()) { + await this.initialize(); + } + }) + ); + + this.enabled = experimentsManager.inExperiment(EnableIPyWidgets.experiment); + } + + public dispose() { + this.ipyWidgetMessageDispatcher?.dispose(); // NOSONAR + } + + // tslint:disable-next-line: no-any + public onMessage(message: string, payload?: any): void { + if (message === InteractiveWindowMessages.NotebookIdentity) { + this.saveIdentity(payload).catch((ex) => traceError('Failed to initialize ipywidgetHandler', ex)); + } else if (message === InteractiveWindowMessages.IPyWidgetLoadSuccess) { + this.sendLoadSucceededTelemetry(payload); + } else if (message === InteractiveWindowMessages.IPyWidgetLoadFailure) { + this.sendLoadFailureTelemetry(payload); + } else if (message === InteractiveWindowMessages.IPyWidgetWidgetVersionNotSupported) { + this.sendUnsupportedWidgetVersionFailureTelemetry(payload); + } else if (message === InteractiveWindowMessages.IPyWidgetRenderFailure) { + this.sendRenderFailureTelemetry(payload); + } else if (message === InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage) { + this.handleUnhandledMessage(payload); + } + // tslint:disable-next-line: no-any + this.getIPyWidgetMessageDispatcher()?.receiveMessage({ message: message as any, payload }); // NOSONAR + } + + private hash(s: string): string { + return this.hashFn().update(s).digest('hex'); + } + + private sendLoadSucceededTelemetry(payload: LoadIPyWidgetClassLoadAction) { + try { + sendTelemetryEvent(Telemetry.IPyWidgetLoadSuccess, 0, { + moduleHash: this.hash(payload.moduleName), + moduleVersion: payload.moduleVersion + }); + } catch { + // do nothing on failure + } + } + + private sendLoadFailureTelemetry(payload: ILoadIPyWidgetClassFailureAction) { + try { + sendTelemetryEvent(Telemetry.IPyWidgetLoadFailure, 0, { + isOnline: payload.isOnline, + moduleHash: this.hash(payload.moduleName), + moduleVersion: payload.moduleVersion, + timedout: payload.timedout + }); + } catch { + // do nothing on failure + } + } + private sendUnsupportedWidgetVersionFailureTelemetry(payload: NotifyIPyWidgeWidgetVersionNotSupportedAction) { + try { + sendTelemetryEvent(Telemetry.IPyWidgetWidgetVersionNotSupportedLoadFailure, 0, { + moduleHash: this.hash(payload.moduleName), + moduleVersion: payload.moduleVersion + }); + } catch { + // do nothing on failure + } + } + private sendRenderFailureTelemetry(payload: Error) { + try { + traceError('Error rendering a widget: ', payload); + sendTelemetryEvent(Telemetry.IPyWidgetRenderFailure); + } catch { + // Do nothing on a failure + } + } + + private handleUnhandledMessage(msg: KernelMessage.IMessage) { + // Skip status messages + if (msg.header.msg_type !== 'status') { + try { + // Special case errors, strip ansi codes from tracebacks so they print better. + if (msg.header.msg_type === 'error') { + const errorMsg = msg as KernelMessage.IErrorMsg; + errorMsg.content.traceback = errorMsg.content.traceback.map(stripAnsi); + } + traceInfo(`Unhandled widget kernel message: ${msg.header.msg_type} ${msg.content}`); + this.jupyterOutput.appendLine( + localize.DataScience.unhandledMessage().format(msg.header.msg_type, JSON.stringify(msg.content)) + ); + sendTelemetryEvent(Telemetry.IPyWidgetUnhandledMessage, undefined, { msg_type: msg.header.msg_type }); + } catch { + // Don't care if this doesn't get logged + } + } + } + private getIPyWidgetMessageDispatcher() { + if (!this.notebookIdentity || !this.enabled) { + return; + } + if (!this.ipyWidgetMessageDispatcher) { + this.ipyWidgetMessageDispatcher = this.widgetMessageDispatcherFactory.create(this.notebookIdentity); + } + return this.ipyWidgetMessageDispatcher; + } + + private async saveIdentity(args: INotebookIdentity) { + this.notebookIdentity = args.resource; + + const dispatcher = this.getIPyWidgetMessageDispatcher(); + if (dispatcher) { + this.disposables.push(dispatcher.postMessage((msg) => this.postEmitter.fire(msg))); + } + + await this.initialize(); + } + + private async initialize() { + if (!this.notebookIdentity) { + return; + } + const dispatcher = this.getIPyWidgetMessageDispatcher(); + if (dispatcher) { + await dispatcher.initialize(); + } + } +} diff --git a/src/client/datascience/ipywidgets/localWidgetScriptSourceProvider.ts b/src/client/datascience/ipywidgets/localWidgetScriptSourceProvider.ts new file mode 100644 index 000000000000..82dd1714835e --- /dev/null +++ b/src/client/datascience/ipywidgets/localWidgetScriptSourceProvider.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { Uri } from 'vscode'; +import { traceError } from '../../common/logger'; + +import { IInterpreterService } from '../../interpreter/contracts'; +import { captureTelemetry } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { + getInterpreterFromKernelConnectionMetadata, + getKernelPathFromKernelConnection, + isPythonKernelConnection +} from '../jupyter/kernels/helpers'; +import { IDataScienceFileSystem, ILocalResourceUriConverter, INotebook } from '../types'; +import { IWidgetScriptSourceProvider, WidgetScriptSource } from './types'; + +/** + * Widget scripts are found in /share/jupyter/nbextensions. + * Here's an example: + * /share/jupyter/nbextensions/k3d/index.js + * /share/jupyter/nbextensions/nglview/index.js + * /share/jupyter/nbextensions/bqplot/index.js + */ +export class LocalWidgetScriptSourceProvider implements IWidgetScriptSourceProvider { + private cachedWidgetScripts?: Promise; + constructor( + private readonly notebook: INotebook, + private readonly localResourceUriConverter: ILocalResourceUriConverter, + private readonly fs: IDataScienceFileSystem, + private readonly interpreterService: IInterpreterService + ) {} + public async getWidgetScriptSource(moduleName: string): Promise> { + const sources = await this.getWidgetScriptSources(); + const found = sources.find((item) => item.moduleName.toLowerCase() === moduleName.toLowerCase()); + return found || { moduleName }; + } + public dispose() { + // Noop. + } + public async getWidgetScriptSources(ignoreCache?: boolean): Promise> { + if (!ignoreCache && this.cachedWidgetScripts) { + return this.cachedWidgetScripts; + } + return (this.cachedWidgetScripts = this.getWidgetScriptSourcesWithoutCache()); + } + @captureTelemetry(Telemetry.DiscoverIPyWidgetNamesLocalPerf) + private async getWidgetScriptSourcesWithoutCache(): Promise { + const sysPrefix = await this.getSysPrefixOfKernel(); + if (!sysPrefix) { + return []; + } + + const nbextensionsPath = path.join(sysPrefix, 'share', 'jupyter', 'nbextensions'); + // Search only one level deep, hence `*/index.js`. + const files = await this.fs.searchLocal(`*${path.sep}index.js`, nbextensionsPath); + + const validFiles = files.filter((file) => { + // Should be of the form `/index.js` + const parts = file.split('/'); // On windows this uses the unix separator too. + if (parts.length !== 2) { + traceError('Incorrect file found when searching for nnbextension entrypoints'); + return false; + } + return true; + }); + + const mappedFiles = validFiles.map(async (file) => { + // Should be of the form `/index.js` + const parts = file.split('/'); + const moduleName = parts[0]; + + const fileUri = Uri.file(path.join(nbextensionsPath, file)); + const scriptUri = (await this.localResourceUriConverter.asWebviewUri(fileUri)).toString(); + // tslint:disable-next-line: no-unnecessary-local-variable + const widgetScriptSource: WidgetScriptSource = { moduleName, scriptUri, source: 'local' }; + return widgetScriptSource; + }); + // tslint:disable-next-line: no-any + return Promise.all(mappedFiles as any); + } + private async getSysPrefixOfKernel() { + const kernelConnectionMetadata = this.notebook.getKernelConnection(); + if (!kernelConnectionMetadata) { + return; + } + const interpreter = getInterpreterFromKernelConnectionMetadata(kernelConnectionMetadata); + if (interpreter?.sysPrefix) { + return interpreter?.sysPrefix; + } + if (!isPythonKernelConnection(kernelConnectionMetadata)) { + return; + } + const interpreterOrKernelPath = + interpreter?.path || getKernelPathFromKernelConnection(kernelConnectionMetadata); + if (!interpreterOrKernelPath) { + return; + } + const interpreterInfo = await this.interpreterService + .getInterpreterDetails(interpreterOrKernelPath) + .catch( + traceError.bind(`Failed to get interpreter details for Kernel/Interpreter ${interpreterOrKernelPath}`) + ); + + if (interpreterInfo) { + return interpreterInfo?.sysPrefix; + } + } +} diff --git a/src/client/datascience/ipywidgets/remoteWidgetScriptSourceProvider.ts b/src/client/datascience/ipywidgets/remoteWidgetScriptSourceProvider.ts new file mode 100644 index 000000000000..194c9ee00b67 --- /dev/null +++ b/src/client/datascience/ipywidgets/remoteWidgetScriptSourceProvider.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { traceWarning } from '../../common/logger'; +import { IHttpClient } from '../../common/types'; +import { IJupyterConnection } from '../types'; +import { IWidgetScriptSourceProvider, WidgetScriptSource } from './types'; + +/** + * When using a remote jupyter connection the widget scripts are accessible over + * `/nbextensions/moduleName/index` + */ +export class RemoteWidgetScriptSourceProvider implements IWidgetScriptSourceProvider { + public static validUrls = new Map(); + constructor(private readonly connection: IJupyterConnection, private readonly httpClient: IHttpClient) {} + public dispose() { + // Noop. + } + public async getWidgetScriptSource(moduleName: string, moduleVersion: string): Promise { + const scriptUri = `${this.connection.baseUrl}nbextensions/${moduleName}/index.js`; + const exists = await this.getUrlForWidget(scriptUri); + if (exists) { + return { moduleName, scriptUri, source: 'cdn' }; + } + traceWarning(`Widget Script not found for ${moduleName}@${moduleVersion}`); + return { moduleName }; + } + private async getUrlForWidget(url: string): Promise { + if (RemoteWidgetScriptSourceProvider.validUrls.has(url)) { + return RemoteWidgetScriptSourceProvider.validUrls.get(url)!; + } + + const exists = await this.httpClient.exists(url); + RemoteWidgetScriptSourceProvider.validUrls.set(url, exists); + return exists; + } +} diff --git a/src/client/datascience/ipywidgets/types.ts b/src/client/datascience/ipywidgets/types.ts new file mode 100644 index 000000000000..e09243ffa1fa --- /dev/null +++ b/src/client/datascience/ipywidgets/types.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Event } from 'vscode'; +import { IDisposable } from '../../common/types'; +import { IPyWidgetMessages } from '../interactive-common/interactiveWindowTypes'; + +export interface IPyWidgetMessage { + message: IPyWidgetMessages; + // tslint:disable-next-line: no-any + payload: any; +} + +/** + * Used to send/receive messages related to IPyWidgets + */ +export interface IIPyWidgetMessageDispatcher extends IDisposable { + // tslint:disable-next-line: no-any + postMessage: Event; + // tslint:disable-next-line: no-any + receiveMessage(message: IPyWidgetMessage): void; + initialize(): Promise; +} + +/** + * Name value pair of widget name/module along with the Uri to the script. + */ +export type WidgetScriptSource = { + moduleName: string; + /** + * Where is the script being source from. + */ + source?: 'cdn' | 'local' | 'remote'; + /** + * Resource Uri (not using Uri type as this needs to be sent from extension to UI). + */ + scriptUri?: string; +}; + +/** + * Used to get an entry for widget (or all of them). + */ +export interface IWidgetScriptSourceProvider extends IDisposable { + /** + * Return the script path for the requested module. + * This is called when ipywidgets needs a source for a particular widget. + */ + getWidgetScriptSource(moduleName: string, moduleVersion: string): Promise>; +} diff --git a/src/client/datascience/jupyter/commandLineSelector.ts b/src/client/datascience/jupyter/commandLineSelector.ts new file mode 100644 index 000000000000..9b8c8dfc1b8b --- /dev/null +++ b/src/client/datascience/jupyter/commandLineSelector.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +// tslint:disable-next-line: import-name +import parseArgsStringToArgv from 'string-argv'; +import { ConfigurationChangeEvent, ConfigurationTarget, QuickPickItem, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types'; +import { IConfigurationService } from '../../common/types'; +import { DataScience } from '../../common/utils/localize'; +import { + IMultiStepInput, + IMultiStepInputFactory, + InputStep, + IQuickPickParameters +} from '../../common/utils/multiStepInput'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; + +@injectable() +export class JupyterCommandLineSelector { + private readonly defaultLabel = `$(zap) ${DataScience.jupyterCommandLineDefaultLabel()}`; + private readonly customLabel = `$(gear) ${DataScience.jupyterCommandLineCustomLabel()}`; + constructor( + @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, + @inject(IConfigurationService) private configuration: IConfigurationService, + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(ICommandManager) private commandManager: ICommandManager + ) { + workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this)); + } + + @captureTelemetry(Telemetry.SelectJupyterURI) + public selectJupyterCommandLine(file: Uri): Promise { + const multiStep = this.multiStepFactory.create<{}>(); + return multiStep.run(this.startSelectingCommandLine.bind(this, file), {}); + } + + private async onDidChangeConfiguration(e: ConfigurationChangeEvent) { + if (e.affectsConfiguration('python.dataScience.jupyterCommandLineArguments')) { + const reload = DataScience.jupyterCommandLineReloadAnswer(); + const item = await this.appShell.showInformationMessage( + DataScience.jupyterCommandLineReloadQuestion(), + reload + ); + if (item === reload) { + this.commandManager.executeCommand('workbench.action.reloadWindow'); + } + } + } + + private async startSelectingCommandLine( + file: Uri, + input: IMultiStepInput<{}>, + _state: {} + ): Promise | void> { + // First step, show a quick pick to choose either the custom or the default. + // newChoice element will be set if the user picked 'enter a new server' + const item = await input.showQuickPick>({ + placeholder: DataScience.jupyterCommandLineQuickPickPlaceholder(), + items: this.getPickList(), + title: DataScience.jupyterCommandLineQuickPickTitle() + }); + if (item.label === this.defaultLabel) { + await this.setJupyterCommandLine(''); + } else { + return this.selectCustomCommandLine.bind(this, file); + } + } + private async selectCustomCommandLine( + file: Uri, + input: IMultiStepInput<{}>, + _state: {} + ): Promise | void> { + // Ask the user to enter a command line + const result = await input.showInputBox({ + title: DataScience.jupyterCommandLinePrompt(), + value: this.configuration.getSettings(file).datascience.jupyterCommandLineArguments.join(' '), + validate: this.validate, + prompt: '' + }); + + if (result) { + await this.setJupyterCommandLine(result); + } + } + + private async setJupyterCommandLine(val: string): Promise { + if (val) { + sendTelemetryEvent(Telemetry.JupyterCommandLineNonDefault); + } + const split = parseArgsStringToArgv(val); + await this.configuration.updateSetting( + 'dataScience.jupyterCommandLineArguments', + split, + undefined, + ConfigurationTarget.Workspace + ); + } + + private validate = async (_inputText: string): Promise => { + return undefined; + }; + + private getPickList(): QuickPickItem[] { + // Always have 'local' and 'custom' + const items: QuickPickItem[] = []; + items.push({ label: this.defaultLabel, detail: DataScience.jupyterCommandLineDefaultDetail() }); + items.push({ label: this.customLabel, detail: DataScience.jupyterCommandLineCustomDetail() }); + + return items; + } +} diff --git a/src/client/datascience/jupyter/debuggerVariableRegistration.ts b/src/client/datascience/jupyter/debuggerVariableRegistration.ts new file mode 100644 index 000000000000..176b1a622936 --- /dev/null +++ b/src/client/datascience/jupyter/debuggerVariableRegistration.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable, named } from 'inversify'; +import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, ProviderResult } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IDebugService } from '../../common/application/types'; +import { PYTHON_LANGUAGE } from '../../common/constants'; +import { IDisposableRegistry } from '../../common/types'; +import { Identifiers } from '../constants'; +import { IJupyterDebugService, IJupyterVariables } from '../types'; + +@injectable() +export class DebuggerVariableRegistration implements IExtensionSingleActivationService, DebugAdapterTrackerFactory { + constructor( + @inject(IJupyterDebugService) @named(Identifiers.MULTIPLEXING_DEBUGSERVICE) private debugService: IDebugService, + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, + @inject(IJupyterVariables) @named(Identifiers.DEBUGGER_VARIABLES) private debugVariables: DebugAdapterTracker + ) {} + public activate(): Promise { + this.disposables.push(this.debugService.registerDebugAdapterTrackerFactory(PYTHON_LANGUAGE, this)); + return Promise.resolve(); + } + + public createDebugAdapterTracker(_session: DebugSession): ProviderResult { + return this.debugVariables; + } +} diff --git a/src/client/datascience/jupyter/debuggerVariables.ts b/src/client/datascience/jupyter/debuggerVariables.ts new file mode 100644 index 000000000000..6b4093774ad1 --- /dev/null +++ b/src/client/datascience/jupyter/debuggerVariables.ts @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable, named } from 'inversify'; + +import { DebugAdapterTracker, Disposable, Event, EventEmitter } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { IDebugService } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import { IConfigurationService, Resource } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { DataFrameLoading, Identifiers, Telemetry } from '../constants'; +import { + IConditionalJupyterVariables, + IJupyterDebugService, + IJupyterVariable, + IJupyterVariablesRequest, + IJupyterVariablesResponse, + INotebook +} from '../types'; + +const DataViewableTypes: Set = new Set(['DataFrame', 'list', 'dict', 'ndarray', 'Series']); +const KnownExcludedVariables = new Set(['In', 'Out', 'exit', 'quit']); + +@injectable() +export class DebuggerVariables implements IConditionalJupyterVariables, DebugAdapterTracker { + private refreshEventEmitter = new EventEmitter(); + private lastKnownVariables: IJupyterVariable[] = []; + private topMostFrameId = 0; + private importedIntoKernel = new Set(); + private watchedNotebooks = new Map(); + private debuggingStarted = false; + constructor( + @inject(IJupyterDebugService) @named(Identifiers.MULTIPLEXING_DEBUGSERVICE) private debugService: IDebugService, + @inject(IConfigurationService) private configService: IConfigurationService + ) {} + + public get refreshRequired(): Event { + return this.refreshEventEmitter.event; + } + + public get active(): boolean { + return this.debugService.activeDebugSession !== undefined && this.debuggingStarted; + } + + // IJupyterVariables implementation + public async getVariables( + notebook: INotebook, + request: IJupyterVariablesRequest + ): Promise { + // Listen to notebook events if we haven't already + this.watchNotebook(notebook); + + const result: IJupyterVariablesResponse = { + executionCount: request.executionCount, + pageStartIndex: 0, + pageResponse: [], + totalCount: 0, + refreshCount: request.refreshCount + }; + + if (this.active) { + const startPos = request.startIndex ? request.startIndex : 0; + const chunkSize = request.pageSize ? request.pageSize : 100; + result.pageStartIndex = startPos; + + // Do one at a time. All at once doesn't work as they all have to wait for each other anyway + for (let i = startPos; i < startPos + chunkSize && i < this.lastKnownVariables.length; i += 1) { + const fullVariable = !this.lastKnownVariables[i].truncated + ? this.lastKnownVariables[i] + : await this.getFullVariable(this.lastKnownVariables[i], notebook); + this.lastKnownVariables[i] = fullVariable; + result.pageResponse.push(fullVariable); + } + result.totalCount = this.lastKnownVariables.length; + } + + return result; + } + + public async getMatchingVariable(notebook: INotebook, name: string): Promise { + if (this.active) { + // Note, full variable results isn't necessary for this call. It only really needs the variable value. + const result = this.lastKnownVariables.find((v) => v.name === name); + if (result) { + if (notebook.identity.fsPath.endsWith('.ipynb')) { + sendTelemetryEvent(Telemetry.RunByLineVariableHover); + } + } + return result; + } + } + + public async getDataFrameInfo(targetVariable: IJupyterVariable, notebook: INotebook): Promise { + if (!this.active) { + // No active server just return the unchanged target variable + return targetVariable; + } + // Listen to notebook events if we haven't already + this.watchNotebook(notebook); + + // See if we imported or not into the kernel our special function + await this.importDataFrameScripts(notebook); + + // Then eval calling the main function with our target variable + const results = await this.evaluate( + `${DataFrameLoading.DataFrameInfoFunc}(${targetVariable.name})`, + // tslint:disable-next-line: no-any + (targetVariable as any).frameId + ); + + // Results should be the updated variable. + return { + ...targetVariable, + ...JSON.parse(results.result.slice(1, -1)) + }; + } + + public async getDataFrameRows( + targetVariable: IJupyterVariable, + notebook: INotebook, + start: number, + end: number + ): Promise<{}> { + // Run the get dataframe rows script + if (!this.debugService.activeDebugSession || targetVariable.columns === undefined) { + // No active server just return no rows + return {}; + } + // Listen to notebook events if we haven't already + this.watchNotebook(notebook); + + // See if we imported or not into the kernel our special function + await this.importDataFrameScripts(notebook); + + // Since the debugger splits up long requests, split this based on the number of items. + + // Maximum 100 cells at a time or one row + // tslint:disable-next-line: no-any + let output: any; + const minnedEnd = Math.min(targetVariable.rowCount || 0, end); + const totalRowCount = end - start; + const cellsPerRow = targetVariable.columns!.length; + const chunkSize = Math.floor(Math.max(1, Math.min(100 / cellsPerRow, totalRowCount / cellsPerRow))); + for (let pos = start; pos < end; pos += chunkSize) { + const chunkEnd = Math.min(pos + chunkSize, minnedEnd); + const results = await this.evaluate( + `${DataFrameLoading.DataFrameRowFunc}(${targetVariable.name}, ${pos}, ${chunkEnd})`, + // tslint:disable-next-line: no-any + (targetVariable as any).frameId + ); + const chunkResults = JSON.parse(results.result.slice(1, -1)); + if (output && output.data) { + output = { + ...output, + data: output.data.concat(chunkResults.data) + }; + } else { + output = chunkResults; + } + } + + // Results should be the rows. + return output; + } + + // tslint:disable-next-line: no-any + public onDidSendMessage(message: any) { + // When the initialize response comes back, indicate we have started. + if (message.type === 'response' && message.command === 'initialize') { + this.debuggingStarted = true; + } else if (message.type === 'response' && message.command === 'variables' && message.body) { + // If using the interactive debugger, update our variables. + // tslint:disable-next-line: no-suspicious-comment + // TODO: Figure out what resource to use + this.updateVariables(undefined, message as DebugProtocol.VariablesResponse); + } else if (message.type === 'response' && message.command === 'stackTrace') { + // This should be the top frame. We need to use this to compute the value of a variable + this.updateStackFrame(message as DebugProtocol.StackTraceResponse); + } else if (message.type === 'event' && message.event === 'terminated') { + // When the debugger exits, make sure the variables are cleared + this.lastKnownVariables = []; + this.topMostFrameId = 0; + this.debuggingStarted = false; + this.refreshEventEmitter.fire(); + } + } + + private watchNotebook(notebook: INotebook) { + const key = notebook.identity.toString(); + if (!this.watchedNotebooks.has(key)) { + const disposables: Disposable[] = []; + disposables.push(notebook.onKernelChanged(this.resetImport.bind(this, key))); + disposables.push(notebook.onKernelRestarted(this.resetImport.bind(this, key))); + disposables.push( + notebook.onDisposed(() => { + this.resetImport(key); + disposables.forEach((d) => d.dispose()); + this.watchedNotebooks.delete(key); + }) + ); + this.watchedNotebooks.set(key, disposables); + } + } + + private resetImport(key: string) { + this.importedIntoKernel.delete(key); + } + + // tslint:disable-next-line: no-any + private async evaluate(code: string, frameId?: number): Promise { + if (this.debugService.activeDebugSession) { + const results = await this.debugService.activeDebugSession.customRequest('evaluate', { + expression: code, + frameId: this.topMostFrameId || frameId, + context: 'repl' + }); + if (results && results.result !== 'None') { + return results; + } else { + traceError(`Cannot evaluate ${code}`); + return undefined; + } + } + throw Error('Debugger is not active, cannot evaluate.'); + } + + private async importDataFrameScripts(notebook: INotebook): Promise { + try { + const key = notebook.identity.toString(); + if (!this.importedIntoKernel.has(key)) { + await this.evaluate(DataFrameLoading.DataFrameSysImport); + await this.evaluate(DataFrameLoading.DataFrameInfoImport); + await this.evaluate(DataFrameLoading.DataFrameRowImport); + await this.evaluate(DataFrameLoading.VariableInfoImport); + this.importedIntoKernel.add(key); + } + } catch (exc) { + traceError('Error attempting to import in debugger', exc); + } + } + + private updateStackFrame(stackResponse: DebugProtocol.StackTraceResponse) { + if (stackResponse.body.stackFrames[0]) { + this.topMostFrameId = stackResponse.body.stackFrames[0].id; + } + } + + private async getFullVariable(variable: IJupyterVariable, notebook: INotebook): Promise { + // See if we imported or not into the kernel our special function + await this.importDataFrameScripts(notebook); + + // Then eval calling the variable info function with our target variable + const results = await this.evaluate( + `${DataFrameLoading.VariableInfoFunc}(${variable.name})`, + // tslint:disable-next-line: no-any + (variable as any).frameId + ); + if (results && results.result) { + // Results should be the updated variable. + return { + ...variable, + truncated: false, + ...JSON.parse(results.result.slice(1, -1)) + }; + } else { + // If no results, just return current value. Better than nothing. + return variable; + } + } + + private updateVariables(resource: Resource, variablesResponse: DebugProtocol.VariablesResponse) { + const exclusionList = this.configService.getSettings(resource).datascience.variableExplorerExclude + ? this.configService.getSettings().datascience.variableExplorerExclude?.split(';') + : []; + + const allowedVariables = variablesResponse.body.variables.filter((v) => { + if (!v.name || !v.type || !v.value) { + return false; + } + if (exclusionList && exclusionList.includes(v.type)) { + return false; + } + if (v.name.startsWith('_')) { + return false; + } + if (KnownExcludedVariables.has(v.name)) { + return false; + } + if (v.type === 'NoneType') { + return false; + } + return true; + }); + + this.lastKnownVariables = allowedVariables.map((v) => { + return { + name: v.name, + type: v.type!, + count: 0, + shape: '', + size: 0, + supportsDataExplorer: DataViewableTypes.has(v.type || ''), + value: v.value, + truncated: true, + frameId: v.variablesReference + }; + }); + + this.refreshEventEmitter.fire(); + } +} diff --git a/src/client/datascience/jupyter/interpreter/README.md b/src/client/datascience/jupyter/interpreter/README.md new file mode 100644 index 000000000000..c8f28751630c --- /dev/null +++ b/src/client/datascience/jupyter/interpreter/README.md @@ -0,0 +1 @@ +# Contains code related to the interpreter(s) used to start Jupyter, get kernel specs, etc. diff --git a/src/client/datascience/jupyter/interpreter/jupyterCommand.ts b/src/client/datascience/jupyter/interpreter/jupyterCommand.ts new file mode 100644 index 000000000000..7204f12a3b28 --- /dev/null +++ b/src/client/datascience/jupyter/interpreter/jupyterCommand.ts @@ -0,0 +1,339 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { SpawnOptions } from 'child_process'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { traceError } from '../../../common/logger'; +import { + ExecutionResult, + IProcessService, + IProcessServiceFactory, + IPythonDaemonExecutionService, + IPythonExecutionFactory, + IPythonExecutionService, + ObservableExecutionResult +} from '../../../common/process/types'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { IEnvironmentActivationService } from '../../../interpreter/activation/types'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { JupyterCommands, JupyterDaemonModule } from '../../constants'; +import { IJupyterCommand, IJupyterCommandFactory } from '../../types'; + +// JupyterCommand objects represent some process that can be launched that should be guaranteed to work because it +// was found by testing it previously +class ProcessJupyterCommand implements IJupyterCommand { + private exe: string; + private requiredArgs: string[]; + private launcherPromise: Promise; + private interpreterPromise: Promise; + private activationHelper: IEnvironmentActivationService; + + constructor( + exe: string, + args: string[], + processServiceFactory: IProcessServiceFactory, + activationHelper: IEnvironmentActivationService, + interpreterService: IInterpreterService + ) { + this.exe = exe; + this.requiredArgs = args; + this.launcherPromise = processServiceFactory.create(); + this.activationHelper = activationHelper; + this.interpreterPromise = interpreterService.getInterpreterDetails(this.exe).catch((_e) => undefined); + } + + public interpreter(): Promise { + return this.interpreterPromise; + } + + public async execObservable(args: string[], options: SpawnOptions): Promise> { + const newOptions = { ...options }; + newOptions.env = await this.fixupEnv(newOptions.env); + const launcher = await this.launcherPromise; + const newArgs = [...this.requiredArgs, ...args]; + return launcher.execObservable(this.exe, newArgs, newOptions); + } + + public async exec(args: string[], options: SpawnOptions): Promise> { + const newOptions = { ...options }; + newOptions.env = await this.fixupEnv(newOptions.env); + const launcher = await this.launcherPromise; + const newArgs = [...this.requiredArgs, ...args]; + return launcher.exec(this.exe, newArgs, newOptions); + } + + private fixupEnv(_env?: NodeJS.ProcessEnv): Promise { + if (this.activationHelper) { + return this.activationHelper.getActivatedEnvironmentVariables(undefined); + } + + return Promise.resolve(process.env); + } +} + +class InterpreterJupyterCommand implements IJupyterCommand { + protected interpreterPromise: Promise; + private pythonLauncher: Promise; + + constructor( + protected readonly moduleName: string, + protected args: string[], + protected readonly pythonExecutionFactory: IPythonExecutionFactory, + private readonly _interpreter: PythonEnvironment, + isActiveInterpreter: boolean + ) { + this.interpreterPromise = Promise.resolve(this._interpreter); + this.pythonLauncher = this.interpreterPromise.then(async (interpreter) => { + // Create a daemon only if the interpreter is the same as the current interpreter. + // We don't want too many daemons (we don't want one for each of the users interpreter on their machine). + if (isActiveInterpreter) { + const svc = await pythonExecutionFactory.createDaemon({ + daemonModule: JupyterDaemonModule, + pythonPath: interpreter!.path + }); + + // If we're using this command to start notebook, then ensure the daemon can start a notebook inside it. + if ( + (moduleName.toLowerCase() === 'jupyter' && + args.join(' ').toLowerCase().startsWith('-m jupyter notebook')) || + (moduleName.toLowerCase() === 'notebook' && args.join(' ').toLowerCase().startsWith('-m notebook')) + ) { + try { + const output = await svc.exec( + [ + path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'jupyter_nbInstalled.py' + ) + ], + {} + ); + if (output.stdout.toLowerCase().includes('available')) { + return svc; + } + } catch (ex) { + traceError('Checking whether notebook is importable failed', ex); + } + } + } + return pythonExecutionFactory.createActivatedEnvironment({ + interpreter: this._interpreter, + bypassCondaExecution: true + }); + }); + } + public interpreter(): Promise { + return this.interpreterPromise; + } + + public async execObservable(args: string[], options: SpawnOptions): Promise> { + const newOptions = { ...options, extraVariables: { PYTHONWARNINGS: 'ignore' } }; + const launcher = await this.pythonLauncher; + const newArgs = [...this.args, ...args]; + const moduleName = newArgs[1]; + newArgs.shift(); // Remove '-m' + newArgs.shift(); // Remove module name + return launcher.execModuleObservable(moduleName, newArgs, newOptions); + } + + public async exec(args: string[], options: SpawnOptions): Promise> { + const newOptions = { ...options, extraVariables: { PYTHONWARNINGS: 'ignore' } }; + const launcher = await this.pythonLauncher; + const newArgs = [...this.args, ...args]; + const moduleName = newArgs[1]; + newArgs.shift(); // Remove '-m' + newArgs.shift(); // Remove module name + return launcher.execModule(moduleName, newArgs, newOptions); + } +} + +/** + * This class is used to launch the notebook. + * I.e. anything to do with the command `python -m jupyter notebook` or `python -m notebook`. + * + * @class InterpreterJupyterNotebookCommand + * @implements {IJupyterCommand} + */ +export class InterpreterJupyterNotebookCommand extends InterpreterJupyterCommand { + constructor( + moduleName: string, + args: string[], + pythonExecutionFactory: IPythonExecutionFactory, + interpreter: PythonEnvironment, + isActiveInterpreter: boolean + ) { + super(moduleName, args, pythonExecutionFactory, interpreter, isActiveInterpreter); + } +} + +/** + * This class is used to handle kernelspecs. + * I.e. anything to do with the command `python -m jupyter kernelspec`. + * + * @class InterpreterJupyterKernelSpecCommand + * @implements {IJupyterCommand} + */ +// tslint:disable-next-line: max-classes-per-file +export class InterpreterJupyterKernelSpecCommand extends InterpreterJupyterCommand { + constructor( + moduleName: string, + args: string[], + pythonExecutionFactory: IPythonExecutionFactory, + interpreter: PythonEnvironment, + isActiveInterpreter: boolean + ) { + super(moduleName, args, pythonExecutionFactory, interpreter, isActiveInterpreter); + } + + /** + * Kernelspec subcommand requires special treatment. + * Its possible the sub command hasn't been registered (i.e. jupyter kernelspec command hasn't been installed). + * However its possible the kernlspec modules are available. + * So here's what we have: + * - python -m jupyter kernelspec --version (throws an error, as kernelspect sub command not installed) + * - `import jupyter_client.kernelspec` (works, hence kernelspec modules are available) + * - Problem is daemon will say that `kernelspec` is avaiable, as daemon can work with the `jupyter_client.kernelspec`. + * But rest of extension will assume kernelspec is available and `python -m jupyter kenerlspec --version` will fall over. + * Solution: + * - Run using daemon wrapper code if possible (we don't know whether daemon or python process will run kernel spec). + * - Now, its possible the python daemon process is busy in which case we fall back (in daemon wrapper) to using a python process to run the code. + * - However `python -m jupyter kernelspec` will fall over (as such a sub command hasn't been installed), hence calling daemon code will fail. + * - What we do in such an instance is run the python code `python xyz.py` to deal with kernels. + * If that works, great. + * If that fails, then we know that `kernelspec` sub command doesn't exist and `import jupyter_client.kernelspec` also doesn't work. + * In such a case re-throw the exception from the first execution (possibly the daemon wrapper). + * @param {string[]} args + * @param {SpawnOptions} options + * @returns {Promise>} + * @memberof InterpreterJupyterKernelSpecCommand + */ + public async exec(args: string[], options: SpawnOptions): Promise> { + let exception: Error | undefined; + let output: ExecutionResult = { stdout: '' }; + try { + output = await super.exec(args, options); + } catch (ex) { + exception = ex; + } + + if (!output.stderr && !exception) { + return output; + } + + const defaultAction = () => { + if (exception) { + traceError(`Exception attempting to enumerate kernelspecs: `, exception); + throw exception; + } + return output; + }; + + // We're only interested in `python -m jupyter kernelspec` + const interpreter = await this.interpreter(); + if ( + !interpreter || + this.moduleName.toLowerCase() !== 'jupyter' || + this.args.join(' ').toLowerCase() !== `-m jupyter ${JupyterCommands.KernelSpecCommand}`.toLowerCase() + ) { + return defaultAction(); + } + + // Otherwise try running a script instead. + try { + if (args.join(' ').toLowerCase() === 'list --json') { + // Try getting kernels using python script, if that fails (even if there's output in stderr) rethrow original exception. + output = await this.getKernelSpecList(interpreter, options); + return output; + } else if (args.join(' ').toLowerCase() === '--version') { + // Try getting kernelspec version using python script, if that fails (even if there's output in stderr) rethrow original exception. + output = await this.getKernelSpecVersion(interpreter, options); + return output; + } + } catch (innerEx) { + traceError('Failed to get a list of the kernelspec using python script', innerEx); + } + return defaultAction(); + } + + private async getKernelSpecList(interpreter: PythonEnvironment, options: SpawnOptions) { + // Try getting kernels using python script, if that fails (even if there's output in stderr) rethrow original exception. + const activatedEnv = await this.pythonExecutionFactory.createActivatedEnvironment({ + interpreter, + bypassCondaExecution: true + }); + return activatedEnv.exec( + [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'vscode_datascience_helpers', 'getJupyterKernels.py')], + { ...options, throwOnStdErr: true } + ); + } + private async getKernelSpecVersion(interpreter: PythonEnvironment, options: SpawnOptions) { + // Try getting kernels using python script, if that fails (even if there's output in stderr) rethrow original exception. + const activatedEnv = await this.pythonExecutionFactory.createActivatedEnvironment({ + interpreter, + bypassCondaExecution: true + }); + return activatedEnv.exec( + [ + path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'getJupyterKernelspecVersion.py' + ) + ], + { ...options, throwOnStdErr: true } + ); + } +} + +// tslint:disable-next-line: max-classes-per-file +@injectable() +export class JupyterCommandFactory implements IJupyterCommandFactory { + constructor( + @inject(IPythonExecutionFactory) private readonly executionFactory: IPythonExecutionFactory, + @inject(IEnvironmentActivationService) private readonly activationHelper: IEnvironmentActivationService, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService + ) {} + + public createInterpreterCommand( + command: JupyterCommands, + moduleName: string, + args: string[], + interpreter: PythonEnvironment, + isActiveInterpreter: boolean + ): IJupyterCommand { + if (command === JupyterCommands.NotebookCommand) { + return new InterpreterJupyterNotebookCommand( + moduleName, + args, + this.executionFactory, + interpreter, + isActiveInterpreter + ); + } else if (command === JupyterCommands.KernelSpecCommand) { + return new InterpreterJupyterKernelSpecCommand( + moduleName, + args, + this.executionFactory, + interpreter, + isActiveInterpreter + ); + } + return new InterpreterJupyterCommand(moduleName, args, this.executionFactory, interpreter, isActiveInterpreter); + } + + public createProcessCommand(exe: string, args: string[]): IJupyterCommand { + return new ProcessJupyterCommand( + exe, + args, + this.processServiceFactory, + this.activationHelper, + this.interpreterService + ); + } +} diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts new file mode 100644 index 000000000000..d951338264bf --- /dev/null +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts @@ -0,0 +1,350 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationToken } from 'vscode'; +import { IApplicationShell } from '../../../common/application/types'; +import { Cancellation, createPromiseFromCancellation, wrapCancellationTokens } from '../../../common/cancellation'; +import { ProductNames } from '../../../common/installer/productNames'; +import { traceError } from '../../../common/logger'; +import { IInstaller, InstallerResponse, Product } from '../../../common/types'; +import { Common, DataScience } from '../../../common/utils/localize'; +import { noop } from '../../../common/utils/misc'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { HelpLinks, JupyterCommands, Telemetry } from '../../constants'; +import { reportAction } from '../../progress/decorator'; +import { ReportableAction } from '../../progress/types'; +import { IJupyterCommandFactory } from '../../types'; +import { JupyterInstallError } from '../jupyterInstallError'; + +export enum JupyterInterpreterDependencyResponse { + ok, + selectAnotherInterpreter, + cancel +} + +/** + * Sorts the given list of products (in place) in the order in which they need to be installed. + * E.g. when installing the modules `notebook` and `Jupyter`, its best to first install `Jupyter`. + * + * @param {Product[]} products + */ +function sortProductsInOrderForInstallation(products: Product[]) { + products.sort((a, b) => { + if (a === Product.jupyter) { + return -1; + } + if (b === Product.jupyter) { + return 1; + } + if (a === Product.notebook) { + return -1; + } + if (b === Product.notebook) { + return 1; + } + return 0; + }); +} +/** + * Given a list of products, this will return an error message of the form: + * `Data Science library jupyter not installed` + * `Data Science libraries, jupyter and notebook not installed` + * `Data Science libraries, jupyter, notebook and nbconvert not installed` + * + * @export + * @param {Product[]} products + * @param {string} [interpreterName] + * @returns {string} + */ +export function getMessageForLibrariesNotInstalled(products: Product[], interpreterName?: string): string { + // Even though kernelspec cannot be installed, display it so user knows what is missing. + const names = products + .map((product) => ProductNames.get(product)) + .filter((name) => !!name) + .map((name) => name as string); + + switch (names.length) { + case 0: + return ''; + case 1: + return interpreterName + ? DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter().format(interpreterName, names[0]) + : DataScience.libraryRequiredToLaunchJupyterNotInstalled().format(names[0]); + default: { + const lastItem = names.pop(); + return interpreterName + ? DataScience.librariesRequiredToLaunchJupyterNotInstalledInterpreter().format( + interpreterName, + `${names.join(', ')} ${Common.and()} ${lastItem}` + ) + : DataScience.librariesRequiredToLaunchJupyterNotInstalled().format( + `${names.join(', ')} ${Common.and()} ${lastItem}` + ); + } + } +} + +/** + * Responsible for managing dependencies of a Python interpreter required to run Jupyter. + * If required modules aren't installed, will prompt user to install them or select another interpreter. + * + * @export + * @class JupyterInterpreterDependencyService + */ +@injectable() +export class JupyterInterpreterDependencyService { + /** + * Keeps track of the fact that all dependencies are available in an interpreter. + * This cache will be cleared only after reloading VS Code or when the background code detects that modules are not available. + * E.g. every time a user makes a request to get the interpreter information, we use the cache if everything is ok. + * However we still run the code in the background to check if the modules are available, and then update the cache with the results. + * + * @private + * @memberof JupyterInterpreterDependencyService + */ + private readonly dependenciesInstalledInInterpreter = new Set(); + /** + * Same as `dependenciesInstalledInInterpreter`. + * + * @private + * @memberof JupyterInterpreterDependencyService + */ + private readonly nbconvertInstalledInInterpreter = new Set(); + constructor( + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(IInstaller) private readonly installer: IInstaller, + @inject(IJupyterCommandFactory) private readonly commandFactory: IJupyterCommandFactory + ) {} + /** + * Configures the python interpreter to ensure it can run Jupyter server by installing any missing dependencies. + * If user opts not to install they can opt to select another interpreter. + * + * @param {PythonEnvironment} interpreter + * @param {JupyterInstallError} [_error] + * @param {CancellationToken} [token] + * @returns {Promise} + * @memberof JupyterInterpreterDependencyService + */ + @reportAction(ReportableAction.InstallingMissingDependencies) + public async installMissingDependencies( + interpreter: PythonEnvironment, + _error?: JupyterInstallError, + token?: CancellationToken + ): Promise { + const missingProducts = await this.getDependenciesNotInstalled(interpreter, token); + if (Cancellation.isCanceled(token)) { + return JupyterInterpreterDependencyResponse.cancel; + } + if (missingProducts.length === 0) { + return JupyterInterpreterDependencyResponse.ok; + } + + const message = getMessageForLibrariesNotInstalled(missingProducts, interpreter.displayName); + + sendTelemetryEvent(Telemetry.JupyterNotInstalledErrorShown); + const selection = await this.applicationShell.showErrorMessage( + message, + DataScience.jupyterInstall(), + DataScience.selectDifferentJupyterInterpreter(), + DataScience.pythonInteractiveHelpLink() + ); + + if (Cancellation.isCanceled(token)) { + return JupyterInterpreterDependencyResponse.cancel; + } + + switch (selection) { + case DataScience.jupyterInstall(): { + // Ignore kernelspec as it not something that can be installed. + // If kernelspec isn't available, then re-install `Jupyter`. + if (missingProducts.includes(Product.kernelspec) && !missingProducts.includes(Product.jupyter)) { + missingProducts.push(Product.jupyter); + } + const productsToInstall = missingProducts.filter((product) => product !== Product.kernelspec); + // Install jupyter, then notebook, then others in that order. + sortProductsInOrderForInstallation(productsToInstall); + + let productToInstall = productsToInstall.shift(); + const cancellatonPromise = createPromiseFromCancellation({ + cancelAction: 'resolve', + defaultValue: InstallerResponse.Ignore, + token + }); + while (productToInstall) { + // Always pass a cancellation token to `install`, to ensure it waits until the module is installed. + const response = await Promise.race([ + this.installer.install(productToInstall, interpreter, wrapCancellationTokens(token)), + cancellatonPromise + ]); + if (response === InstallerResponse.Installed) { + productToInstall = productsToInstall.shift(); + continue; + } else { + return JupyterInterpreterDependencyResponse.cancel; + } + } + sendTelemetryEvent(Telemetry.UserInstalledJupyter); + + // Check if kernelspec module is something that accessible. + return this.checkKernelSpecAvailability(interpreter); + } + + case DataScience.selectDifferentJupyterInterpreter(): { + sendTelemetryEvent(Telemetry.UserDidNotInstallJupyter); + return JupyterInterpreterDependencyResponse.selectAnotherInterpreter; + } + + case DataScience.pythonInteractiveHelpLink(): { + this.applicationShell.openUrl(HelpLinks.PythonInteractiveHelpLink); + sendTelemetryEvent(Telemetry.UserDidNotInstallJupyter); + return JupyterInterpreterDependencyResponse.cancel; + } + + default: + sendTelemetryEvent(Telemetry.UserDidNotInstallJupyter); + return JupyterInterpreterDependencyResponse.cancel; + } + } + /** + * Whether all dependencies required to start & use a jupyter server are available in the provided interpreter. + * + * @param {PythonEnvironment} interpreter + * @param {CancellationToken} [token] + * @returns {Promise} + * @memberof JupyterInterpreterConfigurationService + */ + public async areDependenciesInstalled(interpreter: PythonEnvironment, token?: CancellationToken): Promise { + return this.getDependenciesNotInstalled(interpreter, token).then((items) => items.length === 0); + } + + /** + * Whether its possible to export ipynb to other formats. + * Basically checks whether nbconvert is installed. + * + * @param {PythonEnvironment} interpreter + * @param {CancellationToken} [_token] + * @returns {Promise} + * @memberof JupyterInterpreterConfigurationService + */ + public async isExportSupported(interpreter: PythonEnvironment, _token?: CancellationToken): Promise { + if (this.nbconvertInstalledInInterpreter.has(interpreter.path)) { + return true; + } + const installed = this.installer.isInstalled(Product.nbconvert, interpreter).then((result) => result === true); + if (installed) { + this.nbconvertInstalledInInterpreter.add(interpreter.path); + } + return installed; + } + + /** + * Gets a list of the dependencies not installed, dependencies that are required to launch the jupyter notebook server. + * + * @param {PythonEnvironment} interpreter + * @param {CancellationToken} [token] + * @returns {Promise} + * @memberof JupyterInterpreterConfigurationService + */ + public async getDependenciesNotInstalled( + interpreter: PythonEnvironment, + token?: CancellationToken + ): Promise { + // If we know that all modules were available at one point in time, then use that cache. + if (this.dependenciesInstalledInInterpreter.has(interpreter.path)) { + return []; + } + + const notInstalled: Product[] = []; + await Promise.race([ + Promise.all([ + this.installer + .isInstalled(Product.jupyter, interpreter) + .then((installed) => (installed ? noop() : notInstalled.push(Product.jupyter))), + this.installer + .isInstalled(Product.notebook, interpreter) + .then((installed) => (installed ? noop() : notInstalled.push(Product.notebook))) + ]), + createPromiseFromCancellation({ cancelAction: 'resolve', defaultValue: undefined, token }) + ]); + + if (notInstalled.length > 0) { + return notInstalled; + } + if (Cancellation.isCanceled(token)) { + return []; + } + // Perform this check only if jupyter & notebook modules are installed. + const products = await this.isKernelSpecAvailable(interpreter, token).then((installed) => + installed ? [] : [Product.kernelspec] + ); + if (products.length === 0) { + this.dependenciesInstalledInInterpreter.add(interpreter.path); + } + return products; + } + + /** + * Checks whether the jupyter sub command kernelspec is available. + * + * @private + * @param {PythonEnvironment} interpreter + * @param {CancellationToken} [_token] + * @returns {Promise} + * @memberof JupyterInterpreterConfigurationService + */ + private async isKernelSpecAvailable(interpreter: PythonEnvironment, _token?: CancellationToken): Promise { + const command = this.commandFactory.createInterpreterCommand( + JupyterCommands.KernelSpecCommand, + 'jupyter', + ['-m', 'jupyter', 'kernelspec'], + interpreter, + false + ); + return command + .exec(['--version'], { throwOnStdErr: true }) + .then(() => true) + .catch((e) => { + traceError(`Kernel spec not found: `, e); + sendTelemetryEvent(Telemetry.KernelSpecNotFound); + return false; + }); + } + + /** + * Even if jupyter module is installed, its possible kernelspec isn't available. + * Possible user has an old version of jupyter or something is corrupted. + * This is an edge case, and we need to handle this. + * Current solution is to get user to select another interpreter or update jupyter/python (we don't know what is wrong). + * + * @private + * @param {PythonEnvironment} interpreter + * @param {CancellationToken} [token] + * @returns {Promise} + * @memberof JupyterInterpreterConfigurationService + */ + private async checkKernelSpecAvailability( + interpreter: PythonEnvironment, + token?: CancellationToken + ): Promise { + if (await this.isKernelSpecAvailable(interpreter)) { + return JupyterInterpreterDependencyResponse.ok; + } + // Indicate no kernel spec module. + sendTelemetryEvent(Telemetry.JupyterInstalledButNotKernelSpecModule); + if (Cancellation.isCanceled(token)) { + return JupyterInterpreterDependencyResponse.cancel; + } + const selectionFromError = await this.applicationShell.showErrorMessage( + DataScience.jupyterKernelSpecModuleNotFound().format(interpreter.path), + DataScience.selectDifferentJupyterInterpreter(), + Common.cancel() + ); + return selectionFromError === DataScience.selectDifferentJupyterInterpreter() + ? JupyterInterpreterDependencyResponse.selectAnotherInterpreter + : JupyterInterpreterDependencyResponse.cancel; + } +} diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.ts new file mode 100644 index 000000000000..86b8a4d26889 --- /dev/null +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IWorkspaceService } from '../../../common/application/types'; +import { IPersistentState, IPersistentStateFactory } from '../../../common/types'; + +type CacheInfo = { + /** + * Cache store (across VSC sessions). + * + * @type {IPersistentState} + */ + state: IPersistentState; +}; + +@injectable() +export class JupyterInterpreterOldCacheStateStore { + private readonly workspaceJupyterInterpreter: CacheInfo; + private readonly globalJupyterInterpreter: CacheInfo; + constructor( + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IPersistentStateFactory) persistentStateFactory: IPersistentStateFactory + ) { + // Cache stores to keep track of jupyter interpreters found. + const workspaceState = persistentStateFactory.createWorkspacePersistentState( + 'DS-VSC-JupyterInterpreter' + ); + const globalState = persistentStateFactory.createGlobalPersistentState('DS-VSC-JupyterInterpreter'); + this.workspaceJupyterInterpreter = { state: workspaceState }; + this.globalJupyterInterpreter = { state: globalState }; + } + private get cacheStore(): CacheInfo { + return this.workspace.hasWorkspaceFolders ? this.workspaceJupyterInterpreter : this.globalJupyterInterpreter; + } + public getCachedInterpreterPath(): string | undefined { + return this.cacheStore.state.value; + } + public async clearCache(): Promise { + await this.cacheStore.state.updateValue(undefined); + } +} diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand.ts new file mode 100644 index 000000000000..7ee3aaf42cf0 --- /dev/null +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../../activation/types'; +import { ICommandManager } from '../../../common/application/types'; +import { IDisposableRegistry } from '../../../common/types'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { Telemetry } from '../../constants'; +import { JupyterInterpreterService } from './jupyterInterpreterService'; + +@injectable() +export class JupyterInterpreterSelectionCommand implements IExtensionSingleActivationService { + constructor( + @inject(JupyterInterpreterService) private readonly service: JupyterInterpreterService, + @inject(ICommandManager) private readonly cmdManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry + ) {} + public async activate(): Promise { + this.disposables.push( + this.cmdManager.registerCommand('python.datascience.selectJupyterInterpreter', () => { + sendTelemetryEvent(Telemetry.SelectJupyterInterpreterCommand); + this.service.selectInterpreter().ignoreErrors(); + }) + ); + } +} diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterSelector.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterSelector.ts new file mode 100644 index 000000000000..396119f68af0 --- /dev/null +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterSelector.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { QuickPickOptions } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { IApplicationShell, IWorkspaceService } from '../../../common/application/types'; +import { Cancellation } from '../../../common/cancellation'; +import { IPathUtils } from '../../../common/types'; +import { DataScience } from '../../../common/utils/localize'; +import { IInterpreterSelector } from '../../../interpreter/configuration/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { JupyterInterpreterStateStore } from './jupyterInterpreterStateStore'; + +/** + * Displays interpreter select and returns the selection to the user. + * + * @export + * @class JupyterInterpreterSelector + */ +@injectable() +export class JupyterInterpreterSelector { + constructor( + @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(JupyterInterpreterStateStore) private readonly interpreterSelectionState: JupyterInterpreterStateStore, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IPathUtils) private readonly pathUtils: IPathUtils + ) {} + /** + * Displays interpreter selector and returns the selection. + * + * @param {CancellationToken} [token] + * @returns {(Promise)} + * @memberof JupyterInterpreterSelector + */ + public async selectInterpreter(token?: CancellationToken): Promise { + const workspace = this.workspace.getWorkspaceFolder(undefined); + const currentPythonPath = this.interpreterSelectionState.selectedPythonPath + ? this.pathUtils.getDisplayName(this.interpreterSelectionState.selectedPythonPath, workspace?.uri.fsPath) + : undefined; + + const suggestions = await this.interpreterSelector.getSuggestions(undefined); + if (Cancellation.isCanceled(token)) { + return; + } + const quickPickOptions: QuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: currentPythonPath + ? DataScience.currentlySelectedJupyterInterpreterForPlaceholder().format(currentPythonPath) + : '' + }; + + const selection = await this.applicationShell.showQuickPick(suggestions, quickPickOptions); + if (!selection) { + return; + } + return selection.interpreter; + } +} diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts new file mode 100644 index 000000000000..720ac972f3e4 --- /dev/null +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterService.ts @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { createPromiseFromCancellation } from '../../../common/cancellation'; +import '../../../common/extensions'; +import { noop } from '../../../common/utils/misc'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { Telemetry } from '../../constants'; +import { JupyterInstallError } from '../jupyterInstallError'; +import { + JupyterInterpreterDependencyResponse, + JupyterInterpreterDependencyService +} from './jupyterInterpreterDependencyService'; +import { JupyterInterpreterOldCacheStateStore } from './jupyterInterpreterOldCacheStateStore'; +import { JupyterInterpreterSelector } from './jupyterInterpreterSelector'; +import { JupyterInterpreterStateStore } from './jupyterInterpreterStateStore'; + +@injectable() +export class JupyterInterpreterService { + private _selectedInterpreter?: PythonEnvironment; + private _onDidChangeInterpreter = new EventEmitter(); + private getInitialInterpreterPromise: Promise | undefined; + public get onDidChangeInterpreter(): Event { + return this._onDidChangeInterpreter.event; + } + + constructor( + @inject(JupyterInterpreterOldCacheStateStore) + private readonly oldVersionCacheStateStore: JupyterInterpreterOldCacheStateStore, + @inject(JupyterInterpreterStateStore) private readonly interpreterSelectionState: JupyterInterpreterStateStore, + @inject(JupyterInterpreterSelector) private readonly jupyterInterpreterSelector: JupyterInterpreterSelector, + @inject(JupyterInterpreterDependencyService) + private readonly interpreterConfiguration: JupyterInterpreterDependencyService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService + ) {} + /** + * Gets the selected interpreter configured to run Jupyter. + * + * @param {CancellationToken} [token] + * @returns {(Promise)} + * @memberof JupyterInterpreterService + */ + public async getSelectedInterpreter(token?: CancellationToken): Promise { + // Before we return _selected interpreter make sure that we have run our initial set interpreter once + // because _selectedInterpreter can be changed by other function and at other times, this promise + // is cached to only run once + await this.setInitialInterpreter(token); + + return this._selectedInterpreter; + } + + // To be run one initial time. Check our saved locations and then current interpreter to try to start off + // with a valid jupyter interpreter + public async setInitialInterpreter(token?: CancellationToken): Promise { + if (!this.getInitialInterpreterPromise) { + this.getInitialInterpreterPromise = this.getInitialInterpreterImpl(token).then((result) => { + // Set ourselves as a valid interpreter if we found something + if (result) { + this.changeSelectedInterpreterProperty(result); + } + return result; + }); + } + + return this.getInitialInterpreterPromise; + } + + /** + * Selects and interpreter to run jupyter server. + * Validates and configures the interpreter. + * Once completed, the interpreter is stored in settings, else user can select another interpreter. + * + * @param {CancellationToken} [token] + * @returns {(Promise)} + * @memberof JupyterInterpreterService + */ + public async selectInterpreter(token?: CancellationToken): Promise { + const resolveToUndefinedWhenCancelled = createPromiseFromCancellation({ + cancelAction: 'resolve', + defaultValue: undefined, + token + }); + const interpreter = await Promise.race([ + this.jupyterInterpreterSelector.selectInterpreter(), + resolveToUndefinedWhenCancelled + ]); + if (!interpreter) { + sendTelemetryEvent(Telemetry.SelectJupyterInterpreter, undefined, { result: 'notSelected' }); + return; + } + + const result = await this.interpreterConfiguration.installMissingDependencies(interpreter, undefined, token); + switch (result) { + case JupyterInterpreterDependencyResponse.ok: { + await this.setAsSelectedInterpreter(interpreter); + return interpreter; + } + case JupyterInterpreterDependencyResponse.cancel: + sendTelemetryEvent(Telemetry.SelectJupyterInterpreter, undefined, { result: 'installationCancelled' }); + return; + default: + return this.selectInterpreter(token); + } + } + + // Install jupyter dependencies in the current jupyter selected interpreter + // If there is no jupyter selected interpreter, prompt for install into the + // current active interpreter and set as active if successful + public async installMissingDependencies(err?: JupyterInstallError): Promise { + const jupyterInterpreter = await this.getSelectedInterpreter(); + let interpreter = jupyterInterpreter; + if (!interpreter) { + // Use current interpreter. + interpreter = await this.interpreterService.getActiveInterpreter(undefined); + if (!interpreter) { + // Unlikely scenario, user hasn't selected python, python extension will fall over. + // Get user to select something. + await this.selectInterpreter(); + return; + } + } + + const response = await this.interpreterConfiguration.installMissingDependencies(interpreter, err); + if (response === JupyterInterpreterDependencyResponse.selectAnotherInterpreter) { + await this.selectInterpreter(); + } else if (response === JupyterInterpreterDependencyResponse.ok) { + // We might have installed jupyter in a new active interpreter here, if we did and the install + // went ok we also want to select that interpreter as our jupyter selected interperter + // so that on next launch we use it correctly + if (interpreter !== jupyterInterpreter) { + await this.setAsSelectedInterpreter(interpreter); + } + } + } + + // Set the specified interpreter as our current selected interpreter. Public so can + // be set by the test code. + public async setAsSelectedInterpreter(interpreter: PythonEnvironment): Promise { + // Make sure that our initial set has happened before we allow a set so that + // calculation of the initial interpreter doesn't clobber the existing one + await this.setInitialInterpreter(); + this.changeSelectedInterpreterProperty(interpreter); + } + + // Check the location that we stored jupyter launch path in the old version + // if it's there, return it and clear the location + private getInterpreterFromChangeOfOlderVersionOfExtension(): string | undefined { + const pythonPath = this.oldVersionCacheStateStore.getCachedInterpreterPath(); + if (!pythonPath) { + return; + } + + // Clear the cache to not check again + this.oldVersionCacheStateStore.clearCache().ignoreErrors(); + return pythonPath; + } + + private changeSelectedInterpreterProperty(interpreter: PythonEnvironment) { + this._selectedInterpreter = interpreter; + this._onDidChangeInterpreter.fire(interpreter); + this.interpreterSelectionState.updateSelectedPythonPath(interpreter.path); + sendTelemetryEvent(Telemetry.SelectJupyterInterpreter, undefined, { result: 'selected' }); + } + + // For a given python path check if it can run jupyter for us + // if so, return the interpreter + private async validateInterpreterPath( + pythonPath: string, + token?: CancellationToken + ): Promise { + try { + const resolveToUndefinedWhenCancelled = createPromiseFromCancellation({ + cancelAction: 'resolve', + defaultValue: undefined, + token + }); + + // First see if we can get interpreter details + const interpreter = await Promise.race([ + this.interpreterService.getInterpreterDetails(pythonPath, undefined), + resolveToUndefinedWhenCancelled + ]); + if (interpreter) { + // Then check that dependencies are installed + if (await this.interpreterConfiguration.areDependenciesInstalled(interpreter, token)) { + return interpreter; + } + } + } catch (_err) { + // For any errors we are ok with just returning undefined for an invalid interpreter + noop(); + } + return undefined; + } + + private async getInitialInterpreterImpl(token?: CancellationToken): Promise { + let interpreter: PythonEnvironment | undefined; + + // Check the old version location first, we will clear it if we find it here + const oldVersionPythonPath = this.getInterpreterFromChangeOfOlderVersionOfExtension(); + if (oldVersionPythonPath) { + interpreter = await this.validateInterpreterPath(oldVersionPythonPath, token); + } + + // Next check the saved global path + if (!interpreter && this.interpreterSelectionState.selectedPythonPath) { + interpreter = await this.validateInterpreterPath(this.interpreterSelectionState.selectedPythonPath, token); + + // If we had a global path, but it's not valid, trash it + if (!interpreter) { + this.interpreterSelectionState.updateSelectedPythonPath(undefined); + } + } + + // Nothing saved found, so check our current interpreter + if (!interpreter) { + const currentInterpreter = await this.interpreterService.getActiveInterpreter(undefined); + + if (currentInterpreter) { + // If the current active interpreter has everything installed already just use that + if (await this.interpreterConfiguration.areDependenciesInstalled(currentInterpreter, token)) { + interpreter = currentInterpreter; + } + } + } + + return interpreter; + } +} diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts new file mode 100644 index 000000000000..3fecb01848db --- /dev/null +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterStateStore.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Memento } from 'vscode'; +import { GLOBAL_MEMENTO, IMemento } from '../../../common/types'; +import { noop } from '../../../common/utils/misc'; + +const key = 'INTERPRETER_PATH_SELECTED_FOR_JUPYTER_SERVER'; +const keySelected = 'INTERPRETER_PATH_WAS_SELECTED_FOR_JUPYTER_SERVER'; +/** + * Keeps track of whether the user ever selected an interpreter to be used as the global jupyter interpreter. + * Keeps track of the interpreter path of the interpreter used as the global jupyter interpreter. + * + * @export + * @class JupyterInterpreterStateStore + */ +@injectable() +export class JupyterInterpreterStateStore { + private _interpreterPath?: string; + constructor(@inject(IMemento) @named(GLOBAL_MEMENTO) private readonly memento: Memento) {} + + /** + * Whether the user set an interpreter at least once (an interpreter for starting of jupyter). + * + * @readonly + * @type {Promise} + */ + public get interpreterSetAtleastOnce(): boolean { + return !!this.selectedPythonPath || this.memento.get(keySelected, false); + } + public get selectedPythonPath(): string | undefined { + return this._interpreterPath || this.memento.get(key, undefined); + } + public updateSelectedPythonPath(value: string | undefined) { + this._interpreterPath = value; + this.memento.update(key, value).then(noop, noop); + this.memento.update(keySelected, true).then(noop, noop); + } +} diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.ts new file mode 100644 index 000000000000..e65c08a4c544 --- /dev/null +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.ts @@ -0,0 +1,275 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, Uri } from 'vscode'; +import { Cancellation } from '../../../common/cancellation'; +import { traceError, traceInfo, traceWarning } from '../../../common/logger'; + +import { + IPythonDaemonExecutionService, + IPythonExecutionFactory, + ObservableExecutionResult, + SpawnOptions +} from '../../../common/process/types'; +import { IOutputChannel, IPathUtils, Product } from '../../../common/types'; +import { DataScience } from '../../../common/utils/localize'; +import { noop } from '../../../common/utils/misc'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { JUPYTER_OUTPUT_CHANNEL, JupyterDaemonModule, Telemetry } from '../../constants'; +import { reportAction } from '../../progress/decorator'; +import { ReportableAction } from '../../progress/types'; +import { + IDataScienceFileSystem, + IJupyterInterpreterDependencyManager, + IJupyterSubCommandExecutionService +} from '../../types'; +import { JupyterServerInfo } from '../jupyterConnection'; +import { JupyterInstallError } from '../jupyterInstallError'; +import { JupyterKernelSpec, parseKernelSpecs } from '../kernels/jupyterKernelSpec'; +import { + getMessageForLibrariesNotInstalled, + JupyterInterpreterDependencyService +} from './jupyterInterpreterDependencyService'; +import { JupyterInterpreterService } from './jupyterInterpreterService'; + +/** + * Responsible for execution of jupyter sub commands using a single/global interpreter set aside for launching jupyter server. + * + * @export + * @class JupyterCommandFinderInterpreterExecutionService + * @implements {IJupyterSubCommandExecutionService} + */ +@injectable() +export class JupyterInterpreterSubCommandExecutionService + implements IJupyterSubCommandExecutionService, IJupyterInterpreterDependencyManager { + constructor( + @inject(JupyterInterpreterService) private readonly jupyterInterpreter: JupyterInterpreterService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(JupyterInterpreterDependencyService) + private readonly jupyterDependencyService: JupyterInterpreterDependencyService, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(IPythonExecutionFactory) private readonly pythonExecutionFactory: IPythonExecutionFactory, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private readonly jupyterOutputChannel: IOutputChannel, + @inject(IPathUtils) private readonly pathUtils: IPathUtils + ) {} + + /** + * This is a noop, implemented for backwards compatibility. + * + * @returns {Promise} + * @memberof JupyterInterpreterSubCommandExecutionService + */ + public async refreshCommands(): Promise { + noop(); + } + public async isNotebookSupported(token?: CancellationToken): Promise { + const interpreter = await this.jupyterInterpreter.getSelectedInterpreter(token); + if (!interpreter) { + return false; + } + return this.jupyterDependencyService.areDependenciesInstalled(interpreter, token); + } + public async isExportSupported(token?: CancellationToken): Promise { + const interpreter = await this.jupyterInterpreter.getSelectedInterpreter(token); + if (!interpreter) { + return false; + } + return this.jupyterDependencyService.isExportSupported(interpreter, token); + } + public async getReasonForJupyterNotebookNotBeingSupported(token?: CancellationToken): Promise { + let interpreter = await this.jupyterInterpreter.getSelectedInterpreter(token); + if (!interpreter) { + // Use current interpreter. + interpreter = await this.interpreterService.getActiveInterpreter(undefined); + if (!interpreter) { + // Unlikely scenario, user hasn't selected python, python extension will fall over. + // Get user to select something. + return DataScience.selectJupyterInterpreter(); + } + } + const productsNotInstalled = await this.jupyterDependencyService.getDependenciesNotInstalled( + interpreter, + token + ); + if (productsNotInstalled.length === 0) { + return ''; + } + + if (productsNotInstalled.length === 1 && productsNotInstalled[0] === Product.kernelspec) { + return DataScience.jupyterKernelSpecModuleNotFound().format(interpreter.path); + } + + return getMessageForLibrariesNotInstalled(productsNotInstalled, interpreter.displayName); + } + public async getSelectedInterpreter(token?: CancellationToken): Promise { + return this.jupyterInterpreter.getSelectedInterpreter(token); + } + public async startNotebook( + notebookArgs: string[], + options: SpawnOptions + ): Promise> { + const interpreter = await this.getSelectedInterpreterAndThrowIfNotAvailable(options.token); + this.jupyterOutputChannel.appendLine( + DataScience.startingJupyterLogMessage().format( + this.pathUtils.getDisplayName(interpreter.path), + notebookArgs.join(' ') + ) + ); + const executionService = await this.pythonExecutionFactory.createDaemon({ + daemonModule: JupyterDaemonModule, + pythonPath: interpreter.path + }); + return executionService.execModuleObservable('jupyter', ['notebook'].concat(notebookArgs), options); + } + + public async getRunningJupyterServers(token?: CancellationToken): Promise { + const interpreter = await this.getSelectedInterpreterAndThrowIfNotAvailable(token); + const daemon = await this.pythonExecutionFactory.createDaemon({ + daemonModule: JupyterDaemonModule, + pythonPath: interpreter.path + }); + + // We have a small python file here that we will execute to get the server info from all running Jupyter instances + const newOptions: SpawnOptions = { mergeStdOutErr: true, token: token }; + const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'vscode_datascience_helpers', 'getServerInfo.py'); + const serverInfoString = await daemon.exec([file], newOptions); + + let serverInfos: JupyterServerInfo[]; + try { + // Parse out our results, return undefined if we can't suss it out + serverInfos = JSON.parse(serverInfoString.stdout.trim()) as JupyterServerInfo[]; + } catch (err) { + traceWarning('Failed to parse JSON when getting server info out from getServerInfo.py', err); + return; + } + return serverInfos; + } + + @reportAction(ReportableAction.ExportNotebookToPython) + public async exportNotebookToPython(file: Uri, template?: string, token?: CancellationToken): Promise { + // Before we export check if our selected interpreter is available and supports export + let interpreter = await this.getSelectedInterpreter(token); + if (!interpreter || !(await this.jupyterDependencyService.isExportSupported(interpreter, token))) { + // If not available or not supported install missing dependecies + await this.installMissingDependencies(); + + // Install missing dependencies might change the selected interpreter, so check the new one + interpreter = await this.getSelectedInterpreterAndThrowIfNotAvailable(token); + + if (!(await this.jupyterDependencyService.isExportSupported(interpreter, token))) { + throw new Error(DataScience.jupyterNbConvertNotSupported()); + } + } + + const daemon = await this.pythonExecutionFactory.createDaemon({ + daemonModule: JupyterDaemonModule, + pythonPath: interpreter.path + }); + // Wait for the nbconvert to finish + const args = template + ? [file.fsPath, '--to', 'python', '--stdout', '--template', template] + : [file.fsPath, '--to', 'python', '--stdout']; + // Ignore stderr, as nbconvert writes conversion result to stderr. + // stdout contains the generated python code. + return daemon + .execModule('jupyter', ['nbconvert'].concat(args), { throwOnStdErr: false, encoding: 'utf8', token }) + .then((output) => output.stdout); + } + public async openNotebook(notebookFile: string): Promise { + const interpreter = await this.getSelectedInterpreterAndThrowIfNotAvailable(); + // Do not use the daemon for this, its a waste resources. The user will manage the lifecycle of this process. + const executionService = await this.pythonExecutionFactory.createActivatedEnvironment({ + interpreter, + bypassCondaExecution: true, + allowEnvironmentFetchExceptions: true + }); + const args: string[] = [`--NotebookApp.file_to_run=${notebookFile}`]; + + // Don't wait for the exec to finish and don't dispose. It's up to the user to kill the process + executionService + .execModule('jupyter', ['notebook'].concat(args), { throwOnStdErr: false, encoding: 'utf8' }) + .ignoreErrors(); + } + + public async getKernelSpecs(token?: CancellationToken): Promise { + const interpreter = await this.getSelectedInterpreterAndThrowIfNotAvailable(token); + const daemon = await this.pythonExecutionFactory.createDaemon({ + daemonModule: JupyterDaemonModule, + pythonPath: interpreter.path + }); + if (Cancellation.isCanceled(token)) { + return []; + } + try { + traceInfo('Asking for kernelspecs from jupyter'); + const spawnOptions = { throwOnStdErr: true, encoding: 'utf8' }; + // Ask for our current list. + const stdoutFromDaemonPromise = await daemon + .execModule('jupyter', ['kernelspec', 'list', '--json'], spawnOptions) + .then((output) => output.stdout) + .catch((daemonEx) => { + sendTelemetryEvent(Telemetry.KernelSpecNotFound); + traceError('Failed to list kernels from daemon', daemonEx); + return ''; + }); + // Possible we cannot import ipykernel for some reason. (use as backup option). + const stdoutFromFileExecPromise = daemon + .exec( + [ + path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'getJupyterKernels.py' + ) + ], + spawnOptions + ) + .then((output) => output.stdout) + .catch((fileEx) => { + traceError('Failed to list kernels from getJupyterKernels.py', fileEx); + return ''; + }); + + const [stdoutFromDaemon, stdoutFromFileExec] = await Promise.all([ + stdoutFromDaemonPromise, + stdoutFromFileExecPromise + ]); + + return parseKernelSpecs( + stdoutFromDaemon || stdoutFromFileExec, + this.fs, + this.pythonExecutionFactory, + token + ).catch((parserError) => { + traceError('Failed to parse kernelspecs', parserError); + // This is failing for some folks. In that case return nothing + return []; + }); + } catch (ex) { + traceError('Failed to list kernels', ex); + // This is failing for some folks. In that case return nothing + return []; + } + } + + public async installMissingDependencies(err?: JupyterInstallError): Promise { + await this.jupyterInterpreter.installMissingDependencies(err); + } + + private async getSelectedInterpreterAndThrowIfNotAvailable(token?: CancellationToken): Promise { + const interpreter = await this.jupyterInterpreter.getSelectedInterpreter(token); + if (!interpreter) { + const reason = await this.getReasonForJupyterNotebookNotBeingSupported(); + throw new JupyterInstallError(reason, DataScience.pythonInteractiveHelpLink()); + } + return interpreter; + } +} diff --git a/src/client/datascience/jupyter/invalidNotebookFileError.ts b/src/client/datascience/jupyter/invalidNotebookFileError.ts new file mode 100644 index 000000000000..eb8989a55409 --- /dev/null +++ b/src/client/datascience/jupyter/invalidNotebookFileError.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; +import * as localize from '../../common/utils/localize'; + +export class InvalidNotebookFileError extends Error { + constructor(file?: string) { + super( + file + ? localize.DataScience.invalidNotebookFileErrorFormat().format(file) + : localize.DataScience.invalidNotebookFileError() + ); + } +} diff --git a/src/client/datascience/jupyter/jupyterCellOutputMimeTypeTracker.ts b/src/client/datascience/jupyter/jupyterCellOutputMimeTypeTracker.ts new file mode 100644 index 000000000000..b595b232341f --- /dev/null +++ b/src/client/datascience/jupyter/jupyterCellOutputMimeTypeTracker.ts @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import type { nbformat } from '@jupyterlab/coreutils'; +import { sha256 } from 'hash.js'; +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { CellState, ICell, INotebookEditor, INotebookEditorProvider, INotebookExecutionLogger } from '../types'; +// tslint:disable-next-line:no-require-imports no-var-requires +const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); + +@injectable() +export class CellOutputMimeTypeTracker implements IExtensionSingleActivationService, INotebookExecutionLogger { + private pendingChecks = new Map(); + private sentMimeTypes: Set = new Set(); + + constructor(@inject(INotebookEditorProvider) private notebookEditorProvider: INotebookEditorProvider) { + this.notebookEditorProvider.onDidOpenNotebookEditor((t) => this.onOpenedOrClosedNotebook(t)); + } + + public dispose() { + this.pendingChecks.clear(); + } + + public onKernelRestarted() { + // Do nothing on restarted + } + public async preExecute(_cell: ICell, _silent: boolean): Promise { + // Do nothing on pre execute + } + public async postExecute(cell: ICell, silent: boolean): Promise { + if (!silent && cell.data.cell_type === 'code') { + this.scheduleCheck(this.createCellKey(cell), this.checkCell.bind(this, cell)); + } + } + public async activate(): Promise { + // Act like all of our open documents just opened; our timeout will make sure this is delayed. + this.notebookEditorProvider.editors.forEach((e) => this.onOpenedOrClosedNotebook(e)); + } + + private onOpenedOrClosedNotebook(e: INotebookEditor) { + if (e.file) { + this.scheduleCheck(e.file.fsPath, this.checkNotebook.bind(this, e)); + } + } + private getCellOutputMimeTypes(cell: ICell): string[] { + if (cell.data.cell_type === 'markdown') { + return ['markdown']; + } + if (cell.data.cell_type !== 'code') { + return []; + } + if (!Array.isArray(cell.data.outputs)) { + return []; + } + switch (cell.state) { + case CellState.editing: + case CellState.error: + case CellState.executing: + return []; + default: { + return flatten(cell.data.outputs.map(this.getOutputMimeTypes.bind(this))); + } + } + } + private getOutputMimeTypes(output: nbformat.IOutput): string[] { + // tslint:disable-next-line: no-any + const outputType: nbformat.OutputType = output.output_type as any; + switch (outputType) { + case 'error': + return []; + case 'stream': + return ['stream']; + case 'display_data': + case 'update_display_data': + case 'execute_result': + // tslint:disable-next-line: no-any + const data = (output as any).data; + return data ? Object.keys(data) : []; + default: + // If we have a large number of these, then something is wrong. + return ['unrecognized_cell_output']; + } + } + + private scheduleCheck(id: string, check: () => void) { + // If already scheduled, cancel. + const currentTimeout = this.pendingChecks.get(id); + if (currentTimeout) { + // tslint:disable-next-line: no-any + clearTimeout(currentTimeout as any); + this.pendingChecks.delete(id); + } + + // Now schedule a new one. + // Wait five seconds to make sure we don't already have this document pending. + this.pendingChecks.set(id, setTimeout(check, 5000)); + } + + private createCellKey(cell: ICell): string { + return `${cell.file}${cell.id}`; + } + + @captureTelemetry(Telemetry.HashedCellOutputMimeTypePerf) + private checkCell(cell: ICell) { + this.pendingChecks.delete(this.createCellKey(cell)); + this.getCellOutputMimeTypes(cell).forEach(this.sendTelemetry.bind(this)); + } + + @captureTelemetry(Telemetry.HashedNotebookCellOutputMimeTypePerf) + private checkNotebook(e: INotebookEditor) { + this.pendingChecks.delete(e.file.fsPath); + e.model?.cells.forEach(this.checkCell.bind(this)); + } + + private sendTelemetry(mimeType: string) { + // No need to send duplicate telemetry or waste CPU cycles on an unneeded hash. + if (this.sentMimeTypes.has(mimeType)) { + return; + } + this.sentMimeTypes.add(mimeType); + // Hash the package name so that we will never accidentally see a + // user's private package name. + const hashedName = sha256().update(mimeType).digest('hex'); + + const lowerMimeType = mimeType.toLowerCase(); + // The following gives us clues of the mimetype. + const props = { + hashedName, + hasText: lowerMimeType.includes('text'), + hasLatex: lowerMimeType.includes('latex'), + hasHtml: lowerMimeType.includes('html'), + hasSvg: lowerMimeType.includes('svg'), + hasXml: lowerMimeType.includes('xml'), + hasJson: lowerMimeType.includes('json'), + hasImage: lowerMimeType.includes('image'), + hasGeo: lowerMimeType.includes('geo'), + hasPlotly: lowerMimeType.includes('plotly'), + hasVega: lowerMimeType.includes('vega'), + hasWidget: lowerMimeType.includes('widget'), + hasJupyter: lowerMimeType.includes('jupyter'), + hasVnd: lowerMimeType.includes('vnd') + }; + sendTelemetryEvent(Telemetry.HashedCellOutputMimeType, undefined, props); + } +} diff --git a/src/client/datascience/jupyter/jupyterConnectError.ts b/src/client/datascience/jupyter/jupyterConnectError.ts new file mode 100644 index 000000000000..9cd33eae3a3b --- /dev/null +++ b/src/client/datascience/jupyter/jupyterConnectError.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +export class JupyterConnectError extends Error { + constructor(message: string, stderr?: string) { + super(message + (stderr ? `\n${stderr}` : '')); + } +} diff --git a/src/client/datascience/jupyter/jupyterConnection.ts b/src/client/datascience/jupyter/jupyterConnection.ts new file mode 100644 index 000000000000..691ca2f9462d --- /dev/null +++ b/src/client/datascience/jupyter/jupyterConnection.ts @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { ChildProcess } from 'child_process'; +import { Subscription } from 'rxjs'; +import { CancellationToken, Disposable, Event, EventEmitter } from 'vscode'; +import { Cancellation, CancellationError } from '../../common/cancellation'; +import { traceInfo, traceWarning } from '../../common/logger'; + +import { ObservableExecutionResult, Output } from '../../common/process/types'; +import { IConfigurationService, IDisposable } from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import * as localize from '../../common/utils/localize'; +import { IServiceContainer } from '../../ioc/types'; +import { RegExpValues } from '../constants'; +import { IDataScienceFileSystem, IJupyterConnection } from '../types'; +import { JupyterConnectError } from './jupyterConnectError'; + +// tslint:disable-next-line:no-require-imports no-var-requires no-any +const namedRegexp = require('named-js-regexp'); +const urlMatcher = namedRegexp(RegExpValues.UrlPatternRegEx); + +export type JupyterServerInfo = { + base_url: string; + notebook_dir: string; + hostname: string; + password: boolean; + pid: number; + port: number; + secure: boolean; + token: string; + url: string; +}; + +export class JupyterConnectionWaiter implements IDisposable { + private startPromise: Deferred; + private launchTimeout: NodeJS.Timer | number; + private configService: IConfigurationService; + private fs: IDataScienceFileSystem; + private stderr: string[] = []; + private connectionDisposed = false; + private subscriptions: Subscription[] = []; + + constructor( + private readonly launchResult: ObservableExecutionResult, + private readonly notebookDir: string, + private readonly rootDir: string, + private readonly getServerInfo: (cancelToken?: CancellationToken) => Promise, + serviceContainer: IServiceContainer, + private cancelToken?: CancellationToken + ) { + this.configService = serviceContainer.get(IConfigurationService); + this.fs = serviceContainer.get(IDataScienceFileSystem); + + // Cancel our start promise if a cancellation occurs + if (cancelToken) { + cancelToken.onCancellationRequested(() => this.startPromise.reject(new CancellationError())); + } + + // Setup our start promise + this.startPromise = createDeferred(); + + // We want to reject our Jupyter connection after a specific timeout + const settings = this.configService.getSettings(undefined); + const jupyterLaunchTimeout = settings.datascience.jupyterLaunchTimeout; + + this.launchTimeout = setTimeout(() => { + this.launchTimedOut(); + }, jupyterLaunchTimeout); + + // Listen for crashes + let exitCode = '0'; + if (launchResult.proc) { + launchResult.proc.on('exit', (c) => (exitCode = c ? c.toString() : '0')); + } + let stderr = ''; + // Listen on stderr for its connection information + this.subscriptions.push( + launchResult.out.subscribe( + (output: Output) => { + if (output.source === 'stderr') { + stderr += output.out; + this.stderr.push(output.out); + this.extractConnectionInformation(stderr); + } else { + this.output(output.out); + } + }, + (e) => this.rejectStartPromise(e.message), + // If the process dies, we can't extract connection information. + () => this.rejectStartPromise(localize.DataScience.jupyterServerCrashed().format(exitCode)) + ) + ); + } + public dispose() { + // tslint:disable-next-line: no-any + clearTimeout(this.launchTimeout as any); + this.subscriptions.forEach((d) => d.unsubscribe()); + } + + public waitForConnection(): Promise { + return this.startPromise.promise; + } + + private createConnection(baseUrl: string, token: string, hostName: string, processDisposable: Disposable) { + // tslint:disable-next-line: no-use-before-declare + return new JupyterConnection(baseUrl, token, hostName, this.rootDir, processDisposable, this.launchResult.proc); + } + + // tslint:disable-next-line:no-any + private output(data: any) { + if (!this.connectionDisposed) { + traceInfo(data.toString('utf8')); + } + } + + // From a list of jupyter server infos try to find the matching jupyter that we launched + // tslint:disable-next-line:no-any + private getJupyterURL(serverInfos: JupyterServerInfo[] | undefined, data: any) { + if (serverInfos && serverInfos.length > 0 && !this.startPromise.completed) { + const matchInfo = serverInfos.find((info) => + this.fs.areLocalPathsSame(this.notebookDir, info.notebook_dir) + ); + if (matchInfo) { + const url = matchInfo.url; + const token = matchInfo.token; + const host = matchInfo.hostname; + this.resolveStartPromise(url, token, host); + } + } + // At this point we failed to get the server info or a matching server via the python code, so fall back to + // our URL parse + if (!this.startPromise.completed) { + this.getJupyterURLFromString(data); + } + } + + // tslint:disable-next-line:no-any + private getJupyterURLFromString(data: any) { + // tslint:disable-next-line:no-any + const urlMatch = urlMatcher.exec(data) as any; + const groups = urlMatch.groups() as RegExpValues.IUrlPatternGroupType; + if (urlMatch && !this.startPromise.completed && groups && (groups.LOCAL || groups.IP)) { + // Rebuild the URI from our group hits + const host = groups.LOCAL ? groups.LOCAL : groups.IP; + const uriString = `${groups.PREFIX}${host}${groups.REST}`; + + // URL is not being found for some reason. Pull it in forcefully + // tslint:disable-next-line:no-require-imports + const URL = require('url').URL; + let url: URL; + try { + url = new URL(uriString); + } catch (err) { + // Failed to parse the url either via server infos or the string + this.rejectStartPromise(localize.DataScience.jupyterLaunchNoURL()); + return; + } + + // Here we parsed the URL correctly + this.resolveStartPromise( + `${url.protocol}//${url.host}${url.pathname}`, + `${url.searchParams.get('token')}`, + url.hostname + ); + } + } + + // tslint:disable-next-line:no-any + private extractConnectionInformation = (data: any) => { + this.output(data); + + const httpMatch = RegExpValues.HttpPattern.exec(data); + + if (httpMatch && this.notebookDir && this.startPromise && !this.startPromise.completed && this.getServerInfo) { + // .then so that we can keep from pushing aync up to the subscribed observable function + this.getServerInfo(this.cancelToken) + .then((serverInfos) => this.getJupyterURL(serverInfos, data)) + .catch((ex) => traceWarning('Failed to get server info', ex)); + } + + // Sometimes jupyter will return a 403 error. Not sure why. We used + // to fail on this, but it looks like jupyter works with this error in place. + }; + + private launchTimedOut = () => { + if (!this.startPromise.completed) { + this.rejectStartPromise(localize.DataScience.jupyterLaunchTimedOut()); + } + }; + + private resolveStartPromise = (baseUrl: string, token: string, hostName: string) => { + // tslint:disable-next-line: no-any + clearTimeout(this.launchTimeout as any); + if (!this.startPromise.rejected) { + const connection = this.createConnection(baseUrl, token, hostName, this.launchResult); + const origDispose = connection.dispose.bind(connection); + connection.dispose = () => { + // Stop listening when we disconnect + this.connectionDisposed = true; + return origDispose(); + }; + this.startPromise.resolve(connection); + } + }; + + // tslint:disable-next-line:no-any + private rejectStartPromise = (message: string) => { + // tslint:disable-next-line: no-any + clearTimeout(this.launchTimeout as any); + if (!this.startPromise.resolved) { + this.startPromise.reject( + Cancellation.isCanceled(this.cancelToken) + ? new CancellationError() + : new JupyterConnectError(message, this.stderr.join('\n')) + ); + } + }; +} + +// Represents an active connection to a running jupyter notebook +class JupyterConnection implements IJupyterConnection { + public readonly localLaunch: boolean = true; + public readonly type = 'jupyter'; + public valid: boolean = true; + public localProcExitCode: number | undefined; + private eventEmitter: EventEmitter = new EventEmitter(); + constructor( + public readonly baseUrl: string, + public readonly token: string, + public readonly hostName: string, + public readonly rootDirectory: string, + private readonly disposable: Disposable, + childProc: ChildProcess | undefined + ) { + // If the local process exits, set our exit code and fire our event + if (childProc) { + childProc.on('exit', (c) => { + // Our code expects the exit code to be of type `number` or `undefined`. + const code = typeof c === 'number' ? c : 0; + this.valid = false; + this.localProcExitCode = code; + this.eventEmitter.fire(code); + }); + } + } + + public get displayName(): string { + return getJupyterConnectionDisplayName(this.token, this.baseUrl); + } + + public get disconnected(): Event { + return this.eventEmitter.event; + } + + public dispose() { + if (this.disposable) { + this.disposable.dispose(); + } + } +} + +export function getJupyterConnectionDisplayName(token: string, baseUrl: string): string { + const tokenString = token.length > 0 ? `?token=${token}` : ''; + return `${baseUrl}${tokenString}`; +} diff --git a/src/client/datascience/jupyter/jupyterDataRateLimitError.ts b/src/client/datascience/jupyter/jupyterDataRateLimitError.ts new file mode 100644 index 000000000000..7e08688651dd --- /dev/null +++ b/src/client/datascience/jupyter/jupyterDataRateLimitError.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as localize from '../../common/utils/localize'; + +export class JupyterDataRateLimitError extends Error { + constructor() { + super(localize.DataScience.jupyterDataRateExceeded()); + } +} diff --git a/src/client/datascience/jupyter/jupyterDebugger.ts b/src/client/datascience/jupyter/jupyterDebugger.ts new file mode 100644 index 000000000000..afa7df942137 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterDebugger.ts @@ -0,0 +1,554 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { nbformat } from '@jupyterlab/coreutils'; +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { DebugConfiguration, Disposable } from 'vscode'; +import * as vsls from 'vsls/vscode'; +import { concatMultilineString } from '../../../datascience-ui/common'; +import { ServerStatus } from '../../../datascience-ui/interactive-common/mainState'; +import { IApplicationShell } from '../../common/application/types'; +import { traceError, traceInfo, traceWarning } from '../../common/logger'; +import { IPlatformService } from '../../common/platform/types'; +import { IConfigurationService, Version } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { traceCellResults } from '../common'; +import { Identifiers, Telemetry } from '../constants'; +import { + CellState, + ICell, + ICellHashListener, + IFileHashes, + IJupyterConnection, + IJupyterDebugger, + IJupyterDebugService, + INotebook, + ISourceMapRequest +} from '../types'; +import { JupyterDebuggerNotInstalledError } from './jupyterDebuggerNotInstalledError'; +import { JupyterDebuggerRemoteNotSupported } from './jupyterDebuggerRemoteNotSupported'; +import { ILiveShareHasRole } from './liveshare/types'; + +const pythonShellCommand = `_sysexec = sys.executable\r\n_quoted_sysexec = '"' + _sysexec + '"'\r\n!{_quoted_sysexec}`; + +@injectable() +export class JupyterDebugger implements IJupyterDebugger, ICellHashListener { + private requiredDebugpyVersion: Version = { major: 1, minor: 0, patch: 0, build: [], prerelease: [], raw: '' }; + private configs: Map = new Map(); + private readonly debuggerPackage: string; + private readonly enableDebuggerCode: string; + private readonly waitForDebugClientCode: string; + private readonly tracingEnableCode: string; + private readonly tracingDisableCode: string; + private runningByLine: boolean = false; + constructor( + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IJupyterDebugService) + @named(Identifiers.MULTIPLEXING_DEBUGSERVICE) + private debugService: IJupyterDebugService, + @inject(IPlatformService) private platform: IPlatformService + ) { + this.debuggerPackage = 'debugpy'; + this.enableDebuggerCode = `import debugpy;debugpy.listen(('localhost', 0))`; + this.waitForDebugClientCode = `import debugpy;debugpy.wait_for_client()`; + this.tracingEnableCode = `from debugpy import trace_this_thread;trace_this_thread(True)`; + this.tracingDisableCode = `from debugpy import trace_this_thread;trace_this_thread(False)`; + } + + public get isRunningByLine(): boolean { + return this.debugService.activeDebugSession !== undefined && this.runningByLine; + } + + public startRunByLine(notebook: INotebook, cellHashFileName: string): Promise { + this.runningByLine = true; + traceInfo(`Running by line for ${cellHashFileName}`); + const config: Partial = { + justMyCode: false, + rules: [ + { + include: false, + path: '**/*' + }, + { + include: true, + path: cellHashFileName + } + ] + }; + return this.startDebugSession((c) => this.debugService.startRunByLine(c), notebook, config, true); + } + + public async startDebugging(notebook: INotebook): Promise { + const settings = this.configService.getSettings(notebook.resource); + return this.startDebugSession( + (c) => this.debugService.startDebugging(undefined, c), + notebook, + { + justMyCode: settings.datascience.debugJustMyCode + }, + false + ); + } + + public async stopDebugging(notebook: INotebook): Promise { + this.runningByLine = false; + const config = this.configs.get(notebook.identity.toString()); + if (config) { + traceInfo('stop debugging'); + + // Tell our debug service to shutdown if possible + this.debugService.stop(); + + // Disable tracing after we disconnect because we don't want to step through this + // code if the user was in step mode. + if (notebook.status !== ServerStatus.Dead && notebook.status !== ServerStatus.NotStarted) { + await this.executeSilently(notebook, this.tracingDisableCode); + } + } + } + + public onRestart(notebook: INotebook): void { + this.configs.delete(notebook.identity.toString()); + } + + public async hashesUpdated(hashes: IFileHashes[]): Promise { + // Make sure that we have an active debugging session at this point + if (this.debugService.activeDebugSession) { + await Promise.all( + hashes.map((fileHash) => { + return this.debugService.activeDebugSession!.customRequest( + 'setPydevdSourceMap', + this.buildSourceMap(fileHash) + ); + }) + ); + } + } + + private async startDebugSession( + startCommand: (config: DebugConfiguration) => Thenable, + notebook: INotebook, + extraConfig: Partial, + runByLine: boolean + ) { + traceInfo('start debugging'); + + // Try to connect to this notebook + const config = await this.connect(notebook, runByLine, extraConfig); + if (config) { + traceInfo('connected to notebook during debugging'); + + // First check if this is a live share session. Skip debugging attach on the guest + // tslint:disable-next-line: no-any + const hasRole = (notebook as any) as ILiveShareHasRole; + if (hasRole && hasRole.role && hasRole.role === vsls.Role.Guest) { + traceInfo('guest mode attach skipped'); + } else { + await startCommand(config); + + // Force the debugger to update its list of breakpoints. This is used + // to make sure the breakpoint list is up to date when we do code file hashes + this.debugService.removeBreakpoints([]); + } + + // Wait for attach before we turn on tracing and allow the code to run, if the IDE is already attached this is just a no-op + const importResults = await this.executeSilently(notebook, this.waitForDebugClientCode); + if (importResults.length === 0 || importResults[0].state === CellState.error) { + traceWarning(`${this.debuggerPackage} not found in path.`); + } else { + traceCellResults('import startup', importResults); + } + + // Then enable tracing + await this.executeSilently(notebook, this.tracingEnableCode); + } + } + + private async connect( + notebook: INotebook, + runByLine: boolean, + extraConfig: Partial + ): Promise { + // If we already have configuration, we're already attached, don't do it again. + const key = notebook.identity.toString(); + let result = this.configs.get(key); + if (result) { + return { + ...result, + ...extraConfig + }; + } + traceInfo('enable debugger attach'); + + // Append any specific debugger paths that we have + await this.appendDebuggerPaths(notebook); + + // Check the version of debugger that we have already installed + const debuggerVersion = await this.debuggerCheck(notebook); + const requiredVersion = this.requiredDebugpyVersion; + + // If we don't have debugger installed or the version is too old then we need to install it + if (!debuggerVersion || !this.debuggerMeetsRequirement(debuggerVersion, requiredVersion)) { + await this.promptToInstallDebugger(notebook, debuggerVersion, runByLine); + } + + // Connect local or remote based on what type of notebook we're talking to + result = { + type: 'python', + name: 'IPython', + request: 'attach', + ...extraConfig + }; + const connectionInfo = notebook.connection; + if (connectionInfo && !connectionInfo.localLaunch) { + const { host, port } = await this.connectToRemote(notebook, connectionInfo); + result.host = host; + result.port = port; + } else { + const { host, port } = await this.connectToLocal(notebook); + result.host = host; + result.port = port; + } + + if (result.port) { + this.configs.set(notebook.identity.toString(), result); + + // Sign up for any change to the kernel to delete this config. + const disposables: Disposable[] = []; + const clear = () => { + this.configs.delete(key); + disposables.forEach((d) => d.dispose()); + }; + disposables.push(notebook.onDisposed(clear)); + disposables.push(notebook.onKernelRestarted(clear)); + disposables.push(notebook.onKernelChanged(clear)); + } + + return result; + } + + /** + * Gets the path to debugger. + * Temporary hack to check if python >= 3.7 and if experiments is enabled, then use new debugger, else old. + * (temporary to hard-code and use these in here). + * The old debugger will soon go away into oblivion... + * @private + * @param {INotebook} _notebook + * @returns {Promise} + * @memberof JupyterDebugger + */ + private async getDebuggerPath(_notebook: INotebook): Promise { + // We are here so this is NOT python 3.7, return debugger without wheels + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python'); + } + private async calculateDebuggerPathList(notebook: INotebook): Promise { + const extraPaths: string[] = []; + + // Add the settings path first as it takes precedence over the ptvsd extension path + // tslint:disable-next-line:no-multiline-string + let settingsPath = this.configService.getSettings(notebook.resource).datascience.debugpyDistPath; + // Escape windows path chars so they end up in the source escaped + if (settingsPath) { + if (this.platform.isWindows) { + settingsPath = settingsPath.replace(/\\/g, '\\\\'); + } + + extraPaths.push(settingsPath); + } + + // For a local connection we also need will append on the path to the debugger + // installed locally by the extension + // Actually until this is resolved: https://github.com/microsoft/vscode-python/issues/7615, skip adding + // this path. + const connectionInfo = notebook.connection; + if (connectionInfo && connectionInfo.localLaunch) { + let localPath = await this.getDebuggerPath(notebook); + if (this.platform.isWindows) { + localPath = localPath.replace(/\\/g, '\\\\'); + } + extraPaths.push(localPath); + } + + if (extraPaths && extraPaths.length > 0) { + return extraPaths.reduce((totalPath, currentPath) => { + if (totalPath.length === 0) { + totalPath = `'${currentPath}'`; + } else { + totalPath = `${totalPath}, '${currentPath}'`; + } + + return totalPath; + }, ''); + } + + return undefined; + } + + // Append our local debugger path and debugger settings path to sys.path + private async appendDebuggerPaths(notebook: INotebook): Promise { + const debuggerPathList = await this.calculateDebuggerPathList(notebook); + + if (debuggerPathList && debuggerPathList.length > 0) { + const result = await this.executeSilently( + notebook, + `import sys\r\nsys.path.extend([${debuggerPathList}])\r\nsys.path` + ); + traceCellResults('Appending paths', result); + } + } + + private buildSourceMap(fileHash: IFileHashes): ISourceMapRequest { + const sourceMapRequest: ISourceMapRequest = { source: { path: fileHash.file }, pydevdSourceMaps: [] }; + + sourceMapRequest.pydevdSourceMaps = fileHash.hashes.map((cellHash) => { + return { + line: cellHash.line, + endLine: cellHash.endLine, + runtimeSource: { path: `` }, + runtimeLine: cellHash.runtimeLine + }; + }); + + return sourceMapRequest; + } + + private executeSilently(notebook: INotebook, code: string): Promise { + return notebook.execute(code, Identifiers.EmptyFileName, 0, uuid(), undefined, true); + } + + private async debuggerCheck(notebook: INotebook): Promise { + // We don't want to actually import the debugger to check version so run + // python instead. If we import an old version it's hard to get rid of on + // an 'upgrade needed' scenario + // tslint:disable-next-line:no-multiline-string + const debuggerPathList = await this.calculateDebuggerPathList(notebook); + + let code; + if (debuggerPathList) { + code = `import sys\r\n${pythonShellCommand} -c "import sys;sys.path.extend([${debuggerPathList}]);sys.path;import ${this.debuggerPackage};print(${this.debuggerPackage}.__version__)"`; + } else { + code = `import sys\r\n${pythonShellCommand} -c "import ${this.debuggerPackage};print(${this.debuggerPackage}.__version__)"`; + } + + const debuggerVersionResults = await this.executeSilently(notebook, code); + const purpose = 'parseDebugpyVersionInfo'; + return this.parseVersionInfo(debuggerVersionResults, purpose); + } + + private parseVersionInfo( + cells: ICell[], + purpose: 'parseDebugpyVersionInfo' | 'pythonVersionInfo' + ): Version | undefined { + if (cells.length < 1 || cells[0].state !== CellState.finished) { + traceCellResults(purpose, cells); + return undefined; + } + + const targetCell = cells[0]; + + const outputString = this.extractOutput(targetCell); + + if (outputString) { + // Pull out the version number, note that we can't use SemVer here as python packages don't follow it + const packageVersionRegex = /([0-9]+).([0-9]+).([0-9a-zA-Z]+)/; + const packageVersionMatch = packageVersionRegex.exec(outputString); + + if (packageVersionMatch) { + const major = parseInt(packageVersionMatch[1], 10); + const minor = parseInt(packageVersionMatch[2], 10); + const patch = parseInt(packageVersionMatch[3], 10); + return { + major, + minor, + patch, + build: [], + prerelease: [], + raw: `${major}.${minor}.${patch}` + }; + } + } + + traceCellResults(purpose, cells); + + return undefined; + } + + // Check to see if the we have the required version of debugger to support debugging + private debuggerMeetsRequirement(version: Version, required: Version): boolean { + return version.major > required.major || (version.major === required.major && version.minor >= required.minor); + } + + @captureTelemetry(Telemetry.DebugpyPromptToInstall) + private async promptToInstallDebugger( + notebook: INotebook, + oldVersion: Version | undefined, + runByLine: boolean + ): Promise { + const updateMessage = runByLine + ? localize.DataScience.jupyterDebuggerInstallUpdateRunByLine().format(this.debuggerPackage) + : localize.DataScience.jupyterDebuggerInstallUpdate().format(this.debuggerPackage); + const newMessage = runByLine + ? localize.DataScience.jupyterDebuggerInstallNewRunByLine().format(this.debuggerPackage) + : localize.DataScience.jupyterDebuggerInstallNew().format(this.debuggerPackage); + const promptMessage = oldVersion ? updateMessage : newMessage; + const result = await this.appShell.showInformationMessage( + promptMessage, + localize.DataScience.jupyterDebuggerInstallYes(), + localize.DataScience.jupyterDebuggerInstallNo() + ); + + if (result === localize.DataScience.jupyterDebuggerInstallYes()) { + await this.installDebugger(notebook); + } else { + // If they don't want to install, throw so we exit out of debugging + sendTelemetryEvent(Telemetry.DebugpyInstallCancelled); + throw new JupyterDebuggerNotInstalledError(this.debuggerPackage); + } + } + + private async installDebugger(notebook: INotebook): Promise { + // tslint:disable-next-line:no-multiline-string + const debuggerInstallResults = await this.executeSilently( + notebook, + `import sys\r\n${pythonShellCommand} -m pip install -U ${this.debuggerPackage}` + ); + traceInfo(`Installing ${this.debuggerPackage}`); + + if (debuggerInstallResults.length > 0) { + const installResultsString = this.extractOutput(debuggerInstallResults[0]); + + if (installResultsString && installResultsString.includes('Successfully installed')) { + sendTelemetryEvent(Telemetry.DebugpySuccessfullyInstalled); + traceInfo(`${this.debuggerPackage} successfully installed`); + return; + } + } + traceCellResults(`Installing ${this.debuggerPackage}`, debuggerInstallResults); + sendTelemetryEvent(Telemetry.DebugpyInstallFailed); + traceError(`Failed to install ${this.debuggerPackage}`); + // Failed to install debugger, throw to exit debugging + throw new JupyterDebuggerNotInstalledError(this.debuggerPackage); + } + + // Pull our connection info out from the cells returned by enable_attach + private parseConnectInfo(cells: ICell[]): { port: number; host: string } { + if (cells.length > 0) { + let enableAttachString = this.extractOutput(cells[0]); + if (enableAttachString) { + enableAttachString = enableAttachString.trimQuotes(); + + // Important: This regex matches the format of the string returned from enable_attach. When + // doing enable_attach remotely, make sure to print out a string in the format ('host', port) + const debugInfoRegEx = /\('(.*?)', ([0-9]*)\)/; + const debugInfoMatch = debugInfoRegEx.exec(enableAttachString); + if (debugInfoMatch) { + return { + port: parseInt(debugInfoMatch[2], 10), + host: debugInfoMatch[1] + }; + } + } + } + // if we cannot parse the connect information, throw so we exit out of debugging + if (cells[0]?.data) { + const outputs = cells[0].data.outputs as nbformat.IOutput[]; + if (outputs[0]) { + const error = outputs[0] as nbformat.IError; + throw new JupyterDebuggerNotInstalledError(this.debuggerPackage, error.ename); + } + } + throw new JupyterDebuggerNotInstalledError( + localize.DataScience.jupyterDebuggerOutputParseError().format(this.debuggerPackage) + ); + } + + private extractOutput(cell: ICell): string | undefined { + if (cell.state === CellState.error || cell.state === CellState.finished) { + const outputs = cell.data.outputs as nbformat.IOutput[]; + if (outputs.length > 0) { + const data = outputs[0].data; + if (data && data.hasOwnProperty('text/plain')) { + // tslint:disable-next-line:no-any + return (data as any)['text/plain']; + } + if (outputs[0].output_type === 'stream') { + const stream = outputs[0] as nbformat.IStream; + return concatMultilineString(stream.text, true); + } + } + } + return undefined; + } + + private async connectToLocal(notebook: INotebook): Promise<{ port: number; host: string }> { + const enableDebuggerResults = await this.executeSilently(notebook, this.enableDebuggerCode); + + // Save our connection info to this notebook + return this.parseConnectInfo(enableDebuggerResults); + } + + private async connectToRemote( + _notebook: INotebook, + _connectionInfo: IJupyterConnection + ): Promise<{ port: number; host: string }> { + // We actually need a token. This isn't supported at the moment + throw new JupyterDebuggerRemoteNotSupported(); + + // let portNumber = this.configService.getSettings().datascience.remoteDebuggerPort; + // if (!portNumber) { + // portNumber = -1; + // } + + // // Loop through a bunch of ports until we find one we can use. Note how we + // // are connecting to '0.0.0.0' here. That's the location as far as ptvsd is concerned. + // const attachCode = portNumber !== -1 ? + // `import ptvsd + // ptvsd.enable_attach(('0.0.0.0', ${portNumber})) + // print("('${connectionInfo.hostName}', ${portNumber})")` : + // // tslint:disable-next-line: no-multiline-string + // `import ptvsd + // port = ${Settings.RemoteDebuggerPortBegin} + // attached = False + // while not attached and port <= ${Settings.RemoteDebuggerPortEnd}: + // try: + // ptvsd.enable_attach(('0.0.0.0', port)) + // print("('${connectionInfo.hostName}', " + str(port) + ")") + // attached = True + // except Exception as e: + // print("Exception: " + str(e)) + // port +=1`; + // const enableDebuggerResults = await this.executeSilently(server, attachCode); + + // // Save our connection info to this server + // const result = this.parseConnectInfo(enableDebuggerResults, false); + + // // If that didn't work, throw an error so somebody can open the port + // if (!result) { + // throw new JupyterDebuggerPortNotAvailableError(portNumber, Settings.RemoteDebuggerPortBegin, Settings.RemoteDebuggerPortEnd); + // } + + // // Double check, open a socket? This won't work if we're remote ourselves. Actually the debug adapter runs + // // from the remote machine. + // try { + // const deferred = createDeferred(); + // const socket = net.createConnection(result.port, result.host, () => { + // deferred.resolve(); + // }); + // socket.on('error', (err) => deferred.reject(err)); + // socket.setTimeout(2000, () => deferred.reject(new Error('Timeout trying to ping remote debugger'))); + // await deferred.promise; + // socket.end(); + // } catch (exc) { + // traceWarning(`Cannot connect to remote debugger at ${result.host}:${result.port} => ${exc}`); + // // We can't connect. Must be a firewall issue + // throw new JupyterDebuggerPortBlockedError(portNumber, Settings.RemoteDebuggerPortBegin, Settings.RemoteDebuggerPortEnd); + // } + + // return result; + } +} diff --git a/src/client/datascience/jupyter/jupyterDebuggerNotInstalledError.ts b/src/client/datascience/jupyter/jupyterDebuggerNotInstalledError.ts new file mode 100644 index 000000000000..755d5f208ee1 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterDebuggerNotInstalledError.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; +import * as localize from '../../common/utils/localize'; + +export class JupyterDebuggerNotInstalledError extends Error { + constructor(debuggerPkg: string, message?: string) { + const errorMessage = message + ? message + : localize.DataScience.jupyterDebuggerNotInstalledError().format(debuggerPkg); + super(errorMessage); + } +} diff --git a/src/client/datascience/jupyter/jupyterDebuggerPortBlockedError.ts b/src/client/datascience/jupyter/jupyterDebuggerPortBlockedError.ts new file mode 100644 index 000000000000..8ff97f941dda --- /dev/null +++ b/src/client/datascience/jupyter/jupyterDebuggerPortBlockedError.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; +import * as localize from '../../common/utils/localize'; + +export class JupyterDebuggerPortBlockedError extends Error { + constructor(portNumber: number, rangeBegin: number, rangeEnd: number) { + super( + portNumber === -1 + ? localize.DataScience.jupyterDebuggerPortBlockedSearchError().format( + rangeBegin.toString(), + rangeEnd.toString() + ) + : localize.DataScience.jupyterDebuggerPortBlockedError().format(portNumber.toString()) + ); + } +} diff --git a/src/client/datascience/jupyter/jupyterDebuggerPortNotAvailableError.ts b/src/client/datascience/jupyter/jupyterDebuggerPortNotAvailableError.ts new file mode 100644 index 000000000000..1a33e9eb00ec --- /dev/null +++ b/src/client/datascience/jupyter/jupyterDebuggerPortNotAvailableError.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; +import * as localize from '../../common/utils/localize'; + +export class JupyterDebuggerPortNotAvailableError extends Error { + constructor(portNumber: number, rangeBegin: number, rangeEnd: number) { + super( + portNumber === -1 + ? localize.DataScience.jupyterDebuggerPortNotAvailableSearchError().format( + rangeBegin.toString(), + rangeEnd.toString() + ) + : localize.DataScience.jupyterDebuggerPortNotAvailableError().format(portNumber.toString()) + ); + } +} diff --git a/src/client/datascience/jupyter/jupyterDebuggerRemoteNotSupported.ts b/src/client/datascience/jupyter/jupyterDebuggerRemoteNotSupported.ts new file mode 100644 index 000000000000..03c09ef8d1dd --- /dev/null +++ b/src/client/datascience/jupyter/jupyterDebuggerRemoteNotSupported.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; +import * as localize from '../../common/utils/localize'; + +export class JupyterDebuggerRemoteNotSupported extends Error { + constructor() { + super(localize.DataScience.remoteDebuggerNotSupported()); + } +} diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts new file mode 100644 index 000000000000..476685dff607 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -0,0 +1,453 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { CancellationToken, CancellationTokenSource, Event, EventEmitter, Uri } from 'vscode'; + +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../common/application/types'; +import { Cancellation } from '../../common/cancellation'; +import { WrappedError } from '../../common/errors/errorUtils'; +import { traceError, traceInfo } from '../../common/logger'; +import { IConfigurationService, IDisposableRegistry, IOutputChannel } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { JupyterSessionStartError } from '../baseJupyterSession'; +import { Commands, Identifiers, Telemetry } from '../constants'; +import { reportAction } from '../progress/decorator'; +import { ReportableAction } from '../progress/types'; +import { + IJupyterConnection, + IJupyterExecution, + IJupyterServerUri, + IJupyterSessionManagerFactory, + IJupyterSubCommandExecutionService, + IJupyterUriProviderRegistration, + INotebookServer, + INotebookServerLaunchInfo, + INotebookServerOptions, + JupyterServerUriHandle +} from '../types'; +import { JupyterSelfCertsError } from './jupyterSelfCertsError'; +import { createRemoteConnectionInfo, expandWorkingDir } from './jupyterUtils'; +import { JupyterWaitForIdleError } from './jupyterWaitForIdleError'; +import { getDisplayNameOrNameOfKernelConnection, kernelConnectionMetadataHasKernelSpec } from './kernels/helpers'; +import { KernelSelector } from './kernels/kernelSelector'; +import { KernelConnectionMetadata } from './kernels/types'; +import { NotebookStarter } from './notebookStarter'; + +const LocalHosts = ['localhost', '127.0.0.1', '::1']; + +export class JupyterExecutionBase implements IJupyterExecution { + private usablePythonInterpreter: PythonEnvironment | undefined; + private startedEmitter: EventEmitter = new EventEmitter(); + private disposed: boolean = false; + private readonly jupyterInterpreterService: IJupyterSubCommandExecutionService; + private readonly jupyterPickerRegistration: IJupyterUriProviderRegistration; + private uriToJupyterServerUri = new Map(); + private pendingTimeouts: (NodeJS.Timeout | number)[] = []; + + constructor( + _liveShare: ILiveShareApi, + private readonly interpreterService: IInterpreterService, + private readonly disposableRegistry: IDisposableRegistry, + private readonly workspace: IWorkspaceService, + private readonly configuration: IConfigurationService, + private readonly kernelSelector: KernelSelector, + private readonly notebookStarter: NotebookStarter, + private readonly appShell: IApplicationShell, + private readonly jupyterOutputChannel: IOutputChannel, + private readonly serviceContainer: IServiceContainer + ) { + this.jupyterInterpreterService = serviceContainer.get( + IJupyterSubCommandExecutionService + ); + this.jupyterPickerRegistration = serviceContainer.get( + IJupyterUriProviderRegistration + ); + this.disposableRegistry.push(this.interpreterService.onDidChangeInterpreter(() => this.onSettingsChanged())); + this.disposableRegistry.push(this); + + if (workspace) { + const disposable = workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('python.dataScience', undefined)) { + // When config changes happen, recreate our commands. + this.onSettingsChanged(); + } + if (e.affectsConfiguration('python.dataScience.jupyterServerURI', undefined)) { + // When server URI changes, clear our pending URI timeouts + this.clearTimeouts(); + } + }); + this.disposableRegistry.push(disposable); + } + } + + public get serverStarted(): Event { + return this.startedEmitter.event; + } + + public dispose(): Promise { + this.disposed = true; + this.clearTimeouts(); + return Promise.resolve(); + } + + public async refreshCommands(): Promise { + await this.jupyterInterpreterService.refreshCommands(); + } + + public isNotebookSupported(cancelToken?: CancellationToken): Promise { + // See if we can find the command notebook + return this.jupyterInterpreterService.isNotebookSupported(cancelToken); + } + + public async getNotebookError(): Promise { + return this.jupyterInterpreterService.getReasonForJupyterNotebookNotBeingSupported(); + } + + public async getUsableJupyterPython(cancelToken?: CancellationToken): Promise { + // Only try to compute this once. + if (!this.usablePythonInterpreter && !this.disposed) { + this.usablePythonInterpreter = await Cancellation.race( + () => this.jupyterInterpreterService.getSelectedInterpreter(cancelToken), + cancelToken + ); + } + return this.usablePythonInterpreter; + } + + @reportAction(ReportableAction.CheckingIfImportIsSupported) + public async isImportSupported(cancelToken?: CancellationToken): Promise { + // See if we can find the command nbconvert + return this.jupyterInterpreterService.isExportSupported(cancelToken); + } + + public isSpawnSupported(cancelToken?: CancellationToken): Promise { + // Supported if we can run a notebook + return this.isNotebookSupported(cancelToken); + } + + //tslint:disable:cyclomatic-complexity max-func-body-length + public connectToNotebookServer( + options?: INotebookServerOptions, + cancelToken?: CancellationToken + ): Promise { + // Return nothing if we cancel + // tslint:disable-next-line: max-func-body-length + return Cancellation.race(async () => { + let result: INotebookServer | undefined; + let connection: IJupyterConnection | undefined; + let kernelConnectionMetadata: KernelConnectionMetadata | undefined; + let kernelConnectionMetadataPromise: Promise = Promise.resolve< + KernelConnectionMetadata | undefined + >(undefined); + traceInfo(`Connecting to ${options ? options.purpose : 'unknown type of'} server`); + const allowUI = !options || options.allowUI(); + const kernelSpecCancelSource = new CancellationTokenSource(); + if (cancelToken) { + cancelToken.onCancellationRequested(() => { + kernelSpecCancelSource.cancel(); + }); + } + const isLocalConnection = !options || !options.uri; + + if (isLocalConnection) { + // Get hold of the kernelspec and corresponding (matching) interpreter that'll be used as the spec. + // We can do this in parallel, while starting the server (faster). + traceInfo(`Getting kernel specs for ${options ? options.purpose : 'unknown type of'} server`); + kernelConnectionMetadataPromise = this.kernelSelector.getPreferredKernelForLocalConnection( + undefined, + 'jupyter', + undefined, + options?.metadata, + !allowUI, + kernelSpecCancelSource.token + ); + } + + // Try to connect to our jupyter process. Check our setting for the number of tries + let tryCount = 1; + const maxTries = this.configuration.getSettings(undefined).datascience.jupyterLaunchRetries; + const stopWatch = new StopWatch(); + while (tryCount <= maxTries && !this.disposed) { + try { + // Start or connect to the process + [connection, kernelConnectionMetadata] = await Promise.all([ + this.startOrConnect(options, cancelToken), + kernelConnectionMetadataPromise + ]); + + if (!connection.localLaunch && LocalHosts.includes(connection.hostName.toLowerCase())) { + sendTelemetryEvent(Telemetry.ConnectRemoteJupyterViaLocalHost); + } + // Create a server tha t we will then attempt to connect to. + result = this.serviceContainer.get(INotebookServer); + + // In a remote non quest situation, figure out a kernel spec too. + if ( + (!kernelConnectionMetadata || + !kernelConnectionMetadataHasKernelSpec(kernelConnectionMetadata)) && + connection && + !options?.skipSearchingForKernel + ) { + const sessionManagerFactory = this.serviceContainer.get( + IJupyterSessionManagerFactory + ); + const sessionManager = await sessionManagerFactory.create(connection); + kernelConnectionMetadata = await this.kernelSelector.getPreferredKernelForRemoteConnection( + undefined, + sessionManager, + options?.metadata, + cancelToken + ); + await sessionManager.dispose(); + } + + // Populate the launch info that we are starting our server with + const launchInfo: INotebookServerLaunchInfo = { + connectionInfo: connection!, + kernelConnectionMetadata, + workingDir: options ? options.workingDir : undefined, + uri: options ? options.uri : undefined, + purpose: options ? options.purpose : uuid() + }; + + // tslint:disable-next-line: no-constant-condition + while (true) { + try { + traceInfo( + `Connecting to process for ${options ? options.purpose : 'unknown type of'} server` + ); + await result.connect(launchInfo, cancelToken); + traceInfo( + `Connection complete for ${options ? options.purpose : 'unknown type of'} server` + ); + break; + } catch (ex) { + traceError('Failed to connect to server', ex); + if (ex instanceof JupyterSessionStartError && isLocalConnection && allowUI) { + // Keep retrying, until it works or user cancels. + // Sometimes if a bad kernel is selected, starting a session can fail. + // In such cases we need to let the user know about this and prompt them to select another kernel. + const message = localize.DataScience.sessionStartFailedWithKernel().format( + getDisplayNameOrNameOfKernelConnection(launchInfo.kernelConnectionMetadata), + Commands.ViewJupyterOutput + ); + const selectKernel = localize.DataScience.selectDifferentKernel(); + const cancel = localize.Common.cancel(); + const selection = await this.appShell.showErrorMessage(message, selectKernel, cancel); + if (selection === selectKernel) { + const sessionManagerFactory = this.serviceContainer.get< + IJupyterSessionManagerFactory + >(IJupyterSessionManagerFactory); + const sessionManager = await sessionManagerFactory.create(connection); + const kernelInterpreter = await this.kernelSelector.selectLocalKernel( + undefined, + 'jupyter', + new StopWatch(), + sessionManager, + cancelToken, + getDisplayNameOrNameOfKernelConnection(launchInfo.kernelConnectionMetadata) + ); + if (kernelInterpreter) { + launchInfo.kernelConnectionMetadata = kernelInterpreter; + continue; + } + } + } + throw ex; + } + } + + sendTelemetryEvent( + isLocalConnection ? Telemetry.ConnectLocalJupyter : Telemetry.ConnectRemoteJupyter + ); + return result; + } catch (err) { + // Cleanup after ourselves. server may be running partially. + if (result) { + traceInfo(`Killing server because of error ${err}`); + await result.dispose(); + } + if (err instanceof JupyterWaitForIdleError && tryCount < maxTries) { + // Special case. This sometimes happens where jupyter doesn't ever connect. Cleanup after + // ourselves and propagate the failure outwards. + traceInfo('Retry because of wait for idle problem.'); + sendTelemetryEvent(Telemetry.SessionIdleTimeout); + + // Close existing connection. + connection?.dispose(); + tryCount += 1; + } else if (connection) { + kernelSpecCancelSource.cancel(); + + // If this is occurring during shutdown, don't worry about it. + if (this.disposed) { + return undefined; + } + + // Something else went wrong + if (!isLocalConnection) { + sendTelemetryEvent(Telemetry.ConnectRemoteFailedJupyter); + + // Check for the self signed certs error specifically + if (err.message.indexOf('reason: self signed certificate') >= 0) { + sendTelemetryEvent(Telemetry.ConnectRemoteSelfCertFailedJupyter); + throw new JupyterSelfCertsError(connection.baseUrl); + } else { + throw new WrappedError( + localize.DataScience.jupyterNotebookRemoteConnectFailed().format( + connection.baseUrl, + err + ), + err + ); + } + } else { + sendTelemetryEvent(Telemetry.ConnectFailedJupyter); + throw new WrappedError( + localize.DataScience.jupyterNotebookConnectFailed().format(connection.baseUrl, err), + err + ); + } + } else { + kernelSpecCancelSource.cancel(); + throw err; + } + } + } + + // If we're here, then starting jupyter timeout. + // Kill any existing connections. + connection?.dispose(); + sendTelemetryEvent(Telemetry.JupyterStartTimeout, stopWatch.elapsedTime, { + timeout: stopWatch.elapsedTime + }); + if (allowUI) { + this.appShell + .showErrorMessage(localize.DataScience.jupyterStartTimedout(), localize.Common.openOutputPanel()) + .then((selection) => { + if (selection === localize.Common.openOutputPanel()) { + this.jupyterOutputChannel.show(); + } + }, noop); + } + }, cancelToken); + } + + public async spawnNotebook(file: string): Promise { + return this.jupyterInterpreterService.openNotebook(file); + } + + public async importNotebook(file: Uri, template: string | undefined): Promise { + return this.jupyterInterpreterService.exportNotebookToPython(file, template); + } + + public getServer(_options?: INotebookServerOptions): Promise { + // This is cached at the host or guest level + return Promise.resolve(undefined); + } + + private async startOrConnect( + options?: INotebookServerOptions, + cancelToken?: CancellationToken + ): Promise { + // If our uri is undefined or if it's set to local launch we need to launch a server locally + if (!options || !options.uri) { + // If that works, then attempt to start the server + traceInfo(`Launching ${options ? options.purpose : 'unknown type of'} server`); + const useDefaultConfig = !options || options.skipUsingDefaultConfig ? false : true; + + // Expand the working directory. Create a dummy launching file in the root path (so we expand correctly) + const workingDirectory = expandWorkingDir( + options?.workingDir, + this.workspace.rootPath ? path.join(this.workspace.rootPath, `${uuid()}.txt`) : undefined, + this.workspace + ); + + const connection = await this.startNotebookServer( + useDefaultConfig, + this.configuration.getSettings(undefined).datascience.jupyterCommandLineArguments, + workingDirectory, + cancelToken + ); + if (connection) { + return connection; + } else { + // Throw a cancellation error if we were canceled. + Cancellation.throwIfCanceled(cancelToken); + + // Otherwise we can't connect + throw new Error(localize.DataScience.jupyterNotebookFailure().format('')); + } + } else { + // Prepare our map of server URIs + await this.updateServerUri(options.uri); + + // If we have a URI spec up a connection info for it + return createRemoteConnectionInfo(options.uri, this.getServerUri.bind(this)); + } + } + + // tslint:disable-next-line: max-func-body-length + @captureTelemetry(Telemetry.StartJupyter) + private async startNotebookServer( + useDefaultConfig: boolean, + customCommandLine: string[], + workingDirectory: string, + cancelToken?: CancellationToken + ): Promise { + return this.notebookStarter.start(useDefaultConfig, customCommandLine, workingDirectory, cancelToken); + } + private onSettingsChanged() { + // Clear our usableJupyterInterpreter so that we recompute our values + this.usablePythonInterpreter = undefined; + } + + private extractJupyterServerHandleAndId(uri: string): { handle: JupyterServerUriHandle; id: string } | undefined { + const url: URL = new URL(uri); + + // Id has to be there too. + const id = url.searchParams.get(Identifiers.REMOTE_URI_ID_PARAM); + const uriHandle = url.searchParams.get(Identifiers.REMOTE_URI_HANDLE_PARAM); + return id && uriHandle ? { handle: uriHandle, id } : undefined; + } + + private clearTimeouts() { + // tslint:disable-next-line: no-any + this.pendingTimeouts.forEach((t) => clearTimeout(t as any)); + this.pendingTimeouts = []; + } + + private getServerUri(uri: string): IJupyterServerUri | undefined { + const idAndHandle = this.extractJupyterServerHandleAndId(uri); + if (idAndHandle) { + return this.uriToJupyterServerUri.get(uri); + } + } + + private async updateServerUri(uri: string): Promise { + const idAndHandle = this.extractJupyterServerHandleAndId(uri); + if (idAndHandle) { + const serverUri = await this.jupyterPickerRegistration.getJupyterServerUri( + idAndHandle.id, + idAndHandle.handle + ); + this.uriToJupyterServerUri.set(uri, serverUri); + // See if there's an expiration date + if (serverUri.expiration) { + const timeoutInMS = serverUri.expiration.getTime() - Date.now(); + // Week seems long enough (in case the expiration is ridiculous) + if (timeoutInMS > 0 && timeoutInMS < 604800000) { + this.pendingTimeouts.push(setTimeout(() => this.updateServerUri(uri).ignoreErrors(), timeoutInMS)); + } + } + } + } +} diff --git a/src/client/datascience/jupyter/jupyterExecutionFactory.ts b/src/client/datascience/jupyter/jupyterExecutionFactory.ts new file mode 100644 index 000000000000..9888b3147ab3 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterExecutionFactory.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable, named } from 'inversify'; +import { CancellationToken, Event, EventEmitter, Uri } from 'vscode'; + +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../common/application/types'; + +import { + IAsyncDisposable, + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel +} from '../../common/types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { JUPYTER_OUTPUT_CHANNEL } from '../constants'; +import { IDataScienceFileSystem, IJupyterExecution, INotebookServer, INotebookServerOptions } from '../types'; +import { KernelSelector } from './kernels/kernelSelector'; +import { GuestJupyterExecution } from './liveshare/guestJupyterExecution'; +import { HostJupyterExecution } from './liveshare/hostJupyterExecution'; +import { IRoleBasedObject, RoleBasedFactory } from './liveshare/roleBasedFactory'; +import { NotebookStarter } from './notebookStarter'; + +interface IJupyterExecutionInterface extends IRoleBasedObject, IJupyterExecution {} + +// tslint:disable:callable-types +type JupyterExecutionClassType = { + new ( + liveShare: ILiveShareApi, + interpreterService: IInterpreterService, + disposableRegistry: IDisposableRegistry, + asyncRegistry: IAsyncDisposableRegistry, + fileSystem: IDataScienceFileSystem, + workspace: IWorkspaceService, + configuration: IConfigurationService, + kernelSelector: KernelSelector, + notebookStarter: NotebookStarter, + appShell: IApplicationShell, + jupyterOutputChannel: IOutputChannel, + serviceContainer: IServiceContainer + ): IJupyterExecutionInterface; +}; +// tslint:enable:callable-types + +@injectable() +export class JupyterExecutionFactory implements IJupyterExecution, IAsyncDisposable { + private executionFactory: RoleBasedFactory; + private sessionChangedEventEmitter: EventEmitter = new EventEmitter(); + private serverStartedEventEmitter: EventEmitter = new EventEmitter< + INotebookServerOptions | undefined + >(); + + constructor( + @inject(ILiveShareApi) liveShare: ILiveShareApi, + @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, + @inject(IDataScienceFileSystem) fs: IDataScienceFileSystem, + @inject(IWorkspaceService) workspace: IWorkspaceService, + @inject(IConfigurationService) configuration: IConfigurationService, + @inject(KernelSelector) kernelSelector: KernelSelector, + @inject(NotebookStarter) notebookStarter: NotebookStarter, + @inject(IApplicationShell) appShell: IApplicationShell, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) jupyterOutputChannel: IOutputChannel, + @inject(IServiceContainer) serviceContainer: IServiceContainer + ) { + asyncRegistry.push(this); + this.executionFactory = new RoleBasedFactory( + liveShare, + HostJupyterExecution, + GuestJupyterExecution, + liveShare, + interpreterService, + disposableRegistry, + asyncRegistry, + fs, + workspace, + configuration, + kernelSelector, + notebookStarter, + appShell, + jupyterOutputChannel, + serviceContainer + ); + this.executionFactory.sessionChanged(() => this.onSessionChanged()); + } + + public get sessionChanged(): Event { + return this.sessionChangedEventEmitter.event; + } + + public get serverStarted(): Event { + return this.serverStartedEventEmitter.event; + } + + public async dispose(): Promise { + // Dispose of our execution object + const execution = await this.executionFactory.get(); + return execution.dispose(); + } + + public async refreshCommands(): Promise { + const execution = await this.executionFactory.get(); + return execution.refreshCommands(); + } + + public async isNotebookSupported(cancelToken?: CancellationToken): Promise { + const execution = await this.executionFactory.get(); + return execution.isNotebookSupported(cancelToken); + } + + public async getNotebookError(): Promise { + const execution = await this.executionFactory.get(); + return execution.getNotebookError(); + } + + public async isImportSupported(cancelToken?: CancellationToken): Promise { + const execution = await this.executionFactory.get(); + return execution.isImportSupported(cancelToken); + } + public async isSpawnSupported(cancelToken?: CancellationToken): Promise { + const execution = await this.executionFactory.get(); + return execution.isSpawnSupported(cancelToken); + } + public async connectToNotebookServer( + options?: INotebookServerOptions, + cancelToken?: CancellationToken + ): Promise { + const execution = await this.executionFactory.get(); + const server = await execution.connectToNotebookServer(options, cancelToken); + if (server) { + this.serverStartedEventEmitter.fire(options); + } + return server; + } + public async spawnNotebook(file: string): Promise { + const execution = await this.executionFactory.get(); + return execution.spawnNotebook(file); + } + public async importNotebook(file: Uri, template: string | undefined): Promise { + const execution = await this.executionFactory.get(); + return execution.importNotebook(file, template); + } + public async getUsableJupyterPython(cancelToken?: CancellationToken): Promise { + const execution = await this.executionFactory.get(); + return execution.getUsableJupyterPython(cancelToken); + } + public async getServer(options?: INotebookServerOptions): Promise { + const execution = await this.executionFactory.get(); + return execution.getServer(options); + } + + private onSessionChanged() { + this.sessionChangedEventEmitter.fire(); + } +} diff --git a/src/client/datascience/jupyter/jupyterExporter.ts b/src/client/datascience/jupyter/jupyterExporter.ts new file mode 100644 index 000000000000..23be4872b3d9 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterExporter.ts @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { nbformat } from '@jupyterlab/coreutils'; +import { inject, injectable } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; + +import { Uri } from 'vscode'; +import { concatMultilineString } from '../../../datascience-ui/common'; +import { createCodeCell } from '../../../datascience-ui/common/cellFactory'; +import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import { IPlatformService } from '../../common/platform/types'; +import { IConfigurationService } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { CellMatcher } from '../cellMatcher'; +import { CodeSnippets, Identifiers } from '../constants'; +import { + CellState, + ICell, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IJupyterExecution, + INotebookEditorProvider, + INotebookExporter, + ITrustService +} from '../types'; + +@injectable() +export class JupyterExporter implements INotebookExporter { + constructor( + @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IDataScienceFileSystem) private fileSystem: IDataScienceFileSystem, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(INotebookEditorProvider) protected ipynbProvider: INotebookEditorProvider, + @inject(IDataScienceErrorHandler) protected errorHandler: IDataScienceErrorHandler, + @inject(ITrustService) private readonly trustService: ITrustService + ) {} + + public dispose() { + noop(); + } + + public async exportToFile(cells: ICell[], file: string, showOpenPrompt: boolean = true): Promise { + let directoryChange; + const settings = this.configService.getSettings(); + if (settings.datascience.changeDirOnImportExport) { + directoryChange = file; + } + + const notebook = await this.translateToNotebook(cells, directoryChange); + + try { + // tslint:disable-next-line: no-any + const contents = JSON.stringify(notebook); + await this.trustService.trustNotebook(Uri.file(file), contents); + await this.fileSystem.writeFile(Uri.file(file), contents); + if (!showOpenPrompt) { + return; + } + const openQuestion1 = localize.DataScience.exportOpenQuestion1(); + const openQuestion2 = (await this.jupyterExecution.isSpawnSupported()) + ? localize.DataScience.exportOpenQuestion() + : undefined; + this.showInformationMessage( + localize.DataScience.exportDialogComplete().format(file), + openQuestion1, + openQuestion2 + ).then(async (str: string | undefined) => { + try { + if (str === openQuestion2 && openQuestion2) { + // If the user wants to, open the notebook they just generated. + await this.jupyterExecution.spawnNotebook(file); + } else if (str === openQuestion1) { + await this.ipynbProvider.open(Uri.file(file)); + } + } catch (e) { + await this.errorHandler.handleError(e); + } + }); + } catch (exc) { + traceError('Error in exporting notebook file'); + this.applicationShell.showInformationMessage(localize.DataScience.exportDialogFailed().format(exc)); + } + } + public async translateToNotebook( + cells: ICell[], + changeDirectory?: string, + kernelSpec?: nbformat.IKernelspecMetadata + ): Promise { + // If requested, add in a change directory cell to fix relative paths + if (changeDirectory && this.configService.getSettings().datascience.changeDirOnImportExport) { + cells = await this.addDirectoryChangeCell(cells, changeDirectory); + } + + const pythonNumber = await this.extractPythonMainVersion(); + + // Use this to build our metadata object + const metadata: nbformat.INotebookMetadata = { + language_info: { + codemirror_mode: { + name: 'ipython', + version: pythonNumber + }, + file_extension: '.py', + mimetype: 'text/x-python', + name: 'python', + nbconvert_exporter: 'python', + pygments_lexer: `ipython${pythonNumber}`, + version: pythonNumber + }, + orig_nbformat: 2, + // tslint:disable-next-line: no-any + kernelspec: kernelSpec as any + }; + + // Create an object for matching cell definitions + const matcher = new CellMatcher(this.configService.getSettings().datascience); + + // Combine this into a JSON object + return { + cells: this.pruneCells(cells, matcher), + nbformat: 4, + nbformat_minor: 2, + metadata: metadata + }; + } + + private showInformationMessage( + message: string, + question1: string, + question2?: string + ): Thenable { + if (question2) { + return this.applicationShell.showInformationMessage(message, question1, question2); + } else { + return this.applicationShell.showInformationMessage(message, question1); + } + } + + // For exporting, put in a cell that will change the working directory back to the workspace directory so relative data paths will load correctly + private addDirectoryChangeCell = async (cells: ICell[], file: string): Promise => { + const changeDirectory = await this.calculateDirectoryChange(file, cells); + + if (changeDirectory) { + const exportChangeDirectory = CodeSnippets.ChangeDirectory.join(os.EOL).format( + localize.DataScience.exportChangeDirectoryComment(), + CodeSnippets.ChangeDirectoryCommentIdentifier, + changeDirectory + ); + + const cell: ICell = { + data: createCodeCell(exportChangeDirectory), + id: uuid(), + file: Identifiers.EmptyFileName, + line: 0, + state: CellState.finished + }; + + return [cell, ...cells]; + } else { + return cells; + } + }; + + // When we export we want to our change directory back to the first real file that we saw run from any workspace folder + private firstWorkspaceFolder = async (cells: ICell[]): Promise => { + for (const cell of cells) { + const filename = cell.file; + + // First check that this is an absolute file that exists (we add in temp files to run system cell) + if (path.isAbsolute(filename) && (await this.fileSystem.localFileExists(filename))) { + // We've already check that workspace folders above + for (const folder of this.workspaceService.workspaceFolders!) { + if (filename.toLowerCase().startsWith(folder.uri.fsPath.toLowerCase())) { + return folder.uri.fsPath; + } + } + } + } + + return undefined; + }; + + private calculateDirectoryChange = async (notebookFile: string, cells: ICell[]): Promise => { + // Make sure we don't already have a cell with a ChangeDirectory comment in it. + let directoryChange: string | undefined; + const haveChangeAlready = cells.find((c) => + concatMultilineString(c.data.source).includes(CodeSnippets.ChangeDirectoryCommentIdentifier) + ); + if (!haveChangeAlready) { + const notebookFilePath = path.dirname(notebookFile); + // First see if we have a workspace open, this only works if we have a workspace root to be relative to + if (this.workspaceService.hasWorkspaceFolders) { + const workspacePath = await this.firstWorkspaceFolder(cells); + + // Make sure that we have everything that we need here + if ( + workspacePath && + path.isAbsolute(workspacePath) && + notebookFilePath && + path.isAbsolute(notebookFilePath) + ) { + directoryChange = path.relative(notebookFilePath, workspacePath); + } + } + } + + // If path.relative can't calculate a relative path, then it just returns the full second path + // so check here, we only want this if we were able to calculate a relative path, no network shares or drives + if (directoryChange && !path.isAbsolute(directoryChange)) { + // Escape windows path chars so they end up in the source escaped + if (this.platform.isWindows) { + directoryChange = directoryChange.replace('\\', '\\\\'); + } + + return directoryChange; + } else { + return undefined; + } + }; + + private pruneCells = (cells: ICell[], cellMatcher: CellMatcher): nbformat.IBaseCell[] => { + // First filter out sys info cells. Jupyter doesn't understand these + const filtered = cells.filter((c) => c.data.cell_type !== 'messages'); + + // Then prune each cell down to just the cell data. + return filtered.map((c) => this.pruneCell(c, cellMatcher)); + }; + + private pruneCell = (cell: ICell, cellMatcher: CellMatcher): nbformat.IBaseCell => { + // Remove the #%% of the top of the source if there is any. We don't need + // this to end up in the exported ipynb file. + const copy = { ...cell.data }; + copy.source = this.pruneSource(cell.data.source, cellMatcher); + return copy; + }; + + private pruneSource = (source: nbformat.MultilineString, cellMatcher: CellMatcher): nbformat.MultilineString => { + // Remove the comments on the top if there. + if (Array.isArray(source) && source.length > 0) { + if (cellMatcher.isCell(source[0])) { + return source.slice(1); + } + } else { + const array = source + .toString() + .split('\n') + .map((s) => `${s}\n`); + if (array.length > 0 && cellMatcher.isCell(array[0])) { + return array.slice(1); + } + } + + return source; + }; + + private extractPythonMainVersion = async (): Promise => { + // Use the active interpreter + const usableInterpreter = await this.jupyterExecution.getUsableJupyterPython(); + return usableInterpreter && usableInterpreter.version ? usableInterpreter.version.major : 3; + }; +} diff --git a/src/client/datascience/jupyter/jupyterImporter.ts b/src/client/datascience/jupyter/jupyterImporter.ts new file mode 100644 index 000000000000..cc0016992b83 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterImporter.ts @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; + +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import { IPlatformService } from '../../common/platform/types'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { CodeSnippets, Identifiers } from '../constants'; +import { + IDataScienceFileSystem, + IJupyterExecution, + IJupyterInterpreterDependencyManager, + INotebookImporter +} from '../types'; + +@injectable() +export class JupyterImporter implements INotebookImporter { + public isDisposed: boolean = false; + // Template that changes markdown cells to have # %% [markdown] in the comments + private readonly nbconvertTemplateFormat = + // tslint:disable-next-line:no-multiline-string + `{%- extends 'null.tpl' -%} +{% block codecell %} +{0} +{{ super() }} +{% endblock codecell %} +{% block in_prompt %}{% endblock in_prompt %} +{% block input %}{{ cell.source | ipython2python }}{% endblock input %} +{% block markdowncell scoped %}{0} [markdown] +{{ cell.source | comment_lines }} +{% endblock markdowncell %}`; + + private templatePromise: Promise; + + constructor( + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, + @inject(IConfigurationService) private configuration: IConfigurationService, + @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IJupyterInterpreterDependencyManager) + private readonly dependencyManager: IJupyterInterpreterDependencyManager + ) { + this.templatePromise = this.createTemplateFile(); + } + + public async importFromFile(sourceFile: Uri): Promise { + const template = await this.templatePromise; + + // If the user has requested it, add a cd command to the imported file so that relative paths still work + const settings = this.configuration.getSettings(); + let directoryChange: string | undefined; + if (settings.datascience.changeDirOnImportExport) { + directoryChange = await this.calculateDirectoryChange(sourceFile); + } + + // Before we try the import, see if we don't support it, if we don't give a chance to install dependencies + if (!(await this.jupyterExecution.isImportSupported())) { + await this.dependencyManager.installMissingDependencies(); + } + + // Use the jupyter nbconvert functionality to turn the notebook into a python file + if (await this.jupyterExecution.isImportSupported()) { + let fileOutput: string = await this.jupyterExecution.importNotebook(sourceFile, template); + if (fileOutput.includes('get_ipython()')) { + fileOutput = this.addIPythonImport(fileOutput); + } + if (directoryChange) { + fileOutput = this.addDirectoryChange(fileOutput, directoryChange); + } + return this.addInstructionComments(fileOutput); + } + + throw new Error(localize.DataScience.jupyterNbConvertNotSupported()); + } + + public dispose = () => { + this.isDisposed = true; + }; + + private addInstructionComments = (pythonOutput: string): string => { + const comments = localize.DataScience.instructionComments().format(this.defaultCellMarker); + return comments.concat(pythonOutput); + }; + + private get defaultCellMarker(): string { + return this.configuration.getSettings().datascience.defaultCellMarker || Identifiers.DefaultCodeCellMarker; + } + + private addIPythonImport = (pythonOutput: string): string => { + return CodeSnippets.ImportIPython.format(this.defaultCellMarker, pythonOutput); + }; + + private addDirectoryChange = (pythonOutput: string, directoryChange: string): string => { + const newCode = CodeSnippets.ChangeDirectory.join(os.EOL).format( + localize.DataScience.importChangeDirectoryComment().format(this.defaultCellMarker), + CodeSnippets.ChangeDirectoryCommentIdentifier, + directoryChange + ); + return newCode.concat(pythonOutput); + }; + + // When importing a file, calculate if we can create a %cd so that the relative paths work + private async calculateDirectoryChange(notebookFile: Uri): Promise { + let directoryChange: string | undefined; + try { + // Make sure we don't already have an import/export comment in the file + const contents = await this.fs.readFile(notebookFile); + const haveChangeAlready = contents.includes(CodeSnippets.ChangeDirectoryCommentIdentifier); + + if (!haveChangeAlready) { + const notebookFilePath = path.dirname(notebookFile.fsPath); + // First see if we have a workspace open, this only works if we have a workspace root to be relative to + if (this.workspaceService.hasWorkspaceFolders) { + const workspacePath = this.workspaceService.workspaceFolders![0].uri.fsPath; + + // Make sure that we have everything that we need here + if ( + workspacePath && + path.isAbsolute(workspacePath) && + notebookFilePath && + path.isAbsolute(notebookFilePath) + ) { + directoryChange = path.relative(workspacePath, notebookFilePath); + } + } + } + + // If path.relative can't calculate a relative path, then it just returns the full second path + // so check here, we only want this if we were able to calculate a relative path, no network shares or drives + if (directoryChange && !path.isAbsolute(directoryChange)) { + // Escape windows path chars so they end up in the source escaped + if (this.platform.isWindows) { + directoryChange = directoryChange.replace('\\', '\\\\'); + } + + return directoryChange; + } else { + return undefined; + } + } catch (e) { + traceError(e); + } + } + + private async createTemplateFile(): Promise { + // Create a temp file on disk + const file = await this.fs.createTemporaryLocalFile('.tpl'); + + // Write our template into it + if (file) { + try { + // Save this file into our disposables so the temp file goes away + this.disposableRegistry.push(file); + await this.fs.appendLocalFile( + file.filePath, + this.nbconvertTemplateFormat.format(this.defaultCellMarker) + ); + + // Now we should have a template that will convert + return file.filePath; + } catch { + noop(); + } + } + } +} diff --git a/src/client/datascience/jupyter/jupyterInstallError.ts b/src/client/datascience/jupyter/jupyterInstallError.ts new file mode 100644 index 000000000000..6af845382351 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterInstallError.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; +import { HelpLinks } from '../constants'; + +export class JupyterInstallError extends Error { + public action: string; + public actionTitle: string; + + constructor(message: string, actionFormatString: string) { + super(message); + this.action = HelpLinks.PythonInteractiveHelpLink; + this.actionTitle = actionFormatString.format(HelpLinks.PythonInteractiveHelpLink); + } +} diff --git a/src/client/datascience/jupyter/jupyterInterruptError.ts b/src/client/datascience/jupyter/jupyterInterruptError.ts new file mode 100644 index 000000000000..172899250d3d --- /dev/null +++ b/src/client/datascience/jupyter/jupyterInterruptError.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export class JupyterInterruptError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/src/client/datascience/jupyter/jupyterInvalidKernelError.ts b/src/client/datascience/jupyter/jupyterInvalidKernelError.ts new file mode 100644 index 000000000000..1dc6735a8e90 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterInvalidKernelError.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as localize from '../../common/utils/localize'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { getDisplayNameOrNameOfKernelConnection } from './kernels/helpers'; +import { KernelConnectionMetadata } from './kernels/types'; + +export class JupyterInvalidKernelError extends Error { + constructor(public readonly kernelConnectionMetadata: KernelConnectionMetadata | undefined) { + super( + localize.DataScience.kernelInvalid().format( + getDisplayNameOrNameOfKernelConnection(kernelConnectionMetadata) + ) + ); + sendTelemetryEvent(Telemetry.KernelInvalid); + } +} diff --git a/src/client/datascience/jupyter/jupyterNotebook.ts b/src/client/datascience/jupyter/jupyterNotebook.ts new file mode 100644 index 000000000000..de8886658b00 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterNotebook.ts @@ -0,0 +1,1424 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import type { nbformat } from '@jupyterlab/coreutils'; +import type { Kernel, KernelMessage } from '@jupyterlab/services'; +import type { JSONObject } from '@phosphor/coreutils'; +import { Observable } from 'rxjs/Observable'; +import { Subscriber } from 'rxjs/Subscriber'; +import * as uuid from 'uuid/v4'; +import { Disposable, Event, EventEmitter, Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { ServerStatus } from '../../../datascience-ui/interactive-common/mainState'; +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../common/application/types'; +import { CancellationError, createPromiseFromCancellation } from '../../common/cancellation'; +import '../../common/extensions'; +import { traceError, traceInfo, traceWarning } from '../../common/logger'; + +import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; +import { createDeferred, Deferred, waitForPromise } from '../../common/utils/async'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { generateCells } from '../cellFactory'; +import { CellMatcher } from '../cellMatcher'; +import { CodeSnippets, Identifiers, Telemetry } from '../constants'; +import { + CellState, + ICell, + IDataScienceFileSystem, + IJupyterSession, + INotebook, + INotebookCompletion, + INotebookExecutionInfo, + INotebookExecutionLogger, + InterruptResult, + KernelSocketInformation +} from '../types'; +import { expandWorkingDir } from './jupyterUtils'; +import { KernelConnectionMetadata } from './kernels/types'; + +// tslint:disable-next-line: no-require-imports +import cloneDeep = require('lodash/cloneDeep'); +import { concatMultilineString, formatStreamText } from '../../../datascience-ui/common'; +import { RefBool } from '../../common/refBool'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { getInterpreterFromKernelConnectionMetadata, isPythonKernelConnection } from './kernels/helpers'; + +class CellSubscriber { + public get startTime(): number { + return this._startTime; + } + + public get onCanceled(): Event { + return this.canceledEvent.event; + } + + public get promise(): Promise { + return this.deferred.promise; + } + + public get cell(): ICell { + return this.cellRef; + } + public executionState?: Kernel.Status; + private deferred: Deferred = createDeferred(); + private cellRef: ICell; + private subscriber: Subscriber; + private promiseComplete: (self: CellSubscriber) => void; + private canceledEvent: EventEmitter = new EventEmitter(); + private _startTime: number; + + constructor(cell: ICell, subscriber: Subscriber, promiseComplete: (self: CellSubscriber) => void) { + this.cellRef = cell; + this.subscriber = subscriber; + this.promiseComplete = promiseComplete; + this._startTime = Date.now(); + } + + public isValid(sessionStartTime: number | undefined) { + return sessionStartTime && this.startTime >= sessionStartTime; + } + + public next(sessionStartTime: number | undefined) { + // Tell the subscriber first + if (this.isValid(sessionStartTime)) { + this.subscriber.next(this.cellRef); + } + } + + // tslint:disable-next-line:no-any + public error(sessionStartTime: number | undefined, err: any) { + if (this.isValid(sessionStartTime)) { + this.subscriber.error(err); + } + } + + public complete(sessionStartTime: number | undefined) { + if (this.isValid(sessionStartTime)) { + if (this.cellRef.state !== CellState.error) { + this.cellRef.state = CellState.finished; + } + this.subscriber.next(this.cellRef); + } + this.subscriber.complete(); + + // Then see if we're finished or not. + this.attemptToFinish(); + } + + // tslint:disable-next-line:no-any + public reject(e: any) { + if (!this.deferred.completed) { + this.cellRef.state = CellState.error; + this.subscriber.next(this.cellRef); + this.subscriber.complete(); + this.deferred.reject(e); + this.promiseComplete(this); + } + } + + public cancel() { + this.canceledEvent.fire(); + if (!this.deferred.completed) { + this.cellRef.state = CellState.error; + this.subscriber.next(this.cellRef); + this.subscriber.complete(); + this.deferred.resolve(); + this.promiseComplete(this); + } + } + + private attemptToFinish() { + if ( + !this.deferred.completed && + (this.cell.state === CellState.finished || this.cell.state === CellState.error) + ) { + this.deferred.resolve(this.cell.state); + this.promiseComplete(this); + } + } +} + +// This code is based on the examples here: +// https://www.npmjs.com/package/@jupyterlab/services + +export class JupyterNotebookBase implements INotebook { + private sessionStartTime: number; + private pendingCellSubscriptions: CellSubscriber[] = []; + private ranInitialSetup = false; + private _resource: Resource; + private _identity: Uri; + private _disposed: boolean = false; + private _workingDirectory: string | undefined; + private _executionInfo: INotebookExecutionInfo; + private onStatusChangedEvent: EventEmitter | undefined; + public get onDisposed(): Event { + return this.disposedEvent.event; + } + public get onKernelChanged(): Event { + return this.kernelChanged.event; + } + public get disposed() { + return this._disposed; + } + private kernelChanged = new EventEmitter(); + public get onKernelRestarted(): Event { + return this.kernelRestarted.event; + } + public get onKernelInterrupted(): Event { + return this.kernelInterrupted.event; + } + private readonly kernelRestarted = new EventEmitter(); + private readonly kernelInterrupted = new EventEmitter(); + private disposedEvent = new EventEmitter(); + private sessionStatusChanged: Disposable | undefined; + private initializedMatplotlib = false; + private ioPubListeners = new Set<(msg: KernelMessage.IIOPubMessage, requestId: string) => void>(); + public get kernelSocket(): Observable { + return this.session.kernelSocket; + } + public get session(): IJupyterSession { + return this._session; + } + + constructor( + _liveShare: ILiveShareApi, // This is so the liveshare mixin works + private readonly _session: IJupyterSession, + private configService: IConfigurationService, + private disposableRegistry: IDisposableRegistry, + executionInfo: INotebookExecutionInfo, + private loggers: INotebookExecutionLogger[], + resource: Resource, + identity: Uri, + private getDisposedError: () => Error, + private workspace: IWorkspaceService, + private applicationService: IApplicationShell, + private fs: IDataScienceFileSystem + ) { + this.sessionStartTime = Date.now(); + + const statusChangeHandler = (status: ServerStatus) => { + if (this.onStatusChangedEvent) { + this.onStatusChangedEvent.fire(status); + } + }; + this.sessionStatusChanged = this.session.onSessionStatusChanged(statusChangeHandler); + this._identity = identity; + this._resource = resource; + + // Make a copy of the launch info so we can update it in this class + this._executionInfo = cloneDeep(executionInfo); + } + + public get connection() { + return this._executionInfo.connectionInfo; + } + + public async dispose(): Promise { + if (!this._disposed) { + this._disposed = true; + if (this.onStatusChangedEvent) { + this.onStatusChangedEvent.dispose(); + this.onStatusChangedEvent = undefined; + } + if (this.sessionStatusChanged) { + this.sessionStatusChanged.dispose(); + this.onStatusChangedEvent = undefined; + } + this.loggers.forEach((d) => d.dispose()); + this.disposedEvent.fire(); + + try { + traceInfo(`Shutting down session ${this.identity.toString()}`); + if (this.session) { + await this.session + .dispose() + .catch(traceError.bind('Failed to dispose session from JupyterNotebook')); + } + } catch (exc) { + traceError(`Exception shutting down session `, exc); + } + } + } + + public get onSessionStatusChanged(): Event { + if (!this.onStatusChangedEvent) { + this.onStatusChangedEvent = new EventEmitter(); + } + return this.onStatusChangedEvent.event; + } + + public get status(): ServerStatus { + if (this.session) { + return this.session.status; + } + return ServerStatus.NotStarted; + } + + public get resource(): Resource { + return this._resource; + } + public get identity(): Uri { + return this._identity; + } + + public waitForIdle(timeoutMs: number): Promise { + return this.session ? this.session.waitForIdle(timeoutMs) : Promise.resolve(); + } + + // Set up our initial plotting and imports + public async initialize(cancelToken?: CancellationToken): Promise { + if (this.ranInitialSetup) { + return; + } + this.ranInitialSetup = true; + this._workingDirectory = undefined; + + try { + // When we start our notebook initial, change to our workspace or user specified root directory + await this.updateWorkingDirectoryAndPath(); + + const settings = this.configService.getSettings(this.resource).datascience; + if (settings && settings.themeMatplotlibPlots) { + // We're theming matplotlibs, so we have to setup our default state. + await this.initializeMatplotlib(cancelToken); + } else { + this.initializedMatplotlib = false; + const configInit = + !settings || settings.enablePlotViewer ? CodeSnippets.ConfigSvg : CodeSnippets.ConfigPng; + traceInfo(`Initialize config for plots for ${this.identity.toString()}`); + await this.executeSilently(configInit, cancelToken); + } + + // Run any startup commands that we specified. Support the old form too + let setting = settings.runStartupCommands || settings.runMagicCommands; + + // Convert to string in case we get an array of startup commands. + if (Array.isArray(setting)) { + setting = setting.join(`\n`); + } + + if (setting) { + // Cleanup the line feeds. User may have typed them into the settings UI so they will have an extra \\ on the front. + const cleanedUp = setting.replace(/\\n/g, '\n'); + const cells = await this.executeSilently(cleanedUp, cancelToken); + traceInfo(`Run startup code for notebook: ${cleanedUp} - results: ${cells.length}`); + } + + traceInfo(`Initial setup complete for ${this.identity.toString()}`); + } catch (e) { + traceWarning(e); + } + } + + public clear(_id: string): void { + // We don't do anything as we don't cache results in this class. + noop(); + } + + public execute( + code: string, + file: string, + line: number, + id: string, + cancelToken?: CancellationToken, + silent?: boolean + ): Promise { + // Create a deferred that we'll fire when we're done + const deferred = createDeferred(); + + // Attempt to evaluate this cell in the jupyter notebook. + const observable = this.executeObservable(code, file, line, id, silent); + let output: ICell[]; + + observable.subscribe( + (cells: ICell[]) => { + output = cells; + }, + (error) => { + deferred.reject(error); + }, + () => { + deferred.resolve(output); + } + ); + + if (cancelToken && cancelToken.onCancellationRequested) { + this.disposableRegistry.push( + cancelToken.onCancellationRequested(() => deferred.reject(new CancellationError())) + ); + } + + // Wait for the execution to finish + return deferred.promise; + } + + public inspect(code: string, offsetInCode = 0, cancelToken?: CancellationToken): Promise { + // Create a deferred that will fire when the request completes + const deferred = createDeferred(); + + // First make sure still valid. + const exitError = this.checkForExit(); + if (exitError) { + // Not running, just exit + deferred.reject(exitError); + } else { + // Ask session for inspect result + this.session + .requestInspect({ code, cursor_pos: offsetInCode, detail_level: 0 }) + .then((r) => { + if (r && r.content.status === 'ok') { + deferred.resolve(r.content.data); + } else { + deferred.resolve(undefined); + } + }) + .catch((ex) => { + deferred.reject(ex); + }); + } + + if (cancelToken) { + this.disposableRegistry.push( + cancelToken.onCancellationRequested(() => deferred.reject(new CancellationError())) + ); + } + + return deferred.promise; + } + + public setLaunchingFile(file: string): Promise { + // Update our working directory if we don't have one set already + return this.updateWorkingDirectoryAndPath(file); + } + + public executeObservable( + code: string, + file: string, + line: number, + id: string, + silent: boolean = false + ): Observable { + // Create an observable and wrap the result so we can time it. + const stopWatch = new StopWatch(); + const result = this.executeObservableImpl(code, file, line, id, silent); + return new Observable((subscriber) => { + result.subscribe( + (cells) => { + subscriber.next(cells); + }, + (error) => { + subscriber.error(error); + }, + () => { + subscriber.complete(); + sendTelemetryEvent(Telemetry.ExecuteCell, stopWatch.elapsedTime); + } + ); + }); + } + + public async getSysInfo(): Promise { + // tslint:disable-next-line:no-multiline-string + const versionCells = await this.executeSilently(`import sys\r\nsys.version`); + // tslint:disable-next-line:no-multiline-string + const pathCells = await this.executeSilently(`import sys\r\nsys.executable`); + // tslint:disable-next-line:no-multiline-string + const notebookVersionCells = await this.executeSilently(`import notebook\r\nnotebook.version_info`); + + // Both should have streamed output + const version = versionCells.length > 0 ? this.extractStreamOutput(versionCells[0]).trimQuotes() : ''; + const notebookVersion = + notebookVersionCells.length > 0 ? this.extractStreamOutput(notebookVersionCells[0]).trimQuotes() : ''; + const pythonPath = versionCells.length > 0 ? this.extractStreamOutput(pathCells[0]).trimQuotes() : ''; + + // Combine this data together to make our sys info + return { + data: { + cell_type: 'messages', + messages: [version, notebookVersion, pythonPath], + metadata: {}, + source: [] + }, + id: uuid(), + file: '', + line: 0, + state: CellState.finished + }; + } + + @captureTelemetry(Telemetry.RestartJupyterTime) + public async restartKernel(timeoutMs: number): Promise { + if (this.session) { + // Update our start time so we don't keep sending responses + this.sessionStartTime = Date.now(); + + traceInfo('restartKernel - finishing cells that are outstanding'); + // Complete all pending as an error. We're restarting + this.finishUncompletedCells(); + traceInfo('restartKernel - restarting kernel'); + + // Restart our kernel + await this.session.restart(timeoutMs); + + // Rerun our initial setup for the notebook + this.ranInitialSetup = false; + traceInfo('restartKernel - initialSetup'); + await this.initialize(); + traceInfo('restartKernel - initialSetup completed'); + + // Tell our loggers + this.loggers.forEach((l) => l.onKernelRestarted()); + + this.kernelRestarted.fire(); + return; + } + + throw this.getDisposedError(); + } + + @captureTelemetry(Telemetry.InterruptJupyterTime) + public async interruptKernel(timeoutMs: number): Promise { + if (this.session) { + // Keep track of our current time. If our start time gets reset, we + // restarted the kernel. + const interruptBeginTime = Date.now(); + + // Get just the first pending cell (it should be the oldest). If it doesn't finish + // by our timeout, then our interrupt didn't work. + const firstPending = + this.pendingCellSubscriptions.length > 0 ? this.pendingCellSubscriptions[0] : undefined; + + // Create a promise that resolves when the first pending cell finishes + const finished = firstPending ? firstPending.promise : Promise.resolve(CellState.finished); + + // Create a deferred promise that resolves if we have a failure + const restarted = createDeferred(); + + // Listen to status change events so we can tell if we're restarting + const restartHandler = (e: ServerStatus) => { + if (e === ServerStatus.Restarting) { + // We restarted the kernel. + this.sessionStartTime = Date.now(); + traceWarning('Kernel restarting during interrupt'); + + // Indicate we have to redo initial setup. We can't wait for starting though + // because sometimes it doesn't happen + this.ranInitialSetup = false; + + // Indicate we restarted the race below + restarted.resolve([]); + + // Fail all of the active (might be new ones) pending cell executes. We restarted. + this.finishUncompletedCells(); + } + }; + const restartHandlerToken = this.session.onSessionStatusChanged(restartHandler); + + // Start our interrupt. If it fails, indicate a restart + this.session.interrupt(timeoutMs).catch((exc) => { + traceWarning(`Error during interrupt: ${exc}`); + restarted.resolve([]); + }); + + try { + // Wait for all of the pending cells to finish or the timeout to fire + const result = await waitForPromise(Promise.race([finished, restarted.promise]), timeoutMs); + + // See if we restarted or not + if (restarted.completed) { + return InterruptResult.Restarted; + } + + if (result === null) { + // We timed out. You might think we should stop our pending list, but that's not + // up to us. The cells are still executing. The user has to request a restart or try again + return InterruptResult.TimedOut; + } + + // Cancel all other pending cells as we interrupted. + this.finishUncompletedCells(); + + // Fire event that we interrupted. + this.kernelInterrupted.fire(); + + // Indicate the interrupt worked. + return InterruptResult.Success; + } catch (exc) { + // Something failed. See if we restarted or not. + if (this.sessionStartTime && interruptBeginTime < this.sessionStartTime) { + return InterruptResult.Restarted; + } + + // Otherwise a real error occurred. + throw exc; + } finally { + restartHandlerToken.dispose(); + } + } + + throw this.getDisposedError(); + } + + public async setMatplotLibStyle(useDark: boolean): Promise { + // Make sure matplotlib is initialized + if (!this.initializedMatplotlib) { + await this.initializeMatplotlib(); + } + + const settings = this.configService.getSettings(this.resource).datascience; + if (settings.themeMatplotlibPlots && !settings.ignoreVscodeTheme) { + // Reset the matplotlib style based on if dark or not. + await this.executeSilently( + useDark + ? "matplotlib.style.use('dark_background')" + : `matplotlib.rcParams.update(${Identifiers.MatplotLibDefaultParams})` + ); + } + } + + public async getCompletion( + cellCode: string, + offsetInCode: number, + cancelToken?: CancellationToken + ): Promise { + if (this.session) { + // If server is busy, then don't delay code completion. + if (this.session.status === ServerStatus.Busy) { + return { + matches: [], + cursor: { start: 0, end: 0 }, + metadata: [] + }; + } + const result = await Promise.race([ + this.session!.requestComplete({ + code: cellCode, + cursor_pos: offsetInCode + }), + createPromiseFromCancellation({ defaultValue: undefined, cancelAction: 'resolve', token: cancelToken }) + ]); + if (result && result.content) { + if ('matches' in result.content) { + return { + matches: result.content.matches, + cursor: { + start: result.content.cursor_start, + end: result.content.cursor_end + }, + metadata: result.content.metadata + }; + } + } + return { + matches: [], + cursor: { start: 0, end: 0 }, + metadata: [] + }; + } + + // Default is just say session was disposed + throw new Error(localize.DataScience.sessionDisposed()); + } + + public getMatchingInterpreter(): PythonEnvironment | undefined { + return getInterpreterFromKernelConnectionMetadata(this.getKernelConnection()) as PythonEnvironment | undefined; + } + + public getKernelConnection(): KernelConnectionMetadata | undefined { + return this._executionInfo.kernelConnectionMetadata; + } + + public async setKernelConnection(connectionMetadata: KernelConnectionMetadata, timeoutMS: number): Promise { + // We need to start a new session with the new kernel spec + if (this.session) { + // Turn off setup + this.ranInitialSetup = false; + + // Change the kernel on the session + await this.session.changeKernel(connectionMetadata, timeoutMS); + + // Change our own kernel spec + // Only after session was successfully created. + this._executionInfo.kernelConnectionMetadata = connectionMetadata; + + // Rerun our initial setup + await this.initialize(); + } else { + // Change our own kernel spec + this._executionInfo.kernelConnectionMetadata = connectionMetadata; + } + + this.kernelChanged.fire(connectionMetadata); + } + + public getLoggers(): INotebookExecutionLogger[] { + return this.loggers; + } + + public registerIOPubListener(listener: (msg: KernelMessage.IIOPubMessage, requestId: string) => void): void { + this.ioPubListeners.add(listener); + } + + public registerCommTarget( + targetName: string, + callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike + ) { + if (this.session) { + this.session.registerCommTarget(targetName, callback); + } else { + throw new Error(localize.DataScience.sessionDisposed()); + } + } + + public sendCommMessage( + buffers: (ArrayBuffer | ArrayBufferView)[], + content: { comm_id: string; data: JSONObject; target_name: string | undefined }, + // tslint:disable-next-line: no-any + metadata: any, + // tslint:disable-next-line: no-any + msgId: any + ): Kernel.IShellFuture< + KernelMessage.IShellMessage<'comm_msg'>, + KernelMessage.IShellMessage + > { + if (this.session) { + return this.session.sendCommMessage(buffers, content, metadata, msgId); + } else { + throw new Error(localize.DataScience.sessionDisposed()); + } + } + + public requestCommInfo( + content: KernelMessage.ICommInfoRequestMsg['content'] + ): Promise { + if (this.session) { + return this.session.requestCommInfo(content); + } else { + throw new Error(localize.DataScience.sessionDisposed()); + } + } + public registerMessageHook( + msgId: string, + hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void { + if (this.session) { + return this.session.registerMessageHook(msgId, hook); + } else { + throw new Error(localize.DataScience.sessionDisposed()); + } + } + public removeMessageHook( + msgId: string, + hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void { + if (this.session) { + return this.session.removeMessageHook(msgId, hook); + } else { + throw new Error(localize.DataScience.sessionDisposed()); + } + } + + private async initializeMatplotlib(cancelToken?: CancellationToken): Promise { + const settings = this.configService.getSettings(this.resource).datascience; + if (settings && settings.themeMatplotlibPlots) { + const matplobInit = + !settings || settings.enablePlotViewer + ? CodeSnippets.MatplotLibInitSvg + : CodeSnippets.MatplotLibInitPng; + + traceInfo(`Initialize matplotlib for ${this.identity.toString()}`); + // Force matplotlib to inline and save the default style. We'll use this later if we + // get a request to update style + await this.executeSilently(matplobInit, cancelToken); + + // Use this flag to detemine if we need to rerun this or not. + this.initializedMatplotlib = true; + } + } + + private finishUncompletedCells() { + const copyPending = [...this.pendingCellSubscriptions]; + copyPending.forEach((c) => c.cancel()); + this.pendingCellSubscriptions = []; + } + + @captureTelemetry(Telemetry.HiddenCellTime) + private executeSilently(code: string, cancelToken?: CancellationToken): Promise { + // Create a deferred that we'll fire when we're done + const deferred = createDeferred(); + + // Attempt to evaluate this cell in the jupyter notebook + const observable = this.executeObservableImpl(code, Identifiers.EmptyFileName, 0, uuid(), true); + let output: ICell[]; + + observable.subscribe( + (cells: ICell[]) => { + output = cells; + }, + (error) => { + deferred.reject(error); + }, + () => { + deferred.resolve(output); + } + ); + + if (cancelToken) { + this.disposableRegistry.push( + cancelToken.onCancellationRequested(() => deferred.reject(new CancellationError())) + ); + } + + // Wait for the execution to finish + return deferred.promise; + } + + private extractStreamOutput(cell: ICell): string { + let result = ''; + if (cell.state === CellState.error || cell.state === CellState.finished) { + const outputs = cell.data.outputs as nbformat.IOutput[]; + if (outputs) { + outputs.forEach((o) => { + if (o.output_type === 'stream') { + const stream = o as nbformat.IStream; + result = result.concat(formatStreamText(concatMultilineString(stream.text, true))); + } else { + const data = o.data; + if (data && data.hasOwnProperty('text/plain')) { + // tslint:disable-next-line:no-any + result = result.concat((data as any)['text/plain']); + } + } + }); + } + } + return result; + } + + private executeObservableImpl( + code: string, + file: string, + line: number, + id: string, + silent?: boolean + ): Observable { + // If we have a session, execute the code now. + if (this.session) { + // Generate our cells ahead of time + const cells = generateCells( + this.configService.getSettings(this.resource).datascience, + code, + file, + line, + true, + id + ); + + // Might have more than one (markdown might be split) + if (cells.length > 1) { + // We need to combine results + return this.combineObservables( + this.executeMarkdownObservable(cells[0]), + this.executeCodeObservable(cells[1], silent) + ); + } else if (cells.length > 0) { + // Either markdown or or code + return this.combineObservables( + cells[0].data.cell_type === 'code' + ? this.executeCodeObservable(cells[0], silent) + : this.executeMarkdownObservable(cells[0]) + ); + } + } + + traceError('No session during execute observable'); + + // Can't run because no session + return new Observable((subscriber) => { + subscriber.error(this.getDisposedError()); + subscriber.complete(); + }); + } + + private generateRequest = ( + code: string, + silent?: boolean, + // tslint:disable-next-line: no-any + metadata?: Record + ): Kernel.IShellFuture | undefined => { + //traceInfo(`Executing code in jupyter : ${code}`); + try { + const cellMatcher = new CellMatcher(this.configService.getSettings(this.resource).datascience); + return this.session + ? this.session.requestExecute( + { + // Remove the cell marker if we have one. + code: cellMatcher.stripFirstMarker(code), + stop_on_error: false, + allow_stdin: true, // Allow when silent too in case runStartupCommands asks for a password + store_history: !silent // Silent actually means don't output anything. Store_history is what affects execution_count + }, + silent, // Dispose only silent futures. Otherwise update_display_data doesn't find a future for a previous cell. + metadata + ) + : undefined; + } catch (exc) { + // Any errors generating a request should just be logged. User can't do anything about it. + traceError(exc); + } + + return undefined; + }; + + private combineObservables = (...args: Observable[]): Observable => { + return new Observable((subscriber) => { + // When all complete, we have our results + const results: Record = {}; + + args.forEach((o) => { + o.subscribe( + (c) => { + results[c.id] = c; + + // Convert to an array + const array = Object.keys(results).map((k: string) => { + return results[k]; + }); + + // Update our subscriber of our total results if we have that many + if (array.length === args.length) { + subscriber.next(array); + + // Complete when everybody is finished + if (array.every((a) => a.state === CellState.finished || a.state === CellState.error)) { + subscriber.complete(); + } + } + }, + (e) => { + subscriber.error(e); + } + ); + }); + }); + }; + + private executeMarkdownObservable = (cell: ICell): Observable => { + // Markdown doesn't need any execution + return new Observable((subscriber) => { + subscriber.next(cell); + subscriber.complete(); + }); + }; + + private async updateWorkingDirectoryAndPath(launchingFile?: string): Promise { + if (this._executionInfo && this._executionInfo.connectionInfo.localLaunch && !this._workingDirectory) { + // See what our working dir is supposed to be + const suggested = this._executionInfo.workingDir; + if (suggested && (await this.fs.localDirectoryExists(suggested))) { + // We should use the launch info directory. It trumps the possible dir + this._workingDirectory = suggested; + return this.changeDirectoryIfPossible(this._workingDirectory); + } else if (launchingFile && (await this.fs.localFileExists(launchingFile))) { + // Combine the working directory with this file if possible. + this._workingDirectory = expandWorkingDir( + this._executionInfo.workingDir, + launchingFile, + this.workspace + ); + if (this._workingDirectory) { + return this.changeDirectoryIfPossible(this._workingDirectory); + } + } + } + } + + // Update both current working directory and sys.path with the desired directory + private changeDirectoryIfPossible = async (directory: string): Promise => { + if ( + this._executionInfo && + this._executionInfo.connectionInfo.localLaunch && + isPythonKernelConnection(this._executionInfo.kernelConnectionMetadata) && + (await this.fs.localDirectoryExists(directory)) + ) { + await this.executeSilently(CodeSnippets.UpdateCWDAndPath.format(directory)); + } + }; + + private handleIOPub( + subscriber: CellSubscriber, + silent: boolean | undefined, + clearState: RefBool, + msg: KernelMessage.IIOPubMessage + // tslint:disable-next-line: no-any + ) { + // Let our loggers get a first crack at the message. They may change it + this.getLoggers().forEach((f) => (msg = f.preHandleIOPub ? f.preHandleIOPub(msg) : msg)); + + // tslint:disable-next-line:no-require-imports + const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); + + // Create a trimming function. Only trim user output. Silent output requires the full thing + const trimFunc = silent ? (s: string) => s : this.trimOutput.bind(this); + let shouldUpdateSubscriber = true; + try { + if (jupyterLab.KernelMessage.isExecuteResultMsg(msg)) { + this.handleExecuteResult(msg as KernelMessage.IExecuteResultMsg, clearState, subscriber.cell, trimFunc); + } else if (jupyterLab.KernelMessage.isExecuteInputMsg(msg)) { + this.handleExecuteInput(msg as KernelMessage.IExecuteInputMsg, clearState, subscriber.cell); + } else if (jupyterLab.KernelMessage.isStatusMsg(msg)) { + // If there is no change in the status, then there's no need to update the subscriber. + // Else we end up sending a number of messages unnecessarily uptream. + const statusMsg = msg as KernelMessage.IStatusMsg; + if (statusMsg.content.execution_state === subscriber.executionState) { + shouldUpdateSubscriber = false; + } + subscriber.executionState = statusMsg.content.execution_state; + this.handleStatusMessage(statusMsg, clearState, subscriber.cell); + } else if (jupyterLab.KernelMessage.isStreamMsg(msg)) { + this.handleStreamMesssage(msg as KernelMessage.IStreamMsg, clearState, subscriber.cell, trimFunc); + } else if (jupyterLab.KernelMessage.isDisplayDataMsg(msg)) { + this.handleDisplayData(msg as KernelMessage.IDisplayDataMsg, clearState, subscriber.cell); + } else if (jupyterLab.KernelMessage.isUpdateDisplayDataMsg(msg)) { + // No new data to update UI, hence do not send updates. + shouldUpdateSubscriber = false; + } else if (jupyterLab.KernelMessage.isClearOutputMsg(msg)) { + this.handleClearOutput(msg as KernelMessage.IClearOutputMsg, clearState, subscriber.cell); + } else if (jupyterLab.KernelMessage.isErrorMsg(msg)) { + this.handleError(msg as KernelMessage.IErrorMsg, clearState, subscriber.cell); + } else if (jupyterLab.KernelMessage.isCommOpenMsg(msg)) { + // No new data to update UI, hence do not send updates. + shouldUpdateSubscriber = false; + } else if (jupyterLab.KernelMessage.isCommMsgMsg(msg)) { + // No new data to update UI, hence do not send updates. + shouldUpdateSubscriber = false; + } else if (jupyterLab.KernelMessage.isCommCloseMsg(msg)) { + // No new data to update UI, hence do not send updates. + shouldUpdateSubscriber = false; + } else { + traceWarning(`Unknown message ${msg.header.msg_type} : hasData=${'data' in msg.content}`); + } + + // Set execution count, all messages should have it + if ('execution_count' in msg.content && typeof msg.content.execution_count === 'number') { + subscriber.cell.data.execution_count = msg.content.execution_count as number; + } + + // Tell all of the listeners about the event. + [...this.ioPubListeners].forEach((l) => l(msg, msg.header.msg_id)); + + // Show our update if any new output. + if (shouldUpdateSubscriber) { + subscriber.next(this.sessionStartTime); + } + } catch (err) { + // If not a restart error, then tell the subscriber + subscriber.error(this.sessionStartTime, err); + } + } + + private checkForExit(): Error | undefined { + if (this._executionInfo && this._executionInfo.connectionInfo && !this._executionInfo.connectionInfo.valid) { + if (this._executionInfo.connectionInfo.type === 'jupyter') { + // Not running, just exit + if (this._executionInfo.connectionInfo.localProcExitCode) { + const exitCode = this._executionInfo.connectionInfo.localProcExitCode; + traceError(`Jupyter crashed with code ${exitCode}`); + return new Error(localize.DataScience.jupyterServerCrashed().format(exitCode.toString())); + } + } + } + + return undefined; + } + + private handleInputRequest(_subscriber: CellSubscriber, msg: KernelMessage.IStdinMessage) { + // Ask the user for input + if (msg.content && 'prompt' in msg.content) { + const hasPassword = msg.content.password !== null && (msg.content.password as boolean); + this.applicationService + .showInputBox({ + prompt: msg.content.prompt ? msg.content.prompt.toString() : '', + ignoreFocusOut: true, + password: hasPassword + }) + .then((v) => { + this.session.sendInputReply(v || ''); + }); + } + } + + private handleReply( + subscriber: CellSubscriber, + silent: boolean | undefined, + clearState: RefBool, + msg: KernelMessage.IShellControlMessage + ) { + // tslint:disable-next-line:no-require-imports + const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); + + // Create a trimming function. Only trim user output. Silent output requires the full thing + const trimFunc = silent ? (s: string) => s : this.trimOutput.bind(this); + + if (jupyterLab.KernelMessage.isExecuteReplyMsg(msg)) { + this.handleExecuteReply(msg, clearState, subscriber.cell, trimFunc); + + // Set execution count, all messages should have it + if ('execution_count' in msg.content && typeof msg.content.execution_count === 'number') { + subscriber.cell.data.execution_count = msg.content.execution_count as number; + } + + // Send this event. + subscriber.next(this.sessionStartTime); + } + } + + // tslint:disable-next-line: max-func-body-length + private handleCodeRequest = (subscriber: CellSubscriber, silent?: boolean) => { + // Generate a new request if we still can + if (subscriber.isValid(this.sessionStartTime)) { + // Double check process is still running + const exitError = this.checkForExit(); + if (exitError) { + // Not running, just exit + subscriber.error(this.sessionStartTime, exitError); + subscriber.complete(this.sessionStartTime); + } else { + const request = this.generateRequest(concatMultilineString(subscriber.cell.data.source), silent, { + ...subscriber.cell.data.metadata, + ...{ cellId: subscriber.cell.id } + }); + + // Transition to the busy stage + subscriber.cell.state = CellState.executing; + + // Make sure our connection doesn't go down + let exitHandlerDisposable: Disposable | undefined; + if (this._executionInfo && this._executionInfo.connectionInfo) { + // If the server crashes, cancel the current observable + exitHandlerDisposable = this._executionInfo.connectionInfo.disconnected((c) => { + const str = c ? c.toString() : ''; + // Only do an error if we're not disposed. If we're disposed we already shutdown. + if (!this._disposed) { + subscriber.error( + this.sessionStartTime, + new Error(localize.DataScience.jupyterServerCrashed().format(str)) + ); + } + subscriber.complete(this.sessionStartTime); + }); + } + + // Keep track of our clear state + const clearState = new RefBool(false); + + // Listen to the reponse messages and update state as we go + if (request) { + // Stop handling the request if the subscriber is canceled. + subscriber.onCanceled(() => { + request.onIOPub = noop; + request.onStdin = noop; + request.onReply = noop; + }); + + // Listen to messages. + request.onIOPub = this.handleIOPub.bind(this, subscriber, silent, clearState); + request.onStdin = this.handleInputRequest.bind(this, subscriber); + request.onReply = this.handleReply.bind(this, subscriber, silent, clearState); + + // When the request finishes we are done + request.done + .then(() => subscriber.complete(this.sessionStartTime)) + .catch((e) => { + // @jupyterlab/services throws a `Canceled` error when the kernel is interrupted. + // Such an error must be ignored. + if (e && e instanceof Error && e.message === 'Canceled') { + subscriber.complete(this.sessionStartTime); + } else { + subscriber.error(this.sessionStartTime, e); + } + }) + .finally(() => { + if (exitHandlerDisposable) { + exitHandlerDisposable.dispose(); + } + }) + .ignoreErrors(); + } else { + subscriber.error(this.sessionStartTime, this.getDisposedError()); + } + } + } else { + const sessionDate = new Date(this.sessionStartTime!); + const cellDate = new Date(subscriber.startTime); + traceInfo( + `Session start time is newer than cell : \r\n${sessionDate.toTimeString()}\r\n${cellDate.toTimeString()}` + ); + + // Otherwise just set to an error + this.handleInterrupted(subscriber.cell); + subscriber.cell.state = CellState.error; + subscriber.complete(this.sessionStartTime); + } + }; + + private executeCodeObservable(cell: ICell, silent?: boolean): Observable { + return new Observable((subscriber) => { + // Tell our listener. NOTE: have to do this asap so that markdown cells don't get + // run before our cells. + subscriber.next(cell); + const isSilent = silent !== undefined ? silent : false; + + // Wrap the subscriber and save it. It is now pending and waiting completion. Have to do this + // synchronously so it happens before interruptions. + const cellSubscriber = new CellSubscriber(cell, subscriber, (self: CellSubscriber) => { + // Subscriber completed, remove from subscriptions. + this.pendingCellSubscriptions = this.pendingCellSubscriptions.filter((p) => p !== self); + + // Indicate success or failure + this.logPostCode(cell, isSilent).ignoreErrors(); + }); + this.pendingCellSubscriptions.push(cellSubscriber); + + // Log the pre execution. + this.logPreCode(cell, isSilent) + .then(() => { + // Now send our real request. This should call back on the cellsubscriber when it's done. + this.handleCodeRequest(cellSubscriber, silent); + }) + .ignoreErrors(); + }); + } + + private async logPreCode(cell: ICell, silent: boolean): Promise { + await Promise.all(this.loggers.map((l) => l.preExecute(cell, silent))); + } + + private async logPostCode(cell: ICell, silent: boolean): Promise { + await Promise.all(this.loggers.map((l) => l.postExecute(cloneDeep(cell), silent))); + } + + private addToCellData = ( + cell: ICell, + output: + | nbformat.IUnrecognizedOutput + | nbformat.IExecuteResult + | nbformat.IDisplayData + | nbformat.IStream + | nbformat.IError, + clearState: RefBool + ) => { + const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; + + // Clear if necessary + if (clearState.value) { + data.outputs = []; + clearState.update(false); + } + + // Append to the data. + data.outputs = [...data.outputs, output]; + cell.data = data; + }; + + // See this for docs on the messages: + // https://jupyter-client.readthedocs.io/en/latest/messaging.html#messaging-in-jupyter + private handleExecuteResult( + msg: KernelMessage.IExecuteResultMsg, + clearState: RefBool, + cell: ICell, + trimFunc: (str: string) => string + ) { + // Check our length on text output + if (msg.content.data && msg.content.data.hasOwnProperty('text/plain')) { + msg.content.data['text/plain'] = trimFunc(msg.content.data['text/plain'] as string); + } + + this.addToCellData( + cell, + { + output_type: 'execute_result', + data: msg.content.data, + metadata: msg.content.metadata, + // tslint:disable-next-line: no-any + transient: msg.content.transient as any, // NOSONAR + execution_count: msg.content.execution_count + }, + clearState + ); + } + + private handleExecuteReply( + msg: KernelMessage.IExecuteReplyMsg, + clearState: RefBool, + cell: ICell, + trimFunc: (str: string) => string + ) { + const reply = msg.content as KernelMessage.IExecuteReply; + if (reply.payload) { + reply.payload.forEach((o) => { + if (o.data && o.data.hasOwnProperty('text/plain')) { + // tslint:disable-next-line: no-any + const str = (o.data as any)['text/plain'].toString(); + const data = trimFunc(str) as string; + this.addToCellData( + cell, + { + // Mark as stream output so the text is formatted because it likely has ansi codes in it. + output_type: 'stream', + text: data, + metadata: {}, + execution_count: reply.execution_count + }, + clearState + ); + } + }); + } + } + + private handleExecuteInput(msg: KernelMessage.IExecuteInputMsg, _clearState: RefBool, cell: ICell) { + cell.data.execution_count = msg.content.execution_count; + } + + private handleStatusMessage(msg: KernelMessage.IStatusMsg, _clearState: RefBool, _cell: ICell) { + traceInfo(`Kernel switching to ${msg.content.execution_state}`); + } + + private handleStreamMesssage( + msg: KernelMessage.IStreamMsg, + clearState: RefBool, + cell: ICell, + trimFunc: (str: string) => string + ) { + const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; + let originalTextLength = 0; + let trimmedTextLength = 0; + + // Clear output if waiting for a clear + if (clearState.value) { + data.outputs = []; + clearState.update(false); + } + + // Might already have a stream message. If so, just add on to it. + const existing = + data.outputs.length > 0 && data.outputs[data.outputs.length - 1].output_type === 'stream' + ? data.outputs[data.outputs.length - 1] + : undefined; + if (existing) { + // tslint:disable-next-line:restrict-plus-operands + existing.text = existing.text + msg.content.text; + const originalText = formatStreamText(concatMultilineString(existing.text)); + originalTextLength = originalText.length; + existing.text = trimFunc(originalText); + trimmedTextLength = existing.text.length; + } else { + const originalText = formatStreamText(concatMultilineString(msg.content.text)); + originalTextLength = originalText.length; + // Create a new stream entry + const output: nbformat.IStream = { + output_type: 'stream', + name: msg.content.name, + text: trimFunc(originalText) + }; + data.outputs = [...data.outputs, output]; + trimmedTextLength = output.text.length; + cell.data = data; + } + + // If the output was trimmed, we add the 'outputPrepend' metadata tag. + // Later, the react side will display a message letting the user know + // the output is trimmed and what setting changes that. + // * If data.metadata.tags is undefined, define it so the following + // code is can rely on it being defined. + if (data.metadata.tags === undefined) { + data.metadata.tags = []; + } + + data.metadata.tags = data.metadata.tags.filter((t) => t !== 'outputPrepend'); + + if (trimmedTextLength < originalTextLength) { + data.metadata.tags.push('outputPrepend'); + } + } + + private handleDisplayData(msg: KernelMessage.IDisplayDataMsg, clearState: RefBool, cell: ICell) { + const output: nbformat.IDisplayData = { + output_type: 'display_data', + data: msg.content.data, + metadata: msg.content.metadata, + // tslint:disable-next-line: no-any + transient: msg.content.transient as any // NOSONAR + }; + this.addToCellData(cell, output, clearState); + } + + private handleClearOutput(msg: KernelMessage.IClearOutputMsg, clearState: RefBool, cell: ICell) { + // If the message says wait, add every message type to our clear state. This will + // make us wait for this type of output before we clear it. + if (msg && msg.content.wait) { + clearState.update(true); + } else { + // Clear all outputs and start over again. + const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; + data.outputs = []; + } + } + + private handleInterrupted(cell: ICell) { + this.handleError( + { + channel: 'iopub', + parent_header: {}, + metadata: {}, + header: { username: '', version: '', session: '', msg_id: '', msg_type: 'error', date: '' }, + content: { + ename: 'KeyboardInterrupt', + evalue: '', + // Does this need to be translated? All depends upon if jupyter does or not + traceback: [ + '---------------------------------------------------------------------------', + 'KeyboardInterrupt: ' + ] + } + }, + new RefBool(false), + cell + ); + } + + private handleError(msg: KernelMessage.IErrorMsg, clearState: RefBool, cell: ICell) { + const output: nbformat.IError = { + output_type: 'error', + ename: msg.content.ename, + evalue: msg.content.evalue, + traceback: msg.content.traceback + }; + this.addToCellData(cell, output, clearState); + cell.state = CellState.error; + + // In the error scenario, we want to stop all other pending cells. + if (this.configService.getSettings(this.resource).datascience.stopOnError) { + this.pendingCellSubscriptions.forEach((c) => { + if (c.cell.id !== cell.id) { + c.cancel(); + } + }); + } + } + + // We have a set limit for the number of output text characters that we display by default + // trim down strings to that limit, assuming at this point we have compressed down to a single string + private trimOutput(outputString: string): string { + const outputLimit = this.configService.getSettings(this.resource).datascience.textOutputLimit; + + if (!outputLimit || outputLimit === 0 || outputString.length <= outputLimit) { + return outputString; + } + + return outputString.substr(outputString.length - outputLimit); + } +} diff --git a/src/client/datascience/jupyter/jupyterNotebookProvider.ts b/src/client/datascience/jupyter/jupyterNotebookProvider.ts new file mode 100644 index 000000000000..c1103cab0fb2 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterNotebookProvider.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as localize from '../../common/utils/localize'; +import { + ConnectNotebookProviderOptions, + GetNotebookOptions, + IJupyterConnection, + IJupyterNotebookProvider, + IJupyterServerProvider, + INotebook +} from '../types'; + +// When the NotebookProvider looks to create a notebook it uses this class to create a Jupyter notebook +@injectable() +export class JupyterNotebookProvider implements IJupyterNotebookProvider { + constructor(@inject(IJupyterServerProvider) private readonly serverProvider: IJupyterServerProvider) {} + + public async disconnect(options: ConnectNotebookProviderOptions): Promise { + const server = await this.serverProvider.getOrCreateServer(options); + + return server?.dispose(); + } + + public async connect(options: ConnectNotebookProviderOptions): Promise { + const server = await this.serverProvider.getOrCreateServer(options); + return server?.getConnectionInfo(); + } + + public async createNotebook(options: GetNotebookOptions): Promise { + // Make sure we have a server + const server = await this.serverProvider.getOrCreateServer({ + getOnly: options.getOnly, + disableUI: options.disableUI, + token: options.token + }); + + if (server) { + return server.createNotebook(options.resource, options.identity, options.metadata, options.token); + } + // We want createNotebook to always return a notebook promise, so if we don't have a server + // here throw our generic server disposed message that we use in server creatio n + throw new Error(localize.DataScience.sessionDisposed()); + } + public async getNotebook(options: GetNotebookOptions): Promise { + const server = await this.serverProvider.getOrCreateServer({ + getOnly: options.getOnly, + disableUI: options.disableUI, + token: options.token + }); + if (server) { + return server.getNotebook(options.identity, options.token); + } + } +} diff --git a/src/client/datascience/jupyter/jupyterPasswordConnect.ts b/src/client/datascience/jupyter/jupyterPasswordConnect.ts new file mode 100644 index 000000000000..0f1850218be8 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterPasswordConnect.ts @@ -0,0 +1,461 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Agent as HttpsAgent } from 'https'; +import { inject, injectable } from 'inversify'; +import * as nodeFetch from 'node-fetch'; +import { URLSearchParams } from 'url'; +import { ConfigurationTarget } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { IAsyncDisposableRegistry, IConfigurationService } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { IMultiStepInput, IMultiStepInputFactory } from '../../common/utils/multiStepInput'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { IJupyterPasswordConnect, IJupyterPasswordConnectInfo } from '../types'; +import { Telemetry } from './../constants'; + +@injectable() +export class JupyterPasswordConnect implements IJupyterPasswordConnect { + private savedConnectInfo = new Map>(); + private fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise = + nodeFetch.default; + + constructor( + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, + @inject(IAsyncDisposableRegistry) private readonly asyncDisposableRegistry: IAsyncDisposableRegistry, + @inject(IConfigurationService) private readonly configService: IConfigurationService + ) {} + + @captureTelemetry(Telemetry.GetPasswordAttempt) + public getPasswordConnectionInfo( + url: string, + fetchFunction?: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise + ): Promise { + if (!url || url.length < 1) { + return Promise.resolve(undefined); + } + + // Update our fetch function if necessary + if (fetchFunction) { + this.fetchFunction = fetchFunction; + } + + // Add on a trailing slash to our URL if it's not there already + let newUrl = url; + if (newUrl[newUrl.length - 1] !== '/') { + newUrl = `${newUrl}/`; + } + + // See if we already have this data. Don't need to ask for a password more than once. (This can happen in remote when listing kernels) + let result = this.savedConnectInfo.get(newUrl); + if (!result) { + result = this.getNonCachedPasswordConnectionInfo(newUrl); + this.savedConnectInfo.set(newUrl, result); + } + + return result; + } + + private getSessionCookieString(xsrfCookie: string, sessionCookieName: string, sessionCookieValue: string): string { + return `_xsrf=${xsrfCookie}; ${sessionCookieName}=${sessionCookieValue}`; + } + + private async getNonCachedPasswordConnectionInfo(url: string): Promise { + // If jupyter hub, go down a special path of asking jupyter hub for a token + if (await this.isJupyterHub(url)) { + return this.getJupyterHubConnectionInfo(url); + } else { + return this.getJupyterConnectionInfo(url); + } + } + + private async getJupyterHubConnectionInfo(uri: string): Promise { + // First ask for the user name and password + const userNameAndPassword = await this.getUserNameAndPassword(); + if (userNameAndPassword.username || userNameAndPassword.password) { + // Try the login method. It should work and doesn't require a token to be generated. + const result = await this.getJupyterHubConnectionInfoFromLogin( + uri, + userNameAndPassword.username, + userNameAndPassword.password + ); + + // If login method fails, try generating a token + if (!result) { + return this.getJupyterHubConnectionInfoFromApi( + uri, + userNameAndPassword.username, + userNameAndPassword.password + ); + } + + return result; + } + } + + private async getJupyterHubConnectionInfoFromLogin( + uri: string, + username: string, + password: string + ): Promise { + // We're using jupyter hub. Get the base url + const url = new URL(uri); + const baseUrl = `${url.protocol}//${url.host}`; + + const postParams = new URLSearchParams(); + postParams.append('username', username || ''); + postParams.append('password', password || ''); + + let response = await this.makeRequest(`${baseUrl}/hub/login?next=`, { + method: 'POST', + headers: { + Connection: 'keep-alive', + Referer: `${baseUrl}/hub/login`, + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' + }, + body: postParams.toString(), + redirect: 'manual' + }); + + // The cookies from that response should be used to make the next set of requests + if (response && response.status === 302) { + const cookies = this.getCookies(response); + const cookieString = [...cookies.entries()].reduce((p, c) => `${p};${c[0]}=${c[1]}`, ''); + // See this API for creating a token + // https://jupyterhub.readthedocs.io/en/stable/_static/rest-api/index.html#operation--users--name--tokens-post + response = await this.makeRequest(`${baseUrl}/hub/api/users/${username}/tokens`, { + method: 'POST', + headers: { + Connection: 'keep-alive', + Cookie: cookieString, + Referer: `${baseUrl}/hub/login` + } + }); + + // That should give us a new token. For now server name is hard coded. Not sure + // how to fetch it other than in the info for a default token + if (response.ok && response.status === 200) { + const body = await response.json(); + if (body && body.token && body.id) { + // Response should have the token to use for this user. + + // Make sure the server is running for this user. Don't need + // to check response as it will fail if already running. + // https://jupyterhub.readthedocs.io/en/stable/_static/rest-api/index.html#operation--users--name--server-post + await this.makeRequest(`${baseUrl}/hub/api/users/${username}/server`, { + method: 'POST', + headers: { + Connection: 'keep-alive', + Cookie: cookieString, + Referer: `${baseUrl}/hub/login` + } + }); + + // This token was generated for this request. We should clean it up when + // the user closes VS code + this.asyncDisposableRegistry.push({ + dispose: async () => { + this.makeRequest(`${baseUrl}/hub/api/users/${username}/tokens/${body.id}`, { + method: 'DELETE', + headers: { + Connection: 'keep-alive', + Cookie: cookieString, + Referer: `${baseUrl}/hub/login` + } + }).ignoreErrors(); // Don't wait for this during shutdown. Just make the request + } + }); + + return { + requestHeaders: {}, + remappedBaseUrl: `${baseUrl}/user/${username}`, + remappedToken: body.token + }; + } + } + } + } + + private async getJupyterHubConnectionInfoFromApi( + uri: string, + username: string, + password: string + ): Promise { + // We're using jupyter hub. Get the base url + const url = new URL(uri); + const baseUrl = `${url.protocol}//${url.host}`; + // Use these in a post request to get the token to use + const response = await this.makeRequest( + `${baseUrl}/hub/api/authorizations/token`, // This seems to be deprecated, but it works. It requests a new token + { + method: 'POST', + headers: { + Connection: 'keep-alive', + 'content-type': 'application/json;charset=UTF-8' + }, + body: `{ "username": "${username || ''}", "password": "${password || ''}" }`, + redirect: 'manual' + } + ); + + if (response.ok && response.status === 200) { + const body = await response.json(); + if (body && body.user && body.user.server && body.token) { + // Response should have the token to use for this user. + return { + requestHeaders: {}, + remappedBaseUrl: `${baseUrl}${body.user.server}`, + remappedToken: body.token + }; + } + } + } + + private async getJupyterConnectionInfo(url: string): Promise { + let xsrfCookie: string | undefined; + let sessionCookieName: string | undefined; + let sessionCookieValue: string | undefined; + + // First determine if we need a password. A request for the base URL with /tree? should return a 302 if we do. + if (await this.needPassword(url)) { + // Get password first + let userPassword = await this.getUserPassword(); + + if (userPassword) { + xsrfCookie = await this.getXSRFToken(url); + + // Then get the session cookie by hitting that same page with the xsrftoken and the password + if (xsrfCookie) { + const sessionResult = await this.getSessionCookie(url, xsrfCookie, userPassword); + sessionCookieName = sessionResult.sessionCookieName; + sessionCookieValue = sessionResult.sessionCookieValue; + } + } else { + // If userPassword is undefined or '' then the user didn't pick a password. In this case return back that we should just try to connect + // like a standard connection. Might be the case where there is no token and no password + return {}; + } + userPassword = undefined; + } else { + // If no password needed, act like empty password and no cookie + return {}; + } + + // If we found everything return it all back if not, undefined as partial is useless + if (xsrfCookie && sessionCookieName && sessionCookieValue) { + sendTelemetryEvent(Telemetry.GetPasswordSuccess); + const cookieString = this.getSessionCookieString(xsrfCookie, sessionCookieName, sessionCookieValue); + const requestHeaders = { Cookie: cookieString, 'X-XSRFToken': xsrfCookie }; + return { requestHeaders }; + } else { + sendTelemetryEvent(Telemetry.GetPasswordFailure); + return undefined; + } + } + + // For HTTPS connections respect our allowUnauthorized setting by adding in an agent to enable that on the request + private addAllowUnauthorized( + url: string, + allowUnauthorized: boolean, + options: nodeFetch.RequestInit + ): nodeFetch.RequestInit { + if (url.startsWith('https') && allowUnauthorized) { + const requestAgent = new HttpsAgent({ rejectUnauthorized: false }); + return { ...options, agent: requestAgent }; + } + + return options; + } + + private async getUserNameAndPassword(): Promise<{ username: string; password: string }> { + const multistep = this.multiStepFactory.create<{ username: string; password: string }>(); + const state = { username: '', password: '' }; + await multistep.run(this.getUserNameMultiStep.bind(this), state); + return state; + } + + private async getUserNameMultiStep( + input: IMultiStepInput<{ username: string; password: string }>, + state: { username: string; password: string } + ) { + state.username = await input.showInputBox({ + title: localize.DataScience.jupyterSelectUserAndPasswordTitle(), + prompt: localize.DataScience.jupyterSelectUserPrompt(), + validate: this.validateUserNameOrPassword, + value: '' + }); + if (state.username) { + return this.getPasswordMultiStep.bind(this); + } + } + + private async validateUserNameOrPassword(_value: string): Promise { + return undefined; + } + + private async getPasswordMultiStep( + input: IMultiStepInput<{ username: string; password: string }>, + state: { username: string; password: string } + ) { + state.password = await input.showInputBox({ + title: localize.DataScience.jupyterSelectUserAndPasswordTitle(), + prompt: localize.DataScience.jupyterSelectPasswordPrompt(), + validate: this.validateUserNameOrPassword, + value: '', + password: true + }); + } + + private async getUserPassword(): Promise { + return this.appShell.showInputBox({ + prompt: localize.DataScience.jupyterSelectPasswordPrompt(), + ignoreFocusOut: true, + password: true + }); + } + + private async getXSRFToken(url: string): Promise { + let xsrfCookie: string | undefined; + + const response = await this.makeRequest(`${url}login?`, { + method: 'get', + redirect: 'manual', + headers: { Connection: 'keep-alive' } + }); + + if (response.ok) { + const cookies = this.getCookies(response); + if (cookies.has('_xsrf')) { + xsrfCookie = cookies.get('_xsrf')?.split(';')[0]; + } + } + + return xsrfCookie; + } + + private async needPassword(url: string): Promise { + // A jupyter server will redirect if you ask for the tree when a login is required + const response = await this.makeRequest(`${url}tree?`, { + method: 'get', + redirect: 'manual', + headers: { Connection: 'keep-alive' } + }); + + return response.status !== 200; + } + + private async makeRequest(url: string, options: nodeFetch.RequestInit): Promise { + const allowUnauthorized = this.configService.getSettings(undefined).datascience + .allowUnauthorizedRemoteConnection; + + // Try once and see if it fails with unauthorized. + try { + return await this.fetchFunction( + url, + this.addAllowUnauthorized(url, allowUnauthorized ? true : false, options) + ); + } catch (e) { + if (e.message.indexOf('reason: self signed certificate') >= 0) { + // Ask user to change setting and possibly try again. + const enableOption: string = localize.DataScience.jupyterSelfCertEnable(); + const closeOption: string = localize.DataScience.jupyterSelfCertClose(); + const value = await this.appShell.showErrorMessage( + localize.DataScience.jupyterSelfCertFail().format(e.message), + enableOption, + closeOption + ); + if (value === enableOption) { + sendTelemetryEvent(Telemetry.SelfCertsMessageEnabled); + await this.configService.updateSetting( + 'dataScience.allowUnauthorizedRemoteConnection', + true, + undefined, + ConfigurationTarget.Workspace + ); + return this.fetchFunction(url, this.addAllowUnauthorized(url, true, options)); + } else if (value === closeOption) { + sendTelemetryEvent(Telemetry.SelfCertsMessageClose); + } + } + throw e; + } + } + + private async isJupyterHub(url: string): Promise { + // See this for the different REST endpoints: + // https://jupyterhub.readthedocs.io/en/stable/_static/rest-api/index.html + + // If the URL has the /user/ option in it, it's likely this is jupyter hub + if (url.toLowerCase().includes('/user/')) { + return true; + } + + // Otherwise request hub/api. This should return the json with the hub version + // if this is a hub url + const response = await this.makeRequest(`${url}hub/api`, { + method: 'get', + redirect: 'manual', + headers: { Connection: 'keep-alive' } + }); + + return response.status === 200; + } + + // Jupyter uses a session cookie to validate so by hitting the login page with the password we can get that cookie and use it ourselves + // This workflow can be seen by running fiddler and hitting the login page with a browser + // First you need a get at the login page to get the xsrf token, then you send back that token along with the password in a post + // That will return back the session cookie. This session cookie then needs to be added to our requests and websockets for @jupyterlab/services + private async getSessionCookie( + url: string, + xsrfCookie: string, + password: string + ): Promise<{ sessionCookieName: string | undefined; sessionCookieValue: string | undefined }> { + let sessionCookieName: string | undefined; + let sessionCookieValue: string | undefined; + // Create the form params that we need + const postParams = new URLSearchParams(); + postParams.append('_xsrf', xsrfCookie); + postParams.append('password', password); + + const response = await this.makeRequest(`${url}login?`, { + method: 'post', + headers: { + Cookie: `_xsrf=${xsrfCookie}`, + Connection: 'keep-alive', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' + }, + body: postParams.toString(), + redirect: 'manual' + }); + + // Now from this result we need to extract the session cookie + if (response.status === 302) { + const cookies = this.getCookies(response); + + // Session cookie is the first one + if (cookies.size > 0) { + sessionCookieName = cookies.entries().next().value[0]; + sessionCookieValue = cookies.entries().next().value[1]; + } + } + + return { sessionCookieName, sessionCookieValue }; + } + + private getCookies(response: nodeFetch.Response): Map { + const cookieList: Map = new Map(); + + const cookies = response.headers.raw()['set-cookie']; + + if (cookies) { + cookies.forEach((value) => { + const cookieKey = value.substring(0, value.indexOf('=')); + const cookieVal = value.substring(value.indexOf('=') + 1); + cookieList.set(cookieKey, cookieVal); + }); + } + + return cookieList; + } +} diff --git a/src/client/datascience/jupyter/jupyterRequest.ts b/src/client/datascience/jupyter/jupyterRequest.ts new file mode 100644 index 000000000000..027103fb8d87 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterRequest.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as nodeFetch from 'node-fetch'; + +// Function for creating node Request object that prevents jupyterlab services from writing its own +// authorization header. +// tslint:disable: no-any +export function createAuthorizingRequest(getAuthHeader: () => any) { + class AuthorizingRequest extends nodeFetch.Request { + constructor(input: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) { + super(input, init); + + // Add all of the authorization parts onto the headers. + const origHeaders = this.headers; + const authorizationHeader = getAuthHeader(); + const keys = Object.keys(authorizationHeader); + keys.forEach((k) => origHeaders.append(k, authorizationHeader[k].toString())); + origHeaders.append('Content-Type', 'application/json'); + + // Rewrite the 'append' method for the headers to disallow 'authorization' after this point + const origAppend = origHeaders.append.bind(origHeaders); + origHeaders.append = (k, v) => { + if (k.toLowerCase() !== 'authorization') { + origAppend(k, v); + } + }; + } + } + return AuthorizingRequest; +} diff --git a/src/client/datascience/jupyter/jupyterSelfCertsError.ts b/src/client/datascience/jupyter/jupyterSelfCertsError.ts new file mode 100644 index 000000000000..0c2ee41a5ae9 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterSelfCertsError.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +export class JupyterSelfCertsError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/src/client/datascience/jupyter/jupyterServer.ts b/src/client/datascience/jupyter/jupyterServer.ts new file mode 100644 index 000000000000..b6e28b3cce6c --- /dev/null +++ b/src/client/datascience/jupyter/jupyterServer.ts @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as uuid from 'uuid/v4'; +import { Disposable, Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { ILiveShareApi } from '../../common/application/types'; +import '../../common/extensions'; +import { traceError, traceInfo } from '../../common/logger'; +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + Resource +} from '../../common/types'; +import { createDeferred, Deferred, sleep } from '../../common/utils/async'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { IServiceContainer } from '../../ioc/types'; +import { captureTelemetry } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { + IJupyterConnection, + IJupyterSession, + IJupyterSessionManager, + IJupyterSessionManagerFactory, + INotebook, + INotebookMetadataLive, + INotebookServer, + INotebookServerLaunchInfo +} from '../types'; +import { getDisplayNameOrNameOfKernelConnection } from './kernels/helpers'; + +// This code is based on the examples here: +// https://www.npmjs.com/package/@jupyterlab/services + +export class JupyterServerBase implements INotebookServer { + private launchInfo: INotebookServerLaunchInfo | undefined; + private _id = uuid(); + private connectPromise: Deferred = createDeferred(); + private connectionInfoDisconnectHandler: Disposable | undefined; + private serverExitCode: number | undefined; + private notebooks = new Map>(); + private sessionManager: IJupyterSessionManager | undefined; + private savedSession: IJupyterSession | undefined; + + constructor( + _liveShare: ILiveShareApi, + private asyncRegistry: IAsyncDisposableRegistry, + private disposableRegistry: IDisposableRegistry, + protected readonly configService: IConfigurationService, + private sessionManagerFactory: IJupyterSessionManagerFactory, + private serviceContainer: IServiceContainer, + private jupyterOutputChannel: IOutputChannel + ) { + this.asyncRegistry.push(this); + traceInfo(`Creating jupyter server: ${this._id}`); + } + + public async connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken): Promise { + traceInfo( + `Connecting server ${this.id} kernelSpec ${getDisplayNameOrNameOfKernelConnection( + launchInfo.kernelConnectionMetadata, + 'unknown' + )}` + ); + + // Save our launch info + this.launchInfo = launchInfo; + + // Indicate connect started + this.connectPromise.resolve(launchInfo); + + // Listen to the process going down + if (this.launchInfo && this.launchInfo.connectionInfo) { + this.connectionInfoDisconnectHandler = this.launchInfo.connectionInfo.disconnected((c) => { + try { + this.serverExitCode = c; + traceError(localize.DataScience.jupyterServerCrashed().format(c.toString())); + this.shutdown().ignoreErrors(); + } catch { + noop(); + } + }); + } + + // Indicate we have a new session on the output channel + this.logRemoteOutput(localize.DataScience.connectingToJupyterUri().format(launchInfo.connectionInfo.baseUrl)); + + // Create our session manager + this.sessionManager = await this.sessionManagerFactory.create(launchInfo.connectionInfo); + + // Try creating a session just to ensure we're connected. Callers of this function check to make sure jupyter + // is running and connectable. + let session: IJupyterSession | undefined; + session = await this.sessionManager.startNew( + launchInfo.kernelConnectionMetadata, + launchInfo.connectionInfo.rootDirectory, + cancelToken + ); + const idleTimeout = this.configService.getSettings().datascience.jupyterLaunchTimeout; + // The wait for idle should throw if we can't connect. + await session.waitForIdle(idleTimeout); + // If that works, save this session for the next notebook to use + this.savedSession = session; + } + + @captureTelemetry(Telemetry.JupyterCreatingNotebook, undefined, true) + public createNotebook( + resource: Resource, + identity: Uri, + notebookMetadata?: INotebookMetadataLive, + cancelToken?: CancellationToken + ): Promise { + if (!this.sessionManager || this.isDisposed) { + throw new Error(localize.DataScience.sessionDisposed()); + } + // If we have a saved session send this into the notebook so we don't create a new one + const savedSession = this.savedSession; + this.savedSession = undefined; + + // Create a notebook and return it. + return this.createNotebookInstance( + resource, + identity, + this.sessionManager, + savedSession, + this.disposableRegistry, + this.configService, + this.serviceContainer, + notebookMetadata, + cancelToken + ).then((r) => { + const baseUrl = this.launchInfo?.connectionInfo.baseUrl || ''; + this.logRemoteOutput(localize.DataScience.createdNewNotebook().format(baseUrl)); + return r; + }); + } + + public async shutdown(): Promise { + try { + // Order should be + // 1) connectionInfoDisconnectHandler - listens to process close + // 2) sessions (owned by the notebooks) + // 3) session manager (owned by this object) + // 4) connInfo (owned by this object) - kills the jupyter process + + if (this.connectionInfoDisconnectHandler) { + this.connectionInfoDisconnectHandler.dispose(); + this.connectionInfoDisconnectHandler = undefined; + } + + // Destroy the kernel spec + await this.destroyKernelSpec(); + + // Remove the saved session if we haven't passed it onto a notebook + if (this.savedSession) { + await this.savedSession.dispose(); + this.savedSession = undefined; + } + + traceInfo(`Shutting down notebooks for ${this.id}`); + const notebooks = await Promise.all([...this.notebooks.values()]); + await Promise.all(notebooks.map((n) => n?.dispose())); + traceInfo(`Shut down session manager : ${this.sessionManager ? 'existing' : 'undefined'}`); + if (this.sessionManager) { + // Session manager in remote case may take too long to shutdown. Don't wait that + // long. + const result = await Promise.race([sleep(10_000), this.sessionManager.dispose()]); + if (result === 10_000) { + traceError(`Session shutdown timed out.`); + } + this.sessionManager = undefined; + } + + // After shutting down notebooks and session manager, kill the main process. + if (this.launchInfo && this.launchInfo.connectionInfo) { + traceInfo('Shutdown server - dispose conn info'); + this.launchInfo.connectionInfo.dispose(); // This should kill the process that's running + this.launchInfo = undefined; + } + } catch (e) { + traceError(`Error during shutdown: `, e); + } + } + + public dispose(): Promise { + return this.shutdown(); + } + + public get id(): string { + return this._id; + } + + public waitForConnect(): Promise { + return this.connectPromise.promise; + } + + // Return a copy of the connection information that this server used to connect with + public getConnectionInfo(): IJupyterConnection | undefined { + if (!this.launchInfo) { + return undefined; + } + + // Return a copy with a no-op for dispose + return { + ...this.launchInfo.connectionInfo, + dispose: noop + }; + } + + public getDisposedError(): Error { + // We may have been disposed because of a crash. See if our connection info is indicating shutdown + if (this.serverExitCode) { + return new Error(localize.DataScience.jupyterServerCrashed().format(this.serverExitCode.toString())); + } + + // Default is just say session was disposed + return new Error(localize.DataScience.sessionDisposed()); + } + + public async getNotebook(identity: Uri): Promise { + return this.notebooks.get(identity.toString()); + } + + protected getNotebooks(): Promise[] { + return [...this.notebooks.values()]; + } + + protected setNotebook(identity: Uri, notebook: Promise) { + const removeNotebook = () => { + if (this.notebooks.get(identity.toString()) === notebook) { + this.notebooks.delete(identity.toString()); + } + }; + + notebook + .then((nb) => { + const oldDispose = nb.dispose; + nb.dispose = () => { + this.notebooks.delete(identity.toString()); + return oldDispose(); + }; + }) + .catch(removeNotebook); + + // Save the notebook + this.notebooks.set(identity.toString(), notebook); + } + + protected createNotebookInstance( + _resource: Resource, + _identity: Uri, + _sessionManager: IJupyterSessionManager, + _savedSession: IJupyterSession | undefined, + _disposableRegistry: IDisposableRegistry, + _configService: IConfigurationService, + _serviceContainer: IServiceContainer, + _notebookMetadata?: INotebookMetadataLive, + _cancelToken?: CancellationToken + ): Promise { + throw new Error('You forgot to override createNotebookInstance'); + } + + protected get isDisposed(): boolean { + throw new Error('You forgot to override isDisposed'); + } + + private async destroyKernelSpec() { + if (this.launchInfo) { + this.launchInfo.kernelConnectionMetadata = undefined; + } + } + + private logRemoteOutput(output: string) { + if (this.launchInfo && !this.launchInfo.connectionInfo.localLaunch) { + this.jupyterOutputChannel.appendLine(output); + } + } +} diff --git a/src/client/datascience/jupyter/jupyterServerWrapper.ts b/src/client/datascience/jupyter/jupyterServerWrapper.ts new file mode 100644 index 000000000000..426b1331ec31 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterServerWrapper.ts @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { nbformat } from '@jupyterlab/coreutils'; +import { inject, injectable, named } from 'inversify'; +import * as uuid from 'uuid/v4'; +import { Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import * as vsls from 'vsls/vscode'; +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../common/application/types'; +import '../../common/extensions'; + +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + Resource +} from '../../common/types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { DataScienceStartupTime, JUPYTER_OUTPUT_CHANNEL } from '../constants'; +import { + IDataScienceFileSystem, + IJupyterConnection, + IJupyterSessionManagerFactory, + INotebook, + INotebookServer, + INotebookServerLaunchInfo +} from '../types'; +import { KernelSelector } from './kernels/kernelSelector'; +import { GuestJupyterServer } from './liveshare/guestJupyterServer'; +import { HostJupyterServer } from './liveshare/hostJupyterServer'; +import { IRoleBasedObject, RoleBasedFactory } from './liveshare/roleBasedFactory'; +import { ILiveShareHasRole } from './liveshare/types'; + +interface IJupyterServerInterface extends IRoleBasedObject, INotebookServer {} + +// tslint:disable:callable-types +type JupyterServerClassType = { + new ( + liveShare: ILiveShareApi, + startupTime: number, + asyncRegistry: IAsyncDisposableRegistry, + disposableRegistry: IDisposableRegistry, + configService: IConfigurationService, + sessionManager: IJupyterSessionManagerFactory, + workspaceService: IWorkspaceService, + serviceContainer: IServiceContainer, + appShell: IApplicationShell, + fs: IDataScienceFileSystem, + kernelSelector: KernelSelector, + interpreterService: IInterpreterService, + outputChannel: IOutputChannel + ): IJupyterServerInterface; +}; +// tslint:enable:callable-types + +// This class wraps either a HostJupyterServer or a GuestJupyterServer based on the liveshare state. It abstracts +// out the live share specific parts. +@injectable() +export class JupyterServerWrapper implements INotebookServer, ILiveShareHasRole { + private serverFactory: RoleBasedFactory; + + private launchInfo: INotebookServerLaunchInfo | undefined; + private _id: string = uuid(); + + constructor( + @inject(ILiveShareApi) liveShare: ILiveShareApi, + @inject(DataScienceStartupTime) startupTime: number, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, + @inject(IConfigurationService) configService: IConfigurationService, + @inject(IJupyterSessionManagerFactory) sessionManager: IJupyterSessionManagerFactory, + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IApplicationShell) appShell: IApplicationShell, + @inject(IDataScienceFileSystem) fs: IDataScienceFileSystem, + @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(KernelSelector) kernelSelector: KernelSelector, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) jupyterOutput: IOutputChannel, + @inject(IServiceContainer) serviceContainer: IServiceContainer + ) { + // The server factory will create the appropriate HostJupyterServer or GuestJupyterServer based on + // the liveshare state. + this.serverFactory = new RoleBasedFactory( + liveShare, + HostJupyterServer, + GuestJupyterServer, + liveShare, + startupTime, + asyncRegistry, + disposableRegistry, + configService, + sessionManager, + workspaceService, + serviceContainer, + appShell, + fs, + kernelSelector, + interpreterService, + jupyterOutput + ); + } + + public get role(): vsls.Role { + return this.serverFactory.role; + } + + public get id(): string { + return this._id; + } + + public async connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken): Promise { + this.launchInfo = launchInfo; + const server = await this.serverFactory.get(); + return server.connect(launchInfo, cancelToken); + } + + public async createNotebook( + resource: Resource, + identity: Uri, + notebookMetadata?: nbformat.INotebookMetadata, + cancelToken?: CancellationToken + ): Promise { + const server = await this.serverFactory.get(); + return server.createNotebook(resource, identity, notebookMetadata, cancelToken); + } + + public async shutdown(): Promise { + const server = await this.serverFactory.get(); + return server.shutdown(); + } + + public async dispose(): Promise { + const server = await this.serverFactory.get(); + return server.dispose(); + } + + // Return a copy of the connection information that this server used to connect with + public getConnectionInfo(): IJupyterConnection | undefined { + if (this.launchInfo) { + return this.launchInfo.connectionInfo; + } + + return undefined; + } + + public async getNotebook(resource: Uri, token?: CancellationToken): Promise { + const server = await this.serverFactory.get(); + return server.getNotebook(resource, token); + } + + public async waitForConnect(): Promise { + const server = await this.serverFactory.get(); + return server.waitForConnect(); + } +} diff --git a/src/client/datascience/jupyter/jupyterSession.ts b/src/client/datascience/jupyter/jupyterSession.ts new file mode 100644 index 000000000000..71d4309cdd88 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterSession.ts @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { + Contents, + ContentsManager, + Kernel, + ServerConnection, + Session, + SessionManager +} from '@jupyterlab/services'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { Cancellation } from '../../common/cancellation'; +import { traceError, traceInfo } from '../../common/logger'; +import { IOutputChannel } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { captureTelemetry } from '../../telemetry'; +import { BaseJupyterSession, JupyterSessionStartError } from '../baseJupyterSession'; +import { Telemetry } from '../constants'; +import { reportAction } from '../progress/decorator'; +import { ReportableAction } from '../progress/types'; +import { IJupyterConnection, ISessionWithSocket } from '../types'; +import { JupyterInvalidKernelError } from './jupyterInvalidKernelError'; +import { JupyterWebSockets } from './jupyterWebSocket'; +import { getNameOfKernelConnection } from './kernels/helpers'; +import { KernelConnectionMetadata } from './kernels/types'; + +export class JupyterSession extends BaseJupyterSession { + constructor( + private connInfo: IJupyterConnection, + private serverSettings: ServerConnection.ISettings, + kernelSpec: KernelConnectionMetadata | undefined, + private sessionManager: SessionManager, + private contentsManager: ContentsManager, + private readonly outputChannel: IOutputChannel, + private readonly restartSessionCreated: (id: Kernel.IKernelConnection) => void, + restartSessionUsed: (id: Kernel.IKernelConnection) => void, + readonly workingDirectory: string, + private readonly idleTimeout: number + ) { + super(restartSessionUsed, workingDirectory); + this.kernelConnectionMetadata = kernelSpec; + } + + @reportAction(ReportableAction.JupyterSessionWaitForIdleSession) + @captureTelemetry(Telemetry.WaitForIdleJupyter, undefined, true) + public waitForIdle(timeout: number): Promise { + // Wait for idle on this session + return this.waitForIdleOnSession(this.session, timeout); + } + + public async connect(timeoutMs: number, cancelToken?: CancellationToken): Promise { + if (!this.connInfo) { + throw new Error(localize.DataScience.sessionDisposed()); + } + + // Start a new session + this.setSession(await this.createNewKernelSession(this.kernelConnectionMetadata, timeoutMs, cancelToken)); + + // Listen for session status changes + this.session?.statusChanged.connect(this.statusHandler); // NOSONAR + + // Made it this far, we're connected now + this.connected = true; + } + + public async createNewKernelSession( + kernelConnection: KernelConnectionMetadata | undefined, + timeoutMS: number, + cancelToken?: CancellationToken + ): Promise { + let newSession: ISessionWithSocket | undefined; + + try { + // Don't immediately assume this kernel is valid. Try creating a session with it first. + if ( + kernelConnection && + kernelConnection.kind === 'connectToLiveKernel' && + kernelConnection.kernelModel.id + ) { + // Remote case. + newSession = this.sessionManager.connectTo(kernelConnection.kernelModel.session); + newSession.isRemoteSession = true; + } else { + newSession = await this.createSession(this.serverSettings, kernelConnection, cancelToken); + } + + // Make sure it is idle before we return + await this.waitForIdleOnSession(newSession, timeoutMS); + } catch (exc) { + traceError('Failed to change kernel', exc); + // Throw a new exception indicating we cannot change. + throw new JupyterInvalidKernelError(kernelConnection); + } + + return newSession; + } + + protected async createRestartSession( + kernelConnection: KernelConnectionMetadata | undefined, + session: ISessionWithSocket, + cancelToken?: CancellationToken + ): Promise { + // We need all of the above to create a restart session + if (!session || !this.contentsManager || !this.sessionManager) { + throw new Error(localize.DataScience.sessionDisposed()); + } + let result: ISessionWithSocket | undefined; + let tryCount = 0; + // tslint:disable-next-line: no-any + let exception: any; + while (tryCount < 3) { + try { + result = await this.createSession(session.serverSettings, kernelConnection, cancelToken); + await this.waitForIdleOnSession(result, this.idleTimeout); + this.restartSessionCreated(result.kernel); + return result; + } catch (exc) { + traceInfo(`Error waiting for restart session: ${exc}`); + tryCount += 1; + if (result) { + this.shutdownSession(result, undefined).ignoreErrors(); + } + result = undefined; + exception = exc; + } + } + throw exception; + } + + protected startRestartSession() { + if (!this.restartSessionPromise && this.session && this.contentsManager) { + this.restartSessionPromise = this.createRestartSession(this.kernelConnectionMetadata, this.session); + } + } + + private async createBackingFile(): Promise { + let backingFile: Contents.IModel; + + // First make sure the notebook is in the right relative path (jupyter expects a relative path with unix delimiters) + const relativeDirectory = path.relative(this.connInfo.rootDirectory, this.workingDirectory).replace(/\\/g, '/'); + + // However jupyter does not support relative paths outside of the original root. + const backingFileOptions: Contents.ICreateOptions = + this.connInfo.localLaunch && !relativeDirectory.startsWith('..') + ? { type: 'notebook', path: relativeDirectory } + : { type: 'notebook' }; + + try { + // Create a temporary notebook for this session. Each needs a unique name (otherwise we get the same session every time) + backingFile = await this.contentsManager.newUntitled(backingFileOptions); + const backingFileDir = path.dirname(backingFile.path); + backingFile = await this.contentsManager.rename( + backingFile.path, + backingFileDir.length && backingFileDir !== '.' + ? `${backingFileDir}/t-${uuid()}.ipynb` + : `t-${uuid()}.ipynb` // Note, the docs say the path uses UNIX delimiters. + ); + } catch (exc) { + // If it failed for local, try without a relative directory + if (this.connInfo.localLaunch) { + backingFile = await this.contentsManager.newUntitled({ type: 'notebook' }); + const backingFileDir = path.dirname(backingFile.path); + backingFile = await this.contentsManager.rename( + backingFile.path, + backingFileDir.length && backingFileDir !== '.' + ? `${backingFileDir}/t-${uuid()}.ipynb` + : `t-${uuid()}.ipynb` // Note, the docs say the path uses UNIX delimiters. + ); + } else { + throw exc; + } + } + + if (backingFile) { + return backingFile; + } + throw new Error(`Backing file cannot be generated for Jupyter connection`); + } + + private async createSession( + serverSettings: ServerConnection.ISettings, + kernelConnection: KernelConnectionMetadata | undefined, + cancelToken?: CancellationToken + ): Promise { + // Create our backing file for the notebook + const backingFile = await this.createBackingFile(); + + // Create our session options using this temporary notebook and our connection info + const options: Session.IOptions = { + path: backingFile.path, + kernelName: getNameOfKernelConnection(kernelConnection) || '', + name: uuid(), // This is crucial to distinguish this session from any other. + serverSettings: serverSettings + }; + + return Cancellation.race( + () => + this.sessionManager!.startNew(options) + .then(async (session) => { + this.logRemoteOutput( + localize.DataScience.createdNewKernel().format(this.connInfo.baseUrl, session.kernel.id) + ); + + // Add on the kernel sock information + // tslint:disable-next-line: no-any + (session as any).kernelSocketInformation = { + socket: JupyterWebSockets.get(session.kernel.id), + options: { + clientId: session.kernel.clientId, + id: session.kernel.id, + model: { ...session.kernel.model }, + userName: session.kernel.username + } + }; + + return session; + }) + .catch((ex) => Promise.reject(new JupyterSessionStartError(ex))) + .finally(() => { + if (this.connInfo) { + this.contentsManager.delete(backingFile.path).ignoreErrors(); + } + }), + cancelToken + ); + } + + private logRemoteOutput(output: string) { + if (this.connInfo && !this.connInfo.localLaunch) { + this.outputChannel.appendLine(output); + } + } +} diff --git a/src/client/datascience/jupyter/jupyterSessionManager.ts b/src/client/datascience/jupyter/jupyterSessionManager.ts new file mode 100644 index 000000000000..7b2ad64eb28e --- /dev/null +++ b/src/client/datascience/jupyter/jupyterSessionManager.ts @@ -0,0 +1,390 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { ContentsManager, Kernel, ServerConnection, Session, SessionManager } from '@jupyterlab/services'; +import { Agent as HttpsAgent } from 'https'; +import * as nodeFetch from 'node-fetch'; +import { EventEmitter } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { IApplicationShell } from '../../common/application/types'; + +import { traceError, traceInfo } from '../../common/logger'; +import { IConfigurationService, IOutputChannel, IPersistentState, IPersistentStateFactory } from '../../common/types'; +import { sleep } from '../../common/utils/async'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { + IJupyterConnection, + IJupyterKernel, + IJupyterKernelSpec, + IJupyterPasswordConnect, + IJupyterSession, + IJupyterSessionManager +} from '../types'; +import { createAuthorizingRequest } from './jupyterRequest'; +import { JupyterSession } from './jupyterSession'; +import { createJupyterWebSocket } from './jupyterWebSocket'; +import { createDefaultKernelSpec } from './kernels/helpers'; +import { JupyterKernelSpec } from './kernels/jupyterKernelSpec'; +import { KernelConnectionMetadata } from './kernels/types'; + +// Key for our insecure connection global state +const GlobalStateUserAllowsInsecureConnections = 'DataScienceAllowInsecureConnections'; + +// tslint:disable: no-any + +export class JupyterSessionManager implements IJupyterSessionManager { + private static secureServers = new Map>(); + private sessionManager: SessionManager | undefined; + private contentsManager: ContentsManager | undefined; + private connInfo: IJupyterConnection | undefined; + private serverSettings: ServerConnection.ISettings | undefined; + private _jupyterlab?: typeof import('@jupyterlab/services'); + private readonly userAllowsInsecureConnections: IPersistentState; + private restartSessionCreatedEvent = new EventEmitter(); + private restartSessionUsedEvent = new EventEmitter(); + private get jupyterlab(): typeof import('@jupyterlab/services') { + if (!this._jupyterlab) { + // tslint:disable-next-line: no-require-imports + this._jupyterlab = require('@jupyterlab/services'); + } + return this._jupyterlab!; + } + constructor( + private jupyterPasswordConnect: IJupyterPasswordConnect, + _config: IConfigurationService, + private failOnPassword: boolean | undefined, + private outputChannel: IOutputChannel, + private configService: IConfigurationService, + private readonly appShell: IApplicationShell, + private readonly stateFactory: IPersistentStateFactory + ) { + this.userAllowsInsecureConnections = this.stateFactory.createGlobalPersistentState( + GlobalStateUserAllowsInsecureConnections, + false + ); + } + + public get onRestartSessionCreated() { + return this.restartSessionCreatedEvent.event; + } + + public get onRestartSessionUsed() { + return this.restartSessionUsedEvent.event; + } + public async dispose() { + traceInfo(`Disposing session manager`); + try { + if (this.contentsManager) { + traceInfo('SessionManager - dispose contents manager'); + this.contentsManager.dispose(); + this.contentsManager = undefined; + } + if (this.sessionManager && !this.sessionManager.isDisposed) { + traceInfo('ShutdownSessionAndConnection - dispose session manager'); + // Make sure it finishes startup. + await Promise.race([sleep(10_000), this.sessionManager.ready]); + + // tslint:disable-next-line: no-any + const sessionManager = this.sessionManager as any; + this.sessionManager.dispose(); // Note, shutting down all will kill all kernels on the same connection. We don't want that. + this.sessionManager = undefined; + + // The session manager can actually be stuck in the context of a timer. Clear out the specs inside of + // it so the memory for the session is minimized. Otherwise functional tests can run out of memory + if (sessionManager._specs) { + sessionManager._specs = {}; + } + if (sessionManager._sessions && sessionManager._sessions.clear) { + sessionManager._sessions.clear(); + } + if (sessionManager._pollModels) { + this.clearPoll(sessionManager._pollModels); + } + if (sessionManager._pollSpecs) { + this.clearPoll(sessionManager._pollSpecs); + } + } + } catch (e) { + traceError(`Exception on session manager shutdown: `, e); + } finally { + traceInfo('Finished disposing jupyter session manager'); + } + } + + public getConnInfo(): IJupyterConnection { + return this.connInfo!; + } + + public async initialize(connInfo: IJupyterConnection): Promise { + this.connInfo = connInfo; + this.serverSettings = await this.getServerConnectSettings(connInfo); + this.sessionManager = new this.jupyterlab.SessionManager({ serverSettings: this.serverSettings }); + this.contentsManager = new this.jupyterlab.ContentsManager({ serverSettings: this.serverSettings }); + } + + public async getRunningSessions(): Promise { + if (!this.sessionManager) { + return []; + } + // Not refreshing will result in `running` returning an empty iterator. + await this.sessionManager.refreshRunning(); + + const sessions: Session.IModel[] = []; + const iterator = this.sessionManager.running(); + let session = iterator.next(); + + while (session) { + sessions.push(session); + session = iterator.next(); + } + + return sessions; + } + + public async getRunningKernels(): Promise { + const models = await this.jupyterlab.Kernel.listRunning(this.serverSettings); + // Remove duplicates. + const dup = new Set(); + return models + .map((m) => { + return { + id: m.id, + name: m.name, + lastActivityTime: m.last_activity ? new Date(Date.parse(m.last_activity.toString())) : new Date(), + numberOfConnections: m.connections ? parseInt(m.connections.toString(), 10) : 0 + }; + }) + .filter((item) => { + if (dup.has(item.id)) { + return false; + } + dup.add(item.id); + return true; + }); + } + + public async startNew( + kernelConnection: KernelConnectionMetadata | undefined, + workingDirectory: string, + cancelToken?: CancellationToken + ): Promise { + if (!this.connInfo || !this.sessionManager || !this.contentsManager || !this.serverSettings) { + throw new Error(localize.DataScience.sessionDisposed()); + } + // Create a new session and attempt to connect to it + const session = new JupyterSession( + this.connInfo, + this.serverSettings, + kernelConnection, + this.sessionManager, + this.contentsManager, + this.outputChannel, + this.restartSessionCreatedEvent.fire.bind(this.restartSessionCreatedEvent), + this.restartSessionUsedEvent.fire.bind(this.restartSessionUsedEvent), + workingDirectory, + this.configService.getSettings().datascience.jupyterLaunchTimeout + ); + try { + await session.connect(this.configService.getSettings().datascience.jupyterLaunchTimeout, cancelToken); + } finally { + if (!session.isConnected) { + await session.dispose(); + } + } + return session; + } + + public async getKernelSpecs(): Promise { + if (!this.connInfo || !this.sessionManager || !this.contentsManager) { + throw new Error(localize.DataScience.sessionDisposed()); + } + try { + // Fetch the list the session manager already knows about. Refreshing may not work. + const oldKernelSpecs = + this.sessionManager.specs && Object.keys(this.sessionManager.specs.kernelspecs).length + ? this.sessionManager.specs.kernelspecs + : {}; + + // Wait for the session to be ready + await Promise.race([sleep(10_000), this.sessionManager.ready]); + + // Ask the session manager to refresh its list of kernel specs. This might never + // come back so only wait for ten seconds. + await Promise.race([sleep(10_000), this.sessionManager.refreshSpecs()]); + + // Enumerate all of the kernel specs, turning each into a JupyterKernelSpec + const kernelspecs = + this.sessionManager.specs && Object.keys(this.sessionManager.specs.kernelspecs).length + ? this.sessionManager.specs.kernelspecs + : oldKernelSpecs; + const keys = Object.keys(kernelspecs); + if (keys && keys.length) { + return keys.map((k) => { + const spec = kernelspecs[k]; + return new JupyterKernelSpec(spec) as IJupyterKernelSpec; + }); + } else { + traceError(`SessionManager cannot enumerate kernelspecs. Returning default.`); + // If for some reason the session manager refuses to communicate, fall + // back to a default. This may not exist, but it's likely. + return [createDefaultKernelSpec()]; + } + } catch (e) { + traceError(`SessionManager:getKernelSpecs failure: `, e); + // For some reason this is failing. Just return nothing + return []; + } + } + + // tslint:disable-next-line: no-any + private clearPoll(poll: { _timeout: any }) { + try { + clearTimeout(poll._timeout); + } catch { + noop(); + } + } + + private async getServerConnectSettings(connInfo: IJupyterConnection): Promise { + let serverSettings: Partial = { + baseUrl: connInfo.baseUrl, + appUrl: '', + // A web socket is required to allow token authentication + wsUrl: connInfo.baseUrl.replace('http', 'ws') + }; + + // Before we connect, see if we are trying to make an insecure connection, if we are, warn the user + await this.secureConnectionCheck(connInfo); + + // Agent is allowed to be set on this object, but ts doesn't like it on RequestInit, so any + // tslint:disable-next-line:no-any + let requestInit: any = { cache: 'no-store', credentials: 'same-origin' }; + let cookieString; + // tslint:disable-next-line: no-any + let requestCtor: any = nodeFetch.Request; + + // If authorization header is provided, then we need to prevent jupyterlab services from + // writing the authorization header. + if (connInfo.getAuthHeader) { + requestCtor = createAuthorizingRequest(connInfo.getAuthHeader); + } + + // If no token is specified prompt for a password + if ((connInfo.token === '' || connInfo.token === 'null') && !connInfo.getAuthHeader) { + if (this.failOnPassword) { + throw new Error('Password request not allowed.'); + } + serverSettings = { ...serverSettings, token: '' }; + const pwSettings = await this.jupyterPasswordConnect.getPasswordConnectionInfo(connInfo.baseUrl); + if (pwSettings && pwSettings.requestHeaders) { + requestInit = { ...requestInit, headers: pwSettings.requestHeaders }; + cookieString = (pwSettings.requestHeaders as any).Cookie || ''; + + // Password may have overwritten the base url and token as well + if (pwSettings.remappedBaseUrl) { + (serverSettings as any).baseUrl = pwSettings.remappedBaseUrl; + (serverSettings as any).wsUrl = pwSettings.remappedBaseUrl.replace('http', 'ws'); + } + if (pwSettings.remappedToken) { + (serverSettings as any).token = pwSettings.remappedToken; + } + } else if (pwSettings) { + serverSettings = { ...serverSettings, token: connInfo.token }; + } else { + // Failed to get password info, notify the user + throw new Error(localize.DataScience.passwordFailure()); + } + } else { + serverSettings = { ...serverSettings, token: connInfo.token }; + } + + const allowUnauthorized = this.configService.getSettings(undefined).datascience + .allowUnauthorizedRemoteConnection; + // If this is an https connection and we want to allow unauthorized connections set that option on our agent + // we don't need to save the agent as the previous behaviour is just to create a temporary default agent when not specified + if (connInfo.baseUrl.startsWith('https') && allowUnauthorized) { + const requestAgent = new HttpsAgent({ rejectUnauthorized: false }); + requestInit = { ...requestInit, agent: requestAgent }; + } + + // This replaces the WebSocket constructor in jupyter lab services with our own implementation + // See _createSocket here: + // https://github.com/jupyterlab/jupyterlab/blob/cfc8ebda95e882b4ed2eefd54863bb8cdb0ab763/packages/services/src/kernel/default.ts + serverSettings = { + ...serverSettings, + init: requestInit, + WebSocket: createJupyterWebSocket( + cookieString, + allowUnauthorized, + connInfo.getAuthHeader + // tslint:disable-next-line:no-any + ) as any, + // Redefine fetch to our node-modules so it picks up the correct version. + // Typecasting as any works fine as long as all 3 of these are the same version + // tslint:disable-next-line:no-any + fetch: nodeFetch.default as any, + // tslint:disable-next-line:no-any + Request: requestCtor, + // tslint:disable-next-line:no-any + Headers: nodeFetch.Headers as any + }; + + traceInfo(`Creating server with settings : ${JSON.stringify(serverSettings)}`); + return this.jupyterlab.ServerConnection.makeSettings(serverSettings); + } + + // If connecting on HTTP without a token prompt the user that this connection may not be secure + private async insecureServerWarningPrompt(): Promise { + const insecureMessage = localize.DataScience.insecureSessionMessage(); + const insecureLabels = [ + localize.Common.bannerLabelYes(), + localize.Common.bannerLabelNo(), + localize.Common.doNotShowAgain() + ]; + const response = await this.appShell.showWarningMessage(insecureMessage, ...insecureLabels); + + switch (response) { + case localize.Common.bannerLabelYes(): + // On yes just proceed as normal + return true; + + case localize.Common.doNotShowAgain(): + // For don't ask again turn on the global true + await this.userAllowsInsecureConnections.updateValue(true); + return true; + + case localize.Common.bannerLabelNo(): + default: + // No or for no choice return back false to block + return false; + } + } + + // Check if our server connection is considered secure. If it is not, ask the user if they want to connect + // If not, throw to bail out on the process + private async secureConnectionCheck(connInfo: IJupyterConnection): Promise { + // If they have turned on global server trust then everything is secure + if (this.userAllowsInsecureConnections.value) { + return; + } + + // If they are local launch, https, or have a token, then they are secure + if (connInfo.localLaunch || connInfo.baseUrl.startsWith('https') || connInfo.token !== 'null') { + return; + } + + // At this point prompt the user, cache the promise so we don't ask multiple times for the same server + let serverSecurePromise = JupyterSessionManager.secureServers.get(connInfo.baseUrl); + + if (serverSecurePromise === undefined) { + serverSecurePromise = this.insecureServerWarningPrompt(); + JupyterSessionManager.secureServers.set(connInfo.baseUrl, serverSecurePromise); + } + + // If our server is not secure, throw here to bail out on the process + if (!(await serverSecurePromise)) { + throw new Error(localize.DataScience.insecureSessionDenied()); + } + } +} diff --git a/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts b/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts new file mode 100644 index 000000000000..25a991fb4572 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterSessionManagerFactory.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable, named } from 'inversify'; +import { IApplicationShell } from '../../common/application/types'; + +import type { Kernel } from '@jupyterlab/services'; +import { EventEmitter } from 'vscode'; +import { + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + IPersistentStateFactory +} from '../../common/types'; +import { JUPYTER_OUTPUT_CHANNEL } from '../constants'; +import { + IJupyterConnection, + IJupyterPasswordConnect, + IJupyterSessionManager, + IJupyterSessionManagerFactory +} from '../types'; +import { JupyterSessionManager } from './jupyterSessionManager'; + +@injectable() +export class JupyterSessionManagerFactory implements IJupyterSessionManagerFactory { + private restartSessionCreatedEvent = new EventEmitter(); + private restartSessionUsedEvent = new EventEmitter(); + constructor( + @inject(IJupyterPasswordConnect) private jupyterPasswordConnect: IJupyterPasswordConnect, + @inject(IConfigurationService) private config: IConfigurationService, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private jupyterOutput: IOutputChannel, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly stateFactory: IPersistentStateFactory, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry + ) {} + + /** + * Creates a new IJupyterSessionManager. + * @param connInfo - connection information to the server that's already running. + * @param failOnPassword - whether or not to fail the creation if a password is required. + */ + public async create(connInfo: IJupyterConnection, failOnPassword?: boolean): Promise { + const result = new JupyterSessionManager( + this.jupyterPasswordConnect, + this.config, + failOnPassword, + this.jupyterOutput, + this.config, + this.appShell, + this.stateFactory + ); + await result.initialize(connInfo); + this.disposableRegistry.push( + result.onRestartSessionCreated(this.restartSessionCreatedEvent.fire.bind(this.restartSessionCreatedEvent)) + ); + this.disposableRegistry.push( + result.onRestartSessionUsed(this.restartSessionUsedEvent.fire.bind(this.restartSessionUsedEvent)) + ); + return result; + } + + public get onRestartSessionCreated() { + return this.restartSessionCreatedEvent.event; + } + + public get onRestartSessionUsed() { + return this.restartSessionUsedEvent.event; + } +} diff --git a/src/client/datascience/jupyter/jupyterUtils.ts b/src/client/datascience/jupyter/jupyterUtils.ts new file mode 100644 index 000000000000..73054c152556 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterUtils.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { Uri } from 'vscode'; + +import { IWorkspaceService } from '../../common/application/types'; +import { Resource } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { SystemVariables } from '../../common/variables/systemVariables'; +import { getJupyterConnectionDisplayName } from '../jupyter/jupyterConnection'; +import { IJupyterConnection, IJupyterServerUri } from '../types'; + +export function expandWorkingDir( + workingDir: string | undefined, + launchingFile: string | undefined, + workspace: IWorkspaceService +): string { + if (workingDir) { + const variables = new SystemVariables( + launchingFile ? Uri.file(launchingFile) : undefined, + workspace.rootPath, + workspace + ); + return variables.resolve(workingDir); + } + + // No working dir, just use the path of the launching file. + if (launchingFile) { + return path.dirname(launchingFile); + } + + // No launching file or working dir. Just use the default workspace folder + const workspaceFolder = workspace.getWorkspaceFolder(undefined); + if (workspaceFolder) { + return workspaceFolder.uri.fsPath; + } + + return process.cwd(); +} + +export function createRemoteConnectionInfo( + uri: string, + getJupyterServerUri: (uri: string) => IJupyterServerUri | undefined +): IJupyterConnection { + let url: URL; + try { + url = new URL(uri); + } catch (err) { + // This should already have been parsed when set, so just throw if it's not right here + throw err; + } + + const serverUri = getJupyterServerUri(uri); + const baseUrl = serverUri ? serverUri.baseUrl : `${url.protocol}//${url.host}${url.pathname}`; + const token = serverUri ? serverUri.token : `${url.searchParams.get('token')}`; + const hostName = serverUri ? new URL(serverUri.baseUrl).hostname : url.hostname; + + return { + type: 'jupyter', + baseUrl, + token, + hostName, + localLaunch: false, + localProcExitCode: undefined, + valid: true, + displayName: + serverUri && serverUri.displayName + ? serverUri.displayName + : getJupyterConnectionDisplayName(token, baseUrl), + disconnected: (_l) => { + return { dispose: noop }; + }, + dispose: noop, + rootDirectory: '', + getAuthHeader: serverUri ? () => getJupyterServerUri(uri)?.authorizationHeader : undefined, + url: uri + }; +} + +export async function computeWorkingDirectory(resource: Resource, workspace: IWorkspaceService): Promise { + // Returning directly doesn't seem to work (typescript complains) + // tslint:disable-next-line: no-unnecessary-local-variable + const workingDirectory = + resource && resource.scheme === 'file' && (await fs.pathExists(path.dirname(resource.fsPath))) + ? path.dirname(resource.fsPath) + : workspace.getWorkspaceFolder(resource)?.uri.fsPath || process.cwd(); + + return workingDirectory; +} diff --git a/src/client/datascience/jupyter/jupyterVariables.ts b/src/client/datascience/jupyter/jupyterVariables.ts new file mode 100644 index 000000000000..a9cf9958a118 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterVariables.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { JSONObject } from '@phosphor/coreutils'; +import { inject, injectable, named } from 'inversify'; + +import { Event, EventEmitter } from 'vscode'; +import { ServerStatus } from '../../../datascience-ui/interactive-common/mainState'; +import { RunByLine } from '../../common/experiments/groups'; +import { IDisposableRegistry, IExperimentsManager } from '../../common/types'; +import { captureTelemetry } from '../../telemetry'; +import { Identifiers, Telemetry } from '../constants'; +import { + IConditionalJupyterVariables, + IJupyterVariable, + IJupyterVariables, + IJupyterVariablesRequest, + IJupyterVariablesResponse, + INotebook +} from '../types'; + +/** + * This class provides variable data for showing in the interactive window or a notebook. + * It multiplexes to either one that will use the jupyter kernel or one that uses the debugger. + */ +@injectable() +export class JupyterVariables implements IJupyterVariables { + private refreshEventEmitter = new EventEmitter(); + + constructor( + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IExperimentsManager) private experimentsManager: IExperimentsManager, + @inject(IJupyterVariables) @named(Identifiers.OLD_VARIABLES) private oldVariables: IJupyterVariables, + @inject(IJupyterVariables) @named(Identifiers.KERNEL_VARIABLES) private kernelVariables: IJupyterVariables, + @inject(IJupyterVariables) + @named(Identifiers.DEBUGGER_VARIABLES) + private debuggerVariables: IConditionalJupyterVariables + ) { + disposableRegistry.push(debuggerVariables.refreshRequired(this.fireRefresh.bind(this))); + disposableRegistry.push(kernelVariables.refreshRequired(this.fireRefresh.bind(this))); + disposableRegistry.push(oldVariables.refreshRequired(this.fireRefresh.bind(this))); + } + + public get refreshRequired(): Event { + return this.refreshEventEmitter.event; + } + + // IJupyterVariables implementation + @captureTelemetry(Telemetry.VariableExplorerFetchTime, undefined, true) + public async getVariables( + notebook: INotebook, + request: IJupyterVariablesRequest + ): Promise { + return this.getVariableHandler(notebook).getVariables(notebook, request); + } + + public getMatchingVariable(notebook: INotebook, name: string): Promise { + return this.getVariableHandler(notebook).getMatchingVariable(notebook, name); + } + + public async getDataFrameInfo(targetVariable: IJupyterVariable, notebook: INotebook): Promise { + return this.getVariableHandler(notebook).getDataFrameInfo(targetVariable, notebook); + } + + public async getDataFrameRows( + targetVariable: IJupyterVariable, + notebook: INotebook, + start: number, + end: number + ): Promise { + return this.getVariableHandler(notebook).getDataFrameRows(targetVariable, notebook, start, end); + } + + private getVariableHandler(notebook: INotebook): IJupyterVariables { + if (!this.experimentsManager.inExperiment(RunByLine.experiment)) { + return this.oldVariables; + } + if (this.debuggerVariables.active && notebook.status === ServerStatus.Busy) { + return this.debuggerVariables; + } + + return this.kernelVariables; + } + + private fireRefresh() { + this.refreshEventEmitter.fire(); + } +} diff --git a/src/client/datascience/jupyter/jupyterWaitForIdleError.ts b/src/client/datascience/jupyter/jupyterWaitForIdleError.ts new file mode 100644 index 000000000000..b02bab07b807 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterWaitForIdleError.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export class JupyterWaitForIdleError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/src/client/datascience/jupyter/jupyterWebSocket.ts b/src/client/datascience/jupyter/jupyterWebSocket.ts new file mode 100644 index 000000000000..f11ed9281294 --- /dev/null +++ b/src/client/datascience/jupyter/jupyterWebSocket.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as WebSocketWS from 'ws'; +import { traceError } from '../../common/logger'; +import { noop } from '../../common/utils/misc'; +import { KernelSocketWrapper } from '../kernelSocketWrapper'; +import { IKernelSocket } from '../types'; + +// tslint:disable: no-any +export const JupyterWebSockets = new Map(); // NOSONAR + +// We need to override the websocket that jupyter lab services uses to put in our cookie information +// Do this as a function so that we can pass in variables the the socket will have local access to +export function createJupyterWebSocket(cookieString?: string, allowUnauthorized?: boolean, getAuthHeaders?: () => any) { + class JupyterWebSocket extends KernelSocketWrapper(WebSocketWS) { + private kernelId: string | undefined; + private timer: NodeJS.Timeout | number; + + constructor(url: string, protocols?: string | string[] | undefined) { + let co: WebSocketWS.ClientOptions = {}; + let co_headers: { [key: string]: string } | undefined; + + if (allowUnauthorized) { + co = { ...co, rejectUnauthorized: false }; + } + + if (cookieString) { + co_headers = { Cookie: cookieString }; + } + + // Auth headers have to be refetched every time we create a connection. They may have expired + // since the last connection. + if (getAuthHeaders) { + const authorizationHeader = getAuthHeaders(); + co_headers = co_headers ? { ...co_headers, ...authorizationHeader } : authorizationHeader; + } + if (co_headers) { + co = { ...co, headers: co_headers }; + } + + super(url, protocols, co); + + // Parse the url for the kernel id + const parsed = /.*\/kernels\/(.*)\/.*/.exec(url); + if (parsed && parsed.length > 1) { + this.kernelId = parsed[1]; + } + if (this.kernelId) { + JupyterWebSockets.set(this.kernelId, this); + this.on('close', () => { + clearInterval(this.timer as any); + JupyterWebSockets.delete(this.kernelId!); + }); + } else { + traceError('KernelId not extracted from Kernel WebSocket URL'); + } + + // Ping the websocket connection every 30 seconds to make sure it stays alive + this.timer = setInterval(() => this.ping(noop), 30_000); + } + } + return JupyterWebSocket; +} diff --git a/src/client/datascience/jupyter/jupyterZMQBinariesNotFoundError.ts b/src/client/datascience/jupyter/jupyterZMQBinariesNotFoundError.ts new file mode 100644 index 000000000000..96a5e47107aa --- /dev/null +++ b/src/client/datascience/jupyter/jupyterZMQBinariesNotFoundError.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export class JupyterZMQBinariesNotFoundError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/src/client/datascience/jupyter/kernelVariables.ts b/src/client/datascience/jupyter/kernelVariables.ts new file mode 100644 index 000000000000..7d3d1b004bd3 --- /dev/null +++ b/src/client/datascience/jupyter/kernelVariables.ts @@ -0,0 +1,458 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { nbformat } from '@jupyterlab/coreutils'; +import { inject, injectable } from 'inversify'; +import stripAnsi from 'strip-ansi'; +import * as uuid from 'uuid/v4'; + +import { CancellationToken, Event, EventEmitter, Uri } from 'vscode'; +import { PYTHON_LANGUAGE } from '../../common/constants'; +import { traceError } from '../../common/logger'; +import { IConfigurationService, IDisposable } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { DataFrameLoading, Identifiers, Settings } from '../constants'; +import { + ICell, + IJupyterVariable, + IJupyterVariables, + IJupyterVariablesRequest, + IJupyterVariablesResponse, + INotebook +} from '../types'; +import { JupyterDataRateLimitError } from './jupyterDataRateLimitError'; +import { getKernelConnectionLanguage, isPythonKernelConnection } from './kernels/helpers'; + +// tslint:disable-next-line: no-var-requires no-require-imports + +// Regexes for parsing data from Python kernel. Not sure yet if other +// kernels will add the ansi encoding. +const TypeRegex = /.*?\[.*?;31mType:.*?\[0m\s+(\w+)/; +const ValueRegex = /.*?\[.*?;31mValue:.*?\[0m\s+(.*)/; +const StringFormRegex = /.*?\[.*?;31mString form:.*?\[0m\s*?([\s\S]+?)\n(.*\[.*;31m?)/; +const DocStringRegex = /.*?\[.*?;31mDocstring:.*?\[0m\s+(.*)/; +const CountRegex = /.*?\[.*?;31mLength:.*?\[0m\s+(.*)/; +const ShapeRegex = /^\s+\[(\d+) rows x (\d+) columns\]/m; + +const DataViewableTypes: Set = new Set(['DataFrame', 'list', 'dict', 'ndarray', 'Series']); + +interface INotebookState { + currentExecutionCount: number; + variables: IJupyterVariable[]; +} + +@injectable() +export class KernelVariables implements IJupyterVariables { + private importedDataFrameScripts = new Map(); + private languageToQueryMap = new Map(); + private notebookState = new Map(); + private refreshEventEmitter = new EventEmitter(); + + constructor(@inject(IConfigurationService) private configService: IConfigurationService) {} + + public get refreshRequired(): Event { + return this.refreshEventEmitter.event; + } + + // IJupyterVariables implementation + public async getVariables( + notebook: INotebook, + request: IJupyterVariablesRequest + ): Promise { + // Run the language appropriate variable fetch + return this.getVariablesBasedOnKernel(notebook, request); + } + + public async getMatchingVariable( + notebook: INotebook, + name: string, + token?: CancellationToken + ): Promise { + // See if in the cache + const cache = this.notebookState.get(notebook.identity); + if (cache) { + let match = cache.variables.find((v) => v.name === name); + if (match && !match.value) { + match = await this.getVariableValueFromKernel(match, notebook, token); + } + return match; + } else { + // No items in the cache yet, just ask for the names + const names = await this.getVariableNamesFromKernel(notebook, token); + if (names) { + const matchName = names.find((n) => n === name); + if (matchName) { + return this.getVariableValueFromKernel( + { + name, + value: undefined, + supportsDataExplorer: false, + type: '', + size: 0, + count: 0, + shape: '', + truncated: true + }, + notebook, + token + ); + } + } + } + } + + public async getDataFrameInfo(targetVariable: IJupyterVariable, notebook: INotebook): Promise { + // Import the data frame script directory if we haven't already + await this.importDataFrameScripts(notebook); + + // Then execute a call to get the info and turn it into JSON + const results = await notebook.execute( + `print(${DataFrameLoading.DataFrameInfoFunc}(${targetVariable.name}))`, + Identifiers.EmptyFileName, + 0, + uuid(), + undefined, + true + ); + + // Combine with the original result (the call only returns the new fields) + return { + ...targetVariable, + ...this.deserializeJupyterResult(results) + }; + } + + public async getDataFrameRows( + targetVariable: IJupyterVariable, + notebook: INotebook, + start: number, + end: number + ): Promise<{}> { + // Import the data frame script directory if we haven't already + await this.importDataFrameScripts(notebook); + + if (targetVariable.rowCount) { + end = Math.min(end, targetVariable.rowCount); + } + + // Then execute a call to get the rows and turn it into JSON + const results = await notebook.execute( + `print(${DataFrameLoading.DataFrameRowFunc}(${targetVariable.name}, ${start}, ${end}))`, + Identifiers.EmptyFileName, + 0, + uuid(), + undefined, + true + ); + return this.deserializeJupyterResult(results); + } + + private async importDataFrameScripts(notebook: INotebook, token?: CancellationToken): Promise { + const key = notebook.identity.toString(); + if (!this.importedDataFrameScripts.get(key)) { + // Clear our flag if the notebook disposes or restarts + const disposables: IDisposable[] = []; + const handler = () => { + this.importedDataFrameScripts.delete(key); + disposables.forEach((d) => d.dispose()); + }; + disposables.push(notebook.onDisposed(handler)); + disposables.push(notebook.onKernelChanged(handler)); + disposables.push(notebook.onKernelRestarted(handler)); + + const fullCode = `${DataFrameLoading.DataFrameSysImport}\n${DataFrameLoading.DataFrameInfoImport}\n${DataFrameLoading.DataFrameRowImport}\n${DataFrameLoading.VariableInfoImport}`; + await notebook.execute(fullCode, Identifiers.EmptyFileName, 0, uuid(), token, true); + this.importedDataFrameScripts.set(notebook.identity.toString(), true); + } + } + + private async getFullVariable( + targetVariable: IJupyterVariable, + notebook: INotebook, + token?: CancellationToken + ): Promise { + // Import the data frame script directory if we haven't already + await this.importDataFrameScripts(notebook, token); + + // Then execute a call to get the info and turn it into JSON + const results = await notebook.execute( + `print(${DataFrameLoading.VariableInfoFunc}(${targetVariable.name}))`, + Identifiers.EmptyFileName, + 0, + uuid(), + token, + true + ); + + // Combine with the original result (the call only returns the new fields) + return { + ...targetVariable, + ...this.deserializeJupyterResult(results) + }; + } + + private extractJupyterResultText(cells: ICell[]): string { + // Verify that we have the correct cell type and outputs + if (cells.length > 0 && cells[0].data) { + const codeCell = cells[0].data as nbformat.ICodeCell; + if (codeCell.outputs.length > 0) { + const codeCellOutput = codeCell.outputs[0] as nbformat.IOutput; + if ( + codeCellOutput && + codeCellOutput.output_type === 'stream' && + codeCellOutput.name === 'stderr' && + codeCellOutput.hasOwnProperty('text') + ) { + const resultString = codeCellOutput.text as string; + // See if this the IOPUB data rate limit problem + if (resultString.includes('iopub_data_rate_limit')) { + throw new JupyterDataRateLimitError(); + } else { + const error = localize.DataScience.jupyterGetVariablesExecutionError().format(resultString); + traceError(error); + throw new Error(error); + } + } + if (codeCellOutput && codeCellOutput.output_type === 'execute_result') { + const data = codeCellOutput.data; + if (data && data.hasOwnProperty('text/plain')) { + // tslint:disable-next-line:no-any + return (data as any)['text/plain']; + } + } + if ( + codeCellOutput && + codeCellOutput.output_type === 'stream' && + codeCellOutput.hasOwnProperty('text') + ) { + return codeCellOutput.text as string; + } + if ( + codeCellOutput && + codeCellOutput.output_type === 'error' && + codeCellOutput.hasOwnProperty('traceback') + ) { + const traceback: string[] = codeCellOutput.traceback as string[]; + const stripped = traceback.map(stripAnsi).join('\r\n'); + const error = localize.DataScience.jupyterGetVariablesExecutionError().format(stripped); + traceError(error); + throw new Error(error); + } + } + } + + throw new Error(localize.DataScience.jupyterGetVariablesBadResults()); + } + + // Pull our text result out of the Jupyter cell + private deserializeJupyterResult(cells: ICell[]): T { + const text = this.extractJupyterResultText(cells); + return JSON.parse(text) as T; + } + + private getParser(notebook: INotebook) { + // Figure out kernel language + const language = getKernelConnectionLanguage(notebook?.getKernelConnection()) || PYTHON_LANGUAGE; + + // We may have cached this information + let result = this.languageToQueryMap.get(language); + if (!result) { + let query = this.configService + .getSettings(notebook.resource) + .datascience.variableQueries.find((v) => v.language === language); + if (!query && language === PYTHON_LANGUAGE) { + query = Settings.DefaultVariableQuery; + } + + // Use the query to generate our regex + if (query) { + result = { + query: query.query, + parser: new RegExp(query.parseExpr, 'g') + }; + this.languageToQueryMap.set(language, result); + } + } + + return result; + } + + private getAllMatches(regex: RegExp, text: string): string[] { + const result: string[] = []; + let m: RegExpExecArray | null = null; + // tslint:disable-next-line: no-conditional-assignment + while ((m = regex.exec(text)) !== null) { + if (m.index === regex.lastIndex) { + regex.lastIndex += 1; + } + if (m.length > 1) { + result.push(m[1]); + } + } + // Rest after searching + regex.lastIndex = -1; + return result; + } + + private async getVariablesBasedOnKernel( + notebook: INotebook, + request: IJupyterVariablesRequest + ): Promise { + // See if we already have the name list + let list = this.notebookState.get(notebook.identity); + if (!list || list.currentExecutionCount !== request.executionCount) { + // Refetch the list of names from the notebook. They might have changed. + list = { + currentExecutionCount: request.executionCount, + variables: (await this.getVariableNamesFromKernel(notebook)).map((n) => { + return { + name: n, + value: undefined, + supportsDataExplorer: false, + type: '', + size: 0, + shape: '', + count: 0, + truncated: true + }; + }) + }; + } + + const exclusionList = this.configService.getSettings(notebook.resource).datascience.variableExplorerExclude + ? this.configService.getSettings().datascience.variableExplorerExclude?.split(';') + : []; + + const result: IJupyterVariablesResponse = { + executionCount: request.executionCount, + pageStartIndex: -1, + pageResponse: [], + totalCount: 0, + refreshCount: request.refreshCount + }; + + // Use the list of names to fetch the page of data + if (list) { + const startPos = request.startIndex ? request.startIndex : 0; + const chunkSize = request.pageSize ? request.pageSize : 100; + result.pageStartIndex = startPos; + + // Do one at a time. All at once doesn't work as they all have to wait for each other anyway + for (let i = startPos; i < startPos + chunkSize && i < list.variables.length; ) { + const fullVariable = list.variables[i].value + ? list.variables[i] + : await this.getVariableValueFromKernel(list.variables[i], notebook); + + // See if this is excluded or not. + if (exclusionList && exclusionList.indexOf(fullVariable.type) >= 0) { + // Not part of our actual list. Remove from the real list too + list.variables.splice(i, 1); + } else { + list.variables[i] = fullVariable; + result.pageResponse.push(fullVariable); + i += 1; + } + } + + // Save in our cache + this.notebookState.set(notebook.identity, list); + + // Update total count (exclusions will change this as types are computed) + result.totalCount = list.variables.length; + } + + return result; + } + + private async getVariableNamesFromKernel(notebook: INotebook, token?: CancellationToken): Promise { + // Get our query and parser + const query = this.getParser(notebook); + + // Now execute the query + if (notebook && query) { + const cells = await notebook.execute(query.query, Identifiers.EmptyFileName, 0, uuid(), token, true); + const text = this.extractJupyterResultText(cells); + + // Apply the expression to it + const matches = this.getAllMatches(query.parser, text); + + // Turn each match into a value + if (matches) { + return matches; + } + } + + return []; + } + + private async getVariableValueFromKernel( + targetVariable: IJupyterVariable, + notebook: INotebook, + token?: CancellationToken + ): Promise { + let result = { ...targetVariable }; + if (notebook) { + const output = await notebook.inspect(targetVariable.name, 0, token); + + // Should be a text/plain inside of it (at least IPython does this) + if (output && output.hasOwnProperty('text/plain')) { + // tslint:disable-next-line: no-any + const text = (output as any)['text/plain'].toString(); + + // Parse into bits + const type = TypeRegex.exec(text); + const value = ValueRegex.exec(text); + const stringForm = StringFormRegex.exec(text); + const docString = DocStringRegex.exec(text); + const count = CountRegex.exec(text); + const shape = ShapeRegex.exec(text); + if (type) { + result.type = type[1]; + } + if (value) { + result.value = value[1]; + } else if (stringForm) { + result.value = stringForm[1]; + } else if (docString) { + result.value = docString[1]; + } else { + result.value = ''; + } + if (count) { + result.count = parseInt(count[1], 10); + } + if (shape) { + result.shape = `(${shape[1]}, ${shape[2]})`; + } + } + + // Otherwise look for the appropriate entries + if (output.type) { + result.type = output.type.toString(); + } + if (output.value) { + result.value = output.value.toString(); + } + + // Determine if supports viewing based on type + if (DataViewableTypes.has(result.type)) { + result.supportsDataExplorer = true; + } + } + + // For a python kernel, we might be able to get a better shape. It seems the 'inspect' request doesn't always return it. + // Do this only when necessary as this is a LOT slower than an inspect request. Like 4 or 5 times as slow + if ( + result.type && + result.count && + !result.shape && + isPythonKernelConnection(notebook.getKernelConnection()) && + result.supportsDataExplorer && + result.type !== 'list' // List count is good enough + ) { + result = await this.getFullVariable(result, notebook); + } + + return result; + } +} diff --git a/src/client/datascience/jupyter/kernels/cellExecution.ts b/src/client/datascience/jupyter/kernels/cellExecution.ts new file mode 100644 index 000000000000..5d55da0116fa --- /dev/null +++ b/src/client/datascience/jupyter/kernels/cellExecution.ts @@ -0,0 +1,528 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { nbformat } from '@jupyterlab/coreutils'; +import type { KernelMessage } from '@jupyterlab/services/lib/kernel/messages'; +import { CancellationToken, CellOutputKind, CellStreamOutput, NotebookCell, NotebookCellRunState } from 'vscode'; +import { concatMultilineString, formatStreamText } from '../../../../datascience-ui/common'; +import { IApplicationShell } from '../../../common/application/types'; +import { traceInfo, traceWarning } from '../../../common/logger'; +import { RefBool } from '../../../common/refBool'; +import { createDeferred } from '../../../common/utils/async'; +import { noop } from '../../../common/utils/misc'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { Telemetry } from '../../constants'; +import { updateCellExecutionCount, updateCellWithErrorStatus } from '../../notebook/helpers/executionHelpers'; +import { + cellOutputToVSCCellOutput, + clearCellForExecution, + getCellStatusMessageBasedOnFirstCellErrorOutput, + updateCellExecutionTimes +} from '../../notebook/helpers/helpers'; +import { MultiCancellationTokenSource } from '../../notebook/helpers/multiCancellationToken'; +import { NotebookEditor } from '../../notebook/notebookEditor'; +import { INotebookContentProvider } from '../../notebook/types'; +import { + IDataScienceErrorHandler, + IJupyterSession, + INotebook, + INotebookEditorProvider, + INotebookExecutionLogger +} from '../../types'; +import { IKernel } from './types'; +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); + +export class CellExecutionFactory { + constructor( + private readonly contentProvider: INotebookContentProvider, + private readonly errorHandler: IDataScienceErrorHandler, + private readonly editorProvider: INotebookEditorProvider, + private readonly appShell: IApplicationShell + ) {} + + public create(cell: NotebookCell) { + // tslint:disable-next-line: no-use-before-declare + return CellExecution.fromCell( + cell, + this.contentProvider, + this.errorHandler, + this.editorProvider, + this.appShell + ); + } +} +/** + * Responsible for execution of an individual cell and manages the state of the cell as it progresses through the execution phases. + * Execution phases include - enqueue for execution (done in ctor), start execution, completed execution with/without errors, cancel execution or dequeue. + */ +export class CellExecution { + public get result(): Promise { + return this._result.promise; + } + + public get token(): CancellationToken { + return this.source.token; + } + + public get completed() { + return this._completed; + } + + private static sentExecuteCellTelemetry?: boolean; + + private readonly oldCellRunState?: NotebookCellRunState; + + private stopWatch = new StopWatch(); + + private readonly source = new MultiCancellationTokenSource(); + + private readonly _result = createDeferred(); + + private started?: boolean; + + private _completed?: boolean; + + private constructor( + public readonly cell: NotebookCell, + private readonly contentProvider: INotebookContentProvider, + private readonly errorHandler: IDataScienceErrorHandler, + private readonly editorProvider: INotebookEditorProvider, + private readonly applicationService: IApplicationShell + ) { + this.oldCellRunState = cell.metadata.runState; + this.enqueue(); + } + + public static fromCell( + cell: NotebookCell, + contentProvider: INotebookContentProvider, + errorHandler: IDataScienceErrorHandler, + editorProvider: INotebookEditorProvider, + appService: IApplicationShell + ) { + return new CellExecution(cell, contentProvider, errorHandler, editorProvider, appService); + } + + public start(kernelPromise: Promise, notebook: INotebook) { + this.started = true; + // Ensure we clear the cell state and trigger a change. + clearCellForExecution(this.cell); + this.cell.metadata.runStartTime = new Date().getTime(); + this.stopWatch.reset(); + // Changes to metadata must be saved in ipynb, hence mark doc has dirty. + this.contentProvider.notifyChangesToDocument(this.cell.notebook); + this.notifyCellExecution(); + + // Begin the request that will modify our cell. + kernelPromise + .then((_k) => { + this.execute(notebook.session, notebook.getLoggers()); + }) + .catch((e) => { + this.completedWithErrors(e); + }); + } + + /** + * Cancel execution. + * If execution has commenced, then interrupt (via cancellation token) else dequeue from execution. + */ + public cancel() { + // We need to notify cancellation only if execution is in progress, + // coz if not, we can safely reset the states. + if (this.started && !this._completed) { + this.source.cancel(); + } + + if (!this.started) { + this.dequeue(); + } + this._result.resolve(this.cell.metadata.runState); + } + + private completedWithErrors(error: Partial) { + this.sendPerceivedCellExecute(); + this.cell.metadata.lastRunDuration = this.stopWatch.elapsedTime; + updateCellWithErrorStatus(this.cell, error); + this.contentProvider.notifyChangesToDocument(this.cell.notebook); + this.errorHandler.handleError((error as unknown) as Error).ignoreErrors(); + + this._completed = true; + this._result.resolve(this.cell.metadata.runState); + // Changes to metadata must be saved in ipynb, hence mark doc has dirty. + this.contentProvider.notifyChangesToDocument(this.cell.notebook); + } + + private completedSuccessfully() { + this.sendPerceivedCellExecute(); + // If we requested a cancellation, then assume it did not even run. + // If it did, then we'd get an interrupt error in the output. + this.cell.metadata.runState = this.token.isCancellationRequested + ? vscodeNotebookEnums.NotebookCellRunState.Idle + : vscodeNotebookEnums.NotebookCellRunState.Success; + + this.cell.metadata.statusMessage = ''; + this.cell.metadata.lastRunDuration = this.stopWatch.elapsedTime; + updateCellExecutionTimes(this.cell, { + startTime: this.cell.metadata.runStartTime, + duration: this.cell.metadata.lastRunDuration + }); + // If there are any errors in the cell, then change status to error. + if (this.cell.outputs.some((output) => output.outputKind === vscodeNotebookEnums.CellOutputKind.Error)) { + this.cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Error; + this.cell.metadata.statusMessage = getCellStatusMessageBasedOnFirstCellErrorOutput(this.cell.outputs); + } + + this._completed = true; + this._result.resolve(this.cell.metadata.runState); + // Changes to metadata must be saved in ipynb, hence mark doc has dirty. + this.contentProvider.notifyChangesToDocument(this.cell.notebook); + } + + /** + * Notify other parts of extension about the cell execution. + */ + private notifyCellExecution() { + const editor = this.editorProvider.editors.find((e) => e.file.toString() === this.cell.notebook.uri.toString()); + if (!editor) { + throw new Error('No editor for Model'); + } + if (editor && !(editor instanceof NotebookEditor)) { + throw new Error('Executing Notebook with another Editor'); + } + editor.notifyExecution(this.cell.document.getText()); + } + + /** + * This cell will no longer be processed for execution (even though it was meant to be). + * At this point we revert cell state & indicate that it has nto started & it is not busy. + */ + private dequeue() { + if (this.oldCellRunState === vscodeNotebookEnums.NotebookCellRunState.Running) { + this.cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Idle; + } else { + this.cell.metadata.runState = this.oldCellRunState; + } + this.cell.metadata.runStartTime = undefined; + this._completed = true; + this._result.resolve(this.cell.metadata.runState); + // Changes to metadata must be saved in ipynb, hence mark doc has dirty. + this.contentProvider.notifyChangesToDocument(this.cell.notebook); + } + + /** + * Place in queue for execution with kernel. + * (mark it as busy). + */ + private enqueue() { + this.cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Running; + this.contentProvider.notifyChangesToDocument(this.cell.notebook); + } + + private sendPerceivedCellExecute() { + const props = { notebook: true }; + if (!CellExecution.sentExecuteCellTelemetry) { + CellExecution.sentExecuteCellTelemetry = true; + sendTelemetryEvent(Telemetry.ExecuteCellPerceivedCold, this.stopWatch.elapsedTime, props); + } else { + sendTelemetryEvent(Telemetry.ExecuteCellPerceivedWarm, this.stopWatch.elapsedTime, props); + } + } + + private execute(session: IJupyterSession, loggers: INotebookExecutionLogger[]) { + // Generate metadata from our cell (some kernels expect this.) + const metadata = { + ...this.cell.metadata, + ...{ cellId: this.cell.uri.toString() } + }; + + // Create our initial request + const code = this.cell.document.getText(); + + // Skip if no code to execute + if (code.trim().length > 0) { + const request = session.requestExecute( + { + code, + silent: false, + stop_on_error: false, + allow_stdin: true, + store_history: true // Silent actually means don't output anything. Store_history is what affects execution_count + }, + false, + metadata + ); + + // Listen to messages and update our cell execution state appropriately + + // Keep track of our clear state + const clearState = new RefBool(false); + + // Listen to the reponse messages and update state as we go + if (request) { + // Stop handling the request if the subscriber is canceled. + const cancelDisposable = this.token.onCancellationRequested(() => { + request.onIOPub = noop; + request.onStdin = noop; + request.onReply = noop; + }); + + // Listen to messages. + request.onIOPub = this.handleIOPub.bind(this, clearState, loggers); + request.onStdin = this.handleInputRequest.bind(this, session); + request.onReply = this.handleReply.bind(this, clearState); + + // When the request finishes we are done + request.done + .then(() => this.completedSuccessfully()) + .catch((e) => { + // @jupyterlab/services throws a `Canceled` error when the kernel is interrupted. + // Such an error must be ignored. + if (e && e instanceof Error && e.message === 'Canceled') { + this.completedSuccessfully(); + } else { + this.completedWithErrors(e); + } + }) + .finally(() => { + cancelDisposable.dispose(); + }) + .ignoreErrors(); + } else { + this.completedWithErrors(new Error('Session cannot generate requrests')); + } + } else { + this.completedSuccessfully(); + } + } + + private handleIOPub( + clearState: RefBool, + loggers: INotebookExecutionLogger[], + msg: KernelMessage.IIOPubMessage + // tslint:disable-next-line: no-any + ) { + // Let our loggers get a first crack at the message. They may change it + loggers.forEach((f) => (msg = f.preHandleIOPub ? f.preHandleIOPub(msg) : msg)); + + // tslint:disable-next-line:no-require-imports + const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); + + // Keep track of we need to send an update to VS code or not. + let shouldUpdate = true; + try { + if (jupyterLab.KernelMessage.isExecuteResultMsg(msg)) { + this.handleExecuteResult(msg as KernelMessage.IExecuteResultMsg, clearState); + } else if (jupyterLab.KernelMessage.isExecuteInputMsg(msg)) { + this.handleExecuteInput(msg as KernelMessage.IExecuteInputMsg, clearState); + } else if (jupyterLab.KernelMessage.isStatusMsg(msg)) { + // Status is handled by the result promise. While it is running we are active. Otherwise we're stopped. + // So ignore status messages. + const statusMsg = msg as KernelMessage.IStatusMsg; + shouldUpdate = false; + this.handleStatusMessage(statusMsg, clearState); + } else if (jupyterLab.KernelMessage.isStreamMsg(msg)) { + this.handleStreamMesssage(msg as KernelMessage.IStreamMsg, clearState); + } else if (jupyterLab.KernelMessage.isDisplayDataMsg(msg)) { + this.handleDisplayData(msg as KernelMessage.IDisplayDataMsg, clearState); + } else if (jupyterLab.KernelMessage.isUpdateDisplayDataMsg(msg)) { + // No new data to update UI, hence do not send updates. + shouldUpdate = false; + } else if (jupyterLab.KernelMessage.isClearOutputMsg(msg)) { + this.handleClearOutput(msg as KernelMessage.IClearOutputMsg, clearState); + } else if (jupyterLab.KernelMessage.isErrorMsg(msg)) { + this.handleError(msg as KernelMessage.IErrorMsg, clearState); + } else if (jupyterLab.KernelMessage.isCommOpenMsg(msg)) { + // No new data to update UI, hence do not send updates. + shouldUpdate = false; + } else if (jupyterLab.KernelMessage.isCommMsgMsg(msg)) { + // No new data to update UI, hence do not send updates. + shouldUpdate = false; + } else if (jupyterLab.KernelMessage.isCommCloseMsg(msg)) { + // No new data to update UI, hence do not send updates. + shouldUpdate = false; + } else { + traceWarning(`Unknown message ${msg.header.msg_type} : hasData=${'data' in msg.content}`); + } + + // Set execution count, all messages should have it + if ('execution_count' in msg.content && typeof msg.content.execution_count === 'number') { + if (updateCellExecutionCount(this.cell, msg.content.execution_count)) { + shouldUpdate = true; + } + } + + // Show our update if any new output. + if (shouldUpdate) { + this.contentProvider.notifyChangesToDocument(this.cell.notebook); + } + } catch (err) { + // If not a restart error, then tell the subscriber + this.completedWithErrors(err); + } + } + + private addToCellData( + output: + | nbformat.IUnrecognizedOutput + | nbformat.IExecuteResult + | nbformat.IDisplayData + | nbformat.IStream + | nbformat.IError, + clearState: RefBool + ) { + const converted = cellOutputToVSCCellOutput(output); + + // Clear if necessary + if (clearState.value) { + this.cell.outputs = []; + clearState.update(false); + } + + // Append to the data (we would push here but VS code requires a recreation of the array) + this.cell.outputs = [...this.cell.outputs, converted]; + } + + private handleInputRequest(session: IJupyterSession, msg: KernelMessage.IStdinMessage) { + // Ask the user for input + if (msg.content && 'prompt' in msg.content) { + const hasPassword = msg.content.password !== null && (msg.content.password as boolean); + this.applicationService + .showInputBox({ + prompt: msg.content.prompt ? msg.content.prompt.toString() : '', + ignoreFocusOut: true, + password: hasPassword + }) + .then((v) => { + session.sendInputReply(v || ''); + }); + } + } + + // See this for docs on the messages: + // https://jupyter-client.readthedocs.io/en/latest/messaging.html#messaging-in-jupyter + private handleExecuteResult(msg: KernelMessage.IExecuteResultMsg, clearState: RefBool) { + this.addToCellData( + { + output_type: 'execute_result', + data: msg.content.data, + metadata: msg.content.metadata, + // tslint:disable-next-line: no-any + transient: msg.content.transient as any, // NOSONAR + execution_count: msg.content.execution_count + }, + clearState + ); + } + + private handleExecuteReply(msg: KernelMessage.IExecuteReplyMsg, clearState: RefBool) { + const reply = msg.content as KernelMessage.IExecuteReply; + if (reply.payload) { + reply.payload.forEach((o) => { + if (o.data && o.data.hasOwnProperty('text/plain')) { + this.addToCellData( + { + // Mark as stream output so the text is formatted because it likely has ansi codes in it. + output_type: 'stream', + // tslint:disable-next-line: no-any + text: (o.data as any)['text/plain'].toString(), + metadata: {}, + execution_count: reply.execution_count + }, + clearState + ); + } + }); + } + } + + private handleExecuteInput(msg: KernelMessage.IExecuteInputMsg, _clearState: RefBool) { + if (msg.content.execution_count) { + updateCellExecutionCount(this.cell, msg.content.execution_count); + } + } + + private handleStatusMessage(msg: KernelMessage.IStatusMsg, _clearState: RefBool) { + traceInfo(`Kernel switching to ${msg.content.execution_state}`); + } + + private handleStreamMesssage(msg: KernelMessage.IStreamMsg, clearState: RefBool) { + // Clear output if waiting for a clear + if (clearState.value) { + this.cell.outputs = []; + clearState.update(false); + } + + // Might already have a stream message. If so, just add on to it. + const lastOutput = this.cell.outputs.length > 0 ? this.cell.outputs[this.cell.outputs.length - 1] : undefined; + const existing: CellStreamOutput | undefined = + lastOutput && lastOutput.outputKind === CellOutputKind.Text ? lastOutput : undefined; + if (existing) { + // tslint:disable-next-line:restrict-plus-operands + existing.text = formatStreamText(concatMultilineString(existing.text + msg.content.text)); + this.cell.outputs = [...this.cell.outputs]; // This is necessary to get VS code to update (for now) + } else { + const originalText = formatStreamText(concatMultilineString(msg.content.text)); + // Create a new stream entry + const output: nbformat.IStream = { + output_type: 'stream', + name: msg.content.name, + text: originalText + }; + this.cell.outputs = [...this.cell.outputs, cellOutputToVSCCellOutput(output)]; + } + } + + private handleDisplayData(msg: KernelMessage.IDisplayDataMsg, clearState: RefBool) { + const output: nbformat.IDisplayData = { + output_type: 'display_data', + data: msg.content.data, + metadata: msg.content.metadata, + // tslint:disable-next-line: no-any + transient: msg.content.transient as any // NOSONAR + }; + this.addToCellData(output, clearState); + } + + private handleClearOutput(msg: KernelMessage.IClearOutputMsg, clearState: RefBool) { + // If the message says wait, add every message type to our clear state. This will + // make us wait for this type of output before we clear it. + if (msg && msg.content.wait) { + clearState.update(true); + } else { + // Clear all outputs and start over again. + this.cell.outputs = []; + } + } + + private handleError(msg: KernelMessage.IErrorMsg, clearState: RefBool) { + const output: nbformat.IError = { + output_type: 'error', + ename: msg.content.ename, + evalue: msg.content.evalue, + traceback: msg.content.traceback + }; + this.addToCellData(output, clearState); + } + + private handleReply(clearState: RefBool, msg: KernelMessage.IShellControlMessage) { + // tslint:disable-next-line:no-require-imports + const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); + + if (jupyterLab.KernelMessage.isExecuteReplyMsg(msg)) { + this.handleExecuteReply(msg, clearState); + + // Set execution count, all messages should have it + if ('execution_count' in msg.content && typeof msg.content.execution_count === 'number') { + updateCellExecutionCount(this.cell, msg.content.execution_count); + } + + // Send this event. + this.contentProvider.notifyChangesToDocument(this.cell.notebook); + } + } +} diff --git a/src/client/datascience/jupyter/kernels/helpers.ts b/src/client/datascience/jupyter/kernels/helpers.ts new file mode 100644 index 000000000000..9dfc592c8336 --- /dev/null +++ b/src/client/datascience/jupyter/kernels/helpers.ts @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import type { Kernel } from '@jupyterlab/services'; +import * as fastDeepEqual from 'fast-deep-equal'; +import { IJupyterKernelSpec } from '../../types'; +import { JupyterKernelSpec } from './jupyterKernelSpec'; +// tslint:disable-next-line: no-var-requires no-require-imports +const NamedRegexp = require('named-js-regexp') as typeof import('named-js-regexp'); + +// tslint:disable-next-line: no-require-imports +import cloneDeep = require('lodash/cloneDeep'); +import { PYTHON_LANGUAGE } from '../../../common/constants'; +import { ReadWrite } from '../../../common/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { + DefaultKernelConnectionMetadata, + KernelConnectionMetadata, + KernelSpecConnectionMetadata, + LiveKernelConnectionMetadata, + LiveKernelModel, + PythonKernelConnectionMetadata +} from './types'; + +// Helper functions for dealing with kernels and kernelspecs + +export const defaultKernelSpecName = 'python_defaultSpec_'; + +// https://jupyter-client.readthedocs.io/en/stable/kernels.html +const connectionFilePlaceholder = '{connection_file}'; + +// Find the index of the connection file placeholder in a kernelspec +export function findIndexOfConnectionFile(kernelSpec: Readonly): number { + return kernelSpec.argv.indexOf(connectionFilePlaceholder); +} + +type ConnectionWithKernelSpec = + | KernelSpecConnectionMetadata + | PythonKernelConnectionMetadata + | DefaultKernelConnectionMetadata; +export function kernelConnectionMetadataHasKernelSpec( + connectionMetadata: KernelConnectionMetadata +): connectionMetadata is ConnectionWithKernelSpec { + return connectionMetadata.kind !== 'connectToLiveKernel'; +} +export function kernelConnectionMetadataHasKernelModel( + connectionMetadata: KernelConnectionMetadata +): connectionMetadata is LiveKernelConnectionMetadata { + return connectionMetadata.kind === 'connectToLiveKernel'; +} +export function getDisplayNameOrNameOfKernelConnection( + kernelConnection: KernelConnectionMetadata | undefined, + defaultValue: string = '' +) { + if (!kernelConnection) { + return defaultValue; + } + const displayName = + kernelConnection.kind === 'connectToLiveKernel' + ? kernelConnection.kernelModel.display_name + : kernelConnection.kernelSpec?.display_name; + const name = + kernelConnection.kind === 'connectToLiveKernel' + ? kernelConnection.kernelModel.name + : kernelConnection.kernelSpec?.name; + return displayName || name || defaultValue; +} + +export function getNameOfKernelConnection( + kernelConnection: KernelConnectionMetadata | undefined, + defaultValue: string = '' +) { + if (!kernelConnection) { + return defaultValue; + } + return kernelConnection.kind === 'connectToLiveKernel' + ? kernelConnection.kernelModel.name + : kernelConnection.kernelSpec?.name; +} + +export function getKernelPathFromKernelConnection(kernelConnection?: KernelConnectionMetadata): string | undefined { + if (!kernelConnection) { + return; + } + const model = kernelConnectionMetadataHasKernelModel(kernelConnection) ? kernelConnection.kernelModel : undefined; + const kernelSpec = kernelConnectionMetadataHasKernelSpec(kernelConnection) + ? kernelConnection.kernelSpec + : undefined; + return model?.path || kernelSpec?.path; +} +export function getInterpreterFromKernelConnectionMetadata( + kernelConnection?: KernelConnectionMetadata +): Partial | undefined { + if (!kernelConnection) { + return; + } + if (kernelConnection.interpreter) { + return kernelConnection.interpreter; + } + const model = kernelConnectionMetadataHasKernelModel(kernelConnection) ? kernelConnection.kernelModel : undefined; + if (model?.metadata?.interpreter) { + return model.metadata.interpreter; + } + const kernelSpec = kernelConnectionMetadataHasKernelSpec(kernelConnection) + ? kernelConnection.kernelSpec + : undefined; + return kernelSpec?.metadata?.interpreter; +} +export function isPythonKernelConnection(kernelConnection?: KernelConnectionMetadata): boolean { + if (!kernelConnection) { + return false; + } + if (kernelConnection.kind === 'startUsingPythonInterpreter') { + return true; + } + const model = kernelConnectionMetadataHasKernelModel(kernelConnection) ? kernelConnection.kernelModel : undefined; + const kernelSpec = kernelConnectionMetadataHasKernelSpec(kernelConnection) + ? kernelConnection.kernelSpec + : undefined; + return model?.language === PYTHON_LANGUAGE || kernelSpec?.language === PYTHON_LANGUAGE; +} +export function getKernelConnectionLanguage(kernelConnection?: KernelConnectionMetadata): string | undefined { + if (!kernelConnection) { + return; + } + const model = kernelConnectionMetadataHasKernelModel(kernelConnection) ? kernelConnection.kernelModel : undefined; + const kernelSpec = kernelConnectionMetadataHasKernelSpec(kernelConnection) + ? kernelConnection.kernelSpec + : undefined; + return model?.language || kernelSpec?.language; +} +// Create a default kernelspec with the given display name +export function createDefaultKernelSpec(displayName?: string): IJupyterKernelSpec { + // This creates a default kernel spec. When launched, 'python' argument will map to using the interpreter + // associated with the current resource for launching. + const defaultSpec: Kernel.ISpecModel = { + name: defaultKernelSpecName + Date.now().toString(), + language: 'python', + display_name: displayName || 'Python 3', + metadata: {}, + argv: ['python', '-m', 'ipykernel_launcher', '-f', connectionFilePlaceholder], + env: {}, + resources: {} + }; + + return new JupyterKernelSpec(defaultSpec); +} + +export function areKernelConnectionsEqual( + connection1?: KernelConnectionMetadata, + connection2?: KernelConnectionMetadata +) { + if (!connection1 && !connection2) { + return true; + } + if (!connection1 && connection2) { + return false; + } + if (connection1 && !connection2) { + return false; + } + if (connection1?.kind !== connection2?.kind) { + return false; + } + if (connection1?.kind === 'connectToLiveKernel' && connection2?.kind === 'connectToLiveKernel') { + return areKernelModelsEqual(connection1.kernelModel, connection2.kernelModel); + } else if ( + connection1 && + connection1.kind !== 'connectToLiveKernel' && + connection2 && + connection2.kind !== 'connectToLiveKernel' + ) { + const kernelSpecsAreTheSame = areKernelSpecsEqual(connection1?.kernelSpec, connection2?.kernelSpec); + // If both are launching interpreters, compare interpreter paths. + const interpretersAreSame = + connection1.kind === 'startUsingPythonInterpreter' + ? connection1.interpreter.path === connection2.interpreter?.path + : true; + + return kernelSpecsAreTheSame && interpretersAreSame; + } + return false; +} +function areKernelSpecsEqual(kernelSpec1?: IJupyterKernelSpec, kernelSpec2?: IJupyterKernelSpec) { + if (kernelSpec1 && kernelSpec2) { + const spec1 = cloneDeep(kernelSpec1) as ReadWrite; + spec1.env = spec1.env || {}; + spec1.metadata = spec1.metadata || {}; + const spec2 = cloneDeep(kernelSpec2) as ReadWrite; + spec2.env = spec1.env || {}; + spec2.metadata = spec1.metadata || {}; + + return fastDeepEqual(spec1, spec2); + } else if (!kernelSpec1 && !kernelSpec2) { + return true; + } else { + return false; + } +} +function areKernelModelsEqual(kernelModel1?: LiveKernelModel, kernelModel2?: LiveKernelModel) { + if (kernelModel1 && kernelModel2) { + const model1 = cloneDeep(kernelModel1) as ReadWrite; + model1.env = model1.env || {}; + model1.metadata = model1.metadata || {}; + const model2 = cloneDeep(kernelModel2) as ReadWrite; + model2.env = model1.env || {}; + model2.metadata = model1.metadata || {}; + return fastDeepEqual(model1, model2); + } else if (!kernelModel1 && !kernelModel2) { + return true; + } else { + return false; + } +} +// Check if a name is a default python kernel name and pull the version +export function detectDefaultKernelName(name: string) { + const regEx = NamedRegexp('python\\s*(?(\\d+))', 'g'); + return regEx.exec(name.toLowerCase()); +} + +export function cleanEnvironment(spec: T): T { + // tslint:disable-next-line: no-any + const copy = cloneDeep(spec) as { env?: any }; + + if (copy.env) { + // Scrub the environment of the spec to make sure it has allowed values (they all must be strings) + // See this issue here: https://github.com/microsoft/vscode-python/issues/11749 + const keys = Object.keys(copy.env); + keys.forEach((k) => { + if (copy.env) { + const value = copy.env[k]; + if (value !== null && value !== undefined) { + copy.env[k] = value.toString(); + } + } + }); + } + + return copy as T; +} diff --git a/src/client/datascience/jupyter/kernels/jupyterKernelPromiseFailedError.ts b/src/client/datascience/jupyter/kernels/jupyterKernelPromiseFailedError.ts new file mode 100644 index 000000000000..6ba8afa63045 --- /dev/null +++ b/src/client/datascience/jupyter/kernels/jupyterKernelPromiseFailedError.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export class JupyterKernelPromiseFailedError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/src/client/datascience/jupyter/kernels/jupyterKernelSpec.ts b/src/client/datascience/jupyter/kernels/jupyterKernelSpec.ts new file mode 100644 index 000000000000..26c29d8c3ab1 --- /dev/null +++ b/src/client/datascience/jupyter/kernels/jupyterKernelSpec.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { Kernel } from '@jupyterlab/services'; +import * as path from 'path'; +import { CancellationToken } from 'vscode'; +import { createPromiseFromCancellation } from '../../../common/cancellation'; +import { traceInfo } from '../../../common/logger'; + +import { IPythonExecutionFactory } from '../../../common/process/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { getRealPath } from '../../common'; +import { IDataScienceFileSystem, IJupyterKernelSpec } from '../../types'; + +export class JupyterKernelSpec implements IJupyterKernelSpec { + public name: string; + public language: string; + public path: string; + public specFile: string | undefined; + public readonly env: NodeJS.ProcessEnv | undefined; + public display_name: string; + public argv: string[]; + + // tslint:disable-next-line: no-any + public metadata?: Record & { interpreter?: Partial }; + constructor(specModel: Kernel.ISpecModel, file?: string) { + this.name = specModel.name; + this.argv = specModel.argv; + this.language = specModel.language; + this.path = specModel.argv && specModel.argv.length > 0 ? specModel.argv[0] : ''; + this.specFile = file; + this.display_name = specModel.display_name; + this.metadata = specModel.metadata; + // tslint:disable-next-line: no-any + this.env = specModel.env as any; // JSONObject, but should match + } +} + +/** + * Given the stdout contents from the command `python -m jupyter kernelspec list --json` this will parser that and build a list of kernelspecs. + * + * @export + * @param {string} stdout + * @param {IDataScienceFileSystem} fs + * @param {CancellationToken} [token] + * @returns + */ +export async function parseKernelSpecs( + stdout: string, + fs: IDataScienceFileSystem, + execFactory: IPythonExecutionFactory, + token?: CancellationToken +) { + traceInfo('Parsing kernelspecs from jupyter'); + // This should give us back a key value pair we can parse + const jsOut = JSON.parse(stdout.trim()) as { + kernelspecs: Record }>; + }; + const kernelSpecs = jsOut.kernelspecs; + + const specs = await Promise.race([ + Promise.all( + Object.keys(kernelSpecs).map(async (kernelName) => { + const spec = kernelSpecs[kernelName].spec as Kernel.ISpecModel; + // Add the missing name property. + const model = { + ...spec, + name: kernelName + }; + const specFile = await getRealPath( + fs, + execFactory, + spec.argv[0], + path.join(kernelSpecs[kernelName].resource_dir, 'kernel.json') + ); + if (specFile) { + return new JupyterKernelSpec(model as Kernel.ISpecModel, specFile); + } + }) + ), + createPromiseFromCancellation({ cancelAction: 'resolve', defaultValue: [], token }) + ]); + return specs.filter((item) => !!item).map((item) => item as JupyterKernelSpec); +} diff --git a/src/client/datascience/jupyter/kernels/kernel.ts b/src/client/datascience/jupyter/kernels/kernel.ts new file mode 100644 index 000000000000..53d82be3a28b --- /dev/null +++ b/src/client/datascience/jupyter/kernels/kernel.ts @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { nbformat } from '@jupyterlab/coreutils'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import * as uuid from 'uuid/v4'; +import { + CancellationToken, + CancellationTokenSource, + Event, + EventEmitter, + NotebookCell, + NotebookDocument, + Uri +} from 'vscode'; +import { ServerStatus } from '../../../../datascience-ui/interactive-common/mainState'; +import { IApplicationShell, ICommandManager } from '../../../common/application/types'; +import { traceError } from '../../../common/logger'; +import { IDisposableRegistry } from '../../../common/types'; +import { createDeferred, Deferred } from '../../../common/utils/async'; +import { noop } from '../../../common/utils/misc'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { CodeSnippets } from '../../constants'; +import { INotebookContentProvider } from '../../notebook/types'; +import { getDefaultNotebookContent, updateNotebookMetadata } from '../../notebookStorage/baseModel'; +import { + IDataScienceErrorHandler, + INotebook, + INotebookEditorProvider, + INotebookProvider, + INotebookProviderConnection, + InterruptResult, + KernelSocketInformation +} from '../../types'; +import { isPythonKernelConnection } from './helpers'; +import { KernelExecution } from './kernelExecution'; +import type { IKernel, IKernelProvider, IKernelSelectionUsage, KernelConnectionMetadata } from './types'; + +export class Kernel implements IKernel { + get connection(): INotebookProviderConnection | undefined { + return this.notebook?.connection; + } + get onStatusChanged(): Event { + return this._onStatusChanged.event; + } + get onRestarted(): Event { + return this._onRestarted.event; + } + get onDisposed(): Event { + return this._onDisposed.event; + } + get status(): ServerStatus { + return this.notebook?.status ?? ServerStatus.NotStarted; + } + get disposed(): boolean { + return this._disposed === true || this.notebook?.disposed === true; + } + get kernelSocket(): Observable { + return this._kernelSocket.asObservable(); + } + private notebook?: INotebook; + private _disposed?: boolean; + private readonly _kernelSocket = new Subject(); + private readonly _onStatusChanged = new EventEmitter(); + private readonly _onRestarted = new EventEmitter(); + private readonly _onDisposed = new EventEmitter(); + private _notebookPromise?: Promise; + private readonly hookedNotebookForEvents = new WeakSet(); + private restarting?: Deferred; + private readonly kernelValidated = new Map }>(); + private readonly kernelExecution: KernelExecution; + private startCancellation = new CancellationTokenSource(); + constructor( + public readonly uri: Uri, + public readonly metadata: Readonly, + private readonly notebookProvider: INotebookProvider, + private readonly disposables: IDisposableRegistry, + private readonly launchTimeout: number, + commandManager: ICommandManager, + interpreterService: IInterpreterService, + private readonly errorHandler: IDataScienceErrorHandler, + contentProvider: INotebookContentProvider, + editorProvider: INotebookEditorProvider, + private readonly kernelProvider: IKernelProvider, + private readonly kernelSelectionUsage: IKernelSelectionUsage, + appShell: IApplicationShell + ) { + this.kernelExecution = new KernelExecution( + kernelProvider, + commandManager, + interpreterService, + errorHandler, + contentProvider, + editorProvider, + kernelSelectionUsage, + appShell + ); + } + public async executeCell(cell: NotebookCell): Promise { + await this.start({ disableUI: false, token: this.startCancellation.token }); + await this.kernelExecution.executeCell(cell); + } + public async executeAllCells(document: NotebookDocument): Promise { + await this.start({ disableUI: false, token: this.startCancellation.token }); + await this.kernelExecution.executeAllCells(document); + } + public cancelCell(cell: NotebookCell) { + this.startCancellation.cancel(); + this.kernelExecution.cancelCell(cell); + } + public cancelAllCells(document: NotebookDocument) { + this.startCancellation.cancel(); + this.kernelExecution.cancelAllCells(document); + } + public async start(options?: { disableUI?: boolean; token?: CancellationToken }): Promise { + if (this.restarting) { + await this.restarting.promise; + } + if (this._notebookPromise) { + await this._notebookPromise; + return; + } else { + await this.validate(this.uri); + const metadata = ((getDefaultNotebookContent().metadata || {}) as unknown) as nbformat.INotebookMetadata; + updateNotebookMetadata(metadata, this.metadata); + + this._notebookPromise = this.notebookProvider.getOrCreateNotebook({ + identity: this.uri, + resource: this.uri, + disableUI: options?.disableUI, + getOnly: false, + metadata, + token: options?.token + }); + + this._notebookPromise + .then((nb) => (this.kernelExecution.notebook = this.notebook = nb)) + .catch((ex) => { + traceError('failed to create INotebook in kernel', ex); + this._notebookPromise = undefined; + this.startCancellation.cancel(); + this.errorHandler.handleError(ex).ignoreErrors(); // Just a notification, so don't await this + }); + await this._notebookPromise; + await this.initializeAfterStart(); + } + } + public async interrupt(): Promise { + if (this.restarting) { + await this.restarting.promise; + } + if (!this.notebook) { + throw new Error('No notebook to interrupt'); + } + return this.notebook.interruptKernel(this.launchTimeout); + } + public async dispose(): Promise { + this.restarting = undefined; + this._notebookPromise = undefined; + if (this.notebook) { + await this.notebook.dispose(); + this._disposed = true; + this._onDisposed.fire(); + this._onStatusChanged.fire(ServerStatus.Dead); + this.notebook = undefined; + this.kernelExecution.notebook = undefined; + } + this.kernelExecution.dispose(); + } + public async restart(): Promise { + if (this.restarting) { + return this.restarting.promise; + } + if (this.notebook) { + this.restarting = createDeferred(); + try { + await this.notebook.restartKernel(this.launchTimeout); + await this.initializeAfterStart(); + this.restarting.resolve(); + } catch (ex) { + this.restarting.reject(ex); + } finally { + this.restarting = undefined; + } + } + } + private async validate(uri: Uri): Promise { + const kernel = this.kernelProvider.get(uri); + if (!kernel) { + return; + } + const key = uri.toString(); + if (!this.kernelValidated.get(key)) { + const promise = new Promise((resolve) => + this.kernelSelectionUsage + .useSelectedKernel(kernel?.metadata, uri, 'raw') + .finally(() => { + // If still using the same promise, then remove the exception information. + // Basically if there's an exception, then we cannot use the kernel and a message would have been displayed. + // We don't want to cache such a promise, as its possible the user later installs the dependencies. + if (this.kernelValidated.get(key)?.kernel === kernel) { + this.kernelValidated.delete(key); + } + }) + .finally(resolve) + .catch(noop) + ); + + this.kernelValidated.set(key, { kernel, promise }); + } + await this.kernelValidated.get(key)!.promise; + } + private async initializeAfterStart() { + if (!this.notebook) { + return; + } + this.disableJedi(); + if (!this.hookedNotebookForEvents.has(this.notebook)) { + this.hookedNotebookForEvents.add(this.notebook); + this.notebook.kernelSocket.subscribe(this._kernelSocket); + this.notebook.onDisposed(() => { + this._notebookPromise = undefined; + this._onDisposed.fire(); + }); + this.notebook.onKernelRestarted(() => { + this._onRestarted.fire(); + }); + this.notebook.onSessionStatusChanged((e) => this._onStatusChanged.fire(e), this, this.disposables); + } + if (isPythonKernelConnection(this.metadata)) { + await this.notebook.setLaunchingFile(this.uri.fsPath); + } + await this.notebook.waitForIdle(this.launchTimeout); + } + + private disableJedi() { + if (isPythonKernelConnection(this.metadata) && this.notebook) { + this.notebook.executeObservable(CodeSnippets.disableJedi, this.uri.fsPath, 0, uuid(), true); + } + } +} diff --git a/src/client/datascience/jupyter/kernels/kernelDependencyService.ts b/src/client/datascience/jupyter/kernels/kernelDependencyService.ts new file mode 100644 index 000000000000..b405899e08a5 --- /dev/null +++ b/src/client/datascience/jupyter/kernels/kernelDependencyService.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationToken } from 'vscode'; +import { IApplicationShell } from '../../../common/application/types'; +import { createPromiseFromCancellation, wrapCancellationTokens } from '../../../common/cancellation'; +import { ProductNames } from '../../../common/installer/productNames'; +import { IInstaller, InstallerResponse, Product } from '../../../common/types'; +import { Common, DataScience } from '../../../common/utils/localize'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { IKernelDependencyService, KernelInterpreterDependencyResponse } from '../../types'; + +/** + * Responsible for managing dependencies of a Python interpreter required to run as a Jupyter Kernel. + * If required modules aren't installed, will prompt user to install them. + */ +@injectable() +export class KernelDependencyService implements IKernelDependencyService { + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IInstaller) private readonly installer: IInstaller + ) {} + /** + * Configures the python interpreter to ensure it can run a Jupyter Kernel by installing any missing dependencies. + * If user opts not to install they can opt to select another interpreter. + */ + public async installMissingDependencies( + interpreter: PythonEnvironment, + token?: CancellationToken + ): Promise { + if (await this.areDependenciesInstalled(interpreter, token)) { + return KernelInterpreterDependencyResponse.ok; + } + + const promptCancellationPromise = createPromiseFromCancellation({ + cancelAction: 'resolve', + defaultValue: undefined, + token + }); + const message = DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter().format( + interpreter.displayName || interpreter.envName || interpreter.path, + ProductNames.get(Product.ipykernel)! + ); + const installerToken = wrapCancellationTokens(token); + + const selection = await Promise.race([ + this.appShell.showErrorMessage(message, Common.install()), + promptCancellationPromise + ]); + if (installerToken.isCancellationRequested) { + return KernelInterpreterDependencyResponse.cancel; + } + + if (selection === Common.install()) { + const cancellatonPromise = createPromiseFromCancellation({ + cancelAction: 'resolve', + defaultValue: InstallerResponse.Ignore, + token + }); + // Always pass a cancellation token to `install`, to ensure it waits until the module is installed. + const response = await Promise.race([ + this.installer.install(Product.ipykernel, interpreter, installerToken), + cancellatonPromise + ]); + if (response === InstallerResponse.Installed) { + return KernelInterpreterDependencyResponse.ok; + } + } + return KernelInterpreterDependencyResponse.cancel; + } + public areDependenciesInstalled(interpreter: PythonEnvironment, _token?: CancellationToken): Promise { + return this.installer.isInstalled(Product.ipykernel, interpreter).then((installed) => installed === true); + } +} diff --git a/src/client/datascience/jupyter/kernels/kernelExecution.ts b/src/client/datascience/jupyter/kernels/kernelExecution.ts new file mode 100644 index 000000000000..7dbbad1fd46e --- /dev/null +++ b/src/client/datascience/jupyter/kernels/kernelExecution.ts @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { KernelMessage } from '@jupyterlab/services'; +import { NotebookCell, NotebookCellRunState, NotebookDocument } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../../../common/application/types'; +import { IDisposable } from '../../../common/types'; +import { noop } from '../../../common/utils/misc'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { captureTelemetry } from '../../../telemetry'; +import { Commands, Telemetry, VSCodeNativeTelemetry } from '../../constants'; +import { handleUpdateDisplayDataMessage } from '../../notebook/helpers/executionHelpers'; +import { MultiCancellationTokenSource } from '../../notebook/helpers/multiCancellationToken'; +import { INotebookContentProvider } from '../../notebook/types'; +import { IDataScienceErrorHandler, INotebook, INotebookEditorProvider } from '../../types'; +import { CellExecution, CellExecutionFactory } from './cellExecution'; +import type { IKernel, IKernelProvider, IKernelSelectionUsage } from './types'; +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); + +/** + * Separate class that deals just with kernel execution. + * Else the `Kernel` class gets very big. + */ +export class KernelExecution implements IDisposable { + public notebook?: INotebook; + + private readonly cellExecutions = new WeakMap(); + + private readonly documentExecutions = new WeakMap(); + + private readonly kernelValidated = new WeakMap }>(); + + private readonly executionFactory: CellExecutionFactory; + private readonly disposables: IDisposable[] = []; + constructor( + private readonly kernelProvider: IKernelProvider, + private readonly commandManager: ICommandManager, + private readonly interpreterService: IInterpreterService, + errorHandler: IDataScienceErrorHandler, + private readonly contentProvider: INotebookContentProvider, + editorProvider: INotebookEditorProvider, + readonly kernelSelectionUsage: IKernelSelectionUsage, + readonly appShell: IApplicationShell + ) { + this.executionFactory = new CellExecutionFactory(this.contentProvider, errorHandler, editorProvider, appShell); + } + + @captureTelemetry(Telemetry.ExecuteNativeCell, undefined, true) + public async executeCell(cell: NotebookCell): Promise { + if (!this.notebook) { + throw new Error('executeObservable cannot be called if kernel has not been started!'); + } + // Cannot execute empty cells. + if (this.cellExecutions.has(cell) || cell.document.getText().trim().length === 0) { + return; + } + const cellExecution = this.executionFactory.create(cell); + this.cellExecutions.set(cell, cellExecution); + + const kernel = this.getKernel(cell.notebook); + + try { + await this.executeIndividualCell(kernel, cellExecution); + } finally { + this.cellExecutions.delete(cell); + } + } + + @captureTelemetry(Telemetry.ExecuteNativeCell, undefined, true) + @captureTelemetry(VSCodeNativeTelemetry.RunAllCells, undefined, true) + public async executeAllCells(document: NotebookDocument): Promise { + if (!this.notebook) { + throw new Error('executeObservable cannot be called if kernel has not been started!'); + } + if (this.documentExecutions.has(document)) { + return; + } + const cancelTokenSource = new MultiCancellationTokenSource(); + this.documentExecutions.set(document, cancelTokenSource); + const kernel = this.getKernel(document); + document.metadata.runState = vscodeNotebookEnums.NotebookRunState.Running; + + const codeCellsToExecute = document.cells + .filter((cell) => cell.cellKind === vscodeNotebookEnums.CellKind.Code) + .filter((cell) => cell.document.getText().trim().length > 0) + .map((cell) => { + const cellExecution = this.executionFactory.create(cell); + this.cellExecutions.set(cellExecution.cell, cellExecution); + return cellExecution; + }); + cancelTokenSource.token.onCancellationRequested( + () => codeCellsToExecute.forEach((cell) => cell.cancel()), + this, + this.disposables + ); + + try { + let executingAPreviousCellHasFailed = false; + await codeCellsToExecute.reduce( + (previousPromise, cellToExecute) => + previousPromise.then((previousCellState) => { + // If a previous cell has failed or execution cancelled, the get out. + if ( + executingAPreviousCellHasFailed || + cancelTokenSource.token.isCancellationRequested || + previousCellState === vscodeNotebookEnums.NotebookCellRunState.Error + ) { + executingAPreviousCellHasFailed = true; + codeCellsToExecute.forEach((cell) => cell.cancel()); // Cancel pending cells. + return; + } + const result = this.executeIndividualCell(kernel, cellToExecute); + result.finally(() => this.cellExecutions.delete(cellToExecute.cell)).catch(noop); + return result; + }), + Promise.resolve(undefined) + ); + } finally { + this.documentExecutions.delete(document); + document.metadata.runState = vscodeNotebookEnums.NotebookRunState.Idle; + } + } + + public cancelCell(cell: NotebookCell): void { + if (this.cellExecutions.get(cell)) { + this.cellExecutions.get(cell)!.cancel(); + } + } + + public cancelAllCells(document: NotebookDocument): void { + if (this.documentExecutions.get(document)) { + this.documentExecutions.get(document)!.cancel(); + } + document.cells.forEach((cell) => this.cancelCell(cell)); + } + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + private async getKernel(document: NotebookDocument): Promise { + await this.validateKernel(document); + let kernel = this.kernelProvider.get(document.uri); + if (!kernel) { + const activeInterpreter = await this.interpreterService.getActiveInterpreter(document.uri); + kernel = this.kernelProvider.getOrCreate(document.uri, { + metadata: { + interpreter: activeInterpreter!, + kernelModel: undefined, + kernelSpec: undefined, + kind: 'startUsingPythonInterpreter' + } + }); + } + if (!kernel) { + throw new Error('Unable to create a Kernel to run cell'); + } + await kernel.start(); + return kernel; + } + + private onIoPubMessage(document: NotebookDocument, msg: KernelMessage.IIOPubMessage) { + // tslint:disable-next-line:no-require-imports + const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); + if (jupyterLab.KernelMessage.isUpdateDisplayDataMsg(msg) && handleUpdateDisplayDataMessage(msg, document)) { + this.contentProvider.notifyChangesToDocument(document); + } + } + + private async executeIndividualCell( + kernelPromise: Promise, + cellExecution: CellExecution + ): Promise { + if (!this.notebook) { + throw new Error('No notebook object'); + } + + // Register for IO pub messages + const ioRegistration = this.notebook.session.onIoPubMessage( + this.onIoPubMessage.bind(this, cellExecution.cell.notebook) + ); + cellExecution.token.onCancellationRequested( + () => { + ioRegistration.dispose(); + if (cellExecution.completed) { + return; + } + + // Interrupt kernel only if we need to cancel a cell execution. + this.commandManager.executeCommand(Commands.NotebookEditorInterruptKernel).then(noop, noop); + }, + this, + this.disposables + ); + + // Start execution + cellExecution.start(kernelPromise, this.notebook); + + // The result promise will resolve when complete. + try { + return await cellExecution.result; + } finally { + ioRegistration.dispose(); + } + } + + private async validateKernel(document: NotebookDocument): Promise { + const kernel = this.kernelProvider.get(document.uri); + if (!kernel) { + return; + } + if (!this.kernelValidated.get(document)) { + const promise = new Promise((resolve) => + this.kernelSelectionUsage + .useSelectedKernel(kernel?.metadata, document.uri, 'raw') + .finally(() => { + // If there's an exception, then we cannot use the kernel and a message would have been displayed. + // We don't want to cache such a promise, as its possible the user later installs the dependencies. + if (this.kernelValidated.get(document)?.kernel === kernel) { + this.kernelValidated.delete(document); + } + }) + .finally(resolve) + .catch(noop) + ); + + this.kernelValidated.set(document, { kernel, promise }); + } + await this.kernelValidated.get(document)!.promise; + } +} diff --git a/src/client/datascience/jupyter/kernels/kernelProvider.ts b/src/client/datascience/jupyter/kernels/kernelProvider.ts new file mode 100644 index 000000000000..47b895dc3f6f --- /dev/null +++ b/src/client/datascience/jupyter/kernels/kernelProvider.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fastDeepEqual from 'fast-deep-equal'; +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../../../common/application/types'; +import { traceInfo, traceWarning } from '../../../common/logger'; +import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry } from '../../../common/types'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { INotebookContentProvider } from '../../notebook/types'; +import { IDataScienceErrorHandler, INotebookEditorProvider, INotebookProvider } from '../../types'; +import { Kernel } from './kernel'; +import { KernelSelector } from './kernelSelector'; +import { IKernel, IKernelProvider, IKernelSelectionUsage, KernelOptions } from './types'; + +@injectable() +export class KernelProvider implements IKernelProvider { + private readonly kernelsByUri = new Map(); + constructor( + @inject(IAsyncDisposableRegistry) private asyncDisposables: IAsyncDisposableRegistry, + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, + @inject(INotebookProvider) private notebookProvider: INotebookProvider, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IDataScienceErrorHandler) private readonly errorHandler: IDataScienceErrorHandler, + @inject(INotebookContentProvider) private readonly contentProvider: INotebookContentProvider, + @inject(INotebookEditorProvider) private readonly editorProvider: INotebookEditorProvider, + @inject(KernelSelector) private readonly kernelSelectionUsage: IKernelSelectionUsage, + @inject(IApplicationShell) private readonly appShell: IApplicationShell + ) {} + public get(uri: Uri): IKernel | undefined { + return this.kernelsByUri.get(uri.toString())?.kernel; + } + public getOrCreate(uri: Uri, options: KernelOptions): IKernel | undefined { + const existingKernelInfo = this.kernelsByUri.get(uri.toString()); + if (existingKernelInfo && fastDeepEqual(existingKernelInfo.options.metadata, options.metadata)) { + return existingKernelInfo.kernel; + } + + this.disposeOldKernel(uri); + + const waitForIdleTimeout = this.configService.getSettings(uri).datascience.jupyterLaunchTimeout; + const kernel = new Kernel( + uri, + options.metadata, + this.notebookProvider, + this.disposables, + waitForIdleTimeout, + this.commandManager, + this.interpreterService, + this.errorHandler, + this.contentProvider, + this.editorProvider, + this, + this.kernelSelectionUsage, + this.appShell + ); + this.asyncDisposables.push(kernel); + this.kernelsByUri.set(uri.toString(), { options, kernel }); + this.deleteMappingIfKernelIsDisposed(uri, kernel); + return kernel; + } + /** + * If a kernel has been disposed, then remove the mapping of Uri + Kernel. + */ + private deleteMappingIfKernelIsDisposed(uri: Uri, kernel: IKernel) { + kernel.onDisposed( + () => { + // If the same kernel is associated with this document & it was disposed, then delete it. + if (this.kernelsByUri.get(uri.toString())?.kernel === kernel) { + this.kernelsByUri.delete(uri.toString()); + traceInfo( + `Kernel got disposed, hence there is no longer a kernel associated with ${uri.toString()}`, + kernel + ); + } + }, + this, + this.disposables + ); + } + private disposeOldKernel(uri: Uri) { + this.kernelsByUri + .get(uri.toString()) + ?.kernel.dispose() + .catch((ex) => traceWarning('Failed to dispose old kernel', ex)); // NOSONAR. + this.kernelsByUri.delete(uri.toString()); + } +} + +// export class KernelProvider { diff --git a/src/client/datascience/jupyter/kernels/kernelSelections.ts b/src/client/datascience/jupyter/kernels/kernelSelections.ts new file mode 100644 index 000000000000..ee24a167ddd3 --- /dev/null +++ b/src/client/datascience/jupyter/kernels/kernelSelections.ts @@ -0,0 +1,371 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, EventEmitter } from 'vscode'; +import { traceError } from '../../../common/logger'; +import { IPathUtils, Resource } from '../../../common/types'; +import { createDeferredFromPromise } from '../../../common/utils/async'; +import * as localize from '../../../common/utils/localize'; +import { noop } from '../../../common/utils/misc'; +import { IInterpreterSelector } from '../../../interpreter/configuration/types'; +import { IKernelFinder } from '../../kernel-launcher/types'; +import { IDataScienceFileSystem, IJupyterKernelSpec, IJupyterSessionManager } from '../../types'; +import { detectDefaultKernelName } from './helpers'; +import { KernelService } from './kernelService'; +import { + IKernelSelectionListProvider, + IKernelSpecQuickPickItem, + KernelSpecConnectionMetadata, + LiveKernelConnectionMetadata, + LiveKernelModel, + PythonKernelConnectionMetadata +} from './types'; + +// Small classes, hence all put into one file. +// tslint:disable: max-classes-per-file + +/** + * Given a kernel spec, this will return a quick pick item with appropriate display names and the like. + * + * @param {IJupyterKernelSpec} kernelSpec + * @param {IPathUtils} pathUtils + * @returns {IKernelSpecQuickPickItem} + */ +function getQuickPickItemForKernelSpec( + kernelSpec: IJupyterKernelSpec, + pathUtils: IPathUtils +): IKernelSpecQuickPickItem { + return { + label: kernelSpec.display_name, + // If we have a matching interpreter, then display that path in the dropdown else path of the kernelspec. + detail: pathUtils.getDisplayName(kernelSpec.metadata?.interpreter?.path || kernelSpec.path), + selection: { + kernelModel: undefined, + kernelSpec: kernelSpec, + interpreter: undefined, + kind: 'startUsingKernelSpec' + } + }; +} + +/** + * Given an active kernel, this will return a quick pick item with appropriate display names and the like. + * + * @param {(LiveKernelModel)} kernel + * @param {IPathUtils} pathUtils + * @returns {IKernelSpecQuickPickItem} + */ +function getQuickPickItemForActiveKernel( + kernel: LiveKernelModel, + pathUtils: IPathUtils +): IKernelSpecQuickPickItem { + const pickPath = kernel.metadata?.interpreter?.path || kernel.path; + return { + label: kernel.display_name || kernel.name || '', + // If we have a session, use that path + detail: kernel.session.path || !pickPath ? kernel.session.path : pathUtils.getDisplayName(pickPath), + description: localize.DataScience.jupyterSelectURIRunningDetailFormat().format( + kernel.lastActivityTime.toLocaleString(), + kernel.numberOfConnections.toString() + ), + selection: { kernelModel: kernel, interpreter: undefined, kind: 'connectToLiveKernel' } + }; +} + +/** + * Provider for active kernel specs in a jupyter session. + * + * @export + * @class ActiveJupyterSessionKernelSelectionListProvider + * @implements {IKernelSelectionListProvider} + */ +export class ActiveJupyterSessionKernelSelectionListProvider + implements IKernelSelectionListProvider { + constructor(private readonly sessionManager: IJupyterSessionManager, private readonly pathUtils: IPathUtils) {} + public async getKernelSelections( + _resource: Resource, + _cancelToken?: CancellationToken | undefined + ): Promise[]> { + const [activeKernels, activeSessions, kernelSpecs] = await Promise.all([ + this.sessionManager.getRunningKernels(), + this.sessionManager.getRunningSessions(), + this.sessionManager.getKernelSpecs() + ]); + const items = activeSessions.map((item) => { + const matchingSpec: Partial = + kernelSpecs.find((spec) => spec.name === item.kernel.name) || {}; + const activeKernel = activeKernels.find((active) => active.id === item.kernel.id) || {}; + // tslint:disable-next-line: no-object-literal-type-assertion + return { + ...item.kernel, + ...matchingSpec, + ...activeKernel, + session: item + } as LiveKernelModel; + }); + return items + .filter((item) => item.display_name || item.name) + .filter((item) => 'lastActivityTime' in item && 'numberOfConnections' in item) + .map((item) => getQuickPickItemForActiveKernel(item, this.pathUtils)); + } +} + +/** + * Provider for installed kernel specs (`python -m jupyter kernelspec list`). + * + * @export + * @class InstalledJupyterKernelSelectionListProvider + * @implements {IKernelSelectionListProvider} + */ +export class InstalledJupyterKernelSelectionListProvider + implements IKernelSelectionListProvider { + constructor( + private readonly kernelService: KernelService, + private readonly pathUtils: IPathUtils, + private readonly sessionManager?: IJupyterSessionManager + ) {} + public async getKernelSelections( + _resource: Resource, + cancelToken?: CancellationToken | undefined + ): Promise[]> { + const items = await this.kernelService.getKernelSpecs(this.sessionManager, cancelToken); + return items.map((item) => getQuickPickItemForKernelSpec(item, this.pathUtils)); + } +} + +// Provider for searching for installed kernelspecs on disk without using jupyter to search +export class InstalledRawKernelSelectionListProvider + implements IKernelSelectionListProvider { + constructor(private readonly kernelFinder: IKernelFinder, private readonly pathUtils: IPathUtils) {} + public async getKernelSelections( + resource: Resource, + _cancelToken?: CancellationToken + ): Promise[]> { + const items = await this.kernelFinder.listKernelSpecs(resource); + return items + .filter((item) => { + // If we have a default kernel name and a non-absolute path just hide the item + // Otherwise we end up showing a bunch of "Python 3 - python" default items for + // other interpreters + const match = detectDefaultKernelName(item.name); + if (match) { + return path.isAbsolute(item.path); + } + return true; + }) + .map((item) => getQuickPickItemForKernelSpec(item, this.pathUtils)); + } +} + +/** + * Provider for interpreters to be treated as kernel specs. + * I.e. return interpreters that are to be treated as kernel specs, and not yet installed as kernels. + * + * @export + * @class InterpreterKernelSelectionListProvider + * @implements {IKernelSelectionListProvider} + */ +export class InterpreterKernelSelectionListProvider + implements IKernelSelectionListProvider { + constructor(private readonly interpreterSelector: IInterpreterSelector) {} + public async getKernelSelections( + resource: Resource, + _cancelToken?: CancellationToken + ): Promise[]> { + const items = await this.interpreterSelector.getSuggestions(resource); + return items.map((item) => { + return { + ...item, + // We don't want descriptions. + description: '', + selection: { + kernelModel: undefined, + interpreter: item.interpreter, + kernelSpec: undefined, + kind: 'startUsingPythonInterpreter' + } + }; + }); + } +} + +/** + * Provides a list of kernel specs for selection, for both local and remote sessions. + * + * @export + * @class KernelSelectionProviderFactory + */ +@injectable() +export class KernelSelectionProvider { + private localSuggestionsCache: IKernelSpecQuickPickItem< + KernelSpecConnectionMetadata | PythonKernelConnectionMetadata + >[] = []; + private remoteSuggestionsCache: IKernelSpecQuickPickItem< + LiveKernelConnectionMetadata | KernelSpecConnectionMetadata + >[] = []; + private _listChanged = new EventEmitter(); + public get onDidChangeSelections() { + return this._listChanged.event; + } + constructor( + @inject(KernelService) private readonly kernelService: KernelService, + @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(IPathUtils) private readonly pathUtils: IPathUtils, + @inject(IKernelFinder) private readonly kernelFinder: IKernelFinder + ) {} + /** + * Gets a selection of kernel specs from a remote session. + * + * @param {Resource} resource + * @param {IJupyterSessionManager} sessionManager + * @param {CancellationToken} [cancelToken] + * @returns {Promise} + * @memberof KernelSelectionProvider + */ + public async getKernelSelectionsForRemoteSession( + resource: Resource, + sessionManager: IJupyterSessionManager, + cancelToken?: CancellationToken + ): Promise[]> { + const getSelections = async () => { + const installedKernelsPromise = new InstalledJupyterKernelSelectionListProvider( + this.kernelService, + this.pathUtils, + sessionManager + ).getKernelSelections(resource, cancelToken); + const liveKernelsPromise = new ActiveJupyterSessionKernelSelectionListProvider( + sessionManager, + this.pathUtils + ).getKernelSelections(resource, cancelToken); + const [installedKernels, liveKernels] = await Promise.all([installedKernelsPromise, liveKernelsPromise]); + + // Sort by name. + installedKernels.sort((a, b) => (a.label === b.label ? 0 : a.label > b.label ? 1 : -1)); + liveKernels.sort((a, b) => (a.label === b.label ? 0 : a.label > b.label ? 1 : -1)); + return [...liveKernels!, ...installedKernels!]; + }; + + const liveItems = getSelections().then((items) => (this.remoteSuggestionsCache = items)); + // If we have something in cache, return that, while fetching in the background. + const cachedItems = + this.remoteSuggestionsCache.length > 0 ? Promise.resolve(this.remoteSuggestionsCache) : liveItems; + return Promise.race([cachedItems, liveItems]); + } + /** + * Gets a selection of kernel specs for a local session. + * + * @param {Resource} resource + * @param type + * @param {IJupyterSessionManager} [sessionManager] + * @param {CancellationToken} [cancelToken] + * @returns {Promise} + * @memberof KernelSelectionProvider + */ + public async getKernelSelectionsForLocalSession( + resource: Resource, + type: 'raw' | 'jupyter' | 'noConnection', + sessionManager?: IJupyterSessionManager, + cancelToken?: CancellationToken + ): Promise[]> { + const getSelections = async () => { + // For raw versus jupyter connections we need to use a different method for fetching installed kernelspecs + // There is a possible unknown case for if we have a guest jupyter notebook that has not yet connected + // in that case we don't use either method + let installedKernelsPromise: Promise< + IKernelSpecQuickPickItem[] + > = Promise.resolve([]); + switch (type) { + case 'raw': + installedKernelsPromise = new InstalledRawKernelSelectionListProvider( + this.kernelFinder, + this.pathUtils + ).getKernelSelections(resource, cancelToken); + break; + case 'jupyter': + installedKernelsPromise = new InstalledJupyterKernelSelectionListProvider( + this.kernelService, + this.pathUtils, + sessionManager + ).getKernelSelections(resource, cancelToken); + break; + default: + break; + } + const interpretersPromise = new InterpreterKernelSelectionListProvider( + this.interpreterSelector + ).getKernelSelections(resource, cancelToken); + + // tslint:disable-next-line: prefer-const + let [installedKernels, interpreters] = await Promise.all([installedKernelsPromise, interpretersPromise]); + + interpreters = interpreters + .filter((item) => { + // If the interpreter is registered as a kernel then don't inlcude it. + if ( + installedKernels.find( + (installedKernel) => + installedKernel.selection.kernelSpec?.display_name === + item.selection.interpreter?.displayName && + (this.fs.areLocalPathsSame( + (installedKernel.selection.kernelSpec?.argv || [])[0], + item.selection.interpreter?.path || '' + ) || + this.fs.areLocalPathsSame( + installedKernel.selection.kernelSpec?.metadata?.interpreter?.path || '', + item.selection.interpreter?.path || '' + )) + ) + ) { + return false; + } + return true; + }) + .map((item) => { + // We don't want descriptions. + return { ...item, description: '' }; + }); + + const unifiedList = [...installedKernels!, ...interpreters]; + // Sort by name. + unifiedList.sort((a, b) => (a.label === b.label ? 0 : a.label > b.label ? 1 : -1)); + + return unifiedList; + }; + + const liveItems = getSelections().then((items) => (this.localSuggestionsCache = items)); + // If we have something in cache, return that, while fetching in the background. + const cachedItems = + this.localSuggestionsCache.length > 0 ? Promise.resolve(this.localSuggestionsCache) : liveItems; + + const liveItemsDeferred = createDeferredFromPromise(liveItems); + const cachedItemsDeferred = createDeferredFromPromise(cachedItems); + Promise.race([cachedItems, liveItems]) + .then(async () => { + // If the cached items completed first, then if later the live items completes we need to notify + // others that this selection has changed (however check if the results are different). + if (cachedItemsDeferred.completed && !liveItemsDeferred.completed) { + try { + const [liveItemsList, cachedItemsList] = await Promise.all([liveItems, cachedItems]); + // If the list of live items is different from the cached list, then notify a change. + if ( + liveItemsList.length !== cachedItemsList.length && + liveItemsList.length > 0 && + JSON.stringify(liveItemsList) !== JSON.stringify(cachedItemsList) + ) { + this._listChanged.fire(resource); + } + } catch (ex) { + traceError('Error in fetching kernel selections', ex); + } + } + }) + .catch(noop); + + return Promise.race([cachedItems, liveItems]); + } +} diff --git a/src/client/datascience/jupyter/kernels/kernelSelector.ts b/src/client/datascience/jupyter/kernels/kernelSelector.ts new file mode 100644 index 000000000000..d2623d2df616 --- /dev/null +++ b/src/client/datascience/jupyter/kernels/kernelSelector.ts @@ -0,0 +1,626 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import type { nbformat } from '@jupyterlab/coreutils'; +import type { Kernel } from '@jupyterlab/services'; +import { inject, injectable } from 'inversify'; +// tslint:disable-next-line: no-require-imports +import cloneDeep = require('lodash/cloneDeep'); +import { CancellationToken } from 'vscode-jsonrpc'; +import { IApplicationShell } from '../../../common/application/types'; +import '../../../common/extensions'; +import { traceError, traceInfo, traceVerbose } from '../../../common/logger'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import * as localize from '../../../common/utils/localize'; +import { noop } from '../../../common/utils/misc'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { IEventNamePropertyMapping, sendTelemetryEvent } from '../../../telemetry'; +import { Commands, KnownNotebookLanguages, Settings, Telemetry } from '../../constants'; +import { IKernelFinder } from '../../kernel-launcher/types'; +import { reportAction } from '../../progress/decorator'; +import { ReportableAction } from '../../progress/types'; +import { + IJupyterConnection, + IJupyterKernelSpec, + IJupyterSessionManager, + IJupyterSessionManagerFactory, + IKernelDependencyService, + INotebookMetadataLive, + INotebookProviderConnection +} from '../../types'; +import { createDefaultKernelSpec, getDisplayNameOrNameOfKernelConnection } from './helpers'; +import { KernelSelectionProvider } from './kernelSelections'; +import { KernelService } from './kernelService'; +import { + DefaultKernelConnectionMetadata, + IKernelSelectionUsage, + IKernelSpecQuickPickItem, + KernelConnectionMetadata, + KernelSpecConnectionMetadata, + LiveKernelConnectionMetadata, + PythonKernelConnectionMetadata +} from './types'; + +/** + * All KernelConnections returned (as return values of methods) by the KernelSelector can be used in a number of ways. + * E.g. some part of the code update the `interpreter` property in the `KernelConnectionMetadata` object. + * We need to ensure such changes (i.e. updates to the `KernelConnectionMetadata`) downstream do not change the original `KernelConnectionMetadata`. + * Hence always clone the `KernelConnectionMetadata` returned by the `kernelSelector`. + */ +@injectable() +export class KernelSelector implements IKernelSelectionUsage { + /** + * List of ids of kernels that should be hidden from the kernel picker. + * + * @private + * @type {new Set} + * @memberof KernelSelector + */ + private readonly kernelIdsToHide = new Set(); + constructor( + @inject(KernelSelectionProvider) private readonly selectionProvider: KernelSelectionProvider, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(KernelService) private readonly kernelService: KernelService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IKernelDependencyService) private readonly kernelDependencyService: IKernelDependencyService, + @inject(IKernelFinder) private readonly kernelFinder: IKernelFinder, + @inject(IJupyterSessionManagerFactory) private jupyterSessionManagerFactory: IJupyterSessionManagerFactory, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry + ) { + disposableRegistry.push( + this.jupyterSessionManagerFactory.onRestartSessionCreated(this.addKernelToIgnoreList.bind(this)) + ); + disposableRegistry.push( + this.jupyterSessionManagerFactory.onRestartSessionUsed(this.removeKernelFromIgnoreList.bind(this)) + ); + } + + /** + * Ensure kernels such as those associated with the restart session are not displayed in the kernel picker. + * + * @param {Kernel.IKernelConnection} kernel + * @memberof KernelSelector + */ + public addKernelToIgnoreList(kernel: Kernel.IKernelConnection): void { + this.kernelIdsToHide.add(kernel.id); + this.kernelIdsToHide.add(kernel.clientId); + } + /** + * Opposite of the add counterpart. + * + * @param {Kernel.IKernelConnection} kernel + * @memberof KernelSelector + */ + public removeKernelFromIgnoreList(kernel: Kernel.IKernelConnection): void { + this.kernelIdsToHide.delete(kernel.id); + this.kernelIdsToHide.delete(kernel.clientId); + } + + /** + * Selects a kernel from a remote session. + */ + public async selectRemoteKernel( + resource: Resource, + stopWatch: StopWatch, + session: IJupyterSessionManager, + cancelToken?: CancellationToken, + currentKernelDisplayName?: string + ): Promise { + let suggestions = await this.selectionProvider.getKernelSelectionsForRemoteSession( + resource, + session, + cancelToken + ); + suggestions = suggestions.filter((item) => !this.kernelIdsToHide.has(item.selection.kernelModel?.id || '')); + const selection = await this.selectKernel( + resource, + 'jupyter', + stopWatch, + Telemetry.SelectRemoteJupyterKernel, + suggestions, + session, + cancelToken, + currentKernelDisplayName + ); + return cloneDeep(selection); + } + /** + * Select a kernel from a local session. + */ + public async selectLocalKernel( + resource: Resource, + type: 'raw' | 'jupyter' | 'noConnection', + stopWatch: StopWatch, + session?: IJupyterSessionManager, + cancelToken?: CancellationToken, + currentKernelDisplayName?: string + ): Promise { + const suggestions = await this.selectionProvider.getKernelSelectionsForLocalSession( + resource, + type, + session, + cancelToken + ); + const selection = await this.selectKernel( + resource, + type, + stopWatch, + Telemetry.SelectLocalJupyterKernel, + suggestions, + session, + cancelToken, + currentKernelDisplayName + ); + return cloneDeep(selection); + } + /** + * Gets a kernel that needs to be used with a local session. + * (will attempt to find the best matching kernel, or prompt user to use current interpreter or select one). + */ + @reportAction(ReportableAction.KernelsGetKernelForLocalConnection) + public async getPreferredKernelForLocalConnection( + resource: Resource, + type: 'raw' | 'jupyter' | 'noConnection', + sessionManager?: IJupyterSessionManager, + notebookMetadata?: nbformat.INotebookMetadata, + disableUI?: boolean, + cancelToken?: CancellationToken, + ignoreDependencyCheck?: boolean + ): Promise< + KernelSpecConnectionMetadata | PythonKernelConnectionMetadata | DefaultKernelConnectionMetadata | undefined + > { + const stopWatch = new StopWatch(); + const telemetryProps: IEventNamePropertyMapping[Telemetry.FindKernelForLocalConnection] = { + kernelSpecFound: false, + interpreterFound: false, + promptedToSelect: false + }; + // When this method is called, we know we've started a local jupyter server or are connecting raw + // Lets pre-warm the list of local kernels. + this.selectionProvider + .getKernelSelectionsForLocalSession(resource, type, sessionManager, cancelToken) + .ignoreErrors(); + + let selection: + | KernelSpecConnectionMetadata + | PythonKernelConnectionMetadata + | DefaultKernelConnectionMetadata + | undefined; + + if (type === 'jupyter') { + selection = await this.getKernelForLocalJupyterConnection( + resource, + stopWatch, + telemetryProps, + sessionManager, + notebookMetadata, + disableUI, + cancelToken + ); + } else if (type === 'raw') { + selection = await this.getKernelForLocalRawConnection( + resource, + notebookMetadata, + cancelToken, + ignoreDependencyCheck + ); + } + + // If still not found, log an error (this seems possible for some people, so use the default) + if (!selection || !selection.kernelSpec) { + traceError('Jupyter Kernel Spec not found for a local connection'); + } + + telemetryProps.kernelSpecFound = !!selection?.kernelSpec; + telemetryProps.interpreterFound = !!selection?.interpreter; + sendTelemetryEvent(Telemetry.FindKernelForLocalConnection, stopWatch.elapsedTime, telemetryProps); + const itemToReturn = cloneDeep(selection); + if (itemToReturn) { + itemToReturn.interpreter = + itemToReturn.interpreter || (await this.interpreterService.getActiveInterpreter(resource)); + } + return itemToReturn; + } + + /** + * Gets a kernel that needs to be used with a remote session. + * (will attempt to find the best matching kernel, or prompt user to use current interpreter or select one). + */ + // tslint:disable-next-line: cyclomatic-complexity + @reportAction(ReportableAction.KernelsGetKernelForRemoteConnection) + public async getPreferredKernelForRemoteConnection( + resource: Resource, + sessionManager?: IJupyterSessionManager, + notebookMetadata?: INotebookMetadataLive, + cancelToken?: CancellationToken + ): Promise { + const [interpreter, specs, sessions] = await Promise.all([ + this.interpreterService.getActiveInterpreter(resource), + this.kernelService.getKernelSpecs(sessionManager, cancelToken), + sessionManager?.getRunningSessions() + ]); + + // First check for a live active session. + if (notebookMetadata && notebookMetadata.id) { + const session = sessions?.find((s) => s.kernel.id === notebookMetadata?.id); + if (session) { + // tslint:disable-next-line: no-any + const liveKernel = session.kernel as any; + const lastActivityTime = liveKernel.last_activity + ? new Date(Date.parse(liveKernel.last_activity.toString())) + : new Date(); + const numberOfConnections = liveKernel.connections + ? parseInt(liveKernel.connections.toString(), 10) + : 0; + return cloneDeep({ + kernelModel: { ...session.kernel, lastActivityTime, numberOfConnections, session }, + interpreter: interpreter, + kind: 'connectToLiveKernel' + }); + } + } + + // No running session, try matching based on interpreter + let bestMatch: IJupyterKernelSpec | undefined; + let bestScore = -1; + for (let i = 0; specs && i < specs?.length; i = i + 1) { + const spec = specs[i]; + let score = 0; + + if (spec) { + // See if the path matches. + if (spec && spec.path && spec.path.length > 0 && interpreter && spec.path === interpreter.path) { + // Path match + score += 8; + } + + // See if the version is the same + if (interpreter && interpreter.version && spec && spec.name) { + // Search for a digit on the end of the name. It should match our major version + const match = /\D+(\d+)/.exec(spec.name); + if (match && match !== null && match.length > 0) { + // See if the version number matches + const nameVersion = parseInt(match[1][0], 10); + if (nameVersion && nameVersion === interpreter.version.major) { + score += 4; + } + } + } + + // See if the display name already matches. + if (spec.display_name && spec.display_name === notebookMetadata?.kernelspec?.display_name) { + score += 16; + } + } + + if (score > bestScore) { + bestMatch = spec; + bestScore = score; + } + } + + if (bestMatch) { + return cloneDeep({ + kernelSpec: bestMatch, + interpreter: interpreter, + kind: 'startUsingKernelSpec' + }); + } else { + // Unlikely scenario, we expect there to be at least one kernel spec. + // Either way, return so that we can start using the default kernel. + return cloneDeep({ + interpreter: interpreter, + kind: 'startUsingDefaultKernel' + }); + } + } + public async useSelectedKernel( + selection: KernelConnectionMetadata, + resource: Resource, + type: 'raw' | 'jupyter' | 'noConnection', + session?: IJupyterSessionManager, + cancelToken?: CancellationToken + ): Promise { + // Check if ipykernel is installed in this kernel. + if (selection.interpreter && type === 'jupyter') { + sendTelemetryEvent(Telemetry.SwitchToInterpreterAsKernel); + const item = await this.useInterpreterAsKernel( + resource, + selection.interpreter, + type, + undefined, + session, + false, + cancelToken + ); + return cloneDeep(item); + } else if (selection.interpreter && type === 'raw') { + const item = await this.useInterpreterAndDefaultKernel(selection.interpreter); + return cloneDeep(item); + } else if (selection.kind === 'connectToLiveKernel') { + sendTelemetryEvent(Telemetry.SwitchToExistingKernel, undefined, { + language: this.computeLanguage(selection.kernelModel.language) + }); + // tslint:disable-next-line: no-any + const interpreter = selection.kernelModel + ? await this.kernelService.findMatchingInterpreter(selection.kernelModel, cancelToken) + : undefined; + return cloneDeep({ + interpreter, + kernelModel: selection.kernelModel, + kind: 'connectToLiveKernel' + }); + } else if (selection.kernelSpec) { + sendTelemetryEvent(Telemetry.SwitchToExistingKernel, undefined, { + language: this.computeLanguage(selection.kernelSpec.language) + }); + const interpreter = selection.kernelSpec + ? await this.kernelService.findMatchingInterpreter(selection.kernelSpec, cancelToken) + : undefined; + await this.kernelService.updateKernelEnvironment(interpreter, selection.kernelSpec, cancelToken); + return cloneDeep({ kernelSpec: selection.kernelSpec, interpreter, kind: 'startUsingKernelSpec' }); + } else { + return; + } + } + public async askForLocalKernel( + resource: Resource, + type: 'raw' | 'jupyter' | 'noConnection', + kernelConnection?: KernelConnectionMetadata + ): Promise { + const displayName = getDisplayNameOrNameOfKernelConnection(kernelConnection); + const message = localize.DataScience.sessionStartFailedWithKernel().format( + displayName, + Commands.ViewJupyterOutput + ); + const selectKernel = localize.DataScience.selectDifferentKernel(); + const cancel = localize.Common.cancel(); + const selection = await this.applicationShell.showErrorMessage(message, selectKernel, cancel); + if (selection === selectKernel) { + const item = await this.selectLocalJupyterKernel(resource, type, displayName); + return cloneDeep(item); + } + } + public async selectJupyterKernel( + resource: Resource, + connection: INotebookProviderConnection | undefined, + type: 'raw' | 'jupyter', + currentKernelDisplayName: string | undefined + ): Promise { + let kernelConnection: KernelConnectionMetadata | undefined; + const settings = this.configService.getSettings(resource); + const isLocalConnection = + connection?.localLaunch ?? + settings.datascience.jupyterServerURI.toLowerCase() === Settings.JupyterServerLocalLaunch; + + if (isLocalConnection) { + kernelConnection = await this.selectLocalJupyterKernel( + resource, + connection?.type || type, + currentKernelDisplayName + ); + } else if (connection && connection.type === 'jupyter') { + kernelConnection = await this.selectRemoteJupyterKernel(resource, connection, currentKernelDisplayName); + } + return cloneDeep(kernelConnection); + } + + private async selectLocalJupyterKernel( + resource: Resource, + type: 'raw' | 'jupyter' | 'noConnection', + currentKernelDisplayName: string | undefined + ): Promise { + return this.selectLocalKernel(resource, type, new StopWatch(), undefined, undefined, currentKernelDisplayName); + } + + private async selectRemoteJupyterKernel( + resource: Resource, + connInfo: IJupyterConnection, + currentKernelDisplayName?: string + ): Promise { + const stopWatch = new StopWatch(); + const session = await this.jupyterSessionManagerFactory.create(connInfo); + return this.selectRemoteKernel(resource, stopWatch, session, undefined, currentKernelDisplayName); + } + + // Get our kernelspec and matching interpreter for a connection to a local jupyter server + private async getKernelForLocalJupyterConnection( + resource: Resource, + stopWatch: StopWatch, + telemetryProps: IEventNamePropertyMapping[Telemetry.FindKernelForLocalConnection], + sessionManager?: IJupyterSessionManager, + notebookMetadata?: nbformat.INotebookMetadata, + disableUI?: boolean, + cancelToken?: CancellationToken + ): Promise< + KernelSpecConnectionMetadata | PythonKernelConnectionMetadata | DefaultKernelConnectionMetadata | undefined + > { + if (notebookMetadata?.kernelspec) { + const kernelSpec = await this.kernelService.findMatchingKernelSpec( + notebookMetadata?.kernelspec, + sessionManager, + cancelToken + ); + if (kernelSpec) { + const interpreter = await this.kernelService.findMatchingInterpreter(kernelSpec, cancelToken); + sendTelemetryEvent(Telemetry.UseExistingKernel); + + // Make sure we update the environment in the kernel before using it + await this.kernelService.updateKernelEnvironment(interpreter, kernelSpec, cancelToken); + return { kind: 'startUsingKernelSpec', interpreter, kernelSpec }; + } else if (!cancelToken?.isCancellationRequested) { + // No kernel info, hence prompt to use current interpreter as a kernel. + const activeInterpreter = await this.interpreterService.getActiveInterpreter(resource); + if (activeInterpreter) { + return this.useInterpreterAsKernel( + resource, + activeInterpreter, + 'jupyter', + notebookMetadata.kernelspec.display_name, + sessionManager, + disableUI, + cancelToken + ); + } else { + telemetryProps.promptedToSelect = true; + return this.selectLocalKernel(resource, 'jupyter', stopWatch, sessionManager, cancelToken); + } + } + } else if (!cancelToken?.isCancellationRequested) { + // No kernel info, hence use current interpreter as a kernel. + const activeInterpreter = await this.interpreterService.getActiveInterpreter(resource); + if (activeInterpreter) { + const kernelSpec = await this.kernelService.searchAndRegisterKernel( + activeInterpreter, + disableUI, + cancelToken + ); + if (kernelSpec) { + return { kind: 'startUsingKernelSpec', kernelSpec, interpreter: activeInterpreter }; + } else { + return { kind: 'startUsingDefaultKernel', interpreter: activeInterpreter }; + } + } + } + } + + // Get our kernelspec and interpreter for a local raw connection + private async getKernelForLocalRawConnection( + resource: Resource, + notebookMetadata?: nbformat.INotebookMetadata, + cancelToken?: CancellationToken, + ignoreDependencyCheck?: boolean + ): Promise { + // First use our kernel finder to locate a kernelspec on disk + const kernelSpec = await this.kernelFinder.findKernelSpec( + resource, + notebookMetadata?.kernelspec, + cancelToken, + ignoreDependencyCheck + ); + const activeInterpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!kernelSpec && !activeInterpreter) { + return; + } else if (!kernelSpec && activeInterpreter) { + // Return current interpreter. + return { + kind: 'startUsingPythonInterpreter', + interpreter: activeInterpreter + }; + } else if (kernelSpec) { + // Locate the interpreter that matches our kernelspec + const interpreter = await this.kernelService.findMatchingInterpreter(kernelSpec, cancelToken); + return { kind: 'startUsingKernelSpec', kernelSpec, interpreter }; + } + } + + private async selectKernel( + resource: Resource, + type: 'raw' | 'jupyter' | 'noConnection', + stopWatch: StopWatch, + telemetryEvent: Telemetry, + suggestions: IKernelSpecQuickPickItem[], + session?: IJupyterSessionManager, + cancelToken?: CancellationToken, + currentKernelDisplayName?: string + ) { + const placeHolder = + localize.DataScience.selectKernel() + + (currentKernelDisplayName ? ` (current: ${currentKernelDisplayName})` : ''); + sendTelemetryEvent(telemetryEvent, stopWatch.elapsedTime); + const selection = await this.applicationShell.showQuickPick(suggestions, { placeHolder }, cancelToken); + if (!selection?.selection) { + return; + } + return (this.useSelectedKernel(selection.selection, resource, type, session, cancelToken) as unknown) as + | T + | undefined; + } + + // When switching to an interpreter in raw kernel mode then just create a default kernelspec for that interpreter to use + private async useInterpreterAndDefaultKernel(interpreter: PythonEnvironment): Promise { + const kernelSpec = createDefaultKernelSpec(interpreter.displayName); + return { kernelSpec, interpreter, kind: 'startUsingPythonInterpreter' }; + } + + /** + * Use the provided interpreter as a kernel. + * If `displayNameOfKernelNotFound` is provided, then display a message indicating we're using the `current interpreter`. + * This would happen when we're starting a notebook. + * Otherwise, if not provided user is changing the kernel after starting a notebook. + */ + private async useInterpreterAsKernel( + resource: Resource, + interpreter: PythonEnvironment, + type: 'raw' | 'jupyter' | 'noConnection', + displayNameOfKernelNotFound?: string, + session?: IJupyterSessionManager, + disableUI?: boolean, + cancelToken?: CancellationToken + ): Promise { + let kernelSpec: IJupyterKernelSpec | undefined; + + if (await this.kernelDependencyService.areDependenciesInstalled(interpreter, cancelToken)) { + // Find the kernel associated with this interpreter. + kernelSpec = await this.kernelService.findMatchingKernelSpec(interpreter, session, cancelToken); + + if (kernelSpec) { + traceVerbose(`ipykernel installed in ${interpreter.path}, and matching kernelspec found.`); + // Make sure the environment matches. + await this.kernelService.updateKernelEnvironment(interpreter, kernelSpec, cancelToken); + + // Notify the UI that we didn't find the initially requested kernel and are just using the active interpreter + if (displayNameOfKernelNotFound && !disableUI) { + this.applicationShell + .showInformationMessage( + localize.DataScience.fallbackToUseActiveInterpreterAsKernel().format( + displayNameOfKernelNotFound + ) + ) + .then(noop, noop); + } + + sendTelemetryEvent(Telemetry.UseInterpreterAsKernel); + return { kind: 'startUsingKernelSpec', kernelSpec, interpreter }; + } + traceInfo(`ipykernel installed in ${interpreter.path}, no matching kernel found. Will register kernel.`); + } + + // Try an install this interpreter as a kernel. + try { + kernelSpec = await this.kernelService.registerKernel(interpreter, disableUI, cancelToken); + } catch (e) { + sendTelemetryEvent(Telemetry.KernelRegisterFailed); + throw e; + } + + // If we have a display name of a kernel that could not be found, + // then notify user that we're using current interpreter instead. + if (displayNameOfKernelNotFound && !disableUI) { + this.applicationShell + .showInformationMessage( + localize.DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel().format( + displayNameOfKernelNotFound + ) + ) + .then(noop, noop); + } + + // When this method is called, we know a new kernel may have been registered. + // Lets pre-warm the list of local kernels (with the new list). + this.selectionProvider.getKernelSelectionsForLocalSession(resource, type, session, cancelToken).ignoreErrors(); + + if (kernelSpec) { + return { kind: 'startUsingKernelSpec', kernelSpec, interpreter }; + } + } + + private computeLanguage(language: string | undefined): string { + if (language && KnownNotebookLanguages.includes(language.toLowerCase())) { + return language; + } + return 'unknown'; + } +} diff --git a/src/client/datascience/jupyter/kernels/kernelService.ts b/src/client/datascience/jupyter/kernels/kernelService.ts new file mode 100644 index 000000000000..5a5445d7765e --- /dev/null +++ b/src/client/datascience/jupyter/kernels/kernelService.ts @@ -0,0 +1,558 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import type { nbformat } from '@jupyterlab/coreutils'; +import type { Kernel } from '@jupyterlab/services'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { CancellationToken, CancellationTokenSource } from 'vscode'; +import { Cancellation, wrapCancellationTokens } from '../../../common/cancellation'; +import { PYTHON_LANGUAGE, PYTHON_WARNINGS } from '../../../common/constants'; +import '../../../common/extensions'; +import { traceDecorators, traceError, traceInfo, traceVerbose, traceWarning } from '../../../common/logger'; + +import { IPythonExecutionFactory } from '../../../common/process/types'; +import { ReadWrite } from '../../../common/types'; +import { sleep } from '../../../common/utils/async'; +import { noop } from '../../../common/utils/misc'; +import { IEnvironmentActivationService } from '../../../interpreter/activation/types'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { captureTelemetry, sendTelemetryEvent } from '../../../telemetry'; +import { getRealPath } from '../../common'; +import { Telemetry } from '../../constants'; +import { reportAction } from '../../progress/decorator'; +import { ReportableAction } from '../../progress/types'; +import { + IDataScienceFileSystem, + IJupyterKernelSpec, + IJupyterSessionManager, + IJupyterSubCommandExecutionService, + IKernelDependencyService, + KernelInterpreterDependencyResponse +} from '../../types'; +import { cleanEnvironment, detectDefaultKernelName } from './helpers'; +import { JupyterKernelSpec } from './jupyterKernelSpec'; +import { LiveKernelModel } from './types'; + +// tslint:disable-next-line: no-var-requires no-require-imports +const NamedRegexp = require('named-js-regexp') as typeof import('named-js-regexp'); + +/** + * Helper to ensure we can differentiate between two types in union types, keeping typing information. + * (basically avoiding the need to case using `as`). + * We cannot use `xx in` as jupyter uses `JSONObject` which is too broad and captures anything and everything. + * + * @param {(nbformat.IKernelspecMetadata | PythonEnvironment)} item + * @returns {item is PythonEnvironment} + */ +function isInterpreter(item: nbformat.IKernelspecMetadata | PythonEnvironment): item is PythonEnvironment { + // Interpreters will not have a `display_name` property, but have `path` and `type` properties. + return ( + !!(item as PythonEnvironment).path && + !!(item as PythonEnvironment).envType && + !(item as nbformat.IKernelspecMetadata).display_name + ); +} + +/** + * Responsible for kernel management and the like. + * + * @export + * @class KernelService + */ +@injectable() +export class KernelService { + constructor( + @inject(IJupyterSubCommandExecutionService) + private readonly jupyterInterpreterExecService: IJupyterSubCommandExecutionService, + @inject(IPythonExecutionFactory) private readonly execFactory: IPythonExecutionFactory, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IKernelDependencyService) private readonly kernelDependencyService: IKernelDependencyService, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(IEnvironmentActivationService) private readonly activationHelper: IEnvironmentActivationService + ) {} + /** + * Finds a kernel spec from a given session or jupyter process that matches a given spec. + * + * @param {nbformat.IKernelspecMetadata} kernelSpec The kernelspec (criteria) to be used when searching for a kernel. + * @param {IJupyterSessionManager} [sessionManager] If not provided search against the jupyter process. + * @param {CancellationToken} [cancelToken] + * @returns {(Promise)} + * @memberof KernelService + */ + public async findMatchingKernelSpec( + kernelSpec: nbformat.IKernelspecMetadata, + sessionManager?: IJupyterSessionManager, + cancelToken?: CancellationToken + ): Promise; + /** + * Finds a kernel spec from a given session or jupyter process that matches a given interpreter. + * + * @param {PythonEnvironment} interpreter The interpreter (criteria) to be used when searching for a kernel. + * @param {(IJupyterSessionManager | undefined)} sessionManager If not provided search against the jupyter process. + * @param {CancellationToken} [cancelToken] + * @returns {(Promise)} + * @memberof KernelService + */ + public async findMatchingKernelSpec( + interpreter: PythonEnvironment, + sessionManager?: IJupyterSessionManager | undefined, + cancelToken?: CancellationToken + ): Promise; + public async findMatchingKernelSpec( + option: nbformat.IKernelspecMetadata | PythonEnvironment, + sessionManager: IJupyterSessionManager | undefined, + cancelToken?: CancellationToken + ): Promise { + const specs = await this.getKernelSpecs(sessionManager, cancelToken); + if (isInterpreter(option)) { + return specs.find((item) => { + if (item.language.toLowerCase() !== PYTHON_LANGUAGE.toLowerCase()) { + return false; + } + return ( + this.fs.areLocalPathsSame(item.argv[0], option.path) || + this.fs.areLocalPathsSame(item.metadata?.interpreter?.path || '', option.path) + ); + }); + } else { + return specs.find((item) => item.display_name === option.display_name && item.name === option.name); + } + } + + /** + * Given a kernel, this will find an interpreter that matches the kernel spec. + * Note: When we create our own kernels on behalf of the user, the meta data contains the interpreter information. + * + * @param {IJupyterKernelSpec} kernelSpec + * @param {CancellationToken} [cancelToken] + * @returns {(Promise)} + * @memberof KernelService + */ + // tslint:disable-next-line: cyclomatic-complexity + public async findMatchingInterpreter( + kernelSpec: IJupyterKernelSpec | LiveKernelModel, + cancelToken?: CancellationToken + ): Promise { + const activeInterpreterPromise = this.interpreterService.getActiveInterpreter(undefined); + const allInterpretersPromise = this.interpreterService.getInterpreters(undefined); + // Ensure we handle errors if any (this is required to ensure we do not exit this function without using this promise). + // If promise is rejected and we do not use it, then ignore errors. + activeInterpreterPromise.ignoreErrors(); + // Ensure we handle errors if any (this is required to ensure we do not exit this function without using this promise). + // If promise is rejected and we do not use it, then ignore errors. + allInterpretersPromise.ignoreErrors(); + + // 1. Check if current interpreter has the same path + if (kernelSpec.metadata?.interpreter?.path) { + const interpreter = await this.interpreterService.getInterpreterDetails( + kernelSpec.metadata?.interpreter?.path + ); + if (interpreter) { + traceInfo( + `Found matching interpreter based on metadata, for the kernel ${kernelSpec.name}, ${kernelSpec.display_name}` + ); + return interpreter; + } + traceError( + `KernelSpec has interpreter information, however a matching interepter could not be found for ${kernelSpec.metadata?.interpreter?.path}` + ); + } + + // 2. Check if we have a fully qualified path in `argv` + const pathInArgv = + Array.isArray(kernelSpec.argv) && kernelSpec.argv.length > 0 ? kernelSpec.argv[0] : undefined; + if (pathInArgv && path.basename(pathInArgv) !== pathInArgv) { + const interpreter = await this.interpreterService.getInterpreterDetails(pathInArgv).catch((ex) => { + traceError( + `Failed to get interpreter information for python defined in kernel ${kernelSpec.name}, ${ + kernelSpec.display_name + } with argv: ${(kernelSpec.argv || [])?.join(',')}`, + ex + ); + return; + }); + if (interpreter) { + traceInfo( + `Found matching interpreter based on metadata, for the kernel ${kernelSpec.name}, ${kernelSpec.display_name}` + ); + return interpreter; + } + traceError( + `KernelSpec has interpreter information, however a matching interepter could not be found for ${kernelSpec.metadata?.interpreter?.path}` + ); + } + if (Cancellation.isCanceled(cancelToken)) { + return; + } + + // 3. Check if current interpreter has the same display name + const activeInterpreter = await activeInterpreterPromise; + // If the display name matches the active interpreter then use that. + if (kernelSpec.display_name === activeInterpreter?.displayName) { + return activeInterpreter; + } + + // Check if kernel is `Python2` or `Python3` or a similar generic kernel. + const match = detectDefaultKernelName(kernelSpec.name); + if (match && match.groups()) { + // 3. Look for interpreter with same major version + + const majorVersion = parseInt(match.groups()!.version, 10) || 0; + // If the major versions match, that's sufficient. + if (!majorVersion || (activeInterpreter?.version && activeInterpreter.version.major === majorVersion)) { + traceInfo(`Using current interpreter for kernel ${kernelSpec.name}, ${kernelSpec.display_name}`); + return activeInterpreter; + } + + // Find an interpreter that matches the + const allInterpreters = await allInterpretersPromise; + const found = allInterpreters.find((item) => item.version?.major === majorVersion); + + // If we cannot find a matching one, then use the current interpreter. + if (found) { + traceVerbose( + `Using interpreter ${found.path} for the kernel ${kernelSpec.name}, ${kernelSpec.display_name}` + ); + return found; + } + + traceWarning( + `Unable to find an interpreter that matches the kernel ${kernelSpec.name}, ${kernelSpec.display_name}, some features might not work.` + ); + return activeInterpreter; + } else { + // 5. Look for interpreter with same display name across all interpreters. + + // If the display name matches the active interpreter then use that. + // Look in all of our interpreters if we have somethign that matches this. + const allInterpreters = await allInterpretersPromise; + if (Cancellation.isCanceled(cancelToken)) { + return; + } + + const found = allInterpreters.find((item) => item.displayName === kernelSpec.display_name); + + if (found) { + traceVerbose( + `Found an interpreter that has the same display name as kernelspec ${kernelSpec.display_name}, matches ${found.path}` + ); + return found; + } else { + traceWarning( + `Unable to determine version of Python interpreter to use for kernel ${kernelSpec.name}, ${kernelSpec.display_name}, some features might not work.` + ); + return activeInterpreter; + } + } + } + public async searchAndRegisterKernel( + interpreter: PythonEnvironment, + disableUI?: boolean, + cancelToken?: CancellationToken + ): Promise { + // If a kernelspec already exists for this, then use that. + const found = await this.findMatchingKernelSpec(interpreter, undefined, cancelToken); + if (found) { + sendTelemetryEvent(Telemetry.UseExistingKernel); + + // Make sure the kernel is up to date with the current environment before + // we return it. + await this.updateKernelEnvironment(interpreter, found, cancelToken); + + return found; + } + return this.registerKernel(interpreter, disableUI, cancelToken); + } + + /** + * Registers an interpreter as a kernel. + * The assumption is that `ipykernel` has been installed in the interpreter. + * Kernel created will have following characteristics: + * - display_name = Display name of the interpreter. + * - metadata.interperter = Interpreter information (useful in finding a kernel that matches a given interpreter) + * - env = Will have environment variables of the activated environment. + * + * @param {PythonEnvironment} interpreter + * @param {boolean} [disableUI] + * @param {CancellationToken} [cancelToken] + * @returns {Promise} + * @memberof KernelService + */ + // tslint:disable-next-line: max-func-body-length + // tslint:disable-next-line: cyclomatic-complexity + @captureTelemetry(Telemetry.RegisterInterpreterAsKernel, undefined, true) + @traceDecorators.error('Failed to register an interpreter as a kernel') + @reportAction(ReportableAction.KernelsRegisterKernel) + // tslint:disable-next-line:max-func-body-length + public async registerKernel( + interpreter: PythonEnvironment, + disableUI?: boolean, + cancelToken?: CancellationToken + ): Promise { + if (!interpreter.displayName) { + throw new Error('Interpreter does not have a display name'); + } + + const execServicePromise = this.execFactory.createActivatedEnvironment({ + interpreter, + allowEnvironmentFetchExceptions: true, + bypassCondaExecution: true + }); + // Swallow errors if we get out of here and not resolve this. + execServicePromise.ignoreErrors(); + const name = this.generateKernelNameForIntepreter(interpreter); + // If ipykernel is not installed, prompt to install it. + if (!(await this.kernelDependencyService.areDependenciesInstalled(interpreter, cancelToken)) && !disableUI) { + // If we wish to wait for installation to complete, we must provide a cancel token. + const token = new CancellationTokenSource(); + const response = await this.kernelDependencyService.installMissingDependencies( + interpreter, + wrapCancellationTokens(cancelToken, token.token) + ); + if (response !== KernelInterpreterDependencyResponse.ok) { + traceWarning( + `Prompted to install ipykernel, however ipykernel not installed in the interpreter ${interpreter.path}. Response ${response}` + ); + return; + } + } + + if (Cancellation.isCanceled(cancelToken)) { + return; + } + + const execService = await execServicePromise; + const output = await execService.execModule( + 'ipykernel', + ['install', '--user', '--name', name, '--display-name', interpreter.displayName], + { + throwOnStdErr: true, + encoding: 'utf8', + token: cancelToken + } + ); + if (Cancellation.isCanceled(cancelToken)) { + return; + } + + let kernel = await this.findMatchingKernelSpec( + { display_name: interpreter.displayName, name }, + undefined, + cancelToken + ); + // Wait for at least 5s. We know launching a python (conda env) process on windows can sometimes take around 4s. + for (let counter = 0; counter < 10; counter += 1) { + if (Cancellation.isCanceled(cancelToken)) { + return; + } + if (kernel) { + break; + } + traceWarning('Waiting for 500ms for registered kernel to get detected'); + // Wait for jupyter server to get updated with the new kernel information. + await sleep(500); + kernel = await this.findMatchingKernelSpec( + { display_name: interpreter.displayName, name }, + undefined, + cancelToken + ); + } + if (!kernel) { + // Possible user doesn't have kernelspec installed. + kernel = await this.getKernelSpecFromStdOut(await execService.getExecutablePath(), output.stdout).catch( + (ex) => { + traceError('Failed to get kernelspec from stdout', ex); + return undefined; + } + ); + } + if (!kernel) { + const error = `Kernel not created with the name ${name}, display_name ${interpreter.displayName}. Output is ${output.stdout}`; + throw new Error(error); + } + if (!(kernel instanceof JupyterKernelSpec)) { + const error = `Kernel not registered locally, created with the name ${name}, display_name ${interpreter.displayName}. Output is ${output.stdout}`; + throw new Error(error); + } + if (!kernel.specFile) { + const error = `kernel.json not created with the name ${name}, display_name ${interpreter.displayName}. Output is ${output.stdout}`; + throw new Error(error); + } + + // Update the json with our environment. + await this.updateKernelEnvironment(interpreter, kernel, cancelToken, true); + + sendTelemetryEvent(Telemetry.RegisterAndUseInterpreterAsKernel); + traceInfo( + `Kernel successfully registered for ${interpreter.path} with the name=${name} and spec can be found here ${kernel.specFile}` + ); + return kernel; + } + public async updateKernelEnvironment( + interpreter: PythonEnvironment | undefined, + kernel: IJupyterKernelSpec, + cancelToken?: CancellationToken, + forceWrite?: boolean + ) { + const specedKernel = kernel as JupyterKernelSpec; + if (specedKernel.specFile) { + let specModel: ReadWrite = JSON.parse( + await this.fs.readLocalFile(specedKernel.specFile) + ); + let shouldUpdate = false; + + // Make sure the specmodel has an interpreter or already in the metadata or we + // may overwrite a kernel created by the user + if (interpreter && (specModel.metadata?.interpreter || forceWrite)) { + // Ensure we use a fully qualified path to the python interpreter in `argv`. + if (specModel.argv[0].toLowerCase() === 'conda') { + // If conda is the first word, its possible its a conda activation command. + traceInfo(`Spec argv[0], not updated as it is using conda.`); + } else { + traceInfo(`Spec argv[0] updated from '${specModel.argv[0]}' to '${interpreter.path}'`); + specModel.argv[0] = interpreter.path; + } + + // Get the activated environment variables (as a work around for `conda run` and similar). + // This ensures the code runs within the context of an activated environment. + specModel.env = await this.activationHelper + .getActivatedEnvironmentVariables(undefined, interpreter, true) + .catch(noop) + // tslint:disable-next-line: no-any + .then((env) => (env || {}) as any); + if (Cancellation.isCanceled(cancelToken)) { + return; + } + + // Special case, modify the PYTHONWARNINGS env to the global value. + // otherwise it's forced to 'ignore' because activated variables are cached. + if (specModel.env && process.env[PYTHON_WARNINGS]) { + // tslint:disable-next-line:no-any + specModel.env[PYTHON_WARNINGS] = process.env[PYTHON_WARNINGS] as any; + } else if (specModel.env && specModel.env[PYTHON_WARNINGS]) { + delete specModel.env[PYTHON_WARNINGS]; + } + // Ensure we update the metadata to include interpreter stuff as well (we'll use this to search kernels that match an interpreter). + // We'll need information such as interpreter type, display name, path, etc... + // Its just a JSON file, and the information is small, hence might as well store everything. + specModel.metadata = specModel.metadata || {}; + // tslint:disable-next-line: no-any + specModel.metadata.interpreter = interpreter as any; + + // Indicate we need to write + shouldUpdate = true; + } + + // Scrub the environment of the specmodel to make sure it has allowed values (they all must be strings) + // See this issue here: https://github.com/microsoft/vscode-python/issues/11749 + if (specModel.env) { + specModel = cleanEnvironment(specModel); + shouldUpdate = true; + } + + // Update the kernel.json with our new stuff. + if (shouldUpdate) { + await this.fs.writeLocalFile(specedKernel.specFile, JSON.stringify(specModel, undefined, 2)); + } + + // Always update the metadata for the original kernel. + specedKernel.metadata = specModel.metadata; + } + } + /** + * Gets a list of all kernel specs. + * + * @param {IJupyterSessionManager} [sessionManager] + * @param {CancellationToken} [cancelToken] + * @returns {Promise} + * @memberof KernelService + */ + @reportAction(ReportableAction.KernelsGetKernelSpecs) + public async getKernelSpecs( + sessionManager?: IJupyterSessionManager, + cancelToken?: CancellationToken + ): Promise { + const enumerator = sessionManager + ? sessionManager.getKernelSpecs() + : this.jupyterInterpreterExecService.getKernelSpecs(cancelToken); + if (Cancellation.isCanceled(cancelToken)) { + return []; + } + traceInfo('Enumerating kernel specs...'); + const specs: IJupyterKernelSpec[] = await enumerator; + const result = specs.filter((item) => !!item); + traceInfo(`Found ${result.length} kernelspecs`); + + // Send telemetry on this enumeration. + const anyPython = result.find((k) => k.language === 'python') !== undefined; + sendTelemetryEvent(Telemetry.KernelEnumeration, undefined, { + count: result.length, + isPython: anyPython, + source: sessionManager ? 'connection' : 'cli' + }); + + return result; + } + /** + * Not all characters are allowed in a kernel name. + * This method will generate a name for a kernel based on display name and path. + * Algorithm = + + * + * @private + * @param {PythonEnvironment} interpreter + * @memberof KernelService + */ + private generateKernelNameForIntepreter(interpreter: PythonEnvironment): string { + return `${interpreter.displayName || ''}${uuid()}`.replace(/[^A-Za-z0-9]/g, '').toLowerCase(); + } + + /** + * Will scrape kernelspec info from the output when a new kernel is created. + * + * @private + * @param {string} output + * @returns {JupyterKernelSpec} + * @memberof KernelService + */ + @traceDecorators.error('Failed to parse kernel creation stdout') + private async getKernelSpecFromStdOut(pythonPath: string, output: string): Promise { + if (!output) { + return; + } + + // Output should be of the form + // `Installed kernel in ` + const regEx = NamedRegexp('Installed\\skernelspec\\s(?\\w*)\\sin\\s(?.*)', 'g'); + const match = regEx.exec(output); + if (!match || !match.groups()) { + return; + } + + type RegExGroup = { name: string; path: string }; + const groups = match.groups() as RegExGroup | undefined; + + if (!groups || !groups.name || !groups.path) { + traceError('Kernel Output not parsed', output); + throw new Error('Unable to parse output to get the kernel info'); + } + + const specFile = await getRealPath( + this.fs, + this.execFactory, + pythonPath, + path.join(groups.path, 'kernel.json') + ); + if (!specFile) { + throw new Error('KernelSpec file not found'); + } + + const kernelModel = JSON.parse(await this.fs.readLocalFile(specFile)); + kernelModel.name = groups.name; + return new JupyterKernelSpec(kernelModel as Kernel.ISpecModel, specFile); + } +} diff --git a/src/client/datascience/jupyter/kernels/kernelSwitcher.ts b/src/client/datascience/jupyter/kernels/kernelSwitcher.ts new file mode 100644 index 000000000000..73b0613c4e92 --- /dev/null +++ b/src/client/datascience/jupyter/kernels/kernelSwitcher.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ProgressLocation, ProgressOptions } from 'vscode'; +import { IApplicationShell } from '../../../common/application/types'; +import { IConfigurationService } from '../../../common/types'; +import { DataScience } from '../../../common/utils/localize'; +import { JupyterSessionStartError } from '../../baseJupyterSession'; +import { Settings } from '../../constants'; +import { RawKernelSessionStartError } from '../../raw-kernel/rawJupyterSession'; +import { IKernelDependencyService, INotebook, KernelInterpreterDependencyResponse } from '../../types'; +import { JupyterInvalidKernelError } from '../jupyterInvalidKernelError'; +import { kernelConnectionMetadataHasKernelModel, kernelConnectionMetadataHasKernelSpec } from './helpers'; +import { KernelSelector } from './kernelSelector'; +import { KernelConnectionMetadata } from './types'; + +@injectable() +export class KernelSwitcher { + constructor( + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IKernelDependencyService) private readonly kernelDependencyService: IKernelDependencyService, + @inject(KernelSelector) private readonly selector: KernelSelector + ) {} + + public async switchKernelWithRetry(notebook: INotebook, kernel: KernelConnectionMetadata): Promise { + const settings = this.configService.getSettings(notebook.resource); + const isLocalConnection = + notebook.connection?.localLaunch ?? + settings.datascience.jupyterServerURI.toLowerCase() === Settings.JupyterServerLocalLaunch; + if (!isLocalConnection) { + await this.switchToKernel(notebook, kernel); + return; + } + + // Keep retrying, until it works or user cancels. + // Sometimes if a bad kernel is selected, starting a session can fail. + // In such cases we need to let the user know about this and prompt them to select another kernel. + // tslint:disable-next-line: no-constant-condition + while (true) { + try { + await this.switchToKernel(notebook, kernel); + return; + } catch (ex) { + if ( + isLocalConnection && + (ex instanceof JupyterSessionStartError || + ex instanceof JupyterInvalidKernelError || + ex instanceof RawKernelSessionStartError) + ) { + // Looks like we were unable to start a session for the local connection. + // Possibly something wrong with the kernel. + // At this point we have a valid jupyter server. + const potential = await this.selector.askForLocalKernel( + notebook.resource, + notebook.connection?.type || 'noConnection', + kernel + ); + if (potential && Object.keys(potential).length > 0) { + kernel = potential; + continue; + } + } + throw ex; + } + } + } + private async switchToKernel(notebook: INotebook, kernelConnection: KernelConnectionMetadata): Promise { + if (notebook.connection?.type === 'raw' && kernelConnection.interpreter) { + const response = await this.kernelDependencyService.installMissingDependencies( + kernelConnection.interpreter + ); + if (response === KernelInterpreterDependencyResponse.cancel) { + return; + } + } + + const switchKernel = async (newKernelConnection: KernelConnectionMetadata) => { + // Change the kernel. A status update should fire that changes our display + await notebook.setKernelConnection( + newKernelConnection, + this.configService.getSettings(notebook.resource).datascience.jupyterLaunchTimeout + ); + }; + + const kernelModel = kernelConnectionMetadataHasKernelModel(kernelConnection) ? kernelConnection : undefined; + const kernelSpec = kernelConnectionMetadataHasKernelSpec(kernelConnection) ? kernelConnection : undefined; + const kernelName = kernelSpec?.kernelSpec?.name || kernelModel?.kernelModel?.name; + // One of them is bound to be non-empty. + const displayName = kernelModel?.kernelModel?.display_name || kernelName || ''; + const options: ProgressOptions = { + location: ProgressLocation.Notification, + cancellable: false, + title: DataScience.switchingKernelProgress().format(displayName) + }; + await this.appShell.withProgress(options, async (_, __) => switchKernel(kernelConnection!)); + } +} diff --git a/src/client/datascience/jupyter/kernels/types.ts b/src/client/datascience/jupyter/kernels/types.ts new file mode 100644 index 000000000000..832fbe4e5b60 --- /dev/null +++ b/src/client/datascience/jupyter/kernels/types.ts @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import type { Session } from '@jupyterlab/services'; +import type { Observable } from 'rxjs/Observable'; +import type { CancellationToken, Event, QuickPickItem, Uri } from 'vscode'; +import { NotebookCell, NotebookDocument } from '../../../../../types/vscode-proposed'; +import type { ServerStatus } from '../../../../datascience-ui/interactive-common/mainState'; +import type { IAsyncDisposable, Resource } from '../../../common/types'; +import type { PythonEnvironment } from '../../../pythonEnvironments/info'; +import type { + IJupyterKernel, + IJupyterKernelSpec, + IJupyterSessionManager, + InterruptResult, + KernelSocketInformation +} from '../../types'; + +export type LiveKernelModel = IJupyterKernel & Partial & { session: Session.IModel }; + +/** + * Connection metadata for Live Kernels. + * With this we are able connect to an existing kernel (instead of starting a new session). + */ +export type LiveKernelConnectionMetadata = { + kernelModel: LiveKernelModel; + /** + * Python interpreter will be used for intellisense & the like. + */ + interpreter?: PythonEnvironment; + kind: 'connectToLiveKernel'; +}; +/** + * Connection metadata for Kernels started using kernelspec (JSON). + * This could be a raw kernel (spec might have path to executable for .NET or the like). + * If the executable is not defined in kernelspec json, & it is a Python kernel, then we'll use the provided python interpreter. + */ +export type KernelSpecConnectionMetadata = { + kernelModel?: undefined; + kernelSpec: IJupyterKernelSpec; + /** + * Indicates the interpreter that may be used to start the kernel. + * If possible to start a kernel without this Python interpreter, then this Python interpreter will be used for intellisense & the like. + * This interpreter could also be the interpreter associated with the kernel spec that we are supposed to start. + */ + interpreter?: PythonEnvironment; + kind: 'startUsingKernelSpec'; +}; +/** + * Connection metadata for Kernels started using default kernel. + * Here we tell Jupyter to start a session and let it decide what kernel is to be started. + * (could apply to either local or remote sessions when dealing with Jupyter Servers). + */ +export type DefaultKernelConnectionMetadata = { + /** + * This will be empty as we do not have a kernel spec. + * Left for type compatibility with other types that have kernel spec property. + */ + kernelSpec?: IJupyterKernelSpec; + /** + * Python interpreter will be used for intellisense & the like. + */ + interpreter?: PythonEnvironment; + kind: 'startUsingDefaultKernel'; +}; +/** + * Connection metadata for Kernels started using Python interpreter. + * These are not necessarily raw (it could be plain old Jupyter Kernels, where we register Python interpreter as a kernel). + * We can have KernelSpec information here as well, however that is totally optional. + * We will always start this kernel using old Jupyter style (provided we first register this intrepreter as a kernel) or raw. + */ +export type PythonKernelConnectionMetadata = { + kernelSpec?: IJupyterKernelSpec; + interpreter: PythonEnvironment; + kind: 'startUsingPythonInterpreter'; +}; +export type KernelConnectionMetadata = + | LiveKernelConnectionMetadata + | KernelSpecConnectionMetadata + | PythonKernelConnectionMetadata + | DefaultKernelConnectionMetadata; + +/** + * Returns a string that can be used to uniquely identify a Kernel Connection. + */ +export function getKernelConnectionId(kernelConnection: KernelConnectionMetadata) { + switch (kernelConnection.kind) { + case 'connectToLiveKernel': + return `${kernelConnection.kind}#${kernelConnection.kernelModel.name}.${kernelConnection.kernelModel.session.id}.${kernelConnection.kernelModel.session.name}`; + case 'startUsingDefaultKernel': + return `${kernelConnection.kind}#${kernelConnection}`; + case 'startUsingKernelSpec': + return `${kernelConnection.kind}#${kernelConnection.kernelSpec.name}.${kernelConnection.kernelSpec.display_name}`; + case 'startUsingPythonInterpreter': + return `${kernelConnection.kind}#${kernelConnection.interpreter.path}`; + default: + throw new Error(`Unsupported Kernel Connection ${kernelConnection}`); + } +} + +export interface IKernelSpecQuickPickItem + extends QuickPickItem { + selection: T; +} +export interface IKernelSelectionListProvider { + getKernelSelections(resource: Resource, cancelToken?: CancellationToken): Promise[]>; +} + +export interface IKernelSelectionUsage { + /** + * Given a kernel selection, this method will attempt to use that kernel and return the corresponding Interpreter, Kernel Spec and the like. + * This method will also check if required dependencies are installed or not, and will install them if required. + */ + useSelectedKernel( + selection: KernelConnectionMetadata, + resource: Resource, + type: 'raw' | 'jupyter' | 'noConnection', + session?: IJupyterSessionManager, + cancelToken?: CancellationToken + ): Promise; +} + +export interface IKernel extends IAsyncDisposable { + readonly uri: Uri; + readonly metadata: Readonly; + readonly onStatusChanged: Event; + readonly onDisposed: Event; + readonly onRestarted: Event; + readonly status: ServerStatus; + readonly disposed: boolean; + readonly kernelSocket: Observable; + start(): Promise; + interrupt(): Promise; + restart(): Promise; + executeCell(cell: NotebookCell): Promise; + executeAllCells(document: NotebookDocument): Promise; +} + +export type KernelOptions = { metadata: KernelConnectionMetadata }; +export const IKernelProvider = Symbol('IKernelProvider'); +export interface IKernelProvider { + /** + * Get hold of the active kernel for a given Uri (Notebook or other file). + */ + get(uri: Uri): IKernel | undefined; + /** + * Gets or creates a kernel for a given Uri. + * WARNING: If called with different options for same Uri, old kernel associated with the Uri will be disposed. + */ + getOrCreate(uri: Uri, options: KernelOptions): IKernel | undefined; +} diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts b/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts new file mode 100644 index 000000000000..6e585661ed32 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { injectable } from 'inversify'; +import * as uuid from 'uuid/v4'; +import { CancellationToken } from 'vscode'; + +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; + +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel +} from '../../../common/types'; +import * as localize from '../../../common/utils/localize'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { LiveShare, LiveShareCommands } from '../../constants'; +import { IDataScienceFileSystem, IJupyterConnection, INotebookServer, INotebookServerOptions } from '../../types'; +import { JupyterConnectError } from '../jupyterConnectError'; +import { JupyterExecutionBase } from '../jupyterExecution'; +import { KernelSelector } from '../kernels/kernelSelector'; +import { NotebookStarter } from '../notebookStarter'; +import { LiveShareParticipantGuest } from './liveShareParticipantMixin'; +import { ServerCache } from './serverCache'; + +// This class is really just a wrapper around a jupyter execution that also provides a shared live share service +@injectable() +export class GuestJupyterExecution extends LiveShareParticipantGuest( + JupyterExecutionBase, + LiveShare.JupyterExecutionService +) { + private serverCache: ServerCache; + + constructor( + liveShare: ILiveShareApi, + interpreterService: IInterpreterService, + disposableRegistry: IDisposableRegistry, + asyncRegistry: IAsyncDisposableRegistry, + fs: IDataScienceFileSystem, + workspace: IWorkspaceService, + configuration: IConfigurationService, + kernelSelector: KernelSelector, + notebookStarter: NotebookStarter, + appShell: IApplicationShell, + jupyterOutputChannel: IOutputChannel, + serviceContainer: IServiceContainer + ) { + super( + liveShare, + interpreterService, + disposableRegistry, + workspace, + configuration, + kernelSelector, + notebookStarter, + appShell, + jupyterOutputChannel, + serviceContainer + ); + asyncRegistry.push(this); + this.serverCache = new ServerCache(configuration, workspace, fs); + } + + public async dispose(): Promise { + await super.dispose(); + + // Dispose of all of our cached servers + await this.serverCache.dispose(); + } + + public async isNotebookSupported(cancelToken?: CancellationToken): Promise { + return this.checkSupported(LiveShareCommands.isNotebookSupported, cancelToken); + } + public isImportSupported(cancelToken?: CancellationToken): Promise { + return this.checkSupported(LiveShareCommands.isImportSupported, cancelToken); + } + public isSpawnSupported(_cancelToken?: CancellationToken): Promise { + return Promise.resolve(false); + } + + public async guestConnectToNotebookServer( + options?: INotebookServerOptions, + cancelToken?: CancellationToken + ): Promise { + const service = await this.waitForService(); + if (service) { + const purpose = options ? options.purpose : uuid(); + const connection: IJupyterConnection = await service.request( + LiveShareCommands.connectToNotebookServer, + [options], + cancelToken + ); + + // If that works, then treat this as a remote server and connect to it + if (connection && connection.baseUrl) { + const newUri = `${connection.baseUrl}?token=${connection.token}`; + return super.connectToNotebookServer( + { + uri: newUri, + skipUsingDefaultConfig: options && options.skipUsingDefaultConfig, + workingDir: options ? options.workingDir : undefined, + purpose, + allowUI: () => false, + skipSearchingForKernel: true + }, + cancelToken + ); + } + } + } + + public async connectToNotebookServer( + options?: INotebookServerOptions, + cancelToken?: CancellationToken + ): Promise { + const result = await this.serverCache.getOrCreate( + this.guestConnectToNotebookServer.bind(this), + options, + cancelToken + ); + + if (!result) { + throw new JupyterConnectError(localize.DataScience.liveShareConnectFailure()); + } + + return result; + } + + public spawnNotebook(_file: string): Promise { + // Not supported in liveshare + throw new Error(localize.DataScience.liveShareCannotSpawnNotebooks()); + } + + public async getUsableJupyterPython(cancelToken?: CancellationToken): Promise { + const service = await this.waitForService(); + if (service) { + return service.request(LiveShareCommands.getUsableJupyterPython, [], cancelToken); + } + } + + public async getServer(options?: INotebookServerOptions): Promise { + return this.serverCache.get(options); + } + + private async checkSupported(command: string, cancelToken?: CancellationToken): Promise { + const service = await this.waitForService(); + + // Make a remote call on the proxy + if (service) { + const result = await service.request(command, [], cancelToken); + return result as boolean; + } + + return false; + } +} diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts b/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts new file mode 100644 index 000000000000..5b5d7e97b0d1 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/guestJupyterNotebook.ts @@ -0,0 +1,354 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { Kernel, KernelMessage } from '@jupyterlab/services'; +import type { JSONObject } from '@phosphor/coreutils'; +import { Observable } from 'rxjs/Observable'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import * as vsls from 'vsls/vscode'; +import { ServerStatus } from '../../../../datascience-ui/interactive-common/mainState'; +import { ILiveShareApi } from '../../../common/application/types'; +import { CancellationError } from '../../../common/cancellation'; +import { traceInfo } from '../../../common/logger'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import { createDeferred } from '../../../common/utils/async'; +import * as localize from '../../../common/utils/localize'; +import { noop } from '../../../common/utils/misc'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { LiveShare, LiveShareCommands } from '../../constants'; +import { + ICell, + IJupyterSession, + INotebook, + INotebookCompletion, + INotebookExecutionInfo, + INotebookExecutionLogger, + INotebookProviderConnection, + InterruptResult, + KernelSocketInformation +} from '../../types'; +import { KernelConnectionMetadata } from '../kernels/types'; +import { LiveShareParticipantDefault, LiveShareParticipantGuest } from './liveShareParticipantMixin'; +import { ResponseQueue } from './responseQueue'; +import { IExecuteObservableResponse, ILiveShareParticipant, IServerResponse } from './types'; + +export class GuestJupyterNotebook + extends LiveShareParticipantGuest(LiveShareParticipantDefault, LiveShare.JupyterNotebookSharedService) + implements INotebook, ILiveShareParticipant { + private get jupyterLab(): typeof import('@jupyterlab/services') { + if (!this._jupyterLab) { + // tslint:disable-next-line:no-require-imports + this._jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); // NOSONAR + } + return this._jupyterLab!; + } + + public get identity(): Uri { + return this._identity; + } + + public get resource(): Resource { + return this._resource; + } + + public get connection(): INotebookProviderConnection | undefined { + return this._executionInfo?.connectionInfo; + } + public kernelSocket = new Observable(); + + public get onSessionStatusChanged(): Event { + if (!this.onStatusChangedEvent) { + this.onStatusChangedEvent = new EventEmitter(); + } + return this.onStatusChangedEvent.event; + } + + public get status(): ServerStatus { + return ServerStatus.Idle; + } + + public get session(): IJupyterSession { + throw new Error('Method not implemented'); + } + public onKernelChanged = new EventEmitter().event; + public onKernelRestarted = new EventEmitter().event; + public onKernelInterrupted = new EventEmitter().event; + public onDisposed = new EventEmitter().event; + public get disposed() { + return false; + } + private _jupyterLab?: typeof import('@jupyterlab/services'); + private responseQueue: ResponseQueue = new ResponseQueue(); + private onStatusChangedEvent: EventEmitter | undefined; + + constructor( + liveShare: ILiveShareApi, + private disposableRegistry: IDisposableRegistry, + private configService: IConfigurationService, + private _resource: Resource, + private _identity: Uri, + private _executionInfo: INotebookExecutionInfo | undefined, + private startTime: number + ) { + super(liveShare); + } + + public shutdown(): Promise { + return Promise.resolve(); + } + + public dispose(): Promise { + if (this.onStatusChangedEvent) { + this.onStatusChangedEvent.dispose(); + } + return this.shutdown(); + } + + public waitForIdle(): Promise { + return Promise.resolve(); + } + + public clear(_id: string): void { + // We don't do anything as we don't cache results in this class. + noop(); + } + + public async execute( + code: string, + file: string, + line: number, + id: string, + cancelToken?: CancellationToken + ): Promise { + // Create a deferred that we'll fire when we're done + const deferred = createDeferred(); + + // Attempt to evaluate this cell in the jupyter notebook + const observable = this.executeObservable(code, file, line, id); + let output: ICell[]; + + observable.subscribe( + (cells: ICell[]) => { + output = cells; + }, + (error) => { + deferred.reject(error); + }, + () => { + deferred.resolve(output); + } + ); + + if (cancelToken) { + this.disposableRegistry.push( + cancelToken.onCancellationRequested(() => deferred.reject(new CancellationError())) + ); + } + + // Wait for the execution to finish + return deferred.promise; + } + + public async inspect(code: string): Promise { + // Send to the other side + return this.sendRequest(LiveShareCommands.inspect, [code]); + } + + public setLaunchingFile(_directory: string): Promise { + // Ignore this command on this side + return Promise.resolve(); + } + + public async setMatplotLibStyle(_useDark: boolean): Promise { + // Guest can't change the style. Maybe output a warning here? + } + + public executeObservable(code: string, file: string, line: number, id: string): Observable { + // Mimic this to the other side and then wait for a response + this.waitForService() + .then((s) => { + if (s) { + s.notify(LiveShareCommands.executeObservable, { code, file, line, id }); + } + }) + .ignoreErrors(); + return this.responseQueue.waitForObservable(code, id); + } + + public async restartKernel(): Promise { + // We need to force a restart on the host side + return this.sendRequest(LiveShareCommands.restart, []); + } + + public async interruptKernel(_timeoutMs: number): Promise { + const settings = this.configService.getSettings(this.resource); + const interruptTimeout = settings.datascience.jupyterInterruptTimeout; + + const response = await this.sendRequest(LiveShareCommands.interrupt, [interruptTimeout]); + return response as InterruptResult; + } + + public async waitForServiceName(): Promise { + // Use our base name plus our id. This means one unique server per notebook + // Live share will not accept a '.' in the name so remove any + const uriString = this.identity.toString(); + return Promise.resolve(`${LiveShare.JupyterNotebookSharedService}${uriString}`); + } + + public async getSysInfo(): Promise { + // This is a special case. Ask the shared server + const service = await this.waitForService(); + if (service) { + const result = await service.request(LiveShareCommands.getSysInfo, []); + return result as ICell; + } + } + + public async getCompletion( + _cellCode: string, + _offsetInCode: number, + _cancelToken?: CancellationToken + ): Promise { + return Promise.resolve({ + matches: [], + cursor: { + start: 0, + end: 0 + }, + metadata: {} + }); + } + + public async onAttach(api: vsls.LiveShare | null): Promise { + await super.onAttach(api); + + if (api) { + const service = await this.waitForService(); + + // Wait for sync up + const synced = service ? await service.request(LiveShareCommands.syncRequest, []) : undefined; + if (!synced && api.session && api.session.role !== vsls.Role.None) { + throw new Error(localize.DataScience.liveShareSyncFailure()); + } + + if (service) { + // Listen to responses + service.onNotify(LiveShareCommands.serverResponse, this.onServerResponse); + + // Request all of the responses since this guest was started. We likely missed a bunch + service.notify(LiveShareCommands.catchupRequest, { since: this.startTime }); + } + } + } + + public getMatchingInterpreter(): PythonEnvironment | undefined { + return; + } + + public getKernelConnection(): KernelConnectionMetadata | undefined { + return; + } + + public setKernelConnection(_spec: KernelConnectionMetadata, _timeout: number): Promise { + return Promise.resolve(); + } + public getLoggers(): INotebookExecutionLogger[] { + return []; + } + + public registerCommTarget( + _targetName: string, + _callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike + ) { + noop(); + } + + public sendCommMessage( + buffers: (ArrayBuffer | ArrayBufferView)[], + content: { comm_id: string; data: JSONObject; target_name: string | undefined }, + // tslint:disable-next-line: no-any + metadata: any, + // tslint:disable-next-line: no-any + msgId: any + ): Kernel.IShellFuture< + KernelMessage.IShellMessage<'comm_msg'>, + KernelMessage.IShellMessage + > { + const shellMessage = this.jupyterLab?.KernelMessage.createMessage>({ + // tslint:disable-next-line: no-any + msgType: 'comm_msg', + channel: 'shell', + buffers, + content, + metadata, + msgId, + session: '1', + username: '1' + }); + + return { + done: Promise.resolve(undefined), + msg: shellMessage!, // NOSONAR + onReply: noop, + onIOPub: noop, + onStdin: noop, + registerMessageHook: noop, + removeMessageHook: noop, + sendInputReply: noop, + isDisposed: false, + dispose: noop + }; + } + + public requestCommInfo( + _content: KernelMessage.ICommInfoRequestMsg['content'] + ): Promise { + const shellMessage = this.jupyterLab?.KernelMessage.createMessage({ + msgType: 'comm_info_reply', + channel: 'shell', + content: { + status: 'ok' + // tslint:disable-next-line: no-any + } as any, + metadata: {}, + session: '1', + username: '1' + }); + + return Promise.resolve(shellMessage); + } + public registerMessageHook( + _msgId: string, + _hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void { + noop(); + } + public removeMessageHook( + _msgId: string, + _hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void { + noop(); + } + + public registerIOPubListener(_listener: (msg: KernelMessage.IIOPubMessage, requestId: string) => void): void { + noop(); + } + + private onServerResponse = (args: Object) => { + const er = args as IExecuteObservableResponse; + traceInfo(`Guest serverResponse ${er.pos} ${er.id}`); + // Args should be of type ServerResponse. Stick in our queue if so. + if (args.hasOwnProperty('type')) { + this.responseQueue.push(args as IServerResponse); + } + }; + + // tslint:disable-next-line:no-any + private async sendRequest(command: string, args: any[]): Promise { + const service = await this.waitForService(); + if (service) { + return service.request(command, args); + } + } +} diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts b/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts new file mode 100644 index 000000000000..9e0c9a19c82f --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as uuid from 'uuid/v4'; +import { Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import * as vsls from 'vsls/vscode'; +import { ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; +import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import { createDeferred, Deferred } from '../../../common/utils/async'; +import * as localize from '../../../common/utils/localize'; +import { IServiceContainer } from '../../../ioc/types'; +import { LiveShare, LiveShareCommands } from '../../constants'; +import { + IJupyterConnection, + IJupyterSessionManagerFactory, + INotebook, + INotebookServer, + INotebookServerLaunchInfo +} from '../../types'; +import { GuestJupyterNotebook } from './guestJupyterNotebook'; +import { LiveShareParticipantDefault, LiveShareParticipantGuest } from './liveShareParticipantMixin'; +import { ILiveShareParticipant } from './types'; + +export class GuestJupyterServer + extends LiveShareParticipantGuest(LiveShareParticipantDefault, LiveShare.JupyterServerSharedService) + implements INotebookServer, ILiveShareParticipant { + private launchInfo: INotebookServerLaunchInfo | undefined; + private connectPromise: Deferred = createDeferred(); + private _id = uuid(); + private notebooks = new Map>(); + + constructor( + private liveShare: ILiveShareApi, + private activationStartTime: number, + _asyncRegistry: IAsyncDisposableRegistry, + private disposableRegistry: IDisposableRegistry, + private configService: IConfigurationService, + _sessionManager: IJupyterSessionManagerFactory, + _workspaceService: IWorkspaceService, + _serviceContainer: IServiceContainer + ) { + super(liveShare); + } + + public get id(): string { + return this._id; + } + + public async connect(launchInfo: INotebookServerLaunchInfo, _cancelToken?: CancellationToken): Promise { + this.launchInfo = launchInfo; + this.connectPromise.resolve(launchInfo); + return Promise.resolve(); + } + + public async createNotebook(resource: Resource, identity: Uri): Promise { + // Remember we can have multiple native editors opened against the same ipynb file. + if (this.notebooks.get(identity.toString())) { + return this.notebooks.get(identity.toString())!; + } + + const deferred = createDeferred(); + this.notebooks.set(identity.toString(), deferred.promise); + // Tell the host side to generate a notebook for this uri + const service = await this.waitForService(); + if (service) { + const resourceString = resource ? resource.toString() : undefined; + const identityString = identity.toString(); + await service.request(LiveShareCommands.createNotebook, [resourceString, identityString]); + } + + // Return a new notebook to listen to + const result = new GuestJupyterNotebook( + this.liveShare, + this.disposableRegistry, + this.configService, + resource, + identity, + this.launchInfo, + this.activationStartTime + ); + deferred.resolve(result); + const oldDispose = result.dispose.bind(result); + result.dispose = () => { + this.notebooks.delete(identity.toString()); + return oldDispose(); + }; + + return result; + } + + public async onSessionChange(api: vsls.LiveShare | null): Promise { + await super.onSessionChange(api); + + this.notebooks.forEach(async (notebook) => { + const guestNotebook = (await notebook) as GuestJupyterNotebook; + if (guestNotebook) { + await guestNotebook.onSessionChange(api); + } + }); + } + + public async getNotebook(resource: Uri): Promise { + return this.notebooks.get(resource.toString()); + } + + public async shutdown(): Promise { + // Send this across to the other side. Otherwise the host server will remain running (like during an export) + const service = await this.waitForService(); + if (service) { + await service.request(LiveShareCommands.disposeServer, []); + } + } + + public dispose(): Promise { + return this.shutdown(); + } + + // Return a copy of the connection information that this server used to connect with + public getConnectionInfo(): IJupyterConnection | undefined { + if (this.launchInfo) { + return this.launchInfo.connectionInfo; + } + + return undefined; + } + + public waitForConnect(): Promise { + return this.connectPromise.promise; + } + + public async waitForServiceName(): Promise { + // First wait for connect to occur + const launchInfo = await this.waitForConnect(); + + // Use our base name plus our purpose. This means one unique server per purpose + if (!launchInfo) { + return LiveShare.JupyterServerSharedService; + } + // tslint:disable-next-line:no-suspicious-comment + // TODO: Should there be some separator in the name? + return `${LiveShare.JupyterServerSharedService}${launchInfo.purpose}`; + } + + public async onAttach(api: vsls.LiveShare | null): Promise { + await super.onAttach(api); + + if (api) { + const service = await this.waitForService(); + + // Wait for sync up + const synced = service ? await service.request(LiveShareCommands.syncRequest, []) : undefined; + if (!synced && api.session && api.session.role !== vsls.Role.None) { + throw new Error(localize.DataScience.liveShareSyncFailure()); + } + } + } +} diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts b/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts new file mode 100644 index 000000000000..aa90b60ab082 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { CancellationToken } from 'vscode-jsonrpc'; + +import type { Kernel, Session } from '@jupyterlab/services'; +import { EventEmitter } from 'vscode'; +import { noop } from '../../../common/utils/misc'; +import { + IJupyterConnection, + IJupyterKernel, + IJupyterKernelSpec, + IJupyterSession, + IJupyterSessionManager +} from '../../types'; +import { KernelConnectionMetadata } from '../kernels/types'; + +export class GuestJupyterSessionManager implements IJupyterSessionManager { + private connInfo: IJupyterConnection | undefined; + + private restartSessionCreatedEvent = new EventEmitter(); + private restartSessionUsedEvent = new EventEmitter(); + + public constructor(private realSessionManager: IJupyterSessionManager) { + noop(); + } + + public get onRestartSessionCreated() { + return this.restartSessionCreatedEvent.event; + } + + public get onRestartSessionUsed() { + return this.restartSessionUsedEvent.event; + } + public startNew( + kernelConnection: KernelConnectionMetadata | undefined, + workingDirectory: string, + cancelToken?: CancellationToken + ): Promise { + return this.realSessionManager.startNew(kernelConnection, workingDirectory, cancelToken); + } + + public async getKernelSpecs(): Promise { + // Don't return any kernel specs in guest mode. They're only needed for the host side + return Promise.resolve([]); + } + + public getRunningKernels(): Promise { + return Promise.resolve([]); + } + + public getRunningSessions(): Promise { + return Promise.resolve([]); + } + + public async dispose(): Promise { + noop(); + } + + public async initialize(_connInfo: IJupyterConnection): Promise { + this.connInfo = _connInfo; + } + + public getConnInfo(): IJupyterConnection { + return this.connInfo!; + } +} diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterSessionManagerFactory.ts b/src/client/datascience/jupyter/liveshare/guestJupyterSessionManagerFactory.ts new file mode 100644 index 000000000000..bac9057a4ec4 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/guestJupyterSessionManagerFactory.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { Kernel } from '@jupyterlab/services'; +import { EventEmitter } from 'vscode'; +import { noop } from '../../../common/utils/misc'; +import { IJupyterConnection, IJupyterSessionManager, IJupyterSessionManagerFactory } from '../../types'; +import { GuestJupyterSessionManager } from './guestJupyterSessionManager'; + +export class GuestJupyterSessionManagerFactory implements IJupyterSessionManagerFactory { + private restartSessionCreatedEvent = new EventEmitter(); + private restartSessionUsedEvent = new EventEmitter(); + public constructor(private realSessionManager: IJupyterSessionManagerFactory) { + noop(); + } + + public async create(connInfo: IJupyterConnection, failOnPassword?: boolean): Promise { + return new GuestJupyterSessionManager(await this.realSessionManager.create(connInfo, failOnPassword)); + } + + public get onRestartSessionCreated() { + return this.restartSessionCreatedEvent.event; + } + + public get onRestartSessionUsed() { + return this.restartSessionUsedEvent.event; + } +} diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts b/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts new file mode 100644 index 000000000000..422963c45ba7 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../../common/extensions'; + +import * as uuid from 'uuid/v4'; +import { CancellationToken } from 'vscode'; +import * as vsls from 'vsls/vscode'; + +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; +import { traceInfo } from '../../../common/logger'; + +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel +} from '../../../common/types'; +import { noop } from '../../../common/utils/misc'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import { LiveShare, LiveShareCommands } from '../../constants'; +import { + IDataScienceFileSystem, + IJupyterConnection, + IJupyterExecution, + INotebookServer, + INotebookServerOptions +} from '../../types'; +import { getJupyterConnectionDisplayName } from '../jupyterConnection'; +import { JupyterExecutionBase } from '../jupyterExecution'; +import { KernelSelector } from '../kernels/kernelSelector'; +import { NotebookStarter } from '../notebookStarter'; +import { LiveShareParticipantHost } from './liveShareParticipantMixin'; +import { IRoleBasedObject } from './roleBasedFactory'; +import { ServerCache } from './serverCache'; + +// tslint:disable:no-any + +// This class is really just a wrapper around a jupyter execution that also provides a shared live share service +export class HostJupyterExecution + extends LiveShareParticipantHost(JupyterExecutionBase, LiveShare.JupyterExecutionService) + implements IRoleBasedObject, IJupyterExecution { + private serverCache: ServerCache; + private _disposed = false; + private _id = uuid(); + constructor( + liveShare: ILiveShareApi, + interpreterService: IInterpreterService, + disposableRegistry: IDisposableRegistry, + asyncRegistry: IAsyncDisposableRegistry, + fs: IDataScienceFileSystem, + workspace: IWorkspaceService, + configService: IConfigurationService, + kernelSelector: KernelSelector, + notebookStarter: NotebookStarter, + appShell: IApplicationShell, + jupyterOutputChannel: IOutputChannel, + serviceContainer: IServiceContainer + ) { + super( + liveShare, + interpreterService, + disposableRegistry, + workspace, + configService, + kernelSelector, + notebookStarter, + appShell, + jupyterOutputChannel, + serviceContainer + ); + this.serverCache = new ServerCache(configService, workspace, fs); + asyncRegistry.push(this); + } + + public async dispose(): Promise { + traceInfo(`Disposing HostJupyterExecution ${this._id}`); + if (!this._disposed) { + this._disposed = true; + traceInfo(`Disposing super HostJupyterExecution ${this._id}`); + await super.dispose(); + traceInfo(`Getting live share API during dispose HostJupyterExecution ${this._id}`); + const api = await this.api; + traceInfo(`Detaching HostJupyterExecution ${this._id}`); + await this.onDetach(api); + + // Cleanup on dispose. We are going away permanently + if (this.serverCache) { + traceInfo(`Cleaning up server cache ${this._id}`); + await this.serverCache.dispose(); + } + } + traceInfo(`Finished disposing HostJupyterExecution ${this._id}`); + } + + public async hostConnectToNotebookServer( + options?: INotebookServerOptions, + cancelToken?: CancellationToken + ): Promise { + if (!this._disposed) { + return super.connectToNotebookServer(await this.serverCache.generateDefaultOptions(options), cancelToken); + } + } + + public async connectToNotebookServer( + options?: INotebookServerOptions, + cancelToken?: CancellationToken + ): Promise { + if (!this._disposed) { + return this.serverCache.getOrCreate(this.hostConnectToNotebookServer.bind(this), options, cancelToken); + } + } + + public async onAttach(api: vsls.LiveShare | null): Promise { + if (!this._disposed) { + await super.onAttach(api); + + if (api) { + const service = await this.waitForService(); + + // Register handlers for all of the supported remote calls + if (service) { + service.onRequest(LiveShareCommands.isNotebookSupported, this.onRemoteIsNotebookSupported); + service.onRequest(LiveShareCommands.isImportSupported, this.onRemoteIsImportSupported); + service.onRequest(LiveShareCommands.connectToNotebookServer, this.onRemoteConnectToNotebookServer); + service.onRequest(LiveShareCommands.getUsableJupyterPython, this.onRemoteGetUsableJupyterPython); + } + } + } + } + + public async onDetach(api: vsls.LiveShare | null): Promise { + await super.onDetach(api); + + // clear our cached servers if our role is no longer host or none + const newRole = + api === null || (api.session && api.session.role !== vsls.Role.Guest) ? vsls.Role.Host : vsls.Role.Guest; + if (newRole !== vsls.Role.Host) { + await this.serverCache.dispose(); + } + } + + public async getServer(options?: INotebookServerOptions): Promise { + if (!this._disposed) { + // See if we have this server or not. + return this.serverCache.get(options); + } + } + + private onRemoteIsNotebookSupported = (_args: any[], cancellation: CancellationToken): Promise => { + // Just call local + return this.isNotebookSupported(cancellation); + }; + + private onRemoteIsImportSupported = (_args: any[], cancellation: CancellationToken): Promise => { + // Just call local + return this.isImportSupported(cancellation); + }; + + private onRemoteConnectToNotebookServer = async ( + args: any[], + cancellation: CancellationToken + ): Promise => { + // Connect to the local server. THe local server should have started the port forwarding already + const localServer = await this.connectToNotebookServer( + args[0] as INotebookServerOptions | undefined, + cancellation + ); + + // Extract the URI and token for the other side + if (localServer) { + // The other side should be using 'localhost' for anything it's port forwarding. That should just remap + // on the guest side. However we need to eliminate the dispose method. Methods are not serializable + const connectionInfo = localServer.getConnectionInfo(); + if (connectionInfo) { + return { + type: 'jupyter', + baseUrl: connectionInfo.baseUrl, + token: connectionInfo.token, + hostName: connectionInfo.hostName, + localLaunch: false, + localProcExitCode: undefined, + valid: true, + displayName: getJupyterConnectionDisplayName(connectionInfo.token, connectionInfo.baseUrl), + disconnected: (_l) => { + return { dispose: noop }; + }, + dispose: noop, + rootDirectory: connectionInfo.rootDirectory + }; + } + } + }; + + private onRemoteGetUsableJupyterPython = (_args: any[], cancellation: CancellationToken): Promise => { + // Just call local + return this.getUsableJupyterPython(cancellation); + }; +} diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts b/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts new file mode 100644 index 000000000000..316ed151c639 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/hostJupyterNotebook.ts @@ -0,0 +1,409 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Observable } from 'rxjs/Observable'; +import * as vscode from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import * as vsls from 'vsls/vscode'; +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; +import '../../../common/extensions'; +import { traceError } from '../../../common/logger'; + +import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import { createDeferred } from '../../../common/utils/async'; +import { Identifiers, LiveShare, LiveShareCommands } from '../../constants'; +import { IExecuteInfo } from '../../interactive-common/interactiveWindowTypes'; +import { + ICell, + IDataScienceFileSystem, + IJupyterSession, + INotebook, + INotebookExecutionInfo, + INotebookExecutionLogger, + InterruptResult +} from '../../types'; +import { JupyterNotebookBase } from '../jupyterNotebook'; +import { LiveShareParticipantHost } from './liveShareParticipantMixin'; +import { ResponseQueue } from './responseQueue'; +import { IRoleBasedObject } from './roleBasedFactory'; +import { IExecuteObservableResponse, IResponseMapping, IServerResponse, ServerResponseType } from './types'; + +// tslint:disable-next-line: no-require-imports +import cloneDeep = require('lodash/cloneDeep'); + +// tslint:disable:no-any + +export class HostJupyterNotebook + extends LiveShareParticipantHost(JupyterNotebookBase, LiveShare.JupyterNotebookSharedService) + implements IRoleBasedObject, INotebook { + private catchupResponses: ResponseQueue = new ResponseQueue(); + private localResponses: ResponseQueue = new ResponseQueue(); + private requestLog: Map = new Map(); + private catchupPendingCount: number = 0; + private isDisposed = false; + constructor( + liveShare: ILiveShareApi, + session: IJupyterSession, + configService: IConfigurationService, + disposableRegistry: IDisposableRegistry, + executionInfo: INotebookExecutionInfo, + loggers: INotebookExecutionLogger[], + resource: Resource, + identity: vscode.Uri, + getDisposedError: () => Error, + workspace: IWorkspaceService, + appService: IApplicationShell, + fs: IDataScienceFileSystem + ) { + super( + liveShare, + session, + configService, + disposableRegistry, + executionInfo, + loggers, + resource, + identity, + getDisposedError, + workspace, + appService, + fs + ); + } + + public dispose = async (): Promise => { + if (!this.isDisposed) { + this.isDisposed = true; + await super.dispose(); + const api = await this.api; + return this.onDetach(api); + } + }; + + public async onAttach(api: vsls.LiveShare | null): Promise { + await super.onAttach(api); + + if (api && !this.isDisposed) { + const service = await this.waitForService(); + + // Attach event handlers to different requests + if (service) { + // Requests return arrays + service.onRequest(LiveShareCommands.syncRequest, (_args: any[], _cancellation: CancellationToken) => + this.onSync() + ); + service.onRequest(LiveShareCommands.getSysInfo, (_args: any[], cancellation: CancellationToken) => + this.onGetSysInfoRequest(cancellation) + ); + service.onRequest(LiveShareCommands.inspect, (args: any[], cancellation: CancellationToken) => + this.inspect(args[0], 0, cancellation) + ); + service.onRequest(LiveShareCommands.restart, (args: any[], cancellation: CancellationToken) => + this.onRestartRequest( + args.length > 0 ? (args[0] as number) : LiveShare.InterruptDefaultTimeout, + cancellation + ) + ); + service.onRequest(LiveShareCommands.interrupt, (args: any[], cancellation: CancellationToken) => + this.onInterruptRequest( + args.length > 0 ? (args[0] as number) : LiveShare.InterruptDefaultTimeout, + cancellation + ) + ); + service.onRequest(LiveShareCommands.disposeServer, (_args: any[], _cancellation: CancellationToken) => + this.dispose() + ); + + // Notifications are always objects. + service.onNotify(LiveShareCommands.catchupRequest, (args: object) => this.onCatchupRequest(args)); + service.onNotify(LiveShareCommands.executeObservable, (args: object) => + this.onExecuteObservableRequest(args) + ); + } + } + } + + public async waitForServiceName(): Promise { + // Use our base name plus our id. This means one unique server per notebook + // Convert to our shared URI to match the guest and remove any '.' as live share won't support them + const sharedUri = + this.identity.scheme === 'file' ? this.finishedApi!.convertLocalUriToShared(this.identity) : this.identity; + return Promise.resolve(`${LiveShare.JupyterNotebookSharedService}${sharedUri.toString()}`); + } + + public async onPeerChange(ev: vsls.PeersChangeEvent): Promise { + await super.onPeerChange(ev); + + // Keep track of the number of guests that need to do a catchup request + this.catchupPendingCount += + ev.added.filter((e) => e.role === vsls.Role.Guest).length - + ev.removed.filter((e) => e.role === vsls.Role.Guest).length; + } + + public clear(id: string): void { + this.requestLog.delete(id); + } + + public executeObservable( + code: string, + file: string, + line: number, + id: string, + silent?: boolean + ): Observable { + // See if this has already been asked for not + if (this.requestLog.has(id)) { + // This must be a local call that occurred after a guest call. Just + // use the local responses to return the results. + return this.localResponses.waitForObservable(code, id); + } else { + // Otherwise make a new request and save response in the catchup list. THis is a + // a request that came directly from the host so the host will be listening to the observable returned + // and we don't need to save the response in the local queue. + return this.makeObservableRequest(code, file, line, id, silent, [this.catchupResponses]); + } + } + + public async restartKernel(timeoutMs: number): Promise { + try { + await super.restartKernel(timeoutMs); + } catch (exc) { + this.postException(exc, []); + throw exc; + } + } + + public async interruptKernel(timeoutMs: number): Promise { + try { + return super.interruptKernel(timeoutMs); + } catch (exc) { + this.postException(exc, []); + throw exc; + } + } + + private makeRequest( + code: string, + file: string, + line: number, + id: string, + silent: boolean | undefined, + responseQueues: ResponseQueue[] + ): Promise { + // Create a deferred that we'll fire when we're done + const deferred = createDeferred(); + + // Attempt to evaluate this cell in the jupyter notebook + const observable = this.makeObservableRequest(code, file, line, id, silent, responseQueues); + let output: ICell[]; + + observable.subscribe( + (cells: ICell[]) => { + output = cells; + }, + (error) => { + deferred.reject(error); + }, + () => { + deferred.resolve(output); + } + ); + + // Wait for the execution to finish + return deferred.promise; + } + + private makeObservableRequest( + code: string, + file: string, + line: number, + id: string, + silent: boolean | undefined, + responseQueues: ResponseQueue[] + ): Observable { + try { + this.requestLog.set(id, Date.now()); + const inner = super.executeObservable(code, file, line, id, silent); + + // Cleanup old requests + const now = Date.now(); + for (const [k, val] of this.requestLog) { + if (now - val > LiveShare.ResponseLifetime) { + this.requestLog.delete(k); + } + } + + // Wrap the observable returned to send the responses to the guest(s) too. + return this.postObservableResult(code, inner, id, responseQueues); + } catch (exc) { + this.postException(exc, responseQueues); + throw exc; + } + } + + private translateCellForGuest(cell: ICell): ICell { + const copy = { ...cell }; + if (this.role === vsls.Role.Host && this.finishedApi && copy.file !== Identifiers.EmptyFileName) { + copy.file = this.finishedApi.convertLocalUriToShared(vscode.Uri.file(copy.file)).fsPath; + } + return copy; + } + + private onSync(): Promise { + return Promise.resolve(true); + } + + private onGetSysInfoRequest(_cancellation: CancellationToken): Promise { + // Get the sys info from our local server + return super.getSysInfo(); + } + + private onRestartRequest(timeout: number, _cancellation: CancellationToken): Promise { + // Just call the base + return super.restartKernel(timeout); + } + private onInterruptRequest(timeout: number, _cancellation: CancellationToken): Promise { + // Just call the base + return super.interruptKernel(timeout); + } + + private async onCatchupRequest(args: object): Promise { + if (args.hasOwnProperty('since')) { + const service = await this.waitForService(); + if (service) { + // Send results for all responses that are left. + this.catchupResponses.send(service, this.translateForGuest.bind(this)); + + // Eliminate old responses if possible. + this.catchupPendingCount -= 1; + if (this.catchupPendingCount <= 0) { + this.catchupResponses.clear(); + } + } + } + } + + private onExecuteObservableRequest(args: object) { + // See if we started this execute or not already. + if (args.hasOwnProperty('code')) { + const obj = args as IExecuteInfo; + if (!this.requestLog.has(obj.id)) { + try { + // Convert the file name if necessary + const uri = vscode.Uri.parse(`vsls:${obj.file}`); + const file = + this.finishedApi && obj.file !== Identifiers.EmptyFileName + ? this.finishedApi.convertSharedUriToLocal(uri).fsPath + : obj.file; + + // We need the results of this execute to end up in both the guest responses and the local responses + this.makeRequest(obj.code, file, obj.line, obj.id, false, [ + this.localResponses, + this.catchupResponses + ]).ignoreErrors(); + } catch (e) { + traceError(e); + } + } + } + } + + private postObservableResult( + code: string, + observable: Observable, + id: string, + responseQueues: ResponseQueue[] + ): Observable { + return new Observable((subscriber) => { + let pos = 0; + + // Listen to all of the events on the observable passed in. + observable.subscribe( + (cells) => { + // Forward to the next listener + subscriber.next(cells); + + // Send across to the guest side + try { + this.postObservableNext(code, pos, cells, id, responseQueues); + pos += 1; + } catch (e) { + subscriber.error(e); + this.postException(e, responseQueues); + } + }, + (e) => { + subscriber.error(e); + this.postException(e, responseQueues); + }, + () => { + subscriber.complete(); + this.postObservableComplete(code, pos, id, responseQueues); + } + ); + }); + } + + private translateForGuest = (r: IServerResponse): IServerResponse => { + // Remap the cell paths + const er = r as IExecuteObservableResponse; + if (er && er.cells) { + return { cells: er.cells.map(this.translateCellForGuest, this), ...er }; + } + return r; + }; + + private postObservableNext(code: string, pos: number, cells: ICell[], id: string, responseQueues: ResponseQueue[]) { + this.postResult( + ServerResponseType.ExecuteObservable, + { code, pos, type: ServerResponseType.ExecuteObservable, cells, id, time: Date.now() }, + this.translateForGuest, + responseQueues + ); + } + + private postObservableComplete(code: string, pos: number, id: string, responseQueues: ResponseQueue[]) { + this.postResult( + ServerResponseType.ExecuteObservable, + { code, pos, type: ServerResponseType.ExecuteObservable, cells: undefined, id, time: Date.now() }, + this.translateForGuest, + responseQueues + ); + } + + private postException(exc: any, responseQueues: ResponseQueue[]) { + this.postResult( + ServerResponseType.Exception, + { type: ServerResponseType.Exception, time: Date.now(), message: exc.toString() }, + (r) => r, + responseQueues + ); + } + + private postResult( + _type: T, + result: R[T], + guestTranslator: (r: IServerResponse) => IServerResponse, + responseQueues: ResponseQueue[] + ): void { + const typedResult = (result as any) as IServerResponse; + if (typedResult) { + try { + // Make a deep copy before we send. Don't want local copies being modified + const deepCopy = cloneDeep(typedResult); + this.waitForService() + .then((s) => { + if (s) { + s.notify(LiveShareCommands.serverResponse, guestTranslator(deepCopy)); + } + }) + .ignoreErrors(); + + // Need to also save in memory for those guests that are in the middle of starting up + responseQueues.forEach((r) => r.push(deepCopy)); + } catch (exc) { + traceError(exc); + } + } + } +} diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts new file mode 100644 index 000000000000..09ea53800ffb --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts @@ -0,0 +1,354 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../../common/extensions'; + +import * as os from 'os'; +import * as vscode from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import * as vsls from 'vsls/vscode'; +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; +import { isTestExecution } from '../../../common/constants'; +import { traceInfo } from '../../../common/logger'; +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + Resource +} from '../../../common/types'; +import { createDeferred } from '../../../common/utils/async'; +import * as localize from '../../../common/utils/localize'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import { Identifiers, LiveShare, LiveShareCommands, RegExpValues } from '../../constants'; +import { + IDataScienceFileSystem, + IJupyterSession, + IJupyterSessionManager, + IJupyterSessionManagerFactory, + INotebook, + INotebookExecutionLogger, + INotebookMetadataLive, + INotebookServer, + INotebookServerLaunchInfo +} from '../../types'; +import { JupyterServerBase } from '../jupyterServer'; +import { computeWorkingDirectory } from '../jupyterUtils'; +import { KernelSelector } from '../kernels/kernelSelector'; +import { HostJupyterNotebook } from './hostJupyterNotebook'; +import { LiveShareParticipantHost } from './liveShareParticipantMixin'; +import { IRoleBasedObject } from './roleBasedFactory'; +// tslint:disable:no-any + +export class HostJupyterServer extends LiveShareParticipantHost(JupyterServerBase, LiveShare.JupyterServerSharedService) + implements IRoleBasedObject, INotebookServer { + private disposed = false; + private portToForward = 0; + private sharedPort: vscode.Disposable | undefined; + constructor( + private liveShare: ILiveShareApi, + _startupTime: number, + asyncRegistry: IAsyncDisposableRegistry, + disposableRegistry: IDisposableRegistry, + configService: IConfigurationService, + sessionManager: IJupyterSessionManagerFactory, + private workspaceService: IWorkspaceService, + serviceContainer: IServiceContainer, + private appService: IApplicationShell, + private fs: IDataScienceFileSystem, + private readonly kernelSelector: KernelSelector, + private readonly interpreterService: IInterpreterService, + outputChannel: IOutputChannel + ) { + super( + liveShare, + asyncRegistry, + disposableRegistry, + configService, + sessionManager, + serviceContainer, + outputChannel + ); + } + + public async dispose(): Promise { + if (!this.disposed) { + this.disposed = true; + traceInfo(`Disposing HostJupyterServer`); + await super.dispose(); + const api = await this.api; + await this.onDetach(api); + traceInfo(`Finished disposing HostJupyterServer`); + } + } + + public async connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken): Promise { + if (launchInfo.connectionInfo && launchInfo.connectionInfo.localLaunch) { + const portMatch = RegExpValues.ExtractPortRegex.exec(launchInfo.connectionInfo.baseUrl); + if (portMatch && portMatch.length > 1) { + const port = parseInt(portMatch[1], 10); + await this.attemptToForwardPort(this.finishedApi, port); + } + } + return super.connect(launchInfo, cancelToken); + } + + public async onAttach(api: vsls.LiveShare | null): Promise { + await super.onAttach(api); + + if (api && !this.disposed) { + const service = await this.waitForService(); + + // Attach event handlers to different requests + if (service) { + // Requests return arrays + service.onRequest(LiveShareCommands.syncRequest, (_args: any[], _cancellation: CancellationToken) => + this.onSync() + ); + service.onRequest(LiveShareCommands.disposeServer, (_args: any[], _cancellation: CancellationToken) => + this.dispose() + ); + service.onRequest( + LiveShareCommands.createNotebook, + async (args: any[], cancellation: CancellationToken) => { + const resource = this.parseUri(args[0]); + const identity = this.parseUri(args[1]); + // Don't return the notebook. We don't want it to be serialized. We just want its live share server to be started. + const notebook = (await this.createNotebook( + resource, + identity!, + undefined, + cancellation + )) as HostJupyterNotebook; + await notebook.onAttach(api); + } + ); + + // See if we need to forward the port + await this.attemptToForwardPort(api, this.portToForward); + } + } + } + + public async onSessionChange(api: vsls.LiveShare | null): Promise { + await super.onSessionChange(api); + + this.getNotebooks().forEach(async (notebook) => { + const hostNotebook = (await notebook) as HostJupyterNotebook; + if (hostNotebook) { + await hostNotebook.onSessionChange(api); + } + }); + } + + public async onDetach(api: vsls.LiveShare | null): Promise { + await super.onDetach(api); + + // Make sure to unshare our port + if (api && this.sharedPort) { + this.sharedPort.dispose(); + this.sharedPort = undefined; + } + } + + public async waitForServiceName(): Promise { + // First wait for connect to occur + const launchInfo = await this.waitForConnect(); + + // Use our base name plus our purpose. This means one unique server per purpose + if (!launchInfo) { + return LiveShare.JupyterServerSharedService; + } + // tslint:disable-next-line:no-suspicious-comment + // TODO: Should there be some separator in the name? + return `${LiveShare.JupyterServerSharedService}${launchInfo.purpose}`; + } + + protected get isDisposed() { + return this.disposed; + } + + protected async createNotebookInstance( + resource: Resource, + identity: vscode.Uri, + sessionManager: IJupyterSessionManager, + possibleSession: IJupyterSession | undefined, + disposableRegistry: IDisposableRegistry, + configService: IConfigurationService, + serviceContainer: IServiceContainer, + notebookMetadata?: INotebookMetadataLive, + cancelToken?: CancellationToken + ): Promise { + // See if already exists. + const existing = await this.getNotebook(identity); + if (existing) { + // Dispose the possible session as we don't need it + if (possibleSession) { + await possibleSession.dispose(); + } + + // Then we can return the existing notebook. + return existing; + } + + // Compute launch information from the resource and the notebook metadata + const notebookPromise = createDeferred(); + // Save the notebook + this.setNotebook(identity, notebookPromise.promise); + + const getExistingSession = async () => { + const { info, changedKernel } = await this.computeLaunchInfo( + resource, + sessionManager, + notebookMetadata, + cancelToken + ); + + // If we switched kernels, try switching the possible session + if (changedKernel && possibleSession && info.kernelConnectionMetadata) { + await possibleSession.changeKernel( + info.kernelConnectionMetadata, + this.configService.getSettings(resource).datascience.jupyterLaunchTimeout + ); + } + + // Figure out the working directory we need for our new notebook. + const workingDirectory = await computeWorkingDirectory(resource, this.workspaceService); + + // Start a session (or use the existing one if allowed) + const session = + possibleSession && this.fs.areLocalPathsSame(possibleSession.workingDirectory, workingDirectory) + ? possibleSession + : await sessionManager.startNew(info.kernelConnectionMetadata, workingDirectory, cancelToken); + traceInfo(`Started session ${this.id}`); + return { info, session }; + }; + + try { + const { info, session } = await getExistingSession(); + + if (session) { + // Create our notebook + const notebook = new HostJupyterNotebook( + this.liveShare, + session, + configService, + disposableRegistry, + info, + serviceContainer.getAll(INotebookExecutionLogger), + resource, + identity, + this.getDisposedError.bind(this), + this.workspaceService, + this.appService, + this.fs + ); + + // Wait for it to be ready + traceInfo(`Waiting for idle (session) ${this.id}`); + const idleTimeout = configService.getSettings().datascience.jupyterLaunchTimeout; + await notebook.waitForIdle(idleTimeout); + + // Run initial setup + await notebook.initialize(cancelToken); + + traceInfo(`Finished connecting ${this.id}`); + + notebookPromise.resolve(notebook); + } else { + notebookPromise.reject(this.getDisposedError()); + } + } catch (ex) { + // If there's an error, then reject the promise that is returned. + // This original promise must be rejected as it is cached (check `setNotebook`). + notebookPromise.reject(ex); + } + + return notebookPromise.promise; + } + + private async computeLaunchInfo( + resource: Resource, + sessionManager: IJupyterSessionManager, + notebookMetadata?: INotebookMetadataLive, + cancelToken?: CancellationToken + ): Promise<{ info: INotebookServerLaunchInfo; changedKernel: boolean }> { + // First we need our launch information so we can start a new session (that's what our notebook is really) + let launchInfo = await this.waitForConnect(); + if (!launchInfo) { + throw this.getDisposedError(); + } + // Create a copy of launch info, cuz we're modifying it here. + // This launch info contains the server connection info (that could be shared across other nbs). + // However the kernel info is different. The kernel info is stored as a property of this, hence create a separate instance for each nb. + launchInfo = { + ...launchInfo + }; + + // Determine the interpreter for our resource. If different, we need a different kernel. + const resourceInterpreter = await this.interpreterService.getActiveInterpreter(resource); + + // Find a kernel that can be used. + // Do this only if kernel information has been provided in the metadata, or the resource's interpreter is different. + let changedKernel = false; + if ( + notebookMetadata?.kernelspec || + notebookMetadata?.id || + resourceInterpreter?.displayName !== launchInfo.kernelConnectionMetadata?.interpreter?.displayName + ) { + const kernelInfo = await (launchInfo.connectionInfo.localLaunch + ? this.kernelSelector.getPreferredKernelForLocalConnection( + resource, + 'jupyter', + sessionManager, + notebookMetadata, + isTestExecution(), + cancelToken + ) + : this.kernelSelector.getPreferredKernelForRemoteConnection( + resource, + sessionManager, + notebookMetadata, + cancelToken + )); + + if (kernelInfo) { + launchInfo.kernelConnectionMetadata = kernelInfo; + + // For the interpreter, make sure to select the one matching the kernel. + launchInfo.kernelConnectionMetadata.interpreter = + launchInfo.kernelConnectionMetadata.interpreter || resourceInterpreter; + changedKernel = true; + } + } + + return { info: launchInfo, changedKernel }; + } + + private parseUri(uri: string | undefined): Resource { + const parsed = uri ? vscode.Uri.parse(uri) : undefined; + return parsed && + parsed.scheme && + parsed.scheme !== Identifiers.InteractiveWindowIdentityScheme && + parsed.scheme === 'vsls' + ? this.finishedApi!.convertSharedUriToLocal(parsed) + : parsed; + } + + private async attemptToForwardPort(api: vsls.LiveShare | null | undefined, port: number): Promise { + if (port !== 0 && api && api.session && api.session.role === vsls.Role.Host) { + this.portToForward = 0; + this.sharedPort = await api.shareServer({ + port, + displayName: localize.DataScience.liveShareHostFormat().format(os.hostname()) + }); + } else { + this.portToForward = port; + } + } + + private onSync(): Promise { + return Promise.resolve(true); + } +} diff --git a/src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts b/src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts new file mode 100644 index 000000000000..84a1ac362b32 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as vsls from 'vsls/vscode'; + +import { ILiveShareApi } from '../../../common/application/types'; +import '../../../common/extensions'; +import { IAsyncDisposable } from '../../../common/types'; +import { noop } from '../../../common/utils/misc'; +import { ClassType } from '../../../ioc/types'; +import { ILiveShareParticipant } from './types'; +import { waitForGuestService, waitForHostService } from './utils'; + +// tslint:disable:no-any + +export class LiveShareParticipantDefault implements IAsyncDisposable { + constructor(..._rest: any[]) { + noop(); + } + + public async dispose(): Promise { + noop(); + } +} + +export function LiveShareParticipantGuest>(SuperClass: T, serviceName: string) { + return LiveShareParticipantMixin( + SuperClass, + vsls.Role.Guest, + serviceName, + waitForGuestService + ); +} + +export function LiveShareParticipantHost>(SuperClass: T, serviceName: string) { + return LiveShareParticipantMixin( + SuperClass, + vsls.Role.Host, + serviceName, + waitForHostService + ); +} + +/** + * This is called a mixin class in TypeScript. + * Allows us to have different base classes but inherit behavior (workaround for not allowing multiple inheritance). + * Essentially it sticks a temp class in between the base class and the class you're writing. + * Something like this: + * + * class Base { + * doStuff() { + * + * } + * } + * + * function Mixin = (SuperClass) { + * return class extends SuperClass { + * doExtraStuff() { + * super.doStuff(); + * } + * } + * } + * + * function SubClass extends Mixin(Base) { + * doBar() : { + * super.doExtraStuff(); + * } + * } + * + */ +function LiveShareParticipantMixin, S>( + SuperClass: T, + expectedRole: vsls.Role, + serviceName: string, + serviceWaiter: (api: vsls.LiveShare, name: string) => Promise +) { + return class extends SuperClass implements ILiveShareParticipant { + protected finishedApi: vsls.LiveShare | null | undefined; + protected api: Promise; + private actualRole = vsls.Role.None; + private wantedRole = expectedRole; + private servicePromise: Promise | undefined; + private serviceFullName: string | undefined; + + constructor(...rest: any[]) { + super(...rest); + // First argument should be our live share api + if (rest.length > 0) { + const liveShare = rest[0] as ILiveShareApi; + this.api = liveShare.getApi(); + this.api + .then((a) => { + this.finishedApi = a; + this.onSessionChange(a).ignoreErrors(); + }) + .ignoreErrors(); + } else { + this.api = Promise.resolve(null); + } + } + + public get role() { + return this.actualRole; + } + + public async onPeerChange(_ev: vsls.PeersChangeEvent): Promise { + noop(); + } + + public async onAttach(_api: vsls.LiveShare | null): Promise { + noop(); + } + + public waitForServiceName(): Promise { + // Default is just to return the server name + return Promise.resolve(serviceName); + } + + public onDetach(api: vsls.LiveShare | null): Promise { + if (api && this.serviceFullName && api.session && api.session.role === vsls.Role.Host) { + return api.unshareService(this.serviceFullName); + } + return Promise.resolve(); + } + + public async onSessionChange(api: vsls.LiveShare | null): Promise { + this.servicePromise = undefined; + const newRole = api !== null && api.session ? api.session.role : vsls.Role.None; + if (newRole !== this.actualRole) { + this.actualRole = newRole; + if (newRole === this.wantedRole) { + this.onAttach(api).ignoreErrors(); + } else { + this.onDetach(api).ignoreErrors(); + } + } + } + + public async waitForService(): Promise { + if (this.servicePromise) { + return this.servicePromise; + } + const api = await this.api; + if (!api || api.session.role !== this.wantedRole) { + this.servicePromise = Promise.resolve(undefined); + } else { + this.serviceFullName = this.sanitizeServiceName(await this.waitForServiceName()); + this.servicePromise = serviceWaiter(api, this.serviceFullName); + } + + return this.servicePromise; + } + + // Liveshare doesn't support '.' in service names + private sanitizeServiceName(baseServiceName: string): string { + return baseServiceName.replace('.', ''); + } + }; +} diff --git a/src/client/datascience/jupyter/liveshare/responseQueue.ts b/src/client/datascience/jupyter/liveshare/responseQueue.ts new file mode 100644 index 000000000000..86de298eaad4 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/responseQueue.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Observable } from 'rxjs/Observable'; +import { Subscriber } from 'rxjs/Subscriber'; +import * as vsls from 'vsls/vscode'; + +import { createDeferred, Deferred } from '../../../common/utils/async'; +import { LiveShareCommands } from '../../constants'; +import { ICell } from '../../types'; +import { IExecuteObservableResponse, IServerResponse } from './types'; + +export class ResponseQueue { + private responseQueue: IServerResponse[] = []; + private waitingQueue: { deferred: Deferred; predicate(r: IServerResponse): boolean }[] = []; + + public waitForObservable(code: string, id: string): Observable { + // Create a wrapper observable around the actual server + return new Observable((subscriber) => { + // Wait for the observable responses to come in + this.waitForResponses(subscriber, code, id).catch((e) => { + subscriber.error(e); + subscriber.complete(); + }); + }); + } + + public push(response: IServerResponse) { + this.responseQueue.push(response); + this.dispatchResponse(response); + } + + public send(service: vsls.SharedService, translator: (r: IServerResponse) => IServerResponse) { + this.responseQueue.forEach((r) => service.notify(LiveShareCommands.serverResponse, translator(r))); + } + + public clear() { + this.responseQueue = []; + } + + private async waitForResponses(subscriber: Subscriber, code: string, id: string): Promise { + let pos = 0; + let cells: ICell[] | undefined = []; + while (cells !== undefined) { + // Find all matches in order + const response = await this.waitForSpecificResponse((r) => { + return r.pos === pos && id === r.id && code === r.code; + }); + if (response.cells) { + subscriber.next(response.cells); + pos += 1; + } + cells = response.cells; + } + subscriber.complete(); + + // Clear responses after we respond to the subscriber. + this.responseQueue = this.responseQueue.filter((r) => { + const er = r as IExecuteObservableResponse; + return er.id !== id; + }); + } + + private waitForSpecificResponse(predicate: (response: T) => boolean): Promise { + // See if we have any responses right now with this type + const index = this.responseQueue.findIndex((r) => predicate(r as T)); + if (index >= 0) { + // Pull off the match + const match = this.responseQueue[index]; + + // Return this single item + return Promise.resolve(match as T); + } else { + // We have to wait for a new input to happen + const waitable = { deferred: createDeferred(), predicate }; + this.waitingQueue.push(waitable); + return waitable.deferred.promise; + } + } + + private dispatchResponse(response: IServerResponse) { + // Look through all of our responses that are queued up and see if they make a + // waiting promise resolve + const matchIndex = this.waitingQueue.findIndex((w) => w.predicate(response)); + if (matchIndex >= 0) { + this.waitingQueue[matchIndex].deferred.resolve(response); + this.waitingQueue.splice(matchIndex, 1); + } + } +} diff --git a/src/client/datascience/jupyter/liveshare/roleBasedFactory.ts b/src/client/datascience/jupyter/liveshare/roleBasedFactory.ts new file mode 100644 index 000000000000..c85f6a645018 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/roleBasedFactory.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as vscode from 'vscode'; +import * as vsls from 'vsls/vscode'; + +import { ILiveShareApi } from '../../../common/application/types'; +import { IAsyncDisposable } from '../../../common/types'; +import { ClassType } from '../../../ioc/types'; +import { ILiveShareHasRole, ILiveShareParticipant } from './types'; + +export interface IRoleBasedObject extends IAsyncDisposable, ILiveShareParticipant {} + +// tslint:disable:no-any +export class RoleBasedFactory> implements ILiveShareHasRole { + private ctorArgs: ConstructorParameters[]; + private firstTime: boolean = true; + private createPromise: Promise | undefined; + private sessionChangedEmitter = new vscode.EventEmitter(); + private _role: vsls.Role = vsls.Role.None; + + constructor( + private liveShare: ILiveShareApi, + private hostCtor: CtorType, + private guestCtor: CtorType, + ...args: ConstructorParameters + ) { + this.ctorArgs = args; + this.createPromise = this.createBasedOnRole(); // We need to start creation immediately or one side may call before we init. + } + + public get sessionChanged(): vscode.Event { + return this.sessionChangedEmitter.event; + } + + public get role(): vsls.Role { + return this._role; + } + + public get(): Promise { + // Make sure only one create happens at a time + if (this.createPromise) { + return this.createPromise; + } + this.createPromise = this.createBasedOnRole(); + return this.createPromise; + } + + private async createBasedOnRole(): Promise { + // Figure out our role to compute the object to create. Default is host. This + // allows for the host object to keep existing if we suddenly start a new session. + // For a guest, starting a new session resets the entire workspace. + const api = await this.liveShare.getApi(); + let ctor: CtorType = this.hostCtor; + let role: vsls.Role = vsls.Role.Host; + + if (api) { + // Create based on role. + if (api.session && api.session.role === vsls.Role.Host) { + ctor = this.hostCtor; + } else if (api.session && api.session.role === vsls.Role.Guest) { + ctor = this.guestCtor; + role = vsls.Role.Guest; + } + } + this._role = role; + + // Create our object + const obj = new ctor(...this.ctorArgs); + + // Rewrite the object's dispose so we can get rid of our own state. + let objDisposed = false; + const oldDispose = obj.dispose.bind(obj); + obj.dispose = () => { + objDisposed = true; + // Make sure we don't destroy the create promise. Otherwise + // dispose will end up causing the creation code to run again. + return oldDispose(); + }; + + // If the session changes, tell the listener + if (api && this.firstTime) { + this.firstTime = false; + api.onDidChangeSession((_a) => { + // Dispose the object if the role changes + const newRole = + api !== null && api.session && api.session.role === vsls.Role.Guest + ? vsls.Role.Guest + : vsls.Role.Host; + if (newRole !== role) { + // Also have to clear the create promise so we + // run the create code again. + this.createPromise = undefined; + + obj.dispose().ignoreErrors(); + } + + // Update the object with respect to the api + if (!objDisposed) { + obj.onSessionChange(api).ignoreErrors(); + } + + // Fire our event indicating old data is no longer valid. + if (newRole !== role) { + this.sessionChangedEmitter.fire(); + } + }); + api.onDidChangePeers((e) => { + if (!objDisposed) { + obj.onPeerChange(e).ignoreErrors(); + } + }); + } + + return obj; + } +} diff --git a/src/client/datascience/jupyter/liveshare/serverCache.ts b/src/client/datascience/jupyter/liveshare/serverCache.ts new file mode 100644 index 000000000000..a06f6c828ac0 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/serverCache.ts @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../../common/extensions'; + +import * as uuid from 'uuid/v4'; +import { CancellationToken, CancellationTokenSource } from 'vscode'; + +import { IWorkspaceService } from '../../../common/application/types'; +import { traceError, traceInfo } from '../../../common/logger'; + +import { IAsyncDisposable, IConfigurationService } from '../../../common/types'; +import { sleep } from '../../../common/utils/async'; +import { IDataScienceFileSystem, INotebookServer, INotebookServerOptions } from '../../types'; +import { calculateWorkingDirectory } from '../../utils'; + +interface IServerData { + options: INotebookServerOptions; + promise: Promise; + cancelSource: CancellationTokenSource; + resolved: boolean; +} + +export class ServerCache implements IAsyncDisposable { + private cache: Map = new Map(); + private emptyKey = uuid(); + private disposed = false; + + constructor( + private configService: IConfigurationService, + private workspace: IWorkspaceService, + private fs: IDataScienceFileSystem + ) {} + + public async getOrCreate( + createFunction: ( + options?: INotebookServerOptions, + cancelToken?: CancellationToken + ) => Promise, + options?: INotebookServerOptions, + cancelToken?: CancellationToken + ): Promise { + const cancelSource = new CancellationTokenSource(); + if (cancelToken) { + cancelToken.onCancellationRequested(() => cancelSource.cancel()); + } + const fixedOptions = await this.generateDefaultOptions(options); + const key = this.generateKey(fixedOptions); + let data: IServerData | undefined; + + // Check to see if we already have a promise for this key + data = this.cache.get(key); + + if (!data) { + // Didn't find one, so start up our promise and cache it + data = { + promise: createFunction(options, cancelSource.token), + options: fixedOptions, + cancelSource, + resolved: false + }; + this.cache.set(key, data); + } + + return data.promise + .then((server: INotebookServer | undefined) => { + if (!server) { + this.cache.delete(key); + return undefined; + } + + // Change the dispose on it so we + // can detach from the server when it goes away. + const oldDispose = server.dispose.bind(server); + server.dispose = () => { + this.cache.delete(key); + return oldDispose(); + }; + + // We've resolved the promise at this point + if (data) { + data.resolved = true; + } + + return server; + }) + .catch((e) => { + this.cache.delete(key); + throw e; + }); + } + + public async get(options?: INotebookServerOptions): Promise { + const fixedOptions = await this.generateDefaultOptions(options); + const key = this.generateKey(fixedOptions); + if (this.cache.has(key)) { + return this.cache.get(key)?.promise; + } + } + + public async dispose(): Promise { + if (!this.disposed) { + this.disposed = true; + const entries = [...this.cache.values()]; + this.cache.clear(); + await Promise.all( + entries.map(async (d) => { + try { + // This should be quick. The server is either already up or will never come back. + const server = await Promise.race([d.promise, sleep(1000)]); + if (typeof server !== 'number') { + // tslint:disable-next-line: no-any + await (server as any).dispose(); + } else { + traceInfo('ServerCache Dispose, no server'); + } + } catch (e) { + traceError(`Dispose error in ServerCache: `, e); + } + }) + ); + } + } + + public async generateDefaultOptions(options?: INotebookServerOptions): Promise { + return { + uri: options ? options.uri : undefined, + skipUsingDefaultConfig: options ? options.skipUsingDefaultConfig : false, // Default for this is false + usingDarkTheme: options ? options.usingDarkTheme : undefined, + purpose: options ? options.purpose : uuid(), + workingDir: + options && options.workingDir + ? options.workingDir + : await calculateWorkingDirectory(this.configService, this.workspace, this.fs), + metadata: options?.metadata, + allowUI: options?.allowUI ? options.allowUI : () => false + }; + } + + private generateKey(options?: INotebookServerOptions): string { + if (!options) { + return this.emptyKey; + } else { + // combine all the values together to make a unique key + const uri = options.uri ? options.uri : ''; + const useFlag = options.skipUsingDefaultConfig ? 'true' : 'false'; + return `${options.purpose}${uri}${useFlag}${options.workingDir}`; + } + } +} diff --git a/src/client/datascience/jupyter/liveshare/types.ts b/src/client/datascience/jupyter/liveshare/types.ts new file mode 100644 index 000000000000..5e662b135171 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/types.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as vsls from 'vsls/vscode'; + +import { IAsyncDisposable } from '../../../common/types'; +import { ICell } from '../../types'; + +// tslint:disable:max-classes-per-file + +export enum ServerResponseType { + ExecuteObservable, + Exception +} + +export interface IServerResponse { + type: ServerResponseType; + time: number; +} + +export interface IExecuteObservableResponse extends IServerResponse { + pos: number; + code: string; + id: string; // Unique id so guest side can tell what observable it belongs with + cells: ICell[] | undefined; +} + +export interface IExceptionResponse extends IServerResponse { + message: string; +} + +// Map all responses to their properties +export interface IResponseMapping { + [ServerResponseType.ExecuteObservable]: IExecuteObservableResponse; + [ServerResponseType.Exception]: IExceptionResponse; +} + +export interface ICatchupRequest { + since: number; +} + +export interface ILiveShareHasRole { + readonly role: vsls.Role; +} + +export interface ILiveShareParticipant extends IAsyncDisposable, ILiveShareHasRole { + onSessionChange(api: vsls.LiveShare | null): Promise; + onAttach(api: vsls.LiveShare | null): Promise; + onDetach(api: vsls.LiveShare | null): Promise; + onPeerChange(ev: vsls.PeersChangeEvent): Promise; + waitForServiceName(): Promise; +} diff --git a/src/client/datascience/jupyter/liveshare/utils.ts b/src/client/datascience/jupyter/liveshare/utils.ts new file mode 100644 index 000000000000..9c6f39c10a31 --- /dev/null +++ b/src/client/datascience/jupyter/liveshare/utils.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Disposable, Event } from 'vscode'; +import * as vsls from 'vsls/vscode'; + +import { createDeferred } from '../../../common/utils/async'; + +export async function waitForHostService(api: vsls.LiveShare, name: string): Promise { + const service = await api.shareService(name); + if (service && !service.isServiceAvailable) { + return waitForAvailability(service); + } + return service; +} + +export async function waitForGuestService(api: vsls.LiveShare, name: string): Promise { + const service = await api.getSharedService(name); + if (service && !service.isServiceAvailable) { + return waitForAvailability(service); + } + return service; +} + +interface IChangeWatchable { + readonly onDidChangeIsServiceAvailable: Event; +} + +async function waitForAvailability(service: T): Promise { + const deferred = createDeferred(); + let disposable: Disposable | undefined; + try { + disposable = service.onDidChangeIsServiceAvailable((e) => { + if (e) { + deferred.resolve(service); + } + }); + await deferred.promise; + } finally { + if (disposable) { + disposable.dispose(); + } + } + return service; +} diff --git a/src/client/datascience/jupyter/notebookStarter.ts b/src/client/datascience/jupyter/notebookStarter.ts new file mode 100644 index 000000000000..1eabbd547821 --- /dev/null +++ b/src/client/datascience/jupyter/notebookStarter.ts @@ -0,0 +1,300 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as cp from 'child_process'; +import { inject, injectable, named } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { CancellationToken, Disposable } from 'vscode'; +import { CancellationError, createPromiseFromCancellation } from '../../common/cancellation'; +import { WrappedError } from '../../common/errors/errorUtils'; +import { traceInfo } from '../../common/logger'; +import { TemporaryDirectory } from '../../common/platform/types'; +import { IDisposable, IOutputChannel } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { IServiceContainer } from '../../ioc/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { JUPYTER_OUTPUT_CHANNEL, Telemetry } from '../constants'; +import { reportAction } from '../progress/decorator'; +import { ReportableAction } from '../progress/types'; +import { IDataScienceFileSystem, IJupyterConnection, IJupyterSubCommandExecutionService } from '../types'; +import { JupyterConnectionWaiter } from './jupyterConnection'; +import { JupyterInstallError } from './jupyterInstallError'; + +/** + * Responsible for starting a notebook. + * Separate class as theres quite a lot of work involved in starting a notebook. + * + * @export + * @class NotebookStarter + * @implements {Disposable} + */ +@injectable() +export class NotebookStarter implements Disposable { + private readonly disposables: IDisposable[] = []; + constructor( + @inject(IJupyterSubCommandExecutionService) + private readonly jupyterInterpreterService: IJupyterSubCommandExecutionService, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private readonly jupyterOutputChannel: IOutputChannel + ) {} + public dispose() { + while (this.disposables.length > 0) { + const disposable = this.disposables.shift(); + try { + if (disposable) { + disposable.dispose(); + } + } catch { + // Nohting + } + } + } + // tslint:disable-next-line: max-func-body-length + @reportAction(ReportableAction.NotebookStart) + public async start( + useDefaultConfig: boolean, + customCommandLine: string[], + workingDirectory: string, + cancelToken?: CancellationToken + ): Promise { + traceInfo('Starting Notebook'); + // Now actually launch it + let exitCode: number | null = 0; + let starter: JupyterConnectionWaiter | undefined; + try { + // Generate a temp dir with a unique GUID, both to match up our started server and to easily clean up after + const tempDirPromise = this.generateTempDir(); + tempDirPromise.then((dir) => this.disposables.push(dir)).ignoreErrors(); + // Before starting the notebook process, make sure we generate a kernel spec + const args = await this.generateArguments( + useDefaultConfig, + customCommandLine, + tempDirPromise, + workingDirectory + ); + + // Make sure we haven't canceled already. + if (cancelToken && cancelToken.isCancellationRequested) { + throw new CancellationError(); + } + + // Then use this to launch our notebook process. + traceInfo('Starting Jupyter Notebook'); + const stopWatch = new StopWatch(); + const [launchResult, tempDir] = await Promise.all([ + this.jupyterInterpreterService.startNotebook(args || [], { + throwOnStdErr: false, + encoding: 'utf8', + token: cancelToken + }), + tempDirPromise + ]); + + // Watch for premature exits + if (launchResult.proc) { + launchResult.proc.on('exit', (c: number | null) => (exitCode = c)); + launchResult.out.subscribe((out) => this.jupyterOutputChannel.append(out.out)); + } + + // Make sure this process gets cleaned up. We might be canceled before the connection finishes. + if (launchResult && cancelToken) { + cancelToken.onCancellationRequested(() => { + launchResult.dispose(); + }); + } + + // Wait for the connection information on this result + traceInfo('Waiting for Jupyter Notebook'); + starter = new JupyterConnectionWaiter( + launchResult, + tempDir.path, + workingDirectory, + this.jupyterInterpreterService.getRunningJupyterServers.bind(this.jupyterInterpreterService), + this.serviceContainer, + cancelToken + ); + // Make sure we haven't canceled already. + if (cancelToken && cancelToken.isCancellationRequested) { + throw new CancellationError(); + } + const connection = await Promise.race([ + starter.waitForConnection(), + createPromiseFromCancellation({ + cancelAction: 'reject', + defaultValue: new CancellationError(), + token: cancelToken + }) + ]); + + if (connection instanceof CancellationError) { + throw connection; + } + + // Fire off telemetry for the process being talkable + sendTelemetryEvent(Telemetry.StartJupyterProcess, stopWatch.elapsedTime); + + return connection; + } catch (err) { + if (err instanceof CancellationError) { + throw err; + } + + // Its possible jupyter isn't installed. Check the errors. + if (!(await this.jupyterInterpreterService.isNotebookSupported())) { + throw new JupyterInstallError( + await this.jupyterInterpreterService.getReasonForJupyterNotebookNotBeingSupported(), + localize.DataScience.pythonInteractiveHelpLink() + ); + } + + // Something else went wrong. See if the local proc died or not. + if (exitCode !== 0) { + throw new Error(localize.DataScience.jupyterServerCrashed().format(exitCode?.toString())); + } else { + throw new WrappedError(localize.DataScience.jupyterNotebookFailure().format(err), err); + } + } finally { + starter?.dispose(); + } + } + + private async generateDefaultArguments( + useDefaultConfig: boolean, + tempDirPromise: Promise, + workingDirectory: string + ): Promise { + // Parallelize as much as possible. + const promisedArgs: Promise[] = []; + promisedArgs.push(Promise.resolve('--no-browser')); + promisedArgs.push(Promise.resolve(this.getNotebookDirArgument(workingDirectory))); + if (useDefaultConfig) { + promisedArgs.push(this.getConfigArgument(tempDirPromise)); + } + // Modify the data rate limit if starting locally. The default prevents large dataframes from being returned. + promisedArgs.push(Promise.resolve('--NotebookApp.iopub_data_rate_limit=10000000000.0')); + + const [args, dockerArgs] = await Promise.all([Promise.all(promisedArgs), this.getDockerArguments()]); + + // Check for the debug environment variable being set. Setting this + // causes Jupyter to output a lot more information about what it's doing + // under the covers and can be used to investigate problems with Jupyter. + const debugArgs = process.env && process.env.VSCODE_PYTHON_DEBUG_JUPYTER ? ['--debug'] : []; + + // Use this temp file and config file to generate a list of args for our command + return [...args, ...dockerArgs, ...debugArgs]; + } + + private async generateCustomArguments(customCommandLine: string[]): Promise { + // We still have a bunch of args we have to pass + const requiredArgs = ['--no-browser', '--NotebookApp.iopub_data_rate_limit=10000000000.0']; + + return [...requiredArgs, ...customCommandLine]; + } + + private async generateArguments( + useDefaultConfig: boolean, + customCommandLine: string[], + tempDirPromise: Promise, + workingDirectory: string + ): Promise { + if (!customCommandLine || customCommandLine.length === 0) { + return this.generateDefaultArguments(useDefaultConfig, tempDirPromise, workingDirectory); + } + return this.generateCustomArguments(customCommandLine); + } + + /** + * Gets the `--notebook-dir` argument. + * + * @private + * @param {Promise} tempDirectory + * @returns {Promise} + * @memberof NotebookStarter + */ + private getNotebookDirArgument(workingDirectory: string): string { + // Escape the result so Jupyter can actually read it. + return `--notebook-dir="${workingDirectory.replace(/\\/g, '\\\\')}"`; + } + + /** + * Gets the `--config` argument. + * + * @private + * @param {Promise} tempDirectory + * @returns {Promise} + * @memberof NotebookStarter + */ + private async getConfigArgument(tempDirectory: Promise): Promise { + const tempDir = await tempDirectory; + // In the temp dir, create an empty config python file. This is the same + // as starting jupyter with all of the defaults. + const configFile = path.join(tempDir.path, 'jupyter_notebook_config.py'); + await this.fs.writeLocalFile(configFile, ''); + traceInfo(`Generating custom default config at ${configFile}`); + + // Create extra args based on if we have a config or not + return `--config=${configFile}`; + } + + /** + * Adds the `--ip` and `--allow-root` arguments when in docker. + * + * @private + * @param {Promise} tempDirectory + * @returns {Promise} + * @memberof NotebookStarter + */ + private async getDockerArguments(): Promise { + const args: string[] = []; + // Check for a docker situation. + try { + const cgroup = await this.fs.readLocalFile('/proc/self/cgroup').catch(() => ''); + if (!cgroup.includes('docker') && !cgroup.includes('kubepods')) { + return args; + } + // We definitely need an ip address. + args.push('--ip'); + args.push('127.0.0.1'); + + // Now see if we need --allow-root. + return new Promise((resolve) => { + cp.exec('id', { encoding: 'utf-8' }, (_, stdout: string | Buffer) => { + if (stdout && stdout.toString().includes('(root)')) { + args.push('--allow-root'); + } + resolve(args); + }); + }); + } catch { + return args; + } + } + private async generateTempDir(): Promise { + const resultDir = path.join(os.tmpdir(), uuid()); + await this.fs.createLocalDirectory(resultDir); + + return { + path: resultDir, + dispose: async () => { + // Try ten times. Process may still be up and running. + // We don't want to do async as async dispose means it may never finish and then we don't + // delete + let count = 0; + while (count < 10) { + try { + await this.fs.deleteLocalDirectory(resultDir); + count = 10; + } catch { + count += 1; + } + } + } + }; + } +} diff --git a/src/client/datascience/jupyter/oldJupyterVariables.ts b/src/client/datascience/jupyter/oldJupyterVariables.ts new file mode 100644 index 000000000000..7c1d77de7135 --- /dev/null +++ b/src/client/datascience/jupyter/oldJupyterVariables.ts @@ -0,0 +1,450 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { nbformat } from '@jupyterlab/coreutils'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import stripAnsi from 'strip-ansi'; +import * as uuid from 'uuid/v4'; + +import { Event, EventEmitter, Uri } from 'vscode'; +import { PYTHON_LANGUAGE } from '../../common/constants'; +import { traceError } from '../../common/logger'; + +import { IConfigurationService } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { Identifiers, Settings } from '../constants'; +import { + ICell, + IDataScienceFileSystem, + IJupyterVariable, + IJupyterVariables, + IJupyterVariablesRequest, + IJupyterVariablesResponse, + INotebook +} from '../types'; +import { JupyterDataRateLimitError } from './jupyterDataRateLimitError'; +import { getKernelConnectionLanguage, isPythonKernelConnection } from './kernels/helpers'; + +// tslint:disable-next-line: no-var-requires no-require-imports + +// Regexes for parsing data from Python kernel. Not sure yet if other +// kernels will add the ansi encoding. +const TypeRegex = /.*?\[.*?;31mType:.*?\[0m\s+(\w+)/; +const ValueRegex = /.*?\[.*?;31mValue:.*?\[0m\s+(.*)/; +const StringFormRegex = /.*?\[.*?;31mString form:.*?\[0m\s*?([\s\S]+?)\n(.*\[.*;31m?)/; +const DocStringRegex = /.*?\[.*?;31mDocstring:.*?\[0m\s+(.*)/; +const CountRegex = /.*?\[.*?;31mLength:.*?\[0m\s+(.*)/; +const ShapeRegex = /^\s+\[(\d+) rows x (\d+) columns\]/m; + +const DataViewableTypes: Set = new Set(['DataFrame', 'list', 'dict', 'ndarray', 'Series']); + +interface INotebookState { + currentExecutionCount: number; + variables: IJupyterVariable[]; +} + +@injectable() +export class OldJupyterVariables implements IJupyterVariables { + private fetchDataFrameInfoScript?: string; + private fetchDataFrameRowsScript?: string; + private fetchVariableShapeScript?: string; + private filesLoaded: boolean = false; + private languageToQueryMap = new Map(); + private notebookState = new Map(); + private refreshEventEmitter = new EventEmitter(); + + constructor( + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(IConfigurationService) private configService: IConfigurationService + ) {} + + public get refreshRequired(): Event { + return this.refreshEventEmitter.event; + } + + // IJupyterVariables implementation + public async getVariables( + notebook: INotebook, + request: IJupyterVariablesRequest + ): Promise { + // Run the language appropriate variable fetch + return this.getVariablesBasedOnKernel(notebook, request); + } + + public async getMatchingVariable(_notebook: INotebook, _name: string): Promise { + // Not supported with old method. + return undefined; + } + + public async getDataFrameInfo(targetVariable: IJupyterVariable, notebook: INotebook): Promise { + // Run the get dataframe info script + return this.runScript( + notebook, + targetVariable, + targetVariable, + () => this.fetchDataFrameInfoScript, + [{ key: '_VSCode_JupyterValuesColumn', value: localize.DataScience.valuesColumn() }] + ); + } + + public async getDataFrameRows( + targetVariable: IJupyterVariable, + notebook: INotebook, + start: number, + end: number + ): Promise<{}> { + // Run the get dataframe rows script + return this.runScript<{}>(notebook, targetVariable, {}, () => this.fetchDataFrameRowsScript, [ + { key: '_VSCode_JupyterValuesColumn', value: localize.DataScience.valuesColumn() }, + { key: '_VSCode_JupyterStartRow', value: start.toString() }, + { key: '_VSCode_JupyterEndRow', value: end.toString() } + ]); + } + + // Private methods + // Load our python files for fetching variables + private async loadVariableFiles(): Promise { + let file = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'getJupyterVariableDataFrameInfo.py' + ); + this.fetchDataFrameInfoScript = await this.fs.readLocalFile(file); + + file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'vscode_datascience_helpers', 'getJupyterVariableShape.py'); + this.fetchVariableShapeScript = await this.fs.readLocalFile(file); + + file = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'getJupyterVariableDataFrameRows.py' + ); + this.fetchDataFrameRowsScript = await this.fs.readLocalFile(file); + + this.filesLoaded = true; + } + + private async runScript( + notebook: INotebook, + targetVariable: IJupyterVariable | undefined, + defaultValue: T, + scriptBaseTextFetcher: () => string | undefined, + extraReplacements: { key: string; value: string }[] = [] + ): Promise { + if (!this.filesLoaded) { + await this.loadVariableFiles(); + } + + const scriptBaseText = scriptBaseTextFetcher(); + if (!notebook || !scriptBaseText) { + // No active server just return the unchanged target variable + return defaultValue; + } + + // Prep our targetVariable to send over. Remove the 'value' as it's not necessary for getting df info and can have invalid data in it + const pruned = { ...targetVariable, value: '' }; + const variableString = JSON.stringify(pruned); + + // Setup a regex + const regexPattern = + extraReplacements.length === 0 + ? '_VSCode_JupyterTestValue' + : ['_VSCode_JupyterTestValue', ...extraReplacements.map((v) => v.key)].join('|'); + const replaceRegex = new RegExp(regexPattern, 'g'); + + // Replace the test value with our current value. Replace start and end as well + const scriptText = scriptBaseText.replace(replaceRegex, (match: string) => { + if (match === '_VSCode_JupyterTestValue') { + return variableString; + } else { + const index = extraReplacements.findIndex((v) => v.key === match); + if (index >= 0) { + return extraReplacements[index].value; + } + } + + return match; + }); + + // Execute this on the notebook passed in. + const results = await notebook.execute(scriptText, Identifiers.EmptyFileName, 0, uuid(), undefined, true); + + // Results should be the updated variable. + return this.deserializeJupyterResult(results); + } + + private extractJupyterResultText(cells: ICell[]): string { + // Verify that we have the correct cell type and outputs + if (cells.length > 0 && cells[0].data) { + const codeCell = cells[0].data as nbformat.ICodeCell; + if (codeCell.outputs.length > 0) { + const codeCellOutput = codeCell.outputs[0] as nbformat.IOutput; + if ( + codeCellOutput && + codeCellOutput.output_type === 'stream' && + codeCellOutput.name === 'stderr' && + codeCellOutput.hasOwnProperty('text') + ) { + const resultString = codeCellOutput.text as string; + // See if this the IOPUB data rate limit problem + if (resultString.includes('iopub_data_rate_limit')) { + throw new JupyterDataRateLimitError(); + } else { + const error = localize.DataScience.jupyterGetVariablesExecutionError().format(resultString); + traceError(error); + throw new Error(error); + } + } + if (codeCellOutput && codeCellOutput.output_type === 'execute_result') { + const data = codeCellOutput.data; + if (data && data.hasOwnProperty('text/plain')) { + // tslint:disable-next-line:no-any + return (data as any)['text/plain']; + } + } + if ( + codeCellOutput && + codeCellOutput.output_type === 'stream' && + codeCellOutput.hasOwnProperty('text') + ) { + return codeCellOutput.text as string; + } + if ( + codeCellOutput && + codeCellOutput.output_type === 'error' && + codeCellOutput.hasOwnProperty('traceback') + ) { + const traceback: string[] = codeCellOutput.traceback as string[]; + const stripped = traceback.map(stripAnsi).join('\r\n'); + const error = localize.DataScience.jupyterGetVariablesExecutionError().format(stripped); + traceError(error); + throw new Error(error); + } + } + } + + throw new Error(localize.DataScience.jupyterGetVariablesBadResults()); + } + + // Pull our text result out of the Jupyter cell + private deserializeJupyterResult(cells: ICell[]): T { + const text = this.extractJupyterResultText(cells); + return JSON.parse(text) as T; + } + + private getParser(notebook: INotebook) { + // Figure out kernel language + const language = getKernelConnectionLanguage(notebook?.getKernelConnection()) || PYTHON_LANGUAGE; + + // We may have cached this information + let result = this.languageToQueryMap.get(language); + if (!result) { + let query = this.configService + .getSettings(notebook.resource) + .datascience.variableQueries.find((v) => v.language === language); + if (!query && language === PYTHON_LANGUAGE) { + query = Settings.DefaultVariableQuery; + } + + // Use the query to generate our regex + if (query) { + result = { + query: query.query, + parser: new RegExp(query.parseExpr, 'g') + }; + this.languageToQueryMap.set(language, result); + } + } + + return result; + } + + private getAllMatches(regex: RegExp, text: string): string[] { + const result: string[] = []; + let m: RegExpExecArray | null = null; + // tslint:disable-next-line: no-conditional-assignment + while ((m = regex.exec(text)) !== null) { + if (m.index === regex.lastIndex) { + regex.lastIndex += 1; + } + if (m.length > 1) { + result.push(m[1]); + } + } + // Rest after searching + regex.lastIndex = -1; + return result; + } + + private async getVariablesBasedOnKernel( + notebook: INotebook, + request: IJupyterVariablesRequest + ): Promise { + // See if we already have the name list + let list = this.notebookState.get(notebook.identity); + if (!list || list.currentExecutionCount !== request.executionCount) { + // Refetch the list of names from the notebook. They might have changed. + list = { + currentExecutionCount: request.executionCount, + variables: (await this.getVariableNamesFromKernel(notebook)).map((n) => { + return { + name: n, + value: undefined, + supportsDataExplorer: false, + type: '', + size: 0, + shape: '', + count: 0, + truncated: true + }; + }) + }; + } + + const exclusionList = this.configService.getSettings(notebook.resource).datascience.variableExplorerExclude + ? this.configService.getSettings().datascience.variableExplorerExclude?.split(';') + : []; + + const result: IJupyterVariablesResponse = { + executionCount: request.executionCount, + pageStartIndex: -1, + pageResponse: [], + totalCount: 0, + refreshCount: request.refreshCount + }; + + // Use the list of names to fetch the page of data + if (list) { + const startPos = request.startIndex ? request.startIndex : 0; + const chunkSize = request.pageSize ? request.pageSize : 100; + result.pageStartIndex = startPos; + + // Do one at a time. All at once doesn't work as they all have to wait for each other anyway + for (let i = startPos; i < startPos + chunkSize && i < list.variables.length; ) { + const fullVariable = list.variables[i].value + ? list.variables[i] + : await this.getVariableValueFromKernel(list.variables[i], notebook); + + // See if this is excluded or not. + if (exclusionList && exclusionList.indexOf(fullVariable.type) >= 0) { + // Not part of our actual list. Remove from the real list too + list.variables.splice(i, 1); + } else { + list.variables[i] = fullVariable; + result.pageResponse.push(fullVariable); + i += 1; + } + } + + // Save in our cache + this.notebookState.set(notebook.identity, list); + + // Update total count (exclusions will change this as types are computed) + result.totalCount = list.variables.length; + } + + return result; + } + + private async getVariableNamesFromKernel(notebook: INotebook): Promise { + // Get our query and parser + const query = this.getParser(notebook); + + // Now execute the query + if (notebook && query) { + const cells = await notebook.execute(query.query, Identifiers.EmptyFileName, 0, uuid(), undefined, true); + const text = this.extractJupyterResultText(cells); + + // Apply the expression to it + const matches = this.getAllMatches(query.parser, text); + + // Turn each match into a value + if (matches) { + return matches; + } + } + + return []; + } + + private async getVariableValueFromKernel( + targetVariable: IJupyterVariable, + notebook: INotebook + ): Promise { + let result = { ...targetVariable }; + if (notebook) { + const output = await notebook.inspect(targetVariable.name); + + // Should be a text/plain inside of it (at least IPython does this) + if (output && output.hasOwnProperty('text/plain')) { + // tslint:disable-next-line: no-any + const text = (output as any)['text/plain'].toString(); + + // Parse into bits + const type = TypeRegex.exec(text); + const value = ValueRegex.exec(text); + const stringForm = StringFormRegex.exec(text); + const docString = DocStringRegex.exec(text); + const count = CountRegex.exec(text); + const shape = ShapeRegex.exec(text); + if (type) { + result.type = type[1]; + } + if (value) { + result.value = value[1]; + } else if (stringForm) { + result.value = stringForm[1]; + } else if (docString) { + result.value = docString[1]; + } else { + result.value = ''; + } + if (count) { + result.count = parseInt(count[1], 10); + } + if (shape) { + result.shape = `(${shape[1]}, ${shape[2]})`; + } + } + + // Otherwise look for the appropriate entries + if (output.type) { + result.type = output.type.toString(); + } + if (output.value) { + result.value = output.value.toString(); + } + + // Determine if supports viewing based on type + if (DataViewableTypes.has(result.type)) { + result.supportsDataExplorer = true; + } + } + + // For a python kernel, we might be able to get a better shape. It seems the 'inspect' request doesn't always return it. + // Do this only when necessary as this is a LOT slower than an inspect request. Like 4 or 5 times as slow + if ( + result.type && + result.count && + !result.shape && + isPythonKernelConnection(notebook.getKernelConnection()) && + result.supportsDataExplorer && + result.type !== 'list' // List count is good enough + ) { + const computedShape = await this.runScript( + notebook, + result, + result, + () => this.fetchVariableShapeScript + ); + // Only want shape and count from the request. Other things might have been destroyed + result = { ...result, shape: computedShape.shape, count: computedShape.count }; + } + + return result; + } +} diff --git a/src/client/datascience/jupyter/serverPreload.ts b/src/client/datascience/jupyter/serverPreload.ts new file mode 100644 index 000000000000..481af8e0bfee --- /dev/null +++ b/src/client/datascience/jupyter/serverPreload.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { traceError, traceInfo } from '../../common/logger'; +import { IConfigurationService } from '../../common/types'; +import { + IInteractiveWindow, + IInteractiveWindowProvider, + INotebookAndInteractiveWindowUsageTracker, + INotebookEditorProvider, + INotebookProvider +} from '../types'; + +@injectable() +export class ServerPreload implements IExtensionSingleActivationService { + constructor( + @inject(INotebookAndInteractiveWindowUsageTracker) + private readonly tracker: INotebookAndInteractiveWindowUsageTracker, + @inject(INotebookEditorProvider) private notebookEditorProvider: INotebookEditorProvider, + @inject(IInteractiveWindowProvider) private interactiveProvider: IInteractiveWindowProvider, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(INotebookProvider) private notebookProvider: INotebookProvider + ) { + this.notebookEditorProvider.onDidOpenNotebookEditor(this.onDidOpenNotebook.bind(this)); + this.interactiveProvider.onDidChangeActiveInteractiveWindow(this.onDidOpenOrCloseInteractive.bind(this)); + } + public activate(): Promise { + // This is the list of things that should cause us to start a local server + // 1) Notebook is opened + // 2) Notebook was opened in the past 7 days + // 3) Interactive window was opened in the past 7 days + // 4) Interactive window is opened + // And the user has specified local server in their settings. + this.checkDateForServerStart(); + + // Don't hold up activation though + return Promise.resolve(); + } + + private checkDateForServerStart() { + if ( + this.shouldAutoStartStartServer(this.tracker.lastInteractiveWindowOpened) || + this.shouldAutoStartStartServer(this.tracker.lastNotebookOpened) + ) { + this.createServerIfNecessary().ignoreErrors(); + } + } + private shouldAutoStartStartServer(lastTime?: Date) { + if (!lastTime) { + return false; + } + const currentTime = new Date(); + const diff = currentTime.getTime() - lastTime.getTime(); + const diffInDays = Math.floor(diff / (24 * 3600 * 1000)); + return diffInDays <= 7; + } + + private async createServerIfNecessary() { + try { + traceInfo(`Attempting to start a server because of preload conditions ...`); + + // Check if we are already connected + let providerConnection = await this.notebookProvider.connect({ getOnly: true, disableUI: true }); + + // If it didn't start, attempt for local and if allowed. + if (!providerConnection && !this.configService.getSettings(undefined).datascience.disableJupyterAutoStart) { + // Local case, try creating one + providerConnection = await this.notebookProvider.connect({ + getOnly: false, + disableUI: true, + localOnly: true + }); + } + } catch (exc) { + traceError(`Error starting server in serverPreload: `, exc); + } + } + + private onDidOpenNotebook() { + // Automatically start a server whenever we open a notebook + this.createServerIfNecessary().ignoreErrors(); + } + + private onDidOpenOrCloseInteractive(interactive: IInteractiveWindow | undefined) { + if (interactive) { + this.createServerIfNecessary().ignoreErrors(); + } + } +} diff --git a/src/client/datascience/jupyter/serverSelector.ts b/src/client/datascience/jupyter/serverSelector.ts new file mode 100644 index 000000000000..201c0c6eee6f --- /dev/null +++ b/src/client/datascience/jupyter/serverSelector.ts @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { isNil } from 'lodash'; +import { ConfigurationTarget, Memento, QuickPickItem, Uri } from 'vscode'; +import { IClipboard, ICommandManager } from '../../common/application/types'; +import { GLOBAL_MEMENTO, IConfigurationService, IMemento } from '../../common/types'; +import { DataScience } from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { + IMultiStepInput, + IMultiStepInputFactory, + InputFlowAction, + InputStep, + IQuickPickParameters +} from '../../common/utils/multiStepInput'; +import { captureTelemetry } from '../../telemetry'; +import { getSavedUriList } from '../common'; +import { Identifiers, Settings, Telemetry } from '../constants'; +import { IJupyterUriProvider, IJupyterUriProviderRegistration, JupyterServerUriHandle } from '../types'; + +const defaultUri = 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe'; + +interface ISelectUriQuickPickItem extends QuickPickItem { + newChoice: boolean; + provider?: IJupyterUriProvider; + url?: string; +} + +@injectable() +export class JupyterServerSelector { + private readonly localLabel = `$(zap) ${DataScience.jupyterSelectURILocalLabel()}`; + private readonly newLabel = `$(server) ${DataScience.jupyterSelectURINewLabel()}`; + private readonly remoteLabel = `$(server) ${DataScience.jupyterSelectURIRemoteLabel()}`; + constructor( + @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, + @inject(IClipboard) private readonly clipboard: IClipboard, + @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, + @inject(IConfigurationService) private configuration: IConfigurationService, + @inject(ICommandManager) private cmdManager: ICommandManager, + @inject(IJupyterUriProviderRegistration) + private extraUriProviders: IJupyterUriProviderRegistration + ) {} + + @captureTelemetry(Telemetry.SelectJupyterURI) + public selectJupyterURI(allowLocal: boolean): Promise { + const multiStep = this.multiStepFactory.create<{}>(); + return multiStep.run(this.startSelectingURI.bind(this, allowLocal), {}); + } + + private async startSelectingURI( + allowLocal: boolean, + input: IMultiStepInput<{}>, + _state: {} + ): Promise | void> { + // First step, show a quick pick to choose either the remote or the local. + // newChoice element will be set if the user picked 'enter a new server' + const item = await input.showQuickPick>({ + placeholder: DataScience.jupyterSelectURIQuickPickPlaceholder(), + items: await this.getUriPickList(allowLocal), + title: allowLocal + ? DataScience.jupyterSelectURIQuickPickTitle() + : DataScience.jupyterSelectURIQuickPickTitleRemoteOnly() + }); + if (item.label === this.localLabel) { + await this.setJupyterURIToLocal(); + } else if (!item.newChoice && !item.provider) { + await this.setJupyterURIToRemote(!isNil(item.url) ? item.url : item.label); + } else if (!item.provider) { + return this.selectRemoteURI.bind(this); + } else { + return this.selectProviderURI.bind(this, item.provider, item); + } + } + + private async selectProviderURI( + provider: IJupyterUriProvider, + item: ISelectUriQuickPickItem, + _input: IMultiStepInput<{}>, + _state: {} + ): Promise | void> { + const result = await provider.handleQuickPick(item, true); + if (result === 'back') { + throw InputFlowAction.back; + } + if (result) { + await this.handleProviderQuickPick(provider.id, result); + } + } + private async handleProviderQuickPick(id: string, result: JupyterServerUriHandle | undefined) { + if (result) { + const uri = this.generateUriFromRemoteProvider(id, result); + await this.setJupyterURIToRemote(uri); + } + } + + private generateUriFromRemoteProvider(id: string, result: JupyterServerUriHandle) { + // tslint:disable-next-line: no-http-string + return `${Identifiers.REMOTE_URI}?${Identifiers.REMOTE_URI_ID_PARAM}=${id}&${ + Identifiers.REMOTE_URI_HANDLE_PARAM + }=${encodeURI(result)}`; + } + + private async selectRemoteURI(input: IMultiStepInput<{}>, _state: {}): Promise | void> { + let initialValue = defaultUri; + try { + const text = await this.clipboard.readText().catch(() => ''); + const parsedUri = Uri.parse(text.trim(), true); + // Only display http/https uris. + initialValue = text && parsedUri && parsedUri.scheme.toLowerCase().startsWith('http') ? text : defaultUri; + } catch { + // We can ignore errors. + } + // Ask the user to enter a URI to connect to. + const uri = await input.showInputBox({ + title: DataScience.jupyterSelectURIPrompt(), + value: initialValue || defaultUri, + validate: this.validateSelectJupyterURI, + prompt: '' + }); + + if (uri) { + await this.setJupyterURIToRemote(uri); + } + } + + @captureTelemetry(Telemetry.SetJupyterURIToLocal) + private async setJupyterURIToLocal(): Promise { + const previousValue = this.configuration.getSettings(undefined).datascience.jupyterServerURI; + await this.configuration.updateSetting( + 'dataScience.jupyterServerURI', + Settings.JupyterServerLocalLaunch, + undefined, + ConfigurationTarget.Workspace + ); + + // Reload if there's a change + if (previousValue !== Settings.JupyterServerLocalLaunch) { + this.cmdManager + .executeCommand('python.reloadVSCode', DataScience.reloadAfterChangingJupyterServerConnection()) + .then(noop, noop); + } + } + + @captureTelemetry(Telemetry.SetJupyterURIToUserSpecified) + private async setJupyterURIToRemote(userURI: string): Promise { + const previousValue = this.configuration.getSettings(undefined).datascience.jupyterServerURI; + await this.configuration.updateSetting( + 'dataScience.jupyterServerURI', + userURI, + undefined, + ConfigurationTarget.Workspace + ); + + // Reload if there's a change + if (previousValue !== userURI) { + this.cmdManager + .executeCommand('python.reloadVSCode', DataScience.reloadAfterChangingJupyterServerConnection()) + .then(noop, noop); + } + } + private validateSelectJupyterURI = async (inputText: string): Promise => { + try { + // tslint:disable-next-line:no-unused-expression + new URL(inputText); + + // Double check http + if (!inputText.toLowerCase().includes('http')) { + throw new Error('Has to be http'); + } + } catch { + return DataScience.jupyterSelectURIInvalidURI(); + } + }; + + private async getUriPickList(allowLocal: boolean): Promise { + // Ask our providers to stick on items + let providerItems: ISelectUriQuickPickItem[] = []; + const providers = await this.extraUriProviders.getProviders(); + if (providers) { + providers.forEach((p) => { + const newproviderItems = p.getQuickPickEntryItems().map((i) => { + return { ...i, newChoice: false, provider: p }; + }); + providerItems = providerItems.concat(newproviderItems); + }); + } + + // Always have 'local' and 'add new' + let items: ISelectUriQuickPickItem[] = []; + if (allowLocal) { + items.push({ label: this.localLabel, detail: DataScience.jupyterSelectURILocalDetail(), newChoice: false }); + items = items.concat(providerItems); + items.push({ label: this.newLabel, detail: DataScience.jupyterSelectURINewDetail(), newChoice: true }); + } else { + items = items.concat(providerItems); + items.push({ + label: this.remoteLabel, + detail: DataScience.jupyterSelectURIRemoteDetail(), + newChoice: true + }); + } + + // Get our list of recent server connections and display that as well + const savedURIList = getSavedUriList(this.globalState); + savedURIList.forEach((uriItem) => { + if (uriItem.uri) { + const uriDate = new Date(uriItem.time); + items.push({ + label: !isNil(uriItem.displayName) ? uriItem.displayName : uriItem.uri, + detail: DataScience.jupyterSelectURIMRUDetail().format(uriDate.toLocaleString()), + newChoice: false, + url: uriItem.uri + }); + } + }); + + return items; + } +} diff --git a/src/client/datascience/jupyter/variableScriptLoader.ts b/src/client/datascience/jupyter/variableScriptLoader.ts new file mode 100644 index 000000000000..8e75093f820f --- /dev/null +++ b/src/client/datascience/jupyter/variableScriptLoader.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as path from 'path'; + +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { IDataScienceFileSystem, IJupyterVariable } from '../types'; + +export class VariableScriptLoader { + private fetchVariableShapeScript?: string; + private filesLoaded: boolean = false; + + constructor(private fs: IDataScienceFileSystem) {} + + public readShapeScript(targetVariable: IJupyterVariable): Promise { + return this.readScript(targetVariable, () => this.fetchVariableShapeScript); + } + + private async readScript( + targetVariable: IJupyterVariable | undefined, + scriptBaseTextFetcher: () => string | undefined, + extraReplacements: { key: string; value: string }[] = [] + ): Promise { + if (!this.filesLoaded) { + await this.loadVariableFiles(); + } + + const scriptBaseText = scriptBaseTextFetcher(); + + // Prep our targetVariable to send over. Remove the 'value' as it's not necessary for getting df info and can have invalid data in it + const pruned = { ...targetVariable, value: '' }; + const variableString = JSON.stringify(pruned); + + // Setup a regex + const regexPattern = + extraReplacements.length === 0 + ? '_VSCode_JupyterTestValue' + : ['_VSCode_JupyterTestValue', ...extraReplacements.map((v) => v.key)].join('|'); + const replaceRegex = new RegExp(regexPattern, 'g'); + + // Replace the test value with our current value. Replace start and end as well + return scriptBaseText + ? scriptBaseText.replace(replaceRegex, (match: string) => { + if (match === '_VSCode_JupyterTestValue') { + return variableString; + } else { + const index = extraReplacements.findIndex((v) => v.key === match); + if (index >= 0) { + return extraReplacements[index].value; + } + } + + return match; + }) + : undefined; + } + + // Load our python files for fetching variables + private async loadVariableFiles(): Promise { + const file = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'getJupyterVariableShape.py' + ); + this.fetchVariableShapeScript = await this.fs.readLocalFile(file); + this.filesLoaded = true; + } +} diff --git a/src/client/datascience/jupyterDebugService.ts b/src/client/datascience/jupyterDebugService.ts new file mode 100644 index 000000000000..ca774a2c041f --- /dev/null +++ b/src/client/datascience/jupyterDebugService.ts @@ -0,0 +1,415 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { inject, injectable } from 'inversify'; +import * as net from 'net'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { + Breakpoint, + BreakpointsChangeEvent, + DebugAdapterTracker, + DebugAdapterTrackerFactory, + DebugConfiguration, + DebugConfigurationProvider, + DebugConsole, + DebugSession, + DebugSessionCustomEvent, + Disposable, + Event, + EventEmitter, + SourceBreakpoint, + WorkspaceFolder +} from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { traceError, traceInfo } from '../common/logger'; +import { IDisposable, IDisposableRegistry } from '../common/types'; +import { createDeferred } from '../common/utils/async'; +import { noop } from '../common/utils/misc'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { DebugAdapterDescriptorFactory } from '../debugger/extension/adapter/factory'; +import { IProtocolParser } from '../debugger/extension/types'; +import { IJupyterDebugService } from './types'; + +// tslint:disable:no-any + +// For debugging set these environment variables +// PYDEVD_DEBUG=True +// DEBUGPY_LOG_DIR=

+// PYDEVD_DEBUG_FILE= +class JupyterDebugSession implements DebugSession { + private _name = 'JupyterDebugSession'; + constructor( + private _id: string, + private _configuration: DebugConfiguration, + private customRequestHandler: (command: string, args?: any) => Thenable + ) { + noop(); + } + public get id(): string { + return this._id; + } + public get type(): string { + return 'python'; + } + public get name(): string { + return this._name; + } + public get workspaceFolder(): WorkspaceFolder | undefined { + return undefined; + } + public get configuration(): DebugConfiguration { + return this._configuration; + } + public customRequest(command: string, args?: any): Thenable { + return this.customRequestHandler(command, args); + } +} + +//tslint:disable:trailing-comma no-any no-multiline-string +/** + * IJupyterDebugService that talks directly to the debugger. Supports both run by line and + * regular debugging (regular is used in tests). + */ +@injectable() +export class JupyterDebugService implements IJupyterDebugService, IDisposable { + private socket: net.Socket | undefined; + private session: DebugSession | undefined; + private sequence: number = 1; + private breakpointEmitter: EventEmitter = new EventEmitter(); + private debugAdapterTrackerFactories: DebugAdapterTrackerFactory[] = []; + private debugAdapterTrackers: DebugAdapterTracker[] = []; + private sessionChangedEvent: EventEmitter = new EventEmitter(); + private sessionStartedEvent: EventEmitter = new EventEmitter(); + private sessionTerminatedEvent: EventEmitter = new EventEmitter(); + private sessionCustomEvent: EventEmitter = new EventEmitter(); + private breakpointsChangedEvent: EventEmitter = new EventEmitter(); + private _breakpoints: Breakpoint[] = []; + private _stoppedThreadId: number | undefined; + private _topFrameId: number | undefined; + constructor( + @inject(IProtocolParser) private protocolParser: IProtocolParser, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry + ) { + disposableRegistry.push(this); + } + + public dispose(): void { + if (this.socket) { + this.socket.end(); + this.socket = undefined; + } + } + + public get activeDebugSession(): DebugSession | undefined { + return this.session; + } + public get activeDebugConsole(): DebugConsole { + return { + append(_value: string): void { + noop(); + }, + appendLine(_value: string): void { + noop(); + } + }; + } + public get breakpoints(): Breakpoint[] { + return this._breakpoints; + } + public get onDidChangeActiveDebugSession(): Event { + return this.sessionChangedEvent.event; + } + public get onDidStartDebugSession(): Event { + return this.sessionStartedEvent.event; + } + public get onDidReceiveDebugSessionCustomEvent(): Event { + return this.sessionCustomEvent.event; + } + public get onDidTerminateDebugSession(): Event { + return this.sessionTerminatedEvent.event; + } + public get onDidChangeBreakpoints(): Event { + return this.breakpointsChangedEvent.event; + } + public registerDebugConfigurationProvider(_debugType: string, _provider: DebugConfigurationProvider): Disposable { + return { + dispose: () => { + noop(); + } + }; + } + + public registerDebugAdapterDescriptorFactory( + _debugType: string, + _factory: DebugAdapterDescriptorFactory + ): Disposable { + return { + dispose: () => { + noop(); + } + }; + } + public registerDebugAdapterTrackerFactory(_debugType: string, provider: DebugAdapterTrackerFactory): Disposable { + this.debugAdapterTrackerFactories.push(provider); + return { + dispose: () => { + this.debugAdapterTrackerFactories = this.debugAdapterTrackerFactories.filter((f) => f !== provider); + } + }; + } + + public startRunByLine(config: DebugConfiguration): Thenable { + // This is the same as normal debugging. Just a convenient entry point + // in case we need to make it different. + return this.startDebugging(undefined, config); + } + + public startDebugging( + _folder: WorkspaceFolder | undefined, + nameOrConfiguration: string | DebugConfiguration, + _parentSession?: DebugSession | undefined + ): Thenable { + // Should have a port number. We'll assume it's local + const config = nameOrConfiguration as DebugConfiguration; // NOSONAR + if (config.port) { + this.session = new JupyterDebugSession(uuid(), config, this.sendCustomRequest.bind(this)); + this.sessionChangedEvent.fire(this.session); + + // Create our debug adapter trackers at session start + this.debugAdapterTrackers = this.debugAdapterTrackerFactories.map( + (f) => f.createDebugAdapterTracker(this.session!) as DebugAdapterTracker // NOSONAR + ); + + this.socket = net.createConnection(config.port); + this.protocolParser.connect(this.socket); + this.protocolParser.on('event_stopped', this.onBreakpoint.bind(this)); + this.protocolParser.on('event_output', this.onOutput.bind(this)); + this.protocolParser.on('event_terminated', this.sendToTrackers.bind(this)); + this.socket.on('error', this.onError.bind(this)); + this.socket.on('close', this.onClose.bind(this)); + return this.sendStartSequence(config, this.session.id).then(() => true); + } + return Promise.resolve(true); + } + public addBreakpoints(breakpoints: Breakpoint[]): void { + this._breakpoints = this._breakpoints.concat(breakpoints); + } + public removeBreakpoints(_breakpoints: Breakpoint[]): void { + noop(); + } + public get onBreakpointHit(): Event { + return this.breakpointEmitter.event; + } + + public async continue(): Promise { + await this.sendMessage('continue', { threadId: 0 }); + this.sendToTrackers({ type: 'event', event: 'continue' }); + } + + public async step(): Promise { + await this.sendMessage('stepIn', { threadId: this._stoppedThreadId ? this._stoppedThreadId : 1 }); + this.sendToTrackers({ type: 'event', event: 'stepIn' }); + } + + public async getStack(): Promise { + const deferred = createDeferred(); + this.protocolParser.once('response_stackTrace', (args: any) => { + this.sendToTrackers(args); + const response = args as DebugProtocol.StackTraceResponse; + const frames = response.body.stackFrames ? response.body.stackFrames : []; + deferred.resolve(frames); + this._topFrameId = frames[0]?.id; + }); + await this.emitMessage('stackTrace', { + threadId: this._stoppedThreadId ? this._stoppedThreadId : 1, + startFrame: 0, + levels: 1 + }); + return deferred.promise; + } + + public async requestVariables(): Promise { + // Need a stack trace so we have a topmost frame id + await this.getStack(); + const deferred = createDeferred(); + let variablesReference = 0; + this.protocolParser.once('response_scopes', (args: any) => { + this.sendToTrackers(args); + // Get locals variables reference + const response = args as DebugProtocol.ScopesResponse; + if (response) { + variablesReference = response.body.scopes[0].variablesReference; + } + this.emitMessage('variables', { + threadId: this._stoppedThreadId ? this._stoppedThreadId : 1, + variablesReference + }).ignoreErrors(); + }); + this.protocolParser.once('response_variables', (args: any) => { + this.sendToTrackers(args); + deferred.resolve(); + }); + await this.emitMessage('scopes', { + frameId: this._topFrameId ? this._topFrameId : 1 + }); + return deferred.promise; + } + + public stop(): void { + this.onClose(); + } + + private sendToTrackers(args: any) { + this.debugAdapterTrackers.forEach((d) => d.onDidSendMessage!(args)); + } + + private sendCustomRequest(command: string, args?: any): Promise { + return this.sendMessage(command, args); + } + + private async sendStartSequence(config: DebugConfiguration, sessionId: string): Promise { + traceInfo('Sending debugger initialize...'); + await this.sendInitialize(); + if (this._breakpoints.length > 0) { + traceInfo('Sending breakpoints'); + await this.sendBreakpoints(); + } + traceInfo('Sending debugger attach...'); + const attachPromise = this.sendAttach(config, sessionId); + traceInfo('Sending configuration done'); + await this.sendConfigurationDone(); + traceInfo('Session started.'); + return attachPromise.then(() => { + this.sessionStartedEvent.fire(this.session!); + }); + } + + private sendBreakpoints(): Promise { + // Only supporting a single file now + const sbs = this._breakpoints.map((b) => b as SourceBreakpoint); // NOSONAR + const file = sbs[0].location.uri.fsPath; + return this.sendMessage('setBreakpoints', { + source: { + name: path.basename(file), + path: file + }, + lines: sbs.map((sb) => sb.location.range.start.line), + breakpoints: sbs.map((sb) => { + return { line: sb.location.range.start.line }; + }), + sourceModified: true + }); + } + + private sendAttach(config: DebugConfiguration, sessionId: string): Promise { + // Send our attach request + return this.sendMessage('attach', { + debugOptions: ['RedirectOutput', 'FixFilePathCase', 'WindowsClient', 'ShowReturnValue'], + workspaceFolder: EXTENSION_ROOT_DIR, + __sessionId: sessionId, + ...config + }); + } + + private sendConfigurationDone(): Promise { + return this.sendMessage('configurationDone'); + } + + private sendInitialize(): Promise { + // Send our initialize request. (Got this by dumping debugAdapter output during real run. Set logToFile to true to generate) + return this.sendMessage('initialize', { + clientID: 'vscode', + clientName: 'Visual Studio Code', + adapterID: 'python', + pathFormat: 'path', + linesStartAt1: true, + columnsStartAt1: true, + supportsVariableType: true, + supportsVariablePaging: true, + supportsRunInTerminalRequest: true, + locale: 'en-us' + }); + } + + private sendDisconnect(): Promise { + return this.sendMessage('disconnect', {}); + } + + private sendMessage(command: string, args?: any): Promise { + const response = createDeferred(); + const disposable = this.sessionTerminatedEvent.event(() => { + response.resolve({ body: {} }); + }); + const sequenceNumber = this.sequence; + this.protocolParser.on(`response_${command}`, (resp: any) => { + if (resp.request_seq === sequenceNumber) { + this.sendToTrackers(resp); + traceInfo(`Received response from debugger: ${JSON.stringify(args)}`); + disposable.dispose(); + response.resolve(resp.body); + } + }); + this.socket?.on('error', (err) => response.reject(err)); // NOSONAR + this.emitMessage(command, args).catch((exc) => { + traceError(`Exception attempting to emit ${command} to debugger: `, exc); + }); + return response.promise; + } + + private emitMessage(command: string, args?: any): Promise { + return new Promise((resolve, reject) => { + try { + if (this.socket) { + const obj = { + command, + arguments: args, + type: 'request', + seq: this.sequence + }; + this.sequence += 1; + const objString = JSON.stringify(obj); + traceInfo(`Sending request to debugger: ${objString}`); + const message = `Content-Length: ${objString.length}\r\n\r\n${objString}`; + this.socket.write(message, (_a: any) => { + this.sendToTrackers(obj); + resolve(); + }); + } + } catch (e) { + reject(e); + } + }); + } + + private onBreakpoint(args: DebugProtocol.StoppedEvent): void { + // Save the current thread id. We use this in our stack trace request + this._stoppedThreadId = args.body.threadId; + this.sendToTrackers(args); + + // Indicate we stopped at a breakpoint + this.breakpointEmitter.fire(); + } + + private onOutput(args: any): void { + this.sendToTrackers(args); + traceInfo(JSON.stringify(args)); + } + + private onError(args: any): void { + this.sendToTrackers(args); + traceInfo(JSON.stringify(args)); + } + + private onClose(): void { + if (this.socket) { + this.sessionTerminatedEvent.fire(this.activeDebugSession!); + this.session = undefined; + this.sessionChangedEvent.fire(undefined); + this.debugAdapterTrackers.forEach((d) => (d.onExit ? d.onExit(0, undefined) : noop())); + this.debugAdapterTrackers = []; + this.sendDisconnect().ignoreErrors(); + this.socket.destroy(); + this.socket = undefined; + } + } +} diff --git a/src/client/datascience/jupyterUriProviderRegistration.ts b/src/client/datascience/jupyterUriProviderRegistration.ts new file mode 100644 index 000000000000..9e01b206dece --- /dev/null +++ b/src/client/datascience/jupyterUriProviderRegistration.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { inject, injectable } from 'inversify'; +import * as path from 'path'; + +import { IExtensions } from '../common/types'; +import * as localize from '../common/utils/localize'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { JupyterUriProviderWrapper } from './jupyterUriProviderWrapper'; +import { + IDataScienceFileSystem, + IJupyterServerUri, + IJupyterUriProvider, + IJupyterUriProviderRegistration, + JupyterServerUriHandle +} from './types'; + +@injectable() +export class JupyterUriProviderRegistration implements IJupyterUriProviderRegistration { + private loadedOtherExtensionsPromise: Promise | undefined; + private providers = new Map>(); + + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem + ) {} + + public async getProviders(): Promise> { + await this.checkOtherExtensions(); + + // Other extensions should have registered in their activate callback + return Promise.all([...this.providers.values()]); + } + + public registerProvider(provider: IJupyterUriProvider) { + if (!this.providers.has(provider.id)) { + this.providers.set(provider.id, this.createProvider(provider)); + } else { + throw new Error(`IJupyterUriProvider already exists with id ${provider.id}`); + } + } + + public async getJupyterServerUri(id: string, handle: JupyterServerUriHandle): Promise { + await this.checkOtherExtensions(); + + const providerPromise = this.providers.get(id); + if (providerPromise) { + const provider = await providerPromise; + return provider.getServerUri(handle); + } + throw new Error(localize.DataScience.unknownServerUri()); + } + + private checkOtherExtensions(): Promise { + if (!this.loadedOtherExtensionsPromise) { + this.loadedOtherExtensionsPromise = this.loadOtherExtensions(); + } + return this.loadedOtherExtensionsPromise; + } + + private async loadOtherExtensions(): Promise { + const list = this.extensions.all + .filter((e) => e.packageJSON?.contributes?.pythonRemoteServerProvider) + .map((e) => (e.isActive ? Promise.resolve() : e.activate())); + await Promise.all(list); + } + + private async createProvider(provider: IJupyterUriProvider): Promise { + const packageName = await this.determineExtensionFromCallstack(); + return new JupyterUriProviderWrapper(provider, packageName); + } + + private async determineExtensionFromCallstack(): Promise { + const stack = new Error().stack; + if (stack) { + const root = EXTENSION_ROOT_DIR.toLowerCase(); + const frames = stack.split('\n').map((f) => { + const result = /\((.*)\)/.exec(f); + if (result) { + return result[1].toLowerCase(); + } + }); + for (const frame of frames) { + if (frame && !frame.startsWith(root)) { + // This file is from a different extension. Try to find its package.json + let dirName = path.dirname(frame); + let last = frame; + while (dirName && dirName.length < last.length) { + const possiblePackageJson = path.join(dirName, 'package.json'); + if (await this.fs.localFileExists(possiblePackageJson)) { + const text = await this.fs.readLocalFile(possiblePackageJson); + try { + const json = JSON.parse(text); + return `${json.publisher}.${json.name}`; + } catch { + // If parse fails, then not the extension + } + } + last = dirName; + dirName = path.dirname(dirName); + } + } + } + } + return localize.DataScience.unknownPackage(); + } +} diff --git a/src/client/datascience/jupyterUriProviderWrapper.ts b/src/client/datascience/jupyterUriProviderWrapper.ts new file mode 100644 index 000000000000..7c1019bb62ac --- /dev/null +++ b/src/client/datascience/jupyterUriProviderWrapper.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as vscode from 'vscode'; +import * as localize from '../common/utils/localize'; +import { IJupyterServerUri, IJupyterUriProvider, JupyterServerUriHandle } from './types'; + +/** + * This class wraps an IJupyterUriProvider provided by another extension. It allows us to show + * extra data on the other extension's UI. + */ +export class JupyterUriProviderWrapper implements IJupyterUriProvider { + constructor(private readonly provider: IJupyterUriProvider, private packageName: string) {} + public get id() { + return this.provider.id; + } + public getQuickPickEntryItems(): vscode.QuickPickItem[] { + return this.provider.getQuickPickEntryItems().map((q) => { + return { + ...q, + // Add the package name onto the description + description: localize.DataScience.uriProviderDescriptionFormat().format( + q.description || '', + this.packageName + ), + original: q + }; + }); + } + public handleQuickPick( + item: vscode.QuickPickItem, + back: boolean + ): Promise { + // tslint:disable-next-line: no-any + if ((item as any).original) { + // tslint:disable-next-line: no-any + return this.provider.handleQuickPick((item as any).original, back); + } + return this.provider.handleQuickPick(item, back); + } + + public getServerUri(handle: JupyterServerUriHandle): Promise { + return this.provider.getServerUri(handle); + } +} diff --git a/src/client/datascience/kernel-launcher/helpers.ts b/src/client/datascience/kernel-launcher/helpers.ts new file mode 100644 index 000000000000..ddab7b2d2d25 --- /dev/null +++ b/src/client/datascience/kernel-launcher/helpers.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { IJupyterKernelSpec } from '../types'; + +// For a given IJupyterKernelSpec return the interpreter associated with it or error +export async function getKernelInterpreter( + kernelSpec: IJupyterKernelSpec, + interpreterService: IInterpreterService +): Promise { + // First part of argument is always the executable. + const args = [...kernelSpec.argv]; + const pythonPath = kernelSpec.metadata?.interpreter?.path || args[0]; + + // Use that to find the matching interpeter. + const matchingInterpreter = await interpreterService.getInterpreterDetails(pythonPath); + + if (!matchingInterpreter) { + throw new Error(`Failed to find interpreter for kernelspec ${kernelSpec.display_name}`); + } + + return matchingInterpreter; +} diff --git a/src/client/datascience/kernel-launcher/kernelDaemon.ts b/src/client/datascience/kernel-launcher/kernelDaemon.ts new file mode 100644 index 000000000000..e4e5594f620c --- /dev/null +++ b/src/client/datascience/kernel-launcher/kernelDaemon.ts @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ChildProcess } from 'child_process'; +import { Subject } from 'rxjs/Subject'; +import { MessageConnection, NotificationType, RequestType, RequestType0 } from 'vscode-jsonrpc'; +import { IPlatformService } from '../../common/platform/types'; +import { BasePythonDaemon, ExecResponse } from '../../common/process/baseDaemon'; +import { + IPythonExecutionService, + ObservableExecutionResult, + Output, + SpawnOptions, + StdErrError +} from '../../common/process/types'; +import { IPythonKernelDaemon, PythonKernelDiedError } from './types'; + +export class PythonKernelDaemon extends BasePythonDaemon implements IPythonKernelDaemon { + private started?: boolean; + private killed?: boolean; + private preWarmed?: boolean; + private outputHooked?: boolean; + private readonly subject = new Subject>(); + constructor( + pythonExecutionService: IPythonExecutionService, + platformService: IPlatformService, + pythonPath: string, + proc: ChildProcess, + connection: MessageConnection + ) { + super(pythonExecutionService, platformService, pythonPath, proc, connection); + } + public async interrupt() { + const request = new RequestType0('interrupt_kernel'); + await this.sendRequestWithoutArgs(request); + } + public async kill() { + if (this.killed) { + return; + } + this.killed = true; + const request = new RequestType0('kill_kernel'); + await this.sendRequestWithoutArgs(request); + } + public async preWarm() { + if (this.started) { + return; + } + this.preWarmed = true; + this.monitorOutput(); + const request = new RequestType0('prewarm_kernel'); + + await this.sendRequestWithoutArgs(request); + } + + public async start( + moduleName: string, + args: string[], + options: SpawnOptions + ): Promise> { + if (this.killed) { + throw new Error('Restarting a dead daemon'); + } + if (options.throwOnStdErr) { + throw new Error("'throwOnStdErr' not supported in spawnOptions for KernelDaemon.start"); + } + if (options.mergeStdOutErr) { + throw new Error("'mergeStdOutErr' not supported in spawnOptions for KernelDaemon.start"); + } + if (this.started) { + throw new Error('Kernel has already been started in daemon'); + } + this.started = true; + this.monitorOutput(); + + if (this.preWarmed) { + const request = new RequestType<{ args: string[] }, ExecResponse, void, void>('start_prewarmed_kernel'); + await this.sendRequest(request, { args: [moduleName].concat(args) }); + } else { + // No need of the output here, we'll tap into the output coming from daemon `this.outputObservale`. + // This is required because execModule will never end. + // We cannot use `execModuleObservable` as that only works where the daemon is busy seeerving on request and we wait for it to finish. + // In this case we're never going to wait for the module to run to end. Cuz when we run `pytohn -m ipykernel`, it never ends. + // It only ends when the kernel dies, meaning the kernel process is dead. + // What we need is to be able to run the module and keep getting a stream of stdout/stderr. + // & also be able to execute other python code. I.e. we need a daemon. + // For this we run the `ipykernel` code in a separate thread. + // This is why when we run `execModule` in the Kernel daemon, it finishes (comes back) quickly. + // However in reality it is running in the background. + // See `m_exec_module_observable` in `kernel_launcher_daemon.py`. + await this.execModule(moduleName, args, options); + } + + return { + proc: this.proc, + dispose: () => this.dispose(), + out: this.subject + }; + } + private monitorOutput() { + if (this.outputHooked) { + return; + } + this.outputHooked = true; + // Message from daemon when kernel dies. + const KernelDiedNotification = new NotificationType<{ exit_code: string; reason?: string }, void>( + 'kernel_died' + ); + this.connection.onNotification(KernelDiedNotification, (output) => { + this.subject.error( + new PythonKernelDiedError({ exitCode: parseInt(output.exit_code, 10), reason: output.reason }) + ); + }); + + // All output messages from daemon from here on are considered to be coming from the kernel. + // This is because the kernel is a long running process and that will be the only code in the daemon + // sptting stuff into stdout/stderr. + this.outputObservale.subscribe( + (out) => { + if (out.source === 'stderr') { + this.subject.error(new StdErrError(out.out)); + } else { + this.subject.next(out); + } + }, + this.subject.error.bind(this.subject), + this.subject.complete.bind(this.subject) + ); + + // If the daemon dies, then kernel is also dead. + this.closed.catch((error) => this.subject.error(new PythonKernelDiedError({ error }))); + } +} diff --git a/src/client/datascience/kernel-launcher/kernelDaemonPool.ts b/src/client/datascience/kernel-launcher/kernelDaemonPool.ts new file mode 100644 index 000000000000..de1c40b6c9b8 --- /dev/null +++ b/src/client/datascience/kernel-launcher/kernelDaemonPool.ts @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IWorkspaceService } from '../../common/application/types'; +import { traceError } from '../../common/logger'; + +import { IPythonExecutionFactory, IPythonExecutionService } from '../../common/process/types'; +import { IDisposable, Resource } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { KernelLauncherDaemonModule } from '../constants'; +import { IDataScienceFileSystem, IJupyterKernelSpec, IKernelDependencyService } from '../types'; +import { PythonKernelDaemon } from './kernelDaemon'; +import { IPythonKernelDaemon } from './types'; + +type IKernelDaemonInfo = { + key: string; + workspaceResource: Resource; + workspaceFolderIdentifier: string; + interpreterPath: string; + daemon: Promise; +}; + +@injectable() +export class KernelDaemonPool implements IDisposable { + private readonly disposables: IDisposable[] = []; + private daemonPool: IKernelDaemonInfo[] = []; + private initialized?: boolean; + + public get daemons() { + return this.daemonPool.length; + } + + constructor( + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IEnvironmentVariablesProvider) private readonly envVars: IEnvironmentVariablesProvider, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(IInterpreterService) private readonly interrpeterService: IInterpreterService, + @inject(IPythonExecutionFactory) private readonly pythonExecutionFactory: IPythonExecutionFactory, + @inject(IKernelDependencyService) private readonly kernelDependencyService: IKernelDependencyService + ) {} + public async preWarmKernelDaemons() { + if (this.initialized) { + return; + } + this.initialized = true; + this.envVars.onDidEnvironmentVariablesChange(this.onDidEnvironmentVariablesChange.bind(this)); + this.interrpeterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this)); + const promises: Promise[] = []; + if (this.workspaceService.hasWorkspaceFolders) { + promises.push( + ...(this.workspaceService.workspaceFolders || []).map((item) => this.preWarmKernelDaemon(item.uri)) + ); + } else { + promises.push(this.preWarmKernelDaemon(undefined)); + } + await Promise.all(promises); + } + public dispose() { + this.disposables.forEach((item) => item.dispose()); + } + public async get( + resource: Resource, + kernelSpec: IJupyterKernelSpec, + interpreter?: PythonEnvironment + ): Promise { + const pythonPath = interpreter?.path || kernelSpec.argv[0]; + // If we have environment variables in the kernel.json, then its not we support. + // Cuz there's no way to know before hand what kernelspec can be used, hence no way to know what envs are required. + if (kernelSpec.env && Object.keys(kernelSpec.env).length > 0) { + return this.createDaemon(resource, pythonPath); + } + + const key = this.getDaemonKey(resource, pythonPath); + const index = this.daemonPool.findIndex((item) => item.key === key); + try { + if (index >= 0) { + const daemon = this.daemonPool[index].daemon; + this.daemonPool.splice(index, 1); + return daemon; + } + return this.createDaemon(resource, pythonPath); + } finally { + // If we removed a daemon from the pool, rehydrate it. + if (index >= 0) { + this.preWarmKernelDaemon(resource).ignoreErrors(); + } + } + } + + private getDaemonKey(resource: Resource, pythonPath: string): string { + return `${this.workspaceService.getWorkspaceFolderIdentifier(resource)}#${pythonPath}`; + } + private createDaemon(resource: Resource, pythonPath: string) { + const daemon = this.pythonExecutionFactory.createDaemon({ + daemonModule: KernelLauncherDaemonModule, + pythonPath, + daemonClass: PythonKernelDaemon, + dedicated: true, + resource + }); + daemon + .then((d) => { + if ('dispose' in d) { + this.disposables.push(d); + } + }) + .catch(noop); + return daemon; + } + private async onDidEnvironmentVariablesChange(affectedResoruce: Resource) { + const workspaceFolderIdentifier = this.workspaceService.getWorkspaceFolderIdentifier(affectedResoruce); + this.daemonPool = this.daemonPool.filter((item) => { + if (item.workspaceFolderIdentifier === workspaceFolderIdentifier) { + item.daemon + .then((d) => { + if ('dispose' in d) { + d.dispose(); + } + }) + .catch(noop); + return false; + } + return true; + }); + } + private async preWarmKernelDaemon(resource: Resource) { + const interpreter = await this.interrpeterService.getActiveInterpreter(resource); + if (!interpreter || !(await this.kernelDependencyService.areDependenciesInstalled(interpreter))) { + return; + } + const key = this.getDaemonKey(resource, interpreter.path); + // If we have already created one in the interim, then get out. + if (this.daemonPool.some((item) => item.key === key)) { + return; + } + + const workspaceFolderIdentifier = this.workspaceService.getWorkspaceFolderIdentifier(resource); + const daemon = this.createDaemon(resource, interpreter.path); + // Once a daemon is created ensure we pre-warm it (will load ipykernel and start the kernker process waiting to start the actual kernel code). + // I.e. we'll start python process thats the kernel, but will not start the kernel module (`python -m ipykernel`). + daemon + .then((d) => { + // Prewarm if we support prewarming + if ('preWarm' in d) { + d.preWarm().catch(traceError.bind(`Failed to prewarm kernel daemon ${interpreter.path}`)); + } + }) + .catch((e) => { + traceError(`Failed to load deamon: ${e}`); + }); + this.daemonPool.push({ + daemon, + interpreterPath: interpreter.path, + key, + workspaceFolderIdentifier, + workspaceResource: resource + }); + } + private async onDidChangeInterpreter() { + // Get a list of all unique workspaces + const uniqueResourcesWithKernels = new Map(); + this.daemonPool.forEach((item) => { + uniqueResourcesWithKernels.set(item.workspaceFolderIdentifier, item); + }); + + // Key = workspace identifier, and value is interpreter path. + const currentInterpreterInEachWorksapce = new Map(); + // Get interpreters for each workspace. + await Promise.all( + Array.from(uniqueResourcesWithKernels.entries()).map(async (item) => { + const resource = item[1].workspaceResource; + try { + const interpreter = await this.interrpeterService.getActiveInterpreter(resource); + if (!interpreter) { + return; + } + currentInterpreterInEachWorksapce.set(item[1].key, interpreter.path); + } catch (ex) { + traceError(`Failed to get interpreter information for workspace ${resource?.fsPath}`); + } + }) + ); + + // Go through all interpreters for each workspace. + // If we have a daemon with an interpreter thats not the same as the current interpreter for that workspace + // then kill that daemon, as its no longer valid. + this.daemonPool = this.daemonPool.filter((item) => { + const interpreterForWorkspace = currentInterpreterInEachWorksapce.get(item.key); + if (!interpreterForWorkspace || !this.fs.areLocalPathsSame(interpreterForWorkspace, item.interpreterPath)) { + item.daemon + .then((d) => { + if ('dispose' in d) { + d.dispose(); + } + }) + .catch(noop); + return false; + } + + return true; + }); + } +} diff --git a/src/client/datascience/kernel-launcher/kernelDaemonPreWarmer.ts b/src/client/datascience/kernel-launcher/kernelDaemonPreWarmer.ts new file mode 100644 index 000000000000..1e19f400a4ff --- /dev/null +++ b/src/client/datascience/kernel-launcher/kernelDaemonPreWarmer.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IExtensionActivationService } from '../../activation/types'; +import '../../common/extensions'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; +import { swallowExceptions } from '../../common/utils/decorators'; +import { + IInteractiveWindowProvider, + INotebookAndInteractiveWindowUsageTracker, + INotebookEditorProvider, + IRawNotebookSupportedService +} from '../types'; +import { KernelDaemonPool } from './kernelDaemonPool'; + +@injectable() +export class KernelDaemonPreWarmer implements IExtensionActivationService { + constructor( + @inject(INotebookEditorProvider) private readonly notebookEditorProvider: INotebookEditorProvider, + @inject(IInteractiveWindowProvider) private interactiveProvider: IInteractiveWindowProvider, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(INotebookAndInteractiveWindowUsageTracker) + private readonly usageTracker: INotebookAndInteractiveWindowUsageTracker, + @inject(KernelDaemonPool) private readonly kernelDaemonPool: KernelDaemonPool, + @inject(IRawNotebookSupportedService) private readonly rawNotebookSupported: IRawNotebookSupportedService, + @inject(IConfigurationService) private readonly configService: IConfigurationService + ) {} + public async activate(_resource: Resource): Promise { + // Check to see if raw notebooks are supported + // If not, don't bother with prewarming + // Also respect the disable autostart setting to not do any prewarming for the user + if ( + !(await this.rawNotebookSupported.supported()) || + this.configService.getSettings().datascience.disableJupyterAutoStart + ) { + return; + } + + this.disposables.push(this.notebookEditorProvider.onDidOpenNotebookEditor(this.preWarmKernelDaemonPool, this)); + this.disposables.push( + this.interactiveProvider.onDidChangeActiveInteractiveWindow(this.preWarmKernelDaemonPool, this) + ); + if (this.notebookEditorProvider.editors.length > 0 || this.interactiveProvider.windows.length > 0) { + await this.preWarmKernelDaemonPool(); + } + await this.preWarmDaemonPoolIfNecesary(); + } + private async preWarmDaemonPoolIfNecesary() { + if ( + this.shouldPreWarmDaemonPool(this.usageTracker.lastInteractiveWindowOpened) || + this.shouldPreWarmDaemonPool(this.usageTracker.lastNotebookOpened) + ) { + await this.preWarmKernelDaemonPool(); + } + } + @swallowExceptions('PreWarmKernelDaemon') + private async preWarmKernelDaemonPool() { + await this.kernelDaemonPool.preWarmKernelDaemons(); + } + private shouldPreWarmDaemonPool(lastTime?: Date) { + if (!lastTime) { + return false; + } + const currentTime = new Date(); + const diff = currentTime.getTime() - lastTime.getTime(); + const diffInDays = Math.floor(diff / (24 * 3600 * 1000)); + return diffInDays <= 7; + } +} diff --git a/src/client/datascience/kernel-launcher/kernelFinder.ts b/src/client/datascience/kernel-launcher/kernelFinder.ts new file mode 100644 index 000000000000..4958ea42216d --- /dev/null +++ b/src/client/datascience/kernel-launcher/kernelFinder.ts @@ -0,0 +1,427 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import type { nbformat } from '@jupyterlab/coreutils'; +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, CancellationTokenSource } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { wrapCancellationTokens } from '../../common/cancellation'; +import { traceError, traceInfo } from '../../common/logger'; +import { IPlatformService } from '../../common/platform/types'; +import { IPythonExecutionFactory } from '../../common/process/types'; +import { IExtensionContext, IInstaller, InstallerResponse, IPathUtils, Product, Resource } from '../../common/types'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { IInterpreterLocatorService, IInterpreterService, KNOWN_PATH_SERVICE } from '../../interpreter/contracts'; +import { captureTelemetry } from '../../telemetry'; +import { getRealPath } from '../common'; +import { Telemetry } from '../constants'; +import { defaultKernelSpecName } from '../jupyter/kernels/helpers'; +import { JupyterKernelSpec } from '../jupyter/kernels/jupyterKernelSpec'; +import { IDataScienceFileSystem, IJupyterKernelSpec } from '../types'; +import { getKernelInterpreter } from './helpers'; +import { IKernelFinder } from './types'; +// tslint:disable-next-line:no-require-imports no-var-requires +const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); + +const winJupyterPath = path.join('AppData', 'Roaming', 'jupyter', 'kernels'); +const linuxJupyterPath = path.join('.local', 'share', 'jupyter', 'kernels'); +const macJupyterPath = path.join('Library', 'Jupyter', 'kernels'); +const baseKernelPath = path.join('share', 'jupyter', 'kernels'); + +const cacheFile = 'kernelSpecPathCache.json'; + +// This class searches for a kernel that matches the given kernel name. +// First it searches on a global persistent state, then on the installed python interpreters, +// and finally on the default locations that jupyter installs kernels on. +// If a kernel name is not given, it returns a default IJupyterKernelSpec created from the current interpreter. +// Before returning the IJupyterKernelSpec it makes sure that ipykernel is installed into the kernel spec interpreter +@injectable() +export class KernelFinder implements IKernelFinder { + private cache?: string[]; + private cacheDirty = false; + + // Store our results when listing all possible kernelspecs for a resource + private workspaceToKernels = new Map>(); + + // Store any json file that we have loaded from disk before + private pathToKernelSpec = new Map>(); + + constructor( + @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(IInterpreterLocatorService) + @named(KNOWN_PATH_SERVICE) + private readonly interpreterLocator: IInterpreterLocatorService, + @inject(IPlatformService) private platformService: IPlatformService, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(IPathUtils) private readonly pathUtils: IPathUtils, + @inject(IInstaller) private installer: IInstaller, + @inject(IExtensionContext) private readonly context: IExtensionContext, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IPythonExecutionFactory) private readonly exeFactory: IPythonExecutionFactory, + @inject(IEnvironmentVariablesProvider) private readonly envVarsProvider: IEnvironmentVariablesProvider + ) {} + @captureTelemetry(Telemetry.KernelFinderPerf) + public async findKernelSpec( + resource: Resource, + kernelSpecMetadata?: nbformat.IKernelspecMetadata, + cancelToken?: CancellationToken, + ignoreDependencyCheck?: boolean + ): Promise { + await this.readCache(); + let foundKernel: IJupyterKernelSpec | undefined; + + const kernelName = kernelSpecMetadata?.name; + + if (kernelSpecMetadata && kernelName) { + // For a non default kernelspec search for it + if (!kernelName.includes(defaultKernelSpecName)) { + let kernelSpec = await this.searchCache(kernelName); + + if (kernelSpec) { + return kernelSpec; + } + + // Check in active interpreter first + kernelSpec = await this.getKernelSpecFromActiveInterpreter(kernelName, resource); + + if (kernelSpec) { + this.writeCache().ignoreErrors(); + return kernelSpec; + } + + const diskSearch = this.findDiskPath(kernelName); + const interpreterSearch = this.getInterpreterPaths(resource).then((interpreterPaths) => { + return this.findInterpreterPath(interpreterPaths, kernelName); + }); + + let result = await Promise.race([diskSearch, interpreterSearch]); + if (!result) { + const both = await Promise.all([diskSearch, interpreterSearch]); + result = both[0] ? both[0] : both[1]; + } + + foundKernel = result; + } + } + + this.writeCache().ignoreErrors(); + + // Verify that ipykernel is installed into the given kernelspec interpreter + return ignoreDependencyCheck || !foundKernel ? foundKernel : this.verifyIpyKernel(foundKernel, cancelToken); + } + + // Search all our local file system locations for installed kernel specs and return them + public async listKernelSpecs(resource: Resource): Promise { + if (!resource) { + // We need a resource to search for related kernel specs + return []; + } + + // Get an id for the workspace folder, if we don't have one, use the fsPath of the resource + const workspaceFolderId = this.workspaceService.getWorkspaceFolderIdentifier(resource, resource.fsPath); + + // If we have not already searched for this resource, then generate the search + if (!this.workspaceToKernels.has(workspaceFolderId)) { + this.workspaceToKernels.set(workspaceFolderId, this.findResourceKernelSpecs(resource)); + } + + this.writeCache().ignoreErrors(); + + // ! as the has and set above verify that we have a return here + return this.workspaceToKernels.get(workspaceFolderId)!; + } + + private async findResourceKernelSpecs(resource: Resource): Promise { + const results: IJupyterKernelSpec[] = []; + + // Find all the possible places to look for this resource + const paths = await this.findAllResourcePossibleKernelPaths(resource); + + const searchResults = await this.kernelGlobSearch(paths); + + await Promise.all( + searchResults.map(async (resultPath) => { + // Add these into our path cache to speed up later finds + this.updateCache(resultPath); + const kernelspec = await this.getKernelSpec(resultPath); + + if (kernelspec) { + results.push(kernelspec); + } + }) + ); + + return results; + } + + // Load the IJupyterKernelSpec for a given spec path, check the ones that we have already loaded first + private async getKernelSpec(specPath: string): Promise { + // If we have not already loaded this kernel spec, then load it + if (!this.pathToKernelSpec.has(specPath)) { + this.pathToKernelSpec.set(specPath, this.loadKernelSpec(specPath)); + } + + // ! as the has and set above verify that we have a return here + return this.pathToKernelSpec.get(specPath)!.then((value) => { + if (value) { + return value; + } + + // If we failed to get a kernelspec pull path from our cache and loaded list + this.pathToKernelSpec.delete(specPath); + this.cache = this.cache?.filter((itempath) => itempath !== specPath); + return undefined; + }); + } + + // Load kernelspec json from disk + private async loadKernelSpec(specPath: string): Promise { + let kernelJson; + try { + kernelJson = JSON.parse(await this.fs.readLocalFile(specPath)); + } catch { + traceError(`Failed to parse kernelspec ${specPath}`); + return undefined; + } + const kernelSpec: IJupyterKernelSpec = new JupyterKernelSpec(kernelJson, specPath); + + // Some registered kernel specs do not have a name, in this case use the last part of the path + kernelSpec.name = kernelJson?.name || path.basename(path.dirname(specPath)); + return kernelSpec; + } + + // For the given resource, find atll the file paths for kernel specs that wewant to associate with this + private async findAllResourcePossibleKernelPaths( + resource: Resource, + _cancelToken?: CancellationToken + ): Promise { + const [activePath, interpreterPaths, diskPaths] = await Promise.all([ + this.getActiveInterpreterPath(resource), + this.getInterpreterPaths(resource), + this.getDiskPaths() + ]); + + return [...activePath, ...interpreterPaths, ...diskPaths]; + } + + private async getActiveInterpreterPath(resource: Resource): Promise { + const activeInterpreter = await this.interpreterService.getActiveInterpreter(resource); + + if (activeInterpreter) { + return [path.join(activeInterpreter.sysPrefix, 'share', 'jupyter', 'kernels')]; + } + + return []; + } + + private async getInterpreterPaths(resource: Resource): Promise { + const interpreters = await this.interpreterLocator.getInterpreters(resource, { ignoreCache: false }); + const interpreterPrefixPaths = interpreters.map((interpreter) => interpreter.sysPrefix); + // We can get many duplicates here, so de-dupe the list + const uniqueInterpreterPrefixPaths = [...new Set(interpreterPrefixPaths)]; + return uniqueInterpreterPrefixPaths.map((prefixPath) => path.join(prefixPath, baseKernelPath)); + } + + // Find any paths associated with the JUPYTER_PATH env var. Can be a list of dirs. + // We need to look at the 'kernels' sub-directory and these paths are supposed to come first in the searching + // https://jupyter.readthedocs.io/en/latest/projects/jupyter-directories.html#envvar-JUPYTER_PATH + private async getJupyterPathPaths(): Promise { + const paths: string[] = []; + const vars = await this.envVarsProvider.getEnvironmentVariables(); + const jupyterPathVars = vars.JUPYTER_PATH + ? vars.JUPYTER_PATH.split(path.delimiter).map((jupyterPath) => { + return path.join(jupyterPath, 'kernels'); + }) + : []; + + if (jupyterPathVars.length > 0) { + if (this.platformService.isWindows) { + const activeInterpreter = await this.interpreterService.getActiveInterpreter(); + if (activeInterpreter) { + jupyterPathVars.forEach(async (jupyterPath) => { + const jupyterWinPath = await getRealPath( + this.fs, + this.exeFactory, + activeInterpreter.path, + jupyterPath + ); + + if (jupyterWinPath) { + paths.push(jupyterWinPath); + } + }); + } else { + paths.push(...jupyterPathVars); + } + } else { + // Unix based + paths.push(...jupyterPathVars); + } + } + + return paths; + } + + private async getDiskPaths(): Promise { + // Paths specified in JUPYTER_PATH are supposed to come first in searching + const paths: string[] = await this.getJupyterPathPaths(); + + if (this.platformService.isWindows) { + const activeInterpreter = await this.interpreterService.getActiveInterpreter(); + if (activeInterpreter) { + const winPath = await getRealPath( + this.fs, + this.exeFactory, + activeInterpreter.path, + path.join(this.pathUtils.home, winJupyterPath) + ); + if (winPath) { + paths.push(winPath); + } + } else { + paths.push(path.join(this.pathUtils.home, winJupyterPath)); + } + + if (process.env.ALLUSERSPROFILE) { + paths.push(path.join(process.env.ALLUSERSPROFILE, 'jupyter', 'kernels')); + } + } else { + // Unix based + const secondPart = this.platformService.isMac ? macJupyterPath : linuxJupyterPath; + + paths.push( + path.join('usr', 'share', 'jupyter', 'kernels'), + path.join('usr', 'local', 'share', 'jupyter', 'kernels'), + path.join(this.pathUtils.home, secondPart) + ); + } + + return paths; + } + + // Given a set of paths, search for kernel.json files and return back the full paths of all of them that we find + private async kernelGlobSearch(paths: string[]): Promise { + const promises = paths.map((kernelPath) => this.fs.searchLocal(`**/kernel.json`, kernelPath, true)); + const searchResults = await Promise.all(promises); + + // Append back on the start of each path so we have the full path in the results + const fullPathResults = searchResults + .filter((f) => f) + .map((result, index) => { + return result.map((partialSpecPath) => { + return path.join(paths[index], partialSpecPath); + }); + }); + + return flatten(fullPathResults); + } + + // For the given kernelspec return back the kernelspec with ipykernel installed into it or error + private async verifyIpyKernel( + kernelSpec: IJupyterKernelSpec, + cancelToken?: CancellationToken + ): Promise { + const interpreter = await getKernelInterpreter(kernelSpec, this.interpreterService); + + if (await this.installer.isInstalled(Product.ipykernel, interpreter)) { + return kernelSpec; + } else { + const token = new CancellationTokenSource(); + const response = await this.installer.promptToInstall( + Product.ipykernel, + interpreter, + wrapCancellationTokens(cancelToken, token.token) + ); + if (response === InstallerResponse.Installed) { + return kernelSpec; + } + } + + throw new Error(`IPyKernel not installed into interpreter ${interpreter.displayName}`); + } + + private async getKernelSpecFromActiveInterpreter( + kernelName: string, + resource: Resource + ): Promise { + const activePath = await this.getActiveInterpreterPath(resource); + return this.getKernelSpecFromDisk(activePath, kernelName); + } + + private async findInterpreterPath( + interpreterPaths: string[], + kernelName: string + ): Promise { + const promises = interpreterPaths.map((intPath) => this.getKernelSpecFromDisk([intPath], kernelName)); + + const specs = await Promise.all(promises); + return specs.find((sp) => sp !== undefined); + } + + // Jupyter looks for kernels in these paths: + // https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs + private async findDiskPath(kernelName: string): Promise { + const paths = await this.getDiskPaths(); + + return this.getKernelSpecFromDisk(paths, kernelName); + } + + private async getKernelSpecFromDisk(paths: string[], kernelName: string): Promise { + const searchResults = await this.kernelGlobSearch(paths); + searchResults.forEach((specPath) => { + this.updateCache(specPath); + }); + + return this.searchCache(kernelName); + } + + private async readCache(): Promise { + try { + if (Array.isArray(this.cache) && this.cache.length > 0) { + return; + } + this.cache = JSON.parse( + await this.fs.readLocalFile(path.join(this.context.globalStoragePath, cacheFile)) + ) as string[]; + } catch { + traceInfo('No kernelSpec cache found.'); + } + } + + private updateCache(newPath: string) { + this.cache = Array.isArray(this.cache) ? this.cache : []; + if (!this.cache.includes(newPath)) { + this.cache.push(newPath); + this.cacheDirty = true; + } + } + + private async writeCache() { + if (this.cacheDirty && Array.isArray(this.cache)) { + await this.fs.writeLocalFile( + path.join(this.context.globalStoragePath, cacheFile), + JSON.stringify(this.cache) + ); + this.cacheDirty = false; + } + } + + private async searchCache(kernelName: string): Promise { + const kernelJsonFile = this.cache?.find((kernelPath) => { + try { + return path.basename(path.dirname(kernelPath)) === kernelName; + } catch (e) { + traceInfo('KernelSpec path in cache is not a string.', e); + return false; + } + }); + + if (kernelJsonFile) { + return this.getKernelSpec(kernelJsonFile); + } + + return undefined; + } +} diff --git a/src/client/datascience/kernel-launcher/kernelLauncher.ts b/src/client/datascience/kernel-launcher/kernelLauncher.ts new file mode 100644 index 000000000000..50894ab93a10 --- /dev/null +++ b/src/client/datascience/kernel-launcher/kernelLauncher.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as portfinder from 'portfinder'; +import { promisify } from 'util'; +import * as uuid from 'uuid/v4'; +import { IProcessServiceFactory } from '../../common/process/types'; +import { Resource } from '../../common/types'; +import { captureTelemetry } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { KernelSpecConnectionMetadata, PythonKernelConnectionMetadata } from '../jupyter/kernels/types'; +import { IDataScienceFileSystem } from '../types'; +import { KernelDaemonPool } from './kernelDaemonPool'; +import { KernelProcess } from './kernelProcess'; +import { IKernelConnection, IKernelLauncher, IKernelProcess } from './types'; + +const PortToStartFrom = 9_000; + +// Launches and returns a kernel process given a resource or python interpreter. +// If the given interpreter is undefined, it will try to use the selected interpreter. +// If the selected interpreter doesn't have a kernel, it will find a kernel on disk and use that. +@injectable() +export class KernelLauncher implements IKernelLauncher { + private static nextFreePortToTryAndUse = PortToStartFrom; + constructor( + @inject(IProcessServiceFactory) private processExecutionFactory: IProcessServiceFactory, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(KernelDaemonPool) private readonly daemonPool: KernelDaemonPool + ) {} + + @captureTelemetry(Telemetry.KernelLauncherPerf) + public async launch( + kernelConnectionMetadata: KernelSpecConnectionMetadata | PythonKernelConnectionMetadata, + resource: Resource, + workingDirectory: string + ): Promise { + const connection = await this.getKernelConnection(); + const kernelProcess = new KernelProcess( + this.processExecutionFactory, + this.daemonPool, + connection, + kernelConnectionMetadata, + this.fs, + resource + ); + await kernelProcess.launch(workingDirectory); + return kernelProcess; + } + + private async getKernelConnection(): Promise { + const getPorts = promisify(portfinder.getPorts); + // Ports may have been freed, hence start from begining. + const port = + KernelLauncher.nextFreePortToTryAndUse > PortToStartFrom + 1_000 + ? PortToStartFrom + : KernelLauncher.nextFreePortToTryAndUse; + const ports = await getPorts(5, { host: '127.0.0.1', port }); + // We launch restart kernels in the background, its possible other session hasn't started. + // Ensure we do not use same ports. + KernelLauncher.nextFreePortToTryAndUse = Math.max(...ports) + 1; + + return { + version: 1, + key: uuid(), + signature_scheme: 'hmac-sha256', + transport: 'tcp', + ip: '127.0.0.1', + hb_port: ports[0], + control_port: ports[1], + shell_port: ports[2], + stdin_port: ports[3], + iopub_port: ports[4] + }; + } +} diff --git a/src/client/datascience/kernel-launcher/kernelLauncherDaemon.ts b/src/client/datascience/kernel-launcher/kernelLauncherDaemon.ts new file mode 100644 index 000000000000..e79a699aaf66 --- /dev/null +++ b/src/client/datascience/kernel-launcher/kernelLauncherDaemon.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ChildProcess } from 'child_process'; +import * as fs from 'fs-extra'; +import { inject, injectable } from 'inversify'; +import { IDisposable } from 'monaco-editor'; +import { ObservableExecutionResult } from '../../common/process/types'; +import { Resource } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { IJupyterKernelSpec } from '../types'; +import { KernelDaemonPool } from './kernelDaemonPool'; +import { IPythonKernelDaemon } from './types'; + +/** + * Launches a Python kernel in a daemon. + * We need a daemon for the sole purposes of being able to interrupt kernels in Windows. + * (Else we don't need a kernel). + */ +@injectable() +export class PythonKernelLauncherDaemon implements IDisposable { + private readonly processesToDispose: ChildProcess[] = []; + constructor(@inject(KernelDaemonPool) private readonly daemonPool: KernelDaemonPool) {} + public async launch( + resource: Resource, + workingDirectory: string, + kernelSpec: IJupyterKernelSpec, + interpreter?: PythonEnvironment + ): Promise<{ observableOutput: ObservableExecutionResult; daemon: IPythonKernelDaemon | undefined }> { + const [daemon, wdExists] = await Promise.all([ + this.daemonPool.get(resource, kernelSpec, interpreter), + fs.pathExists(workingDirectory) + ]); + + // Check to see if we have the type of kernelspec that we expect + const args = kernelSpec.argv.slice(); + const modulePrefixIndex = args.findIndex((item) => item === '-m'); + if (modulePrefixIndex === -1) { + throw new Error( + `Unsupported KernelSpec file. args must be [, '-m', , arg1, arg2, ..]. Provied ${args.join( + ' ' + )}` + ); + } + const moduleName = args[modulePrefixIndex + 1]; + const moduleArgs = args.slice(modulePrefixIndex + 2); + const env = kernelSpec.env && Object.keys(kernelSpec.env).length > 0 ? kernelSpec.env : undefined; + + // The daemon pool can return back a non-IPythonKernelDaemon if daemon service is not supported or for Python 2. + // Use a check for the daemon.start function here before we call it. + if (!('start' in daemon)) { + // If we don't have a KernelDaemon here then we have an execution service and should use that to launch + const observableOutput = daemon.execModuleObservable(moduleName, moduleArgs, { + env, + cwd: wdExists ? workingDirectory : process.cwd() + }); + + return { observableOutput, daemon: undefined }; + } else { + // In the case that we do have a kernel deamon, just return it + const observableOutput = await daemon.start(moduleName, moduleArgs, { env, cwd: workingDirectory }); + if (observableOutput.proc) { + this.processesToDispose.push(observableOutput.proc); + } + return { observableOutput, daemon }; + } + } + public dispose() { + while (this.processesToDispose.length) { + try { + this.processesToDispose.shift()!.kill(); + } catch { + noop(); + } + } + } +} diff --git a/src/client/datascience/kernel-launcher/kernelProcess.ts b/src/client/datascience/kernel-launcher/kernelProcess.ts new file mode 100644 index 000000000000..dfeda3d60198 --- /dev/null +++ b/src/client/datascience/kernel-launcher/kernelProcess.ts @@ -0,0 +1,297 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { ChildProcess } from 'child_process'; +import * as tcpPortUsed from 'tcp-port-used'; +import * as tmp from 'tmp'; +import { Event, EventEmitter } from 'vscode'; +import { traceError, traceInfo, traceWarning } from '../../common/logger'; +import { IProcessServiceFactory, ObservableExecutionResult } from '../../common/process/types'; +import { Resource } from '../../common/types'; +import { noop, swallowExceptions } from '../../common/utils/misc'; +import { captureTelemetry } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { + createDefaultKernelSpec, + findIndexOfConnectionFile, + isPythonKernelConnection +} from '../jupyter/kernels/helpers'; +import { KernelSpecConnectionMetadata, PythonKernelConnectionMetadata } from '../jupyter/kernels/types'; +import { IDataScienceFileSystem, IJupyterKernelSpec } from '../types'; +import { KernelDaemonPool } from './kernelDaemonPool'; +import { PythonKernelLauncherDaemon } from './kernelLauncherDaemon'; +import { IKernelConnection, IKernelProcess, IPythonKernelDaemon, PythonKernelDiedError } from './types'; + +// Launches and disposes a kernel process given a kernelspec and a resource or python interpreter. +// Exposes connection information and the process itself. +export class KernelProcess implements IKernelProcess { + public get exited(): Event<{ exitCode?: number; reason?: string }> { + return this.exitEvent.event; + } + public get kernelConnectionMetadata(): Readonly { + return this._kernelConnectionMetadata; + } + public get connection(): Readonly { + return this._connection; + } + private get isPythonKernel(): boolean { + return isPythonKernelConnection(this.kernelConnectionMetadata); + } + private _process?: ChildProcess; + private exitEvent = new EventEmitter<{ exitCode?: number; reason?: string }>(); + private pythonKernelLauncher?: PythonKernelLauncherDaemon; + private launchedOnce?: boolean; + private disposed?: boolean; + private kernelDaemon?: IPythonKernelDaemon; + private connectionFile?: string; + private _launchKernelSpec?: IJupyterKernelSpec; + private readonly _kernelConnectionMetadata: Readonly; + constructor( + private readonly processExecutionFactory: IProcessServiceFactory, + private readonly daemonPool: KernelDaemonPool, + private readonly _connection: IKernelConnection, + kernelConnectionMetadata: KernelSpecConnectionMetadata | PythonKernelConnectionMetadata, + private readonly fs: IDataScienceFileSystem, + private readonly resource: Resource + ) { + this._kernelConnectionMetadata = kernelConnectionMetadata; + } + public async interrupt(): Promise { + if (this.kernelDaemon) { + await this.kernelDaemon?.interrupt(); + } + } + + @captureTelemetry(Telemetry.RawKernelProcessLaunch, undefined, true) + public async launch(workingDirectory: string): Promise { + if (this.launchedOnce) { + throw new Error('Kernel has already been launched.'); + } + this.launchedOnce = true; + + // Update our connection arguments in the kernel spec + await this.updateConnectionArgs(); + + const exeObs = await this.launchAsObservable(workingDirectory); + + let stdout = ''; + let stderr = ''; + exeObs.out.subscribe( + (output) => { + if (output.source === 'stderr') { + // Capture stderr, incase kernel doesn't start. + stderr += output.out; + traceWarning(`StdErr from Kernel Process ${output.out}`); + } else { + stdout += output.out; + traceInfo(`Kernel Output: ${stdout}`); + } + }, + (error) => { + if (this.disposed) { + traceInfo('Kernel died', error, stderr); + return; + } + traceError('Kernel died', error, stderr); + if (error instanceof PythonKernelDiedError) { + if (this.disposed) { + traceInfo('KernelProcess Exit', `Exit - ${error.exitCode}, ${error.reason}`, error); + } else { + traceError('KernelProcess Exit', `Exit - ${error.exitCode}, ${error.reason}`, error); + } + if (this.disposed) { + return; + } + this.exitEvent.fire({ exitCode: error.exitCode, reason: error.reason || error.message }); + } + } + ); + + // Don't return until our heartbeat channel is open for connections + return this.waitForHeartbeat(); + } + + public async dispose(): Promise { + if (this.disposed) { + return; + } + this.disposed = true; + if (this.kernelDaemon) { + await this.kernelDaemon.kill().catch(noop); + swallowExceptions(() => this.kernelDaemon?.dispose()); + } + swallowExceptions(() => { + this._process?.kill(); // NOSONAR + this.exitEvent.fire({}); + }); + swallowExceptions(() => this.pythonKernelLauncher?.dispose()); + swallowExceptions(async () => (this.connectionFile ? this.fs.deleteLocalFile(this.connectionFile) : noop())); + } + + // Make sure that the heartbeat channel is open for connections + private async waitForHeartbeat() { + try { + // Wait until the port is open for connection + // First parameter is wait between retries, second parameter is total wait before error + await tcpPortUsed.waitUntilUsed(this.connection.hb_port, 200, 30_000); + } catch (error) { + // Make sure to dispose if we never get a heartbeat + this.dispose().ignoreErrors(); + traceError('Timed out waiting to get a heartbeat from kernel process.'); + throw new Error('Timed out waiting to get a heartbeat from kernel process.'); + } + } + + private get launchKernelSpec(): IJupyterKernelSpec { + if (this._launchKernelSpec) { + return this._launchKernelSpec; + } + + let kernelSpec = this._kernelConnectionMetadata.kernelSpec; + // If there is no kernelspec & when launching a Python process, generate a dummy `kernelSpec` + if (!kernelSpec && this._kernelConnectionMetadata.kind === 'startUsingPythonInterpreter') { + kernelSpec = createDefaultKernelSpec( + this._kernelConnectionMetadata.interpreter.displayName || + this._kernelConnectionMetadata.interpreter.path + ); + } + // We always expect a kernel spec. + if (!kernelSpec) { + throw new Error('KernelSpec cannot be empty in KernelProcess.ts'); + } + if (!Array.isArray(kernelSpec.argv)) { + traceError('KernelSpec.argv in KernelPrcess is undefined'); + // tslint:disable-next-line: no-any + this._launchKernelSpec = undefined; + } else { + // Copy our kernelspec and assign a new argv array + this._launchKernelSpec = { ...kernelSpec, argv: [...kernelSpec.argv] }; + } + return this._launchKernelSpec!; + } + + // Instead of having to use a connection file update our local copy of the kernelspec to launch + // directly with command line arguments + private async updateConnectionArgs() { + // First check to see if we have a kernelspec that expects a connection file, + // Error if we don't have one. We expect '-f', '{connectionfile}' in our launch args + const indexOfConnectionFile = findIndexOfConnectionFile(this.launchKernelSpec); + + // Technically if we don't have a kernelspec then index should already be -1, but the check here lets us avoid ? on the type + if (indexOfConnectionFile === -1) { + throw new Error( + `Connection file not found in kernelspec json args, ${this.launchKernelSpec.argv.join(' ')}` + ); + } + + if ( + this.isPythonKernel && + indexOfConnectionFile === 0 && + this.launchKernelSpec.argv[indexOfConnectionFile - 1] !== '-f' + ) { + throw new Error( + `Connection file not found in kernelspec json args, ${this.launchKernelSpec.argv.join(' ')}` + ); + } + + // Python kernels are special. Handle the extra arguments. + if (this.isPythonKernel) { + // Slice out -f and the connection file from the args + this.launchKernelSpec.argv.splice(indexOfConnectionFile - 1, 2); + + // Add in our connection command line args + this.launchKernelSpec.argv.push(...this.addPythonConnectionArgs()); + } else { + // For other kernels, just write to the connection file. + // Note: We have to dispose the temp file and recreate it because otherwise the file + // system will hold onto the file with an open handle. THis doesn't work so well when + // a different process tries to open it. + const tempFile = await this.fs.createTemporaryLocalFile('.json'); + this.connectionFile = tempFile.filePath; + await tempFile.dispose(); + await this.fs.writeLocalFile(this.connectionFile, JSON.stringify(this._connection)); + + // Then replace the connection file argument with this file + this.launchKernelSpec.argv[indexOfConnectionFile] = this.connectionFile; + } + } + + // Add the command line arguments + private addPythonConnectionArgs(): string[] { + const newConnectionArgs: string[] = []; + + newConnectionArgs.push(`--ip=${this._connection.ip}`); + newConnectionArgs.push(`--stdin=${this._connection.stdin_port}`); + newConnectionArgs.push(`--control=${this._connection.control_port}`); + newConnectionArgs.push(`--hb=${this._connection.hb_port}`); + newConnectionArgs.push(`--Session.signature_scheme="${this._connection.signature_scheme}"`); + newConnectionArgs.push(`--Session.key=b"${this._connection.key}"`); // Note we need the 'b here at the start for a byte string + newConnectionArgs.push(`--shell=${this._connection.shell_port}`); + newConnectionArgs.push(`--transport="${this._connection.transport}"`); + newConnectionArgs.push(`--iopub=${this._connection.iopub_port}`); + + // Turn this on if you get desparate. It can cause crashes though as the + // logging code isn't that robust. + // if (isTestExecution()) { + // // Extra logging for tests + // newConnectionArgs.push(`--log-level=10`); + // } + + // We still put in the tmp name to make sure the kernel picks a valid connection file name. It won't read it as + // we passed in the arguments, but it will use it as the file name so it doesn't clash with other kernels. + newConnectionArgs.push(`--f=${tmp.tmpNameSync({ postfix: '.json' })}`); + + return newConnectionArgs; + } + + private async launchAsObservable(workingDirectory: string) { + let exeObs: ObservableExecutionResult | undefined; + if (this.isPythonKernel) { + this.pythonKernelLauncher = new PythonKernelLauncherDaemon(this.daemonPool); + const kernelDaemonLaunch = await this.pythonKernelLauncher.launch( + this.resource, + workingDirectory, + this.launchKernelSpec, + this._kernelConnectionMetadata.interpreter + ); + + this.kernelDaemon = kernelDaemonLaunch.daemon; + exeObs = kernelDaemonLaunch.observableOutput; + } + + // If we are not python just use the ProcessExecutionFactory + if (!exeObs) { + // First part of argument is always the executable. + const executable = this.launchKernelSpec.argv[0]; + const executionService = await this.processExecutionFactory.create(this.resource); + exeObs = executionService.execObservable(executable, this.launchKernelSpec.argv.slice(1), { + env: this._kernelConnectionMetadata.kernelSpec?.env, + cwd: workingDirectory + }); + } + + if (exeObs && exeObs.proc) { + exeObs.proc.on('exit', (exitCode) => { + traceInfo('KernelProcess Exit', `Exit - ${exitCode}`); + if (this.disposed) { + return; + } + this.exitEvent.fire({ exitCode: exitCode || undefined }); + }); + // tslint:disable-next-line: no-any + exeObs.proc.stdout.on('data', (data: any) => { + traceInfo(`KernelProcess output: ${data}`); + }); + // tslint:disable-next-line: no-any + exeObs.proc.stderr.on('data', (data: any) => { + traceInfo(`KernelProcess error: ${data}`); + }); + } else { + throw new Error('KernelProcess failed to launch'); + } + + this._process = exeObs.proc; + return exeObs; + } +} diff --git a/src/client/datascience/kernel-launcher/types.ts b/src/client/datascience/kernel-launcher/types.ts new file mode 100644 index 000000000000..625d3226af47 --- /dev/null +++ b/src/client/datascience/kernel-launcher/types.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import type { nbformat } from '@jupyterlab/coreutils'; +import { SpawnOptions } from 'child_process'; +import { CancellationToken, Event } from 'vscode'; +import { InterpreterUri } from '../../common/installer/types'; +import { ObservableExecutionResult } from '../../common/process/types'; +import { IAsyncDisposable, IDisposable, Resource } from '../../common/types'; +import { KernelSpecConnectionMetadata, PythonKernelConnectionMetadata } from '../jupyter/kernels/types'; +import { IJupyterKernelSpec } from '../types'; + +export const IKernelLauncher = Symbol('IKernelLauncher'); +export interface IKernelLauncher { + launch( + kernelConnectionMetadata: KernelSpecConnectionMetadata | PythonKernelConnectionMetadata, + resource: Resource, + workingDirectory: string + ): Promise; +} + +export interface IKernelConnection { + version: number; + iopub_port: number; + shell_port: number; + stdin_port: number; + control_port: number; + signature_scheme: 'hmac-sha256'; + hb_port: number; + ip: string; + key: string; + transport: 'tcp' | 'ipc'; +} + +export interface IKernelProcess extends IAsyncDisposable { + readonly connection: Readonly; + readonly kernelConnectionMetadata: Readonly; + /** + * This event is triggered if the process is exited + */ + readonly exited: Event<{ exitCode?: number; reason?: string }>; + interrupt(): Promise; +} + +export const IKernelFinder = Symbol('IKernelFinder'); +export interface IKernelFinder { + findKernelSpec( + interpreterUri: InterpreterUri, + kernelSpecMetadata?: nbformat.IKernelspecMetadata, + cancelToken?: CancellationToken, + ignoreDependencyCheck?: boolean + ): Promise; + listKernelSpecs(resource: Resource): Promise; +} + +/** + * The daemon responsible for the Python Kernel. + */ +export interface IPythonKernelDaemon extends IDisposable { + interrupt(): Promise; + kill(): Promise; + preWarm(): Promise; + start(moduleName: string, args: string[], options: SpawnOptions): Promise>; +} + +export class PythonKernelDiedError extends Error { + public readonly exitCode: number; + public readonly reason?: string; + constructor(options: { exitCode: number; reason?: string } | { error: Error }) { + const message = + 'exitCode' in options + ? `Kernel died with exit code ${options.exitCode}. ${options.reason}` + : `Kernel died ${options.error.message}`; + super(message); + if ('exitCode' in options) { + this.exitCode = options.exitCode; + this.reason = options.reason; + } else { + this.exitCode = -1; + this.reason = options.error.message; + this.stack = options.error.stack; + this.name = options.error.name; + } + } +} diff --git a/src/client/datascience/kernelSocketWrapper.ts b/src/client/datascience/kernelSocketWrapper.ts new file mode 100644 index 000000000000..ca87b32723f4 --- /dev/null +++ b/src/client/datascience/kernelSocketWrapper.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as WebSocketWS from 'ws'; +import { ClassType } from '../ioc/types'; +import { IKernelSocket } from './types'; + +// tslint:disable: no-any prefer-method-signature +export type IWebSocketLike = { + onopen: (event: { target: any }) => void; + onerror: (event: { error: any; message: string; type: string; target: any }) => void; + onclose: (event: { wasClean: boolean; code: number; reason: string; target: any }) => void; + onmessage: (event: { data: WebSocketWS.Data; type: string; target: any }) => void; + emit(event: string | symbol, ...args: any[]): boolean; + send(data: any, a2: any): void; + close(): void; +}; + +/** + * This is called a mixin class in TypeScript. + * Allows us to have different base classes but inherit behavior (workaround for not allowing multiple inheritance). + * Essentially it sticks a temp class in between the base class and the class you're writing. + * Something like this: + * + * class Base { + * doStuff() { + * + * } + * } + * + * function Mixin = (SuperClass) { + * return class extends SuperClass { + * doExtraStuff() { + * super.doStuff(); + * } + * } + * } + * + * function SubClass extends Mixin(Base) { + * doBar() : { + * super.doExtraStuff(); + * } + * } + * + */ + +export function KernelSocketWrapper>(SuperClass: T) { + return class BaseKernelSocket extends SuperClass implements IKernelSocket { + private receiveHooks: ((data: WebSocketWS.Data) => Promise)[]; + private sendHooks: ((data: any, cb?: (err?: Error) => void) => Promise)[]; + private msgChain: Promise; + private sendChain: Promise; + + constructor(...rest: any[]) { + super(...rest); + // Make sure the message chain is initialized + this.msgChain = Promise.resolve(); + this.sendChain = Promise.resolve(); + this.receiveHooks = []; + this.sendHooks = []; + } + + public sendToRealKernel(data: any, a2: any) { + // This will skip the send hooks. It's coming from + // the UI side. + super.send(data, a2); + } + + public send(data: any, a2: any): void { + if (this.sendHooks) { + // Stick the send hooks into the send chain. We use chain + // to ensure that: + // a) Hooks finish before we fire the event for real + // b) Event fires + // c) Next message happens after this one (so the UI can handle the message before another event goes through) + this.sendChain = this.sendChain + .then(() => Promise.all(this.sendHooks.map((s) => s(data, a2)))) + .then(() => super.send(data, a2)); + } else { + super.send(data, a2); + } + } + + public emit(event: string | symbol, ...args: any[]): boolean { + if (event === 'message' && this.receiveHooks.length) { + // Stick the receive hooks into the message chain. We use chain + // to ensure that: + // a) Hooks finish before we fire the event for real + // b) Event fires + // c) Next message happens after this one (so this side can handle the message before another event goes through) + this.msgChain = this.msgChain + .then(() => Promise.all(this.receiveHooks.map((p) => p(args[0])))) + .then(() => super.emit(event, ...args)); + // True value indicates there were handlers. We definitely have 'message' handlers. + return true; + } else { + return super.emit(event, ...args); + } + } + + public addReceiveHook(hook: (data: WebSocketWS.Data) => Promise) { + this.receiveHooks.push(hook); + } + public removeReceiveHook(hook: (data: WebSocketWS.Data) => Promise) { + this.receiveHooks = this.receiveHooks.filter((l) => l !== hook); + } + + // tslint:disable-next-line: no-any + public addSendHook(patch: (data: any, cb?: (err?: Error) => void) => Promise): void { + this.sendHooks.push(patch); + } + + // tslint:disable-next-line: no-any + public removeSendHook(patch: (data: any, cb?: (err?: Error) => void) => Promise): void { + this.sendHooks = this.sendHooks.filter((p) => p !== patch); + } + }; +} diff --git a/src/client/datascience/liveshare/liveshare.ts b/src/client/datascience/liveshare/liveshare.ts new file mode 100644 index 000000000000..c7ecb30ec1d8 --- /dev/null +++ b/src/client/datascience/liveshare/liveshare.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import * as vsls from 'vsls/vscode'; + +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../common/application/types'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { LiveShareProxy } from './liveshareProxy'; + +// tslint:disable:no-any unified-signatures + +@injectable() +export class LiveShareApi implements ILiveShareApi { + private supported: boolean = false; + private apiPromise: Promise | undefined; + private disposed: boolean = false; + + constructor( + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IWorkspaceService) workspace: IWorkspaceService, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IApplicationShell) private appShell: IApplicationShell + ) { + const disposable = workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('python.dataScience', undefined)) { + // When config changes happen, recreate our commands. + this.onSettingsChanged(); + } + }); + disposableRegistry.push(disposable); + disposableRegistry.push(this); + this.onSettingsChanged(); + } + + public dispose(): void { + this.disposed = true; + } + + public getApi(): Promise { + if (this.disposed) { + return Promise.resolve(null); + } + return this.apiPromise!; + } + + private onSettingsChanged() { + const supported = this.configService.getSettings().datascience.allowLiveShare; + if (supported !== this.supported) { + this.supported = supported ? true : false; + const liveShareTimeout = this.configService.getSettings().datascience.liveShareConnectionTimeout; + this.apiPromise = supported + ? vsls + .getApi() + .then((a) => (a ? new LiveShareProxy(this.appShell, liveShareTimeout, a) : a)) + .catch((_e) => null) + : Promise.resolve(null); + } else if (!this.apiPromise) { + this.apiPromise = Promise.resolve(null); + } + } +} diff --git a/src/client/datascience/liveshare/liveshareProxy.ts b/src/client/datascience/liveshare/liveshareProxy.ts new file mode 100644 index 000000000000..4a4b2f10b5d3 --- /dev/null +++ b/src/client/datascience/liveshare/liveshareProxy.ts @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Disposable, Event, TreeDataProvider, Uri } from 'vscode'; +import * as vsls from 'vsls/vscode'; + +import { IApplicationShell } from '../../common/application/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import * as localize from '../../common/utils/localize'; +import { LiveShare, LiveShareCommands } from '../constants'; +import { ServiceProxy } from './serviceProxy'; + +// tslint:disable:no-any unified-signatures +export class LiveShareProxy implements vsls.LiveShare { + private currentRole: vsls.Role = vsls.Role.None; + private guestChecker: vsls.SharedService | vsls.SharedServiceProxy | null = null; + private pendingGuestCheckCount = 0; + private peerCheckPromise: Deferred | undefined; + constructor( + private applicationShell: IApplicationShell, + private peerTimeout: number | undefined, + private realApi: vsls.LiveShare + ) { + this.realApi.onDidChangePeers(this.onPeersChanged, this); + this.realApi.onDidChangeSession(this.onSessionChanged, this); + this.onSessionChanged({ session: this.realApi.session }).ignoreErrors(); + } + public get session(): vsls.Session { + return this.realApi.session; + } + public get onDidChangeSession(): Event { + return this.realApi.onDidChangeSession; + } + public get peers(): vsls.Peer[] { + return this.realApi.peers; + } + public get onDidChangePeers(): Event { + return this.realApi.onDidChangePeers; + } + public share(options?: vsls.ShareOptions | undefined): Promise { + return this.realApi.share(options); + } + public join(link: Uri, options?: vsls.JoinOptions | undefined): Promise { + return this.realApi.join(link, options); + } + public end(): Promise { + return this.realApi.end(); + } + public async shareService(name: string): Promise { + // Create the real shared service. + const realService = await this.realApi.shareService(name); + + // Create a proxy for the shared service. This allows us to wait for the next request/response + // on the shared service to cause a failure when the guest doesn't have the python extension installed. + if (realService) { + return new ServiceProxy( + realService, + () => this.peersAreOkay(), + () => this.forceShutdown() + ); + } + + return realService; + } + public unshareService(name: string): Promise { + return this.realApi.unshareService(name); + } + public getSharedService(name: string): Promise { + return this.realApi.getSharedService(name); + } + public convertLocalUriToShared(localUri: Uri): Uri { + return this.realApi.convertLocalUriToShared(localUri); + } + public convertSharedUriToLocal(sharedUri: Uri): Uri { + return this.realApi.convertSharedUriToLocal(sharedUri); + } + public registerCommand(command: string, isEnabled?: (() => boolean) | undefined, thisArg?: any): Disposable | null { + return this.realApi.registerCommand(command, isEnabled, thisArg); + } + public registerTreeDataProvider(viewId: vsls.View, treeDataProvider: TreeDataProvider): Disposable | null { + return this.realApi.registerTreeDataProvider(viewId, treeDataProvider); + } + public registerContactServiceProvider( + name: string, + contactServiceProvider: vsls.ContactServiceProvider + ): Disposable | null { + return this.realApi.registerContactServiceProvider(name, contactServiceProvider); + } + public shareServer(server: vsls.Server): Promise { + return this.realApi.shareServer(server); + } + public getContacts(emails: string[]): Promise { + return this.realApi.getContacts(emails); + } + + private async onSessionChanged(ev: vsls.SessionChangeEvent): Promise { + const newRole = ev.session ? ev.session.role : vsls.Role.None; + if (this.currentRole !== newRole) { + // Setup our guest checker service. + if (this.currentRole === vsls.Role.Host) { + await this.realApi.unshareService(LiveShare.GuestCheckerService); + } + this.currentRole = newRole; + + // If host, we need to listen for responses + if (this.currentRole === vsls.Role.Host) { + this.guestChecker = await this.realApi.shareService(LiveShare.GuestCheckerService); + if (this.guestChecker) { + this.guestChecker.onNotify(LiveShareCommands.guestCheck, (_args: object) => this.onGuestResponse()); + } + + // If guest, we need to list for requests. + } else if (this.currentRole === vsls.Role.Guest) { + this.guestChecker = await this.realApi.getSharedService(LiveShare.GuestCheckerService); + if (this.guestChecker) { + this.guestChecker.onNotify(LiveShareCommands.guestCheck, (_args: object) => this.onHostRequest()); + } + } + } + } + + private onPeersChanged(_ev: vsls.PeersChangeEvent) { + if (this.currentRole === vsls.Role.Host && this.guestChecker) { + // Update our pending count. This means we need to ask again if positive. + this.pendingGuestCheckCount = this.realApi.peers.length; + this.peerCheckPromise = undefined; + } + } + + private peersAreOkay(): Promise { + // If already asking, just use that promise + if (this.peerCheckPromise) { + return this.peerCheckPromise.promise; + } + + // Shortcut if we don't need to ask. + if (!this.guestChecker || this.currentRole !== vsls.Role.Host || this.pendingGuestCheckCount <= 0) { + return Promise.resolve(true); + } + + // We need to ask each guest then. + this.peerCheckPromise = createDeferred(); + this.guestChecker.notify(LiveShareCommands.guestCheck, {}); + + // Wait for a second and then check + setTimeout(this.validatePendingGuests.bind(this), this.peerTimeout ? this.peerTimeout : 1000); + return this.peerCheckPromise.promise; + } + + private validatePendingGuests() { + if (this.peerCheckPromise && !this.peerCheckPromise.resolved) { + this.peerCheckPromise.resolve(this.pendingGuestCheckCount <= 0); + } + } + + private onGuestResponse() { + // Guest has responded to a guest check. Update our pending count + this.pendingGuestCheckCount -= 1; + if (this.pendingGuestCheckCount <= 0 && this.peerCheckPromise) { + this.peerCheckPromise.resolve(true); + } + } + + private onHostRequest() { + // Host is asking us to respond + if (this.guestChecker && this.currentRole === vsls.Role.Guest) { + this.guestChecker.notify(LiveShareCommands.guestCheck, {}); + } + } + + private forceShutdown() { + // One or more guests doesn't have the python extension installed. Force our live share session to disconnect + this.realApi + .end() + .then(() => { + this.pendingGuestCheckCount = 0; + this.peerCheckPromise = undefined; + this.applicationShell.showErrorMessage(localize.DataScience.liveShareInvalid()); + }) + .ignoreErrors(); + } +} diff --git a/src/client/datascience/liveshare/postOffice.ts b/src/client/datascience/liveshare/postOffice.ts new file mode 100644 index 000000000000..ac255edd586b --- /dev/null +++ b/src/client/datascience/liveshare/postOffice.ts @@ -0,0 +1,284 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { JSONArray } from '@phosphor/coreutils'; +import * as vscode from 'vscode'; +import * as vsls from 'vsls/vscode'; + +import { ILiveShareApi } from '../../common/application/types'; +import { traceInfo } from '../../common/logger'; +import { IAsyncDisposable } from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { LiveShare } from '../constants'; + +// tslint:disable:no-any + +interface IMessageArgs { + args: string; +} + +// This class is used to register two communication between a host and all of its guests +export class PostOffice implements IAsyncDisposable { + private name: string; + private startedPromise: Deferred | undefined; + private hostServer: vsls.SharedService | null = null; + private guestServer: vsls.SharedServiceProxy | null = null; + private currentRole: vsls.Role = vsls.Role.None; + private currentPeerCount: number = 0; + private peerCountChangedEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private commandMap: { [key: string]: { thisArg: any; callback(...args: any[]): void } } = {}; + + constructor( + name: string, + private liveShareApi: ILiveShareApi, + private hostArgsTranslator?: (api: vsls.LiveShare | null, command: string, role: vsls.Role, args: any[]) => void + ) { + this.name = name; + + // Note to self, could the callbacks be keeping things alive that we don't want to be alive? + } + + public get peerCount() { + return this.currentPeerCount; + } + + public get peerCountChanged(): vscode.Event { + return this.peerCountChangedEmitter.event; + } + + public get role() { + return this.currentRole; + } + + public async dispose() { + this.peerCountChangedEmitter.fire(0); + this.peerCountChangedEmitter.dispose(); + if (this.hostServer) { + traceInfo(`Shutting down live share api`); + const s = await this.getApi(); + if (s !== null) { + await s.unshareService(this.name); + } + this.hostServer = null; + } + this.guestServer = null; + } + + public async postCommand(command: string, ...args: any[]): Promise { + // Make sure startup finished + const api = await this.getApi(); + let skipDefault = false; + + if (api && api.session) { + switch (this.currentRole) { + case vsls.Role.Guest: + // Ask host to broadcast + if (this.guestServer) { + this.guestServer.notify( + LiveShare.LiveShareBroadcastRequest, + this.createBroadcastArgs(command, ...args) + ); + } + skipDefault = true; + break; + case vsls.Role.Host: + // Notify everybody and call our local callback (by falling through) + if (this.hostServer) { + this.hostServer.notify( + this.escapeCommandName(command), + this.translateArgs(api, command, ...args) + ); + } + break; + default: + break; + } + } + + if (!skipDefault) { + // Default when not connected is to just call the registered callback + this.callCallback(command, ...args); + } + } + + public async registerCallback(command: string, callback: (...args: any[]) => void, thisArg?: any): Promise { + const api = await this.getApi(); + + // For a guest, make sure to register the notification + if (api && api.session && api.session.role === vsls.Role.Guest && this.guestServer) { + this.guestServer.onNotify(this.escapeCommandName(command), (a) => + this.onGuestNotify(command, a as IMessageArgs) + ); + } + + // Always stick in the command map so that if we switch roles, we reregister + this.commandMap[command] = { callback, thisArg }; + } + + private createBroadcastArgs(command: string, ...args: any[]): IMessageArgs { + return { args: JSON.stringify([command, ...args]) }; + } + + private translateArgs(api: vsls.LiveShare, command: string, ...args: any[]): IMessageArgs { + // Make sure to eliminate all .toJSON functions on our arguments. Otherwise they're stringified incorrectly + for (let a = 0; a <= args.length; a += 1) { + // Eliminate this on only object types (https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript) + if (args[a] === Object(args[a])) { + args[a].toJSON = undefined; + } + } + + // Copy our args so we don't affect callers. + const copyArgs = JSON.parse(JSON.stringify(args)); + + // Some file path args need to have their values translated to guest + // uri format for use on a guest. Try to find any file arguments + const callback = this.commandMap.hasOwnProperty(command) ? this.commandMap[command].callback : undefined; + if (callback) { + // Give the passed in args translator a chance to attempt a translation + if (this.hostArgsTranslator) { + this.hostArgsTranslator(api, command, vsls.Role.Host, copyArgs); + } + } + + // Then wrap them all up in a string. + return { args: JSON.stringify(copyArgs) }; + } + + private escapeCommandName(command: string): string { + // Replace . with $ instead. + return command.replace(/\./g, '$'); + } + + private unescapeCommandName(command: string): string { + // Turn $ back into . + return command.replace(/\$/g, '.'); + } + + private onGuestNotify = (command: string, m: IMessageArgs) => { + const unescaped = this.unescapeCommandName(command); + const args = JSON.parse(m.args) as JSONArray; + this.callCallback(unescaped, ...args); + }; + + private callCallback(command: string, ...args: any[]) { + const callback = this.getCallback(command); + if (callback) { + callback(...args); + } + } + + private getCallback(command: string): ((...args: any[]) => void) | undefined { + let callback = this.commandMap.hasOwnProperty(command) ? this.commandMap[command].callback : undefined; + if (callback) { + // Bind the this arg if necessary + const thisArg = this.commandMap[command].thisArg; + if (thisArg) { + callback = callback.bind(thisArg); + } + } + + return callback; + } + + private getApi(): Promise { + if (!this.startedPromise) { + this.startedPromise = createDeferred(); + this.startCommandServer() + .then((v) => this.startedPromise!.resolve(v)) + .catch((e) => this.startedPromise!.reject(e)); + } + + return this.startedPromise.promise; + } + + private async startCommandServer(): Promise { + const api = await this.liveShareApi.getApi(); + if (api !== null) { + api.onDidChangeSession(() => this.onChangeSession(api).ignoreErrors()); + api.onDidChangePeers(() => this.onChangePeers(api).ignoreErrors()); + await this.onChangeSession(api); + await this.onChangePeers(api); + } + return api; + } + + private async onChangeSession(api: vsls.LiveShare): Promise { + // Startup or shutdown our connection to the other side + if (api.session) { + if (this.currentRole !== api.session.role) { + this.currentRole = api.session.role; + // We're changing our role. + if (this.hostServer) { + await api.unshareService(this.name); + this.hostServer = null; + } + if (this.guestServer) { + this.guestServer = null; + } + } + + // Startup our proxy or server + if (api.session.role === vsls.Role.Host) { + this.hostServer = await api.shareService(this.name); + + // When we start the host, listen for the broadcast message + if (this.hostServer !== null) { + this.hostServer.onNotify(LiveShare.LiveShareBroadcastRequest, (a) => + this.onBroadcastRequest(api, a as IMessageArgs) + ); + } + } else if (api.session.role === vsls.Role.Guest) { + this.guestServer = await api.getSharedService(this.name); + + // When we switch to guest mode, we may have to reregister all of our commands. + this.registerGuestCommands(api); + } + } + } + + private async onChangePeers(api: vsls.LiveShare): Promise { + let newPeerCount = 0; + if (api.session) { + newPeerCount = api.peers.length; + } + if (newPeerCount !== this.currentPeerCount) { + this.peerCountChangedEmitter.fire(newPeerCount); + this.currentPeerCount = newPeerCount; + } + } + + private onBroadcastRequest = (api: vsls.LiveShare, a: IMessageArgs) => { + // This means we need to rebroadcast a request. We should also handle this request ourselves (as this means + // a guest is trying to tell everybody about a command) + if (a.args.length > 0) { + const jsonArray = JSON.parse(a.args) as JSONArray; + if (jsonArray !== null && jsonArray.length >= 2) { + const firstArg = jsonArray[0]!; // More stupid hygiene problems. + const command = firstArg !== null ? firstArg.toString() : ''; + + // Args need to be translated from guest to host + const rest = jsonArray.slice(1); + if (this.hostArgsTranslator) { + this.hostArgsTranslator(api, command, vsls.Role.Guest, rest); + } + + this.postCommand(command, ...rest).ignoreErrors(); + } + } + }; + + private registerGuestCommands(api: vsls.LiveShare) { + if (api && api.session && api.session.role === vsls.Role.Guest && this.guestServer !== null) { + const keys = Object.keys(this.commandMap); + keys.forEach((k) => { + if (this.guestServer !== null) { + // Hygiene is too dumb to recognize the if above + this.guestServer.onNotify(this.escapeCommandName(k), (a) => + this.onGuestNotify(k, a as IMessageArgs) + ); + } + }); + } + } +} diff --git a/src/client/datascience/liveshare/serviceProxy.ts b/src/client/datascience/liveshare/serviceProxy.ts new file mode 100644 index 000000000000..31d94cb26433 --- /dev/null +++ b/src/client/datascience/liveshare/serviceProxy.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Event } from 'vscode'; +import * as vsls from 'vsls/vscode'; + +// tslint:disable:no-any unified-signatures +export class ServiceProxy implements vsls.SharedService { + constructor( + private realService: vsls.SharedService, + private guestsResponding: () => Promise, + private forceShutdown: () => void + ) {} + public get isServiceAvailable(): boolean { + return this.realService.isServiceAvailable; + } + public get onDidChangeIsServiceAvailable(): Event { + return this.realService.onDidChangeIsServiceAvailable; + } + + public onRequest(name: string, handler: vsls.RequestHandler): void { + return this.realService.onRequest(name, handler); + } + public onNotify(name: string, handler: vsls.NotifyHandler): void { + return this.realService.onNotify(name, handler); + } + public async notify(name: string, args: object): Promise { + if (await this.guestsResponding()) { + return this.realService.notify(name, args); + } else { + this.forceShutdown(); + } + } +} diff --git a/src/client/datascience/messages.ts b/src/client/datascience/messages.ts new file mode 100644 index 000000000000..c7e7ca14dcf8 --- /dev/null +++ b/src/client/datascience/messages.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export enum CssMessages { + GetCssRequest = 'get_css_request', + GetCssResponse = 'get_css_response', + GetMonacoThemeRequest = 'get_monaco_theme_request', + GetMonacoThemeResponse = 'get_monaco_theme_response' +} + +export enum SharedMessages { + UpdateSettings = 'update_settings', + Started = 'started', + LocInit = 'loc_init', + StyleUpdate = 'style_update' +} + +export interface IGetCssRequest { + isDark: boolean; +} + +export interface IGetMonacoThemeRequest { + isDark: boolean; +} + +export interface IGetCssResponse { + css: string; + theme: string; + knownDark?: boolean; +} diff --git a/src/client/datascience/monacoMessages.ts b/src/client/datascience/monacoMessages.ts new file mode 100644 index 000000000000..fcd8fcd4620f --- /dev/null +++ b/src/client/datascience/monacoMessages.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; + +export interface IGetMonacoThemeResponse { + theme: monacoEditor.editor.IStandaloneThemeData; +} diff --git a/src/client/datascience/multiplexingDebugService.ts b/src/client/datascience/multiplexingDebugService.ts new file mode 100644 index 000000000000..f4801a0d4c06 --- /dev/null +++ b/src/client/datascience/multiplexingDebugService.ts @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { inject, injectable, named } from 'inversify'; +import { + Breakpoint, + BreakpointsChangeEvent, + DebugAdapterDescriptorFactory, + DebugAdapterTrackerFactory, + DebugConfiguration, + DebugConfigurationProvider, + DebugConsole, + DebugSession, + DebugSessionCustomEvent, + Disposable, + Event, + EventEmitter, + WorkspaceFolder +} from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { ICommandManager, IDebugService } from '../common/application/types'; +import { IDisposableRegistry } from '../common/types'; +import { Identifiers } from './constants'; +import { IJupyterDebugService } from './types'; + +/** + * IJupyterDebugService that will pick the correct debugger based on if doing run by line or normal debugging. + * RunByLine will use the JupyterDebugService, Normal debugging will use the VS code debug service. + */ +@injectable() +export class MultiplexingDebugService implements IJupyterDebugService { + private lastStartedService: IDebugService | undefined; + private sessionChangedEvent: EventEmitter = new EventEmitter(); + private sessionStartedEvent: EventEmitter = new EventEmitter(); + private sessionTerminatedEvent: EventEmitter = new EventEmitter(); + private sessionCustomEvent: EventEmitter = new EventEmitter(); + + constructor( + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(ICommandManager) private commandManager: ICommandManager, + @inject(IDebugService) private vscodeDebugService: IDebugService, + @inject(IJupyterDebugService) + @named(Identifiers.RUN_BY_LINE_DEBUGSERVICE) + private jupyterDebugService: IJupyterDebugService + ) { + disposableRegistry.push(vscodeDebugService.onDidTerminateDebugSession(this.endedDebugSession.bind(this))); + disposableRegistry.push(jupyterDebugService.onDidTerminateDebugSession(this.endedDebugSession.bind(this))); + disposableRegistry.push(vscodeDebugService.onDidStartDebugSession(this.startedDebugSession.bind(this))); + disposableRegistry.push(jupyterDebugService.onDidStartDebugSession(this.startedDebugSession.bind(this))); + disposableRegistry.push(vscodeDebugService.onDidChangeActiveDebugSession(this.changedDebugSession.bind(this))); + disposableRegistry.push(jupyterDebugService.onDidChangeActiveDebugSession(this.changedDebugSession.bind(this))); + disposableRegistry.push(vscodeDebugService.onDidReceiveDebugSessionCustomEvent(this.gotCustomEvent.bind(this))); + disposableRegistry.push( + jupyterDebugService.onDidReceiveDebugSessionCustomEvent(this.gotCustomEvent.bind(this)) + ); + } + public get activeDebugSession(): DebugSession | undefined { + return this.activeService.activeDebugSession; + } + + public get activeDebugConsole(): DebugConsole { + return this.activeService.activeDebugConsole; + } + public get breakpoints(): Breakpoint[] { + return this.activeService.breakpoints; + } + public get onDidChangeActiveDebugSession(): Event { + return this.sessionChangedEvent.event; + } + public get onDidStartDebugSession(): Event { + return this.sessionStartedEvent.event; + } + public get onDidReceiveDebugSessionCustomEvent(): Event { + return this.sessionCustomEvent.event; + } + public get onDidTerminateDebugSession(): Event { + return this.sessionTerminatedEvent.event; + } + public get onDidChangeBreakpoints(): Event { + return this.activeService.onDidChangeBreakpoints; + } + public get onBreakpointHit(): Event { + return this.jupyterDebugService.onBreakpointHit; + } + public startRunByLine(config: DebugConfiguration): Thenable { + this.lastStartedService = this.jupyterDebugService; + return this.jupyterDebugService.startRunByLine(config); + } + public registerDebugConfigurationProvider(debugType: string, provider: DebugConfigurationProvider): Disposable { + const d1 = this.vscodeDebugService.registerDebugConfigurationProvider(debugType, provider); + const d2 = this.jupyterDebugService.registerDebugConfigurationProvider(debugType, provider); + return this.combineDisposables(d1, d2); + } + public registerDebugAdapterDescriptorFactory( + debugType: string, + factory: DebugAdapterDescriptorFactory + ): Disposable { + const d1 = this.vscodeDebugService.registerDebugAdapterDescriptorFactory(debugType, factory); + const d2 = this.jupyterDebugService.registerDebugAdapterDescriptorFactory(debugType, factory); + return this.combineDisposables(d1, d2); + } + public registerDebugAdapterTrackerFactory(debugType: string, factory: DebugAdapterTrackerFactory): Disposable { + const d1 = this.vscodeDebugService.registerDebugAdapterTrackerFactory(debugType, factory); + const d2 = this.jupyterDebugService.registerDebugAdapterTrackerFactory(debugType, factory); + return this.combineDisposables(d1, d2); + } + public startDebugging( + folder: WorkspaceFolder | undefined, + nameOrConfiguration: string | DebugConfiguration, + parentSession?: DebugSession | undefined + ): Thenable { + this.lastStartedService = this.vscodeDebugService; + return this.vscodeDebugService.startDebugging(folder, nameOrConfiguration, parentSession); + } + public addBreakpoints(breakpoints: Breakpoint[]): void { + return this.activeService.addBreakpoints(breakpoints); + } + public removeBreakpoints(breakpoints: Breakpoint[]): void { + return this.activeService.removeBreakpoints(breakpoints); + } + + public getStack(): Promise { + if (this.lastStartedService === this.jupyterDebugService) { + return this.jupyterDebugService.getStack(); + } + throw new Error('Requesting jupyter specific stack when not debugging.'); + } + public step(): Promise { + if (this.lastStartedService === this.jupyterDebugService) { + return this.jupyterDebugService.step(); + } + throw new Error('Requesting jupyter specific step when not debugging.'); + } + public continue(): Promise { + if (this.lastStartedService === this.jupyterDebugService) { + return this.jupyterDebugService.continue(); + } + throw new Error('Requesting jupyter specific step when not debugging.'); + } + public requestVariables(): Promise { + if (this.lastStartedService === this.jupyterDebugService) { + return this.jupyterDebugService.requestVariables(); + } + throw new Error('Requesting jupyter specific variables when not debugging.'); + } + + public stop(): void { + if (this.lastStartedService === this.jupyterDebugService) { + this.jupyterDebugService.stop(); + } else { + // Stop our debugging UI session, no await as we just want it stopped + this.commandManager.executeCommand('workbench.action.debug.stop'); + } + } + + private get activeService(): IDebugService { + if (this.lastStartedService) { + return this.lastStartedService; + } else { + return this.vscodeDebugService; + } + } + + private combineDisposables(d1: Disposable, d2: Disposable): Disposable { + return { + dispose: () => { + d1.dispose(); + d2.dispose(); + } + }; + } + + private endedDebugSession(session: DebugSession) { + this.sessionTerminatedEvent.fire(session); + this.lastStartedService = undefined; + } + + private startedDebugSession(session: DebugSession) { + this.sessionStartedEvent.fire(session); + } + + private changedDebugSession(session: DebugSession | undefined) { + this.sessionChangedEvent.fire(session); + } + + private gotCustomEvent(e: DebugSessionCustomEvent) { + this.sessionCustomEvent.fire(e); + } +} diff --git a/src/client/datascience/notebook/constants.ts b/src/client/datascience/notebook/constants.ts new file mode 100644 index 000000000000..4cf273010809 --- /dev/null +++ b/src/client/datascience/notebook/constants.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export const JupyterNotebookView = 'jupyter-notebook'; +export const JupyterNotebookRenderer = 'jupyter-notebook-renderer'; +export const RendererExtensionId = 'ms-ai-tools.notebook-renderers'; +export const RendererExtensionDownloadUri = 'https://aka.ms/NotebookRendererDownloadLink'; diff --git a/src/client/datascience/notebook/contentProvider.ts b/src/client/datascience/notebook/contentProvider.ts new file mode 100644 index 000000000000..12e3e0e9d41f --- /dev/null +++ b/src/client/datascience/notebook/contentProvider.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationToken, EventEmitter, Uri } from 'vscode'; +import type { + NotebookCommunication, + NotebookData, + NotebookDocument, + NotebookDocumentBackup, + NotebookDocumentBackupContext, + NotebookDocumentContentChangeEvent, + NotebookDocumentOpenContext +} from 'vscode-proposed'; +import { MARKDOWN_LANGUAGE } from '../../common/constants'; +import { DataScience } from '../../common/utils/localize'; +import { captureTelemetry, sendTelemetryEvent, setSharedProperty } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { INotebookStorageProvider } from '../notebookStorage/notebookStorageProvider'; +import { VSCodeNotebookModel } from '../notebookStorage/vscNotebookModel'; +import { notebookModelToVSCNotebookData } from './helpers/helpers'; +import { NotebookEditorCompatibilitySupport } from './notebookEditorCompatibilitySupport'; +import { INotebookContentProvider } from './types'; +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); + +/** + * This class is responsible for reading a notebook file (ipynb or other files) and returning VS Code with the NotebookData. + * Its up to extension authors to read the files and return it in a format that VSCode understands. + * Same with the cells and cell output. + * + * Also responsible for saving of notebooks. + * When saving, VSC will provide their model and we need to take that and merge it with an existing ipynb json (if any, to preserve metadata). + */ +@injectable() +export class NotebookContentProvider implements INotebookContentProvider { + private notebookChanged = new EventEmitter(); + public get onDidChangeNotebook() { + return this.notebookChanged.event; + } + constructor( + @inject(INotebookStorageProvider) private readonly notebookStorage: INotebookStorageProvider, + @inject(NotebookEditorCompatibilitySupport) + private readonly compatibilitySupport: NotebookEditorCompatibilitySupport + ) {} + public notifyChangesToDocument(document: NotebookDocument) { + this.notebookChanged.fire({ document }); + } + public async resolveNotebook(_document: NotebookDocument, _webview: NotebookCommunication): Promise { + // Later + } + public async openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): Promise { + if (!this.compatibilitySupport.canOpenWithVSCodeNotebookEditor(uri)) { + // If not supported, return a notebook with error displayed. + // We cannot, not display a notebook. + return { + cells: [ + { + cellKind: vscodeNotebookEnums.CellKind.Markdown, + language: MARKDOWN_LANGUAGE, + source: `# ${DataScience.usingPreviewNotebookWithOtherNotebookWarning()}`, + metadata: { editable: false, runnable: false }, + outputs: [] + } + ], + languages: [], + metadata: { cellEditable: false, editable: false, runnable: false } + }; + } + // If there's no backup id, then skip loading dirty contents. + const model = await (openContext.backupId + ? this.notebookStorage.getOrCreateModel(uri, undefined, openContext.backupId, true) + : this.notebookStorage.getOrCreateModel(uri, undefined, true, true)); + if (!(model instanceof VSCodeNotebookModel)) { + throw new Error('Incorrect NotebookModel, expected VSCodeNotebookModel'); + } + setSharedProperty('ds_notebookeditor', 'native'); + sendTelemetryEvent(Telemetry.CellCount, undefined, { count: model.cells.length }); + return notebookModelToVSCNotebookData(model); + } + @captureTelemetry(Telemetry.Save, undefined, true) + public async saveNotebook(document: NotebookDocument, cancellation: CancellationToken) { + const model = await this.notebookStorage.getOrCreateModel(document.uri, undefined, undefined, true); + if (cancellation.isCancellationRequested) { + return; + } + await this.notebookStorage.save(model, cancellation); + } + + public async saveNotebookAs( + targetResource: Uri, + document: NotebookDocument, + cancellation: CancellationToken + ): Promise { + const model = await this.notebookStorage.getOrCreateModel(document.uri, undefined, undefined, true); + if (!cancellation.isCancellationRequested) { + await this.notebookStorage.saveAs(model, targetResource); + } + } + public async backupNotebook( + document: NotebookDocument, + _context: NotebookDocumentBackupContext, + cancellation: CancellationToken + ): Promise { + const model = await this.notebookStorage.getOrCreateModel(document.uri, undefined, undefined, true); + const id = this.notebookStorage.generateBackupId(model); + await this.notebookStorage.backup(model, cancellation, id); + return { + id, + delete: () => this.notebookStorage.deleteBackup(model, id).ignoreErrors() + }; + } +} diff --git a/src/client/datascience/notebook/helpers/executionHelpers.ts b/src/client/datascience/notebook/helpers/executionHelpers.ts new file mode 100644 index 000000000000..7372cc206ed6 --- /dev/null +++ b/src/client/datascience/notebook/helpers/executionHelpers.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import type { nbformat } from '@jupyterlab/coreutils'; +import type { KernelMessage } from '@jupyterlab/services'; +import * as fastDeepEqual from 'fast-deep-equal'; +import { NotebookCell, NotebookCellRunState, NotebookDocument } from 'vscode'; +import { createErrorOutput } from '../../../../datascience-ui/common/cellFactory'; +import { createIOutputFromCellOutputs, createVSCCellOutputsFromOutputs, translateErrorOutput } from './helpers'; +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); + +/** + * Updates the cell in notebook model as well as the notebook document. + * Update notebook document so UI is updated accordingly. + * Notebook model is what we use to update/track changes to ipynb. + * @returns {boolean} Returns `true` if output has changed. + */ +export function handleUpdateDisplayDataMessage( + msg: KernelMessage.IUpdateDisplayDataMsg, + document: NotebookDocument +): boolean { + // Find any cells that have this same display_id + return ( + document.cells.filter((cellToCheck, index) => { + if (cellToCheck.cellKind !== vscodeNotebookEnums.CellKind.Code) { + return false; + } + + let updated = false; + const outputs = createIOutputFromCellOutputs(cellToCheck.outputs); + const changedOutputs = outputs.map((output) => { + if ( + (output.output_type === 'display_data' || output.output_type === 'execute_result') && + output.transient && + // tslint:disable-next-line: no-any + (output.transient as any).display_id === msg.content.transient.display_id + ) { + // Remember we have updated output for this cell. + updated = true; + + return { + ...output, + data: msg.content.data, + metadata: msg.content.metadata + }; + } else { + return output; + } + }); + + if (!updated) { + return false; + } + + const vscCell = document.cells[index]; + updateCellOutput(vscCell, changedOutputs); + return true; + }).length > 0 + ); +} + +/** + * Updates the VSC cell with the error output. + */ +export function updateCellWithErrorStatus(cell: NotebookCell, ex: Partial) { + cell.outputs = [translateErrorOutput(createErrorOutput(ex))]; + cell.metadata.runState = NotebookCellRunState.Error; +} + +/** + * @returns {boolean} Returns `true` if execution count has changed. + */ +export function updateCellExecutionCount(vscCell: NotebookCell, executionCount: number): boolean { + if (vscCell.metadata.executionOrder !== executionCount && executionCount) { + vscCell.metadata.executionOrder = executionCount; + return true; + } + return false; +} + +/** + * Updates our Cell Model with the cell output. + * As we execute a cell we get output from jupyter. This code will ensure the cell is updated with the output. + * Here we update both the VSCode Cell as well as our ICell (cell in our INotebookModel). + * @returns {(boolean | undefined)} Returns `true` if output has changed. + */ +export function updateCellOutput(vscCell: NotebookCell, outputs: nbformat.IOutput[]): boolean | undefined { + const newOutput = createVSCCellOutputsFromOutputs(outputs); + // If there was no output and still no output, then nothing to do. + if (vscCell.outputs.length === 0 && newOutput.length === 0) { + return; + } + // Compare outputs (at the end of the day everything is serializable). + // Hence this is a safe comparison. + if (vscCell.outputs.length === newOutput.length && fastDeepEqual(vscCell.outputs, newOutput)) { + return; + } + vscCell.outputs = newOutput; + return true; +} diff --git a/src/client/datascience/notebook/helpers/helpers.ts b/src/client/datascience/notebook/helpers/helpers.ts new file mode 100644 index 000000000000..6e444f6b9290 --- /dev/null +++ b/src/client/datascience/notebook/helpers/helpers.ts @@ -0,0 +1,676 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { nbformat } from '@jupyterlab/coreutils'; +import * as uuid from 'uuid/v4'; +import type { + CellDisplayOutput, + CellErrorOutput, + CellOutput, + NotebookCell, + NotebookCellData, + NotebookCellMetadata, + NotebookData, + NotebookDocument +} from 'vscode-proposed'; +import { NotebookCellRunState } from '../../../../../typings/vscode-proposed'; +import { concatMultilineString, splitMultilineString } from '../../../../datascience-ui/common'; +import { MARKDOWN_LANGUAGE, PYTHON_LANGUAGE } from '../../../common/constants'; +import { traceError, traceWarning } from '../../../common/logger'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { Telemetry } from '../../constants'; +import { CellState, ICell, INotebookModel } from '../../types'; +import { JupyterNotebookView } from '../constants'; +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); +// tslint:disable-next-line: no-require-imports +import cloneDeep = require('lodash/cloneDeep'); +import { isUntitledFile } from '../../../common/utils/misc'; +import { KernelConnectionMetadata } from '../../jupyter/kernels/types'; +import { updateNotebookMetadata } from '../../notebookStorage/baseModel'; +import { VSCodeNotebookModel } from '../../notebookStorage/vscNotebookModel'; + +// This is the custom type we are adding into nbformat.IBaseCellMetadata +export interface IBaseCellVSCodeMetadata { + end_execution_time?: string; + start_execution_time?: string; +} + +/** + * Whether this is a Notebook we created/manage/use. + * Remember, there could be other notebooks such as GitHub Issues nb by VS Code. + */ +export function isJupyterNotebook(document: NotebookDocument): boolean; +// tslint:disable-next-line: unified-signatures +export function isJupyterNotebook(viewType: string): boolean; +export function isJupyterNotebook(option: NotebookDocument | string) { + if (typeof option === 'string') { + return option === JupyterNotebookView; + } else { + return option.viewType === JupyterNotebookView; + } +} + +const kernelInformationForNotebooks = new WeakMap(); + +export function getNotebookMetadata(document: NotebookDocument): nbformat.INotebookMetadata | undefined { + // tslint:disable-next-line: no-any + let notebookContent: Partial = document.metadata.custom as any; + + // If language isn't specified in the metadata, at least specify that + if (!notebookContent?.metadata?.language_info?.name) { + const content = notebookContent || {}; + const metadata = content.metadata || { orig_nbformat: 3, language_info: {} }; + const language_info = { ...metadata.language_info, name: document.languages[0] }; + // Fix nyc compiler not working. + // tslint:disable-next-line: no-any + notebookContent = { ...content, metadata: { ...metadata, language_info } } as any; + } + notebookContent = cloneDeep(notebookContent); + if (kernelInformationForNotebooks.has(document)) { + updateNotebookMetadata(notebookContent.metadata, kernelInformationForNotebooks.get(document)); + } + + return notebookContent.metadata; +} + +/** + * No need to update the notebook metadata just yet. + * When users open a blank notebook and a kernel is auto selected, document is marked as dirty. Hence as soon as you create a blank notebook it is dr ity. + * Similarly, if you open an existing notebook, it is marked as dirty. + * + * Solution: Store the metadata in some place, when saving, take the metadata & store in the file. + */ +export function updateKernelInNotebookMetadata( + document: NotebookDocument, + kernelConnection: KernelConnectionMetadata | undefined +) { + kernelInformationForNotebooks.set(document, kernelConnection); +} +/** + * Converts a NotebookModel into VSCode friendly format. + */ +export function notebookModelToVSCNotebookData(model: VSCodeNotebookModel): NotebookData { + const cells = model.cells + .map(createVSCNotebookCellDataFromCell.bind(undefined, model)) + .filter((item) => !!item) + .map((item) => item!); + + const defaultLanguage = getDefaultCodeLanguage(model); + if (cells.length === 0 && isUntitledFile(model.file)) { + cells.push({ + cellKind: vscodeNotebookEnums.CellKind.Code, + language: defaultLanguage, + metadata: {}, + outputs: [], + source: '' + }); + } + return { + cells, + languages: ['*'], + metadata: { + custom: model.notebookContentWithoutCells, + cellEditable: model.isTrusted, + cellRunnable: model.isTrusted, + editable: model.isTrusted, + cellHasExecutionOrder: true, + runnable: model.isTrusted, + displayOrder: [ + 'application/vnd.*', + 'application/vdom.*', + 'application/geo+json', + 'application/x-nteract-model-debug+json', + 'text/html', + 'application/javascript', + 'image/gif', + 'text/latex', + 'text/markdown', + 'image/svg+xml', + 'image/png', + 'image/jpeg', + 'application/json', + 'text/plain' + ] + } + }; +} +export function createCellFromVSCNotebookCell(vscCell: NotebookCell, model: INotebookModel): ICell { + let cell: ICell; + if (vscCell.cellKind === vscodeNotebookEnums.CellKind.Markdown) { + const data = createMarkdownCellFromVSCNotebookCell(vscCell); + cell = { + data, + file: model.file.toString(), + id: uuid(), + line: 0, + state: CellState.init + }; + } else if (vscCell.language === 'raw') { + const data = createRawCellFromVSCNotebookCell(vscCell); + cell = { + data, + file: model.file.toString(), + id: uuid(), + line: 0, + state: CellState.init + }; + } else { + const data = createCodeCellFromVSCNotebookCell(vscCell); + cell = { + data, + file: model.file.toString(), + id: uuid(), + line: 0, + state: CellState.init + }; + } + // Delete the `metadata.custom.vscode` property we added. + if ('vscode' in cell.data.metadata) { + const metadata = { ...cell.data.metadata }; + delete metadata.vscode; + cell.data.metadata = metadata; + } + return cell; +} + +/** + * Stores the Jupyter Cell metadata into the VSCode Cells. + * This is used to facilitate: + * 1. When a user copies and pastes a cell, then the corresponding metadata is also copied across. + * 2. Diffing (VSC knows about metadata & stuff that contributes changes to a cell). + */ +export function updateVSCNotebookCellMetadata(cellMetadata: NotebookCellMetadata, cell: ICell) { + cellMetadata.custom = cellMetadata.custom ?? {}; + // We put this only for VSC to display in diff view. + // Else we don't use this. + const propertiesToClone = ['metadata', 'attachments']; + propertiesToClone.forEach((propertyToClone) => { + if (cell.data[propertyToClone]) { + cellMetadata.custom![propertyToClone] = cloneDeep(cell.data[propertyToClone]); + } + }); +} + +export function getDefaultCodeLanguage(model: INotebookModel) { + return model.metadata?.language_info?.name && + model.metadata?.language_info?.name.toLowerCase() !== PYTHON_LANGUAGE.toLowerCase() + ? model.metadata?.language_info?.name + : PYTHON_LANGUAGE; +} +function createRawCellFromVSCNotebookCell(cell: NotebookCell): nbformat.IRawCell { + const rawCell: nbformat.IRawCell = { + cell_type: 'raw', + source: splitMultilineString(cell.document.getText()), + metadata: cell.metadata.custom?.metadata || {} + }; + if (cell.metadata.custom?.attachments) { + rawCell.attachments = cell.metadata.custom?.attachments; + } + return rawCell; +} + +function createVSCNotebookCellDataFromRawCell(model: INotebookModel, cell: ICell): NotebookCellData { + const notebookCellMetadata: NotebookCellMetadata = { + editable: model.isTrusted, + executionOrder: undefined, + hasExecutionOrder: false, + runnable: false + }; + updateVSCNotebookCellMetadata(notebookCellMetadata, cell); + return { + cellKind: vscodeNotebookEnums.CellKind.Code, + language: 'raw', + metadata: notebookCellMetadata, + outputs: [], + source: concatMultilineString(cell.data.source) + }; +} +function createMarkdownCellFromVSCNotebookCell(cell: NotebookCell): nbformat.IMarkdownCell { + const markdownCell: nbformat.IMarkdownCell = { + cell_type: 'markdown', + source: splitMultilineString(cell.document.getText()), + metadata: cell.metadata.custom?.metadata || {} + }; + if (cell.metadata.custom?.attachments) { + markdownCell.attachments = cell.metadata.custom?.attachments; + } + return markdownCell; +} +function createVSCNotebookCellDataFromMarkdownCell(model: INotebookModel, cell: ICell): NotebookCellData { + const notebookCellMetadata: NotebookCellMetadata = { + editable: model.isTrusted, + executionOrder: undefined, + hasExecutionOrder: false, + runnable: false + }; + updateVSCNotebookCellMetadata(notebookCellMetadata, cell); + return { + cellKind: vscodeNotebookEnums.CellKind.Markdown, + language: MARKDOWN_LANGUAGE, + metadata: notebookCellMetadata, + source: concatMultilineString(cell.data.source), + outputs: [] + }; +} +function createVSCNotebookCellDataFromCodeCell(model: INotebookModel, cell: ICell): NotebookCellData { + // tslint:disable-next-line: no-any + const outputs = createVSCCellOutputsFromOutputs(cell.data.outputs as any); + const defaultCodeLanguage = getDefaultCodeLanguage(model); + // If we have an execution count & no errors, then success state. + // If we have an execution count & errors, then error state. + // Else idle state. + const hasErrors = outputs.some((output) => output.outputKind === vscodeNotebookEnums.CellOutputKind.Error); + const hasExecutionCount = typeof cell.data.execution_count === 'number' && cell.data.execution_count > 0; + let runState: NotebookCellRunState; + let statusMessage: string | undefined; + if (!hasExecutionCount) { + runState = vscodeNotebookEnums.NotebookCellRunState.Idle; + } else if (hasErrors) { + runState = vscodeNotebookEnums.NotebookCellRunState.Error; + // Error details are stripped from the output, get raw output. + // tslint:disable-next-line: no-any + statusMessage = getCellStatusMessageBasedOnFirstErrorOutput(cell.data.outputs as any); + } else { + runState = vscodeNotebookEnums.NotebookCellRunState.Success; + } + + const notebookCellMetadata: NotebookCellMetadata = { + editable: model.isTrusted, + executionOrder: typeof cell.data.execution_count === 'number' ? cell.data.execution_count : undefined, + hasExecutionOrder: true, + runState, + runnable: model.isTrusted + }; + + if (statusMessage) { + notebookCellMetadata.statusMessage = statusMessage; + } + const vscodeMetadata = (cell.data.metadata.vscode as unknown) as IBaseCellVSCodeMetadata | undefined; + const startExecutionTime = vscodeMetadata?.start_execution_time + ? new Date(Date.parse(vscodeMetadata.start_execution_time)).getTime() + : undefined; + const endExecutionTime = vscodeMetadata?.end_execution_time + ? new Date(Date.parse(vscodeMetadata.end_execution_time)).getTime() + : undefined; + + if (startExecutionTime && typeof endExecutionTime === 'number') { + notebookCellMetadata.runStartTime = startExecutionTime; + notebookCellMetadata.lastRunDuration = endExecutionTime - startExecutionTime; + } + + updateVSCNotebookCellMetadata(notebookCellMetadata, cell); + + // If not trusted, then clear the output in VSC Cell. + // At this point we have the original output in the ICell. + if (!model.isTrusted) { + while (outputs.length) { + outputs.shift(); + } + } + return { + cellKind: vscodeNotebookEnums.CellKind.Code, + language: defaultCodeLanguage, + metadata: notebookCellMetadata, + source: concatMultilineString(cell.data.source), + outputs + }; +} + +export function createIOutputFromCellOutputs(cellOutputs: CellOutput[]): nbformat.IOutput[] { + return cellOutputs + .map((output) => { + switch (output.outputKind) { + case vscodeNotebookEnums.CellOutputKind.Error: + return translateCellErrorOutput(output); + case vscodeNotebookEnums.CellOutputKind.Rich: + return translateCellDisplayOutput(output); + case vscodeNotebookEnums.CellOutputKind.Text: + // We do not generate text output. + return; + default: + return; + } + }) + .filter((output) => !!output) + .map((output) => output!!); +} + +export function clearCellForExecution(cell: NotebookCell) { + cell.metadata.statusMessage = undefined; + cell.metadata.executionOrder = undefined; + cell.metadata.lastRunDuration = undefined; + cell.metadata.runStartTime = undefined; + cell.outputs = []; + + updateCellExecutionTimes(cell); +} + +/** + * Store execution start and end times. + * Stored as ISO for portability. + */ +export function updateCellExecutionTimes(cell: NotebookCell, times?: { startTime?: number; duration?: number }) { + if (!times || !times.duration || !times.startTime) { + if (cell.metadata.custom?.metadata?.vscode?.start_execution_time) { + delete cell.metadata.custom.metadata.vscode.start_execution_time; + } + if (cell.metadata.custom?.metadata?.vscode?.end_execution_time) { + delete cell.metadata.custom.metadata.vscode.end_execution_time; + } + return; + } + + const startTimeISO = new Date(times.startTime).toISOString(); + const endTimeISO = new Date(times.startTime + times.duration).toISOString(); + cell.metadata.custom = cell.metadata.custom || {}; + cell.metadata.custom.metadata = cell.metadata.custom.metadata || {}; + cell.metadata.custom.metadata.vscode = cell.metadata.custom.metadata.vscode || {}; + cell.metadata.custom.metadata.vscode.end_execution_time = endTimeISO; + cell.metadata.custom.metadata.vscode.start_execution_time = startTimeISO; +} + +function createCodeCellFromVSCNotebookCell(cell: NotebookCell): nbformat.ICodeCell { + const metadata = cell.metadata.custom?.metadata || {}; + return { + cell_type: 'code', + execution_count: cell.metadata.executionOrder ?? null, + source: splitMultilineString(cell.document.getText()), + outputs: createIOutputFromCellOutputs(cell.outputs), + metadata + }; +} +export function createVSCNotebookCellDataFromCell(model: INotebookModel, cell: ICell): NotebookCellData | undefined { + switch (cell.data.cell_type) { + case 'raw': { + return createVSCNotebookCellDataFromRawCell(model, cell); + } + case 'markdown': { + return createVSCNotebookCellDataFromMarkdownCell(model, cell); + } + case 'code': { + return createVSCNotebookCellDataFromCodeCell(model, cell); + } + default: { + traceError(`Conversion of Cell into VS Code NotebookCell not supported ${cell.data.cell_type}`); + } + } +} + +export function createVSCCellOutputsFromOutputs(outputs?: nbformat.IOutput[]): CellOutput[] { + const cellOutputs: nbformat.IOutput[] = Array.isArray(outputs) ? (outputs as []) : []; + return cellOutputs.map(cellOutputToVSCCellOutput); +} +const cellOutputMappers = new Map< + nbformat.OutputType, + (output: nbformat.IOutput, outputType: nbformat.OutputType) => CellOutput +>(); +// tslint:disable-next-line: no-any +cellOutputMappers.set('display_data', translateDisplayDataOutput as any); +// tslint:disable-next-line: no-any +cellOutputMappers.set('error', translateErrorOutput as any); +// tslint:disable-next-line: no-any +cellOutputMappers.set('execute_result', translateDisplayDataOutput as any); +// tslint:disable-next-line: no-any +cellOutputMappers.set('stream', translateStreamOutput as any); +// tslint:disable-next-line: no-any +cellOutputMappers.set('update_display_data', translateDisplayDataOutput as any); +export function cellOutputToVSCCellOutput(output: nbformat.IOutput): CellOutput { + const fn = cellOutputMappers.get(output.output_type as nbformat.OutputType); + let result: CellOutput; + if (fn) { + result = fn(output, (output.output_type as unknown) as nbformat.OutputType); + } else { + traceWarning(`Unable to translate cell from ${output.output_type} to NotebookCellData for VS Code.`); + result = { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + // tslint:disable-next-line: no-any + data: output.data as any, + metadata: { custom: { vscode: { outputType: output.output_type } } } + }; + } + + // Add on transient data if we have any. This should be removed by our save functions elsewhere. + if ( + output.transient && + result && + result.outputKind === vscodeNotebookEnums.CellOutputKind.Rich && + result.metadata + ) { + // tslint:disable-next-line: no-any + result.metadata.custom = { ...result.metadata.custom, transient: output.transient }; + } + return result; +} + +export function vscCellOutputToCellOutput(output: CellOutput): nbformat.IOutput | undefined { + switch (output.outputKind) { + case vscodeNotebookEnums.CellOutputKind.Error: { + return translateCellErrorOutput(output); + } + case vscodeNotebookEnums.CellOutputKind.Rich: { + return translateCellDisplayOutput(output); + } + case vscodeNotebookEnums.CellOutputKind.Text: { + // We do not return such output. + return; + } + default: { + return; + } + } +} + +/** + * Converts a Jupyter display cell output into a VSCode cell output format. + * Handles sizing, adding backgrounds to images and the like. + * E.g. Jupyter cell output contains metadata to add backgrounds to images, here we generate the necessary HTML. + * + * @export + * @param {nbformat.IDisplayData} output + * @returns {(CellDisplayOutput | undefined)} + */ +function translateDisplayDataOutput( + output: nbformat.IDisplayData | nbformat.IDisplayUpdate | nbformat.IExecuteResult, + outputType: nbformat.OutputType +): CellDisplayOutput | undefined { + const data = { ...output.data }; + // tslint:disable-next-line: no-any + const metadata = output.metadata ? ({ custom: output.metadata } as any) : { custom: {} }; + metadata.custom.vscode = { outputType }; + if (output.execution_count) { + metadata.execution_order = output.execution_count; + } + return { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data, + metadata // Used be renderers & VS Code for diffing (it knows what has changed). + }; +} + +function translateStreamOutput(output: nbformat.IStream, outputType: nbformat.OutputType): CellDisplayOutput { + // Do not return as `CellOutputKind.Text`. VSC will not translate ascii output correctly. + // Instead format the output as rich. + return { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { + ['text/plain']: concatMultilineString(output.text, true) + }, + metadata: { + custom: { vscode: { outputType, name: output.name } } + } + }; +} + +// tslint:disable-next-line: no-any +function getSanitizedCellMetadata(metadata?: { [key: string]: any }) { + const cloned = { ...metadata }; + if ('vscode' in cloned) { + delete cloned.vscode; + } + return cloned; +} + +type JupyterOutput = + | nbformat.IUnrecognizedOutput + | nbformat.IExecuteResult + | nbformat.IDisplayData + | nbformat.IStream + | nbformat.IError; + +function translateCellDisplayOutput(output: CellDisplayOutput): JupyterOutput { + const outputType: nbformat.OutputType = output.metadata?.custom?.vscode?.outputType; + let result: JupyterOutput; + switch (outputType) { + case 'stream': + { + result = { + output_type: 'stream', + name: output.metadata?.custom?.vscode?.name, + text: splitMultilineString(output.data['text/plain']) + }; + } + break; + case 'display_data': + { + const metadata = getSanitizedCellMetadata(output.metadata?.custom); + result = { + output_type: 'display_data', + data: output.data, + metadata + }; + } + break; + case 'execute_result': + { + const metadata = getSanitizedCellMetadata(output.metadata?.custom); + result = { + output_type: 'execute_result', + data: output.data, + metadata, + execution_count: output.metadata?.custom?.vscode?.execution_count + }; + } + break; + case 'update_display_data': + { + const metadata = getSanitizedCellMetadata(output.metadata?.custom); + result = { + output_type: 'update_display_data', + data: output.data, + metadata + }; + } + break; + default: + { + sendTelemetryEvent(Telemetry.VSCNotebookCellTranslationFailed, undefined, { + isErrorOutput: outputType === 'error' + }); + const metadata = getSanitizedCellMetadata(output.metadata?.custom); + const unknownOutput: nbformat.IUnrecognizedOutput = { output_type: outputType }; + if (Object.keys(metadata).length > 0) { + unknownOutput.metadata = metadata; + } + if (Object.keys(output.data).length > 0) { + unknownOutput.data = output.data; + } + result = unknownOutput; + } + break; + } + + // Account for transient data as well + if (result && output.metadata && output.metadata.custom?.transient) { + result.transient = { ...output.metadata.custom?.transient }; + } + return result; +} + +/** + * We will display the error message in the status of the cell. + * The `ename` & `evalue` is displayed at the top of the output by VS Code. + * As we're displaying the error in the statusbar, we don't want this dup error in output. + * Hence remove this. + */ +export function translateErrorOutput(output: nbformat.IError): CellErrorOutput { + return { + ename: output.ename, + evalue: output.evalue, + outputKind: vscodeNotebookEnums.CellOutputKind.Error, + traceback: output.traceback + }; +} +export function translateCellErrorOutput(output: CellErrorOutput): nbformat.IError { + return { + output_type: 'error', + ename: output.ename, + evalue: output.evalue, + traceback: output.traceback + }; +} + +export function getCellStatusMessageBasedOnFirstErrorOutput(outputs?: nbformat.IOutput[]): string { + if (!Array.isArray(outputs)) { + return ''; + } + const errorOutput = (outputs.find((output) => output.output_type === 'error') as unknown) as + | nbformat.IError + | undefined; + if (!errorOutput) { + return ''; + } + return `${errorOutput.ename}${errorOutput.evalue ? ': ' : ''}${errorOutput.evalue}`; +} +export function getCellStatusMessageBasedOnFirstCellErrorOutput(outputs?: CellOutput[]): string { + if (!Array.isArray(outputs)) { + return ''; + } + const errorOutput = outputs.find((output) => output.outputKind === vscodeNotebookEnums.CellOutputKind.Error) as + | CellErrorOutput + | undefined; + if (!errorOutput) { + return ''; + } + return `${errorOutput.ename}${errorOutput.evalue ? ': ' : ''}${errorOutput.evalue}`; +} + +/** + * Updates a notebook document as a result of trusting it. + */ +export function updateVSCNotebookAfterTrustingNotebook(document: NotebookDocument, originalCells: ICell[]) { + const areAllCellsEditableAndRunnable = document.cells.every((cell) => { + if (cell.cellKind === vscodeNotebookEnums.CellKind.Markdown) { + return cell.metadata.editable; + } else { + return cell.metadata.editable && cell.metadata.runnable; + } + }); + const isDocumentEditableAndRunnable = + document.metadata.cellEditable && + document.metadata.cellRunnable && + document.metadata.editable && + document.metadata.runnable; + + // If already trusted, then nothing to do. + if (isDocumentEditableAndRunnable && areAllCellsEditableAndRunnable) { + return; + } + + document.metadata.cellEditable = true; + document.metadata.cellRunnable = true; + document.metadata.editable = true; + document.metadata.runnable = true; + + document.cells.forEach((cell, index) => { + cell.metadata.editable = true; + if (cell.cellKind !== vscodeNotebookEnums.CellKind.Markdown) { + cell.metadata.runnable = true; + // Restore the output once we trust the notebook. + // tslint:disable-next-line: no-any + cell.outputs = createVSCCellOutputsFromOutputs(originalCells[index].data.outputs as any); + } + }); +} diff --git a/src/client/datascience/notebook/helpers/multiCancellationToken.ts b/src/client/datascience/notebook/helpers/multiCancellationToken.ts new file mode 100644 index 000000000000..172dff5489fc --- /dev/null +++ b/src/client/datascience/notebook/helpers/multiCancellationToken.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { CancellationToken, EventEmitter } from 'vscode'; + +/** + * Cancellation token source that can be cancelled multiple times. + */ +export class MultiCancellationTokenSource { + /** + * The cancellation token of this source. + */ + public readonly token: CancellationToken; + private readonly eventEmitter = new EventEmitter(); + constructor() { + this.token = { + isCancellationRequested: false, + onCancellationRequested: this.eventEmitter.event.bind(this.eventEmitter) + }; + } + public cancel(): void { + this.token.isCancellationRequested = true; + this.eventEmitter.fire(); + } + + /** + * Dispose object and free resources. + */ + public dispose(): void { + this.eventEmitter.dispose(); + } +} diff --git a/src/client/datascience/notebook/integration.ts b/src/client/datascience/notebook/integration.ts new file mode 100644 index 000000000000..6daa924eed8e --- /dev/null +++ b/src/client/datascience/notebook/integration.ts @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { ConfigurationTarget } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { + IApplicationEnvironment, + IApplicationShell, + IVSCodeNotebook, + IWorkspaceService +} from '../../common/application/types'; +import { NotebookEditorSupport } from '../../common/experiments/groups'; +import { traceError } from '../../common/logger'; +import { IDisposableRegistry, IExperimentsManager, IExtensionContext } from '../../common/types'; +import { DataScience } from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { JupyterNotebookView } from './constants'; +import { isJupyterNotebook } from './helpers/helpers'; +import { VSCodeKernelPickerProvider } from './kernelProvider'; +import { INotebookContentProvider } from './types'; + +const EditorAssociationUpdatedKey = 'EditorAssociationUpdatedToUseNotebooks'; + +/** + * This class basically registers the necessary providers and the like with VSC. + * I.e. this is where we integrate our stuff with VS Code via their extension endpoints. + */ + +@injectable() +export class NotebookIntegration implements IExtensionSingleActivationService { + constructor( + @inject(IVSCodeNotebook) private readonly vscNotebook: IVSCodeNotebook, + @inject(IExperimentsManager) private readonly experiment: IExperimentsManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(INotebookContentProvider) private readonly notebookContentProvider: INotebookContentProvider, + @inject(VSCodeKernelPickerProvider) private readonly kernelProvider: VSCodeKernelPickerProvider, + @inject(IApplicationEnvironment) private readonly env: IApplicationEnvironment, + @inject(IApplicationShell) private readonly shell: IApplicationShell, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IExtensionContext) private readonly extensionContext: IExtensionContext + ) {} + public async activate(): Promise { + // This condition is temporary. + // If user belongs to the experiment, then make the necessary changes to package.json. + // Once the API is final, we won't need to modify the package.json. + if (this.experiment.inExperiment(NotebookEditorSupport.nativeNotebookExperiment)) { + await this.enableNotebooks(); + } else { + // Possible user was in experiment, then they opted out. In this case we need to revert the changes made to the settings file. + // Again, this is temporary code. + await this.disableNotebooks(); + } + if (this.env.channel !== 'insiders') { + return; + } + try { + this.disposables.push( + this.vscNotebook.registerNotebookContentProvider(JupyterNotebookView, this.notebookContentProvider, { + transientOutputs: false, + transientMetadata: { + breakpointMargin: true, + editable: true, + hasExecutionOrder: true, + inputCollapsed: true, + lastRunDuration: true, + outputCollapsed: true, + runStartTime: true, + runnable: true, + executionOrder: false, + custom: false, + runState: false, + statusMessage: false + } + }) + ); + this.disposables.push( + this.vscNotebook.registerNotebookKernelProvider( + { filenamePattern: '**/*.ipynb', viewType: JupyterNotebookView }, + this.kernelProvider + ) + ); + } catch (ex) { + // If something goes wrong, and we're not in Insiders & not using the NativeEditor experiment, then swallow errors. + traceError('Failed to register VS Code Notebook API', ex); + if (this.experiment.inExperiment(NotebookEditorSupport.nativeNotebookExperiment)) { + throw ex; + } + } + } + private async enableNotebooks() { + if (this.env.channel === 'stable') { + this.shell.showErrorMessage(DataScience.previewNotebookOnlySupportedInVSCInsiders()).then(noop, noop); + return; + } + + await this.enableDisableEditorAssociation(true); + } + private async enableDisableEditorAssociation(enable: boolean) { + // This code is temporary. + const settings = this.workspace.getConfiguration('workbench', undefined); + const editorAssociations = settings.get('editorAssociations') as { + viewType: string; + filenamePattern: string; + }[]; + + // Update the settings. + if ( + enable && + (!Array.isArray(editorAssociations) || + editorAssociations.length === 0 || + !editorAssociations.find((item) => isJupyterNotebook(item.viewType))) + ) { + editorAssociations.push({ + viewType: 'jupyter-notebook', + filenamePattern: '*.ipynb' + }); + await Promise.all([ + this.extensionContext.globalState.update(EditorAssociationUpdatedKey, true), + settings.update('editorAssociations', editorAssociations, ConfigurationTarget.Global) + ]); + } + + // Revert the settings. + if ( + !enable && + this.extensionContext.globalState.get(EditorAssociationUpdatedKey, false) && + Array.isArray(editorAssociations) && + editorAssociations.find((item) => isJupyterNotebook(item.viewType)) + ) { + const updatedSettings = editorAssociations.filter((item) => !isJupyterNotebook(item.viewType)); + await Promise.all([ + this.extensionContext.globalState.update(EditorAssociationUpdatedKey, false), + settings.update('editorAssociations', updatedSettings, ConfigurationTarget.Global) + ]); + } + } + private async disableNotebooks() { + if (this.env.channel === 'stable') { + return; + } + // If we never modified the settings, then nothing to do. + if (!this.extensionContext.globalState.get(EditorAssociationUpdatedKey, false)) { + return; + } + await this.enableDisableEditorAssociation(false); + } +} diff --git a/src/client/datascience/notebook/interpreterStatusBarVisbility.ts b/src/client/datascience/notebook/interpreterStatusBarVisbility.ts new file mode 100644 index 000000000000..44b5695e7ddc --- /dev/null +++ b/src/client/datascience/notebook/interpreterStatusBarVisbility.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter } from 'vscode'; +import { IVSCodeNotebook } from '../../common/application/types'; +import { IDisposableRegistry } from '../../common/types'; +import { IInterpreterStatusbarVisibilityFilter } from '../../interpreter/contracts'; +import { isJupyterNotebook } from './helpers/helpers'; + +@injectable() +export class InterpreterStatusBarVisibility implements IInterpreterStatusbarVisibilityFilter { + private _changed = new EventEmitter(); + + constructor( + @inject(IVSCodeNotebook) private readonly vscNotebook: IVSCodeNotebook, + @inject(IDisposableRegistry) disposables: IDisposableRegistry + ) { + vscNotebook.onDidChangeActiveNotebookEditor( + () => { + this._changed.fire(); + }, + this, + disposables + ); + } + public get changed(): Event { + return this._changed.event; + } + public get hidden() { + return this.vscNotebook.activeNotebookEditor && + isJupyterNotebook(this.vscNotebook.activeNotebookEditor.document) + ? true + : false; + } +} diff --git a/src/client/datascience/notebook/kernelProvider.ts b/src/client/datascience/notebook/kernelProvider.ts new file mode 100644 index 000000000000..4faa3bf746db --- /dev/null +++ b/src/client/datascience/notebook/kernelProvider.ts @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +// tslint:disable-next-line: no-require-imports +import cloneDeep = require('lodash/cloneDeep'); +import { CancellationToken, Event, EventEmitter, Uri } from 'vscode'; +import { + NotebookCell, + NotebookDocument, + NotebookKernel as VSCNotebookKernel, + NotebookKernelProvider +} from '../../../../types/vscode-proposed'; +import { IVSCodeNotebook } from '../../common/application/types'; +import { IDisposableRegistry } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { areKernelConnectionsEqual } from '../jupyter/kernels/helpers'; +import { KernelSelectionProvider } from '../jupyter/kernels/kernelSelections'; +import { KernelSelector } from '../jupyter/kernels/kernelSelector'; +import { KernelSwitcher } from '../jupyter/kernels/kernelSwitcher'; +import { getKernelConnectionId, IKernelProvider, KernelConnectionMetadata } from '../jupyter/kernels/types'; +import { INotebookStorageProvider } from '../notebookStorage/notebookStorageProvider'; +import { INotebook, INotebookProvider } from '../types'; +import { getNotebookMetadata, isJupyterNotebook, updateKernelInNotebookMetadata } from './helpers/helpers'; + +class VSCodeNotebookKernelMetadata implements VSCNotebookKernel { + get preloads(): Uri[] { + return []; + } + get id() { + return getKernelConnectionId(this.selection); + } + constructor( + public readonly label: string, + public readonly description: string, + public readonly selection: Readonly, + public readonly isPreferred: boolean, + private readonly kernelProvider: IKernelProvider + ) {} + public executeCell(_: NotebookDocument, cell: NotebookCell) { + this.kernelProvider.getOrCreate(cell.notebook.uri, { metadata: this.selection })?.executeCell(cell); // NOSONAR + } + public executeAllCells(document: NotebookDocument) { + this.kernelProvider.getOrCreate(document.uri, { metadata: this.selection })?.executeAllCells(document); // NOSONAR + } + public cancelCellExecution(_: NotebookDocument, cell: NotebookCell) { + this.kernelProvider.get(cell.notebook.uri)?.interrupt(); // NOSONAR + } + public cancelAllCellsExecution(document: NotebookDocument) { + this.kernelProvider.get(document.uri)?.interrupt(); // NOSONAR + } +} + +@injectable() +export class VSCodeKernelPickerProvider implements NotebookKernelProvider { + public get onDidChangeKernels(): Event { + return this._onDidChangeKernels.event; + } + private readonly _onDidChangeKernels = new EventEmitter(); + private notebookKernelChangeHandled = new WeakSet(); + constructor( + @inject(KernelSelectionProvider) private readonly kernelSelectionProvider: KernelSelectionProvider, + @inject(KernelSelector) private readonly kernelSelector: KernelSelector, + @inject(IKernelProvider) private readonly kernelProvider: IKernelProvider, + @inject(IVSCodeNotebook) private readonly notebook: IVSCodeNotebook, + @inject(INotebookStorageProvider) private readonly storageProvider: INotebookStorageProvider, + @inject(INotebookProvider) private readonly notebookProvider: INotebookProvider, + @inject(KernelSwitcher) private readonly kernelSwitcher: KernelSwitcher, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService + ) { + this.kernelSelectionProvider.onDidChangeSelections( + (e) => { + if (e) { + const doc = this.notebook.notebookDocuments.find((d) => d.uri.fsPath === e.fsPath); + if (doc) { + return this._onDidChangeKernels.fire(doc); + } + } + this._onDidChangeKernels.fire(undefined); + }, + this, + disposables + ); + this.notebook.onDidChangeActiveNotebookKernel(this.onDidChangeActiveNotebookKernel, this, disposables); + } + public async provideKernels( + document: NotebookDocument, + token: CancellationToken + ): Promise { + const [preferredKernel, kernels, activeInterpreter] = await Promise.all([ + this.getPreferredKernel(document, token), + this.kernelSelectionProvider.getKernelSelectionsForLocalSession(document.uri, 'raw', undefined, token), + this.interpreterService.getActiveInterpreter(document.uri) + ]); + if (token.isCancellationRequested) { + return []; + } + + // Default the interpreter to the local interpreter (if none is provided). + const withInterpreter = kernels.map((kernel) => { + const selection = cloneDeep(kernel.selection); // Always clone, so we can make changes to this. + selection.interpreter = selection.interpreter || activeInterpreter; + return { ...kernel, selection }; + }); + + // Turn this into our preferred list. + const mapped = withInterpreter.map((kernel) => { + return new VSCodeNotebookKernelMetadata( + kernel.label, + kernel.description || kernel.detail || '', + kernel.selection, + areKernelConnectionsEqual(kernel.selection, preferredKernel), + this.kernelProvider + ); + }); + + // If no preferred kernel set but we have a language, use that to set preferred instead. + if (!mapped.find((v) => v.isPreferred) && document.cells.length) { + const languages = document.cells.map((c) => c.language); + // Find the first that matches on language + const indexOfKernelMatchingDocumentLanguage = kernels.findIndex((k) => + languages.find((l) => l === k.selection.kernelSpec?.language) + ); + + // If we have a preferred kernel, then add that to the list, & put it on top of the list. + const preferredKernelMetadata = this.createNotebookKernelMetadataFromPreferredKernel(preferredKernel); + if (preferredKernelMetadata) { + mapped.splice(0, 0, preferredKernelMetadata); + } else if (indexOfKernelMatchingDocumentLanguage >= 0) { + const kernel = kernels[indexOfKernelMatchingDocumentLanguage]; + mapped.splice( + indexOfKernelMatchingDocumentLanguage, + 1, + new VSCodeNotebookKernelMetadata( + kernel.label, + kernel.description || kernel.detail || '', + kernel.selection, + true, + this.kernelProvider + ) + ); + } + } + mapped.sort((a, b) => { + if (a.label > b.label) { + return 1; + } else if (a.label === b.label) { + return 0; + } else { + return -1; + } + }); + return mapped; + } + private createNotebookKernelMetadataFromPreferredKernel( + preferredKernel?: KernelConnectionMetadata + ): VSCodeNotebookKernelMetadata | undefined { + if (!preferredKernel) { + return; + } else if (preferredKernel.kind === 'startUsingDefaultKernel') { + return; + } else if (preferredKernel.kind === 'startUsingPythonInterpreter') { + return new VSCodeNotebookKernelMetadata( + preferredKernel.interpreter?.displayName || preferredKernel.interpreter.path, + preferredKernel.interpreter.path, + preferredKernel, + true, + this.kernelProvider + ); + } else if (preferredKernel.kind === 'connectToLiveKernel') { + return new VSCodeNotebookKernelMetadata( + preferredKernel.kernelModel?.display_name || preferredKernel.kernelModel?.name, + preferredKernel.kernelModel?.name, + preferredKernel, + true, + this.kernelProvider + ); + } else { + return new VSCodeNotebookKernelMetadata( + preferredKernel.kernelSpec.display_name, + preferredKernel.kernelSpec.name, + preferredKernel, + true, + this.kernelProvider + ); + } + } + private async getPreferredKernel(document: NotebookDocument, token: CancellationToken) { + // If we already have a kernel selected, then return that. + const editor = + this.notebook.notebookEditors.find((e) => e.document === document) || + (this.notebook.activeNotebookEditor?.document === document + ? this.notebook.activeNotebookEditor + : undefined); + if (editor && editor.kernel && editor.kernel instanceof VSCodeNotebookKernelMetadata) { + return editor.kernel.selection; + } + return this.kernelSelector.getPreferredKernelForLocalConnection( + document.uri, + 'raw', + undefined, + getNotebookMetadata(document), + true, + token, + true + ); + } + private async onDidChangeActiveNotebookKernel({ + document, + kernel + }: { + document: NotebookDocument; + kernel: VSCNotebookKernel | undefined; + }) { + // We're only interested in our Jupyter Notebooks & our kernels. + if (!kernel || !(kernel instanceof VSCodeNotebookKernelMetadata) || !isJupyterNotebook(document)) { + return; + } + const selectedKernelConnectionMetadata = kernel.selection; + + const model = this.storageProvider.get(document.uri); + if (!model || !model.isTrusted) { + // tslint:disable-next-line: no-suspicious-comment + // TODO: https://github.com/microsoft/vscode-python/issues/13476 + // If a model is not trusted, we cannot change the kernel (this results in changes to notebook metadata). + // This is because we store selected kernel in the notebook metadata. + return; + } + + const existingKernel = this.kernelProvider.get(document.uri); + if (existingKernel && areKernelConnectionsEqual(existingKernel.metadata, selectedKernelConnectionMetadata)) { + return; + } + + // Make this the new kernel (calling this method will associate the new kernel with this Uri). + // Calling `getOrCreate` will ensure a kernel is created and it is mapped to the Uri provided. + // This way other parts of extension have access to this kernel immediately after event is handled. + this.kernelProvider.getOrCreate(document.uri, { + metadata: selectedKernelConnectionMetadata + }); + + // Change kernel and update metadata. + const notebook = await this.notebookProvider.getOrCreateNotebook({ + resource: document.uri, + identity: document.uri, + getOnly: true + }); + + // If we have a notebook, change its kernel now + if (notebook) { + if (!this.notebookKernelChangeHandled.has(notebook)) { + this.notebookKernelChangeHandled.add(notebook); + notebook.onKernelChanged( + (e) => { + if (notebook.disposed) { + return; + } + updateKernelInNotebookMetadata(document, e); + }, + this, + this.disposables + ); + } + // tslint:disable-next-line: no-suspicious-comment + // TODO: https://github.com/microsoft/vscode-python/issues/13514 + // We need to handle these exceptions in `siwthKernelWithRetry`. + // We shouldn't handle them here, as we're already handling some errors in the `siwthKernelWithRetry` method. + // Adding comment here, so we have context for the requirement. + this.kernelSwitcher.switchKernelWithRetry(notebook, selectedKernelConnectionMetadata).catch(noop); + } else { + updateKernelInNotebookMetadata(document, selectedKernelConnectionMetadata); + } + } +} diff --git a/src/client/datascience/notebook/notebookDisposeService.ts b/src/client/datascience/notebook/notebookDisposeService.ts new file mode 100644 index 000000000000..a699bd75b663 --- /dev/null +++ b/src/client/datascience/notebook/notebookDisposeService.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { NotebookDocument } from '../../../../typings/vscode-proposed'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IApplicationEnvironment, IVSCodeNotebook } from '../../common/application/types'; +import { IDisposableRegistry } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { IKernelProvider } from '../jupyter/kernels/types'; +import { INotebookProvider } from '../types'; + +@injectable() +export class NotebookDisposeService implements IExtensionSingleActivationService { + constructor( + @inject(IApplicationEnvironment) private readonly env: IApplicationEnvironment, + @inject(IVSCodeNotebook) private readonly vscNotebook: IVSCodeNotebook, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(INotebookProvider) private readonly notebookProvider: INotebookProvider, + @inject(IKernelProvider) private readonly kernelProvider: IKernelProvider + ) {} + public async activate(): Promise { + if (this.env.channel !== 'insiders') { + return; + } + + this.vscNotebook.onDidCloseNotebookDocument(this.onDidCloseNotebookDocument, this, this.disposables); + } + private onDidCloseNotebookDocument(document: NotebookDocument) { + const kernel = this.kernelProvider.get(document.uri); + if (kernel) { + kernel.dispose().catch(noop); + } + this.notebookProvider.disposeAssociatedNotebook({ identity: document.uri }); + } +} diff --git a/src/client/datascience/notebook/notebookEditor.ts b/src/client/datascience/notebook/notebookEditor.ts new file mode 100644 index 000000000000..db4e7c39d605 --- /dev/null +++ b/src/client/datascience/notebook/notebookEditor.ts @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ConfigurationTarget, Event, EventEmitter, Uri, WebviewPanel } from 'vscode'; +import type { NotebookDocument } from 'vscode-proposed'; +import { IApplicationShell, ICommandManager, IVSCodeNotebook } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { DataScience } from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { JupyterKernelPromiseFailedError } from '../jupyter/kernels/jupyterKernelPromiseFailedError'; +import { IKernel, IKernelProvider } from '../jupyter/kernels/types'; +import { + INotebook, + INotebookEditor, + INotebookModel, + INotebookProvider, + InterruptResult, + IStatusProvider +} from '../types'; +import { getDefaultCodeLanguage } from './helpers/helpers'; +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); + +export class NotebookEditor implements INotebookEditor { + public readonly type = 'native'; + public get onDidChangeViewState(): Event { + return this.changedViewState.event; + } + public get closed(): Event { + return this._closed.event; + } + public get modified(): Event { + return this._modified.event; + } + + public get executed(): Event { + return this._executed.event; + } + public get saved(): Event { + return this._saved.event; + } + public get isUntitled(): boolean { + return this.model.isUntitled; + } + public get isDirty(): boolean { + return this.document.isDirty; + } + public get file(): Uri { + return this.model.file; + } + public get visible(): boolean { + return !this.model.isDisposed; + } + public get active(): boolean { + return this.vscodeNotebook.activeNotebookEditor?.document.uri.toString() === this.model.file.toString(); + } + public get onExecutedCode(): Event { + return this.executedCode.event; + } + public notebook?: INotebook | undefined; + + private changedViewState = new EventEmitter(); + private _closed = new EventEmitter(); + private _saved = new EventEmitter(); + private _executed = new EventEmitter(); + private _modified = new EventEmitter(); + private executedCode = new EventEmitter(); + private restartingKernel?: boolean; + constructor( + public readonly model: INotebookModel, + public readonly document: NotebookDocument, + private readonly vscodeNotebook: IVSCodeNotebook, + private readonly commandManager: ICommandManager, + private readonly notebookProvider: INotebookProvider, + private readonly kernelProvider: IKernelProvider, + private readonly statusProvider: IStatusProvider, + private readonly applicationShell: IApplicationShell, + private readonly configurationService: IConfigurationService, + disposables: IDisposableRegistry + ) { + disposables.push(model.onDidEdit(() => this._modified.fire(this))); + disposables.push( + model.changed((e) => { + if (e.kind === 'save') { + this._saved.fire(this); + } + }) + ); + disposables.push(model.onDidDispose(this._closed.fire.bind(this._closed, this))); + } + public async load(_storage: INotebookModel, _webViewPanel?: WebviewPanel): Promise { + // Not used. + } + public runAllCells(): void { + this.commandManager.executeCommand('notebook.execute').then(noop, noop); + } + public runSelectedCell(): void { + this.commandManager.executeCommand('notebook.cell.execute').then(noop, noop); + } + public addCellBelow(): void { + this.commandManager.executeCommand('notebook.cell.insertCodeCellBelow').then(noop, noop); + } + public show(): Promise { + throw new Error('Method not implemented.'); + } + public startProgress(): void { + throw new Error('Method not implemented.'); + } + public stopProgress(): void { + throw new Error('Method not implemented.'); + } + public undoCells(): void { + this.commandManager.executeCommand('notebook.undo').then(noop, noop); + } + public redoCells(): void { + this.commandManager.executeCommand('notebook.redo').then(noop, noop); + } + public async hasCell(id: string): Promise { + return this.model.cells.find((c) => c.id === id) ? true : false; + } + public removeAllCells(): void { + if (!this.vscodeNotebook.activeNotebookEditor) { + return; + } + const defaultLanguage = getDefaultCodeLanguage(this.model); + this.vscodeNotebook.activeNotebookEditor.edit((editor) => { + const totalLength = this.document.cells.length; + editor.insert( + this.document.cells.length, + '', + defaultLanguage, + vscodeNotebookEnums.CellKind.Code, + [], + undefined + ); + for (let i = totalLength - 1; i >= 0; i = i - 1) { + editor.delete(i); + } + }); + } + public expandAllCells(): void { + if (!this.vscodeNotebook.activeNotebookEditor) { + return; + } + this.vscodeNotebook.activeNotebookEditor.document.cells.forEach((cell) => { + cell.metadata.inputCollapsed = false; + cell.metadata.outputCollapsed = false; + }); + } + public collapseAllCells(): void { + if (!this.vscodeNotebook.activeNotebookEditor) { + return; + } + this.vscodeNotebook.activeNotebookEditor.document.cells.forEach((cell) => { + cell.metadata.inputCollapsed = true; + cell.metadata.outputCollapsed = true; + }); + } + public notifyExecution(code: string) { + this._executed.fire(this); + this.executedCode.fire(code); + } + public async interruptKernel(): Promise { + if (this.restartingKernel) { + return; + } + const kernel = this.kernelProvider.get(this.file); + if (!kernel || this.restartingKernel) { + return; + } + const status = this.statusProvider.set(DataScience.interruptKernelStatus(), true, undefined, undefined); + + try { + const result = await kernel.interrupt(); + status.dispose(); + + // We timed out, ask the user if they want to restart instead. + if (result === InterruptResult.TimedOut) { + const message = DataScience.restartKernelAfterInterruptMessage(); + const yes = DataScience.restartKernelMessageYes(); + const no = DataScience.restartKernelMessageNo(); + const v = await this.applicationShell.showInformationMessage(message, yes, no); + if (v === yes) { + this.restartingKernel = false; + await this.restartKernel(); + } + } + } catch (err) { + status.dispose(); + traceError(err); + this.applicationShell.showErrorMessage(err); + } + } + + public async restartKernel(): Promise { + sendTelemetryEvent(Telemetry.RestartKernelCommand); + if (this.restartingKernel) { + return; + } + const kernel = this.kernelProvider.get(this.file); + + if (kernel && !this.restartingKernel) { + if (await this.shouldAskForRestart()) { + // Ask the user if they want us to restart or not. + const message = DataScience.restartKernelMessage(); + const yes = DataScience.restartKernelMessageYes(); + const dontAskAgain = DataScience.restartKernelMessageDontAskAgain(); + const no = DataScience.restartKernelMessageNo(); + + const response = await this.applicationShell.showInformationMessage(message, yes, dontAskAgain, no); + if (response === dontAskAgain) { + await this.disableAskForRestart(); + await this.restartKernelInternal(kernel); + } else if (response === yes) { + await this.restartKernelInternal(kernel); + } + } else { + await this.restartKernelInternal(kernel); + } + } + } + public dispose() { + this._closed.fire(this); + } + private async restartKernelInternal(kernel: IKernel): Promise { + this.restartingKernel = true; + + // Set our status + const status = this.statusProvider.set(DataScience.restartingKernelStatus(), true, undefined, undefined); + + try { + await kernel.restart(); + } catch (exc) { + // If we get a kernel promise failure, then restarting timed out. Just shutdown and restart the entire server. + // Note, this code might not be necessary, as such an error is thrown only when interrupting a kernel times out. + if (exc instanceof JupyterKernelPromiseFailedError && kernel) { + // Old approach (INotebook is not exposed in IKernel, and INotebook will eventually go away). + const notebook = await this.notebookProvider.getOrCreateNotebook({ + resource: this.file, + identity: this.file, + getOnly: true + }); + if (notebook) { + await notebook.dispose(); + } + await this.notebookProvider.connect({ getOnly: false, disableUI: false }); + } else { + // Show the error message + this.applicationShell.showErrorMessage(exc); + traceError(exc); + } + } finally { + status.dispose(); + this.restartingKernel = false; + } + } + private async shouldAskForRestart(): Promise { + const settings = this.configurationService.getSettings(this.file); + return settings && settings.datascience && settings.datascience.askForKernelRestart === true; + } + + private async disableAskForRestart(): Promise { + const settings = this.configurationService.getSettings(this.file); + if (settings && settings.datascience) { + settings.datascience.askForKernelRestart = false; + this.configurationService + .updateSetting('dataScience.askForKernelRestart', false, undefined, ConfigurationTarget.Global) + .ignoreErrors(); + } + } +} diff --git a/src/client/datascience/notebook/notebookEditorCompatibilitySupport.ts b/src/client/datascience/notebook/notebookEditorCompatibilitySupport.ts new file mode 100644 index 000000000000..4ac70ab6823d --- /dev/null +++ b/src/client/datascience/notebook/notebookEditorCompatibilitySupport.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IApplicationShell } from '../../common/application/types'; +import { UseVSCodeNotebookEditorApi } from '../../common/constants'; + +import { DataScience } from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { IServiceContainer } from '../../ioc/types'; +import { OurNotebookProvider, VSCodeNotebookProvider } from '../constants'; +import { IDataScienceFileSystem, INotebookEditorProvider } from '../types'; + +@injectable() +export class NotebookEditorCompatibilitySupport implements IExtensionSingleActivationService { + private ourCustomNotebookEditorProvider?: INotebookEditorProvider; + private vscodeNotebookEditorProvider!: INotebookEditorProvider; + private initialized?: boolean; + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(UseVSCodeNotebookEditorApi) private readonly useVSCodeNotebookEditorApi: boolean, + + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem, + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer + ) {} + public async activate(): Promise { + this.initialize(); + } + public canOpenWithVSCodeNotebookEditor(uri: Uri) { + this.initialize(); + if (!this.ourCustomNotebookEditorProvider) { + return true; + } + // If user has a normal notebook opened for the same document, let them know things can go wonky. + if ( + this.ourCustomNotebookEditorProvider.editors.some((editor) => + this.fs.areLocalPathsSame(editor.file.fsPath, uri.fsPath) + ) + ) { + this.showWarning(false); + return false; + } + return true; + } + public canOpenWithOurNotebookEditor(uri: Uri, throwException = false) { + this.initialize(); + // If user has a VS Code notebook opened for the same document, let them know things can go wonky. + if ( + this.vscodeNotebookEditorProvider.editors.some((editor) => + this.fs.areLocalPathsSame(editor.file.fsPath, uri.fsPath) + ) + ) { + this.showWarning(throwException); + return false; + } + return true; + } + private initialize() { + if (this.initialized) { + return; + } + this.initialized = true; + if (!this.useVSCodeNotebookEditorApi) { + this.ourCustomNotebookEditorProvider = this.serviceContainer.get( + OurNotebookProvider + ); + } + this.vscodeNotebookEditorProvider = this.serviceContainer.get(VSCodeNotebookProvider); + + if (this.ourCustomNotebookEditorProvider) { + this.ourCustomNotebookEditorProvider.onDidOpenNotebookEditor((e) => + this.canOpenWithOurNotebookEditor(e.file) + ); + } + this.vscodeNotebookEditorProvider.onDidOpenNotebookEditor((e) => this.canOpenWithVSCodeNotebookEditor(e.file)); + } + private showWarning(throwException: boolean) { + if (throwException) { + throw new Error(DataScience.usingPreviewNotebookWithOtherNotebookWarning()); + } + this.appShell.showErrorMessage(DataScience.usingPreviewNotebookWithOtherNotebookWarning()).then(noop, noop); + } +} diff --git a/src/client/datascience/notebook/notebookEditorProvider.ts b/src/client/datascience/notebook/notebookEditorProvider.ts new file mode 100644 index 000000000000..3001b87cf8e1 --- /dev/null +++ b/src/client/datascience/notebook/notebookEditorProvider.ts @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter, Uri } from 'vscode'; +import type { NotebookDocument, NotebookEditor as VSCodeNotebookEditor } from 'vscode-proposed'; +import { IApplicationShell, ICommandManager, IVSCodeNotebook } from '../../common/application/types'; +import '../../common/extensions'; + +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { IServiceContainer } from '../../ioc/types'; +import { captureTelemetry, setSharedProperty } from '../../telemetry'; +import { Commands, Telemetry } from '../constants'; +import { IKernelProvider } from '../jupyter/kernels/types'; +import { INotebookStorageProvider } from '../notebookStorage/notebookStorageProvider'; +import { VSCodeNotebookModel } from '../notebookStorage/vscNotebookModel'; +import { + IDataScienceFileSystem, + INotebookEditor, + INotebookEditorProvider, + INotebookProvider, + IStatusProvider +} from '../types'; +import { JupyterNotebookView } from './constants'; +import { isJupyterNotebook } from './helpers/helpers'; +import { NotebookEditor } from './notebookEditor'; + +/** + * Notebook Editor provider used by other parts of DS code. + * This is an adapter, that takes the VSCode api for editors (did notebook editors open, close save, etc) and + * then exposes them in a manner we expect - i.e. INotebookEditorProvider. + * This is also responsible for tracking all notebooks that open and then keeping the VS Code notebook models updated with changes we made to our underlying model. + * E.g. when cells are executed the results in our model is updated, this tracks those changes and syncs VSC cells with those updates. + */ +@injectable() +export class NotebookEditorProvider implements INotebookEditorProvider { + public get onDidChangeActiveNotebookEditor(): Event { + return this._onDidChangeActiveNotebookEditor.event; + } + public get onDidCloseNotebookEditor(): Event { + return this._onDidCloseNotebookEditor.event; + } + public get onDidOpenNotebookEditor(): Event { + return this._onDidOpenNotebookEditor.event; + } + public get activeEditor(): INotebookEditor | undefined { + return this.editors.find((e) => e.visible && e.active); + } + public get editors(): INotebookEditor[] { + return [...this.openedEditors]; + } + protected readonly _onDidChangeActiveNotebookEditor = new EventEmitter(); + protected readonly _onDidOpenNotebookEditor = new EventEmitter(); + private readonly _onDidCloseNotebookEditor = new EventEmitter(); + private readonly openedEditors = new Set(); + private readonly trackedVSCodeNotebookEditors = new Set(); + private readonly notebookEditorsByUri = new Map(); + private readonly notebooksWaitingToBeOpenedByUri = new Map>(); + constructor( + @inject(IVSCodeNotebook) private readonly vscodeNotebook: IVSCodeNotebook, + @inject(INotebookStorageProvider) private readonly storage: INotebookStorageProvider, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IStatusProvider) private readonly statusProvider: IStatusProvider, + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem + ) { + this.disposables.push(this.vscodeNotebook.onDidOpenNotebookDocument(this.onDidOpenNotebookDocument, this)); + this.disposables.push(this.vscodeNotebook.onDidCloseNotebookDocument(this.onDidCloseNotebookDocument, this)); + this.disposables.push( + this.vscodeNotebook.onDidChangeActiveNotebookEditor(this.onDidChangeActiveVsCodeNotebookEditor, this) + ); + this.disposables.push( + this.commandManager.registerCommand(Commands.OpenNotebookInPreviewEditor, async (uri?: Uri) => { + if (uri) { + setSharedProperty('ds_notebookeditor', 'native'); + captureTelemetry(Telemetry.OpenNotebook, { scope: 'command' }, false); + this.open(uri).ignoreErrors(); + } + }) + ); + } + + public async open(file: Uri): Promise { + setSharedProperty('ds_notebookeditor', 'native'); + if (this.notebooksWaitingToBeOpenedByUri.get(file.toString())) { + return this.notebooksWaitingToBeOpenedByUri.get(file.toString())!.promise; + } + + // Wait for editor to get opened up, vscode will notify when it is opened. + // Further below. + this.notebooksWaitingToBeOpenedByUri.set(file.toString(), createDeferred()); + const deferred = this.notebooksWaitingToBeOpenedByUri.get(file.toString())!; + + // Tell VSC to open the notebook, at which point it will fire a callback when a notebook document has been opened. + // Then our promise will get resolved. + await this.commandManager.executeCommand('vscode.openWith', file, JupyterNotebookView); + + // This gets resolved when we have handled the opening of the notebook. + return deferred.promise; + } + public async show(_file: Uri): Promise { + // We do not need this. + return; + } + @captureTelemetry(Telemetry.CreateNewNotebook, undefined, false) + public async createNew(contents?: string): Promise { + setSharedProperty('ds_notebookeditor', 'native'); + const model = await this.storage.createNew(contents, true); + return this.open(model.file); + } + private onEditorOpened(editor: INotebookEditor): void { + this.openedEditors.add(editor); + editor.closed(this.closedEditor, this, this.disposables); + this._onDidOpenNotebookEditor.fire(editor); + this._onDidChangeActiveNotebookEditor.fire(editor); + } + + private closedEditor(editor: INotebookEditor): void { + if (this.openedEditors.has(editor)) { + this.openedEditors.delete(editor); + this._onDidCloseNotebookEditor.fire(editor); + + // Find all notebooks associated with this editor (ipynb file). + const otherEditors = this.editors.filter( + (e) => this.fs.areLocalPathsSame(e.file.fsPath, editor.file.fsPath) && e !== editor + ); + + // If we have no editors for this file, then dispose the notebook. + if (otherEditors.length === 0) { + editor.notebook?.dispose(); + } + } + } + + private async onDidOpenNotebookDocument(doc: NotebookDocument): Promise { + if (!isJupyterNotebook(doc)) { + return; + } + const uri = doc.uri; + const model = await this.storage.getOrCreateModel(uri, undefined, undefined, true); + if (model instanceof VSCodeNotebookModel) { + model.associateNotebookDocument(doc); + } + // In open method we might be waiting. + let editor = this.notebookEditorsByUri.get(uri.toString()); + if (!editor) { + const notebookProvider = this.serviceContainer.get(INotebookProvider); + const kernelProvider = this.serviceContainer.get(IKernelProvider); + editor = new NotebookEditor( + model, + doc, + this.vscodeNotebook, + this.commandManager, + notebookProvider, + kernelProvider, + this.statusProvider, + this.appShell, + this.configurationService, + this.disposables + ); + this.onEditorOpened(editor); + } + if (!this.notebooksWaitingToBeOpenedByUri.get(uri.toString())) { + this.notebooksWaitingToBeOpenedByUri.set(uri.toString(), createDeferred()); + } + const deferred = this.notebooksWaitingToBeOpenedByUri.get(uri.toString())!; + deferred.resolve(editor); + this.notebookEditorsByUri.set(uri.toString(), editor); + if (!model.isTrusted) { + await this.commandManager.executeCommand(Commands.TrustNotebook, model.file); + } + } + private onDidChangeActiveVsCodeNotebookEditor(editor: VSCodeNotebookEditor | undefined) { + if (!editor) { + this._onDidChangeActiveNotebookEditor.fire(undefined); + return; + } + if (this.trackedVSCodeNotebookEditors.has(editor)) { + const ourEditor = this.editors.find((item) => item.file.toString() === editor.document.uri.toString()); + this._onDidChangeActiveNotebookEditor.fire(ourEditor); + return; + } + this.trackedVSCodeNotebookEditors.add(editor); + this.disposables.push(editor.onDidDispose(() => this.onDidDisposeVSCodeNotebookEditor(editor))); + } + private async onDidCloseNotebookDocument(document: NotebookDocument) { + this.disposeResourceRelatedToNotebookEditor(document.uri); + } + private disposeResourceRelatedToNotebookEditor(uri: Uri) { + // Ok, dispose all of the resources associated with this document. + // In our case, we only have one editor. + const editor = this.notebookEditorsByUri.get(uri.toString()); + if (editor) { + this.closedEditor(editor); + editor.dispose(); + if (editor.model) { + editor.model.dispose(); + } + } + this.notebookEditorsByUri.delete(uri.toString()); + this.notebooksWaitingToBeOpenedByUri.delete(uri.toString()); + } + /** + * We know a notebook editor has been closed. + * We need to close/dispose all of our resources related to this notebook document. + * However we also need to check if there are other notebooks opened, that are associated with this same notebook. + * I.e. we may have closed a duplicate editor. + */ + private async onDidDisposeVSCodeNotebookEditor(closedEditor: VSCodeNotebookEditor) { + const uri = closedEditor.document.uri; + if ( + this.vscodeNotebook.notebookEditors.some( + (item) => item !== closedEditor && item.document.uri.toString() === uri.toString() + ) + ) { + return; + } + this.disposeResourceRelatedToNotebookEditor(closedEditor.document.uri); + } +} diff --git a/src/client/datascience/notebook/notebookEditorProviderWrapper.ts b/src/client/datascience/notebook/notebookEditorProviderWrapper.ts new file mode 100644 index 000000000000..6889d7305327 --- /dev/null +++ b/src/client/datascience/notebook/notebookEditorProviderWrapper.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { UseVSCodeNotebookEditorApi } from '../../common/constants'; +import '../../common/extensions'; +import { IDisposableRegistry } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { OurNotebookProvider, VSCodeNotebookProvider } from '../constants'; +import { INotebookEditor, INotebookEditorProvider } from '../types'; +import { NotebookEditorCompatibilitySupport } from './notebookEditorCompatibilitySupport'; + +/** + * Notebook Editor provider used by other parts of DS code. + * This is an adapter, that takes the VSCode api for editors (did notebook editors open, close save, etc) and + * then exposes them in a manner we expect - i.e. INotebookEditorProvider. + * This is also responsible for tracking all notebooks that open and then keeping the VS Code notebook models updated with changes we made to our underlying model. + * E.g. when cells are executed the results in our model is updated, this tracks those changes and syncs VSC cells with those updates. + */ +@injectable() +export class NotebookEditorProviderWrapper implements INotebookEditorProvider { + public get onDidChangeActiveNotebookEditor(): Event { + if (this.useVSCodeNotebookEditorApi) { + return this.vscodeNotebookEditorProvider.onDidChangeActiveNotebookEditor; + } + return this._onDidChangeActiveNotebookEditor.event; + } + public get onDidCloseNotebookEditor(): Event { + if (this.useVSCodeNotebookEditorApi) { + return this.vscodeNotebookEditorProvider.onDidCloseNotebookEditor; + } + return this._onDidCloseNotebookEditor.event; + } + public get onDidOpenNotebookEditor(): Event { + if (this.useVSCodeNotebookEditorApi) { + return this.vscodeNotebookEditorProvider.onDidOpenNotebookEditor; + } + return this._onDidOpenNotebookEditor.event; + } + public get activeEditor(): INotebookEditor | undefined { + if (this.useVSCodeNotebookEditorApi) { + return this.vscodeNotebookEditorProvider.activeEditor; + } + return ( + this.vscodeNotebookEditorProvider.activeEditor || this.ourCustomOrOldNotebookEditorProvider?.activeEditor + ); + } + public get editors(): INotebookEditor[] { + if (this.useVSCodeNotebookEditorApi) { + return this.vscodeNotebookEditorProvider.editors; + } + // If a VS Code notebook is opened, then user vscode notebooks provider. + if (this.vscodeNotebookEditorProvider.activeEditor) { + return this.vscodeNotebookEditorProvider.editors; + } + const provider = this.ourCustomOrOldNotebookEditorProvider || this.vscodeNotebookEditorProvider; + return provider.editors; + } + protected readonly _onDidChangeActiveNotebookEditor = new EventEmitter(); + protected readonly _onDidOpenNotebookEditor = new EventEmitter(); + private readonly _onDidCloseNotebookEditor = new EventEmitter(); + private readonly ourCustomOrOldNotebookEditorProvider?: INotebookEditorProvider; + private hasNotebookOpenedUsingVSCodeNotebook?: boolean; + constructor( + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(UseVSCodeNotebookEditorApi) private readonly useVSCodeNotebookEditorApi: boolean, + @inject(VSCodeNotebookProvider) private readonly vscodeNotebookEditorProvider: INotebookEditorProvider, + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(NotebookEditorCompatibilitySupport) + private readonly compatibilitySupport: NotebookEditorCompatibilitySupport + ) { + // If user doesn't belong to Notebooks experiment, then use old notebook editor API. + if (!this.useVSCodeNotebookEditorApi) { + const ourCustomOrOldNotebookEditorProvider = serviceContainer.get( + OurNotebookProvider + ); + this.ourCustomOrOldNotebookEditorProvider = ourCustomOrOldNotebookEditorProvider; + ourCustomOrOldNotebookEditorProvider.onDidChangeActiveNotebookEditor( + this._onDidChangeActiveNotebookEditor.fire, + this._onDidChangeActiveNotebookEditor, + this.disposables + ); + ourCustomOrOldNotebookEditorProvider.onDidCloseNotebookEditor( + this._onDidCloseNotebookEditor.fire, + this._onDidCloseNotebookEditor, + this.disposables + ); + ourCustomOrOldNotebookEditorProvider.onDidOpenNotebookEditor( + this._onDidOpenNotebookEditor.fire, + this._onDidOpenNotebookEditor, + this.disposables + ); + } + + // Even if user doesn't belong to notebook experiment, they can open a notebook using the new vsc Notebook ui. + this.vscodeNotebookEditorProvider.onDidChangeActiveNotebookEditor( + (e) => { + if (e) { + // Keep track of the fact that we opened something using VS Code notebooks. + this.hasNotebookOpenedUsingVSCodeNotebook = true; + this._onDidChangeActiveNotebookEditor.fire(e); + } else if (this.hasNotebookOpenedUsingVSCodeNotebook) { + // We are only interested in events fired when we had already used a VS Code notebook. + this._onDidChangeActiveNotebookEditor.fire(e); + // Check if we have other VSC Notebooks opened. + if (!this.vscodeNotebookEditorProvider.editors.length) { + this.hasNotebookOpenedUsingVSCodeNotebook = false; + } + } + }, + this, + this.disposables + ); + // This can be done blindly, as th VSCodeNotebook API would trigger these events only if it was explicitly used. + this.vscodeNotebookEditorProvider.onDidCloseNotebookEditor( + this._onDidCloseNotebookEditor.fire, + this._onDidCloseNotebookEditor, + this.disposables + ); + // This can be done blindly, as th VSCodeNotebook API would trigger these events only if it was explicitly used. + this.vscodeNotebookEditorProvider.onDidOpenNotebookEditor( + this._onDidOpenNotebookEditor.fire, + this._onDidOpenNotebookEditor, + this.disposables + ); + } + + public async open(file: Uri): Promise { + if (this.ourCustomOrOldNotebookEditorProvider) { + this.compatibilitySupport.canOpenWithOurNotebookEditor(file, true); + } + + return (this.ourCustomOrOldNotebookEditorProvider || this.vscodeNotebookEditorProvider).open(file); + } + public async show(file: Uri): Promise { + return (this.ourCustomOrOldNotebookEditorProvider || this.vscodeNotebookEditorProvider).show(file); + } + public async createNew(contents?: string): Promise { + return (this.ourCustomOrOldNotebookEditorProvider || this.vscodeNotebookEditorProvider).createNew(contents); + } +} diff --git a/src/client/datascience/notebook/rendererExtension.ts b/src/client/datascience/notebook/rendererExtension.ts new file mode 100644 index 000000000000..0488cb9aa68d --- /dev/null +++ b/src/client/datascience/notebook/rendererExtension.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { NotebookDocument } from '../../../../types/vscode-proposed'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IApplicationEnvironment, IVSCodeNotebook } from '../../common/application/types'; +import { IDisposableRegistry, IExtensions } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { RendererExtensionId } from './constants'; +import { isJupyterNotebook } from './helpers/helpers'; +import { RendererExtensionDownloader } from './rendererExtensionDownloader'; + +@injectable() +export class RendererExtension implements IExtensionSingleActivationService { + constructor( + @inject(IVSCodeNotebook) private readonly notebook: IVSCodeNotebook, + @inject(RendererExtensionDownloader) private readonly downloader: RendererExtensionDownloader, + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IApplicationEnvironment) private readonly env: IApplicationEnvironment, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry + ) {} + public async activate() { + if (this.env.channel === 'stable') { + return; + } + this.notebook.onDidOpenNotebookDocument(this.onDidOpenNotebook, this, this.disposables); + this.notebook.notebookDocuments.forEach((doc) => this.onDidOpenNotebook(doc)); + } + + private onDidOpenNotebook(e: NotebookDocument) { + if (!isJupyterNotebook(e)) { + return; + } + + // Download and install the extension if not already found. + if (!this.extensions.getExtension(RendererExtensionId)) { + this.downloader.downloadAndInstall().catch(noop); + } + } +} diff --git a/src/client/datascience/notebook/rendererExtensionDownloader.ts b/src/client/datascience/notebook/rendererExtensionDownloader.ts new file mode 100644 index 000000000000..d3956a71f7db --- /dev/null +++ b/src/client/datascience/notebook/rendererExtensionDownloader.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, named } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../../common/application/types'; +import { Octicons, STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; +import { vsixFileExtension } from '../../common/installer/extensionBuildInstaller'; + +import { IFileDownloader, IOutputChannel } from '../../common/types'; +import { DataScienceRendererExtension } from '../../common/utils/localize'; +import { traceDecorators } from '../../logging'; +import { IDataScienceFileSystem } from '../types'; +import { RendererExtensionDownloadUri } from './constants'; + +@injectable() +export class RendererExtensionDownloader { + private installed?: boolean; + constructor( + @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly output: IOutputChannel, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(ICommandManager) private readonly cmdManager: ICommandManager, + @inject(IFileDownloader) private readonly fileDownloader: IFileDownloader, + @inject(IDataScienceFileSystem) private readonly fs: IDataScienceFileSystem + ) {} + @traceDecorators.error('Installing Notebook Renderer extension failed') + public async downloadAndInstall(): Promise { + if (this.installed) { + return; + } + this.installed = true; + const vsixFilePath = await this.download(); + try { + this.output.append(DataScienceRendererExtension.installingExtension()); + await this.appShell.withProgressCustomIcon(Octicons.Installing, async (progress) => { + progress.report({ message: DataScienceRendererExtension.installingExtension() }); + return this.cmdManager.executeCommand('workbench.extensions.installExtension', Uri.file(vsixFilePath)); + }); + this.output.appendLine(DataScienceRendererExtension.installationCompleteMessage()); + } finally { + await this.fs.deleteLocalFile(vsixFilePath); + } + } + + @traceDecorators.error('Downloading Notebook Renderer extension failed') + private async download(): Promise { + this.output.appendLine(DataScienceRendererExtension.startingDownloadOutputMessage()); + const downloadOptions = { + extension: vsixFileExtension, + outputChannel: this.output, + progressMessagePrefix: DataScienceRendererExtension.downloadingMessage() + }; + return this.fileDownloader.downloadFile(RendererExtensionDownloadUri, downloadOptions).then((file) => { + this.output.appendLine(DataScienceRendererExtension.downloadCompletedOutputMessage()); + return file; + }); + } +} diff --git a/src/client/datascience/notebook/serviceRegistry.ts b/src/client/datascience/notebook/serviceRegistry.ts new file mode 100644 index 000000000000..97ba21ff8232 --- /dev/null +++ b/src/client/datascience/notebook/serviceRegistry.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IInterpreterStatusbarVisibilityFilter } from '../../interpreter/contracts'; +import { IServiceManager } from '../../ioc/types'; +import { KernelProvider } from '../jupyter/kernels/kernelProvider'; +import { IKernelProvider } from '../jupyter/kernels/types'; +import { NotebookContentProvider } from './contentProvider'; +import { NotebookIntegration } from './integration'; +import { InterpreterStatusBarVisibility } from './interpreterStatusBarVisbility'; +import { VSCodeKernelPickerProvider } from './kernelProvider'; +import { NotebookDisposeService } from './notebookDisposeService'; +import { NotebookSurveyBanner, NotebookSurveyDataLogger } from './survey'; +import { INotebookContentProvider } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(INotebookContentProvider, NotebookContentProvider); + serviceManager.addSingleton( + IExtensionSingleActivationService, + NotebookIntegration + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + NotebookDisposeService + ); + serviceManager.addSingleton(NotebookIntegration, NotebookIntegration); + serviceManager.addSingleton(IKernelProvider, KernelProvider); + serviceManager.addSingleton(NotebookSurveyBanner, NotebookSurveyBanner); + serviceManager.addSingleton(VSCodeKernelPickerProvider, VSCodeKernelPickerProvider); + serviceManager.addSingleton( + IInterpreterStatusbarVisibilityFilter, + InterpreterStatusBarVisibility + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + NotebookSurveyDataLogger + ); +} diff --git a/src/client/datascience/notebook/survey.ts b/src/client/datascience/notebook/survey.ts new file mode 100644 index 000000000000..aabac9de00b6 --- /dev/null +++ b/src/client/datascience/notebook/survey.ts @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IApplicationShell, IVSCodeNotebook } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import { IBrowserService, IDisposableRegistry, IPersistentStateFactory } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { MillisecondsInADay } from '../../constants'; +import { INotebookEditorProvider } from '../types'; + +const surveyLink = 'https://aka.ms/pyaivscnbsurvey'; +const storageKey = 'NotebookSurveyUsageData'; + +export type NotebookSurveyUsageData = { + numberOfExecutionsInCurrentSession?: number; + numberOfCellActionsInCurrentSession?: number; + numberOfExecutionsInPreviousSessions?: number; + numberOfCellActionsInPreviousSessions?: number; + surveyDisabled?: boolean; + lastUsedDateTime?: number; +}; + +@injectable() +export class NotebookSurveyBanner { + public get enabled(): boolean { + return !this.persistentState.createGlobalPersistentState(storageKey, {}).value + .surveyDisabled; + } + private disabledInCurrentSession: boolean = false; + constructor( + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, + @inject(IBrowserService) private browserService: IBrowserService + ) {} + + public async showBanner(): Promise { + if (!this.enabled || this.disabledInCurrentSession) { + return; + } + + const show = await this.shouldShowBanner(); + if (!show || this.disabledInCurrentSession) { + return; + } + + this.disabledInCurrentSession = true; + const response = await this.appShell.showInformationMessage( + localize.DataScienceNotebookSurveyBanner.bannerMessage(), + localize.CommonSurvey.yesLabel(), + localize.CommonSurvey.noLabel(), + localize.CommonSurvey.remindMeLaterLabel() + ); + switch (response) { + case localize.CommonSurvey.yesLabel(): { + this.browserService.launch(surveyLink); + await this.disable(); + break; + } + case localize.CommonSurvey.noLabel(): { + await this.disable(); + break; + } + default: { + // Disable for the current session. + this.disabledInCurrentSession = true; + } + } + } + + private async disable(): Promise { + await this.persistentState + .createGlobalPersistentState(storageKey, {}) + .updateValue({ surveyDisabled: true }); + } + + private async shouldShowBanner(): Promise { + if (!this.enabled || this.disabledInCurrentSession) { + return false; + } + const currentDate = new Date(); + if (currentDate.getMonth() < 7 && currentDate.getFullYear() <= 2020) { + return false; + } + + const data = this.persistentState.createGlobalPersistentState(storageKey, {}); + + const totalActionsInPreviousSessions = + (data.value.numberOfCellActionsInPreviousSessions || 0) + + (data.value.numberOfExecutionsInPreviousSessions || 0); + // If user barely tried nb in a previous session, then possible it wasn't a great experience. + if (totalActionsInPreviousSessions > 0 && totalActionsInPreviousSessions < 5) { + return true; + } + + const totalActionsInCurrentSessions = + (data.value.numberOfCellActionsInCurrentSession || 0) + + (data.value.numberOfExecutionsInCurrentSession || 0); + // If more than 100 actions in total then get feedback. + if (totalActionsInPreviousSessions + totalActionsInCurrentSessions > 100) { + return true; + } + + // If more than 5 actions and not used for 5 days since then. + // Geed feedback, possible it wasn't what they expected, as they have stopped using it. + if (totalActionsInPreviousSessions > 5 && data.value.lastUsedDateTime) { + const daysSinceLastUsage = (new Date().getTime() - data.value.lastUsedDateTime) / MillisecondsInADay; + if (daysSinceLastUsage > 5) { + return true; + } + } + + return false; + } +} + +/* +Survey after > 100 actions in notebooks (+remind me later) +Survey after > 5 & not used for 5 days (+remind me later) +Survey after < 5 operations in notebooks & closed it (+remind me later) +*/ + +@injectable() +export class NotebookSurveyDataLogger implements IExtensionSingleActivationService { + constructor( + @inject(IPersistentStateFactory) private readonly persistentState: IPersistentStateFactory, + @inject(IVSCodeNotebook) private readonly vscNotebook: IVSCodeNotebook, + @inject(INotebookEditorProvider) private readonly notebookEditorProvider: INotebookEditorProvider, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + // tslint:disable-next-line: no-use-before-declare + @inject(NotebookSurveyBanner) private readonly survey: NotebookSurveyBanner + ) {} + public async activate() { + if (!this.survey.enabled) { + return; + } + + this.notebookEditorProvider.onDidOpenNotebookEditor( + (e) => { + if (e.type !== 'native') { + return; + } + e.onExecutedCode(() => this.incrementCellExecution(), this, this.disposables); + }, + this, + this.disposables + ); + this.vscNotebook.onDidChangeNotebookDocument( + (e) => { + if (e.type === 'changeCells' || e.type === 'changeCellLanguage') { + this.incrementCellAction().catch(traceError.bind(undefined, 'Failed to update survey data')); + } + }, + this, + this.disposables + ); + + this.migrateDataAndDisplayBanner().catch(traceError.bind(undefined, 'Failed to migrate survey data')); + } + private async migrateDataAndDisplayBanner() { + const data = this.persistentState.createGlobalPersistentState(storageKey, {}); + // The user has loaded a new instance of VSC, and we need to move numbers from previous session into the respective storage props. + if (data.value.numberOfCellActionsInCurrentSession || data.value.numberOfExecutionsInCurrentSession) { + data.value.numberOfCellActionsInPreviousSessions = data.value.numberOfCellActionsInPreviousSessions || 0; + data.value.numberOfCellActionsInPreviousSessions += data.value.numberOfCellActionsInCurrentSession || 0; + data.value.numberOfCellActionsInCurrentSession = 0; // Reset for new session. + + data.value.numberOfExecutionsInPreviousSessions = data.value.numberOfExecutionsInPreviousSessions || 0; + data.value.numberOfExecutionsInPreviousSessions += data.value.numberOfExecutionsInCurrentSession || 0; + data.value.numberOfExecutionsInCurrentSession = 0; // Reset for new session. + + data.value.lastUsedDateTime = new Date().getTime(); + await data.updateValue(data.value); + } + + await this.survey.showBanner(); + } + private async incrementCellAction() { + const data = this.persistentState.createGlobalPersistentState(storageKey, {}); + + data.value.numberOfCellActionsInCurrentSession = (data.value.numberOfCellActionsInCurrentSession || 0) + 1; + data.value.lastUsedDateTime = new Date().getTime(); + await data.updateValue(data.value); + await this.survey.showBanner(); + } + private async incrementCellExecution() { + const data = this.persistentState.createGlobalPersistentState(storageKey, {}); + data.value.numberOfExecutionsInCurrentSession = (data.value.numberOfExecutionsInCurrentSession || 0) + 1; + data.value.lastUsedDateTime = new Date().getTime(); + await data.updateValue(data.value); + await this.survey.showBanner(); + } +} diff --git a/src/client/datascience/notebook/types.ts b/src/client/datascience/notebook/types.ts new file mode 100644 index 000000000000..743b47337304 --- /dev/null +++ b/src/client/datascience/notebook/types.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import type { NotebookContentProvider as VSCodeNotebookContentProvider, NotebookDocument } from 'vscode-proposed'; + +export const INotebookContentProvider = Symbol('INotebookContentProvider'); +export interface INotebookContentProvider extends VSCodeNotebookContentProvider { + /** + * Notify VS Code that document has changed. + * The change is not something that can be undone by using the `undo`. + * E.g. updating execution count of a cell, or making a notebook readonly, or updating kernel info in ipynb metadata. + */ + notifyChangesToDocument(document: NotebookDocument): void; +} diff --git a/src/client/datascience/notebookAndInteractiveTracker.ts b/src/client/datascience/notebookAndInteractiveTracker.ts new file mode 100644 index 000000000000..b888b7312be4 --- /dev/null +++ b/src/client/datascience/notebookAndInteractiveTracker.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Memento } from 'vscode'; +import { IDisposableRegistry, IMemento, WORKSPACE_MEMENTO } from '../common/types'; +import { + IInteractiveWindowProvider, + INotebookAndInteractiveWindowUsageTracker, + INotebookEditorProvider +} from './types'; + +const LastNotebookOpenedTimeKey = 'last-notebook-start-time'; +const LastInteractiveWindowStartTimeKey = 'last-interactive-window-start-time'; + +@injectable() +export class NotebookAndInteractiveWindowUsageTracker implements INotebookAndInteractiveWindowUsageTracker { + public get lastNotebookOpened() { + const time = this.mementoStorage.get(LastNotebookOpenedTimeKey); + return time ? new Date(time) : undefined; + } + public get lastInteractiveWindowOpened() { + const time = this.mementoStorage.get(LastInteractiveWindowStartTimeKey); + return time ? new Date(time) : undefined; + } + constructor( + @inject(IMemento) @named(WORKSPACE_MEMENTO) private mementoStorage: Memento, + @inject(INotebookEditorProvider) private readonly notebookEditorProvider: INotebookEditorProvider, + @inject(IInteractiveWindowProvider) private readonly interactiveWindowProvider: IInteractiveWindowProvider, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry + ) {} + public async startTracking(): Promise { + this.disposables.push( + this.notebookEditorProvider.onDidOpenNotebookEditor(() => + this.mementoStorage.update(LastNotebookOpenedTimeKey, Date.now()) + ) + ); + this.disposables.push( + this.interactiveWindowProvider.onDidChangeActiveInteractiveWindow(() => + this.mementoStorage.update(LastInteractiveWindowStartTimeKey, Date.now()) + ) + ); + } +} diff --git a/src/client/datascience/notebookStorage/baseModel.ts b/src/client/datascience/notebookStorage/baseModel.ts new file mode 100644 index 000000000000..2a28e231352f --- /dev/null +++ b/src/client/datascience/notebookStorage/baseModel.ts @@ -0,0 +1,300 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; +import { sha256 } from 'hash.js'; +import { Event, EventEmitter, Memento, Uri } from 'vscode'; +import { ICryptoUtils } from '../../common/types'; +import { isUntitledFile } from '../../common/utils/misc'; +import { pruneCell } from '../common'; +import { NotebookModelChange } from '../interactive-common/interactiveWindowTypes'; +import { + getInterpreterFromKernelConnectionMetadata, + kernelConnectionMetadataHasKernelModel +} from '../jupyter/kernels/helpers'; +import { KernelConnectionMetadata } from '../jupyter/kernels/types'; +import { ICell, INotebookMetadataLive, INotebookModel } from '../types'; + +export const ActiveKernelIdList = `Active_Kernel_Id_List`; +// This is the number of kernel ids that will be remembered between opening and closing VS code +export const MaximumKernelIdListSize = 40; +type KernelIdListEntry = { + fileHash: string; + kernelId: string | undefined; +}; + +// tslint:disable-next-line: cyclomatic-complexity +export function updateNotebookMetadata( + metadata?: nbformat.INotebookMetadata, + kernelConnection?: KernelConnectionMetadata +) { + let changed = false; + let kernelId: string | undefined; + if (!metadata) { + return { changed, kernelId }; + } + + // Get our kernel_info and language_info from the current notebook + const interpreter = getInterpreterFromKernelConnectionMetadata(kernelConnection); + if ( + interpreter && + interpreter.version && + metadata && + metadata.language_info && + metadata.language_info.version !== interpreter.version.raw + ) { + metadata.language_info.version = interpreter.version.raw; + changed = true; + } else if (!interpreter && metadata?.language_info) { + // It's possible, such as with raw kernel and a default kernelspec to not have interpreter info + // for this case clear out old invalid language_info entries as they are related to the previous execution + metadata.language_info = undefined; + changed = true; + } + + const kernelSpecOrModel = + kernelConnection && kernelConnectionMetadataHasKernelModel(kernelConnection) + ? kernelConnection.kernelModel + : kernelConnection?.kernelSpec; + if (kernelSpecOrModel && !metadata.kernelspec) { + // Add a new spec in this case + metadata.kernelspec = { + name: kernelSpecOrModel.name || kernelSpecOrModel.display_name || '', + display_name: kernelSpecOrModel.display_name || kernelSpecOrModel.name || '' + }; + kernelId = kernelSpecOrModel.id; + changed = true; + } else if (kernelSpecOrModel && metadata.kernelspec) { + // Spec exists, just update name and display_name + const name = kernelSpecOrModel.name || kernelSpecOrModel.display_name || ''; + const displayName = kernelSpecOrModel.display_name || kernelSpecOrModel.name || ''; + if ( + metadata.kernelspec.name !== name || + metadata.kernelspec.display_name !== displayName || + kernelId !== kernelSpecOrModel.id + ) { + changed = true; + metadata.kernelspec.name = name; + metadata.kernelspec.display_name = displayName; + kernelId = kernelSpecOrModel.id; + } + } else if (kernelConnection?.kind === 'startUsingPythonInterpreter') { + // Store interpreter name, we expect the kernel finder will find the corresponding interpreter based on this name. + const name = kernelConnection.interpreter.displayName || kernelConnection.interpreter.path; + if (metadata.kernelspec?.name !== name || metadata.kernelspec?.display_name !== name) { + changed = true; + metadata.kernelspec = { + name, + display_name: name, + metadata: { + interpreter: { + hash: sha256().update(kernelConnection.interpreter.path).digest('hex') + } + } + }; + } + } + return { changed, kernelId }; +} + +export function getDefaultNotebookContent(pythonNumber: number = 3): Partial { + // Use this to build our metadata object + // Use these as the defaults unless we have been given some in the options. + const metadata: nbformat.INotebookMetadata = { + language_info: { + codemirror_mode: { + name: 'ipython', + version: pythonNumber + }, + file_extension: '.py', + mimetype: 'text/x-python', + name: 'python', + nbconvert_exporter: 'python', + pygments_lexer: `ipython${pythonNumber}`, + version: pythonNumber + }, + orig_nbformat: 2 + }; + + // Default notebook data. + return { + metadata: metadata, + nbformat: 4, + nbformat_minor: 2 + }; +} +export abstract class BaseNotebookModel implements INotebookModel { + public get onDidDispose() { + return this._disposed.event; + } + public get isDisposed() { + return this._isDisposed === true; + } + public get isDirty(): boolean { + return false; + } + public get changed(): Event { + return this._changedEmitter.event; + } + public get file(): Uri { + return this._file; + } + + public get isUntitled(): boolean { + return isUntitledFile(this.file); + } + public get cells(): ICell[] { + return this._cells; + } + public get onDidEdit(): Event { + return this._editEventEmitter.event; + } + public get metadata(): INotebookMetadataLive | undefined { + return this.kernelId && this.notebookJson.metadata + ? { + ...this.notebookJson.metadata, + id: this.kernelId + } + : // Fix nyc compiler problem + // tslint:disable-next-line: no-any + (this.notebookJson.metadata as any); + } + public get isTrusted() { + return this._isTrusted; + } + + protected _disposed = new EventEmitter(); + protected _isDisposed?: boolean; + protected _changedEmitter = new EventEmitter(); + protected _editEventEmitter = new EventEmitter(); + private kernelId: string | undefined; + constructor( + protected _isTrusted: boolean, + protected _file: Uri, + protected _cells: ICell[], + protected globalMemento: Memento, + private crypto: ICryptoUtils, + protected notebookJson: Partial = {}, + public readonly indentAmount: string = ' ', + private readonly pythonNumber: number = 3 + ) { + this.ensureNotebookJson(); + this.kernelId = this.getStoredKernelId(); + } + public dispose() { + this._isDisposed = true; + this._disposed.fire(); + } + public update(change: NotebookModelChange): void { + this.handleModelChange(change); + } + + public getContent(): string { + return this.generateNotebookContent(); + } + public trust() { + this._isTrusted = true; + } + protected handleUndo(_change: NotebookModelChange): boolean { + return false; + } + protected handleRedo(change: NotebookModelChange): boolean { + let changed = false; + switch (change.kind) { + case 'version': + changed = this.updateVersionInfo(change.kernelConnection); + break; + default: + break; + } + + return changed; + } + protected generateNotebookJson() { + // Make sure we have some + this.ensureNotebookJson(); + + // Reuse our original json except for the cells. + const json = { ...this.notebookJson }; + json.cells = this.cells.map((c) => pruneCell(c.data)); + return json; + } + + private handleModelChange(change: NotebookModelChange) { + const oldDirty = this.isDirty; + let changed = false; + + switch (change.source) { + case 'redo': + case 'user': + changed = this.handleRedo(change); + break; + case 'undo': + changed = this.handleUndo(change); + break; + default: + break; + } + + // Forward onto our listeners if necessary + if (changed || this.isDirty !== oldDirty) { + this._changedEmitter.fire({ ...change, newDirty: this.isDirty, oldDirty, model: this }); + } + // Slightly different for the event we send to VS code. Skip version and file changes. Only send user events. + if ((changed || this.isDirty !== oldDirty) && change.kind !== 'version' && change.source === 'user') { + this._editEventEmitter.fire(change); + } + } + // tslint:disable-next-line: cyclomatic-complexity + private updateVersionInfo(kernelConnection: KernelConnectionMetadata | undefined): boolean { + const { changed, kernelId } = updateNotebookMetadata(this.notebookJson.metadata, kernelConnection); + if (kernelId) { + this.kernelId = kernelId; + } + // Update our kernel id in our global storage too + this.setStoredKernelId(kernelId); + + return changed; + } + + private ensureNotebookJson() { + if (!this.notebookJson || !this.notebookJson.metadata) { + this.notebookJson = getDefaultNotebookContent(this.pythonNumber); + } + } + + private generateNotebookContent(): string { + const json = this.generateNotebookJson(); + return JSON.stringify(json, null, this.indentAmount); + } + private getStoredKernelId(): string | undefined { + // Stored as a list so we don't take up too much space + const list: KernelIdListEntry[] = this.globalMemento.get(ActiveKernelIdList, []); + if (list) { + // Not using a map as we're only going to store the last 40 items. + const fileHash = this.crypto.createHash(this._file.toString(), 'string'); + const entry = list.find((l) => l.fileHash === fileHash); + return entry?.kernelId; + } + } + private setStoredKernelId(id: string | undefined) { + const list: KernelIdListEntry[] = this.globalMemento.get(ActiveKernelIdList, []); + const fileHash = this.crypto.createHash(this._file.toString(), 'string'); + const index = list.findIndex((l) => l.fileHash === fileHash); + // Always remove old spot (we'll push on the back for new ones) + if (index >= 0) { + list.splice(index, 1); + } + + // If adding a new one, push + if (id) { + list.push({ fileHash, kernelId: id }); + } + + // Prune list if too big + while (list.length > MaximumKernelIdListSize) { + list.shift(); + } + return this.globalMemento.update(ActiveKernelIdList, list); + } +} diff --git a/src/client/datascience/notebookStorage/factory.ts b/src/client/datascience/notebookStorage/factory.ts new file mode 100644 index 000000000000..b440b40709f9 --- /dev/null +++ b/src/client/datascience/notebookStorage/factory.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; +import { inject, injectable } from 'inversify'; +import { Memento, Uri } from 'vscode'; +import { UseVSCodeNotebookEditorApi } from '../../common/constants'; +import { ICryptoUtils } from '../../common/types'; +import { ICell, INotebookModel } from '../types'; +import { NativeEditorNotebookModel } from './notebookModel'; +import { INotebookModelFactory } from './types'; +import { VSCodeNotebookModel } from './vscNotebookModel'; + +@injectable() +export class NotebookModelFactory implements INotebookModelFactory { + constructor(@inject(UseVSCodeNotebookEditorApi) private readonly useVSCodeNotebookEditorApi: boolean) {} + public createModel( + options: { + trusted: boolean; + file: Uri; + cells: ICell[]; + notebookJson?: Partial; + globalMemento: Memento; + crypto: ICryptoUtils; + indentAmount?: string; + pythonNumber?: number; + initiallyDirty?: boolean; + }, + forVSCodeNotebook?: boolean + ): INotebookModel { + if (forVSCodeNotebook || this.useVSCodeNotebookEditorApi) { + return new VSCodeNotebookModel( + options.trusted, + options.file, + options.cells, + options.globalMemento, + options.crypto, + options.notebookJson, + options.indentAmount, + options.pythonNumber + ); + } + return new NativeEditorNotebookModel( + options.trusted, + options.file, + options.cells, + options.globalMemento, + options.crypto, + options.notebookJson, + options.indentAmount, + options.pythonNumber, + options.initiallyDirty + ); + } +} diff --git a/src/client/datascience/notebookStorage/nativeEditorProvider.ts b/src/client/datascience/notebookStorage/nativeEditorProvider.ts new file mode 100644 index 000000000000..3b891364ceca --- /dev/null +++ b/src/client/datascience/notebookStorage/nativeEditorProvider.ts @@ -0,0 +1,332 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import * as uuid from 'uuid/v4'; +import { Disposable, Event, EventEmitter, Memento, Uri, WebviewPanel } from 'vscode'; +import { CancellationToken } from 'vscode-languageclient/node'; +import { arePathsSame } from '../../../datascience-ui/react-common/arePathsSame'; +import { + CustomDocument, + CustomDocumentBackup, + CustomDocumentBackupContext, + CustomDocumentEditEvent, + CustomDocumentOpenContext, + CustomEditorProvider, + IApplicationShell, + ICommandManager, + ICustomEditorService, + IDocumentManager, + ILiveShareApi, + IWebviewPanelProvider, + IWorkspaceService +} from '../../common/application/types'; +import { UseCustomEditorApi } from '../../common/constants'; +import { traceInfo } from '../../common/logger'; + +import { + GLOBAL_MEMENTO, + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IExperimentsManager, + IMemento, + WORKSPACE_MEMENTO +} from '../../common/types'; +import { createDeferred } from '../../common/utils/async'; +import { IServiceContainer } from '../../ioc/types'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { generateNewNotebookUri } from '../common'; +import { Identifiers, Telemetry } from '../constants'; +import { IDataViewerFactory } from '../data-viewing/types'; +import { NotebookModelChange } from '../interactive-common/interactiveWindowTypes'; +import { NativeEditor } from '../interactive-ipynb/nativeEditor'; +import { NativeEditorSynchronizer } from '../interactive-ipynb/nativeEditorSynchronizer'; +import { KernelSelector } from '../jupyter/kernels/kernelSelector'; +import { + ICodeCssGenerator, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IInteractiveWindowListener, + IJupyterDebugger, + IJupyterVariableDataProviderFactory, + IJupyterVariables, + INotebookEditor, + INotebookEditorProvider, + INotebookExporter, + INotebookImporter, + INotebookModel, + INotebookProvider, + IStatusProvider, + IThemeFinder, + ITrustService +} from '../types'; +import { getNextUntitledCounter } from './nativeEditorStorage'; +import { NotebookModelEditEvent } from './notebookModelEditEvent'; +import { INotebookStorageProvider } from './notebookStorageProvider'; + +// Class that is registered as the custom editor provider for notebooks. VS code will call into this class when +// opening an ipynb file. This class then creates a backing storage, model, and opens a view for the file. +@injectable() +export class NativeEditorProvider implements INotebookEditorProvider, CustomEditorProvider { + public get onDidChangeActiveNotebookEditor(): Event { + return this._onDidChangeActiveNotebookEditor.event; + } + public get onDidCloseNotebookEditor(): Event { + return this._onDidCloseNotebookEditor.event; + } + public get onDidOpenNotebookEditor(): Event { + return this._onDidOpenNotebookEditor.event; + } + public get activeEditor(): INotebookEditor | undefined { + return this.editors.find((e) => e.visible && e.active); + } + public get onDidChangeCustomDocument(): Event { + return this._onDidEdit.event; + } + + public get editors(): INotebookEditor[] { + return [...this.openedEditors]; + } + // Note, this constant has to match the value used in the package.json to register the webview custom editor. + public static readonly customEditorViewType = 'ms-python.python.notebook.ipynb'; + protected readonly _onDidChangeActiveNotebookEditor = new EventEmitter(); + protected readonly _onDidOpenNotebookEditor = new EventEmitter(); + protected readonly _onDidEdit = new EventEmitter(); + protected customDocuments = new Map(); + private readonly _onDidCloseNotebookEditor = new EventEmitter(); + private openedEditors: Set = new Set(); + private models = new Set(); + private _id = uuid(); + private untitledCounter = 1; + constructor( + @inject(IServiceContainer) protected readonly serviceContainer: IServiceContainer, + @inject(IAsyncDisposableRegistry) protected readonly asyncRegistry: IAsyncDisposableRegistry, + @inject(IDisposableRegistry) protected readonly disposables: IDisposableRegistry, + @inject(IWorkspaceService) protected readonly workspace: IWorkspaceService, + @inject(IConfigurationService) protected readonly configuration: IConfigurationService, + @inject(ICustomEditorService) private customEditorService: ICustomEditorService, + @inject(INotebookStorageProvider) protected readonly storage: INotebookStorageProvider, + @inject(INotebookProvider) private readonly notebookProvider: INotebookProvider, + @inject(IDataScienceFileSystem) protected readonly fs: IDataScienceFileSystem + ) { + traceInfo(`id is ${this._id}`); + + // Register for the custom editor service. + customEditorService.registerCustomEditorProvider(NativeEditorProvider.customEditorViewType, this, { + webviewOptions: { + enableFindWidget: true, + retainContextWhenHidden: true + }, + supportsMultipleEditorsPerDocument: true + }); + } + + public async openCustomDocument( + uri: Uri, + context: CustomDocumentOpenContext, // This has info about backups. right now we use our own data. + _cancellation: CancellationToken + ): Promise { + const model = await this.loadModel(uri, undefined, context.backupId); + return { + uri, + dispose: () => model.dispose() + }; + } + public async saveCustomDocument(document: CustomDocument, cancellation: CancellationToken): Promise { + const model = await this.loadModel(document.uri); + return this.storage.save(model, cancellation); + } + public async saveCustomDocumentAs(document: CustomDocument, targetResource: Uri): Promise { + const model = await this.loadModel(document.uri); + return this.storage.saveAs(model, targetResource); + } + public async revertCustomDocument(document: CustomDocument, cancellation: CancellationToken): Promise { + const model = await this.loadModel(document.uri); + return this.storage.revert(model, cancellation); + } + public async backupCustomDocument( + document: CustomDocument, + _context: CustomDocumentBackupContext, + cancellation: CancellationToken + ): Promise { + const model = await this.loadModel(document.uri); + const id = this.storage.generateBackupId(model); + await this.storage.backup(model, cancellation, id); + return { + id, + delete: () => this.storage.deleteBackup(model, id).ignoreErrors() // This cleans up after save has happened. + }; + } + + public async resolveCustomEditor(document: CustomDocument, panel: WebviewPanel) { + this.customDocuments.set(document.uri.fsPath, document); + await this.loadNotebookEditor(document.uri, panel); + } + + public async resolveCustomDocument(document: CustomDocument): Promise { + this.customDocuments.set(document.uri.fsPath, document); + await this.loadModel(document.uri); + } + + public async open(file: Uri): Promise { + // Create a deferred promise that will fire when the notebook + // actually opens + const deferred = createDeferred(); + + // Sign up for open event once it does open + let disposable: Disposable | undefined; + const handler = (e: INotebookEditor) => { + if (arePathsSame(e.file.fsPath, file.fsPath)) { + if (disposable) { + disposable.dispose(); + } + deferred.resolve(e); + } + }; + disposable = this._onDidOpenNotebookEditor.event(handler); + + // Send an open command. + this.customEditorService.openEditor(file, NativeEditorProvider.customEditorViewType).ignoreErrors(); + + // Promise should resolve when the file opens. + return deferred.promise; + } + + public async show(file: Uri): Promise { + return this.open(file); + } + + @captureTelemetry(Telemetry.CreateNewNotebook, undefined, false) + public async createNew(contents?: string, title?: string): Promise { + // Create a new URI for the dummy file using our root workspace path + const uri = this.getNextNewNotebookUri(title); + + // Set these contents into the storage before the file opens. Make sure not + // load from the memento storage though as this is an entirely brand new file. + await this.loadModel(uri, contents, true); + + return this.open(uri); + } + + public async loadModel(file: Uri, contents?: string, skipDirtyContents?: boolean): Promise; + // tslint:disable-next-line: unified-signatures + public async loadModel(file: Uri, contents?: string, backupId?: string): Promise; + // tslint:disable-next-line: no-any + public async loadModel(file: Uri, contents?: string, options?: any): Promise { + // Get the model that may match this file + let model = [...this.models.values()].find((m) => this.fs.arePathsSame(m.file, file)); + if (!model) { + // Every time we load a new untitled file, up the counter past the max value for this counter + this.untitledCounter = getNextUntitledCounter(file, this.untitledCounter); + + // Load our model from our storage object. + model = await this.storage.getOrCreateModel(file, contents, options); + + // Make sure to listen to events on the model + this.trackModel(model); + } + return model; + } + + protected createNotebookEditor(model: INotebookModel, panel?: WebviewPanel): NativeEditor { + const editor = new NativeEditor( + this.serviceContainer.getAll(IInteractiveWindowListener), + this.serviceContainer.get(ILiveShareApi), + this.serviceContainer.get(IApplicationShell), + this.serviceContainer.get(IDocumentManager), + this.serviceContainer.get(IWebviewPanelProvider), + this.serviceContainer.get(IDisposableRegistry), + this.serviceContainer.get(ICodeCssGenerator), + this.serviceContainer.get(IThemeFinder), + this.serviceContainer.get(IStatusProvider), + this.serviceContainer.get(IDataScienceFileSystem), + this.serviceContainer.get(IConfigurationService), + this.serviceContainer.get(ICommandManager), + this.serviceContainer.get(INotebookExporter), + this.serviceContainer.get(IWorkspaceService), + this.serviceContainer.get(NativeEditorSynchronizer), + this.serviceContainer.get(INotebookEditorProvider), + this.serviceContainer.get(IDataViewerFactory), + this.serviceContainer.get(IJupyterVariableDataProviderFactory), + this.serviceContainer.get(IJupyterVariables, Identifiers.ALL_VARIABLES), + this.serviceContainer.get(IJupyterDebugger), + this.serviceContainer.get(INotebookImporter), + this.serviceContainer.get(IDataScienceErrorHandler), + this.serviceContainer.get(IMemento, GLOBAL_MEMENTO), + this.serviceContainer.get(IMemento, WORKSPACE_MEMENTO), + this.serviceContainer.get(IExperimentsManager), + this.serviceContainer.get(IAsyncDisposableRegistry), + this.serviceContainer.get(INotebookProvider), + this.serviceContainer.get(UseCustomEditorApi), + this.serviceContainer.get(ITrustService), + this.serviceContainer.get(IExperimentService), + model, + panel, + this.serviceContainer.get(KernelSelector) + ); + this.openedEditor(editor); + return editor; + } + + protected async loadNotebookEditor(resource: Uri, panel?: WebviewPanel) { + try { + // Get the model + const model = await this.loadModel(resource); + + // Load it (should already be visible) + return this.createNotebookEditor(model, panel); + } catch (exc) { + // Send telemetry indicating a failure + sendTelemetryEvent(Telemetry.OpenNotebookFailure); + throw exc; + } + } + + protected openedEditor(editor: INotebookEditor): void { + this.disposables.push(editor.onDidChangeViewState(this.onChangedViewState, this)); + this.openedEditors.add(editor); + editor.closed(this.closedEditor, this, this.disposables); + this._onDidOpenNotebookEditor.fire(editor); + } + + protected async modelEdited(model: INotebookModel, change: NotebookModelChange) { + // Find the document associated with this edit. + const document = this.customDocuments.get(model.file.fsPath); + + // Tell VS code about model changes if not caused by vs code itself + if (document && change.kind !== 'save' && change.kind !== 'saveAs' && change.source === 'user') { + this._onDidEdit.fire(new NotebookModelEditEvent(document, model, change)); + } + } + + private closedEditor(editor: INotebookEditor): void { + this.openedEditors.delete(editor); + this._onDidCloseNotebookEditor.fire(editor); + } + private trackModel(model: INotebookModel) { + if (!this.models.has(model)) { + this.models.add(model); + this.disposables.push(model.onDidDispose(this.onDisposedModel.bind(this, model))); + this.disposables.push(model.onDidEdit(this.modelEdited.bind(this, model))); + } + } + + private onDisposedModel(model: INotebookModel) { + // When model goes away, dispose of the associated notebook (as all of the editors have closed down) + this.notebookProvider + .getOrCreateNotebook({ identity: model.file, getOnly: true }) + .then((n) => n?.dispose()) + .ignoreErrors(); + this.models.delete(model); + } + + private onChangedViewState(): void { + this._onDidChangeActiveNotebookEditor.fire(this.activeEditor); + } + + private getNextNewNotebookUri(title?: string): Uri { + return generateNewNotebookUri(this.untitledCounter, this.workspace.rootPath, title); + } +} diff --git a/src/client/datascience/notebookStorage/nativeEditorStorage.ts b/src/client/datascience/notebookStorage/nativeEditorStorage.ts new file mode 100644 index 000000000000..0ca96a90bf9e --- /dev/null +++ b/src/client/datascience/notebookStorage/nativeEditorStorage.ts @@ -0,0 +1,509 @@ +import type { nbformat } from '@jupyterlab/coreutils'; +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { CancellationToken, Memento, Uri } from 'vscode'; +import { createCodeCell } from '../../../datascience-ui/common/cellFactory'; +import { traceError } from '../../common/logger'; +import { isFileNotFoundError } from '../../common/platform/errors'; + +import { GLOBAL_MEMENTO, ICryptoUtils, IExtensionContext, IMemento, WORKSPACE_MEMENTO } from '../../common/types'; +import { isUntitledFile, noop } from '../../common/utils/misc'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Identifiers, KnownNotebookLanguages, Telemetry } from '../constants'; +import { InvalidNotebookFileError } from '../jupyter/invalidNotebookFileError'; +import { INotebookModelFactory } from '../notebookStorage/types'; +import { + CellState, + IDataScienceFileSystem, + IJupyterExecution, + INotebookModel, + INotebookStorage, + ITrustService +} from '../types'; + +// tslint:disable-next-line:no-require-imports no-var-requires +import detectIndent = require('detect-indent'); +import { VSCodeNotebookModel } from './vscNotebookModel'; + +export const KeyPrefix = 'notebook-storage-'; +const NotebookTransferKey = 'notebook-transfered'; + +export function getNextUntitledCounter(file: Uri | undefined, currentValue: number): number { + if (file && isUntitledFile(file)) { + const basename = path.basename(file.fsPath, 'ipynb'); + const extname = path.extname(file.fsPath); + if (extname.toLowerCase() === '.ipynb') { + // See if ends with - + const match = /.*-(\d+)/.exec(basename); + if (match && match[1]) { + const fileValue = parseInt(match[1], 10); + if (fileValue) { + return Math.max(currentValue, fileValue + 1); + } + } + } + } + + return currentValue; +} + +@injectable() +export class NativeEditorStorage implements INotebookStorage { + // Keep track of if we are backing up our file already + private backingUp = false; + // If backup requests come in while we are already backing up save the most recent one here + private backupRequested: { model: INotebookModel; cancellation: CancellationToken } | undefined; + + constructor( + @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(ICryptoUtils) private crypto: ICryptoUtils, + @inject(IExtensionContext) private context: IExtensionContext, + @inject(IMemento) @named(GLOBAL_MEMENTO) private globalStorage: Memento, + @inject(IMemento) @named(WORKSPACE_MEMENTO) private localStorage: Memento, + @inject(ITrustService) private trustService: ITrustService, + @inject(INotebookModelFactory) private readonly factory: INotebookModelFactory + ) {} + private static isUntitledFile(file: Uri) { + return isUntitledFile(file); + } + + public generateBackupId(model: INotebookModel): string { + return `${path.basename(model.file.fsPath)}-${uuid()}`; + } + public get(_file: Uri): INotebookModel | undefined { + return undefined; + } + public getOrCreateModel( + file: Uri, + possibleContents?: string, + backupId?: string, + forVSCodeNotebook?: boolean + ): Promise; + public getOrCreateModel( + file: Uri, + possibleContents?: string, + // tslint:disable-next-line: unified-signatures + skipDirtyContents?: boolean, + forVSCodeNotebook?: boolean + ): Promise; + public getOrCreateModel( + file: Uri, + possibleContents?: string, + // tslint:disable-next-line: no-any + options?: any, + forVSCodeNotebook?: boolean + ): Promise { + return this.loadFromFile(file, possibleContents, options, forVSCodeNotebook); + } + public async save(model: INotebookModel, _cancellation: CancellationToken): Promise { + const contents = model.getContent(); + const parallelize = [this.fs.writeFile(model.file, contents)]; + if (model.isTrusted) { + parallelize.push(this.trustService.trustNotebook(model.file, contents)); + } + await Promise.all(parallelize); + model.update({ + source: 'user', + kind: 'save', + oldDirty: model.isDirty, + newDirty: false + }); + } + + public async saveAs(model: INotebookModel, file: Uri): Promise { + const contents = model.getContent(); + const parallelize = [this.fs.writeFile(file, contents)]; + if (model.isTrusted) { + parallelize.push(this.trustService.trustNotebook(file, contents)); + } + await Promise.all(parallelize); + if (model instanceof VSCodeNotebookModel) { + return; + } + model.update({ + source: 'user', + kind: 'saveAs', + oldDirty: model.isDirty, + newDirty: false, + target: file, + sourceUri: model.file + }); + } + public async backup(model: INotebookModel, cancellation: CancellationToken, backupId?: string): Promise { + // If we are already backing up, save this request replacing any other previous requests + if (this.backingUp) { + this.backupRequested = { model, cancellation }; + return; + } + this.backingUp = true; + // Should send to extension context storage path + return this.storeContentsInHotExitFile(model, cancellation, backupId).finally(() => { + this.backingUp = false; + + // If there is a backup request waiting, then clear and start it + if (this.backupRequested) { + const requested = this.backupRequested; + this.backupRequested = undefined; + this.backup(requested.model, requested.cancellation).catch((error) => { + traceError(`Error in backing up NativeEditor Storage: ${error}`); + }); + } + }); + } + + public async revert(model: INotebookModel, _cancellation: CancellationToken): Promise { + // Revert to what is in the hot exit file + await this.loadFromFile(model.file); + } + + public async deleteBackup(model: INotebookModel, backupId: string): Promise { + return this.clearHotExit(model.file, backupId); + } + /** + * Stores the uncommitted notebook changes into a temporary location. + * Also keep track of the current time. This way we can check whether changes were + * made to the file since the last time uncommitted changes were stored. + */ + private async storeContentsInHotExitFile( + model: INotebookModel, + cancelToken?: CancellationToken, + backupId?: string + ): Promise { + const contents = model.getContent(); + const key = backupId || this.getStaticStorageKey(model.file); + const filePath = this.getHashedFileName(key); + + // Keep track of the time when this data was saved. + // This way when we retrieve the data we can compare it against last modified date of the file. + const specialContents = contents ? JSON.stringify({ contents, lastModifiedTimeMs: Date.now() }) : undefined; + return this.writeToStorage(model.file, filePath, specialContents, cancelToken); + } + + private async clearHotExit(file: Uri, backupId?: string): Promise { + const key = backupId || this.getStaticStorageKey(file); + const filePath = this.getHashedFileName(key); + await this.writeToStorage(undefined, filePath); + } + + private async writeToStorage( + owningFile: Uri | undefined, + filePath: string, + contents?: string, + cancelToken?: CancellationToken + ): Promise { + try { + if (!cancelToken?.isCancellationRequested) { + if (contents) { + await this.fs.createLocalDirectory(path.dirname(filePath)); + if (!cancelToken?.isCancellationRequested) { + if (owningFile) { + this.trustService.trustNotebook(owningFile, contents).ignoreErrors(); + } + await this.fs.writeLocalFile(filePath, contents); + } + } else { + await this.fs.deleteLocalFile(filePath).catch((ex) => { + // No need to log error if file doesn't exist. + if (!isFileNotFoundError(ex)) { + traceError('Failed to delete hotExit file. Possible it does not exist', ex); + } + }); + } + } + } catch (exc) { + traceError(`Error writing storage for ${filePath}: `, exc); + } + } + private async extractPythonMainVersion(notebookData: Partial): Promise { + if ( + notebookData && + notebookData.metadata && + notebookData.metadata.language_info && + notebookData.metadata.language_info.codemirror_mode && + // tslint:disable-next-line: no-any + typeof (notebookData.metadata.language_info.codemirror_mode as any).version === 'number' + ) { + // tslint:disable-next-line: no-any + return (notebookData.metadata.language_info.codemirror_mode as any).version; + } + // Use the active interpreter + const usableInterpreter = await this.jupyterExecution.getUsableJupyterPython(); + return usableInterpreter && usableInterpreter.version ? usableInterpreter.version.major : 3; + } + + private sendLanguageTelemetry(notebookJson: Partial) { + try { + // See if we have a language + let language = ''; + if (notebookJson.metadata?.language_info?.name) { + language = notebookJson.metadata?.language_info?.name; + } else if (notebookJson.metadata?.kernelspec?.language) { + language = notebookJson.metadata?.kernelspec?.language.toString(); + } + if (language && !KnownNotebookLanguages.includes(language.toLowerCase())) { + language = 'unknown'; + } + if (language) { + sendTelemetryEvent(Telemetry.NotebookLanguage, undefined, { language }); + } + } catch { + // If this fails, doesn't really matter + noop(); + } + } + private loadFromFile( + file: Uri, + possibleContents?: string, + backupId?: string, + forVSCodeNotebook?: boolean + ): Promise; + private loadFromFile( + file: Uri, + possibleContents?: string, + // tslint:disable-next-line: unified-signatures + skipDirtyContents?: boolean, + forVSCodeNotebook?: boolean + ): Promise; + private async loadFromFile( + file: Uri, + possibleContents?: string, + options?: boolean | string, + forVSCodeNotebook?: boolean + ): Promise { + try { + // Attempt to read the contents if a viable file + const contents = NativeEditorStorage.isUntitledFile(file) ? possibleContents : await this.fs.readFile(file); + + const skipDirtyContents = typeof options === 'boolean' ? options : !!options; + // Use backupId provided, else use static storage key. + const backupId = + typeof options === 'string' ? options : skipDirtyContents ? undefined : this.getStaticStorageKey(file); + + // If skipping dirty contents, delete the dirty hot exit file now + if (skipDirtyContents) { + await this.clearHotExit(file, backupId); + } + + // See if this file was stored in storage prior to shutdown + const dirtyContents = skipDirtyContents ? undefined : await this.getStoredContents(file, backupId); + if (dirtyContents) { + // This means we're dirty. Indicate dirty and load from this content + return this.loadContents(file, dirtyContents, true, forVSCodeNotebook); + } else { + // Load without setting dirty + return this.loadContents(file, contents, undefined, forVSCodeNotebook); + } + } catch (ex) { + // May not exist at this time. Should always have a single cell though + traceError(`Failed to load notebook file ${file.toString()}`, ex); + return this.factory.createModel( + { trusted: true, file, cells: [], crypto: this.crypto, globalMemento: this.globalStorage }, + forVSCodeNotebook + ); + } + } + + private createEmptyCell(id: string) { + return { + id, + line: 0, + file: Identifiers.EmptyFileName, + state: CellState.finished, + data: createCodeCell() + }; + } + + private async loadContents( + file: Uri, + contents: string | undefined, + isInitiallyDirty = false, + forVSCodeNotebook?: boolean + ) { + // tslint:disable-next-line: no-any + const json = contents ? (JSON.parse(contents) as Partial) : undefined; + + // Double check json (if we have any) + if (json && !json.cells) { + throw new InvalidNotebookFileError(file.fsPath); + } + + // Then compute indent. It's computed from the contents + const indentAmount = contents ? detectIndent(contents).indent : undefined; + + // Then save the contents. We'll stick our cells back into this format when we save + if (json) { + // Log language or kernel telemetry + this.sendLanguageTelemetry(json); + } + + // Extract cells from the json + const cells = json ? (json.cells as (nbformat.ICodeCell | nbformat.IRawCell | nbformat.IMarkdownCell)[]) : []; + + // Remap the ids + const remapped = cells.map((c, index) => { + return { + id: `NotebookImport#${index}`, + file: Identifiers.EmptyFileName, + line: 0, + state: CellState.finished, + data: c + }; + }); + + if (!forVSCodeNotebook) { + // Make sure at least one + if (remapped.length === 0) { + remapped.splice(0, 0, this.createEmptyCell(uuid())); + } + } + const pythonNumber = json ? await this.extractPythonMainVersion(json) : 3; + + const model = this.factory.createModel( + { + trusted: isUntitledFile(file) || json === undefined, + file, + cells: remapped, + notebookJson: json, + indentAmount, + pythonNumber, + initiallyDirty: isInitiallyDirty, + crypto: this.crypto, + globalMemento: this.globalStorage + }, + forVSCodeNotebook + ); + + // If no contents or untitled, this is a newly created file + // If dirty, that means it's been edited before in our extension + if (contents !== undefined && !isUntitledFile(file) && !isInitiallyDirty && !model.isTrusted) { + const isNotebookTrusted = await this.trustService.isNotebookTrusted(file, model.getContent()); + if (isNotebookTrusted) { + model.trust(); + } + } else { + model.trust(); + } + + return model; + } + + private getStaticStorageKey(file: Uri): string { + return `${KeyPrefix}${file.toString()}`; + } + + /** + * Gets any unsaved changes to the notebook file from the old locations. + * If the file has been modified since the uncommitted changes were stored, then ignore the uncommitted changes. + * + * @private + * @returns {(Promise)} + * @memberof NativeEditor + */ + private async getStoredContents(file: Uri, backupId?: string): Promise { + const key = backupId || this.getStaticStorageKey(file); + + // First look in the global storage file location + let result = await this.getStoredContentsFromFile(file, key); + if (!result) { + result = await this.getStoredContentsFromGlobalStorage(file, key); + if (!result) { + result = await this.getStoredContentsFromLocalStorage(file, key); + } + } + + return result; + } + + private async getStoredContentsFromFile(file: Uri, key: string): Promise { + const filePath = this.getHashedFileName(key); + try { + // Use this to read from the extension global location + const contents = await this.fs.readLocalFile(filePath); + const data = JSON.parse(contents); + // Check whether the file has been modified since the last time the contents were saved. + if (data && data.lastModifiedTimeMs && file.scheme === 'file') { + const stat = await this.fs.stat(file); + if (stat.mtime > data.lastModifiedTimeMs) { + return; + } + } + if (data && data.contents) { + return data.contents; + } + } catch (exc) { + // No need to log error if file doesn't exist. + if (!isFileNotFoundError(exc)) { + traceError(`Exception reading from temporary storage for ${key}`, exc); + } + } + } + + private async getStoredContentsFromGlobalStorage(file: Uri, key: string): Promise { + try { + const data = this.globalStorage.get<{ contents?: string; lastModifiedTimeMs?: number }>(key); + + // If we have data here, make sure we eliminate any remnants of storage + if (data) { + await this.transferStorage(); + } + + // Check whether the file has been modified since the last time the contents were saved. + if (data && data.lastModifiedTimeMs && file.scheme === 'file') { + const stat = await this.fs.stat(file); + if (stat.mtime > data.lastModifiedTimeMs) { + return; + } + } + if (data && data.contents) { + return data.contents; + } + } catch { + noop(); + } + } + + private async getStoredContentsFromLocalStorage(_file: Uri, key: string): Promise { + const workspaceData = this.localStorage.get(key); + if (workspaceData) { + // Make sure to clear so we don't use this again. + this.localStorage.update(key, undefined); + + return workspaceData; + } + } + + // VS code recommended we use the hidden '_values' to iterate over all of the entries in + // the global storage map and delete the ones we own. + private async transferStorage(): Promise { + const promises: Thenable[] = []; + + // Indicate we ran this function + await this.globalStorage.update(NotebookTransferKey, true); + + try { + // tslint:disable-next-line: no-any + if ((this.globalStorage as any)._value) { + // tslint:disable-next-line: no-any + const keys = Object.keys((this.globalStorage as any)._value); + [...keys].forEach((k: string) => { + if (k.startsWith(KeyPrefix)) { + // Remove from the map so that global storage does not have this anymore. + // Use the real API here as we don't know how the map really gets updated. + promises.push(this.globalStorage.update(k, undefined)); + } + }); + } + } catch (e) { + traceError('Exception eliminating global storage parts:', e); + } + + return Promise.all(promises); + } + + private getHashedFileName(key: string): string { + const file = `${this.crypto.createHash(key, 'string')}.ipynb`; + return path.join(this.context.globalStoragePath, file); + } +} diff --git a/src/client/datascience/notebookStorage/notebookModel.ts b/src/client/datascience/notebookStorage/notebookModel.ts new file mode 100644 index 000000000000..d5dc511fadcb --- /dev/null +++ b/src/client/datascience/notebookStorage/notebookModel.ts @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; +import * as fastDeepEqual from 'fast-deep-equal'; +import * as uuid from 'uuid/v4'; +import { Memento, Uri } from 'vscode'; +import { concatMultilineString, splitMultilineString } from '../../../datascience-ui/common'; +import { createCodeCell } from '../../../datascience-ui/common/cellFactory'; +import { ICryptoUtils } from '../../common/types'; +import { Identifiers } from '../constants'; +import { IEditorContentChange, NotebookModelChange } from '../interactive-common/interactiveWindowTypes'; +import { CellState, ICell } from '../types'; +import { BaseNotebookModel } from './baseModel'; + +export class NativeEditorNotebookModel extends BaseNotebookModel { + public get id() { + return this._id; + } + private _id = uuid(); + private saveChangeCount: number = 0; + private changeCount: number = 0; + public get isDirty(): boolean { + return this.changeCount !== this.saveChangeCount; + } + constructor( + isTrusted: boolean, + file: Uri, + cells: ICell[], + globalMemento: Memento, + crypto: ICryptoUtils, + json: Partial = {}, + indentAmount: string = ' ', + pythonNumber: number = 3, + isInitiallyDirty: boolean = false + ) { + super(isTrusted, file, cells, globalMemento, crypto, json, indentAmount, pythonNumber); + if (isInitiallyDirty) { + // This means we're dirty. Indicate dirty and load from this content + this.saveChangeCount = -1; + } + } + + public async applyEdits(edits: readonly NotebookModelChange[]): Promise { + edits.forEach((e) => this.update({ ...e, source: 'redo' })); + } + public async undoEdits(edits: readonly NotebookModelChange[]): Promise { + edits.forEach((e) => this.update({ ...e, source: 'undo' })); + } + + protected handleRedo(change: NotebookModelChange): boolean { + let changed = false; + switch (change.kind) { + case 'clear': + changed = this.clearOutputs(); + break; + case 'edit': + changed = this.editCell(change.forward, change.id); + break; + case 'insert': + changed = this.insertCell(change.cell, change.index); + break; + case 'changeCellType': + changed = this.changeCellType(change.cell); + break; + case 'modify': + changed = this.modifyCells(change.newCells); + break; + case 'remove': + changed = this.removeCell(change.cell); + break; + case 'remove_all': + changed = this.removeAllCells(change.newCellId); + break; + case 'swap': + changed = this.swapCells(change.firstCellId, change.secondCellId); + break; + case 'updateCellExecutionCount': + changed = this.updateCellExecutionCount(change.cellId, change.executionCount); + break; + case 'save': + this.saveChangeCount = this.changeCount; + break; + case 'saveAs': + this.saveChangeCount = this.changeCount; + this.changeCount = this.saveChangeCount = 0; + this._file = change.target; + break; + default: + changed = super.handleRedo(change); + break; + } + + // Dirty state comes from undo. At least VS code will track it that way. However + // skip file changes as we don't forward those to VS code + if (change.kind !== 'save' && change.kind !== 'saveAs') { + this.changeCount += 1; + } + + return changed; + } + + protected handleUndo(change: NotebookModelChange): boolean { + let changed = false; + switch (change.kind) { + case 'clear': + changed = !fastDeepEqual(this.cells, change.oldCells); + this._cells = change.oldCells; + break; + case 'edit': + this.editCell(change.reverse, change.id); + changed = true; + break; + case 'changeCellType': + this.changeCellType(change.cell); + changed = true; + break; + case 'insert': + changed = this.removeCell(change.cell); + break; + case 'modify': + changed = this.modifyCells(change.oldCells); + break; + case 'remove': + changed = this.insertCell(change.cell, change.index); + break; + case 'remove_all': + this._cells = change.oldCells; + changed = true; + break; + case 'swap': + changed = this.swapCells(change.firstCellId, change.secondCellId); + break; + default: + break; + } + + // Dirty state comes from undo. At least VS code will track it that way. + // Note unlike redo, 'file' and 'version' are not possible on undo as + // we don't send them to VS code. + this.changeCount -= 1; + + return changed; + } + + private removeAllCells(newCellId: string) { + this._cells = []; + this._cells.push(this.createEmptyCell(newCellId)); + return true; + } + + private applyCellContentChange(change: IEditorContentChange, id: string): boolean { + const normalized = change.text.replace(/\r/g, ''); + + // Figure out which cell we're editing. + const index = this.cells.findIndex((c) => c.id === id); + if (index >= 0) { + // This is an actual edit. + const contents = concatMultilineString(this.cells[index].data.source); + const before = contents.substr(0, change.rangeOffset); + const after = contents.substr(change.rangeOffset + change.rangeLength); + const newContents = `${before}${normalized}${after}`; + if (contents !== newContents) { + const newCell = { + ...this.cells[index], + data: { ...this.cells[index].data, source: splitMultilineString(newContents) } + }; + this._cells[index] = this.asCell(newCell); + return true; + } + } + return false; + } + + private editCell(changes: IEditorContentChange[], id: string): boolean { + // Apply the changes to the visible cell list + if (changes && changes.length) { + return changes.map((c) => this.applyCellContentChange(c, id)).reduce((p, c) => p || c, false); + } + + return false; + } + + private swapCells(firstCellId: string, secondCellId: string) { + const first = this.cells.findIndex((v) => v.id === firstCellId); + const second = this.cells.findIndex((v) => v.id === secondCellId); + if (first >= 0 && second >= 0 && first !== second) { + const temp = { ...this.cells[first] }; + this._cells[first] = this.asCell(this.cells[second]); + this._cells[second] = this.asCell(temp); + return true; + } + return false; + } + + private updateCellExecutionCount(cellId: string, executionCount?: number) { + const index = this.cells.findIndex((v) => v.id === cellId); + if (index >= 0) { + this._cells[index].data.execution_count = + typeof executionCount === 'number' && executionCount > 0 ? executionCount : null; + return true; + } + return false; + } + + private modifyCells(cells: ICell[]): boolean { + // Update these cells in our list + cells.forEach((c) => { + const index = this.cells.findIndex((v) => v.id === c.id); + this._cells[index] = this.asCell(c); + }); + return true; + } + + private changeCellType(cell: ICell): boolean { + // Update the cell in our list. + const index = this.cells.findIndex((v) => v.id === cell.id); + this._cells[index] = this.asCell(cell); + return true; + } + + private removeCell(cell: ICell): boolean { + const index = this.cells.findIndex((c) => c.id === cell.id); + if (index >= 0) { + this.cells.splice(index, 1); + return true; + } + return false; + } + + private clearOutputs(): boolean { + const newCells = this.cells.map((c) => + this.asCell({ ...c, data: { ...c.data, execution_count: null, outputs: [] } }) + ); + const result = !fastDeepEqual(newCells, this.cells); + this._cells = newCells; + return result; + } + + private insertCell(cell: ICell, index: number): boolean { + // Insert a cell into our visible list based on the index. They should be in sync + this._cells.splice(index, 0, cell); + return true; + } + + // tslint:disable-next-line: no-any + private asCell(cell: any): ICell { + // Works around problems with setting a cell to another one in the nyc compiler. + return cell as ICell; + } + + private createEmptyCell(id: string) { + return { + id, + line: 0, + file: Identifiers.EmptyFileName, + state: CellState.finished, + data: createCodeCell() + }; + } +} diff --git a/src/client/datascience/notebookStorage/notebookModelEditEvent.ts b/src/client/datascience/notebookStorage/notebookModelEditEvent.ts new file mode 100644 index 000000000000..9fbbcc17b281 --- /dev/null +++ b/src/client/datascience/notebookStorage/notebookModelEditEvent.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CustomDocument, CustomDocumentEditEvent } from '../../common/application/types'; +import { NotebookModelChange } from '../interactive-common/interactiveWindowTypes'; +import { INotebookModel } from '../types'; +import { NativeEditorNotebookModel } from './notebookModel'; +export class NotebookModelEditEvent implements CustomDocumentEditEvent { + public label?: string | undefined; + constructor( + public readonly document: CustomDocument, + private readonly model: INotebookModel, + private readonly change: NotebookModelChange + ) { + this.label = change.kind; + } + public undo(): void | Thenable { + return (this.model as NativeEditorNotebookModel).undoEdits([{ ...this.change, source: 'undo' }]); + } + public redo(): void | Thenable { + return (this.model as NativeEditorNotebookModel).applyEdits([{ ...this.change, source: 'redo' }]); + } +} diff --git a/src/client/datascience/notebookStorage/notebookStorageProvider.ts b/src/client/datascience/notebookStorage/notebookStorageProvider.ts new file mode 100644 index 000000000000..4067751614cc --- /dev/null +++ b/src/client/datascience/notebookStorage/notebookStorageProvider.ts @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { EventEmitter, Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { IWorkspaceService } from '../../common/application/types'; +import { IDisposable, IDisposableRegistry } from '../../common/types'; +import { generateNewNotebookUri } from '../common'; +import { INotebookModel, INotebookStorage } from '../types'; +import { getNextUntitledCounter } from './nativeEditorStorage'; +import { VSCodeNotebookModel } from './vscNotebookModel'; + +// tslint:disable-next-line:no-require-imports no-var-requires + +export const INotebookStorageProvider = Symbol.for('INotebookStorageProvider'); +export interface INotebookStorageProvider extends INotebookStorage { + createNew(contents?: string, forVSCodeNotebook?: boolean): Promise; +} +@injectable() +export class NotebookStorageProvider implements INotebookStorageProvider { + public get onSavedAs() { + return this._savedAs.event; + } + private static untitledCounter = 1; + private readonly _savedAs = new EventEmitter<{ new: Uri; old: Uri }>(); + private readonly storageAndModels = new Map>(); + private readonly resolvedStorageAndModels = new Map(); + private models = new Set(); + private readonly disposables: IDisposable[] = []; + constructor( + @inject(INotebookStorage) private readonly storage: INotebookStorage, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService + ) { + disposables.push(this); + } + public async save(model: INotebookModel, cancellation: CancellationToken) { + await this.storage.save(model, cancellation); + } + public async saveAs(model: INotebookModel, targetResource: Uri) { + const oldUri = model.file; + await this.storage.saveAs(model, targetResource); + if (model instanceof VSCodeNotebookModel) { + return; + } + this.trackModel(model); + this.storageAndModels.delete(oldUri.toString()); + this.storageAndModels.set(targetResource.toString(), Promise.resolve(model)); + } + public generateBackupId(model: INotebookModel): string { + return this.storage.generateBackupId(model); + } + public backup(model: INotebookModel, cancellation: CancellationToken, backupId?: string) { + return this.storage.backup(model, cancellation, backupId); + } + public revert(model: INotebookModel, cancellation: CancellationToken) { + return this.storage.revert(model, cancellation); + } + public deleteBackup(model: INotebookModel, backupId?: string) { + return this.storage.deleteBackup(model, backupId); + } + public get(file: Uri): INotebookModel | undefined { + return this.resolvedStorageAndModels.get(file.toString()); + } + + public getOrCreateModel( + file: Uri, + contents?: string, + backupId?: string, + forVSCodeNotebook?: boolean + ): Promise; + public getOrCreateModel( + file: Uri, + contents?: string, + // tslint:disable-next-line: unified-signatures + skipDirtyContents?: boolean, + forVSCodeNotebook?: boolean + ): Promise; + + public getOrCreateModel( + file: Uri, + contents?: string, + // tslint:disable-next-line: no-any + options?: any, + forVSCodeNotebook?: boolean + ): Promise { + const key = file.toString(); + if (!this.storageAndModels.has(key)) { + // Every time we load a new untitled file, up the counter past the max value for this counter + NotebookStorageProvider.untitledCounter = getNextUntitledCounter( + file, + NotebookStorageProvider.untitledCounter + ); + const promise = this.storage.getOrCreateModel(file, contents, options, forVSCodeNotebook); + this.storageAndModels.set(key, promise.then(this.trackModel.bind(this))); + } + return this.storageAndModels.get(key)!; + } + public dispose() { + while (this.disposables.length) { + this.disposables.shift()?.dispose(); // NOSONAR + } + } + + public async createNew(contents?: string, forVSCodeNotebooks?: boolean): Promise { + // Create a new URI for the dummy file using our root workspace path + const uri = this.getNextNewNotebookUri(forVSCodeNotebooks); + + // Always skip loading from the hot exit file. When creating a new file we want a new file. + return this.getOrCreateModel(uri, contents, true, forVSCodeNotebooks); + } + + private getNextNewNotebookUri(forVSCodeNotebooks?: boolean): Uri { + return generateNewNotebookUri( + NotebookStorageProvider.untitledCounter, + this.workspace.rootPath, + undefined, + forVSCodeNotebooks + ); + } + + private trackModel(model: INotebookModel): INotebookModel { + this.disposables.push(model); + this.models.add(model); + this.resolvedStorageAndModels.set(model.file.toString(), model); + // When a model is no longer used, ensure we remove it from the cache. + model.onDidDispose( + () => { + this.models.delete(model); + this.storageAndModels.delete(model.file.toString()); + this.resolvedStorageAndModels.delete(model.file.toString()); + }, + this, + this.disposables + ); + return model; + } +} diff --git a/src/client/datascience/notebookStorage/types.ts b/src/client/datascience/notebookStorage/types.ts new file mode 100644 index 000000000000..f26f2c5aafe0 --- /dev/null +++ b/src/client/datascience/notebookStorage/types.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; +import { Memento, Uri } from 'vscode'; +import { ICryptoUtils } from '../../common/types'; +import { ICell, INotebookModel } from '../types'; + +export const INotebookModelFactory = Symbol('INotebookModelFactory'); +export interface INotebookModelFactory { + createModel( + options: { + trusted: boolean; + file: Uri; + cells: ICell[]; + notebookJson?: Partial; + indentAmount?: string; + pythonNumber?: number; + initiallyDirty?: boolean; + crypto: ICryptoUtils; + globalMemento: Memento; + }, + forVSCodeNotebook?: boolean + ): INotebookModel; +} diff --git a/src/client/datascience/notebookStorage/vscNotebookModel.ts b/src/client/datascience/notebookStorage/vscNotebookModel.ts new file mode 100644 index 000000000000..afa8ff767cf4 --- /dev/null +++ b/src/client/datascience/notebookStorage/vscNotebookModel.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import type { nbformat } from '@jupyterlab/coreutils'; +import { Memento, Uri } from 'vscode'; +import { NotebookDocument } from '../../../../types/vscode-proposed'; +import { IVSCodeNotebook } from '../../common/application/types'; +import { ICryptoUtils } from '../../common/types'; +import { NotebookModelChange } from '../interactive-common/interactiveWindowTypes'; +import { + createCellFromVSCNotebookCell, + getNotebookMetadata, + updateVSCNotebookAfterTrustingNotebook +} from '../notebook/helpers/helpers'; +import { ICell } from '../types'; +import { BaseNotebookModel } from './baseModel'; + +// https://github.com/microsoft/vscode-python/issues/13155 +// tslint:disable-next-line: no-any +function sortObjectPropertiesRecursively(obj: any): any { + if (Array.isArray(obj)) { + return obj.map(sortObjectPropertiesRecursively); + } + if (obj !== undefined && obj !== null && typeof obj === 'object' && Object.keys(obj).length > 0) { + return ( + Object.keys(obj) + .sort() + // tslint:disable-next-line: no-any + .reduce>((sortedObj, prop) => { + sortedObj[prop] = sortObjectPropertiesRecursively(obj[prop]); + return sortedObj; + // tslint:disable-next-line: no-any + }, {}) as any + ); + } + return obj; +} + +// Exported for test mocks +export class VSCodeNotebookModel extends BaseNotebookModel { + public get isDirty(): boolean { + return this.document?.isDirty === true; + } + public get cells(): ICell[] { + // Possible the document has been closed/disposed + if (this.isDisposed) { + return []; + } + + // When a notebook is not trusted, return original cells. + // This is because the VSCode NotebookDocument object will not have any output in the cells. + return this.document && this.isTrusted + ? this.document.cells.map((cell) => createCellFromVSCNotebookCell(cell, this)) + : this._cells; + } + public get isDisposed() { + // Possible the document has been closed/disposed + if ( + this.document && + this.vscodeNotebook && + !this.vscodeNotebook?.notebookDocuments.find((doc) => doc === this.document) + ) { + return true; + } + return this._isDisposed === true; + } + + private document?: NotebookDocument; + public get notebookContentWithoutCells(): Partial { + return { + ...this.notebookJson, + cells: [] + }; + } + public get isUntitled(): boolean { + return this.document ? this.document.isUntitled : super.isUntitled; + } + + constructor( + isTrusted: boolean, + file: Uri, + cells: ICell[], + globalMemento: Memento, + crypto: ICryptoUtils, + json: Partial = {}, + indentAmount: string = ' ', + pythonNumber: number = 3, + private readonly vscodeNotebook?: IVSCodeNotebook + ) { + super(isTrusted, file, cells, globalMemento, crypto, json, indentAmount, pythonNumber); + } + /** + * Unfortunately Notebook models are created early, well before a VSC Notebook Document is created. + * We can associate an INotebookModel with a VSC Notebook, only after the Notebook has been opened. + */ + public associateNotebookDocument(document: NotebookDocument) { + this.document = document; + } + public trust() { + super.trust(); + if (this.document) { + updateVSCNotebookAfterTrustingNotebook(this.document, this._cells); + // We don't need old cells. + this._cells = []; + } + } + protected generateNotebookJson() { + const json = super.generateNotebookJson(); + if (this.document && this.isTrusted) { + // The metadata will be in the notebook document. + const metadata = getNotebookMetadata(this.document); + if (metadata) { + json.metadata = metadata; + } + } + if (this.document && !this.isTrusted && Array.isArray(json.cells)) { + // The output can contain custom metadata, we need to remove that. + json.cells = json.cells.map((cell) => { + const metadata = { ...cell.metadata }; + if ('vscode' in metadata) { + delete metadata.vscode; + } + return { + ...cell, + metadata + // tslint:disable-next-line: no-any (because ts-node sucks). + } as any; + }); + } + + // https://github.com/microsoft/vscode-python/issues/13155 + // Object keys in metadata, cells and the like need to be sorted alphabetically. + // Jupyter (Python) seems to sort them alphabetically. + // We should do the same to minimize changes to content when saving ipynb. + return sortObjectPropertiesRecursively(json); + } + + protected handleRedo(change: NotebookModelChange): boolean { + super.handleRedo(change); + return true; + } +} diff --git a/src/client/datascience/plotting/plotViewer.ts b/src/client/datascience/plotting/plotViewer.ts new file mode 100644 index 000000000000..7e0119966e43 --- /dev/null +++ b/src/client/datascience/plotting/plotViewer.ts @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Event, EventEmitter, ViewColumn } from 'vscode'; + +import { traceInfo } from '../../../client/common/logger'; +import { createDeferred } from '../../../client/common/utils/async'; +import { IApplicationShell, IWebviewPanelProvider, IWorkspaceService } from '../../common/application/types'; +import { EXTENSION_ROOT_DIR, UseCustomEditorApi } from '../../common/constants'; +import { traceError } from '../../common/logger'; + +import { IConfigurationService, IDisposable } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { ICodeCssGenerator, IDataScienceFileSystem, IPlotViewer, IThemeFinder } from '../types'; +import { WebviewPanelHost } from '../webviews/webviewPanelHost'; +import { PlotViewerMessageListener } from './plotViewerMessageListener'; +import { IExportPlotRequest, IPlotViewerMapping, PlotViewerMessages } from './types'; + +const plotDir = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'viewers'); +@injectable() +export class PlotViewer extends WebviewPanelHost implements IPlotViewer, IDisposable { + private closedEvent: EventEmitter = new EventEmitter(); + private removedEvent: EventEmitter = new EventEmitter(); + + constructor( + @inject(IWebviewPanelProvider) provider: IWebviewPanelProvider, + @inject(IConfigurationService) configuration: IConfigurationService, + @inject(ICodeCssGenerator) cssGenerator: ICodeCssGenerator, + @inject(IThemeFinder) themeFinder: IThemeFinder, + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IApplicationShell) private applicationShell: IApplicationShell, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem, + @inject(UseCustomEditorApi) useCustomEditorApi: boolean + ) { + super( + configuration, + provider, + cssGenerator, + themeFinder, + workspaceService, + (c, v, d) => new PlotViewerMessageListener(c, v, d), + plotDir, + [path.join(plotDir, 'commons.initial.bundle.js'), path.join(plotDir, 'plotViewer.js')], + localize.DataScience.plotViewerTitle(), + ViewColumn.One, + useCustomEditorApi, + false, + Promise.resolve(false) + ); + // Load the web panel using our current directory as we don't expect to load any other files + super.loadWebPanel(process.cwd()).catch(traceError); + } + + public get closed(): Event { + return this.closedEvent.event; + } + + public get removed(): Event { + return this.removedEvent.event; + } + + public async show(): Promise { + if (!this.isDisposed) { + // Then show our web panel. + return super.show(true); + } + } + + public addPlot = async (imageHtml: string): Promise => { + if (!this.isDisposed) { + // Make sure we're shown + await super.show(false); + + // Send a message with our data + this.postMessage(PlotViewerMessages.SendPlot, imageHtml).ignoreErrors(); + } + }; + + public dispose() { + super.dispose(); + if (this.closedEvent) { + this.closedEvent.fire(this); + } + } + + protected get owningResource() { + return undefined; + } + + //tslint:disable-next-line:no-any + protected onMessage(message: string, payload: any) { + switch (message) { + case PlotViewerMessages.CopyPlot: + this.copyPlot(payload.toString()).ignoreErrors(); + break; + + case PlotViewerMessages.ExportPlot: + this.exportPlot(payload).ignoreErrors(); + break; + + case PlotViewerMessages.RemovePlot: + this.removePlot(payload); + break; + + default: + break; + } + + super.onMessage(message, payload); + } + + private removePlot(payload: number) { + this.removedEvent.fire(payload); + } + + private copyPlot(_svg: string): Promise { + // This should be handled actually in the web view. Leaving + // this here for now in case need node to handle it. + return Promise.resolve(); + } + + private async exportPlot(payload: IExportPlotRequest): Promise { + traceInfo('exporting plot...'); + const filtersObject: Record = {}; + filtersObject[localize.DataScience.pdfFilter()] = ['pdf']; + filtersObject[localize.DataScience.pngFilter()] = ['png']; + filtersObject[localize.DataScience.svgFilter()] = ['svg']; + + // Ask the user what file to save to + const file = await this.applicationShell.showSaveDialog({ + saveLabel: localize.DataScience.exportPlotTitle(), + filters: filtersObject + }); + try { + if (file) { + const ext = path.extname(file.fsPath); + switch (ext.toLowerCase()) { + case '.pdf': + traceInfo('Attempting pdf write...'); + // Import here since pdfkit is so huge. + // tslint:disable-next-line: no-require-imports + const SVGtoPDF = require('svg-to-pdfkit'); + const deferred = createDeferred(); + // tslint:disable-next-line: no-require-imports + const pdfkit = require('pdfkit/js/pdfkit.standalone') as typeof import('pdfkit'); + const doc = new pdfkit(); + const ws = this.fs.createLocalWriteStream(file.fsPath); + traceInfo(`Writing pdf to ${file.fsPath}`); + ws.on('finish', () => deferred.resolve); + // See docs or demo from source https://cdn.statically.io/gh/alafr/SVG-to-PDFKit/master/examples/demo.htm + // How to resize to fit (fit within the height & width of page). + SVGtoPDF(doc, payload.svg, 0, 0, { preserveAspectRatio: 'xMinYMin meet' }); + doc.pipe(ws); + doc.end(); + traceInfo(`Finishing pdf to ${file.fsPath}`); + await deferred.promise; + traceInfo(`Completed pdf to ${file.fsPath}`); + break; + + case '.png': + const buffer = new Buffer(payload.png.replace('data:image/png;base64', ''), 'base64'); + await this.fs.writeLocalFile(file.fsPath, buffer); + break; + + default: + case '.svg': + // This is the easy one: + await this.fs.writeLocalFile(file.fsPath, payload.svg); + break; + } + } + } catch (e) { + traceError(e); + this.applicationShell.showErrorMessage(localize.DataScience.exportImageFailed().format(e)); + } + } +} diff --git a/src/client/datascience/plotting/plotViewerMessageListener.ts b/src/client/datascience/plotting/plotViewerMessageListener.ts new file mode 100644 index 000000000000..2e4affa392cf --- /dev/null +++ b/src/client/datascience/plotting/plotViewerMessageListener.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { IWebviewPanel, IWebviewPanelMessageListener } from '../../common/application/types'; + +// tslint:disable:no-any + +// This class listens to messages that come from the local Plot Viewer window +export class PlotViewerMessageListener implements IWebviewPanelMessageListener { + private disposedCallback: () => void; + private callback: (message: string, payload: any) => void; + private viewChanged: (panel: IWebviewPanel) => void; + + constructor( + callback: (message: string, payload: any) => void, + viewChanged: (panel: IWebviewPanel) => void, + disposed: () => void + ) { + // Save our dispose callback so we remove our history window + this.disposedCallback = disposed; + + // Save our local callback so we can handle the non broadcast case(s) + this.callback = callback; + + // Save view changed so we can forward view change events. + this.viewChanged = viewChanged; + } + + public async dispose() { + this.disposedCallback(); + } + + public onMessage(message: string, payload: any) { + // Send to just our local callback. + this.callback(message, payload); + } + + public onChangeViewState(panel: IWebviewPanel) { + // Forward this onto our callback + if (this.viewChanged) { + this.viewChanged(panel); + } + } +} diff --git a/src/client/datascience/plotting/plotViewerProvider.ts b/src/client/datascience/plotting/plotViewerProvider.ts new file mode 100644 index 000000000000..57cd21195976 --- /dev/null +++ b/src/client/datascience/plotting/plotViewerProvider.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; + +import { IAsyncDisposable, IAsyncDisposableRegistry, IDisposable } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { IPlotViewer, IPlotViewerProvider } from '../types'; + +@injectable() +export class PlotViewerProvider implements IPlotViewerProvider, IAsyncDisposable { + private currentViewer: IPlotViewer | undefined; + private currentViewerClosed: IDisposable | undefined; + private imageList: string[] = []; + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry + ) { + asyncRegistry.push(this); + } + + public async dispose() { + if (this.currentViewer) { + this.currentViewer.dispose(); + } + } + + public async showPlot(imageHtml: string): Promise { + this.imageList.push(imageHtml); + // If the viewer closed, send it all of the old images + const imagesToSend = this.currentViewer ? [imageHtml] : this.imageList; + const viewer = await this.getOrCreate(); + await Promise.all(imagesToSend.map(viewer.addPlot)); + } + + private async getOrCreate(): Promise { + // Get or create a new plot viwer + if (!this.currentViewer) { + this.currentViewer = this.serviceContainer.get(IPlotViewer); + this.currentViewerClosed = this.currentViewer.closed(this.closedViewer); + this.currentViewer.removed(this.removedPlot); + sendTelemetryEvent(Telemetry.OpenPlotViewer); + await this.currentViewer.show(); + } + + return this.currentViewer; + } + + private closedViewer = () => { + if (this.currentViewer) { + this.currentViewer = undefined; + } + if (this.currentViewerClosed) { + this.currentViewerClosed.dispose(); + this.currentViewerClosed = undefined; + } + }; + + private removedPlot = (index: number) => { + this.imageList.splice(index, 1); + }; +} diff --git a/src/client/datascience/plotting/types.ts b/src/client/datascience/plotting/types.ts new file mode 100644 index 000000000000..d10491da37e5 --- /dev/null +++ b/src/client/datascience/plotting/types.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { CssMessages, IGetCssRequest, IGetCssResponse, SharedMessages } from '../messages'; + +export namespace PlotViewerMessages { + export const Started = SharedMessages.Started; + export const UpdateSettings = SharedMessages.UpdateSettings; + export const SendPlot = 'send_plot'; + export const CopyPlot = 'copy_plot'; + export const ExportPlot = 'export_plot'; + export const RemovePlot = 'remove_plot'; +} + +export interface IExportPlotRequest { + svg: string; + png: string; +} + +// Map all messages to specific payloads +export class IPlotViewerMapping { + public [PlotViewerMessages.Started]: never | undefined; + public [PlotViewerMessages.UpdateSettings]: string; + public [PlotViewerMessages.SendPlot]: string; + public [PlotViewerMessages.CopyPlot]: string; + public [PlotViewerMessages.ExportPlot]: IExportPlotRequest; + public [PlotViewerMessages.RemovePlot]: number; + public [CssMessages.GetCssRequest]: IGetCssRequest; + public [CssMessages.GetCssResponse]: IGetCssResponse; +} diff --git a/src/client/datascience/preWarmVariables.ts b/src/client/datascience/preWarmVariables.ts new file mode 100644 index 000000000000..9eaad2da74d2 --- /dev/null +++ b/src/client/datascience/preWarmVariables.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../activation/types'; +import '../common/extensions'; +import { IDisposableRegistry } from '../common/types'; +import { noop } from '../common/utils/misc'; +import { IEnvironmentActivationService } from '../interpreter/activation/types'; +import { JupyterInterpreterService } from './jupyter/interpreter/jupyterInterpreterService'; + +@injectable() +export class PreWarmActivatedJupyterEnvironmentVariables implements IExtensionSingleActivationService { + constructor( + @inject(IEnvironmentActivationService) private readonly activationService: IEnvironmentActivationService, + @inject(JupyterInterpreterService) private readonly jupyterInterpreterService: JupyterInterpreterService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry + ) {} + public async activate(): Promise { + this.disposables.push( + this.jupyterInterpreterService.onDidChangeInterpreter(() => this.preWarmInterpreterVariables().catch(noop)) + ); + this.preWarmInterpreterVariables().ignoreErrors(); + } + + private async preWarmInterpreterVariables() { + const interpreter = await this.jupyterInterpreterService.getSelectedInterpreter(); + if (!interpreter) { + return; + } + this.activationService.getActivatedEnvironmentVariables(undefined, interpreter).ignoreErrors(); + } +} diff --git a/src/client/datascience/progress/decorator.ts b/src/client/datascience/progress/decorator.ts new file mode 100644 index 000000000000..5f0215d08736 --- /dev/null +++ b/src/client/datascience/progress/decorator.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { traceError } from '../../common/logger'; +import { PromiseFunction } from '../types'; +import { IProgressReporter, Progress, ReportableAction } from './types'; + +const _reporters = new Set(); + +export function registerReporter(reporter: IProgressReporter) { + _reporters.add(reporter); +} + +export function disposeRegisteredReporters() { + _reporters.clear(); +} + +function report(progress: Progress) { + try { + _reporters.forEach((item) => item.report(progress)); + } catch (ex) { + traceError('Failed to report progress', ex); + } +} + +/** + * Reports a user reportable action. + * Action may be logged or displayed to the user depending on the registered listeners. + * + * @export + * @param {ReportableAction} action + * @returns + */ +export function reportAction(action: ReportableAction) { + return function (_target: Object, _propertyName: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value!; + // tslint:disable-next-line:no-any no-function-expression + descriptor.value = async function (...args: any[]) { + report({ action, phase: 'started' }); + // tslint:disable-next-line:no-invalid-this + return originalMethod.apply(this, args).finally(() => { + report({ action, phase: 'completed' }); + }); + }; + }; +} diff --git a/src/client/datascience/progress/messages.ts b/src/client/datascience/progress/messages.ts new file mode 100644 index 000000000000..89abfaecaf6f --- /dev/null +++ b/src/client/datascience/progress/messages.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { DataScience } from '../../common/utils/localize'; +import { ReportableAction } from './types'; + +const progressMessages = { + [ReportableAction.JupyterSessionWaitForIdleSession]: DataScience.waitingForJupyterSessionToBeIdle(), + [ReportableAction.KernelsGetKernelForLocalConnection]: DataScience.gettingListOfKernelsForLocalConnection(), + [ReportableAction.KernelsGetKernelForRemoteConnection]: DataScience.gettingListOfKernelsForRemoteConnection(), + [ReportableAction.KernelsGetKernelSpecs]: DataScience.gettingListOfKernelSpecs(), + [ReportableAction.KernelsRegisterKernel]: DataScience.registeringKernel(), + [ReportableAction.NotebookConnect]: DataScience.connectingToJupyter(), + [ReportableAction.NotebookStart]: DataScience.startingJupyterNotebook(), + [ReportableAction.RawKernelConnecting]: DataScience.rawKernelConnectingSession(), + [ReportableAction.CheckingIfImportIsSupported]: DataScience.checkingIfImportIsSupported(), // Localize these later + [ReportableAction.InstallingMissingDependencies]: DataScience.installingMissingDependencies(), + [ReportableAction.ExportNotebookToPython]: DataScience.exportNotebookToPython(), + [ReportableAction.PerformingExport]: DataScience.performingExport(), + [ReportableAction.ConvertingToPDF]: DataScience.convertingToPDF() +}; + +/** + * Given a reportable action, this will return the user friendly message. + * + * @export + * @param {ReportableAction} action + * @returns {(string | undefined)} + */ +export function getUserMessageForAction(action: ReportableAction): string | undefined { + return progressMessages[action]; +} diff --git a/src/client/datascience/progress/progressReporter.ts b/src/client/datascience/progress/progressReporter.ts new file mode 100644 index 000000000000..87ccadf9f450 --- /dev/null +++ b/src/client/datascience/progress/progressReporter.ts @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationToken, CancellationTokenSource, Progress as VSCProgress, ProgressLocation } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { IDisposable } from '../../common/types'; +import { createDeferred } from '../../common/utils/async'; +import { noop } from '../../common/utils/misc'; +import { registerReporter } from './decorator'; +import { getUserMessageForAction } from './messages'; +import { IProgressReporter, Progress, ReportableAction } from './types'; + +@injectable() +export class ProgressReporter implements IProgressReporter { + private progressReporters: VSCProgress<{ message?: string | undefined; increment?: number | undefined }>[] = []; + private actionPhases = new Map(); + private currentActions: ReportableAction[] = []; + private get currentAction(): ReportableAction | undefined { + return this.currentActions.length === 0 ? undefined : this.currentActions[this.currentActions.length - 1]; + } + + constructor(@inject(IApplicationShell) private readonly appShell: IApplicationShell) { + registerReporter(this); + } + + /** + * Create and display a progress indicator for starting of Jupyter Notebooks. + * + * @param {string} message + * @returns {(IDisposable & { token: CancellationToken })} + * @memberof ProgressReporter + */ + public createProgressIndicator(message: string, cancellable = false): IDisposable & { token: CancellationToken } { + const cancellation = new CancellationTokenSource(); + const deferred = createDeferred(); + const options = { location: ProgressLocation.Notification, cancellable: cancellable, title: message }; + + this.appShell + .withProgress(options, async (progress, cancelToken) => { + cancelToken.onCancellationRequested(() => { + if (cancelToken.isCancellationRequested) { + cancellation.cancel(); + } + deferred.resolve(); + }); + cancellation.token.onCancellationRequested(() => { + deferred.resolve(); + }); + this.progressReporters.push(progress); + await deferred.promise; + }) + .then(noop, noop); + + return { + token: cancellation.token, + dispose: () => deferred.resolve() + }; + } + + /** + * Reports progress to the user. + * Keep messages in a stack. As we have new actions taking place place them in a stack and notify progress. + * As they finish pop them from the stack (if currently displayed). + * We need a stack, as we have multiple async actions taking place, and each stat & can complete at different times. + * + * @param {Progress} progress + * @returns {void} + * @memberof JupyterStartupProgressReporter + */ + public report(progress: Progress): void { + if (this.progressReporters.length === 0) { + return; + } + this.actionPhases.set(progress.action, progress.phase); + + if (progress.phase === 'started') { + this.currentActions.push(progress.action); + } + + if (!this.currentAction) { + return; + } + + // If current action has been completed, then pop that item. + // Until we have an action that is still in progress + while (this.actionPhases.get(this.currentAction) && this.actionPhases.get(this.currentAction) !== 'started') { + this.actionPhases.delete(this.currentAction); + this.currentActions.pop(); + } + + this.updateProgressMessage(); + } + + private updateProgressMessage() { + if (!this.currentAction || this.progressReporters.length === 0) { + return; + } + const message = getUserMessageForAction(this.currentAction); + if (message) { + this.progressReporters.forEach((item) => item.report({ message })); + } + } +} diff --git a/src/client/datascience/progress/types.ts b/src/client/datascience/progress/types.ts new file mode 100644 index 000000000000..12f51a14f121 --- /dev/null +++ b/src/client/datascience/progress/types.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export type Progress = { action: ReportableAction; phase: 'started' | 'completed' }; +export interface IProgressReporter { + report(progress: Progress): void; +} + +/** + * Actions performed by extension that can be (potentially) reported to the user. + * + * @export + * @enum {number} + */ +export enum ReportableAction { + /** + * Getting kernels for a local connection. + * If not found, user may have to select or we might register a kernel. + */ + KernelsGetKernelForLocalConnection = 'KernelsStartGetKernelForLocalConnection', + /** + * Getting kernels for a remote connection. + * If not found, user may have to select. + */ + KernelsGetKernelForRemoteConnection = 'KernelsGetKernelForRemoteConnection', + /** + * Registering kernel. + */ + KernelsRegisterKernel = 'KernelsRegisterKernel', + /** + * Retrieving kernel specs. + */ + KernelsGetKernelSpecs = 'KernelsGetKernelSpecs', + /** + * Starting Jupyter Notebook & waiting to get connection information. + */ + NotebookStart = 'NotebookStart', + /** + * Connecting to the Jupyter Notebook. + */ + NotebookConnect = 'NotebookConnect', + /** + * Wait for session to go idle. + */ + JupyterSessionWaitForIdleSession = 'JupyterSessionWaitForIdleSession', + /** + * Connecting a raw kernel session + */ + RawKernelConnecting = 'RawKernelConnecting', + CheckingIfImportIsSupported = 'CheckingIfImportIsSupported', + InstallingMissingDependencies = 'InstallingMissingDependencies', + ExportNotebookToPython = 'ExportNotebookToPython', + PerformingExport = 'PerformingExport', + ConvertingToPDF = 'ConvertingToPDF' +} diff --git a/src/client/datascience/raw-kernel/liveshare/guestRawNotebookProvider.ts b/src/client/datascience/raw-kernel/liveshare/guestRawNotebookProvider.ts new file mode 100644 index 000000000000..61c1fe758b07 --- /dev/null +++ b/src/client/datascience/raw-kernel/liveshare/guestRawNotebookProvider.ts @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { nbformat } from '@jupyterlab/coreutils'; +import { Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import * as vsls from 'vsls/vscode'; +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; + +import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import { createDeferred } from '../../../common/utils/async'; +import * as localize from '../../../common/utils/localize'; +import { IServiceContainer } from '../../../ioc/types'; +import { LiveShare, LiveShareCommands } from '../../constants'; +import { GuestJupyterNotebook } from '../../jupyter/liveshare/guestJupyterNotebook'; +import { + LiveShareParticipantDefault, + LiveShareParticipantGuest +} from '../../jupyter/liveshare/liveShareParticipantMixin'; +import { ILiveShareParticipant } from '../../jupyter/liveshare/types'; +import { IDataScienceFileSystem, INotebook, IRawConnection, IRawNotebookProvider } from '../../types'; +import { RawConnection } from '../rawNotebookProvider'; + +export class GuestRawNotebookProvider + extends LiveShareParticipantGuest(LiveShareParticipantDefault, LiveShare.RawNotebookProviderService) + implements IRawNotebookProvider, ILiveShareParticipant { + // Keep track of guest notebooks on this side + private notebooks = new Map>(); + private rawConnection = new RawConnection(); + + constructor( + private readonly liveShare: ILiveShareApi, + private readonly startupTime: number, + private readonly disposableRegistry: IDisposableRegistry, + _asyncRegistry: IAsyncDisposableRegistry, + private readonly configService: IConfigurationService, + _workspaceService: IWorkspaceService, + _appShell: IApplicationShell, + _fs: IDataScienceFileSystem, + _serviceContainer: IServiceContainer + ) { + super(liveShare); + } + + public async supported(): Promise { + // Query the host to see if liveshare is supported + const service = await this.waitForService(); + let result = false; + if (service) { + result = await service.request(LiveShareCommands.rawKernelSupported, []); + } + + return result; + } + + public async createNotebook( + identity: Uri, + resource: Resource, + _disableUI: boolean, + notebookMetadata: nbformat.INotebookMetadata, + _cancelToken: CancellationToken + ): Promise { + // Remember we can have multiple native editors opened against the same ipynb file. + if (this.notebooks.get(identity.toString())) { + return this.notebooks.get(identity.toString())!; + } + + const deferred = createDeferred(); + this.notebooks.set(identity.toString(), deferred.promise); + // Tell the host side to generate a notebook for this uri + const service = await this.waitForService(); + if (service) { + const resourceString = resource ? resource.toString() : undefined; + const identityString = identity.toString(); + const notebookMetadataString = JSON.stringify(notebookMetadata); + await service.request(LiveShareCommands.createRawNotebook, [ + resourceString, + identityString, + notebookMetadataString + ]); + } + + // Return a new notebook to listen to + const result = new GuestJupyterNotebook( + this.liveShare, + this.disposableRegistry, + this.configService, + resource, + identity, + undefined, + this.startupTime + ); + deferred.resolve(result); + const oldDispose = result.dispose.bind(result); + result.dispose = () => { + this.notebooks.delete(identity.toString()); + return oldDispose(); + }; + + return result; + } + + public async connect(): Promise { + return Promise.resolve(this.rawConnection); + } + + public async onSessionChange(api: vsls.LiveShare | null): Promise { + await super.onSessionChange(api); + + this.notebooks.forEach(async (notebook) => { + const guestNotebook = (await notebook) as GuestJupyterNotebook; + if (guestNotebook) { + await guestNotebook.onSessionChange(api); + } + }); + } + + public async getNotebook(resource: Uri): Promise { + return this.notebooks.get(resource.toString()); + } + + public async onAttach(api: vsls.LiveShare | null): Promise { + await super.onAttach(api); + + if (api) { + const service = await this.waitForService(); + + // Wait for sync up + const synced = service ? await service.request(LiveShareCommands.syncRequest, []) : undefined; + if (!synced && api.session && api.session.role !== vsls.Role.None) { + throw new Error(localize.DataScience.liveShareSyncFailure()); + } + } + } + + public async waitForServiceName(): Promise { + return LiveShare.RawNotebookProviderService; + } +} diff --git a/src/client/datascience/raw-kernel/liveshare/hostRawNotebookProvider.ts b/src/client/datascience/raw-kernel/liveshare/hostRawNotebookProvider.ts new file mode 100644 index 000000000000..d26553cf8695 --- /dev/null +++ b/src/client/datascience/raw-kernel/liveshare/hostRawNotebookProvider.ts @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../../common/extensions'; + +import * as vscode from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import * as vsls from 'vsls/vscode'; + +import type { nbformat } from '@jupyterlab/coreutils'; +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; +import { traceError, traceInfo } from '../../../common/logger'; + +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + Resource +} from '../../../common/types'; +import { createDeferred } from '../../../common/utils/async'; +import * as localize from '../../../common/utils/localize'; +import { noop } from '../../../common/utils/misc'; +import { IServiceContainer } from '../../../ioc/types'; +import { Identifiers, LiveShare, LiveShareCommands, Settings } from '../../constants'; +import { computeWorkingDirectory } from '../../jupyter/jupyterUtils'; +import { KernelSelector } from '../../jupyter/kernels/kernelSelector'; +import { KernelConnectionMetadata } from '../../jupyter/kernels/types'; +import { HostJupyterNotebook } from '../../jupyter/liveshare/hostJupyterNotebook'; +import { LiveShareParticipantHost } from '../../jupyter/liveshare/liveShareParticipantMixin'; +import { IRoleBasedObject } from '../../jupyter/liveshare/roleBasedFactory'; +import { IKernelLauncher } from '../../kernel-launcher/types'; +import { ProgressReporter } from '../../progress/progressReporter'; +import { + IDataScienceFileSystem, + INotebook, + INotebookExecutionInfo, + INotebookExecutionLogger, + IRawNotebookProvider, + IRawNotebookSupportedService +} from '../../types'; +import { calculateWorkingDirectory } from '../../utils'; +import { RawJupyterSession } from '../rawJupyterSession'; +import { RawNotebookProviderBase } from '../rawNotebookProvider'; + +// tslint:disable-next-line: no-require-imports +// tslint:disable:no-any + +export class HostRawNotebookProvider + extends LiveShareParticipantHost(RawNotebookProviderBase, LiveShare.RawNotebookProviderService) + implements IRoleBasedObject, IRawNotebookProvider { + private disposed = false; + constructor( + private liveShare: ILiveShareApi, + _t: number, + private disposableRegistry: IDisposableRegistry, + asyncRegistry: IAsyncDisposableRegistry, + private configService: IConfigurationService, + private workspaceService: IWorkspaceService, + private appShell: IApplicationShell, + private fs: IDataScienceFileSystem, + private serviceContainer: IServiceContainer, + private kernelLauncher: IKernelLauncher, + private kernelSelector: KernelSelector, + private progressReporter: ProgressReporter, + private outputChannel: IOutputChannel, + rawNotebookSupported: IRawNotebookSupportedService + ) { + super(liveShare, asyncRegistry, rawNotebookSupported); + } + + public async dispose(): Promise { + if (!this.disposed) { + this.disposed = true; + await super.dispose(); + } + } + + public async onAttach(api: vsls.LiveShare | null): Promise { + await super.onAttach(api); + if (api && !this.disposed) { + const service = await this.waitForService(); + // Attach event handlers to different requests + if (service) { + service.onRequest(LiveShareCommands.syncRequest, (_args: any[], _cancellation: CancellationToken) => + this.onSync() + ); + service.onRequest( + LiveShareCommands.rawKernelSupported, + (_args: any[], _cancellation: CancellationToken) => this.supported() + ); + service.onRequest( + LiveShareCommands.createRawNotebook, + async (args: any[], _cancellation: CancellationToken) => { + const resource = this.parseUri(args[0]); + const identity = this.parseUri(args[1]); + const notebookMetadata = JSON.parse(args[2]) as nbformat.INotebookMetadata; + // Don't return the notebook. We don't want it to be serialized. We just want its live share server to be started. + const notebook = (await this.createNotebook( + identity!, + resource, + true, // Disable UI for this creation + notebookMetadata, + undefined + )) as HostJupyterNotebook; + await notebook.onAttach(api); + } + ); + } + } + } + + public async onSessionChange(api: vsls.LiveShare | null): Promise { + await super.onSessionChange(api); + + this.getNotebooks().forEach(async (notebook) => { + const hostNotebook = (await notebook) as HostJupyterNotebook; + if (hostNotebook) { + await hostNotebook.onSessionChange(api); + } + }); + } + + public async onDetach(api: vsls.LiveShare | null): Promise { + await super.onDetach(api); + } + + public async waitForServiceName(): Promise { + return LiveShare.RawNotebookProviderService; + } + + protected async createNotebookInstance( + resource: Resource, + identity: vscode.Uri, + disableUI?: boolean, + notebookMetadata?: nbformat.INotebookMetadata, + cancelToken?: CancellationToken + ): Promise { + const notebookPromise = createDeferred(); + this.setNotebook(identity, notebookPromise.promise); + + const progressReporter = !disableUI + ? this.progressReporter.createProgressIndicator(localize.DataScience.connectingIPyKernel()) + : undefined; + + const workingDirectory = await computeWorkingDirectory(resource, this.workspaceService); + + const rawSession = new RawJupyterSession( + this.kernelLauncher, + resource, + this.outputChannel, + noop, + noop, + workingDirectory + ); + try { + const launchTimeout = this.configService.getSettings().datascience.jupyterLaunchTimeout; + + // We need to locate kernelspec and possible interpreter for this launch based on resource and notebook metadata + const kernelConnectionMetadata = await this.kernelSelector.getPreferredKernelForLocalConnection( + resource, + 'raw', + undefined, + notebookMetadata, + disableUI, + cancelToken + ); + + // Interpreter is optional, but we must have a kernel spec for a raw launch if using a kernelspec + if ( + !kernelConnectionMetadata || + (!kernelConnectionMetadata?.kernelSpec && kernelConnectionMetadata?.kind === 'startUsingKernelSpec') + ) { + notebookPromise.reject('Failed to find a kernelspec to use for ipykernel launch'); + } else { + await rawSession.connect(kernelConnectionMetadata, launchTimeout, cancelToken); + + // Get the execution info for our notebook + const info = await this.getExecutionInfo(kernelConnectionMetadata); + + if (rawSession.isConnected) { + // Create our notebook + const notebook = new HostJupyterNotebook( + this.liveShare, + rawSession, + this.configService, + this.disposableRegistry, + info, + this.serviceContainer.getAll(INotebookExecutionLogger), + resource, + identity, + this.getDisposedError.bind(this), + this.workspaceService, + this.appShell, + this.fs + ); + + // Run initial setup + await notebook.initialize(cancelToken); + + traceInfo(`Finished connecting ${this.id}`); + + notebookPromise.resolve(notebook); + } else { + notebookPromise.reject(this.getDisposedError()); + } + } + } catch (ex) { + // Make sure we shut down our session in case we started a process + rawSession.dispose().catch((error) => { + traceError(`Failed to dispose of raw session on launch error: ${error} `); + }); + // If there's an error, then reject the promise that is returned. + // This original promise must be rejected as it is cached (check `setNotebook`). + notebookPromise.reject(ex); + } finally { + progressReporter?.dispose(); // NOSONAR + } + + return notebookPromise.promise; + } + + // Get the notebook execution info for this raw session instance + private async getExecutionInfo( + kernelConnectionMetadata: KernelConnectionMetadata + ): Promise { + return { + connectionInfo: this.getConnection(), + uri: Settings.JupyterServerLocalLaunch, + kernelConnectionMetadata, + workingDir: await calculateWorkingDirectory(this.configService, this.workspaceService, this.fs), + purpose: Identifiers.RawPurpose + }; + } + + private parseUri(uri: string | undefined): Resource { + const parsed = uri ? vscode.Uri.parse(uri) : undefined; + return parsed && + parsed.scheme && + parsed.scheme !== Identifiers.InteractiveWindowIdentityScheme && + parsed.scheme === 'vsls' + ? this.finishedApi!.convertSharedUriToLocal(parsed) + : parsed; + } + + private onSync(): Promise { + return Promise.resolve(true); + } +} diff --git a/src/client/datascience/raw-kernel/rawJupyterSession.ts b/src/client/datascience/raw-kernel/rawJupyterSession.ts new file mode 100644 index 000000000000..865403fbf245 --- /dev/null +++ b/src/client/datascience/raw-kernel/rawJupyterSession.ts @@ -0,0 +1,250 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { Kernel } from '@jupyterlab/services'; +import type { Slot } from '@phosphor/signaling'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { CancellationError, createPromiseFromCancellation } from '../../common/cancellation'; +import { traceError, traceInfo } from '../../common/logger'; +import { IDisposable, IOutputChannel, Resource } from '../../common/types'; +import { waitForPromise } from '../../common/utils/async'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { BaseJupyterSession } from '../baseJupyterSession'; +import { Identifiers, Telemetry } from '../constants'; +import { getDisplayNameOrNameOfKernelConnection } from '../jupyter/kernels/helpers'; +import { KernelConnectionMetadata } from '../jupyter/kernels/types'; +import { IKernelLauncher } from '../kernel-launcher/types'; +import { reportAction } from '../progress/decorator'; +import { ReportableAction } from '../progress/types'; +import { RawSession } from '../raw-kernel/rawSession'; +import { ISessionWithSocket } from '../types'; + +// Error thrown when we are unable to start a raw kernel session +export class RawKernelSessionStartError extends Error { + constructor(kernelConnection: KernelConnectionMetadata) { + super( + localize.DataScience.rawKernelSessionFailed().format( + getDisplayNameOrNameOfKernelConnection(kernelConnection) + ) + ); + } +} + +/* +RawJupyterSession is the implementation of IJupyterSession that instead of +connecting to JupyterLab services it instead connects to a kernel directly +through ZMQ. +It's responsible for translating our IJupyterSession interface into the +jupyterlabs interface as well as starting up and connecting to a raw session +*/ +export class RawJupyterSession extends BaseJupyterSession { + private processExitHandler: IDisposable | undefined; + private _disposables: IDisposable[] = []; + constructor( + private readonly kernelLauncher: IKernelLauncher, + private readonly resource: Resource, + private readonly outputChannel: IOutputChannel, + private readonly restartSessionCreated: (id: Kernel.IKernelConnection) => void, + restartSessionUsed: (id: Kernel.IKernelConnection) => void, + workingDirectory: string + ) { + super(restartSessionUsed, workingDirectory); + } + + @reportAction(ReportableAction.JupyterSessionWaitForIdleSession) + public async waitForIdle(timeout: number): Promise { + // Wait until status says idle. + if (this.session) { + return this.waitForIdleOnSession(this.session, timeout); + } + return Promise.resolve(); + } + public async dispose(): Promise { + this._disposables.forEach((d) => d.dispose()); + await super.dispose(); + } + + public shutdown(): Promise { + if (this.processExitHandler) { + this.processExitHandler.dispose(); + this.processExitHandler = undefined; + } + return super.shutdown(); + } + + // Connect to the given kernelspec, which should already have ipykernel installed into its interpreter + @captureTelemetry(Telemetry.RawKernelSessionConnect, undefined, true) + @reportAction(ReportableAction.RawKernelConnecting) + public async connect( + kernelConnection: KernelConnectionMetadata, + timeout: number, + cancelToken?: CancellationToken + ): Promise { + // Save the resource that we connect with + let newSession: RawSession | null | CancellationError = null; + try { + // Try to start up our raw session, allow for cancellation or timeout + // Notebook Provider level will handle the thrown error + newSession = await waitForPromise( + Promise.race([ + this.startRawSession(kernelConnection, cancelToken), + createPromiseFromCancellation({ + cancelAction: 'reject', + defaultValue: new CancellationError(), + token: cancelToken + }) + ]), + timeout + ); + + // Only connect our session if we didn't cancel or timeout + if (newSession instanceof CancellationError) { + sendTelemetryEvent(Telemetry.RawKernelSessionStartUserCancel); + traceInfo('Starting of raw session cancelled by user'); + throw newSession; + } else if (newSession === null) { + sendTelemetryEvent(Telemetry.RawKernelSessionStartTimeout); + traceError('Raw session failed to start in given timeout'); + throw new RawKernelSessionStartError(kernelConnection); + } else { + sendTelemetryEvent(Telemetry.RawKernelSessionStartSuccess); + traceInfo('Raw session started and connected'); + this.setSession(newSession); + + // Listen for session status changes + this.session?.statusChanged.connect(this.statusHandler); // NOSONAR + + // Update kernelspec and interpreter + this.kernelConnectionMetadata = newSession.kernelProcess?.kernelConnectionMetadata; + + this.outputChannel.appendLine( + localize.DataScience.kernelStarted().format( + getDisplayNameOrNameOfKernelConnection(this.kernelConnectionMetadata) + ) + ); + } + } catch (error) { + // Send our telemetry event with the error included + sendTelemetryEvent(Telemetry.RawKernelSessionStartException, undefined, undefined, error); + traceError(`Failed to connect raw kernel session: ${error}`); + this.connected = false; + throw error; + } + + this.connected = true; + return (newSession as RawSession).kernelProcess.kernelConnectionMetadata; + } + + public async createNewKernelSession( + kernelConnection: KernelConnectionMetadata, + timeoutMS: number, + _cancelToken?: CancellationToken + ): Promise { + if (!kernelConnection || 'session' in kernelConnection) { + // Don't allow for connecting to a LiveKernelModel + throw new Error(localize.DataScience.sessionDisposed()); + } + + const displayName = getDisplayNameOrNameOfKernelConnection(kernelConnection); + this.outputChannel.appendLine(localize.DataScience.kernelStarted().format(displayName)); + + const newSession = await waitForPromise(this.startRawSession(kernelConnection), timeoutMS); + + if (!newSession) { + throw new RawKernelSessionStartError(kernelConnection); + } + + return newSession; + } + + protected shutdownSession( + session: ISessionWithSocket | undefined, + statusHandler: Slot | undefined + ): Promise { + return super.shutdownSession(session, statusHandler).then(() => { + if (session) { + return (session as RawSession).kernelProcess.dispose(); + } + }); + } + + protected setSession(session: ISessionWithSocket | undefined) { + super.setSession(session); + + // When setting the session clear our current exit handler and hook up to the + // new session process + if (this.processExitHandler) { + this.processExitHandler.dispose(); + this.processExitHandler = undefined; + } + if (session && (session as RawSession).kernelProcess) { + // Watch to see if our process exits + this.processExitHandler = (session as RawSession).kernelProcess.exited((exitCode) => { + traceError(`Raw kernel process exited code: ${exitCode}`); + this.shutdown().catch((reason) => { + traceError(`Error shutting down jupyter session: ${reason}`); + }); + // Next code the user executes will show a session disposed message + }); + } + } + + protected startRestartSession() { + if (!this.restartSessionPromise && this.session) { + this.restartSessionPromise = this.createRestartSession(this.kernelConnectionMetadata, this.session); + } + } + protected async createRestartSession( + kernelConnection: KernelConnectionMetadata | undefined, + _session: ISessionWithSocket, + cancelToken?: CancellationToken + ): Promise { + if (!kernelConnection || kernelConnection.kind === 'connectToLiveKernel') { + // Need to have connected before restarting and can't use a LiveKernelModel + throw new Error(localize.DataScience.sessionDisposed()); + } + const startPromise = this.startRawSession(kernelConnection, cancelToken); + return startPromise.then((session) => { + this.restartSessionCreated(session.kernel); + return session; + }); + } + + @captureTelemetry(Telemetry.RawKernelStartRawSession, undefined, true) + private async startRawSession( + kernelConnection: KernelConnectionMetadata, + cancelToken?: CancellationToken + ): Promise { + if ( + kernelConnection.kind !== 'startUsingKernelSpec' && + kernelConnection.kind !== 'startUsingPythonInterpreter' + ) { + throw new Error(`Unable to start Raw Kernels for Kernel Connection of type ${kernelConnection.kind}`); + } + const cancellationPromise = createPromiseFromCancellation({ + cancelAction: 'reject', + defaultValue: undefined, + token: cancelToken + }) as Promise; + cancellationPromise.catch(noop); + + const process = await Promise.race([ + this.kernelLauncher.launch(kernelConnection, this.resource, this.workingDirectory), + cancellationPromise + ]); + + // Create our raw session, it will own the process lifetime + const result = new RawSession(process); + + // When our kernel connects and gets a status message it triggers the ready promise + await result.kernel.ready; + + // So that we don't have problems with ipywidgets, always register the default ipywidgets comm target. + // Restart sessions and retries might make this hard to do correctly otherwise. + result.kernel.registerCommTarget(Identifiers.DefaultCommTarget, noop); + + return result; + } +} diff --git a/src/client/datascience/raw-kernel/rawKernel.ts b/src/client/datascience/raw-kernel/rawKernel.ts new file mode 100644 index 000000000000..b80c81646b81 --- /dev/null +++ b/src/client/datascience/raw-kernel/rawKernel.ts @@ -0,0 +1,275 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import type { Kernel, KernelMessage, ServerConnection } from '@jupyterlab/services'; +import * as uuid from 'uuid/v4'; +import { isTestExecution } from '../../common/constants'; +import { IDisposable } from '../../common/types'; +import { swallowExceptions } from '../../common/utils/misc'; +import { getNameOfKernelConnection } from '../jupyter/kernels/helpers'; +import { IKernelProcess } from '../kernel-launcher/types'; +import { IWebSocketLike } from '../kernelSocketWrapper'; +import { IKernelSocket } from '../types'; +import { RawSocket } from './rawSocket'; +// tslint:disable: no-any no-require-imports + +export function suppressShutdownErrors(realKernel: any) { + // When running under a test, mark all futures as done so we + // don't hit this problem: + // https://github.com/jupyterlab/jupyterlab/issues/4252 + // tslint:disable:no-any + if (isTestExecution()) { + const defaultKernel = realKernel as any; // NOSONAR + if (defaultKernel && defaultKernel._futures) { + const futures = defaultKernel._futures as Map; // NOSONAR + if (futures) { + futures.forEach((f) => { + if (f._status !== undefined) { + f._status |= 4; + } + }); + } + } + if (defaultKernel && defaultKernel._reconnectLimit) { + defaultKernel._reconnectLimit = 0; + } + } +} + +/* +RawKernel class represents the mapping from the JupyterLab services IKernel interface +to a raw IPython kernel running on the local machine. RawKernel is in charge of taking +input request, translating them, sending them to an IPython kernel over ZMQ, then passing back the messages +*/ +export class RawKernel implements Kernel.IKernel { + public socket: IKernelSocket & IDisposable; + public get terminated() { + return this.realKernel.terminated as any; // NOSONAR + } + public get statusChanged() { + return this.realKernel.statusChanged as any; // NOSONAR + } + public get iopubMessage() { + return this.realKernel.iopubMessage as any; // NOSONAR + } + public get unhandledMessage() { + return this.realKernel.unhandledMessage as any; // NOSONAR + } + public get anyMessage() { + return this.realKernel.anyMessage as any; // NOSONAR + } + public get serverSettings(): ServerConnection.ISettings { + return this.realKernel.serverSettings; + } + public get id(): string { + return this.realKernel.id; + } + public get name(): string { + return this.realKernel.name; + } + public get model(): Kernel.IModel { + return this.realKernel.model; + } + public get username(): string { + return this.realKernel.username; + } + public get clientId(): string { + return this.realKernel.clientId; + } + public get status(): Kernel.Status { + return this.realKernel.status; + } + public get info(): KernelMessage.IInfoReply | null { + return this.realKernel.info; + } + public get isReady(): boolean { + return this.realKernel.isReady; + } + public get ready(): Promise { + return this.realKernel.ready; + } + public get handleComms(): boolean { + return this.realKernel.handleComms; + } + public get isDisposed(): boolean { + return this.realKernel.isDisposed; + } + constructor( + private realKernel: Kernel.IKernel, + socket: IKernelSocket & IWebSocketLike & IDisposable, + private kernelProcess: IKernelProcess + ) { + // Save this raw socket as our kernel socket. It will be + // used to watch and respond to kernel messages. + this.socket = socket; + + // Pretend like an open occurred. This will prime the real kernel to be connected + socket.emit('open'); + } + + public async shutdown(): Promise { + suppressShutdownErrors(this.realKernel); + await this.kernelProcess.dispose(); + this.socket.dispose(); + } + public getSpec(): Promise { + return this.realKernel.getSpec(); + } + public sendShellMessage( + msg: KernelMessage.IShellMessage, + expectReply?: boolean, + disposeOnDone?: boolean + ): Kernel.IShellFuture< + KernelMessage.IShellMessage, + KernelMessage.IShellMessage + > { + return this.realKernel.sendShellMessage(msg, expectReply, disposeOnDone); + } + public sendControlMessage( + msg: KernelMessage.IControlMessage, + expectReply?: boolean, + disposeOnDone?: boolean + ): Kernel.IControlFuture< + KernelMessage.IControlMessage, + KernelMessage.IControlMessage + > { + return this.realKernel.sendControlMessage(msg, expectReply, disposeOnDone); + } + public reconnect(): Promise { + throw new Error('Reconnect is not supported.'); + } + public interrupt(): Promise { + // Send this directly to our kernel process. Don't send it through the real kernel. The + // real kernel will send a goofy API request to the websocket. + return this.kernelProcess.interrupt(); + } + public restart(): Promise { + throw new Error('This method should not be called. Restart is implemented at a higher level'); + } + public requestKernelInfo(): Promise { + return this.realKernel.requestKernelInfo(); + } + public requestComplete(content: { code: string; cursor_pos: number }): Promise { + return this.realKernel.requestComplete(content); + } + public requestInspect(content: { + code: string; + cursor_pos: number; + detail_level: 0 | 1; + }): Promise { + return this.realKernel.requestInspect(content); + } + public requestHistory( + content: + | KernelMessage.IHistoryRequestRange + | KernelMessage.IHistoryRequestSearch + | KernelMessage.IHistoryRequestTail + ): Promise { + return this.realKernel.requestHistory(content); + } + public requestExecute( + content: { + code: string; + silent?: boolean; + store_history?: boolean; + user_expressions?: import('@phosphor/coreutils').JSONObject; + allow_stdin?: boolean; + stop_on_error?: boolean; + }, + disposeOnDone?: boolean, + metadata?: import('@phosphor/coreutils').JSONObject + ): Kernel.IShellFuture { + return this.realKernel.requestExecute(content, disposeOnDone, metadata); + } + public requestDebug( + // tslint:disable-next-line: no-banned-terms + content: { seq: number; type: 'request'; command: string; arguments?: any }, + disposeOnDone?: boolean + ): Kernel.IControlFuture { + return this.realKernel.requestDebug(content, disposeOnDone); + } + public requestIsComplete(content: { code: string }): Promise { + return this.realKernel.requestIsComplete(content); + } + public requestCommInfo(content: { + target_name?: string; + target?: string; + }): Promise { + return this.realKernel.requestCommInfo(content); + } + public sendInputReply(content: KernelMessage.ReplyContent): void { + return this.realKernel.sendInputReply(content); + } + public connectToComm(targetName: string, commId?: string): Kernel.IComm { + return this.realKernel.connectToComm(targetName, commId); + } + public registerCommTarget( + targetName: string, + callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike + ): void { + return this.realKernel.registerCommTarget(targetName, callback); + } + public removeCommTarget( + targetName: string, + callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike + ): void { + return this.realKernel.removeCommTarget(targetName, callback); + } + public dispose(): void { + swallowExceptions(() => this.realKernel.dispose()); + swallowExceptions(() => this.socket.dispose()); + } + public registerMessageHook( + msgId: string, + hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void { + this.realKernel.registerMessageHook(msgId, hook); + } + public removeMessageHook( + msgId: string, + hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void { + this.realKernel.removeMessageHook(msgId, hook); + } +} + +let nonSerializingKernel: any; + +export function createRawKernel(kernelProcess: IKernelProcess, clientId: string): RawKernel { + const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); // NOSONAR + const jupyterLabSerialize = require('@jupyterlab/services/lib/kernel/serialize') as typeof import('@jupyterlab/services/lib/kernel/serialize'); // NOSONAR + + // Dummy websocket we give to the underlying real kernel + let socketInstance: any; + class RawSocketWrapper extends RawSocket { + constructor() { + super(kernelProcess.connection, jupyterLabSerialize.serialize, jupyterLabSerialize.deserialize); + socketInstance = this; + } + } + + // Remap the server settings for the real kernel to use our dummy websocket + const settings = jupyterLab.ServerConnection.makeSettings({ + WebSocket: RawSocketWrapper as any, // NOSONAR + wsUrl: 'RAW' + }); + + // Then create the real kernel. We will remap its serialize/deserialize functions + // to do nothing so that we can control serialization at our socket layer. + if (!nonSerializingKernel) { + // Note, this is done with a postInstall step (found in build\ci\postInstall.js). In that post install step + // we eliminate the serialize import from the default kernel and remap it to do nothing. + nonSerializingKernel = require('@jupyterlab/services/lib/kernel/nonSerializingKernel') as typeof import('@jupyterlab/services/lib/kernel/default'); // NOSONAR + } + const realKernel = new nonSerializingKernel.DefaultKernel( + { + name: getNameOfKernelConnection(kernelProcess.kernelConnectionMetadata), + serverSettings: settings, + clientId, + handleComms: true + }, + uuid() + ); + + // Use this real kernel in result. + return new RawKernel(realKernel, socketInstance, kernelProcess); +} diff --git a/src/client/datascience/raw-kernel/rawNotebookProvider.ts b/src/client/datascience/raw-kernel/rawNotebookProvider.ts new file mode 100644 index 000000000000..559f6cc8bed7 --- /dev/null +++ b/src/client/datascience/raw-kernel/rawNotebookProvider.ts @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as uuid from 'uuid/v4'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { ILiveShareApi } from '../../common/application/types'; +import '../../common/extensions'; +import { traceInfo } from '../../common/logger'; +import { IAsyncDisposableRegistry, Resource } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { captureTelemetry } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { + ConnectNotebookProviderOptions, + INotebook, + INotebookMetadataLive, + IRawConnection, + IRawNotebookProvider, + IRawNotebookSupportedService +} from '../types'; + +export class RawConnection implements IRawConnection { + public readonly type = 'raw'; + public readonly localLaunch = true; + public readonly valid = true; + public readonly displayName = localize.DataScience.rawConnectionDisplayName(); + private eventEmitter: EventEmitter = new EventEmitter(); + + public dispose() { + noop(); + } + public get disconnected(): Event { + return this.eventEmitter.event; + } +} + +export class RawNotebookProviderBase implements IRawNotebookProvider { + public get id(): string { + return this._id; + } + // Keep track of the notebooks that we have provided + private notebooks = new Map>(); + private rawConnection: IRawConnection | undefined; + private _id = uuid(); + + constructor( + _liveShare: ILiveShareApi, + private asyncRegistry: IAsyncDisposableRegistry, + private rawNotebookSupportedService: IRawNotebookSupportedService + ) { + this.asyncRegistry.push(this); + } + + public connect(options: ConnectNotebookProviderOptions): Promise { + // For getOnly, we don't want to create a connection, even though we don't have a server + // here we only want to be "connected" when requested to mimic jupyter server function + if (options.getOnly) { + return Promise.resolve(this.rawConnection); + } + + // If not get only, create if needed and return + if (!this.rawConnection) { + this.rawConnection = new RawConnection(); + + // Fire our optional event that we have created a connection + if (options.onConnectionMade) { + options.onConnectionMade(); + } + } + return Promise.resolve(this.rawConnection); + } + + // Check to see if we have all that we need for supporting raw kernel launch + public async supported(): Promise { + return this.rawNotebookSupportedService.supported(); + } + + @captureTelemetry(Telemetry.RawKernelCreatingNotebook, undefined, true) + public async createNotebook( + identity: Uri, + resource: Resource, + disableUI: boolean, + notebookMetadata: INotebookMetadataLive, + cancelToken?: CancellationToken + ): Promise { + return this.createNotebookInstance(resource, identity, disableUI, notebookMetadata, cancelToken); + } + + public async getNotebook(identity: Uri): Promise { + return this.notebooks.get(identity.toString()); + } + + public async dispose(): Promise { + traceInfo(`Shutting down notebooks for ${this.id}`); + const notebooks = await Promise.all([...this.notebooks.values()]); + await Promise.all(notebooks.map((n) => n?.dispose())); + } + + // This may be a bit of a noop in the raw case + public getDisposedError(): Error { + return new Error(localize.DataScience.rawConnectionBrokenError()); + } + + protected getNotebooks(): Promise[] { + return [...this.notebooks.values()]; + } + + protected getConnection(): IRawConnection { + // At the time of getConnection force a connection if not created already + // should always have happened already, but the check here lets us avoid returning undefined option + if (!this.rawConnection) { + this.rawConnection = new RawConnection(); + } + return this.rawConnection; + } + + protected setNotebook(identity: Uri, notebook: Promise) { + const removeNotebook = () => { + if (this.notebooks.get(identity.toString()) === notebook) { + this.notebooks.delete(identity.toString()); + } + }; + + notebook + .then((nb) => { + const oldDispose = nb.dispose; + nb.dispose = () => { + this.notebooks.delete(identity.toString()); + return oldDispose(); + }; + }) + .catch(removeNotebook); + + // Save the notebook + this.notebooks.set(identity.toString(), notebook); + } + + protected createNotebookInstance( + _resource: Resource, + _identity: Uri, + _disableUI?: boolean, + _notebookMetadata?: INotebookMetadataLive, + _cancelToken?: CancellationToken + ): Promise { + throw new Error('You forgot to override createNotebookInstance'); + } +} diff --git a/src/client/datascience/raw-kernel/rawNotebookProviderWrapper.ts b/src/client/datascience/raw-kernel/rawNotebookProviderWrapper.ts new file mode 100644 index 000000000000..e37591626fc7 --- /dev/null +++ b/src/client/datascience/raw-kernel/rawNotebookProviderWrapper.ts @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { nbformat } from '@jupyterlab/coreutils'; +import { inject, injectable, named } from 'inversify'; +import { Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import * as vsls from 'vsls/vscode'; +import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../common/application/types'; +import '../../common/extensions'; + +import { + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + Resource +} from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { DataScienceStartupTime, JUPYTER_OUTPUT_CHANNEL } from '../constants'; +import { KernelSelector } from '../jupyter/kernels/kernelSelector'; +import { IRoleBasedObject, RoleBasedFactory } from '../jupyter/liveshare/roleBasedFactory'; +import { ILiveShareHasRole } from '../jupyter/liveshare/types'; +import { IKernelLauncher } from '../kernel-launcher/types'; +import { ProgressReporter } from '../progress/progressReporter'; +import { + ConnectNotebookProviderOptions, + IDataScienceFileSystem, + INotebook, + IRawConnection, + IRawNotebookProvider, + IRawNotebookSupportedService +} from '../types'; +import { GuestRawNotebookProvider } from './liveshare/guestRawNotebookProvider'; +import { HostRawNotebookProvider } from './liveshare/hostRawNotebookProvider'; + +interface IRawNotebookProviderInterface extends IRoleBasedObject, IRawNotebookProvider {} + +// tslint:disable:callable-types +type RawNotebookProviderClassType = { + new ( + liveShare: ILiveShareApi, + startupTime: number, + disposableRegistry: IDisposableRegistry, + asyncRegistry: IAsyncDisposableRegistry, + configService: IConfigurationService, + workspaceService: IWorkspaceService, + appShell: IApplicationShell, + fs: IDataScienceFileSystem, + serviceContainer: IServiceContainer, + kernelLauncher: IKernelLauncher, + kernelSelector: KernelSelector, + progressReporter: ProgressReporter, + outputChannel: IOutputChannel, + rawKernelSupported: IRawNotebookSupportedService + ): IRawNotebookProviderInterface; +}; +// tslint:enable:callable-types + +// This class wraps either a HostRawNotebookProvider or a GuestRawNotebookProvider based on the liveshare state. It abstracts +// out the live share specific parts. +@injectable() +export class RawNotebookProviderWrapper implements IRawNotebookProvider, ILiveShareHasRole { + private serverFactory: RoleBasedFactory; + + constructor( + @inject(ILiveShareApi) liveShare: ILiveShareApi, + @inject(DataScienceStartupTime) startupTime: number, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, + @inject(IConfigurationService) configService: IConfigurationService, + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IApplicationShell) appShell: IApplicationShell, + @inject(IDataScienceFileSystem) fs: IDataScienceFileSystem, + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IKernelLauncher) kernelLauncher: IKernelLauncher, + @inject(KernelSelector) kernelSelector: KernelSelector, + @inject(ProgressReporter) progressReporter: ProgressReporter, + @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) outputChannel: IOutputChannel, + @inject(IRawNotebookSupportedService) rawNotebookSupported: IRawNotebookSupportedService + ) { + // The server factory will create the appropriate HostRawNotebookProvider or GuestRawNotebookProvider based on + // the liveshare state. + this.serverFactory = new RoleBasedFactory( + liveShare, + HostRawNotebookProvider, + GuestRawNotebookProvider, + liveShare, + startupTime, + disposableRegistry, + asyncRegistry, + configService, + workspaceService, + appShell, + fs, + serviceContainer, + kernelLauncher, + kernelSelector, + progressReporter, + outputChannel, + rawNotebookSupported + ); + } + + public get role(): vsls.Role { + return this.serverFactory.role; + } + + public async supported(): Promise { + const notebookProvider = await this.serverFactory.get(); + return notebookProvider.supported(); + } + + public async connect(options: ConnectNotebookProviderOptions): Promise { + const notebookProvider = await this.serverFactory.get(); + return notebookProvider.connect(options); + } + + public async createNotebook( + identity: Uri, + resource: Resource, + disableUI: boolean, + notebookMetadata: nbformat.INotebookMetadata, + cancelToken: CancellationToken + ): Promise { + const notebookProvider = await this.serverFactory.get(); + return notebookProvider.createNotebook(identity, resource, disableUI, notebookMetadata, cancelToken); + } + + public async getNotebook(identity: Uri): Promise { + const notebookProvider = await this.serverFactory.get(); + return notebookProvider.getNotebook(identity); + } + + public async dispose(): Promise { + const server = await this.serverFactory.get(); + return server.dispose(); + } +} diff --git a/src/client/datascience/raw-kernel/rawNotebookSupportedService.ts b/src/client/datascience/raw-kernel/rawNotebookSupportedService.ts new file mode 100644 index 000000000000..5c9cd416f156 --- /dev/null +++ b/src/client/datascience/raw-kernel/rawNotebookSupportedService.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { LocalZMQKernel } from '../../common/experiments/groups'; +import { traceError, traceInfo } from '../../common/logger'; +import { IConfigurationService, IExperimentsManager } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Settings, Telemetry } from '../constants'; +import { IRawNotebookSupportedService } from '../types'; + +// This class check to see if we have everything in place to support a raw kernel launch on the machine +@injectable() +export class RawNotebookSupportedService implements IRawNotebookSupportedService { + // Keep track of our ZMQ import check, this doesn't change with settings so we only want to do this once + private _zmqSupportedPromise: Promise | undefined; + + constructor( + @inject(IConfigurationService) private readonly configuration: IConfigurationService, + @inject(IExperimentsManager) private readonly experimentsManager: IExperimentsManager + ) {} + + // Check to see if we have all that we need for supporting raw kernel launch + public async supported(): Promise { + // Save the ZMQ support for last, since it's probably the slowest part + return this.localLaunch() && this.experimentEnabled() && (await this.zmqSupported()) ? true : false; + } + + private localLaunch(): boolean { + const settings = this.configuration.getSettings(undefined); + const serverURI: string | undefined = settings.datascience.jupyterServerURI; + + if (!serverURI || serverURI.toLowerCase() === Settings.JupyterServerLocalLaunch) { + return true; + } + + return false; + } + + // Enable if we are in our experiment or in the insiders channel + private experimentEnabled(): boolean { + return ( + this.experimentsManager.inExperiment(LocalZMQKernel.experiment) || + (this.configuration.getSettings().insidersChannel && + this.configuration.getSettings().insidersChannel !== 'off') + ); + } + + // Check to see if this machine supports our local ZMQ launching + private async zmqSupported(): Promise { + if (!this._zmqSupportedPromise) { + this._zmqSupportedPromise = this.zmqSupportedImpl(); + } + + return this._zmqSupportedPromise; + } + + private async zmqSupportedImpl(): Promise { + try { + await import('zeromq'); + traceInfo(`ZMQ install verified.`); + sendTelemetryEvent(Telemetry.ZMQSupported); + } catch (e) { + traceError(`Exception while attempting zmq :`, e); + sendTelemetryEvent(Telemetry.ZMQNotSupported); + return false; + } + + return true; + } +} diff --git a/src/client/datascience/raw-kernel/rawSession.ts b/src/client/datascience/raw-kernel/rawSession.ts new file mode 100644 index 000000000000..5f0a79d2c0ec --- /dev/null +++ b/src/client/datascience/raw-kernel/rawSession.ts @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import type { Kernel, KernelMessage, ServerConnection, Session } from '@jupyterlab/services'; +import type { ISignal, Signal } from '@phosphor/signaling'; +import * as uuid from 'uuid/v4'; +import '../../common/extensions'; +import { traceError } from '../../common/logger'; +import { IDisposable } from '../../common/types'; +import { IKernelProcess } from '../kernel-launcher/types'; +import { ISessionWithSocket, KernelSocketInformation } from '../types'; +import { createRawKernel, RawKernel } from './rawKernel'; + +/* +RawSession class implements a jupyterlab ISession object +This provides enough of the ISession interface so that our direct +ZMQ Kernel connection can pretend to be a jupyterlab Session +*/ +export class RawSession implements ISessionWithSocket { + public isDisposed: boolean = false; + private isDisposing?: boolean; + + // Note, ID is the ID of this session + // ClientID is the ID that we pass in messages to the kernel + // and is also the clientID of the active kernel + private _id: string; + private _clientID: string; + private _kernel: RawKernel; + private readonly _statusChanged: Signal; + private readonly _kernelChanged: Signal; + private readonly _ioPubMessage: Signal; + private readonly exitHandler: IDisposable; + + // RawSession owns the lifetime of the kernel process and will dispose it + constructor(public kernelProcess: IKernelProcess) { + // tslint:disable-next-line: no-require-imports + const signaling = require('@phosphor/signaling') as typeof import('@phosphor/signaling'); + this._statusChanged = new signaling.Signal(this); + this._kernelChanged = new signaling.Signal(this); + this._ioPubMessage = new signaling.Signal(this); + // Unique ID for this session instance + this._id = uuid(); + + // ID for our client JMP connection + this._clientID = uuid(); + + // Connect our kernel and hook up status changes + this._kernel = createRawKernel(kernelProcess, this._clientID); + this._kernel.statusChanged.connect(this.onKernelStatus, this); + this._kernel.iopubMessage.connect(this.onIOPubMessage, this); + this.exitHandler = kernelProcess.exited(this.handleUnhandledExitingOfKernelProcess, this); + } + + public async dispose() { + this.isDisposing = true; + if (!this.isDisposed) { + this.exitHandler.dispose(); + await this._kernel.shutdown(); + this._kernel.dispose(); + this.kernelProcess.dispose().ignoreErrors(); + } + this.isDisposed = true; + } + + // Return the ID, this is session's ID, not clientID for messages + get id(): string { + return this._id; + } + + // Return the current kernel for this session + get kernel(): Kernel.IKernelConnection { + return this._kernel; + } + + get kernelSocketInformation(): KernelSocketInformation | undefined { + return { + socket: this._kernel.socket, + options: { + id: this._kernel.id, + clientId: this._clientID, + userName: '', + model: this._kernel.model + } + }; + } + + // Provide status changes for the attached kernel + get statusChanged(): ISignal { + return this._statusChanged; + } + + // Shutdown our session and kernel + public shutdown(): Promise { + return this.dispose(); + } + + // Not Implemented ISession + get terminated(): ISignal { + throw new Error('Not yet implemented'); + } + get kernelChanged(): ISignal { + return this._kernelChanged; + } + get propertyChanged(): ISignal { + throw new Error('Not yet implemented'); + } + get iopubMessage(): ISignal { + return this._ioPubMessage; + } + get unhandledMessage(): ISignal { + throw new Error('Not yet implemented'); + } + get anyMessage(): ISignal { + throw new Error('Not yet implemented'); + } + get path(): string { + throw new Error('Not yet implemented'); + } + get name(): string { + throw new Error('Not yet implemented'); + } + get type(): string { + throw new Error('Not yet implemented'); + } + get serverSettings(): ServerConnection.ISettings { + throw new Error('Not yet implemented'); + } + get model(): Session.IModel { + throw new Error('Not yet implemented'); + } + get status(): Kernel.Status { + throw new Error('Not yet implemented'); + } + public setPath(_path: string): Promise { + throw new Error('Not yet implemented'); + } + public setName(_name: string): Promise { + throw new Error('Not yet implemented'); + } + public setType(_type: string): Promise { + throw new Error('Not yet implemented'); + } + public changeKernel(_options: Partial): Promise { + throw new Error('Not yet implemented'); + } + + // Private + // Send out a message when our kernel changes state + private onKernelStatus(_sender: Kernel.IKernelConnection, state: Kernel.Status) { + this._statusChanged.emit(state); + } + private onIOPubMessage(_sender: Kernel.IKernelConnection, msg: KernelMessage.IIOPubMessage) { + this._ioPubMessage.emit(msg); + } + private handleUnhandledExitingOfKernelProcess(e: { exitCode?: number | undefined; reason?: string | undefined }) { + if (this.isDisposing) { + return; + } + traceError(`Disposing session as kernel process died ExitCode: ${e.exitCode}, Reason: ${e.reason}`); + // Just kill the session. + this.dispose().ignoreErrors(); + } +} diff --git a/src/client/datascience/raw-kernel/rawSocket.ts b/src/client/datascience/raw-kernel/rawSocket.ts new file mode 100644 index 000000000000..c592f35bcade --- /dev/null +++ b/src/client/datascience/raw-kernel/rawSocket.ts @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import type { KernelMessage } from '@jupyterlab/services'; +import * as wireProtocol from '@nteract/messaging/lib/wire-protocol'; +import { IDisposable } from 'monaco-editor'; +import * as uuid from 'uuid/v4'; +import * as WebSocketWS from 'ws'; +import type { Dealer, Subscriber } from 'zeromq'; +import { traceError } from '../../common/logger'; +import { noop } from '../../common/utils/misc'; +import { IKernelConnection } from '../kernel-launcher/types'; +import { IWebSocketLike } from '../kernelSocketWrapper'; +import { IKernelSocket } from '../types'; + +function formConnectionString(config: IKernelConnection, channel: string) { + const portDelimiter = config.transport === 'tcp' ? ':' : '-'; + const port = config[`${channel}_port` as keyof IKernelConnection]; + if (!port) { + throw new Error(`Port not found for channel "${channel}"`); + } + return `${config.transport}://${config.ip}${portDelimiter}${port}`; +} +interface IChannels { + shell: Dealer; + control: Dealer; + stdin: Dealer; + iopub: Subscriber; +} + +// tslint:disable: no-any +/** + * This class creates a WebSocket front end on a ZMQ set of connections. It is special in that + * it does all serialization/deserialization itself. + */ +export class RawSocket implements IWebSocketLike, IKernelSocket, IDisposable { + public onopen: (event: { target: any }) => void = noop; + public onerror: (event: { error: any; message: string; type: string; target: any }) => void = noop; + public onclose: (event: { wasClean: boolean; code: number; reason: string; target: any }) => void = noop; + public onmessage: (event: { data: WebSocketWS.Data; type: string; target: any }) => void = noop; + private receiveHooks: ((data: WebSocketWS.Data) => Promise)[] = []; + private sendHooks: ((data: any, cb?: (err?: Error) => void) => Promise)[] = []; + private msgChain: Promise = Promise.resolve(); + private sendChain: Promise = Promise.resolve(); + private channels: IChannels; + private closed = false; + + constructor( + private connection: IKernelConnection, + private serialize: (msg: KernelMessage.IMessage) => string | ArrayBuffer, + private deserialize: (data: ArrayBuffer | string) => KernelMessage.IMessage + ) { + // Setup our ZMQ channels now + this.channels = this.generateChannels(connection); + } + + public dispose() { + if (!this.closed) { + this.close(); + } + } + + public close(): void { + this.closed = true; + // When the socket is completed / disposed, close all the event + // listeners and shutdown the socket + const closer = (closable: { close(): void }) => { + try { + closable.close(); + } catch (ex) { + traceError(`Error during socket shutdown`, ex); + } + }; + closer(this.channels.control); + closer(this.channels.iopub); + closer(this.channels.shell); + closer(this.channels.stdin); + } + + public emit(event: string | symbol, ...args: any[]): boolean { + switch (event) { + case 'message': + this.onmessage({ data: args[0], type: 'message', target: this }); + break; + case 'close': + this.onclose({ wasClean: true, code: 0, reason: '', target: this }); + break; + case 'error': + this.onerror({ error: '', message: 'to do', type: 'error', target: this }); + break; + case 'open': + this.onopen({ target: this }); + break; + default: + break; + } + return true; + } + public sendToRealKernel(data: any, _callback: any): void { + // If from ipywidgets, this will be serialized already, so turn it back into a message so + // we can add the special hash to it. + const message = this.deserialize(data); + + // Send this directly (don't call back into the hooks) + this.sendMessage(message, true); + } + + public send(data: any, _callback: any): void { + // This comes directly from the jupyter lab kernel. It should be a message already + this.sendMessage(data as KernelMessage.IMessage, false); + } + + public addReceiveHook(hook: (data: WebSocketWS.Data) => Promise): void { + this.receiveHooks.push(hook); + } + public removeReceiveHook(hook: (data: WebSocketWS.Data) => Promise): void { + this.receiveHooks = this.receiveHooks.filter((l) => l !== hook); + } + public addSendHook(hook: (data: any, cb?: ((err?: Error | undefined) => void) | undefined) => Promise): void { + this.sendHooks.push(hook); + } + public removeSendHook( + hook: (data: any, cb?: ((err?: Error | undefined) => void) | undefined) => Promise + ): void { + this.sendHooks = this.sendHooks.filter((p) => p !== hook); + } + private generateChannel( + connection: IKernelConnection, + channel: 'iopub' | 'shell' | 'control' | 'stdin', + ctor: () => T + ): T { + const result = ctor(); + result.connect(formConnectionString(connection, channel)); + this.processSocketMessages(channel, result).catch( + traceError.bind(`Failed to read messages from channel ${channel}`) + ); + return result; + } + private async processSocketMessages( + channel: 'iopub' | 'shell' | 'control' | 'stdin', + readable: Subscriber | Dealer + ) { + // tslint:disable-next-line: await-promise + for await (const msg of readable) { + // Make sure to quit if we are disposed. + if (this.closed) { + break; + } else { + this.onIncomingMessage(channel, msg); + } + } + } + + private generateChannels(connection: IKernelConnection): IChannels { + // tslint:disable-next-line: no-require-imports + const zmq = require('zeromq') as typeof import('zeromq'); + + // Need a routing id for them to share. + const routingId = uuid(); + + // Wire up all of the different channels. + const result: IChannels = { + iopub: this.generateChannel(connection, 'iopub', () => new zmq.Subscriber()), + shell: this.generateChannel(connection, 'shell', () => new zmq.Dealer({ routingId })), + control: this.generateChannel(connection, 'control', () => new zmq.Dealer({ routingId })), + stdin: this.generateChannel(connection, 'stdin', () => new zmq.Dealer({ routingId })) + }; + // What about hb port? Enchannel didn't use this one. + + // Make sure to subscribe to general iopub messages (this is stuff like status changes) + result.iopub.subscribe(); + + return result; + } + + private onIncomingMessage(channel: string, data: any) { + // Decode the message if still possible. + const message = this.closed + ? {} + : (wireProtocol.decode(data, this.connection.key, this.connection.signature_scheme) as any); + + // Make sure it has a channel on it + message.channel = channel; + + if (this.receiveHooks.length) { + // Stick the receive hooks into the message chain. We use chain + // to ensure that: + // a) Hooks finish before we fire the event for real + // b) Event fires + // c) Next message happens after this one (so this side can handle the message before another event goes through) + this.msgChain = this.msgChain + .then(() => { + // Hooks expect serialized data as this normally comes from a WebSocket + const serialized = this.serialize(message); + return Promise.all(this.receiveHooks.map((p) => p(serialized))); + }) + .then(() => this.fireOnMessage(message)); + } else { + this.msgChain = this.msgChain.then(() => this.fireOnMessage(message)); + } + } + + private fireOnMessage(message: any) { + if (!this.closed) { + this.onmessage({ data: message, type: 'message', target: this }); + } + } + + private sendMessage(msg: KernelMessage.IMessage, bypassHooking: boolean) { + // First encode the message. + const data = wireProtocol.encode(msg as any, this.connection.key, this.connection.signature_scheme); + + // Then send through our hooks, and then post to the real zmq socket + if (!bypassHooking && this.sendHooks.length) { + // Separate encoding for ipywidgets. It expects the same result a WebSocket would generate. + const hookData = this.serialize(msg); + + this.sendChain = this.sendChain + .then(() => Promise.all(this.sendHooks.map((s) => s(hookData, noop)))) + .then(() => this.postToSocket(msg.channel, data)); + } else { + this.sendChain = this.sendChain.then(() => { + this.postToSocket(msg.channel, data); + }); + } + } + + private postToSocket(channel: string, data: any) { + const socket = (this.channels as any)[channel]; + if (socket) { + (socket as Dealer).send(data).catch((exc) => { + traceError(`Error communicating with the kernel`, exc); + }); + } else { + traceError(`Attempting to send message on invalid channel: ${channel}`); + } + } +} diff --git a/src/client/datascience/serviceRegistry.ts b/src/client/datascience/serviceRegistry.ts new file mode 100644 index 000000000000..25ed1a6a167c --- /dev/null +++ b/src/client/datascience/serviceRegistry.ts @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { UseCustomEditorApi, UseVSCodeNotebookEditorApi } from '../common/constants'; +import { NotebookEditorSupport } from '../common/experiments/groups'; +import { FileSystemPathUtils } from '../common/platform/fs-paths'; +import { IFileSystemPathUtils } from '../common/platform/types'; +import { StartPage } from '../common/startPage/startPage'; +import { IStartPage } from '../common/startPage/types'; +import { IExperimentsManager } from '../common/types'; +import { ProtocolParser } from '../debugger/extension/helpers/protocolParser'; +import { IProtocolParser } from '../debugger/extension/types'; +import { IServiceManager } from '../ioc/types'; +import { Activation } from './activation'; +import { CodeCssGenerator } from './codeCssGenerator'; +import { JupyterCommandLineSelectorCommand } from './commands/commandLineSelector'; +import { CommandRegistry } from './commands/commandRegistry'; +import { ExportCommands } from './commands/exportCommands'; +import { NotebookCommands } from './commands/notebookCommands'; +import { JupyterServerSelectorCommand } from './commands/serverSelector'; +import { DataScienceStartupTime, Identifiers, OurNotebookProvider, VSCodeNotebookProvider } from './constants'; +import { ActiveEditorContextService } from './context/activeEditorContext'; +import { DataViewer } from './data-viewing/dataViewer'; +import { DataViewerDependencyService } from './data-viewing/dataViewerDependencyService'; +import { DataViewerFactory } from './data-viewing/dataViewerFactory'; +import { JupyterVariableDataProvider } from './data-viewing/jupyterVariableDataProvider'; +import { JupyterVariableDataProviderFactory } from './data-viewing/jupyterVariableDataProviderFactory'; +import { IDataViewer, IDataViewerFactory } from './data-viewing/types'; +import { DataScience } from './datascience'; +import { DataScienceFileSystem } from './dataScienceFileSystem'; +import { DataScienceSurveyBannerLogger } from './dataScienceSurveyBanner'; +import { DebugLocationTrackerFactory } from './debugLocationTrackerFactory'; +import { CellHashProvider } from './editor-integration/cellhashprovider'; +import { CodeLensFactory } from './editor-integration/codeLensFactory'; +import { DataScienceCodeLensProvider } from './editor-integration/codelensprovider'; +import { CodeWatcher } from './editor-integration/codewatcher'; +import { Decorator } from './editor-integration/decorator'; +import { HoverProvider } from './editor-integration/hoverProvider'; +import { DataScienceErrorHandler } from './errorHandler/errorHandler'; +import { ExportBase } from './export/exportBase'; +import { ExportDependencyChecker } from './export/exportDependencyChecker'; +import { ExportFileOpener } from './export/exportFileOpener'; +import { ExportManager } from './export/exportManager'; +import { ExportManagerFilePicker } from './export/exportManagerFilePicker'; +import { ExportToHTML } from './export/exportToHTML'; +import { ExportToPDF } from './export/exportToPDF'; +import { ExportToPython } from './export/exportToPython'; +import { ExportUtil } from './export/exportUtil'; +import { ExportFormat, IExport, IExportManager, IExportManagerFilePicker } from './export/types'; +import { GatherListener } from './gather/gatherListener'; +import { GatherLogger } from './gather/gatherLogger'; +import { DebugListener } from './interactive-common/debugListener'; +import { IntellisenseProvider } from './interactive-common/intellisense/intellisenseProvider'; +import { LinkProvider } from './interactive-common/linkProvider'; +import { NotebookProvider } from './interactive-common/notebookProvider'; +import { NotebookServerProvider } from './interactive-common/notebookServerProvider'; +import { NotebookUsageTracker } from './interactive-common/notebookUsageTracker'; +import { ShowPlotListener } from './interactive-common/showPlotListener'; +import { AutoSaveService } from './interactive-ipynb/autoSaveService'; +import { DigestStorage } from './interactive-ipynb/digestStorage'; +import { NativeEditor } from './interactive-ipynb/nativeEditor'; +import { NativeEditorCommandListener } from './interactive-ipynb/nativeEditorCommandListener'; +import { NativeEditorOldWebView } from './interactive-ipynb/nativeEditorOldWebView'; +import { NativeEditorProviderOld } from './interactive-ipynb/nativeEditorProviderOld'; +import { NativeEditorRunByLineListener } from './interactive-ipynb/nativeEditorRunByLineListener'; +import { NativeEditorSynchronizer } from './interactive-ipynb/nativeEditorSynchronizer'; +import { NativeEditorViewTracker } from './interactive-ipynb/nativeEditorViewTracker'; +import { TrustCommandHandler } from './interactive-ipynb/trustCommandHandler'; +import { TrustService } from './interactive-ipynb/trustService'; +import { InteractiveWindow } from './interactive-window/interactiveWindow'; +import { InteractiveWindowCommandListener } from './interactive-window/interactiveWindowCommandListener'; +import { InteractiveWindowProvider } from './interactive-window/interactiveWindowProvider'; +import { IPyWidgetHandler } from './ipywidgets/ipywidgetHandler'; +import { IPyWidgetMessageDispatcherFactory } from './ipywidgets/ipyWidgetMessageDispatcherFactory'; +import { IPyWidgetScriptSource } from './ipywidgets/ipyWidgetScriptSource'; +import { JupyterCommandLineSelector } from './jupyter/commandLineSelector'; +import { DebuggerVariableRegistration } from './jupyter/debuggerVariableRegistration'; +import { DebuggerVariables } from './jupyter/debuggerVariables'; +import { JupyterCommandFactory } from './jupyter/interpreter/jupyterCommand'; +import { JupyterInterpreterDependencyService } from './jupyter/interpreter/jupyterInterpreterDependencyService'; +import { JupyterInterpreterOldCacheStateStore } from './jupyter/interpreter/jupyterInterpreterOldCacheStateStore'; +import { JupyterInterpreterSelectionCommand } from './jupyter/interpreter/jupyterInterpreterSelectionCommand'; +import { JupyterInterpreterSelector } from './jupyter/interpreter/jupyterInterpreterSelector'; +import { JupyterInterpreterService } from './jupyter/interpreter/jupyterInterpreterService'; +import { JupyterInterpreterStateStore } from './jupyter/interpreter/jupyterInterpreterStateStore'; +import { JupyterInterpreterSubCommandExecutionService } from './jupyter/interpreter/jupyterInterpreterSubCommandExecutionService'; +import { CellOutputMimeTypeTracker } from './jupyter/jupyterCellOutputMimeTypeTracker'; +import { JupyterDebugger } from './jupyter/jupyterDebugger'; +import { JupyterExecutionFactory } from './jupyter/jupyterExecutionFactory'; +import { JupyterExporter } from './jupyter/jupyterExporter'; +import { JupyterImporter } from './jupyter/jupyterImporter'; +import { JupyterNotebookProvider } from './jupyter/jupyterNotebookProvider'; +import { JupyterPasswordConnect } from './jupyter/jupyterPasswordConnect'; +import { JupyterServerWrapper } from './jupyter/jupyterServerWrapper'; +import { JupyterSessionManagerFactory } from './jupyter/jupyterSessionManagerFactory'; +import { JupyterVariables } from './jupyter/jupyterVariables'; +import { KernelDependencyService } from './jupyter/kernels/kernelDependencyService'; +import { KernelSelectionProvider } from './jupyter/kernels/kernelSelections'; +import { KernelSelector } from './jupyter/kernels/kernelSelector'; +import { KernelService } from './jupyter/kernels/kernelService'; +import { KernelSwitcher } from './jupyter/kernels/kernelSwitcher'; +import { KernelVariables } from './jupyter/kernelVariables'; +import { NotebookStarter } from './jupyter/notebookStarter'; +import { OldJupyterVariables } from './jupyter/oldJupyterVariables'; +import { ServerPreload } from './jupyter/serverPreload'; +import { JupyterServerSelector } from './jupyter/serverSelector'; +import { JupyterDebugService } from './jupyterDebugService'; +import { JupyterUriProviderRegistration } from './jupyterUriProviderRegistration'; +import { KernelDaemonPool } from './kernel-launcher/kernelDaemonPool'; +import { KernelDaemonPreWarmer } from './kernel-launcher/kernelDaemonPreWarmer'; +import { KernelFinder } from './kernel-launcher/kernelFinder'; +import { KernelLauncher } from './kernel-launcher/kernelLauncher'; +import { IKernelFinder, IKernelLauncher } from './kernel-launcher/types'; +import { MultiplexingDebugService } from './multiplexingDebugService'; +import { NotebookEditorCompatibilitySupport } from './notebook/notebookEditorCompatibilitySupport'; +import { NotebookEditorProvider } from './notebook/notebookEditorProvider'; +import { NotebookEditorProviderWrapper } from './notebook/notebookEditorProviderWrapper'; +import { registerTypes as registerNotebookTypes } from './notebook/serviceRegistry'; +import { NotebookAndInteractiveWindowUsageTracker } from './notebookAndInteractiveTracker'; +import { NotebookModelFactory } from './notebookStorage/factory'; +import { NativeEditorProvider } from './notebookStorage/nativeEditorProvider'; +import { NativeEditorStorage } from './notebookStorage/nativeEditorStorage'; +import { INotebookStorageProvider, NotebookStorageProvider } from './notebookStorage/notebookStorageProvider'; +import { INotebookModelFactory } from './notebookStorage/types'; +import { PlotViewer } from './plotting/plotViewer'; +import { PlotViewerProvider } from './plotting/plotViewerProvider'; +import { PreWarmActivatedJupyterEnvironmentVariables } from './preWarmVariables'; +import { ProgressReporter } from './progress/progressReporter'; +import { RawNotebookProviderWrapper } from './raw-kernel/rawNotebookProviderWrapper'; +import { RawNotebookSupportedService } from './raw-kernel/rawNotebookSupportedService'; +import { StatusProvider } from './statusProvider'; +import { ThemeFinder } from './themeFinder'; +import { + ICellHashListener, + ICellHashProvider, + ICodeCssGenerator, + ICodeLensFactory, + ICodeWatcher, + IDataScience, + IDataScienceCodeLensProvider, + IDataScienceCommandListener, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IDebugLocationTracker, + IDigestStorage, + IGatherLogger, + IInteractiveWindow, + IInteractiveWindowListener, + IInteractiveWindowProvider, + IJupyterCommandFactory, + IJupyterDebugger, + IJupyterDebugService, + IJupyterExecution, + IJupyterInterpreterDependencyManager, + IJupyterNotebookProvider, + IJupyterPasswordConnect, + IJupyterServerProvider, + IJupyterSessionManagerFactory, + IJupyterSubCommandExecutionService, + IJupyterUriProviderRegistration, + IJupyterVariableDataProvider, + IJupyterVariableDataProviderFactory, + IJupyterVariables, + IKernelDependencyService, + INotebookAndInteractiveWindowUsageTracker, + INotebookEditor, + INotebookEditorProvider, + INotebookExecutionLogger, + INotebookExporter, + INotebookImporter, + INotebookProvider, + INotebookServer, + INotebookStorage, + IPlotViewer, + IPlotViewerProvider, + IRawNotebookProvider, + IRawNotebookSupportedService, + IStatusProvider, + IThemeFinder, + ITrustService +} from './types'; + +// README: Did you make sure "dataScienceIocContainer.ts" has also been updated appropriately? + +// tslint:disable-next-line: max-func-body-length +export function registerTypes(serviceManager: IServiceManager) { + const experiments = serviceManager.get(IExperimentsManager); + const useVSCodeNotebookAPI = experiments.inExperiment(NotebookEditorSupport.nativeNotebookExperiment); + const inCustomEditorApiExperiment = experiments.inExperiment(NotebookEditorSupport.customEditorExperiment); + const usingCustomEditor = inCustomEditorApiExperiment; + serviceManager.addSingletonInstance(UseCustomEditorApi, usingCustomEditor); + serviceManager.addSingletonInstance(UseVSCodeNotebookEditorApi, useVSCodeNotebookAPI); + serviceManager.addSingletonInstance(DataScienceStartupTime, Date.now()); + + // This condition is temporary. + serviceManager.addSingleton(VSCodeNotebookProvider, NotebookEditorProvider); + serviceManager.addSingleton(OurNotebookProvider, usingCustomEditor ? NativeEditorProvider : NativeEditorProviderOld); + serviceManager.addSingleton(INotebookEditorProvider, NotebookEditorProviderWrapper); + serviceManager.add(IExtensionSingleActivationService, NotebookEditorCompatibilitySupport); + serviceManager.add(NotebookEditorCompatibilitySupport, NotebookEditorCompatibilitySupport); + if (!useVSCodeNotebookAPI) { + serviceManager.add(INotebookEditor, usingCustomEditor ? NativeEditor : NativeEditorOldWebView); + // These are never going to be required for new VSC NB. + if (!usingCustomEditor) { + serviceManager.add(IInteractiveWindowListener, AutoSaveService); + } + serviceManager.addSingleton(NativeEditorSynchronizer, NativeEditorSynchronizer); + } + + serviceManager.add(ICellHashProvider, CellHashProvider, undefined, [INotebookExecutionLogger]); + serviceManager.addSingleton(INotebookModelFactory, NotebookModelFactory); + serviceManager.addSingleton(INotebookExecutionLogger, HoverProvider); + serviceManager.add(ICodeWatcher, CodeWatcher); + serviceManager.addSingleton(IDataScienceErrorHandler, DataScienceErrorHandler); + serviceManager.add(IDataViewer, DataViewer); + serviceManager.add(IInteractiveWindow, InteractiveWindow); + serviceManager.add(IInteractiveWindowListener, DebugListener); + serviceManager.add(IInteractiveWindowListener, GatherListener); + serviceManager.add(IInteractiveWindowListener, IntellisenseProvider); + serviceManager.add(IInteractiveWindowListener, LinkProvider); + serviceManager.add(IInteractiveWindowListener, ShowPlotListener); + serviceManager.add(IInteractiveWindowListener, IPyWidgetHandler); + serviceManager.add(IInteractiveWindowListener, IPyWidgetScriptSource); + serviceManager.add(IInteractiveWindowListener, NativeEditorRunByLineListener); + serviceManager.add(IJupyterCommandFactory, JupyterCommandFactory); + serviceManager.add(INotebookExporter, JupyterExporter); + serviceManager.add(INotebookImporter, JupyterImporter); + serviceManager.add(INotebookServer, JupyterServerWrapper); + serviceManager.addSingleton(INotebookStorage, NativeEditorStorage); + serviceManager.addSingleton(INotebookStorageProvider, NotebookStorageProvider); + serviceManager.addSingleton(IRawNotebookProvider, RawNotebookProviderWrapper); + serviceManager.addSingleton(IRawNotebookSupportedService, RawNotebookSupportedService); + serviceManager.addSingleton(IJupyterNotebookProvider, JupyterNotebookProvider); + serviceManager.add(IPlotViewer, PlotViewer); + serviceManager.addSingleton(IKernelLauncher, KernelLauncher); + serviceManager.addSingleton(IKernelFinder, KernelFinder); + serviceManager.addSingleton(ActiveEditorContextService, ActiveEditorContextService); + serviceManager.addSingleton(CellOutputMimeTypeTracker, CellOutputMimeTypeTracker, undefined, [IExtensionSingleActivationService, INotebookExecutionLogger]); + serviceManager.addSingleton(CommandRegistry, CommandRegistry); + serviceManager.addSingleton(DataViewerDependencyService, DataViewerDependencyService); + serviceManager.addSingleton(ICodeCssGenerator, CodeCssGenerator); + serviceManager.addSingleton(ICodeLensFactory, CodeLensFactory, undefined, [IInteractiveWindowListener]); + serviceManager.addSingleton(IDataScience, DataScience); + serviceManager.addSingleton(IDataScienceCodeLensProvider, DataScienceCodeLensProvider); + serviceManager.addSingleton(IDataScienceCommandListener, InteractiveWindowCommandListener); + serviceManager.addSingleton(IDataScienceCommandListener, NativeEditorCommandListener); + serviceManager.addSingleton(IDataViewerFactory, DataViewerFactory); + serviceManager.addSingleton(IDebugLocationTracker, DebugLocationTrackerFactory); + serviceManager.addSingleton(IExtensionSingleActivationService, Activation); + serviceManager.addSingleton(IExtensionSingleActivationService, Decorator); + serviceManager.addSingleton(IExtensionSingleActivationService, JupyterInterpreterSelectionCommand); + serviceManager.addSingleton(IExtensionSingleActivationService, PreWarmActivatedJupyterEnvironmentVariables); + serviceManager.addSingleton(IExtensionSingleActivationService, ServerPreload); + serviceManager.addSingleton(IExtensionSingleActivationService, NativeEditorViewTracker); + serviceManager.addSingleton(IExtensionSingleActivationService, NotebookUsageTracker); + serviceManager.addSingleton(IExtensionSingleActivationService, TrustCommandHandler); + serviceManager.addSingleton(IInteractiveWindowListener, DataScienceSurveyBannerLogger); + serviceManager.addSingleton(IInteractiveWindowProvider, InteractiveWindowProvider); + serviceManager.addSingleton(IJupyterDebugger, JupyterDebugger, undefined, [ICellHashListener]); + serviceManager.addSingleton(IJupyterExecution, JupyterExecutionFactory); + serviceManager.addSingleton(IJupyterPasswordConnect, JupyterPasswordConnect); + serviceManager.addSingleton(IJupyterSessionManagerFactory, JupyterSessionManagerFactory); + serviceManager.addSingleton(IExtensionSingleActivationService, DebuggerVariableRegistration); + serviceManager.addSingleton(IJupyterVariables, JupyterVariables, Identifiers.ALL_VARIABLES); + serviceManager.addSingleton(IJupyterVariables, OldJupyterVariables, Identifiers.OLD_VARIABLES); + serviceManager.addSingleton(IJupyterVariables, KernelVariables, Identifiers.KERNEL_VARIABLES); + serviceManager.addSingleton(IJupyterVariables, DebuggerVariables, Identifiers.DEBUGGER_VARIABLES); + serviceManager.addSingleton(IPlotViewerProvider, PlotViewerProvider); + serviceManager.addSingleton(IStatusProvider, StatusProvider); + serviceManager.addSingleton(IThemeFinder, ThemeFinder); + serviceManager.addSingleton(JupyterCommandLineSelector, JupyterCommandLineSelector); + serviceManager.addSingleton(JupyterCommandLineSelectorCommand, JupyterCommandLineSelectorCommand); + serviceManager.addSingleton(JupyterInterpreterDependencyService, JupyterInterpreterDependencyService); + serviceManager.addSingleton(JupyterInterpreterOldCacheStateStore, JupyterInterpreterOldCacheStateStore); + serviceManager.addSingleton(JupyterInterpreterSelector, JupyterInterpreterSelector); + serviceManager.addSingleton(JupyterInterpreterService, JupyterInterpreterService); + serviceManager.addSingleton(JupyterInterpreterStateStore, JupyterInterpreterStateStore); + serviceManager.addSingleton(JupyterServerSelector, JupyterServerSelector); + serviceManager.addSingleton(JupyterServerSelectorCommand, JupyterServerSelectorCommand); + serviceManager.addSingleton(KernelSelectionProvider, KernelSelectionProvider); + serviceManager.addSingleton(KernelSelector, KernelSelector); + serviceManager.addSingleton(KernelService, KernelService); + serviceManager.addSingleton(KernelSwitcher, KernelSwitcher); + serviceManager.addSingleton(NotebookCommands, NotebookCommands); + serviceManager.addSingleton(NotebookStarter, NotebookStarter); + serviceManager.addSingleton(ProgressReporter, ProgressReporter); + serviceManager.addSingleton(INotebookProvider, NotebookProvider); + serviceManager.addSingleton(IJupyterServerProvider, NotebookServerProvider); + serviceManager.addSingleton(IPyWidgetMessageDispatcherFactory, IPyWidgetMessageDispatcherFactory); + serviceManager.addSingleton(IJupyterInterpreterDependencyManager, JupyterInterpreterSubCommandExecutionService); + serviceManager.addSingleton(IJupyterSubCommandExecutionService, JupyterInterpreterSubCommandExecutionService); + serviceManager.addSingleton(KernelDaemonPool, KernelDaemonPool); + serviceManager.addSingleton(IKernelDependencyService, KernelDependencyService); + serviceManager.addSingleton(INotebookAndInteractiveWindowUsageTracker, NotebookAndInteractiveWindowUsageTracker); + serviceManager.addSingleton(KernelDaemonPreWarmer, KernelDaemonPreWarmer); + serviceManager.addSingleton(IStartPage, StartPage, undefined, [IExtensionSingleActivationService]); + serviceManager.add(IProtocolParser, ProtocolParser); + serviceManager.addSingleton(IJupyterDebugService, MultiplexingDebugService, Identifiers.MULTIPLEXING_DEBUGSERVICE); + serviceManager.addSingleton(IJupyterDebugService, JupyterDebugService, Identifiers.RUN_BY_LINE_DEBUGSERVICE); + serviceManager.add(IJupyterVariableDataProvider, JupyterVariableDataProvider); + serviceManager.addSingleton(IJupyterVariableDataProviderFactory, JupyterVariableDataProviderFactory); + serviceManager.addSingleton(IExportManager, ExportManager); + serviceManager.addSingleton(ExportDependencyChecker, ExportDependencyChecker); + serviceManager.addSingleton(ExportFileOpener, ExportFileOpener); + serviceManager.addSingleton(IExport, ExportToPDF, ExportFormat.pdf); + serviceManager.addSingleton(IExport, ExportToHTML, ExportFormat.html); + serviceManager.addSingleton(IExport, ExportToPython, ExportFormat.python); + serviceManager.addSingleton(IExport, ExportBase, 'Export Base'); + serviceManager.addSingleton(ExportUtil, ExportUtil); + serviceManager.addSingleton(ExportCommands, ExportCommands); + serviceManager.addSingleton(IExportManagerFilePicker, ExportManagerFilePicker); + serviceManager.addSingleton(IJupyterUriProviderRegistration, JupyterUriProviderRegistration); + serviceManager.addSingleton(IDigestStorage, DigestStorage); + serviceManager.addSingleton(ITrustService, TrustService); + serviceManager.addSingleton(IDataScienceFileSystem, DataScienceFileSystem); + serviceManager.addSingleton(IFileSystemPathUtils, FileSystemPathUtils); + + registerGatherTypes(serviceManager); + registerNotebookTypes(serviceManager); +} + +export function registerGatherTypes(serviceManager: IServiceManager) { + serviceManager.add(IGatherLogger, GatherLogger, undefined, [INotebookExecutionLogger]); +} diff --git a/src/client/datascience/shiftEnterBanner.ts b/src/client/datascience/shiftEnterBanner.ts new file mode 100644 index 000000000000..7fadde77a3a6 --- /dev/null +++ b/src/client/datascience/shiftEnterBanner.ts @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ConfigurationTarget } from 'vscode'; +import { IApplicationShell } from '../common/application/types'; +import '../common/extensions'; +import { IConfigurationService, IPersistentStateFactory, IPythonExtensionBanner } from '../common/types'; +import * as localize from '../common/utils/localize'; +import { captureTelemetry, sendTelemetryEvent } from '../telemetry'; +import { Telemetry } from './constants'; +import { IJupyterExecution } from './types'; + +export enum InteractiveShiftEnterStateKeys { + ShowBanner = 'InteractiveShiftEnterBanner' +} + +enum InteractiveShiftEnterLabelIndex { + Yes, + No +} + +// Create a banner to ask users if they want to send shift-enter to the interactive window or not +@injectable() +export class InteractiveShiftEnterBanner implements IPythonExtensionBanner { + private initialized?: boolean; + private disabledInCurrentSession: boolean = false; + private bannerMessage: string = localize.InteractiveShiftEnterBanner.bannerMessage(); + private bannerLabels: string[] = [localize.Common.bannerLabelYes(), localize.Common.bannerLabelNo()]; + + constructor( + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, + @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, + @inject(IConfigurationService) private configuration: IConfigurationService + ) { + this.initialize(); + } + + public initialize() { + if (this.initialized) { + return; + } + this.initialized = true; + + if (!this.enabled) { + return; + } + } + + public get enabled(): boolean { + return this.persistentState.createGlobalPersistentState( + InteractiveShiftEnterStateKeys.ShowBanner, + true + ).value; + } + + public async showBanner(): Promise { + if (!this.enabled) { + return; + } + + const show = await this.shouldShowBanner(); + if (!show) { + return; + } + + // This check is independent from shouldShowBanner, that just checks the persistent state. + // The Jupyter check should only happen once and should disable the banner if it fails (don't reprompt and don't recheck) + const jupyterFound = await this.jupyterExecution.isNotebookSupported(); + if (!jupyterFound) { + await this.disableBanner(); + return; + } + + sendTelemetryEvent(Telemetry.ShiftEnterBannerShown); + const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels); + switch (response) { + case this.bannerLabels[InteractiveShiftEnterLabelIndex.Yes]: { + await this.enableInteractiveShiftEnter(); + break; + } + case this.bannerLabels[InteractiveShiftEnterLabelIndex.No]: { + await this.disableInteractiveShiftEnter(); + break; + } + default: { + // Disable for the current session. + this.disabledInCurrentSession = true; + } + } + } + + public async shouldShowBanner(): Promise { + const settings = this.configuration.getSettings(); + return Promise.resolve( + this.enabled && + !this.disabledInCurrentSession && + !settings.datascience.sendSelectionToInteractiveWindow && + settings.datascience.enabled + ); + } + + @captureTelemetry(Telemetry.DisableInteractiveShiftEnter) + public async disableInteractiveShiftEnter(): Promise { + await this.configuration.updateSetting( + 'dataScience.sendSelectionToInteractiveWindow', + false, + undefined, + ConfigurationTarget.Global + ); + await this.disableBanner(); + } + + @captureTelemetry(Telemetry.EnableInteractiveShiftEnter) + public async enableInteractiveShiftEnter(): Promise { + await this.configuration.updateSetting( + 'dataScience.sendSelectionToInteractiveWindow', + true, + undefined, + ConfigurationTarget.Global + ); + await this.disableBanner(); + } + + private async disableBanner(): Promise { + await this.persistentState + .createGlobalPersistentState(InteractiveShiftEnterStateKeys.ShowBanner, false) + .updateValue(false); + } +} diff --git a/src/client/datascience/statusProvider.ts b/src/client/datascience/statusProvider.ts new file mode 100644 index 000000000000..c82ad4bad9dc --- /dev/null +++ b/src/client/datascience/statusProvider.ts @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { Disposable, ProgressLocation, ProgressOptions } from 'vscode'; + +import { IApplicationShell } from '../common/application/types'; +import { createDeferred, Deferred } from '../common/utils/async'; +import { IInteractiveBase, IStatusProvider } from './types'; + +class StatusItem implements Disposable { + private deferred: Deferred; + private disposed: boolean = false; + private timeout: NodeJS.Timer | number | undefined; + private disposeCallback: () => void; + + constructor(_title: string, disposeCallback: () => void, timeout?: number) { + this.deferred = createDeferred(); + this.disposeCallback = disposeCallback; + + // A timeout is possible too. Auto dispose if that's the case + if (timeout) { + this.timeout = setTimeout(this.dispose, timeout); + } + } + + public dispose = () => { + if (!this.disposed) { + this.disposed = true; + if (this.timeout) { + // tslint:disable-next-line: no-any + clearTimeout(this.timeout as any); + this.timeout = undefined; + } + this.disposeCallback(); + if (!this.deferred.completed) { + this.deferred.resolve(); + } + } + }; + + public promise = (): Promise => { + return this.deferred.promise; + }; + + public reject = () => { + this.deferred.reject(); + this.dispose(); + }; +} + +@injectable() +export class StatusProvider implements IStatusProvider { + private statusCount: number = 0; + + constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell) {} + + public set( + message: string, + showInWebView: boolean, + timeout?: number, + cancel?: () => void, + panel?: IInteractiveBase + ): Disposable { + // Start our progress + this.incrementCount(showInWebView, panel); + + // Create a StatusItem that will return our promise + const statusItem = new StatusItem(message, () => this.decrementCount(panel), timeout); + + const progressOptions: ProgressOptions = { + location: cancel ? ProgressLocation.Notification : ProgressLocation.Window, + title: message, + cancellable: cancel !== undefined + }; + + // Set our application shell status with a busy icon + this.applicationShell.withProgress(progressOptions, (_p, c) => { + if (c && cancel) { + c.onCancellationRequested(() => { + cancel(); + statusItem.reject(); + }); + } + return statusItem.promise(); + }); + + return statusItem; + } + + public async waitWithStatus( + promise: () => Promise, + message: string, + showInWebView: boolean, + timeout?: number, + cancel?: () => void, + panel?: IInteractiveBase + ): Promise { + // Create a status item and wait for our promise to either finish or reject + const status = this.set(message, showInWebView, timeout, cancel, panel); + let result: T; + try { + result = await promise(); + } finally { + status.dispose(); + } + return result; + } + + private incrementCount = (showInWebView: boolean, panel?: IInteractiveBase) => { + if (this.statusCount === 0) { + if (panel && showInWebView) { + panel.startProgress(); + } + } + this.statusCount += 1; + }; + + private decrementCount = (panel?: IInteractiveBase) => { + const updatedCount = this.statusCount - 1; + if (updatedCount === 0) { + if (panel) { + panel.stopProgress(); + } + } + this.statusCount = Math.max(updatedCount, 0); + }; +} diff --git a/src/client/datascience/themeFinder.ts b/src/client/datascience/themeFinder.ts new file mode 100644 index 000000000000..c0deeb79e47c --- /dev/null +++ b/src/client/datascience/themeFinder.ts @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; + +import { LanguageConfiguration } from 'vscode'; +import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../common/constants'; +import { traceError } from '../common/logger'; + +import { ICurrentProcess, IExtensions } from '../common/types'; +import { getLanguageConfiguration } from '../language/languageConfiguration'; +import { IDataScienceFileSystem, IThemeFinder } from './types'; + +// tslint:disable:no-any + +interface IThemeData { + rootFile: string; + isDark: boolean; +} + +@injectable() +export class ThemeFinder implements IThemeFinder { + private themeCache: { [key: string]: IThemeData | undefined } = {}; + private languageCache: { [key: string]: string | undefined } = {}; + + constructor( + @inject(IExtensions) private extensions: IExtensions, + @inject(ICurrentProcess) private currentProcess: ICurrentProcess, + @inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem + ) {} + + public async findThemeRootJson(themeName: string): Promise { + // find our data + const themeData = await this.findThemeData(themeName); + + // Use that data if it worked + if (themeData) { + return themeData.rootFile; + } + } + + public async findTmLanguage(language: string): Promise { + // See if already found it or not + if (!this.themeCache.hasOwnProperty(language)) { + try { + this.languageCache[language] = await this.findMatchingLanguage(language); + } catch (exc) { + traceError(exc); + } + } + return this.languageCache[language]; + } + + public async findLanguageConfiguration(language: string): Promise { + if (language === PYTHON_LANGUAGE) { + // Custom for python. Some of these are required by monaco. + return { + comments: { + lineComment: '#', + blockComment: ['"""', '"""'] + }, + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'] + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"', notIn: ['string'] }, + { open: "'", close: "'", notIn: ['string', 'comment'] } + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" } + ], + folding: { + offSide: true, + markers: { + start: new RegExp('^\\s*#region\\b'), + end: new RegExp('^\\s*#endregion\\b') + } + }, + ...getLanguageConfiguration() + } as any; // NOSONAR + } + return this.findMatchingLanguageConfiguration(language); + } + + public async isThemeDark(themeName: string): Promise { + // find our data + const themeData = await this.findThemeData(themeName); + + // Use that data if it worked + if (themeData) { + return themeData.isDark; + } + } + + private async findThemeData(themeName: string): Promise { + // See if already found it or not + if (!this.themeCache.hasOwnProperty(themeName)) { + try { + this.themeCache[themeName] = await this.findMatchingTheme(themeName); + } catch (exc) { + traceError(exc); + } + } + return this.themeCache[themeName]; + } + + private async findMatchingLanguage(language: string): Promise { + const currentExe = this.currentProcess.execPath; + let currentPath = path.dirname(currentExe); + + // Should be somewhere under currentPath/resources/app/extensions inside of a json file + let extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions'); + if (!(await this.fs.localDirectoryExists(extensionsPath))) { + // Might be on mac or linux. try a different path + currentPath = path.resolve(currentPath, '../../../..'); + extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions'); + } + + // Search through all of the files in this folder + let results = await this.findMatchingLanguages(language, extensionsPath); + + // If that didn't work, see if it's our MagicPython predefined tmLanguage + if (!results && language === PYTHON_LANGUAGE) { + results = await this.fs.readLocalFile( + path.join(EXTENSION_ROOT_DIR, 'resources', 'MagicPython.tmLanguage.json') + ); + } + + return results; + } + + private async findMatchingLanguageConfiguration(language: string): Promise { + try { + const currentExe = this.currentProcess.execPath; + let currentPath = path.dirname(currentExe); + + // Should be somewhere under currentPath/resources/app/extensions inside of a json file + let extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions', language); + if (!(await this.fs.localDirectoryExists(extensionsPath))) { + // Might be on mac or linux. try a different path + currentPath = path.resolve(currentPath, '../../../..'); + extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions', language); + } + + // See if the 'language-configuration.json' file exists + const filePath = path.join(extensionsPath, 'language-configuration.json'); + if (await this.fs.localFileExists(filePath)) { + const contents = await this.fs.readLocalFile(filePath); + return JSON.parse(contents) as LanguageConfiguration; + } + } catch { + // Do nothing if an error + } + + return {}; + } + + private async findMatchingLanguages(language: string, rootPath: string): Promise { + // Environment variable to mimic missing json problem + if (process.env.VSC_PYTHON_MIMIC_REMOTE) { + return undefined; + } + + // Search through all package.json files in the directory and below, looking + // for the themeName in them. + const foundPackages = await this.fs.searchLocal('**/package.json', rootPath); + if (foundPackages && foundPackages.length > 0) { + // For each one, open it up and look for the theme name. + for (const f of foundPackages) { + const fpath = path.join(rootPath, f); + const data = await this.findMatchingLanguageFromJson(fpath, language); + if (data) { + return data; + } + } + } + } + + private async findMatchingTheme(themeName: string): Promise { + // Environment variable to mimic missing json problem + if (process.env.VSC_PYTHON_MIMIC_REMOTE) { + return undefined; + } + + // Look through all extensions to find the theme. This will search + // the default extensions folder and our installed extensions. + const extensions = this.extensions.all; + for (const e of extensions) { + const result = await this.findMatchingThemeFromJson(path.join(e.extensionPath, 'package.json'), themeName); + if (result) { + return result; + } + } + + // If didn't find in the extensions folder, then try searching manually. This shouldn't happen, but + // this is our backup plan in case vscode changes stuff. + const currentExe = this.currentProcess.execPath; + let currentPath = path.dirname(currentExe); + + // Should be somewhere under currentPath/resources/app/extensions inside of a json file + let extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions'); + if (!(await this.fs.localDirectoryExists(extensionsPath))) { + // Might be on mac or linux. try a different path + currentPath = path.resolve(currentPath, '../../../..'); + extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions'); + } + const other = await this.findMatchingThemes(extensionsPath, themeName); + if (other) { + return other; + } + } + + private async findMatchingThemes(rootPath: string, themeName: string): Promise { + // Search through all package.json files in the directory and below, looking + // for the themeName in them. + const foundPackages = await this.fs.searchLocal('**/package.json', rootPath); + if (foundPackages && foundPackages.length > 0) { + // For each one, open it up and look for the theme name. + for (const f of foundPackages) { + const fpath = path.join(rootPath, f); + const data = await this.findMatchingThemeFromJson(fpath, themeName); + if (data) { + return data; + } + } + } + } + + private async findMatchingLanguageFromJson(packageJson: string, language: string): Promise { + // Read the contents of the json file + const text = await this.fs.readLocalFile(packageJson); + const json = JSON.parse(text); + + // Should have a name entry and a contributes entry + if (json.hasOwnProperty('name') && json.hasOwnProperty('contributes')) { + // See if contributes has a grammars + const contributes = json.contributes; + if (contributes.hasOwnProperty('grammars')) { + const grammars = contributes.grammars as any[]; + // Go through each theme, seeing if the label matches our theme name + for (const t of grammars) { + if (t.hasOwnProperty('language') && t.language === language) { + // Path is relative to the package.json file. + const rootFile = t.hasOwnProperty('path') + ? path.join(path.dirname(packageJson), t.path.toString()) + : ''; + return this.fs.readLocalFile(rootFile); + } + } + } + } + } + + private async findMatchingThemeFromJson(packageJson: string, themeName: string): Promise { + // Read the contents of the json file + const text = await this.fs.readLocalFile(packageJson); + const json = JSON.parse(text); + + // Should have a name entry and a contributes entry + if (json.hasOwnProperty('name') && json.hasOwnProperty('contributes')) { + // See if contributes has a theme + const contributes = json.contributes; + if (contributes.hasOwnProperty('themes')) { + const themes = contributes.themes as any[]; + // Go through each theme, seeing if the label matches our theme name + for (const t of themes) { + if ( + (t.hasOwnProperty('label') && t.label === themeName) || + (t.hasOwnProperty('id') && t.id === themeName) + ) { + const isDark = t.hasOwnProperty('uiTheme') && t.uiTheme === 'vs-dark'; + // Path is relative to the package.json file. + const rootFile = t.hasOwnProperty('path') + ? path.join(path.dirname(packageJson), t.path.toString()) + : ''; + + return { isDark, rootFile }; + } + } + } + } + } +} diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts new file mode 100644 index 000000000000..c62b6be237d9 --- /dev/null +++ b/src/client/datascience/types.ts @@ -0,0 +1,1402 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { nbformat } from '@jupyterlab/coreutils'; +import type { Session } from '@jupyterlab/services'; +import type { Kernel, KernelMessage } from '@jupyterlab/services/lib/kernel'; +import type { JSONObject } from '@phosphor/coreutils'; +import { WriteStream } from 'fs-extra'; +import { Observable } from 'rxjs/Observable'; +import { + CancellationToken, + CodeLens, + CodeLensProvider, + DebugConfiguration, + DebugSession, + Disposable, + Event, + LanguageConfiguration, + QuickPickItem, + Range, + TextDocument, + TextEditor, + Uri +} from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import type { Data as WebSocketData } from 'ws'; +import { ServerStatus } from '../../datascience-ui/interactive-common/mainState'; +import { ICommandManager, IDebugService } from '../common/application/types'; +import { FileStat, TemporaryFile } from '../common/platform/types'; +import { ExecutionResult, ObservableExecutionResult, SpawnOptions } from '../common/process/types'; +import { IAsyncDisposable, IDataScienceSettings, IDisposable, InteractiveWindowMode, Resource } from '../common/types'; +import { StopWatch } from '../common/utils/stopWatch'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { JupyterCommands } from './constants'; +import { IDataViewerDataProvider } from './data-viewing/types'; +import { NotebookModelChange } from './interactive-common/interactiveWindowTypes'; +import { JupyterServerInfo } from './jupyter/jupyterConnection'; +import { JupyterInstallError } from './jupyter/jupyterInstallError'; +import { JupyterKernelSpec } from './jupyter/kernels/jupyterKernelSpec'; +import { KernelConnectionMetadata } from './jupyter/kernels/types'; + +// tslint:disable-next-line:no-any +export type PromiseFunction = (...any: any[]) => Promise; + +// Main interface +export const IDataScience = Symbol('IDataScience'); +export interface IDataScience extends Disposable { + activate(): Promise; +} + +export const IDataScienceCommandListener = Symbol('IDataScienceCommandListener'); +export interface IDataScienceCommandListener { + register(commandManager: ICommandManager): void; +} + +export interface IRawConnection extends Disposable { + readonly type: 'raw'; + readonly localLaunch: true; + readonly valid: boolean; + readonly displayName: string; + disconnected: Event; +} + +export interface IJupyterConnection extends Disposable { + readonly type: 'jupyter'; + readonly localLaunch: boolean; + readonly valid: boolean; + readonly displayName: string; + disconnected: Event; + + // Jupyter specific members + readonly baseUrl: string; + readonly token: string; + readonly hostName: string; + localProcExitCode: number | undefined; + readonly rootDirectory: string; // Directory where the notebook server was started. + readonly url?: string; + // tslint:disable-next-line: no-any + getAuthHeader?(): any; // Snould be a json object +} + +export type INotebookProviderConnection = IRawConnection | IJupyterConnection; + +export enum InterruptResult { + Success = 0, + TimedOut = 1, + Restarted = 2 +} + +// Information used to execute a notebook +export interface INotebookExecutionInfo { + // Connection to what has provided our notebook, such as a jupyter + // server or a raw ZMQ kernel + connectionInfo: INotebookProviderConnection; + uri: string | undefined; // Different from the connectionInfo as this is the setting used, not the result + kernelConnectionMetadata?: KernelConnectionMetadata; + workingDir: string | undefined; + purpose: string | undefined; // Purpose this server is for +} + +// Information used to launch a jupyter notebook server + +// Information used to launch a notebook server +export interface INotebookServerLaunchInfo { + connectionInfo: IJupyterConnection; + uri: string | undefined; // Different from the connectionInfo as this is the setting used, not the result + kernelConnectionMetadata?: KernelConnectionMetadata; + workingDir: string | undefined; + purpose: string | undefined; // Purpose this server is for +} + +export interface INotebookCompletion { + matches: ReadonlyArray; + cursor: { + start: number; + end: number; + }; + metadata: {}; +} + +export type INotebookMetadataLive = nbformat.INotebookMetadata & { id?: string }; + +// Talks to a jupyter ipython kernel to retrieve data for cells +export const INotebookServer = Symbol('INotebookServer'); +export interface INotebookServer extends IAsyncDisposable { + readonly id: string; + createNotebook( + resource: Resource, + identity: Uri, + notebookMetadata?: INotebookMetadataLive, + cancelToken?: CancellationToken + ): Promise; + getNotebook(identity: Uri, cancelToken?: CancellationToken): Promise; + connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken): Promise; + getConnectionInfo(): IJupyterConnection | undefined; + waitForConnect(): Promise; + shutdown(): Promise; +} + +// Provides a service to determine if raw notebook is supported or not +export const IRawNotebookSupportedService = Symbol('IRawNotebookSupportedService'); +export interface IRawNotebookSupportedService { + supported(): Promise; +} + +// Provides notebooks that talk directly to kernels as opposed to a jupyter server +export const IRawNotebookProvider = Symbol('IRawNotebookProvider'); +export interface IRawNotebookProvider extends IAsyncDisposable { + supported(): Promise; + connect(connect: ConnectNotebookProviderOptions): Promise; + createNotebook( + identity: Uri, + resource: Resource, + disableUI?: boolean, + notebookMetadata?: nbformat.INotebookMetadata, + cancelToken?: CancellationToken + ): Promise; + getNotebook(identity: Uri, token?: CancellationToken): Promise; +} + +// Provides notebooks that talk to jupyter servers +export const IJupyterNotebookProvider = Symbol('IJupyterNotebookProvider'); +export interface IJupyterNotebookProvider { + connect(options: ConnectNotebookProviderOptions): Promise; + createNotebook(options: GetNotebookOptions): Promise; + getNotebook(options: GetNotebookOptions): Promise; + disconnect(options: ConnectNotebookProviderOptions): Promise; +} + +export interface INotebook extends IAsyncDisposable { + readonly resource: Resource; + readonly connection: INotebookProviderConnection | undefined; + kernelSocket: Observable; + readonly identity: Uri; + readonly status: ServerStatus; + readonly disposed: boolean; + readonly session: IJupyterSession; // Temporary. This just makes it easier to write a notebook that works with VS code types. + onSessionStatusChanged: Event; + onDisposed: Event; + onKernelChanged: Event; + onKernelRestarted: Event; + onKernelInterrupted: Event; + clear(id: string): void; + executeObservable(code: string, file: string, line: number, id: string, silent: boolean): Observable; + execute( + code: string, + file: string, + line: number, + id: string, + cancelToken?: CancellationToken, + silent?: boolean + ): Promise; + inspect(code: string, offsetInCode?: number, cancelToken?: CancellationToken): Promise; + getCompletion( + cellCode: string, + offsetInCode: number, + cancelToken?: CancellationToken + ): Promise; + restartKernel(timeoutInMs: number): Promise; + waitForIdle(timeoutInMs: number): Promise; + interruptKernel(timeoutInMs: number): Promise; + setLaunchingFile(file: string): Promise; + getSysInfo(): Promise; + setMatplotLibStyle(useDark: boolean): Promise; + getMatchingInterpreter(): PythonEnvironment | undefined; + /** + * Gets the metadata that's used to start/connect to a Kernel. + */ + getKernelConnection(): KernelConnectionMetadata | undefined; + /** + * Sets the metadata that's used to start/connect to a Kernel. + * Doing so results in a new kernel being started (i.e. a change in the kernel). + */ + setKernelConnection(connectionMetadata: KernelConnectionMetadata, timeoutMS: number): Promise; + getLoggers(): INotebookExecutionLogger[]; + registerIOPubListener(listener: (msg: KernelMessage.IIOPubMessage, requestId: string) => void): void; + registerCommTarget( + targetName: string, + callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike + ): void; + sendCommMessage( + buffers: (ArrayBuffer | ArrayBufferView)[], + content: { comm_id: string; data: JSONObject; target_name: string | undefined }, + // tslint:disable-next-line: no-any + metadata: any, + // tslint:disable-next-line: no-any + msgId: any + ): Kernel.IShellFuture< + KernelMessage.IShellMessage<'comm_msg'>, + KernelMessage.IShellMessage + >; + requestCommInfo(content: KernelMessage.ICommInfoRequestMsg['content']): Promise; + registerMessageHook( + msgId: string, + hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void; + removeMessageHook(msgId: string, hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike): void; +} + +// Options for connecting to a notebook provider +export type ConnectNotebookProviderOptions = { + getOnly?: boolean; + disableUI?: boolean; + localOnly?: boolean; + token?: CancellationToken; + onConnectionMade?(): void; // Optional callback for when the first connection is made +}; + +export interface INotebookServerOptions { + uri?: string; + usingDarkTheme?: boolean; + skipUsingDefaultConfig?: boolean; + workingDir?: string; + purpose: string; + metadata?: INotebookMetadataLive; + disableUI?: boolean; + skipSearchingForKernel?: boolean; + allowUI(): boolean; +} + +export const INotebookExecutionLogger = Symbol('INotebookExecutionLogger'); +export interface INotebookExecutionLogger extends IDisposable { + preExecute(cell: ICell, silent: boolean): Promise; + postExecute(cell: ICell, silent: boolean): Promise; + onKernelRestarted(): void; + preHandleIOPub?(msg: KernelMessage.IIOPubMessage): KernelMessage.IIOPubMessage; +} + +export interface IGatherProvider { + logExecution(vscCell: ICell): void; + gatherCode(vscCell: ICell): string; + resetLog(): void; +} + +export const IGatherLogger = Symbol('IGatherLogger'); +export interface IGatherLogger extends INotebookExecutionLogger { + getGatherProvider(): IGatherProvider | undefined; +} + +export const IJupyterExecution = Symbol('IJupyterExecution'); +export interface IJupyterExecution extends IAsyncDisposable { + serverStarted: Event; + isNotebookSupported(cancelToken?: CancellationToken): Promise; + isImportSupported(cancelToken?: CancellationToken): Promise; + isSpawnSupported(cancelToken?: CancellationToken): Promise; + connectToNotebookServer( + options?: INotebookServerOptions, + cancelToken?: CancellationToken + ): Promise; + spawnNotebook(file: string): Promise; + importNotebook(file: Uri, template: string | undefined): Promise; + getUsableJupyterPython(cancelToken?: CancellationToken): Promise; + getServer(options?: INotebookServerOptions): Promise; + getNotebookError(): Promise; + refreshCommands(): Promise; +} + +export const IJupyterDebugger = Symbol('IJupyterDebugger'); +export interface IJupyterDebugger { + readonly isRunningByLine: boolean; + startRunByLine(notebook: INotebook, cellHashFileName: string): Promise; + startDebugging(notebook: INotebook): Promise; + stopDebugging(notebook: INotebook): Promise; + onRestart(notebook: INotebook): void; +} + +export interface IJupyterPasswordConnectInfo { + requestHeaders?: HeadersInit; + remappedBaseUrl?: string; + remappedToken?: string; +} + +export const IJupyterPasswordConnect = Symbol('IJupyterPasswordConnect'); +export interface IJupyterPasswordConnect { + getPasswordConnectionInfo(url: string): Promise; +} + +export const IJupyterSession = Symbol('IJupyterSession'); +export interface IJupyterSession extends IAsyncDisposable { + onSessionStatusChanged: Event; + onIoPubMessage: Event; + readonly status: ServerStatus; + readonly workingDirectory: string; + readonly kernelSocket: Observable; + restart(timeout: number): Promise; + interrupt(timeout: number): Promise; + waitForIdle(timeout: number): Promise; + requestExecute( + content: KernelMessage.IExecuteRequestMsg['content'], + disposeOnDone?: boolean, + metadata?: JSONObject + ): Kernel.IShellFuture | undefined; + requestComplete( + content: KernelMessage.ICompleteRequestMsg['content'] + ): Promise; + requestInspect( + content: KernelMessage.IInspectRequestMsg['content'] + ): Promise; + sendInputReply(content: string): void; + changeKernel(kernelConnection: KernelConnectionMetadata, timeoutMS: number): Promise; + registerCommTarget( + targetName: string, + callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike + ): void; + sendCommMessage( + buffers: (ArrayBuffer | ArrayBufferView)[], + content: { comm_id: string; data: JSONObject; target_name: string | undefined }, + // tslint:disable-next-line: no-any + metadata: any, + // tslint:disable-next-line: no-any + msgId: any + ): Kernel.IShellFuture< + KernelMessage.IShellMessage<'comm_msg'>, + KernelMessage.IShellMessage + >; + requestCommInfo(content: KernelMessage.ICommInfoRequestMsg['content']): Promise; + registerMessageHook( + msgId: string, + hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike + ): void; + removeMessageHook(msgId: string, hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike): void; +} + +export type ISessionWithSocket = Session.ISession & { + // Whether this is a remote session that we attached to. + isRemoteSession?: boolean; + // Socket information used for hooking messages to the kernel + kernelSocketInformation?: KernelSocketInformation; +}; + +export const IJupyterSessionManagerFactory = Symbol('IJupyterSessionManagerFactory'); +export interface IJupyterSessionManagerFactory { + readonly onRestartSessionCreated: Event; + readonly onRestartSessionUsed: Event; + create(connInfo: IJupyterConnection, failOnPassword?: boolean): Promise; +} + +export interface IJupyterSessionManager extends IAsyncDisposable { + readonly onRestartSessionCreated: Event; + readonly onRestartSessionUsed: Event; + startNew( + kernelConnection: KernelConnectionMetadata | undefined, + workingDirectory: string, + cancelToken?: CancellationToken + ): Promise; + getKernelSpecs(): Promise; + getConnInfo(): IJupyterConnection; + getRunningKernels(): Promise; + getRunningSessions(): Promise; +} + +export interface IJupyterKernel { + /** + * Id of an existing (active) Kernel from an active session. + * + * @type {string} + * @memberof IJupyterKernel + */ + id?: string; + name: string; + lastActivityTime: Date; + numberOfConnections: number; +} + +export interface IJupyterKernelSpec { + /** + * Id of an existing (active) Kernel from an active session. + * + * @type {string} + * @memberof IJupyterKernel + */ + id?: string; + name: string; + language: string; + path: string; + env: NodeJS.ProcessEnv | undefined; + /** + * Kernel display name. + * + * @type {string} + * @memberof IJupyterKernelSpec + */ + readonly display_name: string; + /** + * A dictionary of additional attributes about this kernel; used by clients to aid in kernel selection. + * Optionally storing the interpreter information in the metadata (helping extension search for kernels that match an interpereter). + */ + // tslint:disable-next-line: no-any + readonly metadata?: Record & { interpreter?: Partial }; + readonly argv: string[]; +} + +export const INotebookImporter = Symbol('INotebookImporter'); +export interface INotebookImporter extends Disposable { + importFromFile(contentsFile: Uri): Promise; +} + +export const INotebookExporter = Symbol('INotebookExporter'); +export interface INotebookExporter extends Disposable { + translateToNotebook( + cells: ICell[], + directoryChange?: string, + kernelSpec?: nbformat.IKernelspecMetadata + ): Promise; + exportToFile(cells: ICell[], file: string, showOpenPrompt?: boolean): Promise; +} + +export const IInteractiveWindowProvider = Symbol('IInteractiveWindowProvider'); +export interface IInteractiveWindowProvider { + /** + * The active interactive window if it has the focus. + */ + readonly activeWindow: IInteractiveWindow | undefined; + /** + * List of open interactive windows + */ + readonly windows: ReadonlyArray; + /** + * Event fired when the active interactive window changes + */ + readonly onDidChangeActiveInteractiveWindow: Event; + /** + * Gets or creates a new interactive window and associates it with the owner. If no owner, marks as a non associated. + * @param owner file that started this interactive window + */ + getOrCreate(owner: Resource): Promise; + /** + * Synchronizes with the other peers in a live share connection to make sure it has the same window open + * @param window window on this side + */ + synchronize(window: IInteractiveWindow): Promise; +} + +export const IDataScienceErrorHandler = Symbol('IDataScienceErrorHandler'); +export interface IDataScienceErrorHandler { + handleError(err: Error): Promise; +} + +/** + * Given a local resource this will convert the Uri into a form such that it can be used in a WebView. + */ +export interface ILocalResourceUriConverter { + /** + * Root folder that scripts should be copied to. + */ + readonly rootScriptFolder: Uri; + /** + * Convert a uri for the local file system to one that can be used inside webviews. + * + * Webviews cannot directly load resources from the workspace or local file system using `file:` uris. The + * `asWebviewUri` function takes a local `file:` uri and converts it into a uri that can be used inside of + * a webview to load the same resource: + * + * ```ts + * webview.html = `` + * ``` + */ + asWebviewUri(localResource: Uri): Promise; +} + +export interface IInteractiveBase extends Disposable { + onExecutedCode: Event; + notebook?: INotebook; + startProgress(): void; + stopProgress(): void; + undoCells(): void; + redoCells(): void; + removeAllCells(): void; + interruptKernel(): Promise; + restartKernel(): Promise; + hasCell(id: string): Promise; +} + +export const IInteractiveWindow = Symbol('IInteractiveWindow'); +export interface IInteractiveWindow extends IInteractiveBase { + readonly onDidChangeViewState: Event; + readonly visible: boolean; + readonly active: boolean; + readonly owner: Resource; + readonly submitters: Uri[]; + readonly identity: Uri; + readonly title: string; + closed: Event; + addCode(code: string, file: Uri, line: number, editor?: TextEditor, runningStopWatch?: StopWatch): Promise; + addMessage(message: string): Promise; + debugCode( + code: string, + file: Uri, + line: number, + editor?: TextEditor, + runningStopWatch?: StopWatch + ): Promise; + expandAllCells(): void; + collapseAllCells(): void; + exportCells(): void; + scrollToCell(id: string): void; +} + +export interface IInteractiveWindowLoadable extends IInteractiveWindow { + changeMode(newMode: InteractiveWindowMode): void; +} + +// For native editing, the provider acts like the IDocumentManager for normal docs +export const INotebookEditorProvider = Symbol('INotebookEditorProvider'); +export interface INotebookEditorProvider { + readonly activeEditor: INotebookEditor | undefined; + readonly editors: INotebookEditor[]; + readonly onDidOpenNotebookEditor: Event; + readonly onDidChangeActiveNotebookEditor: Event; + readonly onDidCloseNotebookEditor: Event; + open(file: Uri): Promise; + show(file: Uri): Promise; + createNew(contents?: string, title?: string): Promise; +} + +// For native editing, the INotebookEditor acts like a TextEditor and a TextDocument together +export const INotebookEditor = Symbol('INotebookEditor'); +export interface INotebookEditor extends Disposable { + /** + * Type of editor, whether it is the old, custom or native notebook editor. + * Once VSC Notebook is stable, this property can be removed. + */ + readonly type: 'old' | 'custom' | 'native'; + readonly onDidChangeViewState: Event; + readonly closed: Event; + readonly executed: Event; + readonly modified: Event; + readonly saved: Event; + /** + * Is this notebook representing an untitled file which has never been saved yet. + */ + readonly isUntitled: boolean; + /** + * `true` if there are unpersisted changes. + */ + readonly isDirty: boolean; + readonly file: Uri; + readonly visible: boolean; + readonly active: boolean; + readonly model: INotebookModel; + onExecutedCode: Event; + notebook?: INotebook; + show(): Promise; + runAllCells(): void; + runSelectedCell(): void; + addCellBelow(): void; + undoCells(): void; + redoCells(): void; + removeAllCells(): void; + expandAllCells(): void; + collapseAllCells(): void; + interruptKernel(): Promise; + restartKernel(): Promise; +} + +export const IInteractiveWindowListener = Symbol('IInteractiveWindowListener'); + +/** + * Listens to history messages to provide extra functionality + */ +export interface IInteractiveWindowListener extends IDisposable { + /** + * Fires this event when posting a response message + */ + // tslint:disable-next-line: no-any + postMessage: Event<{ message: string; payload: any }>; + /** + * Fires this event when posting a message to the interactive base. + */ + // tslint:disable-next-line: no-any + postInternalMessage?: Event<{ message: string; payload: any }>; + /** + * Handles messages that the interactive window receives + * @param message message type + * @param payload message payload + */ + // tslint:disable-next-line: no-any + onMessage(message: string, payload?: any): void; + /** + * Fired when the view state of the interactive window changes + * @param args + */ + onViewStateChanged?(args: WebViewViewChangeEventArgs): void; +} + +// Wraps the vscode API in order to send messages back and forth from a webview +export const IPostOffice = Symbol('IPostOffice'); +export interface IPostOffice { + // tslint:disable-next-line:no-any + post(message: string, params: any[] | undefined): void; + // tslint:disable-next-line:no-any + listen(message: string, listener: (args: any[] | undefined) => void): void; +} + +// Wraps the vscode CodeLensProvider base class +export const IDataScienceCodeLensProvider = Symbol('IDataScienceCodeLensProvider'); +export interface IDataScienceCodeLensProvider extends CodeLensProvider { + getCodeWatcher(document: TextDocument): ICodeWatcher | undefined; +} + +// Wraps the Code Watcher API +export const ICodeWatcher = Symbol('ICodeWatcher'); +export interface ICodeWatcher { + readonly uri: Uri | undefined; + codeLensUpdated: Event; + setDocument(document: TextDocument): void; + getVersion(): number; + getCodeLenses(): CodeLens[]; + getCachedSettings(): IDataScienceSettings | undefined; + runAllCells(): Promise; + runCell(range: Range): Promise; + debugCell(range: Range): Promise; + runCurrentCell(): Promise; + runCurrentCellAndAdvance(): Promise; + runSelectionOrLine(activeEditor: TextEditor | undefined): Promise; + runToLine(targetLine: number): Promise; + runFromLine(targetLine: number): Promise; + runAllCellsAbove(stopLine: number, stopCharacter: number): Promise; + runCellAndAllBelow(startLine: number, startCharacter: number): Promise; + runFileInteractive(): Promise; + debugFileInteractive(): Promise; + addEmptyCellToBottom(): Promise; + runCurrentCellAndAddBelow(): Promise; + insertCellBelowPosition(): void; + insertCellBelow(): void; + insertCellAbove(): void; + deleteCells(): void; + selectCell(): void; + selectCellContents(): void; + extendSelectionByCellAbove(): void; + extendSelectionByCellBelow(): void; + moveCellsUp(): Promise; + moveCellsDown(): Promise; + changeCellToMarkdown(): void; + changeCellToCode(): void; + debugCurrentCell(): Promise; + gotoNextCell(): void; + gotoPreviousCell(): void; +} + +export const ICodeLensFactory = Symbol('ICodeLensFactory'); +export interface ICodeLensFactory { + updateRequired: Event; + createCodeLenses(document: TextDocument): CodeLens[]; + getCellRanges(document: TextDocument): ICellRange[]; +} + +export enum CellState { + editing = -1, + init = 0, + executing = 1, + finished = 2, + error = 3 +} + +// Basic structure for a cell from a notebook +export interface ICell { + id: string; // This value isn't unique. File and line are needed too. + file: string; + line: number; + state: CellState; + data: nbformat.ICodeCell | nbformat.IRawCell | nbformat.IMarkdownCell | IMessageCell; + extraLines?: number[]; +} + +// CellRange is used as the basis for creating new ICells. +// Was only intended to aggregate together ranges to create an ICell +// However the "range" aspect is useful when working with plain text document +// Ultimately, it would probably be ideal to be ICell and change line to range. +// Specificially see how this is being used for the ICodeLensFactory to +// provide cells for the CodeWatcher to use. +export interface ICellRange { + range: Range; + title: string; + cell_type: string; +} + +export interface IInteractiveWindowInfo { + cellCount: number; + undoCount: number; + redoCount: number; + selectedCell: string | undefined; +} + +export interface IMessageCell extends nbformat.IBaseCell { + cell_type: 'messages'; + messages: string[]; +} + +export const ICodeCssGenerator = Symbol('ICodeCssGenerator'); +export interface ICodeCssGenerator { + generateThemeCss(resource: Resource, isDark: boolean, theme: string): Promise; + generateMonacoTheme(resource: Resource, isDark: boolean, theme: string): Promise; +} + +export const IThemeFinder = Symbol('IThemeFinder'); +export interface IThemeFinder { + findThemeRootJson(themeName: string): Promise; + findTmLanguage(language: string): Promise; + findLanguageConfiguration(language: string): Promise; + isThemeDark(themeName: string): Promise; +} + +export const IStatusProvider = Symbol('IStatusProvider'); +export interface IStatusProvider { + // call this function to set the new status on the active + // interactive window. Dispose of the returned object when done. + set( + message: string, + showInWebView: boolean, + timeout?: number, + canceled?: () => void, + interactivePanel?: IInteractiveBase + ): Disposable; + + // call this function to wait for a promise while displaying status + waitWithStatus( + promise: () => Promise, + message: string, + showInWebView: boolean, + timeout?: number, + canceled?: () => void, + interactivePanel?: IInteractiveBase + ): Promise; +} + +export interface IJupyterCommand { + interpreter(): Promise; + execObservable(args: string[], options: SpawnOptions): Promise>; + exec(args: string[], options: SpawnOptions): Promise>; +} + +export const IJupyterCommandFactory = Symbol('IJupyterCommandFactory'); +export interface IJupyterCommandFactory { + createInterpreterCommand( + command: JupyterCommands, + moduleName: string, + args: string[], + interpreter: PythonEnvironment, + isActiveInterpreter: boolean + ): IJupyterCommand; + createProcessCommand(exe: string, args: string[]): IJupyterCommand; +} + +// Config settings we pass to our react code +export type FileSettings = { + autoSaveDelay: number; + autoSave: 'afterDelay' | 'off' | 'onFocusChange' | 'onWindowChange'; +}; + +export interface IDataScienceExtraSettings extends IDataScienceSettings { + extraSettings: { + editor: { + cursor: string; + cursorBlink: string; + fontLigatures: boolean; + autoClosingBrackets: string; + autoClosingQuotes: string; + autoSurround: string; + autoIndent: boolean; + scrollBeyondLastLine: boolean; + horizontalScrollbarSize: number; + verticalScrollbarSize: number; + fontSize: number; + fontFamily: string; + }; + theme: string; + useCustomEditorApi: boolean; + }; + intellisenseOptions: { + quickSuggestions: { + other: boolean; + comments: boolean; + strings: boolean; + }; + acceptSuggestionOnEnter: boolean | 'on' | 'smart' | 'off'; + quickSuggestionsDelay: number; + suggestOnTriggerCharacters: boolean; + tabCompletion: boolean | 'on' | 'off' | 'onlySnippets'; + suggestLocalityBonus: boolean; + suggestSelection: 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix'; + wordBasedSuggestions: boolean; + parameterHintsEnabled: boolean; + }; + variableOptions: { + enableDuringDebugger: boolean; + }; + + webviewExperiments: { + removeKernelToolbarInInteractiveWindow: boolean; + }; + + gatherIsInstalled: boolean; +} + +// Get variables from the currently running active Jupyter server +// Note: This definition is used implicitly by getJupyterVariableValue.py file +// Changes here may need to be reflected there as well +export interface IJupyterVariable { + name: string; + value: string | undefined; + executionCount?: number; + supportsDataExplorer: boolean; + type: string; + size: number; + shape: string; + count: number; + truncated: boolean; + columns?: { key: string; type: string }[]; + rowCount?: number; + indexColumn?: string; +} + +export const IJupyterVariableDataProvider = Symbol('IJupyterVariableDataProvider'); +export interface IJupyterVariableDataProvider extends IDataViewerDataProvider { + setDependencies(variable: IJupyterVariable, notebook: INotebook): void; +} + +export const IJupyterVariableDataProviderFactory = Symbol('IJupyterVariableDataProviderFactory'); +export interface IJupyterVariableDataProviderFactory { + create(variable: IJupyterVariable, notebook: INotebook): Promise; +} + +export const IJupyterVariables = Symbol('IJupyterVariables'); +export interface IJupyterVariables { + readonly refreshRequired: Event; + getVariables(notebook: INotebook, request: IJupyterVariablesRequest): Promise; + getDataFrameInfo(targetVariable: IJupyterVariable, notebook: INotebook): Promise; + getDataFrameRows( + targetVariable: IJupyterVariable, + notebook: INotebook, + start: number, + end: number + ): Promise; + getMatchingVariable( + notebook: INotebook, + name: string, + cancelToken?: CancellationToken + ): Promise; +} + +export interface IConditionalJupyterVariables extends IJupyterVariables { + readonly active: boolean; +} + +// Request for variables +export interface IJupyterVariablesRequest { + executionCount: number; + refreshCount: number; + sortColumn: string; + sortAscending: boolean; + startIndex: number; + pageSize: number; +} + +// Response to a request +export interface IJupyterVariablesResponse { + executionCount: number; + totalCount: number; + pageStartIndex: number; + pageResponse: IJupyterVariable[]; + refreshCount: number; +} + +export const IPlotViewerProvider = Symbol('IPlotViewerProvider'); +export interface IPlotViewerProvider { + showPlot(imageHtml: string): Promise; +} +export const IPlotViewer = Symbol('IPlotViewer'); + +export interface IPlotViewer extends IDisposable { + closed: Event; + removed: Event; + addPlot(imageHtml: string): Promise; + show(): Promise; +} + +export interface ISourceMapMapping { + line: number; + endLine: number; + runtimeSource: { path: string }; + runtimeLine: number; +} + +export interface ISourceMapRequest { + source: { path: string }; + pydevdSourceMaps: ISourceMapMapping[]; +} + +export interface ICellHash { + line: number; // 1 based + endLine: number; // 1 based and inclusive + runtimeLine: number; // Line in the jupyter source to start at + hash: string; + executionCount: number; + id: string; // Cell id as sent to jupyter + timestamp: number; +} + +export interface IFileHashes { + file: string; + hashes: ICellHash[]; +} + +export const ICellHashListener = Symbol('ICellHashListener'); +export interface ICellHashListener { + hashesUpdated(hashes: IFileHashes[]): Promise; +} + +export const ICellHashProvider = Symbol('ICellHashProvider'); +export interface ICellHashProvider { + updated: Event; + getHashes(): IFileHashes[]; + getExecutionCount(): number; + incExecutionCount(): void; + generateHashFileName(cell: ICell, executionCount: number): string; +} + +export interface IDebugLocation { + fileName: string; + lineNumber: number; + column: number; +} + +export const IDebugLocationTracker = Symbol('IDebugLocationTracker'); +export interface IDebugLocationTracker { + updated: Event; + getLocation(debugSession: DebugSession): IDebugLocation | undefined; +} + +export const IJupyterSubCommandExecutionService = Symbol('IJupyterSubCommandExecutionService'); +/** + * Responsible for execution of jupyter subcommands such as `notebook`, `nbconvert`, etc. + * The executed code is as follows `python -m jupyter `. + * + * @export + * @interface IJupyterSubCommandExecutionService + */ +export interface IJupyterSubCommandExecutionService { + /** + * Checks whether notebook is supported. + * + * @param {CancellationToken} [cancelToken] + * @returns {Promise} + * @memberof IJupyterSubCommandExecutionService + */ + isNotebookSupported(cancelToken?: CancellationToken): Promise; + /** + * Checks whether exporting of ipynb is supported. + * + * @param {CancellationToken} [cancelToken] + * @returns {Promise} + * @memberof IJupyterSubCommandExecutionService + */ + isExportSupported(cancelToken?: CancellationToken): Promise; + /** + * Error message indicating why jupyter notebook isn't supported. + * + * @returns {Promise} + * @memberof IJupyterSubCommandExecutionService + */ + getReasonForJupyterNotebookNotBeingSupported(): Promise; + /** + * Used to refresh the command finder. + * + * @returns {Promise} + * @memberof IJupyterSubCommandExecutionService + */ + refreshCommands(): Promise; + /** + * Gets the interpreter to be used for starting of jupyter server. + * + * @param {CancellationToken} [token] + * @returns {(Promise)} + * @memberof IJupyterInterpreterService + */ + getSelectedInterpreter(token?: CancellationToken): Promise; + /** + * Starts the jupyter notebook server + * + * @param {string[]} notebookArgs + * @param {SpawnOptions} options + * @returns {Promise>} + * @memberof IJupyterSubCommandExecutionService + */ + startNotebook(notebookArgs: string[], options: SpawnOptions): Promise>; + /** + * Gets a list of all locally running jupyter notebook servers. + * + * @param {CancellationToken} [token] + * @returns {(Promise)} + * @memberof IJupyterSubCommandExecutionService + */ + getRunningJupyterServers(token?: CancellationToken): Promise; + /** + * Exports a given notebook into a python file. + * + * @param {string} file + * @param {string} [template] + * @param {CancellationToken} [token] + * @returns {Promise} + * @memberof IJupyterSubCommandExecutionService + */ + exportNotebookToPython(file: Uri, template?: string, token?: CancellationToken): Promise; + /** + * Opens an ipynb file in a new instance of a jupyter notebook server. + * + * @param {string} notebookFile + * @returns {Promise} + * @memberof IJupyterSubCommandExecutionService + */ + openNotebook(notebookFile: string): Promise; + /** + * Gets the kernelspecs. + * + * @param {CancellationToken} [token] + * @returns {Promise} + * @memberof IJupyterSubCommandExecutionService + */ + getKernelSpecs(token?: CancellationToken): Promise; +} + +export const IJupyterInterpreterDependencyManager = Symbol('IJupyterInterpreterDependencyManager'); +export interface IJupyterInterpreterDependencyManager { + /** + * Installs the dependencies required to launch jupyter. + * + * @param {JupyterInstallError} [err] + * @returns {Promise} + * @memberof IJupyterInterpreterDependencyManager + */ + installMissingDependencies(err?: JupyterInstallError): Promise; +} + +export interface INotebookModel { + readonly indentAmount: string; + readonly onDidDispose: Event; + readonly file: Uri; + readonly isDirty: boolean; + readonly isUntitled: boolean; + readonly changed: Event; + readonly cells: readonly Readonly[]; + readonly onDidEdit: Event; + readonly isDisposed: boolean; + readonly metadata: INotebookMetadataLive | undefined; + readonly isTrusted: boolean; + getContent(): string; + update(change: NotebookModelChange): void; + /** + * Dispose of the Notebook model. + * + * This is invoked when there are no more references to a given `NotebookModel` (for example when + * all editors associated with the document have been closed.) + */ + dispose(): void; + /** + * Trusts a notebook document. + */ + trust(): void; +} + +export const INotebookStorage = Symbol('INotebookStorage'); + +export interface INotebookStorage { + generateBackupId(model: INotebookModel): string; + save(model: INotebookModel, cancellation: CancellationToken): Promise; + saveAs(model: INotebookModel, targetResource: Uri): Promise; + backup(model: INotebookModel, cancellation: CancellationToken, backupId?: string): Promise; + get(file: Uri): INotebookModel | undefined; + getOrCreateModel( + file: Uri, + contents?: string, + backupId?: string, + forVSCodeNotebook?: boolean + ): Promise; + getOrCreateModel( + file: Uri, + contents?: string, + // tslint:disable-next-line: unified-signatures + skipDirtyContents?: boolean, + forVSCodeNotebook?: boolean + ): Promise; + revert(model: INotebookModel, cancellation: CancellationToken): Promise; + deleteBackup(model: INotebookModel, backupId?: string): Promise; +} +type WebViewViewState = { + readonly visible: boolean; + readonly active: boolean; +}; +export type WebViewViewChangeEventArgs = { current: WebViewViewState; previous: WebViewViewState }; + +export type GetServerOptions = { + getOnly?: boolean; + disableUI?: boolean; + localOnly?: boolean; + token?: CancellationToken; + onConnectionMade?(): void; // Optional callback for when the first connection is made +}; + +/** + * Options for getting a notebook + */ +export type GetNotebookOptions = { + resource?: Uri; + identity: Uri; + getOnly?: boolean; + disableUI?: boolean; + metadata?: nbformat.INotebookMetadata & { id?: string }; + token?: CancellationToken; +}; + +export const INotebookProvider = Symbol('INotebookProvider'); +export interface INotebookProvider { + readonly type: 'raw' | 'jupyter'; + /** + * Fired when a notebook has been created for a given Uri/Identity + */ + onNotebookCreated: Event<{ identity: Uri; notebook: INotebook }>; + onSessionStatusChanged: Event<{ status: ServerStatus; notebook: INotebook }>; + + /** + * Fired just the first time that this provider connects + */ + onConnectionMade: Event; + /** + * Fired when a kernel would have been changed if a notebook had existed. + */ + onPotentialKernelChanged: Event<{ identity: Uri; kernelConnection: KernelConnectionMetadata }>; + + /** + * List of all notebooks (active and ones that are being constructed). + */ + activeNotebooks: Promise[]; + /** + * Disposes notebook associated with the given identity. + * Using `getOrCreateNotebook` would be incorrect as thats async, and its possible a document has been opened in the interim (meaning we could end up disposing something that is required). + */ + disposeAssociatedNotebook(options: { identity: Uri }): void; + /** + * Gets or creates a notebook, and manages the lifetime of notebooks. + */ + getOrCreateNotebook(options: GetNotebookOptions): Promise; + /** + * Connect to a notebook provider to prepare its connection and to get connection information + */ + connect(options: ConnectNotebookProviderOptions): Promise; + + /** + * Disconnect from a notebook provider connection + */ + disconnect(options: ConnectNotebookProviderOptions, cancelToken?: CancellationToken): Promise; + /** + * Fires the potentialKernelChanged event for a notebook that doesn't exist. + * @param identity identity notebook would have + * @param kernel kernel that it was changed to. + */ + firePotentialKernelChanged(identity: Uri, kernel: KernelConnectionMetadata): void; +} + +export const IJupyterServerProvider = Symbol('IJupyterServerProvider'); +export interface IJupyterServerProvider { + /** + * Gets the server used for starting notebooks + */ + getOrCreateServer(options: GetServerOptions): Promise; +} + +export interface IKernelSocket { + // tslint:disable-next-line: no-any + sendToRealKernel(data: any, cb?: (err?: Error) => void): void; + /** + * Adds a listener to a socket that will be called before the socket's onMessage is called. This + * allows waiting for a callback before processing messages + * @param listener + */ + addReceiveHook(hook: (data: WebSocketData) => Promise): void; + /** + * Removes a listener for the socket. When no listeners are present, the socket no longer blocks + * @param listener + */ + removeReceiveHook(hook: (data: WebSocketData) => Promise): void; + /** + * Adds a hook to the sending of data from a websocket. Hooks can block sending so be careful. + * @param patch + */ + // tslint:disable-next-line: no-any + addSendHook(hook: (data: any, cb?: (err?: Error) => void) => Promise): void; + /** + * Removes a send hook from the socket. + * @param hook + */ + // tslint:disable-next-line: no-any + removeSendHook(hook: (data: any, cb?: (err?: Error) => void) => Promise): void; +} + +export type KernelSocketOptions = { + /** + * Kernel Id. + */ + readonly id: string; + /** + * Kernel ClientId. + */ + readonly clientId: string; + /** + * Kernel UserName. + */ + readonly userName: string; + /** + * Kernel model. + */ + readonly model: { + /** + * Unique identifier of the kernel server session. + */ + readonly id: string; + /** + * The name of the kernel. + */ + readonly name: string; + }; +}; +export type KernelSocketInformation = { + /** + * Underlying socket used by jupyterlab/services to communicate with kernel. + * See jupyterlab/services/kernel/default.ts + */ + readonly socket?: IKernelSocket; + /** + * Options used to clone a kernel. + */ + readonly options: KernelSocketOptions; +}; + +export enum KernelInterpreterDependencyResponse { + ok, + cancel +} + +export const IKernelDependencyService = Symbol('IKernelDependencyService'); +export interface IKernelDependencyService { + installMissingDependencies( + interpreter: PythonEnvironment, + token?: CancellationToken + ): Promise; + areDependenciesInstalled(interpreter: PythonEnvironment, _token?: CancellationToken): Promise; +} + +export const INotebookAndInteractiveWindowUsageTracker = Symbol('INotebookAndInteractiveWindowUsageTracker'); +export interface INotebookAndInteractiveWindowUsageTracker { + readonly lastNotebookOpened?: Date; + readonly lastInteractiveWindowOpened?: Date; + startTracking(): void; +} + +export const IJupyterDebugService = Symbol('IJupyterDebugService'); +export interface IJupyterDebugService extends IDebugService { + /** + * Event fired when a breakpoint is hit (debugger has stopped) + */ + readonly onBreakpointHit: Event; + /** + * Start debugging a notebook cell. + * @param nameOrConfiguration Either the name of a debug or compound configuration or a [DebugConfiguration](#DebugConfiguration) object. + * @return A thenable that resolves when debugging could be successfully started. + */ + startRunByLine(config: DebugConfiguration): Thenable; + /** + * Gets the current stack frame for the current thread + */ + getStack(): Promise; + /** + * Steps the current thread. Returns after the request is sent. Wait for onBreakpointHit or onDidTerminateDebugSession to determine when done. + */ + step(): Promise; + /** + * Runs the current thread. Will keep running until a breakpoint or end of session. + */ + continue(): Promise; + /** + * Force a request for variables. DebugAdapterTrackers can listen for the results. + */ + requestVariables(): Promise; + /** + * Stop debugging + */ + stop(): void; +} + +export interface IJupyterServerUri { + baseUrl: string; + token: string; + // tslint:disable-next-line: no-any + authorizationHeader: any; // JSON object for authorization header. + expiration?: Date; // Date/time when header expires and should be refreshed. + displayName: string; +} + +export type JupyterServerUriHandle = string; + +export interface IJupyterUriProvider { + readonly id: string; // Should be a unique string (like a guid) + getQuickPickEntryItems(): QuickPickItem[]; + handleQuickPick(item: QuickPickItem, backEnabled: boolean): Promise; + getServerUri(handle: JupyterServerUriHandle): Promise; +} + +export const IJupyterUriProviderRegistration = Symbol('IJupyterUriProviderRegistration'); + +export interface IJupyterUriProviderRegistration { + getProviders(): Promise>; + registerProvider(picker: IJupyterUriProvider): void; + getJupyterServerUri(id: string, handle: JupyterServerUriHandle): Promise; +} +export const IDigestStorage = Symbol('IDigestStorage'); +export interface IDigestStorage { + readonly key: Promise; + saveDigest(uri: Uri, digest: string): Promise; + containsDigest(uri: Uri, digest: string): Promise; +} + +export const ITrustService = Symbol('ITrustService'); +export interface ITrustService { + readonly onDidSetNotebookTrust: Event; + isNotebookTrusted(uri: Uri, notebookContents: string): Promise; + trustNotebook(uri: Uri, notebookContents: string): Promise; +} + +export const IDataScienceFileSystem = Symbol('IDataScienceFileSystem'); +export interface IDataScienceFileSystem { + // Local-only filesystem utilities + appendLocalFile(path: string, text: string): Promise; + areLocalPathsSame(path1: string, path2: string): boolean; + createLocalDirectory(path: string): Promise; + createLocalWriteStream(path: string): WriteStream; + copyLocal(source: string, destination: string): Promise; + createTemporaryLocalFile(fileExtension: string, mode?: number): Promise; + deleteLocalDirectory(dirname: string): Promise; + deleteLocalFile(path: string): Promise; + getDisplayName(path: string): string; + getFileHash(path: string): Promise; + localDirectoryExists(dirname: string): Promise; + localFileExists(filename: string): Promise; + readLocalData(path: string): Promise; + readLocalFile(path: string): Promise; + searchLocal(globPattern: string, cwd?: string, dot?: boolean): Promise; + writeLocalFile(path: string, text: string | Buffer): Promise; + + // URI-based filesystem utilities wrapping the VS Code filesystem API + arePathsSame(path1: Uri, path2: Uri): boolean; + copy(source: Uri, destination: Uri): Promise; + createDirectory(uri: Uri): Promise; + delete(uri: Uri): Promise; + readFile(uri: Uri): Promise; + stat(uri: Uri): Promise; + writeFile(uri: Uri, text: string | Buffer): Promise; +} +export interface ISwitchKernelOptions { + identity: Resource; + resource: Resource; + currentKernelDisplayName: string | undefined; +} diff --git a/src/client/datascience/utils.ts b/src/client/datascience/utils.ts new file mode 100644 index 000000000000..f91b905b9f02 --- /dev/null +++ b/src/client/datascience/utils.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as path from 'path'; + +import { IWorkspaceService } from '../common/application/types'; + +import { IConfigurationService } from '../common/types'; +import { IDataScienceFileSystem } from './types'; + +export async function calculateWorkingDirectory( + configService: IConfigurationService, + workspace: IWorkspaceService, + fs: IDataScienceFileSystem +): Promise { + let workingDir: string | undefined; + // For a local launch calculate the working directory that we should switch into + const settings = configService.getSettings(undefined); + const fileRoot = settings.datascience.notebookFileRoot; + + // If we don't have a workspace open the notebookFileRoot seems to often have a random location in it (we use ${workspaceRoot} as default) + // so only do this setting if we actually have a valid workspace open + if (fileRoot && workspace.hasWorkspaceFolders) { + const workspaceFolderPath = workspace.workspaceFolders![0].uri.fsPath; + if (path.isAbsolute(fileRoot)) { + if (await fs.localDirectoryExists(fileRoot)) { + // User setting is absolute and exists, use it + workingDir = fileRoot; + } else { + // User setting is absolute and doesn't exist, use workspace + workingDir = workspaceFolderPath; + } + } else if (!fileRoot.includes('${')) { + // fileRoot is a relative path, combine it with the workspace folder + const combinedPath = path.join(workspaceFolderPath, fileRoot); + if (await fs.localDirectoryExists(combinedPath)) { + // combined path exists, use it + workingDir = combinedPath; + } else { + // Combined path doesn't exist, use workspace + workingDir = workspaceFolderPath; + } + } else { + // fileRoot is a variable that hasn't been expanded + workingDir = fileRoot; + } + } + return workingDir; +} diff --git a/src/client/datascience/webviews/webviewHost.ts b/src/client/datascience/webviews/webviewHost.ts new file mode 100644 index 000000000000..0b10c9e20a32 --- /dev/null +++ b/src/client/datascience/webviews/webviewHost.ts @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { injectable, unmanaged } from 'inversify'; +import { ConfigurationChangeEvent, extensions, Uri, WorkspaceConfiguration } from 'vscode'; + +import { IWebview, IWorkspaceService } from '../../common/application/types'; +import { isTestExecution } from '../../common/constants'; +import { IConfigurationService, IDisposable, Resource } from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import * as localize from '../../common/utils/localize'; +import { captureTelemetry } from '../../telemetry'; +import { DefaultTheme, GatherExtension, Telemetry } from '../constants'; +import { CssMessages, IGetCssRequest, IGetMonacoThemeRequest, SharedMessages } from '../messages'; +import { ICodeCssGenerator, IDataScienceExtraSettings, IThemeFinder } from '../types'; + +@injectable() // For some reason this is necessary to get the class hierarchy to work. +export abstract class WebviewHost implements IDisposable { + protected webview?: IWebview; + protected disposed: boolean = false; + + protected themeIsDarkPromise: Deferred | undefined = createDeferred(); + protected webviewInit: Deferred | undefined = createDeferred(); + + protected readonly _disposables: IDisposable[] = []; + constructor( + @unmanaged() protected configService: IConfigurationService, + @unmanaged() private cssGenerator: ICodeCssGenerator, + @unmanaged() protected themeFinder: IThemeFinder, + @unmanaged() protected workspaceService: IWorkspaceService, + @unmanaged() protected readonly useCustomEditorApi: boolean, + @unmanaged() private readonly enableVariablesDuringDebugging: boolean, + @unmanaged() private readonly hideKernelToolbarInInteractiveWindow: Promise + ) { + // Listen for settings changes from vscode. + this._disposables.push(this.workspaceService.onDidChangeConfiguration(this.onPossibleSettingsChange, this)); + + // Listen for settings changes + this._disposables.push( + this.configService.getSettings(undefined).onDidChange(this.onDataScienceSettingsChanged.bind(this)) + ); + } + + public dispose() { + if (!this.disposed) { + this.disposed = true; + this.themeIsDarkPromise = undefined; + this._disposables.forEach((item) => item.dispose()); + } + + this.webviewInit = undefined; + } + + public setTheme(isDark: boolean) { + if (this.themeIsDarkPromise && !this.themeIsDarkPromise.resolved) { + this.themeIsDarkPromise.resolve(isDark); + } else { + this.themeIsDarkPromise = createDeferred(); + this.themeIsDarkPromise.resolve(isDark); + } + } + + // Post a message to our webview and update our new datascience settings + protected onDataScienceSettingsChanged = async () => { + // Stringify our settings to send over to the panel + const dsSettings = JSON.stringify(await this.generateDataScienceExtraSettings()); + this.postMessageInternal(SharedMessages.UpdateSettings, dsSettings).ignoreErrors(); + }; + + protected asWebviewUri(localResource: Uri) { + if (!this.webview) { + throw new Error('asWebViewUri called too early'); + } + return this.webview?.asWebviewUri(localResource); + } + + protected abstract get owningResource(): Resource; + + protected postMessage(type: T, payload?: M[T]): Promise { + // Then send it the message + return this.postMessageInternal(type.toString(), payload); + } + + //tslint:disable-next-line:no-any + protected onMessage(message: string, payload: any) { + switch (message) { + case CssMessages.GetCssRequest: + this.handleCssRequest(payload as IGetCssRequest).ignoreErrors(); + break; + + case CssMessages.GetMonacoThemeRequest: + this.handleMonacoThemeRequest(payload as IGetMonacoThemeRequest).ignoreErrors(); + break; + + default: + break; + } + } + + protected async generateDataScienceExtraSettings(): Promise { + const resource = this.owningResource; + const editor = this.workspaceService.getConfiguration('editor'); + const workbench = this.workspaceService.getConfiguration('workbench'); + const theme = !workbench ? DefaultTheme : workbench.get('colorTheme', DefaultTheme); + const ext = extensions.getExtension(GatherExtension); + + return { + ...this.configService.getSettings(resource).datascience, + extraSettings: { + editor: { + cursor: this.getValue(editor, 'cursorStyle', 'line'), + cursorBlink: this.getValue(editor, 'cursorBlinking', 'blink'), + autoClosingBrackets: this.getValue(editor, 'autoClosingBrackets', 'languageDefined'), + autoClosingQuotes: this.getValue(editor, 'autoClosingQuotes', 'languageDefined'), + autoSurround: this.getValue(editor, 'autoSurround', 'languageDefined'), + autoIndent: this.getValue(editor, 'autoIndent', false), + fontLigatures: this.getValue(editor, 'fontLigatures', false), + scrollBeyondLastLine: this.getValue(editor, 'scrollBeyondLastLine', true), + // VS Code puts a value for this, but it's 10 (the explorer bar size) not 14 the editor size for vert + verticalScrollbarSize: this.getValue(editor, 'scrollbar.verticalScrollbarSize', 14), + horizontalScrollbarSize: this.getValue(editor, 'scrollbar.horizontalScrollbarSize', 10), + fontSize: this.getValue(editor, 'fontSize', 14), + fontFamily: this.getValue(editor, 'fontFamily', "Consolas, 'Courier New', monospace") + }, + theme: theme, + useCustomEditorApi: this.useCustomEditorApi + }, + intellisenseOptions: { + quickSuggestions: { + other: this.getValue(editor, 'quickSuggestions.other', true), + comments: this.getValue(editor, 'quickSuggestions.comments', false), + strings: this.getValue(editor, 'quickSuggestions.strings', false) + }, + acceptSuggestionOnEnter: this.getValue(editor, 'acceptSuggestionOnEnter', 'on'), + quickSuggestionsDelay: this.getValue(editor, 'quickSuggestionsDelay', 10), + suggestOnTriggerCharacters: this.getValue(editor, 'suggestOnTriggerCharacters', true), + tabCompletion: this.getValue(editor, 'tabCompletion', 'on'), + suggestLocalityBonus: this.getValue(editor, 'suggest.localityBonus', true), + suggestSelection: this.getValue(editor, 'suggestSelection', 'recentlyUsed'), + wordBasedSuggestions: this.getValue(editor, 'wordBasedSuggestions', true), + parameterHintsEnabled: this.getValue(editor, 'parameterHints.enabled', true) + }, + variableOptions: { + enableDuringDebugger: this.enableVariablesDuringDebugging + }, + webviewExperiments: { + removeKernelToolbarInInteractiveWindow: await this.hideKernelToolbarInInteractiveWindow + }, + gatherIsInstalled: ext ? true : false + }; + } + + protected async sendLocStrings() { + const locStrings = isTestExecution() ? '{}' : localize.getCollectionJSON(); + this.postMessageInternal(SharedMessages.LocInit, locStrings).ignoreErrors(); + } + + // tslint:disable-next-line:no-any + protected async postMessageInternal(type: string, payload?: any): Promise { + if (this.webviewInit) { + // Make sure the webpanel is up before we send it anything. + await this.webviewInit.promise; + + // Then send it the message + this.webview?.postMessage({ type: type.toString(), payload: payload }); + } + } + + protected isDark(): Promise { + return this.themeIsDarkPromise ? this.themeIsDarkPromise.promise : Promise.resolve(false); + } + + @captureTelemetry(Telemetry.WebviewStyleUpdate) + private async handleCssRequest(request: IGetCssRequest): Promise { + const settings = await this.generateDataScienceExtraSettings(); + const requestIsDark = settings.ignoreVscodeTheme ? false : request?.isDark; + this.setTheme(requestIsDark); + const isDark = settings.ignoreVscodeTheme + ? false + : await this.themeFinder.isThemeDark(settings.extraSettings.theme); + const resource = this.owningResource; + const css = await this.cssGenerator.generateThemeCss(resource, requestIsDark, settings.extraSettings.theme); + return this.postMessageInternal(CssMessages.GetCssResponse, { + css, + theme: settings.extraSettings.theme, + knownDark: isDark + }); + } + + @captureTelemetry(Telemetry.WebviewMonacoStyleUpdate) + private async handleMonacoThemeRequest(request: IGetMonacoThemeRequest): Promise { + const settings = await this.generateDataScienceExtraSettings(); + const isDark = settings.ignoreVscodeTheme ? false : request?.isDark; + this.setTheme(isDark); + const resource = this.owningResource; + const monacoTheme = await this.cssGenerator.generateMonacoTheme(resource, isDark, settings.extraSettings.theme); + return this.postMessageInternal(CssMessages.GetMonacoThemeResponse, { theme: monacoTheme }); + } + + private getValue(workspaceConfig: WorkspaceConfiguration, section: string, defaultValue: T): T { + if (workspaceConfig) { + return workspaceConfig.get(section, defaultValue); + } + return defaultValue; + } + + // Post a message to our webpanel and update our new datascience settings + private onPossibleSettingsChange = async (event: ConfigurationChangeEvent) => { + if ( + event.affectsConfiguration('workbench.colorTheme') || + event.affectsConfiguration('editor.fontSize') || + event.affectsConfiguration('editor.fontFamily') || + event.affectsConfiguration('editor.cursorStyle') || + event.affectsConfiguration('editor.cursorBlinking') || + event.affectsConfiguration('editor.autoClosingBrackets') || + event.affectsConfiguration('editor.autoClosingQuotes') || + event.affectsConfiguration('editor.autoSurround') || + event.affectsConfiguration('editor.autoIndent') || + event.affectsConfiguration('editor.scrollBeyondLastLine') || + event.affectsConfiguration('editor.fontLigatures') || + event.affectsConfiguration('editor.scrollbar.verticalScrollbarSize') || + event.affectsConfiguration('editor.scrollbar.horizontalScrollbarSize') || + event.affectsConfiguration('files.autoSave') || + event.affectsConfiguration('files.autoSaveDelay') || + event.affectsConfiguration('python.dataScience.widgetScriptSources') + ) { + // See if the theme changed + const newSettings = await this.generateDataScienceExtraSettings(); + if (newSettings) { + const dsSettings = JSON.stringify(newSettings); + this.postMessageInternal(SharedMessages.UpdateSettings, dsSettings).ignoreErrors(); + } + } + }; +} diff --git a/src/client/datascience/webviews/webviewPanelHost.ts b/src/client/datascience/webviews/webviewPanelHost.ts new file mode 100644 index 000000000000..7f3f5a8c386a --- /dev/null +++ b/src/client/datascience/webviews/webviewPanelHost.ts @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { injectable, unmanaged } from 'inversify'; +import { Uri, ViewColumn, WebviewPanel } from 'vscode'; + +import { + IWebviewPanel, + IWebviewPanelMessageListener, + IWebviewPanelProvider, + IWorkspaceService +} from '../../common/application/types'; +import { traceInfo } from '../../common/logger'; +import { IConfigurationService, IDisposable } from '../../common/types'; +import { createDeferred } from '../../common/utils/async'; +import { noop } from '../../common/utils/misc'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { sendTelemetryEvent } from '../../telemetry'; +import { Telemetry } from '../constants'; +import { SharedMessages } from '../messages'; +import { ICodeCssGenerator, IThemeFinder, WebViewViewChangeEventArgs } from '../types'; +import { WebviewHost } from './webviewHost'; + +@injectable() // For some reason this is necessary to get the class hierarchy to work. +export abstract class WebviewPanelHost extends WebviewHost implements IDisposable { + protected get isDisposed(): boolean { + return this.disposed; + } + protected viewState: { visible: boolean; active: boolean } = { visible: false, active: false }; + private webPanel: IWebviewPanel | undefined; + private messageListener: IWebviewPanelMessageListener; + private startupStopwatch = new StopWatch(); + + constructor( + @unmanaged() protected configService: IConfigurationService, + @unmanaged() private provider: IWebviewPanelProvider, + @unmanaged() cssGenerator: ICodeCssGenerator, + @unmanaged() protected themeFinder: IThemeFinder, + @unmanaged() protected workspaceService: IWorkspaceService, + @unmanaged() + messageListenerCtor: ( + callback: (message: string, payload: {}) => void, + viewChanged: (panel: IWebviewPanel) => void, + disposed: () => void + ) => IWebviewPanelMessageListener, + @unmanaged() private rootPath: string, + @unmanaged() private scripts: string[], + @unmanaged() private _title: string, + @unmanaged() private viewColumn: ViewColumn, + @unmanaged() protected readonly useCustomEditorApi: boolean, + @unmanaged() enableVariablesDuringDebugging: boolean, + @unmanaged() hideKernelToolbarInInteractiveWindow: Promise + ) { + super( + configService, + cssGenerator, + themeFinder, + workspaceService, + useCustomEditorApi, + enableVariablesDuringDebugging, + hideKernelToolbarInInteractiveWindow + ); + + // Create our message listener for our web panel. + this.messageListener = messageListenerCtor( + this.onMessage.bind(this), + this.webPanelViewStateChanged.bind(this), + this.dispose.bind(this) + ); + } + + public async show(preserveFocus: boolean): Promise { + if (!this.isDisposed) { + // Then show our web panel. + if (this.webPanel) { + await this.webPanel.show(preserveFocus); + } + } + } + + public updateCwd(cwd: string): void { + if (this.webPanel) { + this.webPanel.updateCwd(cwd); + } + } + + public dispose() { + if (!this.isDisposed) { + this.disposed = true; + if (this.webPanel) { + this.webPanel.close(); + this.webPanel = undefined; + } + } + + super.dispose(); + } + public get title() { + return this._title; + } + + public setTitle(newTitle: string) { + this._title = newTitle; + if (!this.isDisposed && this.webPanel) { + this.webPanel.setTitle(newTitle); + } + } + + //tslint:disable-next-line:no-any + protected onMessage(message: string, payload: any) { + switch (message) { + case SharedMessages.Started: + this.webPanelRendered(); + break; + + default: + // Forward unhandled messages to the base class + super.onMessage(message, payload); + break; + } + } + + protected shareMessage(type: T, payload?: M[T]) { + // Send our remote message. + this.messageListener.onMessage(type.toString(), payload); + } + + protected onViewStateChanged(_args: WebViewViewChangeEventArgs) { + noop(); + } + + protected async loadWebPanel(cwd: string, webViewPanel?: WebviewPanel) { + // Make not disposed anymore + this.disposed = false; + + // Setup our init promise for the web panel. We use this to make sure we're in sync with our + // react control. + this.webviewInit = this.webviewInit || createDeferred(); + + // Setup a promise that will wait until the webview passes back + // a message telling us what them is in use + this.themeIsDarkPromise = this.themeIsDarkPromise ? this.themeIsDarkPromise : createDeferred(); + + // Load our actual web panel + + traceInfo(`Loading web panel. Panel is ${this.webPanel ? 'set' : 'notset'}`); + + // Create our web panel (it's the UI that shows up for the history) + if (this.webPanel === undefined) { + // Get our settings to pass along to the react control + const settings = await this.generateDataScienceExtraSettings(); + + traceInfo('Loading web view...'); + + const workspaceFolder = this.workspaceService.getWorkspaceFolder(Uri.file(cwd))?.uri; + + // Use this script to create our web view panel. It should contain all of the necessary + // script to communicate with this class. + this.webPanel = await this.provider.create({ + viewColumn: this.viewColumn, + listener: this.messageListener, + title: this.title, + rootPath: this.rootPath, + scripts: this.scripts, + settings, + cwd, + webViewPanel, + additionalPaths: workspaceFolder ? [workspaceFolder.fsPath] : [] + }); + + // Set our webview after load + this.webview = this.webPanel; + + // Track to seee if our web panel fails to load + this._disposables.push(this.webPanel.loadFailed(this.onWebPanelLoadFailed, this)); + + traceInfo('Web view created.'); + } + + // Send the first settings message + this.onDataScienceSettingsChanged().ignoreErrors(); + + // Send the loc strings (skip during testing as it takes up a lot of memory) + this.sendLocStrings().ignoreErrors(); + } + + // If our webpanel fails to load then just dispose ourselves + private onWebPanelLoadFailed = async () => { + this.dispose(); + }; + + private webPanelViewStateChanged = (webPanel: IWebviewPanel) => { + const visible = webPanel.isVisible(); + const active = webPanel.isActive(); + const current = { visible, active }; + const previous = { visible: this.viewState.visible, active: this.viewState.active }; + this.viewState.visible = visible; + this.viewState.active = active; + this.onViewStateChanged({ current, previous }); + }; + + // tslint:disable-next-line:no-any + private webPanelRendered() { + if (this.webviewInit && !this.webviewInit.resolved) { + // Send telemetry for startup + sendTelemetryEvent(Telemetry.WebviewStartup, this.startupStopwatch.elapsedTime, { type: this.title }); + + // Resolve our started promise. This means the webpanel is ready to go. + this.webviewInit.resolve(); + + traceInfo('Web view react rendered'); + } + + // On started, resend our init data. + this.sendLocStrings().ignoreErrors(); + this.onDataScienceSettingsChanged().ignoreErrors(); + } +} diff --git a/src/client/debugger/Common/Contracts.ts b/src/client/debugger/Common/Contracts.ts deleted file mode 100644 index 6e0e4584c4de..000000000000 --- a/src/client/debugger/Common/Contracts.ts +++ /dev/null @@ -1,222 +0,0 @@ -'use strict'; -import * as net from 'net'; -import {DebugProtocol} from 'vscode-debugprotocol'; - -export const DjangoApp = "DJANGO"; -export enum DebugFlags { - None = 0, - IgnoreCommandBursts = 1 -} - -export class DebugOptions { - public static get WaitOnAbnormalExit(): string { return "WaitOnAbnormalExit"; } - public static get WaitOnNormalExit(): string { return "WaitOnNormalExit"; } - public static get RedirectOutput(): string { return "RedirectOutput"; } - public static get DjangoDebugging(): string { return "DjangoDebugging"; } - public static get DebugStdLib(): string { return "DebugStdLib"; } - public static get BreakOnSystemExitZero(): string { return "BreakOnSystemExitZero"; } -} - -export interface ExceptionHandling { - ignore: string[]; - always: string[]; - unhandled: string[]; -} - -export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { - /** An absolute path to the program to debug. */ - program: string; - pythonPath: string; - /** Automatically stop target after launch. If not specified, target does not stop. */ - stopOnEntry?: boolean; - args: string[]; - applicationType?: string; - externalConsole?: boolean; - cwd?: string; - debugOptions?: string[]; - env?: Object; - exceptionHandling?: ExceptionHandling -} -// -// export interface LaunchDjangoRequestArguments extends LaunchRequestArguments { -// port?: number; -// noReload?: boolean; -// settings?: string; -// } - -export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments { - /** An absolute path to local directory with source. */ - localRoot: string; - remoteRoot: string; - port?: number; - host?: string; - secret?: string; -} - -export interface IDebugServer { - port: number; - host?: string; -} - -export enum FrameKind { - None, - Python, - Django -}; -export enum enum_EXCEPTION_STATE { - BREAK_MODE_NEVER = 0, - BREAK_MODE_ALWAYS = 1, - BREAK_MODE_UNHANDLED = 32 -} -export enum PythonLanguageVersion { - Is2, - Is3 -} - -export enum PythonEvaluationResultReprKind { - Normal, - Raw, - RawLen -} - -export enum PythonEvaluationResultFlags { - None = 0, - Expandable = 1, - MethodCall = 2, - SideEffects = 4, - Raw = 8, - HasRawRepr = 16, -} - -export interface IPythonProcess extends NodeJS.EventEmitter { - Connect(buffer: Buffer, socket: net.Socket, isRemoteProcess: boolean); - HandleIncomingData(buffer: Buffer); - Detach(); - Kill(); - SendStepInto(threadId: number); - SendStepOver(threadId: number); - SendStepOut(threadId: number); - SendResumeThread(threadId: number); - AutoResumeThread(threadId: number); - SendClearStepping(threadId: number); - ExecuteText(text: string, reprKind: any, stackFrame: IPythonStackFrame): Promise; - EnumChildren(text: string, stackFrame: IPythonStackFrame, timeout: number): Promise; - SetLineNumber(pythonStackFrame: IPythonStackFrame, lineNo: number); - Threads: Map; - ProgramDirectory: string; - //TODO:Fix this, shouldn't be exposed - PendingChildEnumCommands: Map; - PendingExecuteCommands: Map; -} - -export interface IPythonEvaluationResult { - Flags: PythonEvaluationResultFlags; - IsExpandable: boolean; - StringRepr: string; - HexRepr: string; - TypeName: string; - Length: number; - ExceptionText?: string; - Expression: string; - ChildName: string; - Process: IPythonProcess; - Frame: IPythonStackFrame; -} - - -export interface IPythonModule { - ModuleId: number; - Name: string; - Filename: string; -} - - -export interface IPythonThread { - IsWorkerThread: boolean; - Process: IPythonProcess; - Name: string; - Id: number; - Frames: IPythonStackFrame[]; -} - -export interface IPythonStackFrame { - StartLine: number; - EndLine: number; - Thread: IPythonThread; - LineNo: number; - FunctionName: string; - FileName: string; - Kind: FrameKind; - FrameId: number; - Locals: IPythonEvaluationResult[]; - Parameters: IPythonEvaluationResult[]; -} - -export interface IDjangoStackFrame extends IPythonStackFrame { - SourceFile: string; - SourceLine: number; -} - -export interface IStepCommand { - PromiseResolve: (pyThread: IPythonThread) => void; - PythonThreadId: number; -} - -export interface IBreakpointCommand { - Id: number; - PromiseResolve: () => void; - PromiseReject: () => void; -} -export interface IChildEnumCommand { - Id: number; - Frame: IPythonStackFrame; - PromiseResolve: (value: IPythonEvaluationResult[]) => void; - PromiseReject: () => void; -} -export interface IExecutionCommand { - Id: number; - Text: string; - Frame: IPythonStackFrame; - PromiseResolve: (value: IPythonEvaluationResult) => void; - PromiseReject: (error: string) => void; -} -// Must be in sync with BREAKPOINT_CONDITION_* constants in visualstudio_py_debugger.py. -export enum PythonBreakpointConditionKind { - Always = 0, - WhenTrue = 1, - WhenChanged = 2 -} - -// Must be in sync with BREAKPOINT_PASS_COUNT_* constants in visualstudio_py_debugger.py. -export enum PythonBreakpointPassCountKind { - Always = 0, - Every = 1, - WhenEqual = 2, - WhenEqualOrGreater = 3 -} - -export interface IPythonBreakpoint { - IsDjangoBreakpoint?: boolean; - Id: number; - Filename: string; - LineNo: number; - ConditionKind: PythonBreakpointConditionKind; - Condition: string; - PassCountKind: PythonBreakpointPassCountKind; - PassCount: number; - Enabled: boolean; -} -export interface IPythonException { - TypeName: string; - Description: string; -} - -export enum StreamDataType { - Int32, - Int64, - String -} -export interface IStreamData { - DataType: StreamDataType; - RawData: any; -} \ No newline at end of file diff --git a/src/client/debugger/Common/OnPortOpenedHandler.ts b/src/client/debugger/Common/OnPortOpenedHandler.ts deleted file mode 100644 index 5753f5cd2eed..000000000000 --- a/src/client/debugger/Common/OnPortOpenedHandler.ts +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -import * as net from 'net'; - -export function WaitForPortToOpen(port: number, timeout: number): Promise { - return new Promise((resolve, reject) => { - var timedOut = false; - const handle = setTimeout(() => { - timedOut = true; - reject(`Timeout after ${timeout} milli-seconds`); - }, timeout); - - tryToConnect(); - - function tryToConnect() { - if (timedOut) { - return; - } - - var socket = net.connect({ port: port }, () => { - if (timedOut) { - return; - } - - resolve(); - socket.end(); - clearTimeout(handle); - }); - socket.on("error", error => { - if (timedOut) { - return; - } - - if (error.code === "ECONNREFUSED" && !timedOut) { - setTimeout(() => { - tryToConnect(); - }, 10); - return; - } - - clearTimeout(handle); - reject(`Connection failed due to ${JSON.stringify(error)}`); - }); - } - }); -} \ No newline at end of file diff --git a/src/client/debugger/Common/SocketStream.ts b/src/client/debugger/Common/SocketStream.ts deleted file mode 100644 index c4928096b155..000000000000 --- a/src/client/debugger/Common/SocketStream.ts +++ /dev/null @@ -1,193 +0,0 @@ -'use strict'; - -import * as net from 'net'; -var uint64be = require("uint64be"); - - -export class SocketStream { - constructor(socket: net.Socket, buffer: Buffer) { - this.buffer = buffer; - this.socket = socket; - } - - private socket: net.Socket; - public WriteInt32(num: number) { - this.WriteInt64(num); - } - - public WriteInt64(num: number) { - var buffer = uint64be.encode(num); - this.socket.write(buffer); - } - public WriteString(value: string) { - var stringBuffer = new Buffer(value, "utf-8"); - this.WriteInt32(stringBuffer.length); - if (stringBuffer.length > 0) { - this.socket.write(stringBuffer); - } - } - public Write(buffer: Buffer) { - this.socket.write(buffer); - } - - - private buffer: Buffer; - private isInTransaction: boolean; - private bytesRead: number = 0; - public get Buffer(): Buffer { - return this.buffer; - } - public BeginTransaction() { - this.isInTransaction = true; - this.bytesRead = 0; - this.ClearErrors(); - } - public EndTransaction() { - this.isInTransaction = true; - this.buffer = this.buffer.slice(this.bytesRead); - this.bytesRead = 0; - this.ClearErrors(); - } - public RollBackTransaction() { - this.isInTransaction = false; - this.bytesRead = 0; - this.ClearErrors(); - } - - public ClearErrors() { - this.hasInsufficientDataForReading = false; - } - - private hasInsufficientDataForReading: boolean = false; - public get HasInsufficientDataForReading(): boolean { - return this.hasInsufficientDataForReading; - } - - public toString(): string { - return this.buffer.toString(); - } - - public get Length(): number { - return this.buffer.length; - } - - public Append(additionalData: Buffer) { - if (this.buffer.length === 0) { - this.buffer = additionalData; - return; - } - var newBuffer = new Buffer(this.buffer.length + additionalData.length); - this.buffer.copy(newBuffer); - additionalData.copy(newBuffer, this.buffer.length) - this.buffer = newBuffer; - } - - private isSufficientDataAvailable(length: number): boolean { - if (this.buffer.length < (this.bytesRead + length)) { - this.hasInsufficientDataForReading = true; - } - - return !this.hasInsufficientDataForReading; - } - - public ReadByte(): number { - if (!this.isSufficientDataAvailable(1)) { - return null; - } - - var value = this.buffer.slice(this.bytesRead, this.bytesRead + 1)[0]; - if (this.isInTransaction) { - this.bytesRead++; - } - else { - this.buffer = this.buffer.slice(1); - } - return value; - } - - public ReadString(): string { - var byteRead = this.ReadByte(); - if (this.HasInsufficientDataForReading) { - return null; - } - - if (byteRead < 0) { - throw new Error("IOException() - Socket.ReadString failed to read string type;"); - } - - var type = new Buffer([byteRead]).toString(); - var isUnicode = false; - switch (type) { - case 'N': // null string - return null; - case 'U': - isUnicode = true; - break; - case 'A': { - isUnicode = false; - break; - } - default: { - throw new Error("IOException(); Socket.ReadString failed to parse unknown string type " + type); - } - } - - var len = this.ReadInt32(); - if (this.HasInsufficientDataForReading) { - return null; - } - - if (!this.isSufficientDataAvailable(len)) { - return null; - } - - var stringBuffer = this.buffer.slice(this.bytesRead, this.bytesRead + len); - if (this.isInTransaction) { - this.bytesRead = this.bytesRead + len; - } - else { - this.buffer = this.buffer.slice(len); - } - - var resp = isUnicode ? stringBuffer.toString('utf-8') : stringBuffer.toString(); - return resp; - } - - public ReadInt32(): number { - return this.ReadInt64(); - } - - public ReadInt64(): number { - if (!this.isSufficientDataAvailable(8)) { - return null; - } - - var buf = this.buffer.slice(this.bytesRead, this.bytesRead + 8); - - if (this.isInTransaction) { - this.bytesRead = this.bytesRead + 8; - } - else { - this.buffer = this.buffer.slice(8); - } - - var returnValue = uint64be.decode(buf); - return returnValue; - } - - public ReadAsciiString(length: number): string { - if (!this.isSufficientDataAvailable(length)) { - return null; - } - - var stringBuffer = this.buffer.slice(this.bytesRead, this.bytesRead + length); - if (this.isInTransaction) { - this.bytesRead = this.bytesRead + length; - } - else { - this.buffer = this.buffer.slice(length); - } - return stringBuffer.toString("ascii"); - } -} - diff --git a/src/client/debugger/Common/TryParser.ts b/src/client/debugger/Common/TryParser.ts deleted file mode 100644 index 800d5406a6ca..000000000000 --- a/src/client/debugger/Common/TryParser.ts +++ /dev/null @@ -1,183 +0,0 @@ -'use strict'; - -import * as path from 'path'; -var LineByLineReader = require('line-by-line'); - -export interface ITryStatement { - StartLineNumber: number; - EndLineNumber: number; - Exceptions: string[]; -} - -interface ITryStatementEx extends ITryStatement { - Column: number; -} - -export function ExtractTryStatements(pythonFile: string): Promise { - return new Promise(resolve=> { - var lr = new LineByLineReader(pythonFile); - var lineNumber = 0; - var tryStatements: ITryStatementEx[] = []; - var tryColumnBlocks = new Map(); - - lr.on('error', function(err) { - resolve(tryStatements); - }); - - lr.on('line', function(line) { - lineNumber++; - - //Valid parts of a try block include - //try:, except :, except: else: finally: - //Anything other than this in the same column indicates a termination of the try block - - var trimmedLine = line.trim(); - var matches = line.match(/^\s*try(\s*):/); - if (matches !== null && matches.length > 0) { - let column = line.indexOf("try"); - if (column === -1) { - return; - } - - //If the new try starts at the same column - //Then the previous try block has ended - if (tryColumnBlocks.has(column)) { - var tryBlockClosed = tryColumnBlocks.get(column); - tryColumnBlocks.delete(column); - tryStatements.push(tryBlockClosed); - } - - var tryStatement: ITryStatementEx = { - Column: column, - EndLineNumber: 0, - Exceptions: [], - StartLineNumber: lineNumber - }; - tryColumnBlocks.set(column, tryStatement); - return; - } - - //look for excepts - matches = line.match(/^\s*except/); - if (matches !== null && matches.length > 0 && - (trimmedLine.startsWith("except ") || trimmedLine.startsWith("except:"))) { - - //Oops something has gone wrong - if (tryColumnBlocks.size === 0) { - resolve(tryStatements); - lr.close(); - return; - } - let column = line.indexOf("except"); - - //Do we have a try block for this same column - if (!tryColumnBlocks.has(column)) { - return; - } - - let currentTryBlock = tryColumnBlocks.get(column); - let exceptions = extractExceptions(line); - currentTryBlock.Exceptions = currentTryBlock.Exceptions.concat(exceptions); - if (currentTryBlock.EndLineNumber === 0) { - currentTryBlock.EndLineNumber = lineNumber; - } - return; - } - - //look for else - matches = line.match(/^\s*else(\s*):/); - if (matches !== null && matches.length > 0 && - (trimmedLine.startsWith("else ") || trimmedLine.startsWith("else:"))) { - - //This is possibly an if else... - if (tryColumnBlocks.size === 0) { - return; - } - - let column = line.indexOf("else"); - //Check if we have a try associated with this column - //If not found, this is probably an if else block or something else - if (!tryColumnBlocks.has(column)) { - return; - } - - //Else marks the end of the try block (of course there could be a finally too) - let currentTryBlock = tryColumnBlocks.get(column); - if (currentTryBlock.EndLineNumber === 0) { - currentTryBlock.EndLineNumber = lineNumber; - } - tryColumnBlocks.delete(column); - tryStatements.push(currentTryBlock); - return; - } - - //look for finally - matches = line.match(/^\s*finally(\s*):/); - if (matches !== null && matches.length > 0 && - (trimmedLine.startsWith("finally ") || trimmedLine.startsWith("finally:"))) { - - let column = line.indexOf("finally"); - //Oops something has gone wrong, or we cleared the previous - //Try block because we encountered an else - if (tryColumnBlocks.size === 0) { - return; - } - - //If this column doesn't match the current exception block, then it is likely we encountered an else for the try block.. - //& we closed it off - //So don't treat it as an exception, but proceed - if (!tryColumnBlocks.has(column)) { - return; - } - - //Finally marks the end of the try block - let currentTryBlock = tryColumnBlocks.get(column); - if (currentTryBlock.EndLineNumber === 0) { - currentTryBlock.EndLineNumber = lineNumber; - } - tryColumnBlocks.delete(column); - tryStatements.push(currentTryBlock); - } - }); - - lr.on('end', function() { - //All try blocks that haven't been popped can be popped now - //Only if their line numbers have valid end lines - tryColumnBlocks.forEach(tryBlock=> { - if (tryBlock.EndLineNumber > 0) { - tryStatements.push(tryBlock); - } - }); - resolve(tryStatements); - }); - }); -} - -const EXCEPT_LENGTH = "except".length; - -function extractExceptions(line: string): string[] { - var matches = line.match(/^\s*except(\s*):/); - if (matches !== null && matches.length > 0) { - return []; - } - - //Remove brackets and : from this - line = line.trim().substring(EXCEPT_LENGTH); - line = line.substring(0, line.indexOf(":")); - line = line.replace(/[\(\)]/g, ""); - var exceptions = []; - line.split(",").forEach(ex=> { - ex = ex.trim(); - if (ex.length === 0) { - return; - } - if (ex.indexOf(" as ") > 0) { - exceptions.push(ex.substring(0, ex.indexOf(" as ")).trim()); - } - else { - exceptions.push(ex); - } - }); - - return exceptions; -} \ No newline at end of file diff --git a/src/client/debugger/Common/Utils.ts b/src/client/debugger/Common/Utils.ts deleted file mode 100644 index 015f0393d5c8..000000000000 --- a/src/client/debugger/Common/Utils.ts +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -import {IPythonProcess, IPythonThread, IPythonModule, IPythonEvaluationResult} from './Contracts'; -import * as path from 'path'; - -export function CreatePythonThread(id: number, isWorker: boolean, process: IPythonProcess, name: string = ""): IPythonThread { - return { - IsWorkerThread: isWorker, - Process: process, - Name: name, - Id: id, - Frames: [] - }; -} - -export function CreatePythonModule(id: number, fileName: string): IPythonModule { - var name = fileName; - if (typeof fileName == "string") { - try { - name = path.basename(fileName); - } - catch (ex) { - } - } - else { - name = ""; - } - - return { - ModuleId: id, - Name: name, - Filename: fileName - }; -} - -export function FixupEscapedUnicodeChars(value: string): string { - return value; -} - -export class IdDispenser { - private _freedInts: number[] = []; - private _curValue: number = 0; - - public Allocate(): number { - if (this._freedInts.length > 0) { - var res: number = this._freedInts[this._freedInts.length - 1]; - this._freedInts.splice(this._freedInts.length - 1, 1); - return res; - } else { - var res: number = this._curValue++; - return res; - } - } - - public Free(id: number) { - if (id + 1 == this._curValue) { - this._curValue--; - } else { - this._freedInts.push(id); - } - } -} \ No newline at end of file diff --git a/src/client/debugger/DebugClients/DebugClient.ts b/src/client/debugger/DebugClients/DebugClient.ts deleted file mode 100644 index d43e10507c4e..000000000000 --- a/src/client/debugger/DebugClients/DebugClient.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {BaseDebugServer} from '../DebugServers/BaseDebugServer'; -import {LocalDebugServer} from '../DebugServers/LocalDebugServer'; -import {IPythonProcess, IPythonThread, IDebugServer} from '../Common/Contracts'; -import {DebugSession, OutputEvent} from 'vscode-debugadapter'; -import * as path from 'path'; -import * as child_process from 'child_process'; -import {DjangoApp, LaunchRequestArguments, AttachRequestArguments} from '../Common/Contracts'; - -export enum DebugType { - Local, - Remote -} -export abstract class DebugClient { - protected debugSession: DebugSession; - constructor(args: any, debugSession: DebugSession) { - this.debugSession = debugSession; - } - public abstract CreateDebugServer(pythonProcess: IPythonProcess): BaseDebugServer; - public get DebugType(): DebugType { - return DebugType.Local; - } - - public Stop() { - } - - public LaunchApplicationToDebug(dbgServer: IDebugServer): Promise { - return Promise.resolve(); - } -} diff --git a/src/client/debugger/DebugClients/DebugFactory.ts b/src/client/debugger/DebugClients/DebugFactory.ts deleted file mode 100644 index 00fecd690ce0..000000000000 --- a/src/client/debugger/DebugClients/DebugFactory.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {BaseDebugServer} from '../DebugServers/BaseDebugServer'; -import {LocalDebugServer} from '../DebugServers/LocalDebugServer'; -import {IPythonProcess, IPythonThread, IDebugServer} from '../Common/Contracts'; -import {DebugSession, OutputEvent} from 'vscode-debugadapter'; -import * as path from 'path'; -import * as child_process from 'child_process'; -import {DjangoApp, LaunchRequestArguments, AttachRequestArguments} from '../Common/Contracts'; -import {LocalDebugClient} from './LocalDebugClient'; -import {RemoteDebugClient} from './RemoteDebugClient'; -import {DebugClient} from './DebugClient'; - -export function CreateLaunchDebugClient(launchRequestOptions: LaunchRequestArguments, debugSession: DebugSession): DebugClient { - return new LocalDebugClient(launchRequestOptions, debugSession); -} -export function CreateAttachDebugClient(attachRequestOptions: AttachRequestArguments, debugSession: DebugSession): DebugClient { - return new RemoteDebugClient(attachRequestOptions, debugSession); -} \ No newline at end of file diff --git a/src/client/debugger/DebugClients/LocalDebugClient.ts b/src/client/debugger/DebugClients/LocalDebugClient.ts deleted file mode 100644 index 29fa3ddba46b..000000000000 --- a/src/client/debugger/DebugClients/LocalDebugClient.ts +++ /dev/null @@ -1,208 +0,0 @@ -import {BaseDebugServer} from '../DebugServers/BaseDebugServer'; -import {LocalDebugServer} from '../DebugServers/LocalDebugServer'; -import {IPythonProcess, IPythonThread, IDebugServer} from '../Common/Contracts'; -import {DebugSession, OutputEvent} from 'vscode-debugadapter'; -import * as path from 'path'; -import * as child_process from 'child_process'; -import {LaunchRequestArguments} from '../Common/Contracts'; -import {DebugClient, DebugType} from './DebugClient'; -import * as fs from 'fs'; -import {open} from '../../common/open'; -let fsExtra = require("fs-extra"); -let tmp = require("tmp"); -let prependFile = require('prepend-file'); -var LineByLineReader = require('line-by-line'); - -const PTVS_FILES = ["visualstudio_ipython_repl.py", "visualstudio_py_debugger.py", - "visualstudio_py_launcher.py", "visualstudio_py_repl.py", "visualstudio_py_util.py"]; - -export class LocalDebugClient extends DebugClient { - protected args: LaunchRequestArguments; - constructor(args: any, debugSession: DebugSession) { - super(args, debugSession); - this.args = args; - } - - private pyProc: child_process.ChildProcess; - private pythonProcess: IPythonProcess; - private debugServer: BaseDebugServer; - public CreateDebugServer(pythonProcess: IPythonProcess): BaseDebugServer { - this.pythonProcess = pythonProcess; - this.debugServer = new LocalDebugServer(this.debugSession, this.pythonProcess); - return this.debugServer; - } - - public get DebugType(): DebugType { - return DebugType.Local; - } - - public Stop() { - if (this.debugServer) { - this.debugServer.Stop() - this.debugServer = null; - } - - if (this.pyProc) { - try { this.pyProc.send("EXIT"); } - catch (ex) { } - try { this.pyProc.stdin.write("EXIT"); } - catch (ex) { } - try { this.pyProc.disconnect(); } - catch (ex) { } - this.pyProc = null; - } - } - private getPTVSToolsFilePath(): Promise { - var currentFileName = module.filename; - - return new Promise((resolve, reject) => { - tmp.dir((error, tmpDir) => { - if (error) { return reject(error); } - var ptVSToolsPath = path.join(path.dirname(currentFileName), "..", "..", "..", "..", "pythonFiles", "PythonTools"); - - var promises = PTVS_FILES.map(ptvsFile=> { - return new Promise((copyResolve, copyReject) => { - var sourceFile = path.join(ptVSToolsPath, ptvsFile); - var targetFile = path.join(tmpDir, ptvsFile); - - fsExtra.copy(sourceFile, targetFile, copyError=> { - if (copyError) { return copyReject(copyError); } - copyResolve(targetFile); - }); - }); - }); - - Promise.all(promises).then(() => { - resolve(path.join(tmpDir, "visualstudio_py_launcher.py")); - }, reject); - }); - }); - } - private displayError(error) { - if (!error) { return; } - var errorMsg = typeof error === "string" ? error : ((error.message && error.message.length > 0) ? error.message : ""); - if (errorMsg.length > 0) { - this.debugSession.sendEvent(new OutputEvent(errorMsg + "\n", "stderr")); - } - } - private getShebangLines(program: string): Promise { - const MAX_SHEBANG_LINES = 2; - return new Promise((resolve, reject) => { - var lr = new LineByLineReader(program); - var shebangLines: string[] = []; - - lr.on('error', err=> { - reject(err); - }); - lr.on('line', (line: string) => { - if (shebangLines.length >= MAX_SHEBANG_LINES) { - lr.close(); - return false; - } - var trimmedLine = line.trim(); - if (trimmedLine.startsWith("#")) { - shebangLines.push(line); - } - else { - shebangLines.push("#"); - } - }); - lr.on('end', function() { - //Ensure we always have two lines, even if no shebangLines - //This way if ever we get lines numbers in errors for the python file, we have a consistency - while (shebangLines.length < MAX_SHEBANG_LINES) { - shebangLines.push("#"); - } - resolve(shebangLines); - }); - }); - } - private prependShebangToPTVSFile(ptVSToolsFilePath: string, program: string): Promise { - return new Promise((resolve, reject) => { - this.getShebangLines(program).then(lines=> { - var linesToPrepend = lines.join('\n') + '\n'; - prependFile(ptVSToolsFilePath, linesToPrepend, error=> { - if (error) { reject(error); } - else { resolve(ptVSToolsFilePath); } - }) - }, reject); - }); - } - public LaunchApplicationToDebug(dbgServer: IDebugServer): Promise { - return new Promise((resolve, reject) => { - var fileDir = path.dirname(this.args.program); - var processCwd = fileDir; - if (typeof this.args.cwd === "string" && this.args.cwd.length > 0){ - processCwd = this.args.cwd; - } - var fileNameWithoutPath = path.basename(this.args.program); - var pythonPath = "python"; - if (typeof this.args.pythonPath === "string" && this.args.pythonPath.trim().length > 0) { - pythonPath = this.args.pythonPath; - } - var environmentVariables = this.args.env ? this.args.env : null; - if (environmentVariables) { - for (let setting in process.env) { - if (!environmentVariables[setting]) { - environmentVariables[setting] = process.env[setting]; - } - } - } - - var currentFileName = module.filename; - //var ptVSToolsFilePath = path.join(path.dirname(currentFileName), "..", "..", "..", "..", "pythonFiles", "PythonTools", "visualstudio_py_launcher.py"); - - this.getPTVSToolsFilePath().then((ptVSToolsFilePath) => { - return this.prependShebangToPTVSFile(ptVSToolsFilePath, this.args.program); - }, error=> { - this.displayError(error); - reject(error); - }).then((ptVSToolsFilePath) => { - var launcherArgs = this.buildLauncherArguments(); - - var args = [ptVSToolsFilePath, processCwd, dbgServer.port.toString(), "34806ad9-833a-4524-8cd6-18ca4aa74f14"].concat(launcherArgs); - if (this.args.externalConsole === true) { - open({ wait: false, app: [pythonPath].concat(args), cwd: processCwd, env: environmentVariables }).then(proc=> { - this.pyProc = proc; - resolve(); - }, error=> { - if (!this.debugServer && this.debugServer.IsRunning) { - return; - } - this.displayError(error); - }); - - return; - } - - this.pyProc = child_process.spawn(pythonPath, args, { cwd: processCwd, env: environmentVariables }); - this.pyProc.on("error", error => { - if (!this.debugServer && this.debugServer.IsRunning) { - return; - } - this.displayError(error); - }); - this.pyProc.on("stderr", error => { - if (!this.debugServer && this.debugServer.IsRunning) { - return; - } - this.displayError(error); - }); - - resolve(); - }, error=> { - this.displayError(error); - reject(error); - }); - }); - } - protected buildLauncherArguments(): string[] { - var vsDebugOptions = "WaitOnAbnormalExit,WaitOnNormalExit,RedirectOutput"; - if (Array.isArray(this.args.debugOptions)) { - vsDebugOptions = this.args.debugOptions.join(","); - } - - var programArgs = Array.isArray(this.args.args) && this.args.args.length > 0 ? this.args.args : []; - return [vsDebugOptions, this.args.program].concat(programArgs); - } -} diff --git a/src/client/debugger/DebugClients/RemoteDebugClient.ts b/src/client/debugger/DebugClients/RemoteDebugClient.ts deleted file mode 100644 index 6ee6c006cca5..000000000000 --- a/src/client/debugger/DebugClients/RemoteDebugClient.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {BaseDebugServer} from '../DebugServers/BaseDebugServer'; -import {RemoteDebugServer} from '../DebugServers/RemoteDebugServer'; -import {IPythonProcess, IPythonThread, IDebugServer} from '../Common/Contracts'; -import {DebugSession, OutputEvent} from 'vscode-debugadapter'; -import * as path from 'path'; -import * as child_process from 'child_process'; -import {AttachRequestArguments} from '../Common/Contracts'; -import {DebugClient, DebugType} from './DebugClient'; - -export class RemoteDebugClient extends DebugClient { - private args: AttachRequestArguments; - constructor(args: any, debugSession: DebugSession) { - super(args, debugSession); - this.args = args; - } - - private pythonProcess: IPythonProcess; - private debugServer: BaseDebugServer; - public CreateDebugServer(pythonProcess: IPythonProcess): BaseDebugServer { - this.pythonProcess = pythonProcess; - this.debugServer = new RemoteDebugServer(this.debugSession, this.pythonProcess, this.args); - return this.debugServer; - } - public get DebugType(): DebugType { - return DebugType.Remote; - } - - public Stop() { - if (this.pythonProcess) { - this.pythonProcess.Detach(); - } - if (this.debugServer) { - this.debugServer.Stop() - this.debugServer = null; - } - } - -} diff --git a/src/client/debugger/DebugServers/BaseDebugServer.ts b/src/client/debugger/DebugServers/BaseDebugServer.ts deleted file mode 100644 index 3ff0ee07cfeb..000000000000 --- a/src/client/debugger/DebugServers/BaseDebugServer.ts +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -import {DebugSession, OutputEvent} from 'vscode-debugadapter'; -import {IPythonProcess, IDebugServer} from '../Common/Contracts'; -import * as net from 'net'; -import {EventEmitter} from 'events'; - -export abstract class BaseDebugServer extends EventEmitter { - protected pythonProcess: IPythonProcess; - protected debugSession: DebugSession; - - protected isRunning: boolean; - public get IsRunning(): boolean { - return this.isRunning; - } - - constructor(debugSession: DebugSession, pythonProcess: IPythonProcess) { - super(); - this.debugSession = debugSession; - this.pythonProcess = pythonProcess; - } - - public abstract Start(): Promise; - public abstract Stop(); -} \ No newline at end of file diff --git a/src/client/debugger/DebugServers/DebugServerFactory.ts b/src/client/debugger/DebugServers/DebugServerFactory.ts deleted file mode 100644 index 15ca848883e4..000000000000 --- a/src/client/debugger/DebugServers/DebugServerFactory.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {BaseDebugServer} from './BaseDebugServer'; -import {LocalDebugServer} from './LocalDebugServer'; -import {RemoteDebugServer} from './RemoteDebugServer'; -import {DebugSession, OutputEvent} from 'vscode-debugadapter'; -import {IPythonProcess, IDebugServer} from '../Common/Contracts'; - -export function CreateDebugServer(debugSession: DebugSession, pythonProcess: IPythonProcess): BaseDebugServer { - return new LocalDebugServer(debugSession, pythonProcess); -} diff --git a/src/client/debugger/DebugServers/LocalDebugServer.ts b/src/client/debugger/DebugServers/LocalDebugServer.ts deleted file mode 100644 index a8be945a4f96..000000000000 --- a/src/client/debugger/DebugServers/LocalDebugServer.ts +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; - -//import {Variable, DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles} from 'vscode-debugadapter'; -import {DebugSession, OutputEvent} from 'vscode-debugadapter'; -import {IPythonProcess, IDebugServer} from '../Common/Contracts'; -import * as net from 'net'; -import {BaseDebugServer} from './BaseDebugServer'; - -export class LocalDebugServer extends BaseDebugServer { - private debugSocketServer: net.Server = null; - - constructor(debugSession: DebugSession, pythonProcess: IPythonProcess) { - super(debugSession, pythonProcess); - } - - public Stop() { - if (this.debugSocketServer === null) return; - try { - this.debugSocketServer.close(); - } - catch (ex) { } - this.debugSocketServer = null; - } - - public Start(): Promise { - return new Promise((resolve, reject) => { - var that = this; - this.debugSocketServer = net.createServer(c => { //'connection' listener - var connected = false; - c.on('end', (ex) => { - //var msg = "Debugger client disconneced, " + ex; - //that.debugSession.sendEvent(new OutputEvent(msg + "\n", "stderr")); - //console.log(msg); - }); - c.on("data", (buffer: Buffer) => { - if (!connected) { - connected = true; - that.pythonProcess.Connect(buffer, c, false); - } - else { - that.pythonProcess.HandleIncomingData(buffer) - that.isRunning = true; - } - }); - c.on("close", d=> { - var msg = "Debugger client closed, " + d; - that.emit("detach", d); - }); - c.on("error", d=> { - // var msg = "Debugger client error, " + d; - // that.sendEvent(new OutputEvent(msg + "\n", "Python")); - // console.log(msg); - // // that.onDetachDebugger(); - }); - c.on("timeout", d=> { - var msg = "Debugger client timedout, " + d; - that.debugSession.sendEvent(new OutputEvent(msg + "\n", "stderr")); - }); - }); - this.debugSocketServer.on("error", ex=> { - var exMessage = JSON.stringify(ex); - var msg = ""; - if (ex.code === "EADDRINUSE") { - msg = `The port used for debugging is in use, please try again or try restarting Visual Studio Code, Error = ${exMessage}`; - } - else { - msg = `There was an error in starting the debug server. Error = ${exMessage}`; - } - that.debugSession.sendEvent(new OutputEvent(msg + "\n", "stderr")); - reject(msg); - }); - - this.debugSocketServer.listen(0, () => { - var server = that.debugSocketServer.address(); - resolve({ port: server.port }); - }); - }); - } -} \ No newline at end of file diff --git a/src/client/debugger/DebugServers/RemoteDebugServer.ts b/src/client/debugger/DebugServers/RemoteDebugServer.ts deleted file mode 100644 index 03a0141888b9..000000000000 --- a/src/client/debugger/DebugServers/RemoteDebugServer.ts +++ /dev/null @@ -1,200 +0,0 @@ -'use strict'; - -import {DebugSession, OutputEvent} from 'vscode-debugadapter'; -import {IPythonProcess, IDebugServer, AttachRequestArguments} from '../Common/Contracts'; -import * as net from 'net'; -import {BaseDebugServer} from './BaseDebugServer'; -import {SocketStream} from '../Common/SocketStream'; - -const DebuggerProtocolVersion = 6; // must be kept in sync with PTVSDBG_VER in attach_server.py -const DebuggerSignature = "PTVSDBG"; -const Accepted = "ACPT"; -const Rejected = "RJCT"; -const DebuggerSignatureBytes: Buffer = new Buffer(DebuggerSignature, "ascii"); -const InfoCommandBytes: Buffer = new Buffer("INFO", "ascii"); -const AttachCommandBytes: Buffer = new Buffer("ATCH", "ascii"); -const ReplCommandBytes: Buffer = new Buffer("REPL", "ascii"); - -export class RemoteDebugServer extends BaseDebugServer { - private socket: net.Socket = null; - private args: AttachRequestArguments; - constructor(debugSession: DebugSession, pythonProcess: IPythonProcess, args: AttachRequestArguments) { - super(debugSession, pythonProcess); - this.args = args; - } - - public Stop() { - if (this.socket === null) return; - try { - this.socket.end(); - } - catch (ex) { } - this.socket = null; - } - private stream: SocketStream = null; - public Start(): Promise { - return new Promise((resolve, reject) => { - var that = this; - var connected = false; - var secretWrittenToDebugProgram = false; - var secretConfirmedByDebugProgram = false; - var infoBytesWritten = false; - var versionRead = false; - var commandBytesWritten = false; - var languageVersionRead = false; - var portNumber = this.args.port; - var debugCommandsAccepted = false; - var options = { port: portNumber}; - if (typeof this.args.host === "string" && this.args.host.length > 0) { - options.host = this.args.host; - } - this.socket = net.connect(options, () => { - resolve(options); - }); - this.socket.on('end', (ex) => { - var msg = `Debugger client disconneced, ex`; - that.debugSession.sendEvent(new OutputEvent(msg + "\n", "stderr")); - }); - this.socket.on("data", (buffer: Buffer) => { - if (connected) { - that.pythonProcess.HandleIncomingData(buffer); - return; - } - - if (that.stream === null) { - that.stream = new SocketStream(that.socket, buffer); - } - else { - if (!connected) { - if (that.stream.Length === 0) { - that.stream = new SocketStream(that.socket, buffer); - } - else { - that.stream.Append(buffer); - } - } - } - - if (!secretWrittenToDebugProgram) { - that.stream.BeginTransaction(); - var sig = that.stream.ReadAsciiString(DebuggerSignature.length); - if (that.stream.HasInsufficientDataForReading) { - that.stream.RollBackTransaction() - return; - } - if (sig != DebuggerSignature) { - throw new Error("ConnErrorMessages.RemoteUnsupportedServer"); - } - - var ver = that.stream.ReadInt64(); - if (that.stream.HasInsufficientDataForReading) { - that.stream.RollBackTransaction() - return; - } - - // If we are talking the same protocol but different version, reply with signature + version before bailing out - // so that ptvsd has a chance to gracefully close the socket on its side. - that.stream.EndTransaction(); - that.stream.Write(DebuggerSignatureBytes); - that.stream.WriteInt64(DebuggerProtocolVersion); - - if (ver != DebuggerProtocolVersion) { - throw new Error("ConnErrorMessages.RemoteUnsupportedServer"); - } - - - that.stream.WriteString(that.args.secret || ""); - secretWrittenToDebugProgram = true; - that.stream.EndTransaction(); - - var secretResp = that.stream.ReadAsciiString(Accepted.length); - if (that.stream.HasInsufficientDataForReading) { - that.stream.RollBackTransaction() - return; - } - if (secretResp != Accepted) { - throw new Error("ConnErrorMessages.RemoteSecretMismatch"); - } - - secretConfirmedByDebugProgram = true; - that.stream.EndTransaction(); - } - - if (!secretConfirmedByDebugProgram) { - var secretResp = that.stream.ReadAsciiString(Accepted.length); - if (that.stream.HasInsufficientDataForReading) { - that.stream.RollBackTransaction() - return; - } - if (secretResp != Accepted) { - throw new Error("ConnErrorMessages.RemoteSecretMismatch"); - } - - secretConfirmedByDebugProgram = true; - that.stream.EndTransaction(); - } - - if (!commandBytesWritten) { - that.stream.Write(AttachCommandBytes); - var debugOptions = "WaitOnAbnormalExit, WaitOnNormalExit, RedirectOutput"; - that.stream.WriteString(debugOptions); - commandBytesWritten = true; - } - - if (commandBytesWritten && !debugCommandsAccepted) { - var attachResp = that.stream.ReadAsciiString(Accepted.length); - if (that.stream.HasInsufficientDataForReading) { - that.stream.RollBackTransaction() - return; - } - - if (attachResp != Accepted) { - throw new Error("ConnErrorMessages.RemoteAttachRejected"); - } - debugCommandsAccepted = true; - that.stream.EndTransaction(); - } - - if (debugCommandsAccepted && !languageVersionRead) { - that.stream.EndTransaction(); - var pid = that.stream.ReadInt32(); - var langMajor = that.stream.ReadInt32(); - var langMinor = that.stream.ReadInt32(); - var langMicro = that.stream.ReadInt32(); - var langVer = ((langMajor << 8) | langMinor); - if (that.stream.HasInsufficientDataForReading) { - that.stream.RollBackTransaction() - return; - } - - that.stream.EndTransaction(); - languageVersionRead = true; - } - - if (languageVersionRead) { - if (connected) { - that.pythonProcess.HandleIncomingData(buffer); - } - else { - that.pythonProcess.Connect(that.stream.Buffer, this.socket, true); - connected = true; - } - } - }); - this.socket.on("close", d => { - var msg = `Debugger client closed, ${d}`; - that.emit("detach", d); - }); - this.socket.on("timeout", d => { - var msg = `Debugger client timedout, ${d}`; - that.debugSession.sendEvent(new OutputEvent(msg + "\n", "stderr")); - }); - this.socket.on("error", ex => { - var exMessage = JSON.stringify(ex); - var msg = `There was an error in starting the debug server. Error = ${exMessage}`; - that.debugSession.sendEvent(new OutputEvent(msg + "\n", "stderr")); - reject(msg); - }); - }); - } -} \ No newline at end of file diff --git a/src/client/debugger/Main.ts b/src/client/debugger/Main.ts deleted file mode 100644 index 5f23195e26ae..000000000000 --- a/src/client/debugger/Main.ts +++ /dev/null @@ -1,524 +0,0 @@ -'use strict'; - -import {Variable, DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles} from 'vscode-debugadapter'; -import {ThreadEvent} from 'vscode-debugadapter'; -import {DebugProtocol} from 'vscode-debugprotocol'; -import {readFileSync} from 'fs'; -import {basename} from 'path'; -import * as path from 'path'; -import * as os from 'os'; -import * as fs from 'fs'; -import * as child_process from 'child_process'; -import * as StringDecoder from 'string_decoder'; -import * as net from 'net'; -import {PythonProcess} from './PythonProcess'; -import {FrameKind, IPythonProcess, IPythonThread, IPythonModule, IPythonEvaluationResult, IPythonStackFrame, IDebugServer} from './Common/Contracts'; -import {IPythonBreakpoint, PythonBreakpointConditionKind, PythonBreakpointPassCountKind, IPythonException, PythonEvaluationResultReprKind, enum_EXCEPTION_STATE} from './Common/Contracts'; -import {BaseDebugServer} from './DebugServers/BaseDebugServer'; -import {DebugClient, DebugType} from './DebugClients/DebugClient'; -import {CreateAttachDebugClient, CreateLaunchDebugClient} from './DebugClients/DebugFactory'; -import {WaitForPortToOpen} from './Common/OnPortOpenedHandler'; -import {DjangoApp, LaunchRequestArguments, AttachRequestArguments, DebugFlags, DebugOptions} from './Common/Contracts'; - -const CHILD_ENUMEARATION_TIMEOUT = 5000; - -interface IDebugVariable { - variables: IPythonEvaluationResult[]; - evaluateChildren?: Boolean; -} - -export class PythonDebugger extends DebugSession { - private _variableHandles: Handles; - private _pythonStackFrames: Handles; - private breakPointCounter: number = 0; - private registeredBreakpoints: Map; - private registeredBreakpointsByFileName: Map; - private debuggerLoaded: Promise; - private debuggerLoadedPromiseResolve: () => void; - - private debugClient: DebugClient; - - public constructor(debuggerLinesStartAt1: boolean, isServer: boolean) { - super(debuggerLinesStartAt1, isServer === true); - this._variableHandles = new Handles(); - this._pythonStackFrames = new Handles(); - this.registeredBreakpoints = new Map(); - this.registeredBreakpointsByFileName = new Map(); - } - protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { - response.body.supportsEvaluateForHovers = true; - this.sendResponse(response); - // now we are ready to accept breakpoints -> fire the initialized event to give UI a chance to set breakpoints - this.sendEvent(new InitializedEvent()); - } - - private pythonProcess: PythonProcess; - private debugServer: BaseDebugServer; - - private startDebugServer(): Promise { - var programDirectory = this.launchArgs ? path.dirname(this.launchArgs.program) : this.attachArgs.localRoot; - this.pythonProcess = new PythonProcess(0, "", programDirectory); - this.debugServer = this.debugClient.CreateDebugServer(this.pythonProcess); - this.InitializeEventHandlers(); - return this.debugServer.Start(); - } - private stopDebugServer() { - if (this.debugClient) { - this.debugClient.Stop(); - this.debugClient = null; - } - if (this.pythonProcess) { - this.pythonProcess.Kill(); - } - } - private InitializeEventHandlers() { - this.pythonProcess.on("last", arg => this.onDetachDebugger()); - this.pythonProcess.on("threadExited", arg => this.onPythonThreadExited(arg)); - this.pythonProcess.on("moduleLoaded", arg => this.onPythonModuleLoaded(arg)); - this.pythonProcess.on("threadCreated", arg => this.onPythonThreadCreated(arg)); - this.pythonProcess.on("processLoaded", arg => this.onPythonProcessLoaded(arg)); - this.pythonProcess.on("output", (pyThread, output) => this.onDebuggerOutput(pyThread, output)); - this.pythonProcess.on("exceptionRaised", (pyThread, ex) => this.onPythonException(pyThread, ex)); - this.pythonProcess.on("breakpointHit", (pyThread, breakpointId) => this.onBreakpointHit(pyThread, breakpointId)); - this.pythonProcess.on("stepCompleted", (pyThread) => this.onStepCompleted(pyThread)); - this.pythonProcess.on("detach", () => this.onDetachDebugger()); - this.pythonProcess.on("error", ex => this.sendEvent(new OutputEvent(ex, "stderr"))); - this.pythonProcess.on("asyncBreakCompleted", arg => this.onPythonProcessPaused(arg)); - - this.debugServer.on("detach", () => this.onDetachDebugger()); - } - private onDetachDebugger() { - this.stopDebugServer(); - this.sendEvent(new TerminatedEvent()); - this.shutdown(); - } - private onPythonThreadCreated(pyThread: IPythonThread) { - this.sendEvent(new ThreadEvent("started", pyThread.Id)); - } - private onStepCompleted(pyThread: IPythonThread) { - this.sendEvent(new StoppedEvent("step", pyThread.Id)); - } - private onPythonException(pyThread: IPythonThread, ex: IPythonException) { - this.sendEvent(new StoppedEvent("exception", pyThread.Id, `${ex.TypeName}, ${ex.Description}`)); - this.sendEvent(new OutputEvent(`${ex.TypeName}, ${ex.Description}\n`, "stderr")); - } - private onPythonThreadExited(pyThread: IPythonThread) { - this.sendEvent(new ThreadEvent("exited", pyThread.Id)); - } - private onPythonProcessPaused(pyThread: IPythonThread) { - this.sendEvent(new StoppedEvent("user request", pyThread.Id)); - } - private onPythonModuleLoaded(module: IPythonModule) { - } - private onPythonProcessLoaded(pyThread: IPythonThread) { - this.sendResponse(this.entryResponse); - this.debuggerLoadedPromiseResolve(); - if (this.launchArgs && this.launchArgs.stopOnEntry === true) { - this.sendEvent(new StoppedEvent("entry", pyThread.Id)); - } - else { - this.pythonProcess.SendResumeThread(pyThread.Id); - } - } - - private onDebuggerOutput(pyThread: IPythonThread, output: string) { - this.sendEvent(new OutputEvent(output, "stdout")); - } - private entryResponse: DebugProtocol.LaunchResponse; - private launchArgs: LaunchRequestArguments; - private attachArgs: AttachRequestArguments; - private canStartDebugger(): Promise { - return Promise.resolve(true); - } - protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { - //Confirm the file exists - if (!fs.existsSync(args.program)) { - return this.sendErrorResponse(response, 2001, `File does not exist. "${args.program}"`); - } - this.launchArgs = args; - this.debugClient = CreateLaunchDebugClient(args, this); - - this.debuggerLoaded = new Promise(resolve => { - this.debuggerLoadedPromiseResolve = resolve; - }); - - this.entryResponse = response; - var that = this; - - this.canStartDebugger().then(() => { - this.startDebugServer().then(dbgServer => { - that.debugClient.LaunchApplicationToDebug(dbgServer).then(() => { - - }, error => { - this.sendEvent(new OutputEvent(error + "\n", "stderr")); - this.sendErrorResponse(that.entryResponse, 2000, error); - }); - }); - }, error => { - this.sendEvent(new OutputEvent(error + "\n", "stderr")); - this.sendErrorResponse(that.entryResponse, 2000, error); - }); - } - protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) { - this.attachArgs = args; - this.debugClient = CreateAttachDebugClient(args, this); - - this.debuggerLoaded = new Promise(resolve => { - this.debuggerLoadedPromiseResolve = resolve; - }); - - this.entryResponse = response; - var that = this; - - this.canStartDebugger().then(() => { - this.startDebugServer().then(dbgServer => { - that.debugClient.LaunchApplicationToDebug(dbgServer); - }); - }, error => { - this.sendEvent(new OutputEvent(error + "\n", "stderr")); - this.sendErrorResponse(that.entryResponse, 2000, error); - }); - } - private onBreakpointHit(pyThread: IPythonThread, breakpointId: number) { - //Break only if the breakpoint exists and it is enabled - if (this.registeredBreakpoints.has(breakpointId) && this.registeredBreakpoints.get(breakpointId).Enabled === true) { - this.sendEvent(new StoppedEvent("breakpoint", pyThread.Id)); - } - else { - this.pythonProcess.SendResumeThread(pyThread.Id); - } - } - private buildBreakpointDetails(filePath: string, line: number, condition: string): IPythonBreakpoint { - var isDjangoFile = false; - if (this.launchArgs != null && - Array.isArray(this.launchArgs.debugOptions) && - this.launchArgs.debugOptions.indexOf(DebugOptions.DjangoDebugging) >= 0) { - isDjangoFile = filePath.toUpperCase().endsWith(".HTML"); - } - - var condition = typeof condition === "string" ? condition : ""; - - return { - Condition: condition, - ConditionKind: condition.length === 0 ? PythonBreakpointConditionKind.Always : PythonBreakpointConditionKind.WhenTrue, - Filename: filePath, - Id: this.breakPointCounter++, - LineNo: line, - PassCount: 0, - PassCountKind: PythonBreakpointPassCountKind.Always, - IsDjangoBreakpoint: isDjangoFile, - Enabled: true - }; - } - protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { - this.debuggerLoaded.then(() => { - if (!this.registeredBreakpointsByFileName.has(args.source.path)) { - this.registeredBreakpointsByFileName.set(args.source.path, []); - } - - var breakpoints: { verified: boolean, line: number }[] = []; - var breakpointsToRemove = []; - var linesToAdd = args.breakpoints.map(b => b.line); - var registeredBks = this.registeredBreakpointsByFileName.get(args.source.path); - var linesToRemove = registeredBks.map(b => b.LineNo).filter(oldLine => linesToAdd.indexOf(oldLine) === -1); - var linesToUpdate = registeredBks.map(b => b.LineNo).filter(oldLine => linesToAdd.indexOf(oldLine) >= 0); - - //Always add new breakpoints, don't re-enable previous breakpoints - //Cuz sometimes some breakpoints get added too early (e.g. in django) and don't get registeredBks - //and the response comes back indicating it wasn't set properly - //However, at a later point in time, the program breaks at that point!!! - var linesToAddPromises = args.breakpoints.map(bk => { - return new Promise(resolve => { - var breakpoint: IPythonBreakpoint; - var existingBreakpointsForThisLine = registeredBks.filter(registeredBk => registeredBk.LineNo === bk.line); - if (existingBreakpointsForThisLine.length > 0) { - //We have an existing breakpoint for this line - //just enable that - breakpoint = existingBreakpointsForThisLine[0] - breakpoint.Enabled = true; - } - else { - breakpoint = this.buildBreakpointDetails(this.convertClientPathToDebugger(args.source.path), bk.line, bk.condition); - } - - this.pythonProcess.BindBreakpoint(breakpoint).then(() => { - this.registeredBreakpoints.set(breakpoint.Id, breakpoint); - breakpoints.push({ verified: true, line: bk.line }); - registeredBks.push(breakpoint); - resolve(); - }, reason => { - this.registeredBreakpoints.set(breakpoint.Id, breakpoint); - breakpoints.push({ verified: false, line: bk.line }); - registeredBks.push(breakpoint); - resolve(); - }); - }); - }); - - var linesToRemovePromises = linesToRemove.map(line => { - return new Promise(resolve => { - var registeredBks = this.registeredBreakpointsByFileName.get(args.source.path); - var bk = registeredBks.filter(b => b.LineNo === line)[0]; - //Ok, we won't get a response back, so update the breakpoints list indicating this has been disabled - bk.Enabled = false; - this.pythonProcess.DisableBreakPoint(bk); - resolve(); - }); - }); - - var promises = linesToAddPromises.concat(linesToRemovePromises); - Promise.all(promises).then(() => { - response.body = { - breakpoints: breakpoints - }; - - this.sendResponse(response); - }); - }); - } - - protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { - var threads = []; - this.pythonProcess.Threads.forEach(t => { - threads.push(new Thread(t.Id, t.Name)); - }); - - response.body = { - threads: threads - }; - this.sendResponse(response); - } - /** converts the remote path to local path */ - protected convertDebuggerPathToClient(remotePath: string): string { - if (this.attachArgs && this.attachArgs.localRoot && this.attachArgs.remoteRoot) { - // get the part of the path that is relative to the source root - const pathRelativeToSourceRoot = path.relative(this.attachArgs.remoteRoot, remotePath); - // resolve from the local source root - return path.resolve(this.attachArgs.localRoot, pathRelativeToSourceRoot); - } else { - return remotePath; - } - } - /** converts the local path to remote path */ - protected convertClientPathToDebugger(clientPath: string): string { - if (this.attachArgs && this.attachArgs.localRoot && this.attachArgs.remoteRoot) { - // get the part of the path that is relative to the client root - const pathRelativeToClientRoot = path.relative(this.attachArgs.localRoot, clientPath); - // resolve from the remote source root - return path.resolve(this.attachArgs.remoteRoot, pathRelativeToClientRoot); - } else { - return clientPath; - } - } - protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { - this.debuggerLoaded.then(() => { - if (!this.pythonProcess.Threads.has(args.threadId)) { - response.body = { - stackFrames: [] - }; - this.sendResponse(response); - } - - var pyThread = this.pythonProcess.Threads.get(args.threadId); - var maxFrames = typeof args.levels === "number" && args.levels > 0 ? args.levels : pyThread.Frames.length - 1; - maxFrames = maxFrames < pyThread.Frames.length ? maxFrames : pyThread.Frames.length; - - var frames = []; - for (var counter = 0; counter < maxFrames; counter++) { - var frame = pyThread.Frames[counter]; - var frameId = this._pythonStackFrames.create(frame); - frames.push(new StackFrame(frameId, frame.FunctionName, - new Source(path.basename(frame.FileName), this.convertDebuggerPathToClient(frame.FileName)), - this.convertDebuggerLineToClient(frame.LineNo - 1), - 0)); - } - - response.body = { - stackFrames: frames - }; - - this.sendResponse(response); - }); - } - protected stepInRequest(response: DebugProtocol.StepInResponse): void { - this.sendResponse(response); - this.pythonProcess.SendStepInto(this.pythonProcess.LastExecutedThread.Id); - } - protected stepOutRequest(response: DebugProtocol.StepInResponse): void { - this.sendResponse(response); - this.pythonProcess.SendStepOut(this.pythonProcess.LastExecutedThread.Id); - } - protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { - this.sendResponse(response); - this.pythonProcess.SendContinue(); - } - protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { - this.sendResponse(response); - this.pythonProcess.SendStepOver(this.pythonProcess.LastExecutedThread.Id); - } - protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { - this.debuggerLoaded.then(() => { - var frame = this._pythonStackFrames.get(args.frameId); - if (!frame) { - response.body = { - result: null, - variablesReference: 0 - }; - return this.sendResponse(response); - } - - this.pythonProcess.ExecuteText(args.expression, PythonEvaluationResultReprKind.Normal, frame).then(result => { - let variablesReference = 0; - //If this value can be expanded, then create a vars ref for user to expand it - if (result.IsExpandable) { - const parentVariable: IDebugVariable = { - variables: [result], - evaluateChildren: true - }; - variablesReference = this._variableHandles.create(parentVariable); - } - - response.body = { - result: result.StringRepr, - variablesReference: variablesReference - }; - this.sendResponse(response); - }, - error => { - // this.sendResponse(response); - this.sendErrorResponse(response, 2000, error); - } - ); - }); - } - protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { - this.debuggerLoaded.then(() => { - var frame = this._pythonStackFrames.get(args.frameId); - if (!frame) { - response.body = { - scopes: [] - }; - return this.sendResponse(response); - } - - var scopes = []; - if (Array.isArray(frame.Locals) && frame.Locals.length > 0) { - let values: IDebugVariable = { variables: frame.Locals }; - scopes.push(new Scope("Local", this._variableHandles.create(values), false)); - } - if (Array.isArray(frame.Parameters) && frame.Parameters.length > 0) { - let values: IDebugVariable = { variables: frame.Parameters }; - scopes.push(new Scope("Arguments", this._variableHandles.create(values), false)); - } - response.body = { scopes }; - this.sendResponse(response); - }); - } - protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void { - var varRef = this._variableHandles.get(args.variablesReference); - - if (varRef.evaluateChildren !== true) { - let variables = []; - varRef.variables.forEach(variable => { - let variablesReference = 0; - //If this value can be expanded, then create a vars ref for user to expand it - if (variable.IsExpandable) { - const parentVariable: IDebugVariable = { - variables: [variable], - evaluateChildren: true - }; - variablesReference = this._variableHandles.create(parentVariable); - } - - variables.push({ - name: variable.Expression, - value: variable.StringRepr, - variablesReference: variablesReference - }); - }); - - response.body = { - variables: variables - }; - - return this.sendResponse(response); - } - - //Ok, we need to evaluate the children of the current variable - var variables = []; - var promises = varRef.variables.map(variable => { - return variable.Process.EnumChildren(variable.Expression, variable.Frame, CHILD_ENUMEARATION_TIMEOUT).then(children => { - children.forEach(child => { - let variablesReference = 0; - //If this value can be expanded, then create a vars ref for user to expand it - if (child.IsExpandable) { - const childVariable: IDebugVariable = { - variables: [child], - evaluateChildren: true - }; - variablesReference = this._variableHandles.create(childVariable); - } - - variables.push({ - name: child.ChildName, - value: child.StringRepr, - variablesReference: variablesReference - }); - }); - }, error => { - this.sendErrorResponse(response, 2001, error); - }); - }); - - Promise.all(promises).then(() => { - response.body = { - variables: variables - }; - - return this.sendResponse(response); - }); - } - protected pauseRequest(response: DebugProtocol.PauseResponse): void { - this.pythonProcess.Break(); - this.sendResponse(response); - } - protected setExceptionBreakPointsRequest(response: DebugProtocol.SetExceptionBreakpointsResponse, args: DebugProtocol.SetExceptionBreakpointsArguments): void { - this.debuggerLoaded.then(() => { - var mode = enum_EXCEPTION_STATE.BREAK_MODE_NEVER; - if (args.filters.indexOf("uncaught") >= 0) { - mode = enum_EXCEPTION_STATE.BREAK_MODE_UNHANDLED; - } - if (args.filters.indexOf("all") >= 0) { - mode = enum_EXCEPTION_STATE.BREAK_MODE_ALWAYS; - } - var exToIgnore = null; - var exceptionHandling = this.launchArgs.exceptionHandling; - if (exceptionHandling) { - exToIgnore = new Map(); - if (Array.isArray(exceptionHandling.ignore)) { - exceptionHandling.ignore.forEach(exType => { - exToIgnore.set(exType, enum_EXCEPTION_STATE.BREAK_MODE_NEVER); - }); - } - if (Array.isArray(exceptionHandling.always)) { - exceptionHandling.always.forEach(exType => { - exToIgnore.set(exType, enum_EXCEPTION_STATE.BREAK_MODE_ALWAYS); - }); - } - if (Array.isArray(exceptionHandling.unhandled)) { - exceptionHandling.unhandled.forEach(exType => { - exToIgnore.set(exType, enum_EXCEPTION_STATE.BREAK_MODE_UNHANDLED); - }); - } - } - this.pythonProcess.SendExceptionInfo(mode, exToIgnore); - this.sendResponse(response); - }); - } - protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments) { - this.stopDebugServer(); - this.sendResponse(response); - } -} - -DebugSession.run(PythonDebugger); \ No newline at end of file diff --git a/src/client/debugger/ProxyCommands.ts b/src/client/debugger/ProxyCommands.ts deleted file mode 100644 index 9069556115f3..000000000000 --- a/src/client/debugger/ProxyCommands.ts +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -export class Commands { - public static ExitCommandBytes: Buffer = new Buffer("exit"); - public static StepIntoCommandBytes: Buffer = new Buffer("stpi"); - public static StepOutCommandBytes: Buffer = new Buffer("stpo"); - public static StepOverCommandBytes: Buffer = new Buffer("stpv"); - public static BreakAllCommandBytes: Buffer = new Buffer("brka"); - public static SetBreakPointCommandBytes: Buffer = new Buffer("brkp"); - public static SetBreakPointConditionCommandBytes: Buffer = new Buffer("brkc"); - public static SetBreakPointPassCountCommandBytes: Buffer = new Buffer("bkpc"); - public static GetBreakPointHitCountCommandBytes: Buffer = new Buffer("bkgh"); - public static SetBreakPointHitCountCommandBytes: Buffer = new Buffer("bksh"); - public static RemoveBreakPointCommandBytes: Buffer = new Buffer("brkr"); - public static ResumeAllCommandBytes: Buffer = new Buffer("resa"); - public static GetThreadFramesCommandBytes: Buffer = new Buffer("thrf"); - public static ExecuteTextCommandBytes: Buffer = new Buffer("exec"); - public static ResumeThreadCommandBytes: Buffer = new Buffer("rest"); - public static AutoResumeThreadCommandBytes: Buffer = new Buffer("ares"); - public static ClearSteppingCommandBytes: Buffer = new Buffer("clst"); - public static SetLineNumberCommand: Buffer = new Buffer("setl"); - public static GetChildrenCommandBytes: Buffer = new Buffer("chld"); - public static DetachCommandBytes: Buffer = new Buffer("detc"); - public static SetExceptionInfoCommandBytes: Buffer = new Buffer("sexi"); - public static SetExceptionHandlerInfoCommandBytes: Buffer = new Buffer("sehi"); - public static RemoveDjangoBreakPointCommandBytes: Buffer = new Buffer("bkdr"); - public static AddDjangoBreakPointCommandBytes: Buffer = new Buffer("bkda"); - public static ConnectReplCommandBytes: Buffer = new Buffer("crep"); - public static DisconnectReplCommandBytes: Buffer = new Buffer("drep"); - public static LastAckCommandBytes: Buffer = new Buffer("lack"); -} diff --git a/src/client/debugger/PythonProcess.ts b/src/client/debugger/PythonProcess.ts deleted file mode 100644 index 8c79cce9c0a0..000000000000 --- a/src/client/debugger/PythonProcess.ts +++ /dev/null @@ -1,358 +0,0 @@ -'use strict'; - -import * as net from 'net'; -import {EventEmitter} from 'events'; -import {FrameKind, IPythonProcess, IPythonThread, IPythonModule, IPythonEvaluationResult, IPythonStackFrame, IStepCommand} from './Common/Contracts'; -import {IPythonBreakpoint, PythonBreakpointConditionKind, PythonBreakpointPassCountKind, IBreakpointCommand, IChildEnumCommand} from './Common/Contracts'; -import {PythonEvaluationResultReprKind, IExecutionCommand, enum_EXCEPTION_STATE} from './Common/Contracts'; -import {Commands} from './ProxyCommands'; -import * as utils from './Common/Utils'; -import {PythonProcessCallbackHandler} from './PythonProcessCallbackHandler'; -import {SocketStream} from './Common/SocketStream'; - -export class PythonProcess extends EventEmitter implements IPythonProcess { - private id: number; - public get Id(): number { - return this.id; - } - private guid: string; - public get Guid(): string { - return this.guid; - } - - private hasExited: boolean; - public get HasExited(): boolean { - return this.hasExited; - } - - private _mainThread: IPythonThread; - public get MainThread(): IPythonThread { - return this._mainThread; - } - - private _lastExecutedThread: IPythonThread; - public get LastExecutedThread(): IPythonThread { - return this._lastExecutedThread; - } - private _idDispenser: utils.IdDispenser; - private _threads: Map; - public get Threads(): Map { - return this._threads; - } - - public PendingChildEnumCommands: Map; - public PendingExecuteCommands: Map; - private callbackHandler: PythonProcessCallbackHandler; - private stream: SocketStream = null; - private programDirectory: string; - public get ProgramDirectory(): string { - return this.programDirectory; - } - constructor(id: number, guid: string, programDirectory: string) { - super(); - this.id = id; - this.guid = guid; - this._threads = new Map(); - this._idDispenser = new utils.IdDispenser(); - this.PendingChildEnumCommands = new Map(); - this.PendingExecuteCommands = new Map(); - this.programDirectory = programDirectory; - } - - public Kill() { - if (!this.isRemoteProcess && typeof this.pid === "number") { - try { - var kill = require('tree-kill'); - kill(this.pid); - this.pid = null; - } - catch (ex) { } - } - } - - public Terminate() { - this.stream.Write(Commands.ExitCommandBytes); - } - - public Detach() { - this.stream.Write(Commands.DetachCommandBytes); - } - - private guidRead: boolean; - private statusRead: boolean; - private pidRead: boolean; - private pid: number = 0; - private isRemoteProcess: Boolean; - public Connect(buffer: Buffer, socket: net.Socket, isRemoteProcess: boolean = false) { - this.isRemoteProcess = isRemoteProcess; - this.stream = new SocketStream(socket, buffer); - if (!isRemoteProcess) { - this.stream.BeginTransaction(); - var guid = this.stream.ReadString(); - if (this.stream.HasInsufficientDataForReading) { - this.stream.RollBackTransaction(); - return; - } - this.guidRead = true; - this.stream.EndTransaction(); - - this.stream.BeginTransaction(); - var result = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - this.stream.RollBackTransaction(); - return; - } - this.statusRead = true; - this.stream.EndTransaction(); - - this.stream.BeginTransaction(); - this.pid = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - this.stream.RollBackTransaction(); - return; - } - this.pidRead = true; - this.stream.EndTransaction(); - } - - this.callbackHandler = new PythonProcessCallbackHandler(this, this.stream, this._idDispenser); - this.callbackHandler.on("detach", () => this.emit("detach")); - this.callbackHandler.on("last", () => this.emit("last")); - this.callbackHandler.on("moduleLoaded", arg=> this.emit("moduleLoaded", arg)); - this.callbackHandler.on("asyncBreakCompleted", arg=> this.emit("asyncBreakCompleted", arg)); - this.callbackHandler.on("threadCreated", arg=> this.emit("threadCreated", arg)); - this.callbackHandler.on("threadExited", arg=> this.emit("threadExited", arg)); - this.callbackHandler.on("stepCompleted", arg=> this.onPythonStepCompleted(arg)); - this.callbackHandler.on("breakpointSet", arg=> this.onBreakpointSet(arg, true)); - this.callbackHandler.on("breakpointNotSet", arg=> this.onBreakpointSet(arg, false)); - this.callbackHandler.on("output", (pyThread, output) => this.emit("output", pyThread, output)); - this.callbackHandler.on("exceptionRaised", (pyThread, ex, brkType) => { - this._lastExecutedThread = pyThread; - this.emit("exceptionRaised", pyThread, ex, brkType); - }); - this.callbackHandler.on("breakpointHit", (pyThread, breakpointId) => this.onBreakpointHit(pyThread, breakpointId)); - this.callbackHandler.on("processLoaded", arg=> { - this._mainThread = arg; - this._lastExecutedThread = this._mainThread; - this.emit("processLoaded", arg); - }); - this.callbackHandler.HandleIncomingData(); - } - - public HandleIncomingData(buffer: Buffer) { - this.stream.Append(buffer); - - if (!this.isRemoteProcess) { - if (!this.guidRead) { - this.stream.RollBackTransaction(); - var guid = this.stream.ReadString(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - this.guidRead = true; - this.stream.EndTransaction(); - } - if (!this.statusRead) { - this.stream.BeginTransaction(); - var result = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - this.stream.RollBackTransaction(); - return; - } - this.statusRead = true; - this.stream.EndTransaction(); - } - if (!this.pidRead) { - this.stream.BeginTransaction(); - this.pid = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - this.stream.RollBackTransaction(); - return; - } - this.pidRead = true; - this.stream.EndTransaction(); - } - } - - this.callbackHandler.HandleIncomingData(); - } - - //#region Step Commands - private onPythonStepCompleted(pyThread: IPythonThread) { - this._lastExecutedThread = pyThread; - this.emit("stepCompleted", pyThread); - } - private sendStepCommand(threadId: number, command: Buffer) { - this.stream.Write(command); - this.stream.WriteInt64(threadId); - } - - public SendExceptionInfo(defaultBreakOnMode: enum_EXCEPTION_STATE, breakOn: Map) { - this.stream.Write(Commands.SetExceptionInfoCommandBytes); - this.stream.WriteInt32(defaultBreakOnMode); - if (breakOn === null || breakOn === undefined) { - this.stream.WriteInt32(0); - } - else { - this.stream.WriteInt32(breakOn.size); - breakOn.forEach((value,key)=>{ - this.stream.WriteInt32(value); - this.stream.WriteString(key); - }); - } - } - - public SendStepOver(threadId: number) { - return this.sendStepCommand(threadId, Commands.StepOverCommandBytes); - } - public SendStepOut(threadId: number) { - return this.sendStepCommand(threadId, Commands.StepOutCommandBytes); - } - public SendStepInto(threadId: number) { - return this.sendStepCommand(threadId, Commands.StepIntoCommandBytes); - } - //#endregion - private onBreakpointHit(pyThread: IPythonThread, breakpointId: number) { - this._lastExecutedThread = pyThread; - this.emit("breakpointHit", pyThread, breakpointId); - } - private onBreakpointSet(breakpointId: number, success: boolean) { - //Find the last breakpoint command associated with this breakpoint - var index = this.breakpointCommands.findIndex(cmd=> cmd.Id === breakpointId); - if (index === -1) { - //Hmm this is not possible, log this exception and carry on - this.emit("error", "command.breakpoint.hit", `Uknown Breakpoit Id ${breakpointId}`); - return; - } - - var cmd = this.breakpointCommands.splice(index, 1)[0]; - if (success) { - cmd.PromiseResolve(); - } - else { - cmd.PromiseReject(); - } - } - - private breakpointCommands: IBreakpointCommand[] = []; - public DisableBreakPoint(breakpoint: IPythonBreakpoint) { - if (breakpoint.IsDjangoBreakpoint) { - this.stream.Write(Commands.RemoveDjangoBreakPointCommandBytes); - } else { - this.stream.Write(Commands.RemoveBreakPointCommandBytes); - } - this.stream.WriteInt32(breakpoint.LineNo); - this.stream.WriteInt32(breakpoint.Id); - if (breakpoint.IsDjangoBreakpoint) { - this.stream.WriteString(breakpoint.Filename); - } - } - - public BindBreakpoint(brkpoint: IPythonBreakpoint): Promise { - return new Promise((resolve, reject) => { - var bkCmd: IBreakpointCommand = { - Id: brkpoint.Id, - PromiseResolve: resolve, - PromiseReject: reject - }; - this.breakpointCommands.push(bkCmd); - - if (brkpoint.IsDjangoBreakpoint) { - this.stream.Write(Commands.AddDjangoBreakPointCommandBytes); - } - else { - this.stream.Write(Commands.SetBreakPointCommandBytes); - } - this.stream.WriteInt32(brkpoint.Id); - this.stream.WriteInt32(brkpoint.LineNo); - this.stream.WriteString(brkpoint.Filename); - - if (!brkpoint.IsDjangoBreakpoint) { - this.SendCondition(brkpoint); - this.SendPassCount(brkpoint); - } - }); - } - - private SendCondition(breakpoint: IPythonBreakpoint) { - this.stream.WriteInt32(breakpoint.ConditionKind); - this.stream.WriteString(breakpoint.Condition || ""); - } - private SendPassCount(breakpoint: IPythonBreakpoint) { - // DebugWriteCommand("Send BP pass count"); - this.stream.WriteInt32(breakpoint.PassCountKind); - this.stream.WriteInt32(breakpoint.PassCount); - } - - public SendResumeThread(threadId: number) { - return this.sendStepCommand(threadId, Commands.ResumeThreadCommandBytes); - } - public SendContinue(): Promise { - return new Promise(resolve => { - this.stream.Write(Commands.ResumeAllCommandBytes); - resolve(); - }) - } - public AutoResumeThread(threadId: number) { - - } - public SendClearStepping(threadId: number) { - - } - public Break() { - this.stream.Write(Commands.BreakAllCommandBytes); - } - public ExecuteText(text: string, reprKind: PythonEvaluationResultReprKind, stackFrame: IPythonStackFrame): Promise { - return new Promise((resolve, reject) => { - var executeId = this._idDispenser.Allocate(); - var cmd: IExecutionCommand = { - Id: executeId, - Text: text, - Frame: stackFrame, - PromiseResolve: resolve, - PromiseReject: reject - }; - this.PendingExecuteCommands.set(executeId, cmd); - this.stream.Write(Commands.ExecuteTextCommandBytes); - this.stream.WriteString(text); - this.stream.WriteInt64(stackFrame.Thread.Id); - this.stream.WriteInt32(stackFrame.FrameId); - this.stream.WriteInt32(executeId); - this.stream.WriteInt32(stackFrame.Kind); - this.stream.WriteInt32(reprKind); - }); - } - - public EnumChildren(text: string, stackFrame: IPythonStackFrame, timeout: number): Promise { - return new Promise((resolve, reject) => { - var executeId = this._idDispenser.Allocate(); - if (typeof (executeId) !== "number") { - var y = ""; - } - var cmd: IChildEnumCommand = { - Id: executeId, - Frame: stackFrame, - PromiseResolve: resolve, - PromiseReject: reject - }; - this.PendingChildEnumCommands.set(executeId, cmd); - setTimeout(() => { - if (this.PendingChildEnumCommands.has(executeId)) { - this.PendingChildEnumCommands.delete(executeId); - } - var seconds = timeout / 1000; - reject(`Enumerating children for ${text} timed out after ${seconds} seconds.`); - }, timeout); - - this.stream.Write(Commands.GetChildrenCommandBytes); - this.stream.WriteString(text); - this.stream.WriteInt64(stackFrame.Thread.Id); - this.stream.WriteInt32(stackFrame.FrameId); - this.stream.WriteInt32(executeId); - this.stream.WriteInt32(stackFrame.Kind); - }); - } - public SetLineNumber(pythonStackFrame: IPythonStackFrame, lineNo: number) { - - } -} diff --git a/src/client/debugger/PythonProcessCallbackHandler.ts b/src/client/debugger/PythonProcessCallbackHandler.ts deleted file mode 100644 index 731e621fd15e..000000000000 --- a/src/client/debugger/PythonProcessCallbackHandler.ts +++ /dev/null @@ -1,510 +0,0 @@ -'use strict'; - -import {FrameKind, IPythonProcess, IPythonThread, IPythonModule, IPythonEvaluationResult, IPythonStackFrame} from './Common/Contracts'; -import {IDjangoStackFrame, PythonEvaluationResultFlags, PythonLanguageVersion, IChildEnumCommand, IPythonException, IExecutionCommand} from './Common/Contracts'; -import * as utils from './Common/Utils'; -import {EventEmitter} from 'events'; -import {Commands} from './ProxyCommands'; -import {SocketStream} from './Common/SocketStream'; -import {ExtractTryStatements} from './Common/TryParser'; -import * as path from 'path'; - -export class PythonProcessCallbackHandler extends EventEmitter { - private process: IPythonProcess; - private idDispenser: utils.IdDispenser; - private stream: SocketStream; - private _stoppedForException: boolean; - constructor(process: IPythonProcess, stream: SocketStream, idDispenser: utils.IdDispenser) { - super(); - this.process = process; - this.stream = stream; - this.idDispenser = idDispenser; - } - - public HandleIncomingData() { - if (this.stream.Length === 0) { - return; - } - this.stream.BeginTransaction(); - - var cmd = this.stream.ReadAsciiString(4); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - switch (cmd) { - case "MODL": this.HandleModuleLoad(); break; - case "LOAD": this.HandleProcessLoad(); break; - case "STPD": this.HandleStepDone(); break; - case "NEWT": this.HandleThreadCreate(); break; - case "EXTT": this.HandleThreadExit(); break; - case "THRF": this.HandleThreadFrameList(); break; - case "OUTP": this.HandleDebuggerOutput(); break; - case "BRKS": this.HandleBreakPointSet(); break; - case "BRKF": this.HandleBreakPointFailed(); break; - case "BRKH": this.HandleBreakPointHit(); break; - case "DETC": this.HandleDetach(); break; // detach, report process exit - case "LAST": this.HandleLast(); break; - case "CHLD": this.HandleEnumChildren(); break; - case "REQH": this.HandleRequestHandlers(); break; - case "EXCP": this.HandleException(); break; - case "EXCR": this.HandleExecutionResult(); break; - case "EXCE": this.HandleExecutionException(); break; - case "ASBR": this.HandleAsyncBreak(); break; - default: { - - this.emit("error", `Unhandled command '${cmd}'`); - } - } - - if (this.stream.HasInsufficientDataForReading) { - //Most possibly due to insufficient data - this.stream.RollBackTransaction(); - return; - } - - this.stream.EndTransaction(); - if (this.stream.Length > 0) { - this.HandleIncomingData(); - } - } - - private get LanguageVersion(): PythonLanguageVersion { - return PythonLanguageVersion.Is2; - } - private HandleDetach() { - this.emit("detach"); - } - private HandleLast() { - this.stream.Write(Commands.LastAckCommandBytes); - this.emit("last"); - } - private HandleModuleLoad() { - var moduleId = this.stream.ReadInt32(); - var filename = this.stream.ReadString(); - - if (this.stream.HasInsufficientDataForReading) { - return; - } - if (filename != null) { - this.emit("moduleLoaded", utils.CreatePythonModule(moduleId, filename)); - } - } - private HandleDebuggerOutput() { - var threadId = this.stream.ReadInt64(); - var output = this.stream.ReadString(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var pyThread: IPythonThread; - if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); - } - this.emit("output", pyThread, output); - } - - private _createdFirstThread: boolean; - private HandleThreadCreate() { - var threadId = this.stream.ReadInt64(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var pyThread = utils.CreatePythonThread(threadId, this._createdFirstThread, this.process); - this._createdFirstThread = true; - this.process.Threads.set(threadId, pyThread); - this.emit("threadCreated", pyThread); - } - - private HandleThreadExit() { - var threadId = this.stream.ReadInt64(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var thread: IPythonThread; - if (this.process.Threads.has(threadId)) { - thread = this.process.Threads.get(threadId); - this.emit("threadExited", thread); - // this.process.Threads.delete(threadId); - } - } - - private HandleProcessLoad() { - var threadId = this.stream.ReadInt64(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - var pyThread: IPythonThread; - if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); - } - this.emit("processLoaded", pyThread); - } - - private HandleStepDone() { - var threadId = this.stream.ReadInt64(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - var pyThread: IPythonThread; - if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); - } - this.emit("stepCompleted", pyThread); - } - private HandleAsyncBreak() { - var threadId = this.stream.ReadInt64(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - var pyThread: IPythonThread; - if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); - } - this.emit("asyncBreakCompleted", pyThread); - } - private HandleBreakPointFailed() { - var id = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - this.emit("breakpointNotSet", id); - } - private HandleBreakPointSet() { - var id = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - this.emit("breakpointSet", id); - } - private HandleBreakPointHit() { - var breakId = this.stream.ReadInt32(); - var threadId = this.stream.ReadInt64(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var pyThread: IPythonThread; - if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); - } - this.emit("breakpointHit", pyThread, breakId); - } - private HandleRequestHandlers() { - var filename = this.stream.ReadString(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var fullyQualifiedFileName = filename; - if (!path.isAbsolute(fullyQualifiedFileName)) { - fullyQualifiedFileName = path.join(this.process.ProgramDirectory, filename); - } - - this.GetHandledExceptionRanges(fullyQualifiedFileName).then(statements=> { - this.stream.Write(Commands.SetExceptionHandlerInfoCommandBytes); - this.stream.WriteString(filename); - - this.stream.WriteInt32(statements.length); - - statements.forEach(statement=> { - this.stream.WriteInt32(statement.startLine); - this.stream.WriteInt32(statement.endLine); - - statement.expressions.forEach(expr=> { - this.stream.WriteString(expr); - }); - this.stream.WriteString("-"); - }); - }); - } - private GetHandledExceptionRanges(fileName: string): Promise<{ startLine: number, endLine: number, expressions: string[] }[]> { - return new Promise<{ startLine: number, endLine: number, expressions: string[] }[]>(resolve=> { - ExtractTryStatements(fileName).then(statements=> { - var exceptionRanges: { startLine: number, endLine: number, expressions: string[] }[] = [] - statements.forEach(statement=> { - var expressions = []; - if (statement.Exceptions.length === 0 || statement.Exceptions.indexOf("*") >= 0) { - expressions = ["*"]; - } - else { - statement.Exceptions.forEach(ex=> { - if (expressions.indexOf(ex) === -1) { - expressions.push(ex); - } - }); - } - - exceptionRanges.push({ - endLine: statement.EndLineNumber, - startLine: statement.StartLineNumber, - expressions: expressions - }); - }); - - resolve(exceptionRanges); - }); - }); - } - - private HandleException() { - var typeName = this.stream.ReadString(); - var threadId = this.stream.ReadInt64(); - var breakType = this.stream.ReadInt32(); - var desc = this.stream.ReadString(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - if (typeName != null && desc != null) { - var ex: IPythonException = { - TypeName: typeName, - Description: desc - }; - var pyThread: IPythonThread; - if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); - } - this.emit("exceptionRaised", pyThread, ex, breakType == 1 /* BREAK_TYPE_UNHANLDED */); - } - this._stoppedForException = true; - } - private HandleExecutionException() { - var execId = this.stream.ReadInt32(); - var exceptionText = this.stream.ReadString(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var cmd: IExecutionCommand = null; - if (this.process.PendingExecuteCommands.has(execId)) { - cmd = this.process.PendingExecuteCommands.get(execId); - if (this.process.PendingExecuteCommands.has(execId)) { - this.process.PendingExecuteCommands.delete(execId); - } - cmd.PromiseReject(exceptionText); - //console.log(`ExecuteText Exception ${execId}`); - } else { - //console.error("Received execution result with unknown execution ID " + execId); - } - this.idDispenser.Free(execId); - } - private HandleExecutionResult() { - var execId = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var cmd: IExecutionCommand = null; - if (this.process.PendingExecuteCommands.has(execId)) { - cmd = this.process.PendingExecuteCommands.get(execId); - } else { - //console.error("Received execution result with unknown execution ID " + execId); - } - - if (cmd === null) { - // Passing null for parameters other than stream is okay as long - // as we drop the result. - this.ReadPythonObject(null, null, null); - if (this.stream.HasInsufficientDataForReading) { - return; - } - } - else { - var evalResult = this.ReadPythonObject(cmd.Text, null, cmd.Frame); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - cmd.PromiseResolve(evalResult); - } - - if (cmd != null) { - if (this.process.PendingExecuteCommands.has(execId)) { - this.process.PendingExecuteCommands.delete(execId); - } - } - this.idDispenser.Free(execId); - } - - private HandleEnumChildren() { - var execId = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var cmd: IChildEnumCommand = null; - if (this.process.PendingChildEnumCommands.has(execId)) { - cmd = this.process.PendingChildEnumCommands.get(execId); - } else { - //console.error("Received enum children result with unknown execution ID " + execId); - } - - var childrenCount = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - const children: IPythonEvaluationResult[] = []; - for (var childCount = 0; childCount < childrenCount; childCount++) { - const childName = this.stream.ReadString(); - const childExpr = this.stream.ReadString(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var obj = this.ReadPythonObject(childExpr, childName, cmd === null ? null : cmd.Frame); - if (this.stream.HasInsufficientDataForReading) { - return; - } - children.push(obj); - } - - if (cmd != null) { - cmd.PromiseResolve(children); - if (this.process.PendingChildEnumCommands.has(execId)) { - this.process.PendingChildEnumCommands.delete(execId); - } - } - this.idDispenser.Free(execId); - } - private HandleThreadFrameList() { - var frames: IPythonStackFrame[] = []; - var threadId = this.stream.ReadInt64(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var pyThread: IPythonThread; - if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); - } - - var threadName = this.stream.ReadString(); - var frameCount = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - for (var i = 0; i < frameCount; i++) { - var startLine = this.stream.ReadInt32(); - var endLine = this.stream.ReadInt32(); - var lineNo = this.stream.ReadInt32(); - var frameName = this.stream.ReadString(); - var filename = this.stream.ReadString(); - var argCount = this.stream.ReadInt32(); - var frameKind = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var frame: IPythonStackFrame = null; - if (pyThread != null) { - switch (frameKind) { - case FrameKind.Django: { - var sourceFile = this.stream.ReadString(); - var sourceLine = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var djangoFrame: IDjangoStackFrame = { - EndLine: endLine, FileName: filename, - FrameId: i, FunctionName: frameName, - Kind: frameKind, LineNo: lineNo, - Locals: [], Parameters: [], - Thread: pyThread, SourceFile: sourceFile, - SourceLine: sourceLine, StartLine: startLine - }; - - frame = djangoFrame; - break; - } - default: { - frame = { - EndLine: endLine, FileName: filename, - FrameId: i, FunctionName: frameName, - Kind: frameKind, LineNo: lineNo, - Locals: [], Parameters: [], - Thread: pyThread, StartLine: startLine - }; - break; - } - } - - } - - var varCount = this.stream.ReadInt32(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - var variables: IPythonEvaluationResult[] = []; - for (var j = 0; j < varCount; j++) { - var name = this.stream.ReadString(); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - if (frame != null) { - var variableObj = this.ReadPythonObject(name, name, frame); - if (this.stream.HasInsufficientDataForReading) { - return; - } - - variables.push(variableObj); - } - } - if (frame != null) { - frame.Parameters = variables.splice(0, argCount); - frame.Locals = variables; - frames.push(frame); - } - } - - if (pyThread != null) { - pyThread.Frames = frames; - if (typeof threadName === "string" && threadName.length > 0) { - pyThread.Name = threadName; - } - } - } - - private ReadPythonObject(expr: string, childName: string, frame: IPythonStackFrame): IPythonEvaluationResult { - var objRepr = this.stream.ReadString(); - var hexRepr = this.stream.ReadString(); - var typeName = this.stream.ReadString(); - var length = this.stream.ReadInt64(); - var flags = this.stream.ReadInt32(); - - if (this.stream.HasInsufficientDataForReading) { - return; - } - - if ((flags & PythonEvaluationResultFlags.Raw) == 0 && ((typeName === "unicode" && this.LanguageVersion === PythonLanguageVersion.Is2) - || (typeName === "str" && this.LanguageVersion === PythonLanguageVersion.Is3))) { - objRepr = utils.FixupEscapedUnicodeChars(objRepr); - } - - if (typeName == "bool") { - hexRepr = null; - } - - var pythonEvaluationResult: IPythonEvaluationResult = { - ChildName: childName, - Process: this.process, - IsExpandable: (flags & PythonEvaluationResultFlags.Expandable) > 0, - Flags: flags, - StringRepr: objRepr, - HexRepr: hexRepr, - TypeName: typeName, - Expression: expr, - Length: length, - Frame: frame - }; - - return pythonEvaluationResult; - } -} diff --git a/src/client/debugger/constants.ts b/src/client/debugger/constants.ts new file mode 100644 index 000000000000..6d4af08019be --- /dev/null +++ b/src/client/debugger/constants.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../common/constants'; + +export const DEBUGGER_PATH = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy'); +export const DebuggerTypeName = 'python'; diff --git a/src/client/debugger/extension/adapter/activator.ts b/src/client/debugger/extension/adapter/activator.ts new file mode 100644 index 000000000000..be64226946c5 --- /dev/null +++ b/src/client/debugger/extension/adapter/activator.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../../activation/types'; +import { IDebugService } from '../../../common/application/types'; +import { IDisposableRegistry } from '../../../common/types'; +import { DebuggerTypeName } from '../../constants'; +import { IAttachProcessProviderFactory } from '../attachQuickPick/types'; +import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory, IOutdatedDebuggerPromptFactory } from '../types'; + +@injectable() +export class DebugAdapterActivator implements IExtensionSingleActivationService { + constructor( + @inject(IDebugService) private readonly debugService: IDebugService, + @inject(IDebugAdapterDescriptorFactory) private descriptorFactory: IDebugAdapterDescriptorFactory, + @inject(IDebugSessionLoggingFactory) private debugSessionLoggingFactory: IDebugSessionLoggingFactory, + @inject(IOutdatedDebuggerPromptFactory) private debuggerPromptFactory: IOutdatedDebuggerPromptFactory, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IAttachProcessProviderFactory) + private readonly attachProcessProviderFactory: IAttachProcessProviderFactory + ) {} + public async activate(): Promise { + this.attachProcessProviderFactory.registerCommands(); + + this.disposables.push( + this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debugSessionLoggingFactory) + ); + this.disposables.push( + this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debuggerPromptFactory) + ); + this.disposables.push( + this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.descriptorFactory) + ); + } +} diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts new file mode 100644 index 000000000000..6379874d8e47 --- /dev/null +++ b/src/client/debugger/extension/adapter/factory.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { + DebugAdapterDescriptor, + DebugAdapterExecutable, + DebugAdapterServer, + DebugSession, + WorkspaceFolder +} from 'vscode'; +import { IApplicationShell } from '../../../common/application/types'; +import { traceVerbose } from '../../../common/logger'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; +import { IDebugAdapterDescriptorFactory } from '../types'; + +@injectable() +export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFactory { + constructor( + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IApplicationShell) private readonly appShell: IApplicationShell + ) {} + public async createDebugAdapterDescriptor( + session: DebugSession, + _executable: DebugAdapterExecutable | undefined + ): Promise { + const configuration = session.configuration as LaunchRequestArguments | AttachRequestArguments; + + // There are four distinct scenarios here: + // + // 1. "launch"; + // 2. "attach" with "processId"; + // 3. "attach" with "listen"; + // 4. "attach" with "connect" (or legacy "host"/"port"); + // + // For the first three, we want to spawn the debug adapter directly. + // For the last one, the adapter is already listening on the specified socket. + // When "debugServer" is used, the standard adapter factory takes care of it - no need to check here. + + if (configuration.request === 'attach') { + if (configuration.connect !== undefined) { + return new DebugAdapterServer(configuration.connect.port, configuration.connect.host ?? '127.0.0.1'); + } else if (configuration.port !== undefined) { + return new DebugAdapterServer(configuration.port, configuration.host ?? '127.0.0.1'); + } else if (configuration.listen === undefined && configuration.processId === undefined) { + throw new Error('"request":"attach" requires either "connect", "listen", or "processId"'); + } + } + + const pythonPath = await this.getPythonPath(configuration, session.workspaceFolder); + if (pythonPath.length !== 0) { + if (configuration.request === 'attach' && configuration.processId !== undefined) { + sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS); + } + + // "logToFile" is not handled directly by the adapter - instead, we need to pass + // the corresponding CLI switch when spawning it. + const logArgs = configuration.logToFile ? ['--log-dir', EXTENSION_ROOT_DIR] : []; + + if (configuration.debugAdapterPath !== undefined) { + return new DebugAdapterExecutable(pythonPath, [configuration.debugAdapterPath, ...logArgs]); + } + + const debuggerAdapterPathToUse = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'lib', + 'python', + 'debugpy', + 'adapter' + ); + + sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, { usingWheels: true }); + return new DebugAdapterExecutable(pythonPath, [debuggerAdapterPathToUse, ...logArgs]); + } + + // Unlikely scenario. + throw new Error('Debug Adapter Executable not provided'); + } + + /** + * Get the python executable used to launch the Python Debug Adapter. + * In the case of `attach` scenarios, just use the workspace interpreter, else first available one. + * It is unlike user won't have a Python interpreter + * + * @private + * @param {(LaunchRequestArguments | AttachRequestArguments)} configuration + * @param {WorkspaceFolder} [workspaceFolder] + * @returns {Promise} Path to the python interpreter for this workspace. + * @memberof DebugAdapterDescriptorFactory + */ + private async getPythonPath( + configuration: LaunchRequestArguments | AttachRequestArguments, + workspaceFolder?: WorkspaceFolder + ): Promise { + if (configuration.pythonPath) { + return configuration.pythonPath; + } + const resourceUri = workspaceFolder ? workspaceFolder.uri : undefined; + const interpreter = await this.interpreterService.getActiveInterpreter(resourceUri); + if (interpreter) { + traceVerbose(`Selecting active interpreter as Python Executable for DA '${interpreter.path}'`); + return interpreter.path; + } + + const interpreters = await this.interpreterService.getInterpreters(resourceUri); + if (interpreters.length === 0) { + this.notifySelectInterpreter().ignoreErrors(); + return ''; + } + + traceVerbose(`Picking first available interpreter to launch the DA '${interpreters[0].path}'`); + return interpreters[0].path; + } + + /** + * Notify user about the requirement for Python. + * Unlikely scenario, as ex expect users to have Python in order to use the extension. + * However it is possible to ignore the warnings and continue using the extension. + * + * @private + * @memberof DebugAdapterDescriptorFactory + */ + private async notifySelectInterpreter() { + await this.appShell.showErrorMessage( + // tslint:disable-next-line: messages-must-be-localized + 'Please install Python or select a Python Interpreter to use the debugger.' + ); + } +} diff --git a/src/client/debugger/extension/adapter/logging.ts b/src/client/debugger/extension/adapter/logging.ts new file mode 100644 index 000000000000..58b2f7ff4358 --- /dev/null +++ b/src/client/debugger/extension/adapter/logging.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { + DebugAdapterTracker, + DebugAdapterTrackerFactory, + DebugConfiguration, + DebugSession, + ProviderResult +} from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +import { IFileSystem, WriteStream } from '../../../common/platform/types'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; + +class DebugSessionLoggingTracker implements DebugAdapterTracker { + private readonly enabled: boolean = false; + private stream?: WriteStream; + private timer = new StopWatch(); + + constructor(private readonly session: DebugSession, fileSystem: IFileSystem) { + this.enabled = this.session.configuration.logToFile as boolean; + if (this.enabled) { + const fileName = `debugger.vscode_${this.session.id}.log`; + this.stream = fileSystem.createWriteStream(path.join(EXTENSION_ROOT_DIR, fileName)); + } + } + + public onWillStartSession() { + this.timer.reset(); + this.log(`Starting Session:\n${this.stringify(this.session.configuration)}\n`); + } + + public onWillReceiveMessage(message: DebugProtocol.Message) { + this.log(`Client --> Adapter:\n${this.stringify(message)}\n`); + } + + public onDidSendMessage(message: DebugProtocol.Message) { + this.log(`Client <-- Adapter:\n${this.stringify(message)}\n`); + } + + public onWillStopSession() { + this.log('Stopping Session\n'); + } + + public onError(error: Error) { + this.log(`Error:\n${this.stringify(error)}\n`); + } + + public onExit(code: number | undefined, signal: string | undefined) { + this.log(`Exit:\nExit-Code: ${code ? code : 0}\nSignal: ${signal ? signal : 'none'}\n`); + this.stream?.close(); + } + + private log(message: string) { + if (this.enabled) { + this.stream!.write(`${this.timer.elapsedTime} ${message}`); + } + } + + private stringify(data: DebugProtocol.Message | Error | DebugConfiguration) { + return JSON.stringify(data, null, 4); + } +} + +@injectable() +export class DebugSessionLoggingFactory implements DebugAdapterTrackerFactory { + constructor(@inject(IFileSystem) private readonly fileSystem: IFileSystem) {} + + public createDebugAdapterTracker(session: DebugSession): ProviderResult { + return new DebugSessionLoggingTracker(session, this.fileSystem); + } +} diff --git a/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts new file mode 100644 index 000000000000..b9aa0d14542b --- /dev/null +++ b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, ProviderResult } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { IApplicationShell } from '../../../common/application/types'; +import { IBrowserService } from '../../../common/types'; +import { Common, OutdatedDebugger } from '../../../common/utils/localize'; +import { IPromptShowState } from './types'; + +// This situation occurs when user connects to old containers or server where +// the debugger they had installed was ptvsd. We should show a prompt to ask them to update. +class OutdatedDebuggerPrompt implements DebugAdapterTracker { + constructor( + private promptCheck: IPromptShowState, + private appShell: IApplicationShell, + private browserService: IBrowserService + ) {} + + public onDidSendMessage(message: DebugProtocol.ProtocolMessage) { + if (this.promptCheck.shouldShowPrompt() && this.isPtvsd(message)) { + const prompts = [Common.moreInfo()]; + this.appShell + .showInformationMessage(OutdatedDebugger.outdatedDebuggerMessage(), ...prompts) + .then((selection) => { + if (selection === prompts[0]) { + this.browserService.launch('https://aka.ms/migrateToDebugpy'); + } + }); + } + } + + private isPtvsd(message: DebugProtocol.ProtocolMessage) { + if (message.type === 'event') { + const eventMessage = message as DebugProtocol.Event; + if (eventMessage.event === 'output') { + const outputMessage = eventMessage as DebugProtocol.OutputEvent; + if (outputMessage.body.category === 'telemetry') { + // debugpy sends telemetry as both ptvsd and debugpy. This was done to help with + // transition from ptvsd to debugpy while analyzing usage telemetry. + if ( + outputMessage.body.output === 'ptvsd' && + !outputMessage.body.data.packageVersion.startsWith('1') + ) { + this.promptCheck.setShowPrompt(false); + return true; + } + if (outputMessage.body.output === 'debugpy') { + this.promptCheck.setShowPrompt(false); + } + } + } + } + return false; + } +} + +class OutdatedDebuggerPromptState implements IPromptShowState { + private shouldShow: boolean = true; + public shouldShowPrompt(): boolean { + return this.shouldShow; + } + public setShowPrompt(show: boolean) { + this.shouldShow = show; + } +} + +@injectable() +export class OutdatedDebuggerPromptFactory implements DebugAdapterTrackerFactory { + private readonly promptCheck: OutdatedDebuggerPromptState; + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IBrowserService) private browserService: IBrowserService + ) { + this.promptCheck = new OutdatedDebuggerPromptState(); + } + public createDebugAdapterTracker(_session: DebugSession): ProviderResult { + return new OutdatedDebuggerPrompt(this.promptCheck, this.appShell, this.browserService); + } +} diff --git a/src/client/debugger/extension/adapter/remoteLaunchers.ts b/src/client/debugger/extension/adapter/remoteLaunchers.ts new file mode 100644 index 000000000000..8f1eab6f9b51 --- /dev/null +++ b/src/client/debugger/extension/adapter/remoteLaunchers.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../../../common/constants'; +import '../../../common/extensions'; + +const pathToPythonLibDir = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python'); +const pathToDebugger = path.join(pathToPythonLibDir, 'debugpy'); + +export type RemoteDebugOptions = { + host: string; + port: number; + waitUntilDebuggerAttaches: boolean; +}; + +export function getDebugpyLauncherArgs(options: RemoteDebugOptions, debuggerPath: string = pathToDebugger) { + const waitArgs = options.waitUntilDebuggerAttaches ? ['--wait-for-client'] : []; + return [debuggerPath.fileToCommandArgument(), '--listen', `${options.host}:${options.port}`, ...waitArgs]; +} + +export function getDebugpyPackagePath(): string { + return pathToDebugger; +} diff --git a/src/client/debugger/extension/adapter/types.ts b/src/client/debugger/extension/adapter/types.ts new file mode 100644 index 000000000000..6c082a801ad6 --- /dev/null +++ b/src/client/debugger/extension/adapter/types.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export const IPromptShowState = Symbol('IPromptShowState'); +export interface IPromptShowState { + shouldShowPrompt(): boolean; + setShowPrompt(show: boolean): void; +} diff --git a/src/client/debugger/extension/attachQuickPick/factory.ts b/src/client/debugger/extension/attachQuickPick/factory.ts new file mode 100644 index 000000000000..f36c45183965 --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/factory.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IApplicationShell, ICommandManager } from '../../../common/application/types'; +import { Commands } from '../../../common/constants'; +import { IPlatformService } from '../../../common/platform/types'; +import { IProcessServiceFactory } from '../../../common/process/types'; +import { IDisposableRegistry } from '../../../common/types'; +import { AttachPicker } from './picker'; +import { AttachProcessProvider } from './provider'; +import { IAttachProcessProviderFactory } from './types'; + +@injectable() +export class AttachProcessProviderFactory implements IAttachProcessProviderFactory { + constructor( + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry + ) {} + + public registerCommands() { + const provider = new AttachProcessProvider(this.platformService, this.processServiceFactory); + const picker = new AttachPicker(this.applicationShell, provider); + const disposable = this.commandManager.registerCommand( + Commands.PickLocalProcess, + () => picker.showQuickPick(), + this + ); + this.disposableRegistry.push(disposable); + } +} diff --git a/src/client/debugger/extension/attachQuickPick/picker.ts b/src/client/debugger/extension/attachQuickPick/picker.ts new file mode 100644 index 000000000000..db1d4a8a8a74 --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/picker.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable } from 'vscode'; +import { IApplicationShell } from '../../../common/application/types'; +import { getIcon } from '../../../common/utils/icons'; +import { AttachProcess } from '../../../common/utils/localize'; +import { IAttachItem, IAttachPicker, IAttachProcessProvider, REFRESH_BUTTON_ICON } from './types'; + +@injectable() +export class AttachPicker implements IAttachPicker { + constructor( + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + private readonly attachItemsProvider: IAttachProcessProvider + ) {} + + public showQuickPick(): Promise { + return new Promise(async (resolve, reject) => { + const processEntries = await this.attachItemsProvider.getAttachItems(); + + const refreshButton = { + iconPath: getIcon(REFRESH_BUTTON_ICON), + tooltip: AttachProcess.refreshList() + }; + + const quickPick = this.applicationShell.createQuickPick(); + quickPick.title = AttachProcess.attachTitle(); + quickPick.placeholder = AttachProcess.selectProcessPlaceholder(); + quickPick.canSelectMany = false; + quickPick.matchOnDescription = true; + quickPick.matchOnDetail = true; + quickPick.items = processEntries; + quickPick.buttons = [refreshButton]; + + const disposables: Disposable[] = []; + + quickPick.onDidTriggerButton( + async () => { + const attachItems = await this.attachItemsProvider.getAttachItems(); + quickPick.items = attachItems; + }, + this, + disposables + ); + + quickPick.onDidAccept( + () => { + if (quickPick.selectedItems.length !== 1) { + reject(new Error(AttachProcess.noProcessSelected())); + } + + const selectedId = quickPick.selectedItems[0].id; + + disposables.forEach((item) => item.dispose()); + quickPick.dispose(); + + resolve(selectedId); + }, + undefined, + disposables + ); + + quickPick.onDidHide( + () => { + disposables.forEach((item) => item.dispose()); + quickPick.dispose(); + + reject(new Error(AttachProcess.noProcessSelected())); + }, + undefined, + disposables + ); + + quickPick.show(); + }); + } +} diff --git a/src/client/debugger/extension/attachQuickPick/provider.ts b/src/client/debugger/extension/attachQuickPick/provider.ts new file mode 100644 index 000000000000..fbeb3eee34ac --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/provider.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IPlatformService } from '../../../common/platform/types'; +import { IProcessServiceFactory } from '../../../common/process/types'; +import { AttachProcess as AttachProcessLocalization } from '../../../common/utils/localize'; +import { PsProcessParser } from './psProcessParser'; +import { IAttachItem, IAttachProcessProvider, ProcessListCommand } from './types'; +import { WmicProcessParser } from './wmicProcessParser'; + +@injectable() +export class AttachProcessProvider implements IAttachProcessProvider { + constructor( + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory + ) {} + + public getAttachItems(): Promise { + return this._getInternalProcessEntries().then((processEntries) => { + processEntries.sort( + ( + { processName: aprocessName, commandLine: aCommandLine }, + { processName: bProcessName, commandLine: bCommandLine } + ) => { + const compare = (aString: string, bString: string): number => { + // localeCompare is significantly slower than < and > (2000 ms vs 80 ms for 10,000 elements) + // We can change to localeCompare if this becomes an issue + const aLower = aString.toLowerCase(); + const bLower = bString.toLowerCase(); + + if (aLower === bLower) { + return 0; + } + + return aLower < bLower ? -1 : 1; + }; + + const aPython = aprocessName.startsWith('python'); + const bPython = bProcessName.startsWith('python'); + + if (aPython || bPython) { + if (aPython && !bPython) { + return -1; + } + if (bPython && !aPython) { + return 1; + } + + return aPython ? compare(aCommandLine!, bCommandLine!) : compare(bCommandLine!, aCommandLine!); + } + + return compare(aprocessName, bProcessName); + } + ); + + return processEntries; + }); + } + + public async _getInternalProcessEntries(): Promise { + let processCmd: ProcessListCommand; + if (this.platformService.isMac) { + processCmd = PsProcessParser.psDarwinCommand; + } else if (this.platformService.isLinux) { + processCmd = PsProcessParser.psLinuxCommand; + } else if (this.platformService.isWindows) { + processCmd = WmicProcessParser.wmicCommand; + } else { + throw new Error(AttachProcessLocalization.unsupportedOS().format(this.platformService.osType)); + } + + const processService = await this.processServiceFactory.create(); + const output = await processService.exec(processCmd.command, processCmd.args, { throwOnStdErr: true }); + + return this.platformService.isWindows + ? WmicProcessParser.parseProcesses(output.stdout) + : PsProcessParser.parseProcesses(output.stdout); + } +} diff --git a/src/client/debugger/extension/attachQuickPick/psProcessParser.ts b/src/client/debugger/extension/attachQuickPick/psProcessParser.ts new file mode 100644 index 000000000000..e5ab6f404a74 --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/psProcessParser.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IAttachItem, ProcessListCommand } from './types'; + +export namespace PsProcessParser { + const secondColumnCharacters = 50; + const commColumnTitle = ''.padStart(secondColumnCharacters, 'a'); + + // Perf numbers: + // OS X 10.10 + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 272 | 52 | + // | 296 | 49 | + // | 384 | 53 | + // | 784 | 116 | + // + // Ubuntu 16.04 + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 232 | 26 | + // | 336 | 34 | + // | 736 | 62 | + // | 1039 | 115 | + // | 1239 | 182 | + + // ps outputs as a table. With the option "ww", ps will use as much width as necessary. + // However, that only applies to the right-most column. Here we use a hack of setting + // the column header to 50 a's so that the second column will have at least that many + // characters. 50 was chosen because that's the maximum length of a "label" in the + // QuickPick UI in VS Code. + + // the BSD version of ps uses '-c' to have 'comm' only output the executable name and not + // the full path. The Linux version of ps has 'comm' to only display the name of the executable + // Note that comm on Linux systems is truncated to 16 characters: + // https://bugzilla.redhat.com/show_bug.cgi?id=429565 + // Since 'args' contains the full path to the executable, even if truncated, searching will work as desired. + export const psLinuxCommand: ProcessListCommand = { + command: 'ps', + args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`] + }; + export const psDarwinCommand: ProcessListCommand = { + command: 'ps', + args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`, '-c'] + }; + + export function parseProcesses(processes: string): IAttachItem[] { + const lines: string[] = processes.split('\n'); + return parseProcessesFromPsArray(lines); + } + + function parseProcessesFromPsArray(processArray: string[]): IAttachItem[] { + const processEntries: IAttachItem[] = []; + + // lines[0] is the header of the table + for (let i = 1; i < processArray.length; i += 1) { + const line = processArray[i]; + if (!line) { + continue; + } + + const processEntry = parseLineFromPs(line); + if (processEntry) { + processEntries.push(processEntry); + } + } + + return processEntries; + } + + function parseLineFromPs(line: string): IAttachItem | undefined { + // Explanation of the regex: + // - any leading whitespace + // - PID + // - whitespace + // - executable name --> this is PsAttachItemsProvider.secondColumnCharacters - 1 because ps reserves one character + // for the whitespace separator + // - whitespace + // - args (might be empty) + const psEntry: RegExp = new RegExp(`^\\s*([0-9]+)\\s+(.{${secondColumnCharacters - 1}})\\s+(.*)$`); + const matches = psEntry.exec(line); + + if (matches?.length === 4) { + const pid = matches[1].trim(); + const executable = matches[2].trim(); + const cmdline = matches[3].trim(); + + return { + label: executable, + description: pid, + detail: cmdline, + id: pid, + processName: executable, + commandLine: cmdline + }; + } + } +} diff --git a/src/client/debugger/extension/attachQuickPick/types.ts b/src/client/debugger/extension/attachQuickPick/types.ts new file mode 100644 index 000000000000..c668231b29e2 --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/types.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { QuickPickItem, Uri } from 'vscode'; + +export type ProcessListCommand = { command: string; args: string[] }; + +export interface IAttachItem extends QuickPickItem { + id: string; + processName: string; + commandLine: string; +} + +export interface IAttachProcessProvider { + getAttachItems(): Promise; +} + +export const IAttachProcessProviderFactory = Symbol('IAttachProcessProviderFactory'); +export interface IAttachProcessProviderFactory { + registerCommands(): void; +} + +export interface IAttachPicker { + showQuickPick(): Promise; +} + +export interface IRefreshButton { + iconPath: { light: string | Uri; dark: string | Uri }; + tooltip: string; +} + +export const REFRESH_BUTTON_ICON = 'refresh.svg'; diff --git a/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts b/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts new file mode 100644 index 000000000000..d7da2b98ace3 --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IAttachItem, ProcessListCommand } from './types'; + +export namespace WmicProcessParser { + const wmicNameTitle = 'Name'; + const wmicCommandLineTitle = 'CommandLine'; + const wmicPidTitle = 'ProcessId'; + const defaultEmptyEntry: IAttachItem = { + label: '', + description: '', + detail: '', + id: '', + processName: '', + commandLine: '' + }; + + // Perf numbers on Win10: + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 309 | 413 | + // | 407 | 463 | + // | 887 | 746 | + // | 1308 | 1132 | + export const wmicCommand: ProcessListCommand = { + command: 'wmic', + args: ['process', 'get', 'Name,ProcessId,CommandLine', '/FORMAT:list'] + }; + + export function parseProcesses(processes: string): IAttachItem[] { + const lines: string[] = processes.split('\r\n'); + const processEntries: IAttachItem[] = []; + let entry = { ...defaultEmptyEntry }; + + for (const line of lines) { + if (!line.length) { + continue; + } + + parseLineFromWmic(line, entry); + + // Each entry of processes has ProcessId as the last line + if (line.lastIndexOf(wmicPidTitle, 0) === 0) { + processEntries.push(entry); + entry = { ...defaultEmptyEntry }; + } + } + + return processEntries; + } + + function parseLineFromWmic(line: string, item: IAttachItem): IAttachItem { + const splitter = line.indexOf('='); + const currentItem = item; + + if (splitter > 0) { + const key = line.slice(0, splitter).trim(); + let value = line.slice(splitter + 1).trim(); + + if (key === wmicNameTitle) { + currentItem.label = value; + currentItem.processName = value; + } else if (key === wmicPidTitle) { + currentItem.description = value; + currentItem.id = value; + } else if (key === wmicCommandLineTitle) { + const dosDevicePrefix = '\\??\\'; // DOS device prefix, see https://reverseengineering.stackexchange.com/a/15178 + if (value.lastIndexOf(dosDevicePrefix, 0) === 0) { + value = value.slice(dosDevicePrefix.length); + } + + currentItem.detail = value; + currentItem.commandLine = value; + } + } + + return currentItem; + } +} diff --git a/src/client/debugger/extension/banner.ts b/src/client/debugger/extension/banner.ts new file mode 100644 index 000000000000..f2cea5c64fd3 --- /dev/null +++ b/src/client/debugger/extension/banner.ts @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable } from 'vscode'; +import { IApplicationShell, IDebugService } from '../../common/application/types'; +import '../../common/extensions'; +import { traceError } from '../../common/logger'; +import { IBrowserService, IDisposableRegistry, IPersistentStateFactory, IRandom } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { DebuggerTypeName } from '../constants'; +import { IDebuggerBanner } from './types'; + +const SAMPLE_SIZE_PER_HUNDRED = 10; + +export enum PersistentStateKeys { + ShowBanner = 'ShowBanner', + DebuggerLaunchCounter = 'DebuggerLaunchCounter', + DebuggerLaunchThresholdCounter = 'DebuggerLaunchThresholdCounter', + UserSelected = 'DebuggerUserSelected' +} + +@injectable() +export class DebuggerBanner implements IDebuggerBanner { + private initialized?: boolean; + private disabledInCurrentSession?: boolean; + private userSelected?: boolean; + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + + public initialize() { + if (this.initialized) { + return; + } + this.initialized = true; + + // Don't even bother adding handlers if banner has been turned off. + if (!this.isEnabled()) { + return; + } + + this.addCallback(); + } + + // "enabled" state + + public isEnabled(): boolean { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const key = PersistentStateKeys.ShowBanner; + const state = factory.createGlobalPersistentState(key, true); + return state.value; + } + + public async disable(): Promise { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const key = PersistentStateKeys.ShowBanner; + const state = factory.createGlobalPersistentState(key, false); + await state.updateValue(false); + } + + // showing banner + + public async shouldShow(): Promise { + if (!this.isEnabled() || this.disabledInCurrentSession) { + return false; + } + if (!(await this.passedThreshold())) { + return false; + } + return this.isUserSelected(); + } + + public async show(): Promise { + const appShell = this.serviceContainer.get(IApplicationShell); + const msg = 'Can you please take 2 minutes to tell us how the debugger is working for you?'; + const yes = 'Yes, take survey now'; + const no = 'No thanks'; + const later = 'Remind me later'; + const response = await appShell.showInformationMessage(msg, yes, no, later); + switch (response) { + case yes: { + await this.action(); + await this.disable(); + break; + } + case no: { + await this.disable(); + break; + } + default: { + // Disable for the current session. + this.disabledInCurrentSession = true; + } + } + } + + private async action(): Promise { + const debuggerLaunchCounter = await this.getGetDebuggerLaunchCounter(); + const browser = this.serviceContainer.get(IBrowserService); + browser.launch(`https://www.research.net/r/N7B25RV?n=${debuggerLaunchCounter}`); + } + + // user selection + + private async isUserSelected(): Promise { + if (this.userSelected !== undefined) { + return this.userSelected; + } + + const factory = this.serviceContainer.get(IPersistentStateFactory); + const key = PersistentStateKeys.UserSelected; + const state = factory.createGlobalPersistentState(key, undefined); + let selected = state.value; + if (selected === undefined) { + const runtime = this.serviceContainer.get(IRandom); + const randomSample = runtime.getRandomInt(0, 100); + selected = randomSample < SAMPLE_SIZE_PER_HUNDRED; + state.updateValue(selected).ignoreErrors(); + } + this.userSelected = selected; + return selected; + } + + // persistent counter + + private async passedThreshold(): Promise { + const [threshold, debuggerCounter] = await Promise.all([ + this.getDebuggerLaunchThresholdCounter(), + this.getGetDebuggerLaunchCounter() + ]); + return debuggerCounter >= threshold; + } + + private async incrementDebuggerLaunchCounter(): Promise { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const key = PersistentStateKeys.DebuggerLaunchCounter; + const state = factory.createGlobalPersistentState(key, 0); + await state.updateValue(state.value + 1); + } + + private async getGetDebuggerLaunchCounter(): Promise { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const key = PersistentStateKeys.DebuggerLaunchCounter; + const state = factory.createGlobalPersistentState(key, 0); + return state.value; + } + + private async getDebuggerLaunchThresholdCounter(): Promise { + const factory = this.serviceContainer.get(IPersistentStateFactory); + const key = PersistentStateKeys.DebuggerLaunchThresholdCounter; + const state = factory.createGlobalPersistentState(key, undefined); + if (state.value === undefined) { + const runtime = this.serviceContainer.get(IRandom); + const randomNumber = runtime.getRandomInt(1, 11); + await state.updateValue(randomNumber); + } + return state.value!; + } + + // debugger-specific functionality + + private addCallback() { + const debuggerService = this.serviceContainer.get(IDebugService); + const disposable = debuggerService.onDidTerminateDebugSession(async (e) => { + if (e.type === DebuggerTypeName) { + await this.onDidTerminateDebugSession().catch((ex) => traceError('Error in debugger Banner', ex)); + } + }); + this.serviceContainer.get(IDisposableRegistry).push(disposable); + } + + private async onDidTerminateDebugSession(): Promise { + if (!this.isEnabled()) { + return; + } + await this.incrementDebuggerLaunchCounter(); + const show = await this.shouldShow(); + if (!show) { + return; + } + + await this.show(); + } +} diff --git a/src/client/debugger/extension/configuration/debugConfigurationService.ts b/src/client/debugger/extension/configuration/debugConfigurationService.ts new file mode 100644 index 000000000000..655de8b6a5df --- /dev/null +++ b/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { CancellationToken, DebugConfiguration, QuickPickItem, WorkspaceFolder } from 'vscode'; +import { DebugConfigStrings } from '../../../common/utils/localize'; +import { + IMultiStepInput, + IMultiStepInputFactory, + InputStep, + IQuickPickParameters +} from '../../../common/utils/multiStepInput'; +import { AttachRequestArguments, DebugConfigurationArguments, LaunchRequestArguments } from '../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationService } from '../types'; +import { IDebugConfigurationProviderFactory, IDebugConfigurationResolver } from './types'; + +@injectable() +export class PythonDebugConfigurationService implements IDebugConfigurationService { + constructor( + @inject(IDebugConfigurationResolver) + @named('attach') + private readonly attachResolver: IDebugConfigurationResolver, + @inject(IDebugConfigurationResolver) + @named('launch') + private readonly launchResolver: IDebugConfigurationResolver, + @inject(IDebugConfigurationProviderFactory) + private readonly providerFactory: IDebugConfigurationProviderFactory, + @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory + ) {} + public async provideDebugConfigurations( + folder: WorkspaceFolder | undefined, + token?: CancellationToken + ): Promise { + const config: Partial = {}; + const state = { config, folder, token }; + + // Disabled until configuration issues are addressed by VS Code. See #4007 + const multiStep = this.multiStepFactory.create(); + await multiStep.run((input, s) => this.pickDebugConfiguration(input, s), state); + + if (Object.keys(state.config).length === 0) { + return; + } else { + return [state.config as DebugConfiguration]; + } + } + public async resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: DebugConfiguration, + token?: CancellationToken + ): Promise { + if (debugConfiguration.request === 'attach') { + return this.attachResolver.resolveDebugConfiguration( + folder, + debugConfiguration as AttachRequestArguments, + token + ); + } else if (debugConfiguration.request === 'test') { + throw Error("Please use the command 'Python: Debug Unit Tests'"); + } else { + if (Object.keys(debugConfiguration).length === 0) { + const configs = await this.provideDebugConfigurations(folder, token); + if (configs === undefined) { + return; + } + if (Array.isArray(configs) && configs.length === 1) { + debugConfiguration = configs[0]; + } + } + return this.launchResolver.resolveDebugConfiguration( + folder, + debugConfiguration as LaunchRequestArguments, + token + ); + } + } + protected async pickDebugConfiguration( + input: IMultiStepInput, + state: DebugConfigurationState + ): Promise | void> { + type DebugConfigurationQuickPickItem = QuickPickItem & { type: DebugConfigurationType }; + const items: DebugConfigurationQuickPickItem[] = [ + { + label: DebugConfigStrings.file.selectConfiguration.label(), + type: DebugConfigurationType.launchFile, + description: DebugConfigStrings.file.selectConfiguration.description() + }, + { + label: DebugConfigStrings.module.selectConfiguration.label(), + type: DebugConfigurationType.launchModule, + description: DebugConfigStrings.module.selectConfiguration.description() + }, + { + label: DebugConfigStrings.attach.selectConfiguration.label(), + type: DebugConfigurationType.remoteAttach, + description: DebugConfigStrings.attach.selectConfiguration.description() + }, + { + label: DebugConfigStrings.attachPid.selectConfiguration.label(), + type: DebugConfigurationType.pidAttach, + description: DebugConfigStrings.attachPid.selectConfiguration.description() + }, + { + label: DebugConfigStrings.django.selectConfiguration.label(), + type: DebugConfigurationType.launchDjango, + description: DebugConfigStrings.django.selectConfiguration.description() + }, + { + label: DebugConfigStrings.flask.selectConfiguration.label(), + type: DebugConfigurationType.launchFlask, + description: DebugConfigStrings.flask.selectConfiguration.description() + }, + { + label: DebugConfigStrings.pyramid.selectConfiguration.label(), + type: DebugConfigurationType.launchPyramid, + description: DebugConfigStrings.pyramid.selectConfiguration.description() + } + ]; + state.config = {}; + const pick = await input.showQuickPick< + DebugConfigurationQuickPickItem, + IQuickPickParameters + >({ + title: DebugConfigStrings.selectConfiguration.title(), + placeholder: DebugConfigStrings.selectConfiguration.placeholder(), + activeItem: items[0], + items: items + }); + if (pick) { + const provider = this.providerFactory.create(pick.type); + return provider.buildConfiguration.bind(provider); + } + } +} diff --git a/src/client/debugger/extension/configuration/launch.json/completionProvider.ts b/src/client/debugger/extension/configuration/launch.json/completionProvider.ts new file mode 100644 index 000000000000..16d5c14608c8 --- /dev/null +++ b/src/client/debugger/extension/configuration/launch.json/completionProvider.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { getLocation } from 'jsonc-parser'; +import * as path from 'path'; +import { + CancellationToken, + CompletionItem, + CompletionItemKind, + CompletionItemProvider, + Position, + SnippetString, + TextDocument +} from 'vscode'; +import { IExtensionSingleActivationService } from '../../../../activation/types'; +import { ILanguageService } from '../../../../common/application/types'; +import { IDisposableRegistry } from '../../../../common/types'; +import { DebugConfigStrings } from '../../../../common/utils/localize'; + +const configurationNodeName = 'configurations'; +enum JsonLanguages { + json = 'json', + jsonWithComments = 'jsonc' +} + +@injectable() +export class LaunchJsonCompletionProvider implements CompletionItemProvider, IExtensionSingleActivationService { + constructor( + @inject(ILanguageService) private readonly languageService: ILanguageService, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry + ) {} + public async activate(): Promise { + this.disposableRegistry.push( + this.languageService.registerCompletionItemProvider({ language: JsonLanguages.json }, this) + ); + this.disposableRegistry.push( + this.languageService.registerCompletionItemProvider({ language: JsonLanguages.jsonWithComments }, this) + ); + } + public async provideCompletionItems( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise { + if (!this.canProvideCompletions(document, position)) { + return []; + } + + return [ + { + command: { + command: 'python.SelectAndInsertDebugConfiguration', + title: DebugConfigStrings.launchJsonCompletions.description(), + arguments: [document, position, token] + }, + documentation: DebugConfigStrings.launchJsonCompletions.description(), + sortText: 'AAAA', + preselect: true, + kind: CompletionItemKind.Enum, + label: DebugConfigStrings.launchJsonCompletions.label(), + insertText: new SnippetString() + } + ]; + } + public canProvideCompletions(document: TextDocument, position: Position) { + if (path.basename(document.uri.fsPath) !== 'launch.json') { + return false; + } + const location = getLocation(document.getText(), document.offsetAt(position)); + // Cursor must be inside the configurations array and not in any nested items. + // Hence path[0] = array, path[1] = array element index. + return location.path[0] === configurationNodeName && location.path.length === 2; + } +} diff --git a/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts b/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts new file mode 100644 index 000000000000..6ba5ce3a9942 --- /dev/null +++ b/src/client/debugger/extension/configuration/launch.json/interpreterPathCommand.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../../../activation/types'; +import { ICommandManager } from '../../../../common/application/types'; +import { Commands } from '../../../../common/constants'; +import { IConfigurationService, IDisposable, IDisposableRegistry } from '../../../../common/types'; + +@injectable() +export class InterpreterPathCommand implements IExtensionSingleActivationService { + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IDisposableRegistry) private readonly disposables: IDisposable[] + ) {} + + public async activate() { + this.disposables.push( + this.commandManager.registerCommand(Commands.GetSelectedInterpreterPath, (args) => { + return this._getSelectedInterpreterPath(args); + }) + ); + } + + public _getSelectedInterpreterPath(args: { workspaceFolder: string } | string[]): string { + // If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder + // If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder + const workspaceFolder = 'workspaceFolder' in args ? args.workspaceFolder : args[1] ? args[1] : undefined; + return this.configurationService.getSettings(workspaceFolder ? Uri.parse(workspaceFolder) : undefined) + .pythonPath; + } +} diff --git a/src/client/debugger/extension/configuration/launch.json/updaterService.ts b/src/client/debugger/extension/configuration/launch.json/updaterService.ts new file mode 100644 index 000000000000..199cd0ecffa1 --- /dev/null +++ b/src/client/debugger/extension/configuration/launch.json/updaterService.ts @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { createScanner, parse, SyntaxKind } from 'jsonc-parser'; +import { CancellationToken, DebugConfiguration, Position, Range, TextDocument, WorkspaceEdit } from 'vscode'; +import { IExtensionSingleActivationService } from '../../../../activation/types'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; +import { IDisposableRegistry } from '../../../../common/types'; +import { noop } from '../../../../common/utils/misc'; +import { captureTelemetry } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { IDebugConfigurationService } from '../../types'; + +type PositionOfCursor = 'InsideEmptyArray' | 'BeforeItem' | 'AfterItem'; +type PositionOfComma = 'BeforeCursor'; + +export class LaunchJsonUpdaterServiceHelper { + constructor( + private readonly commandManager: ICommandManager, + private readonly workspace: IWorkspaceService, + private readonly documentManager: IDocumentManager, + private readonly configurationProvider: IDebugConfigurationService + ) {} + @captureTelemetry(EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON) + public async selectAndInsertDebugConfig( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise { + if (this.documentManager.activeTextEditor && this.documentManager.activeTextEditor.document === document) { + const folder = this.workspace.getWorkspaceFolder(document.uri); + const configs = await this.configurationProvider.provideDebugConfigurations!(folder, token); + + if (!token.isCancellationRequested && Array.isArray(configs) && configs.length > 0) { + // Always use the first available debug configuration. + await this.insertDebugConfiguration(document, position, configs[0]); + } + } + } + /** + * Inserts the debug configuration into the document. + * Invokes the document formatter to ensure JSON is formatted nicely. + * @param {TextDocument} document + * @param {Position} position + * @param {DebugConfiguration} config + * @returns {Promise} + * @memberof LaunchJsonCompletionItemProvider + */ + public async insertDebugConfiguration( + document: TextDocument, + position: Position, + config: DebugConfiguration + ): Promise { + const cursorPosition = this.getCursorPositionInConfigurationsArray(document, position); + if (!cursorPosition) { + return; + } + const commaPosition = this.isCommaImmediatelyBeforeCursor(document, position) ? 'BeforeCursor' : undefined; + const formattedJson = this.getTextForInsertion(config, cursorPosition, commaPosition); + const workspaceEdit = new WorkspaceEdit(); + workspaceEdit.insert(document.uri, position, formattedJson); + await this.documentManager.applyEdit(workspaceEdit); + this.commandManager.executeCommand('editor.action.formatDocument').then(noop, noop); + } + /** + * Gets the string representation of the debug config for insertion in the document. + * Adds necessary leading or trailing commas (remember the text is added into an array). + * @param {DebugConfiguration} config + * @param {PositionOfCursor} cursorPosition + * @param {PositionOfComma} [commaPosition] + * @returns + * @memberof LaunchJsonCompletionItemProvider + */ + public getTextForInsertion( + config: DebugConfiguration, + cursorPosition: PositionOfCursor, + commaPosition?: PositionOfComma + ) { + const json = JSON.stringify(config); + if (cursorPosition === 'AfterItem') { + // If we already have a comma immediatley before the cursor, then no need of adding a comma. + return commaPosition === 'BeforeCursor' ? json : `,${json}`; + } + if (cursorPosition === 'BeforeItem') { + return `${json},`; + } + return json; + } + public getCursorPositionInConfigurationsArray( + document: TextDocument, + position: Position + ): PositionOfCursor | undefined { + if (this.isConfigurationArrayEmpty(document)) { + return 'InsideEmptyArray'; + } + const scanner = createScanner(document.getText(), true); + scanner.setPosition(document.offsetAt(position)); + const nextToken = scanner.scan(); + if (nextToken === SyntaxKind.CommaToken || nextToken === SyntaxKind.CloseBracketToken) { + return 'AfterItem'; + } + if (nextToken === SyntaxKind.OpenBraceToken) { + return 'BeforeItem'; + } + } + public isConfigurationArrayEmpty(document: TextDocument): boolean { + const configuration = parse(document.getText(), [], { allowTrailingComma: true, disallowComments: false }) as { + configurations: []; + }; + return ( + !configuration || !Array.isArray(configuration.configurations) || configuration.configurations.length === 0 + ); + } + public isCommaImmediatelyBeforeCursor(document: TextDocument, position: Position) { + const line = document.lineAt(position.line); + // Get text from start of line until the cursor. + const currentLine = document.getText(new Range(line.range.start, position)); + if (currentLine.trim().endsWith(',')) { + return true; + } + // If there are other characters, then don't bother. + if (currentLine.trim().length !== 0) { + return false; + } + + // Keep walking backwards until we hit a non-comma character or a comm character. + let startLineNumber = position.line - 1; + while (startLineNumber > 0) { + const lineText = document.lineAt(startLineNumber).text; + if (lineText.trim().endsWith(',')) { + return true; + } + // If there are other characters, then don't bother. + if (lineText.trim().length !== 0) { + return false; + } + startLineNumber -= 1; + continue; + } + return false; + } +} + +@injectable() +export class LaunchJsonUpdaterService implements IExtensionSingleActivationService { + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IDocumentManager) private readonly documentManager: IDocumentManager, + @inject(IDebugConfigurationService) private readonly configurationProvider: IDebugConfigurationService + ) {} + public async activate(): Promise { + const handler = new LaunchJsonUpdaterServiceHelper( + this.commandManager, + this.workspace, + this.documentManager, + this.configurationProvider + ); + this.disposableRegistry.push( + this.commandManager.registerCommand( + 'python.SelectAndInsertDebugConfiguration', + handler.selectAndInsertDebugConfig, + handler + ) + ); + } +} diff --git a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts new file mode 100644 index 000000000000..78839f495750 --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../common/application/types'; +import { IFileSystem } from '../../../../common/platform/types'; +import { IPathUtils } from '../../../../common/types'; +import { DebugConfigStrings } from '../../../../common/utils/localize'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { SystemVariables } from '../../../../common/variables/systemVariables'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { DebuggerTypeName } from '../../../constants'; +import { LaunchRequestArguments } from '../../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +// tslint:disable-next-line:no-invalid-template-strings +const workspaceFolderToken = '${workspaceFolder}'; + +@injectable() +export class DjangoLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { + constructor( + @inject(IFileSystem) private fs: IFileSystem, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IPathUtils) private pathUtils: IPathUtils + ) {} + public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { + const program = await this.getManagePyPath(state.folder); + let manuallyEnteredAValue: boolean | undefined; + const defaultProgram = `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; + const config: Partial = { + name: DebugConfigStrings.django.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + program: program || defaultProgram, + args: ['runserver'], + django: true + }; + if (!program) { + const selectedProgram = await input.showInputBox({ + title: DebugConfigStrings.django.enterManagePyPath.title(), + value: defaultProgram, + prompt: DebugConfigStrings.django.enterManagePyPath.prompt(), + validate: (value) => this.validateManagePy(state.folder, defaultProgram, value) + }); + if (selectedProgram) { + manuallyEnteredAValue = true; + config.program = selectedProgram; + } + } + + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.launchDjango, + autoDetectedDjangoManagePyPath: !!program, + manuallyEnteredAValue + }); + Object.assign(state.config, config); + } + public async validateManagePy( + folder: WorkspaceFolder | undefined, + defaultValue: string, + selected?: string + ): Promise { + const error = DebugConfigStrings.django.enterManagePyPath.invalid(); + if (!selected || selected.trim().length === 0) { + return error; + } + const resolvedPath = this.resolveVariables(selected, folder ? folder.uri : undefined); + if (selected !== defaultValue && !(await this.fs.fileExists(resolvedPath))) { + return error; + } + if (!resolvedPath.trim().toLowerCase().endsWith('.py')) { + return error; + } + return; + } + protected resolveVariables(pythonPath: string, resource: Uri | undefined): string { + const systemVariables = new SystemVariables(resource, undefined, this.workspace); + return systemVariables.resolveAny(pythonPath); + } + + protected async getManagePyPath(folder: WorkspaceFolder | undefined): Promise { + if (!folder) { + return; + } + const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'manage.py'); + if (await this.fs.fileExists(defaultLocationOfManagePy)) { + return `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; + } + } +} diff --git a/src/client/debugger/extension/configuration/providers/fileLaunch.ts b/src/client/debugger/extension/configuration/providers/fileLaunch.ts new file mode 100644 index 000000000000..1762709e4554 --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/fileLaunch.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { DebugConfigStrings } from '../../../../common/utils/localize'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { captureTelemetry } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { DebuggerTypeName } from '../../../constants'; +import { LaunchRequestArguments } from '../../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +@injectable() +export class FileLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { + @captureTelemetry( + EventName.DEBUGGER_CONFIGURATION_PROMPTS, + { configurationType: DebugConfigurationType.launchFile }, + false + ) + public async buildConfiguration(_input: MultiStepInput, state: DebugConfigurationState) { + const config: Partial = { + name: DebugConfigStrings.file.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + // tslint:disable-next-line:no-invalid-template-strings + program: '${file}', + console: 'integratedTerminal' + }; + Object.assign(state.config, config); + } +} diff --git a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts new file mode 100644 index 000000000000..7ac3ad6455ff --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; +import { IFileSystem } from '../../../../common/platform/types'; +import { DebugConfigStrings } from '../../../../common/utils/localize'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { DebuggerTypeName } from '../../../constants'; +import { LaunchRequestArguments } from '../../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +@injectable() +export class FlaskLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { + constructor(@inject(IFileSystem) private fs: IFileSystem) {} + public isSupported(debugConfigurationType: DebugConfigurationType): boolean { + return debugConfigurationType === DebugConfigurationType.launchFlask; + } + public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { + const application = await this.getApplicationPath(state.folder); + let manuallyEnteredAValue: boolean | undefined; + const config: Partial = { + name: DebugConfigStrings.flask.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + module: 'flask', + env: { + FLASK_APP: application || 'app.py', + FLASK_ENV: 'development', + FLASK_DEBUG: '0' + }, + args: ['run', '--no-debugger'], + jinja: true + }; + + if (!application) { + const selectedApp = await input.showInputBox({ + title: DebugConfigStrings.flask.enterAppPathOrNamePath.title(), + value: 'app.py', + prompt: DebugConfigStrings.flask.enterAppPathOrNamePath.prompt(), + validate: (value) => + Promise.resolve( + value && value.trim().length > 0 + ? undefined + : DebugConfigStrings.flask.enterAppPathOrNamePath.invalid() + ) + }); + if (selectedApp) { + manuallyEnteredAValue = true; + config.env!.FLASK_APP = selectedApp; + } + } + + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.launchFlask, + autoDetectedFlaskAppPyPath: !!application, + manuallyEnteredAValue + }); + Object.assign(state.config, config); + } + protected async getApplicationPath(folder: WorkspaceFolder | undefined): Promise { + if (!folder) { + return; + } + const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'app.py'); + if (await this.fs.fileExists(defaultLocationOfManagePy)) { + return 'app.py'; + } + } +} diff --git a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts new file mode 100644 index 000000000000..e2d6db959d7d --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { DebugConfigStrings } from '../../../../common/utils/localize'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { DebuggerTypeName } from '../../../constants'; +import { LaunchRequestArguments } from '../../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +@injectable() +export class ModuleLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { + public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { + let manuallyEnteredAValue: boolean | undefined; + const config: Partial = { + name: DebugConfigStrings.module.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + module: DebugConfigStrings.module.snippet.default() + }; + const selectedModule = await input.showInputBox({ + title: DebugConfigStrings.module.enterModule.title(), + value: config.module || DebugConfigStrings.module.enterModule.default(), + prompt: DebugConfigStrings.module.enterModule.prompt(), + validate: (value) => + Promise.resolve( + value && value.trim().length > 0 ? undefined : DebugConfigStrings.module.enterModule.invalid() + ) + }); + if (selectedModule) { + manuallyEnteredAValue = true; + config.module = selectedModule; + } + + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.launchModule, + manuallyEnteredAValue + }); + Object.assign(state.config, config); + } +} diff --git a/src/client/debugger/extension/configuration/providers/pidAttach.ts b/src/client/debugger/extension/configuration/providers/pidAttach.ts new file mode 100644 index 000000000000..fe95e82b654c --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/pidAttach.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { DebugConfigStrings } from '../../../../common/utils/localize'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { captureTelemetry } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { DebuggerTypeName } from '../../../constants'; +import { AttachRequestArguments } from '../../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +@injectable() +export class PidAttachDebugConfigurationProvider implements IDebugConfigurationProvider { + @captureTelemetry( + EventName.DEBUGGER_CONFIGURATION_PROMPTS, + { configurationType: DebugConfigurationType.pidAttach }, + false + ) + public async buildConfiguration(_input: MultiStepInput, state: DebugConfigurationState) { + const config: Partial = { + name: DebugConfigStrings.attachPid.snippet.name(), + type: DebuggerTypeName, + request: 'attach', + // tslint:disable-next-line:no-invalid-template-strings + processId: '${command:pickProcess}' + }; + Object.assign(state.config, config); + } +} diff --git a/src/client/debugger/extension/configuration/providers/providerFactory.ts b/src/client/debugger/extension/configuration/providers/providerFactory.ts new file mode 100644 index 000000000000..0504ca398fb0 --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/providerFactory.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; +import { IDebugConfigurationProviderFactory } from '../types'; + +@injectable() +export class DebugConfigurationProviderFactory implements IDebugConfigurationProviderFactory { + private readonly providers: Map; + constructor( + @inject(IDebugConfigurationProvider) + @named(DebugConfigurationType.launchFlask) + flaskProvider: IDebugConfigurationProvider, + @inject(IDebugConfigurationProvider) + @named(DebugConfigurationType.launchDjango) + djangoProvider: IDebugConfigurationProvider, + @inject(IDebugConfigurationProvider) + @named(DebugConfigurationType.launchModule) + moduleProvider: IDebugConfigurationProvider, + @inject(IDebugConfigurationProvider) + @named(DebugConfigurationType.launchFile) + fileProvider: IDebugConfigurationProvider, + @inject(IDebugConfigurationProvider) + @named(DebugConfigurationType.launchPyramid) + pyramidProvider: IDebugConfigurationProvider, + @inject(IDebugConfigurationProvider) + @named(DebugConfigurationType.remoteAttach) + remoteAttachProvider: IDebugConfigurationProvider, + @inject(IDebugConfigurationProvider) + @named(DebugConfigurationType.pidAttach) + pidAttachProvider: IDebugConfigurationProvider + ) { + this.providers = new Map(); + this.providers.set(DebugConfigurationType.launchDjango, djangoProvider); + this.providers.set(DebugConfigurationType.launchFlask, flaskProvider); + this.providers.set(DebugConfigurationType.launchFile, fileProvider); + this.providers.set(DebugConfigurationType.launchModule, moduleProvider); + this.providers.set(DebugConfigurationType.launchPyramid, pyramidProvider); + this.providers.set(DebugConfigurationType.remoteAttach, remoteAttachProvider); + this.providers.set(DebugConfigurationType.pidAttach, pidAttachProvider); + } + public create(configurationType: DebugConfigurationType): IDebugConfigurationProvider { + return this.providers.get(configurationType)!; + } +} diff --git a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts b/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts new file mode 100644 index 000000000000..9aab8b77c583 --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../common/application/types'; +import { IFileSystem } from '../../../../common/platform/types'; +import { IPathUtils } from '../../../../common/types'; +import { DebugConfigStrings } from '../../../../common/utils/localize'; +import { MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { SystemVariables } from '../../../../common/variables/systemVariables'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { DebuggerTypeName } from '../../../constants'; +import { LaunchRequestArguments } from '../../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +// tslint:disable-next-line:no-invalid-template-strings +const workspaceFolderToken = '${workspaceFolder}'; + +@injectable() +export class PyramidLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { + constructor( + @inject(IFileSystem) private fs: IFileSystem, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IPathUtils) private pathUtils: IPathUtils + ) {} + public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { + const iniPath = await this.getDevelopmentIniPath(state.folder); + const defaultIni = `${workspaceFolderToken}${this.pathUtils.separator}development.ini`; + let manuallyEnteredAValue: boolean | undefined; + + const config: Partial = { + name: DebugConfigStrings.pyramid.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + module: 'pyramid.scripts.pserve', + args: [iniPath || defaultIni], + pyramid: true, + jinja: true + }; + + if (!iniPath) { + const selectedIniPath = await input.showInputBox({ + title: DebugConfigStrings.pyramid.enterDevelopmentIniPath.title(), + value: defaultIni, + prompt: DebugConfigStrings.pyramid.enterDevelopmentIniPath.prompt(), + validate: (value) => this.validateIniPath(state ? state.folder : undefined, defaultIni, value) + }); + if (selectedIniPath) { + manuallyEnteredAValue = true; + config.args = [selectedIniPath]; + } + } + + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.launchPyramid, + autoDetectedPyramidIniPath: !!iniPath, + manuallyEnteredAValue + }); + Object.assign(state.config, config); + } + public async validateIniPath( + folder: WorkspaceFolder | undefined, + defaultValue: string, + selected?: string + ): Promise { + if (!folder) { + return; + } + const error = DebugConfigStrings.pyramid.enterDevelopmentIniPath.invalid(); + if (!selected || selected.trim().length === 0) { + return error; + } + const resolvedPath = this.resolveVariables(selected, folder.uri); + if (selected !== defaultValue && !(await this.fs.fileExists(resolvedPath))) { + return error; + } + if (!resolvedPath.trim().toLowerCase().endsWith('.ini')) { + return error; + } + } + protected resolveVariables(pythonPath: string, resource: Uri | undefined): string { + const systemVariables = new SystemVariables(resource, undefined, this.workspace); + return systemVariables.resolveAny(pythonPath); + } + + protected async getDevelopmentIniPath(folder: WorkspaceFolder | undefined): Promise { + if (!folder) { + return; + } + const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'development.ini'); + if (await this.fs.fileExists(defaultLocationOfManagePy)) { + return `${workspaceFolderToken}${this.pathUtils.separator}development.ini`; + } + } +} diff --git a/src/client/debugger/extension/configuration/providers/remoteAttach.ts b/src/client/debugger/extension/configuration/providers/remoteAttach.ts new file mode 100644 index 000000000000..51189093ebc2 --- /dev/null +++ b/src/client/debugger/extension/configuration/providers/remoteAttach.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { DebugConfigStrings } from '../../../../common/utils/localize'; +import { InputStep, MultiStepInput } from '../../../../common/utils/multiStepInput'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { DebuggerTypeName } from '../../../constants'; +import { AttachRequestArguments } from '../../../types'; +import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; + +const defaultHost = 'localhost'; +const defaultPort = 5678; + +@injectable() +export class RemoteAttachDebugConfigurationProvider implements IDebugConfigurationProvider { + public async buildConfiguration( + input: MultiStepInput, + state: DebugConfigurationState + ): Promise | void> { + const config: Partial = { + name: DebugConfigStrings.attach.snippet.name(), + type: DebuggerTypeName, + request: 'attach', + connect: { + host: defaultHost, + port: defaultPort + }, + pathMappings: [ + { + // tslint:disable-next-line:no-invalid-template-strings + localRoot: '${workspaceFolder}', + remoteRoot: '.' + } + ] + }; + + const connect = config.connect!; + connect.host = await input.showInputBox({ + title: DebugConfigStrings.attach.enterRemoteHost.title(), + step: 1, + totalSteps: 2, + value: connect.host || defaultHost, + prompt: DebugConfigStrings.attach.enterRemoteHost.prompt(), + validate: (value) => + Promise.resolve( + value && value.trim().length > 0 ? undefined : DebugConfigStrings.attach.enterRemoteHost.invalid() + ) + }); + if (!connect.host) { + connect.host = defaultHost; + } + + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.remoteAttach, + manuallyEnteredAValue: connect.host !== defaultHost + }); + Object.assign(state.config, config); + return (_) => this.configurePort(input, state.config); + } + + protected async configurePort( + input: MultiStepInput, + config: Partial + ) { + const connect = config.connect || (config.connect = {}); + const port = await input.showInputBox({ + title: DebugConfigStrings.attach.enterRemotePort.title(), + step: 2, + totalSteps: 2, + value: (connect.port || defaultPort).toString(), + prompt: DebugConfigStrings.attach.enterRemotePort.prompt(), + validate: (value) => + Promise.resolve( + value && /^\d+$/.test(value.trim()) + ? undefined + : DebugConfigStrings.attach.enterRemotePort.invalid() + ) + }); + if (port && /^\d+$/.test(port.trim())) { + connect.port = parseInt(port, 10); + } + if (!connect.port) { + connect.port = defaultPort; + } + sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { + configurationType: DebugConfigurationType.remoteAttach, + manuallyEnteredAValue: connect.port !== defaultPort + }); + } +} diff --git a/src/client/debugger/extension/configuration/resolvers/attach.ts b/src/client/debugger/extension/configuration/resolvers/attach.ts new file mode 100644 index 000000000000..bb8fdf913bed --- /dev/null +++ b/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; +import { IPlatformService } from '../../../../common/platform/types'; +import { IConfigurationService } from '../../../../common/types'; +import { AttachRequestArguments, DebugOptions, PathMapping } from '../../../types'; +import { BaseConfigurationResolver } from './base'; + +@injectable() +export class AttachConfigurationResolver extends BaseConfigurationResolver { + constructor( + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IDocumentManager) documentManager: IDocumentManager, + @inject(IPlatformService) platformService: IPlatformService, + @inject(IConfigurationService) configurationService: IConfigurationService + ) { + super(workspaceService, documentManager, platformService, configurationService); + } + public async resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: AttachRequestArguments, + _token?: CancellationToken + ): Promise { + const workspaceFolder = this.getWorkspaceFolder(folder); + + await this.provideAttachDefaults(workspaceFolder, debugConfiguration as AttachRequestArguments); + + const dbgConfig = debugConfiguration; + if (Array.isArray(dbgConfig.debugOptions)) { + dbgConfig.debugOptions = dbgConfig.debugOptions!.filter( + (item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos + ); + } + return debugConfiguration; + } + // tslint:disable-next-line:cyclomatic-complexity + protected async provideAttachDefaults( + workspaceFolder: Uri | undefined, + debugConfiguration: AttachRequestArguments + ): Promise { + if (!Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = []; + } + if (!(debugConfiguration.connect || debugConfiguration.listen) && !debugConfiguration.host) { + // Connect and listen cannot be mixed with host property. + debugConfiguration.host = 'localhost'; + } + if (debugConfiguration.justMyCode === undefined) { + // Populate justMyCode using debugStdLib + debugConfiguration.justMyCode = !debugConfiguration.debugStdLib; + } + debugConfiguration.showReturnValue = debugConfiguration.showReturnValue !== false; + // Pass workspace folder so we can get this when we get debug events firing. + debugConfiguration.workspaceFolder = workspaceFolder ? workspaceFolder.fsPath : undefined; + const debugOptions = debugConfiguration.debugOptions!; + if (!debugConfiguration.justMyCode) { + this.debugOption(debugOptions, DebugOptions.DebugStdLib); + } + if (debugConfiguration.django) { + this.debugOption(debugOptions, DebugOptions.Django); + } + if (debugConfiguration.jinja) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.subProcess === true) { + this.debugOption(debugOptions, DebugOptions.SubProcess); + } + if ( + debugConfiguration.pyramid && + debugOptions.indexOf(DebugOptions.Jinja) === -1 && + debugConfiguration.jinja !== false + ) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { + this.debugOption(debugOptions, DebugOptions.RedirectOutput); + } + + // We'll need paths to be fixed only in the case where local and remote hosts are the same + // I.e. only if hostName === 'localhost' or '127.0.0.1' or '' + const isLocalHost = this.isLocalHost(debugConfiguration.host); + if (this.platformService.isWindows && isLocalHost) { + this.debugOption(debugOptions, DebugOptions.FixFilePathCase); + } + if (this.platformService.isWindows) { + this.debugOption(debugOptions, DebugOptions.WindowsClient); + } else { + this.debugOption(debugOptions, DebugOptions.UnixClient); + } + if (debugConfiguration.showReturnValue) { + this.debugOption(debugOptions, DebugOptions.ShowReturnValue); + } + + debugConfiguration.pathMappings = this.resolvePathMappings( + debugConfiguration.pathMappings || [], + debugConfiguration.host, + debugConfiguration.localRoot, + debugConfiguration.remoteRoot, + workspaceFolder + ); + this.sendTelemetry('attach', debugConfiguration); + } + + private resolvePathMappings( + pathMappings: PathMapping[], + host?: string, + localRoot?: string, + remoteRoot?: string, + workspaceFolder?: Uri + ) { + // This is for backwards compatibility. + if (localRoot && remoteRoot) { + pathMappings.push({ + localRoot: localRoot, + remoteRoot: remoteRoot + }); + } + // If attaching to local host, then always map local root and remote roots. + if (this.isLocalHost(host)) { + pathMappings = this.fixUpPathMappings(pathMappings, workspaceFolder ? workspaceFolder.fsPath : ''); + } + return pathMappings.length > 0 ? pathMappings : undefined; + } +} diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts new file mode 100644 index 000000000000..1132928fbd99 --- /dev/null +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-template-strings no-suspicious-comment + +import { injectable } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; +import { PYTHON_LANGUAGE } from '../../../../common/constants'; +import { IPlatformService } from '../../../../common/platform/types'; +import { IConfigurationService } from '../../../../common/types'; +import { SystemVariables } from '../../../../common/variables/systemVariables'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { DebuggerTelemetry } from '../../../../telemetry/types'; +import { AttachRequestArguments, DebugOptions, LaunchRequestArguments, PathMapping } from '../../../types'; +import { PythonPathSource } from '../../types'; +import { IDebugConfigurationResolver } from '../types'; + +@injectable() +export abstract class BaseConfigurationResolver + implements IDebugConfigurationResolver { + protected pythonPathSource: PythonPathSource = PythonPathSource.launchJson; + constructor( + protected readonly workspaceService: IWorkspaceService, + protected readonly documentManager: IDocumentManager, + protected readonly platformService: IPlatformService, + protected readonly configurationService: IConfigurationService + ) {} + public abstract resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: DebugConfiguration, + token?: CancellationToken + ): Promise; + protected getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { + if (folder) { + return folder.uri; + } + const program = this.getProgram(); + if ( + !Array.isArray(this.workspaceService.workspaceFolders) || + this.workspaceService.workspaceFolders.length === 0 + ) { + return program ? Uri.file(path.dirname(program)) : undefined; + } + if (this.workspaceService.workspaceFolders.length === 1) { + return this.workspaceService.workspaceFolders[0].uri; + } + if (program) { + const workspaceFolder = this.workspaceService.getWorkspaceFolder(Uri.file(program)); + if (workspaceFolder) { + return workspaceFolder.uri; + } + } + } + protected getProgram(): string | undefined { + const editor = this.documentManager.activeTextEditor; + if (editor && editor.document.languageId === PYTHON_LANGUAGE) { + return editor.document.fileName; + } + } + protected resolveAndUpdatePaths( + workspaceFolder: Uri | undefined, + debugConfiguration: LaunchRequestArguments + ): void { + this.resolveAndUpdateEnvFilePath(workspaceFolder, debugConfiguration); + this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); + } + protected resolveAndUpdateEnvFilePath( + workspaceFolder: Uri | undefined, + debugConfiguration: LaunchRequestArguments + ): void { + if (!debugConfiguration) { + return; + } + if (debugConfiguration.envFile && (workspaceFolder || debugConfiguration.cwd)) { + const systemVariables = new SystemVariables( + undefined, + (workspaceFolder ? workspaceFolder.fsPath : undefined) || debugConfiguration.cwd + ); + debugConfiguration.envFile = systemVariables.resolveAny(debugConfiguration.envFile); + } + } + protected resolveAndUpdatePythonPath( + workspaceFolder: Uri | undefined, + debugConfiguration: LaunchRequestArguments + ): void { + if (!debugConfiguration) { + return; + } + if (debugConfiguration.pythonPath === '${command:python.interpreterPath}' || !debugConfiguration.pythonPath) { + const pythonPath = this.configurationService.getSettings(workspaceFolder).pythonPath; + debugConfiguration.pythonPath = pythonPath; + this.pythonPathSource = PythonPathSource.settingsJson; + } else { + this.pythonPathSource = PythonPathSource.launchJson; + } + } + protected debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { + if (debugOptions.indexOf(debugOption) >= 0) { + return; + } + debugOptions.push(debugOption); + } + protected isLocalHost(hostName?: string) { + const LocalHosts = ['localhost', '127.0.0.1', '::1']; + return hostName && LocalHosts.indexOf(hostName.toLowerCase()) >= 0 ? true : false; + } + protected fixUpPathMappings( + pathMappings: PathMapping[], + defaultLocalRoot?: string, + defaultRemoteRoot?: string + ): PathMapping[] { + if (!defaultLocalRoot) { + return []; + } + if (!defaultRemoteRoot) { + defaultRemoteRoot = defaultLocalRoot; + } + + if (pathMappings.length === 0) { + pathMappings = [ + { + localRoot: defaultLocalRoot, + remoteRoot: defaultRemoteRoot + } + ]; + } else { + // Expand ${workspaceFolder} variable first if necessary. + const systemVariables = new SystemVariables(undefined, defaultLocalRoot); + pathMappings = pathMappings.map(({ localRoot: mappedLocalRoot, remoteRoot }) => ({ + localRoot: systemVariables.resolveAny(mappedLocalRoot), + // TODO: Apply to remoteRoot too? + remoteRoot + })); + } + + // If on Windows, lowercase the drive letter for path mappings. + // TODO: Apply even if no localRoot? + if (this.platformService.isWindows) { + // TODO: Apply to remoteRoot too? + pathMappings = pathMappings.map(({ localRoot: windowsLocalRoot, remoteRoot }) => { + let localRoot = windowsLocalRoot; + if (windowsLocalRoot.match(/^[A-Z]:/)) { + localRoot = `${windowsLocalRoot[0].toLowerCase()}${windowsLocalRoot.substr(1)}`; + } + return { localRoot, remoteRoot }; + }); + } + + return pathMappings; + } + protected isDebuggingFlask(debugConfiguration: Partial) { + return debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK' ? true : false; + } + protected sendTelemetry( + trigger: 'launch' | 'attach' | 'test', + debugConfiguration: Partial + ) { + const name = debugConfiguration.name || ''; + const moduleName = debugConfiguration.module || ''; + const telemetryProps: DebuggerTelemetry = { + trigger, + console: debugConfiguration.console, + hasEnvVars: typeof debugConfiguration.env === 'object' && Object.keys(debugConfiguration.env).length > 0, + django: !!debugConfiguration.django, + flask: this.isDebuggingFlask(debugConfiguration), + hasArgs: Array.isArray(debugConfiguration.args) && debugConfiguration.args.length > 0, + isLocalhost: this.isLocalHost(debugConfiguration.host), + isModule: moduleName.length > 0, + isSudo: !!debugConfiguration.sudo, + jinja: !!debugConfiguration.jinja, + pyramid: !!debugConfiguration.pyramid, + stopOnEntry: !!debugConfiguration.stopOnEntry, + showReturnValue: !!debugConfiguration.showReturnValue, + subProcess: !!debugConfiguration.subProcess, + watson: name.toLowerCase().indexOf('watson') >= 0, + pyspark: name.toLowerCase().indexOf('pyspark') >= 0, + gevent: name.toLowerCase().indexOf('gevent') >= 0, + scrapy: moduleName.toLowerCase() === 'scrapy' + }; + sendTelemetryEvent(EventName.DEBUGGER, undefined, telemetryProps); + } +} diff --git a/src/client/debugger/extension/configuration/resolvers/helper.ts b/src/client/debugger/extension/configuration/resolvers/helper.ts new file mode 100644 index 000000000000..e0e31b3210c3 --- /dev/null +++ b/src/client/debugger/extension/configuration/resolvers/helper.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ICurrentProcess, IPathUtils } from '../../../../common/types'; +import { EnvironmentVariables, IEnvironmentVariablesService } from '../../../../common/variables/types'; +import { LaunchRequestArguments } from '../../../types'; + +export const IDebugEnvironmentVariablesService = Symbol('IDebugEnvironmentVariablesService'); +export interface IDebugEnvironmentVariablesService { + getEnvironmentVariables(args: LaunchRequestArguments): Promise; +} + +@injectable() +export class DebugEnvironmentVariablesHelper implements IDebugEnvironmentVariablesService { + constructor( + @inject(IEnvironmentVariablesService) private envParser: IEnvironmentVariablesService, + @inject(IPathUtils) private pathUtils: IPathUtils, + @inject(ICurrentProcess) private process: ICurrentProcess + ) {} + public async getEnvironmentVariables(args: LaunchRequestArguments): Promise { + const pathVariableName = this.pathUtils.getPathVariableName(); + + // Merge variables from both .env file and env json variables. + const debugLaunchEnvVars: Record = + // tslint:disable-next-line:no-any + args.env && Object.keys(args.env).length > 0 ? ({ ...args.env } as any) : ({} as any); + const envFileVars = await this.envParser.parseFile(args.envFile, debugLaunchEnvVars); + const env = envFileVars ? { ...envFileVars! } : {}; + this.envParser.mergeVariables(debugLaunchEnvVars, env); + + // Append the PYTHONPATH and PATH variables. + this.envParser.appendPath(env, debugLaunchEnvVars[pathVariableName]); + this.envParser.appendPythonPath(env, debugLaunchEnvVars.PYTHONPATH); + + if (typeof env[pathVariableName] === 'string' && env[pathVariableName]!.length > 0) { + // Now merge this path with the current system path. + // We need to do this to ensure the PATH variable always has the system PATHs as well. + this.envParser.appendPath(env, this.process.env[pathVariableName]!); + } + if (typeof env.PYTHONPATH === 'string' && env.PYTHONPATH.length > 0) { + // We didn't have a value for PATH earlier and now we do. + // Now merge this path with the current system path. + // We need to do this to ensure the PATH variable always has the system PATHs as well. + this.envParser.appendPythonPath(env, this.process.env.PYTHONPATH!); + } + + if (args.console === 'internalConsole') { + // For debugging, when not using any terminal, then we need to provide all env variables. + // As we're spawning the process, we need to ensure all env variables are passed. + // Including those from the current process (i.e. everything, not just custom vars). + this.envParser.mergeVariables(this.process.env, env); + + if (env[pathVariableName] === undefined && typeof this.process.env[pathVariableName] === 'string') { + env[pathVariableName] = this.process.env[pathVariableName]; + } + if (env.PYTHONPATH === undefined && typeof this.process.env.PYTHONPATH === 'string') { + env.PYTHONPATH = this.process.env.PYTHONPATH; + } + } + + if (!env.hasOwnProperty('PYTHONIOENCODING')) { + env.PYTHONIOENCODING = 'UTF-8'; + } + if (!env.hasOwnProperty('PYTHONUNBUFFERED')) { + env.PYTHONUNBUFFERED = '1'; + } + + if (args.gevent) { + env.GEVENT_SUPPORT = 'True'; // this is read in pydevd_constants.py + } + + return env; + } +} diff --git a/src/client/debugger/extension/configuration/resolvers/launch.ts b/src/client/debugger/extension/configuration/resolvers/launch.ts new file mode 100644 index 000000000000..9d78188207df --- /dev/null +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; +import { InvalidPythonPathInDebuggerServiceId } from '../../../../application/diagnostics/checks/invalidPythonPathInDebugger'; +import { IDiagnosticsService, IInvalidPythonPathInDebuggerService } from '../../../../application/diagnostics/types'; +import { IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; +import { IPlatformService } from '../../../../common/platform/types'; +import { IConfigurationService } from '../../../../common/types'; +import { DebuggerTypeName } from '../../../constants'; +import { DebugOptions, LaunchRequestArguments } from '../../../types'; +import { BaseConfigurationResolver } from './base'; +import { IDebugEnvironmentVariablesService } from './helper'; + +@injectable() +export class LaunchConfigurationResolver extends BaseConfigurationResolver { + constructor( + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IDocumentManager) documentManager: IDocumentManager, + @inject(IDiagnosticsService) + @named(InvalidPythonPathInDebuggerServiceId) + private readonly invalidPythonPathInDebuggerService: IInvalidPythonPathInDebuggerService, + @inject(IPlatformService) platformService: IPlatformService, + @inject(IConfigurationService) configurationService: IConfigurationService, + @inject(IDebugEnvironmentVariablesService) private readonly debugEnvHelper: IDebugEnvironmentVariablesService + ) { + super(workspaceService, documentManager, platformService, configurationService); + } + public async resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: LaunchRequestArguments, + _token?: CancellationToken + ): Promise { + const workspaceFolder = this.getWorkspaceFolder(folder); + + const config = debugConfiguration as LaunchRequestArguments; + const numberOfSettings = Object.keys(config); + + if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { + const defaultProgram = this.getProgram(); + + config.name = 'Launch'; + config.type = DebuggerTypeName; + config.request = 'launch'; + config.program = defaultProgram ? defaultProgram : ''; + config.env = {}; + } + + await this.provideLaunchDefaults(workspaceFolder, config); + + const isValid = await this.validateLaunchConfiguration(folder, config); + if (!isValid) { + return; + } + + const dbgConfig = debugConfiguration; + if (Array.isArray(dbgConfig.debugOptions)) { + dbgConfig.debugOptions = dbgConfig.debugOptions!.filter( + (item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos + ); + } + return debugConfiguration; + } + // tslint:disable-next-line:cyclomatic-complexity + protected async provideLaunchDefaults( + workspaceFolder: Uri | undefined, + debugConfiguration: LaunchRequestArguments + ): Promise { + this.resolveAndUpdatePaths(workspaceFolder, debugConfiguration); + if (typeof debugConfiguration.cwd !== 'string' && workspaceFolder) { + debugConfiguration.cwd = workspaceFolder.fsPath; + } + if (typeof debugConfiguration.envFile !== 'string' && workspaceFolder) { + const settings = this.configurationService.getSettings(workspaceFolder); + debugConfiguration.envFile = settings.envFile; + } + // Extract environment variables from .env file in the vscode context and + // set the "env" debug configuration argument. This expansion should be + // done here before handing of the environment settings to the debug adapter + debugConfiguration.env = await this.debugEnvHelper.getEnvironmentVariables(debugConfiguration); + if (typeof debugConfiguration.stopOnEntry !== 'boolean') { + debugConfiguration.stopOnEntry = false; + } + debugConfiguration.showReturnValue = debugConfiguration.showReturnValue !== false; + if (!debugConfiguration.console) { + debugConfiguration.console = 'integratedTerminal'; + } + // If using a terminal, then never open internal console. + if (debugConfiguration.console !== 'internalConsole' && !debugConfiguration.internalConsoleOptions) { + debugConfiguration.internalConsoleOptions = 'neverOpen'; + } + if (!Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = []; + } + if (debugConfiguration.justMyCode === undefined) { + // Populate justMyCode using debugStdLib + debugConfiguration.justMyCode = !debugConfiguration.debugStdLib; + } + // Pass workspace folder so we can get this when we get debug events firing. + debugConfiguration.workspaceFolder = workspaceFolder ? workspaceFolder.fsPath : undefined; + const debugOptions = debugConfiguration.debugOptions!; + if (!debugConfiguration.justMyCode) { + this.debugOption(debugOptions, DebugOptions.DebugStdLib); + } + if (debugConfiguration.stopOnEntry) { + this.debugOption(debugOptions, DebugOptions.StopOnEntry); + } + if (debugConfiguration.showReturnValue) { + this.debugOption(debugOptions, DebugOptions.ShowReturnValue); + } + if (debugConfiguration.django) { + this.debugOption(debugOptions, DebugOptions.Django); + } + if (debugConfiguration.jinja) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.redirectOutput === undefined && debugConfiguration.console === 'internalConsole') { + debugConfiguration.redirectOutput = true; + } + if (debugConfiguration.redirectOutput) { + this.debugOption(debugOptions, DebugOptions.RedirectOutput); + } + if (debugConfiguration.sudo) { + this.debugOption(debugOptions, DebugOptions.Sudo); + } + if (debugConfiguration.subProcess === true) { + this.debugOption(debugOptions, DebugOptions.SubProcess); + } + if (this.platformService.isWindows) { + this.debugOption(debugOptions, DebugOptions.FixFilePathCase); + } + const isFlask = this.isDebuggingFlask(debugConfiguration); + if ( + (debugConfiguration.pyramid || isFlask) && + debugOptions.indexOf(DebugOptions.Jinja) === -1 && + debugConfiguration.jinja !== false + ) { + this.debugOption(debugOptions, DebugOptions.Jinja); + } + // Unlike with attach, we do not set a default path mapping. + // (See: https://github.com/microsoft/vscode-python/issues/3568) + if (debugConfiguration.pathMappings) { + let pathMappings = debugConfiguration.pathMappings; + if (pathMappings.length > 0) { + pathMappings = this.fixUpPathMappings( + pathMappings || [], + workspaceFolder ? workspaceFolder.fsPath : '' + ); + } + debugConfiguration.pathMappings = pathMappings.length > 0 ? pathMappings : undefined; + } + this.sendTelemetry(debugConfiguration.request as 'launch' | 'test', debugConfiguration); + } + + protected async validateLaunchConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: LaunchRequestArguments + ): Promise { + const diagnosticService = this.invalidPythonPathInDebuggerService; + return diagnosticService.validatePythonPath( + debugConfiguration.pythonPath, + this.pythonPathSource, + folder ? folder.uri : undefined + ); + } +} diff --git a/src/client/debugger/extension/configuration/types.ts b/src/client/debugger/extension/configuration/types.ts new file mode 100644 index 000000000000..1331ea39551a --- /dev/null +++ b/src/client/debugger/extension/configuration/types.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; +import { DebugConfigurationType, IDebugConfigurationProvider } from '../types'; + +export const IDebugConfigurationResolver = Symbol('IDebugConfigurationResolver'); +export interface IDebugConfigurationResolver { + resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: T, + token?: CancellationToken + ): Promise; +} + +export const IDebugConfigurationProviderFactory = Symbol('IDebugConfigurationProviderFactory'); +export interface IDebugConfigurationProviderFactory { + create(configurationType: DebugConfigurationType): IDebugConfigurationProvider; +} diff --git a/src/client/debugger/extension/helpers/protocolParser.ts b/src/client/debugger/extension/helpers/protocolParser.ts new file mode 100644 index 000000000000..86a8374d51a5 --- /dev/null +++ b/src/client/debugger/extension/helpers/protocolParser.ts @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-constant-condition no-typeof-undefined + +import { EventEmitter } from 'events'; +import { injectable } from 'inversify'; +import { Readable } from 'stream'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { IProtocolParser } from '../types'; + +const PROTOCOL_START_INDENTIFIER = '\r\n\r\n'; + +// tslint:disable-next-line: no-any +type Listener = (...args: any[]) => void; + +/** + * Parsers the debugger Protocol messages and raises the following events: + * 1. 'data', message (for all protocol messages) + * 1. 'event_', message (for all protocol events) + * 1. 'request_', message (for all protocol requests) + * 1. 'response_', message (for all protocol responses) + * 1. '', message (for all protocol messages that are not events, requests nor responses) + * @export + * @class ProtocolParser + * @extends {EventEmitter} + * @implements {IProtocolParser} + */ +@injectable() +export class ProtocolParser implements IProtocolParser { + private rawData = new Buffer(0); + private contentLength: number = -1; + private disposed: boolean = false; + private stream?: Readable; + private events: EventEmitter; + constructor() { + this.events = new EventEmitter(); + } + public dispose() { + if (this.stream) { + this.stream.removeListener('data', this.dataCallbackHandler); + this.stream = undefined; + } + } + public connect(stream: Readable) { + this.stream = stream; + stream.addListener('data', this.dataCallbackHandler); + } + public on(event: string | symbol, listener: Listener): this { + this.events.on(event, listener); + return this; + } + public once(event: string | symbol, listener: Listener): this { + this.events.once(event, listener); + return this; + } + private dataCallbackHandler = (data: string | Buffer) => { + this.handleData(data as Buffer); + }; + private dispatch(body: string): void { + const message = JSON.parse(body) as DebugProtocol.ProtocolMessage; + + switch (message.type) { + case 'event': { + const event = message as DebugProtocol.Event; + if (typeof event.event === 'string') { + this.events.emit(`${message.type}_${event.event}`, event); + } + break; + } + case 'request': { + const request = message as DebugProtocol.Request; + if (typeof request.command === 'string') { + this.events.emit(`${message.type}_${request.command}`, request); + } + break; + } + case 'response': { + const reponse = message as DebugProtocol.Response; + if (typeof reponse.command === 'string') { + this.events.emit(`${message.type}_${reponse.command}`, reponse); + } + break; + } + default: { + this.events.emit(`${message.type}`, message); + } + } + + this.events.emit('data', message); + } + private handleData(data: Buffer): void { + if (this.disposed) { + return; + } + this.rawData = Buffer.concat([this.rawData, data]); + + while (true) { + if (this.contentLength >= 0) { + if (this.rawData.length >= this.contentLength) { + const message = this.rawData.toString('utf8', 0, this.contentLength); + this.rawData = this.rawData.slice(this.contentLength); + this.contentLength = -1; + if (message.length > 0) { + this.dispatch(message); + } + // there may be more complete messages to process. + continue; + } + } else { + const idx = this.rawData.indexOf(PROTOCOL_START_INDENTIFIER); + if (idx !== -1) { + const header = this.rawData.toString('utf8', 0, idx); + const lines = header.split('\r\n'); + for (const line of lines) { + const pair = line.split(/: +/); + if (pair[0] === 'Content-Length') { + this.contentLength = +pair[1]; + } + } + this.rawData = this.rawData.slice(idx + PROTOCOL_START_INDENTIFIER.length); + continue; + } + } + break; + } + } +} diff --git a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts new file mode 100644 index 000000000000..9a3a7bda4935 --- /dev/null +++ b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { DebugConfiguration, DebugSessionCustomEvent } from 'vscode'; +import { swallowExceptions } from '../../../common/utils/decorators'; +import { AttachRequestArguments } from '../../types'; +import { DebuggerEvents } from './constants'; +import { IChildProcessAttachService, IDebugSessionEventHandlers } from './types'; + +/** + * This class is responsible for automatically attaching the debugger to any + * child processes launched. I.e. this is the class responsible for multi-proc debugging. + * @export + * @class ChildProcessAttachEventHandler + * @implements {IDebugSessionEventHandlers} + */ +@injectable() +export class ChildProcessAttachEventHandler implements IDebugSessionEventHandlers { + constructor( + @inject(IChildProcessAttachService) private readonly childProcessAttachService: IChildProcessAttachService + ) {} + + @swallowExceptions('Handle child process launch') + public async handleCustomEvent(event: DebugSessionCustomEvent): Promise { + if (!event) { + return; + } + + let data: AttachRequestArguments & DebugConfiguration; + if ( + event.event === DebuggerEvents.PtvsdAttachToSubprocess || + event.event === DebuggerEvents.DebugpyAttachToSubprocess + ) { + data = event.body! as AttachRequestArguments & DebugConfiguration; + } else { + return; + } + + if (Object.keys(data).length > 0) { + await this.childProcessAttachService.attach(data, event.session); + } + } +} diff --git a/src/client/debugger/extension/hooks/childProcessAttachService.ts b/src/client/debugger/extension/hooks/childProcessAttachService.ts new file mode 100644 index 000000000000..c7771b845340 --- /dev/null +++ b/src/client/debugger/extension/hooks/childProcessAttachService.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; +import { IApplicationShell, IDebugService, IWorkspaceService } from '../../../common/application/types'; +import { noop } from '../../../common/utils/misc'; +import { captureTelemetry } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { AttachRequestArguments } from '../../types'; +import { IChildProcessAttachService } from './types'; + +/** + * This class is responsible for attaching the debugger to any + * child processes launched. I.e. this is the class responsible for multi-proc debugging. + * @export + * @class ChildProcessAttachEventHandler + * @implements {IChildProcessAttachService} + */ +@injectable() +export class ChildProcessAttachService implements IChildProcessAttachService { + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IDebugService) private readonly debugService: IDebugService, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService + ) {} + + @captureTelemetry(EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS) + public async attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise { + const debugConfig: AttachRequestArguments & DebugConfiguration = data; + const processId = debugConfig.subProcessId!; + const folder = this.getRelatedWorkspaceFolder(debugConfig); + const launched = await this.debugService.startDebugging(folder, debugConfig, parentSession); + if (!launched) { + this.appShell.showErrorMessage(`Failed to launch debugger for child process ${processId}`).then(noop, noop); + } + } + + private getRelatedWorkspaceFolder( + config: AttachRequestArguments & DebugConfiguration + ): WorkspaceFolder | undefined { + const workspaceFolder = config.workspaceFolder; + if (!this.workspaceService.hasWorkspaceFolders || !workspaceFolder) { + return; + } + return this.workspaceService.workspaceFolders!.find((ws) => ws.uri.fsPath === workspaceFolder); + } +} diff --git a/src/client/debugger/extension/hooks/constants.ts b/src/client/debugger/extension/hooks/constants.ts new file mode 100644 index 000000000000..b2a2e0a8ec52 --- /dev/null +++ b/src/client/debugger/extension/hooks/constants.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export enum DebuggerEvents { + // Event sent by PTVSD when a child process is launched and ready to be attached to for multi-proc debugging. + PtvsdAttachToSubprocess = 'ptvsd_attach', + DebugpyAttachToSubprocess = 'debugpyAttach' +} diff --git a/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts b/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts new file mode 100644 index 000000000000..3aac7df6663f --- /dev/null +++ b/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, multiInject } from 'inversify'; +import { IDebugService } from '../../../common/application/types'; +import { IDisposableRegistry } from '../../../common/types'; +import { IDebugSessionEventHandlers } from './types'; + +export class DebugSessionEventDispatcher { + constructor( + @multiInject(IDebugSessionEventHandlers) private readonly eventHandlers: IDebugSessionEventHandlers[], + @inject(IDebugService) private readonly debugService: IDebugService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry + ) {} + public registerEventHandlers() { + this.disposables.push( + this.debugService.onDidReceiveDebugSessionCustomEvent((e) => { + this.eventHandlers.forEach((handler) => + handler.handleCustomEvent ? handler.handleCustomEvent(e).ignoreErrors() : undefined + ); + }) + ); + this.disposables.push( + this.debugService.onDidTerminateDebugSession((e) => { + this.eventHandlers.forEach((handler) => + handler.handleTerminateEvent ? handler.handleTerminateEvent(e).ignoreErrors() : undefined + ); + }) + ); + } +} diff --git a/src/client/debugger/extension/hooks/types.ts b/src/client/debugger/extension/hooks/types.ts new file mode 100644 index 000000000000..80d393057fb4 --- /dev/null +++ b/src/client/debugger/extension/hooks/types.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { DebugConfiguration, DebugSession, DebugSessionCustomEvent } from 'vscode'; +import { AttachRequestArguments } from '../../types'; + +export const IDebugSessionEventHandlers = Symbol('IDebugSessionEventHandlers'); +export interface IDebugSessionEventHandlers { + handleCustomEvent?(e: DebugSessionCustomEvent): Promise; + handleTerminateEvent?(e: DebugSession): Promise; +} + +export const IChildProcessAttachService = Symbol('IChildProcessAttachService'); +export interface IChildProcessAttachService { + attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise; +} diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts new file mode 100644 index 000000000000..4e785a82f0cd --- /dev/null +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IServiceManager } from '../../ioc/types'; +import { AttachRequestArguments, LaunchRequestArguments } from '../types'; +import { DebugAdapterActivator } from './adapter/activator'; +import { DebugAdapterDescriptorFactory } from './adapter/factory'; +import { DebugSessionLoggingFactory } from './adapter/logging'; +import { OutdatedDebuggerPromptFactory } from './adapter/outdatedDebuggerPrompt'; +import { AttachProcessProviderFactory } from './attachQuickPick/factory'; +import { IAttachProcessProviderFactory } from './attachQuickPick/types'; +import { DebuggerBanner } from './banner'; +import { PythonDebugConfigurationService } from './configuration/debugConfigurationService'; +import { LaunchJsonCompletionProvider } from './configuration/launch.json/completionProvider'; +import { InterpreterPathCommand } from './configuration/launch.json/interpreterPathCommand'; +import { LaunchJsonUpdaterService } from './configuration/launch.json/updaterService'; +import { DjangoLaunchDebugConfigurationProvider } from './configuration/providers/djangoLaunch'; +import { FileLaunchDebugConfigurationProvider } from './configuration/providers/fileLaunch'; +import { FlaskLaunchDebugConfigurationProvider } from './configuration/providers/flaskLaunch'; +import { ModuleLaunchDebugConfigurationProvider } from './configuration/providers/moduleLaunch'; +import { PidAttachDebugConfigurationProvider } from './configuration/providers/pidAttach'; +import { DebugConfigurationProviderFactory } from './configuration/providers/providerFactory'; +import { PyramidLaunchDebugConfigurationProvider } from './configuration/providers/pyramidLaunch'; +import { RemoteAttachDebugConfigurationProvider } from './configuration/providers/remoteAttach'; +import { AttachConfigurationResolver } from './configuration/resolvers/attach'; +import { DebugEnvironmentVariablesHelper, IDebugEnvironmentVariablesService } from './configuration/resolvers/helper'; +import { LaunchConfigurationResolver } from './configuration/resolvers/launch'; +import { IDebugConfigurationProviderFactory, IDebugConfigurationResolver } from './configuration/types'; +import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler'; +import { ChildProcessAttachService } from './hooks/childProcessAttachService'; +import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types'; +import { + DebugConfigurationType, + IDebugAdapterDescriptorFactory, + IDebugConfigurationProvider, + IDebugConfigurationService, + IDebuggerBanner, + IDebugSessionLoggingFactory, + IOutdatedDebuggerPromptFactory +} from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton( + IExtensionSingleActivationService, + LaunchJsonCompletionProvider + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + InterpreterPathCommand + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + LaunchJsonUpdaterService + ); + serviceManager.addSingleton( + IDebugConfigurationService, + PythonDebugConfigurationService + ); + serviceManager.addSingleton(IDebuggerBanner, DebuggerBanner); + serviceManager.addSingleton(IChildProcessAttachService, ChildProcessAttachService); + serviceManager.addSingleton(IDebugSessionEventHandlers, ChildProcessAttachEventHandler); + serviceManager.addSingleton>( + IDebugConfigurationResolver, + LaunchConfigurationResolver, + 'launch' + ); + serviceManager.addSingleton>( + IDebugConfigurationResolver, + AttachConfigurationResolver, + 'attach' + ); + serviceManager.addSingleton( + IDebugConfigurationProviderFactory, + DebugConfigurationProviderFactory + ); + serviceManager.addSingleton( + IDebugConfigurationProvider, + FileLaunchDebugConfigurationProvider, + DebugConfigurationType.launchFile + ); + serviceManager.addSingleton( + IDebugConfigurationProvider, + DjangoLaunchDebugConfigurationProvider, + DebugConfigurationType.launchDjango + ); + serviceManager.addSingleton( + IDebugConfigurationProvider, + FlaskLaunchDebugConfigurationProvider, + DebugConfigurationType.launchFlask + ); + serviceManager.addSingleton( + IDebugConfigurationProvider, + RemoteAttachDebugConfigurationProvider, + DebugConfigurationType.remoteAttach + ); + serviceManager.addSingleton( + IDebugConfigurationProvider, + ModuleLaunchDebugConfigurationProvider, + DebugConfigurationType.launchModule + ); + serviceManager.addSingleton( + IDebugConfigurationProvider, + PyramidLaunchDebugConfigurationProvider, + DebugConfigurationType.launchPyramid + ); + serviceManager.addSingleton( + IDebugConfigurationProvider, + PidAttachDebugConfigurationProvider, + DebugConfigurationType.pidAttach + ); + serviceManager.addSingleton( + IDebugEnvironmentVariablesService, + DebugEnvironmentVariablesHelper + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugAdapterActivator + ); + serviceManager.addSingleton( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory + ); + serviceManager.addSingleton(IDebugSessionLoggingFactory, DebugSessionLoggingFactory); + serviceManager.addSingleton( + IOutdatedDebuggerPromptFactory, + OutdatedDebuggerPromptFactory + ); + serviceManager.addSingleton( + IAttachProcessProviderFactory, + AttachProcessProviderFactory + ); +} diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts new file mode 100644 index 000000000000..b5e6826b3b7e --- /dev/null +++ b/src/client/debugger/extension/types.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Readable } from 'stream'; +import { + CancellationToken, + DebugAdapterDescriptorFactory, + DebugAdapterTrackerFactory, + DebugConfigurationProvider, + Disposable, + WorkspaceFolder +} from 'vscode'; + +import { InputStep, MultiStepInput } from '../../common/utils/multiStepInput'; +import { DebugConfigurationArguments } from '../types'; + +export const IDebugConfigurationService = Symbol('IDebugConfigurationService'); +export interface IDebugConfigurationService extends DebugConfigurationProvider {} +export const IDebuggerBanner = Symbol('IDebuggerBanner'); +export interface IDebuggerBanner { + initialize(): void; +} + +export const IDebugConfigurationProvider = Symbol('IDebugConfigurationProvider'); +export type DebugConfigurationState = { + config: Partial; + folder?: WorkspaceFolder; + token?: CancellationToken; +}; +export interface IDebugConfigurationProvider { + buildConfiguration( + input: MultiStepInput, + state: DebugConfigurationState + ): Promise | void>; +} + +export enum DebugConfigurationType { + launchFile = 'launchFile', + remoteAttach = 'remoteAttach', + launchDjango = 'launchDjango', + launchFlask = 'launchFlask', + launchModule = 'launchModule', + launchPyramid = 'launchPyramid', + pidAttach = 'pidAttach' +} + +export enum PythonPathSource { + launchJson = 'launch.json', + settingsJson = 'settings.json' +} + +export const IDebugAdapterDescriptorFactory = Symbol('IDebugAdapterDescriptorFactory'); +export interface IDebugAdapterDescriptorFactory extends DebugAdapterDescriptorFactory {} + +export const IDebugSessionLoggingFactory = Symbol('IDebugSessionLoggingFactory'); + +export interface IDebugSessionLoggingFactory extends DebugAdapterTrackerFactory {} + +export const IOutdatedDebuggerPromptFactory = Symbol('IOutdatedDebuggerPromptFactory'); + +export interface IOutdatedDebuggerPromptFactory extends DebugAdapterTrackerFactory {} + +export const IProtocolParser = Symbol('IProtocolParser'); +export interface IProtocolParser extends Disposable { + connect(stream: Readable): void; + once(event: string | symbol, listener: Function): this; + on(event: string | symbol, listener: Function): this; +} diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts new file mode 100644 index 000000000000..68ad0e49a149 --- /dev/null +++ b/src/client/debugger/types.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { DebugConfiguration } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; +import { DebuggerTypeName } from './constants'; + +export enum DebugOptions { + RedirectOutput = 'RedirectOutput', + Django = 'Django', + Jinja = 'Jinja', + DebugStdLib = 'DebugStdLib', + Sudo = 'Sudo', + Pyramid = 'Pyramid', + FixFilePathCase = 'FixFilePathCase', + WindowsClient = 'WindowsClient', + UnixClient = 'UnixClient', + StopOnEntry = 'StopOnEntry', + ShowReturnValue = 'ShowReturnValue', + SubProcess = 'Multiprocess' +} + +export type PathMapping = { + localRoot: string; + remoteRoot: string; +}; +export type Connection = { + host?: string; + port?: number; +}; + +interface ICommonDebugArguments { + redirectOutput?: boolean; + django?: boolean; + gevent?: boolean; + jinja?: boolean; + debugStdLib?: boolean; + justMyCode?: boolean; + logToFile?: boolean; + debugOptions?: DebugOptions[]; + port?: number; + host?: string; + // Show return values of functions while stepping. + showReturnValue?: boolean; + subProcess?: boolean; + // An absolute path to local directory with source. + pathMappings?: PathMapping[]; +} +export interface IKnownAttachDebugArguments extends ICommonDebugArguments { + workspaceFolder?: string; + customDebugger?: boolean; + // localRoot and remoteRoot are deprecated (replaced by pathMappings). + localRoot?: string; + remoteRoot?: string; + + // Internal field used to attach to subprocess using python debug adapter + subProcessId?: number; + + processId?: number | string; + connect?: Connection; + listen?: Connection; +} + +export interface IKnownLaunchRequestArguments extends ICommonDebugArguments { + sudo?: boolean; + pyramid?: boolean; + workspaceFolder?: string; + // An absolute path to the program to debug. + module?: string; + program?: string; + pythonPath: string; + // Automatically stop target after launch. If not specified, target does not stop. + stopOnEntry?: boolean; + args: string[]; + cwd?: string; + debugOptions?: DebugOptions[]; + env?: Record; + envFile: string; + console?: ConsoleType; + + // Internal field used to set custom python debug adapter (for testing) + debugAdapterPath?: string; +} +// tslint:disable-next-line:interface-name +export interface LaunchRequestArguments + extends DebugProtocol.LaunchRequestArguments, + IKnownLaunchRequestArguments, + DebugConfiguration { + type: typeof DebuggerTypeName; +} + +// tslint:disable-next-line:interface-name +export interface AttachRequestArguments + extends DebugProtocol.AttachRequestArguments, + IKnownAttachDebugArguments, + DebugConfiguration { + type: typeof DebuggerTypeName; +} + +// tslint:disable-next-line:interface-name +export interface DebugConfigurationArguments extends LaunchRequestArguments, AttachRequestArguments {} + +export type ConsoleType = 'internalConsole' | 'integratedTerminal' | 'externalTerminal'; + +export type TriggerType = 'launch' | 'attach' | 'test'; diff --git a/src/client/extension.ts b/src/client/extension.ts index 3f72b0811f54..2abbf7d8658e 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -1,74 +1,150 @@ 'use strict'; -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below -import * as vscode from 'vscode'; -import {PythonCompletionItemProvider} from './providers/completionProvider'; -import {PythonHoverProvider} from './providers/hoverProvider'; -import {PythonDefinitionProvider} from './providers/definitionProvider'; -import {PythonReferenceProvider} from './providers/referenceProvider'; -import {PythonRenameProvider} from './providers/renameProvider'; -import {PythonFormattingEditProvider} from './providers/formatProvider'; -import * as sortImports from './sortImports'; -import {LintProvider} from './providers/lintProvider'; -import {PythonSymbolProvider} from './providers/symbolProvider'; -import {activateFormatOnSaveProvider} from './providers/formatOnSaveProvider'; -import * as path from 'path'; -import * as settings from './common/configSettings' -import {activateUnitTestProvider} from './providers/testProvider'; - -// import {PythonSignatureHelpProvider} from './providers/signatureProvider'; - -const PYTHON: vscode.DocumentFilter = { language: 'python', scheme: 'file' } -let unitTestOutChannel: vscode.OutputChannel; -let formatOutChannel: vscode.OutputChannel; -let lintingOutChannel: vscode.OutputChannel; - -// this method is called when your extension is activated -// your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { - var rootDir = context.asAbsolutePath("."); - var pythonSettings = new settings.PythonSettings(); - unitTestOutChannel = vscode.window.createOutputChannel(pythonSettings.unitTest.outputWindow); - unitTestOutChannel.clear(); - formatOutChannel = unitTestOutChannel; - lintingOutChannel = unitTestOutChannel; - if (pythonSettings.unitTest.outputWindow !== pythonSettings.formatting.outputWindow) { - formatOutChannel = vscode.window.createOutputChannel(pythonSettings.formatting.outputWindow); - formatOutChannel.clear(); +// tslint:disable:no-var-requires no-require-imports + +// This line should always be right on top. +// tslint:disable:no-any +if ((Reflect as any).metadata === undefined) { + require('reflect-metadata'); +} + +// Initialize source maps (this must never be moved up nor further down). +import { initialize } from './sourceMapSupport'; +initialize(require('vscode')); +// Initialize the logger first. +require('./common/logger'); + +//=============================================== +// We start tracking the extension's startup time at this point. The +// locations at which we record various Intervals are marked below in +// the same way as this. + +const durations: Record = {}; +import { StopWatch } from './common/utils/stopWatch'; +// Do not move this line of code (used to measure extension load times). +const stopWatch = new StopWatch(); + +//=============================================== +// loading starts here + +import { ProgressLocation, ProgressOptions, window } from 'vscode'; + +import { buildApi, IExtensionApi } from './api'; +import { IApplicationShell } from './common/application/types'; +import { traceError } from './common/logger'; +import { IAsyncDisposableRegistry, IExtensionContext } from './common/types'; +import { createDeferred } from './common/utils/async'; +import { Common } from './common/utils/localize'; +import { activateComponents } from './extensionActivation'; +import { initializeComponents, initializeGlobals } from './extensionInit'; +import { IServiceContainer } from './ioc/types'; +import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; + +durations.codeLoadingTime = stopWatch.elapsedTime; + +//=============================================== +// loading ends here + +// These persist between activations: +let activatedServiceContainer: IServiceContainer | undefined; + +///////////////////////////// +// public functions + +export async function activate(context: IExtensionContext): Promise { + let api: IExtensionApi; + let ready: Promise; + let serviceContainer: IServiceContainer; + try { + [api, ready, serviceContainer] = await activateUnsafe(context, stopWatch, durations); + } catch (ex) { + // We want to completely handle the error + // before notifying VS Code. + await handleError(ex, durations); + throw ex; // re-raise } - if (pythonSettings.unitTest.outputWindow !== pythonSettings.linting.outputWindow) { - lintingOutChannel = vscode.window.createOutputChannel(pythonSettings.linting.outputWindow); - lintingOutChannel.clear(); + // Send the "success" telemetry only if activation did not fail. + // Otherwise Telemetry is send via the error handler. + sendStartupTelemetry(ready, durations, stopWatch, serviceContainer) + // Run in the background. + .ignoreErrors(); + return api; +} + +export function deactivate(): Thenable { + // Make sure to shutdown anybody who needs it. + if (activatedServiceContainer) { + const registry = activatedServiceContainer.get(IAsyncDisposableRegistry); + if (registry) { + return registry.dispose(); + } } + return Promise.resolve(); +} - sortImports.activate(context); - activateUnitTestProvider(context, pythonSettings, unitTestOutChannel); - activateFormatOnSaveProvider(PYTHON, context, pythonSettings, formatOutChannel); - - //Enable indentAction - vscode.languages.setLanguageConfiguration(PYTHON.language, { - onEnterRules: [ - { - beforeText: /^\s*(?:def|class|for|if|elif|else|while|try|with|finally).*?:\s*$/, - action: { indentAction: vscode.IndentAction.Indent } - } - ] - }); - - context.subscriptions.push(vscode.languages.registerRenameProvider(PYTHON, new PythonRenameProvider(context))); - context.subscriptions.push(vscode.languages.registerHoverProvider(PYTHON, new PythonHoverProvider(context))); - context.subscriptions.push(vscode.languages.registerDefinitionProvider(PYTHON, new PythonDefinitionProvider(context))); - context.subscriptions.push(vscode.languages.registerReferenceProvider(PYTHON, new PythonReferenceProvider(context))); - context.subscriptions.push(vscode.languages.registerCompletionItemProvider(PYTHON, new PythonCompletionItemProvider(context), '.')); - context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(PYTHON, new PythonSymbolProvider(context))); - // context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(PYTHON, new PythonSignatureHelpProvider(context), '(')); - - context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider(PYTHON, new PythonFormattingEditProvider(context, pythonSettings, formatOutChannel))); - context.subscriptions.push(new LintProvider(context, pythonSettings, lintingOutChannel)); +///////////////////////////// +// activation helpers + +// tslint:disable-next-line:max-func-body-length +async function activateUnsafe( + context: IExtensionContext, + startupStopWatch: StopWatch, + startupDurations: Record +): Promise<[IExtensionApi, Promise, IServiceContainer]> { + const activationDeferred = createDeferred(); + displayProgress(activationDeferred.promise); + startupDurations.startActivateTime = startupStopWatch.elapsedTime; + + //=============================================== + // activation starts here + + const [serviceManager, serviceContainer] = initializeGlobals(context); + activatedServiceContainer = serviceContainer; + initializeComponents(context, serviceManager, serviceContainer); + const { activationPromise } = await activateComponents(context, serviceManager, serviceContainer); + + //=============================================== + // activation ends here + + startupDurations.endActivateTime = startupStopWatch.elapsedTime; + activationDeferred.resolve(); + + const api = buildApi(activationPromise, serviceManager, serviceContainer); + return [api, activationPromise, serviceContainer]; +} + +// tslint:disable-next-line:no-any +function displayProgress(promise: Promise) { + const progressOptions: ProgressOptions = { location: ProgressLocation.Window, title: Common.loadingExtension() }; + window.withProgress(progressOptions, () => promise); } -// this method is called when your extension is deactivated -export function deactivate() { -} \ No newline at end of file +///////////////////////////// +// error handling + +async function handleError(ex: Error, startupDurations: Record) { + notifyUser( + "Extension activation failed, run the 'Developer: Toggle Developer Tools' command for more information." + ); + traceError('extension activation failed', ex); + await sendErrorTelemetry(ex, startupDurations, activatedServiceContainer); +} + +interface IAppShell { + showErrorMessage(string: string): Promise; +} + +function notifyUser(msg: string) { + try { + // tslint:disable-next-line:no-any + let appShell: IAppShell = (window as any) as IAppShell; + if (activatedServiceContainer) { + // tslint:disable-next-line:no-any + appShell = (activatedServiceContainer.get(IApplicationShell) as any) as IAppShell; + } + appShell.showErrorMessage(msg).ignoreErrors(); + } catch (ex) { + traceError('failed to notify user', ex); + } +} diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts new file mode 100644 index 000000000000..b2c7704812c5 --- /dev/null +++ b/src/client/extensionActivation.ts @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { CodeActionKind, debug, DebugConfigurationProvider, languages, OutputChannel, window } from 'vscode'; + +import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; +import { IExtensionActivationManager, ILanguageServerExtension } from './activation/types'; +import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; +import { IApplicationDiagnostics } from './application/types'; +import { DebugService } from './common/application/debugService'; +import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from './common/application/types'; +import { Commands, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL, UseProposedApi } from './common/constants'; +import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; +import { traceError } from './common/logger'; +import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; +import { IFileSystem } from './common/platform/types'; +import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; +import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentsManager, + IExtensionContext, + IFeatureDeprecationManager, + IOutputChannel +} from './common/types'; +import { OutputChannelNames } from './common/utils/localize'; +import { noop } from './common/utils/misc'; +import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; +import { JUPYTER_OUTPUT_CHANNEL } from './datascience/constants'; +import { registerTypes as dataScienceRegisterTypes } from './datascience/serviceRegistry'; +import { IDataScience } from './datascience/types'; +import { DebuggerTypeName } from './debugger/constants'; +import { DebugSessionEventDispatcher } from './debugger/extension/hooks/eventHandlerDispatcher'; +import { IDebugSessionEventHandlers } from './debugger/extension/hooks/types'; +import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; +import { IDebugConfigurationService, IDebuggerBanner } from './debugger/extension/types'; +import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; +import { + IInterpreterLocatorProgressHandler, + IInterpreterLocatorProgressService, + IInterpreterService +} from './interpreter/contracts'; +import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; +import { IServiceContainer, IServiceManager } from './ioc/types'; +import { getLanguageConfiguration } from './language/languageConfiguration'; +import { LinterCommands } from './linters/linterCommands'; +import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; +import { addOutputChannelLogging, setLoggingLevel } from './logging'; +import { PythonCodeActionProvider } from './providers/codeActionProvider/pythonCodeActionProvider'; +import { PythonFormattingEditProvider } from './providers/formatProvider'; +import { ReplProvider } from './providers/replProvider'; +import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; +import { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider'; +import { TerminalProvider } from './providers/terminalProvider'; +import { ISortImportsEditingProvider } from './providers/types'; +import { setExtensionInstallTelemetryProperties } from './telemetry/extensionInstallTelemetry'; +import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry'; +import { ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types'; +import { TEST_OUTPUT_CHANNEL } from './testing/common/constants'; +import { ITestContextService } from './testing/common/types'; +import { ITestCodeNavigatorCommandHandler, ITestExplorerCommandHandler } from './testing/navigation/types'; +import { registerTypes as unitTestsRegisterTypes } from './testing/serviceRegistry'; + +export async function activateComponents( + context: IExtensionContext, + serviceManager: IServiceManager, + serviceContainer: IServiceContainer +) { + // We will be pulling code over from activateLegacy(). + + return activateLegacy(context, serviceManager, serviceContainer); +} + +///////////////////////////// +// old activation code + +// tslint:disable-next-line:no-suspicious-comment +// TODO: Gradually move simple initialization +// and DI registration currently in this function over +// to initializeComponents(). Likewise with complex +// init and activation: move them to activateComponents(). +// See https://github.com/microsoft/vscode-python/issues/10454. + +async function activateLegacy( + context: IExtensionContext, + serviceManager: IServiceManager, + serviceContainer: IServiceContainer +) { + // register "services" + + const standardOutputChannel = window.createOutputChannel(OutputChannelNames.python()); + addOutputChannelLogging(standardOutputChannel); + const unitTestOutChannel = window.createOutputChannel(OutputChannelNames.pythonTest()); + const jupyterOutputChannel = window.createOutputChannel(OutputChannelNames.jupyter()); + serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); + serviceManager.addSingletonInstance(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); + serviceManager.addSingletonInstance(IOutputChannel, jupyterOutputChannel, JUPYTER_OUTPUT_CHANNEL); + + // Core registrations (non-feature specific). + commonRegisterTypes(serviceManager); + platformRegisterTypes(serviceManager); + processRegisterTypes(serviceManager); + + // We need to setup this property before any telemetry is sent + const fs = serviceManager.get(IFileSystem); + await setExtensionInstallTelemetryProperties(fs); + + const applicationEnv = serviceManager.get(IApplicationEnvironment); + const enableProposedApi = applicationEnv.packageJson.enableProposedApi; + serviceManager.addSingletonInstance(UseProposedApi, enableProposedApi); + // Feature specific registrations. + variableRegisterTypes(serviceManager); + unitTestsRegisterTypes(serviceManager); + lintersRegisterTypes(serviceManager); + interpretersRegisterTypes(serviceManager); + formattersRegisterTypes(serviceManager); + installerRegisterTypes(serviceManager); + commonRegisterTerminalTypes(serviceManager); + debugConfigurationRegisterTypes(serviceManager); + + const configuration = serviceManager.get(IConfigurationService); + // We should start logging using the log level as soon as possible, so set it as soon as we can access the level. + // `IConfigurationService` may depend any of the registered types, so doing it after all registrations are finished. + // XXX Move this *after* abExperiments is activated? + setLoggingLevel(configuration.getSettings().logging.level); + + const abExperiments = serviceContainer.get(IExperimentsManager); + await abExperiments.activate(); + + // Register datascience types after experiments have loaded. + // To ensure we can register types based on experiments. + dataScienceRegisterTypes(serviceManager); + + const languageServerType = configuration.getSettings().languageServer; + + // Language feature registrations. + appRegisterTypes(serviceManager, languageServerType); + providersRegisterTypes(serviceManager); + activationRegisterTypes(serviceManager, languageServerType); + + // "initialize" "services" + + const interpreterManager = serviceContainer.get(IInterpreterService); + interpreterManager.initialize(); + + const handlers = serviceManager.getAll(IDebugSessionEventHandlers); + const disposables = serviceManager.get(IDisposableRegistry); + const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); + dispatcher.registerEventHandlers(); + + const cmdManager = serviceContainer.get(ICommandManager); + const outputChannel = serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); + cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); + + // Display progress of interpreter refreshes only after extension has activated. + serviceContainer.get(IInterpreterLocatorProgressHandler).register(); + serviceContainer.get(IInterpreterLocatorProgressService).register(); + serviceContainer.get(IApplicationDiagnostics).register(); + serviceContainer.get(ITestCodeNavigatorCommandHandler).register(); + serviceContainer.get(ITestExplorerCommandHandler).register(); + serviceContainer.get(ILanguageServerExtension).register(); + serviceContainer.get(ITestContextService).register(); + + // "activate" everything else + + const manager = serviceContainer.get(IExtensionActivationManager); + context.subscriptions.push(manager); + const activationPromise = manager.activate(); + + serviceManager.get(ITerminalAutoActivation).register(); + const pythonSettings = configuration.getSettings(); + + activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); + + const sortImports = serviceContainer.get(ISortImportsEditingProvider); + sortImports.registerCommands(); + + serviceManager.get(ICodeExecutionManager).registerCommands(); + + const workspaceService = serviceContainer.get(IWorkspaceService); + interpreterManager + .refresh(workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined) + .catch((ex) => traceError('Python Extension: interpreterManager.refresh', ex)); + + // Activate data science features + const dataScience = serviceManager.get(IDataScience); + dataScience.activate().ignoreErrors(); + + context.subscriptions.push(new LinterCommands(serviceManager)); + + languages.setLanguageConfiguration(PYTHON_LANGUAGE, getLanguageConfiguration()); + + if (pythonSettings && pythonSettings.formatting && pythonSettings.formatting.provider !== 'internalConsole') { + const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); + context.subscriptions.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); + context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); + } + + const deprecationMgr = serviceContainer.get(IFeatureDeprecationManager); + deprecationMgr.initialize(); + context.subscriptions.push(deprecationMgr); + + context.subscriptions.push(new ReplProvider(serviceContainer)); + + const terminalProvider = new TerminalProvider(serviceContainer); + terminalProvider.initialize(window.activeTerminal).ignoreErrors(); + context.subscriptions.push(terminalProvider); + + context.subscriptions.push( + languages.registerCodeActionsProvider(PYTHON, new PythonCodeActionProvider(), { + providedCodeActionKinds: [CodeActionKind.SourceOrganizeImports] + }) + ); + + serviceContainer.getAll(IDebugConfigurationService).forEach((debugConfigProvider) => { + context.subscriptions.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); + }); + + serviceContainer.get(IDebuggerBanner).initialize(); + + return { activationPromise }; +} diff --git a/src/client/extensionInit.ts b/src/client/extensionInit.ts new file mode 100644 index 000000000000..07ea0a882d17 --- /dev/null +++ b/src/client/extensionInit.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { Container } from 'inversify'; +import { Disposable, Memento } from 'vscode'; + +import { GLOBAL_MEMENTO, IDisposableRegistry, IExtensionContext, IMemento, WORKSPACE_MEMENTO } from './common/types'; +import { ServiceContainer } from './ioc/container'; +import { ServiceManager } from './ioc/serviceManager'; +import { IServiceContainer, IServiceManager } from './ioc/types'; +import { registerForIOC } from './pythonEnvironments/legacyIOC'; + +// The code in this module should do nothing more complex than register +// objects to DI and simple init (e.g. no side effects). That implies +// that constructors are likewise simple and do no work. It also means +// that it is inherently synchronous. + +export function initializeGlobals(context: IExtensionContext): [IServiceManager, IServiceContainer] { + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + const serviceContainer = new ServiceContainer(cont); + + serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); + serviceManager.addSingletonInstance(IServiceManager, serviceManager); + + serviceManager.addSingletonInstance(IDisposableRegistry, context.subscriptions); + serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); + serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); + serviceManager.addSingletonInstance(IExtensionContext, context); + + return [serviceManager, serviceContainer]; +} + +export function initializeComponents( + _context: IExtensionContext, + serviceManager: IServiceManager, + serviceContainer: IServiceContainer +) { + registerForIOC(serviceManager, serviceContainer); + // We will be pulling code over from activateLegacy(). +} diff --git a/src/client/formatters/autoPep8Formatter.ts b/src/client/formatters/autoPep8Formatter.ts index 4456cc02bad5..e086e37f549b 100644 --- a/src/client/formatters/autoPep8Formatter.ts +++ b/src/client/formatters/autoPep8Formatter.ts @@ -1,24 +1,44 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - import * as vscode from 'vscode'; -import * as path from 'path'; -import {BaseFormatter} from './baseFormatter'; -import * as settings from './../common/configSettings'; +import { Product } from '../common/installer/productInstaller'; +import { IConfigurationService } from '../common/types'; +import { StopWatch } from '../common/utils/stopWatch'; +import { IServiceContainer } from '../ioc/types'; +import { sendTelemetryWhenDone } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { BaseFormatter } from './baseFormatter'; export class AutoPep8Formatter extends BaseFormatter { - private pythonSettings: settings.IPythonSettings; - constructor(settings: settings.IPythonSettings, outputChannel:vscode.OutputChannel) { - super("autopep8", outputChannel); - this.pythonSettings = settings; + constructor(serviceContainer: IServiceContainer) { + super('autopep8', Product.autopep8, serviceContainer); } - public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): Thenable { - var autopep8Path = this.pythonSettings.formatting.autopep8Path; - var fileDir = path.dirname(document.uri.fsPath); - return super.provideDocumentFormattingEdits(document, options, token, `${autopep8Path} "${document.uri.fsPath}"`); + public formatDocument( + document: vscode.TextDocument, + options: vscode.FormattingOptions, + token: vscode.CancellationToken, + range?: vscode.Range + ): Thenable { + const stopWatch = new StopWatch(); + const settings = this.serviceContainer + .get(IConfigurationService) + .getSettings(document.uri); + const hasCustomArgs = + Array.isArray(settings.formatting.autopep8Args) && settings.formatting.autopep8Args.length > 0; + const formatSelection = range ? !range.isEmpty : false; + + const autoPep8Args = ['--diff']; + if (formatSelection) { + // tslint:disable-next-line:no-non-null-assertion + autoPep8Args.push( + ...['--line-range', (range!.start.line + 1).toString(), (range!.end.line + 1).toString()] + ); + } + const promise = super.provideDocumentFormattingEdits(document, options, token, autoPep8Args); + sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { + tool: 'autopep8', + hasCustomArgs, + formatSelection + }); + return promise; } -} \ No newline at end of file +} diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts index 4fbc6986226f..435a5503337c 100644 --- a/src/client/formatters/baseFormatter.ts +++ b/src/client/formatters/baseFormatter.ts @@ -1,56 +1,148 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - -import * as vscode from 'vscode'; import * as path from 'path'; -import * as fs from 'fs'; -import {sendCommand} from './../common/childProc'; +import * as vscode from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../common/application/types'; +import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import '../common/extensions'; +import { isNotInstalledError } from '../common/helpers'; +import { traceError } from '../common/logger'; +import { IFileSystem } from '../common/platform/types'; +import { IPythonToolExecutionService } from '../common/process/types'; +import { IDisposableRegistry, IInstaller, IOutputChannel, Product } from '../common/types'; +import { isNotebookCell } from '../common/utils/misc'; +import { IServiceContainer } from '../ioc/types'; +import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor'; +import { IFormatterHelper } from './types'; export abstract class BaseFormatter { - public Id: string; - protected outputChannel: vscode.OutputChannel; - - constructor(id: string, outputChannel: vscode.OutputChannel) { - this.Id = id; - this.outputChannel = outputChannel; - } - public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): Thenable { - return Promise.resolve([]); - } - - protected provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, cmdLine: string): Thenable { - var fileDir = path.dirname(document.uri.fsPath); - return new Promise((resolve, reject) => { - //Todo: Save the contents of the file to a temporary file and format that instead saving the actual file - //This could unnecessarily trigger other behaviours - document.save().then(saved=> { - var filePath = document.uri.fsPath; - if (!fs.existsSync(filePath)) { - vscode.window.showErrorMessage(`File ${filePath} does not exist`) - return resolve([]); - } + protected readonly outputChannel: vscode.OutputChannel; + protected readonly workspace: IWorkspaceService; + private readonly helper: IFormatterHelper; - this.outputChannel.clear(); + constructor(public Id: string, private product: Product, protected serviceContainer: IServiceContainer) { + this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.helper = serviceContainer.get(IFormatterHelper); + this.workspace = serviceContainer.get(IWorkspaceService); + } - sendCommand(cmdLine, fileDir).then(data=> { - var formattedText = data; - if (document.getText() === formattedText) { - return resolve([]); - } + public abstract formatDocument( + document: vscode.TextDocument, + options: vscode.FormattingOptions, + token: vscode.CancellationToken, + range?: vscode.Range + ): Thenable; + protected getDocumentPath(document: vscode.TextDocument, fallbackPath: string) { + if (path.basename(document.uri.fsPath) === document.uri.fsPath) { + return fallbackPath; + } + return path.dirname(document.fileName); + } + protected getWorkspaceUri(document: vscode.TextDocument) { + const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); + if (workspaceFolder) { + return workspaceFolder.uri; + } + const folders = this.workspace.workspaceFolders; + if (Array.isArray(folders) && folders.length > 0) { + return folders[0].uri; + } + return vscode.Uri.file(__dirname); + } + protected async provideDocumentFormattingEdits( + document: vscode.TextDocument, + _options: vscode.FormattingOptions, + token: vscode.CancellationToken, + args: string[], + cwd?: string + ): Promise { + if (typeof cwd !== 'string' || cwd.length === 0) { + cwd = this.getWorkspaceUri(document).fsPath; + } - var range = new vscode.Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end) - var txtEdit = new vscode.TextEdit(range, formattedText); - resolve([txtEdit]); + // autopep8 and yapf have the ability to read from the process input stream and return the formatted code out of the output stream. + // However they don't support returning the diff of the formatted text when reading data from the input stream. + // Yet getting text formatted that way avoids having to create a temporary file, however the diffing will have + // to be done here in node (extension), i.e. extension CPU, i.e. less responsive solution. + // Also, always create temp files for Notebook cells. + const tempFile = await this.createTempFile(document); + if (this.checkCancellation(document.fileName, tempFile, token)) { + return []; + } - }, errorMsg => { - vscode.window.showErrorMessage(`There was an error in formatting the document. View the Python output window for details.`); - this.outputChannel.appendLine(errorMsg); - return resolve([]); - }); + const executionInfo = this.helper.getExecutionInfo(this.product, args, document.uri); + executionInfo.args.push(tempFile); + const pythonToolsExecutionService = this.serviceContainer.get( + IPythonToolExecutionService + ); + const promise = pythonToolsExecutionService + .exec(executionInfo, { cwd, throwOnStdErr: false, token }, document.uri) + .then((output) => output.stdout) + .then((data) => { + if (this.checkCancellation(document.fileName, tempFile, token)) { + return [] as vscode.TextEdit[]; + } + return getTextEditsFromPatch(document.getText(), data); + }) + .catch((error) => { + if (this.checkCancellation(document.fileName, tempFile, token)) { + return [] as vscode.TextEdit[]; + } + // tslint:disable-next-line:no-empty + this.handleError(this.Id, error, document.uri).catch(() => {}); + return [] as vscode.TextEdit[]; + }) + .then((edits) => { + this.deleteTempFile(document.fileName, tempFile).ignoreErrors(); + return edits; }); - }); + + const appShell = this.serviceContainer.get(IApplicationShell); + const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); + const disposable = appShell.setStatusBarMessage(`Formatting with ${this.Id}`, promise); + disposableRegistry.push(disposable); + return promise; + } + + protected async handleError(_expectedFileName: string, error: Error, resource?: vscode.Uri) { + let customError = `Formatting with ${this.Id} failed.`; + + if (isNotInstalledError(error)) { + const installer = this.serviceContainer.get(IInstaller); + const isInstalled = await installer.isInstalled(this.product, resource); + if (!isInstalled) { + customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`; + installer + .promptToInstall(this.product, resource) + .catch((ex) => traceError('Python Extension: promptToInstall', ex)); + } + } + + this.outputChannel.appendLine(`\n${customError}\n${error}`); + } + + /** + * Always create a temporary file when formatting notebook cells. + * This is because there is no physical file associated with notebook cells (they are all virtual). + */ + private async createTempFile(document: vscode.TextDocument): Promise { + const fs = this.serviceContainer.get(IFileSystem); + return document.isDirty || isNotebookCell(document) + ? getTempFileWithDocumentContents(document, fs) + : document.fileName; + } + + private deleteTempFile(originalFile: string, tempFile: string): Promise { + if (originalFile !== tempFile) { + const fs = this.serviceContainer.get(IFileSystem); + return fs.deleteFile(tempFile); + } + return Promise.resolve(); + } + + private checkCancellation(originalFile: string, tempFile: string, token?: vscode.CancellationToken): boolean { + if (token && token.isCancellationRequested) { + this.deleteTempFile(originalFile, tempFile).ignoreErrors(); + return true; + } + return false; } } diff --git a/src/client/formatters/blackFormatter.ts b/src/client/formatters/blackFormatter.ts new file mode 100644 index 000000000000..4c13ef1acbc3 --- /dev/null +++ b/src/client/formatters/blackFormatter.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as vscode from 'vscode'; +import { IApplicationShell } from '../common/application/types'; +import { Product } from '../common/installer/productInstaller'; +import { IConfigurationService } from '../common/types'; +import { noop } from '../common/utils/misc'; +import { StopWatch } from '../common/utils/stopWatch'; +import { IServiceContainer } from '../ioc/types'; +import { sendTelemetryWhenDone } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { BaseFormatter } from './baseFormatter'; + +export class BlackFormatter extends BaseFormatter { + constructor(serviceContainer: IServiceContainer) { + super('black', Product.black, serviceContainer); + } + + public async formatDocument( + document: vscode.TextDocument, + options: vscode.FormattingOptions, + token: vscode.CancellationToken, + range?: vscode.Range + ): Promise { + const stopWatch = new StopWatch(); + const settings = this.serviceContainer + .get(IConfigurationService) + .getSettings(document.uri); + const hasCustomArgs = Array.isArray(settings.formatting.blackArgs) && settings.formatting.blackArgs.length > 0; + const formatSelection = range ? !range.isEmpty : false; + + if (formatSelection) { + const shell = this.serviceContainer.get(IApplicationShell); + // Black does not support partial formatting on purpose. + shell.showErrorMessage('Black does not support the "Format Selection" command').then(noop, noop); + return []; + } + + const blackArgs = ['--diff', '--quiet']; + const promise = super.provideDocumentFormattingEdits(document, options, token, blackArgs); + sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'black', hasCustomArgs, formatSelection }); + return promise; + } +} diff --git a/src/client/formatters/dummyFormatter.ts b/src/client/formatters/dummyFormatter.ts new file mode 100644 index 000000000000..b82496a5f1b0 --- /dev/null +++ b/src/client/formatters/dummyFormatter.ts @@ -0,0 +1,19 @@ +import * as vscode from 'vscode'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { BaseFormatter } from './baseFormatter'; + +export class DummyFormatter extends BaseFormatter { + constructor(serviceContainer: IServiceContainer) { + super('none', Product.yapf, serviceContainer); + } + + public formatDocument( + _document: vscode.TextDocument, + _options: vscode.FormattingOptions, + _token: vscode.CancellationToken, + _range?: vscode.Range + ): Thenable { + return Promise.resolve([]); + } +} diff --git a/src/client/formatters/helper.ts b/src/client/formatters/helper.ts new file mode 100644 index 000000000000..3c09be283815 --- /dev/null +++ b/src/client/formatters/helper.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { ExecutionInfo, IConfigurationService, IFormattingSettings, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { FormatterId, FormatterSettingsPropertyNames, IFormatterHelper } from './types'; + +@injectable() +export class FormatterHelper implements IFormatterHelper { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + public translateToId(formatter: Product): FormatterId { + switch (formatter) { + case Product.autopep8: + return 'autopep8'; + case Product.black: + return 'black'; + case Product.yapf: + return 'yapf'; + default: { + throw new Error(`Unrecognized Formatter '${formatter}'`); + } + } + } + public getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames { + const id = this.translateToId(formatter); + return { + argsName: `${id}Args` as keyof IFormattingSettings, + pathName: `${id}Path` as keyof IFormattingSettings + }; + } + public getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo { + const settings = this.serviceContainer.get(IConfigurationService).getSettings(resource); + const names = this.getSettingsPropertyNames(formatter); + + const execPath = settings.formatting[names.pathName] as string; + let args: string[] = Array.isArray(settings.formatting[names.argsName]) + ? (settings.formatting[names.argsName] as string[]) + : []; + args = args.concat(customArgs); + + let moduleName: string | undefined; + + // If path information is not available, then treat it as a module, + if (path.basename(execPath) === execPath) { + moduleName = execPath; + } + + return { execPath, moduleName, args, product: formatter }; + } +} diff --git a/src/client/formatters/lineFormatter.ts b/src/client/formatters/lineFormatter.ts new file mode 100644 index 000000000000..e6a3bb6f1728 --- /dev/null +++ b/src/client/formatters/lineFormatter.ts @@ -0,0 +1,484 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable-next-line:import-name +import Char from 'typescript-char'; +import { Position, Range, TextDocument } from 'vscode'; +import { BraceCounter } from '../language/braceCounter'; +import { TextBuilder } from '../language/textBuilder'; +import { TextRangeCollection } from '../language/textRangeCollection'; +import { Tokenizer } from '../language/tokenizer'; +import { ITextRangeCollection, IToken, TokenType } from '../language/types'; + +const keywordsWithSpaceBeforeBrace = [ + 'and', + 'as', + 'assert', + 'await', + 'del', + 'except', + 'elif', + 'for', + 'from', + 'global', + 'if', + 'import', + 'in', + 'is', + 'lambda', + 'nonlocal', + 'not', + 'or', + 'raise', + 'return', + 'while', + 'with', + 'yield' +]; + +export class LineFormatter { + private builder = new TextBuilder(); + private tokens: ITextRangeCollection = new TextRangeCollection([]); + private braceCounter = new BraceCounter(); + private text = ''; + private document?: TextDocument; + private lineNumber = 0; + + // tslint:disable-next-line:cyclomatic-complexity + public formatLine(document: TextDocument, lineNumber: number): string { + this.document = document; + this.lineNumber = lineNumber; + this.text = document.lineAt(lineNumber).text; + this.tokens = new Tokenizer().tokenize(this.text); + this.builder = new TextBuilder(); + this.braceCounter = new BraceCounter(); + + if (this.tokens.count === 0) { + return this.text; + } + + const ws = this.text.substr(0, this.tokens.getItemAt(0).start); + if (ws.length > 0) { + this.builder.append(ws); // Preserve leading indentation. + } + + for (let i = 0; i < this.tokens.count; i += 1) { + const t = this.tokens.getItemAt(i); + const prev = i > 0 ? this.tokens.getItemAt(i - 1) : undefined; + const next = i < this.tokens.count - 1 ? this.tokens.getItemAt(i + 1) : undefined; + + switch (t.type) { + case TokenType.Operator: + this.handleOperator(i); + break; + + case TokenType.Comma: + this.builder.append(','); + if (next && !this.isCloseBraceType(next.type) && next.type !== TokenType.Colon) { + this.builder.softAppendSpace(); + } + break; + + case TokenType.Identifier: + if ( + prev && + !this.isOpenBraceType(prev.type) && + prev.type !== TokenType.Colon && + prev.type !== TokenType.Operator + ) { + this.builder.softAppendSpace(); + } + const id = this.text.substring(t.start, t.end); + this.builder.append(id); + if (this.isKeywordWithSpaceBeforeBrace(id) && next && this.isOpenBraceType(next.type)) { + // for x in () + this.builder.softAppendSpace(); + } + break; + + case TokenType.Colon: + // x: 1 if not in slice, x[1:y] if inside the slice. + this.builder.append(':'); + if (!this.braceCounter.isOpened(TokenType.OpenBracket) && next && next.type !== TokenType.Colon) { + // Not inside opened [[ ... ] sequence. + this.builder.softAppendSpace(); + } + break; + + case TokenType.Comment: + // Add 2 spaces before in-line comment per PEP guidelines. + if (prev) { + this.builder.softAppendSpace(2); + } + this.builder.append(this.text.substring(t.start, t.end)); + break; + + case TokenType.Semicolon: + this.builder.append(';'); + break; + + default: + this.handleOther(t, i); + break; + } + } + return this.builder.getText(); + } + + // tslint:disable-next-line:cyclomatic-complexity + private handleOperator(index: number): void { + const t = this.tokens.getItemAt(index); + const prev = index > 0 ? this.tokens.getItemAt(index - 1) : undefined; + const opCode = this.text.charCodeAt(t.start); + const next = index < this.tokens.count - 1 ? this.tokens.getItemAt(index + 1) : undefined; + + if (t.length === 1) { + switch (opCode) { + case Char.Equal: + this.handleEqual(t, index); + return; + case Char.Period: + if (prev && this.isKeyword(prev, 'from')) { + this.builder.softAppendSpace(); + } + this.builder.append('.'); + if (next && this.isKeyword(next, 'import')) { + this.builder.softAppendSpace(); + } + return; + case Char.At: + if (prev) { + // Binary case + this.builder.softAppendSpace(); + this.builder.append('@'); + this.builder.softAppendSpace(); + } else { + this.builder.append('@'); + } + return; + case Char.ExclamationMark: + this.builder.append('!'); + return; + case Char.Asterisk: + if (prev && this.isKeyword(prev, 'lambda')) { + this.builder.softAppendSpace(); + this.builder.append('*'); + return; + } + if (this.handleStarOperator(t, prev!)) { + return; + } + break; + default: + break; + } + } else if (t.length === 2) { + if ( + this.text.charCodeAt(t.start) === Char.Asterisk && + this.text.charCodeAt(t.start + 1) === Char.Asterisk + ) { + if (this.handleStarOperator(t, prev!)) { + return; + } + } + } + + // Do not append space if operator is preceded by '(' or ',' as in foo(**kwarg) + if (prev && (this.isOpenBraceType(prev.type) || prev.type === TokenType.Comma)) { + this.builder.append(this.text.substring(t.start, t.end)); + return; + } + + this.builder.softAppendSpace(); + this.builder.append(this.text.substring(t.start, t.end)); + + // Check unary case + if (prev && prev.type === TokenType.Operator) { + if (opCode === Char.Hyphen || opCode === Char.Plus || opCode === Char.Tilde) { + return; + } + } + this.builder.softAppendSpace(); + } + + private handleStarOperator(current: IToken, prev: IToken): boolean { + if ( + this.text.charCodeAt(current.start) === Char.Asterisk && + this.text.charCodeAt(current.start + 1) === Char.Asterisk + ) { + if (!prev || (prev.type !== TokenType.Identifier && prev.type !== TokenType.Number)) { + this.builder.append('**'); + return true; + } + if (prev && this.isKeyword(prev, 'lambda')) { + this.builder.softAppendSpace(); + this.builder.append('**'); + return true; + } + } + // Check previous line for the **/* condition + const lastLine = this.getPreviousLineTokens(); + const lastToken = lastLine && lastLine.count > 0 ? lastLine.getItemAt(lastLine.count - 1) : undefined; + if (lastToken && (this.isOpenBraceType(lastToken.type) || lastToken.type === TokenType.Comma)) { + this.builder.append(this.text.substring(current.start, current.end)); + return true; + } + return false; + } + + private handleEqual(_t: IToken, index: number): void { + if (this.isMultipleStatements(index) && !this.braceCounter.isOpened(TokenType.OpenBrace)) { + // x = 1; x, y = y, x + this.builder.softAppendSpace(); + this.builder.append('='); + this.builder.softAppendSpace(); + return; + } + + // Check if this is = in function arguments. If so, do not add spaces around it. + if (this.isEqualsInsideArguments(index)) { + this.builder.append('='); + return; + } + + this.builder.softAppendSpace(); + this.builder.append('='); + this.builder.softAppendSpace(); + } + + private handleOther(t: IToken, index: number): void { + if (this.isBraceType(t.type)) { + this.braceCounter.countBrace(t); + this.builder.append(this.text.substring(t.start, t.end)); + return; + } + + const prev = index > 0 ? this.tokens.getItemAt(index - 1) : undefined; + if ( + prev && + prev.length === 1 && + this.text.charCodeAt(prev.start) === Char.Equal && + this.isEqualsInsideArguments(index - 1) + ) { + // Don't add space around = inside function arguments. + this.builder.append(this.text.substring(t.start, t.end)); + return; + } + + if (prev && (this.isOpenBraceType(prev.type) || prev.type === TokenType.Colon)) { + // Don't insert space after (, [ or { . + this.builder.append(this.text.substring(t.start, t.end)); + return; + } + + if ( + t.type === TokenType.Number && + prev && + prev.type === TokenType.Operator && + prev.length === 1 && + this.text.charCodeAt(prev.start) === Char.Tilde + ) { + // Special case for ~ before numbers + this.builder.append(this.text.substring(t.start, t.end)); + return; + } + + if (t.type === TokenType.Unknown) { + this.handleUnknown(t); + } else { + // In general, keep tokens separated. + this.builder.softAppendSpace(); + this.builder.append(this.text.substring(t.start, t.end)); + } + } + + private handleUnknown(t: IToken): void { + const prevChar = t.start > 0 ? this.text.charCodeAt(t.start - 1) : 0; + if (prevChar === Char.Space || prevChar === Char.Tab) { + this.builder.softAppendSpace(); + } + this.builder.append(this.text.substring(t.start, t.end)); + + const nextChar = t.end < this.text.length - 1 ? this.text.charCodeAt(t.end) : 0; + if (nextChar === Char.Space || nextChar === Char.Tab) { + this.builder.softAppendSpace(); + } + } + + // tslint:disable-next-line:cyclomatic-complexity + private isEqualsInsideArguments(index: number): boolean { + if (index < 1) { + return false; + } + + // We are looking for IDENT = ? + const prev = this.tokens.getItemAt(index - 1); + if (prev.type !== TokenType.Identifier) { + return false; + } + + if (index > 1 && this.tokens.getItemAt(index - 2).type === TokenType.Colon) { + return false; // Type hint should have spaces around like foo(x: int = 1) per PEP 8 + } + + return this.isInsideFunctionArguments(this.tokens.getItemAt(index).start); + } + + private isOpenBraceType(type: TokenType): boolean { + return type === TokenType.OpenBrace || type === TokenType.OpenBracket || type === TokenType.OpenCurly; + } + private isCloseBraceType(type: TokenType): boolean { + return type === TokenType.CloseBrace || type === TokenType.CloseBracket || type === TokenType.CloseCurly; + } + private isBraceType(type: TokenType): boolean { + return this.isOpenBraceType(type) || this.isCloseBraceType(type); + } + + private isMultipleStatements(index: number): boolean { + for (let i = index; i >= 0; i -= 1) { + if (this.tokens.getItemAt(i).type === TokenType.Semicolon) { + return true; + } + } + return false; + } + + private isKeywordWithSpaceBeforeBrace(s: string): boolean { + return keywordsWithSpaceBeforeBrace.indexOf(s) >= 0; + } + private isKeyword(t: IToken, keyword: string): boolean { + return ( + t.type === TokenType.Identifier && + t.length === keyword.length && + this.text.substr(t.start, t.length) === keyword + ); + } + + // tslint:disable-next-line:cyclomatic-complexity + private isInsideFunctionArguments(position: number): boolean { + if (!this.document) { + return false; // unable to determine + } + + // Walk up until beginning of the document or line with 'def IDENT(' or line ending with : + // IDENT( by itself is not reliable since they can be nested in IDENT(IDENT(a), x=1) + let start = new Position(0, 0); + for (let i = this.lineNumber; i >= 0; i -= 1) { + const line = this.document.lineAt(i); + const lineTokens = new Tokenizer().tokenize(line.text); + if (lineTokens.count === 0) { + continue; + } + // 'def IDENT(' + const first = lineTokens.getItemAt(0); + if ( + lineTokens.count >= 3 && + first.length === 3 && + line.text.substr(first.start, first.length) === 'def' && + lineTokens.getItemAt(1).type === TokenType.Identifier && + lineTokens.getItemAt(2).type === TokenType.OpenBrace + ) { + start = line.range.start; + break; + } + + if (lineTokens.count > 0 && i < this.lineNumber) { + // One of previous lines ends with : + const last = lineTokens.getItemAt(lineTokens.count - 1); + if (last.type === TokenType.Colon) { + start = this.document.lineAt(i + 1).range.start; + break; + } else if (lineTokens.count > 1) { + const beforeLast = lineTokens.getItemAt(lineTokens.count - 2); + if (beforeLast.type === TokenType.Colon && last.type === TokenType.Comment) { + start = this.document.lineAt(i + 1).range.start; + break; + } + } + } + } + + // Now tokenize from the nearest reasonable point + const currentLine = this.document.lineAt(this.lineNumber); + const text = this.document.getText(new Range(start, currentLine.range.end)); + const tokens = new Tokenizer().tokenize(text); + + // Translate position in the line being formatted to the position in the tokenized block + position = this.document.offsetAt(currentLine.range.start) + position - this.document.offsetAt(start); + + // Walk tokens locating narrowest function signature as in IDENT( | ) + let funcCallStartIndex = -1; + let funcCallEndIndex = -1; + for (let i = 0; i < tokens.count - 1; i += 1) { + const t = tokens.getItemAt(i); + if (t.type === TokenType.Identifier) { + const next = tokens.getItemAt(i + 1); + if ( + next.type === TokenType.OpenBrace && + !this.isKeywordWithSpaceBeforeBrace(text.substr(t.start, t.length)) + ) { + // We are at IDENT(, try and locate the closing brace + let closeBraceIndex = this.findClosingBrace(tokens, i + 1); + // Closing brace is not required in case construct is not yet terminated + closeBraceIndex = closeBraceIndex > 0 ? closeBraceIndex : tokens.count - 1; + // Are we in range? + if (position > next.start && position < tokens.getItemAt(closeBraceIndex).start) { + funcCallStartIndex = i; + funcCallEndIndex = closeBraceIndex; + } + } + } + } + // Did we find anything? + if (funcCallStartIndex < 0) { + // No? See if we are between 'lambda' and ':' + for (let i = 0; i < tokens.count; i += 1) { + const t = tokens.getItemAt(i); + if (t.type === TokenType.Identifier && text.substr(t.start, t.length) === 'lambda') { + if (position < t.start) { + break; // Position is before the nearest 'lambda' + } + let colonIndex = this.findNearestColon(tokens, i + 1); + // Closing : is not required in case construct is not yet terminated + colonIndex = colonIndex > 0 ? colonIndex : tokens.count - 1; + if (position > t.start && position < tokens.getItemAt(colonIndex).start) { + funcCallStartIndex = i; + funcCallEndIndex = colonIndex; + } + } + } + } + return funcCallStartIndex >= 0 && funcCallEndIndex > 0; + } + + private findNearestColon(tokens: ITextRangeCollection, index: number): number { + for (let i = index; i < tokens.count; i += 1) { + if (tokens.getItemAt(i).type === TokenType.Colon) { + return i; + } + } + return -1; + } + + private findClosingBrace(tokens: ITextRangeCollection, index: number): number { + const braceCounter = new BraceCounter(); + for (let i = index; i < tokens.count; i += 1) { + const t = tokens.getItemAt(i); + if (t.type === TokenType.OpenBrace || t.type === TokenType.CloseBrace) { + braceCounter.countBrace(t); + } + if (braceCounter.count === 0) { + return i; + } + } + return -1; + } + + private getPreviousLineTokens(): ITextRangeCollection | undefined { + if (!this.document || this.lineNumber === 0) { + return undefined; // unable to determine + } + const line = this.document.lineAt(this.lineNumber - 1); + return new Tokenizer().tokenize(line.text); + } +} diff --git a/src/client/formatters/serviceRegistry.ts b/src/client/formatters/serviceRegistry.ts new file mode 100644 index 000000000000..196e6c806b5f --- /dev/null +++ b/src/client/formatters/serviceRegistry.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceManager } from '../ioc/types'; +import { FormatterHelper } from './helper'; +import { IFormatterHelper } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IFormatterHelper, FormatterHelper); +} diff --git a/src/client/formatters/types.ts b/src/client/formatters/types.ts new file mode 100644 index 000000000000..7f4bcf5b7524 --- /dev/null +++ b/src/client/formatters/types.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { ExecutionInfo, IFormattingSettings, Product } from '../common/types'; + +export const IFormatterHelper = Symbol('IFormatterHelper'); + +export type FormatterId = 'autopep8' | 'black' | 'yapf'; + +export type FormatterSettingsPropertyNames = { + argsName: keyof IFormattingSettings; + pathName: keyof IFormattingSettings; +}; + +export interface IFormatterHelper { + translateToId(formatter: Product): FormatterId; + getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames; + getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo; +} diff --git a/src/client/formatters/yapfFormatter.ts b/src/client/formatters/yapfFormatter.ts index 90f552e1ebbc..8bf0d3ad413b 100644 --- a/src/client/formatters/yapfFormatter.ts +++ b/src/client/formatters/yapfFormatter.ts @@ -1,24 +1,39 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; - import * as vscode from 'vscode'; -import * as path from 'path'; -import {BaseFormatter} from './baseFormatter'; -import * as settings from './../common/configSettings'; +import { IConfigurationService, Product } from '../common/types'; +import { StopWatch } from '../common/utils/stopWatch'; +import { IServiceContainer } from '../ioc/types'; +import { sendTelemetryWhenDone } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { BaseFormatter } from './baseFormatter'; export class YapfFormatter extends BaseFormatter { - private pythonSettings: settings.IPythonSettings; - constructor(settings: settings.IPythonSettings, outputChannel: vscode.OutputChannel) { - super("yapf", outputChannel); - this.pythonSettings = settings; + constructor(serviceContainer: IServiceContainer) { + super('yapf', Product.yapf, serviceContainer); } - public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): Thenable { - var yapfPath = this.pythonSettings.formatting.yapfPath; - var fileDir = path.dirname(document.uri.fsPath); - return super.provideDocumentFormattingEdits(document, options, token, `${yapfPath} "${document.uri.fsPath}"`); + public formatDocument( + document: vscode.TextDocument, + options: vscode.FormattingOptions, + token: vscode.CancellationToken, + range?: vscode.Range + ): Thenable { + const stopWatch = new StopWatch(); + const settings = this.serviceContainer + .get(IConfigurationService) + .getSettings(document.uri); + const hasCustomArgs = Array.isArray(settings.formatting.yapfArgs) && settings.formatting.yapfArgs.length > 0; + const formatSelection = range ? !range.isEmpty : false; + + const yapfArgs = ['--diff']; + if (formatSelection) { + // tslint:disable-next-line:no-non-null-assertion + yapfArgs.push(...['--lines', `${range!.start.line + 1}-${range!.end.line + 1}`]); + } + // Yapf starts looking for config file starting from the file path. + const fallbarFolder = this.getWorkspaceUri(document).fsPath; + const cwd = this.getDocumentPath(document, fallbarFolder); + const promise = super.provideDocumentFormattingEdits(document, options, token, yapfArgs, cwd); + sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'yapf', hasCustomArgs, formatSelection }); + return promise; } -} \ No newline at end of file +} diff --git a/src/client/interpreter/activation/preWarmVariables.ts b/src/client/interpreter/activation/preWarmVariables.ts new file mode 100644 index 000000000000..1532b152eaab --- /dev/null +++ b/src/client/interpreter/activation/preWarmVariables.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import '../../common/extensions'; +import { IInterpreterService } from '../contracts'; +import { IEnvironmentActivationService } from './types'; + +@injectable() +export class PreWarmActivatedEnvironmentVariables implements IExtensionSingleActivationService { + constructor( + @inject(IEnvironmentActivationService) private readonly activationService: IEnvironmentActivationService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService + ) {} + public async activate(): Promise { + this.interpreterService.onDidChangeInterpreter(() => + this.activationService.getActivatedEnvironmentVariables(undefined).ignoreErrors() + ); + this.activationService.getActivatedEnvironmentVariables(undefined).ignoreErrors(); + } +} diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts new file mode 100644 index 000000000000..e8355ee14ee3 --- /dev/null +++ b/src/client/interpreter/activation/service.ts @@ -0,0 +1,258 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; + +import { IWorkspaceService } from '../../common/application/types'; +import { PYTHON_WARNINGS } from '../../common/constants'; +import { LogOptions, traceDecorators, traceError, traceInfo, traceVerbose } from '../../common/logger'; +import { IPlatformService } from '../../common/platform/types'; +import * as internalScripts from '../../common/process/internal/scripts'; +import { ExecutionResult, IProcessServiceFactory } from '../../common/process/types'; +import { ITerminalHelper, TerminalShellType } from '../../common/terminal/types'; +import { ICurrentProcess, IDisposable, Resource } from '../../common/types'; +import { sleep } from '../../common/utils/async'; +import { InMemoryCache } from '../../common/utils/cacheUtils'; +import { OSType } from '../../common/utils/platform'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IInterpreterService } from '../contracts'; +import { IEnvironmentActivationService } from './types'; + +const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6'; +const cacheDuration = 10 * 60 * 1000; +export const getEnvironmentTimeout = 30000; + +// The shell under which we'll execute activation scripts. +const defaultShells = { + [OSType.Windows]: { shell: 'cmd', shellType: TerminalShellType.commandPrompt }, + [OSType.OSX]: { shell: 'bash', shellType: TerminalShellType.bash }, + [OSType.Linux]: { shell: 'bash', shellType: TerminalShellType.bash }, + [OSType.Unknown]: undefined +}; + +const condaRetryMessages = [ + 'The process cannot access the file because it is being used by another process', + 'The directory is not empty' +]; + +/** + * This class exists so that the environment variable fetching can be cached in between tests. Normally + * this cache resides in memory for the duration of the EnvironmentActivationService's lifetime, but in the case + * of our functional tests, we want the cached data to exist outside of each test (where each test will destroy the EnvironmentActivationService) + * This gives each test a 3 or 4 second speedup. + */ +export class EnvironmentActivationServiceCache { + private static useStatic = false; + private static staticMap = new Map>(); + private normalMap = new Map>(); + + public static forceUseStatic() { + EnvironmentActivationServiceCache.useStatic = true; + } + public static forceUseNormal() { + EnvironmentActivationServiceCache.useStatic = false; + } + public get(key: string): InMemoryCache | undefined { + if (EnvironmentActivationServiceCache.useStatic) { + return EnvironmentActivationServiceCache.staticMap.get(key); + } + return this.normalMap.get(key); + } + + public set(key: string, value: InMemoryCache) { + if (EnvironmentActivationServiceCache.useStatic) { + EnvironmentActivationServiceCache.staticMap.set(key, value); + } else { + this.normalMap.set(key, value); + } + } + + public delete(key: string) { + if (EnvironmentActivationServiceCache.useStatic) { + EnvironmentActivationServiceCache.staticMap.delete(key); + } else { + this.normalMap.delete(key); + } + } + + public clear() { + // Don't clear during a test as the environment isn't going to change + if (!EnvironmentActivationServiceCache.useStatic) { + this.normalMap.clear(); + } + } +} + +@injectable() +export class EnvironmentActivationService implements IEnvironmentActivationService, IDisposable { + private readonly disposables: IDisposable[] = []; + private readonly activatedEnvVariablesCache = new EnvironmentActivationServiceCache(); + constructor( + @inject(ITerminalHelper) private readonly helper: ITerminalHelper, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IProcessServiceFactory) private processServiceFactory: IProcessServiceFactory, + @inject(ICurrentProcess) private currentProcess: ICurrentProcess, + @inject(IWorkspaceService) private workspace: IWorkspaceService, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider + ) { + this.envVarsService.onDidEnvironmentVariablesChange( + () => this.activatedEnvVariablesCache.clear(), + this, + this.disposables + ); + + this.interpreterService.onDidChangeInterpreter( + () => this.activatedEnvVariablesCache.clear(), + this, + this.disposables + ); + } + + public dispose(): void { + this.disposables.forEach((d) => d.dispose()); + } + @traceDecorators.verbose('getActivatedEnvironmentVariables', LogOptions.Arguments) + @captureTelemetry(EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, { failed: false }, true) + public async getActivatedEnvironmentVariables( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean + ): Promise { + // Cache key = resource + interpreter. + const workspaceKey = this.workspace.getWorkspaceFolderIdentifier(resource); + const interpreterPath = this.platform.isWindows ? interpreter?.path.toLowerCase() : interpreter?.path; + const cacheKey = `${workspaceKey}_${interpreterPath}`; + + if (this.activatedEnvVariablesCache.get(cacheKey)?.hasData) { + return this.activatedEnvVariablesCache.get(cacheKey)!.data; + } + + // Cache only if successful, else keep trying & failing if necessary. + const cache = new InMemoryCache(cacheDuration, ''); + return this.getActivatedEnvironmentVariablesImpl(resource, interpreter, allowExceptions).then((vars) => { + cache.data = vars; + this.activatedEnvVariablesCache.set(cacheKey, cache); + return vars; + }); + } + + public async getActivatedEnvironmentVariablesImpl( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean + ): Promise { + const shellInfo = defaultShells[this.platform.osType]; + if (!shellInfo) { + return; + } + let isPossiblyCondaEnv = false; + try { + const activationCommands = await this.helper.getEnvironmentActivationShellCommands( + resource, + shellInfo.shellType, + interpreter + ); + traceVerbose(`Activation Commands received ${activationCommands} for shell ${shellInfo.shell}`); + if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { + return; + } + isPossiblyCondaEnv = activationCommands.join(' ').toLowerCase().includes('conda'); + // Run the activate command collect the environment from it. + const activationCommand = this.fixActivationCommands(activationCommands).join(' && '); + const processService = await this.processServiceFactory.create(resource); + const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); + const hasCustomEnvVars = Object.keys(customEnvVars).length; + const env = hasCustomEnvVars ? customEnvVars : { ...this.currentProcess.env }; + + // Make sure python warnings don't interfere with getting the environment. However + // respect the warning in the returned values + const oldWarnings = env[PYTHON_WARNINGS]; + env[PYTHON_WARNINGS] = 'ignore'; + + traceVerbose(`${hasCustomEnvVars ? 'Has' : 'No'} Custom Env Vars`); + + // In order to make sure we know where the environment output is, + // put in a dummy echo we can look for + const [args, parse] = internalScripts.printEnvVariables(); + args.forEach((arg, i) => { + args[i] = arg.toCommandArgument(); + }); + const command = `${activationCommand} && echo '${getEnvironmentPrefix}' && python ${args.join(' ')}`; + traceVerbose(`Activating Environment to capture Environment variables, ${command}`); + + // Do some wrapping of the call. For two reasons: + // 1) Conda activate can hang on certain systems. Fail after 30 seconds. + // See the discussion from hidesoon in this issue: https://github.com/Microsoft/vscode-python/issues/4424 + // His issue is conda never finishing during activate. This is a conda issue, but we + // should at least tell the user. + // 2) Retry because of this issue here: https://github.com/microsoft/vscode-python/issues/9244 + // This happens on AzDo machines a bunch when using Conda (and we can't dictate the conda version in order to get the fix) + let result: ExecutionResult | undefined; + let tryCount = 1; + while (!result) { + try { + result = await processService.shellExec(command, { + env, + shell: shellInfo.shell, + timeout: getEnvironmentTimeout, + maxBuffer: 1000 * 1000, + throwOnStdErr: false + }); + if (result.stderr && result.stderr.length > 0) { + throw new Error(`StdErr from ShellExec, ${result.stderr} for ${command}`); + } + } catch (exc) { + // Special case. Conda for some versions will state a file is in use. If + // that's the case, wait and try again. This happens especially on AzDo + const excString = exc.toString(); + if (condaRetryMessages.find((m) => excString.includes(m)) && tryCount < 10) { + traceInfo(`Conda is busy, attempting to retry ...`); + result = undefined; + tryCount += 1; + await sleep(500); + } else { + throw exc; + } + } + } + const returnedEnv = this.parseEnvironmentOutput(result.stdout, parse); + + // Put back the PYTHONWARNINGS value + if (oldWarnings && returnedEnv) { + returnedEnv[PYTHON_WARNINGS] = oldWarnings; + } else if (returnedEnv) { + delete returnedEnv[PYTHON_WARNINGS]; + } + return returnedEnv; + } catch (e) { + traceError('getActivatedEnvironmentVariables', e); + sendTelemetryEvent(EventName.ACTIVATE_ENV_TO_GET_ENV_VARS_FAILED, undefined, { + isPossiblyCondaEnv, + terminal: shellInfo.shellType + }); + + // Some callers want this to bubble out, others don't + if (allowExceptions) { + throw e; + } + } + } + + protected fixActivationCommands(commands: string[]): string[] { + // Replace 'source ' with '. ' as that works in shell exec + return commands.map((cmd) => cmd.replace(/^source\s+/, '. ')); + } + @traceDecorators.error('Failed to parse Environment variables') + @traceDecorators.verbose('parseEnvironmentOutput', LogOptions.None) + protected parseEnvironmentOutput(output: string, parse: (out: string) => NodeJS.ProcessEnv | undefined) { + output = output.substring(output.indexOf(getEnvironmentPrefix) + getEnvironmentPrefix.length); + const js = output.substring(output.indexOf('{')).trim(); + return parse(js); + } +} diff --git a/src/client/interpreter/activation/terminalEnvironmentActivationService.ts b/src/client/interpreter/activation/terminalEnvironmentActivationService.ts new file mode 100644 index 000000000000..a29c005d6369 --- /dev/null +++ b/src/client/interpreter/activation/terminalEnvironmentActivationService.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationTokenSource } from 'vscode'; +import { LogOptions, traceDecorators } from '../../common/logger'; +import { IFileSystem } from '../../common/platform/types'; +import * as internalScripts from '../../common/process/internal/scripts'; +import { ITerminalServiceFactory } from '../../common/terminal/types'; +import { Resource } from '../../common/types'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { captureTelemetry } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IEnvironmentActivationService } from './types'; + +/** + * This class will provide the environment variables of an interpreter by activating it in a terminal. + * This has the following benefit: + * - Using a shell that's configured by the user (using their default shell). + * - Environment variables are dumped into a file instead of reading from stdout. + * + * @export + * @class TerminalEnvironmentActivationService + * @implements {IEnvironmentActivationService} + * @implements {IDisposable} + */ +@injectable() +export class TerminalEnvironmentActivationService implements IEnvironmentActivationService { + constructor( + @inject(ITerminalServiceFactory) private readonly terminalFactory: ITerminalServiceFactory, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IEnvironmentVariablesProvider) private readonly envVarsProvider: IEnvironmentVariablesProvider + ) {} + @traceDecorators.verbose('getActivatedEnvironmentVariables', LogOptions.Arguments) + @captureTelemetry( + EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, + { failed: false, activatedInTerminal: true }, + true + ) + public async getActivatedEnvironmentVariables( + resource: Resource, + interpreter?: PythonEnvironment | undefined, + _allowExceptions?: boolean | undefined + ): Promise { + const env = (await this.envVarsProvider.getCustomEnvironmentVariables(resource)) as + | { [key: string]: string | null } + | undefined; + const terminal = this.terminalFactory.getTerminalService({ + env, + hideFromUser: true, + interpreter, + resource, + title: `${interpreter?.displayName}${new Date().getTime()}` + }); + + const command = interpreter?.path || 'python'; + const jsonFile = await this.fs.createTemporaryFile('.json'); + try { + const [args, parse] = internalScripts.printEnvVariablesToFile(jsonFile.filePath); + + // Pass a cancellation token to ensure we wait until command has completed. + // If there are any errors in executing in the terminal, throw them so they get logged and bubbled up. + await terminal.sendCommand(command, args, new CancellationTokenSource().token, false); + + const contents = await this.fs.readFile(jsonFile.filePath); + return parse(contents); + } finally { + // We created a hidden terminal for temp usage, hence dispose when done. + terminal.dispose(); + jsonFile.dispose(); + } + } +} diff --git a/src/client/interpreter/activation/types.ts b/src/client/interpreter/activation/types.ts new file mode 100644 index 000000000000..e8ae94e3f284 --- /dev/null +++ b/src/client/interpreter/activation/types.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Resource } from '../../common/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; + +export const IEnvironmentActivationService = Symbol('IEnvironmentActivationService'); +export interface IEnvironmentActivationService { + getActivatedEnvironmentVariables( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean + ): Promise; +} diff --git a/src/client/interpreter/activation/wrapperEnvironmentActivationService.ts b/src/client/interpreter/activation/wrapperEnvironmentActivationService.ts new file mode 100644 index 000000000000..6bf4230ece33 --- /dev/null +++ b/src/client/interpreter/activation/wrapperEnvironmentActivationService.ts @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { UseTerminalToGetActivatedEnvVars } from '../../common/experiments/groups'; +import '../../common/extensions'; +import { traceError } from '../../common/logger'; +import { IFileSystem } from '../../common/platform/types'; +import { + ICryptoUtils, + IDisposableRegistry, + IExperimentsManager, + IExtensionContext, + Resource +} from '../../common/types'; +import { createDeferredFromPromise } from '../../common/utils/async'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { captureTelemetry } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IInterpreterService } from '../contracts'; +import { EnvironmentActivationService } from './service'; +import { TerminalEnvironmentActivationService } from './terminalEnvironmentActivationService'; +import { IEnvironmentActivationService } from './types'; + +// We have code in terminal activation that waits for a min of 500ms. +// Observed on a Mac that it can take up to 2.5s (the delay is caused by initialization scripts running in the shell). +// On some other machines (windows) with Conda, this could take up to 40s. +// To get around this we will: +// 1. Load the variables when extension loads +// 2. Cache variables in a file (so its available when VSC re-loads). + +type EnvVariablesInCachedFile = { env?: NodeJS.ProcessEnv }; +@injectable() +export class WrapperEnvironmentActivationService implements IEnvironmentActivationService { + private readonly cachePerResourceAndInterpreter = new Map>(); + constructor( + @inject(EnvironmentActivationService) private readonly procActivation: IEnvironmentActivationService, + @inject(TerminalEnvironmentActivationService) + private readonly terminalActivation: IEnvironmentActivationService, + @inject(IExperimentsManager) private readonly experiment: IExperimentsManager, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IEnvironmentVariablesProvider) private readonly envVarsProvider: IEnvironmentVariablesProvider, + @inject(IExtensionContext) private readonly context: IExtensionContext, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(ICryptoUtils) private readonly crypto: ICryptoUtils, + + @inject(IDisposableRegistry) disposables: IDisposableRegistry + ) { + // Environment variables rely on custom variables defined by the user in `.env` files. + disposables.push( + envVarsProvider.onDidEnvironmentVariablesChange(() => this.cachePerResourceAndInterpreter.clear()) + ); + } + @captureTelemetry( + EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, + { failed: false, activatedByWrapper: true }, + true + ) + public async getActivatedEnvironmentVariables( + resource: Resource, + interpreter?: PythonEnvironment | undefined, + allowExceptions?: boolean | undefined + ): Promise { + let key: string; + [key, interpreter] = await Promise.all([ + this.getCacheKey(resource, interpreter), + interpreter || (await this.interpreterService.getActiveInterpreter(undefined)) + ]); + + const procCacheKey = `Process${key}`; + const terminalCacheKey = `Terminal${key}`; + const procEnvVarsPromise = this.cacheCallback(procCacheKey, () => + this.getActivatedEnvVarsFromProc(resource, interpreter, allowExceptions) + ); + const terminalEnvVarsPromise = this.cacheCallback(terminalCacheKey, () => + this.getActivatedEnvVarsFromTerminal(procEnvVarsPromise, resource, interpreter, allowExceptions) + ); + + const procEnvVars = createDeferredFromPromise(procEnvVarsPromise); + const terminalEnvVars = createDeferredFromPromise(terminalEnvVarsPromise); + + // Do not return this value, its possible both complete almost at the same time. + // Hence wait for another tick, then check and return. + await Promise.race([terminalEnvVars.promise, procEnvVars.promise]); + + // Give preference to the terminal environment variables promise. + return terminalEnvVars.completed ? terminalEnvVars.promise : procEnvVars.promise; + } + /** + * Cache the implementation so it can be used in the future. + * If a cached entry already exists, then ignore the implementation. + * + * @private + * @param {string} cacheKey + * @param {(() => Promise)} implementation + * @returns {(Promise)} + * @memberof WrapperEnvironmentActivationService + */ + private async cacheCallback( + cacheKey: string, + implementation: () => Promise + ): Promise { + const contents = await this.getDataCachedInFile(cacheKey); + if (contents) { + // If we have it in file cache, then blow away in memory cache, we don't need that anymore. + this.cachePerResourceAndInterpreter.delete(cacheKey); + return contents.env; + } + + // If we don't have this cached in file, we need to ensure the request is cached in memory. + // This way if two different parts of the extension request variables for the same resource + interpreter, they get the same result (promise). + if (!this.cachePerResourceAndInterpreter.get(cacheKey)) { + const promise = implementation(); + this.cachePerResourceAndInterpreter.set(cacheKey, promise); + + // What ever result we get back, store that in file (cache it for other VSC sessions). + promise + .then((env) => this.writeDataToCacheFile(cacheKey, { env })) + .catch((ex) => traceError('Failed to write Env Vars to disc', ex)); + } + + return this.cachePerResourceAndInterpreter.get(cacheKey)!; + } + private getCacheFile(cacheKey: string): string | undefined { + return this.context.storagePath + ? path.join(this.context.storagePath, `pvscEnvVariables${cacheKey}.json`) + : undefined; + } + private async getDataCachedInFile(cacheKey: string): Promise { + const cacheFile = this.getCacheFile(cacheKey); + if (!cacheFile) { + return; + } + return this.fs + .readFile(cacheFile) + .then((data) => JSON.parse(data) as EnvVariablesInCachedFile) + .catch(() => undefined); + } + /** + * Writes the environment variables to disc. + * This way it is available to other VSC Sessions (between VSC reloads). + */ + private async writeDataToCacheFile(cacheKey: string, data: EnvVariablesInCachedFile): Promise { + const cacheFile = this.getCacheFile(cacheKey); + if (!cacheFile || !this.context.storagePath) { + return; + } + if (!(await this.fs.directoryExists(this.context.storagePath))) { + await this.fs.createDirectory(this.context.storagePath); + } + await this.fs.writeFile(cacheFile, JSON.stringify(data)); + } + /** + * Get environment variables by spawning a process (old approach). + */ + private async getActivatedEnvVarsFromProc( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean + ): Promise { + return this.procActivation.getActivatedEnvironmentVariables(resource, interpreter, allowExceptions); + } + /** + * Get environment variables by activating a terminal. + * As a fallback use the `fallback` promise passed in (old approach). + */ + private async getActivatedEnvVarsFromTerminal( + fallback: Promise, + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean + ): Promise { + if (!this.experiment.inExperiment(UseTerminalToGetActivatedEnvVars.experiment)) { + return fallback; + } + + return this.terminalActivation + .getActivatedEnvironmentVariables(resource, interpreter, allowExceptions) + .then((vars) => { + // If no variables in terminal, then revert to old approach. + return vars || fallback; + }) + .catch((ex) => { + // Swallow exceptions when using terminal env and revert to using old approach. + traceError('Failed to get variables using Terminal Service', ex); + return fallback; + }); + } + /** + * Computes a key used to cache environment variables. + * 1. If resource changes, then environment variables could be different, as user could have `.env` files or similar. + * (this might not be necessary, if there are no custom variables, but paths such as PYTHONPATH, PATH might be different too when computed). + * 2. If interpreter changes, then environment variables could be different (conda has its own env variables). + * 3. Similarly, each workspace could have its own env variables defined in `.env` files, and these could change as well. + * Hence the key is computed based off of these three. + */ + private async getCacheKey(resource: Resource, interpreter?: PythonEnvironment | undefined): Promise { + // Get the custom environment variables as a string (if any errors, ignore and use empty string). + const customEnvVariables = await this.envVarsProvider + .getCustomEnvironmentVariables(resource) + .then((item) => (item ? JSON.stringify(item) : '')) + .catch(() => ''); + + return this.crypto.createHash( + `${customEnvVariables}${interpreter?.path}${interpreter?.version?.raw}`, + 'string', + 'SHA256' + ); + } +} diff --git a/src/client/interpreter/autoSelection/constants.ts b/src/client/interpreter/autoSelection/constants.ts new file mode 100644 index 000000000000..ca3b4d131e97 --- /dev/null +++ b/src/client/interpreter/autoSelection/constants.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export const unsafeInterpreterPromptKey = 'unsafeInterpreterPromptKey'; +export const unsafeInterpretersKey = 'unsafeInterpretersKey'; +export const safeInterpretersKey = 'safeInterpretersKey'; +export const flaggedWorkspacesKeysStorageKey = 'flaggedWorkspacesKeysInterpreterSecurityStorageKey'; +export const learnMoreOnInterpreterSecurityURI = 'https://aka.ms/AA7jfor'; diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts new file mode 100644 index 000000000000..a9ca7e1b4b25 --- /dev/null +++ b/src/client/interpreter/autoSelection/index.ts @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { compare } from 'semver'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import '../../common/extensions'; +import { IFileSystem } from '../../common/platform/types'; +import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IInterpreterHelper } from '../contracts'; +import { + AutoSelectionRule, + IInterpreterAutoSelectionRule, + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService, + IInterpreterSecurityService +} from './types'; + +const preferredGlobalInterpreter = 'preferredGlobalPyInterpreter'; +const workspacePathNameForGlobalWorkspaces = ''; + +@injectable() +export class InterpreterAutoSelectionService implements IInterpreterAutoSelectionService { + protected readonly autoSelectedWorkspacePromises = new Map>(); + private readonly didAutoSelectedInterpreterEmitter = new EventEmitter(); + private readonly autoSelectedInterpreterByWorkspace = new Map(); + private globallyPreferredInterpreter!: IPersistentState; + private readonly rules: IInterpreterAutoSelectionRule[] = []; + constructor( + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IPersistentStateFactory) private readonly stateFactory: IPersistentStateFactory, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IInterpreterAutoSelectionRule) + @named(AutoSelectionRule.systemWide) + systemInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) + @named(AutoSelectionRule.currentPath) + currentPathInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) + @named(AutoSelectionRule.windowsRegistry) + winRegInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) + @named(AutoSelectionRule.cachedInterpreters) + cachedPaths: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) + @named(AutoSelectionRule.settings) + private readonly userDefinedInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) + @named(AutoSelectionRule.workspaceVirtualEnvs) + workspaceInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSeletionProxyService) proxy: IInterpreterAutoSeletionProxyService, + @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper, + @inject(IInterpreterSecurityService) private readonly interpreterSecurityService: IInterpreterSecurityService + ) { + // It is possible we area always opening the same workspace folder, but we still need to determine and cache + // the best available interpreters based on other rules (cache for furture use). + this.rules.push( + ...[ + winRegInterpreter, + currentPathInterpreter, + systemInterpreter, + cachedPaths, + userDefinedInterpreter, + workspaceInterpreter + ] + ); + proxy.registerInstance!(this); + // Rules are as follows in order + // 1. First check user settings.json + // If we have user settings, then always use that, do not proceed. + // 2. Check workspace virtual environments (pipenv, etc). + // If we have some, then use those as preferred workspace environments. + // 3. Check list of cached interpreters (previously cachced from all the rules). + // If we find a good one, use that as preferred global env. + // Provided its better than what we have already cached as globally preffered interpreter (globallyPreferredInterpreter). + // 4. Check current path. + // If we find a good one, use that as preferred global env. + // Provided its better than what we have already cached as globally preffered interpreter (globallyPreferredInterpreter). + // 5. Check windows registry. + // If we find a good one, use that as preferred global env. + // Provided its better than what we have already cached as globally preffered interpreter (globallyPreferredInterpreter). + // 6. Check the entire system. + // If we find a good one, use that as preferred global env. + // Provided its better than what we have already cached as globally preffered interpreter (globallyPreferredInterpreter). + userDefinedInterpreter.setNextRule(workspaceInterpreter); + workspaceInterpreter.setNextRule(cachedPaths); + cachedPaths.setNextRule(currentPathInterpreter); + currentPathInterpreter.setNextRule(winRegInterpreter); + winRegInterpreter.setNextRule(systemInterpreter); + } + @captureTelemetry(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, { rule: AutoSelectionRule.all }, true) + public async autoSelectInterpreter(resource: Resource): Promise { + const key = this.getWorkspacePathKey(resource); + if (!this.autoSelectedWorkspacePromises.has(key)) { + const deferred = createDeferred(); + this.autoSelectedWorkspacePromises.set(key, deferred); + await this.initializeStore(resource); + await this.clearWorkspaceStoreIfInvalid(resource); + await this.userDefinedInterpreter.autoSelectInterpreter(resource, this); + this.didAutoSelectedInterpreterEmitter.fire(); + Promise.all(this.rules.map((item) => item.autoSelectInterpreter(resource))).ignoreErrors(); + deferred.resolve(); + } + return this.autoSelectedWorkspacePromises.get(key)!.promise; + } + public get onDidChangeAutoSelectedInterpreter(): Event { + return this.didAutoSelectedInterpreterEmitter.event; + } + public getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined { + // Do not execute anycode other than fetching fromm a property. + // This method gets invoked from settings class, and this class in turn uses classes that relies on settings. + // I.e. we can end up in a recursive loop. + const interpreter = this._getAutoSelectedInterpreter(resource); + // Unless the interpreter is marked as unsafe, return interpreter. + return interpreter && this.interpreterSecurityService.isSafe(interpreter) === false ? undefined : interpreter; + } + + public _getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined { + const workspaceState = this.getWorkspaceState(resource); + if (workspaceState && workspaceState.value) { + return workspaceState.value; + } + + const workspaceFolderPath = this.getWorkspacePathKey(resource); + if (this.autoSelectedInterpreterByWorkspace.has(workspaceFolderPath)) { + return this.autoSelectedInterpreterByWorkspace.get(workspaceFolderPath); + } + + return this.globallyPreferredInterpreter.value; + } + public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonEnvironment | undefined) { + await this.storeAutoSelectedInterpreter(resource, interpreter); + } + public async setGlobalInterpreter(interpreter: PythonEnvironment) { + await this.storeAutoSelectedInterpreter(undefined, interpreter); + } + protected async clearWorkspaceStoreIfInvalid(resource: Resource) { + const stateStore = this.getWorkspaceState(resource); + if (stateStore && stateStore.value && !(await this.fs.fileExists(stateStore.value.path))) { + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, {}, { interpreterMissing: true }); + await stateStore.updateValue(undefined); + } + } + protected async storeAutoSelectedInterpreter(resource: Resource, interpreter: PythonEnvironment | undefined) { + const workspaceFolderPath = this.getWorkspacePathKey(resource); + if (workspaceFolderPath === workspacePathNameForGlobalWorkspaces) { + // Update store only if this version is better. + if ( + this.globallyPreferredInterpreter.value && + this.globallyPreferredInterpreter.value.version && + interpreter && + interpreter.version && + compare(this.globallyPreferredInterpreter.value.version.raw, interpreter.version.raw) > 0 + ) { + return; + } + + // Don't pass in manager instance, as we don't want any updates to take place. + await this.globallyPreferredInterpreter.updateValue(interpreter); + this.autoSelectedInterpreterByWorkspace.set(workspaceFolderPath, interpreter); + } else { + const workspaceState = this.getWorkspaceState(resource); + if (workspaceState && interpreter) { + await workspaceState.updateValue(interpreter); + } + this.autoSelectedInterpreterByWorkspace.set(workspaceFolderPath, interpreter); + } + } + protected async initializeStore(resource: Resource) { + const workspaceFolderPath = this.getWorkspacePathKey(resource); + // Since we're initializing for this resource, + // Ensure any cached information for this workspace have been removed. + this.autoSelectedInterpreterByWorkspace.delete(workspaceFolderPath); + if (this.globallyPreferredInterpreter) { + return; + } + await this.clearStoreIfFileIsInvalid(); + } + private async clearStoreIfFileIsInvalid() { + this.globallyPreferredInterpreter = this.stateFactory.createGlobalPersistentState< + PythonEnvironment | undefined + >(preferredGlobalInterpreter, undefined); + if ( + this.globallyPreferredInterpreter.value && + !(await this.fs.fileExists(this.globallyPreferredInterpreter.value.path)) + ) { + await this.globallyPreferredInterpreter.updateValue(undefined); + } + } + private getWorkspacePathKey(resource: Resource): string { + return this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces); + } + private getWorkspaceState(resource: Resource): undefined | IPersistentState { + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + if (!workspaceUri) { + return; + } + const key = `autoSelectedWorkspacePythonInterpreter-${workspaceUri.folderUri.fsPath}`; + return this.stateFactory.createWorkspacePersistentState(key, undefined); + } +} diff --git a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation.ts b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation.ts new file mode 100644 index 000000000000..0a0ba074e3d1 --- /dev/null +++ b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationShell } from '../../../common/application/types'; +import { IBrowserService, Resource } from '../../../common/types'; +import { Common, Interpreters } from '../../../common/utils/localize'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { IInterpreterHelper } from '../../contracts'; +import { isInterpreterLocatedInWorkspace } from '../../helpers'; +import { learnMoreOnInterpreterSecurityURI } from '../constants'; +import { IInterpreterEvaluation, IInterpreterSecurityStorage } from '../types'; + +const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.learnMore(), Common.doNotShowAgain()]; +const telemetrySelections: ['Yes', 'No', 'Learn more', 'Do not show again'] = [ + 'Yes', + 'No', + 'Learn more', + 'Do not show again' +]; + +@injectable() +export class InterpreterEvaluation implements IInterpreterEvaluation { + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IBrowserService) private browserService: IBrowserService, + @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper, + @inject(IInterpreterSecurityStorage) private readonly interpreterSecurityStorage: IInterpreterSecurityStorage + ) {} + + public async evaluateIfInterpreterIsSafe(interpreter: PythonEnvironment, resource: Resource): Promise { + const activeWorkspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource)?.folderUri; + if (!activeWorkspaceUri) { + return true; + } + const isSafe = this.inferValueUsingCurrentState(interpreter, resource); + return isSafe !== undefined ? isSafe : this._inferValueUsingPrompt(activeWorkspaceUri); + } + + public inferValueUsingCurrentState(interpreter: PythonEnvironment, resource: Resource) { + const activeWorkspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource)?.folderUri; + if (!activeWorkspaceUri) { + return true; + } + if (!isInterpreterLocatedInWorkspace(interpreter, activeWorkspaceUri)) { + return true; + } + const isSafe = this.interpreterSecurityStorage.hasUserApprovedWorkspaceInterpreters(activeWorkspaceUri).value; + if (isSafe !== undefined) { + return isSafe; + } + if (!this.interpreterSecurityStorage.unsafeInterpreterPromptEnabled.value) { + // If the prompt is disabled, assume all environments are safe from now on. + return true; + } + } + + public async _inferValueUsingPrompt(activeWorkspaceUri: Uri): Promise { + const areInterpretersInWorkspaceSafe = this.interpreterSecurityStorage.hasUserApprovedWorkspaceInterpreters( + activeWorkspaceUri + ); + await this.interpreterSecurityStorage.storeKeyForWorkspace(activeWorkspaceUri); + let selection = await this.showPromptAndGetSelection(); + while (selection === Common.learnMore()) { + this.browserService.launch(learnMoreOnInterpreterSecurityURI); + selection = await this.showPromptAndGetSelection(); + } + if (!selection || selection === Common.bannerLabelNo()) { + await areInterpretersInWorkspaceSafe.updateValue(false); + return false; + } else if (selection === Common.doNotShowAgain()) { + await this.interpreterSecurityStorage.unsafeInterpreterPromptEnabled.updateValue(false); + } + await areInterpretersInWorkspaceSafe.updateValue(true); + return true; + } + + private async showPromptAndGetSelection(): Promise { + const selection = await this.appShell.showInformationMessage( + Interpreters.unsafeInterpreterMessage(), + ...prompts + ); + sendTelemetryEvent(EventName.UNSAFE_INTERPRETER_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined + }); + return selection; + } +} diff --git a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService.ts b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService.ts new file mode 100644 index 000000000000..72f09e347f7b --- /dev/null +++ b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter } from 'vscode'; +import { Resource } from '../../../common/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { IInterpreterEvaluation, IInterpreterSecurityService, IInterpreterSecurityStorage } from '../types'; + +@injectable() +export class InterpreterSecurityService implements IInterpreterSecurityService { + public _didSafeInterpretersChange = new EventEmitter(); + constructor( + @inject(IInterpreterSecurityStorage) private readonly interpreterSecurityStorage: IInterpreterSecurityStorage, + @inject(IInterpreterEvaluation) private readonly interpreterEvaluation: IInterpreterEvaluation + ) {} + + public isSafe(interpreter: PythonEnvironment, resource?: Resource): boolean | undefined { + const unsafeInterpreters = this.interpreterSecurityStorage.unsafeInterpreters.value; + if (unsafeInterpreters.includes(interpreter.path)) { + return false; + } + const safeInterpreters = this.interpreterSecurityStorage.safeInterpreters.value; + if (safeInterpreters.includes(interpreter.path)) { + return true; + } + return this.interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + } + + public async evaluateAndRecordInterpreterSafety(interpreter: PythonEnvironment, resource: Resource): Promise { + const unsafeInterpreters = this.interpreterSecurityStorage.unsafeInterpreters.value; + const safeInterpreters = this.interpreterSecurityStorage.safeInterpreters.value; + if (unsafeInterpreters.includes(interpreter.path) || safeInterpreters.includes(interpreter.path)) { + return; + } + const isSafe = await this.interpreterEvaluation.evaluateIfInterpreterIsSafe(interpreter, resource); + if (isSafe) { + await this.interpreterSecurityStorage.safeInterpreters.updateValue([interpreter.path, ...safeInterpreters]); + } else { + await this.interpreterSecurityStorage.unsafeInterpreters.updateValue([ + interpreter.path, + ...unsafeInterpreters + ]); + } + this._didSafeInterpretersChange.fire(); + } + + public get onDidChangeSafeInterpreters(): Event { + return this._didSafeInterpretersChange.event; + } +} diff --git a/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts new file mode 100644 index 000000000000..f75317a859cb --- /dev/null +++ b/src/client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { ICommandManager, IWorkspaceService } from '../../../common/application/types'; +import { Commands } from '../../../common/constants'; +import { IDisposable, IDisposableRegistry, IPersistentState, IPersistentStateFactory } from '../../../common/types'; +import { + flaggedWorkspacesKeysStorageKey, + safeInterpretersKey, + unsafeInterpreterPromptKey, + unsafeInterpretersKey +} from '../constants'; +import { IInterpreterSecurityStorage } from '../types'; + +@injectable() +export class InterpreterSecurityStorage implements IInterpreterSecurityStorage { + public get unsafeInterpreterPromptEnabled(): IPersistentState { + return this._unsafeInterpreterPromptEnabled; + } + public get unsafeInterpreters(): IPersistentState { + return this._unsafeInterpreters; + } + public get safeInterpreters(): IPersistentState { + return this._safeInterpreters; + } + private _unsafeInterpreterPromptEnabled: IPersistentState; + private _unsafeInterpreters: IPersistentState; + private _safeInterpreters: IPersistentState; + private flaggedWorkspacesKeysStorage: IPersistentState; + + constructor( + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposable[] + ) { + this._unsafeInterpreterPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + unsafeInterpreterPromptKey, + true + ); + this._unsafeInterpreters = this.persistentStateFactory.createGlobalPersistentState( + unsafeInterpretersKey, + [] + ); + this._safeInterpreters = this.persistentStateFactory.createGlobalPersistentState( + safeInterpretersKey, + [] + ); + this.flaggedWorkspacesKeysStorage = this.persistentStateFactory.createGlobalPersistentState( + flaggedWorkspacesKeysStorageKey, + [] + ); + } + + public hasUserApprovedWorkspaceInterpreters(resource: Uri): IPersistentState { + return this.persistentStateFactory.createGlobalPersistentState( + this._getKeyForWorkspace(resource), + undefined + ); + } + + public async activate(): Promise { + this.disposables.push( + this.commandManager.registerCommand( + Commands.ResetInterpreterSecurityStorage, + this.resetInterpreterSecurityStorage.bind(this) + ) + ); + } + + public async resetInterpreterSecurityStorage(): Promise { + this.flaggedWorkspacesKeysStorage.value.forEach(async (key) => { + const areInterpretersInWorkspaceSafe = this.persistentStateFactory.createGlobalPersistentState< + boolean | undefined + >(key, undefined); + await areInterpretersInWorkspaceSafe.updateValue(undefined); + }); + await this.flaggedWorkspacesKeysStorage.updateValue([]); + await this._safeInterpreters.updateValue([]); + await this._unsafeInterpreters.updateValue([]); + await this._unsafeInterpreterPromptEnabled.updateValue(true); + } + + public _getKeyForWorkspace(resource: Uri): string { + return `ARE_INTERPRETERS_SAFE_FOR_WS_${this.workspaceService.getWorkspaceFolderIdentifier(resource)}`; + } + + public async storeKeyForWorkspace(resource: Uri): Promise { + const key = this._getKeyForWorkspace(resource); + const flaggedWorkspacesKeys = this.flaggedWorkspacesKeysStorage.value; + if (!flaggedWorkspacesKeys.includes(key)) { + await this.flaggedWorkspacesKeysStorage.updateValue([key, ...flaggedWorkspacesKeys]); + } + } +} diff --git a/src/client/interpreter/autoSelection/proxy.ts b/src/client/interpreter/autoSelection/proxy.ts new file mode 100644 index 000000000000..7fef629f1463 --- /dev/null +++ b/src/client/interpreter/autoSelection/proxy.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { IAsyncDisposableRegistry, IDisposableRegistry, Resource } from '../../common/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { IInterpreterAutoSeletionProxyService } from './types'; + +@injectable() +export class InterpreterAutoSeletionProxyService implements IInterpreterAutoSeletionProxyService { + private readonly didAutoSelectedInterpreterEmitter = new EventEmitter(); + private instance?: IInterpreterAutoSeletionProxyService; + constructor(@inject(IDisposableRegistry) private readonly disposables: IAsyncDisposableRegistry) {} + public registerInstance(instance: IInterpreterAutoSeletionProxyService): void { + this.instance = instance; + this.disposables.push( + this.instance.onDidChangeAutoSelectedInterpreter(() => this.didAutoSelectedInterpreterEmitter.fire()) + ); + } + public get onDidChangeAutoSelectedInterpreter(): Event { + return this.didAutoSelectedInterpreterEmitter.event; + } + public getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined { + return this.instance ? this.instance.getAutoSelectedInterpreter(resource) : undefined; + } + public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonEnvironment | undefined): Promise { + return this.instance ? this.instance.setWorkspaceInterpreter(resource, interpreter) : undefined; + } +} diff --git a/src/client/interpreter/autoSelection/rules/baseRule.ts b/src/client/interpreter/autoSelection/rules/baseRule.ts new file mode 100644 index 000000000000..5edae0a6c3e4 --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/baseRule.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, unmanaged } from 'inversify'; +import { compare } from 'semver'; +import '../../../common/extensions'; +import { traceDecorators, traceVerbose } from '../../../common/logger'; +import { IFileSystem } from '../../../common/platform/types'; +import { IPersistentState, IPersistentStateFactory, Resource } from '../../../common/types'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; + +export enum NextAction { + runNextRule = 'runNextRule', + exit = 'exit' +} + +@injectable() +export abstract class BaseRuleService implements IInterpreterAutoSelectionRule { + protected nextRule?: IInterpreterAutoSelectionRule; + private readonly stateStore: IPersistentState; + constructor( + @unmanaged() protected readonly ruleName: AutoSelectionRule, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory + ) { + this.stateStore = stateFactory.createGlobalPersistentState( + `InterpreterAutoSeletionRule-${this.ruleName}`, + undefined + ); + } + public setNextRule(rule: IInterpreterAutoSelectionRule): void { + this.nextRule = rule; + } + @traceDecorators.verbose('autoSelectInterpreter') + public async autoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + await this.clearCachedInterpreterIfInvalid(resource); + const stopWatch = new StopWatch(); + const action = await this.onAutoSelectInterpreter(resource, manager); + traceVerbose(`Rule = ${this.ruleName}, result = ${action}`); + const identified = action === NextAction.runNextRule; + sendTelemetryEvent( + EventName.PYTHON_INTERPRETER_AUTO_SELECTION, + { elapsedTime: stopWatch.elapsedTime }, + { rule: this.ruleName, identified } + ); + if (action === NextAction.runNextRule) { + await this.next(resource, manager); + } + } + public getPreviouslyAutoSelectedInterpreter(_resource: Resource): PythonEnvironment | undefined { + const value = this.stateStore.value; + traceVerbose(`Current value for rule ${this.ruleName} is ${value ? JSON.stringify(value) : 'nothing'}`); + return value; + } + protected abstract onAutoSelectInterpreter( + resource: Resource, + manager?: IInterpreterAutoSelectionService + ): Promise; + @traceDecorators.verbose('setGlobalInterpreter') + protected async setGlobalInterpreter( + interpreter?: PythonEnvironment, + manager?: IInterpreterAutoSelectionService + ): Promise { + await this.cacheSelectedInterpreter(undefined, interpreter); + if (!interpreter || !manager || !interpreter.version) { + return false; + } + const preferredInterpreter = manager.getAutoSelectedInterpreter(undefined); + const comparison = + preferredInterpreter && preferredInterpreter.version + ? compare(interpreter.version.raw, preferredInterpreter.version.raw) + : 1; + if (comparison > 0) { + await manager.setGlobalInterpreter(interpreter); + return true; + } + if (comparison === 0) { + return true; + } + + return false; + } + protected async clearCachedInterpreterIfInvalid(resource: Resource) { + if (!this.stateStore.value || (await this.fs.fileExists(this.stateStore.value.path))) { + return; + } + sendTelemetryEvent( + EventName.PYTHON_INTERPRETER_AUTO_SELECTION, + {}, + { rule: this.ruleName, interpreterMissing: true } + ); + await this.cacheSelectedInterpreter(resource, undefined); + } + protected async cacheSelectedInterpreter(_resource: Resource, interpreter: PythonEnvironment | undefined) { + const interpreterPath = interpreter ? interpreter.path : ''; + const interpreterPathInCache = this.stateStore.value ? this.stateStore.value.path : ''; + const updated = interpreterPath === interpreterPathInCache; + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, {}, { rule: this.ruleName, updated }); + await this.stateStore.updateValue(interpreter); + } + protected async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { + traceVerbose(`Executing next rule from ${this.ruleName}`); + return this.nextRule && manager ? this.nextRule.autoSelectInterpreter(resource, manager) : undefined; + } +} diff --git a/src/client/interpreter/autoSelection/rules/cached.ts b/src/client/interpreter/autoSelection/rules/cached.ts new file mode 100644 index 000000000000..134a53203051 --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/cached.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { traceVerbose } from '../../../common/logger'; +import { IFileSystem } from '../../../common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { IInterpreterHelper } from '../../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class CachedInterpretersAutoSelectionRule extends BaseRuleService { + protected readonly rules: IInterpreterAutoSelectionRule[]; + constructor( + @inject(IFileSystem) fs: IFileSystem, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IInterpreterAutoSelectionRule) + @named(AutoSelectionRule.systemWide) + systemInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) + @named(AutoSelectionRule.currentPath) + currentPathInterpreter: IInterpreterAutoSelectionRule, + @inject(IInterpreterAutoSelectionRule) + @named(AutoSelectionRule.windowsRegistry) + winRegInterpreter: IInterpreterAutoSelectionRule + ) { + super(AutoSelectionRule.cachedInterpreters, fs, stateFactory); + this.rules = [systemInterpreter, currentPathInterpreter, winRegInterpreter]; + } + protected async onAutoSelectInterpreter( + resource: Resource, + manager?: IInterpreterAutoSelectionService + ): Promise { + const cachedInterpreters = this.rules + .map((item) => item.getPreviouslyAutoSelectedInterpreter(resource)) + .filter((item) => !!item) + .map((item) => item!); + const bestInterpreter = this.helper.getBestInterpreter(cachedInterpreters); + traceVerbose( + `Selected Interpreter from ${this.ruleName}, ${ + bestInterpreter ? JSON.stringify(bestInterpreter) : 'Nothing Selected' + }` + ); + return (await this.setGlobalInterpreter(bestInterpreter, manager)) ? NextAction.exit : NextAction.runNextRule; + } +} diff --git a/src/client/interpreter/autoSelection/rules/currentPath.ts b/src/client/interpreter/autoSelection/rules/currentPath.ts new file mode 100644 index 000000000000..a45f9cbbc98b --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/currentPath.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { traceVerbose } from '../../../common/logger'; +import { IFileSystem } from '../../../common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { CURRENT_PATH_SERVICE, IInterpreterHelper, IInterpreterLocatorService } from '../../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class CurrentPathInterpretersAutoSelectionRule extends BaseRuleService { + constructor( + @inject(IFileSystem) fs: IFileSystem, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IInterpreterLocatorService) + @named(CURRENT_PATH_SERVICE) + private readonly currentPathInterpreterLocator: IInterpreterLocatorService + ) { + super(AutoSelectionRule.currentPath, fs, stateFactory); + } + protected async onAutoSelectInterpreter( + resource: Resource, + manager?: IInterpreterAutoSelectionService + ): Promise { + const interpreters = await this.currentPathInterpreterLocator.getInterpreters(resource); + const bestInterpreter = this.helper.getBestInterpreter(interpreters); + traceVerbose( + `Selected Interpreter from ${this.ruleName}, ${ + bestInterpreter ? JSON.stringify(bestInterpreter) : 'Nothing Selected' + }` + ); + return (await this.setGlobalInterpreter(bestInterpreter, manager)) ? NextAction.exit : NextAction.runNextRule; + } +} diff --git a/src/client/interpreter/autoSelection/rules/settings.ts b/src/client/interpreter/autoSelection/rules/settings.ts new file mode 100644 index 000000000000..eae85e915ca2 --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/settings.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IWorkspaceService } from '../../../common/application/types'; +import { DeprecatePythonPath } from '../../../common/experiments/groups'; +import { IFileSystem } from '../../../common/platform/types'; +import { IExperimentsManager, IInterpreterPathService, IPersistentStateFactory, Resource } from '../../../common/types'; +import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class SettingsInterpretersAutoSelectionRule extends BaseRuleService { + constructor( + @inject(IFileSystem) fs: IFileSystem, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IExperimentsManager) private readonly experiments: IExperimentsManager, + @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService + ) { + super(AutoSelectionRule.settings, fs, stateFactory); + } + protected async onAutoSelectInterpreter( + _resource: Resource, + _manager?: IInterpreterAutoSelectionService + ): Promise { + // tslint:disable-next-line:no-any + const pythonConfig = this.workspaceService.getConfiguration('python', null as any)!; + const pythonPathInConfig = this.experiments.inExperiment(DeprecatePythonPath.experiment) + ? this.interpreterPathService.inspect(undefined) + : pythonConfig.inspect('pythonPath')!; + this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + // No need to store python paths defined in settings in our caches, they can be retrieved from the settings directly. + return pythonPathInConfig.globalValue && pythonPathInConfig.globalValue !== 'python' + ? NextAction.exit + : NextAction.runNextRule; + } +} diff --git a/src/client/interpreter/autoSelection/rules/system.ts b/src/client/interpreter/autoSelection/rules/system.ts new file mode 100644 index 000000000000..00be6177cdff --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/system.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { traceVerbose } from '../../../common/logger'; +import { IFileSystem } from '../../../common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { EnvironmentType } from '../../../pythonEnvironments/info'; +import { IInterpreterHelper, IInterpreterService } from '../../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class SystemWideInterpretersAutoSelectionRule extends BaseRuleService { + constructor( + @inject(IFileSystem) fs: IFileSystem, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService + ) { + super(AutoSelectionRule.systemWide, fs, stateFactory); + } + protected async onAutoSelectInterpreter( + resource: Resource, + manager?: IInterpreterAutoSelectionService + ): Promise { + const interpreters = await this.interpreterService.getInterpreters(resource); + // Exclude non-local interpreters. + const filteredInterpreters = interpreters.filter( + (int) => + int.envType !== EnvironmentType.VirtualEnv && + int.envType !== EnvironmentType.Venv && + int.envType !== EnvironmentType.Pipenv + ); + const bestInterpreter = this.helper.getBestInterpreter(filteredInterpreters); + traceVerbose( + `Selected Interpreter from ${this.ruleName}, ${ + bestInterpreter ? JSON.stringify(bestInterpreter) : 'Nothing Selected' + }` + ); + return (await this.setGlobalInterpreter(bestInterpreter, manager)) ? NextAction.exit : NextAction.runNextRule; + } +} diff --git a/src/client/interpreter/autoSelection/rules/winRegistry.ts b/src/client/interpreter/autoSelection/rules/winRegistry.ts new file mode 100644 index 000000000000..2442e52eaf24 --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/winRegistry.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { traceVerbose } from '../../../common/logger'; +import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../common/types'; +import { OSType } from '../../../common/utils/platform'; +import { IInterpreterHelper, IInterpreterLocatorService, WINDOWS_REGISTRY_SERVICE } from '../../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class WindowsRegistryInterpretersAutoSelectionRule extends BaseRuleService { + constructor( + @inject(IFileSystem) fs: IFileSystem, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IInterpreterLocatorService) + @named(WINDOWS_REGISTRY_SERVICE) + private winRegInterpreterLocator: IInterpreterLocatorService + ) { + super(AutoSelectionRule.windowsRegistry, fs, stateFactory); + } + protected async onAutoSelectInterpreter( + resource: Resource, + manager?: IInterpreterAutoSelectionService + ): Promise { + if (this.platform.osType !== OSType.Windows) { + return NextAction.runNextRule; + } + const interpreters = await this.winRegInterpreterLocator.getInterpreters(resource); + const bestInterpreter = this.helper.getBestInterpreter(interpreters); + traceVerbose( + `Selected Interpreter from ${this.ruleName}, ${ + bestInterpreter ? JSON.stringify(bestInterpreter) : 'Nothing Selected' + }` + ); + return (await this.setGlobalInterpreter(bestInterpreter, manager)) ? NextAction.exit : NextAction.runNextRule; + } +} diff --git a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts b/src/client/interpreter/autoSelection/rules/workspaceEnv.ts new file mode 100644 index 000000000000..735965dc2344 --- /dev/null +++ b/src/client/interpreter/autoSelection/rules/workspaceEnv.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { DeprecatePythonPath } from '../../../common/experiments/groups'; +import { traceVerbose } from '../../../common/logger'; +import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { IExperimentsManager, IInterpreterPathService, IPersistentStateFactory, Resource } from '../../../common/types'; +import { createDeferredFromPromise } from '../../../common/utils/async'; +import { OSType } from '../../../common/utils/platform'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { IInterpreterHelper, IInterpreterLocatorService, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../../contracts'; +import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; +import { BaseRuleService, NextAction } from './baseRule'; + +@injectable() +export class WorkspaceVirtualEnvInterpretersAutoSelectionRule extends BaseRuleService { + constructor( + @inject(IFileSystem) fs: IFileSystem, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IInterpreterLocatorService) + @named(WORKSPACE_VIRTUAL_ENV_SERVICE) + private readonly workspaceVirtualEnvInterpreterLocator: IInterpreterLocatorService, + @inject(IExperimentsManager) private readonly experiments: IExperimentsManager, + @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService + ) { + super(AutoSelectionRule.workspaceVirtualEnvs, fs, stateFactory); + } + protected async onAutoSelectInterpreter( + resource: Resource, + manager?: IInterpreterAutoSelectionService + ): Promise { + const workspacePath = this.helper.getActiveWorkspaceUri(resource); + if (!workspacePath) { + return NextAction.runNextRule; + } + + const pythonConfig = this.workspaceService.getConfiguration('python', workspacePath.folderUri)!; + const pythonPathInConfig = this.experiments.inExperiment(DeprecatePythonPath.experiment) + ? this.interpreterPathService.inspect(workspacePath.folderUri) + : pythonConfig.inspect('pythonPath')!; + this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + // If user has defined custom values in settings for this workspace folder, then use that. + if (pythonPathInConfig.workspaceFolderValue || pythonPathInConfig.workspaceValue) { + return NextAction.runNextRule; + } + const virtualEnvPromise = createDeferredFromPromise( + this.getWorkspaceVirtualEnvInterpreters(workspacePath.folderUri) + ); + + const interpreters = await virtualEnvPromise.promise; + const bestInterpreter = + Array.isArray(interpreters) && interpreters.length > 0 + ? this.helper.getBestInterpreter(interpreters) + : undefined; + + if (bestInterpreter && manager) { + await super.cacheSelectedInterpreter(workspacePath.folderUri, bestInterpreter); + await manager.setWorkspaceInterpreter(workspacePath.folderUri!, bestInterpreter); + } + + traceVerbose( + `Selected Interpreter from ${this.ruleName}, ${ + bestInterpreter ? JSON.stringify(bestInterpreter) : 'Nothing Selected' + }` + ); + return NextAction.runNextRule; + } + protected async getWorkspaceVirtualEnvInterpreters(resource: Resource): Promise { + if (!resource) { + return; + } + const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + if (!workspaceFolder) { + return; + } + // Now check virtual environments under the workspace root + const interpreters = await this.workspaceVirtualEnvInterpreterLocator.getInterpreters(resource, { + ignoreCache: true + }); + const workspacePath = + this.platform.osType === OSType.Windows + ? workspaceFolder.uri.fsPath.toUpperCase() + : workspaceFolder.uri.fsPath; + + return interpreters.filter((interpreter) => { + const fsPath = Uri.file(interpreter.path).fsPath; + const fsPathToCompare = this.platform.osType === OSType.Windows ? fsPath.toUpperCase() : fsPath; + return fsPathToCompare.startsWith(workspacePath); + }); + } +} diff --git a/src/client/interpreter/autoSelection/types.ts b/src/client/interpreter/autoSelection/types.ts new file mode 100644 index 000000000000..03b952ece6f0 --- /dev/null +++ b/src/client/interpreter/autoSelection/types.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Event, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IPersistentState, Resource } from '../../common/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; + +export const IInterpreterAutoSeletionProxyService = Symbol('IInterpreterAutoSeletionProxyService'); +/** + * Interface similar to IInterpreterAutoSelectionService, to avoid chickn n egg situation. + * Do we get python path from config first or get auto selected interpreter first!? + * However, the class that reads python Path, must first give preference to selected interpreter. + * But all classes everywhere make use of python settings! + * Solution - Use a proxy that does nothing first, but later the real instance is injected. + * + * @export + * @interface IInterpreterAutoSeletionProxyService + */ +export interface IInterpreterAutoSeletionProxyService { + readonly onDidChangeAutoSelectedInterpreter: Event; + getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined; + registerInstance?(instance: IInterpreterAutoSeletionProxyService): void; + setWorkspaceInterpreter(resource: Uri, interpreter: PythonEnvironment | undefined): Promise; +} + +export const IInterpreterAutoSelectionService = Symbol('IInterpreterAutoSelectionService'); +export interface IInterpreterAutoSelectionService extends IInterpreterAutoSeletionProxyService { + readonly onDidChangeAutoSelectedInterpreter: Event; + autoSelectInterpreter(resource: Resource): Promise; + getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined; + setGlobalInterpreter(interpreter: PythonEnvironment | undefined): Promise; +} + +export enum AutoSelectionRule { + all = 'all', + currentPath = 'currentPath', + workspaceVirtualEnvs = 'workspaceEnvs', + settings = 'settings', + cachedInterpreters = 'cachedInterpreters', + systemWide = 'system', + windowsRegistry = 'windowsRegistry' +} + +export const IInterpreterAutoSelectionRule = Symbol('IInterpreterAutoSelectionRule'); +export interface IInterpreterAutoSelectionRule { + setNextRule(rule: IInterpreterAutoSelectionRule): void; + autoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise; + getPreviouslyAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined; +} + +export const IInterpreterSecurityService = Symbol('IInterpreterSecurityService'); +export interface IInterpreterSecurityService { + readonly onDidChangeSafeInterpreters: Event; + evaluateAndRecordInterpreterSafety(interpreter: PythonEnvironment, resource: Resource): Promise; + isSafe(interpreter: PythonEnvironment, resource?: Resource): boolean | undefined; +} + +export const IInterpreterSecurityStorage = Symbol('IInterpreterSecurityStorage'); +export interface IInterpreterSecurityStorage extends IExtensionSingleActivationService { + readonly unsafeInterpreterPromptEnabled: IPersistentState; + readonly unsafeInterpreters: IPersistentState; + readonly safeInterpreters: IPersistentState; + hasUserApprovedWorkspaceInterpreters(resource: Uri): IPersistentState; + storeKeyForWorkspace(resource: Uri): Promise; +} + +export const IInterpreterEvaluation = Symbol('IInterpreterEvaluation'); +export interface IInterpreterEvaluation { + evaluateIfInterpreterIsSafe(interpreter: PythonEnvironment, resource: Resource): Promise; + inferValueUsingCurrentState(interpreter: PythonEnvironment, resource: Resource): boolean | undefined; +} diff --git a/src/client/interpreter/configuration/interpreterComparer.ts b/src/client/interpreter/configuration/interpreterComparer.ts new file mode 100644 index 000000000000..b309050590a3 --- /dev/null +++ b/src/client/interpreter/configuration/interpreterComparer.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { getArchitectureDisplayName } from '../../common/platform/registry'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { IInterpreterHelper } from '../contracts'; +import { IInterpreterComparer } from './types'; + +@injectable() +export class InterpreterComparer implements IInterpreterComparer { + constructor(@inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper) {} + public compare(a: PythonEnvironment, b: PythonEnvironment): number { + const nameA = this.getSortName(a); + const nameB = this.getSortName(b); + if (nameA === nameB) { + return 0; + } + return nameA > nameB ? 1 : -1; + } + private getSortName(info: PythonEnvironment): string { + const sortNameParts: string[] = []; + const envSuffixParts: string[] = []; + + // Sort order for interpreters is: + // * Version + // * Architecture + // * Interpreter Type + // * Environment name + if (info.version) { + sortNameParts.push(info.version.raw); + } + if (info.architecture) { + sortNameParts.push(getArchitectureDisplayName(info.architecture)); + } + if (info.companyDisplayName && info.companyDisplayName.length > 0) { + sortNameParts.push(info.companyDisplayName.trim()); + } else { + sortNameParts.push('Python'); + } + + if (info.envType) { + const name = this.interpreterHelper.getInterpreterTypeDisplayName(info.envType); + if (name) { + envSuffixParts.push(name); + } + } + if (info.envName && info.envName.length > 0) { + envSuffixParts.push(info.envName); + } + + const envSuffix = envSuffixParts.length === 0 ? '' : `(${envSuffixParts.join(': ')})`; + return `${sortNameParts.join(' ')} ${envSuffix}`.trim(); + } +} diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/base.ts b/src/client/interpreter/configuration/interpreterSelector/commands/base.ts new file mode 100644 index 000000000000..d47bcab9aff5 --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/commands/base.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable, unmanaged } from 'inversify'; +import * as path from 'path'; +import { ConfigurationTarget, Disposable, QuickPickItem, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../../../activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../common/application/types'; +import { IDisposable, Resource } from '../../../../common/types'; +import { Interpreters } from '../../../../common/utils/localize'; +import { IPythonPathUpdaterServiceManager } from '../../types'; + +@injectable() +export abstract class BaseInterpreterSelectorCommand implements IExtensionSingleActivationService, IDisposable { + protected disposables: Disposable[] = []; + constructor( + @unmanaged() protected readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @unmanaged() protected readonly commandManager: ICommandManager, + @unmanaged() protected readonly applicationShell: IApplicationShell, + @unmanaged() protected readonly workspaceService: IWorkspaceService + ) { + this.disposables.push(this); + } + + public dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + + public abstract async activate(): Promise; + + protected async getConfigTarget(): Promise< + | { + folderUri: Resource; + configTarget: ConfigurationTarget; + } + | undefined + > { + if ( + !Array.isArray(this.workspaceService.workspaceFolders) || + this.workspaceService.workspaceFolders.length === 0 + ) { + return { + folderUri: undefined, + configTarget: ConfigurationTarget.Global + }; + } + if (!this.workspaceService.workspaceFile && this.workspaceService.workspaceFolders.length === 1) { + return { + folderUri: this.workspaceService.workspaceFolders[0].uri, + configTarget: ConfigurationTarget.WorkspaceFolder + }; + } + + // Ok we have multiple workspaces, get the user to pick a folder. + + type WorkspaceSelectionQuickPickItem = QuickPickItem & { uri: Uri }; + const quickPickItems: WorkspaceSelectionQuickPickItem[] = [ + ...this.workspaceService.workspaceFolders.map((w) => ({ + label: w.name, + description: path.dirname(w.uri.fsPath), + uri: w.uri + })), + { + label: Interpreters.entireWorkspace(), + uri: this.workspaceService.workspaceFolders[0].uri + } + ]; + + const selection = await this.applicationShell.showQuickPick(quickPickItems, { + placeHolder: 'Select the workspace to set the interpreter' + }); + + return selection + ? selection.label === Interpreters.entireWorkspace() + ? { folderUri: selection.uri, configTarget: ConfigurationTarget.Workspace } + : { folderUri: selection.uri, configTarget: ConfigurationTarget.WorkspaceFolder } + : undefined; + } +} diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts new file mode 100644 index 000000000000..b605b15389be --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../common/application/types'; +import { Commands } from '../../../../common/constants'; +import { IPythonPathUpdaterServiceManager } from '../../types'; +import { BaseInterpreterSelectorCommand } from './base'; + +@injectable() +export class ResetInterpreterCommand extends BaseInterpreterSelectorCommand { + constructor( + @inject(IPythonPathUpdaterServiceManager) pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(IApplicationShell) applicationShell: IApplicationShell, + @inject(IWorkspaceService) workspaceService: IWorkspaceService + ) { + super(pythonPathUpdaterService, commandManager, applicationShell, workspaceService); + } + + public async activate() { + this.disposables.push( + this.commandManager.registerCommand(Commands.ClearWorkspaceInterpreter, this.resetInterpreter.bind(this)) + ); + } + + public async resetInterpreter() { + const targetConfig = await this.getConfigTarget(); + if (!targetConfig) { + return; + } + const configTarget = targetConfig.configTarget; + const wkspace = targetConfig.folderUri; + + await this.pythonPathUpdaterService.updatePythonPath(undefined, configTarget, 'ui', wkspace); + } +} diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts new file mode 100644 index 000000000000..3c021195ef31 --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { QuickPickItem } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../common/application/types'; +import { Commands } from '../../../../common/constants'; +import { IPlatformService } from '../../../../common/platform/types'; +import { IConfigurationService, IPathUtils, Resource } from '../../../../common/types'; +import { InterpreterQuickPickList } from '../../../../common/utils/localize'; +import { + IMultiStepInput, + IMultiStepInputFactory, + InputStep, + IQuickPickParameters +} from '../../../../common/utils/multiStepInput'; +import { captureTelemetry, sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { IInterpreterQuickPickItem, IInterpreterSelector, IPythonPathUpdaterServiceManager } from '../../types'; +import { BaseInterpreterSelectorCommand } from './base'; + +export type InterpreterStateArgs = { path?: string; workspace: Resource }; +@injectable() +export class SetInterpreterCommand extends BaseInterpreterSelectorCommand { + constructor( + @inject(IApplicationShell) applicationShell: IApplicationShell, + @inject(IPathUtils) private readonly pathUtils: IPathUtils, + @inject(IPythonPathUpdaterServiceManager) + pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, + @inject(IWorkspaceService) workspaceService: IWorkspaceService + ) { + super(pythonPathUpdaterService, commandManager, applicationShell, workspaceService); + } + + public async activate() { + this.disposables.push( + this.commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this)) + ); + } + + public async _pickInterpreter( + input: IMultiStepInput, + state: InterpreterStateArgs + ): Promise> { + const interpreterSuggestions = await this.interpreterSelector.getSuggestions(state.workspace); + const enterInterpreterPathSuggestion = { + label: InterpreterQuickPickList.enterPath.label(), + detail: InterpreterQuickPickList.enterPath.detail(), + alwaysShow: true + }; + const suggestions = [enterInterpreterPathSuggestion, ...interpreterSuggestions]; + const currentPythonPath = this.pathUtils.getDisplayName( + this.configurationService.getSettings(state.workspace).pythonPath, + state.workspace ? state.workspace.fsPath : undefined + ); + + state.path = undefined; + const selection = await input.showQuickPick< + IInterpreterQuickPickItem | typeof enterInterpreterPathSuggestion, + IQuickPickParameters + >({ + placeholder: InterpreterQuickPickList.quickPickListPlaceholder().format(currentPythonPath), + items: suggestions, + activeItem: suggestions[1], + matchOnDetail: true, + matchOnDescription: true + }); + + if (selection === undefined) { + return; + } else if (selection.label === enterInterpreterPathSuggestion.label) { + return this._enterOrBrowseInterpreterPath(input, state); + } else { + state.path = (selection as IInterpreterQuickPickItem).path; + } + } + + @captureTelemetry(EventName.SELECT_INTERPRETER_ENTER_BUTTON) + public async _enterOrBrowseInterpreterPath( + input: IMultiStepInput, + state: InterpreterStateArgs + ): Promise> { + const items: QuickPickItem[] = [ + { + label: InterpreterQuickPickList.browsePath.label(), + detail: InterpreterQuickPickList.browsePath.detail() + } + ]; + + const selection = await input.showQuickPick({ + placeholder: InterpreterQuickPickList.enterPath.placeholder(), + items, + acceptFilterBoxTextAsSelection: true + }); + + if (typeof selection === 'string') { + // User entered text in the filter box to enter path to python, store it + sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTER_CHOICE, undefined, { choice: 'enter' }); + state.path = selection; + } else if (selection && selection.label === InterpreterQuickPickList.browsePath.label()) { + sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTER_CHOICE, undefined, { choice: 'browse' }); + const filtersKey = 'Executables'; + const filtersObject: { [name: string]: string[] } = {}; + filtersObject[filtersKey] = ['exe']; + const uris = await this.applicationShell.showOpenDialog({ + filters: this.platformService.isWindows ? filtersObject : undefined, + openLabel: InterpreterQuickPickList.browsePath.openButtonLabel(), + canSelectMany: false, + title: InterpreterQuickPickList.browsePath.title() + }); + if (uris && uris.length > 0) { + state.path = uris[0].fsPath; + } + } + } + + @captureTelemetry(EventName.SELECT_INTERPRETER) + public async setInterpreter() { + const targetConfig = await this.getConfigTarget(); + if (!targetConfig) { + return; + } + const configTarget = targetConfig.configTarget; + const wkspace = targetConfig.folderUri; + const interpreterState: InterpreterStateArgs = { path: undefined, workspace: wkspace }; + const multiStep = this.multiStepFactory.create(); + await multiStep.run((input, s) => this._pickInterpreter(input, s), interpreterState); + if (interpreterState.path !== undefined) { + // User may choose to have an empty string stored, so variable `interpreterState.path` may be + // an empty string, in which case we should update. + // Having the value `undefined` means user cancelled the quickpick, so we update nothing in that case. + await this.pythonPathUpdaterService.updatePythonPath(interpreterState.path, configTarget, 'ui', wkspace); + } + } +} diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts new file mode 100644 index 000000000000..e3e63b299f83 --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ConfigurationTarget } from 'vscode'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService +} from '../../../../common/application/types'; +import { Commands } from '../../../../common/constants'; +import { IShebangCodeLensProvider } from '../../../contracts'; +import { IPythonPathUpdaterServiceManager } from '../../types'; +import { BaseInterpreterSelectorCommand } from './base'; + +@injectable() +export class SetShebangInterpreterCommand extends BaseInterpreterSelectorCommand { + constructor( + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IDocumentManager) private readonly documentManager: IDocumentManager, + @inject(IPythonPathUpdaterServiceManager) pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @inject(IShebangCodeLensProvider) private readonly shebangCodeLensProvider: IShebangCodeLensProvider, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(ICommandManager) applicationShell: IApplicationShell + ) { + super(pythonPathUpdaterService, commandManager, applicationShell, workspaceService); + } + + public async activate() { + this.disposables.push( + this.commandManager.registerCommand(Commands.Set_ShebangInterpreter, this.setShebangInterpreter.bind(this)) + ); + } + + protected async setShebangInterpreter(): Promise { + const shebang = await this.shebangCodeLensProvider.detectShebang( + this.documentManager.activeTextEditor!.document, + true + ); + if (!shebang) { + return; + } + + const isGlobalChange = + !Array.isArray(this.workspaceService.workspaceFolders) || + this.workspaceService.workspaceFolders.length === 0; + const workspaceFolder = this.workspaceService.getWorkspaceFolder( + this.documentManager.activeTextEditor!.document.uri + ); + const isWorkspaceChange = + Array.isArray(this.workspaceService.workspaceFolders) && + this.workspaceService.workspaceFolders.length === 1; + + if (isGlobalChange) { + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Global, 'shebang'); + return; + } + + if (isWorkspaceChange || !workspaceFolder) { + await this.pythonPathUpdaterService.updatePythonPath( + shebang, + ConfigurationTarget.Workspace, + 'shebang', + this.workspaceService.workspaceFolders![0].uri + ); + return; + } + + await this.pythonPathUpdaterService.updatePythonPath( + shebang, + ConfigurationTarget.WorkspaceFolder, + 'shebang', + workspaceFolder.uri + ); + } +} diff --git a/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts new file mode 100644 index 000000000000..e8f9a7356e31 --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable, Uri } from 'vscode'; +import { DeprecatePythonPath } from '../../../common/experiments/groups'; +import { IExperimentsManager, IPathUtils, Resource } from '../../../common/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { IInterpreterSecurityService } from '../../autoSelection/types'; +import { IInterpreterService } from '../../contracts'; +import { IInterpreterComparer, IInterpreterQuickPickItem, IInterpreterSelector } from '../types'; + +@injectable() +export class InterpreterSelector implements IInterpreterSelector { + private disposables: Disposable[] = []; + + constructor( + @inject(IInterpreterService) private readonly interpreterManager: IInterpreterService, + @inject(IInterpreterComparer) private readonly interpreterComparer: IInterpreterComparer, + @inject(IExperimentsManager) private readonly experimentsManager: IExperimentsManager, + @inject(IInterpreterSecurityService) private readonly interpreterSecurityService: IInterpreterSecurityService, + @inject(IPathUtils) private readonly pathUtils: IPathUtils + ) {} + public dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + + public async getSuggestions(resource: Resource) { + let interpreters = await this.interpreterManager.getInterpreters(resource, { onSuggestion: true }); + if (this.experimentsManager.inExperiment(DeprecatePythonPath.experiment)) { + interpreters = interpreters + ? interpreters.filter((item) => this.interpreterSecurityService.isSafe(item) !== false) + : []; + } + this.experimentsManager.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + interpreters.sort(this.interpreterComparer.compare.bind(this.interpreterComparer)); + return Promise.all(interpreters.map((item) => this.suggestionToQuickPickItem(item, resource))); + } + + protected async suggestionToQuickPickItem( + suggestion: PythonEnvironment, + workspaceUri?: Uri + ): Promise { + const detail = this.pathUtils.getDisplayName(suggestion.path, workspaceUri ? workspaceUri.fsPath : undefined); + const cachedPrefix = suggestion.cachedEntry ? '(cached) ' : ''; + return { + // tslint:disable-next-line:no-non-null-assertion + label: suggestion.displayName!, + detail: `${cachedPrefix}${detail}`, + path: suggestion.path, + interpreter: suggestion + }; + } +} diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts new file mode 100644 index 000000000000..8ad4a38f3155 --- /dev/null +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -0,0 +1,103 @@ +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, window } from 'vscode'; +import { traceError } from '../../common/logger'; +import { IPythonExecutionFactory } from '../../common/process/types'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { IServiceContainer } from '../../ioc/types'; +import { InterpreterInformation } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { PythonInterpreterTelemetry } from '../../telemetry/types'; +import { IInterpreterVersionService } from '../contracts'; +import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './types'; + +@injectable() +export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager { + private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory; + private readonly interpreterVersionService: IInterpreterVersionService; + private readonly executionFactory: IPythonExecutionFactory; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.pythonPathSettingsUpdaterFactory = serviceContainer.get( + IPythonPathUpdaterServiceFactory + ); + this.interpreterVersionService = serviceContainer.get(IInterpreterVersionService); + this.executionFactory = serviceContainer.get(IPythonExecutionFactory); + } + public async updatePythonPath( + pythonPath: string | undefined, + configTarget: ConfigurationTarget, + trigger: 'ui' | 'shebang' | 'load', + wkspace?: Uri + ): Promise { + const stopWatch = new StopWatch(); + const pythonPathUpdater = this.getPythonUpdaterService(configTarget, wkspace); + let failed = false; + try { + await pythonPathUpdater.updatePythonPath(pythonPath ? path.normalize(pythonPath) : undefined); + } catch (reason) { + failed = true; + // tslint:disable-next-line:no-unsafe-any prefer-type-cast + const message = reason && typeof reason.message === 'string' ? (reason.message as string) : ''; + window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${message}`); + traceError(reason); + } + // do not wait for this to complete + this.sendTelemetry(stopWatch.elapsedTime, failed, trigger, pythonPath).catch((ex) => + traceError('Python Extension: sendTelemetry', ex) + ); + } + private async sendTelemetry( + duration: number, + failed: boolean, + trigger: 'ui' | 'shebang' | 'load', + pythonPath: string | undefined + ) { + const telemetryProperties: PythonInterpreterTelemetry = { + failed, + trigger + }; + if (!failed && pythonPath) { + const processService = await this.executionFactory.create({ pythonPath }); + const infoPromise = processService + .getInterpreterInformation() + .catch(() => undefined); + const pipVersionPromise = this.interpreterVersionService + .getPipVersion(pythonPath) + .then((value) => (value.length === 0 ? undefined : value)) + .catch(() => ''); + const [info, pipVersion] = await Promise.all([infoPromise, pipVersionPromise]); + if (info) { + telemetryProperties.architecture = info.architecture; + if (info.version) { + telemetryProperties.pythonVersion = info.version.raw; + } + } + if (pipVersion) { + telemetryProperties.pipVersion = pipVersion; + } + } + sendTelemetryEvent(EventName.PYTHON_INTERPRETER, duration, telemetryProperties); + } + private getPythonUpdaterService(configTarget: ConfigurationTarget, wkspace?: Uri) { + switch (configTarget) { + case ConfigurationTarget.Global: { + return this.pythonPathSettingsUpdaterFactory.getGlobalPythonPathConfigurationService(); + } + case ConfigurationTarget.Workspace: { + if (!wkspace) { + throw new Error('Workspace Uri not defined'); + } + // tslint:disable-next-line:no-non-null-assertion + return this.pythonPathSettingsUpdaterFactory.getWorkspacePythonPathConfigurationService(wkspace!); + } + default: { + if (!wkspace) { + throw new Error('Workspace Uri not defined'); + } + // tslint:disable-next-line:no-non-null-assertion + return this.pythonPathSettingsUpdaterFactory.getWorkspaceFolderPythonPathConfigurationService(wkspace!); + } + } + } +} diff --git a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts new file mode 100644 index 000000000000..aecf7beeb5b6 --- /dev/null +++ b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts @@ -0,0 +1,47 @@ +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { DeprecatePythonPath } from '../../common/experiments/groups'; +import { IExperimentsManager, IInterpreterPathService } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { GlobalPythonPathUpdaterService } from './services/globalUpdaterService'; +import { WorkspaceFolderPythonPathUpdaterService } from './services/workspaceFolderUpdaterService'; +import { WorkspacePythonPathUpdaterService } from './services/workspaceUpdaterService'; +import { IPythonPathUpdaterService, IPythonPathUpdaterServiceFactory } from './types'; + +@injectable() +export class PythonPathUpdaterServiceFactory implements IPythonPathUpdaterServiceFactory { + private readonly inDeprecatePythonPathExperiment: boolean; + private readonly workspaceService: IWorkspaceService; + private readonly interpreterPathService: IInterpreterPathService; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + const experiments = serviceContainer.get(IExperimentsManager); + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.interpreterPathService = serviceContainer.get(IInterpreterPathService); + this.inDeprecatePythonPathExperiment = experiments.inExperiment(DeprecatePythonPath.experiment); + experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + } + public getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService { + return new GlobalPythonPathUpdaterService( + this.inDeprecatePythonPathExperiment, + this.workspaceService, + this.interpreterPathService + ); + } + public getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService { + return new WorkspacePythonPathUpdaterService( + wkspace, + this.inDeprecatePythonPathExperiment, + this.workspaceService, + this.interpreterPathService + ); + } + public getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService { + return new WorkspaceFolderPythonPathUpdaterService( + workspaceFolder, + this.inDeprecatePythonPathExperiment, + this.workspaceService, + this.interpreterPathService + ); + } +} diff --git a/src/client/interpreter/configuration/services/globalUpdaterService.ts b/src/client/interpreter/configuration/services/globalUpdaterService.ts new file mode 100644 index 000000000000..39a1c1731019 --- /dev/null +++ b/src/client/interpreter/configuration/services/globalUpdaterService.ts @@ -0,0 +1,27 @@ +import { ConfigurationTarget } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { IInterpreterPathService } from '../../../common/types'; +import { IPythonPathUpdaterService } from '../types'; + +export class GlobalPythonPathUpdaterService implements IPythonPathUpdaterService { + constructor( + private readonly inDeprecatePythonPathExperiment: boolean, + private readonly workspaceService: IWorkspaceService, + private readonly interpreterPathService: IInterpreterPathService + ) {} + public async updatePythonPath(pythonPath: string | undefined): Promise { + const pythonConfig = this.workspaceService.getConfiguration('python'); + const pythonPathValue = this.inDeprecatePythonPathExperiment + ? this.interpreterPathService.inspect(undefined) + : pythonConfig.inspect('pythonPath')!; + + if (pythonPathValue && pythonPathValue.globalValue === pythonPath) { + return; + } + if (this.inDeprecatePythonPathExperiment) { + await this.interpreterPathService.update(undefined, ConfigurationTarget.Global, pythonPath); + } else { + await pythonConfig.update('pythonPath', pythonPath, true); + } + } +} diff --git a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts new file mode 100644 index 000000000000..3e7888bab2b3 --- /dev/null +++ b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts @@ -0,0 +1,36 @@ +import * as path from 'path'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { IInterpreterPathService } from '../../../common/types'; +import { IPythonPathUpdaterService } from '../types'; + +export class WorkspaceFolderPythonPathUpdaterService implements IPythonPathUpdaterService { + constructor( + private workspaceFolder: Uri, + private readonly inDeprecatePythonPathExperiment: boolean, + private readonly workspaceService: IWorkspaceService, + private readonly interpreterPathService: IInterpreterPathService + ) {} + public async updatePythonPath(pythonPath: string | undefined): Promise { + const pythonConfig = this.workspaceService.getConfiguration('python', this.workspaceFolder); + const pythonPathValue = this.inDeprecatePythonPathExperiment + ? this.interpreterPathService.inspect(this.workspaceFolder) + : pythonConfig.inspect('pythonPath')!; + + if (pythonPathValue && pythonPathValue.workspaceFolderValue === pythonPath) { + return; + } + if (pythonPath && pythonPath.startsWith(this.workspaceFolder.fsPath)) { + pythonPath = path.relative(this.workspaceFolder.fsPath, pythonPath); + } + if (this.inDeprecatePythonPathExperiment) { + await this.interpreterPathService.update( + this.workspaceFolder, + ConfigurationTarget.WorkspaceFolder, + pythonPath + ); + } else { + await pythonConfig.update('pythonPath', pythonPath, ConfigurationTarget.WorkspaceFolder); + } + } +} diff --git a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts new file mode 100644 index 000000000000..198eaf5f59cb --- /dev/null +++ b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts @@ -0,0 +1,32 @@ +import * as path from 'path'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { IInterpreterPathService } from '../../../common/types'; +import { IPythonPathUpdaterService } from '../types'; + +export class WorkspacePythonPathUpdaterService implements IPythonPathUpdaterService { + constructor( + private workspace: Uri, + private readonly inDeprecatePythonPathExperiment: boolean, + private readonly workspaceService: IWorkspaceService, + private readonly interpreterPathService: IInterpreterPathService + ) {} + public async updatePythonPath(pythonPath: string | undefined): Promise { + const pythonConfig = this.workspaceService.getConfiguration('python', this.workspace); + const pythonPathValue = this.inDeprecatePythonPathExperiment + ? this.interpreterPathService.inspect(this.workspace) + : pythonConfig.inspect('pythonPath')!; + + if (pythonPathValue && pythonPathValue.workspaceValue === pythonPath) { + return; + } + if (pythonPath && pythonPath.startsWith(this.workspace.fsPath)) { + pythonPath = path.relative(this.workspace.fsPath, pythonPath); + } + if (this.inDeprecatePythonPathExperiment) { + await this.interpreterPathService.update(this.workspace, ConfigurationTarget.Workspace, pythonPath); + } else { + await pythonConfig.update('pythonPath', pythonPath, false); + } + } +} diff --git a/src/client/interpreter/configuration/types.ts b/src/client/interpreter/configuration/types.ts new file mode 100644 index 000000000000..4d5c7ad36a51 --- /dev/null +++ b/src/client/interpreter/configuration/types.ts @@ -0,0 +1,45 @@ +import { ConfigurationTarget, Disposable, QuickPickItem, Uri } from 'vscode'; +import { Resource } from '../../common/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; + +export interface IPythonPathUpdaterService { + updatePythonPath(pythonPath: string | undefined): Promise; +} + +export const IPythonPathUpdaterServiceFactory = Symbol('IPythonPathUpdaterServiceFactory'); +export interface IPythonPathUpdaterServiceFactory { + getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService; + getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService; + getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService; +} + +export const IPythonPathUpdaterServiceManager = Symbol('IPythonPathUpdaterServiceManager'); +export interface IPythonPathUpdaterServiceManager { + updatePythonPath( + pythonPath: string | undefined, + configTarget: ConfigurationTarget, + trigger: 'ui' | 'shebang' | 'load', + wkspace?: Uri + ): Promise; +} + +export const IInterpreterSelector = Symbol('IInterpreterSelector'); +export interface IInterpreterSelector extends Disposable { + getSuggestions(resource: Resource): Promise; +} + +export interface IInterpreterQuickPickItem extends QuickPickItem { + path: string; + /** + * The interpreter related to this quickpick item. + * + * @type {PythonEnvironment} + * @memberof IInterpreterQuickPickItem + */ + interpreter: PythonEnvironment; +} + +export const IInterpreterComparer = Symbol('IInterpreterComparer'); +export interface IInterpreterComparer { + compare(a: PythonEnvironment, b: PythonEnvironment): number; +} diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts new file mode 100644 index 000000000000..ded0c0ffa00e --- /dev/null +++ b/src/client/interpreter/contracts.ts @@ -0,0 +1,131 @@ +import { SemVer } from 'semver'; +import { CodeLensProvider, Disposable, Event, TextDocument, Uri } from 'vscode'; +import { Resource } from '../common/types'; +import { CondaEnvironmentInfo, CondaInfo } from '../pythonEnvironments/discovery/locators/services/conda'; +import { GetInterpreterLocatorOptions } from '../pythonEnvironments/discovery/locators/types'; +import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; +import { WorkspacePythonPath } from './helpers'; +import { GetInterpreterOptions } from './interpreterService'; + +export const INTERPRETER_LOCATOR_SERVICE = 'IInterpreterLocatorService'; +export const WINDOWS_REGISTRY_SERVICE = 'WindowsRegistryService'; +export const CONDA_ENV_FILE_SERVICE = 'CondaEnvFileService'; +export const CONDA_ENV_SERVICE = 'CondaEnvService'; +export const CURRENT_PATH_SERVICE = 'CurrentPathService'; +export const KNOWN_PATH_SERVICE = 'KnownPathsService'; +export const GLOBAL_VIRTUAL_ENV_SERVICE = 'VirtualEnvService'; +export const WORKSPACE_VIRTUAL_ENV_SERVICE = 'WorkspaceVirtualEnvService'; +export const PIPENV_SERVICE = 'PipEnvService'; +export const IInterpreterVersionService = Symbol('IInterpreterVersionService'); +export interface IInterpreterVersionService { + getVersion(pythonPath: string, defaultValue: string): Promise; + getPipVersion(pythonPath: string): Promise; +} + +export const IKnownSearchPathsForInterpreters = Symbol('IKnownSearchPathsForInterpreters'); +export interface IKnownSearchPathsForInterpreters { + getSearchPaths(): string[]; +} +export const IVirtualEnvironmentsSearchPathProvider = Symbol('IVirtualEnvironmentsSearchPathProvider'); +export interface IVirtualEnvironmentsSearchPathProvider { + getSearchPaths(resource?: Uri): Promise; +} + +export const IInterpreterLocatorService = Symbol('IInterpreterLocatorService'); + +export interface IInterpreterLocatorService extends Disposable { + readonly onLocating: Event>; + readonly hasInterpreters: Promise; + didTriggerInterpreterSuggestions?: boolean; + getInterpreters(resource?: Uri, options?: GetInterpreterLocatorOptions): Promise; +} + +export const ICondaService = Symbol('ICondaService'); + +export interface ICondaService { + readonly condaEnvironmentsFile: string | undefined; + getCondaFile(): Promise; + isCondaAvailable(): Promise; + getCondaVersion(): Promise; + getCondaInfo(): Promise; + getCondaEnvironments(ignoreCache: boolean): Promise; + getInterpreterPath(condaEnvironmentPath: string): string; + getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise; + isCondaEnvironment(interpreterPath: string): Promise; + getCondaEnvironment(interpreterPath: string): Promise; +} + +export const IInterpreterService = Symbol('IInterpreterService'); +export interface IInterpreterService { + onDidChangeInterpreterConfiguration: Event; + onDidChangeInterpreter: Event; + onDidChangeInterpreterInformation: Event; + hasInterpreters: Promise; + getInterpreters(resource?: Uri, options?: GetInterpreterOptions): Promise; + getActiveInterpreter(resource?: Uri): Promise; + getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise; + refresh(resource: Resource): Promise; + initialize(): void; + getDisplayName(interpreter: Partial): Promise; +} + +export const IInterpreterDisplay = Symbol('IInterpreterDisplay'); +export interface IInterpreterDisplay { + refresh(resource?: Uri): Promise; +} + +export const IShebangCodeLensProvider = Symbol('IShebangCodeLensProvider'); +export interface IShebangCodeLensProvider extends CodeLensProvider { + detectShebang(document: TextDocument, resolveShebangAsInterpreter?: boolean): Promise; +} + +export const IInterpreterHelper = Symbol('IInterpreterHelper'); +export interface IInterpreterHelper { + getActiveWorkspaceUri(resource: Resource): WorkspacePythonPath | undefined; + getInterpreterInformation(pythonPath: string): Promise>; + isMacDefaultPythonPath(pythonPath: string): Boolean; + getInterpreterTypeDisplayName(interpreterType: EnvironmentType): string | undefined; + getBestInterpreter(interpreters?: PythonEnvironment[]): PythonEnvironment | undefined; +} + +export const IPipEnvService = Symbol('IPipEnvService'); +export interface IPipEnvService extends IInterpreterLocatorService { + executable: string; + isRelatedPipEnvironment(dir: string, pythonPath: string): Promise; +} + +export const IInterpreterLocatorHelper = Symbol('IInterpreterLocatorHelper'); +export interface IInterpreterLocatorHelper { + mergeInterpreters(interpreters: PythonEnvironment[]): Promise; +} + +export const IInterpreterWatcher = Symbol('IInterpreterWatcher'); +export interface IInterpreterWatcher { + onDidCreate: Event; +} + +export const IInterpreterWatcherBuilder = Symbol('IInterpreterWatcherBuilder'); +export interface IInterpreterWatcherBuilder { + getWorkspaceVirtualEnvInterpreterWatcher(resource: Resource): Promise; +} + +export const IInterpreterLocatorProgressHandler = Symbol('IInterpreterLocatorProgressHandler'); +export interface IInterpreterLocatorProgressHandler { + register(): void; +} + +export const IInterpreterLocatorProgressService = Symbol('IInterpreterLocatorProgressService'); +export interface IInterpreterLocatorProgressService { + readonly onRefreshing: Event; + readonly onRefreshed: Event; + register(): void; +} + +export const IInterpreterStatusbarVisibilityFilter = Symbol('IInterpreterStatusbarVisibilityFilter'); +/** + * Implement this interface to control the visibility of the interpreter statusbar. + */ +export interface IInterpreterStatusbarVisibilityFilter { + readonly changed?: Event; + readonly hidden: boolean; +} diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts new file mode 100644 index 000000000000..b521dcee9b81 --- /dev/null +++ b/src/client/interpreter/display/index.ts @@ -0,0 +1,122 @@ +import { inject, injectable, multiInject } from 'inversify'; +import { Disposable, OutputChannel, StatusBarAlignment, StatusBarItem, Uri } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; +import '../../common/extensions'; +import { IDisposableRegistry, IOutputChannel, IPathUtils, Resource } from '../../common/types'; +import { Interpreters } from '../../common/utils/localize'; +import { IServiceContainer } from '../../ioc/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { IInterpreterAutoSelectionService } from '../autoSelection/types'; +import { + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterService, + IInterpreterStatusbarVisibilityFilter +} from '../contracts'; + +/** + * Create this class as Inversify doesn't allow @multiinject if there are no registered items. + * i.e. we must always have one for @multiinject to work. + */ +@injectable() +export class AlwaysDisplayStatusBar implements IInterpreterStatusbarVisibilityFilter { + public get hidden(): boolean { + return false; + } +} +// tslint:disable-next-line:completed-docs +@injectable() +export class InterpreterDisplay implements IInterpreterDisplay { + private readonly statusBar: StatusBarItem; + private readonly helper: IInterpreterHelper; + private readonly workspaceService: IWorkspaceService; + private readonly pathUtils: IPathUtils; + private readonly interpreterService: IInterpreterService; + private currentlySelectedInterpreterPath?: string; + private currentlySelectedWorkspaceFolder: Resource; + private readonly autoSelection: IInterpreterAutoSelectionService; + private interpreterPath: string | undefined; + private statusBarCanBeDisplayed?: boolean; + + constructor( + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @multiInject(IInterpreterStatusbarVisibilityFilter) + private readonly visibilityFilters: IInterpreterStatusbarVisibilityFilter[] + ) { + this.helper = serviceContainer.get(IInterpreterHelper); + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.pathUtils = serviceContainer.get(IPathUtils); + this.interpreterService = serviceContainer.get(IInterpreterService); + this.autoSelection = serviceContainer.get(IInterpreterAutoSelectionService); + + const application = serviceContainer.get(IApplicationShell); + const disposableRegistry = serviceContainer.get(IDisposableRegistry); + + this.statusBar = application.createStatusBarItem(StatusBarAlignment.Left, 100); + this.statusBar.command = 'python.setInterpreter'; + disposableRegistry.push(this.statusBar); + + this.interpreterService.onDidChangeInterpreterInformation( + this.onDidChangeInterpreterInformation, + this, + disposableRegistry + ); + this.visibilityFilters + .filter((item) => item.changed) + .forEach((item) => item.changed!(this.updateVisibility, this, disposableRegistry)); + } + public async refresh(resource?: Uri) { + // Use the workspace Uri if available + if (resource && this.workspaceService.getWorkspaceFolder(resource)) { + resource = this.workspaceService.getWorkspaceFolder(resource)!.uri; + } + if (!resource) { + const wkspc = this.helper.getActiveWorkspaceUri(resource); + resource = wkspc ? wkspc.folderUri : undefined; + } + await this.updateDisplay(resource); + } + private onDidChangeInterpreterInformation(info: PythonEnvironment) { + if (!this.currentlySelectedInterpreterPath || this.currentlySelectedInterpreterPath === info.path) { + this.updateDisplay(this.currentlySelectedWorkspaceFolder).ignoreErrors(); + } + } + private async updateDisplay(workspaceFolder?: Uri) { + await this.autoSelection.autoSelectInterpreter(workspaceFolder); + const interpreter = await this.interpreterService.getActiveInterpreter(workspaceFolder); + this.currentlySelectedWorkspaceFolder = workspaceFolder; + if (interpreter) { + this.statusBar.color = ''; + this.statusBar.tooltip = this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath); + if (this.interpreterPath !== interpreter.path) { + const output = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + output.appendLine( + Interpreters.pythonInterpreterPath().format( + this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath) + ) + ); + this.interpreterPath = interpreter.path; + } + this.statusBar.text = interpreter.displayName!; + this.currentlySelectedInterpreterPath = interpreter.path; + } else { + this.statusBar.tooltip = ''; + this.statusBar.color = 'yellow'; + this.statusBar.text = '$(alert) Select Python Interpreter'; + this.currentlySelectedInterpreterPath = undefined; + } + this.statusBarCanBeDisplayed = true; + this.updateVisibility(); + } + private updateVisibility() { + if (!this.statusBarCanBeDisplayed) { + return; + } + if (this.visibilityFilters.length === 0 || this.visibilityFilters.every((filter) => !filter.hidden)) { + this.statusBar.show(); + } else { + this.statusBar.hide(); + } + } +} diff --git a/src/client/interpreter/display/interpreterSelectionTip.ts b/src/client/interpreter/display/interpreterSelectionTip.ts new file mode 100644 index 000000000000..2d0eb0747b3a --- /dev/null +++ b/src/client/interpreter/display/interpreterSelectionTip.ts @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IApplicationShell } from '../../common/application/types'; +import { SurveyAndInterpreterTipNotification } from '../../common/experiments/groups'; +import { IBrowserService, IExperimentService, IPersistentState, IPersistentStateFactory } from '../../common/types'; +import { swallowExceptions } from '../../common/utils/decorators'; +import { Common } from '../../common/utils/localize'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +enum NotificationType { + Tip, + Survey, + NoPrompt +} + +@injectable() +export class InterpreterSelectionTip implements IExtensionSingleActivationService { + private readonly storage: IPersistentState; + private notificationType: NotificationType; + private notificationContent: string | undefined; + + constructor( + @inject(IApplicationShell) private readonly shell: IApplicationShell, + @inject(IPersistentStateFactory) factory: IPersistentStateFactory, + @inject(IExperimentService) private readonly experiments: IExperimentService, + @inject(IBrowserService) private browserService: IBrowserService + ) { + this.storage = factory.createGlobalPersistentState('InterpreterSelectionTip', false); + this.notificationType = NotificationType.NoPrompt; + } + + public async activate(): Promise { + // Only show the prompt if we have never shown it before. True here, means we have + // shown the prompt before. + if (this.storage.value) { + return; + } + + if (await this.experiments.inExperiment(SurveyAndInterpreterTipNotification.surveyExperiment)) { + this.notificationType = NotificationType.Survey; + this.notificationContent = await this.experiments.getExperimentValue( + SurveyAndInterpreterTipNotification.surveyExperiment + ); + } else if (await this.experiments.inExperiment(SurveyAndInterpreterTipNotification.tipExperiment)) { + this.notificationType = NotificationType.Tip; + this.notificationContent = await this.experiments.getExperimentValue( + SurveyAndInterpreterTipNotification.tipExperiment + ); + } + + this.showTip().ignoreErrors(); + + // We will disable this prompt for all users even if they are not + // in any experiment. The idea is that people should get either the + // tip or survey or nothing, on first load. If we are here then that + // means we are done with this prompt (even if it was not shown). + await this.storage.updateValue(true); + } + @swallowExceptions('Failed to display tip') + private async showTip() { + if (this.notificationType === NotificationType.Tip) { + await this.shell.showInformationMessage(this.notificationContent!, Common.gotIt()); + sendTelemetryEvent(EventName.ACTIVATION_TIP_PROMPT, undefined); + } else if (this.notificationType === NotificationType.Survey) { + const selection = await this.shell.showInformationMessage( + this.notificationContent!, + Common.bannerLabelYes(), + Common.bannerLabelNo() + ); + + if (selection === Common.bannerLabelYes()) { + sendTelemetryEvent(EventName.ACTIVATION_SURVEY_PROMPT, undefined); + this.browserService.launch('https://aka.ms/mailingListSurvey'); + } + } + } +} diff --git a/src/client/interpreter/display/progressDisplay.ts b/src/client/interpreter/display/progressDisplay.ts new file mode 100644 index 000000000000..83b5b9dfbe30 --- /dev/null +++ b/src/client/interpreter/display/progressDisplay.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable, ProgressLocation, ProgressOptions } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { traceDecorators } from '../../common/logger'; +import { IDisposableRegistry } from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { IInterpreterLocatorProgressHandler, IInterpreterLocatorProgressService } from '../contracts'; + +@injectable() +export class InterpreterLocatorProgressStatubarHandler implements IInterpreterLocatorProgressHandler { + private deferred: Deferred | undefined; + private isFirstTimeLoadingInterpreters = true; + constructor( + @inject(IApplicationShell) private readonly shell: IApplicationShell, + @inject(IInterpreterLocatorProgressService) + private readonly progressService: IInterpreterLocatorProgressService, + @inject(IDisposableRegistry) private readonly disposables: Disposable[] + ) {} + public register() { + this.progressService.onRefreshing(() => this.showProgress(), this, this.disposables); + this.progressService.onRefreshed(() => this.hideProgress(), this, this.disposables); + } + @traceDecorators.verbose('Display locator refreshing progress') + private showProgress(): void { + if (!this.deferred) { + this.createProgress(); + } + } + @traceDecorators.verbose('Hide locator refreshing progress') + private hideProgress(): void { + if (this.deferred) { + this.deferred.resolve(); + this.deferred = undefined; + } + } + private createProgress() { + const progressOptions: ProgressOptions = { + location: ProgressLocation.Window, + title: this.isFirstTimeLoadingInterpreters ? Common.loadingExtension() : Interpreters.refreshing() + }; + this.isFirstTimeLoadingInterpreters = false; + this.shell.withProgress(progressOptions, () => { + this.deferred = createDeferred(); + return this.deferred.promise; + }); + } +} diff --git a/src/client/interpreter/display/shebangCodeLensProvider.ts b/src/client/interpreter/display/shebangCodeLensProvider.ts new file mode 100644 index 000000000000..8737981c8369 --- /dev/null +++ b/src/client/interpreter/display/shebangCodeLensProvider.ts @@ -0,0 +1,86 @@ +import { inject, injectable } from 'inversify'; +import { CancellationToken, CodeLens, Command, Event, Position, Range, TextDocument, Uri } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { IPlatformService } from '../../common/platform/types'; +import * as internalPython from '../../common/process/internal/python'; +import { IProcessServiceFactory } from '../../common/process/types'; +import { IConfigurationService } from '../../common/types'; +import { IShebangCodeLensProvider } from '../contracts'; + +@injectable() +export class ShebangCodeLensProvider implements IShebangCodeLensProvider { + public readonly onDidChangeCodeLenses: Event; + constructor( + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IWorkspaceService) workspaceService: IWorkspaceService + ) { + // tslint:disable-next-line:no-any + this.onDidChangeCodeLenses = (workspaceService.onDidChangeConfiguration as any) as Event; + } + public async detectShebang( + document: TextDocument, + resolveShebangAsInterpreter: boolean = false + ): Promise { + const firstLine = document.lineAt(0); + if (firstLine.isEmptyOrWhitespace) { + return; + } + + if (!firstLine.text.startsWith('#!')) { + return; + } + + const shebang = firstLine.text.substr(2).trim(); + if (resolveShebangAsInterpreter) { + const pythonPath = await this.getFullyQualifiedPathToInterpreter(shebang, document.uri); + return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined; + } else { + return typeof shebang === 'string' && shebang.length > 0 ? shebang : undefined; + } + } + public async provideCodeLenses(document: TextDocument, _token?: CancellationToken): Promise { + return this.createShebangCodeLens(document); + } + private async getFullyQualifiedPathToInterpreter(pythonPath: string, resource: Uri) { + let cmdFile = pythonPath; + const [args, parse] = internalPython.getExecutable(); + if (pythonPath.indexOf('bin/env ') >= 0 && !this.platformService.isWindows) { + // In case we have pythonPath as '/usr/bin/env python'. + const parts = pythonPath + .split(' ') + .map((part) => part.trim()) + .filter((part) => part.length > 0); + cmdFile = parts.shift()!; + args.splice(0, 0, ...parts); + } + const processService = await this.processServiceFactory.create(resource); + return processService + .exec(cmdFile, args) + .then((output) => parse(output.stdout)) + .catch(() => ''); + } + private async createShebangCodeLens(document: TextDocument) { + const shebang = await this.detectShebang(document); + if (!shebang) { + return []; + } + const pythonPath = this.configurationService.getSettings(document.uri).pythonPath; + const resolvedPythonPath = await this.getFullyQualifiedPathToInterpreter(pythonPath, document.uri); + if (shebang === resolvedPythonPath) { + return []; + } + const firstLine = document.lineAt(0); + const startOfShebang = new Position(0, 0); + const endOfShebang = new Position(0, firstLine.text.length - 1); + const shebangRange = new Range(startOfShebang, endOfShebang); + + const cmd: Command = { + command: 'python.setShebangInterpreter', + title: 'Set as interpreter' + }; + + return [new CodeLens(shebangRange, cmd)]; + } +} diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts new file mode 100644 index 000000000000..59e7f213552d --- /dev/null +++ b/src/client/interpreter/helpers.ts @@ -0,0 +1,131 @@ +import { inject, injectable } from 'inversify'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { traceError } from '../common/logger'; +import { FileSystemPaths } from '../common/platform/fs-paths'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { IPersistentStateFactory, Resource } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { isMacDefaultPythonPath } from '../pythonEnvironments/discovery'; +import { InterpeterHashProviderFactory } from '../pythonEnvironments/discovery/locators/services/hashProviderFactory'; +import { + EnvironmentType, + getEnvironmentTypeName, + InterpreterInformation, + PythonEnvironment, + sortInterpreters +} from '../pythonEnvironments/info'; +import { IInterpreterHelper } from './contracts'; +import { IInterpreterHashProviderFactory } from './locators/types'; + +const EXPITY_DURATION = 24 * 60 * 60 * 1000; +type CachedPythonInterpreter = Partial & { fileHash: string }; + +export type WorkspacePythonPath = { + folderUri: Uri; + configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder; +}; + +export function getFirstNonEmptyLineFromMultilineString(stdout: string) { + if (!stdout) { + return ''; + } + const lines = stdout + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); + return lines.length > 0 ? lines[0] : ''; +} + +export function isInterpreterLocatedInWorkspace(interpreter: PythonEnvironment, activeWorkspaceUri: Uri) { + const fileSystemPaths = FileSystemPaths.withDefaults(); + const interpreterPath = fileSystemPaths.normCase(interpreter.path); + const resourcePath = fileSystemPaths.normCase(activeWorkspaceUri.fsPath); + return interpreterPath.startsWith(resourcePath); +} + +@injectable() +export class InterpreterHelper implements IInterpreterHelper { + private readonly persistentFactory: IPersistentStateFactory; + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(InterpeterHashProviderFactory) private readonly hashProviderFactory: IInterpreterHashProviderFactory + ) { + this.persistentFactory = this.serviceContainer.get(IPersistentStateFactory); + } + public getActiveWorkspaceUri(resource: Resource): WorkspacePythonPath | undefined { + const workspaceService = this.serviceContainer.get(IWorkspaceService); + if (!workspaceService.hasWorkspaceFolders) { + return; + } + if (Array.isArray(workspaceService.workspaceFolders) && workspaceService.workspaceFolders.length === 1) { + return { folderUri: workspaceService.workspaceFolders[0].uri, configTarget: ConfigurationTarget.Workspace }; + } + + if (resource) { + const workspaceFolder = workspaceService.getWorkspaceFolder(resource); + if (workspaceFolder) { + return { configTarget: ConfigurationTarget.WorkspaceFolder, folderUri: workspaceFolder.uri }; + } + } + const documentManager = this.serviceContainer.get(IDocumentManager); + + if (documentManager.activeTextEditor) { + const workspaceFolder = workspaceService.getWorkspaceFolder(documentManager.activeTextEditor.document.uri); + if (workspaceFolder) { + return { configTarget: ConfigurationTarget.WorkspaceFolder, folderUri: workspaceFolder.uri }; + } + } + } + public async getInterpreterInformation(pythonPath: string): Promise> { + const fileHash = await this.hashProviderFactory + .create({ pythonPath }) + .then((provider) => provider.getInterpreterHash(pythonPath)) + .catch((ex) => { + traceError(`Failed to create File hash for interpreter ${pythonPath}`, ex); + return ''; + }); + const store = this.persistentFactory.createGlobalPersistentState( + `${pythonPath}.v3`, + undefined, + EXPITY_DURATION + ); + if (store.value && fileHash && store.value.fileHash === fileHash) { + return store.value; + } + const processService = await this.serviceContainer + .get(IPythonExecutionFactory) + .create({ pythonPath }); + + try { + const info = await processService + .getInterpreterInformation() + .catch(() => undefined); + if (!info) { + return; + } + const details = { + ...info, + fileHash + }; + await store.updateValue(details); + return details; + } catch (ex) { + traceError(`Failed to get interpreter information for '${pythonPath}'`, ex); + return; + } + } + public isMacDefaultPythonPath(pythonPath: string) { + return isMacDefaultPythonPath(pythonPath); + } + public getInterpreterTypeDisplayName(interpreterType: EnvironmentType) { + return getEnvironmentTypeName(interpreterType); + } + public getBestInterpreter(interpreters?: PythonEnvironment[]): PythonEnvironment | undefined { + if (!Array.isArray(interpreters) || interpreters.length === 0) { + return; + } + const sorted = sortInterpreters(interpreters); + return sorted[sorted.length - 1]; + } +} diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts new file mode 100644 index 000000000000..5fa9202b95cc --- /dev/null +++ b/src/client/interpreter/interpreterService.ts @@ -0,0 +1,358 @@ +import { inject, injectable } from 'inversify'; +import * as md5 from 'md5'; +import * as path from 'path'; +import { Disposable, Event, EventEmitter, Uri } from 'vscode'; +import '../../client/common/extensions'; +import { IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { DeprecatePythonPath } from '../common/experiments/groups'; +import { traceError } from '../common/logger'; +import { getArchitectureDisplayName } from '../common/platform/registry'; +import { IFileSystem } from '../common/platform/types'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentsManager, + IInterpreterPathService, + IPersistentState, + IPersistentStateFactory, + Resource +} from '../common/types'; +import { sleep } from '../common/utils/async'; +import { IServiceContainer } from '../ioc/types'; +import { InterpeterHashProviderFactory } from '../pythonEnvironments/discovery/locators/services/hashProviderFactory'; +import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterLocatorService, + IInterpreterService, + INTERPRETER_LOCATOR_SERVICE +} from './contracts'; +import { IInterpreterHashProviderFactory } from './locators/types'; +import { IVirtualEnvironmentManager } from './virtualEnvs/types'; + +const EXPITY_DURATION = 24 * 60 * 60 * 1000; + +export type GetInterpreterOptions = { + onSuggestion?: boolean; +}; + +@injectable() +export class InterpreterService implements Disposable, IInterpreterService { + public get hasInterpreters(): Promise { + return this.locator.hasInterpreters; + } + + public get onDidChangeInterpreter(): Event { + return this.didChangeInterpreterEmitter.event; + } + + public get onDidChangeInterpreterInformation(): Event { + return this.didChangeInterpreterInformation.event; + } + public get onDidChangeInterpreterConfiguration(): Event { + return this.didChangeInterpreterConfigurationEmitter.event; + } + public _pythonPathSetting: string = ''; + private readonly didChangeInterpreterConfigurationEmitter = new EventEmitter(); + private readonly locator: IInterpreterLocatorService; + private readonly persistentStateFactory: IPersistentStateFactory; + private readonly configService: IConfigurationService; + private readonly interpreterPathService: IInterpreterPathService; + private readonly experiments: IExperimentsManager; + private readonly didChangeInterpreterEmitter = new EventEmitter(); + private readonly didChangeInterpreterInformation = new EventEmitter(); + private readonly inMemoryCacheOfDisplayNames = new Map(); + private readonly updatedInterpreters = new Set(); + + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(InterpeterHashProviderFactory) private readonly hashProviderFactory: IInterpreterHashProviderFactory + ) { + this.locator = serviceContainer.get( + IInterpreterLocatorService, + INTERPRETER_LOCATOR_SERVICE + ); + this.persistentStateFactory = this.serviceContainer.get(IPersistentStateFactory); + this.configService = this.serviceContainer.get(IConfigurationService); + this.interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + this.experiments = this.serviceContainer.get(IExperimentsManager); + } + + public async refresh(resource?: Uri) { + const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); + return interpreterDisplay.refresh(resource); + } + + public initialize() { + const disposables = this.serviceContainer.get(IDisposableRegistry); + const documentManager = this.serviceContainer.get(IDocumentManager); + disposables.push( + documentManager.onDidChangeActiveTextEditor((e) => + e && e.document ? this.refresh(e.document.uri) : undefined + ) + ); + const workspaceService = this.serviceContainer.get(IWorkspaceService); + const pySettings = this.configService.getSettings(); + this._pythonPathSetting = pySettings.pythonPath; + if (this.experiments.inExperiment(DeprecatePythonPath.experiment)) { + disposables.push( + this.interpreterPathService.onDidChange((i) => { + this._onConfigChanged(i.uri); + }) + ); + } else { + const workspacesUris: (Uri | undefined)[] = workspaceService.hasWorkspaceFolders + ? workspaceService.workspaceFolders!.map((workspace) => workspace.uri) + : [undefined]; + const disposable = workspaceService.onDidChangeConfiguration((e) => { + const workspaceUriIndex = workspacesUris.findIndex((uri) => + e.affectsConfiguration('python.pythonPath', uri) + ); + const workspaceUri = workspaceUriIndex === -1 ? undefined : workspacesUris[workspaceUriIndex]; + this._onConfigChanged(workspaceUri); + }); + disposables.push(disposable); + } + this.experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + } + + @captureTelemetry(EventName.PYTHON_INTERPRETER_DISCOVERY, { locator: 'all' }, true) + public async getInterpreters(resource?: Uri, options?: GetInterpreterOptions): Promise { + const interpreters = await this.locator.getInterpreters(resource, options); + await Promise.all( + interpreters + .filter((item) => !item.displayName) + .map(async (item) => { + item.displayName = await this.getDisplayName(item, resource); + // Keep information up to date with latest details. + if (!item.cachedEntry) { + this.updateCachedInterpreterInformation(item, resource).ignoreErrors(); + } + }) + ); + return interpreters; + } + + public dispose(): void { + this.locator.dispose(); + this.didChangeInterpreterEmitter.dispose(); + this.didChangeInterpreterInformation.dispose(); + } + + public async getActiveInterpreter(resource?: Uri): Promise { + // During shutdown we might not be able to get items out of the service container. + const pythonExecutionFactory = this.serviceContainer.tryGet(IPythonExecutionFactory); + const pythonExecutionService = pythonExecutionFactory + ? await pythonExecutionFactory.create({ resource }) + : undefined; + const fullyQualifiedPath = pythonExecutionService + ? await pythonExecutionService.getExecutablePath().catch(() => undefined) + : undefined; + // Python path is invalid or python isn't installed. + if (!fullyQualifiedPath) { + return; + } + + return this.getInterpreterDetails(fullyQualifiedPath, resource); + } + public async getInterpreterDetails(pythonPath: string, resource?: Uri): Promise { + // If we don't have the fully qualified path, then get it. + if (path.basename(pythonPath) === pythonPath) { + const pythonExecutionFactory = this.serviceContainer.get(IPythonExecutionFactory); + const pythonExecutionService = await pythonExecutionFactory.create({ resource }); + pythonPath = await pythonExecutionService.getExecutablePath().catch(() => ''); + // Python path is invalid or python isn't installed. + if (!pythonPath) { + return; + } + } + + const store = await this.getInterpreterCache(pythonPath); + if (store.value && store.value.info) { + return store.value.info; + } + + const fs = this.serviceContainer.get(IFileSystem); + + // Don't want for all interpreters are collected. + // Try to collect the infromation manually, that's faster. + // Get from which ever comes first. + const option1 = (async () => { + const result = this.collectInterpreterDetails(pythonPath, resource); + await sleep(1000); // let the other option complete within 1s if possible. + return result; + })(); + + // This is the preferred approach, hence the delay in option 1. + const option2 = (async () => { + const interpreters = await this.getInterpreters(resource); + const found = interpreters.find((i) => fs.arePathsSame(i.path, pythonPath)); + if (found) { + // Cache the interpreter info, only if we get the data from interpretr list. + // tslint:disable-next-line:no-any + (found as any).__store = true; + return found; + } + // Use option1 as a fallback. + return option1; + })(); + + // Get the first one that doesn't return undefined + let interpreterInfo = await Promise.race([option2, option1]); + if (!interpreterInfo) { + // If undefined, wait for both + const both = await Promise.all([option1, option2]); + interpreterInfo = both[0] ? both[0] : both[1]; + } + + // tslint:disable-next-line:no-any + if (interpreterInfo && (interpreterInfo as any).__store) { + await this.updateCachedInterpreterInformation(interpreterInfo, resource); + } + return interpreterInfo; + } + /** + * Gets the display name of an interpreter. + * The format is `Python (: )` + * E.g. `Python 3.5.1 32-bit (myenv2: virtualenv)` + * @param {Partial} info + * @param {Uri} [resource] + * @returns {string} + * @memberof InterpreterService + */ + public async getDisplayName(info: Partial, resource?: Uri): Promise { + // faster than calculating file has agian and again, only when deailing with cached items. + if (!info.cachedEntry && info.path && this.inMemoryCacheOfDisplayNames.has(info.path)) { + return this.inMemoryCacheOfDisplayNames.get(info.path)!; + } + const fileHash = (info.path ? await this.getInterepreterFileHash(info.path).catch(() => '') : '') || ''; + // Do not include dipslay name into hash as that changes. + const interpreterHash = `${fileHash}-${md5(JSON.stringify({ ...info, displayName: '' }))}`; + const store = this.persistentStateFactory.createGlobalPersistentState<{ hash: string; displayName: string }>( + `${info.path}.interpreter.displayName.v7`, + undefined, + EXPITY_DURATION + ); + if (store.value && store.value.hash === interpreterHash && store.value.displayName) { + this.inMemoryCacheOfDisplayNames.set(info.path!, store.value.displayName); + return store.value.displayName; + } + + const displayName = await this.buildInterpreterDisplayName(info, resource); + + // If dealing with cached entry, then do not store the display name in cache. + if (!info.cachedEntry) { + await store.updateValue({ displayName, hash: interpreterHash }); + this.inMemoryCacheOfDisplayNames.set(info.path!, displayName); + } + + return displayName; + } + public async getInterpreterCache( + pythonPath: string + ): Promise> { + const fileHash = (pythonPath ? await this.getInterepreterFileHash(pythonPath).catch(() => '') : '') || ''; + const store = this.persistentStateFactory.createGlobalPersistentState<{ + fileHash: string; + info?: PythonEnvironment; + }>(`${pythonPath}.interpreter.Details.v7`, undefined, EXPITY_DURATION); + if (!store.value || store.value.fileHash !== fileHash) { + await store.updateValue({ fileHash }); + } + return store; + } + public _onConfigChanged = (resource?: Uri) => { + this.didChangeInterpreterConfigurationEmitter.fire(resource); + // Check if we actually changed our python path + const pySettings = this.configService.getSettings(resource); + if (this._pythonPathSetting === '' || this._pythonPathSetting !== pySettings.pythonPath) { + this._pythonPathSetting = pySettings.pythonPath; + this.didChangeInterpreterEmitter.fire(); + const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); + interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); + } + }; + protected async getInterepreterFileHash(pythonPath: string): Promise { + return this.hashProviderFactory + .create({ pythonPath }) + .then((provider) => provider.getInterpreterHash(pythonPath)); + } + protected async updateCachedInterpreterInformation(info: PythonEnvironment, resource: Resource): Promise { + const key = JSON.stringify(info); + if (this.updatedInterpreters.has(key)) { + return; + } + this.updatedInterpreters.add(key); + const state = await this.getInterpreterCache(info.path); + info.displayName = await this.getDisplayName(info, resource); + // Check if info has indeed changed. + if (state.value && state.value.info && JSON.stringify(info) === JSON.stringify(state.value.info)) { + return; + } + this.inMemoryCacheOfDisplayNames.delete(info.path); + await state.updateValue({ fileHash: state.value.fileHash, info }); + this.didChangeInterpreterInformation.fire(info); + } + protected async buildInterpreterDisplayName(info: Partial, resource?: Uri): Promise { + const displayNameParts: string[] = ['Python']; + const envSuffixParts: string[] = []; + + if (info.version) { + displayNameParts.push(`${info.version.major}.${info.version.minor}.${info.version.patch}`); + } + if (info.architecture) { + displayNameParts.push(getArchitectureDisplayName(info.architecture)); + } + if (!info.envName && info.path && info.envType && info.envType === EnvironmentType.Pipenv) { + // If we do not have the name of the environment, then try to get it again. + // This can happen based on the context (i.e. resource). + // I.e. we can determine if an environment is PipEnv only when giving it the right workspacec path (i.e. resource). + const virtualEnvMgr = this.serviceContainer.get(IVirtualEnvironmentManager); + info.envName = await virtualEnvMgr.getEnvironmentName(info.path, resource); + } + if (info.envName && info.envName.length > 0) { + envSuffixParts.push(`'${info.envName}'`); + } + if (info.envType) { + const interpreterHelper = this.serviceContainer.get(IInterpreterHelper); + const name = interpreterHelper.getInterpreterTypeDisplayName(info.envType); + if (name) { + envSuffixParts.push(name); + } + } + + const envSuffix = envSuffixParts.length === 0 ? '' : `(${envSuffixParts.join(': ')})`; + return `${displayNameParts.join(' ')} ${envSuffix}`.trim(); + } + private async collectInterpreterDetails(pythonPath: string, resource: Uri | undefined) { + const interpreterHelper = this.serviceContainer.get(IInterpreterHelper); + const virtualEnvManager = this.serviceContainer.get(IVirtualEnvironmentManager); + const [info, type] = await Promise.all([ + interpreterHelper.getInterpreterInformation(pythonPath), + virtualEnvManager.getEnvironmentType(pythonPath) + ]); + if (!info) { + return; + } + const details: Partial = { + ...(info as PythonEnvironment), + path: pythonPath, + envType: type + }; + + const envName = + type === EnvironmentType.Unknown + ? undefined + : await virtualEnvManager.getEnvironmentName(pythonPath, resource); + const pthonInfo = { + ...(details as PythonEnvironment), + envName + }; + pthonInfo.displayName = await this.getDisplayName(pthonInfo, resource); + return pthonInfo; + } +} diff --git a/src/client/interpreter/interpreterVersion.ts b/src/client/interpreter/interpreterVersion.ts new file mode 100644 index 000000000000..47d551a94122 --- /dev/null +++ b/src/client/interpreter/interpreterVersion.ts @@ -0,0 +1,37 @@ +import { inject, injectable } from 'inversify'; +import '../common/extensions'; +import * as internalPython from '../common/process/internal/python'; +import { IProcessServiceFactory } from '../common/process/types'; +import { getPythonVersion } from '../pythonEnvironments/info/pythonVersion'; +import { IInterpreterVersionService } from './contracts'; + +export const PIP_VERSION_REGEX = '\\d+\\.\\d+(\\.\\d+)?'; + +@injectable() +export class InterpreterVersionService implements IInterpreterVersionService { + constructor(@inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory) {} + + public async getVersion(pythonPath: string, defaultValue: string): Promise { + const processService = await this.processServiceFactory.create(); + return getPythonVersion(pythonPath, defaultValue, (cmd, args) => + processService.exec(cmd, args, { mergeStdOutErr: true }) + ); + } + + public async getPipVersion(pythonPath: string): Promise { + const [args, parse] = internalPython.getModuleVersion('pip'); + const processService = await this.processServiceFactory.create(); + const output = await processService.exec(pythonPath, args, { mergeStdOutErr: true }); + const version = parse(output.stdout); + if (version.length > 0) { + // Here's a sample output: + // pip 9.0.1 from /Users/donjayamanne/anaconda3/lib/python3.6/site-packages (python 3.6). + const re = new RegExp(PIP_VERSION_REGEX, 'g'); + const matches = re.exec(version); + if (matches && matches.length > 0) { + return matches[0].trim(); + } + } + throw new Error(`Unable to determine pip version from output '${output.stdout}'`); + } +} diff --git a/src/client/interpreter/locators/types.ts b/src/client/interpreter/locators/types.ts new file mode 100644 index 000000000000..8cb5a9d15c08 --- /dev/null +++ b/src/client/interpreter/locators/types.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Uri } from 'vscode'; + +export const IPythonInPathCommandProvider = Symbol('IPythonInPathCommandProvider'); +export interface IPythonInPathCommandProvider { + getCommands(): { command: string; args?: string[] }[]; +} +export const IPipEnvServiceHelper = Symbol('IPipEnvServiceHelper'); +export interface IPipEnvServiceHelper { + getPipEnvInfo(pythonPath: string): Promise<{ workspaceFolder: Uri; envName: string } | undefined>; + trackWorkspaceFolder(pythonPath: string, workspaceFolder: Uri): Promise; +} + +/** + * Factory to create a hash provider. + * Getting the hash of an interpreter can vary based on the type of the interpreter. + * + * @export + * @interface IInterpreterHashProviderFactory + */ +export const IInterpreterHashProviderFactory = Symbol('IInterpreterHashProviderFactory'); +export interface IInterpreterHashProviderFactory { + create(options: { pythonPath: string } | { resource: Uri }): Promise; +} + +/** + * Provides the ability to get the has of a given interpreter. + * + * @export + * @interface IInterpreterHashProvider + */ +export interface IInterpreterHashProvider { + /** + * Gets the hash of a given Python Interpreter. + * (hash is calculated based on last modified timestamp of executable) + * + * @param {string} pythonPath + * @returns {Promise} + * @memberof IInterpreterHashProvider + */ + getInterpreterHash(pythonPath: string): Promise; +} + +export interface IWindowsStoreInterpreter { + /** + * Whether this is a Windows Store/App Interpreter. + * + * @param {string} pythonPath + * @returns {boolean} + * @memberof WindowsStoreInterpreter + */ + isWindowsStoreInterpreter(pythonPath: string): boolean; + /** + * Whether this is a python executable in a windows app store folder that is internal and can be hidden from users. + * + * @param {string} pythonPath + * @returns {boolean} + * @memberof IInterpreterHelper + */ + isHiddenInterpreter(pythonPath: string): boolean; +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts new file mode 100644 index 000000000000..3ba92d220e1a --- /dev/null +++ b/src/client/interpreter/serviceRegistry.ts @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; +import { IServiceManager } from '../ioc/types'; +import { PreWarmActivatedEnvironmentVariables } from './activation/preWarmVariables'; +import { EnvironmentActivationService } from './activation/service'; +import { TerminalEnvironmentActivationService } from './activation/terminalEnvironmentActivationService'; +import { IEnvironmentActivationService } from './activation/types'; +import { InterpreterAutoSelectionService } from './autoSelection/index'; +import { InterpreterEvaluation } from './autoSelection/interpreterSecurity/interpreterEvaluation'; +import { InterpreterSecurityService } from './autoSelection/interpreterSecurity/interpreterSecurityService'; +import { InterpreterSecurityStorage } from './autoSelection/interpreterSecurity/interpreterSecurityStorage'; +import { InterpreterAutoSeletionProxyService } from './autoSelection/proxy'; +import { CachedInterpretersAutoSelectionRule } from './autoSelection/rules/cached'; +import { CurrentPathInterpretersAutoSelectionRule } from './autoSelection/rules/currentPath'; +import { SettingsInterpretersAutoSelectionRule } from './autoSelection/rules/settings'; +import { SystemWideInterpretersAutoSelectionRule } from './autoSelection/rules/system'; +import { WindowsRegistryInterpretersAutoSelectionRule } from './autoSelection/rules/winRegistry'; +import { WorkspaceVirtualEnvInterpretersAutoSelectionRule } from './autoSelection/rules/workspaceEnv'; +import { + AutoSelectionRule, + IInterpreterAutoSelectionRule, + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService, + IInterpreterEvaluation, + IInterpreterSecurityService, + IInterpreterSecurityStorage +} from './autoSelection/types'; +import { InterpreterComparer } from './configuration/interpreterComparer'; +import { ResetInterpreterCommand } from './configuration/interpreterSelector/commands/resetInterpreter'; +import { SetInterpreterCommand } from './configuration/interpreterSelector/commands/setInterpreter'; +import { SetShebangInterpreterCommand } from './configuration/interpreterSelector/commands/setShebangInterpreter'; +import { InterpreterSelector } from './configuration/interpreterSelector/interpreterSelector'; +import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; +import { + IInterpreterComparer, + IInterpreterSelector, + IPythonPathUpdaterServiceFactory, + IPythonPathUpdaterServiceManager +} from './configuration/types'; +import { + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterLocatorProgressHandler, + IInterpreterService, + IInterpreterStatusbarVisibilityFilter, + IInterpreterVersionService, + IShebangCodeLensProvider +} from './contracts'; +import { AlwaysDisplayStatusBar, InterpreterDisplay } from './display'; +import { InterpreterSelectionTip } from './display/interpreterSelectionTip'; +import { InterpreterLocatorProgressStatubarHandler } from './display/progressDisplay'; +import { ShebangCodeLensProvider } from './display/shebangCodeLensProvider'; +import { InterpreterHelper } from './helpers'; +import { InterpreterService } from './interpreterService'; +import { InterpreterVersionService } from './interpreterVersion'; +import { CondaInheritEnvPrompt } from './virtualEnvs/condaInheritEnvPrompt'; +import { VirtualEnvironmentManager } from './virtualEnvs/index'; +import { IVirtualEnvironmentManager } from './virtualEnvs/types'; +import { VirtualEnvironmentPrompt } from './virtualEnvs/virtualEnvPrompt'; + +/** + * Register all the new types inside this method. + * This method is created for testing purposes. Registers all interpreter types except `IInterpreterAutoSeletionProxyService`, `IEnvironmentActivationService`. + * See use case in `src\test\serviceRegistry.ts` for details + * @param serviceManager + */ +// tslint:disable-next-line: max-func-body-length +export function registerInterpreterTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton( + IExtensionSingleActivationService, + InterpreterSecurityStorage + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + SetInterpreterCommand + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + ResetInterpreterCommand + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + SetShebangInterpreterCommand + ); + serviceManager.addSingleton(IInterpreterEvaluation, InterpreterEvaluation); + serviceManager.addSingleton(IInterpreterSecurityStorage, InterpreterSecurityStorage); + serviceManager.addSingleton(IInterpreterSecurityService, InterpreterSecurityService); + + serviceManager.addSingleton(IVirtualEnvironmentManager, VirtualEnvironmentManager); + serviceManager.addSingleton(IExtensionActivationService, VirtualEnvironmentPrompt); + serviceManager.addSingleton( + IExtensionSingleActivationService, + InterpreterSelectionTip + ); + + serviceManager.addSingleton(IInterpreterVersionService, InterpreterVersionService); + + serviceManager.addSingleton(IInterpreterService, InterpreterService); + serviceManager.addSingleton(IInterpreterDisplay, InterpreterDisplay); + + serviceManager.addSingleton( + IPythonPathUpdaterServiceFactory, + PythonPathUpdaterServiceFactory + ); + serviceManager.addSingleton( + IPythonPathUpdaterServiceManager, + PythonPathUpdaterService + ); + + serviceManager.addSingleton(IInterpreterSelector, InterpreterSelector); + serviceManager.addSingleton(IShebangCodeLensProvider, ShebangCodeLensProvider); + serviceManager.addSingleton(IInterpreterHelper, InterpreterHelper); + + serviceManager.addSingleton(IInterpreterComparer, InterpreterComparer); + + serviceManager.addSingleton( + IInterpreterLocatorProgressHandler, + InterpreterLocatorProgressStatubarHandler + ); + + serviceManager.addSingleton( + IInterpreterAutoSelectionRule, + CurrentPathInterpretersAutoSelectionRule, + AutoSelectionRule.currentPath + ); + serviceManager.addSingleton( + IInterpreterAutoSelectionRule, + SystemWideInterpretersAutoSelectionRule, + AutoSelectionRule.systemWide + ); + serviceManager.addSingleton( + IInterpreterAutoSelectionRule, + WindowsRegistryInterpretersAutoSelectionRule, + AutoSelectionRule.windowsRegistry + ); + serviceManager.addSingleton( + IInterpreterAutoSelectionRule, + WorkspaceVirtualEnvInterpretersAutoSelectionRule, + AutoSelectionRule.workspaceVirtualEnvs + ); + serviceManager.addSingleton( + IInterpreterAutoSelectionRule, + CachedInterpretersAutoSelectionRule, + AutoSelectionRule.cachedInterpreters + ); + serviceManager.addSingleton( + IInterpreterAutoSelectionRule, + SettingsInterpretersAutoSelectionRule, + AutoSelectionRule.settings + ); + serviceManager.addSingleton( + IInterpreterAutoSelectionService, + InterpreterAutoSelectionService + ); + + serviceManager.addSingleton(IExtensionActivationService, CondaInheritEnvPrompt); + + serviceManager.addSingleton( + IExtensionSingleActivationService, + PreWarmActivatedEnvironmentVariables + ); + serviceManager.addSingleton( + IInterpreterStatusbarVisibilityFilter, + AlwaysDisplayStatusBar + ); +} + +export function registerTypes(serviceManager: IServiceManager) { + registerInterpreterTypes(serviceManager); + serviceManager.addSingleton( + IInterpreterAutoSeletionProxyService, + InterpreterAutoSeletionProxyService + ); + serviceManager.addSingleton( + EnvironmentActivationService, + EnvironmentActivationService + ); + serviceManager.addSingleton( + TerminalEnvironmentActivationService, + TerminalEnvironmentActivationService + ); + serviceManager.addSingleton( + IEnvironmentActivationService, + EnvironmentActivationService + ); +} diff --git a/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts b/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts new file mode 100644 index 000000000000..a3fdac906e39 --- /dev/null +++ b/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, optional } from 'inversify'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IExtensionActivationService } from '../../activation/types'; +import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { traceDecorators, traceError } from '../../common/logger'; +import { IPlatformService } from '../../common/platform/types'; +import { IBrowserService, IPersistentStateFactory } from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { EnvironmentType } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IInterpreterService } from '../contracts'; + +export const condaInheritEnvPromptKey = 'CONDA_INHERIT_ENV_PROMPT_KEY'; + +@injectable() +export class CondaInheritEnvPrompt implements IExtensionActivationService { + constructor( + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IBrowserService) private browserService: IBrowserService, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @optional() public hasPromptBeenShownInCurrentSession: boolean = false + ) {} + + public async activate(resource: Uri): Promise { + this.initializeInBackground(resource).ignoreErrors(); + } + + @traceDecorators.error('Failed to intialize conda inherit env prompt') + public async initializeInBackground(resource: Uri): Promise { + const show = await this.shouldShowPrompt(resource); + if (!show) { + return; + } + await this.promptAndUpdate(); + } + + @traceDecorators.error('Failed to display conda inherit env prompt') + public async promptAndUpdate() { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + condaInheritEnvPromptKey, + true + ); + if (!notificationPromptEnabled.value) { + return; + } + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.moreInfo()]; + const telemetrySelections: ['Yes', 'No', 'More Info'] = ['Yes', 'No', 'More Info']; + const selection = await this.appShell.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts); + sendTelemetryEvent(EventName.CONDA_INHERIT_ENV_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await this.workspaceService + .getConfiguration('terminal') + .update('integrated.inheritEnv', false, ConfigurationTarget.Global); + } else if (selection === prompts[1]) { + await notificationPromptEnabled.updateValue(false); + } else if (selection === prompts[2]) { + this.browserService.launch('https://aka.ms/AA66i8f'); + } + } + + @traceDecorators.error('Failed to check whether to display prompt for conda inherit env setting') + public async shouldShowPrompt(resource: Uri): Promise { + if (this.hasPromptBeenShownInCurrentSession) { + return false; + } + if (this.platformService.isWindows) { + return false; + } + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter || interpreter.envType !== EnvironmentType.Conda) { + return false; + } + const setting = this.workspaceService + .getConfiguration('terminal', resource) + .inspect('integrated.inheritEnv'); + if (!setting) { + traceError( + 'WorkspaceConfiguration.inspect returns `undefined` for setting `terminal.integrated.inheritEnv`' + ); + return false; + } + if ( + setting.globalValue !== undefined || + setting.workspaceValue !== undefined || + setting.workspaceFolderValue !== undefined + ) { + return false; + } + this.hasPromptBeenShownInCurrentSession = true; + return true; + } +} diff --git a/src/client/interpreter/virtualEnvs/index.ts b/src/client/interpreter/virtualEnvs/index.ts new file mode 100644 index 000000000000..0886b1df99a4 --- /dev/null +++ b/src/client/interpreter/virtualEnvs/index.ts @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { IFileSystem, IPlatformService } from '../../common/platform/types'; +import { IProcessServiceFactory } from '../../common/process/types'; +import { getAllScripts as getNonWindowsScripts } from '../../common/terminal/environmentActivationProviders/bash'; +import { getAllScripts as getWindowsScripts } from '../../common/terminal/environmentActivationProviders/commandPrompt'; +import { ICurrentProcess, IPathUtils } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import * as globalenvs from '../../pythonEnvironments/discovery/globalenv'; +import * as subenvs from '../../pythonEnvironments/discovery/subenv'; +import { EnvironmentType } from '../../pythonEnvironments/info'; +import { IInterpreterLocatorService, IPipEnvService, PIPENV_SERVICE } from '../contracts'; +import { IVirtualEnvironmentManager } from './types'; + +@injectable() +export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { + private processServiceFactory: IProcessServiceFactory; + private pipEnvService: IPipEnvService; + private fs: IFileSystem; + private workspaceService: IWorkspaceService; + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { + this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); + this.fs = serviceContainer.get(IFileSystem); + this.pipEnvService = serviceContainer.get( + IInterpreterLocatorService, + PIPENV_SERVICE + ) as IPipEnvService; + this.workspaceService = serviceContainer.get(IWorkspaceService); + } + + public async getEnvironmentName(pythonPath: string, resource?: Uri): Promise { + const finders = subenvs.getNameFinders( + await this.getWorkspaceRoot(resource), + path.dirname, + path.basename, + // We use a closure on "this". + (d: string, p: string) => this.pipEnvService.isRelatedPipEnvironment(d, p) + ); + return (await subenvs.getName(pythonPath, finders)) || ''; + } + + public async getEnvironmentType(pythonPath: string, resource?: Uri): Promise { + const pathUtils = this.serviceContainer.get(IPathUtils); + const plat = this.serviceContainer.get(IPlatformService); + const candidates = plat.isWindows ? getWindowsScripts(path.join) : getNonWindowsScripts(); + const finders = subenvs.getTypeFinders( + pathUtils.home, + candidates, + path.sep, + path.join, + path.dirname, + () => this.getWorkspaceRoot(resource), + (d: string, p: string) => this.pipEnvService.isRelatedPipEnvironment(d, p), + (n: string) => { + const curProc = this.serviceContainer.get(ICurrentProcess); + return curProc.env[n]; + }, + (n: string) => this.fs.fileExists(n), + async (c: string, a: string[]) => { + const processService = await this.processServiceFactory.create(resource); + return processService.exec(c, a); + } + ); + return (await subenvs.getType(pythonPath, finders)) || EnvironmentType.Unknown; + } + + public async isVenvEnvironment(pythonPath: string) { + const find = subenvs.getVenvTypeFinder( + path.dirname, + path.join, + // We use a closure on "this". + (n: string) => this.fs.fileExists(n) + ); + return (await find(pythonPath)) === EnvironmentType.Venv; + } + + public async isPyEnvEnvironment(pythonPath: string, resource?: Uri) { + const pathUtils = this.serviceContainer.get(IPathUtils); + const find = globalenvs.getPyenvTypeFinder( + pathUtils.home, + path.sep, + path.join, + (n: string) => { + const curProc = this.serviceContainer.get(ICurrentProcess); + return curProc.env[n]; + }, + async (c: string, a: string[]) => { + const processService = await this.processServiceFactory.create(resource); + return processService.exec(c, a); + } + ); + return (await find(pythonPath)) === EnvironmentType.Pyenv; + } + + public async isPipEnvironment(pythonPath: string, resource?: Uri) { + const find = subenvs.getPipenvTypeFinder( + () => this.getWorkspaceRoot(resource), + // We use a closure on "this". + (d: string, p: string) => this.pipEnvService.isRelatedPipEnvironment(d, p) + ); + return (await find(pythonPath)) === EnvironmentType.Pipenv; + } + + public async getPyEnvRoot(resource?: Uri): Promise { + const pathUtils = this.serviceContainer.get(IPathUtils); + const find = globalenvs.getPyenvRootFinder( + pathUtils.home, + path.join, + (n: string) => { + const curProc = this.serviceContainer.get(ICurrentProcess); + return curProc.env[n]; + }, + async (c: string, a: string[]) => { + const processService = await this.processServiceFactory.create(resource); + return processService.exec(c, a); + } + ); + return find(); + } + + public async isVirtualEnvironment(pythonPath: string) { + const plat = this.serviceContainer.get(IPlatformService); + const candidates = plat.isWindows ? getWindowsScripts(path.join) : getNonWindowsScripts(); + const find = subenvs.getVirtualenvTypeFinder( + candidates, + path.dirname, + path.join, + // We use a closure on "this". + (n: string) => this.fs.fileExists(n) + ); + return (await find(pythonPath)) === EnvironmentType.VirtualEnv; + } + + private async getWorkspaceRoot(resource?: Uri): Promise { + const defaultWorkspaceUri = this.workspaceService.hasWorkspaceFolders + ? this.workspaceService.workspaceFolders![0].uri + : undefined; + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + const uri = workspaceFolder ? workspaceFolder.uri : defaultWorkspaceUri; + return uri ? uri.fsPath : undefined; + } +} diff --git a/src/client/interpreter/virtualEnvs/types.ts b/src/client/interpreter/virtualEnvs/types.ts new file mode 100644 index 000000000000..3781e4926d89 --- /dev/null +++ b/src/client/interpreter/virtualEnvs/types.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { EnvironmentType } from '../../pythonEnvironments/info'; +export const IVirtualEnvironmentManager = Symbol('VirtualEnvironmentManager'); +export interface IVirtualEnvironmentManager { + getEnvironmentName(pythonPath: string, resource?: Uri): Promise; + getEnvironmentType(pythonPath: string, resource?: Uri): Promise; + getPyEnvRoot(resource?: Uri): Promise; +} diff --git a/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts new file mode 100644 index 000000000000..c1056cadab6d --- /dev/null +++ b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, named } from 'inversify'; +import { ConfigurationTarget, Disposable, Uri } from 'vscode'; +import { IExtensionActivationService } from '../../activation/types'; +import { IApplicationShell } from '../../common/application/types'; +import { traceDecorators } from '../../common/logger'; +import { IDisposableRegistry, IPersistentStateFactory } from '../../common/types'; +import { sleep } from '../../common/utils/async'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IPythonPathUpdaterServiceManager } from '../configuration/types'; +import { + IInterpreterHelper, + IInterpreterLocatorService, + IInterpreterWatcherBuilder, + WORKSPACE_VIRTUAL_ENV_SERVICE +} from '../contracts'; + +const doNotDisplayPromptStateKey = 'MESSAGE_KEY_FOR_VIRTUAL_ENV'; +@injectable() +export class VirtualEnvironmentPrompt implements IExtensionActivationService { + constructor( + @inject(IInterpreterWatcherBuilder) private readonly builder: IInterpreterWatcherBuilder, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPythonPathUpdaterServiceManager) + private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @inject(IInterpreterLocatorService) + @named(WORKSPACE_VIRTUAL_ENV_SERVICE) + private readonly locator: IInterpreterLocatorService, + @inject(IDisposableRegistry) private readonly disposableRegistry: Disposable[], + @inject(IApplicationShell) private readonly appShell: IApplicationShell + ) {} + + public async activate(resource: Uri): Promise { + const watcher = await this.builder.getWorkspaceVirtualEnvInterpreterWatcher(resource); + watcher.onDidCreate( + () => { + this.handleNewEnvironment(resource).ignoreErrors(); + }, + this, + this.disposableRegistry + ); + } + + @traceDecorators.error('Error in event handler for detection of new environment') + protected async handleNewEnvironment(resource: Uri): Promise { + // Wait for a while, to ensure environment gets created and is accessible (as this is slow on Windows) + await sleep(1000); + const interpreters = await this.locator.getInterpreters(resource); + const interpreter = this.helper.getBestInterpreter(interpreters); + if (!interpreter) { + return; + } + await this.notifyUser(interpreter, resource); + } + protected async notifyUser(interpreter: PythonEnvironment, resource: Uri): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createWorkspacePersistentState( + doNotDisplayPromptStateKey, + true + ); + if (!notificationPromptEnabled.value) { + return; + } + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; + const telemetrySelections: ['Yes', 'No', 'Ignore'] = ['Yes', 'No', 'Ignore']; + const selection = await this.appShell.showInformationMessage( + Interpreters.environmentPromptMessage(), + ...prompts + ); + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await this.pythonPathUpdaterService.updatePythonPath( + interpreter.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + resource + ); + } else if (selection === prompts[2]) { + await notificationPromptEnabled.updateValue(false); + } + } +} diff --git a/src/client/ioc/container.ts b/src/client/ioc/container.ts new file mode 100644 index 000000000000..07db6f95bca2 --- /dev/null +++ b/src/client/ioc/container.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { EventEmitter } from 'events'; +import { Container, decorate, injectable, interfaces } from 'inversify'; +import { traceWarning } from '../common/logger'; +import { Abstract, IServiceContainer, Newable } from './types'; + +// This needs to be done once, hence placed in a common location. +// Used by UnitTestSockerServer and also the extension unit tests. +// Place within try..catch, as this can only be done once (it's +// possible another extension would perform this before our extension). +try { + decorate(injectable(), EventEmitter); +} catch (ex) { + traceWarning('Failed to decorate EventEmitter for DI (possibly already decorated by another Extension)', ex); +} + +@injectable() +export class ServiceContainer implements IServiceContainer { + constructor(private container: Container) {} + public get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T { + return name ? this.container.getNamed(serviceIdentifier, name) : this.container.get(serviceIdentifier); + } + public getAll( + serviceIdentifier: string | symbol | Newable | Abstract, + name?: string | number | symbol | undefined + ): T[] { + return name + ? this.container.getAllNamed(serviceIdentifier, name) + : this.container.getAll(serviceIdentifier); + } + public tryGet( + serviceIdentifier: interfaces.ServiceIdentifier, + name?: string | number | symbol | undefined + ): T | undefined { + try { + return name + ? this.container.getNamed(serviceIdentifier, name) + : this.container.get(serviceIdentifier); + } catch { + // This might happen after the container has been destroyed + } + } +} diff --git a/src/client/ioc/index.ts b/src/client/ioc/index.ts new file mode 100644 index 000000000000..0ee4070d5ded --- /dev/null +++ b/src/client/ioc/index.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceContainer } from './types'; + +let container: IServiceContainer; +export function getServiceContainer() { + return container; +} +export function setServiceContainer(serviceContainer: IServiceContainer) { + container = serviceContainer; +} diff --git a/src/client/ioc/serviceManager.ts b/src/client/ioc/serviceManager.ts new file mode 100644 index 000000000000..837308dff514 --- /dev/null +++ b/src/client/ioc/serviceManager.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { Container, injectable, interfaces } from 'inversify'; + +import { Abstract, ClassType, IServiceManager, Newable } from './types'; + +type identifier = string | symbol | Newable | Abstract; + +@injectable() +export class ServiceManager implements IServiceManager { + constructor(private container: Container) {} + public add( + serviceIdentifier: identifier, + // tslint:disable-next-line:no-any + constructor: new (...args: any[]) => T, + name?: string | number | symbol | undefined, + bindings?: symbol[] + ): void { + if (name) { + this.container.bind(serviceIdentifier).to(constructor).whenTargetNamed(name); + } else { + this.container.bind(serviceIdentifier).to(constructor); + } + + if (bindings) { + bindings.forEach((binding) => { + this.addBinding(serviceIdentifier, binding); + }); + } + } + public addFactory( + factoryIdentifier: interfaces.ServiceIdentifier>, + factoryMethod: interfaces.FactoryCreator + ): void { + this.container.bind>(factoryIdentifier).toFactory(factoryMethod); + } + + public addBinding(from: identifier, to: identifier): void { + this.container.bind(to).toService(from); + } + + public addSingleton( + serviceIdentifier: identifier, + // tslint:disable-next-line:no-any + constructor: new (...args: any[]) => T, + name?: string | number | symbol | undefined, + bindings?: symbol[] + ): void { + if (name) { + this.container.bind(serviceIdentifier).to(constructor).inSingletonScope().whenTargetNamed(name); + } else { + this.container.bind(serviceIdentifier).to(constructor).inSingletonScope(); + } + + if (bindings) { + bindings.forEach((binding) => { + this.addBinding(serviceIdentifier, binding); + }); + } + } + + public addSingletonInstance( + serviceIdentifier: identifier, + instance: T, + name?: string | number | symbol | undefined + ): void { + if (name) { + this.container.bind(serviceIdentifier).toConstantValue(instance).whenTargetNamed(name); + } else { + this.container.bind(serviceIdentifier).toConstantValue(instance); + } + } + public get(serviceIdentifier: identifier, name?: string | number | symbol | undefined): T { + return name ? this.container.getNamed(serviceIdentifier, name) : this.container.get(serviceIdentifier); + } + public tryGet(serviceIdentifier: identifier, name?: string | number | symbol | undefined): T | undefined { + try { + return name + ? this.container.getNamed(serviceIdentifier, name) + : this.container.get(serviceIdentifier); + } catch { + // This might happen after the container has been destroyed + } + } + public getAll(serviceIdentifier: identifier, name?: string | number | symbol | undefined): T[] { + return name + ? this.container.getAllNamed(serviceIdentifier, name) + : this.container.getAll(serviceIdentifier); + } + + public rebind( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol + ): void { + if (name) { + this.container.rebind(serviceIdentifier).to(constructor).whenTargetNamed(name); + } else { + this.container.rebind(serviceIdentifier).to(constructor); + } + } + + public rebindSingleton( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol + ): void { + if (name) { + this.container.rebind(serviceIdentifier).to(constructor).inSingletonScope().whenTargetNamed(name); + } else { + this.container.rebind(serviceIdentifier).to(constructor).inSingletonScope(); + } + } + + public rebindInstance( + serviceIdentifier: interfaces.ServiceIdentifier, + instance: T, + name?: string | number | symbol + ): void { + if (name) { + this.container.rebind(serviceIdentifier).toConstantValue(instance).whenTargetNamed(name); + } else { + this.container.rebind(serviceIdentifier).toConstantValue(instance); + } + } + + public dispose() { + this.container.unbindAll(); + this.container.unload(); + } +} diff --git a/src/client/ioc/types.ts b/src/client/ioc/types.ts new file mode 100644 index 000000000000..079fca02e5bd --- /dev/null +++ b/src/client/ioc/types.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { interfaces } from 'inversify'; +import { IDisposable } from '../common/types'; + +//tslint:disable:callable-types +// tslint:disable-next-line:interface-name +export interface Newable { + // tslint:disable-next-line:no-any + new (...args: any[]): T; +} +//tslint:enable:callable-types + +// tslint:disable-next-line:interface-name +export interface Abstract { + prototype: T; +} + +//tslint:disable:callable-types +export type ClassType = { + // tslint:disable-next-line:no-any + new (...args: any[]): T; +}; +//tslint:enable:callable-types + +export const IServiceManager = Symbol('IServiceManager'); + +export interface IServiceManager extends IDisposable { + add( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol | undefined, + bindings?: symbol[] + ): void; + addSingleton( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol, + bindings?: symbol[] + ): void; + addSingletonInstance( + serviceIdentifier: interfaces.ServiceIdentifier, + instance: T, + name?: string | number | symbol + ): void; + addFactory( + factoryIdentifier: interfaces.ServiceIdentifier>, + factoryMethod: interfaces.FactoryCreator + ): void; + addBinding(from: interfaces.ServiceIdentifier, to: interfaces.ServiceIdentifier): void; + get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T; + tryGet(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T | undefined; + getAll(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T[]; + rebind( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol + ): void; + rebindSingleton( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol + ): void; + rebindInstance( + serviceIdentifier: interfaces.ServiceIdentifier, + instance: T, + name?: string | number | symbol + ): void; +} + +export const IServiceContainer = Symbol('IServiceContainer'); +export interface IServiceContainer { + get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T; + getAll(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T[]; + tryGet(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T | undefined; +} diff --git a/src/client/language/braceCounter.ts b/src/client/language/braceCounter.ts new file mode 100644 index 000000000000..30d91b537544 --- /dev/null +++ b/src/client/language/braceCounter.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IToken, TokenType } from './types'; + +class BracePair { + public readonly openBrace: TokenType; + public readonly closeBrace: TokenType; + + constructor(openBrace: TokenType, closeBrace: TokenType) { + this.openBrace = openBrace; + this.closeBrace = closeBrace; + } +} + +class Stack { + private store: IToken[] = []; + public push(val: IToken) { + this.store.push(val); + } + public pop(): IToken | undefined { + return this.store.pop(); + } + public get length(): number { + return this.store.length; + } +} + +export class BraceCounter { + private readonly bracePairs: BracePair[] = [ + new BracePair(TokenType.OpenBrace, TokenType.CloseBrace), + new BracePair(TokenType.OpenBracket, TokenType.CloseBracket), + new BracePair(TokenType.OpenCurly, TokenType.CloseCurly) + ]; + private braceStacks: Stack[] = [new Stack(), new Stack(), new Stack()]; + + public get count(): number { + let c = 0; + for (const s of this.braceStacks) { + c += s.length; + } + return c; + } + + public isOpened(type: TokenType): boolean { + for (let i = 0; i < this.bracePairs.length; i += 1) { + const pair = this.bracePairs[i]; + if (pair.openBrace === type || pair.closeBrace === type) { + return this.braceStacks[i].length > 0; + } + } + return false; + } + + public countBrace(brace: IToken): boolean { + for (let i = 0; i < this.bracePairs.length; i += 1) { + const pair = this.bracePairs[i]; + if (pair.openBrace === brace.type) { + this.braceStacks[i].push(brace); + return true; + } + if (pair.closeBrace === brace.type) { + if (this.braceStacks[i].length > 0) { + this.braceStacks[i].pop(); + } + return true; + } + } + return false; + } +} diff --git a/src/client/language/characterStream.ts b/src/client/language/characterStream.ts new file mode 100644 index 000000000000..09f3bed33f9d --- /dev/null +++ b/src/client/language/characterStream.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// tslint:disable-next-line:import-name +import Char from 'typescript-char'; +import { isLineBreak, isWhiteSpace } from './characters'; +import { TextIterator } from './textIterator'; +import { ICharacterStream, ITextIterator } from './types'; + +export class CharacterStream implements ICharacterStream { + private text: ITextIterator; + private _position: number; + private _currentChar: number; + private _isEndOfStream: boolean; + + constructor(text: string | ITextIterator) { + this.text = typeof text === 'string' ? new TextIterator(text) : text; + this._position = 0; + this._currentChar = text.length > 0 ? text.charCodeAt(0) : 0; + this._isEndOfStream = text.length === 0; + } + + public getText(): string { + return this.text.getText(); + } + + public get position(): number { + return this._position; + } + + public set position(value: number) { + this._position = value; + this.checkBounds(); + } + + public get currentChar(): number { + return this._currentChar; + } + + public get nextChar(): number { + return this.position + 1 < this.text.length ? this.text.charCodeAt(this.position + 1) : 0; + } + + public get prevChar(): number { + return this.position - 1 >= 0 ? this.text.charCodeAt(this.position - 1) : 0; + } + + public isEndOfStream(): boolean { + return this._isEndOfStream; + } + + public lookAhead(offset: number): number { + const pos = this._position + offset; + return pos < 0 || pos >= this.text.length ? 0 : this.text.charCodeAt(pos); + } + + public advance(offset: number) { + this.position += offset; + } + + public moveNext(): boolean { + if (this._position < this.text.length - 1) { + // Most common case, no need to check bounds extensively + this._position += 1; + this._currentChar = this.text.charCodeAt(this._position); + return true; + } + this.advance(1); + return !this.isEndOfStream(); + } + + public isAtWhiteSpace(): boolean { + return isWhiteSpace(this.currentChar); + } + + public isAtLineBreak(): boolean { + return isLineBreak(this.currentChar); + } + + public skipLineBreak(): void { + if (this._currentChar === Char.CarriageReturn) { + this.moveNext(); + if (this.currentChar === Char.LineFeed) { + this.moveNext(); + } + } else if (this._currentChar === Char.LineFeed) { + this.moveNext(); + } + } + + public skipWhitespace(): void { + while (!this.isEndOfStream() && this.isAtWhiteSpace()) { + this.moveNext(); + } + } + + public skipToEol(): void { + while (!this.isEndOfStream() && !this.isAtLineBreak()) { + this.moveNext(); + } + } + + public skipToWhitespace(): void { + while (!this.isEndOfStream() && !this.isAtWhiteSpace()) { + this.moveNext(); + } + } + + public isAtString(): boolean { + return this.currentChar === Char.SingleQuote || this.currentChar === Char.DoubleQuote; + } + + public charCodeAt(index: number): number { + return this.text.charCodeAt(index); + } + + public get length(): number { + return this.text.length; + } + + private checkBounds(): void { + if (this._position < 0) { + this._position = 0; + } + + this._isEndOfStream = this._position >= this.text.length; + if (this._isEndOfStream) { + this._position = this.text.length; + } + + this._currentChar = this._isEndOfStream ? 0 : this.text.charCodeAt(this._position); + } +} diff --git a/src/client/language/characters.ts b/src/client/language/characters.ts new file mode 100644 index 000000000000..50d5f716a039 --- /dev/null +++ b/src/client/language/characters.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable-next-line:import-name +import Char from 'typescript-char'; +import { getUnicodeCategory, UnicodeCategory } from './unicode'; + +export function isIdentifierStartChar(ch: number) { + switch (ch) { + // Underscore is explicitly allowed to start an identifier + case Char.Underscore: + return true; + // Characters with the Other_ID_Start property + case 0x1885: + case 0x1886: + case 0x2118: + case 0x212e: + case 0x309b: + case 0x309c: + return true; + default: + break; + } + + const cat = getUnicodeCategory(ch); + switch (cat) { + // Supported categories for starting an identifier + case UnicodeCategory.UppercaseLetter: + case UnicodeCategory.LowercaseLetter: + case UnicodeCategory.TitlecaseLetter: + case UnicodeCategory.ModifierLetter: + case UnicodeCategory.OtherLetter: + case UnicodeCategory.LetterNumber: + return true; + default: + break; + } + return false; +} + +export function isIdentifierChar(ch: number) { + if (isIdentifierStartChar(ch)) { + return true; + } + + switch (ch) { + // Characters with the Other_ID_Continue property + case 0x00b7: + case 0x0387: + case 0x1369: + case 0x136a: + case 0x136b: + case 0x136c: + case 0x136d: + case 0x136e: + case 0x136f: + case 0x1370: + case 0x1371: + case 0x19da: + return true; + default: + break; + } + + switch (getUnicodeCategory(ch)) { + // Supported categories for continuing an identifier + case UnicodeCategory.NonSpacingMark: + case UnicodeCategory.SpacingCombiningMark: + case UnicodeCategory.DecimalDigitNumber: + case UnicodeCategory.ConnectorPunctuation: + return true; + default: + break; + } + return false; +} + +export function isWhiteSpace(ch: number): boolean { + return ch <= Char.Space || ch === 0x200b; // Unicode whitespace +} + +export function isLineBreak(ch: number): boolean { + return ch === Char.CarriageReturn || ch === Char.LineFeed; +} + +export function isNumber(ch: number): boolean { + return (ch >= Char._0 && ch <= Char._9) || ch === Char.Underscore; +} + +export function isDecimal(ch: number): boolean { + return (ch >= Char._0 && ch <= Char._9) || ch === Char.Underscore; +} + +export function isHex(ch: number): boolean { + return isDecimal(ch) || (ch >= Char.a && ch <= Char.f) || (ch >= Char.A && ch <= Char.F) || ch === Char.Underscore; +} + +export function isOctal(ch: number): boolean { + return (ch >= Char._0 && ch <= Char._7) || ch === Char.Underscore; +} + +export function isBinary(ch: number): boolean { + return ch === Char._0 || ch === Char._1 || ch === Char.Underscore; +} diff --git a/src/client/language/iterableTextRange.ts b/src/client/language/iterableTextRange.ts new file mode 100644 index 000000000000..c69c62132e73 --- /dev/null +++ b/src/client/language/iterableTextRange.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ITextRange, ITextRangeCollection } from './types'; + +export class IterableTextRange implements Iterable { + constructor(private textRangeCollection: ITextRangeCollection) {} + public [Symbol.iterator](): Iterator { + let index = -1; + + return { + next: (): IteratorResult => { + if (index < this.textRangeCollection.count - 1) { + return { + done: false, + value: this.textRangeCollection.getItemAt((index += 1)) + }; + } else { + return { + done: true, + // tslint:disable-next-line:no-any + value: undefined as any + }; + } + } + }; + } +} diff --git a/src/client/language/languageConfiguration.ts b/src/client/language/languageConfiguration.ts new file mode 100644 index 000000000000..9e80d3a3b704 --- /dev/null +++ b/src/client/language/languageConfiguration.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { IndentAction, LanguageConfiguration } from 'vscode'; +import { verboseRegExp } from '../common/utils/regexp'; + +// tslint:disable:no-multiline-string + +// tslint:disable-next-line:max-func-body-length +export function getLanguageConfiguration(): LanguageConfiguration { + return { + onEnterRules: [ + // multi-line separator + { + beforeText: verboseRegExp(` + ^ + (?! \\s+ \\\\ ) + [^#\n]+ + \\\\ + $ + `), + action: { + indentAction: IndentAction.Indent + } + }, + // continue comments + { + beforeText: /^\s*#.*/, + afterText: /.+$/, + action: { + indentAction: IndentAction.None, + appendText: '# ' + } + }, + // indent on enter (block-beginning statements) + { + /** + * This does not handle all cases. However, it does handle nearly all usage. + * Here's what it does not cover: + * - the statement is split over multiple lines (and hence the ":" is on a different line) + * - the code block is inlined (after the ":") + * - there are multiple statements on the line (separated by semicolons) + * Also note that `lambda` is purposefully excluded. + */ + beforeText: verboseRegExp(` + ^ + \\s* + (?: + (?: + (?: + class | + def | + async \\s+ def | + except | + for | + async \\s+ for | + if | + elif | + while | + with | + async \\s+ with + ) + \\b .* + ) | + else | + try | + finally + ) + \\s* + [:] + \\s* + (?: [#] .* )? + $ + `), + action: { + indentAction: IndentAction.Indent + } + }, + // outdent on enter (block-ending statements) + { + beforeText: verboseRegExp(` + ^ + (?: + (?: + \\s* + (?: + pass | + raise \\s+ [^#\\s] [^#]* + ) + ) | + (?: + \\s+ + (?: + raise | + break | + continue + ) + ) + ) + \\s* + (?: [#] .* )? + $ + `), + action: { + indentAction: IndentAction.Outdent + } + } + // Note that we do not currently have an auto-dedent + // solution for "elif", "else", "except", and "finally". + // We had one but had to remove it (see issue #6886). + ] + }; +} diff --git a/src/client/language/textBuilder.ts b/src/client/language/textBuilder.ts new file mode 100644 index 000000000000..e11f2a1299c4 --- /dev/null +++ b/src/client/language/textBuilder.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { isWhiteSpace } from './characters'; + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export class TextBuilder { + private segments: string[] = []; + + public getText(): string { + if (this.isLastWhiteSpace()) { + this.segments.pop(); + } + return this.segments.join(''); + } + + public softAppendSpace(count: number = 1): void { + if (this.segments.length === 0) { + return; + } + if (this.isLastWhiteSpace()) { + count = count - 1; + } + for (let i = 0; i < count; i += 1) { + this.segments.push(' '); + } + } + + public append(text: string): void { + this.segments.push(text); + } + + private isLastWhiteSpace(): boolean { + return this.segments.length > 0 && this.isWhitespace(this.segments[this.segments.length - 1]); + } + + private isWhitespace(s: string): boolean { + for (let i = 0; i < s.length; i += 1) { + if (!isWhiteSpace(s.charCodeAt(i))) { + return false; + } + } + return true; + } +} diff --git a/src/client/language/textIterator.ts b/src/client/language/textIterator.ts new file mode 100644 index 000000000000..6037cbf67cb9 --- /dev/null +++ b/src/client/language/textIterator.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { Position, Range, TextDocument } from 'vscode'; +import { ITextIterator } from './types'; + +export class TextIterator implements ITextIterator { + private text: string; + + constructor(text: string) { + this.text = text; + } + + public charCodeAt(index: number): number { + if (index >= 0 && index < this.text.length) { + return this.text.charCodeAt(index); + } + return 0; + } + + public get length(): number { + return this.text.length; + } + + public getText(): string { + return this.text; + } +} + +export class DocumentTextIterator implements ITextIterator { + public readonly length: number; + + private document: TextDocument; + + constructor(document: TextDocument) { + this.document = document; + + const lastIndex = this.document.lineCount - 1; + const lastLine = this.document.lineAt(lastIndex); + const end = new Position(lastIndex, lastLine.range.end.character); + this.length = this.document.offsetAt(end); + } + + public charCodeAt(index: number): number { + const position = this.document.positionAt(index); + return this.document.getText(new Range(position, position.translate(0, 1))).charCodeAt(position.character); + } + + public getText(): string { + return this.document.getText(); + } +} diff --git a/src/client/language/textRangeCollection.ts b/src/client/language/textRangeCollection.ts new file mode 100644 index 000000000000..8ce5a744c9a6 --- /dev/null +++ b/src/client/language/textRangeCollection.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { ITextRange, ITextRangeCollection } from './types'; + +export class TextRangeCollection implements ITextRangeCollection { + private items: T[]; + + constructor(items: T[]) { + this.items = items; + } + + public get start(): number { + return this.items.length > 0 ? this.items[0].start : 0; + } + + public get end(): number { + return this.items.length > 0 ? this.items[this.items.length - 1].end : 0; + } + + public get length(): number { + return this.end - this.start; + } + + public get count(): number { + return this.items.length; + } + + public contains(position: number) { + return position >= this.start && position < this.end; + } + + public getItemAt(index: number): T { + if (index < 0 || index >= this.items.length) { + throw new Error('index is out of range'); + } + return this.items[index] as T; + } + + public getItemAtPosition(position: number): number { + if (this.count === 0) { + return -1; + } + if (position < this.start) { + return -1; + } + if (position >= this.end) { + return -1; + } + + let min = 0; + let max = this.count - 1; + + while (min <= max) { + const mid = Math.floor(min + (max - min) / 2); + const item = this.items[mid]; + + if (item.start === position) { + return mid; + } + + if (position < item.start) { + max = mid - 1; + } else { + min = mid + 1; + } + } + return -1; + } + + public getItemContaining(position: number): number { + if (this.count === 0) { + return -1; + } + if (position < this.start) { + return -1; + } + if (position > this.end) { + return -1; + } + + let min = 0; + let max = this.count - 1; + + while (min <= max) { + const mid = Math.floor(min + (max - min) / 2); + const item = this.items[mid]; + + if (item.contains(position)) { + return mid; + } + if (mid < this.count - 1 && item.end <= position && position < this.items[mid + 1].start) { + return -1; + } + + if (position < item.start) { + max = mid - 1; + } else { + min = mid + 1; + } + } + return -1; + } +} diff --git a/src/client/language/tokenizer.ts b/src/client/language/tokenizer.ts new file mode 100644 index 000000000000..f51ceaa350c7 --- /dev/null +++ b/src/client/language/tokenizer.ts @@ -0,0 +1,514 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// tslint:disable-next-line:import-name +import Char from 'typescript-char'; +import { isBinary, isDecimal, isHex, isIdentifierChar, isIdentifierStartChar, isOctal } from './characters'; +import { CharacterStream } from './characterStream'; +import { TextRangeCollection } from './textRangeCollection'; +import { + ICharacterStream, + ITextRangeCollection, + IToken, + ITokenizer, + TextRange, + TokenizerMode, + TokenType +} from './types'; + +enum QuoteType { + None, + Single, + Double, + TripleSingle, + TripleDouble +} + +class Token extends TextRange implements IToken { + public readonly type: TokenType; + + constructor(type: TokenType, start: number, length: number) { + super(start, length); + this.type = type; + } +} + +export class Tokenizer implements ITokenizer { + private cs: ICharacterStream = new CharacterStream(''); + private tokens: IToken[] = []; + private mode = TokenizerMode.Full; + + public tokenize(text: string): ITextRangeCollection; + public tokenize(text: string, start: number, length: number, mode: TokenizerMode): ITextRangeCollection; + + public tokenize(text: string, start?: number, length?: number, mode?: TokenizerMode): ITextRangeCollection { + if (start === undefined) { + start = 0; + } else if (start < 0 || start >= text.length) { + throw new Error('Invalid range start'); + } + + if (length === undefined) { + length = text.length; + } else if (length < 0 || start + length > text.length) { + throw new Error('Invalid range length'); + } + + this.mode = mode !== undefined ? mode : TokenizerMode.Full; + + this.cs = new CharacterStream(text); + this.cs.position = start; + + const end = start + length; + while (!this.cs.isEndOfStream()) { + this.AddNextToken(); + if (this.cs.position >= end) { + break; + } + } + return new TextRangeCollection(this.tokens); + } + + private AddNextToken(): void { + this.cs.skipWhitespace(); + if (this.cs.isEndOfStream()) { + return; + } + + if (!this.handleCharacter()) { + this.cs.moveNext(); + } + } + + // tslint:disable-next-line:cyclomatic-complexity + private handleCharacter(): boolean { + // f-strings, b-strings, etc + const stringPrefixLength = this.getStringPrefixLength(); + if (stringPrefixLength >= 0) { + // Indeed a string + this.cs.advance(stringPrefixLength); + + const quoteType = this.getQuoteType(); + if (quoteType !== QuoteType.None) { + this.handleString(quoteType, stringPrefixLength); + return true; + } + } + if (this.cs.currentChar === Char.Hash) { + this.handleComment(); + return true; + } + if (this.mode === TokenizerMode.CommentsAndStrings) { + return false; + } + + switch (this.cs.currentChar) { + case Char.OpenParenthesis: + this.tokens.push(new Token(TokenType.OpenBrace, this.cs.position, 1)); + break; + case Char.CloseParenthesis: + this.tokens.push(new Token(TokenType.CloseBrace, this.cs.position, 1)); + break; + case Char.OpenBracket: + this.tokens.push(new Token(TokenType.OpenBracket, this.cs.position, 1)); + break; + case Char.CloseBracket: + this.tokens.push(new Token(TokenType.CloseBracket, this.cs.position, 1)); + break; + case Char.OpenBrace: + this.tokens.push(new Token(TokenType.OpenCurly, this.cs.position, 1)); + break; + case Char.CloseBrace: + this.tokens.push(new Token(TokenType.CloseCurly, this.cs.position, 1)); + break; + case Char.Comma: + this.tokens.push(new Token(TokenType.Comma, this.cs.position, 1)); + break; + case Char.Semicolon: + this.tokens.push(new Token(TokenType.Semicolon, this.cs.position, 1)); + break; + case Char.Colon: + this.tokens.push(new Token(TokenType.Colon, this.cs.position, 1)); + break; + default: + if (this.isPossibleNumber()) { + if (this.tryNumber()) { + return true; + } + } + if (this.cs.currentChar === Char.Period) { + this.tokens.push(new Token(TokenType.Operator, this.cs.position, 1)); + break; + } + if (!this.tryIdentifier()) { + if (!this.tryOperator()) { + this.handleUnknown(); + } + } + return true; + } + return false; + } + + private tryIdentifier(): boolean { + const start = this.cs.position; + if (isIdentifierStartChar(this.cs.currentChar)) { + this.cs.moveNext(); + while (isIdentifierChar(this.cs.currentChar)) { + this.cs.moveNext(); + } + } + if (this.cs.position > start) { + // const text = this.cs.getText().substr(start, this.cs.position - start); + // const type = this.keywords.find((value, index) => value === text) ? TokenType.Keyword : TokenType.Identifier; + this.tokens.push(new Token(TokenType.Identifier, start, this.cs.position - start)); + return true; + } + return false; + } + + // tslint:disable-next-line:cyclomatic-complexity + private isPossibleNumber(): boolean { + if (isDecimal(this.cs.currentChar)) { + return true; + } + + if (this.cs.currentChar === Char.Period && isDecimal(this.cs.nextChar)) { + return true; + } + + const next = this.cs.currentChar === Char.Hyphen || this.cs.currentChar === Char.Plus ? 1 : 0; + // Next character must be decimal or a dot otherwise + // it is not a number. No whitespace is allowed. + if (isDecimal(this.cs.lookAhead(next)) || this.cs.lookAhead(next) === Char.Period) { + // Check what previous token is, if any + if (this.tokens.length === 0) { + // At the start of the file this can only be a number + return true; + } + + const prev = this.tokens[this.tokens.length - 1]; + if ( + prev.type === TokenType.OpenBrace || + prev.type === TokenType.OpenBracket || + prev.type === TokenType.Comma || + prev.type === TokenType.Colon || + prev.type === TokenType.Semicolon || + prev.type === TokenType.Operator + ) { + return true; + } + } + + if (this.cs.lookAhead(next) === Char._0) { + const nextNext = this.cs.lookAhead(next + 1); + if (nextNext === Char.x || nextNext === Char.X) { + return true; + } + if (nextNext === Char.b || nextNext === Char.B) { + return true; + } + if (nextNext === Char.o || nextNext === Char.O) { + return true; + } + } + + return false; + } + + // tslint:disable-next-line:cyclomatic-complexity + private tryNumber(): boolean { + const start = this.cs.position; + let leadingSign = 0; + + if (this.cs.currentChar === Char.Hyphen || this.cs.currentChar === Char.Plus) { + this.cs.moveNext(); // Skip leading +/- + leadingSign = 1; + } + + if (this.cs.currentChar === Char._0) { + let radix = 0; + // Try hex => hexinteger: "0" ("x" | "X") (["_"] hexdigit)+ + if ((this.cs.nextChar === Char.x || this.cs.nextChar === Char.X) && isHex(this.cs.lookAhead(2))) { + this.cs.advance(2); + while (isHex(this.cs.currentChar)) { + this.cs.moveNext(); + } + radix = 16; + } + // Try binary => bininteger: "0" ("b" | "B") (["_"] bindigit)+ + if ((this.cs.nextChar === Char.b || this.cs.nextChar === Char.B) && isBinary(this.cs.lookAhead(2))) { + this.cs.advance(2); + while (isBinary(this.cs.currentChar)) { + this.cs.moveNext(); + } + radix = 2; + } + // Try octal => octinteger: "0" ("o" | "O") (["_"] octdigit)+ + if ((this.cs.nextChar === Char.o || this.cs.nextChar === Char.O) && isOctal(this.cs.lookAhead(2))) { + this.cs.advance(2); + while (isOctal(this.cs.currentChar)) { + this.cs.moveNext(); + } + radix = 8; + } + if (radix > 0) { + const text = this.cs.getText().substr(start + leadingSign, this.cs.position - start - leadingSign); + if (!isNaN(parseInt(text, radix))) { + this.tokens.push(new Token(TokenType.Number, start, text.length + leadingSign)); + return true; + } + } + } + + let decimal = false; + // Try decimal int => + // decinteger: nonzerodigit (["_"] digit)* | "0" (["_"] "0")* + // nonzerodigit: "1"..."9" + // digit: "0"..."9" + if (this.cs.currentChar >= Char._1 && this.cs.currentChar <= Char._9) { + while (isDecimal(this.cs.currentChar)) { + this.cs.moveNext(); + } + decimal = + this.cs.currentChar !== Char.Period && this.cs.currentChar !== Char.e && this.cs.currentChar !== Char.E; + } + + if (this.cs.currentChar === Char._0) { + // "0" (["_"] "0")* + while (this.cs.currentChar === Char._0 || this.cs.currentChar === Char.Underscore) { + this.cs.moveNext(); + } + decimal = + this.cs.currentChar !== Char.Period && this.cs.currentChar !== Char.e && this.cs.currentChar !== Char.E; + } + + if (decimal) { + const text = this.cs.getText().substr(start + leadingSign, this.cs.position - start - leadingSign); + if (!isNaN(parseInt(text, 10))) { + this.tokens.push(new Token(TokenType.Number, start, text.length + leadingSign)); + return true; + } + } + + // Floating point. Sign was already skipped over. + if ( + (this.cs.currentChar >= Char._0 && this.cs.currentChar <= Char._9) || + (this.cs.currentChar === Char.Period && this.cs.nextChar >= Char._0 && this.cs.nextChar <= Char._9) + ) { + if (this.skipFloatingPointCandidate(false)) { + const text = this.cs.getText().substr(start, this.cs.position - start); + if (!isNaN(parseFloat(text))) { + this.tokens.push(new Token(TokenType.Number, start, this.cs.position - start)); + return true; + } + } + } + + this.cs.position = start; + return false; + } + + // tslint:disable-next-line:cyclomatic-complexity + private tryOperator(): boolean { + let length = 0; + const nextChar = this.cs.nextChar; + switch (this.cs.currentChar) { + case Char.Plus: + case Char.Ampersand: + case Char.Bar: + case Char.Caret: + case Char.Equal: + case Char.ExclamationMark: + case Char.Percent: + case Char.Tilde: + length = nextChar === Char.Equal ? 2 : 1; + break; + + case Char.Hyphen: + length = nextChar === Char.Equal || nextChar === Char.Greater ? 2 : 1; + break; + + case Char.Asterisk: + if (nextChar === Char.Asterisk) { + length = this.cs.lookAhead(2) === Char.Equal ? 3 : 2; + } else { + length = nextChar === Char.Equal ? 2 : 1; + } + break; + + case Char.Slash: + if (nextChar === Char.Slash) { + length = this.cs.lookAhead(2) === Char.Equal ? 3 : 2; + } else { + length = nextChar === Char.Equal ? 2 : 1; + } + break; + + case Char.Less: + if (nextChar === Char.Greater) { + length = 2; + } else if (nextChar === Char.Less) { + length = this.cs.lookAhead(2) === Char.Equal ? 3 : 2; + } else { + length = nextChar === Char.Equal ? 2 : 1; + } + break; + + case Char.Greater: + if (nextChar === Char.Greater) { + length = this.cs.lookAhead(2) === Char.Equal ? 3 : 2; + } else { + length = nextChar === Char.Equal ? 2 : 1; + } + break; + + case Char.At: + length = nextChar === Char.Equal ? 2 : 1; + break; + + default: + return false; + } + this.tokens.push(new Token(TokenType.Operator, this.cs.position, length)); + this.cs.advance(length); + return length > 0; + } + + private handleUnknown(): boolean { + const start = this.cs.position; + this.cs.skipToWhitespace(); + const length = this.cs.position - start; + if (length > 0) { + this.tokens.push(new Token(TokenType.Unknown, start, length)); + return true; + } + return false; + } + + private handleComment(): void { + const start = this.cs.position; + this.cs.skipToEol(); + this.tokens.push(new Token(TokenType.Comment, start, this.cs.position - start)); + } + + // tslint:disable-next-line:cyclomatic-complexity + private getStringPrefixLength(): number { + if (this.cs.currentChar === Char.SingleQuote || this.cs.currentChar === Char.DoubleQuote) { + return 0; // Simple string, no prefix + } + + if (this.cs.nextChar === Char.SingleQuote || this.cs.nextChar === Char.DoubleQuote) { + switch (this.cs.currentChar) { + case Char.f: + case Char.F: + case Char.r: + case Char.R: + case Char.b: + case Char.B: + case Char.u: + case Char.U: + return 1; // single-char prefix like u"" or r"" + default: + break; + } + } + + if (this.cs.lookAhead(2) === Char.SingleQuote || this.cs.lookAhead(2) === Char.DoubleQuote) { + const prefix = this.cs.getText().substr(this.cs.position, 2).toLowerCase(); + switch (prefix) { + case 'rf': + case 'ur': + case 'br': + return 2; + default: + break; + } + } + return -1; + } + + private getQuoteType(): QuoteType { + if (this.cs.currentChar === Char.SingleQuote) { + return this.cs.nextChar === Char.SingleQuote && this.cs.lookAhead(2) === Char.SingleQuote + ? QuoteType.TripleSingle + : QuoteType.Single; + } + if (this.cs.currentChar === Char.DoubleQuote) { + return this.cs.nextChar === Char.DoubleQuote && this.cs.lookAhead(2) === Char.DoubleQuote + ? QuoteType.TripleDouble + : QuoteType.Double; + } + return QuoteType.None; + } + + private handleString(quoteType: QuoteType, stringPrefixLength: number): void { + const start = this.cs.position - stringPrefixLength; + if (quoteType === QuoteType.Single || quoteType === QuoteType.Double) { + this.cs.moveNext(); + this.skipToSingleEndQuote(quoteType === QuoteType.Single ? Char.SingleQuote : Char.DoubleQuote); + } else { + this.cs.advance(3); + this.skipToTripleEndQuote(quoteType === QuoteType.TripleSingle ? Char.SingleQuote : Char.DoubleQuote); + } + this.tokens.push(new Token(TokenType.String, start, this.cs.position - start)); + } + + private skipToSingleEndQuote(quote: number): void { + while (!this.cs.isEndOfStream()) { + if (this.cs.currentChar === Char.LineFeed || this.cs.currentChar === Char.CarriageReturn) { + return; // Unterminated single-line string + } + if (this.cs.currentChar === Char.Backslash && this.cs.nextChar === quote) { + this.cs.advance(2); + continue; + } + if (this.cs.currentChar === quote) { + break; + } + this.cs.moveNext(); + } + this.cs.moveNext(); + } + + private skipToTripleEndQuote(quote: number): void { + while ( + !this.cs.isEndOfStream() && + (this.cs.currentChar !== quote || this.cs.nextChar !== quote || this.cs.lookAhead(2) !== quote) + ) { + this.cs.moveNext(); + } + this.cs.advance(3); + } + + private skipFloatingPointCandidate(allowSign: boolean): boolean { + // Determine end of the potential floating point number + const start = this.cs.position; + this.skipFractionalNumber(allowSign); + if (this.cs.position > start) { + if (this.cs.currentChar === Char.e || this.cs.currentChar === Char.E) { + this.cs.moveNext(); // Optional exponent sign + } + this.skipDecimalNumber(true); // skip exponent value + } + return this.cs.position > start; + } + + private skipFractionalNumber(allowSign: boolean): void { + this.skipDecimalNumber(allowSign); + if (this.cs.currentChar === Char.Period) { + this.cs.moveNext(); // Optional period + } + this.skipDecimalNumber(false); + } + + private skipDecimalNumber(allowSign: boolean): void { + if (allowSign && (this.cs.currentChar === Char.Hyphen || this.cs.currentChar === Char.Plus)) { + this.cs.moveNext(); // Optional sign + } + while (isDecimal(this.cs.currentChar)) { + this.cs.moveNext(); // skip integer part + } + } +} diff --git a/src/client/language/types.ts b/src/client/language/types.ts new file mode 100644 index 000000000000..51618039a3d4 --- /dev/null +++ b/src/client/language/types.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export interface ITextRange { + readonly start: number; + readonly end: number; + readonly length: number; + contains(position: number): boolean; +} + +export class TextRange implements ITextRange { + public static readonly empty = TextRange.fromBounds(0, 0); + + public readonly start: number; + public readonly length: number; + + constructor(start: number, length: number) { + if (start < 0) { + throw new Error('start must be non-negative'); + } + if (length < 0) { + throw new Error('length must be non-negative'); + } + this.start = start; + this.length = length; + } + + public static fromBounds(start: number, end: number) { + return new TextRange(start, end - start); + } + + public get end(): number { + return this.start + this.length; + } + + public contains(position: number): boolean { + return position >= this.start && position < this.end; + } +} + +export interface ITextRangeCollection extends ITextRange { + count: number; + getItemAt(index: number): T; + getItemAtPosition(position: number): number; + getItemContaining(position: number): number; +} + +export interface ITextIterator { + readonly length: number; + charCodeAt(index: number): number; + getText(): string; +} + +export interface ICharacterStream extends ITextIterator { + position: number; + readonly currentChar: number; + readonly nextChar: number; + readonly prevChar: number; + getText(): string; + isEndOfStream(): boolean; + lookAhead(offset: number): number; + advance(offset: number): void; + moveNext(): boolean; + isAtWhiteSpace(): boolean; + isAtLineBreak(): boolean; + isAtString(): boolean; + skipLineBreak(): void; + skipWhitespace(): void; + skipToEol(): void; + skipToWhitespace(): void; +} + +export enum TokenType { + Unknown, + String, + Comment, + Keyword, + Number, + Identifier, + Operator, + Colon, + Semicolon, + Comma, + OpenBrace, + CloseBrace, + OpenBracket, + CloseBracket, + OpenCurly, + CloseCurly +} + +export interface IToken extends ITextRange { + readonly type: TokenType; +} + +export enum TokenizerMode { + CommentsAndStrings, + Full +} + +export interface ITokenizer { + tokenize(text: string): ITextRangeCollection; + tokenize(text: string, start: number, length: number, mode: TokenizerMode): ITextRangeCollection; +} diff --git a/src/client/language/unicode.ts b/src/client/language/unicode.ts new file mode 100644 index 000000000000..9b3ca0b15b25 --- /dev/null +++ b/src/client/language/unicode.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// tslint:disable:no-require-imports no-var-requires + +export enum UnicodeCategory { + Unknown, + UppercaseLetter, + LowercaseLetter, + TitlecaseLetter, + ModifierLetter, + OtherLetter, + LetterNumber, + NonSpacingMark, + SpacingCombiningMark, + DecimalDigitNumber, + ConnectorPunctuation +} + +export function getUnicodeCategory(ch: number): UnicodeCategory { + const unicodeLu = require('unicode/category/Lu'); + const unicodeLl = require('unicode/category/Ll'); + const unicodeLt = require('unicode/category/Lt'); + const unicodeLo = require('unicode/category/Lo'); + const unicodeLm = require('unicode/category/Lm'); + const unicodeNl = require('unicode/category/Nl'); + const unicodeMn = require('unicode/category/Mn'); + const unicodeMc = require('unicode/category/Mc'); + const unicodeNd = require('unicode/category/Nd'); + const unicodePc = require('unicode/category/Pc'); + + if (unicodeLu[ch]) { + return UnicodeCategory.UppercaseLetter; + } + if (unicodeLl[ch]) { + return UnicodeCategory.LowercaseLetter; + } + if (unicodeLt[ch]) { + return UnicodeCategory.TitlecaseLetter; + } + if (unicodeLo[ch]) { + return UnicodeCategory.OtherLetter; + } + if (unicodeLm[ch]) { + return UnicodeCategory.ModifierLetter; + } + if (unicodeNl[ch]) { + return UnicodeCategory.LetterNumber; + } + if (unicodeMn[ch]) { + return UnicodeCategory.NonSpacingMark; + } + if (unicodeMc[ch]) { + return UnicodeCategory.SpacingCombiningMark; + } + if (unicodeNd[ch]) { + return UnicodeCategory.DecimalDigitNumber; + } + if (unicodePc[ch]) { + return UnicodeCategory.ConnectorPunctuation; + } + return UnicodeCategory.Unknown; +} diff --git a/src/client/languageClient.ts b/src/client/languageClient.ts deleted file mode 100644 index bd10ce25e7fd..000000000000 --- a/src/client/languageClient.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; -import { workspace, Disposable, ExtensionContext } from 'vscode'; -import { LanguageClient, LanguageClientOptions, SettingMonitor, ServerOptions, TransportKind } from 'vscode-languageclient'; -import { RequestType } from 'vscode-languageclient'; - -var pythonLanguageClient: LanguageClient; -export function activate(context: ExtensionContext) { - - // The server is implemented in node - let serverModule = context.asAbsolutePath(path.join('out', 'server', 'server.js')); - // The debug options for the server - let debugOptions = { execArgv: ["--nolazy", "--debug=6004"] }; - - // If the extension is launch in debug mode the debug server options are use - // Otherwise the run options are used - let serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc }, - debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } - } - - // Options to control the language client - let clientOptions: LanguageClientOptions = { - // Register the server for plain text documents - documentSelector: ['python'], - synchronize: { - // Synchronize the setting section 'python' to the server - configurationSection: 'python', - // Notify the server about file changes to '.clientrc files contain in the workspace - fileEvents: workspace.createFileSystemWatcher('**/.clientrc') - } - } - - // Create the language client and start the client. - pythonLanguageClient = new LanguageClient('Python', serverOptions, clientOptions); - var disposable = pythonLanguageClient.start(); - - //Send info as soon as it starts - var config = workspace.getConfiguration(); - pythonLanguageClient.notifyConfigurationChanged(config) - - var isWin = /^win/.test(process.platform); - if (isWin) { - // workspace.onDidSaveTextDocument(onDidSaveTextDocument, this, context.subscriptions); - } - // Push the disposable to the context's subscriptions so that the - // client can be deactivated on extension deactivation - context.subscriptions.push(disposable); -} -// -// function onDidSaveTextDocument(textDocument: vscode.TextDocument) { -// if (textDocument.languageId !== 'python') { -// return; -// } -// var args: RequestParams = { processId: 0, uri: textDocument.uri }; -// pythonLanguageClient.sendRequest(Request.type, args) -// } -// -// function _registerEvents(): void { -// // subscribe to trigger when the file is saved -// let subscriptions: Disposable[] = []; -// workspace.onDidSaveTextDocument(this._onSave, this, subscriptions); -// } -// // -// namespace Request { -// export const type: RequestType = { get method() { return 'request'; } }; -// } -// /** -// * The Request parameters -// */ -// export interface RequestParams { -// /** -// * The process Id of the parent process that started -// * the server. -// */ -// processId: number; -// -// /** -// * The uri. Is null -// * if no folder is open. -// */ -// uri: vscode.Uri; -// } -// -// /** -// * The result returned from an initilize request. -// */ -// export interface RequestResult { -// succesful: boolean; -// } -// -// -// /** -// * The error returned if the initilize request fails. -// */ -// export interface RequestError { -// /** -// * Indicates whether the client should retry to send the -// * initilize request after showing the message provided -// * in the {@link ResponseError} -// */ -// retry: boolean; -// } \ No newline at end of file diff --git a/src/client/languageServices/jediProxyFactory.ts b/src/client/languageServices/jediProxyFactory.ts new file mode 100644 index 000000000000..9a491ecf3889 --- /dev/null +++ b/src/client/languageServices/jediProxyFactory.ts @@ -0,0 +1,50 @@ +import { Disposable, Uri, workspace } from 'vscode'; + +import { IServiceContainer } from '../ioc/types'; +import { ICommandResult, JediProxy, JediProxyHandler } from '../providers/jediProxy'; +import { PythonEnvironment } from '../pythonEnvironments/info'; + +export class JediFactory implements Disposable { + private disposables: Disposable[]; + private jediProxyHandlers: Map>; + + constructor( + private interpreter: PythonEnvironment | undefined, + // This is passed through to JediProxy(). + private serviceContainer: IServiceContainer + ) { + this.disposables = []; + this.jediProxyHandlers = new Map>(); + } + + public dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + this.disposables = []; + } + + public getJediProxyHandler(resource?: Uri): JediProxyHandler { + const workspacePath = this.getWorkspacePath(resource); + if (!this.jediProxyHandlers.has(workspacePath)) { + const jediProxy = new JediProxy(workspacePath, this.interpreter, this.serviceContainer); + const jediProxyHandler = new JediProxyHandler(jediProxy); + this.disposables.push(jediProxy, jediProxyHandler); + this.jediProxyHandlers.set(workspacePath, jediProxyHandler); + } + return this.jediProxyHandlers.get(workspacePath)! as JediProxyHandler; + } + + private getWorkspacePath(resource?: Uri): string { + if (resource) { + const workspaceFolder = workspace.getWorkspaceFolder(resource); + if (workspaceFolder) { + return workspaceFolder.uri.fsPath; + } + } + + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + return workspace.workspaceFolders[0].uri.fsPath; + } else { + return __dirname; + } + } +} diff --git a/src/client/languageServices/proposeLanguageServerBanner.ts b/src/client/languageServices/proposeLanguageServerBanner.ts new file mode 100644 index 000000000000..59096edc2271 --- /dev/null +++ b/src/client/languageServices/proposeLanguageServerBanner.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { LanguageServerType } from '../activation/types'; +import { IApplicationEnvironment, IApplicationShell } from '../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../common/constants'; +import { TryPylance } from '../common/experiments/groups'; +import '../common/extensions'; +import { + IConfigurationService, + IExperimentService, + IExtensions, + IPersistentStateFactory, + IPythonExtensionBanner +} from '../common/types'; +import { Common, Pylance } from '../common/utils/localize'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +export function getPylanceExtensionUri(appEnv: IApplicationEnvironment): string { + return `${appEnv.uriScheme}:extension/${PYLANCE_EXTENSION_ID}`; +} + +// persistent state names, exported to make use of in testing +export enum ProposeLSStateKeys { + ShowBanner = 'TryPylanceBanner' +} + +/* +This class represents a popup that propose that the user try out a new +feature of the extension, and optionally enable that new feature if they +choose to do so. It is meant to be shown only to a subset of our users, +and will show as soon as it is instructed to do so, if a random sample +function enables the popup for this user. +*/ +@injectable() +export class ProposePylanceBanner implements IPythonExtensionBanner { + private disabledInCurrentSession: boolean = false; + + constructor( + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IApplicationEnvironment) private appEnv: IApplicationEnvironment, + @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, + @inject(IConfigurationService) private configuration: IConfigurationService, + @inject(IExperimentService) private experiments: IExperimentService, + @inject(IExtensions) readonly extensions: IExtensions + ) {} + + public get enabled(): boolean { + const lsType = this.configuration.getSettings().languageServer ?? LanguageServerType.Jedi; + if (lsType === LanguageServerType.Jedi || lsType === LanguageServerType.Node) { + return false; + } + return this.persistentState.createGlobalPersistentState(ProposeLSStateKeys.ShowBanner, true).value; + } + + public async showBanner(): Promise { + if (!this.enabled) { + return; + } + + const show = await this.shouldShowBanner(); + if (!show) { + return; + } + + const response = await this.appShell.showInformationMessage( + Pylance.proposePylanceMessage(), + Pylance.tryItNow(), + Common.bannerLabelNo(), + Pylance.remindMeLater() + ); + + let userAction: string; + if (response === Pylance.tryItNow()) { + this.appShell.openUrl(getPylanceExtensionUri(this.appEnv)); + userAction = 'yes'; + await this.disable(); + } else if (response === Common.bannerLabelNo()) { + await this.disable(); + userAction = 'no'; + } else { + this.disabledInCurrentSession = true; + userAction = 'later'; + } + sendTelemetryEvent(EventName.LANGUAGE_SERVER_TRY_PYLANCE, undefined, { userAction }); + } + + public async shouldShowBanner(): Promise { + // Do not prompt if Pylance is already installed. + if (this.extensions.getExtension(PYLANCE_EXTENSION_ID)) { + return false; + } + // Only prompt for users in experiment. + const inExperiment = await this.experiments.inExperiment(TryPylance.experiment); + return inExperiment && this.enabled && !this.disabledInCurrentSession; + } + + public async disable(): Promise { + await this.persistentState + .createGlobalPersistentState(ProposeLSStateKeys.ShowBanner, false) + .updateValue(false); + } +} diff --git a/src/client/linters/bandit.ts b/src/client/linters/bandit.ts new file mode 100644 index 000000000000..137d95d48275 --- /dev/null +++ b/src/client/linters/bandit.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { BaseLinter } from './baseLinter'; +import { ILintMessage, LintMessageSeverity } from './types'; + +const severityMapping: Record = { + LOW: LintMessageSeverity.Information, + MEDIUM: LintMessageSeverity.Warning, + HIGH: LintMessageSeverity.Error +}; + +export class Bandit extends BaseLinter { + constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + super(Product.bandit, outputChannel, serviceContainer); + } + + protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { + // View all errors in bandit <= 1.5.1 (https://github.com/PyCQA/bandit/issues/371) + const messages = await this.run( + ['-f', 'custom', '--msg-template', '{line},0,{severity},{test_id}:{msg}', '-n', '-1', document.uri.fsPath], + document, + cancellation + ); + + messages.forEach((msg) => { + msg.severity = severityMapping[msg.type]; + }); + return messages; + } +} diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts index 69185b922409..3799e31660b3 100644 --- a/src/client/linters/baseLinter.ts +++ b/src/client/linters/baseLinter.ts @@ -1,119 +1,210 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 'use strict'; -import * as child_process from 'child_process'; + import * as path from 'path'; -import { exec } from 'child_process'; -import {sendCommand} from './../common/childProc'; -import * as settings from './../common/configSettings'; -import {OutputChannel, window} from 'vscode'; +import * as vscode from 'vscode'; +import { IWorkspaceService } from '../common/application/types'; +import { isTestExecution } from '../common/constants'; +import '../common/extensions'; +import { traceError } from '../common/logger'; +import { IPythonToolExecutionService } from '../common/process/types'; +import { ExecutionInfo, IConfigurationService, IPythonSettings, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { ErrorHandler } from './errorHandlers/errorHandler'; +import { ILinter, ILinterInfo, ILinterManager, ILintMessage, LinterId, LintMessageSeverity } from './types'; -var NamedRegexp = null; -const REGEX = '(?\\d+),(?\\d+),(?\\w+),(?\\w\\d+):(?.*)\\r?(\\n|$)'; +// tslint:disable-next-line:no-require-imports no-var-requires no-any +const namedRegexp = require('named-js-regexp'); +// Allow negative column numbers (https://github.com/PyCQA/pylint/issues/1822) +// Allow codes with more than one letter (i.e. ABC123) +const REGEX = '(?\\d+),(?-?\\d+),(?\\w+),(?\\w+\\d+):(?.*)\\r?(\\n|$)'; export interface IRegexGroup { - line: number - column: number - code: string - message: string - type: string + line: number; + column: number; + code: string; + message: string; + type: string; } -export interface ILintMessage { - line: number - column: number - code: string - message: string - type: string - possibleWord?: string - severity?: LintMessageSeverity - provider: string +export function matchNamedRegEx(data: string, regex: string): IRegexGroup | undefined { + const compiledRegexp = namedRegexp(regex, 'g'); + const rawMatch = compiledRegexp.exec(data); + if (rawMatch !== null) { + return rawMatch.groups(); + } + + return undefined; } -export enum LintMessageSeverity { - Hint, - Error, - Warning, - Information + +export function parseLine( + line: string, + regex: string, + linterID: LinterId, + colOffset: number = 0 +): ILintMessage | undefined { + const match = matchNamedRegEx(line, regex)!; + if (!match) { + return; + } + + // tslint:disable-next-line:no-any + match.line = Number(match.line); + // tslint:disable-next-line:no-any + match.column = Number(match.column); + + return { + code: match.code, + message: match.message, + column: isNaN(match.column) || match.column <= 0 ? 0 : match.column - colOffset, + line: match.line, + type: match.type, + provider: linterID + }; } -export function matchNamedRegEx(data, regex): IRegexGroup { - if (NamedRegexp === null) { - NamedRegexp = require('named-js-regexp'); +export abstract class BaseLinter implements ILinter { + protected readonly configService: IConfigurationService; + + private errorHandler: ErrorHandler; + private _pythonSettings!: IPythonSettings; + private _info: ILinterInfo; + private workspace: IWorkspaceService; + + protected get pythonSettings(): IPythonSettings { + return this._pythonSettings; } - var compiledRegexp = NamedRegexp(regex, "g"); - var rawMatch = compiledRegexp.exec(data); - if (rawMatch !== null) { - return rawMatch.groups() + constructor( + product: Product, + protected readonly outputChannel: vscode.OutputChannel, + protected readonly serviceContainer: IServiceContainer, + protected readonly columnOffset = 0 + ) { + this._info = serviceContainer.get(ILinterManager).getLinterInfo(product); + this.errorHandler = new ErrorHandler(this.info.product, outputChannel, serviceContainer); + this.configService = serviceContainer.get(IConfigurationService); + this.workspace = serviceContainer.get(IWorkspaceService); } - return null; -} + public get info(): ILinterInfo { + return this._info; + } -export abstract class BaseLinter { - public Id: string; - protected pythonSettings: settings.IPythonSettings; - protected outputChannel: OutputChannel; - constructor(id: string, pythonSettings: settings.IPythonSettings, outputChannel: OutputChannel) { - this.Id = id; - this.pythonSettings = pythonSettings; - this.outputChannel = outputChannel; + public async lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise { + this._pythonSettings = this.configService.getSettings(document.uri); + return this.runLinter(document, cancellation); } - public runLinter(filePath: string, txtDocumentLines: string[]): Promise { - return Promise.resolve([]); + protected getWorkspaceRootPath(document: vscode.TextDocument): string { + const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); + const workspaceRootPath = + workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string' ? workspaceFolder.uri.fsPath : undefined; + return typeof workspaceRootPath === 'string' ? workspaceRootPath : path.dirname(document.uri.fsPath); } + protected abstract runLinter( + document: vscode.TextDocument, + cancellation: vscode.CancellationToken + ): Promise; - protected run(commandLine: string, filePath: string, txtDocumentLines: string[], cwd: string, regEx: string = REGEX): Promise { - var outputChannel = this.outputChannel; - var linterId = this.Id; - - return new Promise((resolve, reject) => { - sendCommand(commandLine, cwd, true).then(data => { - outputChannel.clear(); - outputChannel.append(data); - var outputLines = data.split(/\r?\n/g); - var diagnostics: ILintMessage[] = []; - outputLines.filter((value, index) => index <= this.pythonSettings.linting.maxNumberOfProblems).forEach(line => { - var match = matchNamedRegEx(line, regEx); - if (match == null) { - return; + // tslint:disable-next-line:no-any + protected parseMessagesSeverity(error: string, categorySeverity: any): LintMessageSeverity { + if (categorySeverity[error]) { + const severityName = categorySeverity[error]; + switch (severityName) { + case 'Error': + return LintMessageSeverity.Error; + case 'Hint': + return LintMessageSeverity.Hint; + case 'Information': + return LintMessageSeverity.Information; + case 'Warning': + return LintMessageSeverity.Warning; + default: { + if (LintMessageSeverity[severityName]) { + // tslint:disable-next-line:no-any + return (LintMessageSeverity[severityName]); } + } + } + } + return LintMessageSeverity.Information; + } - try { - match.line = Number(match.line); - match.column = Number(match.column); - - var sourceLine = txtDocumentLines[match.line - 1]; - var sourceStart = sourceLine.substring(match.column - 1); - var endCol = txtDocumentLines[match.line - 1].length; - - //try to get the first word from the startig position - var possibleProblemWords = sourceStart.match(/\w+/g); - var possibleWord: string; - if (possibleProblemWords != null && possibleProblemWords.length > 0 && sourceStart.startsWith(possibleProblemWords[0])) { - possibleWord = possibleProblemWords[0]; - } - - diagnostics.push({ - code: match.code, - message: match.message, - column: match.column, - line: match.line, - possibleWord: possibleWord, - type: match.type, - provider: this.Id - }); - } - catch (ex) { - //Hmm, need to handle this later - var y = ""; + protected async run( + args: string[], + document: vscode.TextDocument, + cancellation: vscode.CancellationToken, + regEx: string = REGEX + ): Promise { + if (!this.info.isEnabled(document.uri)) { + return []; + } + const executionInfo = this.info.getExecutionInfo(args, document.uri); + const cwd = this.getWorkspaceRootPath(document); + const pythonToolsExecutionService = this.serviceContainer.get( + IPythonToolExecutionService + ); + try { + const result = await pythonToolsExecutionService.exec( + executionInfo, + { cwd, token: cancellation, mergeStdOutErr: false }, + document.uri + ); + this.displayLinterResultHeader(result.stdout); + return await this.parseMessages(result.stdout, document, cancellation, regEx); + } catch (error) { + await this.handleError(error, document.uri, executionInfo); + return []; + } + } + + protected async parseMessages( + output: string, + _document: vscode.TextDocument, + _token: vscode.CancellationToken, + regEx: string + ) { + const outputLines = output.splitLines({ removeEmptyEntries: false, trim: false }); + return this.parseLines(outputLines, regEx); + } + + protected async handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo) { + if (isTestExecution()) { + this.errorHandler.handleError(error, resource, execInfo).ignoreErrors(); + } else { + this.errorHandler + .handleError(error, resource, execInfo) + .catch((ex) => traceError('Error in errorHandler.handleError', ex)) + .ignoreErrors(); + } + } + + private parseLine(line: string, regEx: string): ILintMessage | undefined { + return parseLine(line, regEx, this.info.id, this.columnOffset); + } + + private parseLines(outputLines: string[], regEx: string): ILintMessage[] { + const messages: ILintMessage[] = []; + for (const line of outputLines) { + try { + const msg = this.parseLine(line, regEx); + if (msg) { + messages.push(msg); + if (messages.length >= this.pythonSettings.linting.maxNumberOfProblems) { + break; } - }); - - resolve(diagnostics); - }, error => { - outputChannel.appendLine(`Linting with ${linterId} failed. If not installed please turn if off in settings.\n ${error}`); - window.showInformationMessage(`Linting with ${linterId} failed. If not installed please turn if off in settings. View Python output for details.`); - }); - }); + } + } catch (ex) { + traceError(`Linter '${this.info.id}' failed to parse the line '${line}.`, ex); + } + } + return messages; + } + + private displayLinterResultHeader(data: string) { + this.outputChannel.append(`${'#'.repeat(10)}Linting Output - ${this.info.id}${'#'.repeat(10)}\n`); + this.outputChannel.append(data); } } diff --git a/src/client/linters/constants.ts b/src/client/linters/constants.ts new file mode 100644 index 000000000000..3f1bfa3f5dcc --- /dev/null +++ b/src/client/linters/constants.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Product } from '../common/types'; +import { LinterId } from './types'; + +// All supported linters must be in this map. +export const LINTERID_BY_PRODUCT = new Map([ + [Product.bandit, LinterId.Bandit], + [Product.flake8, LinterId.Flake8], + [Product.pylint, LinterId.PyLint], + [Product.mypy, LinterId.MyPy], + [Product.pycodestyle, LinterId.PyCodeStyle], + [Product.prospector, LinterId.Prospector], + [Product.pydocstyle, LinterId.PyDocStyle], + [Product.pylama, LinterId.PyLama] +]); diff --git a/src/client/linters/errorHandlers/baseErrorHandler.ts b/src/client/linters/errorHandlers/baseErrorHandler.ts new file mode 100644 index 000000000000..7ef13f4e615c --- /dev/null +++ b/src/client/linters/errorHandlers/baseErrorHandler.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { OutputChannel, Uri } from 'vscode'; +import { ExecutionInfo, IInstaller, Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IErrorHandler } from '../types'; + +export abstract class BaseErrorHandler implements IErrorHandler { + protected installer: IInstaller; + + private handler?: IErrorHandler; + + constructor( + protected product: Product, + protected outputChannel: OutputChannel, + protected serviceContainer: IServiceContainer + ) { + this.installer = this.serviceContainer.get(IInstaller); + } + protected get nextHandler(): IErrorHandler | undefined { + return this.handler; + } + public setNextHandler(handler: IErrorHandler): void { + this.handler = handler; + } + public abstract handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise; +} diff --git a/src/client/linters/errorHandlers/errorHandler.ts b/src/client/linters/errorHandlers/errorHandler.ts new file mode 100644 index 000000000000..ea4009ddd85d --- /dev/null +++ b/src/client/linters/errorHandlers/errorHandler.ts @@ -0,0 +1,21 @@ +import { OutputChannel, Uri } from 'vscode'; +import { ExecutionInfo, Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IErrorHandler } from '../types'; +import { BaseErrorHandler } from './baseErrorHandler'; +import { NotInstalledErrorHandler } from './notInstalled'; +import { StandardErrorHandler } from './standard'; + +export class ErrorHandler implements IErrorHandler { + private handler: BaseErrorHandler; + constructor(product: Product, outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + // Create chain of handlers. + const standardErrorHandler = new StandardErrorHandler(product, outputChannel, serviceContainer); + this.handler = new NotInstalledErrorHandler(product, outputChannel, serviceContainer); + this.handler.setNextHandler(standardErrorHandler); + } + + public handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { + return this.handler.handleError(error, resource, execInfo); + } +} diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts new file mode 100644 index 000000000000..16871e7ee71f --- /dev/null +++ b/src/client/linters/errorHandlers/notInstalled.ts @@ -0,0 +1,33 @@ +import { OutputChannel, Uri } from 'vscode'; +import { traceError, traceWarning } from '../../common/logger'; +import { IPythonExecutionFactory } from '../../common/process/types'; +import { ExecutionInfo, Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { ILinterManager } from '../types'; +import { BaseErrorHandler } from './baseErrorHandler'; + +export class NotInstalledErrorHandler extends BaseErrorHandler { + constructor(product: Product, outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + super(product, outputChannel, serviceContainer); + } + public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { + const pythonExecutionService = await this.serviceContainer + .get(IPythonExecutionFactory) + .create({ resource }); + const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); + if (isModuleInstalled) { + return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : false; + } + + this.installer + .promptToInstall(this.product, resource) + .catch((ex) => traceError('NotInstalledErrorHandler.promptToInstall', ex)); + + const linterManager = this.serviceContainer.get(ILinterManager); + const info = linterManager.getLinterInfo(execInfo.product!); + const customError = `Linter '${info.id}' is not installed. Please install it or select another linter".`; + this.outputChannel.appendLine(`\n${customError}\n${error}`); + traceWarning(customError, error); + return true; + } +} diff --git a/src/client/linters/errorHandlers/standard.ts b/src/client/linters/errorHandlers/standard.ts new file mode 100644 index 000000000000..088206269c8f --- /dev/null +++ b/src/client/linters/errorHandlers/standard.ts @@ -0,0 +1,37 @@ +import { OutputChannel, Uri } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import { ExecutionInfo, Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { ILinterManager, LinterId } from '../types'; +import { BaseErrorHandler } from './baseErrorHandler'; + +export class StandardErrorHandler extends BaseErrorHandler { + constructor(product: Product, outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + super(product, outputChannel, serviceContainer); + } + public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { + if ( + typeof error === 'string' && + (error as string).indexOf("OSError: [Errno 2] No such file or directory: '/") > 0 + ) { + return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : Promise.resolve(false); + } + + const linterManager = this.serviceContainer.get(ILinterManager); + const info = linterManager.getLinterInfo(execInfo.product!); + + traceError(`There was an error in running the linter ${info.id}`, error); + this.outputChannel.appendLine(`Linting with ${info.id} failed.`); + this.outputChannel.appendLine(error.toString()); + + this.displayLinterError(info.id).ignoreErrors(); + return true; + } + private async displayLinterError(linterId: LinterId) { + const message = `There was an error in running the linter '${linterId}'`; + const appShell = this.serviceContainer.get(IApplicationShell); + await appShell.showErrorMessage(message, 'View Errors'); + this.outputChannel.show(); + } +} diff --git a/src/client/linters/flake8.ts b/src/client/linters/flake8.ts index 31055d0d405b..5d2cd0d4e6c1 100644 --- a/src/client/linters/flake8.ts +++ b/src/client/linters/flake8.ts @@ -1,33 +1,31 @@ -'use strict'; +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { BaseLinter } from './baseLinter'; +import { ILintMessage } from './types'; -import * as path from 'path'; -import * as baseLinter from './baseLinter'; -import * as settings from './../common/configSettings'; -import {OutputChannel, workspace} from 'vscode'; +const COLUMN_OFF_SET = 1; -const FLAKE8_COMMANDLINE = " --format='%(row)d,%(col)d,%(code)s,%(code)s:%(text)s'"; - -export class Linter extends baseLinter.BaseLinter { - constructor(rootDir: string, pythonSettings: settings.IPythonSettings, outputChannel: OutputChannel) { - super("flake8", pythonSettings, outputChannel); +export class Flake8 extends BaseLinter { + constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + super(Product.flake8, outputChannel, serviceContainer, COLUMN_OFF_SET); } - public runLinter(filePath: string, txtDocumentLines: string[]): Promise { - if (!this.pythonSettings.linting.flake8Enabled) { - return Promise.resolve([]); - } - - var flake8Path = this.pythonSettings.linting.flake8Path; - var cmdLine = `${flake8Path} ${FLAKE8_COMMANDLINE} "${filePath}"`; - return new Promise((resolve, reject) => { - this.run(cmdLine, filePath, txtDocumentLines, workspace.rootPath).then(messages=> { - //All messages in pep8 are treated as warnings for now - messages.forEach(msg=> { - msg.severity = baseLinter.LintMessageSeverity.Information; - }); - - resolve(messages); - }, reject); + protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { + const messages = await this.run( + ['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', document.uri.fsPath], + document, + cancellation + ); + messages.forEach((msg) => { + msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.flake8CategorySeverity); + // flake8 uses 0th line for some file-wide problems + // but diagnostics expects positive line numbers. + if (msg.line === 0) { + msg.line = 1; + } }); + return messages; } } diff --git a/src/client/linters/linterAvailability.ts b/src/client/linters/linterAvailability.ts new file mode 100644 index 000000000000..834a86284610 --- /dev/null +++ b/src/client/linters/linterAvailability.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { LanguageServerType } from '../activation/types'; +import { IApplicationShell, IWorkspaceService } from '../common/application/types'; +import '../common/extensions'; +import { IFileSystem } from '../common/platform/types'; +import { IConfigurationService, IPersistentStateFactory, Resource } from '../common/types'; +import { Common, Linters } from '../common/utils/localize'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { IAvailableLinterActivator, ILinterInfo } from './types'; + +const doNotDisplayPromptStateKey = 'MESSAGE_KEY_FOR_CONFIGURE_AVAILABLE_LINTER_PROMPT'; +@injectable() +export class AvailableLinterActivator implements IAvailableLinterActivator { + constructor( + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IFileSystem) private fs: IFileSystem, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory + ) {} + + /** + * Check if it is possible to enable an otherwise-unconfigured linter in + * the current workspace, and if so ask the user if they want that linter + * configured explicitly. + * + * @param linterInfo The linter to check installation status. + * @param resource Context for the operation (required when in multi-root workspaces). + * + * @returns true if configuration was updated in any way, false otherwise. + */ + public async promptIfLinterAvailable(linterInfo: ILinterInfo, resource?: Uri): Promise { + // Has the feature been enabled yet? + if (!this.isFeatureEnabled) { + return false; + } + + // Has the linter in question has been configured explicitly? If so, no need to continue. + if (!this.isLinterUsingDefaultConfiguration(linterInfo, resource)) { + return false; + } + + // Is the linter available in the current workspace? + if (await this.isLinterAvailable(linterInfo, resource)) { + // great, it is - ask the user if they'd like to enable it. + return this.promptToConfigureAvailableLinter(linterInfo); + } + return false; + } + + /** + * Raise a dialog asking the user if they would like to explicitly configure a + * linter or not in their current workspace. + * + * @param linterInfo The linter to ask the user to enable or not. + * + * @returns true if the user requested a configuration change, false otherwise. + */ + public async promptToConfigureAvailableLinter(linterInfo: ILinterInfo): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createWorkspacePersistentState( + doNotDisplayPromptStateKey, + true + ); + if (!notificationPromptEnabled.value) { + return false; + } + const optButtons = [Linters.enableLinter().format(linterInfo.id), Common.notNow(), Common.doNotShowAgain()]; + + const telemetrySelections: ['enable', 'ignore', 'disablePrompt'] = ['enable', 'ignore', 'disablePrompt']; + const pick = await this.appShell.showInformationMessage( + Linters.enablePylint().format(linterInfo.id), + ...optButtons + ); + sendTelemetryEvent(EventName.CONFIGURE_AVAILABLE_LINTER_PROMPT, undefined, { + tool: linterInfo.id, + action: pick ? telemetrySelections[optButtons.indexOf(pick)] : undefined + }); + if (pick === optButtons[0]) { + await linterInfo.enableAsync(true); + return true; + } else if (pick === optButtons[2]) { + await notificationPromptEnabled.updateValue(false); + } + return false; + } + + /** + * Check if the linter itself is available in the workspace's Python environment or + * not. + * + * @param linterInfo Linter to check in the current workspace environment. + * @param resource Context information for workspace. + */ + public async isLinterAvailable(linterInfo: ILinterInfo, resource: Resource): Promise { + if (!this.workspaceService.hasWorkspaceFolders) { + return false; + } + const workspaceFolder = + this.workspaceService.getWorkspaceFolder(resource) || this.workspaceService.workspaceFolders![0]; + let isAvailable = false; + for (const configName of linterInfo.configFileNames) { + const configPath = path.join(workspaceFolder.uri.fsPath, configName); + isAvailable = isAvailable || (await this.fs.fileExists(configPath)); + } + return isAvailable; + } + + /** + * Check if the given linter has been configured by the user in this workspace or not. + * + * @param linterInfo Linter to check for configuration status. + * @param resource Context information. + * + * @returns true if the linter has not been configured at the user, workspace, or workspace-folder scope. false otherwise. + */ + public isLinterUsingDefaultConfiguration(linterInfo: ILinterInfo, resource?: Uri): boolean { + const ws = this.workspaceService.getConfiguration('python.linting', resource); + const pe = ws!.inspect(linterInfo.enabledSettingName); + return ( + pe!.globalValue === undefined && pe!.workspaceValue === undefined && pe!.workspaceFolderValue === undefined + ); + } + + /** + * Check if this feature is enabled yet. + * + * This is a feature of the vscode-python extension that will become enabled once the + * Python Language Server becomes the default, replacing Jedi as the default. Testing + * the global default setting for `"python.languageServer": !Jedi` enables it. + * + * @returns true if the global default for python.languageServer is not Jedi. + */ + public get isFeatureEnabled(): boolean { + return this.configService.getSettings().languageServer !== LanguageServerType.Jedi; + } +} diff --git a/src/client/linters/linterCommands.ts b/src/client/linters/linterCommands.ts new file mode 100644 index 000000000000..3f7f11dcc206 --- /dev/null +++ b/src/client/linters/linterCommands.ts @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { DiagnosticCollection, Disposable, QuickPickOptions, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; +import { Commands } from '../common/constants'; +import { IDisposable } from '../common/types'; +import { Linters } from '../common/utils/localize'; +import { IServiceContainer } from '../ioc/types'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { ILinterManager, ILintingEngine, LinterId } from './types'; + +export class LinterCommands implements IDisposable { + private disposables: Disposable[] = []; + private linterManager: ILinterManager; + private readonly appShell: IApplicationShell; + private readonly documentManager: IDocumentManager; + + constructor(private serviceContainer: IServiceContainer) { + this.linterManager = this.serviceContainer.get(ILinterManager); + this.appShell = this.serviceContainer.get(IApplicationShell); + this.documentManager = this.serviceContainer.get(IDocumentManager); + + const commandManager = this.serviceContainer.get(ICommandManager); + commandManager.registerCommand(Commands.Set_Linter, this.setLinterAsync.bind(this)); + commandManager.registerCommand(Commands.Enable_Linter, this.enableLintingAsync.bind(this)); + commandManager.registerCommand(Commands.Run_Linter, this.runLinting.bind(this)); + } + public dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + + public async setLinterAsync(): Promise { + const linters = this.linterManager.getAllLinterInfos(); + const suggestions = linters.map((x) => x.id).sort(); + const linterList = ['Disable Linting', ...suggestions]; + const activeLinters = await this.linterManager.getActiveLinters(true, this.settingsUri); + + let current: string; + switch (activeLinters.length) { + case 0: + current = 'none'; + break; + case 1: + current = activeLinters[0].id; + break; + default: + current = 'multiple selected'; + break; + } + + const quickPickOptions: QuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${current}` + }; + + const selection = await this.appShell.showQuickPick(linterList, quickPickOptions); + if (selection !== undefined) { + if (selection === 'Disable Linting') { + await this.linterManager.enableLintingAsync(false); + sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { enabled: false }); + } else { + const index = linters.findIndex((x) => x.id === selection); + if (activeLinters.length > 1) { + const response = await this.appShell.showWarningMessage( + Linters.replaceWithSelectedLinter().format(selection), + 'Yes', + 'No' + ); + if (response !== 'Yes') { + return; + } + } + await this.linterManager.setActiveLintersAsync([linters[index].product], this.settingsUri); + sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { tool: selection as LinterId, enabled: true }); + } + } + } + + public async enableLintingAsync(): Promise { + const options = ['on', 'off']; + const current = (await this.linterManager.isLintingEnabled(true, this.settingsUri)) ? options[0] : options[1]; + + const quickPickOptions: QuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${current}` + }; + + const selection = await this.appShell.showQuickPick(options, quickPickOptions); + if (selection !== undefined) { + const enable = selection === options[0]; + await this.linterManager.enableLintingAsync(enable, this.settingsUri); + } + } + + public runLinting(): Promise { + const engine = this.serviceContainer.get(ILintingEngine); + return engine.lintOpenPythonFiles(); + } + + private get settingsUri(): Uri | undefined { + return this.documentManager.activeTextEditor ? this.documentManager.activeTextEditor.document.uri : undefined; + } +} diff --git a/src/client/linters/linterInfo.ts b/src/client/linters/linterInfo.ts new file mode 100644 index 000000000000..307afe4c0cc2 --- /dev/null +++ b/src/client/linters/linterInfo.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Uri } from 'vscode'; +import { LanguageServerType } from '../activation/types'; +import { IWorkspaceService } from '../common/application/types'; +import { ExecutionInfo, IConfigurationService, Product } from '../common/types'; +import { ILinterInfo, LinterId } from './types'; + +// tslint:disable:no-any + +export class LinterInfo implements ILinterInfo { + private _id: LinterId; + private _product: Product; + private _configFileNames: string[]; + + constructor( + product: Product, + id: LinterId, + protected configService: IConfigurationService, + configFileNames: string[] = [] + ) { + this._product = product; + this._id = id; + this._configFileNames = configFileNames; + } + + public get id(): LinterId { + return this._id; + } + public get product(): Product { + return this._product; + } + + public get pathSettingName(): string { + return `${this.id}Path`; + } + public get argsSettingName(): string { + return `${this.id}Args`; + } + public get enabledSettingName(): string { + return `${this.id}Enabled`; + } + public get configFileNames(): string[] { + return this._configFileNames; + } + + public async enableAsync(enabled: boolean, resource?: Uri): Promise { + return this.configService.updateSetting(`linting.${this.enabledSettingName}`, enabled, resource); + } + public isEnabled(resource?: Uri): boolean { + const settings = this.configService.getSettings(resource); + return (settings.linting as any)[this.enabledSettingName] as boolean; + } + + public pathName(resource?: Uri): string { + const settings = this.configService.getSettings(resource); + return (settings.linting as any)[this.pathSettingName] as string; + } + public linterArgs(resource?: Uri): string[] { + const settings = this.configService.getSettings(resource); + const args = (settings.linting as any)[this.argsSettingName]; + return Array.isArray(args) ? (args as string[]) : []; + } + public getExecutionInfo(customArgs: string[], resource?: Uri): ExecutionInfo { + const execPath = this.pathName(resource); + const args = this.linterArgs(resource).concat(customArgs); + let moduleName: string | undefined; + + // If path information is not available, then treat it as a module, + if (path.basename(execPath) === execPath) { + moduleName = execPath; + } + + return { execPath, moduleName, args, product: this.product }; + } +} + +export class PylintLinterInfo extends LinterInfo { + constructor( + configService: IConfigurationService, + private readonly workspaceService: IWorkspaceService, + configFileNames: string[] = [] + ) { + super(Product.pylint, LinterId.PyLint, configService, configFileNames); + } + public isEnabled(resource?: Uri): boolean { + // We want to be sure the setting is not default since default is `true` and hence + // missing setting yields `true`. When setting is missing and LS is non-Jedi, + // we want default to be `false`. So inspection here makes sure we are not getting + // `true` because there is no setting and LS is active. + const enabled = super.isEnabled(resource); // Is it enabled by settings? + const usingJedi = this.configService.getSettings(resource).languageServer === LanguageServerType.Jedi; + if (usingJedi) { + // In Jedi case adhere to default behavior. Missing setting means `enabled`. + return enabled; + } + // If we're using LS, then by default Pylint is disabled unless user provided + // the value. We have to resort to direct inspection of settings here. + const configuration = this.workspaceService.getConfiguration('python', resource); + const inspection = configuration.inspect(`linting.${this.enabledSettingName}`); + if ( + !inspection || + (inspection.globalValue === undefined && + inspection.workspaceFolderValue === undefined && + inspection.workspaceValue === undefined) + ) { + return false; + } + return enabled; + } +} diff --git a/src/client/linters/linterManager.ts b/src/client/linters/linterManager.ts new file mode 100644 index 000000000000..a9dd8e1c9c70 --- /dev/null +++ b/src/client/linters/linterManager.ts @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationToken, OutputChannel, TextDocument, Uri } from 'vscode'; +import { IWorkspaceService } from '../common/application/types'; +import { traceError } from '../common/logger'; +import { IConfigurationService, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { Bandit } from './bandit'; +import { Flake8 } from './flake8'; +import { LinterInfo, PylintLinterInfo } from './linterInfo'; +import { MyPy } from './mypy'; +import { Prospector } from './prospector'; +import { Pycodestyle } from './pycodestyle'; +import { PyDocStyle } from './pydocstyle'; +import { PyLama } from './pylama'; +import { Pylint } from './pylint'; +import { IAvailableLinterActivator, ILinter, ILinterInfo, ILinterManager, ILintMessage, LinterId } from './types'; + +class DisabledLinter implements ILinter { + constructor(private configService: IConfigurationService) {} + public get info() { + return new LinterInfo(Product.pylint, LinterId.PyLint, this.configService); + } + public async lint(_document: TextDocument, _cancellation: CancellationToken): Promise { + return []; + } +} + +@injectable() +export class LinterManager implements ILinterManager { + protected linters: ILinterInfo[]; + private configService: IConfigurationService; + private checkedForInstalledLinters = new Set(); + + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService + ) { + this.configService = serviceContainer.get(IConfigurationService); + // Note that we use unit tests to ensure all the linters are here. + this.linters = [ + new LinterInfo(Product.bandit, LinterId.Bandit, this.configService), + new LinterInfo(Product.flake8, LinterId.Flake8, this.configService), + new PylintLinterInfo(this.configService, this.workspaceService, ['.pylintrc', 'pylintrc']), + new LinterInfo(Product.mypy, LinterId.MyPy, this.configService), + new LinterInfo(Product.pycodestyle, LinterId.PyCodeStyle, this.configService), + new LinterInfo(Product.prospector, LinterId.Prospector, this.configService), + new LinterInfo(Product.pydocstyle, LinterId.PyDocStyle, this.configService), + new LinterInfo(Product.pylama, LinterId.PyLama, this.configService) + ]; + } + + public getAllLinterInfos(): ILinterInfo[] { + return this.linters; + } + + public getLinterInfo(product: Product): ILinterInfo { + const x = this.linters.findIndex((value, _index, _obj) => value.product === product); + if (x >= 0) { + return this.linters[x]; + } + throw new Error(`Invalid linter '${Product[product]}'`); + } + + public async isLintingEnabled(silent: boolean, resource?: Uri): Promise { + const settings = this.configService.getSettings(resource); + const activeLintersPresent = await this.getActiveLinters(silent, resource); + return settings.linting.enabled && activeLintersPresent.length > 0; + } + + public async enableLintingAsync(enable: boolean, resource?: Uri): Promise { + await this.configService.updateSetting('linting.enabled', enable, resource); + } + + public async getActiveLinters(silent: boolean, resource?: Uri): Promise { + if (!silent) { + await this.enableUnconfiguredLinters(resource); + } + return this.linters.filter((x) => x.isEnabled(resource)); + } + + public async setActiveLintersAsync(products: Product[], resource?: Uri): Promise { + // ensure we only allow valid linters to be set, otherwise leave things alone. + // filter out any invalid products: + const validProducts = products.filter((product) => { + const foundIndex = this.linters.findIndex((validLinter) => validLinter.product === product); + return foundIndex !== -1; + }); + + // if we have valid linter product(s), enable only those + if (validProducts.length > 0) { + const active = await this.getActiveLinters(true, resource); + for (const x of active) { + await x.enableAsync(false, resource); + } + if (products.length > 0) { + const toActivate = this.linters.filter((x) => products.findIndex((p) => x.product === p) >= 0); + for (const x of toActivate) { + await x.enableAsync(true, resource); + } + await this.enableLintingAsync(true, resource); + } + } + } + + public async createLinter( + product: Product, + outputChannel: OutputChannel, + serviceContainer: IServiceContainer, + resource?: Uri + ): Promise { + if (!(await this.isLintingEnabled(true, resource))) { + return new DisabledLinter(this.configService); + } + const error = 'Linter manager: Unknown linter'; + switch (product) { + case Product.bandit: + return new Bandit(outputChannel, serviceContainer); + case Product.flake8: + return new Flake8(outputChannel, serviceContainer); + case Product.pylint: + return new Pylint(outputChannel, serviceContainer); + case Product.mypy: + return new MyPy(outputChannel, serviceContainer); + case Product.prospector: + return new Prospector(outputChannel, serviceContainer); + case Product.pylama: + return new PyLama(outputChannel, serviceContainer); + case Product.pydocstyle: + return new PyDocStyle(outputChannel, serviceContainer); + case Product.pycodestyle: + return new Pycodestyle(outputChannel, serviceContainer); + default: + traceError(error); + break; + } + throw new Error(error); + } + + protected async enableUnconfiguredLinters(resource?: Uri): Promise { + const settings = this.configService.getSettings(resource); + if (!settings.linting.pylintEnabled || !settings.linting.enabled) { + return; + } + // If we've already checked during this session for the same workspace and Python path, then don't bother again. + const workspaceKey = `${this.workspaceService.getWorkspaceFolderIdentifier(resource)}${settings.pythonPath}`; + if (this.checkedForInstalledLinters.has(workspaceKey)) { + return; + } + this.checkedForInstalledLinters.add(workspaceKey); + + // only check & ask the user if they'd like to enable pylint + const pylintInfo = this.linters.find((linter) => linter.id === 'pylint'); + const activator = this.serviceContainer.get(IAvailableLinterActivator); + await activator.promptIfLinterAvailable(pylintInfo!, resource); + } +} diff --git a/src/client/linters/lintingEngine.ts b/src/client/linters/lintingEngine.ts new file mode 100644 index 000000000000..c9bf05ff8e74 --- /dev/null +++ b/src/client/linters/lintingEngine.ts @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Minimatch } from 'minimatch'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import { IFileSystem } from '../common/platform/types'; +import { IConfigurationService, IOutputChannel } from '../common/types'; +import { isNotebookCell } from '../common/utils/misc'; +import { StopWatch } from '../common/utils/stopWatch'; +import { IServiceContainer } from '../ioc/types'; +import { sendTelemetryWhenDone } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { LinterTrigger, LintingTelemetry } from '../telemetry/types'; +import { ILinterInfo, ILinterManager, ILintingEngine, ILintMessage, LintMessageSeverity } from './types'; + +const PYTHON: vscode.DocumentFilter = { language: 'python' }; + +const lintSeverityToVSSeverity = new Map(); +lintSeverityToVSSeverity.set(LintMessageSeverity.Error, vscode.DiagnosticSeverity.Error); +lintSeverityToVSSeverity.set(LintMessageSeverity.Hint, vscode.DiagnosticSeverity.Hint); +lintSeverityToVSSeverity.set(LintMessageSeverity.Information, vscode.DiagnosticSeverity.Information); +lintSeverityToVSSeverity.set(LintMessageSeverity.Warning, vscode.DiagnosticSeverity.Warning); + +@injectable() +export class LintingEngine implements ILintingEngine { + private workspace: IWorkspaceService; + private documents: IDocumentManager; + private configurationService: IConfigurationService; + private linterManager: ILinterManager; + private diagnosticCollection: vscode.DiagnosticCollection; + private pendingLintings = new Map(); + private outputChannel: vscode.OutputChannel; + private fileSystem: IFileSystem; + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.documents = serviceContainer.get(IDocumentManager); + this.workspace = serviceContainer.get(IWorkspaceService); + this.configurationService = serviceContainer.get(IConfigurationService); + this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.linterManager = serviceContainer.get(ILinterManager); + this.fileSystem = serviceContainer.get(IFileSystem); + this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python'); + } + + public get diagnostics(): vscode.DiagnosticCollection { + return this.diagnosticCollection; + } + + public clearDiagnostics(document: vscode.TextDocument): void { + if (this.diagnosticCollection.has(document.uri)) { + this.diagnosticCollection.delete(document.uri); + } + } + + public async lintOpenPythonFiles(): Promise { + this.diagnosticCollection.clear(); + const promises = this.documents.textDocuments.map(async (document) => this.lintDocument(document, 'auto')); + await Promise.all(promises); + return this.diagnosticCollection; + } + + public async lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { + if (isNotebookCell(document)) { + return; + } + this.diagnosticCollection.set(document.uri, []); + + // Check if we need to lint this document + if (!(await this.shouldLintDocument(document))) { + return; + } + + if (this.pendingLintings.has(document.uri.fsPath)) { + this.pendingLintings.get(document.uri.fsPath)!.cancel(); + this.pendingLintings.delete(document.uri.fsPath); + } + + const cancelToken = new vscode.CancellationTokenSource(); + cancelToken.token.onCancellationRequested(() => { + if (this.pendingLintings.has(document.uri.fsPath)) { + this.pendingLintings.delete(document.uri.fsPath); + } + }); + + this.pendingLintings.set(document.uri.fsPath, cancelToken); + + const activeLinters = await this.linterManager.getActiveLinters(false, document.uri); + const promises: Promise[] = activeLinters.map(async (info: ILinterInfo) => { + const stopWatch = new StopWatch(); + const linter = await this.linterManager.createLinter( + info.product, + this.outputChannel, + this.serviceContainer, + document.uri + ); + const promise = linter.lint(document, cancelToken.token); + this.sendLinterRunTelemetry(info, document.uri, promise, stopWatch, trigger); + return promise; + }); + + // linters will resolve asynchronously - keep a track of all + // diagnostics reported as them come in. + let diagnostics: vscode.Diagnostic[] = []; + const settings = this.configurationService.getSettings(document.uri); + + for (const p of promises) { + const msgs = await p; + if (cancelToken.token.isCancellationRequested) { + break; + } + + if (this.isDocumentOpen(document.uri)) { + // Build the message and suffix the message with the name of the linter used. + for (const m of msgs) { + diagnostics.push(this.createDiagnostics(m, document)); + } + // Limit the number of messages to the max value. + diagnostics = diagnostics.filter((_value, index) => index <= settings.linting.maxNumberOfProblems); + } + } + // Set all diagnostics found in this pass, as this method always clears existing diagnostics. + this.diagnosticCollection.set(document.uri, diagnostics); + } + + private sendLinterRunTelemetry( + info: ILinterInfo, + resource: vscode.Uri, + promise: Promise, + stopWatch: StopWatch, + trigger: LinterTrigger + ): void { + const linterExecutablePathName = info.pathName(resource); + const properties: LintingTelemetry = { + tool: info.id, + hasCustomArgs: info.linterArgs(resource).length > 0, + trigger, + executableSpecified: linterExecutablePathName.length > 0 + }; + sendTelemetryWhenDone(EventName.LINTING, promise, stopWatch, properties); + } + + private isDocumentOpen(uri: vscode.Uri): boolean { + return this.documents.textDocuments.some((document) => document.uri.fsPath === uri.fsPath); + } + + private createDiagnostics(message: ILintMessage, _document: vscode.TextDocument): vscode.Diagnostic { + const position = new vscode.Position(message.line - 1, message.column); + const range = new vscode.Range(position, position); + + const severity = lintSeverityToVSSeverity.get(message.severity!)!; + const diagnostic = new vscode.Diagnostic(range, message.message, severity); + diagnostic.code = message.code; + diagnostic.source = message.provider; + return diagnostic; + } + + private async shouldLintDocument(document: vscode.TextDocument): Promise { + if (!(await this.linterManager.isLintingEnabled(false, document.uri))) { + this.diagnosticCollection.set(document.uri, []); + return false; + } + + if (document.languageId !== PYTHON.language) { + return false; + } + + const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); + const workspaceRootPath = + workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string' ? workspaceFolder.uri.fsPath : undefined; + const relativeFileName = + typeof workspaceRootPath === 'string' + ? path.relative(workspaceRootPath, document.fileName) + : document.fileName; + + const settings = this.configurationService.getSettings(document.uri); + // { dot: true } is important so dirs like `.venv` will be matched by globs + const ignoreMinmatches = settings.linting.ignorePatterns.map( + (pattern) => new Minimatch(pattern, { dot: true }) + ); + if (ignoreMinmatches.some((matcher) => matcher.match(document.fileName) || matcher.match(relativeFileName))) { + return false; + } + if (document.uri.scheme !== 'file' || !document.uri.fsPath) { + return false; + } + return this.fileSystem.fileExists(document.uri.fsPath); + } +} diff --git a/src/client/linters/mypy.ts b/src/client/linters/mypy.ts new file mode 100644 index 000000000000..eff5c71be37a --- /dev/null +++ b/src/client/linters/mypy.ts @@ -0,0 +1,23 @@ +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { BaseLinter } from './baseLinter'; +import { ILintMessage } from './types'; + +export const REGEX = '(?[^:]+):(?\\d+)(:(?\\d+))?: (?\\w+): (?.*)\\r?(\\n|$)'; + +export class MyPy extends BaseLinter { + constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + super(Product.mypy, outputChannel, serviceContainer); + } + + protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { + const messages = await this.run([document.uri.fsPath], document, cancellation, REGEX); + messages.forEach((msg) => { + msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.mypyCategorySeverity); + msg.code = msg.type; + }); + return messages; + } +} diff --git a/src/client/linters/pep8Linter.ts b/src/client/linters/pep8Linter.ts deleted file mode 100644 index adc412807a10..000000000000 --- a/src/client/linters/pep8Linter.ts +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -import * as path from 'path'; -import * as baseLinter from './baseLinter'; -import * as settings from './../common/configSettings'; -import {OutputChannel, workspace} from 'vscode'; - -const PEP_COMMANDLINE = " --format='%(row)d,%(col)d,%(code)s,%(code)s:%(text)s'"; - -export class Linter extends baseLinter.BaseLinter { - constructor(rootDir: string, pythonSettings: settings.IPythonSettings, outputChannel:OutputChannel) { - super("pep8", pythonSettings, outputChannel); - } - - public runLinter(filePath: string, txtDocumentLines: string[]): Promise { - if (!this.pythonSettings.linting.pep8Enabled) { - return Promise.resolve([]); - } - - var pep8Path = this.pythonSettings.linting.pep8Path; - var cmdLine = `${pep8Path} ${PEP_COMMANDLINE} "${filePath}"`; - return new Promise(resolve => { - this.run(cmdLine, filePath, txtDocumentLines, workspace.rootPath).then(messages=> { - //All messages in pep8 are treated as warnings for now - messages.forEach(msg=> { - msg.severity = baseLinter.LintMessageSeverity.Information; - }); - - resolve(messages); - }); - }); - } -} diff --git a/src/client/linters/prospector.ts b/src/client/linters/prospector.ts index f31974ed7793..2341b49e155b 100644 --- a/src/client/linters/prospector.ts +++ b/src/client/linters/prospector.ts @@ -1,40 +1,62 @@ -'use strict'; - import * as path from 'path'; -import * as baseLinter from './baseLinter'; -import * as settings from './../common/configSettings'; -import {OutputChannel, workspace} from 'vscode'; - -const PROSPECTOR_COMMANDLINE = " --output-format=vscode"; - -const REGEX = '(?\\d+),(?\\d+),(?[\\w-]+),(?[\\w-]+):(?.*)\\r?(\\n|$)'; - +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; +import { traceError } from '../common/logger'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { BaseLinter } from './baseLinter'; +import { ILintMessage } from './types'; + +interface IProspectorResponse { + messages: IProspectorMessage[]; +} +interface IProspectorMessage { + source: string; + message: string; + code: string; + location: IProspectorLocation; +} +interface IProspectorLocation { + function: string; + path: string; + line: number; + character: number; + module: 'beforeFormat'; +} -export class Linter extends baseLinter.BaseLinter { - constructor(rootDir: string, pythonSettings: settings.IPythonSettings, outputChannel: OutputChannel) { - super("prospector", pythonSettings, outputChannel); +export class Prospector extends BaseLinter { + constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + super(Product.prospector, outputChannel, serviceContainer); } - public runLinter(filePath: string, txtDocumentLines: string[]): Promise { - if (!this.pythonSettings.linting.prospectorEnabled) { - return Promise.resolve([]); + protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { + const cwd = this.getWorkspaceRootPath(document); + const relativePath = path.relative(cwd, document.uri.fsPath); + return this.run(['--absolute-paths', '--output-format=json', relativePath], document, cancellation); + } + protected async parseMessages(output: string, _document: TextDocument, _token: CancellationToken, _regEx: string) { + let parsedData: IProspectorResponse; + try { + parsedData = JSON.parse(output); + } catch (ex) { + this.outputChannel.appendLine(`${'#'.repeat(10)}Linting Output - ${this.info.id}${'#'.repeat(10)}`); + this.outputChannel.append(output); + traceError('Failed to parse Prospector output', ex); + return []; } - - var prospectorPath = this.pythonSettings.linting.prospectorPath; - var prospectorSourcePath = this.pythonSettings.linting.prospectorSourcePath; - var prospectorExtraCommands = this.pythonSettings.linting.prospectorExtraCommands; - // prospector works best with relative path - var fileName = filePath.replace(path.join(workspace.rootPath, prospectorSourcePath, '/'), ''); - var cmdLine = `${prospectorPath} ${PROSPECTOR_COMMANDLINE} ${prospectorExtraCommands} "${fileName}"`; - return new Promise((resolve, reject) => { - this.run(cmdLine, filePath, txtDocumentLines, path.join(workspace.rootPath, prospectorSourcePath) , REGEX).then(messages=> { - //All messages in prospector are treated as warn, ings for now - messages.forEach(msg=> { - msg.severity = baseLinter.LintMessageSeverity.Information; - }); - - resolve(messages); - }, reject); - }); + return parsedData.messages + .filter((_value, index) => index <= this.pythonSettings.linting.maxNumberOfProblems) + .map((msg) => { + const lineNumber = msg.location.line === null || isNaN(msg.location.line) ? 1 : msg.location.line; + + return { + code: msg.code, + message: msg.message, + column: msg.location.character, + line: lineNumber, + type: msg.code, + provider: `${this.info.id} - ${msg.source}` + }; + }); } } diff --git a/src/client/linters/pycodestyle.ts b/src/client/linters/pycodestyle.ts new file mode 100644 index 000000000000..2d425ab29b18 --- /dev/null +++ b/src/client/linters/pycodestyle.ts @@ -0,0 +1,29 @@ +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { BaseLinter } from './baseLinter'; +import { ILintMessage } from './types'; + +const COLUMN_OFF_SET = 1; + +export class Pycodestyle extends BaseLinter { + constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + super(Product.pycodestyle, outputChannel, serviceContainer, COLUMN_OFF_SET); + } + + protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { + const messages = await this.run( + ['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', document.uri.fsPath], + document, + cancellation + ); + messages.forEach((msg) => { + msg.severity = this.parseMessagesSeverity( + msg.type, + this.pythonSettings.linting.pycodestyleCategorySeverity + ); + }); + return messages; + } +} diff --git a/src/client/linters/pydocstyle.ts b/src/client/linters/pydocstyle.ts index c66ceabb5192..718e8b359b21 100644 --- a/src/client/linters/pydocstyle.ts +++ b/src/client/linters/pydocstyle.ts @@ -1,97 +1,84 @@ -'use strict'; - import * as path from 'path'; -import * as baseLinter from './baseLinter'; -import {ILintMessage} from './baseLinter'; -import * as settings from './../common/configSettings'; -import {OutputChannel, window} from 'vscode'; -import { exec } from 'child_process'; -import {sendCommand} from './../common/childProc'; +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; +import { traceError } from '../common/logger'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { IS_WINDOWS } from './../common/platform/constants'; +import { BaseLinter } from './baseLinter'; +import { ILintMessage, LintMessageSeverity } from './types'; -export class Linter extends baseLinter.BaseLinter { - constructor(rootDir: string, pythonSettings: settings.IPythonSettings, outputChannel: OutputChannel) { - super("pydocstyle", pythonSettings, outputChannel); +export class PyDocStyle extends BaseLinter { + constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + super(Product.pydocstyle, outputChannel, serviceContainer); } - public runLinter(filePath: string, txtDocumentLines: string[]): Promise { - if (!this.pythonSettings.linting.pydocstyleEnabled) { - return Promise.resolve([]); - } - - var pydocStylePath = this.pythonSettings.linting.pydocStylePath; - var cmdLine = `${pydocStylePath} "${filePath}"`; - return new Promise(resolve => { - this.run(cmdLine, filePath, txtDocumentLines).then(messages => { - //All messages in pep8 are treated as warnings for now - messages.forEach(msg => { - msg.severity = baseLinter.LintMessageSeverity.Information; - }); - - resolve(messages); - }); + protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { + const messages = await this.run([document.uri.fsPath], document, cancellation); + // All messages in pep8 are treated as warnings for now. + messages.forEach((msg) => { + msg.severity = LintMessageSeverity.Warning; }); - } - protected run(commandLine: string, filePath: string, txtDocumentLines: string[]): Promise { - var outputChannel = this.outputChannel; - var linterId = this.Id; + return messages; + } - return new Promise((resolve, reject) => { - var fileDir = path.dirname(filePath); - sendCommand(commandLine, fileDir, true).then(data => { - outputChannel.clear(); - outputChannel.append(data); - var outputLines = data.split(/\r?\n/g); - var diagnostics: ILintMessage[] = []; - var baseFileName = path.basename(filePath); + protected async parseMessages(output: string, document: TextDocument, _token: CancellationToken, _regEx: string) { + let outputLines = output.split(/\r?\n/g); + const baseFileName = path.basename(document.uri.fsPath); - //Remember, the first line of the response contains the file name and line number, the next line contains the error message - //So we have two lines per message, hence we need to take lines in pairs - var maxLines = this.pythonSettings.linting.maxNumberOfProblems * 2; - //First line is almost always empty - while (outputLines.length > 0 && outputLines[0].trim().length === 0) { - outputLines.splice(0, 1); - } - outputLines = outputLines.filter((value, index) => index < maxLines); + // Remember, the first line of the response contains the file name and line number, the next line contains the error message. + // So we have two lines per message, hence we need to take lines in pairs. + const maxLines = this.pythonSettings.linting.maxNumberOfProblems * 2; + // First line is almost always empty. + const oldOutputLines = outputLines.filter((line) => line.length > 0); + outputLines = []; + for (let counter = 0; counter < oldOutputLines.length / 2; counter += 1) { + outputLines.push(oldOutputLines[2 * counter] + oldOutputLines[2 * counter + 1]); + } - //Iterate through the lines (skipping the messages) - //So, just iterate the response in pairs - for (var counter = 0; counter < outputLines.length; counter = counter + 2) { + return ( + outputLines + .filter((value, index) => index < maxLines && value.indexOf(':') >= 0) + .map((line) => { + // Windows will have a : after the drive letter (e.g. c:\). + if (IS_WINDOWS) { + return line.substring(line.indexOf(`${baseFileName}:`) + baseFileName.length + 1).trim(); + } + return line.substring(line.indexOf(':') + 1).trim(); + }) + // Iterate through the lines (skipping the messages). + // So, just iterate the response in pairs. + .map((line) => { try { - var line = outputLines[counter]; if (line.trim().length === 0) { - continue; + return; } - var messageLine = outputLines[counter + 1]; - var lineNumber = parseInt(line.substring(line.indexOf(baseFileName) + baseFileName.length + 1)); - var code = messageLine.substring(0, messageLine.indexOf(":")).trim(); - var message = messageLine.substring(messageLine.indexOf(":") + 1).trim(); + const lineNumber = parseInt(line.substring(0, line.indexOf(' ')), 10); + const part = line.substring(line.indexOf(':') + 1).trim(); + const code = part.substring(0, part.indexOf(':')).trim(); + const message = part.substring(part.indexOf(':') + 1).trim(); - var sourceLine = txtDocumentLines[lineNumber - 1]; - var trmmedSourceLine = sourceLine.trim(); - var sourceStart = sourceLine.indexOf(trmmedSourceLine); - var endCol = sourceStart + trmmedSourceLine.length; + const sourceLine = document.lineAt(lineNumber - 1).text; + const trmmedSourceLine = sourceLine.trim(); + const sourceStart = sourceLine.indexOf(trmmedSourceLine); - diagnostics.push({ + // tslint:disable-next-line:no-object-literal-type-assertion + return { code: code, message: message, column: sourceStart, line: lineNumber, - type: "", - provider: this.Id - }); - } - catch (ex) { - //Hmm, need to handle this later - var y = ""; + type: '', + provider: this.info.id + } as ILintMessage; + } catch (ex) { + traceError(`Failed to parse pydocstyle line '${line}'`, ex); + return; } - } - - resolve(diagnostics); - }, error => { - outputChannel.appendLine(`Linting with ${linterId} failed. If not installed please turn if off in settings.\n ${error}`); - window.showInformationMessage(`Linting with ${linterId} failed. If not installed please turn if off in settings. View Python output for details.`); - }); - }); + }) + .filter((item) => item !== undefined) + .map((item) => item!) + ); } } diff --git a/src/client/linters/pylama.ts b/src/client/linters/pylama.ts new file mode 100644 index 000000000000..c1159b2d9544 --- /dev/null +++ b/src/client/linters/pylama.ts @@ -0,0 +1,26 @@ +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { BaseLinter } from './baseLinter'; +import { ILintMessage, LintMessageSeverity } from './types'; + +const REGEX = + '(?.py):(?\\d+):(?\\d+): \\[(?\\w+)\\] (?\\w\\d+):? (?.*)\\r?(\\n|$)'; +const COLUMN_OFF_SET = 1; + +export class PyLama extends BaseLinter { + constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + super(Product.pylama, outputChannel, serviceContainer, COLUMN_OFF_SET); + } + + protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { + const messages = await this.run(['--format=parsable', document.uri.fsPath], document, cancellation, REGEX); + // All messages in pylama are treated as warnings for now. + messages.forEach((msg) => { + msg.severity = LintMessageSeverity.Warning; + }); + + return messages; + } +} diff --git a/src/client/linters/pylint.ts b/src/client/linters/pylint.ts index 55f2ad6e4503..cf3474427dd7 100644 --- a/src/client/linters/pylint.ts +++ b/src/client/linters/pylint.ts @@ -1,44 +1,173 @@ -'use strict'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as os from 'os'; import * as path from 'path'; -import * as baseLinter from './baseLinter'; -import * as settings from './../common/configSettings'; -import {OutputChannel, workspace} from 'vscode'; +import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; +import '../common/extensions'; +import { IFileSystem, IPlatformService } from '../common/platform/types'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { BaseLinter } from './baseLinter'; +import { ILintMessage } from './types'; -const PYLINT_COMMANDLINE = " --msg-template='{line},{column},{category},{msg_id}:{msg}' --reports=n --output-format=text"; +const pylintrc = 'pylintrc'; +const dotPylintrc = '.pylintrc'; +const REGEX = '(?\\d+),(?-?\\d+),(?\\w+),(?[\\w-]+):(?.*)\\r?(\\n|$)'; -export class Linter extends baseLinter.BaseLinter { - constructor(rootDir: string, pythonSettings: settings.IPythonSettings, outputChannel: OutputChannel) { - super("pylint", pythonSettings, outputChannel); +export class Pylint extends BaseLinter { + private fileSystem: IFileSystem; + private platformService: IPlatformService; + + constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { + super(Product.pylint, outputChannel, serviceContainer); + this.fileSystem = serviceContainer.get(IFileSystem); + this.platformService = serviceContainer.get(IPlatformService); } - private parseMessagesSeverity(category: string): baseLinter.LintMessageSeverity { - if (this.pythonSettings.linting.pylintCategorySeverity[category]) { - var severityName = this.pythonSettings.linting.pylintCategorySeverity[category]; - if (baseLinter.LintMessageSeverity[severityName]) { - return baseLinter.LintMessageSeverity[severityName] - } + protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { + let minArgs: string[] = []; + // Only use minimal checkers if + // a) there are no custom arguments and + // b) there is no pylintrc file next to the file or at the workspace root + const uri = document.uri; + const workspaceRoot = this.getWorkspaceRootPath(document); + const settings = this.configService.getSettings(uri); + if ( + settings.linting.pylintUseMinimalCheckers && + this.info.linterArgs(uri).length === 0 && + // Check pylintrc next to the file or above up to and including the workspace root + !(await Pylint.hasConfigurationFileInWorkspace(this.fileSystem, path.dirname(uri.fsPath), workspaceRoot)) && + // Check for pylintrc at the root and above + !(await Pylint.hasConfigurationFile( + this.fileSystem, + this.getWorkspaceRootPath(document), + this.platformService + )) + ) { + // Disable all checkers up front and then selectively add back in: + // - All F checkers + // - Select W checkers + // - All E checkers _manually_ + // (see https://github.com/Microsoft/vscode-python/issues/722 for + // why; see + // https://gist.github.com/brettcannon/eff7f38a60af48d39814cbb2f33b3d1d + // for a script to regenerate the list of E checkers) + minArgs = [ + '--disable=all', + '--enable=F' + + ',unreachable,duplicate-key,unnecessary-semicolon' + + ',global-variable-not-assigned,unused-variable' + + ',unused-wildcard-import,binary-op-exception' + + ',bad-format-string,anomalous-backslash-in-string' + + ',bad-open-mode' + + ',E0001,E0011,E0012,E0100,E0101,E0102,E0103,E0104,E0105,E0107' + + ',E0108,E0110,E0111,E0112,E0113,E0114,E0115,E0116,E0117,E0118' + + ',E0202,E0203,E0211,E0213,E0236,E0237,E0238,E0239,E0240,E0241' + + ',E0301,E0302,E0303,E0401,E0402,E0601,E0602,E0603,E0604,E0611' + + ',E0632,E0633,E0701,E0702,E0703,E0704,E0710,E0711,E0712,E1003' + + ',E1101,E1102,E1111,E1120,E1121,E1123,E1124,E1125,E1126,E1127' + + ',E1128,E1129,E1130,E1131,E1132,E1133,E1134,E1135,E1136,E1137' + + ',E1138,E1139,E1200,E1201,E1205,E1206,E1300,E1301,E1302,E1303' + + ',E1304,E1305,E1306,E1310,E1700,E1701' + ]; } + const args = [ + "--msg-template='{line},{column},{category},{symbol}:{msg}'", + '--reports=n', + '--output-format=text', + uri.fsPath + ]; + const messages = await this.run(minArgs.concat(args), document, cancellation, REGEX); + messages.forEach((msg) => { + msg.severity = this.parseMessagesSeverity(msg.type, settings.linting.pylintCategorySeverity); + }); - return baseLinter.LintMessageSeverity.Information; + return messages; } - public runLinter(filePath: string, txtDocumentLines: string[]): Promise { - if (!this.pythonSettings.linting.pylintEnabled) { - return Promise.resolve([]); + // tslint:disable-next-line:member-ordering + public static async hasConfigurationFile( + fs: IFileSystem, + folder: string, + platformService: IPlatformService + ): Promise { + // https://pylint.readthedocs.io/en/latest/user_guide/run.html + // https://github.com/PyCQA/pylint/blob/975e08148c0faa79958b459303c47be1a2e1500a/pylint/config.py + // 1. pylintrc in the current working directory + // 2. .pylintrc in the current working directory + // 3. If the current working directory is in a Python module, Pylint searches + // up the hierarchy of Python modules until it finds a pylintrc file. + // This allows you to specify coding standards on a module by module basis. + // A directory is judged to be a Python module if it contains an __init__.py file. + // 4. The file named by environment variable PYLINTRC + // 5. if you have a home directory which isn’t /root: + // a) .pylintrc in your home directory + // b) .config/pylintrc in your home directory + // 6. /etc/pylintrc + if (process.env.PYLINTRC) { + return true; + } + + if ( + (await fs.fileExists(path.join(folder, pylintrc))) || + (await fs.fileExists(path.join(folder, dotPylintrc))) + ) { + return true; } - var pylintPath = this.pythonSettings.linting.pylintPath; - var cmdLine = `${pylintPath} ${PYLINT_COMMANDLINE} "${filePath}"`; - return new Promise((resolve, reject) => { - this.run(cmdLine, filePath, txtDocumentLines, workspace.rootPath).then(messages=> { - messages.forEach(msg=> { - msg.severity = this.parseMessagesSeverity(msg.type); - }); + let current = folder; + let above = path.dirname(folder); + do { + if (!(await fs.fileExists(path.join(current, '__init__.py')))) { + break; + } + if ( + (await fs.fileExists(path.join(current, pylintrc))) || + (await fs.fileExists(path.join(current, dotPylintrc))) + ) { + return true; + } + current = above; + above = path.dirname(above); + } while (!fs.arePathsSame(current, above)); - resolve(messages); - }, reject); - }); + const home = os.homedir(); + if (await fs.fileExists(path.join(home, dotPylintrc))) { + return true; + } + if (await fs.fileExists(path.join(home, '.config', pylintrc))) { + return true; + } + + if (!platformService.isWindows) { + if (await fs.fileExists(path.join('/etc', pylintrc))) { + return true; + } + } + return false; + } + + // tslint:disable-next-line:member-ordering + public static async hasConfigurationFileInWorkspace( + fs: IFileSystem, + folder: string, + root: string + ): Promise { + // Search up from file location to the workspace root + let current = folder; + let above = path.dirname(current); + do { + if ( + (await fs.fileExists(path.join(current, pylintrc))) || + (await fs.fileExists(path.join(current, dotPylintrc))) + ) { + return true; + } + current = above; + above = path.dirname(above); + } while (!fs.arePathsSame(current, root) && !fs.arePathsSame(current, above)); + return false; } -} \ No newline at end of file +} diff --git a/src/client/linters/serviceRegistry.ts b/src/client/linters/serviceRegistry.ts new file mode 100644 index 000000000000..89ea2da1c10e --- /dev/null +++ b/src/client/linters/serviceRegistry.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { IExtensionActivationService } from '../activation/types'; +import { IServiceManager } from '../ioc/types'; +import { LinterProvider } from '../providers/linterProvider'; +import { AvailableLinterActivator } from './linterAvailability'; +import { LinterManager } from './linterManager'; +import { LintingEngine } from './lintingEngine'; +import { IAvailableLinterActivator, ILinterManager, ILintingEngine } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(ILintingEngine, LintingEngine); + serviceManager.addSingleton(ILinterManager, LinterManager); + serviceManager.add(IAvailableLinterActivator, AvailableLinterActivator); + serviceManager.addSingleton(IExtensionActivationService, LinterProvider); +} diff --git a/src/client/linters/types.ts b/src/client/linters/types.ts new file mode 100644 index 000000000000..6dba6e9d15ac --- /dev/null +++ b/src/client/linters/types.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as vscode from 'vscode'; +import { ExecutionInfo, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { LinterTrigger } from '../telemetry/types'; + +export interface IErrorHandler { + handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo): Promise; +} + +export enum LinterId { + Flake8 = 'flake8', + MyPy = 'mypy', + PyCodeStyle = 'pycodestyle', + Prospector = 'prospector', + PyDocStyle = 'pydocstyle', + PyLama = 'pylama', + PyLint = 'pylint', + Bandit = 'bandit' +} + +export interface ILinterInfo { + readonly id: LinterId; + readonly product: Product; + readonly pathSettingName: string; + readonly argsSettingName: string; + readonly enabledSettingName: string; + readonly configFileNames: string[]; + enableAsync(enabled: boolean, resource?: vscode.Uri): Promise; + isEnabled(resource?: vscode.Uri): boolean; + pathName(resource?: vscode.Uri): string; + linterArgs(resource?: vscode.Uri): string[]; + getExecutionInfo(customArgs: string[], resource?: vscode.Uri): ExecutionInfo; +} + +export interface ILinter { + readonly info: ILinterInfo; + lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise; +} + +export const IAvailableLinterActivator = Symbol('IAvailableLinterActivator'); +export interface IAvailableLinterActivator { + promptIfLinterAvailable(linter: ILinterInfo, resource?: vscode.Uri): Promise; +} + +export const ILinterManager = Symbol('ILinterManager'); +export interface ILinterManager { + getAllLinterInfos(): ILinterInfo[]; + getLinterInfo(product: Product): ILinterInfo; + getActiveLinters(silent: boolean, resource?: vscode.Uri): Promise; + isLintingEnabled(silent: boolean, resource?: vscode.Uri): Promise; + enableLintingAsync(enable: boolean, resource?: vscode.Uri): Promise; + setActiveLintersAsync(products: Product[], resource?: vscode.Uri): Promise; + createLinter( + product: Product, + outputChannel: vscode.OutputChannel, + serviceContainer: IServiceContainer, + resource?: vscode.Uri + ): Promise; +} + +export interface ILintMessage { + line: number; + column: number; + code: string | undefined; + message: string; + type: string; + severity?: LintMessageSeverity; + provider: string; +} +export enum LintMessageSeverity { + Hint, + Error, + Warning, + Information +} + +export const ILintingEngine = Symbol('ILintingEngine'); +export interface ILintingEngine { + readonly diagnostics: vscode.DiagnosticCollection; + lintOpenPythonFiles(): Promise; + lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise; + clearDiagnostics(document: vscode.TextDocument): void; +} diff --git a/src/client/logging/_global.ts b/src/client/logging/_global.ts new file mode 100644 index 000000000000..dabd864551f3 --- /dev/null +++ b/src/client/logging/_global.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as winston from 'winston'; +import { IOutputChannel } from '../common/types'; +import { CallInfo } from '../common/utils/decorators'; +import { getFormatter } from './formatters'; +import { LogLevel, resolveLevelName } from './levels'; +import { configureLogger, createLogger, getPreDefinedConfiguration, logToAll } from './logger'; +import { createTracingDecorator, LogInfo, TraceOptions, tracing as _tracing } from './trace'; +import { getPythonOutputChannelTransport } from './transports'; +import { Arguments } from './util'; + +const globalLogger = createLogger(); +initialize(); + +/** + * Initialize the logger. + * + * For console we do two things here: + * - Anything written to the logger will be displayed in the console + * window as well This is the behavior of the extension when running + * it. When running tests on CI, we might not want this behavior, as + * it'll pollute the test output with logging (as mentioned this is + * optional). Messages logged using our logger will be prefixed with + * `Python Extension: ....` for console window. This way, its easy + * to identify messages specific to the python extension. + * - Monkey patch the console.log and similar methods to send messages + * to the file logger. When running UI tests or similar, and we want + * to see everything that was dumped into `console window`, then we + * need to hijack the console logger. To do this we need to monkey + * patch the console methods. This is optional (generally done when + * running tests on CI). + * + * For the logfile: + * - we send all logging output to a log file. We log to the file + * only if a file has been specified as an env variable. Currently + * this is setup on CI servers. + */ +function initialize() { + configureLogger(globalLogger, getPreDefinedConfiguration()); +} + +// Set the logging level the extension logs at. +export function setLoggingLevel(level: LogLevel | 'off') { + if (level === 'off') { + // For now we disable all logging. One alternative would be + // to only disable logging to the output channel (by removing + // the transport from the logger). + globalLogger.clear(); + } else { + const levelName = resolveLevelName(level, winston.config.npm.levels); + if (levelName) { + globalLogger.level = levelName; + } + } +} + +// Register the output channel transport the logger will log into. +export function addOutputChannelLogging(channel: IOutputChannel) { + const formatter = getFormatter(); + const transport = getPythonOutputChannelTransport(channel, formatter); + globalLogger.add(transport); +} + +// Emit a log message derived from the args to all enabled transports. +export function log(logLevel: LogLevel, ...args: Arguments) { + logToAll([globalLogger], logLevel, args); +} + +// tslint:disable-next-line:no-any +export function logVerbose(...args: any[]) { + log(LogLevel.Info, ...args); +} + +// tslint:disable-next-line:no-any +export function logError(...args: any[]) { + log(LogLevel.Error, ...args); +} + +// tslint:disable-next-line:no-any +export function logInfo(...args: any[]) { + log(LogLevel.Info, ...args); +} + +// tslint:disable-next-line:no-any +export function logWarning(...args: any[]) { + log(LogLevel.Warn, ...args); +} + +// This is like a "context manager" that logs tracing info. +export function tracing(info: LogInfo, run: () => T, call?: CallInfo): T { + return _tracing([globalLogger], info, run, call); +} + +export namespace traceDecorators { + const DEFAULT_OPTS: TraceOptions = TraceOptions.Arguments | TraceOptions.ReturnValue; + + export function verbose(message: string, opts: TraceOptions = DEFAULT_OPTS) { + return createTracingDecorator([globalLogger], { message, opts }); + } + export function error(message: string) { + const opts = DEFAULT_OPTS; + const level = LogLevel.Error; + return createTracingDecorator([globalLogger], { message, opts, level }); + } + export function info(message: string) { + const opts = TraceOptions.None; + return createTracingDecorator([globalLogger], { message, opts }); + } + export function warn(message: string) { + const opts = DEFAULT_OPTS; + const level = LogLevel.Warn; + return createTracingDecorator([globalLogger], { message, opts, level }); + } +} diff --git a/src/client/logging/formatters.ts b/src/client/logging/formatters.ts new file mode 100644 index 000000000000..b3dd4e52761f --- /dev/null +++ b/src/client/logging/formatters.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { format } from 'winston'; +import { getLevel, LogLevel, LogLevelName } from './levels'; + +const TIMESTAMP = 'YYYY-MM-DD HH:mm:ss'; + +// Knobs used when creating a formatter. +export type FormatterOptions = { + label?: string; +}; + +// Pascal casing is used so log files get highlighted when viewing +// in VSC and other editors. +const formattedLogLevels: { [K in LogLevel]: string } = { + [LogLevel.Error]: 'Error', + [LogLevel.Warn]: 'Warn', + [LogLevel.Info]: 'Info', + [LogLevel.Debug]: 'Debug', + [LogLevel.Trace]: 'Trace' +}; + +// Return a consistent representation of the given log level. +function normalizeLevel(name: LogLevelName): string { + const level = getLevel(name); + if (level) { + const norm = formattedLogLevels[level]; + if (norm) { + return norm; + } + } + return `${name.substring(0, 1).toUpperCase()}${name.substring(1).toLowerCase()}`; +} + +// Return a log entry that can be emitted as-is. +function formatMessage(level: LogLevelName, timestamp: string, message: string): string { + const levelFormatted = normalizeLevel(level); + return `${levelFormatted} ${timestamp}: ${message}`; +} + +// Return a log entry that can be emitted as-is. +function formatLabeledMessage(level: LogLevelName, timestamp: string, label: string, message: string): string { + const levelFormatted = normalizeLevel(level); + return `${levelFormatted} ${label} ${timestamp}: ${message}`; +} + +// Return a minimal format object that can be used with a "winston" +// logging transport. +function getMinimalFormatter() { + return format.combine( + format.timestamp({ format: TIMESTAMP }), + format.printf( + // This relies on the timestamp formatter we added above: + ({ level, message, timestamp }) => formatMessage(level as LogLevelName, timestamp, message) + ) + ); +} + +// Return a minimal format object that can be used with a "winston" +// logging transport. +function getLabeledFormatter(label_: string) { + return format.combine( + format.label({ label: label_ }), + format.timestamp({ format: TIMESTAMP }), + format.printf( + // This relies on the label and timestamp formatters we added above: + ({ level, message, label, timestamp }) => + formatLabeledMessage(level as LogLevelName, timestamp, label, message) + ) + ); +} + +// Return a format object that can be used with a "winston" logging transport. +export function getFormatter(opts: FormatterOptions = {}) { + if (opts.label) { + return getLabeledFormatter(opts.label); + } + return getMinimalFormatter(); +} diff --git a/src/client/logging/index.ts b/src/client/logging/index.ts new file mode 100644 index 000000000000..861c98f38a41 --- /dev/null +++ b/src/client/logging/index.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export { + // aliases + // (for convenience) + setLoggingLevel, + addOutputChannelLogging, + logError, + logInfo, + logVerbose, + logWarning, + traceDecorators, + tracing +} from './_global'; diff --git a/src/client/logging/levels.ts b/src/client/logging/levels.ts new file mode 100644 index 000000000000..c0982dd3c282 --- /dev/null +++ b/src/client/logging/levels.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// IMPORTANT: This file should only be importing from the '../client/logging' directory, as we +// delete everything in '../client' except for '../client/logging' before running smoke tests. + +import * as winston from 'winston'; + +//====================== +// Our custom log levels + +export enum LogLevel { + // Larger numbers are higher priority. + Error = 40, + Warn = 30, + Info = 20, + Debug = 10, + Trace = 5 +} +export type LogLevelName = 'ERROR' | 'WARNING' | 'INFORMATION' | 'DEBUG' | 'DEBUG-TRACE'; +const logLevelMap: { [K in LogLevel]: LogLevelName } = { + [LogLevel.Error]: 'ERROR', + [LogLevel.Warn]: 'WARNING', + [LogLevel.Info]: 'INFORMATION', + [LogLevel.Debug]: 'DEBUG', + [LogLevel.Trace]: 'DEBUG-TRACE' +}; +// This can be used for winston.LoggerOptions.levels. +export const configLevels: winston.config.AbstractConfigSetLevels = { + ERROR: 0, + WARNING: 1, + INFORMATION: 2, + DEBUG: 4, + 'DEBUG-TRACE': 5 +}; + +//====================== +// Other log levels + +// The level names from winston/config.npm. +// See: https://www.npmjs.com/package/winston#logging-levels +type NPMLogLevelName = 'error' | 'warn' | 'info' | 'verbose' | 'debug' | 'silly'; +const npmLogLevelMap: { [K in LogLevel]: NPMLogLevelName } = { + [LogLevel.Error]: 'error', + [LogLevel.Warn]: 'warn', + [LogLevel.Info]: 'info', + [LogLevel.Debug]: 'debug', + [LogLevel.Trace]: 'silly' +}; + +//====================== +// lookup functions + +// Convert from LogLevel enum to the proper level name. +export function resolveLevelName( + level: LogLevel, + // Default to configLevels. + levels?: winston.config.AbstractConfigSetLevels +): string | undefined { + if (levels === undefined) { + return getLevelName(level); + } else if (levels === configLevels) { + return getLevelName(level); + } else if (levels === winston.config.npm.levels) { + return npmLogLevelMap[level]; + } else { + return undefined; + } +} +export function getLevelName(level: LogLevel): LogLevelName | undefined { + return logLevelMap[level]; +} + +// Convert from a level name to the actual level. +export function resolveLevel( + levelName: string, + // Default to configLevels. + levels?: winston.config.AbstractConfigSetLevels +): LogLevel | undefined { + let levelMap: { [K in LogLevel]: string }; + if (levels === undefined) { + levelMap = logLevelMap; + } else if (levels === configLevels) { + levelMap = logLevelMap; + } else if (levels === winston.config.npm.levels) { + levelMap = npmLogLevelMap; + } else { + return undefined; + } + for (const level of Object.keys(levelMap)) { + if (typeof level === 'string') { + continue; + } + if (logLevelMap[level] === levelName) { + return level; + } + } + return undefined; +} +export function getLevel(name: LogLevelName): LogLevel | undefined { + return resolveLevel(name); +} diff --git a/src/client/logging/logger.ts b/src/client/logging/logger.ts new file mode 100644 index 000000000000..ab5ba55cb519 --- /dev/null +++ b/src/client/logging/logger.ts @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// IMPORTANT: This file should only be importing from the '../client/logging' directory, as we +// delete everything in '../client' except for '../client/logging' before running smoke tests. + +import * as util from 'util'; +import * as winston from 'winston'; +import * as Transport from 'winston-transport'; +import { getFormatter } from './formatters'; +import { LogLevel, resolveLevelName } from './levels'; +import { getConsoleTransport, getFileTransport, isConsoleTransport } from './transports'; +import { Arguments } from './util'; + +export type LoggerConfig = { + level?: LogLevel; + file?: { + logfile: string; + }; + console?: { + label?: string; + }; +}; + +// Create a logger just the way we like it. +export function createLogger(config?: LoggerConfig) { + const logger = winston.createLogger({ + // We would also set "levels" here. + }); + if (config) { + configureLogger(logger, config); + } + return logger; +} + +interface IConfigurableLogger { + level: string; + add(transport: Transport): void; +} + +// tslint:disable-next-line: no-suspicious-comment +/** + * TODO: We should actually have this method in `./_global.ts` as this is exported globally. + * But for some reason, importing '../client/logging/_global' fails when launching the tests. + * More details in the comment https://github.com/microsoft/vscode-python/pull/11897#discussion_r433954993 + * https://github.com/microsoft/vscode-python/issues/12137 + */ +export function getPreDefinedConfiguration(): LoggerConfig { + const config: LoggerConfig = {}; + + // Do not log to console if running tests and we're not + // asked to do so. + if (process.env.VSC_PYTHON_FORCE_LOGGING) { + config.console = {}; + // In CI there's no need for the label. + const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; + if (!isCI) { + config.console.label = 'Python Extension:'; + } + } + if (process.env.VSC_PYTHON_LOG_FILE) { + config.file = { + logfile: process.env.VSC_PYTHON_LOG_FILE + }; + } + return config; +} + +// Set up a logger just the way we like it. +export function configureLogger(logger: IConfigurableLogger, config: LoggerConfig) { + if (config.level) { + const levelName = resolveLevelName(config.level); + if (levelName) { + logger.level = levelName; + } + } + + if (config.file) { + const formatter = getFormatter(); + const transport = getFileTransport(config.file.logfile, formatter); + logger.add(transport); + } + if (config.console) { + const formatter = getFormatter({ label: config.console.label }); + const transport = getConsoleTransport(formatter); + logger.add(transport); + } +} + +export interface ILogger { + transports: unknown[]; + levels: winston.config.AbstractConfigSetLevels; + log(level: string, message: string): void; +} + +// Emit a log message derived from the args to all enabled transports. +export function logToAll(loggers: ILogger[], logLevel: LogLevel, args: Arguments) { + const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); + for (const logger of loggers) { + if (logger.transports.length > 0) { + const levelName = getLevelName(logLevel, logger.levels, isConsoleLogger(logger)); + logger.log(levelName, message); + } + } +} + +function isConsoleLogger(logger: ILogger): boolean { + for (const transport of logger.transports) { + if (isConsoleTransport(transport)) { + return true; + } + } + return false; +} + +function getLevelName(level: LogLevel, levels: winston.config.AbstractConfigSetLevels, isConsole?: boolean): string { + const levelName = resolveLevelName(level, levels); + if (levelName) { + return levelName; + } else if (isConsole) { + // XXX Hard-coding this is fragile: + return 'silly'; + } else { + return resolveLevelName(LogLevel.Info, levels) || 'info'; + } +} diff --git a/src/client/logging/trace.ts b/src/client/logging/trace.ts new file mode 100644 index 000000000000..3191df5bd9d7 --- /dev/null +++ b/src/client/logging/trace.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { CallInfo, trace as traceDecorator } from '../common/utils/decorators'; +import { TraceInfo, tracing as _tracing } from '../common/utils/misc'; +import { sendTelemetryEvent } from '../telemetry'; +import { LogLevel } from './levels'; +import { ILogger, logToAll } from './logger'; +import { argsToLogString, returnValueToLogString } from './util'; + +// The information we want to log. +export enum TraceOptions { + None = 0, + Arguments = 1, + ReturnValue = 2 +} + +export function createTracingDecorator(loggers: ILogger[], logInfo: LogInfo) { + return traceDecorator((call, traced) => logResult(loggers, logInfo, traced, call)); +} + +// This is like a "context manager" that logs tracing info. +export function tracing(loggers: ILogger[], logInfo: LogInfo, run: () => T, call?: CallInfo): T { + return _tracing((traced) => logResult(loggers, logInfo, traced, call), run); +} + +export type LogInfo = { + opts: TraceOptions; + message: string; + level?: LogLevel; +}; + +function normalizeCall(call: CallInfo): CallInfo { + let { kind, name, args } = call; + if (!kind || kind === '') { + kind = 'Function'; + } + if (!name || name === '') { + name = ''; + } + if (!args) { + args = []; + } + return { kind, name, args }; +} + +function formatMessages(info: LogInfo, traced: TraceInfo, call?: CallInfo): string { + call = normalizeCall(call!); + const messages = [info.message]; + messages.push( + `${call.kind} name = ${call.name}`.trim(), + `completed in ${traced.elapsed}ms`, + `has a ${traced.returnValue ? 'truthy' : 'falsy'} return value` + ); + if ((info.opts & TraceOptions.Arguments) === TraceOptions.Arguments) { + messages.push(argsToLogString(call.args)); + } + if ((info.opts & TraceOptions.ReturnValue) === TraceOptions.ReturnValue) { + messages.push(returnValueToLogString(traced.returnValue)); + } + return messages.join(', '); +} + +function logResult(loggers: ILogger[], info: LogInfo, traced: TraceInfo, call?: CallInfo) { + const formatted = formatMessages(info, traced, call); + if (traced.err === undefined) { + // The call did not fail. + if (!info.level || info.level > LogLevel.Error) { + logToAll(loggers, LogLevel.Info, [formatted]); + } + } else { + logToAll(loggers, LogLevel.Error, [formatted, traced.err]); + // tslint:disable-next-line:no-any + sendTelemetryEvent('ERROR' as any, undefined, undefined, traced.err); + } +} diff --git a/src/client/logging/transports.ts b/src/client/logging/transports.ts new file mode 100644 index 000000000000..2a8e83a5fa52 --- /dev/null +++ b/src/client/logging/transports.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// IMPORTANT: This file should only be importing from the '../client/logging' directory, as we +// delete everything in '../client' except for '../client/logging' before running smoke tests. + +import * as logform from 'logform'; +import * as path from 'path'; +import { OutputChannel } from 'vscode'; +import * as winston from 'winston'; +import * as Transport from 'winston-transport'; +import { LogLevel, resolveLevel } from './levels'; +import { Arguments } from './util'; + +const folderPath = path.dirname(__dirname); +const folderName = path.basename(folderPath); +const EXTENSION_ROOT_DIR = + folderName === 'client' ? path.join(folderPath, '..', '..') : path.join(folderPath, '..', '..', '..', '..'); +const formattedMessage = Symbol.for('message'); + +export function isConsoleTransport(transport: unknown): boolean { + // tslint:disable-next-line:no-any + return (transport as any).isConsole; +} + +// A winston-compatible transport type. +// We do not use transports.ConsoleTransport because it cannot +// adapt to our custom log levels very well. +class ConsoleTransport extends Transport { + // tslint:disable:no-console + private static funcByLevel: { [K in LogLevel]: (...args: Arguments) => void } = { + [LogLevel.Error]: console.error, + [LogLevel.Warn]: console.warn, + [LogLevel.Info]: console.info, + [LogLevel.Debug]: console.debug, + [LogLevel.Trace]: console.trace + }; + private static defaultFunc = console.log; + // tslint:enable:no-console + + // This is used to identify the type. + public readonly isConsole = true; + + constructor( + // tslint:disable-next-line:no-any + options?: any, + private readonly levels?: winston.config.AbstractConfigSetLevels + ) { + super(options); + } + + // tslint:disable-next-line:no-any + public log?(info: { level: string; message: string; [formattedMessage]: string }, next: () => void): any { + setImmediate(() => this.emit('logged', info)); + const level = resolveLevel(info.level, this.levels); + const msg = info[formattedMessage] || info.message; + this.logToConsole(level, msg); + if (next) { + next(); + } + } + + private logToConsole(level?: LogLevel, msg?: string) { + let func = ConsoleTransport.defaultFunc; + if (level) { + func = ConsoleTransport.funcByLevel[level] || func; + } + func(msg); + } +} + +// Create a console-targeting transport that can be added to a winston logger. +export function getConsoleTransport(formatter: logform.Format): Transport { + return new ConsoleTransport({ + // We minimize customization. + format: formatter + }); +} + +class PythonOutputChannelTransport extends Transport { + // tslint:disable-next-line: no-any + constructor(private readonly channel: OutputChannel, options?: any) { + super(options); + } + // tslint:disable-next-line: no-any + public log?(info: { message: string; [formattedMessage]: string }, next: () => void): any { + setImmediate(() => this.emit('logged', info)); + this.channel.appendLine(info[formattedMessage] || info.message); + if (next) { + next(); + } + } +} + +// Create a Python output channel targeting transport that can be added to a winston logger. +export function getPythonOutputChannelTransport(channel: OutputChannel, formatter: logform.Format) { + return new PythonOutputChannelTransport(channel, { + // We minimize customization. + format: formatter + }); +} + +// Create a file-targeting transport that can be added to a winston logger. +export function getFileTransport(logfile: string, formatter: logform.Format): Transport { + if (!path.isAbsolute(logfile)) { + logfile = path.join(EXTENSION_ROOT_DIR, logfile); + } + return new winston.transports.File({ + format: formatter, + filename: logfile, + handleExceptions: true + }); +} diff --git a/src/client/logging/util.ts b/src/client/logging/util.ts new file mode 100644 index 000000000000..79d977136a89 --- /dev/null +++ b/src/client/logging/util.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// tslint:disable-next-line:no-any +export type Arguments = any[]; + +function valueToLogString(value: unknown, kind: string): string { + if (value === undefined) { + return 'undefined'; + } + if (value === null) { + return 'null'; + } + try { + // tslint:disable-next-line:no-any + if (value && (value as any).fsPath) { + // tslint:disable-next-line:no-any + return ``; + } + return JSON.stringify(value); + } catch { + return `<${kind} cannot be serialized for logging>`; + } +} + +// Convert the given array of values (func call arguments) into a string +// suitable to be used in a log message. +export function argsToLogString(args: Arguments): string { + if (!args) { + return ''; + } + try { + const argStrings = args.map((item, index) => { + const valueString = valueToLogString(item, 'argument'); + return `Arg ${index + 1}: ${valueString}`; + }); + return argStrings.join(', '); + } catch { + return ''; + } +} + +// Convert the given return value into a string +// suitable to be used in a log message. +export function returnValueToLogString(returnValue: unknown): string { + const valueString = valueToLogString(returnValue, 'Return value'); + return `Return Value: ${valueString}`; +} diff --git a/src/client/package.json b/src/client/package.json deleted file mode 100644 index 4df71e8754f8..000000000000 --- a/src/client/package.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "name": "python", - "displayName": "Python", - "description": "Python auto complete and linting", - "version": "0.0.1", - "publisher": "donjayamanne", - "license": "SEE LICENSE IN LICENSE or README.MD", - "homepage": "https://github.com/DonJayamanne/pythonVSCode/blob/master/README.md", - "repository": { - "type": "git", - "url": "https://github.com/DonJayamanne/pythonVSCode" - }, - "bugs": { - "url": "https://github.com/DonJayamanne/pythonVSCode/issues" - }, - "icon": "images/icon.png", - "galleryBanner": { - "color": "#0000FF", - "theme": "dark" - }, - "engines": { - "vscode": "^0.10.1" - }, - "categories": [ - "Languages", - "Linters" - ], - "activationEvents": [ - "onLanguage:python", - "onLanguage:plaintext" - ], - "main": "../../out/client/extension", - "contributes": { - "debuggers": [ - { - "type": "python", - "label": "Python (pdb)", - "enableBreakpointsFor": { - "languageIds": [ - "python" - ] - }, - "program": "./out/client/debugger/pdb/debuggerMain.js", - "runtime": "node", - "configurationAttributes": { - "launch": { - "required": [ - "program" - ], - "properties": { - "program": { - "type": "string", - "description": "Workspace relative path to a text file.", - "default": "__init__.py" - }, - "pythonPath": { - "type": "string", - "description": "Path (fully qualified) to python executable. Use this if you want to use a custom pthon executable version.", - "default": "" - }, - "stopOnEntry": { - "type": "boolean", - "description": "Automatically stop after launch.", - "default": true - }, - "args": { - "type": "array", - "description": "List of arguments for the program", - "default": [] - } - } - } - }, - "initialConfigurations": [ - { - "name": "Python (Pdb)", - "type": "python_pdb", - "request": "launch", - "program": "__init__.py", - "stopOnEntry": true - } - ] - }, - { - "type": "python_win", - "label": "Python (Windows)", - "enableBreakpointsFor": { - "languageIds": [ - "python" - ] - }, - "program": "./out/client/debugger/vs/VSDebugger.js", - "runtime": "node", - "configurationAttributes": { - "launch": { - "required": [ - "program" - ], - "properties": { - "program": { - "type": "string", - "description": "Workspace relative path to a text file.", - "default": "__init__.py" - }, - "pythonPath": { - "type": "string", - "description": "Path (fully qualified) to python executable. Use this if you want to use a custom pthon executable version.", - "default": "" - }, - "args": { - "type": "array", - "description": "List of arguments for the program", - "default": [] - } - } - } - }, - "initialConfigurations": [ - { - "name": "Python (Windows)", - "type": "python_win", - "request": "launch", - "program": "__init__.py" - } - ] - } - ], - "configuration": { - "type": "object", - "title": "Python Configuration", - "properties": { - "python.maxNumberOfProblems": { - "type": "number", - "default": 100, - "description": "Controls the maximum number of problems produced by the server." - } - } - } - }, - "scripts": { - "vscode:prepublish": "node ./node_modules/vscode/bin/compile", - "compile": "node ./node_modules/vscode/bin/compile -watch -p ./" - }, - "dependencies": { - "named-js-regexp": "^1.3.1", - "tmp": "0.0.28", - "path": "0.12.7", - "uint64be": "^1.0.1", - "vscode-debugadapter": "^1.0.1", - "vscode-debugprotocol": "^1.0.1", - "vscode-languageclient": "^1.1.0" - }, - "devDependencies": { - "typescript": "^1.6.2", - "vscode": "0.10.x" - } -} diff --git a/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts b/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts new file mode 100644 index 000000000000..805614794bd7 --- /dev/null +++ b/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CodeAction, + CodeActionContext, + CodeActionKind, + CodeActionProvider, + Diagnostic, + Range, + TextDocument, + WorkspaceEdit +} from 'vscode'; + +/** + * Provides code actions for launch.json + */ +export class LaunchJsonCodeActionProvider implements CodeActionProvider { + public provideCodeActions(document: TextDocument, _: Range, context: CodeActionContext): CodeAction[] { + return context.diagnostics + .filter((diagnostic) => diagnostic.message === 'Incorrect type. Expected "string".') + .map((diagnostic) => this.createFix(document, diagnostic)); + } + + private createFix(document: TextDocument, diagnostic: Diagnostic): CodeAction { + const finalText = `"${document.getText(diagnostic.range)}"`; + const fix = new CodeAction(`Convert to ${finalText}`, CodeActionKind.QuickFix); + fix.edit = new WorkspaceEdit(); + fix.edit.replace(document.uri, diagnostic.range, finalText); + return fix; + } +} diff --git a/src/client/providers/codeActionProvider/main.ts b/src/client/providers/codeActionProvider/main.ts new file mode 100644 index 000000000000..375c9986d949 --- /dev/null +++ b/src/client/providers/codeActionProvider/main.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as vscodeTypes from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IDisposableRegistry } from '../../common/types'; +import { LaunchJsonCodeActionProvider } from './launchJsonCodeActionProvider'; + +@injectable() +export class CodeActionProviderService implements IExtensionSingleActivationService { + constructor(@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry) {} + public async activate(): Promise { + // tslint:disable-next-line:no-require-imports + const vscode = require('vscode') as typeof vscodeTypes; + const documentSelector: vscodeTypes.DocumentFilter = { + scheme: 'file', + language: 'jsonc', + pattern: '**/launch.json' + }; + this.disposableRegistry.push( + vscode.languages.registerCodeActionsProvider(documentSelector, new LaunchJsonCodeActionProvider(), { + providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] + }) + ); + } +} diff --git a/src/client/providers/codeActionProvider/pythonCodeActionProvider.ts b/src/client/providers/codeActionProvider/pythonCodeActionProvider.ts new file mode 100644 index 000000000000..d1c45dd8610e --- /dev/null +++ b/src/client/providers/codeActionProvider/pythonCodeActionProvider.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as vscode from 'vscode'; +import { isNotebookCell } from '../../common/utils/misc'; + +export class PythonCodeActionProvider implements vscode.CodeActionProvider { + public provideCodeActions( + document: vscode.TextDocument, + _range: vscode.Range, + _context: vscode.CodeActionContext, + _token: vscode.CancellationToken + ): vscode.ProviderResult { + if (isNotebookCell(document)) { + return []; + } + const sortImports = new vscode.CodeAction('Sort imports', vscode.CodeActionKind.SourceOrganizeImports); + sortImports.command = { + title: 'Sort imports', + command: 'python.sortImports' + }; + + return [sortImports]; + } +} diff --git a/src/client/providers/completionProvider.ts b/src/client/providers/completionProvider.ts index 9536c2dbdd44..8426cff4ae6d 100644 --- a/src/client/providers/completionProvider.ts +++ b/src/client/providers/completionProvider.ts @@ -1,55 +1,48 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - 'use strict'; import * as vscode from 'vscode'; -import * as proxy from './jediProxy'; - -function parseData(data: proxy.ICompletionResult): vscode.CompletionItem[] { - if (data && data.items.length > 0) { - return data.items.map(item=> { - var completionItem = new vscode.CompletionItem(item.text); - completionItem.documentation = item.description; - return completionItem; - }); - } - return []; -} +import { IConfigurationService } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { CompletionSource } from './completionSource'; +import { ItemInfoSource } from './itemInfoSource'; export class PythonCompletionItemProvider implements vscode.CompletionItemProvider { - private jediProxyHandler: proxy.JediProxyHandler; + private completionSource: CompletionSource; + private configService: IConfigurationService; - public constructor(context: vscode.ExtensionContext) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, [], parseData); + constructor(jediFactory: JediFactory, serviceContainer: IServiceContainer) { + this.completionSource = new CompletionSource(jediFactory, serviceContainer, new ItemInfoSource(jediFactory)); + this.configService = serviceContainer.get(IConfigurationService); } - public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { - return new Promise((resolve, reject) => { - var filename = document.fileName; - if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return resolve([]); - } - if (position.character <= 0) { - return resolve([]); + @captureTelemetry(EventName.COMPLETION) + public async provideCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): Promise { + const items = await this.completionSource.getVsCodeCompletionItems(document, position, token); + if (this.configService.isTestExecution()) { + for (let i = 0; i < Math.min(3, items.length); i += 1) { + items[i] = await this.resolveCompletionItem(items[i], token); } - - var txt = document.getText(new vscode.Range(new vscode.Position(position.line, position.character - 1), position)); - var type = proxy.CommandType.Completions; - var columnIndex = position.character; - - var source = document.getText(); - var cmd: proxy.ICommand = { - command: type, - fileName: filename, - columnIndex: columnIndex, - lineIndex: position.line, - source: source - }; - - this.jediProxyHandler.sendCommand(cmd, resolve, token); - }); + } + return items; } -} \ No newline at end of file + public async resolveCompletionItem( + item: vscode.CompletionItem, + token: vscode.CancellationToken + ): Promise { + if (!item.documentation) { + const itemInfos = await this.completionSource.getDocumentation(item, token); + if (itemInfos && itemInfos.length > 0) { + item.documentation = itemInfos[0].tooltip; + } + } + return item; + } +} diff --git a/src/client/providers/completionSource.ts b/src/client/providers/completionSource.ts new file mode 100644 index 000000000000..e135ba18be7b --- /dev/null +++ b/src/client/providers/completionSource.ts @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as vscode from 'vscode'; +import { IConfigurationService } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { IItemInfoSource, LanguageItemInfo } from './itemInfoSource'; +import * as proxy from './jediProxy'; +import { isPositionInsideStringOrComment } from './providerUtilities'; + +class DocumentPosition { + constructor(public document: vscode.TextDocument, public position: vscode.Position) {} + + public static fromObject(item: object): DocumentPosition { + // tslint:disable-next-line:no-any + return (item as any)._documentPosition as DocumentPosition; + } + + public attachTo(item: object): void { + // tslint:disable-next-line:no-any + (item as any)._documentPosition = this; + } +} + +export class CompletionSource { + private jediFactory: JediFactory; + + constructor( + jediFactory: JediFactory, + private serviceContainer: IServiceContainer, + private itemInfoSource: IItemInfoSource + ) { + this.jediFactory = jediFactory; + } + + public async getVsCodeCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): Promise { + const result = await this.getCompletionResult(document, position, token); + if (result === undefined) { + return Promise.resolve([]); + } + return this.toVsCodeCompletions(new DocumentPosition(document, position), result, document.uri); + } + + public async getDocumentation( + completionItem: vscode.CompletionItem, + token: vscode.CancellationToken + ): Promise { + const documentPosition = DocumentPosition.fromObject(completionItem); + if (documentPosition === undefined) { + return; + } + + // Supply hover source with simulated document text where item in question was 'already typed'. + const document = documentPosition.document; + const position = documentPosition.position; + const wordRange = document.getWordRangeAtPosition(position); + + const leadingRange = + wordRange !== undefined + ? new vscode.Range(new vscode.Position(0, 0), wordRange.start) + : new vscode.Range(new vscode.Position(0, 0), position); + + const itemString = completionItem.label; + const sourceText = `${document.getText(leadingRange)}${itemString}`; + const range = new vscode.Range(leadingRange.end, leadingRange.end.translate(0, itemString.length)); + + return this.itemInfoSource.getItemInfoFromText(document.uri, document.fileName, range, sourceText, token); + } + + private async getCompletionResult( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): Promise { + if (position.character <= 0 || isPositionInsideStringOrComment(document, position)) { + return undefined; + } + + const type = proxy.CommandType.Completions; + const columnIndex = position.character; + + const source = document.getText(); + const cmd: proxy.ICommand = { + command: type, + fileName: document.fileName, + columnIndex: columnIndex, + lineIndex: position.line, + source: source + }; + + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); + } + + private toVsCodeCompletions( + documentPosition: DocumentPosition, + data: proxy.ICompletionResult, + resource: vscode.Uri + ): vscode.CompletionItem[] { + return data && data.items.length > 0 + ? data.items.map((item) => this.toVsCodeCompletion(documentPosition, item, resource)) + : []; + } + + private toVsCodeCompletion( + documentPosition: DocumentPosition, + item: proxy.IAutoCompleteItem, + resource: vscode.Uri + ): vscode.CompletionItem { + const completionItem = new vscode.CompletionItem(item.text); + completionItem.kind = item.type; + const configurationService = this.serviceContainer.get(IConfigurationService); + const pythonSettings = configurationService.getSettings(resource); + if ( + pythonSettings.autoComplete.addBrackets === true && + (item.kind === vscode.SymbolKind.Function || item.kind === vscode.SymbolKind.Method) + ) { + completionItem.insertText = new vscode.SnippetString(item.text) + .appendText('(') + .appendTabstop() + .appendText(')'); + } + // Ensure the built in members are at the bottom. + completionItem.sortText = + (completionItem.label.startsWith('__') ? 'z' : completionItem.label.startsWith('_') ? 'y' : '__') + + completionItem.label; + documentPosition.attachTo(completionItem); + return completionItem; + } +} diff --git a/src/client/providers/definitionProvider.ts b/src/client/providers/definitionProvider.ts index 06145589153a..9b3d8f1a7ce6 100644 --- a/src/client/providers/definitionProvider.ts +++ b/src/client/providers/definitionProvider.ts @@ -1,52 +1,59 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - 'use strict'; import * as vscode from 'vscode'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; import * as proxy from './jediProxy'; -import * as fs from 'fs'; - -function parseData(data: proxy.IDefinitionResult): vscode.Definition { - if (data) { - var definitionResource = vscode.Uri.file(data.definition.fileName); - var range = new vscode.Range(data.definition.lineIndex, data.definition.columnIndex, data.definition.lineIndex, data.definition.columnIndex); - - return new vscode.Location(definitionResource, range); - } - return null; -} export class PythonDefinitionProvider implements vscode.DefinitionProvider { - private jediProxyHandler: proxy.JediProxyHandler; - - public constructor(context: vscode.ExtensionContext) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, null, parseData); + public constructor(private jediFactory: JediFactory) {} + private static parseData(data: proxy.IDefinitionResult, possibleWord: string): vscode.Definition | undefined { + if (data && Array.isArray(data.definitions) && data.definitions.length > 0) { + const definitions = data.definitions.filter((d) => d.text === possibleWord); + const definition = definitions.length > 0 ? definitions[0] : data.definitions[data.definitions.length - 1]; + const definitionResource = vscode.Uri.file(definition.fileName); + const range = new vscode.Range( + definition.range.startLine, + definition.range.startColumn, + definition.range.endLine, + definition.range.endColumn + ); + return new vscode.Location(definitionResource, range); + } } - - public provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { - return new Promise((resolve, reject) => { - var filename = document.fileName; - if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return resolve(); - } - if (position.character <= 0) { - return resolve(); - } - - var source = document.getText(); - var range = document.getWordRangeAtPosition(position); - var columnIndex = range.isEmpty ? position.character : range.end.character; - var cmd: proxy.ICommand = { - command: proxy.CommandType.Definitions, - fileName: filename, - columnIndex: columnIndex, - lineIndex: position.line, - source: source - }; - - this.jediProxyHandler.sendCommand(cmd, resolve, token); - }); + @captureTelemetry(EventName.DEFINITION) + public async provideDefinition( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): Promise { + const filename = document.fileName; + if (document.lineAt(position.line).text.match(/^\s*\/\//)) { + return; + } + if (position.character <= 0) { + return; + } + + const range = document.getWordRangeAtPosition(position); + if (!range) { + return; + } + const columnIndex = range.isEmpty ? position.character : range.end.character; + const cmd: proxy.ICommand = { + command: proxy.CommandType.Definitions, + fileName: filename, + columnIndex: columnIndex, + lineIndex: position.line + }; + if (document.isDirty) { + cmd.source = document.getText(); + } + const possibleWord = document.getText(range); + const data = await this.jediFactory + .getJediProxyHandler(document.uri) + .sendCommand(cmd, token); + return data ? PythonDefinitionProvider.parseData(data, possibleWord) : undefined; } } diff --git a/src/client/providers/docStringFoldingProvider.ts b/src/client/providers/docStringFoldingProvider.ts new file mode 100644 index 000000000000..424b9b6a74ec --- /dev/null +++ b/src/client/providers/docStringFoldingProvider.ts @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { + CancellationToken, + FoldingContext, + FoldingRange, + FoldingRangeKind, + FoldingRangeProvider, + ProviderResult, + Range, + TextDocument +} from 'vscode'; +import { IterableTextRange } from '../language/iterableTextRange'; +import { IToken, TokenizerMode, TokenType } from '../language/types'; +import { getDocumentTokens } from './providerUtilities'; + +export class DocStringFoldingProvider implements FoldingRangeProvider { + public provideFoldingRanges( + document: TextDocument, + _context: FoldingContext, + _token: CancellationToken + ): ProviderResult { + return this.getFoldingRanges(document); + } + + private getFoldingRanges(document: TextDocument) { + const tokenCollection = getDocumentTokens( + document, + document.lineAt(document.lineCount - 1).range.end, + TokenizerMode.CommentsAndStrings + ); + const tokens = new IterableTextRange(tokenCollection); + + const docStringRanges: FoldingRange[] = []; + const commentRanges: FoldingRange[] = []; + + for (const token of tokens) { + const docstringRange = this.getDocStringFoldingRange(document, token); + if (docstringRange) { + docStringRanges.push(docstringRange); + continue; + } + + const commentRange = this.getSingleLineCommentRange(document, token); + if (commentRange) { + this.buildMultiLineCommentRange(commentRange, commentRanges); + } + } + + this.removeLastSingleLineComment(commentRanges); + return docStringRanges.concat(commentRanges); + } + private buildMultiLineCommentRange(commentRange: FoldingRange, commentRanges: FoldingRange[]) { + if (commentRanges.length === 0) { + commentRanges.push(commentRange); + return; + } + const previousComment = commentRanges[commentRanges.length - 1]; + if (previousComment.end + 1 === commentRange.start) { + previousComment.end = commentRange.end; + return; + } + if (previousComment.start === previousComment.end) { + commentRanges[commentRanges.length - 1] = commentRange; + return; + } + commentRanges.push(commentRange); + } + private removeLastSingleLineComment(commentRanges: FoldingRange[]) { + // Remove last comment folding range if its a single line entry. + if (commentRanges.length === 0) { + return; + } + const lastComment = commentRanges[commentRanges.length - 1]; + if (lastComment.start === lastComment.end) { + commentRanges.pop(); + } + } + private getDocStringFoldingRange(document: TextDocument, token: IToken) { + if (token.type !== TokenType.String) { + return; + } + + const startPosition = document.positionAt(token.start); + const endPosition = document.positionAt(token.end); + if (startPosition.line === endPosition.line) { + return; + } + + const startLine = document.lineAt(startPosition); + if (startLine.firstNonWhitespaceCharacterIndex !== startPosition.character) { + return; + } + const startIndex1 = startLine.text.indexOf("'''"); + const startIndex2 = startLine.text.indexOf('"""'); + if (startIndex1 !== startPosition.character && startIndex2 !== startPosition.character) { + return; + } + + const range = new Range(startPosition, endPosition); + + return new FoldingRange(range.start.line, range.end.line); + } + private getSingleLineCommentRange(document: TextDocument, token: IToken) { + if (token.type !== TokenType.Comment) { + return; + } + + const startPosition = document.positionAt(token.start); + const endPosition = document.positionAt(token.end); + if (startPosition.line !== endPosition.line) { + return; + } + if (document.lineAt(startPosition).firstNonWhitespaceCharacterIndex !== startPosition.character) { + return; + } + + const range = new Range(startPosition, endPosition); + return new FoldingRange(range.start.line, range.end.line, FoldingRangeKind.Comment); + } +} diff --git a/src/client/providers/formatOnSaveProvider.ts b/src/client/providers/formatOnSaveProvider.ts deleted file mode 100644 index 69e1277f240e..000000000000 --- a/src/client/providers/formatOnSaveProvider.ts +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -//Solution for auto-formatting borrowed from the "go" language VSCode extension. - -import * as vscode from 'vscode'; -import {BaseFormatter} from './../formatters/baseFormatter'; -import {YapfFormatter} from './../formatters/yapfFormatter'; -import {AutoPep8Formatter} from './../formatters/autoPep8Formatter'; -import * as settings from './../common/configSettings'; - -export function activateFormatOnSaveProvider(languageFilter: vscode.DocumentFilter, context: vscode.ExtensionContext, settings: settings.IPythonSettings, outputChannel: vscode.OutputChannel) { - let rootDir = context.asAbsolutePath("."); - let formatters = new Map(); - let pythonSettings = settings; - - var yapfFormatter = new YapfFormatter(settings, outputChannel); - var autoPep8 = new AutoPep8Formatter(settings, outputChannel); - - formatters.set(yapfFormatter.Id, yapfFormatter); - formatters.set(autoPep8.Id, autoPep8); - - // TODO: This is really ugly. I'm not sure we can do better until - // Code supports a pre-save event where we can do the formatting before - // the file is written to disk. - let ignoreNextSave = new WeakSet(); - - var subscription = vscode.workspace.onDidSaveTextDocument(document => { - if (document.languageId !== languageFilter.language || ignoreNextSave.has(document)) { - return; - } - let textEditor = vscode.window.activeTextEditor; - if (pythonSettings.formatting.formatOnSave && textEditor.document === document) { - var formatter = formatters.get(pythonSettings.formatting.provider); - formatter.formatDocument(document, null, null).then(edits => { - return textEditor.edit(editBuilder => { - edits.forEach(edit => editBuilder.replace(edit.range, edit.newText)); - }); - }).then(applied => { - ignoreNextSave.add(document); - return document.save(); - }).then(() => { - ignoreNextSave.delete(document); - }, () => { - // Catch any errors and ignore so that we still trigger - // the file save. - }); - } - }, null, null); - - context.subscriptions.push(subscription); -} diff --git a/src/client/providers/formatProvider.ts b/src/client/providers/formatProvider.ts index 8cf5c2c36f3e..cd4d8f3956c0 100644 --- a/src/client/providers/formatProvider.ts +++ b/src/client/providers/formatProvider.ts @@ -1,34 +1,125 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. import * as vscode from 'vscode'; -import * as path from 'path'; -import {BaseFormatter} from './../formatters/baseFormatter'; -import {YapfFormatter} from './../formatters/yapfFormatter'; -import {AutoPep8Formatter} from './../formatters/autoPep8Formatter'; -import * as settings from './../common/configSettings'; - -export class PythonFormattingEditProvider implements vscode.DocumentFormattingEditProvider { - private rootDir: string; - private settings: settings.IPythonSettings; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { IConfigurationService } from '../common/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { AutoPep8Formatter } from './../formatters/autoPep8Formatter'; +import { BaseFormatter } from './../formatters/baseFormatter'; +import { BlackFormatter } from './../formatters/blackFormatter'; +import { DummyFormatter } from './../formatters/dummyFormatter'; +import { YapfFormatter } from './../formatters/yapfFormatter'; + +export class PythonFormattingEditProvider + implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider, vscode.Disposable { + private readonly config: IConfigurationService; + private readonly workspace: IWorkspaceService; + private readonly documentManager: IDocumentManager; + private readonly commands: ICommandManager; private formatters = new Map(); + private disposables: vscode.Disposable[] = []; - public constructor(context: vscode.ExtensionContext, settings: settings.IPythonSettings, outputChannel: vscode.OutputChannel) { - this.rootDir = context.asAbsolutePath("."); - this.settings = settings; - var yapfFormatter = new YapfFormatter(settings, outputChannel); - var autoPep8 = new AutoPep8Formatter(settings, outputChannel); + // Workaround for https://github.com/Microsoft/vscode/issues/41194 + private documentVersionBeforeFormatting = -1; + private formatterMadeChanges = false; + private saving = false; + + public constructor(_context: vscode.ExtensionContext, serviceContainer: IServiceContainer) { + const yapfFormatter = new YapfFormatter(serviceContainer); + const autoPep8 = new AutoPep8Formatter(serviceContainer); + const black = new BlackFormatter(serviceContainer); + const dummy = new DummyFormatter(serviceContainer); this.formatters.set(yapfFormatter.Id, yapfFormatter); + this.formatters.set(black.Id, black); this.formatters.set(autoPep8.Id, autoPep8); + this.formatters.set(dummy.Id, dummy); + + this.commands = serviceContainer.get(ICommandManager); + this.workspace = serviceContainer.get(IWorkspaceService); + this.documentManager = serviceContainer.get(IDocumentManager); + this.config = serviceContainer.get(IConfigurationService); + const interpreterService = serviceContainer.get(IInterpreterService); + this.disposables.push( + this.documentManager.onDidSaveTextDocument(async (document) => this.onSaveDocument(document)) + ); + this.disposables.push( + interpreterService.onDidChangeInterpreter(async () => { + if (this.documentManager.activeTextEditor) { + return this.onSaveDocument(this.documentManager.activeTextEditor.document); + } + }) + ); } - public provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): Thenable { - var formatter = this.formatters.get(this.settings.formatting.provider); + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + + public provideDocumentFormattingEdits( + document: vscode.TextDocument, + options: vscode.FormattingOptions, + token: vscode.CancellationToken + ): Promise { + return this.provideDocumentRangeFormattingEdits(document, undefined, options, token); + } + + public async provideDocumentRangeFormattingEdits( + document: vscode.TextDocument, + range: vscode.Range | undefined, + options: vscode.FormattingOptions, + token: vscode.CancellationToken + ): Promise { + // Workaround for https://github.com/Microsoft/vscode/issues/41194 + // VSC rejects 'format on save' promise in 750 ms. Python formatting may take quite a bit longer. + // Workaround is to resolve promise to nothing here, then execute format document and force new save. + // However, we need to know if this is 'format document' or formatting on save. + + if (this.saving) { + // We are saving after formatting (see onSaveDocument below) + // so we do not want to format again. + return []; + } + + // Remember content before formatting so we can detect if + // formatting edits have been really applied + const editorConfig = this.workspace.getConfiguration('editor', document.uri); + if (editorConfig.get('formatOnSave') === true) { + this.documentVersionBeforeFormatting = document.version; + } + + const settings = this.config.getSettings(document.uri); + const formatter = this.formatters.get(settings.formatting.provider)!; + const edits = await formatter.formatDocument(document, options, token, range); + + this.formatterMadeChanges = edits.length > 0; + return edits; + } - var fileDir = path.dirname(document.uri.fsPath); - return formatter.formatDocument(document, options, token); + private async onSaveDocument(document: vscode.TextDocument): Promise { + // Promise was rejected = formatting took too long. + // Don't format inside the event handler, do it on timeout + setTimeout(() => { + try { + if ( + this.formatterMadeChanges && + !document.isDirty && + document.version === this.documentVersionBeforeFormatting + ) { + // Formatter changes were not actually applied due to the timeout on save. + // Force formatting now and then save the document. + this.commands.executeCommand('editor.action.formatDocument').then(async () => { + this.saving = true; + await document.save(); + this.saving = false; + }); + } + } finally { + this.documentVersionBeforeFormatting = -1; + this.saving = false; + this.formatterMadeChanges = false; + } + }, 50); } } diff --git a/src/client/providers/hoverProvider.ts b/src/client/providers/hoverProvider.ts index d8e821e0bd5a..13a7ed7e205f 100644 --- a/src/client/providers/hoverProvider.ts +++ b/src/client/providers/hoverProvider.ts @@ -1,54 +1,27 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - 'use strict'; import * as vscode from 'vscode'; -import * as proxy from './jediProxy'; - -function parseData(data: proxy.ICompletionResult) { - if (data && data.items.length > 0) { - var definition = data.items[0]; - - var txt = definition.description || definition.text; - return new vscode.Hover({ language: "python", value: txt }); - } - return null; -} +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { ItemInfoSource } from './itemInfoSource'; export class PythonHoverProvider implements vscode.HoverProvider { - private jediProxyHandler: proxy.JediProxyHandler; + private itemInfoSource: ItemInfoSource; - public constructor(context: vscode.ExtensionContext) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, null, parseData); + constructor(jediFactory: JediFactory) { + this.itemInfoSource = new ItemInfoSource(jediFactory); } - public provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { - return new Promise((resolve, reject) => { - var filename = document.fileName; - if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return resolve(); - } - if (position.character <= 0) { - return resolve(); - } - - var source = document.getText(); - var range = document.getWordRangeAtPosition(position); - if (range == undefined || range.isEmpty) { - return resolve(); - } - var columnIndex = range.end.character; - var cmd: proxy.ICommand = { - command: proxy.CommandType.Completions, - fileName: filename, - columnIndex: columnIndex, - lineIndex: position.line, - source: source - }; - - this.jediProxyHandler.sendCommand(cmd, resolve, token); - }); + @captureTelemetry(EventName.HOVER_DEFINITION) + public async provideHover( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): Promise { + const itemInfos = await this.itemInfoSource.getItemInfoFromDocument(document, position, token); + if (itemInfos) { + return new vscode.Hover(itemInfos.map((item) => item.tooltip)); + } } } diff --git a/src/client/providers/importSortProvider.ts b/src/client/providers/importSortProvider.ts index 01fccb1e0e91..c55b20fcc49f 100644 --- a/src/client/providers/importSortProvider.ts +++ b/src/client/providers/importSortProvider.ts @@ -1,64 +1,248 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ +import { inject, injectable } from 'inversify'; +import { EOL } from 'os'; +import * as path from 'path'; +import { CancellationToken, CancellationTokenSource, TextDocument, Uri, WorkspaceEdit } from 'vscode'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; +import { Commands, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import { traceError } from '../common/logger'; +import * as internalScripts from '../common/process/internal/scripts'; +import { IProcessServiceFactory, IPythonExecutionFactory, ObservableExecutionResult } from '../common/process/types'; +import { + IConfigurationService, + IDisposableRegistry, + IEditorUtils, + IOutputChannel, + IPersistentStateFactory +} from '../common/types'; +import { createDeferred, createDeferredFromPromise, Deferred } from '../common/utils/async'; +import { Common, Diagnostics } from '../common/utils/localize'; +import { noop } from '../common/utils/misc'; +import { IServiceContainer } from '../ioc/types'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { ISortImportsEditingProvider } from './types'; -'use strict'; +const doNotDisplayPromptStateKey = 'ISORT5_UPGRADE_WARNING_KEY'; -import vscode = require('vscode'); -import * as path from 'path'; -import * as fs from 'fs'; -import * as child_process from 'child_process'; - -export class PythonImportSortProvider { - public sortImports(extensionDir: string, document: vscode.TextDocument): Thenable { - return new Promise((resolve, reject) => { - var filePath = document.uri.fsPath; - var importScript = path.join(extensionDir, "pythonFiles", "sortImports.py"); - if (!fs.existsSync(filePath)) { - vscode.window.showErrorMessage(`File ${filePath} does not exist`) - return resolve([]); +@injectable() +export class SortImportsEditingProvider implements ISortImportsEditingProvider { + private readonly isortPromises = new Map< + string, + { deferred: Deferred; tokenSource: CancellationTokenSource } + >(); + private readonly processServiceFactory: IProcessServiceFactory; + private readonly pythonExecutionFactory: IPythonExecutionFactory; + private readonly shell: IApplicationShell; + private readonly persistentStateFactory: IPersistentStateFactory; + private readonly documentManager: IDocumentManager; + private readonly configurationService: IConfigurationService; + private readonly editorUtils: IEditorUtils; + private readonly output: IOutputChannel; + + public constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.shell = serviceContainer.get(IApplicationShell); + this.documentManager = serviceContainer.get(IDocumentManager); + this.configurationService = serviceContainer.get(IConfigurationService); + this.pythonExecutionFactory = serviceContainer.get(IPythonExecutionFactory); + this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); + this.editorUtils = serviceContainer.get(IEditorUtils); + this.output = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.persistentStateFactory = serviceContainer.get(IPersistentStateFactory); + } + + @captureTelemetry(EventName.FORMAT_SORT_IMPORTS) + public async provideDocumentSortImportsEdits(uri: Uri): Promise { + if (this.isortPromises.has(uri.fsPath)) { + const isortPromise = this.isortPromises.get(uri.fsPath)!; + if (!isortPromise.deferred.completed) { + // Cancelling the token will kill the previous isort process & discard its result. + isortPromise.tokenSource.cancel(); } + } + const tokenSource = new CancellationTokenSource(); + const promise = this._provideDocumentSortImportsEdits(uri, tokenSource.token); + const deferred = createDeferredFromPromise(promise); + this.isortPromises.set(uri.fsPath, { deferred, tokenSource }); + // If token has been cancelled discard the result. + return promise.then((edit) => (tokenSource.token.isCancellationRequested ? undefined : edit)); + } - var ext = path.extname(filePath); - var tmp = require("tmp"); - tmp.file({ postfix: ext }, function _tempFileCreated(err, tmpFilePath, fd) { - if (err) { - reject(err); - return; - } - var documentText = document.getText(); - fs.writeFile(tmpFilePath, documentText, ex=> { - if (ex) { - vscode.window.showErrorMessage(`Failed to create a temporary file, ${ex.message}`); - return; - } + public async _provideDocumentSortImportsEdits( + uri: Uri, + token?: CancellationToken + ): Promise { + const document = await this.documentManager.openTextDocument(uri); + if (!document) { + return; + } + if (document.lineCount <= 1) { + return; + } + + const execIsort = await this.getExecIsort(document, uri, token); + if (token && token.isCancellationRequested) { + return; + } + const diffPatch = await execIsort(document.getText()); - child_process.exec(`python "${importScript}" "${tmpFilePath}"`, (error, stdout, stderr) => { - if (error || stderr) { - vscode.window.showErrorMessage(`File ${filePath} does not exist`) - return resolve([]); - } - - fs.readFile(tmpFilePath, (ex, data) => { - if (ex) { - vscode.window.showErrorMessage(`Failed to create a temporary file for sorting, ${ex.message}`); - return; - } - - var formattedText = data.toString('utf-8'); - //Nothing to do - if (document.getText() === formattedText) { - return resolve([]); - } - - var range = new vscode.Range(document.lineAt(0).range.start, document.lineAt(document.lineCount - 1).range.end) - var txtEdit = new vscode.TextEdit(range, formattedText); - resolve([txtEdit]); - }); - }); - }); - }); + return diffPatch + ? this.editorUtils.getWorkspaceEditsFromPatch(document.getText(), diffPatch, document.uri) + : undefined; + } + + public registerCommands() { + const cmdManager = this.serviceContainer.get(ICommandManager); + const disposable = cmdManager.registerCommand(Commands.Sort_Imports, this.sortImports, this); + this.serviceContainer.get(IDisposableRegistry).push(disposable); + } + + public async sortImports(uri?: Uri): Promise { + if (!uri) { + const activeEditor = this.documentManager.activeTextEditor; + if (!activeEditor || activeEditor.document.languageId !== PYTHON_LANGUAGE) { + this.shell.showErrorMessage('Please open a Python file to sort the imports.').then(noop, noop); + return; + } + uri = activeEditor.document.uri; + } + const document = await this.documentManager.openTextDocument(uri); + if (document.lineCount <= 1) { + return; + } + + // Hack, if the document doesn't contain an empty line at the end, then add it + // Else the library strips off the last line + const lastLine = document.lineAt(document.lineCount - 1); + if (lastLine.text.trim().length > 0) { + const edit = new WorkspaceEdit(); + edit.insert(uri, lastLine.range.end, EOL); + await this.documentManager.applyEdit(edit); + } + + try { + const changes = await this.provideDocumentSortImportsEdits(uri); + if (!changes || changes.entries().length === 0) { + return; + } + await this.documentManager.applyEdit(changes); + } catch (error) { + const message = typeof error === 'string' ? error : error.message ? error.message : error; + const outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + outputChannel.appendLine(error); + traceError(`Failed to format imports for '${uri.fsPath}'.`, error); + this.shell.showErrorMessage(message).then(noop, noop); + } + } + + public async _showWarningAndOptionallyShowOutput() { + const neverShowAgain = this.persistentStateFactory.createGlobalPersistentState( + doNotDisplayPromptStateKey, + false + ); + if (neverShowAgain.value) { + return; + } + const selection = await this.shell.showWarningMessage( + Diagnostics.checkIsort5UpgradeGuide(), + Common.openOutputPanel(), + Common.doNotShowAgain() + ); + if (selection === Common.openOutputPanel()) { + this.output.show(true); + } else if (selection === Common.doNotShowAgain()) { + await neverShowAgain.updateValue(true); + } + } + + private async getExecIsort( + document: TextDocument, + uri: Uri, + token?: CancellationToken + ): Promise<(documentText: string) => Promise> { + const settings = this.configurationService.getSettings(uri); + const _isort = settings.sortImports.path; + const isort = typeof _isort === 'string' && _isort.length > 0 ? _isort : undefined; + const isortArgs = settings.sortImports.args; + + // We pass the content of the file to be sorted via stdin. This avoids + // saving the file (as well as a potential temporary file), but does + // mean that we need another way to tell `isort` where to look for + // configuration. We do that by setting the working directory to the + // directory which contains the file. + const filename = '-'; + + const spawnOptions = { + token, + cwd: path.dirname(uri.fsPath) + }; + + if (isort) { + const procService = await this.processServiceFactory.create(document.uri); + // Use isort directly instead of the internal script. + return async (documentText: string) => { + const args = getIsortArgs(filename, isortArgs); + const result = procService.execObservable(isort, args, spawnOptions); + return this.communicateWithIsortProcess(result, documentText); + }; + } else { + const procService = await this.pythonExecutionFactory.create({ resource: document.uri }); + return async (documentText: string) => { + const [args, parse] = internalScripts.sortImports(filename, isortArgs); + const result = procService.execObservable(args, spawnOptions); + return parse(await this.communicateWithIsortProcess(result, documentText)); + }; + } + } + + private async communicateWithIsortProcess( + observableResult: ObservableExecutionResult, + inputText: string + ): Promise { + // Configure our listening to the output from isort ... + let outputBuffer = ''; + let isAnyErrorRelatedToUpgradeGuide = false; + const isortOutput = createDeferred(); + observableResult.out.subscribe({ + next: (output) => { + if (output.source === 'stdout') { + outputBuffer += output.out; + } else { + // All the W0500 warning codes point to isort5 upgrade guide: https://pycqa.github.io/isort/docs/warning_and_error_codes/W0500/ + // Do not throw error on these types of stdErrors + isAnyErrorRelatedToUpgradeGuide = isAnyErrorRelatedToUpgradeGuide || output.out.includes('W050'); + traceError(output.out); + if (!output.out.includes('W050')) { + isortOutput.reject(output.out); + } + } + }, + complete: () => { + isortOutput.resolve(outputBuffer); + } }); + + // ... then send isort the document content ... + observableResult.proc?.stdin.write(inputText); + observableResult.proc?.stdin.end(); + + // .. and finally wait for isort to do its thing + await isortOutput.promise; + + if (isAnyErrorRelatedToUpgradeGuide) { + this._showWarningAndOptionallyShowOutput().ignoreErrors(); + } + return outputBuffer; + } +} + +function getIsortArgs(filename: string, extraArgs?: string[]): string[] { + // We could just adapt internalScripts.sortImports(). However, + // the following is simpler and the alternative doesn't offer + // any signficant benefit. + const args = [filename, '--diff']; + if (extraArgs) { + args.push(...extraArgs); } + return args; } diff --git a/src/client/providers/itemInfoSource.ts b/src/client/providers/itemInfoSource.ts new file mode 100644 index 000000000000..b7a38a65a884 --- /dev/null +++ b/src/client/providers/itemInfoSource.ts @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { EOL } from 'os'; +import * as vscode from 'vscode'; +import { RestTextConverter } from '../common/markdown/restTextConverter'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import * as proxy from './jediProxy'; + +export class LanguageItemInfo { + constructor( + public tooltip: vscode.MarkdownString, + public detail: string, + public signature: vscode.MarkdownString + ) {} +} + +export interface IItemInfoSource { + getItemInfoFromText( + documentUri: vscode.Uri, + fileName: string, + range: vscode.Range, + sourceText: string, + token: vscode.CancellationToken + ): Promise; + getItemInfoFromDocument( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): Promise; +} + +export class ItemInfoSource implements IItemInfoSource { + private textConverter = new RestTextConverter(); + constructor(private jediFactory: JediFactory) {} + + public async getItemInfoFromText( + documentUri: vscode.Uri, + fileName: string, + range: vscode.Range, + sourceText: string, + token: vscode.CancellationToken + ): Promise { + const result = await this.getHoverResultFromTextRange(documentUri, fileName, range, sourceText, token); + if (!result || !result.items.length) { + return; + } + return this.getItemInfoFromHoverResult(result, ''); + } + + public async getItemInfoFromDocument( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): Promise { + const range = document.getWordRangeAtPosition(position); + if (!range || range.isEmpty) { + return; + } + const result = await this.getHoverResultFromDocument(document, position, token); + if (!result || !result.items.length) { + return; + } + const word = document.getText(range); + return this.getItemInfoFromHoverResult(result, word); + } + + private async getHoverResultFromDocument( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken + ): Promise { + if (position.character <= 0 || document.lineAt(position.line).text.match(/^\s*\/\//)) { + return; + } + const range = document.getWordRangeAtPosition(position); + if (!range || range.isEmpty) { + return; + } + return this.getHoverResultFromDocumentRange(document, range, token); + } + + private async getHoverResultFromDocumentRange( + document: vscode.TextDocument, + range: vscode.Range, + token: vscode.CancellationToken + ): Promise { + const cmd: proxy.ICommand = { + command: proxy.CommandType.Hover, + fileName: document.fileName, + columnIndex: range.end.character, + lineIndex: range.end.line + }; + if (document.isDirty) { + cmd.source = document.getText(); + } + return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); + } + + private async getHoverResultFromTextRange( + documentUri: vscode.Uri, + fileName: string, + range: vscode.Range, + sourceText: string, + token: vscode.CancellationToken + ): Promise { + const cmd: proxy.ICommand = { + command: proxy.CommandType.Hover, + fileName: fileName, + columnIndex: range.end.character, + lineIndex: range.end.line, + source: sourceText + }; + return this.jediFactory.getJediProxyHandler(documentUri).sendCommand(cmd, token); + } + + private getItemInfoFromHoverResult(data: proxy.IHoverResult, currentWord: string): LanguageItemInfo[] { + const infos: LanguageItemInfo[] = []; + + data.items.forEach((item) => { + const signature = this.getSignature(item, currentWord); + let tooltip = new vscode.MarkdownString(); + if (item.docstring) { + let lines = item.docstring.split(/\r?\n/); + + // If the docstring starts with the signature, then remove those lines from the docstring. + if (lines.length > 0 && item.signature.indexOf(lines[0]) === 0) { + lines.shift(); + const endIndex = lines.findIndex((line) => item.signature.endsWith(line)); + if (endIndex >= 0) { + lines = lines.filter((_line, index) => index > endIndex); + } + } + if ( + lines.length > 0 && + currentWord.length > 0 && + item.signature.startsWith(currentWord) && + lines[0].startsWith(currentWord) && + lines[0].endsWith(')') + ) { + lines.shift(); + } + + if (signature.length > 0) { + tooltip = tooltip.appendMarkdown(['```python', signature, '```', ''].join(EOL)); + } + + const description = this.textConverter.toMarkdown(lines.join(EOL)); + tooltip = tooltip.appendMarkdown(description); + + infos.push(new LanguageItemInfo(tooltip, item.description, new vscode.MarkdownString(signature))); + return; + } + + if (item.description) { + if (signature.length > 0) { + tooltip.appendMarkdown(['```python', signature, '```', ''].join(EOL)); + } + const description = this.textConverter.toMarkdown(item.description); + tooltip.appendMarkdown(description); + infos.push(new LanguageItemInfo(tooltip, item.description, new vscode.MarkdownString(signature))); + return; + } + + if (item.text) { + // Most probably variable type + const code = currentWord && currentWord.length > 0 ? `${currentWord}: ${item.text}` : item.text; + tooltip.appendMarkdown(['```python', code, '```', ''].join(EOL)); + infos.push(new LanguageItemInfo(tooltip, '', new vscode.MarkdownString())); + } + }); + return infos; + } + + private getSignature(item: proxy.IHoverItem, currentWord: string): string { + let { signature } = item; + switch (item.kind) { + case vscode.SymbolKind.Constructor: + case vscode.SymbolKind.Function: + case vscode.SymbolKind.Method: { + signature = `def ${signature}`; + break; + } + case vscode.SymbolKind.Class: { + signature = `class ${signature}`; + break; + } + case vscode.SymbolKind.Module: { + if (signature.length > 0) { + signature = `module ${signature}`; + } + break; + } + default: { + signature = typeof item.text === 'string' && item.text.length > 0 ? item.text : currentWord; + } + } + return signature; + } +} diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts index c840da9e17f5..0841bc6fbf47 100644 --- a/src/client/providers/jediProxy.ts +++ b/src/client/providers/jediProxy.ts @@ -1,493 +1,918 @@ -'use strict'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. -import * as fs from 'fs'; -import * as os from 'os'; -import * as child_process from 'child_process'; -import * as vscode from 'vscode'; +// tslint:disable:no-var-requires no-require-imports no-any +import { ChildProcess } from 'child_process'; import * as path from 'path'; -import * as settings from './../common/configSettings'; -import * as logger from './../common/logger'; - -var proc: child_process.ChildProcess; -var pythonSettings = new settings.PythonSettings(); - -const pythonVSCodeTypeMappings = new Map(); -var mappings = { - "none": vscode.CompletionItemKind.Value, - "type": vscode.CompletionItemKind.Class, - "tuple": vscode.CompletionItemKind.Class, - "dict": vscode.CompletionItemKind.Class, - "dictionary": vscode.CompletionItemKind.Class, - "function": vscode.CompletionItemKind.Function, - "lambda": vscode.CompletionItemKind.Function, - "generator": vscode.CompletionItemKind.Function, - "class": vscode.CompletionItemKind.Class, - "instance": vscode.CompletionItemKind.Reference, - "method": vscode.CompletionItemKind.Method, - "builtin": vscode.CompletionItemKind.Class, - "builtinfunction": vscode.CompletionItemKind.Function, - "module": vscode.CompletionItemKind.Module, - "file": vscode.CompletionItemKind.File, - "xrange": vscode.CompletionItemKind.Class, - "slice": vscode.CompletionItemKind.Class, - "traceback": vscode.CompletionItemKind.Class, - "frame": vscode.CompletionItemKind.Class, - "buffer": vscode.CompletionItemKind.Class, - "dictproxy": vscode.CompletionItemKind.Class, - "funcdef": vscode.CompletionItemKind.Function, - "property": vscode.CompletionItemKind.Property, - "import": vscode.CompletionItemKind.Module, - "keyword": vscode.CompletionItemKind.Keyword, - "constant": vscode.CompletionItemKind.Variable, - "variable": vscode.CompletionItemKind.Variable, - "value": vscode.CompletionItemKind.Value, - "param": vscode.CompletionItemKind.Variable, - "statement": vscode.CompletionItemKind.Keyword -}; - -Object.keys(mappings).forEach(key=> { - pythonVSCodeTypeMappings.set(key, mappings[key]); -}); - -const pythonVSCodeSymbolMappings = new Map(); -var symbolMappings = { - "none": vscode.SymbolKind.Variable, - "type": vscode.SymbolKind.Class, - "tuple": vscode.SymbolKind.Class, - "dict": vscode.SymbolKind.Class, - "dictionary": vscode.SymbolKind.Class, - "function": vscode.SymbolKind.Function, - "lambda": vscode.SymbolKind.Function, - "generator": vscode.SymbolKind.Function, - "class": vscode.SymbolKind.Class, - "instance": vscode.SymbolKind.Class, - "method": vscode.SymbolKind.Method, - "builtin": vscode.SymbolKind.Class, - "builtinfunction": vscode.SymbolKind.Function, - "module": vscode.SymbolKind.Module, - "file": vscode.SymbolKind.File, - "xrange": vscode.SymbolKind.Array, - "slice": vscode.SymbolKind.Class, - "traceback": vscode.SymbolKind.Class, - "frame": vscode.SymbolKind.Class, - "buffer": vscode.SymbolKind.Array, - "dictproxy": vscode.SymbolKind.Class, - "funcdef": vscode.SymbolKind.Function, - "property": vscode.SymbolKind.Property, - "import": vscode.SymbolKind.Module, - "keyword": vscode.SymbolKind.Variable, - "constant": vscode.SymbolKind.Constant, - "variable": vscode.SymbolKind.Variable, - "value": vscode.SymbolKind.Variable, - "param": vscode.SymbolKind.Variable, - "statement": vscode.SymbolKind.Variable, - "boolean": vscode.SymbolKind.Boolean, - "int": vscode.SymbolKind.Number, - "longlean": vscode.SymbolKind.Number, - "float": vscode.SymbolKind.Number, - "complex": vscode.SymbolKind.Number, - "string": vscode.SymbolKind.String, - "unicode": vscode.SymbolKind.String, - "list": vscode.SymbolKind.Array -}; - -Object.keys(symbolMappings).forEach(key=> { - pythonVSCodeSymbolMappings.set(key, symbolMappings[key]); -}); - -function getMappedVSCodeType(pythonType: string): vscode.CompletionItemKind { +// @ts-ignore +import * as pidusage from 'pidusage'; +import { CancellationToken, CancellationTokenSource, CompletionItemKind, Disposable, SymbolKind, Uri } from 'vscode'; +import '../common/extensions'; +import { IS_WINDOWS } from '../common/platform/constants'; +import { IFileSystem } from '../common/platform/types'; +import * as internalPython from '../common/process/internal/python'; +import * as internalScripts from '../common/process/internal/scripts'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { IConfigurationService, IPythonSettings } from '../common/types'; +import { createDeferred, Deferred } from '../common/utils/async'; +import { swallowExceptions } from '../common/utils/decorators'; +import { StopWatch } from '../common/utils/stopWatch'; +import { IEnvironmentVariablesProvider } from '../common/variables/types'; +import { IServiceContainer } from '../ioc/types'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { traceError, traceWarning } from './../common/logger'; + +const pythonVSCodeTypeMappings = new Map(); +pythonVSCodeTypeMappings.set('none', CompletionItemKind.Value); +pythonVSCodeTypeMappings.set('type', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('tuple', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('dict', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('dictionary', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('function', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('lambda', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('generator', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('class', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('instance', CompletionItemKind.Reference); +pythonVSCodeTypeMappings.set('method', CompletionItemKind.Method); +pythonVSCodeTypeMappings.set('builtin', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('builtinfunction', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('module', CompletionItemKind.Module); +pythonVSCodeTypeMappings.set('file', CompletionItemKind.File); +pythonVSCodeTypeMappings.set('xrange', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('slice', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('traceback', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('frame', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('buffer', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('dictproxy', CompletionItemKind.Class); +pythonVSCodeTypeMappings.set('funcdef', CompletionItemKind.Function); +pythonVSCodeTypeMappings.set('property', CompletionItemKind.Property); +pythonVSCodeTypeMappings.set('import', CompletionItemKind.Module); +pythonVSCodeTypeMappings.set('keyword', CompletionItemKind.Keyword); +pythonVSCodeTypeMappings.set('constant', CompletionItemKind.Variable); +pythonVSCodeTypeMappings.set('variable', CompletionItemKind.Variable); +pythonVSCodeTypeMappings.set('value', CompletionItemKind.Value); +pythonVSCodeTypeMappings.set('param', CompletionItemKind.Variable); +pythonVSCodeTypeMappings.set('statement', CompletionItemKind.Keyword); + +const pythonVSCodeSymbolMappings = new Map(); +pythonVSCodeSymbolMappings.set('none', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('type', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('tuple', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('dict', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('dictionary', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('function', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('lambda', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('generator', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('class', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('instance', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('method', SymbolKind.Method); +pythonVSCodeSymbolMappings.set('builtin', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('builtinfunction', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('module', SymbolKind.Module); +pythonVSCodeSymbolMappings.set('file', SymbolKind.File); +pythonVSCodeSymbolMappings.set('xrange', SymbolKind.Array); +pythonVSCodeSymbolMappings.set('slice', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('traceback', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('frame', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('buffer', SymbolKind.Array); +pythonVSCodeSymbolMappings.set('dictproxy', SymbolKind.Class); +pythonVSCodeSymbolMappings.set('funcdef', SymbolKind.Function); +pythonVSCodeSymbolMappings.set('property', SymbolKind.Property); +pythonVSCodeSymbolMappings.set('import', SymbolKind.Module); +pythonVSCodeSymbolMappings.set('keyword', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('constant', SymbolKind.Constant); +pythonVSCodeSymbolMappings.set('variable', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('value', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('param', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('statement', SymbolKind.Variable); +pythonVSCodeSymbolMappings.set('boolean', SymbolKind.Boolean); +pythonVSCodeSymbolMappings.set('int', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('longlean', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('float', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('complex', SymbolKind.Number); +pythonVSCodeSymbolMappings.set('string', SymbolKind.String); +pythonVSCodeSymbolMappings.set('unicode', SymbolKind.String); +pythonVSCodeSymbolMappings.set('list', SymbolKind.Array); + +function getMappedVSCodeType(pythonType: string): CompletionItemKind { if (pythonVSCodeTypeMappings.has(pythonType)) { - return pythonVSCodeTypeMappings.get(pythonType); - } - else { - return vscode.CompletionItemKind.Keyword; + const value = pythonVSCodeTypeMappings.get(pythonType); + if (value) { + return value; + } } + return CompletionItemKind.Keyword; } -function getMappedVSCodeSymbol(pythonType: string): vscode.SymbolKind { - if (pythonVSCodeTypeMappings.has(pythonType)) { - return pythonVSCodeSymbolMappings.get(pythonType); - } - else { - return vscode.SymbolKind.Variable; +function getMappedVSCodeSymbol(pythonType: string): SymbolKind { + if (pythonVSCodeSymbolMappings.has(pythonType)) { + const value = pythonVSCodeSymbolMappings.get(pythonType); + if (value) { + return value; + } } + return SymbolKind.Variable; } export enum CommandType { Arguments, Completions, + Hover, Usages, Definitions, Symbols } -var commandNames = new Map(); -commandNames.set(CommandType.Arguments, "arguments"); -commandNames.set(CommandType.Completions, "completions"); -commandNames.set(CommandType.Definitions, "definitions"); -commandNames.set(CommandType.Usages, "usages"); -commandNames.set(CommandType.Symbols, "names"); - -export class JediProxy extends vscode.Disposable { - public constructor(context: vscode.ExtensionContext) { - super(killProcess); +const commandNames = new Map(); +commandNames.set(CommandType.Arguments, 'arguments'); +commandNames.set(CommandType.Completions, 'completions'); +commandNames.set(CommandType.Definitions, 'definitions'); +commandNames.set(CommandType.Hover, 'tooltip'); +commandNames.set(CommandType.Usages, 'usages'); +commandNames.set(CommandType.Symbols, 'names'); + +type JediProxyConfig = { + extraPaths: string[]; + useSnippets: boolean; + caseInsensitiveCompletion: boolean; + showDescriptions: boolean; + fuzzyMatcher: boolean; +}; - context.subscriptions.push(this); - initialize(context.asAbsolutePath(".")); - } +type JediProxyPayload = { + id: number; + prefix: string; + lookup?: string; + path: string; + source?: string; + line?: number; + column?: number; + config: JediProxyConfig; +}; +export class JediProxy implements Disposable { + private proc?: ChildProcess; + private pythonSettings: IPythonSettings; private cmdId: number = 0; + private lastKnownPythonInterpreter: string; + private previousData = ''; + private commands = new Map>(); + private commandQueue: number[] = []; + private spawnRetryAttempts = 0; + private additionalAutoCompletePaths: string[] = []; + private workspacePath: string; + private languageServerStarted!: Deferred; + private initialized: Deferred; + private environmentVariablesProvider!: IEnvironmentVariablesProvider; + private ignoreJediMemoryFootprint: boolean = false; + private pidUsageFailures = { timer: new StopWatch(), counter: 0 }; + private lastCmdIdProcessed?: number; + private lastCmdIdProcessedForPidUsage?: number; + private readonly disposables: Disposable[] = []; + private timer?: NodeJS.Timer | number; + + public constructor( + workspacePath: string, + interpreter: PythonEnvironment | undefined, + private serviceContainer: IServiceContainer + ) { + this.workspacePath = workspacePath; + const configurationService = serviceContainer.get(IConfigurationService); + this.pythonSettings = configurationService.getSettings(Uri.file(workspacePath)); + this.lastKnownPythonInterpreter = interpreter ? interpreter.path : this.pythonSettings.pythonPath; + this.initialized = createDeferred(); + this.startLanguageServer() + .then(() => this.initialized.resolve()) + .ignoreErrors(); + this.checkJediMemoryFootprint().ignoreErrors(); + } - public getNextCommandId(): number { - return this.cmdId++; + private static getProperty(o: object, name: string): T { + return (o as any)[name]; } - public sendCommand(cmd: ICommand): Promise { - return sendCommand(cmd); + + public dispose() { + while (this.disposables.length > 0) { + const disposable = this.disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + if (this.timer) { + clearTimeout(this.timer as any); + } + this.killProcess(); } -} -function initialize(dir: string) { - spawnProcess(path.join(dir, "pythonFiles")); -} + public getNextCommandId(): number { + const result = this.cmdId; + this.cmdId += 1; + return result; + } -var previousData = ""; -var commands = new Map>(); -var commandQueue: number[] = []; + public async sendCommand(cmd: ICommand): Promise { + await this.initialized.promise; + await this.languageServerStarted.promise; + if (!this.proc) { + return Promise.reject(new Error('Python proc not initialized')); + } -function killProcess() { - try { - if (proc) { - proc.kill(); + const executionCmd = >cmd; + const payload = this.createPayload(executionCmd); + executionCmd.deferred = createDeferred(); + try { + this.proc.stdin.write(`${JSON.stringify(payload)}\n`); + this.commands.set(executionCmd.id, executionCmd); + this.commandQueue.push(executionCmd.id); + } catch (ex) { + traceError(ex); + //If 'This socket is closed.' that means process didn't start at all (at least not properly). + if (ex.message === 'This socket is closed.') { + this.killProcess(); + } else { + this.handleError('sendCommand', ex.message); + } + return Promise.reject(ex); } + return executionCmd.deferred.promise; } - catch (ex) { } -} -function handleError(source: string, errorMessage: string) { - logger.error(source + ' jediProxy', `Error (${source}) ${errorMessage}`); -} - -function spawnProcess(dir: string) { - try { - logger.log('child_process.spawn in jediProxy', 'Value of pythonSettings.pythonPath is :' + pythonSettings.pythonPath); - proc = child_process.spawn(pythonSettings.pythonPath, ["-u", "completion.py"], { - cwd: dir + // keep track of the directory so we can re-spawn the process. + private initialize(): Promise { + return this.spawnProcess().catch((ex) => { + if (this.languageServerStarted) { + this.languageServerStarted.reject(ex); + } + this.handleError('spawnProcess', ex); }); } - catch (ex) { - return handleError("spawnProcess", ex.message); - } - proc.stderr.on("data", (data) => { - handleError("stderr", data); - }); - proc.on("end", (end) => { - logger.error('spawnProcess.end', "End - " + end); - }); - proc.on("error", error => { - handleError("error", error); - }); - - proc.stdout.on("data", (data) => { - //Possible there was an exception in parsing the data returned - //So append the data then parse it - var dataStr = previousData = previousData + data + "" - var responses: any[]; - try { - responses = dataStr.split("\n").filter(line=> line.length > 0).map(resp=> JSON.parse(resp)); - previousData = ""; + private shouldCheckJediMemoryFootprint() { + if (this.ignoreJediMemoryFootprint || this.pythonSettings.jediMemoryLimit === -1) { + return false; } - catch (ex) { - //Possible we've only received part of the data, hence don't clear previousData - handleError("stdout", ex.message); + if ( + this.lastCmdIdProcessedForPidUsage && + this.lastCmdIdProcessed && + this.lastCmdIdProcessedForPidUsage === this.lastCmdIdProcessed + ) { + // If no more commands were processed since the last time, + // then there's no need to check again. + return false; + } + return true; + } + private async checkJediMemoryFootprint() { + // Check memory footprint periodically. Do not check on every request due to + // the performance impact. See https://github.com/soyuka/pidusage - on Windows + // it is using wmic which means spawning cmd.exe process on every request. + if (this.pythonSettings.jediMemoryLimit === -1) { return; } - responses.forEach((response) => { - if (response["argments"]) { - var index = commandQueue.indexOf(cmd.id); - commandQueue.splice(index, 1); - return; + await this.checkJediMemoryFootprintImpl(); + if (this.timer) { + clearTimeout(this.timer as any); + } + this.timer = setTimeout(() => this.checkJediMemoryFootprint(), 15 * 1000); + } + private async checkJediMemoryFootprintImpl(): Promise { + if (!this.proc || this.proc.killed) { + return; + } + if (!this.shouldCheckJediMemoryFootprint()) { + return; + } + this.lastCmdIdProcessedForPidUsage = this.lastCmdIdProcessed; + + // Do not run pidusage over and over, wait for it to finish. + const deferred = createDeferred(); + (pidusage as any).stat(this.proc.pid, async (err: any, result: any) => { + if (err) { + this.pidUsageFailures.counter += 1; + // If this function fails 2 times in the last 60 seconds, lets not try ever again. + if (this.pidUsageFailures.timer.elapsedTime > 60 * 1000) { + this.ignoreJediMemoryFootprint = this.pidUsageFailures.counter > 2; + this.pidUsageFailures.counter = 0; + this.pidUsageFailures.timer.reset(); + } + traceError('Python Extension: (pidusage)', err); + } else { + const limit = Math.min(Math.max(this.pythonSettings.jediMemoryLimit, 1024), 8192); + let restartJedi = false; + if (result && result.memory) { + restartJedi = result.memory > limit * 1024 * 1024; + const props = { + mem_use: result.memory, + limit: limit * 1024 * 1024, + isUserDefinedLimit: limit !== 1024, + restart: restartJedi + }; + sendTelemetryEvent(EventName.JEDI_MEMORY, undefined, props); + } + if (restartJedi) { + traceWarning( + `IntelliSense process memory consumption exceeded limit of ${limit} MB and process will be restarted.\nThe limit is controlled by the 'python.jediMemoryLimit' setting.` + ); + await this.restartLanguageServer(); + } } - var responseId = response["id"]; - var cmd = >commands.get(responseId); + deferred.resolve(); + }); - if (typeof cmd === "object" && cmd !== null) { - commands.delete(responseId); - var index = commandQueue.indexOf(cmd.id); - commandQueue.splice(index, 1); + return deferred.promise; + } - //Check if this command has expired - if (cmd.token.isCancellationRequested) { - return; - } + // @debounce(1500) + @swallowExceptions('JediProxy') + private async environmentVariablesChangeHandler() { + const newAutoComletePaths = await this.buildAutoCompletePaths(); + if (this.additionalAutoCompletePaths.join(',') !== newAutoComletePaths.join(',')) { + this.additionalAutoCompletePaths = newAutoComletePaths; + this.restartLanguageServer().ignoreErrors(); + } + } + @swallowExceptions('JediProxy') + private async startLanguageServer(): Promise { + const newAutoComletePaths = await this.buildAutoCompletePaths(); + this.additionalAutoCompletePaths = newAutoComletePaths; + return this.restartLanguageServer(); + } + private restartLanguageServer(): Promise { + this.killProcess(); + this.clearPendingRequests(); + return this.initialize(); + } - switch (cmd.command) { - case CommandType.Completions: { - var results = response['results']; - if (results.length > 0) { - results.forEach(item=> { - item.type = getMappedVSCodeType(item.type); - item.kind = getMappedVSCodeSymbol(item.type); - }); - - var completionResult: ICompletionResult = { - items: results, - requestId: cmd.id - } - cmd.resolve(completionResult); - } - break; + private clearPendingRequests() { + this.commandQueue = []; + this.commands.forEach((item) => { + if (item.deferred !== undefined) { + item.deferred.resolve(); + } + }); + this.commands.clear(); + } + + private killProcess() { + try { + if (this.proc) { + this.proc.kill(); + } + // tslint:disable-next-line:no-empty + } catch (ex) {} + this.proc = undefined; + } + + private handleError(source: string, errorMessage: string) { + traceError(`${source} jediProxy`, `Error (${source}) ${errorMessage}`); + } + + // tslint:disable-next-line:max-func-body-length + private async spawnProcess() { + if (this.languageServerStarted && !this.languageServerStarted.completed) { + this.languageServerStarted.reject(new Error('Language server not started.')); + } + this.languageServerStarted = createDeferred(); + const pythonProcess = await this.serviceContainer + .get(IPythonExecutionFactory) + .create({ resource: Uri.file(this.workspacePath), pythonPath: this.lastKnownPythonInterpreter }); + // Check if the python path is valid. + if ((await pythonProcess.getExecutablePath().catch(() => '')).length === 0) { + return; + } + const [args, parse] = internalScripts.completion(this.pythonSettings.jediPath); + const result = pythonProcess.execObservable(args, {}); + this.proc = result.proc; + this.languageServerStarted.resolve(); + this.proc!.on('end', (end) => { + traceError('spawnProcess.end', `End - ${end}`); + }); + this.proc!.on('error', (error) => { + this.handleError('error', `${error}`); + this.spawnRetryAttempts += 1; + if ( + this.spawnRetryAttempts < 10 && + error && + error.message && + error.message.indexOf('This socket has been ended by the other party') >= 0 + ) { + this.spawnProcess().catch((ex) => { + if (this.languageServerStarted) { + this.languageServerStarted.reject(ex); } - case CommandType.Definitions: { - var defs = response['results']; - if (defs.length > 0) { - var def = defs[0]; - var defResult: IDefinitionResult = { - requestId: cmd.id, - definition: { - columnIndex: Number(def.column), - fileName: def.fileName, - lineIndex: Number(def.line), - text: def.text, - type: getMappedVSCodeType(def.type), - kind: getMappedVSCodeSymbol(def.type) - } - }; - - cmd.resolve(defResult); + this.handleError('spawnProcess', ex); + }); + } + }); + result.out.subscribe( + (output) => { + if (output.source === 'stderr') { + this.handleError('stderr', output.out); + } else { + const data = output.out; + // Possible there was an exception in parsing the data returned, + // so append the data and then parse it. + const dataStr = (this.previousData = `${this.previousData}${data}`); + // tslint:disable-next-line:no-any + let responses: any[]; + try { + responses = parse(dataStr); + this.previousData = ''; + } catch (ex) { + // Possible we've only received part of the data, hence don't clear previousData. + // Don't log errors when we haven't received the entire response. + if ( + ex.message.indexOf('Unexpected end of input') === -1 && + ex.message.indexOf('Unexpected end of JSON input') === -1 && + ex.message.indexOf('Unexpected token') === -1 + ) { + this.handleError('stdout', ex.message); } - break; + return; } - case CommandType.Symbols: { - var defs = response['results']; - if (defs.length > 0) { - var defResults: ISymbolResult = { - requestId: cmd.id, - definitions: [] - } - defResults.definitions = defs.map(def=> { - return { - columnIndex: def.column, - fileName: def.fileName, - lineIndex: def.line, - text: def.text, - type: getMappedVSCodeType(def.type), - kind: getMappedVSCodeSymbol(def.type) - }; - }); - - cmd.resolve(defResults); + + responses.forEach((response) => { + if (!response) { + return; } - break; - } - case CommandType.Usages: { - var defs = response['results']; - if (defs.length > 0) { - var refResult: IReferenceResult = { - requestId: cmd.id, - references: defs.map(item=> { - return { - columnIndex: item.column, - fileName: item.fileName, - lineIndex: item.line - 1, - moduleName: item.moduleName, - name: item.name - }; - } - ) - }; - - cmd.resolve(refResult); + const responseId = JediProxy.getProperty(response, 'id'); + if (!this.commands.has(responseId)) { + return; } - break; - } + const cmd = this.commands.get(responseId); + if (!cmd) { + return; + } + this.lastCmdIdProcessed = cmd.id; + if (JediProxy.getProperty(response, 'arguments')) { + this.commandQueue.splice(this.commandQueue.indexOf(cmd.id), 1); + return; + } + + this.commands.delete(responseId); + const index = this.commandQueue.indexOf(cmd.id); + if (index) { + this.commandQueue.splice(index, 1); + } + + // Check if this command has expired. + if (cmd.token.isCancellationRequested) { + this.safeResolve(cmd, undefined); + return; + } + + const handler = this.getCommandHandler(cmd.command); + if (handler) { + handler.call(this, cmd, response); + } + // Check if too many pending requests. + this.checkQueueLength(); + }); } - } - - //Ok, check if too many pending requets - if (commandQueue.length > 10) { - var items = commandQueue.splice(0, commandQueue.length - 10); - items.forEach(id=> { - if (commands.has(id)) { - commands.delete(id); + }, + (error) => this.handleError('subscription.error', `${error}`) + ); + } + private getCommandHandler( + command: CommandType + ): undefined | ((command: IExecutionCommand, response: object) => void) { + switch (command) { + case CommandType.Completions: + return this.onCompletion; + case CommandType.Definitions: + return this.onDefinition; + case CommandType.Hover: + return this.onHover; + case CommandType.Symbols: + return this.onSymbols; + case CommandType.Usages: + return this.onUsages; + case CommandType.Arguments: + return this.onArguments; + default: + return; + } + } + private onCompletion(command: IExecutionCommand, response: object): void { + let results = JediProxy.getProperty(response, 'results'); + results = Array.isArray(results) ? results : []; + results.forEach((item) => { + // tslint:disable-next-line:no-any + const originalType = (item.type); + item.type = getMappedVSCodeType(originalType); + item.kind = getMappedVSCodeSymbol(originalType); + item.rawType = getMappedVSCodeType(originalType); + }); + const completionResult: ICompletionResult = { + items: results, + requestId: command.id + }; + this.safeResolve(command, completionResult); + } + + private onDefinition(command: IExecutionCommand, response: object): void { + // tslint:disable-next-line:no-any + const defs = JediProxy.getProperty(response, 'results'); + const defResult: IDefinitionResult = { + requestId: command.id, + definitions: [] + }; + if (defs.length > 0) { + defResult.definitions = defs.map((def) => { + const originalType = def.type as string; + return { + fileName: def.fileName, + text: def.text, + rawType: originalType, + type: getMappedVSCodeType(originalType), + kind: getMappedVSCodeSymbol(originalType), + container: def.container, + range: { + startLine: def.range.start_line, + startColumn: def.range.start_column, + endLine: def.range.end_line, + endColumn: def.range.end_column } - }) - } + }; + }); + } + this.safeResolve(command, defResult); + } + + private onHover(command: IExecutionCommand, response: object): void { + // tslint:disable-next-line:no-any + const defs = JediProxy.getProperty(response, 'results'); + const defResult: IHoverResult = { + requestId: command.id, + items: defs.map((def) => { + return { + kind: getMappedVSCodeSymbol(def.type), + description: def.description, + signature: def.signature, + docstring: def.docstring, + text: def.text + }; + }) + }; + this.safeResolve(command, defResult); + } + + private onSymbols(command: IExecutionCommand, response: object): void { + // tslint:disable-next-line:no-any + let defs = JediProxy.getProperty(response, 'results'); + defs = Array.isArray(defs) ? defs : []; + const defResults: ISymbolResult = { + requestId: command.id, + definitions: [] + }; + defResults.definitions = defs.map((def) => { + const originalType = def.type as string; + return { + fileName: def.fileName, + text: def.text, + rawType: originalType, + type: getMappedVSCodeType(originalType), + kind: getMappedVSCodeSymbol(originalType), + container: def.container, + range: { + startLine: def.range.start_line, + startColumn: def.range.start_column, + endLine: def.range.end_line, + endColumn: def.range.end_column + } + }; }); - }); -} + this.safeResolve(command, defResults); + } + + private onUsages(command: IExecutionCommand, response: object): void { + // tslint:disable-next-line:no-any + let defs = JediProxy.getProperty(response, 'results'); + defs = Array.isArray(defs) ? defs : []; + const refResult: IReferenceResult = { + requestId: command.id, + references: defs.map((item) => { + return { + columnIndex: item.column, + fileName: item.fileName, + lineIndex: item.line - 1, + moduleName: item.moduleName, + name: item.name + }; + }) + }; + this.safeResolve(command, refResult); + } -function sendCommand(cmd: ICommand): Promise { - return new Promise((resolve, reject) => { - if (!proc) { - return reject("Python proc not initialized"); + private onArguments(command: IExecutionCommand, response: object): void { + // tslint:disable-next-line:no-any + const defs = JediProxy.getProperty(response, 'results'); + // tslint:disable-next-line:no-object-literal-type-assertion + this.safeResolve(command, { + requestId: command.id, + definitions: defs + }); + } + + private checkQueueLength(): void { + if (this.commandQueue.length > 10) { + const items = this.commandQueue.splice(0, this.commandQueue.length - 10); + items.forEach((id) => { + if (this.commands.has(id)) { + const cmd1 = this.commands.get(id); + try { + this.safeResolve(cmd1, undefined); + // tslint:disable-next-line:no-empty + } catch (ex) { + } finally { + this.commands.delete(id); + } + } + }); } - var exexcutionCmd = >cmd; - var payload = createPayload(exexcutionCmd); - exexcutionCmd.resolve = resolve; - exexcutionCmd.reject = reject; - try { - proc.stdin.write(JSON.stringify(payload) + "\n"); - commands.set(exexcutionCmd.id, exexcutionCmd); - commandQueue.push(exexcutionCmd.id); + } + + private createPayload(cmd: IExecutionCommand): JediProxyPayload { + const payload: JediProxyPayload = { + id: cmd.id, + prefix: '', + lookup: commandNames.get(cmd.command), + path: cmd.fileName, + source: cmd.source, + line: cmd.lineIndex, + column: cmd.columnIndex, + config: this.getConfig() + }; + + if (cmd.command === CommandType.Symbols) { + delete payload.column; + delete payload.line; } - catch (ex) { - //If 'This socket is closed.' that means process didn't start at all (at least not properly) - if (ex.message === "This socket is closed.") { - killProcess(); - } - else { - handleError("sendCommand", ex.message); + + return payload; + } + + private async getPathFromPython(getArgs = internalPython.getExecutable): Promise { + const [args, parse] = getArgs(); + try { + const pythonProcess = await this.serviceContainer + .get(IPythonExecutionFactory) + .create({ resource: Uri.file(this.workspacePath), pythonPath: this.lastKnownPythonInterpreter }); + const result = await pythonProcess.exec(args, { cwd: this.workspacePath }); + const lines = parse(result.stdout).splitLines(); + if (lines.length === 0) { + return ''; } - reject(ex.message); + const fs = this.serviceContainer.get(IFileSystem); + const exists = await fs.fileExists(lines[0]); + return exists ? lines[0] : ''; + } catch { + return ''; } - }); -} - -function createPayload(cmd: IExecutionCommand): any { - var payload = { - id: cmd.id, - prefix: "", - lookup: commandNames.get(cmd.command), - path: cmd.fileName, - source: cmd.source, - line: cmd.lineIndex, - column: cmd.columnIndex, - config: getConfig() - }; + } + private async buildAutoCompletePaths(): Promise { + const filePathPromises = [ + // Sysprefix. + this.getPathFromPython(internalPython.getSysPrefix).catch(() => ''), + // exeucutable path. + this.getPathFromPython(internalPython.getExecutable) + .then((execPath) => path.dirname(execPath)) + .catch(() => ''), + // Python specific site packages. + this.getPathFromPython(internalPython.getSitePackages) + .then((libPath) => { + // On windows we also need the libs path (second item will return c:\xxx\lib\site-packages). + // This is returned by "from distutils.sysconfig import get_python_lib; print(get_python_lib())". + return IS_WINDOWS && libPath.length > 0 ? path.join(libPath, '..') : libPath; + }) + .catch(() => ''), + // Python global site packages, as a fallback in case user hasn't installed them in custom environment. + this.getPathFromPython(internalPython.getUserSitePackages).catch(() => '') + ]; - if (cmd.command === CommandType.Symbols) { - delete payload.column; - delete payload.line; + try { + const pythonPaths = await this.getEnvironmentVariablesProvider() + .getEnvironmentVariables(Uri.file(this.workspacePath)) + .then((customEnvironmentVars) => + customEnvironmentVars ? JediProxy.getProperty(customEnvironmentVars, 'PYTHONPATH') : '' + ) + .then((pythonPath) => + typeof pythonPath === 'string' && pythonPath.trim().length > 0 ? pythonPath.trim() : '' + ) + .then((pythonPath) => pythonPath.split(path.delimiter).filter((item) => item.trim().length > 0)); + const resolvedPaths = pythonPaths + .filter((pythonPath) => !path.isAbsolute(pythonPath)) + .map((pythonPath) => path.resolve(this.workspacePath, pythonPath)); + const filePaths = await Promise.all(filePathPromises); + return filePaths.concat(...pythonPaths, ...resolvedPaths).filter((p) => p.length > 0); + } catch (ex) { + traceError('Python Extension: jediProxy.filePaths', ex); + return []; + } } + private getEnvironmentVariablesProvider() { + if (!this.environmentVariablesProvider) { + this.environmentVariablesProvider = this.serviceContainer.get( + IEnvironmentVariablesProvider + ); + this.environmentVariablesProvider.onDidEnvironmentVariablesChange( + this.environmentVariablesChangeHandler.bind(this) + ); + } + return this.environmentVariablesProvider; + } + private getConfig(): JediProxyConfig { + // Add support for paths relative to workspace. + const extraPaths = this.pythonSettings.autoComplete + ? this.pythonSettings.autoComplete.extraPaths.map((extraPath) => { + if (path.isAbsolute(extraPath)) { + return extraPath; + } + if (typeof this.workspacePath !== 'string') { + return ''; + } + return path.join(this.workspacePath, extraPath); + }) + : []; + + // Always add workspace path into extra paths. + if (typeof this.workspacePath === 'string') { + extraPaths.unshift(this.workspacePath); + } - return payload; -} + const distinctExtraPaths = extraPaths + .concat(this.additionalAutoCompletePaths) + .filter((value) => value.length > 0) + .filter((value, index, self) => self.indexOf(value) === index); + + return { + extraPaths: distinctExtraPaths, + useSnippets: false, + caseInsensitiveCompletion: true, + showDescriptions: true, + fuzzyMatcher: true + }; + } -function getConfig() { - //Add support for paths relative to workspace - var extraPaths = pythonSettings.autoComplete.extraPaths.map(extraPath=> { - if (path.isAbsolute(extraPath)) { - return extraPath; + private safeResolve( + command: IExecutionCommand | undefined | null, + result: ICommandResult | PromiseLike | undefined + ): void { + if (command && command.deferred) { + command.deferred.resolve(result); } - return path.join(vscode.workspace.rootPath, extraPath); - }); - - return { - extraPaths: extraPaths, - useSnippets: false, - caseInsensitiveCompletion: true, - showDescriptions: true, - fuzzyMatcher: true - }; + } } -export interface ICommand { +// tslint:disable-next-line:no-unused-variable +export interface ICommand { + telemetryEvent?: string; command: CommandType; - source: string; + source?: string; fileName: string; lineIndex: number; columnIndex: number; } -interface IExecutionCommand extends ICommand { - id?: number; - resolve: (value?: T) => void - reject: (ICommandError) => void; - token: vscode.CancellationToken; +interface IExecutionCommand extends ICommand { + id: number; + deferred?: Deferred; + token: CancellationToken; + delay?: number; } export interface ICommandError { - message: string + message: string; } export interface ICommandResult { - requestId: number + requestId: number; } export interface ICompletionResult extends ICommandResult { items: IAutoCompleteItem[]; } +export interface IHoverResult extends ICommandResult { + items: IHoverItem[]; +} export interface IDefinitionResult extends ICommandResult { - definition: IDefinition; + definitions: IDefinition[]; } export interface IReferenceResult extends ICommandResult { references: IReference[]; } export interface ISymbolResult extends ICommandResult { - definitions: IDefinition[] + definitions: IDefinition[]; +} +export interface IArgumentsResult extends ICommandResult { + definitions: ISignature[]; +} + +export interface ISignature { + name: string; + docstring: string; + description: string; + paramindex: number; + params: IArgument[]; +} +export interface IArgument { + name: string; + value: string; + docstring: string; + description: string; } export interface IReference { - name: string, - fileName: string, - columnIndex: number, - lineIndex: number, - moduleName: string + name: string; + fileName: string; + columnIndex: number; + lineIndex: number; + moduleName: string; } export interface IAutoCompleteItem { - type: vscode.CompletionItemKind; - kind: vscode.SymbolKind; + type: CompletionItemKind; + rawType: CompletionItemKind; + kind: SymbolKind; text: string; description: string; + raw_docstring: string; rightLabel: string; } +export interface IDefinitionRange { + startLine: number; + startColumn: number; + endLine: number; + endColumn: number; +} export interface IDefinition { - type: vscode.CompletionItemKind; - kind: vscode.SymbolKind; + rawType: string; + type: CompletionItemKind; + kind: SymbolKind; text: string; fileName: string; - columnIndex: number; - lineIndex: number; + container: string; + range: IDefinitionRange; +} + +export interface IHoverItem { + kind: SymbolKind; + text: string; + description: string; + docstring: string; + signature: string; } +export class JediProxyHandler implements Disposable { + private commandCancellationTokenSources: Map; -export class JediProxyHandler { - private jediProxy: JediProxy; - private defaultCallbackData: T; + public constructor(private jediProxy: JediProxy) { + this.commandCancellationTokenSources = new Map(); + } - private lastToken: vscode.CancellationToken; - private lastCommandId: number; - private promiseResolve: (value?: T) => void; - private parseResponse: (data: R) => T; - private cancellationTokenSource: vscode.CancellationTokenSource; + public get JediProxy(): JediProxy { + return this.jediProxy; + } - public constructor(context: vscode.ExtensionContext, defaultCallbackData: T, parseResponse: (data: R) => T) { - this.jediProxy = new JediProxy(context); - this.defaultCallbackData = defaultCallbackData; - this.parseResponse = parseResponse; + public dispose() { + if (this.jediProxy) { + this.jediProxy.dispose(); + } } - public sendCommand(cmd: ICommand, resolve: (value: T) => void, token?: vscode.CancellationToken) { - var executionCmd = >cmd; + public sendCommand(cmd: ICommand, _token?: CancellationToken): Promise { + const executionCmd = >cmd; executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); - if (this.cancellationTokenSource) { - try { - this.cancellationTokenSource.cancel(); + if (this.commandCancellationTokenSources.has(cmd.command)) { + const ct = this.commandCancellationTokenSources.get(cmd.command); + if (ct) { + ct.cancel(); } - catch (ex) { } } - this.cancellationTokenSource = new vscode.CancellationTokenSource(); - executionCmd.token = this.cancellationTokenSource.token; + const cancellation = new CancellationTokenSource(); + this.commandCancellationTokenSources.set(cmd.command, cancellation); + executionCmd.token = cancellation.token; - this.jediProxy.sendCommand(executionCmd).then(data=> this.onResolved(data), () => { }); - this.lastCommandId = executionCmd.id; - this.lastToken = token; - this.promiseResolve = resolve; + return this.jediProxy.sendCommand(executionCmd).catch((reason) => { + traceError(reason); + return undefined; + }); } - private onResolved(data: R) { - if (this.lastToken.isCancellationRequested || data.requestId !== this.lastCommandId) { - this.promiseResolve(this.defaultCallbackData); - } - if (data) { - this.promiseResolve(this.parseResponse(data)); - } - else { - this.promiseResolve(this.defaultCallbackData); + public sendCommandNonCancellableCommand(cmd: ICommand, token?: CancellationToken): Promise { + const executionCmd = >cmd; + executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); + if (token) { + executionCmd.token = token; } + + return this.jediProxy.sendCommand(executionCmd).catch((reason) => { + traceError(reason); + return undefined; + }); } } diff --git a/src/client/providers/lintProvider.ts b/src/client/providers/lintProvider.ts deleted file mode 100644 index 1e75d58c03b9..000000000000 --- a/src/client/providers/lintProvider.ts +++ /dev/null @@ -1,147 +0,0 @@ -/*--------------------------------------------------------- - ** Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ -'use strict'; - -import * as vscode from 'vscode'; -import * as path from 'path'; -import * as linter from '../linters/baseLinter'; -import * as prospector from './../linters/prospector'; -import * as pylint from './../linters/pylint'; -import * as pep8 from './../linters/pep8Linter'; -import * as flake8 from './../linters/flake8'; -import * as pydocstyle from './../linters/pydocstyle'; -import * as settings from '../common/configSettings'; - -const FILE_PROTOCOL = "file:///" - -function uriToPath(pathValue: string): string { - if (pathValue.startsWith(FILE_PROTOCOL)) { - pathValue = pathValue.substring(FILE_PROTOCOL.length); - } - - return path.normalize(decodeURIComponent(pathValue)); -} - -const lintSeverityToVSSeverity = new Map(); -lintSeverityToVSSeverity.set(linter.LintMessageSeverity.Error, vscode.DiagnosticSeverity.Error) -lintSeverityToVSSeverity.set(linter.LintMessageSeverity.Hint, vscode.DiagnosticSeverity.Hint) -lintSeverityToVSSeverity.set(linter.LintMessageSeverity.Information, vscode.DiagnosticSeverity.Information) -lintSeverityToVSSeverity.set(linter.LintMessageSeverity.Warning, vscode.DiagnosticSeverity.Warning) - -function createDiagnostics(message: linter.ILintMessage, txtDocumentLines: string[]): vscode.Diagnostic { - var sourceLine = txtDocumentLines[message.line - 1]; - var sourceStart = sourceLine.substring(message.column - 1); - var endCol = txtDocumentLines[message.line - 1].length; - - //try to get the first word from the startig position - if (message.possibleWord === "string" && message.possibleWord.length > 0) { - endCol = message.column + message.possibleWord.length; - } - - var range = new vscode.Range(new vscode.Position(message.line - 1, message.column), new vscode.Position(message.line - 1, endCol)); - - var severity = lintSeverityToVSSeverity.get(message.severity); - return new vscode.Diagnostic(range, message.code + ":" + message.message, severity); -} - -export class LintProvider extends vscode.Disposable { - private settings: settings.IPythonSettings; - private diagnosticCollection: vscode.DiagnosticCollection; - private linters: linter.BaseLinter[] = []; - private pendingLintings = new Map(); - private outputChannel: vscode.OutputChannel; - private context: vscode.ExtensionContext; - - public constructor(context: vscode.ExtensionContext, settings: settings.IPythonSettings, outputChannel: vscode.OutputChannel) { - super(() => { }); - this.outputChannel = outputChannel; - this.context = context; - this.settings = settings; - - this.initialize(); - } - - private initialize() { - this.diagnosticCollection = vscode.languages.createDiagnosticCollection("python"); - var disposables = []; - - this.linters.push(new prospector.Linter(this.context.asAbsolutePath("."), this.settings, this.outputChannel)); - this.linters.push(new pylint.Linter(this.context.asAbsolutePath("."), this.settings, this.outputChannel)); - this.linters.push(new pep8.Linter(this.context.asAbsolutePath("."), this.settings, this.outputChannel)); - this.linters.push(new flake8.Linter(this.context.asAbsolutePath("."), this.settings, this.outputChannel)); - this.linters.push(new pydocstyle.Linter(this.context.asAbsolutePath("."), this.settings, this.outputChannel)); - - var disposable = vscode.workspace.onDidChangeTextDocument((e) => { - if (e.document.languageId !== "python" || !this.settings.linting.enabled || !this.settings.linting.lintOnTextChange) { - return; - } - this.lintDocument(e.document.uri, e.document.getText().split(/\r?\n/g), 1000); - }); - this.context.subscriptions.push(disposable); - - disposable = vscode.workspace.onDidSaveTextDocument((e) => { - if (e.languageId !== "python" || !this.settings.linting.enabled || !this.settings.linting.lintOnSave) { - return; - } - this.lintDocument(e.uri, e.getText().split(/\r?\n/g), 100); - }); - this.context.subscriptions.push(disposable); - } - - private lastTimeout: number; - private lintDocument(documentUri: vscode.Uri, documentLines: string[], delay: number): void { - //Since this is a hack, lets wait for 2 seconds before linting - //Give user to continue typing before we waste CPU time - if (this.lastTimeout) { - clearTimeout(this.lastTimeout); - this.lastTimeout = 0; - } - - this.lastTimeout = setTimeout(() => { - this.onLintDocument(documentUri, documentLines); - }, delay); - } - - private onLintDocument(documentUri: vscode.Uri, documentLines: string[]): void { - if (this.pendingLintings.has(documentUri.fsPath)) { - this.pendingLintings.get(documentUri.fsPath).cancel(); - this.pendingLintings.delete(documentUri.fsPath); - } - - var cancelToken = new vscode.CancellationTokenSource(); - cancelToken.token.onCancellationRequested(() => { - if (this.pendingLintings.has(documentUri.fsPath)) { - this.pendingLintings.delete(documentUri.fsPath); - } - }); - this.pendingLintings.set(documentUri.fsPath, cancelToken); - - var consolidatedMessages: linter.ILintMessage[] = []; - var promises = []; - this.linters.forEach(linter=> { - promises.push(linter.runLinter(documentUri.fsPath, documentLines).then(diagnostics=> { - consolidatedMessages = consolidatedMessages.concat(diagnostics); - })); - }) - - Promise.all(promises).then(() => { - if (cancelToken.token.isCancellationRequested) { - return; - } - - var messages = []; - //Limit the number of messages to the max value - consolidatedMessages = consolidatedMessages.filter((value, index) => index <= this.settings.linting.maxNumberOfProblems); - - //Build the message and suffix the message with the name of the linter used - consolidatedMessages.forEach(d=> { - d.message = `${d.message} (${d.provider})`; - messages.push(createDiagnostics(d, documentLines)); - }); - - this.diagnosticCollection.delete(documentUri); - this.diagnosticCollection.set(documentUri, messages); - }); - } -} \ No newline at end of file diff --git a/src/client/providers/linterProvider.ts b/src/client/providers/linterProvider.ts new file mode 100644 index 000000000000..8bfde47e192b --- /dev/null +++ b/src/client/providers/linterProvider.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { ConfigurationChangeEvent, Disposable, TextDocument, Uri, workspace } from 'vscode'; +import { IExtensionActivationService } from '../activation/types'; +import { IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { isTestExecution } from '../common/constants'; +import '../common/extensions'; +import { IFileSystem } from '../common/platform/types'; +import { IConfigurationService, IDisposable } from '../common/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { ILinterManager, ILintingEngine } from '../linters/types'; + +@injectable() +export class LinterProvider implements IExtensionActivationService, Disposable { + private interpreterService: IInterpreterService; + private documents: IDocumentManager; + private configuration: IConfigurationService; + private linterManager: ILinterManager; + private engine: ILintingEngine; + private fs: IFileSystem; + private readonly disposables: IDisposable[] = []; + private workspaceService: IWorkspaceService; + private activatedOnce: boolean = false; + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.serviceContainer = serviceContainer; + this.fs = this.serviceContainer.get(IFileSystem); + this.engine = this.serviceContainer.get(ILintingEngine); + this.linterManager = this.serviceContainer.get(ILinterManager); + this.interpreterService = this.serviceContainer.get(IInterpreterService); + this.documents = this.serviceContainer.get(IDocumentManager); + this.configuration = this.serviceContainer.get(IConfigurationService); + this.workspaceService = this.serviceContainer.get(IWorkspaceService); + } + + public async activate(): Promise { + if (this.activatedOnce) { + return; + } + this.activatedOnce = true; + this.disposables.push(this.interpreterService.onDidChangeInterpreter(() => this.engine.lintOpenPythonFiles())); + + this.documents.onDidOpenTextDocument((e) => this.onDocumentOpened(e), this.disposables); + this.documents.onDidCloseTextDocument((e) => this.onDocumentClosed(e), this.disposables); + this.documents.onDidSaveTextDocument((e) => this.onDocumentSaved(e), this.disposables); + + const disposable = this.workspaceService.onDidChangeConfiguration(this.lintSettingsChangedHandler.bind(this)); + this.disposables.push(disposable); + + // On workspace reopen we don't get `onDocumentOpened` since it is first opened + // and then the extension is activated. So schedule linting pass now. + if (!isTestExecution()) { + const timer = setTimeout(() => this.engine.lintOpenPythonFiles().ignoreErrors(), 1200); + this.disposables.push({ dispose: () => clearTimeout(timer) }); + } + } + + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + + private isDocumentOpen(uri: Uri): boolean { + return this.documents.textDocuments.some((document) => this.fs.arePathsSame(document.uri.fsPath, uri.fsPath)); + } + + private lintSettingsChangedHandler(e: ConfigurationChangeEvent) { + // Look for python files that belong to the specified workspace folder. + workspace.textDocuments.forEach((document) => { + if (e.affectsConfiguration('python.linting', document.uri)) { + this.engine.lintDocument(document, 'auto').ignoreErrors(); + } + }); + } + + private onDocumentOpened(document: TextDocument): void { + this.engine.lintDocument(document, 'auto').ignoreErrors(); + } + + private onDocumentSaved(document: TextDocument): void { + const settings = this.configuration.getSettings(document.uri); + if (document.languageId === 'python' && settings.linting.enabled && settings.linting.lintOnSave) { + this.engine.lintDocument(document, 'save').ignoreErrors(); + return; + } + + this.linterManager + .getActiveLinters(false, document.uri) + .then((linters) => { + const fileName = path.basename(document.uri.fsPath).toLowerCase(); + const watchers = linters.filter((info) => info.configFileNames.indexOf(fileName) >= 0); + if (watchers.length > 0) { + setTimeout(() => this.engine.lintOpenPythonFiles(), 1000); + } + }) + .ignoreErrors(); + } + + private onDocumentClosed(document: TextDocument) { + if (!document || !document.fileName || !document.uri) { + return; + } + // Check if this document is still open as a duplicate editor. + if (!this.isDocumentOpen(document.uri)) { + this.engine.clearDiagnostics(document); + } + } +} diff --git a/src/client/providers/objectDefinitionProvider.ts b/src/client/providers/objectDefinitionProvider.ts new file mode 100644 index 000000000000..fa886cfe05ef --- /dev/null +++ b/src/client/providers/objectDefinitionProvider.ts @@ -0,0 +1,87 @@ +'use strict'; + +import * as vscode from 'vscode'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import * as defProvider from './definitionProvider'; + +export class PythonObjectDefinitionProvider { + private readonly _defProvider: defProvider.PythonDefinitionProvider; + public constructor(jediFactory: JediFactory) { + this._defProvider = new defProvider.PythonDefinitionProvider(jediFactory); + } + + @captureTelemetry(EventName.GO_TO_OBJECT_DEFINITION) + public async goToObjectDefinition() { + const pathDef = await this.getObjectDefinition(); + if (typeof pathDef !== 'string' || pathDef.length === 0) { + return; + } + + const parts = pathDef.split('.'); + let source = ''; + let startColumn = 0; + if (parts.length === 1) { + source = `import ${parts[0]}`; + startColumn = 'import '.length; + } else { + const mod = parts.shift(); + source = `from ${mod} import ${parts.join('.')}`; + startColumn = `from ${mod} import `.length; + } + const range = new vscode.Range(0, startColumn, 0, source.length - 1); + // tslint:disable-next-line:no-any + const doc = ({ + fileName: 'test.py', + lineAt: (_line: number) => { + return { text: source }; + }, + getWordRangeAtPosition: (_position: vscode.Position) => range, + isDirty: true, + getText: () => source + }); + + const tokenSource = new vscode.CancellationTokenSource(); + const defs = await this._defProvider.provideDefinition(doc, range.start, tokenSource.token); + + if (defs === null) { + await vscode.window.showInformationMessage(`Definition not found for '${pathDef}'`); + return; + } + + let uri: vscode.Uri | undefined; + let lineNumber: number; + if (Array.isArray(defs) && defs.length > 0) { + uri = defs[0].uri; + lineNumber = defs[0].range.start.line; + } + if (defs && !Array.isArray(defs) && defs.uri) { + uri = defs.uri; + lineNumber = defs.range.start.line; + } + + if (uri) { + const openedDoc = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(openedDoc); + await vscode.commands.executeCommand('revealLine', { lineNumber: lineNumber!, at: 'top' }); + } else { + await vscode.window.showInformationMessage(`Definition not found for '${pathDef}'`); + } + } + + private intputValidation(value: string): string | undefined | null { + if (typeof value !== 'string') { + return ''; + } + value = value.trim(); + if (value.length === 0) { + return ''; + } + + return null; + } + private async getObjectDefinition(): Promise { + return vscode.window.showInputBox({ prompt: 'Enter Object Path', validateInput: this.intputValidation }); + } +} diff --git a/src/client/providers/providerUtilities.ts b/src/client/providers/providerUtilities.ts new file mode 100644 index 000000000000..7a330b51eec0 --- /dev/null +++ b/src/client/providers/providerUtilities.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Position, Range, TextDocument } from 'vscode'; +import { Tokenizer } from '../language/tokenizer'; +import { ITextRangeCollection, IToken, TokenizerMode, TokenType } from '../language/types'; + +export function getDocumentTokens( + document: TextDocument, + tokenizeTo: Position, + mode: TokenizerMode +): ITextRangeCollection { + const text = document.getText(new Range(new Position(0, 0), tokenizeTo)); + return new Tokenizer().tokenize(text, 0, text.length, mode); +} + +export function isPositionInsideStringOrComment(document: TextDocument, position: Position): boolean { + const tokenizeTo = position.translate(1, 0); + const tokens = getDocumentTokens(document, tokenizeTo, TokenizerMode.CommentsAndStrings); + const offset = document.offsetAt(position); + const index = tokens.getItemContaining(offset - 1); + if (index >= 0) { + const token = tokens.getItemAt(index); + return token.type === TokenType.String || token.type === TokenType.Comment; + } + if (offset > 0 && index >= 0) { + // In case position is at the every end of the comment or unterminated string + const token = tokens.getItemAt(index); + return token.end === offset && token.type === TokenType.Comment; + } + return false; +} diff --git a/src/client/providers/referenceProvider.ts b/src/client/providers/referenceProvider.ts index 59fbbf0542c4..1a392dcb79d3 100644 --- a/src/client/providers/referenceProvider.ts +++ b/src/client/providers/referenceProvider.ts @@ -1,57 +1,77 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - 'use strict'; import * as vscode from 'vscode'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; import * as proxy from './jediProxy'; -function parseData(data: proxy.IReferenceResult): vscode.Location[] { - if (data && data.references.length > 0) { - var references = data.references.map(ref=> { - var definitionResource = vscode.Uri.file(ref.fileName); - var range = new vscode.Range(ref.lineIndex, ref.columnIndex, ref.lineIndex, ref.columnIndex); +export class PythonReferenceProvider implements vscode.ReferenceProvider { + public constructor(private jediFactory: JediFactory) {} + private static parseData(data: proxy.IReferenceResult): vscode.Location[] { + if (data && data.references.length > 0) { + // tslint:disable-next-line:no-unnecessary-local-variable + const references = data.references + .filter((ref) => { + if ( + !ref || + typeof ref.columnIndex !== 'number' || + typeof ref.lineIndex !== 'number' || + typeof ref.fileName !== 'string' || + ref.columnIndex === -1 || + ref.lineIndex === -1 || + ref.fileName.length === 0 + ) { + return false; + } + return true; + }) + .map((ref) => { + const definitionResource = vscode.Uri.file(ref.fileName); + const range = new vscode.Range(ref.lineIndex, ref.columnIndex, ref.lineIndex, ref.columnIndex); - return new vscode.Location(definitionResource, range); - }); + return new vscode.Location(definitionResource, range); + }); - return references; + return references; + } + return []; } - return []; -} -export class PythonReferenceProvider implements vscode.ReferenceProvider { - private jediProxyHandler: proxy.JediProxyHandler; + @captureTelemetry(EventName.REFERENCE) + public async provideReferences( + document: vscode.TextDocument, + position: vscode.Position, + _context: vscode.ReferenceContext, + token: vscode.CancellationToken + ): Promise { + const filename = document.fileName; + if (document.lineAt(position.line).text.match(/^\s*\/\//)) { + return; + } + if (position.character <= 0) { + return; + } - public constructor(context: vscode.ExtensionContext) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, null, parseData); - } + const range = document.getWordRangeAtPosition(position); + if (!range) { + return; + } + const columnIndex = range.isEmpty ? position.character : range.end.character; + const cmd: proxy.ICommand = { + command: proxy.CommandType.Usages, + fileName: filename, + columnIndex: columnIndex, + lineIndex: position.line + }; + + if (document.isDirty) { + cmd.source = document.getText(); + } - public provideReferences(document: vscode.TextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Thenable { - return new Promise((resolve, reject) => { - var filename = document.fileName; - if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return resolve(); - } - if (position.character <= 0) { - return resolve(); - } - - var source = document.getText(); - var range = document.getWordRangeAtPosition(position); - var columnIndex = range.isEmpty ? position.character : range.end.character; - var cmd: proxy.ICommand = { - command: proxy.CommandType.Usages, - fileName: filename, - columnIndex: columnIndex, - lineIndex: position.line, - source: source - }; - - var definition: proxy.IAutoCompleteItem = null; - - this.jediProxyHandler.sendCommand(cmd, resolve, token); - }); + const data = await this.jediFactory + .getJediProxyHandler(document.uri) + .sendCommand(cmd, token); + return data ? PythonReferenceProvider.parseData(data) : undefined; } } diff --git a/src/client/providers/renameProvider.ts b/src/client/providers/renameProvider.ts index 4cd61a6fdfad..86037f894c60 100644 --- a/src/client/providers/renameProvider.ts +++ b/src/client/providers/renameProvider.ts @@ -1,78 +1,93 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ +import { + CancellationToken, + OutputChannel, + Position, + ProviderResult, + RenameProvider, + TextDocument, + Uri, + window, + workspace, + WorkspaceEdit +} from 'vscode'; +import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import { getWorkspaceEditsFromPatch } from '../common/editor'; +import { traceError } from '../common/logger'; +import { IFileSystem } from '../common/platform/types'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { IInstaller, IOutputChannel, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { RefactorProxy } from '../refactor/proxy'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; -'use strict'; +type RenameResponse = { + results: [{ diff: string }]; +}; -import * as vscode from 'vscode'; -import * as proxy from './jediProxy'; - -var _oldName = ""; -var _newName = ""; - -function parseData(data: proxy.IReferenceResult): vscode.WorkspaceEdit { - if (data && data.references.length > 0) { - var references = data.references.filter(ref=> { - var relPath = vscode.workspace.asRelativePath(ref.fileName); - return !relPath.startsWith(".."); - }); - - var workSpaceEdit = new vscode.WorkspaceEdit(); - references.forEach(ref=> { - var uri = vscode.Uri.file(ref.fileName); - var range = new vscode.Range(ref.lineIndex, ref.columnIndex, ref.lineIndex, ref.columnIndex + _oldName.length); - workSpaceEdit.replace(uri, range, _newName); - }); - return workSpaceEdit; +export class PythonRenameProvider implements RenameProvider { + private readonly outputChannel: OutputChannel; + constructor(private serviceContainer: IServiceContainer) { + this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); } - return; -} - - -export class PythonRenameProvider implements vscode.RenameProvider { - private jediProxyHandler: proxy.JediProxyHandler; - - public constructor(context: vscode.ExtensionContext) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, null, parseData); - } - - public provideRenameEdits(document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Thenable { - return vscode.workspace.saveAll(false).then(() => { - return this.doRename(document, position, newName, token); + @captureTelemetry(EventName.REFACTOR_RENAME) + public provideRenameEdits( + document: TextDocument, + position: Position, + newName: string, + _token: CancellationToken + ): ProviderResult { + return workspace.saveAll(false).then(() => { + return this.doRename(document, position, newName); }); } - private doRename(document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Thenable { - return new Promise((resolve, reject) => { - var filename = document.fileName; - if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return resolve(); - } - if (position.character <= 0) { - return resolve(); - } + private doRename(document: TextDocument, position: Position, newName: string): ProviderResult { + if (document.lineAt(position.line).text.match(/^\s*\/\//)) { + return; + } + if (position.character <= 0) { + return; + } - var source = document.getText(); - var range = document.getWordRangeAtPosition(position); - if (range == undefined || range == null || range.isEmpty) { - return resolve(); - } - _oldName = document.getText(range); - _newName = newName; - if (_oldName === newName) { - return resolve(); - } + const range = document.getWordRangeAtPosition(position); + if (!range || range.isEmpty) { + return; + } + const oldName = document.getText(range); + if (oldName === newName) { + return; + } - var columnIndex = range.isEmpty ? position.character : range.end.character; - var cmd: proxy.ICommand = { - command: proxy.CommandType.Usages, - fileName: filename, - columnIndex: columnIndex, - lineIndex: position.line, - source: source - }; + let workspaceFolder = workspace.getWorkspaceFolder(document.uri); + if (!workspaceFolder && Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspaceFolder = workspace.workspaceFolders[0]; + } + const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; - this.jediProxyHandler.sendCommand(cmd, resolve, token); + const proxy = new RefactorProxy(workspaceRoot, async () => { + const factory = this.serviceContainer.get(IPythonExecutionFactory); + return factory.create({ resource: Uri.file(workspaceRoot) }); }); + return proxy + .rename(document, newName, document.uri.fsPath, range) + .then((response) => { + const fileDiffs = response.results.map((fileChanges) => fileChanges.diff); + const fs = this.serviceContainer.get(IFileSystem); + return getWorkspaceEditsFromPatch(fileDiffs, workspaceRoot, fs); + }) + .catch((reason) => { + if (reason === 'Not installed') { + const installer = this.serviceContainer.get(IInstaller); + installer + .promptToInstall(Product.rope, document.uri) + .catch((ex) => traceError('Python Extension: promptToInstall', ex)); + return Promise.reject(''); + } else { + window.showErrorMessage(reason); + this.outputChannel.appendLine(reason); + } + return Promise.reject(reason); + }); } } diff --git a/src/client/providers/replProvider.ts b/src/client/providers/replProvider.ts new file mode 100644 index 000000000000..5753df8d1d83 --- /dev/null +++ b/src/client/providers/replProvider.ts @@ -0,0 +1,30 @@ +import { Disposable } from 'vscode'; +import { IActiveResourceService, ICommandManager } from '../common/application/types'; +import { Commands } from '../common/constants'; +import { IServiceContainer } from '../ioc/types'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { ICodeExecutionService } from '../terminals/types'; + +export class ReplProvider implements Disposable { + private readonly disposables: Disposable[] = []; + private activeResourceService: IActiveResourceService; + constructor(private serviceContainer: IServiceContainer) { + this.activeResourceService = this.serviceContainer.get(IActiveResourceService); + this.registerCommand(); + } + public dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + private registerCommand() { + const commandManager = this.serviceContainer.get(ICommandManager); + const disposable = commandManager.registerCommand(Commands.Start_REPL, this.commandHandler, this); + this.disposables.push(disposable); + } + @captureTelemetry(EventName.REPL) + private async commandHandler() { + const resource = this.activeResourceService.getActiveResource(); + const replProvider = this.serviceContainer.get(ICodeExecutionService, 'repl'); + await replProvider.initializeRepl(resource); + } +} diff --git a/src/client/providers/serviceRegistry.ts b/src/client/providers/serviceRegistry.ts new file mode 100644 index 000000000000..66640455f08a --- /dev/null +++ b/src/client/providers/serviceRegistry.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IExtensionSingleActivationService } from '../activation/types'; +import { IServiceManager } from '../ioc/types'; +import { CodeActionProviderService } from './codeActionProvider/main'; +import { SortImportsEditingProvider } from './importSortProvider'; +import { ISortImportsEditingProvider } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(ISortImportsEditingProvider, SortImportsEditingProvider); + serviceManager.addSingleton( + IExtensionSingleActivationService, + CodeActionProviderService + ); +} diff --git a/src/client/providers/signatureProvider.ts b/src/client/providers/signatureProvider.ts index 7c8cf42f8102..df10f120b21a 100644 --- a/src/client/providers/signatureProvider.ts +++ b/src/client/providers/signatureProvider.ts @@ -1,64 +1,139 @@ -// /*--------------------------------------------------------- -// * Copyright (C) Microsoft Corporation. All rights reserved. -// *--------------------------------------------------------*/ -// -// 'use strict'; -// -// import * as vscode from 'vscode'; -// import * as proxy from './jediProxy'; -// -// var _oldName = ""; -// var _newName = ""; -// -// function parseData(data: proxy.IReferenceResult): vscode.SignatureHelp { -// if (data && data.references.length > 0) { -// var references = data.references.filter(ref=> { -// var relPath = vscode.workspace.asRelativePath(ref.fileName); -// return !relPath.startsWith(".."); -// }); -// -// var workSpaceEdit = new vscode.WorkspaceEdit(); -// references.forEach(ref=> { -// var uri = vscode.Uri.file(ref.fileName); -// var range = new vscode.Range(ref.lineIndex, ref.columnIndex, ref.lineIndex, ref.columnIndex + _oldName.length); -// workSpaceEdit.replace(uri, range, _newName); -// }); -// return workSpaceEdit; -// } -// return; -// } -// -// export class PythonSignatureHelpProvider implements vscode.SignatureHelpProvider { -// private jediProxyHandler: proxy.JediProxyHandler; -// -// public constructor(context: vscode.ExtensionContext) { -// this.jediProxyHandler = new proxy.JediProxyHandler(context, null, parseData); -// } -// -// public provideSignatureHelp(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { -// return new Promise((resolve, reject) => { -// var filename = document.fileName; -// if (document.lineAt(position.line).text.match(/^\s*\/\//)) { -// return resolve(); -// } -// if (position.character <= 0) { -// return resolve(); -// } -// //if current character is '(', then ignore it -// var txt = document.getText(new vscode.Range(new vscode.Position(position.line, position.character - 1), position)); -// if (txt !== "(") { -// return resolve(); -// } -// -// var help = new vscode.SignatureHelp(); -// var signatureInfo = new vscode.SignatureInformation("Join", "Do some joining"); -// var parameters = []; -// parameters.push(new vscode.ParameterInformation("item", "something")); -// parameters.push(new vscode.ParameterInformation("item2", "something2")); -// signatureInfo.parameters = parameters; -// -// help.activeParameter = parameters[0]; -// resolve(help); -// }); -// } -// } \ No newline at end of file +'use strict'; + +import { EOL } from 'os'; +import { + CancellationToken, + ParameterInformation, + Position, + SignatureHelp, + SignatureHelpProvider, + SignatureInformation, + TextDocument +} from 'vscode'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import * as proxy from './jediProxy'; +import { isPositionInsideStringOrComment } from './providerUtilities'; + +const DOCSTRING_PARAM_PATTERNS = [ + '\\s*:type\\s*PARAMNAME:\\s*([^\\n, ]+)', // Sphinx + '\\s*:param\\s*(\\w?)\\s*PARAMNAME:[^\\n]+', // Sphinx param with type + '\\s*@type\\s*PARAMNAME:\\s*([^\\n, ]+)' // Epydoc +]; + +/** + * Extract the documentation for parameters from a given docstring. + * @param {string} paramName Name of the parameter + * @param {string} docString The docstring for the function + * @returns {string} Docstring for the parameter + */ +function extractParamDocString(paramName: string, docString: string): string { + let paramDocString = ''; + // In docstring the '*' is escaped with a backslash + paramName = paramName.replace(new RegExp('\\*', 'g'), '\\\\\\*'); + + DOCSTRING_PARAM_PATTERNS.forEach((pattern) => { + if (paramDocString.length > 0) { + return; + } + pattern = pattern.replace('PARAMNAME', paramName); + const regExp = new RegExp(pattern); + const matches = regExp.exec(docString); + if (matches && matches.length > 0) { + paramDocString = matches[0]; + if (paramDocString.indexOf(':') >= 0) { + paramDocString = paramDocString.substring(paramDocString.indexOf(':') + 1); + } + if (paramDocString.indexOf(':') >= 0) { + paramDocString = paramDocString.substring(paramDocString.indexOf(':') + 1); + } + } + }); + + return paramDocString.trim(); +} +export class PythonSignatureProvider implements SignatureHelpProvider { + public constructor(private jediFactory: JediFactory) {} + private static parseData(data: proxy.IArgumentsResult): SignatureHelp { + if (data && Array.isArray(data.definitions) && data.definitions.length > 0) { + const signature = new SignatureHelp(); + signature.activeSignature = 0; + + data.definitions.forEach((def) => { + signature.activeParameter = def.paramindex; + // Don't display the documentation, as vs code doesn't format the documentation. + // i.e. line feeds are not respected, long content is stripped. + + // Some functions do not come with parameter docs + let label: string; + let documentation: string; + const validParamInfo = + def.params && def.params.length > 0 && def.docstring && def.docstring.startsWith(`${def.name}(`); + + if (validParamInfo) { + const docLines = def.docstring.splitLines(); + label = docLines.shift()!.trim(); + documentation = docLines.join(EOL).trim(); + } else { + if (def.params && def.params.length > 0) { + label = `${def.name}(${def.params.map((p) => p.name).join(', ')})`; + documentation = def.docstring; + } else { + label = def.description; + documentation = def.docstring; + } + } + + // tslint:disable-next-line:no-object-literal-type-assertion + const sig = { + label, + documentation, + parameters: [] + }; + + if (def.params && def.params.length) { + sig.parameters = def.params.map((arg) => { + if (arg.docstring.length === 0) { + arg.docstring = extractParamDocString(arg.name, def.docstring); + } + // tslint:disable-next-line:no-object-literal-type-assertion + return { + documentation: arg.docstring.length > 0 ? arg.docstring : arg.description, + label: arg.name.trim() + }; + }); + } + signature.signatures.push(sig); + }); + return signature; + } + + return new SignatureHelp(); + } + @captureTelemetry(EventName.SIGNATURE) + public provideSignatureHelp( + document: TextDocument, + position: Position, + token: CancellationToken + ): Thenable { + // early exit if we're in a string or comment (or in an undefined position) + if (position.character <= 0 || isPositionInsideStringOrComment(document, position)) { + return Promise.resolve(new SignatureHelp()); + } + + const cmd: proxy.ICommand = { + command: proxy.CommandType.Arguments, + fileName: document.fileName, + columnIndex: position.character, + lineIndex: position.line, + source: document.getText() + }; + return this.jediFactory + .getJediProxyHandler(document.uri) + .sendCommand(cmd, token) + .then((data) => { + return data ? PythonSignatureProvider.parseData(data) : new SignatureHelp(); + }); + } +} diff --git a/src/client/providers/simpleRefactorProvider.ts b/src/client/providers/simpleRefactorProvider.ts new file mode 100644 index 000000000000..185c22522539 --- /dev/null +++ b/src/client/providers/simpleRefactorProvider.ts @@ -0,0 +1,230 @@ +import * as vscode from 'vscode'; +import { Commands } from '../common/constants'; +import { getTextEditsFromPatch } from '../common/editor'; +import { traceError } from '../common/logger'; +import { IPythonExecutionFactory } from '../common/process/types'; +import { IInstaller, Product } from '../common/types'; +import { StopWatch } from '../common/utils/stopWatch'; +import { IServiceContainer } from '../ioc/types'; +import { RefactorProxy } from '../refactor/proxy'; +import { sendTelemetryWhenDone } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +type RenameResponse = { + results: [{ diff: string }]; +}; + +let installer: IInstaller; + +export function activateSimplePythonRefactorProvider( + context: vscode.ExtensionContext, + outputChannel: vscode.OutputChannel, + serviceContainer: IServiceContainer +) { + installer = serviceContainer.get(IInstaller); + let disposable = vscode.commands.registerCommand(Commands.Refactor_Extract_Variable, () => { + const stopWatch = new StopWatch(); + const promise = extractVariable( + vscode.window.activeTextEditor!, + vscode.window.activeTextEditor!.selection, + outputChannel, + serviceContainer + // tslint:disable-next-line:no-empty + ).catch(() => {}); + sendTelemetryWhenDone(EventName.REFACTOR_EXTRACT_VAR, promise, stopWatch); + }); + context.subscriptions.push(disposable); + + disposable = vscode.commands.registerCommand(Commands.Refactor_Extract_Method, () => { + const stopWatch = new StopWatch(); + const promise = extractMethod( + vscode.window.activeTextEditor!, + vscode.window.activeTextEditor!.selection, + outputChannel, + serviceContainer + // tslint:disable-next-line:no-empty + ).catch(() => {}); + sendTelemetryWhenDone(EventName.REFACTOR_EXTRACT_FUNCTION, promise, stopWatch); + }); + context.subscriptions.push(disposable); +} + +// Exported for unit testing +export function extractVariable( + textEditor: vscode.TextEditor, + range: vscode.Range, + outputChannel: vscode.OutputChannel, + serviceContainer: IServiceContainer + // tslint:disable-next-line:no-any +): Promise { + let workspaceFolder = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + if ( + !workspaceFolder && + Array.isArray(vscode.workspace.workspaceFolders) && + vscode.workspace.workspaceFolders.length > 0 + ) { + workspaceFolder = vscode.workspace.workspaceFolders[0]; + } + const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; + + return validateDocumentForRefactor(textEditor).then(() => { + const newName = `newvariable${new Date().getMilliseconds().toString()}`; + const proxy = new RefactorProxy(workspaceRoot, async () => { + const factory = serviceContainer.get(IPythonExecutionFactory); + return factory.create({ resource: vscode.Uri.file(workspaceRoot) }); + }); + const rename = proxy + .extractVariable( + textEditor.document, + newName, + textEditor.document.uri.fsPath, + range, + textEditor.options + ) + .then((response) => { + return response.results[0].diff; + }); + + return extractName(textEditor, newName, rename, outputChannel); + }); +} + +// Exported for unit testing +export function extractMethod( + textEditor: vscode.TextEditor, + range: vscode.Range, + outputChannel: vscode.OutputChannel, + serviceContainer: IServiceContainer + // tslint:disable-next-line:no-any +): Promise { + let workspaceFolder = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); + if ( + !workspaceFolder && + Array.isArray(vscode.workspace.workspaceFolders) && + vscode.workspace.workspaceFolders.length > 0 + ) { + workspaceFolder = vscode.workspace.workspaceFolders[0]; + } + const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; + + return validateDocumentForRefactor(textEditor).then(() => { + const newName = `newmethod${new Date().getMilliseconds().toString()}`; + const proxy = new RefactorProxy(workspaceRoot, async () => { + const factory = serviceContainer.get(IPythonExecutionFactory); + return factory.create({ resource: vscode.Uri.file(workspaceRoot) }); + }); + const rename = proxy + .extractMethod( + textEditor.document, + newName, + textEditor.document.uri.fsPath, + range, + textEditor.options + ) + .then((response) => { + return response.results[0].diff; + }); + + return extractName(textEditor, newName, rename, outputChannel); + }); +} + +// tslint:disable-next-line:no-any +function validateDocumentForRefactor(textEditor: vscode.TextEditor): Promise { + if (!textEditor.document.isDirty) { + return Promise.resolve(); + } + + // tslint:disable-next-line:no-any + return new Promise((resolve, reject) => { + vscode.window.showInformationMessage('Please save changes before refactoring', 'Save').then((item) => { + if (item === 'Save') { + textEditor.document.save().then(resolve, reject); + } else { + return reject(); + } + }); + }); +} + +function extractName( + textEditor: vscode.TextEditor, + newName: string, + renameResponse: Promise, + outputChannel: vscode.OutputChannel + // tslint:disable-next-line:no-any +): Promise { + let changeStartsAtLine = -1; + return renameResponse + .then((diff) => { + if (diff.length === 0) { + return []; + } + return getTextEditsFromPatch(textEditor.document.getText(), diff); + }) + .then((edits) => { + return textEditor.edit((editBuilder) => { + edits.forEach((edit) => { + if (changeStartsAtLine === -1 || changeStartsAtLine > edit.range.start.line) { + changeStartsAtLine = edit.range.start.line; + } + editBuilder.replace(edit.range, edit.newText); + }); + }); + }) + .then((done) => { + if (done && changeStartsAtLine >= 0) { + let newWordPosition: vscode.Position | undefined; + for (let lineNumber = changeStartsAtLine; lineNumber < textEditor.document.lineCount; lineNumber += 1) { + const line = textEditor.document.lineAt(lineNumber); + const indexOfWord = line.text.indexOf(newName); + if (indexOfWord >= 0) { + newWordPosition = new vscode.Position(line.range.start.line, indexOfWord); + break; + } + } + + if (newWordPosition) { + textEditor.selections = [ + new vscode.Selection( + newWordPosition, + new vscode.Position(newWordPosition.line, newWordPosition.character + newName.length) + ) + ]; + textEditor.revealRange( + new vscode.Range(textEditor.selection.start, textEditor.selection.end), + vscode.TextEditorRevealType.Default + ); + } + return newWordPosition; + } + return null; + }) + .then((newWordPosition) => { + if (newWordPosition) { + return textEditor.document.save().then(() => { + // Now that we have selected the new variable, lets invoke the rename command + return vscode.commands.executeCommand('editor.action.rename'); + }); + } + }) + .catch((error) => { + if (error === 'Not installed') { + installer + .promptToInstall(Product.rope, textEditor.document.uri) + .catch((ex) => traceError('Python Extension: simpleRefactorProvider.promptToInstall', ex)); + return Promise.reject(''); + } + let errorMessage = `${error}`; + if (typeof error === 'string') { + errorMessage = error; + } + if (typeof error === 'object' && error.message) { + errorMessage = error.message; + } + outputChannel.appendLine(`${'#'.repeat(10)}Refactor Output${'#'.repeat(10)}`); + outputChannel.appendLine(`Error in refactoring:\n${errorMessage}`); + vscode.window.showErrorMessage(`Cannot perform refactoring using selected element(s). (${errorMessage})`); + return Promise.reject(error); + }); +} diff --git a/src/client/providers/symbolProvider.ts b/src/client/providers/symbolProvider.ts index e108ae4570c6..07327b97f3a4 100644 --- a/src/client/providers/symbolProvider.ts +++ b/src/client/providers/symbolProvider.ts @@ -1,45 +1,202 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - 'use strict'; -import * as vscode from 'vscode'; +import { + CancellationToken, + DocumentSymbol, + DocumentSymbolProvider, + Location, + Range, + SymbolInformation, + SymbolKind, + TextDocument, + Uri +} from 'vscode'; +import { LanguageClient } from 'vscode-languageclient/node'; +import { IFileSystem } from '../common/platform/types'; +import { createDeferred, Deferred } from '../common/utils/async'; +import { IServiceContainer } from '../ioc/types'; +import { JediFactory } from '../languageServices/jediProxyFactory'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; import * as proxy from './jediProxy'; -function parseData(data: proxy.ISymbolResult): vscode.SymbolInformation[] { - if (data) { - var symbols = data.definitions.map(sym=> { - var symbol = sym.kind; - var range = new vscode.Range(sym.lineIndex, sym.columnIndex, sym.lineIndex, sym.columnIndex); - return new vscode.SymbolInformation(sym.text, symbol, range, vscode.Uri.file(sym.fileName)); - }); +function flattenSymbolTree(tree: DocumentSymbol, uri: Uri, containerName: string = ''): SymbolInformation[] { + const flattened: SymbolInformation[] = []; + + const range = new Range( + tree.range.start.line, + tree.range.start.character, + tree.range.end.line, + tree.range.end.character + ); + // For whatever reason, the values of VS Code's SymbolKind enum + // are off-by-one relative to the LSP: + // https://microsoft.github.io/language-server-protocol/specification#document-symbols-request-leftwards_arrow_with_hook + const kind: SymbolKind = tree.kind - 1; + const info = new SymbolInformation( + tree.name, + // Type coercion is a bit fuzzy when it comes to enums, so we + // play it safe by explicitly converting. + // tslint:disable-next-line:no-any + (SymbolKind as any)[(SymbolKind as any)[kind]], + containerName, + new Location(uri, range) + ); + flattened.push(info); - return symbols; + if (tree.children && tree.children.length > 0) { + // FYI: Jedi doesn't fully-qualify the container name so we + // don't bother here either. + //const fullName = `${containerName}.${tree.name}`; + for (const child of tree.children) { + const flattenedChild = flattenSymbolTree(child, uri, tree.name); + flattened.push(...flattenedChild); + } } - return; + + return flattened; } -export class PythonSymbolProvider implements vscode.DocumentSymbolProvider { - private jediProxyHandler: proxy.JediProxyHandler; +/** + * Provides Python symbols to VS Code (from the language server). + * + * See: + * https://code.visualstudio.com/docs/extensionAPI/vscode-api#DocumentSymbolProvider + */ +export class LanguageServerSymbolProvider implements DocumentSymbolProvider { + constructor(private readonly languageClient: LanguageClient) {} - public constructor(context: vscode.ExtensionContext) { - this.jediProxyHandler = new proxy.JediProxyHandler(context, null, parseData); + public async provideDocumentSymbols( + document: TextDocument, + token: CancellationToken + ): Promise { + const uri = document.uri; + const args = { textDocument: { uri: uri.toString() } }; + const raw = await this.languageClient.sendRequest('textDocument/documentSymbol', args, token); + const symbols: SymbolInformation[] = []; + for (const tree of raw) { + const flattened = flattenSymbolTree(tree, uri); + symbols.push(...flattened); + } + return Promise.resolve(symbols); } +} + +/** + * Provides Python symbols to VS Code (from Jedi). + * + * See: + * https://code.visualstudio.com/docs/extensionAPI/vscode-api#DocumentSymbolProvider + */ +export class JediSymbolProvider implements DocumentSymbolProvider { + private debounceRequest: Map }>; + private readonly fs: IFileSystem; + + public constructor( + serviceContainer: IServiceContainer, + private jediFactory: JediFactory, + private readonly debounceTimeoutMs = 500 + ) { + this.debounceRequest = new Map< + string, + { timer: NodeJS.Timer | number; deferred: Deferred } + >(); + this.fs = serviceContainer.get(IFileSystem); + } + + @captureTelemetry(EventName.SYMBOL) + public provideDocumentSymbols(document: TextDocument, token: CancellationToken): Thenable { + return this.provideDocumentSymbolsThrottled(document, token); + } + + private provideDocumentSymbolsThrottled( + document: TextDocument, + token: CancellationToken + ): Thenable { + const key = `${document.uri.fsPath}`; + if (this.debounceRequest.has(key)) { + const item = this.debounceRequest.get(key)!; + // tslint:disable-next-line: no-any + clearTimeout(item.timer as any); + item.deferred.resolve([]); + } - public provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): Thenable { - return new Promise((resolve, reject) => { - var filename = document.fileName; + const deferred = createDeferred(); + const timer: NodeJS.Timer | number = setTimeout(() => { + if (token.isCancellationRequested) { + return deferred.resolve([]); + } - var source = document.getText(); - var cmd: proxy.ICommand = { + const filename = document.fileName; + const cmd: proxy.ICommand = { command: proxy.CommandType.Symbols, fileName: filename, columnIndex: 0, - lineIndex: 0, - source: source + lineIndex: 0 }; - this.jediProxyHandler.sendCommand(cmd, resolve, token); + + if (document.isDirty) { + cmd.source = document.getText(); + } + + this.jediFactory + .getJediProxyHandler(document.uri) + .sendCommand(cmd, token) + .then((data) => this.parseData(document, data)) + .then((items) => deferred.resolve(items)) + .catch((ex) => deferred.reject(ex)); + }, this.debounceTimeoutMs); + + token.onCancellationRequested(() => { + clearTimeout(timer); + deferred.resolve([]); + this.debounceRequest.delete(key); }); + + // When a document is not saved on FS, we cannot uniquely identify it, so lets not debounce, but delay the symbol provider. + if (!document.isUntitled) { + this.debounceRequest.set(key, { timer, deferred }); + } + + return deferred.promise; + } + + // This does not appear to be used anywhere currently... + // tslint:disable-next-line:no-unused-variable + // private provideDocumentSymbolsUnthrottled(document: TextDocument, token: CancellationToken): Thenable { + // const filename = document.fileName; + + // const cmd: proxy.ICommand = { + // command: proxy.CommandType.Symbols, + // fileName: filename, + // columnIndex: 0, + // lineIndex: 0 + // }; + + // if (document.isDirty) { + // cmd.source = document.getText(); + // } + + // return this.jediFactory.getJediProxyHandler(document.uri).sendCommandNonCancellableCommand(cmd, token) + // .then(data => this.parseData(document, data)); + // } + + private parseData(document: TextDocument, data?: proxy.ISymbolResult): SymbolInformation[] { + if (data) { + const symbols = data.definitions.filter((sym) => this.fs.arePathsSame(sym.fileName, document.fileName)); + return symbols.map((sym) => { + const symbol = sym.kind; + const range = new Range( + sym.range.startLine, + sym.range.startColumn, + sym.range.endLine, + sym.range.endColumn + ); + const uri = Uri.file(sym.fileName); + const location = new Location(uri, range); + return new SymbolInformation(sym.text, symbol, sym.container, location); + }); + } + return []; } } diff --git a/src/client/providers/terminalProvider.ts b/src/client/providers/terminalProvider.ts new file mode 100644 index 000000000000..15f1241d88ce --- /dev/null +++ b/src/client/providers/terminalProvider.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, Terminal } from 'vscode'; +import { IActiveResourceService, ICommandManager } from '../common/application/types'; +import { Commands } from '../common/constants'; +import { ITerminalActivator, ITerminalServiceFactory } from '../common/terminal/types'; +import { IConfigurationService } from '../common/types'; +import { swallowExceptions } from '../common/utils/decorators'; +import { IServiceContainer } from '../ioc/types'; +import { captureTelemetry, sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +export class TerminalProvider implements Disposable { + private disposables: Disposable[] = []; + private activeResourceService: IActiveResourceService; + constructor(private serviceContainer: IServiceContainer) { + this.registerCommands(); + this.activeResourceService = this.serviceContainer.get(IActiveResourceService); + } + + @swallowExceptions('Failed to initialize terminal provider') + public async initialize(currentTerminal: Terminal | undefined) { + const configuration = this.serviceContainer.get(IConfigurationService); + const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource()); + + if (currentTerminal && pythonSettings.terminal.activateEnvInCurrentTerminal) { + const hideFromUser = + 'hideFromUser' in currentTerminal.creationOptions && currentTerminal.creationOptions.hideFromUser; + if (!hideFromUser) { + const terminalActivator = this.serviceContainer.get(ITerminalActivator); + await terminalActivator.activateEnvironmentInTerminal(currentTerminal, { preserveFocus: true }); + } + sendTelemetryEvent(EventName.ACTIVATE_ENV_IN_CURRENT_TERMINAL, undefined, { + isTerminalVisible: !hideFromUser + }); + } + } + public dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + private registerCommands() { + const commandManager = this.serviceContainer.get(ICommandManager); + const disposable = commandManager.registerCommand(Commands.Create_Terminal, this.onCreateTerminal, this); + + this.disposables.push(disposable); + } + @captureTelemetry(EventName.TERMINAL_CREATE, { triggeredBy: 'commandpalette' }) + private async onCreateTerminal() { + const terminalService = this.serviceContainer.get(ITerminalServiceFactory); + const activeResource = this.activeResourceService.getActiveResource(); + await terminalService.createTerminalService(activeResource, 'Python').show(false); + } +} diff --git a/src/client/providers/testprovider.ts b/src/client/providers/testprovider.ts deleted file mode 100644 index 689c2149677a..000000000000 --- a/src/client/providers/testprovider.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -'use strict'; -import * as vscode from 'vscode'; -import * as baseTest from './../unittest/baseTestRunner'; -import * as unittest from './../unittest/unittest'; -import * as nosetest from './../unittest/nosetests'; -import * as settings from './../common/configSettings'; - -let pythonOutputChannel: vscode.OutputChannel; -let testProviders: baseTest.BaseTestRunner[] = []; - -export function activateUnitTestProvider(context: vscode.ExtensionContext, settings: settings.IPythonSettings, outputChannel: vscode.OutputChannel) { - pythonOutputChannel = outputChannel; - vscode.commands.registerCommand("python.runtests", () => runUnitTests()); - // - // vscode.commands.registerTextEditorCommand("extension.paython.runCurrentTest", (textEditor) => { - // runUnitTests(textEditor.document.fileName); - // }); - - testProviders.push(new unittest.PythonUnitTest(settings, outputChannel)); - testProviders.push(new nosetest.NoseTests(settings, outputChannel)); -} - -function runUnitTests(filePath: string = "") { - pythonOutputChannel.clear(); - - var promise = []; - testProviders.forEach(t=> { - promise.push(t.runTests(filePath)); - }); - - Promise.all(promise).then(() => { - pythonOutputChannel.show(); - }) -} \ No newline at end of file diff --git a/src/client/providers/types.ts b/src/client/providers/types.ts new file mode 100644 index 000000000000..f2d1bc6eea3a --- /dev/null +++ b/src/client/providers/types.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { CancellationToken, Uri, WorkspaceEdit } from 'vscode'; + +export const ISortImportsEditingProvider = Symbol('ISortImportsEditingProvider'); +export interface ISortImportsEditingProvider { + provideDocumentSortImportsEdits(uri: Uri, token?: CancellationToken): Promise; + sortImports(uri?: Uri): Promise; + registerCommands(): void; +} diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts new file mode 100644 index 000000000000..11398217a512 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { Architecture } from '../../../common/utils/platform'; +import { BasicVersionInfo, VersionInfo } from '../../../common/utils/version'; + +/** + * IDs for the various supported Python environments. + */ +export enum PythonEnvKind { + Unknown = 'unknown', + // "global" + System = 'global-system', + MacDefault = 'global-mac-default', + WindowsStore = 'global-windows-store', + Pyenv = 'global-pyenv', + CondaBase = 'global-conda-base', + Custom = 'global-custom', + OtherGlobal = 'global-other', + // "virtual" + Venv = 'virt-venv', + VirtualEnv = 'virt-virtualenv', + Pipenv = 'virt-pipenv', + Conda = 'virt-conda', + OtherVirtual = 'virt-other' +} + +/** + * Information about a Python binary/executable. + */ +export type PythonExecutableInfo = { + filename: string; + sysPrefix: string; + ctime: number; + mtime: number; +}; + +/** + * A (system-global) unique ID for a single Python environment. + */ +export type PythonEnvID = string; + +/** + * The most fundamental information about a Python environment. + * + * You should expect these objects to be complete (no empty props). + * Note that either `name` or `location` must be non-empty, though + * the other *can* be empty. + * + * @prop id - the env's unique ID + * @prop kind - the env's kind + * @prop executable - info about the env's Python binary + * @prop name - the env's distro-specific name, if any + * @prop location - the env's location (on disk), if relevant + */ +export type PythonEnvBaseInfo = { + id: PythonEnvID; + kind: PythonEnvKind; + executable: PythonExecutableInfo; + // One of (name, location) must be non-empty. + name: string; + location: string; + // Other possible fields: + // * managed: boolean (if the env is "managed") + // * parent: PythonEnvBaseInfo (the env from which this one was created) + // * binDir: string (where env-installed executables are found) +}; + +/** + * The possible Python release levels. + */ +export enum PythonReleaseLevel { + Alpha = 'alpha', + Beta = 'beta', + Candidate = 'candidate', + Final = 'final' +} + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + level: PythonReleaseLevel; + serial: number; +}; + +/** + * Version information for a Python build/installation. + * + * @prop sysVersion - the raw text from `sys.version` + */ +export type PythonVersion = BasicVersionInfo & { + release: PythonVersionRelease; + sysVersion?: string; +}; + +/** + * Information for a Python build/installation. + */ +export type PythonBuildInfo = { + version: PythonVersion; // incl. raw, AKA sys.version + arch: Architecture; +}; + +/** + * Meta information about a Python distribution. + * + * @prop org - the name of the distro's creator/publisher + * @prop defaultDisplayName - the text to use when showing the distro to users + */ +export type PythonDistroMetaInfo = { + org: string; + defaultDisplayName?: string; +}; + +/** + * Information about an installed Python distribution. + * + * @prop version - the installed *distro* version (not the Python version) + * @prop binDir - where to look for the distro's executables (i.e. tools) + */ +export type PythonDistroInfo = PythonDistroMetaInfo & { + version?: VersionInfo; + binDir?: string; +}; + +type _PythonEnvInfo = PythonEnvBaseInfo & PythonBuildInfo; + +/** + * All the available information about a Python environment. + * + * Note that not all the information will necessarily be filled in. + * Locators are only required to fill in the "base" info, though + * they will usually be able to provide the version as well. + * + * @prop distro - the installed Python distro that this env is using or belongs to + * @prop defaultDisplayName - the text to use when showing the env to users + * @prop searchLocation - the root under which a locator found this env, if any + */ +export type PythonEnvInfo = _PythonEnvInfo & { + distro: PythonDistroInfo; + defaultDisplayName?: string; + searchLocation?: Uri; +}; diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts new file mode 100644 index 000000000000..618f4b1fc0eb --- /dev/null +++ b/src/client/pythonEnvironments/base/locator.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event, Uri } from 'vscode'; +import { iterEmpty } from '../../common/utils/async'; +import { PythonEnvInfo, PythonEnvKind } from './info'; +import { BasicPythonEnvsChangedEvent, IPythonEnvsWatcher, PythonEnvsChangedEvent, PythonEnvsWatcher } from './watcher'; + +/** + * An async iterator of `PythonEnvInfo`. + */ +export type PythonEnvsIterator = AsyncIterator; + +/** + * An empty Python envs iterator. + */ +export const NOOP_ITERATOR: PythonEnvsIterator = iterEmpty(); + +/** + * The most basic info to send to a locator when requesting environments. + * + * This is directly correlated with the `BasicPythonEnvsChangedEvent` + * emitted by watchers. + * + * @prop kinds - if provided, results should be limited to these env kinds + */ +export type BasicPythonLocatorQuery = { + kinds?: PythonEnvKind[]; +}; + +/** + * The full set of possible info to send to a locator when requesting environments. + * + * This is directly correlated with the `PythonEnvsChangedEvent` + * emitted by watchers. + * + * @prop - searchLocations - if provided, results should be limited to + * within these locations + */ +export type PythonLocatorQuery = BasicPythonLocatorQuery & { + searchLocations?: Uri[]; +}; + +type QueryForEvent = E extends PythonEnvsChangedEvent ? PythonLocatorQuery : BasicPythonLocatorQuery; + +/** + * A single Python environment locator. + * + * Each locator object is responsible for identifying the Python + * environments in a single location, whether a directory, a directory + * tree, or otherwise. That location is identified when the locator + * is instantiated. + * + * Based on the narrow focus of each locator, the assumption is that + * calling iterEnvs() to pick up a changed env is effectively no more + * expensive than tracking down that env specifically. Consequently, + * events emitted via `onChanged` do not need to provide information + * for the specific environments that changed. + */ +export interface ILocator + extends IPythonEnvsWatcher { + /** + * Iterate over the enviroments known tos this locator. + * + * Locators are not required to have provide all info about + * an environment. However, each yielded item will at least + * include all the `PythonEnvBaseInfo` data. + * + * @param query - if provided, the locator will limit results to match + */ + iterEnvs(query?: QueryForEvent): PythonEnvsIterator; + + /** + * Find the given Python environment and fill in as much missing info as possible. + * + * If the locator can find the environment then the result is as + * much info about that env as the locator has. At the least this + * will include all the `PythonEnvBaseInfo` data. If a `PythonEnvInfo` + * was provided then the result will be a copy with any updates or + * extra info applied. + * + * If the locator could not find the environment then `undefined` + * is returned. + * + * @param env - the Python executable path or partial env info to find and update + */ + resolveEnv(env: string | PythonEnvInfo): Promise; +} + +interface IEmitter { + fire(e: E): void; +} + +/** + * The generic base for Python envs locators. + * + * By default `resolveEnv()` returns undefined. Subclasses may override + * the method to provide an implementation. + * + * Subclasses will call `this.emitter.fire()` to emit events. + * + * Also, in most cases the default event type (`PythonEnvsChangedEvent`) + * should be used. Only in low-level cases should you consider using + * `BasicPythonEnvsChangedEvent`. + */ +export abstract class LocatorBase implements ILocator { + public readonly onChanged: Event; + protected readonly emitter: IEmitter; + constructor(watcher: IPythonEnvsWatcher & IEmitter) { + this.emitter = watcher; + this.onChanged = watcher.onChanged; + } + + public abstract iterEnvs(query?: QueryForEvent): PythonEnvsIterator; + + public async resolveEnv(_env: string | PythonEnvInfo): Promise { + return undefined; + } +} + +/** + * The base for most Python envs locators. + * + * By default `resolveEnv()` returns undefined. Subclasses may override + * the method to provide an implementation. + * + * Subclasses will call `this.emitter.fire()` * to emit events. + * + * In most cases this is the class you will want to subclass. + * Only in low-level cases should you consider subclassing `LocatorBase` + * using `BasicPythonEnvsChangedEvent. + */ +export abstract class Locator extends LocatorBase { + constructor() { + super(new PythonEnvsWatcher()); + } +} diff --git a/src/client/pythonEnvironments/base/locators.ts b/src/client/pythonEnvironments/base/locators.ts new file mode 100644 index 000000000000..50169bfa590f --- /dev/null +++ b/src/client/pythonEnvironments/base/locators.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { chain } from '../../common/utils/async'; +import { PythonEnvInfo } from './info'; +import { ILocator, NOOP_ITERATOR, PythonEnvsIterator, PythonLocatorQuery } from './locator'; +import { DisableableEnvsWatcher, PythonEnvsWatchers } from './watchers'; + +/** + * A wrapper around a set of locators, exposing them as a single locator. + * + * Events and iterator results are combined. + */ +export class Locators extends PythonEnvsWatchers implements ILocator { + constructor( + // The locators will be watched as well as iterated. + private readonly locators: ReadonlyArray + ) { + super(locators); + } + + public iterEnvs(query?: PythonLocatorQuery): PythonEnvsIterator { + const iterators = this.locators.map((loc) => loc.iterEnvs(query)); + return chain(iterators); + } + + public async resolveEnv(env: string | PythonEnvInfo): Promise { + for (const locator of this.locators) { + const resolved = await locator.resolveEnv(env); + if (resolved !== undefined) { + return resolved; + } + } + return undefined; + } +} + +/** + * A locator wrapper that can be disabled. + * + * If disabled, events emitted by the wrapped locator are discarded, + * `iterEnvs()` yields nothing, and `resolveEnv()` already returns + * `undefined`. + */ +export class DisableableLocator extends DisableableEnvsWatcher implements ILocator { + constructor( + // To wrapp more than one use `Locators`. + private readonly locator: ILocator + ) { + super(locator); + } + + public iterEnvs(query?: PythonLocatorQuery): PythonEnvsIterator { + if (!this.enabled) { + return NOOP_ITERATOR; + } + return this.locator.iterEnvs(query); + } + + public async resolveEnv(env: string | PythonEnvInfo): Promise { + if (!this.enabled) { + return undefined; + } + return this.locator.resolveEnv(env); + } +} diff --git a/src/client/pythonEnvironments/base/watcher.ts b/src/client/pythonEnvironments/base/watcher.ts new file mode 100644 index 000000000000..c64a139d31c2 --- /dev/null +++ b/src/client/pythonEnvironments/base/watcher.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-classes-per-file + +import { Event, EventEmitter, Uri } from 'vscode'; +import { PythonEnvKind } from './info'; + +// The use cases for `BasicPythonEnvsChangedEvent` are currently +// hypothetical. However, there's a real chance they may prove +// useful for the concrete low-level locators. So for now we are +// keeping the separate "basic" type. + +/** + * The most basic info for a Python environments event. + * + * @prop kind - the env kind, if any, affected by the event + */ +export type BasicPythonEnvsChangedEvent = { + kind?: PythonEnvKind; +}; + +/** + * The full set of possible info for a Python environments event. + * + * @prop searchLocation - the location, if any, affected by the event + */ +export type PythonEnvsChangedEvent = BasicPythonEnvsChangedEvent & { + searchLocation?: Uri; +}; + +/** + * A "watcher" for events related to changes to Python environemts. + * + * The watcher will notify listeners (callbacks registered through + * `onChanged`) of events at undetermined times. The actual emitted + * events, their source, and the timing is entirely up to the watcher + * implementation. + */ +export interface IPythonEnvsWatcher { + /** + * The hook for registering event listeners (callbacks). + */ + readonly onChanged: Event; +} + +/** + * This provides the fundamental functionality of a Python envs watcher. + * + * Consumers register listeners (callbacks) using `onChanged`. Each + * listener is invoked when `fire()` is called. + * + * Note that in most cases classes will not inherit from this class, + * but instead keep a private watcher property. The rule of thumb + * is to follow whether or not consumers of *that* class should be able + * to trigger events (via `fire()`). + * + * Also, in most cases the default event type (`PythonEnvsChangedEvent`) + * should be used. Only in low-level cases should you consider using + * `BasicPythonEnvsChangedEvent`. + */ +export class PythonEnvsWatcher implements IPythonEnvsWatcher { + /** + * The hook for registering event listeners (callbacks). + */ + public readonly onChanged: Event; + private readonly didChange = new EventEmitter(); + + constructor() { + this.onChanged = this.didChange.event; + } + + /** + * Send the event to all registered listeners. + */ + public fire(event: T) { + this.didChange.fire(event); + } +} diff --git a/src/client/pythonEnvironments/base/watchers.ts b/src/client/pythonEnvironments/base/watchers.ts new file mode 100644 index 000000000000..860658a4fad2 --- /dev/null +++ b/src/client/pythonEnvironments/base/watchers.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, Event } from 'vscode'; +import { IPythonEnvsWatcher, PythonEnvsChangedEvent, PythonEnvsWatcher } from './watcher'; + +/** + * A wrapper around a set of watchers, exposing them as a single watcher. + * + * If any of the wrapped watchers emits an event then this wrapper + * emits that event. + */ +export class PythonEnvsWatchers implements IPythonEnvsWatcher { + public readonly onChanged: Event; + private watcher = new PythonEnvsWatcher(); + + constructor(watchers: ReadonlyArray) { + this.onChanged = this.watcher.onChanged; + watchers.forEach((w) => { + w.onChanged((e) => this.watcher.fire(e)); + }); + } +} + +// This matches the `vscode.Event` arg. +type EnvsEventListener = (e: PythonEnvsChangedEvent) => unknown; + +/** + * A watcher wrapper that can be disabled. + * + * If disabled, events emitted by the wrapped watcher are discarded. + */ +export class DisableableEnvsWatcher implements IPythonEnvsWatcher { + protected enabled = true; + constructor( + // To wrap more than one use `PythonEnvWatchers`. + private readonly wrapped: IPythonEnvsWatcher + ) {} + + /** + * Ensure that the watcher is enabled. + */ + public enable() { + this.enabled = true; + } + + /** + * Ensure that the watcher is disabled. + */ + public disable() { + this.enabled = false; + } + + // This matches the signature of `vscode.Event`. + public onChanged(listener: EnvsEventListener, thisArgs?: unknown, disposables?: Disposable[]): Disposable { + return this.wrapped.onChanged( + (e: PythonEnvsChangedEvent) => { + if (this.enabled) { + listener(e); + } + }, + thisArgs, + disposables + ); + } +} diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts new file mode 100644 index 000000000000..9f0c90cb017e --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fsapi from 'fs-extra'; +import * as path from 'path'; +import { createDeferred } from '../../common/utils/async'; +import { isWindowsStoreEnvironment } from '../discovery/locators/services/windowsStoreLocator'; +import { EnvironmentType } from '../info'; + +function pathExists(absPath: string): Promise { + const deferred = createDeferred(); + fsapi.exists(absPath, (result) => { + deferred.resolve(result); + }); + return deferred.promise; +} + +/** + * Checks if the given interpreter path belongs to a conda environment. Using + * known folder layout, and presence of 'conda-meta' directory. + * @param {string} interpreterPath: Absolute path to any python interpreter. + * + * Remarks: This is what we will use to begin with. Another approach we can take + * here is to parse ~/.conda/environments.txt. This file will have list of conda + * environments. We can compare the interpreter path against the paths in that file. + * We don't want to rely on this file because it is an implementation detail of + * conda. If it turns out that the layout based identification is not sufficient + * that is the next alternative that is cheap. + * + * sample content of the ~/.conda/environments.txt: + * C:\envs\\myenv + * C:\ProgramData\Miniconda3 + * + * Yet another approach is to use `conda env list --json` and compare the returned env + * list to see if the given interpreter path belongs to any of the returned environments. + * This approach is heavy, and involves running a binary. For now we decided not to + * take this approach, since it does not look like we need it. + * + * sample output from `conda env list --json`: + * conda env list --json + * { + * "envs": [ + * "C:\\envs\\myenv", + * "C:\\ProgramData\\Miniconda3" + * ] + * } + */ +async function isCondaEnvironment(interpreterPath: string): Promise { + const condaMetaDir = 'conda-meta'; + + // Check if the conda-meta directory is in the same directory as the interpreter. + // This layout is common in Windows. + // env + // |__ conda-meta <--- check if this directory exists + // |__ python.exe <--- interpreterPath + const condaEnvDir1 = path.join(path.dirname(interpreterPath), condaMetaDir); + + // Check if the conda-meta directory is in the parent directory relative to the interpreter. + // This layout is common on linux/Mac. + // env + // |__ conda-meta <--- check if this directory exists + // |__ bin + // |__ python <--- interpreterPath + const condaEnvDir2 = path.join(path.dirname(path.dirname(interpreterPath)), condaMetaDir); + + return [await pathExists(condaEnvDir1), await pathExists(condaEnvDir2)].includes(true); +} + +/** + * Returns environment type. + * @param {string} interpreterPath : Absolute path to the python interpreter binary. + * @returns {EnvironmentType} + * + * Remarks: This is the order of detection based on how the various distributions and tools + * configure the environment, and the fall back for identification. + * Top level we have the following environment types, since they leave a unique signature + * in the environment or * use a unique path for the environments they create. + * 1. Conda + * 2. Windows Store + * 3. PipEnv + * 4. Pyenv + * 5. Poetry + * + * Next level we have the following virtual environment tools. The are here because they + * are consumed by the tools above, and can also be used independently. + * 1. venv + * 2. virtualenvwrapper + * 3. virtualenv + * + * Last category is globally installed python, or system python. + */ +export async function identifyEnvironment(interpreterPath: string): Promise { + if (await isCondaEnvironment(interpreterPath)) { + return EnvironmentType.Conda; + } + + if (await isWindowsStoreEnvironment(interpreterPath)) { + return EnvironmentType.WindowsStore; + } + + // additional identifiers go here + + return EnvironmentType.Unknown; +} diff --git a/src/client/pythonEnvironments/common/externalDependencies.ts b/src/client/pythonEnvironments/common/externalDependencies.ts new file mode 100644 index 000000000000..6383b3d3e515 --- /dev/null +++ b/src/client/pythonEnvironments/common/externalDependencies.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ExecutionResult, IProcessServiceFactory } from '../../common/process/types'; +import { IServiceContainer } from '../../ioc/types'; + +let internalServiceContainer: IServiceContainer; +export function initializeExternalDependencies(serviceContainer: IServiceContainer): void { + internalServiceContainer = serviceContainer; +} + +function getProcessFactory(): IProcessServiceFactory { + return internalServiceContainer.get(IProcessServiceFactory); +} + +export async function shellExecute(command: string, timeout: number): Promise> { + const proc = await getProcessFactory().create(); + return proc.shellExec(command, { timeout }); +} diff --git a/src/client/pythonEnvironments/common/windowsUtils.ts b/src/client/pythonEnvironments/common/windowsUtils.ts new file mode 100644 index 000000000000..d9be7d78fdf9 --- /dev/null +++ b/src/client/pythonEnvironments/common/windowsUtils.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; + +/** + * Checks if a given path ends with python*.exe + * @param {string} interpreterPath : Path to python interpreter. + * @returns {boolean} : Returns true if the path matches pattern for windows python executable. + */ +export function isWindowsPythonExe(interpreterPath:string): boolean { + /** + * This Reg-ex matches following file names: + * python.exe + * python3.exe + * python38.exe + * python3.8.exe + */ + const windowsPythonExes = /^python(\d+(.\d+)?)?\.exe$/; + + return windowsPythonExes.test(path.basename(interpreterPath)); +} diff --git a/src/client/pythonEnvironments/discovery/globalenv.ts b/src/client/pythonEnvironments/discovery/globalenv.ts new file mode 100644 index 000000000000..7859b174da38 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/globalenv.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { logVerbose } from '../../logging'; +import { EnvironmentType } from '../info'; + +type ExecFunc = (cmd: string, args: string[]) => Promise<{ stdout: string }>; + +type TypeFinderFunc = (python: string) => Promise; +type RootFinderFunc = () => Promise; + +/** + * Build a "type finder" function that identifies pyenv environments. + * + * @param homedir - the user's home directory (e.g. `$HOME`) + * @param pathSep - the path separator to use (typically `path.sep`) + * @param pathJoin - typically `path.join` + * @param getEnvVar - a function to look up a process environment variable (i,e. `process.env[name]`) + * @param exec - the function to use to run pyenv + */ +export function getPyenvTypeFinder( + homedir: string, + // + pathSep: string, + pathJoin: (...parts: string[]) => string, + // + getEnvVar: (name: string) => string | undefined, + exec: ExecFunc, +): TypeFinderFunc { + const find = getPyenvRootFinder(homedir, pathJoin, getEnvVar, exec); + return async (python) => { + const root = await find(); + if (root && python.startsWith(`${root}${pathSep}`)) { + return EnvironmentType.Pyenv; + } + return undefined; + }; +} + +/** + * Build a "root finder" function that finds pyenv environments. + * + * @param homedir - the user's home directory (e.g. `$HOME`) + * @param pathJoin - typically `path.join` + * @param getEnvVar - a function to look up a process environment variable (i,e. `process.env[name]`) + * @param exec - the function to use to run pyenv + */ +export function getPyenvRootFinder( + homedir: string, + pathJoin: (...parts: string[]) => string, + getEnvVar: (name: string) => string | undefined, + exec: ExecFunc, +): RootFinderFunc { + return async () => { + const root = getEnvVar('PYENV_ROOT'); + if (root /* ...or empty... */) { + return root; + } + + try { + const result = await exec('pyenv', ['root']); + const text = result.stdout.trim(); + if (text.length > 0) { + return text; + } + } catch (err) { + // Ignore the error. + logVerbose(`"pyenv root" failed (${err})`); + } + return pathJoin(homedir, '.pyenv'); + }; +} diff --git a/src/client/pythonEnvironments/discovery/index.ts b/src/client/pythonEnvironments/discovery/index.ts new file mode 100644 index 000000000000..6529b65d59c6 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/index.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Decide if the given Python executable looks like the MacOS default Python. + */ +export function isMacDefaultPythonPath(pythonPath: string) { + return pythonPath === 'python' || pythonPath === '/usr/bin/python'; +} diff --git a/src/client/pythonEnvironments/discovery/locators/helpers.ts b/src/client/pythonEnvironments/discovery/locators/helpers.ts new file mode 100644 index 000000000000..8b72b92b0e25 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/helpers.ts @@ -0,0 +1,101 @@ +import * as fsapi from 'fs-extra'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { traceError } from '../../../common/logger'; +import { IS_WINDOWS } from '../../../common/platform/constants'; +import { IFileSystem } from '../../../common/platform/types'; +import { IInterpreterLocatorHelper } from '../../../interpreter/contracts'; +import { IPipEnvServiceHelper } from '../../../interpreter/locators/types'; +import { EnvironmentType, PythonEnvironment } from '../../info'; + +const CheckPythonInterpreterRegEx = IS_WINDOWS ? /^python(\d+(.\d+)?)?\.exe$/ : /^python(\d+(.\d+)?)?$/; + +export async function lookForInterpretersInDirectory(pathToCheck: string, _: IFileSystem): Promise { + // Technically, we should be able to use fs.getFiles(). However, + // that breaks some tests. So we stick with the broader behavior. + try { + // tslint:disable-next-line: no-suspicious-comment + // TODO https://github.com/microsoft/vscode-python/issues/11338 + const files = await fsapi.readdir(pathToCheck); + return files + .map((filename) => path.join(pathToCheck, filename)) + .filter((fileName) => CheckPythonInterpreterRegEx.test(path.basename(fileName))); + } catch (err) { + traceError('Python Extension (lookForInterpretersInDirectory.fs.readdir):', err); + return [] as string[]; + } +} + +@injectable() +export class InterpreterLocatorHelper implements IInterpreterLocatorHelper { + constructor( + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IPipEnvServiceHelper) private readonly pipEnvServiceHelper: IPipEnvServiceHelper, + ) {} + + public async mergeInterpreters(interpreters: PythonEnvironment[]): Promise { + const items = interpreters + .map((item) => ({ ...item })) + .map((item) => { + item.path = path.normalize(item.path); + return item; + }) + .reduce((accumulator, current) => { + const currentVersion = current && current.version ? current.version.raw : undefined; + const existingItem = accumulator.find((item) => { + // If same version and same base path, then ignore. + // Could be Python 3.6 with path = python.exe, and Python 3.6 and path = python3.exe. + if ( + item.version + && item.version.raw === currentVersion + && item.path + && current.path + && this.fs.arePathsSame(path.dirname(item.path), path.dirname(current.path)) + ) { + return true; + } + return false; + }); + if (!existingItem) { + accumulator.push(current); + } else { + // Preserve type information. + // Possible we identified environment as unknown, but a later provider has identified env type. + if ( + existingItem.envType === EnvironmentType.Unknown + && current.envType !== EnvironmentType.Unknown + ) { + existingItem.envType = current.envType; + } + const props: (keyof PythonEnvironment)[] = [ + 'envName', + 'envPath', + 'path', + 'sysPrefix', + 'architecture', + 'sysVersion', + 'version', + ]; + for (const prop of props) { + if (!existingItem[prop] && current[prop]) { + // tslint:disable-next-line: no-any + (existingItem as any)[prop] = current[prop]; + } + } + } + return accumulator; + }, []); + // This stuff needs to be fast. + await Promise.all( + items.map(async (item) => { + const info = await this.pipEnvServiceHelper.getPipEnvInfo(item.path); + if (info) { + item.envType = EnvironmentType.Pipenv; + item.pipEnvWorkspaceFolder = info.workspaceFolder.fsPath; + item.envName = info.envName || item.envName; + } + }), + ); + return items; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/index.ts b/src/client/pythonEnvironments/discovery/locators/index.ts new file mode 100644 index 000000000000..63db14647c79 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/index.ts @@ -0,0 +1,140 @@ +import { inject, injectable } from 'inversify'; +import { + Disposable, Event, EventEmitter, Uri, +} from 'vscode'; +import { traceDecorators } from '../../../common/logger'; +import { IPlatformService } from '../../../common/platform/types'; +import { IDisposableRegistry } from '../../../common/types'; +import { createDeferred, Deferred } from '../../../common/utils/async'; +import { OSType } from '../../../common/utils/platform'; +import { + CONDA_ENV_FILE_SERVICE, + CONDA_ENV_SERVICE, + CURRENT_PATH_SERVICE, + GLOBAL_VIRTUAL_ENV_SERVICE, + IInterpreterLocatorHelper, + IInterpreterLocatorService, + KNOWN_PATH_SERVICE, + PIPENV_SERVICE, + WINDOWS_REGISTRY_SERVICE, + WORKSPACE_VIRTUAL_ENV_SERVICE, +} from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import { PythonEnvironment } from '../../info'; +import { isHiddenInterpreter } from './services/interpreterFilter'; +import { GetInterpreterLocatorOptions } from './types'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); + +/** + * Facilitates locating Python interpreters. + */ +@injectable() +export class PythonInterpreterLocatorService implements IInterpreterLocatorService { + public didTriggerInterpreterSuggestions: boolean; + + private readonly disposables: Disposable[] = []; + + private readonly platform: IPlatformService; + + private readonly interpreterLocatorHelper: IInterpreterLocatorHelper; + + private readonly _hasInterpreters: Deferred; + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this._hasInterpreters = createDeferred(); + serviceContainer.get(IDisposableRegistry).push(this); + this.platform = serviceContainer.get(IPlatformService); + this.interpreterLocatorHelper = serviceContainer.get(IInterpreterLocatorHelper); + this.didTriggerInterpreterSuggestions = false; + } + + /** + * This class should never emit events when we're locating. + * The events will be fired by the indivitual locators retrieved in `getLocators`. + * + * @readonly + * @type {Event>} + * @memberof PythonInterpreterLocatorService + */ + public get onLocating(): Event> { + return new EventEmitter>().event; + } + + public get hasInterpreters(): Promise { + return this._hasInterpreters.completed ? this._hasInterpreters.promise : Promise.resolve(false); + } + + /** + * Release any held resources. + * + * Called by VS Code to indicate it is done with the resource. + */ + public dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + + /** + * Return the list of known Python interpreters. + * + * The optional resource arg may control where locators look for + * interpreters. + */ + @traceDecorators.verbose('Get Interpreters') + public async getInterpreters(resource?: Uri, options?: GetInterpreterLocatorOptions): Promise { + const locators = this.getLocators(options); + const promises = locators.map(async (provider) => provider.getInterpreters(resource)); + locators.forEach((locator) => { + locator.hasInterpreters + .then((found) => { + if (found) { + this._hasInterpreters.resolve(true); + } + }) + .ignoreErrors(); + }); + const listOfInterpreters = await Promise.all(promises); + + const items = flatten(listOfInterpreters) + .filter((item) => !!item) + .map((item) => item!) + .filter((item) => !isHiddenInterpreter(item)); + this._hasInterpreters.resolve(items.length > 0); + return this.interpreterLocatorHelper.mergeInterpreters(items); + } + + /** + * Return the list of applicable interpreter locators. + * + * The locators are pulled from the registry. + */ + private getLocators(options?: GetInterpreterLocatorOptions): IInterpreterLocatorService[] { + // The order of the services is important. + // The order is important because the data sources at the bottom of the list do not contain all, + // the information about the interpreters (e.g. type, environment name, etc). + // This way, the items returned from the top of the list will win, when we combine the items returned. + const keys: [string, OSType | undefined][] = [ + [WINDOWS_REGISTRY_SERVICE, OSType.Windows], + [CONDA_ENV_SERVICE, undefined], + [CONDA_ENV_FILE_SERVICE, undefined], + [PIPENV_SERVICE, undefined], + [GLOBAL_VIRTUAL_ENV_SERVICE, undefined], + [WORKSPACE_VIRTUAL_ENV_SERVICE, undefined], + [KNOWN_PATH_SERVICE, undefined], + [CURRENT_PATH_SERVICE, undefined], + ]; + + const locators = keys + .filter((item) => item[1] === undefined || item[1] === this.platform.osType) + .map((item) => this.serviceContainer.get(IInterpreterLocatorService, item[0])); + + // Set it to true the first time the user selects an interpreter + if (!this.didTriggerInterpreterSuggestions && options?.onSuggestion === true) { + this.didTriggerInterpreterSuggestions = true; + locators.forEach((locator) => (locator.didTriggerInterpreterSuggestions = true)); + } + + return locators; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/progressService.ts b/src/client/pythonEnvironments/discovery/locators/progressService.ts new file mode 100644 index 000000000000..f261fe858227 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/progressService.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable, Event, EventEmitter } from 'vscode'; +import { traceDecorators } from '../../../common/logger'; +import { IDisposableRegistry } from '../../../common/types'; +import { createDeferredFrom, Deferred } from '../../../common/utils/async'; +import { noop } from '../../../common/utils/misc'; +import { IInterpreterLocatorProgressService, IInterpreterLocatorService } from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import { PythonEnvironment } from '../../info'; + +@injectable() +export class InterpreterLocatorProgressService implements IInterpreterLocatorProgressService { + private deferreds: Deferred[] = []; + + private readonly refreshing = new EventEmitter(); + + private readonly refreshed = new EventEmitter(); + + private readonly locators: IInterpreterLocatorService[] = []; + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IDisposableRegistry) private readonly disposables: Disposable[], + ) { + this.locators = serviceContainer.getAll(IInterpreterLocatorService); + } + + public get onRefreshing(): Event { + return this.refreshing.event; + } + + public get onRefreshed(): Event { + return this.refreshed.event; + } + + public register(): void { + this.locators.forEach((locator) => { + locator.onLocating(this.handleProgress, this, this.disposables); + }); + } + + @traceDecorators.verbose('Detected refreshing of Interpreters') + private handleProgress(promise: Promise) { + this.deferreds.push(createDeferredFrom(promise)); + this.notifyRefreshing(); + this.checkProgress(); + } + + @traceDecorators.verbose('All locators have completed locating') + private notifyCompleted() { + this.refreshed.fire(); + } + + @traceDecorators.verbose('Notify locators are locating') + private notifyRefreshing() { + this.refreshing.fire(); + } + + private checkProgress() { + if (this.deferreds.length === 0) { + return; + } + if (this.areAllItemsComplete()) { + return this.notifyCompleted(); + } + Promise.all(this.deferreds.map((item) => item.promise)) + .catch(noop) + .then(() => this.checkProgress()) + .ignoreErrors(); + } + + @traceDecorators.verbose('Checking whether locactors have completed locating') + private areAllItemsComplete() { + this.deferreds = this.deferreds.filter((item) => !item.completed); + return this.deferreds.length === 0; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/KnownPathsService.ts b/src/client/pythonEnvironments/discovery/locators/services/KnownPathsService.ts new file mode 100644 index 000000000000..40a83ededdb2 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/KnownPathsService.ts @@ -0,0 +1,119 @@ +// tslint:disable:no-require-imports no-var-requires no-unnecessary-callback-wrapper +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IFileSystem, IPlatformService } from '../../../../common/platform/types'; +import { ICurrentProcess, IPathUtils } from '../../../../common/types'; +import { IInterpreterHelper, IKnownSearchPathsForInterpreters } from '../../../../interpreter/contracts'; +import { IServiceContainer } from '../../../../ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../info'; +import { lookForInterpretersInDirectory } from '../helpers'; +import { CacheableLocatorService } from './cacheableLocatorService'; + +const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); + +/** + * Locates "known" paths. + */ +@injectable() +export class KnownPathsService extends CacheableLocatorService { + public constructor( + @inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: IKnownSearchPathsForInterpreters, + @inject(IInterpreterHelper) private helper: IInterpreterHelper, + @inject(IServiceContainer) serviceContainer: IServiceContainer, + ) { + super('KnownPathsService', serviceContainer); + } + + /** + * Release any held resources. + * + * Called by VS Code to indicate it is done with the resource. + */ + // tslint:disable-next-line:no-empty + public dispose() {} + + /** + * Return the located interpreters. + * + * This is used by CacheableLocatorService.getInterpreters(). + */ + protected getInterpretersImplementation(_resource?: Uri): Promise { + return this.suggestionsFromKnownPaths(); + } + + /** + * Return the located interpreters. + */ + private suggestionsFromKnownPaths() { + const promises = this.knownSearchPaths.getSearchPaths().map((dir) => this.getInterpretersInDirectory(dir)); + return Promise.all(promises) + .then((listOfInterpreters) => flatten(listOfInterpreters)) + .then((interpreters) => interpreters.filter( + (item) => item.length > 0, + )) + .then((interpreters) => Promise.all( + interpreters.map((interpreter) => this.getInterpreterDetails(interpreter)), + )) + .then((interpreters) => interpreters.filter( + (interpreter) => !!interpreter, + ).map((interpreter) => interpreter!)); + } + + /** + * Return the information about the identified interpreter binary. + */ + private async getInterpreterDetails(interpreter: string) { + const details = await this.helper.getInterpreterInformation(interpreter); + if (!details) { + return; + } + this._hasInterpreters.resolve(true); + return { + ...(details as PythonEnvironment), + path: interpreter, + envType: EnvironmentType.Unknown, + }; + } + + /** + * Return the interpreters in the given directory. + */ + private getInterpretersInDirectory(dir: string) { + const fs = this.serviceContainer.get(IFileSystem); + return fs + .directoryExists(dir) + .then((exists) => (exists ? lookForInterpretersInDirectory(dir, fs) : Promise.resolve([]))); + } +} + +@injectable() +export class KnownSearchPathsForInterpreters implements IKnownSearchPathsForInterpreters { + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} + + /** + * Return the paths where Python interpreters might be found. + */ + public getSearchPaths(): string[] { + const currentProcess = this.serviceContainer.get(ICurrentProcess); + const platformService = this.serviceContainer.get(IPlatformService); + const pathUtils = this.serviceContainer.get(IPathUtils); + + const searchPaths = currentProcess.env[platformService.pathVariableName]!.split(pathUtils.delimiter) + .map((p) => p.trim()) + .filter((p) => p.length > 0); + + if (!platformService.isWindows) { + ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin'].forEach((p) => { + searchPaths.push(p); + searchPaths.push(path.join(pathUtils.home, p)); + }); + // Add support for paths such as /Users/xxx/anaconda/bin. + if (process.env.HOME) { + searchPaths.push(path.join(pathUtils.home, 'anaconda', 'bin')); + searchPaths.push(path.join(pathUtils.home, 'python', 'bin')); + } + } + return searchPaths; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/baseVirtualEnvService.ts b/src/client/pythonEnvironments/discovery/locators/services/baseVirtualEnvService.ts new file mode 100644 index 000000000000..a969b9bea14a --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/baseVirtualEnvService.ts @@ -0,0 +1,107 @@ +// tslint:disable:no-unnecessary-callback-wrapper no-require-imports no-var-requires + +import { injectable, unmanaged } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { traceError } from '../../../../common/logger'; +import { IFileSystem, IPlatformService } from '../../../../common/platform/types'; +import { IInterpreterHelper, IVirtualEnvironmentsSearchPathProvider } from '../../../../interpreter/contracts'; +import { IVirtualEnvironmentManager } from '../../../../interpreter/virtualEnvs/types'; +import { IServiceContainer } from '../../../../ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../info'; +import { lookForInterpretersInDirectory } from '../helpers'; +import { CacheableLocatorService } from './cacheableLocatorService'; + +const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); + +@injectable() +export class BaseVirtualEnvService extends CacheableLocatorService { + private readonly virtualEnvMgr: IVirtualEnvironmentManager; + + private readonly helper: IInterpreterHelper; + + private readonly fileSystem: IFileSystem; + + public constructor( + @unmanaged() private searchPathsProvider: IVirtualEnvironmentsSearchPathProvider, + @unmanaged() serviceContainer: IServiceContainer, + @unmanaged() name: string, + @unmanaged() cachePerWorkspace = false, + ) { + super(name, serviceContainer, cachePerWorkspace); + this.virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); + this.helper = serviceContainer.get(IInterpreterHelper); + this.fileSystem = serviceContainer.get(IFileSystem); + } + + // tslint:disable-next-line:no-empty + public dispose() {} + + protected getInterpretersImplementation(resource?: Uri): Promise { + return this.suggestionsFromKnownVenvs(resource); + } + + private async suggestionsFromKnownVenvs(resource?: Uri) { + const searchPaths = await this.searchPathsProvider.getSearchPaths(resource); + return Promise.all( + searchPaths.map((dir) => this.lookForInterpretersInVenvs(dir, resource)), + ).then((listOfInterpreters) => flatten(listOfInterpreters)); + } + + private async lookForInterpretersInVenvs(pathToCheck: string, resource?: Uri) { + return this.fileSystem + .getSubDirectories(pathToCheck) + .then((subDirs) => Promise.all(this.getProspectiveDirectoriesForLookup(subDirs))) + .then((dirs) => dirs.filter((dir) => dir.length > 0)) + .then((dirs) => Promise.all(dirs.map((d) => lookForInterpretersInDirectory(d, this.fileSystem)))) + .then((pathsWithInterpreters) => flatten(pathsWithInterpreters)) + .then((interpreters) => Promise.all( + interpreters.map((interpreter) => this.getVirtualEnvDetails(interpreter, resource)), + )) + .then((interpreters) => interpreters.filter( + (interpreter) => !!interpreter, + ).map((interpreter) => interpreter!)) + .catch((err) => { + traceError('Python Extension (lookForInterpretersInVenvs):', err); + // Ignore exceptions. + return [] as PythonEnvironment[]; + }); + } + + private getProspectiveDirectoriesForLookup(subDirs: string[]) { + const platform = this.serviceContainer.get(IPlatformService); + const dirToLookFor = platform.virtualEnvBinName; + return subDirs.map((subDir) => this.fileSystem + .getSubDirectories(subDir) + .then((dirs) => { + const scriptOrBinDirs = dirs.filter((dir) => { + const folderName = path.basename(dir); + return this.fileSystem.arePathsSame(folderName, dirToLookFor); + }); + return scriptOrBinDirs.length === 1 ? scriptOrBinDirs[0] : ''; + }) + .catch((err) => { + traceError('Python Extension (getProspectiveDirectoriesForLookup):', err); + // Ignore exceptions. + return ''; + })); + } + + private async getVirtualEnvDetails(interpreter: string, resource?: Uri): Promise { + return Promise.all([ + this.helper.getInterpreterInformation(interpreter), + this.virtualEnvMgr.getEnvironmentName(interpreter, resource), + this.virtualEnvMgr.getEnvironmentType(interpreter, resource), + ]).then(([details, virtualEnvName, type]) => { + if (!details) { + return; + } + this._hasInterpreters.resolve(true); + return { + ...(details as PythonEnvironment), + envName: virtualEnvName, + type: type! as EnvironmentType, + }; + }); + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/cacheableLocatorService.ts b/src/client/pythonEnvironments/discovery/locators/services/cacheableLocatorService.ts new file mode 100644 index 000000000000..0292f5521947 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/cacheableLocatorService.ts @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-any + +import { injectable, unmanaged } from 'inversify'; +import * as md5 from 'md5'; +import { + Disposable, Event, EventEmitter, Uri, +} from 'vscode'; +import { IWorkspaceService } from '../../../../common/application/types'; +import '../../../../common/extensions'; +import { traceDecorators, traceVerbose } from '../../../../common/logger'; +import { IDisposableRegistry, IPersistentStateFactory } from '../../../../common/types'; +import { createDeferred, Deferred } from '../../../../common/utils/async'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { IInterpreterLocatorService, IInterpreterWatcher } from '../../../../interpreter/contracts'; +import { IServiceContainer } from '../../../../ioc/types'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { PythonEnvironment } from '../../../info'; +import { GetInterpreterLocatorOptions } from '../types'; + +/** + * This class exists so that the interpreter fetching can be cached in between tests. Normally + * this cache resides in memory for the duration of the CacheableLocatorService's lifetime, but in the case + * of our functional tests, we want the cached data to exist outside of each test (where each test will destroy the CacheableLocatorService) + * This gives each test a 20 second speedup. + */ +export class CacheableLocatorPromiseCache { + private static useStatic = false; + + private static staticMap = new Map>(); + + private normalMap = new Map>(); + + public static forceUseStatic() { + CacheableLocatorPromiseCache.useStatic = true; + } + + public static forceUseNormal() { + CacheableLocatorPromiseCache.useStatic = false; + } + + public get(key: string): Deferred | undefined { + if (CacheableLocatorPromiseCache.useStatic) { + return CacheableLocatorPromiseCache.staticMap.get(key); + } + return this.normalMap.get(key); + } + + public set(key: string, value: Deferred) { + if (CacheableLocatorPromiseCache.useStatic) { + CacheableLocatorPromiseCache.staticMap.set(key, value); + } else { + this.normalMap.set(key, value); + } + } + + public delete(key: string) { + if (CacheableLocatorPromiseCache.useStatic) { + CacheableLocatorPromiseCache.staticMap.delete(key); + } else { + this.normalMap.delete(key); + } + } +} + +@injectable() +export abstract class CacheableLocatorService implements IInterpreterLocatorService { + protected readonly _hasInterpreters: Deferred; + + private readonly promisesPerResource = new CacheableLocatorPromiseCache(); + + private readonly handlersAddedToResource = new Set(); + + private readonly cacheKeyPrefix: string; + + private readonly locating = new EventEmitter>(); + + private _didTriggerInterpreterSuggestions: boolean; + + constructor( + @unmanaged() private readonly name: string, + @unmanaged() protected readonly serviceContainer: IServiceContainer, + @unmanaged() private cachePerWorkspace: boolean = false, + ) { + this._hasInterpreters = createDeferred(); + this.cacheKeyPrefix = `INTERPRETERS_CACHE_v3_${name}`; + this._didTriggerInterpreterSuggestions = false; + } + + public get didTriggerInterpreterSuggestions(): boolean { + return this._didTriggerInterpreterSuggestions; + } + + public set didTriggerInterpreterSuggestions(value: boolean) { + this._didTriggerInterpreterSuggestions = value; + } + + public get onLocating(): Event> { + return this.locating.event; + } + + public get hasInterpreters(): Promise { + return this._hasInterpreters.completed ? this._hasInterpreters.promise : Promise.resolve(false); + } + + public abstract dispose(): void; + + @traceDecorators.verbose('Get Interpreters in CacheableLocatorService') + public async getInterpreters(resource?: Uri, options?: GetInterpreterLocatorOptions): Promise { + const cacheKey = this.getCacheKey(resource); + let deferred = this.promisesPerResource.get(cacheKey); + if (!deferred || options?.ignoreCache) { + deferred = createDeferred(); + this.promisesPerResource.set(cacheKey, deferred); + + this.addHandlersForInterpreterWatchers(cacheKey, resource).ignoreErrors(); + + const stopWatch = new StopWatch(); + this.getInterpretersImplementation(resource) + .then(async (items) => { + await this.cacheInterpreters(items, resource); + traceVerbose( + `Interpreters returned by ${this.name} are of count ${Array.isArray(items) ? items.length : 0}`, + ); + traceVerbose(`Interpreters returned by ${this.name} are ${JSON.stringify(items)}`); + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_DISCOVERY, stopWatch.elapsedTime, { + locator: this.name, + interpreters: Array.isArray(items) ? items.length : 0, + }); + deferred!.resolve(items); + }) + .catch((ex) => { + sendTelemetryEvent( + EventName.PYTHON_INTERPRETER_DISCOVERY, + stopWatch.elapsedTime, + { locator: this.name }, + ex, + ); + deferred!.reject(ex); + }); + + this.locating.fire(deferred.promise); + } + deferred.promise + .then((items) => this._hasInterpreters.resolve(items.length > 0)) + .catch((_) => this._hasInterpreters.resolve(false)); + + if (deferred.completed) { + return deferred.promise; + } + + const cachedInterpreters = options?.ignoreCache ? undefined : this.getCachedInterpreters(resource); + return Array.isArray(cachedInterpreters) ? cachedInterpreters : deferred.promise; + } + + protected async addHandlersForInterpreterWatchers(cacheKey: string, resource: Uri | undefined): Promise { + if (this.handlersAddedToResource.has(cacheKey)) { + return; + } + this.handlersAddedToResource.add(cacheKey); + const watchers = await this.getInterpreterWatchers(resource); + const disposableRegisry = this.serviceContainer.get(IDisposableRegistry); + watchers.forEach((watcher) => { + watcher.onDidCreate( + () => { + traceVerbose(`Interpreter Watcher change handler for ${this.cacheKeyPrefix}`); + this.promisesPerResource.delete(cacheKey); + this.getInterpreters(resource).ignoreErrors(); + }, + this, + disposableRegisry, + ); + }); + } + + protected async getInterpreterWatchers(_resource: Uri | undefined): Promise { + return []; + } + + protected abstract getInterpretersImplementation(resource?: Uri): Promise; + + protected createPersistenceStore(resource?: Uri) { + const cacheKey = this.getCacheKey(resource); + const persistentFactory = this.serviceContainer.get(IPersistentStateFactory); + if (this.cachePerWorkspace) { + return persistentFactory.createWorkspacePersistentState(cacheKey, undefined as any); + } + return persistentFactory.createGlobalPersistentState(cacheKey, undefined as any); + } + + protected getCachedInterpreters(resource?: Uri): PythonEnvironment[] | undefined { + const persistence = this.createPersistenceStore(resource); + if (!Array.isArray(persistence.value)) { + return; + } + return persistence.value.map((item) => ({ + ...item, + cachedEntry: true, + })); + } + + protected async cacheInterpreters(interpreters: PythonEnvironment[], resource?: Uri) { + const persistence = this.createPersistenceStore(resource); + await persistence.updateValue(interpreters); + } + + protected getCacheKey(resource?: Uri) { + if (!resource || !this.cachePerWorkspace) { + return this.cacheKeyPrefix; + } + // Ensure we have separate caches per workspace where necessary.Î + const workspaceService = this.serviceContainer.get(IWorkspaceService); + if (!Array.isArray(workspaceService.workspaceFolders)) { + return this.cacheKeyPrefix; + } + + const workspace = workspaceService.getWorkspaceFolder(resource); + return workspace ? `${this.cacheKeyPrefix}:${md5(workspace.uri.fsPath)}` : this.cacheKeyPrefix; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/conda.ts b/src/client/pythonEnvironments/discovery/locators/services/conda.ts new file mode 100644 index 000000000000..5a50c56288c4 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/conda.ts @@ -0,0 +1,70 @@ +import { EnvironmentType, PythonEnvironment } from '../../../info'; + +// tslint:disable-next-line:variable-name +export const AnacondaCompanyNames = ['Anaconda, Inc.', 'Continuum Analytics, Inc.']; +// tslint:disable-next-line:variable-name +export const AnacondaCompanyName = 'Anaconda, Inc.'; +// tslint:disable-next-line:variable-name +export const AnacondaDisplayName = 'Anaconda'; +// tslint:disable-next-line:variable-name +export const AnacondaIdentifiers = ['Anaconda', 'Conda', 'Continuum']; + +export type CondaEnvironmentInfo = { + name: string; + path: string; +}; + +export type CondaInfo = { + envs?: string[]; + 'sys.version'?: string; + 'sys.prefix'?: string; + python_version?: string; + default_prefix?: string; + conda_version?: string; +}; + +/** + * Return the list of conda env interpreters. + */ +export async function parseCondaInfo( + info: CondaInfo, + getPythonPath: (condaEnv: string) => string, + fileExists: (filename: string) => Promise, + getPythonInfo: (python: string) => Promise | undefined>, +) { + // The root of the conda environment is itself a Python interpreter + // envs reported as e.g.: /Users/bob/miniconda3/envs/someEnv. + const envs = Array.isArray(info.envs) ? info.envs : []; + if (info.default_prefix && info.default_prefix.length > 0) { + envs.push(info.default_prefix); + } + + const promises = envs.map(async (envPath) => { + const pythonPath = getPythonPath(envPath); + + if (!(await fileExists(pythonPath))) { + return; + } + const details = await getPythonInfo(pythonPath); + if (!details) { + return; + } + + return { + ...(details as PythonEnvironment), + path: pythonPath, + companyDisplayName: AnacondaCompanyName, + envType: EnvironmentType.Conda, + envPath, + }; + }); + + return ( + Promise.all(promises) + .then((interpreters) => interpreters.filter( + (interpreter) => interpreter !== null && interpreter !== undefined, + )) + // tslint:disable-next-line:no-non-null-assertion + .then((interpreters) => interpreters.map((interpreter) => interpreter!)) + ); +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/condaEnvFileService.ts b/src/client/pythonEnvironments/discovery/locators/services/condaEnvFileService.ts new file mode 100644 index 000000000000..36b6818bad2f --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/condaEnvFileService.ts @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This file is essential to have for us to discover conda interpreters, `CondaEnvService` alone is not sufficient. + * CondaEnvService runs ` env list` command, which requires that we know the path to conda. + * In cases where we're not able to figure that out, we can still use this file to discover paths to conda environments. + * More details: https://github.com/microsoft/vscode-python/issues/8886 + */ + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { traceError } from '../../../../common/logger'; +import { IFileSystem } from '../../../../common/platform/types'; +import { ICondaService, IInterpreterHelper } from '../../../../interpreter/contracts'; +import { IServiceContainer } from '../../../../ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../info'; +import { CacheableLocatorService } from './cacheableLocatorService'; +import { AnacondaCompanyName } from './conda'; + +/** + * Locate conda env interpreters based on the "conda environments file". + */ +@injectable() +export class CondaEnvFileService extends CacheableLocatorService { + constructor( + @inject(IInterpreterHelper) private helperService: IInterpreterHelper, + @inject(ICondaService) private condaService: ICondaService, + @inject(IFileSystem) private fileSystem: IFileSystem, + @inject(IServiceContainer) serviceContainer: IServiceContainer, + ) { + super('CondaEnvFileService', serviceContainer); + } + + /** + * Release any held resources. + * + * Called by VS Code to indicate it is done with the resource. + */ + // tslint:disable-next-line:no-empty + public dispose() {} + + /** + * Return the located interpreters. + * + * This is used by CacheableLocatorService.getInterpreters(). + */ + protected getInterpretersImplementation(_resource?: Uri): Promise { + return this.getSuggestionsFromConda(); + } + + /** + * Return the list of interpreters identified by the "conda environments file". + */ + private async getSuggestionsFromConda(): Promise { + if (!this.condaService.condaEnvironmentsFile) { + return []; + } + return this.fileSystem + .fileExists(this.condaService.condaEnvironmentsFile!) + .then((exists) => ( + exists ? this.getEnvironmentsFromFile(this.condaService.condaEnvironmentsFile!) : Promise.resolve([]) + )); + } + + /** + * Return the list of environments identified in the given file. + */ + private async getEnvironmentsFromFile(envFile: string) { + try { + const fileContents = await this.fileSystem.readFile(envFile); + const environmentPaths = fileContents + .split(/\r?\n/g) + .map((environmentPath) => environmentPath.trim()) + .filter((environmentPath) => environmentPath.length > 0); + + const interpreters = ( + await Promise.all( + environmentPaths.map((environmentPath) => this.getInterpreterDetails(environmentPath)), + ) + ) + .filter((item) => !!item) + .map((item) => item!); + + const environments = await this.condaService.getCondaEnvironments(true); + if (Array.isArray(environments) && environments.length > 0) { + interpreters.forEach((interpreter) => { + const environment = environments.find( + (item) => this.fileSystem.arePathsSame(item.path, interpreter!.envPath!), + ); + if (environment) { + interpreter.envName = environment!.name; + } + }); + } + return interpreters; + } catch (err) { + traceError('Python Extension (getEnvironmentsFromFile.readFile):', err); + // Ignore errors in reading the file. + return [] as PythonEnvironment[]; + } + } + + /** + * Return the interpreter info for the given anaconda environment. + */ + private async getInterpreterDetails(environmentPath: string): Promise { + const interpreter = this.condaService.getInterpreterPath(environmentPath); + if (!interpreter || !(await this.fileSystem.fileExists(interpreter))) { + return; + } + + const details = await this.helperService.getInterpreterInformation(interpreter); + if (!details) { + return; + } + const envName = details.envName ? details.envName : path.basename(environmentPath); + this._hasInterpreters.resolve(true); + return { + ...(details as PythonEnvironment), + path: interpreter, + companyDisplayName: AnacondaCompanyName, + envType: EnvironmentType.Conda, + envPath: environmentPath, + envName, + }; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/condaEnvService.ts b/src/client/pythonEnvironments/discovery/locators/services/condaEnvService.ts new file mode 100644 index 000000000000..41ca380a88c6 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/condaEnvService.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { traceError } from '../../../../common/logger'; +import { IFileSystem } from '../../../../common/platform/types'; +import { ICondaService, IInterpreterHelper } from '../../../../interpreter/contracts'; +import { IServiceContainer } from '../../../../ioc/types'; +import { PythonEnvironment } from '../../../info'; +import { CacheableLocatorService } from './cacheableLocatorService'; +import { parseCondaInfo } from './conda'; + +/** + * Locates conda env interpreters based on the conda service's info. + */ +@injectable() +export class CondaEnvService extends CacheableLocatorService { + constructor( + @inject(ICondaService) private condaService: ICondaService, + @inject(IInterpreterHelper) private helper: IInterpreterHelper, + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IFileSystem) private fileSystem: IFileSystem, + ) { + super('CondaEnvService', serviceContainer); + } + + /** + * Release any held resources. + * + * Called by VS Code to indicate it is done with the resource. + */ + // tslint:disable-next-line:no-empty + public dispose() {} + + /** + * Return the located interpreters. + * + * This is used by CacheableLocatorService.getInterpreters(). + */ + protected getInterpretersImplementation(_resource?: Uri): Promise { + return this.getSuggestionsFromConda(); + } + + /** + * Return the list of interpreters for all the conda envs. + */ + private async getSuggestionsFromConda(): Promise { + try { + const info = await this.condaService.getCondaInfo(); + if (!info) { + return []; + } + const interpreters = await parseCondaInfo( + info, + (env) => this.condaService.getInterpreterPath(env), + (f) => this.fileSystem.fileExists(f), + (p) => this.helper.getInterpreterInformation(p), + ); + this._hasInterpreters.resolve(interpreters.length > 0); + const environments = await this.condaService.getCondaEnvironments(true); + if (Array.isArray(environments) && environments.length > 0) { + interpreters.forEach((interpreter) => { + const environment = environments.find( + (item) => this.fileSystem.arePathsSame(item.path, interpreter!.envPath!), + ); + if (environment) { + interpreter.envName = environment!.name; + } + }); + } + + return interpreters; + } catch (ex) { + // Failed because either: + // 1. conda is not installed. + // 2. `conda info --json` has changed signature. + // 3. output of `conda info --json` has changed in structure. + // In all cases, we can't offer conda pythonPath suggestions. + traceError('Failed to get Suggestions from conda', ex); + return []; + } + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/condaHelper.ts b/src/client/pythonEnvironments/discovery/locators/services/condaHelper.ts new file mode 100644 index 000000000000..66930f5ac09d --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/condaHelper.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import '../../../../common/extensions'; +import { AnacondaDisplayName, AnacondaIdentifiers, CondaInfo } from './conda'; + +export type EnvironmentPath = string; +export type EnvironmentName = string; + +/** + * Helpers for conda. + */ + +/** + * Return the string to display for the conda interpreter. + */ +export function getDisplayName(condaInfo: CondaInfo = {}): string { + // Samples. + // "3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]". + // "3.6.2 |Anaconda, Inc.| (default, Sep 21 2017, 18:29:43) \n[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)]". + const sysVersion = condaInfo['sys.version']; + if (!sysVersion) { + return AnacondaDisplayName; + } + + // Take the second part of the sys.version. + const sysVersionParts = sysVersion.split('|', 2); + if (sysVersionParts.length === 2) { + const displayName = sysVersionParts[1].trim(); + if (isIdentifiableAsAnaconda(displayName)) { + return displayName; + } + return `${displayName} : ${AnacondaDisplayName}`; + } + return AnacondaDisplayName; +} + +/** + * Parses output returned by the command `conda env list`. + * Sample output is as follows: + * # conda environments: + * # + * base * /Users/donjayamanne/anaconda3 + * one /Users/donjayamanne/anaconda3/envs/one + * py27 /Users/donjayamanne/anaconda3/envs/py27 + * py36 /Users/donjayamanne/anaconda3/envs/py36 + * three /Users/donjayamanne/anaconda3/envs/three + * /Users/donjayamanne/anaconda3/envs/four + * /Users/donjayamanne/anaconda3/envs/five 5 + * aaaa_bbbb_cccc_dddd_eeee_ffff_gggg /Users/donjayamanne/anaconda3/envs/aaaa_bbbb_cccc_dddd_eeee_ffff_gggg + * with*star /Users/donjayamanne/anaconda3/envs/with*star + * "/Users/donjayamanne/anaconda3/envs/seven " + */ +export function parseCondaEnvFileContents( + condaEnvFileContents: string, +): { name: string; path: string; isActive: boolean }[] | undefined { + // Don't trim the lines. `path` portion of the line can end with a space. + const lines = condaEnvFileContents.splitLines({ trim: false }); + const envs: { name: string; path: string; isActive: boolean }[] = []; + + lines.forEach((line) => { + const item = parseCondaEnvFileLine(line); + if (item) { + envs.push(item); + } + }); + + return envs.length > 0 ? envs : undefined; +} + +function parseCondaEnvFileLine(line: string): { name: string; path: string; isActive: boolean } | undefined { + // Empty lines or lines starting with `#` are comments and can be ignored. + if (line.length === 0 || line.startsWith('#')) { + return undefined; + } + + // This extraction is based on the following code for `conda env list`: + // https://github.com/conda/conda/blob/f207a2114c388fd17644ee3a5f980aa7cf86b04b/conda/cli/common.py#L188 + // It uses "%-20s %s %s" as the format string. Where the middle %s is '*' + // if the environment is active, and ' ' if it is not active. + + // If conda environment was created using `-p` then it may NOT have a name. + // Use empty string as default name for envs created using path only. + let name = ''; + let remainder = line; + + // The `name` and `path` parts are separated by at least 5 spaces. We cannot + // use a single space here since it can be part of the name (see below for + // name spec). Another assumption here is that `name` does not start with + // 5*spaces or somewhere in the center. However, ` name` or `a b` is + // a valid name when using --clone. Highly unlikely that users will have this + // form as the environment name. lastIndexOf() can also be used but that assumes + // that `path` does NOT end with 5*spaces. + let spaceIndex = line.indexOf(' '); + if (spaceIndex === -1) { + // This means the environment name is longer than 17 characters and it is + // active. Try ' * ' for separator between name and path. + spaceIndex = line.indexOf(' * '); + } + + if (spaceIndex > 0) { + // Parsing `name` + // > `conda create -n ` + // conda environment `name` should NOT have following characters + // ('/', ' ', ':', '#'). So we can use the index of 5*space + // characters to extract the name. + // + // > `conda create --clone one -p "~/envs/one two"` + // this can generate a cloned env with name `one two`. This is + // only allowed for cloned environments. In both cases, the best + // separator is 5*spaces. It is highly unlikely that users will have + // 5*spaces in their environment name. + // + // Notes: When using clone if the path has a trailing space, it will + // not be preserved for the name. Trailing spaces in environment names + // are NOT allowed. But leading spaces are allowed. Currently there no + // special separator character between name and path, other than spaces. + // We will need a well known separator if this ever becomes a issue. + name = line.substring(0, spaceIndex).trimRight(); + remainder = line.substring(spaceIndex); + } + + // Detecting Active Environment: + // Only active environment will have `*` between `name` and `path`. `name` + // or `path` can have `*` in them as well. So we have to look for `*` in + // between `name` and `path`. We already extracted the name, the next non- + // whitespace character should either be `*` or environment path. + remainder = remainder.trimLeft(); + const isActive = remainder.startsWith('*'); + + // Parsing `path` + // If `*` is the first then we can skip that character. Trim left again, + // don't do trim() or trimRight(), since paths can end with a space. + remainder = (isActive ? remainder.substring(1) : remainder).trimLeft(); + + return { name, path: remainder, isActive }; +} +/** + * Does the given string match a known Anaconda identifier. + */ +function isIdentifiableAsAnaconda(value: string) { + const valueToSearch = value.toLowerCase(); + return AnacondaIdentifiers.some((item) => valueToSearch.indexOf(item.toLowerCase()) !== -1); +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/condaService.ts b/src/client/pythonEnvironments/discovery/locators/services/condaService.ts new file mode 100644 index 000000000000..775e87ef20da --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/condaService.ts @@ -0,0 +1,413 @@ +import { + inject, injectable, named, optional, +} from 'inversify'; +import * as path from 'path'; +import { compare, parse, SemVer } from 'semver'; +import { ConfigurationChangeEvent, Uri } from 'vscode'; + +import { IWorkspaceService } from '../../../../common/application/types'; +import { + traceDecorators, traceError, traceVerbose, traceWarning, +} from '../../../../common/logger'; +import { IFileSystem, IPlatformService } from '../../../../common/platform/types'; +import { IProcessServiceFactory } from '../../../../common/process/types'; +import { IConfigurationService, IDisposableRegistry, IPersistentStateFactory } from '../../../../common/types'; +import { cache } from '../../../../common/utils/decorators'; +import { ICondaService, IInterpreterLocatorService, WINDOWS_REGISTRY_SERVICE } from '../../../../interpreter/contracts'; +import { EnvironmentType, PythonEnvironment } from '../../../info'; +import { CondaEnvironmentInfo, CondaInfo } from './conda'; +import { parseCondaEnvFileContents } from './condaHelper'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify: (value: string) => string = require('untildify'); + +// This glob pattern will match all of the following: +// ~/anaconda/bin/conda, ~/anaconda3/bin/conda, ~/miniconda/bin/conda, ~/miniconda3/bin/conda +// /usr/share/anaconda/bin/conda, /usr/share/anaconda3/bin/conda, /usr/share/miniconda/bin/conda, /usr/share/miniconda3/bin/conda + +const condaGlobPathsForLinuxMac = [ + untildify('~/opt/*conda*/bin/conda'), + '/opt/*conda*/bin/conda', + '/usr/share/*conda*/bin/conda', + untildify('~/*conda*/bin/conda'), +]; + +export const CondaLocationsGlob = `{${condaGlobPathsForLinuxMac.join(',')}}`; + +// ...and for windows, the known default install locations: +const condaGlobPathsForWindows = [ + '/ProgramData/[Mm]iniconda*/Scripts/conda.exe', + '/ProgramData/[Aa]naconda*/Scripts/conda.exe', + untildify('~/[Mm]iniconda*/Scripts/conda.exe'), + untildify('~/[Aa]naconda*/Scripts/conda.exe'), + untildify('~/AppData/Local/Continuum/[Mm]iniconda*/Scripts/conda.exe'), + untildify('~/AppData/Local/Continuum/[Aa]naconda*/Scripts/conda.exe'), +]; + +// format for glob processing: +export const CondaLocationsGlobWin = `{${condaGlobPathsForWindows.join(',')}}`; + +export const CondaGetEnvironmentPrefix = 'Outputting Environment Now...'; + +/** + * A wrapper around a conda installation. + */ +@injectable() +export class CondaService implements ICondaService { + private condaFile?: Promise; + + private isAvailable: boolean | undefined; + + constructor( + @inject(IProcessServiceFactory) private processServiceFactory: IProcessServiceFactory, + @inject(IPlatformService) private platform: IPlatformService, + @inject(IFileSystem) private fileSystem: IFileSystem, + @inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IInterpreterLocatorService) + @named(WINDOWS_REGISTRY_SERVICE) + @optional() + private registryLookupForConda?: IInterpreterLocatorService, + ) { + this.addCondaPathChangedHandler(); + } + + public get condaEnvironmentsFile(): string | undefined { + const homeDir = this.platform.isWindows ? process.env.USERPROFILE : process.env.HOME || process.env.HOMEPATH; + return homeDir ? path.join(homeDir, '.conda', 'environments.txt') : undefined; + } + + /** + * Release any held resources. + * + * Called by VS Code to indicate it is done with the resource. + */ + // tslint:disable-next-line:no-empty + public dispose() {} + + /** + * Return the path to the "conda file". + */ + public async getCondaFile(): Promise { + if (!this.condaFile) { + this.condaFile = this.getCondaFileImpl(); + } + // tslint:disable-next-line:no-unnecessary-local-variable + const condaFile = await this.condaFile!; + return condaFile!; + } + + /** + * Is there a conda install to use? + */ + public async isCondaAvailable(): Promise { + if (typeof this.isAvailable === 'boolean') { + return this.isAvailable; + } + return this.getCondaVersion() + .then((version) => (this.isAvailable = version !== undefined)) + .catch(() => (this.isAvailable = false)); + } + + /** + * Return the conda version. + * The version info is cached for some time. + * Remember, its possible the user can update the path to `conda` executable in settings.json, + * or environment variables. + * Doing that could change this value. + */ + @cache(120_000) + public async getCondaVersion(): Promise { + const processService = await this.processServiceFactory.create(); + const info = await this.getCondaInfo().catch(() => undefined); + let versionString: string | undefined; + if (info && info.conda_version) { + versionString = info.conda_version; + } else { + const stdOut = await this.getCondaFile() + .then((condaFile) => processService.exec(condaFile, ['--version'], {})) + .then((result) => result.stdout.trim()) + .catch(() => undefined); + + versionString = stdOut && stdOut.startsWith('conda ') ? stdOut.substring('conda '.length).trim() : stdOut; + } + if (!versionString) { + return; + } + const version = parse(versionString, true); + if (version) { + return version; + } + // Use a bogus version, at least to indicate the fact that a version was returned. + traceWarning(`Unable to parse Version of Conda, ${versionString}`); + return new SemVer('0.0.1'); + } + + /** + * Can the shell find conda (to run it)? + */ + public async isCondaInCurrentPath() { + const processService = await this.processServiceFactory.create(); + return processService + .exec('conda', ['--version']) + .then((output) => output.stdout.length > 0) + .catch(() => false); + } + + /** + * Return the info reported by the conda install. + * The result is cached for 30s. + */ + @cache(60_000) + public async getCondaInfo(): Promise { + try { + const condaFile = await this.getCondaFile(); + const processService = await this.processServiceFactory.create(); + const condaInfo = await processService.exec(condaFile, ['info', '--json']).then((output) => output.stdout); + + return JSON.parse(condaInfo) as CondaInfo; + } catch (ex) { + // Failed because either: + // 1. conda is not installed. + // 2. `conda info --json` has changed signature. + } + } + + /** + * Determines whether a python interpreter is a conda environment or not. + * The check is done by simply looking for the 'conda-meta' directory. + * @param {string} interpreterPath + * @returns {Promise} + * @memberof CondaService + */ + public async isCondaEnvironment(interpreterPath: string): Promise { + const dir = path.dirname(interpreterPath); + const { isWindows } = this.platform; + const condaMetaDirectory = isWindows ? path.join(dir, 'conda-meta') : path.join(dir, '..', 'conda-meta'); + return this.fileSystem.directoryExists(condaMetaDirectory); + } + + /** + * Return (env name, interpreter filename) for the interpreter. + */ + public async getCondaEnvironment(interpreterPath: string): Promise<{ name: string; path: string } | undefined> { + const isCondaEnv = await this.isCondaEnvironment(interpreterPath); + if (!isCondaEnv) { + return; + } + let environments = await this.getCondaEnvironments(false); + const dir = path.dirname(interpreterPath); + + // If interpreter is in bin or Scripts, then go up one level + const subDirName = path.basename(dir); + const goUpOnLevel = ['BIN', 'SCRIPTS'].indexOf(subDirName.toUpperCase()) !== -1; + const interpreterPathToMatch = goUpOnLevel ? path.join(dir, '..') : dir; + + // From the list of conda environments find this dir. + let matchingEnvs = Array.isArray(environments) + ? environments.filter((item) => this.fileSystem.arePathsSame(item.path, interpreterPathToMatch)) + : []; + if (matchingEnvs.length === 0) { + environments = await this.getCondaEnvironments(true); + matchingEnvs = Array.isArray(environments) + ? environments.filter((item) => this.fileSystem.arePathsSame(item.path, interpreterPathToMatch)) + : []; + } + + if (matchingEnvs.length > 0) { + return { name: matchingEnvs[0].name, path: interpreterPathToMatch }; + } + + // If still not available, then the user created the env after starting vs code. + // The only solution is to get the user to re-start vscode. + } + + /** + * Return the list of conda envs (by name, interpreter filename). + */ + @traceDecorators.verbose('Get Conda environments') + public async getCondaEnvironments(ignoreCache: boolean): Promise { + // Global cache. + const globalPersistence = this.persistentStateFactory.createGlobalPersistentState<{ + data: CondaEnvironmentInfo[] | undefined; + // tslint:disable-next-line:no-any + }>('CONDA_ENVIRONMENTS', undefined as any); + if (!ignoreCache && globalPersistence.value) { + return globalPersistence.value.data; + } + + try { + const condaFile = await this.getCondaFile(); + const processService = await this.processServiceFactory.create(); + let envInfo = await processService.exec(condaFile, ['env', 'list']).then((output) => output.stdout); + traceVerbose(`Conda Env List ${envInfo}}`); + if (!envInfo) { + traceVerbose('Conda env list failure, attempting path additions.'); + // Try adding different folders to the path. Miniconda fails to run + // without them. + const baseFolder = path.dirname(path.dirname(condaFile)); + const binFolder = path.join(baseFolder, 'bin'); + const condaBinFolder = path.join(baseFolder, 'condabin'); + const libaryBinFolder = path.join(baseFolder, 'library', 'bin'); + const newEnv = process.env; + newEnv.PATH = `${binFolder};${condaBinFolder};${libaryBinFolder};${newEnv.PATH}`; + traceVerbose(`Attempting new path for conda env list: ${newEnv.PATH}`); + envInfo = await processService + .exec(condaFile, ['env', 'list'], { env: newEnv }) + .then((output) => output.stdout); + } + const environments = parseCondaEnvFileContents(envInfo); + await globalPersistence.updateValue({ data: environments }); + return environments; + } catch (ex) { + await globalPersistence.updateValue({ data: undefined }); + // Failed because either: + // 1. conda is not installed. + // 2. `conda env list has changed signature. + traceError('Failed to get conda environment list from conda', ex); + } + } + + /** + * Return the interpreter's filename for the given environment. + */ + public getInterpreterPath(condaEnvironmentPath: string): string { + // where to find the Python binary within a conda env. + const relativePath = this.platform.isWindows ? 'python.exe' : path.join('bin', 'python'); + return path.join(condaEnvironmentPath, relativePath); + } + + /** + * Get the conda exe from the path to an interpreter's python. This might be different than the globally registered conda.exe + * The value is cached for a while. + * The only way this can change is if user installs conda into this same environment. + * Generally we expect that to happen the other way, the user creates a conda environment with conda in it. + */ + @traceDecorators.verbose('Get Conda File from interpreter') + @cache(120_000) + public async getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise { + const condaExe = this.platform.isWindows ? 'conda.exe' : 'conda'; + const scriptsDir = this.platform.isWindows ? 'Scripts' : 'bin'; + const interpreterDir = interpreterPath ? path.dirname(interpreterPath) : ''; + + // Might be in a situation where this is not the default python env, but rather one running + // from a virtualenv + const envsPos = envName ? interpreterDir.indexOf(path.join('envs', envName)) : -1; + if (envsPos > 0) { + // This should be where the original python was run from when the environment was created. + const originalPath = interpreterDir.slice(0, envsPos); + let condaPath1 = path.join(originalPath, condaExe); + + if (await this.fileSystem.fileExists(condaPath1)) { + return condaPath1; + } + + // Also look in the scripts directory here too. + condaPath1 = path.join(originalPath, scriptsDir, condaExe); + if (await this.fileSystem.fileExists(condaPath1)) { + return condaPath1; + } + } + + let condaPath2 = path.join(interpreterDir, condaExe); + if (await this.fileSystem.fileExists(condaPath2)) { + return condaPath2; + } + // Conda path has changed locations, check the new location in the scripts directory after checking + // the old location + condaPath2 = path.join(interpreterDir, scriptsDir, condaExe); + if (await this.fileSystem.fileExists(condaPath2)) { + return condaPath2; + } + } + + /** + * Is the given interpreter from conda? + */ + private detectCondaEnvironment(env: PythonEnvironment) { + return ( + env.envType === EnvironmentType.Conda + || (env.displayName ? env.displayName : '').toUpperCase().indexOf('ANACONDA') >= 0 + || (env.companyDisplayName ? env.companyDisplayName : '').toUpperCase().indexOf('ANACONDA') >= 0 + || (env.companyDisplayName ? env.companyDisplayName : '').toUpperCase().indexOf('CONTINUUM') >= 0 + ); + } + + /** + * Return the highest Python version from the given list. + */ + private getLatestVersion(interpreters: PythonEnvironment[]) { + const sortedInterpreters = interpreters.slice(); + // tslint:disable-next-line:no-non-null-assertion + sortedInterpreters.sort((a, b) => (a.version && b.version ? compare(a.version.raw, b.version.raw) : 0)); + if (sortedInterpreters.length > 0) { + return sortedInterpreters[sortedInterpreters.length - 1]; + } + } + + private addCondaPathChangedHandler() { + const disposable = this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this)); + this.disposableRegistry.push(disposable); + } + + private async onDidChangeConfiguration(event: ConfigurationChangeEvent) { + const workspacesUris: (Uri | undefined)[] = this.workspaceService.hasWorkspaceFolders + ? this.workspaceService.workspaceFolders!.map((workspace) => workspace.uri) + : [undefined]; + if (workspacesUris.findIndex((uri) => event.affectsConfiguration('python.condaPath', uri)) === -1) { + return; + } + this.condaFile = undefined; + } + + /** + * Return the path to the "conda file", if there is one (in known locations). + */ + private async getCondaFileImpl() { + const settings = this.configService.getSettings(); + + const setting = settings.condaPath; + if (setting && setting !== '') { + return setting; + } + + const isAvailable = await this.isCondaInCurrentPath(); + if (isAvailable) { + return 'conda'; + } + if (this.platform.isWindows && this.registryLookupForConda) { + const interpreters = await this.registryLookupForConda.getInterpreters(); + const condaInterpreters = interpreters.filter(this.detectCondaEnvironment); + const condaInterpreter = this.getLatestVersion(condaInterpreters); + if (condaInterpreter) { + const interpreterPath = await this.getCondaFileFromInterpreter( + condaInterpreter.path, + condaInterpreter.envName, + ); + if (interpreterPath) { + return interpreterPath; + } + } + } + return this.getCondaFileFromKnownLocations(); + } + + /** + * Return the path to the "conda file", if there is one (in known locations). + * Note: For now we simply return the first one found. + */ + private async getCondaFileFromKnownLocations(): Promise { + const globPattern = this.platform.isWindows ? CondaLocationsGlobWin : CondaLocationsGlob; + const condaFiles = await this.fileSystem.search(globPattern).catch((failReason) => { + traceWarning( + 'Default conda location search failed.', + `Searching for default install locations for conda results in error: ${failReason}`, + ); + return []; + }); + const validCondaFiles = condaFiles.filter((condaPath) => condaPath.length > 0); + return validCondaFiles.length === 0 ? 'conda' : validCondaFiles[0]; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/currentPathService.ts b/src/client/pythonEnvironments/discovery/locators/services/currentPathService.ts new file mode 100644 index 000000000000..0a2467aa9885 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/currentPathService.ts @@ -0,0 +1,143 @@ +// tslint:disable:no-require-imports no-var-requires underscore-consistent-invocation no-unnecessary-callback-wrapper +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { traceError, traceInfo } from '../../../../common/logger'; +import { IFileSystem, IPlatformService } from '../../../../common/platform/types'; +import * as internalPython from '../../../../common/process/internal/python'; +import { IProcessServiceFactory } from '../../../../common/process/types'; +import { IConfigurationService } from '../../../../common/types'; +import { OSType } from '../../../../common/utils/platform'; +import { IInterpreterHelper } from '../../../../interpreter/contracts'; +import { IPythonInPathCommandProvider } from '../../../../interpreter/locators/types'; +import { IServiceContainer } from '../../../../ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../info'; +import { CacheableLocatorService } from './cacheableLocatorService'; + +/** + * Locates the currently configured Python interpreter. + * + * If no interpreter is configured then it falls back to the system + * Python (3 then 2). + */ +@injectable() +export class CurrentPathService extends CacheableLocatorService { + private readonly fs: IFileSystem; + + public constructor( + @inject(IInterpreterHelper) private helper: IInterpreterHelper, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + @inject(IPythonInPathCommandProvider) private readonly pythonCommandProvider: IPythonInPathCommandProvider, + @inject(IServiceContainer) serviceContainer: IServiceContainer, + ) { + super('CurrentPathService', serviceContainer); + this.fs = serviceContainer.get(IFileSystem); + } + + /** + * Release any held resources. + * + * Called by VS Code to indicate it is done with the resource. + */ + // tslint:disable-next-line:no-empty + public dispose() {} + + /** + * Return the located interpreters. + * + * This is used by CacheableLocatorService.getInterpreters(). + */ + protected getInterpretersImplementation(resource?: Uri): Promise { + return this.suggestionsFromKnownPaths(resource); + } + + /** + * Return the located interpreters. + */ + private async suggestionsFromKnownPaths(resource?: Uri) { + const configSettings = this.serviceContainer + .get(IConfigurationService) + .getSettings(resource); + const pathsToCheck = [...this.pythonCommandProvider.getCommands(), { command: configSettings.pythonPath }]; + + const pythonPaths = Promise.all(pathsToCheck.map((item) => this.getInterpreter(item))); + return ( + pythonPaths + .then((interpreters) => interpreters.filter((item) => item.length > 0)) + // tslint:disable-next-line:promise-function-async + .then((interpreters) => Promise.all( + interpreters.map((interpreter) => this.getInterpreterDetails(interpreter)), + )) + .then((interpreters) => interpreters.filter((item) => !!item).map((item) => item!)) + ); + } + + /** + * Return the information about the identified interpreter binary. + */ + private async getInterpreterDetails(pythonPath: string): Promise { + return this.helper.getInterpreterInformation(pythonPath).then((details) => { + if (!details) { + return; + } + this._hasInterpreters.resolve(true); + return { + ...(details as PythonEnvironment), + path: pythonPath, + envType: details.envType ? details.envType : EnvironmentType.Unknown, + }; + }); + } + + /** + * Return the path to the interpreter (or the default if not found). + */ + private async getInterpreter(options: { command: string; args?: string[] }) { + try { + const processService = await this.processServiceFactory.create(); + const pyArgs = Array.isArray(options.args) ? options.args : []; + const [args, parse] = internalPython.getExecutable(); + return processService + .exec(options.command, pyArgs.concat(args), {}) + .then((output) => parse(output.stdout)) + .then(async (value) => { + if (value.length > 0 && (await this.fs.fileExists(value))) { + return value; + } + traceError( + `Detection of Python Interpreter for Command ${options.command} and args ${pyArgs.join( + ' ', + )} failed as file ${value} does not exist`, + ); + return ''; + }) + .catch((_ex) => { + traceInfo( + `Detection of Python Interpreter for Command ${options.command} and args ${pyArgs.join( + ' ', + )} failed`, + ); + return ''; + }); // Ignore exceptions in getting the executable. + } catch (ex) { + traceError(`Detection of Python Interpreter for Command ${options.command} failed`, ex); + return ''; // Ignore exceptions in getting the executable. + } + } +} + +@injectable() +export class PythonInPathCommandProvider implements IPythonInPathCommandProvider { + constructor(@inject(IPlatformService) private readonly platform: IPlatformService) {} + + public getCommands(): { command: string; args?: string[] }[] { + const paths = ['python3.7', 'python3.6', 'python3', 'python2', 'python'].map((item) => ({ command: item })); + if (this.platform.osType !== OSType.Windows) { + return paths; + } + + const versions = ['3.7', '3.6', '3', '2']; + return paths.concat( + versions.map((version) => ({ command: 'py', args: [`-${version}`] })), + ); + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/globalVirtualEnvService.ts b/src/client/pythonEnvironments/discovery/locators/services/globalVirtualEnvService.ts new file mode 100644 index 000000000000..77d6a20618e8 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/globalVirtualEnvService.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IConfigurationService, ICurrentProcess } from '../../../../common/types'; +import { IVirtualEnvironmentsSearchPathProvider } from '../../../../interpreter/contracts'; +import { IVirtualEnvironmentManager } from '../../../../interpreter/virtualEnvs/types'; +import { IServiceContainer } from '../../../../ioc/types'; +import { BaseVirtualEnvService } from './baseVirtualEnvService'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify: (value: string) => string = require('untildify'); + +@injectable() +export class GlobalVirtualEnvService extends BaseVirtualEnvService { + public constructor( + @inject(IVirtualEnvironmentsSearchPathProvider) + @named('global') + globalVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider, + @inject(IServiceContainer) serviceContainer: IServiceContainer, + ) { + super(globalVirtualEnvPathProvider, serviceContainer, 'VirtualEnvService'); + } +} + +@injectable() +export class GlobalVirtualEnvironmentsSearchPathProvider implements IVirtualEnvironmentsSearchPathProvider { + private readonly config: IConfigurationService; + + private readonly currentProcess: ICurrentProcess; + + private readonly virtualEnvMgr: IVirtualEnvironmentManager; + + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.config = serviceContainer.get(IConfigurationService); + this.virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); + this.currentProcess = serviceContainer.get(ICurrentProcess); + } + + public async getSearchPaths(resource?: Uri): Promise { + const homedir = os.homedir(); + const venvFolders = [ + 'envs', + '.pyenv', + '.direnv', + '.virtualenvs', + ...this.config.getSettings(resource).venvFolders, + ]; + const folders = [...new Set(venvFolders.map((item) => path.join(homedir, item)))]; + + // Add support for the WORKON_HOME environment variable used by pipenv and virtualenvwrapper. + const workonHomePath = this.currentProcess.env.WORKON_HOME; + if (workonHomePath) { + folders.push(untildify(workonHomePath)); + } + + // tslint:disable-next-line:no-string-literal + const pyenvRoot = await this.virtualEnvMgr.getPyEnvRoot(resource); + if (pyenvRoot) { + folders.push(pyenvRoot); + folders.push(path.join(pyenvRoot, 'versions')); + } + return folders; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/hashProvider.ts b/src/client/pythonEnvironments/discovery/locators/services/hashProvider.ts new file mode 100644 index 000000000000..de9aba8a1e00 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/hashProvider.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IFileSystem } from '../../../../common/platform/types'; +import { IInterpreterHashProvider } from '../../../../interpreter/locators/types'; + +@injectable() +export class InterpreterHashProvider implements IInterpreterHashProvider { + constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {} + + public async getInterpreterHash(pythonPath: string): Promise { + return this.fs.getFileHash(pythonPath); + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/hashProviderFactory.ts b/src/client/pythonEnvironments/discovery/locators/services/hashProviderFactory.ts new file mode 100644 index 000000000000..c00d8fc6b625 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/hashProviderFactory.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IConfigurationService } from '../../../../common/types'; +import { + IInterpreterHashProvider, + IInterpreterHashProviderFactory, + IWindowsStoreInterpreter, +} from '../../../../interpreter/locators/types'; +import { InterpreterHashProvider } from './hashProvider'; +import { WindowsStoreInterpreter } from './windowsStoreInterpreter'; + +@injectable() +export class InterpeterHashProviderFactory implements IInterpreterHashProviderFactory { + constructor( + @inject(IConfigurationService) private readonly configService: IConfigurationService, + @inject(WindowsStoreInterpreter) private readonly windowsStoreInterpreter: IWindowsStoreInterpreter, + @inject(WindowsStoreInterpreter) private readonly windowsStoreHashProvider: IInterpreterHashProvider, + @inject(InterpreterHashProvider) private readonly hashProvider: IInterpreterHashProvider, + ) {} + + public async create(options: { pythonPath: string } | { resource: Uri }): Promise { + const pythonPath = 'pythonPath' in options + ? options.pythonPath + : this.configService.getSettings(options.resource).pythonPath; + + return this.windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath) + ? this.windowsStoreHashProvider + : this.hashProvider; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/interpreterFilter.ts b/src/client/pythonEnvironments/discovery/locators/services/interpreterFilter.ts new file mode 100644 index 000000000000..84097aff3c44 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/interpreterFilter.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { PythonEnvironment } from '../../../info'; +import { isRestrictedWindowsStoreInterpreterPath } from './windowsStoreInterpreter'; + +export function isHiddenInterpreter(interpreter: PythonEnvironment): boolean { + // Any set of rules to hide interpreters should go here + return isRestrictedWindowsStoreInterpreterPath(interpreter.path); +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/interpreterWatcherBuilder.ts b/src/client/pythonEnvironments/discovery/locators/services/interpreterWatcherBuilder.ts new file mode 100644 index 000000000000..aa9756f26ee0 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/interpreterWatcherBuilder.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../../common/application/types'; +import { traceDecorators } from '../../../../common/logger'; +import { createDeferred } from '../../../../common/utils/async'; +import { + IInterpreterWatcher, + IInterpreterWatcherBuilder, + WORKSPACE_VIRTUAL_ENV_SERVICE, +} from '../../../../interpreter/contracts'; +import { IServiceContainer } from '../../../../ioc/types'; +import { WorkspaceVirtualEnvWatcherService } from './workspaceVirtualEnvWatcherService'; + +@injectable() +export class InterpreterWatcherBuilder implements IInterpreterWatcherBuilder { + private readonly watchersByResource = new Map>(); + + /** + * Creates an instance of InterpreterWatcherBuilder. + * Inject the DI container, as we need to get a new instance of IInterpreterWatcher to build it. + * @param {IWorkspaceService} workspaceService + * @param {IServiceContainer} serviceContainer + * @memberof InterpreterWatcherBuilder + */ + constructor( + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + ) {} + + @traceDecorators.verbose('Build the workspace interpreter watcher') + public async getWorkspaceVirtualEnvInterpreterWatcher(resource: Uri | undefined): Promise { + const key = this.getResourceKey(resource); + if (!this.watchersByResource.has(key)) { + const deferred = createDeferred(); + this.watchersByResource.set(key, deferred.promise); + const watcher = this.serviceContainer.get( + IInterpreterWatcher, + WORKSPACE_VIRTUAL_ENV_SERVICE, + ); + await watcher.register(resource); + deferred.resolve(watcher); + } + return this.watchersByResource.get(key)!; + } + + protected getResourceKey(resource: Uri | undefined): string { + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + return workspaceFolder ? workspaceFolder.uri.fsPath : ''; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/pipEnvService.ts b/src/client/pythonEnvironments/discovery/locators/services/pipEnvService.ts new file mode 100644 index 000000000000..8a5c23c43109 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/pipEnvService.ts @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../../../common/application/types'; +import { traceError, traceWarning } from '../../../../common/logger'; +import { IFileSystem, IPlatformService } from '../../../../common/platform/types'; +import { IProcessServiceFactory } from '../../../../common/process/types'; +import { IConfigurationService, ICurrentProcess } from '../../../../common/types'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { IInterpreterHelper, IPipEnvService } from '../../../../interpreter/contracts'; +import { IPipEnvServiceHelper } from '../../../../interpreter/locators/types'; +import { IServiceContainer } from '../../../../ioc/types'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { EnvironmentType, PythonEnvironment } from '../../../info'; +import { GetInterpreterLocatorOptions } from '../types'; +import { CacheableLocatorService } from './cacheableLocatorService'; + +const pipEnvFileNameVariable = 'PIPENV_PIPFILE'; + +@injectable() +export class PipEnvService extends CacheableLocatorService implements IPipEnvService { + private readonly helper: IInterpreterHelper; + + private readonly processServiceFactory: IProcessServiceFactory; + + private readonly workspace: IWorkspaceService; + + private readonly fs: IFileSystem; + + private readonly configService: IConfigurationService; + + private readonly pipEnvServiceHelper: IPipEnvServiceHelper; + + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super('PipEnvService', serviceContainer, true); + this.helper = this.serviceContainer.get(IInterpreterHelper); + this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + this.workspace = this.serviceContainer.get(IWorkspaceService); + this.fs = this.serviceContainer.get(IFileSystem); + this.configService = this.serviceContainer.get(IConfigurationService); + this.pipEnvServiceHelper = this.serviceContainer.get(IPipEnvServiceHelper); + } + + // tslint:disable-next-line:no-empty + public dispose() {} + + public async isRelatedPipEnvironment(dir: string, pythonPath: string): Promise { + if (!this.didTriggerInterpreterSuggestions) { + return false; + } + + // In PipEnv, the name of the cwd is used as a prefix in the virtual env. + if (pythonPath.indexOf(`${path.sep}${path.basename(dir)}-`) === -1) { + return false; + } + const envName = await this.getInterpreterPathFromPipenv(dir, true); + return !!envName; + } + + public get executable(): string { + return this.didTriggerInterpreterSuggestions ? this.configService.getSettings().pipenvPath : ''; + } + + public async getInterpreters(resource?: Uri, options?: GetInterpreterLocatorOptions): Promise { + if (!this.didTriggerInterpreterSuggestions) { + return []; + } + + const stopwatch = new StopWatch(); + const startDiscoveryTime = stopwatch.elapsedTime; + + const interpreters = await super.getInterpreters(resource, options); + + const discoveryDuration = stopwatch.elapsedTime - startDiscoveryTime; + sendTelemetryEvent(EventName.PIPENV_INTERPRETER_DISCOVERY, discoveryDuration); + + return interpreters; + } + + protected getInterpretersImplementation(resource?: Uri): Promise { + if (!this.didTriggerInterpreterSuggestions) { + return Promise.resolve([]); + } + + const pipenvCwd = this.getPipenvWorkingDirectory(resource); + if (!pipenvCwd) { + return Promise.resolve([]); + } + + return this.getInterpreterFromPipenv(pipenvCwd) + .then((item) => (item ? [item] : [])) + .catch(() => []); + } + + private async getInterpreterFromPipenv(pipenvCwd: string): Promise { + const interpreterPath = await this.getInterpreterPathFromPipenv(pipenvCwd); + if (!interpreterPath) { + return; + } + + const details = await this.helper.getInterpreterInformation(interpreterPath); + if (!details) { + return; + } + this._hasInterpreters.resolve(true); + await this.pipEnvServiceHelper.trackWorkspaceFolder(interpreterPath, Uri.file(pipenvCwd)); + return { + ...(details as PythonEnvironment), + path: interpreterPath, + envType: EnvironmentType.Pipenv, + pipEnvWorkspaceFolder: pipenvCwd, + }; + } + + private getPipenvWorkingDirectory(resource?: Uri): string | undefined { + // The file is not in a workspace. However, workspace may be opened + // and file is just a random file opened from elsewhere. In this case + // we still want to provide interpreter associated with the workspace. + // Otherwise if user tries and formats the file, we may end up using + // plain pip module installer to bring in the formatter and it is wrong. + const wsFolder = resource ? this.workspace.getWorkspaceFolder(resource) : undefined; + return wsFolder ? wsFolder.uri.fsPath : this.workspace.rootPath; + } + + private async getInterpreterPathFromPipenv(cwd: string, ignoreErrors = false): Promise { + // Quick check before actually running pipenv + if (!(await this.checkIfPipFileExists(cwd))) { + return; + } + try { + // call pipenv --version just to see if pipenv is in the PATH + const version = await this.invokePipenv('--version', cwd); + if (version === undefined) { + const appShell = this.serviceContainer.get(IApplicationShell); + appShell.showWarningMessage( + `Workspace contains Pipfile but '${this.executable}' was not found. Make sure '${this.executable}' is on the PATH.`, + ); + return; + } + // The --py command will fail if the virtual environment has not been setup yet. + // so call pipenv --venv to check for the virtual environment first. + const venv = await this.invokePipenv('--venv', cwd); + if (venv === undefined) { + const appShell = this.serviceContainer.get(IApplicationShell); + appShell.showWarningMessage( + 'Workspace contains Pipfile but the associated virtual environment has not been setup. Setup the virtual environment manually if needed.', + ); + return; + } + const pythonPath = await this.invokePipenv('--py', cwd); + return pythonPath && (await this.fs.fileExists(pythonPath)) ? pythonPath : undefined; + // tslint:disable-next-line:no-empty + } catch (error) { + traceError('PipEnv identification failed', error); + if (ignoreErrors) { + return undefined; + } + } + } + + private async checkIfPipFileExists(cwd: string): Promise { + const currentProcess = this.serviceContainer.get(ICurrentProcess); + const pipFileName = currentProcess.env[pipEnvFileNameVariable]; + if (typeof pipFileName === 'string' && (await this.fs.fileExists(path.join(cwd, pipFileName)))) { + return true; + } + if (await this.fs.fileExists(path.join(cwd, 'Pipfile'))) { + return true; + } + return false; + } + + private async invokePipenv(arg: string, rootPath: string): Promise { + try { + const processService = await this.processServiceFactory.create(Uri.file(rootPath)); + const execName = this.executable; + const result = await processService.exec(execName, [arg], { cwd: rootPath }); + if (result) { + const stdout = result.stdout ? result.stdout.trim() : ''; + const stderr = result.stderr ? result.stderr.trim() : ''; + if (stderr.length > 0 && stdout.length === 0) { + throw new Error(stderr); + } + return stdout; + } + // tslint:disable-next-line:no-empty + } catch (error) { + const platformService = this.serviceContainer.get(IPlatformService); + const currentProc = this.serviceContainer.get(ICurrentProcess); + const enviromentVariableValues: Record = { + LC_ALL: currentProc.env.LC_ALL, + LANG: currentProc.env.LANG, + }; + enviromentVariableValues[platformService.pathVariableName] = currentProc.env[platformService.pathVariableName]; + + traceWarning('Error in invoking PipEnv', error); + traceWarning(`Relevant Environment Variables ${JSON.stringify(enviromentVariableValues, undefined, 4)}`); + } + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/pipEnvServiceHelper.ts b/src/client/pythonEnvironments/discovery/locators/services/pipEnvServiceHelper.ts new file mode 100644 index 000000000000..56acffb936be --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/pipEnvServiceHelper.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IFileSystem } from '../../../../common/platform/types'; +import { IPersistentState, IPersistentStateFactory } from '../../../../common/types'; +import { IPipEnvServiceHelper } from '../../../../interpreter/locators/types'; + +type PipEnvInformation = { pythonPath: string; workspaceFolder: string; envName: string }; +@injectable() +export class PipEnvServiceHelper implements IPipEnvServiceHelper { + private initialized = false; + + private readonly state: IPersistentState>; + + constructor( + @inject(IPersistentStateFactory) private readonly statefactory: IPersistentStateFactory, + @inject(IFileSystem) private readonly fs: IFileSystem, + ) { + this.state = this.statefactory.createGlobalPersistentState>( + 'PipEnvInformation', + [], + ); + } + + public async getPipEnvInfo(pythonPath: string): Promise<{ workspaceFolder: Uri; envName: string } | undefined> { + await this.initializeStateStore(); + const info = this.state.value.find((item) => this.fs.arePathsSame(item.pythonPath, pythonPath)); + return info ? { workspaceFolder: Uri.file(info.workspaceFolder), envName: info.envName } : undefined; + } + + public async trackWorkspaceFolder(pythonPath: string, workspaceFolder: Uri): Promise { + await this.initializeStateStore(); + const values = [...this.state.value].filter((item) => !this.fs.arePathsSame(item.pythonPath, pythonPath)); + const envName = path.basename(workspaceFolder.fsPath); + values.push({ pythonPath, workspaceFolder: workspaceFolder.fsPath, envName }); + await this.state.updateValue(values); + } + + protected async initializeStateStore() { + if (this.initialized) { + return; + } + const list = await Promise.all( + this.state.value.map(async (item) => ((await this.fs.fileExists(item.pythonPath)) ? item : undefined)), + ); + const filteredList = list.filter((item) => !!item) as PipEnvInformation[]; + await this.state.updateValue(filteredList); + this.initialized = true; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/windowsRegistryService.ts b/src/client/pythonEnvironments/discovery/locators/services/windowsRegistryService.ts new file mode 100644 index 000000000000..0dabadf1fdf9 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/windowsRegistryService.ts @@ -0,0 +1,217 @@ +// tslint:disable:no-require-imports no-var-requires underscore-consistent-invocation + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { traceError } from '../../../../common/logger'; +import { + IFileSystem, IPlatformService, IRegistry, RegistryHive, +} from '../../../../common/platform/types'; +import { IPathUtils } from '../../../../common/types'; +import { Architecture } from '../../../../common/utils/platform'; +import { IInterpreterHelper } from '../../../../interpreter/contracts'; +import { IWindowsStoreInterpreter } from '../../../../interpreter/locators/types'; +import { IServiceContainer } from '../../../../ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../info'; +import { parsePythonVersion } from '../../../info/pythonVersion'; +import { CacheableLocatorService } from './cacheableLocatorService'; +import { AnacondaCompanyName, AnacondaCompanyNames } from './conda'; +import { WindowsStoreInterpreter } from './windowsStoreInterpreter'; + +const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); + +// tslint:disable-next-line:variable-name +const DefaultPythonExecutable = 'python.exe'; +// tslint:disable-next-line:variable-name +const CompaniesToIgnore = ['PYLAUNCHER']; +// tslint:disable-next-line:variable-name +const PythonCoreCompanyDisplayName = 'Python Software Foundation'; +// tslint:disable-next-line:variable-name +const PythonCoreComany = 'PYTHONCORE'; + +type CompanyInterpreter = { + companyKey: string; + hive: RegistryHive; + arch?: Architecture; +}; + +@injectable() +export class WindowsRegistryService extends CacheableLocatorService { + private readonly pathUtils: IPathUtils; + + private readonly fs: IFileSystem; + + constructor( + @inject(IRegistry) private registry: IRegistry, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(WindowsStoreInterpreter) private readonly windowsStoreInterpreter: IWindowsStoreInterpreter, + ) { + super('WindowsRegistryService', serviceContainer); + this.pathUtils = serviceContainer.get(IPathUtils); + this.fs = serviceContainer.get(IFileSystem); + } + + // tslint:disable-next-line:no-empty + public dispose() {} + + protected async getInterpretersImplementation(_resource?: Uri): Promise { + return this.platform.isWindows + ? this.getInterpretersFromRegistry().catch((ex) => { + traceError('Fetching interpreters from registry failed with error', ex); + return []; + }) + : []; + } + + private async getInterpretersFromRegistry() { + // https://github.com/python/peps/blob/master/pep-0514.txt#L357 + const hkcuArch = this.platform.is64bit ? undefined : Architecture.x86; + const promises: Promise[] = [ + this.getCompanies(RegistryHive.HKCU, hkcuArch), + this.getCompanies(RegistryHive.HKLM, Architecture.x86), + ]; + // https://github.com/Microsoft/PTVS/blob/ebfc4ca8bab234d453f15ee426af3b208f3c143c/Python/Product/Cookiecutter/Shared/Interpreters/PythonRegistrySearch.cs#L44 + if (this.platform.is64bit) { + promises.push(this.getCompanies(RegistryHive.HKLM, Architecture.x64)); + } + + const companies = await Promise.all(promises); + const companyInterpreters = await Promise.all( + flatten(companies) + .filter((item) => item !== undefined && item !== null) + .map((company) => this.getInterpretersForCompany(company.companyKey, company.hive, company.arch)), + ); + + return ( + flatten(companyInterpreters) + .filter((item) => item !== undefined && item !== null) + // tslint:disable-next-line:no-non-null-assertion + .map((item) => item!) + .reduce((prev, current) => { + if (prev.findIndex((item) => item.path.toUpperCase() === current.path.toUpperCase()) === -1) { + prev.push(current); + } + return prev; + }, []) + ); + } + + private async getCompanies(hive: RegistryHive, arch?: Architecture): Promise { + return this.registry.getKeys('\\Software\\Python', hive, arch).then((companyKeys) => companyKeys + .filter( + (companyKey) => CompaniesToIgnore.indexOf(this.pathUtils.basename(companyKey).toUpperCase()) === -1, + ) + .map((companyKey) => ({ companyKey, hive, arch }))); + } + + private async getInterpretersForCompany(companyKey: string, hive: RegistryHive, arch?: Architecture) { + const tagKeys = await this.registry.getKeys(companyKey, hive, arch); + return Promise.all( + tagKeys.map((tagKey) => this.getInreterpreterDetailsForCompany(tagKey, companyKey, hive, arch)), + ); + } + + private getInreterpreterDetailsForCompany( + tagKey: string, + companyKey: string, + hive: RegistryHive, + arch?: Architecture, + ): Promise { + const key = `${tagKey}\\InstallPath`; + type InterpreterInformation = + | null + | undefined + | { + installPath: string; + executablePath?: string; + displayName?: string; + version?: string; + companyDisplayName?: string; + }; + return this.registry + .getValue(key, hive, arch) + .then((installPath) => { + // Install path is mandatory. + if (!installPath) { + return Promise.resolve(null); + } + // Check if 'ExecutablePath' exists. + // Remember Python 2.7 doesn't have 'ExecutablePath' (there could be others). + // Treat all other values as optional. + return Promise.all([ + Promise.resolve(installPath), + this.registry.getValue(key, hive, arch, 'ExecutablePath'), + this.registry.getValue(tagKey, hive, arch, 'SysVersion'), + this.getCompanyDisplayName(companyKey, hive, arch), + ]).then(([installedPath, executablePath, version, companyDisplayName]) => { + companyDisplayName = AnacondaCompanyNames.indexOf(companyDisplayName!) === -1 + ? companyDisplayName + : AnacondaCompanyName; + // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion + return { + installPath: installedPath, + executablePath, + version, + companyDisplayName, + } as InterpreterInformation; + }); + }) + .then(async (interpreterInfo?: InterpreterInformation) => { + if (!interpreterInfo) { + return; + } + + const executablePath = interpreterInfo.executablePath && interpreterInfo.executablePath.length > 0 + ? interpreterInfo.executablePath + : path.join(interpreterInfo.installPath, DefaultPythonExecutable); + + if (this.windowsStoreInterpreter.isHiddenInterpreter(executablePath)) { + return; + } + const helper = this.serviceContainer.get(IInterpreterHelper); + const details = await helper.getInterpreterInformation(executablePath); + if (!details) { + return; + } + const version = interpreterInfo.version + ? this.pathUtils.basename(interpreterInfo.version) + : this.pathUtils.basename(tagKey); + this._hasInterpreters.resolve(true); + // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion + return { + ...(details as PythonEnvironment), + path: executablePath, + // Do not use version info from registry, this doesn't contain the release level. + // Give preference to what we have retrieved from getInterpreterInformation. + version: details.version || parsePythonVersion(version), + companyDisplayName: interpreterInfo.companyDisplayName, + envType: this.windowsStoreInterpreter.isWindowsStoreInterpreter(executablePath) + ? EnvironmentType.WindowsStore + : EnvironmentType.Unknown, + } as PythonEnvironment; + }) + .then((interpreter) => (interpreter + ? this.fs + .fileExists(interpreter.path) + .catch(() => false) + .then((exists) => (exists ? interpreter : null)) + : null)) + .catch((error) => { + traceError( + `Failed to retrieve interpreter details for company ${companyKey},tag: ${tagKey}, hive: ${hive}, arch: ${arch}`, + error, + ); + return null; + }); + } + + private async getCompanyDisplayName(companyKey: string, hive: RegistryHive, arch?: Architecture) { + const displayName = await this.registry.getValue(companyKey, hive, arch, 'DisplayName'); + if (displayName && displayName.length > 0) { + return displayName; + } + const company = this.pathUtils.basename(companyKey); + return company.toUpperCase() === PythonCoreComany ? PythonCoreCompanyDisplayName : company; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter.ts b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter.ts new file mode 100644 index 000000000000..72fe2b82e3a9 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { traceDecorators } from '../../../../common/logger'; +import { IFileSystem } from '../../../../common/platform/types'; +import { IPythonExecutionFactory } from '../../../../common/process/types'; +import { IPersistentStateFactory } from '../../../../common/types'; +import { IInterpreterHashProvider, IWindowsStoreInterpreter } from '../../../../interpreter/locators/types'; +import { IServiceContainer } from '../../../../ioc/types'; + +/** + * When using Windows Store interpreter the path that should be used is under + * %USERPROFILE%\AppData\Local\Microsoft\WindowsApps\python*.exe. The python.exe path + * under ProgramFiles\WindowsApps should not be used at all. Execute permissions on + * that instance of the store interpreter are restricted to system. Paths under + * %USERPROFILE%\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation* are also ok + * to use. But currently this results in duplicate entries. + * + * @param {string} pythonPath + * @returns {boolean} + */ +export function isRestrictedWindowsStoreInterpreterPath(pythonPath: string): boolean { + const pythonPathToCompare = pythonPath.toUpperCase().replace(/\//g, '\\'); + return ( + pythonPathToCompare.includes('\\Program Files\\WindowsApps\\'.toUpperCase()) + || pythonPathToCompare.includes('\\Microsoft\\WindowsApps\\PythonSoftwareFoundation'.toUpperCase()) + ); +} + +/** + * The default location of Windows apps are `%ProgramFiles%\WindowsApps`. + * (https://www.samlogic.net/articles/windows-8-windowsapps-folder.htm) + * When users access Python interpreter it is installed in `\Microsoft\WindowsApps` + * Based on our testing this is where Python interpreters installed from Windows Store is always installed. + * (unfortunately couldn't find any documentation on this). + * What we've identified is the fact that: + * - The Python interpreter in Microsoft\WIndowsApps\python.exe is a symbolic link to files located in: + * - Program Files\WindowsApps\ & Microsoft\WindowsApps\PythonSoftwareFoundation + * - I.e. they all point to the same place. + * However when the user launches the executable, its located in `Microsoft\WindowsApps\python.exe` + * Hence for all intensive purposes that's the main executable, that's what the user uses. + * As a result: + * - We'll only display what the user has access to, that being `Microsoft\WindowsApps\python.exe` + * - Others are hidden. + * + * Details can be found here (original issue https://github.com/microsoft/vscode-python/issues/5926). + * + * @export + * @class WindowsStoreInterpreter + * @implements {IWindowsStoreInterpreter} + * @implements {IInterpreterHashProvider} + */ +@injectable() +export class WindowsStoreInterpreter implements IWindowsStoreInterpreter, IInterpreterHashProvider { + constructor( + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(IPersistentStateFactory) private readonly persistentFactory: IPersistentStateFactory, + @inject(IFileSystem) private readonly fs: IFileSystem, + ) {} + + /** + * Whether this is a Windows Store/App Interpreter. + * + * @param {string} pythonPath + * @returns {boolean} + * @memberof WindowsStoreInterpreter + */ + public isWindowsStoreInterpreter(pythonPath: string): boolean { + const pythonPathToCompare = pythonPath.toUpperCase().replace(/\//g, '\\'); + return ( + pythonPathToCompare.includes('\\Microsoft\\WindowsApps\\'.toUpperCase()) + || pythonPathToCompare.includes('\\Program Files\\WindowsApps\\'.toUpperCase()) + || pythonPathToCompare.includes('\\Microsoft\\WindowsApps\\PythonSoftwareFoundation'.toUpperCase()) + ); + } + + /** + * Whether this is a python executable in a windows app store folder that is internal and can be hidden from users. + * Interpreters that fall into this category will not be displayed to the users. + * + * @param {string} pythonPath + * @returns {Promise} + * @memberof IInterpreterHelper + */ + public isHiddenInterpreter(pythonPath: string): boolean { + return isRestrictedWindowsStoreInterpreterPath(pythonPath); + } + + /** + * Gets the hash of the Python interpreter (installed from the windows store). + * We need to use a special way to get the hash for these, by first resolving the + * path to the actual executable and then calculating the hash on that file. + * + * Using fs.lstat or similar nodejs functions do not work, as these are some weird form of symbolic linked files. + * + * Note: Store the hash in a temporary state store (as we're spawning processes here). + * Spawning processes to get a hash of a terminal is expensive. + * Hence to minimize resource usage (just to get a file hash), we will cache the generated hash for 1hr. + * (why 1hr, simple, why 2hrs, or 3hrs.) + * If user installs/updates/uninstalls Windows Store Python apps, 1hr is enough time to get things rolling again. + * + * @param {string} pythonPath + * @returns {Promise} + * @memberof InterpreterHelper + */ + @traceDecorators.error('Get Windows Store Interpreter Hash') + public async getInterpreterHash(pythonPath: string): Promise { + const key = `WINDOWS_STORE_INTERPRETER_HASH_${pythonPath}`; + const stateStore = this.persistentFactory.createGlobalPersistentState( + key, + undefined, + 60 * 60 * 1000, + ); + + if (stateStore.value) { + return stateStore.value; + } + const executionFactory = this.serviceContainer.get(IPythonExecutionFactory); + const pythonService = await executionFactory.create({ pythonPath }); + const executablePath = await pythonService.getExecutablePath(); + // If we are unable to get file hash of executable, then get hash of parent directory. + // Its likely it will fail for the executable (fails during development, but try nevertheless - in case things start working). + const hash = await this.fs + .getFileHash(executablePath) + .catch(() => this.fs.getFileHash(path.dirname(executablePath))); + await stateStore.updateValue(hash); + + return hash; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts new file mode 100644 index 000000000000..6eae9dac5126 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/windowsStoreLocator.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fsapi from 'fs-extra'; +import * as path from 'path'; +import { traceWarning } from '../../../../common/logger'; +import { getEnvironmentVariable } from '../../../../common/utils/platform'; +import { isWindowsPythonExe } from '../../../common/windowsUtils'; + +/** + * Gets path to the Windows Apps directory. + * @returns {string} : Returns path to the Windows Apps directory under + * `%LOCALAPPDATA%/Microsoft/WindowsApps`. + */ +export function getWindowsStoreAppsRoot(): string { + const localAppData = getEnvironmentVariable('LOCALAPPDATA') || ''; + return path.join(localAppData, 'Microsoft', 'WindowsApps'); +} + +/** + * Checks if a given path is under the forbidden windows store directory. + * @param {string} interpreterPath : Absolute path to the python interpreter. + * @returns {boolean} : Returns true if `interpreterPath` is under + * `%ProgramFiles%/WindowsApps`. + */ +export function isForbiddenStorePath(interpreterPath:string):boolean { + const programFilesStorePath = path + .join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps') + .normalize() + .toUpperCase(); + return path.normalize(interpreterPath).toUpperCase().includes(programFilesStorePath); +} + +/** + * Checks if the given interpreter belongs to Windows Store Python environment. + * @param interpreterPath: Absolute path to any python interpreter. + * + * Remarks: + * 1. Checking if the path includes `Microsoft\WindowsApps`, `Program Files\WindowsApps`, is + * NOT enough. In WSL, `/mnt/c/users/user/AppData/Local/Microsoft/WindowsApps` is available as a search + * path. It is possible to get a false positive for that path. So the comparison should check if the + * absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to + * 'WindowsApps' is not a valid path to access, Windows Store Python. + * + * 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows. + * + * 3. A limitation of the checks here is that they don't handle 8.3 style windows paths. + * For example, + * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` + * is the shortened form of + * `C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe` + * + * The correct way to compare these would be to always convert given paths to long path (or to short path). + * For either approach to work correctly you need actual file to exist, and accessible from the user's + * account. + * + * To convert to short path without using N-API in node would be to use this command. This is very expensive: + * `> cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA` + * The above command will print out this: + * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` + * + * If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from, + * Kernel32 to convert between the two path variants. + * + */ +export async function isWindowsStoreEnvironment(interpreterPath: string): Promise { + const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase(); + const localAppDataStorePath = path + .normalize(getWindowsStoreAppsRoot()) + .toUpperCase(); + if (pythonPathToCompare.includes(localAppDataStorePath)) { + return true; + } + + // Program Files store path is a forbidden path. Only admins and system has access this path. + // We should never have to look at this path or even execute python from this path. + if (isForbiddenStorePath(pythonPathToCompare)) { + traceWarning('isWindowsStoreEnvironment called with Program Files store path.'); + return true; + } + return false; +} + +/** + * Gets paths to the Python executable under Windows Store apps. + * @returns: Returns python*.exe for the windows store app root directory. + * + * Remarks: We don't need to find the path to the interpreter under the specific application + * directory. Such as: + * `%LOCALAPPDATA%/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0` + * The same python executable is also available at: + * `%LOCALAPPDATA%/Microsoft/WindowsApps` + * It would be a duplicate. + * + * All python executable under `%LOCALAPPDATA%/Microsoft/WindowsApps` or the sub-directories + * are 'reparse points' that point to the real executable at `%PROGRAMFILES%/WindowsApps`. + * However, that directory is off limits to users. So no need to populate interpreters from + * that location. + */ +export async function getWindowsStorePythonExes(): Promise { + const windowsAppsRoot = getWindowsStoreAppsRoot(); + + // Collect python*.exe directly under %LOCALAPPDATA%/Microsoft/WindowsApps + const files = await fsapi.readdir(windowsAppsRoot); + return files + .map((filename:string) => path.join(windowsAppsRoot, filename)) + .filter(isWindowsPythonExe); +} + +// tslint:disable-next-line: no-suspicious-comment +// TODO: The above APIs will be consumed by the Windows Store locator class when we have it. diff --git a/src/client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvService.ts b/src/client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvService.ts new file mode 100644 index 000000000000..a3e3b99f3176 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvService.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-require-imports + +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../../common/application/types'; +import { IConfigurationService } from '../../../../common/types'; +import { + IInterpreterWatcher, + IInterpreterWatcherBuilder, + IVirtualEnvironmentsSearchPathProvider, +} from '../../../../interpreter/contracts'; +import { IServiceContainer } from '../../../../ioc/types'; +import { BaseVirtualEnvService } from './baseVirtualEnvService'; + +// tslint:disable-next-line: no-var-requires +const untildify = require('untildify'); + +@injectable() +export class WorkspaceVirtualEnvService extends BaseVirtualEnvService { + public constructor( + @inject(IVirtualEnvironmentsSearchPathProvider) + @named('workspace') + workspaceVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider, + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IInterpreterWatcherBuilder) private readonly builder: IInterpreterWatcherBuilder, + ) { + super(workspaceVirtualEnvPathProvider, serviceContainer, 'WorkspaceVirtualEnvService', true); + } + + protected async getInterpreterWatchers(resource: Uri | undefined): Promise { + return [await this.builder.getWorkspaceVirtualEnvInterpreterWatcher(resource)]; + } +} + +@injectable() +export class WorkspaceVirtualEnvironmentsSearchPathProvider implements IVirtualEnvironmentsSearchPathProvider { + public constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + + public async getSearchPaths(resource?: Uri): Promise { + const configService = this.serviceContainer.get(IConfigurationService); + const paths: string[] = []; + const { venvPath } = configService.getSettings(resource); + if (venvPath) { + paths.push(untildify(venvPath)); + } + const workspaceService = this.serviceContainer.get(IWorkspaceService); + if (Array.isArray(workspaceService.workspaceFolders) && workspaceService.workspaceFolders.length > 0) { + let wsPath: string | undefined; + if (resource && workspaceService.workspaceFolders.length > 1) { + const wkspaceFolder = workspaceService.getWorkspaceFolder(resource); + if (wkspaceFolder) { + wsPath = wkspaceFolder.uri.fsPath; + } + } else { + wsPath = workspaceService.workspaceFolders[0].uri.fsPath; + } + if (wsPath) { + paths.push(wsPath); + paths.push(path.join(wsPath, '.direnv')); + } + } + return paths; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvWatcherService.ts b/src/client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvWatcherService.ts new file mode 100644 index 000000000000..592c65e4315d --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvWatcherService.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { + Disposable, Event, EventEmitter, FileSystemWatcher, RelativePattern, Uri, +} from 'vscode'; +import { IWorkspaceService } from '../../../../common/application/types'; +import '../../../../common/extensions'; +import { traceDecorators, traceVerbose } from '../../../../common/logger'; +import { IPlatformService } from '../../../../common/platform/types'; +import { IPythonExecutionFactory } from '../../../../common/process/types'; +import { IDisposableRegistry, Resource } from '../../../../common/types'; +import { IInterpreterWatcher } from '../../../../interpreter/contracts'; + +const maxTimeToWaitForEnvCreation = 60_000; +const timeToPollForEnvCreation = 2_000; + +@injectable() +export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, Disposable { + private readonly didCreate: EventEmitter; + + private timers = new Map(); + + private fsWatchers: FileSystemWatcher[] = []; + + private resource: Resource; + + constructor( + @inject(IDisposableRegistry) private readonly disposableRegistry: Disposable[], + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, + ) { + this.didCreate = new EventEmitter(); + disposableRegistry.push(this); + } + + public get onDidCreate(): Event { + return this.didCreate.event; + } + + public dispose() { + this.clearTimers(); + } + + @traceDecorators.verbose('Register Interpreter Watcher') + public async register(resource: Resource): Promise { + if (this.fsWatchers.length > 0) { + return; + } + this.resource = resource; + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + const executable = this.platformService.isWindows ? 'python.exe' : 'python'; + const patterns = [path.join('*', executable), path.join('*', '*', executable)]; + + for (const pattern of patterns) { + const globPatern = workspaceFolder ? new RelativePattern(workspaceFolder.uri.fsPath, pattern) : pattern; + traceVerbose(`Create file systemwatcher with pattern ${pattern}`); + + const fsWatcher = this.workspaceService.createFileSystemWatcher(globPatern); + fsWatcher.onDidCreate((e) => this.createHandler(e), this, this.disposableRegistry); + + this.disposableRegistry.push(fsWatcher); + this.fsWatchers.push(fsWatcher); + } + } + + @traceDecorators.verbose('Interpreter Watcher change handler') + public async createHandler(e: Uri) { + this.didCreate.fire(this.resource); + // On Windows, creation of environments are very slow, hence lets notify again after + // the python executable is accessible (i.e. when we can launch the process). + this.notifyCreationWhenReady(e.fsPath).ignoreErrors(); + } + + protected async notifyCreationWhenReady(pythonPath: string) { + const counter = this.timers.has(pythonPath) ? this.timers.get(pythonPath)!.counter + 1 : 0; + const isValid = await this.isValidExecutable(pythonPath); + if (isValid) { + if (counter > 0) { + this.didCreate.fire(this.resource); + } + return this.timers.delete(pythonPath); + } + if (counter > maxTimeToWaitForEnvCreation / timeToPollForEnvCreation) { + // Send notification before we give up trying. + this.didCreate.fire(this.resource); + this.timers.delete(pythonPath); + return; + } + + const timer = setTimeout( + () => this.notifyCreationWhenReady(pythonPath).ignoreErrors(), + timeToPollForEnvCreation, + ); + this.timers.set(pythonPath, { timer, counter }); + } + + private clearTimers() { + // tslint:disable-next-line: no-any + this.timers.forEach((item) => clearTimeout(item.timer as any)); + this.timers.clear(); + } + + private async isValidExecutable(pythonPath: string): Promise { + const execService = await this.pythonExecFactory.create({ pythonPath }); + const info = await execService.getInterpreterInformation().catch(() => undefined); + return info !== undefined; + } +} diff --git a/src/client/pythonEnvironments/discovery/locators/types.ts b/src/client/pythonEnvironments/discovery/locators/types.ts new file mode 100644 index 000000000000..ffcace4f8022 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/locators/types.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { GetInterpreterOptions } from '../../../interpreter/interpreterService'; + +export type GetInterpreterLocatorOptions = GetInterpreterOptions & { ignoreCache?: boolean }; diff --git a/src/client/pythonEnvironments/discovery/subenv.ts b/src/client/pythonEnvironments/discovery/subenv.ts new file mode 100644 index 000000000000..42cf092c5681 --- /dev/null +++ b/src/client/pythonEnvironments/discovery/subenv.ts @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { EnvironmentType } from '../info'; +import { getPyenvTypeFinder } from './globalenv'; + +type ExecFunc = (cmd: string, args: string[]) => Promise<{ stdout: string }>; + +type NameFinderFunc = (python: string) => Promise; +type TypeFinderFunc = (python: string) => Promise; +type ExecutableFinderFunc = (python: string) => Promise; + +/** + * Determine the environment name for the given Python executable. + * + * @param python - the executable to inspect + * @param finders - the functions specific to different Python environment types + */ +export async function getName(python: string, finders: NameFinderFunc[]): Promise { + for (const find of finders) { + const found = await find(python); + if (found && found !== '') { + return found; + } + } + return undefined; +} + +/** + * Determine the environment type for the given Python executable. + * + * @param python - the executable to inspect + * @param finders - the functions specific to different Python environment types + */ +export async function getType(python: string, finders: TypeFinderFunc[]): Promise { + for (const find of finders) { + const found = await find(python); + if (found && found !== EnvironmentType.Unknown) { + return found; + } + } + return undefined; +} + +// ======= default sets ======== + +/** + * Build the list of default "name finder" functions to pass to `getName()`. + * + * @param dirname - the "root" of a directory tree to search + * @param pathDirname - typically `path.dirname` + * @param pathBasename - typically `path.basename` + * @param isPipenvRoot - a function the determines if it's a pipenv dir + */ +export function getNameFinders( + dirname: string | undefined, + // + pathDirname: (filename: string) => string, + pathBasename: (filename: string) => string, + // + isPipenvRoot: (dir: string, python: string) => Promise, +): NameFinderFunc[] { + return [ + // Note that currently there is only one finder function in + // the list. That is only a temporary situation as we + // consolidate code under the py-envs component. + async (python) => { + if (dirname && (await isPipenvRoot(dirname, python))) { + // In pipenv, return the folder name of the root dir. + return pathBasename(dirname); + } + return pathBasename(pathDirname(pathDirname(python))); + }, + ]; +} + +/** + * Build the list of default "type finder" functions to pass to `getType()`. + * + * @param homedir - the user's home directory (e.g. `$HOME`) + * @param scripts - the names of possible activation scripts (e.g. `activate.sh`) + * @param pathSep - the path separator to use (typically `path.sep`) + * @param pathJoin - typically `path.join` + * @param pathDirname - typically `path.dirname` + * @param getCurDir - a function that returns `$CWD` + * @param isPipenvRoot - a function the determines if it's a pipenv dir + * @param getEnvVar - a function to look up a process environment variable (i,e. `process.env[name]`) + * @param fileExists - typically `fs.exists` + * @param exec - the function to use to run a command in a subprocess + */ +export function getTypeFinders( + homedir: string, + scripts: string[], + // + pathSep: string, + pathJoin: (...parts: string[]) => string, + pathDirname: (filename: string) => string, + // + getCurDir: () => Promise, + isPipenvRoot: (dir: string, python: string) => Promise, + getEnvVar: (name: string) => string | undefined, + fileExists: (n: string) => Promise, + exec: ExecFunc, +): TypeFinderFunc[] { + return [ + getVenvTypeFinder(pathDirname, pathJoin, fileExists), + // For now we treat pyenv as a "virtual" environment (to keep compatibility)... + getPyenvTypeFinder(homedir, pathSep, pathJoin, getEnvVar, exec), + getPipenvTypeFinder(getCurDir, isPipenvRoot), + getVirtualenvTypeFinder(scripts, pathDirname, pathJoin, fileExists), + // Lets not try to determine whether this is a conda environment or not. + ]; +} + +// ======= venv ======== + +/** + * Build a "type finder" function that identifies venv environments. + * + * @param pathDirname - typically `path.dirname` + * @param pathJoin - typically `path.join` + * @param fileExists - typically `fs.exists` + */ +export function getVenvTypeFinder( + // + pathDirname: (filename: string) => string, + pathJoin: (...parts: string[]) => string, + // + fileExists: (n: string) => Promise, +): TypeFinderFunc { + return async (python: string) => { + const dir = pathDirname(python); + const VENVFILES = ['pyvenv.cfg', pathJoin('..', 'pyvenv.cfg')]; + const cfgFiles = VENVFILES.map((file) => pathJoin(dir, file)); + for (const file of cfgFiles) { + if (await fileExists(file)) { + return EnvironmentType.Venv; + } + } + return undefined; + }; +} + +/** + * Build an "executable finder" function that identifies venv environments. + * + * @param basename - the venv name or names to look for + * @param pathDirname - typically `path.dirname` + * @param pathJoin - typically `path.join` + * @param fileExists - typically `fs.exists` + */ +export function getVenvExecutableFinder( + basename: string | string[], + // + pathDirname: (filename: string) => string, + pathJoin: (...parts: string[]) => string, + // + fileExists: (n: string) => Promise, +): ExecutableFinderFunc { + const basenames = typeof basename === 'string' ? [basename] : basename; + return async (python: string) => { + // Generated scripts are found in the same directory as the interpreter. + const binDir = pathDirname(python); + for (const name of basenames) { + const filename = pathJoin(binDir, name); + if (await fileExists(filename)) { + return filename; + } + } + // No matches so return undefined. + }; +} + +// ======= virtualenv ======== + +/** + * Build a "type finder" function that identifies virtualenv environments. + * + * @param scripts - the names of possible activation scripts (e.g. `activate.sh`) + * @param pathDirname - typically `path.dirname` + * @param pathJoin - typically `path.join` + * @param fileExists - typically `fs.exists` + */ +export function getVirtualenvTypeFinder( + scripts: string[], + // + pathDirname: (filename: string) => string, + pathJoin: (...parts: string[]) => string, + // + fileExists: (n: string) => Promise, +) { + const find = getVenvExecutableFinder(scripts, pathDirname, pathJoin, fileExists); + return async (python: string) => { + const found = await find(python); + return found !== undefined ? EnvironmentType.VirtualEnv : undefined; + }; +} + +// ======= pipenv ======== + +/** + * Build a "type finder" function that identifies pipenv environments. + * + * @param getCurDir - a function that returns `$CWD` + * @param isPipenvRoot - a function the determines if it's a pipenv dir + */ +export function getPipenvTypeFinder( + getCurDir: () => Promise, + isPipenvRoot: (dir: string, python: string) => Promise, +) { + return async (python: string) => { + const curDir = await getCurDir(); + if (curDir && (await isPipenvRoot(curDir, python))) { + return EnvironmentType.Pipenv; + } + return undefined; + }; +} diff --git a/src/client/pythonEnvironments/exec.ts b/src/client/pythonEnvironments/exec.ts new file mode 100644 index 000000000000..f758fa1dbb3b --- /dev/null +++ b/src/client/pythonEnvironments/exec.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * A representation of the information needed to run a Python executable. + * + * @prop command - the executable to execute in a new OS process + * @prop args - the full list of arguments with which to invoke the command + * @prop python - the command + the arguments needed just to invoke Python + * @prop pythonExecutable - the path the the Python executable + */ +export type PythonExecInfo = { + command: string; + args: string[]; + + python: string[]; + pythonExecutable: string; +}; + +/** + * Compose Python execution info for the given executable. + * + * @param python - the path (or command + arguments) to use to invoke Python + * @param pythonArgs - any extra arguments to use when running Python + */ +export function buildPythonExecInfo(python: string | string[], pythonArgs?: string[]): PythonExecInfo { + if (Array.isArray(python)) { + const args = python.slice(1); + if (pythonArgs) { + args.push(...pythonArgs); + } + return { + args, + command: python[0], + python: [...python], + // It isn't necessarily the last item but our supported + // cases it currently is. + pythonExecutable: python[python.length - 1], + }; + } + return { + command: python, + args: pythonArgs || [], + python: [python], + pythonExecutable: python, + }; +} + +/** + * Create a copy, optionally adding to the args to pass to Python. + * + * @param orig - the object to copy + * @param extraPythonArgs - any arguments to add to the end of `orig.args` + */ +export function copyPythonExecInfo(orig: PythonExecInfo, extraPythonArgs?: string[]): PythonExecInfo { + const info = { + command: orig.command, + args: [...orig.args], + python: [...orig.python], + pythonExecutable: orig.pythonExecutable, + }; + if (extraPythonArgs) { + info.args.push(...extraPythonArgs); + } + if (info.pythonExecutable === undefined) { + // It isn't necessarily the last item but our supported + // cases it currently is. + info.pythonExecutable = info.python[info.python.length - 1]; + } + return info; +} diff --git a/src/client/pythonEnvironments/info/environmentInfoService.ts b/src/client/pythonEnvironments/info/environmentInfoService.ts new file mode 100644 index 000000000000..12a84f017692 --- /dev/null +++ b/src/client/pythonEnvironments/info/environmentInfoService.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import { EnvironmentType, PythonEnvironment } from '.'; +import { createWorkerPool, IWorkerPool, QueuePosition } from '../../common/utils/workerPool'; +import { shellExecute } from '../common/externalDependencies'; +import { buildPythonExecInfo } from '../exec'; +import { getInterpreterInfo } from './interpreter'; + +export enum EnvironmentInfoServiceQueuePriority { + Default, + High +} + +export const IEnvironmentInfoService = Symbol('IEnvironmentInfoService'); +export interface IEnvironmentInfoService { + getEnvironmentInfo( + interpreterPath: string, + priority?: EnvironmentInfoServiceQueuePriority + ): Promise; +} + +async function buildEnvironmentInfo(interpreterPath: string): Promise { + const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(interpreterPath), shellExecute); + if (interpreterInfo === undefined || interpreterInfo.version === undefined) { + return undefined; + } + return { + path: interpreterInfo.path, + // Have to do this because the type returned by getInterpreterInfo is SemVer + // But we expect this to be PythonVersion + version: { + raw: interpreterInfo.version.raw, + major: interpreterInfo.version.major, + minor: interpreterInfo.version.minor, + patch: interpreterInfo.version.patch, + build: interpreterInfo.version.build, + prerelease: interpreterInfo.version.prerelease, + }, + sysVersion: interpreterInfo.sysVersion, + architecture: interpreterInfo.architecture, + sysPrefix: interpreterInfo.sysPrefix, + pipEnvWorkspaceFolder: interpreterInfo.pipEnvWorkspaceFolder, + companyDisplayName: '', + displayName: '', + envType: EnvironmentType.Unknown, // Code to handle This will be added later. + envName: '', + envPath: '', + cachedEntry: false, + }; +} + +@injectable() +export class EnvironmentInfoService implements IEnvironmentInfoService { + // Caching environment here in-memory. This is so that we don't have to run this on the same + // path again and again in a given session. This information will likely not change in a given + // session. There are definitely cases where this will change. But a simple reload should address + // those. + private readonly cache: Map = new Map(); + + private readonly workerPool: IWorkerPool; + + public constructor() { + this.workerPool = createWorkerPool(buildEnvironmentInfo); + } + + public async getEnvironmentInfo( + interpreterPath: string, + priority?: EnvironmentInfoServiceQueuePriority, + ): Promise { + const result = this.cache.get(interpreterPath); + if (result !== undefined) { + return result; + } + + return (priority === EnvironmentInfoServiceQueuePriority.High + ? this.workerPool.addToQueue(interpreterPath, QueuePosition.Front) + : this.workerPool.addToQueue(interpreterPath, QueuePosition.Back) + ).then((r) => { + if (r !== undefined) { + this.cache.set(interpreterPath, r); + } + return r; + }); + } +} diff --git a/src/client/pythonEnvironments/info/executable.ts b/src/client/pythonEnvironments/info/executable.ts new file mode 100644 index 000000000000..f561a4cc077e --- /dev/null +++ b/src/client/pythonEnvironments/info/executable.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { getExecutable as getPythonExecutableCommand } from '../../common/process/internal/python'; +import { copyPythonExecInfo, PythonExecInfo } from '../exec'; + +type ExecResult = { + stdout: string; +}; +type ExecFunc = (command: string, args: string[]) => Promise; + +/** + * Find the filename for the corresponding Python executable. + * + * Effectively, we look up `sys.executable`. + * + * @param python - the information to use when running Python + * @param exec - the function to use to run Python + */ +export async function getExecutablePath(python: PythonExecInfo, exec: ExecFunc): Promise { + const [args, parse] = getPythonExecutableCommand(); + const info = copyPythonExecInfo(python, args); + const result = await exec(info.command, info.args); + return parse(result.stdout); +} diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts new file mode 100644 index 000000000000..ba0c90b5ab68 --- /dev/null +++ b/src/client/pythonEnvironments/info/index.ts @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import * as semver from 'semver'; +import { IFileSystem } from '../../common/platform/types'; +import { Architecture } from '../../common/utils/platform'; +import { areSameVersion, PythonVersion } from './pythonVersion'; + +/** + * The supported Python environment types. + */ +export enum EnvironmentType { + Unknown = 'Unknown', + Conda = 'Conda', + VirtualEnv = 'VirtualEnv', + Pipenv = 'PipEnv', + Pyenv = 'Pyenv', + Venv = 'Venv', + WindowsStore = 'WindowsStore', + Poetry = 'Poetry', + VirtualEnvWrapper = 'VirtualEnvWrapper', + Global = 'Global', + System = 'System' +} + +type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final' | 'unknown'; + +/** + * The components of a Python version. + * + * These match the elements of `sys.version_info`. + */ +export type PythonVersionInfo = [number, number, number, ReleaseLevel]; + +/** + * Details about a Python runtime. + * + * @prop path - the location of the executable file + * @prop version - the runtime version + * @prop sysVersion - the raw value of `sys.version` + * @prop architecture - of the host CPU (e.g. `x86`) + * @prop sysPrefix - the environment's install root (`sys.prefix`) + * @prop pipEnvWorkspaceFolder - the pipenv root, if applicable + */ +export type InterpreterInformation = { + path: string; + version?: PythonVersion; + sysVersion?: string; + architecture: Architecture; + sysPrefix: string; + pipEnvWorkspaceFolder?: string; +}; + +/** + * Details about a Python environment. + * + * @prop companyDisplayName - the user-facing name of the distro publisher + * @prop displayName - the user-facing name for the environment + * @prop type - the kind of Python environment + * @prop envName - the environment's name, if applicable (else `envPath` is set) + * @prop envPath - the environment's root dir, if applicable (else `envName`) + * @prop cachedEntry - whether or not the info came from a cache + */ +// Note that "cachedEntry" is specific to the caching machinery +// and doesn't really belong here. +export type PythonEnvironment = InterpreterInformation & { + companyDisplayName?: string; + displayName?: string; + envType: EnvironmentType; + envName?: string; + envPath?: string; + cachedEntry?: boolean; +}; + +/** + * Python environment containing only partial info. But it will contain the environment path. + */ +export type PartialPythonEnvironment = Partial> & { path: string }; + +/** + * Standardize the given env info. + * + * @param environment = the env info to normalize + */ +export function normalizeEnvironment(environment: PartialPythonEnvironment): void { + environment.path = path.normalize(environment.path); +} + +/** + * Convert the Python environment type to a user-facing name. + */ +export function getEnvironmentTypeName(environmentType: EnvironmentType) { + switch (environmentType) { + case EnvironmentType.Conda: { + return 'conda'; + } + case EnvironmentType.Pipenv: { + return 'pipenv'; + } + case EnvironmentType.Pyenv: { + return 'pyenv'; + } + case EnvironmentType.Venv: { + return 'venv'; + } + case EnvironmentType.VirtualEnv: { + return 'virtualenv'; + } + default: { + return ''; + } + } +} + +/** + * Determine if the given infos correspond to the same env. + * + * @param environment1 - one of the two envs to compare + * @param environment2 - one of the two envs to compare + */ +export function areSameEnvironment( + environment1: PartialPythonEnvironment | undefined, + environment2: PartialPythonEnvironment | undefined, + fs: IFileSystem, +): boolean { + if (!environment1 || !environment2) { + return false; + } + if (fs.arePathsSame(environment1.path, environment2.path)) { + return true; + } + if (!areSameVersion(environment1.version, environment2.version)) { + return false; + } + // Could be Python 3.6 with path = python.exe, and Python 3.6 + // and path = python3.exe, so we check the parent directory. + if (!inSameDirectory(environment1.path, environment2.path, fs)) { + return false; + } + return true; +} + +/** + * Update one env info with another. + * + * @param environment - the info to update + * @param other - the info to copy in + */ +export function updateEnvironment(environment: PartialPythonEnvironment, other: PartialPythonEnvironment): void { + // Preserve type information. + // Possible we identified environment as unknown, but a later provider has identified env type. + if (environment.envType === EnvironmentType.Unknown && other.envType && other.envType !== EnvironmentType.Unknown) { + environment.envType = other.envType; + } + const props: (keyof PythonEnvironment)[] = [ + 'envName', + 'envPath', + 'path', + 'sysPrefix', + 'architecture', + 'sysVersion', + 'version', + 'pipEnvWorkspaceFolder', + ]; + props.forEach((prop) => { + if (!environment[prop] && other[prop]) { + // tslint:disable-next-line: no-any + (environment as any)[prop] = other[prop]; + } + }); +} + +/** + * Combine env info for matching environments. + * + * Environments are matched by path and version. + * + * @param environments - the env infos to merge + */ +export function mergeEnvironments( + environments: PartialPythonEnvironment[], + fs: IFileSystem, +): PartialPythonEnvironment[] { + return environments.reduce((accumulator, current) => { + const existingItem = accumulator.find((item) => areSameEnvironment(current, item, fs)); + if (!existingItem) { + const copied: PartialPythonEnvironment = { ...current }; + normalizeEnvironment(copied); + accumulator.push(copied); + } else { + updateEnvironment(existingItem, current); + } + return accumulator; + }, []); +} + +/** + * Determine if the given paths are in the same directory. + * + * @param path1 - one of the two paths to compare + * @param path2 - one of the two paths to compare + */ +export function inSameDirectory(path1: string | undefined, path2: string | undefined, fs: IFileSystem): boolean { + if (!path1 || !path2) { + return false; + } + const dir1 = path.dirname(path1); + const dir2 = path.dirname(path2); + return fs.arePathsSame(dir1, dir2); +} + +/** + * Build a version-sorted list from the given one, with lowest first. + */ +export function sortInterpreters(interpreters: PythonEnvironment[]): PythonEnvironment[] { + if (interpreters.length === 0) { + return []; + } + if (interpreters.length === 1) { + return [interpreters[0]]; + } + const sorted = interpreters.slice(); + sorted.sort((a, b) => (a.version && b.version ? semver.compare(a.version.raw, b.version.raw) : 0)); + return sorted; +} diff --git a/src/client/pythonEnvironments/info/interpreter.ts b/src/client/pythonEnvironments/info/interpreter.ts new file mode 100644 index 000000000000..5082c38ea727 --- /dev/null +++ b/src/client/pythonEnvironments/info/interpreter.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { InterpreterInformation } from '.'; +import { interpreterInfo as getInterpreterInfoCommand, PythonEnvInfo } from '../../common/process/internal/scripts'; +import { Architecture } from '../../common/utils/platform'; +import { copyPythonExecInfo, PythonExecInfo } from '../exec'; +import { parsePythonVersion } from './pythonVersion'; + +/** + * Compose full interpreter information based on the given data. + * + * The data format corresponds to the output of the `interpreterInfo.py` script. + * + * @param python - the path to the Python executable + * @param raw - the information returned by the `interpreterInfo.py` script + */ +export function extractInterpreterInfo(python: string, raw: PythonEnvInfo): InterpreterInformation { + const rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}-${raw.versionInfo[3]}`; + return { + architecture: raw.is64Bit ? Architecture.x64 : Architecture.x86, + path: python, + version: parsePythonVersion(rawVersion), + sysVersion: raw.sysVersion, + sysPrefix: raw.sysPrefix, + }; +} + +type ShellExecResult = { + stdout: string; + stderr?: string; +}; +type ShellExecFunc = (command: string, timeout: number) => Promise; + +type Logger = { + info(msg: string): void; + error(msg: string): void; +}; + +/** + * Collect full interpreter information from the given Python executable. + * + * @param python - the information to use when running Python + * @param shellExec - the function to use to exec Python + * @param logger - if provided, used to log failures or other info + */ +export async function getInterpreterInfo( + python: PythonExecInfo, + shellExec: ShellExecFunc, + logger?: Logger, +): Promise { + const [args, parse] = getInterpreterInfoCommand(); + const info = copyPythonExecInfo(python, args); + const argv = [info.command, ...info.args]; + + // Concat these together to make a set of quoted strings + const quoted = argv.reduce((p, c) => (p ? `${p} "${c}"` : `"${c.replace('\\', '\\\\')}"`), ''); + + // Try shell execing the command, followed by the arguments. This will make node kill the process if it + // takes too long. + // Sometimes the python path isn't valid, timeout if that's the case. + // See these two bugs: + // https://github.com/microsoft/vscode-python/issues/7569 + // https://github.com/microsoft/vscode-python/issues/7760 + const result = await shellExec(quoted, 15000); + if (result.stderr) { + if (logger) { + logger.error(`Failed to parse interpreter information for ${argv} stderr: ${result.stderr}`); + } + return; + } + const json = parse(result.stdout); + if (logger) { + logger.info(`Found interpreter for ${argv}`); + } + return extractInterpreterInfo(python.pythonExecutable, json); +} diff --git a/src/client/pythonEnvironments/info/pythonVersion.ts b/src/client/pythonEnvironments/info/pythonVersion.ts new file mode 100644 index 000000000000..ccd9524d0388 --- /dev/null +++ b/src/client/pythonEnvironments/info/pythonVersion.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { SemVer } from 'semver'; +import '../../common/extensions'; // For string.splitLines() +import { getVersion as getPythonVersionCommand } from '../../common/process/internal/python'; + +/** + * A representation of a Python runtime's version. + * + * @prop raw - the original version string + * @prop major - the "major" version + * @prop minor - the "minor" version + * @prop patch - the "patch" (or "micro") version + * @prop build - the build ID of the executable + * @prop prerelease - identifies a tag in the release process (e.g. beta 1) + */ +// Note that this is currently compatible with SemVer objects, +// but we may change it to match the format of sys.version_info. +export type PythonVersion = { + raw: string; + major: number; + minor: number; + patch: number; + // Eventually it may be useful to match what sys.version_info + // provides for the remainder here: + // * releaseLevel: 'alpha' | 'beta' | 'candidate' | 'final'; + // * serial: number; + build: string[]; + prerelease: string[]; +}; + +/** + * Convert a Python version string. + * + * The supported formats are: + * + * * MAJOR.MINOR.MICRO-RELEASE_LEVEL + * + * (where RELEASE_LEVEL is one of {alpha,beta,candidate,final}) + * + * Everything else, including an empty string, results in `undefined`. + */ +// Eventually we will want to also support the release serial +// (e.g. beta1, candidate3) and maybe even release abbreviations +// (e.g. 3.9.2b1, 3.8.10rc3). +export function parsePythonVersion(raw: string): PythonVersion | undefined { + if (!raw || raw.trim().length === 0) { + return; + } + const versionParts = (raw || '') + .split('.') + .map((item) => item.trim()) + .filter((item) => item.length > 0) + .filter((_, index) => index < 4); + + if (versionParts.length > 0 && versionParts[versionParts.length - 1].indexOf('-') > 0) { + const lastPart = versionParts[versionParts.length - 1]; + versionParts[versionParts.length - 1] = lastPart.split('-')[0].trim(); + versionParts.push(lastPart.split('-')[1].trim()); + } + while (versionParts.length < 4) { + versionParts.push(''); + } + // Exclude PII from `version_info` to ensure we don't send this up via telemetry. + for (let index = 0; index < 3; index += 1) { + versionParts[index] = /^\d+$/.test(versionParts[index]) ? versionParts[index] : '0'; + } + if (['alpha', 'beta', 'candidate', 'final'].indexOf(versionParts[3]) === -1) { + versionParts.pop(); + } + const numberParts = `${versionParts[0]}.${versionParts[1]}.${versionParts[2]}`; + const rawVersion = versionParts.length === 4 ? `${numberParts}-${versionParts[3]}` : numberParts; + return new SemVer(rawVersion); +} + +/** + * Determine if the given versions are the same. + * + * @param version1 - one of the two versions to compare + * @param version2 - one of the two versions to compare + */ +export function areSameVersion(version1?: PythonVersion, version2?: PythonVersion): boolean { + if (!version1 || !version2) { + return false; + } + return version1.raw === version2.raw; +} + +type ExecResult = { + stdout: string; +}; +type ExecFunc = (command: string, args: string[]) => Promise; + +/** + * Get the version string of the given Python executable by running it. + * + * Effectively, we look up `sys.version`. + * + * @param pythonPath - the Python executable to exec + * @param defaultValue - the value to return if anything goes wrong + * @param exec - the function to call to run the Python executable + */ +export async function getPythonVersion(pythonPath: string, defaultValue: string, exec: ExecFunc): Promise { + const [args, parse] = getPythonVersionCommand(); + // It may make sense eventually to use buildPythonExecInfo() here + // instead of using pythonPath and args directly. That would allow + // buildPythonExecInfo() to assume any burden of flexibility. + return exec(pythonPath, args) + .then((result) => parse(result.stdout).splitLines()[0]) + .then((version) => (version.length === 0 ? defaultValue : version)) + .catch(() => defaultValue); +} diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts new file mode 100644 index 000000000000..977a1f7bc207 --- /dev/null +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CONDA_ENV_FILE_SERVICE, + CONDA_ENV_SERVICE, + CURRENT_PATH_SERVICE, + GLOBAL_VIRTUAL_ENV_SERVICE, + ICondaService, + IInterpreterLocatorHelper, + IInterpreterLocatorProgressService, + IInterpreterLocatorService, + IInterpreterWatcher, + IInterpreterWatcherBuilder, + IKnownSearchPathsForInterpreters, + INTERPRETER_LOCATOR_SERVICE, + IVirtualEnvironmentsSearchPathProvider, + KNOWN_PATH_SERVICE, + PIPENV_SERVICE, + WINDOWS_REGISTRY_SERVICE, + WORKSPACE_VIRTUAL_ENV_SERVICE, +} from '../interpreter/contracts'; +import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from '../interpreter/locators/types'; +import { IServiceContainer, IServiceManager } from '../ioc/types'; +import { initializeExternalDependencies } from './common/externalDependencies'; +import { PythonInterpreterLocatorService } from './discovery/locators'; +import { InterpreterLocatorHelper } from './discovery/locators/helpers'; +import { InterpreterLocatorProgressService } from './discovery/locators/progressService'; +import { CondaEnvFileService } from './discovery/locators/services/condaEnvFileService'; +import { CondaEnvService } from './discovery/locators/services/condaEnvService'; +import { CondaService } from './discovery/locators/services/condaService'; +import { CurrentPathService, PythonInPathCommandProvider } from './discovery/locators/services/currentPathService'; +import { + GlobalVirtualEnvironmentsSearchPathProvider, + GlobalVirtualEnvService, +} from './discovery/locators/services/globalVirtualEnvService'; +import { InterpreterHashProvider } from './discovery/locators/services/hashProvider'; +import { InterpeterHashProviderFactory } from './discovery/locators/services/hashProviderFactory'; +import { InterpreterWatcherBuilder } from './discovery/locators/services/interpreterWatcherBuilder'; +import { KnownPathsService, KnownSearchPathsForInterpreters } from './discovery/locators/services/KnownPathsService'; +import { PipEnvService } from './discovery/locators/services/pipEnvService'; +import { PipEnvServiceHelper } from './discovery/locators/services/pipEnvServiceHelper'; +import { WindowsRegistryService } from './discovery/locators/services/windowsRegistryService'; +import { WindowsStoreInterpreter } from './discovery/locators/services/windowsStoreInterpreter'; +import { + WorkspaceVirtualEnvironmentsSearchPathProvider, + WorkspaceVirtualEnvService, +} from './discovery/locators/services/workspaceVirtualEnvService'; +import { WorkspaceVirtualEnvWatcherService } from './discovery/locators/services/workspaceVirtualEnvWatcherService'; +import { EnvironmentInfoService, IEnvironmentInfoService } from './info/environmentInfoService'; + +export function registerForIOC(serviceManager: IServiceManager, serviceContainer: IServiceContainer): void { + serviceManager.addSingleton(IInterpreterLocatorHelper, InterpreterLocatorHelper); + serviceManager.addSingleton( + IInterpreterLocatorService, + PythonInterpreterLocatorService, + INTERPRETER_LOCATOR_SERVICE, + ); + serviceManager.addSingleton( + IInterpreterLocatorProgressService, + InterpreterLocatorProgressService, + ); + serviceManager.addSingleton( + IInterpreterLocatorService, + CondaEnvFileService, + CONDA_ENV_FILE_SERVICE, + ); + serviceManager.addSingleton( + IInterpreterLocatorService, + CondaEnvService, + CONDA_ENV_SERVICE, + ); + serviceManager.addSingleton( + IInterpreterLocatorService, + CurrentPathService, + CURRENT_PATH_SERVICE, + ); + serviceManager.addSingleton( + IInterpreterLocatorService, + GlobalVirtualEnvService, + GLOBAL_VIRTUAL_ENV_SERVICE, + ); + serviceManager.addSingleton( + IInterpreterLocatorService, + WorkspaceVirtualEnvService, + WORKSPACE_VIRTUAL_ENV_SERVICE, + ); + serviceManager.addSingleton(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE); + + serviceManager.addSingleton( + IInterpreterLocatorService, + WindowsRegistryService, + WINDOWS_REGISTRY_SERVICE, + ); + serviceManager.addSingleton( + IInterpreterLocatorService, + KnownPathsService, + KNOWN_PATH_SERVICE, + ); + serviceManager.addSingleton(ICondaService, CondaService); + serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelper); + serviceManager.addSingleton( + IPythonInPathCommandProvider, + PythonInPathCommandProvider, + ); + + serviceManager.add( + IInterpreterWatcher, + WorkspaceVirtualEnvWatcherService, + WORKSPACE_VIRTUAL_ENV_SERVICE, + ); + serviceManager.addSingleton(WindowsStoreInterpreter, WindowsStoreInterpreter); + serviceManager.addSingleton(InterpreterHashProvider, InterpreterHashProvider); + serviceManager.addSingleton( + InterpeterHashProviderFactory, + InterpeterHashProviderFactory, + ); + serviceManager.addSingleton( + IVirtualEnvironmentsSearchPathProvider, + GlobalVirtualEnvironmentsSearchPathProvider, + 'global', + ); + serviceManager.addSingleton( + IVirtualEnvironmentsSearchPathProvider, + WorkspaceVirtualEnvironmentsSearchPathProvider, + 'workspace', + ); + serviceManager.addSingleton( + IKnownSearchPathsForInterpreters, + KnownSearchPathsForInterpreters, + ); + serviceManager.addSingleton(IInterpreterWatcherBuilder, InterpreterWatcherBuilder); + + serviceManager.addSingletonInstance(IEnvironmentInfoService, new EnvironmentInfoService()); + initializeExternalDependencies(serviceContainer); +} diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts new file mode 100644 index 000000000000..94bc5cba1b7a --- /dev/null +++ b/src/client/refactor/proxy.ts @@ -0,0 +1,209 @@ +// tslint:disable:no-any no-empty member-ordering prefer-const prefer-template no-var-self + +import { ChildProcess } from 'child_process'; +import { Disposable, Position, Range, TextDocument, TextEditorOptions, window } from 'vscode'; +import '../common/extensions'; +import { traceError } from '../common/logger'; +import { IS_WINDOWS } from '../common/platform/constants'; +import * as internalScripts from '../common/process/internal/scripts'; +import { IPythonExecutionService } from '../common/process/types'; +import { createDeferred, Deferred } from '../common/utils/async'; +import { getWindowsLineEndingCount } from '../common/utils/text'; + +export class RefactorProxy extends Disposable { + private _process?: ChildProcess; + private _previousOutData: string = ''; + private _previousStdErrData: string = ''; + private _startedSuccessfully: boolean = false; + private _commandResolve?: (value?: any | PromiseLike) => void; + private _commandReject!: (reason?: any) => void; + private initialized!: Deferred; + constructor( + private workspaceRoot: string, + private getPythonExecutionService: () => Promise + ) { + super(() => {}); + } + + public dispose() { + try { + this._process!.kill(); + } catch (ex) {} + this._process = undefined; + } + private getOffsetAt(document: TextDocument, position: Position): number { + if (!IS_WINDOWS) { + return document.offsetAt(position); + } + + // get line count + // Rope always uses LF, instead of CRLF on windows, funny isn't it + // So for each line, reduce one characer (for CR) + // But Not all Windows users use CRLF + const offset = document.offsetAt(position); + const winEols = getWindowsLineEndingCount(document, offset); + + return offset - winEols; + } + public rename( + document: TextDocument, + name: string, + filePath: string, + range: Range, + options?: TextEditorOptions + ): Promise { + if (!options) { + options = window.activeTextEditor!.options; + } + const command = { + lookup: 'rename', + file: filePath, + start: this.getOffsetAt(document, range.start).toString(), + id: '1', + name: name, + indent_size: options.tabSize + }; + + return this.sendCommand(JSON.stringify(command)); + } + public extractVariable( + document: TextDocument, + name: string, + filePath: string, + range: Range, + options?: TextEditorOptions + ): Promise { + if (!options) { + options = window.activeTextEditor!.options; + } + const command = { + lookup: 'extract_variable', + file: filePath, + start: this.getOffsetAt(document, range.start).toString(), + end: this.getOffsetAt(document, range.end).toString(), + id: '1', + name: name, + indent_size: options.tabSize + }; + return this.sendCommand(JSON.stringify(command)); + } + public extractMethod( + document: TextDocument, + name: string, + filePath: string, + range: Range, + options?: TextEditorOptions + ): Promise { + if (!options) { + options = window.activeTextEditor!.options; + } + // Ensure last line is an empty line + if ( + !document.lineAt(document.lineCount - 1).isEmptyOrWhitespace && + range.start.line === document.lineCount - 1 + ) { + return Promise.reject('Missing blank line at the end of document (PEP8).'); + } + const command = { + lookup: 'extract_method', + file: filePath, + start: this.getOffsetAt(document, range.start).toString(), + end: this.getOffsetAt(document, range.end).toString(), + id: '1', + name: name, + indent_size: options.tabSize + }; + return this.sendCommand(JSON.stringify(command)); + } + private sendCommand(command: string): Promise { + return this.initialize().then(() => { + // tslint:disable-next-line:promise-must-complete + return new Promise((resolve, reject) => { + this._commandResolve = resolve; + this._commandReject = reject; + this._process!.stdin.write(command + '\n'); + }); + }); + } + private async initialize(): Promise { + const pythonProc = await this.getPythonExecutionService(); + this.initialized = createDeferred(); + const [args, parse] = internalScripts.refactor(this.workspaceRoot); + const result = pythonProc.execObservable(args, {}); + this._process = result.proc; + result.out.subscribe( + (output) => { + if (output.source === 'stdout') { + if (!this._startedSuccessfully && output.out.startsWith('STARTED')) { + this._startedSuccessfully = true; + return this.initialized.resolve(); + } + this.onData(output.out, parse); + } else { + this.handleStdError(output.out); + } + }, + (error) => this.handleError(error) + ); + + return this.initialized.promise; + } + private handleStdError(data: string) { + // Possible there was an exception in parsing the data returned + // So append the data then parse it + let dataStr = (this._previousStdErrData = this._previousStdErrData + data + ''); + let errorResponse: { message: string; traceback: string; type: string }[]; + try { + errorResponse = dataStr + .split(/\r?\n/g) + .filter((line) => line.length > 0) + .map((resp) => JSON.parse(resp)); + this._previousStdErrData = ''; + } catch (ex) { + traceError(ex); + // Possible we've only received part of the data, hence don't clear previousData + return; + } + if (typeof errorResponse[0].message !== 'string' || errorResponse[0].message.length === 0) { + errorResponse[0].message = errorResponse[0].traceback.splitLines().pop()!; + } + let errorMessage = errorResponse[0].message + '\n' + errorResponse[0].traceback; + + if (this._startedSuccessfully) { + this._commandReject(`Refactor failed. ${errorMessage}`); + } else { + if (typeof errorResponse[0].type === 'string' && errorResponse[0].type === 'ModuleNotFoundError') { + this.initialized.reject('Not installed'); + return; + } + + this.initialized.reject(`Refactor failed. ${errorMessage}`); + } + } + private handleError(error: Error) { + if (this._startedSuccessfully) { + return this._commandReject(error); + } + this.initialized.reject(error); + } + private onData(data: string, parse: (out: string) => object[]) { + if (!this._commandResolve) { + return; + } + + // Possible there was an exception in parsing the data returned + // So append the data then parse it + let dataStr = (this._previousOutData = this._previousOutData + data + ''); + let response: any; + try { + response = parse(dataStr); + this._previousOutData = ''; + } catch (ex) { + // Possible we've only received part of the data, hence don't clear previousData + return; + } + this.dispose(); + this._commandResolve!(response[0]); + this._commandResolve = undefined; + } +} diff --git a/src/client/sortImports.ts b/src/client/sortImports.ts deleted file mode 100644 index a3ab2783bb72..000000000000 --- a/src/client/sortImports.ts +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below -import * as vscode from 'vscode'; -import * as sortProvider from './providers/importSortProvider'; - -// this method is called when your extension is activated -// your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { - var rootDir = context.asAbsolutePath("."); - var disposable = vscode.commands.registerCommand('python.sortImports', () => { - - var activeEditor = vscode.window.activeTextEditor; - if (activeEditor) { - new sortProvider.PythonImportSortProvider().sortImports(rootDir, activeEditor.document).then(changes=> { - if (!Array.isArray(changes) || changes.length === 0) { - return; - } - - activeEditor.edit(builder=> { - builder.replace(changes[0].range, changes[0].newText) - }); - }); - } - }); - - - context.subscriptions.push(disposable); -} diff --git a/src/client/sourceMapSupport.ts b/src/client/sourceMapSupport.ts new file mode 100644 index 000000000000..0303e1e43a93 --- /dev/null +++ b/src/client/sourceMapSupport.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { WorkspaceConfiguration } from 'vscode'; +import './common/extensions'; +import { traceError } from './common/logger'; +import { FileSystem } from './common/platform/fileSystem'; +import { EXTENSION_ROOT_DIR } from './constants'; + +type VSCode = typeof import('vscode'); + +// tslint:disable:no-require-imports +const setting = 'sourceMapsEnabled'; + +export class SourceMapSupport { + private readonly config: WorkspaceConfiguration; + constructor(private readonly vscode: VSCode) { + this.config = this.vscode.workspace.getConfiguration('python.diagnostics', null); + } + public async initialize(): Promise { + if (!this.enabled) { + return; + } + await this.enableSourceMaps(true); + require('source-map-support').install(); + const localize = require('./common/utils/localize') as typeof import('./common/utils/localize'); + const disable = localize.Diagnostics.disableSourceMaps(); + this.vscode.window.showWarningMessage(localize.Diagnostics.warnSourceMaps(), disable).then((selection) => { + if (selection === disable) { + this.disable().ignoreErrors(); + } + }); + } + public get enabled(): boolean { + return this.config.get(setting, false); + } + public async disable(): Promise { + if (this.enabled) { + await this.config.update(setting, false, this.vscode.ConfigurationTarget.Global); + } + await this.enableSourceMaps(false); + } + protected async enableSourceMaps(enable: boolean) { + const extensionSourceFile = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'extension.js'); + const debuggerSourceFile = path.join( + EXTENSION_ROOT_DIR, + 'out', + 'client', + 'debugger', + 'debugAdapter', + 'main.js' + ); + await Promise.all([ + this.enableSourceMap(enable, extensionSourceFile), + this.enableSourceMap(enable, debuggerSourceFile) + ]); + } + protected async enableSourceMap(enable: boolean, sourceFile: string) { + const sourceMapFile = `${sourceFile}.map`; + const disabledSourceMapFile = `${sourceFile}.map.disabled`; + if (enable) { + await this.rename(disabledSourceMapFile, sourceMapFile); + } else { + await this.rename(sourceMapFile, disabledSourceMapFile); + } + } + protected async rename(sourceFile: string, targetFile: string) { + const fs = new FileSystem(); + if (await fs.fileExists(targetFile)) { + return; + } + await fs.move(sourceFile, targetFile); + } +} +export function initialize(vscode: VSCode = require('vscode')) { + if (!vscode.workspace.getConfiguration('python.diagnostics', null).get('sourceMapsEnabled', false)) { + new SourceMapSupport(vscode).disable().ignoreErrors(); + return; + } + new SourceMapSupport(vscode).initialize().catch((_ex) => { + traceError('Failed to initialize source map support in extension'); + }); +} diff --git a/src/client/startupTelemetry.ts b/src/client/startupTelemetry.ts new file mode 100644 index 000000000000..13f8bcfbd5da --- /dev/null +++ b/src/client/startupTelemetry.ts @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IWorkspaceService } from './common/application/types'; +import { isTestExecution } from './common/constants'; +import { DeprecatePythonPath } from './common/experiments/groups'; +import { traceError } from './common/logger'; +import { ITerminalHelper } from './common/terminal/types'; +import { + IConfigurationService, + IExperimentsManager, + IInterpreterPathService, + InspectInterpreterSettingType, + Resource +} from './common/types'; +import { + AutoSelectionRule, + IInterpreterAutoSelectionRule, + IInterpreterAutoSelectionService +} from './interpreter/autoSelection/types'; +import { ICondaService, IInterpreterService } from './interpreter/contracts'; +import { IServiceContainer } from './ioc/types'; +import { PythonEnvironment } from './pythonEnvironments/info'; +import { sendTelemetryEvent } from './telemetry'; +import { EventName } from './telemetry/constants'; +import { EditorLoadTelemetry } from './telemetry/types'; + +interface IStopWatch { + elapsedTime: number; +} + +export async function sendStartupTelemetry( + // tslint:disable-next-line:no-any + activatedPromise: Promise, + durations: Record, + stopWatch: IStopWatch, + serviceContainer: IServiceContainer +) { + if (isTestExecution()) { + return; + } + + try { + await activatedPromise; + durations.totalActivateTime = stopWatch.elapsedTime; + const props = await getActivationTelemetryProps(serviceContainer); + sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props); + } catch (ex) { + traceError('sendStartupTelemetry() failed.', ex); + } +} + +export async function sendErrorTelemetry( + ex: Error, + durations: Record, + serviceContainer?: IServiceContainer +) { + try { + // tslint:disable-next-line:no-any + let props: any = {}; + if (serviceContainer) { + try { + props = await getActivationTelemetryProps(serviceContainer); + } catch (ex) { + traceError('getActivationTelemetryProps() failed.', ex); + } + } + sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props, ex); + } catch (exc2) { + traceError('sendErrorTelemetry() failed.', exc2); + } +} + +function isUsingGlobalInterpreterInWorkspace(currentPythonPath: string, serviceContainer: IServiceContainer): boolean { + const service = serviceContainer.get(IInterpreterAutoSelectionService); + const globalInterpreter = service.getAutoSelectedInterpreter(undefined); + if (!globalInterpreter) { + return false; + } + return currentPythonPath === globalInterpreter.path; +} + +export function hasUserDefinedPythonPath(resource: Resource, serviceContainer: IServiceContainer) { + const abExperiments = serviceContainer.get(IExperimentsManager); + const workspaceService = serviceContainer.get(IWorkspaceService); + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + let settings: InspectInterpreterSettingType; + if (abExperiments.inExperiment(DeprecatePythonPath.experiment)) { + settings = interpreterPathService.inspect(resource); + } else { + settings = workspaceService.getConfiguration('python', resource)!.inspect('pythonPath')!; + } + abExperiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + return (settings.workspaceFolderValue && settings.workspaceFolderValue !== 'python') || + (settings.workspaceValue && settings.workspaceValue !== 'python') || + (settings.globalValue && settings.globalValue !== 'python') + ? true + : false; +} + +function getPreferredWorkspaceInterpreter(resource: Resource, serviceContainer: IServiceContainer) { + const workspaceInterpreterSelector = serviceContainer.get( + IInterpreterAutoSelectionRule, + AutoSelectionRule.workspaceVirtualEnvs + ); + const interpreter = workspaceInterpreterSelector.getPreviouslyAutoSelectedInterpreter(resource); + return interpreter ? interpreter.path : undefined; +} + +async function getActivationTelemetryProps(serviceContainer: IServiceContainer): Promise { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Not all of this data is showing up in the database... + // tslint:disable-next-line:no-suspicious-comment + // TODO: If any one of these parts fails we send no info. We should + // be able to partially populate as much as possible instead + // (through granular try-catch statements). + const terminalHelper = serviceContainer.get(ITerminalHelper); + const terminalShellType = terminalHelper.identifyTerminalShell(); + const condaLocator = serviceContainer.get(ICondaService); + const interpreterService = serviceContainer.get(IInterpreterService); + const workspaceService = serviceContainer.get(IWorkspaceService); + const configurationService = serviceContainer.get(IConfigurationService); + const mainWorkspaceUri = workspaceService.hasWorkspaceFolders + ? workspaceService.workspaceFolders![0].uri + : undefined; + const settings = configurationService.getSettings(mainWorkspaceUri); + const [condaVersion, interpreter, interpreters] = await Promise.all([ + condaLocator + .getCondaVersion() + .then((ver) => (ver ? ver.raw : '')) + .catch(() => ''), + interpreterService.getActiveInterpreter().catch(() => undefined), + interpreterService.getInterpreters(mainWorkspaceUri).catch(() => []) + ]); + const workspaceFolderCount = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.length : 0; + const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; + const interpreterType = interpreter ? interpreter.envType : undefined; + const usingUserDefinedInterpreter = hasUserDefinedPythonPath(mainWorkspaceUri, serviceContainer); + const preferredWorkspaceInterpreter = getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer); + const usingGlobalInterpreter = isUsingGlobalInterpreterInWorkspace(settings.pythonPath, serviceContainer); + const usingAutoSelectedWorkspaceInterpreter = preferredWorkspaceInterpreter + ? settings.pythonPath === getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer) + : false; + const hasPython3 = + interpreters!.filter((item) => (item && item.version ? item.version.major === 3 : false)).length > 0; + + return { + condaVersion, + terminal: terminalShellType, + pythonVersion, + interpreterType, + workspaceFolderCount, + hasPython3, + usingUserDefinedInterpreter, + usingAutoSelectedWorkspaceInterpreter, + usingGlobalInterpreter + }; +} diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts new file mode 100644 index 000000000000..b04516bd48b5 --- /dev/null +++ b/src/client/telemetry/constants.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export enum EventName { + COMPLETION = 'COMPLETION', + COMPLETION_ADD_BRACKETS = 'COMPLETION.ADD_BRACKETS', + DEFINITION = 'DEFINITION', + HOVER_DEFINITION = 'HOVER_DEFINITION', + REFERENCE = 'REFERENCE', + SIGNATURE = 'SIGNATURE', + SYMBOL = 'SYMBOL', + FORMAT_SORT_IMPORTS = 'FORMAT.SORT_IMPORTS', + FORMAT = 'FORMAT.FORMAT', + FORMAT_ON_TYPE = 'FORMAT.FORMAT_ON_TYPE', + EDITOR_LOAD = 'EDITOR.LOAD', + LINTING = 'LINTING', + GO_TO_OBJECT_DEFINITION = 'GO_TO_OBJECT_DEFINITION', + REFACTOR_RENAME = 'REFACTOR_RENAME', + REFACTOR_EXTRACT_VAR = 'REFACTOR_EXTRACT_VAR', + REFACTOR_EXTRACT_FUNCTION = 'REFACTOR_EXTRACT_FUNCTION', + REPL = 'REPL', + SELECT_INTERPRETER = 'SELECT_INTERPRETER', + SELECT_INTERPRETER_ENTER_BUTTON = 'SELECT_INTERPRETER_ENTER_BUTTON', + SELECT_INTERPRETER_ENTER_CHOICE = 'SELECT_INTERPRETER_ENTER_CHOICE', + PYTHON_INTERPRETER = 'PYTHON_INTERPRETER', + PYTHON_INSTALL_PACKAGE = 'PYTHON_INSTALL_PACKAGE', + PYTHON_INTERPRETER_DISCOVERY = 'PYTHON_INTERPRETER_DISCOVERY', + PYTHON_INTERPRETER_AUTO_SELECTION = 'PYTHON_INTERPRETER_AUTO_SELECTION', + PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES = 'PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES', + PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE = 'PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE', + PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL = 'PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL', + PIPENV_INTERPRETER_DISCOVERY = 'PIPENV_INTERPRETER_DISCOVERY', + TERMINAL_SHELL_IDENTIFICATION = 'TERMINAL_SHELL_IDENTIFICATION', + PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT = 'PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT', + PYTHON_NOT_INSTALLED_PROMPT = 'PYTHON_NOT_INSTALLED_PROMPT', + CONDA_INHERIT_ENV_PROMPT = 'CONDA_INHERIT_ENV_PROMPT', + UNSAFE_INTERPRETER_PROMPT = 'UNSAFE_INTERPRETER_PROMPT', + INSIDERS_RELOAD_PROMPT = 'INSIDERS_RELOAD_PROMPT', + INSIDERS_PROMPT = 'INSIDERS_PROMPT', + ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION', + ENVFILE_WORKSPACE = 'ENVFILE_WORKSPACE', + WORKSPACE_SYMBOLS_BUILD = 'WORKSPACE_SYMBOLS.BUILD', + WORKSPACE_SYMBOLS_GO_TO = 'WORKSPACE_SYMBOLS.GO_TO', + EXECUTION_CODE = 'EXECUTION_CODE', + EXECUTION_DJANGO = 'EXECUTION_DJANGO', + DEBUG_ADAPTER_USING_WHEELS_PATH = 'DEBUG_ADAPTER.USING_WHEELS_PATH', + DEBUG_SESSION_ERROR = 'DEBUG_SESSION.ERROR', + DEBUG_SESSION_START = 'DEBUG_SESSION.START', + DEBUG_SESSION_STOP = 'DEBUG_SESSION.STOP', + DEBUG_SESSION_USER_CODE_RUNNING = 'DEBUG_SESSION.USER_CODE_RUNNING', + DEBUGGER = 'DEBUGGER', + DEBUGGER_ATTACH_TO_CHILD_PROCESS = 'DEBUGGER.ATTACH_TO_CHILD_PROCESS', + DEBUGGER_ATTACH_TO_LOCAL_PROCESS = 'DEBUGGER.ATTACH_TO_LOCAL_PROCESS', + DEBUGGER_CONFIGURATION_PROMPTS = 'DEBUGGER.CONFIGURATION.PROMPTS', + DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON = 'DEBUGGER.CONFIGURATION.PROMPTS.IN.LAUNCH.JSON', + UNITTEST_STOP = 'UNITTEST.STOP', + UNITTEST_DISABLE = 'UNITTEST.DISABLE', + UNITTEST_RUN = 'UNITTEST.RUN', + UNITTEST_DISCOVER = 'UNITTEST.DISCOVER', + UNITTEST_DISCOVER_WITH_PYCODE = 'UNITTEST.DISCOVER.WITH.PYTHONCODE', + UNITTEST_CONFIGURE = 'UNITTEST.CONFIGURE', + UNITTEST_CONFIGURING = 'UNITTEST.CONFIGURING', + UNITTEST_VIEW_OUTPUT = 'UNITTEST.VIEW_OUTPUT', + UNITTEST_NAVIGATE = 'UNITTEST.NAVIGATE', + UNITTEST_ENABLED = 'UNITTEST.ENABLED', + UNITTEST_EXPLORER_WORK_SPACE_COUNT = 'UNITTEST.TEST_EXPLORER.WORK_SPACE_COUNT', + + PYTHON_EXPERIMENTS = 'PYTHON_EXPERIMENTS', + PYTHON_EXPERIMENTS_DISABLED = 'PYTHON_EXPERIMENTS_DISABLED', + PYTHON_EXPERIMENTS_OPT_IN_OUT = 'PYTHON_EXPERIMENTS_OPT_IN_OUT', + PYTHON_EXPERIMENTS_DOWNLOAD_SUCCESS_RATE = 'PYTHON_EXPERIMENTS_DOWNLOAD_SUCCESS_RATE', + PLAY_BUTTON_ICON_DISABLED = 'PLAY_BUTTON_ICON.DISABLED', + PYTHON_WEB_APP_RELOAD = 'PYTHON_WEB_APP.RELOAD', + EXTENSION_SURVEY_PROMPT = 'EXTENSION_SURVEY_PROMPT', + ACTIVATION_TIP_PROMPT = 'ACTIVATION_TIP_PROMPT', + ACTIVATION_SURVEY_PROMPT = 'ACTIVATION_SURVEY_PROMPT', + JOIN_MAILING_LIST_PROMPT = 'JOIN_MAILING_LIST_PROMPT', + + PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION = 'PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION', + PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES = 'PYTHON_LANGUAGE_SERVER.LIST_BLOB_PACKAGES', + PYTHON_LANGUAGE_SERVER_EXTRACTED = 'PYTHON_LANGUAGE_SERVER.EXTRACTED', + PYTHON_LANGUAGE_SERVER_DOWNLOADED = 'PYTHON_LANGUAGE_SERVER.DOWNLOADED', + PYTHON_LANGUAGE_SERVER_ERROR = 'PYTHON_LANGUAGE_SERVER.ERROR', + + PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED = 'PYTHON_LANGUAGE_SERVER.PLATFORM_SUPPORTED', + PYTHON_LANGUAGE_SERVER_ENABLED = 'PYTHON_LANGUAGE_SERVER.ENABLED', + PYTHON_LANGUAGE_SERVER_STARTUP = 'PYTHON_LANGUAGE_SERVER.STARTUP', + PYTHON_LANGUAGE_SERVER_READY = 'PYTHON_LANGUAGE_SERVER.READY', + PYTHON_LANGUAGE_SERVER_TELEMETRY = 'PYTHON_LANGUAGE_SERVER.EVENT', + PYTHON_LANGUAGE_SERVER_REQUEST = 'PYTHON_LANGUAGE_SERVER.REQUEST', + + LANGUAGE_SERVER_ENABLED = 'LANGUAGE_SERVER.ENABLED', + LANGUAGE_SERVER_STARTUP = 'LANGUAGE_SERVER.STARTUP', + LANGUAGE_SERVER_READY = 'LANGUAGE_SERVER.READY', + LANGUAGE_SERVER_TELEMETRY = 'LANGUAGE_SERVER.EVENT', + LANGUAGE_SERVER_REQUEST = 'LANGUAGE_SERVER.REQUEST', + LANGUAGE_SERVER_TRY_PYLANCE = 'LANGUAGE_SERVER.TRY_PYLANCE', + + TERMINAL_CREATE = 'TERMINAL.CREATE', + ACTIVATE_ENV_IN_CURRENT_TERMINAL = 'ACTIVATE_ENV_IN_CURRENT_TERMINAL', + ACTIVATE_ENV_TO_GET_ENV_VARS_FAILED = 'ACTIVATE_ENV_TO_GET_ENV_VARS_FAILED', + DIAGNOSTICS_ACTION = 'DIAGNOSTICS.ACTION', + DIAGNOSTICS_MESSAGE = 'DIAGNOSTICS.MESSAGE', + PLATFORM_INFO = 'PLATFORM.INFO', + + SELECT_LINTER = 'LINTING.SELECT', + + LINTER_NOT_INSTALLED_PROMPT = 'LINTER_NOT_INSTALLED_PROMPT', + CONFIGURE_AVAILABLE_LINTER_PROMPT = 'CONFIGURE_AVAILABLE_LINTER_PROMPT', + HASHED_PACKAGE_NAME = 'HASHED_PACKAGE_NAME', + HASHED_PACKAGE_PERF = 'HASHED_PACKAGE_PERF', + + JEDI_MEMORY = 'JEDI_MEMORY' +} + +export enum PlatformErrors { + FailedToParseVersion = 'FailedToParseVersion', + FailedToDetermineOS = 'FailedToDetermineOS' +} diff --git a/src/client/telemetry/envFileTelemetry.ts b/src/client/telemetry/envFileTelemetry.ts new file mode 100644 index 000000000000..dfb30d851297 --- /dev/null +++ b/src/client/telemetry/envFileTelemetry.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IWorkspaceService } from '../common/application/types'; +import { IFileSystem } from '../common/platform/types'; +import { Resource } from '../common/types'; +import { SystemVariables } from '../common/variables/systemVariables'; + +import { sendTelemetryEvent } from '.'; +import { EventName } from './constants'; + +let _defaultEnvFileSetting: string | undefined; +let envFileTelemetrySent = false; + +export function sendSettingTelemetry(workspaceService: IWorkspaceService, envFileSetting?: string) { + if (shouldSendTelemetry() && envFileSetting !== defaultEnvFileSetting(workspaceService)) { + sendTelemetry(true); + } +} + +export function sendFileCreationTelemetry() { + if (shouldSendTelemetry()) { + sendTelemetry(); + } +} + +export async function sendActivationTelemetry( + fileSystem: IFileSystem, + workspaceService: IWorkspaceService, + resource: Resource +) { + if (shouldSendTelemetry()) { + const systemVariables = new SystemVariables(resource, undefined, workspaceService); + const envFilePath = systemVariables.resolveAny(defaultEnvFileSetting(workspaceService))!; + const envFileExists = await fileSystem.fileExists(envFilePath); + + if (envFileExists) { + sendTelemetry(); + } + } +} + +function sendTelemetry(hasCustomEnvPath: boolean = false) { + sendTelemetryEvent(EventName.ENVFILE_WORKSPACE, undefined, { hasCustomEnvPath }); + + envFileTelemetrySent = true; +} + +function shouldSendTelemetry(): boolean { + return !envFileTelemetrySent; +} + +function defaultEnvFileSetting(workspaceService: IWorkspaceService) { + if (!_defaultEnvFileSetting) { + const section = workspaceService.getConfiguration('python'); + _defaultEnvFileSetting = section.inspect('envFile')?.defaultValue || ''; + } + + return _defaultEnvFileSetting; +} + +// Set state for tests. +export namespace EnvFileTelemetryTests { + export function setState({ telemetrySent, defaultSetting }: { telemetrySent?: boolean; defaultSetting?: string }) { + if (telemetrySent !== undefined) { + envFileTelemetrySent = telemetrySent; + } + if (defaultEnvFileSetting !== undefined) { + _defaultEnvFileSetting = defaultSetting; + } + } + + export function resetState() { + _defaultEnvFileSetting = undefined; + envFileTelemetrySent = false; + } +} diff --git a/src/client/telemetry/extensionInstallTelemetry.ts b/src/client/telemetry/extensionInstallTelemetry.ts new file mode 100644 index 000000000000..87e6ec50e3ea --- /dev/null +++ b/src/client/telemetry/extensionInstallTelemetry.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { setSharedProperty } from '.'; +import { IFileSystem } from '../common/platform/types'; +import { EXTENSION_ROOT_DIR } from '../constants'; + +/** + * Sets shared telemetry property about where the extension was installed from + * currently we only detect installations from the Python coding pack installer. + * Those installations get the 'pythonCodingPack'. Otherwise assume the default + * case as 'MarketPlace'. + * + */ +export async function setExtensionInstallTelemetryProperties(fs: IFileSystem) { + // Look for PythonCodingPack file under `%USERPROFILE%/.vscode/extensions` + // folder. If that file exists treat this extension as installed from coding + // pack. + // + // Use parent of EXTENSION_ROOT_DIR to access %USERPROFILE%/.vscode/extensions + // this is because the installer will add PythonCodingPack to %USERPROFILE%/.vscode/extensions + // or %USERPROFILE%/.vscode-insiders/extensions depending on what was installed + // previously by the user. If we always join (, .vscode, extensions), we will + // end up looking at the wrong place, with respect to the extension that was launched. + const fileToCheck = path.join(path.dirname(EXTENSION_ROOT_DIR), 'PythonCodingPack'); + if (await fs.fileExists(fileToCheck)) { + setSharedProperty('installSource', 'pythonCodingPack'); + } else { + // We did not file the `PythonCodingPack` file, assume market place install. + setSharedProperty('installSource', 'marketPlace'); + } +} diff --git a/src/client/telemetry/importTracker.ts b/src/client/telemetry/importTracker.ts new file mode 100644 index 000000000000..44fa02794b60 --- /dev/null +++ b/src/client/telemetry/importTracker.ts @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { TextDocument } from 'vscode'; +import { captureTelemetry, sendTelemetryEvent } from '.'; +import { splitMultilineString } from '../../datascience-ui/common'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { IDocumentManager } from '../common/application/types'; +import { isTestExecution } from '../common/constants'; +import '../common/extensions'; +import { noop } from '../common/utils/misc'; +import { ICell, INotebookEditor, INotebookEditorProvider, INotebookExecutionLogger } from '../datascience/types'; +import { EventName } from './constants'; + +/* +Python has a fairly rich import statement. Originally the matching regexp was kept simple for +performance worries, but it led to false-positives due to matching things like docstrings with +phrases along the lines of "from the thing" or "import the thing". To minimize false-positives the +regexp does its best to validate the structure of the import line _within reason_. This leads to +us supporting the following (where `pkg` represents what we are actually capturing for telemetry): + +- `from pkg import _` +- `from pkg import _, _` +- `from pkg import _ as _` +- `import pkg` +- `import pkg, pkg` +- `import pkg as _` + +Things we are ignoring the following for simplicity/performance: + +- `from pkg import (...)` (this includes single-line and multi-line imports with parentheses) +- `import pkg # ... and anything else with a trailing comment.` +- Non-standard whitespace separators within the import statement (i.e. more than a single space, tabs) + +*/ +const ImportRegEx = /^\s*(from (?\w+)(?:\.\w+)* import \w+(?:, \w+)*(?: as \w+)?|import (?\w+(?:, \w+)*)(?: as \w+)?)$/; +const MAX_DOCUMENT_LINES = 1000; + +// Capture isTestExecution on module load so that a test can turn it off and still +// have this value set. +const testExecution = isTestExecution(); + +@injectable() +export class ImportTracker implements IExtensionSingleActivationService, INotebookExecutionLogger { + private pendingChecks = new Map(); + private sentMatches: Set = new Set(); + // tslint:disable-next-line:no-require-imports + private hashFn = require('hash.js').sha256; + + constructor( + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(INotebookEditorProvider) private notebookEditorProvider: INotebookEditorProvider + ) { + this.documentManager.onDidOpenTextDocument((t) => this.onOpenedOrSavedDocument(t)); + this.documentManager.onDidSaveTextDocument((t) => this.onOpenedOrSavedDocument(t)); + this.notebookEditorProvider.onDidOpenNotebookEditor((t) => this.onOpenedOrClosedNotebook(t)); + this.notebookEditorProvider.onDidCloseNotebookEditor((t) => this.onOpenedOrClosedNotebook(t)); + } + + public dispose() { + this.pendingChecks.clear(); + } + + public onKernelRestarted() { + // Do nothing on restarted + } + public async preExecute(_cell: ICell, _silent: boolean): Promise { + // Do nothing on pre execute + } + public async postExecute(cell: ICell, silent: boolean): Promise { + // Check for imports in the cell itself. + if (!silent && cell.data.cell_type === 'code') { + this.scheduleCheck(this.createCellKey(cell), this.checkCell.bind(this, cell)); + } + } + + public async activate(): Promise { + // Act like all of our open documents just opened; our timeout will make sure this is delayed. + this.documentManager.textDocuments.forEach((d) => this.onOpenedOrSavedDocument(d)); + this.notebookEditorProvider.editors.forEach((e) => this.onOpenedOrClosedNotebook(e)); + } + + private getDocumentLines(document: TextDocument): (string | undefined)[] { + const array = Array(Math.min(document.lineCount, MAX_DOCUMENT_LINES)).fill(''); + return array + .map((_a: string, i: number) => { + const line = document.lineAt(i); + if (line && !line.isEmptyOrWhitespace) { + return line.text; + } + return undefined; + }) + .filter((f: string | undefined) => f); + } + + private getNotebookLines(e: INotebookEditor): (string | undefined)[] { + let result: (string | undefined)[] = []; + if (e.model) { + e.model.cells + .filter((c) => c.data.cell_type === 'code') + .forEach((c) => { + const cellArray = this.getCellLines(c); + if (result.length < MAX_DOCUMENT_LINES) { + result = [...result, ...cellArray]; + } + }); + } + return result; + } + + private getCellLines(cell: ICell): (string | undefined)[] { + // Split into multiple lines removing line feeds on the end. + return splitMultilineString(cell.data.source).map((s) => s.replace(/\n/g, '')); + } + + private onOpenedOrSavedDocument(document: TextDocument) { + // Make sure this is a Python file. + if (path.extname(document.fileName) === '.py') { + this.scheduleDocument(document); + } + } + + private onOpenedOrClosedNotebook(e: INotebookEditor) { + if (e.file) { + this.scheduleCheck(e.file.fsPath, this.checkNotebook.bind(this, e)); + } + } + + private scheduleDocument(document: TextDocument) { + this.scheduleCheck(document.fileName, this.checkDocument.bind(this, document)); + } + + private scheduleCheck(file: string, check: () => void) { + // If already scheduled, cancel. + const currentTimeout = this.pendingChecks.get(file); + if (currentTimeout) { + // tslint:disable-next-line: no-any + clearTimeout(currentTimeout as any); + this.pendingChecks.delete(file); + } + + // Now schedule a new one. + if (testExecution) { + // During a test, check right away. It needs to be synchronous. + check(); + } else { + // Wait five seconds to make sure we don't already have this document pending. + this.pendingChecks.set(file, setTimeout(check, 5000)); + } + } + + private createCellKey(cell: ICell): string { + return `${cell.file}${cell.id}`; + } + + @captureTelemetry(EventName.HASHED_PACKAGE_PERF) + private checkCell(cell: ICell) { + this.pendingChecks.delete(this.createCellKey(cell)); + const lines = this.getCellLines(cell); + this.lookForImports(lines); + } + + @captureTelemetry(EventName.HASHED_PACKAGE_PERF) + private checkNotebook(e: INotebookEditor) { + this.pendingChecks.delete(e.file.fsPath); + const lines = this.getNotebookLines(e); + this.lookForImports(lines); + } + + @captureTelemetry(EventName.HASHED_PACKAGE_PERF) + private checkDocument(document: TextDocument) { + this.pendingChecks.delete(document.fileName); + const lines = this.getDocumentLines(document); + this.lookForImports(lines); + } + + private sendTelemetry(packageName: string) { + // No need to send duplicate telemetry or waste CPU cycles on an unneeded hash. + if (this.sentMatches.has(packageName)) { + return; + } + this.sentMatches.add(packageName); + // Hash the package name so that we will never accidentally see a + // user's private package name. + const hash = this.hashFn().update(packageName).digest('hex'); + sendTelemetryEvent(EventName.HASHED_PACKAGE_NAME, undefined, { hashedName: hash }); + } + + private lookForImports(lines: (string | undefined)[]) { + try { + for (const s of lines) { + const match = s ? ImportRegEx.exec(s) : null; + if (match !== null && match.groups !== undefined) { + if (match.groups.fromImport !== undefined) { + // `from pkg ...` + this.sendTelemetry(match.groups.fromImport); + } else if (match.groups.importImport !== undefined) { + // `import pkg1, pkg2, ...` + const packageNames = match.groups.importImport + .split(',') + .map((rawPackageName) => rawPackageName.trim()); + // Can't pass in `this.sendTelemetry` directly as that rebinds `this`. + packageNames.forEach((p) => this.sendTelemetry(p)); + } + } + } + } catch { + // Don't care about failures since this is just telemetry. + noop(); + } + } +} diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts new file mode 100644 index 000000000000..434b929f59c6 --- /dev/null +++ b/src/client/telemetry/index.ts @@ -0,0 +1,2281 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import type { JSONObject } from '@phosphor/coreutils'; +import * as stackTrace from 'stack-trace'; +// tslint:disable-next-line: import-name +import TelemetryReporter from 'vscode-extension-telemetry/lib/telemetryReporter'; + +import { LanguageServerType } from '../activation/types'; +import { DiagnosticCodes } from '../application/diagnostics/constants'; +import { IWorkspaceService } from '../common/application/types'; +import { AppinsightsKey, isTestExecution, isUnitTestExecution, PVSC_EXTENSION_ID } from '../common/constants'; +import { traceError, traceInfo } from '../common/logger'; +import { TerminalShellType } from '../common/terminal/types'; +import { Architecture } from '../common/utils/platform'; +import { StopWatch } from '../common/utils/stopWatch'; +import { + JupyterCommands, + NativeKeyboardCommandTelemetry, + NativeMouseCommandTelemetry, + Telemetry, + VSCodeNativeTelemetry +} from '../datascience/constants'; +import { ExportFormat } from '../datascience/export/types'; +import { DebugConfigurationType } from '../debugger/extension/types'; +import { ConsoleType, TriggerType } from '../debugger/types'; +import { AutoSelectionRule } from '../interpreter/autoSelection/types'; +import { LinterId } from '../linters/types'; +import { EnvironmentType } from '../pythonEnvironments/info'; +import { TestProvider } from '../testing/common/types'; +import { EventName, PlatformErrors } from './constants'; +import { LinterTrigger, TestTool } from './types'; + +// tslint:disable: no-any + +/** + * Checks whether telemetry is supported. + * Its possible this function gets called within Debug Adapter, vscode isn't available in there. + * Within DA, there's a completely different way to send telemetry. + * @returns {boolean} + */ +function isTelemetrySupported(): boolean { + try { + // tslint:disable-next-line:no-require-imports + const vsc = require('vscode'); + // tslint:disable-next-line:no-require-imports + const reporter = require('vscode-extension-telemetry'); + return vsc !== undefined && reporter !== undefined; + } catch { + return false; + } +} + +/** + * Checks if the telemetry is disabled in user settings + * @returns {boolean} + */ +export function isTelemetryDisabled(workspaceService: IWorkspaceService): boolean { + const settings = workspaceService.getConfiguration('telemetry').inspect('enableTelemetry')!; + return settings.globalValue === false ? true : false; +} + +const sharedProperties: Record = {}; +/** + * Set shared properties for all telemetry events. + */ +export function setSharedProperty

(name: E, value?: P[E]): void { + const propertyName = name as string; + // Ignore such shared telemetry during unit tests. + if (isUnitTestExecution() && propertyName.startsWith('ds_')) { + return; + } + if (value === undefined) { + delete sharedProperties[propertyName]; + } else { + sharedProperties[propertyName] = value; + } +} + +/** + * Reset shared properties for testing purposes. + */ +export function _resetSharedProperties(): void { + for (const key of Object.keys(sharedProperties)) { + delete sharedProperties[key]; + } +} + +let telemetryReporter: TelemetryReporter | undefined; +function getTelemetryReporter() { + if (!isTestExecution() && telemetryReporter) { + return telemetryReporter; + } + const extensionId = PVSC_EXTENSION_ID; + // tslint:disable-next-line:no-require-imports + const extensions = (require('vscode') as typeof import('vscode')).extensions; + const extension = extensions.getExtension(extensionId)!; + const extensionVersion = extension.packageJSON.version; + + // tslint:disable-next-line:no-require-imports + const reporter = require('vscode-extension-telemetry').default as typeof TelemetryReporter; + return (telemetryReporter = new reporter(extensionId, extensionVersion, AppinsightsKey, true)); +} + +export function clearTelemetryReporter() { + telemetryReporter = undefined; +} + +export function sendTelemetryEvent

( + eventName: E, + durationMs?: Record | number, + properties?: P[E], + ex?: Error +) { + if (isTestExecution() || !isTelemetrySupported()) { + return; + } + const reporter = getTelemetryReporter(); + const measures = typeof durationMs === 'number' ? { duration: durationMs } : durationMs ? durationMs : undefined; + let customProperties: Record = {}; + let eventNameSent = eventName as string; + + if (ex) { + // When sending telemetry events for exceptions no need to send custom properties. + // Else we have to review all properties every time as part of GDPR. + // Assume we have 10 events all with their own properties. + // As we have errors for each event, those properties are treated as new data items. + // Hence they need to be classified as part of the GDPR process, and thats unnecessary and onerous. + eventNameSent = 'ERROR'; + customProperties = { originalEventName: eventName as string, stackTrace: serializeStackTrace(ex) }; + reporter.sendTelemetryErrorEvent(eventNameSent, customProperties, measures, []); + } else { + if (properties) { + const data = properties as any; + Object.getOwnPropertyNames(data).forEach((prop) => { + if (data[prop] === undefined || data[prop] === null) { + return; + } + try { + // If there are any errors in serializing one property, ignore that and move on. + // Else nothing will be sent. + customProperties[prop] = + typeof data[prop] === 'string' + ? data[prop] + : typeof data[prop] === 'object' + ? 'object' + : data[prop].toString(); + } catch (ex) { + traceError(`Failed to serialize ${prop} for ${eventName}`, ex); + } + }); + } + + // Add shared properties to telemetry props (we may overwrite existing ones). + Object.assign(customProperties, sharedProperties); + + // Remove shared DS properties from core extension telemetry. + Object.keys(sharedProperties).forEach((shareProperty) => { + if ( + customProperties[shareProperty] && + shareProperty.startsWith('ds_') && + !(eventNameSent.startsWith('DS_') || eventNameSent.startsWith('DATASCIENCE')) + ) { + delete customProperties[shareProperty]; + } + }); + + reporter.sendTelemetryEvent(eventNameSent, customProperties, measures); + } + + if (process.env && process.env.VSC_PYTHON_LOG_TELEMETRY) { + traceInfo( + `Telemetry Event : ${eventNameSent} Measures: ${JSON.stringify(measures)} Props: ${JSON.stringify( + customProperties + )} ` + ); + } +} + +// Type-parameterized form of MethodDecorator in lib.es5.d.ts. +type TypedMethodDescriptor = ( + target: Object, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor +) => TypedPropertyDescriptor | void; + +/** + * Decorates a method, sending a telemetry event with the given properties. + * @param eventName The event name to send. + * @param properties Properties to send with the event; must be valid for the event. + * @param captureDuration True if the method's execution duration should be captured. + * @param failureEventName If the decorated method returns a Promise and fails, send this event instead of eventName. + * @param lazyProperties A static function on the decorated class which returns extra properties to add to the event. + * This can be used to provide properties which are only known at runtime (after the decorator has executed). + */ +// tslint:disable-next-line:no-any function-name +export function captureTelemetry( + eventName: E, + properties?: P[E], + captureDuration: boolean = true, + failureEventName?: E, + lazyProperties?: (obj: This) => P[E] +): TypedMethodDescriptor<(this: This, ...args: any[]) => any> { + // tslint:disable-next-line:no-function-expression no-any + return function ( + _target: Object, + _propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor<(this: This, ...args: any[]) => any> + ) { + const originalMethod = descriptor.value!; + // tslint:disable-next-line:no-function-expression no-any + descriptor.value = function (this: This, ...args: any[]) { + // Legacy case; fast path that sends event before method executes. + // Does not set "failed" if the result is a Promise and throws an exception. + if (!captureDuration && !lazyProperties) { + sendTelemetryEvent(eventName, undefined, properties); + // tslint:disable-next-line:no-invalid-this + return originalMethod.apply(this, args); + } + + const props = () => { + if (lazyProperties) { + return { ...properties, ...lazyProperties(this) }; + } + return properties; + }; + + const stopWatch = captureDuration ? new StopWatch() : undefined; + + // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any + const result = originalMethod.apply(this, args); + + // If method being wrapped returns a promise then wait for it. + // tslint:disable-next-line:no-unsafe-any + if (result && typeof result.then === 'function' && typeof result.catch === 'function') { + // tslint:disable-next-line:prefer-type-cast + (result as Promise) + .then((data) => { + sendTelemetryEvent(eventName, stopWatch?.elapsedTime, props()); + return data; + }) + // tslint:disable-next-line:promise-function-async + .catch((ex) => { + // tslint:disable-next-line:no-any + const failedProps: P[E] = props() || ({} as any); + (failedProps as any).failed = true; + sendTelemetryEvent( + failureEventName ? failureEventName : eventName, + stopWatch?.elapsedTime, + failedProps, + ex + ); + }); + } else { + sendTelemetryEvent(eventName, stopWatch?.elapsedTime, props()); + } + + return result; + }; + + return descriptor; + }; +} + +// function sendTelemetryWhenDone(eventName: K, properties?: T[K]); +export function sendTelemetryWhenDone

( + eventName: E, + promise: Promise | Thenable, + stopWatch?: StopWatch, + properties?: P[E] +) { + stopWatch = stopWatch ? stopWatch : new StopWatch(); + if (typeof promise.then === 'function') { + // tslint:disable-next-line:prefer-type-cast no-any + (promise as Promise).then( + (data) => { + // tslint:disable-next-line:no-non-null-assertion + sendTelemetryEvent(eventName, stopWatch!.elapsedTime, properties); + return data; + // tslint:disable-next-line:promise-function-async + }, + (ex) => { + // tslint:disable-next-line:no-non-null-assertion + sendTelemetryEvent(eventName, stopWatch!.elapsedTime, properties, ex); + return Promise.reject(ex); + } + ); + } else { + throw new Error('Method is neither a Promise nor a Theneable'); + } +} + +function serializeStackTrace(ex: Error): string { + // We aren't showing the error message (ex.message) since it might contain PII. + let trace = ''; + for (const frame of stackTrace.parse(ex)) { + const filename = frame.getFileName(); + if (filename) { + const lineno = frame.getLineNumber(); + const colno = frame.getColumnNumber(); + trace += `\n\tat ${getCallsite(frame)} ${filename}:${lineno}:${colno}`; + } else { + trace += '\n\tat '; + } + } + // Ensure we always use `/` as path separators. + // This way stack traces (with relative paths) coming from different OS will always look the same. + return trace.trim().replace(/\\/g, '/'); +} + +function getCallsite(frame: stackTrace.StackFrame) { + const parts: string[] = []; + if (typeof frame.getTypeName() === 'string' && frame.getTypeName().length > 0) { + parts.push(frame.getTypeName()); + } + if (typeof frame.getMethodName() === 'string' && frame.getMethodName().length > 0) { + parts.push(frame.getMethodName()); + } + if (typeof frame.getFunctionName() === 'string' && frame.getFunctionName().length > 0) { + if (parts.length !== 2 || parts.join('.') !== frame.getFunctionName()) { + parts.push(frame.getFunctionName()); + } + } + return parts.join('.'); +} + +/** + * Map all shared properties to their data types. + */ +export interface ISharedPropertyMapping { + /** + * For every DS telemetry we would like to know the type of Notebook Editor used when doing something. + */ + ['ds_notebookeditor']: undefined | 'old' | 'custom' | 'native'; + + /** + * For every telemetry event from the extension we want to make sure we can associate it with install + * source. We took this approach to work around very limiting query performance issues. + */ + ['installSource']: undefined | 'marketPlace' | 'pythonCodingPack'; +} + +// Map all events to their properties +export interface IEventNamePropertyMapping { + /** + * Telemetry event sent when providing completion items for the given position and document. + */ + [EventName.COMPLETION]: never | undefined; + /** + * Telemetry event sent with details 'python.autoComplete.addBrackets' setting + */ + [EventName.COMPLETION_ADD_BRACKETS]: { + /** + * Carries boolean `true` if 'python.autoComplete.addBrackets' is set to true, `false` otherwise + */ + enabled: boolean; + }; + /** + * Telemetry event captured when debug adapter executable is created + */ + [EventName.DEBUG_ADAPTER_USING_WHEELS_PATH]: { + /** + * Carries boolean + * - `true` if path used for the adapter is the debugger with wheels. + * - `false` if path used for the adapter is the source only version of the debugger. + */ + usingWheels: boolean; + }; + /** + * Telemetry captured before starting debug session. + */ + [EventName.DEBUG_SESSION_START]: { + /** + * Trigger for starting the debugger. + * - `launch`: Launch/start new code and debug it. + * - `attach`: Attach to an exiting python process (remote debugging). + * - `test`: Debugging python tests. + * + * @type {TriggerType} + */ + trigger: TriggerType; + /** + * Type of console used. + * -`internalConsole`: Use VS Code debug console (no shells/terminals). + * - `integratedTerminal`: Use VS Code terminal. + * - `externalTerminal`: Use an External terminal. + * + * @type {ConsoleType} + */ + console?: ConsoleType; + }; + /** + * Telemetry captured when debug session runs into an error. + */ + [EventName.DEBUG_SESSION_ERROR]: { + /** + * Trigger for starting the debugger. + * - `launch`: Launch/start new code and debug it. + * - `attach`: Attach to an exiting python process (remote debugging). + * - `test`: Debugging python tests. + * + * @type {TriggerType} + */ + trigger: TriggerType; + /** + * Type of console used. + * -`internalConsole`: Use VS Code debug console (no shells/terminals). + * - `integratedTerminal`: Use VS Code terminal. + * - `externalTerminal`: Use an External terminal. + * + * @type {ConsoleType} + */ + console?: ConsoleType; + }; + /** + * Telemetry captured after stopping debug session. + */ + [EventName.DEBUG_SESSION_STOP]: { + /** + * Trigger for starting the debugger. + * - `launch`: Launch/start new code and debug it. + * - `attach`: Attach to an exiting python process (remote debugging). + * - `test`: Debugging python tests. + * + * @type {TriggerType} + */ + trigger: TriggerType; + /** + * Type of console used. + * -`internalConsole`: Use VS Code debug console (no shells/terminals). + * - `integratedTerminal`: Use VS Code terminal. + * - `externalTerminal`: Use an External terminal. + * + * @type {ConsoleType} + */ + console?: ConsoleType; + }; + /** + * Telemetry captured when user code starts running after loading the debugger. + */ + [EventName.DEBUG_SESSION_USER_CODE_RUNNING]: { + /** + * Trigger for starting the debugger. + * - `launch`: Launch/start new code and debug it. + * - `attach`: Attach to an exiting python process (remote debugging). + * - `test`: Debugging python tests. + * + * @type {TriggerType} + */ + trigger: TriggerType; + /** + * Type of console used. + * -`internalConsole`: Use VS Code debug console (no shells/terminals). + * - `integratedTerminal`: Use VS Code terminal. + * - `externalTerminal`: Use an External terminal. + * + * @type {ConsoleType} + */ + console?: ConsoleType; + }; + /** + * Telemetry captured when starting the debugger. + */ + [EventName.DEBUGGER]: { + /** + * Trigger for starting the debugger. + * - `launch`: Launch/start new code and debug it. + * - `attach`: Attach to an exiting python process (remote debugging). + * - `test`: Debugging python tests. + * + * @type {TriggerType} + */ + trigger: TriggerType; + /** + * Type of console used. + * -`internalConsole`: Use VS Code debug console (no shells/terminals). + * - `integratedTerminal`: Use VS Code terminal. + * - `externalTerminal`: Use an External terminal. + * + * @type {ConsoleType} + */ + console?: ConsoleType; + /** + * Whether user has defined environment variables. + * Could have been defined in launch.json or the env file (defined in `settings.json`). + * Default `env file` is `.env` in the workspace folder. + * + * @type {boolean} + */ + hasEnvVars: boolean; + /** + * Whether there are any CLI arguments that need to be passed into the program being debugged. + * + * @type {boolean} + */ + hasArgs: boolean; + /** + * Whether the user is debugging `django`. + * + * @type {boolean} + */ + django: boolean; + /** + * Whether the user is debugging `flask`. + * + * @type {boolean} + */ + flask: boolean; + /** + * Whether the user is debugging `jinja` templates. + * + * @type {boolean} + */ + jinja: boolean; + /** + * Whether user is attaching to a local python program (attach scenario). + * + * @type {boolean} + */ + isLocalhost: boolean; + /** + * Whether debugging a module. + * + * @type {boolean} + */ + isModule: boolean; + /** + * Whether debugging with `sudo`. + * + * @type {boolean} + */ + isSudo: boolean; + /** + * Whether required to stop upon entry. + * + * @type {boolean} + */ + stopOnEntry: boolean; + /** + * Whether required to display return types in debugger. + * + * @type {boolean} + */ + showReturnValue: boolean; + /** + * Whether debugging `pyramid`. + * + * @type {boolean} + */ + pyramid: boolean; + /** + * Whether debugging a subprocess. + * + * @type {boolean} + */ + subProcess: boolean; + /** + * Whether debugging `watson`. + * + * @type {boolean} + */ + watson: boolean; + /** + * Whether degbugging `pyspark`. + * + * @type {boolean} + */ + pyspark: boolean; + /** + * Whether using `gevent` when debugging. + * + * @type {boolean} + */ + gevent: boolean; + /** + * Whether debugging `scrapy`. + * + * @type {boolean} + */ + scrapy: boolean; + }; + /** + * Telemetry event sent when attaching to child process + */ + [EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS]: never | undefined; + /** + * Telemetry event sent when attaching to a local process. + */ + [EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS]: never | undefined; + /** + * Telemetry sent after building configuration for debugger + */ + [EventName.DEBUGGER_CONFIGURATION_PROMPTS]: { + /** + * The type of debug configuration to build configuration for + * + * @type {DebugConfigurationType} + */ + configurationType: DebugConfigurationType; + /** + * Carries `true` if we are able to auto-detect manage.py path for Django, `false` otherwise + * + * @type {boolean} + */ + autoDetectedDjangoManagePyPath?: boolean; + /** + * Carries `true` if we are able to auto-detect .ini file path for Pyramid, `false` otherwise + * + * @type {boolean} + */ + autoDetectedPyramidIniPath?: boolean; + /** + * Carries `true` if we are able to auto-detect app.py path for Flask, `false` otherwise + * + * @type {boolean} + */ + autoDetectedFlaskAppPyPath?: boolean; + /** + * Carries `true` if user manually entered the required path for the app + * (path to `manage.py` for Django, path to `.ini` for Pyramid, path to `app.py` for Flask), `false` otherwise + * + * @type {boolean} + */ + manuallyEnteredAValue?: boolean; + }; + /** + * Telemetry event sent when providing completion provider in launch.json. It is sent just *after* inserting the completion. + */ + [EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON]: never | undefined; + /** + * Telemetry is sent when providing definitions for python code, particularly when [go to definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition) + * and peek definition features are used. + */ + [EventName.DEFINITION]: never | undefined; + /** + * Telemetry event sent with details of actions when invoking a diagnostic command + */ + [EventName.DIAGNOSTICS_ACTION]: { + /** + * Diagnostics command executed. + * @type {string} + */ + commandName?: string; + /** + * Diagnostisc code ignored (message will not be seen again). + * @type {string} + */ + ignoreCode?: string; + /** + * Url of web page launched in browser. + * @type {string} + */ + url?: string; + /** + * Custom actions performed. + * @type {'switchToCommandPrompt'} + */ + action?: 'switchToCommandPrompt'; + }; + /** + * Telemetry event sent when we are checking if we can handle the diagnostic code + */ + [EventName.DIAGNOSTICS_MESSAGE]: { + /** + * Code of diagnostics message detected and displayed. + * @type {string} + */ + code: DiagnosticCodes; + }; + /** + * Telemetry event sent with details just after editor loads + */ + [EventName.EDITOR_LOAD]: { + /** + * The conda version if selected + */ + condaVersion: string | undefined; + /** + * The python interpreter version if selected + */ + pythonVersion: string | undefined; + /** + * The type of interpreter (conda, virtualenv, pipenv etc.) + */ + interpreterType: EnvironmentType | undefined; + /** + * The type of terminal shell created: powershell, cmd, zsh, bash etc. + * + * @type {TerminalShellType} + */ + terminal: TerminalShellType; + /** + * Number of workspace folders opened + */ + workspaceFolderCount: number; + /** + * If interpreters found for the main workspace contains a python3 interpreter + */ + hasPython3: boolean; + /** + * If user has defined an interpreter in settings.json + */ + usingUserDefinedInterpreter: boolean; + /** + * If interpreter is auto selected for the workspace + */ + usingAutoSelectedWorkspaceInterpreter: boolean; + /** + * If global interpreter is being used + */ + usingGlobalInterpreter: boolean; + }; + /** + * Telemetry event sent when substituting Environment variables to calculate value of variables + */ + [EventName.ENVFILE_VARIABLE_SUBSTITUTION]: never | undefined; + /** + * Telemetry event sent when an environment file is detected in the workspace. + */ + [EventName.ENVFILE_WORKSPACE]: { + /** + * If there's a custom path specified in the python.envFile workspace settings. + */ + hasCustomEnvPath: boolean; + }; + /** + * Telemetry Event sent when user sends code to be executed in the terminal. + * + */ + [EventName.EXECUTION_CODE]: { + /** + * Whether the user executed a file in the terminal or just the selected text. + * + * @type {('file' | 'selection')} + */ + scope: 'file' | 'selection'; + /** + * How was the code executed (through the command or by clicking the `Run File` icon). + * + * @type {('command' | 'icon')} + */ + trigger?: 'command' | 'icon'; + }; + /** + * Telemetry Event sent when user executes code against Django Shell. + * Values sent: + * scope + * + */ + [EventName.EXECUTION_DJANGO]: { + /** + * If `file`, then the file was executed in the django shell. + * If `selection`, then the selected text was sent to the django shell. + * + * @type {('file' | 'selection')} + */ + scope: 'file' | 'selection'; + }; + /** + * Telemetry event sent with details when formatting a document + */ + [EventName.FORMAT]: { + /** + * Tool being used to format + */ + tool: 'autopep8' | 'black' | 'yapf'; + /** + * If arguments for formatter is provided in resource settings + */ + hasCustomArgs: boolean; + /** + * Carries `true` when formatting a selection of text, `false` otherwise + */ + formatSelection: boolean; + }; + /** + * Telemetry event sent with the value of setting 'Format on type' + */ + [EventName.FORMAT_ON_TYPE]: { + /** + * Carries `true` if format on type is enabled, `false` otherwise + * + * @type {boolean} + */ + enabled: boolean; + }; + /** + * Telemetry event sent when sorting imports using formatter + */ + [EventName.FORMAT_SORT_IMPORTS]: never | undefined; + /** + * Telemetry event sent when Go to Python object command is executed + */ + [EventName.GO_TO_OBJECT_DEFINITION]: never | undefined; + /** + * Telemetry event sent when providing a hover for the given position and document for interactive window using Jedi. + */ + [EventName.HOVER_DEFINITION]: never | undefined; + /** + * Telemetry event sent with details when tracking imports + */ + [EventName.HASHED_PACKAGE_NAME]: { + /** + * Hash of the package name + * + * @type {string} + */ + hashedName: string; + }; + [Telemetry.HashedCellOutputMimeTypePerf]: never | undefined; + [Telemetry.HashedNotebookCellOutputMimeTypePerf]: never | undefined; + [Telemetry.HashedCellOutputMimeType]: { + /** + * Hash of the cell output mimetype + * + * @type {string} + */ + hashedName: string; + hasText: boolean; + hasLatex: boolean; + hasHtml: boolean; + hasSvg: boolean; + hasXml: boolean; + hasJson: boolean; + hasImage: boolean; + hasGeo: boolean; + hasPlotly: boolean; + hasVega: boolean; + hasWidget: boolean; + hasJupyter: boolean; + hasVnd: boolean; + }; + [EventName.HASHED_PACKAGE_PERF]: never | undefined; + /** + * Telemetry event sent with details of selection in prompt + * `Prompt message` :- 'Linter ${productName} is not installed' + */ + [EventName.LINTER_NOT_INSTALLED_PROMPT]: { + /** + * Name of the linter + * + * @type {LinterId} + */ + tool?: LinterId; + /** + * `select` When 'Select linter' option is selected + * `disablePrompt` When 'Do not show again' option is selected + * `install` When 'Install' option is selected + * + * @type {('select' | 'disablePrompt' | 'install')} + */ + action: 'select' | 'disablePrompt' | 'install'; + }; + /** + * Telemetry event sent when installing modules + */ + [EventName.PYTHON_INSTALL_PACKAGE]: { + /** + * The name of the module. (pipenv, Conda etc.) + * + * @type {string} + */ + installer: string; + }; + /** + * Telemetry sent with details immediately after linting a document completes + */ + [EventName.LINTING]: { + /** + * Name of the linter being used + * + * @type {LinterId} + */ + tool: LinterId; + /** + * If custom arguments for linter is provided in settings.json + * + * @type {boolean} + */ + hasCustomArgs: boolean; + /** + * Carries the source which triggered configuration of tests + * + * @type {LinterTrigger} + */ + trigger: LinterTrigger; + /** + * Carries `true` if linter executable is specified, `false` otherwise + * + * @type {boolean} + */ + executableSpecified: boolean; + }; + /** + * Telemetry event sent after fetching the OS version + */ + [EventName.PLATFORM_INFO]: { + /** + * If fetching OS version fails, list the failure type + * + * @type {PlatformErrors} + */ + failureType?: PlatformErrors; + /** + * The OS version of the platform + * + * @type {string} + */ + osVersion?: string; + }; + /** + * Telemetry is sent with details about the play run file icon + */ + [EventName.PLAY_BUTTON_ICON_DISABLED]: { + /** + * Carries `true` if play button icon is not shown (because code runner is installed), `false` otherwise + */ + disabled: boolean; + }; + /** + * Telemetry event sent when 'Select Interpreter' command is invoked. + */ + [EventName.SELECT_INTERPRETER]: never | undefined; + /** + * Telemetry event sent when 'Enter interpreter path' button is clicked. + */ + [EventName.SELECT_INTERPRETER_ENTER_BUTTON]: never | undefined; + /** + * Telemetry event sent with details about what choice user made to input the interpreter path. + */ + [EventName.SELECT_INTERPRETER_ENTER_CHOICE]: { + /** + * Carries 'enter' if user chose to enter the path to executable. + * Carries 'browse' if user chose to browse for the path to the executable. + */ + choice: 'enter' | 'browse'; + }; + /** + * Telemetry event sent with details after updating the python interpreter + */ + [EventName.PYTHON_INTERPRETER]: { + /** + * Carries the source which triggered the update + * + * @type {('ui' | 'shebang' | 'load')} + */ + trigger: 'ui' | 'shebang' | 'load'; + /** + * Carries `true` if updating python interpreter failed + * + * @type {boolean} + */ + failed: boolean; + /** + * The python version of the interpreter + * + * @type {string} + */ + pythonVersion?: string; + /** + * The version of pip module installed in the python interpreter + * + * @type {string} + */ + pipVersion?: string; + /** + * The bit-ness of the python interpreter represented using architecture. + * + * @type {Architecture} + */ + architecture?: Architecture; + }; + [EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES]: { + /** + * Carries `true` if environment variables are present, `false` otherwise + * + * @type {boolean} + */ + hasEnvVars?: boolean; + /** + * Carries `true` if fetching environment variables failed, `false` otherwise + * + * @type {boolean} + */ + failed?: boolean; + /** + * Whether the environment was activated within a terminal or not. + * + * @type {boolean} + */ + activatedInTerminal?: boolean; + /** + * Whether the environment was activated by the wrapper class. + * If `true`, this telemetry is sent by the class that wraps the two activation providers . + * + * @type {boolean} + */ + activatedByWrapper?: boolean; + }; + /** + * Telemetry event sent when getting activation commands for active interpreter + */ + [EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE]: { + /** + * Carries `true` if activation commands exists for interpreter, `false` otherwise + * + * @type {boolean} + */ + hasCommands?: boolean; + /** + * Carries `true` if fetching activation commands for interpreter failed, `false` otherwise + * + * @type {boolean} + */ + failed?: boolean; + /** + * The type of terminal shell to activate + * + * @type {TerminalShellType} + */ + terminal: TerminalShellType; + /** + * The Python interpreter version of the active interpreter for the resource + * + * @type {string} + */ + pythonVersion?: string; + /** + * The type of the interpreter used + * + * @type {EnvironmentType} + */ + interpreterType: EnvironmentType; + }; + /** + * Telemetry event sent when getting activation commands for terminal when interpreter is not specified + */ + [EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL]: { + /** + * Carries `true` if activation commands exists for terminal, `false` otherwise + * + * @type {boolean} + */ + hasCommands?: boolean; + /** + * Carries `true` if fetching activation commands for terminal failed, `false` otherwise + * + * @type {boolean} + */ + failed?: boolean; + /** + * The type of terminal shell to activate + * + * @type {TerminalShellType} + */ + terminal: TerminalShellType; + /** + * The Python interpreter version of the interpreter for the resource + * + * @type {string} + */ + pythonVersion?: string; + /** + * The type of the interpreter used + * + * @type {EnvironmentType} + */ + interpreterType: EnvironmentType; + }; + [EventName.PYTHON_INTERPRETER_AUTO_SELECTION]: { + /** + * The rule used to auto-select the interpreter + * + * @type {AutoSelectionRule} + */ + rule?: AutoSelectionRule; + /** + * If cached interpreter no longer exists or is invalid + * + * @type {boolean} + */ + interpreterMissing?: boolean; + /** + * Carries `true` if next rule is identified for autoselecting interpreter + * + * @type {boolean} + */ + identified?: boolean; + /** + * Carries `true` if cached interpreter is updated to use the current interpreter, `false` otherwise + * + * @type {boolean} + */ + updated?: boolean; + }; + /** + * Sends information regarding discovered python environments (virtualenv, conda, pipenv etc.) + */ + [EventName.PYTHON_INTERPRETER_DISCOVERY]: { + /** + * Name of the locator + */ + locator: string; + /** + * The number of the interpreters returned by locator + */ + interpreters?: number; + }; + /** + * Telemetry event sent when pipenv interpreter discovery is executed. + */ + [EventName.PIPENV_INTERPRETER_DISCOVERY]: never | undefined; + /** + * Telemetry event sent with details when user clicks the prompt with the following message + * `Prompt message` :- 'We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we suggest the "terminal.integrated.inheritEnv" setting to be changed to false. Would you like to update this setting?' + */ + [EventName.CONDA_INHERIT_ENV_PROMPT]: { + /** + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected + * `More info` When 'More Info' option is selected + */ + selection: 'Yes' | 'No' | 'More Info' | undefined; + }; + /** + * Telemetry event sent with details when user clicks the prompt with the following message + * `Prompt message` :- 'We found a Python environment in this workspace. Do you want to select it to start up the features in the Python extension? Only accept if you trust this environment.' + */ + [EventName.UNSAFE_INTERPRETER_PROMPT]: { + /** + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected + * `Learn more` When 'More Info' option is selected + * `Do not show again` When 'Do not show again' option is selected + */ + selection: 'Yes' | 'No' | 'Learn more' | 'Do not show again' | undefined; + }; + /** + * Telemetry event sent with details when user clicks a button in the virtual environment prompt. + * `Prompt message` :- 'We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?' + */ + [EventName.PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT]: { + /** + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected + * `Ignore` When 'Do not show again' option is clicked + * + * @type {('Yes' | 'No' | 'Ignore' | undefined)} + */ + selection: 'Yes' | 'No' | 'Ignore' | undefined; + }; + /** + * Telemetry event sent with details when the user clicks a button in the "Python is not installed" prompt. + * * `Prompt message` :- 'Python is not installed. Please download and install Python before using the extension.' + */ + [EventName.PYTHON_NOT_INSTALLED_PROMPT]: { + /** + * `Download` When the 'Download' option is clicked + * `Ignore` When the prompt is dismissed + * + * @type {('Download' | 'Ignore' | undefined)} + */ + selection: 'Download' | 'Ignore' | undefined; + }; + + /** + * Telemetry event sent with details when user clicks a button in the following prompt + * `Prompt message` :- 'We noticed you are using Visual Studio Code Insiders. Would you like to use the Insiders build of the Python extension?' + */ + [EventName.INSIDERS_PROMPT]: { + /** + * `Yes, weekly` When user selects to use "weekly" as extension channel in insiders prompt + * `Yes, daily` When user selects to use "daily" as extension channel in insiders prompt + * `No, thanks` When user decides to keep using the same extension channel as before + */ + selection: 'Yes, weekly' | 'Yes, daily' | 'No, thanks' | undefined; + }; + /** + * Telemetry event sent with details when user clicks a button in the 'Reload to install insiders prompt'. + * `Prompt message` :- 'Please reload Visual Studio Code to use the insiders build of the extension' + */ + [EventName.INSIDERS_RELOAD_PROMPT]: { + /** + * `Reload` When 'Reload' option is clicked + * `undefined` When prompt is closed + * + * @type {('Reload' | undefined)} + */ + selection: 'Reload' | undefined; + }; + /** + * Telemetry sent with details about the current selection of language server + */ + [EventName.PYTHON_LANGUAGE_SERVER_CURRENT_SELECTION]: { + /** + * The startup value of the language server setting + */ + lsStartup?: LanguageServerType; + /** + * Used to track switch between language servers. Carries the final state after the switch. + */ + switchTo?: LanguageServerType; + }; + /** + * Telemetry event sent with details after attempting to download LS + */ + [EventName.PYTHON_LANGUAGE_SERVER_DOWNLOADED]: { + /** + * Whether LS downloading succeeds + */ + success: boolean; + /** + * Version of LS downloaded + */ + lsVersion?: string; + /** + * Whether download uri starts with `https:` or not + */ + usedSSL?: boolean; + + /** + * Name of LS downloaded + */ + lsName?: string; + }; + /** + * Telemetry event sent when LS is started for workspace (workspace folder in case of multi-root) + */ + [EventName.PYTHON_LANGUAGE_SERVER_ENABLED]: { + lsVersion?: string; + }; + /** + * Telemetry event sent with details when downloading or extracting LS fails + */ + [EventName.PYTHON_LANGUAGE_SERVER_ERROR]: { + /** + * The error associated with initializing language server + */ + error: string; + }; + /** + * Telemetry event sent with details after attempting to extract LS + */ + [EventName.PYTHON_LANGUAGE_SERVER_EXTRACTED]: { + /** + * Whether LS extracting succeeds + */ + success: boolean; + /** + * Version of LS extracted + */ + lsVersion?: string; + /** + * Whether download uri starts with `https:` or not + */ + usedSSL?: boolean; + /** + * Package name of LS extracted + */ + lsName?: string; + }; + /** + * Telemetry event sent if azure blob packages are being listed + */ + [EventName.PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES]: never | undefined; + /** + * Tracks if LS is supported on platform or not + */ + [EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED]: { + /** + * Carries `true` if LS is supported, `false` otherwise + * + * @type {boolean} + */ + supported: boolean; + /** + * If checking support for LS failed + * + * @type {'UnknownError'} + */ + failureType?: 'UnknownError'; + }; + /** + * Telemetry event sent when LS is ready to start + */ + [EventName.PYTHON_LANGUAGE_SERVER_READY]: { + lsVersion?: string; + }; + /** + * Telemetry event sent when starting LS + */ + [EventName.PYTHON_LANGUAGE_SERVER_STARTUP]: { + lsVersion?: string; + }; + /** + * Telemetry sent from language server (details of telemetry sent can be provided by LS team) + */ + [EventName.PYTHON_LANGUAGE_SERVER_TELEMETRY]: any; + /** + * Telemetry sent when the client makes a request to the language server + */ + [EventName.PYTHON_LANGUAGE_SERVER_REQUEST]: any; + /** + * Telemetry event sent with details when inExperiment() API is called + */ + [EventName.PYTHON_EXPERIMENTS]: { + /** + * Name of the experiment group the user is in + * @type {string} + */ + expName?: string; + }; + /** + * Telemetry event sent when Experiments have been disabled. + */ + [EventName.PYTHON_EXPERIMENTS_DISABLED]: never | undefined; + /** + * Telemetry event sent with details when a user has requested to opt it or out of an experiment group + */ + [EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT]: { + /** + * Carries the name of the experiment user has been opted into manually + */ + expNameOptedInto?: string; + /** + * Carries the name of the experiment user has been opted out of manually + */ + expNameOptedOutOf?: string; + }; + /** + * Telemetry event sent with details when doing best effort to download the experiments within timeout and using it in the current session only + */ + [EventName.PYTHON_EXPERIMENTS_DOWNLOAD_SUCCESS_RATE]: { + /** + * Carries `true` if downloading experiments successfully finishes within timeout, `false` otherwise + * @type {boolean} + */ + success?: boolean; + /** + * Carries an error string if downloading experiments fails with error + * @type {string} + */ + error?: string; + }; + /** + * Telemetry event sent when LS is started for workspace (workspace folder in case of multi-root) + */ + [EventName.LANGUAGE_SERVER_ENABLED]: { + lsVersion?: string; + }; + /** + * Telemetry event sent when Node.js server is ready to start + */ + [EventName.LANGUAGE_SERVER_READY]: { + lsVersion?: string; + }; + /** + * Telemetry event sent when starting Node.js server + */ + [EventName.LANGUAGE_SERVER_STARTUP]: { + lsVersion?: string; + }; + /** + * Telemetry sent from Node.js server (details of telemetry sent can be provided by LS team) + */ + [EventName.LANGUAGE_SERVER_TELEMETRY]: any; + /** + * Telemetry sent when the client makes a request to the Node.js server + */ + [EventName.LANGUAGE_SERVER_REQUEST]: any; + /** + * Telemetry sent on user response to 'Try Pylance' prompt. + */ + [EventName.LANGUAGE_SERVER_TRY_PYLANCE]: { + /** + * User response to the prompt. + * @type {string} + */ + userAction: string; + }; + /** + * Telemetry captured for enabling reload. + */ + [EventName.PYTHON_WEB_APP_RELOAD]: { + /** + * Carries value indicating if the experiment modified `subProcess` field in debug config: + * - `true` if reload experiment modified the `subProcess` field. + * - `false` if user provided debug configuration was not changed (already setup for reload) + */ + subProcessModified?: boolean; + /** + * Carries value indicating if the experiment modified `args` field in debug config: + * - `true` if reload experiment modified the `args` field. + * - `false` if user provided debug configuration was not changed (already setup for reload) + */ + argsModified?: boolean; + }; + /** + * When user clicks a button in the python extension survey prompt, this telemetry event is sent with details + */ + [EventName.EXTENSION_SURVEY_PROMPT]: { + /** + * Carries the selection of user when they are asked to take the extension survey + */ + selection: 'Yes' | 'Maybe later' | 'Do not show again' | undefined; + }; + /** + * Telemetry event sent when the Python interpreter tip is shown on activation for new users. + */ + [EventName.ACTIVATION_TIP_PROMPT]: never | undefined; + /** + * Telemetry event sent when the feedback survey prompt is shown on activation for new users, and they click on the survey link. + */ + [EventName.ACTIVATION_SURVEY_PROMPT]: never | undefined; + /** + * Telemetry sent back when join mailing list prompt is shown. + */ + [EventName.JOIN_MAILING_LIST_PROMPT]: { + /** + * Carries the selection of user when they are asked to join the mailing list. + */ + selection: 'Yes' | 'No' | undefined; + }; + /** + * Telemetry event sent when 'Extract Method' command is invoked + */ + [EventName.REFACTOR_EXTRACT_FUNCTION]: never | undefined; + /** + * Telemetry event sent when 'Extract Variable' command is invoked + */ + [EventName.REFACTOR_EXTRACT_VAR]: never | undefined; + /** + * Telemetry event sent when providing an edit that describes changes to rename a symbol to a different name + */ + [EventName.REFACTOR_RENAME]: never | undefined; + /** + * Telemetry event sent when providing a set of project-wide references for the given position and document + */ + [EventName.REFERENCE]: never | undefined; + /** + * Telemetry event sent when starting REPL + */ + [EventName.REPL]: never | undefined; + /** + * Telemetry event sent with details of linter selected in quickpick of linter list. + */ + [EventName.SELECT_LINTER]: { + /** + * The name of the linter + */ + tool?: LinterId; + /** + * Carries `true` if linter is enabled, `false` otherwise + */ + enabled: boolean; + }; + /** + * Telemetry event sent with details when clicking the prompt with the following message, + * `Prompt message` :- 'You have a pylintrc file in your workspace. Do you want to enable pylint?' + */ + [EventName.CONFIGURE_AVAILABLE_LINTER_PROMPT]: { + /** + * Name of the linter tool + * + * @type {LinterId} + */ + tool: LinterId; + /** + * `enable` When 'Enable [linter name]' option is clicked + * `ignore` When 'Not now' option is clicked + * `disablePrompt` When 'Do not show again` option is clicked + * + * @type {('enable' | 'ignore' | 'disablePrompt' | undefined)} + */ + action: 'enable' | 'ignore' | 'disablePrompt' | undefined; + }; + /** + * Telemetry event sent when providing help for the signature at the given position and document. + */ + [EventName.SIGNATURE]: never | undefined; + /** + * Telemetry event sent when providing document symbol information for Jedi autocomplete intellisense + */ + [EventName.SYMBOL]: never | undefined; + /** + * Telemetry event sent if and when user configure tests command. This command can be trigerred from multiple places in the extension. (Command palette, prompt etc.) + */ + [EventName.UNITTEST_CONFIGURE]: never | undefined; + /** + * Telemetry event sent when user chooses a test framework in the Quickpick displayed for enabling and configuring test framework + */ + [EventName.UNITTEST_CONFIGURING]: { + /** + * Name of the test framework to configure + */ + tool?: TestTool; + /** + * Carries the source which triggered configuration of tests + * + * @type {('ui' | 'commandpalette')} + */ + trigger: 'ui' | 'commandpalette'; + /** + * Carries `true` if configuring test framework failed, `false` otherwise + * + * @type {boolean} + */ + failed: boolean; + }; + /** + * Telemetry event sent when the extension is activated, if an active terminal is present and + * the `python.terminal.activateEnvInCurrentTerminal` setting is set to `true`. + */ + [EventName.ACTIVATE_ENV_IN_CURRENT_TERMINAL]: { + /** + * Carries boolean `true` if an active terminal is present (terminal is visible), `false` otherwise + */ + isTerminalVisible?: boolean; + }; + /** + * Telemetry event sent with details when a terminal is created + */ + [EventName.TERMINAL_CREATE]: { + /** + * The type of terminal shell created: powershell, cmd, zsh, bash etc. + * + * @type {TerminalShellType} + */ + terminal?: TerminalShellType; + /** + * The source which triggered creation of terminal + * + * @type {'commandpalette'} + */ + triggeredBy?: 'commandpalette'; + /** + * The default Python interpreter version to be used in terminal, inferred from resource's 'settings.json' + * + * @type {string} + */ + pythonVersion?: string; + /** + * The Python interpreter type: Conda, Virtualenv, Venv, Pipenv etc. + * + * @type {EnvironmentType} + */ + interpreterType?: EnvironmentType; + }; + /** + * Telemetry event sent with details about discovering tests + */ + [EventName.UNITTEST_DISCOVER]: { + /** + * The test framework used to discover tests + * + * @type {TestTool} + */ + tool: TestTool; + /** + * Carries the source which triggered discovering of tests + * + * @type {('ui' | 'commandpalette')} + */ + trigger: 'ui' | 'commandpalette'; + /** + * Carries `true` if discovering tests failed, `false` otherwise + * + * @type {boolean} + */ + failed: boolean; + }; + /** + * Telemetry event is sent if we are doing test discovery using python code + */ + [EventName.UNITTEST_DISCOVER_WITH_PYCODE]: never | undefined; + /** + * Telemetry event sent when user clicks a file, function, or suite in test explorer. + */ + [EventName.UNITTEST_NAVIGATE]: { + /** + * Carries `true` if user clicks a file, `false` otherwise + * + * @type {boolean} + */ + byFile?: boolean; + /** + * Carries `true` if user clicks a function, `false` otherwise + * + * @type {boolean} + */ + byFunction?: boolean; + /** + * Carries `true` if user clicks a suite, `false` otherwise + * + * @type {boolean} + */ + bySuite?: boolean; + /** + * Carries `true` if we are changing focus to the suite/file/function, `false` otherwise + * + * @type {boolean} + */ + focus_code?: boolean; + }; + /** + * Tracks number of workspace folders shown in test explorer + */ + [EventName.UNITTEST_EXPLORER_WORK_SPACE_COUNT]: { count: number }; + /** + * Telemetry event sent with details about running the tests, what is being run, what framework is being used etc. + */ + [EventName.UNITTEST_RUN]: { + /** + * Framework being used to run tests + */ + tool: TestTool; + /** + * Carries info what is being run + */ + scope: 'currentFile' | 'all' | 'file' | 'class' | 'function' | 'failed'; + /** + * Carries `true` if debugging, `false` otherwise + */ + debugging: boolean; + /** + * Carries what triggered the execution of the tests + */ + triggerSource: 'ui' | 'codelens' | 'commandpalette' | 'auto' | 'testExplorer'; + /** + * Carries `true` if running tests failed, `false` otherwise + */ + failed: boolean; + }; + /** + * Telemetry event sent when cancelling running or discovering tests + */ + [EventName.UNITTEST_STOP]: never | undefined; + /** + * Telemetry event sent when disabling all test frameworks + */ + [EventName.UNITTEST_DISABLE]: never | undefined; + /** + * Telemetry event sent when viewing Python test log output + */ + [EventName.UNITTEST_VIEW_OUTPUT]: never | undefined; + /** + * Tracks which testing framework has been enabled by the user. + * Telemetry is sent when settings have been modified by the user. + * Values sent include: + * unittest - If this value is `true`, then unittest has been enabled by the user. + * pytest - If this value is `true`, then pytest has been enabled by the user. + * nosetest - If this value is `true`, then nose has been enabled by the user. + * @type {(never | undefined)} + * @memberof IEventNamePropertyMapping + */ + [EventName.UNITTEST_ENABLED]: Partial>; + /** + * Telemetry sent when building workspace symbols + */ + [EventName.WORKSPACE_SYMBOLS_BUILD]: never | undefined; + /** + * Telemetry sent when providing workspace symbols doing Project-wide search for a symbol matching the given query string + */ + [EventName.WORKSPACE_SYMBOLS_GO_TO]: never | undefined; + // Data Science + [Telemetry.AddCellBelow]: never | undefined; + [Telemetry.CodeLensAverageAcquisitionTime]: never | undefined; + [Telemetry.CollapseAll]: never | undefined; + [Telemetry.ConnectFailedJupyter]: never | undefined; + [Telemetry.ConnectLocalJupyter]: never | undefined; + [Telemetry.ConnectRemoteJupyter]: never | undefined; + /** + * Connecting to an existing Jupyter server, but connecting to localhost. + */ + [Telemetry.ConnectRemoteJupyterViaLocalHost]: never | undefined; + [Telemetry.ConnectRemoteFailedJupyter]: never | undefined; + [Telemetry.ConnectRemoteSelfCertFailedJupyter]: never | undefined; + [Telemetry.RegisterAndUseInterpreterAsKernel]: never | undefined; + [Telemetry.UseInterpreterAsKernel]: never | undefined; + [Telemetry.UseExistingKernel]: never | undefined; + [Telemetry.SwitchToExistingKernel]: { language: string }; + [Telemetry.SwitchToInterpreterAsKernel]: never | undefined; + [Telemetry.ConvertToPythonFile]: never | undefined; + [Telemetry.CopySourceCode]: never | undefined; + [Telemetry.CreateNewNotebook]: never | undefined; + [Telemetry.DataScienceSettings]: JSONObject; + [Telemetry.DebugContinue]: never | undefined; + [Telemetry.DebugCurrentCell]: never | undefined; + [Telemetry.DebugStepOver]: never | undefined; + [Telemetry.DebugStop]: never | undefined; + [Telemetry.DebugFileInteractive]: never | undefined; + [Telemetry.DeleteAllCells]: never | undefined; + [Telemetry.DeleteCell]: never | undefined; + [Telemetry.FindJupyterCommand]: { command: string }; + [Telemetry.FindJupyterKernelSpec]: never | undefined; + [Telemetry.DisableInteractiveShiftEnter]: never | undefined; + [Telemetry.EnableInteractiveShiftEnter]: never | undefined; + [Telemetry.ExecuteCell]: never | undefined; + /** + * Telemetry sent to capture first time execution of a cell. + * If `notebook = true`, this its telemetry for native editor/notebooks. + */ + [Telemetry.ExecuteCellPerceivedCold]: undefined | { notebook: boolean }; + /** + * Telemetry sent to capture subsequent execution of a cell. + * If `notebook = true`, this its telemetry for native editor/notebooks. + */ + [Telemetry.ExecuteCellPerceivedWarm]: undefined | { notebook: boolean }; + /** + * Time take for jupyter server to start and be ready to run first user cell. + */ + [Telemetry.PerceivedJupyterStartupNotebook]: never | undefined; + /** + * Time take for jupyter server to be busy from the time user first hit `run` cell until jupyter reports it is busy running a cell. + */ + [Telemetry.StartExecuteNotebookCellPerceivedCold]: never | undefined; + [Telemetry.ExecuteNativeCell]: never | undefined; + [Telemetry.ExpandAll]: never | undefined; + [Telemetry.ExportNotebookInteractive]: never | undefined; + [Telemetry.ExportPythonFileInteractive]: never | undefined; + [Telemetry.ExportPythonFileAndOutputInteractive]: never | undefined; + [Telemetry.ClickedExportNotebookAsQuickPick]: { format: ExportFormat }; + [Telemetry.ExportNotebookAs]: { format: ExportFormat; cancelled?: boolean; successful?: boolean; opened?: boolean }; + [Telemetry.ExportNotebookAsCommand]: { format: ExportFormat }; + [Telemetry.ExportNotebookAsFailed]: { format: ExportFormat }; + [Telemetry.GetPasswordAttempt]: never | undefined; + [Telemetry.GetPasswordFailure]: never | undefined; + [Telemetry.GetPasswordSuccess]: never | undefined; + [Telemetry.GotoSourceCode]: never | undefined; + [Telemetry.HiddenCellTime]: never | undefined; + [Telemetry.ImportNotebook]: { scope: 'command' | 'file' }; + [Telemetry.Interrupt]: never | undefined; + [Telemetry.InterruptJupyterTime]: never | undefined; + [Telemetry.NotebookRunCount]: { count: number }; + [Telemetry.NotebookWorkspaceCount]: { count: number }; + [Telemetry.NotebookOpenCount]: { count: number }; + [Telemetry.NotebookOpenTime]: number; + [Telemetry.PandasNotInstalled]: never | undefined; + [Telemetry.PandasTooOld]: never | undefined; + [Telemetry.DebugpyInstallCancelled]: never | undefined; + [Telemetry.DebugpyInstallFailed]: never | undefined; + [Telemetry.DebugpyPromptToInstall]: never | undefined; + [Telemetry.DebugpySuccessfullyInstalled]: never | undefined; + [Telemetry.OpenNotebook]: { scope: 'command' | 'file' }; + [Telemetry.OpenNotebookAll]: never | undefined; + [Telemetry.OpenedInteractiveWindow]: never | undefined; + [Telemetry.OpenPlotViewer]: never | undefined; + [Telemetry.Redo]: never | undefined; + [Telemetry.RemoteAddCode]: never | undefined; + [Telemetry.RemoteReexecuteCode]: never | undefined; + [Telemetry.RestartJupyterTime]: never | undefined; + [Telemetry.RestartKernel]: never | undefined; + [Telemetry.RestartKernelCommand]: never | undefined; + /** + * Run Cell Commands in Interactive Python + */ + [Telemetry.RunAllCells]: never | undefined; + [Telemetry.RunSelectionOrLine]: never | undefined; + [Telemetry.RunCell]: never | undefined; + [Telemetry.RunCurrentCell]: never | undefined; + [Telemetry.RunAllCellsAbove]: never | undefined; + [Telemetry.RunCellAndAllBelow]: never | undefined; + [Telemetry.RunCurrentCellAndAdvance]: never | undefined; + [Telemetry.RunToLine]: never | undefined; + [Telemetry.RunFileInteractive]: never | undefined; + [Telemetry.RunFromLine]: never | undefined; + [Telemetry.ScrolledToCell]: never | undefined; + /** + * Cell Edit Commands in Interactive Python + */ + [Telemetry.InsertCellBelowPosition]: never | undefined; + [Telemetry.InsertCellBelow]: never | undefined; + [Telemetry.InsertCellAbove]: never | undefined; + [Telemetry.DeleteCells]: never | undefined; + [Telemetry.SelectCell]: never | undefined; + [Telemetry.SelectCellContents]: never | undefined; + [Telemetry.ExtendSelectionByCellAbove]: never | undefined; + [Telemetry.ExtendSelectionByCellBelow]: never | undefined; + [Telemetry.MoveCellsUp]: never | undefined; + [Telemetry.MoveCellsDown]: never | undefined; + [Telemetry.ChangeCellToMarkdown]: never | undefined; + [Telemetry.ChangeCellToCode]: never | undefined; + [Telemetry.GotoNextCellInFile]: never | undefined; + [Telemetry.GotoPrevCellInFile]: never | undefined; + /** + * Misc + */ + [Telemetry.AddEmptyCellToBottom]: never | undefined; + [Telemetry.RunCurrentCellAndAddBelow]: never | undefined; + [Telemetry.CellCount]: { count: number }; + [Telemetry.Save]: never | undefined; + [Telemetry.SelfCertsMessageClose]: never | undefined; + [Telemetry.SelfCertsMessageEnabled]: never | undefined; + [Telemetry.SelectJupyterURI]: never | undefined; + [Telemetry.SelectLocalJupyterKernel]: never | undefined; + [Telemetry.SelectRemoteJupyterKernel]: never | undefined; + [Telemetry.SessionIdleTimeout]: never | undefined; + [Telemetry.JupyterNotInstalledErrorShown]: never | undefined; + [Telemetry.JupyterCommandSearch]: { + where: 'activeInterpreter' | 'otherInterpreter' | 'path' | 'nowhere'; + command: JupyterCommands; + }; + [Telemetry.UserInstalledJupyter]: never | undefined; + [Telemetry.UserInstalledPandas]: never | undefined; + [Telemetry.UserDidNotInstallJupyter]: never | undefined; + [Telemetry.UserDidNotInstallPandas]: never | undefined; + [Telemetry.SetJupyterURIToLocal]: never | undefined; + [Telemetry.SetJupyterURIToUserSpecified]: never | undefined; + [Telemetry.ShiftEnterBannerShown]: never | undefined; + [Telemetry.ShowDataViewer]: { rows: number | undefined; columns: number | undefined }; + [Telemetry.CreateNewInteractive]: never | undefined; + [Telemetry.StartJupyter]: never | undefined; + [Telemetry.StartJupyterProcess]: never | undefined; + /** + * Telemetry event sent when jupyter has been found in interpreter but we cannot find kernelspec. + * + * @type {(never | undefined)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.JupyterInstalledButNotKernelSpecModule]: never | undefined; + [Telemetry.JupyterStartTimeout]: { + /** + * Total time spent in attempting to start and connect to jupyter before giving up. + * + * @type {number} + */ + timeout: number; + }; + [Telemetry.SubmitCellThroughInput]: never | undefined; + [Telemetry.Undo]: never | undefined; + [Telemetry.VariableExplorerFetchTime]: never | undefined; + [Telemetry.VariableExplorerToggled]: { open: boolean; runByLine: boolean }; + [Telemetry.VariableExplorerVariableCount]: { variableCount: number }; + [Telemetry.WaitForIdleJupyter]: never | undefined; + [Telemetry.WebviewMonacoStyleUpdate]: never | undefined; + [Telemetry.WebviewStartup]: { type: string }; + [Telemetry.WebviewStyleUpdate]: never | undefined; + [Telemetry.RegisterInterpreterAsKernel]: never | undefined; + /** + * Telemetry sent when user selects an interpreter to start jupyter server. + * + * @type {(never | undefined)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.SelectJupyterInterpreterCommand]: never | undefined; + [Telemetry.SelectJupyterInterpreter]: { + /** + * The result of the selection. + * notSelected - No interpreter was selected. + * selected - An interpreter was selected (and configured to have jupyter and notebook). + * installationCancelled - Installation of jupyter and/or notebook was cancelled for an interpreter. + * + * @type {('notSelected' | 'selected' | 'installationCancelled')} + */ + result?: 'notSelected' | 'selected' | 'installationCancelled'; + }; + [NativeKeyboardCommandTelemetry.ArrowDown]: never | undefined; + [NativeKeyboardCommandTelemetry.ArrowUp]: never | undefined; + [NativeKeyboardCommandTelemetry.ChangeToCode]: never | undefined; + [NativeKeyboardCommandTelemetry.ChangeToMarkdown]: never | undefined; + [NativeKeyboardCommandTelemetry.DeleteCell]: never | undefined; + [NativeKeyboardCommandTelemetry.InsertAbove]: never | undefined; + [NativeKeyboardCommandTelemetry.InsertBelow]: never | undefined; + [NativeKeyboardCommandTelemetry.Redo]: never | undefined; + [NativeKeyboardCommandTelemetry.Run]: never | undefined; + [NativeKeyboardCommandTelemetry.RunAndAdd]: never | undefined; + [NativeKeyboardCommandTelemetry.RunAndMove]: never | undefined; + [NativeKeyboardCommandTelemetry.Save]: never | undefined; + [NativeKeyboardCommandTelemetry.ToggleLineNumbers]: never | undefined; + [NativeKeyboardCommandTelemetry.ToggleOutput]: never | undefined; + [NativeKeyboardCommandTelemetry.Undo]: never | undefined; + [NativeKeyboardCommandTelemetry.Unfocus]: never | undefined; + [NativeMouseCommandTelemetry.AddToEnd]: never | undefined; + [NativeMouseCommandTelemetry.ChangeToCode]: never | undefined; + [NativeMouseCommandTelemetry.ChangeToMarkdown]: never | undefined; + [NativeMouseCommandTelemetry.DeleteCell]: never | undefined; + [NativeMouseCommandTelemetry.InsertBelow]: never | undefined; + [NativeMouseCommandTelemetry.MoveCellDown]: never | undefined; + [NativeMouseCommandTelemetry.MoveCellUp]: never | undefined; + [NativeMouseCommandTelemetry.Run]: never | undefined; + [NativeMouseCommandTelemetry.RunAbove]: never | undefined; + [NativeMouseCommandTelemetry.RunAll]: never | undefined; + [NativeMouseCommandTelemetry.RunBelow]: never | undefined; + [NativeMouseCommandTelemetry.Save]: never | undefined; + [NativeMouseCommandTelemetry.SelectKernel]: never | undefined; + [NativeMouseCommandTelemetry.SelectServer]: never | undefined; + [NativeMouseCommandTelemetry.ToggleVariableExplorer]: never | undefined; + /* + Telemetry event sent with details of Jedi Memory usage. + mem_use - Memory usage of Process in kb. + limit - Upper bound for memory usage of Jedi process. + isUserDefinedLimit - Whether the user has configfured the upper bound limit. + restart - Whether to restart the Jedi Process (i.e. memory > limit). + */ + [EventName.JEDI_MEMORY]: { mem_use: number; limit: number; isUserDefinedLimit: boolean; restart: boolean }; + /* + Telemetry event sent to provide information on whether we have successfully identify the type of shell used. + This information is useful in determining how well we identify shells on users machines. + This impacts executing code in terminals and activation of environments in terminal. + So, the better this works, the better it is for the user. + failed - If true, indicates we have failed to identify the shell. Note this impacts impacts ability to activate environments in the terminal & code. + shellIdentificationSource - How was the shell identified. One of 'terminalName' | 'settings' | 'environment' | 'default' + If terminalName, then this means we identified the type of the shell based on the name of the terminal. + If settings, then this means we identified the type of the shell based on user settings in VS Code. + If environment, then this means we identified the type of the shell based on their environment (env variables, etc). + I.e. their default OS Shell. + If default, then we reverted to OS defaults (cmd on windows, and bash on the rest). + This is the worst case scenario. + I.e. we could not identify the shell at all. + terminalProvided - If true, we used the terminal provided to detec the shell. If not provided, we use the default shell on user machine. + hasCustomShell - If undefined (not set), we didn't check. + If true, user has customzied their shell in VSC Settings. + hasShellInEnv - If undefined (not set), we didn't check. + If true, user has a shell in their environment. + If false, user does not have a shell in their environment. + */ + [EventName.TERMINAL_SHELL_IDENTIFICATION]: { + failed: boolean; + terminalProvided: boolean; + shellIdentificationSource: 'terminalName' | 'settings' | 'environment' | 'default' | 'vscode'; + hasCustomShell: undefined | boolean; + hasShellInEnv: undefined | boolean; + }; + /** + * Telemetry event sent when getting environment variables for an activated environment has failed. + * + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [EventName.ACTIVATE_ENV_TO_GET_ENV_VARS_FAILED]: { + /** + * Whether the activation commands contain the name `conda`. + * + * @type {boolean} + */ + isPossiblyCondaEnv: boolean; + /** + * The type of terminal shell created: powershell, cmd, zsh, bash etc. + * + * @type {TerminalShellType} + */ + terminal: TerminalShellType; + }; + /** + * Telemetry event sent once done searching for kernel spec and interpreter for a local connection. + * + * @type {{ + * kernelSpecFound: boolean; + * interpreterFound: boolean; + * }} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.FindKernelForLocalConnection]: { + /** + * Whether a kernel spec was found. + * + * @type {boolean} + */ + kernelSpecFound: boolean; + /** + * Whether an interpreter was found. + * + * @type {boolean} + */ + interpreterFound: boolean; + /** + * Whether user was prompted to select a kernel spec. + * + * @type {boolean} + */ + promptedToSelect?: boolean; + }; + /** + * Telemetry event sent when starting a session for a local connection failed. + * + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.StartSessionFailedJupyter]: undefined | never; + /** + * Telemetry event fired if a failure occurs loading a notebook + */ + [Telemetry.OpenNotebookFailure]: undefined | never; + /** + * Telemetry event sent to capture total time taken for completions list to be provided by LS. + * This is used to compare against time taken by Jupyter. + * + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.CompletionTimeFromLS]: undefined | never; + /** + * Telemetry event sent to capture total time taken for completions list to be provided by Jupyter. + * This is used to compare against time taken by LS. + * + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.CompletionTimeFromJupyter]: undefined | never; + /** + * Telemetry event sent to indicate the language used in a notebook + * + * @type { language: string } + * @memberof IEventNamePropertyMapping + */ + [Telemetry.NotebookLanguage]: { + /** + * Language found in the notebook if a known language. Otherwise 'unknown' + */ + language: string; + }; + /** + * Telemetry event sent to indicate 'jupyter kernelspec' is not possible. + * + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.KernelSpecNotFound]: undefined | never; + /** + * Telemetry event sent to indicate registering a kernel with jupyter failed. + * + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.KernelRegisterFailed]: undefined | never; + /** + * Telemetry event sent to every time a kernel enumeration is done + * + * @type {...} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.KernelEnumeration]: { + /** + * Count of the number of kernels found + */ + count: number; + /** + * Boolean indicating if any are python or not + */ + isPython: boolean; + /** + * Indicates how the enumeration was acquired. + */ + source: 'cli' | 'connection'; + }; + /** + * Total time taken to Launch a raw kernel. + */ + [Telemetry.KernelLauncherPerf]: undefined | never; + /** + * Total time taken to find a kernel on disc. + */ + [Telemetry.KernelFinderPerf]: undefined | never; + /** + * Telemetry event sent if there's an error installing a jupyter required dependency + * + * @type { product: string } + * @memberof IEventNamePropertyMapping + */ + [Telemetry.JupyterInstallFailed]: { + /** + * Product being installed (jupyter or ipykernel or other) + */ + product: string; + }; + /** + * Telemetry event sent when installing a jupyter dependency + * + * @type {product: string} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.UserInstalledModule]: { product: string }; + /** + * Telemetry event sent to when user customizes the jupyter command line + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.JupyterCommandLineNonDefault]: undefined | never; + /** + * Telemetry event sent when a user runs the interactive window with a new file + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.NewFileForInteractiveWindow]: undefined | never; + /** + * Telemetry event sent when a kernel picked crashes on startup + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + [Telemetry.KernelInvalid]: undefined | never; + [Telemetry.GatherIsInstalled]: undefined | never; + [Telemetry.GatherCompleted]: { + /** + * result indicates whether the gather was completed to a script, notebook or suffered an internal error. + */ + result: 'err' | 'script' | 'notebook' | 'unavailable'; + }; + [Telemetry.GatherStats]: { + linesSubmitted: number; + cellsSubmitted: number; + linesGathered: number; + cellsGathered: number; + }; + [Telemetry.GatherException]: { + exceptionType: 'activate' | 'gather' | 'log' | 'reset'; + }; + /** + * Telemetry event sent when a gathered notebook has been saved by the user. + */ + [Telemetry.GatheredNotebookSaved]: undefined | never; + /** + * Telemetry event sent when the user reports whether Gathered notebook was good or not + */ + [Telemetry.GatherQualityReport]: { result: 'yes' | 'no' }; + /** + * Telemetry event sent when the ZMQ native binaries do not work. + */ + [Telemetry.ZMQNotSupported]: undefined | never; + /** + * Telemetry event sent when the ZMQ native binaries do work. + */ + [Telemetry.ZMQSupported]: undefined | never; + /** + * Telemetry event sent with name of a Widget that is used. + */ + [Telemetry.HashedIPyWidgetNameUsed]: { + /** + * Hash of the widget + */ + hashedName: string; + /** + * Where did we find the hashed name (CDN or user environment or remote jupyter). + */ + source?: 'cdn' | 'local' | 'remote'; + /** + * Whether we searched CDN or not. + */ + cdnSearched: boolean; + }; + /** + * Telemetry event sent with name of a Widget found. + */ + [Telemetry.HashedIPyWidgetNameDiscovered]: { + /** + * Hash of the widget + */ + hashedName: string; + /** + * Where did we find the hashed name (CDN or user environment or remote jupyter). + */ + source?: 'cdn' | 'local' | 'remote'; + }; + /** + * Total time taken to discover all IPyWidgets on disc. + * This is how long it takes to discover a single widget on disc (from python environment). + */ + [Telemetry.DiscoverIPyWidgetNamesLocalPerf]: never | undefined; + /** + * Something went wrong in looking for a widget. + */ + [Telemetry.HashedIPyWidgetScriptDiscoveryError]: never | undefined; + /** + * Telemetry event sent when an ipywidget module loads. Module name is hashed. + */ + [Telemetry.IPyWidgetLoadSuccess]: { moduleHash: string; moduleVersion: string }; + /** + * Telemetry event sent when an ipywidget module fails to load. Module name is hashed. + */ + [Telemetry.IPyWidgetLoadFailure]: { + isOnline: boolean; + moduleHash: string; + moduleVersion: string; + // Whether we timedout getting the source of the script (fetching script source in extension code). + timedout: boolean; + }; + /** + * Telemetry event sent when an ipywidget version that is not supported is used & we have trapped this and warned the user abou it. + */ + [Telemetry.IPyWidgetWidgetVersionNotSupportedLoadFailure]: { moduleHash: string; moduleVersion: string }; + /** + * Telemetry event sent when an loading of 3rd party ipywidget JS scripts from 3rd party source has been disabled. + */ + [Telemetry.IPyWidgetLoadDisabled]: { moduleHash: string; moduleVersion: string }; + /** + * Total time taken to discover a widget script on CDN. + */ + [Telemetry.DiscoverIPyWidgetNamesCDNPerf]: { + // The CDN we were testing. + cdn: string; + // Whether we managed to find the widget on the CDN or not. + exists: boolean; + }; + /** + * Telemetry sent when we prompt user to use a CDN for IPyWidget scripts. + * This is always sent when we display a prompt. + */ + [Telemetry.IPyWidgetPromptToUseCDN]: never | undefined; + /** + * Telemetry sent when user does somethign with the prompt displsyed to user about using CDN for IPyWidget scripts. + */ + [Telemetry.IPyWidgetPromptToUseCDNSelection]: { + selection: 'ok' | 'cancel' | 'dismissed' | 'doNotShowAgain'; + }; + /** + * Telemetry event sent to indicate the overhead of syncing the kernel with the UI. + */ + [Telemetry.IPyWidgetOverhead]: { + totalOverheadInMs: number; + numberOfMessagesWaitedOn: number; + averageWaitTime: number; + numberOfRegisteredHooks: number; + }; + /** + * Telemetry event sent when the widget render function fails (note, this may not be sufficient to capture all failures). + */ + [Telemetry.IPyWidgetRenderFailure]: never | undefined; + /** + * Telemetry event sent when the widget tries to send a kernel message but nothing was listening + */ + [Telemetry.IPyWidgetUnhandledMessage]: { + msg_type: string; + }; + + // Telemetry send when we create a notebook for a raw kernel or jupyter + [Telemetry.RawKernelCreatingNotebook]: never | undefined; + [Telemetry.JupyterCreatingNotebook]: never | undefined; + + // Raw kernel timing events + [Telemetry.RawKernelSessionConnect]: never | undefined; + [Telemetry.RawKernelStartRawSession]: never | undefined; + [Telemetry.RawKernelProcessLaunch]: never | undefined; + + // Raw kernel single events + [Telemetry.RawKernelSessionStartSuccess]: never | undefined; + [Telemetry.RawKernelSessionStartException]: never | undefined; + [Telemetry.RawKernelSessionStartTimeout]: never | undefined; + [Telemetry.RawKernelSessionStartUserCancel]: never | undefined; + + // Start Page Events + [Telemetry.StartPageViewed]: never | undefined; + [Telemetry.StartPageOpenedFromCommandPalette]: never | undefined; + [Telemetry.StartPageOpenedFromNewInstall]: never | undefined; + [Telemetry.StartPageOpenedFromNewUpdate]: never | undefined; + [Telemetry.StartPageWebViewError]: never | undefined; + [Telemetry.StartPageTime]: never | undefined; + [Telemetry.StartPageClickedDontShowAgain]: never | undefined; + [Telemetry.StartPageClosedWithoutAction]: never | undefined; + [Telemetry.StartPageUsedAnActionOnFirstTime]: never | undefined; + [Telemetry.StartPageOpenBlankNotebook]: never | undefined; + [Telemetry.StartPageOpenBlankPythonFile]: never | undefined; + [Telemetry.StartPageOpenInteractiveWindow]: never | undefined; + [Telemetry.StartPageOpenCommandPalette]: never | undefined; + [Telemetry.StartPageOpenCommandPaletteWithOpenNBSelected]: never | undefined; + [Telemetry.StartPageOpenSampleNotebook]: never | undefined; + [Telemetry.StartPageOpenFileBrowser]: never | undefined; + [Telemetry.StartPageOpenFolder]: never | undefined; + [Telemetry.StartPageOpenWorkspace]: never | undefined; + + // Run by line events + [Telemetry.RunByLineStart]: never | undefined; + [Telemetry.RunByLineStep]: never | undefined; + [Telemetry.RunByLineStop]: never | undefined; + [Telemetry.RunByLineVariableHover]: never | undefined; + + // Trusted notebooks events + [Telemetry.NotebookTrustPromptShown]: never | undefined; + [Telemetry.TrustNotebook]: never | undefined; + [Telemetry.TrustAllNotebooks]: never | undefined; + [Telemetry.DoNotTrustNotebook]: never | undefined; + + // Native notebooks events + [VSCodeNativeTelemetry.AddCell]: never | undefined; + [VSCodeNativeTelemetry.DeleteCell]: never | undefined; + [VSCodeNativeTelemetry.MoveCell]: never | undefined; + [VSCodeNativeTelemetry.ChangeToCode]: never | undefined; + [VSCodeNativeTelemetry.ChangeToMarkdown]: never | undefined; + [VSCodeNativeTelemetry.RunAllCells]: never | undefined; + [Telemetry.VSCNotebookCellTranslationFailed]: { + isErrorOutput: boolean; // Whether we're trying to translate an error output when we shuldn't be. + }; +} diff --git a/src/client/telemetry/types.ts b/src/client/telemetry/types.ts new file mode 100644 index 000000000000..cc17128c858b --- /dev/null +++ b/src/client/telemetry/types.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { IEventNamePropertyMapping } from '../telemetry/index'; +import { EventName } from './constants'; + +export type EditorLoadTelemetry = IEventNamePropertyMapping[EventName.EDITOR_LOAD]; + +export type LinterTrigger = 'auto' | 'save'; + +export type LintingTelemetry = IEventNamePropertyMapping[EventName.LINTING]; + +export type PythonInterpreterTelemetry = IEventNamePropertyMapping[EventName.PYTHON_INTERPRETER]; +export type CodeExecutionTelemetry = IEventNamePropertyMapping[EventName.EXECUTION_CODE]; +export type DebuggerTelemetry = IEventNamePropertyMapping[EventName.DEBUGGER]; +export type TestTool = 'nosetest' | 'pytest' | 'unittest'; +export type TestRunTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_RUN]; +export type TestDiscoverytTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVER]; +export type TestConfiguringTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_CONFIGURING]; +export type ImportNotebook = { + scope: 'command'; +}; +export const IImportTracker = Symbol('IImportTracker'); +export interface IImportTracker {} diff --git a/src/client/terminals/activation.ts b/src/client/terminals/activation.ts new file mode 100644 index 000000000000..b42d4b3bd4b0 --- /dev/null +++ b/src/client/terminals/activation.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Terminal } from 'vscode'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { IActiveResourceService, ICommandManager, ITerminalManager } from '../common/application/types'; +import { CODE_RUNNER_EXTENSION_ID } from '../common/constants'; +import { ITerminalActivator } from '../common/terminal/types'; +import { IDisposable, IDisposableRegistry, IExtensions } from '../common/types'; +import { noop } from '../common/utils/misc'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { ITerminalAutoActivation } from './types'; + +@injectable() +export class ExtensionActivationForTerminalActivation implements IExtensionSingleActivationService { + constructor( + @inject(ICommandManager) private commands: ICommandManager, + @inject(IExtensions) private extensions: IExtensions, + @inject(IDisposableRegistry) disposables: IDisposable[] + ) { + disposables.push(this.extensions.onDidChange(this.activate.bind(this))); + } + + public async activate(): Promise { + const isInstalled = this.isCodeRunnerInstalled(); + // Hide the play icon if code runner is installed, otherwise display the play icon. + this.commands.executeCommand('setContext', 'python.showPlayIcon', !isInstalled).then(noop, noop); + sendTelemetryEvent(EventName.PLAY_BUTTON_ICON_DISABLED, undefined, { disabled: isInstalled }); + } + + private isCodeRunnerInstalled(): boolean { + const extension = this.extensions.getExtension(CODE_RUNNER_EXTENSION_ID)!; + return extension === undefined ? false : true; + } +} + +@injectable() +export class TerminalAutoActivation implements ITerminalAutoActivation { + private handler?: IDisposable; + constructor( + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(ITerminalActivator) private readonly activator: ITerminalActivator, + @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService + ) { + disposableRegistry.push(this); + } + public dispose() { + if (this.handler) { + this.handler.dispose(); + this.handler = undefined; + } + } + public register() { + if (this.handler) { + return; + } + this.handler = this.terminalManager.onDidOpenTerminal(this.activateTerminal, this); + } + private async activateTerminal(terminal: Terminal): Promise { + if ('hideFromUser' in terminal.creationOptions && terminal.creationOptions.hideFromUser) { + return; + } + // If we have just one workspace, then pass that as the resource. + // Until upstream VSC issue is resolved https://github.com/Microsoft/vscode/issues/63052. + await this.activator.activateEnvironmentInTerminal(terminal, { + resource: this.activeResourceService.getActiveResource() + }); + } +} diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts new file mode 100644 index 000000000000..312eef37b180 --- /dev/null +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Disposable, Event, EventEmitter, Uri } from 'vscode'; + +import { ICommandManager, IDocumentManager } from '../../common/application/types'; +import { Commands } from '../../common/constants'; +import '../../common/extensions'; +import { traceError } from '../../common/logger'; +import { IFileSystem } from '../../common/platform/types'; +import { + BANNER_NAME_INTERACTIVE_SHIFTENTER, + IDisposableRegistry, + IPythonExtensionBanner, + Resource +} from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { IServiceContainer } from '../../ioc/types'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from '../../terminals/types'; + +@injectable() +export class CodeExecutionManager implements ICodeExecutionManager { + private eventEmitter: EventEmitter = new EventEmitter(); + constructor( + @inject(ICommandManager) private commandManager: ICommandManager, + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IDisposableRegistry) private disposableRegistry: Disposable[], + @inject(IFileSystem) private fileSystem: IFileSystem, + @inject(IPythonExtensionBanner) + @named(BANNER_NAME_INTERACTIVE_SHIFTENTER) + private readonly shiftEnterBanner: IPythonExtensionBanner, + @inject(IServiceContainer) private serviceContainer: IServiceContainer + ) {} + + public get onExecutedCode(): Event { + return this.eventEmitter.event; + } + + public registerCommands() { + [Commands.Exec_In_Terminal, Commands.Exec_In_Terminal_Icon].forEach((cmd) => { + this.disposableRegistry.push( + this.commandManager.registerCommand( + // tslint:disable-next-line:no-any + cmd as any, + async (file: Resource) => { + const trigger = cmd === Commands.Exec_In_Terminal ? 'command' : 'icon'; + await this.executeFileInTerminal(file, trigger).catch((ex) => + traceError('Failed to execute file in terminal', ex) + ); + } + ) + ); + }); + this.disposableRegistry.push( + this.commandManager.registerCommand( + Commands.Exec_Selection_In_Terminal, + this.executeSelectionInTerminal.bind(this) + ) + ); + this.disposableRegistry.push( + this.commandManager.registerCommand( + Commands.Exec_Selection_In_Django_Shell, + this.executeSelectionInDjangoShell.bind(this) + ) + ); + } + private async executeFileInTerminal(file: Resource, trigger: 'command' | 'icon') { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'file', trigger }); + const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); + file = file instanceof Uri ? file : undefined; + const fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); + if (!fileToExecute) { + return; + } + await codeExecutionHelper.saveFileIfDirty(fileToExecute); + + try { + const contents = await this.fileSystem.readFile(fileToExecute.fsPath); + this.eventEmitter.fire(contents); + } catch { + // Ignore any errors that occur for firing this event. It's only used + // for telemetry + noop(); + } + + const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); + await executionService.executeFile(fileToExecute); + } + + @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) + private async executeSelectionInTerminal(): Promise { + const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); + + await this.executeSelection(executionService); + // Prompt one time to ask if they want to send shift-enter to the Interactive Window + this.shiftEnterBanner.showBanner().ignoreErrors(); + } + + @captureTelemetry(EventName.EXECUTION_DJANGO, { scope: 'selection' }, false) + private async executeSelectionInDjangoShell(): Promise { + const executionService = this.serviceContainer.get(ICodeExecutionService, 'djangoShell'); + await this.executeSelection(executionService); + } + + private async executeSelection(executionService: ICodeExecutionService): Promise { + const activeEditor = this.documentManager.activeTextEditor; + if (!activeEditor) { + return; + } + const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); + const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!); + const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!); + if (!normalizedCode || normalizedCode.trim().length === 0) { + return; + } + + try { + this.eventEmitter.fire(normalizedCode); + } catch { + // Ignore any errors that occur for firing this event. It's only used + // for telemetry + noop(); + } + + await executionService.execute(normalizedCode, activeEditor!.document.uri); + } +} diff --git a/src/client/terminals/codeExecution/djangoContext.ts b/src/client/terminals/codeExecution/djangoContext.ts new file mode 100644 index 000000000000..2c3de7b79a07 --- /dev/null +++ b/src/client/terminals/codeExecution/djangoContext.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import * as path from 'path'; +import { Disposable } from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import { ContextKey } from '../../common/contextKey'; +import { traceError } from '../../common/logger'; +import { IFileSystem } from '../../common/platform/types'; + +@injectable() +export class DjangoContextInitializer implements Disposable { + private readonly isDjangoProject: ContextKey; + private monitoringActiveTextEditor: boolean = false; + private workspaceContextKeyValues = new Map(); + private lastCheckedWorkspace: string = ''; + private disposables: Disposable[] = []; + + constructor( + private documentManager: IDocumentManager, + private workpaceService: IWorkspaceService, + private fileSystem: IFileSystem, + commandManager: ICommandManager + ) { + this.isDjangoProject = new ContextKey('python.isDjangoProject', commandManager); + this.ensureContextStateIsSet().catch((ex) => traceError('Python Extension: ensureState', ex)); + this.disposables.push( + this.workpaceService.onDidChangeWorkspaceFolders(() => this.updateContextKeyBasedOnActiveWorkspace()) + ); + } + + public dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + private updateContextKeyBasedOnActiveWorkspace() { + if (this.monitoringActiveTextEditor) { + return; + } + this.monitoringActiveTextEditor = true; + this.disposables.push(this.documentManager.onDidChangeActiveTextEditor(() => this.ensureContextStateIsSet())); + } + private getActiveWorkspace(): string | undefined { + if ( + !Array.isArray(this.workpaceService.workspaceFolders) || + this.workpaceService.workspaceFolders.length === 0 + ) { + return; + } + if (this.workpaceService.workspaceFolders.length === 1) { + return this.workpaceService.workspaceFolders[0].uri.fsPath; + } + const activeEditor = this.documentManager.activeTextEditor; + if (!activeEditor) { + return; + } + const workspaceFolder = this.workpaceService.getWorkspaceFolder(activeEditor.document.uri); + return workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + } + private async ensureContextStateIsSet(): Promise { + const activeWorkspace = this.getActiveWorkspace(); + if (!activeWorkspace) { + return this.isDjangoProject.set(false); + } + if (this.lastCheckedWorkspace === activeWorkspace) { + return; + } + if (this.workspaceContextKeyValues.has(activeWorkspace)) { + await this.isDjangoProject.set(this.workspaceContextKeyValues.get(activeWorkspace)!); + } else { + const exists = await this.fileSystem.fileExists(path.join(activeWorkspace, 'manage.py')); + await this.isDjangoProject.set(exists); + this.workspaceContextKeyValues.set(activeWorkspace, exists); + this.lastCheckedWorkspace = activeWorkspace; + } + } +} diff --git a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts new file mode 100644 index 000000000000..0e96c13e1690 --- /dev/null +++ b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Disposable, Uri } from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import '../../common/extensions'; +import { IFileSystem, IPlatformService } from '../../common/platform/types'; +import { ITerminalServiceFactory } from '../../common/terminal/types'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { copyPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/exec'; +import { DjangoContextInitializer } from './djangoContext'; +import { TerminalCodeExecutionProvider } from './terminalCodeExecution'; + +@injectable() +export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvider { + constructor( + @inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, + @inject(IConfigurationService) configurationService: IConfigurationService, + @inject(IWorkspaceService) workspace: IWorkspaceService, + @inject(IDocumentManager) documentManager: IDocumentManager, + @inject(IPlatformService) platformService: IPlatformService, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(IFileSystem) fileSystem: IFileSystem, + @inject(IDisposableRegistry) disposableRegistry: Disposable[] + ) { + super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService); + this.terminalTitle = 'Django Shell'; + disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager)); + } + + public async getExecutableInfo(resource?: Uri, args: string[] = []): Promise { + const info = await super.getExecutableInfo(resource, args); + + const workspaceUri = resource ? this.workspace.getWorkspaceFolder(resource) : undefined; + const defaultWorkspace = + Array.isArray(this.workspace.workspaceFolders) && this.workspace.workspaceFolders.length > 0 + ? this.workspace.workspaceFolders[0].uri.fsPath + : ''; + const workspaceRoot = workspaceUri ? workspaceUri.uri.fsPath : defaultWorkspace; + const managePyPath = workspaceRoot.length === 0 ? 'manage.py' : path.join(workspaceRoot, 'manage.py'); + + return copyPythonExecInfo(info, [managePyPath.fileToCommandArgument(), 'shell']); + } + + public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { + // We need the executable info but not the 'manage.py shell' args + const info = await super.getExecutableInfo(resource); + return copyPythonExecInfo(info, executeArgs); + } +} diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts new file mode 100644 index 000000000000..87af541f7547 --- /dev/null +++ b/src/client/terminals/codeExecution/helper.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import '../../common/extensions'; + +import { inject, injectable } from 'inversify'; +import { Range, TextEditor, Uri } from 'vscode'; + +import { IApplicationShell, IDocumentManager } from '../../common/application/types'; +import { PYTHON_LANGUAGE } from '../../common/constants'; +import { traceError } from '../../common/logger'; +import * as internalScripts from '../../common/process/internal/scripts'; +import { IProcessServiceFactory } from '../../common/process/types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { ICodeExecutionHelper } from '../types'; + +@injectable() +export class CodeExecutionHelper implements ICodeExecutionHelper { + private readonly documentManager: IDocumentManager; + private readonly applicationShell: IApplicationShell; + private readonly processServiceFactory: IProcessServiceFactory; + private readonly interpreterService: IInterpreterService; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.documentManager = serviceContainer.get(IDocumentManager); + this.applicationShell = serviceContainer.get(IApplicationShell); + this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); + this.interpreterService = serviceContainer.get(IInterpreterService); + } + public async normalizeLines(code: string, resource?: Uri): Promise { + try { + if (code.trim().length === 0) { + return ''; + } + // On windows cr is not handled well by python when passing in/out via stdin/stdout. + // So just remove cr from the input. + code = code.replace(new RegExp('\\r', 'g'), ''); + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const processService = await this.processServiceFactory.create(resource); + const [args, parse] = internalScripts.normalizeForInterpreter(code); + const proc = await processService.exec(interpreter?.path || 'python', args, { throwOnStdErr: true }); + + return parse(proc.stdout); + } catch (ex) { + traceError(ex, 'Python: Failed to normalize code for execution in terminal'); + return code; + } + } + + public async getFileToExecute(): Promise { + const activeEditor = this.documentManager.activeTextEditor!; + if (!activeEditor) { + this.applicationShell.showErrorMessage('No open file to run in terminal'); + return; + } + if (activeEditor.document.isUntitled) { + this.applicationShell.showErrorMessage('The active file needs to be saved before it can be run'); + return; + } + if (activeEditor.document.languageId !== PYTHON_LANGUAGE) { + this.applicationShell.showErrorMessage('The active file is not a Python source file'); + return; + } + if (activeEditor.document.isDirty) { + await activeEditor.document.save(); + } + return activeEditor.document.uri; + } + + public async getSelectedTextToExecute(textEditor: TextEditor): Promise { + if (!textEditor) { + return; + } + + const selection = textEditor.selection; + let code: string; + if (selection.isEmpty) { + code = textEditor.document.lineAt(selection.start.line).text; + } else { + const textRange = new Range(selection.start, selection.end); + code = textEditor.document.getText(textRange); + } + return code; + } + public async saveFileIfDirty(file: Uri): Promise { + const docs = this.documentManager.textDocuments.filter((d) => d.uri.path === file.path); + if (docs.length === 1 && docs[0].isDirty) { + await docs[0].save(); + } + } +} diff --git a/src/client/terminals/codeExecution/repl.ts b/src/client/terminals/codeExecution/repl.ts new file mode 100644 index 000000000000..f3c620e83b75 --- /dev/null +++ b/src/client/terminals/codeExecution/repl.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { IPlatformService } from '../../common/platform/types'; +import { ITerminalServiceFactory } from '../../common/terminal/types'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { TerminalCodeExecutionProvider } from './terminalCodeExecution'; + +@injectable() +export class ReplProvider extends TerminalCodeExecutionProvider { + constructor( + @inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, + @inject(IConfigurationService) configurationService: IConfigurationService, + @inject(IWorkspaceService) workspace: IWorkspaceService, + @inject(IDisposableRegistry) disposableRegistry: Disposable[], + @inject(IPlatformService) platformService: IPlatformService + ) { + super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService); + this.terminalTitle = 'REPL'; + } +} diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts new file mode 100644 index 000000000000..4c5b8034129d --- /dev/null +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Disposable, Uri } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import '../../common/extensions'; +import { IPlatformService } from '../../common/platform/types'; +import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { buildPythonExecInfo, PythonExecInfo } from '../../pythonEnvironments/exec'; +import { ICodeExecutionService } from '../../terminals/types'; + +@injectable() +export class TerminalCodeExecutionProvider implements ICodeExecutionService { + protected terminalTitle!: string; + private _terminalService!: ITerminalService; + private replActive?: Promise; + constructor( + @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, + @inject(IConfigurationService) protected readonly configurationService: IConfigurationService, + @inject(IWorkspaceService) protected readonly workspace: IWorkspaceService, + @inject(IDisposableRegistry) protected readonly disposables: Disposable[], + @inject(IPlatformService) protected readonly platformService: IPlatformService + ) {} + + public async executeFile(file: Uri) { + await this.setCwdForFileExecution(file); + const { command, args } = await this.getExecuteFileArgs(file, [file.fsPath.fileToCommandArgument()]); + + await this.getTerminalService(file).sendCommand(command, args); + } + + public async execute(code: string, resource?: Uri): Promise { + if (!code || code.trim().length === 0) { + return; + } + + await this.initializeRepl(); + await this.getTerminalService(resource).sendText(code); + } + public async initializeRepl(resource?: Uri) { + if (this.replActive && (await this.replActive!)) { + await this._terminalService!.show(); + return; + } + this.replActive = new Promise(async (resolve) => { + const replCommandArgs = await this.getExecutableInfo(resource); + await this.getTerminalService(resource).sendCommand(replCommandArgs.command, replCommandArgs.args); + + // Give python repl time to start before we start sending text. + setTimeout(() => resolve(true), 1000); + }); + + await this.replActive; + } + + public async getExecutableInfo(resource?: Uri, args: string[] = []): Promise { + const pythonSettings = this.configurationService.getSettings(resource); + const command = this.platformService.isWindows + ? pythonSettings.pythonPath.replace(/\\/g, '/') + : pythonSettings.pythonPath; + const launchArgs = pythonSettings.terminal.launchArgs; + return buildPythonExecInfo(command, [...launchArgs, ...args]); + } + + // Overridden in subclasses, see djangoShellCodeExecution.ts + public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { + return this.getExecutableInfo(resource, executeArgs); + } + private getTerminalService(resource?: Uri): ITerminalService { + if (!this._terminalService) { + this._terminalService = this.terminalServiceFactory.getTerminalService(resource, this.terminalTitle); + this.disposables.push( + this._terminalService.onDidCloseTerminal(() => { + this.replActive = undefined; + }) + ); + } + return this._terminalService; + } + private async setCwdForFileExecution(file: Uri) { + const pythonSettings = this.configurationService.getSettings(file); + if (!pythonSettings.terminal.executeInFileDir) { + return; + } + const fileDirPath = path.dirname(file.fsPath); + const wkspace = this.workspace.getWorkspaceFolder(file); + if ((!wkspace || fileDirPath !== wkspace.uri.fsPath) && fileDirPath.length > 0) { + await this.getTerminalService(file).sendText(`cd ${fileDirPath.fileToCommandArgument()}`); + } + } +} diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts new file mode 100644 index 000000000000..619c69eab7c0 --- /dev/null +++ b/src/client/terminals/serviceRegistry.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { interfaces } from 'inversify'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { ClassType } from '../ioc/types'; +import { ExtensionActivationForTerminalActivation, TerminalAutoActivation } from './activation'; +import { CodeExecutionManager } from './codeExecution/codeExecutionManager'; +import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution'; +import { CodeExecutionHelper } from './codeExecution/helper'; +import { ReplProvider } from './codeExecution/repl'; +import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution'; +import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation } from './types'; + +interface IServiceRegistry { + addSingleton( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol + ): void; +} + +export function registerTypes(serviceManager: IServiceRegistry) { + serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); + + serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); + + serviceManager.addSingleton( + ICodeExecutionService, + DjangoShellCodeExecutionProvider, + 'djangoShell' + ); + serviceManager.addSingleton( + ICodeExecutionService, + TerminalCodeExecutionProvider, + 'standard' + ); + serviceManager.addSingleton(ICodeExecutionService, ReplProvider, 'repl'); + + serviceManager.addSingleton(ITerminalAutoActivation, TerminalAutoActivation); + + serviceManager.addSingleton( + IExtensionSingleActivationService, + ExtensionActivationForTerminalActivation + ); +} diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts new file mode 100644 index 000000000000..b096153a3db8 --- /dev/null +++ b/src/client/terminals/types.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event, TextEditor, Uri } from 'vscode'; +import { IDisposable } from '../common/types'; + +export const ICodeExecutionService = Symbol('ICodeExecutionService'); + +export interface ICodeExecutionService { + execute(code: string, resource?: Uri): Promise; + executeFile(file: Uri): Promise; + initializeRepl(resource?: Uri): Promise; +} + +export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); + +export interface ICodeExecutionHelper { + normalizeLines(code: string): Promise; + getFileToExecute(): Promise; + saveFileIfDirty(file: Uri): Promise; + getSelectedTextToExecute(textEditor: TextEditor): Promise; +} + +export const ICodeExecutionManager = Symbol('ICodeExecutionManager'); + +export interface ICodeExecutionManager { + onExecutedCode: Event; + registerCommands(): void; +} + +export const ITerminalAutoActivation = Symbol('ITerminalAutoActivation'); +export interface ITerminalAutoActivation extends IDisposable { + register(): void; +} diff --git a/src/client/testing/codeLenses/main.ts b/src/client/testing/codeLenses/main.ts new file mode 100644 index 000000000000..2179c10d2ec8 --- /dev/null +++ b/src/client/testing/codeLenses/main.ts @@ -0,0 +1,27 @@ +import * as vscode from 'vscode'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { PYTHON } from '../../common/constants'; +import { ITestCollectionStorageService } from '../common/types'; +import { TestFileCodeLensProvider } from './testFiles'; + +export function activateCodeLenses( + onDidChange: vscode.EventEmitter, + symbolProvider: vscode.DocumentSymbolProvider, + testCollectionStorage: ITestCollectionStorageService, + serviceContainer: IServiceContainer +): vscode.Disposable { + const disposables: vscode.Disposable[] = []; + const codeLensProvider = new TestFileCodeLensProvider( + onDidChange, + symbolProvider, + testCollectionStorage, + serviceContainer + ); + disposables.push(vscode.languages.registerCodeLensProvider(PYTHON, codeLensProvider)); + + return { + dispose: () => { + disposables.forEach((d) => d.dispose()); + } + }; +} diff --git a/src/client/testing/codeLenses/testFiles.ts b/src/client/testing/codeLenses/testFiles.ts new file mode 100644 index 000000000000..269f7f7d4539 --- /dev/null +++ b/src/client/testing/codeLenses/testFiles.ts @@ -0,0 +1,315 @@ +'use strict'; + +// tslint:disable:no-object-literal-type-assertion + +import { + CancellationToken, + CancellationTokenSource, + CodeLens, + CodeLensProvider, + DocumentSymbolProvider, + Event, + EventEmitter, + Position, + Range, + SymbolInformation, + SymbolKind, + TextDocument, + Uri +} from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import * as constants from '../../common/constants'; +import { CommandSource } from '../common/constants'; +import { + ITestCollectionStorageService, + TestFile, + TestFunction, + TestStatus, + TestsToRun, + TestSuite +} from '../common/types'; + +type FunctionsAndSuites = { + functions: TestFunction[]; + suites: TestSuite[]; +}; + +export class TestFileCodeLensProvider implements CodeLensProvider { + private workspaceService: IWorkspaceService; + private fileSystem: IFileSystem; + // tslint:disable-next-line:variable-name + constructor( + private _onDidChange: EventEmitter, + private symbolProvider: DocumentSymbolProvider, + private testCollectionStorage: ITestCollectionStorageService, + serviceContainer: IServiceContainer + ) { + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.fileSystem = serviceContainer.get(IFileSystem); + } + + get onDidChangeCodeLenses(): Event { + return this._onDidChange.event; + } + + public async provideCodeLenses(document: TextDocument, token: CancellationToken) { + const wkspace = this.workspaceService.getWorkspaceFolder(document.uri); + if (!wkspace) { + return []; + } + const testItems = this.testCollectionStorage.getTests(wkspace.uri); + if (!testItems || testItems.testFiles.length === 0 || testItems.testFunctions.length === 0) { + return []; + } + + const cancelTokenSrc = new CancellationTokenSource(); + token.onCancellationRequested(() => { + cancelTokenSrc.cancel(); + }); + + // Strop trying to build the code lenses if unable to get a list of + // symbols in this file afrer x time. + setTimeout(() => { + if (!cancelTokenSrc.token.isCancellationRequested) { + cancelTokenSrc.cancel(); + } + }, constants.Delays.MaxUnitTestCodeLensDelay); + + return this.getCodeLenses(document, cancelTokenSrc.token, this.symbolProvider); + } + + public resolveCodeLens(codeLens: CodeLens, _token: CancellationToken): CodeLens | Thenable { + codeLens.command = { command: 'python.runtests', title: 'Test' }; + return Promise.resolve(codeLens); + } + + public getTestFileWhichNeedsCodeLens(document: TextDocument): TestFile | undefined { + const wkspace = this.workspaceService.getWorkspaceFolder(document.uri); + if (!wkspace) { + return; + } + const tests = this.testCollectionStorage.getTests(wkspace.uri); + if (!tests) { + return; + } + return tests.testFiles.find((item) => this.fileSystem.arePathsSame(item.fullPath, document.uri.fsPath)); + } + + private async getCodeLenses( + document: TextDocument, + token: CancellationToken, + symbolProvider: DocumentSymbolProvider + ) { + const file = this.getTestFileWhichNeedsCodeLens(document); + if (!file) { + return []; + } + const allFuncsAndSuites = getAllTestSuitesAndFunctionsPerFile(file); + + try { + const symbols = (await symbolProvider.provideDocumentSymbols(document, token)) as SymbolInformation[]; + if (!symbols) { + return []; + } + return symbols + .filter( + (symbol) => + symbol.kind === SymbolKind.Function || + symbol.kind === SymbolKind.Method || + symbol.kind === SymbolKind.Class + ) + .map((symbol) => { + // This is bloody crucial, if the start and end columns are the same + // then vscode goes bonkers when ever you edit a line (start scrolling magically). + const range = new Range( + symbol.location.range.start, + new Position(symbol.location.range.end.line, symbol.location.range.end.character + 1) + ); + + return this.getCodeLens( + document.uri, + allFuncsAndSuites, + range, + symbol.name, + symbol.kind, + symbol.containerName + ); + }) + .reduce((previous, current) => previous.concat(current), []) + .filter((codeLens) => codeLens !== null); + } catch (reason) { + if (token.isCancellationRequested) { + return []; + } + return Promise.reject(reason); + } + } + + private getCodeLens( + file: Uri, + allFuncsAndSuites: FunctionsAndSuites, + range: Range, + symbolName: string, + symbolKind: SymbolKind, + symbolContainer: string + ): CodeLens[] { + switch (symbolKind) { + case SymbolKind.Function: + case SymbolKind.Method: { + return getFunctionCodeLens(file, allFuncsAndSuites, symbolName, range, symbolContainer); + } + case SymbolKind.Class: { + const cls = allFuncsAndSuites.suites.find((item) => item.name === symbolName); + if (!cls) { + return []; + } + return [ + new CodeLens(range, { + title: getTestStatusIcon(cls.status) + constants.Text.CodeLensRunUnitTest, + command: constants.Commands.Tests_Run, + arguments: [undefined, CommandSource.codelens, file, { testSuite: [cls] }] + }), + new CodeLens(range, { + title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, + command: constants.Commands.Tests_Debug, + arguments: [undefined, CommandSource.codelens, file, { testSuite: [cls] }] + }) + ]; + } + default: { + return []; + } + } + } +} + +function getTestStatusIcon(status?: TestStatus): string { + switch (status) { + case TestStatus.Pass: { + return `${constants.Octicons.Test_Pass} `; + } + case TestStatus.Error: { + return `${constants.Octicons.Test_Error} `; + } + case TestStatus.Fail: { + return `${constants.Octicons.Test_Fail} `; + } + case TestStatus.Skipped: { + return `${constants.Octicons.Test_Skip} `; + } + default: { + return ''; + } + } +} + +function getTestStatusIcons(fns: TestFunction[]): string { + const statuses: string[] = []; + let count = fns.filter((fn) => fn.status === TestStatus.Pass).length; + if (count > 0) { + statuses.push(`${constants.Octicons.Test_Pass} ${count}`); + } + count = fns.filter((fn) => fn.status === TestStatus.Skipped).length; + if (count > 0) { + statuses.push(`${constants.Octicons.Test_Skip} ${count}`); + } + count = fns.filter((fn) => fn.status === TestStatus.Fail).length; + if (count > 0) { + statuses.push(`${constants.Octicons.Test_Fail} ${count}`); + } + count = fns.filter((fn) => fn.status === TestStatus.Error).length; + if (count > 0) { + statuses.push(`${constants.Octicons.Test_Error} ${count}`); + } + + return statuses.join(' '); +} +function getFunctionCodeLens( + file: Uri, + functionsAndSuites: FunctionsAndSuites, + symbolName: string, + range: Range, + symbolContainer: string +): CodeLens[] { + let fn: TestFunction | undefined; + if (symbolContainer.length === 0) { + fn = functionsAndSuites.functions.find((func) => func.name === symbolName); + } else { + // Assume single levels for now. + functionsAndSuites.suites + .filter((s) => s.name === symbolContainer) + .forEach((s) => { + const f = s.functions.find((item) => item.name === symbolName); + if (f) { + fn = f; + } + }); + } + + if (fn) { + return [ + new CodeLens(range, { + title: getTestStatusIcon(fn.status) + constants.Text.CodeLensRunUnitTest, + command: constants.Commands.Tests_Run, + arguments: [undefined, CommandSource.codelens, file, { testFunction: [fn] }] + }), + new CodeLens(range, { + title: getTestStatusIcon(fn.status) + constants.Text.CodeLensDebugUnitTest, + command: constants.Commands.Tests_Debug, + arguments: [undefined, CommandSource.codelens, file, { testFunction: [fn] }] + }) + ]; + } + + // Ok, possible we're dealing with parameterized unit tests. + // If we have [ in the name, then this is a parameterized function. + const functions = functionsAndSuites.functions.filter( + (func) => func.name.startsWith(`${symbolName}[`) && func.name.endsWith(']') + ); + if (functions.length === 0) { + return []; + } + + // Find all flattened functions. + return [ + new CodeLens(range, { + title: `${getTestStatusIcons(functions)} ${constants.Text.CodeLensRunUnitTest} (Multiple)`, + command: constants.Commands.Tests_Picker_UI, + arguments: [undefined, CommandSource.codelens, file, functions] + }), + new CodeLens(range, { + title: `${getTestStatusIcons(functions)} ${constants.Text.CodeLensDebugUnitTest} (Multiple)`, + command: constants.Commands.Tests_Picker_UI_Debug, + arguments: [undefined, CommandSource.codelens, file, functions] + }) + ]; +} + +function getAllTestSuitesAndFunctionsPerFile(testFile: TestFile): FunctionsAndSuites { + // tslint:disable-next-line:prefer-type-cast + const all = { functions: [...testFile.functions], suites: [] as TestSuite[] }; + testFile.suites.forEach((suite) => { + all.suites.push(suite); + + const allChildItems = getAllTestSuitesAndFunctions(suite); + all.functions.push(...allChildItems.functions); + all.suites.push(...allChildItems.suites); + }); + return all; +} +function getAllTestSuitesAndFunctions(testSuite: TestSuite): FunctionsAndSuites { + const all: { functions: TestFunction[]; suites: TestSuite[] } = { functions: [], suites: [] }; + testSuite.functions.forEach((fn) => { + all.functions.push(fn); + }); + testSuite.suites.forEach((suite) => { + all.suites.push(suite); + + const allChildItems = getAllTestSuitesAndFunctions(suite); + all.functions.push(...allChildItems.functions); + all.suites.push(...allChildItems.suites); + }); + return all; +} diff --git a/src/client/testing/common/argumentsHelper.ts b/src/client/testing/common/argumentsHelper.ts new file mode 100644 index 000000000000..2633f99ab50c --- /dev/null +++ b/src/client/testing/common/argumentsHelper.ts @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { traceWarning } from '../../common/logger'; +import { IArgumentsHelper } from '../types'; + +@injectable() +export class ArgumentsHelper implements IArgumentsHelper { + public getOptionValues(args: string[], option: string): string | string[] | undefined { + const values: string[] = []; + let returnNextValue = false; + for (const arg of args) { + if (returnNextValue) { + values.push(arg); + returnNextValue = false; + continue; + } + if (arg.startsWith(`${option}=`)) { + values.push(arg.substring(`${option}=`.length)); + continue; + } + if (arg === option) { + returnNextValue = true; + } + } + switch (values.length) { + case 0: { + return; + } + case 1: { + return values[0]; + } + default: { + return values; + } + } + } + public getPositionalArguments( + args: string[], + optionsWithArguments: string[] = [], + optionsWithoutArguments: string[] = [] + ): string[] { + const nonPositionalIndexes: number[] = []; + args.forEach((arg, index) => { + if (optionsWithoutArguments.indexOf(arg) !== -1) { + nonPositionalIndexes.push(index); + return; + } else if (optionsWithArguments.indexOf(arg) !== -1) { + nonPositionalIndexes.push(index); + // Cuz the next item is the value. + nonPositionalIndexes.push(index + 1); + } else if (optionsWithArguments.findIndex((item) => arg.startsWith(`${item}=`)) !== -1) { + nonPositionalIndexes.push(index); + return; + } else if (arg.startsWith('-')) { + // Ok this is an unknown option, lets treat this as one without values. + traceWarning( + `Unknown command line option passed into args parser for tests '${arg}'. Please report on https://github.com/Microsoft/vscode-python/issues/new` + ); + nonPositionalIndexes.push(index); + return; + } else if (arg.indexOf('=') > 0) { + // Ok this is an unknown option with a value + traceWarning( + `Unknown command line option passed into args parser for tests '${arg}'. Please report on https://github.com/Microsoft/vscode-python/issues/new` + ); + nonPositionalIndexes.push(index); + } + }); + return args.filter((_, index) => nonPositionalIndexes.indexOf(index) === -1); + } + public filterArguments( + args: string[], + optionsWithArguments: string[] = [], + optionsWithoutArguments: string[] = [] + ): string[] { + let ignoreIndex = -1; + return args.filter((arg, index) => { + if (ignoreIndex === index) { + return false; + } + // Options can use willd cards (with trailing '*') + if ( + optionsWithoutArguments.indexOf(arg) >= 0 || + optionsWithoutArguments.filter((option) => option.endsWith('*') && arg.startsWith(option.slice(0, -1))) + .length > 0 + ) { + return false; + } + // Ignore args that match exactly. + if (optionsWithArguments.indexOf(arg) >= 0) { + ignoreIndex = index + 1; + return false; + } + // Ignore args that match exactly with wild cards & do not have inline values. + if (optionsWithArguments.filter((option) => arg.startsWith(`${option}=`)).length > 0) { + return false; + } + // Ignore args that match a wild card (ending with *) and no ineline values. + // Eg. arg='--log-cli-level' and optionsArguments=['--log-*'] + if ( + arg.indexOf('=') === -1 && + optionsWithoutArguments.filter((option) => option.endsWith('*') && arg.startsWith(option.slice(0, -1))) + .length > 0 + ) { + ignoreIndex = index + 1; + return false; + } + // Ignore args that match a wild card (ending with *) and have ineline values. + // Eg. arg='--log-cli-level=XYZ' and optionsArguments=['--log-*'] + if ( + arg.indexOf('=') >= 0 && + optionsWithoutArguments.filter((option) => option.endsWith('*') && arg.startsWith(option.slice(0, -1))) + .length > 0 + ) { + return false; + } + return true; + }); + } +} diff --git a/src/client/testing/common/constants.ts b/src/client/testing/common/constants.ts new file mode 100644 index 000000000000..002b250d58c5 --- /dev/null +++ b/src/client/testing/common/constants.ts @@ -0,0 +1,24 @@ +import { Product } from '../../common/types'; +import { TestProvider, UnitTestProduct } from './types'; + +export const CANCELLATION_REASON = 'cancelled_user_request'; +export enum CommandSource { + auto = 'auto', + ui = 'ui', + codelens = 'codelens', + commandPalette = 'commandpalette', + testExplorer = 'testExplorer' +} +export const TEST_OUTPUT_CHANNEL = 'TEST_OUTPUT_CHANNEL'; + +export const UNIT_TEST_PRODUCTS: UnitTestProduct[] = [Product.pytest, Product.unittest, Product.nosetest]; +export const NOSETEST_PROVIDER: TestProvider = 'nosetest'; +export const PYTEST_PROVIDER: TestProvider = 'pytest'; +export const UNITTEST_PROVIDER: TestProvider = 'unittest'; + +export enum Icons { + discovering = 'discovering-tests.svg', + passed = 'status-ok.svg', + failed = 'status-error.svg', + unknown = 'status-unknown.svg' +} diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts new file mode 100644 index 000000000000..969c02e19eb7 --- /dev/null +++ b/src/client/testing/common/debugLauncher.ts @@ -0,0 +1,202 @@ +import { inject, injectable, named } from 'inversify'; +import { parse } from 'jsonc-parser'; +import * as path from 'path'; +import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; +import { IApplicationShell, IDebugService, IWorkspaceService } from '../../common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../common/constants'; +import { traceError } from '../../common/logger'; +import { IFileSystem } from '../../common/platform/types'; +import * as internalScripts from '../../common/process/internal/scripts'; +import { IConfigurationService, IPythonSettings } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { DebuggerTypeName } from '../../debugger/constants'; +import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/types'; +import { LaunchRequestArguments } from '../../debugger/types'; +import { IServiceContainer } from '../../ioc/types'; +import { ITestDebugConfig, ITestDebugLauncher, LaunchOptions, TestProvider } from './types'; + +@injectable() +export class DebugLauncher implements ITestDebugLauncher { + private readonly configService: IConfigurationService; + private readonly workspaceService: IWorkspaceService; + private readonly fs: IFileSystem; + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IDebugConfigurationResolver) + @named('launch') + private readonly launchResolver: IDebugConfigurationResolver + ) { + this.configService = this.serviceContainer.get(IConfigurationService); + this.workspaceService = this.serviceContainer.get(IWorkspaceService); + this.fs = this.serviceContainer.get(IFileSystem); + } + + public async launchDebugger(options: LaunchOptions) { + if (options.token && options.token!.isCancellationRequested) { + return; + } + + const workspaceFolder = this.resolveWorkspaceFolder(options.cwd); + const launchArgs = await this.getLaunchArgs( + options, + workspaceFolder, + this.configService.getSettings(workspaceFolder.uri) + ); + const debugManager = this.serviceContainer.get(IDebugService); + return debugManager + .startDebugging(workspaceFolder, launchArgs) + .then(noop, (ex) => traceError('Failed to start debugging tests', ex)); + } + public async readAllDebugConfigs(workspaceFolder: WorkspaceFolder): Promise { + const filename = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); + if (!(await this.fs.fileExists(filename))) { + return []; + } + try { + const text = await this.fs.readFile(filename); + const parsed = parse(text, [], { allowTrailingComma: true, disallowComments: false }); + if (!parsed.version || !parsed.configurations || !Array.isArray(parsed.configurations)) { + throw Error('malformed launch.json'); + } + // We do not bother ensuring each item is a DebugConfiguration... + return parsed.configurations; + } catch (exc) { + traceError('could not get debug config', exc); + const appShell = this.serviceContainer.get(IApplicationShell); + await appShell.showErrorMessage('Could not load unit test config from launch.json'); + return []; + } + } + private resolveWorkspaceFolder(cwd: string): WorkspaceFolder { + if (!this.workspaceService.hasWorkspaceFolders) { + throw new Error('Please open a workspace'); + } + + const cwdUri = cwd ? Uri.file(cwd) : undefined; + let workspaceFolder = this.workspaceService.getWorkspaceFolder(cwdUri); + if (!workspaceFolder) { + workspaceFolder = this.workspaceService.workspaceFolders![0]; + } + return workspaceFolder; + } + + private async getLaunchArgs( + options: LaunchOptions, + workspaceFolder: WorkspaceFolder, + configSettings: IPythonSettings + ): Promise { + let debugConfig = await this.readDebugConfig(workspaceFolder); + if (!debugConfig) { + debugConfig = { + name: 'Debug Unit Test', + type: 'python', + request: 'test', + subProcess: true + }; + } + if (!debugConfig.rules) { + debugConfig.rules = []; + } + debugConfig.rules.push({ + path: path.join(EXTENSION_ROOT_DIR, 'pythonFiles'), + include: false + }); + this.applyDefaults(debugConfig!, workspaceFolder, configSettings); + + return this.convertConfigToArgs(debugConfig!, workspaceFolder, options); + } + + private async readDebugConfig(workspaceFolder: WorkspaceFolder): Promise { + const configs = await this.readAllDebugConfigs(workspaceFolder); + for (const cfg of configs) { + if (!cfg.name || cfg.type !== DebuggerTypeName || cfg.request !== 'test') { + continue; + } + // Return the first one. + return cfg as ITestDebugConfig; + } + return undefined; + } + private applyDefaults(cfg: ITestDebugConfig, workspaceFolder: WorkspaceFolder, configSettings: IPythonSettings) { + // cfg.pythonPath is handled by LaunchConfigurationResolver. + + // Default value of justMyCode is not provided intentionally, for now we derive its value required for launchArgs using debugStdLib + // Have to provide it if and when we remove complete support for debugStdLib + if (!cfg.console) { + cfg.console = 'internalConsole'; + } + if (!cfg.cwd) { + cfg.cwd = workspaceFolder.uri.fsPath; + } + if (!cfg.env) { + cfg.env = {}; + } + if (!cfg.envFile) { + cfg.envFile = configSettings.envFile; + } + + if (cfg.stopOnEntry === undefined) { + cfg.stopOnEntry = false; + } + cfg.showReturnValue = cfg.showReturnValue !== false; + if (cfg.redirectOutput === undefined) { + cfg.redirectOutput = true; + } + if (cfg.debugStdLib === undefined) { + cfg.debugStdLib = false; + } + if (cfg.subProcess === undefined) { + cfg.subProcess = true; + } + } + + private async convertConfigToArgs( + debugConfig: ITestDebugConfig, + workspaceFolder: WorkspaceFolder, + options: LaunchOptions + ): Promise { + const configArgs = debugConfig as LaunchRequestArguments; + + const testArgs = this.fixArgs(options.args, options.testProvider); + const script = this.getTestLauncherScript(options.testProvider); + const args = script(testArgs); + configArgs.program = args[0]; + configArgs.args = args.slice(1); + // We leave configArgs.request as "test" so it will be sent in telemetry. + + const launchArgs = await this.launchResolver.resolveDebugConfiguration( + workspaceFolder, + configArgs, + options.token + ); + if (!launchArgs) { + throw Error(`Invalid debug config "${debugConfig.name}"`); + } + launchArgs.request = 'launch'; + + return launchArgs!; + } + + private fixArgs(args: string[], testProvider: TestProvider): string[] { + if (testProvider === 'unittest') { + return args.filter((item) => item !== '--debug'); + } else { + return args; + } + } + + private getTestLauncherScript(testProvider: TestProvider) { + switch (testProvider) { + case 'unittest': { + return internalScripts.visualstudio_py_testlauncher; + } + case 'pytest': + case 'nosetest': { + return internalScripts.testlauncher; + } + default: { + throw new Error(`Unknown test provider '${testProvider}'`); + } + } + } +} diff --git a/src/client/testing/common/enablementTracker.ts b/src/client/testing/common/enablementTracker.ts new file mode 100644 index 000000000000..fe6b319873ec --- /dev/null +++ b/src/client/testing/common/enablementTracker.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IWorkspaceService } from '../../common/application/types'; +import { IDisposableRegistry, Resource } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { ITestConfigSettingsService } from '../types'; +import { ITestsHelper, TestProvider } from './types'; + +@injectable() +export class EnablementTracker implements IExtensionSingleActivationService { + constructor( + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(ITestConfigSettingsService) private readonly testConfig: ITestConfigSettingsService, + @inject(ITestsHelper) private readonly testsHelper: ITestsHelper + ) {} + public async activate(): Promise { + this.disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration, this)); + } + public onDidChangeConfiguration(args: ConfigurationChangeEvent) { + const resourcesToCheck: Resource[] = [undefined]; + if (Array.isArray(this.workspaceService.workspaceFolders)) { + this.workspaceService.workspaceFolders.forEach((item) => resourcesToCheck.push(item.uri)); + } + + const testProviders: TestProvider[] = ['nosetest', 'pytest', 'unittest']; + resourcesToCheck.forEach((resource) => { + const telemetry: Partial> = {}; + testProviders.forEach((item) => { + const product = this.testsHelper.parseProduct(item); + const testingSetting = this.testConfig.getTestEnablingSetting(product); + const settingToCheck = `python.${testingSetting}`; + // If the setting was modified and if its value is true, then track this. + if ( + args.affectsConfiguration(settingToCheck) && + this.workspaceService.getConfiguration('python', resource).get(testingSetting, false) + ) { + telemetry[item] = true; + } + }); + // If anyone of the items have been enabled, then send telemetry. + if (telemetry.nosetest || telemetry.pytest || telemetry.unittest) { + this.sendTelemetry(telemetry); + } + }); + } + public sendTelemetry(telemetry: Partial>) { + sendTelemetryEvent(EventName.UNITTEST_ENABLED, undefined, telemetry); + } +} diff --git a/src/client/testing/common/managers/baseTestManager.ts b/src/client/testing/common/managers/baseTestManager.ts new file mode 100644 index 000000000000..ded6581f62cf --- /dev/null +++ b/src/client/testing/common/managers/baseTestManager.ts @@ -0,0 +1,516 @@ +import { + CancellationToken, + CancellationTokenSource, + Diagnostic, + DiagnosticCollection, + DiagnosticRelatedInformation, + Disposable, + Event, + EventEmitter, + languages, + OutputChannel, + Uri +} from 'vscode'; +import { ICommandManager, IWorkspaceService } from '../../../common/application/types'; +import '../../../common/extensions'; +import { isNotInstalledError } from '../../../common/helpers'; +import { traceError } from '../../../common/logger'; +import { IFileSystem } from '../../../common/platform/types'; +import { + IConfigurationService, + IDisposableRegistry, + IInstaller, + IOutputChannel, + IPythonSettings, + Product +} from '../../../common/types'; +import { getNamesAndValues } from '../../../common/utils/enum'; +import { noop } from '../../../common/utils/misc'; +import { IServiceContainer } from '../../../ioc/types'; +import { EventName } from '../../../telemetry/constants'; +import { sendTelemetryEvent } from '../../../telemetry/index'; +import { TestDiscoverytTelemetry, TestRunTelemetry } from '../../../telemetry/types'; +import { IPythonTestMessage, ITestDiagnosticService, WorkspaceTestStatus } from '../../types'; +import { copyDesiredTestResults } from '../testUtils'; +import { CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL } from './../constants'; +import { + ITestCollectionStorageService, + ITestDiscoveryService, + ITestManager, + ITestResultsService, + ITestsHelper, + ITestsStatusUpdaterService, + TestDiscoveryOptions, + TestProvider, + Tests, + TestStatus, + TestsToRun +} from './../types'; + +enum CancellationTokenType { + testDiscovery, + testRunner +} + +// tslint:disable: member-ordering max-func-body-length + +export abstract class BaseTestManager implements ITestManager { + public diagnosticCollection: DiagnosticCollection; + protected readonly settings: IPythonSettings; + private readonly unitTestDiagnosticService: ITestDiagnosticService; + public abstract get enabled(): boolean; + protected get outputChannel() { + return this._outputChannel; + } + protected get testResultsService() { + return this._testResultsService; + } + private readonly testCollectionStorage: ITestCollectionStorageService; + private readonly _testResultsService: ITestResultsService; + private readonly commandManager: ICommandManager; + private readonly workspaceService: IWorkspaceService; + private readonly _outputChannel: OutputChannel; + protected tests?: Tests; + private _status: TestStatus = TestStatus.Unknown; + private testDiscoveryCancellationTokenSource?: CancellationTokenSource; + private testRunnerCancellationTokenSource?: CancellationTokenSource; + private _installer!: IInstaller; + private readonly testsStatusUpdaterService: ITestsStatusUpdaterService; + private discoverTestsPromise?: Promise; + private readonly _onDidStatusChange = new EventEmitter(); + private get installer(): IInstaller { + if (!this._installer) { + this._installer = this.serviceContainer.get(IInstaller); + } + return this._installer; + } + constructor( + public readonly testProvider: TestProvider, + private readonly product: Product, + public readonly workspaceFolder: Uri, + protected rootDirectory: string, + protected serviceContainer: IServiceContainer + ) { + this.updateStatus(TestStatus.Unknown); + const configService = serviceContainer.get(IConfigurationService); + this.settings = configService.getSettings(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined); + const disposables = serviceContainer.get(IDisposableRegistry); + this._outputChannel = this.serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); + this.testCollectionStorage = this.serviceContainer.get( + ITestCollectionStorageService + ); + this._testResultsService = this.serviceContainer.get(ITestResultsService); + this.workspaceService = this.serviceContainer.get(IWorkspaceService); + this.diagnosticCollection = languages.createDiagnosticCollection(this.testProvider); + this.unitTestDiagnosticService = serviceContainer.get(ITestDiagnosticService); + this.testsStatusUpdaterService = serviceContainer.get(ITestsStatusUpdaterService); + this.commandManager = serviceContainer.get(ICommandManager); + disposables.push(this); + } + protected get testDiscoveryCancellationToken(): CancellationToken | undefined { + return this.testDiscoveryCancellationTokenSource ? this.testDiscoveryCancellationTokenSource.token : undefined; + } + protected get testRunnerCancellationToken(): CancellationToken | undefined { + return this.testRunnerCancellationTokenSource ? this.testRunnerCancellationTokenSource.token : undefined; + } + public dispose() { + this.stop(); + } + public get status(): TestStatus { + return this._status; + } + public get onDidStatusChange(): Event { + return this._onDidStatusChange.event; + } + public get workingDirectory(): string { + return this.settings.testing.cwd && this.settings.testing.cwd.length > 0 + ? this.settings.testing.cwd + : this.rootDirectory; + } + public stop() { + if (this.testDiscoveryCancellationTokenSource) { + this.testDiscoveryCancellationTokenSource.cancel(); + } + if (this.testRunnerCancellationTokenSource) { + this.testRunnerCancellationTokenSource.cancel(); + } + } + public reset() { + this.tests = undefined; + this.updateStatus(TestStatus.Unknown); + } + public resetTestResults() { + if (!this.tests) { + return; + } + + this.testResultsService.resetResults(this.tests!); + } + public async discoverTests( + cmdSource: CommandSource, + ignoreCache: boolean = false, + quietMode: boolean = false, + userInitiated: boolean = false, + clearTestStatus: boolean = false + ): Promise { + if (this.discoverTestsPromise) { + return this.discoverTestsPromise; + } + this.discoverTestsPromise = this._discoverTests( + cmdSource, + ignoreCache, + quietMode, + userInitiated, + clearTestStatus + ); + this.discoverTestsPromise + .catch(noop) + .then(() => (this.discoverTestsPromise = undefined)) + .ignoreErrors(); + return this.discoverTestsPromise; + } + private async _discoverTests( + cmdSource: CommandSource, + ignoreCache: boolean = false, + quietMode: boolean = false, + userInitiated: boolean = false, + clearTestStatus: boolean = false + ): Promise { + if (!ignoreCache && this.tests! && this.tests!.testFunctions.length > 0) { + this.updateStatus(TestStatus.Idle); + return Promise.resolve(this.tests!); + } + if (userInitiated) { + this.testsStatusUpdaterService.updateStatusAsDiscovering(this.workspaceFolder, this.tests); + } + this.updateStatus(TestStatus.Discovering); + // If ignoreCache is true, its an indication of the fact that its a user invoked operation. + // Hence we can stop the debugger. + if (userInitiated) { + this.stop(); + } + const telementryProperties: TestDiscoverytTelemetry = { + tool: this.testProvider, + // tslint:disable-next-line:no-any prefer-type-cast + trigger: cmdSource as any, + failed: false + }; + this.commandManager.executeCommand('setContext', 'testsDiscovered', true).then(noop, noop); + this.createCancellationToken(CancellationTokenType.testDiscovery); + const discoveryOptions = this.getDiscoveryOptions(ignoreCache); + const discoveryService = this.serviceContainer.get( + ITestDiscoveryService, + this.testProvider + ); + return discoveryService + .discoverTests(discoveryOptions) + .then((tests) => { + const wkspace = this.workspaceService.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri; + const existingTests = this.testCollectionStorage.getTests(wkspace)!; + if (clearTestStatus) { + this.resetTestResults(); + } else if (existingTests) { + copyDesiredTestResults(existingTests, tests); + this._testResultsService.updateResults(tests); + } + this.testCollectionStorage.storeTests(wkspace, tests); + this.tests = tests; + this.updateStatus(TestStatus.Idle); + this.discoverTestsPromise = undefined; + + // have errors in Discovering + let haveErrorsInDiscovering = false; + tests.testFiles.forEach((file) => { + if (file.errorsWhenDiscovering && file.errorsWhenDiscovering.length > 0) { + haveErrorsInDiscovering = true; + this.outputChannel.append('_'.repeat(10)); + this.outputChannel.append(`There was an error in identifying unit tests in ${file.nameToRun}`); + this.outputChannel.appendLine('_'.repeat(10)); + this.outputChannel.appendLine(file.errorsWhenDiscovering); + } + }); + if (haveErrorsInDiscovering && !quietMode) { + const testsHelper = this.serviceContainer.get(ITestsHelper); + testsHelper.displayTestErrorMessage('There were some errors in discovering unit tests'); + } + this.disposeCancellationToken(CancellationTokenType.testDiscovery); + sendTelemetryEvent(EventName.UNITTEST_DISCOVER, undefined, telementryProperties); + return tests; + }) + .catch(async (reason: {}) => { + if (userInitiated) { + this.testsStatusUpdaterService.updateStatusAsUnknown(this.workspaceFolder, this.tests); + } + if ( + isNotInstalledError(reason as Error) && + !quietMode && + !(await this.installer.isInstalled(this.product, this.workspaceFolder)) + ) { + this.installer + .promptToInstall(this.product, this.workspaceFolder) + .catch((ex) => traceError('isNotInstalledError', ex)); + } + + this.tests = undefined; + this.discoverTestsPromise = undefined; + if ( + this.testDiscoveryCancellationToken && + this.testDiscoveryCancellationToken.isCancellationRequested + ) { + reason = CANCELLATION_REASON; + this.updateStatus(TestStatus.Idle); + } else { + telementryProperties.failed = true; + sendTelemetryEvent(EventName.UNITTEST_DISCOVER, undefined, telementryProperties); + this.updateStatus(TestStatus.Error); + this.outputChannel.appendLine('Test Discovery failed: '); + this.outputChannel.appendLine(reason.toString()); + } + const wkspace = this.workspaceService.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri; + this.testCollectionStorage.storeTests(wkspace, undefined); + this.disposeCancellationToken(CancellationTokenType.testDiscovery); + return Promise.reject(reason); + }); + } + public async runTest( + cmdSource: CommandSource, + testsToRun?: TestsToRun, + runFailedTests?: boolean, + debug?: boolean + ): Promise { + const moreInfo = { + Test_Provider: this.testProvider, + Run_Failed_Tests: 'false', + Run_Specific_File: 'false', + Run_Specific_Class: 'false', + Run_Specific_Function: 'false' + }; + //Ensure valid values are sent. + const validCmdSourceValues = getNamesAndValues(CommandSource).map((item) => item.value); + const telementryProperties: TestRunTelemetry = { + tool: this.testProvider, + scope: 'all', + debugging: debug === true, + triggerSource: validCmdSourceValues.indexOf(cmdSource) === -1 ? 'commandpalette' : cmdSource, + failed: false + }; + + if (!runFailedTests && !testsToRun) { + this.testsStatusUpdaterService.updateStatusAsRunning(this.workspaceFolder, this.tests); + } + + this.updateStatus(TestStatus.Running); + if (this.testRunnerCancellationTokenSource) { + this.testRunnerCancellationTokenSource.cancel(); + } + + if (runFailedTests === true) { + moreInfo.Run_Failed_Tests = runFailedTests.toString(); + telementryProperties.scope = 'failed'; + this.testsStatusUpdaterService.updateStatusAsRunningFailedTests(this.workspaceFolder, this.tests); + } + if (testsToRun && typeof testsToRun === 'object') { + if (Array.isArray(testsToRun.testFile) && testsToRun.testFile.length > 0) { + telementryProperties.scope = 'file'; + moreInfo.Run_Specific_File = 'true'; + } + if (Array.isArray(testsToRun.testSuite) && testsToRun.testSuite.length > 0) { + telementryProperties.scope = 'class'; + moreInfo.Run_Specific_Class = 'true'; + } + if (Array.isArray(testsToRun.testFunction) && testsToRun.testFunction.length > 0) { + telementryProperties.scope = 'function'; + moreInfo.Run_Specific_Function = 'true'; + } + this.testsStatusUpdaterService.updateStatusAsRunningSpecificTests( + this.workspaceFolder, + testsToRun, + this.tests + ); + } + + this.testsStatusUpdaterService.triggerUpdatesToTests(this.workspaceFolder, this.tests); + // If running failed tests, then don't clear the previously build UnitTests + // If we do so, then we end up re-discovering the unit tests and clearing previously cached list of failed tests + // Similarly, if running a specific test or test file, don't clear the cache (possible tests have some state information retained) + const clearDiscoveredTestCache = + runFailedTests || + moreInfo.Run_Specific_File || + moreInfo.Run_Specific_Class || + moreInfo.Run_Specific_Function + ? false + : true; + return this.discoverTests(cmdSource, clearDiscoveredTestCache, true, true) + .catch((reason) => { + if ( + this.testDiscoveryCancellationToken && + this.testDiscoveryCancellationToken.isCancellationRequested + ) { + return Promise.reject(reason); + } + const testsHelper = this.serviceContainer.get(ITestsHelper); + testsHelper.displayTestErrorMessage('Errors in discovering tests, continuing with tests'); + return { + rootTestFolders: [], + testFiles: [], + testFolders: [], + testFunctions: [], + testSuites: [], + summary: { errors: 0, failures: 0, passed: 0, skipped: 0 } + }; + }) + .then((tests) => { + this.updateStatus(TestStatus.Running); + this.createCancellationToken(CancellationTokenType.testRunner); + return this.runTestImpl(tests, testsToRun, runFailedTests, debug); + }) + .then(() => { + this.updateStatus(TestStatus.Idle); + this.disposeCancellationToken(CancellationTokenType.testRunner); + sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, telementryProperties); + this.testsStatusUpdaterService.updateStatusOfRunningTestsAsIdle(this.workspaceFolder, this.tests); + this.testsStatusUpdaterService.triggerUpdatesToTests(this.workspaceFolder, this.tests); + return this.tests!; + }) + .catch((reason) => { + this.testsStatusUpdaterService.updateStatusOfRunningTestsAsIdle(this.workspaceFolder, this.tests); + this.testsStatusUpdaterService.triggerUpdatesToTests(this.workspaceFolder, this.tests); + if (this.testRunnerCancellationToken && this.testRunnerCancellationToken.isCancellationRequested) { + reason = CANCELLATION_REASON; + this.updateStatus(TestStatus.Idle); + } else { + this.updateStatus(TestStatus.Error); + telementryProperties.failed = true; + sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, telementryProperties); + } + this.disposeCancellationToken(CancellationTokenType.testRunner); + return Promise.reject(reason); + }); + } + public async updateDiagnostics(tests: Tests, messages: IPythonTestMessage[]): Promise { + await this.stripStaleDiagnostics(tests, messages); + + // Update relevant file diagnostics for tests that have problems. + const uniqueMsgFiles = messages.reduce((filtered, msg) => { + if (filtered.indexOf(msg.testFilePath) === -1 && msg.testFilePath !== undefined) { + filtered.push(msg.testFilePath); + } + return filtered; + }, []); + const fs = this.serviceContainer.get(IFileSystem); + for (const msgFile of uniqueMsgFiles) { + // Check all messages against each test file. + const fileUri = Uri.file(msgFile); + if (!this.diagnosticCollection.has(fileUri)) { + // Create empty diagnostic for file URI so the rest of the logic can assume one already exists. + const diagnostics: Diagnostic[] = []; + this.diagnosticCollection.set(fileUri, diagnostics); + } + // Get the diagnostics for this file's URI before updating it so old tests that weren't run can still show problems. + const oldDiagnostics = this.diagnosticCollection.get(fileUri)!; + const newDiagnostics: Diagnostic[] = []; + for (const diagnostic of oldDiagnostics) { + newDiagnostics.push(diagnostic); + } + for (const msg of messages) { + if ( + fs.arePathsSame(fileUri.fsPath, Uri.file(msg.testFilePath).fsPath) && + msg.status !== TestStatus.Pass + ) { + const diagnostic = this.createDiagnostics(msg); + newDiagnostics.push(diagnostic); + } + } + + // Set the diagnostics for the file. + this.diagnosticCollection.set(fileUri, newDiagnostics); + } + } + protected abstract runTestImpl( + tests: Tests, + testsToRun?: TestsToRun, + runFailedTests?: boolean, + debug?: boolean + ): Promise; + protected abstract getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions; + private updateStatus(status: TestStatus): void { + this._status = status; + // Fire after 1ms, let existing code run to completion, + // We need to allow for code to get into a consistent state. + setTimeout(() => this._onDidStatusChange.fire({ workspace: this.workspaceFolder, status }), 1); + } + private createCancellationToken(tokenType: CancellationTokenType) { + this.disposeCancellationToken(tokenType); + if (tokenType === CancellationTokenType.testDiscovery) { + this.testDiscoveryCancellationTokenSource = new CancellationTokenSource(); + } else { + this.testRunnerCancellationTokenSource = new CancellationTokenSource(); + } + } + private disposeCancellationToken(tokenType: CancellationTokenType) { + if (tokenType === CancellationTokenType.testDiscovery) { + if (this.testDiscoveryCancellationTokenSource) { + this.testDiscoveryCancellationTokenSource.dispose(); + } + this.testDiscoveryCancellationTokenSource = undefined; + } else { + if (this.testRunnerCancellationTokenSource) { + this.testRunnerCancellationTokenSource.dispose(); + } + this.testRunnerCancellationTokenSource = undefined; + } + } + /** + * Whenever a test is run, any previous problems it had should be removed. This runs through + * every already existing set of diagnostics for any that match the tests that were just run + * so they can be stripped out (as they are now no longer relevant). If the tests pass, then + * there is no need to have a diagnostic for it. If they fail, the stale diagnostic will be + * replaced by an up-to-date diagnostic showing the most recent problem with that test. + * + * In order to identify diagnostics associated with the tests that were run, the `nameToRun` + * property of each messages is compared to the `code` property of each diagnostic. + * + * @param messages Details about the tests that were just run. + */ + private async stripStaleDiagnostics(tests: Tests, messages: IPythonTestMessage[]): Promise { + this.diagnosticCollection.forEach((diagnosticUri, oldDiagnostics, collection) => { + const newDiagnostics: Diagnostic[] = []; + for (const diagnostic of oldDiagnostics) { + const matchingMsg = messages.find((msg) => msg.code === diagnostic.code); + if (matchingMsg === undefined) { + // No matching message was found, so this test was not included in the test run. + const matchingTest = tests.testFunctions.find( + (tf) => tf.testFunction.nameToRun === diagnostic.code + ); + if (matchingTest !== undefined) { + // Matching test was found, so the diagnostic is still relevant. + newDiagnostics.push(diagnostic); + } + } + } + // Set the diagnostics for the file. + collection.set(diagnosticUri, newDiagnostics); + }); + } + + private createDiagnostics(message: IPythonTestMessage): Diagnostic { + const stackStart = message.locationStack![0]; + const diagPrefix = this.unitTestDiagnosticService.getMessagePrefix(message.status!); + const severity = this.unitTestDiagnosticService.getSeverity(message.severity)!; + const diagMsg = message.message ? message.message.split('\n')[0] : ''; + const diagnostic = new Diagnostic( + stackStart.location.range, + `${diagPrefix ? `${diagPrefix}: ` : ''}${diagMsg}`, + severity + ); + diagnostic.code = message.code; + diagnostic.source = message.provider; + const relatedInfoArr: DiagnosticRelatedInformation[] = []; + for (const frameDetails of message.locationStack!) { + const relatedInfo = new DiagnosticRelatedInformation(frameDetails.location, frameDetails.lineText); + relatedInfoArr.push(relatedInfo); + } + diagnostic.relatedInformation = relatedInfoArr; + return diagnostic; + } +} diff --git a/src/client/testing/common/managers/testConfigurationManager.ts b/src/client/testing/common/managers/testConfigurationManager.ts new file mode 100644 index 000000000000..22c19e0b821d --- /dev/null +++ b/src/client/testing/common/managers/testConfigurationManager.ts @@ -0,0 +1,128 @@ +import * as path from 'path'; +import { OutputChannel, QuickPickItem, Uri } from 'vscode'; +import { IApplicationShell } from '../../../common/application/types'; +import { traceInfo } from '../../../common/logger'; +import { IFileSystem } from '../../../common/platform/types'; +import { IInstaller, IOutputChannel } from '../../../common/types'; +import { createDeferred } from '../../../common/utils/async'; +import { IServiceContainer } from '../../../ioc/types'; +import { ITestConfigSettingsService, ITestConfigurationManager } from '../../types'; +import { TEST_OUTPUT_CHANNEL, UNIT_TEST_PRODUCTS } from '../constants'; +import { UnitTestProduct } from '../types'; + +export abstract class TestConfigurationManager implements ITestConfigurationManager { + protected readonly outputChannel: OutputChannel; + protected readonly installer: IInstaller; + protected readonly testConfigSettingsService: ITestConfigSettingsService; + constructor( + protected workspace: Uri, + protected product: UnitTestProduct, + protected readonly serviceContainer: IServiceContainer, + cfg?: ITestConfigSettingsService + ) { + this.outputChannel = serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); + this.installer = serviceContainer.get(IInstaller); + this.testConfigSettingsService = cfg + ? cfg + : serviceContainer.get(ITestConfigSettingsService); + } + public abstract configure(wkspace: Uri): Promise; + public abstract requiresUserToConfigure(wkspace: Uri): Promise; + public async enable() { + // Disable other test frameworks. + await Promise.all( + UNIT_TEST_PRODUCTS.filter((prod) => prod !== this.product).map((prod) => + this.testConfigSettingsService.disable(this.workspace, prod) + ) + ); + await this.testConfigSettingsService.enable(this.workspace, this.product); + } + // tslint:disable-next-line:no-any + public async disable() { + return this.testConfigSettingsService.enable(this.workspace, this.product); + } + protected selectTestDir(rootDir: string, subDirs: string[], customOptions: QuickPickItem[] = []): Promise { + const options = { + ignoreFocusOut: true, + matchOnDescription: true, + matchOnDetail: true, + placeHolder: 'Select the directory containing the tests' + }; + let items: QuickPickItem[] = subDirs + .map((dir) => { + const dirName = path.relative(rootDir, dir); + if (dirName.indexOf('.') === 0) { + return; + } + return { + label: dirName, + description: '' + }; + }) + .filter((item) => item !== undefined) + .map((item) => item!); + + items = [{ label: '.', description: 'Root directory' }, ...items]; + items = customOptions.concat(items); + const def = createDeferred(); + const appShell = this.serviceContainer.get(IApplicationShell); + appShell.showQuickPick(items, options).then((item) => { + if (!item) { + this.handleCancelled(); // This will throw an exception. + return; + } + + def.resolve(item.label); + }); + + return def.promise; + } + + protected selectTestFilePattern(): Promise { + const options = { + ignoreFocusOut: true, + matchOnDescription: true, + matchOnDetail: true, + placeHolder: 'Select the pattern to identify test files' + }; + const items: QuickPickItem[] = [ + { label: '*test.py', description: "Python Files ending with 'test'" }, + { label: '*_test.py', description: "Python Files ending with '_test'" }, + { label: 'test*.py', description: "Python Files beginning with 'test'" }, + { label: 'test_*.py', description: "Python Files beginning with 'test_'" }, + { label: '*test*.py', description: "Python Files containing the word 'test'" } + ]; + + const def = createDeferred(); + const appShell = this.serviceContainer.get(IApplicationShell); + appShell.showQuickPick(items, options).then((item) => { + if (!item) { + this.handleCancelled(); // This will throw an exception. + return; + } + + def.resolve(item.label); + }); + + return def.promise; + } + protected getTestDirs(rootDir: string): Promise { + const fs = this.serviceContainer.get(IFileSystem); + return fs.getSubDirectories(rootDir).then((subDirs) => { + subDirs.sort(); + + // Find out if there are any dirs with the name test and place them on the top. + const possibleTestDirs = subDirs.filter((dir) => dir.match(/test/i)); + const nonTestDirs = subDirs.filter((dir) => possibleTestDirs.indexOf(dir) === -1); + possibleTestDirs.push(...nonTestDirs); + + // The test dirs are now on top. + return possibleTestDirs; + }); + } + + private handleCancelled() { + traceInfo('testing configuration (in UI) cancelled'); + throw Error('cancelled'); + } +} diff --git a/src/client/testing/common/runner.ts b/src/client/testing/common/runner.ts new file mode 100644 index 000000000000..fd2f3145b7d8 --- /dev/null +++ b/src/client/testing/common/runner.ts @@ -0,0 +1,140 @@ +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { ErrorUtils } from '../../common/errors/errorUtils'; +import { ModuleNotInstalledError } from '../../common/errors/moduleNotInstalledError'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + IPythonToolExecutionService, + ObservableExecutionResult, + SpawnOptions +} from '../../common/process/types'; +import { ExecutionInfo, IConfigurationService, IPythonSettings } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from './constants'; +import { ITestRunner, ITestsHelper, Options, TestProvider } from './types'; +export { Options } from './types'; + +@injectable() +export class TestRunner implements ITestRunner { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + public run(testProvider: TestProvider, options: Options): Promise { + return run(this.serviceContainer, testProvider, options); + } +} + +export async function run( + serviceContainer: IServiceContainer, + testProvider: TestProvider, + options: Options +): Promise { + const testExecutablePath = getExecutablePath( + testProvider, + serviceContainer.get(IConfigurationService).getSettings(options.workspaceFolder) + ); + const moduleName = getTestModuleName(testProvider); + const spawnOptions = options as SpawnOptions; + let pythonExecutionServicePromise: Promise; + spawnOptions.mergeStdOutErr = typeof spawnOptions.mergeStdOutErr === 'boolean' ? spawnOptions.mergeStdOutErr : true; + + let promise: Promise>; + + // Since conda 4.4.0 we have found that running python code needs the environment activated. + // So if running an executable, there's no way we can activate, if its a module, then activate and run the module. + const testHelper = serviceContainer.get(ITestsHelper); + const executionInfo: ExecutionInfo = { + execPath: testExecutablePath, + args: options.args, + moduleName: testExecutablePath && testExecutablePath.length > 0 ? undefined : moduleName, + product: testHelper.parseProduct(testProvider) + }; + + if (testProvider === UNITTEST_PROVIDER) { + promise = serviceContainer + .get(IPythonExecutionFactory) + .createActivatedEnvironment({ resource: options.workspaceFolder }) + .then((executionService) => executionService.execObservable(options.args, { ...spawnOptions })); + } else if (typeof executionInfo.moduleName === 'string' && executionInfo.moduleName.length > 0) { + pythonExecutionServicePromise = serviceContainer + .get(IPythonExecutionFactory) + .createActivatedEnvironment({ resource: options.workspaceFolder }); + promise = pythonExecutionServicePromise.then((executionService) => + executionService.execModuleObservable(executionInfo.moduleName!, executionInfo.args, options) + ); + } else { + const pythonToolsExecutionService = serviceContainer.get( + IPythonToolExecutionService + ); + promise = pythonToolsExecutionService.execObservable(executionInfo, spawnOptions, options.workspaceFolder); + } + + return promise.then((result) => { + return new Promise((resolve, reject) => { + let stdOut = ''; + let stdErr = ''; + result.out.subscribe( + (output) => { + stdOut += output.out; + // If the test runner python module is not installed we'll have something in stderr. + // Hence track that separately and check at the end. + if (output.source === 'stderr') { + stdErr += output.out; + } + if (options.outChannel) { + options.outChannel.append(output.out); + } + }, + reject, + async () => { + // If the test runner python module is not installed we'll have something in stderr. + if ( + moduleName && + pythonExecutionServicePromise && + ErrorUtils.outputHasModuleNotInstalledError(moduleName, stdErr) + ) { + const pythonExecutionService = await pythonExecutionServicePromise; + const isInstalled = await pythonExecutionService.isModuleInstalled(moduleName); + if (!isInstalled) { + return reject(new ModuleNotInstalledError(moduleName)); + } + } + resolve(stdOut); + } + ); + }); + }); +} + +function getExecutablePath(testProvider: TestProvider, settings: IPythonSettings): string | undefined { + let testRunnerExecutablePath: string | undefined; + switch (testProvider) { + case NOSETEST_PROVIDER: { + testRunnerExecutablePath = settings.testing.nosetestPath; + break; + } + case PYTEST_PROVIDER: { + testRunnerExecutablePath = settings.testing.pytestPath; + break; + } + default: { + return undefined; + } + } + return path.basename(testRunnerExecutablePath) === testRunnerExecutablePath ? undefined : testRunnerExecutablePath; +} +function getTestModuleName(testProvider: TestProvider) { + switch (testProvider) { + case NOSETEST_PROVIDER: { + return 'nose'; + } + case PYTEST_PROVIDER: { + return 'pytest'; + } + case UNITTEST_PROVIDER: { + return 'unittest'; + } + default: { + throw new Error(`Test provider '${testProvider}' not supported`); + } + } +} diff --git a/src/client/testing/common/services/configSettingService.ts b/src/client/testing/common/services/configSettingService.ts new file mode 100644 index 000000000000..3fd888620750 --- /dev/null +++ b/src/client/testing/common/services/configSettingService.ts @@ -0,0 +1,119 @@ +import { inject, injectable } from 'inversify'; +import { Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { Product } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { ITestConfigSettingsService } from '../../types'; +import { UnitTestProduct } from './../types'; + +@injectable() +export class TestConfigSettingsService implements ITestConfigSettingsService { + private readonly workspaceService: IWorkspaceService; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.workspaceService = serviceContainer.get(IWorkspaceService); + } + public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]) { + const setting = this.getTestArgSetting(product); + return this.updateSetting(testDirectory, setting, args); + } + + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = this.getTestEnablingSetting(product); + return this.updateSetting(testDirectory, setting, true); + } + + public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = this.getTestEnablingSetting(product); + return this.updateSetting(testDirectory, setting, false); + } + public getTestEnablingSetting(product: UnitTestProduct) { + switch (product) { + case Product.unittest: + return 'testing.unittestEnabled'; + case Product.pytest: + return 'testing.pytestEnabled'; + case Product.nosetest: + return 'testing.nosetestsEnabled'; + default: + throw new Error('Invalid Test Product'); + } + } + private getTestArgSetting(product: UnitTestProduct) { + switch (product) { + case Product.unittest: + return 'testing.unittestArgs'; + case Product.pytest: + return 'testing.pytestArgs'; + case Product.nosetest: + return 'testing.nosetestArgs'; + default: + throw new Error('Invalid Test Product'); + } + } + // tslint:disable-next-line:no-any + private async updateSetting(testDirectory: string | Uri, setting: string, value: any) { + let pythonConfig: WorkspaceConfiguration; + const resource = typeof testDirectory === 'string' ? Uri.file(testDirectory) : testDirectory; + if (!this.workspaceService.hasWorkspaceFolders) { + pythonConfig = this.workspaceService.getConfiguration('python'); + } else if (this.workspaceService.workspaceFolders!.length === 1) { + pythonConfig = this.workspaceService.getConfiguration( + 'python', + this.workspaceService.workspaceFolders![0].uri + ); + } else { + const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + if (!workspaceFolder) { + throw new Error(`Test directory does not belong to any workspace (${testDirectory})`); + } + // tslint:disable-next-line:no-non-null-assertion + pythonConfig = this.workspaceService.getConfiguration('python', workspaceFolder!.uri); + } + + return pythonConfig.update(setting, value); + } +} + +export class BufferedTestConfigSettingsService implements ITestConfigSettingsService { + private ops: [string, string | Uri, UnitTestProduct, string[]][]; + constructor() { + this.ops = []; + } + + public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]) { + this.ops.push(['updateTestArgs', testDirectory, product, args]); + } + + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + this.ops.push(['enable', testDirectory, product, []]); + } + + public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + this.ops.push(['disable', testDirectory, product, []]); + } + + public async apply(cfg: ITestConfigSettingsService) { + const ops = this.ops; + this.ops = []; + // Note that earlier ops do not get rolled back if a later + // one fails. + for (const [op, testDir, prod, args] of ops) { + switch (op) { + case 'updateTestArgs': + await cfg.updateTestArgs(testDir, prod, args); + break; + case 'enable': + await cfg.enable(testDir, prod); + break; + case 'disable': + await cfg.disable(testDir, prod); + break; + default: + break; + } + } + } + public getTestEnablingSetting(_: UnitTestProduct): string { + throw new Error('Method not implemented.'); + } +} diff --git a/src/client/testing/common/services/contextService.ts b/src/client/testing/common/services/contextService.ts new file mode 100644 index 000000000000..40b70561b649 --- /dev/null +++ b/src/client/testing/common/services/contextService.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ICommandManager } from '../../../common/application/types'; +import { ContextKey } from '../../../common/contextKey'; +import { IDisposable } from '../../../common/types'; +import { swallowExceptions } from '../../../common/utils/decorators'; +import { ITestManagementService, WorkspaceTestStatus } from '../../types'; +import { ITestCollectionStorageService, ITestContextService, TestStatus } from '../types'; + +@injectable() +export class TestContextService implements ITestContextService { + private readonly hasFailedTests: ContextKey; + private readonly runningTests: ContextKey; + private readonly discoveringTests: ContextKey; + private readonly busyTests: ContextKey; + private readonly disposables: IDisposable[] = []; + constructor( + @inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService, + @inject(ITestManagementService) private readonly testManager: ITestManagementService, + @inject(ICommandManager) cmdManager: ICommandManager + ) { + this.hasFailedTests = new ContextKey('hasFailedTests', cmdManager); + this.runningTests = new ContextKey('runningTests', cmdManager); + this.discoveringTests = new ContextKey('discoveringTests', cmdManager); + this.busyTests = new ContextKey('busyTests', cmdManager); + } + public dispose(): void { + this.disposables.forEach((d) => d.dispose()); + } + public register(): void { + this.testManager.onDidStatusChange(this.onStatusChange, this, this.disposables); + } + @swallowExceptions('Handle status change of tests') + protected async onStatusChange(status: WorkspaceTestStatus): Promise { + const tests = this.storage.getTests(status.workspace); + const promises: Promise[] = []; + if (tests && tests.summary) { + promises.push(this.hasFailedTests.set(tests.summary.failures > 0)); + } + promises.push( + ...[ + this.runningTests.set(status.status === TestStatus.Running), + this.discoveringTests.set(status.status === TestStatus.Discovering), + this.busyTests.set(status.status === TestStatus.Running || status.status === TestStatus.Discovering) + ] + ); + + await Promise.all(promises); + } +} diff --git a/src/client/testing/common/services/discoveredTestParser.ts b/src/client/testing/common/services/discoveredTestParser.ts new file mode 100644 index 000000000000..200c50cd88bd --- /dev/null +++ b/src/client/testing/common/services/discoveredTestParser.ts @@ -0,0 +1,384 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { traceError } from '../../../common/logger'; +import { TestDataItem, TestDataItemType } from '../../types'; +import { getParentFile, getParentSuite, getTestDataItemType } from '../testUtils'; +import * as testing from '../types'; +import * as discovery from './types'; + +@injectable() +export class TestDiscoveredTestParser implements discovery.ITestDiscoveredTestParser { + constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {} + + public parse(resource: Uri, discoveredTests: discovery.DiscoveredTests[]): testing.Tests { + const tests: testing.Tests = { + rootTestFolders: [], + summary: { errors: 0, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFolders: [], + testFunctions: [], + testSuites: [] + }; + + const workspace = this.workspaceService.getWorkspaceFolder(resource); + if (!workspace) { + traceError('Resource does not belong to any workspace folder'); + return tests; + } + + for (const data of discoveredTests) { + const rootFolder = { + name: data.root, + folders: [], + time: 0, + testFiles: [], + resource: resource, + nameToRun: data.rootid + }; + tests.rootTestFolders.push(rootFolder); + tests.testFolders.push(rootFolder); + this.buildChildren(rootFolder, rootFolder, data, tests); + } + + return tests; + } + + /** + * Not the best solution to use `case statements`, but it keeps the code simple and easy to read in one place. + * Could go with separate classes for each type and use stratergies, but that just ends up a class for + * 10 lines of code. Hopefully this is more readable and maintainable than having multiple classes for + * the simple processing of the children. + * + * @protected + * @param {TestFolder} rootFolder + * @param {TestDataItem} parent + * @param {DiscoveredTests} discoveredTests + * @param {Tests} tests + * @memberof TestsDiscovery + */ + public buildChildren( + rootFolder: testing.TestFolder, + parent: TestDataItem, + discoveredTests: discovery.DiscoveredTests, + tests: testing.Tests + ) { + const parentType = getTestDataItemType(parent); + switch (parentType) { + case TestDataItemType.folder: { + this.processFolder(rootFolder, parent as testing.TestFolder, discoveredTests, tests); + break; + } + case TestDataItemType.file: { + this.processFile(rootFolder, parent as testing.TestFile, discoveredTests, tests); + break; + } + case TestDataItemType.suite: { + this.processSuite(rootFolder, parent as testing.TestSuite, discoveredTests, tests); + break; + } + default: + break; + } + } + + /** + * Process the children of a folder. + * A folder can only contain other folders and files. + * Hence limit processing to those items. + * + * @protected + * @param {TestFolder} rootFolder + * @param {TestFolder} parentFolder + * @param {DiscoveredTests} discoveredTests + * @param {Tests} tests + * @memberof TestDiscoveredTestParser + */ + protected processFolder( + rootFolder: testing.TestFolder, + parentFolder: testing.TestFolder, + discoveredTests: discovery.DiscoveredTests, + tests: testing.Tests + ) { + const folders = discoveredTests.parents + .filter((child) => child.kind === 'folder' && child.parentid === parentFolder.nameToRun) + .map((folder) => createTestFolder(rootFolder, folder as discovery.TestFolder)); + folders.forEach((folder) => { + parentFolder.folders.push(folder); + tests.testFolders.push(folder); + this.buildChildren(rootFolder, folder, discoveredTests, tests); + }); + + const files = discoveredTests.parents + .filter((child) => child.kind === 'file' && child.parentid === parentFolder.nameToRun) + .map((file) => createTestFile(rootFolder, file as discovery.TestFile)); + files.forEach((file) => { + parentFolder.testFiles.push(file); + tests.testFiles.push(file); + this.buildChildren(rootFolder, file, discoveredTests, tests); + }); + } + + /** + * Process the children of a file. + * A file can only contain suites, functions and paramerterized functions. + * Hence limit processing just to those items. + * + * @protected + * @param {TestFolder} rootFolder + * @param {TestFile} parentFile + * @param {DiscoveredTests} discoveredTests + * @param {Tests} tests + * @memberof TestDiscoveredTestParser + */ + protected processFile( + rootFolder: testing.TestFolder, + parentFile: testing.TestFile, + discoveredTests: discovery.DiscoveredTests, + tests: testing.Tests + ) { + const suites = discoveredTests.parents + .filter((child) => child.kind === 'suite' && child.parentid === parentFile.nameToRun) + .map((suite) => createTestSuite(parentFile, rootFolder.resource, suite as discovery.TestSuite)); + suites.forEach((suite) => { + parentFile.suites.push(suite); + tests.testSuites.push(createFlattenedSuite(tests, suite)); + this.buildChildren(rootFolder, suite, discoveredTests, tests); + }); + + const functions = discoveredTests.tests + .filter((test) => test.parentid === parentFile.nameToRun) + .map((test) => createTestFunction(rootFolder, test)); + functions.forEach((func) => { + parentFile.functions.push(func); + tests.testFunctions.push(createFlattenedFunction(tests, func)); + }); + + const parameterizedFunctions = discoveredTests.parents + .filter((child) => child.kind === 'function' && child.parentid === parentFile.nameToRun) + .map((func) => createParameterizedTestFunction(rootFolder, func as discovery.TestFunction)); + parameterizedFunctions.forEach((func) => + this.processParameterizedFunction(rootFolder, parentFile, func, discoveredTests, tests) + ); + } + + /** + * Process the children of a suite. + * A suite can only contain suites, functions and paramerterized functions. + * Hence limit processing just to those items. + * + * @protected + * @param {TestFolder} rootFolder + * @param {TestSuite} parentSuite + * @param {DiscoveredTests} discoveredTests + * @param {Tests} tests + * @memberof TestDiscoveredTestParser + */ + protected processSuite( + rootFolder: testing.TestFolder, + parentSuite: testing.TestSuite, + discoveredTests: discovery.DiscoveredTests, + tests: testing.Tests + ) { + const suites = discoveredTests.parents + .filter((child) => child.kind === 'suite' && child.parentid === parentSuite.nameToRun) + .map((suite) => createTestSuite(parentSuite, rootFolder.resource, suite as discovery.TestSuite)); + suites.forEach((suite) => { + parentSuite.suites.push(suite); + tests.testSuites.push(createFlattenedSuite(tests, suite)); + this.buildChildren(rootFolder, suite, discoveredTests, tests); + }); + + const functions = discoveredTests.tests + .filter((test) => test.parentid === parentSuite.nameToRun) + .map((test) => createTestFunction(rootFolder, test)); + functions.forEach((func) => { + parentSuite.functions.push(func); + tests.testFunctions.push(createFlattenedFunction(tests, func)); + }); + + const parameterizedFunctions = discoveredTests.parents + .filter((child) => child.kind === 'function' && child.parentid === parentSuite.nameToRun) + .map((func) => createParameterizedTestFunction(rootFolder, func as discovery.TestFunction)); + parameterizedFunctions.forEach((func) => + this.processParameterizedFunction(rootFolder, parentSuite, func, discoveredTests, tests) + ); + } + + /** + * Process the children of a parameterized function. + * A parameterized function can only contain functions (in tests). + * Hence limit processing just to those items. + * + * @protected + * @param {TestFolder} rootFolder + * @param {TestFile | TestSuite} parent + * @param {TestFunction} parentFunction + * @param {DiscoveredTests} discoveredTests + * @param {Tests} tests + * @returns + * @memberof TestDiscoveredTestParser + */ + protected processParameterizedFunction( + rootFolder: testing.TestFolder, + parent: testing.TestFile | testing.TestSuite, + parentFunction: testing.SubtestParent, + discoveredTests: discovery.DiscoveredTests, + tests: testing.Tests + ) { + if (!parentFunction.asSuite) { + return; + } + const functions = discoveredTests.tests + .filter((test) => test.parentid === parentFunction.nameToRun) + .map((test) => createTestFunction(rootFolder, test)); + functions.forEach((func) => { + func.subtestParent = parentFunction; + parentFunction.asSuite.functions.push(func); + parent.functions.push(func); + tests.testFunctions.push(createFlattenedParameterizedFunction(tests, func, parent)); + }); + } +} + +function createTestFolder(root: testing.TestFolder, item: discovery.TestFolder): testing.TestFolder { + return { + name: item.name, + nameToRun: item.id, + resource: root.resource, + time: 0, + folders: [], + testFiles: [] + }; +} + +function createTestFile(root: testing.TestFolder, item: discovery.TestFile): testing.TestFile { + const fullpath = path.isAbsolute(item.relpath) ? item.relpath : path.resolve(root.name, item.relpath); + return { + fullPath: fullpath, + functions: [], + name: item.name, + nameToRun: item.id, + resource: root.resource, + suites: [], + time: 0, + xmlName: createXmlName(item.id) + }; +} + +function createTestSuite( + parentSuiteFile: testing.TestFile | testing.TestSuite, + resource: Uri, + item: discovery.TestSuite +): testing.TestSuite { + const suite = { + functions: [], + name: item.name, + nameToRun: item.id, + resource: resource, + suites: [], + time: 0, + xmlName: '', + isInstance: false, + isUnitTest: false + }; + suite.xmlName = `${parentSuiteFile.xmlName}.${item.name}`; + return suite; +} + +function createFlattenedSuite(tests: testing.Tests, suite: testing.TestSuite): testing.FlattenedTestSuite { + const parentFile = getParentFile(tests, suite); + return { + parentTestFile: parentFile, + testSuite: suite, + xmlClassName: parentFile.xmlName + }; +} + +function createFlattenedParameterizedFunction( + tests: testing.Tests, + func: testing.TestFunction, + parent: testing.TestFile | testing.TestSuite +): testing.FlattenedTestFunction { + const type = getTestDataItemType(parent); + const parentFile = + type && type === TestDataItemType.suite ? getParentFile(tests, func) : (parent as testing.TestFile); + const parentSuite = type && type === TestDataItemType.suite ? (parent as testing.TestSuite) : undefined; + return { + parentTestFile: parentFile, + parentTestSuite: parentSuite, + xmlClassName: parentSuite ? parentSuite.xmlName : parentFile.xmlName, + testFunction: func + }; +} + +function createFlattenedFunction(tests: testing.Tests, func: testing.TestFunction): testing.FlattenedTestFunction { + const parent = getParentFile(tests, func); + const type = parent ? getTestDataItemType(parent) : undefined; + const parentFile = + type && type === TestDataItemType.suite ? getParentFile(tests, func) : (parent as testing.TestFile); + const parentSuite = getParentSuite(tests, func); + return { + parentTestFile: parentFile, + parentTestSuite: parentSuite, + xmlClassName: parentSuite ? parentSuite.xmlName : parentFile.xmlName, + testFunction: func + }; +} + +function createParameterizedTestFunction( + root: testing.TestFolder, + item: discovery.TestFunction +): testing.SubtestParent { + const suite: testing.TestSuite = { + functions: [], + isInstance: false, + isUnitTest: false, + name: item.name, + nameToRun: item.id, + resource: root.resource, + time: 0, + suites: [], + xmlName: '' + }; + return { + asSuite: suite, + name: item.name, + nameToRun: item.id, + time: 0 + }; +} + +function createTestFunction(root: testing.TestFolder, item: discovery.Test): testing.TestFunction { + return { + name: item.name, + nameToRun: item.id, + resource: root.resource, + time: 0, + file: item.source.substr(0, item.source.lastIndexOf(':')) + }; +} + +/** + * Creates something known as an Xml Name, used to identify items + * from an xunit test result. + * Once we have the test runner done in Python, this can be discarded. + * @param {string} fileId + * @returns + */ +function createXmlName(fileId: string) { + let name = path.join(path.dirname(fileId), path.basename(fileId, path.extname(fileId))); + // Replace all path separators with ".". + name = name.replace(/\\/g, '.').replace(/\//g, '.'); + // Remove leading "." and path separators. + while (name.startsWith('.') || name.startsWith('/') || name.startsWith('\\')) { + name = name.substring(1); + } + return name; +} diff --git a/src/client/testing/common/services/discovery.ts b/src/client/testing/common/services/discovery.ts new file mode 100644 index 000000000000..0042f5e003f0 --- /dev/null +++ b/src/client/testing/common/services/discovery.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { OutputChannel } from 'vscode'; +import { traceError } from '../../../common/logger'; +import * as internalScripts from '../../../common/process/internal/scripts'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions +} from '../../../common/process/types'; +import { IOutputChannel } from '../../../common/types'; +import { captureTelemetry } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { TEST_OUTPUT_CHANNEL } from '../constants'; +import { ITestDiscoveryService, TestDiscoveryOptions, Tests } from '../types'; +import { DiscoveredTests, ITestDiscoveredTestParser } from './types'; + +@injectable() +export class TestsDiscoveryService implements ITestDiscoveryService { + constructor( + @inject(IPythonExecutionFactory) private readonly execFactory: IPythonExecutionFactory, + @inject(ITestDiscoveredTestParser) private readonly parser: ITestDiscoveredTestParser, + @inject(IOutputChannel) @named(TEST_OUTPUT_CHANNEL) private readonly outChannel: OutputChannel + ) {} + @captureTelemetry(EventName.UNITTEST_DISCOVER_WITH_PYCODE, undefined, true) + public async discoverTests(options: TestDiscoveryOptions): Promise { + try { + const discoveredTests = await this.exec(options); + return this.parser.parse(options.workspaceFolder, discoveredTests); + } catch (ex) { + if (ex.stdout) { + traceError('Failed to parse discovered Test', new Error(ex.stdout)); + } + traceError('Failed to parse discovered Test', ex); + throw ex; + } + } + public async exec(options: TestDiscoveryOptions): Promise { + const [args, parse] = internalScripts.testing_tools.run_adapter(options.args); + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: options.workspaceFolder + }; + const execService = await this.execFactory.createActivatedEnvironment(creationOptions); + const spawnOptions: SpawnOptions = { + token: options.token, + cwd: options.cwd, + throwOnStdErr: true + }; + this.outChannel.appendLine(`python ${args.join(' ')}`); + const proc = await execService.exec(args, spawnOptions); + try { + return parse(proc.stdout); + } catch (ex) { + ex.stdout = proc.stdout; + throw ex; // re-throw + } + } +} diff --git a/src/client/testing/common/services/storageService.ts b/src/client/testing/common/services/storageService.ts new file mode 100644 index 000000000000..46aafe1c74de --- /dev/null +++ b/src/client/testing/common/services/storageService.ts @@ -0,0 +1,60 @@ +import { inject, injectable } from 'inversify'; +import { Disposable, Event, EventEmitter, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { IDisposableRegistry } from '../../../common/types'; +import { TestDataItem } from '../../types'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + ITestCollectionStorageService, + TestFunction, + Tests, + TestSuite +} from './../types'; + +@injectable() +export class TestCollectionStorageService implements ITestCollectionStorageService { + private readonly _onDidChange = new EventEmitter<{ uri: Uri; data?: TestDataItem }>(); + private readonly testsIndexedByWorkspaceUri = new Map(); + + constructor( + @inject(IDisposableRegistry) disposables: Disposable[], + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService + ) { + disposables.push(this); + } + public get onDidChange(): Event<{ uri: Uri; data?: TestDataItem }> { + return this._onDidChange.event; + } + public getTests(resource: Uri): Tests | undefined { + const workspaceFolder = this.workspaceService.getWorkspaceFolderIdentifier(resource); + return this.testsIndexedByWorkspaceUri.has(workspaceFolder) + ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) + : undefined; + } + public storeTests(resource: Uri, tests: Tests | undefined): void { + const workspaceFolder = this.workspaceService.getWorkspaceFolderIdentifier(resource); + this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests); + this._onDidChange.fire({ uri: resource }); + } + public findFlattendTestFunction(resource: Uri, func: TestFunction): FlattenedTestFunction | undefined { + const tests = this.getTests(resource); + if (!tests) { + return; + } + return tests.testFunctions.find((f) => f.testFunction === func); + } + public findFlattendTestSuite(resource: Uri, suite: TestSuite): FlattenedTestSuite | undefined { + const tests = this.getTests(resource); + if (!tests) { + return; + } + return tests.testSuites.find((f) => f.testSuite === suite); + } + public dispose() { + this.testsIndexedByWorkspaceUri.clear(); + } + public update(resource: Uri, item: TestDataItem): void { + this._onDidChange.fire({ uri: resource, data: item }); + } +} diff --git a/src/client/testing/common/services/testManagerService.ts b/src/client/testing/common/services/testManagerService.ts new file mode 100644 index 000000000000..42a96877ef0c --- /dev/null +++ b/src/client/testing/common/services/testManagerService.ts @@ -0,0 +1,50 @@ +import { Disposable, Uri } from 'vscode'; +import { IConfigurationService, IDisposableRegistry, Product } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { ITestManager, ITestManagerFactory, ITestManagerService, ITestsHelper, UnitTestProduct } from './../types'; + +export class TestManagerService implements ITestManagerService { + private cachedTestManagers = new Map(); + private readonly configurationService: IConfigurationService; + constructor(private wkspace: Uri, private testsHelper: ITestsHelper, private serviceContainer: IServiceContainer) { + const disposables = serviceContainer.get(IDisposableRegistry); + this.configurationService = serviceContainer.get(IConfigurationService); + disposables.push(this); + } + public dispose() { + this.cachedTestManagers.forEach((info) => { + info.dispose(); + }); + } + public getTestManager(): ITestManager | undefined { + const preferredTestManager = this.getPreferredTestManager(); + if (typeof preferredTestManager !== 'number') { + return; + } + + // tslint:disable-next-line:no-non-null-assertion + if (!this.cachedTestManagers.has(preferredTestManager)) { + const testDirectory = this.getTestWorkingDirectory(); + const testProvider = this.testsHelper.parseProviderName(preferredTestManager); + const factory = this.serviceContainer.get(ITestManagerFactory); + this.cachedTestManagers.set(preferredTestManager, factory(testProvider, this.wkspace, testDirectory)); + } + const testManager = this.cachedTestManagers.get(preferredTestManager)!; + return testManager.enabled ? testManager : undefined; + } + public getTestWorkingDirectory() { + const settings = this.configurationService.getSettings(this.wkspace); + return settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : this.wkspace.fsPath; + } + public getPreferredTestManager(): UnitTestProduct | undefined { + const settings = this.configurationService.getSettings(this.wkspace); + if (settings.testing.nosetestsEnabled) { + return Product.nosetest; + } else if (settings.testing.pytestEnabled) { + return Product.pytest; + } else if (settings.testing.unittestEnabled) { + return Product.unittest; + } + return undefined; + } +} diff --git a/src/client/testing/common/services/testResultsService.ts b/src/client/testing/common/services/testResultsService.ts new file mode 100644 index 000000000000..0112b3599521 --- /dev/null +++ b/src/client/testing/common/services/testResultsService.ts @@ -0,0 +1,77 @@ +import { inject, injectable, named } from 'inversify'; +import { TestDataItem, TestDataItemType } from '../../types'; +import { getChildren, getTestDataItemType } from '../testUtils'; +import { ITestResultsService, ITestVisitor, Tests, TestStatus } from '../types'; + +@injectable() +export class TestResultsService implements ITestResultsService { + constructor(@inject(ITestVisitor) @named('TestResultResetVisitor') private resultResetVisitor: ITestVisitor) {} + public resetResults(tests: Tests): void { + tests.testFolders.forEach((f) => this.resultResetVisitor.visitTestFolder(f)); + tests.testFunctions.forEach((fn) => this.resultResetVisitor.visitTestFunction(fn.testFunction)); + tests.testSuites.forEach((suite) => this.resultResetVisitor.visitTestSuite(suite.testSuite)); + tests.testFiles.forEach((testFile) => this.resultResetVisitor.visitTestFile(testFile)); + } + public updateResults(tests: Tests): void { + // Update Test tree bottom to top + const testQueue: TestDataItem[] = []; + const testStack: TestDataItem[] = []; + tests.rootTestFolders.forEach((folder) => testQueue.push(folder)); + + while (testQueue.length > 0) { + const item = testQueue.shift(); + if (!item) { + continue; + } + testStack.push(item); + const children = getChildren(item); + children.forEach((child) => testQueue.push(child)); + } + while (testStack.length > 0) { + const item = testStack.pop(); + this.updateTestItem(item!); + } + } + private updateTestItem(test: TestDataItem): void { + if (getTestDataItemType(test) === TestDataItemType.function) { + return; + } + let allChildrenPassed = true; + let noChildrenRan = true; + test.functionsPassed = test.functionsFailed = test.functionsDidNotRun = 0; + + const children = getChildren(test); + children.forEach((child) => { + if (getTestDataItemType(child) === TestDataItemType.function) { + if (typeof child.passed === 'boolean') { + noChildrenRan = false; + if (child.passed) { + test.functionsPassed! += 1; + } else { + test.functionsFailed! += 1; + allChildrenPassed = false; + } + } else { + test.functionsDidNotRun! += 1; + } + } else { + if (typeof child.passed === 'boolean') { + noChildrenRan = false; + if (!child.passed) { + allChildrenPassed = false; + } + } + test.functionsFailed! += child.functionsFailed!; + test.functionsPassed! += child.functionsPassed!; + test.functionsDidNotRun! += child.functionsDidNotRun!; + } + }); + if (noChildrenRan) { + test.passed = undefined; + test.status = TestStatus.Unknown; + } else { + test.passed = allChildrenPassed; + test.status = test.passed ? TestStatus.Pass : TestStatus.Fail; + } + } +} diff --git a/src/client/testing/common/services/testsStatusService.ts b/src/client/testing/common/services/testsStatusService.ts new file mode 100644 index 000000000000..b2094a3e26b1 --- /dev/null +++ b/src/client/testing/common/services/testsStatusService.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { TestDataItem } from '../../types'; +import { visitRecursive } from '../testVisitors/visitor'; +import { ITestCollectionStorageService, ITestsStatusUpdaterService, Tests, TestStatus, TestsToRun } from '../types'; + +@injectable() +export class TestsStatusUpdaterService implements ITestsStatusUpdaterService { + constructor(@inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService) {} + public updateStatusAsDiscovering(resource: Uri, tests?: Tests): void { + if (!tests) { + return; + } + const visitor = (item: TestDataItem) => { + item.status = TestStatus.Discovering; + this.storage.update(resource, item); + }; + tests.rootTestFolders.forEach((item) => visitRecursive(tests, item, visitor)); + } + public updateStatusAsUnknown(resource: Uri, tests?: Tests): void { + if (!tests) { + return; + } + const visitor = (item: TestDataItem) => { + item.status = TestStatus.Unknown; + this.storage.update(resource, item); + }; + tests.rootTestFolders.forEach((item) => visitRecursive(tests, item, visitor)); + } + public updateStatusAsRunning(resource: Uri, tests?: Tests): void { + if (!tests) { + return; + } + const visitor = (item: TestDataItem) => { + item.status = TestStatus.Running; + this.storage.update(resource, item); + }; + tests.rootTestFolders.forEach((item) => visitRecursive(tests, item, visitor)); + } + public updateStatusAsRunningFailedTests(resource: Uri, tests?: Tests): void { + if (!tests) { + return; + } + const predicate = (item: TestDataItem) => item.status === TestStatus.Fail || item.status === TestStatus.Error; + const visitor = (item: TestDataItem) => { + if (item.status && predicate(item)) { + item.status = TestStatus.Running; + this.storage.update(resource, item); + } + }; + const failedItems = [ + ...tests.testFunctions.map((f) => f.testFunction).filter(predicate), + ...tests.testSuites.map((f) => f.testSuite).filter(predicate) + ]; + failedItems.forEach((failedItem) => visitRecursive(tests, failedItem, visitor)); + } + public updateStatusAsRunningSpecificTests(resource: Uri, testsToRun: TestsToRun, tests?: Tests): void { + if (!tests) { + return; + } + const itemsRunning = [ + ...(testsToRun.testFile || []), + ...(testsToRun.testSuite || []), + ...(testsToRun.testFunction || []) + ]; + const visitor = (item: TestDataItem) => { + item.status = TestStatus.Running; + this.storage.update(resource, item); + }; + itemsRunning.forEach((item) => visitRecursive(tests, item, visitor)); + } + public updateStatusOfRunningTestsAsIdle(resource: Uri, tests?: Tests): void { + if (!tests) { + return; + } + const visitor = (item: TestDataItem) => { + if (item.status === TestStatus.Running) { + item.status = TestStatus.Idle; + this.storage.update(resource, item); + } + }; + tests.rootTestFolders.forEach((item) => visitRecursive(tests, item, visitor)); + } + public triggerUpdatesToTests(resource: Uri, tests?: Tests): void { + if (!tests) { + return; + } + const visitor = (item: TestDataItem) => this.storage.update(resource, item); + tests.rootTestFolders.forEach((item) => visitRecursive(tests, item, visitor)); + } +} diff --git a/src/client/testing/common/services/types.ts b/src/client/testing/common/services/types.ts new file mode 100644 index 000000000000..e2b929379bbb --- /dev/null +++ b/src/client/testing/common/services/types.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Uri } from 'vscode'; +import * as internalScripts from '../../../common/process/internal/scripts'; +import { Tests } from '../types'; + +// We expose these here as a convenience and to cut down on churn +// elsewhere in the code. +export type DiscoveredTests = internalScripts.testing_tools.DiscoveredTests; +export type Test = internalScripts.testing_tools.Test; +export type TestFolder = internalScripts.testing_tools.TestFolder; +export type TestFile = internalScripts.testing_tools.TestFile; +export type TestSuite = internalScripts.testing_tools.TestSuite; +export type TestFunction = internalScripts.testing_tools.TestFunction; + +export const ITestDiscoveredTestParser = Symbol('ITestDiscoveredTestParser'); +export interface ITestDiscoveredTestParser { + parse(resource: Uri, discoveredTests: DiscoveredTests[]): Tests; +} diff --git a/src/client/testing/common/services/unitTestDiagnosticService.ts b/src/client/testing/common/services/unitTestDiagnosticService.ts new file mode 100644 index 000000000000..5e88a6df615b --- /dev/null +++ b/src/client/testing/common/services/unitTestDiagnosticService.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { DiagnosticSeverity } from 'vscode'; +import * as localize from '../../../common/utils/localize'; +import { DiagnosticMessageType, ITestDiagnosticService, PythonTestMessageSeverity } from '../../types'; +import { TestStatus } from '../types'; + +@injectable() +export class UnitTestDiagnosticService implements ITestDiagnosticService { + private MessageTypes = new Map(); + private MessageSeverities = new Map(); + private MessagePrefixes = new Map(); + + constructor() { + this.MessageTypes.set(TestStatus.Error, DiagnosticMessageType.Error); + this.MessageTypes.set(TestStatus.Fail, DiagnosticMessageType.Fail); + this.MessageTypes.set(TestStatus.Skipped, DiagnosticMessageType.Skipped); + this.MessageTypes.set(TestStatus.Pass, DiagnosticMessageType.Pass); + this.MessageSeverities.set(PythonTestMessageSeverity.Error, DiagnosticSeverity.Error); + this.MessageSeverities.set(PythonTestMessageSeverity.Failure, DiagnosticSeverity.Error); + this.MessageSeverities.set(PythonTestMessageSeverity.Skip, DiagnosticSeverity.Information); + this.MessageSeverities.set(PythonTestMessageSeverity.Pass, undefined); + this.MessagePrefixes.set(DiagnosticMessageType.Error, localize.Testing.testErrorDiagnosticMessage()); + this.MessagePrefixes.set(DiagnosticMessageType.Fail, localize.Testing.testFailDiagnosticMessage()); + this.MessagePrefixes.set(DiagnosticMessageType.Skipped, localize.Testing.testSkippedDiagnosticMessage()); + this.MessagePrefixes.set(DiagnosticMessageType.Pass, ''); + } + public getMessagePrefix(status: TestStatus): string | undefined { + const msgType = this.MessageTypes.get(status); + return msgType !== undefined ? this.MessagePrefixes.get(msgType!) : undefined; + } + public getSeverity(unitTestSeverity: PythonTestMessageSeverity): DiagnosticSeverity | undefined { + return this.MessageSeverities.get(unitTestSeverity); + } +} diff --git a/src/client/testing/common/services/workspaceTestManagerService.ts b/src/client/testing/common/services/workspaceTestManagerService.ts new file mode 100644 index 000000000000..e0061997e8dd --- /dev/null +++ b/src/client/testing/common/services/workspaceTestManagerService.ts @@ -0,0 +1,63 @@ +import { inject, injectable, named } from 'inversify'; +import { Disposable, OutputChannel, Uri, workspace } from 'vscode'; +import { IDisposableRegistry, IOutputChannel } from '../../../common/types'; +import { TEST_OUTPUT_CHANNEL } from './../constants'; +import { + ITestManager, + ITestManagerService, + ITestManagerServiceFactory, + IWorkspaceTestManagerService, + UnitTestProduct +} from './../types'; + +@injectable() +export class WorkspaceTestManagerService implements IWorkspaceTestManagerService, Disposable { + private workspaceTestManagers = new Map(); + constructor( + @inject(IOutputChannel) @named(TEST_OUTPUT_CHANNEL) private outChannel: OutputChannel, + @inject(ITestManagerServiceFactory) private testManagerServiceFactory: ITestManagerServiceFactory, + @inject(IDisposableRegistry) disposables: Disposable[] + ) { + disposables.push(this); + } + public dispose() { + this.workspaceTestManagers.forEach((info) => info.dispose()); + } + public getTestManager(resource: Uri): ITestManager | undefined { + const wkspace = this.getWorkspace(resource); + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath)!.getTestManager(); + } + public getTestWorkingDirectory(resource: Uri) { + const wkspace = this.getWorkspace(resource); + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath)!.getTestWorkingDirectory(); + } + public getPreferredTestManager(resource: Uri): UnitTestProduct | undefined { + const wkspace = this.getWorkspace(resource); + this.ensureTestManagerService(wkspace); + return this.workspaceTestManagers.get(wkspace.fsPath)!.getPreferredTestManager(); + } + private getWorkspace(resource: Uri): Uri { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + const noWkspaceMessage = 'Please open a workspace'; + this.outChannel.appendLine(noWkspaceMessage); + throw new Error(noWkspaceMessage); + } + if (!resource || workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } + const workspaceFolder = workspace.getWorkspaceFolder(resource); + if (workspaceFolder) { + return workspaceFolder.uri; + } + const message = `Resource '${resource.fsPath}' does not belong to any workspace`; + this.outChannel.appendLine(message); + throw new Error(message); + } + private ensureTestManagerService(wkspace: Uri) { + if (!this.workspaceTestManagers.has(wkspace.fsPath)) { + this.workspaceTestManagers.set(wkspace.fsPath, this.testManagerServiceFactory(wkspace)); + } + } +} diff --git a/src/client/testing/common/testUtils.ts b/src/client/testing/common/testUtils.ts new file mode 100644 index 000000000000..2aa97f6950d8 --- /dev/null +++ b/src/client/testing/common/testUtils.ts @@ -0,0 +1,607 @@ +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +import { Uri, workspace } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../../common/application/types'; +import * as constants from '../../common/constants'; +import { ITestingSettings, Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { TestDataItem, TestDataItemType, TestWorkspaceFolder } from '../types'; +import { CommandSource } from './constants'; +import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + ITestsHelper, + ITestVisitor, + TestFile, + TestFolder, + TestFunction, + TestProvider, + Tests, + TestSettingsPropertyNames, + TestsToRun, + TestSuite, + UnitTestProduct +} from './types'; + +export async function selectTestWorkspace(appShell: IApplicationShell): Promise { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } else if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } else { + const workspaceFolder = await appShell.showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspaceFolder ? workspaceFolder.uri : undefined; + } +} + +export function extractBetweenDelimiters(content: string, startDelimiter: string, endDelimiter: string): string { + content = content.substring(content.indexOf(startDelimiter) + startDelimiter.length); + return content.substring(0, content.lastIndexOf(endDelimiter)); +} + +export function convertFileToPackage(filePath: string): string { + const lastIndex = filePath.lastIndexOf('.'); + return filePath.substring(0, lastIndex).replace(/\//g, '.').replace(/\\/g, '.'); +} + +@injectable() +export class TestsHelper implements ITestsHelper { + private readonly appShell: IApplicationShell; + private readonly commandManager: ICommandManager; + constructor( + @inject(ITestVisitor) @named('TestFlatteningVisitor') private readonly flatteningVisitor: TestFlatteningVisitor, + @inject(IServiceContainer) serviceContainer: IServiceContainer + ) { + this.appShell = serviceContainer.get(IApplicationShell); + this.commandManager = serviceContainer.get(ICommandManager); + } + public parseProviderName(product: UnitTestProduct): TestProvider { + switch (product) { + case Product.nosetest: + return 'nosetest'; + case Product.pytest: + return 'pytest'; + case Product.unittest: + return 'unittest'; + default: { + throw new Error(`Unknown Test Product ${product}`); + } + } + } + public parseProduct(provider: TestProvider): UnitTestProduct { + switch (provider) { + case 'nosetest': + return Product.nosetest; + case 'pytest': + return Product.pytest; + case 'unittest': + return Product.unittest; + default: { + throw new Error(`Unknown Test Provider ${provider}`); + } + } + } + public getSettingsPropertyNames(product: UnitTestProduct): TestSettingsPropertyNames { + const id = this.parseProviderName(product); + switch (id) { + case 'pytest': { + return { + argsName: 'pytestArgs' as keyof ITestingSettings, + pathName: 'pytestPath' as keyof ITestingSettings, + enabledName: 'pytestEnabled' as keyof ITestingSettings + }; + } + case 'nosetest': { + return { + argsName: 'nosetestArgs' as keyof ITestingSettings, + pathName: 'nosetestPath' as keyof ITestingSettings, + enabledName: 'nosetestsEnabled' as keyof ITestingSettings + }; + } + case 'unittest': { + return { + argsName: 'unittestArgs' as keyof ITestingSettings, + enabledName: 'unittestEnabled' as keyof ITestingSettings + }; + } + default: { + throw new Error(`Unknown Test Provider '${product}'`); + } + } + } + public flattenTestFiles(testFiles: TestFile[], workspaceFolder: string): Tests { + testFiles.forEach((testFile) => this.flatteningVisitor.visitTestFile(testFile)); + + // tslint:disable-next-line:no-object-literal-type-assertion + const tests = { + testFiles: testFiles, + testFunctions: this.flatteningVisitor.flattenedTestFunctions, + testSuites: this.flatteningVisitor.flattenedTestSuites, + testFolders: [], + rootTestFolders: [], + summary: { passed: 0, failures: 0, errors: 0, skipped: 0 } + }; + + this.placeTestFilesIntoFolders(tests, workspaceFolder); + + return tests; + } + public placeTestFilesIntoFolders(tests: Tests, workspaceFolder: string): void { + // First get all the unique folders + const folders: string[] = []; + tests.testFiles.forEach((file) => { + const relativePath = path.relative(workspaceFolder, file.fullPath); + const dir = path.dirname(relativePath); + if (folders.indexOf(dir) === -1) { + folders.push(dir); + } + }); + + tests.testFolders = []; + const folderMap = new Map(); + folders.sort(); + const resource = Uri.file(workspaceFolder); + folders.forEach((dir) => { + dir.split(path.sep).reduce((parentPath, currentName, _index, _values) => { + let newPath = currentName; + let parentFolder: TestFolder | undefined; + if (parentPath.length > 0) { + parentFolder = folderMap.get(parentPath); + newPath = path.join(parentPath, currentName); + } + if (!folderMap.has(newPath)) { + const testFolder: TestFolder = { + resource, + name: newPath, + testFiles: [], + folders: [], + nameToRun: newPath, + time: 0, + functionsPassed: 0, + functionsFailed: 0, + functionsDidNotRun: 0 + }; + folderMap.set(newPath, testFolder); + if (parentFolder) { + parentFolder!.folders.push(testFolder); + } else { + tests.rootTestFolders.push(testFolder); + } + tests.testFiles + .filter((fl) => path.dirname(path.relative(workspaceFolder, fl.fullPath)) === newPath) + .forEach((testFile) => { + testFolder.testFiles.push(testFile); + }); + tests.testFolders.push(testFolder); + } + return newPath; + }, ''); + }); + } + public parseTestName(name: string, rootDirectory: string, tests: Tests): TestsToRun | undefined { + // tslint:disable-next-line:no-suspicious-comment + // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we. + // Use to identify a file given the full file name, similarly for a folder and function. + // Perhaps something like a parser or methods like TestFunction.fromString()... something). + if (!tests) { + return undefined; + } + const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); + const testFolders = tests.testFolders.filter( + (folder) => folder.nameToRun === name || folder.name === name || folder.name === absolutePath + ); + if (testFolders.length > 0) { + return { testFolder: testFolders }; + } + + const testFiles = tests.testFiles.filter( + (file) => file.nameToRun === name || file.name === name || file.fullPath === absolutePath + ); + if (testFiles.length > 0) { + return { testFile: testFiles }; + } + + const testFns = tests.testFunctions + .filter((fn) => fn.testFunction.nameToRun === name || fn.testFunction.name === name) + .map((fn) => fn.testFunction); + if (testFns.length > 0) { + return { testFunction: testFns }; + } + + // Just return this as a test file. + return { + testFile: [ + { + resource: Uri.file(rootDirectory), + name: name, + nameToRun: name, + functions: [], + suites: [], + xmlName: name, + fullPath: '', + time: 0, + functionsPassed: 0, + functionsFailed: 0, + functionsDidNotRun: 0 + } + ] + }; + } + public displayTestErrorMessage(message: string) { + this.appShell.showErrorMessage(message, constants.Button_Text_Tests_View_Output).then((action) => { + if (action === constants.Button_Text_Tests_View_Output) { + this.commandManager.executeCommand(constants.Commands.Tests_ViewOutput, undefined, CommandSource.ui); + } + }); + } + public mergeTests(items: Tests[]): Tests { + return items.reduce((tests, otherTests, index) => { + if (index === 0) { + return tests; + } + + tests.summary.errors += otherTests.summary.errors; + tests.summary.failures += otherTests.summary.failures; + tests.summary.passed += otherTests.summary.passed; + tests.summary.skipped += otherTests.summary.skipped; + tests.rootTestFolders.push(...otherTests.rootTestFolders); + tests.testFiles.push(...otherTests.testFiles); + tests.testFolders.push(...otherTests.testFolders); + tests.testFunctions.push(...otherTests.testFunctions); + tests.testSuites.push(...otherTests.testSuites); + + return tests; + }, items[0]); + } + + public shouldRunAllTests(testsToRun?: TestsToRun) { + if (!testsToRun) { + return true; + } + if ( + (Array.isArray(testsToRun.testFile) && testsToRun.testFile.length > 0) || + (Array.isArray(testsToRun.testFolder) && testsToRun.testFolder.length > 0) || + (Array.isArray(testsToRun.testFunction) && testsToRun.testFunction.length > 0) || + (Array.isArray(testsToRun.testSuite) && testsToRun.testSuite.length > 0) + ) { + return false; + } + + return true; + } +} + +export function getTestDataItemType(test: TestDataItem): TestDataItemType { + if (test instanceof TestWorkspaceFolder) { + return TestDataItemType.workspaceFolder; + } + if (getTestFile(test)) { + return TestDataItemType.file; + } + if (getTestFolder(test)) { + return TestDataItemType.folder; + } + if (getTestSuite(test)) { + return TestDataItemType.suite; + } + if (getTestFunction(test)) { + return TestDataItemType.function; + } + throw new Error('Unknown test type'); +} +export function getTestFile(test: TestDataItem): TestFile | undefined { + if (!test) { + return; + } + // Only TestFile has a `fullPath` property. + return typeof (test as TestFile).fullPath === 'string' ? (test as TestFile) : undefined; +} +export function getTestSuite(test: TestDataItem): TestSuite | undefined { + if (!test) { + return; + } + // Only TestSuite has a `suites` property. + return Array.isArray((test as TestSuite).suites) && !getTestFile(test) ? (test as TestSuite) : undefined; +} +export function getTestFolder(test: TestDataItem): TestFolder | undefined { + if (!test) { + return; + } + // Only TestFolder has a `folders` property. + return Array.isArray((test as TestFolder).folders) ? (test as TestFolder) : undefined; +} +export function getTestFunction(test: TestDataItem): TestFunction | undefined { + if (!test) { + return; + } + if (test instanceof TestWorkspaceFolder || getTestFile(test) || getTestFolder(test) || getTestSuite(test)) { + return; + } + return test as TestFunction; +} + +/** + * Gets the parent for a given test item. + * For test functions, this will return either a test suite or a test file. + * For test suites, this will return either a test suite or a test file. + * For test files, this will return a test folder. + * For a test folder, this will return either a test folder or `undefined`. + * @export + * @param {Tests} tests + * @param {TestDataItem} data + * @returns {(TestDataItem | undefined)} + */ +export function getParent(tests: Tests, data: TestDataItem): TestDataItem | undefined { + switch (getTestDataItemType(data)) { + case TestDataItemType.file: { + return getParentTestFolderForFile(tests, data as TestFile); + } + case TestDataItemType.folder: { + return getParentTestFolder(tests, data as TestFolder); + } + case TestDataItemType.suite: { + const suite = data as TestSuite; + if (isSubtestsParent(suite)) { + const fn = suite.functions[0]; + const parent = tests.testSuites.find((item) => item.testSuite.functions.indexOf(fn) >= 0); + if (parent) { + return parent.testSuite; + } + return tests.testFiles.find((item) => item.functions.indexOf(fn) >= 0); + } + const parentSuite = tests.testSuites.find((item) => item.testSuite.suites.indexOf(suite) >= 0); + if (parentSuite) { + return parentSuite.testSuite; + } + return tests.testFiles.find((item) => item.suites.indexOf(suite) >= 0); + } + case TestDataItemType.function: { + const fn = data as TestFunction; + if (fn.subtestParent) { + return fn.subtestParent.asSuite; + } + const parentSuite = tests.testSuites.find((item) => item.testSuite.functions.indexOf(fn) >= 0); + if (parentSuite) { + return parentSuite.testSuite; + } + return tests.testFiles.find((item) => item.functions.indexOf(fn) >= 0); + } + default: { + throw new Error('Unknown test type'); + } + } +} + +/** + * Returns the parent test folder give a given test file or folder. + * + * @export + * @param {Tests} tests + * @param {(TestFolder | TestFile)} item + * @returns {(TestFolder | undefined)} + */ +function getParentTestFolder(tests: Tests, item: TestFolder | TestFile): TestFolder | undefined { + if (getTestDataItemType(item) === TestDataItemType.folder) { + return getParentTestFolderForFolder(tests, item as TestFolder); + } + return getParentTestFolderForFile(tests, item as TestFile); +} + +/** + * Gets the parent test file for a test item. + * + * @param {Tests} tests + * @param {(TestSuite | TestFunction)} suite + * @returns {TestFile} + */ +export function getParentFile(tests: Tests, suite: TestSuite | TestFunction): TestFile { + let parent = getParent(tests, suite); + while (parent) { + if (getTestDataItemType(parent) === TestDataItemType.file) { + return parent as TestFile; + } + parent = getParent(tests, parent); + } + throw new Error('No parent file for provided test item'); +} +/** + * Gets the parent test suite for a suite/function. + * + * @param {Tests} tests + * @param {(TestSuite | TestFunction)} suite + * @returns {(TestSuite | undefined)} + */ +export function getParentSuite(tests: Tests, suite: TestSuite | TestFunction): TestSuite | undefined { + let parent = getParent(tests, suite); + while (parent) { + if (getTestDataItemType(parent) === TestDataItemType.suite) { + return parent as TestSuite; + } + parent = getParent(tests, parent); + } + return; +} + +/** + * Returns the parent test folder give a given test file. + * + * @param {Tests} tests + * @param {TestFile} file + * @returns {(TestFolder | undefined)} + */ +function getParentTestFolderForFile(tests: Tests, file: TestFile): TestFolder | undefined { + return tests.testFolders.find((folder) => folder.testFiles.some((item) => item === file)); +} + +/** + * Returns the parent test folder for a given test folder. + * + * @param {Tests} tests + * @param {TestFolder} folder + * @returns {(TestFolder | undefined)} + */ +function getParentTestFolderForFolder(tests: Tests, folder: TestFolder): TestFolder | undefined { + if (tests.rootTestFolders.indexOf(folder) >= 0) { + return; + } + return tests.testFolders.find((item) => item.folders.some((child) => child === folder)); +} + +/** + * Given a test function will return the corresponding flattened test function. + * + * @export + * @param {Tests} tests + * @param {TestFunction} func + * @returns {(FlattenedTestFunction | undefined)} + */ +export function findFlattendTestFunction(tests: Tests, func: TestFunction): FlattenedTestFunction | undefined { + return tests.testFunctions.find((f) => f.testFunction === func); +} + +/** + * Given a test suite, will return the corresponding flattened test suite. + * + * @export + * @param {Tests} tests + * @param {TestSuite} suite + * @returns {(FlattenedTestSuite | undefined)} + */ +export function findFlattendTestSuite(tests: Tests, suite: TestSuite): FlattenedTestSuite | undefined { + return tests.testSuites.find((f) => f.testSuite === suite); +} + +/** + * Returns the children of a given test data item. + * + * @export + * @param {Tests} tests + * @param {TestDataItem} item + * @returns {TestDataItem[]} + */ +export function getChildren(item: TestDataItem): TestDataItem[] { + switch (getTestDataItemType(item)) { + case TestDataItemType.folder: { + return [...(item as TestFolder).folders, ...(item as TestFolder).testFiles]; + } + case TestDataItemType.file: { + const [subSuites, functions] = divideSubtests((item as TestFile).functions); + return [...functions, ...(item as TestFile).suites, ...subSuites]; + } + case TestDataItemType.suite: { + let subSuites: TestSuite[] = []; + let functions = (item as TestSuite).functions; + if (!isSubtestsParent(item as TestSuite)) { + [subSuites, functions] = divideSubtests((item as TestSuite).functions); + } + return [...functions, ...(item as TestSuite).suites, ...subSuites]; + } + case TestDataItemType.function: { + return []; + } + default: { + throw new Error('Unknown Test Type'); + } + } +} + +function divideSubtests(mixed: TestFunction[]): [TestSuite[], TestFunction[]] { + const suites: TestSuite[] = []; + const functions: TestFunction[] = []; + mixed.forEach((func) => { + if (!func.subtestParent) { + functions.push(func); + return; + } + const parent = func.subtestParent.asSuite; + if (suites.indexOf(parent) < 0) { + suites.push(parent); + } + }); + return [suites, functions]; +} + +export function isSubtestsParent(suite: TestSuite): boolean { + const functions = suite.functions; + if (functions.length === 0) { + return false; + } + const subtestParent = functions[0].subtestParent; + if (subtestParent === undefined) { + return false; + } + return subtestParent.asSuite === suite; +} + +export function copyDesiredTestResults(source: Tests, target: Tests): void { + copyResultsForFolders(source.testFolders, target.testFolders); +} + +function copyResultsForFolders(source: TestFolder[], target: TestFolder[]): void { + source.forEach((sourceFolder) => { + const targetFolder = target.find( + (folder) => folder.name === sourceFolder.name && folder.nameToRun === sourceFolder.nameToRun + ); + if (!targetFolder) { + return; + } + copyValueTypes(sourceFolder, targetFolder); + copyResultsForFiles(sourceFolder.testFiles, targetFolder.testFiles); + // These should be reinitialized + targetFolder.functionsPassed = targetFolder.functionsDidNotRun = targetFolder.functionsFailed = 0; + }); +} +function copyResultsForFiles(source: TestFile[], target: TestFile[]): void { + source.forEach((sourceFile) => { + const targetFile = target.find((file) => file.name === sourceFile.name); + if (!targetFile) { + return; + } + copyValueTypes(sourceFile, targetFile); + copyResultsForFunctions(sourceFile.functions, targetFile.functions); + copyResultsForSuites(sourceFile.suites, targetFile.suites); + // These should be reinitialized + targetFile.functionsPassed = targetFile.functionsDidNotRun = targetFile.functionsFailed = 0; + }); +} + +function copyResultsForFunctions(source: TestFunction[], target: TestFunction[]): void { + source.forEach((sourceFn) => { + const targetFn = target.find((fn) => fn.name === sourceFn.name && fn.nameToRun === sourceFn.nameToRun); + if (!targetFn) { + return; + } + copyValueTypes(sourceFn, targetFn); + }); +} + +function copyResultsForSuites(source: TestSuite[], target: TestSuite[]): void { + source.forEach((sourceSuite) => { + const targetSuite = target.find( + (suite) => + suite.name === sourceSuite.name && + suite.nameToRun === sourceSuite.nameToRun && + suite.xmlName === sourceSuite.xmlName + ); + if (!targetSuite) { + return; + } + copyValueTypes(sourceSuite, targetSuite); + copyResultsForFunctions(sourceSuite.functions, targetSuite.functions); + copyResultsForSuites(sourceSuite.suites, targetSuite.suites); + // These should be reinitialized + targetSuite.functionsPassed = targetSuite.functionsDidNotRun = targetSuite.functionsFailed = 0; + }); +} + +function copyValueTypes(source: T, target: T): void { + Object.keys(source).forEach((key) => { + // tslint:disable-next-line:no-any + const value = (source as any)[key]; + if (['boolean', 'number', 'string', 'undefined'].indexOf(typeof value) >= 0) { + // tslint:disable-next-line:no-any + (target as any)[key] = value; + } + }); +} diff --git a/src/client/testing/common/testVisitors/flatteningVisitor.ts b/src/client/testing/common/testVisitors/flatteningVisitor.ts new file mode 100644 index 000000000000..bca5cc0cc271 --- /dev/null +++ b/src/client/testing/common/testVisitors/flatteningVisitor.ts @@ -0,0 +1,76 @@ +import { injectable } from 'inversify'; +import { convertFileToPackage } from '../testUtils'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + ITestVisitor, + TestFile, + TestFolder, + TestFunction, + TestSuite +} from '../types'; + +@injectable() +export class TestFlatteningVisitor implements ITestVisitor { + // tslint:disable-next-line:variable-name + private _flattedTestFunctions = new Map(); + // tslint:disable-next-line:variable-name + private _flattenedTestSuites = new Map(); + public get flattenedTestFunctions(): FlattenedTestFunction[] { + return [...this._flattedTestFunctions.values()]; + } + public get flattenedTestSuites(): FlattenedTestSuite[] { + return [...this._flattenedTestSuites.values()]; + } + // tslint:disable-next-line:no-empty + public visitTestFunction(_testFunction: TestFunction): void {} + // tslint:disable-next-line:no-empty + public visitTestSuite(_testSuite: TestSuite): void {} + public visitTestFile(testFile: TestFile): void { + // sample test_three (file name without extension and all / replaced with ., meaning this is the package) + const packageName = convertFileToPackage(testFile.name); + + testFile.functions.forEach((fn) => this.addTestFunction(fn, testFile, packageName)); + testFile.suites.forEach((suite) => this.visitTestSuiteOfAFile(suite, testFile)); + } + // tslint:disable-next-line:no-empty + public visitTestFolder(_testFile: TestFolder) {} + private visitTestSuiteOfAFile(testSuite: TestSuite, parentTestFile: TestFile): void { + testSuite.functions.forEach((fn) => this.visitTestFunctionOfASuite(fn, testSuite, parentTestFile)); + testSuite.suites.forEach((suite) => this.visitTestSuiteOfAFile(suite, parentTestFile)); + this.addTestSuite(testSuite, parentTestFile); + } + private visitTestFunctionOfASuite( + testFunction: TestFunction, + parentTestSuite: TestSuite, + parentTestFile: TestFile + ) { + const key = `Function:${testFunction.name},Suite:${parentTestSuite.name},SuiteXmlName:${parentTestSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedFunction = { + testFunction, + xmlClassName: parentTestSuite.xmlName, + parentTestFile, + parentTestSuite + }; + this._flattedTestFunctions.set(key, flattenedFunction); + } + private addTestSuite(testSuite: TestSuite, parentTestFile: TestFile) { + const key = `Suite:${testSuite.name},SuiteXmlName:${testSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; + if (this._flattenedTestSuites.has(key)) { + return; + } + const flattenedSuite = { parentTestFile, testSuite, xmlClassName: testSuite.xmlName }; + this._flattenedTestSuites.set(key, flattenedSuite); + } + private addTestFunction(testFunction: TestFunction, parentTestFile: TestFile, parentTestPackage: string) { + const key = `Function:${testFunction.name},ParentFile:${parentTestFile.fullPath}`; + if (this._flattedTestFunctions.has(key)) { + return; + } + const flattendFunction = { testFunction, xmlClassName: parentTestPackage, parentTestFile }; + this._flattedTestFunctions.set(key, flattendFunction); + } +} diff --git a/src/client/testing/common/testVisitors/resultResetVisitor.ts b/src/client/testing/common/testVisitors/resultResetVisitor.ts new file mode 100644 index 000000000000..5b4113bd43e5 --- /dev/null +++ b/src/client/testing/common/testVisitors/resultResetVisitor.ts @@ -0,0 +1,40 @@ +import { injectable } from 'inversify'; +import { ITestVisitor, TestFile, TestFolder, TestFunction, TestStatus, TestSuite } from '../types'; + +@injectable() +export class TestResultResetVisitor implements ITestVisitor { + public visitTestFunction(testFunction: TestFunction): void { + testFunction.passed = undefined; + testFunction.time = 0; + testFunction.message = ''; + testFunction.traceback = ''; + testFunction.status = TestStatus.Unknown; + testFunction.functionsFailed = 0; + testFunction.functionsPassed = 0; + testFunction.functionsDidNotRun = 0; + } + public visitTestSuite(testSuite: TestSuite): void { + testSuite.passed = undefined; + testSuite.time = 0; + testSuite.status = TestStatus.Unknown; + testSuite.functionsFailed = 0; + testSuite.functionsPassed = 0; + testSuite.functionsDidNotRun = 0; + } + public visitTestFile(testFile: TestFile): void { + testFile.passed = undefined; + testFile.time = 0; + testFile.status = TestStatus.Unknown; + testFile.functionsFailed = 0; + testFile.functionsPassed = 0; + testFile.functionsDidNotRun = 0; + } + public visitTestFolder(testFolder: TestFolder) { + testFolder.functionsDidNotRun = 0; + testFolder.functionsFailed = 0; + testFolder.functionsPassed = 0; + testFolder.passed = undefined; + testFolder.time = 0; + testFolder.status = TestStatus.Unknown; + } +} diff --git a/src/client/testing/common/testVisitors/visitor.ts b/src/client/testing/common/testVisitors/visitor.ts new file mode 100644 index 000000000000..6b511f5200be --- /dev/null +++ b/src/client/testing/common/testVisitors/visitor.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { TestDataItem } from '../../types'; +import { getChildren, getParent } from '../testUtils'; +import { Tests } from '../types'; + +export type Visitor = (item: TestDataItem) => void; + +/** + * Vists tests recursively. + * + * @export + * @param {Tests} tests + * @param {Visitor} visitor + */ +export function visitRecursive(tests: Tests, visitor: Visitor): void; + +/** + * Vists tests recursively. + * + * @export + * @param {Tests} tests + * @param {TestDataItem} start + * @param {Visitor} visitor + */ +export function visitRecursive(tests: Tests, start: TestDataItem, visitor: Visitor): void; +export function visitRecursive(tests: Tests, arg1: TestDataItem | Visitor, arg2?: Visitor): void { + const startItem = typeof arg1 === 'function' ? undefined : (arg1 as TestDataItem); + const visitor = startItem ? arg2! : (arg1 as Visitor); + let children: TestDataItem[] = []; + if (startItem) { + visitor(startItem); + children = getChildren(startItem); + } else { + children = tests.rootTestFolders; + } + children.forEach((folder) => visitRecursive(tests, folder, visitor)); +} + +/** + * Visits parents recursively. + * + * @export + * @param {Tests} tests + * @param {TestDataItem} startItem + * @param {Visitor} visitor + * @returns {void} + */ +export function visitParentsRecursive(tests: Tests, startItem: TestDataItem, visitor: Visitor): void { + visitor(startItem); + const parent = getParent(tests, startItem); + if (!parent) { + return; + } + visitor(parent); + visitParentsRecursive(tests, parent, visitor); +} diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts new file mode 100644 index 000000000000..009afc50d311 --- /dev/null +++ b/src/client/testing/common/types.ts @@ -0,0 +1,337 @@ +import { + CancellationToken, + DebugConfiguration, + DiagnosticCollection, + Disposable, + Event, + OutputChannel, + Uri +} from 'vscode'; +import { ITestingSettings, Product } from '../../common/types'; +import { DebuggerTypeName } from '../../debugger/constants'; +import { ConsoleType } from '../../debugger/types'; +import { IPythonTestMessage, TestDataItem, WorkspaceTestStatus } from '../types'; +import { CommandSource } from './constants'; + +export type TestProvider = 'nosetest' | 'pytest' | 'unittest'; + +export type UnitTestProduct = Product.nosetest | Product.pytest | Product.unittest; + +export type TestSettingsPropertyNames = { + enabledName: keyof ITestingSettings; + argsName: keyof ITestingSettings; + pathName?: keyof ITestingSettings; +}; + +export type TestDiscoveryOptions = { + workspaceFolder: Uri; + cwd: string; + args: string[]; + token: CancellationToken; + ignoreCache: boolean; + outChannel: OutputChannel; +}; + +export type TestRunOptions = { + workspaceFolder: Uri; + cwd: string; + tests: Tests; + args: string[]; + testsToRun?: TestsToRun; + token: CancellationToken; + outChannel?: OutputChannel; + debug?: boolean; +}; + +export type UnitTestParserOptions = TestDiscoveryOptions & { startDirectory: string }; + +export type LaunchOptions = { + cwd: string; + args: string[]; + testProvider: TestProvider; + token?: CancellationToken; + outChannel?: OutputChannel; +}; + +export type ParserOptions = TestDiscoveryOptions; + +export type Options = { + workspaceFolder: Uri; + cwd: string; + args: string[]; + outChannel?: OutputChannel; + token: CancellationToken; +}; + +export type TestsToRun = { + testFolder?: TestFolder[]; + testFile?: TestFile[]; + testSuite?: TestSuite[]; + testFunction?: TestFunction[]; +}; + +//***************** +// test results + +export enum TestingType { + folder = 'folder', + file = 'file', + suite = 'suite', + function = 'function' +} + +export enum TestStatus { + Unknown = 'Unknown', + Discovering = 'Discovering', + Idle = 'Idle', + Running = 'Running', + Fail = 'Fail', + Error = 'Error', + Skipped = 'Skipped', + Pass = 'Pass' +} + +export type TestResult = { + status?: TestStatus; + passed?: boolean; + time: number; + line?: number; + file?: string; + message?: string; + traceback?: string; + functionsPassed?: number; + functionsFailed?: number; + functionsDidNotRun?: number; +}; + +export type TestingNode = TestResult & { + name: string; + nameToRun: string; + resource: Uri; +}; + +export type TestFolder = TestingNode & { + folders: TestFolder[]; + testFiles: TestFile[]; +}; + +export type TestingXMLNode = TestingNode & { + xmlName: string; +}; + +export type TestFile = TestingXMLNode & { + fullPath: string; + functions: TestFunction[]; + suites: TestSuite[]; + errorsWhenDiscovering?: string; +}; + +export type TestSuite = TestingXMLNode & { + functions: TestFunction[]; + suites: TestSuite[]; + isUnitTest: Boolean; + isInstance: Boolean; +}; + +export type TestFunction = TestingNode & { + subtestParent?: SubtestParent; +}; + +export type SubtestParent = TestResult & { + name: string; + nameToRun: string; + asSuite: TestSuite; +}; + +export type FlattenedTestFunction = { + testFunction: TestFunction; + parentTestSuite?: TestSuite; + parentTestFile: TestFile; + xmlClassName: string; +}; + +export type FlattenedTestSuite = { + testSuite: TestSuite; + parentTestFile: TestFile; + xmlClassName: string; +}; + +export type TestSummary = { + passed: number; + failures: number; + errors: number; + skipped: number; +}; + +export type Tests = { + summary: TestSummary; + testFiles: TestFile[]; + testFunctions: FlattenedTestFunction[]; + testSuites: FlattenedTestSuite[]; + testFolders: TestFolder[]; + rootTestFolders: TestFolder[]; +}; + +//***************** +// interfaces + +export interface ITestManagerService extends Disposable { + getTestManager(): ITestManager | undefined; + getTestWorkingDirectory(): string; + getPreferredTestManager(): UnitTestProduct | undefined; +} + +export const IWorkspaceTestManagerService = Symbol('IWorkspaceTestManagerService'); +export interface IWorkspaceTestManagerService extends Disposable { + getTestManager(resource: Uri): ITestManager | undefined; + getTestWorkingDirectory(resource: Uri): string; + getPreferredTestManager(resource: Uri): UnitTestProduct | undefined; +} + +export const ITestsHelper = Symbol('ITestsHelper'); +export interface ITestsHelper { + parseProviderName(product: UnitTestProduct): TestProvider; + parseProduct(provider: TestProvider): UnitTestProduct; + getSettingsPropertyNames(product: Product): TestSettingsPropertyNames; + flattenTestFiles(testFiles: TestFile[], workspaceFolder: string): Tests; + placeTestFilesIntoFolders(tests: Tests, workspaceFolder: string): void; + displayTestErrorMessage(message: string): void; + shouldRunAllTests(testsToRun?: TestsToRun): boolean; + mergeTests(items: Tests[]): Tests; +} + +export const ITestVisitor = Symbol('ITestVisitor'); +export interface ITestVisitor { + visitTestFunction(testFunction: TestFunction): void; + visitTestSuite(testSuite: TestSuite): void; + visitTestFile(testFile: TestFile): void; + visitTestFolder(testFile: TestFolder): void; +} + +export const ITestCollectionStorageService = Symbol('ITestCollectionStorageService'); +export interface ITestCollectionStorageService extends Disposable { + onDidChange: Event<{ uri: Uri; data?: TestDataItem }>; + getTests(wkspace: Uri): Tests | undefined; + storeTests(wkspace: Uri, tests: Tests | null | undefined): void; + findFlattendTestFunction(resource: Uri, func: TestFunction): FlattenedTestFunction | undefined; + findFlattendTestSuite(resource: Uri, suite: TestSuite): FlattenedTestSuite | undefined; + update(resource: Uri, item: TestDataItem): void; +} + +export const ITestResultsService = Symbol('ITestResultsService'); +export interface ITestResultsService { + resetResults(tests: Tests): void; + updateResults(tests: Tests): void; +} + +export const ITestDebugLauncher = Symbol('ITestDebugLauncher'); +export interface ITestDebugLauncher { + launchDebugger(options: LaunchOptions): Promise; +} + +export const ITestManagerFactory = Symbol('ITestManagerFactory'); +export interface ITestManagerFactory extends Function { + // tslint:disable-next-line:callable-types + (testProvider: TestProvider, workspaceFolder: Uri, rootDirectory: string): ITestManager; +} + +export const ITestManagerServiceFactory = Symbol('TestManagerServiceFactory'); +export interface ITestManagerServiceFactory extends Function { + // tslint:disable-next-line:callable-types + (workspaceFolder: Uri): ITestManagerService; +} + +export const ITestManager = Symbol('ITestManager'); +export interface ITestManager extends Disposable { + readonly status: TestStatus; + readonly enabled: boolean; + readonly workingDirectory: string; + readonly workspaceFolder: Uri; + diagnosticCollection: DiagnosticCollection; + readonly onDidStatusChange: Event; + stop(): void; + resetTestResults(): void; + discoverTests( + cmdSource: CommandSource, + ignoreCache?: boolean, + quietMode?: boolean, + userInitiated?: boolean, + clearTestStatus?: boolean + ): Promise; + runTest( + cmdSource: CommandSource, + testsToRun?: TestsToRun, + runFailedTests?: boolean, + debug?: boolean + ): Promise; +} + +export const ITestDiscoveryService = Symbol('ITestDiscoveryService'); +export interface ITestDiscoveryService { + discoverTests(options: TestDiscoveryOptions): Promise; +} + +export const ITestsParser = Symbol('ITestsParser'); +export interface ITestsParser { + parse(content: string, options: ParserOptions): Tests; +} + +export const IUnitTestSocketServer = Symbol('IUnitTestSocketServer'); +export interface IUnitTestSocketServer extends Disposable { + on(event: string | symbol, listener: Function): this; + removeListener(event: string | symbol, listener: Function): this; + removeAllListeners(event?: string | symbol): this; + start(options?: { port?: number; host?: string }): Promise; + stop(): void; +} + +export const ITestRunner = Symbol('ITestRunner'); +export interface ITestRunner { + run(testProvider: TestProvider, options: Options): Promise; +} + +export const IXUnitParser = Symbol('IXUnitParser'); +export interface IXUnitParser { + // Update "tests" with the results parsed from the given file. + updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string): Promise; +} + +export const ITestMessageService = Symbol('ITestMessageService'); +export interface ITestMessageService { + getFilteredTestMessages(rootDirectory: string, testResults: Tests): Promise; +} + +export interface ITestDebugConfig extends DebugConfiguration { + type: typeof DebuggerTypeName; + request: 'test'; + + pythonPath?: string; + console?: ConsoleType; + cwd?: string; + env?: Record; + envFile?: string; + + // converted to DebugOptions: + stopOnEntry?: boolean; + showReturnValue?: boolean; + redirectOutput?: boolean; // default: true + debugStdLib?: boolean; + justMyCode?: boolean; + subProcess?: boolean; +} + +export const ITestContextService = Symbol('ITestContextService'); +export interface ITestContextService extends Disposable { + register(): void; +} + +export const ITestsStatusUpdaterService = Symbol('ITestsStatusUpdaterService'); +export interface ITestsStatusUpdaterService { + updateStatusAsDiscovering(resource: Uri, tests?: Tests): void; + updateStatusAsUnknown(resource: Uri, tests?: Tests): void; + updateStatusAsRunning(resource: Uri, tests?: Tests): void; + updateStatusAsRunningFailedTests(resource: Uri, tests?: Tests): void; + updateStatusAsRunningSpecificTests(resource: Uri, testsToRun: TestsToRun, tests?: Tests): void; + updateStatusOfRunningTestsAsIdle(resource: Uri, tests?: Tests): void; + triggerUpdatesToTests(resource: Uri, tests?: Tests): void; +} diff --git a/src/client/testing/common/updateTestSettings.ts b/src/client/testing/common/updateTestSettings.ts new file mode 100644 index 000000000000..dc549bdd4fae --- /dev/null +++ b/src/client/testing/common/updateTestSettings.ts @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { applyEdits, findNodeAtLocation, getNodeValue, ModificationOptions, modify, parseTree } from 'jsonc-parser'; +import * as path from 'path'; +import { IExtensionActivationService, LanguageServerType } from '../../activation/types'; +import { IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; +import '../../common/extensions'; +import { traceDecorators, traceError } from '../../common/logger'; +import { IFileSystem } from '../../common/platform/types'; +import { Resource } from '../../common/types'; +import { swallowExceptions } from '../../common/utils/decorators'; + +// tslint:disable-next-line:no-suspicious-comment +// TODO: rename the class since it is not used just for test settings +@injectable() +export class UpdateTestSettingService implements IExtensionActivationService { + constructor( + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IApplicationEnvironment) private readonly application: IApplicationEnvironment, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService + ) {} + public async activate(resource: Resource): Promise { + this.updateTestSettings(resource).ignoreErrors(); + } + @traceDecorators.error('Failed to update test settings') + public async updateTestSettings(resource: Resource): Promise { + const filesToBeFixed = await this.getFilesToBeFixed(resource); + await Promise.all(filesToBeFixed.map((file) => this.fixSettingInFile(file))); + } + public getSettingsFiles(resource: Resource): string[] { + const settingsFiles: string[] = []; + if (this.application.userSettingsFile) { + settingsFiles.push(this.application.userSettingsFile); + } + const workspaceFolder = this.workspace.getWorkspaceFolder(resource); + if (workspaceFolder) { + settingsFiles.push(path.join(workspaceFolder.uri.fsPath, '.vscode', 'settings.json')); + } + return settingsFiles; + } + public async getFilesToBeFixed(resource: Resource): Promise { + const files = this.getSettingsFiles(resource); + const result = await Promise.all( + files.map(async (file) => { + const needsFixing = await this.doesFileNeedToBeFixed(file); + return { file, needsFixing }; + }) + ); + return result.filter((item) => item.needsFixing).map((item) => item.file); + } + // fixLanguageServerSetting provided for tests so not all tests have to + // deal with potential whitespace changes. + @swallowExceptions('Failed to update settings.json') + public async fixSettingInFile(filePath: string, fixLanguageServerSetting = true): Promise { + let fileContents = await this.fs.readFile(filePath); + + const setting = new RegExp('"python\\.unitTest', 'g'); + fileContents = fileContents.replace(setting, '"python.testing'); + + const setting_pytest_enabled = new RegExp('\\.pyTestEnabled"', 'g'); + const setting_pytest_args = new RegExp('\\.pyTestArgs"', 'g'); + const setting_pytest_path = new RegExp('\\.pyTestPath"', 'g'); + fileContents = fileContents.replace(setting_pytest_enabled, '.pytestEnabled"'); + fileContents = fileContents.replace(setting_pytest_args, '.pytestArgs"'); + fileContents = fileContents.replace(setting_pytest_path, '.pytestPath"'); + + const setting_pep8_args = new RegExp('\\.(? { + try { + const contents = await this.fs.readFile(filePath); + return ( + contents.indexOf('python.jediEnabled') > 0 || + contents.indexOf('python.unitTest.') > 0 || + contents.indexOf('.pyTest') > 0 || + contents.indexOf('.pep8') > 0 + ); + } catch (ex) { + traceError('Failed to check if file needs to be fixed', ex); + return false; + } + } + + private fixLanguageServerSettings(fileContent: string): string { + // `python.jediEnabled` is deprecated: + // - If missing, do nothing. + // - `true`, then set to `languageServer: Jedi`. + // - `false` and `languageServer` is present, do nothing. + // - `false` and `languageServer` is NOT present, set `languageServer` to `Microsoft`. + // `jediEnabled` is NOT removed since JSONC parser may also remove comments. + const jediEnabledPath = ['python.jediEnabled']; + const languageServerPath = ['python.languageServer']; + + try { + const ast = parseTree(fileContent); + + const jediEnabledNode = findNodeAtLocation(ast, jediEnabledPath); + const languageServerNode = findNodeAtLocation(ast, languageServerPath); + + // If missing, do nothing. + if (!jediEnabledNode) { + return fileContent; + } + + const jediEnabled = getNodeValue(jediEnabledNode); + + const modificationOptions: ModificationOptions = { + formattingOptions: { + tabSize: 4, + insertSpaces: true + } + }; + + // `jediEnabled` is true, set it to Jedi. + if (jediEnabled) { + return applyEdits( + fileContent, + modify(fileContent, languageServerPath, LanguageServerType.Jedi, modificationOptions) + ); + } + + // `jediEnabled` is false. if languageServer is missing, set it to Microsoft. + if (!languageServerNode) { + return applyEdits( + fileContent, + modify(fileContent, languageServerPath, LanguageServerType.Microsoft, modificationOptions) + ); + } + + // tslint:disable-next-line:no-empty + } catch {} + return fileContent; + } +} diff --git a/src/client/testing/common/xUnitParser.ts b/src/client/testing/common/xUnitParser.ts new file mode 100644 index 000000000000..76ed830672ba --- /dev/null +++ b/src/client/testing/common/xUnitParser.ts @@ -0,0 +1,183 @@ +import { inject, injectable } from 'inversify'; +import { IFileSystem } from '../../common/platform/types'; +import { FlattenedTestFunction, IXUnitParser, TestFunction, TestResult, Tests, TestStatus, TestSummary } from './types'; + +type TestSuiteResult = { + $: { + errors: string; + failures: string; + name: string; + skips: string; + skip: string; + tests: string; + time: string; + }; + testcase: TestCaseResult[]; +}; +type TestCaseResult = { + $: { + classname: string; + file: string; + line: string; + name: string; + time: string; + }; + failure: { + _: string; + $: { message: string; type: string }; + }[]; + error: { + _: string; + $: { message: string; type: string }; + }[]; + skipped: { + _: string; + $: { message: string; type: string }; + }[]; +}; + +// tslint:disable-next-line:no-any +function getSafeInt(value: string, defaultValue: any = 0): number { + const num = parseInt(value, 10); + if (isNaN(num)) { + return defaultValue; + } + return num; +} + +@injectable() +export class XUnitParser implements IXUnitParser { + constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {} + + // Update "tests" with the results parsed from the given file. + public async updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string) { + const data = await this.fs.readFile(outputXmlFile); + + const parserResult = await parseXML(data); + const junitResults = getJunitResults(parserResult); + if (junitResults) { + updateTests(tests, junitResults); + } + } +} + +// An async wrapper around xml2js.parseString(). +// tslint:disable-next-line:no-any +async function parseXML(data: string): Promise { + const xml2js = await import('xml2js'); + // tslint:disable-next-line:no-any + return new Promise((resolve, reject) => { + // tslint:disable-next-line:no-any + xml2js.parseString(data, (error: Error, result: any) => { + if (error) { + return reject(error); + } + return resolve(result); + }); + }); +} + +// Return the actual test results from the given data. +// tslint:disable-next-line:no-any +function getJunitResults(parserResult: any): TestSuiteResult | undefined { + // This is the newer JUnit XML format (e.g. pytest 5.1 and later). + const fullResults = parserResult as { testsuites: { testsuite: TestSuiteResult[] } }; + if (!fullResults.testsuites) { + return (parserResult as { testsuite: TestSuiteResult }).testsuite; + } + + const junitSuites = fullResults.testsuites.testsuite; + if (!Array.isArray(junitSuites)) { + throw Error('bad JUnit XML data'); + } + if (junitSuites.length === 0) { + return; + } + if (junitSuites.length > 1) { + throw Error('got multiple XML results'); + } + return junitSuites[0]; +} + +// Update "tests" with the given results. +function updateTests(tests: Tests, testSuiteResult: TestSuiteResult) { + updateSummary(tests.summary, testSuiteResult); + + if (!Array.isArray(testSuiteResult.testcase)) { + return; + } + + // Update the results for each test. + // Previously unknown tests are ignored. + testSuiteResult.testcase.forEach((testcase: TestCaseResult) => { + const testFunc = findTestFunction(tests.testFunctions, testcase.$.classname, testcase.$.name); + if (testFunc) { + updateResultInfo(testFunc, testcase); + updateResultStatus(testFunc, testcase); + } else { + // Possible we're dealing with nosetests, where the file name isn't returned to us + // When dealing with nose tests + // It is possible to have a test file named x in two separate test sub directories and have same functions/classes + // And unforutnately xunit log doesn't ouput the filename + + // result = tests.testFunctions.find(fn => fn.testFunction.name === testcase.$.name && + // fn.parentTestSuite && fn.parentTestSuite.name === testcase.$.classname); + + // Look for failed file test + const fileTest = testcase.$.file && tests.testFiles.find((file) => file.nameToRun === testcase.$.file); + if (fileTest && testcase.error) { + updateResultStatus(fileTest, testcase); + } + } + }); +} + +// Update the summary with the information in the given results. +function updateSummary(summary: TestSummary, testSuiteResult: TestSuiteResult) { + summary.errors = getSafeInt(testSuiteResult.$.errors); + summary.failures = getSafeInt(testSuiteResult.$.failures); + summary.skipped = getSafeInt(testSuiteResult.$.skips ? testSuiteResult.$.skips : testSuiteResult.$.skip); + const testCount = getSafeInt(testSuiteResult.$.tests); + summary.passed = testCount - summary.failures - summary.skipped - summary.errors; +} + +function findTestFunction( + candidates: FlattenedTestFunction[], + className: string, + funcName: string +): TestFunction | undefined { + const xmlClassName = className.replace(/\(\)/g, '').replace(/\.\./g, '.').replace(/\.\./g, '.').replace(/\.+$/, ''); + const flattened = candidates.find((fn) => fn.xmlClassName === xmlClassName && fn.testFunction.name === funcName); + if (!flattened) { + return; + } + return flattened.testFunction; +} + +function updateResultInfo(result: TestResult, testCase: TestCaseResult) { + result.file = testCase.$.file; + result.line = getSafeInt(testCase.$.line, null); + result.time = parseFloat(testCase.$.time); +} + +function updateResultStatus(result: TestResult, testCase: TestCaseResult) { + if (testCase.error) { + result.status = TestStatus.Error; + result.passed = false; + result.message = testCase.error[0].$.message; + result.traceback = testCase.error[0]._; + } else if (testCase.failure) { + result.status = TestStatus.Fail; + result.passed = false; + result.message = testCase.failure[0].$.message; + result.traceback = testCase.failure[0]._; + } else if (testCase.skipped) { + result.status = TestStatus.Skipped; + result.passed = undefined; + result.message = testCase.skipped[0].$.message; + result.traceback = ''; + } else { + result.status = TestStatus.Pass; + result.passed = true; + } +} diff --git a/src/client/testing/configuration.ts b/src/client/testing/configuration.ts new file mode 100644 index 000000000000..6c7adde7ede1 --- /dev/null +++ b/src/client/testing/configuration.ts @@ -0,0 +1,157 @@ +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../common/application/types'; +import { traceError } from '../common/logger'; +import { IConfigurationService, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { TestConfiguringTelemetry, TestTool } from '../telemetry/types'; +import { BufferedTestConfigSettingsService } from './common/services/configSettingService'; +import { ITestsHelper, UnitTestProduct } from './common/types'; +import { + ITestConfigSettingsService, + ITestConfigurationManager, + ITestConfigurationManagerFactory, + ITestConfigurationService +} from './types'; + +@injectable() +export class UnitTestConfigurationService implements ITestConfigurationService { + private readonly configurationService: IConfigurationService; + private readonly appShell: IApplicationShell; + private readonly workspaceService: IWorkspaceService; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.configurationService = serviceContainer.get(IConfigurationService); + this.appShell = serviceContainer.get(IApplicationShell); + this.workspaceService = serviceContainer.get(IWorkspaceService); + } + public async displayTestFrameworkError(wkspace: Uri): Promise { + const settings = this.configurationService.getSettings(wkspace); + let enabledCount = settings.testing.pytestEnabled ? 1 : 0; + enabledCount += settings.testing.nosetestsEnabled ? 1 : 0; + enabledCount += settings.testing.unittestEnabled ? 1 : 0; + if (enabledCount > 1) { + return this._promptToEnableAndConfigureTestFramework( + wkspace, + 'Enable only one of the test frameworks (unittest, pytest or nosetest).', + true + ); + } else { + const option = 'Enable and configure a Test Framework'; + const item = await this.appShell.showInformationMessage( + 'No test framework configured (unittest, pytest or nosetest)', + option + ); + if (item === option) { + return this._promptToEnableAndConfigureTestFramework(wkspace); + } + return Promise.reject(null); + } + } + public async selectTestRunner(placeHolderMessage: string): Promise { + const items = [ + { + label: 'unittest', + product: Product.unittest, + description: 'Standard Python test framework', + detail: 'https://docs.python.org/3/library/unittest.html' + }, + { + label: 'pytest', + product: Product.pytest, + description: 'pytest framework', + // tslint:disable-next-line:no-http-string + detail: 'http://docs.pytest.org/' + }, + { + label: 'nose', + product: Product.nosetest, + description: 'nose framework', + detail: 'https://nose.readthedocs.io/' + } + ]; + const options = { + ignoreFocusOut: true, + matchOnDescription: true, + matchOnDetail: true, + placeHolder: placeHolderMessage + }; + const selectedTestRunner = await this.appShell.showQuickPick(items, options); + // tslint:disable-next-line:prefer-type-cast + return selectedTestRunner ? (selectedTestRunner.product as UnitTestProduct) : undefined; + } + public async enableTest(wkspace: Uri, product: UnitTestProduct): Promise { + const factory = this.serviceContainer.get(ITestConfigurationManagerFactory); + const configMgr = factory.create(wkspace, product); + return this._enableTest(wkspace, configMgr); + } + + public async promptToEnableAndConfigureTestFramework(wkspace: Uri) { + await this._promptToEnableAndConfigureTestFramework(wkspace, undefined, false, 'commandpalette'); + } + + private _enableTest(wkspace: Uri, configMgr: ITestConfigurationManager) { + const pythonConfig = this.workspaceService.getConfiguration('python', wkspace); + if (pythonConfig.get('testing.promptToConfigure')) { + return configMgr.enable(); + } + return pythonConfig.update('testing.promptToConfigure', undefined).then( + () => { + return configMgr.enable(); + }, + (reason) => { + return configMgr.enable().then(() => Promise.reject(reason)); + } + ); + } + + private async _promptToEnableAndConfigureTestFramework( + wkspace: Uri, + messageToDisplay: string = 'Select a test framework/tool to enable', + enableOnly: boolean = false, + trigger: 'ui' | 'commandpalette' = 'ui' + ) { + const telemetryProps: TestConfiguringTelemetry = { + trigger: trigger, + failed: false + }; + try { + const selectedTestRunner = await this.selectTestRunner(messageToDisplay); + if (typeof selectedTestRunner !== 'number') { + return Promise.reject(null); + } + const helper = this.serviceContainer.get(ITestsHelper); + telemetryProps.tool = helper.parseProviderName(selectedTestRunner) as TestTool; + const delayed = new BufferedTestConfigSettingsService(); + const factory = this.serviceContainer.get( + ITestConfigurationManagerFactory + ); + const configMgr = factory.create(wkspace, selectedTestRunner, delayed); + if (enableOnly) { + await configMgr.enable(); + } else { + // Configure everything before enabling. + // Cuz we don't want the test engine (in main.ts file - tests get discovered when config changes are detected) + // to start discovering tests when tests haven't been configured properly. + await configMgr + .configure(wkspace) + .then(() => this._enableTest(wkspace, configMgr)) + .catch((reason) => { + return this._enableTest(wkspace, configMgr).then(() => Promise.reject(reason)); + }); + } + const cfg = this.serviceContainer.get(ITestConfigSettingsService); + try { + await delayed.apply(cfg); + } catch (exc) { + traceError('Python Extension: applying unit test config updates', exc); + telemetryProps.failed = true; + } + } finally { + sendTelemetryEvent(EventName.UNITTEST_CONFIGURING, undefined, telemetryProps); + } + } +} diff --git a/src/client/testing/configurationFactory.ts b/src/client/testing/configurationFactory.ts new file mode 100644 index 000000000000..72a9065260e9 --- /dev/null +++ b/src/client/testing/configurationFactory.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import * as nose from './nosetest/testConfigurationManager'; +import * as pytest from './pytest/testConfigurationManager'; +import { ITestConfigSettingsService, ITestConfigurationManager, ITestConfigurationManagerFactory } from './types'; +import * as unittest from './unittest/testConfigurationManager'; + +@injectable() +export class TestConfigurationManagerFactory implements ITestConfigurationManagerFactory { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + public create(wkspace: Uri, product: Product, cfg?: ITestConfigSettingsService): ITestConfigurationManager { + switch (product) { + case Product.unittest: { + return new unittest.ConfigurationManager(wkspace, this.serviceContainer, cfg); + } + case Product.pytest: { + return new pytest.ConfigurationManager(wkspace, this.serviceContainer, cfg); + } + case Product.nosetest: { + return new nose.ConfigurationManager(wkspace, this.serviceContainer, cfg); + } + default: { + throw new Error('Invalid test configuration'); + } + } + } +} diff --git a/src/client/testing/display/main.ts b/src/client/testing/display/main.ts new file mode 100644 index 000000000000..319ec5d2e3f9 --- /dev/null +++ b/src/client/testing/display/main.ts @@ -0,0 +1,224 @@ +'use strict'; +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter, StatusBarAlignment, StatusBarItem } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../../common/application/types'; +import * as constants from '../../common/constants'; +import { isNotInstalledError } from '../../common/helpers'; +import { traceError } from '../../common/logger'; +import { IConfigurationService } from '../../common/types'; +import { Testing } from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { IServiceContainer } from '../../ioc/types'; +import { captureTelemetry } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { CANCELLATION_REASON } from '../common/constants'; +import { ITestsHelper, Tests } from '../common/types'; +import { ITestResultDisplay } from '../types'; + +@injectable() +export class TestResultDisplay implements ITestResultDisplay { + private statusBar: StatusBarItem; + private discoverCounter = 0; + private ticker = ['|', '/', '-', '|', '/', '-', '\\']; + private progressTimeout: NodeJS.Timer | number | null = null; + private _enabled: boolean = false; + private progressPrefix!: string; + private readonly didChange = new EventEmitter(); + private readonly appShell: IApplicationShell; + private readonly testsHelper: ITestsHelper; + private readonly cmdManager: ICommandManager; + public get onDidChange(): Event { + return this.didChange.event; + } + + // tslint:disable-next-line:no-any + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.appShell = serviceContainer.get(IApplicationShell); + this.statusBar = this.appShell.createStatusBarItem(StatusBarAlignment.Left); + this.testsHelper = serviceContainer.get(ITestsHelper); + this.cmdManager = serviceContainer.get(ICommandManager); + } + public dispose() { + this.clearProgressTicker(); + this.statusBar.dispose(); + } + public get enabled() { + return this._enabled; + } + public set enabled(enable: boolean) { + this._enabled = enable; + if (enable) { + this.statusBar.show(); + } else { + this.statusBar.hide(); + } + } + public displayProgressStatus(testRunResult: Promise, debug: boolean = false) { + this.displayProgress( + 'Running Tests', + 'Running Tests (Click to Stop)', + constants.Commands.Tests_Ask_To_Stop_Test + ); + testRunResult + .then((tests) => this.updateTestRunWithSuccess(tests, debug)) + .catch(this.updateTestRunWithFailure.bind(this)) + // We don't care about any other exceptions returned by updateTestRunWithFailure + .catch(noop); + } + public displayDiscoverStatus(testDiscovery: Promise, quietMode: boolean = false) { + this.displayProgress( + 'Discovering Tests', + 'Discovering tests (click to stop)', + constants.Commands.Tests_Ask_To_Stop_Discovery + ); + return testDiscovery + .then((tests) => { + this.updateWithDiscoverSuccess(tests, quietMode); + return tests; + }) + .catch((reason) => { + this.updateWithDiscoverFailure(reason); + return Promise.reject(reason); + }); + } + + private updateTestRunWithSuccess(tests: Tests, debug: boolean = false): Tests { + this.clearProgressTicker(); + + // Treat errors as a special case, as we generally wouldn't have any errors + const statusText: string[] = []; + const toolTip: string[] = []; + + if (tests.summary.passed > 0) { + statusText.push(`${constants.Octicons.Test_Pass} ${tests.summary.passed}`); + toolTip.push(`${tests.summary.passed} Passed`); + } + if (tests.summary.skipped > 0) { + statusText.push(`${constants.Octicons.Test_Skip} ${tests.summary.skipped}`); + toolTip.push(`${tests.summary.skipped} Skipped`); + } + if (tests.summary.failures > 0) { + statusText.push(`${constants.Octicons.Test_Fail} ${tests.summary.failures}`); + toolTip.push(`${tests.summary.failures} Failed`); + } + if (tests.summary.errors > 0) { + statusText.push(`${constants.Octicons.Test_Error} ${tests.summary.errors}`); + toolTip.push(`${tests.summary.errors} Error${tests.summary.errors > 1 ? 's' : ''}`); + } + this.statusBar.tooltip = toolTip.length === 0 ? 'No Tests Ran' : `${toolTip.join(', ')} (Tests)`; + this.statusBar.text = statusText.length === 0 ? 'No Tests Ran' : statusText.join(' '); + this.statusBar.command = constants.Commands.Tests_View_UI; + this.didChange.fire(); + if (statusText.length === 0 && !debug) { + this.appShell.showWarningMessage('No tests ran, please check the configuration settings for the tests.'); + } + return tests; + } + + // tslint:disable-next-line:no-any + private updateTestRunWithFailure(reason: any): Promise { + this.clearProgressTicker(); + this.statusBar.command = constants.Commands.Tests_View_UI; + if (reason === CANCELLATION_REASON) { + this.statusBar.text = '$(zap) Run Tests'; + this.statusBar.tooltip = 'Run Tests'; + } else { + this.statusBar.text = '$(alert) Tests Failed'; + this.statusBar.tooltip = 'Running Tests Failed'; + this.testsHelper.displayTestErrorMessage('There was an error in running the tests.'); + } + return Promise.reject(reason); + } + + private displayProgress(message: string, tooltip: string, command: string) { + this.progressPrefix = this.statusBar.text = `$(stop) ${message}`; + this.statusBar.command = command; + this.statusBar.tooltip = tooltip; + this.statusBar.show(); + this.clearProgressTicker(); + this.progressTimeout = setInterval(() => this.updateProgressTicker(), 1000); + } + private updateProgressTicker() { + const text = `${this.progressPrefix} ${this.ticker[this.discoverCounter % 7]}`; + this.discoverCounter += 1; + this.statusBar.text = text; + } + private clearProgressTicker() { + if (this.progressTimeout) { + // tslint:disable-next-line: no-any + clearInterval(this.progressTimeout as any); + } + this.progressTimeout = null; + this.discoverCounter = 0; + } + + @captureTelemetry(EventName.UNITTEST_DISABLE) + // tslint:disable-next-line:no-any + private async disableTests(): Promise { + const configurationService = this.serviceContainer.get(IConfigurationService); + const settingsToDisable = [ + 'testing.promptToConfigure', + 'testing.pytestEnabled', + 'testing.unittestEnabled', + 'testing.nosetestsEnabled' + ]; + + for (const setting of settingsToDisable) { + await configurationService.updateSetting(setting, false).catch(noop); + } + this.cmdManager.executeCommand('setContext', 'testsDiscovered', false); + } + + private updateWithDiscoverSuccess(tests: Tests, quietMode: boolean = false) { + this.clearProgressTicker(); + const haveTests = tests && tests.testFunctions.length > 0; + this.statusBar.text = '$(zap) Run Tests'; + this.statusBar.tooltip = 'Run Tests'; + this.statusBar.command = constants.Commands.Tests_View_UI; + this.statusBar.show(); + if (this.didChange) { + this.didChange.fire(); + } + + if (!haveTests && !quietMode) { + this.appShell + .showInformationMessage( + 'No tests discovered, please check the configuration settings for the tests.', + Testing.disableTests(), + Testing.configureTests() + ) + .then((item) => { + if (item === Testing.disableTests()) { + this.disableTests().catch((ex) => traceError('Python Extension: disableTests', ex)); + } else if (item === Testing.configureTests()) { + this.cmdManager + .executeCommand(constants.Commands.Tests_Configure, undefined, undefined, undefined) + .then(noop); + } + }); + } + } + + // tslint:disable-next-line:no-any + private updateWithDiscoverFailure(reason: any) { + this.clearProgressTicker(); + this.statusBar.text = '$(zap) Discover Tests'; + this.statusBar.tooltip = 'Discover Tests'; + this.statusBar.command = constants.Commands.Tests_Discover; + this.statusBar.show(); + if (reason !== CANCELLATION_REASON) { + this.statusBar.text = '$(alert) Test discovery failed'; + this.statusBar.tooltip = "Discovering Tests failed (view 'Python Test Log' output panel for details)"; + // tslint:disable-next-line:no-suspicious-comment + // TODO: ignore this quitemode, always display the error message (inform the user). + if (!isNotInstalledError(reason)) { + // tslint:disable-next-line:no-suspicious-comment + // TODO: show an option that will invoke a command 'python.test.configureTest' or similar. + // This will be hanlded by main.ts that will capture input from user and configure the tests. + this.appShell.showErrorMessage( + 'Test discovery error, please check the configuration settings for the tests.' + ); + } + } + } +} diff --git a/src/client/testing/display/picker.ts b/src/client/testing/display/picker.ts new file mode 100644 index 000000000000..7ad8657d337f --- /dev/null +++ b/src/client/testing/display/picker.ts @@ -0,0 +1,353 @@ +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { QuickPickItem, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../../common/application/types'; +import * as constants from '../../common/constants'; +import { IFileSystem } from '../../common/platform/types'; +import { IServiceContainer } from '../../ioc/types'; +import { CommandSource } from '../common/constants'; +import { + FlattenedTestFunction, + ITestCollectionStorageService, + TestFile, + TestFunction, + Tests, + TestStatus, + TestsToRun +} from '../common/types'; +import { ITestDisplay } from '../types'; + +@injectable() +export class TestDisplay implements ITestDisplay { + private readonly testCollectionStorage: ITestCollectionStorageService; + private readonly appShell: IApplicationShell; + constructor( + @inject(IServiceContainer) private readonly serviceRegistry: IServiceContainer, + @inject(ICommandManager) private readonly commandManager: ICommandManager + ) { + this.testCollectionStorage = serviceRegistry.get(ITestCollectionStorageService); + this.appShell = serviceRegistry.get(IApplicationShell); + } + public displayStopTestUI(workspace: Uri, message: string) { + this.appShell.showQuickPick([message]).then((item) => { + if (item === message) { + this.commandManager.executeCommand(constants.Commands.Tests_Stop, undefined, workspace); + } + }); + } + public displayTestUI(cmdSource: CommandSource, wkspace: Uri) { + const tests = this.testCollectionStorage.getTests(wkspace); + this.appShell + .showQuickPick(buildItems(tests), { matchOnDescription: true, matchOnDetail: true }) + .then((item) => + item ? onItemSelected(this.commandManager, cmdSource, wkspace, item, false) : Promise.resolve() + ); + } + public selectTestFunction(rootDirectory: string, tests: Tests): Promise { + return new Promise((resolve, reject) => { + this.appShell + .showQuickPick(buildItemsForFunctions(rootDirectory, tests.testFunctions), { + matchOnDescription: true, + matchOnDetail: true + }) + .then((item) => { + if (item && item.fn) { + return resolve(item.fn); + } + return reject(); + }, reject); + }); + } + public selectTestFile(rootDirectory: string, tests: Tests): Promise { + return new Promise((resolve, reject) => { + this.appShell + .showQuickPick(buildItemsForTestFiles(rootDirectory, tests.testFiles), { + matchOnDescription: true, + matchOnDetail: true + }) + .then((item) => { + if (item && item.testFile) { + return resolve(item.testFile); + } + return reject(); + }, reject); + }); + } + public displayFunctionTestPickerUI( + cmdSource: CommandSource, + wkspace: Uri, + rootDirectory: string, + file: Uri, + testFunctions: TestFunction[], + debug?: boolean + ) { + const tests = this.testCollectionStorage.getTests(wkspace); + if (!tests) { + return; + } + const fileName = file.fsPath; + const fs = this.serviceRegistry.get(IFileSystem); + const testFile = tests.testFiles.find( + (item) => item.name === fileName || fs.arePathsSame(item.fullPath, fileName) + ); + if (!testFile) { + return; + } + const flattenedFunctions = tests.testFunctions.filter((fn) => { + return ( + fn.parentTestFile.name === testFile.name && + testFunctions.some((testFunc) => testFunc.nameToRun === fn.testFunction.nameToRun) + ); + }); + const runAllItem = buildRunAllParametrizedItem(flattenedFunctions, debug); + const functionItems = buildItemsForFunctions(rootDirectory, flattenedFunctions, undefined, undefined, debug); + this.appShell + .showQuickPick(runAllItem.concat(...functionItems), { matchOnDescription: true, matchOnDetail: true }) + .then((testItem) => + testItem ? onItemSelected(this.commandManager, cmdSource, wkspace, testItem, debug) : Promise.resolve() + ); + } +} + +export enum Type { + RunAll = 0, + ReDiscover = 1, + RunFailed = 2, + RunFolder = 3, + RunFile = 4, + RunClass = 5, + RunMethod = 6, + ViewTestOutput = 7, + Null = 8, + SelectAndRunMethod = 9, + DebugMethod = 10, + Configure = 11, + RunParametrized = 12 +} +const statusIconMapping = new Map(); +statusIconMapping.set(TestStatus.Pass, constants.Octicons.Test_Pass); +statusIconMapping.set(TestStatus.Fail, constants.Octicons.Test_Fail); +statusIconMapping.set(TestStatus.Error, constants.Octicons.Test_Error); +statusIconMapping.set(TestStatus.Skipped, constants.Octicons.Test_Skip); + +type TestItem = QuickPickItem & { + type: Type; + fn?: FlattenedTestFunction; + fns?: TestFunction[]; +}; + +type TestFileItem = QuickPickItem & { + type: Type; + testFile?: TestFile; +}; + +function getSummary(tests?: Tests) { + if (!tests || !tests.summary) { + return ''; + } + const statusText: string[] = []; + if (tests.summary.passed > 0) { + statusText.push(`${constants.Octicons.Test_Pass} ${tests.summary.passed} Passed`); + } + if (tests.summary.failures > 0) { + statusText.push(`${constants.Octicons.Test_Fail} ${tests.summary.failures} Failed`); + } + if (tests.summary.errors > 0) { + const plural = tests.summary.errors === 1 ? '' : 's'; + statusText.push(`${constants.Octicons.Test_Error} ${tests.summary.errors} Error${plural}`); + } + if (tests.summary.skipped > 0) { + statusText.push(`${constants.Octicons.Test_Skip} ${tests.summary.skipped} Skipped`); + } + return statusText.join(', ').trim(); +} +function buildItems(tests?: Tests): TestItem[] { + const items: TestItem[] = []; + items.push({ description: '', label: 'Run All Tests', type: Type.RunAll }); + items.push({ description: '', label: 'Discover Tests', type: Type.ReDiscover }); + items.push({ description: '', label: 'Run Test Method ...', type: Type.SelectAndRunMethod }); + items.push({ description: '', label: 'Configure Tests', type: Type.Configure }); + + const summary = getSummary(tests); + items.push({ description: '', label: 'View Test Output', type: Type.ViewTestOutput, detail: summary }); + + if (tests && tests.summary.failures > 0) { + items.push({ + description: '', + label: 'Run Failed Tests', + type: Type.RunFailed, + detail: `${constants.Octicons.Test_Fail} ${tests.summary.failures} Failed` + }); + } + + return items; +} + +const statusSortPrefix = { + [TestStatus.Error]: '1', + [TestStatus.Fail]: '2', + [TestStatus.Skipped]: '3', + [TestStatus.Pass]: '4', + [TestStatus.Discovering]: undefined, + [TestStatus.Idle]: undefined, + [TestStatus.Running]: undefined, + [TestStatus.Unknown]: undefined +}; + +function buildRunAllParametrizedItem(tests: FlattenedTestFunction[], debug: boolean = false): TestItem[] { + const testFunctions: TestFunction[] = []; + tests.forEach((fn) => { + testFunctions.push(fn.testFunction); + }); + return [ + { + description: '', + label: debug ? 'Debug All' : 'Run All', + type: Type.RunParametrized, + fns: testFunctions + } + ]; +} +function buildItemsForFunctions( + rootDirectory: string, + tests: FlattenedTestFunction[], + sortBasedOnResults: boolean = false, + displayStatusIcons: boolean = false, + debug: boolean = false +): TestItem[] { + const functionItems: TestItem[] = []; + tests.forEach((fn) => { + let icon = ''; + if (displayStatusIcons && fn.testFunction.status && statusIconMapping.has(fn.testFunction.status)) { + icon = `${statusIconMapping.get(fn.testFunction.status)} `; + } + + functionItems.push({ + description: '', + detail: path.relative(rootDirectory, fn.parentTestFile.fullPath), + label: icon + fn.testFunction.name, + type: debug === true ? Type.DebugMethod : Type.RunMethod, + fn: fn + }); + }); + functionItems.sort((a, b) => { + let sortAPrefix = '5-'; + let sortBPrefix = '5-'; + if (sortBasedOnResults && a.fn && a.fn.testFunction.status && b.fn && b.fn.testFunction.status) { + sortAPrefix = statusSortPrefix[a.fn.testFunction.status] + ? statusSortPrefix[a.fn.testFunction.status]! + : sortAPrefix; + sortBPrefix = statusSortPrefix[b.fn.testFunction.status] + ? statusSortPrefix[b.fn.testFunction.status]! + : sortBPrefix; + } + if (`${sortAPrefix}${a.detail}${a.label}` < `${sortBPrefix}${b.detail}${b.label}`) { + return -1; + } + if (`${sortAPrefix}${a.detail}${a.label}` > `${sortBPrefix}${b.detail}${b.label}`) { + return 1; + } + return 0; + }); + return functionItems; +} +function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): TestFileItem[] { + const fileItems: TestFileItem[] = testFiles.map((testFile) => { + return { + description: '', + detail: path.relative(rootDirectory, testFile.fullPath), + type: Type.RunFile, + label: path.basename(testFile.fullPath), + testFile: testFile + }; + }); + fileItems.sort((a, b) => { + if (!a.detail && !b.detail) { + return 0; + } + if (!a.detail || a.detail < b.detail!) { + return -1; + } + if (!b.detail || a.detail! > b.detail) { + return 1; + } + return 0; + }); + return fileItems; +} +export function onItemSelected( + commandManager: ICommandManager, + cmdSource: CommandSource, + wkspace: Uri, + selection: TestItem, + debug?: boolean +) { + if (!selection || typeof selection.type !== 'number') { + return; + } + switch (selection.type) { + case Type.Null: { + return; + } + case Type.RunAll: { + return commandManager.executeCommand( + constants.Commands.Tests_Run, + undefined, + cmdSource, + wkspace, + undefined + ); + } + case Type.RunParametrized: { + return commandManager.executeCommand( + constants.Commands.Tests_Run_Parametrized, + undefined, + cmdSource, + wkspace, + selection.fns!, + debug! + ); + } + case Type.ReDiscover: { + return commandManager.executeCommand(constants.Commands.Tests_Discover, undefined, cmdSource, wkspace); + } + case Type.ViewTestOutput: { + return commandManager.executeCommand(constants.Commands.Tests_ViewOutput, undefined, cmdSource); + } + case Type.RunFailed: { + return commandManager.executeCommand(constants.Commands.Tests_Run_Failed, undefined, cmdSource, wkspace); + } + case Type.SelectAndRunMethod: { + const cmd = debug + ? constants.Commands.Tests_Select_And_Debug_Method + : constants.Commands.Tests_Select_And_Run_Method; + return commandManager.executeCommand(cmd, undefined, cmdSource, wkspace); + } + case Type.RunMethod: { + const testsToRun: TestsToRun = { testFunction: [selection.fn!.testFunction] }; + return commandManager.executeCommand( + constants.Commands.Tests_Run, + undefined, + cmdSource, + wkspace, + testsToRun + ); + } + case Type.DebugMethod: { + const testsToRun: TestsToRun = { testFunction: [selection.fn!.testFunction] }; + return commandManager.executeCommand( + constants.Commands.Tests_Debug, + undefined, + cmdSource, + wkspace, + testsToRun + ); + } + case Type.Configure: { + return commandManager.executeCommand(constants.Commands.Tests_Configure, undefined, cmdSource, wkspace); + } + default: { + return; + } + } +} diff --git a/src/client/testing/explorer/commandHandlers.ts b/src/client/testing/explorer/commandHandlers.ts new file mode 100644 index 000000000000..2dd96039c491 --- /dev/null +++ b/src/client/testing/explorer/commandHandlers.ts @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { ICommandManager } from '../../common/application/types'; +import { Commands } from '../../common/constants'; +import { traceDecorators } from '../../common/logger'; +import { IDisposable } from '../../common/types'; +import { swallowExceptions } from '../../common/utils/decorators'; +import { CommandSource } from '../common/constants'; +import { getTestDataItemType } from '../common/testUtils'; +import { TestFile, TestFolder, TestFunction, TestsToRun, TestSuite } from '../common/types'; +import { ITestExplorerCommandHandler } from '../navigation/types'; +import { ITestDataItemResource, TestDataItem, TestDataItemType } from '../types'; + +type NavigationCommands = + | typeof Commands.navigateToTestFile + | typeof Commands.navigateToTestFunction + | typeof Commands.navigateToTestSuite; +const testNavigationCommandMapping: { [key: string]: NavigationCommands } = { + [TestDataItemType.file]: Commands.navigateToTestFile, + [TestDataItemType.function]: Commands.navigateToTestFunction, + [TestDataItemType.suite]: Commands.navigateToTestSuite +}; + +@injectable() +export class TestExplorerCommandHandler implements ITestExplorerCommandHandler { + private readonly disposables: IDisposable[] = []; + constructor( + @inject(ICommandManager) private readonly cmdManager: ICommandManager, + @inject(ITestDataItemResource) private readonly testResource: ITestDataItemResource + ) {} + public register(): void { + this.disposables.push(this.cmdManager.registerCommand(Commands.runTestNode, this.onRunTestNode, this)); + this.disposables.push(this.cmdManager.registerCommand(Commands.debugTestNode, this.onDebugTestNode, this)); + this.disposables.push( + this.cmdManager.registerCommand(Commands.openTestNodeInEditor, this.onOpenTestNodeInEditor, this) + ); + } + public dispose(): void { + this.disposables.forEach((item) => item.dispose()); + } + @swallowExceptions('Run test node') + @traceDecorators.error('Run test node failed') + protected async onRunTestNode(item: TestDataItem): Promise { + await this.runDebugTestNode(item, 'run'); + } + @swallowExceptions('Debug test node') + @traceDecorators.error('Debug test node failed') + protected async onDebugTestNode(item: TestDataItem): Promise { + await this.runDebugTestNode(item, 'debug'); + } + @swallowExceptions('Open test node in Editor') + @traceDecorators.error('Open test node in editor failed') + protected async onOpenTestNodeInEditor(item: TestDataItem): Promise { + const testType = getTestDataItemType(item); + if (testType === TestDataItemType.folder) { + throw new Error('Unknown Test Type'); + } + const command = testNavigationCommandMapping[testType]; + const testUri = this.testResource.getResource(item); + if (!command) { + throw new Error('Unknown Test Type'); + } + this.cmdManager.executeCommand(command, testUri, item, true); + } + + protected async runDebugTestNode(item: TestDataItem, runType: 'run' | 'debug'): Promise { + let testToRun: TestsToRun; + + switch (getTestDataItemType(item)) { + case TestDataItemType.file: { + testToRun = { testFile: [item as TestFile] }; + break; + } + case TestDataItemType.folder: { + testToRun = { testFolder: [item as TestFolder] }; + break; + } + case TestDataItemType.suite: { + testToRun = { testSuite: [item as TestSuite] }; + break; + } + case TestDataItemType.function: { + testToRun = { testFunction: [item as TestFunction] }; + break; + } + default: + throw new Error('Unknown Test Type'); + } + const testUri = this.testResource.getResource(item); + const cmd = runType === 'run' ? Commands.Tests_Run : Commands.Tests_Debug; + this.cmdManager.executeCommand(cmd, undefined, CommandSource.testExplorer, testUri, testToRun); + } +} diff --git a/src/client/testing/explorer/failedTestHandler.ts b/src/client/testing/explorer/failedTestHandler.ts new file mode 100644 index 000000000000..ee2ae7a3ba7b --- /dev/null +++ b/src/client/testing/explorer/failedTestHandler.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { ICommandManager } from '../../common/application/types'; +import { Commands } from '../../common/constants'; +import '../../common/extensions'; +import { IDisposable, IDisposableRegistry } from '../../common/types'; +import { debounceAsync } from '../../common/utils/decorators'; +import { getTestDataItemType } from '../common/testUtils'; +import { ITestCollectionStorageService, TestStatus } from '../common/types'; +import { TestDataItem, TestDataItemType } from '../types'; + +@injectable() +export class FailedTestHandler implements IExtensionSingleActivationService, IDisposable { + private readonly disposables: IDisposable[] = []; + private readonly failedItems: TestDataItem[] = []; + constructor( + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService + ) { + disposableRegistry.push(this); + } + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + public async activate(): Promise { + this.storage.onDidChange(this.onDidChangeTestData, this, this.disposables); + } + public onDidChangeTestData(args: { uri: Uri; data?: TestDataItem }): void { + if ( + args.data && + (args.data.status === TestStatus.Error || args.data.status === TestStatus.Fail) && + getTestDataItemType(args.data) === TestDataItemType.function + ) { + this.failedItems.push(args.data); + this.revealFailedNodes().ignoreErrors(); + } + } + + @debounceAsync(500) + private async revealFailedNodes(): Promise { + while (this.failedItems.length > 0) { + const item = this.failedItems.pop()!; + await this.commandManager.executeCommand(Commands.Test_Reveal_Test_Item, item); + } + } +} diff --git a/src/client/testing/explorer/testTreeViewItem.ts b/src/client/testing/explorer/testTreeViewItem.ts new file mode 100644 index 000000000000..a9c48b67d1a0 --- /dev/null +++ b/src/client/testing/explorer/testTreeViewItem.ts @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-classes-per-file + +import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { Commands } from '../../common/constants'; +import { getIcon } from '../../common/utils/icons'; +import { noop } from '../../common/utils/misc'; +import { Icons } from '../common/constants'; +import { getTestDataItemType, isSubtestsParent } from '../common/testUtils'; +import { TestResult, TestStatus, TestSuite } from '../common/types'; +import { TestDataItem, TestDataItemType } from '../types'; + +function getDefaultCollapsibleState(data: TestDataItem): TreeItemCollapsibleState { + return getTestDataItemType(data) === TestDataItemType.function + ? TreeItemCollapsibleState.None + : TreeItemCollapsibleState.Collapsed; +} + +/** + * Class that represents a visual node on the + * Test Explorer tree view. Is essentially a wrapper for the underlying + * TestDataItem. + */ +export class TestTreeItem extends TreeItem { + public readonly testType: TestDataItemType; + + constructor( + public readonly resource: Uri, + public readonly data: Readonly, + collapsibleStatue: TreeItemCollapsibleState = getDefaultCollapsibleState(data) + ) { + super(data.name, collapsibleStatue); + this.testType = getTestDataItemType(this.data); + this.setCommand(); + } + + // @ts-ignore https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-rc/#properties-overridding-accessors-and-vice-versa-is-an-error + public get contextValue(): string { + return this.testType; + } + + // @ts-ignore https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-rc/#properties-overridding-accessors-and-vice-versa-is-an-error + public get iconPath(): string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon { + if (this.testType === TestDataItemType.workspaceFolder) { + return ThemeIcon.Folder; + } + if (!this.data) { + return ''; + } + const status = this.data.status; + switch (status) { + case TestStatus.Error: + case TestStatus.Fail: { + return getIcon(Icons.failed); + } + case TestStatus.Pass: { + return getIcon(Icons.passed); + } + case TestStatus.Discovering: + case TestStatus.Running: { + return getIcon(Icons.discovering); + } + case TestStatus.Idle: + case TestStatus.Unknown: { + return getIcon(Icons.unknown); + } + default: { + return getIcon(Icons.unknown); + } + } + } + + // @ts-ignore https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-rc/#properties-overridding-accessors-and-vice-versa-is-an-error + public get tooltip(): string { + if (!this.data || this.testType === TestDataItemType.workspaceFolder) { + return ''; + } + const result = this.data as TestResult; + if ( + !result.status || + result.status === TestStatus.Idle || + result.status === TestStatus.Unknown || + result.status === TestStatus.Skipped + ) { + return ''; + } + if (this.testType !== TestDataItemType.function) { + if (result.functionsPassed === undefined) { + return ''; + } + if (result.functionsDidNotRun) { + return `${result.functionsFailed} failed, ${result.functionsDidNotRun} not run and ${result.functionsPassed} passed`; + } + return `${result.functionsFailed} failed, ${result.functionsPassed} passed`; + } + switch (this.data.status) { + case TestStatus.Error: + case TestStatus.Fail: { + return `Failed in ${+result.time.toFixed(3)} seconds`; + } + case TestStatus.Pass: { + return `Passed in ${+result.time.toFixed(3)} seconds`; + } + case TestStatus.Discovering: + case TestStatus.Running: { + return 'Loading...'; + } + default: { + return ''; + } + } + } + + /** + * Tooltip for our tree nodes is the test status + */ + public get testStatus(): string { + return this.data.status ? this.data.status : TestStatus.Unknown; + } + + private setCommand() { + switch (this.testType) { + case TestDataItemType.file: { + this.command = { + command: Commands.navigateToTestFile, + title: 'Open', + arguments: [this.resource, this.data] + }; + break; + } + case TestDataItemType.function: { + this.command = { + command: Commands.navigateToTestFunction, + title: 'Open', + arguments: [this.resource, this.data, false] + }; + break; + } + case TestDataItemType.suite: { + if (isSubtestsParent(this.data as TestSuite)) { + this.command = { + command: Commands.navigateToTestFunction, + title: 'Open', + arguments: [this.resource, this.data, false] + }; + break; + } + this.command = { + command: Commands.navigateToTestSuite, + title: 'Open', + arguments: [this.resource, this.data, false] + }; + break; + } + default: { + noop(); + } + } + } +} diff --git a/src/client/testing/explorer/testTreeViewProvider.ts b/src/client/testing/explorer/testTreeViewProvider.ts new file mode 100644 index 000000000000..aa5aa4405f5a --- /dev/null +++ b/src/client/testing/explorer/testTreeViewProvider.ts @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Event, EventEmitter, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { ICommandManager, IWorkspaceService } from '../../common/application/types'; +import { Commands } from '../../common/constants'; +import { IDisposable, IDisposableRegistry } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { CommandSource } from '../common/constants'; +import { getChildren, getParent, getTestDataItemType } from '../common/testUtils'; +import { ITestCollectionStorageService, Tests, TestStatus } from '../common/types'; +import { + ITestDataItemResource, + ITestManagementService, + ITestTreeViewProvider, + TestDataItem, + TestDataItemType, + TestWorkspaceFolder, + WorkspaceTestStatus +} from '../types'; +import { TestTreeItem } from './testTreeViewItem'; + +@injectable() +export class TestTreeViewProvider implements ITestTreeViewProvider, ITestDataItemResource, IDisposable { + public readonly onDidChangeTreeData: Event; + public readonly discovered = new Set(); + public readonly testsAreBeingDiscovered: Map; + + private _onDidChangeTreeData = new EventEmitter(); + private disposables: IDisposable[] = []; + + constructor( + @inject(ITestCollectionStorageService) private testStore: ITestCollectionStorageService, + @inject(ITestManagementService) private testService: ITestManagementService, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry + ) { + this.onDidChangeTreeData = this._onDidChangeTreeData.event; + + disposableRegistry.push(this); + this.testsAreBeingDiscovered = new Map(); + this.disposables.push(this.testService.onDidStatusChange(this.onTestStatusChanged, this)); + this.testStore.onDidChange((e) => this._onDidChangeTreeData.fire(e.data), this, this.disposables); + this.workspace.onDidChangeWorkspaceFolders( + () => this._onDidChangeTreeData.fire(undefined), + this, + this.disposables + ); + + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + this.refresh(workspace.workspaceFolders[0].uri); + } + } + + /** + * We need a way to map a given TestDataItem to a Uri, so that other consumers (such + * as the commandHandler for the Test Explorer) have a way of accessing the Uri outside + * the purview off the TestTreeView. + * + * @param testData Test data item to map to a Uri + * @returns A Uri representing the workspace that the test data item exists within + */ + public getResource(testData: Readonly): Uri { + return testData.resource; + } + + /** + * As the TreeViewProvider itself is getting disposed, ensure all registered listeners are disposed + * from our internal emitter. + */ + public dispose() { + this.disposables.forEach((d) => d.dispose()); + this._onDidChangeTreeData.dispose(); + } + + /** + * Get [TreeItem](#TreeItem) representation of the `element` + * + * @param element The element for which [TreeItem](#TreeItem) representation is asked for. + * @return [TreeItem](#TreeItem) representation of the element + */ + public async getTreeItem(element: TestDataItem): Promise { + const defaultCollapsibleState = (await this.shouldElementBeExpandedByDefault(element)) + ? TreeItemCollapsibleState.Expanded + : undefined; + return new TestTreeItem(element.resource, element, defaultCollapsibleState); + } + + /** + * Get the children of `element` or root if no element is passed. + * + * @param element The element from which the provider gets children. Can be `undefined`. + * @return Children of `element` or root if no element is passed. + */ + public async getChildren(element?: TestDataItem): Promise { + if (element) { + if (element instanceof TestWorkspaceFolder) { + let tests = this.testStore.getTests(element.workspaceFolder.uri); + if (!tests && !this.discovered.has(element.workspaceFolder.uri.fsPath)) { + this.discovered.add(element.workspaceFolder.uri.fsPath); + await this.commandManager.executeCommand( + Commands.Tests_Discover, + element, + CommandSource.testExplorer, + undefined + ); + tests = this.testStore.getTests(element.workspaceFolder.uri); + } + return this.getRootNodes(tests); + } + return getChildren(element!); + } + + if (!Array.isArray(this.workspace.workspaceFolders) || this.workspace.workspaceFolders.length === 0) { + return []; + } + + sendTelemetryEvent(EventName.UNITTEST_EXPLORER_WORK_SPACE_COUNT, undefined, { + count: this.workspace.workspaceFolders.length + }); + + // If we are in a single workspace + if (this.workspace.workspaceFolders.length === 1) { + const tests = this.testStore.getTests(this.workspace.workspaceFolders[0].uri); + return this.getRootNodes(tests); + } + + // If we are in a mult-root workspace, then nest the test data within a + // virtual node, represending the workspace folder. + return this.workspace.workspaceFolders.map((workspaceFolder) => new TestWorkspaceFolder(workspaceFolder)); + } + + /** + * Optional method to return the parent of `element`. + * Return `null` or `undefined` if `element` is a child of root. + * + * **NOTE:** This method should be implemented in order to access [reveal](#TreeView.reveal) API. + * + * @param element The element for which the parent has to be returned. + * @return Parent of `element`. + */ + public async getParent(element: TestDataItem): Promise { + if (element instanceof TestWorkspaceFolder) { + return; + } + const tests = this.testStore.getTests(element.resource); + return tests ? getParent(tests, element) : undefined; + } + /** + * If we have test files directly in root directory, return those. + * If we have test folders and no test files under the root directory, then just return the test directories. + * The goal is not avoid returning an empty root node, when all it contains are child nodes for folders. + * + * @param {Tests} [tests] + * @returns + * @memberof TestTreeViewProvider + */ + public getRootNodes(tests?: Tests) { + if (tests && tests.rootTestFolders && tests.rootTestFolders.length === 1) { + return [...tests.rootTestFolders[0].testFiles, ...tests.rootTestFolders[0].folders]; + } + return tests ? tests.rootTestFolders : []; + } + /** + * Refresh the view by rebuilding the model and signaling the tree view to update itself. + * + * @param resource The resource 'root' for this refresh to occur under. + */ + public refresh(resource: Uri): void { + const workspaceFolder = this.workspace.getWorkspaceFolder(resource); + if (!workspaceFolder) { + return; + } + const tests = this.testStore.getTests(resource); + if (tests && tests.testFolders) { + this._onDidChangeTreeData.fire(new TestWorkspaceFolder(workspaceFolder)); + } + } + + /** + * Event handler for TestStatusChanged (coming from the ITestManagementService). + * ThisThe TreeView needs to know when we begin discovery and when discovery completes. + * + * @param e The event payload containing context for the status change + */ + private onTestStatusChanged(e: WorkspaceTestStatus) { + if (e.status === TestStatus.Discovering) { + this.testsAreBeingDiscovered.set(e.workspace.fsPath, true); + return; + } + if (!this.testsAreBeingDiscovered.get(e.workspace.fsPath)) { + return; + } + this.testsAreBeingDiscovered.set(e.workspace.fsPath, false); + this.refresh(e.workspace); + } + + private async shouldElementBeExpandedByDefault(element: TestDataItem) { + const parent = await this.getParent(element); + if (!parent || getTestDataItemType(parent) === TestDataItemType.workspaceFolder) { + return true; + } + return false; + } +} diff --git a/src/client/testing/explorer/treeView.ts b/src/client/testing/explorer/treeView.ts new file mode 100644 index 000000000000..0c683ca307db --- /dev/null +++ b/src/client/testing/explorer/treeView.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { TreeView } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IApplicationShell, ICommandManager } from '../../common/application/types'; +import { Commands } from '../../common/constants'; +import { IDisposable, IDisposableRegistry } from '../../common/types'; +import { ITestTreeViewProvider, TestDataItem } from '../types'; + +@injectable() +export class TreeViewService implements IExtensionSingleActivationService, IDisposable { + private _treeView!: TreeView; + private readonly disposables: IDisposable[] = []; + public get treeView(): TreeView { + return this._treeView; + } + constructor( + @inject(ITestTreeViewProvider) private readonly treeViewProvider: ITestTreeViewProvider, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager + ) { + disposableRegistry.push(this); + } + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + public async activate(): Promise { + this._treeView = this.appShell.createTreeView('python_tests', { + showCollapseAll: true, + treeDataProvider: this.treeViewProvider + }); + this.disposables.push(this._treeView); + this.disposables.push( + this.commandManager.registerCommand(Commands.Test_Reveal_Test_Item, this.onRevealTestItem, this) + ); + } + public async onRevealTestItem(testItem: TestDataItem): Promise { + await this.treeView.reveal(testItem, { select: false }); + } +} diff --git a/src/client/testing/main.ts b/src/client/testing/main.ts new file mode 100644 index 000000000000..f4763bd74b23 --- /dev/null +++ b/src/client/testing/main.ts @@ -0,0 +1,621 @@ +'use strict'; + +// tslint:disable:no-duplicate-imports no-unnecessary-callback-wrapper + +import { inject, injectable } from 'inversify'; +import { + ConfigurationChangeEvent, + Disposable, + DocumentSymbolProvider, + Event, + EventEmitter, + OutputChannel, + TextDocument, + Uri +} from 'vscode'; +import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; +import * as constants from '../common/constants'; +import { AlwaysDisplayTestExplorerGroups } from '../common/experiments/groups'; +import '../common/extensions'; +import { traceError } from '../common/logger'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentsManager, + IOutputChannel, + Resource +} from '../common/types'; +import { noop } from '../common/utils/misc'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { EventName } from '../telemetry/constants'; +import { captureTelemetry, sendTelemetryEvent } from '../telemetry/index'; +import { activateCodeLenses } from './codeLenses/main'; +import { CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL } from './common/constants'; +import { selectTestWorkspace } from './common/testUtils'; +import { + ITestCollectionStorageService, + ITestManager, + IWorkspaceTestManagerService, + TestFile, + TestFunction, + TestStatus, + TestsToRun +} from './common/types'; +import { + ITestConfigurationService, + ITestDisplay, + ITestManagementService, + ITestResultDisplay, + TestWorkspaceFolder, + WorkspaceTestStatus +} from './types'; + +// tslint:disable:no-any + +@injectable() +export class UnitTestManagementService implements ITestManagementService, Disposable { + private readonly outputChannel: OutputChannel; + private activatedOnce: boolean = false; + private readonly disposableRegistry: Disposable[]; + private workspaceTestManagerService?: IWorkspaceTestManagerService; + private documentManager: IDocumentManager; + private workspaceService: IWorkspaceService; + private testResultDisplay?: ITestResultDisplay; + private autoDiscoverTimer?: NodeJS.Timer | number; + private configChangedTimer?: NodeJS.Timer | number; + private testManagers = new Set(); + private readonly _onDidStatusChange: EventEmitter = new EventEmitter(); + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.disposableRegistry = serviceContainer.get(IDisposableRegistry); + this.outputChannel = serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.documentManager = serviceContainer.get(IDocumentManager); + + this.disposableRegistry.push(this); + } + public dispose() { + if (this.workspaceTestManagerService) { + this.workspaceTestManagerService.dispose(); + } + if (this.configChangedTimer) { + clearTimeout(this.configChangedTimer as any); + this.configChangedTimer = undefined; + } + if (this.autoDiscoverTimer) { + clearTimeout(this.autoDiscoverTimer as any); + this.autoDiscoverTimer = undefined; + } + } + public get onDidStatusChange(): Event { + return this._onDidStatusChange.event; + } + public async activate(symbolProvider: DocumentSymbolProvider): Promise { + if (this.activatedOnce) { + return; + } + this.activatedOnce = true; + this.workspaceTestManagerService = this.serviceContainer.get( + IWorkspaceTestManagerService + ); + + this.registerHandlers(); + this.registerCommands(); + this.checkExperiments(); + this.autoDiscoverTests(undefined).catch((ex) => + traceError('Failed to auto discover tests upon activation', ex) + ); + await this.registerSymbolProvider(symbolProvider); + } + public checkExperiments() { + const experiments = this.serviceContainer.get(IExperimentsManager); + if (experiments.inExperiment(AlwaysDisplayTestExplorerGroups.experiment)) { + const commandManager = this.serviceContainer.get(ICommandManager); + commandManager.executeCommand('setContext', 'testsDiscovered', true).then(noop, noop); + } else { + experiments.sendTelemetryIfInExperiment(AlwaysDisplayTestExplorerGroups.control); + } + } + public async getTestManager( + displayTestNotConfiguredMessage: boolean, + resource?: Uri + ): Promise { + let wkspace: Uri | undefined; + if (resource) { + const wkspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; + } else { + const appShell = this.serviceContainer.get(IApplicationShell); + wkspace = await selectTestWorkspace(appShell); + } + if (!wkspace) { + return; + } + const testManager = this.workspaceTestManagerService!.getTestManager(wkspace); + if (testManager) { + if (!this.testManagers.has(testManager)) { + this.testManagers.add(testManager); + const handler = testManager.onDidStatusChange((e) => this._onDidStatusChange.fire(e)); + this.disposableRegistry.push(handler); + } + return testManager; + } + if (displayTestNotConfiguredMessage) { + const configurationService = this.serviceContainer.get( + ITestConfigurationService + ); + await configurationService.displayTestFrameworkError(wkspace); + } + } + public async configurationChangeHandler(eventArgs: ConfigurationChangeEvent) { + // If there's one workspace, then stop the tests and restart, + // else let the user do this manually. + if (!this.workspaceService.hasWorkspaceFolders || this.workspaceService.workspaceFolders!.length > 1) { + return; + } + if (!Array.isArray(this.workspaceService.workspaceFolders)) { + return; + } + const workspaceFolderUri = this.workspaceService.workspaceFolders.find((w) => + eventArgs.affectsConfiguration('python.testing', w.uri) + ); + if (!workspaceFolderUri) { + return; + } + const workspaceUri = workspaceFolderUri.uri; + const settings = this.serviceContainer + .get(IConfigurationService) + .getSettings(workspaceUri); + if ( + !settings.testing.nosetestsEnabled && + !settings.testing.pytestEnabled && + !settings.testing.unittestEnabled + ) { + if (this.testResultDisplay) { + this.testResultDisplay.enabled = false; + } + // tslint:disable-next-line:no-suspicious-comment + // TODO: Why are we disposing, what happens when tests are enabled. + if (this.workspaceTestManagerService) { + this.workspaceTestManagerService.dispose(); + } + return; + } + if (this.testResultDisplay) { + this.testResultDisplay.enabled = true; + } + this.autoDiscoverTests(workspaceUri).catch((ex) => + traceError('Failed to auto discover tests upon activation', ex) + ); + } + + public async discoverTestsForDocument(doc: TextDocument): Promise { + const testManager = await this.getTestManager(false, doc.uri); + if (!testManager) { + return; + } + const tests = await testManager.discoverTests(CommandSource.auto, false, true); + if (!tests || !Array.isArray(tests.testFiles) || tests.testFiles.length === 0) { + return; + } + if (tests.testFiles.findIndex((f: TestFile) => f.fullPath === doc.uri.fsPath) === -1) { + return; + } + + if (this.autoDiscoverTimer) { + clearTimeout(this.autoDiscoverTimer as any); + } + this.autoDiscoverTimer = setTimeout( + () => this.discoverTests(CommandSource.auto, doc.uri, true, false, true), + 1000 + ); + } + public async autoDiscoverTests(resource: Resource) { + if (!this.workspaceService.hasWorkspaceFolders) { + return; + } + // Default to discovering tests in first folder if none specified. + if (!resource) { + resource = this.workspaceService.workspaceFolders![0].uri; + } + const configurationService = this.serviceContainer.get(IConfigurationService); + const settings = configurationService.getSettings(resource); + if ( + !settings.testing.nosetestsEnabled && + !settings.testing.pytestEnabled && + !settings.testing.unittestEnabled + ) { + return; + } + + this.discoverTests(CommandSource.auto, resource, true).ignoreErrors(); + } + public async discoverTests( + cmdSource: CommandSource, + resource?: Uri, + ignoreCache?: boolean, + userInitiated?: boolean, + quietMode?: boolean, + clearTestStatus?: boolean + ) { + const testManager = await this.getTestManager(true, resource); + if (!testManager) { + return; + } + + if (testManager.status === TestStatus.Discovering || testManager.status === TestStatus.Running) { + return; + } + + if (!this.testResultDisplay) { + this.testResultDisplay = this.serviceContainer.get(ITestResultDisplay); + } + const discoveryPromise = testManager.discoverTests( + cmdSource, + ignoreCache, + quietMode, + userInitiated, + clearTestStatus + ); + this.testResultDisplay + .displayDiscoverStatus(discoveryPromise, quietMode) + .catch((ex) => traceError('Python Extension: displayDiscoverStatus', ex)); + await discoveryPromise; + } + public async stopTests(resource: Uri) { + sendTelemetryEvent(EventName.UNITTEST_STOP); + const testManager = await this.getTestManager(true, resource); + if (testManager) { + testManager.stop(); + } + } + public async displayStopUI(message: string): Promise { + const testManager = await this.getTestManager(true); + if (!testManager) { + return; + } + + const testDisplay = this.serviceContainer.get(ITestDisplay); + testDisplay.displayStopTestUI(testManager.workspaceFolder, message); + } + public async displayUI(cmdSource: CommandSource) { + const testManager = await this.getTestManager(true); + if (!testManager) { + return; + } + + const testDisplay = this.serviceContainer.get(ITestDisplay); + testDisplay.displayTestUI(cmdSource, testManager.workspaceFolder); + } + public async displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean) { + const testManager = await this.getTestManager(true, file); + if (!testManager) { + return; + } + + const testDisplay = this.serviceContainer.get(ITestDisplay); + testDisplay.displayFunctionTestPickerUI( + cmdSource, + testManager.workspaceFolder, + testManager.workingDirectory, + file, + testFunctions, + debug + ); + } + public async runParametrizedTests( + cmdSource: CommandSource, + resource: Uri, + testFunctions: TestFunction[], + debug?: boolean + ) { + const testManager = await this.getTestManager(true, resource); + if (!testManager) { + return; + } + await this.runTestsImpl(cmdSource, resource, { testFunction: testFunctions }, undefined, debug); + } + public viewOutput(_cmdSource: CommandSource) { + sendTelemetryEvent(EventName.UNITTEST_VIEW_OUTPUT); + this.outputChannel.show(); + } + public async selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean) { + const testManager = await this.getTestManager(true, resource); + if (!testManager) { + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } + + const testCollectionStorage = this.serviceContainer.get( + ITestCollectionStorageService + ); + const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; + const testDisplay = this.serviceContainer.get(ITestDisplay); + const selectedTestFn = await testDisplay.selectTestFunction(testManager.workspaceFolder.fsPath, tests); + if (!selectedTestFn) { + return; + } + // tslint:disable-next-line:prefer-type-cast + await this.runTestsImpl( + cmdSource, + testManager.workspaceFolder, + // tslint:disable-next-line:no-object-literal-type-assertion + { testFunction: [selectedTestFn.testFunction] } as TestsToRun, + false, + debug + ); + } + public async selectAndRunTestFile(cmdSource: CommandSource) { + const testManager = await this.getTestManager(true); + if (!testManager) { + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } + + const testCollectionStorage = this.serviceContainer.get( + ITestCollectionStorageService + ); + const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; + const testDisplay = this.serviceContainer.get(ITestDisplay); + const selectedFile = await testDisplay.selectTestFile(testManager.workspaceFolder.fsPath, tests); + if (!selectedFile) { + return; + } + await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [selectedFile] }); + } + public async runCurrentTestFile(cmdSource: CommandSource) { + if (!this.documentManager.activeTextEditor) { + return; + } + const testManager = await this.getTestManager(true, this.documentManager.activeTextEditor.document.uri); + if (!testManager) { + return; + } + try { + await testManager.discoverTests(cmdSource, true, true, true); + } catch (ex) { + return; + } + const testCollectionStorage = this.serviceContainer.get( + ITestCollectionStorageService + ); + const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; + const testFiles = tests.testFiles.filter((testFile) => { + return testFile.fullPath === this.documentManager.activeTextEditor!.document.uri.fsPath; + }); + if (testFiles.length < 1) { + return; + } + await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [testFiles[0]] }); + } + + public async runTestsImpl( + cmdSource: CommandSource, + resource?: Uri, + testsToRun?: TestsToRun, + runFailedTests?: boolean, + debug: boolean = false + ) { + const testManager = await this.getTestManager(true, resource); + if (!testManager) { + return; + } + + if (!this.testResultDisplay) { + this.testResultDisplay = this.serviceContainer.get(ITestResultDisplay); + } + + const promise = testManager.runTest(cmdSource, testsToRun, runFailedTests, debug).catch((reason) => { + if (reason !== CANCELLATION_REASON) { + this.outputChannel.appendLine(`Error: ${reason}`); + } + return Promise.reject(reason); + }); + + this.testResultDisplay.displayProgressStatus(promise, debug); + await promise; + } + + public async registerSymbolProvider(symbolProvider: DocumentSymbolProvider): Promise { + const testCollectionStorage = this.serviceContainer.get( + ITestCollectionStorageService + ); + const event = new EventEmitter(); + this.disposableRegistry.push(event); + const handler = this._onDidStatusChange.event((e) => { + if (e.status !== TestStatus.Discovering && e.status !== TestStatus.Running) { + event.fire(); + } + }); + this.disposableRegistry.push(handler); + this.disposableRegistry.push( + activateCodeLenses(event, symbolProvider, testCollectionStorage, this.serviceContainer) + ); + } + + @captureTelemetry(EventName.UNITTEST_CONFIGURE, undefined, false) + public async configureTests(resource?: Uri) { + let wkspace: Uri | undefined; + if (resource) { + const wkspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; + } else { + const appShell = this.serviceContainer.get(IApplicationShell); + wkspace = await selectTestWorkspace(appShell); + } + if (!wkspace) { + return; + } + const configurationService = this.serviceContainer.get(ITestConfigurationService); + await configurationService.promptToEnableAndConfigureTestFramework(wkspace!); + } + // tslint:disable-next-line: max-func-body-length + public registerCommands(): void { + const disposablesRegistry = this.serviceContainer.get(IDisposableRegistry); + const commandManager = this.serviceContainer.get(ICommandManager); + + const disposables = [ + commandManager.registerCommand( + constants.Commands.Tests_Discover, + ( + treeNode?: TestWorkspaceFolder, + cmdSource: CommandSource = CommandSource.commandPalette, + resource?: Uri + ) => { + if (treeNode && treeNode instanceof TestWorkspaceFolder) { + resource = treeNode.resource; + cmdSource = CommandSource.testExplorer; + } + // Ignore the exceptions returned. + // This command will be invoked from other places of the extension. + return this.discoverTests(cmdSource, resource, true, true, false, true).ignoreErrors(); + } + ), + commandManager.registerCommand( + constants.Commands.Tests_Configure, + (_, _cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => { + // Ignore the exceptions returned. + // This command will be invoked from other places of the extension. + this.configureTests(resource).ignoreErrors(); + } + ), + commandManager.registerCommand( + constants.Commands.Tests_Run_Failed, + (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => + this.runTestsImpl(cmdSource, resource, undefined, true) + ), + commandManager.registerCommand( + constants.Commands.Tests_Run, + ( + treeNode?: TestWorkspaceFolder, + cmdSource: CommandSource = CommandSource.commandPalette, + resource?: Uri, + testToRun?: TestsToRun + ) => { + if (treeNode && treeNode instanceof TestWorkspaceFolder) { + resource = treeNode.resource; + cmdSource = CommandSource.testExplorer; + } + return this.runTestsImpl(cmdSource, resource, testToRun); + } + ), + commandManager.registerCommand( + constants.Commands.Tests_Debug, + ( + treeNode?: TestWorkspaceFolder, + cmdSource: CommandSource = CommandSource.commandPalette, + resource?: Uri, + testToRun?: TestsToRun + ) => { + if (treeNode && treeNode instanceof TestWorkspaceFolder) { + resource = treeNode.resource; + cmdSource = CommandSource.testExplorer; + } + return this.runTestsImpl(cmdSource, resource, testToRun, false, true); + } + ), + commandManager.registerCommand(constants.Commands.Tests_View_UI, () => + this.displayUI(CommandSource.commandPalette) + ), + commandManager.registerCommand( + constants.Commands.Tests_Picker_UI, + ( + _, + cmdSource: CommandSource = CommandSource.commandPalette, + file: Uri, + testFunctions: TestFunction[] + ) => this.displayPickerUI(cmdSource, file, testFunctions) + ), + commandManager.registerCommand( + constants.Commands.Tests_Picker_UI_Debug, + ( + _, + cmdSource: CommandSource = CommandSource.commandPalette, + file: Uri, + testFunctions: TestFunction[] + ) => this.displayPickerUI(cmdSource, file, testFunctions, true) + ), + commandManager.registerCommand( + constants.Commands.Tests_Run_Parametrized, + ( + _, + cmdSource: CommandSource = CommandSource.commandPalette, + resource: Uri, + testFunctions: TestFunction[], + debug: boolean + ) => this.runParametrizedTests(cmdSource, resource, testFunctions, debug) + ), + commandManager.registerCommand(constants.Commands.Tests_Stop, (_, resource: Uri) => + this.stopTests(resource) + ), + commandManager.registerCommand( + constants.Commands.Tests_ViewOutput, + (_, cmdSource: CommandSource = CommandSource.commandPalette) => this.viewOutput(cmdSource) + ), + commandManager.registerCommand(constants.Commands.Tests_Ask_To_Stop_Discovery, () => + this.displayStopUI('Stop discovering tests') + ), + commandManager.registerCommand(constants.Commands.Tests_Ask_To_Stop_Test, () => + this.displayStopUI('Stop running tests') + ), + commandManager.registerCommand( + constants.Commands.Tests_Select_And_Run_Method, + (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => + this.selectAndRunTestMethod(cmdSource, resource) + ), + commandManager.registerCommand( + constants.Commands.Tests_Select_And_Debug_Method, + (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => + this.selectAndRunTestMethod(cmdSource, resource, true) + ), + commandManager.registerCommand( + constants.Commands.Tests_Select_And_Run_File, + (_, cmdSource: CommandSource = CommandSource.commandPalette) => this.selectAndRunTestFile(cmdSource) + ), + commandManager.registerCommand( + constants.Commands.Tests_Run_Current_File, + (_, cmdSource: CommandSource = CommandSource.commandPalette) => this.runCurrentTestFile(cmdSource) + ), + commandManager.registerCommand(constants.Commands.Tests_Discovering, noop) + ]; + + disposablesRegistry.push(...disposables); + } + public onDocumentSaved(doc: TextDocument) { + const settings = this.serviceContainer.get(IConfigurationService).getSettings(doc.uri); + if (!settings.testing.autoTestDiscoverOnSaveEnabled) { + return; + } + this.discoverTestsForDocument(doc).ignoreErrors(); + } + public registerHandlers() { + const documentManager = this.serviceContainer.get(IDocumentManager); + const interpreterService = this.serviceContainer.get(IInterpreterService); + + this.disposableRegistry.push(documentManager.onDidSaveTextDocument(this.onDocumentSaved.bind(this))); + this.disposableRegistry.push( + this.workspaceService.onDidChangeConfiguration((e) => { + if (this.configChangedTimer) { + clearTimeout(this.configChangedTimer as any); + } + this.configChangedTimer = setTimeout(() => this.configurationChangeHandler(e), 1000); + }) + ); + this.disposableRegistry.push( + interpreterService.onDidChangeInterpreter(() => + this.autoDiscoverTests(undefined).catch((ex) => + traceError('Failed to auto discover tests upon changing interpreter', ex) + ) + ) + ); + } +} diff --git a/src/client/testing/navigation/commandHandler.ts b/src/client/testing/navigation/commandHandler.ts new file mode 100644 index 000000000000..27df132391bb --- /dev/null +++ b/src/client/testing/navigation/commandHandler.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { ICommandManager } from '../../common/application/types'; +import { Commands } from '../../common/constants'; +import { IDisposable, IDisposableRegistry } from '../../common/types'; +import { ITestCodeNavigator, ITestCodeNavigatorCommandHandler, NavigableItemType } from './types'; + +@injectable() +export class TestCodeNavigatorCommandHandler implements ITestCodeNavigatorCommandHandler { + private disposables: IDisposable[] = []; + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(ITestCodeNavigator) + @named(NavigableItemType.testFile) + private readonly testFileNavigator: ITestCodeNavigator, + @inject(ITestCodeNavigator) + @named(NavigableItemType.testFunction) + private readonly testFunctionNavigator: ITestCodeNavigator, + @inject(ITestCodeNavigator) + @named(NavigableItemType.testSuite) + private readonly testSuiteNavigator: ITestCodeNavigator, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry + ) { + disposableRegistry.push(this); + } + public dispose() { + this.disposables.forEach((item) => item.dispose()); + } + public register(): void { + if (this.disposables.length > 0) { + return; + } + let disposable = this.commandManager.registerCommand( + Commands.navigateToTestFile, + this.testFileNavigator.navigateTo, + this.testFileNavigator + ); + this.disposables.push(disposable); + disposable = this.commandManager.registerCommand( + Commands.navigateToTestFunction, + this.testFunctionNavigator.navigateTo, + this.testFunctionNavigator + ); + this.disposables.push(disposable); + disposable = this.commandManager.registerCommand( + Commands.navigateToTestSuite, + this.testSuiteNavigator.navigateTo, + this.testSuiteNavigator + ); + this.disposables.push(disposable); + } +} diff --git a/src/client/testing/navigation/fileNavigator.ts b/src/client/testing/navigation/fileNavigator.ts new file mode 100644 index 000000000000..12f5ac69f8ab --- /dev/null +++ b/src/client/testing/navigation/fileNavigator.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { swallowExceptions } from '../../common/utils/decorators'; +import { captureTelemetry } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { TestFile } from '../common/types'; +import { ITestCodeNavigator, ITestNavigatorHelper } from './types'; + +@injectable() +export class TestFileCodeNavigator implements ITestCodeNavigator { + constructor(@inject(ITestNavigatorHelper) private readonly helper: ITestNavigatorHelper) {} + @swallowExceptions('Navigate to test file') + @captureTelemetry(EventName.UNITTEST_NAVIGATE, { byFile: true }) + public async navigateTo(_: Uri, item: TestFile, __: boolean): Promise { + await this.helper.openFile(Uri.file(item.fullPath)); + } +} diff --git a/src/client/testing/navigation/functionNavigator.ts b/src/client/testing/navigation/functionNavigator.ts new file mode 100644 index 000000000000..0c95e42128b6 --- /dev/null +++ b/src/client/testing/navigation/functionNavigator.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationTokenSource, Range, SymbolInformation, SymbolKind, TextEditorRevealType, Uri } from 'vscode'; +import { IDocumentManager } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import { swallowExceptions } from '../../common/utils/decorators'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { ITestCollectionStorageService, TestFunction } from '../common/types'; +import { ITestCodeNavigator, ITestNavigatorHelper } from './types'; + +@injectable() +export class TestFunctionCodeNavigator implements ITestCodeNavigator { + private cancellationToken?: CancellationTokenSource; + constructor( + @inject(ITestNavigatorHelper) private readonly helper: ITestNavigatorHelper, + @inject(IDocumentManager) private readonly docManager: IDocumentManager, + @inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService + ) {} + @swallowExceptions('Navigate to test function') + @captureTelemetry(EventName.UNITTEST_NAVIGATE, { byFunction: true }, true) // To measure execution time. + public async navigateTo(resource: Uri, fn: TestFunction, focus: boolean = true): Promise { + sendTelemetryEvent(EventName.UNITTEST_NAVIGATE, undefined, { focus_code: focus, byFunction: true }); + if (this.cancellationToken) { + this.cancellationToken.cancel(); + } + const item = this.storage.findFlattendTestFunction(resource, fn); + if (!item) { + throw new Error('Flattend test function not found'); + } + this.cancellationToken = new CancellationTokenSource(); + const [doc, editor] = await this.helper.openFile(Uri.file(item.parentTestFile.fullPath)); + let range: Range | undefined; + if (item.testFunction.line) { + range = new Range(item.testFunction.line, 0, item.testFunction.line, 0); + } else { + const predicate = (s: SymbolInformation) => + s.name === item.testFunction.name && (s.kind === SymbolKind.Method || s.kind === SymbolKind.Function); + const symbol = await this.helper.findSymbol(doc, predicate, this.cancellationToken.token); + range = symbol ? symbol.location.range : undefined; + } + if (!range) { + traceError('Unable to navigate to test function', new Error('Test Function not found')); + return; + } + if (focus) { + range = new Range(range.start.line, range.start.character, range.start.line, range.start.character); + await this.docManager.showTextDocument(doc, { preserveFocus: false, selection: range }); + } else { + editor.revealRange(range, TextEditorRevealType.Default); + } + } +} diff --git a/src/client/testing/navigation/helper.ts b/src/client/testing/navigation/helper.ts new file mode 100644 index 000000000000..7c5e092ea4a7 --- /dev/null +++ b/src/client/testing/navigation/helper.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { CancellationToken, SymbolInformation, TextDocument, TextEditor, Uri } from 'vscode'; +import { IDocumentManager } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import { IDocumentSymbolProvider } from '../../common/types'; +import { ITestNavigatorHelper, SymbolSearch } from './types'; + +@injectable() +export class TestNavigatorHelper implements ITestNavigatorHelper { + constructor( + @inject(IDocumentManager) private readonly documentManager: IDocumentManager, + @inject(IDocumentSymbolProvider) @named('test') private readonly symbolProvider: IDocumentSymbolProvider + ) {} + public async openFile(file?: Uri): Promise<[TextDocument, TextEditor]> { + if (!file) { + throw new Error('Unable to navigate to an undefined test file'); + } + const doc = await this.documentManager.openTextDocument(file); + const editor = await this.documentManager.showTextDocument(doc); + return [doc, editor]; + } + public async findSymbol( + doc: TextDocument, + search: SymbolSearch, + token: CancellationToken + ): Promise { + const symbols = (await this.symbolProvider.provideDocumentSymbols(doc, token)) as SymbolInformation[]; + if (!Array.isArray(symbols) || symbols.length === 0) { + traceError('Symbol information not found', new Error('Symbol information not found')); + return; + } + return symbols.find(search); + } +} diff --git a/src/client/testing/navigation/serviceRegistry.ts b/src/client/testing/navigation/serviceRegistry.ts new file mode 100644 index 000000000000..bccf480a5b9f --- /dev/null +++ b/src/client/testing/navigation/serviceRegistry.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IDocumentSymbolProvider } from '../../common/types'; +import { IServiceManager } from '../../ioc/types'; +import { TestCodeNavigatorCommandHandler } from './commandHandler'; +import { TestFileCodeNavigator } from './fileNavigator'; +import { TestFunctionCodeNavigator } from './functionNavigator'; +import { TestNavigatorHelper } from './helper'; +import { TestSuiteCodeNavigator } from './suiteNavigator'; +import { TestFileSymbolProvider } from './symbolProvider'; +import { ITestCodeNavigator, ITestCodeNavigatorCommandHandler, ITestNavigatorHelper, NavigableItemType } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(ITestNavigatorHelper, TestNavigatorHelper); + serviceManager.addSingleton( + ITestCodeNavigatorCommandHandler, + TestCodeNavigatorCommandHandler + ); + serviceManager.addSingleton( + ITestCodeNavigator, + TestFileCodeNavigator, + NavigableItemType.testFile + ); + serviceManager.addSingleton( + ITestCodeNavigator, + TestFunctionCodeNavigator, + NavigableItemType.testFunction + ); + serviceManager.addSingleton( + ITestCodeNavigator, + TestSuiteCodeNavigator, + NavigableItemType.testSuite + ); + serviceManager.addSingleton(IDocumentSymbolProvider, TestFileSymbolProvider, 'test'); +} diff --git a/src/client/testing/navigation/suiteNavigator.ts b/src/client/testing/navigation/suiteNavigator.ts new file mode 100644 index 000000000000..0230daa95e56 --- /dev/null +++ b/src/client/testing/navigation/suiteNavigator.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { CancellationTokenSource, Range, SymbolInformation, SymbolKind, TextEditorRevealType, Uri } from 'vscode'; +import { IDocumentManager } from '../../common/application/types'; +import { traceError } from '../../common/logger'; +import { swallowExceptions } from '../../common/utils/decorators'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { ITestCollectionStorageService, TestSuite } from '../common/types'; +import { ITestCodeNavigator, ITestNavigatorHelper } from './types'; + +@injectable() +export class TestSuiteCodeNavigator implements ITestCodeNavigator { + private cancellationToken?: CancellationTokenSource; + constructor( + @inject(ITestNavigatorHelper) private readonly helper: ITestNavigatorHelper, + @inject(IDocumentManager) private readonly docManager: IDocumentManager, + @inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService + ) {} + @swallowExceptions('Navigate to test suite') + @captureTelemetry(EventName.UNITTEST_NAVIGATE, { bySuite: true }, true) // For measuring execution time. + public async navigateTo(resource: Uri, suite: TestSuite, focus: boolean = true): Promise { + sendTelemetryEvent(EventName.UNITTEST_NAVIGATE, undefined, { focus_code: focus, bySuite: true }); + if (this.cancellationToken) { + this.cancellationToken.cancel(); + } + const item = this.storage.findFlattendTestSuite(resource, suite); + if (!item) { + throw new Error('Flattened test suite not found'); + } + this.cancellationToken = new CancellationTokenSource(); + const [doc, editor] = await this.helper.openFile(Uri.file(item.parentTestFile.fullPath)); + let range: Range | undefined; + if (item.testSuite.line) { + range = new Range(item.testSuite.line, 0, item.testSuite.line, 0); + } else { + const predicate = (s: SymbolInformation) => s.name === item.testSuite.name && s.kind === SymbolKind.Class; + const symbol = await this.helper.findSymbol(doc, predicate, this.cancellationToken.token); + range = symbol ? symbol.location.range : undefined; + } + if (!range) { + traceError('Unable to navigate to test suite', new Error('Test Suite not found')); + return; + } + if (focus) { + range = new Range(range.start.line, range.start.character, range.start.line, range.start.character); + await this.docManager.showTextDocument(doc, { preserveFocus: false, selection: range }); + } else { + editor.revealRange(range, TextEditorRevealType.Default); + } + } +} diff --git a/src/client/testing/navigation/symbolProvider.ts b/src/client/testing/navigation/symbolProvider.ts new file mode 100644 index 000000000000..806776d89245 --- /dev/null +++ b/src/client/testing/navigation/symbolProvider.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { + CancellationToken, + DocumentSymbolProvider, + Location, + Range, + SymbolInformation, + SymbolKind, + TextDocument, + Uri +} from 'vscode'; +import { traceError } from '../../common/logger'; +import * as internalScripts from '../../common/process/internal/scripts'; +import { IPythonExecutionFactory } from '../../common/process/types'; + +type RawSymbol = { namespace: string; name: string; range: Range }; +type Symbols = { + classes: RawSymbol[]; + methods: RawSymbol[]; + functions: RawSymbol[]; +}; + +@injectable() +export class TestFileSymbolProvider implements DocumentSymbolProvider { + constructor(@inject(IPythonExecutionFactory) private readonly pythonServiceFactory: IPythonExecutionFactory) {} + public async provideDocumentSymbols( + document: TextDocument, + token: CancellationToken + ): Promise { + const rawSymbols = await this.getSymbols(document, token); + if (!rawSymbols) { + return []; + } + return [ + ...rawSymbols.classes.map((item) => this.parseRawSymbol(document.uri, item, SymbolKind.Class)), + ...rawSymbols.methods.map((item) => this.parseRawSymbol(document.uri, item, SymbolKind.Method)), + ...rawSymbols.functions.map((item) => this.parseRawSymbol(document.uri, item, SymbolKind.Function)) + ]; + } + private parseRawSymbol(uri: Uri, symbol: RawSymbol, kind: SymbolKind): SymbolInformation { + const range = new Range( + symbol.range.start.line, + symbol.range.start.character, + symbol.range.end.line, + symbol.range.end.character + ); + return { + containerName: symbol.namespace, + kind, + name: symbol.name, + location: new Location(uri, range) + }; + } + private async getSymbols(document: TextDocument, token: CancellationToken): Promise { + try { + if (document.isUntitled) { + return; + } + const [args, parse] = internalScripts.symbolProvider( + document.uri.fsPath, + document.isDirty ? document.getText() : undefined + ); + const pythonService = await this.pythonServiceFactory.create({ resource: document.uri }); + const proc = await pythonService.exec(args, { throwOnStdErr: true, token }); + + return (parse(proc.stdout) as unknown) as Symbols; + } catch (ex) { + traceError('Python: Failed to get symbols', ex); + return; + } + } +} diff --git a/src/client/testing/navigation/types.ts b/src/client/testing/navigation/types.ts new file mode 100644 index 000000000000..62b9e4b809c4 --- /dev/null +++ b/src/client/testing/navigation/types.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { CancellationToken, SymbolInformation, TextDocument, TextEditor, Uri } from 'vscode'; +import { IDisposable } from '../../common/types'; +import { TestFile, TestFunction, TestSuite } from '../common/types'; + +export const ITestCodeNavigatorCommandHandler = Symbol('ITestCodeNavigatorCommandHandler'); +export interface ITestCodeNavigatorCommandHandler extends IDisposable { + register(): void; +} +export type NavigableItem = TestFile | TestFunction | TestSuite; +export enum NavigableItemType { + testFile = 'testFile', + testFunction = 'testFunction', + testSuite = 'testSuite' +} + +export const ITestCodeNavigator = Symbol('ITestCodeNavigator'); +export interface ITestCodeNavigator { + navigateTo(resource: Uri, item: NavigableItem, focus: boolean): Promise; +} + +export const ITestNavigatorHelper = Symbol('ITestNavigatorHelper'); +export interface ITestNavigatorHelper { + openFile(file?: Uri): Promise<[TextDocument, TextEditor]>; + findSymbol( + doc: TextDocument, + predicate: SymbolSearch, + token: CancellationToken + ): Promise; +} +export type SymbolSearch = (item: SymbolInformation) => boolean; + +export const ITestExplorerCommandHandler = Symbol('ITestExplorerCommandHandler'); +export interface ITestExplorerCommandHandler extends IDisposable { + register(): void; +} diff --git a/src/client/testing/nosetest/main.ts b/src/client/testing/nosetest/main.ts new file mode 100644 index 000000000000..e8bccbf26bf9 --- /dev/null +++ b/src/client/testing/nosetest/main.ts @@ -0,0 +1,78 @@ +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { NOSETEST_PROVIDER } from '../common/constants'; +import { BaseTestManager } from '../common/managers/baseTestManager'; +import { ITestsHelper, TestDiscoveryOptions, TestRunOptions, Tests, TestsToRun } from '../common/types'; +import { IArgumentsService, ITestManagerRunner, TestFilter } from '../types'; + +@injectable() +export class TestManager extends BaseTestManager { + private readonly argsService: IArgumentsService; + private readonly helper: ITestsHelper; + private readonly runner: ITestManagerRunner; + public get enabled() { + return this.settings.testing.nosetestsEnabled; + } + constructor( + workspaceFolder: Uri, + rootDirectory: string, + @inject(IServiceContainer) serviceContainer: IServiceContainer + ) { + super(NOSETEST_PROVIDER, Product.nosetest, workspaceFolder, rootDirectory, serviceContainer); + this.argsService = this.serviceContainer.get(IArgumentsService, this.testProvider); + this.helper = this.serviceContainer.get(ITestsHelper); + this.runner = this.serviceContainer.get(ITestManagerRunner, this.testProvider); + } + public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { + const args = this.settings.testing.nosetestArgs.slice(0); + return { + workspaceFolder: this.workspaceFolder, + cwd: this.rootDirectory, + args, + token: this.testDiscoveryCancellationToken!, + ignoreCache, + outChannel: this.outputChannel + }; + } + public runTestImpl( + tests: Tests, + testsToRun?: TestsToRun, + runFailedTests?: boolean, + debug?: boolean + ): Promise { + let args: string[]; + + const runAllTests = this.helper.shouldRunAllTests(testsToRun); + if (debug) { + args = this.argsService.filterArguments( + this.settings.testing.nosetestArgs, + runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific + ); + } else { + args = this.argsService.filterArguments( + this.settings.testing.nosetestArgs, + runAllTests ? TestFilter.runAll : TestFilter.runSpecific + ); + } + + if (runFailedTests === true && args.indexOf('--failed') === -1) { + args.splice(0, 0, '--failed'); + } + if (!runFailedTests && args.indexOf('--with-id') === -1) { + args.splice(0, 0, '--with-id'); + } + const options: TestRunOptions = { + workspaceFolder: Uri.file(this.rootDirectory), + cwd: this.rootDirectory, + tests, + args, + testsToRun, + token: this.testRunnerCancellationToken!, + outChannel: this.outputChannel, + debug + }; + return this.runner.runTest(this.testResultsService, options, this); + } +} diff --git a/src/client/testing/nosetest/runner.ts b/src/client/testing/nosetest/runner.ts new file mode 100644 index 000000000000..e29a95e155ce --- /dev/null +++ b/src/client/testing/nosetest/runner.ts @@ -0,0 +1,124 @@ +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IFileSystem, TemporaryFile } from '../../common/platform/types'; +import { noop } from '../../common/utils/misc'; +import { IServiceContainer } from '../../ioc/types'; +import { NOSETEST_PROVIDER } from '../common/constants'; +import { Options } from '../common/runner'; +import { + ITestDebugLauncher, + ITestManager, + ITestResultsService, + ITestRunner, + IXUnitParser, + LaunchOptions, + TestRunOptions, + Tests +} from '../common/types'; +import { IArgumentsHelper, IArgumentsService, ITestManagerRunner } from '../types'; + +const WITH_XUNIT = '--with-xunit'; +const XUNIT_FILE = '--xunit-file'; + +@injectable() +export class TestManagerRunner implements ITestManagerRunner { + private readonly argsService: IArgumentsService; + private readonly argsHelper: IArgumentsHelper; + private readonly testRunner: ITestRunner; + private readonly xUnitParser: IXUnitParser; + private readonly fs: IFileSystem; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.argsService = serviceContainer.get(IArgumentsService, NOSETEST_PROVIDER); + this.argsHelper = serviceContainer.get(IArgumentsHelper); + this.testRunner = serviceContainer.get(ITestRunner); + this.xUnitParser = this.serviceContainer.get(IXUnitParser); + this.fs = this.serviceContainer.get(IFileSystem); + } + public async runTest( + testResultsService: ITestResultsService, + options: TestRunOptions, + _: ITestManager + ): Promise { + let testPaths: string[] = []; + if (options.testsToRun && options.testsToRun.testFolder) { + testPaths = testPaths.concat(options.testsToRun.testFolder.map((f) => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testFile) { + testPaths = testPaths.concat(options.testsToRun.testFile.map((f) => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testSuite) { + testPaths = testPaths.concat(options.testsToRun.testSuite.map((f) => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testFunction) { + testPaths = testPaths.concat(options.testsToRun.testFunction.map((f) => f.nameToRun)); + } + + let deleteJUnitXmlFile: Function = noop; + const args = options.args; + // Check if '--with-xunit' is in args list + if (args.indexOf(WITH_XUNIT) === -1) { + args.splice(0, 0, WITH_XUNIT); + } + + try { + const xmlLogResult = await this.getUnitXmlFile(args); + const xmlLogFile = xmlLogResult.filePath; + deleteJUnitXmlFile = xmlLogResult.dispose; + // Remove the '--unixml' if it exists, and add it with our path. + const testArgs = this.argsService.filterArguments(args, [XUNIT_FILE]); + testArgs.splice(0, 0, `${XUNIT_FILE}=${xmlLogFile}`); + + // Positional arguments control the tests to be run. + testArgs.push(...testPaths); + + if (options.debug === true) { + const debugLauncher = this.serviceContainer.get(ITestDebugLauncher); + const debuggerArgs = [options.cwd, 'nose'].concat(testArgs); + const launchOptions: LaunchOptions = { + cwd: options.cwd, + args: debuggerArgs, + token: options.token, + outChannel: options.outChannel, + testProvider: NOSETEST_PROVIDER + }; + await debugLauncher.launchDebugger(launchOptions); + } else { + const runOptions: Options = { + args: testArgs.concat(testPaths), + cwd: options.cwd, + outChannel: options.outChannel, + token: options.token, + workspaceFolder: options.workspaceFolder + }; + await this.testRunner.run(NOSETEST_PROVIDER, runOptions); + } + + return options.debug + ? options.tests + : await this.updateResultsFromLogFiles(options.tests, xmlLogFile, testResultsService); + } catch (ex) { + return Promise.reject(ex); + } finally { + deleteJUnitXmlFile(); + } + } + + private async updateResultsFromLogFiles( + tests: Tests, + outputXmlFile: string, + testResultsService: ITestResultsService + ): Promise { + await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile); + testResultsService.updateResults(tests); + return tests; + } + private async getUnitXmlFile(args: string[]): Promise { + const xmlFile = this.argsHelper.getOptionValues(args, XUNIT_FILE); + if (typeof xmlFile === 'string') { + return { filePath: xmlFile, dispose: noop }; + } + + return this.fs.createTemporaryFile('.xml'); + } +} diff --git a/src/client/testing/nosetest/services/argsService.ts b/src/client/testing/nosetest/services/argsService.ts new file mode 100644 index 000000000000..cba04ef343b2 --- /dev/null +++ b/src/client/testing/nosetest/services/argsService.ts @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IServiceContainer } from '../../../ioc/types'; +import { IArgumentsHelper, IArgumentsService, TestFilter } from '../../types'; + +const OptionsWithArguments = [ + '--attr', + '--config', + '--cover-html-dir', + '--cover-min-percentage', + '--cover-package', + '--cover-xml-file', + '--debug', + '--debug-log', + '--doctest-extension', + '--doctest-fixtures', + '--doctest-options', + '--doctest-result-variable', + '--eval-attr', + '--exclude', + '--id-file', + '--ignore-files', + '--include', + '--log-config', + '--logging-config', + '--logging-datefmt', + '--logging-filter', + '--logging-format', + '--logging-level', + '--match', + '--process-timeout', + '--processes', + '--py3where', + '--testmatch', + '--tests', + '--verbosity', + '--where', + '--xunit-file', + '--xunit-testsuite-name', + '-A', + '-a', + '-c', + '-e', + '-i', + '-I', + '-l', + '-m', + '-w', + '--profile-restrict', + '--profile-sort', + '--profile-stats-file' +]; + +const OptionsWithoutArguments = [ + '-h', + '--help', + '-V', + '--version', + '-p', + '--plugins', + '-v', + '--verbose', + '--quiet', + '-x', + '--stop', + '-P', + '--no-path-adjustment', + '--exe', + '--noexe', + '--traverse-namespace', + '--first-package-wins', + '--first-pkg-wins', + '--1st-pkg-wins', + '--no-byte-compile', + '-s', + '--nocapture', + '--nologcapture', + '--logging-clear-handlers', + '--with-coverage', + '--cover-erase', + '--cover-tests', + '--cover-inclusive', + '--cover-html', + '--cover-branches', + '--cover-xml', + '--pdb', + '--pdb-failures', + '--pdb-errors', + '--no-deprecated', + '--with-doctest', + '--doctest-tests', + '--with-isolation', + '-d', + '--detailed-errors', + '--failure-detail', + '--no-skip', + '--with-id', + '--failed', + '--process-restartworker', + '--with-xunit', + '--all-modules', + '--collect-only', + '--with-profile' +]; + +@injectable() +export class ArgumentsService implements IArgumentsService { + private readonly helper: IArgumentsHelper; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.helper = serviceContainer.get(IArgumentsHelper); + } + public getKnownOptions(): { withArgs: string[]; withoutArgs: string[] } { + return { + withArgs: OptionsWithArguments, + withoutArgs: OptionsWithoutArguments + }; + } + public getOptionValue(args: string[], option: string): string | string[] | undefined { + return this.helper.getOptionValues(args, option); + } + // tslint:disable-next-line:max-func-body-length + public filterArguments(args: string[], argumentToRemoveOrFilter: string[] | TestFilter): string[] { + const optionsWithoutArgsToRemove: string[] = []; + const optionsWithArgsToRemove: string[] = []; + // Positional arguments in nosetest are test directories and files. + // So if we want to run a specific test, then remove positional args. + let removePositionalArgs = false; + if (Array.isArray(argumentToRemoveOrFilter)) { + argumentToRemoveOrFilter.forEach((item) => { + if (OptionsWithArguments.indexOf(item) >= 0) { + optionsWithArgsToRemove.push(item); + } + if (OptionsWithoutArguments.indexOf(item) >= 0) { + optionsWithoutArgsToRemove.push(item); + } + }); + } else { + switch (argumentToRemoveOrFilter) { + case TestFilter.removeTests: { + removePositionalArgs = true; + break; + } + case TestFilter.discovery: { + optionsWithoutArgsToRemove.push( + ...[ + '-v', + '--verbose', + '-q', + '--quiet', + '-x', + '--stop', + '--with-coverage', + ...OptionsWithoutArguments.filter((item) => item.startsWith('--cover')), + ...OptionsWithoutArguments.filter((item) => item.startsWith('--logging')), + ...OptionsWithoutArguments.filter((item) => item.startsWith('--pdb')), + ...OptionsWithoutArguments.filter((item) => item.indexOf('xunit') >= 0) + ] + ); + optionsWithArgsToRemove.push( + ...[ + '--verbosity', + '-l', + '--debug', + '--cover-package', + ...OptionsWithoutArguments.filter((item) => item.startsWith('--cover')), + ...OptionsWithArguments.filter((item) => item.startsWith('--logging')), + ...OptionsWithoutArguments.filter((item) => item.indexOf('xunit') >= 0) + ] + ); + break; + } + case TestFilter.debugAll: + case TestFilter.runAll: { + break; + } + case TestFilter.debugSpecific: + case TestFilter.runSpecific: { + removePositionalArgs = true; + break; + } + default: { + throw new Error(`Unsupported Filter '${argumentToRemoveOrFilter}'`); + } + } + } + + let filteredArgs = args.slice(); + if (removePositionalArgs) { + const positionalArgs = this.helper.getPositionalArguments( + filteredArgs, + OptionsWithArguments, + OptionsWithoutArguments + ); + filteredArgs = filteredArgs.filter((item) => positionalArgs.indexOf(item) === -1); + } + return this.helper.filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); + } + public getTestFolders(args: string[]): string[] { + return this.helper.getPositionalArguments(args, OptionsWithArguments, OptionsWithoutArguments); + } +} diff --git a/src/client/testing/nosetest/services/discoveryService.ts b/src/client/testing/nosetest/services/discoveryService.ts new file mode 100644 index 000000000000..616b40d14f6c --- /dev/null +++ b/src/client/testing/nosetest/services/discoveryService.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, named } from 'inversify'; +import { CancellationTokenSource } from 'vscode'; +import { IServiceContainer } from '../../../ioc/types'; +import { NOSETEST_PROVIDER } from '../../common/constants'; +import { Options } from '../../common/runner'; +import { ITestDiscoveryService, ITestRunner, ITestsParser, TestDiscoveryOptions, Tests } from '../../common/types'; +import { IArgumentsService, TestFilter } from '../../types'; + +@injectable() +export class TestDiscoveryService implements ITestDiscoveryService { + private argsService: IArgumentsService; + private runner: ITestRunner; + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(ITestsParser) @named(NOSETEST_PROVIDER) private testParser: ITestsParser + ) { + this.argsService = this.serviceContainer.get(IArgumentsService, NOSETEST_PROVIDER); + this.runner = this.serviceContainer.get(ITestRunner); + } + public async discoverTests(options: TestDiscoveryOptions): Promise { + // Remove unwanted arguments. + const args = this.argsService.filterArguments(options.args, TestFilter.discovery); + + const token = options.token ? options.token : new CancellationTokenSource().token; + const runOptions: Options = { + args: ['--collect-only', '-vvv'].concat(args), + cwd: options.cwd, + workspaceFolder: options.workspaceFolder, + token, + outChannel: options.outChannel + }; + + const data = await this.runner.run(NOSETEST_PROVIDER, runOptions); + if (options.token && options.token.isCancellationRequested) { + return Promise.reject('cancelled'); + } + + return this.testParser.parse(data, options); + } +} diff --git a/src/client/testing/nosetest/services/parserService.ts b/src/client/testing/nosetest/services/parserService.ts new file mode 100644 index 000000000000..80fb184dfc97 --- /dev/null +++ b/src/client/testing/nosetest/services/parserService.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { convertFileToPackage, extractBetweenDelimiters } from '../../common/testUtils'; +import { + ITestsHelper, + ITestsParser, + ParserOptions, + TestFile, + TestFunction, + Tests, + TestSuite +} from '../../common/types'; + +const NOSE_WANT_FILE_PREFIX = 'nose.selector: DEBUG: wantFile '; +const NOSE_WANT_FILE_SUFFIX = '.py? True'; +const NOSE_WANT_FILE_SUFFIX_WITHOUT_EXT = '? True'; + +@injectable() +export class TestsParser implements ITestsParser { + constructor(@inject(ITestsHelper) private testsHelper: ITestsHelper) {} + public parse(content: string, options: ParserOptions): Tests { + let testFiles = this.getTestFiles(content, options); + // Exclude tests that don't have any functions or test suites. + testFiles = testFiles.filter((testFile) => testFile.suites.length > 0 || testFile.functions.length > 0); + return this.testsHelper.flattenTestFiles(testFiles, options.cwd); + } + + private getTestFiles(content: string, options: ParserOptions) { + let logOutputLines: string[] = ['']; + const testFiles: TestFile[] = []; + content.split(/\r?\n/g).forEach((line, index, lines) => { + if ( + (line.startsWith(NOSE_WANT_FILE_PREFIX) && line.endsWith(NOSE_WANT_FILE_SUFFIX)) || + index === lines.length - 1 + ) { + // process the previous lines. + this.parseNoseTestModuleCollectionResult(options.cwd, logOutputLines, testFiles); + logOutputLines = ['']; + } + + if (index === 0) { + if (content.startsWith(os.EOL) || lines.length > 1) { + this.appendLine(line, logOutputLines); + return; + } + logOutputLines[logOutputLines.length - 1] += line; + return; + } + if (index === lines.length - 1) { + logOutputLines[logOutputLines.length - 1] += line; + return; + } + this.appendLine(line, logOutputLines); + return; + }); + + return testFiles; + } + private appendLine(line: string, logOutputLines: string[]) { + const lastLineIndex = logOutputLines.length - 1; + logOutputLines[lastLineIndex] += line; + + // Check whether the previous line is something that we need. + // What we need is a line that ends with ? True, + // and starts with nose.selector: DEBUG: want. + if (logOutputLines[lastLineIndex].endsWith('? True')) { + logOutputLines.push(''); + } else { + // We don't need this line + logOutputLines[lastLineIndex] = ''; + } + } + + private parseNoseTestModuleCollectionResult(rootDirectory: string, lines: string[], testFiles: TestFile[]) { + let currentPackage: string = ''; + let fileName = ''; + let testFile: TestFile; + const resource = Uri.file(rootDirectory); + // tslint:disable-next-line: max-func-body-length + lines.forEach((line) => { + if (line.startsWith(NOSE_WANT_FILE_PREFIX) && line.endsWith(NOSE_WANT_FILE_SUFFIX)) { + fileName = line.substring(NOSE_WANT_FILE_PREFIX.length); + fileName = fileName.substring(0, fileName.lastIndexOf(NOSE_WANT_FILE_SUFFIX_WITHOUT_EXT)); + + // We need to display the path relative to the current directory. + fileName = fileName.substring(rootDirectory.length + 1); + // we don't care about the compiled file. + if (path.extname(fileName) === '.pyc' || path.extname(fileName) === '.pyo') { + fileName = fileName.substring(0, fileName.length - 1); + } + currentPackage = convertFileToPackage(fileName); + const fullyQualifiedName = path.isAbsolute(fileName) ? fileName : path.resolve(rootDirectory, fileName); + testFile = { + resource, + functions: [], + suites: [], + name: fileName, + nameToRun: fileName, + xmlName: currentPackage, + time: 0, + functionsFailed: 0, + functionsPassed: 0, + fullPath: fullyQualifiedName + }; + testFiles.push(testFile); + return; + } + + if (line.startsWith("nose.selector: DEBUG: wantClass ? True"); + const clsName = path.extname(name).substring(1); + const testSuite: TestSuite = { + resource, + name: clsName, + nameToRun: `${fileName}:${clsName}`, + functions: [], + suites: [], + xmlName: name, + time: 0, + isUnitTest: false, + isInstance: false, + functionsFailed: 0, + functionsPassed: 0 + }; + testFile.suites.push(testSuite); + return; + } + if (line.startsWith('nose.selector: DEBUG: wantClass ')) { + const name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass ', '? True'); + const testSuite: TestSuite = { + resource, + name: path.extname(name).substring(1), + nameToRun: `${fileName}:.${name}`, + functions: [], + suites: [], + xmlName: name, + time: 0, + isUnitTest: false, + isInstance: false, + functionsFailed: 0, + functionsPassed: 0 + }; + testFile.suites.push(testSuite); + return; + } + if (line.startsWith('nose.selector: DEBUG: wantMethod ? True' + ); + const fnName = path.extname(name).substring(1); + const clsName = path.basename(name, path.extname(name)); + const fn: TestFunction = { + resource, + name: fnName, + nameToRun: `${fileName}:${clsName}.${fnName}`, + time: 0, + functionsFailed: 0, + functionsPassed: 0 + }; + + const cls = testFile.suites.find((suite) => suite.name === clsName); + if (cls) { + cls.functions.push(fn); + } + return; + } + if (line.startsWith('nose.selector: DEBUG: wantFunction { + const fs = this.serviceContainer.get(IFileSystem); + for (const cfg of ['.noserc', 'nose.cfg']) { + if (await fs.fileExists(path.join(wkspace.fsPath, cfg))) { + return true; + } + } + return false; + } + public async configure(wkspace: Uri): Promise { + const args: string[] = []; + const configFileOptionLabel = 'Use existing config file'; + // If a config file exits, there's nothing to be configured. + if (await this.requiresUserToConfigure(wkspace)) { + return; + } + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); + if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { + args.push(testDir); + } + const installed = await this.installer.isInstalled(Product.nosetest); + if (!installed) { + await this.installer.install(Product.nosetest); + } + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.nosetest, args); + } +} diff --git a/src/client/testing/pytest/main.ts b/src/client/testing/pytest/main.ts new file mode 100644 index 000000000000..3873db1c5b1b --- /dev/null +++ b/src/client/testing/pytest/main.ts @@ -0,0 +1,89 @@ +'use strict'; + +import { Uri } from 'vscode'; +import { Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { PYTEST_PROVIDER } from '../common/constants'; +import { BaseTestManager } from '../common/managers/baseTestManager'; +import { + ITestMessageService, + ITestsHelper, + TestDiscoveryOptions, + TestRunOptions, + Tests, + TestsToRun +} from '../common/types'; +import { IArgumentsService, IPythonTestMessage, ITestManagerRunner, TestFilter } from '../types'; + +export class TestManager extends BaseTestManager { + private readonly argsService: IArgumentsService; + private readonly helper: ITestsHelper; + private readonly runner: ITestManagerRunner; + private readonly testMessageService: ITestMessageService; + public get enabled() { + return this.settings.testing.pytestEnabled; + } + constructor(workspaceFolder: Uri, rootDirectory: string, serviceContainer: IServiceContainer) { + super(PYTEST_PROVIDER, Product.pytest, workspaceFolder, rootDirectory, serviceContainer); + this.argsService = this.serviceContainer.get(IArgumentsService, this.testProvider); + this.helper = this.serviceContainer.get(ITestsHelper); + this.runner = this.serviceContainer.get(ITestManagerRunner, this.testProvider); + this.testMessageService = this.serviceContainer.get( + ITestMessageService, + this.testProvider + ); + } + public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { + const args = this.settings.testing.pytestArgs.slice(0); + return { + workspaceFolder: this.workspaceFolder, + cwd: this.rootDirectory, + args, + token: this.testDiscoveryCancellationToken!, + ignoreCache, + outChannel: this.outputChannel + }; + } + public async runTestImpl( + tests: Tests, + testsToRun?: TestsToRun, + runFailedTests?: boolean, + debug?: boolean + ): Promise { + let args: string[]; + + const runAllTests = this.helper.shouldRunAllTests(testsToRun); + if (debug) { + args = this.argsService.filterArguments( + this.settings.testing.pytestArgs, + runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific + ); + } else { + args = this.argsService.filterArguments( + this.settings.testing.pytestArgs, + runAllTests ? TestFilter.runAll : TestFilter.runSpecific + ); + } + + if (runFailedTests === true && args.indexOf('--lf') === -1 && args.indexOf('--last-failed') === -1) { + args.splice(0, 0, '--last-failed'); + } + const options: TestRunOptions = { + workspaceFolder: this.workspaceFolder, + cwd: this.rootDirectory, + tests, + args, + testsToRun, + debug, + token: this.testRunnerCancellationToken!, + outChannel: this.outputChannel + }; + const testResults = await this.runner.runTest(this.testResultsService, options, this); + const messages: IPythonTestMessage[] = await this.testMessageService.getFilteredTestMessages( + this.rootDirectory, + testResults + ); + await this.updateDiagnostics(tests, messages); + return testResults; + } +} diff --git a/src/client/testing/pytest/runner.ts b/src/client/testing/pytest/runner.ts new file mode 100644 index 000000000000..cde8e4765125 --- /dev/null +++ b/src/client/testing/pytest/runner.ts @@ -0,0 +1,120 @@ +'use strict'; +import { inject, injectable } from 'inversify'; +import { IFileSystem, TemporaryFile } from '../../common/platform/types'; +import { noop } from '../../common/utils/misc'; +import { IServiceContainer } from '../../ioc/types'; +import { PYTEST_PROVIDER } from '../common/constants'; +import { Options } from '../common/runner'; +import { + ITestDebugLauncher, + ITestManager, + ITestResultsService, + ITestRunner, + IXUnitParser, + LaunchOptions, + TestRunOptions, + Tests +} from '../common/types'; +import { IArgumentsHelper, IArgumentsService, ITestManagerRunner } from '../types'; + +const JunitXmlArgOld = '--junitxml'; +const JunitXmlArg = '--junit-xml'; +@injectable() +export class TestManagerRunner implements ITestManagerRunner { + private readonly argsService: IArgumentsService; + private readonly argsHelper: IArgumentsHelper; + private readonly testRunner: ITestRunner; + private readonly xUnitParser: IXUnitParser; + private readonly fs: IFileSystem; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.argsService = serviceContainer.get(IArgumentsService, PYTEST_PROVIDER); + this.argsHelper = serviceContainer.get(IArgumentsHelper); + this.testRunner = serviceContainer.get(ITestRunner); + this.xUnitParser = this.serviceContainer.get(IXUnitParser); + this.fs = this.serviceContainer.get(IFileSystem); + } + public async runTest( + testResultsService: ITestResultsService, + options: TestRunOptions, + _: ITestManager + ): Promise { + let testPaths: string[] = []; + if (options.testsToRun && options.testsToRun.testFolder) { + testPaths = testPaths.concat(options.testsToRun.testFolder.map((f) => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testFile) { + testPaths = testPaths.concat(options.testsToRun.testFile.map((f) => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testSuite) { + testPaths = testPaths.concat(options.testsToRun.testSuite.map((f) => f.nameToRun)); + } + if (options.testsToRun && options.testsToRun.testFunction) { + testPaths = testPaths.concat(options.testsToRun.testFunction.map((f) => f.nameToRun)); + } + + let deleteJUnitXmlFile: Function = noop; + const args = options.args; + try { + const xmlLogResult = await this.getJUnitXmlFile(args); + const xmlLogFile = xmlLogResult.filePath; + deleteJUnitXmlFile = xmlLogResult.dispose; + // Remove the '--junitxml' or '--junit-xml' if it exists, and add it with our path. + const testArgs = this.argsService.filterArguments(args, [JunitXmlArg, JunitXmlArgOld]); + testArgs.splice(0, 0, `${JunitXmlArg}=${xmlLogFile}`); + + testArgs.splice(0, 0, '--rootdir', options.workspaceFolder.fsPath); + testArgs.splice(0, 0, '--override-ini', 'junit_family=xunit1'); + + // Positional arguments control the tests to be run. + testArgs.push(...testPaths); + + if (options.debug) { + const debugLauncher = this.serviceContainer.get(ITestDebugLauncher); + const debuggerArgs = [options.cwd, 'pytest'].concat(testArgs); + const launchOptions: LaunchOptions = { + cwd: options.cwd, + args: debuggerArgs, + token: options.token, + outChannel: options.outChannel, + testProvider: PYTEST_PROVIDER + }; + await debugLauncher.launchDebugger(launchOptions); + } else { + const runOptions: Options = { + args: testArgs, + cwd: options.cwd, + outChannel: options.outChannel, + token: options.token, + workspaceFolder: options.workspaceFolder + }; + await this.testRunner.run(PYTEST_PROVIDER, runOptions); + } + + return options.debug + ? options.tests + : await this.updateResultsFromLogFiles(options.tests, xmlLogFile, testResultsService); + } catch (ex) { + return Promise.reject(ex); + } finally { + deleteJUnitXmlFile(); + } + } + + private async updateResultsFromLogFiles( + tests: Tests, + outputXmlFile: string, + testResultsService: ITestResultsService + ): Promise { + await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile); + testResultsService.updateResults(tests); + return tests; + } + + private async getJUnitXmlFile(args: string[]): Promise { + const xmlFile = this.argsHelper.getOptionValues(args, JunitXmlArg); + if (typeof xmlFile === 'string') { + return { filePath: xmlFile, dispose: noop }; + } + return this.fs.createTemporaryFile('.xml'); + } +} diff --git a/src/client/testing/pytest/services/argsService.ts b/src/client/testing/pytest/services/argsService.ts new file mode 100644 index 000000000000..ffa80c329dfe --- /dev/null +++ b/src/client/testing/pytest/services/argsService.ts @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IServiceContainer } from '../../../ioc/types'; +import { IArgumentsHelper, IArgumentsService, TestFilter } from '../../types'; + +const OptionsWithArguments = [ + '-c', + '-k', + '-m', + '-o', + '-p', + '-r', + '-W', + '-n', // -n is a pytest-xdist option + '--assert', + '--basetemp', + '--cache-show', + '--capture', + '--color', + '--confcutdir', + '--cov', + '--cov-config', + '--cov-fail-under', + '--cov-report', + '--deselect', + '--dist', + '--doctest-glob', + '--doctest-report', + '--durations', + '--ignore', + '--ignore-glob', + '--import-mode', + '--junit-prefix', + '--junit-xml', + '--last-failed-no-failures', + '--lfnf', + '--log-auto-indent', + '--log-cli-date-format', + '--log-cli-format', + '--log-cli-level', + '--log-date-format', + '--log-file', + '--log-file-date-format', + '--log-file-format', + '--log-file-level', + '--log-format', + '--log-level', + '--maxfail', + '--override-ini', + '--pastebin', + '--pdbcls', + '--pythonwarnings', + '--result-log', + '--rootdir', + '--show-capture', + '--tb', + '--verbosity', + '--max-slave-restart', + '--numprocesses', + '--rsyncdir', + '--rsyncignore', + '--tx' +]; + +const OptionsWithoutArguments = [ + '--cache-clear', + '--collect-in-virtualenv', + '--collect-only', + '--co', + '--continue-on-collection-errors', + '--cov-append', + '--cov-branch', + '--debug', + '--disable-pytest-warnings', + '--disable-warnings', + '--doctest-continue-on-failure', + '--doctest-ignore-import-errors', + '--doctest-modules', + '--exitfirst', + '--failed-first', + '--ff', + '--fixtures', + '--fixtures-per-test', + '--force-sugar', + '--full-trace', + '--funcargs', + '--help', + '--keep-duplicates', + '--last-failed', + '--lf', + '--markers', + '--new-first', + '--nf', + '--no-cov', + '--no-cov-on-fail', + '--no-print-logs', + '--noconftest', + '--old-summary', + '--pdb', + '--pyargs', + '-PyTest, Unittest-pyargs', + '--quiet', + '--runxfail', + '--setup-only', + '--setup-plan', + '--setup-show', + '--showlocals', + '--stepwise', + '--sw', + '--stepwise-skip', + '--strict', + '--strict-markers', + '--trace-config', + '--verbose', + '--version', + '-V', + '-h', + '-l', + '-q', + '-s', + '-v', + '-x', + '--boxed', + '--forked', + '--looponfail', + '--trace', + '--tx', + '-d' +]; + +@injectable() +export class ArgumentsService implements IArgumentsService { + private readonly helper: IArgumentsHelper; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.helper = serviceContainer.get(IArgumentsHelper); + } + public getKnownOptions(): { withArgs: string[]; withoutArgs: string[] } { + return { + withArgs: OptionsWithArguments, + withoutArgs: OptionsWithoutArguments + }; + } + public getOptionValue(args: string[], option: string): string | string[] | undefined { + return this.helper.getOptionValues(args, option); + } + // tslint:disable-next-line: max-func-body-length + public filterArguments(args: string[], argumentToRemoveOrFilter: string[] | TestFilter): string[] { + const optionsWithoutArgsToRemove: string[] = []; + const optionsWithArgsToRemove: string[] = []; + // Positional arguments in pytest are test directories and files. + // So if we want to run a specific test, then remove positional args. + let removePositionalArgs = false; + if (Array.isArray(argumentToRemoveOrFilter)) { + argumentToRemoveOrFilter.forEach((item) => { + if (OptionsWithArguments.indexOf(item) >= 0) { + optionsWithArgsToRemove.push(item); + } + if (OptionsWithoutArguments.indexOf(item) >= 0) { + optionsWithoutArgsToRemove.push(item); + } + }); + } else { + switch (argumentToRemoveOrFilter) { + case TestFilter.removeTests: { + optionsWithoutArgsToRemove.push( + ...['--lf', '--last-failed', '--ff', '--failed-first', '--nf', '--new-first'] + ); + optionsWithArgsToRemove.push(...['-k', '-m', '--lfnf', '--last-failed-no-failures']); + removePositionalArgs = true; + break; + } + case TestFilter.discovery: { + optionsWithoutArgsToRemove.push( + ...[ + '-x', + '--exitfirst', + '--fixtures', + '--funcargs', + '--fixtures-per-test', + '--pdb', + '--lf', + '--last-failed', + '--ff', + '--failed-first', + '--nf', + '--new-first', + '--cache-show', + '-v', + '--verbose', + '-q', + '-quiet', + '-l', + '--showlocals', + '--no-print-logs', + '--debug', + '--setup-only', + '--setup-show', + '--setup-plan', + '--trace' + ] + ); + optionsWithArgsToRemove.push( + ...[ + '-m', + '--maxfail', + '--pdbcls', + '--capture', + '--lfnf', + '--last-failed-no-failures', + '--verbosity', + '-r', + '--tb', + '--rootdir', + '--show-capture', + '--durations', + '--junit-xml', + '--junit-prefix', + '--result-log', + '-W', + '--pythonwarnings', + '--log-*' + ] + ); + removePositionalArgs = true; + break; + } + case TestFilter.debugAll: + case TestFilter.runAll: { + optionsWithoutArgsToRemove.push(...['--collect-only', '--trace']); + break; + } + case TestFilter.debugSpecific: + case TestFilter.runSpecific: { + optionsWithoutArgsToRemove.push( + ...[ + '--collect-only', + '--lf', + '--last-failed', + '--ff', + '--failed-first', + '--nf', + '--new-first', + '--trace' + ] + ); + optionsWithArgsToRemove.push(...['-k', '-m', '--lfnf', '--last-failed-no-failures']); + removePositionalArgs = true; + break; + } + default: { + throw new Error(`Unsupported Filter '${argumentToRemoveOrFilter}'`); + } + } + } + + let filteredArgs = args.slice(); + if (removePositionalArgs) { + const positionalArgs = this.helper.getPositionalArguments( + filteredArgs, + OptionsWithArguments, + OptionsWithoutArguments + ); + filteredArgs = filteredArgs.filter((item) => positionalArgs.indexOf(item) === -1); + } + return this.helper.filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); + } + public getTestFolders(args: string[]): string[] { + const testDirs = this.helper.getOptionValues(args, '--rootdir'); + if (typeof testDirs === 'string') { + return [testDirs]; + } + if (Array.isArray(testDirs) && testDirs.length > 0) { + return testDirs; + } + const positionalArgs = this.helper.getPositionalArguments(args, OptionsWithArguments, OptionsWithoutArguments); + // Positional args in pytest are files or directories. + // Remove files from the args, and what's left are test directories. + // If users enter test modules/methods, then its not supported. + return positionalArgs.filter((arg) => !arg.toUpperCase().endsWith('.PY')); + } +} diff --git a/src/client/testing/pytest/services/discoveryService.ts b/src/client/testing/pytest/services/discoveryService.ts new file mode 100644 index 000000000000..965336217405 --- /dev/null +++ b/src/client/testing/pytest/services/discoveryService.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { CancellationTokenSource } from 'vscode'; +import { IServiceContainer } from '../../../ioc/types'; +import { PYTEST_PROVIDER } from '../../common/constants'; +import { ITestDiscoveryService, ITestsHelper, TestDiscoveryOptions, Tests } from '../../common/types'; +import { IArgumentsService, TestFilter } from '../../types'; + +@injectable() +export class TestDiscoveryService implements ITestDiscoveryService { + private argsService: IArgumentsService; + private helper: ITestsHelper; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.argsService = this.serviceContainer.get(IArgumentsService, PYTEST_PROVIDER); + this.helper = this.serviceContainer.get(ITestsHelper); + } + public async discoverTests(options: TestDiscoveryOptions): Promise { + const args = this.buildTestCollectionArgs(options); + + // Collect tests for each test directory separately and merge. + const testDirectories = this.argsService.getTestFolders(options.args); + if (testDirectories.length === 0) { + const opts = { + ...options, + args + }; + return this.discoverTestsInTestDirectory(opts); + } + const results = await Promise.all( + testDirectories.map((testDir) => { + // Add test directory as a positional argument. + const opts = { + ...options, + args: [...args, testDir] + }; + return this.discoverTestsInTestDirectory(opts); + }) + ); + + return this.helper.mergeTests(results); + } + protected buildTestCollectionArgs(options: TestDiscoveryOptions) { + // Remove unwnted arguments (which happen to be test directories & test specific args). + const args = this.argsService.filterArguments(options.args, TestFilter.discovery); + if (options.ignoreCache && args.indexOf('--cache-clear') === -1) { + args.splice(0, 0, '--cache-clear'); + } + if (args.indexOf('-s') === -1) { + args.splice(0, 0, '-s'); + } + args.splice(0, 0, '--rootdir', options.workspaceFolder.fsPath); + return args; + } + protected async discoverTestsInTestDirectory(options: TestDiscoveryOptions): Promise { + const token = options.token ? options.token : new CancellationTokenSource().token; + const discoveryOptions = { ...options }; + discoveryOptions.args = ['discover', 'pytest', '--', ...options.args]; + discoveryOptions.token = token; + + const discoveryService = this.serviceContainer.get(ITestDiscoveryService, 'common'); + if (discoveryOptions.token && discoveryOptions.token.isCancellationRequested) { + return Promise.reject('cancelled'); + } + + return discoveryService.discoverTests(discoveryOptions); + } +} diff --git a/src/client/testing/pytest/services/testMessageService.ts b/src/client/testing/pytest/services/testMessageService.ts new file mode 100644 index 000000000000..049833c001e9 --- /dev/null +++ b/src/client/testing/pytest/services/testMessageService.ts @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Location, Position, Range, TextLine, Uri, workspace } from 'vscode'; +import '../../../common/extensions'; +import { ProductNames } from '../../../common/installer/productNames'; +import { IFileSystem } from '../../../common/platform/types'; +import { Product } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { FlattenedTestFunction, ITestMessageService, Tests, TestStatus } from '../../common/types'; +import { ILocationStackFrameDetails, IPythonTestMessage, PythonTestMessageSeverity } from '../../types'; + +@injectable() +export class TestMessageService implements ITestMessageService { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + /** + * Condense the test details down to just the potentially relevant information. Messages + * should only be created for tests that were actually run. + * + * @param rootDirectory + * @param testResults Details about all known tests. + */ + public async getFilteredTestMessages(rootDirectory: string, testResults: Tests): Promise { + const testFuncs = testResults.testFunctions.reduce((filtered, test) => { + if (test.testFunction.passed !== undefined || test.testFunction.status === TestStatus.Skipped) { + filtered.push(test); + } + return filtered; + }, []); + const messages: IPythonTestMessage[] = []; + for (const tf of testFuncs) { + const nameToRun = tf.testFunction.nameToRun; + const provider = ProductNames.get(Product.pytest)!; + const status = tf.testFunction.status!; + if (status === TestStatus.Pass) { + // If the test passed, there's not much to do with it. + const msg: IPythonTestMessage = { + code: nameToRun, + severity: PythonTestMessageSeverity.Pass, + provider: provider, + testTime: tf.testFunction.time, + status: status, + testFilePath: tf.parentTestFile.fullPath + }; + messages.push(msg); + } else { + // If the test did not pass, we need to parse the traceback to find each line in + // their respective files so they can be included as related information for the + // diagnostic. + const locationStack = await this.getLocationStack(rootDirectory, tf); + const message = tf.testFunction.message; + const testFilePath = tf.parentTestFile.fullPath; + let severity = PythonTestMessageSeverity.Error; + if (tf.testFunction.status === TestStatus.Skipped) { + severity = PythonTestMessageSeverity.Skip; + } + + const msg: IPythonTestMessage = { + code: nameToRun, + message: message, + severity: severity, + provider: provider, + traceback: tf.testFunction.traceback, + testTime: tf.testFunction.time, + testFilePath: testFilePath, + status: status, + locationStack: locationStack + }; + messages.push(msg); + } + } + return messages; + } + /** + * Given a FlattenedTestFunction, parse its traceback to piece together where each line in the + * traceback was in its respective file and grab the entire text of each line so they can be + * included in the Diagnostic as related information. + * + * @param testFunction The FlattenedTestFunction with the traceback that we need to parse. + */ + private async getLocationStack( + rootDirectory: string, + testFunction: FlattenedTestFunction + ): Promise { + const locationStack: ILocationStackFrameDetails[] = []; + if (testFunction.testFunction.traceback) { + const fileMatches = + testFunction.testFunction.traceback.match(/^((\.\.[\\\/])*.+\.py)\:(\d+)\:.*$/gim) || []; + for (const fileDetailsMatch of fileMatches) { + const fileDetails = fileDetailsMatch.split(':'); + let filePath = fileDetails[0]; + filePath = path.isAbsolute(filePath) ? filePath : path.resolve(rootDirectory, filePath); + const fileUri = Uri.file(filePath); + const file = await workspace.openTextDocument(fileUri); + const fileLineNum = parseInt(fileDetails[1], 10); + const line = file.lineAt(fileLineNum - 1); + const location = new Location( + fileUri, + new Range( + new Position(fileLineNum - 1, line.firstNonWhitespaceCharacterIndex), + new Position(fileLineNum - 1, line.text.length) + ) + ); + const stackFrame: ILocationStackFrameDetails = { + location: location, + lineText: file.getText(location.range) + }; + locationStack.push(stackFrame); + } + } + // Find where the file the test was defined. + let testSourceFilePath = testFunction.testFunction.file!; + testSourceFilePath = path.isAbsolute(testSourceFilePath) + ? testSourceFilePath + : path.resolve(rootDirectory, testSourceFilePath); + const testSourceFileUri = Uri.file(testSourceFilePath); + const testSourceFile = await workspace.openTextDocument(testSourceFileUri); + let testDefLine: TextLine | null = null; + let lineNum = testFunction.testFunction.line!; + let lineText: string = ''; + let trimmedLineText: string = ''; + const testDefPrefix = 'def '; + const testAsyncDefPrefix = 'async def '; + let prefix = ''; + + while (testDefLine === null) { + const possibleTestDefLine = testSourceFile.lineAt(lineNum); + lineText = possibleTestDefLine.text; + trimmedLineText = lineText.trimLeft()!; + if (trimmedLineText.toLowerCase().startsWith(testDefPrefix)) { + testDefLine = possibleTestDefLine; + prefix = testDefPrefix; + } else if (trimmedLineText.toLowerCase().startsWith(testAsyncDefPrefix)) { + testDefLine = possibleTestDefLine; + prefix = testAsyncDefPrefix; + } else { + // The test definition may have been decorated, and there may be multiple + // decorations, so move to the next line and check it. + lineNum += 1; + } + } + const matches = trimmedLineText!.slice(prefix.length).match(/[^ \(:]+/); + const testSimpleName = matches ? matches[0] : ''; + const testDefStartCharNum = lineText.length - trimmedLineText.length + prefix.length; + const testDefEndCharNum = testDefStartCharNum + testSimpleName.length; + const lineStart = new Position(testDefLine!.lineNumber, testDefStartCharNum); + const lineEnd = new Position(testDefLine!.lineNumber, testDefEndCharNum); + const lineRange = new Range(lineStart, lineEnd); + const testDefLocation = new Location(testSourceFileUri, lineRange); + const testSourceLocationDetails = { location: testDefLocation, lineText: testSourceFile.getText(lineRange) }; + locationStack.unshift(testSourceLocationDetails); + + // Put the class declaration at the top of the stack if the test was imported. + if (testFunction.parentTestSuite !== undefined) { + // This could be an imported test method + const fs = this.serviceContainer.get(IFileSystem); + if ( + !fs.arePathsSame( + Uri.file(testFunction.parentTestFile.fullPath).fsPath, + locationStack[0].location.uri.fsPath + ) + ) { + // test method was imported, so reference class declaration line. + // this should be the first thing in the stack to show where the failure/error originated. + locationStack.unshift(await this.getParentSuiteLocation(testFunction)); + } + } + return locationStack; + } + /** + * The test that's associated with the FlattenedtestFunction was imported from another file, as the file + * location found in the traceback that shows what file the test was actually defined in is different than + * the file that the test was executed in. This must also mean that the test was part of a class that was + * imported and then inherited by the class that was actually run in the file. + * + * Test classes can be defined inside of other test classes, and even nested test classes of those that were + * imported will be discovered and ran. Luckily, for pytest, the entire chain of classes is preserved in the + * test's ID. However, in order to keep the Diagnostic as relevant as possible, it should point only at the + * most-nested test class that exists in the file that the test was actually run in, in order to provide the + * most context. This method attempts to go as far down the chain as it can, and resolves to the + * LocationStackFrameDetails for that test class. + * + * @param testFunction The FlattenedTestFunction that was executed. + */ + private async getParentSuiteLocation(testFunction: FlattenedTestFunction): Promise { + const suiteStackWithFileAndTest = testFunction.testFunction.nameToRun.replace('::()', '').split('::'); + // Don't need the file location or the test's name. + const suiteStack = suiteStackWithFileAndTest.slice(1, suiteStackWithFileAndTest.length - 1); + const testFileUri = Uri.file(testFunction.parentTestFile.fullPath); + const testFile = await workspace.openTextDocument(testFileUri); + const testFileLines = testFile.getText().splitLines({ trim: false, removeEmptyEntries: false }); + const reversedTestFileLines = testFileLines.slice().reverse(); + // Track the end of the parent scope. + let parentScopeEndIndex = 0; + let parentScopeStartIndex = testFileLines.length; + let parentIndentation: number | undefined; + const suiteLocationStackFrameDetails: ILocationStackFrameDetails[] = []; + + const classPrefix = 'class '; + while (suiteStack.length > 0) { + let indentation: number = 0; + let prevLowestIndentation: number | undefined; + // Get the name of the suite on top of the stack so it can be located. + const suiteName = suiteStack.shift()!; + let suiteDefLineIndex: number | undefined; + for (let index = parentScopeEndIndex; index < parentScopeStartIndex; index += 1) { + const lineText = reversedTestFileLines[index]; + if (lineText.trim().length === 0) { + // This line is just whitespace. + continue; + } + const trimmedLineText = lineText.trimLeft()!; + if (!trimmedLineText.toLowerCase().startsWith(classPrefix)) { + // line is not a class declaration + continue; + } + const matches = trimmedLineText.slice(classPrefix.length).match(/[^ \(:]+/); + const lineClassName = matches ? matches[0] : undefined; + + // Check if the indentation is proper. + if (parentIndentation === undefined) { + // The parentIndentation hasn't been set yet, so we are looking for a class that was + // defined in the global scope of the module. + if (trimmedLineText.length === lineText.length) { + // This line doesn't start with whitespace. + if (lineClassName === suiteName) { + // This is the line that we want. + suiteDefLineIndex = index; + indentation = 0; + // We have our line for the root suite declaration, so move on to processing the Location. + break; + } else { + // This is not the line we want, but may be the line that ends the scope of the class we want. + parentScopeEndIndex = index + 1; + } + } + } else { + indentation = lineText.length - trimmedLineText.length; + if (indentation <= parentIndentation) { + // This is not the line we want, but may be the line that ends the scope of the parent class. + parentScopeEndIndex = index + 1; + continue; + } + if (prevLowestIndentation === undefined || indentation < prevLowestIndentation) { + if (lineClassName === suiteName) { + // This might be the line that we want. + suiteDefLineIndex = index; + prevLowestIndentation = indentation; + } else { + // This is not the line we want, but may be the line that ends the scope of the class we want. + parentScopeEndIndex = index + 1; + } + } + } + } + if (suiteDefLineIndex === undefined) { + // Could not find the suite declaration line, so give up and move on with the latest one that we found. + break; + } + // Found the line to process. + parentScopeStartIndex = suiteDefLineIndex; + parentIndentation = indentation!; + + // Invert the index to get the unreversed equivalent. + const realIndex = reversedTestFileLines.length - 1 - suiteDefLineIndex; + const startChar = indentation! + classPrefix.length; + const suiteStartPos = new Position(realIndex, startChar); + const suiteEndPos = new Position(realIndex, startChar + suiteName!.length); + const suiteRange = new Range(suiteStartPos, suiteEndPos); + const suiteLocation = new Location(testFileUri, suiteRange); + suiteLocationStackFrameDetails.push({ location: suiteLocation, lineText: testFile.getText(suiteRange) }); + } + return suiteLocationStackFrameDetails[suiteLocationStackFrameDetails.length - 1]; + } +} diff --git a/src/client/testing/pytest/testConfigurationManager.ts b/src/client/testing/pytest/testConfigurationManager.ts new file mode 100644 index 000000000000..2c3b627342a1 --- /dev/null +++ b/src/client/testing/pytest/testConfigurationManager.ts @@ -0,0 +1,56 @@ +import * as path from 'path'; +import { QuickPickItem, Uri } from 'vscode'; +import { IFileSystem } from '../../common/platform/types'; +import { Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { TestConfigurationManager } from '../common/managers/testConfigurationManager'; +import { ITestConfigSettingsService } from '../types'; + +export class ConfigurationManager extends TestConfigurationManager { + constructor(workspace: Uri, serviceContainer: IServiceContainer, cfg?: ITestConfigSettingsService) { + super(workspace, Product.pytest, serviceContainer, cfg); + } + public async requiresUserToConfigure(wkspace: Uri): Promise { + const configFiles = await this.getConfigFiles(wkspace.fsPath); + // If a config file exits, there's nothing to be configured. + if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { + return false; + } + return true; + } + public async configure(wkspace: Uri) { + const args: string[] = []; + const configFileOptionLabel = 'Use existing config file'; + const options: QuickPickItem[] = []; + const configFiles = await this.getConfigFiles(wkspace.fsPath); + // If a config file exits, there's nothing to be configured. + if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { + return; + } + + if (configFiles.length === 1 && configFiles[0] === 'setup.cfg') { + options.push({ + label: configFileOptionLabel, + description: 'setup.cfg' + }); + } + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs, options); + if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { + args.push(testDir); + } + const installed = await this.installer.isInstalled(Product.pytest); + if (!installed) { + await this.installer.install(Product.pytest); + } + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); + } + private async getConfigFiles(rootDir: string): Promise { + const fs = this.serviceContainer.get(IFileSystem); + const promises = ['pytest.ini', 'tox.ini', 'setup.cfg'].map(async (cfg) => + (await fs.fileExists(path.join(rootDir, cfg))) ? cfg : '' + ); + const values = await Promise.all(promises); + return values.filter((exists) => exists.length > 0); + } +} diff --git a/src/client/testing/serviceRegistry.ts b/src/client/testing/serviceRegistry.ts new file mode 100644 index 000000000000..5ebb8dc4f36f --- /dev/null +++ b/src/client/testing/serviceRegistry.ts @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; +import { IServiceContainer, IServiceManager } from '../ioc/types'; +import { ArgumentsHelper } from './common/argumentsHelper'; +import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from './common/constants'; +import { DebugLauncher } from './common/debugLauncher'; +import { EnablementTracker } from './common/enablementTracker'; +import { TestRunner } from './common/runner'; +import { TestConfigSettingsService } from './common/services/configSettingService'; +import { TestContextService } from './common/services/contextService'; +import { TestDiscoveredTestParser } from './common/services/discoveredTestParser'; +import { TestsDiscoveryService } from './common/services/discovery'; +import { TestCollectionStorageService } from './common/services/storageService'; +import { TestManagerService } from './common/services/testManagerService'; +import { TestResultsService } from './common/services/testResultsService'; +import { TestsStatusUpdaterService } from './common/services/testsStatusService'; +import { ITestDiscoveredTestParser } from './common/services/types'; +import { UnitTestDiagnosticService } from './common/services/unitTestDiagnosticService'; +import { WorkspaceTestManagerService } from './common/services/workspaceTestManagerService'; +import { TestsHelper } from './common/testUtils'; +import { TestFlatteningVisitor } from './common/testVisitors/flatteningVisitor'; +import { TestResultResetVisitor } from './common/testVisitors/resultResetVisitor'; +import { + ITestCollectionStorageService, + ITestContextService, + ITestDebugLauncher, + ITestDiscoveryService, + ITestManager, + ITestManagerFactory, + ITestManagerService, + ITestManagerServiceFactory, + ITestMessageService, + ITestResultsService, + ITestRunner, + ITestsHelper, + ITestsParser, + ITestsStatusUpdaterService, + ITestVisitor, + IUnitTestSocketServer, + IWorkspaceTestManagerService, + IXUnitParser, + TestProvider +} from './common/types'; +import { UpdateTestSettingService } from './common/updateTestSettings'; +import { XUnitParser } from './common/xUnitParser'; +import { UnitTestConfigurationService } from './configuration'; +import { TestConfigurationManagerFactory } from './configurationFactory'; +import { TestResultDisplay } from './display/main'; +import { TestDisplay } from './display/picker'; +import { TestExplorerCommandHandler } from './explorer/commandHandlers'; +import { FailedTestHandler } from './explorer/failedTestHandler'; +import { TestTreeViewProvider } from './explorer/testTreeViewProvider'; +import { TreeViewService } from './explorer/treeView'; +import { UnitTestManagementService } from './main'; +import { registerTypes as registerNavigationTypes } from './navigation/serviceRegistry'; +import { ITestExplorerCommandHandler } from './navigation/types'; +import { TestManager as NoseTestManager } from './nosetest/main'; +import { TestManagerRunner as NoseTestManagerRunner } from './nosetest/runner'; +import { ArgumentsService as NoseTestArgumentsService } from './nosetest/services/argsService'; +import { TestDiscoveryService as NoseTestDiscoveryService } from './nosetest/services/discoveryService'; +import { TestsParser as NoseTestTestsParser } from './nosetest/services/parserService'; +import { TestManager as PyTestTestManager } from './pytest/main'; +import { TestManagerRunner as PytestManagerRunner } from './pytest/runner'; +import { ArgumentsService as PyTestArgumentsService } from './pytest/services/argsService'; +import { TestDiscoveryService as PytestTestDiscoveryService } from './pytest/services/discoveryService'; +import { TestMessageService } from './pytest/services/testMessageService'; +import { + IArgumentsHelper, + IArgumentsService, + ITestConfigSettingsService, + ITestConfigurationManagerFactory, + ITestConfigurationService, + ITestDataItemResource, + ITestDiagnosticService, + ITestDisplay, + ITestManagementService, + ITestManagerRunner, + ITestResultDisplay, + ITestTreeViewProvider, + IUnitTestHelper +} from './types'; +import { UnitTestHelper } from './unittest/helper'; +import { TestManager as UnitTestTestManager } from './unittest/main'; +import { TestManagerRunner as UnitTestTestManagerRunner } from './unittest/runner'; +import { ArgumentsService as UnitTestArgumentsService } from './unittest/services/argsService'; +import { TestDiscoveryService as UnitTestTestDiscoveryService } from './unittest/services/discoveryService'; +import { TestsParser as UnitTestTestsParser } from './unittest/services/parserService'; +import { UnitTestSocketServer } from './unittest/socketServer'; + +export function registerTypes(serviceManager: IServiceManager) { + registerNavigationTypes(serviceManager); + serviceManager.addSingleton(ITestDebugLauncher, DebugLauncher); + serviceManager.addSingleton( + ITestCollectionStorageService, + TestCollectionStorageService + ); + serviceManager.addSingleton( + IWorkspaceTestManagerService, + WorkspaceTestManagerService + ); + + serviceManager.add(ITestsHelper, TestsHelper); + serviceManager.add(ITestDiscoveredTestParser, TestDiscoveredTestParser); + serviceManager.add(ITestDiscoveryService, TestsDiscoveryService, 'common'); + serviceManager.add(IUnitTestSocketServer, UnitTestSocketServer); + serviceManager.addSingleton(ITestContextService, TestContextService); + serviceManager.addSingleton(ITestsStatusUpdaterService, TestsStatusUpdaterService); + + serviceManager.add(ITestResultsService, TestResultsService); + + serviceManager.add(ITestVisitor, TestFlatteningVisitor, 'TestFlatteningVisitor'); + serviceManager.add(ITestVisitor, TestResultResetVisitor, 'TestResultResetVisitor'); + + serviceManager.add(ITestsParser, UnitTestTestsParser, UNITTEST_PROVIDER); + serviceManager.add(ITestsParser, NoseTestTestsParser, NOSETEST_PROVIDER); + + serviceManager.add(ITestDiscoveryService, UnitTestTestDiscoveryService, UNITTEST_PROVIDER); + serviceManager.add(ITestDiscoveryService, PytestTestDiscoveryService, PYTEST_PROVIDER); + serviceManager.add(ITestDiscoveryService, NoseTestDiscoveryService, NOSETEST_PROVIDER); + + serviceManager.add(IArgumentsHelper, ArgumentsHelper); + serviceManager.add(ITestRunner, TestRunner); + serviceManager.add(IXUnitParser, XUnitParser); + serviceManager.add(IUnitTestHelper, UnitTestHelper); + + serviceManager.add(IArgumentsService, PyTestArgumentsService, PYTEST_PROVIDER); + serviceManager.add(IArgumentsService, NoseTestArgumentsService, NOSETEST_PROVIDER); + serviceManager.add(IArgumentsService, UnitTestArgumentsService, UNITTEST_PROVIDER); + serviceManager.add(ITestManagerRunner, PytestManagerRunner, PYTEST_PROVIDER); + serviceManager.add(ITestManagerRunner, NoseTestManagerRunner, NOSETEST_PROVIDER); + serviceManager.add(ITestManagerRunner, UnitTestTestManagerRunner, UNITTEST_PROVIDER); + + serviceManager.addSingleton(ITestConfigurationService, UnitTestConfigurationService); + serviceManager.addSingleton(ITestManagementService, UnitTestManagementService); + serviceManager.addSingleton(ITestResultDisplay, TestResultDisplay); + serviceManager.addSingleton(ITestDisplay, TestDisplay); + serviceManager.addSingleton(ITestConfigSettingsService, TestConfigSettingsService); + serviceManager.addSingleton( + ITestConfigurationManagerFactory, + TestConfigurationManagerFactory + ); + + serviceManager.addSingleton(ITestDiagnosticService, UnitTestDiagnosticService); + serviceManager.addSingleton(ITestMessageService, TestMessageService, PYTEST_PROVIDER); + serviceManager.addSingleton(ITestTreeViewProvider, TestTreeViewProvider); + serviceManager.addSingleton(ITestDataItemResource, TestTreeViewProvider); + serviceManager.addSingleton(ITestExplorerCommandHandler, TestExplorerCommandHandler); + serviceManager.addSingleton(IExtensionSingleActivationService, TreeViewService); + serviceManager.addSingleton( + IExtensionSingleActivationService, + FailedTestHandler + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + EnablementTracker + ); + serviceManager.addSingleton(IExtensionActivationService, UpdateTestSettingService); + + serviceManager.addFactory(ITestManagerFactory, (context) => { + return (testProvider: TestProvider, workspaceFolder: Uri, rootDirectory: string) => { + const serviceContainer = context.container.get(IServiceContainer); + + switch (testProvider) { + case NOSETEST_PROVIDER: { + return new NoseTestManager(workspaceFolder, rootDirectory, serviceContainer); + } + case PYTEST_PROVIDER: { + return new PyTestTestManager(workspaceFolder, rootDirectory, serviceContainer); + } + case UNITTEST_PROVIDER: { + return new UnitTestTestManager(workspaceFolder, rootDirectory, serviceContainer); + } + default: { + throw new Error(`Unrecognized test provider '${testProvider}'`); + } + } + }; + }); + + serviceManager.addFactory(ITestManagerServiceFactory, (context) => { + return (workspaceFolder: Uri) => { + const serviceContainer = context.container.get(IServiceContainer); + const testsHelper = context.container.get(ITestsHelper); + return new TestManagerService(workspaceFolder, testsHelper, serviceContainer); + }; + }); +} diff --git a/src/client/testing/types.ts b/src/client/testing/types.ts new file mode 100644 index 000000000000..3949e0c502f8 --- /dev/null +++ b/src/client/testing/types.ts @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable-next-line:ordered-imports +import { + DiagnosticSeverity, + Disposable, + DocumentSymbolProvider, + Event, + Location, + ProviderResult, + TextDocument, + TreeDataProvider, + TreeItem, + Uri, + WorkspaceFolder +} from 'vscode'; +import { Product, Resource } from '../common/types'; +import { CommandSource } from './common/constants'; +import { + FlattenedTestFunction, + ITestManager, + ITestResultsService, + TestFile, + TestFolder, + TestFunction, + TestRunOptions, + Tests, + TestStatus, + TestsToRun, + TestSuite, + UnitTestProduct +} from './common/types'; + +export const ITestConfigurationService = Symbol('ITestConfigurationService'); +export interface ITestConfigurationService { + displayTestFrameworkError(wkspace: Uri): Promise; + selectTestRunner(placeHolderMessage: string): Promise; + enableTest(wkspace: Uri, product: UnitTestProduct): Promise; + promptToEnableAndConfigureTestFramework(wkspace: Uri): Promise; +} + +export const ITestResultDisplay = Symbol('ITestResultDisplay'); + +export interface ITestResultDisplay extends Disposable { + enabled: boolean; + readonly onDidChange: Event; + displayProgressStatus(testRunResult: Promise, debug?: boolean): void; + displayDiscoverStatus(testDiscovery: Promise, quietMode?: boolean): Promise; +} + +export const ITestDisplay = Symbol('ITestDisplay'); +export interface ITestDisplay { + displayStopTestUI(workspace: Uri, message: string): void; + displayTestUI(cmdSource: CommandSource, wkspace: Uri): void; + selectTestFunction(rootDirectory: string, tests: Tests): Promise; + selectTestFile(rootDirectory: string, tests: Tests): Promise; + displayFunctionTestPickerUI( + cmdSource: CommandSource, + wkspace: Uri, + rootDirectory: string, + file: Uri, + testFunctions: TestFunction[], + debug?: boolean + ): void; +} + +export const ITestManagementService = Symbol('ITestManagementService'); +export interface ITestManagementService { + readonly onDidStatusChange: Event; + activate(symbolProvider: DocumentSymbolProvider): Promise; + getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise; + discoverTestsForDocument(doc: TextDocument): Promise; + autoDiscoverTests(resource: Resource): Promise; + discoverTests( + cmdSource: CommandSource, + resource?: Uri, + ignoreCache?: boolean, + userInitiated?: boolean, + quietMode?: boolean + ): Promise; + stopTests(resource: Uri): Promise; + displayStopUI(message: string): Promise; + displayUI(cmdSource: CommandSource): Promise; + displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean): Promise; + runTestsImpl( + cmdSource: CommandSource, + resource?: Uri, + testsToRun?: TestsToRun, + runFailedTests?: boolean, + debug?: boolean + ): Promise; + runCurrentTestFile(cmdSource: CommandSource): Promise; + + selectAndRunTestFile(cmdSource: CommandSource): Promise; + + selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean): Promise; + + viewOutput(cmdSource: CommandSource): void; +} + +export const ITestConfigSettingsService = Symbol('ITestConfigSettingsService'); +export interface ITestConfigSettingsService { + updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]): Promise; + enable(testDirectory: string | Uri, product: UnitTestProduct): Promise; + disable(testDirectory: string | Uri, product: UnitTestProduct): Promise; + getTestEnablingSetting(product: UnitTestProduct): string; +} + +export interface ITestConfigurationManager { + requiresUserToConfigure(wkspace: Uri): Promise; + configure(wkspace: Uri): Promise; + enable(): Promise; + disable(): Promise; +} + +export const ITestConfigurationManagerFactory = Symbol('ITestConfigurationManagerFactory'); +export interface ITestConfigurationManagerFactory { + create(wkspace: Uri, product: Product, cfg?: ITestConfigSettingsService): ITestConfigurationManager; +} + +export enum TestFilter { + removeTests = 'removeTests', + discovery = 'discovery', + runAll = 'runAll', + runSpecific = 'runSpecific', + debugAll = 'debugAll', + debugSpecific = 'debugSpecific' +} +export const IArgumentsService = Symbol('IArgumentsService'); +export interface IArgumentsService { + getKnownOptions(): { withArgs: string[]; withoutArgs: string[] }; + getOptionValue(args: string[], option: string): string | string[] | undefined; + filterArguments(args: string[], argumentToRemove: string[]): string[]; + // tslint:disable-next-line:unified-signatures + filterArguments(args: string[], filter: TestFilter): string[]; + getTestFolders(args: string[]): string[]; +} +export const IArgumentsHelper = Symbol('IArgumentsHelper'); +export interface IArgumentsHelper { + getOptionValues(args: string[], option: string): string | string[] | undefined; + filterArguments(args: string[], optionsWithArguments?: string[], optionsWithoutArguments?: string[]): string[]; + getPositionalArguments( + args: string[], + optionsWithArguments?: string[], + optionsWithoutArguments?: string[] + ): string[]; +} + +export const ITestManagerRunner = Symbol('ITestManagerRunner'); +export interface ITestManagerRunner { + runTest( + testResultsService: ITestResultsService, + options: TestRunOptions, + testManager: ITestManager + ): Promise; +} + +export const IUnitTestHelper = Symbol('IUnitTestHelper'); +export interface IUnitTestHelper { + getStartDirectory(args: string[]): string; + getIdsOfTestsToRun(tests: Tests, testsToRun: TestsToRun): string[]; +} + +export const ITestDiagnosticService = Symbol('ITestDiagnosticService'); +export interface ITestDiagnosticService { + getMessagePrefix(status: TestStatus): string | undefined; + getSeverity(unitTestSeverity: PythonTestMessageSeverity): DiagnosticSeverity | undefined; +} + +export interface IPythonTestMessage { + code: string | undefined; + message?: string; + severity: PythonTestMessageSeverity; + provider: string | undefined; + traceback?: string; + testTime: number; + status?: TestStatus; + locationStack?: ILocationStackFrameDetails[]; + testFilePath: string; +} +export enum PythonTestMessageSeverity { + Error, + Failure, + Skip, + Pass +} +export enum DiagnosticMessageType { + Error, + Fail, + Skipped, + Pass +} + +export interface ILocationStackFrameDetails { + location: Location; + lineText: string; +} + +export type WorkspaceTestStatus = { workspace: Uri; status: TestStatus }; + +export enum TestDataItemType { + workspaceFolder = 'workspaceFolder', + folder = 'folder', + file = 'file', + suite = 'suite', + function = 'function' +} +export type TestDataItem = TestWorkspaceFolder | TestFolder | TestFile | TestSuite | TestFunction; + +export class TestWorkspaceFolder { + public status?: TestStatus; + public time?: number; + public functionsPassed?: number; + public functionsFailed?: number; + public functionsDidNotRun?: number; + public passed?: boolean; + constructor(public readonly workspaceFolder: WorkspaceFolder) {} + public get resource(): Uri { + return this.workspaceFolder.uri; + } + public get name(): string { + return this.workspaceFolder.name; + } +} + +export const ITestTreeViewProvider = Symbol('ITestTreeViewProvider'); +export interface ITestTreeViewProvider extends TreeDataProvider { + onDidChangeTreeData: Event; + getTreeItem(element: TestDataItem): Promise; + getChildren(element?: TestDataItem): ProviderResult; + refresh(resource: Uri): void; +} + +export const ITestDataItemResource = Symbol('ITestDataItemResource'); + +export interface ITestDataItemResource { + getResource(testData: Readonly): Uri; +} diff --git a/src/client/testing/unittest/helper.ts b/src/client/testing/unittest/helper.ts new file mode 100644 index 000000000000..be77ed07a7eb --- /dev/null +++ b/src/client/testing/unittest/helper.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IServiceContainer } from '../../ioc/types'; +import { Tests, TestsToRun } from '../common/types'; +import { IArgumentsHelper, IUnitTestHelper } from '../types'; + +@injectable() +export class UnitTestHelper implements IUnitTestHelper { + private readonly argsHelper: IArgumentsHelper; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.argsHelper = serviceContainer.get(IArgumentsHelper); + } + public getStartDirectory(args: string[]): string { + const shortValue = this.argsHelper.getOptionValues(args, '-s'); + if (typeof shortValue === 'string') { + return shortValue; + } + const longValue = this.argsHelper.getOptionValues(args, '--start-directory'); + if (typeof longValue === 'string') { + return longValue; + } + return '.'; + } + public getIdsOfTestsToRun(tests: Tests, testsToRun: TestsToRun): string[] { + const testIds: string[] = []; + if (testsToRun && testsToRun.testFolder) { + // Get test ids of files in these folders. + testsToRun.testFolder.forEach((folder) => { + tests.testFiles.forEach((f) => { + if (f.fullPath.startsWith(folder.name)) { + testIds.push(f.nameToRun); + } + }); + }); + } + if (testsToRun && testsToRun.testFile) { + testIds.push(...testsToRun.testFile.map((f) => f.nameToRun)); + } + if (testsToRun && testsToRun.testSuite) { + testIds.push(...testsToRun.testSuite.map((f) => f.nameToRun)); + } + if (testsToRun && testsToRun.testFunction) { + testIds.push(...testsToRun.testFunction.map((f) => f.nameToRun)); + } + return testIds; + } +} diff --git a/src/client/testing/unittest/main.ts b/src/client/testing/unittest/main.ts new file mode 100644 index 000000000000..cb81d3de5f23 --- /dev/null +++ b/src/client/testing/unittest/main.ts @@ -0,0 +1,86 @@ +import { Uri } from 'vscode'; +import { Product } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { IServiceContainer } from '../../ioc/types'; +import { CommandSource, UNITTEST_PROVIDER } from '../common/constants'; +import { BaseTestManager } from '../common/managers/baseTestManager'; +import { ITestsHelper, TestDiscoveryOptions, TestRunOptions, Tests, TestStatus, TestsToRun } from '../common/types'; +import { IArgumentsService, ITestManagerRunner, TestFilter } from '../types'; + +export class TestManager extends BaseTestManager { + private readonly argsService: IArgumentsService; + private readonly helper: ITestsHelper; + private readonly runner: ITestManagerRunner; + public get enabled() { + return this.settings.testing.unittestEnabled; + } + constructor(workspaceFolder: Uri, rootDirectory: string, serviceContainer: IServiceContainer) { + super(UNITTEST_PROVIDER, Product.unittest, workspaceFolder, rootDirectory, serviceContainer); + this.argsService = this.serviceContainer.get(IArgumentsService, this.testProvider); + this.helper = this.serviceContainer.get(ITestsHelper); + this.runner = this.serviceContainer.get(ITestManagerRunner, this.testProvider); + } + public configure() { + noop(); + } + public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { + const args = this.settings.testing.unittestArgs.slice(0); + return { + workspaceFolder: this.workspaceFolder, + cwd: this.rootDirectory, + args, + token: this.testDiscoveryCancellationToken!, + ignoreCache, + outChannel: this.outputChannel + }; + } + public async runTest( + cmdSource: CommandSource, + testsToRun?: TestsToRun, + runFailedTests?: boolean, + debug?: boolean + ): Promise { + if (runFailedTests === true && this.tests) { + testsToRun = { testFile: [], testFolder: [], testSuite: [], testFunction: [] }; + testsToRun.testFunction = this.tests.testFunctions + .filter((fn) => { + return fn.testFunction.status === TestStatus.Error || fn.testFunction.status === TestStatus.Fail; + }) + .map((fn) => fn.testFunction); + } + return super.runTest(cmdSource, testsToRun, runFailedTests, debug); + } + public async runTestImpl( + tests: Tests, + testsToRun?: TestsToRun, + _runFailedTests?: boolean, + debug?: boolean + ): Promise { + let args: string[]; + + const runAllTests = this.helper.shouldRunAllTests(testsToRun); + if (debug) { + args = this.argsService.filterArguments( + this.settings.testing.unittestArgs, + runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific + ); + } else { + args = this.argsService.filterArguments( + this.settings.testing.unittestArgs, + runAllTests ? TestFilter.runAll : TestFilter.runSpecific + ); + } + + const options: TestRunOptions = { + workspaceFolder: this.workspaceFolder, + cwd: this.rootDirectory, + tests, + args, + testsToRun, + debug, + token: this.testRunnerCancellationToken!, + outChannel: this.outputChannel + }; + return this.runner.runTest(this.testResultsService, options, this); + } +} diff --git a/src/client/testing/unittest/runner.ts b/src/client/testing/unittest/runner.ts new file mode 100644 index 000000000000..77f23ee3de54 --- /dev/null +++ b/src/client/testing/unittest/runner.ts @@ -0,0 +1,228 @@ +'use strict'; + +import { inject, injectable } from 'inversify'; +import { traceError } from '../../common/logger'; +import * as internalScripts from '../../common/process/internal/scripts'; +import { IDisposableRegistry } from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { noop } from '../../common/utils/misc'; +import { IServiceContainer } from '../../ioc/types'; +import { UNITTEST_PROVIDER } from '../common/constants'; +import { Options } from '../common/runner'; +import { + ITestDebugLauncher, + ITestManager, + ITestResultsService, + ITestRunner, + IUnitTestSocketServer, + LaunchOptions, + TestRunOptions, + Tests, + TestStatus +} from '../common/types'; +import { IArgumentsHelper, ITestManagerRunner, IUnitTestHelper } from '../types'; + +type TestStatusMap = { + status: TestStatus; + summaryProperty: 'passed' | 'failures' | 'errors' | 'skipped'; +}; + +const outcomeMapping = new Map(); +outcomeMapping.set('passed', { status: TestStatus.Pass, summaryProperty: 'passed' }); +outcomeMapping.set('failed', { status: TestStatus.Fail, summaryProperty: 'failures' }); +outcomeMapping.set('error', { status: TestStatus.Error, summaryProperty: 'errors' }); +outcomeMapping.set('skipped', { status: TestStatus.Skipped, summaryProperty: 'skipped' }); + +interface ITestData { + test: string; + message: string; + outcome: string; + traceback: string; +} + +@injectable() +export class TestManagerRunner implements ITestManagerRunner { + private readonly argsHelper: IArgumentsHelper; + private readonly helper: IUnitTestHelper; + private readonly testRunner: ITestRunner; + private readonly server: IUnitTestSocketServer; + private busy!: Deferred; + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.argsHelper = serviceContainer.get(IArgumentsHelper); + this.testRunner = serviceContainer.get(ITestRunner); + this.server = this.serviceContainer.get(IUnitTestSocketServer); + this.helper = this.serviceContainer.get(IUnitTestHelper); + this.serviceContainer.get(IDisposableRegistry).push(this.server); + } + + // tslint:disable-next-line:max-func-body-length + public async runTest( + testResultsService: ITestResultsService, + options: TestRunOptions, + testManager: ITestManager + ): Promise { + if (this.busy && !this.busy.completed) { + return this.busy.promise; + } + this.busy = createDeferred(); + + options.tests.summary.errors = 0; + options.tests.summary.failures = 0; + options.tests.summary.passed = 0; + options.tests.summary.skipped = 0; + let failFast = false; + this.server.on('error', (message: string, ...data: string[]) => traceError(`${message} ${data.join(' ')}`)); + this.server.on('log', noop); + this.server.on('connect', noop); + this.server.on('start', noop); + this.server.on('result', (data: ITestData) => { + const test = options.tests.testFunctions.find((t) => t.testFunction.nameToRun === data.test); + const statusDetails = outcomeMapping.get(data.outcome)!; + if (test) { + test.testFunction.status = statusDetails.status; + switch (test.testFunction.status) { + case TestStatus.Error: + case TestStatus.Fail: { + test.testFunction.passed = false; + break; + } + case TestStatus.Pass: { + test.testFunction.passed = true; + break; + } + default: { + test.testFunction.passed = undefined; + } + } + test.testFunction.message = data.message; + test.testFunction.traceback = data.traceback; + options.tests.summary[statusDetails.summaryProperty] += 1; + + if ( + failFast && + (statusDetails.summaryProperty === 'failures' || statusDetails.summaryProperty === 'errors') + ) { + testManager.stop(); + } + } else { + if (statusDetails) { + options.tests.summary[statusDetails.summaryProperty] += 1; + } + } + }); + + const port = await this.server.start(); + const testPaths: string[] = this.helper.getIdsOfTestsToRun(options.tests, options.testsToRun!); + for (let counter = 0; counter < testPaths.length; counter += 1) { + testPaths[counter] = `-t${testPaths[counter].trim()}`; + } + + const runTestInternal = async (testFile: string = '', testId: string = '') => { + let testArgs = this.buildTestArgs(options.args); + failFast = testArgs.indexOf('--uf') >= 0; + testArgs = testArgs.filter((arg) => arg !== '--uf'); + + testArgs.push(`--result-port=${port}`); + if (testId.length > 0) { + testArgs.push(`-t${testId}`); + } + if (testFile.length > 0) { + testArgs.push(`--testFile=${testFile}`); + } + if (options.debug === true) { + const debugLauncher = this.serviceContainer.get(ITestDebugLauncher); + testArgs.push('--debug'); + const launchOptions: LaunchOptions = { + cwd: options.cwd, + args: testArgs, + token: options.token, + outChannel: options.outChannel, + testProvider: UNITTEST_PROVIDER + }; + return debugLauncher.launchDebugger(launchOptions); + } else { + const args = internalScripts.visualstudio_py_testlauncher(testArgs); + + const runOptions: Options = { + args: args, + cwd: options.cwd, + outChannel: options.outChannel, + token: options.token, + workspaceFolder: options.workspaceFolder + }; + await this.testRunner.run(UNITTEST_PROVIDER, runOptions); + } + }; + + // Test everything. + if (testPaths.length === 0) { + await this.removeListenersAfter(runTestInternal()); + } else { + // Ok, the test runner can only work with one test at a time. + if (options.testsToRun) { + if (Array.isArray(options.testsToRun.testFile)) { + for (const testFile of options.testsToRun.testFile) { + await runTestInternal(testFile.fullPath, testFile.nameToRun); + } + } + if (Array.isArray(options.testsToRun.testSuite)) { + for (const testSuite of options.testsToRun.testSuite) { + const item = options.tests.testSuites.find((t) => t.testSuite === testSuite); + if (item) { + const testFileName = item.parentTestFile.fullPath; + await runTestInternal(testFileName, testSuite.nameToRun); + } + } + } + if (Array.isArray(options.testsToRun.testFunction)) { + for (const testFn of options.testsToRun.testFunction) { + const item = options.tests.testFunctions.find((t) => t.testFunction === testFn); + if (item) { + const testFileName = item.parentTestFile.fullPath; + await runTestInternal(testFileName, testFn.nameToRun); + } + } + } + + await this.removeListenersAfter(Promise.resolve()); + } + } + + testResultsService.updateResults(options.tests); + this.busy.resolve(options.tests); + return options.tests; + } + + // remove all the listeners from the server after all tests are complete, + // and just pass the promise `after` through as we do not want to get in + // the way here. + // tslint:disable-next-line:no-any + private async removeListenersAfter(after: Promise): Promise { + return after + .then(() => this.server.removeAllListeners()) + .catch((err) => { + this.server.removeAllListeners(); + throw err; // keep propagating this downward + }); + } + + private buildTestArgs(args: string[]): string[] { + const startTestDiscoveryDirectory = this.helper.getStartDirectory(args); + let pattern = 'test*.py'; + const shortValue = this.argsHelper.getOptionValues(args, '-p'); + const longValueValue = this.argsHelper.getOptionValues(args, '--pattern'); + if (typeof shortValue === 'string') { + pattern = shortValue; + } else if (typeof longValueValue === 'string') { + pattern = longValueValue; + } + const failFast = args.some((arg) => arg.trim() === '-f' || arg.trim() === '--failfast'); + const verbosity = args.some((arg) => arg.trim().indexOf('-v') === 0) ? 2 : 1; + const testArgs = [`--us=${startTestDiscoveryDirectory}`, `--up=${pattern}`, `--uvInt=${verbosity}`]; + if (failFast) { + testArgs.push('--uf'); + } + return testArgs; + } +} diff --git a/src/client/testing/unittest/services/argsService.ts b/src/client/testing/unittest/services/argsService.ts new file mode 100644 index 000000000000..73e27087a01e --- /dev/null +++ b/src/client/testing/unittest/services/argsService.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IServiceContainer } from '../../../ioc/types'; +import { IArgumentsHelper, IArgumentsService, TestFilter } from '../../types'; + +const OptionsWithArguments = ['-k', '-p', '-s', '-t', '--pattern', '--start-directory', '--top-level-directory']; + +const OptionsWithoutArguments = [ + '-b', + '-c', + '-f', + '-h', + '-q', + '-v', + '--buffer', + '--catch', + '--failfast', + '--help', + '--locals', + '--quiet', + '--verbose' +]; + +@injectable() +export class ArgumentsService implements IArgumentsService { + private readonly helper: IArgumentsHelper; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.helper = serviceContainer.get(IArgumentsHelper); + } + public getKnownOptions(): { withArgs: string[]; withoutArgs: string[] } { + return { + withArgs: OptionsWithArguments, + withoutArgs: OptionsWithoutArguments + }; + } + public getOptionValue(args: string[], option: string): string | string[] | undefined { + return this.helper.getOptionValues(args, option); + } + public filterArguments(args: string[], argumentToRemoveOrFilter: string[] | TestFilter): string[] { + const optionsWithoutArgsToRemove: string[] = []; + const optionsWithArgsToRemove: string[] = []; + // Positional arguments in pytest positional args are test directories and files. + // So if we want to run a specific test, then remove positional args. + let removePositionalArgs = false; + if (Array.isArray(argumentToRemoveOrFilter)) { + argumentToRemoveOrFilter.forEach((item) => { + if (OptionsWithArguments.indexOf(item) >= 0) { + optionsWithArgsToRemove.push(item); + } + if (OptionsWithoutArguments.indexOf(item) >= 0) { + optionsWithoutArgsToRemove.push(item); + } + }); + } else { + removePositionalArgs = true; + } + + let filteredArgs = args.slice(); + if (removePositionalArgs) { + const positionalArgs = this.helper.getPositionalArguments( + filteredArgs, + OptionsWithArguments, + OptionsWithoutArguments + ); + filteredArgs = filteredArgs.filter((item) => positionalArgs.indexOf(item) === -1); + } + return this.helper.filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); + } + public getTestFolders(args: string[]): string[] { + const shortValue = this.helper.getOptionValues(args, '-s'); + if (typeof shortValue === 'string') { + return [shortValue]; + } + const longValue = this.helper.getOptionValues(args, '--start-directory'); + if (typeof longValue === 'string') { + return [longValue]; + } + return ['.']; + } +} diff --git a/src/client/testing/unittest/services/discoveryService.ts b/src/client/testing/unittest/services/discoveryService.ts new file mode 100644 index 000000000000..f09901e37f5f --- /dev/null +++ b/src/client/testing/unittest/services/discoveryService.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, named } from 'inversify'; +import * as internalPython from '../../../common/process/internal/python'; +import { IServiceContainer } from '../../../ioc/types'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import { Options } from '../../common/runner'; +import { ITestDiscoveryService, ITestRunner, ITestsParser, TestDiscoveryOptions, Tests } from '../../common/types'; +import { IArgumentsHelper } from '../../types'; + +type UnitTestDiscoveryOptions = TestDiscoveryOptions & { + startDirectory: string; + pattern: string; +}; + +@injectable() +export class TestDiscoveryService implements ITestDiscoveryService { + private readonly argsHelper: IArgumentsHelper; + private readonly runner: ITestRunner; + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(ITestsParser) @named(UNITTEST_PROVIDER) private testParser: ITestsParser + ) { + this.argsHelper = serviceContainer.get(IArgumentsHelper); + this.runner = serviceContainer.get(ITestRunner); + } + public async discoverTests(options: TestDiscoveryOptions): Promise { + const pythonScript = this.getDiscoveryScript(options); + const unitTestOptions = this.translateOptions(options); + const runOptions: Options = { + // unittest needs to load modules in the workspace + // isolating it breaks unittest discovery + args: internalPython.execCode(pythonScript, false), + cwd: options.cwd, + workspaceFolder: options.workspaceFolder, + token: options.token, + outChannel: options.outChannel + }; + + const data = await this.runner.run(UNITTEST_PROVIDER, runOptions); + + if (options.token && options.token.isCancellationRequested) { + return Promise.reject('cancelled'); + } + + return this.testParser.parse(data, unitTestOptions); + } + public getDiscoveryScript(options: TestDiscoveryOptions): string { + const unitTestOptions = this.translateOptions(options); + return ` +import unittest +loader = unittest.TestLoader() +suites = loader.discover("${unitTestOptions.startDirectory}", pattern="${unitTestOptions.pattern}") +print("start") #Don't remove this line +for suite in suites._tests: + for cls in suite._tests: + try: + for m in cls._tests: + print(m.id()) + except: + pass`; + } + public translateOptions(options: TestDiscoveryOptions): UnitTestDiscoveryOptions { + return { + ...options, + startDirectory: this.getStartDirectory(options), + pattern: this.getTestPattern(options) + }; + } + private getStartDirectory(options: TestDiscoveryOptions) { + const shortValue = this.argsHelper.getOptionValues(options.args, '-s'); + if (typeof shortValue === 'string') { + return shortValue; + } + const longValue = this.argsHelper.getOptionValues(options.args, '--start-directory'); + if (typeof longValue === 'string') { + return longValue; + } + return '.'; + } + private getTestPattern(options: TestDiscoveryOptions) { + const shortValue = this.argsHelper.getOptionValues(options.args, '-p'); + if (typeof shortValue === 'string') { + return shortValue; + } + const longValue = this.argsHelper.getOptionValues(options.args, '--pattern'); + if (typeof longValue === 'string') { + return longValue; + } + return 'test*.py'; + } +} diff --git a/src/client/testing/unittest/services/parserService.ts b/src/client/testing/unittest/services/parserService.ts new file mode 100644 index 000000000000..41ad7c9fa28c --- /dev/null +++ b/src/client/testing/unittest/services/parserService.ts @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { + ITestsHelper, + ITestsParser, + TestFile, + TestFunction, + Tests, + TestStatus, + UnitTestParserOptions +} from '../../common/types'; + +@injectable() +export class TestsParser implements ITestsParser { + constructor(@inject(ITestsHelper) private testsHelper: ITestsHelper) {} + public parse(content: string, options: UnitTestParserOptions): Tests { + const testIds = this.getTestIds(content); + let testsDirectory = options.cwd; + if (options.startDirectory.length > 1) { + testsDirectory = path.isAbsolute(options.startDirectory) + ? options.startDirectory + : path.resolve(options.cwd, options.startDirectory); + } + return this.parseTestIds(options.cwd, testsDirectory, testIds); + } + private getTestIds(content: string): string[] { + let startedCollecting = false; + return content + .split(/\r?\n/g) + .map((line) => { + if (!startedCollecting) { + if (line === 'start') { + startedCollecting = true; + } + return ''; + } + return line.trim(); + }) + .filter((line) => line.length > 0); + } + private parseTestIds(workspaceDirectory: string, testsDirectory: string, testIds: string[]): Tests { + const testFiles: TestFile[] = []; + testIds.forEach((testId) => this.addTestId(testsDirectory, testId, testFiles)); + + return this.testsHelper.flattenTestFiles(testFiles, workspaceDirectory); + } + + /** + * Add the test Ids into the array provided. + * TestIds are fully qualified including the method names. + * E.g. tone_test.Failing2Tests.test_failure + * Where tone_test = folder, Failing2Tests = class/suite, test_failure = method. + * @private + * @param {string} rootDirectory + * @param {string} testId + * @param {TestFile[]} testFiles + * @returns {Tests} + * @memberof TestsParser + */ + private addTestId(rootDirectory: string, testId: string, testFiles: TestFile[]) { + const testIdParts = testId.split('.'); + // We must have a file, class and function name + if (testIdParts.length <= 2) { + return null; + } + + const paths = testIdParts.slice(0, testIdParts.length - 2); + const filePath = `${path.join(rootDirectory, ...paths)}.py`; + const functionName = testIdParts.pop()!; + const suiteToRun = testIdParts.join('.'); + const className = testIdParts.pop()!; + const moduleName = testIdParts.join('.'); + const resource = Uri.file(rootDirectory); + + // Check if we already have this test file + let testFile = testFiles.find((test) => test.fullPath === filePath); + if (!testFile) { + testFile = { + resource, + name: path.basename(filePath), + fullPath: filePath, + functions: [], + suites: [], + nameToRun: moduleName, + xmlName: '', + status: TestStatus.Idle, + time: 0 + }; + testFiles.push(testFile); + } + + // Check if we already have this suite + // nameToRun = testId - method name + let testSuite = testFile.suites.find((cls) => cls.nameToRun === suiteToRun); + if (!testSuite) { + testSuite = { + resource, + name: className, + functions: [], + suites: [], + isUnitTest: true, + isInstance: false, + nameToRun: suiteToRun, + xmlName: '', + status: TestStatus.Idle, + time: 0 + }; + testFile.suites.push(testSuite!); + } + + const testFunction: TestFunction = { + resource, + name: functionName, + nameToRun: testId, + status: TestStatus.Idle, + time: 0 + }; + + testSuite!.functions.push(testFunction); + } +} diff --git a/src/client/testing/unittest/socketServer.ts b/src/client/testing/unittest/socketServer.ts new file mode 100644 index 000000000000..9dc23b87e33a --- /dev/null +++ b/src/client/testing/unittest/socketServer.ts @@ -0,0 +1,125 @@ +'use strict'; +import { EventEmitter } from 'events'; +import { injectable } from 'inversify'; +import * as net from 'net'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { IUnitTestSocketServer } from '../common/types'; + +// tslint:disable:variable-name no-any +const MaxConnections = 100; + +@injectable() +export class UnitTestSocketServer extends EventEmitter implements IUnitTestSocketServer { + private server?: net.Server; + private startedDef?: Deferred; + private sockets: net.Socket[] = []; + private ipcBuffer: string = ''; + constructor() { + super(); + } + public get clientsConnected(): boolean { + return this.sockets.length > 0; + } + public dispose() { + this.stop(); + } + public stop() { + if (this.server) { + this.server!.close(); + this.server = undefined; + } + } + public start(options: { port?: number; host?: string } = { port: 0, host: 'localhost' }): Promise { + this.ipcBuffer = ''; + this.startedDef = createDeferred(); + this.server = net.createServer(this.connectionListener.bind(this)); + this.server!.maxConnections = MaxConnections; + this.server!.on('error', (err) => { + if (this.startedDef) { + this.startedDef.reject(err); + this.startedDef = undefined; + } + this.emit('error', err); + }); + this.log('starting server as', 'TCP'); + options.port = typeof options.port === 'number' ? options.port! : 0; + options.host = + typeof options.host === 'string' && options.host!.trim().length > 0 ? options.host!.trim() : 'localhost'; + this.server!.listen(options, (socket: net.Socket) => { + this.startedDef!.resolve((this.server!.address() as net.AddressInfo).port); + this.startedDef = undefined; + this.emit('start', socket); + }); + return this.startedDef!.promise; + } + + private connectionListener(socket: net.Socket) { + this.sockets.push(socket); + socket.setEncoding('utf8'); + this.log('## socket connection to server detected ##'); + socket.on('close', () => { + this.ipcBuffer = ''; + this.onCloseSocket(); + }); + socket.on('error', (err) => { + this.log('server socket error', err); + this.emit('error', err); + }); + socket.on('data', (data) => { + const sock = socket; + // Assume we have just one client socket connection + let dataStr = (this.ipcBuffer += data); + + // tslint:disable-next-line:no-constant-condition + while (true) { + const startIndex = dataStr.indexOf('{'); + if (startIndex === -1) { + return; + } + const lengthOfMessage = parseInt( + dataStr.slice(dataStr.indexOf(':') + 1, dataStr.indexOf('{')).trim(), + 10 + ); + if (dataStr.length < startIndex + lengthOfMessage) { + return; + } + // tslint:disable-next-line:no-any + let message: any; + try { + message = JSON.parse(dataStr.substring(startIndex, lengthOfMessage + startIndex)); + } catch (jsonErr) { + this.emit('error', jsonErr); + return; + } + dataStr = this.ipcBuffer = dataStr.substring(startIndex + lengthOfMessage); + this.emit(message.event, message.body, sock); + } + }); + this.emit('connect', socket); + } + private log(message: string, ...data: any[]) { + this.emit('log', message, ...data); + } + private onCloseSocket() { + // tslint:disable-next-line:one-variable-per-declaration + for (let i = 0, count = this.sockets.length; i < count; i += 1) { + const socket = this.sockets[i]; + let destroyedSocketId = false; + if (socket && socket.readable) { + continue; + } + // tslint:disable-next-line:no-any prefer-type-cast + if ((socket as any).id) { + // tslint:disable-next-line:no-any prefer-type-cast + destroyedSocketId = (socket as any).id; + } + this.log('socket disconnected', destroyedSocketId.toString()); + if (socket && socket.destroy) { + socket.destroy(); + } + this.sockets.splice(i, 1); + this.emit('socket.disconnected', socket, destroyedSocketId); + return; + } + } +} diff --git a/src/client/testing/unittest/testConfigurationManager.ts b/src/client/testing/unittest/testConfigurationManager.ts new file mode 100644 index 000000000000..df35f70d2365 --- /dev/null +++ b/src/client/testing/unittest/testConfigurationManager.ts @@ -0,0 +1,34 @@ +import { Uri } from 'vscode'; +import { Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { TestConfigurationManager } from '../common/managers/testConfigurationManager'; +import { ITestConfigSettingsService } from '../types'; + +export class ConfigurationManager extends TestConfigurationManager { + constructor(workspace: Uri, serviceContainer: IServiceContainer, cfg?: ITestConfigSettingsService) { + super(workspace, Product.unittest, serviceContainer, cfg); + } + public async requiresUserToConfigure(_wkspace: Uri): Promise { + return true; + } + public async configure(wkspace: Uri) { + const args = ['-v']; + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); + args.push('-s'); + if (typeof testDir === 'string' && testDir !== '.') { + args.push(`./${testDir}`); + } else { + args.push('.'); + } + + const testfilePattern = await this.selectTestFilePattern(); + args.push('-p'); + if (typeof testfilePattern === 'string') { + args.push(testfilePattern); + } else { + args.push('test*.py'); + } + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.unittest, args); + } +} diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json deleted file mode 100644 index 2185fbf67f6f..000000000000 --- a/src/client/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "module": "commonjs", - "moduleResolution": "node", - "outDir": "../../out/client", - "noLib": true, - "sourceMap": true - }, - "exclude": [ - "node_modules", - "server" - ] -} \ No newline at end of file diff --git a/src/client/typeFormatters/blockFormatProvider.ts b/src/client/typeFormatters/blockFormatProvider.ts new file mode 100644 index 000000000000..822caa5632e9 --- /dev/null +++ b/src/client/typeFormatters/blockFormatProvider.ts @@ -0,0 +1,80 @@ +import { + CancellationToken, + FormattingOptions, + OnTypeFormattingEditProvider, + Position, + TextDocument, + TextEdit +} from 'vscode'; +import { CodeBlockFormatProvider } from './codeBlockFormatProvider'; +import { + ASYNC_DEF_REGEX, + ASYNC_FOR_IN_REGEX, + CLASS_REGEX, + DEF_REGEX, + ELIF_REGEX, + ELSE_REGEX, + EXCEPT_REGEX, + FINALLY_REGEX, + FOR_IN_REGEX, + IF_REGEX, + TRY_REGEX, + WHILE_REGEX +} from './contracts'; + +export class BlockFormatProviders implements OnTypeFormattingEditProvider { + private providers: CodeBlockFormatProvider[]; + constructor() { + this.providers = []; + const boundaryBlocks = [DEF_REGEX, ASYNC_DEF_REGEX, CLASS_REGEX]; + + const elseParentBlocks = [ + IF_REGEX, + ELIF_REGEX, + FOR_IN_REGEX, + ASYNC_FOR_IN_REGEX, + WHILE_REGEX, + TRY_REGEX, + EXCEPT_REGEX + ]; + this.providers.push(new CodeBlockFormatProvider(ELSE_REGEX, elseParentBlocks, boundaryBlocks)); + + const elifParentBlocks = [IF_REGEX, ELIF_REGEX]; + this.providers.push(new CodeBlockFormatProvider(ELIF_REGEX, elifParentBlocks, boundaryBlocks)); + + const exceptParentBlocks = [TRY_REGEX, EXCEPT_REGEX]; + this.providers.push(new CodeBlockFormatProvider(EXCEPT_REGEX, exceptParentBlocks, boundaryBlocks)); + + const finallyParentBlocks = [TRY_REGEX, EXCEPT_REGEX]; + this.providers.push(new CodeBlockFormatProvider(FINALLY_REGEX, finallyParentBlocks, boundaryBlocks)); + } + + public provideOnTypeFormattingEdits( + document: TextDocument, + position: Position, + ch: string, + options: FormattingOptions, + _token: CancellationToken + ): TextEdit[] { + if (position.line === 0) { + return []; + } + + const currentLine = document.lineAt(position.line); + const prevousLine = document.lineAt(position.line - 1); + + // We're only interested in cases where the current block is at the same indentation level as the previous line + // E.g. if we have an if..else block, generally the else statement would be at the same level as the code in the if... + if (currentLine.firstNonWhitespaceCharacterIndex !== prevousLine.firstNonWhitespaceCharacterIndex) { + return []; + } + + const currentLineText = currentLine.text; + const provider = this.providers.find((p) => p.canProvideEdits(currentLineText)); + if (provider) { + return provider.provideEdits(document, position, ch, options, currentLine); + } + + return []; + } +} diff --git a/src/client/typeFormatters/codeBlockFormatProvider.ts b/src/client/typeFormatters/codeBlockFormatProvider.ts new file mode 100644 index 000000000000..25d677003bb9 --- /dev/null +++ b/src/client/typeFormatters/codeBlockFormatProvider.ts @@ -0,0 +1,74 @@ +import { FormattingOptions, Position, Range, TextDocument, TextEdit, TextLine } from 'vscode'; +import { BlockRegEx } from './contracts'; + +export class CodeBlockFormatProvider { + constructor( + private blockRegExp: BlockRegEx, + private previousBlockRegExps: BlockRegEx[], + private boundaryRegExps: BlockRegEx[] + ) {} + public canProvideEdits(line: string): boolean { + return this.blockRegExp.test(line); + } + + public provideEdits( + document: TextDocument, + position: Position, + _ch: string, + options: FormattingOptions, + line: TextLine + ): TextEdit[] { + // We can have else for the following blocks: + // if: + // elif x: + // for x in y: + // while x: + + // We need to find a block statement that is less than or equal to this statement block (but not greater) + for (let lineNumber = position.line - 1; lineNumber >= 0; lineNumber -= 1) { + const prevLine = document.lineAt(lineNumber); + const prevLineText = prevLine.text; + + // Oops, we've reached a boundary (like the function or class definition) + // Get out of here + if (this.boundaryRegExps.some((value) => value.test(prevLineText))) { + return []; + } + + const blockRegEx = this.previousBlockRegExps.find((value) => value.test(prevLineText)); + if (!blockRegEx) { + continue; + } + + const startOfBlockInLine = prevLine.firstNonWhitespaceCharacterIndex; + if (startOfBlockInLine > line.firstNonWhitespaceCharacterIndex) { + continue; + } + + const startPosition = new Position(position.line, 0); + const endPosition = new Position(position.line, line.firstNonWhitespaceCharacterIndex - startOfBlockInLine); + + if (startPosition.isEqual(endPosition)) { + // current block cannot be at the same level as a preivous block + continue; + } + + if (options.insertSpaces) { + return [TextEdit.delete(new Range(startPosition, endPosition))]; + } else { + // Delete everything before the block and insert the same characters we have in the previous block + const prefixOfPreviousBlock = prevLineText.substring(0, startOfBlockInLine); + + const startDeletePosition = new Position(position.line, 0); + const endDeletePosition = new Position(position.line, line.firstNonWhitespaceCharacterIndex); + + return [ + TextEdit.delete(new Range(startDeletePosition, endDeletePosition)), + TextEdit.insert(startDeletePosition, prefixOfPreviousBlock) + ]; + } + } + + return []; + } +} diff --git a/src/client/typeFormatters/contracts.ts b/src/client/typeFormatters/contracts.ts new file mode 100644 index 000000000000..62bf68fd46a3 --- /dev/null +++ b/src/client/typeFormatters/contracts.ts @@ -0,0 +1,21 @@ +export class BlockRegEx { + constructor(private regEx: RegExp, public startWord: String) {} + public test(value: string): boolean { + // Clear the cache + this.regEx.lastIndex = -1; + return this.regEx.test(value); + } +} + +export const IF_REGEX = new BlockRegEx(/^( |\t)*if +.*: *$/g, 'if'); +export const ELIF_REGEX = new BlockRegEx(/^( |\t)*elif +.*: *$/g, 'elif'); +export const ELSE_REGEX = new BlockRegEx(/^( |\t)*else *: *$/g, 'else'); +export const FOR_IN_REGEX = new BlockRegEx(/^( |\t)*for \w in .*: *$/g, 'for'); +export const ASYNC_FOR_IN_REGEX = new BlockRegEx(/^( |\t)*async *for \w in .*: *$/g, 'for'); +export const WHILE_REGEX = new BlockRegEx(/^( |\t)*while .*: *$/g, 'while'); +export const TRY_REGEX = new BlockRegEx(/^( |\t)*try *: *$/g, 'try'); +export const FINALLY_REGEX = new BlockRegEx(/^( |\t)*finally *: *$/g, 'finally'); +export const EXCEPT_REGEX = new BlockRegEx(/^( |\t)*except *\w* *(as)? *\w* *: *$/g, 'except'); +export const DEF_REGEX = new BlockRegEx(/^( |\t)*def \w *\(.*$/g, 'def'); +export const ASYNC_DEF_REGEX = new BlockRegEx(/^( |\t)*async *def \w *\(.*$/g, 'async'); +export const CLASS_REGEX = new BlockRegEx(/^( |\t)*class *\w* *.*: *$/g, 'class'); diff --git a/src/client/typeFormatters/dispatcher.ts b/src/client/typeFormatters/dispatcher.ts new file mode 100644 index 000000000000..452eb2f38d79 --- /dev/null +++ b/src/client/typeFormatters/dispatcher.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { + CancellationToken, + FormattingOptions, + OnTypeFormattingEditProvider, + Position, + ProviderResult, + TextDocument, + TextEdit +} from 'vscode'; + +export class OnTypeFormattingDispatcher implements OnTypeFormattingEditProvider { + private readonly providers: Record; + + constructor(providers: Record) { + this.providers = providers; + } + + public provideOnTypeFormattingEdits( + document: TextDocument, + position: Position, + ch: string, + options: FormattingOptions, + cancellationToken: CancellationToken + ): ProviderResult { + const provider = this.providers[ch]; + + if (provider) { + return provider.provideOnTypeFormattingEdits(document, position, ch, options, cancellationToken); + } + + return []; + } + + public getTriggerCharacters(): { first: string; more: string[] } | undefined { + const keys = Object.keys(this.providers); + keys.sort(); // Make output deterministic + + const first = keys.shift(); + + if (first) { + return { + first: first, + more: keys + }; + } + + return undefined; + } +} diff --git a/src/client/typeFormatters/onEnterFormatter.ts b/src/client/typeFormatters/onEnterFormatter.ts new file mode 100644 index 000000000000..f4464b26cf6b --- /dev/null +++ b/src/client/typeFormatters/onEnterFormatter.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + FormattingOptions, + OnTypeFormattingEditProvider, + Position, + TextDocument, + TextEdit +} from 'vscode'; +import { LineFormatter } from '../formatters/lineFormatter'; +import { TokenizerMode, TokenType } from '../language/types'; +import { getDocumentTokens } from '../providers/providerUtilities'; + +export class OnEnterFormatter implements OnTypeFormattingEditProvider { + private readonly formatter = new LineFormatter(); + + public provideOnTypeFormattingEdits( + document: TextDocument, + position: Position, + _ch: string, + _options: FormattingOptions, + _cancellationToken: CancellationToken + ): TextEdit[] { + if (position.line === 0) { + return []; + } + + // Check case when the entire line belongs to a comment or string + const prevLine = document.lineAt(position.line - 1); + const tokens = getDocumentTokens(document, position, TokenizerMode.CommentsAndStrings); + const lineStartTokenIndex = tokens.getItemContaining(document.offsetAt(prevLine.range.start)); + const lineEndTokenIndex = tokens.getItemContaining(document.offsetAt(prevLine.range.end)); + if (lineStartTokenIndex >= 0 && lineStartTokenIndex === lineEndTokenIndex) { + const token = tokens.getItemAt(lineStartTokenIndex); + if (token.type === TokenType.Semicolon || token.type === TokenType.String) { + return []; + } + } + const formatted = this.formatter.formatLine(document, prevLine.lineNumber); + if (formatted === prevLine.text) { + return []; + } + return [new TextEdit(prevLine.range, formatted)]; + } +} diff --git a/src/client/typings/node.d.ts b/src/client/typings/node.d.ts deleted file mode 100644 index 4260ab2f6e36..000000000000 --- a/src/client/typings/node.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/src/client/typings/vscode-typings.d.ts b/src/client/typings/vscode-typings.d.ts deleted file mode 100644 index 63c80890cbb0..000000000000 --- a/src/client/typings/vscode-typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/src/client/unittest/baseTestRunner.ts b/src/client/unittest/baseTestRunner.ts deleted file mode 100644 index 72efcc276fed..000000000000 --- a/src/client/unittest/baseTestRunner.ts +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; -import * as child_process from 'child_process'; -import * as path from 'path'; -import { exec } from 'child_process'; -import {sendCommand} from './../common/childProc'; -import * as settings from './../common/configSettings'; -import {OutputChannel, window, workspace} from 'vscode'; - -export abstract class BaseTestRunner { - public Id: string; - protected pythonSettings: settings.IPythonSettings; - protected outputChannel: OutputChannel; - private includeErrorAsResponse: boolean; - constructor(id: string, pythonSettings: settings.IPythonSettings, outputChannel: OutputChannel, includeErrorAsResponse: boolean = false) { - this.Id = id; - this.pythonSettings = pythonSettings; - this.outputChannel = outputChannel; - this.includeErrorAsResponse = includeErrorAsResponse; - } - - public runTests(filePath: string): Promise { - return Promise.resolve(); - } - - protected run(commandLine: string): Promise { - var outputChannel = this.outputChannel; - var linterId = this.Id; - - return new Promise((resolve, reject) => { - sendCommand(commandLine, workspace.rootPath, this.includeErrorAsResponse).then(data => { - outputChannel.append(data); - outputChannel.show(); - }, error=> { - outputChannel.append(error); - outputChannel.show(); - }); - }); - } -} diff --git a/src/client/unittest/nosetests.ts b/src/client/unittest/nosetests.ts deleted file mode 100644 index aab1153fa9e4..000000000000 --- a/src/client/unittest/nosetests.ts +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -import * as path from 'path'; -import * as baseTestRunner from './baseTestRunner'; -import * as settings from './../common/configSettings'; -import {OutputChannel} from 'vscode'; - -export class NoseTests extends baseTestRunner.BaseTestRunner { - constructor(pythonSettings: settings.IPythonSettings, outputChannel: OutputChannel) { - super("nosetests", pythonSettings, outputChannel, true); - } - - public runTests(filePath: string = ""): Promise { - if (!this.pythonSettings.unitTest.nosetestsEnabled) { - return Promise.resolve(); - } - - var nosetestsPath = this.pythonSettings.unitTest.nosetestPath; - var cmdLine = `${nosetestsPath} ${filePath}`; - return new Promise(resolve => { - this.run(cmdLine).then(messages=> { - resolve(messages); - }); - }); - } -} diff --git a/src/client/unittest/unittest.ts b/src/client/unittest/unittest.ts deleted file mode 100644 index 26685765ddcf..000000000000 --- a/src/client/unittest/unittest.ts +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -import * as path from 'path'; -import * as baseTestRunner from './baseTestRunner'; -import * as settings from './../common/configSettings'; -import {OutputChannel} from 'vscode'; - -export class PythonUnitTest extends baseTestRunner.BaseTestRunner { - constructor(pythonSettings: settings.IPythonSettings, outputChannel: OutputChannel) { - super("unittest", pythonSettings, outputChannel, true); - } - - public runTests(filePath: string = ""): Promise { - if (!this.pythonSettings.unitTest.unittestEnabled) { - return Promise.resolve(); - } - - var ptyhonPath = this.pythonSettings.pythonPath; - var unittestPath = " unittest"; - var cmdLine = ""; - if (typeof filePath !== "string" || filePath.length === 0) { - cmdLine = `${ptyhonPath} -m unittest discover`; - } - else { - cmdLine = `${ptyhonPath} -m unittest ${filePath}`; - } - return new Promise(resolve => { - this.run(cmdLine).then(messages=> { - resolve(messages); - }); - }); - } -} diff --git a/src/client/workspaceSymbols/contracts.ts b/src/client/workspaceSymbols/contracts.ts new file mode 100644 index 000000000000..ae447a4e2fbb --- /dev/null +++ b/src/client/workspaceSymbols/contracts.ts @@ -0,0 +1,9 @@ +import { Position, SymbolKind } from 'vscode'; + +export interface ITag { + fileName: string; + symbolName: string; + symbolKind: SymbolKind; + position: Position; + code: string; +} diff --git a/src/client/workspaceSymbols/generator.ts b/src/client/workspaceSymbols/generator.ts new file mode 100644 index 000000000000..e90db302d26f --- /dev/null +++ b/src/client/workspaceSymbols/generator.ts @@ -0,0 +1,99 @@ +import * as path from 'path'; +import { Disposable, OutputChannel, Uri } from 'vscode'; +import { IApplicationShell } from '../common/application/types'; +import { IFileSystem } from '../common/platform/types'; +import { IProcessServiceFactory } from '../common/process/types'; +import { IConfigurationService, IPythonSettings } from '../common/types'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +export class Generator implements Disposable { + private optionsFile: string; + private disposables: Disposable[]; + private pythonSettings: IPythonSettings; + public get tagFilePath(): string { + return this.pythonSettings.workspaceSymbols.tagFilePath; + } + public get enabled(): boolean { + return this.pythonSettings.workspaceSymbols.enabled; + } + constructor( + public readonly workspaceFolder: Uri, + private readonly output: OutputChannel, + private readonly appShell: IApplicationShell, + private readonly fs: IFileSystem, + private readonly processServiceFactory: IProcessServiceFactory, + configurationService: IConfigurationService + ) { + this.disposables = []; + this.optionsFile = path.join(EXTENSION_ROOT_DIR, 'resources', 'ctagOptions'); + this.pythonSettings = configurationService.getSettings(workspaceFolder); + } + + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + public async generateWorkspaceTags(): Promise { + if (!this.pythonSettings.workspaceSymbols.enabled) { + return; + } + return this.generateTags({ directory: this.workspaceFolder.fsPath }); + } + private buildCmdArgs(): string[] { + const exclusions = this.pythonSettings.workspaceSymbols.exclusionPatterns; + const excludes = exclusions.length === 0 ? [] : exclusions.map((pattern) => `--exclude=${pattern}`); + + return [`--options=${this.optionsFile}`, '--languages=Python'].concat(excludes); + } + @captureTelemetry(EventName.WORKSPACE_SYMBOLS_BUILD) + private async generateTags(source: { directory?: string; file?: string }): Promise { + const tagFile = path.normalize(this.pythonSettings.workspaceSymbols.tagFilePath); + const cmd = this.pythonSettings.workspaceSymbols.ctagsPath; + const args = this.buildCmdArgs(); + let outputFile = tagFile; + if (source.file && source.file.length > 0) { + source.directory = path.dirname(source.file); + } + + if (path.dirname(outputFile) === source.directory) { + outputFile = path.basename(outputFile); + } + const outputDir = path.dirname(outputFile); + if (!(await this.fs.directoryExists(outputDir))) { + await this.fs.createDirectory(outputDir); + } + args.push('-o', outputFile, '.'); + this.output.appendLine(`${'-'.repeat(10)}Generating Tags${'-'.repeat(10)}`); + this.output.appendLine(`${cmd} ${args.join(' ')}`); + const promise = new Promise(async (resolve, reject) => { + try { + const processService = await this.processServiceFactory.create(); + const result = processService.execObservable(cmd, args, { cwd: source.directory }); + let errorMsg = ''; + result.out.subscribe( + (output) => { + if (output.source === 'stderr') { + errorMsg += output.out; + } + this.output.append(output.out); + }, + reject, + () => { + if (errorMsg.length > 0) { + reject(new Error(errorMsg)); + } else { + resolve(); + } + } + ); + } catch (ex) { + reject(ex); + } + }); + + this.appShell.setStatusBarMessage('Generating Tags', promise); + + await promise; + } +} diff --git a/src/client/workspaceSymbols/main.ts b/src/client/workspaceSymbols/main.ts new file mode 100644 index 000000000000..256373af7a9c --- /dev/null +++ b/src/client/workspaceSymbols/main.ts @@ -0,0 +1,152 @@ +import { CancellationToken, Disposable, languages, OutputChannel, TextDocument } from 'vscode'; +import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { Commands, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import { isNotInstalledError } from '../common/helpers'; +import { IFileSystem } from '../common/platform/types'; +import { IProcessServiceFactory } from '../common/process/types'; +import { IConfigurationService, IInstaller, InstallerResponse, IOutputChannel, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { Generator } from './generator'; +import { WorkspaceSymbolProvider } from './provider'; + +const MAX_NUMBER_OF_ATTEMPTS_TO_INSTALL_AND_BUILD = 2; + +export class WorkspaceSymbols implements Disposable { + private disposables: Disposable[]; + private generators: Generator[] = []; + private readonly outputChannel: OutputChannel; + private commandMgr: ICommandManager; + private fs: IFileSystem; + private workspace: IWorkspaceService; + private processFactory: IProcessServiceFactory; + private appShell: IApplicationShell; + private configurationService: IConfigurationService; + private documents: IDocumentManager; + + constructor(private serviceContainer: IServiceContainer) { + this.outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.commandMgr = this.serviceContainer.get(ICommandManager); + this.fs = this.serviceContainer.get(IFileSystem); + this.workspace = this.serviceContainer.get(IWorkspaceService); + this.processFactory = this.serviceContainer.get(IProcessServiceFactory); + this.appShell = this.serviceContainer.get(IApplicationShell); + this.configurationService = this.serviceContainer.get(IConfigurationService); + this.documents = this.serviceContainer.get(IDocumentManager); + this.disposables = []; + this.disposables.push(this.outputChannel); + this.registerCommands(); + this.initializeGenerators(); + languages.registerWorkspaceSymbolProvider( + new WorkspaceSymbolProvider(this.fs, this.commandMgr, this.generators) + ); + this.disposables.push(this.workspace.onDidChangeWorkspaceFolders(() => this.initializeGenerators())); + this.disposables.push(this.documents.onDidSaveTextDocument((e) => this.onDocumentSaved(e))); + this.buildSymbolsOnStart(); + } + public dispose() { + this.disposables.forEach((d) => d.dispose()); + } + private initializeGenerators() { + while (this.generators.length > 0) { + const generator = this.generators.shift()!; + generator.dispose(); + } + + if (Array.isArray(this.workspace.workspaceFolders)) { + this.workspace.workspaceFolders.forEach((wkSpc) => { + this.generators.push( + new Generator( + wkSpc.uri, + this.outputChannel, + this.appShell, + this.fs, + this.processFactory, + this.configurationService + ) + ); + }); + } + } + + private buildSymbolsOnStart() { + if (Array.isArray(this.workspace.workspaceFolders)) { + this.workspace.workspaceFolders.forEach((workspaceFolder) => { + const pythonSettings = this.configurationService.getSettings(workspaceFolder.uri); + if (pythonSettings.workspaceSymbols.rebuildOnStart) { + const promises = this.buildWorkspaceSymbols(true); + return Promise.all(promises); + } + }); + } + } + + private registerCommands() { + this.disposables.push( + this.commandMgr.registerCommand( + Commands.Build_Workspace_Symbols, + async (rebuild: boolean = true, token?: CancellationToken) => { + const promises = this.buildWorkspaceSymbols(rebuild, token); + return Promise.all(promises); + } + ) + ); + } + + private onDocumentSaved(document: TextDocument) { + const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); + const pythonSettings = this.configurationService.getSettings(workspaceFolder?.uri); + if (pythonSettings.workspaceSymbols.rebuildOnFileSave) { + const promises = this.buildWorkspaceSymbols(true); + return Promise.all(promises); + } + } + + // tslint:disable-next-line:no-any + private buildWorkspaceSymbols(rebuild: boolean = true, token?: CancellationToken): Promise[] { + if (token && token.isCancellationRequested) { + return []; + } + if (this.generators.length === 0) { + return []; + } + + let promptPromise: Promise; + let promptResponse: InstallerResponse; + return this.generators.map(async (generator) => { + if (!generator.enabled) { + return; + } + const exists = await this.fs.fileExists(generator.tagFilePath); + // If file doesn't exist, then run the ctag generator, + // or check if required to rebuild. + if (!rebuild && exists) { + return; + } + for (let counter = 0; counter < MAX_NUMBER_OF_ATTEMPTS_TO_INSTALL_AND_BUILD; counter += 1) { + try { + await generator.generateWorkspaceTags(); + return; + } catch (error) { + if (!isNotInstalledError(error)) { + return; + } + } + if (!token || token.isCancellationRequested) { + return; + } + // Display prompt once for all workspaces. + if (promptPromise) { + promptResponse = await promptPromise; + continue; + } else { + const installer = this.serviceContainer.get(IInstaller); + promptPromise = installer.promptToInstall(Product.ctags, this.workspace.workspaceFolders![0]!.uri); + promptResponse = await promptPromise; + } + if (promptResponse !== InstallerResponse.Installed || !token || token.isCancellationRequested) { + return; + } + } + }); + } +} diff --git a/src/client/workspaceSymbols/parser.ts b/src/client/workspaceSymbols/parser.ts new file mode 100644 index 000000000000..7ec49ba082ce --- /dev/null +++ b/src/client/workspaceSymbols/parser.ts @@ -0,0 +1,172 @@ +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IFileSystem } from '../common/platform/types'; +import { ITag } from './contracts'; + +// tslint:disable:no-require-imports no-var-requires no-suspicious-comment +// tslint:disable:no-any +// TODO: Turn these into imports. +const LineByLineReader = require('line-by-line'); +const NamedRegexp = require('named-js-regexp'); +const fuzzy = require('fuzzy'); + +const IsFileRegEx = /\tkind:file\tline:\d+$/g; +const LINE_REGEX = '(?\\w+)\\t(?.*)\\t\\/\\^(?.*)\\$\\/;"\\tkind:(?\\w+)\\tline:(?\\d+)$'; + +export interface IRegexGroup { + name: string; + file: string; + code: string; + type: string; + line: number; +} + +export function matchNamedRegEx(data: String, regex: String): IRegexGroup | null { + const compiledRegexp = NamedRegexp(regex, 'g'); + const rawMatch = compiledRegexp.exec(data); + if (rawMatch !== null) { + return rawMatch.groups(); + } + + return null; +} + +const CTagKinMapping = new Map(); +CTagKinMapping.set('_array', vscode.SymbolKind.Array); +CTagKinMapping.set('_boolean', vscode.SymbolKind.Boolean); +CTagKinMapping.set('_class', vscode.SymbolKind.Class); +CTagKinMapping.set('_classes', vscode.SymbolKind.Class); +CTagKinMapping.set('_constant', vscode.SymbolKind.Constant); +CTagKinMapping.set('_constants', vscode.SymbolKind.Constant); +CTagKinMapping.set('_constructor', vscode.SymbolKind.Constructor); +CTagKinMapping.set('_enum', vscode.SymbolKind.Enum); +CTagKinMapping.set('_enums', vscode.SymbolKind.Enum); +CTagKinMapping.set('_enumeration', vscode.SymbolKind.Enum); +CTagKinMapping.set('_enumerations', vscode.SymbolKind.Enum); +CTagKinMapping.set('_field', vscode.SymbolKind.Field); +CTagKinMapping.set('_fields', vscode.SymbolKind.Field); +CTagKinMapping.set('_file', vscode.SymbolKind.File); +CTagKinMapping.set('_files', vscode.SymbolKind.File); +CTagKinMapping.set('_function', vscode.SymbolKind.Function); +CTagKinMapping.set('_functions', vscode.SymbolKind.Function); +CTagKinMapping.set('_member', vscode.SymbolKind.Function); +CTagKinMapping.set('_interface', vscode.SymbolKind.Interface); +CTagKinMapping.set('_interfaces', vscode.SymbolKind.Interface); +CTagKinMapping.set('_key', vscode.SymbolKind.Key); +CTagKinMapping.set('_keys', vscode.SymbolKind.Key); +CTagKinMapping.set('_method', vscode.SymbolKind.Method); +CTagKinMapping.set('_methods', vscode.SymbolKind.Method); +CTagKinMapping.set('_module', vscode.SymbolKind.Module); +CTagKinMapping.set('_modules', vscode.SymbolKind.Module); +CTagKinMapping.set('_namespace', vscode.SymbolKind.Namespace); +CTagKinMapping.set('_namespaces', vscode.SymbolKind.Namespace); +CTagKinMapping.set('_number', vscode.SymbolKind.Number); +CTagKinMapping.set('_numbers', vscode.SymbolKind.Number); +CTagKinMapping.set('_null', vscode.SymbolKind.Null); +CTagKinMapping.set('_object', vscode.SymbolKind.Object); +CTagKinMapping.set('_package', vscode.SymbolKind.Package); +CTagKinMapping.set('_packages', vscode.SymbolKind.Package); +CTagKinMapping.set('_property', vscode.SymbolKind.Property); +CTagKinMapping.set('_properties', vscode.SymbolKind.Property); +CTagKinMapping.set('_objects', vscode.SymbolKind.Object); +CTagKinMapping.set('_string', vscode.SymbolKind.String); +CTagKinMapping.set('_variable', vscode.SymbolKind.Variable); +CTagKinMapping.set('_variables', vscode.SymbolKind.Variable); +CTagKinMapping.set('_projects', vscode.SymbolKind.Package); +CTagKinMapping.set('_defines', vscode.SymbolKind.Module); +CTagKinMapping.set('_labels', vscode.SymbolKind.Interface); +CTagKinMapping.set('_macros', vscode.SymbolKind.Function); +CTagKinMapping.set('_types (structs and records)', vscode.SymbolKind.Class); +CTagKinMapping.set('_subroutine', vscode.SymbolKind.Method); +CTagKinMapping.set('_subroutines', vscode.SymbolKind.Method); +CTagKinMapping.set('_types', vscode.SymbolKind.Class); +CTagKinMapping.set('_programs', vscode.SymbolKind.Class); +CTagKinMapping.set("_Object's method", vscode.SymbolKind.Method); +CTagKinMapping.set('_Module or functor', vscode.SymbolKind.Module); +CTagKinMapping.set('_Global variable', vscode.SymbolKind.Variable); +CTagKinMapping.set('_Type name', vscode.SymbolKind.Class); +CTagKinMapping.set('_A function', vscode.SymbolKind.Function); +CTagKinMapping.set('_A constructor', vscode.SymbolKind.Constructor); +CTagKinMapping.set('_An exception', vscode.SymbolKind.Class); +CTagKinMapping.set("_A 'structure' field", vscode.SymbolKind.Field); +CTagKinMapping.set('_procedure', vscode.SymbolKind.Function); +CTagKinMapping.set('_procedures', vscode.SymbolKind.Function); +CTagKinMapping.set('_constant definitions', vscode.SymbolKind.Constant); +CTagKinMapping.set('_javascript functions', vscode.SymbolKind.Function); +CTagKinMapping.set('_singleton methods', vscode.SymbolKind.Method); + +const newValuesAndKeys = {}; +CTagKinMapping.forEach((value, key) => { + (newValuesAndKeys as any)[key.substring(1)] = value; +}); +Object.keys(newValuesAndKeys).forEach((key) => { + CTagKinMapping.set(key, (newValuesAndKeys as any)[key]); +}); + +export function parseTags( + workspaceFolder: string, + tagFile: string, + query: string, + token: vscode.CancellationToken, + fs: IFileSystem +): Promise { + return fs.fileExists(tagFile).then((exists) => { + if (!exists) { + return Promise.resolve([]); + } + + return new Promise((resolve, reject) => { + const lr = new LineByLineReader(tagFile); + let lineNumber = 0; + const tags: ITag[] = []; + + lr.on('error', (err: Error) => { + reject(err); + }); + + lr.on('line', (line: string) => { + lineNumber = lineNumber + 1; + if (token.isCancellationRequested) { + lr.close(); + return; + } + const tag = parseTagsLine(workspaceFolder, line, query); + if (tag) { + tags.push(tag); + } + if (tags.length >= 100) { + lr.close(); + } + }); + + lr.on('end', () => { + resolve(tags); + }); + }); + }); +} +function parseTagsLine(workspaceFolder: string, line: string, searchPattern: string): ITag | undefined { + if (IsFileRegEx.test(line)) { + return; + } + const match = matchNamedRegEx(line, LINE_REGEX); + if (!match) { + return; + } + if (!fuzzy.test(searchPattern, match.name)) { + return; + } + let file = match.file; + if (!path.isAbsolute(file)) { + file = path.resolve(workspaceFolder, '.vscode', file); + } + + const symbolKind = CTagKinMapping.get(match.type) || vscode.SymbolKind.Null; + return { + fileName: file, + code: match.code, + position: new vscode.Position(Number(match.line) - 1, 0), + symbolName: match.name, + symbolKind: symbolKind + }; +} diff --git a/src/client/workspaceSymbols/provider.ts b/src/client/workspaceSymbols/provider.ts new file mode 100644 index 000000000000..1719445aa0ae --- /dev/null +++ b/src/client/workspaceSymbols/provider.ts @@ -0,0 +1,76 @@ +'use strict'; + +// tslint:disable-next-line:no-var-requires no-require-imports +const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); +import { + CancellationToken, + Location, + SymbolInformation, + Uri, + WorkspaceSymbolProvider as IWorspaceSymbolProvider +} from 'vscode'; +import { ICommandManager } from '../common/application/types'; +import { Commands } from '../common/constants'; +import { IFileSystem } from '../common/platform/types'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { Generator } from './generator'; +import { parseTags } from './parser'; + +export class WorkspaceSymbolProvider implements IWorspaceSymbolProvider { + public constructor( + private fs: IFileSystem, + private commands: ICommandManager, + private tagGenerators: Generator[] + ) {} + + @captureTelemetry(EventName.WORKSPACE_SYMBOLS_GO_TO) + public async provideWorkspaceSymbols(query: string, token: CancellationToken): Promise { + if (this.tagGenerators.length === 0) { + return []; + } + const generatorsWithTagFiles = await Promise.all( + this.tagGenerators.map((generator) => this.fs.fileExists(generator.tagFilePath)) + ); + if (generatorsWithTagFiles.filter((exists) => exists).length !== this.tagGenerators.length) { + await this.commands.executeCommand(Commands.Build_Workspace_Symbols, true, token); + } + + const generators: Generator[] = []; + await Promise.all( + this.tagGenerators.map(async (generator) => { + if (await this.fs.fileExists(generator.tagFilePath)) { + generators.push(generator); + } + }) + ); + + const promises = generators + .filter((generator) => generator !== undefined && generator.enabled) + .map(async (generator) => { + // load tags + const items = await parseTags( + generator!.workspaceFolder.fsPath, + generator!.tagFilePath, + query, + token, + this.fs + ); + if (!Array.isArray(items)) { + return []; + } + return items.map( + (item) => + new SymbolInformation( + item.symbolName, + item.symbolKind, + '', + new Location(Uri.file(item.fileName), item.position) + ) + ); + }); + + const symbols = await Promise.all(promises); + return flatten(symbols); + } +} diff --git a/src/datascience-ui/common/cellFactory.ts b/src/datascience-ui/common/cellFactory.ts new file mode 100644 index 000000000000..b65dff477de2 --- /dev/null +++ b/src/datascience-ui/common/cellFactory.ts @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { nbformat } from '@jupyterlab/coreutils'; +// tslint:disable-next-line: no-require-imports no-var-requires +const cloneDeep = require('lodash/cloneDeep'); +import '../../client/common/extensions'; +import { appendLineFeed, generateMarkdownFromCodeLines } from './index'; + +function uncommentMagicCommands(line: string): string { + // Uncomment lines that are shell assignments (starting with #!), + // line magic (starting with #!%) or cell magic (starting with #!%%). + if (/^#\s*!/.test(line)) { + // If the regex test passes, it's either line or cell magic. + // Hence, remove the leading # and ! including possible white space. + if (/^#\s*!\s*%%?/.test(line)) { + return line.replace(/^#\s*!\s*/, ''); + } + // If the test didn't pass, it's a shell assignment. In this case, only + // remove leading # including possible white space. + return line.replace(/^#\s*/, ''); + } else { + // If it's regular Python code, just return it. + return line; + } +} + +export function createMarkdownCell(code: string | string[], useSourceAsIs: boolean = false): nbformat.IMarkdownCell { + code = Array.isArray(code) ? code : [code]; + return { + cell_type: 'markdown', + metadata: {}, + source: useSourceAsIs ? code : generateMarkdownFromCodeLines(code) + }; +} + +export function createErrorOutput(error: Partial): nbformat.IError { + return { + output_type: 'error', + ename: error.name || error.message || 'Error', + evalue: error.message || error.name || 'Error', + traceback: (error.stack || '').splitLines() + }; +} +export function createCodeCell(): nbformat.ICodeCell; +// tslint:disable-next-line: unified-signatures +export function createCodeCell(code: string): nbformat.ICodeCell; +export function createCodeCell(code: string[], outputs: nbformat.IOutput[]): nbformat.ICodeCell; +// tslint:disable-next-line: unified-signatures +export function createCodeCell(code: string[], magicCommandsAsComments: boolean): nbformat.ICodeCell; +export function createCodeCell(code?: string | string[], options?: boolean | nbformat.IOutput[]): nbformat.ICodeCell { + const magicCommandsAsComments = typeof options === 'boolean' ? options : false; + const outputs = typeof options === 'boolean' ? [] : options || []; + code = code || ''; + // If we get a string, then no need to append line feeds. Leave as is (to preserve existing functionality). + // If we get an array, the append a linefeed. + const source = Array.isArray(code) + ? appendLineFeed(code, magicCommandsAsComments ? uncommentMagicCommands : undefined) + : code; + return { + cell_type: 'code', + execution_count: null, + metadata: {}, + outputs, + source + }; +} +/** + * Clones a cell. + * Also dumps unrecognized attributes from cells. + * + * @export + * @template T + * @param {T} cell + * @returns {T} + */ +export function cloneCell(cell: T): T { + // Construct the cell by hand so we drop unwanted/unrecognized properties from cells. + // This way, the cell contains only the attributes that are valid (supported type). + const clonedCell = cloneDeep(cell); + const source = Array.isArray(clonedCell.source) || typeof clonedCell.source === 'string' ? clonedCell.source : ''; + switch (cell.cell_type) { + case 'code': { + const codeCell: nbformat.ICodeCell = { + cell_type: 'code', + // tslint:disable-next-line: no-any + metadata: (clonedCell.metadata ?? {}) as any, + execution_count: typeof clonedCell.execution_count === 'number' ? clonedCell.execution_count : null, + outputs: Array.isArray(clonedCell.outputs) ? (clonedCell.outputs as nbformat.IOutput[]) : [], + source + }; + // tslint:disable-next-line: no-any + return (codeCell as any) as T; + } + case 'markdown': { + const markdownCell: nbformat.IMarkdownCell = { + cell_type: 'markdown', + // tslint:disable-next-line: no-any + metadata: (clonedCell.metadata ?? {}) as any, + source, + // tslint:disable-next-line: no-any + attachments: clonedCell.attachments as any + }; + // tslint:disable-next-line: no-any + return (markdownCell as any) as T; + } + case 'raw': { + const rawCell: nbformat.IRawCell = { + cell_type: 'raw', + // tslint:disable-next-line: no-any + metadata: (clonedCell.metadata ?? {}) as any, + source, + // tslint:disable-next-line: no-any + attachments: clonedCell.attachments as any + }; + // tslint:disable-next-line: no-any + return (rawCell as any) as T; + } + default: { + // Possibly one of our cell types (`message`). + return clonedCell; + } + } +} + +export function createCellFrom( + source: nbformat.IBaseCell, + target: nbformat.CellType +): nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell { + // If we're creating a new cell from the same base type, then ensure we preserve the metadata. + const baseCell: nbformat.IBaseCell = + source.cell_type === target + ? // tslint:disable-next-line: no-any + (cloneCell(source) as any) + : { + source: source.source, + cell_type: target, + // tslint:disable-next-line: no-any + metadata: cloneDeep(source.metadata) as any + }; + + switch (target) { + case 'code': { + // tslint:disable-next-line: no-unnecessary-local-variable no-any + const codeCell = (baseCell as any) as nbformat.ICodeCell; + codeCell.execution_count = null; + codeCell.outputs = []; + return codeCell; + } + case 'markdown': { + return baseCell as nbformat.IMarkdownCell; + } + case 'raw': { + return baseCell as nbformat.IRawCell; + } + default: { + throw new Error(`Unsupported target type, ${target}`); + } + } +} diff --git a/src/datascience-ui/common/index.css b/src/datascience-ui/common/index.css new file mode 100644 index 000000000000..b4cc7250b98c --- /dev/null +++ b/src/datascience-ui/common/index.css @@ -0,0 +1,5 @@ +body { + margin: 0; + padding: 0; + font-family: sans-serif; +} diff --git a/src/datascience-ui/common/index.ts b/src/datascience-ui/common/index.ts new file mode 100644 index 000000000000..c744c2d56464 --- /dev/null +++ b/src/datascience-ui/common/index.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; +import { noop } from '../../client/common/utils/misc'; + +const SingleQuoteMultiline = "'''"; +const DoubleQuoteMultiline = '"""'; + +export function concatMultilineString(str: nbformat.MultilineString, trim?: boolean): string { + const nonLineFeedWhiteSpaceTrim = /(^[\t\f\v\r ]+|[\t\f\v\r ]+$)/g; // Local var so don't have to reset the lastIndex. + if (Array.isArray(str)) { + let result = ''; + for (let i = 0; i < str.length; i += 1) { + const s = str[i]; + if (i < str.length - 1 && !s.endsWith('\n')) { + result = result.concat(`${s}\n`); + } else { + result = result.concat(s); + } + } + + // Just trim whitespace. Leave \n in place + return trim ? result.replace(nonLineFeedWhiteSpaceTrim, '') : result; + } + return trim ? str.toString().replace(nonLineFeedWhiteSpaceTrim, '') : str.toString(); +} + +export function splitMultilineString(source: nbformat.MultilineString): string[] { + // Make sure a multiline string is back the way Jupyter expects it + if (Array.isArray(source)) { + return source as string[]; + } + const str = source.toString(); + if (str.length > 0) { + // Each line should be a separate entry, but end with a \n if not last entry + const arr = str.split('\n'); + return arr + .map((s, i) => { + if (i < arr.length - 1) { + return `${s}\n`; + } + return s; + }) + .filter((s) => s.length > 0); // Skip last one if empty (it's the only one that could be length 0) + } + return []; +} + +export function removeLinesFromFrontAndBack(code: string): string { + const lines = code.splitLines({ trim: false, removeEmptyEntries: false }); + let foundNonEmptyLine = false; + let lastNonEmptyLine = -1; + let result: string[] = []; + parseForComments( + lines, + (_s, i) => { + result.push(lines[i]); + lastNonEmptyLine = i; + }, + (s, i) => { + const trimmed = s.trim(); + if (foundNonEmptyLine || trimmed) { + result.push(lines[i]); + foundNonEmptyLine = true; + } + if (trimmed) { + lastNonEmptyLine = i; + } + } + ); + + // Remove empty lines off the bottom too + if (lastNonEmptyLine < lines.length - 1) { + result = result.slice(0, result.length - (lines.length - 1 - lastNonEmptyLine)); + } + + return result.join('\n'); +} + +// Strip out comment lines from code +export function stripComments(str: string): string { + let result: string = ''; + parseForComments( + str.splitLines({ trim: false, removeEmptyEntries: false }), + (_s) => noop, + (s) => (result = result.concat(`${s}\n`)) + ); + return result; +} + +// Took this from jupyter/notebook +// https://github.com/jupyter/notebook/blob/b8b66332e2023e83d2ee04f83d8814f567e01a4e/notebook/static/base/js/utils.js +// Remove characters that are overridden by backspace characters +function fixBackspace(txt: string) { + let tmp = txt; + do { + txt = tmp; + // Cancel out anything-but-newline followed by backspace + tmp = txt.replace(/[^\n]\x08/gm, ''); + } while (tmp.length < txt.length); + return txt; +} + +// Using our own version for fixCarriageReturn. The jupyter version seems to not work. +function fixCarriageReturn(str: string): string { + // Go through the string, looking for \r's that are not followed by \n. This is + // a special case that means replace the string before. This is necessary to + // get an html display of this string to behave correctly. + + // Note: According to this: + // https://jsperf.com/javascript-concat-vs-join/2. + // Concat is way faster than array join for building up a string. + let result = ''; + let previousLinePos = 0; + for (let i = 0; i < str.length; i += 1) { + if (str[i] === '\r') { + // See if this is a line feed. If so, leave alone. This is goofy windows \r\n + if (i < str.length - 1 && str[i + 1] === '\n') { + // This line is legit, output it and convert to '\n' only. + result += str.substr(previousLinePos, i - previousLinePos); + result += '\n'; + previousLinePos = i + 2; + i += 1; + } else { + // This line should replace the previous one. Skip our \r + previousLinePos = i + 1; + } + } else if (str[i] === '\n') { + // This line is legit, output it. (Single linefeed) + result += str.substr(previousLinePos, i - previousLinePos + 1); + previousLinePos = i + 1; + } + } + result += str.substr(previousLinePos, str.length - previousLinePos); + return result; +} + +export function formatStreamText(str: string): string { + // Do the same thing jupyter is doing + return fixCarriageReturn(fixBackspace(str)); +} + +export function appendLineFeed(arr: string[], modifier?: (s: string) => string) { + return arr.map((s: string, i: number) => { + const out = modifier ? modifier(s) : s; + return i === arr.length - 1 ? `${out}` : `${out}\n`; + }); +} + +export function generateMarkdownFromCodeLines(lines: string[]) { + // Generate markdown by stripping out the comments and markdown header + return appendLineFeed(extractComments(lines.slice(lines.length > 1 ? 1 : 0))); +} + +// tslint:disable-next-line: cyclomatic-complexity +export function parseForComments( + lines: string[], + foundCommentLine: (s: string, i: number) => void, + foundNonCommentLine: (s: string, i: number) => void +) { + // Check for either multiline or single line comments + let insideMultilineComment: string | undefined; + let insideMultilineQuote: string | undefined; + let pos = 0; + for (const l of lines) { + const trim = l.trim(); + // Multiline is triple quotes of either kind + const isMultilineComment = trim.startsWith(SingleQuoteMultiline) + ? SingleQuoteMultiline + : trim.startsWith(DoubleQuoteMultiline) + ? DoubleQuoteMultiline + : undefined; + const isMultilineQuote = trim.includes(SingleQuoteMultiline) + ? SingleQuoteMultiline + : trim.includes(DoubleQuoteMultiline) + ? DoubleQuoteMultiline + : undefined; + + // Check for ending quotes of multiline string + if (insideMultilineQuote) { + if (insideMultilineQuote === isMultilineQuote) { + insideMultilineQuote = undefined; + } + foundNonCommentLine(l, pos); + // Not inside quote, see if inside a comment + } else if (insideMultilineComment) { + if (insideMultilineComment === isMultilineComment) { + insideMultilineComment = undefined; + } + if (insideMultilineComment) { + foundCommentLine(l, pos); + } + // Not inside either, see if starting a quote + } else if (isMultilineQuote && !isMultilineComment) { + // Make sure doesn't begin and end on the same line. + const beginQuote = trim.indexOf(isMultilineQuote); + const endQuote = trim.lastIndexOf(isMultilineQuote); + insideMultilineQuote = endQuote !== beginQuote ? undefined : isMultilineQuote; + foundNonCommentLine(l, pos); + // Not starting a quote, might be starting a comment + } else if (isMultilineComment) { + // See if this line ends the comment too or not + const endIndex = trim.indexOf(isMultilineComment, 3); + insideMultilineComment = endIndex >= 0 ? undefined : isMultilineComment; + + // Might end with text too + if (trim.length > 3) { + foundCommentLine(trim.slice(3, endIndex >= 0 ? endIndex : undefined), pos); + } + } else { + // Normal line + if (trim.startsWith('#')) { + foundCommentLine(trim.slice(1), pos); + } else { + foundNonCommentLine(l, pos); + } + } + pos += 1; + } +} + +function extractComments(lines: string[]): string[] { + const result: string[] = []; + parseForComments( + lines, + (s) => result.push(s), + (_s) => noop() + ); + return result; +} diff --git a/src/datascience-ui/common/main.ts b/src/datascience-ui/common/main.ts new file mode 100644 index 000000000000..870ae21034f8 --- /dev/null +++ b/src/datascience-ui/common/main.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +declare let __webpack_public_path__: string; + +// tslint:disable-next-line: no-any +if ((window as any).__PVSC_Public_Path) { + // This variable tells Webpack to this as the root path used to request webpack bundles. + // tslint:disable-next-line: no-any + __webpack_public_path__ = (window as any).__PVSC_Public_Path; +} diff --git a/src/datascience-ui/data-explorer/cellFormatter.css b/src/datascience-ui/data-explorer/cellFormatter.css new file mode 100644 index 000000000000..10c63a79c8ac --- /dev/null +++ b/src/datascience-ui/data-explorer/cellFormatter.css @@ -0,0 +1,8 @@ +.number-formatter { + text-align: right; +} + +.cell-formatter { + /* Note: This is impacted by the RowHeightAdjustment in reactSlickGrid.tsx */ + margin: 0px 0px 0px 0px; +} \ No newline at end of file diff --git a/src/datascience-ui/data-explorer/cellFormatter.tsx b/src/datascience-ui/data-explorer/cellFormatter.tsx new file mode 100644 index 000000000000..7c29d00a1265 --- /dev/null +++ b/src/datascience-ui/data-explorer/cellFormatter.tsx @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import './cellFormatter.css'; + +import * as React from 'react'; +import * as ReactDOMServer from 'react-dom/server'; +import { ColumnType } from '../../client/datascience/data-viewing/types'; +import { ISlickRow } from './reactSlickGrid'; + +interface ICellFormatterProps { + value: string | number | object | boolean; + columnDef: Slick.Column; +} + +class CellFormatter extends React.Component { + constructor(props: ICellFormatterProps) { + super(props); + } + + public render() { + // Render based on type + if (this.props.value !== null && this.props.columnDef && this.props.columnDef.hasOwnProperty('type')) { + // tslint:disable-next-line: no-any + const columnType = (this.props.columnDef as any).type; + switch (columnType) { + case ColumnType.Bool: + return this.renderBool(this.props.value as boolean); + break; + + case ColumnType.Number: + return this.renderNumber(this.props.value as number); + break; + + default: + break; + } + } + + // Otherwise an unknown type or a string + const val = this.props.value !== null ? this.props.value.toString() : ''; + return ( +

+ {val} +
+ ); + } + + private renderBool(value: boolean) { + return ( +
+ {value.toString()} +
+ ); + } + + private renderNumber(value: number) { + const val = value.toString(); + return ( +
+ {val} +
+ ); + } +} + +export function cellFormatterFunc( + _row: number, + _cell: number, + // tslint:disable-next-line: no-any + value: any, + columnDef: Slick.Column, + _dataContext: Slick.SlickData +): string { + return ReactDOMServer.renderToString(); +} diff --git a/src/datascience-ui/data-explorer/emptyRowsView.css b/src/datascience-ui/data-explorer/emptyRowsView.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/datascience-ui/data-explorer/emptyRowsView.tsx b/src/datascience-ui/data-explorer/emptyRowsView.tsx new file mode 100644 index 000000000000..96eb82385872 --- /dev/null +++ b/src/datascience-ui/data-explorer/emptyRowsView.tsx @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import './emptyRowsView.css'; + +import * as React from 'react'; +import { getLocString } from '../react-common/locReactSide'; + +export interface IEmptyRowsProps {} + +export const EmptyRows = (_props: IEmptyRowsProps) => { + const message = getLocString('DataScience.noRowsInDataViewer', 'No rows match current filter'); + + return
{message}
; +}; diff --git a/src/datascience-ui/data-explorer/globalJQueryImports.ts b/src/datascience-ui/data-explorer/globalJQueryImports.ts new file mode 100644 index 000000000000..d3bfae0a054b --- /dev/null +++ b/src/datascience-ui/data-explorer/globalJQueryImports.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/* +This file exists for the sole purpose of ensuring jQuery and slickgrid load in the right sequence. +We need to first load jquery into window.jQuery. +After that we need to load slickgrid, and then the jQuery plugin from slickgrid event.drag. +*/ + +// Slickgrid requires jquery to be defined. Globally. So we do some hacks here. +// We need to manipulate the grid with the same jquery that it uses +// use slickgridJQ instead of the usual $ to make it clear that we need that JQ and not +// the one currently in node-modules + +// tslint:disable-next-line: no-var-requires no-require-imports +require('expose-loader?jQuery!slickgrid/lib/jquery-1.11.2.min'); + +// tslint:disable-next-line: no-var-requires no-require-imports +require('slickgrid/lib/jquery-1.11.2.min'); + +// tslint:disable-next-line: no-var-requires no-require-imports +require('expose-loader?jQuery.fn.drag!slickgrid/lib/jquery.event.drag-2.3.0'); diff --git a/src/datascience-ui/data-explorer/index.html b/src/datascience-ui/data-explorer/index.html new file mode 100644 index 000000000000..b94a027725e1 --- /dev/null +++ b/src/datascience-ui/data-explorer/index.html @@ -0,0 +1,356 @@ + + + + + + + React App + + + + + +
+ + + diff --git a/src/datascience-ui/data-explorer/index.tsx b/src/datascience-ui/data-explorer/index.tsx new file mode 100644 index 000000000000..aa6136295e4e --- /dev/null +++ b/src/datascience-ui/data-explorer/index.tsx @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// This must be on top, do not change. Required by webpack. +import '../common/main'; +// This must be on top, do not change. Required by webpack. + +// tslint:disable-next-line: ordered-imports +import '../common/index.css'; + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { IVsCodeApi } from '../react-common/postOffice'; +import { detectBaseTheme } from '../react-common/themeDetector'; +import { MainPanel } from './mainPanel'; + +// This special function talks to vscode from a web panel +export declare function acquireVsCodeApi(): IVsCodeApi; + +const baseTheme = detectBaseTheme(); + +// tslint:disable:no-typeof-undefined +ReactDOM.render( + , // Turn this back off when we have real variable explorer data + document.getElementById('root') as HTMLElement +); diff --git a/src/datascience-ui/data-explorer/mainPanel.css b/src/datascience-ui/data-explorer/mainPanel.css new file mode 100644 index 000000000000..0616067bf249 --- /dev/null +++ b/src/datascience-ui/data-explorer/mainPanel.css @@ -0,0 +1,13 @@ + +.main-panel { + position: absolute; + bottom: 0; + top: 0px; + left: 0px; + right: 0; + font-size: var(--code-font-size); + font-family: var(--code-font-family); + background-color: var(--vscode-editor-background); + overflow: hidden; +} + diff --git a/src/datascience-ui/data-explorer/mainPanel.tsx b/src/datascience-ui/data-explorer/mainPanel.tsx new file mode 100644 index 000000000000..7df86002ae67 --- /dev/null +++ b/src/datascience-ui/data-explorer/mainPanel.tsx @@ -0,0 +1,336 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import './mainPanel.css'; + +import { JSONArray } from '@phosphor/coreutils'; +import * as React from 'react'; +import * as uuid from 'uuid/v4'; + +import { + CellFetchAllLimit, + CellFetchSizeFirst, + CellFetchSizeSubsequent, + ColumnType, + DataViewerMessages, + IDataFrameInfo, + IDataViewerMapping, + IGetRowsResponse, + IRowsResponse +} from '../../client/datascience/data-viewing/types'; +import { SharedMessages } from '../../client/datascience/messages'; +import { IDataScienceExtraSettings } from '../../client/datascience/types'; +import { getLocString, storeLocStrings } from '../react-common/locReactSide'; +import { IMessageHandler, PostOffice } from '../react-common/postOffice'; +import { Progress } from '../react-common/progress'; +import { StyleInjector } from '../react-common/styleInjector'; +import { cellFormatterFunc } from './cellFormatter'; +import { ISlickGridAdd, ISlickRow, ReactSlickGrid } from './reactSlickGrid'; +import { generateTestData } from './testData'; + +// Our css has to come after in order to override body styles +export interface IMainPanelProps { + skipDefault?: boolean; + baseTheme: string; + testMode?: boolean; +} + +//tslint:disable:no-any +interface IMainPanelState { + gridColumns: Slick.Column[]; + gridRows: ISlickRow[]; + fetchedRowCount: number; + totalRowCount: number; + filters: {}; + indexColumn: string; + styleReady: boolean; + settings?: IDataScienceExtraSettings; +} + +export class MainPanel extends React.Component implements IMessageHandler { + private container: React.Ref = React.createRef(); + private sentDone = false; + private postOffice: PostOffice = new PostOffice(); + private gridAddEvent: Slick.Event = new Slick.Event(); + private rowFetchSizeFirst: number = 0; + private rowFetchSizeSubsequent: number = 0; + private rowFetchSizeAll: number = 0; + // Just used for testing. + private grid: React.RefObject = React.createRef(); + private updateTimeout?: NodeJS.Timer | number; + + // tslint:disable-next-line:max-func-body-length + constructor(props: IMainPanelProps, _state: IMainPanelState) { + super(props); + + if (!this.props.skipDefault) { + const data = generateTestData(5000); + this.state = { + gridColumns: data.columns.map((c) => { + return { ...c, formatter: cellFormatterFunc }; + }), + gridRows: [], + totalRowCount: data.rows.length, + fetchedRowCount: -1, + filters: {}, + indexColumn: data.primaryKeys[0], + styleReady: false + }; + + // Fire off a timer to mimic dynamic loading + setTimeout(() => this.handleGetAllRowsResponse(data.rows), 1000); + } else { + this.state = { + gridColumns: [], + gridRows: [], + totalRowCount: 0, + fetchedRowCount: -1, + filters: {}, + indexColumn: 'index', + styleReady: false + }; + } + } + + public componentWillMount() { + // Add ourselves as a handler for the post office + this.postOffice.addHandler(this); + + // Tell the dataviewer code we have started. + this.postOffice.sendMessage(DataViewerMessages.Started); + } + + public componentWillUnmount() { + this.postOffice.removeHandler(this); + this.postOffice.dispose(); + } + + public render = () => { + if (!this.state.settings) { + return
; + } + + // Send our done message if we haven't yet and we just reached full capacity. Do it here so we + // can guarantee our render will run before somebody checks our rendered output. + if (this.state.totalRowCount && this.state.totalRowCount === this.state.fetchedRowCount && !this.sentDone) { + this.sentDone = true; + this.sendMessage(DataViewerMessages.CompletedData); + } + + const progressBar = this.state.totalRowCount > this.state.fetchedRowCount ? : undefined; + + return ( +
+ + {progressBar} + {this.state.totalRowCount > 0 && this.state.styleReady && this.renderGrid()} +
+ ); + }; + + // tslint:disable-next-line:no-any + public handleMessage = (msg: string, payload?: any) => { + switch (msg) { + case DataViewerMessages.InitializeData: + this.initializeData(payload); + break; + + case DataViewerMessages.GetAllRowsResponse: + this.handleGetAllRowsResponse(payload as IRowsResponse); + break; + + case DataViewerMessages.GetRowsResponse: + this.handleGetRowChunkResponse(payload as IGetRowsResponse); + break; + + case SharedMessages.UpdateSettings: + this.updateSettings(payload); + break; + + case SharedMessages.LocInit: + this.initializeLoc(payload); + break; + + default: + break; + } + + return false; + }; + + private initializeLoc(content: string) { + const locJSON = JSON.parse(content); + storeLocStrings(locJSON); + } + + private updateSettings(content: string) { + const newSettingsJSON = JSON.parse(content); + const newSettings = newSettingsJSON as IDataScienceExtraSettings; + this.setState({ + settings: newSettings + }); + } + + private saveReadyState = () => { + this.setState({ styleReady: true }); + }; + + private renderGrid() { + const filterRowsText = getLocString('DataScience.filterRowsButton', 'Filter Rows'); + const filterRowsTooltip = getLocString('DataScience.filterRowsTooltip', 'Click to filter.'); + + return ( + + ); + } + + // tslint:disable-next-line:no-any + private initializeData(payload: any) { + // Payload should be an IJupyterVariable with the first 100 rows filled out + if (payload) { + const variable = payload as IDataFrameInfo; + if (variable) { + const columns = this.generateColumns(variable); + const totalRowCount = variable.rowCount ? variable.rowCount : 0; + const initialRows: ISlickRow[] = []; + const indexColumn = variable.indexColumn ? variable.indexColumn : 'index'; + + this.setState({ + gridColumns: columns, + gridRows: initialRows, + totalRowCount, + fetchedRowCount: initialRows.length, + indexColumn: indexColumn + }); + + // Compute our row fetch sizes based on the number of columns + this.rowFetchSizeAll = Math.round(CellFetchAllLimit / columns.length); + this.rowFetchSizeFirst = Math.round(Math.max(2, CellFetchSizeFirst / columns.length)); + this.rowFetchSizeSubsequent = Math.round(Math.max(2, CellFetchSizeSubsequent / columns.length)); + + // Request the rest of the data if necessary + if (initialRows.length !== totalRowCount) { + // Get all at once if less than 1000 + if (totalRowCount < this.rowFetchSizeAll) { + this.getAllRows(); + } else { + this.getRowsInChunks(initialRows.length, totalRowCount); + } + } + } + } + } + + private getAllRows() { + this.sendMessage(DataViewerMessages.GetAllRowsRequest); + } + + private getRowsInChunks(startIndex: number, endIndex: number) { + // Ask for our first chunk. Don't spam jupyter though with all requests at once + // Instead, do them one at a time. + const chunkEnd = startIndex + Math.min(this.rowFetchSizeFirst, endIndex); + const chunkStart = startIndex; + this.sendMessage(DataViewerMessages.GetRowsRequest, { start: chunkStart, end: chunkEnd }); + } + + private handleGetAllRowsResponse(response: IRowsResponse) { + const rows = response ? (response as JSONArray) : []; + const normalized = this.normalizeRows(rows); + + // Update our fetched count and actual rows + this.setState({ + gridRows: this.state.gridRows.concat(normalized), + fetchedRowCount: this.state.totalRowCount + }); + + // Add all of these rows to the grid + this.updateRows(normalized); + } + + private handleGetRowChunkResponse(response: IGetRowsResponse) { + // We have a new fetched row count + const rows = response.rows ? (response.rows as JSONArray) : []; + const normalized = this.normalizeRows(rows); + const newFetched = this.state.fetchedRowCount + (response.end - response.start); + + // gridRows should have our entire list. We need to replace our part with our new results + const before = this.state.gridRows.slice(0, response.start); + const after = response.end < this.state.gridRows.length ? this.state.gridRows.slice(response.end) : []; + const newActual = before.concat(normalized.concat(after)); + + // Apply this to our state + this.setState({ + fetchedRowCount: newFetched, + gridRows: newActual + }); + + // Tell our grid about the new ros + this.updateRows(normalized); + + // Get the next chunk + if (newFetched < this.state.totalRowCount) { + const chunkStart = response.end; + const chunkEnd = Math.min(chunkStart + this.rowFetchSizeSubsequent, this.state.totalRowCount); + this.sendMessage(DataViewerMessages.GetRowsRequest, { start: chunkStart, end: chunkEnd }); + } + } + + private generateColumns(variable: IDataFrameInfo): Slick.Column[] { + if (variable.columns) { + return variable.columns.map((c: { key: string; type: ColumnType }, i: number) => { + return { + type: c.type, + field: c.key.toString(), + id: `${i}`, + name: c.key.toString(), + sortable: true, + formatter: cellFormatterFunc + }; + }); + } + return []; + } + + private normalizeRows(rows: JSONArray): ISlickRow[] { + // Make sure we have an index field and all rows have an item + return rows.map((r: any | undefined) => { + if (!r) { + r = {}; + } + if (!r.hasOwnProperty(this.state.indexColumn)) { + r[this.state.indexColumn] = uuid(); + } + return r; + }); + } + + private sendMessage(type: T, payload?: M[T]) { + this.postOffice.sendMessage(type, payload); + } + + private updateRows(newRows: ISlickRow[]) { + if (this.updateTimeout !== undefined) { + clearTimeout(this.updateTimeout as any); + this.updateTimeout = undefined; + } + if (!this.grid.current) { + // This might happen before we render the grid. Postpone till then. + this.updateTimeout = setTimeout(() => this.updateRows(newRows), 10); + } else { + this.gridAddEvent.notify({ newRows }); + } + } +} diff --git a/src/datascience-ui/data-explorer/progressBar.css b/src/datascience-ui/data-explorer/progressBar.css new file mode 100644 index 000000000000..8249651f7029 --- /dev/null +++ b/src/datascience-ui/data-explorer/progressBar.css @@ -0,0 +1,9 @@ +.progress-bar { + margin:2px; + text-align: center; +} + +.progress-container { + padding: 20px; + text-align:center; +} \ No newline at end of file diff --git a/src/datascience-ui/data-explorer/progressBar.tsx b/src/datascience-ui/data-explorer/progressBar.tsx new file mode 100644 index 000000000000..97c279b5ca68 --- /dev/null +++ b/src/datascience-ui/data-explorer/progressBar.tsx @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import './progressBar.css'; + +import * as React from 'react'; +import { getLocString } from '../react-common/locReactSide'; + +export interface IEmptyRowsProps { + total: number; + current: number; +} + +export const ProgressBar = (props: IEmptyRowsProps) => { + const percent = (props.current / props.total) * 100; + const percentText = `${Math.round(percent)}%`; + const style: React.CSSProperties = { + width: percentText + }; + const message = getLocString('DataScience.fetchingDataViewer', 'Fetching data ...'); + + return ( +
+ {message} +
+ {percentText} +
+
+ ); +}; diff --git a/src/datascience-ui/data-explorer/reactSlickGrid.css b/src/datascience-ui/data-explorer/reactSlickGrid.css new file mode 100644 index 000000000000..b52165279386 --- /dev/null +++ b/src/datascience-ui/data-explorer/reactSlickGrid.css @@ -0,0 +1,87 @@ +.outer-container { + width:auto; + height:100%; +} + +.react-grid-container { + border-color: var(--vscode-editor-inactiveSelectionBackground); + border-style: solid; + border-width: 1px; +} + +.react-grid-measure { + position: absolute; + bottom: 5px; +} + +.react-grid-filter-button { + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + padding: 10px; + border: none; + border-radius: 5px; + cursor: pointer; + margin:8px 4px; +} + +.react-grid-header-cell { + padding: 0px 4px; + background-color: var(--vscode-debugToolBar-background); + color: var(--vscode-editor-foreground); + text-align: left; + font-weight: bold; + border-right-color: var(--vscode-editor-inactiveSelectionBackground); +} + +.react-grid-cell { + padding: 0px 4px; + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + border-bottom-color: var(--vscode-editor-inactiveSelectionBackground); + border-right-color: var(--vscode-editor-inactiveSelectionBackground); + border-right-style: solid; + box-sizing: border-box; +} + +.react-grid-cell.active { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +/* Some overrides necessary to get the colors we want */ +.slick-headerrow-column { + background-color: var(--vscode-debugToolBar-background); + border-right-color: var(--vscode-editor-inactiveSelectionBackground); + border-right-style: solid; +} + +.slick-header-column.ui-state-default, .slick-group-header-column.ui-state-default { + border-right-color: var(--vscode-editor-inactiveSelectionBackground); +} + +.slick-sort-indicator { + float: right; + width: 0px; + margin-right: 12px; + margin-top: 1px; +} + +.react-grid-header-cell:hover { + background-color: var(--vscode-editor-inactiveSelectionBackground); +} + +.react-grid-header-cell > .slick-sort-indicator-asc::before { + background: none; + content: '▲'; + align-items: center; +} + +.react-grid-header-cell > .slick-sort-indicator-desc::before { + background: none; + content: '▼'; + align-items: center; +} + +.slick-row:hover > .react-grid-cell { + background-color: var(--override-selection-background, var(--vscode-editor-selectionBackground)); +} \ No newline at end of file diff --git a/src/datascience-ui/data-explorer/reactSlickGrid.tsx b/src/datascience-ui/data-explorer/reactSlickGrid.tsx new file mode 100644 index 000000000000..12a67ede2f66 --- /dev/null +++ b/src/datascience-ui/data-explorer/reactSlickGrid.tsx @@ -0,0 +1,538 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { ColumnType, MaxStringCompare } from '../../client/datascience/data-viewing/types'; +import { KeyCodes } from '../react-common/constants'; +import { measureText } from '../react-common/textMeasure'; +import './globalJQueryImports'; +import { ReactSlickGridFilterBox } from './reactSlickGridFilterBox'; + +/* +WARNING: Do not change the order of these imports. +Slick grid MUST be imported after we load jQuery and other stuff from `./globalJQueryImports` +*/ +// tslint:disable-next-line: no-var-requires no-require-imports +const slickgridJQ = require('slickgrid/lib/jquery-1.11.2.min'); + +// Adding comments to ensure order of imports does not change due to auto formatters. +// tslint:disable-next-line: ordered-imports +import 'slickgrid/slick.core'; +// Adding comments to ensure order of imports does not change due to auto formatters. +// tslint:disable-next-line: ordered-imports +import 'slickgrid/slick.dataview'; +// Adding comments to ensure order of imports does not change due to auto formatters. +// tslint:disable-next-line: ordered-imports +import 'slickgrid/slick.grid'; +// Adding comments to ensure order of imports does not change due to auto formatters. +// tslint:disable-next-line: ordered-imports +import 'slickgrid/plugins/slick.autotooltips'; +// Adding comments to ensure order of imports does not change due to auto formatters. +// tslint:disable-next-line: ordered-imports +import 'slickgrid/slick.grid.css'; +// Make sure our css comes after the slick grid css. We override some of its styles. +// tslint:disable-next-line: ordered-imports +import './reactSlickGrid.css'; +/* +WARNING: Do not change the order of these imports. +Slick grid MUST be imported after we load jQuery and other stuff from `./globalJQueryImports` +*/ + +const MinColumnWidth = 70; +const MaxColumnWidth = 500; + +export interface ISlickRow extends Slick.SlickData { + id: string; +} + +export interface ISlickGridAdd { + newRows: ISlickRow[]; +} + +// tslint:disable:no-any +export interface ISlickGridProps { + idProperty: string; + columns: Slick.Column[]; + rowsAdded: Slick.Event; + filterRowsText: string; + filterRowsTooltip: string; + forceHeight?: number; +} + +interface ISlickGridState { + grid?: Slick.Grid; + showingFilters?: boolean; + fontSize: number; +} + +class ColumnFilter { + private matchFunc: (v: any) => boolean; + private lessThanRegEx = /^\s*<\s*(\d+.*)/; + private lessThanEqualRegEx = /^\s*<=\s*(\d+.*).*/; + private greaterThanRegEx = /^\s*>\s*(\d+.*).*/; + private greaterThanEqualRegEx = /^\s*>=\s*(\d+.*).*/; + private equalThanRegEx = /^\s*=\s*(\d+.*).*/; + + constructor(text: string, column: Slick.Column) { + if (text && text.length > 0) { + const columnType = (column as any).type; + switch (columnType) { + case ColumnType.String: + default: + this.matchFunc = (v: any) => !v || v.toString().includes(text); + break; + + case ColumnType.Number: + this.matchFunc = this.generateNumericOperation(text); + break; + } + } else { + this.matchFunc = (_v: any) => true; + } + } + + public matches(value: any): boolean { + return this.matchFunc(value); + } + + private extractDigits(text: string, regex: RegExp): number { + const match = regex.exec(text); + if (match && match.length > 1) { + return parseFloat(match[1]); + } + return 0; + } + + private generateNumericOperation(text: string): (v: any) => boolean { + if (this.lessThanRegEx.test(text)) { + const n1 = this.extractDigits(text, this.lessThanRegEx); + return (v: any) => v !== undefined && v < n1; + } else if (this.lessThanEqualRegEx.test(text)) { + const n2 = this.extractDigits(text, this.lessThanEqualRegEx); + return (v: any) => v !== undefined && v <= n2; + } else if (this.greaterThanRegEx.test(text)) { + const n3 = this.extractDigits(text, this.greaterThanRegEx); + return (v: any) => v !== undefined && v > n3; + } else if (this.greaterThanEqualRegEx.test(text)) { + const n4 = this.extractDigits(text, this.greaterThanEqualRegEx); + return (v: any) => v !== undefined && v >= n4; + } else if (this.equalThanRegEx.test(text)) { + const n5 = this.extractDigits(text, this.equalThanRegEx); + return (v: any) => v !== undefined && v === n5; + } else { + const n6 = parseFloat(text); + return (v: any) => v !== undefined && v === n6; + } + } +} + +export class ReactSlickGrid extends React.Component { + private containerRef: React.RefObject; + private measureRef: React.RefObject; + private dataView: Slick.Data.DataView = new Slick.Data.DataView(); + private columnFilters: Map = new Map(); + private resizeTimer?: number; + private autoResizedColumns: boolean = false; + + constructor(props: ISlickGridProps) { + super(props); + this.state = { fontSize: 15 }; + this.containerRef = React.createRef(); + this.measureRef = React.createRef(); + this.props.rowsAdded.subscribe(this.addedRows); + } + + // tslint:disable-next-line:max-func-body-length + public componentDidMount = () => { + window.addEventListener('resize', this.windowResized); + + if (this.containerRef.current) { + // Compute font size. Default to 15 if not found. + let fontSize = parseInt( + getComputedStyle(this.containerRef.current).getPropertyValue('--code-font-size'), + 10 + ); + if (isNaN(fontSize)) { + fontSize = 15; + } + + // Setup options for the grid + const options: Slick.GridOptions = { + asyncEditorLoading: true, + editable: false, + enableCellNavigation: true, + showHeaderRow: true, + enableColumnReorder: false, + explicitInitialization: false, + viewportClass: 'react-grid', + rowHeight: this.getAppropiateRowHeight(fontSize) + }; + + // Transform columns so they are sortable and stylable + const columns = this.props.columns.map((c) => { + c.sortable = true; + c.headerCssClass = 'react-grid-header-cell'; + c.cssClass = 'react-grid-cell'; + return c; + }); + + // Create the grid + const grid = new Slick.Grid(this.containerRef.current, this.dataView, columns, options); + grid.registerPlugin(new Slick.AutoTooltips({ enableForCells: true, enableForHeaderCells: true })); + + // Setup our dataview + this.dataView.beginUpdate(); + this.dataView.setFilter(this.filter.bind(this)); + this.dataView.setItems([], this.props.idProperty); + this.dataView.endUpdate(); + + this.dataView.onRowCountChanged.subscribe((_e, _args) => { + grid.updateRowCount(); + grid.render(); + }); + + this.dataView.onRowsChanged.subscribe((_e, args) => { + grid.invalidateRows(args.rows); + grid.render(); + }); + + // Setup the filter render + grid.onHeaderRowCellRendered.subscribe(this.renderFilterCell); + + grid.onHeaderCellRendered.subscribe((_e, args) => { + // Add a tab index onto our header cell + args.node.tabIndex = 0; + }); + + // Unbind the slickgrid key handler from the canvas code + // We want to keep EnableCellNavigation on so that we can use the slickgrid + // public navigations functions, but we don't want the slickgrid keyhander + // to eat tab keys and prevent us from tabbing to input boxes or column headers + const canvasElement = grid.getCanvasNode(); + slickgridJQ(canvasElement).off('keydown'); + + if (this.containerRef && this.containerRef.current) { + // slickgrid creates empty focus sink div elements that capture tab input we don't want that + // so unhook their key handlers and remove their tabindex + const firstFocus = slickgridJQ('.react-grid-container').children().first(); + const lastFocus = slickgridJQ('.react-grid-container').children().last(); + slickgridJQ(firstFocus).off('keydown').removeAttr('tabindex'); + slickgridJQ(lastFocus).off('keydown').removeAttr('tabindex'); + + // Set our key handling on the actual grid viewport + slickgridJQ('.react-grid') + .on('keydown', this.slickgridHandleKeyDown) + .attr('role', 'grid') + .on('focusin', this.slickgridFocus); + slickgridJQ('.grid-canvas').on('keydown', this.slickgridHandleKeyDown); + } + + // Setup the sorting + grid.onSort.subscribe(this.sort); + + // Init to force the actual render. + grid.init(); + + // Set the initial sort column to our index column + const indexColumn = columns.find((c) => c.field === this.props.idProperty); + if (indexColumn && indexColumn.id) { + grid.setSortColumn(indexColumn.id, true); + } + + // Save in our state + this.setState({ grid, fontSize }); + } + + // Act like a resize happened to refresh the layout. + this.windowResized(); + }; + + public componentWillUnmount = () => { + if (this.resizeTimer) { + window.clearTimeout(this.resizeTimer); + } + window.removeEventListener('resize', this.windowResized); + if (this.state.grid) { + this.state.grid.destroy(); + } + }; + + public componentDidUpdate = (_prevProps: ISlickGridProps, prevState: ISlickGridState) => { + if (this.state.showingFilters && this.state.grid) { + this.state.grid.setHeaderRowVisibility(true); + } else if (this.state.showingFilters === false && this.state.grid) { + this.state.grid.setHeaderRowVisibility(false); + } + + // If this is our first time setting the grid, we need to dynanically modify the styles + // that the slickGrid generates for the rows. It's eliminating some of the height + if (!prevState.grid && this.state.grid && this.containerRef.current) { + this.updateCssStyles(); + } + }; + + public render() { + const style: React.CSSProperties = this.props.forceHeight + ? { + height: `${this.props.forceHeight}px`, + width: `${this.props.forceHeight}px` + } + : {}; + + return ( +
+ +
+
+
+ ); + } + + // public for testing + public sort = (_e: Slick.EventData, args: Slick.OnSortEventArgs) => { + // Note: dataView.fastSort is an IE workaround. Not necessary. + this.dataView.sort((l: any, r: any) => this.compareElements(l, r, args.sortCol), args.sortAsc); + args.grid.invalidateAllRows(); + args.grid.render(); + }; + + // Public for testing + public filterChanged = (text: string, column: Slick.Column) => { + if (column && column.field) { + this.columnFilters.set(column.field, new ColumnFilter(text, column)); + this.dataView.refresh(); + } + }; + + // These adjustments for the row height come from trial and error, by changing the font size in VS code, + // opening a new Data Viewer, and making sure the data is visible + // They were tested up to a font size of 60, and the row height still allows the content to be seen + private getAppropiateRowHeight(fontSize: number): number { + switch (true) { + case fontSize < 15: + return fontSize + 4; + case fontSize < 20: + return fontSize + 8; + case fontSize < 30: + return fontSize + 10; + default: + return fontSize + 12; + } + } + + // If the slickgrid gets focus and nothing is selected select the first item + // so that you can keyboard navigate from there + private slickgridFocus = (_e: any): void => { + if (this.state.grid) { + if (!this.state.grid.getActiveCell()) { + this.state.grid.setActiveCell(0, 0); + } + } + }; + + private slickgridHandleKeyDown = (e: KeyboardEvent): void => { + let handled: boolean = false; + + // Defined here: + // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Grid_Role#Keyboard_interactions + + if (this.state.grid) { + // The slickgrid version of jquery populates keyCode not code, so use the numerical values here + switch (e.keyCode) { + case KeyCodes.LeftArrow: + this.state.grid.navigateLeft(); + handled = true; + break; + case KeyCodes.UpArrow: + this.state.grid.navigateUp(); + handled = true; + break; + case KeyCodes.RightArrow: + this.state.grid.navigateRight(); + handled = true; + break; + case KeyCodes.DownArrow: + this.state.grid.navigateDown(); + handled = true; + break; + case KeyCodes.PageUp: + this.state.grid.navigatePageUp(); + handled = true; + break; + case KeyCodes.PageDown: + this.state.grid.navigatePageDown(); + handled = true; + break; + case KeyCodes.End: + e.ctrlKey ? this.state.grid.navigateBottom() : this.state.grid.navigateRowEnd(); + handled = true; + break; + case KeyCodes.Home: + e.ctrlKey ? this.state.grid.navigateTop() : this.state.grid.navigateRowStart(); + handled = true; + break; + default: + } + } + + if (handled) { + // Don't let the parent / browser do stuff if we handle it + // otherwise we'll both move the cell selection and scroll the window + // with up and down keys + e.stopPropagation(); + e.preventDefault(); + } + }; + + private updateCssStyles = () => { + if (this.state.grid && this.containerRef.current) { + const gridName = (this.state.grid as any).getUID() as string; + const document = this.containerRef.current.ownerDocument; + if (document) { + const cssOverrideNode = document.createElement('style'); + const rule = `.${gridName} .slick-cell {height: ${this.getAppropiateRowHeight( + this.state.fontSize + )}px;}`; + cssOverrideNode.setAttribute('type', 'text/css'); + cssOverrideNode.setAttribute('rel', 'stylesheet'); + cssOverrideNode.appendChild(document.createTextNode(rule)); + document.head.appendChild(cssOverrideNode); + } + } + }; + + private windowResized = () => { + if (this.resizeTimer) { + clearTimeout(this.resizeTimer); + } + this.resizeTimer = window.setTimeout(this.updateGridSize, 10); + }; + + private updateGridSize = () => { + if (this.state.grid && this.containerRef.current && this.measureRef.current) { + // We use a div at the bottom to figure out our expected height. Slickgrid isn't + // so good without a specific height set in the style. + const height = this.measureRef.current.offsetTop - this.containerRef.current.offsetTop; + this.containerRef.current.style.height = `${this.props.forceHeight ? this.props.forceHeight : height}px`; + this.state.grid.resizeCanvas(); + } + }; + + private autoResizeColumns(rows: ISlickRow[]) { + if (this.state.grid) { + const fontString = this.computeFont(); + const columns = this.state.grid.getColumns(); + columns.forEach((c) => { + let colWidth = MinColumnWidth; + rows.forEach((r: any) => { + const field = c.field ? r[c.field] : ''; + const fieldWidth = field ? measureText(field.toString(), fontString) : 0; + colWidth = Math.min(MaxColumnWidth, Math.max(colWidth, fieldWidth)); + }); + c.width = colWidth; + }); + this.state.grid.setColumns(columns); + + // We also need to update the styles as slickgrid will mess up the height of rows + // again + setTimeout(() => { + this.updateCssStyles(); + + // Hide the header row after we finally resize our columns + this.state.grid!.setHeaderRowVisibility(false); + }, 0); + } + } + + private computeFont(): string | null { + if (this.containerRef.current) { + const style = getComputedStyle(this.containerRef.current); + return style ? style.font : null; + } + return null; + } + + private addedRows = (_e: Slick.EventData, data: ISlickGridAdd) => { + // Add all of these new rows into our data. + this.dataView.beginUpdate(); + for (const row of data.newRows) { + this.dataView.addItem(row); + } + + // Update columns if we haven't already + if (!this.autoResizedColumns) { + this.autoResizedColumns = true; + this.autoResizeColumns(data.newRows); + } + + this.dataView.endUpdate(); + + // This should cause a rowsChanged event in the dataview that will + // refresh the grid. + }; + + // tslint:disable-next-line: no-any + private filter(item: any, _args: any): boolean { + const fields = Array.from(this.columnFilters.keys()); + for (const field of fields) { + if (field) { + const filter = this.columnFilters.get(field); + if (filter) { + if (!filter.matches(item[field])) { + return false; + } + } + } + } + return true; + } + + private clickFilterButton = (e: React.SyntheticEvent) => { + e.preventDefault(); + this.setState({ showingFilters: !this.state.showingFilters }); + }; + + private renderFilterCell = (_e: Slick.EventData, args: Slick.OnHeaderRowCellRenderedEventArgs) => { + ReactDOM.render(, args.node); + }; + + private compareElements(a: any, b: any, col?: Slick.Column): number { + if (col) { + const sortColumn = col.field; + if (sortColumn && col.hasOwnProperty('type')) { + const columnType = (col as any).type; + const isStringColumn = columnType === 'string' || columnType === 'object'; + if (isStringColumn) { + const aVal = a[sortColumn] ? a[sortColumn].toString() : ''; + const bVal = b[sortColumn] ? b[sortColumn].toString() : ''; + const aStr = aVal ? aVal.substring(0, Math.min(aVal.length, MaxStringCompare)) : aVal; + const bStr = bVal ? bVal.substring(0, Math.min(bVal.length, MaxStringCompare)) : bVal; + return aStr.localeCompare(bStr); + } else { + const aVal = a[sortColumn]; + const bVal = b[sortColumn]; + return aVal === bVal ? 0 : aVal > bVal ? 1 : -1; + } + } + } + + // No sort column, try index column + if (a.hasOwnProperty(this.props.idProperty) && b.hasOwnProperty(this.props.idProperty)) { + const sortColumn = this.props.idProperty; + const aVal = a[sortColumn]; + const bVal = b[sortColumn]; + return aVal === bVal ? 0 : aVal > bVal ? 1 : -1; + } + + return -1; + } +} diff --git a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.css b/src/datascience-ui/data-explorer/reactSlickGridFilterBox.css new file mode 100644 index 000000000000..0d5670650776 --- /dev/null +++ b/src/datascience-ui/data-explorer/reactSlickGridFilterBox.css @@ -0,0 +1,17 @@ +.filter-box { + border-color: var(--vscode-editor-inactiveSelectionBackground); + border-style: solid; + border-width: 1px; + display: block; + position: relative; + left: -2px; + top: -3px; + width: 98%; + padding: 1px; + margin: 0px; +} + +.filter-box:focus { + border-color: var(--vscode-editor-selectionBackground); + outline: none; +} \ No newline at end of file diff --git a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx b/src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx new file mode 100644 index 000000000000..12c1a9530cdd --- /dev/null +++ b/src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as React from 'react'; + +import './reactSlickGridFilterBox.css'; + +interface IFilterProps { + column: Slick.Column; + onChange(val: string, column: Slick.Column): void; +} + +export class ReactSlickGridFilterBox extends React.Component { + constructor(props: IFilterProps) { + super(props); + } + + public render() { + return ( + + ); + } + + private updateInputValue = (evt: React.SyntheticEvent) => { + const element = evt.currentTarget as HTMLInputElement; + if (element) { + this.props.onChange(element.value, this.props.column); + } + }; +} diff --git a/src/datascience-ui/data-explorer/testData.ts b/src/datascience-ui/data-explorer/testData.ts new file mode 100644 index 000000000000..963a702ee1a0 --- /dev/null +++ b/src/datascience-ui/data-explorer/testData.ts @@ -0,0 +1,12518 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export interface ITestData { + columns: { id: string; name: string; type: string }[]; + primaryKeys: string[]; + rows: {}[]; + loadingRows: {}[]; +} + +// tslint:disable +export function generateTestData(_numberOfRows: number): ITestData { + const columns = [ + { id: 'PassengerId', name: 'PassengerId', field: 'PassengerId', type: 'integer' }, + { id: 'SibSp', name: 'SibSp', field: 'SibSp', type: 'integer' }, + { id: 'Ticket', name: 'Ticket', field: 'Ticket', type: 'string' }, + { id: 'Parch', name: 'Parch', field: 'Parch', type: 'integer' }, + { id: 'Cabin', name: 'Cabin', field: 'Cabin', type: 'string' }, + { id: 'Age', name: 'Age', field: 'Age', type: 'integer' }, + { id: 'Fare', name: 'Fare', field: 'Fare', type: 'number' }, + { id: 'Name', name: 'Name', field: 'Name', type: 'string' }, + { id: 'Survived', name: 'Survived', field: 'Survived', type: 'bool' }, + { id: 'Pclass', name: 'Pclass', field: 'Pclass', type: 'integer' }, + { id: 'Embarked', name: 'Embarked', field: 'Embarked', type: 'string' }, + { id: 'Sex', name: 'Sex', field: 'Sex', type: 'string' } + ]; + + const keys = ['PassengerId']; + + const rows: {}[] = titanicData; + + return { + columns, + primaryKeys: keys, + rows, + loadingRows: titanicData.map((_t) => { + return {}; + }) + }; +} + +const titanicData = [ + { + SibSp: 1, + Ticket: 'A/5 21171', + Parch: 0, + Cabin: null, + PassengerId: 1, + Age: 22, + Fare: 7.25, + Name: 'Braund, Mr. Owen Harris', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'PC 17599', + Parch: 0, + Cabin: 'C85', + PassengerId: 2, + Age: 38, + Fare: 71.2833, + Name: 'Cumings, Mrs. John Bradley (Florence Briggs Thayer)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'STON/O2. 3101282', + Parch: 0, + Cabin: null, + PassengerId: 3, + Age: 26, + Fare: 7.925, + Name: 'Heikkinen, Miss. Laina', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '113803', + Parch: 0, + Cabin: 'C123', + PassengerId: 4, + Age: 35, + Fare: 53.1, + Name: 'Futrelle, Mrs. Jacques Heath (Lily May Peel)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '373450', + Parch: 0, + Cabin: null, + PassengerId: 5, + Age: 35, + Fare: 8.05, + Name: 'Allen, Mr. William Henry', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '330877', + Parch: 0, + Cabin: null, + PassengerId: 6, + Age: null, + Fare: 8.4583, + Name: 'Moran, Mr. James', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '17463', + Parch: 0, + Cabin: 'E46', + PassengerId: 7, + Age: 54, + Fare: 51.8625, + Name: 'McCarthy, Mr. Timothy J', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '349909', + Parch: 1, + Cabin: null, + PassengerId: 8, + Age: 2, + Fare: 21.075, + Name: 'Palsson, Master. Gosta Leonard', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347742', + Parch: 2, + Cabin: null, + PassengerId: 9, + Age: 27, + Fare: 11.1333, + Name: 'Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '237736', + Parch: 0, + Cabin: null, + PassengerId: 10, + Age: 14, + Fare: 30.0708, + Name: 'Nasser, Mrs. Nicholas (Adele Achem)', + Survived: true, + Pclass: 2, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'PP 9549', + Parch: 1, + Cabin: 'G6', + PassengerId: 11, + Age: 4, + Fare: 16.7, + Name: 'Sandstrom, Miss. Marguerite Rut', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113783', + Parch: 0, + Cabin: 'C103', + PassengerId: 12, + Age: 58, + Fare: 26.55, + Name: 'Bonnell, Miss. Elizabeth', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'A/5. 2151', + Parch: 0, + Cabin: null, + PassengerId: 13, + Age: 20, + Fare: 8.05, + Name: 'Saundercock, Mr. William Henry', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '347082', + Parch: 5, + Cabin: null, + PassengerId: 14, + Age: 39, + Fare: 31.275, + Name: 'Andersson, Mr. Anders Johan', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '350406', + Parch: 0, + Cabin: null, + PassengerId: 15, + Age: 14, + Fare: 7.8542, + Name: 'Vestrom, Miss. Hulda Amanda Adolfina', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '248706', + Parch: 0, + Cabin: null, + PassengerId: 16, + Age: 55, + Fare: 16, + Name: 'Hewlett, Mrs. (Mary D Kingcome) ', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 4, + Ticket: '382652', + Parch: 1, + Cabin: null, + PassengerId: 17, + Age: 2, + Fare: 29.125, + Name: 'Rice, Master. Eugene', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '244373', + Parch: 0, + Cabin: null, + PassengerId: 18, + Age: null, + Fare: 13, + Name: 'Williams, Mr. Charles Eugene', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '345763', + Parch: 0, + Cabin: null, + PassengerId: 19, + Age: 31, + Fare: 18, + Name: 'Vander Planke, Mrs. Julius (Emelia Maria Vandemoortele)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2649', + Parch: 0, + Cabin: null, + PassengerId: 20, + Age: null, + Fare: 7.225, + Name: 'Masselmani, Mrs. Fatima', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '239865', + Parch: 0, + Cabin: null, + PassengerId: 21, + Age: 35, + Fare: 26, + Name: 'Fynney, Mr. Joseph J', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '248698', + Parch: 0, + Cabin: 'D56', + PassengerId: 22, + Age: 34, + Fare: 13, + Name: 'Beesley, Mr. Lawrence', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '330923', + Parch: 0, + Cabin: null, + PassengerId: 23, + Age: 15, + Fare: 8.0292, + Name: 'McGowan, Miss. Anna "Annie"', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113788', + Parch: 0, + Cabin: 'A6', + PassengerId: 24, + Age: 28, + Fare: 35.5, + Name: 'Sloper, Mr. William Thompson', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '349909', + Parch: 1, + Cabin: null, + PassengerId: 25, + Age: 8, + Fare: 21.075, + Name: 'Palsson, Miss. Torborg Danira', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '347077', + Parch: 5, + Cabin: null, + PassengerId: 26, + Age: 38, + Fare: 31.3875, + Name: 'Asplund, Mrs. Carl Oscar (Selma Augusta Emilia Johansson)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2631', + Parch: 0, + Cabin: null, + PassengerId: 27, + Age: null, + Fare: 7.225, + Name: 'Emir, Mr. Farred Chehab', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '19950', + Parch: 2, + Cabin: 'C23 C25 C27', + PassengerId: 28, + Age: 19, + Fare: 263, + Name: 'Fortune, Mr. Charles Alexander', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '330959', + Parch: 0, + Cabin: null, + PassengerId: 29, + Age: null, + Fare: 7.8792, + Name: 'O\'Dwyer, Miss. Ellen "Nellie"', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349216', + Parch: 0, + Cabin: null, + PassengerId: 30, + Age: null, + Fare: 7.8958, + Name: 'Todoroff, Mr. Lalio', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17601', + Parch: 0, + Cabin: null, + PassengerId: 31, + Age: 40, + Fare: 27.7208, + Name: 'Uruchurtu, Don. Manuel E', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'PC 17569', + Parch: 0, + Cabin: 'B78', + PassengerId: 32, + Age: null, + Fare: 146.5208, + Name: 'Spencer, Mrs. William Augustus (Marie Eugenie)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '335677', + Parch: 0, + Cabin: null, + PassengerId: 33, + Age: null, + Fare: 7.75, + Name: 'Glynn, Miss. Mary Agatha', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'C.A. 24579', + Parch: 0, + Cabin: null, + PassengerId: 34, + Age: 66, + Fare: 10.5, + Name: 'Wheadon, Mr. Edward H', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'PC 17604', + Parch: 0, + Cabin: null, + PassengerId: 35, + Age: 28, + Fare: 82.1708, + Name: 'Meyer, Mr. Edgar Joseph', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '113789', + Parch: 0, + Cabin: null, + PassengerId: 36, + Age: 42, + Fare: 52, + Name: 'Holverson, Mr. Alexander Oskar', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2677', + Parch: 0, + Cabin: null, + PassengerId: 37, + Age: null, + Fare: 7.2292, + Name: 'Mamee, Mr. Hanna', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A./5. 2152', + Parch: 0, + Cabin: null, + PassengerId: 38, + Age: 21, + Fare: 8.05, + Name: 'Cann, Mr. Ernest Charles', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '345764', + Parch: 0, + Cabin: null, + PassengerId: 39, + Age: 18, + Fare: 18, + Name: 'Vander Planke, Miss. Augusta Maria', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '2651', + Parch: 0, + Cabin: null, + PassengerId: 40, + Age: 14, + Fare: 11.2417, + Name: 'Nicola-Yarred, Miss. Jamila', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '7546', + Parch: 0, + Cabin: null, + PassengerId: 41, + Age: 40, + Fare: 9.475, + Name: 'Ahlin, Mrs. Johan (Johanna Persdotter Larsson)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '11668', + Parch: 0, + Cabin: null, + PassengerId: 42, + Age: 27, + Fare: 21, + Name: 'Turpin, Mrs. William John Robert (Dorothy Ann Wonnacott)', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349253', + Parch: 0, + Cabin: null, + PassengerId: 43, + Age: null, + Fare: 7.8958, + Name: 'Kraeff, Mr. Theodor', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'SC/Paris 2123', + Parch: 2, + Cabin: null, + PassengerId: 44, + Age: 3, + Fare: 41.5792, + Name: 'Laroche, Miss. Simonne Marie Anne Andree', + Survived: true, + Pclass: 2, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '330958', + Parch: 0, + Cabin: null, + PassengerId: 45, + Age: 19, + Fare: 7.8792, + Name: 'Devaney, Miss. Margaret Delia', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'S.C./A.4. 23567', + Parch: 0, + Cabin: null, + PassengerId: 46, + Age: null, + Fare: 8.05, + Name: 'Rogers, Mr. William John', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '370371', + Parch: 0, + Cabin: null, + PassengerId: 47, + Age: null, + Fare: 15.5, + Name: 'Lennon, Mr. Denis', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '14311', + Parch: 0, + Cabin: null, + PassengerId: 48, + Age: null, + Fare: 7.75, + Name: "O'Driscoll, Miss. Bridget", + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 2, + Ticket: '2662', + Parch: 0, + Cabin: null, + PassengerId: 49, + Age: null, + Fare: 21.6792, + Name: 'Samaan, Mr. Youssef', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '349237', + Parch: 0, + Cabin: null, + PassengerId: 50, + Age: 18, + Fare: 17.8, + Name: 'Arnold-Franchi, Mrs. Josef (Josefine Franchi)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 4, + Ticket: '3101295', + Parch: 1, + Cabin: null, + PassengerId: 51, + Age: 7, + Fare: 39.6875, + Name: 'Panula, Master. Juha Niilo', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/4. 39886', + Parch: 0, + Cabin: null, + PassengerId: 52, + Age: 21, + Fare: 7.8, + Name: 'Nosworthy, Mr. Richard Cater', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'PC 17572', + Parch: 0, + Cabin: 'D33', + PassengerId: 53, + Age: 49, + Fare: 76.7292, + Name: 'Harper, Mrs. Henry Sleeper (Myna Haxtun)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '2926', + Parch: 0, + Cabin: null, + PassengerId: 54, + Age: 29, + Fare: 26, + Name: 'Faunthorpe, Mrs. Lizzie (Elizabeth Anne Wilkinson)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113509', + Parch: 1, + Cabin: 'B30', + PassengerId: 55, + Age: 65, + Fare: 61.9792, + Name: 'Ostby, Mr. Engelhart Cornelius', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '19947', + Parch: 0, + Cabin: 'C52', + PassengerId: 56, + Age: null, + Fare: 35.5, + Name: 'Woolner, Mr. Hugh', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C.A. 31026', + Parch: 0, + Cabin: null, + PassengerId: 57, + Age: 21, + Fare: 10.5, + Name: 'Rugg, Miss. Emily', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2697', + Parch: 0, + Cabin: null, + PassengerId: 58, + Age: 28.5, + Fare: 7.2292, + Name: 'Novel, Mr. Mansouer', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 34651', + Parch: 2, + Cabin: null, + PassengerId: 59, + Age: 5, + Fare: 27.75, + Name: 'West, Miss. Constance Mirium', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 5, + Ticket: 'CA 2144', + Parch: 2, + Cabin: null, + PassengerId: 60, + Age: 11, + Fare: 46.9, + Name: 'Goodwin, Master. William Frederick', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2669', + Parch: 0, + Cabin: null, + PassengerId: 61, + Age: 22, + Fare: 7.2292, + Name: 'Sirayanian, Mr. Orsen', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113572', + Parch: 0, + Cabin: 'B28', + PassengerId: 62, + Age: 38, + Fare: 80, + Name: 'Icard, Miss. Amelie', + Survived: true, + Pclass: 1, + Embarked: null, + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '36973', + Parch: 0, + Cabin: 'C83', + PassengerId: 63, + Age: 45, + Fare: 83.475, + Name: 'Harris, Mr. Henry Birkhardt', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '347088', + Parch: 2, + Cabin: null, + PassengerId: 64, + Age: 4, + Fare: 27.9, + Name: 'Skoog, Master. Harald', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17605', + Parch: 0, + Cabin: null, + PassengerId: 65, + Age: null, + Fare: 27.7208, + Name: 'Stewart, Mr. Albert A', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2661', + Parch: 1, + Cabin: null, + PassengerId: 66, + Age: null, + Fare: 15.2458, + Name: 'Moubarek, Master. Gerios', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C.A. 29395', + Parch: 0, + Cabin: 'F33', + PassengerId: 67, + Age: 29, + Fare: 10.5, + Name: 'Nye, Mrs. (Elizabeth Ramell)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'S.P. 3464', + Parch: 0, + Cabin: null, + PassengerId: 68, + Age: 19, + Fare: 8.1583, + Name: 'Crease, Mr. Ernest James', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 4, + Ticket: '3101281', + Parch: 2, + Cabin: null, + PassengerId: 69, + Age: 17, + Fare: 7.925, + Name: 'Andersson, Miss. Erna Alexandra', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 2, + Ticket: '315151', + Parch: 0, + Cabin: null, + PassengerId: 70, + Age: 26, + Fare: 8.6625, + Name: 'Kink, Mr. Vincenz', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C.A. 33111', + Parch: 0, + Cabin: null, + PassengerId: 71, + Age: 32, + Fare: 10.5, + Name: 'Jenkin, Mr. Stephen Curnow', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 5, + Ticket: 'CA 2144', + Parch: 2, + Cabin: null, + PassengerId: 72, + Age: 16, + Fare: 46.9, + Name: 'Goodwin, Miss. Lillian Amy', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'S.O.C. 14879', + Parch: 0, + Cabin: null, + PassengerId: 73, + Age: 21, + Fare: 73.5, + Name: 'Hood, Mr. Ambrose Jr', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2680', + Parch: 0, + Cabin: null, + PassengerId: 74, + Age: 26, + Fare: 14.4542, + Name: 'Chronopoulos, Mr. Apostolos', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '1601', + Parch: 0, + Cabin: null, + PassengerId: 75, + Age: 32, + Fare: 56.4958, + Name: 'Bing, Mr. Lee', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '348123', + Parch: 0, + Cabin: 'F G73', + PassengerId: 76, + Age: 25, + Fare: 7.65, + Name: 'Moen, Mr. Sigurd Hansen', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349208', + Parch: 0, + Cabin: null, + PassengerId: 77, + Age: null, + Fare: 7.8958, + Name: 'Staneff, Mr. Ivan', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '374746', + Parch: 0, + Cabin: null, + PassengerId: 78, + Age: null, + Fare: 8.05, + Name: 'Moutal, Mr. Rahamin Haim', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '248738', + Parch: 2, + Cabin: null, + PassengerId: 79, + Age: 0.83, + Fare: 29, + Name: 'Caldwell, Master. Alden Gates', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '364516', + Parch: 0, + Cabin: null, + PassengerId: 80, + Age: 30, + Fare: 12.475, + Name: 'Dowdell, Miss. Elizabeth', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '345767', + Parch: 0, + Cabin: null, + PassengerId: 81, + Age: 22, + Fare: 9, + Name: 'Waelens, Mr. Achille', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '345779', + Parch: 0, + Cabin: null, + PassengerId: 82, + Age: 29, + Fare: 9.5, + Name: 'Sheerlinck, Mr. Jan Baptist', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '330932', + Parch: 0, + Cabin: null, + PassengerId: 83, + Age: null, + Fare: 7.7875, + Name: 'McDermott, Miss. Brigdet Delia', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113059', + Parch: 0, + Cabin: null, + PassengerId: 84, + Age: 28, + Fare: 47.1, + Name: 'Carrau, Mr. Francisco M', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SO/C 14885', + Parch: 0, + Cabin: null, + PassengerId: 85, + Age: 17, + Fare: 10.5, + Name: 'Ilett, Miss. Bertha', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 3, + Ticket: '3101278', + Parch: 0, + Cabin: null, + PassengerId: 86, + Age: 33, + Fare: 15.85, + Name: 'Backstrom, Mrs. Karl Alfred (Maria Mathilda Gustafsson)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'W./C. 6608', + Parch: 3, + Cabin: null, + PassengerId: 87, + Age: 16, + Fare: 34.375, + Name: 'Ford, Mr. William Neal', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/OQ 392086', + Parch: 0, + Cabin: null, + PassengerId: 88, + Age: null, + Fare: 8.05, + Name: 'Slocovski, Mr. Selman Francis', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '19950', + Parch: 2, + Cabin: 'C23 C25 C27', + PassengerId: 89, + Age: 23, + Fare: 263, + Name: 'Fortune, Miss. Mabel Helen', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '343275', + Parch: 0, + Cabin: null, + PassengerId: 90, + Age: 24, + Fare: 8.05, + Name: 'Celotti, Mr. Francesco', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '343276', + Parch: 0, + Cabin: null, + PassengerId: 91, + Age: 29, + Fare: 8.05, + Name: 'Christmann, Mr. Emil', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347466', + Parch: 0, + Cabin: null, + PassengerId: 92, + Age: 20, + Fare: 7.8542, + Name: 'Andreasson, Mr. Paul Edvin', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'W.E.P. 5734', + Parch: 0, + Cabin: 'E31', + PassengerId: 93, + Age: 46, + Fare: 61.175, + Name: 'Chaffee, Mr. Herbert Fuller', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 2315', + Parch: 2, + Cabin: null, + PassengerId: 94, + Age: 26, + Fare: 20.575, + Name: 'Dean, Mr. Bertram Frank', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '364500', + Parch: 0, + Cabin: null, + PassengerId: 95, + Age: 59, + Fare: 7.25, + Name: 'Coxon, Mr. Daniel', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '374910', + Parch: 0, + Cabin: null, + PassengerId: 96, + Age: null, + Fare: 8.05, + Name: 'Shorney, Mr. Charles Joseph', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17754', + Parch: 0, + Cabin: 'A5', + PassengerId: 97, + Age: 71, + Fare: 34.6542, + Name: 'Goldschmidt, Mr. George B', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17759', + Parch: 1, + Cabin: 'D10 D12', + PassengerId: 98, + Age: 23, + Fare: 63.3583, + Name: 'Greenfield, Mr. William Bertram', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '231919', + Parch: 1, + Cabin: null, + PassengerId: 99, + Age: 34, + Fare: 23, + Name: 'Doling, Mrs. John T (Ada Julia Bone)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '244367', + Parch: 0, + Cabin: null, + PassengerId: 100, + Age: 34, + Fare: 26, + Name: 'Kantor, Mr. Sinai', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349245', + Parch: 0, + Cabin: null, + PassengerId: 101, + Age: 28, + Fare: 7.8958, + Name: 'Petranec, Miss. Matilda', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349215', + Parch: 0, + Cabin: null, + PassengerId: 102, + Age: null, + Fare: 7.8958, + Name: 'Petroff, Mr. Pastcho ("Pentcho")', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '35281', + Parch: 1, + Cabin: 'D26', + PassengerId: 103, + Age: 21, + Fare: 77.2875, + Name: 'White, Mr. Richard Frasar', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '7540', + Parch: 0, + Cabin: null, + PassengerId: 104, + Age: 33, + Fare: 8.6542, + Name: 'Johansson, Mr. Gustaf Joel', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '3101276', + Parch: 0, + Cabin: null, + PassengerId: 105, + Age: 37, + Fare: 7.925, + Name: 'Gustafsson, Mr. Anders Vilhelm', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349207', + Parch: 0, + Cabin: null, + PassengerId: 106, + Age: 28, + Fare: 7.8958, + Name: 'Mionoff, Mr. Stoytcho', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '343120', + Parch: 0, + Cabin: null, + PassengerId: 107, + Age: 21, + Fare: 7.65, + Name: 'Salkjelsvik, Miss. Anna Kristine', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '312991', + Parch: 0, + Cabin: null, + PassengerId: 108, + Age: null, + Fare: 7.775, + Name: 'Moss, Mr. Albert Johan', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349249', + Parch: 0, + Cabin: null, + PassengerId: 109, + Age: 38, + Fare: 7.8958, + Name: 'Rekic, Mr. Tido', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '371110', + Parch: 0, + Cabin: null, + PassengerId: 110, + Age: null, + Fare: 24.15, + Name: 'Moran, Miss. Bertha', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '110465', + Parch: 0, + Cabin: 'C110', + PassengerId: 111, + Age: 47, + Fare: 52, + Name: 'Porter, Mr. Walter Chamberlain', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2665', + Parch: 0, + Cabin: null, + PassengerId: 112, + Age: 14.5, + Fare: 14.4542, + Name: 'Zabour, Miss. Hileni', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '324669', + Parch: 0, + Cabin: null, + PassengerId: 113, + Age: 22, + Fare: 8.05, + Name: 'Barton, Mr. David John', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '4136', + Parch: 0, + Cabin: null, + PassengerId: 114, + Age: 20, + Fare: 9.825, + Name: 'Jussila, Miss. Katriina', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2627', + Parch: 0, + Cabin: null, + PassengerId: 115, + Age: 17, + Fare: 14.4583, + Name: 'Attalah, Miss. Malake', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'STON/O 2. 3101294', + Parch: 0, + Cabin: null, + PassengerId: 116, + Age: 21, + Fare: 7.925, + Name: 'Pekoniemi, Mr. Edvard', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '370369', + Parch: 0, + Cabin: null, + PassengerId: 117, + Age: 70.5, + Fare: 7.75, + Name: 'Connors, Mr. Patrick', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '11668', + Parch: 0, + Cabin: null, + PassengerId: 118, + Age: 29, + Fare: 21, + Name: 'Turpin, Mr. William John Robert', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17558', + Parch: 1, + Cabin: 'B58 B60', + PassengerId: 119, + Age: 24, + Fare: 247.5208, + Name: 'Baxter, Mr. Quigg Edmond', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 4, + Ticket: '347082', + Parch: 2, + Cabin: null, + PassengerId: 120, + Age: 2, + Fare: 31.275, + Name: 'Andersson, Miss. Ellis Anna Maria', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 2, + Ticket: 'S.O.C. 14879', + Parch: 0, + Cabin: null, + PassengerId: 121, + Age: 21, + Fare: 73.5, + Name: 'Hickman, Mr. Stanley George', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A4. 54510', + Parch: 0, + Cabin: null, + PassengerId: 122, + Age: null, + Fare: 8.05, + Name: 'Moore, Mr. Leonard Charles', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '237736', + Parch: 0, + Cabin: null, + PassengerId: 123, + Age: 32.5, + Fare: 30.0708, + Name: 'Nasser, Mr. Nicholas', + Survived: false, + Pclass: 2, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '27267', + Parch: 0, + Cabin: 'E101', + PassengerId: 124, + Age: 32.5, + Fare: 13, + Name: 'Webber, Miss. Susan', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '35281', + Parch: 1, + Cabin: 'D26', + PassengerId: 125, + Age: 54, + Fare: 77.2875, + Name: 'White, Mr. Percival Wayland', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2651', + Parch: 0, + Cabin: null, + PassengerId: 126, + Age: 12, + Fare: 11.2417, + Name: 'Nicola-Yarred, Master. Elias', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '370372', + Parch: 0, + Cabin: null, + PassengerId: 127, + Age: null, + Fare: 7.75, + Name: 'McMahon, Mr. Martin', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C 17369', + Parch: 0, + Cabin: null, + PassengerId: 128, + Age: 24, + Fare: 7.1417, + Name: 'Madsen, Mr. Fridtjof Arne', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2668', + Parch: 1, + Cabin: 'F E69', + PassengerId: 129, + Age: null, + Fare: 22.3583, + Name: 'Peter, Miss. Anna', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '347061', + Parch: 0, + Cabin: null, + PassengerId: 130, + Age: 45, + Fare: 6.975, + Name: 'Ekstrom, Mr. Johan', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349241', + Parch: 0, + Cabin: null, + PassengerId: 131, + Age: 33, + Fare: 7.8958, + Name: 'Drazenoic, Mr. Jozef', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/O.Q. 3101307', + Parch: 0, + Cabin: null, + PassengerId: 132, + Age: 20, + Fare: 7.05, + Name: 'Coelho, Mr. Domingos Fernandeo', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'A/5. 3337', + Parch: 0, + Cabin: null, + PassengerId: 133, + Age: 47, + Fare: 14.5, + Name: 'Robins, Mrs. Alexander A (Grace Charity Laury)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '228414', + Parch: 0, + Cabin: null, + PassengerId: 134, + Age: 29, + Fare: 26, + Name: 'Weisz, Mrs. Leopold (Mathilde Francoise Pede)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'C.A. 29178', + Parch: 0, + Cabin: null, + PassengerId: 135, + Age: 25, + Fare: 13, + Name: 'Sobey, Mr. Samuel James Hayden', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SC/PARIS 2133', + Parch: 0, + Cabin: null, + PassengerId: 136, + Age: 23, + Fare: 15.0458, + Name: 'Richard, Mr. Emile', + Survived: false, + Pclass: 2, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '11752', + Parch: 2, + Cabin: 'D47', + PassengerId: 137, + Age: 19, + Fare: 26.2833, + Name: 'Newsom, Miss. Helen Monypeny', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '113803', + Parch: 0, + Cabin: 'C123', + PassengerId: 138, + Age: 37, + Fare: 53.1, + Name: 'Futrelle, Mr. Jacques Heath', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '7534', + Parch: 0, + Cabin: null, + PassengerId: 139, + Age: 16, + Fare: 9.2167, + Name: 'Osen, Mr. Olaf Elon', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17593', + Parch: 0, + Cabin: 'B86', + PassengerId: 140, + Age: 24, + Fare: 79.2, + Name: 'Giglio, Mr. Victor', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2678', + Parch: 2, + Cabin: null, + PassengerId: 141, + Age: null, + Fare: 15.2458, + Name: 'Boulos, Mrs. Joseph (Sultana)', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '347081', + Parch: 0, + Cabin: null, + PassengerId: 142, + Age: 22, + Fare: 7.75, + Name: 'Nysten, Miss. Anna Sofia', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'STON/O2. 3101279', + Parch: 0, + Cabin: null, + PassengerId: 143, + Age: 24, + Fare: 15.85, + Name: 'Hakkarainen, Mrs. Pekka Pietari (Elin Matilda Dolck)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '365222', + Parch: 0, + Cabin: null, + PassengerId: 144, + Age: 19, + Fare: 6.75, + Name: 'Burke, Mr. Jeremiah', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '231945', + Parch: 0, + Cabin: null, + PassengerId: 145, + Age: 18, + Fare: 11.5, + Name: 'Andrew, Mr. Edgardo Samuel', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 33112', + Parch: 1, + Cabin: null, + PassengerId: 146, + Age: 19, + Fare: 36.75, + Name: 'Nicholls, Mr. Joseph Charles', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '350043', + Parch: 0, + Cabin: null, + PassengerId: 147, + Age: 27, + Fare: 7.7958, + Name: 'Andersson, Mr. August Edvard ("Wennerstrom")', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: 'W./C. 6608', + Parch: 2, + Cabin: null, + PassengerId: 148, + Age: 9, + Fare: 34.375, + Name: 'Ford, Miss. Robina Maggie "Ruby"', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '230080', + Parch: 2, + Cabin: 'F2', + PassengerId: 149, + Age: 36.5, + Fare: 26, + Name: 'Navratil, Mr. Michel ("Louis M Hoffman")', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '244310', + Parch: 0, + Cabin: null, + PassengerId: 150, + Age: 42, + Fare: 13, + Name: 'Byles, Rev. Thomas Roussel Davids', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'S.O.P. 1166', + Parch: 0, + Cabin: null, + PassengerId: 151, + Age: 51, + Fare: 12.525, + Name: 'Bateman, Rev. Robert James', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '113776', + Parch: 0, + Cabin: 'C2', + PassengerId: 152, + Age: 22, + Fare: 66.6, + Name: 'Pears, Mrs. Thomas (Edith Wearne)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'A.5. 11206', + Parch: 0, + Cabin: null, + PassengerId: 153, + Age: 55.5, + Fare: 8.05, + Name: 'Meo, Mr. Alfonzo', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/5. 851', + Parch: 2, + Cabin: null, + PassengerId: 154, + Age: 40.5, + Fare: 14.5, + Name: 'van Billiard, Mr. Austin Blyler', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'Fa 265302', + Parch: 0, + Cabin: null, + PassengerId: 155, + Age: null, + Fare: 7.3125, + Name: 'Olsen, Mr. Ole Martin', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17597', + Parch: 1, + Cabin: null, + PassengerId: 156, + Age: 51, + Fare: 61.3792, + Name: 'Williams, Mr. Charles Duane', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '35851', + Parch: 0, + Cabin: null, + PassengerId: 157, + Age: 16, + Fare: 7.7333, + Name: 'Gilnagh, Miss. Katherine "Katie"', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'SOTON/OQ 392090', + Parch: 0, + Cabin: null, + PassengerId: 158, + Age: 30, + Fare: 8.05, + Name: 'Corn, Mr. Harry', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '315037', + Parch: 0, + Cabin: null, + PassengerId: 159, + Age: null, + Fare: 8.6625, + Name: 'Smiljanic, Mr. Mile', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 8, + Ticket: 'CA. 2343', + Parch: 2, + Cabin: null, + PassengerId: 160, + Age: null, + Fare: 69.55, + Name: 'Sage, Master. Thomas Henry', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '371362', + Parch: 1, + Cabin: null, + PassengerId: 161, + Age: 44, + Fare: 16.1, + Name: 'Cribb, Mr. John Hatfield', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C.A. 33595', + Parch: 0, + Cabin: null, + PassengerId: 162, + Age: 40, + Fare: 15.75, + Name: 'Watt, Mrs. James (Elizabeth "Bessie" Inglis Milne)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '347068', + Parch: 0, + Cabin: null, + PassengerId: 163, + Age: 26, + Fare: 7.775, + Name: 'Bengtsson, Mr. John Viktor', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '315093', + Parch: 0, + Cabin: null, + PassengerId: 164, + Age: 17, + Fare: 8.6625, + Name: 'Calic, Mr. Jovo', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 4, + Ticket: '3101295', + Parch: 1, + Cabin: null, + PassengerId: 165, + Age: 1, + Fare: 39.6875, + Name: 'Panula, Master. Eino Viljami', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '363291', + Parch: 2, + Cabin: null, + PassengerId: 166, + Age: 9, + Fare: 20.525, + Name: 'Goldsmith, Master. Frank John William "Frankie"', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113505', + Parch: 1, + Cabin: 'E33', + PassengerId: 167, + Age: null, + Fare: 55, + Name: 'Chibnall, Mrs. (Edith Martha Bowerman)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '347088', + Parch: 4, + Cabin: null, + PassengerId: 168, + Age: 45, + Fare: 27.9, + Name: 'Skoog, Mrs. William (Anna Bernhardina Karlsson)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17318', + Parch: 0, + Cabin: null, + PassengerId: 169, + Age: null, + Fare: 25.925, + Name: 'Baumann, Mr. John D', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '1601', + Parch: 0, + Cabin: null, + PassengerId: 170, + Age: 28, + Fare: 56.4958, + Name: 'Ling, Mr. Lee', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '111240', + Parch: 0, + Cabin: 'B19', + PassengerId: 171, + Age: 61, + Fare: 33.5, + Name: 'Van der hoef, Mr. Wyckoff', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 4, + Ticket: '382652', + Parch: 1, + Cabin: null, + PassengerId: 172, + Age: 4, + Fare: 29.125, + Name: 'Rice, Master. Arthur', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '347742', + Parch: 1, + Cabin: null, + PassengerId: 173, + Age: 1, + Fare: 11.1333, + Name: 'Johnson, Miss. Eleanor Ileen', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'STON/O 2. 3101280', + Parch: 0, + Cabin: null, + PassengerId: 174, + Age: 21, + Fare: 7.925, + Name: 'Sivola, Mr. Antti Wilhelm', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '17764', + Parch: 0, + Cabin: 'A7', + PassengerId: 175, + Age: 56, + Fare: 30.6958, + Name: 'Smith, Mr. James Clinch', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '350404', + Parch: 1, + Cabin: null, + PassengerId: 176, + Age: 18, + Fare: 7.8542, + Name: 'Klasen, Mr. Klas Albin', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '4133', + Parch: 1, + Cabin: null, + PassengerId: 177, + Age: null, + Fare: 25.4667, + Name: 'Lefebre, Master. Henry Forbes', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17595', + Parch: 0, + Cabin: 'C49', + PassengerId: 178, + Age: 50, + Fare: 28.7125, + Name: 'Isham, Miss. Ann Elizabeth', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '250653', + Parch: 0, + Cabin: null, + PassengerId: 179, + Age: 30, + Fare: 13, + Name: 'Hale, Mr. Reginald', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'LINE', + Parch: 0, + Cabin: null, + PassengerId: 180, + Age: 36, + Fare: 0, + Name: 'Leonard, Mr. Lionel', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 8, + Ticket: 'CA. 2343', + Parch: 2, + Cabin: null, + PassengerId: 181, + Age: null, + Fare: 69.55, + Name: 'Sage, Miss. Constance Gladys', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'SC/PARIS 2131', + Parch: 0, + Cabin: null, + PassengerId: 182, + Age: null, + Fare: 15.05, + Name: 'Pernot, Mr. Rene', + Survived: false, + Pclass: 2, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 4, + Ticket: '347077', + Parch: 2, + Cabin: null, + PassengerId: 183, + Age: 9, + Fare: 31.3875, + Name: 'Asplund, Master. Clarence Gustaf Hugo', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '230136', + Parch: 1, + Cabin: 'F4', + PassengerId: 184, + Age: 1, + Fare: 39, + Name: 'Becker, Master. Richard F', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '315153', + Parch: 2, + Cabin: null, + PassengerId: 185, + Age: 4, + Fare: 22.025, + Name: 'Kink-Heilmann, Miss. Luise Gretchen', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113767', + Parch: 0, + Cabin: 'A32', + PassengerId: 186, + Age: null, + Fare: 50, + Name: 'Rood, Mr. Hugh Roscoe', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '370365', + Parch: 0, + Cabin: null, + PassengerId: 187, + Age: null, + Fare: 15.5, + Name: 'O\'Brien, Mrs. Thomas (Johanna "Hannah" Godfrey)', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '111428', + Parch: 0, + Cabin: null, + PassengerId: 188, + Age: 45, + Fare: 26.55, + Name: 'Romaine, Mr. Charles Hallace ("Mr C Rolmane")', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '364849', + Parch: 1, + Cabin: null, + PassengerId: 189, + Age: 40, + Fare: 15.5, + Name: 'Bourke, Mr. John', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349247', + Parch: 0, + Cabin: null, + PassengerId: 190, + Age: 36, + Fare: 7.8958, + Name: 'Turcin, Mr. Stjepan', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '234604', + Parch: 0, + Cabin: null, + PassengerId: 191, + Age: 32, + Fare: 13, + Name: 'Pinsky, Mrs. (Rosa)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '28424', + Parch: 0, + Cabin: null, + PassengerId: 192, + Age: 19, + Fare: 13, + Name: 'Carbines, Mr. William', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '350046', + Parch: 0, + Cabin: null, + PassengerId: 193, + Age: 19, + Fare: 7.8542, + Name: 'Andersen-Jensen, Miss. Carla Christine Nielsine', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '230080', + Parch: 1, + Cabin: 'F2', + PassengerId: 194, + Age: 3, + Fare: 26, + Name: 'Navratil, Master. Michel M', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17610', + Parch: 0, + Cabin: 'B4', + PassengerId: 195, + Age: 44, + Fare: 27.7208, + Name: 'Brown, Mrs. James Joseph (Margaret Tobin)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17569', + Parch: 0, + Cabin: 'B80', + PassengerId: 196, + Age: 58, + Fare: 146.5208, + Name: 'Lurette, Miss. Elise', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '368703', + Parch: 0, + Cabin: null, + PassengerId: 197, + Age: null, + Fare: 7.75, + Name: 'Mernagh, Mr. Robert', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '4579', + Parch: 1, + Cabin: null, + PassengerId: 198, + Age: 42, + Fare: 8.4042, + Name: 'Olsen, Mr. Karl Siegwart Andreas', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '370370', + Parch: 0, + Cabin: null, + PassengerId: 199, + Age: null, + Fare: 7.75, + Name: 'Madigan, Miss. Margaret "Maggie"', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '248747', + Parch: 0, + Cabin: null, + PassengerId: 200, + Age: 24, + Fare: 13, + Name: 'Yrois, Miss. Henriette ("Mrs Harbeck")', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '345770', + Parch: 0, + Cabin: null, + PassengerId: 201, + Age: 28, + Fare: 9.5, + Name: 'Vande Walle, Mr. Nestor Cyriel', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 8, + Ticket: 'CA. 2343', + Parch: 2, + Cabin: null, + PassengerId: 202, + Age: null, + Fare: 69.55, + Name: 'Sage, Mr. Frederick', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '3101264', + Parch: 0, + Cabin: null, + PassengerId: 203, + Age: 34, + Fare: 6.4958, + Name: 'Johanson, Mr. Jakob Alfred', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2628', + Parch: 0, + Cabin: null, + PassengerId: 204, + Age: 45.5, + Fare: 7.225, + Name: 'Youseff, Mr. Gerious', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/5 3540', + Parch: 0, + Cabin: null, + PassengerId: 205, + Age: 18, + Fare: 8.05, + Name: 'Cohen, Mr. Gurshon "Gus"', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347054', + Parch: 1, + Cabin: 'G6', + PassengerId: 206, + Age: 2, + Fare: 10.4625, + Name: 'Strom, Miss. Telma Matilda', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '3101278', + Parch: 0, + Cabin: null, + PassengerId: 207, + Age: 32, + Fare: 15.85, + Name: 'Backstrom, Mr. Karl Alfred', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2699', + Parch: 0, + Cabin: null, + PassengerId: 208, + Age: 26, + Fare: 18.7875, + Name: 'Albimona, Mr. Nassef Cassem', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '367231', + Parch: 0, + Cabin: null, + PassengerId: 209, + Age: 16, + Fare: 7.75, + Name: 'Carr, Miss. Helen "Ellen"', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '112277', + Parch: 0, + Cabin: 'A31', + PassengerId: 210, + Age: 40, + Fare: 31, + Name: 'Blank, Mr. Henry', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/O.Q. 3101311', + Parch: 0, + Cabin: null, + PassengerId: 211, + Age: 24, + Fare: 7.05, + Name: 'Ali, Mr. Ahmed', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'F.C.C. 13528', + Parch: 0, + Cabin: null, + PassengerId: 212, + Age: 35, + Fare: 21, + Name: 'Cameron, Miss. Clear Annie', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'A/5 21174', + Parch: 0, + Cabin: null, + PassengerId: 213, + Age: 22, + Fare: 7.25, + Name: 'Perkin, Mr. John Henry', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '250646', + Parch: 0, + Cabin: null, + PassengerId: 214, + Age: 30, + Fare: 13, + Name: 'Givard, Mr. Hans Kristensen', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '367229', + Parch: 0, + Cabin: null, + PassengerId: 215, + Age: null, + Fare: 7.75, + Name: 'Kiernan, Mr. Philip', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '35273', + Parch: 0, + Cabin: 'D36', + PassengerId: 216, + Age: 31, + Fare: 113.275, + Name: 'Newell, Miss. Madeleine', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'STON/O2. 3101283', + Parch: 0, + Cabin: null, + PassengerId: 217, + Age: 27, + Fare: 7.925, + Name: 'Honkanen, Miss. Eliina', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '243847', + Parch: 0, + Cabin: null, + PassengerId: 218, + Age: 42, + Fare: 27, + Name: 'Jacobsohn, Mr. Sidney Samuel', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '11813', + Parch: 0, + Cabin: 'D15', + PassengerId: 219, + Age: 32, + Fare: 76.2917, + Name: 'Bazzani, Miss. Albina', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'W/C 14208', + Parch: 0, + Cabin: null, + PassengerId: 220, + Age: 30, + Fare: 10.5, + Name: 'Harris, Mr. Walter', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/OQ 392089', + Parch: 0, + Cabin: null, + PassengerId: 221, + Age: 16, + Fare: 8.05, + Name: 'Sunderland, Mr. Victor Francis', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '220367', + Parch: 0, + Cabin: null, + PassengerId: 222, + Age: 27, + Fare: 13, + Name: 'Bracken, Mr. James H', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '21440', + Parch: 0, + Cabin: null, + PassengerId: 223, + Age: 51, + Fare: 8.05, + Name: 'Green, Mr. George Henry', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349234', + Parch: 0, + Cabin: null, + PassengerId: 224, + Age: null, + Fare: 7.8958, + Name: 'Nenkoff, Mr. Christo', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '19943', + Parch: 0, + Cabin: 'C93', + PassengerId: 225, + Age: 38, + Fare: 90, + Name: 'Hoyt, Mr. Frederick Maxfield', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PP 4348', + Parch: 0, + Cabin: null, + PassengerId: 226, + Age: 22, + Fare: 9.35, + Name: 'Berglund, Mr. Karl Ivar Sven', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SW/PP 751', + Parch: 0, + Cabin: null, + PassengerId: 227, + Age: 19, + Fare: 10.5, + Name: 'Mellors, Mr. William John', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/5 21173', + Parch: 0, + Cabin: null, + PassengerId: 228, + Age: 20.5, + Fare: 7.25, + Name: 'Lovell, Mr. John Hall ("Henry")', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '236171', + Parch: 0, + Cabin: null, + PassengerId: 229, + Age: 18, + Fare: 13, + Name: 'Fahlstrom, Mr. Arne Jonas', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '4133', + Parch: 1, + Cabin: null, + PassengerId: 230, + Age: null, + Fare: 25.4667, + Name: 'Lefebre, Miss. Mathilde', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '36973', + Parch: 0, + Cabin: 'C83', + PassengerId: 231, + Age: 35, + Fare: 83.475, + Name: 'Harris, Mrs. Henry Birkhardt (Irene Wallach)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '347067', + Parch: 0, + Cabin: null, + PassengerId: 232, + Age: 29, + Fare: 7.775, + Name: 'Larsson, Mr. Bengt Edvin', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '237442', + Parch: 0, + Cabin: null, + PassengerId: 233, + Age: 59, + Fare: 13.5, + Name: 'Sjostedt, Mr. Ernst Adolf', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 4, + Ticket: '347077', + Parch: 2, + Cabin: null, + PassengerId: 234, + Age: 5, + Fare: 31.3875, + Name: 'Asplund, Miss. Lillian Gertrud', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'C.A. 29566', + Parch: 0, + Cabin: null, + PassengerId: 235, + Age: 24, + Fare: 10.5, + Name: 'Leyson, Mr. Robert William Norman', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'W./C. 6609', + Parch: 0, + Cabin: null, + PassengerId: 236, + Age: null, + Fare: 7.55, + Name: 'Harknett, Miss. Alice Phoebe', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '26707', + Parch: 0, + Cabin: null, + PassengerId: 237, + Age: 44, + Fare: 26, + Name: 'Hold, Mr. Stephen', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C.A. 31921', + Parch: 2, + Cabin: null, + PassengerId: 238, + Age: 8, + Fare: 26.25, + Name: 'Collyer, Miss. Marjorie "Lottie"', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '28665', + Parch: 0, + Cabin: null, + PassengerId: 239, + Age: 19, + Fare: 10.5, + Name: 'Pengelly, Mr. Frederick William', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SCO/W 1585', + Parch: 0, + Cabin: null, + PassengerId: 240, + Age: 33, + Fare: 12.275, + Name: 'Hunt, Mr. George Henry', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2665', + Parch: 0, + Cabin: null, + PassengerId: 241, + Age: null, + Fare: 14.4542, + Name: 'Zabour, Miss. Thamine', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '367230', + Parch: 0, + Cabin: null, + PassengerId: 242, + Age: null, + Fare: 15.5, + Name: 'Murphy, Miss. Katherine "Kate"', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'W./C. 14263', + Parch: 0, + Cabin: null, + PassengerId: 243, + Age: 29, + Fare: 10.5, + Name: 'Coleridge, Mr. Reginald Charles', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'STON/O 2. 3101275', + Parch: 0, + Cabin: null, + PassengerId: 244, + Age: 22, + Fare: 7.125, + Name: 'Maenpaa, Mr. Matti Alexanteri', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2694', + Parch: 0, + Cabin: null, + PassengerId: 245, + Age: 30, + Fare: 7.225, + Name: 'Attalah, Mr. Sleiman', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '19928', + Parch: 0, + Cabin: 'C78', + PassengerId: 246, + Age: 44, + Fare: 90, + Name: 'Minahan, Dr. William Edward', + Survived: false, + Pclass: 1, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347071', + Parch: 0, + Cabin: null, + PassengerId: 247, + Age: 25, + Fare: 7.775, + Name: 'Lindahl, Miss. Agda Thorilda Viktoria', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '250649', + Parch: 2, + Cabin: null, + PassengerId: 248, + Age: 24, + Fare: 14.5, + Name: 'Hamalainen, Mrs. William (Anna)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '11751', + Parch: 1, + Cabin: 'D35', + PassengerId: 249, + Age: 37, + Fare: 52.5542, + Name: 'Beckwith, Mr. Richard Leonard', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '244252', + Parch: 0, + Cabin: null, + PassengerId: 250, + Age: 54, + Fare: 26, + Name: 'Carter, Rev. Ernest Courtenay', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '362316', + Parch: 0, + Cabin: null, + PassengerId: 251, + Age: null, + Fare: 7.25, + Name: 'Reed, Mr. James George', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '347054', + Parch: 1, + Cabin: 'G6', + PassengerId: 252, + Age: 29, + Fare: 10.4625, + Name: 'Strom, Mrs. Wilhelm (Elna Matilda Persson)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113514', + Parch: 0, + Cabin: 'C87', + PassengerId: 253, + Age: 62, + Fare: 26.55, + Name: 'Stead, Mr. William Thomas', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'A/5. 3336', + Parch: 0, + Cabin: null, + PassengerId: 254, + Age: 30, + Fare: 16.1, + Name: 'Lobb, Mr. William Arthur', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '370129', + Parch: 2, + Cabin: null, + PassengerId: 255, + Age: 41, + Fare: 20.2125, + Name: 'Rosblom, Mrs. Viktor (Helena Wilhelmina)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2650', + Parch: 2, + Cabin: null, + PassengerId: 256, + Age: 29, + Fare: 15.2458, + Name: 'Touma, Mrs. Darwis (Hanne Youssef Razi)', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17585', + Parch: 0, + Cabin: null, + PassengerId: 257, + Age: null, + Fare: 79.2, + Name: 'Thorne, Mrs. Gertrude Maybelle', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '110152', + Parch: 0, + Cabin: 'B77', + PassengerId: 258, + Age: 30, + Fare: 86.5, + Name: 'Cherry, Miss. Gladys', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17755', + Parch: 0, + Cabin: null, + PassengerId: 259, + Age: 35, + Fare: 512.3292, + Name: 'Ward, Miss. Anna', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '230433', + Parch: 1, + Cabin: null, + PassengerId: 260, + Age: 50, + Fare: 26, + Name: 'Parrish, Mrs. (Lutie Davis)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '384461', + Parch: 0, + Cabin: null, + PassengerId: 261, + Age: null, + Fare: 7.75, + Name: 'Smith, Mr. Thomas', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 4, + Ticket: '347077', + Parch: 2, + Cabin: null, + PassengerId: 262, + Age: 3, + Fare: 31.3875, + Name: 'Asplund, Master. Edvin Rojj Felix', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '110413', + Parch: 1, + Cabin: 'E67', + PassengerId: 263, + Age: 52, + Fare: 79.65, + Name: 'Taussig, Mr. Emil', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '112059', + Parch: 0, + Cabin: 'B94', + PassengerId: 264, + Age: 40, + Fare: 0, + Name: 'Harrison, Mr. William', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '382649', + Parch: 0, + Cabin: null, + PassengerId: 265, + Age: null, + Fare: 7.75, + Name: 'Henry, Miss. Delia', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'C.A. 17248', + Parch: 0, + Cabin: null, + PassengerId: 266, + Age: 36, + Fare: 10.5, + Name: 'Reeves, Mr. David', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 4, + Ticket: '3101295', + Parch: 1, + Cabin: null, + PassengerId: 267, + Age: 16, + Fare: 39.6875, + Name: 'Panula, Mr. Ernesti Arvid', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '347083', + Parch: 0, + Cabin: null, + PassengerId: 268, + Age: 25, + Fare: 7.775, + Name: 'Persson, Mr. Ernst Ulrik', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17582', + Parch: 1, + Cabin: 'C125', + PassengerId: 269, + Age: 58, + Fare: 153.4625, + Name: 'Graham, Mrs. William Thompson (Edith Junkins)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17760', + Parch: 0, + Cabin: 'C99', + PassengerId: 270, + Age: 35, + Fare: 135.6333, + Name: 'Bissette, Miss. Amelia', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113798', + Parch: 0, + Cabin: null, + PassengerId: 271, + Age: null, + Fare: 31, + Name: 'Cairns, Mr. Alexander', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'LINE', + Parch: 0, + Cabin: null, + PassengerId: 272, + Age: 25, + Fare: 0, + Name: 'Tornquist, Mr. William Henry', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '250644', + Parch: 1, + Cabin: null, + PassengerId: 273, + Age: 41, + Fare: 19.5, + Name: 'Mellinger, Mrs. (Elizabeth Anne Maidment)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17596', + Parch: 1, + Cabin: 'C118', + PassengerId: 274, + Age: 37, + Fare: 29.7, + Name: 'Natsch, Mr. Charles H', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '370375', + Parch: 0, + Cabin: null, + PassengerId: 275, + Age: null, + Fare: 7.75, + Name: 'Healy, Miss. Hanora "Nora"', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '13502', + Parch: 0, + Cabin: 'D7', + PassengerId: 276, + Age: 63, + Fare: 77.9583, + Name: 'Andrews, Miss. Kornelia Theodosia', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '347073', + Parch: 0, + Cabin: null, + PassengerId: 277, + Age: 45, + Fare: 7.75, + Name: 'Lindblom, Miss. Augusta Charlotta', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '239853', + Parch: 0, + Cabin: null, + PassengerId: 278, + Age: null, + Fare: 0, + Name: 'Parkes, Mr. Francis "Frank"', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 4, + Ticket: '382652', + Parch: 1, + Cabin: null, + PassengerId: 279, + Age: 7, + Fare: 29.125, + Name: 'Rice, Master. Eric', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 2673', + Parch: 1, + Cabin: null, + PassengerId: 280, + Age: 35, + Fare: 20.25, + Name: 'Abbott, Mrs. Stanton (Rosa Hunt)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '336439', + Parch: 0, + Cabin: null, + PassengerId: 281, + Age: 65, + Fare: 7.75, + Name: 'Duane, Mr. Frank', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347464', + Parch: 0, + Cabin: null, + PassengerId: 282, + Age: 28, + Fare: 7.8542, + Name: 'Olsson, Mr. Nils Johan Goransson', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '345778', + Parch: 0, + Cabin: null, + PassengerId: 283, + Age: 16, + Fare: 9.5, + Name: 'de Pelsmaeker, Mr. Alfons', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/5. 10482', + Parch: 0, + Cabin: null, + PassengerId: 284, + Age: 19, + Fare: 8.05, + Name: 'Dorking, Mr. Edward Arthur', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113056', + Parch: 0, + Cabin: 'A19', + PassengerId: 285, + Age: null, + Fare: 26, + Name: 'Smith, Mr. Richard William', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349239', + Parch: 0, + Cabin: null, + PassengerId: 286, + Age: 33, + Fare: 8.6625, + Name: 'Stankovic, Mr. Ivan', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '345774', + Parch: 0, + Cabin: null, + PassengerId: 287, + Age: 30, + Fare: 9.5, + Name: 'de Mulder, Mr. Theodore', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349206', + Parch: 0, + Cabin: null, + PassengerId: 288, + Age: 22, + Fare: 7.8958, + Name: 'Naidenoff, Mr. Penko', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '237798', + Parch: 0, + Cabin: null, + PassengerId: 289, + Age: 42, + Fare: 13, + Name: 'Hosono, Mr. Masabumi', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '370373', + Parch: 0, + Cabin: null, + PassengerId: 290, + Age: 22, + Fare: 7.75, + Name: 'Connolly, Miss. Kate', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '19877', + Parch: 0, + Cabin: null, + PassengerId: 291, + Age: 26, + Fare: 78.85, + Name: 'Barber, Miss. Ellen "Nellie"', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '11967', + Parch: 0, + Cabin: 'B49', + PassengerId: 292, + Age: 19, + Fare: 91.0792, + Name: 'Bishop, Mrs. Dickinson H (Helen Walton)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'SC/Paris 2163', + Parch: 0, + Cabin: 'D', + PassengerId: 293, + Age: 36, + Fare: 12.875, + Name: 'Levy, Mr. Rene Jacques', + Survived: false, + Pclass: 2, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349236', + Parch: 0, + Cabin: null, + PassengerId: 294, + Age: 24, + Fare: 8.85, + Name: 'Haas, Miss. Aloisia', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349233', + Parch: 0, + Cabin: null, + PassengerId: 295, + Age: 24, + Fare: 7.8958, + Name: 'Mineff, Mr. Ivan', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17612', + Parch: 0, + Cabin: null, + PassengerId: 296, + Age: null, + Fare: 27.7208, + Name: 'Lewy, Mr. Ervin G', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2693', + Parch: 0, + Cabin: null, + PassengerId: 297, + Age: 23.5, + Fare: 7.2292, + Name: 'Hanna, Mr. Mansour', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '113781', + Parch: 2, + Cabin: 'C22 C26', + PassengerId: 298, + Age: 2, + Fare: 151.55, + Name: 'Allison, Miss. Helen Loraine', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '19988', + Parch: 0, + Cabin: 'C106', + PassengerId: 299, + Age: null, + Fare: 30.5, + Name: 'Saalfeld, Mr. Adolphe', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17558', + Parch: 1, + Cabin: 'B58 B60', + PassengerId: 300, + Age: 50, + Fare: 247.5208, + Name: 'Baxter, Mrs. James (Helene DeLaudeniere Chaput)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '9234', + Parch: 0, + Cabin: null, + PassengerId: 301, + Age: null, + Fare: 7.75, + Name: 'Kelly, Miss. Anna Katherine "Annie Kate"', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 2, + Ticket: '367226', + Parch: 0, + Cabin: null, + PassengerId: 302, + Age: null, + Fare: 23.25, + Name: 'McCoy, Mr. Bernard', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'LINE', + Parch: 0, + Cabin: null, + PassengerId: 303, + Age: 19, + Fare: 0, + Name: 'Johnson, Mr. William Cahoone Jr', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '226593', + Parch: 0, + Cabin: 'E101', + PassengerId: 304, + Age: null, + Fare: 12.35, + Name: 'Keane, Miss. Nora A', + Survived: true, + Pclass: 2, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'A/5 2466', + Parch: 0, + Cabin: null, + PassengerId: 305, + Age: null, + Fare: 8.05, + Name: 'Williams, Mr. Howard Hugh "Harry"', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '113781', + Parch: 2, + Cabin: 'C22 C26', + PassengerId: 306, + Age: 0.92, + Fare: 151.55, + Name: 'Allison, Master. Hudson Trevor', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '17421', + Parch: 0, + Cabin: null, + PassengerId: 307, + Age: null, + Fare: 110.8833, + Name: 'Fleming, Miss. Margaret', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'PC 17758', + Parch: 0, + Cabin: 'C65', + PassengerId: 308, + Age: 17, + Fare: 108.9, + Name: 'Penasco y Castellana, Mrs. Victor de Satode (Maria Josefa Perez de Soto y Vallejo)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'P/PP 3381', + Parch: 0, + Cabin: null, + PassengerId: 309, + Age: 30, + Fare: 24, + Name: 'Abelson, Mr. Samuel', + Survived: false, + Pclass: 2, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17485', + Parch: 0, + Cabin: 'E36', + PassengerId: 310, + Age: 30, + Fare: 56.9292, + Name: 'Francatelli, Miss. Laura Mabel', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '11767', + Parch: 0, + Cabin: 'C54', + PassengerId: 311, + Age: 24, + Fare: 83.1583, + Name: 'Hays, Miss. Margaret Bechstein', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 2, + Ticket: 'PC 17608', + Parch: 2, + Cabin: 'B57 B59 B63 B66', + PassengerId: 312, + Age: 18, + Fare: 262.375, + Name: 'Ryerson, Miss. Emily Borie', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '250651', + Parch: 1, + Cabin: null, + PassengerId: 313, + Age: 26, + Fare: 26, + Name: 'Lahtinen, Mrs. William (Anna Sylfven)', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349243', + Parch: 0, + Cabin: null, + PassengerId: 314, + Age: 28, + Fare: 7.8958, + Name: 'Hendekovic, Mr. Ignjac', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'F.C.C. 13529', + Parch: 1, + Cabin: null, + PassengerId: 315, + Age: 43, + Fare: 26.25, + Name: 'Hart, Mr. Benjamin', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347470', + Parch: 0, + Cabin: null, + PassengerId: 316, + Age: 26, + Fare: 7.8542, + Name: 'Nilsson, Miss. Helmina Josefina', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '244367', + Parch: 0, + Cabin: null, + PassengerId: 317, + Age: 24, + Fare: 26, + Name: 'Kantor, Mrs. Sinai (Miriam Sternin)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '29011', + Parch: 0, + Cabin: null, + PassengerId: 318, + Age: 54, + Fare: 14, + Name: 'Moraweck, Dr. Ernest', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '36928', + Parch: 2, + Cabin: 'C7', + PassengerId: 319, + Age: 31, + Fare: 164.8667, + Name: 'Wick, Miss. Mary Natalie', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '16966', + Parch: 1, + Cabin: 'E34', + PassengerId: 320, + Age: 40, + Fare: 134.5, + Name: 'Spedden, Mrs. Frederic Oakley (Margaretta Corning Stone)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'A/5 21172', + Parch: 0, + Cabin: null, + PassengerId: 321, + Age: 22, + Fare: 7.25, + Name: 'Dennis, Mr. Samuel', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349219', + Parch: 0, + Cabin: null, + PassengerId: 322, + Age: 27, + Fare: 7.8958, + Name: 'Danoff, Mr. Yoto', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '234818', + Parch: 0, + Cabin: null, + PassengerId: 323, + Age: 30, + Fare: 12.35, + Name: 'Slayter, Miss. Hilda Mary', + Survived: true, + Pclass: 2, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '248738', + Parch: 1, + Cabin: null, + PassengerId: 324, + Age: 22, + Fare: 29, + Name: 'Caldwell, Mrs. Albert Francis (Sylvia Mae Harbaugh)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 8, + Ticket: 'CA. 2343', + Parch: 2, + Cabin: null, + PassengerId: 325, + Age: null, + Fare: 69.55, + Name: 'Sage, Mr. George John Jr', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17760', + Parch: 0, + Cabin: 'C32', + PassengerId: 326, + Age: 36, + Fare: 135.6333, + Name: 'Young, Miss. Marie Grice', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '345364', + Parch: 0, + Cabin: null, + PassengerId: 327, + Age: 61, + Fare: 6.2375, + Name: 'Nysveen, Mr. Johan Hansen', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '28551', + Parch: 0, + Cabin: 'D', + PassengerId: 328, + Age: 36, + Fare: 13, + Name: 'Ball, Mrs. (Ada E Hall)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '363291', + Parch: 1, + Cabin: null, + PassengerId: 329, + Age: 31, + Fare: 20.525, + Name: 'Goldsmith, Mrs. Frank John (Emily Alice Brown)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '111361', + Parch: 1, + Cabin: 'B18', + PassengerId: 330, + Age: 16, + Fare: 57.9792, + Name: 'Hippach, Miss. Jean Gertrude', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 2, + Ticket: '367226', + Parch: 0, + Cabin: null, + PassengerId: 331, + Age: null, + Fare: 23.25, + Name: 'McCoy, Miss. Agnes', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113043', + Parch: 0, + Cabin: 'C124', + PassengerId: 332, + Age: 45.5, + Fare: 28.5, + Name: 'Partner, Mr. Austen', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17582', + Parch: 1, + Cabin: 'C91', + PassengerId: 333, + Age: 38, + Fare: 153.4625, + Name: 'Graham, Mr. George Edward', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '345764', + Parch: 0, + Cabin: null, + PassengerId: 334, + Age: 16, + Fare: 18, + Name: 'Vander Planke, Mr. Leo Edmondus', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'PC 17611', + Parch: 0, + Cabin: null, + PassengerId: 335, + Age: null, + Fare: 133.65, + Name: 'Frauenthal, Mrs. Henry William (Clara Heinsheimer)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349225', + Parch: 0, + Cabin: null, + PassengerId: 336, + Age: null, + Fare: 7.8958, + Name: 'Denkoff, Mr. Mitto', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '113776', + Parch: 0, + Cabin: 'C2', + PassengerId: 337, + Age: 29, + Fare: 66.6, + Name: 'Pears, Mr. Thomas Clinton', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '16966', + Parch: 0, + Cabin: 'E40', + PassengerId: 338, + Age: 41, + Fare: 134.5, + Name: 'Burns, Miss. Elizabeth Margaret', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '7598', + Parch: 0, + Cabin: null, + PassengerId: 339, + Age: 45, + Fare: 8.05, + Name: 'Dahl, Mr. Karl Edwart', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113784', + Parch: 0, + Cabin: 'T', + PassengerId: 340, + Age: 45, + Fare: 35.5, + Name: 'Blackwell, Mr. Stephen Weart', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '230080', + Parch: 1, + Cabin: 'F2', + PassengerId: 341, + Age: 2, + Fare: 26, + Name: 'Navratil, Master. Edmond Roger', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '19950', + Parch: 2, + Cabin: 'C23 C25 C27', + PassengerId: 342, + Age: 24, + Fare: 263, + Name: 'Fortune, Miss. Alice Elizabeth', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '248740', + Parch: 0, + Cabin: null, + PassengerId: 343, + Age: 28, + Fare: 13, + Name: 'Collander, Mr. Erik Gustaf', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '244361', + Parch: 0, + Cabin: null, + PassengerId: 344, + Age: 25, + Fare: 13, + Name: 'Sedgwick, Mr. Charles Frederick Waddington', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '229236', + Parch: 0, + Cabin: null, + PassengerId: 345, + Age: 36, + Fare: 13, + Name: 'Fox, Mr. Stanley Hubert', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '248733', + Parch: 0, + Cabin: 'F33', + PassengerId: 346, + Age: 24, + Fare: 13, + Name: 'Brown, Miss. Amelia "Mildred"', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '31418', + Parch: 0, + Cabin: null, + PassengerId: 347, + Age: 40, + Fare: 13, + Name: 'Smith, Miss. Marion Elsie', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '386525', + Parch: 0, + Cabin: null, + PassengerId: 348, + Age: null, + Fare: 16.1, + Name: 'Davison, Mrs. Thomas Henry (Mary E Finck)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'C.A. 37671', + Parch: 1, + Cabin: null, + PassengerId: 349, + Age: 3, + Fare: 15.9, + Name: 'Coutts, Master. William Loch "William"', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '315088', + Parch: 0, + Cabin: null, + PassengerId: 350, + Age: 42, + Fare: 8.6625, + Name: 'Dimic, Mr. Jovan', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '7267', + Parch: 0, + Cabin: null, + PassengerId: 351, + Age: 23, + Fare: 9.225, + Name: 'Odahl, Mr. Nils Martin', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113510', + Parch: 0, + Cabin: 'C128', + PassengerId: 352, + Age: null, + Fare: 35, + Name: 'Williams-Lambert, Mr. Fletcher Fellows', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2695', + Parch: 1, + Cabin: null, + PassengerId: 353, + Age: 15, + Fare: 7.2292, + Name: 'Elias, Mr. Tannous', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '349237', + Parch: 0, + Cabin: null, + PassengerId: 354, + Age: 25, + Fare: 17.8, + Name: 'Arnold-Franchi, Mr. Josef', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2647', + Parch: 0, + Cabin: null, + PassengerId: 355, + Age: null, + Fare: 7.225, + Name: 'Yousif, Mr. Wazli', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '345783', + Parch: 0, + Cabin: null, + PassengerId: 356, + Age: 28, + Fare: 9.5, + Name: 'Vanden Steen, Mr. Leo Peter', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113505', + Parch: 1, + Cabin: 'E33', + PassengerId: 357, + Age: 22, + Fare: 55, + Name: 'Bowerman, Miss. Elsie Edith', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '237671', + Parch: 0, + Cabin: null, + PassengerId: 358, + Age: 38, + Fare: 13, + Name: 'Funk, Miss. Annie Clemmer', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '330931', + Parch: 0, + Cabin: null, + PassengerId: 359, + Age: null, + Fare: 7.8792, + Name: 'McGovern, Miss. Mary', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '330980', + Parch: 0, + Cabin: null, + PassengerId: 360, + Age: null, + Fare: 7.8792, + Name: 'Mockler, Miss. Helen Mary "Ellie"', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '347088', + Parch: 4, + Cabin: null, + PassengerId: 361, + Age: 40, + Fare: 27.9, + Name: 'Skoog, Mr. Wilhelm', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'SC/PARIS 2167', + Parch: 0, + Cabin: null, + PassengerId: 362, + Age: 29, + Fare: 27.7208, + Name: 'del Carlo, Mr. Sebastiano', + Survived: false, + Pclass: 2, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2691', + Parch: 1, + Cabin: null, + PassengerId: 363, + Age: 45, + Fare: 14.4542, + Name: 'Barbara, Mrs. (Catherine David)', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'SOTON/O.Q. 3101310', + Parch: 0, + Cabin: null, + PassengerId: 364, + Age: 35, + Fare: 7.05, + Name: 'Asim, Mr. Adola', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '370365', + Parch: 0, + Cabin: null, + PassengerId: 365, + Age: null, + Fare: 15.5, + Name: "O'Brien, Mr. Thomas", + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C 7076', + Parch: 0, + Cabin: null, + PassengerId: 366, + Age: 30, + Fare: 7.25, + Name: 'Adahl, Mr. Mauritz Nils Martin', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '110813', + Parch: 0, + Cabin: 'D37', + PassengerId: 367, + Age: 60, + Fare: 75.25, + Name: 'Warren, Mrs. Frank Manley (Anna Sophia Atkinson)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2626', + Parch: 0, + Cabin: null, + PassengerId: 368, + Age: null, + Fare: 7.2292, + Name: 'Moussa, Mrs. (Mantoura Boulos)', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '14313', + Parch: 0, + Cabin: null, + PassengerId: 369, + Age: null, + Fare: 7.75, + Name: 'Jermyn, Miss. Annie', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17477', + Parch: 0, + Cabin: 'B35', + PassengerId: 370, + Age: 24, + Fare: 69.3, + Name: 'Aubart, Mme. Leontine Pauline', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '11765', + Parch: 0, + Cabin: 'E50', + PassengerId: 371, + Age: 25, + Fare: 55.4417, + Name: 'Harder, Mr. George Achilles', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '3101267', + Parch: 0, + Cabin: null, + PassengerId: 372, + Age: 18, + Fare: 6.4958, + Name: 'Wiklund, Mr. Jakob Alfred', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '323951', + Parch: 0, + Cabin: null, + PassengerId: 373, + Age: 19, + Fare: 8.05, + Name: 'Beavan, Mr. William Thomas', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17760', + Parch: 0, + Cabin: null, + PassengerId: 374, + Age: 22, + Fare: 135.6333, + Name: 'Ringhini, Mr. Sante', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '349909', + Parch: 1, + Cabin: null, + PassengerId: 375, + Age: 3, + Fare: 21.075, + Name: 'Palsson, Miss. Stina Viola', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'PC 17604', + Parch: 0, + Cabin: null, + PassengerId: 376, + Age: null, + Fare: 82.1708, + Name: 'Meyer, Mrs. Edgar Joseph (Leila Saks)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'C 7077', + Parch: 0, + Cabin: null, + PassengerId: 377, + Age: 22, + Fare: 7.25, + Name: 'Landergren, Miss. Aurora Adelia', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113503', + Parch: 2, + Cabin: 'C82', + PassengerId: 378, + Age: 27, + Fare: 211.5, + Name: 'Widener, Mr. Harry Elkins', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2648', + Parch: 0, + Cabin: null, + PassengerId: 379, + Age: 20, + Fare: 4.0125, + Name: 'Betros, Mr. Tannous', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347069', + Parch: 0, + Cabin: null, + PassengerId: 380, + Age: 19, + Fare: 7.775, + Name: 'Gustafsson, Mr. Karl Gideon', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17757', + Parch: 0, + Cabin: null, + PassengerId: 381, + Age: 42, + Fare: 227.525, + Name: 'Bidois, Miss. Rosalie', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2653', + Parch: 2, + Cabin: null, + PassengerId: 382, + Age: 1, + Fare: 15.7417, + Name: 'Nakid, Miss. Maria ("Mary")', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'STON/O 2. 3101293', + Parch: 0, + Cabin: null, + PassengerId: 383, + Age: 32, + Fare: 7.925, + Name: 'Tikkanen, Mr. Juho', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '113789', + Parch: 0, + Cabin: null, + PassengerId: 384, + Age: 35, + Fare: 52, + Name: 'Holverson, Mrs. Alexander Oskar (Mary Aline Towner)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349227', + Parch: 0, + Cabin: null, + PassengerId: 385, + Age: null, + Fare: 7.8958, + Name: 'Plotcharsky, Mr. Vasil', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'S.O.C. 14879', + Parch: 0, + Cabin: null, + PassengerId: 386, + Age: 18, + Fare: 73.5, + Name: 'Davies, Mr. Charles Henry', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 5, + Ticket: 'CA 2144', + Parch: 2, + Cabin: null, + PassengerId: 387, + Age: 1, + Fare: 46.9, + Name: 'Goodwin, Master. Sidney Leonard', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '27849', + Parch: 0, + Cabin: null, + PassengerId: 388, + Age: 36, + Fare: 13, + Name: 'Buss, Miss. Kate', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '367655', + Parch: 0, + Cabin: null, + PassengerId: 389, + Age: null, + Fare: 7.7292, + Name: 'Sadlier, Mr. Matthew', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SC 1748', + Parch: 0, + Cabin: null, + PassengerId: 390, + Age: 17, + Fare: 12, + Name: 'Lehmann, Miss. Bertha', + Survived: true, + Pclass: 2, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '113760', + Parch: 2, + Cabin: 'B96 B98', + PassengerId: 391, + Age: 36, + Fare: 120, + Name: 'Carter, Mr. William Ernest', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '350034', + Parch: 0, + Cabin: null, + PassengerId: 392, + Age: 21, + Fare: 7.7958, + Name: 'Jansson, Mr. Carl Olof', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '3101277', + Parch: 0, + Cabin: null, + PassengerId: 393, + Age: 28, + Fare: 7.925, + Name: 'Gustafsson, Mr. Johan Birger', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '35273', + Parch: 0, + Cabin: 'D36', + PassengerId: 394, + Age: 23, + Fare: 113.275, + Name: 'Newell, Miss. Marjorie', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PP 9549', + Parch: 2, + Cabin: 'G6', + PassengerId: 395, + Age: 24, + Fare: 16.7, + Name: 'Sandstrom, Mrs. Hjalmar (Agnes Charlotta Bengtsson)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '350052', + Parch: 0, + Cabin: null, + PassengerId: 396, + Age: 22, + Fare: 7.7958, + Name: 'Johansson, Mr. Erik', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '350407', + Parch: 0, + Cabin: null, + PassengerId: 397, + Age: 31, + Fare: 7.8542, + Name: 'Olsson, Miss. Elina', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '28403', + Parch: 0, + Cabin: null, + PassengerId: 398, + Age: 46, + Fare: 26, + Name: 'McKane, Mr. Peter David', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '244278', + Parch: 0, + Cabin: null, + PassengerId: 399, + Age: 23, + Fare: 10.5, + Name: 'Pain, Dr. Alfred', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '240929', + Parch: 0, + Cabin: null, + PassengerId: 400, + Age: 28, + Fare: 12.65, + Name: 'Trout, Mrs. William H (Jessie L)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'STON/O 2. 3101289', + Parch: 0, + Cabin: null, + PassengerId: 401, + Age: 39, + Fare: 7.925, + Name: 'Niskanen, Mr. Juha', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '341826', + Parch: 0, + Cabin: null, + PassengerId: 402, + Age: 26, + Fare: 8.05, + Name: 'Adams, Mr. John', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '4137', + Parch: 0, + Cabin: null, + PassengerId: 403, + Age: 21, + Fare: 9.825, + Name: 'Jussila, Miss. Mari Aina', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'STON/O2. 3101279', + Parch: 0, + Cabin: null, + PassengerId: 404, + Age: 28, + Fare: 15.85, + Name: 'Hakkarainen, Mr. Pekka Pietari', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '315096', + Parch: 0, + Cabin: null, + PassengerId: 405, + Age: 20, + Fare: 8.6625, + Name: 'Oreskovic, Miss. Marija', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '28664', + Parch: 0, + Cabin: null, + PassengerId: 406, + Age: 34, + Fare: 21, + Name: 'Gale, Mr. Shadrach', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347064', + Parch: 0, + Cabin: null, + PassengerId: 407, + Age: 51, + Fare: 7.75, + Name: 'Widegren, Mr. Carl/Charles Peter', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '29106', + Parch: 1, + Cabin: null, + PassengerId: 408, + Age: 3, + Fare: 18.75, + Name: 'Richards, Master. William Rowe', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '312992', + Parch: 0, + Cabin: null, + PassengerId: 409, + Age: 21, + Fare: 7.775, + Name: 'Birkeland, Mr. Hans Martin Monsen', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '4133', + Parch: 1, + Cabin: null, + PassengerId: 410, + Age: null, + Fare: 25.4667, + Name: 'Lefebre, Miss. Ida', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349222', + Parch: 0, + Cabin: null, + PassengerId: 411, + Age: null, + Fare: 7.8958, + Name: 'Sdycoff, Mr. Todor', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '394140', + Parch: 0, + Cabin: null, + PassengerId: 412, + Age: null, + Fare: 6.8583, + Name: 'Hart, Mr. Henry', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '19928', + Parch: 0, + Cabin: 'C78', + PassengerId: 413, + Age: 33, + Fare: 90, + Name: 'Minahan, Miss. Daisy E', + Survived: true, + Pclass: 1, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '239853', + Parch: 0, + Cabin: null, + PassengerId: 414, + Age: null, + Fare: 0, + Name: 'Cunningham, Mr. Alfred Fleming', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'STON/O 2. 3101269', + Parch: 0, + Cabin: null, + PassengerId: 415, + Age: 44, + Fare: 7.925, + Name: 'Sundman, Mr. Johan Julian', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '343095', + Parch: 0, + Cabin: null, + PassengerId: 416, + Age: null, + Fare: 8.05, + Name: 'Meek, Mrs. Thomas (Annie Louise Rowley)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '28220', + Parch: 1, + Cabin: null, + PassengerId: 417, + Age: 34, + Fare: 32.5, + Name: 'Drew, Mrs. James Vivian (Lulu Thorne Christian)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '250652', + Parch: 2, + Cabin: null, + PassengerId: 418, + Age: 18, + Fare: 13, + Name: 'Silven, Miss. Lyyli Karoliina', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '28228', + Parch: 0, + Cabin: null, + PassengerId: 419, + Age: 30, + Fare: 13, + Name: 'Matthews, Mr. William John', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '345773', + Parch: 2, + Cabin: null, + PassengerId: 420, + Age: 10, + Fare: 24.15, + Name: 'Van Impe, Miss. Catharina', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349254', + Parch: 0, + Cabin: null, + PassengerId: 421, + Age: null, + Fare: 7.8958, + Name: 'Gheorgheff, Mr. Stanio', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/5. 13032', + Parch: 0, + Cabin: null, + PassengerId: 422, + Age: 21, + Fare: 7.7333, + Name: 'Charters, Mr. David', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '315082', + Parch: 0, + Cabin: null, + PassengerId: 423, + Age: 29, + Fare: 7.875, + Name: 'Zimmerman, Mr. Leo', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '347080', + Parch: 1, + Cabin: null, + PassengerId: 424, + Age: 28, + Fare: 14.4, + Name: 'Danbom, Mrs. Ernst Gilbert (Anna Sigrid Maria Brogren)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '370129', + Parch: 1, + Cabin: null, + PassengerId: 425, + Age: 18, + Fare: 20.2125, + Name: 'Rosblom, Mr. Viktor Richard', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/4. 34244', + Parch: 0, + Cabin: null, + PassengerId: 426, + Age: null, + Fare: 7.25, + Name: 'Wiseman, Mr. Phillippe', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2003', + Parch: 0, + Cabin: null, + PassengerId: 427, + Age: 28, + Fare: 26, + Name: 'Clarke, Mrs. Charles V (Ada Maria Winfield)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '250655', + Parch: 0, + Cabin: null, + PassengerId: 428, + Age: 19, + Fare: 26, + Name: 'Phillips, Miss. Kate Florence ("Mrs Kate Louise Phillips Marshall")', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '364851', + Parch: 0, + Cabin: null, + PassengerId: 429, + Age: null, + Fare: 7.75, + Name: 'Flynn, Mr. James', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/O.Q. 392078', + Parch: 0, + Cabin: 'E10', + PassengerId: 430, + Age: 32, + Fare: 8.05, + Name: 'Pickard, Mr. Berk (Berk Trembisky)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '110564', + Parch: 0, + Cabin: 'C52', + PassengerId: 431, + Age: 28, + Fare: 26.55, + Name: 'Bjornstrom-Steffansson, Mr. Mauritz Hakan', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '376564', + Parch: 0, + Cabin: null, + PassengerId: 432, + Age: null, + Fare: 16.1, + Name: 'Thorneycroft, Mrs. Percival (Florence Kate White)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'SC/AH 3085', + Parch: 0, + Cabin: null, + PassengerId: 433, + Age: 42, + Fare: 26, + Name: 'Louch, Mrs. Charles Alexander (Alice Adelaide Slow)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'STON/O 2. 3101274', + Parch: 0, + Cabin: null, + PassengerId: 434, + Age: 17, + Fare: 7.125, + Name: 'Kallio, Mr. Nikolai Erland', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '13507', + Parch: 0, + Cabin: 'E44', + PassengerId: 435, + Age: 50, + Fare: 55.9, + Name: 'Silvey, Mr. William Baird', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '113760', + Parch: 2, + Cabin: 'B96 B98', + PassengerId: 436, + Age: 14, + Fare: 120, + Name: 'Carter, Miss. Lucile Polk', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 2, + Ticket: 'W./C. 6608', + Parch: 2, + Cabin: null, + PassengerId: 437, + Age: 21, + Fare: 34.375, + Name: 'Ford, Miss. Doolina Margaret "Daisy"', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 2, + Ticket: '29106', + Parch: 3, + Cabin: null, + PassengerId: 438, + Age: 24, + Fare: 18.75, + Name: 'Richards, Mrs. Sidney (Emily Hocking)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '19950', + Parch: 4, + Cabin: 'C23 C25 C27', + PassengerId: 439, + Age: 64, + Fare: 263, + Name: 'Fortune, Mr. Mark', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C.A. 18723', + Parch: 0, + Cabin: null, + PassengerId: 440, + Age: 31, + Fare: 10.5, + Name: 'Kvillner, Mr. Johan Henrik Johannesson', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'F.C.C. 13529', + Parch: 1, + Cabin: null, + PassengerId: 441, + Age: 45, + Fare: 26.25, + Name: 'Hart, Mrs. Benjamin (Esther Ada Bloomfield)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '345769', + Parch: 0, + Cabin: null, + PassengerId: 442, + Age: 20, + Fare: 9.5, + Name: 'Hampe, Mr. Leon', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '347076', + Parch: 0, + Cabin: null, + PassengerId: 443, + Age: 25, + Fare: 7.775, + Name: 'Petterson, Mr. Johan Emil', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '230434', + Parch: 0, + Cabin: null, + PassengerId: 444, + Age: 28, + Fare: 13, + Name: 'Reynaldo, Ms. Encarnacion', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '65306', + Parch: 0, + Cabin: null, + PassengerId: 445, + Age: null, + Fare: 8.1125, + Name: 'Johannesen-Bratthammer, Mr. Bernt', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '33638', + Parch: 2, + Cabin: 'A34', + PassengerId: 446, + Age: 4, + Fare: 81.8583, + Name: 'Dodge, Master. Washington', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '250644', + Parch: 1, + Cabin: null, + PassengerId: 447, + Age: 13, + Fare: 19.5, + Name: 'Mellinger, Miss. Madeleine Violet', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113794', + Parch: 0, + Cabin: null, + PassengerId: 448, + Age: 34, + Fare: 26.55, + Name: 'Seward, Mr. Frederic Kimber', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '2666', + Parch: 1, + Cabin: null, + PassengerId: 449, + Age: 5, + Fare: 19.2583, + Name: 'Baclini, Miss. Marie Catherine', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113786', + Parch: 0, + Cabin: 'C104', + PassengerId: 450, + Age: 52, + Fare: 30.5, + Name: 'Peuchen, Major. Arthur Godfrey', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 34651', + Parch: 2, + Cabin: null, + PassengerId: 451, + Age: 36, + Fare: 27.75, + Name: 'West, Mr. Edwy Arthur', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '65303', + Parch: 0, + Cabin: null, + PassengerId: 452, + Age: null, + Fare: 19.9667, + Name: 'Hagland, Mr. Ingvald Olai Olsen', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113051', + Parch: 0, + Cabin: 'C111', + PassengerId: 453, + Age: 30, + Fare: 27.75, + Name: 'Foreman, Mr. Benjamin Laventall', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '17453', + Parch: 0, + Cabin: 'C92', + PassengerId: 454, + Age: 49, + Fare: 89.1042, + Name: 'Goldenberg, Mr. Samuel L', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/5 2817', + Parch: 0, + Cabin: null, + PassengerId: 455, + Age: null, + Fare: 8.05, + Name: 'Peduzzi, Mr. Joseph', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349240', + Parch: 0, + Cabin: null, + PassengerId: 456, + Age: 29, + Fare: 7.8958, + Name: 'Jalsevac, Mr. Ivan', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '13509', + Parch: 0, + Cabin: 'E38', + PassengerId: 457, + Age: 65, + Fare: 26.55, + Name: 'Millet, Mr. Francis Davis', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '17464', + Parch: 0, + Cabin: 'D21', + PassengerId: 458, + Age: null, + Fare: 51.8625, + Name: 'Kenyon, Mrs. Frederick R (Marion)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'F.C.C. 13531', + Parch: 0, + Cabin: null, + PassengerId: 459, + Age: 50, + Fare: 10.5, + Name: 'Toomey, Miss. Ellen', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '371060', + Parch: 0, + Cabin: null, + PassengerId: 460, + Age: null, + Fare: 7.75, + Name: "O'Connor, Mr. Maurice", + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '19952', + Parch: 0, + Cabin: 'E12', + PassengerId: 461, + Age: 48, + Fare: 26.55, + Name: 'Anderson, Mr. Harry', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '364506', + Parch: 0, + Cabin: null, + PassengerId: 462, + Age: 34, + Fare: 8.05, + Name: 'Morley, Mr. William', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '111320', + Parch: 0, + Cabin: 'E63', + PassengerId: 463, + Age: 47, + Fare: 38.5, + Name: 'Gee, Mr. Arthur H', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '234360', + Parch: 0, + Cabin: null, + PassengerId: 464, + Age: 48, + Fare: 13, + Name: 'Milling, Mr. Jacob Christian', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/S 2816', + Parch: 0, + Cabin: null, + PassengerId: 465, + Age: null, + Fare: 8.05, + Name: 'Maisner, Mr. Simon', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/O.Q. 3101306', + Parch: 0, + Cabin: null, + PassengerId: 466, + Age: 38, + Fare: 7.05, + Name: 'Goncalves, Mr. Manuel Estanslas', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '239853', + Parch: 0, + Cabin: null, + PassengerId: 467, + Age: null, + Fare: 0, + Name: 'Campbell, Mr. William', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113792', + Parch: 0, + Cabin: null, + PassengerId: 468, + Age: 56, + Fare: 26.55, + Name: 'Smart, Mr. John Montgomery', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '36209', + Parch: 0, + Cabin: null, + PassengerId: 469, + Age: null, + Fare: 7.725, + Name: 'Scanlan, Mr. James', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '2666', + Parch: 1, + Cabin: null, + PassengerId: 470, + Age: 0.75, + Fare: 19.2583, + Name: 'Baclini, Miss. Helene Barbara', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '323592', + Parch: 0, + Cabin: null, + PassengerId: 471, + Age: null, + Fare: 7.25, + Name: 'Keefe, Mr. Arthur', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '315089', + Parch: 0, + Cabin: null, + PassengerId: 472, + Age: 38, + Fare: 8.6625, + Name: 'Cacic, Mr. Luka', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 34651', + Parch: 2, + Cabin: null, + PassengerId: 473, + Age: 33, + Fare: 27.75, + Name: 'West, Mrs. Edwy Arthur (Ada Mary Worth)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'SC/AH Basle 541', + Parch: 0, + Cabin: 'D', + PassengerId: 474, + Age: 23, + Fare: 13.7917, + Name: 'Jerwan, Mrs. Amin S (Marie Marthe Thuillard)', + Survived: true, + Pclass: 2, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '7553', + Parch: 0, + Cabin: null, + PassengerId: 475, + Age: 22, + Fare: 9.8375, + Name: 'Strandberg, Miss. Ida Sofia', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '110465', + Parch: 0, + Cabin: 'A14', + PassengerId: 476, + Age: null, + Fare: 52, + Name: 'Clifford, Mr. George Quincy', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '31027', + Parch: 0, + Cabin: null, + PassengerId: 477, + Age: 34, + Fare: 21, + Name: 'Renouf, Mr. Peter Henry', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '3460', + Parch: 0, + Cabin: null, + PassengerId: 478, + Age: 29, + Fare: 7.0458, + Name: 'Braund, Mr. Lewis Richard', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '350060', + Parch: 0, + Cabin: null, + PassengerId: 479, + Age: 22, + Fare: 7.5208, + Name: 'Karlsson, Mr. Nils August', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '3101298', + Parch: 1, + Cabin: null, + PassengerId: 480, + Age: 2, + Fare: 12.2875, + Name: 'Hirvonen, Miss. Hildur E', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 5, + Ticket: 'CA 2144', + Parch: 2, + Cabin: null, + PassengerId: 481, + Age: 9, + Fare: 46.9, + Name: 'Goodwin, Master. Harold Victor', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '239854', + Parch: 0, + Cabin: null, + PassengerId: 482, + Age: null, + Fare: 0, + Name: 'Frost, Mr. Anthony Wood "Archie"', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/5 3594', + Parch: 0, + Cabin: null, + PassengerId: 483, + Age: 50, + Fare: 8.05, + Name: 'Rouse, Mr. Richard Henry', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '4134', + Parch: 0, + Cabin: null, + PassengerId: 484, + Age: 63, + Fare: 9.5875, + Name: 'Turkula, Mrs. (Hedwig)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '11967', + Parch: 0, + Cabin: 'B49', + PassengerId: 485, + Age: 25, + Fare: 91.0792, + Name: 'Bishop, Mr. Dickinson H', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '4133', + Parch: 1, + Cabin: null, + PassengerId: 486, + Age: null, + Fare: 25.4667, + Name: 'Lefebre, Miss. Jeannie', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '19943', + Parch: 0, + Cabin: 'C93', + PassengerId: 487, + Age: 35, + Fare: 90, + Name: 'Hoyt, Mrs. Frederick Maxfield (Jane Anne Forby)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '11771', + Parch: 0, + Cabin: 'B37', + PassengerId: 488, + Age: 58, + Fare: 29.7, + Name: 'Kent, Mr. Edward Austin', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A.5. 18509', + Parch: 0, + Cabin: null, + PassengerId: 489, + Age: 30, + Fare: 8.05, + Name: 'Somerton, Mr. Francis William', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 37671', + Parch: 1, + Cabin: null, + PassengerId: 490, + Age: 9, + Fare: 15.9, + Name: 'Coutts, Master. Eden Leslie "Neville"', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '65304', + Parch: 0, + Cabin: null, + PassengerId: 491, + Age: null, + Fare: 19.9667, + Name: 'Hagland, Mr. Konrad Mathias Reiersen', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/OQ 3101317', + Parch: 0, + Cabin: null, + PassengerId: 492, + Age: 21, + Fare: 7.25, + Name: 'Windelov, Mr. Einar', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113787', + Parch: 0, + Cabin: 'C30', + PassengerId: 493, + Age: 55, + Fare: 30.5, + Name: 'Molson, Mr. Harry Markland', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17609', + Parch: 0, + Cabin: null, + PassengerId: 494, + Age: 71, + Fare: 49.5042, + Name: 'Artagaveytia, Mr. Ramon', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/4 45380', + Parch: 0, + Cabin: null, + PassengerId: 495, + Age: 21, + Fare: 8.05, + Name: 'Stanley, Mr. Edward Roland', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2627', + Parch: 0, + Cabin: null, + PassengerId: 496, + Age: null, + Fare: 14.4583, + Name: 'Yousseff, Mr. Gerious', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '36947', + Parch: 0, + Cabin: 'D20', + PassengerId: 497, + Age: 54, + Fare: 78.2667, + Name: 'Eustis, Miss. Elizabeth Mussey', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'C.A. 6212', + Parch: 0, + Cabin: null, + PassengerId: 498, + Age: null, + Fare: 15.1, + Name: 'Shellard, Mr. Frederick William', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '113781', + Parch: 2, + Cabin: 'C22 C26', + PassengerId: 499, + Age: 25, + Fare: 151.55, + Name: 'Allison, Mrs. Hudson J C (Bessie Waldo Daniels)', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '350035', + Parch: 0, + Cabin: null, + PassengerId: 500, + Age: 24, + Fare: 7.7958, + Name: 'Svensson, Mr. Olof', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '315086', + Parch: 0, + Cabin: null, + PassengerId: 501, + Age: 17, + Fare: 8.6625, + Name: 'Calic, Mr. Petar', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '364846', + Parch: 0, + Cabin: null, + PassengerId: 502, + Age: 21, + Fare: 7.75, + Name: 'Canavan, Miss. Mary', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '330909', + Parch: 0, + Cabin: null, + PassengerId: 503, + Age: null, + Fare: 7.6292, + Name: "O'Sullivan, Miss. Bridget Mary", + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '4135', + Parch: 0, + Cabin: null, + PassengerId: 504, + Age: 37, + Fare: 9.5875, + Name: 'Laitinen, Miss. Kristina Sofia', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '110152', + Parch: 0, + Cabin: 'B79', + PassengerId: 505, + Age: 16, + Fare: 86.5, + Name: 'Maioni, Miss. Roberta', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'PC 17758', + Parch: 0, + Cabin: 'C65', + PassengerId: 506, + Age: 18, + Fare: 108.9, + Name: 'Penasco y Castellana, Mr. Victor de Satode', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '26360', + Parch: 2, + Cabin: null, + PassengerId: 507, + Age: 33, + Fare: 26, + Name: 'Quick, Mrs. Frederick Charles (Jane Richards)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '111427', + Parch: 0, + Cabin: null, + PassengerId: 508, + Age: null, + Fare: 26.55, + Name: 'Bradley, Mr. George ("George Arthur Brayton")', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C 4001', + Parch: 0, + Cabin: null, + PassengerId: 509, + Age: 28, + Fare: 22.525, + Name: 'Olsen, Mr. Henry Margido', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '1601', + Parch: 0, + Cabin: null, + PassengerId: 510, + Age: 26, + Fare: 56.4958, + Name: 'Lang, Mr. Fang', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '382651', + Parch: 0, + Cabin: null, + PassengerId: 511, + Age: 29, + Fare: 7.75, + Name: 'Daly, Mr. Eugene Patrick', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/OQ 3101316', + Parch: 0, + Cabin: null, + PassengerId: 512, + Age: null, + Fare: 8.05, + Name: 'Webber, Mr. James', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17473', + Parch: 0, + Cabin: 'E25', + PassengerId: 513, + Age: 36, + Fare: 26.2875, + Name: 'McGough, Mr. James Robert', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'PC 17603', + Parch: 0, + Cabin: null, + PassengerId: 514, + Age: 54, + Fare: 59.4, + Name: 'Rothschild, Mrs. Martin (Elizabeth L. Barrett)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349209', + Parch: 0, + Cabin: null, + PassengerId: 515, + Age: 24, + Fare: 7.4958, + Name: 'Coleff, Mr. Satio', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '36967', + Parch: 0, + Cabin: 'D46', + PassengerId: 516, + Age: 47, + Fare: 34.0208, + Name: 'Walker, Mr. William Anderson', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C.A. 34260', + Parch: 0, + Cabin: 'F33', + PassengerId: 517, + Age: 34, + Fare: 10.5, + Name: 'Lemore, Mrs. (Amelia Milley)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '371110', + Parch: 0, + Cabin: null, + PassengerId: 518, + Age: null, + Fare: 24.15, + Name: 'Ryan, Mr. Patrick', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '226875', + Parch: 0, + Cabin: null, + PassengerId: 519, + Age: 36, + Fare: 26, + Name: 'Angle, Mrs. William A (Florence "Mary" Agnes Hughes)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349242', + Parch: 0, + Cabin: null, + PassengerId: 520, + Age: 32, + Fare: 7.8958, + Name: 'Pavlovic, Mr. Stefo', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '12749', + Parch: 0, + Cabin: 'B73', + PassengerId: 521, + Age: 30, + Fare: 93.5, + Name: 'Perreault, Miss. Anne', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349252', + Parch: 0, + Cabin: null, + PassengerId: 522, + Age: 22, + Fare: 7.8958, + Name: 'Vovk, Mr. Janko', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2624', + Parch: 0, + Cabin: null, + PassengerId: 523, + Age: null, + Fare: 7.225, + Name: 'Lahoud, Mr. Sarkis', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '111361', + Parch: 1, + Cabin: 'B18', + PassengerId: 524, + Age: 44, + Fare: 57.9792, + Name: 'Hippach, Mrs. Louis Albert (Ida Sophia Fischer)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2700', + Parch: 0, + Cabin: null, + PassengerId: 525, + Age: null, + Fare: 7.2292, + Name: 'Kassem, Mr. Fared', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '367232', + Parch: 0, + Cabin: null, + PassengerId: 526, + Age: 40.5, + Fare: 7.75, + Name: 'Farrell, Mr. James', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'W./C. 14258', + Parch: 0, + Cabin: null, + PassengerId: 527, + Age: 50, + Fare: 10.5, + Name: 'Ridsdale, Miss. Lucy', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17483', + Parch: 0, + Cabin: 'C95', + PassengerId: 528, + Age: null, + Fare: 221.7792, + Name: 'Farthing, Mr. John', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '3101296', + Parch: 0, + Cabin: null, + PassengerId: 529, + Age: 39, + Fare: 7.925, + Name: 'Salonen, Mr. Johan Werner', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '29104', + Parch: 1, + Cabin: null, + PassengerId: 530, + Age: 23, + Fare: 11.5, + Name: 'Hocking, Mr. Richard George', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '26360', + Parch: 1, + Cabin: null, + PassengerId: 531, + Age: 2, + Fare: 26, + Name: 'Quick, Miss. Phyllis May', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2641', + Parch: 0, + Cabin: null, + PassengerId: 532, + Age: null, + Fare: 7.2292, + Name: 'Toufik, Mr. Nakli', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2690', + Parch: 1, + Cabin: null, + PassengerId: 533, + Age: 17, + Fare: 7.2292, + Name: 'Elias, Mr. Joseph Jr', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2668', + Parch: 2, + Cabin: null, + PassengerId: 534, + Age: null, + Fare: 22.3583, + Name: 'Peter, Mrs. Catherine (Catherine Rizk)', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '315084', + Parch: 0, + Cabin: null, + PassengerId: 535, + Age: 30, + Fare: 8.6625, + Name: 'Cacic, Miss. Marija', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'F.C.C. 13529', + Parch: 2, + Cabin: null, + PassengerId: 536, + Age: 7, + Fare: 26.25, + Name: 'Hart, Miss. Eva Miriam', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113050', + Parch: 0, + Cabin: 'B38', + PassengerId: 537, + Age: 45, + Fare: 26.55, + Name: 'Butt, Major. Archibald Willingham', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17761', + Parch: 0, + Cabin: null, + PassengerId: 538, + Age: 30, + Fare: 106.425, + Name: 'LeRoy, Miss. Bertha', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '364498', + Parch: 0, + Cabin: null, + PassengerId: 539, + Age: null, + Fare: 14.5, + Name: 'Risien, Mr. Samuel Beard', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '13568', + Parch: 2, + Cabin: 'B39', + PassengerId: 540, + Age: 22, + Fare: 49.5, + Name: 'Frolicher, Miss. Hedwig Margaritha', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'WE/P 5735', + Parch: 2, + Cabin: 'B22', + PassengerId: 541, + Age: 36, + Fare: 71, + Name: 'Crosby, Miss. Harriet R', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 4, + Ticket: '347082', + Parch: 2, + Cabin: null, + PassengerId: 542, + Age: 9, + Fare: 31.275, + Name: 'Andersson, Miss. Ingeborg Constanzia', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 4, + Ticket: '347082', + Parch: 2, + Cabin: null, + PassengerId: 543, + Age: 11, + Fare: 31.275, + Name: 'Andersson, Miss. Sigrid Elisabeth', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '2908', + Parch: 0, + Cabin: null, + PassengerId: 544, + Age: 32, + Fare: 26, + Name: 'Beane, Mr. Edward', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'PC 17761', + Parch: 0, + Cabin: 'C86', + PassengerId: 545, + Age: 50, + Fare: 106.425, + Name: 'Douglas, Mr. Walter Donald', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '693', + Parch: 0, + Cabin: null, + PassengerId: 546, + Age: 64, + Fare: 26, + Name: 'Nicholson, Mr. Arthur Ernest', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2908', + Parch: 0, + Cabin: null, + PassengerId: 547, + Age: 19, + Fare: 26, + Name: 'Beane, Mrs. Edward (Ethel Clarke)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'SC/PARIS 2146', + Parch: 0, + Cabin: null, + PassengerId: 548, + Age: null, + Fare: 13.8625, + Name: 'Padro y Manent, Mr. Julian', + Survived: true, + Pclass: 2, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '363291', + Parch: 1, + Cabin: null, + PassengerId: 549, + Age: 33, + Fare: 20.525, + Name: 'Goldsmith, Mr. Frank John', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 33112', + Parch: 1, + Cabin: null, + PassengerId: 550, + Age: 8, + Fare: 36.75, + Name: 'Davies, Master. John Morgan Jr', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '17421', + Parch: 2, + Cabin: 'C70', + PassengerId: 551, + Age: 17, + Fare: 110.8833, + Name: 'Thayer, Mr. John Borland Jr', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '244358', + Parch: 0, + Cabin: null, + PassengerId: 552, + Age: 27, + Fare: 26, + Name: 'Sharp, Mr. Percival James R', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '330979', + Parch: 0, + Cabin: null, + PassengerId: 553, + Age: null, + Fare: 7.8292, + Name: "O'Brien, Mr. Timothy", + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2620', + Parch: 0, + Cabin: null, + PassengerId: 554, + Age: 22, + Fare: 7.225, + Name: 'Leeni, Mr. Fahim ("Philip Zenni")', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347085', + Parch: 0, + Cabin: null, + PassengerId: 555, + Age: 22, + Fare: 7.775, + Name: 'Ohman, Miss. Velin', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113807', + Parch: 0, + Cabin: null, + PassengerId: 556, + Age: 62, + Fare: 26.55, + Name: 'Wright, Mr. George', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '11755', + Parch: 0, + Cabin: 'A16', + PassengerId: 557, + Age: 48, + Fare: 39.6, + Name: 'Duff Gordon, Lady. (Lucille Christiana Sutherland) ("Mrs Morgan")', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17757', + Parch: 0, + Cabin: null, + PassengerId: 558, + Age: null, + Fare: 227.525, + Name: 'Robbins, Mr. Victor', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '110413', + Parch: 1, + Cabin: 'E67', + PassengerId: 559, + Age: 39, + Fare: 79.65, + Name: 'Taussig, Mrs. Emil (Tillie Mandelbaum)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '345572', + Parch: 0, + Cabin: null, + PassengerId: 560, + Age: 36, + Fare: 17.4, + Name: 'de Messemaeker, Mrs. Guillaume Joseph (Emma)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '372622', + Parch: 0, + Cabin: null, + PassengerId: 561, + Age: null, + Fare: 7.75, + Name: 'Morrow, Mr. Thomas Rowan', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349251', + Parch: 0, + Cabin: null, + PassengerId: 562, + Age: 40, + Fare: 7.8958, + Name: 'Sivic, Mr. Husein', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '218629', + Parch: 0, + Cabin: null, + PassengerId: 563, + Age: 28, + Fare: 13.5, + Name: 'Norman, Mr. Robert Douglas', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/OQ 392082', + Parch: 0, + Cabin: null, + PassengerId: 564, + Age: null, + Fare: 8.05, + Name: 'Simmons, Mr. John', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/O.Q. 392087', + Parch: 0, + Cabin: null, + PassengerId: 565, + Age: null, + Fare: 8.05, + Name: 'Meanwell, Miss. (Marion Ogden)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 2, + Ticket: 'A/4 48871', + Parch: 0, + Cabin: null, + PassengerId: 566, + Age: 24, + Fare: 24.15, + Name: 'Davies, Mr. Alfred J', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349205', + Parch: 0, + Cabin: null, + PassengerId: 567, + Age: 19, + Fare: 7.8958, + Name: 'Stoytcheff, Mr. Ilia', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349909', + Parch: 4, + Cabin: null, + PassengerId: 568, + Age: 29, + Fare: 21.075, + Name: 'Palsson, Mrs. Nils (Alma Cornelia Berglund)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2686', + Parch: 0, + Cabin: null, + PassengerId: 569, + Age: null, + Fare: 7.2292, + Name: 'Doharr, Mr. Tannous', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '350417', + Parch: 0, + Cabin: null, + PassengerId: 570, + Age: 32, + Fare: 7.8542, + Name: 'Jonsson, Mr. Carl', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'S.W./PP 752', + Parch: 0, + Cabin: null, + PassengerId: 571, + Age: 62, + Fare: 10.5, + Name: 'Harris, Mr. George', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '11769', + Parch: 0, + Cabin: 'C101', + PassengerId: 572, + Age: 53, + Fare: 51.4792, + Name: 'Appleton, Mrs. Edward Dale (Charlotte Lamson)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17474', + Parch: 0, + Cabin: 'E25', + PassengerId: 573, + Age: 36, + Fare: 26.3875, + Name: 'Flynn, Mr. John Irwin ("Irving")', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '14312', + Parch: 0, + Cabin: null, + PassengerId: 574, + Age: null, + Fare: 7.75, + Name: 'Kelly, Miss. Mary', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'A/4. 20589', + Parch: 0, + Cabin: null, + PassengerId: 575, + Age: 16, + Fare: 8.05, + Name: 'Rush, Mr. Alfred George John', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '358585', + Parch: 0, + Cabin: null, + PassengerId: 576, + Age: 19, + Fare: 14.5, + Name: 'Patchett, Mr. George', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '243880', + Parch: 0, + Cabin: null, + PassengerId: 577, + Age: 34, + Fare: 13, + Name: 'Garside, Miss. Ethel', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '13507', + Parch: 0, + Cabin: 'E44', + PassengerId: 578, + Age: 39, + Fare: 55.9, + Name: 'Silvey, Mrs. William Baird (Alice Munger)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '2689', + Parch: 0, + Cabin: null, + PassengerId: 579, + Age: null, + Fare: 14.4583, + Name: 'Caram, Mrs. Joseph (Maria Elias)', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'STON/O 2. 3101286', + Parch: 0, + Cabin: null, + PassengerId: 580, + Age: 32, + Fare: 7.925, + Name: 'Jussila, Mr. Eiriik', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '237789', + Parch: 1, + Cabin: null, + PassengerId: 581, + Age: 25, + Fare: 30, + Name: 'Christy, Miss. Julie Rachel', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '17421', + Parch: 1, + Cabin: 'C68', + PassengerId: 582, + Age: 39, + Fare: 110.8833, + Name: 'Thayer, Mrs. John Borland (Marian Longstreth Morris)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '28403', + Parch: 0, + Cabin: null, + PassengerId: 583, + Age: 54, + Fare: 26, + Name: 'Downton, Mr. William James', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '13049', + Parch: 0, + Cabin: 'A10', + PassengerId: 584, + Age: 36, + Fare: 40.125, + Name: 'Ross, Mr. John Hugo', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '3411', + Parch: 0, + Cabin: null, + PassengerId: 585, + Age: null, + Fare: 8.7125, + Name: 'Paulner, Mr. Uscher', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '110413', + Parch: 2, + Cabin: 'E68', + PassengerId: 586, + Age: 18, + Fare: 79.65, + Name: 'Taussig, Miss. Ruth', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '237565', + Parch: 0, + Cabin: null, + PassengerId: 587, + Age: 47, + Fare: 15, + Name: 'Jarvis, Mr. John Denzil', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '13567', + Parch: 1, + Cabin: 'B41', + PassengerId: 588, + Age: 60, + Fare: 79.2, + Name: 'Frolicher-Stehli, Mr. Maxmillian', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '14973', + Parch: 0, + Cabin: null, + PassengerId: 589, + Age: 22, + Fare: 8.05, + Name: 'Gilinski, Mr. Eliezer', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A./5. 3235', + Parch: 0, + Cabin: null, + PassengerId: 590, + Age: null, + Fare: 8.05, + Name: 'Murdlin, Mr. Joseph', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'STON/O 2. 3101273', + Parch: 0, + Cabin: null, + PassengerId: 591, + Age: 35, + Fare: 7.125, + Name: 'Rintamaki, Mr. Matti', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '36947', + Parch: 0, + Cabin: 'D20', + PassengerId: 592, + Age: 52, + Fare: 78.2667, + Name: 'Stephenson, Mrs. Walter Bertram (Martha Eustis)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'A/5 3902', + Parch: 0, + Cabin: null, + PassengerId: 593, + Age: 47, + Fare: 7.25, + Name: 'Elsbury, Mr. William James', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '364848', + Parch: 2, + Cabin: null, + PassengerId: 594, + Age: null, + Fare: 7.75, + Name: 'Bourke, Miss. Mary', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'SC/AH 29037', + Parch: 0, + Cabin: null, + PassengerId: 595, + Age: 37, + Fare: 26, + Name: 'Chapman, Mr. John Henry', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '345773', + Parch: 1, + Cabin: null, + PassengerId: 596, + Age: 36, + Fare: 24.15, + Name: 'Van Impe, Mr. Jean Baptiste', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '248727', + Parch: 0, + Cabin: null, + PassengerId: 597, + Age: null, + Fare: 33, + Name: 'Leitch, Miss. Jessie Wills', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'LINE', + Parch: 0, + Cabin: null, + PassengerId: 598, + Age: 49, + Fare: 0, + Name: 'Johnson, Mr. Alfred', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2664', + Parch: 0, + Cabin: null, + PassengerId: 599, + Age: null, + Fare: 7.225, + Name: 'Boulos, Mr. Hanna', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'PC 17485', + Parch: 0, + Cabin: 'A20', + PassengerId: 600, + Age: 49, + Fare: 56.9292, + Name: 'Duff Gordon, Sir. Cosmo Edmund ("Mr Morgan")', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '243847', + Parch: 1, + Cabin: null, + PassengerId: 601, + Age: 24, + Fare: 27, + Name: 'Jacobsohn, Mrs. Sidney Samuel (Amy Frances Christy)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349214', + Parch: 0, + Cabin: null, + PassengerId: 602, + Age: null, + Fare: 7.8958, + Name: 'Slabenoff, Mr. Petco', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113796', + Parch: 0, + Cabin: null, + PassengerId: 603, + Age: null, + Fare: 42.4, + Name: 'Harrington, Mr. Charles H', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '364511', + Parch: 0, + Cabin: null, + PassengerId: 604, + Age: 44, + Fare: 8.05, + Name: 'Torber, Mr. Ernst William', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '111426', + Parch: 0, + Cabin: null, + PassengerId: 605, + Age: 35, + Fare: 26.55, + Name: 'Homer, Mr. Harry ("Mr E Haven")', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '349910', + Parch: 0, + Cabin: null, + PassengerId: 606, + Age: 36, + Fare: 15.55, + Name: 'Lindell, Mr. Edvard Bengtsson', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349246', + Parch: 0, + Cabin: null, + PassengerId: 607, + Age: 30, + Fare: 7.8958, + Name: 'Karaic, Mr. Milan', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113804', + Parch: 0, + Cabin: null, + PassengerId: 608, + Age: 27, + Fare: 30.5, + Name: 'Daniel, Mr. Robert Williams', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'SC/Paris 2123', + Parch: 2, + Cabin: null, + PassengerId: 609, + Age: 22, + Fare: 41.5792, + Name: 'Laroche, Mrs. Joseph (Juliette Marie Louise Lafargue)', + Survived: true, + Pclass: 2, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17582', + Parch: 0, + Cabin: 'C125', + PassengerId: 610, + Age: 40, + Fare: 153.4625, + Name: 'Shutes, Miss. Elizabeth W', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '347082', + Parch: 5, + Cabin: null, + PassengerId: 611, + Age: 39, + Fare: 31.275, + Name: 'Andersson, Mrs. Anders Johan (Alfrida Konstantia Brogren)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'SOTON/O.Q. 3101305', + Parch: 0, + Cabin: null, + PassengerId: 612, + Age: null, + Fare: 7.05, + Name: 'Jardin, Mr. Jose Neto', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '367230', + Parch: 0, + Cabin: null, + PassengerId: 613, + Age: null, + Fare: 15.5, + Name: 'Murphy, Miss. Margaret Jane', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '370377', + Parch: 0, + Cabin: null, + PassengerId: 614, + Age: null, + Fare: 7.75, + Name: 'Horgan, Mr. John', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '364512', + Parch: 0, + Cabin: null, + PassengerId: 615, + Age: 35, + Fare: 8.05, + Name: 'Brocklebank, Mr. William Alfred', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '220845', + Parch: 2, + Cabin: null, + PassengerId: 616, + Age: 24, + Fare: 65, + Name: 'Herman, Miss. Alice', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '347080', + Parch: 1, + Cabin: null, + PassengerId: 617, + Age: 34, + Fare: 14.4, + Name: 'Danbom, Mr. Ernst Gilbert', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'A/5. 3336', + Parch: 0, + Cabin: null, + PassengerId: 618, + Age: 26, + Fare: 16.1, + Name: 'Lobb, Mrs. William Arthur (Cordelia K Stanlick)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 2, + Ticket: '230136', + Parch: 1, + Cabin: 'F4', + PassengerId: 619, + Age: 4, + Fare: 39, + Name: 'Becker, Miss. Marion Louise', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '31028', + Parch: 0, + Cabin: null, + PassengerId: 620, + Age: 26, + Fare: 10.5, + Name: 'Gavey, Mr. Lawrence', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2659', + Parch: 0, + Cabin: null, + PassengerId: 621, + Age: 27, + Fare: 14.4542, + Name: 'Yasbeck, Mr. Antoni', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '11753', + Parch: 0, + Cabin: 'D19', + PassengerId: 622, + Age: 42, + Fare: 52.5542, + Name: 'Kimball, Mr. Edwin Nelson Jr', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2653', + Parch: 1, + Cabin: null, + PassengerId: 623, + Age: 20, + Fare: 15.7417, + Name: 'Nakid, Mr. Sahid', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '350029', + Parch: 0, + Cabin: null, + PassengerId: 624, + Age: 21, + Fare: 7.8542, + Name: 'Hansen, Mr. Henry Damsgaard', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '54636', + Parch: 0, + Cabin: null, + PassengerId: 625, + Age: 21, + Fare: 16.1, + Name: 'Bowen, Mr. David John "Dai"', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '36963', + Parch: 0, + Cabin: 'D50', + PassengerId: 626, + Age: 61, + Fare: 32.3208, + Name: 'Sutton, Mr. Frederick', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '219533', + Parch: 0, + Cabin: null, + PassengerId: 627, + Age: 57, + Fare: 12.35, + Name: 'Kirkland, Rev. Charles Leonard', + Survived: false, + Pclass: 2, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '13502', + Parch: 0, + Cabin: 'D9', + PassengerId: 628, + Age: 21, + Fare: 77.9583, + Name: 'Longley, Miss. Gretchen Fiske', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349224', + Parch: 0, + Cabin: null, + PassengerId: 629, + Age: 26, + Fare: 7.8958, + Name: 'Bostandyeff, Mr. Guentcho', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '334912', + Parch: 0, + Cabin: null, + PassengerId: 630, + Age: null, + Fare: 7.7333, + Name: "O'Connell, Mr. Patrick D", + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '27042', + Parch: 0, + Cabin: 'A23', + PassengerId: 631, + Age: 80, + Fare: 30, + Name: 'Barkworth, Mr. Algernon Henry Wilson', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347743', + Parch: 0, + Cabin: null, + PassengerId: 632, + Age: 51, + Fare: 7.0542, + Name: 'Lundahl, Mr. Johan Svensson', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '13214', + Parch: 0, + Cabin: 'B50', + PassengerId: 633, + Age: 32, + Fare: 30.5, + Name: 'Stahelin-Maeglin, Dr. Max', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '112052', + Parch: 0, + Cabin: null, + PassengerId: 634, + Age: null, + Fare: 0, + Name: 'Parr, Mr. William Henry Marsh', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '347088', + Parch: 2, + Cabin: null, + PassengerId: 635, + Age: 9, + Fare: 27.9, + Name: 'Skoog, Miss. Mabel', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '237668', + Parch: 0, + Cabin: null, + PassengerId: 636, + Age: 28, + Fare: 13, + Name: 'Davis, Miss. Mary', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'STON/O 2. 3101292', + Parch: 0, + Cabin: null, + PassengerId: 637, + Age: 32, + Fare: 7.925, + Name: 'Leinonen, Mr. Antti Gustaf', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 31921', + Parch: 1, + Cabin: null, + PassengerId: 638, + Age: 31, + Fare: 26.25, + Name: 'Collyer, Mr. Harvey', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '3101295', + Parch: 5, + Cabin: null, + PassengerId: 639, + Age: 41, + Fare: 39.6875, + Name: 'Panula, Mrs. Juha (Maria Emilia Ojala)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '376564', + Parch: 0, + Cabin: null, + PassengerId: 640, + Age: null, + Fare: 16.1, + Name: 'Thorneycroft, Mr. Percival', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '350050', + Parch: 0, + Cabin: null, + PassengerId: 641, + Age: 20, + Fare: 7.8542, + Name: 'Jensen, Mr. Hans Peder', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17477', + Parch: 0, + Cabin: 'B35', + PassengerId: 642, + Age: 24, + Fare: 69.3, + Name: 'Sagesser, Mlle. Emma', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 3, + Ticket: '347088', + Parch: 2, + Cabin: null, + PassengerId: 643, + Age: 2, + Fare: 27.9, + Name: 'Skoog, Miss. Margit Elizabeth', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '1601', + Parch: 0, + Cabin: null, + PassengerId: 644, + Age: null, + Fare: 56.4958, + Name: 'Foo, Mr. Choong', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '2666', + Parch: 1, + Cabin: null, + PassengerId: 645, + Age: 0.75, + Fare: 19.2583, + Name: 'Baclini, Miss. Eugenie', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'PC 17572', + Parch: 0, + Cabin: 'D33', + PassengerId: 646, + Age: 48, + Fare: 76.7292, + Name: 'Harper, Mr. Henry Sleeper', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349231', + Parch: 0, + Cabin: null, + PassengerId: 647, + Age: 19, + Fare: 7.8958, + Name: 'Cor, Mr. Liudevit', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '13213', + Parch: 0, + Cabin: 'A26', + PassengerId: 648, + Age: 56, + Fare: 35.5, + Name: 'Simonius-Blumer, Col. Oberst Alfons', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'S.O./P.P. 751', + Parch: 0, + Cabin: null, + PassengerId: 649, + Age: null, + Fare: 7.55, + Name: 'Willey, Mr. Edward', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'CA. 2314', + Parch: 0, + Cabin: null, + PassengerId: 650, + Age: 23, + Fare: 7.55, + Name: 'Stanley, Miss. Amy Zillah Elsie', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349221', + Parch: 0, + Cabin: null, + PassengerId: 651, + Age: null, + Fare: 7.8958, + Name: 'Mitkoff, Mr. Mito', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '231919', + Parch: 1, + Cabin: null, + PassengerId: 652, + Age: 18, + Fare: 23, + Name: 'Doling, Miss. Elsie', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '8475', + Parch: 0, + Cabin: null, + PassengerId: 653, + Age: 21, + Fare: 8.4333, + Name: 'Kalvik, Mr. Johannes Halvorsen', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '330919', + Parch: 0, + Cabin: null, + PassengerId: 654, + Age: null, + Fare: 7.8292, + Name: 'O\'Leary, Miss. Hanora "Norah"', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '365226', + Parch: 0, + Cabin: null, + PassengerId: 655, + Age: 18, + Fare: 6.75, + Name: 'Hegarty, Miss. Hanora "Nora"', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 2, + Ticket: 'S.O.C. 14879', + Parch: 0, + Cabin: null, + PassengerId: 656, + Age: 24, + Fare: 73.5, + Name: 'Hickman, Mr. Leonard Mark', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349223', + Parch: 0, + Cabin: null, + PassengerId: 657, + Age: null, + Fare: 7.8958, + Name: 'Radeff, Mr. Alexander', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '364849', + Parch: 1, + Cabin: null, + PassengerId: 658, + Age: 32, + Fare: 15.5, + Name: 'Bourke, Mrs. John (Catherine)', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '29751', + Parch: 0, + Cabin: null, + PassengerId: 659, + Age: 23, + Fare: 13, + Name: 'Eitemiller, Mr. George Floyd', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '35273', + Parch: 2, + Cabin: 'D48', + PassengerId: 660, + Age: 58, + Fare: 113.275, + Name: 'Newell, Mr. Arthur Webster', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: 'PC 17611', + Parch: 0, + Cabin: null, + PassengerId: 661, + Age: 50, + Fare: 133.65, + Name: 'Frauenthal, Dr. Henry William', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2623', + Parch: 0, + Cabin: null, + PassengerId: 662, + Age: 40, + Fare: 7.225, + Name: 'Badt, Mr. Mohamed', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '5727', + Parch: 0, + Cabin: 'E58', + PassengerId: 663, + Age: 47, + Fare: 25.5875, + Name: 'Colley, Mr. Edward Pomeroy', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349210', + Parch: 0, + Cabin: null, + PassengerId: 664, + Age: 36, + Fare: 7.4958, + Name: 'Coleff, Mr. Peju', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'STON/O 2. 3101285', + Parch: 0, + Cabin: null, + PassengerId: 665, + Age: 20, + Fare: 7.925, + Name: 'Lindqvist, Mr. Eino William', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: 'S.O.C. 14879', + Parch: 0, + Cabin: null, + PassengerId: 666, + Age: 32, + Fare: 73.5, + Name: 'Hickman, Mr. Lewis', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '234686', + Parch: 0, + Cabin: null, + PassengerId: 667, + Age: 25, + Fare: 13, + Name: 'Butler, Mr. Reginald Fenton', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '312993', + Parch: 0, + Cabin: null, + PassengerId: 668, + Age: null, + Fare: 7.775, + Name: 'Rommetvedt, Mr. Knud Paust', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/5 3536', + Parch: 0, + Cabin: null, + PassengerId: 669, + Age: 43, + Fare: 8.05, + Name: 'Cook, Mr. Jacob', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '19996', + Parch: 0, + Cabin: 'C126', + PassengerId: 670, + Age: null, + Fare: 52, + Name: 'Taylor, Mrs. Elmer Zebley (Juliet Cummins Wright)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '29750', + Parch: 1, + Cabin: null, + PassengerId: 671, + Age: 40, + Fare: 39, + Name: 'Brown, Mrs. Thomas William Solomon (Elizabeth Catherine Ford)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'F.C. 12750', + Parch: 0, + Cabin: 'B71', + PassengerId: 672, + Age: 31, + Fare: 52, + Name: 'Davidson, Mr. Thornton', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C.A. 24580', + Parch: 0, + Cabin: null, + PassengerId: 673, + Age: 70, + Fare: 10.5, + Name: 'Mitchell, Mr. Henry Michael', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '244270', + Parch: 0, + Cabin: null, + PassengerId: 674, + Age: 31, + Fare: 13, + Name: 'Wilhelms, Mr. Charles', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '239856', + Parch: 0, + Cabin: null, + PassengerId: 675, + Age: null, + Fare: 0, + Name: 'Watson, Mr. Ennis Hastings', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349912', + Parch: 0, + Cabin: null, + PassengerId: 676, + Age: 18, + Fare: 7.775, + Name: 'Edvardsson, Mr. Gustaf Hjalmar', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '342826', + Parch: 0, + Cabin: null, + PassengerId: 677, + Age: 24.5, + Fare: 8.05, + Name: 'Sawyer, Mr. Frederick Charles', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '4138', + Parch: 0, + Cabin: null, + PassengerId: 678, + Age: 18, + Fare: 9.8417, + Name: 'Turja, Miss. Anna Sofia', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'CA 2144', + Parch: 6, + Cabin: null, + PassengerId: 679, + Age: 43, + Fare: 46.9, + Name: 'Goodwin, Mrs. Frederick (Augusta Tyler)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17755', + Parch: 1, + Cabin: 'B51 B53 B55', + PassengerId: 680, + Age: 36, + Fare: 512.3292, + Name: 'Cardeza, Mr. Thomas Drake Martinez', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '330935', + Parch: 0, + Cabin: null, + PassengerId: 681, + Age: null, + Fare: 8.1375, + Name: 'Peters, Miss. Katie', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17572', + Parch: 0, + Cabin: 'D49', + PassengerId: 682, + Age: 27, + Fare: 76.7292, + Name: 'Hassab, Mr. Hammad', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '6563', + Parch: 0, + Cabin: null, + PassengerId: 683, + Age: 20, + Fare: 9.225, + Name: 'Olsvigen, Mr. Thor Anderson', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 5, + Ticket: 'CA 2144', + Parch: 2, + Cabin: null, + PassengerId: 684, + Age: 14, + Fare: 46.9, + Name: 'Goodwin, Mr. Charles Edward', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '29750', + Parch: 1, + Cabin: null, + PassengerId: 685, + Age: 60, + Fare: 39, + Name: 'Brown, Mr. Thomas William Solomon', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'SC/Paris 2123', + Parch: 2, + Cabin: null, + PassengerId: 686, + Age: 25, + Fare: 41.5792, + Name: 'Laroche, Mr. Joseph Philippe Lemercier', + Survived: false, + Pclass: 2, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 4, + Ticket: '3101295', + Parch: 1, + Cabin: null, + PassengerId: 687, + Age: 14, + Fare: 39.6875, + Name: 'Panula, Mr. Jaako Arnold', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349228', + Parch: 0, + Cabin: null, + PassengerId: 688, + Age: 19, + Fare: 10.1708, + Name: 'Dakic, Mr. Branko', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '350036', + Parch: 0, + Cabin: null, + PassengerId: 689, + Age: 18, + Fare: 7.7958, + Name: 'Fischer, Mr. Eberhard Thelander', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '24160', + Parch: 1, + Cabin: 'B5', + PassengerId: 690, + Age: 15, + Fare: 211.3375, + Name: 'Madill, Miss. Georgette Alexandra', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '17474', + Parch: 0, + Cabin: 'B20', + PassengerId: 691, + Age: 31, + Fare: 57, + Name: 'Dick, Mr. Albert Adrian', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349256', + Parch: 1, + Cabin: null, + PassengerId: 692, + Age: 4, + Fare: 13.4167, + Name: 'Karun, Miss. Manca', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '1601', + Parch: 0, + Cabin: null, + PassengerId: 693, + Age: null, + Fare: 56.4958, + Name: 'Lam, Mr. Ali', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2672', + Parch: 0, + Cabin: null, + PassengerId: 694, + Age: 25, + Fare: 7.225, + Name: 'Saad, Mr. Khalil', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113800', + Parch: 0, + Cabin: null, + PassengerId: 695, + Age: 60, + Fare: 26.55, + Name: 'Weir, Col. John', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '248731', + Parch: 0, + Cabin: null, + PassengerId: 696, + Age: 52, + Fare: 13.5, + Name: 'Chapman, Mr. Charles Henry', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '363592', + Parch: 0, + Cabin: null, + PassengerId: 697, + Age: 44, + Fare: 8.05, + Name: 'Kelly, Mr. James', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '35852', + Parch: 0, + Cabin: null, + PassengerId: 698, + Age: null, + Fare: 7.7333, + Name: 'Mullens, Miss. Katherine "Katie"', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '17421', + Parch: 1, + Cabin: 'C68', + PassengerId: 699, + Age: 49, + Fare: 110.8833, + Name: 'Thayer, Mr. John Borland', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '348121', + Parch: 0, + Cabin: 'F G63', + PassengerId: 700, + Age: 42, + Fare: 7.65, + Name: 'Humblen, Mr. Adolf Mathias Nicolai Olsen', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'PC 17757', + Parch: 0, + Cabin: 'C62 C64', + PassengerId: 701, + Age: 18, + Fare: 227.525, + Name: 'Astor, Mrs. John Jacob (Madeleine Talmadge Force)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17475', + Parch: 0, + Cabin: 'E24', + PassengerId: 702, + Age: 35, + Fare: 26.2875, + Name: 'Silverthorne, Mr. Spencer Victor', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2691', + Parch: 1, + Cabin: null, + PassengerId: 703, + Age: 18, + Fare: 14.4542, + Name: 'Barbara, Miss. Saiide', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '36864', + Parch: 0, + Cabin: null, + PassengerId: 704, + Age: 25, + Fare: 7.7417, + Name: 'Gallagher, Mr. Martin', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '350025', + Parch: 0, + Cabin: null, + PassengerId: 705, + Age: 26, + Fare: 7.8542, + Name: 'Hansen, Mr. Henrik Juul', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '250655', + Parch: 0, + Cabin: null, + PassengerId: 706, + Age: 39, + Fare: 26, + Name: 'Morley, Mr. Henry Samuel ("Mr Henry Marshall")', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '223596', + Parch: 0, + Cabin: null, + PassengerId: 707, + Age: 45, + Fare: 13.5, + Name: 'Kelly, Mrs. Florence "Fannie"', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17476', + Parch: 0, + Cabin: 'E24', + PassengerId: 708, + Age: 42, + Fare: 26.2875, + Name: 'Calderhead, Mr. Edward Pennington', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113781', + Parch: 0, + Cabin: null, + PassengerId: 709, + Age: 22, + Fare: 151.55, + Name: 'Cleaver, Miss. Alice', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '2661', + Parch: 1, + Cabin: null, + PassengerId: 710, + Age: null, + Fare: 15.2458, + Name: 'Moubarek, Master. Halim Gonios ("William George")', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17482', + Parch: 0, + Cabin: 'C90', + PassengerId: 711, + Age: 24, + Fare: 49.5042, + Name: 'Mayne, Mlle. Berthe Antonine ("Mrs de Villiers")', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113028', + Parch: 0, + Cabin: 'C124', + PassengerId: 712, + Age: null, + Fare: 26.55, + Name: 'Klaber, Mr. Herman', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '19996', + Parch: 0, + Cabin: 'C126', + PassengerId: 713, + Age: 48, + Fare: 52, + Name: 'Taylor, Mr. Elmer Zebley', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '7545', + Parch: 0, + Cabin: null, + PassengerId: 714, + Age: 29, + Fare: 9.4833, + Name: 'Larsson, Mr. August Viktor', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '250647', + Parch: 0, + Cabin: null, + PassengerId: 715, + Age: 52, + Fare: 13, + Name: 'Greenberg, Mr. Samuel', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '348124', + Parch: 0, + Cabin: 'F G73', + PassengerId: 716, + Age: 19, + Fare: 7.65, + Name: 'Soholt, Mr. Peter Andreas Lauritz Andersen', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17757', + Parch: 0, + Cabin: 'C45', + PassengerId: 717, + Age: 38, + Fare: 227.525, + Name: 'Endres, Miss. Caroline Louise', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '34218', + Parch: 0, + Cabin: 'E101', + PassengerId: 718, + Age: 27, + Fare: 10.5, + Name: 'Troutt, Miss. Edwina Celia "Winnie"', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '36568', + Parch: 0, + Cabin: null, + PassengerId: 719, + Age: null, + Fare: 15.5, + Name: 'McEvoy, Mr. Michael', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347062', + Parch: 0, + Cabin: null, + PassengerId: 720, + Age: 33, + Fare: 7.775, + Name: 'Johnson, Mr. Malkolm Joackim', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '248727', + Parch: 1, + Cabin: null, + PassengerId: 721, + Age: 6, + Fare: 33, + Name: 'Harper, Miss. Annie Jessie "Nina"', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '350048', + Parch: 0, + Cabin: null, + PassengerId: 722, + Age: 17, + Fare: 7.0542, + Name: 'Jensen, Mr. Svend Lauritz', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '12233', + Parch: 0, + Cabin: null, + PassengerId: 723, + Age: 34, + Fare: 13, + Name: 'Gillespie, Mr. William Henry', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '250643', + Parch: 0, + Cabin: null, + PassengerId: 724, + Age: 50, + Fare: 13, + Name: 'Hodges, Mr. Henry Price', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '113806', + Parch: 0, + Cabin: 'E8', + PassengerId: 725, + Age: 27, + Fare: 53.1, + Name: 'Chambers, Mr. Norman Campbell', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '315094', + Parch: 0, + Cabin: null, + PassengerId: 726, + Age: 20, + Fare: 8.6625, + Name: 'Oreskovic, Mr. Luka', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '31027', + Parch: 0, + Cabin: null, + PassengerId: 727, + Age: 30, + Fare: 21, + Name: 'Renouf, Mrs. Peter Henry (Lillian Jefferys)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '36866', + Parch: 0, + Cabin: null, + PassengerId: 728, + Age: null, + Fare: 7.7375, + Name: 'Mannion, Miss. Margareth', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '236853', + Parch: 0, + Cabin: null, + PassengerId: 729, + Age: 25, + Fare: 26, + Name: 'Bryhl, Mr. Kurt Arnold Gottfrid', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'STON/O2. 3101271', + Parch: 0, + Cabin: null, + PassengerId: 730, + Age: 25, + Fare: 7.925, + Name: 'Ilmakangas, Miss. Pieta Sofia', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '24160', + Parch: 0, + Cabin: 'B5', + PassengerId: 731, + Age: 29, + Fare: 211.3375, + Name: 'Allen, Miss. Elisabeth Walton', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2699', + Parch: 0, + Cabin: null, + PassengerId: 732, + Age: 11, + Fare: 18.7875, + Name: 'Hassan, Mr. Houssein G N', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '239855', + Parch: 0, + Cabin: null, + PassengerId: 733, + Age: null, + Fare: 0, + Name: 'Knight, Mr. Robert J', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '28425', + Parch: 0, + Cabin: null, + PassengerId: 734, + Age: 23, + Fare: 13, + Name: 'Berriman, Mr. William John', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '233639', + Parch: 0, + Cabin: null, + PassengerId: 735, + Age: 23, + Fare: 13, + Name: 'Troupiansky, Mr. Moses Aaron', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '54636', + Parch: 0, + Cabin: null, + PassengerId: 736, + Age: 28.5, + Fare: 16.1, + Name: 'Williams, Mr. Leslie', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'W./C. 6608', + Parch: 3, + Cabin: null, + PassengerId: 737, + Age: 48, + Fare: 34.375, + Name: 'Ford, Mrs. Edward (Margaret Ann Watson)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17755', + Parch: 0, + Cabin: 'B101', + PassengerId: 738, + Age: 35, + Fare: 512.3292, + Name: 'Lesurer, Mr. Gustave J', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349201', + Parch: 0, + Cabin: null, + PassengerId: 739, + Age: null, + Fare: 7.8958, + Name: 'Ivanoff, Mr. Kanio', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349218', + Parch: 0, + Cabin: null, + PassengerId: 740, + Age: null, + Fare: 7.8958, + Name: 'Nankoff, Mr. Minko', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '16988', + Parch: 0, + Cabin: 'D45', + PassengerId: 741, + Age: null, + Fare: 30, + Name: 'Hawksford, Mr. Walter James', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '19877', + Parch: 0, + Cabin: 'C46', + PassengerId: 742, + Age: 36, + Fare: 78.85, + Name: 'Cavendish, Mr. Tyrell William', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: 'PC 17608', + Parch: 2, + Cabin: 'B57 B59 B63 B66', + PassengerId: 743, + Age: 21, + Fare: 262.375, + Name: 'Ryerson, Miss. Susan Parker "Suzette"', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '376566', + Parch: 0, + Cabin: null, + PassengerId: 744, + Age: 24, + Fare: 16.1, + Name: 'McNamee, Mr. Neal', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'STON/O 2. 3101288', + Parch: 0, + Cabin: null, + PassengerId: 745, + Age: 31, + Fare: 7.925, + Name: 'Stranden, Mr. Juho', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'WE/P 5735', + Parch: 1, + Cabin: 'B22', + PassengerId: 746, + Age: 70, + Fare: 71, + Name: 'Crosby, Capt. Edward Gifford', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 2673', + Parch: 1, + Cabin: null, + PassengerId: 747, + Age: 16, + Fare: 20.25, + Name: 'Abbott, Mr. Rossmore Edward', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '250648', + Parch: 0, + Cabin: null, + PassengerId: 748, + Age: 30, + Fare: 13, + Name: 'Sinkkonen, Miss. Anna', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '113773', + Parch: 0, + Cabin: 'D30', + PassengerId: 749, + Age: 19, + Fare: 53.1, + Name: 'Marvin, Mr. Daniel Warner', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '335097', + Parch: 0, + Cabin: null, + PassengerId: 750, + Age: 31, + Fare: 7.75, + Name: 'Connaghton, Mr. Michael', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '29103', + Parch: 1, + Cabin: null, + PassengerId: 751, + Age: 4, + Fare: 23, + Name: 'Wells, Miss. Joan', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '392096', + Parch: 1, + Cabin: 'E121', + PassengerId: 752, + Age: 6, + Fare: 12.475, + Name: 'Moor, Master. Meier', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '345780', + Parch: 0, + Cabin: null, + PassengerId: 753, + Age: 33, + Fare: 9.5, + Name: 'Vande Velde, Mr. Johannes Joseph', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349204', + Parch: 0, + Cabin: null, + PassengerId: 754, + Age: 23, + Fare: 7.8958, + Name: 'Jonkoff, Mr. Lalio', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '220845', + Parch: 2, + Cabin: null, + PassengerId: 755, + Age: 48, + Fare: 65, + Name: 'Herman, Mrs. Samuel (Jane Laver)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '250649', + Parch: 1, + Cabin: null, + PassengerId: 756, + Age: 0.67, + Fare: 14.5, + Name: 'Hamalainen, Master. Viljo', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '350042', + Parch: 0, + Cabin: null, + PassengerId: 757, + Age: 28, + Fare: 7.7958, + Name: 'Carlsson, Mr. August Sigfrid', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '29108', + Parch: 0, + Cabin: null, + PassengerId: 758, + Age: 18, + Fare: 11.5, + Name: 'Bailey, Mr. Percy Andrew', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '363294', + Parch: 0, + Cabin: null, + PassengerId: 759, + Age: 34, + Fare: 8.05, + Name: 'Theobald, Mr. Thomas Leonard', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '110152', + Parch: 0, + Cabin: 'B77', + PassengerId: 760, + Age: 33, + Fare: 86.5, + Name: 'Rothes, the Countess. of (Lucy Noel Martha Dyer-Edwards)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '358585', + Parch: 0, + Cabin: null, + PassengerId: 761, + Age: null, + Fare: 14.5, + Name: 'Garfirth, Mr. John', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/O2 3101272', + Parch: 0, + Cabin: null, + PassengerId: 762, + Age: 41, + Fare: 7.125, + Name: 'Nirva, Mr. Iisakki Antino Aijo', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2663', + Parch: 0, + Cabin: null, + PassengerId: 763, + Age: 20, + Fare: 7.2292, + Name: 'Barah, Mr. Hanna Assi', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '113760', + Parch: 2, + Cabin: 'B96 B98', + PassengerId: 764, + Age: 36, + Fare: 120, + Name: 'Carter, Mrs. William Ernest (Lucile Polk)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '347074', + Parch: 0, + Cabin: null, + PassengerId: 765, + Age: 16, + Fare: 7.775, + Name: 'Eklund, Mr. Hans Linus', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '13502', + Parch: 0, + Cabin: 'D11', + PassengerId: 766, + Age: 51, + Fare: 77.9583, + Name: 'Hogeboom, Mrs. John C (Anna Andrews)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '112379', + Parch: 0, + Cabin: null, + PassengerId: 767, + Age: null, + Fare: 39.6, + Name: 'Brewe, Dr. Arthur Jackson', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '364850', + Parch: 0, + Cabin: null, + PassengerId: 768, + Age: 30.5, + Fare: 7.75, + Name: 'Mangan, Miss. Mary', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '371110', + Parch: 0, + Cabin: null, + PassengerId: 769, + Age: null, + Fare: 24.15, + Name: 'Moran, Mr. Daniel J', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '8471', + Parch: 0, + Cabin: null, + PassengerId: 770, + Age: 32, + Fare: 8.3625, + Name: 'Gronnestad, Mr. Daniel Danielsen', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '345781', + Parch: 0, + Cabin: null, + PassengerId: 771, + Age: 24, + Fare: 9.5, + Name: 'Lievens, Mr. Rene Aime', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '350047', + Parch: 0, + Cabin: null, + PassengerId: 772, + Age: 48, + Fare: 7.8542, + Name: 'Jensen, Mr. Niels Peder', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'S.O./P.P. 3', + Parch: 0, + Cabin: 'E77', + PassengerId: 773, + Age: 57, + Fare: 10.5, + Name: 'Mack, Mrs. (Mary)', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2674', + Parch: 0, + Cabin: null, + PassengerId: 774, + Age: null, + Fare: 7.225, + Name: 'Elias, Mr. Dibo', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '29105', + Parch: 3, + Cabin: null, + PassengerId: 775, + Age: 54, + Fare: 23, + Name: 'Hocking, Mrs. Elizabeth (Eliza Needs)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '347078', + Parch: 0, + Cabin: null, + PassengerId: 776, + Age: 18, + Fare: 7.75, + Name: 'Myhrman, Mr. Pehr Fabian Oliver Malkolm', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '383121', + Parch: 0, + Cabin: 'F38', + PassengerId: 777, + Age: null, + Fare: 7.75, + Name: 'Tobin, Mr. Roger', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '364516', + Parch: 0, + Cabin: null, + PassengerId: 778, + Age: 5, + Fare: 12.475, + Name: 'Emanuel, Miss. Virginia Ethel', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '36865', + Parch: 0, + Cabin: null, + PassengerId: 779, + Age: null, + Fare: 7.7375, + Name: 'Kilgannon, Mr. Thomas J', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '24160', + Parch: 1, + Cabin: 'B3', + PassengerId: 780, + Age: 43, + Fare: 211.3375, + Name: 'Robert, Mrs. Edward Scott (Elisabeth Walton McMillan)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2687', + Parch: 0, + Cabin: null, + PassengerId: 781, + Age: 13, + Fare: 7.2292, + Name: 'Ayoub, Miss. Banoura', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '17474', + Parch: 0, + Cabin: 'B20', + PassengerId: 782, + Age: 17, + Fare: 57, + Name: 'Dick, Mrs. Albert Adrian (Vera Gillespie)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113501', + Parch: 0, + Cabin: 'D6', + PassengerId: 783, + Age: 29, + Fare: 30, + Name: 'Long, Mr. Milton Clyde', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'W./C. 6607', + Parch: 2, + Cabin: null, + PassengerId: 784, + Age: null, + Fare: 23.45, + Name: 'Johnston, Mr. Andrew G', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/O.Q. 3101312', + Parch: 0, + Cabin: null, + PassengerId: 785, + Age: 25, + Fare: 7.05, + Name: 'Ali, Mr. William', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '374887', + Parch: 0, + Cabin: null, + PassengerId: 786, + Age: 25, + Fare: 7.25, + Name: 'Harmer, Mr. Abraham (David Lishin)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '3101265', + Parch: 0, + Cabin: null, + PassengerId: 787, + Age: 18, + Fare: 7.4958, + Name: 'Sjoblom, Miss. Anna Sofia', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 4, + Ticket: '382652', + Parch: 1, + Cabin: null, + PassengerId: 788, + Age: 8, + Fare: 29.125, + Name: 'Rice, Master. George Hugh', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 2315', + Parch: 2, + Cabin: null, + PassengerId: 789, + Age: 1, + Fare: 20.575, + Name: 'Dean, Master. Bertram Vere', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'PC 17593', + Parch: 0, + Cabin: 'B82 B84', + PassengerId: 790, + Age: 46, + Fare: 79.2, + Name: 'Guggenheim, Mr. Benjamin', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '12460', + Parch: 0, + Cabin: null, + PassengerId: 791, + Age: null, + Fare: 7.75, + Name: 'Keane, Mr. Andrew "Andy"', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '239865', + Parch: 0, + Cabin: null, + PassengerId: 792, + Age: 16, + Fare: 26, + Name: 'Gaskell, Mr. Alfred', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 8, + Ticket: 'CA. 2343', + Parch: 2, + Cabin: null, + PassengerId: 793, + Age: null, + Fare: 69.55, + Name: 'Sage, Miss. Stella Anna', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17600', + Parch: 0, + Cabin: null, + PassengerId: 794, + Age: null, + Fare: 30.6958, + Name: 'Hoyt, Mr. William Fisher', + Survived: false, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349203', + Parch: 0, + Cabin: null, + PassengerId: 795, + Age: 25, + Fare: 7.8958, + Name: 'Dantcheff, Mr. Ristiu', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '28213', + Parch: 0, + Cabin: null, + PassengerId: 796, + Age: 39, + Fare: 13, + Name: 'Otter, Mr. Richard', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '17465', + Parch: 0, + Cabin: 'D17', + PassengerId: 797, + Age: 49, + Fare: 25.9292, + Name: 'Leader, Dr. Alice (Farnham)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349244', + Parch: 0, + Cabin: null, + PassengerId: 798, + Age: 31, + Fare: 8.6833, + Name: 'Osman, Mrs. Mara', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2685', + Parch: 0, + Cabin: null, + PassengerId: 799, + Age: 30, + Fare: 7.2292, + Name: 'Ibrahim Shawah, Mr. Yousseff', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '345773', + Parch: 1, + Cabin: null, + PassengerId: 800, + Age: 30, + Fare: 24.15, + Name: 'Van Impe, Mrs. Jean Baptiste (Rosalie Paula Govaert)', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '250647', + Parch: 0, + Cabin: null, + PassengerId: 801, + Age: 34, + Fare: 13, + Name: 'Ponesell, Mr. Martin', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'C.A. 31921', + Parch: 1, + Cabin: null, + PassengerId: 802, + Age: 31, + Fare: 26.25, + Name: 'Collyer, Mrs. Harvey (Charlotte Annie Tate)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '113760', + Parch: 2, + Cabin: 'B96 B98', + PassengerId: 803, + Age: 11, + Fare: 120, + Name: 'Carter, Master. William Thornton II', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2625', + Parch: 1, + Cabin: null, + PassengerId: 804, + Age: 0.42, + Fare: 8.5167, + Name: 'Thomas, Master. Assad Alexander', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347089', + Parch: 0, + Cabin: null, + PassengerId: 805, + Age: 27, + Fare: 6.975, + Name: 'Hedman, Mr. Oskar Arvid', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347063', + Parch: 0, + Cabin: null, + PassengerId: 806, + Age: 31, + Fare: 7.775, + Name: 'Johansson, Mr. Karl Johan', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '112050', + Parch: 0, + Cabin: 'A36', + PassengerId: 807, + Age: 39, + Fare: 0, + Name: 'Andrews, Mr. Thomas Jr', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347087', + Parch: 0, + Cabin: null, + PassengerId: 808, + Age: 18, + Fare: 7.775, + Name: 'Pettersson, Miss. Ellen Natalia', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '248723', + Parch: 0, + Cabin: null, + PassengerId: 809, + Age: 39, + Fare: 13, + Name: 'Meyer, Mr. August', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '113806', + Parch: 0, + Cabin: 'E8', + PassengerId: 810, + Age: 33, + Fare: 53.1, + Name: 'Chambers, Mrs. Norman Campbell (Bertha Griggs)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '3474', + Parch: 0, + Cabin: null, + PassengerId: 811, + Age: 26, + Fare: 7.8875, + Name: 'Alexander, Mr. William', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'A/4 48871', + Parch: 0, + Cabin: null, + PassengerId: 812, + Age: 39, + Fare: 24.15, + Name: 'Lester, Mr. James', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '28206', + Parch: 0, + Cabin: null, + PassengerId: 813, + Age: 35, + Fare: 10.5, + Name: 'Slemen, Mr. Richard James', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 4, + Ticket: '347082', + Parch: 2, + Cabin: null, + PassengerId: 814, + Age: 6, + Fare: 31.275, + Name: 'Andersson, Miss. Ebba Iris Alfrida', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '364499', + Parch: 0, + Cabin: null, + PassengerId: 815, + Age: 30.5, + Fare: 8.05, + Name: 'Tomlin, Mr. Ernest Portage', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '112058', + Parch: 0, + Cabin: 'B102', + PassengerId: 816, + Age: null, + Fare: 0, + Name: 'Fry, Mr. Richard', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'STON/O2. 3101290', + Parch: 0, + Cabin: null, + PassengerId: 817, + Age: 23, + Fare: 7.925, + Name: 'Heininen, Miss. Wendla Maria', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'S.C./PARIS 2079', + Parch: 1, + Cabin: null, + PassengerId: 818, + Age: 31, + Fare: 37.0042, + Name: 'Mallet, Mr. Albert', + Survived: false, + Pclass: 2, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C 7075', + Parch: 0, + Cabin: null, + PassengerId: 819, + Age: 43, + Fare: 6.45, + Name: 'Holm, Mr. John Fredrik Alexander', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 3, + Ticket: '347088', + Parch: 2, + Cabin: null, + PassengerId: 820, + Age: 10, + Fare: 27.9, + Name: 'Skoog, Master. Karl Thorsten', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '12749', + Parch: 1, + Cabin: 'B69', + PassengerId: 821, + Age: 52, + Fare: 93.5, + Name: 'Hays, Mrs. Charles Melville (Clara Jennings Gregg)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '315098', + Parch: 0, + Cabin: null, + PassengerId: 822, + Age: 27, + Fare: 8.6625, + Name: 'Lulic, Mr. Nikola', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '19972', + Parch: 0, + Cabin: null, + PassengerId: 823, + Age: 38, + Fare: 0, + Name: 'Reuchlin, Jonkheer. John George', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '392096', + Parch: 1, + Cabin: 'E121', + PassengerId: 824, + Age: 27, + Fare: 12.475, + Name: 'Moor, Mrs. (Beila)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 4, + Ticket: '3101295', + Parch: 1, + Cabin: null, + PassengerId: 825, + Age: 2, + Fare: 39.6875, + Name: 'Panula, Master. Urho Abraham', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '368323', + Parch: 0, + Cabin: null, + PassengerId: 826, + Age: null, + Fare: 6.95, + Name: 'Flynn, Mr. John', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '1601', + Parch: 0, + Cabin: null, + PassengerId: 827, + Age: null, + Fare: 56.4958, + Name: 'Lam, Mr. Len', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'S.C./PARIS 2079', + Parch: 2, + Cabin: null, + PassengerId: 828, + Age: 1, + Fare: 37.0042, + Name: 'Mallet, Master. Andre', + Survived: true, + Pclass: 2, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '367228', + Parch: 0, + Cabin: null, + PassengerId: 829, + Age: null, + Fare: 7.75, + Name: 'McCormack, Mr. Thomas Joseph', + Survived: true, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113572', + Parch: 0, + Cabin: 'B28', + PassengerId: 830, + Age: 62, + Fare: 80, + Name: 'Stone, Mrs. George Nelson (Martha Evelyn)', + Survived: true, + Pclass: 1, + Embarked: null, + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '2659', + Parch: 0, + Cabin: null, + PassengerId: 831, + Age: 15, + Fare: 14.4542, + Name: 'Yasbeck, Mrs. Antoni (Selini Alexander)', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '29106', + Parch: 1, + Cabin: null, + PassengerId: 832, + Age: 0.83, + Fare: 18.75, + Name: 'Richards, Master. George Sibley', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2671', + Parch: 0, + Cabin: null, + PassengerId: 833, + Age: null, + Fare: 7.2292, + Name: 'Saad, Mr. Amin', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347468', + Parch: 0, + Cabin: null, + PassengerId: 834, + Age: 23, + Fare: 7.8542, + Name: 'Augustsson, Mr. Albert', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2223', + Parch: 0, + Cabin: null, + PassengerId: 835, + Age: 18, + Fare: 8.3, + Name: 'Allum, Mr. Owen George', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'PC 17756', + Parch: 1, + Cabin: 'E49', + PassengerId: 836, + Age: 39, + Fare: 83.1583, + Name: 'Compton, Miss. Sara Rebecca', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '315097', + Parch: 0, + Cabin: null, + PassengerId: 837, + Age: 21, + Fare: 8.6625, + Name: 'Pasic, Mr. Jakob', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '392092', + Parch: 0, + Cabin: null, + PassengerId: 838, + Age: null, + Fare: 8.05, + Name: 'Sirota, Mr. Maurice', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '1601', + Parch: 0, + Cabin: null, + PassengerId: 839, + Age: 32, + Fare: 56.4958, + Name: 'Chip, Mr. Chang', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '11774', + Parch: 0, + Cabin: 'C47', + PassengerId: 840, + Age: null, + Fare: 29.7, + Name: 'Marechal, Mr. Pierre', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/O2 3101287', + Parch: 0, + Cabin: null, + PassengerId: 841, + Age: 20, + Fare: 7.925, + Name: 'Alhomaki, Mr. Ilmari Rudolf', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'S.O./P.P. 3', + Parch: 0, + Cabin: null, + PassengerId: 842, + Age: 16, + Fare: 10.5, + Name: 'Mudd, Mr. Thomas Charles', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '113798', + Parch: 0, + Cabin: null, + PassengerId: 843, + Age: 30, + Fare: 31, + Name: 'Serepeca, Miss. Augusta', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2683', + Parch: 0, + Cabin: null, + PassengerId: 844, + Age: 34.5, + Fare: 6.4375, + Name: 'Lemberopolous, Mr. Peter L', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '315090', + Parch: 0, + Cabin: null, + PassengerId: 845, + Age: 17, + Fare: 8.6625, + Name: 'Culumovic, Mr. Jeso', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'C.A. 5547', + Parch: 0, + Cabin: null, + PassengerId: 846, + Age: 42, + Fare: 7.55, + Name: 'Abbing, Mr. Anthony', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 8, + Ticket: 'CA. 2343', + Parch: 2, + Cabin: null, + PassengerId: 847, + Age: null, + Fare: 69.55, + Name: 'Sage, Mr. Douglas Bullen', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349213', + Parch: 0, + Cabin: null, + PassengerId: 848, + Age: 35, + Fare: 7.8958, + Name: 'Markoff, Mr. Marin', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '248727', + Parch: 1, + Cabin: null, + PassengerId: 849, + Age: 28, + Fare: 33, + Name: 'Harper, Rev. John', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '17453', + Parch: 0, + Cabin: 'C92', + PassengerId: 850, + Age: null, + Fare: 89.1042, + Name: 'Goldenberg, Mrs. Samuel L (Edwiga Grabowska)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 4, + Ticket: '347082', + Parch: 2, + Cabin: null, + PassengerId: 851, + Age: 4, + Fare: 31.275, + Name: 'Andersson, Master. Sigvard Harald Elias', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '347060', + Parch: 0, + Cabin: null, + PassengerId: 852, + Age: 74, + Fare: 7.775, + Name: 'Svensson, Mr. Johan', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '2678', + Parch: 1, + Cabin: null, + PassengerId: 853, + Age: 9, + Fare: 15.2458, + Name: 'Boulos, Miss. Nourelain', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17592', + Parch: 1, + Cabin: 'D28', + PassengerId: 854, + Age: 16, + Fare: 39.4, + Name: 'Lines, Miss. Mary Conover', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '244252', + Parch: 0, + Cabin: null, + PassengerId: 855, + Age: 44, + Fare: 26, + Name: 'Carter, Mrs. Ernest Courtenay (Lilian Hughes)', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '392091', + Parch: 1, + Cabin: null, + PassengerId: 856, + Age: 18, + Fare: 9.35, + Name: 'Aks, Mrs. Sam (Leah Rosen)', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: '36928', + Parch: 1, + Cabin: null, + PassengerId: 857, + Age: 45, + Fare: 164.8667, + Name: 'Wick, Mrs. George Dennick (Mary Hitchcock)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '113055', + Parch: 0, + Cabin: 'E17', + PassengerId: 858, + Age: 51, + Fare: 26.55, + Name: 'Daly, Mr. Peter Denis ', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '2666', + Parch: 3, + Cabin: null, + PassengerId: 859, + Age: 24, + Fare: 19.2583, + Name: 'Baclini, Mrs. Solomon (Latifa Qurban)', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2629', + Parch: 0, + Cabin: null, + PassengerId: 860, + Age: null, + Fare: 7.2292, + Name: 'Razi, Mr. Raihed', + Survived: false, + Pclass: 3, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 2, + Ticket: '350026', + Parch: 0, + Cabin: null, + PassengerId: 861, + Age: 41, + Fare: 14.1083, + Name: 'Hansen, Mr. Claus Peter', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '28134', + Parch: 0, + Cabin: null, + PassengerId: 862, + Age: 21, + Fare: 11.5, + Name: 'Giles, Mr. Frederick Edward', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '17466', + Parch: 0, + Cabin: 'D17', + PassengerId: 863, + Age: 48, + Fare: 25.9292, + Name: 'Swift, Mrs. Frederick Joel (Margaret Welles Barron)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 8, + Ticket: 'CA. 2343', + Parch: 2, + Cabin: null, + PassengerId: 864, + Age: null, + Fare: 69.55, + Name: 'Sage, Miss. Dorothy Edith "Dolly"', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '233866', + Parch: 0, + Cabin: null, + PassengerId: 865, + Age: 24, + Fare: 13, + Name: 'Gill, Mr. John William', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '236852', + Parch: 0, + Cabin: null, + PassengerId: 866, + Age: 42, + Fare: 13, + Name: 'Bystrom, Mrs. (Karolina)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'SC/PARIS 2149', + Parch: 0, + Cabin: null, + PassengerId: 867, + Age: 27, + Fare: 13.8583, + Name: 'Duran y More, Miss. Asuncion', + Survived: true, + Pclass: 2, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'PC 17590', + Parch: 0, + Cabin: 'A24', + PassengerId: 868, + Age: 31, + Fare: 50.4958, + Name: 'Roebling, Mr. Washington Augustus II', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '345777', + Parch: 0, + Cabin: null, + PassengerId: 869, + Age: null, + Fare: 9.5, + Name: 'van Melkebeke, Mr. Philemon', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '347742', + Parch: 1, + Cabin: null, + PassengerId: 870, + Age: 4, + Fare: 11.1333, + Name: 'Johnson, Master. Harold Theodor', + Survived: true, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349248', + Parch: 0, + Cabin: null, + PassengerId: 871, + Age: 26, + Fare: 7.8958, + Name: 'Balkic, Mr. Cerin', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: '11751', + Parch: 1, + Cabin: 'D35', + PassengerId: 872, + Age: 47, + Fare: 52.5542, + Name: 'Beckwith, Mrs. Richard Leonard (Sallie Monypeny)', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '695', + Parch: 0, + Cabin: 'B51 B53 B55', + PassengerId: 873, + Age: 33, + Fare: 5, + Name: 'Carlsson, Mr. Frans Olof', + Survived: false, + Pclass: 1, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '345765', + Parch: 0, + Cabin: null, + PassengerId: 874, + Age: 47, + Fare: 9, + Name: 'Vander Cruyssen, Mr. Victor', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 1, + Ticket: 'P/PP 3381', + Parch: 0, + Cabin: null, + PassengerId: 875, + Age: 28, + Fare: 24, + Name: 'Abelson, Mrs. Samuel (Hannah Wizosky)', + Survived: true, + Pclass: 2, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '2667', + Parch: 0, + Cabin: null, + PassengerId: 876, + Age: 15, + Fare: 7.225, + Name: 'Najib, Miss. Adele Kiamie "Jane"', + Survived: true, + Pclass: 3, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '7534', + Parch: 0, + Cabin: null, + PassengerId: 877, + Age: 20, + Fare: 9.8458, + Name: 'Gustafsson, Mr. Alfred Ossian', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349212', + Parch: 0, + Cabin: null, + PassengerId: 878, + Age: 19, + Fare: 7.8958, + Name: 'Petroff, Mr. Nedelio', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '349217', + Parch: 0, + Cabin: null, + PassengerId: 879, + Age: null, + Fare: 7.8958, + Name: 'Laleff, Mr. Kristo', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '11767', + Parch: 1, + Cabin: 'C50', + PassengerId: 880, + Age: 56, + Fare: 83.1583, + Name: 'Potter, Mrs. Thomas Jr (Lily Alexenia Wilson)', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '230433', + Parch: 1, + Cabin: null, + PassengerId: 881, + Age: 25, + Fare: 26, + Name: 'Shelley, Mrs. William (Imanita Parrish Hall)', + Survived: true, + Pclass: 2, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '349257', + Parch: 0, + Cabin: null, + PassengerId: 882, + Age: 33, + Fare: 7.8958, + Name: 'Markun, Mr. Johann', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '7552', + Parch: 0, + Cabin: null, + PassengerId: 883, + Age: 22, + Fare: 10.5167, + Name: 'Dahlberg, Miss. Gerda Ulrika', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: 'C.A./SOTON 34068', + Parch: 0, + Cabin: null, + PassengerId: 884, + Age: 28, + Fare: 10.5, + Name: 'Banfield, Mr. Frederick James', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: 'SOTON/OQ 392076', + Parch: 0, + Cabin: null, + PassengerId: 885, + Age: 25, + Fare: 7.05, + Name: 'Sutehall, Mr. Henry Jr', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '382652', + Parch: 5, + Cabin: null, + PassengerId: 886, + Age: 39, + Fare: 29.125, + Name: 'Rice, Mrs. William (Margaret Norton)', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '211536', + Parch: 0, + Cabin: null, + PassengerId: 887, + Age: 27, + Fare: 13, + Name: 'Montvila, Rev. Juozas', + Survived: false, + Pclass: 2, + Embarked: 'S', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '112053', + Parch: 0, + Cabin: 'B42', + PassengerId: 888, + Age: 19, + Fare: 30, + Name: 'Graham, Miss. Margaret Edith', + Survived: true, + Pclass: 1, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 1, + Ticket: 'W./C. 6607', + Parch: 2, + Cabin: null, + PassengerId: 889, + Age: null, + Fare: 23.45, + Name: 'Johnston, Miss. Catherine Helen "Carrie"', + Survived: false, + Pclass: 3, + Embarked: 'S', + Sex: 'female' + }, + { + SibSp: 0, + Ticket: '111369', + Parch: 0, + Cabin: 'C148', + PassengerId: 890, + Age: 26, + Fare: 30, + Name: 'Behr, Mr. Karl Howell', + Survived: true, + Pclass: 1, + Embarked: 'C', + Sex: 'male' + }, + { + SibSp: 0, + Ticket: '370376', + Parch: 0, + Cabin: null, + PassengerId: 891, + Age: 32, + Fare: 7.75, + Name: 'Dooley, Mr. Patrick', + Survived: false, + Pclass: 3, + Embarked: 'Q', + Sex: 'male' + } +]; diff --git a/src/datascience-ui/history-react/index.html b/src/datascience-ui/history-react/index.html new file mode 100644 index 000000000000..5f8c85aee905 --- /dev/null +++ b/src/datascience-ui/history-react/index.html @@ -0,0 +1,379 @@ + + + + + + + React App + + + + +
+ + + + + + + diff --git a/src/datascience-ui/history-react/index.tsx b/src/datascience-ui/history-react/index.tsx new file mode 100644 index 000000000000..e50937db6593 --- /dev/null +++ b/src/datascience-ui/history-react/index.tsx @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// This must be on top, do not change. Required by webpack. +import '../common/main'; +// This must be on top, do not change. Required by webpack. + +// tslint:disable-next-line: ordered-imports +import '../common/index.css'; + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; + +import { WidgetManagerComponent } from '../ipywidgets/container'; +import { IVsCodeApi, PostOffice } from '../react-common/postOffice'; +import { detectBaseTheme } from '../react-common/themeDetector'; +import { getConnectedInteractiveEditor } from './interactivePanel'; +import { createStore } from './redux/store'; + +// This special function talks to vscode from a web panel +export declare function acquireVsCodeApi(): IVsCodeApi; +const baseTheme = detectBaseTheme(); +// tslint:disable-next-line: no-any +const testMode = (window as any).inTestMode; +// tslint:disable-next-line: no-typeof-undefined +const skipDefault = testMode ? false : typeof acquireVsCodeApi !== 'undefined'; + +// Create the redux store +const postOffice = new PostOffice(); +const store = createStore(skipDefault, baseTheme, testMode, postOffice); + +// Wire up a connected react control for our InteractiveEditor +const ConnectedInteractiveEditor = getConnectedInteractiveEditor(); + +// Stick them all together +// tslint:disable:no-typeof-undefined +ReactDOM.render( + + + + , + document.getElementById('root') as HTMLElement +); diff --git a/src/datascience-ui/history-react/interactiveCell.tsx b/src/datascience-ui/history-react/interactiveCell.tsx new file mode 100644 index 000000000000..63a5e7bdf10f --- /dev/null +++ b/src/datascience-ui/history-react/interactiveCell.tsx @@ -0,0 +1,464 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../client/common/extensions'; + +import { nbformat } from '@jupyterlab/coreutils'; +import * as fastDeepEqual from 'fast-deep-equal'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as React from 'react'; +import { connect } from 'react-redux'; + +import { Identifiers } from '../../client/datascience/constants'; +import { CellState, IDataScienceExtraSettings } from '../../client/datascience/types'; +import { CellInput } from '../interactive-common/cellInput'; +import { CellOutput } from '../interactive-common/cellOutput'; +import { CollapseButton } from '../interactive-common/collapseButton'; +import { ExecutionCount } from '../interactive-common/executionCount'; +import { InformationMessages } from '../interactive-common/informationMessages'; +import { InputHistory } from '../interactive-common/inputHistory'; +import { ICellViewModel, IFont } from '../interactive-common/mainState'; +import { IKeyboardEvent } from '../react-common/event'; +import { Image, ImageName } from '../react-common/image'; +import { ImageButton } from '../react-common/imageButton'; +import { getLocString } from '../react-common/locReactSide'; +import { IMonacoModelContentChangeEvent } from '../react-common/monacoHelpers'; +import { actionCreators } from './redux/actions'; + +interface IInteractiveCellBaseProps { + role?: string; + cellVM: ICellViewModel; + language: string; + baseTheme: string; + codeTheme: string; + testMode?: boolean; + autoFocus: boolean; + maxTextSize?: number; + enableScroll?: boolean; + showWatermark: boolean; + monacoTheme: string | undefined; + editorOptions?: monacoEditor.editor.IEditorOptions; + editExecutionCount?: string; + editorMeasureClassName?: string; + font: IFont; + settings: IDataScienceExtraSettings; + focusPending: number; +} + +type IInteractiveCellProps = IInteractiveCellBaseProps & typeof actionCreators; + +// tslint:disable: react-this-binding-issue +export class InteractiveCell extends React.Component { + private codeRef: React.RefObject = React.createRef(); + private wrapperRef: React.RefObject = React.createRef(); + private inputHistory: InputHistory | undefined; + + constructor(prop: IInteractiveCellProps) { + super(prop); + this.state = { showingMarkdownEditor: false }; + if (prop.cellVM.cell.id === Identifiers.EditCellId) { + this.inputHistory = new InputHistory(); + } + } + + public render() { + if (this.props.cellVM.cell.data.cell_type === 'messages') { + return ; + } else { + return this.renderNormalCell(); + } + } + + public componentDidUpdate(prevProps: IInteractiveCellProps) { + if (this.props.cellVM.selected && !prevProps.cellVM.selected && !this.props.cellVM.focused) { + this.giveFocus(); + } + if (this.props.cellVM.scrollCount !== prevProps.cellVM.scrollCount) { + this.scrollAndFlash(); + } + } + + public shouldComponentUpdate(nextProps: IInteractiveCellProps): boolean { + return !fastDeepEqual(this.props, nextProps); + } + + private scrollAndFlash() { + if (this.wrapperRef && this.wrapperRef.current) { + // tslint:disable-next-line: no-any + if ((this.wrapperRef.current as any).scrollIntoView) { + this.wrapperRef.current.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' }); + } + this.wrapperRef.current.classList.add('flash'); + setTimeout(() => { + if (this.wrapperRef.current) { + this.wrapperRef.current.classList.remove('flash'); + } + }, 1000); + } + } + + private giveFocus() { + // Start out with ourselves + if (this.wrapperRef && this.wrapperRef.current) { + // Give focus to the cell if not already owning focus + if (!this.wrapperRef.current.contains(document.activeElement)) { + this.wrapperRef.current.focus(); + } + + // Scroll into view (since we have focus). However this function + // is not supported on enzyme + // tslint:disable-next-line: no-any + if ((this.wrapperRef.current as any).scrollIntoView) { + this.wrapperRef.current.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' }); + } + } + } + + private toggleInputBlock = () => { + const cellId: string = this.getCell().id; + this.props.toggleInputBlock(cellId); + }; + + private getCell = () => { + return this.props.cellVM.cell; + }; + + private isCodeCell = () => { + return this.props.cellVM.cell.data.cell_type === 'code'; + }; + + private renderNormalCell() { + const allowsPlainInput = + this.props.settings.showCellInputCode || this.props.cellVM.directInput || this.props.cellVM.editable; + const shouldRender = allowsPlainInput || this.shouldRenderResults(); + const cellOuterClass = this.props.cellVM.editable ? 'cell-outer-editable' : 'cell-outer'; + const cellWrapperClass = this.props.cellVM.editable ? 'cell-wrapper' : 'cell-wrapper cell-wrapper-noneditable'; + const themeMatplotlibPlots = this.props.settings.themeMatplotlibPlots ? true : false; + // Only render if we are allowed to. + if (shouldRender) { + return ( +
+
+ {this.renderControls()} +
+
+ {this.renderInput()} +
+ +
+
+
+
+
+ ); + } + + // Shouldn't be rendered because not allowing empty input and not a direct input cell + return null; + } + + private renderNormalToolbar = () => { + const cell = this.getCell(); + const cellId = cell.id; + const gotoCode = () => this.props.gotoCell(cellId); + const deleteCode = () => this.props.deleteCell(cellId); + const copyCode = () => this.props.copyCellCode(cellId); + const gatherCode = () => this.props.gatherCellToScript(cellId); + const hasNoSource = !cell || !cell.file || cell.file === Identifiers.EmptyFileName; + + return ( +
+ + + + + + +
+ ); + }; + + private onMouseClick = (ev: React.MouseEvent) => { + // When we receive a click, propagate upwards. Might change our state + ev.stopPropagation(); + this.props.clickCell(this.props.cellVM.cell.id); + }; + + private renderControls = () => { + const busy = + this.props.cellVM.cell.state === CellState.init || this.props.cellVM.cell.state === CellState.executing; + const collapseVisible = + this.props.cellVM.inputBlockCollapseNeeded && + this.props.cellVM.inputBlockShow && + !this.props.cellVM.editable && + this.isCodeCell(); + const executionCount = + this.props.cellVM && + this.props.cellVM.cell && + this.props.cellVM.cell.data && + this.props.cellVM.cell.data.execution_count + ? this.props.cellVM.cell.data.execution_count.toString() + : '-'; + const isEditOnlyCell = this.props.cellVM.cell.id === Identifiers.EditCellId; + const toolbar = isEditOnlyCell ? null : this.renderNormalToolbar(); + + return ( +
+ + + {toolbar} +
+ ); + }; + + private renderInput = () => { + if (this.isCodeCell()) { + return ( + + ); + } + return null; + }; + + private isEditCell(): boolean { + return this.getCell().id === Identifiers.EditCellId; + } + + private onUnfocused = () => { + this.props.unfocus(this.getCell().id); + }; + + private onCodeChange = (e: IMonacoModelContentChangeEvent) => { + this.props.editCell(this.getCell().id, e); + }; + + private onCodeCreated = (_code: string, _file: string, cellId: string, modelId: string) => { + this.props.codeCreated(cellId, modelId); + }; + + private hasOutput = () => { + return ( + this.getCell().state === CellState.finished || + this.getCell().state === CellState.error || + this.getCell().state === CellState.executing + ); + }; + + private getCodeCell = () => { + return this.props.cellVM.cell.data as nbformat.ICodeCell; + }; + + private shouldRenderResults(): boolean { + return ( + this.isCodeCell() && + this.hasOutput() && + this.getCodeCell().outputs && + this.getCodeCell().outputs.length > 0 && + !this.props.cellVM.hideOutput + ); + } + + private onKeyDown = (event: React.KeyboardEvent) => { + // Handle keydown events for the entire cell + if (this.getCell().id === Identifiers.EditCellId) { + const e: IKeyboardEvent = { + code: event.key, + shiftKey: event.shiftKey, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + altKey: event.altKey, + target: event.target as HTMLDivElement, + stopPropagation: () => event.stopPropagation(), + preventDefault: () => event.preventDefault() + }; + this.onEditCellKeyDown(Identifiers.EditCellId, e); + } + }; + + private onKeyUp = (event: React.KeyboardEvent) => { + // Handle keydown events for the entire cell + if (this.getCell().id === Identifiers.EditCellId) { + const e: IKeyboardEvent = { + code: event.key, + shiftKey: event.shiftKey, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + altKey: event.altKey, + target: event.target as HTMLDivElement, + stopPropagation: () => event.stopPropagation(), + preventDefault: () => event.preventDefault() + }; + this.onEditCellKeyUp(Identifiers.EditCellId, e); + } + }; + + private onEditCellKeyDown = (_cellId: string, e: IKeyboardEvent) => { + if (e.code === 'Enter' && e.shiftKey) { + this.editCellSubmit(e); + } else if (e.code === 'NumpadEnter' && e.shiftKey) { + this.editCellSubmit(e); + } else if (e.code === 'KeyU' && e.ctrlKey && e.editorInfo && !e.editorInfo.isSuggesting) { + e.editorInfo.clear(); + e.stopPropagation(); + e.preventDefault(); + } else if (e.code === 'Escape' && !e.shiftKey && e.editorInfo && !e.editorInfo.isSuggesting) { + e.editorInfo.clear(); + e.stopPropagation(); + e.preventDefault(); + } + }; + + private onEditCellKeyUp = (_cellId: string, e: IKeyboardEvent) => { + // Special case. Escape + Shift only comes as a key up because shift comes as the + // key down. + if (e.code === 'Escape' && e.shiftKey) { + this.editCellShiftEscape(e); + } + }; + + private editCellSubmit(e: IKeyboardEvent) { + if (e.editorInfo && e.editorInfo.contents) { + // Prevent shift+enter from turning into a enter + e.stopPropagation(); + e.preventDefault(); + + // Remove empty lines off the end + let endPos = e.editorInfo.contents.length - 1; + while (endPos >= 0 && e.editorInfo.contents[endPos] === '\n') { + endPos -= 1; + } + const content = e.editorInfo.contents.slice(0, endPos + 1); + + // Send to the input history too if necessary + if (this.inputHistory) { + this.inputHistory.add(content, e.editorInfo.isDirty); + } + + // Clear our editor + e.editorInfo.clear(); + + // Send to jupyter + this.props.submitInput(content, this.props.cellVM.cell.id); + } + } + + private findTabStop(direction: number, element: Element): HTMLElement | undefined { + if (element) { + const allFocusable = document.querySelectorAll('input, button, select, textarea, a[href]'); + if (allFocusable) { + const tabable = Array.prototype.filter.call(allFocusable, (i: HTMLElement) => i.tabIndex >= 0); + const self = tabable.indexOf(element); + return direction >= 0 ? tabable[self + 1] || tabable[0] : tabable[self - 1] || tabable[0]; + } + } + } + + private editCellShiftEscape = (e: IKeyboardEvent) => { + const focusedElement = document.activeElement; + if (focusedElement !== null) { + const nextTabStop = this.findTabStop(1, focusedElement); + if (nextTabStop) { + e.stopPropagation(); + e.preventDefault(); + nextTabStop.focus(); + } + } + }; + + private openLink = (uri: monacoEditor.Uri) => { + this.props.linkClick(uri.toString()); + }; +} + +// Main export, return a redux connected editor +export const InteractiveCellComponent = connect(null, actionCreators)(InteractiveCell); diff --git a/src/datascience-ui/history-react/interactivePanel.less b/src/datascience-ui/history-react/interactivePanel.less new file mode 100644 index 000000000000..419fd1b5cfe9 --- /dev/null +++ b/src/datascience-ui/history-react/interactivePanel.less @@ -0,0 +1,74 @@ +/* Import common styles and then override them below */ +@import '../interactive-common/common.css'; + +.toolbar-menu-bar-child { + background: var(--override-background, var(--vscode-editor-background)); + z-index: 10; +} + +#main-panel-content { + grid-area: content; + max-height: 100%; + overflow-x: hidden; + overflow-y: auto; +} + +.messages-result-container { + width: 100%; +} + +.messages-result-container pre { + white-space: pre-wrap; + font-family: monospace; + margin: 0px; + word-break: break-all; +} + +.cell-wrapper { + margin: 0px; + padding: 0px; + display: block; +} + +.cell-result-container { + margin: 0px; + display: grid; + grid-auto-columns: minmax(0, 1fr); +} + +.cell-outer { + display: grid; + grid-template-columns: auto minmax(0, 1fr) 8px; + grid-column-gap: 3px; + width: 100%; +} + +.cell-output { + margin: 0px; + width: 100%; + overflow-x: scroll; + background: transparent; +} + +.cell-output > div { + background: var(--override-widget-background, var(--vscode-notifications-background)); +} + +xmp { + margin: 0px; +} + +.cell-input { + margin: 0; +} + +.markdown-cell-output { + width: 100%; + overflow-x: scroll; +} + +.cell-output-text { + white-space: pre-wrap; + word-break: break-all; + overflow-x: hidden; +} diff --git a/src/datascience-ui/history-react/interactivePanel.tsx b/src/datascience-ui/history-react/interactivePanel.tsx new file mode 100644 index 000000000000..051b1993ae98 --- /dev/null +++ b/src/datascience-ui/history-react/interactivePanel.tsx @@ -0,0 +1,455 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Identifiers } from '../../client/datascience/constants'; +import { buildSettingsCss } from '../interactive-common/buildSettingsCss'; +import { ContentPanel, IContentPanelProps } from '../interactive-common/contentPanel'; +import { handleLinkClick } from '../interactive-common/handlers'; +import { JupyterInfo } from '../interactive-common/jupyterInfo'; +import { ICellViewModel } from '../interactive-common/mainState'; +import { IMainWithVariables, IStore } from '../interactive-common/redux/store'; +import { IVariablePanelProps, VariablePanel } from '../interactive-common/variablePanel'; +import { ErrorBoundary } from '../react-common/errorBoundary'; +import { Image, ImageName } from '../react-common/image'; +import { ImageButton } from '../react-common/imageButton'; +import { getLocString } from '../react-common/locReactSide'; +import { Progress } from '../react-common/progress'; +import { InteractiveCellComponent } from './interactiveCell'; +import './interactivePanel.less'; +import { actionCreators } from './redux/actions'; + +// tslint:disable: no-suspicious-comment + +export type IInteractivePanelProps = IMainWithVariables & typeof actionCreators; + +function mapStateToProps(state: IStore): IMainWithVariables { + return { ...state.main, variableState: state.variables }; +} + +export class InteractivePanel extends React.Component { + private mainPanelRef: React.RefObject = React.createRef(); + private mainPanelToolbarRef: React.RefObject = React.createRef(); + private contentPanelRef: React.RefObject = React.createRef(); + private renderCount: number = 0; + private internalScrollCount: number = 0; + + constructor(props: IInteractivePanelProps) { + super(props); + } + + public componentDidMount() { + document.addEventListener('click', this.linkClick, true); + this.props.editorLoaded(); + } + + public componentWillUnmount() { + document.removeEventListener('click', this.linkClick); + this.props.editorUnmounted(); + } + + public render() { + const dynamicFont: React.CSSProperties = { + fontSize: this.props.font.size, + fontFamily: this.props.font.family + }; + + const progressBar = (this.props.busy || !this.props.loaded) && !this.props.testMode ? : undefined; + + // If in test mode, update our count. Use this to determine how many renders a normal update takes. + if (this.props.testMode) { + this.renderCount = this.renderCount + 1; + } + + return ( +
+
+ +
+
+ {this.renderToolbarPanel()} + {progressBar} +
+
+ {this.renderVariablePanel(this.props.baseTheme)} +
+
+ {this.renderContentPanel(this.props.baseTheme)} +
+ +
+ ); + } + + // Make the entire footer focus our input, instead of having to click directly on the monaco editor + private footerPanelClick = (_event: React.MouseEvent) => { + this.props.focusInput(); + }; + + // tslint:disable-next-line: max-func-body-length + private renderToolbarPanel() { + const variableExplorerTooltip = this.props.variableState.visible + ? getLocString('DataScience.collapseVariableExplorerTooltip', 'Hide variables active in jupyter kernel') + : getLocString('DataScience.expandVariableExplorerTooltip', 'Show variables active in jupyter kernel'); + + return ( +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {this.renderKernelSelection()} +
+
+ ); + } + + private renderKernelSelection() { + if ( + this.props.settings && + this.props.settings.webviewExperiments && + this.props.settings.webviewExperiments.removeKernelToolbarInInteractiveWindow + ) { + if (this.props.settings.showKernelSelectionOnInteractiveWindow) { + return ( + + ); + } else if (this.props.kernel.localizedUri === getLocString('DataScience.localJupyterServer', 'local')) { + return; + } + } + + return ( + + ); + } + + private renderVariablePanel(baseTheme: string) { + if (this.props.variableState.visible) { + const variableProps = this.getVariableProps(baseTheme); + return ; + } + + return null; + } + + private renderContentPanel(baseTheme: string) { + // Skip if the tokenizer isn't finished yet. It needs + // to finish loading so our code editors work. + if (!this.props.monacoReady && !this.props.testMode) { + return null; + } + + // Otherwise render our cells. + const contentProps = this.getContentProps(baseTheme); + return ; + } + + private renderFooterPanel(baseTheme: string) { + // Skip if the tokenizer isn't finished yet. It needs + // to finish loading so our code editors work. + if ( + !this.props.monacoReady || + !this.props.editCellVM || + !this.props.settings || + !this.props.editorOptions || + !this.props.settings.allowInput + ) { + return null; + } + + const executionCount = this.getInputExecutionCount(); + const editPanelClass = this.props.settings.colorizeInputBox ? 'edit-panel-colorized' : 'edit-panel'; + + return ( +
+ + + +
+ ); + } + + private getInputExecutionCount = (): number => { + return this.props.currentExecutionCount + 1; + }; + + private getContentProps = (baseTheme: string): IContentPanelProps => { + return { + baseTheme: baseTheme, + cellVMs: this.props.cellVMs, + testMode: this.props.testMode, + codeTheme: this.props.codeTheme, + submittedText: this.props.submittedText, + settings: this.props.settings, + skipNextScroll: this.props.skipNextScroll ? true : false, + editable: false, + renderCell: this.renderCell, + scrollToBottom: this.scrollDiv, + scrollBeyondLastLine: this.props.settings + ? this.props.settings.extraSettings.editor.scrollBeyondLastLine + : false + }; + }; + private getVariableProps = (baseTheme: string): IVariablePanelProps => { + let toolbarHeight = 0; + if (this.mainPanelToolbarRef.current) { + toolbarHeight = this.mainPanelToolbarRef.current.offsetHeight; + } + return { + gridHeight: this.props.variableState.gridHeight, + containerHeight: this.props.variableState.containerHeight, + variables: this.props.variableState.variables, + debugging: this.props.debugging, + busy: this.props.busy, + showDataExplorer: this.props.showDataViewer, + skipDefault: this.props.skipDefault, + testMode: this.props.testMode, + closeVariableExplorer: this.props.toggleVariableExplorer, + setVariableExplorerHeight: this.props.setVariableExplorerHeight, + baseTheme: baseTheme, + pageIn: this.pageInVariableData, + fontSize: this.props.font.size, + executionCount: this.props.currentExecutionCount, + refreshCount: this.props.variableState.refreshCount, + offsetHeight: toolbarHeight, + supportsDebugging: + this.props.settings && this.props.settings.variableOptions + ? this.props.settings.variableOptions.enableDuringDebugger + : false + }; + }; + + private pageInVariableData = (startIndex: number, pageSize: number) => { + this.props.getVariableData( + this.props.currentExecutionCount, + this.props.variableState.refreshCount, + startIndex, + pageSize + ); + }; + + private renderCell = ( + cellVM: ICellViewModel, + _index: number, + containerRef?: React.RefObject + ): JSX.Element | null => { + // Note: MaxOutputSize and enableScrollingForCellOutputs is being ignored on purpose for + // the interactive window. See bug: https://github.com/microsoft/vscode-python/issues/11421 + if (this.props.settings && this.props.editorOptions) { + // Disable hover for collapsed code blocks + const options = { ...this.props.editorOptions, hover: { enabled: cellVM.inputBlockOpen } }; + return ( +
+ + + +
+ ); + } else { + return null; + } + }; + + // This handles the scrolling. Its called from the props of contentPanel. + // We only scroll when the state indicates we are at the bottom of the interactive window, + // otherwise it sometimes scrolls when the user wasn't at the bottom. + private scrollDiv = (div: HTMLDivElement) => { + if (this.props.isAtBottom) { + this.internalScrollCount += 1; + // Force auto here as smooth scrolling can be canceled by updates to the window + // from elsewhere (and keeping track of these would make this hard to maintain) + if (div && div.scrollIntoView) { + div.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' }); + } + } + }; + + private handleScroll = (e: React.UIEvent) => { + if (this.internalScrollCount > 0) { + this.internalScrollCount -= 1; + } else if (this.contentPanelRef.current) { + const isAtBottom = this.props.settings?.alwaysScrollOnNewCell + ? true + : this.contentPanelRef.current.computeIsAtBottom(e.currentTarget); + this.props.scroll(isAtBottom); + } + }; + + private linkClick = (ev: MouseEvent) => { + handleLinkClick(ev, this.props.linkClick); + }; +} + +// Main export, return a redux connected editor +export function getConnectedInteractiveEditor() { + return connect(mapStateToProps, actionCreators)(InteractivePanel); +} diff --git a/src/datascience-ui/history-react/redux/actions.ts b/src/datascience-ui/history-react/redux/actions.ts new file mode 100644 index 000000000000..4a8083675c1c --- /dev/null +++ b/src/datascience-ui/history-react/redux/actions.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { + IInteractiveWindowMapping, + InteractiveWindowMessages +} from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { IJupyterVariable, IJupyterVariablesRequest } from '../../../client/datascience/types'; +import { + CommonAction, + CommonActionType, + CommonActionTypeMapping, + ICellAction, + ICodeAction, + ICodeCreatedAction, + IEditCellAction, + ILinkClickAction, + IOpenSettingsAction, + IScrollAction, + IShowDataViewerAction, + IVariableExplorerHeight +} from '../../interactive-common/redux/reducers/types'; +import { IMonacoModelContentChangeEvent } from '../../react-common/monacoHelpers'; + +// This function isn't made common and not exported, to ensure it isn't used elsewhere. +function createIncomingActionWithPayload< + M extends IInteractiveWindowMapping & CommonActionTypeMapping, + K extends keyof M +>(type: K, data: M[K]): CommonAction { + // tslint:disable-next-line: no-any + return { type, payload: { data, messageDirection: 'incoming' } as any } as any; +} +// This function isn't made common and not exported, to ensure it isn't used elsewhere. +function createIncomingAction(type: CommonActionType | InteractiveWindowMessages): CommonAction { + return { type, payload: { messageDirection: 'incoming', data: undefined } }; +} + +// See https://react-redux.js.org/using-react-redux/connect-mapdispatch#defining-mapdispatchtoprops-as-an-object +export const actionCreators = { + focusInput: (): CommonAction => createIncomingAction(CommonActionType.FOCUS_INPUT), + restartKernel: (): CommonAction => createIncomingAction(CommonActionType.RESTART_KERNEL), + interruptKernel: (): CommonAction => createIncomingAction(CommonActionType.INTERRUPT_KERNEL), + deleteAllCells: (): CommonAction => createIncomingAction(InteractiveWindowMessages.DeleteAllCells), + deleteCell: (cellId: string): CommonAction => + createIncomingActionWithPayload(CommonActionType.DELETE_CELL, { cellId }), + undo: (): CommonAction => createIncomingAction(InteractiveWindowMessages.Undo), + redo: (): CommonAction => createIncomingAction(InteractiveWindowMessages.Redo), + linkClick: (href: string): CommonAction => + createIncomingActionWithPayload(CommonActionType.LINK_CLICK, { href }), + showPlot: (imageHtml: string) => createIncomingActionWithPayload(InteractiveWindowMessages.ShowPlot, imageHtml), + toggleInputBlock: (cellId: string): CommonAction => + createIncomingActionWithPayload(CommonActionType.TOGGLE_INPUT_BLOCK, { cellId }), + gotoCell: (cellId: string): CommonAction => + createIncomingActionWithPayload(CommonActionType.GOTO_CELL, { cellId }), + copyCellCode: (cellId: string): CommonAction => + createIncomingActionWithPayload(CommonActionType.COPY_CELL_CODE, { cellId }), + gatherCell: (cellId: string): CommonAction => + createIncomingActionWithPayload(CommonActionType.GATHER_CELL, { cellId }), + gatherCellToScript: (cellId: string): CommonAction => + createIncomingActionWithPayload(CommonActionType.GATHER_CELL_TO_SCRIPT, { cellId }), + clickCell: (cellId: string): CommonAction => + createIncomingActionWithPayload(CommonActionType.CLICK_CELL, { cellId }), + editCell: (cellId: string, e: IMonacoModelContentChangeEvent): CommonAction => + createIncomingActionWithPayload(CommonActionType.EDIT_CELL, { + cellId, + version: e.versionId, + modelId: e.model.id, + forward: e.forward, + reverse: e.reverse, + id: cellId, + code: e.model.getValue() + }), + submitInput: (code: string, cellId: string): CommonAction => + createIncomingActionWithPayload(CommonActionType.SUBMIT_INPUT, { code, cellId }), + toggleVariableExplorer: (): CommonAction => createIncomingAction(CommonActionType.TOGGLE_VARIABLE_EXPLORER), + setVariableExplorerHeight: (containerHeight: number, gridHeight: number): CommonAction => + createIncomingActionWithPayload(CommonActionType.SET_VARIABLE_EXPLORER_HEIGHT, { containerHeight, gridHeight }), + expandAll: (): CommonAction => createIncomingAction(InteractiveWindowMessages.ExpandAll), + collapseAll: (): CommonAction => createIncomingAction(InteractiveWindowMessages.CollapseAll), + export: (): CommonAction => createIncomingAction(CommonActionType.EXPORT), + exportAs: (): CommonAction => createIncomingAction(CommonActionType.EXPORT_NOTEBOOK_AS), + showDataViewer: (variable: IJupyterVariable, columnSize: number): CommonAction => + createIncomingActionWithPayload(CommonActionType.SHOW_DATA_VIEWER, { variable, columnSize }), + editorLoaded: (): CommonAction => createIncomingAction(CommonActionType.EDITOR_LOADED), + scroll: (isAtBottom: boolean): CommonAction => + createIncomingActionWithPayload(CommonActionType.SCROLL, { isAtBottom }), + unfocus: (cellId: string | undefined): CommonAction => + createIncomingActionWithPayload(CommonActionType.UNFOCUS_CELL, { cellId }), + codeCreated: (cellId: string | undefined, modelId: string): CommonAction => + createIncomingActionWithPayload(CommonActionType.CODE_CREATED, { cellId, modelId }), + editorUnmounted: (): CommonAction => createIncomingAction(CommonActionType.UNMOUNT), + selectKernel: (): CommonAction => createIncomingAction(InteractiveWindowMessages.SelectKernel), + selectServer: (): CommonAction => createIncomingAction(CommonActionType.SELECT_SERVER), + openSettings: (setting?: string): CommonAction => + createIncomingActionWithPayload(CommonActionType.OPEN_SETTINGS, { setting }), + getVariableData: ( + newExecutionCount: number, + refreshCount: number, + startIndex: number = 0, + pageSize: number = 100 + ): CommonAction => + createIncomingActionWithPayload(CommonActionType.GET_VARIABLE_DATA, { + executionCount: newExecutionCount, + sortColumn: 'name', + sortAscending: true, + startIndex, + pageSize, + refreshCount + }), + widgetFailed: (ex: Error): CommonAction => + createIncomingActionWithPayload(CommonActionType.IPYWIDGET_RENDER_FAILURE, ex) +}; diff --git a/src/datascience-ui/history-react/redux/mapping.ts b/src/datascience-ui/history-react/redux/mapping.ts new file mode 100644 index 000000000000..92002dfe409b --- /dev/null +++ b/src/datascience-ui/history-react/redux/mapping.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { + IInteractiveWindowMapping, + InteractiveWindowMessages +} from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { BaseReduxActionPayload } from '../../../client/datascience/interactive-common/types'; +import { IMainState } from '../../interactive-common/mainState'; +import { CommonActionType, CommonActionTypeMapping } from '../../interactive-common/redux/reducers/types'; +import { ReducerArg, ReducerFunc } from '../../react-common/reduxUtils'; + +export type InteractiveReducerFunc = ReducerFunc< + IMainState, + CommonActionType | InteractiveWindowMessages, + BaseReduxActionPayload +>; + +export type InteractiveReducerArg = ReducerArg< + IMainState, + CommonActionType | InteractiveWindowMessages, + BaseReduxActionPayload +>; + +type InteractiveWindowReducerFunctions = { + [P in keyof T]: T[P] extends never | undefined ? InteractiveReducerFunc : InteractiveReducerFunc; +}; + +export type IInteractiveActionMapping = InteractiveWindowReducerFunctions & + InteractiveWindowReducerFunctions; diff --git a/src/datascience-ui/history-react/redux/reducers/creation.ts b/src/datascience-ui/history-react/redux/reducers/creation.ts new file mode 100644 index 000000000000..5d309b6731c8 --- /dev/null +++ b/src/datascience-ui/history-react/redux/reducers/creation.ts @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Identifiers } from '../../../../client/datascience/constants'; +import { + IFinishCell, + InteractiveWindowMessages +} from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { ICell, IDataScienceExtraSettings } from '../../../../client/datascience/types'; +import { removeLinesFromFrontAndBack } from '../../../common'; +import { createCellVM, extractInputText, ICellViewModel, IMainState } from '../../../interactive-common/mainState'; +import { postActionToExtension } from '../../../interactive-common/redux/helpers'; +import { Helpers } from '../../../interactive-common/redux/reducers/helpers'; +import { IAddCellAction, ICellAction } from '../../../interactive-common/redux/reducers/types'; +import { InteractiveReducerArg } from '../mapping'; + +export namespace Creation { + function isCellSupported(state: IMainState, cell: ICell): boolean { + // Skip message cells in test mode + if (state.testMode) { + return cell.data.cell_type !== 'messages'; + } + return true; + } + + function extractInputBlockText(cellVM: ICellViewModel, settings?: IDataScienceExtraSettings) { + // Use the base function first + const text = extractInputText(cellVM, settings); + + // Then remove text on the front and back. We only do this for the interactive window + return removeLinesFromFrontAndBack(text); + } + + export function alterCellVM( + cellVM: ICellViewModel, + settings?: IDataScienceExtraSettings, + visible?: boolean, + expanded?: boolean + ): ICellViewModel { + if (cellVM.cell.data.cell_type === 'code') { + // If we are already in the correct state, return back our initial cell vm + if (cellVM.inputBlockShow === visible && cellVM.inputBlockOpen === expanded) { + return cellVM; + } + + const newCellVM = { ...cellVM }; + if (cellVM.inputBlockShow !== visible) { + if (visible) { + // Show the cell, the rest of the function will add on correct collapse state + newCellVM.inputBlockShow = true; + } else { + // Hide this cell + newCellVM.inputBlockShow = false; + } + } + + // No elseif as we want newly visible cells to pick up the correct expand / collapse state + if (cellVM.inputBlockOpen !== expanded && cellVM.inputBlockCollapseNeeded && cellVM.inputBlockShow) { + let newText = extractInputBlockText(cellVM, settings); + + // While extracting the text, we might eliminate all extra lines + if (newText.includes('\n')) { + if (expanded) { + // Expand the cell + newCellVM.inputBlockOpen = true; + newCellVM.inputBlockText = newText; + } else { + // Collapse the cell + if (newText.length > 0) { + newText = newText.split('\n', 1)[0]; + newText = newText.slice(0, 255); // Slice to limit length, slicing past length is fine + newText = newText.concat('...'); + } + + newCellVM.inputBlockOpen = false; + newCellVM.inputBlockText = newText; + } + } else { + // If all lines eliminated, get rid of the collapse bar. + newCellVM.inputBlockCollapseNeeded = false; + newCellVM.inputBlockOpen = true; + newCellVM.inputBlockText = newText; + } + } + + return newCellVM; + } + + return cellVM; + } + + export function prepareCellVM(cell: ICell, mainState: IMainState): ICellViewModel { + let cellVM: ICellViewModel = createCellVM(cell, mainState.settings, false, mainState.debugging); + + const visible = mainState.settings ? mainState.settings.showCellInputCode : false; + const expanded = !mainState.settings?.collapseCellInputCodeByDefault; + + // Set initial cell visibility and collapse + cellVM = alterCellVM(cellVM, mainState.settings, visible, expanded); + cellVM.hasBeenRun = true; + + return cellVM; + } + + export function startCell(arg: InteractiveReducerArg): IMainState { + if (isCellSupported(arg.prevState, arg.payload.data)) { + const result = Helpers.updateOrAdd(arg, prepareCellVM); + if ( + result.cellVMs.length > arg.prevState.cellVMs.length && + arg.payload.data.id !== Identifiers.EditCellId + ) { + const cellVM = result.cellVMs[result.cellVMs.length - 1]; + + // We're adding a new cell here. Tell the intellisense engine we have a new cell + postActionToExtension(arg, InteractiveWindowMessages.UpdateModel, { + source: 'user', + kind: 'add', + oldDirty: arg.prevState.dirty, + newDirty: true, + cell: cellVM.cell, + fullText: extractInputText(cellVM, result.settings), + currentText: cellVM.inputBlockText + }); + } + + return result; + } + return arg.prevState; + } + + export function updateCell(arg: InteractiveReducerArg): IMainState { + if (isCellSupported(arg.prevState, arg.payload.data)) { + return Helpers.updateOrAdd(arg, prepareCellVM); + } + return arg.prevState; + } + + export function finishCell(arg: InteractiveReducerArg): IMainState { + if (isCellSupported(arg.prevState, arg.payload.data.cell)) { + return Helpers.updateOrAdd( + { ...arg, payload: { ...arg.payload, data: arg.payload.data.cell } }, + prepareCellVM + ); + } + return arg.prevState; + } + + export function deleteAllCells(arg: InteractiveReducerArg): IMainState { + // Send messages to other side to indicate the deletes + postActionToExtension(arg, InteractiveWindowMessages.DeleteAllCells); + + return { + ...arg.prevState, + cellVMs: [], + undoStack: Helpers.pushStack(arg.prevState.undoStack, arg.prevState.cellVMs) + }; + } + + export function deleteCell(arg: InteractiveReducerArg): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (index >= 0 && arg.payload.data.cellId) { + // Send messages to other side to indicate the delete + postActionToExtension(arg, InteractiveWindowMessages.UpdateModel, { + source: 'user', + kind: 'remove', + index, + oldDirty: arg.prevState.dirty, + newDirty: true, + cell: arg.prevState.cellVMs[index].cell + }); + + const newVMs = arg.prevState.cellVMs.filter((_c, i) => i !== index); + return { + ...arg.prevState, + cellVMs: newVMs, + undoStack: Helpers.pushStack(arg.prevState.undoStack, arg.prevState.cellVMs) + }; + } + + return arg.prevState; + } + + export function unmount(arg: InteractiveReducerArg): IMainState { + return { + ...arg.prevState, + cellVMs: [], + undoStack: [], + redoStack: [], + editCellVM: undefined + }; + } + + export function loaded(arg: InteractiveReducerArg<{ cells: ICell[] }>): IMainState { + postActionToExtension(arg, InteractiveWindowMessages.LoadAllCellsComplete, { + cells: [] + }); + return { + ...arg.prevState, + loaded: true, + busy: false + }; + } +} diff --git a/src/datascience-ui/history-react/redux/reducers/effects.ts b/src/datascience-ui/history-react/redux/reducers/effects.ts new file mode 100644 index 000000000000..83cf3d14104d --- /dev/null +++ b/src/datascience-ui/history-react/redux/reducers/effects.ts @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Identifiers } from '../../../../client/datascience/constants'; +import { IScrollToCell } from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { CssMessages } from '../../../../client/datascience/messages'; +import { IDataScienceExtraSettings } from '../../../../client/datascience/types'; +import { IMainState } from '../../../interactive-common/mainState'; +import { postActionToExtension } from '../../../interactive-common/redux/helpers'; +import { Helpers } from '../../../interactive-common/redux/reducers/helpers'; +import { ICellAction, IScrollAction } from '../../../interactive-common/redux/reducers/types'; +import { computeEditorOptions } from '../../../react-common/settingsReactSide'; +import { InteractiveReducerArg } from '../mapping'; +import { Creation } from './creation'; + +export namespace Effects { + export function expandAll(arg: InteractiveReducerArg): IMainState { + if (arg.prevState.settings?.showCellInputCode) { + const newVMs = arg.prevState.cellVMs.map((c) => + Creation.alterCellVM({ ...c }, arg.prevState.settings, true, true) + ); + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + return arg.prevState; + } + + export function collapseAll(arg: InteractiveReducerArg): IMainState { + if (arg.prevState.settings?.showCellInputCode) { + const newVMs = arg.prevState.cellVMs.map((c) => + Creation.alterCellVM({ ...c }, arg.prevState.settings, true, false) + ); + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + return arg.prevState; + } + + export function toggleInputBlock(arg: InteractiveReducerArg): IMainState { + if (arg.payload.data.cellId) { + const newVMs = [...arg.prevState.cellVMs]; + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + const oldVM = arg.prevState.cellVMs[index]; + newVMs[index] = Creation.alterCellVM({ ...oldVM }, arg.prevState.settings, true, !oldVM.inputBlockOpen); + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + return arg.prevState; + } + + export function updateSettings(arg: InteractiveReducerArg): IMainState { + // String arg should be the IDataScienceExtraSettings + const newSettingsJSON = JSON.parse(arg.payload.data); + const newSettings = newSettingsJSON; + const newEditorOptions = computeEditorOptions(newSettings); + const newFontFamily = newSettings.extraSettings + ? newSettings.extraSettings.editor.fontFamily + : arg.prevState.font.family; + const newFontSize = newSettings.extraSettings + ? newSettings.extraSettings.editor.fontSize + : arg.prevState.font.size; + + // Ask for new theme data if necessary + if ( + newSettings && + newSettings.extraSettings && + newSettings.extraSettings.theme !== arg.prevState.vscodeThemeName + ) { + const knownDark = Helpers.computeKnownDark(newSettings); + // User changed the current theme. Rerender + postActionToExtension(arg, CssMessages.GetCssRequest, { isDark: knownDark }); + postActionToExtension(arg, CssMessages.GetMonacoThemeRequest, { isDark: knownDark }); + } + + // Update our input cell state if the user changed this setting + let newVMs = arg.prevState.cellVMs; + if (newSettings.showCellInputCode !== arg.prevState.settings?.showCellInputCode) { + newVMs = arg.prevState.cellVMs.map((c) => + Creation.alterCellVM( + c, + newSettings, + newSettings.showCellInputCode, + !newSettings.collapseCellInputCodeByDefault + ) + ); + } + + return { + ...arg.prevState, + cellVMs: newVMs, + settings: newSettings, + editorOptions: newEditorOptions, + font: { + size: newFontSize, + family: newFontFamily + } + }; + } + + export function scrollToCell(arg: InteractiveReducerArg): IMainState { + // Up the scroll count on the necessary cell + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.id); + if (index >= 0) { + const newVMs = [...arg.prevState.cellVMs]; + + // Scroll one cell and unscroll another. + newVMs[index] = { ...newVMs[index], scrollCount: newVMs[index].scrollCount + 1 }; + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + + return arg.prevState; + } + + export function scrolled(arg: InteractiveReducerArg): IMainState { + return { + ...arg.prevState, + isAtBottom: arg.payload.data.isAtBottom + }; + } + + export function clickCell(arg: InteractiveReducerArg): IMainState { + if ( + arg.payload.data.cellId === Identifiers.EditCellId && + arg.prevState.editCellVM && + !arg.prevState.editCellVM.focused + ) { + return { + ...arg.prevState, + editCellVM: { + ...arg.prevState.editCellVM, + focused: true + } + }; + } else if (arg.prevState.editCellVM) { + return { + ...arg.prevState, + editCellVM: { + ...arg.prevState.editCellVM, + focused: false + } + }; + } + + return arg.prevState; + } + + export function unfocusCell(arg: InteractiveReducerArg): IMainState { + if ( + arg.payload.data.cellId === Identifiers.EditCellId && + arg.prevState.editCellVM && + arg.prevState.editCellVM.focused + ) { + return { + ...arg.prevState, + editCellVM: { + ...arg.prevState.editCellVM, + focused: false + } + }; + } + + return arg.prevState; + } +} diff --git a/src/datascience-ui/history-react/redux/reducers/execution.ts b/src/datascience-ui/history-react/redux/reducers/execution.ts new file mode 100644 index 000000000000..1605cc79f8c5 --- /dev/null +++ b/src/datascience-ui/history-react/redux/reducers/execution.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +// tslint:disable-next-line: no-require-imports no-var-requires +const cloneDeep = require('lodash/cloneDeep'); +import * as uuid from 'uuid/v4'; + +import { CellMatcher } from '../../../../client/datascience/cellMatcher'; +import { InteractiveWindowMessages } from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { CellState } from '../../../../client/datascience/types'; +import { generateMarkdownFromCodeLines } from '../../../common'; +import { createCellFrom } from '../../../common/cellFactory'; +import { createCellVM, IMainState } from '../../../interactive-common/mainState'; +import { postActionToExtension } from '../../../interactive-common/redux/helpers'; +import { Helpers } from '../../../interactive-common/redux/reducers/helpers'; +import { ICodeAction } from '../../../interactive-common/redux/reducers/types'; +import { InteractiveReducerArg } from '../mapping'; +import { Creation } from './creation'; + +export namespace Execution { + export function undo(arg: InteractiveReducerArg): IMainState { + if (arg.prevState.undoStack.length > 0) { + // Pop one off of our undo stack and update our redo + const cells = arg.prevState.undoStack[arg.prevState.undoStack.length - 1]; + const undoStack = arg.prevState.undoStack.slice(0, arg.prevState.undoStack.length - 1); + const redoStack = Helpers.pushStack(arg.prevState.redoStack, arg.prevState.cellVMs); + postActionToExtension(arg, InteractiveWindowMessages.Undo); + return { + ...arg.prevState, + cellVMs: cells, + undoStack: undoStack, + redoStack: redoStack, + skipNextScroll: true + }; + } + + return arg.prevState; + } + + export function redo(arg: InteractiveReducerArg): IMainState { + if (arg.prevState.redoStack.length > 0) { + // Pop one off of our redo stack and update our undo + const cells = arg.prevState.redoStack[arg.prevState.redoStack.length - 1]; + const redoStack = arg.prevState.redoStack.slice(0, arg.prevState.redoStack.length - 1); + const undoStack = Helpers.pushStack(arg.prevState.undoStack, arg.prevState.cellVMs); + postActionToExtension(arg, InteractiveWindowMessages.Redo); + return { + ...arg.prevState, + cellVMs: cells, + undoStack: undoStack, + redoStack: redoStack, + skipNextScroll: true + }; + } + + return arg.prevState; + } + + export function startDebugging(arg: InteractiveReducerArg): IMainState { + return { + ...arg.prevState, + debugging: true + }; + } + + export function stopDebugging(arg: InteractiveReducerArg): IMainState { + return { + ...arg.prevState, + debugging: false + }; + } + + export function submitInput(arg: InteractiveReducerArg): IMainState { + // noop if the submitted code is just a cell marker + const matcher = new CellMatcher(arg.prevState.settings); + if (matcher.stripFirstMarker(arg.payload.data.code).length > 0 && arg.prevState.editCellVM) { + // This should be from the edit cell VM. Copy it and change the cell id + let newCell = cloneDeep(arg.prevState.editCellVM); + + // Change this editable cell to not editable. + newCell.cell.state = CellState.executing; + newCell.cell.data.source = arg.payload.data.code; + + // Change type to markdown if necessary + const split = arg.payload.data.code.splitLines({ trim: false }); + const firstLine = split[0]; + if (matcher.isMarkdown(firstLine)) { + newCell.cell.data = createCellFrom(newCell.cell.data, 'markdown'); + newCell.cell.data.source = generateMarkdownFromCodeLines(split); + newCell.cell.state = CellState.finished; + } else if (newCell.cell.data.cell_type === 'markdown') { + newCell.cell.state = CellState.finished; + } + + // Update input controls (always show expanded since we just edited it.) + newCell = createCellVM(newCell.cell, arg.prevState.settings, false, false); + const collapseInputs = arg.prevState.settings + ? arg.prevState.settings.collapseCellInputCodeByDefault + : false; + newCell = Creation.alterCellVM(newCell, arg.prevState.settings, true, !collapseInputs); + newCell.useQuickEdit = false; + + // Generate a new id + newCell.cell.id = uuid(); + + // Indicate this is direct input so that we don't hide it if the user has + // hide all inputs turned on. + newCell.directInput = true; + + // Send a message to execute this code if necessary. + if (newCell.cell.state !== CellState.finished) { + postActionToExtension(arg, InteractiveWindowMessages.SubmitNewCell, { + code: arg.payload.data.code, + id: newCell.cell.id + }); + } + + // Stick in a new cell at the bottom that's editable and update our state + // so that the last cell becomes busy + return { + ...arg.prevState, + cellVMs: [...arg.prevState.cellVMs, newCell], + undoStack: Helpers.pushStack(arg.prevState.undoStack, arg.prevState.cellVMs), + skipNextScroll: false, + submittedText: true + }; + } + return arg.prevState; + } +} diff --git a/src/datascience-ui/history-react/redux/reducers/index.ts b/src/datascience-ui/history-react/redux/reducers/index.ts new file mode 100644 index 000000000000..4cf821b85bd0 --- /dev/null +++ b/src/datascience-ui/history-react/redux/reducers/index.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { InteractiveWindowMessages } from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { CssMessages, SharedMessages } from '../../../../client/datascience/messages'; +import { CommonEffects } from '../../../interactive-common/redux/reducers/commonEffects'; +import { Kernel } from '../../../interactive-common/redux/reducers/kernel'; +import { Transfer } from '../../../interactive-common/redux/reducers/transfer'; +import { CommonActionType } from '../../../interactive-common/redux/reducers/types'; +import { IInteractiveActionMapping } from '../mapping'; +import { Creation } from './creation'; +import { Effects } from './effects'; +import { Execution } from './execution'; + +// The list of reducers. 1 per message/action. +export const reducerMap: Partial = { + // State updates + [CommonActionType.RESTART_KERNEL]: Kernel.restartKernel, + [CommonActionType.INTERRUPT_KERNEL]: Kernel.interruptKernel, + [InteractiveWindowMessages.SelectKernel]: Kernel.selectKernel, + [CommonActionType.SELECT_SERVER]: Kernel.selectJupyterURI, + [CommonActionType.OPEN_SETTINGS]: CommonEffects.openSettings, + [CommonActionType.EXPORT]: Transfer.exportCells, + [CommonActionType.EXPORT_NOTEBOOK_AS]: Transfer.showExportAsMenu, + [CommonActionType.SAVE]: Transfer.save, + [CommonActionType.SHOW_DATA_VIEWER]: Transfer.showDataViewer, + [CommonActionType.DELETE_CELL]: Creation.deleteCell, + [InteractiveWindowMessages.ShowPlot]: Transfer.showPlot, + [CommonActionType.LINK_CLICK]: Transfer.linkClick, + [CommonActionType.GOTO_CELL]: Transfer.gotoCell, + [CommonActionType.TOGGLE_INPUT_BLOCK]: Effects.toggleInputBlock, + [CommonActionType.COPY_CELL_CODE]: Transfer.copyCellCode, + [CommonActionType.GATHER_CELL]: Transfer.gather, + [CommonActionType.GATHER_CELL_TO_SCRIPT]: Transfer.gatherToScript, + [CommonActionType.EDIT_CELL]: Transfer.editCell, + [CommonActionType.SUBMIT_INPUT]: Execution.submitInput, + [InteractiveWindowMessages.ExpandAll]: Effects.expandAll, + [CommonActionType.EDITOR_LOADED]: Transfer.started, + [InteractiveWindowMessages.LoadAllCells]: Creation.loaded, + [CommonActionType.SCROLL]: Effects.scrolled, + [CommonActionType.CLICK_CELL]: Effects.clickCell, + [CommonActionType.UNFOCUS_CELL]: Effects.unfocusCell, + [CommonActionType.UNMOUNT]: Creation.unmount, + [CommonActionType.FOCUS_INPUT]: CommonEffects.focusInput, + [CommonActionType.LOAD_IPYWIDGET_CLASS_SUCCESS]: CommonEffects.handleLoadIPyWidgetClassSuccess, + [CommonActionType.LOAD_IPYWIDGET_CLASS_FAILURE]: CommonEffects.handleLoadIPyWidgetClassFailure, + [CommonActionType.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED]: CommonEffects.notifyAboutUnsupportedWidgetVersions, + [CommonActionType.IPYWIDGET_RENDER_FAILURE]: CommonEffects.handleIPyWidgetRenderFailure, + + // Messages from the webview (some are ignored) + [InteractiveWindowMessages.Undo]: Execution.undo, + [InteractiveWindowMessages.Redo]: Execution.redo, + [InteractiveWindowMessages.StartCell]: Creation.startCell, + [InteractiveWindowMessages.FinishCell]: Creation.finishCell, + [InteractiveWindowMessages.UpdateCellWithExecutionResults]: Creation.updateCell, + [InteractiveWindowMessages.Activate]: CommonEffects.activate, + [InteractiveWindowMessages.RestartKernel]: Kernel.handleRestarted, + [CssMessages.GetCssResponse]: CommonEffects.handleCss, + [InteractiveWindowMessages.MonacoReady]: CommonEffects.monacoReady, + [CssMessages.GetMonacoThemeResponse]: CommonEffects.monacoThemeChange, + [InteractiveWindowMessages.GetAllCells]: Transfer.getAllCells, + [InteractiveWindowMessages.ExpandAll]: Effects.expandAll, + [InteractiveWindowMessages.CollapseAll]: Effects.collapseAll, + [InteractiveWindowMessages.DeleteAllCells]: Creation.deleteAllCells, + [InteractiveWindowMessages.StartProgress]: CommonEffects.startProgress, + [InteractiveWindowMessages.StopProgress]: CommonEffects.stopProgress, + [SharedMessages.UpdateSettings]: Effects.updateSettings, + [InteractiveWindowMessages.StartDebugging]: Execution.startDebugging, + [InteractiveWindowMessages.StopDebugging]: Execution.stopDebugging, + [InteractiveWindowMessages.ScrollToCell]: Effects.scrollToCell, + [InteractiveWindowMessages.UpdateKernel]: Kernel.updateStatus, + [SharedMessages.LocInit]: CommonEffects.handleLocInit, + [InteractiveWindowMessages.UpdateDisplayData]: CommonEffects.handleUpdateDisplayData, + [InteractiveWindowMessages.HasCell]: Transfer.hasCell, + [InteractiveWindowMessages.Gathering]: Transfer.gathering +}; diff --git a/src/datascience-ui/history-react/redux/store.ts b/src/datascience-ui/history-react/redux/store.ts new file mode 100644 index 000000000000..c9b6551d26e5 --- /dev/null +++ b/src/datascience-ui/history-react/redux/store.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as ReduxCommon from '../../interactive-common/redux/store'; +import { PostOffice } from '../../react-common/postOffice'; +import { reducerMap } from './reducers'; + +// This special version uses the reducer map from the IInteractiveWindowMapping +export function createStore(skipDefault: boolean, baseTheme: string, testMode: boolean, postOffice: PostOffice) { + return ReduxCommon.createStore(skipDefault, baseTheme, testMode, false, false, reducerMap, postOffice); +} diff --git a/src/datascience-ui/interactive-common/buildSettingsCss.ts b/src/datascience-ui/interactive-common/buildSettingsCss.ts new file mode 100644 index 000000000000..68a2eaa0e89e --- /dev/null +++ b/src/datascience-ui/interactive-common/buildSettingsCss.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { IDataScienceExtraSettings } from '../../client/datascience/types'; + +// From a set of data science settings build up any settings that need to be +// inserted into our StyleSetter divs some things like pseudo elements +// can't be put into inline styles +export function buildSettingsCss(settings: IDataScienceExtraSettings | undefined): string { + return settings + ? `#main-panel-content::-webkit-scrollbar { + width: ${settings.extraSettings.editor.verticalScrollbarSize}px; +} + +.cell-output::-webkit-scrollbar { + height: ${settings.extraSettings.editor.horizontalScrollbarSize}px; +} + +.cell-output > *::-webkit-scrollbar { + width: ${settings.extraSettings.editor.verticalScrollbarSize}px; +}` + : ''; +} diff --git a/src/datascience-ui/interactive-common/cellInput.tsx b/src/datascience-ui/interactive-common/cellInput.tsx new file mode 100644 index 000000000000..89cb3c2f1645 --- /dev/null +++ b/src/datascience-ui/interactive-common/cellInput.tsx @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../client/common/extensions'; + +import { nbformat } from '@jupyterlab/coreutils'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as React from 'react'; + +import { concatMultilineString } from '../common'; +import { IKeyboardEvent } from '../react-common/event'; +import { IMonacoModelContentChangeEvent } from '../react-common/monacoHelpers'; +import { Code } from './code'; +import { InputHistory } from './inputHistory'; +import { ICellViewModel, IFont } from './mainState'; +import { Markdown } from './markdown'; + +// tslint:disable-next-line: no-require-importss +interface ICellInputProps { + cellVM: ICellViewModel; + language: string; + codeVersion: number; + codeTheme: string; + testMode?: boolean; + history: InputHistory | undefined; + showWatermark: boolean; + monacoTheme: string | undefined; + editorOptions?: monacoEditor.editor.IEditorOptions; + editorMeasureClassName?: string; + showLineNumbers?: boolean; + font: IFont; + disableUndoStack: boolean; + isNotebookTrusted: boolean; + /** + * Only used in interactive window. + */ + focusPending: number; + onCodeChange(e: IMonacoModelContentChangeEvent): void; + onCodeCreated(code: string, file: string, cellId: string, modelId: string): void; + openLink(uri: monacoEditor.Uri): void; + keyDown?(cellId: string, e: IKeyboardEvent): void; + focused?(cellId: string): void; + unfocused?(cellId: string): void; +} + +// tslint:disable: react-this-binding-issue +export class CellInput extends React.Component { + private codeRef: React.RefObject = React.createRef(); + private markdownRef: React.RefObject = React.createRef(); + + constructor(prop: ICellInputProps) { + super(prop); + } + + public render() { + if (this.isCodeCell()) { + return this.renderCodeInputs(); + } else { + return this.renderMarkdownInputs(); + } + } + + public getContents(): string | undefined { + if (this.codeRef.current) { + return this.codeRef.current.getContents(); + } else if (this.markdownRef.current) { + return this.markdownRef.current.getContents(); + } + } + + private isCodeCell = () => { + return this.props.cellVM.cell.data.cell_type === 'code'; + }; + + private isMarkdownCell = () => { + return this.props.cellVM.cell.data.cell_type === 'markdown'; + }; + + private getMarkdownCell = () => { + return this.props.cellVM.cell.data as nbformat.IMarkdownCell; + }; + + private shouldRenderCodeEditor = (): boolean => { + return this.isCodeCell() && (this.props.cellVM.inputBlockShow || this.props.cellVM.editable); + }; + + private shouldRenderMarkdownEditor = (): boolean => { + return this.isMarkdownCell(); + }; + + private getRenderableInputCode = (): string => { + return this.props.cellVM.inputBlockText; + }; + + private renderCodeInputs = () => { + if (this.shouldRenderCodeEditor()) { + return ( +
+ +
+ ); + } + + return null; + }; + + private renderMarkdownInputs = () => { + if (this.shouldRenderMarkdownEditor()) { + const source = concatMultilineString(this.getMarkdownCell().source); + return ( +
+ +
+ ); + } + + return null; + }; + + private onKeyDown = (e: IKeyboardEvent) => { + if (this.props.keyDown) { + this.props.keyDown(this.props.cellVM.cell.id, e); + } + }; + + private onCodeFocused = () => { + if (this.props.focused) { + this.props.focused(this.props.cellVM.cell.id); + } + }; + + private onCodeUnfocused = () => { + if (this.props.unfocused) { + this.props.unfocused(this.props.cellVM.cell.id); + } + }; + + private onMarkdownFocused = () => { + if (this.props.focused) { + this.props.focused(this.props.cellVM.cell.id); + } + }; + + private onMarkdownUnfocused = () => { + if (this.props.unfocused) { + this.props.unfocused(this.props.cellVM.cell.id); + } + }; + + private onCodeCreated = (code: string, modelId: string) => { + this.props.onCodeCreated(code, this.props.cellVM.cell.file, this.props.cellVM.cell.id, modelId); + }; + + private getIpLine(): number | undefined { + if (this.props.cellVM.currentStack && this.props.cellVM.currentStack.length > 0) { + return this.props.cellVM.currentStack[0].line - 1; + } + } +} diff --git a/src/datascience-ui/interactive-common/cellOutput.tsx b/src/datascience-ui/interactive-common/cellOutput.tsx new file mode 100644 index 000000000000..b48cc8d69cfd --- /dev/null +++ b/src/datascience-ui/interactive-common/cellOutput.tsx @@ -0,0 +1,595 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { nbformat } from '@jupyterlab/coreutils'; +import { JSONObject } from '@phosphor/coreutils'; +import ansiRegex from 'ansi-regex'; +import * as fastDeepEqual from 'fast-deep-equal'; +import * as React from 'react'; +import '../../client/common/extensions'; +import { Identifiers } from '../../client/datascience/constants'; +import { CellState } from '../../client/datascience/types'; +import { ClassType } from '../../client/ioc/types'; +import { WidgetManager } from '../ipywidgets'; +import { Image, ImageName } from '../react-common/image'; +import { ImageButton } from '../react-common/imageButton'; +import { getLocString } from '../react-common/locReactSide'; +import { ICellViewModel } from './mainState'; +import { fixMarkdown } from './markdownManipulation'; +import { getRichestMimetype, getTransform, isIPyWidgetOutput, isMimeTypeSupported } from './transforms'; + +// tslint:disable-next-line: no-var-requires no-require-imports +const ansiToHtml = require('ansi-to-html'); + +// tslint:disable-next-line: no-require-imports no-var-requires +const cloneDeep = require('lodash/cloneDeep'); +import { Widget } from '@phosphor/widgets'; +import { noop } from '../../client/common/utils/misc'; +import { WIDGET_MIMETYPE } from '../../client/datascience/ipywidgets/constants'; +import { concatMultilineString } from '../common'; +import { TrimmedOutputMessage } from './trimmedOutputLink'; + +interface ICellOutputProps { + cellVM: ICellViewModel; + baseTheme: string; + maxTextSize?: number; + enableScroll?: boolean; + hideOutput?: boolean; + themeMatplotlibPlots?: boolean; + expandImage(imageHtml: string): void; + widgetFailed(ex: Error): void; + openSettings(settings?: string): void; +} + +interface ICellOutputData { + mimeType: string; + data: nbformat.MultilineString | JSONObject; + mimeBundle: nbformat.IMimeBundle; + renderWithScrollbars: boolean; + isText: boolean; + isError: boolean; +} + +interface ICellOutput { + output: ICellOutputData; + extraButton: JSX.Element | null; // Extra button for plot viewing is stored here + outputSpanClassName?: string; // Wrap this output in a span with the following className, undefined to not wrap + doubleClick(): void; // Double click handler for plot viewing is stored here +} +// tslint:disable: react-this-binding-issue +export class CellOutput extends React.Component { + // tslint:disable-next-line: no-any + private static get ansiToHtmlClass(): ClassType { + if (!CellOutput.ansiToHtmlClass_ctor) { + // ansiToHtml is different between the tests running and webpack. figure out which one + if (ansiToHtml instanceof Function) { + CellOutput.ansiToHtmlClass_ctor = ansiToHtml; + } else { + CellOutput.ansiToHtmlClass_ctor = ansiToHtml.default; + } + } + return CellOutput.ansiToHtmlClass_ctor!; + } + // tslint:disable-next-line: no-any + private static ansiToHtmlClass_ctor: ClassType | undefined; + private ipyWidgetRef: React.RefObject; + private renderedViews = new Map>(); + private widgetManager: WidgetManager | undefined; + // tslint:disable-next-line: no-any + constructor(prop: ICellOutputProps) { + super(prop); + this.ipyWidgetRef = React.createRef(); + } + private static getAnsiToHtmlOptions(): { fg: string; bg: string; colors: string[] } { + // Here's the default colors for ansiToHtml. We need to use the + // colors from our current theme. + // const colors = { + // 0: '#000', + // 1: '#A00', + // 2: '#0A0', + // 3: '#A50', + // 4: '#00A', + // 5: '#A0A', + // 6: '#0AA', + // 7: '#AAA', + // 8: '#555', + // 9: '#F55', + // 10: '#5F5', + // 11: '#FF5', + // 12: '#55F', + // 13: '#F5F', + // 14: '#5FF', + // 15: '#FFF' + // }; + return { + fg: 'var(--vscode-terminal-foreground)', + bg: 'var(--vscode-terminal-background)', + colors: [ + 'var(--vscode-terminal-ansiBlack)', // 0 + 'var(--vscode-terminal-ansiBrightRed)', // 1 + 'var(--vscode-terminal-ansiGreen)', // 2 + 'var(--vscode-terminal-ansiYellow)', // 3 + 'var(--vscode-terminal-ansiBrightBlue)', // 4 + 'var(--vscode-terminal-ansiMagenta)', // 5 + 'var(--vscode-terminal-ansiCyan)', // 6 + 'var(--vscode-terminal-ansiBrightBlack)', // 7 + 'var(--vscode-terminal-ansiWhite)', // 8 + 'var(--vscode-terminal-ansiRed)', // 9 + 'var(--vscode-terminal-ansiBrightGreen)', // 10 + 'var(--vscode-terminal-ansiBrightYellow)', // 11 + 'var(--vscode-terminal-ansiBlue)', // 12 + 'var(--vscode-terminal-ansiBrightMagenta)', // 13 + 'var(--vscode-terminal-ansiBrightCyan)', // 14 + 'var(--vscode-terminal-ansiBrightWhite)' // 15 + ] + }; + } + public render() { + // Only render results if not an edit cell + if (this.props.cellVM.cell.id !== Identifiers.EditCellId) { + const outputClassNames = this.isCodeCell() + ? `cell-output cell-output-${this.props.baseTheme}` + : 'markdown-cell-output-container'; + + // Then combine them inside a div. IPyWidget ref has to be separate so we don't end up + // with a div in the way. If we try setting all div's background colors, we break + // some widgets + return ( +
+ {this.renderResults()} +
+
+ ); + } + return null; + } + public componentWillUnmount() { + this.destroyIPyWidgets(); + } + public componentDidMount() { + if (!this.isCodeCell() || !this.hasOutput() || !this.getCodeCell().outputs || this.props.hideOutput) { + return; + } + } + // tslint:disable-next-line: max-func-body-length + public componentDidUpdate(prevProps: ICellOutputProps) { + if (!this.isCodeCell() || !this.hasOutput() || !this.getCodeCell().outputs || this.props.hideOutput) { + return; + } + if (fastDeepEqual(this.props, prevProps)) { + return; + } + // Check if outupt has changed. + if ( + prevProps.cellVM.cell.data.cell_type === 'code' && + prevProps.cellVM.cell.state === this.getCell()!.state && + prevProps.hideOutput === this.props.hideOutput && + fastDeepEqual(this.props.cellVM.cell.data, prevProps.cellVM.cell.data) + ) { + return; + } + } + + public shouldComponentUpdate( + nextProps: Readonly, + _nextState: Readonly, + // tslint:disable-next-line: no-any + _nextContext: any + ): boolean { + if (nextProps === this.props) { + return false; + } + if (nextProps.baseTheme !== this.props.baseTheme) { + return true; + } + if (nextProps.maxTextSize !== this.props.maxTextSize) { + return true; + } + if (nextProps.themeMatplotlibPlots !== this.props.themeMatplotlibPlots) { + return true; + } + // If they are the same, then nothing has changed. + // Note, we're using redux, hence we'll never have the same reference object with different property values. + if (nextProps.cellVM === this.props.cellVM) { + return false; + } + if (nextProps.cellVM.cell.data.cell_type !== this.props.cellVM.cell.data.cell_type) { + return true; + } + if (nextProps.cellVM.cell.state !== this.props.cellVM.cell.state) { + return true; + } + if (nextProps.cellVM.cell.data.outputs !== this.props.cellVM.cell.data.outputs) { + return true; + } + if (nextProps.cellVM.uiSideError !== this.props.cellVM.uiSideError) { + return true; + } + if ( + !this.isCodeCell() && + nextProps.cellVM.cell.id !== Identifiers.EditCellId && + nextProps.cellVM.cell.data.source !== this.props.cellVM.cell.data.source + ) { + return true; + } + + return false; + } + // Public for testing + public getUnknownMimeTypeFormatString() { + return getLocString('DataScience.unknownMimeTypeFormat', 'Unknown Mime Type'); + } + private destroyIPyWidgets() { + this.renderedViews.forEach((viewPromise) => { + viewPromise.then((v) => v?.dispose()).ignoreErrors(); + }); + this.renderedViews.clear(); + } + + private getCell = () => { + return this.props.cellVM.cell; + }; + + private isCodeCell = () => { + return this.props.cellVM.cell.data.cell_type === 'code'; + }; + + private hasOutput = () => { + return ( + this.getCell().state === CellState.finished || + this.getCell().state === CellState.error || + this.getCell().state === CellState.executing + ); + }; + + private getCodeCell = () => { + return this.props.cellVM.cell.data as nbformat.ICodeCell; + }; + + private getMarkdownCell = () => { + return this.props.cellVM.cell.data as nbformat.IMarkdownCell; + }; + + private renderResults = (): JSX.Element[] => { + // Results depend upon the type of cell + if (this.isCodeCell()) { + return ( + this.renderCodeOutputs() + .filter((item) => !!item) + // tslint:disable-next-line: no-any + .map((item) => (item as any) as JSX.Element) + ); + } else if (this.props.cellVM.cell.id !== Identifiers.EditCellId) { + return this.renderMarkdownOutputs(); + } else { + return []; + } + }; + + private renderCodeOutputs = () => { + // return []; + if (this.isCodeCell() && this.hasOutput() && this.getCodeCell().outputs && !this.props.hideOutput) { + const trim = this.props.cellVM.cell.data.metadata.tags ? this.props.cellVM.cell.data.metadata.tags[0] : ''; + // Render the outputs + const outputs = this.renderOutputs(this.getCodeCell().outputs, trim); + + // Render any UI side errors + // tslint:disable: react-no-dangerous-html + if (this.props.cellVM.uiSideError) { + outputs.push( +
+
+
+ ); + } + + return outputs; + } + return []; + }; + + private renderMarkdownOutputs = () => { + const markdown = this.getMarkdownCell(); + // React-markdown expects that the source is a string + const source = fixMarkdown(concatMultilineString(markdown.source)); + const Transform = getTransform('text/markdown'); + const MarkdownClassName = 'markdown-cell-output'; + + return [ +
+ +
+ ]; + }; + + private computeOutputData(output: nbformat.IOutput): ICellOutputData { + let isText = false; + let isError = false; + let mimeType = 'text/plain'; + let input = output.data; + let renderWithScrollbars = false; + + // Special case for json. Just turn into a string + if (input && input.hasOwnProperty('application/json')) { + input = JSON.stringify(output.data); + renderWithScrollbars = true; + isText = true; + } else if (output.output_type === 'stream') { + // Stream output needs to be wrapped in xmp so it + // show literally. Otherwise < chars start a new html element. + mimeType = 'text/html'; + isText = true; + isError = false; + renderWithScrollbars = true; + // Sonar is wrong, TS won't compile without this AS + const stream = output as nbformat.IStream; // NOSONAR + const formatted = concatMultilineString(stream.text); + input = { + 'text/html': formatted.includes('<') ? `${formatted}` : `
${formatted}
` + }; + + // Output may have goofy ascii colorization chars in it. Try + // colorizing if we don't have html that needs around it (ex. <type ='string'>) + try { + if (ansiRegex().test(formatted)) { + const converter = new CellOutput.ansiToHtmlClass(CellOutput.getAnsiToHtmlOptions()); + const html = converter.toHtml(formatted); + input = { + 'text/html': html + }; + } + } catch { + noop(); + } + } else if (output.output_type === 'error') { + mimeType = 'text/html'; + isText = true; + isError = true; + renderWithScrollbars = true; + // Sonar is wrong, TS won't compile without this AS + const error = output as nbformat.IError; // NOSONAR + try { + const converter = new CellOutput.ansiToHtmlClass(CellOutput.getAnsiToHtmlOptions()); + const trace = error.traceback.length ? converter.toHtml(error.traceback.join('\n')) : error.evalue; + input = { + 'text/html': trace + }; + } catch { + // This can fail during unit tests, just use the raw data + input = { + 'text/html': error.evalue + }; + } + } else if (input) { + // Compute the mime type + mimeType = getRichestMimetype(input); + isText = mimeType === 'text/plain'; + } + + // Then parse the mime type + const mimeBundle = input as nbformat.IMimeBundle; // NOSONAR + let data: nbformat.MultilineString | JSONObject = mimeBundle[mimeType]; + + // For un-executed output we might get text or svg output as multiline string arrays + // we want to concat those so we don't display a bunch of weird commas as we expect + // Single strings in our output + if (Array.isArray(data)) { + data = concatMultilineString(data as nbformat.MultilineString, true); + } + + // Fixup latex to make sure it has the requisite $$ around it + if (mimeType === 'text/latex') { + data = fixMarkdown(concatMultilineString(data as nbformat.MultilineString, true), true); + } + + return { + isText, + isError, + renderWithScrollbars, + data: data, + mimeType, + mimeBundle + }; + } + + private transformOutput(output: nbformat.IOutput): ICellOutput { + // First make a copy of the outputs. + const copy = cloneDeep(output); + + // Then compute the data + const data = this.computeOutputData(copy); + let extraButton: JSX.Element | null = null; + + // Then parse the mime type + try { + // Text based mimeTypes don't get a white background + if (/^text\//.test(data.mimeType)) { + return { + output: data, + extraButton, + doubleClick: noop + }; + } else if (data.mimeType === 'image/svg+xml' || data.mimeType === 'image/png') { + // If we have a png or svg enable the plot viewer button + // There should be two mime bundles. Well if enablePlotViewer is turned on. See if we have both + const svg = data.mimeBundle['image/svg+xml']; + const png = data.mimeBundle['image/png']; + const buttonTheme = this.props.themeMatplotlibPlots ? this.props.baseTheme : 'vscode-light'; + let doubleClick: () => void = noop; + if (svg && png) { + // Save the svg in the extra button. + const openClick = () => { + this.props.expandImage(svg.toString()); + }; + extraButton = ( + <div className="plot-open-button"> + <ImageButton + baseTheme={buttonTheme} + tooltip={getLocString('DataScience.plotOpen', 'Expand image')} + onClick={openClick} + > + <Image baseTheme={buttonTheme} class="image-button-image" image={ImageName.OpenPlot} /> + </ImageButton> + </div> + ); + + // Switch the data to the png + data.data = png; + data.mimeType = 'image/png'; + + // Switch double click to do the same thing as the extra button + doubleClick = openClick; + } + + // return the image + // If not theming plots then wrap in a span + return { + output: data, + extraButton, + doubleClick, + outputSpanClassName: this.props.themeMatplotlibPlots ? undefined : 'cell-output-plot-background' + }; + } else { + // For anything else just return it with a white plot background. This lets stuff like vega look good in + // dark mode + return { + output: data, + extraButton, + doubleClick: noop, + outputSpanClassName: this.props.themeMatplotlibPlots ? undefined : 'cell-output-plot-background' + }; + } + } catch (e) { + return { + output: { + data: e.toString(), + isText: true, + isError: false, + renderWithScrollbars: false, + mimeType: 'text/plain', + mimeBundle: {} + }, + extraButton: null, + doubleClick: noop + }; + } + } + + // tslint:disable-next-line: max-func-body-length + private renderOutputs(outputs: nbformat.IOutput[], trim: string): JSX.Element[] { + return [this.renderOutput(outputs, trim)]; + } + + private renderOutput = (outputs: nbformat.IOutput[], trim: string): JSX.Element => { + const buffer: JSX.Element[] = []; + const transformedList = outputs.map(this.transformOutput.bind(this)); + + transformedList.forEach((transformed, index) => { + const mimeType = transformed.output.mimeType; + if (isIPyWidgetOutput(transformed.output.mimeBundle)) { + // Create a view for this output if not already there. + this.renderWidget(transformed.output); + } else if (mimeType && isMimeTypeSupported(mimeType)) { + // If that worked, use the transform + // Get the matching React.Component for that mimetype + const Transform = getTransform(mimeType); + + let className = transformed.output.isText ? 'cell-output-text' : 'cell-output-html'; + className = transformed.output.isError ? `${className} cell-output-error` : className; + + // If we are not theming plots then wrap them in a white span + if (transformed.outputSpanClassName) { + buffer.push( + <div role="group" key={index} onDoubleClick={transformed.doubleClick} className={className}> + <span className={transformed.outputSpanClassName}> + {transformed.extraButton} + <Transform data={transformed.output.data} /> + </span> + </div> + ); + } else { + if (trim === 'outputPrepend') { + buffer.push( + <div role="group" key={index} onDoubleClick={transformed.doubleClick} className={className}> + {transformed.extraButton} + <TrimmedOutputMessage openSettings={this.props.openSettings} /> + <Transform data={transformed.output.data} /> + </div> + ); + } else { + buffer.push( + <div role="group" key={index} onDoubleClick={transformed.doubleClick} className={className}> + {transformed.extraButton} + <Transform data={transformed.output.data} /> + </div> + ); + } + } + } else if ( + !mimeType || + mimeType.startsWith('application/scrapbook.scrap.') || + mimeType.startsWith('application/aml') + ) { + // Silently skip rendering of these mime types, render an empty div so the user sees the cell was executed. + buffer.push(<div key={index}></div>); + } else { + const str: string = this.getUnknownMimeTypeFormatString().format(mimeType); + buffer.push(<div key={index}>{str}</div>); + } + }); + + // Create a default set of properties + const style: React.CSSProperties = {}; + + // Create a scrollbar style if necessary + if (transformedList.some((transformed) => transformed.output.renderWithScrollbars) && this.props.enableScroll) { + style.overflowY = 'auto'; + style.maxHeight = `${this.props.maxTextSize}px`; + } + + return ( + <div key={0} style={style}> + {buffer} + </div> + ); + }; + + private renderWidget(widgetOutput: ICellOutputData) { + // Create a view for this widget if we haven't already + // tslint:disable-next-line: no-any + const widgetData: any = widgetOutput.mimeBundle[WIDGET_MIMETYPE]; + if (widgetData.model_id) { + if (!this.renderedViews.has(widgetData.model_id)) { + this.renderedViews.set(widgetData.model_id, this.createWidgetView(widgetData)); + } + } + } + + private async getWidgetManager() { + if (!this.widgetManager) { + const wm: WidgetManager | undefined = await new Promise((resolve) => + WidgetManager.instance.subscribe(resolve) + ); + this.widgetManager = wm; + if (wm) { + const oldDispose = wm.dispose.bind(wm); + wm.dispose = () => { + this.renderedViews.clear(); + this.widgetManager = undefined; + return oldDispose(); + }; + } + } + return this.widgetManager; + } + + private async createWidgetView(widgetData: nbformat.IMimeBundle & { model_id: string; version_major: number }) { + const wm = await this.getWidgetManager(); + const element = this.ipyWidgetRef.current!; + try { + return await wm?.renderWidget(widgetData, element); + } catch (ex) { + this.props.widgetFailed(ex); + } + } +} diff --git a/src/datascience-ui/interactive-common/code.tsx b/src/datascience-ui/interactive-common/code.tsx new file mode 100644 index 000000000000..dd8755e8cec6 --- /dev/null +++ b/src/datascience-ui/interactive-common/code.tsx @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as React from 'react'; + +import { PYTHON_LANGUAGE } from '../../client/common/constants'; +import { InputHistory } from '../interactive-common/inputHistory'; +import { IKeyboardEvent } from '../react-common/event'; +import { getLocString } from '../react-common/locReactSide'; +import { IMonacoModelContentChangeEvent } from '../react-common/monacoHelpers'; +import { Editor } from './editor'; +import { CursorPos, IFont } from './mainState'; + +export interface ICodeProps { + code: string; + language: string | undefined; + version: number; + codeTheme: string; + testMode: boolean; + readOnly: boolean; + history: InputHistory | undefined; + showWatermark: boolean; + monacoTheme: string | undefined; + outermostParentClass: string; + editorOptions?: monacoEditor.editor.IEditorOptions; + editorMeasureClassName?: string; + showLineNumbers?: boolean; + useQuickEdit?: boolean; + font: IFont; + hasFocus: boolean; + cursorPos: CursorPos | monacoEditor.IPosition; + disableUndoStack: boolean; + focusPending: number; + ipLocation: number | undefined; + onCreated(code: string, modelId: string): void; + onChange(e: IMonacoModelContentChangeEvent): void; + openLink(uri: monacoEditor.Uri): void; + keyDown?(e: IKeyboardEvent): void; + focused?(): void; + unfocused?(): void; +} + +interface ICodeState { + allowWatermark: boolean; +} + +export class Code extends React.Component<ICodeProps, ICodeState> { + private editorRef: React.RefObject<Editor> = React.createRef<Editor>(); + + constructor(prop: ICodeProps) { + super(prop); + this.state = { allowWatermark: true }; + } + + public componentDidUpdate(prevProps: ICodeProps) { + if (prevProps.focusPending !== this.props.focusPending) { + this.giveFocus(CursorPos.Current); + } + } + + public render() { + const readOnly = this.props.readOnly; + const waterMarkClass = + this.props.showWatermark && this.state.allowWatermark && !readOnly ? 'code-watermark' : 'hide'; + const classes = readOnly ? 'code-area' : 'code-area code-area-editable'; + + return ( + <div className={classes}> + <Editor + codeTheme={this.props.codeTheme} + readOnly={readOnly} + history={this.props.history} + onCreated={this.props.onCreated} + onChange={this.onModelChanged} + testMode={this.props.testMode} + content={this.props.code} + outermostParentClass={this.props.outermostParentClass} + monacoTheme={this.props.monacoTheme} + language={this.props.language ? this.props.language : PYTHON_LANGUAGE} + editorOptions={this.props.editorOptions} + openLink={this.props.openLink} + ref={this.editorRef} + editorMeasureClassName={this.props.editorMeasureClassName} + keyDown={this.props.keyDown} + hasFocus={this.props.hasFocus} + cursorPos={this.props.cursorPos} + focused={this.props.focused} + unfocused={this.props.unfocused} + showLineNumbers={this.props.showLineNumbers} + useQuickEdit={this.props.useQuickEdit} + font={this.props.font} + disableUndoStack={this.props.disableUndoStack} + version={this.props.version} + ipLocation={this.props.ipLocation} + /> + <div className={waterMarkClass} role="textbox" onClick={this.clickWatermark}> + {this.getWatermarkString()} + </div> + </div> + ); + } + + public getContents(): string | undefined { + if (this.editorRef.current) { + return this.editorRef.current.getContents(); + } + } + + private giveFocus(cursorPos: CursorPos) { + if (this.editorRef && this.editorRef.current) { + this.editorRef.current.giveFocus(cursorPos); + } + } + + private clickWatermark = (ev: React.MouseEvent<HTMLDivElement>) => { + ev.stopPropagation(); + // Give focus to the editor + this.giveFocus(CursorPos.Current); + }; + + private getWatermarkString = (): string => { + return getLocString('DataScience.inputWatermark', 'Type code here and press shift-enter to run'); + }; + + private onModelChanged = (e: IMonacoModelContentChangeEvent) => { + if (!this.props.readOnly && e.model) { + this.setState({ allowWatermark: e.model.getValueLength() === 0 }); + } + this.props.onChange(e); + }; +} diff --git a/src/datascience-ui/interactive-common/collapseButton.tsx b/src/datascience-ui/interactive-common/collapseButton.tsx new file mode 100644 index 000000000000..2410ad9eb046 --- /dev/null +++ b/src/datascience-ui/interactive-common/collapseButton.tsx @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as React from 'react'; +import { getLocString } from '../react-common/locReactSide'; + +interface ICollapseButtonProps { + theme: string; + tooltip: string; + visible: boolean; + open: boolean; + label?: string; + onClick(): void; +} + +export class CollapseButton extends React.Component<ICollapseButtonProps> { + constructor(props: ICollapseButtonProps) { + super(props); + } + + public render() { + const collapseInputPolygonClassNames = `collapse-input-svg ${ + this.props.open ? ' collapse-input-svg-rotate' : '' + } collapse-input-svg-${this.props.theme}`; + const collapseInputClassNames = `collapse-input remove-style ${this.props.visible ? '' : ' invisible'}`; + const tooltip = this.props.open + ? getLocString('DataScience.collapseSingle', 'Collapse') + : getLocString('DataScience.expandSingle', 'Expand'); + const ariaExpanded = this.props.open ? 'true' : 'false'; + // https://reactjs.org/docs/conditional-rendering.html#inline-if-with-logical--operator + // Comment here just because the (boolean && statement) was new to me + return ( + <button + className={collapseInputClassNames} + title={tooltip} + onClick={this.props.onClick} + aria-expanded={ariaExpanded} + > + <svg version="1.1" baseProfile="full" width="8px" height="11px"> + <polygon points="0,0 0,10 5,5" className={collapseInputPolygonClassNames} fill="black" /> + </svg> + {this.props.label && <label className="collapseInputLabel">{this.props.label}</label>} + </button> + ); + } +} diff --git a/src/datascience-ui/interactive-common/common.css b/src/datascience-ui/interactive-common/common.css new file mode 100644 index 000000000000..32522d88b894 --- /dev/null +++ b/src/datascience-ui/interactive-common/common.css @@ -0,0 +1,578 @@ +body, +html { + height: 100%; + margin: 0; +} + +#root { + height: 100%; + overflow: hidden; +} + +#main-panel { + background: var(--override-background, var(--vscode-editor-background)); + color: var(--override-foreground, var(--vscode-editor-foreground)); + display: grid; + grid-template-rows: auto auto 1fr auto; + grid-template-columns: 1fr; + grid-template-areas: + 'toolbar' + 'variable' + 'content' + 'footer'; + height: 100%; + width: 100%; + position: absolute; + overflow: hidden; +} + +#main-panel-toolbar { + grid-area: toolbar; + overflow: hidden; +} + +#main-panel-variable { + grid-area: variable; + overflow: auto; +} +#main-panel-content { + grid-area: content; + max-height: 100%; + overflow: auto; +} + +#main-panel-footer { + grid-area: footer; + overflow: hidden; +} + +#content-panel-div.content-panel-scrollBeyondLastLine { + margin-bottom: 100vh; +} + +.hide { + display: none; +} + +.invisible { + visibility: hidden; +} + +.edit-panel { + min-height: 50px; + padding: 10px 0px 10px 0px; + width: 100%; + border-top-color: var(--override-badge-background, var(--vscode-badge-background)); + border-top-style: solid; + border-top-width: 2px; +} + +.edit-panel-colorized { + min-height: 50px; + padding: 10px 0px 10px 0px; + width: 100%; + border-top-color: var(--override-badge-background, var(--vscode-badge-background)); + border-top-style: solid; + border-top-width: 2px; + background-color: var(--override-peek-background, var(--vscode-peekViewEditor-background)); +} + +/* Cell */ +.cell-wrapper { + margin: 0px; + padding: 2px 5px 2px 2px; + display: block; +} + +.cell-wrapper:focus { + outline-width: 0px; + outline-color: var(--override-selection-background, var(--vscode-editor-selectionBackground)); +} + +.cell-wrapper-preview { + background-color: var(--override-peek-background, var(--vscode-peekViewEditor-background)); +} + +.cell-wrapper-noneditable { + border-bottom-color: var(--override-widget-background, var(--vscode-editorGroupHeader-tabsBackground)); + border-bottom-style: solid; + border-bottom-width: 1px; +} + +.cell-wrapper:after { + content: ''; + clear: both; + display: block; +} + +.edit-panel-colorized .cell-wrapper:focus { + outline-width: 0px; +} +.edit-panel .cell-wrapper:focus { + outline-width: 0px; +} + +.cell-outer { + display: grid; + grid-template-columns: auto 1fr; + grid-column-gap: 3px; + width: 100%; +} + +.cell-outer-editable { + display: grid; + grid-template-columns: auto 1fr; + grid-column-gap: 3px; + width: 100%; + margin-top: 16px; +} + +.content-div { + grid-column: 2; + width: 100%; +} + +.controls-div { + grid-column: 1; + grid-template-columns: max-content min-content; + justify-content: grid; + grid-template-rows: min-content max-content; + display: grid; +} + +.cell-result-container { + width: 100%; +} + +.cell-input { + margin: 0; +} + +.cell-input pre { + margin: 0px; + padding: 0px; +} + +.cell-output { + margin-top: 5px; + background: var(--override-widget-background, var(--vscode-notifications-background)); +} + +.cell-output-text { + white-space: pre-wrap; + font-family: var(--vscode-editor-font-family); + font-weight: var(--vscode-editor-font-weight); +} + +.cell-output-text pre { + white-space: pre-wrap; + font-family: var(--vscode-editor-font-family); + font-weight: var(--vscode-editor-font-weight); +} + +.cell-output-text xmp { + white-space: pre-wrap; + font-family: var(--vscode-editor-font-family); + font-weight: var(--vscode-editor-font-weight); +} + +.cell-output-html { + white-space: unset; + position: relative; +} + +.cell-output-plot-background { + background: white; + display: inline-block; + margin: 3px; +} + +.cell-output-ipywidget-background { + background: white !important; +} + +.cell-output-plot-background * { + margin: 0px; +} + +.cell-output table { + background-color: transparent; + border: none; + border-collapse: collapse; + border-spacing: 0px; + font-size: 12px; + table-layout: fixed; +} + +.cell-output thead { + border-bottom-color: var(--override-foreground, var(--vscode-editor-foreground)); + border-bottom-style: solid; + border-bottom-width: 1px; + vertical-align: bottom; +} + +.cell-output tr, +.cell-output th, +.cell-output td { + text-align: right; + vertical-align: middle; + padding: 0.5em 0.5em; + line-height: normal; + white-space: normal; + max-width: none; + border: none; +} +.cell-output th { + font-weight: bold; +} +.cell-output tbody tr:nth-child(even) { + background: var( + --override-background, + var(--vscode-editor-background) + ); /* Force to white because the default color for output is gray */ +} +.cell-output tbody tr:hover { + background: var(--override-selection-background, var(--vscode-editor-selectionBackground)); +} +.cell-output * + table { + margin-top: 1em; +} + +.center-img { + display: block; + margin: 0 auto; +} + +.cell-output-html .plot-open-button { + z-index: 10; + position: absolute; + left: 2px; + visibility: hidden; +} + +.cell-output-html:hover .plot-open-button { + visibility: visible; +} + +.cell-output-error { + background: var(--override-background, var(--vscode-editor-background)); +} + +/* Code */ +.code-area { + position: relative; + width: 100%; + margin-bottom: 16px; + top: -2px; /* Account for spacing removed from the monaco editor */ +} + +.code-area-editable { + margin-bottom: 10px; +} + +.code-watermark { + position: absolute; + top: 3px; + left: 3px; + z-index: 500; + font-style: italic; + color: var(--override-watermark-color, var(--vscode-panelTitle-inactiveForeground)); +} + +.code-watermark:focus { + outline-width: 0; +} + +.collapse-input-svg-rotate { + transform: rotate(45deg); + transform-origin: 0% 100%; +} + +.collapse-input-svg-vscode-light { + fill: black; +} + +.collapse-input-svg-vscode-dark { + fill: lightgray; +} + +.collapse-input { + grid-column: 2; + padding: 2px; + margin-top: 2px; + height: min-content; +} + +.remove-style { + background-color: transparent; + border: transparent; + font: inherit; +} + +.inputLabel { + background-color: var(--override-background, var(--vscode-editor-background)); + color: var(--override-foreground, var(--vscode-editor-foreground)); + font-family: var(--vscode-editor-font-family); + font-weight: var(--vscode-editor-font-weight); + margin: 3px; +} + +#cell-table { + display: block; + width: 100%; +} + +.flash { + animation-name: flash-animation; + animation-duration: 1s; +} + +@keyframes flash-animation { + from { + background: var(--override-peek-background, var(--vscode-peekViewEditor-background)); + } + to { + background: default; + } +} + +.messages-wrapper { + padding: 12px; + display: block; + border-bottom-color: var(--override-tabs-background, var(--vscode-editorGroupHeader-tabsBackground)); + border-bottom-style: solid; + border-bottom-width: 1px; +} + +.messages-outer { + background: var(--override-widget-background, var(--vscode-notifications-background)); + white-space: pre-wrap; + font-family: monospace; + width: 100%; +} + +.messages-outer-preview { + font-weight: bold; + background-color: var(--override-peek-background, var(--vscode-peekViewEditor-background)); + font-family: var(--code-font-family); +} + +.messages-wrapper-preview { + background-color: var(--override-peek-background, var(--vscode-peekViewEditor-background)); +} + +.messages-result-container pre { + white-space: pre-wrap; + font-family: monospace; + margin: 0px; +} + +.cell-menu-bar-outer { + justify-self: right; +} + +.cell-toolbar { + justify-self: end; + display: flex; + flex-flow: column; +} + +.cell-menu-bar-child-container { + margin-top: 2px; + margin-bottom: 2px; + display: flex; + flex-direction: column; +} + +#toolbar-panel { + margin-top: 2px; + margin-bottom: 2px; + margin-left: 2px; + margin-right: 2px; +} + +.toolbar-extra-button { + margin-left: auto; +} +#variable-panel-padding { + padding-top: 2px; + padding-left: 20px; + padding-right: 20px; + padding-bottom: 2px; +} +.variable-explorer-menu-bar { + display: flex; + width: 100%; + justify-content: end; +} +.variable-explorer-label { + margin-right: auto; +} +.variable-explorer-close-button { + justify-self: end; +} + +.execution-count { + grid-column: 1; + font-weight: bold; + display: flex; + color: var(--vscode-descriptionForeground); + font-family: var(--code-font-family); +} + +.execution-count-busy-outer { + grid-column: 1; + font-weight: bold; + color: var(--code-comment-color); + display: block; + width: 8px; + height: 8px; + white-space: nowrap; +} + +@keyframes execution-spin { + from { + transform: rotate(0); + } + to { + transform: rotate(360deg); + } +} + +.execution-count-busy-svg { + animation: execution-spin 4s linear infinite; +} + +.execution-count-busy-polyline { + fill: none; + stroke: var(--code-comment-color); + stroke-width: 1; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.plain-editor { + background: transparent; + border: none; + outline: none; + width: 100%; +} + +.plain-editor:focus { + outline: none; +} + +.toolbar-divider { + width: 100%; + margin-top: 2px; + border-bottom-color: var(--override-badge-background, var(--vscode-badge-background)); + border-bottom-style: solid; + border-bottom-width: 2px; +} +.toolbar-menu-bar { + display: inline-flex; + width: 100%; +} + +.jupyter-info-section { + padding: 0px 5px; + align-self: center; + margin-top: 1px; + margin-bottom: 2px; + background-color: transparent; + border: none; + outline: none; + font-size: var(--vscode-font-size); + font-family: var(--vscode-font-family); +} +.jupyter-info-section-hoverable:hover { + background-color: var(--override-badge-background, var(--vscode-badge-background)); +} + +.kernel-status { + display: flex; + margin-left: auto; +} + +.kernel-status-section { + padding: 0px 5px; + align-self: center; + margin-top: 1px; + margin-bottom: 2px; +} + +.kernel-status-section-hoverable:hover { + background-color: var(--override-badge-background, var(--vscode-badge-background)); + cursor: pointer; +} + +.kernel-status-divider { + border-left: 1px solid var(--vscode-badge-background); + flex: 1; + max-width: 1px; +} + +.kernel-status-text { + padding-right: 5px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + text-align: end; +} + +.kernel-status-server { + display: grid; + grid-template-columns: 1fr auto; +} + +.kernel-status-status { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.kernel-status-icon { + width: 16px; + height: 0px; + padding: 1px; + margin-top: 2px; +} + +.image-button-image { + cursor: pointer; +} + +.codicon-button { + cursor: pointer; + color: var(--vscode-editor-foreground); +} + +.image-button-inner-disabled-filter .image-button-image { + cursor: unset; +} + +.outputTrimmedSettingsLink { + text-decoration: underline; +} + +.toolbar-menu-bar .image-button { + margin-top: 8px; + margin-bottom: 9px; + margin-left: 7px; + margin-right: 7px; +} + +.image-button:focus { + outline: none; +} + +.rotate { + animation: spin 2s infinite linear; +} diff --git a/src/datascience-ui/interactive-common/contentPanel.tsx b/src/datascience-ui/interactive-common/contentPanel.tsx new file mode 100644 index 000000000000..f8653aa48fa4 --- /dev/null +++ b/src/datascience-ui/interactive-common/contentPanel.tsx @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as React from 'react'; + +import * as fastDeepEqual from 'fast-deep-equal'; +import { IDataScienceExtraSettings } from '../../client/datascience/types'; +import { InputHistory } from './inputHistory'; +import { ICellViewModel } from './mainState'; + +// See the discussion here: https://github.com/Microsoft/tslint-microsoft-contrib/issues/676 +// tslint:disable: react-this-binding-issue +// tslint:disable-next-line:no-require-imports no-var-requires +const throttle = require('lodash/throttle') as typeof import('lodash/throttle'); + +export interface IContentPanelProps { + baseTheme: string; + cellVMs: ICellViewModel[]; + history?: InputHistory; + testMode?: boolean; + settings?: IDataScienceExtraSettings; + codeTheme: string; + submittedText: boolean; + skipNextScroll: boolean; + editable: boolean; + scrollBeyondLastLine: boolean; + renderCell(cellVM: ICellViewModel, index: number): JSX.Element | null; + scrollToBottom(div: HTMLDivElement): void; +} + +export class ContentPanel extends React.Component<IContentPanelProps> { + private bottomRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>(); + private containerRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>(); + private throttledScrollIntoView = throttle(this.scrollIntoView.bind(this), 100); + constructor(prop: IContentPanelProps) { + super(prop); + } + public componentDidMount() { + this.scrollToBottom(); + } + public componentWillReceiveProps(prevProps: IContentPanelProps) { + // Scroll if we suddenly finished or updated a cell. This should happen on + // finish, updating output, etc. + if (!fastDeepEqual(prevProps.cellVMs.map(this.outputCheckable), this.props.cellVMs.map(this.outputCheckable))) { + this.scrollToBottom(); + } + } + + public computeIsAtBottom(parent: HTMLDivElement): boolean { + if (this.bottomRef.current) { + // if the bottom div is on the screen, the content is at the bottom + return this.bottomRef.current.offsetTop - parent.offsetTop - 2 < parent.clientHeight + parent.scrollTop; + } + return false; + } + + public render() { + const className = `${this.props.scrollBeyondLastLine ? 'content-panel-scrollBeyondLastLine' : ''}`; + return ( + <div id="content-panel-div" ref={this.containerRef} className={className}> + <div id="cell-table" role="list"> + {this.renderCells()} + </div> + <div id="bottomDiv" ref={this.bottomRef} /> + </div> + ); + } + + private outputCheckable = (cellVM: ICellViewModel) => { + // Return the properties that if they change means a cell updated something + return { + outputs: cellVM.cell.data.outputs, + state: cellVM.cell.state + }; + }; + + private renderCells = () => { + return this.props.cellVMs.map((cellVM: ICellViewModel, index: number) => { + return this.props.renderCell(cellVM, index); + }); + }; + + private scrollIntoView() { + if (this.bottomRef.current && this.props.scrollToBottom) { + this.props.scrollToBottom(this.bottomRef.current); + } + } + + private scrollToBottom() { + if (this.bottomRef.current && !this.props.skipNextScroll && !this.props.testMode && this.containerRef.current) { + // Make sure to debounce this so it doesn't take up too much time. + this.throttledScrollIntoView(); + } + } +} diff --git a/src/datascience-ui/interactive-common/editor.tsx b/src/datascience-ui/interactive-common/editor.tsx new file mode 100644 index 000000000000..7eca634b2521 --- /dev/null +++ b/src/datascience-ui/interactive-common/editor.tsx @@ -0,0 +1,277 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as React from 'react'; + +import { noop } from '../../client/common/utils/misc'; +import { IKeyboardEvent } from '../react-common/event'; +import { MonacoEditor } from '../react-common/monacoEditor'; +import { IMonacoModelContentChangeEvent } from '../react-common/monacoHelpers'; +import { InputHistory } from './inputHistory'; +import { CursorPos, IFont } from './mainState'; + +const stickiness = monacoEditor.editor.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; + +// we need a separate decoration for glyph margin, since we do not want it on each line of a multi line statement. +const TOP_STACK_FRAME_MARGIN: monacoEditor.editor.IModelDecorationOptions = { + glyphMarginClassName: 'codicon codicon-debug-stackframe', + stickiness +}; +const TOP_STACK_FRAME_DECORATION: monacoEditor.editor.IModelDecorationOptions = { + isWholeLine: true, + className: 'debug-top-stack-frame-line', + stickiness +}; + +// tslint:disable-next-line: import-name +export interface IEditorProps { + content: string; + version: number; + codeTheme: string; + readOnly: boolean; + testMode: boolean; + monacoTheme: string | undefined; + outermostParentClass: string; + editorOptions?: monacoEditor.editor.IEditorOptions; + history: InputHistory | undefined; + editorMeasureClassName?: string; + language: string; + showLineNumbers?: boolean; + useQuickEdit?: boolean; + font: IFont; + hasFocus: boolean; + cursorPos: CursorPos | monacoEditor.IPosition; + disableUndoStack: boolean; + ipLocation: number | undefined; + onCreated(code: string, modelId: string): void; + onChange(e: IMonacoModelContentChangeEvent): void; + openLink(uri: monacoEditor.Uri): void; + keyDown?(e: IKeyboardEvent): void; + focused?(): void; + unfocused?(): void; +} + +export class Editor extends React.Component<IEditorProps> { + private subscriptions: monacoEditor.IDisposable[] = []; + private lastCleanVersionId: number = 0; + private monacoRef: React.RefObject<MonacoEditor> = React.createRef<MonacoEditor>(); + private modelRef: monacoEditor.editor.ITextModel | null = null; + private editorRef: monacoEditor.editor.IStandaloneCodeEditor | null = null; + private decorationIds: string[] = []; + + constructor(prop: IEditorProps) { + super(prop); + } + + public componentWillUnmount = () => { + this.subscriptions.forEach((d) => d.dispose()); + }; + + public componentDidUpdate(prevProps: IEditorProps) { + if (this.modelRef) { + if (prevProps.ipLocation !== this.props.ipLocation) { + if (this.props.ipLocation) { + const newDecorations = this.createIpDelta(); + this.decorationIds = this.modelRef.deltaDecorations(this.decorationIds, newDecorations); + } else if (this.decorationIds.length) { + this.decorationIds = this.modelRef.deltaDecorations(this.decorationIds, []); + } + if (this.editorRef && this.props.ipLocation) { + this.editorRef.setPosition({ lineNumber: this.props.ipLocation, column: 1 }); + } + } + } + if (prevProps.readOnly === true && this.props.readOnly === false && this.editorRef) { + this.subscriptions.push(this.editorRef.onKeyDown(this.onKeyDown)); + this.subscriptions.push(this.editorRef.onKeyUp(this.onKeyUp)); + } + } + + public render() { + const classes = this.props.readOnly ? 'editor-area' : 'editor-area editor-area-editable'; + const renderEditor = this.renderMonacoEditor; + return <div className={classes}>{renderEditor()}</div>; + } + + public giveFocus(cursorPos: CursorPos | monacoEditor.IPosition) { + if (this.monacoRef.current) { + this.monacoRef.current.giveFocus(cursorPos); + } + } + + public getContents(): string { + if (this.monacoRef.current) { + return this.monacoRef.current.getContents(); + } + return ''; + } + + private createIpDelta(): monacoEditor.editor.IModelDeltaDecoration[] { + const result: monacoEditor.editor.IModelDeltaDecoration[] = []; + if (this.props.ipLocation) { + const columnUntilEOLRange = new monacoEditor.Range( + this.props.ipLocation, + 1, + this.props.ipLocation, + 1 << 30 + ); + const range = new monacoEditor.Range(this.props.ipLocation, 1, this.props.ipLocation, 2); + + result.push({ + options: TOP_STACK_FRAME_MARGIN, + range + }); + + result.push({ + options: TOP_STACK_FRAME_DECORATION, + range: columnUntilEOLRange + }); + } + return result; + } + + private renderMonacoEditor = (): JSX.Element => { + const readOnly = this.props.readOnly; + const options: monacoEditor.editor.IEditorConstructionOptions = { + minimap: { + enabled: false + }, + glyphMargin: false, + wordWrap: 'on', + scrollBeyondLastLine: false, + scrollbar: { + vertical: 'hidden', + horizontal: 'hidden' + }, + lineNumbers: this.props.showLineNumbers ? 'on' : 'off', + renderLineHighlight: 'none', + highlightActiveIndentGuide: false, + renderIndentGuides: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + folding: false, + readOnly: readOnly, + occurrencesHighlight: false, + selectionHighlight: false, + lineDecorationsWidth: 0, + contextmenu: false, + matchBrackets: false, + fontSize: this.props.font.size, + fontFamily: this.props.font.family, + ...this.props.editorOptions + }; + + return ( + <MonacoEditor + measureWidthClassName={this.props.editorMeasureClassName} + testMode={this.props.testMode} + value={this.props.content} + outermostParentClass={this.props.outermostParentClass} + theme={this.props.monacoTheme ? this.props.monacoTheme : 'vs'} + language={this.props.language} + editorMounted={this.editorDidMount} + modelChanged={this.props.onChange} + options={options} + version={this.props.version} + openLink={this.props.openLink} + ref={this.monacoRef} + hasFocus={this.props.hasFocus} + cursorPos={this.props.cursorPos} + /> + ); + }; + + private editorDidMount = (editor: monacoEditor.editor.IStandaloneCodeEditor) => { + this.editorRef = editor; + const model = editor.getModel(); + this.modelRef = model; + + // Disable undo/redo on the model if asked + // tslint:disable: no-any + if (this.props.disableUndoStack && (model as any).undo && (model as any).redo) { + (model as any).undo = noop; + (model as any).redo = noop; + } + + // List for key up/down events if not read only + if (!this.props.readOnly) { + this.subscriptions.push(editor.onKeyDown(this.onKeyDown)); + this.subscriptions.push(editor.onKeyUp(this.onKeyUp)); + } + + // Indicate we're ready + this.props.onCreated(this.props.content, model!.id); + + // Track focus changes + this.subscriptions.push(editor.onDidFocusEditorWidget(this.props.focused ? this.props.focused : noop)); + this.subscriptions.push(editor.onDidBlurEditorWidget(this.props.unfocused ? this.props.unfocused : noop)); + }; + + // tslint:disable-next-line: cyclomatic-complexity + private onKeyDown = (e: monacoEditor.IKeyboardEvent) => { + if (this.monacoRef.current) { + const cursor = this.monacoRef.current.getPosition(); + const currentLine = this.monacoRef.current.getCurrentVisibleLine(); + const visibleLineCount = this.monacoRef.current.getVisibleLineCount(); + const isSuggesting = this.monacoRef.current.isSuggesting(); + const isFirstLine = currentLine === 0; + const isLastLine = currentLine === visibleLineCount - 1; + const isDirty = this.monacoRef.current.getVersionId() > this.lastCleanVersionId; + + // See if we need to use the history or not + if (cursor && this.props.history && e.code === 'ArrowUp' && isFirstLine && !isSuggesting) { + const currentValue = this.getContents(); + const newValue = this.props.history.completeUp(currentValue); + if (newValue !== currentValue) { + this.monacoRef.current.setValue(newValue, CursorPos.Top); + this.lastCleanVersionId = this.monacoRef.current.getVersionId(); + e.stopPropagation(); + } + } else if (cursor && this.props.history && e.code === 'ArrowDown' && isLastLine && !isSuggesting) { + const currentValue = this.getContents(); + const newValue = this.props.history.completeDown(currentValue); + if (newValue !== currentValue) { + this.monacoRef.current.setValue(newValue, CursorPos.Bottom); + this.lastCleanVersionId = this.monacoRef.current.getVersionId(); + e.stopPropagation(); + } + } else if (this.props.keyDown) { + // Forward up the chain + this.props.keyDown({ + code: e.code, + shiftKey: e.shiftKey, + altKey: e.altKey, + ctrlKey: e.ctrlKey, + target: e.target, + metaKey: e.metaKey, + editorInfo: { + isFirstLine, + isLastLine, + isDirty, + isSuggesting, + contents: this.getContents(), + clear: this.clear + }, + stopPropagation: () => e.stopPropagation(), + preventDefault: () => e.preventDefault() + }); + } + } + }; + + private onKeyUp = (e: monacoEditor.IKeyboardEvent) => { + if (e.shiftKey && e.keyCode === monacoEditor.KeyCode.Enter) { + // Shift enter was hit + e.stopPropagation(); + e.preventDefault(); + } + }; + + private clear = () => { + if (this.monacoRef.current) { + this.monacoRef.current.setValue('', CursorPos.Top); + } + }; +} diff --git a/src/datascience-ui/interactive-common/executionCount.tsx b/src/datascience-ui/interactive-common/executionCount.tsx new file mode 100644 index 000000000000..538b7ac96cbb --- /dev/null +++ b/src/datascience-ui/interactive-common/executionCount.tsx @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import * as React from 'react'; + +interface IExecutionCountProps { + isBusy: boolean; + count: string; + visible: boolean; +} + +export class ExecutionCount extends React.Component<IExecutionCountProps> { + constructor(props: IExecutionCountProps) { + super(props); + } + + public render() { + if (this.props.visible) { + return this.props.isBusy ? ( + <div className="execution-count-busy-outer"> + [ + <svg className="execution-count-busy-svg" viewBox="0 0 16 16"> + <polyline + points="8,0, 8,8, 14,3, 8,8, 16,8, 8,8, 14,14, 8,8 8,16 8,8 3,14 8,8 0,8 8,8 3,3" + className="execution-count-busy-polyline" + /> + </svg> + ] + </div> + ) : ( + <div className="execution-count">{`[${this.props.count}]`}</div> + ); + } else { + return null; + } + } +} diff --git a/src/datascience-ui/interactive-common/handlers.ts b/src/datascience-ui/interactive-common/handlers.ts new file mode 100644 index 000000000000..059ace4951ac --- /dev/null +++ b/src/datascience-ui/interactive-common/handlers.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export function handleLinkClick(ev: MouseEvent, linkClick: (href: string) => void) { + // If this is an anchor element, forward the click as Jupyter does. + let anchor = ev.target as HTMLAnchorElement; + if (anchor && anchor.href) { + // Href may be redirected to an inner anchor + if (anchor.href.startsWith('vscode') || anchor.href.startsWith(anchor.baseURI)) { + const inner = anchor.getElementsByTagName('a'); + if (inner && inner.length > 0) { + anchor = inner[0]; + } + } + if (!anchor || !anchor.href || anchor.href.startsWith('vscode')) { + return; + } + + // Don't want a link click to cause a refresh of the webpage + ev.stopPropagation(); + ev.preventDefault(); + + // Look for a blob link. + if (!anchor.href.startsWith('blob:')) { + linkClick(anchor.href); + } else { + // We an have an image (as a blob) and the reference is blob://null:<someguid> + // We need to get the blob, for that make a http request and the response will be the Blob + // Next convert the blob into something that can be sent to the client side. + // Just send an inlined base64 image to `linkClick`, such as `data:image/png;base64,xxxxx` + const xhr = new XMLHttpRequest(); + xhr.open('GET', anchor.href, true); + xhr.responseType = 'blob'; + xhr.onload = () => { + const blob = xhr.response; + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onload = () => { + if (typeof reader.result === 'string') { + linkClick(reader.result); + } + }; + }; + xhr.send(); + } + } +} diff --git a/src/datascience-ui/interactive-common/images.d.ts b/src/datascience-ui/interactive-common/images.d.ts new file mode 100644 index 000000000000..f83b33cbc711 --- /dev/null +++ b/src/datascience-ui/interactive-common/images.d.ts @@ -0,0 +1,7 @@ +// tslint:disable:copyright +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +declare module '*.svg'; +declare module '*.png'; +declare module '*.jpg'; diff --git a/src/datascience-ui/interactive-common/informationMessages.tsx b/src/datascience-ui/interactive-common/informationMessages.tsx new file mode 100644 index 000000000000..3d7a6599477d --- /dev/null +++ b/src/datascience-ui/interactive-common/informationMessages.tsx @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as React from 'react'; + +// tslint:disable-next-line:match-default-export-name import-name +interface IInformationMessagesProps { + messages: string[]; +} + +export class InformationMessages extends React.Component<IInformationMessagesProps> { + constructor(prop: IInformationMessagesProps) { + super(prop); + } + + public render() { + const output = this.props.messages.join('\n'); + const wrapperClassName = 'messages-wrapper'; + const outerClassName = 'messages-outer'; + + return ( + <div className={wrapperClassName}> + <div className={outerClassName}> + <div className="messages-result-container"> + <pre> + <span>{output}</span> + </pre> + </div> + </div> + </div> + ); + } +} diff --git a/src/datascience-ui/interactive-common/inputHistory.ts b/src/datascience-ui/interactive-common/inputHistory.ts new file mode 100644 index 000000000000..d75dbb4861f7 --- /dev/null +++ b/src/datascience-ui/interactive-common/inputHistory.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export class InputHistory { + private historyStack: string[] = []; + private up: number | undefined; + private down: number | undefined; + private last: number | undefined; + + public completeUp(code: string): string { + // If going up, only move if anything in the history + if (this.historyStack.length > 0) { + if (this.up === undefined) { + this.up = 0; + } + + const result = this.up < this.historyStack.length ? this.historyStack[this.up] : code; + this.adjustCursors(this.up); + return result; + } + + return code; + } + + public completeDown(code: string): string { + // If going down, move and then return something if we have a position + if (this.historyStack.length > 0 && this.down !== undefined) { + const result = this.historyStack[this.down]; + this.adjustCursors(this.down); + return result; + } + + return code; + } + + public add(code: string, typed: boolean) { + // Compute our new history. Behavior depends upon if the user typed it in or + // just used the arrows + + // Only skip adding a dupe if it's the same as the top item. Otherwise + // add it as normal. + this.historyStack = + this.last === 0 && this.historyStack.length > 0 && this.historyStack[this.last] === code + ? this.historyStack + : [code, ...this.historyStack]; + + // Position is more complicated. If we typed something start over + if (typed) { + this.reset(); + } else { + // We want our next up push to match the index of the item that was + // actually entered. + if (this.last === 0) { + this.up = undefined; + this.down = undefined; + } else if (this.last) { + this.up = this.last + 1; + this.down = this.last - 1; + } + } + } + + private reset() { + this.up = undefined; + this.down = undefined; + } + + private adjustCursors(currentPos: number) { + // Save last position we entered. + this.last = currentPos; + + // For a single item, ony up works. But never modify it. + if (this.historyStack.length > 1) { + if (currentPos < this.historyStack.length) { + this.up = currentPos + 1; + } else { + this.up = this.historyStack.length; + + // If we go off the end, don't make the down go up to the last. + // CMD prompt behaves this way. Down is always one off. + currentPos = this.historyStack.length - 1; + } + if (currentPos > 0) { + this.down = currentPos - 1; + } else { + this.down = undefined; + } + } + } +} diff --git a/src/datascience-ui/interactive-common/intellisenseProvider.ts b/src/datascience-ui/interactive-common/intellisenseProvider.ts new file mode 100644 index 000000000000..035340ca480f --- /dev/null +++ b/src/datascience-ui/interactive-common/intellisenseProvider.ts @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as uuid from 'uuid/v4'; + +import { IDisposable } from '../../client/common/types'; +import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { noop } from '../../client/common/utils/misc'; +import { Identifiers } from '../../client/datascience/constants'; +import { + IInteractiveWindowMapping, + InteractiveWindowMessages, + IProvideCompletionItemsResponse, + IProvideHoverResponse, + IProvideSignatureHelpResponse, + IResolveCompletionItemResponse +} from '../../client/datascience/interactive-common/interactiveWindowTypes'; + +interface IRequestData<T> { + promise: Deferred<T>; + cancelDisposable: monacoEditor.IDisposable; +} + +export class IntellisenseProvider + implements + monacoEditor.languages.CompletionItemProvider, + monacoEditor.languages.HoverProvider, + monacoEditor.languages.SignatureHelpProvider, + IDisposable { + public triggerCharacters?: string[] | undefined = ['.']; + public readonly signatureHelpTriggerCharacters?: ReadonlyArray<string> = ['(', ',', '<']; + public readonly signatureHelpRetriggerCharacters?: ReadonlyArray<string> = [')']; + private completionRequests: Map<string, IRequestData<monacoEditor.languages.CompletionList>> = new Map< + string, + IRequestData<monacoEditor.languages.CompletionList> + >(); + private resolveCompletionRequests: Map<string, IRequestData<monacoEditor.languages.CompletionItem>> = new Map< + string, + IRequestData<monacoEditor.languages.CompletionItem> + >(); + private hoverRequests: Map<string, IRequestData<monacoEditor.languages.Hover>> = new Map< + string, + IRequestData<monacoEditor.languages.Hover> + >(); + private signatureHelpRequests: Map<string, IRequestData<monacoEditor.languages.SignatureHelpResult>> = new Map< + string, + IRequestData<monacoEditor.languages.SignatureHelpResult> + >(); + private registerDisposables: monacoEditor.IDisposable[] = []; + private monacoIdToCellId: Map<string, string> = new Map<string, string>(); + private cellIdToMonacoId: Map<string, string> = new Map<string, string>(); + private disposed = false; + constructor( + private messageSender: <M extends IInteractiveWindowMapping, T extends keyof M>( + type: T, + payload?: M[T] + ) => void, + readonly language: string + ) { + // Register a completion provider + this.registerDisposables.push(monacoEditor.languages.registerCompletionItemProvider(language, this)); + this.registerDisposables.push(monacoEditor.languages.registerHoverProvider(language, this)); + this.registerDisposables.push(monacoEditor.languages.registerSignatureHelpProvider(language, this)); + } + + public provideCompletionItems( + model: monacoEditor.editor.ITextModel, + position: monacoEditor.Position, + context: monacoEditor.languages.CompletionContext, + token: monacoEditor.CancellationToken + ): monacoEditor.languages.ProviderResult<monacoEditor.languages.CompletionList> { + // Emit a new request + const requestId = uuid(); + const promise = createDeferred<monacoEditor.languages.CompletionList>(); + + const cancelDisposable = token.onCancellationRequested(() => { + promise.resolve(); + this.sendMessage(InteractiveWindowMessages.CancelCompletionItemsRequest, { requestId }); + }); + + this.completionRequests.set(requestId, { promise, cancelDisposable }); + this.sendMessage(InteractiveWindowMessages.ProvideCompletionItemsRequest, { + position, + context, + requestId, + cellId: this.getCellId(model.id) + }); + + return promise.promise; + } + + public async resolveCompletionItem( + model: monacoEditor.editor.ITextModel, + position: monacoEditor.Position, + item: monacoEditor.languages.CompletionItem, + token: monacoEditor.CancellationToken + ): Promise<monacoEditor.languages.CompletionItem> { + // If the item has already resolved documentation (as with MS LS) we don't need to do this + if (!item.documentation) { + // Emit a new request + const requestId = uuid(); + const promise = createDeferred<monacoEditor.languages.CompletionItem>(); + + const cancelDisposable = token.onCancellationRequested(() => { + promise.resolve(); + this.sendMessage(InteractiveWindowMessages.CancelResolveCompletionItemRequest, { requestId }); + }); + + this.resolveCompletionRequests.set(requestId, { promise, cancelDisposable }); + this.sendMessage(InteractiveWindowMessages.ResolveCompletionItemRequest, { + position, + item, + requestId, + cellId: this.getCellId(model.id) + }); + + const newItem = await promise.promise; + // Our code strips out _documentPosition and possibly other items that are too large to send + // so instead of returning the new resolve completion item, just return the old item with documentation added in + // which is what we are resolving the item to get + return Promise.resolve({ ...item, documentation: newItem.documentation }); + } else { + return Promise.resolve(item); + } + } + + public provideHover( + model: monacoEditor.editor.ITextModel, + position: monacoEditor.Position, + token: monacoEditor.CancellationToken + ): monacoEditor.languages.ProviderResult<monacoEditor.languages.Hover> { + // Emit a new request + const requestId = uuid(); + const promise = createDeferred<monacoEditor.languages.Hover>(); + const wordAtPosition = model.getWordAtPosition(position); + + const cancelDisposable = token.onCancellationRequested(() => { + promise.resolve(); + this.sendMessage(InteractiveWindowMessages.CancelCompletionItemsRequest, { requestId }); + }); + + this.hoverRequests.set(requestId, { promise, cancelDisposable }); + this.sendMessage(InteractiveWindowMessages.ProvideHoverRequest, { + position, + requestId, + cellId: this.getCellId(model.id), + wordAtPosition: wordAtPosition ? wordAtPosition.word : undefined + }); + + return promise.promise; + } + + public provideSignatureHelp( + model: monacoEditor.editor.ITextModel, + position: monacoEditor.Position, + token: monacoEditor.CancellationToken, + context: monacoEditor.languages.SignatureHelpContext + ): monacoEditor.languages.ProviderResult<monacoEditor.languages.SignatureHelpResult> { + // Emit a new request + const requestId = uuid(); + const promise = createDeferred<monacoEditor.languages.SignatureHelpResult>(); + + const cancelDisposable = token.onCancellationRequested(() => { + promise.resolve(); + this.sendMessage(InteractiveWindowMessages.CancelSignatureHelpRequest, { requestId }); + }); + + this.signatureHelpRequests.set(requestId, { promise, cancelDisposable }); + this.sendMessage(InteractiveWindowMessages.ProvideSignatureHelpRequest, { + position, + context, + requestId, + cellId: this.getCellId(model.id) + }); + + return promise.promise; + } + + public dispose() { + this.disposed = true; + this.registerDisposables.forEach((r) => r.dispose()); + this.completionRequests.forEach((r) => r.promise.resolve()); + this.resolveCompletionRequests.forEach((r) => r.promise.resolve()); + this.hoverRequests.forEach((r) => r.promise.resolve()); + + this.registerDisposables = []; + this.completionRequests.clear(); + this.hoverRequests.clear(); + } + + public mapCellIdToModelId(cellId: string, modelId: string) { + this.cellIdToMonacoId.set(cellId, modelId); + this.monacoIdToCellId.set(modelId, cellId); + } + + // Handle completion response + public handleCompletionResponse(response: IProvideCompletionItemsResponse) { + // Resolve our waiting promise if we have one + const waiting = this.completionRequests.get(response.requestId); + if (waiting) { + waiting.promise.resolve(response.list); + this.completionRequests.delete(response.requestId); + } + } + + // Handle hover response + public handleHoverResponse(response: IProvideHoverResponse) { + // Resolve our waiting promise if we have one + const waiting = this.hoverRequests.get(response.requestId); + if (waiting) { + waiting.promise.resolve(response.hover); + this.hoverRequests.delete(response.requestId); + } + } + + // Handle signature response + public handleSignatureHelpResponse(response: IProvideSignatureHelpResponse) { + // Resolve our waiting promise if we have one + const waiting = this.signatureHelpRequests.get(response.requestId); + if (waiting) { + waiting.promise.resolve({ + value: response.signatureHelp, + dispose: noop + }); + this.signatureHelpRequests.delete(response.requestId); + } + } + + public handleResolveCompletionItemResponse(response: IResolveCompletionItemResponse) { + // Resolve our waiting promise if we have one + const waiting = this.resolveCompletionRequests.get(response.requestId); + if (waiting) { + waiting.promise.resolve(response.item); + this.completionRequests.delete(response.requestId); + } + } + + private getCellId(monacoId: string): string { + const result = this.monacoIdToCellId.get(monacoId); + if (result) { + return result; + } + + // Just assume it's the edit cell if not found. + return Identifiers.EditCellId; + } + + private sendMessage<M extends IInteractiveWindowMapping, T extends keyof M>(type: T, payload?: M[T]): void { + if (!this.disposed) { + this.messageSender(type, payload); + } + } +} diff --git a/src/datascience-ui/interactive-common/jupyterInfo.tsx b/src/datascience-ui/interactive-common/jupyterInfo.tsx new file mode 100644 index 000000000000..ec94902757f3 --- /dev/null +++ b/src/datascience-ui/interactive-common/jupyterInfo.tsx @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { isEmpty, isNil } from 'lodash'; +import * as React from 'react'; +import { IDataScienceExtraSettings } from '../../client/datascience/types'; +import { Image, ImageName } from '../react-common/image'; +import { getLocString } from '../react-common/locReactSide'; +import { IFont, IServerState, ServerStatus } from './mainState'; +import { TrustMessage } from './trustMessage'; +import { getMaxWidth } from './utils'; + +export interface IJupyterInfoProps { + baseTheme: string; + font: IFont; + kernel: IServerState; + isNotebookTrusted?: boolean; + shouldShowTrustMessage: boolean; + settings?: IDataScienceExtraSettings | undefined; + selectServer(): void; + launchNotebookTrustPrompt?(): void; // Native editor-specific + selectKernel(): void; +} + +export class JupyterInfo extends React.Component<IJupyterInfoProps> { + private get isKernelSelectionAllowed() { + return ( + this.props.isNotebookTrusted !== false && + this.props.kernel.jupyterServerStatus !== ServerStatus.Restarting && + this.props.kernel.jupyterServerStatus !== ServerStatus.Starting + ); + } + constructor(prop: IJupyterInfoProps) { + super(prop); + this.selectKernel = this.selectKernel.bind(this); + this.selectServer = this.selectServer.bind(this); + } + + public render() { + let jupyterServerDisplayName: string = this.props.kernel.localizedUri; + if (!isNil(this.props.settings) && isEmpty(jupyterServerDisplayName)) { + const jupyterServerUriSetting: string = this.props.settings.jupyterServerURI; + if (!isEmpty(jupyterServerUriSetting) && this.isUriOfComputeInstance(jupyterServerUriSetting)) { + jupyterServerDisplayName = this.getComputeInstanceNameFromId(jupyterServerUriSetting); + } + } + + const serverTextSize = + getLocString('DataScience.jupyterServer', 'Jupyter Server').length + jupyterServerDisplayName.length + 4; // plus 4 for the icon + const displayNameTextSize = this.props.kernel.displayName.length + this.props.kernel.jupyterServerStatus.length; + const dynamicFont: React.CSSProperties = { + fontSize: 'var(--vscode-font-size)', // Use the same font and size as the menu + fontFamily: 'var(--vscode-font-family)', + maxWidth: getMaxWidth(serverTextSize + displayNameTextSize + 5) // plus 5 for the line and margins + }; + const serverTextWidth: React.CSSProperties = { + maxWidth: getMaxWidth(serverTextSize) + }; + const displayNameTextWidth: React.CSSProperties = { + maxWidth: getMaxWidth(displayNameTextSize) + }; + + const ariaDisabled = this.props.isNotebookTrusted === undefined ? false : this.props.isNotebookTrusted; + return ( + <div className="kernel-status" style={dynamicFont}> + {this.renderTrustMessage()} + <div className="kernel-status-section kernel-status-server" style={serverTextWidth}> + <div + className="kernel-status-text kernel-status-section-hoverable" + style={serverTextWidth} + onClick={this.selectServer} + role="button" + aria-disabled={ariaDisabled} + > + {getLocString('DataScience.jupyterServer', 'Jupyter Server')}: {jupyterServerDisplayName} + </div> + <Image + baseTheme={this.props.baseTheme} + class="image-button-image kernel-status-icon" + image={this.getIcon()} + title={this.getStatus()} + /> + </div> + <div className="kernel-status-divider" /> + {this.renderKernelStatus(displayNameTextWidth)} + </div> + ); + } + + private renderKernelStatus(displayNameTextWidth: React.CSSProperties) { + const ariaDisabled = this.props.isNotebookTrusted === undefined ? false : this.props.isNotebookTrusted; + if (this.isKernelSelectionAllowed) { + return ( + <div + className="kernel-status-section kernel-status-section-hoverable kernel-status-status" + style={displayNameTextWidth} + onClick={this.selectKernel} + role="button" + aria-disabled={ariaDisabled} + > + {this.props.kernel.displayName}: {this.props.kernel.jupyterServerStatus} + </div> + ); + } else { + const displayName = this.props.kernel.displayName ?? getLocString('DataScience.noKernel', 'No Kernel'); + return ( + <div className="kernel-status-section kernel-status-status" style={displayNameTextWidth} role="button"> + {displayName}: {this.props.kernel.jupyterServerStatus} + </div> + ); + } + } + + private renderTrustMessage() { + if (this.props.shouldShowTrustMessage) { + return ( + <TrustMessage + shouldShowTrustMessage={this.props.shouldShowTrustMessage} + isNotebookTrusted={this.props.isNotebookTrusted} + launchNotebookTrustPrompt={this.props.launchNotebookTrustPrompt} + /> + ); + } + } + + private selectKernel() { + this.props.selectKernel(); + } + private getIcon(): ImageName { + return this.props.kernel.jupyterServerStatus === ServerStatus.NotStarted + ? ImageName.JupyterServerDisconnected + : ImageName.JupyterServerConnected; + } + + private getStatus() { + return this.props.kernel.jupyterServerStatus === ServerStatus.NotStarted + ? getLocString('DataScience.disconnected', 'Disconnected') + : getLocString('DataScience.connected', 'Connected'); + } + + private isUriOfComputeInstance(uri: string): boolean { + try { + const parsedUrl: URL = new URL(uri); + return parsedUrl.searchParams.get('id') === 'azureml_compute_instances'; + } catch (e) { + return false; + } + } + + private getComputeInstanceNameFromId(id: string | undefined): string { + if (isNil(id)) { + return ''; + } + + const res: string[] | null = id.match( + /\/providers\/Microsoft.MachineLearningServices\/workspaces\/[^\/]+\/computes\/([^\/]+)(\/)?/ + ); + if (isNil(res) || res.length < 2) { + return ''; + } + + return res[1]; + } + + private selectServer(): void { + this.props.selectServer(); + } +} diff --git a/src/datascience-ui/interactive-common/mainState.ts b/src/datascience-ui/interactive-common/mainState.ts new file mode 100644 index 000000000000..250831ac5c3a --- /dev/null +++ b/src/datascience-ui/interactive-common/mainState.ts @@ -0,0 +1,609 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { nbformat } from '@jupyterlab/coreutils'; +// tslint:disable-next-line: no-require-imports no-var-requires +const cloneDeep = require('lodash/cloneDeep'); +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as path from 'path'; + +import { DebugProtocol } from 'vscode-debugprotocol'; +import { PYTHON_LANGUAGE } from '../../client/common/constants'; +import { IDataScienceSettings } from '../../client/common/types'; +import { CellMatcher } from '../../client/datascience/cellMatcher'; +import { Identifiers } from '../../client/datascience/constants'; +import { IEditorPosition } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { CellState, ICell, IDataScienceExtraSettings, IMessageCell } from '../../client/datascience/types'; +import { concatMultilineString, splitMultilineString } from '../common'; +import { createCodeCell } from '../common/cellFactory'; +import { getDefaultSettings } from '../react-common/settingsReactSide'; + +export enum CursorPos { + Top, + Bottom, + Current +} + +// The state we are in for run by line debugging +export enum DebugState { + Break, + Design, + Run +} + +export function activeDebugState(state: DebugState): boolean { + return state === DebugState.Break || state === DebugState.Run; +} + +export interface ICellViewModel { + cell: ICell; + inputBlockShow: boolean; + inputBlockOpen: boolean; + inputBlockText: string; + inputBlockCollapseNeeded: boolean; + editable: boolean; + directInput?: boolean; + showLineNumbers?: boolean; + hideOutput?: boolean; + useQuickEdit?: boolean; + selected: boolean; + focused: boolean; + scrollCount: number; + cursorPos: CursorPos | IEditorPosition; + hasBeenRun: boolean; + runDuringDebug?: boolean; + codeVersion?: number; + uiSideError?: string; + runningByLine: DebugState; + currentStack?: DebugProtocol.StackFrame[]; + gathering: boolean; +} + +export type IMainState = { + cellVMs: ICellViewModel[]; + editCellVM: ICellViewModel | undefined; + busy: boolean; + skipNextScroll?: boolean; + undoStack: ICellViewModel[][]; + redoStack: ICellViewModel[][]; + submittedText: boolean; + rootStyle?: string; + rootCss?: string; + font: IFont; + vscodeThemeName?: string; + baseTheme: string; + monacoTheme?: string; + knownDark: boolean; + editorOptions?: monacoEditor.editor.IEditorOptions; + currentExecutionCount: number; + debugging: boolean; + dirty: boolean; + isAtBottom: boolean; + newCellId?: string; + loadTotal?: number; + skipDefault?: boolean; + testMode?: boolean; + codeTheme: string; + settings?: IDataScienceExtraSettings; + focusPending: number; + monacoReady: boolean; + loaded: boolean; + kernel: IServerState; + isNotebookTrusted: boolean; +}; + +export type SelectionAndFocusedInfo = { + selectedCellId?: string; + selectedCellIndex?: number; + focusedCellId?: string; + focusedCellIndex?: number; +}; + +/** + * Returns the cell id and index of selected and focused cells. + */ +export function getSelectedAndFocusedInfo(state: { cellVMs: ICellViewModel[] }): SelectionAndFocusedInfo { + const info: { + selectedCellId?: string; + selectedCellIndex?: number; + focusedCellId?: string; + focusedCellIndex?: number; + } = {}; + for (let index = 0; index < state.cellVMs.length; index += 1) { + const cell = state.cellVMs[index]; + if (cell.selected) { + info.selectedCellId = cell.cell.id; + info.selectedCellIndex = index; + } + if (cell.focused) { + info.focusedCellId = cell.cell.id; + info.focusedCellIndex = index; + } + if (info.selectedCellId && info.focusedCellId) { + break; + } + } + + return info; +} + +export interface IFont { + size: number; + family: string; +} + +export interface IServerState { + jupyterServerStatus: ServerStatus; + localizedUri: string; + displayName: string; + language: string; +} + +export enum ServerStatus { + NotStarted = 'Not Started', + Busy = 'Busy', + Idle = 'Idle', + Dead = 'Dead', + Starting = 'Starting', + Restarting = 'Restarting' +} + +// tslint:disable-next-line: no-multiline-string +const darkStyle = ` + :root { + --code-comment-color: #6A9955; + --code-numeric-color: #b5cea8; + --code-string-color: #ce9178; + --code-variable-color: #9CDCFE; + --code-type-color: #4EC9B0; + --code-font-family: Consolas, 'Courier New', monospace; + --code-font-size: 14px; + } +`; + +// This function generates test state when running under a browser instead of inside of +export function generateTestState(filePath: string = '', editable: boolean = false): IMainState { + const defaultSettings = getDefaultSettings(); + + return { + cellVMs: generateTestVMs(filePath, editable), + editCellVM: createEditableCellVM(1), + busy: false, + skipNextScroll: false, + undoStack: [], + redoStack: [], + submittedText: false, + rootStyle: darkStyle, + editorOptions: {}, + currentExecutionCount: 0, + knownDark: false, + baseTheme: 'vscode-light', + debugging: false, + isAtBottom: false, + font: { + size: 14, + family: "Consolas, 'Courier New', monospace" + }, + dirty: false, + codeTheme: 'Foo', + settings: defaultSettings, + focusPending: 0, + monacoReady: true, + loaded: false, + testMode: true, + kernel: { + localizedUri: 'No Kernel', + displayName: 'Python', + jupyterServerStatus: ServerStatus.NotStarted, + language: PYTHON_LANGUAGE + }, + isNotebookTrusted: true + }; +} + +export function createEmptyCell(id: string | undefined, executionCount: number | null): ICell { + const emptyCodeCell = createCodeCell(); + emptyCodeCell.execution_count = executionCount ?? null; + return { + data: emptyCodeCell, + id: id ? id : Identifiers.EditCellId, + file: Identifiers.EmptyFileName, + line: 0, + state: CellState.finished + }; +} + +export function createEditableCellVM(executionCount: number): ICellViewModel { + return { + cell: createEmptyCell(Identifiers.EditCellId, executionCount), + editable: true, + inputBlockOpen: true, + inputBlockShow: true, + inputBlockText: '', + inputBlockCollapseNeeded: false, + selected: false, + focused: false, + cursorPos: CursorPos.Current, + hasBeenRun: false, + scrollCount: 0, + runningByLine: DebugState.Design, + gathering: false + }; +} + +export function extractInputText(inputCellVM: ICellViewModel, settings: IDataScienceSettings | undefined): string { + const inputCell = inputCellVM.cell; + let source: string[] = []; + if (inputCell.data.source) { + source = splitMultilineString(cloneDeep(inputCell.data.source)); + } + const matcher = new CellMatcher(settings); + + // Eliminate the #%% on the front if it has nothing else on the line + if (source.length > 0) { + const title = matcher.exec(source[0].trim()); + if (title !== undefined && title.length <= 0) { + source.splice(0, 1); + } + // Eliminate the lines to hide if we're debugging + if (inputCell.extraLines) { + inputCell.extraLines.forEach((i) => source.splice(i, 1)); + inputCell.extraLines = undefined; + } + } + + // Eliminate breakpoint on the front if we're debugging and breakpoints are expected to be prefixed + if (source.length > 0 && inputCellVM.runDuringDebug && (!settings || settings.stopOnFirstLineWhileDebugging)) { + if (source[0].trim() === 'breakpoint()') { + source.splice(0, 1); + } + } + + return concatMultilineString(source); +} + +export function createCellVM( + inputCell: ICell, + settings: IDataScienceSettings | undefined, + editable: boolean, + runDuringDebug: boolean +): ICellViewModel { + const vm = { + cell: inputCell, + editable, + inputBlockOpen: true, + inputBlockShow: true, + inputBlockText: '', + inputBlockCollapseNeeded: false, + selected: false, + focused: false, + cursorPos: CursorPos.Current, + hasBeenRun: false, + scrollCount: 0, + runDuringDebug, + runningByLine: DebugState.Design, + gathering: false + }; + + // Update the input text + let inputLinesCount = 0; + // If the cell is markdown, initialize inputBlockText with the mardown value. + // `inputBlockText` will be used to maintain diffs of editor changes. So whether its markdown or code, we need to generate it. + const inputText = + inputCell.data.cell_type === 'code' + ? extractInputText(vm, settings) + : inputCell.data.cell_type === 'markdown' + ? concatMultilineString(vm.cell.data.source) + : ''; + if (inputText) { + inputLinesCount = inputText.split('\n').length; + } + + vm.inputBlockText = inputText; + vm.inputBlockCollapseNeeded = inputLinesCount > 1; + + return vm; +} + +function generateTestVMs(filePath: string, editable: boolean): ICellViewModel[] { + const cells = generateTestCells(filePath, 10); + return cells.map((cell: ICell) => { + const vm = createCellVM(cell, undefined, editable, false); + vm.useQuickEdit = false; + vm.hasBeenRun = true; + return vm; + }); +} + +export function generateTestCells(filePath: string, repetitions: number): ICell[] { + // Dupe a bunch times for perf reasons + let cellData: (nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | IMessageCell)[] = []; + for (let i = 0; i < repetitions; i += 1) { + cellData = [...cellData, ...generateCellData()]; + } + return cellData.map( + (data: nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | IMessageCell, key: number) => { + return { + id: key.toString(), + file: path.join(filePath, 'foo.py').toLowerCase(), + line: 1, + state: key === cellData.length - 1 ? CellState.executing : CellState.finished, + type: key === 3 ? 'preview' : 'execute', + data: data + }; + } + ); +} + +//tslint:disable:max-func-body-length +function generateCellData(): (nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | IMessageCell)[] { + // Hopefully new entries here can just be copied out of a jupyter notebook (ipynb) + return [ + { + cell_type: 'code', + execution_count: 467, + metadata: { + slideshow: { + slide_type: '-' + } + }, + outputs: [ + { + data: { + // tslint:disable-next-line: no-multiline-string + 'text/html': [ + ` + <div style=" + overflow: auto; + "> + <style scoped=""> + .dataframe tbody tr th:only-of-type { + vertical-align: middle; + } + .dataframe tbody tr th { + vertical-align: top; + } + .dataframe thead th { + text-align: right; + } + </style> + <table border="1" class="dataframe"> + <thead> + <tr style="text-align: right;"> + <th></th> + <th>0</th> + <th>1</th> + <th>2</th> + <th>3</th> + <th>4</th> + <th>5</th> + <th>6</th> + <th>7</th> + <th>8</th> + <th>9</th> + <th>...</th> + <th>2990</th> + <th>2991</th> + <th>2992</th> + <th>2993</th> + <th>2994</th> + <th>2995</th> + <th>2996</th> + <th>2997</th> + <th>2998</th> + <th>2999</th> + </tr> + <tr> + <th>idx</th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + <th></th> + </tr> + </thead> + <tbody> + <tr> + <th>2007-01-31</th> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>...</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + <td>37.060604</td> + </tr> + <tr> + <th>2007-02-28</th> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>...</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + <td>20.603407</td> + </tr> + <tr> + <th>2007-03-31</th> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>...</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + <td>6.142031</td> + </tr> + <tr> + <th>2007-04-30</th> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>...</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + <td>6.931635</td> + </tr> + <tr> + <th>2007-05-31</th> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>...</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + <td>52.642243</td> + </tr> + </tbody> + </table> + <p>5 rows × 3000 columns</p> + </div>` + ] + }, + execution_count: 4, + metadata: {}, + output_type: 'execute_result' + } + ], + source: [ + 'myvar = """ # Lorem Ipsum\n', + '\n', + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n', + 'Nullam eget varius ligula, eget fermentum mauris.\n', + 'Cras ultrices, enim sit amet iaculis ornare, nisl nibh aliquet elit, sed ultrices velit ipsum dignissim nisl.\n', + 'Nunc quis orci ante. Vivamus vel blandit velit.\n","Sed mattis dui diam, et blandit augue mattis vestibulum.\n', + 'Suspendisse ornare interdum velit. Suspendisse potenti.\n', + 'Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi.\n', + '"""' + ] + }, + { + cell_type: 'markdown', + metadata: {}, + source: ['## Cell 3\n', "Here's some markdown\n", '- A List\n', '- Of Items'] + }, + { + cell_type: 'code', + execution_count: 1, + metadata: {}, + outputs: [ + { + ename: 'NameError', + evalue: 'name "df" is not defined', + output_type: 'error', + traceback: [ + '\u001b[1;31m---------------------------------------------------------------------------\u001b[0m', + '\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)', + '\u001b[1;32m<ipython-input-1-00cf07b74dcd>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mdf\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m', + '\u001b[1;31mNameError\u001b[0m: name "df" is not defined' + ] + } + ], + source: ['df'] + }, + { + cell_type: 'code', + execution_count: 1, + metadata: {}, + outputs: [ + { + ename: 'NameError', + evalue: 'name "df" is not defined', + output_type: 'error', + traceback: [ + '\u001b[1;31m---------------------------------------------------------------------------\u001b[0m', + '\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)', + '\u001b[1;32m<ipython-input-1-00cf07b74dcd>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m()\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mdf\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m', + '\u001b[1;31mNameError\u001b[0m: name "df" is not defined' + ] + } + ], + source: ['df'] + } + ]; +} diff --git a/src/datascience-ui/interactive-common/markdown.tsx b/src/datascience-ui/interactive-common/markdown.tsx new file mode 100644 index 000000000000..a6a8e083a183 --- /dev/null +++ b/src/datascience-ui/interactive-common/markdown.tsx @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as React from 'react'; + +import { IKeyboardEvent } from '../react-common/event'; +import { IMonacoModelContentChangeEvent } from '../react-common/monacoHelpers'; +import { Editor } from './editor'; +import { CursorPos, IFont } from './mainState'; + +export interface IMarkdownProps { + markdown: string; + version: number; + codeTheme: string; + testMode: boolean; + monacoTheme: string | undefined; + outermostParentClass: string; + editorOptions?: monacoEditor.editor.IEditorOptions; + editorMeasureClassName?: string; + showLineNumbers?: boolean; + useQuickEdit?: boolean; + font: IFont; + hasFocus: boolean; + cursorPos: CursorPos | monacoEditor.IPosition; + disableUndoStack: boolean; + readOnly: boolean; + onCreated(code: string, modelId: string): void; + onChange(e: IMonacoModelContentChangeEvent): void; + focused?(): void; + unfocused?(): void; + openLink(uri: monacoEditor.Uri): void; + keyDown?(e: IKeyboardEvent): void; +} + +export class Markdown extends React.Component<IMarkdownProps> { + private editorRef: React.RefObject<Editor> = React.createRef<Editor>(); + + constructor(prop: IMarkdownProps) { + super(prop); + } + + public render() { + const classes = 'markdown-editor-area'; + + return ( + <div className={classes}> + <Editor + codeTheme={this.props.codeTheme} + readOnly={this.props.readOnly} + history={undefined} + onCreated={this.props.onCreated} + onChange={this.props.onChange} + testMode={this.props.testMode} + content={this.props.markdown} + outermostParentClass={this.props.outermostParentClass} + monacoTheme={this.props.monacoTheme} + language="markdown" + editorOptions={this.props.editorOptions} + openLink={this.props.openLink} + ref={this.editorRef} + editorMeasureClassName={this.props.editorMeasureClassName} + keyDown={this.props.keyDown} + hasFocus={this.props.hasFocus} + cursorPos={this.props.cursorPos} + focused={this.props.focused} + unfocused={this.props.unfocused} + showLineNumbers={this.props.showLineNumbers} + useQuickEdit={this.props.useQuickEdit} + font={this.props.font} + disableUndoStack={this.props.disableUndoStack} + version={this.props.version} + ipLocation={undefined} + /> + </div> + ); + } + + public getContents(): string | undefined { + if (this.editorRef.current) { + return this.editorRef.current.getContents(); + } + } +} diff --git a/src/datascience-ui/interactive-common/markdownManipulation.ts b/src/datascience-ui/interactive-common/markdownManipulation.ts new file mode 100644 index 000000000000..f35c06dc7ffd --- /dev/null +++ b/src/datascience-ui/interactive-common/markdownManipulation.ts @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable-next-line:no-require-imports no-var-requires +const _escapeRegExp = require('lodash/escapeRegExp') as typeof import('lodash/escapeRegExp'); + +export function fixMarkdown(input: string, wrapSingles: boolean = false): string { + const latexFixed = fixLatex(input, wrapSingles); + + try { + return fixLinks(latexFixed); + } catch { + return latexFixed; + } +} + +// Adds '$$' to latex formulas that don't have a '$', allowing users to input the formula directly. +// +// The general algorithm here is: +// Search for either $$ or $ or a \begin{name} item. +// If a $$ or $ is found, output up to the next dollar sign +// If a \begin{name} is found, find the matching \end{name}, wrap the section in $$ and output up to the \end. +// +// LaTeX seems to follow the pattern of \begin{name} or is escaped with $$ or $. See here for a bunch of examples: +// https://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Typesetting%20Equations.html +export function fixLatex(input: string, wrapSingles: boolean = false): string { + const output: string[] = []; + + // change latex + // Search for begin/end pairs, outputting as we go + let start = 0; + + // Loop until we run out string + while (start < input.length) { + // Check $$, $ and begin + const dollars = /\$\$/.exec(input.substr(start)); + const dollar = /\$/.exec(input.substr(start)); + const begin = /\\begin\{([a-z,\*]+)\}/.exec(input.substr(start)); + let endRegex = /\$\$/; + let endRegexLength = 2; + + // Pick the first that matches + let match = dollars; + let isBeginMatch = false; + const isDollarsMatch = dollars?.index === dollar?.index; + if (!match || (dollar && dollar.index < match.index)) { + match = dollar; + endRegex = /\$/; + endRegexLength = 1; + } + if (!match || (begin && begin.index < match.index)) { + match = begin; + endRegex = begin ? new RegExp(`\\\\end\\{${_escapeRegExp(begin[1])}\\}`) : /\$/; + endRegexLength = begin ? `\\end{${begin[1]}}`.length : 1; + isBeginMatch = true; + } + + // Output this match + if (match) { + if (isBeginMatch) { + // Begin match is a little more complicated. + const offset = match.index + start; + const end = endRegex.exec(input.substr(start)); + if (end) { + const prefix = input.substr(start, match.index); + const wrapped = input.substr(offset, endRegexLength + end.index - match.index); + output.push(`${prefix}\n$$\n${wrapped}\n$$\n`); + start = start + prefix.length + wrapped.length; + } else { + // Invalid, just return + return input; + } + } else if (isDollarsMatch) { + // Output till the next $$ + const offset = match.index + 2 + start; + const endDollar = endRegex.exec(input.substr(offset)); + if (endDollar) { + const length = endDollar.index + 2; + const before = input.substr(start, offset - start); + const after = input.substr(offset, length); + output.push(`${before}${after}`); + start = offset + length; + } else { + // Invalid, just return + return input; + } + } else { + // Output till the next $ (wrapping in an extra $ so it works with latex cells too) + const offset = match.index + 1 + start; + const endDollar = endRegex.exec(input.substr(offset)); + if (endDollar) { + const length = endDollar.index + 1; + const before = input.substr(start, offset - start); + const after = input.substr(offset, length); + output.push(wrapSingles ? `${before}$${after}$` : `${before}${after}`); + start = offset + length; + } else { + // Invalid, just return + return input; + } + } + } else { + // No more matches + output.push(input.substr(start)); + start = input.length; + } + } + + return output.join(''); +} + +// Look for HTML 'A' tags to replace them with the Markdown format +export function fixLinks(input: string): string { + let linkStartIndex = input.indexOf('<a'); + while (linkStartIndex !== -1) { + const linkEnd = '</a>'; + const linkEndIndex = input.indexOf(linkEnd, linkStartIndex); + + if (linkEndIndex !== -1) { + const hferIndex = input.indexOf('href', linkStartIndex); + + const quoteSearch1 = input.indexOf("'", hferIndex); + const urlStartIndex = quoteSearch1 === -1 ? input.indexOf('"', hferIndex) : quoteSearch1; + + const quoteSearch2 = input.indexOf("'", urlStartIndex + 1); + const urlEndIndex = quoteSearch2 === -1 ? input.indexOf('"', urlStartIndex + 1) : quoteSearch2; + + const url = input.substring(urlStartIndex + 1, urlEndIndex); + + const textStartIndex = input.indexOf('>', linkStartIndex); + + if (textStartIndex < linkEndIndex) { + const text = input.substring(textStartIndex + 1, linkEndIndex); + input = input.replace( + input.substring(linkStartIndex, linkEndIndex + linkEnd.length), + `[${text}](${url})` + ); + } + } + + linkStartIndex = input.indexOf('<a', linkStartIndex + 1); + } + + return input; +} diff --git a/src/datascience-ui/interactive-common/redux/helpers.ts b/src/datascience-ui/interactive-common/redux/helpers.ts new file mode 100644 index 000000000000..051fe2ddc0d5 --- /dev/null +++ b/src/datascience-ui/interactive-common/redux/helpers.ts @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as Redux from 'redux'; +import { StartPageMessages } from '../../../client/common/startPage/types'; +import { + IInteractiveWindowMapping, + InteractiveWindowMessages +} from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { + checkToPostBasedOnOriginalMessageType, + MessageType, + shouldRebroadcast +} from '../../../client/datascience/interactive-common/synchronization'; +import { BaseReduxActionPayload, SyncPayload } from '../../../client/datascience/interactive-common/types'; +import { CssMessages, SharedMessages } from '../../../client/datascience/messages'; +import { QueueAnotherFunc } from '../../react-common/reduxUtils'; +import { CommonActionType, CommonActionTypeMapping } from './reducers/types'; + +const AllowedMessages = [ + ...Object.values(InteractiveWindowMessages), + ...Object.values(CssMessages), + ...Object.values(SharedMessages), + ...Object.values(CommonActionType), + ...Object.values(StartPageMessages) +]; +export function isAllowedMessage(message: string) { + // tslint:disable-next-line: no-any + return AllowedMessages.includes(message as any); +} +export function isAllowedAction(action: Redux.AnyAction) { + return isAllowedMessage(action.type); +} + +type ReducerArg = { + // tslint:disable-next-line: no-any + queueAction: QueueAnotherFunc<any>; + // tslint:disable-next-line: no-any + payload?: BaseReduxActionPayload<any>; +}; + +export function queueIncomingActionWithPayload< + M extends IInteractiveWindowMapping & CommonActionTypeMapping, + K extends keyof M +>(originalReducerArg: ReducerArg, type: K, data: M[K]): void { + if (!checkToPostBasedOnOriginalMessageType(originalReducerArg.payload?.messageType)) { + return; + } + + // tslint:disable-next-line: no-any + const action = { type, payload: { data, messageDirection: 'incoming' } as any } as any; + originalReducerArg.queueAction(action); +} + +export function queueIncomingAction<M extends IInteractiveWindowMapping & CommonActionTypeMapping, K extends keyof M>( + originalReducerArg: ReducerArg, + type: K +): void { + // tslint:disable-next-line: no-any + queueIncomingActionWithPayload(originalReducerArg, type as any, undefined); +} + +/** + * Post a message to the extension (via dispatcher actions). + */ +export function postActionToExtension<K, M extends IInteractiveWindowMapping, T extends keyof M = keyof M>( + originalReducerArg: ReducerArg, + message: T, + payload?: M[T] +): void; +/** + * Post a message to the extension (via dispatcher actions). + */ +// tslint:disable-next-line: unified-signatures +export function postActionToExtension<K, M extends IInteractiveWindowMapping, T extends keyof M = keyof M>( + originalReducerArg: ReducerArg, + message: T, + payload?: M[T] +): void; +// tslint:disable-next-line: no-any +export function postActionToExtension(originalReducerArg: ReducerArg, message: any, payload?: any) { + if (!checkToPostBasedOnOriginalMessageType(originalReducerArg.payload?.messageType)) { + return; + } + + // tslint:disable-next-line: no-any + const newPayload: BaseReduxActionPayload<any> = ({ + data: payload, + messageDirection: 'outgoing', + messageType: MessageType.other + // tslint:disable-next-line: no-any + } as any) as BaseReduxActionPayload<any>; + const action = { type: CommonActionType.PostOutgoingMessage, payload: { payload: newPayload, type: message } }; + originalReducerArg.queueAction(action); +} +export function unwrapPostableAction( + action: Redux.AnyAction +): { type: keyof IInteractiveWindowMapping; payload?: BaseReduxActionPayload<{}> } { + // Unwrap the payload that was created in `createPostableAction`. + const type = action.type; + const payload: BaseReduxActionPayload<{}> | undefined = action.payload; + return { type, payload }; +} + +/** + * Whether this is a message type that indicates it is part of a scynchronization message. + */ +export function isSyncingMessage(messageType?: MessageType) { + if (!messageType) { + return false; + } + + return ( + (messageType && MessageType.syncAcrossSameNotebooks) === MessageType.syncAcrossSameNotebooks || + (messageType && MessageType.syncWithLiveShare) === MessageType.syncWithLiveShare + ); +} +export function reBroadcastMessageIfRequired( + dispatcher: Function, + message: InteractiveWindowMessages | SharedMessages | CommonActionType | CssMessages, + payload?: BaseReduxActionPayload<{}> +) { + const messageType = payload?.messageType || 0; + if ( + message === InteractiveWindowMessages.Sync || + (messageType && MessageType.syncAcrossSameNotebooks) === MessageType.syncAcrossSameNotebooks || + (messageType && MessageType.syncWithLiveShare) === MessageType.syncWithLiveShare || + payload?.messageDirection === 'outgoing' + ) { + return; + } + // Check if we need to re-broadcast this message to other editors/sessions. + // tslint:disable-next-line: no-any + const result = shouldRebroadcast(message as any, payload); + if (result[0]) { + // Mark message as incoming, to indicate this will be sent into the other webviews. + // tslint:disable-next-line: no-any + const syncPayloadData: BaseReduxActionPayload<any> = { + data: payload?.data, + messageType: result[1], + messageDirection: 'incoming' + }; + // tslint:disable-next-line: no-any + const syncPayload: SyncPayload = { type: message, payload: syncPayloadData }; + // First focus on UX perf, hence the setTimeout (i.e. ensure other code in event loop executes). + setTimeout(() => dispatcher(InteractiveWindowMessages.Sync, syncPayload), 1); + } +} diff --git a/src/datascience-ui/interactive-common/redux/postOffice.ts b/src/datascience-ui/interactive-common/redux/postOffice.ts new file mode 100644 index 000000000000..bfddb6a37a90 --- /dev/null +++ b/src/datascience-ui/interactive-common/redux/postOffice.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as Redux from 'redux'; + +import { + IInteractiveWindowMapping, + IPyWidgetMessages +} from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { BaseReduxActionPayload } from '../../../client/datascience/interactive-common/types'; +import { PostOffice } from '../../react-common/postOffice'; +import { isAllowedAction, reBroadcastMessageIfRequired, unwrapPostableAction } from './helpers'; +import { CommonActionType } from './reducers/types'; + +export const AllowedIPyWidgetMessages = [...Object.values(IPyWidgetMessages)]; + +export function generatePostOfficeSendReducer(postOffice: PostOffice): Redux.Reducer<{}, Redux.AnyAction> { + // tslint:disable-next-line: no-function-expression + return function (_state: {} | undefined, action: Redux.AnyAction): {} { + if (isAllowedAction(action)) { + // Make sure a valid message + if (action.type === CommonActionType.PostOutgoingMessage) { + const { type, payload } = unwrapPostableAction(action.payload); + // Just post this to the post office. + // tslint:disable-next-line: no-any + postOffice.sendMessage<IInteractiveWindowMapping>(type, payload?.data as any); + } else { + const payload: BaseReduxActionPayload<{}> | undefined = action.payload; + // Do not rebroadcast messages that have been sent through as part of a synchronization packet. + // If `messageType` is a number, then its some part of a synchronization packet. + if (payload?.messageDirection === 'incoming') { + reBroadcastMessageIfRequired(postOffice.sendMessage.bind(postOffice), action.type, action?.payload); + } + } + } + + // We don't modify the state. + return {}; + }; +} diff --git a/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts b/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts new file mode 100644 index 000000000000..5ad40578894a --- /dev/null +++ b/src/datascience-ui/interactive-common/redux/reducers/commonEffects.ts @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { nbformat } from '@jupyterlab/coreutils'; +import type { KernelMessage } from '@jupyterlab/services'; +import { Identifiers } from '../../../../client/datascience/constants'; +import { InteractiveWindowMessages } from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { IGetCssResponse } from '../../../../client/datascience/messages'; +import { IGetMonacoThemeResponse } from '../../../../client/datascience/monacoMessages'; +import { CellState, ICell } from '../../../../client/datascience/types'; +import { ICellViewModel, IMainState } from '../../../interactive-common/mainState'; +import { Helpers } from '../../../interactive-common/redux/reducers/helpers'; +import { getLocString, storeLocStrings } from '../../../react-common/locReactSide'; +import { postActionToExtension } from '../helpers'; +import { Transfer } from './transfer'; +import { + CommonActionType, + CommonReducerArg, + ILoadIPyWidgetClassFailureAction, + IOpenSettingsAction, + LoadIPyWidgetClassLoadAction, + NotifyIPyWidgeWidgetVersionNotSupportedAction +} from './types'; + +export namespace CommonEffects { + export function notebookDirty(arg: CommonReducerArg): IMainState { + return { + ...arg.prevState, + dirty: true + }; + } + + export function notebookClean(arg: CommonReducerArg): IMainState { + return { + ...arg.prevState, + dirty: false + }; + } + + export function trustNotebook(arg: CommonReducerArg): IMainState { + return { + ...arg.prevState, + isNotebookTrusted: true + }; + } + + export function startProgress(arg: CommonReducerArg): IMainState { + return { + ...arg.prevState, + busy: true + }; + } + + export function stopProgress(arg: CommonReducerArg): IMainState { + return { + ...arg.prevState, + busy: false + }; + } + + export function activate(arg: CommonReducerArg): IMainState { + return focusPending(arg.prevState); + } + + export function focusInput(arg: CommonReducerArg): IMainState { + return focusPending(arg.prevState); + } + + export function handleLocInit(arg: CommonReducerArg<CommonActionType, string>): IMainState { + // Read in the loc strings + const locJSON = JSON.parse(arg.payload.data); + storeLocStrings(locJSON); + return arg.prevState; + } + + export function handleCss(arg: CommonReducerArg<CommonActionType, IGetCssResponse>): IMainState { + // Recompute our known dark value from the class name in the body + // VS code should update this dynamically when the theme changes + const computedKnownDark = Helpers.computeKnownDark(arg.prevState.settings); + + // We also get this in our response, but computing is more reliable + // than searching for it. + const newBaseTheme = + arg.prevState.knownDark !== computedKnownDark && !arg.prevState.testMode + ? computedKnownDark + ? 'vscode-dark' + : 'vscode-light' + : arg.prevState.baseTheme; + + let fontSize: number = 14; + let fontFamily: string = "Consolas, 'Courier New', monospace"; + const sizeSetting = '--code-font-size: '; + const familySetting = '--code-font-family: '; + const fontSizeIndex = arg.payload.data.css.indexOf(sizeSetting); + const fontFamilyIndex = arg.payload.data.css.indexOf(familySetting); + + if (fontSizeIndex > -1) { + const fontSizeEndIndex = arg.payload.data.css.indexOf('px;', fontSizeIndex + sizeSetting.length); + fontSize = parseInt( + arg.payload.data.css.substring(fontSizeIndex + sizeSetting.length, fontSizeEndIndex), + 10 + ); + } + + if (fontFamilyIndex > -1) { + const fontFamilyEndIndex = arg.payload.data.css.indexOf(';', fontFamilyIndex + familySetting.length); + fontFamily = arg.payload.data.css.substring(fontFamilyIndex + familySetting.length, fontFamilyEndIndex); + } + + return { + ...arg.prevState, + rootCss: arg.payload.data.css, + font: { + size: fontSize, + family: fontFamily + }, + vscodeThemeName: arg.payload.data.theme, + knownDark: computedKnownDark, + baseTheme: newBaseTheme + }; + } + + export function monacoReady<T>(arg: CommonReducerArg<T>): IMainState { + return { + ...arg.prevState, + monacoReady: true + }; + } + + export function monacoThemeChange<T>(arg: CommonReducerArg<T, IGetMonacoThemeResponse>): IMainState { + return { + ...arg.prevState, + monacoTheme: Identifiers.GeneratedThemeName + }; + } + + function focusPending(prevState: IMainState): IMainState { + return { + ...prevState, + // This is only applicable for interactive window & not native editor. + focusPending: prevState.focusPending + 1 + }; + } + + export function openSettings(arg: CommonReducerArg<CommonActionType, IOpenSettingsAction>): IMainState { + postActionToExtension(arg, InteractiveWindowMessages.OpenSettings, arg.payload.data.setting); + return arg.prevState; + } + + export function handleUpdateDisplayData( + arg: CommonReducerArg<CommonActionType, KernelMessage.IUpdateDisplayDataMsg> + ): IMainState { + const newCells: ICell[] = []; + const oldCells: ICell[] = []; + + // Find any cells that have this display_id + const newVMs = arg.prevState.cellVMs.map((c: ICellViewModel) => { + if (c.cell.data.cell_type === 'code') { + let isMatch = false; + const data: nbformat.ICodeCell = c.cell.data as nbformat.ICodeCell; + const changedOutputs = data.outputs.map((o) => { + if ( + (o.output_type === 'display_data' || o.output_type === 'execute_result') && + o.transient && + // tslint:disable-next-line: no-any + (o.transient as any).display_id === arg.payload.data.content.transient.display_id + ) { + // Remember this as a match + isMatch = true; + + // If the output has this display_id, update the output + return { + ...o, + data: arg.payload.data.content.data, + metadata: arg.payload.data.content.metadata + }; + } else { + return o; + } + }); + + // Save in our new cell list so we can tell the extension + // about our update + const newCell = isMatch + ? Helpers.asCell({ + ...c.cell, + data: { + ...c.cell.data, + outputs: changedOutputs + } + }) + : c.cell; + if (isMatch) { + newCells.push(newCell); + } else { + oldCells.push(newCell); + } + return Helpers.asCellViewModel({ + ...c, + cell: newCell + }); + } else { + oldCells.push(c.cell); + return c; + } + }); + + // If we found the display id, then an update happened. Tell the model about it + if (newCells.length) { + Transfer.postModelCellUpdate(arg, newCells, oldCells); + } + + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + export function handleLoadIPyWidgetClassSuccess( + arg: CommonReducerArg<CommonActionType, LoadIPyWidgetClassLoadAction> + ): IMainState { + // Make sure to tell the extension so it can log telemetry. + postActionToExtension(arg, InteractiveWindowMessages.IPyWidgetLoadSuccess, arg.payload.data); + return arg.prevState; + } + export function handleLoadIPyWidgetClassFailure( + arg: CommonReducerArg<CommonActionType, ILoadIPyWidgetClassFailureAction> + ): IMainState { + // Find the first currently executing cell and add an error to its output + let index = arg.prevState.cellVMs.findIndex((c) => c.cell.state === CellState.executing); + + // If there isn't one, then find the latest that matches the current execution count. + if (index < 0) { + index = arg.prevState.cellVMs.findIndex( + (c) => c.cell.data.execution_count === arg.prevState.currentExecutionCount + ); + } + if (index >= 0 && arg.prevState.cellVMs[index].cell.data.cell_type === 'code') { + const newVMs = [...arg.prevState.cellVMs]; + const current = arg.prevState.cellVMs[index]; + + let errorMessage = arg.payload.data.error.toString(); + if (!arg.payload.data.isOnline) { + errorMessage = getLocString( + 'DataScience.loadClassFailedWithNoInternet', + 'Error loading {0}:{1}. Internet connection required for loading 3rd party widgets.' + ).format(arg.payload.data.moduleName, arg.payload.data.moduleVersion); + } else if (!arg.payload.data.cdnsUsed) { + errorMessage = getLocString( + 'DataScience.enableCDNForWidgetsSetting', + "Widgets require us to download supporting files from a 3rd party website. Click <a href='https://command:python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'>here</a> to enable this or click <a href='https://aka.ms/PVSCIPyWidgets'>here</a> for more information. (Error loading {0}:{1})." + ).format(arg.payload.data.moduleName, arg.payload.data.moduleVersion); + } + // Preserve existing error messages. + const existingErrorMessage = current.uiSideError ? `${current.uiSideError}\n` : ''; + newVMs[index] = Helpers.asCellViewModel({ + ...current, + uiSideError: `${existingErrorMessage}${errorMessage}` + }); + + // Make sure to tell the extension so it can log telemetry. + postActionToExtension(arg, InteractiveWindowMessages.IPyWidgetLoadFailure, arg.payload.data); + + return { + ...arg.prevState, + cellVMs: newVMs + }; + } else { + return arg.prevState; + } + } + export function notifyAboutUnsupportedWidgetVersions( + arg: CommonReducerArg<CommonActionType, NotifyIPyWidgeWidgetVersionNotSupportedAction> + ): IMainState { + // Find the first currently executing cell and add an error to its output + let index = arg.prevState.cellVMs.findIndex((c) => c.cell.state === CellState.executing); + + // If there isn't one, then find the latest that matches the current execution count. + if (index < 0) { + index = arg.prevState.cellVMs.findIndex( + (c) => c.cell.data.execution_count === arg.prevState.currentExecutionCount + ); + } + if (index >= 0 && arg.prevState.cellVMs[index].cell.data.cell_type === 'code') { + const newVMs = [...arg.prevState.cellVMs]; + const current = arg.prevState.cellVMs[index]; + + const errorMessage = getLocString( + 'DataScience.qgridWidgetScriptVersionCompatibilityWarning', + "Unable to load a compatible version of the widget 'qgrid'. Consider downgrading to version 1.1.1." + ); + newVMs[index] = Helpers.asCellViewModel({ + ...current, + uiSideError: errorMessage + }); + + // Make sure to tell the extension so it can log telemetry. + postActionToExtension(arg, InteractiveWindowMessages.IPyWidgetWidgetVersionNotSupported, arg.payload.data); + + return { + ...arg.prevState, + cellVMs: newVMs + }; + } else { + return arg.prevState; + } + } + export function handleIPyWidgetRenderFailure(arg: CommonReducerArg<CommonActionType, Error>): IMainState { + // Make sure to tell the extension so it can log telemetry. + postActionToExtension(arg, InteractiveWindowMessages.IPyWidgetRenderFailure, arg.payload.data); + return arg.prevState; + } +} diff --git a/src/datascience-ui/interactive-common/redux/reducers/helpers.ts b/src/datascience-ui/interactive-common/redux/reducers/helpers.ts new file mode 100644 index 000000000000..7d2261053822 --- /dev/null +++ b/src/datascience-ui/interactive-common/redux/reducers/helpers.ts @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { min } from 'lodash'; +// tslint:disable-next-line: no-require-imports no-var-requires +const cloneDeep = require('lodash/cloneDeep'); + +import { CellState, ICell, IDataScienceExtraSettings } from '../../../../client/datascience/types'; +import { arePathsSame } from '../../../react-common/arePathsSame'; +import { detectBaseTheme } from '../../../react-common/themeDetector'; +import { DebugState, ICellViewModel, IMainState } from '../../mainState'; +import { CommonActionType, CommonReducerArg } from './types'; + +const StackLimit = 10; + +export namespace Helpers { + export function computeKnownDark(settings?: IDataScienceExtraSettings): boolean { + const ignore = settings?.ignoreVscodeTheme ? true : false; + const baseTheme = ignore ? 'vscode-light' : detectBaseTheme(); + return baseTheme !== 'vscode-light'; + } + + export function pushStack(stack: ICellViewModel[][], cells: ICellViewModel[]) { + // Get the undo stack up to the maximum length + const slicedUndo = stack.slice(0, min([stack.length, StackLimit])); + + // make a copy of the cells so that further changes don't modify them. + const copy = cloneDeep(cells); + return [...slicedUndo, copy]; + } + + export function firstCodeCellAbove(state: IMainState, cellId: string | undefined) { + const codeCells = state.cellVMs.filter((c) => c.cell.data.cell_type === 'code'); + const index = codeCells.findIndex((c) => c.cell.id === cellId); + if (index > 0) { + return codeCells[index - 1].cell.id; + } + return undefined; + } + + // This function is because the unit test typescript compiler can't handle ICell.metadata + // tslint:disable-next-line: no-any + export function asCellViewModel(cvm: any): ICellViewModel { + return cvm as ICellViewModel; + } + + // This function is because the unit test typescript compiler can't handle ICell.metadata + // tslint:disable-next-line: no-any + export function asCell(cell: any): ICell { + return cell as ICell; + } + + export function updateOrAdd( + arg: CommonReducerArg<CommonActionType, ICell>, + generateVM: (cell: ICell, mainState: IMainState) => ICellViewModel + ): IMainState { + // First compute new execution count. + const newExecutionCount = arg.payload.data.data.execution_count + ? Math.max( + arg.prevState.currentExecutionCount, + parseInt(arg.payload.data.data.execution_count.toString(), 10) + ) + : arg.prevState.currentExecutionCount; + + const index = arg.prevState.cellVMs.findIndex((c: ICellViewModel) => { + return ( + c.cell.id === arg.payload.data.id && + c.cell.line === arg.payload.data.line && + arePathsSame(c.cell.file, arg.payload.data.file) + ); + }); + if (index >= 0) { + // This means the cell existed already so it was actual executed code. + // Use its execution count to update our execution count. + const finished = + arg.payload.data.state === CellState.finished || arg.payload.data.state === CellState.error; + + // Have to make a copy of the cell VM array or + // we won't actually update. + const newVMs = [...arg.prevState.cellVMs]; + + // Live share has been disabled for now, see https://github.com/microsoft/vscode-python/issues/7972 + // Check to see if our code still matches for the cell (in liveshare it might be updated from the other side) + // if (concatMultilineString(arg.prevState.cellVMs[index].cell.data.source) !== concatMultilineString(cell.data.source)) { + + // Prevent updates to the source, as its possible we have recieved a response for a cell execution + // and the user has updated the cell text since then. + const newVM: ICellViewModel = { + ...newVMs[index], + hasBeenRun: true, + cell: { + ...newVMs[index].cell, + state: arg.payload.data.state, + data: { + ...arg.payload.data.data, + source: newVMs[index].cell.data.source + } + }, + runningByLine: finished ? DebugState.Design : newVMs[index].runningByLine + }; + newVMs[index] = newVM; + + return { + ...arg.prevState, + cellVMs: newVMs, + currentExecutionCount: newExecutionCount + }; + } else { + // This is an entirely new cell (it may have started out as finished) + const newVM = generateVM(arg.payload.data, arg.prevState); + const newVMs = [...arg.prevState.cellVMs, newVM]; + return { + ...arg.prevState, + cellVMs: newVMs, + undoStack: pushStack(arg.prevState.undoStack, arg.prevState.cellVMs), + currentExecutionCount: newExecutionCount + }; + } + } +} diff --git a/src/datascience-ui/interactive-common/redux/reducers/kernel.ts b/src/datascience-ui/interactive-common/redux/reducers/kernel.ts new file mode 100644 index 000000000000..7f8f2a8ad95c --- /dev/null +++ b/src/datascience-ui/interactive-common/redux/reducers/kernel.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { InteractiveWindowMessages } from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { CellState } from '../../../../client/datascience/types'; +import { IMainState, IServerState } from '../../mainState'; +import { postActionToExtension } from '../helpers'; +import { CommonActionType, CommonReducerArg } from './types'; + +export namespace Kernel { + // tslint:disable-next-line: no-any + export function selectKernel( + arg: CommonReducerArg<CommonActionType | InteractiveWindowMessages, IServerState | undefined> + ): IMainState { + postActionToExtension(arg, InteractiveWindowMessages.SelectKernel); + + return arg.prevState; + } + export function selectJupyterURI(arg: CommonReducerArg): IMainState { + postActionToExtension(arg, InteractiveWindowMessages.SelectJupyterServer); + + return arg.prevState; + } + export function restartKernel(arg: CommonReducerArg): IMainState { + postActionToExtension(arg, InteractiveWindowMessages.RestartKernel); + + return arg.prevState; + } + + export function interruptKernel(arg: CommonReducerArg): IMainState { + postActionToExtension(arg, InteractiveWindowMessages.Interrupt); + + return arg.prevState; + } + + export function updateStatus( + arg: CommonReducerArg<CommonActionType | InteractiveWindowMessages, IServerState | undefined> + ): IMainState { + if (arg.payload.data) { + return { + ...arg.prevState, + kernel: { + localizedUri: arg.payload.data.localizedUri, + jupyterServerStatus: arg.payload.data.jupyterServerStatus, + displayName: arg.payload.data.displayName, + language: arg.payload.data.language + } + }; + } + return arg.prevState; + } + + export function handleRestarted<T>(arg: CommonReducerArg<T>) { + // When we restart, make sure to turn off all executing cells. They aren't executing anymore + const newVMs = [...arg.prevState.cellVMs]; + newVMs.forEach((vm, i) => { + if (vm.cell.state !== CellState.finished && vm.cell.state !== CellState.error) { + newVMs[i] = { ...vm, hasBeenRun: false, cell: { ...vm.cell, state: CellState.finished } }; + } + }); + + return { + ...arg.prevState, + cellVMs: newVMs, + pendingVariableCount: 0, + variables: [], + currentExecutionCount: 0 + }; + } +} diff --git a/src/datascience-ui/interactive-common/redux/reducers/monaco.ts b/src/datascience-ui/interactive-common/redux/reducers/monaco.ts new file mode 100644 index 000000000000..532fff8ce1cd --- /dev/null +++ b/src/datascience-ui/interactive-common/redux/reducers/monaco.ts @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { Reducer } from 'redux'; + +import { PYTHON_LANGUAGE } from '../../../../client/common/constants'; +import { createDeferred } from '../../../../client/common/utils/async'; +import { Identifiers } from '../../../../client/datascience/constants'; +import { + ILoadTmLanguageResponse, + InteractiveWindowMessages, + IProvideCompletionItemsResponse, + IProvideHoverResponse, + IProvideSignatureHelpResponse, + IResolveCompletionItemResponse +} from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { deserializeLanguageConfiguration } from '../../../../client/datascience/interactive-common/serialization'; +import { BaseReduxActionPayload } from '../../../../client/datascience/interactive-common/types'; +import { CssMessages } from '../../../../client/datascience/messages'; +import { IGetMonacoThemeResponse } from '../../../../client/datascience/monacoMessages'; +import { PostOffice } from '../../../react-common/postOffice'; +import { combineReducers, QueuableAction, ReducerArg, ReducerFunc } from '../../../react-common/reduxUtils'; +import { IntellisenseProvider } from '../../intellisenseProvider'; +import { IServerState } from '../../mainState'; +import { Tokenizer } from '../../tokenizer'; +import { postActionToExtension, queueIncomingAction } from '../helpers'; +import { CommonActionType, ICodeCreatedAction, IEditCellAction } from './types'; + +// Global state so we only load the onigasm bits once. +const onigasmPromise = createDeferred<boolean>(); + +export interface IMonacoState { + testMode: boolean; + intellisenseProvider: IntellisenseProvider | undefined; + postOffice: PostOffice; + language: string; +} + +type MonacoReducerFunc<T = never | undefined> = ReducerFunc< + IMonacoState, + CommonActionType | InteractiveWindowMessages, + BaseReduxActionPayload<T> +>; + +type MonacoReducerArg<T = never | undefined> = ReducerArg< + IMonacoState, + CommonActionType | InteractiveWindowMessages, + BaseReduxActionPayload<T> +>; + +function handleLoaded<T>(arg: MonacoReducerArg<T>): IMonacoState { + // Send the requests to get the onigasm and tmlanguage data if necessary + if (!Tokenizer.hasOnigasm()) { + postActionToExtension(arg, InteractiveWindowMessages.LoadOnigasmAssemblyRequest); + } + if (arg.prevState.language && !Tokenizer.hasLanguage(arg.prevState.language)) { + postActionToExtension(arg, InteractiveWindowMessages.LoadTmLanguageRequest, arg.prevState.language); + } + // If have both, tell other side monaco is ready + if (Tokenizer.hasOnigasm() && Tokenizer.hasLanguage(arg.prevState.language)) { + onigasmPromise.resolve(true); + + // Both queue to the reducers and to the extension side that we're ready + queueIncomingAction(arg, InteractiveWindowMessages.MonacoReady); + postActionToExtension(arg, InteractiveWindowMessages.MonacoReady); + } + + return arg.prevState; +} + +function handleStarted<T>(arg: MonacoReducerArg<T>): IMonacoState { + // When the window is first starting up, create our intellisense provider + // + // Note: We're not using arg.queueAction to send messages because of two reasons + // 1) The queueAction would be used outside of a reducer. This is a no no because its state would be off + // 2) A reducer can cause an IntellisenseProvider update, this would mean we'd be dispatching inside of a reducer + // and that's not allowed in redux. + // So instead, just post messages directly. + if (!arg.prevState.intellisenseProvider && arg.prevState.postOffice) { + return { + ...arg.prevState, + intellisenseProvider: new IntellisenseProvider( + arg.prevState.postOffice.sendMessage.bind(arg.prevState.postOffice), + arg.prevState.language ?? PYTHON_LANGUAGE + ) + }; + } + + return arg.prevState; +} + +function handleLoadOnigasmResponse(arg: MonacoReducerArg<Buffer>): IMonacoState { + if (!Tokenizer.hasOnigasm()) { + // Have to convert the buffer into an ArrayBuffer for the tokenizer to load it. + let typedArray = new Uint8Array(arg.payload.data); + if (typedArray.length <= 0) { + // tslint:disable-next-line: no-any + typedArray = new Uint8Array((arg.payload.data as any).data); + } + Tokenizer.loadOnigasm(typedArray.buffer); + onigasmPromise.resolve(true); + } + + return arg.prevState; +} + +function handleLoadTmLanguageResponse(arg: MonacoReducerArg<ILoadTmLanguageResponse>): IMonacoState { + // First make sure we have the onigasm data first. + onigasmPromise.promise + .then(async () => { + // Then load the language data + if (!Tokenizer.hasLanguage(arg.payload.data.languageId)) { + await Tokenizer.loadLanguage( + arg.payload.data.languageId, + arg.payload.data.extensions, + arg.payload.data.scopeName, + deserializeLanguageConfiguration(arg.payload.data.languageConfiguration), + arg.payload.data.languageJSON + ); + } + + // Both queue to the reducers and to the extension side that we're ready + queueIncomingAction(arg, InteractiveWindowMessages.MonacoReady); + postActionToExtension(arg, InteractiveWindowMessages.MonacoReady); + }) + .ignoreErrors(); + + return arg.prevState; +} + +function handleKernelUpdate(arg: MonacoReducerArg<IServerState | undefined>): IMonacoState { + const newLanguage = arg.payload.data?.language ?? PYTHON_LANGUAGE; + if (newLanguage !== arg.prevState.language) { + if (!Tokenizer.hasLanguage(newLanguage)) { + postActionToExtension(arg, InteractiveWindowMessages.LoadTmLanguageRequest, newLanguage); + } + + // Recreate the intellisense provider + arg.prevState.intellisenseProvider?.dispose(); // NOSONAR + return { + ...arg.prevState, + language: newLanguage, + intellisenseProvider: new IntellisenseProvider( + arg.prevState.postOffice.sendMessage.bind(arg.prevState.postOffice), + newLanguage + ) + }; + } + + return arg.prevState; +} + +function handleThemeResponse(arg: MonacoReducerArg<IGetMonacoThemeResponse>): IMonacoState { + // Tell monaco we have a new theme. THis is like a state update for monaco + monacoEditor.editor.defineTheme(Identifiers.GeneratedThemeName, arg.payload.data.theme); + return arg.prevState; +} + +function handleCompletionItemsResponse(arg: MonacoReducerArg<IProvideCompletionItemsResponse>): IMonacoState { + const ensuredProvider = handleStarted(arg); + ensuredProvider.intellisenseProvider!.handleCompletionResponse(arg.payload.data); + return ensuredProvider; +} + +function handleResolveCompletionItemResponse(arg: MonacoReducerArg<IResolveCompletionItemResponse>): IMonacoState { + const ensuredProvider = handleStarted(arg); + ensuredProvider.intellisenseProvider!.handleResolveCompletionItemResponse(arg.payload.data); + return ensuredProvider; +} + +function handleSignatureHelpResponse(arg: MonacoReducerArg<IProvideSignatureHelpResponse>): IMonacoState { + const ensuredProvider = handleStarted(arg); + ensuredProvider.intellisenseProvider!.handleSignatureHelpResponse(arg.payload.data); + return ensuredProvider; +} + +function handleHoverResponse(arg: MonacoReducerArg<IProvideHoverResponse>): IMonacoState { + const ensuredProvider = handleStarted(arg); + ensuredProvider.intellisenseProvider!.handleHoverResponse(arg.payload.data); + return ensuredProvider; +} + +function handleCodeCreated(arg: MonacoReducerArg<ICodeCreatedAction>): IMonacoState { + const ensuredProvider = handleStarted(arg); + if (arg.payload.data.cellId) { + ensuredProvider.intellisenseProvider!.mapCellIdToModelId(arg.payload.data.cellId, arg.payload.data.modelId); + } + return ensuredProvider; +} + +function handleEditCell(arg: MonacoReducerArg<IEditCellAction>): IMonacoState { + const ensuredProvider = handleStarted(arg); + if (arg.payload.data.cellId) { + ensuredProvider.intellisenseProvider!.mapCellIdToModelId(arg.payload.data.cellId, arg.payload.data.modelId); + } + return ensuredProvider; +} + +function handleUnmount(arg: MonacoReducerArg): IMonacoState { + if (arg.prevState.intellisenseProvider) { + arg.prevState.intellisenseProvider.dispose(); + } + + return arg.prevState; +} + +// type MonacoReducerFunctions<T> = { +// [P in keyof T]: T[P] extends never | undefined ? MonacoReducerFunc : MonacoReducerFunc<T[P]>; +// }; + +// type IMonacoActionMapping = MonacoReducerFunctions<IInteractiveWindowMapping> & MonacoReducerFunctions<CommonActionTypeMapping>; +// Create a mapping between message and reducer type +class IMonacoActionMapping { + public [InteractiveWindowMessages.Started]: MonacoReducerFunc; + public [InteractiveWindowMessages.LoadOnigasmAssemblyResponse]: MonacoReducerFunc<Buffer>; + public [InteractiveWindowMessages.LoadTmLanguageResponse]: MonacoReducerFunc<ILoadTmLanguageResponse>; + public [CssMessages.GetMonacoThemeResponse]: MonacoReducerFunc<IGetMonacoThemeResponse>; + public [InteractiveWindowMessages.ProvideCompletionItemsResponse]: MonacoReducerFunc< + IProvideCompletionItemsResponse + >; + public [InteractiveWindowMessages.ProvideSignatureHelpResponse]: MonacoReducerFunc<IProvideSignatureHelpResponse>; + public [InteractiveWindowMessages.ProvideHoverResponse]: MonacoReducerFunc<IProvideHoverResponse>; + public [InteractiveWindowMessages.ResolveCompletionItemResponse]: MonacoReducerFunc<IResolveCompletionItemResponse>; + public [InteractiveWindowMessages.UpdateKernel]: MonacoReducerFunc<IServerState | undefined>; + + public [CommonActionType.CODE_CREATED]: MonacoReducerFunc<ICodeCreatedAction>; + public [CommonActionType.EDIT_CELL]: MonacoReducerFunc<IEditCellAction>; + public [CommonActionType.UNMOUNT]: MonacoReducerFunc; + public [CommonActionType.EDITOR_LOADED]: MonacoReducerFunc; +} + +// Create the map between message type and the actual function to call to update state +const reducerMap: IMonacoActionMapping = { + [InteractiveWindowMessages.Started]: handleStarted, + [InteractiveWindowMessages.LoadOnigasmAssemblyResponse]: handleLoadOnigasmResponse, + [InteractiveWindowMessages.LoadTmLanguageResponse]: handleLoadTmLanguageResponse, + [CssMessages.GetMonacoThemeResponse]: handleThemeResponse, + [InteractiveWindowMessages.ProvideCompletionItemsResponse]: handleCompletionItemsResponse, + [InteractiveWindowMessages.ProvideSignatureHelpResponse]: handleSignatureHelpResponse, + [InteractiveWindowMessages.ProvideHoverResponse]: handleHoverResponse, + [InteractiveWindowMessages.ResolveCompletionItemResponse]: handleResolveCompletionItemResponse, + [InteractiveWindowMessages.UpdateKernel]: handleKernelUpdate, + [CommonActionType.CODE_CREATED]: handleCodeCreated, + [CommonActionType.EDIT_CELL]: handleEditCell, + [CommonActionType.UNMOUNT]: handleUnmount, + [CommonActionType.EDITOR_LOADED]: handleLoaded +}; + +export function generateMonacoReducer( + testMode: boolean, + postOffice: PostOffice +): Reducer<IMonacoState, QueuableAction<IMonacoActionMapping>> { + // First create our default state. + const defaultState: IMonacoState = { + testMode, + intellisenseProvider: undefined, + postOffice, + language: PYTHON_LANGUAGE + }; + + // Then combine that with our map of state change message to reducer + return combineReducers<IMonacoState, IMonacoActionMapping>(defaultState, reducerMap); +} diff --git a/src/datascience-ui/interactive-common/redux/reducers/transfer.ts b/src/datascience-ui/interactive-common/redux/reducers/transfer.ts new file mode 100644 index 000000000000..31835412722b --- /dev/null +++ b/src/datascience-ui/interactive-common/redux/reducers/transfer.ts @@ -0,0 +1,351 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Identifiers } from '../../../../client/datascience/constants'; +import { + IEditorContentChange, + InteractiveWindowMessages, + NotebookModelChange +} from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { CssMessages } from '../../../../client/datascience/messages'; +import { ICell } from '../../../../client/datascience/types'; +import { extractInputText, getSelectedAndFocusedInfo, ICellViewModel, IMainState } from '../../mainState'; +import { isSyncingMessage, postActionToExtension } from '../helpers'; +import { Helpers } from './helpers'; +import { + CommonActionType, + CommonReducerArg, + ICellAction, + IChangeGatherStatus, + IEditCellAction, + ILinkClickAction, + ISendCommandAction, + IShowDataViewerAction +} from './types'; + +// These are all reducers that don't actually change state. They merely dispatch a message to the other side. +export namespace Transfer { + export function exportCells(arg: CommonReducerArg): IMainState { + const cellContents = arg.prevState.cellVMs.map((v) => v.cell); + postActionToExtension(arg, InteractiveWindowMessages.Export, cellContents); + + // Indicate busy + return { + ...arg.prevState, + busy: true + }; + } + + export function showExportAsMenu(arg: CommonReducerArg): IMainState { + const cellContents = arg.prevState.cellVMs.map((v) => v.cell); + postActionToExtension(arg, InteractiveWindowMessages.ExportNotebookAs, cellContents); + + return { + ...arg.prevState + }; + } + + export function save(arg: CommonReducerArg): IMainState { + // Note: this is assuming editor contents have already been saved. That should happen as a result of focus change + + // Actually waiting for save results before marking as not dirty, so don't do it here. + postActionToExtension(arg, InteractiveWindowMessages.SaveAll, { + cells: arg.prevState.cellVMs.map((cvm) => cvm.cell) + }); + return arg.prevState; + } + + export function showDataViewer(arg: CommonReducerArg<CommonActionType, IShowDataViewerAction>): IMainState { + postActionToExtension(arg, InteractiveWindowMessages.ShowDataViewer, { + variable: arg.payload.data.variable, + columnSize: arg.payload.data.columnSize + }); + return arg.prevState; + } + + export function sendCommand(arg: CommonReducerArg<CommonActionType, ISendCommandAction>): IMainState { + postActionToExtension(arg, InteractiveWindowMessages.NativeCommand, { + command: arg.payload.data.command + }); + return arg.prevState; + } + + export function showPlot( + arg: CommonReducerArg<CommonActionType | InteractiveWindowMessages, string | undefined> + ): IMainState { + if (arg.payload.data) { + postActionToExtension(arg, InteractiveWindowMessages.ShowPlot, arg.payload.data); + } + return arg.prevState; + } + + export function launchNotebookTrustPrompt(arg: CommonReducerArg) { + postActionToExtension(arg, InteractiveWindowMessages.LaunchNotebookTrustPrompt); + return arg.prevState; + } + + export function linkClick(arg: CommonReducerArg<CommonActionType, ILinkClickAction>): IMainState { + if (arg.payload.data.href.startsWith('data:image/png')) { + postActionToExtension(arg, InteractiveWindowMessages.SavePng, arg.payload.data.href); + } else { + postActionToExtension(arg, InteractiveWindowMessages.OpenLink, arg.payload.data.href); + } + return arg.prevState; + } + + export function getAllCells(arg: CommonReducerArg): IMainState { + const cells = arg.prevState.cellVMs.map((c) => c.cell); + postActionToExtension(arg, InteractiveWindowMessages.ReturnAllCells, cells); + return arg.prevState; + } + + export function hasCell(arg: CommonReducerArg<CommonActionType, string>): IMainState { + const foundCell = arg.prevState.cellVMs.find((c) => c.cell.id === arg.payload.data); + postActionToExtension(arg, InteractiveWindowMessages.HasCellResponse, { + id: arg.payload.data, + result: foundCell !== undefined + }); + return arg.prevState; + } + + export function gotoCell(arg: CommonReducerArg<CommonActionType, ICellAction>): IMainState { + const cellVM = arg.prevState.cellVMs.find((c) => c.cell.id === arg.payload.data.cellId); + if (cellVM && cellVM.cell.data.cell_type === 'code') { + postActionToExtension(arg, InteractiveWindowMessages.GotoCodeCell, { + file: cellVM.cell.file, + line: cellVM.cell.line + }); + } + return arg.prevState; + } + + export function copyCellCode(arg: CommonReducerArg<CommonActionType, ICellAction>): IMainState { + let cellVM = arg.prevState.cellVMs.find((c) => c.cell.id === arg.payload.data.cellId); + if (!cellVM && arg.prevState.editCellVM && arg.payload.data.cellId === arg.prevState.editCellVM.cell.id) { + cellVM = arg.prevState.editCellVM; + } + + // Send a message to the other side to jump to a particular cell + if (cellVM) { + postActionToExtension(arg, InteractiveWindowMessages.CopyCodeCell, { + source: extractInputText(cellVM, arg.prevState.settings) + }); + } + + return arg.prevState; + } + + export function gather(arg: CommonReducerArg<CommonActionType, ICellAction>): IMainState { + const cellVM = arg.prevState.cellVMs.find((c) => c.cell.id === arg.payload.data.cellId); + if (cellVM) { + postActionToExtension(arg, InteractiveWindowMessages.GatherCode, cellVM.cell); + } + return arg.prevState; + } + + export function gatherToScript(arg: CommonReducerArg<CommonActionType, ICellAction>): IMainState { + const cellVM = arg.prevState.cellVMs.find((c) => c.cell.id === arg.payload.data.cellId); + if (cellVM) { + postActionToExtension(arg, InteractiveWindowMessages.GatherCodeToScript, cellVM.cell); + } + return arg.prevState; + } + + function postModelUpdate<T>(arg: CommonReducerArg<CommonActionType, T>, update: NotebookModelChange) { + postActionToExtension(arg, InteractiveWindowMessages.UpdateModel, update); + } + + export function postModelEdit<T>( + arg: CommonReducerArg<CommonActionType, T>, + forward: IEditorContentChange[], + reverse: IEditorContentChange[], + id: string + ) { + postModelUpdate(arg, { + source: 'user', + kind: 'edit', + newDirty: true, + oldDirty: arg.prevState.dirty, + forward, + reverse, + id + }); + } + + export function postModelInsert<T>( + arg: CommonReducerArg<CommonActionType, T>, + index: number, + cell: ICell, + codeCellAboveId?: string + ) { + postModelUpdate(arg, { + source: 'user', + kind: 'insert', + newDirty: true, + oldDirty: arg.prevState.dirty, + index, + cell, + codeCellAboveId + }); + } + + export function changeCellType<T>(arg: CommonReducerArg<CommonActionType, T>, cell: ICell) { + postModelUpdate(arg, { + source: 'user', + kind: 'changeCellType', + newDirty: true, + oldDirty: arg.prevState.dirty, + cell + }); + } + + export function postModelRemove<T>(arg: CommonReducerArg<CommonActionType, T>, index: number, cell: ICell) { + postModelUpdate(arg, { + source: 'user', + kind: 'remove', + oldDirty: arg.prevState.dirty, + newDirty: true, + cell, + index + }); + } + + export function postModelClearOutputs<T>(arg: CommonReducerArg<CommonActionType, T>) { + postModelUpdate(arg, { + source: 'user', + kind: 'clear', + oldDirty: arg.prevState.dirty, + newDirty: true, + // tslint:disable-next-line: no-any + oldCells: arg.prevState.cellVMs.map((c) => c.cell as any) as ICell[] + }); + } + + export function postModelCellUpdate<T>( + arg: CommonReducerArg<CommonActionType, T>, + newCells: ICell[], + oldCells: ICell[] + ) { + postModelUpdate(arg, { + source: 'user', + kind: 'modify', + newCells, + oldCells, + oldDirty: arg.prevState.dirty, + newDirty: true + }); + } + + export function postModelRemoveAll<T>(arg: CommonReducerArg<CommonActionType, T>, newCellId: string) { + postModelUpdate(arg, { + source: 'user', + kind: 'remove_all', + oldDirty: arg.prevState.dirty, + newDirty: true, + // tslint:disable-next-line: no-any + oldCells: arg.prevState.cellVMs.map((c) => c.cell as any) as ICell[], + newCellId + }); + } + + export function postModelSwap<T>( + arg: CommonReducerArg<CommonActionType, T>, + firstCellId: string, + secondCellId: string + ) { + postModelUpdate(arg, { + source: 'user', + kind: 'swap', + oldDirty: arg.prevState.dirty, + newDirty: true, + firstCellId, + secondCellId + }); + } + + export function editCell(arg: CommonReducerArg<CommonActionType, IEditCellAction>): IMainState { + const cellVM = + arg.payload.data.cellId === Identifiers.EditCellId + ? arg.prevState.editCellVM + : arg.prevState.cellVMs.find((c) => c.cell.id === arg.payload.data.cellId); + if (cellVM) { + // Tell the underlying model on the extension side + postModelEdit(arg, arg.payload.data.forward, arg.payload.data.reverse, cellVM.cell.id); + + // Update the uncommitted text on the cell view model + // We keep this saved here so we don't re-render and we put this code into the input / code data + // when focus is lost + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + const selectionInfo = getSelectedAndFocusedInfo(arg.prevState); + // If this is the focused cell, then user is editing it, hence it needs to be updated. + const isThisTheFocusedCell = selectionInfo.focusedCellId === arg.payload.data.cellId; + // If this edit is part of a sycning comging from another notebook, then we need to update it again. + const isSyncFromAnotherNotebook = isSyncingMessage(arg.payload.messageType); + if (index >= 0 && (isThisTheFocusedCell || isSyncFromAnotherNotebook)) { + const newVMs = [...arg.prevState.cellVMs]; + const current = arg.prevState.cellVMs[index]; + const newCell = { + ...current, + inputBlockText: arg.payload.data.code, + cell: { + ...current.cell, + data: { + ...current.cell.data, + source: arg.payload.data.code + } + }, + codeVersion: arg.payload.data.version + }; + + // tslint:disable-next-line: no-any + newVMs[index] = Helpers.asCellViewModel(newCell); // This is because IMessageCell doesn't fit in here + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + } + return arg.prevState; + } + + export function started(arg: CommonReducerArg): IMainState { + // Send all of our initial requests + postActionToExtension(arg, InteractiveWindowMessages.Started); + postActionToExtension(arg, CssMessages.GetCssRequest, { isDark: arg.prevState.baseTheme !== 'vscode-light' }); + postActionToExtension(arg, CssMessages.GetMonacoThemeRequest, { + isDark: arg.prevState.baseTheme !== 'vscode-light' + }); + return arg.prevState; + } + + export function loadedAllCells(arg: CommonReducerArg): IMainState { + postActionToExtension(arg, InteractiveWindowMessages.LoadAllCellsComplete, { + cells: arg.prevState.cellVMs.map((c) => c.cell) + }); + if (!arg.prevState.isNotebookTrusted) { + // As soon as an untrusted notebook is loaded, prompt the user to trust it + postActionToExtension(arg, InteractiveWindowMessages.LaunchNotebookTrustPrompt); + } + return arg.prevState; + } + + export function gathering(arg: CommonReducerArg<CommonActionType, IChangeGatherStatus>): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (index >= 0) { + const cellVMs = [...arg.prevState.cellVMs]; + const current = arg.prevState.cellVMs[index]; + const newCell: ICellViewModel = { + ...current, + gathering: arg.payload.data.gathering + }; + cellVMs[index] = newCell; + + return { + ...arg.prevState, + cellVMs + }; + } + + return arg.prevState; + } +} diff --git a/src/datascience-ui/interactive-common/redux/reducers/types.ts b/src/datascience-ui/interactive-common/redux/reducers/types.ts new file mode 100644 index 000000000000..40b9b9c140da --- /dev/null +++ b/src/datascience-ui/interactive-common/redux/reducers/types.ts @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { NativeKeyboardCommandTelemetry, NativeMouseCommandTelemetry } from '../../../../client/datascience/constants'; +import { + IEditorContentChange, + InteractiveWindowMessages, + IShowDataViewer +} from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { BaseReduxActionPayload } from '../../../../client/datascience/interactive-common/types'; +import { IJupyterVariablesRequest } from '../../../../client/datascience/types'; +import { ActionWithPayload, ReducerArg } from '../../../react-common/reduxUtils'; +import { CursorPos, IMainState } from '../../mainState'; + +/** + * How to add a new state change: + * 1) Add a new <name> to CommonActionType (preferably `InteractiveWindowMessages` - to keep messages in the same place). + * 2) Add a new interface (or reuse 1 below) if the action takes any parameters (ex: ICellAction) + * 3) Add a new actionCreator function (this is how you use it from a react control) to the + * appropriate actionCreator list (one for native and one for interactive). + * The creator should 'create' an instance of the action. + * 4) Add an entry into the appropriate mapping.ts. This is how the type of the list of reducers is enforced. + * 5) Add a new handler for the action under the 'reducer's folder. Handle the expected state change + * 6) Add the handler to the main reducer map in reducers\index.ts + */ +export enum CommonActionType { + ADD_AND_FOCUS_NEW_CELL = 'action.add_new_cell_and_focus_cell', + INSERT_ABOVE_AND_FOCUS_NEW_CELL = 'action.insert_above_and_focus_cell', + INSERT_BELOW_AND_FOCUS_NEW_CELL = 'action.insert_below_and_focus_cell', + INSERT_ABOVE_FIRST_AND_FOCUS_NEW_CELL = 'action.insert_above_first_and_focus_cell', + ADD_NEW_CELL = 'action.add_new_cell', + ARROW_DOWN = 'action.arrow_down', + ARROW_UP = 'action.arrow_up', + CHANGE_CELL_TYPE = 'action.change_cell_type', + CLICK_CELL = 'action.click_cell', + CODE_CREATED = 'action.code_created', + CONTINUE = 'action.continue', + COPY_CELL_CODE = 'action.copy_cell_code', + DELETE_CELL = 'action.delete_cell', + EDITOR_LOADED = 'action.editor_loaded', + EDIT_CELL = 'action.edit_cell', + EXECUTE_ABOVE = 'action.execute_above', + EXECUTE_ALL_CELLS = 'action.execute_all_cells', + EXECUTE_CELL = 'action.execute_cell', + EXECUTE_CELL_AND_ADVANCE = 'action.execute_cell_and_advance', + EXECUTE_CELL_AND_BELOW = 'action.execute_cell_and_below', + EXPORT = 'action.export', + EXPORT_NOTEBOOK_AS = 'action.export_As', + FOCUS_CELL = 'action.focus_cell', + FOCUS_INPUT = 'action.focus_input', + GATHER_CELL = 'action.gather_cell', + GATHER_CELL_TO_SCRIPT = 'action.gather_cell_to_script', + GET_VARIABLE_DATA = 'action.get_variable_data', + GOTO_CELL = 'action.goto_cell', + INSERT_ABOVE = 'action.insert_above', + INSERT_ABOVE_FIRST = 'action.insert_above_first', + INSERT_BELOW = 'action.insert_below', + INTERRUPT_KERNEL = 'action.interrupt_kernel_action', + IPYWIDGET_RENDER_FAILURE = 'action.ipywidget_render_failure', + LAUNCH_NOTEBOOK_TRUST_PROMPT = 'action.launch_notebook_trust_prompt', + LOAD_IPYWIDGET_CLASS_SUCCESS = 'action.load_ipywidget_class_success', + LOAD_IPYWIDGET_CLASS_FAILURE = 'action.load_ipywidget_class_failure', + IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED = 'action.ipywidget_widget_version_not_supported', + LOADED_ALL_CELLS = 'action.loaded_all_cells', + LINK_CLICK = 'action.link_click', + MOVE_CELL_DOWN = 'action.move_cell_down', + MOVE_CELL_UP = 'action.move_cell_up', + OPEN_SETTINGS = 'action.open_settings', + PostOutgoingMessage = 'action.postOutgoingMessage', + REFRESH_VARIABLES = 'action.refresh_variables', + RESTART_KERNEL = 'action.restart_kernel_action', + RUN_BY_LINE = 'action.run_by_line', + SAVE = 'action.save', + SCROLL = 'action.scroll', + SELECT_CELL = 'action.select_cell', + SELECT_SERVER = 'action.select_server', + SET_VARIABLE_EXPLORER_HEIGHT = 'action.set_variable_explorer_height', + SEND_COMMAND = 'action.send_command', + SHOW_DATA_VIEWER = 'action.show_data_viewer', + STEP = 'action.step', + SUBMIT_INPUT = 'action.submit_input', + TOGGLE_INPUT_BLOCK = 'action.toggle_input_block', + TOGGLE_LINE_NUMBERS = 'action.toggle_line_numbers', + TOGGLE_OUTPUT = 'action.toggle_output', + TOGGLE_VARIABLE_EXPLORER = 'action.toggle_variable_explorer', + UNFOCUS_CELL = 'action.unfocus_cell', + UNMOUNT = 'action.unmount' +} + +export type CommonActionTypeMapping = { + [CommonActionType.ADD_AND_FOCUS_NEW_CELL]: IAddCellAction; + [CommonActionType.INSERT_ABOVE]: ICellAction & IAddCellAction; + [CommonActionType.INSERT_BELOW]: ICellAction & IAddCellAction; + [CommonActionType.INSERT_ABOVE_FIRST]: IAddCellAction; + [CommonActionType.INSERT_ABOVE_FIRST_AND_FOCUS_NEW_CELL]: IAddCellAction; + [CommonActionType.INSERT_BELOW_AND_FOCUS_NEW_CELL]: ICellAction & IAddCellAction; + [CommonActionType.INSERT_ABOVE_AND_FOCUS_NEW_CELL]: ICellAction & IAddCellAction; + [CommonActionType.FOCUS_CELL]: ICellAndCursorAction; + [CommonActionType.UNFOCUS_CELL]: ICellAction | ICodeAction; + [CommonActionType.ADD_NEW_CELL]: IAddCellAction; + [CommonActionType.EDIT_CELL]: IEditCellAction; + [CommonActionType.EXECUTE_CELL_AND_ADVANCE]: IExecuteAction; + [CommonActionType.EXECUTE_CELL]: IExecuteAction; + [CommonActionType.EXECUTE_ALL_CELLS]: never | undefined; + [CommonActionType.EXECUTE_ABOVE]: ICellAction; + [CommonActionType.EXECUTE_CELL_AND_BELOW]: ICellAction; + [CommonActionType.RESTART_KERNEL]: never | undefined; + [CommonActionType.INTERRUPT_KERNEL]: never | undefined; + [CommonActionType.EXPORT]: never | undefined; + [CommonActionType.EXPORT_NOTEBOOK_AS]: never | undefined; + [CommonActionType.SAVE]: never | undefined; + [CommonActionType.SHOW_DATA_VIEWER]: IShowDataViewerAction; + [CommonActionType.SEND_COMMAND]: ISendCommandAction; + [CommonActionType.SELECT_CELL]: ICellAndCursorAction; + [CommonActionType.MOVE_CELL_UP]: ICellAction; + [CommonActionType.MOVE_CELL_DOWN]: ICellAction; + [CommonActionType.TOGGLE_LINE_NUMBERS]: ICellAction; + [CommonActionType.TOGGLE_OUTPUT]: ICellAction; + [CommonActionType.ARROW_UP]: ICodeAction; + [CommonActionType.ARROW_DOWN]: ICodeAction; + [CommonActionType.CHANGE_CELL_TYPE]: IChangeCellTypeAction; + [CommonActionType.LINK_CLICK]: ILinkClickAction; + [CommonActionType.GOTO_CELL]: ICellAction; + [CommonActionType.TOGGLE_INPUT_BLOCK]: ICellAction; + [CommonActionType.SUBMIT_INPUT]: ICodeAction; + [CommonActionType.SCROLL]: IScrollAction; + [CommonActionType.CLICK_CELL]: ICellAction; + [CommonActionType.COPY_CELL_CODE]: ICellAction; + [CommonActionType.DELETE_CELL]: ICellAction; + [CommonActionType.GATHER_CELL]: ICellAction; + [CommonActionType.GATHER_CELL_TO_SCRIPT]: ICellAction; + [CommonActionType.EDITOR_LOADED]: never | undefined; + [CommonActionType.LOADED_ALL_CELLS]: never | undefined; + [CommonActionType.UNMOUNT]: never | undefined; + [CommonActionType.SELECT_SERVER]: never | undefined; + [CommonActionType.CODE_CREATED]: ICodeCreatedAction; + [CommonActionType.GET_VARIABLE_DATA]: IJupyterVariablesRequest; + [CommonActionType.TOGGLE_VARIABLE_EXPLORER]: never | undefined; + [CommonActionType.SET_VARIABLE_EXPLORER_HEIGHT]: IVariableExplorerHeight; + [CommonActionType.PostOutgoingMessage]: never | undefined; + [CommonActionType.REFRESH_VARIABLES]: never | undefined; + [CommonActionType.OPEN_SETTINGS]: IOpenSettingsAction; + [CommonActionType.FOCUS_INPUT]: never | undefined; + [CommonActionType.LAUNCH_NOTEBOOK_TRUST_PROMPT]: never | undefined; + [CommonActionType.LOAD_IPYWIDGET_CLASS_SUCCESS]: LoadIPyWidgetClassLoadAction; + [CommonActionType.LOAD_IPYWIDGET_CLASS_FAILURE]: ILoadIPyWidgetClassFailureAction; + [CommonActionType.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED]: NotifyIPyWidgeWidgetVersionNotSupportedAction; + [CommonActionType.IPYWIDGET_RENDER_FAILURE]: Error; + [CommonActionType.STEP]: ICellAction; + [CommonActionType.CONTINUE]: ICellAction; + [CommonActionType.RUN_BY_LINE]: ICellAction; +}; + +export interface IShowDataViewerAction extends IShowDataViewer {} + +export interface ILinkClickAction { + href: string; +} + +export interface IScrollAction { + isAtBottom: boolean; +} + +// tslint:disable-next-line: no-any +export type CommonReducerArg<AT = CommonActionType | InteractiveWindowMessages, T = never | undefined> = ReducerArg< + IMainState, + AT, + BaseReduxActionPayload<T> +>; + +export interface ICellAction { + cellId: string | undefined; +} + +export interface IAddCellAction { + /** + * Id of the new cell that is to be added. + * If none provided, then generate a new id. + */ + newCellId: string; +} + +export interface ICodeAction extends ICellAction { + code: string; +} + +export interface IEditCellAction extends ICodeAction { + forward: IEditorContentChange[]; + reverse: IEditorContentChange[]; + id: string; + modelId: string; + version: number; +} + +// I.e. when using the operation `add`, we need the corresponding `IAddCellAction`. +// They are mutually exclusive, if not `add`, then there's no `newCellId`. +export type IExecuteAction = ICellAction & { + moveOp: 'select' | 'none' | 'add'; +}; + +export interface ICodeCreatedAction extends ICellAction { + modelId: string; +} + +export interface ICellAndCursorAction extends ICellAction { + cursorPos: CursorPos; +} + +export interface IRefreshVariablesAction { + newExecutionCount?: number; +} + +export interface IShowDataViewerAction extends IShowDataViewer {} + +export interface ISendCommandAction { + command: NativeKeyboardCommandTelemetry | NativeMouseCommandTelemetry; +} + +export interface IChangeCellTypeAction { + cellId: string; +} + +export interface IOpenSettingsAction { + setting: string | undefined; +} + +export interface ILoadIPyWidgetClassFailureAction { + className: string; + moduleName: string; + moduleVersion: string; + cdnsUsed: boolean; + isOnline: boolean; + // tslint:disable-next-line: no-any + error: any; + timedout: boolean; +} + +export interface IVariableExplorerHeight { + containerHeight: number; + gridHeight: number; +} + +export type LoadIPyWidgetClassDisabledAction = { + className: string; + moduleName: string; + moduleVersion: string; +}; + +export type LoadIPyWidgetClassLoadAction = { + className: string; + moduleName: string; + moduleVersion: string; +}; +export type NotifyIPyWidgeWidgetVersionNotSupportedAction = { + moduleName: 'qgrid'; + moduleVersion: string; +}; + +export interface IChangeGatherStatus { + cellId: string; + gathering: boolean; +} + +export type CommonAction<T = never | undefined> = ActionWithPayload<T, CommonActionType | InteractiveWindowMessages>; diff --git a/src/datascience-ui/interactive-common/redux/reducers/variables.ts b/src/datascience-ui/interactive-common/redux/reducers/variables.ts new file mode 100644 index 000000000000..a2b87187f9a8 --- /dev/null +++ b/src/datascience-ui/interactive-common/redux/reducers/variables.ts @@ -0,0 +1,310 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Reducer } from 'redux'; +import { + IFinishCell, + IInteractiveWindowMapping, + InteractiveWindowMessages +} from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { BaseReduxActionPayload } from '../../../../client/datascience/interactive-common/types'; +import { + IJupyterVariable, + IJupyterVariablesRequest, + IJupyterVariablesResponse +} from '../../../../client/datascience/types'; +import { combineReducers, QueuableAction, ReducerArg, ReducerFunc } from '../../../react-common/reduxUtils'; +import { postActionToExtension } from '../helpers'; +import { CommonActionType, CommonActionTypeMapping, ICellAction, IVariableExplorerHeight } from './types'; + +export type IVariableState = { + currentExecutionCount: number; + visible: boolean; + sortColumn: string; + sortAscending: boolean; + variables: IJupyterVariable[]; + pageSize: number; + containerHeight: number; + gridHeight: number; + refreshCount: number; + showVariablesOnDebug: boolean; +}; + +type VariableReducerFunc<T = never | undefined> = ReducerFunc< + IVariableState, + InteractiveWindowMessages, + BaseReduxActionPayload<T> +>; +type VariableReducerArg<T = never | undefined> = ReducerArg< + IVariableState, + InteractiveWindowMessages, + BaseReduxActionPayload<T> +>; + +function handleRequest(arg: VariableReducerArg<IJupyterVariablesRequest>): IVariableState { + const newExecutionCount = + arg.payload.data.executionCount !== undefined + ? arg.payload.data.executionCount + : arg.prevState.currentExecutionCount; + postActionToExtension(arg, InteractiveWindowMessages.GetVariablesRequest, { + executionCount: newExecutionCount, + sortColumn: arg.payload.data.sortColumn, + startIndex: arg.payload.data.startIndex, + sortAscending: arg.payload.data.sortAscending, + pageSize: arg.payload.data.pageSize, + refreshCount: arg.payload.data.refreshCount + }); + return { + ...arg.prevState, + pageSize: Math.max(arg.prevState.pageSize, arg.payload.data.pageSize) + }; +} + +function toggleVariableExplorer(arg: VariableReducerArg): IVariableState { + const newState: IVariableState = { + ...arg.prevState, + visible: !arg.prevState.visible, + showVariablesOnDebug: false // If user does any toggling don't auto open this. + }; + + postActionToExtension(arg, InteractiveWindowMessages.VariableExplorerToggle, newState.visible); + + // If going visible for the first time, refresh our variables + if (newState.visible) { + return handleRequest({ + ...arg, + prevState: newState, + payload: { + ...arg.payload, + data: { + executionCount: arg.prevState.currentExecutionCount, + sortColumn: 'name', + sortAscending: true, + startIndex: 0, + pageSize: arg.prevState.pageSize, + refreshCount: arg.prevState.refreshCount + } + } + }); + } else { + return newState; + } +} + +function handleVariableExplorerHeightResponse(arg: VariableReducerArg<IVariableExplorerHeight>): IVariableState { + if (arg.payload.data) { + const containerHeight = arg.payload.data.containerHeight; + const gridHeight = arg.payload.data.gridHeight; + if (containerHeight && gridHeight) { + return { + ...arg.prevState, + containerHeight: containerHeight, + gridHeight: gridHeight + }; + } + } + return { + ...arg.prevState + }; +} + +function setVariableExplorerHeight(arg: VariableReducerArg<IVariableExplorerHeight>): IVariableState { + const containerHeight = arg.payload.data.containerHeight; + const gridHeight = arg.payload.data.gridHeight; + + if (containerHeight && gridHeight) { + postActionToExtension(arg, InteractiveWindowMessages.SetVariableExplorerHeight, { + containerHeight, + gridHeight + }); + return { + ...arg.prevState, + containerHeight: containerHeight, + gridHeight: gridHeight + }; + } + return { + ...arg.prevState + }; +} + +function handleResponse(arg: VariableReducerArg<IJupyterVariablesResponse>): IVariableState { + const response = arg.payload.data; + + // Check to see if we have moved to a new execution count + if ( + response.executionCount > arg.prevState.currentExecutionCount || + response.refreshCount > arg.prevState.refreshCount || + (response.executionCount === arg.prevState.currentExecutionCount && arg.prevState.variables.length === 0) || + (response.refreshCount === arg.prevState.refreshCount && arg.prevState.variables.length === 0) + ) { + // Should be an entirely new request. Make an empty list + const variables = Array<IJupyterVariable>(response.totalCount); + + // Replace the page with the values returned + for (let i = 0; i < response.pageResponse.length; i += 1) { + variables[i + response.pageStartIndex] = response.pageResponse[i]; + } + + // Also update the execution count. + return { + ...arg.prevState, + currentExecutionCount: response.executionCount, + refreshCount: response.refreshCount, + variables + }; + } else if ( + response.executionCount === arg.prevState.currentExecutionCount && + response.refreshCount === arg.prevState.refreshCount + ) { + // This is a response for a page in an already existing list. + const variables = [...arg.prevState.variables]; + + // See if we need to remove any from this page + const removeCount = Math.max(0, arg.prevState.variables.length - response.totalCount); + if (removeCount) { + variables.splice(response.pageStartIndex, removeCount); + } + + // Replace the page with the values returned + for (let i = 0; i < response.pageResponse.length; i += 1) { + variables[i + response.pageStartIndex] = response.pageResponse[i]; + } + + return { + ...arg.prevState, + variables + }; + } + + // Otherwise this response is for an old execution. + return arg.prevState; +} + +function handleRestarted(arg: VariableReducerArg): IVariableState { + const result = handleRequest({ + ...arg, + payload: { + ...arg.payload, + data: { + executionCount: 0, + sortColumn: 'name', + sortAscending: true, + startIndex: 0, + pageSize: arg.prevState.pageSize, + refreshCount: 0 + } + } + }); + return { + ...result, + currentExecutionCount: -1, + variables: [] + }; +} + +function handleFinishCell(arg: VariableReducerArg<IFinishCell>): IVariableState { + const executionCount = arg.payload.data.cell.data.execution_count + ? parseInt(arg.payload.data.cell.data.execution_count.toString(), 10) + : undefined; + + // If the variables are visible, refresh them + if (arg.prevState.visible && executionCount) { + return handleRequest({ + ...arg, + payload: { + ...arg.payload, + data: { + executionCount, + sortColumn: 'name', + sortAscending: true, + startIndex: 0, + pageSize: arg.prevState.pageSize, + refreshCount: arg.prevState.refreshCount + } + } + }); + } + return { + ...arg.prevState, + currentExecutionCount: executionCount ? executionCount : arg.prevState.currentExecutionCount + }; +} + +function handleRefresh(arg: VariableReducerArg): IVariableState { + // If the variables are visible, refresh them + if (arg.prevState.visible) { + return handleRequest({ + ...arg, + payload: { + ...arg.payload, + data: { + executionCount: arg.prevState.currentExecutionCount, + sortColumn: 'name', + sortAscending: true, + startIndex: 0, + pageSize: arg.prevState.pageSize, + refreshCount: arg.prevState.refreshCount + 1 + } + }, + prevState: arg.prevState + }); + } + return { + ...arg.prevState, + variables: [] + }; +} + +function handleDebugStart(arg: VariableReducerArg<ICellAction>): IVariableState { + // If we haven't already turned on variables, do so now + if (arg.prevState.showVariablesOnDebug) { + return { + ...arg.prevState, + showVariablesOnDebug: false, + visible: true + }; + } + return arg.prevState; +} + +type VariableReducerFunctions<T> = { + [P in keyof T]: T[P] extends never | undefined ? VariableReducerFunc : VariableReducerFunc<T[P]>; +}; + +type VariableActionMapping = VariableReducerFunctions<IInteractiveWindowMapping> & + VariableReducerFunctions<CommonActionTypeMapping>; + +// Create the map between message type and the actual function to call to update state +const reducerMap: Partial<VariableActionMapping> = { + [InteractiveWindowMessages.RestartKernel]: handleRestarted, + [InteractiveWindowMessages.FinishCell]: handleFinishCell, + [InteractiveWindowMessages.ForceVariableRefresh]: handleRefresh, + [CommonActionType.TOGGLE_VARIABLE_EXPLORER]: toggleVariableExplorer, + [CommonActionType.SET_VARIABLE_EXPLORER_HEIGHT]: setVariableExplorerHeight, + [InteractiveWindowMessages.VariableExplorerHeightResponse]: handleVariableExplorerHeightResponse, + [CommonActionType.GET_VARIABLE_DATA]: handleRequest, + [InteractiveWindowMessages.GetVariablesResponse]: handleResponse, + [CommonActionType.RUN_BY_LINE]: handleDebugStart +}; + +export function generateVariableReducer( + showVariablesOnDebug: boolean +): Reducer<IVariableState, QueuableAction<Partial<VariableActionMapping>>> { + // First create our default state. + const defaultState: IVariableState = { + currentExecutionCount: 0, + variables: [], + visible: false, + sortAscending: true, + sortColumn: 'name', + pageSize: 5, + containerHeight: 0, + gridHeight: 200, + refreshCount: 0, + showVariablesOnDebug + }; + + // Then combine that with our map of state change message to reducer + return combineReducers<IVariableState, Partial<VariableActionMapping>>(defaultState, reducerMap); +} diff --git a/src/datascience-ui/interactive-common/redux/store.ts b/src/datascience-ui/interactive-common/redux/store.ts new file mode 100644 index 000000000000..345be932aa1f --- /dev/null +++ b/src/datascience-ui/interactive-common/redux/store.ts @@ -0,0 +1,458 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as fastDeepEqual from 'fast-deep-equal'; +import * as path from 'path'; +import * as Redux from 'redux'; +import { createLogger } from 'redux-logger'; + +import { PYTHON_LANGUAGE } from '../../../client/common/constants'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { Identifiers } from '../../../client/datascience/constants'; +import { InteractiveWindowMessages } from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { MessageType } from '../../../client/datascience/interactive-common/synchronization'; +import { BaseReduxActionPayload } from '../../../client/datascience/interactive-common/types'; +import { CssMessages } from '../../../client/datascience/messages'; +import { CellState } from '../../../client/datascience/types'; +import { + activeDebugState, + DebugState, + getSelectedAndFocusedInfo, + ICellViewModel, + IMainState, + ServerStatus +} from '../../interactive-common/mainState'; +import { getLocString } from '../../react-common/locReactSide'; +import { PostOffice } from '../../react-common/postOffice'; +import { combineReducers, createQueueableActionMiddleware, QueuableAction } from '../../react-common/reduxUtils'; +import { computeEditorOptions, getDefaultSettings } from '../../react-common/settingsReactSide'; +import { createEditableCellVM, generateTestState } from '../mainState'; +import { forceLoad } from '../transforms'; +import { isAllowedAction, isAllowedMessage, postActionToExtension } from './helpers'; +import { generatePostOfficeSendReducer } from './postOffice'; +import { generateMonacoReducer, IMonacoState } from './reducers/monaco'; +import { CommonActionType } from './reducers/types'; +import { generateVariableReducer, IVariableState } from './reducers/variables'; + +function generateDefaultState( + skipDefault: boolean, + testMode: boolean, + baseTheme: string, + editable: boolean +): IMainState { + if (!skipDefault) { + return generateTestState('', editable); + } else { + return { + // tslint:disable-next-line: no-typeof-undefined + skipDefault, + testMode, + baseTheme: baseTheme, + cellVMs: [], + busy: true, + undoStack: [], + redoStack: [], + submittedText: false, + currentExecutionCount: 0, + debugging: false, + knownDark: false, + dirty: false, + editCellVM: editable ? undefined : createEditableCellVM(0), + isAtBottom: true, + font: { + size: 14, + family: "Consolas, 'Courier New', monospace" + }, + codeTheme: Identifiers.GeneratedThemeName, + focusPending: 0, + monacoReady: testMode, // When testing, monaco starts out ready + loaded: false, + kernel: { + displayName: getLocString('DataScience.noKernel', 'No Kernel'), + localizedUri: getLocString('DataScience.serverNotStarted', 'Not Started'), + jupyterServerStatus: ServerStatus.NotStarted, + language: PYTHON_LANGUAGE + }, + settings: testMode ? getDefaultSettings() : undefined, // When testing, we don't send (or wait) for the real settings. + editorOptions: testMode ? computeEditorOptions(getDefaultSettings()) : undefined, + isNotebookTrusted: true + }; + } +} + +function generateMainReducer<M>( + skipDefault: boolean, + testMode: boolean, + baseTheme: string, + editable: boolean, + reducerMap: M +): Redux.Reducer<IMainState, QueuableAction<M>> { + // First create our default state. + const defaultState = generateDefaultState(skipDefault, testMode, baseTheme, editable); + + // Then combine that with our map of state change message to reducer + return combineReducers<IMainState, M>(defaultState, reducerMap); +} + +function createSendInfoMiddleware(): Redux.Middleware<{}, IStore> { + return (store) => (next) => (action) => { + const prevState = store.getState(); + const res = next(action); + const afterState = store.getState(); + + // If the action is part of a sync message, then do not send it to the extension. + const messageType = (action?.payload as BaseReduxActionPayload).messageType ?? MessageType.other; + const isSyncMessage = + (messageType & MessageType.syncAcrossSameNotebooks) === MessageType.syncAcrossSameNotebooks && + (messageType & MessageType.syncAcrossSameNotebooks) === MessageType.syncWithLiveShare; + if (isSyncMessage) { + return res; + } + + // If cell vm count changed or selected cell changed, send the message + if (!action.type || action.type !== CommonActionType.UNMOUNT) { + const currentSelection = getSelectedAndFocusedInfo(afterState.main); + if ( + prevState.main.cellVMs.length !== afterState.main.cellVMs.length || + getSelectedAndFocusedInfo(prevState.main).selectedCellId !== currentSelection.selectedCellId || + prevState.main.undoStack.length !== afterState.main.undoStack.length || + prevState.main.redoStack.length !== afterState.main.redoStack.length + ) { + postActionToExtension({ queueAction: store.dispatch }, InteractiveWindowMessages.SendInfo, { + cellCount: afterState.main.cellVMs.length, + undoCount: afterState.main.undoStack.length, + redoCount: afterState.main.redoStack.length, + selectedCell: currentSelection.selectedCellId + }); + } + } + return res; + }; +} + +function createTestLogger() { + const logFileEnv = process.env.VSC_PYTHON_WEBVIEW_LOG_FILE; + if (logFileEnv) { + // tslint:disable-next-line: no-require-imports + const log4js = require('log4js') as typeof import('log4js'); + const logFilePath = path.isAbsolute(logFileEnv) ? logFileEnv : path.join(EXTENSION_ROOT_DIR, logFileEnv); + log4js.configure({ + appenders: { reduxLogger: { type: 'file', filename: logFilePath } }, + categories: { default: { appenders: ['reduxLogger'], level: 'debug' } } + }); + return log4js.getLogger(); + } +} + +function createTestMiddleware(): Redux.Middleware<{}, IStore> { + // Make sure all dynamic imports are loaded. + const transformPromise = forceLoad(); + + // tslint:disable-next-line: cyclomatic-complexity + return (store) => (next) => (action) => { + const prevState = store.getState(); + const res = next(action); + const afterState = store.getState(); + // tslint:disable-next-line: no-any + const sendMessage = (message: any, payload?: any) => { + setTimeout(() => { + transformPromise + .then(() => postActionToExtension({ queueAction: store.dispatch }, message, payload)) + .ignoreErrors(); + }); + }; + + if (!action.type || action.type !== CommonActionType.UNMOUNT) { + // Special case for focusing a cell + const previousSelection = getSelectedAndFocusedInfo(prevState.main); + const currentSelection = getSelectedAndFocusedInfo(afterState.main); + if (previousSelection.focusedCellId !== currentSelection.focusedCellId && currentSelection.focusedCellId) { + // Send async so happens after render state changes (so our enzyme wrapper is up to date) + sendMessage(InteractiveWindowMessages.FocusedCellEditor, { cellId: action.payload.cellId }); + } + if ( + previousSelection.selectedCellId !== currentSelection.selectedCellId && + currentSelection.selectedCellId + ) { + // Send async so happens after render state changes (so our enzyme wrapper is up to date) + sendMessage(InteractiveWindowMessages.SelectedCell, { cellId: action.payload.cellId }); + } + // Special case for unfocusing a cell + if (previousSelection.focusedCellId !== currentSelection.focusedCellId && !currentSelection.focusedCellId) { + // Send async so happens after render state changes (so our enzyme wrapper is up to date) + sendMessage(InteractiveWindowMessages.UnfocusedCellEditor); + } + } + + // Indicate settings updates + if (!fastDeepEqual(prevState.main.settings, afterState.main.settings)) { + // Send async so happens after render state changes (so our enzyme wrapper is up to date) + sendMessage(InteractiveWindowMessages.SettingsUpdated); + } + + // Indicate clean changes + if (prevState.main.dirty && !afterState.main.dirty) { + sendMessage(InteractiveWindowMessages.NotebookClean); + } + + // Indicate dirty changes + if (!prevState.main.dirty && afterState.main.dirty) { + sendMessage(InteractiveWindowMessages.NotebookDirty); + } + + // Indicate variables complete + if ( + (!fastDeepEqual(prevState.variables.variables, afterState.variables.variables) || + prevState.variables.currentExecutionCount !== afterState.variables.currentExecutionCount || + prevState.variables.refreshCount !== afterState.variables.refreshCount) && + action.type === InteractiveWindowMessages.GetVariablesResponse + ) { + sendMessage(InteractiveWindowMessages.VariablesComplete); + } + + // Indicate update from extension side + if (action.type && action.type === InteractiveWindowMessages.UpdateModel) { + sendMessage(InteractiveWindowMessages.ReceivedUpdateModel); + } + + // Special case for rendering complete + if ( + action.type && + action.type === InteractiveWindowMessages.FinishCell && + action.payload.data && + action.payload.data.cell.data?.cell_type === 'code' + ) { + // Send async so happens after the render is actually finished. + sendMessage(InteractiveWindowMessages.ExecutionRendered); + } + + if ( + !action.type || + (action.type !== InteractiveWindowMessages.FinishCell && action.type !== CommonActionType.UNMOUNT) + ) { + // Might be a non finish but still update cells (like an undo or a delete) + const prevFinished = prevState.main.cellVMs + .filter((c) => c.cell.state === CellState.finished || c.cell.state === CellState.error) + .map((c) => c.cell.id); + const afterFinished = afterState.main.cellVMs + .filter((c) => c.cell.state === CellState.finished || c.cell.state === CellState.error) + .map((c) => c.cell.id); + if ( + afterFinished.length > prevFinished.length || + (afterFinished.length !== prevFinished.length && + afterState.main.cellVMs.length !== prevState.main.cellVMs.length) + ) { + // Send async so happens after the render is actually finished. + sendMessage(InteractiveWindowMessages.ExecutionRendered); + } + } + + // Hiding/displaying output + const prevHidingOutput = prevState.main.cellVMs.filter((c) => c.hideOutput).map((c) => c.cell.id); + const afterHidingOutput = afterState.main.cellVMs.filter((c) => c.hideOutput).map((c) => c.cell.id); + if (!fastDeepEqual(prevHidingOutput, afterHidingOutput)) { + // Send async so happens after the render is actually finished. + sendMessage(InteractiveWindowMessages.OutputToggled); + } + + // Entering break state in a native cell + const prevBreak = prevState.main.cellVMs.find((cvm) => cvm.currentStack); + const newBreak = afterState.main.cellVMs.find((cvm) => cvm.currentStack); + if (prevBreak !== newBreak || !fastDeepEqual(prevBreak?.currentStack, newBreak?.currentStack)) { + sendMessage(InteractiveWindowMessages.ShowingIp); + } + + // Kernel state changing + const afterKernel = afterState.main.kernel; + const prevKernel = prevState.main.kernel; + if ( + afterKernel.jupyterServerStatus !== prevKernel.jupyterServerStatus && + afterKernel.jupyterServerStatus === ServerStatus.Idle + ) { + sendMessage(InteractiveWindowMessages.KernelIdle); + } + + // Debug state changing + const oldState = getDebugState(prevState.main.cellVMs); + const newState = getDebugState(afterState.main.cellVMs); + if (oldState !== newState) { + sendMessage(InteractiveWindowMessages.DebugStateChange, { oldState, newState }); + } + + if (action.type !== 'action.postOutgoingMessage') { + sendMessage(`DISPATCHED_ACTION_${action.type}`, {}); + } + return res; + }; +} + +// Find the debug state for cell view models +function getDebugState(vms: ICellViewModel[]): DebugState { + const firstNonDesign = vms.find((cvm) => activeDebugState(cvm.runningByLine)); + + return firstNonDesign ? firstNonDesign.runningByLine : DebugState.Design; +} + +function createMiddleWare(testMode: boolean): Redux.Middleware<{}, IStore>[] { + // Create the middleware that modifies actions to queue new actions + const queueableActions = createQueueableActionMiddleware(); + + // Create the update context middle ware. It handles the 'sendInfo' message that + // requires sending on every cell vm length change + const updateContext = createSendInfoMiddleware(); + + // Create the test middle ware. It sends messages that are used for testing only + // Or if testing in UI Test. + // tslint:disable-next-line: no-any + const acquireVsCodeApi = (window as any).acquireVsCodeApi as Function; + const isUITest = acquireVsCodeApi && acquireVsCodeApi().handleMessage ? true : false; + const testMiddleware = testMode || isUITest ? createTestMiddleware() : undefined; + + // Create the logger if we're not in production mode or we're forcing logging + const reduceLogMessage = '<payload too large to displayed in logs (at least on CI)>'; + const actionsWithLargePayload = [ + InteractiveWindowMessages.LoadOnigasmAssemblyResponse, + CssMessages.GetCssResponse, + InteractiveWindowMessages.LoadTmLanguageResponse + ]; + const logger = createLogger({ + // tslint:disable-next-line: no-any + stateTransformer: (state: any) => { + if (!state || typeof state !== 'object') { + return state; + } + // tslint:disable-next-line: no-any + const rootState = { ...state } as any; + if ('main' in rootState && typeof rootState.main === 'object') { + // tslint:disable-next-line: no-any + const main = (rootState.main = ({ ...rootState.main } as any) as Partial<IMainState>); + main.rootCss = reduceLogMessage; + main.rootStyle = reduceLogMessage; + // tslint:disable-next-line: no-any + main.editorOptions = reduceLogMessage as any; + // tslint:disable-next-line: no-any + main.settings = reduceLogMessage as any; + } + rootState.monaco = reduceLogMessage; + + return rootState; + }, + // tslint:disable-next-line: no-any + actionTransformer: (action: any) => { + if (!action) { + return action; + } + if (actionsWithLargePayload.indexOf(action.type) >= 0) { + return { ...action, payload: reduceLogMessage }; + } + return action; + }, + logger: testMode ? createTestLogger() : window.console + }); + const loggerMiddleware = + process.env.VSC_PYTHON_FORCE_LOGGING !== undefined && !process.env.VSC_PYTHON_DS_NO_REDUX_LOGGING + ? logger + : undefined; + + const results: Redux.Middleware<{}, IStore>[] = []; + results.push(queueableActions); + results.push(updateContext); + if (testMiddleware) { + results.push(testMiddleware); + } + if (loggerMiddleware) { + results.push(loggerMiddleware); + } + + return results; +} + +export interface IStore { + main: IMainState; + variables: IVariableState; + monaco: IMonacoState; + post: {}; +} + +export interface IMainWithVariables extends IMainState { + variableState: IVariableState; +} + +/** + * Middleware that will ensure all actions have `messageDirection` property. + */ +const addMessageDirectionMiddleware: Redux.Middleware = (_store) => (next) => (action: Redux.AnyAction) => { + if (isAllowedAction(action)) { + // Ensure all dispatched messages have been flagged as `incoming`. + const payload: BaseReduxActionPayload<{}> = action.payload || {}; + if (!payload.messageDirection) { + action.payload = { ...payload, messageDirection: 'incoming' }; + } + } + + return next(action); +}; + +export function createStore<M>( + skipDefault: boolean, + baseTheme: string, + testMode: boolean, + editable: boolean, + showVariablesOnDebug: boolean, + reducerMap: M, + postOffice: PostOffice +) { + // Create reducer for the main react UI + const mainReducer = generateMainReducer(skipDefault, testMode, baseTheme, editable, reducerMap); + + // Create reducer to pass window messages to the other side + const postOfficeReducer = generatePostOfficeSendReducer(postOffice); + + // Create another reducer for handling monaco state + const monacoReducer = generateMonacoReducer(testMode, postOffice); + + // Create another reducer for handling variable state + const variableReducer = generateVariableReducer(showVariablesOnDebug); + + // Combine these together + const rootReducer = Redux.combineReducers<IStore>({ + main: mainReducer, + variables: variableReducer, + monaco: monacoReducer, + post: postOfficeReducer + }); + + // Create our middleware + const middleware = createMiddleWare(testMode).concat([addMessageDirectionMiddleware]); + + // Use this reducer and middle ware to create a store + const store = Redux.createStore(rootReducer, Redux.applyMiddleware(...middleware)); + + // Make all messages from the post office dispatch to the store, changing the type to + // turn them into actions. + postOffice.addHandler({ + // tslint:disable-next-line: no-any + handleMessage(message: string, payload?: any): boolean { + // Double check this is one of our messages. React will actually post messages here too during development + if (isAllowedMessage(message)) { + const basePayload: BaseReduxActionPayload = { data: payload }; + if (message === InteractiveWindowMessages.Sync) { + // This is a message that has been sent from extension purely for synchronization purposes. + // Unwrap the message. + message = payload.type; + // This is a message that came in as a result of an outgoing message from another view. + basePayload.messageDirection = 'outgoing'; + basePayload.messageType = payload.payload.messageType ?? MessageType.syncAcrossSameNotebooks; + basePayload.data = payload.payload.data; + } else { + // Messages result of some user action. + basePayload.messageType = basePayload.messageType ?? MessageType.other; + } + store.dispatch({ type: message, payload: basePayload }); + } + return true; + } + }); + + return store; +} diff --git a/src/datascience-ui/interactive-common/tokenizer.ts b/src/datascience-ui/interactive-common/tokenizer.ts new file mode 100644 index 000000000000..a1e353ed715f --- /dev/null +++ b/src/datascience-ui/interactive-common/tokenizer.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { wireTmGrammars } from 'monaco-editor-textmate'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { Registry } from 'monaco-textmate'; +import { loadWASM } from 'onigasm'; + +// Map of grammars to tmlanguage contents +const grammarMap = new Map<string, string>(); + +// Map of language ids to scope names +const languageMap = new Map<string, string>(); + +async function getGrammarDefinition(scopeName: string) { + const mappedGrammar = grammarMap.get(scopeName); + if (mappedGrammar) { + return { + format: 'json', + content: mappedGrammar + }; + } + return { + format: 'json', + content: '{}' + // tslint:disable-next-line: no-any + } as any; +} + +// Loading the onigasm bundles is process wide, so don't bother doing it more than once. Creates memory leaks +// when running tests. +let onigasmData: ArrayBuffer | undefined; +let loadedOnigasm = false; + +// Global registry for grammars +const registry = new Registry({ getGrammarDefinition: getGrammarDefinition }); + +export namespace Tokenizer { + // Export for loading language data. + export async function loadLanguage( + languageId: string, + extensions: string[], + scopeName: string, + config: monacoEditor.languages.LanguageConfiguration, + languageJSON: string + ) { + // See if this language was already registered or not. + if (!grammarMap.has(scopeName)) { + grammarMap.set(scopeName, languageJSON); + monacoEditor.languages.register({ id: languageId, extensions }); + monacoEditor.languages.setLanguageConfiguration(languageId, config); + + // Load the web assembly if necessary + if (onigasmData && onigasmData.byteLength !== 0 && !loadedOnigasm) { + loadedOnigasm = true; + await loadWASM(onigasmData); + } + + // add scope map + languageMap.set(languageId, scopeName); + + // Wire everything together. + await wireTmGrammars(monacoEditor, registry, languageMap); + } + } + + // Export for saving onigasm data + export function loadOnigasm(onigasm: ArrayBuffer) { + if (!onigasmData) { + onigasmData = onigasm; + } + } + + export function hasOnigasm(): boolean { + return loadedOnigasm; + } + + export function hasLanguage(languageId: string): boolean { + return languageMap.has(languageId); + } +} diff --git a/src/datascience-ui/interactive-common/transforms.tsx b/src/datascience-ui/interactive-common/transforms.tsx new file mode 100644 index 000000000000..d9ac734a6e72 --- /dev/null +++ b/src/datascience-ui/interactive-common/transforms.tsx @@ -0,0 +1,170 @@ +/* tslint:disable */ +import * as React from 'react'; +import Loadable, { LoadableComponent } from '@loadable/component'; +import { getLocString } from '../react-common/locReactSide'; + +class TransformData { + private cachedPromise: undefined | Promise<any>; + constructor(public mimeType: string, private importer: () => Promise<any>) {} + public getComponent(): Promise<any> { + if (!this.cachedPromise) { + this.cachedPromise = this.importer(); + } + return this.cachedPromise; + } +} + +// Hardcode mimeType here so we can do a quick lookup without loading all of the +// other components. +const mimeTypeToImport: TransformData[] = [ + new TransformData('application/vnd.vega.v2+json', async () => { + const module = await import(/* webpackChunkName: "vega" */ '@nteract/transform-vega'); + return module.Vega2; + }), + new TransformData('application/vnd.vega.v3+json', async () => { + const module = await import(/* webpackChunkName: "vega" */ '@nteract/transform-vega'); + return module.Vega3; + }), + new TransformData('application/vnd.vega.v4+json', async () => { + const module = await import(/* webpackChunkName: "vega" */ '@nteract/transform-vega'); + return module.Vega4; + }), + new TransformData('application/vnd.vega.v5+json', async () => { + const module = await import(/* webpackChunkName: "vega" */ '@nteract/transform-vega'); + return module.Vega5; + }), + new TransformData('application/vnd.vegalite.v1+json', async () => { + const module = await import(/* webpackChunkName: "vega" */ '@nteract/transform-vega'); + return module.VegaLite1; + }), + new TransformData('application/vnd.vegalite.v2+json', async () => { + const module = await import(/* webpackChunkName: "vega" */ '@nteract/transform-vega'); + return module.VegaLite2; + }), + new TransformData('application/vnd.vegalite.v3+json', async () => { + const module = await import(/* webpackChunkName: "vega" */ '@nteract/transform-vega'); + return module.VegaLite3; + }), + new TransformData('application/vnd.vegalite.v4+json', async () => { + const module = await import(/* webpackChunkName: "vega" */ '@nteract/transform-vega'); + return module.VegaLite3; + }), + new TransformData('application/geo+json', async () => { + const module = await import(/* webpackChunkName: "geojson" */ '@nteract/transform-geojson'); + return module.GeoJSONTransform; + }), + new TransformData('application/vnd.dataresource+json', async () => { + const module = await import(/* webpackChunkName: "dataresource" */ '@nteract/transform-dataresource'); + return module.DataResourceTransform; + }), + new TransformData('application/x-nteract-model-debug+json', async () => { + const module = await import(/* webpackChunkName: "modeldebug" */ '@nteract/transform-model-debug'); + return module.default; + }), + new TransformData('text/vnd.plotly.v1+html', async () => { + const module = await import(/* webpackChunkName: "plotly" */ '@nteract/transform-plotly'); + return module.PlotlyNullTransform; + }), + new TransformData('application/vnd.plotly.v1+json', async () => { + const module = await import(/* webpackChunkName: "plotly" */ '@nteract/transform-plotly'); + return module.PlotlyTransform; + }), + new TransformData('image/svg+xml', async () => { + const module = await import(/* webpackChunkName: "nteract_transforms" */ '@nteract/transforms'); + return module.SVGTransform; + }), + new TransformData('image/png', async () => { + const module = await import(/* webpackChunkName: "nteract_transforms" */ '@nteract/transforms'); + return module.PNGTransform; + }), + new TransformData('image/gif', async () => { + const module = await import(/* webpackChunkName: "nteract_transforms" */ '@nteract/transforms'); + return module.GIFTransform; + }), + new TransformData('image/jpeg', async () => { + const module = await import(/* webpackChunkName: "nteract_transforms" */ '@nteract/transforms'); + return module.JPEGTransform; + }), + new TransformData('application/json', async () => { + const module = await import(/* webpackChunkName: "nteract_transforms" */ '@nteract/transforms'); + return module.JSONTransform; + }), + new TransformData('application/javascript', async () => { + const module = await import(/* webpackChunkName: "nteract_transforms" */ '@nteract/transforms'); + return module.JavaScriptTransform; + }), + new TransformData('application/vdom.v1+json', async () => { + const module = await import(/* webpackChunkName: "nteract_transforms_vsdom" */ '@nteract/transform-vdom'); + return module.VDOM; + }), + new TransformData('text/markdown', async () => { + const module = await import(/* webpackChunkName: "nteract_transforms" */ '@nteract/transforms'); + return module.MarkdownTransform; + }), + new TransformData('text/latex', async () => { + const module = await import(/* webpackChunkName: "nteract_transforms" */ '@nteract/transforms'); + return module.LaTeXTransform; + }), + new TransformData('text/html', async () => { + const module = await import(/* webpackChunkName: "nteract_transforms" */ '@nteract/transforms'); + return module.HTMLTransform; + }), + new TransformData('text/plain', async () => { + const module = await import(/* webpackChunkName: "nteract_transforms" */ '@nteract/transforms'); + return module.TextTransform; + }) +]; + +export function getRichestMimetype(data: any): string { + // Go through the keys of this object and find their index in the map + let index = mimeTypeToImport.length; + const keys = Object.keys(data); + keys.forEach((k) => { + const keyIndex = mimeTypeToImport.findIndex((m) => m.mimeType === k); + if (keyIndex >= 0 && keyIndex < index) { + // If higher up the chain, pick the higher up key + index = keyIndex; + } + }); + + // If this index is found, return the mimetype to use. + if (index < mimeTypeToImport.length) { + return mimeTypeToImport[index].mimeType; + } + + // Don't know which to pick, just pick the first. + return keys[0]; +} + +export function getTransform(mimeType: string): LoadableComponent<{ data: any }> { + return Loadable<{ data: any }>( + async () => { + const match = mimeTypeToImport.find((m) => m.mimeType === mimeType); + if (match) { + const transform = await match.getComponent(); + return transform; + } + + return <div>`Transform not found for mimetype ${mimeType}`</div>; + }, + { fallback: <div>{getLocString('DataScience.variableLoadingValue', 'Loading...')}</div> } + ); +} + +export async function forceLoad() { + // Used for tests to make sure we don't end up with 'Loading ...' anywhere in a test + await Promise.all(mimeTypeToImport.map((m) => m.getComponent())); +} + +export function isMimeTypeSupported(mimeType: string): boolean { + const match = mimeTypeToImport.find((m) => m.mimeType === mimeType); + return match ? true : false; +} + +export function isIPyWidgetOutput(data: {}): boolean { + return ( + data && + (data as Object).hasOwnProperty && + (data as Object).hasOwnProperty('application/vnd.jupyter.widget-view+json') + ); +} diff --git a/src/datascience-ui/interactive-common/trimmedOutputLink.tsx b/src/datascience-ui/interactive-common/trimmedOutputLink.tsx new file mode 100644 index 000000000000..9300356204c1 --- /dev/null +++ b/src/datascience-ui/interactive-common/trimmedOutputLink.tsx @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as React from 'react'; +import { getLocString } from '../react-common/locReactSide'; + +interface ITrimmedOutputMessage { + openSettings(setting?: string): void; +} + +export class TrimmedOutputMessage extends React.PureComponent<ITrimmedOutputMessage> { + constructor(props: ITrimmedOutputMessage) { + super(props); + } + + public render() { + const newLine = '\n...\n'; + return ( + <a + onClick={this.changeTextOutputLimit} + role="button" + className="image-button-image outputTrimmedSettingsLink" + > + {getLocString( + 'DataScience.trimmedOutput', + 'Output was trimmed for performance reasons.\nTo see the full output set the setting "python.dataScience.textOutputLimit" to 0.' + ) + newLine} + </a> + ); + } + private changeTextOutputLimit = () => { + this.props.openSettings('python.dataScience.textOutputLimit'); + }; +} diff --git a/src/datascience-ui/interactive-common/trustMessage.tsx b/src/datascience-ui/interactive-common/trustMessage.tsx new file mode 100644 index 000000000000..c97a97e6c6cb --- /dev/null +++ b/src/datascience-ui/interactive-common/trustMessage.tsx @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as React from 'react'; +import { getLocString } from '../react-common/locReactSide'; +import { getMaxWidth } from './utils'; + +interface ITrustMessageProps { + shouldShowTrustMessage: boolean; + isNotebookTrusted?: boolean; + launchNotebookTrustPrompt?(): void; // Native editor-specific +} + +export class TrustMessage extends React.PureComponent<ITrustMessageProps> { + public render() { + const text = this.props.isNotebookTrusted + ? getLocString('DataScience.notebookIsTrusted', 'Trusted') + : getLocString('DataScience.notebookIsNotTrusted', 'Not Trusted'); + const textSize = text.length; + const maxWidth: React.CSSProperties = { + maxWidth: getMaxWidth(textSize + 5) // plus 5 for the line and margins, + }; + const dynamicStyle: React.CSSProperties = { + maxWidth: getMaxWidth(textSize), + color: this.props.isNotebookTrusted + ? 'var(--vscode-editor-foreground)' + : 'var(--vscode-editorError-foreground)', + cursor: this.props.isNotebookTrusted ? undefined : 'pointer' + }; + + return ( + <div className="kernel-status" style={maxWidth}> + <button + type="button" + disabled={this.props.isNotebookTrusted} + aria-disabled={this.props.isNotebookTrusted} + className={`jupyter-info-section${ + this.props.isNotebookTrusted ? '' : ' jupyter-info-section-hoverable' + }`} // Disable animation on hover for already-trusted notebooks + style={dynamicStyle} + onClick={this.props.launchNotebookTrustPrompt} + > + <div className="kernel-status-text">{text}</div> + </button> + <div className="kernel-status-divider" /> + </div> + ); + } +} diff --git a/src/datascience-ui/interactive-common/utils.ts b/src/datascience-ui/interactive-common/utils.ts new file mode 100644 index 000000000000..11e62d3b8a70 --- /dev/null +++ b/src/datascience-ui/interactive-common/utils.ts @@ -0,0 +1,6 @@ +export function getMaxWidth(charLength: number): string { + // This comes from a linear regression + const width = 0.57674 * charLength + 1.70473; + const unit = 'em'; + return Math.round(width).toString() + unit; +} diff --git a/src/datascience-ui/interactive-common/variableExplorer.css b/src/datascience-ui/interactive-common/variableExplorer.css new file mode 100644 index 000000000000..5a47b522b165 --- /dev/null +++ b/src/datascience-ui/interactive-common/variableExplorer.css @@ -0,0 +1,24 @@ +.variable-explorer { + margin: 5px; + background: var(--override-background, var(--vscode-editor-background)); + color: var(--override-foreground, var(--vscode-editor-foreground)); + overflow: hidden; +} + +#variable-explorer-data-grid { + margin: 4px; +} + +.span-debug-message { + margin: 4px; +} + +#variable-divider { + width: 100%; + border-top-color: var(--override-badge-background, var(--vscode-badge-background)); + border-top-style: solid; + border-top-width: 2px; + cursor: ns-resize; + z-index: 999; + position: fixed; +} diff --git a/src/datascience-ui/interactive-common/variableExplorer.tsx b/src/datascience-ui/interactive-common/variableExplorer.tsx new file mode 100644 index 000000000000..7d4ea054d751 --- /dev/null +++ b/src/datascience-ui/interactive-common/variableExplorer.tsx @@ -0,0 +1,460 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import './variableExplorer.css'; + +import * as fastDeepEqual from 'fast-deep-equal'; +import * as React from 'react'; + +import { RegExpValues } from '../../client/datascience/constants'; +import { IJupyterVariable } from '../../client/datascience/types'; +import { Image, ImageName } from '../react-common/image'; +import { ImageButton } from '../react-common/imageButton'; +import { getLocString } from '../react-common/locReactSide'; +import { IButtonCellValue, VariableExplorerButtonCellFormatter } from './variableExplorerButtonCellFormatter'; +import { CellStyle, VariableExplorerCellFormatter } from './variableExplorerCellFormatter'; +import { VariableExplorerEmptyRowsView } from './variableExplorerEmptyRows'; + +import * as AdazzleReactDataGrid from 'react-data-grid'; +import { VariableExplorerHeaderCellFormatter } from './variableExplorerHeaderCellFormatter'; +import { VariableExplorerRowRenderer } from './variableExplorerRowRenderer'; + +// tslint:disable-next-line: import-name +import Draggable from 'react-draggable'; + +import { IVariableState } from './redux/reducers/variables'; +import './variableExplorerGrid.less'; + +interface IVariableExplorerProps { + baseTheme: string; + skipDefault?: boolean; + variables: IJupyterVariable[]; + debugging: boolean; + supportsDebugging: boolean; + fontSize: number; + executionCount: number; + refreshCount: number; + offsetHeight: number; + gridHeight: number; + containerHeight: number; + showDataExplorer(targetVariable: IJupyterVariable, numberOfColumns: number): void; + closeVariableExplorer(): void; + setVariableExplorerHeight(containerHeight: number, gridHeight: number): void; + pageIn(startIndex: number, pageSize: number): void; +} + +const defaultColumnProperties = { + filterable: false, + sortable: false, + resizable: true +}; + +interface IFormatterArgs { + isScrolling?: boolean; + value?: string | number | object | boolean; + row?: IGridRow; +} + +interface IGridRow { + // tslint:disable-next-line:no-any + name: string; + type: string; + size: string; + value: string | undefined; + index: number; + buttons: IButtonCellValue; +} + +interface IVariableExplorerState { + containerHeight: number; + gridHeight: number; +} + +// tslint:disable:no-any +export class VariableExplorer extends React.Component<IVariableExplorerProps, IVariableExplorerState> { + private variableExplorerRef: React.RefObject<HTMLDivElement>; + private variableExplorerMenuBarRef: React.RefObject<HTMLDivElement>; + private variablePanelRef: React.RefObject<HTMLDivElement>; + + private pageSize: number = -1; + + // These values keep track of variable requests so we don't make the same ones over and over again + // Note: This isn't in the redux state because the requests will come before the state + // has been updated. We don't want to force a wait for redraw to determine if a request + // has been sent or not. + private requestedPages: number[] = []; + private requestedPagesExecutionCount: number = 0; + private requestedRefreshCount: number = 0; + private gridColumns: { + key: string; + name: string; + type: string; + width: number; + formatter: any; + headerRenderer?: JSX.Element; + sortable?: boolean; + resizable?: boolean; + }[]; + + constructor(prop: IVariableExplorerProps) { + super(prop); + + this.state = { + containerHeight: this.props.containerHeight, + gridHeight: this.props.gridHeight + }; + + this.handleResizeMouseMove = this.handleResizeMouseMove.bind(this); + this.setInitialHeight = this.setInitialHeight.bind(this); + this.saveCurrentSize = this.saveCurrentSize.bind(this); + + this.gridColumns = [ + { + key: 'buttons', + name: '', + type: 'boolean', + width: 36, + sortable: false, + resizable: false, + formatter: ( + <VariableExplorerButtonCellFormatter + showDataExplorer={this.props.showDataExplorer} + baseTheme={this.props.baseTheme} + /> + ) + }, + { + key: 'name', + name: getLocString('DataScience.variableExplorerNameColumn', 'Name'), + type: 'string', + width: 120, + formatter: this.formatNameColumn, + headerRenderer: <VariableExplorerHeaderCellFormatter /> + }, + { + key: 'type', + name: getLocString('DataScience.variableExplorerTypeColumn', 'Type'), + type: 'string', + width: 120, + formatter: <VariableExplorerCellFormatter cellStyle={CellStyle.string} />, + headerRenderer: <VariableExplorerHeaderCellFormatter /> + }, + { + key: 'size', + name: getLocString('DataScience.variableExplorerCountColumn', 'Size'), + type: 'string', + width: 120, + formatter: <VariableExplorerCellFormatter cellStyle={CellStyle.numeric} />, + headerRenderer: <VariableExplorerHeaderCellFormatter /> + }, + { + key: 'value', + name: getLocString('DataScience.variableExplorerValueColumn', 'Value'), + type: 'string', + width: 300, + formatter: <VariableExplorerCellFormatter cellStyle={CellStyle.string} />, + headerRenderer: <VariableExplorerHeaderCellFormatter /> + } + ]; + + this.variableExplorerRef = React.createRef<HTMLDivElement>(); + this.variablePanelRef = React.createRef<HTMLDivElement>(); + this.variableExplorerMenuBarRef = React.createRef<HTMLDivElement>(); + } + + public componentDidMount() { + if (this.state.containerHeight === 0) { + this.setInitialHeight(); + } + } + + public shouldComponentUpdate(nextProps: IVariableExplorerProps, prevState: IVariableState): boolean { + if (this.props.fontSize !== nextProps.fontSize) { + // Size has changed, recompute page size + this.pageSize = -1; + return true; + } + if (!fastDeepEqual(this.props.variables, nextProps.variables)) { + return true; + } + if ( + prevState.containerHeight !== this.state.containerHeight || + prevState.gridHeight !== this.state.gridHeight + ) { + return true; + } + + return false; + } + + public render() { + const contentClassName = `variable-explorer-content`; + const containerHeight = this.state.containerHeight; + let variableExplorerStyles: React.CSSProperties = { fontSize: `${this.props.fontSize.toString()}px` }; + + // add properties to explorer styles + if (containerHeight !== 0) { + variableExplorerStyles = { ...variableExplorerStyles, height: containerHeight }; + } + + return ( + <Draggable handle=".handle-resize" onDrag={this.handleResizeMouseMove} onStop={this.saveCurrentSize}> + <span> + <div id="variable-panel" ref={this.variablePanelRef}> + <div id="variable-panel-padding"> + <div + className="variable-explorer" + ref={this.variableExplorerRef} + style={variableExplorerStyles} + > + <div className="variable-explorer-menu-bar" ref={this.variableExplorerMenuBarRef}> + <label className="inputLabel variable-explorer-label"> + {getLocString('DataScience.collapseVariableExplorerLabel', 'Variables')} + </label> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.props.closeVariableExplorer} + className="variable-explorer-close-button" + tooltip={getLocString('DataScience.close', 'Close')} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.Cancel} + /> + </ImageButton> + </div> + <div className={contentClassName}>{this.renderGrid()}</div> + </div> + </div> + <div id="variable-divider" className="handle-resize" /> + </div> + </span> + </Draggable> + ); + } + + private renderGrid() { + if (this.props.debugging && !this.props.supportsDebugging) { + return ( + <span className="span-debug-message"> + {getLocString( + 'DataScience.variableExplorerDisabledDuringDebugging', + "Please see the Debug Side Bar's VARIABLES section." + )} + </span> + ); + } else { + return ( + <div + id="variable-explorer-data-grid" + role="table" + aria-label={getLocString('DataScience.collapseVariableExplorerLabel', 'Variables')} + > + <AdazzleReactDataGrid + columns={this.gridColumns.map((c) => { + return { ...defaultColumnProperties, ...c }; + })} + // tslint:disable-next-line: react-this-binding-issue + rowGetter={this.getRow} + rowsCount={this.props.variables.length} + minHeight={this.state.gridHeight} + headerRowHeight={this.getRowHeight()} + rowHeight={this.getRowHeight()} + onRowDoubleClick={this.rowDoubleClick} + emptyRowsView={VariableExplorerEmptyRowsView} + rowRenderer={VariableExplorerRowRenderer} + /> + </div> + ); + } + } + + private saveCurrentSize() { + this.props.setVariableExplorerHeight(this.state.containerHeight, this.state.gridHeight); + } + + private getRowHeight() { + return this.props.fontSize + 11; + } + + private setInitialHeight() { + const variablePanel = this.variablePanelRef.current; + if (!variablePanel) { + return; + } + this.setState({ + containerHeight: variablePanel.offsetHeight + }); + } + + private handleResizeMouseMove(e: any) { + this.setVariableExplorerHeight(e); + this.setVariableGridHeight(); + } + + private setVariableExplorerHeight(e: MouseEvent) { + const variableExplorerMenuBar = this.variableExplorerMenuBarRef.current; + const variablePanel = this.variablePanelRef.current; + const variableExplorer = this.variableExplorerRef.current; + + if (!variableExplorerMenuBar || !variablePanel || !variableExplorer) { + return; + } + + const relY = e.pageY - variableExplorer.clientTop; + const addHeight = relY - variablePanel.offsetHeight - this.props.offsetHeight; + const updatedHeight = this.state.containerHeight + addHeight; + + // min height is one row of visible data + const minHeight = this.getRowHeight() * 2 + variableExplorerMenuBar.clientHeight; + const maxHeight = document.body.scrollHeight - this.props.offsetHeight - variableExplorerMenuBar.clientHeight; + + if (updatedHeight >= minHeight && updatedHeight <= maxHeight) { + this.setState({ + containerHeight: updatedHeight + }); + } + } + + private setVariableGridHeight() { + const variableExplorerMenuBar = this.variableExplorerMenuBarRef.current; + + if (!variableExplorerMenuBar) { + return; + } + + const updatedHeight = this.state.containerHeight - variableExplorerMenuBar.clientHeight; + + this.setState({ + gridHeight: updatedHeight + }); + } + + private formatNameColumn = (args: IFormatterArgs): JSX.Element => { + if (!args.isScrolling && args.row !== undefined && !args.value) { + this.ensureLoaded(args.row.index); + } + + return <VariableExplorerCellFormatter value={args.value} role={'cell'} cellStyle={CellStyle.variable} />; + }; + + private getRow = (index: number): IGridRow => { + if (index >= 0 && index < this.props.variables.length) { + const variable = this.props.variables[index]; + if (variable && variable.value) { + let newSize = ''; + if (variable.shape && variable.shape !== '') { + newSize = variable.shape; + } else if (variable.count) { + newSize = variable.count.toString(); + } + return { + buttons: { + name: variable.name, + supportsDataExplorer: variable.supportsDataExplorer, + variable, + numberOfColumns: this.getColumnCountFromShape(variable.shape) + }, + name: variable.name, + type: variable.type, + size: newSize, + index, + value: variable.value + ? variable.value + : getLocString('DataScience.variableLoadingValue', 'Loading...') + }; + } + } + + return { + buttons: { supportsDataExplorer: false, name: '', numberOfColumns: 0, variable: undefined }, + name: '', + type: '', + size: '', + index, + value: getLocString('DataScience.variableLoadingValue', 'Loading...') + }; + }; + + private computePageSize(): number { + if (this.pageSize === -1) { + // Based on font size and height of the main div + if (this.variableExplorerRef.current) { + this.pageSize = Math.max( + 16, + Math.round(this.variableExplorerRef.current.offsetHeight / this.props.fontSize) + ); + } else { + this.pageSize = 50; + } + } + return this.pageSize; + } + + private ensureLoaded = (index: number) => { + // Figure out how many items in a page + const pageSize = this.computePageSize(); + + // Skip if already pending or already have a value + const haveValue = this.props.variables[index]?.value; + const newExecution = + this.props.executionCount !== this.requestedPagesExecutionCount || + this.props.refreshCount !== this.requestedRefreshCount; + // tslint:disable-next-line: restrict-plus-operands + const notRequested = !this.requestedPages.find((n) => n <= index && index < n + pageSize); + if (!haveValue && (newExecution || notRequested)) { + // Try to find a page of data around this index. + let pageIndex = index; + while ( + pageIndex >= 0 && + pageIndex > index - pageSize / 2 && + (!this.props.variables[pageIndex] || !this.props.variables[pageIndex].value) + ) { + pageIndex -= 1; + } + + // Clear out requested pages if new requested execution + if ( + this.requestedPagesExecutionCount !== this.props.executionCount || + this.requestedRefreshCount !== this.props.refreshCount + ) { + this.requestedPages = []; + } + + // Save in the list of requested pages + this.requestedPages.push(pageIndex + 1); + + // Save the execution count for this request so we can verify we can skip it on next request. + this.requestedPagesExecutionCount = this.props.executionCount; + this.requestedRefreshCount = this.props.refreshCount; + + // Load this page. + this.props.pageIn(pageIndex + 1, pageSize); + } + }; + + private getColumnCountFromShape(shape: string | undefined): number { + if (shape) { + // Try to match on the second value if there is one + const matches = RegExpValues.ShapeSplitterRegEx.exec(shape); + if (matches && matches.length > 1) { + return parseInt(matches[1], 10); + } + } + return 0; + } + + private rowDoubleClick = (_rowIndex: number, row: IGridRow) => { + // On row double click, see if data explorer is supported and open it if it is + if ( + row.buttons && + row.buttons.supportsDataExplorer !== undefined && + row.buttons.name && + row.buttons.supportsDataExplorer && + row.buttons.variable + ) { + this.props.showDataExplorer(row.buttons.variable, row.buttons.numberOfColumns); + } + }; +} diff --git a/src/datascience-ui/interactive-common/variableExplorerButtonCellFormatter.css b/src/datascience-ui/interactive-common/variableExplorerButtonCellFormatter.css new file mode 100644 index 000000000000..8ad219b7558c --- /dev/null +++ b/src/datascience-ui/interactive-common/variableExplorerButtonCellFormatter.css @@ -0,0 +1,4 @@ +.variable-explorer-button-cell { + height: 18px; + width: 18px; +} diff --git a/src/datascience-ui/interactive-common/variableExplorerButtonCellFormatter.tsx b/src/datascience-ui/interactive-common/variableExplorerButtonCellFormatter.tsx new file mode 100644 index 000000000000..cda13175141f --- /dev/null +++ b/src/datascience-ui/interactive-common/variableExplorerButtonCellFormatter.tsx @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as React from 'react'; + +import { Image, ImageName } from '../react-common/image'; +import { ImageButton } from '../react-common/imageButton'; +import { getLocString } from '../react-common/locReactSide'; + +import { IJupyterVariable } from '../../client/datascience/types'; +import './variableExplorerButtonCellFormatter.css'; + +export interface IButtonCellValue { + supportsDataExplorer: boolean; + name: string; + variable?: IJupyterVariable; + numberOfColumns: number; +} + +interface IVariableExplorerButtonCellFormatterProps { + baseTheme: string; + value?: IButtonCellValue; + showDataExplorer(targetVariable: IJupyterVariable, numberOfColumns: number): void; +} + +export class VariableExplorerButtonCellFormatter extends React.Component<IVariableExplorerButtonCellFormatterProps> { + public shouldComponentUpdate(nextProps: IVariableExplorerButtonCellFormatterProps) { + return nextProps.value !== this.props.value; + } + + public render() { + const className = 'variable-explorer-button-cell'; + if (this.props.value !== null && this.props.value !== undefined) { + if (this.props.value.supportsDataExplorer) { + return ( + <div className={className}> + <ImageButton + baseTheme={this.props.baseTheme} + tooltip={getLocString( + 'DataScience.showDataExplorerTooltip', + 'Show variable in data viewer.' + )} + onClick={this.onDataExplorerClick} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.OpenInNewWindow} + /> + </ImageButton> + </div> + ); + } else { + return null; + } + } + return []; + } + + private onDataExplorerClick = () => { + if (this.props.value !== null && this.props.value !== undefined && this.props.value.variable) { + this.props.showDataExplorer(this.props.value.variable, this.props.value.numberOfColumns); + } + }; +} diff --git a/src/datascience-ui/interactive-common/variableExplorerCellFormatter.css b/src/datascience-ui/interactive-common/variableExplorerCellFormatter.css new file mode 100644 index 000000000000..ffdc679ccacb --- /dev/null +++ b/src/datascience-ui/interactive-common/variableExplorerCellFormatter.css @@ -0,0 +1,32 @@ +.react-grid-variable-explorer-cell-variable { + color: var(--code-variable-color, var(--vscode-editor-foreground)); +} + +.react-grid-variable-explorer-cell-type { + color: var(--code-type-color, var(--vscode-editor-foreground)); +} + +.react-grid-variable-explorer-cell-string { + color: var(--code-string-color, var(--vscode-editor-foreground)); +} + +.react-grid-variable-explorer-cell-numeric { + color: var(--code-numeric-color, var(--vscode-editor-foreground)); +} + +/* High contrast uses an inverse for selection highlights, so just use foreground when hovering */ +body.vscode-high-contrast .react-grid-Row:hover .react-grid-variable-explorer-cell-variable { + color: var(--override-selection-foreground, var(--vscode-editor-selectionForeground)); +} + +body.vscode-high-contrast .react-grid-Row:hover .react-grid-variable-explorer-cell-type { + color: var(--override-selection-foreground, var(--vscode-editor-selectionForeground)); +} + +body.vscode-high-contrast .react-grid-Row:hover .react-grid-variable-explorer-cell-string { + color: var(--override-selection-foreground, var(--vscode-editor-selectionForeground)); +} + +body.vscode-high-contrast .react-grid-Row:hover .react-grid-variable-explorer-cell-numeric { + color: var(--override-selection-foreground, var(--vscode-editor-selectionForeground)); +} \ No newline at end of file diff --git a/src/datascience-ui/interactive-common/variableExplorerCellFormatter.tsx b/src/datascience-ui/interactive-common/variableExplorerCellFormatter.tsx new file mode 100644 index 000000000000..eea7b1db1b66 --- /dev/null +++ b/src/datascience-ui/interactive-common/variableExplorerCellFormatter.tsx @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import './variableExplorerCellFormatter.css'; + +import * as React from 'react'; + +export enum CellStyle { + variable = 'variable', + type = 'type', + string = 'string', + numeric = 'numeric' +} + +interface IVariableExplorerCellFormatterProps { + cellStyle: CellStyle; + // value gets populated by the default cell formatter props + value?: string | number | object | boolean; + role?: string; +} + +// Our formatter for cells in the variable explorer. Allow for different styles per column type +export class VariableExplorerCellFormatter extends React.Component<IVariableExplorerCellFormatterProps> { + public shouldComponentUpdate(nextProps: IVariableExplorerCellFormatterProps) { + return nextProps.value !== this.props.value; + } + + public render() { + const className = `react-grid-variable-explorer-cell-${this.props.cellStyle.toString()}`; + if (this.props.value !== null && this.props.value !== undefined) { + return ( + <div + className={className} + role={this.props.role ? this.props.role : 'cell'} + title={this.props.value.toString()} + > + {this.props.value} + </div> + ); + } + return []; + } +} diff --git a/src/datascience-ui/interactive-common/variableExplorerEmptyRows.css b/src/datascience-ui/interactive-common/variableExplorerEmptyRows.css new file mode 100644 index 000000000000..23fe2407049e --- /dev/null +++ b/src/datascience-ui/interactive-common/variableExplorerEmptyRows.css @@ -0,0 +1,4 @@ +#variable-explorer-empty-rows { + margin: 5px; + font-family: var(--code-font-family); +} \ No newline at end of file diff --git a/src/datascience-ui/interactive-common/variableExplorerEmptyRows.tsx b/src/datascience-ui/interactive-common/variableExplorerEmptyRows.tsx new file mode 100644 index 000000000000..1668e1d35c99 --- /dev/null +++ b/src/datascience-ui/interactive-common/variableExplorerEmptyRows.tsx @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import './variableExplorerEmptyRows.css'; + +import * as React from 'react'; +import { getLocString } from '../react-common/locReactSide'; + +export const VariableExplorerEmptyRowsView = () => { + const message = getLocString('DataScience.noRowsInVariableExplorer', 'No variables defined'); + + return <div id="variable-explorer-empty-rows">{message}</div>; +}; diff --git a/src/datascience-ui/interactive-common/variableExplorerGrid.less b/src/datascience-ui/interactive-common/variableExplorerGrid.less new file mode 100644 index 000000000000..eb261d0ba265 --- /dev/null +++ b/src/datascience-ui/interactive-common/variableExplorerGrid.less @@ -0,0 +1,127 @@ +/* Import bootstrap, but prefix it all with our grid div so we don't clobber our interactive windows styles */ +#variable-explorer-data-grid { + @import "~bootstrap-less/bootstrap/bootstrap.less"; +} + +#variable-explorer-data-grid .form-control { + height: auto; + padding: 0px; + font-size: inherit; + font-weight: inherit; + line-height: inherit; + border-radius: 0px; +} + +#variable-explorer-data-grid .react-grid-Main { + font-family: var(--code-font-family); + background-color: var(--override-background, var(--vscode-editor-background)); + color: var(--override-foreground, var(--vscode-editor-foreground)); + outline: none; +} + +#variable-explorer-data-grid .react-grid-Grid { + background-color: var(--override-background, var(--vscode-editor-background)); + color: var(--override-foreground, var(--vscode-editor-foreground)); + border-style: none; +} + +#variable-explorer-data-grid .react-grid-Canvas { + background-color: var(--override-background, var(--vscode-editor-background)); + color: var(--override-foreground, var(--vscode-editor-foreground)); +} + +#variable-explorer-data-grid .react-grid-Header { + background-color: var(--override-tabs-background, var(--vscode-notifications-background)); +} + +#variable-explorer-data-grid .react-grid-HeaderCell { + background-color: var(--override-lineHighlightBorder, var(--vscode-editor-lineHighlightBorder)); + color: var(--override-foreground, var(--vscode-editor-foreground)); + border-style: solid; + border-color: var(--override-badge-background, var(--vscode-badge-background)); + padding: 2px; +} + + +#variable-explorer-data-grid .react-grid-Row--even { + background-color: var(--override-background, var(--vscode-editor-background)); + color: var(--override-foreground, var(--vscode-editor-foreground)); + border-right-color: var(--override-badge-background, var(--vscode-badge-background)); + border-bottom-color: var(--override-badge-background, var(--vscode-badge-background)); +} + +#variable-explorer-data-grid .react-grid-Row--odd { + background-color: var(--override-lineHighlightBorder, var(--vscode-editor-lineHighlightBorder)); + color: var(--override-foreground, var(--vscode-editor-foreground)); + border-right-color: var(--override-badge-background, var(--vscode-badge-background)); + border-bottom-color: var(--override-badge-background, var(--vscode-badge-background)); +} + +#variable-explorer-data-grid .react-grid-Cell { + background-color: transparent; + color: var(--override-foreground, var(--vscode-editor-foreground)); + border-style: none; +} + +#variable-explorer-data-grid .react-grid-Cell:hover { + background-color: var(--override-selection-background, var(--vscode-editor-selectionBackground)); +} + +#variable-explorer-data-grid .react-grid-Row:hover { + background-color: var(--override-selection-background, var(--vscode-editor-selectionBackground)); +} + +#variable-explorer-data-grid .react-grid-Row:hover .react-grid-Cell { + background-color: var(--override-selection-background, var(--vscode-editor-selectionBackground)); +} + +#variable-explorer-data-grid .rdg-selected { + visibility: hidden; +} + +// High contrast theme changes + +// Notifications-background and line-highlight-border don't work in high contrast mode, so skip the header color in high contrast themes +body.vscode-high-contrast #variable-explorer-data-grid .react-grid-Header { + background-color: var(--override-background, var(--vscode-editor-background)); + border-color: var(--override-foreground, var(--vscode-editor-foreground)); +} + +body.vscode-high-contrast #variable-explorer-data-grid .react-grid-HeaderCell { + background-color: var(--override-background, var(--vscode-editor-background)); + border-color: var(--override-foreground, var(--vscode-editor-foreground)); +} + +body.vscode-high-contrast #variable-explorer-data-grid .react-grid-Row--odd { + background-color: var(--override-background, var(--vscode-editor-background)); + border-color: var(--override-foreground, var(--vscode-editor-foreground)); +} + +// Since we have removed zebra striping in HC mode, add back in grid lines +body.vscode-high-contrast #variable-explorer-data-grid .react-grid-Cell { + border-style: solid; + border-width: 1px; + border-color: var(--override-foreground, var(--vscode-editor-foreground)); +} + + +// HC inverts selection colors, so use the selection foreground color on hover +body.vscode-high-contrast #variable-explorer-data-grid .react-grid-Cell:hover { + color: var(--override-selection-foreground, var(--vscode-editor-selectionForeground)); +} + +body.vscode-high-contrast #variable-explorer-data-grid .react-grid-Row:hover { + color: var(--override-selection-foreground, var(--vscode-editor-selectionForeground)); +} + +body.vscode-high-contrast #variable-explorer-data-grid .react-grid-Row:hover .react-grid-Cell { + color: var(--override-selection-foreground, var(--vscode-editor-selectionForeground)); +} + +// HC inverts selection, which messes up our icons, just keep them with the same foreground / background combo +body.vscode-high-contrast #variable-explorer-data-grid .react-grid-Row:hover .react-grid-Cell .variable-explorer-button-cell { + color: var(--override-foreground, var(--vscode-editor-foreground)); + background-color: var(--override-background, var(--vscode-editor-background)); +} + + diff --git a/src/datascience-ui/interactive-common/variableExplorerHeaderCellFormatter.tsx b/src/datascience-ui/interactive-common/variableExplorerHeaderCellFormatter.tsx new file mode 100644 index 000000000000..69840df5d7bb --- /dev/null +++ b/src/datascience-ui/interactive-common/variableExplorerHeaderCellFormatter.tsx @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as React from 'react'; + +interface IVariableExplorerHeaderCellFormatterProps { + // value gets populated by the default cell formatter props + column?: { + name: string; + }; +} + +// Our formatter for cells in the variable explorer. Allow for different styles per column type +export class VariableExplorerHeaderCellFormatter extends React.Component<IVariableExplorerHeaderCellFormatterProps> { + public render() { + if (this.props.column) { + return ( + <div role="columnheader"> + <span>{this.props.column.name}</span> + </div> + ); + } + } +} diff --git a/src/datascience-ui/interactive-common/variableExplorerRowRenderer.tsx b/src/datascience-ui/interactive-common/variableExplorerRowRenderer.tsx new file mode 100644 index 000000000000..c346a7ef3ead --- /dev/null +++ b/src/datascience-ui/interactive-common/variableExplorerRowRenderer.tsx @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as React from 'react'; + +// tslint:disable:no-any +interface IVariableExplorerRowProps { + renderBaseRow(props: any): JSX.Element; +} + +export const VariableExplorerRowRenderer: React.SFC<IVariableExplorerRowProps & any> = (props) => { + return <div role="row">{props.renderBaseRow(props)}</div>; +}; diff --git a/src/datascience-ui/interactive-common/variablePanel.tsx b/src/datascience-ui/interactive-common/variablePanel.tsx new file mode 100644 index 000000000000..dbd577c54ee7 --- /dev/null +++ b/src/datascience-ui/interactive-common/variablePanel.tsx @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as React from 'react'; + +import { IJupyterVariable } from '../../client/datascience/types'; +import { VariableExplorer } from './variableExplorer'; + +export interface IVariablePanelProps { + baseTheme: string; + busy: boolean; + skipDefault?: boolean; + testMode?: boolean; + variables: IJupyterVariable[]; + executionCount: number; + refreshCount: number; + debugging: boolean; + supportsDebugging: boolean; + fontSize: number; + offsetHeight: number; + gridHeight: number; + containerHeight: number; + showDataExplorer(targetVariable: IJupyterVariable, numberOfColumns: number): void; + closeVariableExplorer(): void; + // tslint:disable-next-line: no-any + setVariableExplorerHeight(containerHeight: number, gridHeight: number): any; + pageIn(startIndex: number, pageSize: number): void; +} + +export class VariablePanel extends React.Component<IVariablePanelProps> { + constructor(prop: IVariablePanelProps) { + super(prop); + } + + public render() { + return ( + <VariableExplorer + gridHeight={this.props.gridHeight} + containerHeight={this.props.containerHeight} + offsetHeight={this.props.offsetHeight} + fontSize={this.props.fontSize} + variables={this.props.variables} + debugging={this.props.debugging} + baseTheme={this.props.baseTheme} + skipDefault={this.props.skipDefault} + showDataExplorer={this.props.showDataExplorer} + closeVariableExplorer={this.props.closeVariableExplorer} + setVariableExplorerHeight={this.props.setVariableExplorerHeight} + pageIn={this.props.pageIn} + executionCount={this.props.executionCount} + supportsDebugging={this.props.supportsDebugging} + refreshCount={this.props.refreshCount} + /> + ); + } +} diff --git a/src/datascience-ui/ipywidgets/README.md b/src/datascience-ui/ipywidgets/README.md new file mode 100644 index 000000000000..1162960b7ba5 --- /dev/null +++ b/src/datascience-ui/ipywidgets/README.md @@ -0,0 +1,17 @@ +# Hosting ipywidgets in non-notebook context + +- Much of the work is influenced by sample `web3` from https://github.com/jupyter-widgets/ipywidgets/blob/master/examples/web3 +- Displaying `ipywidgets` in non notebook context requires 3 things: + - [requirejs](https://requirejs.org) + - [HTML Manager](https://github.com/jupyter-widgets/ipywidgets/blob/master/examples/web3/src/manager.ts) + - Live Kerne (the widget manager plugs directly into the `kernel` messages to read/write and build data for displaying the data) + +# Kernel + +- As the kernel connection is only available in back end (`extension code`), the HTML manager will not work. +- To get this working, all we need to do is create a `proxy kernel` connection in the `react` layer. +- Thats what the code in this folder does (wraps the html manager + custom kernel connection) +- Kernel messages from the extension are sent to this layer using the `postoffice` +- Similarly messages from sent from html manager via the kernel are sent to the actual kernel via the postoffice. +- However, the sequence and massaging of the kernel messages requires a lot of work. Basically majority of the message processing from `/node_modules/@jupyterlab/services/lib/kernel/*.js` + - Much of the message processing logic is borrowed from `comm.js`, `default.js`, `future.js`, `kernel.js` and `manager.js`. diff --git a/src/datascience-ui/ipywidgets/container.tsx b/src/datascience-ui/ipywidgets/container.tsx new file mode 100644 index 000000000000..ef5fed6a19d5 --- /dev/null +++ b/src/datascience-ui/ipywidgets/container.tsx @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as isonline from 'is-online'; +import * as React from 'react'; +import { Store } from 'redux'; +import '../../client/common/extensions'; +import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { + IInteractiveWindowMapping, + IPyWidgetMessages +} from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { WidgetScriptSource } from '../../client/datascience/ipywidgets/types'; +import { SharedMessages } from '../../client/datascience/messages'; +import { IDataScienceExtraSettings } from '../../client/datascience/types'; +import { + CommonAction, + CommonActionType, + ILoadIPyWidgetClassFailureAction, + LoadIPyWidgetClassLoadAction, + NotifyIPyWidgeWidgetVersionNotSupportedAction +} from '../interactive-common/redux/reducers/types'; +import { IStore } from '../interactive-common/redux/store'; +import { PostOffice } from '../react-common/postOffice'; +import { warnAboutWidgetVersionsThatAreNotSupported } from './incompatibleWidgetHandler'; +import { WidgetManager } from './manager'; +import { registerScripts } from './requirejsRegistry'; + +type Props = { + postOffice: PostOffice; + widgetContainerId: string; + store: Store<IStore> & { dispatch: unknown }; +}; + +export class WidgetManagerComponent extends React.Component<Props> { + private readonly widgetManager: WidgetManager; + private readonly widgetSourceRequests = new Map< + string, + { deferred: Deferred<void>; timer: NodeJS.Timeout | number | undefined } + >(); + private readonly registeredWidgetSources = new Map<string, WidgetScriptSource>(); + private timedoutWaitingForWidgetsToGetLoaded?: boolean; + private widgetsCanLoadFromCDN: boolean = false; + private readonly loaderSettings = { + // Total time to wait for a script to load. This includes ipywidgets making a request to extension for a Uri of a widget, + // then extension replying back with the Uri (max 5 seconds round trip time). + // If expires, then Widget downloader will attempt to download with what ever information it has (potentially failing). + // Note, we might have a message displayed at the user end (asking for consent to use CDN). + // Hence use 60 seconds. + timeoutWaitingForScriptToLoad: 60_000, + // List of widgets that must always be loaded using requirejs instead of using a CDN or the like. + widgetsRegisteredInRequireJs: new Set<string>(), + // Callback when loading a widget fails. + errorHandler: this.handleLoadError.bind(this), + // Callback when requesting a module be registered with requirejs (if possible). + loadWidgetScript: this.loadWidgetScript.bind(this), + successHandler: this.handleLoadSuccess.bind(this) + }; + constructor(props: Props) { + super(props); + this.widgetManager = new WidgetManager( + document.getElementById(this.props.widgetContainerId)!, + this.props.postOffice, + this.loaderSettings + ); + + props.postOffice.addHandler({ + // tslint:disable-next-line: no-any + handleMessage: (type: string, payload?: any) => { + if (type === SharedMessages.UpdateSettings) { + const settings = JSON.parse(payload) as IDataScienceExtraSettings; + this.widgetsCanLoadFromCDN = settings.widgetScriptSources.length > 0; + } else if (type === IPyWidgetMessages.IPyWidgets_WidgetScriptSourceResponse) { + this.registerScriptSourceInRequirejs(payload as WidgetScriptSource); + } else if ( + type === IPyWidgetMessages.IPyWidgets_kernelOptions || + type === IPyWidgetMessages.IPyWidgets_onKernelChanged + ) { + // This happens when we have restarted a kernel. + // If user changed the kernel, then some widgets might exist now and some might now. + this.widgetSourceRequests.clear(); + this.registeredWidgetSources.clear(); + } + return true; + } + }); + } + public render() { + return null; + } + public componentWillUnmount() { + this.widgetManager.dispose(); + } + /** + * Given a list of the widgets along with the sources, we will need to register them with requirejs. + * IPyWidgets uses requirejs to dynamically load modules. + * (https://requirejs.org/docs/api.html) + * All we're doing here is given a widget (module) name, we register the path where the widget (module) can be loaded from. + * E.g. + * requirejs.config({ paths:{ + * 'widget_xyz': '<Url of script without trailing .js>' + * }}); + */ + private registerScriptSourcesInRequirejs(sources: WidgetScriptSource[]) { + if (!Array.isArray(sources) || sources.length === 0) { + return; + } + + registerScripts(sources); + + // Now resolve promises (anything that was waiting for modules to get registered can carry on). + sources.forEach((source) => { + this.registeredWidgetSources.set(source.moduleName, source); + // We have fetched the script sources for all of these modules. + // In some cases we might not have the source, meaning we don't have it or couldn't find it. + let request = this.widgetSourceRequests.get(source.moduleName); + if (!request) { + request = { + deferred: createDeferred(), + timer: undefined + }; + this.widgetSourceRequests.set(source.moduleName, request); + } + request.deferred.resolve(); + if (request.timer !== undefined) { + // tslint:disable-next-line: no-any + clearTimeout(request.timer as any); // This is to make this work on Node and Browser + } + }); + } + private registerScriptSourceInRequirejs(source?: WidgetScriptSource) { + if (!source) { + return; + } + this.registerScriptSourcesInRequirejs([source]); + } + private createLoadSuccessAction( + className: string, + moduleName: string, + moduleVersion: string + ): CommonAction<LoadIPyWidgetClassLoadAction> { + return { + type: CommonActionType.LOAD_IPYWIDGET_CLASS_SUCCESS, + payload: { messageDirection: 'incoming', data: { className, moduleName, moduleVersion } } + }; + } + + private createLoadErrorAction( + className: string, + moduleName: string, + moduleVersion: string, + isOnline: boolean, + // tslint:disable-next-line: no-any + error: any, + timedout: boolean + ): CommonAction<ILoadIPyWidgetClassFailureAction> { + return { + type: CommonActionType.LOAD_IPYWIDGET_CLASS_FAILURE, + payload: { + messageDirection: 'incoming', + data: { + className, + moduleName, + moduleVersion, + isOnline, + timedout, + error, + cdnsUsed: this.widgetsCanLoadFromCDN + } + } + }; + } + private createWidgetVersionNotSupportedErrorAction( + moduleName: 'qgrid', + moduleVersion: string + ): CommonAction<NotifyIPyWidgeWidgetVersionNotSupportedAction> { + return { + type: CommonActionType.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED, + payload: { + messageDirection: 'incoming', + data: { + moduleName, + moduleVersion + } + } + }; + } + private async handleLoadError( + className: string, + moduleName: string, + moduleVersion: string, + // tslint:disable-next-line: no-any + error: any, + timedout: boolean = false + ) { + const isOnline = await isonline.default({ timeout: 1000 }); + this.props.store.dispatch( + this.createLoadErrorAction(className, moduleName, moduleVersion, isOnline, error, timedout) + ); + } + + /** + * Method called by ipywidgets to get the source for a widget. + * When we get a source for the widget, we register it in requriejs. + * We need to check if it is avaialble on CDN, if not then fallback to local FS. + * Or check local FS then fall back to CDN (depending on the order defined by the user). + */ + private loadWidgetScript(moduleName: string, moduleVersion: string): Promise<void> { + // tslint:disable-next-line: no-console + console.log(`Fetch IPyWidget source for ${moduleName}`); + let request = this.widgetSourceRequests.get(moduleName); + if (!request) { + request = { + deferred: createDeferred<void>(), + timer: undefined + }; + + // If we timeout, then resolve this promise. + // We don't want the calling code to unnecessary wait for too long. + // Else UI will not get rendered due to blocking ipywidets (at the end of the day ipywidgets gets loaded via kernel) + // And kernel blocks the UI from getting processed. + // Also, if we timeout once, then for subsequent attempts, wait for just 1 second. + // Possible user has ignored some UI prompt and things are now in a state of limbo. + // This way thigns will fall over sooner due to missing widget sources. + const timeoutTime = this.timedoutWaitingForWidgetsToGetLoaded + ? 5_000 + : this.loaderSettings.timeoutWaitingForScriptToLoad; + + request.timer = setTimeout(() => { + if (request && !request.deferred.resolved) { + // tslint:disable-next-line: no-console + console.error(`Timeout waiting to get widget source for ${moduleName}, ${moduleVersion}`); + this.handleLoadError( + '<class>', + moduleName, + moduleVersion, + new Error(`Timeout getting source for ${moduleName}:${moduleVersion}`), + true + ).ignoreErrors(); + request.deferred.resolve(); + this.timedoutWaitingForWidgetsToGetLoaded = true; + } + }, timeoutTime); + + this.widgetSourceRequests.set(moduleName, request); + } + // Whether we have the scripts or not, send message to extension. + // Useful telemetry and also we know it was explicity requestd by ipywidgest. + this.props.postOffice.sendMessage<IInteractiveWindowMapping>( + IPyWidgetMessages.IPyWidgets_WidgetScriptSourceRequest, + { moduleName, moduleVersion } + ); + + return request.deferred.promise + .then(() => { + const widgetSource = this.registeredWidgetSources.get(moduleName); + if (widgetSource) { + warnAboutWidgetVersionsThatAreNotSupported( + widgetSource, + moduleVersion, + this.widgetsCanLoadFromCDN, + (info) => + this.props.store.dispatch( + this.createWidgetVersionNotSupportedErrorAction(info.moduleName, info.moduleVersion) + ) + ); + } + }) + .catch((ex) => + // tslint:disable-next-line: no-console + console.error(`Failed to load Widget Script from Extension for for ${moduleName}, ${moduleVersion}`, ex) + ); + } + + private handleLoadSuccess(className: string, moduleName: string, moduleVersion: string) { + this.props.store.dispatch(this.createLoadSuccessAction(className, moduleName, moduleVersion)); + } +} diff --git a/src/datascience-ui/ipywidgets/incompatibleWidgetHandler.ts b/src/datascience-ui/ipywidgets/incompatibleWidgetHandler.ts new file mode 100644 index 000000000000..a6d3ce4e1207 --- /dev/null +++ b/src/datascience-ui/ipywidgets/incompatibleWidgetHandler.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as semver from 'semver'; +import { WidgetScriptSource } from '../../client/datascience/ipywidgets/types'; +const supportedVersionOfQgrid = '1.1.1'; +const qgridModuleName = 'qgrid'; + +/** + * For now only warns about qgrid. + * Warn user about qgrid versions > 1.1.1 (we know CDN isn't available for newer versions and local widget source will not work). + * Recommend to downgrade to 1.1.1. + * Returns `true` if a warning has been displayed. + */ +export function warnAboutWidgetVersionsThatAreNotSupported( + widgetSource: WidgetScriptSource, + moduleVersion: string, + cdnSupported: boolean, + errorDispatcher: (info: { moduleName: typeof qgridModuleName; moduleVersion: string }) => void +) { + // if widget exists on CDN or CDN is disabled, get out. + if (widgetSource.source === 'cdn' || !cdnSupported) { + return false; + } + // Warn about qrid. + if (widgetSource.moduleName !== qgridModuleName) { + return false; + } + // We're only interested in versions > 1.1.1. + try { + // If we have an exact version, & if that is <= 1.1.1, then no warning needs to be displayed. + if (!moduleVersion.startsWith('^') && semver.compare(moduleVersion, supportedVersionOfQgrid) <= 0) { + return false; + } + // If we have a version range, then check the range. + // Basically if our minimum version 1.1.1 is met, then nothing to do. + // Eg. requesting script source for version `^1.3.0`. + if (moduleVersion.startsWith('^') && semver.satisfies(supportedVersionOfQgrid, moduleVersion)) { + return false; + } + } catch { + return false; + } + errorDispatcher({ moduleName: widgetSource.moduleName, moduleVersion }); +} diff --git a/src/datascience-ui/ipywidgets/index.ts b/src/datascience-ui/ipywidgets/index.ts new file mode 100644 index 000000000000..6d243f31da68 --- /dev/null +++ b/src/datascience-ui/ipywidgets/index.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export { WidgetManager } from './manager'; +export { WidgetManagerComponent } from './container'; diff --git a/src/datascience-ui/ipywidgets/kernel.ts b/src/datascience-ui/ipywidgets/kernel.ts new file mode 100644 index 000000000000..c3efa52b5724 --- /dev/null +++ b/src/datascience-ui/ipywidgets/kernel.ts @@ -0,0 +1,514 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Kernel, KernelMessage, ServerConnection } from '@jupyterlab/services'; +import { DefaultKernel } from '@jupyterlab/services/lib/kernel/default'; +import type { ISignal, Signal } from '@phosphor/signaling'; +import * as WebSocketWS from 'ws'; +import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { deserializeDataViews, serializeDataViews } from '../../client/common/utils/serializers'; +import { + IInteractiveWindowMapping, + IPyWidgetMessages +} from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { KernelSocketOptions } from '../../client/datascience/types'; +import { IMessageHandler, PostOffice } from '../react-common/postOffice'; + +// tslint:disable:no-any + +// tslint:disable: no-any +// Proxy kernel that wraps the default kernel. We need this entire class because +// we can't derive from DefaultKernel. +class ProxyKernel implements IMessageHandler, Kernel.IKernel { + private readonly _ioPubMessageSignal: Signal<this, KernelMessage.IIOPubMessage>; + public get iopubMessage(): ISignal<this, KernelMessage.IIOPubMessage> { + return this._ioPubMessageSignal; + } + public get terminated() { + return this.realKernel.terminated as any; + } + public get statusChanged() { + return this.realKernel.statusChanged as any; + } + public get unhandledMessage() { + return this.realKernel.unhandledMessage as any; + } + public get anyMessage() { + return this.realKernel.anyMessage as any; + } + public get serverSettings(): ServerConnection.ISettings { + return this.realKernel.serverSettings; + } + public get id(): string { + return this.realKernel.id; + } + public get name(): string { + return this.realKernel.name; + } + public get model(): Kernel.IModel { + return this.realKernel.model; + } + public get username(): string { + return this.realKernel.username; + } + public get clientId(): string { + return this.realKernel.clientId; + } + public get status(): Kernel.Status { + return this.realKernel.status; + } + public get info(): KernelMessage.IInfoReply | null { + return this.realKernel.info; + } + public get isReady(): boolean { + return this.realKernel.isReady; + } + public get ready(): Promise<void> { + return this.realKernel.ready; + } + public get handleComms(): boolean { + return this.realKernel.handleComms; + } + public get isDisposed(): boolean { + return this.realKernel.isDisposed; + } + private realKernel: Kernel.IKernel; + private hookResults = new Map<string, boolean | PromiseLike<boolean>>(); + private websocket: WebSocketWS & { sendEnabled: boolean }; + private messageHook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>; + private messageHooks: Map<string, (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>>; + private lastHookedMessageId: string | undefined; + // Messages that are awaiting extension messages to be fully handled + private awaitingExtensionMessage: Map<string, Deferred<void>>; + constructor(options: KernelSocketOptions, private postOffice: PostOffice) { + // Dummy websocket we give to the underlying real kernel + let proxySocketInstance: any; + class ProxyWebSocket { + public onopen?: ((this: ProxyWebSocket) => any) | null; + public onmessage?: ((this: ProxyWebSocket, ev: MessageEvent) => any) | null; + public sendEnabled: boolean = true; + constructor() { + proxySocketInstance = this; + } + public close(_code?: number | undefined, _reason?: string | undefined): void { + // Nothing. + } + public send(data: string | ArrayBuffer | SharedArrayBuffer | Blob | ArrayBufferView): void { + // This is a command being sent from the UI kernel to the websocket. We mirror that to + // the extension side. + if (this.sendEnabled) { + if (typeof data === 'string') { + postOffice.sendMessage<IInteractiveWindowMapping>(IPyWidgetMessages.IPyWidgets_msg, data); + } else { + // Serialize binary data properly before sending to extension. + postOffice.sendMessage<IInteractiveWindowMapping>( + IPyWidgetMessages.IPyWidgets_binary_msg, + serializeDataViews([data as any]) + ); + } + } + } + } + const settings = ServerConnection.makeSettings({ WebSocket: ProxyWebSocket as any, wsUrl: 'BOGUS_PVSC' }); + + this.awaitingExtensionMessage = new Map<string, Deferred<void>>(); + + // This is crucial, the clientId must match the real kernel in extension. + // All messages contain the clientId as `session` in the request. + // If this doesn't match the actual value, then things can and will go wrong. + this.realKernel = new DefaultKernel( + { + name: options.model.name, + serverSettings: settings, + clientId: options.clientId, + handleComms: true, + username: options.userName + }, + options.id + ); + + // Hook up to watch iopub messages from the real kernel + // tslint:disable-next-line: no-require-imports + const signaling = require('@phosphor/signaling') as typeof import('@phosphor/signaling'); + this._ioPubMessageSignal = new signaling.Signal<this, KernelMessage.IIOPubMessage>(this); + this.realKernel.iopubMessage.connect(this.onIOPubMessage, this); + + postOffice.addHandler(this); + this.websocket = proxySocketInstance; + this.messageHook = this.messageHookInterceptor.bind(this); + this.messageHooks = new Map<string, (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>>(); + this.fakeOpenSocket(); + } + + public shutdown(): Promise<void> { + return this.realKernel.shutdown(); + } + public getSpec(): Promise<Kernel.ISpecModel> { + return this.realKernel.getSpec(); + } + public sendShellMessage<T extends KernelMessage.ShellMessageType>( + msg: KernelMessage.IShellMessage<T>, + expectReply?: boolean, + disposeOnDone?: boolean + ): Kernel.IShellFuture< + KernelMessage.IShellMessage<T>, + KernelMessage.IShellMessage<KernelMessage.ShellMessageType> + > { + return this.realKernel.sendShellMessage(msg, expectReply, disposeOnDone); + } + public sendControlMessage<T extends KernelMessage.ControlMessageType>( + msg: KernelMessage.IControlMessage<T>, + expectReply?: boolean, + disposeOnDone?: boolean + ): Kernel.IControlFuture< + KernelMessage.IControlMessage<T>, + KernelMessage.IControlMessage<KernelMessage.ControlMessageType> + > { + return this.realKernel.sendControlMessage(msg, expectReply, disposeOnDone); + } + public reconnect(): Promise<void> { + return this.realKernel.reconnect(); + } + public interrupt(): Promise<void> { + return this.realKernel.interrupt(); + } + public restart(): Promise<void> { + return this.realKernel.restart(); + } + public requestKernelInfo(): Promise<KernelMessage.IInfoReplyMsg> { + return this.realKernel.requestKernelInfo(); + } + public requestComplete(content: { code: string; cursor_pos: number }): Promise<KernelMessage.ICompleteReplyMsg> { + return this.realKernel.requestComplete(content); + } + public requestInspect(content: { + code: string; + cursor_pos: number; + detail_level: 0 | 1; + }): Promise<KernelMessage.IInspectReplyMsg> { + return this.realKernel.requestInspect(content); + } + public requestHistory( + content: + | KernelMessage.IHistoryRequestRange + | KernelMessage.IHistoryRequestSearch + | KernelMessage.IHistoryRequestTail + ): Promise<KernelMessage.IHistoryReplyMsg> { + return this.realKernel.requestHistory(content); + } + public requestExecute( + content: { + code: string; + silent?: boolean; + store_history?: boolean; + user_expressions?: import('@phosphor/coreutils').JSONObject; + allow_stdin?: boolean; + stop_on_error?: boolean; + }, + disposeOnDone?: boolean, + metadata?: import('@phosphor/coreutils').JSONObject + ): Kernel.IShellFuture<KernelMessage.IExecuteRequestMsg, KernelMessage.IExecuteReplyMsg> { + return this.realKernel.requestExecute(content, disposeOnDone, metadata); + } + public requestDebug( + // tslint:disable-next-line: no-banned-terms + content: { seq: number; type: 'request'; command: string; arguments?: any }, + disposeOnDone?: boolean + ): Kernel.IControlFuture<KernelMessage.IDebugRequestMsg, KernelMessage.IDebugReplyMsg> { + return this.realKernel.requestDebug(content, disposeOnDone); + } + public requestIsComplete(content: { code: string }): Promise<KernelMessage.IIsCompleteReplyMsg> { + return this.realKernel.requestIsComplete(content); + } + public requestCommInfo(content: { + target_name?: string; + target?: string; + }): Promise<KernelMessage.ICommInfoReplyMsg> { + return this.realKernel.requestCommInfo(content); + } + public sendInputReply(content: KernelMessage.ReplyContent<KernelMessage.IInputReply>): void { + return this.realKernel.sendInputReply(content); + } + public connectToComm(targetName: string, commId?: string): Kernel.IComm { + return this.realKernel.connectToComm(targetName, commId); + } + public registerCommTarget( + targetName: string, + callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike<void> + ): void { + // When a comm target has been registered, we need to register this in the real kernel in extension side. + // Hence send that message to extension. + this.postOffice.sendMessage<IInteractiveWindowMapping>( + IPyWidgetMessages.IPyWidgets_registerCommTarget, + targetName + ); + return this.realKernel.registerCommTarget(targetName, callback); + } + public removeCommTarget( + targetName: string, + callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike<void> + ): void { + return this.realKernel.removeCommTarget(targetName, callback); + } + public dispose(): void { + this.postOffice.removeHandler(this); + return this.realKernel.dispose(); + } + public handleMessage(type: string, payload?: any): boolean { + // Handle messages as they come in. Note: Do not await anything here. THey have to be inorder. + // If not, we could switch to message chaining or an observable instead. + switch (type) { + case IPyWidgetMessages.IPyWidgets_MessageHookCall: + this.sendHookResult(payload); + break; + + case IPyWidgetMessages.IPyWidgets_msg: + if (this.websocket && this.websocket.onmessage) { + this.websocket.onmessage({ target: this.websocket, data: payload.data, type: '' }); + } + this.sendResponse(payload.id); + break; + + case IPyWidgetMessages.IPyWidgets_binary_msg: + if (this.websocket && this.websocket.onmessage) { + const deserialized = deserializeDataViews(payload.data)![0]; + this.websocket.onmessage({ target: this.websocket, data: deserialized as any, type: '' }); + } + this.sendResponse(payload.id); + break; + + case IPyWidgetMessages.IPyWidgets_mirror_execute: + this.handleMirrorExecute(payload); + break; + + case IPyWidgetMessages.IPyWidgets_ExtensionOperationHandled: + this.extensionOperationFinished(payload); + break; + + default: + break; + } + return true; + } + public registerMessageHook( + msgId: string, + hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean> + ): void { + // We don't want to finish our processing of this message until the extension has told us that it has finished + // With the extension side registering of the message hook + const waitPromise = createDeferred<void>(); + + // A message could cause multiple callback waits, so use id+type as key + const key = this.generateExtensionResponseKey( + msgId, + IPyWidgetMessages.IPyWidgets_RegisterMessageHook.toString() + ); + this.awaitingExtensionMessage.set(key, waitPromise); + + // Tell the other side about this. + this.postOffice.sendMessage<IInteractiveWindowMapping>(IPyWidgetMessages.IPyWidgets_RegisterMessageHook, msgId); + + // Save the real hook so we can call it + this.messageHooks.set(msgId, hook); + + // Wrap the hook and send it to the real kernel + window.console.log(`Registering hook for ${msgId}`); + this.realKernel.registerMessageHook(msgId, this.messageHook); + } + + public removeMessageHook( + msgId: string, + _hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean> + ): void { + // We don't want to finish our processing of this message until the extension has told us that it has finished + // With the extension side removing of the message hook + const waitPromise = createDeferred<void>(); + + // A message could cause multiple callback waits, so use id+type as key + const key = this.generateExtensionResponseKey(msgId, IPyWidgetMessages.IPyWidgets_RemoveMessageHook.toString()); + this.awaitingExtensionMessage.set(key, waitPromise); + + this.postOffice.sendMessage<IInteractiveWindowMapping>(IPyWidgetMessages.IPyWidgets_RemoveMessageHook, { + hookMsgId: msgId, + lastHookedMsgId: this.lastHookedMessageId + }); + + // Remove our mapping + this.messageHooks.delete(msgId); + this.lastHookedMessageId = undefined; + + // Remove from the real kernel + window.console.log(`Removing hook for ${msgId}`); + this.realKernel.removeMessageHook(msgId, this.messageHook); + } + + // Called when the extension has finished an operation that we are waiting for in message processing + private extensionOperationFinished(payload: any) { + //const key = payload.id + payload.type; + const key = `${payload.id}${payload.type}`; + + const waitPromise = this.awaitingExtensionMessage.get(key); + + if (waitPromise) { + waitPromise.resolve(); + this.awaitingExtensionMessage.delete(key); + } + } + + private sendResponse(id: string) { + this.postOffice.sendMessage<IInteractiveWindowMapping>(IPyWidgetMessages.IPyWidgets_msg_received, { + id + }); + } + + private generateExtensionResponseKey(msgId: string, msgType: string): string { + return `${msgId}${msgType}`; + } + + private fakeOpenSocket() { + // This is kind of the hand shake. + // As soon as websocket opens up, the kernel sends a request to check if it is alive. + // If it gets a response, then it is deemed ready. + const originalRequestKernelInfo = this.realKernel.requestKernelInfo.bind(this.realKernel); + this.realKernel.requestKernelInfo = () => { + this.realKernel.requestKernelInfo = originalRequestKernelInfo; + return Promise.resolve() as any; + }; + if (this.websocket) { + this.websocket.onopen({ target: this.websocket }); + } + this.realKernel.requestKernelInfo = originalRequestKernelInfo; + } + private messageHookInterceptor(msg: KernelMessage.IIOPubMessage): boolean | PromiseLike<boolean> { + try { + window.console.log( + `Message hook callback for ${(msg as any).header.msg_type} and ${(msg.parent_header as any).msg_id}` + ); + // Save the active message that is currently being hooked. The Extension + // side needs this information during removeMessageHook so it can delay removal until after a message is called + this.lastHookedMessageId = msg.header.msg_id; + + const hook = this.messageHooks.get((msg.parent_header as any).msg_id); + if (hook) { + // When the kernel calls the hook, save the result for this message. The other side will ask for it + const result = hook(msg); + this.hookResults.set(msg.header.msg_id, result); + if ((result as any).then) { + return (result as any).then((r: boolean) => { + return r; + }); + } + + // When not a promise reset right after + return result; + } + } catch (ex) { + // Swallow exceptions so processing continues + } + return false; + } + + private sendHookResult(args: { requestId: string; parentId: string; msg: KernelMessage.IIOPubMessage }) { + const result = this.hookResults.get(args.msg.header.msg_id); + if (result !== undefined) { + this.hookResults.delete(args.msg.header.msg_id); + + // tslint:disable-next-line: no-any + if ((result as any).then) { + // tslint:disable-next-line: no-any + (result as any).then((r: boolean) => { + this.postOffice.sendMessage<IInteractiveWindowMapping>( + IPyWidgetMessages.IPyWidgets_MessageHookResult, + { + requestId: args.requestId, + parentId: args.parentId, + msgType: args.msg.header.msg_type, + result: r + } + ); + }); + } else { + this.postOffice.sendMessage<IInteractiveWindowMapping>(IPyWidgetMessages.IPyWidgets_MessageHookResult, { + requestId: args.requestId, + parentId: args.parentId, + msgType: args.msg.header.msg_type, + result: result === true + }); + } + } else { + // If no hook registered, make sure not to remove messages. + this.postOffice.sendMessage<IInteractiveWindowMapping>(IPyWidgetMessages.IPyWidgets_MessageHookResult, { + requestId: args.requestId, + parentId: args.parentId, + msgType: args.msg.header.msg_type, + result: true + }); + } + } + + private handleMirrorExecute(payload: { id: string; msg: KernelMessage.IExecuteRequestMsg }) { + // Special case. This is a mirrored execute. We want this to go to the real kernel, but not send a message + // back to the websocket. This should cause the appropriate futures to be generated. + try { + this.websocket.sendEnabled = false; + // Make sure we don't dispose on done (that will eliminate the future when it's done) + this.realKernel.sendShellMessage(payload.msg, false, payload.msg.content.silent); + } finally { + this.websocket.sendEnabled = true; + } + this.sendResponse(payload.id); + } + + // When the real kernel handles iopub messages notify the Extension side and then forward on the message + // Note, this message comes from the kernel after it is done handling the message async + private onIOPubMessage(_sender: Kernel.IKernel, message: KernelMessage.IIOPubMessage) { + // If we are not waiting for anything on the extension just send it + if (this.awaitingExtensionMessage.size <= 0) { + this.finishIOPubMessage(message); + } else { + // If we are waiting for something from the extension, wait for all that to finish before + // we send the message that we are done handling this message + // Since the Extension is blocking waiting for this message to be handled we know all extension message are + // related to this message or before and should be resolved before we move on + const extensionPromises = Array.from(this.awaitingExtensionMessage.values()).map((value) => { + return value.promise; + }); + Promise.all(extensionPromises) + .then(() => { + // Fine to wait and send this in the catch as the Extension is blocking new messages for this and the UI kernel + // has already finished handling it + this.finishIOPubMessage(message); + }) + .catch(() => { + window.console.log('Failed to send iopub_msg_handled message'); + }); + } + } + + // Finish an iopub message by sending a message to the UI and then emitting that we are done with it + private finishIOPubMessage(message: KernelMessage.IIOPubMessage) { + this.postOffice.sendMessage<IInteractiveWindowMapping>(IPyWidgetMessages.IPyWidgets_iopub_msg_handled, { + id: message.header.msg_id + }); + this._ioPubMessageSignal.emit(message); + } +} + +/** + * Creates a kernel from a websocket. + * Check code in `node_modules/@jupyterlab/services/lib/kernel/default.js`. + * The `_createSocket` method basically connects to a websocket and listens to messages. + * Hence to create a kernel, all we need is a socket connection (class with onMessage and postMessage methods). + */ +export function create( + options: KernelSocketOptions, + postOffice: PostOffice, + pendingMessages: { message: string; payload: any }[] +): Kernel.IKernel { + const result = new ProxyKernel(options, postOffice); + // Make sure to handle all the missed messages + pendingMessages.forEach((m) => result.handleMessage(m.message, m.payload)); + return result; +} diff --git a/src/datascience-ui/ipywidgets/manager.ts b/src/datascience-ui/ipywidgets/manager.ts new file mode 100644 index 000000000000..88d4b5a07750 --- /dev/null +++ b/src/datascience-ui/ipywidgets/manager.ts @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import '@jupyter-widgets/controls/css/labvariables.css'; + +import type { Kernel, KernelMessage } from '@jupyterlab/services'; +import type { nbformat } from '@jupyterlab/services/node_modules/@jupyterlab/coreutils'; +import { Widget } from '@phosphor/widgets'; +import * as fastDeepEqual from 'fast-deep-equal'; +import 'rxjs/add/operator/concatMap'; +import { Observable } from 'rxjs/Observable'; +import { ReplaySubject } from 'rxjs/ReplaySubject'; +import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { + IInteractiveWindowMapping, + InteractiveWindowMessages, + IPyWidgetMessages +} from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { WIDGET_MIMETYPE } from '../../client/datascience/ipywidgets/constants'; +import { KernelSocketOptions } from '../../client/datascience/types'; +import { IMessageHandler, PostOffice } from '../react-common/postOffice'; +import { create as createKernel } from './kernel'; +import { IIPyWidgetManager, IJupyterLabWidgetManager, IJupyterLabWidgetManagerCtor } from './types'; + +// tslint:disable: no-any + +export class WidgetManager implements IIPyWidgetManager, IMessageHandler { + public static get instance(): Observable<WidgetManager | undefined> { + return WidgetManager._instance; + } + private static _instance = new ReplaySubject<WidgetManager | undefined>(); + private manager?: IJupyterLabWidgetManager; + private proxyKernel?: Kernel.IKernel; + private options?: KernelSocketOptions; + private pendingMessages: { message: string; payload: any }[] = []; + /** + * Contains promises related to model_ids that need to be displayed. + * When we receive a message from the kernel of type = `display_data` for a widget (`application/vnd.jupyter.widget-view+json`), + * then its time to display this. + * We need to keep track of this. A boolean is sufficient, but we're using a promise so we can be notified when it is ready. + * + * @private + * @memberof WidgetManager + */ + private modelIdsToBeDisplayed = new Map<string, Deferred<void>>(); + constructor( + private readonly widgetContainer: HTMLElement, + private readonly postOffice: PostOffice, + private readonly scriptLoader: { + readonly widgetsRegisteredInRequireJs: Readonly<Set<string>>; + // tslint:disable-next-line: no-any + errorHandler(className: string, moduleName: string, moduleVersion: string, error: any): void; + loadWidgetScript(moduleName: string, moduleVersion: string): void; + successHandler(className: string, moduleName: string, moduleVersion: string): void; + } + ) { + // tslint:disable-next-line: no-any + this.postOffice.addHandler(this); + + // Handshake. + this.postOffice.sendMessage<IInteractiveWindowMapping>(IPyWidgetMessages.IPyWidgets_Ready); + } + public dispose(): void { + this.proxyKernel?.dispose(); // NOSONAR + this.postOffice.removeHandler(this); + this.clear().ignoreErrors(); + } + public async clear(): Promise<void> { + await this.manager?.clear_state(); + } + public handleMessage(message: string, payload?: any) { + if (message === IPyWidgetMessages.IPyWidgets_kernelOptions) { + this.initializeKernelAndWidgetManager(payload); + } else if (message === IPyWidgetMessages.IPyWidgets_onRestartKernel) { + // Kernel was restarted. + this.manager?.dispose(); // NOSONAR + this.manager = undefined; + this.proxyKernel?.dispose(); // NOSONAR + this.proxyKernel = undefined; + WidgetManager._instance.next(undefined); + } else if (!this.proxyKernel) { + this.pendingMessages.push({ message, payload }); + } + return true; + } + + /** + * Renders a widget and returns a disposable (to remove the widget). + * + * @param {(nbformat.IMimeBundle & {model_id: string; version_major: number})} data + * @param {HTMLElement} ele + * @returns {Promise<{ dispose: Function }>} + * @memberof WidgetManager + */ + public async renderWidget( + data: nbformat.IMimeBundle & { model_id: string; version_major: number }, + ele: HTMLElement + ): Promise<Widget | undefined> { + if (!data) { + throw new Error( + "application/vnd.jupyter.widget-view+json not in msg.content.data, as msg.content.data is 'undefined'." + ); + } + if (!this.manager) { + throw new Error('DS IPyWidgetManager not initialized.'); + } + + if (!data || data.version_major !== 2) { + console.warn('Widget data not avaialble to render an ipywidget'); + return undefined; + } + + const modelId = data.model_id as string; + // Check if we have processed the data for this model. + // If not wait. + if (!this.modelIdsToBeDisplayed.has(modelId)) { + this.modelIdsToBeDisplayed.set(modelId, createDeferred()); + } + // Wait until it is flagged as ready to be processed. + // This widget manager must have recieved this message and performed all operations before this. + // Once all messages prior to this have been processed in sequence and this message is receievd, + // then, and only then are we ready to render the widget. + // I.e. this is a way of synchronzing the render with the processing of the messages. + await this.modelIdsToBeDisplayed.get(modelId)!.promise; + + const modelPromise = this.manager.get_model(data.model_id); + if (!modelPromise) { + console.warn('Widget model not avaialble to render an ipywidget'); + return undefined; + } + + // ipywdigets may not have completed creating the model. + // ipywidgets have a promise, as the model may get created by a 3rd party library. + // That 3rd party library may not be available and may have to be downloaded. + // Hence the promise to wait until it has been created. + const model = await modelPromise; + const view = await this.manager.create_view(model, { el: ele }); + // tslint:disable-next-line: no-any + return this.manager.display_view(data, view, { node: ele }); + } + private initializeKernelAndWidgetManager(options: KernelSocketOptions) { + if (this.proxyKernel && fastDeepEqual(options, this.options)) { + return; + } + this.proxyKernel?.dispose(); // NOSONAR + this.proxyKernel = createKernel(options, this.postOffice, this.pendingMessages); + this.pendingMessages = []; + + // Dispose any existing managers. + this.manager?.dispose(); // NOSONAR + try { + // The JupyterLabWidgetManager will be exposed in the global variable `window.ipywidgets.main` (check webpack config - src/ipywidgets/webpack.config.js). + // tslint:disable-next-line: no-any + const JupyterLabWidgetManager = (window as any).vscIPyWidgets.WidgetManager as IJupyterLabWidgetManagerCtor; + if (!JupyterLabWidgetManager) { + throw new Error('JupyterLabWidgetManadger not defined. Please include/check ipywidgets.js file'); + } + // Create the real manager and point it at our proxy kernel. + this.manager = new JupyterLabWidgetManager(this.proxyKernel, this.widgetContainer, this.scriptLoader); + + // Listen for display data messages so we can prime the model for a display data + this.proxyKernel.iopubMessage.connect(this.handleDisplayDataMessage.bind(this)); + + // Listen for unhandled IO pub so we can forward to the extension + this.manager.onUnhandledIOPubMessage.connect(this.handleUnhanldedIOPubMessage.bind(this)); + + // Tell the observable about our new manager + WidgetManager._instance.next(this); + } catch (ex) { + // tslint:disable-next-line: no-console + console.error('Failed to initialize WidgetManager', ex); + } + } + /** + * Ensure we create the model for the display data. + */ + private handleDisplayDataMessage(_sender: any, payload: KernelMessage.IIOPubMessage) { + // tslint:disable-next-line:no-require-imports + const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); // NOSONAR + + if (!jupyterLab.KernelMessage.isDisplayDataMsg(payload)) { + return; + } + const displayMsg = payload as KernelMessage.IDisplayDataMsg; + + if (displayMsg.content && displayMsg.content.data && displayMsg.content.data[WIDGET_MIMETYPE]) { + // tslint:disable-next-line: no-any + const data = displayMsg.content.data[WIDGET_MIMETYPE] as any; + const modelId = data.model_id; + let deferred = this.modelIdsToBeDisplayed.get(modelId); + if (!deferred) { + deferred = createDeferred(); + this.modelIdsToBeDisplayed.set(modelId, deferred); + } + if (!this.manager) { + throw new Error('DS IPyWidgetManager not initialized'); + } + const modelPromise = this.manager.get_model(data.model_id); + if (modelPromise) { + modelPromise.then((_m) => deferred?.resolve()).catch((e) => deferred?.reject(e)); + } else { + deferred.resolve(); + } + } + } + + private handleUnhanldedIOPubMessage(_manager: any, msg: KernelMessage.IIOPubMessage) { + // Send this to the other side + this.postOffice.sendMessage<IInteractiveWindowMapping>( + InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage, + msg + ); + } +} diff --git a/src/datascience-ui/ipywidgets/requirejsRegistry.ts b/src/datascience-ui/ipywidgets/requirejsRegistry.ts new file mode 100644 index 000000000000..bf5c7755ccec --- /dev/null +++ b/src/datascience-ui/ipywidgets/requirejsRegistry.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { WidgetScriptSource } from '../../client/datascience/ipywidgets/types'; + +type NonPartial<T> = { + [P in keyof T]-?: T[P]; +}; + +// Key = module name, value = path to script. +const scriptsAlreadyRegisteredInRequireJs = new Map<string, string>(); + +function getScriptsToBeRegistered(scripts: WidgetScriptSource[]) { + return scripts.filter((script) => { + // Ignore scripts that have already been registered once before. + if ( + scriptsAlreadyRegisteredInRequireJs.has(script.moduleName) && + scriptsAlreadyRegisteredInRequireJs.get(script.moduleName) === script.scriptUri + ) { + return false; + } + return true; + }); +} + +function getScriptsWithAValidScriptUriToBeRegistered(scripts: WidgetScriptSource[]) { + return scripts + .filter((source) => { + if (source.scriptUri) { + // tslint:disable-next-line: no-console + console.log( + `Source for IPyWidget ${source.moduleName} found in ${source.source} @ ${source.scriptUri}.` + ); + return true; + } else { + // tslint:disable-next-line: no-console + console.error(`Source for IPyWidget ${source.moduleName} not found.`); + return false; + } + }) + .map((source) => source as NonPartial<WidgetScriptSource>); +} + +function registerScriptsInRequireJs(scripts: NonPartial<WidgetScriptSource>[]) { + // tslint:disable-next-line: no-any + const requirejs = (window as any).requirejs as { config: Function }; + if (!requirejs) { + window.console.error('Requirejs not found'); + throw new Error('Requirejs not found'); + } + const config: { paths: Record<string, string> } = { + paths: {} + }; + scripts.forEach((script) => { + scriptsAlreadyRegisteredInRequireJs.set(script.moduleName, script.scriptUri); + // Drop the `.js` from the scriptUri. + const scriptUri = script.scriptUri.toLowerCase().endsWith('.js') + ? script.scriptUri.substring(0, script.scriptUri.length - 3) + : script.scriptUri; + // Register the script source into requirejs so it gets loaded via requirejs. + config.paths[script.moduleName] = scriptUri; + }); + + requirejs.config(config); +} + +export function registerScripts(scripts: WidgetScriptSource[]) { + const scriptsToRegister = getScriptsToBeRegistered(scripts); + const validScriptsToRegister = getScriptsWithAValidScriptUriToBeRegistered(scriptsToRegister); + registerScriptsInRequireJs(validScriptsToRegister); +} diff --git a/src/datascience-ui/ipywidgets/types.ts b/src/datascience-ui/ipywidgets/types.ts new file mode 100644 index 000000000000..528eb0035d69 --- /dev/null +++ b/src/datascience-ui/ipywidgets/types.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as jupyterlab from '@jupyter-widgets/base/lib'; +import type { Kernel, KernelMessage } from '@jupyterlab/services'; +import type { nbformat } from '@jupyterlab/services/node_modules/@jupyterlab/coreutils'; +import { ISignal } from '@phosphor/signaling'; +import { Widget } from '@phosphor/widgets'; +import { IInteractiveWindowMapping } from '../../client/datascience/interactive-common/interactiveWindowTypes'; + +export interface IMessageSender { + sendMessage<M extends IInteractiveWindowMapping, T extends keyof M>(type: T, payload?: M[T]): void; +} + +export type CommTargetCallback = (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike<void>; + +export type IJupyterLabWidgetManagerCtor = new ( + kernel: Kernel.IKernelConnection, + el: HTMLElement, + scriptLoader: { + // tslint:disable-next-line: no-any + errorHandler(className: string, moduleName: string, moduleVersion: string, error: any): void; + } +) => IJupyterLabWidgetManager; + +export interface IJupyterLabWidgetManager { + /** + * Signal emitted when a view emits an IO Pub message but nothing handles it. + */ + readonly onUnhandledIOPubMessage: ISignal<this, KernelMessage.IIOPubMessage>; + dispose(): void; + /** + * Close all widgets and empty the widget state. + * @return Promise that resolves when the widget state is cleared. + */ + clear_state(): Promise<void>; + /** + * Get a promise for a model by model id. + * + * #### Notes + * If a model is not found, undefined is returned (NOT a promise). However, + * the calling code should also deal with the case where a rejected promise + * is returned, and should treat that also as a model not found. + */ + get_model(model_id: string): Promise<jupyterlab.DOMWidgetModel> | undefined; + /** + * Display a DOMWidget view. + * + */ + // tslint:disable-next-line: no-any + display_view(msg: any, view: Backbone.View<Backbone.Model>, options: any): Promise<Widget>; + /** + * Creates a promise for a view of a given model + * + * Make sure the view creation is not out of order with + * any state updates. + */ + // tslint:disable-next-line: no-any + create_view(model: jupyterlab.DOMWidgetModel, options: any): Promise<jupyterlab.DOMWidgetView>; +} + +// export interface IIPyWidgetManager extends IMessageHandler { +export interface IIPyWidgetManager { + dispose(): void; + /** + * Clears/removes all the widgets + * + * @memberof IIPyWidgetManager + */ + clear(): Promise<void>; + /** + * Displays a widget for the mesasge with header.msg_type === 'display_data'. + * The widget is rendered in a given HTML element. + * Returns a disposable that can be used to dispose/remove the rendered Widget. + * The message must + * + * @param {KernelMessage.IIOPubMessage} msg + * @param {HTMLElement} ele + * @returns {Promise<{ dispose: Function }>} + * @memberof IIPyWidgetManager + */ + renderWidget(data: nbformat.IMimeBundle, ele: HTMLElement): Promise<Widget | undefined>; +} diff --git a/src/datascience-ui/native-editor/addCellLine.tsx b/src/datascience-ui/native-editor/addCellLine.tsx new file mode 100644 index 000000000000..f5513aafecc9 --- /dev/null +++ b/src/datascience-ui/native-editor/addCellLine.tsx @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import * as React from 'react'; +import { Image, ImageName } from '../react-common/image'; +import { getLocString } from '../react-common/locReactSide'; + +interface IAddCellLineProps { + baseTheme: string; + includePlus: boolean; + className: string; + isNotebookTrusted: boolean; + click(): void; +} + +export class AddCellLine extends React.Component<IAddCellLineProps> { + constructor(props: IAddCellLineProps) { + super(props); + } + + public render() { + const className = `add-cell-line ${this.props.className}`; + const tooltip = getLocString('DataScience.insertBelow', 'Insert cell below'); + const plus = this.props.includePlus ? ( + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.InsertBelow} /> + ) : null; + const disabled = !this.props.isNotebookTrusted; + const innerFilter = disabled ? 'image-button-inner-disabled-filter' : ''; + return ( + <div className={className}> + <button + role="button" + aria-pressed="false" + title={tooltip} + disabled={disabled} + aria-label={tooltip} + className="add-cell-line-button" + onClick={this.props.click} + > + <span className={innerFilter}> + {plus} + <span className="add-cell-line-divider" /> + </span> + </button> + </div> + ); + } +} diff --git a/src/datascience-ui/native-editor/index.html b/src/datascience-ui/native-editor/index.html new file mode 100644 index 000000000000..dbc0b671d27a --- /dev/null +++ b/src/datascience-ui/native-editor/index.html @@ -0,0 +1,392 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no" /> + <meta name="theme-color" content="#000000" /> + <title>React App</title> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> + <style id="default-styles"> + :root { + --background-color: #ffffff; + --comment-color: green; + --color: #000000; + --font-family: -apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', HelveticaNeue-Light, Ubuntu, + 'Droid Sans', sans-serif; + --font-size: 13px; + --font-weight: normal; + --link-active-color: #006ab1; + --link-color: #006ab1; + --vscode-activityBar-background: #2c2c2c; + --vscode-activityBar-dropBackground: rgba(255, 255, 255, 0.12); + --vscode-activityBar-foreground: #ffffff; + --vscode-activityBar-inactiveForeground: rgba(255, 255, 255, 0.6); + --vscode-activityBarBadge-background: #007acc; + --vscode-activityBarBadge-foreground: #ffffff; + --vscode-badge-background: #c4c4c4; + --vscode-badge-foreground: #333333; + --vscode-breadcrumb-activeSelectionForeground: #4e4e4e; + --vscode-breadcrumb-background: #ffffff; + --vscode-breadcrumb-focusForeground: #4e4e4e; + --vscode-breadcrumb-foreground: rgba(97, 97, 97, 0.8); + --vscode-breadcrumbPicker-background: #f3f3f3; + --vscode-button-background: #007acc; + --vscode-button-foreground: #ffffff; + --vscode-button-hoverBackground: #0062a3; + --vscode-debugExceptionWidget-background: #f1dfde; + --vscode-debugExceptionWidget-border: #a31515; + --vscode-debugToolBar-background: #f3f3f3; + --vscode-descriptionForeground: #717171; + --vscode-diffEditor-insertedTextBackground: rgba(155, 185, 85, 0.2); + --vscode-diffEditor-removedTextBackground: rgba(255, 0, 0, 0.2); + --vscode-dropdown-background: #ffffff; + --vscode-dropdown-border: #cecece; + --vscode-editor-background: #ffffff; + --vscode-editor-findMatchBackground: #a8ac94; + --vscode-editor-findMatchHighlightBackground: rgba(234, 92, 0, 0.33); + --vscode-editor-findRangeHighlightBackground: rgba(180, 180, 180, 0.3); + --vscode-editor-font-family: -apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', + HelveticaNeue-Light, Ubuntu, 'Droid Sans', sans-serif; + --vscode-editor-font-size: 13px; + --vscode-editor-font-weight: normal; + --vscode-editor-foreground: #000000; + --vscode-editor-hoverHighlightBackground: rgba(173, 214, 255, 0.15); + --vscode-editor-inactiveSelectionBackground: #e5ebf1; + --vscode-editor-lineHighlightBorder: #eeeeee; + --vscode-editor-rangeHighlightBackground: rgba(253, 255, 0, 0.2); + --vscode-editor-selectionBackground: #add6ff; + --vscode-editor-selectionHighlightBackground: rgba(173, 214, 255, 0.3); + --vscode-editor-snippetFinalTabstopHighlightBorder: rgba(10, 50, 100, 0.5); + --vscode-editor-snippetTabstopHighlightBackground: rgba(10, 50, 100, 0.2); + --vscode-editor-wordHighlightBackground: rgba(87, 87, 87, 0.25); + --vscode-editor-wordHighlightStrongBackground: rgba(14, 99, 156, 0.25); + --vscode-editorActiveLineNumber-foreground: #0b216f; + --vscode-editorBracketMatch-background: rgba(0, 100, 0, 0.1); + --vscode-editorBracketMatch-border: #b9b9b9; + --vscode-editorCodeLens-foreground: #999999; + --vscode-editorCursor-foreground: #000000; + --vscode-editorError-foreground: #d60a0a; + --vscode-editorGroup-border: #e7e7e7; + --vscode-editorGroup-dropBackground: rgba(38, 119, 203, 0.18); + --vscode-editorGroupHeader-noTabsBackground: #ffffff; + --vscode-editorGroupHeader-tabsBackground: #f3f3f3; + --vscode-editorGutter-addedBackground: #81b88b; + --vscode-editorGutter-background: #ffffff; + --vscode-editorGutter-commentRangeForeground: #c5c5c5; + --vscode-editorGutter-deletedBackground: #ca4b51; + --vscode-editorGutter-modifiedBackground: #66afe0; + --vscode-editorHint-foreground: #6c6c6c; + --vscode-editorHoverWidget-background: #f3f3f3; + --vscode-editorHoverWidget-border: #c8c8c8; + --vscode-editorIndentGuide-activeBackground: #939393; + --vscode-editorIndentGuide-background: #d3d3d3; + --vscode-editorInfo-foreground: #008000; + --vscode-editorLineNumber-activeForeground: #0b216f; + --vscode-editorLineNumber-foreground: #237893; + --vscode-editorLink-activeForeground: #0000ff; + --vscode-editorMarkerNavigation-background: #ffffff; + --vscode-editorMarkerNavigationError-background: #d60a0a; + --vscode-editorMarkerNavigationInfo-background: #008000; + --vscode-editorMarkerNavigationWarning-background: #117711; + --vscode-editorOverviewRuler-addedForeground: rgba(0, 122, 204, 0.6); + --vscode-editorOverviewRuler-border: rgba(127, 127, 127, 0.3); + --vscode-editorOverviewRuler-bracketMatchForeground: #a0a0a0; + --vscode-editorOverviewRuler-commonContentForeground: rgba(96, 96, 96, 0.4); + --vscode-editorOverviewRuler-currentContentForeground: rgba(64, 200, 174, 0.5); + --vscode-editorOverviewRuler-deletedForeground: rgba(0, 122, 204, 0.6); + --vscode-editorOverviewRuler-errorForeground: rgba(255, 18, 18, 0.7); + --vscode-editorOverviewRuler-findMatchForeground: rgba(246, 185, 77, 0.7); + --vscode-editorOverviewRuler-incomingContentForeground: rgba(64, 166, 255, 0.5); + --vscode-editorOverviewRuler-infoForeground: rgba(18, 18, 136, 0.7); + --vscode-editorOverviewRuler-modifiedForeground: rgba(0, 122, 204, 0.6); + --vscode-editorOverviewRuler-rangeHighlightForeground: rgba(0, 122, 204, 0.6); + --vscode-editorOverviewRuler-selectionHighlightForeground: rgba(160, 160, 160, 0.8); + --vscode-editorOverviewRuler-warningForeground: rgba(18, 136, 18, 0.7); + --vscode-editorOverviewRuler-wordHighlightForeground: rgba(160, 160, 160, 0.8); + --vscode-editorOverviewRuler-wordHighlightStrongForeground: rgba(192, 160, 192, 0.8); + --vscode-editorPane-background: #ffffff; + --vscode-editorRuler-foreground: #d3d3d3; + --vscode-editorSuggestWidget-background: #f3f3f3; + --vscode-editorSuggestWidget-border: #c8c8c8; + --vscode-editorSuggestWidget-foreground: #000000; + --vscode-editorSuggestWidget-highlightForeground: #0066bf; + --vscode-editorSuggestWidget-selectedBackground: #d6ebff; + --vscode-editorUnnecessaryCode-opacity: rgba(0, 0, 0, 0.47); + --vscode-editorWarning-foreground: #117711; + --vscode-editorWhitespace-foreground: rgba(51, 51, 51, 0.2); + --vscode-editorWidget-background: #f3f3f3; + --vscode-editorWidget-border: #c8c8c8; + --vscode-errorForeground: #a1260d; + --vscode-extensionButton-prominentBackground: #327e36; + --vscode-extensionButton-prominentForeground: #ffffff; + --vscode-extensionButton-prominentHoverBackground: #28632b; + --vscode-focusBorder: rgba(0, 122, 204, 0.4); + --vscode-foreground: #616161; + --vscode-gitDecoration-addedResourceForeground: #587c0c; + --vscode-gitDecoration-conflictingResourceForeground: #6c6cc4; + --vscode-gitDecoration-deletedResourceForeground: #ad0707; + --vscode-gitDecoration-ignoredResourceForeground: #8e8e90; + --vscode-gitDecoration-modifiedResourceForeground: #895503; + --vscode-gitDecoration-submoduleResourceForeground: #1258a7; + --vscode-gitDecoration-untrackedResourceForeground: #007100; + --vscode-input-background: #ffffff; + --vscode-input-foreground: #616161; + --vscode-input-placeholderForeground: #767676; + --vscode-inputOption-activeBorder: #007acc; + --vscode-inputValidation-errorBackground: #f2dede; + --vscode-inputValidation-errorBorder: #be1100; + --vscode-inputValidation-infoBackground: #d6ecf2; + --vscode-inputValidation-infoBorder: #007acc; + --vscode-inputValidation-warningBackground: #f6f5d2; + --vscode-inputValidation-warningBorder: #b89500; + --vscode-list-activeSelectionBackground: #2477ce; + --vscode-list-activeSelectionForeground: #ffffff; + --vscode-list-dropBackground: #d6ebff; + --vscode-list-errorForeground: #b01011; + --vscode-list-focusBackground: #d6ebff; + --vscode-list-highlightForeground: #0066bf; + --vscode-list-hoverBackground: #e8e8e8; + --vscode-list-inactiveFocusBackground: #d8dae6; + --vscode-list-inactiveSelectionBackground: #e4e6f1; + --vscode-list-invalidItemForeground: #b89500; + --vscode-list-warningForeground: #117711; + --vscode-menu-background: #ffffff; + --vscode-menu-selectionBackground: #2477ce; + --vscode-menu-selectionForeground: #ffffff; + --vscode-menu-separatorBackground: #888888; + --vscode-menubar-selectionBackground: rgba(0, 0, 0, 0.1); + --vscode-menubar-selectionForeground: #333333; + --vscode-merge-commonContentBackground: rgba(96, 96, 96, 0.16); + --vscode-merge-commonHeaderBackground: rgba(96, 96, 96, 0.4); + --vscode-merge-currentContentBackground: rgba(64, 200, 174, 0.2); + --vscode-merge-currentHeaderBackground: rgba(64, 200, 174, 0.5); + --vscode-merge-incomingContentBackground: rgba(64, 166, 255, 0.2); + --vscode-merge-incomingHeaderBackground: rgba(64, 166, 255, 0.5); + --vscode-notificationCenterHeader-background: #e7e7e7; + --vscode-notificationLink-foreground: #006ab1; + --vscode-notifications-background: #f3f3f3; + --vscode-notifications-border: #e7e7e7; + --vscode-panel-background: #ffffff; + --vscode-panel-border: rgba(128, 128, 128, 0.35); + --vscode-panel-dropBackground: rgba(38, 119, 203, 0.18); + --vscode-panelTitle-activeBorder: rgba(128, 128, 128, 0.35); + --vscode-panelTitle-activeForeground: #424242; + --vscode-panelTitle-inactiveForeground: rgba(66, 66, 66, 0.75); + --vscode-peekView-border: #007acc; + --vscode-peekViewEditor-background: #f2f8fc; + --vscode-peekViewEditor-matchHighlightBackground: rgba(245, 216, 2, 0.87); + --vscode-peekViewEditorGutter-background: #f2f8fc; + --vscode-peekViewResult-background: #f3f3f3; + --vscode-peekViewResult-fileForeground: #1e1e1e; + --vscode-peekViewResult-lineForeground: #646465; + --vscode-peekViewResult-matchHighlightBackground: rgba(234, 92, 0, 0.3); + --vscode-peekViewResult-selectionBackground: rgba(51, 153, 255, 0.2); + --vscode-peekViewResult-selectionForeground: #6c6c6c; + --vscode-peekViewTitle-background: #ffffff; + --vscode-peekViewTitleDescription-foreground: rgba(108, 108, 108, 0.7); + --vscode-peekViewTitleLabel-foreground: #333333; + --vscode-pickerGroup-border: #cccedb; + --vscode-pickerGroup-foreground: #0066bf; + --vscode-progressBar-background: #0e70c0; + --vscode-scrollbar-shadow: #dddddd; + --vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6); + --vscode-scrollbarSlider-background: rgba(100, 100, 100, 0.4); + --vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7); + --vscode-settings-checkboxBackground: #ffffff; + --vscode-settings-checkboxBorder: #cecece; + --vscode-settings-dropdownBackground: #ffffff; + --vscode-settings-dropdownBorder: #cecece; + --vscode-settings-dropdownListBorder: #c8c8c8; + --vscode-settings-headerForeground: #444444; + --vscode-settings-modifiedItemIndicator: #66afe0; + --vscode-settings-numberInputBackground: #ffffff; + --vscode-settings-numberInputBorder: #cecece; + --vscode-settings-numberInputForeground: #616161; + --vscode-settings-textInputBackground: #ffffff; + --vscode-settings-textInputBorder: #cecece; + --vscode-settings-textInputForeground: #616161; + --vscode-sideBar-background: #f3f3f3; + --vscode-sideBar-dropBackground: rgba(255, 255, 255, 0.12); + --vscode-sideBarSectionHeader-background: rgba(128, 128, 128, 0.2); + --vscode-sideBarTitle-foreground: #6f6f6f; + --vscode-statusBar-background: #007acc; + --vscode-statusBar-debuggingBackground: #cc6633; + --vscode-statusBar-debuggingForeground: #ffffff; + --vscode-statusBar-foreground: #ffffff; + --vscode-statusBar-noFolderBackground: #68217a; + --vscode-statusBar-noFolderForeground: #ffffff; + --vscode-statusBarItem-activeBackground: rgba(255, 255, 255, 0.18); + --vscode-statusBarItem-hoverBackground: rgba(255, 255, 255, 0.12); + --vscode-statusBarItem-prominentBackground: #388a34; + --vscode-statusBarItem-prominentHoverBackground: #369432; + --vscode-tab-activeBackground: #ffffff; + --vscode-tab-activeForeground: #333333; + --vscode-tab-border: #f3f3f3; + --vscode-tab-inactiveBackground: #ececec; + --vscode-tab-inactiveForeground: rgba(51, 51, 51, 0.5); + --vscode-tab-unfocusedActiveForeground: rgba(51, 51, 51, 0.7); + --vscode-tab-unfocusedInactiveForeground: rgba(51, 51, 51, 0.25); + --vscode-terminal-ansiBlack: #000000; + --vscode-terminal-ansiBlue: #0451a5; + --vscode-terminal-ansiBrightBlack: #666666; + --vscode-terminal-ansiBrightBlue: #0451a5; + --vscode-terminal-ansiBrightCyan: #0598bc; + --vscode-terminal-ansiBrightGreen: #14ce14; + --vscode-terminal-ansiBrightMagenta: #bc05bc; + --vscode-terminal-ansiBrightRed: #cd3131; + --vscode-terminal-ansiBrightWhite: #a5a5a5; + --vscode-terminal-ansiBrightYellow: #b5ba00; + --vscode-terminal-ansiCyan: #0598bc; + --vscode-terminal-ansiGreen: #00bc00; + --vscode-terminal-ansiMagenta: #bc05bc; + --vscode-terminal-ansiRed: #cd3131; + --vscode-terminal-ansiWhite: #555555; + --vscode-terminal-ansiYellow: #949800; + --vscode-terminal-background: #ffffff; + --vscode-terminal-border: rgba(128, 128, 128, 0.35); + --vscode-terminal-foreground: #333333; + --vscode-terminal-selectionBackground: rgba(0, 0, 0, 0.25); + --vscode-textBlockQuote-background: rgba(127, 127, 127, 0.1); + --vscode-textBlockQuote-border: rgba(0, 122, 204, 0.5); + --vscode-textCodeBlock-background: rgba(220, 220, 220, 0.4); + --vscode-textLink-activeForeground: #006ab1; + --vscode-textLink-foreground: #006ab1; + --vscode-textPreformat-foreground: #a31515; + --vscode-textSeparator-foreground: rgba(0, 0, 0, 0.18); + --vscode-titleBar-activeBackground: #dddddd; + --vscode-titleBar-activeForeground: #333333; + --vscode-titleBar-inactiveBackground: rgba(221, 221, 221, 0.6); + --vscode-titleBar-inactiveForeground: rgba(51, 51, 51, 0.6); + --code-comment-color: black; + --vscode-widget-shadow: #a8a8a8; + } + + body { + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + font-family: var(--vscode-editor-font-family); + font-weight: var(--vscode-editor-font-weight); + font-size: var(--vscode-editor-font-size); + margin: 0; + padding: 0 20px; + } + + img { + max-width: 100%; + max-height: 100%; + } + + a { + color: var(--vscode-textLink-foreground); + } + + a:hover { + color: var(--vscode-textLink-activeForeground); + } + + a:focus, + input:focus, + select:focus, + textarea:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; + } + + code { + color: var(--vscode-textPreformat-foreground); + } + + blockquote { + background: var(--vscode-textBlockQuote-background); + border-color: var(--vscode-textBlockQuote-border); + } + + ::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + ::-webkit-scrollbar-thumb { + background-color: rgba(121, 121, 121, 0.4); + } + body.vscode-light::-webkit-scrollbar-thumb { + background-color: rgba(100, 100, 100, 0.4); + } + body.vscode-high-contrast::-webkit-scrollbar-thumb { + background-color: rgba(111, 195, 223, 0.3); + } + + ::-webkit-scrollbar-thumb:hover { + background-color: rgba(100, 100, 100, 0.7); + } + body.vscode-light::-webkit-scrollbar-thumb:hover { + background-color: rgba(100, 100, 100, 0.7); + } + body.vscode-high-contrast::-webkit-scrollbar-thumb:hover { + background-color: rgba(111, 195, 223, 0.8); + } + + ::-webkit-scrollbar-thumb:active { + background-color: rgba(85, 85, 85, 0.8); + } + body.vscode-light::-webkit-scrollbar-thumb:active { + background-color: rgba(0, 0, 0, 0.6); + } + body.vscode-high-contrast::-webkit-scrollbar-thumb:active { + background-color: rgba(111, 195, 223, 0.8); + } + </style> + </head> + <body> + <div id="root"></div> + <!-- Assumption is we'll be using a browser to load the UI, if this is the case we're debugging. Socket.io is used to push/pull messages to/from extension (post office) --> + <script src="/socket.io/socket.io.js"></script> + <script type="text/javascript"> + function resolvePath(relativePath) { + if (relativePath && relativePath[0] == '.' && relativePath[1] != '.') { + return '<%= htmlWebpackPlugin.options.imageBaseUrl %>' + relativePath.substring(1); + } + + return '<%= htmlWebpackPlugin.options.imageBaseUrl %>' + relativePath; + } + function getInitialSettings() { + return { + allowInput: true, + showCellInputCode: true, + extraSettings: { editorCursor: 'block', editorCursorBlink: 'blink' } + }; + } + // Assume that we're always using socket.io and running this in a browser. + if (io) { + var socket = io(); + var messageHandler = undefined; + let messagesReceived = []; + socket.on('fromServer', function(msg) { + if (messageHandler) { + if (messagesReceived.length > 0) { + while (messagesReceived.length) { + const data = messagesReceived.shift(); + messageHandler({ data: data }); + } + } + messageHandler({ data: msg }); + } else { + messagesReceived.push(msg); + } + }); + function acquireVsCodeApi() { + return { + postMessage: function(message) { + socket.emit('fromClient', message); + }, + handleMessage: function(handler) { + messageHandler = handler; + } + }; + } + } + </script> + <script type="text/javascript" src="require.js"></script> + <script type="text/javascript" src="ipywidgets.js"></script> + </body> +</html> diff --git a/src/datascience-ui/native-editor/index.tsx b/src/datascience-ui/native-editor/index.tsx new file mode 100644 index 000000000000..22c592903b56 --- /dev/null +++ b/src/datascience-ui/native-editor/index.tsx @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// This must be on top, do not change. Required by webpack. +import '../common/main'; +// This must be on top, do not change. Required by webpack. + +// tslint:disable-next-line: ordered-imports +import '../common/index.css'; + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; + +import { WidgetManagerComponent } from '../ipywidgets/container'; +import { IVsCodeApi, PostOffice } from '../react-common/postOffice'; +import { detectBaseTheme } from '../react-common/themeDetector'; +import { getConnectedNativeEditor } from './nativeEditor'; +import { createStore } from './redux/store'; + +// This special function talks to vscode from a web panel +export declare function acquireVsCodeApi(): IVsCodeApi; +const baseTheme = detectBaseTheme(); +// tslint:disable-next-line: no-any +const testMode = (window as any).inTestMode; +// tslint:disable-next-line: no-typeof-undefined +const skipDefault = testMode ? false : typeof acquireVsCodeApi !== 'undefined'; + +// Create the redux store +const postOffice = new PostOffice(); +const store = createStore(skipDefault, baseTheme, testMode, postOffice); + +// Wire up a connected react control for our NativeEditor +const ConnectedNativeEditor = getConnectedNativeEditor(); + +// Stick them all together +ReactDOM.render( + <Provider store={store}> + <ConnectedNativeEditor /> + <WidgetManagerComponent postOffice={postOffice} widgetContainerId={'rootWidget'} store={store} /> + </Provider>, + document.getElementById('root') as HTMLElement +); diff --git a/src/datascience-ui/native-editor/nativeCell.tsx b/src/datascience-ui/native-editor/nativeCell.tsx new file mode 100644 index 000000000000..ad367cfca7cf --- /dev/null +++ b/src/datascience-ui/native-editor/nativeCell.tsx @@ -0,0 +1,904 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import '../../client/common/extensions'; + +import { nbformat } from '@jupyterlab/coreutils'; +import * as fastDeepEqual from 'fast-deep-equal'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as React from 'react'; +import { connect } from 'react-redux'; + +import { OSType } from '../../client/common/utils/platform'; +import { + Identifiers, + NativeKeyboardCommandTelemetry, + NativeMouseCommandTelemetry +} from '../../client/datascience/constants'; +import { CellState } from '../../client/datascience/types'; +import { concatMultilineString } from '../common'; +import { CellInput } from '../interactive-common/cellInput'; +import { CellOutput } from '../interactive-common/cellOutput'; +import { ExecutionCount } from '../interactive-common/executionCount'; +import { InformationMessages } from '../interactive-common/informationMessages'; +import { activeDebugState, CursorPos, DebugState, ICellViewModel, IFont } from '../interactive-common/mainState'; +import { getOSType } from '../react-common/constants'; +import { IKeyboardEvent } from '../react-common/event'; +import { Image, ImageName } from '../react-common/image'; +import { ImageButton } from '../react-common/imageButton'; +import { getLocString } from '../react-common/locReactSide'; +import { IMonacoModelContentChangeEvent } from '../react-common/monacoHelpers'; +import { AddCellLine } from './addCellLine'; +import { actionCreators } from './redux/actions'; + +namespace CssConstants { + export const CellOutputWrapper = 'cell-output-wrapper'; + export const CellOutputWrapperClass = `.${CellOutputWrapper}`; + export const ImageButtonClass = '.image-button'; +} + +interface INativeCellBaseProps { + role?: string; + cellVM: ICellViewModel; + language: string; + + baseTheme: string; + codeTheme: string; + testMode?: boolean; + maxTextSize?: number; + enableScroll?: boolean; + monacoTheme: string | undefined; + lastCell: boolean; + firstCell: boolean; + font: IFont; + allowUndo: boolean; + gatherIsInstalled: boolean; + editorOptions: monacoEditor.editor.IEditorOptions; + themeMatplotlibPlots: boolean | undefined; + focusPending: number; + busy: boolean; + useCustomEditorApi: boolean; + runningByLine: DebugState; + supportsRunByLine: boolean; + isNotebookTrusted: boolean; +} + +type INativeCellProps = INativeCellBaseProps & typeof actionCreators; + +// tslint:disable: react-this-binding-issue +export class NativeCell extends React.Component<INativeCellProps> { + private inputRef: React.RefObject<CellInput> = React.createRef<CellInput>(); + private wrapperRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>(); + private lastKeyPressed: string | undefined; + + constructor(prop: INativeCellProps) { + super(prop); + } + + public render() { + if (this.props.cellVM.cell.data.cell_type === 'messages') { + return <InformationMessages messages={this.props.cellVM.cell.data.messages} />; + } else { + return this.renderNormalCell(); + } + } + + public componentDidUpdate(prevProps: INativeCellProps) { + if (this.props.cellVM.selected && !prevProps.cellVM.selected && !this.props.cellVM.focused) { + this.giveFocus(); + } + + // Anytime we update, reset the key. This object will be reused for different cell ids + this.lastKeyPressed = undefined; + } + + public shouldComponentUpdate(nextProps: INativeCellProps): boolean { + return !fastDeepEqual(this.props, nextProps); + } + + // Public for testing + public getUnknownMimeTypeFormatString() { + return getLocString('DataScience.unknownMimeTypeFormat', 'Unknown Mime Type'); + } + + private giveFocus() { + if (this.wrapperRef && this.wrapperRef.current) { + // Give focus to the cell if not already owning focus + if (!this.wrapperRef.current.contains(document.activeElement)) { + this.wrapperRef.current.focus(); + } + + // Scroll into view (since we have focus). However this function + // is not supported on enzyme + // tslint:disable-next-line: no-any + if ((this.wrapperRef.current as any).scrollIntoView) { + this.wrapperRef.current.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' }); + } + } + } + + private getCell = () => { + return this.props.cellVM.cell; + }; + + private isCodeCell = () => { + return this.props.cellVM.cell.data.cell_type === 'code'; + }; + + private isMarkdownCell = () => { + return this.props.cellVM.cell.data.cell_type === 'markdown'; + }; + + private isSelected = () => { + return this.props.cellVM.selected; + }; + + private isNotebookTrusted = () => { + return this.props.isNotebookTrusted; + }; + + private isFocused = () => { + return this.props.cellVM.focused; + }; + + private isError = () => { + return this.props.cellVM.cell.state === CellState.error; + }; + + private renderNormalCell() { + const cellOuterClass = this.props.cellVM.editable ? 'cell-outer-editable' : 'cell-outer'; + let cellWrapperClass = this.props.cellVM.editable ? 'cell-wrapper' : 'cell-wrapper cell-wrapper-noneditable'; + if (this.isSelected() && !this.isFocused()) { + cellWrapperClass += ' cell-wrapper-selected'; + } + if (this.isFocused()) { + cellWrapperClass += ' cell-wrapper-focused'; + } + + // Content changes based on if a markdown cell or not. + const content = + this.isMarkdownCell() && !this.isShowingMarkdownEditor() ? ( + <div className="cell-result-container"> + <div className="cell-row-container"> + {this.renderCollapseBar(false)} + {this.renderOutput()} + </div> + {this.renderAddDivider(false)} + </div> + ) : ( + <div className="cell-result-container"> + <div className="cell-row-container"> + {this.renderCollapseBar(true)} + {this.renderControls()} + {this.renderInput()} + </div> + {this.renderAddDivider(true)} + <div className="cell-row-container"> + {this.renderCollapseBar(false)} + {this.renderOutput()} + </div> + </div> + ); + + return ( + <div + className={cellWrapperClass} + role={this.props.role} + ref={this.wrapperRef} + tabIndex={0} + onKeyDown={this.onOuterKeyDown} + onClick={this.onMouseClick} + onDoubleClick={this.onMouseDoubleClick} + > + <div className={cellOuterClass}> + {this.renderNavbar()} + <div className="content-div">{content}</div> + </div> + </div> + ); + } + + private allowClickPropagation(elem: HTMLElement): boolean { + if (this.isMarkdownCell()) { + return true; + } + if (!elem.closest(CssConstants.ImageButtonClass) && !elem.closest(CssConstants.CellOutputWrapperClass)) { + return true; + } + return false; + } + + private onMouseClick = (ev: React.MouseEvent<HTMLDivElement>) => { + if (ev.nativeEvent.target) { + const elem = ev.nativeEvent.target as HTMLElement; + if (this.allowClickPropagation(elem)) { + // Not a click on an button in a toolbar or in output, select the cell. + ev.stopPropagation(); + this.lastKeyPressed = undefined; + this.props.selectCell(this.cellId); + } + } + }; + + private onMouseDoubleClick = (ev: React.MouseEvent<HTMLDivElement>) => { + const elem = ev.nativeEvent.target as HTMLElement; + if (this.allowClickPropagation(elem)) { + // When we receive double click, propagate upwards. Might change our state + ev.stopPropagation(); + this.props.focusCell(this.cellId, CursorPos.Current); + } + }; + + private shouldRenderCodeEditor = (): boolean => { + return this.isCodeCell() && (this.props.cellVM.inputBlockShow || this.props.cellVM.editable); + }; + + private shouldRenderMarkdownEditor = (): boolean => { + return ( + this.isMarkdownCell() && + (this.isShowingMarkdownEditor() || this.props.cellVM.cell.id === Identifiers.EditCellId) + ); + }; + + private isShowingMarkdownEditor = (): boolean => { + return this.isMarkdownCell() && (this.props.cellVM.focused || !this.isNotebookTrusted()); + }; + + private shouldRenderInput(): boolean { + return this.shouldRenderCodeEditor() || this.shouldRenderMarkdownEditor(); + } + + private hasOutput = () => { + return ( + this.getCell().state === CellState.finished || + this.getCell().state === CellState.error || + this.getCell().state === CellState.executing + ); + }; + + private getCodeCell = () => { + return this.props.cellVM.cell.data as nbformat.ICodeCell; + }; + + private shouldRenderOutput(): boolean { + if (!this.isNotebookTrusted()) { + return false; + } + if (this.isCodeCell()) { + const cell = this.getCodeCell(); + return ( + this.hasOutput() && + cell.outputs && + !this.props.cellVM.hideOutput && + Array.isArray(cell.outputs) && + cell.outputs.length !== 0 + ); + } else if (this.isMarkdownCell()) { + return !this.isShowingMarkdownEditor(); + } + return false; + } + + // tslint:disable-next-line: cyclomatic-complexity max-func-body-length + private keyDownInput = (cellId: string, e: IKeyboardEvent) => { + if (!this.isNotebookTrusted() && !isCellNavigationKeyboardEvent(e)) { + return; + } + const isFocusedWhenNotSuggesting = this.isFocused() && e.editorInfo && !e.editorInfo.isSuggesting; + switch (e.code) { + case 'ArrowUp': + case 'k': + if ((isFocusedWhenNotSuggesting && e.editorInfo!.isFirstLine && !e.shiftKey) || !this.isFocused()) { + this.arrowUpFromCell(e); + } + break; + case 'ArrowDown': + case 'j': + if ((isFocusedWhenNotSuggesting && e.editorInfo!.isLastLine && !e.shiftKey) || !this.isFocused()) { + this.arrowDownFromCell(e); + } + break; + case 's': + if ((e.ctrlKey && getOSType() !== OSType.OSX) || (e.metaKey && getOSType() === OSType.OSX)) { + // This is save, save our cells + this.props.save(); + } + break; + + case 'Escape': + if (isFocusedWhenNotSuggesting) { + this.escapeCell(e); + } + break; + case 'y': + if (!this.isFocused() && this.isSelected() && this.isMarkdownCell()) { + e.stopPropagation(); + e.preventDefault(); + this.props.changeCellType(cellId); + this.props.sendCommand(NativeKeyboardCommandTelemetry.ChangeToCode); + } + break; + case 'm': + if (!this.isFocused() && this.isSelected() && this.isCodeCell()) { + e.stopPropagation(); + e.preventDefault(); + this.props.changeCellType(cellId); + this.props.sendCommand(NativeKeyboardCommandTelemetry.ChangeToMarkdown); + } + break; + case 'l': + if (!this.isFocused() && this.isSelected()) { + e.stopPropagation(); + e.preventDefault(); + this.props.toggleLineNumbers(cellId); + this.props.sendCommand(NativeKeyboardCommandTelemetry.ToggleLineNumbers); + } + break; + case 'o': + if (!this.isFocused() && this.isSelected()) { + e.stopPropagation(); + e.preventDefault(); + this.props.toggleOutput(cellId); + this.props.sendCommand(NativeKeyboardCommandTelemetry.ToggleOutput); + } + break; + case 'NumpadEnter': + case 'Enter': + if (e.shiftKey) { + this.shiftEnterCell(e); + } else if (e.ctrlKey) { + this.ctrlEnterCell(e); + } else if (e.altKey) { + this.altEnterCell(e); + } else { + this.enterCell(e); + } + break; + case 'd': + if (this.lastKeyPressed === 'd' && !this.isFocused() && this.isSelected()) { + e.stopPropagation(); + this.lastKeyPressed = undefined; // Reset it so we don't keep deleting + this.props.deleteCell(cellId); + this.props.sendCommand(NativeKeyboardCommandTelemetry.DeleteCell); + } + break; + case 'a': + if (!this.isFocused()) { + e.stopPropagation(); + e.preventDefault(); + setTimeout(() => this.props.insertAbove(cellId), 1); + this.props.sendCommand(NativeKeyboardCommandTelemetry.InsertAbove); + } + break; + case 'b': + if (!this.isFocused()) { + e.stopPropagation(); + e.preventDefault(); + setTimeout(() => this.props.insertBelow(cellId), 1); + this.props.sendCommand(NativeKeyboardCommandTelemetry.InsertBelow); + } + break; + case 'z': + case 'Z': + if (!this.isFocused() && !this.props.useCustomEditorApi) { + if (e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { + e.stopPropagation(); + this.props.redo(); + this.props.sendCommand(NativeKeyboardCommandTelemetry.Redo); + } else if (!e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { + e.stopPropagation(); + this.props.undo(); + this.props.sendCommand(NativeKeyboardCommandTelemetry.Undo); + } + } + break; + default: + break; + } + + this.lastKeyPressed = e.code; + }; + + private get cellId(): string { + return this.props.cellVM.cell.id; + } + + private escapeCell = (e: IKeyboardEvent) => { + // Unfocus the current cell by giving focus to the cell itself + if (this.wrapperRef && this.wrapperRef.current && this.isFocused()) { + e.stopPropagation(); + this.wrapperRef.current.focus(); + this.props.sendCommand(NativeKeyboardCommandTelemetry.Unfocus); + } + }; + + private arrowUpFromCell = (e: IKeyboardEvent) => { + e.stopPropagation(); + e.preventDefault(); + this.props.arrowUp(this.cellId, this.getCurrentCode()); + this.props.sendCommand(NativeKeyboardCommandTelemetry.ArrowUp); + }; + + private arrowDownFromCell = (e: IKeyboardEvent) => { + e.stopPropagation(); + e.preventDefault(); + this.props.arrowDown(this.cellId, this.getCurrentCode()); + this.props.sendCommand(NativeKeyboardCommandTelemetry.ArrowDown); + }; + + private enterCell = (e: IKeyboardEvent) => { + // If focused, then ignore this call. It should go to the focused cell instead. + if (!this.isFocused() && !e.editorInfo && this.wrapperRef && this.wrapperRef && this.isSelected()) { + e.stopPropagation(); + e.preventDefault(); + this.props.focusCell(this.cellId, CursorPos.Current); + } + }; + + private shiftEnterCell = (e: IKeyboardEvent) => { + // Prevent shift enter from add an enter + e.stopPropagation(); + e.preventDefault(); + + // Submit and move to the next. + this.runAndMove(); + + this.props.sendCommand(NativeKeyboardCommandTelemetry.RunAndMove); + }; + + private altEnterCell = (e: IKeyboardEvent) => { + // Prevent shift enter from add an enter + e.stopPropagation(); + e.preventDefault(); + + // Submit this cell + this.runAndAdd(); + + this.props.sendCommand(NativeKeyboardCommandTelemetry.RunAndAdd); + }; + + private runAndMove() { + // Submit this cell + this.submitCell(this.props.lastCell ? 'add' : 'select'); + } + + private runAndAdd() { + // Submit this cell + this.submitCell('add'); + } + + private ctrlEnterCell = (e: IKeyboardEvent) => { + // Prevent shift enter from add an enter + e.stopPropagation(); + e.preventDefault(); + + // Escape the current cell if it is markdown to make it render + if (this.isMarkdownCell()) { + this.escapeCell(e); + } + + // Submit this cell + this.submitCell('none'); + this.props.sendCommand(NativeKeyboardCommandTelemetry.Run); + }; + + private submitCell = (moveOp: 'add' | 'select' | 'none') => { + this.props.executeCell(this.cellId, moveOp); + }; + + private addNewCell = () => { + setTimeout(() => this.props.insertBelow(this.cellId), 1); + this.props.sendCommand(NativeMouseCommandTelemetry.AddToEnd); + }; + private addNewCellBelow = () => { + setTimeout(() => this.props.insertBelow(this.cellId), 1); + this.props.sendCommand(NativeMouseCommandTelemetry.InsertBelow); + }; + + private renderNavbar = () => { + const moveUp = () => { + this.props.moveCellUp(this.cellId); + this.props.sendCommand(NativeMouseCommandTelemetry.MoveCellUp); + }; + const moveDown = () => { + this.props.moveCellDown(this.cellId); + this.props.sendCommand(NativeMouseCommandTelemetry.MoveCellDown); + }; + const addButtonRender = !this.props.lastCell ? ( + <div className="navbar-add-button"> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.addNewCellBelow} + disabled={!this.props.isNotebookTrusted} + tooltip={getLocString('DataScience.insertBelow', 'Insert cell below')} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.InsertBelow} /> + </ImageButton> + </div> + ) : null; + + return ( + <div className="navbar-div"> + <div> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={moveUp} + disabled={this.props.firstCell || !this.props.isNotebookTrusted} + tooltip={getLocString('DataScience.moveCellUp', 'Move cell up')} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.Up} /> + </ImageButton> + </div> + <div> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={moveDown} + disabled={this.props.lastCell || !this.props.isNotebookTrusted} + tooltip={getLocString('DataScience.moveCellDown', 'Move cell down')} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.Down} /> + </ImageButton> + </div> + {addButtonRender} + </div> + ); + }; + + private renderAddDivider = (checkOutput: boolean) => { + // Skip on the last cell + if (!this.props.lastCell) { + // Divider should only show if no output + if (!checkOutput || !this.shouldRenderOutput()) { + return ( + <AddCellLine + className="add-divider" + baseTheme={this.props.baseTheme} + includePlus={false} + isNotebookTrusted={this.props.isNotebookTrusted} + click={this.addNewCell} + /> + ); + } + } + + return null; + }; + + private getCurrentCode(): string { + // Input may not be open at this time. If not, then use current cell contents. + const contents = this.inputRef.current ? this.inputRef.current.getContents() : undefined; + return contents || concatMultilineString(this.props.cellVM.cell.data.source); + } + + private renderMiddleToolbar = () => { + const cellId = this.props.cellVM.cell.id; + const runCell = () => { + this.runAndMove(); + this.props.sendCommand(NativeMouseCommandTelemetry.Run); + }; + const gatherCell = () => { + this.props.gatherCell(cellId); + }; + const deleteCell = () => { + this.props.deleteCell(cellId); + this.props.sendCommand(NativeMouseCommandTelemetry.DeleteCell); + }; + const runbyline = () => { + this.props.focusCell(cellId); + this.props.runByLine(cellId); + }; + const stop = () => { + this.props.interruptKernel(); + }; + const step = () => { + this.props.focusCell(cellId); + this.props.step(cellId); + }; + const gatherDisabled = + this.props.cellVM.cell.data.execution_count === null || + this.props.cellVM.hasBeenRun === null || + this.props.cellVM.hasBeenRun === false || + this.props.cellVM.cell.state === CellState.executing || + this.isError() || + this.isMarkdownCell() || + !this.props.gatherIsInstalled; + const switchTooltip = + this.props.cellVM.cell.data.cell_type === 'code' + ? getLocString('DataScience.switchToMarkdown', 'Change to markdown') + : getLocString('DataScience.switchToCode', 'Change to code'); + const otherCellType = this.props.cellVM.cell.data.cell_type === 'code' ? 'markdown' : 'code'; + const otherCellTypeCommand = + otherCellType === 'markdown' + ? NativeMouseCommandTelemetry.ChangeToMarkdown + : NativeMouseCommandTelemetry.ChangeToCode; + const otherCellImage = otherCellType === 'markdown' ? ImageName.SwitchToMarkdown : ImageName.SwitchToCode; + const switchCellType = (event: React.MouseEvent<HTMLButtonElement>) => { + // Prevent this mouse click from stealing focus so that we + // can give focus to the cell input. + event.stopPropagation(); + event.preventDefault(); + this.props.changeCellType(cellId); + this.props.sendCommand(otherCellTypeCommand); + }; + const toolbarClassName = this.props.cellVM.cell.data.cell_type === 'code' ? '' : 'markdown-toolbar'; + + if (activeDebugState(this.props.runningByLine) && !this.isMarkdownCell()) { + return ( + <div className={toolbarClassName}> + <div className="native-editor-celltoolbar-middle"> + <ImageButton + className={'image-button-empty'} // Just takes up space for now + baseTheme={this.props.baseTheme} + onClick={runCell} + tooltip={getLocString('DataScience.runCell', 'Run cell')} + hidden={this.isMarkdownCell()} + disabled={true} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.Run} /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={step} + tooltip={getLocString('DataScience.step', 'Run next line (F10)')} + hidden={this.isMarkdownCell()} + disabled={this.props.busy || this.props.runningByLine === DebugState.Run} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.RunByLine} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={stop} + tooltip={getLocString('DataScience.stopRunByLine', 'Stop')} + hidden={this.isMarkdownCell()} + disabled={false} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.Interrupt} + /> + </ImageButton> + </div> + <div className="native-editor-celltoolbar-divider" /> + </div> + ); + } + return ( + <div className={toolbarClassName}> + <div className="native-editor-celltoolbar-middle"> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={runCell} + tooltip={getLocString('DataScience.runCell', 'Run cell')} + hidden={this.isMarkdownCell()} + disabled={this.props.busy || !this.props.isNotebookTrusted} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.Run} /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={runbyline} + tooltip={getLocString('DataScience.runByLine', 'Run by line')} + hidden={this.isMarkdownCell() || !this.props.supportsRunByLine} + disabled={this.props.busy || !this.props.isNotebookTrusted} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.RunByLine} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onMouseDown={switchCellType} + tooltip={switchTooltip} + disabled={!this.props.isNotebookTrusted} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={otherCellImage} /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={gatherCell} + tooltip={getLocString( + 'DataScience.gatherCell', + 'Gather the code required to generate this cell into a new notebook' + )} + hidden={gatherDisabled} + disabled={!this.props.isNotebookTrusted || this.props.cellVM.gathering} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={this.props.cellVM.gathering ? ImageName.Sync : ImageName.GatherCode} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={deleteCell} + tooltip={getLocString('DataScience.deleteCell', 'Delete cell')} + className="delete-cell-button hover-cell-button" + disabled={!this.props.isNotebookTrusted} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.Delete} /> + </ImageButton> + </div> + <div className="native-editor-celltoolbar-divider" /> + </div> + ); + }; + + private renderControls = () => { + const busy = + this.props.cellVM.cell.state === CellState.init || this.props.cellVM.cell.state === CellState.executing; + const executionCount = + this.props.cellVM && + this.props.cellVM.cell && + this.props.cellVM.cell.data && + this.props.cellVM.cell.data.execution_count + ? this.props.cellVM.cell.data.execution_count.toString() + : '-'; + + return ( + <div className="controls-div"> + <ExecutionCount isBusy={busy} count={executionCount} visible={this.isCodeCell()} /> + </div> + ); + }; + + private renderInput = () => { + if (this.shouldRenderInput()) { + // Make sure the glyph margin is always there for native cells. + // We need it for debugging. + const options = { + ...this.props.editorOptions, + glyphMargin: true + }; + return ( + <div className="cell-input-wrapper"> + {this.renderMiddleToolbar()} + <CellInput + cellVM={this.props.cellVM} + editorOptions={options} + history={undefined} + codeTheme={this.props.codeTheme} + onCodeChange={this.onCodeChange} + onCodeCreated={this.onCodeCreated} + testMode={this.props.testMode ? true : false} + showWatermark={false} + ref={this.inputRef} + monacoTheme={this.props.monacoTheme} + openLink={this.openLink} + editorMeasureClassName={undefined} + focused={this.onCodeFocused} + unfocused={this.onCodeUnfocused} + keyDown={this.keyDownInput} + showLineNumbers={this.props.cellVM.showLineNumbers} + font={this.props.font} + disableUndoStack={this.props.useCustomEditorApi} + codeVersion={this.props.cellVM.codeVersion ? this.props.cellVM.codeVersion : 1} + focusPending={this.props.focusPending} + language={this.props.language} + isNotebookTrusted={this.props.isNotebookTrusted} + /> + </div> + ); + } + return null; + }; + + private onCodeFocused = () => { + this.props.focusCell(this.cellId, CursorPos.Current); + }; + + private onCodeUnfocused = () => { + // Make sure to save the code from the editor into the cell + this.props.unfocusCell(this.cellId, this.getCurrentCode()); + }; + + private onCodeChange = (e: IMonacoModelContentChangeEvent) => { + this.props.editCell(this.getCell().id, e); + }; + + private onCodeCreated = (_code: string, _file: string, cellId: string, modelId: string) => { + this.props.codeCreated(cellId, modelId); + }; + + private renderOutput = (): JSX.Element | null => { + const themeMatplotlibPlots = this.props.themeMatplotlibPlots ? true : false; + const toolbar = this.props.cellVM.cell.data.cell_type === 'markdown' ? this.renderMiddleToolbar() : null; + if (this.shouldRenderOutput()) { + return ( + <div className={CssConstants.CellOutputWrapper}> + {toolbar} + <CellOutput + cellVM={this.props.cellVM} + baseTheme={this.props.baseTheme} + expandImage={this.props.showPlot} + maxTextSize={this.props.maxTextSize} + enableScroll={this.props.enableScroll} + themeMatplotlibPlots={themeMatplotlibPlots} + widgetFailed={this.props.widgetFailed} + openSettings={this.props.openSettings} + /> + </div> + ); + } + return null; + }; + + private onOuterKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => { + // Handle keydown events for the entire cell when we don't have focus + if (event.key !== 'Tab' && !this.isFocused() && !this.focusInOutput()) { + this.keyDownInput(this.props.cellVM.cell.id, { + code: event.key, + shiftKey: event.shiftKey, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + altKey: event.altKey, + target: event.target as HTMLDivElement, + stopPropagation: () => event.stopPropagation(), + preventDefault: () => event.preventDefault() + }); + } + }; + + private focusInOutput(): boolean { + const focusedElement = document.activeElement as HTMLElement; + if (focusedElement) { + return focusedElement.closest(CssConstants.CellOutputWrapperClass) !== null; + } + return false; + } + + private renderCollapseBar = (input: boolean) => { + let classes = 'collapse-bar'; + + if (this.isSelected() && !this.isFocused()) { + classes += ' collapse-bar-selected'; + } + if (this.isFocused()) { + classes += ' collapse-bar-focused'; + } + + if (input) { + return <div className={classes}></div>; + } + + if (this.props.cellVM.cell.data.cell_type === 'markdown') { + classes += ' collapse-bar-markdown'; + } else if ( + Array.isArray(this.props.cellVM.cell.data.outputs) && + this.props.cellVM.cell.data.outputs.length !== 0 + ) { + classes += ' collapse-bar-output'; + } else { + return null; + } + + return <div className={classes}></div>; + }; + + private openLink = (uri: monacoEditor.Uri) => { + this.props.linkClick(uri.toString()); + }; +} + +// Main export, return a redux connected editor +export function getConnectedNativeCell() { + return connect(null, actionCreators)(NativeCell); +} + +function isCellNavigationKeyboardEvent(e: IKeyboardEvent) { + return ( + ((e.code === 'Enter' || e.code === 'NumpadEnter') && !e.shiftKey && !e.ctrlKey && !e.altKey) || + e.code === 'ArrowUp' || + e.code === 'k' || + e.code === 'ArrowDown' || + e.code === 'j' || + e.code === 'Escape' + ); +} diff --git a/src/datascience-ui/native-editor/nativeEditor.less b/src/datascience-ui/native-editor/nativeEditor.less new file mode 100644 index 000000000000..ac52ff444f02 --- /dev/null +++ b/src/datascience-ui/native-editor/nativeEditor.less @@ -0,0 +1,494 @@ +/* Import common styles and then override them below */ +@import '../interactive-common/common.css'; + +.toolbar-panel-button { + border-width: 1px; + border-style: solid; + border-color: var(--override-badge-background, var(--vscode-badge-background)); + color: var(--override-foreground, var(--vscode-editor-foreground)); + text-align: center; + overflow: hidden; + margin-left: 2px; + padding: 2px; + background-color: transparent; + cursor: hand; +} + +.cell-wrapper { + margin: 2px 2px 0px 0px; + position: relative; + min-height: 55px; +} + +.cell-wrapper-focused { + margin: 2px 2px 0px 0px; +} + +.cell-wrapper-selected { + margin: 2px 2px 0px 0px; +} + +.cell-menu-bar-outer { + justify-self: right; +} + +.cell-output-wrapper { + grid-row: 1; + grid-column: 3; +} + +.cell-output { + margin-top: 5px; + background: transparent; + width: 100%; + overflow-x: scroll; +} + +.cell-output-text { + white-space: pre-wrap; + word-break: break-all; + overflow-x: hidden; +} + +.markdown-cell-output-container { + grid-row: 1; + grid-column: 3; +} + +.markdown-cell-output { + width: 100%; + overflow-x: scroll; +} + +.cell-outer { + display: grid; + grid-template-columns: auto auto minmax(0, 1fr); +} + +.cell-outer-editable { + display: grid; + grid-template-columns: auto auto minmax(0, 1fr); + margin-top: 0px; +} + +.cell-state-selector { + border-width: 1px; + border-style: solid; + border-color: var(--override-badge-background, var(--vscode-badge-background)); + color: var(--override-foreground, var(--vscode-editor-foreground)); + text-align: center; + overflow: hidden; + margin-left: 2px; + padding: 2px; + background-color: transparent; + cursor: hand; +} + +.cell-state-selector-option { + border-width: 1px; + border-style: solid; + border-color: var(--override-badge-background, var(--vscode-badge-background)); + color: var(--override-foreground, var(--vscode-editor-foreground)); + background-color: var(--override-background, var(--vscode-editor-background)); +} + +.code-area { + position: relative; + width: 100%; + padding-right: 8px; + margin-bottom: 0px; + padding-left: 2px; + padding-top: 2px; + background: var(--override-widget-background, var(--vscode-notifications-background)); +} + +.markdown-editor-area { + position: relative; + width: 100%; + padding-right: 8px; + margin-bottom: 0px; + padding-left: 2px; + padding-top: 2px; + background: var(--override-widget-background, var(--vscode-notifications-background)); +} + +.code-watermark { + top: 5px; /* Account for extra padding and border in native editor */ +} + +.cell-input-wrapper { + grid-column: 3; + grid-row: 1; +} + +.cell-input { + margin: 2px 10px 0px 0px; +} + +.content-div { + grid-column: 3; +} + +.controls-div { + min-width: 34px; + padding-left: 4px; + padding-right: 4px; + display: block; + grid-column: 2; + grid-row: 1; +} + +.navbar-div { + grid-column: 1; + visibility: hidden; + display: grid; + grid-template-rows: var(--button-size) var(--button-size) auto; +} + +.navbar-add-button { + align-self: end; +} + +.execution-count { + justify-self: end; + margin-bottom: 10px; + margin-top: 1px; +} + +.execution-count-busy-outer { + justify-self: end; +} + +.native-editor-celltoolbar-inner { + justify-self: center; + grid-column: 1; +} + +.native-editor-celltoolbar-middle { + display: flex; + grid-column: 3; + grid-row: 2; + justify-items: left; + background: var(--vscode-notifications-background); +} + +.native-editor-celltoolbar-divider { + background-color: var(--vscode-badge-background); + height: 2px; +} + +.code-toolbar { + visibility: visible; +} + +.markdown-toolbar { + visibility: collapse; +} + +.hover-cell-button { + visibility: collapse; +} + +.cell-wrapper:hover .hover-cell-button { + visibility: visible; +} + +.cell-wrapper-selected .hover-cell-button { + visibility: visible; +} + +.cell-wrapper-focused .hover-cell-button { + visibility: visible; +} + +.delete-cell-button { + right: 2px; + position: absolute; + visibility: collapse; +} + +.cell-wrapper:hover .navbar-div { + visibility: visible; +} + +.cell-wrapper-selected .navbar-div { + visibility: visible; +} + +.cell-wrapper-focused .navbar-div { + visibility: visible; +} + +.cell-wrapper:hover .markdown-toolbar { + visibility: visible; +} + +.cell-wrapper-selected .markdown-toolbar { + visibility: visible; +} + +.cell-wrapper-focused .markdown-toolbar { + visibility: visible; +} + +// .cell-wrapper:hover .native-editor-celltoolbar-middle { +// visibility: visible; +// } + +// .cell-wrapper-selected .native-editor-celltoolbar-middle { +// visibility: visible; +// } + +// .cell-wrapper-focused .native-editor-celltoolbar-middle { +// visibility: visible; +// } + +.native-editor-flyout-button { + width: auto; + height: auto; + border-color: transparent; + background-color: transparent; + padding: 0px; + margin-left: 4px; + margin-right: 0px; + margin-top: 0px; + margin-bottom: 0px; + border-width: 0px; +} + +.native-editor-flyout-button:focus { + outline: none; +} + +.native-editor-cellflyout { + position: relative; + left: 20px; + top: -15px; + width: auto; + height: auto; + padding-top: 2px; + padding-right: 2px; + z-index: 100; +} + +.native-editor-cellflyout-selected { + background-color: var(--vscode-peekView-border); +} + +.native-editor-cellflyout-focused { + background-color: var(--vscode-editorInfo-foreground); +} + +.flyout-button-content { + color: var(--override-foreground, var(--vscode-editor-foreground)); +} + +.native-button { + background: transparent; + z-index: 10; +} + +#toolbar-panel { + margin-top: 2px; + margin-bottom: 2px; + margin-left: 0px; + margin-right: 0px; +} + +#content-panel-div { + overflow: hidden; +} + +/* Fix image buttons that are supposed to be hidden from showing up */ +.flyout-children-hidden .image-button { + width: 0px; + height: 0px; + margin-left: 0px; + padding: 0px; +} + +.add-cell-line { + display: flex; + justify-content: left; + margin-top: 5px; + margin-bottom: 0px; + margin-left: 5px; + margin-right: 5px; +} + +.add-cell-line:focus-within { + outline: 1px solid black; +} + +.add-cell-line-top { + margin-top: 2px; + margin-bottom: 0px; +} + +.add-cell-line-top-force-visible { + margin-top: 2px; + margin-bottom: 0px; +} + +.add-cell-line-top .add-cell-line-button { + visibility: hidden; +} + +.add-cell-line-button { + border-width: 0px; + border-style: solid; + text-align: center; + line-height: 16px; + background-color: transparent; + cursor: hand; + height: var(--button-size); + padding: 0px; + display: flex; +} + +.add-cell-line-button:focus { + outline: none; +} + +.add-cell-line-top:hover .add-cell-line-button { + visibility: visible; +} + +.add-cell-line-button .image-button-image { + height: var(--button-size); +} + +.add-cell-line-button .image-button-image svg { + height: var(--button-size); +} + +.add-cell-line-divider { + margin-top: 8px; + margin-left: 2px; + width: calc(100% - 40px); + border-width: 0px; + border-top-color: var(--override-badge-background, var(--vscode-badge-background)); + border-top-width: 1px; + border-style: solid; +} + +.add-cell-line-divider:hover { + cursor: pointer; +} + +.cell-wrapper-selected .add-cell-line { + visibility: visible; +} + +.cell-wrapper-focused .add-cell-line { + visibility: visible; +} + +/* +Cell Row Container layout +-------------------------- +collapse-bar controls-div [cell-input, cell-output, markdown-cell-output-container] +(expanded c-bar) celltoolbar-middle +*/ +.cell-row-container { + display: grid; + grid-template-columns: auto auto minmax(0, 1fr); + grid-template-rows: 1fr auto; +} + +.collapse-bar { + grid-column: 1; + grid-row-start: 1; + grid-row-end: 2; + background-color: transparent; + max-width: 8px; + min-width: 8px; +} + +.cell-wrapper:hover .collapse-bar { + background-color: var(--override-widget-background, var(--vscode-notifications-background)); +} + +.collapse-bar-markdown { + margin: 0px 44px 0px 0px; +} + +.collapse-bar-output { + margin: 0px 28px 0px 16px; +} + +.collapse-bar-selected { + background-color: var(--vscode-peekView-border); + grid-row-start: 1; + grid-row-end: 3; +} + +.collapse-bar-focused { + background: repeating-linear-gradient( + -45deg, + transparent, + transparent 3px, + var(--vscode-editorGutter-addedBackground) 3px, + var(--vscode-editorGutter-addedBackground) 6px + ); + grid-row-start: 1; + grid-row-end: 3; +} + +.cell-wrapper:hover .collapse-bar-selected { + background-color: var(--vscode-peekView-border); +} + +.cell-wrapper:hover .collapse-bar-focused { + background: repeating-linear-gradient( + -45deg, + transparent, + transparent 3px, + var(--vscode-editorGutter-addedBackground) 3px, + var(--vscode-editorGutter-addedBackground) 6px + ); +} + +.add-divider { + visibility: hidden; + margin: 0px; + position: absolute; + bottom: 8px; +} + +.cell-wrapper:hover .add-divider { + visibility: hidden; + z-index: -100; + pointer-events: none; +} + +.cell-wrapper-selected .add-divider { + visibility: hidden; + z-index: -100; + pointer-events: none; +} + +.cell-wrapper-focused .add-divider { + visibility: hidden; + z-index: -100; + pointer-events: none; +} + +.cell-wrapper-selected:hover .add-divider { + visibility: hidden; + z-index: -100; + pointer-events: none; +} + +.cell-wrapper-focused:hover .add-divider { + visibility: hidden; + z-index: -100; + pointer-events: none; +} + +.native-editor-celltoolbar-middle .image-button { + margin-right: 3px; + margin-top: 4px; + margin-bottom: 4px; + margin-left: 3px; +} diff --git a/src/datascience-ui/native-editor/nativeEditor.tsx b/src/datascience-ui/native-editor/nativeEditor.tsx new file mode 100644 index 000000000000..96b1bd0fcd4d --- /dev/null +++ b/src/datascience-ui/native-editor/nativeEditor.tsx @@ -0,0 +1,397 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { OSType } from '../../client/common/utils/platform'; +import { NativeKeyboardCommandTelemetry, NativeMouseCommandTelemetry } from '../../client/datascience/constants'; +import { buildSettingsCss } from '../interactive-common/buildSettingsCss'; +import { ContentPanel, IContentPanelProps } from '../interactive-common/contentPanel'; +import { handleLinkClick } from '../interactive-common/handlers'; +import { + activeDebugState, + DebugState, + getSelectedAndFocusedInfo, + ICellViewModel, + IMainState +} from '../interactive-common/mainState'; +import { IMainWithVariables, IStore } from '../interactive-common/redux/store'; +import { IVariablePanelProps, VariablePanel } from '../interactive-common/variablePanel'; +import { getOSType } from '../react-common/constants'; +import { ErrorBoundary } from '../react-common/errorBoundary'; +import { getLocString } from '../react-common/locReactSide'; +import { Progress } from '../react-common/progress'; +import { AddCellLine } from './addCellLine'; +import { getConnectedNativeCell } from './nativeCell'; +import './nativeEditor.less'; +import { actionCreators } from './redux/actions'; +import { ToolbarComponent } from './toolbar'; + +type INativeEditorProps = IMainWithVariables & typeof actionCreators; + +function mapStateToProps(state: IStore): IMainWithVariables { + return { ...state.main, variableState: state.variables }; +} + +const ConnectedNativeCell = getConnectedNativeCell(); + +export class NativeEditor extends React.Component<INativeEditorProps> { + private renderCount: number = 0; + private waitingForLoadRender = true; + private mainPanelToolbarRef: React.RefObject<HTMLDivElement> = React.createRef(); + + constructor(props: INativeEditorProps) { + super(props); + this.insertAboveFirst = this.insertAboveFirst.bind(this); + } + + public componentDidMount() { + this.props.editorLoaded(); + window.addEventListener('keydown', this.mainKeyDown); + window.addEventListener('resize', () => this.forceUpdate(), true); + document.addEventListener('click', this.linkClick, true); + } + + public componentWillUnmount() { + window.removeEventListener('keydown', this.mainKeyDown); + window.removeEventListener('resize', () => this.forceUpdate()); + document.removeEventListener('click', this.linkClick); + this.props.editorUnmounted(); + } + + public componentDidUpdate(prevProps: IMainState) { + if (this.props.loaded && !prevProps.loaded && this.waitingForLoadRender) { + this.waitingForLoadRender = false; + // After this render is complete (see this SO) + // https://stackoverflow.com/questions/26556436/react-after-render-code, + // indicate we are done loading. We want to wait for the render + // so we get accurate timing on first launch. + setTimeout(() => { + window.requestAnimationFrame(() => { + this.props.loadedAllCells(); + }); + }); + } + } + + public render() { + const dynamicFont: React.CSSProperties = { + fontSize: this.props.font.size, + fontFamily: this.props.font.family + }; + + // If in test mode, update our count. Use this to determine how many renders a normal update takes. + if (this.props.testMode) { + this.renderCount = this.renderCount + 1; + } + + // Update the state controller with our new state + const progressBar = (this.props.busy || !this.props.loaded) && !this.props.testMode ? <Progress /> : undefined; + const addCellLine = + this.props.cellVMs.length === 0 ? null : ( + <AddCellLine + includePlus={true} + className="add-cell-line-top" + click={this.insertAboveFirst} + baseTheme={this.props.baseTheme} + isNotebookTrusted={this.props.isNotebookTrusted} + /> + ); + + return ( + <div id="main-panel" role="Main" style={dynamicFont}> + <div className="styleSetter"> + <style>{`${this.props.rootCss ? this.props.rootCss : ''} +${buildSettingsCss(this.props.settings)}`}</style> + </div> + <header ref={this.mainPanelToolbarRef} id="main-panel-toolbar"> + {this.renderToolbarPanel()} + {progressBar} + </header> + <section + id="main-panel-variable" + aria-label={getLocString('DataScience.collapseVariableExplorerLabel', 'Variables')} + > + {this.renderVariablePanel(this.props.baseTheme)} + </section> + <main id="main-panel-content"> + {addCellLine} + {this.renderContentPanel(this.props.baseTheme)} + </main> + </div> + ); + } + + private insertAboveFirst() { + setTimeout(() => this.props.insertAboveFirst(), 1); + } + private renderToolbarPanel() { + return <ToolbarComponent isNotebookTrusted={this.props.isNotebookTrusted}></ToolbarComponent>; + } + + private renderVariablePanel(baseTheme: string) { + if (this.props.variableState.visible) { + const variableProps = this.getVariableProps(baseTheme); + return <VariablePanel {...variableProps} />; + } + + return null; + } + + private renderContentPanel(baseTheme: string) { + // Skip if the tokenizer isn't finished yet. It needs + // to finish loading so our code editors work. + if (!this.props.monacoReady && !this.props.testMode) { + return null; + } + + // Otherwise render our cells. + const contentProps = this.getContentProps(baseTheme); + return <ContentPanel {...contentProps} />; + } + + private getContentProps = (baseTheme: string): IContentPanelProps => { + return { + baseTheme: baseTheme, + cellVMs: this.props.cellVMs, + testMode: this.props.testMode, + codeTheme: this.props.codeTheme, + submittedText: this.props.submittedText, + skipNextScroll: this.props.skipNextScroll ? true : false, + editable: true, + renderCell: this.renderCell, + scrollToBottom: this.scrollDiv, + scrollBeyondLastLine: this.props.settings + ? this.props.settings.extraSettings.editor.scrollBeyondLastLine + : false + }; + }; + private getVariableProps = (baseTheme: string): IVariablePanelProps => { + let toolbarHeight = 0; + if (this.mainPanelToolbarRef.current) { + toolbarHeight = this.mainPanelToolbarRef.current.offsetHeight; + } + return { + variables: this.props.variableState.variables, + containerHeight: this.props.variableState.containerHeight, + gridHeight: this.props.variableState.gridHeight, + debugging: this.props.debugging, + busy: this.props.busy, + showDataExplorer: this.props.showDataViewer, + skipDefault: this.props.skipDefault, + testMode: this.props.testMode, + closeVariableExplorer: this.props.toggleVariableExplorer, + setVariableExplorerHeight: this.props.setVariableExplorerHeight, + baseTheme: baseTheme, + pageIn: this.pageInVariableData, + fontSize: this.props.font.size, + executionCount: this.props.currentExecutionCount, + refreshCount: this.props.variableState.refreshCount, + offsetHeight: toolbarHeight, + supportsDebugging: + this.props.settings && this.props.settings.variableOptions + ? this.props.settings.variableOptions.enableDuringDebugger + : false + }; + }; + + private pageInVariableData = (startIndex: number, pageSize: number) => { + this.props.getVariableData( + this.props.currentExecutionCount, + this.props.variableState.refreshCount, + startIndex, + pageSize + ); + }; + + private isNotebookTrusted = () => { + return this.props.isNotebookTrusted; + }; + + // tslint:disable-next-line: cyclomatic-complexity + private mainKeyDown = (event: KeyboardEvent) => { + if (!this.isNotebookTrusted()) { + return; // Disable keyboard interaction with untrusted notebooks + } + // Handler for key down presses in the main panel + switch (event.key) { + // tslint:disable-next-line: no-suspicious-comment + // TODO: How to have this work for when the keyboard shortcuts are changed? + case 's': { + if (!this.props.settings?.extraSettings.useCustomEditorApi) { + if ( + (event.ctrlKey && getOSType() !== OSType.OSX) || + (event.metaKey && getOSType() === OSType.OSX) + ) { + // This is save, save our cells + this.props.save(); + this.props.sendCommand(NativeKeyboardCommandTelemetry.Save); + } + } + break; + } + case 'z': + case 'Z': + if ( + !getSelectedAndFocusedInfo(this.props).focusedCellId && + !this.props.settings?.extraSettings.useCustomEditorApi + ) { + if (event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) { + event.stopPropagation(); + this.props.redo(); + this.props.sendCommand(NativeKeyboardCommandTelemetry.Redo); + } else if (!event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) { + event.stopPropagation(); + this.props.undo(); + this.props.sendCommand(NativeKeyboardCommandTelemetry.Undo); + } + } + break; + + case 'F10': + if (this.props.debugging) { + // Only allow step if debugging in break mode + const debuggingCell = this.props.cellVMs.find((cvm) => cvm.runningByLine === DebugState.Break); + if (debuggingCell) { + this.props.step(debuggingCell.cell.id); + } + event.stopPropagation(); + } else { + // Otherwise if not debugging, run by line the current focused cell + const focusedCell = getSelectedAndFocusedInfo(this.props).focusedCellId; + if (focusedCell) { + this.props.runByLine(focusedCell); + } + } + break; + case 'F5': + if (this.props.debugging) { + // Only allow continue if debugging in break mode + const debuggingCell = this.props.cellVMs.find((cvm) => cvm.runningByLine === DebugState.Break); + if (debuggingCell) { + this.props.continue(debuggingCell.cell.id); + } + event.stopPropagation(); + } + break; + default: + break; + } + }; + + // private copyToClipboard = (cellId: string) => { + // const cell = this.props.findCell(cellId); + // if (cell) { + // // Need to do this in this process so it copies to the user's clipboard and not + // // the remote clipboard where the extension is running + // const textArea = document.createElement('textarea'); + // textArea.value = concatMultilineString(cell.cell.data.source); + // document.body.appendChild(textArea); + // textArea.select(); + // document.execCommand('Copy'); + // textArea.remove(); + // } + // } + + // private pasteFromClipboard = (cellId: string) => { + // const editedCells = this.props.cellVMs; + // const index = editedCells.findIndex(x => x.cell.id === cellId) + 1; + + // if (index > -1) { + // const textArea = document.createElement('textarea'); + // document.body.appendChild(textArea); + // textArea.select(); + // document.execCommand('Paste'); + // editedCells[index].cell.data.source = textArea.value.split(/\r?\n/); + // textArea.remove(); + // } + + // this.setState({ + // cellVMs: editedCells + // }); + // } + + private renderCell = (cellVM: ICellViewModel, index: number): JSX.Element | null => { + // Don't render until we have settings + if (!this.props.settings || !this.props.editorOptions) { + return null; + } + const addNewCell = () => { + setTimeout(() => this.props.insertBelow(cellVM.cell.id), 1); + this.props.sendCommand(NativeMouseCommandTelemetry.AddToEnd); + }; + const firstLine = index === 0; + const lastLine = + index === this.props.cellVMs.length - 1 ? ( + <AddCellLine + includePlus={true} + baseTheme={this.props.baseTheme} + className="add-cell-line-cell" + click={addNewCell} + isNotebookTrusted={this.props.isNotebookTrusted} + /> + ) : null; + + const otherCellRunningByLine = this.props.cellVMs.find( + (cvm) => activeDebugState(cvm.runningByLine) && cvm.cell.id !== cellVM.cell.id + ); + const maxOutputSize = this.props.settings.maxOutputSize; + const outputSizeLimit = 10000; + const maxTextSize = + maxOutputSize && maxOutputSize < outputSizeLimit && maxOutputSize > 0 + ? maxOutputSize + : this.props.settings.enableScrollingForCellOutputs + ? 400 + : undefined; + + return ( + <div key={cellVM.cell.id} id={cellVM.cell.id}> + <ErrorBoundary> + <ConnectedNativeCell + role="listitem" + maxTextSize={maxTextSize} + enableScroll={this.props.settings.enableScrollingForCellOutputs} + testMode={this.props.testMode} + cellVM={cellVM} + baseTheme={this.props.baseTheme} + codeTheme={this.props.codeTheme} + monacoTheme={this.props.monacoTheme} + lastCell={lastLine !== null} + firstCell={firstLine} + font={this.props.font} + allowUndo={this.props.undoStack.length > 0} + editorOptions={this.props.editorOptions} + gatherIsInstalled={this.props.settings.gatherIsInstalled} + themeMatplotlibPlots={this.props.settings.themeMatplotlibPlots} + // Focus pending does not apply to native editor. + focusPending={0} + busy={this.props.busy} + useCustomEditorApi={this.props.settings?.extraSettings.useCustomEditorApi} + runningByLine={cellVM.runningByLine} + supportsRunByLine={ + this.props.settings?.variableOptions?.enableDuringDebugger + ? otherCellRunningByLine === undefined + : false + } + language={this.props.kernel.language} + isNotebookTrusted={this.props.isNotebookTrusted} + /> + </ErrorBoundary> + {lastLine} + </div> + ); + }; + + private scrollDiv = (_div: HTMLDivElement) => { + // Doing nothing for now. This should be implemented once redux refactor is done. + }; + + private linkClick = (ev: MouseEvent) => { + handleLinkClick(ev, this.props.linkClick); + }; +} + +// Main export, return a redux connected editor +export function getConnectedNativeEditor() { + return connect(mapStateToProps, actionCreators)(NativeEditor); +} diff --git a/src/datascience-ui/native-editor/redux/actions.ts b/src/datascience-ui/native-editor/redux/actions.ts new file mode 100644 index 000000000000..b1c83818a5d4 --- /dev/null +++ b/src/datascience-ui/native-editor/redux/actions.ts @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as uuid from 'uuid/v4'; +import { NativeKeyboardCommandTelemetry, NativeMouseCommandTelemetry } from '../../../client/datascience/constants'; +import { + IInteractiveWindowMapping, + InteractiveWindowMessages +} from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { IJupyterVariable, IJupyterVariablesRequest } from '../../../client/datascience/types'; +import { CursorPos } from '../../interactive-common/mainState'; +import { + CommonAction, + CommonActionType, + CommonActionTypeMapping, + ICellAction, + ICellAndCursorAction, + ICodeAction, + ICodeCreatedAction, + IEditCellAction, + ILinkClickAction, + IOpenSettingsAction, + ISendCommandAction, + IShowDataViewerAction, + IVariableExplorerHeight +} from '../../interactive-common/redux/reducers/types'; +import { IMonacoModelContentChangeEvent } from '../../react-common/monacoHelpers'; + +// This function isn't made common and not exported, to ensure it isn't used elsewhere. +function createIncomingActionWithPayload< + M extends IInteractiveWindowMapping & CommonActionTypeMapping, + K extends keyof M +>(type: K, data: M[K]): CommonAction<M[K]> { + // tslint:disable-next-line: no-any + return { type, payload: { data, messageDirection: 'incoming' } as any } as any; +} +// This function isn't made common and not exported, to ensure it isn't used elsewhere. +function createIncomingAction(type: CommonActionType | InteractiveWindowMessages): CommonAction { + return { type, payload: { messageDirection: 'incoming', data: undefined } }; +} + +// See https://react-redux.js.org/using-react-redux/connect-mapdispatch#defining-mapdispatchtoprops-as-an-object +export const actionCreators = { + addCell: () => createIncomingActionWithPayload(CommonActionType.ADD_AND_FOCUS_NEW_CELL, { newCellId: uuid() }), + insertAboveFirst: () => + createIncomingActionWithPayload(CommonActionType.INSERT_ABOVE_FIRST_AND_FOCUS_NEW_CELL, { newCellId: uuid() }), + insertAbove: (cellId: string | undefined) => + createIncomingActionWithPayload(CommonActionType.INSERT_ABOVE_AND_FOCUS_NEW_CELL, { + cellId, + newCellId: uuid() + }), + insertBelow: (cellId: string | undefined) => + createIncomingActionWithPayload(CommonActionType.INSERT_BELOW_AND_FOCUS_NEW_CELL, { + cellId, + newCellId: uuid() + }), + executeCell: (cellId: string, moveOp: 'add' | 'select' | 'none') => + createIncomingActionWithPayload(CommonActionType.EXECUTE_CELL_AND_ADVANCE, { cellId, moveOp }), + focusCell: (cellId: string, cursorPos: CursorPos = CursorPos.Current): CommonAction<ICellAndCursorAction> => + createIncomingActionWithPayload(CommonActionType.FOCUS_CELL, { cellId, cursorPos }), + unfocusCell: (cellId: string, code: string) => + createIncomingActionWithPayload(CommonActionType.UNFOCUS_CELL, { cellId, code }), + selectCell: (cellId: string, cursorPos: CursorPos = CursorPos.Current): CommonAction<ICellAndCursorAction> => + createIncomingActionWithPayload(CommonActionType.SELECT_CELL, { cellId, cursorPos }), + executeAllCells: (): CommonAction => createIncomingAction(CommonActionType.EXECUTE_ALL_CELLS), + executeAbove: (cellId: string): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.EXECUTE_ABOVE, { cellId }), + executeCellAndBelow: (cellId: string): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.EXECUTE_CELL_AND_BELOW, { cellId }), + toggleVariableExplorer: (): CommonAction => createIncomingAction(CommonActionType.TOGGLE_VARIABLE_EXPLORER), + setVariableExplorerHeight: (containerHeight: number, gridHeight: number): CommonAction<IVariableExplorerHeight> => + createIncomingActionWithPayload(CommonActionType.SET_VARIABLE_EXPLORER_HEIGHT, { containerHeight, gridHeight }), + restartKernel: (): CommonAction => createIncomingAction(CommonActionType.RESTART_KERNEL), + interruptKernel: (): CommonAction => createIncomingAction(CommonActionType.INTERRUPT_KERNEL), + clearAllOutputs: (): CommonAction => createIncomingAction(InteractiveWindowMessages.ClearAllOutputs), + export: (): CommonAction => createIncomingAction(CommonActionType.EXPORT), + exportAs: (): CommonAction => createIncomingAction(CommonActionType.EXPORT_NOTEBOOK_AS), + save: (): CommonAction => createIncomingAction(CommonActionType.SAVE), + showDataViewer: (variable: IJupyterVariable, columnSize: number): CommonAction<IShowDataViewerAction> => + createIncomingActionWithPayload(CommonActionType.SHOW_DATA_VIEWER, { variable, columnSize }), + sendCommand: ( + command: NativeKeyboardCommandTelemetry | NativeMouseCommandTelemetry + ): CommonAction<ISendCommandAction> => createIncomingActionWithPayload(CommonActionType.SEND_COMMAND, { command }), + moveCellUp: (cellId: string): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.MOVE_CELL_UP, { cellId }), + moveCellDown: (cellId: string): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.MOVE_CELL_DOWN, { cellId }), + changeCellType: (cellId: string) => createIncomingActionWithPayload(CommonActionType.CHANGE_CELL_TYPE, { cellId }), + toggleLineNumbers: (cellId: string): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.TOGGLE_LINE_NUMBERS, { cellId }), + toggleOutput: (cellId: string): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.TOGGLE_OUTPUT, { cellId }), + deleteCell: (cellId: string): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.DELETE_CELL, { cellId }), + undo: (): CommonAction => createIncomingAction(InteractiveWindowMessages.Undo), + redo: (): CommonAction => createIncomingAction(InteractiveWindowMessages.Redo), + arrowUp: (cellId: string, code: string): CommonAction<ICodeAction> => + createIncomingActionWithPayload(CommonActionType.ARROW_UP, { cellId, code }), + arrowDown: (cellId: string, code: string): CommonAction<ICodeAction> => + createIncomingActionWithPayload(CommonActionType.ARROW_DOWN, { cellId, code }), + editCell: (cellId: string, e: IMonacoModelContentChangeEvent): CommonAction<IEditCellAction> => + createIncomingActionWithPayload(CommonActionType.EDIT_CELL, { + cellId, + version: e.versionId, + modelId: e.model.id, + forward: e.forward, + reverse: e.reverse, + id: cellId, + code: e.model.getValue() + }), + linkClick: (href: string): CommonAction<ILinkClickAction> => + createIncomingActionWithPayload(CommonActionType.LINK_CLICK, { href }), + showPlot: (imageHtml: string) => createIncomingActionWithPayload(InteractiveWindowMessages.ShowPlot, imageHtml), + gatherCell: (cellId: string | undefined): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.GATHER_CELL, { cellId }), + gatherCellToScript: (cellId: string | undefined): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.GATHER_CELL_TO_SCRIPT, { cellId }), + editorLoaded: (): CommonAction => createIncomingAction(CommonActionType.EDITOR_LOADED), + codeCreated: (cellId: string | undefined, modelId: string): CommonAction<ICodeCreatedAction> => + createIncomingActionWithPayload(CommonActionType.CODE_CREATED, { cellId, modelId }), + loadedAllCells: (): CommonAction => createIncomingAction(CommonActionType.LOADED_ALL_CELLS), + editorUnmounted: (): CommonAction => createIncomingAction(CommonActionType.UNMOUNT), + selectKernel: (): CommonAction => createIncomingAction(InteractiveWindowMessages.SelectKernel), + selectServer: (): CommonAction => createIncomingAction(CommonActionType.SELECT_SERVER), + launchNotebookTrustPrompt: (): CommonAction => createIncomingAction(CommonActionType.LAUNCH_NOTEBOOK_TRUST_PROMPT), + openSettings: (setting?: string): CommonAction<IOpenSettingsAction> => + createIncomingActionWithPayload(CommonActionType.OPEN_SETTINGS, { setting }), + getVariableData: ( + newExecutionCount: number, + refreshCount: number, + startIndex: number = 0, + pageSize: number = 100 + ): CommonAction<IJupyterVariablesRequest> => + createIncomingActionWithPayload(CommonActionType.GET_VARIABLE_DATA, { + executionCount: newExecutionCount, + sortColumn: 'name', + sortAscending: true, + startIndex, + pageSize, + refreshCount + }), + widgetFailed: (ex: Error): CommonAction<Error> => + createIncomingActionWithPayload(CommonActionType.IPYWIDGET_RENDER_FAILURE, ex), + runByLine: (cellId: string): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.RUN_BY_LINE, { cellId }), + continue: (cellId: string): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.CONTINUE, { cellId }), + step: (cellId: string): CommonAction<ICellAction> => + createIncomingActionWithPayload(CommonActionType.STEP, { cellId }) +}; diff --git a/src/datascience-ui/native-editor/redux/mapping.ts b/src/datascience-ui/native-editor/redux/mapping.ts new file mode 100644 index 000000000000..354cbe665814 --- /dev/null +++ b/src/datascience-ui/native-editor/redux/mapping.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { + IInteractiveWindowMapping, + InteractiveWindowMessages +} from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { BaseReduxActionPayload } from '../../../client/datascience/interactive-common/types'; +import { IMainState } from '../../interactive-common/mainState'; +import { CommonActionType, CommonActionTypeMapping } from '../../interactive-common/redux/reducers/types'; +import { ReducerArg, ReducerFunc } from '../../react-common/reduxUtils'; + +type NativeEditorReducerFunc<T = never | undefined> = ReducerFunc< + IMainState, + CommonActionType | InteractiveWindowMessages, + BaseReduxActionPayload<T> +>; + +export type NativeEditorReducerArg<T = never | undefined> = ReducerArg< + IMainState, + CommonActionType | InteractiveWindowMessages, + BaseReduxActionPayload<T> +>; + +type NativeEditorReducerFunctions<T> = { + [P in keyof T]: T[P] extends never | undefined ? NativeEditorReducerFunc : NativeEditorReducerFunc<T[P]>; +}; + +export type INativeEditorActionMapping = NativeEditorReducerFunctions<IInteractiveWindowMapping> & + NativeEditorReducerFunctions<CommonActionTypeMapping>; diff --git a/src/datascience-ui/native-editor/redux/reducers/creation.ts b/src/datascience-ui/native-editor/redux/reducers/creation.ts new file mode 100644 index 000000000000..4d032e3e5cda --- /dev/null +++ b/src/datascience-ui/native-editor/redux/reducers/creation.ts @@ -0,0 +1,494 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { noop } from '../../../../client/common/utils/misc'; +import { + IEditorContentChange, + IFinishCell, + ILoadAllCells, + NotebookModelChange +} from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { ICell, IDataScienceExtraSettings } from '../../../../client/datascience/types'; +import { splitMultilineString } from '../../../common'; +import { + createCellVM, + createEmptyCell, + CursorPos, + DebugState, + extractInputText, + getSelectedAndFocusedInfo, + ICellViewModel, + IMainState +} from '../../../interactive-common/mainState'; +import { queueIncomingActionWithPayload } from '../../../interactive-common/redux/helpers'; +import { Helpers } from '../../../interactive-common/redux/reducers/helpers'; +import { Transfer } from '../../../interactive-common/redux/reducers/transfer'; +import { CommonActionType, IAddCellAction, ICellAction } from '../../../interactive-common/redux/reducers/types'; +import { NativeEditorReducerArg } from '../mapping'; +import { Effects } from './effects'; +import { Execution } from './execution'; +import { Movement } from './movement'; + +export namespace Creation { + function prepareCellVM(cell: ICell, hasBeenRun: boolean, settings?: IDataScienceExtraSettings): ICellViewModel { + const cellVM: ICellViewModel = createCellVM(cell, settings, true, false); + + // Set initial cell visibility and collapse + cellVM.editable = true; + + // Always have the cell input text open + const newText = extractInputText(cellVM, settings); + + cellVM.inputBlockOpen = true; + cell.data.source = splitMultilineString(newText); + cellVM.hasBeenRun = hasBeenRun; + + return cellVM; + } + + export function addAndFocusCell(arg: NativeEditorReducerArg<IAddCellAction>): IMainState { + queueIncomingActionWithPayload(arg, CommonActionType.ADD_NEW_CELL, { newCellId: arg.payload.data.newCellId }); + queueIncomingActionWithPayload(arg, CommonActionType.FOCUS_CELL, { + cellId: arg.payload.data.newCellId, + cursorPos: CursorPos.Current + }); + return arg.prevState; + } + + export function insertAboveAndFocusCell(arg: NativeEditorReducerArg<IAddCellAction & ICellAction>): IMainState { + queueIncomingActionWithPayload(arg, CommonActionType.INSERT_ABOVE, { + cellId: arg.payload.data.cellId, + newCellId: arg.payload.data.newCellId + }); + queueIncomingActionWithPayload(arg, CommonActionType.SELECT_CELL, { + cellId: arg.payload.data.newCellId, + cursorPos: CursorPos.Current + }); + return arg.prevState; + } + + export function insertBelowAndFocusCell(arg: NativeEditorReducerArg<IAddCellAction & ICellAction>): IMainState { + queueIncomingActionWithPayload(arg, CommonActionType.INSERT_BELOW, { + cellId: arg.payload.data.cellId, + newCellId: arg.payload.data.newCellId + }); + queueIncomingActionWithPayload(arg, CommonActionType.SELECT_CELL, { + cellId: arg.payload.data.newCellId, + cursorPos: CursorPos.Current + }); + return arg.prevState; + } + + export function insertAboveFirstAndFocusCell(arg: NativeEditorReducerArg<IAddCellAction>): IMainState { + queueIncomingActionWithPayload(arg, CommonActionType.INSERT_ABOVE_FIRST, { + newCellId: arg.payload.data.newCellId + }); + queueIncomingActionWithPayload(arg, CommonActionType.FOCUS_CELL, { + cellId: arg.payload.data.newCellId, + cursorPos: CursorPos.Current + }); + return arg.prevState; + } + + function insertAbove(arg: NativeEditorReducerArg<ICellAction & { vm: ICellViewModel }>): IMainState { + const newList = [...arg.prevState.cellVMs]; + const newVM = arg.payload.data.vm; + + // Find the position where we want to insert + let position = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (position >= 0) { + newList.splice(position, 0, newVM); + } else { + newList.splice(0, 0, newVM); + position = 0; + } + + const result = { + ...arg.prevState, + undoStack: Helpers.pushStack(arg.prevState.undoStack, arg.prevState.cellVMs), + cellVMs: newList + }; + + // Send a messsage that we inserted a cell + Transfer.postModelInsert(arg, position, newVM.cell, arg.payload.data.cellId); + + return result; + } + + export function insertExistingAbove(arg: NativeEditorReducerArg<ICellAction & { cell: ICell }>): IMainState { + const newVM = prepareCellVM(arg.payload.data.cell, false, arg.prevState.settings); + return insertAbove({ + ...arg, + payload: { + ...arg.payload, + data: { + cellId: arg.payload.data.cellId, + vm: newVM + } + } + }); + } + + export function insertNewAbove(arg: NativeEditorReducerArg<ICellAction & IAddCellAction>): IMainState { + const newVM = prepareCellVM(createEmptyCell(arg.payload.data.newCellId, null), false, arg.prevState.settings); + return insertAbove({ + ...arg, + payload: { + ...arg.payload, + data: { + cellId: arg.payload.data.cellId, + vm: newVM + } + } + }); + } + + export function insertBelow(arg: NativeEditorReducerArg<ICellAction & IAddCellAction>): IMainState { + const newVM = prepareCellVM(createEmptyCell(arg.payload.data.newCellId, null), false, arg.prevState.settings); + const newList = [...arg.prevState.cellVMs]; + + // Find the position where we want to insert + let position = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (position >= 0) { + position += 1; + newList.splice(position, 0, newVM); + } else { + newList.push(newVM); + position = newList.length; + } + + const result = { + ...arg.prevState, + undoStack: Helpers.pushStack(arg.prevState.undoStack, arg.prevState.cellVMs), + cellVMs: newList + }; + + // Send a messsage that we inserted a cell + Transfer.postModelInsert(arg, position, newVM.cell, arg.payload.data.cellId); + + return result; + } + + export function insertAboveFirst(arg: NativeEditorReducerArg<IAddCellAction>): IMainState { + // Get the first cell id + const firstCellId = arg.prevState.cellVMs.length > 0 ? arg.prevState.cellVMs[0].cell.id : undefined; + + // Do what an insertAbove does + return insertNewAbove({ + ...arg, + payload: { ...arg.payload, data: { cellId: firstCellId, newCellId: arg.payload.data.newCellId } } + }); + } + + export function addNewCell(arg: NativeEditorReducerArg<IAddCellAction>): IMainState { + // Do the same thing that an insertBelow does using the currently selected cell. + return insertBelow({ + ...arg, + payload: { + ...arg.payload, + data: { + cellId: getSelectedAndFocusedInfo(arg.prevState).selectedCellId, + newCellId: arg.payload.data.newCellId + } + } + }); + } + + export function startCell(arg: NativeEditorReducerArg<ICell>): IMainState { + return Helpers.updateOrAdd(arg, (c: ICell, s: IMainState) => prepareCellVM(c, true, s.settings)); + } + + export function updateCell(arg: NativeEditorReducerArg<ICell>): IMainState { + return Helpers.updateOrAdd(arg, (c: ICell, s: IMainState) => prepareCellVM(c, true, s.settings)); + } + + export function finishCell(arg: NativeEditorReducerArg<IFinishCell>): IMainState { + return Helpers.updateOrAdd( + { ...arg, payload: { ...arg.payload, data: arg.payload.data.cell } }, + (c: ICell, s: IMainState) => prepareCellVM(c, true, s.settings) + ); + } + + export function deleteAllCells(arg: NativeEditorReducerArg<IAddCellAction>): IMainState { + // Just leave one single blank empty cell + const newVM: ICellViewModel = { + cell: createEmptyCell(arg.payload.data.newCellId, null), + editable: true, + inputBlockOpen: true, + inputBlockShow: true, + inputBlockText: '', + inputBlockCollapseNeeded: false, + selected: false, + focused: false, + cursorPos: CursorPos.Current, + hasBeenRun: false, + scrollCount: 0, + runningByLine: DebugState.Design, + gathering: false + }; + + Transfer.postModelRemoveAll(arg, newVM.cell.id); + + return { + ...arg.prevState, + cellVMs: [newVM], + undoStack: Helpers.pushStack(arg.prevState.undoStack, arg.prevState.cellVMs) + }; + } + + export function applyCellEdit( + arg: NativeEditorReducerArg<{ id: string; changes: IEditorContentChange[] }> + ): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.id); + if (index >= 0) { + const newVM = { ...arg.prevState.cellVMs[index] }; + arg.payload.data.changes.forEach((c) => { + const source = newVM.inputBlockText; + const before = source.slice(0, c.rangeOffset); + // tslint:disable-next-line: restrict-plus-operands + const after = source.slice(c.rangeOffset + c.rangeLength); + newVM.inputBlockText = `${before}${c.text}${after}`; + }); + newVM.codeVersion = newVM.codeVersion ? newVM.codeVersion + 1 : 1; + newVM.cell.data.source = splitMultilineString(newVM.inputBlockText); + newVM.cursorPos = arg.payload.data.changes[0].position; + const newVMs = [...arg.prevState.cellVMs]; + newVMs[index] = Helpers.asCellViewModel(newVM); + // When editing, make sure we focus the edited cell (otherwise undo looks weird because it undoes a non focused cell) + return Effects.focusCell({ + ...arg, + prevState: { ...arg.prevState, cellVMs: newVMs }, + payload: { ...arg.payload, data: { cursorPos: CursorPos.Current, cellId: arg.payload.data.id } } + }); + } + return arg.prevState; + } + + export function deleteCell(arg: NativeEditorReducerArg<ICellAction>): IMainState { + const cells = arg.prevState.cellVMs; + if (cells.length === 1 && cells[0].cell.id === arg.payload.data.cellId) { + // Special case, if this is the last cell, don't delete it, just clear it's output and input + const newVM: ICellViewModel = { + cell: createEmptyCell(arg.payload.data.cellId, null), + editable: true, + inputBlockOpen: true, + inputBlockShow: true, + inputBlockText: '', + inputBlockCollapseNeeded: false, + selected: cells[0].selected, + focused: cells[0].focused, + cursorPos: CursorPos.Current, + hasBeenRun: false, + scrollCount: 0, + runningByLine: DebugState.Design, + gathering: false + }; + + // Send messages to other side to indicate the new add + Transfer.postModelRemove(arg, 0, cells[0].cell); + Transfer.postModelInsert(arg, 0, newVM.cell); + + return { + ...arg.prevState, + undoStack: Helpers.pushStack(arg.prevState.undoStack, arg.prevState.cellVMs), + cellVMs: [newVM] + }; + } else if (arg.payload.data.cellId) { + // Otherwise just a straight delete + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (index >= 0) { + Transfer.postModelRemove(arg, 0, cells[index].cell); + + // Recompute select/focus if this item has either + const previousSelection = getSelectedAndFocusedInfo(arg.prevState); + const newVMs = [...arg.prevState.cellVMs.filter((c) => c.cell.id !== arg.payload.data.cellId)]; + const nextOrPrev = index === arg.prevState.cellVMs.length - 1 ? index - 1 : index; + if ( + previousSelection.selectedCellId === arg.payload.data.cellId || + previousSelection.focusedCellId === arg.payload.data.cellId + ) { + if (nextOrPrev >= 0) { + newVMs[nextOrPrev] = { + ...newVMs[nextOrPrev], + selected: true, + focused: previousSelection.focusedCellId === arg.payload.data.cellId + }; + } + } + + return { + ...arg.prevState, + cellVMs: newVMs, + undoStack: Helpers.pushStack(arg.prevState.undoStack, arg.prevState.cellVMs), + skipNextScroll: true + }; + } + } + + return arg.prevState; + } + + export function loadAllCells(arg: NativeEditorReducerArg<ILoadAllCells>): IMainState { + const vms = arg.payload.data.cells.map((c) => prepareCellVM(c, false, arg.prevState.settings)); + return { + ...arg.prevState, + busy: false, + loadTotal: arg.payload.data.cells.length, + undoStack: [], + cellVMs: vms, + loaded: true, + isNotebookTrusted: arg.payload.data.isNotebookTrusted! + }; + } + + export function unmount(arg: NativeEditorReducerArg): IMainState { + return { + ...arg.prevState, + cellVMs: [], + undoStack: [], + redoStack: [] + }; + } + + function handleUndoModel(arg: NativeEditorReducerArg<NotebookModelChange>): IMainState { + // Disable the queueAction in the arg so that calling other reducers doesn't cause + // messages to be posted back (as were handling a message from the extension here) + const disabledQueueArg = { ...arg, queueAction: noop }; + switch (arg.payload.data.kind) { + case 'clear': + return loadAllCells({ + ...disabledQueueArg, + payload: { ...arg.payload, data: { cells: arg.payload.data.oldCells } } + }); + case 'edit': + return applyCellEdit({ + ...disabledQueueArg, + payload: { ...arg.payload, data: { id: arg.payload.data.id, changes: arg.payload.data.reverse } } + }); + case 'insert': + return deleteCell({ + ...disabledQueueArg, + payload: { ...arg.payload, data: { cellId: arg.payload.data.cell.id } } + }); + case 'remove': + const cellBelow = + arg.prevState.cellVMs.length > arg.payload.data.index + ? arg.prevState.cellVMs[arg.payload.data.index].cell + : undefined; + return insertExistingAbove({ + ...disabledQueueArg, + payload: { + ...arg.payload, + data: { cell: arg.payload.data.cell, cellId: cellBelow ? cellBelow.id : undefined } + } + }); + case 'remove_all': + return loadAllCells({ + ...disabledQueueArg, + payload: { ...arg.payload, data: { cells: arg.payload.data.oldCells } } + }); + case 'swap': + return Movement.swapCells({ + ...disabledQueueArg, + payload: { + ...arg.payload, + data: { + firstCellId: arg.payload.data.secondCellId, + secondCellId: arg.payload.data.firstCellId + } + } + }); + case 'modify': + // Undo for modify should reapply the outputs. Go through each and apply the update + let result = arg.prevState; + arg.payload.data.oldCells.forEach((c) => { + result = updateCell({ + ...disabledQueueArg, + prevState: result, + payload: { ...arg.payload, data: c } + }); + }); + return result; + + default: + // File, version can be ignored. + break; + } + + return arg.prevState; + } + + function handleRedoModel(arg: NativeEditorReducerArg<NotebookModelChange>): IMainState { + // Disable the queueAction in the arg so that calling other reducers doesn't cause + // messages to be posted back (as were handling a message from the extension here) + const disabledQueueArg = { ...arg, queueAction: noop }; + switch (arg.payload.data.kind) { + case 'clear': + // tslint:disable-next-line: no-any + return Execution.clearAllOutputs(disabledQueueArg as any); + case 'edit': + return applyCellEdit({ + ...disabledQueueArg, + payload: { ...arg.payload, data: { id: arg.payload.data.id, changes: arg.payload.data.forward } } + }); + case 'insert': + return insertExistingAbove({ + ...disabledQueueArg, + payload: { + ...arg.payload, + data: { cell: arg.payload.data.cell, cellId: arg.payload.data.codeCellAboveId } + } + }); + case 'remove': + return deleteCell({ + ...disabledQueueArg, + payload: { ...arg.payload, data: { cellId: arg.payload.data.cell.id } } + }); + case 'remove_all': + return deleteAllCells({ + ...disabledQueueArg, + payload: { ...arg.payload, data: { newCellId: arg.payload.data.newCellId } } + }); + case 'swap': + return Movement.swapCells({ + ...disabledQueueArg, + payload: { + ...arg.payload, + data: { + firstCellId: arg.payload.data.secondCellId, + secondCellId: arg.payload.data.firstCellId + } + } + }); + case 'modify': + // Redo for modify should reapply the outputs. Go through each and apply the update + let result = arg.prevState; + arg.payload.data.newCells.forEach((c) => { + result = updateCell({ + ...disabledQueueArg, + prevState: result, + payload: { ...arg.payload, data: c } + }); + }); + return result; + default: + // Modify, file, version can all be ignored. + break; + } + + return arg.prevState; + } + + export function handleUpdate(arg: NativeEditorReducerArg<NotebookModelChange>): IMainState { + switch (arg.payload.data.source) { + case 'undo': + return handleUndoModel(arg); + case 'redo': + return handleRedoModel(arg); + default: + break; + } + return arg.prevState; + } +} diff --git a/src/datascience-ui/native-editor/redux/reducers/effects.ts b/src/datascience-ui/native-editor/redux/reducers/effects.ts new file mode 100644 index 000000000000..982e4776d290 --- /dev/null +++ b/src/datascience-ui/native-editor/redux/reducers/effects.ts @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { CssMessages } from '../../../../client/datascience/messages'; +import { IDataScienceExtraSettings } from '../../../../client/datascience/types'; +import { getSelectedAndFocusedInfo, IMainState } from '../../../interactive-common/mainState'; +import { postActionToExtension } from '../../../interactive-common/redux/helpers'; +import { Helpers } from '../../../interactive-common/redux/reducers/helpers'; +import { ICellAction, ICellAndCursorAction } from '../../../interactive-common/redux/reducers/types'; +import { computeEditorOptions } from '../../../react-common/settingsReactSide'; +import { NativeEditorReducerArg } from '../mapping'; + +export namespace Effects { + export function focusCell(arg: NativeEditorReducerArg<ICellAndCursorAction>): IMainState { + // Do nothing if already the focused cell. + let selectionInfo = getSelectedAndFocusedInfo(arg.prevState); + if (selectionInfo.focusedCellId !== arg.payload.data.cellId) { + let prevState = arg.prevState; + + // Ensure we unfocus & unselect all cells. + while (selectionInfo.focusedCellId || selectionInfo.selectedCellId) { + selectionInfo = getSelectedAndFocusedInfo(prevState); + // First find the old focused cell and unfocus it + let removeFocusIndex = selectionInfo.focusedCellIndex; + if (typeof removeFocusIndex !== 'number') { + removeFocusIndex = selectionInfo.selectedCellIndex; + } + + if (typeof removeFocusIndex === 'number') { + prevState = unfocusCell({ + ...arg, + prevState, + payload: { + ...arg.payload, + data: { cellId: prevState.cellVMs[removeFocusIndex].cell.id } + } + }); + prevState = deselectCell({ + ...arg, + prevState, + payload: { ...arg.payload, data: { cellId: prevState.cellVMs[removeFocusIndex].cell.id } } + }); + } + } + + const newVMs = [...prevState.cellVMs]; + + // Add focus on new cell + const addFocusIndex = newVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (addFocusIndex >= 0) { + newVMs[addFocusIndex] = { + ...newVMs[addFocusIndex], + focused: true, + selected: true, + cursorPos: arg.payload.data.cursorPos + }; + } + + return { + ...prevState, + cellVMs: newVMs + }; + } + + return arg.prevState; + } + + export function unfocusCell(arg: NativeEditorReducerArg<ICellAction>): IMainState { + // Unfocus the cell + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + const selectionInfo = getSelectedAndFocusedInfo(arg.prevState); + if (index >= 0 && selectionInfo.focusedCellId === arg.payload.data.cellId) { + const newVMs = [...arg.prevState.cellVMs]; + const current = arg.prevState.cellVMs[index]; + const newCell = { + ...current, + focused: false + }; + + // tslint:disable-next-line: no-any + newVMs[index] = Helpers.asCellViewModel(newCell); // This is because IMessageCell doesn't fit in here + + return { + ...arg.prevState, + cellVMs: newVMs + }; + } else if (index >= 0) { + // Dont change focus state if not the focused cell. Just update the code. + const newVMs = [...arg.prevState.cellVMs]; + const current = arg.prevState.cellVMs[index]; + const newCell = { + ...current + }; + + // tslint:disable-next-line: no-any + newVMs[index] = newCell as any; // This is because IMessageCell doesn't fit in here + + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + + return arg.prevState; + } + + export function deselectCell(arg: NativeEditorReducerArg<ICellAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + const selectionInfo = getSelectedAndFocusedInfo(arg.prevState); + if (index >= 0 && selectionInfo.selectedCellId === arg.payload.data.cellId) { + const newVMs = [...arg.prevState.cellVMs]; + const target = arg.prevState.cellVMs[index]; + const newCell = { + ...target, + selected: false + }; + + // tslint:disable-next-line: no-any + newVMs[index] = newCell as any; // This is because IMessageCell doesn't fit in here + + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + + return arg.prevState; + } + + /** + * Select a cell. + * + * @param {boolean} [shouldFocusCell] If provided, then will control the focus behavior of the cell. (defaults to focus state of previously selected cell). + */ + export function selectCell( + arg: NativeEditorReducerArg<ICellAndCursorAction>, + shouldFocusCell?: boolean + ): IMainState { + // Skip doing anything if already selected. + const selectionInfo = getSelectedAndFocusedInfo(arg.prevState); + if (arg.payload.data.cellId !== selectionInfo.selectedCellId) { + let prevState = arg.prevState; + const addIndex = prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + const someOtherCellWasFocusedAndSelected = + selectionInfo.focusedCellId === selectionInfo.selectedCellId && !!selectionInfo.focusedCellId; + // First find the old focused cell and unfocus it + let removeFocusIndex = arg.prevState.cellVMs.findIndex((c) => c.cell.id === selectionInfo.focusedCellId); + if (removeFocusIndex < 0) { + removeFocusIndex = arg.prevState.cellVMs.findIndex((c) => c.cell.id === selectionInfo.selectedCellId); + } + + if (removeFocusIndex >= 0) { + prevState = unfocusCell({ + ...arg, + prevState, + payload: { + ...arg.payload, + data: { cellId: prevState.cellVMs[removeFocusIndex].cell.id } + } + }); + prevState = deselectCell({ + ...arg, + prevState, + payload: { ...arg.payload, data: { cellId: prevState.cellVMs[removeFocusIndex].cell.id } } + }); + } + + const newVMs = [...prevState.cellVMs]; + if (addIndex >= 0 && arg.payload.data.cellId !== selectionInfo.selectedCellId) { + newVMs[addIndex] = { + ...newVMs[addIndex], + focused: + typeof shouldFocusCell === 'boolean' ? shouldFocusCell : someOtherCellWasFocusedAndSelected, + selected: true, + cursorPos: arg.payload.data.cursorPos + }; + } + + return { + ...prevState, + cellVMs: newVMs + }; + } + return arg.prevState; + } + + export function toggleLineNumbers(arg: NativeEditorReducerArg<ICellAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (index >= 0) { + const newVMs = [...arg.prevState.cellVMs]; + newVMs[index] = { ...newVMs[index], showLineNumbers: !newVMs[index].showLineNumbers }; + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + return arg.prevState; + } + + export function toggleOutput(arg: NativeEditorReducerArg<ICellAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (index >= 0) { + const newVMs = [...arg.prevState.cellVMs]; + newVMs[index] = { ...newVMs[index], hideOutput: !newVMs[index].hideOutput }; + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + return arg.prevState; + } + + export function updateSettings(arg: NativeEditorReducerArg<string>): IMainState { + // String arg should be the IDataScienceExtraSettings + const newSettingsJSON = JSON.parse(arg.payload.data); + const newSettings = <IDataScienceExtraSettings>newSettingsJSON; + const newEditorOptions = computeEditorOptions(newSettings); + const newFontFamily = newSettings.extraSettings + ? newSettings.extraSettings.editor.fontFamily + : arg.prevState.font.family; + const newFontSize = newSettings.extraSettings + ? newSettings.extraSettings.editor.fontSize + : arg.prevState.font.size; + + // Ask for new theme data if necessary + if ( + newSettings && + newSettings.extraSettings && + newSettings.extraSettings.theme !== arg.prevState.vscodeThemeName + ) { + const knownDark = Helpers.computeKnownDark(newSettings); + // User changed the current theme. Rerender + postActionToExtension(arg, CssMessages.GetCssRequest, { isDark: knownDark }); + postActionToExtension(arg, CssMessages.GetMonacoThemeRequest, { isDark: knownDark }); + } + + return { + ...arg.prevState, + settings: newSettings, + editorOptions: { ...newEditorOptions, lineDecorationsWidth: 5 }, + font: { + size: newFontSize, + family: newFontFamily + } + }; + } +} diff --git a/src/datascience-ui/native-editor/redux/reducers/execution.ts b/src/datascience-ui/native-editor/redux/reducers/execution.ts new file mode 100644 index 000000000000..57ecc2477781 --- /dev/null +++ b/src/datascience-ui/native-editor/redux/reducers/execution.ts @@ -0,0 +1,357 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +// tslint:disable-next-line: no-require-imports no-var-requires +const cloneDeep = require('lodash/cloneDeep'); +import * as uuid from 'uuid/v4'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { InteractiveWindowMessages } from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { CellState, ICell } from '../../../../client/datascience/types'; +import { concatMultilineString } from '../../../common'; +import { createCellFrom } from '../../../common/cellFactory'; +import { + CursorPos, + DebugState, + getSelectedAndFocusedInfo, + ICellViewModel, + IMainState +} from '../../../interactive-common/mainState'; +import { postActionToExtension, queueIncomingActionWithPayload } from '../../../interactive-common/redux/helpers'; +import { Helpers } from '../../../interactive-common/redux/reducers/helpers'; +import { Transfer } from '../../../interactive-common/redux/reducers/transfer'; +import { + CommonActionType, + ICellAction, + IChangeCellTypeAction, + IExecuteAction +} from '../../../interactive-common/redux/reducers/types'; +import { NativeEditorReducerArg } from '../mapping'; +import { Effects } from './effects'; + +export namespace Execution { + function executeRange( + prevState: IMainState, + cellIds: string[], + // tslint:disable-next-line: no-any + originalArg: NativeEditorReducerArg<any> + ): IMainState { + const newVMs = [...prevState.cellVMs]; + const cellIdsToExecute: string[] = []; + cellIds.forEach((cellId) => { + const index = prevState.cellVMs.findIndex((cell) => cell.cell.id === cellId); + if (index === -1) { + return; + } + const orig = prevState.cellVMs[index]; + // noop if the submitted code is just a cell marker + if (orig.cell.data.cell_type === 'code' && concatMultilineString(orig.cell.data.source)) { + // When cloning cells, preserve the metadata (hence deep clone). + const clonedCell = cloneDeep(orig.cell.data); + // Update our input cell to be in progress again and clear outputs + clonedCell.outputs = []; + newVMs[index] = Helpers.asCellViewModel({ + ...orig, + cell: { ...orig.cell, state: CellState.executing, data: clonedCell } + }); + cellIdsToExecute.push(orig.cell.id); + } + }); + + // If any cells to execute, execute them all + if (cellIdsToExecute.length > 0) { + // Send a message if a code cell + postActionToExtension(originalArg, InteractiveWindowMessages.ReExecuteCells, { + cellIds: cellIdsToExecute + }); + } + + return { + ...prevState, + cellVMs: newVMs + }; + } + + export function executeAbove(arg: NativeEditorReducerArg<ICellAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (index > 0) { + // Get all cellIds until `index`. + const cellIds = arg.prevState.cellVMs.slice(0, index).map((cellVm) => cellVm.cell.id); + return executeRange(arg.prevState, cellIds, arg); + } + return arg.prevState; + } + + export function executeCellAndAdvance(arg: NativeEditorReducerArg<IExecuteAction>): IMainState { + queueIncomingActionWithPayload(arg, CommonActionType.EXECUTE_CELL, { + cellId: arg.payload.data.cellId, + moveOp: arg.payload.data.moveOp + }); + if (arg.payload.data.moveOp === 'add') { + const newCellId = uuid(); + queueIncomingActionWithPayload(arg, CommonActionType.INSERT_BELOW, { + cellId: arg.payload.data.cellId, + newCellId + }); + queueIncomingActionWithPayload(arg, CommonActionType.FOCUS_CELL, { + cellId: newCellId, + cursorPos: CursorPos.Current + }); + } + return arg.prevState; + } + + export function executeCell(arg: NativeEditorReducerArg<IExecuteAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (index >= 0 && arg.payload.data.cellId) { + // Start executing this cell. + const executeResult = executeRange(arg.prevState, [arg.payload.data.cellId], arg); + + // Modify the execute result if moving + if (arg.payload.data.moveOp === 'select') { + // Select the cell below this one, but don't focus it + if (index < arg.prevState.cellVMs.length - 1) { + return Effects.selectCell( + { + ...arg, + prevState: { + ...executeResult + }, + payload: { + ...arg.payload, + data: { + ...arg.payload.data, + cellId: arg.prevState.cellVMs[index + 1].cell.id, + cursorPos: CursorPos.Current + } + } + }, + // Select the next cell, but do not set focus to it. + false + ); + } + return executeResult; + } else { + return executeResult; + } + } + return arg.prevState; + } + + export function executeCellAndBelow(arg: NativeEditorReducerArg<ICellAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (index >= 0) { + // Get all cellIds starting from `index`. + const cellIds = arg.prevState.cellVMs.slice(index).map((cellVm) => cellVm.cell.id); + return executeRange(arg.prevState, cellIds, arg); + } + return arg.prevState; + } + + export function executeAllCells(arg: NativeEditorReducerArg): IMainState { + if (arg.prevState.cellVMs.length > 0) { + const cellIds = arg.prevState.cellVMs.map((cellVm) => cellVm.cell.id); + return executeRange(arg.prevState, cellIds, arg); + } + return arg.prevState; + } + + export function executeSelectedCell(arg: NativeEditorReducerArg): IMainState { + // This is the same thing as executing the selected cell + const selectionInfo = getSelectedAndFocusedInfo(arg.prevState); + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === selectionInfo.selectedCellId); + if (selectionInfo.selectedCellId && index >= 0) { + return executeCell({ + ...arg, + payload: { + ...arg.payload, + data: { + cellId: selectionInfo.selectedCellId, + moveOp: 'none' + } + } + }); + } + + return arg.prevState; + } + + export function clearAllOutputs(arg: NativeEditorReducerArg): IMainState { + const newList = arg.prevState.cellVMs.map((cellVM) => { + return Helpers.asCellViewModel({ + ...cellVM, + cell: { ...cellVM.cell, data: { ...cellVM.cell.data, outputs: [], execution_count: null } } + }); + }); + + Transfer.postModelClearOutputs(arg); + + return { + ...arg.prevState, + cellVMs: newList + }; + } + + export function changeCellType(arg: NativeEditorReducerArg<IChangeCellTypeAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (index >= 0) { + const cellVMs = [...arg.prevState.cellVMs]; + const current = arg.prevState.cellVMs[index]; + const newType = current.cell.data.cell_type === 'code' ? 'markdown' : 'code'; + const newNotebookCell = createCellFrom(current.cell.data, newType); + const newCell: ICellViewModel = { + ...current, + cell: { + ...current.cell, + data: newNotebookCell + } + }; + cellVMs[index] = newCell; + Transfer.changeCellType(arg, cellVMs[index].cell); + + return { + ...arg.prevState, + cellVMs + }; + } + + return arg.prevState; + } + + export function undo(arg: NativeEditorReducerArg): IMainState { + if (arg.prevState.undoStack.length > 0) { + // Pop one off of our undo stack and update our redo + const cells = arg.prevState.undoStack[arg.prevState.undoStack.length - 1]; + const undoStack = arg.prevState.undoStack.slice(0, arg.prevState.undoStack.length - 1); + const redoStack = Helpers.pushStack(arg.prevState.redoStack, arg.prevState.cellVMs); + postActionToExtension(arg, InteractiveWindowMessages.Undo); + return { + ...arg.prevState, + cellVMs: cells, + undoStack: undoStack, + redoStack: redoStack, + skipNextScroll: true + }; + } + + return arg.prevState; + } + + export function redo(arg: NativeEditorReducerArg): IMainState { + if (arg.prevState.redoStack.length > 0) { + // Pop one off of our redo stack and update our undo + const cells = arg.prevState.redoStack[arg.prevState.redoStack.length - 1]; + const redoStack = arg.prevState.redoStack.slice(0, arg.prevState.redoStack.length - 1); + const undoStack = Helpers.pushStack(arg.prevState.undoStack, arg.prevState.cellVMs); + postActionToExtension(arg, InteractiveWindowMessages.Redo); + return { + ...arg.prevState, + cellVMs: cells, + undoStack: undoStack, + redoStack: redoStack, + skipNextScroll: true + }; + } + + return arg.prevState; + } + + export function continueExec(arg: NativeEditorReducerArg<ICellAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((cv) => cv.cell.id === arg.payload.data.cellId); + if (index >= 0) { + postActionToExtension(arg, InteractiveWindowMessages.Continue); + } + return arg.prevState; + } + + export function step(arg: NativeEditorReducerArg<ICellAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((cv) => cv.cell.id === arg.payload.data.cellId); + if (index >= 0) { + postActionToExtension(arg, InteractiveWindowMessages.Step); + } + return arg.prevState; + } + + export function runByLine(arg: NativeEditorReducerArg<ICellAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((cv) => cv.cell.id === arg.payload.data.cellId); + if (index >= 0) { + postActionToExtension(arg, InteractiveWindowMessages.RunByLine, { + cell: arg.prevState.cellVMs[index].cell, + expectedExecutionCount: arg.prevState.currentExecutionCount + 1 + }); + const newVM = { + ...arg.prevState.cellVMs[index], + runningByLine: DebugState.Run + }; + const newVMs = [...arg.prevState.cellVMs]; + newVMs[index] = newVM; + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + return arg.prevState; + } + + export function handleBreakState( + arg: NativeEditorReducerArg<{ frames: DebugProtocol.StackFrame[]; cell: ICell }> + ): IMainState { + const index = arg.prevState.cellVMs.findIndex((cv) => cv.cell.id === arg.payload.data.cell.id); + if (index >= 0) { + const newVM = { + ...arg.prevState.cellVMs[index], + runningByLine: DebugState.Break, + currentStack: arg.payload.data.frames + }; + const newVMs = [...arg.prevState.cellVMs]; + newVMs[index] = newVM; + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + return arg.prevState; + } + + export function handleContinue(arg: NativeEditorReducerArg<ICell>): IMainState { + const index = arg.prevState.cellVMs.findIndex((cv) => cv.cell.id === arg.payload.data.id); + if (index >= 0) { + const newVM = { + ...arg.prevState.cellVMs[index], + runningByLine: DebugState.Run, + currentStack: undefined + }; + const newVMs = [...arg.prevState.cellVMs]; + newVMs[index] = newVM; + return { + ...arg.prevState, + cellVMs: newVMs + }; + } + return arg.prevState; + } + + export function startDebugging(arg: NativeEditorReducerArg): IMainState { + return { + ...arg.prevState, + debugging: true + }; + } + + export function stopDebugging(arg: NativeEditorReducerArg): IMainState { + // Clear out any cells that have frames + const index = arg.prevState.cellVMs.findIndex((cvm) => cvm.currentStack); + const newVMs = [...arg.prevState.cellVMs]; + if (index >= 0) { + const newVM = { + ...newVMs[index], + currentStack: undefined + }; + newVMs[index] = newVM; + } + return { + ...arg.prevState, + cellVMs: newVMs, + debugging: false + }; + } +} diff --git a/src/datascience-ui/native-editor/redux/reducers/index.ts b/src/datascience-ui/native-editor/redux/reducers/index.ts new file mode 100644 index 000000000000..0fe8955906f7 --- /dev/null +++ b/src/datascience-ui/native-editor/redux/reducers/index.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { InteractiveWindowMessages } from '../../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { CssMessages, SharedMessages } from '../../../../client/datascience/messages'; +import { CommonEffects } from '../../../interactive-common/redux/reducers/commonEffects'; +import { Kernel } from '../../../interactive-common/redux/reducers/kernel'; +import { Transfer } from '../../../interactive-common/redux/reducers/transfer'; +import { CommonActionType } from '../../../interactive-common/redux/reducers/types'; +import { INativeEditorActionMapping } from '../mapping'; +import { Creation } from './creation'; +import { Effects } from './effects'; +import { Execution } from './execution'; +import { Movement } from './movement'; + +// The list of reducers. 1 per message/action. +export const reducerMap: Partial<INativeEditorActionMapping> = { + // State updates + [CommonActionType.INSERT_ABOVE_AND_FOCUS_NEW_CELL]: Creation.insertAboveAndFocusCell, + [CommonActionType.INSERT_ABOVE_FIRST_AND_FOCUS_NEW_CELL]: Creation.insertAboveFirstAndFocusCell, + [CommonActionType.INSERT_BELOW_AND_FOCUS_NEW_CELL]: Creation.insertBelowAndFocusCell, + [CommonActionType.INSERT_ABOVE]: Creation.insertNewAbove, + [CommonActionType.INSERT_ABOVE_FIRST]: Creation.insertAboveFirst, + [CommonActionType.INSERT_BELOW]: Creation.insertBelow, + [CommonActionType.FOCUS_CELL]: Effects.focusCell, + [CommonActionType.UNFOCUS_CELL]: Effects.unfocusCell, + [CommonActionType.ADD_AND_FOCUS_NEW_CELL]: Creation.addAndFocusCell, + [CommonActionType.ADD_NEW_CELL]: Creation.addNewCell, + [CommonActionType.EXECUTE_CELL_AND_ADVANCE]: Execution.executeCellAndAdvance, + [CommonActionType.EXECUTE_CELL]: Execution.executeCell, + [CommonActionType.EXECUTE_ALL_CELLS]: Execution.executeAllCells, + [CommonActionType.EXECUTE_ABOVE]: Execution.executeAbove, + [CommonActionType.EXECUTE_CELL_AND_BELOW]: Execution.executeCellAndBelow, + [CommonActionType.RESTART_KERNEL]: Kernel.restartKernel, + [CommonActionType.INTERRUPT_KERNEL]: Kernel.interruptKernel, + [InteractiveWindowMessages.ClearAllOutputs]: Execution.clearAllOutputs, + [CommonActionType.EXPORT]: Transfer.exportCells, + [CommonActionType.EXPORT_NOTEBOOK_AS]: Transfer.showExportAsMenu, + [CommonActionType.SAVE]: Transfer.save, + [CommonActionType.SHOW_DATA_VIEWER]: Transfer.showDataViewer, + [CommonActionType.SEND_COMMAND]: Transfer.sendCommand, + [CommonActionType.SELECT_CELL]: Effects.selectCell, + [InteractiveWindowMessages.SelectKernel]: Kernel.selectKernel, + [CommonActionType.SELECT_SERVER]: Kernel.selectJupyterURI, + [CommonActionType.MOVE_CELL_UP]: Movement.moveCellUp, + [CommonActionType.MOVE_CELL_DOWN]: Movement.moveCellDown, + [CommonActionType.DELETE_CELL]: Creation.deleteCell, + [CommonActionType.TOGGLE_LINE_NUMBERS]: Effects.toggleLineNumbers, + [CommonActionType.TOGGLE_OUTPUT]: Effects.toggleOutput, + [CommonActionType.CHANGE_CELL_TYPE]: Execution.changeCellType, + [InteractiveWindowMessages.Undo]: Execution.undo, + [InteractiveWindowMessages.Redo]: Execution.redo, + [CommonActionType.ARROW_UP]: Movement.arrowUp, + [CommonActionType.ARROW_DOWN]: Movement.arrowDown, + [CommonActionType.EDIT_CELL]: Transfer.editCell, + [InteractiveWindowMessages.ShowPlot]: Transfer.showPlot, + [CommonActionType.LINK_CLICK]: Transfer.linkClick, + [CommonActionType.GATHER_CELL]: Transfer.gather, + [CommonActionType.GATHER_CELL_TO_SCRIPT]: Transfer.gatherToScript, + [InteractiveWindowMessages.Gathering]: Transfer.gathering, + [CommonActionType.EDITOR_LOADED]: Transfer.started, + [CommonActionType.LOADED_ALL_CELLS]: Transfer.loadedAllCells, + [CommonActionType.LAUNCH_NOTEBOOK_TRUST_PROMPT]: Transfer.launchNotebookTrustPrompt, + [CommonActionType.UNMOUNT]: Creation.unmount, + [CommonActionType.LOAD_IPYWIDGET_CLASS_SUCCESS]: CommonEffects.handleLoadIPyWidgetClassSuccess, + [CommonActionType.LOAD_IPYWIDGET_CLASS_FAILURE]: CommonEffects.handleLoadIPyWidgetClassFailure, + [CommonActionType.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED]: CommonEffects.notifyAboutUnsupportedWidgetVersions, + [CommonActionType.CONTINUE]: Execution.continueExec, + [CommonActionType.STEP]: Execution.step, + [CommonActionType.RUN_BY_LINE]: Execution.runByLine, + [CommonActionType.OPEN_SETTINGS]: CommonEffects.openSettings, + + // Messages from the webview (some are ignored) + [InteractiveWindowMessages.StartCell]: Creation.startCell, + [InteractiveWindowMessages.FinishCell]: Creation.finishCell, + [InteractiveWindowMessages.UpdateCellWithExecutionResults]: Creation.updateCell, + [InteractiveWindowMessages.NotebookDirty]: CommonEffects.notebookDirty, + [InteractiveWindowMessages.NotebookClean]: CommonEffects.notebookClean, + [InteractiveWindowMessages.LoadAllCells]: Creation.loadAllCells, + [InteractiveWindowMessages.TrustNotebookComplete]: CommonEffects.trustNotebook, + [InteractiveWindowMessages.NotebookRunAllCells]: Execution.executeAllCells, + [InteractiveWindowMessages.NotebookRunSelectedCell]: Execution.executeSelectedCell, + [InteractiveWindowMessages.NotebookAddCellBelow]: Creation.addAndFocusCell, + [InteractiveWindowMessages.DoSave]: Transfer.save, + [InteractiveWindowMessages.DeleteAllCells]: Creation.deleteAllCells, + [InteractiveWindowMessages.Undo]: Execution.undo, + [InteractiveWindowMessages.Redo]: Execution.redo, + [InteractiveWindowMessages.StartProgress]: CommonEffects.startProgress, + [InteractiveWindowMessages.StopProgress]: CommonEffects.stopProgress, + [SharedMessages.UpdateSettings]: Effects.updateSettings, + [InteractiveWindowMessages.Activate]: CommonEffects.activate, + [InteractiveWindowMessages.RestartKernel]: Kernel.handleRestarted, + [CssMessages.GetCssResponse]: CommonEffects.handleCss, + [InteractiveWindowMessages.MonacoReady]: CommonEffects.monacoReady, + [CssMessages.GetMonacoThemeResponse]: CommonEffects.monacoThemeChange, + [InteractiveWindowMessages.UpdateModel]: Creation.handleUpdate, + [InteractiveWindowMessages.UpdateKernel]: Kernel.updateStatus, + [SharedMessages.LocInit]: CommonEffects.handleLocInit, + [InteractiveWindowMessages.UpdateDisplayData]: CommonEffects.handleUpdateDisplayData, + [InteractiveWindowMessages.ShowBreak]: Execution.handleBreakState, + [InteractiveWindowMessages.ShowContinue]: Execution.handleContinue, + [InteractiveWindowMessages.StartDebugging]: Execution.startDebugging, + [InteractiveWindowMessages.StopDebugging]: Execution.stopDebugging +}; diff --git a/src/datascience-ui/native-editor/redux/reducers/movement.ts b/src/datascience-ui/native-editor/redux/reducers/movement.ts new file mode 100644 index 000000000000..bacac77d8398 --- /dev/null +++ b/src/datascience-ui/native-editor/redux/reducers/movement.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { CursorPos, IMainState } from '../../../interactive-common/mainState'; +import { queueIncomingActionWithPayload } from '../../../interactive-common/redux/helpers'; +import { Helpers } from '../../../interactive-common/redux/reducers/helpers'; +import { Transfer } from '../../../interactive-common/redux/reducers/transfer'; +import { CommonActionType, ICellAction, ICodeAction } from '../../../interactive-common/redux/reducers/types'; +import { NativeEditorReducerArg } from '../mapping'; + +export namespace Movement { + export function swapCells(arg: NativeEditorReducerArg<{ firstCellId: string; secondCellId: string }>) { + const newVMs = [...arg.prevState.cellVMs]; + const first = newVMs.findIndex((cvm) => cvm.cell.id === arg.payload.data.firstCellId); + const second = newVMs.findIndex((cvm) => cvm.cell.id === arg.payload.data.secondCellId); + if (first >= 0 && second >= 0 && first !== second) { + const temp = newVMs[first]; + newVMs[first] = newVMs[second]; + newVMs[second] = temp; + Transfer.postModelSwap(arg, arg.payload.data.firstCellId, arg.payload.data.secondCellId); + return { + ...arg.prevState, + cellVMs: newVMs, + undoStack: Helpers.pushStack(arg.prevState.undoStack, arg.prevState.cellVMs) + }; + } + + return arg.prevState; + } + + export function moveCellUp(arg: NativeEditorReducerArg<ICellAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((cvm) => cvm.cell.id === arg.payload.data.cellId); + if (index > 0 && arg.payload.data.cellId) { + return swapCells({ + ...arg, + payload: { + ...arg.payload, + data: { + firstCellId: arg.prevState.cellVMs[index - 1].cell.id, + secondCellId: arg.payload.data.cellId + } + } + }); + } + + return arg.prevState; + } + + export function moveCellDown(arg: NativeEditorReducerArg<ICellAction>): IMainState { + const newVMs = [...arg.prevState.cellVMs]; + const index = newVMs.findIndex((cvm) => cvm.cell.id === arg.payload.data.cellId); + if (index < newVMs.length - 1 && arg.payload.data.cellId) { + return swapCells({ + ...arg, + payload: { + ...arg.payload, + data: { + firstCellId: arg.payload.data.cellId, + secondCellId: arg.prevState.cellVMs[index + 1].cell.id + } + } + }); + } + + return arg.prevState; + } + + export function arrowUp(arg: NativeEditorReducerArg<ICodeAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (index > 0) { + queueIncomingActionWithPayload(arg, CommonActionType.SELECT_CELL, { + cellId: arg.prevState.cellVMs[index - 1].cell.id, + cursorPos: CursorPos.Bottom + }); + } + + return arg.prevState; + } + + export function arrowDown(arg: NativeEditorReducerArg<ICodeAction>): IMainState { + const index = arg.prevState.cellVMs.findIndex((c) => c.cell.id === arg.payload.data.cellId); + if (index < arg.prevState.cellVMs.length - 1) { + queueIncomingActionWithPayload(arg, CommonActionType.SELECT_CELL, { + cellId: arg.prevState.cellVMs[index + 1].cell.id, + cursorPos: CursorPos.Top + }); + } + + return arg.prevState; + } +} diff --git a/src/datascience-ui/native-editor/redux/store.ts b/src/datascience-ui/native-editor/redux/store.ts new file mode 100644 index 000000000000..53f228f59411 --- /dev/null +++ b/src/datascience-ui/native-editor/redux/store.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as ReduxCommon from '../../interactive-common/redux/store'; +import { PostOffice } from '../../react-common/postOffice'; +import { reducerMap } from './reducers'; + +// This special version uses the reducer map from the INativeEditorMapping +export function createStore(skipDefault: boolean, baseTheme: string, testMode: boolean, postOffice: PostOffice) { + return ReduxCommon.createStore(skipDefault, baseTheme, testMode, true, true, reducerMap, postOffice); +} diff --git a/src/datascience-ui/native-editor/toolbar.tsx b/src/datascience-ui/native-editor/toolbar.tsx new file mode 100644 index 000000000000..63ffab3abf18 --- /dev/null +++ b/src/datascience-ui/native-editor/toolbar.tsx @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { NativeMouseCommandTelemetry } from '../../client/datascience/constants'; +import { IDataScienceExtraSettings } from '../../client/datascience/types'; +import { JupyterInfo } from '../interactive-common/jupyterInfo'; +import { + getSelectedAndFocusedInfo, + IFont, + IServerState, + SelectionAndFocusedInfo, + ServerStatus +} from '../interactive-common/mainState'; +import { IStore } from '../interactive-common/redux/store'; +import { Image, ImageName } from '../react-common/image'; +import { ImageButton } from '../react-common/imageButton'; +import { getLocString } from '../react-common/locReactSide'; +import './nativeEditor.less'; +import { actionCreators } from './redux/actions'; + +type INativeEditorDataProps = { + busy: boolean; + dirty: boolean; + baseTheme: string; + cellCount: number; + font: IFont; + kernel: IServerState; + selectionFocusedInfo: SelectionAndFocusedInfo; + variablesVisible: boolean; + settings?: IDataScienceExtraSettings; +}; +export type INativeEditorToolbarProps = INativeEditorDataProps & { + sendCommand: typeof actionCreators.sendCommand; + clearAllOutputs: typeof actionCreators.clearAllOutputs; + export: typeof actionCreators.export; + exportAs: typeof actionCreators.exportAs; + addCell: typeof actionCreators.addCell; + save: typeof actionCreators.save; + executeAllCells: typeof actionCreators.executeAllCells; + toggleVariableExplorer: typeof actionCreators.toggleVariableExplorer; + setVariableExplorerHeight: typeof actionCreators.setVariableExplorerHeight; + executeAbove: typeof actionCreators.executeAbove; + executeCellAndBelow: typeof actionCreators.executeCellAndBelow; + restartKernel: typeof actionCreators.restartKernel; + interruptKernel: typeof actionCreators.interruptKernel; + selectKernel: typeof actionCreators.selectKernel; + selectServer: typeof actionCreators.selectServer; + launchNotebookTrustPrompt: typeof actionCreators.launchNotebookTrustPrompt; + isNotebookTrusted: boolean; +}; + +function mapStateToProps(state: IStore): INativeEditorDataProps { + return { + ...state.main, + cellCount: state.main.cellVMs.length, + selectionFocusedInfo: getSelectedAndFocusedInfo(state.main), + variablesVisible: state.variables.visible + }; +} + +export class Toolbar extends React.PureComponent<INativeEditorToolbarProps> { + constructor(props: INativeEditorToolbarProps) { + super(props); + } + + // tslint:disable: react-this-binding-issue + // tslint:disable-next-line: max-func-body-length + public render() { + const selectedInfo = this.props.selectionFocusedInfo; + + const addCell = () => { + setTimeout(() => this.props.addCell(), 1); + this.props.sendCommand(NativeMouseCommandTelemetry.AddToEnd); + }; + const runAll = () => { + // Run all cells currently available. + this.props.executeAllCells(); + this.props.sendCommand(NativeMouseCommandTelemetry.RunAll); + }; + const save = () => { + this.props.save(); + this.props.sendCommand(NativeMouseCommandTelemetry.Save); + }; + const toggleVariableExplorer = () => { + this.props.toggleVariableExplorer(); + this.props.sendCommand(NativeMouseCommandTelemetry.ToggleVariableExplorer); + }; + const variableExplorerTooltip = this.props.variablesVisible + ? getLocString('DataScience.collapseVariableExplorerTooltip', 'Hide variables active in jupyter kernel') + : getLocString('DataScience.expandVariableExplorerTooltip', 'Show variables active in jupyter kernel'); + const runAbove = () => { + if (selectedInfo.selectedCellId) { + this.props.executeAbove(selectedInfo.selectedCellId); + this.props.sendCommand(NativeMouseCommandTelemetry.RunAbove); + } + }; + const runBelow = () => { + if (selectedInfo.selectedCellId && typeof selectedInfo.selectedCellIndex === 'number') { + // tslint:disable-next-line: no-suspicious-comment + // TODO: Is the source going to be up to date during run below? + this.props.executeCellAndBelow(selectedInfo.selectedCellId); + this.props.sendCommand(NativeMouseCommandTelemetry.RunBelow); + } + }; + const selectKernel = () => { + this.props.selectKernel(); + this.props.sendCommand(NativeMouseCommandTelemetry.SelectKernel); + }; + const selectServer = () => { + this.props.selectServer(); + this.props.sendCommand(NativeMouseCommandTelemetry.SelectServer); + }; + const launchNotebookTrustPrompt = () => { + if (!this.props.isNotebookTrusted) { + this.props.launchNotebookTrustPrompt(); + } + }; + const canRunAbove = (selectedInfo.selectedCellIndex ?? -1) > 0; + const canRunBelow = + (selectedInfo.selectedCellIndex ?? -1) < this.props.cellCount - 1 && + (selectedInfo.selectedCellId || '').length > 0; + + const canRestartAndInterruptKernel = this.props.kernel.jupyterServerStatus !== ServerStatus.NotStarted; + + return ( + <div id="toolbar-panel"> + <div className="toolbar-menu-bar"> + <div className="toolbar-menu-bar-child"> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={runAll} + disabled={this.props.busy || !this.props.isNotebookTrusted} + className="native-button" + tooltip={getLocString('DataScience.runAll', 'Run All Cells')} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.RunAll} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={runAbove} + disabled={!canRunAbove || this.props.busy || !this.props.isNotebookTrusted} + className="native-button" + tooltip={getLocString('DataScience.runAbove', 'Run cells above')} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.RunAbove} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={runBelow} + disabled={!canRunBelow || this.props.busy || !this.props.isNotebookTrusted} + className="native-button" + tooltip={getLocString('DataScience.runBelow', 'Run cell and below')} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.RunBelow} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.props.restartKernel} + disabled={!canRestartAndInterruptKernel || !this.props.isNotebookTrusted} + className="native-button" + tooltip={getLocString('DataScience.restartServer', 'Restart IPython kernel')} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.Restart} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.props.interruptKernel} + disabled={!canRestartAndInterruptKernel || !this.props.isNotebookTrusted} + className="native-button" + tooltip={getLocString('DataScience.interruptKernel', 'Interrupt IPython kernel')} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.Interrupt} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={addCell} + disabled={!this.props.isNotebookTrusted} + className="native-button" + tooltip={getLocString('DataScience.addNewCell', 'Insert cell')} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.InsertBelow} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.props.clearAllOutputs} + disabled={!this.props.cellCount || !this.props.isNotebookTrusted} + className="native-button" + tooltip={getLocString('DataScience.clearAllOutput', 'Clear All Output')} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.ClearAllOutput} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={toggleVariableExplorer} + disabled={!this.props.isNotebookTrusted} + className="native-button" + tooltip={variableExplorerTooltip} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.VariableExplorer} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={save} + disabled={!this.props.dirty || !this.props.isNotebookTrusted} + className="native-button" + tooltip={getLocString('DataScience.save', 'Save File')} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.SaveAs} + /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.props.exportAs} + disabled={!this.props.cellCount || this.props.busy || !this.props.isNotebookTrusted} + className="native-button" + tooltip={getLocString('DataScience.notebookExportAs', 'Export as')} + > + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.ExportToPython} + /> + </ImageButton> + </div> + <JupyterInfo + baseTheme={this.props.baseTheme} + font={this.props.font} + kernel={this.props.kernel} + selectServer={selectServer} + selectKernel={selectKernel} + shouldShowTrustMessage={true} + isNotebookTrusted={this.props.isNotebookTrusted} + launchNotebookTrustPrompt={launchNotebookTrustPrompt} + settings={this.props.settings} + /> + </div> + <div className="toolbar-divider" /> + </div> + ); + } +} + +export const ToolbarComponent = connect(mapStateToProps, actionCreators)(Toolbar); diff --git a/src/datascience-ui/plot/index.html b/src/datascience-ui/plot/index.html new file mode 100644 index 000000000000..9dd3ffa71749 --- /dev/null +++ b/src/datascience-ui/plot/index.html @@ -0,0 +1,356 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"> + <meta name="theme-color" content="#000000"> + <title>Python Extension Plot Viewer</title> + <base href="<%= htmlWebpackPlugin.options.indexUrl %>"> + <style id='default-styles'> +:root { --background-color: #ffffff; + --comment-color: green; +--color: #000000; +--font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; +--font-size: 13px; +--font-weight: normal; +--link-active-color: #006ab1; +--link-color: #006ab1; +--vscode-activityBar-background: #2c2c2c; +--vscode-activityBar-dropBackground: rgba(255, 255, 255, 0.12); +--vscode-activityBar-foreground: #ffffff; +--vscode-activityBar-inactiveForeground: rgba(255, 255, 255, 0.6); +--vscode-activityBarBadge-background: #007acc; +--vscode-activityBarBadge-foreground: #ffffff; +--vscode-badge-background: #c4c4c4; +--vscode-badge-foreground: #333333; +--vscode-breadcrumb-activeSelectionForeground: #4e4e4e; +--vscode-breadcrumb-background: #ffffff; +--vscode-breadcrumb-focusForeground: #4e4e4e; +--vscode-breadcrumb-foreground: rgba(97, 97, 97, 0.8); +--vscode-breadcrumbPicker-background: #f3f3f3; +--vscode-button-background: #007acc; +--vscode-button-foreground: #ffffff; +--vscode-button-hoverBackground: #0062a3; +--vscode-debugExceptionWidget-background: #f1dfde; +--vscode-debugExceptionWidget-border: #a31515; +--vscode-debugToolBar-background: #f3f3f3; +--vscode-descriptionForeground: #717171; +--vscode-diffEditor-insertedTextBackground: rgba(155, 185, 85, 0.2); +--vscode-diffEditor-removedTextBackground: rgba(255, 0, 0, 0.2); +--vscode-dropdown-background: #ffffff; +--vscode-dropdown-border: #cecece; +--vscode-editor-background: #ffffff; +--vscode-editor-findMatchBackground: #a8ac94; +--vscode-editor-findMatchHighlightBackground: rgba(234, 92, 0, 0.33); +--vscode-editor-findRangeHighlightBackground: rgba(180, 180, 180, 0.3); +--vscode-editor-font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; +--vscode-editor-font-size: 13px; +--vscode-editor-font-weight: normal; +--vscode-editor-foreground: #000000; +--vscode-editor-hoverHighlightBackground: rgba(173, 214, 255, 0.15); +--vscode-editor-inactiveSelectionBackground: #e5ebf1; +--vscode-editor-lineHighlightBorder: #eeeeee; +--vscode-editor-rangeHighlightBackground: rgba(253, 255, 0, 0.2); +--vscode-editor-selectionBackground: #add6ff; +--vscode-editor-selectionHighlightBackground: rgba(173, 214, 255, 0.3); +--vscode-editor-snippetFinalTabstopHighlightBorder: rgba(10, 50, 100, 0.5); +--vscode-editor-snippetTabstopHighlightBackground: rgba(10, 50, 100, 0.2); +--vscode-editor-wordHighlightBackground: rgba(87, 87, 87, 0.25); +--vscode-editor-wordHighlightStrongBackground: rgba(14, 99, 156, 0.25); +--vscode-editorActiveLineNumber-foreground: #0b216f; +--vscode-editorBracketMatch-background: rgba(0, 100, 0, 0.1); +--vscode-editorBracketMatch-border: #b9b9b9; +--vscode-editorCodeLens-foreground: #999999; +--vscode-editorCursor-foreground: #000000; +--vscode-editorError-foreground: #d60a0a; +--vscode-editorGroup-border: #e7e7e7; +--vscode-editorGroup-dropBackground: rgba(38, 119, 203, 0.18); +--vscode-editorGroupHeader-noTabsBackground: #ffffff; +--vscode-editorGroupHeader-tabsBackground: #f3f3f3; +--vscode-editorGutter-addedBackground: #81b88b; +--vscode-editorGutter-background: #ffffff; +--vscode-editorGutter-commentRangeForeground: #c5c5c5; +--vscode-editorGutter-deletedBackground: #ca4b51; +--vscode-editorGutter-modifiedBackground: #66afe0; +--vscode-editorHint-foreground: #6c6c6c; +--vscode-editorHoverWidget-background: #f3f3f3; +--vscode-editorHoverWidget-border: #c8c8c8; +--vscode-editorIndentGuide-activeBackground: #939393; +--vscode-editorIndentGuide-background: #d3d3d3; +--vscode-editorInfo-foreground: #008000; +--vscode-editorLineNumber-activeForeground: #0b216f; +--vscode-editorLineNumber-foreground: #237893; +--vscode-editorLink-activeForeground: #0000ff; +--vscode-editorMarkerNavigation-background: #ffffff; +--vscode-editorMarkerNavigationError-background: #d60a0a; +--vscode-editorMarkerNavigationInfo-background: #008000; +--vscode-editorMarkerNavigationWarning-background: #117711; +--vscode-editorOverviewRuler-addedForeground: rgba(0, 122, 204, 0.6); +--vscode-editorOverviewRuler-border: rgba(127, 127, 127, 0.3); +--vscode-editorOverviewRuler-bracketMatchForeground: #a0a0a0; +--vscode-editorOverviewRuler-commonContentForeground: rgba(96, 96, 96, 0.4); +--vscode-editorOverviewRuler-currentContentForeground: rgba(64, 200, 174, 0.5); +--vscode-editorOverviewRuler-deletedForeground: rgba(0, 122, 204, 0.6); +--vscode-editorOverviewRuler-errorForeground: rgba(255, 18, 18, 0.7); +--vscode-editorOverviewRuler-findMatchForeground: rgba(246, 185, 77, 0.7); +--vscode-editorOverviewRuler-incomingContentForeground: rgba(64, 166, 255, 0.5); +--vscode-editorOverviewRuler-infoForeground: rgba(18, 18, 136, 0.7); +--vscode-editorOverviewRuler-modifiedForeground: rgba(0, 122, 204, 0.6); +--vscode-editorOverviewRuler-rangeHighlightForeground: rgba(0, 122, 204, 0.6); +--vscode-editorOverviewRuler-selectionHighlightForeground: rgba(160, 160, 160, 0.8); +--vscode-editorOverviewRuler-warningForeground: rgba(18, 136, 18, 0.7); +--vscode-editorOverviewRuler-wordHighlightForeground: rgba(160, 160, 160, 0.8); +--vscode-editorOverviewRuler-wordHighlightStrongForeground: rgba(192, 160, 192, 0.8); +--vscode-editorPane-background: #ffffff; +--vscode-editorRuler-foreground: #d3d3d3; +--vscode-editorSuggestWidget-background: #f3f3f3; +--vscode-editorSuggestWidget-border: #c8c8c8; +--vscode-editorSuggestWidget-foreground: #000000; +--vscode-editorSuggestWidget-highlightForeground: #0066bf; +--vscode-editorSuggestWidget-selectedBackground: #d6ebff; +--vscode-editorUnnecessaryCode-opacity: rgba(0, 0, 0, 0.47); +--vscode-editorWarning-foreground: #117711; +--vscode-editorWhitespace-foreground: rgba(51, 51, 51, 0.2); +--vscode-editorWidget-background: #f3f3f3; +--vscode-editorWidget-border: #c8c8c8; +--vscode-errorForeground: #a1260d; +--vscode-extensionButton-prominentBackground: #327e36; +--vscode-extensionButton-prominentForeground: #ffffff; +--vscode-extensionButton-prominentHoverBackground: #28632b; +--vscode-focusBorder: rgba(0, 122, 204, 0.4); +--vscode-foreground: #616161; +--vscode-gitDecoration-addedResourceForeground: #587c0c; +--vscode-gitDecoration-conflictingResourceForeground: #6c6cc4; +--vscode-gitDecoration-deletedResourceForeground: #ad0707; +--vscode-gitDecoration-ignoredResourceForeground: #8e8e90; +--vscode-gitDecoration-modifiedResourceForeground: #895503; +--vscode-gitDecoration-submoduleResourceForeground: #1258a7; +--vscode-gitDecoration-untrackedResourceForeground: #007100; +--vscode-input-background: #ffffff; +--vscode-input-foreground: #616161; +--vscode-input-placeholderForeground: #767676; +--vscode-inputOption-activeBorder: #007acc; +--vscode-inputValidation-errorBackground: #f2dede; +--vscode-inputValidation-errorBorder: #be1100; +--vscode-inputValidation-infoBackground: #d6ecf2; +--vscode-inputValidation-infoBorder: #007acc; +--vscode-inputValidation-warningBackground: #f6f5d2; +--vscode-inputValidation-warningBorder: #b89500; +--vscode-list-activeSelectionBackground: #2477ce; +--vscode-list-activeSelectionForeground: #ffffff; +--vscode-list-dropBackground: #d6ebff; +--vscode-list-errorForeground: #b01011; +--vscode-list-focusBackground: #d6ebff; +--vscode-list-highlightForeground: #0066bf; +--vscode-list-hoverBackground: #e8e8e8; +--vscode-list-inactiveFocusBackground: #d8dae6; +--vscode-list-inactiveSelectionBackground: #e4e6f1; +--vscode-list-invalidItemForeground: #b89500; +--vscode-list-warningForeground: #117711; +--vscode-menu-background: #ffffff; +--vscode-menu-selectionBackground: #2477ce; +--vscode-menu-selectionForeground: #ffffff; +--vscode-menu-separatorBackground: #888888; +--vscode-menubar-selectionBackground: rgba(0, 0, 0, 0.1); +--vscode-menubar-selectionForeground: #333333; +--vscode-merge-commonContentBackground: rgba(96, 96, 96, 0.16); +--vscode-merge-commonHeaderBackground: rgba(96, 96, 96, 0.4); +--vscode-merge-currentContentBackground: rgba(64, 200, 174, 0.2); +--vscode-merge-currentHeaderBackground: rgba(64, 200, 174, 0.5); +--vscode-merge-incomingContentBackground: rgba(64, 166, 255, 0.2); +--vscode-merge-incomingHeaderBackground: rgba(64, 166, 255, 0.5); +--vscode-notificationCenterHeader-background: #e7e7e7; +--vscode-notificationLink-foreground: #006ab1; +--vscode-notifications-background: #f3f3f3; +--vscode-notifications-border: #e7e7e7; +--vscode-panel-background: #ffffff; +--vscode-panel-border: rgba(128, 128, 128, 0.35); +--vscode-panel-dropBackground: rgba(38, 119, 203, 0.18); +--vscode-panelTitle-activeBorder: rgba(128, 128, 128, 0.35); +--vscode-panelTitle-activeForeground: #424242; +--vscode-panelTitle-inactiveForeground: rgba(66, 66, 66, 0.75); +--vscode-peekView-border: #007acc; +--vscode-peekViewEditor-background: #f2f8fc; +--vscode-peekViewEditor-matchHighlightBackground: rgba(245, 216, 2, 0.87); +--vscode-peekViewEditorGutter-background: #f2f8fc; +--vscode-peekViewResult-background: #f3f3f3; +--vscode-peekViewResult-fileForeground: #1e1e1e; +--vscode-peekViewResult-lineForeground: #646465; +--vscode-peekViewResult-matchHighlightBackground: rgba(234, 92, 0, 0.3); +--vscode-peekViewResult-selectionBackground: rgba(51, 153, 255, 0.2); +--vscode-peekViewResult-selectionForeground: #6c6c6c; +--vscode-peekViewTitle-background: #ffffff; +--vscode-peekViewTitleDescription-foreground: rgba(108, 108, 108, 0.7); +--vscode-peekViewTitleLabel-foreground: #333333; +--vscode-pickerGroup-border: #cccedb; +--vscode-pickerGroup-foreground: #0066bf; +--vscode-progressBar-background: #0e70c0; +--vscode-scrollbar-shadow: #dddddd; +--vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6); +--vscode-scrollbarSlider-background: rgba(100, 100, 100, 0.4); +--vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7); +--vscode-settings-checkboxBackground: #ffffff; +--vscode-settings-checkboxBorder: #cecece; +--vscode-settings-dropdownBackground: #ffffff; +--vscode-settings-dropdownBorder: #cecece; +--vscode-settings-dropdownListBorder: #c8c8c8; +--vscode-settings-headerForeground: #444444; +--vscode-settings-modifiedItemIndicator: #66afe0; +--vscode-settings-numberInputBackground: #ffffff; +--vscode-settings-numberInputBorder: #cecece; +--vscode-settings-numberInputForeground: #616161; +--vscode-settings-textInputBackground: #ffffff; +--vscode-settings-textInputBorder: #cecece; +--vscode-settings-textInputForeground: #616161; +--vscode-sideBar-background: #f3f3f3; +--vscode-sideBar-dropBackground: rgba(255, 255, 255, 0.12); +--vscode-sideBarSectionHeader-background: rgba(128, 128, 128, 0.2); +--vscode-sideBarTitle-foreground: #6f6f6f; +--vscode-statusBar-background: #007acc; +--vscode-statusBar-debuggingBackground: #cc6633; +--vscode-statusBar-debuggingForeground: #ffffff; +--vscode-statusBar-foreground: #ffffff; +--vscode-statusBar-noFolderBackground: #68217a; +--vscode-statusBar-noFolderForeground: #ffffff; +--vscode-statusBarItem-activeBackground: rgba(255, 255, 255, 0.18); +--vscode-statusBarItem-hoverBackground: rgba(255, 255, 255, 0.12); +--vscode-statusBarItem-prominentBackground: #388a34; +--vscode-statusBarItem-prominentHoverBackground: #369432; +--vscode-tab-activeBackground: #ffffff; +--vscode-tab-activeForeground: #333333; +--vscode-tab-border: #f3f3f3; +--vscode-tab-inactiveBackground: #ececec; +--vscode-tab-inactiveForeground: rgba(51, 51, 51, 0.5); +--vscode-tab-unfocusedActiveForeground: rgba(51, 51, 51, 0.7); +--vscode-tab-unfocusedInactiveForeground: rgba(51, 51, 51, 0.25); +--vscode-terminal-ansiBlack: #000000; +--vscode-terminal-ansiBlue: #0451a5; +--vscode-terminal-ansiBrightBlack: #666666; +--vscode-terminal-ansiBrightBlue: #0451a5; +--vscode-terminal-ansiBrightCyan: #0598bc; +--vscode-terminal-ansiBrightGreen: #14ce14; +--vscode-terminal-ansiBrightMagenta: #bc05bc; +--vscode-terminal-ansiBrightRed: #cd3131; +--vscode-terminal-ansiBrightWhite: #a5a5a5; +--vscode-terminal-ansiBrightYellow: #b5ba00; +--vscode-terminal-ansiCyan: #0598bc; +--vscode-terminal-ansiGreen: #00bc00; +--vscode-terminal-ansiMagenta: #bc05bc; +--vscode-terminal-ansiRed: #cd3131; +--vscode-terminal-ansiWhite: #555555; +--vscode-terminal-ansiYellow: #949800; +--vscode-terminal-background: #ffffff; +--vscode-terminal-border: rgba(128, 128, 128, 0.35); +--vscode-terminal-foreground: #333333; +--vscode-terminal-selectionBackground: rgba(0, 0, 0, 0.25); +--vscode-textBlockQuote-background: rgba(127, 127, 127, 0.1); +--vscode-textBlockQuote-border: rgba(0, 122, 204, 0.5); +--vscode-textCodeBlock-background: rgba(220, 220, 220, 0.4); +--vscode-textLink-activeForeground: #006ab1; +--vscode-textLink-foreground: #006ab1; +--vscode-textPreformat-foreground: #a31515; +--vscode-textSeparator-foreground: rgba(0, 0, 0, 0.18); +--vscode-titleBar-activeBackground: #dddddd; +--vscode-titleBar-activeForeground: #333333; +--vscode-titleBar-inactiveBackground: rgba(221, 221, 221, 0.6); +--vscode-titleBar-inactiveForeground: rgba(51, 51, 51, 0.6); +--vscode-widget-shadow: #a8a8a8; +--code-font-family: 'Comic-Sans'; +--code-font-size: 15px; +} + + body { + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + font-family: var(--vscode-editor-font-family); + font-weight: var(--vscode-editor-font-weight); + font-size: var(--vscode-editor-font-size); + margin: 0; + padding: 0 20px; + } + + img { + max-width: 100%; + max-height: 100%; + } + + a { + color: var(--vscode-textLink-foreground); + } + + a:hover { + color: var(--vscode-textLink-activeForeground); + } + + a:focus, + input:focus, + select:focus, + textarea:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; + } + + code { + color: var(--vscode-textPreformat-foreground); + } + + blockquote { + background: var(--vscode-textBlockQuote-background); + border-color: var(--vscode-textBlockQuote-border); + } + + ::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + ::-webkit-scrollbar-thumb { + background-color: rgba(121, 121, 121, 0.4); + } + body.vscode-light::-webkit-scrollbar-thumb { + background-color: rgba(100, 100, 100, 0.4); + } + body.vscode-high-contrast::-webkit-scrollbar-thumb { + background-color: rgba(111, 195, 223, 0.3); + } + + ::-webkit-scrollbar-thumb:hover { + background-color: rgba(100, 100, 100, 0.7); + } + body.vscode-light::-webkit-scrollbar-thumb:hover { + background-color: rgba(100, 100, 100, 0.7); + } + body.vscode-high-contrast::-webkit-scrollbar-thumb:hover { + background-color: rgba(111, 195, 223, 0.8); + } + + ::-webkit-scrollbar-thumb:active { + background-color: rgba(85, 85, 85, 0.8); + } + body.vscode-light::-webkit-scrollbar-thumb:active { + background-color: rgba(0, 0, 0, 0.6); + } + body.vscode-high-contrast::-webkit-scrollbar-thumb:active { + background-color: rgba(111, 195, 223, 0.8); + } + </style> + + </head> + <body> + <div id="root"></div> + <script type="text/javascript"> + function resolvePath(relativePath) { + if (relativePath && relativePath[0] == '.' && relativePath[1] != '.') { + return "<%= htmlWebpackPlugin.options.imageBaseUrl %>" + relativePath.substring(1); + } + + return "<%= htmlWebpackPlugin.options.imageBaseUrl %>" + relativePath; + } + function getInitialSettings() { + return { allowInput: true, + extraSettings: { editorCursor: 'block', editorCursorBlink: 'blink' } + }; + } + </script> + </body> +</html> diff --git a/src/datascience-ui/plot/index.tsx b/src/datascience-ui/plot/index.tsx new file mode 100644 index 000000000000..aa6136295e4e --- /dev/null +++ b/src/datascience-ui/plot/index.tsx @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// This must be on top, do not change. Required by webpack. +import '../common/main'; +// This must be on top, do not change. Required by webpack. + +// tslint:disable-next-line: ordered-imports +import '../common/index.css'; + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { IVsCodeApi } from '../react-common/postOffice'; +import { detectBaseTheme } from '../react-common/themeDetector'; +import { MainPanel } from './mainPanel'; + +// This special function talks to vscode from a web panel +export declare function acquireVsCodeApi(): IVsCodeApi; + +const baseTheme = detectBaseTheme(); + +// tslint:disable:no-typeof-undefined +ReactDOM.render( + <MainPanel baseTheme={baseTheme} skipDefault={typeof acquireVsCodeApi !== 'undefined'} />, // Turn this back off when we have real variable explorer data + document.getElementById('root') as HTMLElement +); diff --git a/src/datascience-ui/plot/mainPanel.css b/src/datascience-ui/plot/mainPanel.css new file mode 100644 index 000000000000..aa507ba5a6b9 --- /dev/null +++ b/src/datascience-ui/plot/mainPanel.css @@ -0,0 +1,13 @@ + +.main-panel { + position: absolute; + bottom: 0; + top: 0; + left: 0; + right: 0; + font-size: var(--code-font-size); + font-family: var(--code-font-family); + background-color: var(--vscode-editor-background); + overflow: hidden; +} + diff --git a/src/datascience-ui/plot/mainPanel.tsx b/src/datascience-ui/plot/mainPanel.tsx new file mode 100644 index 000000000000..a706aabe6f61 --- /dev/null +++ b/src/datascience-ui/plot/mainPanel.tsx @@ -0,0 +1,428 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import './mainPanel.css'; + +import * as React from 'react'; +import { Tool, Value } from 'react-svg-pan-zoom'; +import * as uuid from 'uuid/v4'; + +import { createDeferred } from '../../client/common/utils/async'; +import { RegExpValues } from '../../client/datascience/constants'; +import { SharedMessages } from '../../client/datascience/messages'; +import { IPlotViewerMapping, PlotViewerMessages } from '../../client/datascience/plotting/types'; +import { IDataScienceExtraSettings } from '../../client/datascience/types'; +import { storeLocStrings } from '../react-common/locReactSide'; +import { IMessageHandler, PostOffice } from '../react-common/postOffice'; +import { getDefaultSettings } from '../react-common/settingsReactSide'; +import { StyleInjector } from '../react-common/styleInjector'; +import { SvgList } from '../react-common/svgList'; +import { SvgViewer } from '../react-common/svgViewer'; +import { TestSvg } from './testSvg'; +import { Toolbar } from './toolbar'; + +// Our css has to come after in order to override body styles +export interface IMainPanelProps { + skipDefault?: boolean; + baseTheme: string; + testMode?: boolean; +} + +interface ISize { + width: string; + height: string; +} + +//tslint:disable:no-any +interface IMainPanelState { + images: string[]; + thumbnails: string[]; + sizes: ISize[]; + values: (Value | undefined)[]; + ids: string[]; + currentImage: number; + tool: Tool; + forceDark?: boolean; + settings?: IDataScienceExtraSettings; +} + +const PanKeyboardSize = 10; + +export class MainPanel extends React.Component<IMainPanelProps, IMainPanelState> implements IMessageHandler { + private container: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>(); + private viewer: React.RefObject<SvgViewer> = React.createRef<SvgViewer>(); + private postOffice: PostOffice = new PostOffice(); + private currentValue: Value | undefined; + + // tslint:disable-next-line:max-func-body-length + constructor(props: IMainPanelProps, _state: IMainPanelState) { + super(props); + const images = !props.skipDefault ? [TestSvg, TestSvg, TestSvg] : []; + const thumbnails = images.map(this.generateThumbnail); + const sizes = images.map(this.extractSize); + const values = images.map((_i) => undefined); + const ids = images.map((_i) => uuid()); + + this.state = { + images, + thumbnails, + sizes, + values, + ids, + tool: 'pan', + currentImage: images.length > 0 ? 0 : -1, + settings: this.props.testMode ? getDefaultSettings() : undefined + }; + } + + public componentWillMount() { + // Add ourselves as a handler for the post office + this.postOffice.addHandler(this); + + // Tell the plot viewer code we have started. + this.postOffice.sendMessage<IPlotViewerMapping>(PlotViewerMessages.Started); + + // Listen to key events + window.addEventListener('keydown', this.onKeyDown); + } + + public componentWillUnmount() { + this.postOffice.removeHandler(this); + this.postOffice.dispose(); + // Stop listening to key events + window.removeEventListener('keydown', this.onKeyDown); + } + + public render = () => { + if (this.state.settings) { + const baseTheme = this.computeBaseTheme(); + return ( + <div className="main-panel" role="group" ref={this.container}> + <StyleInjector + expectingDark={this.props.baseTheme !== 'vscode-light'} + settings={this.state.settings} + darkChanged={this.darkChanged} + postOffice={this.postOffice} + /> + {this.renderToolbar(baseTheme)} + {this.renderThumbnails(baseTheme)} + {this.renderPlot(baseTheme)} + </div> + ); + } else { + return null; + } + }; + + // tslint:disable-next-line:no-any + public handleMessage = (msg: string, payload?: any) => { + switch (msg) { + case PlotViewerMessages.SendPlot: + this.addPlot(payload); + break; + + case SharedMessages.UpdateSettings: + this.updateSettings(payload); + break; + + case SharedMessages.LocInit: + this.initializeLoc(payload); + break; + + default: + break; + } + + return false; + }; + + private initializeLoc(content: string) { + const locJSON = JSON.parse(content); + storeLocStrings(locJSON); + } + + private updateSettings(content: string) { + const newSettingsJSON = JSON.parse(content); + const newSettings = newSettingsJSON as IDataScienceExtraSettings; + this.setState({ + settings: newSettings + }); + } + + private darkChanged = (newDark: boolean) => { + // update our base theme if allowed. Don't do this + // during testing as it will mess up the expected render count. + if (!this.props.testMode) { + this.setState({ + forceDark: newDark + }); + } + }; + + private computeBaseTheme(): string { + // If we're ignoring, always light + if (this.state.settings?.ignoreVscodeTheme) { + return 'vscode-light'; + } + + // Otherwise see if the style injector has figured out + // the theme is dark or not + if (this.state.forceDark !== undefined) { + return this.state.forceDark ? 'vscode-dark' : 'vscode-light'; + } + + return this.props.baseTheme; + } + + private onKeyDown = (event: KeyboardEvent) => { + if (!event.ctrlKey) { + switch (event.key) { + case 'ArrowRight': + if (this.state.currentImage < this.state.images.length - 1) { + this.setState({ currentImage: this.state.currentImage + 1 }); + } + break; + + case 'ArrowLeft': + if (this.state.currentImage > 0) { + this.setState({ currentImage: this.state.currentImage - 1 }); + } + break; + + default: + break; + } + } else if (event.ctrlKey && !event.altKey && this.viewer && this.viewer.current) { + switch (event.key) { + case 'ArrowRight': + this.viewer.current.move(PanKeyboardSize, 0); + break; + + case 'ArrowLeft': + this.viewer.current.move(-PanKeyboardSize, 0); + break; + + case 'ArrowUp': + this.viewer.current.move(0, -PanKeyboardSize); + break; + + case 'ArrowDown': + this.viewer.current.move(0, PanKeyboardSize); + break; + + default: + break; + } + } else if (event.ctrlKey && event.altKey && this.viewer && this.viewer.current) { + switch (event.key) { + case '+': + this.viewer.current.zoom(1.5); + break; + + case '-': + this.viewer.current.zoom(0.66666); + break; + + default: + break; + } + } + }; + + private addPlot(payload: any) { + this.setState({ + images: [...this.state.images, payload as string], + thumbnails: [...this.state.thumbnails, this.generateThumbnail(payload)], + sizes: [...this.state.sizes, this.extractSize(payload)], + values: [...this.state.values, undefined], + ids: [...this.state.ids, uuid()], + currentImage: this.state.images.length + }); + } + + private renderThumbnails(_baseTheme: string) { + return ( + <SvgList + images={this.state.thumbnails} + currentImage={this.state.currentImage} + imageClicked={this.imageClicked} + themeMatplotlibBackground={this.state.settings?.themeMatplotlibPlots ? true : false} + /> + ); + } + + private renderToolbar(baseTheme: string) { + const prev = this.state.currentImage > 0 ? this.prevClicked : undefined; + const next = this.state.currentImage < this.state.images.length - 1 ? this.nextClicked : undefined; + const deleteClickHandler = this.state.currentImage !== -1 ? this.deleteClicked : undefined; + return ( + <Toolbar + baseTheme={baseTheme} + changeTool={this.changeTool} + exportButtonClicked={this.exportCurrent} + copyButtonClicked={this.copyCurrent} + prevButtonClicked={prev} + nextButtonClicked={next} + deleteButtonClicked={deleteClickHandler} + /> + ); + } + private renderPlot(baseTheme: string) { + // Render current plot + const currentPlot = this.state.currentImage >= 0 ? this.state.images[this.state.currentImage] : undefined; + const currentSize = this.state.currentImage >= 0 ? this.state.sizes[this.state.currentImage] : undefined; + const currentId = this.state.currentImage >= 0 ? this.state.ids[this.state.currentImage] : undefined; + const value = this.state.currentImage >= 0 ? this.state.values[this.state.currentImage] : undefined; + if (currentPlot && currentSize && currentId) { + return ( + <SvgViewer + baseTheme={baseTheme} + themeMatplotlibPlots={this.state.settings?.themeMatplotlibPlots ? true : false} + svg={currentPlot} + id={currentId} + size={currentSize} + defaultValue={value} + tool={this.state.tool} + changeValue={this.changeCurrentValue} + ref={this.viewer} + /> + ); + } + + return null; + } + + private generateThumbnail(image: string): string { + // A 'thumbnail' is really just an svg image with + // the width and height forced to 100% + const h = image.replace(RegExpValues.SvgHeightRegex, '$1100%"'); + return h.replace(RegExpValues.SvgWidthRegex, '$1100%"'); + } + + private changeCurrentValue = (value: Value) => { + this.currentValue = { ...value }; + }; + + private changeTool = (tool: Tool) => { + this.setState({ tool }); + }; + + private extractSize(image: string): ISize { + let height = '100px'; + let width = '100px'; + + // Try the tags that might have been added by the cell formatter + const sizeTagMatch = RegExpValues.SvgSizeTagRegex.exec(image); + if (sizeTagMatch && sizeTagMatch.length > 2) { + width = sizeTagMatch[1]; + height = sizeTagMatch[2]; + } else { + // Otherwise just parse the height/width directly + const heightMatch = RegExpValues.SvgHeightRegex.exec(image); + if (heightMatch && heightMatch.length > 2) { + height = heightMatch[2]; + } + const widthMatch = RegExpValues.SvgHeightRegex.exec(image); + if (widthMatch && widthMatch.length > 2) { + width = widthMatch[2]; + } + } + + return { + height, + width + }; + } + + private changeCurrentImage(index: number) { + // Update our state for our current image and our current value + if (index !== this.state.currentImage) { + const newValues = [...this.state.values]; + newValues[this.state.currentImage] = this.currentValue; + this.setState({ + currentImage: index, + values: newValues + }); + + // Reassign the current value to the new index so we track it. + this.currentValue = newValues[index]; + } + } + + private imageClicked = (index: number) => { + this.changeCurrentImage(index); + }; + + private sendMessage<M extends IPlotViewerMapping, T extends keyof M>(type: T, payload?: M[T]) { + this.postOffice.sendMessage<M, T>(type, payload); + } + + private exportCurrent = async () => { + // In order to export, we need the png and the svg. Generate + // a png by drawing to a canvas and then turning the canvas into a dataurl. + if (this.container && this.container.current) { + const doc = this.container.current.ownerDocument; + if (doc) { + const canvas = doc.createElement('canvas'); + if (canvas) { + const ctx = canvas.getContext('2d'); + if (ctx) { + const waitable = createDeferred(); + const svgBlob = new Blob([this.state.images[this.state.currentImage]], { + type: 'image/svg+xml;charset=utf-8' + }); + const img = new Image(); + const url = window.URL.createObjectURL(svgBlob); + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0); + waitable.resolve(); + }; + img.src = url; + await waitable.promise; + const png = canvas.toDataURL('png'); + canvas.remove(); + + // Send both our image and the png. + this.sendMessage(PlotViewerMessages.ExportPlot, { + svg: this.state.images[this.state.currentImage], + png + }); + } + } + } + } + }; + + private copyCurrent = async () => { + // Not supported at the moment. + }; + + private prevClicked = () => { + this.changeCurrentImage(this.state.currentImage - 1); + }; + + private nextClicked = () => { + this.changeCurrentImage(this.state.currentImage + 1); + }; + + private deleteClicked = () => { + if (this.state.currentImage >= 0) { + const oldCurrent = this.state.currentImage; + const newCurrent = this.state.images.length > 1 ? this.state.currentImage : -1; + + this.setState({ + images: this.state.images.filter((_v, i) => i !== oldCurrent), + sizes: this.state.sizes.filter((_v, i) => i !== oldCurrent), + values: this.state.values.filter((_v, i) => i !== oldCurrent), + thumbnails: this.state.thumbnails.filter((_v, i) => i !== oldCurrent), + currentImage: newCurrent + }); + + // Tell the other side too as we don't want it sending this image again + this.sendMessage(PlotViewerMessages.RemovePlot, oldCurrent); + } + }; +} diff --git a/src/datascience-ui/plot/testSvg.ts b/src/datascience-ui/plot/testSvg.ts new file mode 100644 index 000000000000..64bfd3e54221 --- /dev/null +++ b/src/datascience-ui/plot/testSvg.ts @@ -0,0 +1,571 @@ +// tslint:disable: no-multiline-string no-trailing-whitespace +export const TestSvg = ` +<svg height="574.678125pt" version="1.1" viewBox="0 0 331.045312 574.678125" width="331.045312pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + + </defs> + <g> + <g> + <path d="M 0 574.678125 +L 331.045312 574.678125 +L 331.045312 0 +L 0 0 +z +" style="fill:none;"></path> + </g> + <g> + <g> + <path d="M 44.845313 550.8 +L 323.845312 550.8 +L 323.845312 7.2 +L 44.845313 7.2 +z +" style="fill:#ffffff;"></path> + </g> + <g> + <g> + <g> + <defs> + <path d="M 0 0 +L 0 3.5 +" style="stroke:#000000;stroke-width:0.8;"></path> + </defs> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="57.527131" xlink:href="#m539de8c21e" y="550.8"></use> + </g> + </g> + <g> + <!-- 0.0 --> + <defs> + <path d="M 31.78125 66.40625 +Q 24.171875 66.40625 20.328125 58.90625 +Q 16.5 51.421875 16.5 36.375 +Q 16.5 21.390625 20.328125 13.890625 +Q 24.171875 6.390625 31.78125 6.390625 +Q 39.453125 6.390625 43.28125 13.890625 +Q 47.125 21.390625 47.125 36.375 +Q 47.125 51.421875 43.28125 58.90625 +Q 39.453125 66.40625 31.78125 66.40625 +z +M 31.78125 74.21875 +Q 44.046875 74.21875 50.515625 64.515625 +Q 56.984375 54.828125 56.984375 36.375 +Q 56.984375 17.96875 50.515625 8.265625 +Q 44.046875 -1.421875 31.78125 -1.421875 +Q 19.53125 -1.421875 13.0625 8.265625 +Q 6.59375 17.96875 6.59375 36.375 +Q 6.59375 54.828125 13.0625 64.515625 +Q 19.53125 74.21875 31.78125 74.21875 +z +"></path> + <path d="M 10.6875 12.40625 +L 21 12.40625 +L 21 0 +L 10.6875 0 +z +"></path> + </defs> + <g transform="translate(49.575568 565.398438)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-48"></use> + <use x="63.623047" xlink:href="#DejaVuSans-46"></use> + <use x="95.410156" xlink:href="#DejaVuSans-48"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="89.231676" xlink:href="#m539de8c21e" y="550.8"></use> + </g> + </g> + <g> + <!-- 2.5 --> + <defs> + <path d="M 19.1875 8.296875 +L 53.609375 8.296875 +L 53.609375 0 +L 7.328125 0 +L 7.328125 8.296875 +Q 12.9375 14.109375 22.625 23.890625 +Q 32.328125 33.6875 34.8125 36.53125 +Q 39.546875 41.84375 41.421875 45.53125 +Q 43.3125 49.21875 43.3125 52.78125 +Q 43.3125 58.59375 39.234375 62.25 +Q 35.15625 65.921875 28.609375 65.921875 +Q 23.96875 65.921875 18.8125 64.3125 +Q 13.671875 62.703125 7.8125 59.421875 +L 7.8125 69.390625 +Q 13.765625 71.78125 18.9375 73 +Q 24.125 74.21875 28.421875 74.21875 +Q 39.75 74.21875 46.484375 68.546875 +Q 53.21875 62.890625 53.21875 53.421875 +Q 53.21875 48.921875 51.53125 44.890625 +Q 49.859375 40.875 45.40625 35.40625 +Q 44.1875 33.984375 37.640625 27.21875 +Q 31.109375 20.453125 19.1875 8.296875 +z +"></path> + <path d="M 10.796875 72.90625 +L 49.515625 72.90625 +L 49.515625 64.59375 +L 19.828125 64.59375 +L 19.828125 46.734375 +Q 21.96875 47.46875 24.109375 47.828125 +Q 26.265625 48.1875 28.421875 48.1875 +Q 40.625 48.1875 47.75 41.5 +Q 54.890625 34.8125 54.890625 23.390625 +Q 54.890625 11.625 47.5625 5.09375 +Q 40.234375 -1.421875 26.90625 -1.421875 +Q 22.3125 -1.421875 17.546875 -0.640625 +Q 12.796875 0.140625 7.71875 1.703125 +L 7.71875 11.625 +Q 12.109375 9.234375 16.796875 8.0625 +Q 21.484375 6.890625 26.703125 6.890625 +Q 35.15625 6.890625 40.078125 11.328125 +Q 45.015625 15.765625 45.015625 23.390625 +Q 45.015625 31 40.078125 35.4375 +Q 35.15625 39.890625 26.703125 39.890625 +Q 22.75 39.890625 18.8125 39.015625 +Q 14.890625 38.140625 10.796875 36.28125 +z +"></path> + </defs> + <g transform="translate(81.280114 565.398438)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-50"></use> + <use x="63.623047" xlink:href="#DejaVuSans-46"></use> + <use x="95.410156" xlink:href="#DejaVuSans-53"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="120.936222" xlink:href="#m539de8c21e" y="550.8"></use> + </g> + </g> + <g> + <!-- 5.0 --> + <g transform="translate(112.984659 565.398438)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-53"></use> + <use x="63.623047" xlink:href="#DejaVuSans-46"></use> + <use x="95.410156" xlink:href="#DejaVuSans-48"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="152.640767" xlink:href="#m539de8c21e" y="550.8"></use> + </g> + </g> + <g> + <!-- 7.5 --> + <defs> + <path d="M 8.203125 72.90625 +L 55.078125 72.90625 +L 55.078125 68.703125 +L 28.609375 0 +L 18.3125 0 +L 43.21875 64.59375 +L 8.203125 64.59375 +z +"></path> + </defs> + <g transform="translate(144.689205 565.398438)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-55"></use> + <use x="63.623047" xlink:href="#DejaVuSans-46"></use> + <use x="95.410156" xlink:href="#DejaVuSans-53"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="184.345313" xlink:href="#m539de8c21e" y="550.8"></use> + </g> + </g> + <g> + <!-- 10.0 --> + <defs> + <path d="M 12.40625 8.296875 +L 28.515625 8.296875 +L 28.515625 63.921875 +L 10.984375 60.40625 +L 10.984375 69.390625 +L 28.421875 72.90625 +L 38.28125 72.90625 +L 38.28125 8.296875 +L 54.390625 8.296875 +L 54.390625 0 +L 12.40625 0 +z +"></path> + </defs> + <g transform="translate(173.2125 565.398438)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-49"></use> + <use x="63.623047" xlink:href="#DejaVuSans-48"></use> + <use x="127.246094" xlink:href="#DejaVuSans-46"></use> + <use x="159.033203" xlink:href="#DejaVuSans-48"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="216.049858" xlink:href="#m539de8c21e" y="550.8"></use> + </g> + </g> + <g> + <!-- 12.5 --> + <g transform="translate(204.917045 565.398438)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-49"></use> + <use x="63.623047" xlink:href="#DejaVuSans-50"></use> + <use x="127.246094" xlink:href="#DejaVuSans-46"></use> + <use x="159.033203" xlink:href="#DejaVuSans-53"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="247.754403" xlink:href="#m539de8c21e" y="550.8"></use> + </g> + </g> + <g> + <!-- 15.0 --> + <g transform="translate(236.621591 565.398438)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-49"></use> + <use x="63.623047" xlink:href="#DejaVuSans-53"></use> + <use x="127.246094" xlink:href="#DejaVuSans-46"></use> + <use x="159.033203" xlink:href="#DejaVuSans-48"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="279.458949" xlink:href="#m539de8c21e" y="550.8"></use> + </g> + </g> + <g> + <!-- 17.5 --> + <g transform="translate(268.326136 565.398438)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-49"></use> + <use x="63.623047" xlink:href="#DejaVuSans-55"></use> + <use x="127.246094" xlink:href="#DejaVuSans-46"></use> + <use x="159.033203" xlink:href="#DejaVuSans-53"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="311.163494" xlink:href="#m539de8c21e" y="550.8"></use> + </g> + </g> + <g> + <!-- 20.0 --> + <g transform="translate(300.030682 565.398438)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-50"></use> + <use x="63.623047" xlink:href="#DejaVuSans-48"></use> + <use x="127.246094" xlink:href="#DejaVuSans-46"></use> + <use x="159.033203" xlink:href="#DejaVuSans-48"></use> + </g> + </g> + </g> + </g> + <g> + <g> + <g> + <defs> + <path d="M 0 0 +L -3.5 0 +" style="stroke:#000000;stroke-width:0.8;"></path> + </defs> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="526.628231"></use> + </g> + </g> + <g> + <!-- −1.00 --> + <defs> + <path d="M 10.59375 35.5 +L 73.1875 35.5 +L 73.1875 27.203125 +L 10.59375 27.203125 +z +"></path> + </defs> + <g transform="translate(7.2 530.42745)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-8722"></use> + <use x="83.789062" xlink:href="#DejaVuSans-49"></use> + <use x="147.412109" xlink:href="#DejaVuSans-46"></use> + <use x="179.199219" xlink:href="#DejaVuSans-48"></use> + <use x="242.822266" xlink:href="#DejaVuSans-48"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="464.78806"></use> + </g> + </g> + <g> + <!-- −0.75 --> + <g transform="translate(7.2 468.587279)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-8722"></use> + <use x="83.789062" xlink:href="#DejaVuSans-48"></use> + <use x="147.412109" xlink:href="#DejaVuSans-46"></use> + <use x="179.199219" xlink:href="#DejaVuSans-55"></use> + <use x="242.822266" xlink:href="#DejaVuSans-53"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="402.947889"></use> + </g> + </g> + <g> + <!-- −0.50 --> + <g transform="translate(7.2 406.747107)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-8722"></use> + <use x="83.789062" xlink:href="#DejaVuSans-48"></use> + <use x="147.412109" xlink:href="#DejaVuSans-46"></use> + <use x="179.199219" xlink:href="#DejaVuSans-53"></use> + <use x="242.822266" xlink:href="#DejaVuSans-48"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="341.107717"></use> + </g> + </g> + <g> + <!-- −0.25 --> + <g transform="translate(7.2 344.906936)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-8722"></use> + <use x="83.789062" xlink:href="#DejaVuSans-48"></use> + <use x="147.412109" xlink:href="#DejaVuSans-46"></use> + <use x="179.199219" xlink:href="#DejaVuSans-50"></use> + <use x="242.822266" xlink:href="#DejaVuSans-53"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="279.267546"></use> + </g> + </g> + <g> + <!-- 0.00 --> + <g transform="translate(15.579688 283.066764)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-48"></use> + <use x="63.623047" xlink:href="#DejaVuSans-46"></use> + <use x="95.410156" xlink:href="#DejaVuSans-48"></use> + <use x="159.033203" xlink:href="#DejaVuSans-48"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="217.427374"></use> + </g> + </g> + <g> + <!-- 0.25 --> + <g transform="translate(15.579688 221.226593)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-48"></use> + <use x="63.623047" xlink:href="#DejaVuSans-46"></use> + <use x="95.410156" xlink:href="#DejaVuSans-50"></use> + <use x="159.033203" xlink:href="#DejaVuSans-53"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="155.587203"></use> + </g> + </g> + <g> + <!-- 0.50 --> + <g transform="translate(15.579688 159.386422)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-48"></use> + <use x="63.623047" xlink:href="#DejaVuSans-46"></use> + <use x="95.410156" xlink:href="#DejaVuSans-53"></use> + <use x="159.033203" xlink:href="#DejaVuSans-48"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="93.747031"></use> + </g> + </g> + <g> + <!-- 0.75 --> + <g transform="translate(15.579688 97.54625)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-48"></use> + <use x="63.623047" xlink:href="#DejaVuSans-46"></use> + <use x="95.410156" xlink:href="#DejaVuSans-55"></use> + <use x="159.033203" xlink:href="#DejaVuSans-53"></use> + </g> + </g> + </g> + <g> + <g> + <g> + <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="31.90686"></use> + </g> + </g> + <g> + <!-- 1.00 --> + <g transform="translate(15.579688 35.706079)scale(0.1 -0.1)"> + <use xlink:href="#DejaVuSans-49"></use> + <use x="63.623047" xlink:href="#DejaVuSans-46"></use> + <use x="95.410156" xlink:href="#DejaVuSans-48"></use> + <use x="159.033203" xlink:href="#DejaVuSans-48"></use> + </g> + </g> + </g> + </g> + <g> + <path clip-path="url(#pffcc3726a6)" d="M 57.527131 279.267546 +L 60.089114 229.634907 +L 62.651098 182.021004 +L 65.213081 138.362462 +L 67.775065 100.435031 +L 70.337048 69.781352 +L 72.899032 47.64822 +L 75.461015 34.935868 +L 78.022998 32.161352 +L 80.584982 39.437521 +L 83.146965 56.468429 +L 85.708949 82.561367 +L 88.270932 116.655043 +L 90.832916 157.362747 +L 93.394899 203.028752 +L 95.956883 251.795658 +L 98.518866 301.679944 +L 101.08085 350.652638 +L 103.642833 396.721847 +L 106.204817 438.013773 +L 108.7668 472.848927 +L 111.328784 499.810439 +L 113.890767 517.801689 +L 116.452751 526.090909 +L 119.014734 524.340947 +L 121.576717 512.622981 +L 124.138701 491.413621 +L 126.700684 461.575527 +L 129.262668 424.322321 +L 131.824651 381.169222 +L 134.386635 333.871421 +L 136.948618 284.352687 +L 139.510602 234.627122 +L 142.072585 186.717241 +L 144.634569 142.571709 +L 147.196552 103.986082 +L 149.758536 72.529774 +L 152.320519 49.482225 +L 154.882503 35.78086 +L 157.444486 31.982963 +L 160.00647 38.243006 +L 162.568453 54.306373 +L 165.130436 79.519709 +L 167.69242 112.857499 +L 170.254403 152.963775 +L 172.816387 198.207274 +L 175.37837 246.747781 +L 177.940354 296.610983 +L 180.502337 345.768766 +L 183.064321 392.221708 +L 185.626304 434.080403 +L 188.188288 469.642311 +L 190.750271 497.461001 +L 193.312255 516.404989 +L 195.874238 525.703756 +L 198.436222 524.979088 +L 200.998205 514.26046 +L 203.560189 493.983836 +L 206.122172 464.973939 +L 208.684155 428.410704 +L 211.246139 385.781288 +L 213.808122 338.819579 +L 216.370106 289.435679 +L 218.932089 239.638204 +L 221.494073 191.452595 +L 224.056056 146.838732 +L 226.61804 107.611218 +L 229.180023 75.365577 +L 231.742007 51.413351 +L 234.30399 36.728765 +L 236.865974 31.909091 +L 239.427957 37.150363 +L 241.989941 52.2394 +L 244.551924 76.562477 +L 247.113908 109.130289 +L 249.675891 148.618186 +L 252.237874 193.420056 +L 254.799858 241.713649 +L 257.361841 291.534691 +L 259.923825 340.856786 +L 262.485808 387.673827 +L 265.047792 430.0816 +L 267.609775 466.355231 +L 270.171759 495.019341 +L 272.733742 514.908061 +L 275.295726 525.212445 +L 277.857709 525.513377 +L 280.419693 515.798617 +L 282.981676 496.4633 +L 285.54366 468.293861 +L 288.105643 432.43605 +L 290.667627 390.348334 +L 293.22961 343.742567 +L 295.791593 294.514373 +L 298.353577 244.666036 +L 300.91556 196.225065 +L 303.477544 151.161727 +L 306.039527 111.308906 +L 308.601511 78.28756 +L 311.163494 53.440782 +" style="fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;"></path> + </g> + <g> + <path d="M 44.845313 550.8 +L 44.845313 7.2 +" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;"></path> + </g> + <g> + <path d="M 323.845312 550.8 +L 323.845312 7.2 +" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;"></path> + </g> + <g> + <path d="M 44.845313 550.8 +L 323.845312 550.8 +" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;"></path> + </g> + <g> + <path d="M 44.845313 7.2 +L 323.845312 7.2 +" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;"></path> + </g> + </g> + </g> + <defs> + <clipPath> + <rect height="543.6" width="279" x="44.845313" y="7.2"></rect> + </clipPath> + </defs> +</svg> +`; diff --git a/src/datascience-ui/plot/toolbar.css b/src/datascience-ui/plot/toolbar.css new file mode 100644 index 000000000000..82df52ea1834 --- /dev/null +++ b/src/datascience-ui/plot/toolbar.css @@ -0,0 +1,7 @@ +#plot-toolbar-panel { + position: absolute; + top: 0; + left: 0; + background-color: var(--vscode-editor-background); + border: 1px solid black; +} \ No newline at end of file diff --git a/src/datascience-ui/plot/toolbar.tsx b/src/datascience-ui/plot/toolbar.tsx new file mode 100644 index 000000000000..692fe4f68ed6 --- /dev/null +++ b/src/datascience-ui/plot/toolbar.tsx @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as React from 'react'; +import { Tool } from 'react-svg-pan-zoom'; +import { Image, ImageName } from '../react-common/image'; +import { ImageButton } from '../react-common/imageButton'; +import { getLocString } from '../react-common/locReactSide'; + +interface IToolbarProps { + baseTheme: string; + changeTool(tool: Tool): void; + prevButtonClicked?(): void; + nextButtonClicked?(): void; + exportButtonClicked(): void; + copyButtonClicked(): void; + deleteButtonClicked?(): void; +} + +export class Toolbar extends React.Component<IToolbarProps> { + constructor(props: IToolbarProps) { + super(props); + } + + public render() { + return ( + <div id="plot-toolbar-panel"> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.props.prevButtonClicked} + disabled={!this.props.prevButtonClicked} + tooltip={getLocString('DataScience.previousPlot', 'Previous')} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.Prev} /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.props.nextButtonClicked} + disabled={!this.props.nextButtonClicked} + tooltip={getLocString('DataScience.nextPlot', 'Next')} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.Next} /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.pan} + tooltip={getLocString('DataScience.panPlot', 'Pan')} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.Pan} /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.zoomIn} + tooltip={getLocString('DataScience.zoomInPlot', 'Zoom in')} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.Zoom} /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.zoomOut} + tooltip={getLocString('DataScience.zoomOutPlot', 'Zoom out')} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.ZoomOut} /> + </ImageButton> + {/* This isn't possible until VS Code supports copying images to the clipboard. See https://github.com/microsoft/vscode/issues/217 + <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.copyButtonClicked} tooltip={getLocString('DataScience.copyPlot', 'Copy image to clipboard')}> + <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Copy}/> + </ImageButton> */} + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.props.exportButtonClicked} + tooltip={getLocString('DataScience.exportPlot', 'Export to different formats.')} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.SaveAs} /> + </ImageButton> + <ImageButton + baseTheme={this.props.baseTheme} + onClick={this.props.deleteButtonClicked} + disabled={!this.props.deleteButtonClicked} + tooltip={getLocString('DataScience.deletePlot', 'Remove')} + > + <Image baseTheme={this.props.baseTheme} class="image-button-image" image={ImageName.Delete} /> + </ImageButton> + </div> + ); + } + + private pan = () => { + this.props.changeTool('pan'); + }; + + private zoomIn = () => { + this.props.changeTool('zoom-in'); + }; + + private zoomOut = () => { + this.props.changeTool('zoom-out'); + }; +} diff --git a/src/datascience-ui/react-common/arePathsSame.ts b/src/datascience-ui/react-common/arePathsSame.ts new file mode 100644 index 000000000000..8e46dc12f41d --- /dev/null +++ b/src/datascience-ui/react-common/arePathsSame.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import * as path from 'path'; +import { getOSType, OSType } from '../../client/common/utils/platform'; + +// Provide functionality of IFileSystem arePathsSame for the React components +export function arePathsSame(path1: string, path2: string): boolean { + path1 = path.normalize(path1); + path2 = path.normalize(path2); + if (getOSType() === OSType.Windows) { + return path1.toUpperCase() === path2.toUpperCase(); + } else { + return path1 === path2; + } +} diff --git a/src/datascience-ui/react-common/button.tsx b/src/datascience-ui/react-common/button.tsx new file mode 100644 index 000000000000..e11cb502714a --- /dev/null +++ b/src/datascience-ui/react-common/button.tsx @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import * as React from 'react'; + +interface IButtonProps { + className: string; + tooltip: string; + disabled?: boolean; + hidden?: boolean; + onClick?(event?: React.MouseEvent<HTMLButtonElement>): void; +} + +export class Button extends React.Component<IButtonProps> { + constructor(props: IButtonProps) { + super(props); + } + + public render() { + const innerFilter = this.props.disabled ? 'button-inner-disabled-filter' : ''; + const ariaDisabled = this.props.disabled ? 'true' : 'false'; + + return ( + <button + role="button" + aria-pressed="false" + disabled={this.props.disabled} + aria-disabled={ariaDisabled} + title={this.props.tooltip} + aria-label={this.props.tooltip} + className={this.props.className} + onClick={this.props.onClick} + > + <span className={innerFilter}>{this.props.children}</span> + </button> + ); + } +} diff --git a/src/datascience-ui/react-common/codicon/codicon-animations.css b/src/datascience-ui/react-common/codicon/codicon-animations.css new file mode 100644 index 000000000000..abfde40dede6 --- /dev/null +++ b/src/datascience-ui/react-common/codicon/codicon-animations.css @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +@keyframes codicon-spin { + 100% { + transform:rotate(360deg); + } +} + +.codicon-animation-spin { + animation: codicon-spin 1.5s linear infinite; +} diff --git a/src/datascience-ui/react-common/codicon/codicon-modifications.css b/src/datascience-ui/react-common/codicon/codicon-modifications.css new file mode 100644 index 000000000000..950493dd6bed --- /dev/null +++ b/src/datascience-ui/react-common/codicon/codicon-modifications.css @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.codicon-wrench-subaction { + opacity: 0.5; +} diff --git a/src/datascience-ui/react-common/codicon/codicon.css b/src/datascience-ui/react-common/codicon/codicon.css new file mode 100644 index 000000000000..70e60afd42af --- /dev/null +++ b/src/datascience-ui/react-common/codicon/codicon.css @@ -0,0 +1,429 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +@font-face { + font-family: "codicon"; + src: url("./codicon.ttf?b72c513f65e30cf5c3607d5a7971b6a9") format("truetype"); +} + +.codicon[class*='codicon-'] { + font: normal normal normal 16px/1 codicon; + display: inline-block; + text-decoration: none; + text-rendering: auto; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + + +.codicon-add:before { content: "\ea60" } +.codicon-plus:before { content: "\ea60" } +.codicon-gist-new:before { content: "\ea60" } +.codicon-repo-create:before { content: "\ea60" } +.codicon-lightbulb:before { content: "\ea61" } +.codicon-light-bulb:before { content: "\ea61" } +.codicon-repo:before { content: "\ea62" } +.codicon-repo-delete:before { content: "\ea62" } +.codicon-gist-fork:before { content: "\ea63" } +.codicon-repo-forked:before { content: "\ea63" } +.codicon-git-pull-request:before { content: "\ea64" } +.codicon-git-pull-request-abandoned:before { content: "\ea64" } +.codicon-record-keys:before { content: "\ea65" } +.codicon-keyboard:before { content: "\ea65" } +.codicon-tag:before { content: "\ea66" } +.codicon-tag-add:before { content: "\ea66" } +.codicon-tag-remove:before { content: "\ea66" } +.codicon-person:before { content: "\ea67" } +.codicon-person-add:before { content: "\ea67" } +.codicon-person-follow:before { content: "\ea67" } +.codicon-person-outline:before { content: "\ea67" } +.codicon-person-filled:before { content: "\ea67" } +.codicon-git-branch:before { content: "\ea68" } +.codicon-git-branch-create:before { content: "\ea68" } +.codicon-git-branch-delete:before { content: "\ea68" } +.codicon-source-control:before { content: "\ea68" } +.codicon-mirror:before { content: "\ea69" } +.codicon-mirror-public:before { content: "\ea69" } +.codicon-star:before { content: "\ea6a" } +.codicon-star-add:before { content: "\ea6a" } +.codicon-star-delete:before { content: "\ea6a" } +.codicon-star-empty:before { content: "\ea6a" } +.codicon-comment:before { content: "\ea6b" } +.codicon-comment-add:before { content: "\ea6b" } +.codicon-alert:before { content: "\ea6c" } +.codicon-warning:before { content: "\ea6c" } +.codicon-search:before { content: "\ea6d" } +.codicon-search-save:before { content: "\ea6d" } +.codicon-log-out:before { content: "\ea6e" } +.codicon-sign-out:before { content: "\ea6e" } +.codicon-log-in:before { content: "\ea6f" } +.codicon-sign-in:before { content: "\ea6f" } +.codicon-eye:before { content: "\ea70" } +.codicon-eye-unwatch:before { content: "\ea70" } +.codicon-eye-watch:before { content: "\ea70" } +.codicon-circle-filled:before { content: "\ea71" } +.codicon-primitive-dot:before { content: "\ea71" } +.codicon-close-dirty:before { content: "\ea71" } +.codicon-debug-breakpoint:before { content: "\ea71" } +.codicon-debug-breakpoint-disabled:before { content: "\ea71" } +.codicon-debug-hint:before { content: "\ea71" } +.codicon-primitive-square:before { content: "\ea72" } +.codicon-edit:before { content: "\ea73" } +.codicon-pencil:before { content: "\ea73" } +.codicon-info:before { content: "\ea74" } +.codicon-issue-opened:before { content: "\ea74" } +.codicon-gist-private:before { content: "\ea75" } +.codicon-git-fork-private:before { content: "\ea75" } +.codicon-lock:before { content: "\ea75" } +.codicon-mirror-private:before { content: "\ea75" } +.codicon-close:before { content: "\ea76" } +.codicon-remove-close:before { content: "\ea76" } +.codicon-x:before { content: "\ea76" } +.codicon-repo-sync:before { content: "\ea77" } +.codicon-sync:before { content: "\ea77" } +.codicon-clone:before { content: "\ea78" } +.codicon-desktop-download:before { content: "\ea78" } +.codicon-beaker:before { content: "\ea79" } +.codicon-microscope:before { content: "\ea79" } +.codicon-vm:before { content: "\ea7a" } +.codicon-device-desktop:before { content: "\ea7a" } +.codicon-file:before { content: "\ea7b" } +.codicon-file-text:before { content: "\ea7b" } +.codicon-more:before { content: "\ea7c" } +.codicon-ellipsis:before { content: "\ea7c" } +.codicon-kebab-horizontal:before { content: "\ea7c" } +.codicon-mail-reply:before { content: "\ea7d" } +.codicon-reply:before { content: "\ea7d" } +.codicon-organization:before { content: "\ea7e" } +.codicon-organization-filled:before { content: "\ea7e" } +.codicon-organization-outline:before { content: "\ea7e" } +.codicon-new-file:before { content: "\ea7f" } +.codicon-file-add:before { content: "\ea7f" } +.codicon-new-folder:before { content: "\ea80" } +.codicon-file-directory-create:before { content: "\ea80" } +.codicon-trash:before { content: "\ea81" } +.codicon-trashcan:before { content: "\ea81" } +.codicon-history:before { content: "\ea82" } +.codicon-clock:before { content: "\ea82" } +.codicon-folder:before { content: "\ea83" } +.codicon-file-directory:before { content: "\ea83" } +.codicon-symbol-folder:before { content: "\ea83" } +.codicon-logo-github:before { content: "\ea84" } +.codicon-mark-github:before { content: "\ea84" } +.codicon-github:before { content: "\ea84" } +.codicon-terminal:before { content: "\ea85" } +.codicon-console:before { content: "\ea85" } +.codicon-repl:before { content: "\ea85" } +.codicon-zap:before { content: "\ea86" } +.codicon-symbol-event:before { content: "\ea86" } +.codicon-error:before { content: "\ea87" } +.codicon-stop:before { content: "\ea87" } +.codicon-variable:before { content: "\ea88" } +.codicon-symbol-variable:before { content: "\ea88" } +.codicon-array:before { content: "\ea8a" } +.codicon-symbol-array:before { content: "\ea8a" } +.codicon-symbol-module:before { content: "\ea8b" } +.codicon-symbol-package:before { content: "\ea8b" } +.codicon-symbol-namespace:before { content: "\ea8b" } +.codicon-symbol-object:before { content: "\ea8b" } +.codicon-symbol-method:before { content: "\ea8c" } +.codicon-symbol-function:before { content: "\ea8c" } +.codicon-symbol-constructor:before { content: "\ea8c" } +.codicon-symbol-boolean:before { content: "\ea8f" } +.codicon-symbol-null:before { content: "\ea8f" } +.codicon-symbol-numeric:before { content: "\ea90" } +.codicon-symbol-number:before { content: "\ea90" } +.codicon-symbol-structure:before { content: "\ea91" } +.codicon-symbol-struct:before { content: "\ea91" } +.codicon-symbol-parameter:before { content: "\ea92" } +.codicon-symbol-type-parameter:before { content: "\ea92" } +.codicon-symbol-key:before { content: "\ea93" } +.codicon-symbol-text:before { content: "\ea93" } +.codicon-symbol-reference:before { content: "\ea94" } +.codicon-go-to-file:before { content: "\ea94" } +.codicon-symbol-enum:before { content: "\ea95" } +.codicon-symbol-value:before { content: "\ea95" } +.codicon-symbol-ruler:before { content: "\ea96" } +.codicon-symbol-unit:before { content: "\ea96" } +.codicon-activate-breakpoints:before { content: "\ea97" } +.codicon-archive:before { content: "\ea98" } +.codicon-arrow-both:before { content: "\ea99" } +.codicon-arrow-down:before { content: "\ea9a" } +.codicon-arrow-left:before { content: "\ea9b" } +.codicon-arrow-right:before { content: "\ea9c" } +.codicon-arrow-small-down:before { content: "\ea9d" } +.codicon-arrow-small-left:before { content: "\ea9e" } +.codicon-arrow-small-right:before { content: "\ea9f" } +.codicon-arrow-small-up:before { content: "\eaa0" } +.codicon-arrow-up:before { content: "\eaa1" } +.codicon-bell:before { content: "\eaa2" } +.codicon-bold:before { content: "\eaa3" } +.codicon-book:before { content: "\eaa4" } +.codicon-bookmark:before { content: "\eaa5" } +.codicon-debug-breakpoint-conditional-unverified:before { content: "\eaa6" } +.codicon-debug-breakpoint-conditional:before { content: "\eaa7" } +.codicon-debug-breakpoint-conditional-disabled:before { content: "\eaa7" } +.codicon-debug-breakpoint-data-unverified:before { content: "\eaa8" } +.codicon-debug-breakpoint-data:before { content: "\eaa9" } +.codicon-debug-breakpoint-data-disabled:before { content: "\eaa9" } +.codicon-debug-breakpoint-log-unverified:before { content: "\eaaa" } +.codicon-debug-breakpoint-log:before { content: "\eaab" } +.codicon-debug-breakpoint-log-disabled:before { content: "\eaab" } +.codicon-briefcase:before { content: "\eaac" } +.codicon-broadcast:before { content: "\eaad" } +.codicon-browser:before { content: "\eaae" } +.codicon-bug:before { content: "\eaaf" } +.codicon-calendar:before { content: "\eab0" } +.codicon-case-sensitive:before { content: "\eab1" } +.codicon-check:before { content: "\eab2" } +.codicon-checklist:before { content: "\eab3" } +.codicon-chevron-down:before { content: "\eab4" } +.codicon-chevron-left:before { content: "\eab5" } +.codicon-chevron-right:before { content: "\eab6" } +.codicon-chevron-up:before { content: "\eab7" } +.codicon-chrome-close:before { content: "\eab8" } +.codicon-chrome-maximize:before { content: "\eab9" } +.codicon-chrome-minimize:before { content: "\eaba" } +.codicon-chrome-restore:before { content: "\eabb" } +.codicon-circle-outline:before { content: "\eabc" } +.codicon-debug-breakpoint-unverified:before { content: "\eabc" } +.codicon-circle-slash:before { content: "\eabd" } +.codicon-circuit-board:before { content: "\eabe" } +.codicon-clear-all:before { content: "\eabf" } +.codicon-clippy:before { content: "\eac0" } +.codicon-close-all:before { content: "\eac1" } +.codicon-cloud-download:before { content: "\eac2" } +.codicon-cloud-upload:before { content: "\eac3" } +.codicon-code:before { content: "\eac4" } +.codicon-collapse-all:before { content: "\eac5" } +.codicon-color-mode:before { content: "\eac6" } +.codicon-comment-discussion:before { content: "\eac7" } +.codicon-compare-changes:before { content: "\eac8" } +.codicon-credit-card:before { content: "\eac9" } +.codicon-dash:before { content: "\eacc" } +.codicon-dashboard:before { content: "\eacd" } +.codicon-database:before { content: "\eace" } +.codicon-debug-continue:before { content: "\eacf" } +.codicon-debug-disconnect:before { content: "\ead0" } +.codicon-debug-pause:before { content: "\ead1" } +.codicon-debug-restart:before { content: "\ead2" } +.codicon-debug-start:before { content: "\ead3" } +.codicon-debug-step-into:before { content: "\ead4" } +.codicon-debug-step-out:before { content: "\ead5" } +.codicon-debug-step-over:before { content: "\ead6" } +.codicon-debug-stop:before { content: "\ead7" } +.codicon-debug:before { content: "\ead8" } +.codicon-device-camera-video:before { content: "\ead9" } +.codicon-device-camera:before { content: "\eada" } +.codicon-device-mobile:before { content: "\eadb" } +.codicon-diff-added:before { content: "\eadc" } +.codicon-diff-ignored:before { content: "\eadd" } +.codicon-diff-modified:before { content: "\eade" } +.codicon-diff-removed:before { content: "\eadf" } +.codicon-diff-renamed:before { content: "\eae0" } +.codicon-diff:before { content: "\eae1" } +.codicon-discard:before { content: "\eae2" } +.codicon-editor-layout:before { content: "\eae3" } +.codicon-empty-window:before { content: "\eae4" } +.codicon-exclude:before { content: "\eae5" } +.codicon-extensions:before { content: "\eae6" } +.codicon-eye-closed:before { content: "\eae7" } +.codicon-file-binary:before { content: "\eae8" } +.codicon-file-code:before { content: "\eae9" } +.codicon-file-media:before { content: "\eaea" } +.codicon-file-pdf:before { content: "\eaeb" } +.codicon-file-submodule:before { content: "\eaec" } +.codicon-file-symlink-directory:before { content: "\eaed" } +.codicon-file-symlink-file:before { content: "\eaee" } +.codicon-file-zip:before { content: "\eaef" } +.codicon-files:before { content: "\eaf0" } +.codicon-filter:before { content: "\eaf1" } +.codicon-flame:before { content: "\eaf2" } +.codicon-fold-down:before { content: "\eaf3" } +.codicon-fold-up:before { content: "\eaf4" } +.codicon-fold:before { content: "\eaf5" } +.codicon-folder-active:before { content: "\eaf6" } +.codicon-folder-opened:before { content: "\eaf7" } +.codicon-gear:before { content: "\eaf8" } +.codicon-gift:before { content: "\eaf9" } +.codicon-gist-secret:before { content: "\eafa" } +.codicon-gist:before { content: "\eafb" } +.codicon-git-commit:before { content: "\eafc" } +.codicon-git-compare:before { content: "\eafd" } +.codicon-git-merge:before { content: "\eafe" } +.codicon-github-action:before { content: "\eaff" } +.codicon-github-alt:before { content: "\eb00" } +.codicon-globe:before { content: "\eb01" } +.codicon-grabber:before { content: "\eb02" } +.codicon-graph:before { content: "\eb03" } +.codicon-gripper:before { content: "\eb04" } +.codicon-heart:before { content: "\eb05" } +.codicon-home:before { content: "\eb06" } +.codicon-horizontal-rule:before { content: "\eb07" } +.codicon-hubot:before { content: "\eb08" } +.codicon-inbox:before { content: "\eb09" } +.codicon-issue-closed:before { content: "\eb0a" } +.codicon-issue-reopened:before { content: "\eb0b" } +.codicon-issues:before { content: "\eb0c" } +.codicon-italic:before { content: "\eb0d" } +.codicon-jersey:before { content: "\eb0e" } +.codicon-json:before { content: "\eb0f" } +.codicon-kebab-vertical:before { content: "\eb10" } +.codicon-key:before { content: "\eb11" } +.codicon-law:before { content: "\eb12" } +.codicon-lightbulb-autofix:before { content: "\eb13" } +.codicon-link-external:before { content: "\eb14" } +.codicon-link:before { content: "\eb15" } +.codicon-list-ordered:before { content: "\eb16" } +.codicon-list-unordered:before { content: "\eb17" } +.codicon-live-share:before { content: "\eb18" } +.codicon-loading:before { content: "\eb19" } +.codicon-location:before { content: "\eb1a" } +.codicon-mail-read:before { content: "\eb1b" } +.codicon-mail:before { content: "\eb1c" } +.codicon-markdown:before { content: "\eb1d" } +.codicon-megaphone:before { content: "\eb1e" } +.codicon-mention:before { content: "\eb1f" } +.codicon-milestone:before { content: "\eb20" } +.codicon-mortar-board:before { content: "\eb21" } +.codicon-move:before { content: "\eb22" } +.codicon-multiple-windows:before { content: "\eb23" } +.codicon-mute:before { content: "\eb24" } +.codicon-no-newline:before { content: "\eb25" } +.codicon-note:before { content: "\eb26" } +.codicon-octoface:before { content: "\eb27" } +.codicon-open-preview:before { content: "\eb28" } +.codicon-package:before { content: "\eb29" } +.codicon-paintcan:before { content: "\eb2a" } +.codicon-pin:before { content: "\eb2b" } +.codicon-play:before { content: "\eb2c" } +.codicon-run:before { content: "\eb2c" } +.codicon-plug:before { content: "\eb2d" } +.codicon-preserve-case:before { content: "\eb2e" } +.codicon-preview:before { content: "\eb2f" } +.codicon-project:before { content: "\eb30" } +.codicon-pulse:before { content: "\eb31" } +.codicon-question:before { content: "\eb32" } +.codicon-quote:before { content: "\eb33" } +.codicon-radio-tower:before { content: "\eb34" } +.codicon-reactions:before { content: "\eb35" } +.codicon-references:before { content: "\eb36" } +.codicon-refresh:before { content: "\eb37" } +.codicon-regex:before { content: "\eb38" } +.codicon-remote-explorer:before { content: "\eb39" } +.codicon-remote:before { content: "\eb3a" } +.codicon-remove:before { content: "\eb3b" } +.codicon-replace-all:before { content: "\eb3c" } +.codicon-replace:before { content: "\eb3d" } +.codicon-repo-clone:before { content: "\eb3e" } +.codicon-repo-force-push:before { content: "\eb3f" } +.codicon-repo-pull:before { content: "\eb40" } +.codicon-repo-push:before { content: "\eb41" } +.codicon-report:before { content: "\eb42" } +.codicon-request-changes:before { content: "\eb43" } +.codicon-rocket:before { content: "\eb44" } +.codicon-root-folder-opened:before { content: "\eb45" } +.codicon-root-folder:before { content: "\eb46" } +.codicon-rss:before { content: "\eb47" } +.codicon-ruby:before { content: "\eb48" } +.codicon-save-all:before { content: "\eb49" } +.codicon-save-as:before { content: "\eb4a" } +.codicon-save:before { content: "\eb4b" } +.codicon-screen-full:before { content: "\eb4c" } +.codicon-screen-normal:before { content: "\eb4d" } +.codicon-search-stop:before { content: "\eb4e" } +.codicon-server:before { content: "\eb50" } +.codicon-settings-gear:before { content: "\eb51" } +.codicon-settings:before { content: "\eb52" } +.codicon-shield:before { content: "\eb53" } +.codicon-smiley:before { content: "\eb54" } +.codicon-sort-precedence:before { content: "\eb55" } +.codicon-split-horizontal:before { content: "\eb56" } +.codicon-split-vertical:before { content: "\eb57" } +.codicon-squirrel:before { content: "\eb58" } +.codicon-star-full:before { content: "\eb59" } +.codicon-star-half:before { content: "\eb5a" } +.codicon-symbol-class:before { content: "\eb5b" } +.codicon-symbol-color:before { content: "\eb5c" } +.codicon-symbol-constant:before { content: "\eb5d" } +.codicon-symbol-enum-member:before { content: "\eb5e" } +.codicon-symbol-field:before { content: "\eb5f" } +.codicon-symbol-file:before { content: "\eb60" } +.codicon-symbol-interface:before { content: "\eb61" } +.codicon-symbol-keyword:before { content: "\eb62" } +.codicon-symbol-misc:before { content: "\eb63" } +.codicon-symbol-operator:before { content: "\eb64" } +.codicon-symbol-property:before { content: "\eb65" } +.codicon-wrench:before { content: "\eb65" } +.codicon-wrench-subaction:before { content: "\eb65" } +.codicon-symbol-snippet:before { content: "\eb66" } +.codicon-tasklist:before { content: "\eb67" } +.codicon-telescope:before { content: "\eb68" } +.codicon-text-size:before { content: "\eb69" } +.codicon-three-bars:before { content: "\eb6a" } +.codicon-thumbsdown:before { content: "\eb6b" } +.codicon-thumbsup:before { content: "\eb6c" } +.codicon-tools:before { content: "\eb6d" } +.codicon-triangle-down:before { content: "\eb6e" } +.codicon-triangle-left:before { content: "\eb6f" } +.codicon-triangle-right:before { content: "\eb70" } +.codicon-triangle-up:before { content: "\eb71" } +.codicon-twitter:before { content: "\eb72" } +.codicon-unfold:before { content: "\eb73" } +.codicon-unlock:before { content: "\eb74" } +.codicon-unmute:before { content: "\eb75" } +.codicon-unverified:before { content: "\eb76" } +.codicon-verified:before { content: "\eb77" } +.codicon-versions:before { content: "\eb78" } +.codicon-vm-active:before { content: "\eb79" } +.codicon-vm-outline:before { content: "\eb7a" } +.codicon-vm-running:before { content: "\eb7b" } +.codicon-watch:before { content: "\eb7c" } +.codicon-whitespace:before { content: "\eb7d" } +.codicon-whole-word:before { content: "\eb7e" } +.codicon-window:before { content: "\eb7f" } +.codicon-word-wrap:before { content: "\eb80" } +.codicon-zoom-in:before { content: "\eb81" } +.codicon-zoom-out:before { content: "\eb82" } +.codicon-list-filter:before { content: "\eb83" } +.codicon-list-flat:before { content: "\eb84" } +.codicon-list-selection:before { content: "\eb85" } +.codicon-selection:before { content: "\eb85" } +.codicon-list-tree:before { content: "\eb86" } +.codicon-debug-breakpoint-function-unverified:before { content: "\eb87" } +.codicon-debug-breakpoint-function:before { content: "\eb88" } +.codicon-debug-breakpoint-function-disabled:before { content: "\eb88" } +.codicon-debug-stackframe-active:before { content: "\eb89" } +.codicon-debug-stackframe-dot:before { content: "\eb8a" } +.codicon-debug-stackframe:before { content: "\eb8b" } +.codicon-debug-stackframe-focused:before { content: "\eb8b" } +.codicon-debug-breakpoint-unsupported:before { content: "\eb8c" } +.codicon-symbol-string:before { content: "\eb8d" } +.codicon-debug-reverse-continue:before { content: "\eb8e" } +.codicon-debug-step-back:before { content: "\eb8f" } +.codicon-debug-restart-frame:before { content: "\eb90" } +.codicon-debug-alternate:before { content: "\eb91" } +.codicon-call-incoming:before { content: "\eb92" } +.codicon-call-outgoing:before { content: "\eb93" } +.codicon-menu:before { content: "\eb94" } +.codicon-expand-all:before { content: "\eb95" } +.codicon-feedback:before { content: "\eb96" } +.codicon-group-by-ref-type:before { content: "\eb97" } +.codicon-ungroup-by-ref-type:before { content: "\eb98" } +.codicon-account:before { content: "\eb99" } +.codicon-bell-dot:before { content: "\eb9a" } +.codicon-debug-console:before { content: "\eb9b" } +.codicon-library:before { content: "\eb9c" } +.codicon-output:before { content: "\eb9d" } +.codicon-run-all:before { content: "\eb9e" } +.codicon-sync-ignored:before { content: "\eb9f" } +.codicon-pinned:before { content: "\eba0" } +.codicon-github-inverted:before { content: "\eba1" } +.codicon-debug-alt-2:before { content: "\f101" } +.codicon-debug-alt:before { content: "\f102" } diff --git a/src/datascience-ui/react-common/codicon/codicon.ts b/src/datascience-ui/react-common/codicon/codicon.ts new file mode 100644 index 000000000000..65ca2f1662aa --- /dev/null +++ b/src/datascience-ui/react-common/codicon/codicon.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// These values come from VS code source. +export enum CodIcon { + RunByLine = '\uead4', // This is actually debug-step-into + Stop = '\uead7' +} diff --git a/src/datascience-ui/react-common/codicon/codicon.ttf b/src/datascience-ui/react-common/codicon/codicon.ttf new file mode 100644 index 000000000000..b4066b7d08c4 Binary files /dev/null and b/src/datascience-ui/react-common/codicon/codicon.ttf differ diff --git a/src/datascience-ui/react-common/constants.ts b/src/datascience-ui/react-common/constants.ts new file mode 100644 index 000000000000..c7bd757316d7 --- /dev/null +++ b/src/datascience-ui/react-common/constants.ts @@ -0,0 +1,25 @@ +import { OSType } from '../../client/common/utils/platform'; + +// Javascript keyCodes +export const KeyCodes = { + LeftArrow: 37, + UpArrow: 38, + RightArrow: 39, + DownArrow: 40, + PageUp: 33, + PageDown: 34, + End: 35, + Home: 36 +}; + +export function getOSType() { + if (window.navigator.platform.startsWith('Mac')) { + return OSType.OSX; + } else if (window.navigator.platform.startsWith('Win')) { + return OSType.Windows; + } else if (window.navigator.userAgent.indexOf('Linux') > 0) { + return OSType.Linux; + } else { + return OSType.Unknown; + } +} diff --git a/src/datascience-ui/react-common/errorBoundary.tsx b/src/datascience-ui/react-common/errorBoundary.tsx new file mode 100644 index 000000000000..a37bc55cc147 --- /dev/null +++ b/src/datascience-ui/react-common/errorBoundary.tsx @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import * as React from 'react'; + +interface IErrorState { + hasError: boolean; + errorMessage: string; +} + +export class ErrorBoundary extends React.Component<{}, IErrorState> { + constructor(props: {}) { + super(props); + this.state = { hasError: false, errorMessage: '' }; + } + + public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + const stack = errorInfo.componentStack; + + // Display fallback UI + this.setState({ hasError: true, errorMessage: `${error} at \n ${stack}` }); + } + + public render() { + if (this.state.hasError) { + // Render our error message; + const style: React.CSSProperties = {}; + // tslint:disable-next-line:no-string-literal + style['whiteSpace'] = 'pre'; + + return <h1 style={style}>{this.state.errorMessage}</h1>; + } + return this.props.children; + } +} diff --git a/src/datascience-ui/react-common/event.ts b/src/datascience-ui/react-common/event.ts new file mode 100644 index 000000000000..ab2fed369556 --- /dev/null +++ b/src/datascience-ui/react-common/event.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +// tslint:disable: no-any +export type Event<T> = (listener: (e?: T) => any) => void; + +// Simpler version of the vscode event emitter for passing down through react components. +// Easier to manage than forwarding refs when not sure what the type of the ref should be. +// +// We can't use the vscode version because pulling in vscode apis is not allowed in a webview +export class EventEmitter<T> { + private _event: Event<T> | undefined; + private _listeners: Set<(e?: T) => any> = new Set<(e?: T) => any>(); + + public get event(): Event<T> { + if (!this._event) { + this._event = (listener: (e?: T) => any): void => { + this._listeners.add(listener); + }; + } + return this._event; + } + + public fire(data?: T): void { + this._listeners.forEach((c) => c(data)); + } + + public dispose(): void { + this._listeners.clear(); + } +} + +export interface IKeyboardEvent { + readonly code: string; + readonly target: HTMLElement; + readonly ctrlKey: boolean; + readonly shiftKey: boolean; + readonly altKey: boolean; + readonly metaKey: boolean; + readonly editorInfo?: { + isFirstLine: boolean; + isLastLine: boolean; + isSuggesting: boolean; + isDirty: boolean; + contents: string; + clear(): void; + }; + preventDefault(): void; + stopPropagation(): void; +} diff --git a/src/datascience-ui/react-common/flyout.css b/src/datascience-ui/react-common/flyout.css new file mode 100644 index 000000000000..3f9ad5ce827d --- /dev/null +++ b/src/datascience-ui/react-common/flyout.css @@ -0,0 +1,20 @@ + +.flyout-container { + position: relative; + height: 16px; + width: auto; +} + +.flyout-inner-disabled-filter { + opacity: 0.5; +} + +.flyout-button-hidden { + visibility: hidden; +} + +.flyout-children-hidden { + visibility: hidden; + width: 0px; +} + diff --git a/src/datascience-ui/react-common/flyout.tsx b/src/datascience-ui/react-common/flyout.tsx new file mode 100644 index 000000000000..3e3f634eddfc --- /dev/null +++ b/src/datascience-ui/react-common/flyout.tsx @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import './flyout.css'; + +import * as React from 'react'; + +interface IFlyoutProps { + buttonClassName: string; + flyoutContainerName: string; + buttonContent: JSX.Element; + buttonTooltip?: string; + disabled?: boolean; + hidden?: boolean; +} + +interface IFlyoutState { + visible: boolean; +} + +export class Flyout extends React.Component<IFlyoutProps, IFlyoutState> { + constructor(props: IFlyoutProps) { + super(props); + this.state = { visible: false }; + } + + public render() { + const innerFilter = this.props.disabled ? 'flyout-inner-disabled-filter' : ''; + const ariaDisabled = this.props.disabled ? 'true' : 'false'; + const buttonClassName = this.props.buttonClassName; + const flyoutClassName = this.state.visible + ? `flyout-children-visible ${this.props.flyoutContainerName}` + : `flyout-children-hidden ${this.props.flyoutContainerName}`; + + return ( + <div className="flyout-container" onMouseLeave={this.mouseLeave}> + <button + role="button" + aria-pressed="false" + disabled={this.props.disabled} + aria-disabled={ariaDisabled} + title={this.props.buttonTooltip} + aria-label={this.props.buttonTooltip} + onMouseEnter={this.mouseEnter} + className={buttonClassName} + > + <span className={innerFilter}>{this.props.buttonContent}</span> + </button> + <div className={flyoutClassName}>{this.props.children}</div> + </div> + ); + } + + private mouseEnter = () => { + this.setState({ visible: true }); + }; + + private mouseLeave = () => { + this.setState({ visible: false }); + }; +} diff --git a/src/datascience-ui/react-common/image.tsx b/src/datascience-ui/react-common/image.tsx new file mode 100644 index 000000000000..40b09332a062 --- /dev/null +++ b/src/datascience-ui/react-common/image.tsx @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as React from 'react'; +// tslint:disable-next-line:import-name match-default-export-name +import InlineSVG from 'svg-inline-react'; + +// This react component loads our svg files inline so that we can load them in vscode as it no longer +// supports loading svgs from disk. Please put new images in this list as appropriate. +export enum ImageName { + Cancel, + CollapseAll, + ExpandAll, + GoToSourceCode, + Interrupt, + OpenInNewWindow, + PopIn, + PopOut, + Redo, + Restart, + SaveAs, + Undo, + Pan, + Zoom, + ZoomOut, + Next, + Prev, + Copy, + GatherCode, + Up, + Down, + Run, + RunAbove, + RunBelow, + InsertAbove, + InsertBelow, + SwitchToCode, + SwitchToMarkdown, + OpenPlot, + RunAll, + Delete, + VariableExplorer, + ExportToPython, + ClearAllOutput, + JupyterServerConnected, + JupyterServerDisconnected, + Notebook, + Interactive, + Python, + PythonColor, + OpenFolder, + RunByLine, + Sync +} + +// All of the images must be 'require' so that webpack doesn't rewrite the import as requiring a .default. +// tslint:disable:no-require-imports +const images: { [key: string]: { light: string; dark: string } } = { + Cancel: { + light: require('./images/Cancel/Cancel_16xMD_vscode.svg'), + dark: require('./images/Cancel/Cancel_16xMD_vscode_dark.svg') + }, + CollapseAll: { + light: require('./images/CollapseAll/CollapseAll_16x_vscode.svg'), + dark: require('./images/CollapseAll/CollapseAll_16x_vscode_dark.svg') + }, + ExpandAll: { + light: require('./images/ExpandAll/ExpandAll_16x_vscode.svg'), + dark: require('./images/ExpandAll/ExpandAll_16x_vscode_dark.svg') + }, + GatherCode: { + light: require('./images/GatherCode/gather_light.svg'), + dark: require('./images/GatherCode/gather_dark.svg') + }, + GoToSourceCode: { + light: require('./images/GoToSourceCode/GoToSourceCode_16x_vscode.svg'), + dark: require('./images/GoToSourceCode/GoToSourceCode_16x_vscode_dark.svg') + }, + Interrupt: { + light: require('./images/Interrupt/Interrupt_16x_vscode.svg'), + dark: require('./images/Interrupt/Interrupt_16x_vscode_dark.svg') + }, + OpenInNewWindow: { + light: require('./images/OpenInNewWindow/OpenInNewWindow_16x_vscode.svg'), + dark: require('./images/OpenInNewWindow/OpenInNewWindow_16x_vscode_dark.svg') + }, + PopIn: { + light: require('./images/PopIn/PopIn_16x_vscode.svg'), + dark: require('./images/PopIn/PopIn_16x_vscode_dark.svg') + }, + PopOut: { + light: require('./images/PopOut/PopOut_16x_vscode.svg'), + dark: require('./images/PopOut/PopOut_16x_vscode_dark.svg') + }, + Redo: { + light: require('./images/Redo/Redo_16x_vscode.svg'), + dark: require('./images/Redo/Redo_16x_vscode_dark.svg') + }, + Restart: { + light: require('./images/Restart/Restart_grey_16x_vscode.svg'), + dark: require('./images/Restart/Restart_grey_16x_vscode_dark.svg') + }, + SaveAs: { + light: require('./images/SaveAs/SaveAs_16x_vscode.svg'), + dark: require('./images/SaveAs/SaveAs_16x_vscode_dark.svg') + }, + Undo: { + light: require('./images/Undo/Undo_16x_vscode.svg'), + dark: require('./images/Undo/Undo_16x_vscode_dark.svg') + }, + Next: { + light: require('./images/Next/next.svg'), + dark: require('./images/Next/next-inverse.svg') + }, + Prev: { + light: require('./images/Prev/previous.svg'), + dark: require('./images/Prev/previous-inverse.svg') + }, + // tslint:disable-next-line: no-suspicious-comment + // Todo: Get new images from a designer. These are all temporary. + Pan: { + light: require('./images/Pan/pan.svg'), + dark: require('./images/Pan/pan_inverse.svg') + }, + Zoom: { + light: require('./images/Zoom/zoom.svg'), + dark: require('./images/Zoom/zoom_inverse.svg') + }, + ZoomOut: { + light: require('./images/ZoomOut/zoomout.svg'), + dark: require('./images/ZoomOut/zoomout_inverse.svg') + }, + Copy: { + light: require('./images/Copy/copy.svg'), + dark: require('./images/Copy/copy_inverse.svg') + }, + Up: { + light: require('./images/Up/up.svg'), + dark: require('./images/Up/up-inverse.svg') + }, + Down: { + light: require('./images/Down/down.svg'), + dark: require('./images/Down/down-inverse.svg') + }, + Run: { + light: require('./images/Run/run-light.svg'), + dark: require('./images/Run/run-dark.svg') + }, + RunAbove: { + light: require('./images/RunAbove/runabove.svg'), + dark: require('./images/RunAbove/runabove-inverse.svg') + }, + RunBelow: { + light: require('./images/RunBelow/runbelow.svg'), + dark: require('./images/RunBelow/runbelow-inverse.svg') + }, + InsertAbove: { + light: require('./images/InsertAbove/above.svg'), + dark: require('./images/InsertAbove/above-inverse.svg') + }, + InsertBelow: { + light: require('./images/InsertBelow/below.svg'), + dark: require('./images/InsertBelow/below-inverse.svg') + }, + SwitchToCode: { + light: require('./images/SwitchToCode/switchtocode.svg'), + dark: require('./images/SwitchToCode/switchtocode-inverse.svg') + }, + SwitchToMarkdown: { + light: require('./images/SwitchToMarkdown/switchtomarkdown.svg'), + dark: require('./images/SwitchToMarkdown/switchtomarkdown-inverse.svg') + }, + OpenPlot: { + light: require('./images/OpenPlot/plot_light.svg'), + dark: require('./images/OpenPlot/plot_dark.svg') + }, + RunAll: { + light: require('./images/RunAll/run_all_light.svg'), + dark: require('./images/RunAll/run_all_dark.svg') + }, + Delete: { + light: require('./images/Delete/delete_light.svg'), + dark: require('./images/Delete/delete_dark.svg') + }, + VariableExplorer: { + light: require('./images/VariableExplorer/variable_explorer_light.svg'), + dark: require('./images/VariableExplorer/variable_explorer_dark.svg') + }, + ExportToPython: { + light: require('./images/ExportToPython/export_to_python_light.svg'), + dark: require('./images/ExportToPython/export_to_python_dark.svg') + }, + ClearAllOutput: { + light: require('./images/ClearAllOutput/clear_all_output_light.svg'), + dark: require('./images/ClearAllOutput/clear_all_output_dark.svg') + }, + JupyterServerConnected: { + light: require('./images/JupyterServerConnected/connected-light.svg'), + dark: require('./images/JupyterServerConnected/connected-dark.svg') + }, + JupyterServerDisconnected: { + light: require('./images/JupyterServerDisconnected/disconnected-light.svg'), + dark: require('./images/JupyterServerDisconnected/disconnected-dark.svg') + }, + Notebook: { + light: require('./images/StartPage/Notebook.svg'), + dark: require('./images/StartPage/Notebook-inverse.svg') + }, + Interactive: { + light: require('./images/StartPage/Interactive.svg'), + dark: require('./images/StartPage/Interactive-inverse.svg') + }, + Python: { + light: require('./images/StartPage/Python.svg'), + dark: require('./images/StartPage/Python-inverse.svg') + }, + PythonColor: { + light: require('./images/StartPage/Python-color.svg'), + dark: require('./images/StartPage/Python-color.svg') + }, + OpenFolder: { + light: require('./images/StartPage/OpenFolder.svg'), + dark: require('./images/StartPage/OpenFolder-inverse.svg') + }, + RunByLine: { + light: require('./images/RunByLine/runbyline_light.svg'), + dark: require('./images/RunByLine/runbyline_dark.svg') + }, + Sync: { + light: require('./images/Sync/sync.svg'), + dark: require('./images/Sync/sync-inverse.svg') + } +}; + +interface IImageProps { + baseTheme: string; + image: ImageName; + class: string; + title?: string; +} + +export class Image extends React.Component<IImageProps> { + constructor(props: IImageProps) { + super(props); + } + + public render() { + const key = ImageName[this.props.image].toString(); + const image = images.hasOwnProperty(key) ? images[key] : images.Cancel; // Default is cancel. + const source = this.props.baseTheme.includes('dark') ? image.dark : image.light; + return <InlineSVG className={this.props.class} src={source} title={this.props.title} />; + } +} diff --git a/src/datascience-ui/react-common/imageButton.css b/src/datascience-ui/react-common/imageButton.css new file mode 100644 index 000000000000..1b47cd8b6d8e --- /dev/null +++ b/src/datascience-ui/react-common/imageButton.css @@ -0,0 +1,50 @@ +:root { + --button-size: 18px; +} + +.image-button { + border-width: 0px; + border-style: solid; + text-align: center; + line-height: 16px; + overflow: hidden; + width: var(--button-size); + height: var(--button-size); + margin-left: 2px; + padding: 1px; + background-color: transparent; + cursor: hand; +} + +.image-button-empty { + visibility: hidden; +} + +.image-button-inner-disabled-filter { + opacity: 0.5; +} + +.image-button-child { + max-width: 100%; + max-height: 100%; +} + +.image-button-child img{ + max-width: 100%; + max-height: 100%; +} + +.image-button-image svg{ + pointer-events: none; +} + +.image-button-vscode-light:disabled { + border-color: gray; + filter: grayscale(100%); +} + +.image-button-vscode-dark:disabled { + border-color: gray; + filter: grayscale(100%); +} + diff --git a/src/datascience-ui/react-common/imageButton.tsx b/src/datascience-ui/react-common/imageButton.tsx new file mode 100644 index 000000000000..6105b49ec693 --- /dev/null +++ b/src/datascience-ui/react-common/imageButton.tsx @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import * as React from 'react'; +import './imageButton.css'; + +interface IImageButtonProps { + baseTheme: string; + tooltip: string; + disabled?: boolean; + hidden?: boolean; + className?: string; + onClick?(event?: React.MouseEvent<HTMLButtonElement>): void; + onMouseDown?(event?: React.MouseEvent<HTMLButtonElement>): void; +} + +export class ImageButton extends React.Component<IImageButtonProps> { + constructor(props: IImageButtonProps) { + super(props); + } + + public render() { + const classNames = `image-button image-button-${this.props.baseTheme} ${this.props.hidden ? 'hide' : ''} ${ + this.props.className ? this.props.className : '' + }`; + const innerFilter = this.props.disabled ? 'image-button-inner-disabled-filter' : ''; + const ariaDisabled = this.props.disabled ? 'true' : 'false'; + + return ( + <button + role="button" + aria-pressed="false" + disabled={this.props.disabled} + aria-disabled={ariaDisabled} + title={this.props.tooltip} + aria-label={this.props.tooltip} + className={classNames} + onClick={this.props.onClick} + onMouseDown={this.props.onMouseDown} + > + <span className={innerFilter}> + <span className="image-button-child">{this.props.children}</span> + </span> + </button> + ); + } +} diff --git a/src/datascience-ui/react-common/images/Cancel/Cancel_16xMD_vscode.svg b/src/datascience-ui/react-common/images/Cancel/Cancel_16xMD_vscode.svg new file mode 100644 index 000000000000..6cf0d7996f55 --- /dev/null +++ b/src/datascience-ui/react-common/images/Cancel/Cancel_16xMD_vscode.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M8.00001 8.70708L11.6465 12.3535L12.3536 11.6464L8.70711 7.99998L12.3536 4.35353L11.6465 3.64642L8.00001 7.29287L4.35356 3.64642L3.64645 4.35353L7.2929 7.99998L3.64645 11.6464L4.35356 12.3535L8.00001 8.70708Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Cancel/Cancel_16xMD_vscode_dark.svg b/src/datascience-ui/react-common/images/Cancel/Cancel_16xMD_vscode_dark.svg new file mode 100644 index 000000000000..c4df2f91fee2 --- /dev/null +++ b/src/datascience-ui/react-common/images/Cancel/Cancel_16xMD_vscode_dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M8.00001 8.70708L11.6465 12.3535L12.3536 11.6464L8.70711 7.99998L12.3536 4.35353L11.6465 3.64642L8.00001 7.29287L4.35356 3.64642L3.64645 4.35353L7.2929 7.99998L3.64645 11.6464L4.35356 12.3535L8.00001 8.70708Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/ClearAllOutput/clear_all_output_dark.svg b/src/datascience-ui/react-common/images/ClearAllOutput/clear_all_output_dark.svg new file mode 100644 index 000000000000..f8811f5ea310 --- /dev/null +++ b/src/datascience-ui/react-common/images/ClearAllOutput/clear_all_output_dark.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important;" d="M5 13.25H13.25V10.25H5V13.25ZM5.75 11H12.5V12.5H5.75V11ZM13.25 2.75V5.75H7.5605L6.833 5.0225L6.85625 5H12.5V3.5H8.2655L7.5155 2.75H13.25ZM8.3105 6.5H13.25V9.5H5V6.90125L5.75 7.65125V8.75H12.5V7.25H7.5605L8.3105 6.5Z"/> +<path style="fill: #F48771 !important;" d="M5.77258 5.0225L7.25008 6.5L6.45508 7.295L4.97758 5.81825L3.50008 7.295L2.70508 6.5L4.18183 5.0225L2.70508 3.545L3.50008 2.75L4.97758 4.2275L6.45508 2.75L7.25008 3.545L5.77258 5.0225Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/ClearAllOutput/clear_all_output_light.svg b/src/datascience-ui/react-common/images/ClearAllOutput/clear_all_output_light.svg new file mode 100644 index 000000000000..2c2015b6cb27 --- /dev/null +++ b/src/datascience-ui/react-common/images/ClearAllOutput/clear_all_output_light.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M5 13.25H13.25V10.25H5V13.25ZM5.75 11H12.5V12.5H5.75V11ZM13.25 2.75V5.75H7.5605L6.833 5.0225L6.85625 5H12.5V3.5H8.2655L7.5155 2.75H13.25ZM8.3105 6.5H13.25V9.5H5V6.90125L5.75 7.65125V8.75H12.5V7.25H7.5605L8.3105 6.5Z"/> +<path style="fill: #A1260D !important;" d="M5.77258 5.0225L7.25008 6.5L6.45508 7.295L4.97758 5.81825L3.50008 7.295L2.70508 6.5L4.18183 5.0225L2.70508 3.545L3.50008 2.75L4.97758 4.2275L6.45508 2.75L7.25008 3.545L5.77258 5.0225Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode.svg b/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode.svg new file mode 100644 index 000000000000..ffddd4cbdb95 --- /dev/null +++ b/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M9 9H4V10H9V9Z"/> +<path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M5 3L6 2H13L14 3V10L13 11H11V13L10 14H3L2 13V6L3 5H5V3ZM6 5H10L11 6V10H13V3H6V5ZM10 6H3V13H10V6Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode_dark.svg new file mode 100644 index 000000000000..11e1f39ad968 --- /dev/null +++ b/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode_dark.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important;" d="M9 9H4V10H9V9Z"/> +<path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M5 3L6 2H13L14 3V10L13 11H11V13L10 14H3L2 13V6L3 5H5V3ZM6 5H10L11 6V10H13V3H6V5ZM10 6H3V13H10V6Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Copy/copy.svg b/src/datascience-ui/react-common/images/Copy/copy.svg new file mode 100644 index 000000000000..c8d97a3e23be --- /dev/null +++ b/src/datascience-ui/react-common/images/Copy/copy.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M13.25 6.7168V14H5.75V11.75H2.75V2H7.7832L10.0332 4.25H10.7832L13.25 6.7168ZM11 6.5H11.9668L11 5.5332V6.5ZM5.75 4.25H8.9668L7.4668 2.75H3.5V11H5.75V4.25ZM12.5 7.25H10.25V5H6.5V13.25H12.5V7.25Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Copy/copy_inverse.svg b/src/datascience-ui/react-common/images/Copy/copy_inverse.svg new file mode 100644 index 000000000000..b5d2606adb65 --- /dev/null +++ b/src/datascience-ui/react-common/images/Copy/copy_inverse.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important;" d="M13.25 6.7168V14H5.75V11.75H2.75V2H7.7832L10.0332 4.25H10.7832L13.25 6.7168ZM11 6.5H11.9668L11 5.5332V6.5ZM5.75 4.25H8.9668L7.4668 2.75H3.5V11H5.75V4.25ZM12.5 7.25H10.25V5H6.5V13.25H12.5V7.25Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Delete/delete_dark.svg b/src/datascience-ui/react-common/images/Delete/delete_dark.svg new file mode 100644 index 000000000000..909517562dfb --- /dev/null +++ b/src/datascience-ui/react-common/images/Delete/delete_dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M6.69231 2.92308H9.46154V3.84615H10.3846V2.92308C10.3846 2.41328 9.97134 2 9.46154 2H6.69231C6.18251 2 5.76923 2.41328 5.76923 2.92308V3.84615H6.69231V2.92308ZM3.92308 3.84616H3V4.76924H3.92308V13.0769L4.84616 14H11.3077L12.2308 13.0769V4.76924H13.1538V3.84616H12.2308H11.3077H4.84616H3.92308ZM4.84616 4.76924V13.0769H11.3077V4.76924H4.84616ZM6.69231 5.69232H5.76923V12.1539H6.69231V5.69232ZM7.61538 5.69232H8.53846V12.1539H7.61538V5.69232ZM10.3846 5.69232H9.46154V12.1539H10.3846V5.69232Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Delete/delete_light.svg b/src/datascience-ui/react-common/images/Delete/delete_light.svg new file mode 100644 index 000000000000..708df4baba32 --- /dev/null +++ b/src/datascience-ui/react-common/images/Delete/delete_light.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M6.69231 2.92308H9.46154V3.84615H10.3846V2.92308C10.3846 2.41328 9.97134 2 9.46154 2H6.69231C6.18251 2 5.76923 2.41328 5.76923 2.92308V3.84615H6.69231V2.92308ZM3.92308 3.84616H3V4.76924H3.92308V13.0769L4.84616 14H11.3077L12.2308 13.0769V4.76924H13.1538V3.84616H12.2308H11.3077H4.84616H3.92308ZM4.84616 4.76924V13.0769H11.3077V4.76924H4.84616ZM6.69231 5.69232H5.76923V12.1539H6.69231V5.69232ZM7.61538 5.69232H8.53846V12.1539H7.61538V5.69232ZM10.3846 5.69232H9.46154V12.1539H10.3846V5.69232Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Down/down-inverse.svg b/src/datascience-ui/react-common/images/Down/down-inverse.svg new file mode 100644 index 000000000000..d3015672bd49 --- /dev/null +++ b/src/datascience-ui/react-common/images/Down/down-inverse.svg @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + id="svg4" + version="1.1" + fill="none" + viewBox="0 0 16 16" + height="16" + width="16"> + <metadata + id="metadata10"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs8" /> + <path + style="clip-rule:evenodd;fill:#c5c5c5 !important;fill-rule:evenodd" + id="path2" + d="M 8,9.95954 3.02022,4.97976 2.31311,5.68686 7.64644,11.0202 H 8.35355 L 13.6869,5.68686 12.9798,4.97976 Z" /> +</svg> diff --git a/src/datascience-ui/react-common/images/Down/down.svg b/src/datascience-ui/react-common/images/Down/down.svg new file mode 100644 index 000000000000..30d5faa11be2 --- /dev/null +++ b/src/datascience-ui/react-common/images/Down/down.svg @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + id="svg4" + version="1.1" + fill="none" + viewBox="0 0 16 16" + height="16" + width="16"> + <metadata + id="metadata10"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs8" /> + <path + style="clip-rule:evenodd;fill:#424242 !important;fill-rule:evenodd" + id="path2" + d="M 8,9.95954 3.02022,4.97976 2.31311,5.68686 7.64644,11.0202 H 8.35355 L 13.6869,5.68686 12.9798,4.97976 Z" /> +</svg> diff --git a/src/datascience-ui/react-common/images/ExpandAll/ExpandAll_16x_vscode.svg b/src/datascience-ui/react-common/images/ExpandAll/ExpandAll_16x_vscode.svg new file mode 100644 index 000000000000..d82b92292bee --- /dev/null +++ b/src/datascience-ui/react-common/images/ExpandAll/ExpandAll_16x_vscode.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M9 9H4V10H9V9Z"/> +<path style="fill: #424242 !important;" d="M7 12L7 7L6 7L6 12L7 12Z"/> +<path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M5 3L6 2H13L14 3V10L13 11H11V13L10 14H3L2 13V6L3 5H5V3ZM6 5H10L11 6V10H13V3H6V5ZM10 6H3V13H10V6Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/ExpandAll/ExpandAll_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/ExpandAll/ExpandAll_16x_vscode_dark.svg new file mode 100644 index 000000000000..8db4b6139c80 --- /dev/null +++ b/src/datascience-ui/react-common/images/ExpandAll/ExpandAll_16x_vscode_dark.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important;" d="M9 9H4V10H9V9Z"/> +<path style="fill: #C5C5C5 !important;" d="M7 12L7 7L6 7L6 12L7 12Z"/> +<path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M5 3L6 2H13L14 3V10L13 11H11V13L10 14H3L2 13V6L3 5H5V3ZM6 5H10L11 6V10H13V3H6V5ZM10 6H3V13H10V6Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/ExportToPython/export_to_python_dark.svg b/src/datascience-ui/react-common/images/ExportToPython/export_to_python_dark.svg new file mode 100644 index 000000000000..a68ca2942cb7 --- /dev/null +++ b/src/datascience-ui/react-common/images/ExportToPython/export_to_python_dark.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important;" d="M11 7.394L13.0177 9.41086L12.4109 10.0169L11.4286 9.03457V12.2857H8V11.4286H10.5714V9.03457L9.58914 10.0169L8.98229 9.41086L11 7.394ZM14 3.53686V8.85714H13.1429V4.57143H11.4286V2.85714H9.71429V8H8.85714V2H12.4631L14 3.53686ZM12.9654 3.71429L12.2857 3.03457V3.71429H12.9654ZM2 12.2857H7.14286V11.4286H2V12.2857ZM2 14H7.14286V13.1429H2V14ZM2 10.5714H7.14286V9.71429H2V10.5714Z"/> +<path style="fill: #75BEFF !important;" d="M11 7.38538L13.0177 9.40223L12.4109 10.0082L11.4286 9.02595V12.2771H8V11.4199H10.5714V9.02595L9.58914 10.0082L8.98229 9.40223L11 7.38538Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/ExportToPython/export_to_python_light.svg b/src/datascience-ui/react-common/images/ExportToPython/export_to_python_light.svg new file mode 100644 index 000000000000..873383aaeb21 --- /dev/null +++ b/src/datascience-ui/react-common/images/ExportToPython/export_to_python_light.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M11 7.394L13.0177 9.41086L12.4109 10.0169L11.4286 9.03457V12.2857H8V11.4286H10.5714V9.03457L9.58914 10.0169L8.98229 9.41086L11 7.394ZM14 3.53686V8.85714H13.1429V4.57143H11.4286V2.85714H9.71429V8H8.85714V2H12.4631L14 3.53686ZM12.9654 3.71429L12.2857 3.03457V3.71429H12.9654ZM2 12.2857H7.14286V11.4286H2V12.2857ZM2 14H7.14286V13.1429H2V14ZM2 10.5714H7.14286V9.71429H2V10.5714Z"/> +<path style="fill: #007ACC !important;" d="M11 7.38538L13.0177 9.40223L12.4109 10.0082L11.4286 9.02595V12.2771H8V11.4199H10.5714V9.02595L9.58914 10.0082L8.98229 9.40223L11 7.38538Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/GatherCode/gather_dark.svg b/src/datascience-ui/react-common/images/GatherCode/gather_dark.svg new file mode 100644 index 000000000000..135fc3c97683 --- /dev/null +++ b/src/datascience-ui/react-common/images/GatherCode/gather_dark.svg @@ -0,0 +1,12 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> + <defs> + <style>.canvas{fill:none;opacity:0;}.cls-1{fill:#C5C5C5;fill-rule:evenodd;}</style> + </defs> + <title>GatherIcon_16x</title> + <g id="canvas"> + <path class="canvas" d="M16,16H0V0H16Z"/> + </g> + <g id="level-1"> + <path class="cls-1" d="M1.5,1,1,1.5v3l.5.5h3L5,4.5v-3L4.5,1ZM2,4V2H4V4ZM1.5,6,1,6.5v3l.5.5h3L5,9.5v-3L4.5,6ZM2,9V7H4V9ZM1,11.5l.5-.5h3l.5.5v3l-.5.5h-3L1,14.5ZM2,12v2H4V12ZM12.5,5l-.5.5v6l.5.5h3l.5-.5v-6L15.5,5ZM15,8H13V6h2Zm0,3H13V9h2ZM9.1,8H6V9H9.1l-1,1,.7.6,1.8-1.8V8.1L8.8,6.3,8.1,7Z"/> + </g> +</svg> diff --git a/src/datascience-ui/react-common/images/GatherCode/gather_light.svg b/src/datascience-ui/react-common/images/GatherCode/gather_light.svg new file mode 100644 index 000000000000..72ec63e06b65 --- /dev/null +++ b/src/datascience-ui/react-common/images/GatherCode/gather_light.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> + <defs> + <style>.canvas{fill:none;opacity:0;}.cls-1{fill:#424242;fill-rule:evenodd;}</style> + </defs> + <title>GatherIcon_16x</title> + <g id="canvas"> + <path class="canvas" d="M16,16H0V0H16Z"/> + </g> + <g id="level-1"> + <path class="cls-1" d="M1.5,1,1,1.5v3l.5.5h3L5,4.5v-3L4.5,1ZM2,4V2H4V4ZM1.5,6,1,6.5v3l.5.5h3L5,9.5v-3L4.5,6ZM2,9V7H4V9ZM1,11.5l.5-.5h3l.5.5v3l-.5.5h-3L1,14.5ZM2,12v2H4V12ZM12.5,5l-.5.5v6l.5.5h3l.5-.5v-6L15.5,5ZM15,8H13V6h2Zm0,3H13V9h2ZM9.1,8H6V9H9.1l-1,1,.7.6,1.8-1.8V8.1L8.8,6.3,8.1,7Z"/> + </g> +</svg> + diff --git a/src/datascience-ui/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode.svg b/src/datascience-ui/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode.svg new file mode 100644 index 000000000000..d4bca23d4a0e --- /dev/null +++ b/src/datascience-ui/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M8.06065 3.85356L5.91421 6L5.2071 5.29289L6.49999 4H3.5C3.10218 4 2.72064 4.15804 2.43934 4.43934C2.15804 4.72065 2 5.10218 2 5.5C2 5.89783 2.15804 6.27936 2.43934 6.56066C2.72064 6.84197 3.10218 7 3.5 7H4V8H3.5C2.83696 8 2.20107 7.73661 1.73223 7.26777C1.26339 6.79893 1 6.16305 1 5.5C1 4.83696 1.26339 4.20108 1.73223 3.73224C2.20107 3.2634 2.83696 3 3.5 3H6.49999L6.49999 3H6.49996L6 2.50004V2.50001L5.2071 1.70711L5.91421 1L8.06065 3.14645L8.06065 3.85356ZM5 6.50003L5.91421 7.41424L6 7.32845V14H14V7H10V3H9.06065V2.73227L8.32838 2H11.2L11.5 2.1L14.9 5.6L15 6V14.5L14.5 15H5.5L5 14.5V9.00003V6.50003ZM11 3V6H13.9032L11 3Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode_dark.svg new file mode 100644 index 000000000000..b54d042ca46d --- /dev/null +++ b/src/datascience-ui/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode_dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M8.06065 3.85356L5.91421 6L5.2071 5.29289L6.49999 4H3.5C3.10218 4 2.72064 4.15804 2.43934 4.43934C2.15804 4.72065 2 5.10218 2 5.5C2 5.89783 2.15804 6.27936 2.43934 6.56066C2.72064 6.84197 3.10218 7 3.5 7H4V8H3.5C2.83696 8 2.20107 7.73661 1.73223 7.26777C1.26339 6.79893 1 6.16305 1 5.5C1 4.83696 1.26339 4.20108 1.73223 3.73224C2.20107 3.2634 2.83696 3 3.5 3H6.49999L6.49999 3H6.49996L6 2.50004V2.50001L5.2071 1.70711L5.91421 1L8.06065 3.14645L8.06065 3.85356ZM5 6.50003L5.91421 7.41424L6 7.32845V14H14V7H10V3H9.06065V2.73227L8.32838 2H11.2L11.5 2.1L14.9 5.6L15 6V14.5L14.5 15H5.5L5 14.5V9.00003V6.50003ZM11 3V6H13.9032L11 3Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/InsertAbove/above-inverse.svg b/src/datascience-ui/react-common/images/InsertAbove/above-inverse.svg new file mode 100644 index 000000000000..6b36517f6efa --- /dev/null +++ b/src/datascience-ui/react-common/images/InsertAbove/above-inverse.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important;" d="M14 7V8H8V14H7V8H1V7H7V1H8V7H14Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/InsertAbove/above.svg b/src/datascience-ui/react-common/images/InsertAbove/above.svg new file mode 100644 index 000000000000..cc0ee1bbd824 --- /dev/null +++ b/src/datascience-ui/react-common/images/InsertAbove/above.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M14 7V8H8V14H7V8H1V7H7V1H8V7H14Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/InsertBelow/below-inverse.svg b/src/datascience-ui/react-common/images/InsertBelow/below-inverse.svg new file mode 100644 index 000000000000..6b36517f6efa --- /dev/null +++ b/src/datascience-ui/react-common/images/InsertBelow/below-inverse.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important;" d="M14 7V8H8V14H7V8H1V7H7V1H8V7H14Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/InsertBelow/below.svg b/src/datascience-ui/react-common/images/InsertBelow/below.svg new file mode 100644 index 000000000000..cc0ee1bbd824 --- /dev/null +++ b/src/datascience-ui/react-common/images/InsertBelow/below.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M14 7V8H8V14H7V8H1V7H7V1H8V7H14Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Interrupt/Interrupt_16x_vscode.svg b/src/datascience-ui/react-common/images/Interrupt/Interrupt_16x_vscode.svg new file mode 100644 index 000000000000..391089006ec5 --- /dev/null +++ b/src/datascience-ui/react-common/images/Interrupt/Interrupt_16x_vscode.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #F48771 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M12.75 3.25H3.25V12.75H12.75V3.25ZM2 2V14H14V2H2Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Interrupt/Interrupt_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/Interrupt/Interrupt_16x_vscode_dark.svg new file mode 100644 index 000000000000..391089006ec5 --- /dev/null +++ b/src/datascience-ui/react-common/images/Interrupt/Interrupt_16x_vscode_dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #F48771 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M12.75 3.25H3.25V12.75H12.75V3.25ZM2 2V14H14V2H2Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/JupyterServerConnected/connected-dark.svg b/src/datascience-ui/react-common/images/JupyterServerConnected/connected-dark.svg new file mode 100644 index 000000000000..f44818031c24 --- /dev/null +++ b/src/datascience-ui/react-common/images/JupyterServerConnected/connected-dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #89D185 !important;" d="M2.75 9.3125C2.75 8.9069 2.82747 8.51725 2.98242 8.14355C3.13737 7.7653 3.3584 7.43262 3.64551 7.14551L5.19727 5.60059L9.52441 9.92773L7.97949 11.4795C7.69238 11.7666 7.3597 11.9876 6.98145 12.1426C6.60775 12.2975 6.2181 12.375 5.8125 12.375C5.47982 12.375 5.15625 12.3249 4.8418 12.2246C4.5319 12.1198 4.24479 11.9671 3.98047 11.7666L1.74512 13.9951L1.12988 13.3799L3.3584 11.1445C3.15788 10.8802 3.00521 10.5931 2.90039 10.2832C2.80013 9.96875 2.75 9.64518 2.75 9.3125ZM5.8125 11.5C6.10417 11.5 6.38216 11.4453 6.64648 11.3359C6.91536 11.222 7.15234 11.0625 7.35742 10.8574L8.28711 9.92773L5.19727 6.83789L4.26758 7.76758C4.0625 7.97266 3.90299 8.20964 3.78906 8.47852C3.67969 8.74284 3.625 9.02083 3.625 9.3125C3.625 9.61328 3.68197 9.89811 3.7959 10.167C3.90983 10.4313 4.06478 10.6637 4.26074 10.8643C4.46126 11.0602 4.69368 11.2152 4.95801 11.3291C5.22689 11.443 5.51172 11.5 5.8125 11.5ZM11.7666 3.98047C11.9671 4.24479 12.1175 4.53418 12.2178 4.84863C12.3226 5.15853 12.375 5.47982 12.375 5.8125C12.375 6.2181 12.2975 6.61003 12.1426 6.98828C11.9876 7.36198 11.7666 7.69238 11.4795 7.97949L9.92773 9.52441L5.60059 5.19727L7.14551 3.64551C7.43262 3.3584 7.76302 3.13737 8.13672 2.98242C8.51497 2.82747 8.9069 2.75 9.3125 2.75C9.64518 2.75 9.96647 2.80241 10.2764 2.90723C10.5908 3.00749 10.8802 3.15788 11.1445 3.3584L13.3799 1.12988L13.9951 1.74512L11.7666 3.98047ZM10.8574 7.35742C11.0625 7.15234 11.2197 6.91764 11.3291 6.65332C11.443 6.38444 11.5 6.10417 11.5 5.8125C11.5 5.51172 11.4408 5.22917 11.3223 4.96484C11.2083 4.70052 11.0511 4.47038 10.8506 4.27441C10.6546 4.07389 10.4245 3.91667 10.1602 3.80273C9.89583 3.68424 9.61328 3.625 9.3125 3.625C9.02083 3.625 8.74056 3.68197 8.47168 3.7959C8.20736 3.90527 7.97266 4.0625 7.76758 4.26758L6.83789 5.19727L9.92773 8.28711L10.8574 7.35742Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/JupyterServerConnected/connected-hc.svg b/src/datascience-ui/react-common/images/JupyterServerConnected/connected-hc.svg new file mode 100644 index 000000000000..f44818031c24 --- /dev/null +++ b/src/datascience-ui/react-common/images/JupyterServerConnected/connected-hc.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #89D185 !important;" d="M2.75 9.3125C2.75 8.9069 2.82747 8.51725 2.98242 8.14355C3.13737 7.7653 3.3584 7.43262 3.64551 7.14551L5.19727 5.60059L9.52441 9.92773L7.97949 11.4795C7.69238 11.7666 7.3597 11.9876 6.98145 12.1426C6.60775 12.2975 6.2181 12.375 5.8125 12.375C5.47982 12.375 5.15625 12.3249 4.8418 12.2246C4.5319 12.1198 4.24479 11.9671 3.98047 11.7666L1.74512 13.9951L1.12988 13.3799L3.3584 11.1445C3.15788 10.8802 3.00521 10.5931 2.90039 10.2832C2.80013 9.96875 2.75 9.64518 2.75 9.3125ZM5.8125 11.5C6.10417 11.5 6.38216 11.4453 6.64648 11.3359C6.91536 11.222 7.15234 11.0625 7.35742 10.8574L8.28711 9.92773L5.19727 6.83789L4.26758 7.76758C4.0625 7.97266 3.90299 8.20964 3.78906 8.47852C3.67969 8.74284 3.625 9.02083 3.625 9.3125C3.625 9.61328 3.68197 9.89811 3.7959 10.167C3.90983 10.4313 4.06478 10.6637 4.26074 10.8643C4.46126 11.0602 4.69368 11.2152 4.95801 11.3291C5.22689 11.443 5.51172 11.5 5.8125 11.5ZM11.7666 3.98047C11.9671 4.24479 12.1175 4.53418 12.2178 4.84863C12.3226 5.15853 12.375 5.47982 12.375 5.8125C12.375 6.2181 12.2975 6.61003 12.1426 6.98828C11.9876 7.36198 11.7666 7.69238 11.4795 7.97949L9.92773 9.52441L5.60059 5.19727L7.14551 3.64551C7.43262 3.3584 7.76302 3.13737 8.13672 2.98242C8.51497 2.82747 8.9069 2.75 9.3125 2.75C9.64518 2.75 9.96647 2.80241 10.2764 2.90723C10.5908 3.00749 10.8802 3.15788 11.1445 3.3584L13.3799 1.12988L13.9951 1.74512L11.7666 3.98047ZM10.8574 7.35742C11.0625 7.15234 11.2197 6.91764 11.3291 6.65332C11.443 6.38444 11.5 6.10417 11.5 5.8125C11.5 5.51172 11.4408 5.22917 11.3223 4.96484C11.2083 4.70052 11.0511 4.47038 10.8506 4.27441C10.6546 4.07389 10.4245 3.91667 10.1602 3.80273C9.89583 3.68424 9.61328 3.625 9.3125 3.625C9.02083 3.625 8.74056 3.68197 8.47168 3.7959C8.20736 3.90527 7.97266 4.0625 7.76758 4.26758L6.83789 5.19727L9.92773 8.28711L10.8574 7.35742Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/JupyterServerConnected/connected-light.svg b/src/datascience-ui/react-common/images/JupyterServerConnected/connected-light.svg new file mode 100644 index 000000000000..28987e1c1723 --- /dev/null +++ b/src/datascience-ui/react-common/images/JupyterServerConnected/connected-light.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #388A34 !important;" d="M2.75 9.3125C2.75 8.9069 2.82747 8.51725 2.98242 8.14355C3.13737 7.7653 3.3584 7.43262 3.64551 7.14551L5.19727 5.60059L9.52441 9.92773L7.97949 11.4795C7.69238 11.7666 7.3597 11.9876 6.98145 12.1426C6.60775 12.2975 6.2181 12.375 5.8125 12.375C5.47982 12.375 5.15625 12.3249 4.8418 12.2246C4.5319 12.1198 4.24479 11.9671 3.98047 11.7666L1.74512 13.9951L1.12988 13.3799L3.3584 11.1445C3.15788 10.8802 3.00521 10.5931 2.90039 10.2832C2.80013 9.96875 2.75 9.64518 2.75 9.3125ZM5.8125 11.5C6.10417 11.5 6.38216 11.4453 6.64648 11.3359C6.91536 11.222 7.15234 11.0625 7.35742 10.8574L8.28711 9.92773L5.19727 6.83789L4.26758 7.76758C4.0625 7.97266 3.90299 8.20964 3.78906 8.47852C3.67969 8.74284 3.625 9.02083 3.625 9.3125C3.625 9.61328 3.68197 9.89811 3.7959 10.167C3.90983 10.4313 4.06478 10.6637 4.26074 10.8643C4.46126 11.0602 4.69368 11.2152 4.95801 11.3291C5.22689 11.443 5.51172 11.5 5.8125 11.5ZM11.7666 3.98047C11.9671 4.24479 12.1175 4.53418 12.2178 4.84863C12.3226 5.15853 12.375 5.47982 12.375 5.8125C12.375 6.2181 12.2975 6.61003 12.1426 6.98828C11.9876 7.36198 11.7666 7.69238 11.4795 7.97949L9.92773 9.52441L5.60059 5.19727L7.14551 3.64551C7.43262 3.3584 7.76302 3.13737 8.13672 2.98242C8.51497 2.82747 8.9069 2.75 9.3125 2.75C9.64518 2.75 9.96647 2.80241 10.2764 2.90723C10.5908 3.00749 10.8802 3.15788 11.1445 3.3584L13.3799 1.12988L13.9951 1.74512L11.7666 3.98047ZM10.8574 7.35742C11.0625 7.15234 11.2197 6.91764 11.3291 6.65332C11.443 6.38444 11.5 6.10417 11.5 5.8125C11.5 5.51172 11.4408 5.22917 11.3223 4.96484C11.2083 4.70052 11.0511 4.47038 10.8506 4.27441C10.6546 4.07389 10.4245 3.91667 10.1602 3.80273C9.89583 3.68424 9.61328 3.625 9.3125 3.625C9.02083 3.625 8.74056 3.68197 8.47168 3.7959C8.20736 3.90527 7.97266 4.0625 7.76758 4.26758L6.83789 5.19727L9.92773 8.28711L10.8574 7.35742Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-dark.svg b/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-dark.svg new file mode 100644 index 000000000000..02a598dc9878 --- /dev/null +++ b/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #F48771 !important;" d="M8.88672 9.20996L7.03418 11.0625L7.64941 11.6777L6.10449 13.2295C5.81738 13.5166 5.4847 13.7376 5.10645 13.8926C4.73275 14.0475 4.3431 14.125 3.9375 14.125C3.60482 14.125 3.28125 14.0749 2.9668 13.9746C2.6569 13.8698 2.36979 13.7171 2.10547 13.5166L0.745117 14.8701L0.129883 14.2549L1.4834 12.8945C1.28288 12.6302 1.13021 12.3431 1.02539 12.0332C0.92513 11.7188 0.875 11.3952 0.875 11.0625C0.875 10.6569 0.952474 10.2673 1.10742 9.89355C1.26237 9.5153 1.4834 9.18262 1.77051 8.89551L3.32227 7.35059L3.9375 7.96582L5.79004 6.11328L6.41211 6.72852L4.55273 8.58789L6.41211 10.4473L8.27148 8.58789L8.88672 9.20996ZM3.9375 13.25C4.22917 13.25 4.50716 13.1953 4.77148 13.0859C5.04036 12.972 5.27734 12.8125 5.48242 12.6074L6.41211 11.6777L3.32227 8.58789L2.39258 9.51758C2.1875 9.72266 2.02799 9.95964 1.91406 10.2285C1.80469 10.4928 1.75 10.7708 1.75 11.0625C1.75 11.3633 1.80697 11.6481 1.9209 11.917C2.03483 12.1813 2.18978 12.4137 2.38574 12.6143C2.58626 12.8102 2.81868 12.9652 3.08301 13.0791C3.35189 13.193 3.63672 13.25 3.9375 13.25ZM12.5166 3.10547C12.7171 3.36979 12.8675 3.65918 12.9678 3.97363C13.0726 4.28353 13.125 4.60482 13.125 4.9375C13.125 5.3431 13.0475 5.73503 12.8926 6.11328C12.7376 6.48698 12.5166 6.81738 12.2295 7.10449L10.6777 8.64941L6.35059 4.32227L7.89551 2.77051C8.18262 2.4834 8.51302 2.26237 8.88672 2.10742C9.26497 1.95247 9.6569 1.875 10.0625 1.875C10.3952 1.875 10.7165 1.92741 11.0264 2.03223C11.3408 2.13249 11.6302 2.28288 11.8945 2.4834L13.2549 1.12988L13.8701 1.74512L12.5166 3.10547ZM11.6074 6.48242C11.8125 6.27734 11.9697 6.04264 12.0791 5.77832C12.193 5.50944 12.25 5.22917 12.25 4.9375C12.25 4.63672 12.1908 4.35417 12.0723 4.08984C11.9583 3.82552 11.8011 3.59538 11.6006 3.39941C11.4046 3.19889 11.1745 3.04167 10.9102 2.92773C10.6458 2.80924 10.3633 2.75 10.0625 2.75C9.77083 2.75 9.49056 2.80697 9.22168 2.9209C8.95736 3.03027 8.72266 3.1875 8.51758 3.39258L7.58789 4.32227L10.6777 7.41211L11.6074 6.48242Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-hc.svg b/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-hc.svg new file mode 100644 index 000000000000..02a598dc9878 --- /dev/null +++ b/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-hc.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #F48771 !important;" d="M8.88672 9.20996L7.03418 11.0625L7.64941 11.6777L6.10449 13.2295C5.81738 13.5166 5.4847 13.7376 5.10645 13.8926C4.73275 14.0475 4.3431 14.125 3.9375 14.125C3.60482 14.125 3.28125 14.0749 2.9668 13.9746C2.6569 13.8698 2.36979 13.7171 2.10547 13.5166L0.745117 14.8701L0.129883 14.2549L1.4834 12.8945C1.28288 12.6302 1.13021 12.3431 1.02539 12.0332C0.92513 11.7188 0.875 11.3952 0.875 11.0625C0.875 10.6569 0.952474 10.2673 1.10742 9.89355C1.26237 9.5153 1.4834 9.18262 1.77051 8.89551L3.32227 7.35059L3.9375 7.96582L5.79004 6.11328L6.41211 6.72852L4.55273 8.58789L6.41211 10.4473L8.27148 8.58789L8.88672 9.20996ZM3.9375 13.25C4.22917 13.25 4.50716 13.1953 4.77148 13.0859C5.04036 12.972 5.27734 12.8125 5.48242 12.6074L6.41211 11.6777L3.32227 8.58789L2.39258 9.51758C2.1875 9.72266 2.02799 9.95964 1.91406 10.2285C1.80469 10.4928 1.75 10.7708 1.75 11.0625C1.75 11.3633 1.80697 11.6481 1.9209 11.917C2.03483 12.1813 2.18978 12.4137 2.38574 12.6143C2.58626 12.8102 2.81868 12.9652 3.08301 13.0791C3.35189 13.193 3.63672 13.25 3.9375 13.25ZM12.5166 3.10547C12.7171 3.36979 12.8675 3.65918 12.9678 3.97363C13.0726 4.28353 13.125 4.60482 13.125 4.9375C13.125 5.3431 13.0475 5.73503 12.8926 6.11328C12.7376 6.48698 12.5166 6.81738 12.2295 7.10449L10.6777 8.64941L6.35059 4.32227L7.89551 2.77051C8.18262 2.4834 8.51302 2.26237 8.88672 2.10742C9.26497 1.95247 9.6569 1.875 10.0625 1.875C10.3952 1.875 10.7165 1.92741 11.0264 2.03223C11.3408 2.13249 11.6302 2.28288 11.8945 2.4834L13.2549 1.12988L13.8701 1.74512L12.5166 3.10547ZM11.6074 6.48242C11.8125 6.27734 11.9697 6.04264 12.0791 5.77832C12.193 5.50944 12.25 5.22917 12.25 4.9375C12.25 4.63672 12.1908 4.35417 12.0723 4.08984C11.9583 3.82552 11.8011 3.59538 11.6006 3.39941C11.4046 3.19889 11.1745 3.04167 10.9102 2.92773C10.6458 2.80924 10.3633 2.75 10.0625 2.75C9.77083 2.75 9.49056 2.80697 9.22168 2.9209C8.95736 3.03027 8.72266 3.1875 8.51758 3.39258L7.58789 4.32227L10.6777 7.41211L11.6074 6.48242Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-light.svg b/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-light.svg new file mode 100644 index 000000000000..107b8e22f7c3 --- /dev/null +++ b/src/datascience-ui/react-common/images/JupyterServerDisconnected/disconnected-light.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #A1260D !important;" d="M8.88672 9.20996L7.03418 11.0625L7.64941 11.6777L6.10449 13.2295C5.81738 13.5166 5.4847 13.7376 5.10645 13.8926C4.73275 14.0475 4.3431 14.125 3.9375 14.125C3.60482 14.125 3.28125 14.0749 2.9668 13.9746C2.6569 13.8698 2.36979 13.7171 2.10547 13.5166L0.745117 14.8701L0.129883 14.2549L1.4834 12.8945C1.28288 12.6302 1.13021 12.3431 1.02539 12.0332C0.92513 11.7188 0.875 11.3952 0.875 11.0625C0.875 10.6569 0.952474 10.2673 1.10742 9.89355C1.26237 9.5153 1.4834 9.18262 1.77051 8.89551L3.32227 7.35059L3.9375 7.96582L5.79004 6.11328L6.41211 6.72852L4.55273 8.58789L6.41211 10.4473L8.27148 8.58789L8.88672 9.20996ZM3.9375 13.25C4.22917 13.25 4.50716 13.1953 4.77148 13.0859C5.04036 12.972 5.27734 12.8125 5.48242 12.6074L6.41211 11.6777L3.32227 8.58789L2.39258 9.51758C2.1875 9.72266 2.02799 9.95964 1.91406 10.2285C1.80469 10.4928 1.75 10.7708 1.75 11.0625C1.75 11.3633 1.80697 11.6481 1.9209 11.917C2.03483 12.1813 2.18978 12.4137 2.38574 12.6143C2.58626 12.8102 2.81868 12.9652 3.08301 13.0791C3.35189 13.193 3.63672 13.25 3.9375 13.25ZM12.5166 3.10547C12.7171 3.36979 12.8675 3.65918 12.9678 3.97363C13.0726 4.28353 13.125 4.60482 13.125 4.9375C13.125 5.3431 13.0475 5.73503 12.8926 6.11328C12.7376 6.48698 12.5166 6.81738 12.2295 7.10449L10.6777 8.64941L6.35059 4.32227L7.89551 2.77051C8.18262 2.4834 8.51302 2.26237 8.88672 2.10742C9.26497 1.95247 9.6569 1.875 10.0625 1.875C10.3952 1.875 10.7165 1.92741 11.0264 2.03223C11.3408 2.13249 11.6302 2.28288 11.8945 2.4834L13.2549 1.12988L13.8701 1.74512L12.5166 3.10547ZM11.6074 6.48242C11.8125 6.27734 11.9697 6.04264 12.0791 5.77832C12.193 5.50944 12.25 5.22917 12.25 4.9375C12.25 4.63672 12.1908 4.35417 12.0723 4.08984C11.9583 3.82552 11.8011 3.59538 11.6006 3.39941C11.4046 3.19889 11.1745 3.04167 10.9102 2.92773C10.6458 2.80924 10.3633 2.75 10.0625 2.75C9.77083 2.75 9.49056 2.80697 9.22168 2.9209C8.95736 3.03027 8.72266 3.1875 8.51758 3.39258L7.58789 4.32227L10.6777 7.41211L11.6074 6.48242Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Next/next-inverse.svg b/src/datascience-ui/react-common/images/Next/next-inverse.svg new file mode 100644 index 000000000000..d1c3e8dbd956 --- /dev/null +++ b/src/datascience-ui/react-common/images/Next/next-inverse.svg @@ -0,0 +1,3 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #CCCCCC !important;" d="M3.5 12L2.44428 10.9453L7.38955 6L2.44428 1.05473L3.5 0L9.5 6L3.5 12Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Next/next.svg b/src/datascience-ui/react-common/images/Next/next.svg new file mode 100644 index 000000000000..465903659681 --- /dev/null +++ b/src/datascience-ui/react-common/images/Next/next.svg @@ -0,0 +1,3 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M3.5 12L2.44428 10.9453L7.38955 6L2.44428 1.05473L3.5 0L9.5 6L3.5 12Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode.svg b/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode.svg new file mode 100644 index 000000000000..d50b0367205a --- /dev/null +++ b/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode.svg @@ -0,0 +1,3 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M12 0V9.75H9.75V12H0V2.25H2.25V0H12ZM11.25 9V0.75H3V2.25H4.5V3H0.75V11.25H9V7.5H9.75V9H11.25ZM5.51367 7.01367L4.98633 6.48633L8.4668 3H6V2.25H9.75V6H9V3.5332L5.51367 7.01367Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode_dark.svg new file mode 100644 index 000000000000..93a0af00cad6 --- /dev/null +++ b/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode_dark.svg @@ -0,0 +1,3 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #D4D4D4 !important;" d="M12 0V9.75H9.75V12H0V2.25H2.25V0H12ZM11.25 9V0.75H3V2.25H4.5V3H0.75V11.25H9V7.5H9.75V9H11.25ZM5.51367 7.01367L4.98633 6.48633L8.4668 3H6V2.25H9.75V6H9V3.5332L5.51367 7.01367Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/OpenPlot/plot_dark.svg b/src/datascience-ui/react-common/images/OpenPlot/plot_dark.svg new file mode 100644 index 000000000000..8ac26cda0805 --- /dev/null +++ b/src/datascience-ui/react-common/images/OpenPlot/plot_dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important;" d="M9.71429 6.28571V12.2857H7.14286V6.28571H9.71429ZM13.1429 2.85714V12.2857H10.5714V2.85714H13.1429ZM2.85714 13.1429H14V14H2V2H2.85714V13.1429ZM6.28571 4.57143V12.2857H3.71429V4.57143H6.28571Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/OpenPlot/plot_light.svg b/src/datascience-ui/react-common/images/OpenPlot/plot_light.svg new file mode 100644 index 000000000000..67600ef32a6e --- /dev/null +++ b/src/datascience-ui/react-common/images/OpenPlot/plot_light.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M9.71429 6.28571V12.2857H7.14286V6.28571H9.71429ZM13.1429 2.85714V12.2857H10.5714V2.85714H13.1429ZM2.85714 13.1429H14V14H2V2H2.85714V13.1429ZM6.28571 4.57143V12.2857H3.71429V4.57143H6.28571Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Pan/pan.svg b/src/datascience-ui/react-common/images/Pan/pan.svg new file mode 100644 index 000000000000..25f33c9e5b4b --- /dev/null +++ b/src/datascience-ui/react-common/images/Pan/pan.svg @@ -0,0 +1,3 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M4.3905 3.45001L3.8595 2.92501L6 0.782257L8.1405 2.92501L7.6095 3.45601L6.375 2.21776V4.50001H5.625V2.21776L4.3905 3.45001ZM4.5 6.37501V5.62501H2.21775L3.45 4.39051L2.925 3.85951L0.782249 6.00001L2.925 8.14051L3.456 7.60951L2.21775 6.37501H4.5ZM6.375 9.78226V7.50001H5.625V9.78226L4.3905 8.55001L3.8595 9.08101L6 11.2178L8.1405 9.07501L7.6095 8.54401L6.375 9.78226ZM9.075 3.85726L8.544 4.38826L9.78225 5.62501H7.5V6.37501H9.78225L8.55 7.60951L9.081 8.14051L11.2177 6.00001L9.075 3.85726Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Pan/pan_inverse.svg b/src/datascience-ui/react-common/images/Pan/pan_inverse.svg new file mode 100644 index 000000000000..75e2f52f6c4f --- /dev/null +++ b/src/datascience-ui/react-common/images/Pan/pan_inverse.svg @@ -0,0 +1,3 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important;" d="M4.3905 3.45001L3.8595 2.92501L6 0.782257L8.1405 2.92501L7.6095 3.45601L6.375 2.21776V4.50001H5.625V2.21776L4.3905 3.45001ZM4.5 6.37501V5.62501H2.21775L3.45 4.39051L2.925 3.85951L0.782249 6.00001L2.925 8.14051L3.456 7.60951L2.21775 6.37501H4.5ZM6.375 9.78226V7.50001H5.625V9.78226L4.3905 8.55001L3.8595 9.08101L6 11.2178L8.1405 9.07501L7.6095 8.54401L6.375 9.78226ZM9.075 3.85726L8.544 4.38826L9.78225 5.62501H7.5V6.37501H9.78225L8.55 7.60951L9.081 8.14051L11.2177 6.00001L9.075 3.85726Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode.svg b/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode.svg new file mode 100644 index 000000000000..906f8244eb8b --- /dev/null +++ b/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>PopIn_16x</title><g ><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g style="display: none;"><path class="icon-vs-out" d="M16,0V15H13V5.121L7.121,11H12v3H2V4H5V8.879L10.879,3H1V0Z" style="display: none;"/></g><g ><path class="icon-vs-bg" d="M15,1V14H14V2H2V1ZM11.146,4.146,4,11.293V5H3v8h8V12H4.707l7.147-7.146Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode_dark.svg new file mode 100644 index 000000000000..041b6e015305 --- /dev/null +++ b/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode_dark.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#252526;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>PopIn_16x</title><g ><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g style="display: none;"><path class="icon-vs-out" d="M16,0V15H13V5.121L7.121,11H12v3H2V4H5V8.879L10.879,3H1V0Z" style="display: none;"/></g><g ><path class="icon-vs-bg" d="M15,1V14H14V2H2V1ZM11.146,4.146,4,11.293V5H3v8h8V12H4.707l7.147-7.146Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode.svg b/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode.svg new file mode 100644 index 000000000000..e979568ea576 --- /dev/null +++ b/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>PopOut_16x</title><g ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g style="display: none;"><path class="icon-vs-out" d="M2,4H12V14H9V9.121L2.854,15.268.732,13.146,6.879,7H2ZM1,0V3H13V15h3V0Z" style="display: none;"/></g><g ><path class="icon-vs-bg" d="M15,1V14H14V2H2V1ZM3,6H9.293L2.146,13.146l.708.708L10,6.707V13h1V5H3Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode_dark.svg new file mode 100644 index 000000000000..a72e56c2dc70 --- /dev/null +++ b/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode_dark.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#252526;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>PopOut_16x</title><g ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g style="display: none;"><path class="icon-vs-out" d="M2,4H12V14H9V9.121L2.854,15.268.732,13.146,6.879,7H2ZM1,0V3H13V15h3V0Z" style="display: none;"/></g><g ><path class="icon-vs-bg" d="M15,1V14H14V2H2V1ZM3,6H9.293L2.146,13.146l.708.708L10,6.707V13h1V5H3Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/Prev/previous-inverse.svg b/src/datascience-ui/react-common/images/Prev/previous-inverse.svg new file mode 100644 index 000000000000..d779fba43c47 --- /dev/null +++ b/src/datascience-ui/react-common/images/Prev/previous-inverse.svg @@ -0,0 +1,3 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #CCCCCC !important;" d="M8.5 0L9.55572 1.05473L4.61045 6L9.55572 10.9453L8.5 12L2.5 6L8.5 0Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Prev/previous.svg b/src/datascience-ui/react-common/images/Prev/previous.svg new file mode 100644 index 000000000000..730afafcfd32 --- /dev/null +++ b/src/datascience-ui/react-common/images/Prev/previous.svg @@ -0,0 +1,3 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M8.5 -8.41007e-08L9.55572 1.05473L4.61045 6L9.55572 10.9453L8.5 12L2.5 6L8.5 -8.41007e-08Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Redo/Redo_16x_vscode.svg b/src/datascience-ui/react-common/images/Redo/Redo_16x_vscode.svg new file mode 100644 index 000000000000..3e537188666c --- /dev/null +++ b/src/datascience-ui/react-common/images/Redo/Redo_16x_vscode.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M12.5 2V5.5L12.0001 6H8.50087V5H11.0213L10.0801 4.05869C8.69321 2.67157 6.44468 2.67157 5.05826 4.05869C3.67136 5.4458 3.67136 7.69476 5.05826 9.08188L10.2549 14.2799L9.53483 14.9999L4.33821 9.80194C2.55393 8.01715 2.55393 5.12341 4.33821 3.33859C6.12249 1.5538 9.0159 1.5538 10.8002 3.33859L11.5002 4.03882V2H12.5Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Redo/Redo_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/Redo/Redo_16x_vscode_dark.svg new file mode 100644 index 000000000000..5c1c7c98bc5c --- /dev/null +++ b/src/datascience-ui/react-common/images/Redo/Redo_16x_vscode_dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M13 2V5.5L12.5001 6H9.00087V5H11.5213L10.5801 4.05869C9.19322 2.67157 6.94469 2.67157 5.55827 4.05869C4.17137 5.4458 4.17137 7.69476 5.55827 9.08188L10.7549 14.2799L10.0348 14.9999L4.83822 9.80194C3.05394 8.01715 3.05394 5.12341 4.83822 3.33859C6.6225 1.5538 9.51591 1.5538 11.3002 3.33859L12.0002 4.03882V2H13Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Restart/Restart_grey_16x_vscode.svg b/src/datascience-ui/react-common/images/Restart/Restart_grey_16x_vscode.svg new file mode 100644 index 000000000000..4ce884e15bbe --- /dev/null +++ b/src/datascience-ui/react-common/images/Restart/Restart_grey_16x_vscode.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #388A34 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M12.75 8C12.75 10.4853 10.7353 12.5 8.24999 12.5C6.41795 12.5 4.84162 11.4052 4.13953 9.83416L2.74882 10.399C3.67446 12.5186 5.78923 14 8.24999 14C11.5637 14 14.25 11.3137 14.25 8C14.25 4.68629 11.5637 2 8.24999 2C6.3169 2 4.59732 2.91418 3.5 4.3338V2.5H2V6.5L2.75 7.25H6.25V5.75H4.35201C5.13008 4.40495 6.58436 3.5 8.24999 3.5C10.7353 3.5 12.75 5.51472 12.75 8Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Restart/Restart_grey_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/Restart/Restart_grey_16x_vscode_dark.svg new file mode 100644 index 000000000000..720bc53df88c --- /dev/null +++ b/src/datascience-ui/react-common/images/Restart/Restart_grey_16x_vscode_dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #89D185 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M12.75 8C12.75 10.4853 10.7353 12.5 8.24999 12.5C6.41795 12.5 4.84162 11.4052 4.13953 9.83416L2.74882 10.399C3.67446 12.5186 5.78923 14 8.24999 14C11.5637 14 14.25 11.3137 14.25 8C14.25 4.68629 11.5637 2 8.24999 2C6.3169 2 4.59732 2.91418 3.5 4.3338V2.5H2V6.5L2.75 7.25H6.25V5.75H4.35201C5.13008 4.40495 6.58436 3.5 8.24999 3.5C10.7353 3.5 12.75 5.51472 12.75 8Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Run/run-dark.svg b/src/datascience-ui/react-common/images/Run/run-dark.svg new file mode 100644 index 000000000000..345289f1e577 --- /dev/null +++ b/src/datascience-ui/react-common/images/Run/run-dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #89D185 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M3.99977 14V2.18091L12.9998 8.06215L3.99977 14ZM5.5 4.99997L10.3145 8.06215L5.5 11.1809L5.5 4.99997Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Run/run-light.svg b/src/datascience-ui/react-common/images/Run/run-light.svg new file mode 100644 index 000000000000..ef2819326598 --- /dev/null +++ b/src/datascience-ui/react-common/images/Run/run-light.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #388A34 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M3.99977 14V2.18091L12.9998 8.06215L3.99977 14ZM5.5 4.99997L10.3145 8.06215L5.5 11.1809L5.5 4.99997Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/RunAbove/runabove-inverse.svg b/src/datascience-ui/react-common/images/RunAbove/runabove-inverse.svg new file mode 100644 index 000000000000..d8f93fb68a32 --- /dev/null +++ b/src/datascience-ui/react-common/images/RunAbove/runabove-inverse.svg @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#CCCCCC;} +</style> +<g> + <path class="st0" d="M1.77,1.01L1,1.42v12l0.78,0.42l9-6V7.01L1.77,1.01z M2,12.49V2.36l7.6,5.07L2,12.49z"/> + <path class="st0" d="M12.15,8h0.71l2.5,2.5l-0.71,0.71L13,9.56V15h-1V9.55l-1.65,1.65L9.65,10.5L12.15,8z"/> +</g> +</svg> diff --git a/src/datascience-ui/react-common/images/RunAbove/runabove.svg b/src/datascience-ui/react-common/images/RunAbove/runabove.svg new file mode 100644 index 000000000000..8b5ccb9ee047 --- /dev/null +++ b/src/datascience-ui/react-common/images/RunAbove/runabove.svg @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#424242;} +</style> +<g> + <path class="st0" d="M1.77,1.01L1,1.42v12l0.78,0.42l9-6V7.01L1.77,1.01z M2,12.49V2.36l7.6,5.07L2,12.49z"/> + <path class="st0" d="M12.15,8h0.71l2.5,2.5l-0.71,0.71L13,9.56V15h-1V9.55l-1.65,1.65L9.65,10.5L12.15,8z"/> +</g> +</svg> diff --git a/src/datascience-ui/react-common/images/RunAll/run_all_dark.svg b/src/datascience-ui/react-common/images/RunAll/run_all_dark.svg new file mode 100644 index 000000000000..e9497b351ed4 --- /dev/null +++ b/src/datascience-ui/react-common/images/RunAll/run_all_dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important" d="M9 2.96094L15.2969 8L9 13.0781V2.96094ZM10 5.03906V10.9922L13.7031 8L10 5.03906ZM2 13.0781V2.96094L8.29688 8L2 13.0781ZM3 5.03906V10.9922L6.70312 8L3 5.03906Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/RunAll/run_all_light.svg b/src/datascience-ui/react-common/images/RunAll/run_all_light.svg new file mode 100644 index 000000000000..36af4345aa03 --- /dev/null +++ b/src/datascience-ui/react-common/images/RunAll/run_all_light.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important" d="M9 2.96094L15.2969 8L9 13.0781V2.96094ZM10 5.03906V10.9922L13.7031 8L10 5.03906ZM2 13.0781V2.96094L8.29688 8L2 13.0781ZM3 5.03906V10.9922L6.70312 8L3 5.03906Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/RunBelow/runbelow-inverse.svg b/src/datascience-ui/react-common/images/RunBelow/runbelow-inverse.svg new file mode 100644 index 000000000000..1b820cd29fb9 --- /dev/null +++ b/src/datascience-ui/react-common/images/RunBelow/runbelow-inverse.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#CCCCCC;} +</style> +<g> + <path class="st0" d="M1.8,1.01L1.02,1.42v12l0.78,0.42l9-6V7.01L1.8,1.01z M2.02,12.49V2.36l7.6,5.07L2.02,12.49z"/> + <path class="st0" d="M12.85,15h-0.71l-2.5-2.5l0.71-0.71L12,13.44V8h1v5.45l1.65-1.65l0.71,0.71L12.85,15z"/> +</g> +</svg> + diff --git a/src/datascience-ui/react-common/images/RunBelow/runbelow.svg b/src/datascience-ui/react-common/images/RunBelow/runbelow.svg new file mode 100644 index 000000000000..38730888f13a --- /dev/null +++ b/src/datascience-ui/react-common/images/RunBelow/runbelow.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#424242;} +</style> +<g> + <path class="st0" d="M1.8,1.01L1.02,1.42v12l0.78,0.42l9-6V7.01L1.8,1.01z M2.02,12.49V2.36l7.6,5.07L2.02,12.49z"/> + <path class="st0" d="M12.85,15h-0.71l-2.5-2.5l0.71-0.71L12,13.44V8h1v5.45l1.65-1.65l0.71,0.71L12.85,15z"/> +</g> +</svg> + diff --git a/src/datascience-ui/react-common/images/RunByLine/runbyline_dark.svg b/src/datascience-ui/react-common/images/RunByLine/runbyline_dark.svg new file mode 100644 index 000000000000..5193f5d4902e --- /dev/null +++ b/src/datascience-ui/react-common/images/RunByLine/runbyline_dark.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> + <defs> + <style>.canvas{fill:none;opacity:0;}.cls-arrrow{fill:#89D185 !important;}.cls-text{fill:#C5C5C5 !important;}</style> + </defs> + <title>RunByLine_16x</title> + <g> + <path class="canvas" d="M16,16H0V0H16Z"/> + </g> + <g> + <path class="cls-text" d="M15,3V4H12V3Zm-5,8h5V10H10Zm1-8H6V4h5ZM6,14h5V13H6Zm3-4H6v1H9Zm1-4H6V8h9V6H10Z"/> + </g> + <g> + <path class="cls-arrrow" d="M0,10,5,7,0,4Z"/> + </g> +</svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/RunByLine/runbyline_light.svg b/src/datascience-ui/react-common/images/RunByLine/runbyline_light.svg new file mode 100644 index 000000000000..2a18ee6ac53a --- /dev/null +++ b/src/datascience-ui/react-common/images/RunByLine/runbyline_light.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> + <defs> + <style>.canvas{fill:none;opacity:0;}.cls-arrrow{fill:#388A34 !important;}.cls-text{fill:#424242 !important;}</style> + </defs> + <title>RunByLine_16x</title> + <g> + <path class="canvas" d="M16,16H0V0H16Z"/> + </g> + <g> + <path class="cls-text" d="M15,3V4H12V3Zm-5,8h5V10H10Zm1-8H6V4h5ZM6,14h5V13H6Zm3-4H6v1H9Zm1-4H6V8h9V6H10Z"/> + </g> + <g> + <path class="cls-arrrow" d="M0,10,5,7,0,4Z"/> + </g> +</svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/SaveAs/SaveAs_16x_vscode.svg b/src/datascience-ui/react-common/images/SaveAs/SaveAs_16x_vscode.svg new file mode 100644 index 000000000000..2b461e5c7568 --- /dev/null +++ b/src/datascience-ui/react-common/images/SaveAs/SaveAs_16x_vscode.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M12.0147 2.8595L13.1397 3.9845L13.25 4.25V12.875L12.875 13.25H3.125L2.75 12.875V3.125L3.125 2.75H11.75L12.0147 2.8595ZM3.5 3.5V12.5H12.5V4.406L11.5947 3.5H10.25V6.5H5V3.5H3.5ZM8 3.5V5.75H9.5V3.5H8Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/SaveAs/SaveAs_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/SaveAs/SaveAs_16x_vscode_dark.svg new file mode 100644 index 000000000000..865c8fc584cb --- /dev/null +++ b/src/datascience-ui/react-common/images/SaveAs/SaveAs_16x_vscode_dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M12.0147 2.8595L13.1397 3.9845L13.25 4.25V12.875L12.875 13.25H3.125L2.75 12.875V3.125L3.125 2.75H11.75L12.0147 2.8595ZM3.5 3.5V12.5H12.5V4.406L11.5947 3.5H10.25V6.5H5V3.5H3.5ZM8 3.5V5.75H9.5V3.5H8Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/StartPage/Interactive-inverse.svg b/src/datascience-ui/react-common/images/StartPage/Interactive-inverse.svg new file mode 100644 index 000000000000..87819244977e --- /dev/null +++ b/src/datascience-ui/react-common/images/StartPage/Interactive-inverse.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="32" height="32"> + <path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M0 1792V256h896v1536H0zM128 384v1280h640V384H128zm896-128h896v1536h-896V256zm768 1408V384h-640v1280h640z" /> +</svg> diff --git a/src/datascience-ui/react-common/images/StartPage/Interactive.svg b/src/datascience-ui/react-common/images/StartPage/Interactive.svg new file mode 100644 index 000000000000..6731d50fee39 --- /dev/null +++ b/src/datascience-ui/react-common/images/StartPage/Interactive.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="32" height="32"> + <path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M0 1792V256h896v1536H0zM128 384v1280h640V384H128zm896-128h896v1536h-896V256zm768 1408V384h-640v1280h640z" /> +</svg> diff --git a/src/datascience-ui/react-common/images/StartPage/Notebook-inverse.svg b/src/datascience-ui/react-common/images/StartPage/Notebook-inverse.svg new file mode 100644 index 000000000000..12b217d48fb8 --- /dev/null +++ b/src/datascience-ui/react-common/images/StartPage/Notebook-inverse.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="32" height="32"> + <path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M1408 384v384H640V384h768zm-128 256V512H768v128h512zM128 0h1664v2048H128v-384H0v-128h128v-256H0v-128h128V896H0V768h128V512H0V384h128V0zm1536 1920V128H256v256h128v128H256v256h128v128H256v256h128v128H256v256h128v128H256v256h1408z" /> +</svg> diff --git a/src/datascience-ui/react-common/images/StartPage/Notebook.svg b/src/datascience-ui/react-common/images/StartPage/Notebook.svg new file mode 100644 index 000000000000..5e058afb30eb --- /dev/null +++ b/src/datascience-ui/react-common/images/StartPage/Notebook.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="32" height="32"> + <path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M1408 384v384H640V384h768zm-128 256V512H768v128h512zM128 0h1664v2048H128v-384H0v-128h128v-256H0v-128h128V896H0V768h128V512H0V384h128V0zm1536 1920V128H256v256h128v128H256v256h128v128H256v256h128v128H256v256h128v128H256v256h1408z" /> +</svg> diff --git a/src/datascience-ui/react-common/images/StartPage/OpenFolder-inverse.svg b/src/datascience-ui/react-common/images/StartPage/OpenFolder-inverse.svg new file mode 100644 index 000000000000..e3d0f3e933e7 --- /dev/null +++ b/src/datascience-ui/react-common/images/StartPage/OpenFolder-inverse.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"> + <path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M1.5 14h11l.48-.37 2.63-7-.48-.63H14V3.5l-.5-.5H7.71l-.86-.85L6.5 2h-5l-.5.5v11l.5.5zM2 3h4.29l.86.85.35.15H13v2H8.5l-.35.15-.86.85H3.5l-.47.34-1 3.08L2 3zm10.13 10H2.19l1.67-5H7.5l.35-.15.86-.85h5.79l-2.37 6z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/StartPage/OpenFolder.svg b/src/datascience-ui/react-common/images/StartPage/OpenFolder.svg new file mode 100644 index 000000000000..309858c9f469 --- /dev/null +++ b/src/datascience-ui/react-common/images/StartPage/OpenFolder.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"> + <path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M1.5 14h11l.48-.37 2.63-7-.48-.63H14V3.5l-.5-.5H7.71l-.86-.85L6.5 2h-5l-.5.5v11l.5.5zM2 3h4.29l.86.85.35.15H13v2H8.5l-.35.15-.86.85H3.5l-.47.34-1 3.08L2 3zm10.13 10H2.19l1.67-5H7.5l.35-.15.86-.85h5.79l-2.37 6z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/StartPage/Python-color.svg b/src/datascience-ui/react-common/images/StartPage/Python-color.svg new file mode 100644 index 000000000000..e9b909eab46b --- /dev/null +++ b/src/datascience-ui/react-common/images/StartPage/Python-color.svg @@ -0,0 +1,14 @@ +<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M35.1827 0.000588916C32.2463 0.0142333 29.4421 0.264669 26.9746 0.70128C19.7059 1.98543 18.3862 4.67326 18.3862 9.63008V16.1765H35.5631V18.3587H18.3862H11.9398C6.9477 18.3587 2.57648 21.3592 1.20921 27.0673C-0.367905 33.61 -0.437859 37.6928 1.20921 44.5245C2.43021 49.6097 5.34612 53.2331 10.3382 53.2331H16.244V45.3853C16.244 39.7158 21.1495 34.7148 26.9746 34.7148H44.1315C48.9074 34.7148 52.72 30.7825 52.72 25.9862V9.63008C52.72 4.97504 48.7929 1.47819 44.1315 0.70128C41.1808 0.210091 38.1191 -0.0130555 35.1827 0.000588916ZM25.8936 5.26578C27.6678 5.26578 29.1167 6.73837 29.1167 8.54902C29.1167 10.3533 27.6678 11.8122 25.8936 11.8122C24.1129 11.8122 22.6704 10.3533 22.6704 8.54902C22.6704 6.73837 24.1129 5.26578 25.8936 5.26578Z" fill="url(#paint0_linear)"/> +<path d="M54.8622 18.3589V25.9864C54.8622 31.8999 49.8487 36.8771 44.1316 36.8771H26.9747C22.2752 36.8771 18.3863 40.8993 18.3863 45.6058V61.9619C18.3863 66.6169 22.4341 69.355 26.9747 70.6905C32.412 72.2893 37.626 72.5782 44.1316 70.6905C48.456 69.4384 52.7201 66.9187 52.7201 61.9619V55.4154H35.5632V53.2333H52.7201H61.3086C66.3007 53.2333 68.1609 49.7512 69.8971 44.5247C71.6904 39.1441 71.6141 33.9698 69.8971 27.0675C68.6633 22.0978 66.307 18.3589 61.3086 18.3589H54.8622ZM45.2127 59.7797C46.9933 59.7797 48.4359 61.2387 48.4359 63.0429C48.4359 64.8536 46.9933 66.3262 45.2127 66.3262C43.4385 66.3262 41.9895 64.8536 41.9895 63.0429C41.9895 61.2387 43.4385 59.7797 45.2127 59.7797Z" fill="url(#paint1_linear)"/> +<defs> +<linearGradient id="paint0_linear" x1="5.60631e-08" y1="4.87005e-08" x2="39.6081" y2="33.7516" gradientUnits="userSpaceOnUse"> +<stop stop-color="#5A9FD4"/> +<stop offset="1" stop-color="#306998"/> +</linearGradient> +<linearGradient id="paint1_linear" x1="44.7999" y1="62.4926" x2="30.59" y2="42.5803" gradientUnits="userSpaceOnUse"> +<stop stop-color="#FFD43B"/> +<stop offset="1" stop-color="#FFE873"/> +</linearGradient> +</defs> +</svg> diff --git a/src/datascience-ui/react-common/images/StartPage/Python-inverse.svg b/src/datascience-ui/react-common/images/StartPage/Python-inverse.svg new file mode 100644 index 000000000000..73708a57db29 --- /dev/null +++ b/src/datascience-ui/react-common/images/StartPage/Python-inverse.svg @@ -0,0 +1,4 @@ +<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M35.1827 0.000588916C32.2463 0.0142333 29.4421 0.264669 26.9746 0.70128C19.7059 1.98543 18.3862 4.67326 18.3862 9.63008V16.1765H35.5631V18.3587H18.3862H11.9398C6.9477 18.3587 2.57648 21.3592 1.20921 27.0673C-0.367905 33.61 -0.437859 37.6928 1.20921 44.5245C2.43021 49.6097 5.34612 53.2331 10.3382 53.2331H16.244V45.3853C16.244 39.7158 21.1495 34.7148 26.9746 34.7148H44.1315C48.9074 34.7148 52.72 30.7825 52.72 25.9862V9.63008C52.72 4.97504 48.7929 1.47819 44.1315 0.70128C41.1808 0.210091 38.1191 -0.0130555 35.1827 0.000588916ZM25.8936 5.26578C27.6678 5.26578 29.1167 6.73837 29.1167 8.54902C29.1167 10.3533 27.6678 11.8122 25.8936 11.8122C24.1129 11.8122 22.6704 10.3533 22.6704 8.54902C22.6704 6.73837 24.1129 5.26578 25.8936 5.26578Z"/> + <path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M54.8622 18.3589V25.9864C54.8622 31.8999 49.8487 36.8771 44.1316 36.8771H26.9747C22.2752 36.8771 18.3863 40.8993 18.3863 45.6058V61.9619C18.3863 66.6169 22.4341 69.355 26.9747 70.6905C32.412 72.2893 37.626 72.5782 44.1316 70.6905C48.456 69.4384 52.7201 66.9187 52.7201 61.9619V55.4154H35.5632V53.2333H52.7201H61.3086C66.3007 53.2333 68.1609 49.7512 69.8971 44.5247C71.6904 39.1441 71.6141 33.9698 69.8971 27.0675C68.6633 22.0978 66.307 18.3589 61.3086 18.3589H54.8622ZM45.2127 59.7797C46.9933 59.7797 48.4359 61.2387 48.4359 63.0429C48.4359 64.8536 46.9933 66.3262 45.2127 66.3262C43.4385 66.3262 41.9895 64.8536 41.9895 63.0429C41.9895 61.2387 43.4385 59.7797 45.2127 59.7797Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/StartPage/Python.svg b/src/datascience-ui/react-common/images/StartPage/Python.svg new file mode 100644 index 000000000000..9a4d621e91c0 --- /dev/null +++ b/src/datascience-ui/react-common/images/StartPage/Python.svg @@ -0,0 +1,4 @@ +<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M35.1827 0.000588916C32.2463 0.0142333 29.4421 0.264669 26.9746 0.70128C19.7059 1.98543 18.3862 4.67326 18.3862 9.63008V16.1765H35.5631V18.3587H18.3862H11.9398C6.9477 18.3587 2.57648 21.3592 1.20921 27.0673C-0.367905 33.61 -0.437859 37.6928 1.20921 44.5245C2.43021 49.6097 5.34612 53.2331 10.3382 53.2331H16.244V45.3853C16.244 39.7158 21.1495 34.7148 26.9746 34.7148H44.1315C48.9074 34.7148 52.72 30.7825 52.72 25.9862V9.63008C52.72 4.97504 48.7929 1.47819 44.1315 0.70128C41.1808 0.210091 38.1191 -0.0130555 35.1827 0.000588916ZM25.8936 5.26578C27.6678 5.26578 29.1167 6.73837 29.1167 8.54902C29.1167 10.3533 27.6678 11.8122 25.8936 11.8122C24.1129 11.8122 22.6704 10.3533 22.6704 8.54902C22.6704 6.73837 24.1129 5.26578 25.8936 5.26578Z"/> + <path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M54.8622 18.3589V25.9864C54.8622 31.8999 49.8487 36.8771 44.1316 36.8771H26.9747C22.2752 36.8771 18.3863 40.8993 18.3863 45.6058V61.9619C18.3863 66.6169 22.4341 69.355 26.9747 70.6905C32.412 72.2893 37.626 72.5782 44.1316 70.6905C48.456 69.4384 52.7201 66.9187 52.7201 61.9619V55.4154H35.5632V53.2333H52.7201H61.3086C66.3007 53.2333 68.1609 49.7512 69.8971 44.5247C71.6904 39.1441 71.6141 33.9698 69.8971 27.0675C68.6633 22.0978 66.307 18.3589 61.3086 18.3589H54.8622ZM45.2127 59.7797C46.9933 59.7797 48.4359 61.2387 48.4359 63.0429C48.4359 64.8536 46.9933 66.3262 45.2127 66.3262C43.4385 66.3262 41.9895 64.8536 41.9895 63.0429C41.9895 61.2387 43.4385 59.7797 45.2127 59.7797Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/SwitchToCode/switchtocode-inverse.svg b/src/datascience-ui/react-common/images/SwitchToCode/switchtocode-inverse.svg new file mode 100644 index 000000000000..3b93100ffac3 --- /dev/null +++ b/src/datascience-ui/react-common/images/SwitchToCode/switchtocode-inverse.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M6 2.98361V2.97184V2H5.91083C5.59743 2 5.29407 2.06161 5.00128 2.18473C4.70818 2.30798 4.44942 2.48474 4.22578 2.71498C4.00311 2.94422 3.83792 3.19498 3.73282 3.46766L3.73233 3.46898C3.63382 3.7352 3.56814 4.01201 3.53533 4.29917L3.53519 4.30053C3.50678 4.5805 3.4987 4.86844 3.51084 5.16428C3.52272 5.45379 3.52866 5.74329 3.52866 6.03279C3.52866 6.23556 3.48974 6.42594 3.412 6.60507L3.4116 6.60601C3.33687 6.78296 3.23423 6.93866 3.10317 7.07359C2.97644 7.20405 2.82466 7.31055 2.64672 7.3925C2.4706 7.46954 2.28497 7.5082 2.08917 7.5082H2V7.6V8.4V8.4918H2.08917C2.28465 8.4918 2.47001 8.53238 2.64601 8.61334L2.64742 8.61396C2.82457 8.69157 2.97577 8.79762 3.10221 8.93161L3.10412 8.93352C3.23428 9.0637 3.33659 9.21871 3.41129 9.39942L3.41201 9.40108C3.48986 9.58047 3.52866 9.76883 3.52866 9.96721C3.52866 10.2567 3.52272 10.5462 3.51084 10.8357C3.4987 11.1316 3.50677 11.4215 3.53516 11.7055L3.53535 11.7072C3.56819 11.9903 3.63387 12.265 3.73232 12.531L3.73283 12.5323C3.83793 12.805 4.00311 13.0558 4.22578 13.285C4.44942 13.5153 4.70818 13.692 5.00128 13.8153C5.29407 13.9384 5.59743 14 5.91083 14H6V13.2V13.0164H5.91083C5.71095 13.0164 5.52346 12.9777 5.34763 12.9008C5.17396 12.8191 5.02194 12.7126 4.89086 12.5818C4.76386 12.4469 4.66104 12.2911 4.58223 12.1137C4.50838 11.9346 4.47134 11.744 4.47134 11.541C4.47134 11.3127 4.4753 11.0885 4.48321 10.8686C4.49125 10.6411 4.49127 10.4195 4.48324 10.2039C4.47914 9.98246 4.46084 9.76883 4.42823 9.56312C4.39513 9.35024 4.33921 9.14757 4.26039 8.95536C4.18091 8.76157 4.07258 8.57746 3.93616 8.40298C3.82345 8.25881 3.68538 8.12462 3.52283 8C3.68538 7.87538 3.82345 7.74119 3.93616 7.59702C4.07258 7.42254 4.18091 7.23843 4.26039 7.04464C4.33913 6.85263 4.39513 6.65175 4.42826 6.44285C4.46082 6.2333 4.47914 6.01973 4.48324 5.80219C4.49127 5.58262 4.49125 5.36105 4.48321 5.13749C4.4753 4.9134 4.47134 4.68725 4.47134 4.45902C4.47134 4.26019 4.50833 4.07152 4.58238 3.89205C4.66135 3.71034 4.76421 3.55475 4.89086 3.42437C5.02193 3.28942 5.17461 3.18275 5.34802 3.10513C5.5238 3.02427 5.71113 2.98361 5.91083 2.98361H6ZM10 13.0164V13.0282V14H10.0892C10.4026 14 10.7059 13.9384 10.9987 13.8153C11.2918 13.692 11.5506 13.5153 11.7742 13.285C11.9969 13.0558 12.1621 12.805 12.2672 12.5323L12.2677 12.531C12.3662 12.2648 12.4319 11.988 12.4647 11.7008L12.4648 11.6995C12.4932 11.4195 12.5013 11.1316 12.4892 10.8357C12.4773 10.5462 12.4713 10.2567 12.4713 9.96721C12.4713 9.76444 12.5103 9.57406 12.588 9.39493L12.5884 9.39399C12.6631 9.21704 12.7658 9.06134 12.8968 8.92642C13.0236 8.79595 13.1753 8.68945 13.3533 8.6075C13.5294 8.53046 13.715 8.4918 13.9108 8.4918H14V8.4V7.6V7.5082H13.9108C13.7153 7.5082 13.53 7.46762 13.354 7.38666L13.3526 7.38604C13.1754 7.30844 13.0242 7.20238 12.8978 7.06839L12.8959 7.06648C12.7657 6.9363 12.6634 6.78129 12.5887 6.60058L12.588 6.59892C12.5101 6.41953 12.4713 6.23117 12.4713 6.03279C12.4713 5.74329 12.4773 5.45379 12.4892 5.16428C12.5013 4.86842 12.4932 4.57848 12.4648 4.29454L12.4646 4.29285C12.4318 4.00971 12.3661 3.73502 12.2677 3.46897L12.2672 3.46766C12.1621 3.19499 11.9969 2.94422 11.7742 2.71498C11.5506 2.48474 11.2918 2.30798 10.9987 2.18473C10.7059 2.06161 10.4026 2 10.0892 2H10V2.8V2.98361H10.0892C10.2891 2.98361 10.4765 3.0223 10.6524 3.09917C10.826 3.18092 10.9781 3.28736 11.1091 3.41823C11.2361 3.55305 11.339 3.70889 11.4178 3.88628C11.4916 4.0654 11.5287 4.25596 11.5287 4.45902C11.5287 4.68727 11.5247 4.91145 11.5168 5.13142C11.5088 5.35894 11.5087 5.58049 11.5168 5.79605C11.5209 6.01754 11.5392 6.23117 11.5718 6.43688C11.6049 6.64976 11.6608 6.85243 11.7396 7.04464C11.8191 7.23843 11.9274 7.42254 12.0638 7.59702C12.1765 7.74119 12.3146 7.87538 12.4772 8C12.3146 8.12462 12.1765 8.25881 12.0638 8.40298C11.9274 8.57746 11.8191 8.76157 11.7396 8.95536C11.6609 9.14737 11.6049 9.34825 11.5717 9.55715C11.5392 9.7667 11.5209 9.98027 11.5168 10.1978C11.5087 10.4174 11.5087 10.6389 11.5168 10.8625C11.5247 11.0866 11.5287 11.3128 11.5287 11.541C11.5287 11.7398 11.4917 11.9285 11.4176 12.1079C11.3386 12.2897 11.2358 12.4452 11.1091 12.5756C10.9781 12.7106 10.8254 12.8173 10.652 12.8949C10.4762 12.9757 10.2889 13.0164 10.0892 13.0164H10Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/SwitchToCode/switchtocode.svg b/src/datascience-ui/react-common/images/SwitchToCode/switchtocode.svg new file mode 100644 index 000000000000..bc21caf9f4b6 --- /dev/null +++ b/src/datascience-ui/react-common/images/SwitchToCode/switchtocode.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M6 2.98361V2.97184V2H5.91083C5.59743 2 5.29407 2.06161 5.00128 2.18473C4.70818 2.30798 4.44942 2.48474 4.22578 2.71498C4.00311 2.94422 3.83792 3.19498 3.73282 3.46766L3.73233 3.46898C3.63382 3.7352 3.56814 4.01201 3.53533 4.29917L3.53519 4.30053C3.50678 4.5805 3.4987 4.86844 3.51084 5.16428C3.52272 5.45379 3.52866 5.74329 3.52866 6.03279C3.52866 6.23556 3.48974 6.42594 3.412 6.60507L3.4116 6.60601C3.33687 6.78296 3.23423 6.93866 3.10317 7.07359C2.97644 7.20405 2.82466 7.31055 2.64672 7.3925C2.4706 7.46954 2.28497 7.5082 2.08917 7.5082H2V7.6V8.4V8.4918H2.08917C2.28465 8.4918 2.47001 8.53238 2.64601 8.61334L2.64742 8.61396C2.82457 8.69157 2.97577 8.79762 3.10221 8.93161L3.10412 8.93352C3.23428 9.0637 3.33659 9.21871 3.41129 9.39942L3.41201 9.40108C3.48986 9.58047 3.52866 9.76883 3.52866 9.96721C3.52866 10.2567 3.52272 10.5462 3.51084 10.8357C3.4987 11.1316 3.50677 11.4215 3.53516 11.7055L3.53535 11.7072C3.56819 11.9903 3.63387 12.265 3.73232 12.531L3.73283 12.5323C3.83793 12.805 4.00311 13.0558 4.22578 13.285C4.44942 13.5153 4.70818 13.692 5.00128 13.8153C5.29407 13.9384 5.59743 14 5.91083 14H6V13.2V13.0164H5.91083C5.71095 13.0164 5.52346 12.9777 5.34763 12.9008C5.17396 12.8191 5.02194 12.7126 4.89086 12.5818C4.76386 12.4469 4.66104 12.2911 4.58223 12.1137C4.50838 11.9346 4.47134 11.744 4.47134 11.541C4.47134 11.3127 4.4753 11.0885 4.48321 10.8686C4.49125 10.6411 4.49127 10.4195 4.48324 10.2039C4.47914 9.98246 4.46084 9.76883 4.42823 9.56312C4.39513 9.35024 4.33921 9.14757 4.26039 8.95536C4.18091 8.76157 4.07258 8.57746 3.93616 8.40298C3.82345 8.25881 3.68538 8.12462 3.52283 8C3.68538 7.87538 3.82345 7.74119 3.93616 7.59702C4.07258 7.42254 4.18091 7.23843 4.26039 7.04464C4.33913 6.85263 4.39513 6.65175 4.42826 6.44285C4.46082 6.2333 4.47914 6.01973 4.48324 5.80219C4.49127 5.58262 4.49125 5.36105 4.48321 5.13749C4.4753 4.9134 4.47134 4.68725 4.47134 4.45902C4.47134 4.26019 4.50833 4.07152 4.58238 3.89205C4.66135 3.71034 4.76421 3.55475 4.89086 3.42437C5.02193 3.28942 5.17461 3.18275 5.34802 3.10513C5.5238 3.02427 5.71113 2.98361 5.91083 2.98361H6ZM10 13.0164V13.0282V14H10.0892C10.4026 14 10.7059 13.9384 10.9987 13.8153C11.2918 13.692 11.5506 13.5153 11.7742 13.285C11.9969 13.0558 12.1621 12.805 12.2672 12.5323L12.2677 12.531C12.3662 12.2648 12.4319 11.988 12.4647 11.7008L12.4648 11.6995C12.4932 11.4195 12.5013 11.1316 12.4892 10.8357C12.4773 10.5462 12.4713 10.2567 12.4713 9.96721C12.4713 9.76444 12.5103 9.57406 12.588 9.39493L12.5884 9.39399C12.6631 9.21704 12.7658 9.06134 12.8968 8.92642C13.0236 8.79595 13.1753 8.68945 13.3533 8.6075C13.5294 8.53046 13.715 8.4918 13.9108 8.4918H14V8.4V7.6V7.5082H13.9108C13.7153 7.5082 13.53 7.46762 13.354 7.38666L13.3526 7.38604C13.1754 7.30844 13.0242 7.20238 12.8978 7.06839L12.8959 7.06648C12.7657 6.9363 12.6634 6.78129 12.5887 6.60058L12.588 6.59892C12.5101 6.41953 12.4713 6.23117 12.4713 6.03279C12.4713 5.74329 12.4773 5.45379 12.4892 5.16428C12.5013 4.86842 12.4932 4.57848 12.4648 4.29454L12.4646 4.29285C12.4318 4.00971 12.3661 3.73502 12.2677 3.46897L12.2672 3.46766C12.1621 3.19499 11.9969 2.94422 11.7742 2.71498C11.5506 2.48474 11.2918 2.30798 10.9987 2.18473C10.7059 2.06161 10.4026 2 10.0892 2H10V2.8V2.98361H10.0892C10.2891 2.98361 10.4765 3.0223 10.6524 3.09917C10.826 3.18092 10.9781 3.28736 11.1091 3.41823C11.2361 3.55305 11.339 3.70889 11.4178 3.88628C11.4916 4.0654 11.5287 4.25596 11.5287 4.45902C11.5287 4.68727 11.5247 4.91145 11.5168 5.13142C11.5088 5.35894 11.5087 5.58049 11.5168 5.79605C11.5209 6.01754 11.5392 6.23117 11.5718 6.43688C11.6049 6.64976 11.6608 6.85243 11.7396 7.04464C11.8191 7.23843 11.9274 7.42254 12.0638 7.59702C12.1765 7.74119 12.3146 7.87538 12.4772 8C12.3146 8.12462 12.1765 8.25881 12.0638 8.40298C11.9274 8.57746 11.8191 8.76157 11.7396 8.95536C11.6609 9.14737 11.6049 9.34825 11.5717 9.55715C11.5392 9.7667 11.5209 9.98027 11.5168 10.1978C11.5087 10.4174 11.5087 10.6389 11.5168 10.8625C11.5247 11.0866 11.5287 11.3128 11.5287 11.541C11.5287 11.7398 11.4917 11.9285 11.4176 12.1079C11.3386 12.2897 11.2358 12.4452 11.1091 12.5756C10.9781 12.7106 10.8254 12.8173 10.652 12.8949C10.4762 12.9757 10.2889 13.0164 10.0892 13.0164H10Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/SwitchToMarkdown/switchtomarkdown-inverse.svg b/src/datascience-ui/react-common/images/SwitchToMarkdown/switchtomarkdown-inverse.svg new file mode 100644 index 000000000000..388652f13b4c --- /dev/null +++ b/src/datascience-ui/react-common/images/SwitchToMarkdown/switchtomarkdown-inverse.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important;" d="M5.76758 10.3789C5.82227 10.2227 5.87695 10.0684 5.93164 9.91602C5.99023 9.75977 6.05273 9.60742 6.11914 9.45898L8.42188 4.25H9.5V11.75H8.64453V6.7168C8.64453 6.4707 8.65039 6.22852 8.66211 5.99023C8.67383 5.74805 8.68555 5.50391 8.69727 5.25781C8.66602 5.37891 8.63281 5.50195 8.59766 5.62695C8.56641 5.75195 8.52539 5.87109 8.47461 5.98438L5.96094 11.75H5.53906L3.03125 6.03125C2.97656 5.91016 2.93164 5.7832 2.89648 5.65039C2.86523 5.51758 2.83008 5.38672 2.79102 5.25781C2.81055 5.50391 2.82227 5.75 2.82617 5.99609C2.83008 6.23828 2.83203 6.48242 2.83203 6.72852V11.75H2V4.25H3.13672L5.39844 9.48242C5.46094 9.62695 5.52344 9.77539 5.58594 9.92773C5.64844 10.0762 5.69727 10.2266 5.73242 10.3789H5.76758ZM13.8887 10.1387L12.125 11.9023L10.3613 10.1387L10.8887 9.61133L11.75 10.4668V4.25H12.5V10.4668L13.3613 9.61133L13.8887 10.1387Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/SwitchToMarkdown/switchtomarkdown.svg b/src/datascience-ui/react-common/images/SwitchToMarkdown/switchtomarkdown.svg new file mode 100644 index 000000000000..34347fe785d1 --- /dev/null +++ b/src/datascience-ui/react-common/images/SwitchToMarkdown/switchtomarkdown.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M5.76758 10.3789C5.82227 10.2227 5.87695 10.0684 5.93164 9.91602C5.99023 9.75977 6.05273 9.60742 6.11914 9.45898L8.42188 4.25H9.5V11.75H8.64453V6.7168C8.64453 6.4707 8.65039 6.22852 8.66211 5.99023C8.67383 5.74805 8.68555 5.50391 8.69727 5.25781C8.66602 5.37891 8.63281 5.50195 8.59766 5.62695C8.56641 5.75195 8.52539 5.87109 8.47461 5.98438L5.96094 11.75H5.53906L3.03125 6.03125C2.97656 5.91016 2.93164 5.7832 2.89648 5.65039C2.86523 5.51758 2.83008 5.38672 2.79102 5.25781C2.81055 5.50391 2.82227 5.75 2.82617 5.99609C2.83008 6.23828 2.83203 6.48242 2.83203 6.72852V11.75H2V4.25H3.13672L5.39844 9.48242C5.46094 9.62695 5.52344 9.77539 5.58594 9.92773C5.64844 10.0762 5.69727 10.2266 5.73242 10.3789H5.76758ZM13.8887 10.1387L12.125 11.9023L10.3613 10.1387L10.8887 9.61133L11.75 10.4668V4.25H12.5V10.4668L13.3613 9.61133L13.8887 10.1387Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Sync/sync-inverse.svg b/src/datascience-ui/react-common/images/Sync/sync-inverse.svg new file mode 100644 index 000000000000..7406dc99d8e9 --- /dev/null +++ b/src/datascience-ui/react-common/images/Sync/sync-inverse.svg @@ -0,0 +1,3 @@ +<svg class="rotate" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> +<path style="fill: #C5C5C5 !important;" fill-rule="evenodd" clip-rule="evenodd" d="M2.006 8.267L.78 9.5 0 8.73l2.09-2.07.76.01 2.09 2.12-.76.76-1.167-1.18a5 5 0 0 0 9.4 1.983l.813.597a6 6 0 0 1-11.22-2.683zm10.99-.466L11.76 6.55l-.76.76 2.09 2.11.76.01 2.09-2.07-.75-.76-1.194 1.18a6 6 0 0 0-11.11-2.92l.81.594a5 5 0 0 1 9.3 2.346z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Sync/sync.svg b/src/datascience-ui/react-common/images/Sync/sync.svg new file mode 100644 index 000000000000..74a4f483e00c --- /dev/null +++ b/src/datascience-ui/react-common/images/Sync/sync.svg @@ -0,0 +1,3 @@ +<svg class="rotate" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> +<path style="fill: #424242 !important;" fill-rule="evenodd" clip-rule="evenodd" d="M2.006 8.267L.78 9.5 0 8.73l2.09-2.07.76.01 2.09 2.12-.76.76-1.167-1.18a5 5 0 0 0 9.4 1.983l.813.597a6 6 0 0 1-11.22-2.683zm10.99-.466L11.76 6.55l-.76.76 2.09 2.11.76.01 2.09-2.07-.75-.76-1.194 1.18a6 6 0 0 0-11.11-2.92l.81.594a5 5 0 0 1 9.3 2.346z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Undo/Undo_16x_vscode.svg b/src/datascience-ui/react-common/images/Undo/Undo_16x_vscode.svg new file mode 100644 index 000000000000..670fc2eb3404 --- /dev/null +++ b/src/datascience-ui/react-common/images/Undo/Undo_16x_vscode.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill:#424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M3.5 2V5.5L4 6H7.5V5H4.979L5.92041 4.05869C7.30762 2.67157 9.55664 2.67157 10.9434 4.05869C12.3306 5.4458 12.3306 7.69476 10.9434 9.08188L5.74561 14.2799L6.46582 14.9999L11.6636 9.80194C13.4482 8.01715 13.4482 5.12341 11.6636 3.33859C9.87891 1.5538 6.98486 1.5538 5.2002 3.33859L4.5 4.03882V2H3.5Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Undo/Undo_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/Undo/Undo_16x_vscode_dark.svg new file mode 100644 index 000000000000..4db7708d1b13 --- /dev/null +++ b/src/datascience-ui/react-common/images/Undo/Undo_16x_vscode_dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M3.5 2V5.5L4 6H7.5V5H4.979L5.92041 4.05869C7.30762 2.67157 9.55664 2.67157 10.9434 4.05869C12.3306 5.4458 12.3306 7.69476 10.9434 9.08188L5.74561 14.2799L6.46582 14.9999L11.6636 9.80194C13.4482 8.01715 13.4482 5.12341 11.6636 3.33859C9.87891 1.5538 6.98486 1.5538 5.2002 3.33859L4.5 4.03882V2H3.5Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Up/up-inverse.svg b/src/datascience-ui/react-common/images/Up/up-inverse.svg new file mode 100644 index 000000000000..72f710713430 --- /dev/null +++ b/src/datascience-ui/react-common/images/Up/up-inverse.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill:#C5C5C5 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M8 6.04042L3.02022 11.0202L2.31311 10.3131L7.64644 4.97976L8.35355 4.97976L13.6869 10.3131L12.9798 11.0202L8 6.04042Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Up/up.svg b/src/datascience-ui/react-common/images/Up/up.svg new file mode 100644 index 000000000000..ff5354d2977a --- /dev/null +++ b/src/datascience-ui/react-common/images/Up/up.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M8 6.04042L3.02022 11.0202L2.31311 10.3131L7.64644 4.97976L8.35355 4.97976L13.6869 10.3131L12.9798 11.0202L8 6.04042Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/VariableExplorer/variable_explorer_dark.svg b/src/datascience-ui/react-common/images/VariableExplorer/variable_explorer_dark.svg new file mode 100644 index 000000000000..4deff429c333 --- /dev/null +++ b/src/datascience-ui/react-common/images/VariableExplorer/variable_explorer_dark.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill:#C5C5C5 !important;" d="M2.75 3.5H12.5V12.5H2.75V3.5ZM6.5 8V9.5H8.75V8H6.5ZM8.75 7.25V5.75H6.5V7.25H8.75ZM5.75 7.25V5.75H3.5V7.25H5.75ZM3.5 8V9.5H5.75V8H3.5ZM5.75 11.75V10.25H3.5V11.75H5.75ZM8.75 11.75V10.25H6.5V11.75H8.75ZM11.75 11.75V10.25H9.5V11.75H11.75ZM11.75 9.5V8H9.5V9.5H11.75ZM11.75 7.25V5.75H9.5V7.25H11.75ZM3.5 5H11.75V4.25H3.5V5Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/VariableExplorer/variable_explorer_light.svg b/src/datascience-ui/react-common/images/VariableExplorer/variable_explorer_light.svg new file mode 100644 index 000000000000..6fdbb1675a37 --- /dev/null +++ b/src/datascience-ui/react-common/images/VariableExplorer/variable_explorer_light.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M2.75 3.5H12.5V12.5H2.75V3.5ZM6.5 8V9.5H8.75V8H6.5ZM8.75 7.25V5.75H6.5V7.25H8.75ZM5.75 7.25V5.75H3.5V7.25H5.75ZM3.5 8V9.5H5.75V8H3.5ZM5.75 11.75V10.25H3.5V11.75H5.75ZM8.75 11.75V10.25H6.5V11.75H8.75ZM11.75 11.75V10.25H9.5V11.75H11.75ZM11.75 9.5V8H9.5V9.5H11.75ZM11.75 7.25V5.75H9.5V7.25H11.75ZM3.5 5H11.75V4.25H3.5V5Z"/> +</svg> diff --git a/src/datascience-ui/react-common/images/Zoom/zoom.svg b/src/datascience-ui/react-common/images/Zoom/zoom.svg new file mode 100644 index 000000000000..213e6224adb6 --- /dev/null +++ b/src/datascience-ui/react-common/images/Zoom/zoom.svg @@ -0,0 +1,11 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path style="fill:#424242 !important;" d="M11.1405 10.6095L8.043 7.51276C8.72354 6.69576 9.06291 5.64783 8.9905 4.58698C8.91808 3.52613 8.43947 2.53403 7.65421 1.81708C6.86896 1.10013 5.83753 0.713516 4.77448 0.73767C3.71144 0.761824 2.69863 1.19489 1.94675 1.94677C1.19487 2.69865 0.761809 3.71145 0.737655 4.7745C0.7135 5.83754 1.10011 6.86897 1.81707 7.65423C2.53402 8.43948 3.52611 8.9181 4.58696 8.99051C5.64781 9.06293 6.69574 8.72356 7.51275 8.04301L10.6095 11.1405C10.6444 11.1754 10.6858 11.203 10.7313 11.2219C10.7769 11.2408 10.8257 11.2505 10.875 11.2505C10.9243 11.2505 10.9731 11.2408 11.0187 11.2219C11.0642 11.203 11.1056 11.1754 11.1405 11.1405C11.1754 11.1056 11.203 11.0643 11.2219 11.0187C11.2408 10.9731 11.2505 10.9243 11.2505 10.875C11.2505 10.8257 11.2408 10.7769 11.2219 10.7313C11.203 10.6858 11.1754 10.6444 11.1405 10.6095ZM4.875 8.25001C4.20749 8.25001 3.55496 8.05207 2.99995 7.68122C2.44493 7.31037 2.01235 6.78327 1.7569 6.16657C1.50146 5.54987 1.43462 4.87127 1.56485 4.21658C1.69507 3.5619 2.01651 2.96053 2.48851 2.48853C2.96051 2.01653 3.56188 1.69509 4.21657 1.56486C4.87125 1.43464 5.54985 1.50147 6.16655 1.75692C6.78325 2.01237 7.31036 2.44495 7.68121 2.99996C8.05206 3.55498 8.25 4.2075 8.25 4.87501C8.24901 5.76981 7.89311 6.62768 7.26039 7.2604C6.62767 7.89312 5.7698 8.24902 4.875 8.25001Z"/> +<path style="fill:#00539C !important;" d="M6.75 4.5V5.25H5.25V6.75H4.5V5.25H3V4.5H4.5V3H5.25V4.5H6.75Z"/> +</g> +<defs> +<clipPath id="clip0"> +<rect width="12" height="12" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/src/datascience-ui/react-common/images/Zoom/zoom_inverse.svg b/src/datascience-ui/react-common/images/Zoom/zoom_inverse.svg new file mode 100644 index 000000000000..8cecde9b3b9d --- /dev/null +++ b/src/datascience-ui/react-common/images/Zoom/zoom_inverse.svg @@ -0,0 +1,11 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path style="fill:#C5C5C5 !important;" d="M11.1405 10.6095L8.043 7.51276C8.72354 6.69576 9.06291 5.64783 8.9905 4.58698C8.91808 3.52613 8.43947 2.53403 7.65421 1.81708C6.86896 1.10013 5.83753 0.713516 4.77448 0.73767C3.71144 0.761824 2.69863 1.19489 1.94675 1.94677C1.19487 2.69865 0.761809 3.71145 0.737655 4.7745C0.7135 5.83754 1.10011 6.86897 1.81707 7.65423C2.53402 8.43948 3.52611 8.9181 4.58696 8.99051C5.64781 9.06293 6.69574 8.72356 7.51275 8.04301L10.6095 11.1405C10.6444 11.1754 10.6858 11.203 10.7313 11.2219C10.7769 11.2408 10.8257 11.2505 10.875 11.2505C10.9243 11.2505 10.9731 11.2408 11.0187 11.2219C11.0642 11.203 11.1056 11.1754 11.1405 11.1405C11.1754 11.1056 11.203 11.0643 11.2219 11.0187C11.2408 10.9731 11.2505 10.9243 11.2505 10.875C11.2505 10.8257 11.2408 10.7769 11.2219 10.7313C11.203 10.6858 11.1754 10.6444 11.1405 10.6095ZM4.875 8.25001C4.20749 8.25001 3.55496 8.05207 2.99995 7.68122C2.44493 7.31037 2.01235 6.78327 1.7569 6.16657C1.50146 5.54987 1.43462 4.87127 1.56485 4.21658C1.69507 3.5619 2.01651 2.96053 2.48851 2.48853C2.96051 2.01653 3.56188 1.69509 4.21657 1.56486C4.87125 1.43464 5.54985 1.50147 6.16655 1.75692C6.78325 2.01237 7.31036 2.44495 7.68121 2.99996C8.05206 3.55498 8.25 4.2075 8.25 4.87501C8.24901 5.76981 7.89311 6.62768 7.26039 7.2604C6.62767 7.89312 5.7698 8.24902 4.875 8.25001Z"/> +<path style="fill:#75BEFF !important;" d="M6.75 4.5V5.25H5.25V6.75H4.5V5.25H3V4.5H4.5V3H5.25V4.5H6.75Z"/> +</g> +<defs> +<clipPath id="clip0"> +<rect width="12" height="12" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/src/datascience-ui/react-common/images/ZoomOut/zoomout.svg b/src/datascience-ui/react-common/images/ZoomOut/zoomout.svg new file mode 100644 index 000000000000..009bb8b89e0e --- /dev/null +++ b/src/datascience-ui/react-common/images/ZoomOut/zoomout.svg @@ -0,0 +1,11 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path style="fill:#424242 !important;" d="M11.1405 10.6095L8.043 7.51276C8.72354 6.69576 9.06291 5.64783 8.9905 4.58698C8.91808 3.52613 8.43947 2.53403 7.65421 1.81708C6.86896 1.10013 5.83753 0.713516 4.77448 0.73767C3.71144 0.761824 2.69863 1.19489 1.94675 1.94677C1.19487 2.69865 0.761809 3.71145 0.737655 4.7745C0.7135 5.83754 1.10011 6.86897 1.81707 7.65423C2.53402 8.43948 3.52611 8.9181 4.58696 8.99051C5.64781 9.06293 6.69574 8.72356 7.51275 8.04301L10.6095 11.1405C10.6444 11.1754 10.6858 11.203 10.7313 11.2219C10.7769 11.2408 10.8257 11.2505 10.875 11.2505C10.9243 11.2505 10.9731 11.2408 11.0187 11.2219C11.0642 11.203 11.1056 11.1754 11.1405 11.1405C11.1754 11.1056 11.203 11.0643 11.2219 11.0187C11.2408 10.9731 11.2505 10.9243 11.2505 10.875C11.2505 10.8257 11.2408 10.7769 11.2219 10.7313C11.203 10.6858 11.1754 10.6444 11.1405 10.6095ZM4.875 8.25001C4.20749 8.25001 3.55496 8.05207 2.99995 7.68122C2.44493 7.31037 2.01235 6.78327 1.7569 6.16657C1.50146 5.54987 1.43462 4.87127 1.56485 4.21658C1.69507 3.5619 2.01651 2.96053 2.48851 2.48853C2.96051 2.01653 3.56188 1.69509 4.21657 1.56486C4.87125 1.43464 5.54985 1.50147 6.16655 1.75692C6.78325 2.01237 7.31036 2.44495 7.68121 2.99996C8.05206 3.55498 8.25 4.2075 8.25 4.87501C8.24901 5.76981 7.89311 6.62768 7.26039 7.2604C6.62767 7.89312 5.7698 8.24902 4.875 8.25001Z"/> +<path style="fill:#00539C !important;" d="M6.75 4.5V5.25H3V4.5H6.75Z"/> +</g> +<defs> +<clipPath id="clip0"> +<rect width="12" height="12" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/src/datascience-ui/react-common/images/ZoomOut/zoomout_inverse.svg b/src/datascience-ui/react-common/images/ZoomOut/zoomout_inverse.svg new file mode 100644 index 000000000000..a2e887d89c74 --- /dev/null +++ b/src/datascience-ui/react-common/images/ZoomOut/zoomout_inverse.svg @@ -0,0 +1,11 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path style="fill:#C5C5C5 !important;" d="M11.1405 10.6095L8.043 7.51276C8.72354 6.69576 9.06291 5.64783 8.9905 4.58698C8.91808 3.52613 8.43947 2.53403 7.65421 1.81708C6.86896 1.10013 5.83753 0.713516 4.77448 0.73767C3.71144 0.761824 2.69863 1.19489 1.94675 1.94677C1.19487 2.69865 0.761809 3.71145 0.737655 4.7745C0.7135 5.83754 1.10011 6.86897 1.81707 7.65423C2.53402 8.43948 3.52611 8.9181 4.58696 8.99051C5.64781 9.06293 6.69574 8.72356 7.51275 8.04301L10.6095 11.1405C10.6444 11.1754 10.6858 11.203 10.7313 11.2219C10.7769 11.2408 10.8257 11.2505 10.875 11.2505C10.9243 11.2505 10.9731 11.2408 11.0187 11.2219C11.0642 11.203 11.1056 11.1754 11.1405 11.1405C11.1754 11.1056 11.203 11.0643 11.2219 11.0187C11.2408 10.9731 11.2505 10.9243 11.2505 10.875C11.2505 10.8257 11.2408 10.7769 11.2219 10.7313C11.203 10.6858 11.1754 10.6444 11.1405 10.6095ZM4.875 8.25001C4.20749 8.25001 3.55496 8.05207 2.99995 7.68122C2.44493 7.31037 2.01235 6.78327 1.7569 6.16657C1.50146 5.54987 1.43462 4.87127 1.56485 4.21658C1.69507 3.5619 2.01651 2.96053 2.48851 2.48853C2.96051 2.01653 3.56188 1.69509 4.21657 1.56486C4.87125 1.43464 5.54985 1.50147 6.16655 1.75692C6.78325 2.01237 7.31036 2.44495 7.68121 2.99996C8.05206 3.55498 8.25 4.2075 8.25 4.87501C8.24901 5.76981 7.89311 6.62768 7.26039 7.2604C6.62767 7.89312 5.7698 8.24902 4.875 8.25001Z"/> +<path style="fill:#75BEFF !important;" d="M6.75 4.5V5.25H3V4.5H6.75Z"/> +</g> +<defs> +<clipPath id="clip0"> +<rect width="12" height="12" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/src/datascience-ui/react-common/locReactSide.ts b/src/datascience-ui/react-common/locReactSide.ts new file mode 100644 index 000000000000..dec1f5fa6946 --- /dev/null +++ b/src/datascience-ui/react-common/locReactSide.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +// The react code can't use the localize.ts module because it reads from +// disk. This isn't allowed inside a browser, so we pass the collection +// through the javascript. +let loadedCollection: Record<string, string> | undefined; + +export function getLocString(key: string, defValue: string): string { + if (loadedCollection && loadedCollection.hasOwnProperty(key)) { + return loadedCollection[key]; + } + + return defValue; +} + +export function storeLocStrings(collection: Record<string, string>) { + loadedCollection = collection; +} diff --git a/src/datascience-ui/react-common/logger.ts b/src/datascience-ui/react-common/logger.ts new file mode 100644 index 000000000000..b062fac823ff --- /dev/null +++ b/src/datascience-ui/react-common/logger.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { isTestExecution } from '../../client/common/constants'; + +const enableLogger = !isTestExecution() || process.env.VSC_PYTHON_FORCE_LOGGING || process.env.VSC_PYTHON_LOG_FILE; + +// Might want to post this back to the other side too. This was +export function logMessage(message: string) { + // put here to prevent having to disable the console log warning + if (enableLogger) { + // tslint:disable-next-line: no-console + console.log(message); + } +} diff --git a/src/datascience-ui/react-common/monacoEditor.css b/src/datascience-ui/react-common/monacoEditor.css new file mode 100644 index 000000000000..eb52202d2c7e --- /dev/null +++ b/src/datascience-ui/react-common/monacoEditor.css @@ -0,0 +1,123 @@ +.measure-width-div { + width: 95vw; + visibility: hidden; + position: absolute; +} + +.monaco-editor-outer-container .mtk1 { + /* For some reason the monaco editor refuses to update this style no matter the theme. It's always black */ + color: var(--override-foreground, var(--vscode-editor-foreground)); +} + +.monaco-editor .mtk1 { + /* For some reason the monaco editor refuses to update this style no matter the theme. It's always black */ + color: var(--override-foreground, var(--vscode-editor-foreground)); +} + +/* Bunch of styles copied from vscode. Handles the hover window */ + + .monaco-editor-hover { + cursor: default; + position: absolute; + overflow: hidden; + z-index: 50; + -webkit-user-select: text; + -ms-user-select: text; + -khtml-user-select: text; + -moz-user-select: text; + -o-user-select: text; + user-select: text; + box-sizing: initial; + animation: fadein 100ms linear; + line-height: 1.5em; +} + +.monaco-editor-hover.hidden { + display: none; +} + +.monaco-editor-hover .hover-contents { + padding: 4px 8px; +} + +.monaco-editor-hover .markdown-hover > .hover-contents:not(.code-hover-contents) { + max-width: 500px; +} + +.monaco-editor-hover p, +.monaco-editor-hover ul { + margin: 8px 0; +} + +.monaco-editor-hover hr { + margin-top: 4px; + margin-bottom: -6px; + margin-left: -10px; + margin-right: -10px; + height: 1px; +} + +.monaco-editor-hover p:first-child, +.monaco-editor-hover ul:first-child { + margin-top: 0; +} + +.monaco-editor-hover p:last-child, +.monaco-editor-hover ul:last-child { + margin-bottom: 0; +} + +.monaco-editor-hover ul { + padding-left: 20px; +} + +.monaco-editor-hover li > p { + margin-bottom: 0; +} + +.monaco-editor-hover li > ul { + margin-top: 0; +} + +.monaco-editor-hover code { + border-radius: 3px; + padding: 0 0.4em; +} + +.monaco-editor-hover .monaco-tokenized-source { + white-space: pre-wrap; + word-break: break-all; +} + +.monaco-editor-hover .hover-row.status-bar { + font-size: 12px; + line-height: 22px; +} + +.monaco-editor-hover .hover-row.status-bar .actions { + display: flex; +} + +.monaco-editor-hover .hover-row.status-bar .actions .action-container { + margin: 0px 8px; + cursor: pointer; +} + +.monaco-editor-hover .hover-row.status-bar .actions .action-container .action .icon { + padding-right: 4px; +} + +.monaco-editor .margin { + background-color: transparent !important; +} + +.monaco-editor .parameter-hints-widget > .wrapper { + overflow: hidden; +} + +.monaco-editor .view-overlays .debug-top-stack-frame-line { background: var(--vscode-editor-stackFrameHighlightBackground); } + +.codicon-debug-stackframe-active:before { content: "\eb89" } +.codicon-debug-stackframe-dot:before { content: "\eb8a" } +.codicon-debug-stackframe:before { content: "\eb8b"; color: var(--vscode-debugIcon-breakpointCurrentStackframeForeground); } +.codicon-debug-stackframe-focused:before { content: "\eb8b" } diff --git a/src/datascience-ui/react-common/monacoEditor.tsx b/src/datascience-ui/react-common/monacoEditor.tsx new file mode 100644 index 000000000000..cc1136d77df5 --- /dev/null +++ b/src/datascience-ui/react-common/monacoEditor.tsx @@ -0,0 +1,1049 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as fastDeepEqual from 'fast-deep-equal'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import * as React from 'react'; +import { isTestExecution } from '../../client/common/constants'; +import { IDisposable } from '../../client/common/types'; +import { logMessage } from './logger'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const debounce = require('lodash/debounce') as typeof import('lodash/debounce'); + +// See the discussion here: https://github.com/Microsoft/tslint-microsoft-contrib/issues/676 +// tslint:disable: react-this-binding-issue +// tslint:disable-next-line:no-require-imports no-var-requires +const throttle = require('lodash/throttle') as typeof import('lodash/throttle'); + +import { noop } from '../../client/common/utils/misc'; +import { CursorPos } from '../interactive-common/mainState'; +import './codicon/codicon.css'; +import './monacoEditor.css'; +import { generateChangeEvent, IMonacoModelContentChangeEvent } from './monacoHelpers'; + +const LINE_HEIGHT = 18; + +const HOVER_DISPOSABLE_EVENT_COUNT = 8; +const HOVER_DISPOSABLE_LEAVE_INDEX = 5; + +enum WidgetCSSSelector { + /** + * CSS Selector for the parameters widget displayed by Monaco. + */ + Parameters = '.parameter-hints-widget', + /** + * CSS Selector for the hover widget displayed by Monaco. + */ + Hover = '.monaco-editor-hover' +} + +export interface IMonacoEditorProps { + language: string; + value: string; + theme?: string; + outermostParentClass: string; + options: monacoEditor.editor.IEditorConstructionOptions; + testMode?: boolean; + forceBackground?: string; + measureWidthClassName?: string; + version: number; + hasFocus: boolean; + cursorPos: CursorPos | monacoEditor.IPosition; + modelChanged(e: IMonacoModelContentChangeEvent): void; + editorMounted(editor: monacoEditor.editor.IStandaloneCodeEditor): void; + openLink(uri: monacoEditor.Uri): void; +} + +export interface IMonacoEditorState { + editor?: monacoEditor.editor.IStandaloneCodeEditor; + model: monacoEditor.editor.ITextModel | null; + widgetsReparented: boolean; // Keeps track of when we reparent the hover widgets so they work inside something with overflow +} + +// Need this to prevent wiping of the current value on a componentUpdate. react-monaco-editor has that problem. + +export class MonacoEditor extends React.Component<IMonacoEditorProps, IMonacoEditorState> { + private static lineHeight: number = 0; + private containerRef: React.RefObject<HTMLDivElement>; + private measureWidthRef: React.RefObject<HTMLDivElement>; + private resizeTimer?: number; + private leaveTimer?: number; + private subscriptions: monacoEditor.IDisposable[] = []; + private widgetParent: HTMLDivElement | undefined; + private outermostParent: HTMLElement | null = null; + private enteredHover: boolean = false; + private lastOffsetLeft: number | undefined; + private lastOffsetTop: number | undefined; + private debouncedUpdateEditorSize: () => void | undefined; + private styleObserver: MutationObserver | undefined; + private watchingMargin: boolean = false; + private throttledUpdateWidgetPosition = throttle(this.updateWidgetPosition.bind(this), 100); + private throttledScrollCurrentPosition = throttle(this.tryToScrollToCurrentPosition.bind(this), 100); + private monacoContainer: HTMLDivElement | undefined; + private lineTops: { top: number; index: number }[] = []; + private debouncedComputeLineTops = debounce(this.computeLineTops.bind(this), 100); + private skipNotifications = false; + private previousModelValue: string = ''; + /** + * Reference to parameter widget (used by monaco to display parameter docs). + */ + private parameterWidget?: Element; + private keyHasBeenPressed = false; + private visibleLineCount: number = -1; + private attached: boolean = false; // Keeps track of when we reparent the editor out of the dummy dom node. + private pendingLayoutScroll = false; + private lastPasteCommandTime = 0; + private lastPasteCommandText = ''; + + constructor(props: IMonacoEditorProps) { + super(props); + this.state = { + editor: undefined, + model: null, + widgetsReparented: false + }; + this.containerRef = React.createRef<HTMLDivElement>(); + this.measureWidthRef = React.createRef<HTMLDivElement>(); + this.debouncedUpdateEditorSize = debounce(this.updateEditorSize.bind(this), 150); + this.hideAllOtherHoverAndParameterWidgets = debounce(this.hideAllOtherHoverAndParameterWidgets.bind(this), 150); + } + + // tslint:disable-next-line: max-func-body-length + public componentDidMount = () => { + if (window) { + window.addEventListener('resize', this.windowResized); + } + if (this.containerRef.current) { + // Compute our outermost parent + let outerParent = this.containerRef.current.parentElement; + while (outerParent && !outerParent.classList.contains(this.props.outermostParentClass)) { + outerParent = outerParent.parentElement; + } + this.outermostParent = outerParent; + if (this.outermostParent) { + this.outermostParent.addEventListener('mouseleave', this.outermostParentLeave); + } + + // Create a dummy DOM node to attach the editor to so that it skips layout. + this.monacoContainer = document.createElement('div'); + this.monacoContainer.setAttribute('class', 'monaco-editor-container'); + + // Create the editor + const editor = monacoEditor.editor.create(this.monacoContainer, { + value: this.props.value, + language: this.props.language, + ...this.props.options + }); + + // Listen for commands on the editor. This is to work around a problem + // with double pasting + // tslint:disable: no-any + if ((editor as any)._commandService) { + const commandService = (editor as any)._commandService as any; + if (commandService._onWillExecuteCommand) { + this.subscriptions.push( + commandService._onWillExecuteCommand.event(this.onCommandWillExecute.bind(this)) + ); + } + } + + // Force the editor to behave like a unix editor as + // all of our code is assuming that. + const model = editor.getModel(); + if (model) { + model.setEOL(monacoEditor.editor.EndOfLineSequence.LF); + + // Listen to model changes too. + this.subscriptions.push(model?.onDidChangeContent(this.onModelChanged)); + } + + // When testing, eliminate the _assertNotDisposed call. It can break tests if autocomplete + // is still open at the end of a test + // tslint:disable-next-line: no-any + if (isTestExecution() && model && (model as any)._assertNotDisposed) { + // tslint:disable-next-line: no-any + (model as any)._assertNotDisposed = noop; + } + + // Listen for keydown events. We don't do auto scrolling until the user types something + this.subscriptions.push(editor.onKeyDown((_e) => (this.keyHasBeenPressed = true))); + + // Register a link opener so when a user clicks on a link we can navigate to it. + // tslint:disable-next-line: no-any + const openerService = (editor.getContribution('editor.linkDetector') as any).openerService; + if (openerService && openerService.open) { + openerService.open = this.props.openLink; + } + + // Save the editor and the model in our state. + this.setState({ editor, model }); + if (this.props.hasFocus) { + this.giveFocusToEditor(editor, this.props.cursorPos, this.props.options.readOnly); + } + if (this.props.theme) { + monacoEditor.editor.setTheme(this.props.theme); + } + + // do the initial set of the height (wait a bit) + this.windowResized(); + + // on each edit recompute height (wait a bit) + if (model) { + this.subscriptions.push( + model.onDidChangeContent(() => { + this.windowResized(); + if (this.state.editor && this.state.editor.hasWidgetFocus()) { + this.hideAllOtherHoverAndParameterWidgets(); + } + }) + ); + } + + // On layout recompute height + this.subscriptions.push( + editor.onDidLayoutChange(() => { + this.windowResized(); + // Recompute visible line tops + this.debouncedComputeLineTops(); + + // A layout change may be because of a new line + this.throttledScrollCurrentPosition(); + }) + ); + + // Setup our context menu to show up outside. Autocomplete doesn't have this problem so it just works + this.subscriptions.push( + editor.onContextMenu((e) => { + if (this.state.editor) { + const domNode = this.state.editor.getDomNode(); + const contextMenuElement = domNode + ? (domNode.querySelector('.monaco-menu-container') as HTMLElement) + : null; + if (contextMenuElement) { + const posY = + e.event.posy + contextMenuElement.clientHeight > window.outerHeight + ? e.event.posy - contextMenuElement.clientHeight + : e.event.posy; + const posX = + e.event.posx + contextMenuElement.clientWidth > window.outerWidth + ? e.event.posx - contextMenuElement.clientWidth + : e.event.posx; + contextMenuElement.style.position = 'fixed'; + contextMenuElement.style.top = `${Math.max(0, Math.floor(posY))}px`; + contextMenuElement.style.left = `${Math.max(0, Math.floor(posX))}px`; + } + } + }) + ); + + // When editor loses focus, hide parameter widgets (if any currently displayed). + this.subscriptions.push( + editor.onDidBlurEditorWidget(() => { + this.hideParameterWidget(); + }) + ); + + // Track focus changes to make sure we update our widget parent and widget position + this.subscriptions.push( + editor.onDidFocusEditorWidget(() => { + this.throttledUpdateWidgetPosition(); + this.updateWidgetParent(editor); + this.hideAllOtherHoverAndParameterWidgets(); + + // Also update our scroll position, but do that after focus is established. + // This is necessary so that markdown can switch to edit mode before we + // try to scroll to it. + setTimeout(() => this.throttledScrollCurrentPosition(), 0); + }) + ); + + // Track cursor changes and make sure line is on the screen + this.subscriptions.push( + editor.onDidChangeCursorPosition(() => { + this.throttledUpdateWidgetPosition(); + + // Do this after the cursor changes so the text has time to appear + setTimeout(() => this.throttledScrollCurrentPosition(), 0); + }) + ); + + // Update our margin to include the correct line number style + this.updateMargin(editor); + + // If we're readonly, monaco is not putting the aria-readonly property on the textarea + // We should do that + if (this.props.options.readOnly) { + this.setAriaReadOnly(editor); + } + + // Eliminate the find action if possible + // tslint:disable-next-line: no-any + const editorAny = editor as any; + if (editorAny._standaloneKeybindingService) { + editorAny._standaloneKeybindingService.addDynamicKeybinding('-actions.find'); + } + + // Tell our parent the editor is ready to use + this.props.editorMounted(editor); + + if (editor) { + this.subscriptions.push( + editor.onMouseMove(() => { + this.hideAllOtherHoverAndParameterWidgets(); + }) + ); + } + } + }; + + public componentWillUnmount = () => { + if (this.resizeTimer) { + window.clearTimeout(this.resizeTimer); + } + + if (window) { + window.removeEventListener('resize', this.windowResized); + } + if (this.parameterWidget) { + this.parameterWidget.removeEventListener('mouseleave', this.outermostParentLeave); + this.parameterWidget = undefined; + } + if (this.outermostParent) { + this.outermostParent.removeEventListener('mouseleave', this.outermostParentLeave); + this.outermostParent = null; + } + if (this.widgetParent) { + this.widgetParent.remove(); + } + + this.subscriptions.forEach((d) => d.dispose()); + if (this.state.editor) { + this.state.editor.dispose(); + } + + if (this.styleObserver) { + this.styleObserver.disconnect(); + } + }; + + // tslint:disable-next-line: cyclomatic-complexity + public componentDidUpdate(prevProps: IMonacoEditorProps, prevState: IMonacoEditorState) { + if (this.state.editor) { + if (prevProps.language !== this.props.language && this.state.model) { + monacoEditor.editor.setModelLanguage(this.state.model, this.props.language); + } + if (prevProps.theme !== this.props.theme && this.props.theme) { + monacoEditor.editor.setTheme(this.props.theme); + } + if (!fastDeepEqual(prevProps.options, this.props.options)) { + if (prevProps.options.lineNumbers !== this.props.options.lineNumbers) { + this.updateMargin(this.state.editor); + } + this.state.editor.updateOptions(this.props.options); + MonacoEditor.lineHeight = 0; // Font size and family come from theoptions. + } + if ( + prevProps.value !== this.props.value && + this.state.model && + this.state.model.getValue() !== this.props.value + ) { + this.forceValue(this.props.value, this.props.cursorPos); + } else if ( + prevProps.version !== this.props.version && + this.state.model && + this.state.model.getVersionId() < this.props.version + ) { + this.forceValue(this.props.value, this.props.cursorPos); + } + } + + if (this.visibleLineCount === -1) { + this.updateEditorSize(); + } else { + // Debounce the call. This can happen too fast + this.debouncedUpdateEditorSize(); + } + // If this is our first time setting the editor, we might need to dynanically modify the styles + // that the editor generates for the background colors. + if (!prevState.editor && this.state.editor && this.containerRef.current) { + this.updateBackgroundStyle(); + } + if (this.state.editor && !prevProps.hasFocus && this.props.hasFocus) { + this.giveFocusToEditor(this.state.editor, this.props.cursorPos, this.props.options.readOnly); + } + + // Reset key press tracking. + this.keyHasBeenPressed = false; + } + public shouldComponentUpdate( + nextProps: Readonly<IMonacoEditorProps>, + nextState: Readonly<IMonacoEditorState>, + // tslint:disable-next-line: no-any + _nextContext: any + ): boolean { + if (!fastDeepEqual(nextProps, this.props)) { + return true; + } + if (nextState === this.state) { + return false; + } + if (nextState.model?.id !== this.state.model?.id) { + return true; + } + return false; + } + public render() { + const measureWidthClassName = this.props.measureWidthClassName + ? this.props.measureWidthClassName + : 'measure-width-div'; + return ( + <div className="monaco-editor-outer-container" ref={this.containerRef}> + <div className={measureWidthClassName} ref={this.measureWidthRef} /> + </div> + ); + } + + public isSuggesting(): boolean { + // This should mean our widgetParent has some height + if (this.widgetParent && this.widgetParent.firstChild && this.widgetParent.firstChild.childNodes.length >= 2) { + const htmlFirstChild = this.widgetParent.firstChild as HTMLElement; + const suggestWidget = htmlFirstChild.getElementsByClassName('suggest-widget')[0] as HTMLDivElement; + const signatureHelpWidget = htmlFirstChild.getElementsByClassName( + 'parameter-hints-widget' + )[0] as HTMLDivElement; + return this.isElementVisible(suggestWidget) || this.isElementVisible(signatureHelpWidget); + } + return false; + } + + public getCurrentVisibleLine(): number | undefined { + return this.getCurrentVisibleLinePosOrIndex((pos, _i) => pos); + } + + public getVisibleLineCount(): number { + return this.getVisibleLines().length; + } + + public giveFocus(cursorPos: CursorPos | monacoEditor.IPosition) { + if (this.state.editor) { + this.giveFocusToEditor(this.state.editor, cursorPos, this.props.options.readOnly); + } + } + + public getContents(): string { + if (this.state.model) { + // Make sure to remove any carriage returns as they're not expected + // in an ipynb file (and would mess up updates from the file) + return this.state.model.getValue().replace(/\r/g, ''); + } + return ''; + } + + public getVersionId(): number { + return this.state.model ? this.state.model.getVersionId() : 1; + } + + public getPosition(): monacoEditor.Position | null { + return this.state.editor!.getPosition(); + } + + public setValue(text: string, cursorPos: CursorPos) { + if (this.state.model && this.state.editor && this.state.model.getValue() !== text) { + this.forceValue(text, cursorPos, true); + } + } + + /** + * Give focus to the specified editor and clear the property used to track whether to set focus to an editor or not. + */ + private giveFocusToEditor( + editor: monacoEditor.editor.IStandaloneCodeEditor, + cursorPos: CursorPos | monacoEditor.IPosition, + readonly?: boolean + ) { + if (!readonly) { + editor.focus(); + } + if (cursorPos !== CursorPos.Current) { + const current = editor.getPosition(); + const lineNumber = cursorPos === CursorPos.Top ? 1 : editor.getModel()!.getLineCount(); + const column = current && current.lineNumber === lineNumber ? current.column : 1; + editor.setPosition({ lineNumber, column }); + this.scrollToCurrentPosition(); + } + } + + private closeSuggestWidget() { + // tslint:disable-next-line: no-any + const suggest = this.state.editor?.getContribution('editor.contrib.suggestController') as any; + if (suggest && suggest._widget) { + suggest._widget.getValue().hideWidget(); + } + } + + private forceValue(text: string, cursorPos: CursorPos | monacoEditor.IPosition, allowNotifications?: boolean) { + if (this.state.model && this.state.editor) { + // Save current position. May need it to update after setting. + const current = this.state.editor.getPosition(); + + // Disable change notifications if forcing this value should not allow them + this.skipNotifications = allowNotifications ? false : true; + + // Close any suggestions that are open + this.closeSuggestWidget(); + + // Change our text. + this.previousModelValue = text; + this.state.model.setValue(text); + + this.skipNotifications = false; + + // Compute new position + if (typeof cursorPos !== 'object') { + const lineNumber = cursorPos === CursorPos.Top ? 1 : this.state.editor.getModel()!.getLineCount(); + const column = current && current.lineNumber === lineNumber ? current.column : 1; + this.state.editor.setPosition({ lineNumber, column }); + } else { + this.state.editor.setPosition(cursorPos); + } + } + } + + private onModelChanged = (e: monacoEditor.editor.IModelContentChangedEvent) => { + // If not skipping notifications, send an event + if (!this.skipNotifications && this.state.model && this.state.editor) { + this.props.modelChanged(generateChangeEvent(e, this.state.model, this.previousModelValue)); + // Any changes from now onw will be considered previous. + this.previousModelValue = this.getContents(); + } + }; + + private getCurrentVisibleLinePosOrIndex(pickResult: (pos: number, index: number) => number): number | undefined { + // Convert the current cursor into a top and use that to find which visible + // line it is in. + if (this.state.editor) { + const cursor = this.state.editor.getPosition(); + if (cursor) { + const top = this.state.editor.getTopForPosition(cursor.lineNumber, cursor.column); + const count = this.getVisibleLineCount(); + const lineTops = count === this.lineTops.length ? this.lineTops : this.computeLineTops(); + for (let i = 0; i < count; i += 1) { + if (top <= lineTops[i].top) { + return pickResult(i, lineTops[i].index); + } + } + } + } + } + + private getCurrentVisibleLineIndex(): number | undefined { + return this.getCurrentVisibleLinePosOrIndex((_pos, i) => i); + } + + private getVisibleLines(): HTMLDivElement[] { + if (this.state.editor && this.state.model) { + // This is going to use just the dom to compute the visible lines + const editorDom = this.state.editor.getDomNode(); + if (editorDom) { + return Array.from(editorDom.getElementsByClassName('view-line')) as HTMLDivElement[]; + } + } + return []; + } + + private computeLineTops() { + const lines = this.getVisibleLines(); + + // Lines are not sorted by monaco, so we have to sort them by their top value + this.lineTops = lines + .map((l, i) => { + const match = l.style.top ? /(.+)px/.exec(l.style.top) : null; + return { top: match ? parseInt(match[0], 10) : Infinity, index: i }; + }) + .sort((a, b) => a.top - b.top); + return this.lineTops; + } + + private tryToScrollToCurrentPosition() { + // Don't scroll if no key has been pressed + if (!this.keyHasBeenPressed) { + return; + } + this.scrollToCurrentPosition(); + } + + private scrollToCurrentPosition() { + // Unfortunately during functional tests we hack the line count and the like. + if (isTestExecution() || !this.props.hasFocus) { + return; + } + // Scroll to the visible line that has our current line. Note: Visible lines are not sorted by monaco + // so we have to retrieve the current line's index (not its visible position) + const visibleLineDivs = this.getVisibleLines(); + const current = this.getCurrentVisibleLineIndex(); + if (current !== undefined && current >= 0 && visibleLineDivs[current].scrollIntoView) { + visibleLineDivs[current].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); + } + } + + private isElementVisible(element: HTMLElement | undefined): boolean { + if (element && element.clientHeight > 0) { + // See if it has the visibility set on the style + const visibility = element.style.visibility; + return visibility ? visibility !== 'hidden' : true; + } + return false; + } + + private onCommandWillExecute(ev: { commandId: string; args: any[] }) { + if (ev.commandId === 'paste' && ev.args.length > 0 && ev.args[0].text) { + // See if same as last paste and within 100ms. This is the error condition. + const diff = Date.now() - this.lastPasteCommandTime; + if (diff < 100 && ev.args[0].text && ev.args[0].text === this.lastPasteCommandText) { + ev.args[0].text = ''; // Turn into an empty paste to null the operation. + } + this.lastPasteCommandText = ev.args[0].text; + this.lastPasteCommandTime = Date.now(); + } + } + + private setAriaReadOnly(editor: monacoEditor.editor.IStandaloneCodeEditor) { + const editorDomNode = editor.getDomNode(); + if (editorDomNode) { + const textArea = editorDomNode.getElementsByTagName('textarea'); + if (textArea && textArea.length > 0) { + const item = textArea.item(0); + if (item) { + item.setAttribute('aria-readonly', 'true'); + } + } + } + } + + private windowResized = () => { + if (this.resizeTimer) { + clearTimeout(this.resizeTimer); + } + this.resizeTimer = window.setTimeout(this.updateEditorSize.bind(this), 0); + }; + + private startUpdateWidgetPosition = () => { + this.throttledUpdateWidgetPosition(); + }; + + private updateBackgroundStyle = () => { + if (this.state.editor && this.containerRef.current) { + let nodes = this.containerRef.current.getElementsByClassName('monaco-editor-background'); + if (nodes && nodes.length > 0) { + const backgroundNode = nodes[0] as HTMLDivElement; + if (backgroundNode && backgroundNode.style) { + backgroundNode.style.backgroundColor = 'transparent'; + } + } + nodes = this.containerRef.current.getElementsByClassName('monaco-editor'); + if (nodes && nodes.length > 0) { + const editorNode = nodes[0] as HTMLDivElement; + if (editorNode && editorNode.style) { + editorNode.style.backgroundColor = 'transparent'; + } + } + } + }; + + private updateWidgetPosition(width?: number) { + if (this.state.editor && this.widgetParent) { + // Position should be at the top of the editor. + const editorDomNode = this.state.editor.getDomNode(); + if (editorDomNode) { + const rect = editorDomNode.getBoundingClientRect(); + if (rect && (rect.left !== this.lastOffsetLeft || rect.top !== this.lastOffsetTop)) { + this.lastOffsetLeft = rect.left; + this.lastOffsetTop = rect.top; + + this.widgetParent.setAttribute( + 'style', + `position: absolute; left: ${rect.left}px; top: ${rect.top}px; width:${ + width ? width : rect.width + }px` + ); + } + } + } else { + // If widget parent isn't set yet, try again later + this.updateWidgetParent(this.state.editor); + this.throttledUpdateWidgetPosition(width); + } + } + + private computeLineHeight(): number { + if (MonacoEditor.lineHeight <= 0 && this.state.editor) { + const editorDomNode = this.state.editor.getDomNode(); + if (editorDomNode) { + const container = editorDomNode.getElementsByClassName('view-lines')[0] as HTMLElement; + if (container.firstChild && (container.firstChild as HTMLElement).style.height) { + const lineHeightPx = (container.firstChild as HTMLElement).style.height; + MonacoEditor.lineHeight = parseInt(lineHeightPx, 10); + } else { + return LINE_HEIGHT; + } + } + } + return MonacoEditor.lineHeight; + } + + private updateEditorSize() { + if ( + this.measureWidthRef.current && + this.containerRef.current && + this.containerRef.current.parentElement && + this.containerRef.current.parentElement.parentElement && + this.state.editor && + this.state.model + ) { + const editorDomNode = this.state.editor.getDomNode(); + if (!editorDomNode) { + return; + } + const grandParent = this.containerRef.current.parentElement.parentElement; + const container = editorDomNode.getElementsByClassName('view-lines')[0] as HTMLElement; + const currLineCount = Math.max(container.childElementCount, this.state.model.getLineCount()); + const lineHeight = this.computeLineHeight(); + const height = currLineCount * lineHeight + 3; // Fudge factor + const width = this.measureWidthRef.current.clientWidth - grandParent.offsetLeft - 15; // Leave room for the scroll bar in regular cell table + + const layoutInfo = this.state.editor.getLayoutInfo(); + if (layoutInfo.height !== height || layoutInfo.width !== width || currLineCount !== this.visibleLineCount) { + // Make sure to attach to a real dom node. + if (!this.attached && this.state.editor && this.monacoContainer) { + this.containerRef.current.appendChild(this.monacoContainer); + this.monacoContainer.addEventListener('mousemove', this.onContainerMove); + } + this.visibleLineCount = currLineCount; + this.attached = true; + this.state.editor.layout({ width, height }); + + // Once layout completes, scroll to the current position again. It may have disappeared + if (!this.pendingLayoutScroll) { + this.pendingLayoutScroll = true; + setTimeout(() => { + this.scrollToCurrentPosition(); + this.pendingLayoutScroll = false; + }, 0); + } + } + } + } + + private onContainerMove = () => { + if (!this.widgetParent && !this.state.widgetsReparented && this.monacoContainer) { + // Only need to do this once, but update the widget parents and move them. + this.updateWidgetParent(this.state.editor); + this.startUpdateWidgetPosition(); + + // Since only doing this once, remove the listener. + this.monacoContainer.removeEventListener('mousemove', this.onContainerMove); + } + }; + + private onHoverLeave = (e: MouseEvent) => { + // If the hover is active, make sure to hide it. + if (this.state.editor && this.widgetParent) { + this.enteredHover = false; + + // Hide only if not still inside the same editor. Monaco will handle closing otherwise + if (!this.coordsInsideEditor(e.clientX, e.clientY)) { + // tslint:disable-next-line: no-any + const hover = this.state.editor.getContribution('editor.contrib.hover') as any; + if (hover._hideWidgets) { + hover._hideWidgets(); + } + } + } + }; + + private onHoverEnter = () => { + if (this.state.editor && this.widgetParent) { + // If we enter the hover, indicate it so we don't leave + this.enteredHover = true; + } + }; + + // tslint:disable-next-line: no-any + private outermostParentLeave = (e: any) => { + // Have to bounce this because the leave for the cell is the + // enter for the hover + if (this.leaveTimer) { + clearTimeout(this.leaveTimer); + } + this.leaveTimer = window.setTimeout(() => this.outermostParentLeaveBounced(e), 0); + }; + + // tslint:disable-next-line: no-any + private outermostParentLeaveBounced = (e: MouseEvent) => { + if (this.state.editor && !this.enteredHover && !this.coordsInsideEditor(e.clientX, e.clientY)) { + // If we haven't already entered hover, then act like it shuts down + this.onHoverLeave(e); + // Possible user is viewing the parameter hints, wait before user moves the mouse. + // Waiting for 1s is too long to move the mouse and hide the hints (100ms seems like a good fit). + setTimeout(() => this.hideParameterWidget(), 100); + } + }; + + private coordsInsideEditor(x: number, y: number): boolean { + if (this.monacoContainer) { + const clientRect = this.monacoContainer.getBoundingClientRect(); + if (x >= clientRect.left && x <= clientRect.right && y >= clientRect.top && y <= clientRect.bottom) { + return true; + } + } + return false; + } + + /** + * This will hide the parameter widget if the user is not hovering over + * the parameter widget for this monaco editor. + * + * Notes: See issue https://github.com/microsoft/vscode-python/issues/7851 for further info. + * Hide the parameter widget if all of the following conditions have been met: + * - ditor doesn't have focus + * - Mouse is not over the editor + * - Mouse is not over (hovering) the parameter widget + * + * @private + * @returns + * @memberof MonacoEditor + */ + private hideParameterWidget() { + if (!this.state.editor || !this.state.editor.getDomNode() || !this.widgetParent) { + return; + } + // Find all elements that the user is hovering over. + // Its possible the parameter widget is one of them. + const hoverElements: Element[] = Array.prototype.slice.call(document.querySelectorAll(':hover')); + // Find all parameter widgets related to this monaco editor that are currently displayed. + const visibleParameterHintsWidgets: Element[] = Array.prototype.slice.call( + this.widgetParent.querySelectorAll('.parameter-hints-widget.visible') + ); + if (hoverElements.length === 0 && visibleParameterHintsWidgets.length === 0) { + // If user is not hovering over anything and there are no visible parameter widgets, + // then, we have nothing to do but get out of here. + return; + } + + // Find all parameter widgets related to this monaco editor. + const knownParameterHintsWidgets: HTMLDivElement[] = Array.prototype.slice.call( + this.widgetParent.querySelectorAll(WidgetCSSSelector.Parameters) + ); + + // Lets not assume we'll have the exact same DOM for parameter widgets. + // So, just remove the event handler, and add it again later. + if (this.parameterWidget) { + this.parameterWidget.removeEventListener('mouseleave', this.outermostParentLeave); + } + // These are the classes that will appear on a parameter widget when they are visible. + const parameterWidgetClasses = ['editor-widget', 'parameter-hints-widget', 'visible']; + + // Find the parameter widget the user is currently hovering over. + this.parameterWidget = hoverElements.find((item) => { + if (typeof item.className !== 'string') { + return false; + } + // Check if user is hovering over a parameter widget. + const classes = item.className.split(' '); + if (!parameterWidgetClasses.every((cls) => classes.indexOf(cls) >= 0)) { + // Not all classes required in a parameter hint widget are in this element. + // Hence this is not a parameter widget. + return false; + } + + // Ok, this element that the user is hovering over is a parameter widget. + + // Next, check whether this parameter widget belongs to this monaco editor. + // We have a list of parameter widgets that belong to this editor, hence a simple lookup. + return knownParameterHintsWidgets.some((widget) => widget === item); + }); + + if (this.parameterWidget) { + // We know the user is hovering over the parameter widget for this editor. + // Hovering could mean the user is scrolling through a large parameter list. + // We need to add a mouse leave event handler, so as to hide this. + this.parameterWidget.addEventListener('mouseleave', this.outermostParentLeave); + + // In case the event handler doesn't get fired, have a backup of checking within 1s. + setTimeout(() => this.hideParameterWidget(), 1000); + return; + } + if (visibleParameterHintsWidgets.length === 0) { + // There are no parameter widgets displayed for this editor. + // Hence nothing to do. + return; + } + // If the editor has focus, don't hide the parameter widget. + // This is the default behavior. Let the user hit `Escape` or click somewhere + // to forcefully hide the parameter widget. + if (this.state.editor.hasWidgetFocus()) { + return; + } + + // If we got here, then the user is not hovering over the paramter widgets. + // & the editor doesn't have focus. + // However some of the parameter widgets associated with this monaco editor are visible. + // We need to hide them. + + // Solution: Hide the widgets manually. + this.hideWidgets(this.widgetParent, [WidgetCSSSelector.Parameters]); + } + /** + * Hides widgets such as parameters and hover, that belong to a given parent HTML element. + * + * @private + * @param {HTMLDivElement} widgetParent + * @param {string[]} selectors + * @memberof MonacoEditor + */ + private hideWidgets(widgetParent: HTMLDivElement, selectors: string[]) { + for (const selector of selectors) { + for (const widget of Array.from<HTMLDivElement>(widgetParent.querySelectorAll(selector))) { + widget.setAttribute( + 'class', + widget.className + .split(' ') + .filter((cls: string) => cls !== 'visible') + .join(' ') + ); + if (widget.style.visibility !== 'hidden') { + widget.style.visibility = 'hidden'; + } + } + } + } + /** + * Hides the hover and parameters widgets related to other monaco editors. + * Use this to ensure we only display hover/parameters widgets for current editor (by hiding others). + * + * @private + * @returns + * @memberof MonacoEditor + */ + private hideAllOtherHoverAndParameterWidgets() { + const root = document.getElementById('root'); + if (!root || !this.widgetParent) { + return; + } + const widgetParents: HTMLDivElement[] = Array.prototype.slice.call( + root.querySelectorAll('div.monaco-editor-pretend-parent') + ); + widgetParents + .filter((widgetParent) => widgetParent !== this.widgetParent) + .forEach((widgetParent) => + this.hideWidgets(widgetParent, [WidgetCSSSelector.Parameters, WidgetCSSSelector.Hover]) + ); + } + private updateMargin(editor: monacoEditor.editor.IStandaloneCodeEditor) { + const editorNode = editor.getDomNode(); + if (editorNode) { + try { + const elements = editorNode.getElementsByClassName('margin-view-overlays'); + if (elements && elements.length) { + const margin = elements[0] as HTMLDivElement; + + // Create special class name based on the line number property + const specialExtra = `margin-view-overlays-border-${this.props.options.lineNumbers}`; + if (margin.className && !margin.className.includes(specialExtra)) { + margin.className = `margin-view-overlays ${specialExtra}`; + } + + // Watch the scrollable element (it's where the code lines up) + const scrollable = editorNode.getElementsByClassName('monaco-scrollable-element'); + if (!this.watchingMargin && scrollable && scrollable.length && this.styleObserver) { + const watching = scrollable[0] as HTMLDivElement; + this.watchingMargin = true; + this.styleObserver.observe(watching, { attributes: true, attributeFilter: ['style'] }); + } + } + } catch { + // Ignore if we can't get modify the margin class + } + } + } + + private updateWidgetParent(editor: monacoEditor.editor.IStandaloneCodeEditor | undefined) { + // Reparent the hover widgets. They cannot be inside anything that has overflow hidden or scrolling or they won't show + // up overtop of anything. Warning, this is a big hack. If the class name changes or the logic + // for figuring out the position of hover widgets changes, this won't work anymore. + // appendChild on a DOM node moves it, but doesn't clone it. + // https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild + const editorNode = editor ? editor.getDomNode() : undefined; + if (editor && editorNode && !this.state.widgetsReparented) { + this.setState({ widgetsReparented: true }); + try { + const elements = editorNode.getElementsByClassName('overflowingContentWidgets'); + if (elements && elements.length) { + const contentWidgets = elements[0] as HTMLDivElement; + if (contentWidgets) { + // Go up to the document. + const document = contentWidgets.getRootNode() as HTMLDocument; + + // His first child with the id 'root' should be where we want to parent our overflow widgets + if (document && document.getElementById) { + const root = document.getElementById('root'); + if (root) { + // We need to create a dummy 'monaco-editor' div so that the content widgets get the same styles. + this.widgetParent = document.createElement('div', {}); + this.widgetParent.setAttribute( + 'class', + `${editorNode.className} monaco-editor-pretend-parent` + ); + + root.appendChild(this.widgetParent); + this.widgetParent.appendChild(contentWidgets); + + // Listen for changes so we can update the position dynamically + editorNode.addEventListener('mouseenter', this.startUpdateWidgetPosition); + + // We also need to trick the editor into thinking mousing over the hover does not + // mean the mouse has left the editor. + // tslint:disable-next-line: no-any + const hover = editor.getContribution('editor.contrib.hover') as any; + if ( + hover._toUnhook && + hover._toUnhook._toDispose && + hover._toUnhook._toDispose && + hover.contentWidget + ) { + // This should mean our 5th element is the event handler for mouse leave. Remove it. + const set = hover._toUnhook._toDispose as Set<IDisposable>; + if (set.size === HOVER_DISPOSABLE_EVENT_COUNT) { + // This is horribly flakey, Is set always guaranteed to put stuff in the same order? + const array = Array.from(set); + array[HOVER_DISPOSABLE_LEAVE_INDEX].dispose(); + set.delete(array[HOVER_DISPOSABLE_LEAVE_INDEX]); + + // Instead listen to mouse leave for our hover widget + const hoverWidget = this.widgetParent.getElementsByClassName( + 'monaco-editor-hover' + )[0] as HTMLElement; + if (hoverWidget) { + hoverWidget.addEventListener('mouseenter', this.onHoverEnter); + hoverWidget.addEventListener('mouseleave', this.onHoverLeave); + } + } + } + } + } + } + } + } catch (e) { + // If something fails, then the hover will just work inside the main frame + if (!this.props.testMode) { + logMessage(`Error moving editor widgets: ${e}`); + } + + // Make sure we don't try moving it around. + this.widgetParent = undefined; + } + } + } +} diff --git a/src/datascience-ui/react-common/monacoHelpers.ts b/src/datascience-ui/react-common/monacoHelpers.ts new file mode 100644 index 000000000000..0258d9c86fea --- /dev/null +++ b/src/datascience-ui/react-common/monacoHelpers.ts @@ -0,0 +1,113 @@ +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { + IEditorContentChange, + IEditorPosition, + IEditorRange +} from '../../client/datascience/interactive-common/interactiveWindowTypes'; + +export interface IMonacoTextModel { + readonly id: string; + getValue(): string; + getValueLength(): number; + getVersionId(): number; + getPositionAt(offset: number): IEditorPosition; +} + +export interface IMonacoModelContentChangeEvent { + // Changes to apply + readonly forward: IEditorContentChange[]; + // Change to undo the apply + readonly reverse: IEditorContentChange[]; + readonly eol: string; + readonly versionId: number; + readonly isUndoing: boolean; + readonly isRedoing: boolean; + readonly isFlush: boolean; + readonly model: IMonacoTextModel; +} + +function getValueInRange(text: string, r: IEditorRange): string { + // Compute start and end offset using line and column data + let startOffset = -1; + let endOffset = -1; + let line = 1; + let col = 1; + + // Go forwards through the text searching for matching lines + for (let pos = 0; pos <= text.length && (startOffset < 0 || endOffset < 0); pos += 1) { + if (line === r.startLineNumber && col === r.startColumn) { + startOffset = pos; + } else if (line === r.endLineNumber && col === r.endColumn) { + endOffset = pos; + } + if (pos < text.length) { + if (text[pos] === '\n') { + line += 1; + col = 1; + } else { + col += 1; + } + } + } + + if (startOffset >= 0 && endOffset >= 0) { + return text.slice(startOffset, endOffset); + } + + return ''; +} + +export function generateReverseChange( + oldModelValue: string, + model: IMonacoTextModel, + c: monacoEditor.editor.IModelContentChange +): monacoEditor.editor.IModelContentChange { + const oldStart = model.getPositionAt(c.rangeOffset); + const oldEnd = model.getPositionAt(c.rangeOffset + c.rangeLength); + const oldText = getValueInRange(oldModelValue, c.range); + const oldRange: monacoEditor.IRange = { + startColumn: oldStart.column, + startLineNumber: oldStart.lineNumber, + endColumn: oldEnd.column, + endLineNumber: oldEnd.lineNumber + }; + return { + rangeLength: c.text.length, + rangeOffset: c.rangeOffset, + text: oldText ? oldText : '', + range: oldRange + }; +} + +export function generateChangeEvent( + ev: monacoEditor.editor.IModelContentChangedEvent, + m: IMonacoTextModel, + oldText: string +): IMonacoModelContentChangeEvent { + // Figure out the end position from the offset plus the length of the text we added + const currentOffset = ev.changes[ev.changes.length - 1].rangeOffset + ev.changes[ev.changes.length - 1].text.length; + const currentPosition = m.getPositionAt(currentOffset); + + // Create the reverse changes + const reverseChanges = ev.changes.map(generateReverseChange.bind(undefined, oldText, m)).reverse(); + + // Figure out the old position by using the first offset + const oldOffset = ev.changes[0].rangeOffset; + const oldPosition = m.getPositionAt(oldOffset); + + // Combine position and change to create result + return { + forward: ev.changes.map((c) => { + return { ...c, position: currentPosition! }; + }), + reverse: reverseChanges.map((r) => { + return { ...r, position: oldPosition! }; + }), + eol: ev.eol, + isFlush: ev.isFlush, + isUndoing: ev.isUndoing, + isRedoing: ev.isRedoing, + versionId: m.getVersionId(), + model: m + }; +} diff --git a/src/datascience-ui/react-common/postOffice.ts b/src/datascience-ui/react-common/postOffice.ts new file mode 100644 index 000000000000..8e671b2376e1 --- /dev/null +++ b/src/datascience-ui/react-common/postOffice.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { WebviewMessage } from '../../client/common/application/types'; +import { IDisposable } from '../../client/common/types'; +import { logMessage } from './logger'; + +export interface IVsCodeApi { + // tslint:disable-next-line:no-any + postMessage(msg: any): void; + // tslint:disable-next-line:no-any + setState(state: any): void; + // tslint:disable-next-line:no-any + getState(): any; +} + +export interface IMessageHandler { + // tslint:disable-next-line:no-any + handleMessage(type: string, payload?: any): boolean; + dispose?(): void; +} + +// This special function talks to vscode from a web panel +export declare function acquireVsCodeApi(): IVsCodeApi; +// tslint:disable-next-line: no-any +export type PostOfficeMessage = { type: string; payload?: any }; +// tslint:disable-next-line: no-unnecessary-class +export class PostOffice implements IDisposable { + private registered: boolean = false; + private vscodeApi: IVsCodeApi | undefined; + private handlers: IMessageHandler[] = []; + private baseHandler = this.handleMessages.bind(this); + private readonly subject = new Subject<PostOfficeMessage>(); + private readonly observable: Observable<PostOfficeMessage>; + constructor() { + this.observable = this.subject.asObservable(); + } + public asObservable(): Observable<PostOfficeMessage> { + return this.observable; + } + public dispose() { + if (this.registered) { + this.registered = false; + window.removeEventListener('message', this.baseHandler); + } + } + + public sendMessage<M, T extends keyof M = keyof M>(type: T, payload?: M[T]) { + return this.sendUnsafeMessage(type.toString(), payload); + } + + // tslint:disable-next-line:no-any + public sendUnsafeMessage(type: string, payload?: any) { + const api = this.acquireApi(); + if (api) { + api.postMessage({ type: type, payload }); + } else { + logMessage(`No vscode API to post message ${type}`); + } + } + + public addHandler(handler: IMessageHandler) { + // Acquire here too so that the message handlers are setup during tests. + this.acquireApi(); + this.handlers.push(handler); + } + + public removeHandler(handler: IMessageHandler) { + this.handlers = this.handlers.filter((f) => f !== handler); + } + + private acquireApi(): IVsCodeApi | undefined { + // Only do this once as it crashes if we ask more than once + // tslint:disable-next-line:no-typeof-undefined + if (!this.vscodeApi && typeof acquireVsCodeApi !== 'undefined') { + this.vscodeApi = acquireVsCodeApi(); // NOSONAR + } + if (!this.registered) { + this.registered = true; + window.addEventListener('message', this.baseHandler); + + try { + // For testing, we might use a browser to load the stuff. + // In such instances the `acquireVSCodeApi` will return the event handler to get messages from extension. + // See ./src/datascience-ui/native-editor/index.html + // tslint:disable-next-line: no-any + const api = (this.vscodeApi as any) as { handleMessage?: Function }; + if (api.handleMessage) { + api.handleMessage(this.handleMessages.bind(this)); + } + } catch { + // Ignore. + } + } + + return this.vscodeApi; + } + + private async handleMessages(ev: MessageEvent) { + if (this.handlers) { + const msg = ev.data as WebviewMessage; + if (msg) { + this.subject.next({ type: msg.type, payload: msg.payload }); + this.handlers.forEach((h: IMessageHandler | null) => { + if (h) { + h.handleMessage(msg.type, msg.payload); + } + }); + } + } + } +} diff --git a/src/datascience-ui/react-common/progress.css b/src/datascience-ui/react-common/progress.css new file mode 100644 index 000000000000..c5802e7ee863 --- /dev/null +++ b/src/datascience-ui/react-common/progress.css @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + .monaco-progress-container { + width: 100%; + height: 5px; + overflow: hidden; /* keep progress bit in bounds */ + position: fixed; + z-index: 10; +} + +.monaco-progress-container .progress-bit { + width: 2%; + height: 5px; + position: absolute; + left: 0; + display: none; + background-color:var(--vscode-editorSuggestWidget-highlightForeground); +} + +.monaco-progress-container.active .progress-bit { + display: inherit; +} + +.monaco-progress-container.discrete .progress-bit { + left: 0; + transition: width 100ms linear; + -webkit-transition: width 100ms linear; + -o-transition: width 100ms linear; + -moz-transition: width 100ms linear; + -ms-transition: width 100ms linear; +} + +.monaco-progress-container.discrete.done .progress-bit { + width: 100%; +} + +.monaco-progress-container.infinite .progress-bit { + animation-name: progress; + animation-duration: 4s; + animation-iteration-count: infinite; + animation-timing-function: linear; + -ms-animation-name: progress; + -ms-animation-duration: 4s; + -ms-animation-iteration-count: infinite; + -ms-animation-timing-function: linear; + -webkit-animation-name: progress; + -webkit-animation-duration: 4s; + -webkit-animation-iteration-count: infinite; + -webkit-animation-timing-function: linear; + -moz-animation-name: progress; + -moz-animation-duration: 4s; + -moz-animation-iteration-count: infinite; + -moz-animation-timing-function: linear; + will-change: transform; +} + +/** + * The progress bit has a width: 2% (1/50) of the parent container. The animation moves it from 0% to 100% of + * that container. Since translateX is relative to the progress bit size, we have to multiple it with + * its relative size to the parent container: + * 50%: 50 * 50 = 2500% + * 100%: 50 * 100 - 50 (do not overflow): 4950% + */ +@keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4950%) scaleX(1) } } +@-ms-keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4950%) scaleX(1) } } +@-webkit-keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4950%) scaleX(1) } } +@-moz-keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4950%) scaleX(1) } } diff --git a/src/datascience-ui/react-common/progress.tsx b/src/datascience-ui/react-common/progress.tsx new file mode 100644 index 000000000000..4bbf7981f108 --- /dev/null +++ b/src/datascience-ui/react-common/progress.tsx @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import './progress.css'; + +import * as React from 'react'; + +export class Progress extends React.Component { + constructor(props: {}) { + super(props); + } + + public render() { + // Vscode does this with two parts, a progress container and a progress bit + return ( + <div className="monaco-progress-container active infinite"> + <div className="progress-bit" /> + </div> + ); + } +} diff --git a/src/datascience-ui/react-common/reduxUtils.ts b/src/datascience-ui/react-common/reduxUtils.ts new file mode 100644 index 000000000000..b651517aff8a --- /dev/null +++ b/src/datascience-ui/react-common/reduxUtils.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Action, AnyAction, Middleware, Reducer } from 'redux'; +import { BaseReduxActionPayload } from '../../client/datascience/interactive-common/types'; + +// tslint:disable-next-line: interface-name +interface TypedAnyAction<T> extends Action<T> { + // Allows any extra properties to be defined in an action. + // tslint:disable-next-line: no-any + [extraProps: string]: any; +} +export type QueueAnotherFunc<T> = (nextAction: Action<T>) => void; +export type QueuableAction<M> = TypedAnyAction<keyof M> & { queueAction: QueueAnotherFunc<keyof M> }; +export type ReducerArg<S, AT = AnyAction, T = BaseReduxActionPayload> = T extends never | undefined + ? { + prevState: S; + queueAction: QueueAnotherFunc<AT>; + payload: BaseReduxActionPayload; + } + : { + prevState: S; + queueAction: QueueAnotherFunc<AT>; + payload: T; + }; + +export type ReducerFunc<S, AT, T> = (args: ReducerArg<S, AT, T>) => S; +export type ActionWithPayload<T, K> = TypedAnyAction<K> & { payload: BaseReduxActionPayload<T> }; +export type ActionWithOutPayloadData<K> = TypedAnyAction<K> & { payload: BaseReduxActionPayload }; + +/** + * CombineReducers takes in a map of action.type to func and creates a reducer that will call the appropriate function for + * each action + * @param defaultState - original state to use for the store + * @param postMessage - function passed in to use to post messages back to the extension + * @param map - map of action type to func to call + */ +export function combineReducers<S, M>(defaultState: S, map: M): Reducer<S, QueuableAction<M>> { + return (currentState: S = defaultState, action: QueuableAction<M>) => { + const func = map[action.type]; + if (typeof func === 'function') { + // Call the reducer, giving it + // - current state + // - function to potentially post stuff to the other side + // - queue function to dispatch again + // - payload containing the data from the action + return func({ prevState: currentState, queueAction: action.queueAction, payload: action.payload }); + } else { + return currentState; + } + }; +} + +// This middleware allows a reducer to dispatch another action after the reducer +// has returned state (it queues up the dispatch). +// +// Got this idea from here: +// https://stackoverflow.com/questions/36730793/can-i-dispatch-an-action-in-reducer +// +// Careful when using the queueAction though. Don't store it past the point of a reducer as +// the local state inside of this middleware function will be wrong. +export function createQueueableActionMiddleware(): Middleware { + return (store) => (next) => (action) => { + let pendingActions: Action[] = []; + let complete = false; + + function flush() { + pendingActions.forEach((a) => store.dispatch(a)); + pendingActions = []; + } + + function queueAction(nextAction: AnyAction) { + pendingActions.push(nextAction); + + // If already done, run the pending actions (this means + // this was pushed async) + if (complete) { + flush(); + } + } + + // Add queue to the action + const modifiedAction = { ...action, queueAction }; + + // Call the next item in the middle ware chain + const res = next(modifiedAction); + + // When done, run all the queued actions + complete = true; + flush(); + + return res; + }; +} diff --git a/src/datascience-ui/react-common/relativeImage.tsx b/src/datascience-ui/react-common/relativeImage.tsx new file mode 100644 index 000000000000..9c67995b7c51 --- /dev/null +++ b/src/datascience-ui/react-common/relativeImage.tsx @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import * as path from 'path'; +import * as React from 'react'; + +// This special function finds relative paths when loading inside of vscode. It's not defined +// when loading outside, so the Image component should still work. +export declare function resolvePath(relativePath: string): string; + +interface IRelativeImageProps { + class: string; + path: string; +} + +export class RelativeImage extends React.Component<IRelativeImageProps> { + constructor(props: IRelativeImageProps) { + super(props); + } + + public render() { + return <img src={this.getImageSource()} className={this.props.class} alt={path.basename(this.props.path)} />; + } + + private getImageSource = () => { + // tslint:disable-next-line:no-typeof-undefined + if (typeof resolvePath === 'undefined') { + return this.props.path; + } else { + return resolvePath(this.props.path); + } + }; +} diff --git a/src/datascience-ui/react-common/settingsReactSide.ts b/src/datascience-ui/react-common/settingsReactSide.ts new file mode 100644 index 000000000000..04d8b78f26b3 --- /dev/null +++ b/src/datascience-ui/react-common/settingsReactSide.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; + +import { IDataScienceExtraSettings } from '../../client/datascience/types'; + +export function getDefaultSettings() { + // Default settings for tests + // tslint:disable-next-line: no-unnecessary-local-variable + const result: IDataScienceExtraSettings = { + allowImportFromNotebook: true, + alwaysTrustNotebooks: true, + jupyterLaunchTimeout: 10, + jupyterLaunchRetries: 3, + enabled: true, + jupyterServerURI: 'local', + // tslint:disable-next-line: no-invalid-template-strings + notebookFileRoot: '${fileDirname}', + changeDirOnImportExport: false, + useDefaultConfigForJupyter: true, + jupyterInterruptTimeout: 10000, + searchForJupyter: true, + allowInput: true, + showCellInputCode: true, + collapseCellInputCodeByDefault: true, + maxOutputSize: 400, + enableScrollingForCellOutputs: true, + errorBackgroundColor: '#FFFFFF', + sendSelectionToInteractiveWindow: false, + markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', + codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', + variableExplorerExclude: 'module;function;builtin_function_or_method', + enablePlotViewer: true, + interactiveWindowMode: 'multiple', + extraSettings: { + editor: { + cursor: 'line', + cursorBlink: 'blink', + autoClosingBrackets: 'languageDefined', + autoClosingQuotes: 'languageDefined', + autoSurround: 'languageDefined', + autoIndent: false, + fontLigatures: false, + scrollBeyondLastLine: true, + // VS Code puts a value for this, but it's 10 (the explorer bar size) not 14 the editor size for vert + verticalScrollbarSize: 14, + horizontalScrollbarSize: 14, + fontSize: 14, + fontFamily: "Consolas, 'Courier New', monospace" + }, + theme: 'Default Dark+', + useCustomEditorApi: false + }, + intellisenseOptions: { + quickSuggestions: { + other: true, + comments: false, + strings: false + }, + acceptSuggestionOnEnter: 'on', + quickSuggestionsDelay: 10, + suggestOnTriggerCharacters: true, + tabCompletion: 'on', + suggestLocalityBonus: true, + suggestSelection: 'recentlyUsed', + wordBasedSuggestions: true, + parameterHintsEnabled: true + }, + variableOptions: { + enableDuringDebugger: false + }, + webviewExperiments: { + removeKernelToolbarInInteractiveWindow: false + }, + gatherIsInstalled: false, + runStartupCommands: '', + debugJustMyCode: true, + variableQueries: [], + jupyterCommandLineArguments: [], + widgetScriptSources: [] + }; + + return result; +} + +//tslint:disable:no-any +export function computeEditorOptions(settings: IDataScienceExtraSettings): monacoEditor.editor.IEditorOptions { + const intellisenseOptions = settings.intellisenseOptions; + const extraSettings = settings.extraSettings; + if (intellisenseOptions && extraSettings) { + return { + quickSuggestions: { + other: intellisenseOptions.quickSuggestions.other, + comments: intellisenseOptions.quickSuggestions.comments, + strings: intellisenseOptions.quickSuggestions.strings + }, + acceptSuggestionOnEnter: intellisenseOptions.acceptSuggestionOnEnter, + quickSuggestionsDelay: intellisenseOptions.quickSuggestionsDelay, + suggestOnTriggerCharacters: intellisenseOptions.suggestOnTriggerCharacters, + tabCompletion: intellisenseOptions.tabCompletion, + suggest: { + localityBonus: intellisenseOptions.suggestLocalityBonus + }, + suggestSelection: intellisenseOptions.suggestSelection, + wordBasedSuggestions: intellisenseOptions.wordBasedSuggestions, + parameterHints: { + enabled: intellisenseOptions.parameterHintsEnabled + }, + cursorStyle: extraSettings.editor.cursor, + cursorBlinking: extraSettings.editor.cursorBlink, + autoClosingBrackets: extraSettings.editor.autoClosingBrackets as any, + autoClosingQuotes: extraSettings.editor.autoClosingQuotes as any, + autoIndent: extraSettings.editor.autoIndent as any, + autoSurround: extraSettings.editor.autoSurround as any, + fontLigatures: extraSettings.editor.fontLigatures + }; + } + + return {}; +} diff --git a/src/datascience-ui/react-common/styleInjector.tsx b/src/datascience-ui/react-common/styleInjector.tsx new file mode 100644 index 000000000000..4ff54f1786e5 --- /dev/null +++ b/src/datascience-ui/react-common/styleInjector.tsx @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as React from 'react'; + +import { CssMessages, IGetCssResponse, SharedMessages } from '../../client/datascience/messages'; +import { IDataScienceExtraSettings } from '../../client/datascience/types'; +import { IMessageHandler, PostOffice } from './postOffice'; +import { detectBaseTheme } from './themeDetector'; + +export interface IStyleInjectorProps { + expectingDark: boolean; + postOffice: PostOffice; + settings: IDataScienceExtraSettings; + darkChanged?(newDark: boolean): void; + onReady?(): void; +} + +interface IStyleInjectorState { + rootCss?: string; + theme?: string; + knownDark?: boolean; +} + +export class StyleInjector extends React.Component<IStyleInjectorProps, IStyleInjectorState> + implements IMessageHandler { + constructor(props: IStyleInjectorProps) { + super(props); + this.state = { rootCss: undefined, theme: undefined }; + } + + public componentWillMount() { + // Add ourselves as a handler for the post office + this.props.postOffice.addHandler(this); + } + + public componentWillUnmount() { + // Remove ourselves as a handler for the post office + this.props.postOffice.removeHandler(this); + } + + public componentDidMount() { + if (!this.state.rootCss) { + // Set to a temporary value. + this.setState({ rootCss: ' ' }); + this.props.postOffice.sendUnsafeMessage(CssMessages.GetCssRequest, { isDark: this.props.expectingDark }); + } + } + + public render() { + return ( + <div className="styleSetter"> + <style>{this.state.rootCss}</style> + {this.props.children} + </div> + ); + } + + // tslint:disable-next-line:no-any + public handleMessage = (msg: string, payload?: any): boolean => { + switch (msg) { + case CssMessages.GetCssResponse: + this.handleCssResponse(payload); + break; + + case SharedMessages.UpdateSettings: + this.updateSettings(payload); + break; + + default: + break; + } + + return true; + }; + + // tslint:disable-next-line:no-any + private handleCssResponse(payload?: any) { + const response = payload as IGetCssResponse; + if (response && response.css) { + // Recompute our known dark value from the class name in the body + // VS code should update this dynamically when the theme changes + const computedKnownDark = this.computeKnownDark(); + + // We also get this in our response, but computing is more reliable + // than searching for it. + + if (this.state.knownDark !== computedKnownDark && this.props.darkChanged) { + this.props.darkChanged(computedKnownDark); + } + + this.setState( + { + rootCss: response.css, + theme: response.theme, + knownDark: computedKnownDark + }, + this.props.onReady + ); + } + } + + // tslint:disable-next-line:no-any + private updateSettings(payload: any) { + if (payload) { + const newSettings = JSON.parse(payload as string); + const dsSettings = newSettings as IDataScienceExtraSettings; + if (dsSettings && dsSettings.extraSettings && dsSettings.extraSettings.theme !== this.state.theme) { + // User changed the current theme. Rerender + this.props.postOffice.sendUnsafeMessage(CssMessages.GetCssRequest, { isDark: this.computeKnownDark() }); + } + } + } + + private computeKnownDark(): boolean { + const ignore = this.props.settings.ignoreVscodeTheme ? true : false; + const baseTheme = ignore ? 'vscode-light' : detectBaseTheme(); + return baseTheme !== 'vscode-light'; + } +} diff --git a/src/datascience-ui/react-common/svgList.css b/src/datascience-ui/react-common/svgList.css new file mode 100644 index 000000000000..6c7d9e34ba39 --- /dev/null +++ b/src/datascience-ui/react-common/svgList.css @@ -0,0 +1,36 @@ +.svg-list-container { + width: 100%; + height: 100px; + margin: 10px; +} + +.svg-list { + list-style-type: none; + overflow-x: scroll; + overflow-y: hidden; + white-space: nowrap; + padding-inline-start: 0px; +} + +.svg-list-item { + width: 100px; + height: 100%; + border: 1px solid transparent; + display: inline-block; + overflow: hidden; +} + +.svg-list-item-selected { + border-color: var(--vscode-list-highlightForeground); + border-style: solid; + border-width: 2px; +} + +.svg-list-white-background { + background: white; +} + +.svg-list-item-image { + width: 100px; + height: 100px; +} \ No newline at end of file diff --git a/src/datascience-ui/react-common/svgList.tsx b/src/datascience-ui/react-common/svgList.tsx new file mode 100644 index 000000000000..b3e75028d71c --- /dev/null +++ b/src/datascience-ui/react-common/svgList.tsx @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as React from 'react'; +import { SvgLoader } from 'react-svgmt'; +import { getLocString } from '../react-common/locReactSide'; + +import './svgList.css'; + +interface ISvgListProps { + images: string[]; + currentImage: number; + themeMatplotlibBackground: boolean; + imageClicked(index: number): void; +} + +export class SvgList extends React.Component<ISvgListProps> { + constructor(props: ISvgListProps) { + super(props); + } + + public render() { + return ( + <div className="svg-list-container"> + <div className="svg-list">{this.renderImages()}</div> + </div> + ); + } + + private renderImages() { + return this.props.images.map((image, index) => { + const className = `svg-list-item${this.props.currentImage === index ? ' svg-list-item-selected' : ''}${ + this.props.themeMatplotlibBackground ? '' : ' svg-list-white-background' + }`; + const ariaLabel = + index === this.props.currentImage + ? getLocString('DataScience.selectedImageListLabel', 'Selected Image') + : getLocString('DataScience.selectedImageLabel', 'Image'); + const ariaPressed = index === this.props.currentImage ? 'true' : 'false'; + const clickHandler = () => this.props.imageClicked(index); + const keyDownHandler = (e: React.KeyboardEvent<HTMLDivElement>) => this.onKeyDown(e, index); + return ( + <div + className={className} + tabIndex={0} + role="button" + aria-label={ariaLabel} + aria-pressed={ariaPressed} + // See the comments here: https://github.com/Microsoft/tslint-microsoft-contrib/issues/676 + // tslint:disable-next-line: react-this-binding-issue + onClick={clickHandler} + // See the comments here: https://github.com/Microsoft/tslint-microsoft-contrib/issues/676 + // tslint:disable-next-line: react-this-binding-issue + onKeyDown={keyDownHandler} + key={index} + > + <div className="svg-list-item-image"> + <SvgLoader svgXML={image}></SvgLoader> + </div> + </div> + ); + }); + } + + private onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>, index: number) => { + // Enter and Space commit an action the same as a click does + if (event.key === 'Enter' || event.key === ' ') { + this.props.imageClicked(index); + } + }; +} diff --git a/src/datascience-ui/react-common/svgViewer.css b/src/datascience-ui/react-common/svgViewer.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/datascience-ui/react-common/svgViewer.tsx b/src/datascience-ui/react-common/svgViewer.tsx new file mode 100644 index 000000000000..e477eca355fa --- /dev/null +++ b/src/datascience-ui/react-common/svgViewer.tsx @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as React from 'react'; +import { POSITION_TOP, ReactSVGPanZoom, Tool, Value } from 'react-svg-pan-zoom'; +import { SvgLoader } from 'react-svgmt'; +import { AutoSizer } from 'react-virtualized'; +import './svgViewer.css'; + +interface ISvgViewerProps { + svg: string; + id: string; // Unique identified for this svg (in case they are the same) + baseTheme: string; + themeMatplotlibPlots: boolean; + size: { width: string; height: string }; + defaultValue: Value | undefined; + tool: Tool; + changeValue(value: Value): void; +} + +interface ISvgViewerState { + value: Value; + tool: Tool; +} + +export class SvgViewer extends React.Component<ISvgViewerProps, ISvgViewerState> { + private svgPanZoomRef: React.RefObject<ReactSVGPanZoom> = React.createRef<ReactSVGPanZoom>(); + constructor(props: ISvgViewerProps) { + super(props); + // tslint:disable-next-line: no-object-literal-type-assertion + this.state = { value: props.defaultValue ? props.defaultValue : ({} as Value), tool: props.tool }; + } + + public componentDidUpdate(prevProps: ISvgViewerProps) { + // May need to update state if props changed + if (prevProps.defaultValue !== this.props.defaultValue || this.props.id !== prevProps.id) { + this.setState({ + // tslint:disable-next-line: no-object-literal-type-assertion + value: this.props.defaultValue ? this.props.defaultValue : ({} as Value), + tool: this.props.tool + }); + } else if (this.props.tool !== this.state.tool) { + this.setState({ tool: this.props.tool }); + } + } + + public move(offsetX: number, offsetY: number) { + if (this.svgPanZoomRef && this.svgPanZoomRef.current) { + this.svgPanZoomRef.current.pan(offsetX, offsetY); + } + } + + public zoom(amount: number) { + if (this.svgPanZoomRef && this.svgPanZoomRef.current) { + this.svgPanZoomRef.current.zoomOnViewerCenter(amount); + } + } + + public render() { + const plotBackground = this.props.themeMatplotlibPlots + ? 'var(--override-widget-background, var(--vscode-notifications-background))' + : 'white'; + return ( + <AutoSizer> + {({ height, width }) => + width === 0 || height === 0 ? null : ( + <ReactSVGPanZoom + ref={this.svgPanZoomRef} + width={width} + height={height} + toolbarProps={{ position: POSITION_TOP }} + detectAutoPan={true} + tool={this.state.tool} + value={this.state.value} + onChangeTool={this.changeTool} + onChangeValue={this.changeValue} + customToolbar={this.renderToolbar} + customMiniature={this.renderMiniature} + SVGBackground={'transparent'} + background={plotBackground} + detectWheel={true} + > + <svg width={this.props.size.width} height={this.props.size.height}> + <SvgLoader svgXML={this.props.svg} /> + </svg> + </ReactSVGPanZoom> + ) + } + </AutoSizer> + ); + } + + private changeTool = (tool: Tool) => { + this.setState({ tool }); + }; + + private changeValue = (value: Value) => { + this.setState({ value }); + this.props.changeValue(value); + }; + + private renderToolbar = () => { + // Hide toolbar too + return <div />; + }; + + private renderMiniature = () => { + return ( + <div /> // Hide miniature + ); + }; +} diff --git a/src/datascience-ui/react-common/textMeasure.ts b/src/datascience-ui/react-common/textMeasure.ts new file mode 100644 index 000000000000..8e75f75b5da7 --- /dev/null +++ b/src/datascience-ui/react-common/textMeasure.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +let canvas: HTMLCanvasElement | undefined; + +function getCanvas(): HTMLCanvasElement { + if (!canvas) { + canvas = document.createElement('canvas'); + } + return canvas; +} + +export function measureText(text: string, font: string | null): number { + const context = getCanvas().getContext('2d'); + if (context) { + if (font) { + context.font = font; + } + const metrics = context.measureText(text); + return metrics.width; + } + return 0; +} diff --git a/src/datascience-ui/react-common/themeDetector.ts b/src/datascience-ui/react-common/themeDetector.ts new file mode 100644 index 000000000000..b217a7334f22 --- /dev/null +++ b/src/datascience-ui/react-common/themeDetector.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// From here: +// https://stackoverflow.com/questions/37257911/detect-light-dark-theme-programatically-in-visual-studio-code +// Detect vscode-light, vscode-dark, and vscode-high-contrast class name on the body element. +export function detectBaseTheme(): 'vscode-light' | 'vscode-dark' | 'vscode-high-contrast' { + const body = document.body; + if (body) { + switch (body.className) { + default: + case 'vscode-light': + return 'vscode-light'; + case 'vscode-dark': + return 'vscode-dark'; + case 'vscode-high-contrast': + return 'vscode-high-contrast'; + } + } + + return 'vscode-light'; +} diff --git a/src/datascience-ui/renderers/constants.ts b/src/datascience-ui/renderers/constants.ts new file mode 100644 index 000000000000..bc14f44742b4 --- /dev/null +++ b/src/datascience-ui/renderers/constants.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export const JupyterNotebookRenderer = 'jupyter-notebook-renderer'; diff --git a/src/datascience-ui/renderers/index.tsx b/src/datascience-ui/renderers/index.tsx new file mode 100644 index 000000000000..6d574cb1ee77 --- /dev/null +++ b/src/datascience-ui/renderers/index.tsx @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// This must be on top, do not change. Required by webpack. +declare let __webpack_public_path__: string; +const getPublicPath = () => { + const currentDirname = (document.currentScript as HTMLScriptElement).src.replace(/[^/]+$/, ''); + return new URL(currentDirname).toString(); +}; + +__webpack_public_path__ = getPublicPath(); +// This must be on top, do not change. Required by webpack. + +import type { nbformat } from '@jupyterlab/coreutils'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import type { NotebookOutputEventParams } from 'vscode-notebook-renderer'; +import '../../client/common/extensions'; +import { JupyterNotebookRenderer } from './constants'; +import { CellOutput } from './render'; + +const notebookApi = acquireNotebookRendererApi(JupyterNotebookRenderer); + +notebookApi.onDidCreateOutput(renderOutput); + +/** + * Called from renderer to render output. + * This will be exposed as a public method on window for renderer to render output. + */ +function renderOutput(request: NotebookOutputEventParams) { + try { + // tslint:disable-next-line: no-console + console.error('request', request); + const output = convertVSCodeOutputToExecutResultOrDisplayData(request); + // tslint:disable-next-line: no-console + console.log(`Rendering mimeType ${request.mimeType}`, output); + // tslint:disable-next-line: no-console + console.error('request output', output); + + ReactDOM.render(React.createElement(CellOutput, { mimeType: request.mimeType, output }, null), request.element); + } catch (ex) { + // tslint:disable-next-line: no-console + console.error(`Failed to render mime type ${request.mimeType}`, ex); + } +} + +function convertVSCodeOutputToExecutResultOrDisplayData( + request: NotebookOutputEventParams +): nbformat.IExecuteResult | nbformat.IDisplayData { + // tslint:disable-next-line: no-any + const metadata: Record<string, any> = {}; + // Send metadata only for the mimeType we are interested in. + const customMetadata = request.output.metadata?.custom; + if (customMetadata) { + if (customMetadata[request.mimeType]) { + metadata[request.mimeType] = customMetadata[request.mimeType]; + } + if (customMetadata.needs_background) { + metadata.needs_background = customMetadata.needs_background; + } + if (customMetadata.unconfined) { + metadata.unconfined = customMetadata.unconfined; + } + } + + return { + data: { + [request.mimeType]: request.output.data[request.mimeType] + }, + metadata, + execution_count: null, + output_type: request.output.metadata?.custom?.vscode?.outputType || 'execute_result' + }; +} diff --git a/src/datascience-ui/renderers/render.tsx b/src/datascience-ui/renderers/render.tsx new file mode 100644 index 000000000000..e1e4fbdd8f73 --- /dev/null +++ b/src/datascience-ui/renderers/render.tsx @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import type { nbformat } from '@jupyterlab/coreutils'; +import type { JSONObject } from '@phosphor/coreutils'; +import * as React from 'react'; +import { concatMultilineString } from '../common'; +import { fixMarkdown } from '../interactive-common/markdownManipulation'; +import { getTransform } from '../interactive-common/transforms'; + +export interface ICellOutputProps { + output: nbformat.IExecuteResult | nbformat.IDisplayData; + mimeType: string; +} + +export class CellOutput extends React.Component<ICellOutputProps> { + constructor(prop: ICellOutputProps) { + super(prop); + } + public render() { + const mimeBundle = this.props.output.data; + const data: nbformat.MultilineString | JSONObject = mimeBundle[this.props.mimeType!]; + + switch (this.props.mimeType) { + case 'text/latex': + return this.renderLatex(data); + case 'image/png': + case 'image/jpeg': + return this.renderImage(mimeBundle, this.props.output.metadata); + + default: + return this.renderOutput(data, this.props.mimeType); + } + } + /** + * Custom rendering of image/png and image/jpeg to handle custom Jupyter metadata. + * Behavior adopted from Jupyter lab. + */ + // tslint:disable-next-line: no-any + private renderImage(mimeBundle: nbformat.IMimeBundle, metadata: Record<string, any> = {}) { + const mimeType = 'image/png' in mimeBundle ? 'image/png' : 'image/jpeg'; + + const imgStyle: Record<string, string | number> = {}; + const divStyle: Record<string, string | number> = { overflow: 'scroll' }; // This is the default style used by Jupyter lab. + const imgSrc = `data:${mimeType};base64,${mimeBundle[mimeType]}`; + + if (typeof metadata.needs_background === 'string') { + divStyle.backgroundColor = metadata.needs_background === 'light' ? 'white' : 'black'; + } + // tslint:disable-next-line: no-any + const imageMetadata = metadata[mimeType] as Record<string, any> | undefined; + if (imageMetadata) { + if (imageMetadata.height) { + imgStyle.height = imageMetadata.height; + } + if (imageMetadata.width) { + imgStyle.width = imageMetadata.width; + } + if (imageMetadata.unconfined === true) { + imgStyle.maxWidth = 'none'; + } + } + + // Hack, use same classes as used in VSCode for images (keep things as similar as possible). + // This is to maintain consistently in displaying images (if we hadn't used HTML). + // See src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts + // tslint:disable: react-a11y-img-has-alt + return ( + <div className={'display'} style={divStyle}> + <img src={imgSrc} style={imgStyle}></img> + </div> + ); + } + private renderOutput(data: nbformat.MultilineString | JSONObject, mimeType?: string) { + const Transform = getTransform(this.props.mimeType!); + const divStyle: React.CSSProperties = { + backgroundColor: mimeType && isAltairPlot(mimeType) ? 'white' : undefined + }; + return ( + <div style={divStyle}> + <Transform data={data} /> + </div> + ); + } + private renderLatex(data: nbformat.MultilineString | JSONObject) { + // Fixup latex to make sure it has the requisite $$ around it + data = fixMarkdown(concatMultilineString(data as nbformat.MultilineString, true), true); + return this.renderOutput(data); + } +} + +function isAltairPlot(mimeType: string) { + return mimeType.includes('application/vnd.vega'); +} diff --git a/src/datascience-ui/startPage/index.html b/src/datascience-ui/startPage/index.html new file mode 100644 index 000000000000..b405e61a8e80 --- /dev/null +++ b/src/datascience-ui/startPage/index.html @@ -0,0 +1,356 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no" /> + <meta name="theme-color" content="#000000" /> + <title>Python Extension Plot Viewer</title> + <base href="<%= htmlWebpackPlugin.options.indexUrl %>" /> + <style id="default-styles"> + :root { + --background-color: #ffffff; + --comment-color: green; + --color: #000000; + --font-family: -apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', HelveticaNeue-Light, Ubuntu, + 'Droid Sans', sans-serif; + --font-size: 13px; + --font-weight: normal; + --link-active-color: #006ab1; + --link-color: #006ab1; + --vscode-activityBar-background: #2c2c2c; + --vscode-activityBar-dropBackground: rgba(255, 255, 255, 0.12); + --vscode-activityBar-foreground: #ffffff; + --vscode-activityBar-inactiveForeground: rgba(255, 255, 255, 0.6); + --vscode-activityBarBadge-background: #007acc; + --vscode-activityBarBadge-foreground: #ffffff; + --vscode-badge-background: #c4c4c4; + --vscode-badge-foreground: #333333; + --vscode-breadcrumb-activeSelectionForeground: #4e4e4e; + --vscode-breadcrumb-background: #ffffff; + --vscode-breadcrumb-focusForeground: #4e4e4e; + --vscode-breadcrumb-foreground: rgba(97, 97, 97, 0.8); + --vscode-breadcrumbPicker-background: #f3f3f3; + --vscode-button-background: #007acc; + --vscode-button-foreground: #ffffff; + --vscode-button-hoverBackground: #0062a3; + --vscode-debugExceptionWidget-background: #f1dfde; + --vscode-debugExceptionWidget-border: #a31515; + --vscode-debugToolBar-background: #f3f3f3; + --vscode-descriptionForeground: #717171; + --vscode-diffEditor-insertedTextBackground: rgba(155, 185, 85, 0.2); + --vscode-diffEditor-removedTextBackground: rgba(255, 0, 0, 0.2); + --vscode-dropdown-background: #ffffff; + --vscode-dropdown-border: #cecece; + --vscode-editor-background: #ffffff; + --vscode-editor-findMatchBackground: #a8ac94; + --vscode-editor-findMatchHighlightBackground: rgba(234, 92, 0, 0.33); + --vscode-editor-findRangeHighlightBackground: rgba(180, 180, 180, 0.3); + --vscode-editor-font-family: -apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', + HelveticaNeue-Light, Ubuntu, 'Droid Sans', sans-serif; + --vscode-editor-font-size: 13px; + --vscode-editor-font-weight: normal; + --vscode-editor-foreground: #000000; + --vscode-editor-hoverHighlightBackground: rgba(173, 214, 255, 0.15); + --vscode-editor-inactiveSelectionBackground: #e5ebf1; + --vscode-editor-lineHighlightBorder: #eeeeee; + --vscode-editor-rangeHighlightBackground: rgba(253, 255, 0, 0.2); + --vscode-editor-selectionBackground: #add6ff; + --vscode-editor-selectionHighlightBackground: rgba(173, 214, 255, 0.3); + --vscode-editor-snippetFinalTabstopHighlightBorder: rgba(10, 50, 100, 0.5); + --vscode-editor-snippetTabstopHighlightBackground: rgba(10, 50, 100, 0.2); + --vscode-editor-wordHighlightBackground: rgba(87, 87, 87, 0.25); + --vscode-editor-wordHighlightStrongBackground: rgba(14, 99, 156, 0.25); + --vscode-editorActiveLineNumber-foreground: #0b216f; + --vscode-editorBracketMatch-background: rgba(0, 100, 0, 0.1); + --vscode-editorBracketMatch-border: #b9b9b9; + --vscode-editorCodeLens-foreground: #999999; + --vscode-editorCursor-foreground: #000000; + --vscode-editorError-foreground: #d60a0a; + --vscode-editorGroup-border: #e7e7e7; + --vscode-editorGroup-dropBackground: rgba(38, 119, 203, 0.18); + --vscode-editorGroupHeader-noTabsBackground: #ffffff; + --vscode-editorGroupHeader-tabsBackground: #f3f3f3; + --vscode-editorGutter-addedBackground: #81b88b; + --vscode-editorGutter-background: #ffffff; + --vscode-editorGutter-commentRangeForeground: #c5c5c5; + --vscode-editorGutter-deletedBackground: #ca4b51; + --vscode-editorGutter-modifiedBackground: #66afe0; + --vscode-editorHint-foreground: #6c6c6c; + --vscode-editorHoverWidget-background: #f3f3f3; + --vscode-editorHoverWidget-border: #c8c8c8; + --vscode-editorIndentGuide-activeBackground: #939393; + --vscode-editorIndentGuide-background: #d3d3d3; + --vscode-editorInfo-foreground: #008000; + --vscode-editorLineNumber-activeForeground: #0b216f; + --vscode-editorLineNumber-foreground: #237893; + --vscode-editorLink-activeForeground: #0000ff; + --vscode-editorMarkerNavigation-background: #ffffff; + --vscode-editorMarkerNavigationError-background: #d60a0a; + --vscode-editorMarkerNavigationInfo-background: #008000; + --vscode-editorMarkerNavigationWarning-background: #117711; + --vscode-editorOverviewRuler-addedForeground: rgba(0, 122, 204, 0.6); + --vscode-editorOverviewRuler-border: rgba(127, 127, 127, 0.3); + --vscode-editorOverviewRuler-bracketMatchForeground: #a0a0a0; + --vscode-editorOverviewRuler-commonContentForeground: rgba(96, 96, 96, 0.4); + --vscode-editorOverviewRuler-currentContentForeground: rgba(64, 200, 174, 0.5); + --vscode-editorOverviewRuler-deletedForeground: rgba(0, 122, 204, 0.6); + --vscode-editorOverviewRuler-errorForeground: rgba(255, 18, 18, 0.7); + --vscode-editorOverviewRuler-findMatchForeground: rgba(246, 185, 77, 0.7); + --vscode-editorOverviewRuler-incomingContentForeground: rgba(64, 166, 255, 0.5); + --vscode-editorOverviewRuler-infoForeground: rgba(18, 18, 136, 0.7); + --vscode-editorOverviewRuler-modifiedForeground: rgba(0, 122, 204, 0.6); + --vscode-editorOverviewRuler-rangeHighlightForeground: rgba(0, 122, 204, 0.6); + --vscode-editorOverviewRuler-selectionHighlightForeground: rgba(160, 160, 160, 0.8); + --vscode-editorOverviewRuler-warningForeground: rgba(18, 136, 18, 0.7); + --vscode-editorOverviewRuler-wordHighlightForeground: rgba(160, 160, 160, 0.8); + --vscode-editorOverviewRuler-wordHighlightStrongForeground: rgba(192, 160, 192, 0.8); + --vscode-editorPane-background: #ffffff; + --vscode-editorRuler-foreground: #d3d3d3; + --vscode-editorSuggestWidget-background: #f3f3f3; + --vscode-editorSuggestWidget-border: #c8c8c8; + --vscode-editorSuggestWidget-foreground: #000000; + --vscode-editorSuggestWidget-highlightForeground: #0066bf; + --vscode-editorSuggestWidget-selectedBackground: #d6ebff; + --vscode-editorUnnecessaryCode-opacity: rgba(0, 0, 0, 0.47); + --vscode-editorWarning-foreground: #117711; + --vscode-editorWhitespace-foreground: rgba(51, 51, 51, 0.2); + --vscode-editorWidget-background: #f3f3f3; + --vscode-editorWidget-border: #c8c8c8; + --vscode-errorForeground: #a1260d; + --vscode-extensionButton-prominentBackground: #327e36; + --vscode-extensionButton-prominentForeground: #ffffff; + --vscode-extensionButton-prominentHoverBackground: #28632b; + --vscode-focusBorder: rgba(0, 122, 204, 0.4); + --vscode-foreground: #616161; + --vscode-gitDecoration-addedResourceForeground: #587c0c; + --vscode-gitDecoration-conflictingResourceForeground: #6c6cc4; + --vscode-gitDecoration-deletedResourceForeground: #ad0707; + --vscode-gitDecoration-ignoredResourceForeground: #8e8e90; + --vscode-gitDecoration-modifiedResourceForeground: #895503; + --vscode-gitDecoration-submoduleResourceForeground: #1258a7; + --vscode-gitDecoration-untrackedResourceForeground: #007100; + --vscode-input-background: #ffffff; + --vscode-input-foreground: #616161; + --vscode-input-placeholderForeground: #767676; + --vscode-inputOption-activeBorder: #007acc; + --vscode-inputValidation-errorBackground: #f2dede; + --vscode-inputValidation-errorBorder: #be1100; + --vscode-inputValidation-infoBackground: #d6ecf2; + --vscode-inputValidation-infoBorder: #007acc; + --vscode-inputValidation-warningBackground: #f6f5d2; + --vscode-inputValidation-warningBorder: #b89500; + --vscode-list-activeSelectionBackground: #2477ce; + --vscode-list-activeSelectionForeground: #ffffff; + --vscode-list-dropBackground: #d6ebff; + --vscode-list-errorForeground: #b01011; + --vscode-list-focusBackground: #d6ebff; + --vscode-list-highlightForeground: #0066bf; + --vscode-list-hoverBackground: #e8e8e8; + --vscode-list-inactiveFocusBackground: #d8dae6; + --vscode-list-inactiveSelectionBackground: #e4e6f1; + --vscode-list-invalidItemForeground: #b89500; + --vscode-list-warningForeground: #117711; + --vscode-menu-background: #ffffff; + --vscode-menu-selectionBackground: #2477ce; + --vscode-menu-selectionForeground: #ffffff; + --vscode-menu-separatorBackground: #888888; + --vscode-menubar-selectionBackground: rgba(0, 0, 0, 0.1); + --vscode-menubar-selectionForeground: #333333; + --vscode-merge-commonContentBackground: rgba(96, 96, 96, 0.16); + --vscode-merge-commonHeaderBackground: rgba(96, 96, 96, 0.4); + --vscode-merge-currentContentBackground: rgba(64, 200, 174, 0.2); + --vscode-merge-currentHeaderBackground: rgba(64, 200, 174, 0.5); + --vscode-merge-incomingContentBackground: rgba(64, 166, 255, 0.2); + --vscode-merge-incomingHeaderBackground: rgba(64, 166, 255, 0.5); + --vscode-notificationCenterHeader-background: #e7e7e7; + --vscode-notificationLink-foreground: #006ab1; + --vscode-notifications-background: #f3f3f3; + --vscode-notifications-border: #e7e7e7; + --vscode-panel-background: #ffffff; + --vscode-panel-border: rgba(128, 128, 128, 0.35); + --vscode-panel-dropBackground: rgba(38, 119, 203, 0.18); + --vscode-panelTitle-activeBorder: rgba(128, 128, 128, 0.35); + --vscode-panelTitle-activeForeground: #424242; + --vscode-panelTitle-inactiveForeground: rgba(66, 66, 66, 0.75); + --vscode-peekView-border: #007acc; + --vscode-peekViewEditor-background: #f2f8fc; + --vscode-peekViewEditor-matchHighlightBackground: rgba(245, 216, 2, 0.87); + --vscode-peekViewEditorGutter-background: #f2f8fc; + --vscode-peekViewResult-background: #f3f3f3; + --vscode-peekViewResult-fileForeground: #1e1e1e; + --vscode-peekViewResult-lineForeground: #646465; + --vscode-peekViewResult-matchHighlightBackground: rgba(234, 92, 0, 0.3); + --vscode-peekViewResult-selectionBackground: rgba(51, 153, 255, 0.2); + --vscode-peekViewResult-selectionForeground: #6c6c6c; + --vscode-peekViewTitle-background: #ffffff; + --vscode-peekViewTitleDescription-foreground: rgba(108, 108, 108, 0.7); + --vscode-peekViewTitleLabel-foreground: #333333; + --vscode-pickerGroup-border: #cccedb; + --vscode-pickerGroup-foreground: #0066bf; + --vscode-progressBar-background: #0e70c0; + --vscode-scrollbar-shadow: #dddddd; + --vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6); + --vscode-scrollbarSlider-background: rgba(100, 100, 100, 0.4); + --vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7); + --vscode-settings-checkboxBackground: #ffffff; + --vscode-settings-checkboxBorder: #cecece; + --vscode-settings-dropdownBackground: #ffffff; + --vscode-settings-dropdownBorder: #cecece; + --vscode-settings-dropdownListBorder: #c8c8c8; + --vscode-settings-headerForeground: #444444; + --vscode-settings-modifiedItemIndicator: #66afe0; + --vscode-settings-numberInputBackground: #ffffff; + --vscode-settings-numberInputBorder: #cecece; + --vscode-settings-numberInputForeground: #616161; + --vscode-settings-textInputBackground: #ffffff; + --vscode-settings-textInputBorder: #cecece; + --vscode-settings-textInputForeground: #616161; + --vscode-sideBar-background: #f3f3f3; + --vscode-sideBar-dropBackground: rgba(255, 255, 255, 0.12); + --vscode-sideBarSectionHeader-background: rgba(128, 128, 128, 0.2); + --vscode-sideBarTitle-foreground: #6f6f6f; + --vscode-statusBar-background: #007acc; + --vscode-statusBar-debuggingBackground: #cc6633; + --vscode-statusBar-debuggingForeground: #ffffff; + --vscode-statusBar-foreground: #ffffff; + --vscode-statusBar-noFolderBackground: #68217a; + --vscode-statusBar-noFolderForeground: #ffffff; + --vscode-statusBarItem-activeBackground: rgba(255, 255, 255, 0.18); + --vscode-statusBarItem-hoverBackground: rgba(255, 255, 255, 0.12); + --vscode-statusBarItem-prominentBackground: #388a34; + --vscode-statusBarItem-prominentHoverBackground: #369432; + --vscode-tab-activeBackground: #ffffff; + --vscode-tab-activeForeground: #333333; + --vscode-tab-border: #f3f3f3; + --vscode-tab-inactiveBackground: #ececec; + --vscode-tab-inactiveForeground: rgba(51, 51, 51, 0.5); + --vscode-tab-unfocusedActiveForeground: rgba(51, 51, 51, 0.7); + --vscode-tab-unfocusedInactiveForeground: rgba(51, 51, 51, 0.25); + --vscode-terminal-ansiBlack: #000000; + --vscode-terminal-ansiBlue: #0451a5; + --vscode-terminal-ansiBrightBlack: #666666; + --vscode-terminal-ansiBrightBlue: #0451a5; + --vscode-terminal-ansiBrightCyan: #0598bc; + --vscode-terminal-ansiBrightGreen: #14ce14; + --vscode-terminal-ansiBrightMagenta: #bc05bc; + --vscode-terminal-ansiBrightRed: #cd3131; + --vscode-terminal-ansiBrightWhite: #a5a5a5; + --vscode-terminal-ansiBrightYellow: #b5ba00; + --vscode-terminal-ansiCyan: #0598bc; + --vscode-terminal-ansiGreen: #00bc00; + --vscode-terminal-ansiMagenta: #bc05bc; + --vscode-terminal-ansiRed: #cd3131; + --vscode-terminal-ansiWhite: #555555; + --vscode-terminal-ansiYellow: #949800; + --vscode-terminal-background: #ffffff; + --vscode-terminal-border: rgba(128, 128, 128, 0.35); + --vscode-terminal-foreground: #333333; + --vscode-terminal-selectionBackground: rgba(0, 0, 0, 0.25); + --vscode-textBlockQuote-background: rgba(127, 127, 127, 0.1); + --vscode-textBlockQuote-border: rgba(0, 122, 204, 0.5); + --vscode-textCodeBlock-background: rgba(220, 220, 220, 0.4); + --vscode-textLink-activeForeground: #006ab1; + --vscode-textLink-foreground: #006ab1; + --vscode-textPreformat-foreground: #a31515; + --vscode-textSeparator-foreground: rgba(0, 0, 0, 0.18); + --vscode-titleBar-activeBackground: #dddddd; + --vscode-titleBar-activeForeground: #333333; + --vscode-titleBar-inactiveBackground: rgba(221, 221, 221, 0.6); + --vscode-titleBar-inactiveForeground: rgba(51, 51, 51, 0.6); + --vscode-widget-shadow: #a8a8a8; + --code-font-family: 'Comic-Sans'; + --code-font-size: 15px; + } + + body { + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + font-family: var(--vscode-editor-font-family); + font-weight: var(--vscode-editor-font-weight); + font-size: var(--vscode-editor-font-size); + margin: 0; + padding: 0 20px; + } + + img { + max-width: 100%; + max-height: 100%; + } + + a { + color: var(--vscode-textLink-foreground); + } + + a:hover { + color: var(--vscode-textLink-activeForeground); + } + + a:focus, + input:focus, + select:focus, + textarea:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; + } + + code { + color: var(--vscode-textPreformat-foreground); + } + + blockquote { + background: var(--vscode-textBlockQuote-background); + border-color: var(--vscode-textBlockQuote-border); + } + + ::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + ::-webkit-scrollbar-thumb { + background-color: rgba(121, 121, 121, 0.4); + } + body.vscode-light::-webkit-scrollbar-thumb { + background-color: rgba(100, 100, 100, 0.4); + } + body.vscode-high-contrast::-webkit-scrollbar-thumb { + background-color: rgba(111, 195, 223, 0.3); + } + + ::-webkit-scrollbar-thumb:hover { + background-color: rgba(100, 100, 100, 0.7); + } + body.vscode-light::-webkit-scrollbar-thumb:hover { + background-color: rgba(100, 100, 100, 0.7); + } + body.vscode-high-contrast::-webkit-scrollbar-thumb:hover { + background-color: rgba(111, 195, 223, 0.8); + } + + ::-webkit-scrollbar-thumb:active { + background-color: rgba(85, 85, 85, 0.8); + } + body.vscode-light::-webkit-scrollbar-thumb:active { + background-color: rgba(0, 0, 0, 0.6); + } + body.vscode-high-contrast::-webkit-scrollbar-thumb:active { + background-color: rgba(111, 195, 223, 0.8); + } + </style> + </head> + <body> + <div id="root"></div> + <script type="text/javascript"> + function resolvePath(relativePath) { + if (relativePath && relativePath[0] == '.' && relativePath[1] != '.') { + return '<%= htmlWebpackPlugin.options.imageBaseUrl %>' + relativePath.substring(1); + } + + return '<%= htmlWebpackPlugin.options.imageBaseUrl %>' + relativePath; + } + function getInitialSettings() { + return { allowInput: true, extraSettings: { editorCursor: 'block', editorCursorBlink: 'blink' } }; + } + </script> + </body> +</html> diff --git a/src/datascience-ui/startPage/index.tsx b/src/datascience-ui/startPage/index.tsx new file mode 100644 index 000000000000..1b9828708914 --- /dev/null +++ b/src/datascience-ui/startPage/index.tsx @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// This must be on top, do not change. Required by webpack. +import '../common/main'; +// This must be on top, do not change. Required by webpack. + +// tslint:disable-next-line: ordered-imports +import '../common/index.css'; + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { IVsCodeApi } from '../react-common/postOffice'; +import { detectBaseTheme } from '../react-common/themeDetector'; +import { StartPage } from './startPage'; + +// This special function talks to vscode from a web panel +export declare function acquireVsCodeApi(): IVsCodeApi; + +const baseTheme = detectBaseTheme(); +// tslint:disable-next-line: no-any +const testMode = (window as any).inTestMode; +// tslint:disable-next-line: no-typeof-undefined +const skipDefault = testMode ? false : typeof acquireVsCodeApi !== 'undefined'; + +ReactDOM.render( + <StartPage baseTheme={baseTheme} skipDefault={skipDefault} testMode={testMode} />, + document.getElementById('root') as HTMLElement +); diff --git a/src/datascience-ui/startPage/startPage.css b/src/datascience-ui/startPage/startPage.css new file mode 100644 index 000000000000..035610dcebf8 --- /dev/null +++ b/src/datascience-ui/startPage/startPage.css @@ -0,0 +1,93 @@ +.main-page { + margin: 50px; + font-family: var(--font-family); + min-width: 700px; +} + +.title-icon { + display: inline-block; + vertical-align: top; + width: 40px; + margin-right: 10px; +} + +.icon { + display: inline-block; + width: 45px; + padding: 20px 25px 20px 25px; + margin-right: 30px; + background-color: var(--vscode-titleBar-activeBackground); +} + +.icon:hover, +.text:hover { + background-color: var(--vscode-editorIndentGuide-activeBackground); + cursor: pointer; +} + +.title { + display: inline-block; + vertical-align: top; + font-size: xx-large; +} + +.title-row { + display: block; + height: 80px; +} + +.text { + font-weight: 100; + font-size: x-large; + width: fit-content; +} + +.block { + display: inline-block; + vertical-align: top; +} + +.row { + display: block; + min-height: 120px; + white-space: nowrap; +} + +.releaseNotesRow { + display: block; + min-height: 50px; + white-space: nowrap; +} + +.link { + display: inline; + color: var(--vscode-debugIcon-continueForeground); + text-decoration: none; +} + +.link:hover { + cursor: pointer; + color: var(--vscode-button-hoverBackground); +} + +.italics { + display: inline; + font-style: italic; +} + +.checkbox { + margin: 1em 1em 1em 0em; +} + +.paragraph { + display: block; + margin-block-start: 5px; + margin-block-end: 5px; + margin-inline-start: 0px; + margin-inline-end: 0px; +} + +.list { + line-height: 1.5; + white-space: initial; +} diff --git a/src/datascience-ui/startPage/startPage.tsx b/src/datascience-ui/startPage/startPage.tsx new file mode 100644 index 000000000000..3794c22919fe --- /dev/null +++ b/src/datascience-ui/startPage/startPage.tsx @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as React from 'react'; +import '../../client/common/extensions'; +import { ISettingPackage, IStartPageMapping, StartPageMessages } from '../../client/common/startPage/types'; +import { Image, ImageName } from '../react-common/image'; +import { getLocString } from '../react-common/locReactSide'; +import { IMessageHandler, PostOffice } from '../react-common/postOffice'; +import './startPage.css'; + +export interface IStartPageProps { + skipDefault?: boolean; + baseTheme: string; + testMode?: boolean; +} + +// Front end of the Python extension start page. +// In general it consists of its render method and methods that send and receive messages. +export class StartPage extends React.Component<IStartPageProps> implements IMessageHandler { + private releaseNotes: ISettingPackage = { + showAgainSetting: false + }; + private postOffice: PostOffice = new PostOffice(); + + constructor(props: IStartPageProps) { + super(props); + } + + public componentDidMount() { + this.postOffice.sendMessage<IStartPageMapping>(StartPageMessages.RequestShowAgainSetting); + } + + // tslint:disable: no-any + public componentWillMount() { + // Add ourselves as a handler for the post office + this.postOffice.addHandler(this); + + // Tell the start page code we have started. + this.postOffice.sendMessage<IStartPageMapping>(StartPageMessages.Started); + + // Bind some functions to the window, as we need them to be accessible with clean HTML to use translations + (window as any).openBlankNotebook = this.openBlankNotebook.bind(this); + (window as any).createPythonFile = this.createPythonFile.bind(this); + (window as any).openFileBrowser = this.openFileBrowser.bind(this); + (window as any).openFolder = this.openFolder.bind(this); + (window as any).openWorkspace = this.openWorkspace.bind(this); + (window as any).openCommandPalette = this.openCommandPalette.bind(this); + (window as any).openCommandPaletteWithSelection = this.openCommandPaletteWithSelection.bind(this); + (window as any).openSampleNotebook = this.openSampleNotebook.bind(this); + } + + public render() { + // tslint:disable: react-a11y-anchors + return ( + <div className="main-page"> + <div className="title-row"> + <div className="title-icon"> + <Image + baseTheme={this.props.baseTheme} + class="image-button-image" + image={ImageName.PythonColor} + /> + </div> + <div className="title">{getLocString('StartPage.pythonExtensionTitle', 'Python Extension')}</div> + </div> + <div className="row"> + <div className="icon" onClick={this.openBlankNotebook} role="button"> + <Image + baseTheme={this.props.baseTheme ? this.props.baseTheme : 'vscode-dark'} + class="image-button-image" + image={ImageName.Notebook} + /> + </div> + <div className="block"> + <div className="text" onClick={this.openBlankNotebook} role="button"> + {getLocString('StartPage.CreateJupyterNotebook', 'Create a Jupyter Notebook')} + </div> + {this.renderNotebookDescription()} + </div> + </div> + <div className="row"> + <div className="icon" role="button" onClick={this.createPythonFile}> + <Image + baseTheme={this.props.baseTheme ? this.props.baseTheme : 'vscode-dark'} + class="image-button-image" + image={ImageName.Python} + /> + </div> + <div className="block"> + <div className="text" role="button" onClick={this.createPythonFile}> + {getLocString('StartPage.createAPythonFile', 'Create a Python File')} + </div> + {this.renderPythonFileDescription()} + </div> + </div> + <div className="row"> + <div className="icon" role="button" onClick={this.openFolder}> + <Image + baseTheme={this.props.baseTheme ? this.props.baseTheme : 'vscode-dark'} + class="image-button-image" + image={ImageName.OpenFolder} + /> + </div> + <div className="block"> + <div className="text" role="button" onClick={this.openFolder}> + {getLocString('StartPage.openFolder', 'Open a Folder or Workspace')} + </div> + {this.renderFolderDescription()} + </div> + </div> + <div className="row"> + <div className="icon" role="button" onClick={this.openInteractiveWindow}> + <Image + baseTheme={this.props.baseTheme ? this.props.baseTheme : 'vscode-dark'} + class="image-button-image" + image={ImageName.Interactive} + /> + </div> + <div className="block"> + <div className="text" role="button" onClick={this.openInteractiveWindow}> + {getLocString( + 'StartPage.openInteractiveWindow', + 'Use the Interactive Window to develop Python Scripts' + )} + </div> + {this.renderInteractiveWindowDescription()} + </div> + </div> + <div className="releaseNotesRow"> + {this.renderReleaseNotesLink()} + {this.renderTutorialAndDoc()} + </div> + <div className="block"> + <input + type="checkbox" + aria-checked={!this.releaseNotes.showAgainSetting} + className="checkbox" + onClick={this.updateSettings} + ></input> + </div> + <div className="block"> + <p>{getLocString('StartPage.dontShowAgain', "Don't show this page again")}</p> + </div> + </div> + ); + } + + // tslint:disable-next-line: no-any + public handleMessage = (msg: string, payload?: any) => { + if (msg === StartPageMessages.SendSetting) { + this.releaseNotes.showAgainSetting = payload.showAgainSetting; + this.setState({}); + } + + return false; + }; + + public openFileBrowser() { + this.postOffice.sendMessage<IStartPageMapping>(StartPageMessages.OpenFileBrowser); + } + + public openFolder = () => { + this.postOffice.sendMessage<IStartPageMapping>(StartPageMessages.OpenFolder); + }; + + public openWorkspace() { + this.postOffice.sendMessage<IStartPageMapping>(StartPageMessages.OpenWorkspace); + } + + private renderNotebookDescription(): JSX.Element { + // tslint:disable: react-no-dangerous-html + return ( + <div + className="paragraph list" + dangerouslySetInnerHTML={{ + __html: getLocString( + 'StartPage.notebookDescription', + '- Run "<div class="link italics" role="button" onclick={0}>Create New Blank Jupyter Notebook</div>" in the Command Palette (<div class="italics">Shift + Command + P</div>)<br />- Explore our <div class="link" role="button" onclick={1}>sample notebook</div> to learn about notebook features' + ).format('openCommandPaletteWithSelection()', 'openSampleNotebook()') + }} + /> + ); + } + + private renderPythonFileDescription(): JSX.Element { + // tslint:disable: react-no-dangerous-html + return ( + <div + className="paragraph list" + dangerouslySetInnerHTML={{ + __html: getLocString( + 'StartPage.pythonFileDescription', + '- Create a <div class="link" role="button" onclick={0}>new file</div> with a .py extension' + ).format('createPythonFile()') + }} + /> + ); + } + + private renderInteractiveWindowDescription(): JSX.Element { + // tslint:disable: react-no-dangerous-html + return ( + <div + className="paragraph list" + dangerouslySetInnerHTML={{ + __html: getLocString( + 'StartPage.interactiveWindowDesc', + '- You can create cells on a Python file by typing "#%%" <br /> - Use "<div class="italics">Shift + Enter</div> " to run a cell, the output will be shown in the interactive window' + ) + }} + /> + ); + } + + private renderFolderDescription(): JSX.Element { + // tslint:disable: react-no-dangerous-html + return ( + <div + className="paragraph list" + dangerouslySetInnerHTML={{ + __html: getLocString( + 'StartPage.folderDesc', + '- Open a <div class="link" role="button" onclick={0}>Folder</div><br /> - Open a <div class="link" role="button" onclick={1}>Workspace</div>' + ).format('openFolder()', 'openWorkspace()') + }} + /> + ); + } + + private renderReleaseNotesLink(): JSX.Element { + // tslint:disable: react-no-dangerous-html + return ( + <div + className="paragraph" + dangerouslySetInnerHTML={{ + __html: getLocString( + 'StartPage.releaseNotes', + 'Take a look at our <a class="link" href={0}>Release Notes</a> to learn more about the latest features.' + ).format('https://aka.ms/AA8dxtb') + }} + /> + ); + } + + private renderTutorialAndDoc(): JSX.Element { + // tslint:disable: react-no-dangerous-html + return ( + <div + className="paragraph" + dangerouslySetInnerHTML={{ + __html: getLocString( + 'StartPage.tutorialAndDoc', + 'Explore more features in our <a class="link" href={0}>Tutorials</a> or check <a class="link" href={1}>Documentation</a> for tips and troubleshooting.' + ).format('https://aka.ms/AA8dqti', 'https://aka.ms/AA8dxwy') + }} + /> + ); + } + + private openBlankNotebook = () => { + this.postOffice.sendMessage<IStartPageMapping>(StartPageMessages.OpenBlankNotebook); + }; + + private createPythonFile = () => { + this.postOffice.sendMessage<IStartPageMapping>(StartPageMessages.OpenBlankPythonFile); + }; + + private openCommandPalette = () => { + this.postOffice.sendMessage<IStartPageMapping>(StartPageMessages.OpenCommandPalette); + }; + + private openCommandPaletteWithSelection = () => { + this.postOffice.sendMessage<IStartPageMapping>(StartPageMessages.OpenCommandPaletteWithOpenNBSelected); + }; + + private openSampleNotebook = () => { + this.postOffice.sendMessage<IStartPageMapping>(StartPageMessages.OpenSampleNotebook); + }; + + private openInteractiveWindow = () => { + this.postOffice.sendMessage<IStartPageMapping>(StartPageMessages.OpenInteractiveWindow); + }; + + private updateSettings = () => { + this.releaseNotes.showAgainSetting = !this.releaseNotes.showAgainSetting; + this.postOffice.sendMessage<IStartPageMapping>( + StartPageMessages.UpdateSettings, + this.releaseNotes.showAgainSetting + ); + }; +} diff --git a/src/ipywidgets/.gitignore b/src/ipywidgets/.gitignore new file mode 100644 index 000000000000..9095dd4beb82 --- /dev/null +++ b/src/ipywidgets/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +out +dist +**/node_modules +.nyc_output +npm-debug.log +bin/** +obj/** +tmp/** diff --git a/src/ipywidgets/.vscode/settings.json b/src/ipywidgets/.vscode/settings.json new file mode 100644 index 000000000000..c9aa44e04a63 --- /dev/null +++ b/src/ipywidgets/.vscode/settings.json @@ -0,0 +1,44 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": true, + "**/*.pyc": true, + ".nyc_output": true, + "obj": true, + "bin": true, + "**/__pycache__": true, + "**/node_modules": true, + ".vscode test": false, + ".vscode-test": false, + "**/.mypy_cache/**": true, + "**/.ropeproject/**": true + }, + "search.exclude": { + "out": true, + "**/node_modules": true, + "coverage": true, + "languageServer*/**": true, + ".vscode-test": true, + ".vscode test": true + }, + "[python]": { + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.formatOnSave": true + }, + "typescript.preferences.quoteStyle": "single", + "javascript.preferences.quoteStyle": "single", + "typescriptHero.imports.stringQuoteStyle": "'", + "cucumberautocomplete.skipDocStringsFormat": true, + "[javascript]": { + "editor.formatOnSave": true + }, + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.fixAll.tslint": true + }, + "eslint.options": { + "ignorePath": "../.eslintignore" + } +} diff --git a/src/ipywidgets/README.md b/src/ipywidgets/README.md new file mode 100644 index 000000000000..35736be4092b --- /dev/null +++ b/src/ipywidgets/README.md @@ -0,0 +1,29 @@ +# This folder is based off the the sample `web3` from https://github.com/jupyter-widgets/ipywidgets/blob/master/examples/web3 + +- We have built a custom solution based on `web3` sample to host ipywidgets outside of `Jupyter Notebook`. + +# Warning + +- Most of the code has been copied as is from `https://github.com/jupyter-widgets/ipywidgets/blob/master/examples/web3` & `https://github.com/jupyter-widgets/ipywidgets/blob/master/packages/html-manager/webpack.config.js`. + - Please try to minimize changes to original code to facilitate easier updatess. + +# Solution for IPywidgets + +- IPywidgets traditionally use [requirejs](https://requirejs.org). + - `traditionally` as there seems to be some ongoing work to use `commonjs2`, though unsure how this will work with 3rd party widgets. +- Using 3rd party widgets require: + - [requirejs](https://requirejs.org) to be available in the current browser context (i.e. `window`) + - Base `IPywidgets` to be defined using `define` in [requirejs](https://requirejs.org). +- Rather than bundling using `amd` or `umd` its easier to just import everything using `commonjs2`, then export for `requirejs` using `define` by hand. + - `define('xyz', () => 'a')` is a simple way of declaring a named `xyz` module with the value `a` (using `requirejs`). + - This is generally done using tools, however we'll hand craft this as it works better and easier. + - `amd` is not what we want, as out `react ui` doesn't use `amd`. + - `umd` is does not work as we have multiple `entry points` in `webpack`. + - Heres' the solution `define('@jupyter-widgets/controls', () => widgets);` +- We bundling the widget controls into our JS and exposing them for AMD using `define` + - We could instead include `https://unpkg.com/browse/@jupyter-widgets/html-manager@0.18.3/dist/embed-amd.js` + - However this is a 3.2MB file. + - Then our Widget manager also needs the widget controls. That would mean widget controls get included twice, once in our bundle and the other in the above mentioned `embed-amd.js` file. + - Solution is to include everything thats in `embed-amd.js` into our bundle. +- We need types for `requirejs`, but installing this into `node_modules`, for extension causes conflicts as we use `require` in standard node (extension and UI). + - Solution is to just copy the `@types/requirejs/index.d.ts` into the `types` folder. diff --git a/src/ipywidgets/package.json b/src/ipywidgets/package.json new file mode 100644 index 000000000000..7aef5889121e --- /dev/null +++ b/src/ipywidgets/package.json @@ -0,0 +1,12 @@ +{ + "name": "ipywidgets", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "build": "npm run clean && tsc && node scripts/copyfiles.js && webpack --mode='production' && npm run copyFile && npm run clean", + "build:dev": "npm run clean && tsc && node scripts/copyfiles.js && webpack --mode='development' && npm run copyFile && npm run clean", + "clean": "rimraf out && rimraf tsconfig.tsbuildinfo && rimraf dist", + "lint": "tslint --project tsconfig.json", + "copyFile": "node scripts/copyBuild.js" + } +} diff --git a/src/ipywidgets/scripts/clean.js b/src/ipywidgets/scripts/clean.js new file mode 100644 index 000000000000..acbfb2be97bc --- /dev/null +++ b/src/ipywidgets/scripts/clean.js @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const rimraf = require('rimraf'); +const path = require('path'); + +rimraf.sync(path.join(__dirname, '..', '..', '..', 'out', 'ipywidgets')); diff --git a/src/ipywidgets/scripts/copyfiles.js b/src/ipywidgets/scripts/copyfiles.js new file mode 100644 index 000000000000..19bf24824656 --- /dev/null +++ b/src/ipywidgets/scripts/copyfiles.js @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const outputDir = path.join(__dirname, '..', '..', '..', 'out/ipywidgets'); + +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); +} +fs.copyFileSync(path.join(__dirname, '../src/widgets.css'), path.join(outputDir, 'widgets.css')); diff --git a/src/ipywidgets/src/documentContext.ts b/src/ipywidgets/src/documentContext.ts new file mode 100644 index 000000000000..5c1bf0fed5cc --- /dev/null +++ b/src/ipywidgets/src/documentContext.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IClientSession } from '@jupyterlab/apputils'; +import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { INotebookModel, NotebookModel } from '@jupyterlab/notebook/lib'; +import { IRenderMime } from '@jupyterlab/rendermime'; +import { Contents, Kernel, KernelMessage, Session } from '@jupyterlab/services'; +import { Widget } from '@phosphor/widgets'; +import { Signal } from './signal'; +// tslint:disable: no-any +export class DocumentContext implements DocumentRegistry.IContext<INotebookModel>, IClientSession { + public pathChanged = new Signal<this, string>(); + public fileChanged = new Signal<this, Contents.IModel>(); + public saveState = new Signal<this, DocumentRegistry.SaveState>(); + public disposed = new Signal<this, void>(); + public model: INotebookModel; + public session: IClientSession = this; + public path: string; + public localPath: string; + public contentsModel: Contents.IModel; + public urlResolver: IRenderMime.IResolver; + public isReady: boolean; + public ready: Promise<void>; + public isDisposed: boolean; + public terminated = new Signal<this, void>(); + public kernelChanged = new Signal<this, Session.IKernelChangedArgs>(); + public statusChanged = new Signal<this, Kernel.Status>(); + public iopubMessage = new Signal<this, KernelMessage.IMessage>(); + public unhandledMessage = new Signal<this, KernelMessage.IMessage>(); + public propertyChanged = new Signal<this, 'path' | 'name' | 'type'>(); + public name: string; + public type: string; + public status: Kernel.Status; + public kernelPreference: IClientSession.IKernelPreference; + public kernelDisplayName: string; + constructor(public kernel: Kernel.IKernelConnection) { + // We are the session. + + // Generate a dummy notebook model + this.model = new NotebookModel(); + } + + public changeKernel(_options: Partial<Kernel.IModel>): Promise<Kernel.IKernelConnection> { + throw new Error('Method not implemented.'); + } + public shutdown(): Promise<void> { + throw new Error('Method not implemented.'); + } + public selectKernel(): Promise<void> { + throw new Error('Method not implemented.'); + } + public restart(): Promise<boolean> { + throw new Error('Method not implemented.'); + } + public setPath(_path: string): Promise<void> { + throw new Error('Method not implemented.'); + } + public setName(_name: string): Promise<void> { + throw new Error('Method not implemented.'); + } + public setType(_type: string): Promise<void> { + throw new Error('Method not implemented.'); + } + + public addSibling(_widget: Widget, _options?: any): any { + throw new Error('Method not implemented.'); + } + public save(): Promise<void> { + throw new Error('Method not implemented.'); + } + public saveAs(): Promise<void> { + throw new Error('Method not implemented.'); + } + public revert(): Promise<void> { + throw new Error('Method not implemented.'); + } + public createCheckpoint(): Promise<import('@jupyterlab/services').Contents.ICheckpointModel> { + throw new Error('Method not implemented.'); + } + public deleteCheckpoint(_checkpointID: string): Promise<void> { + throw new Error('Method not implemented.'); + } + public restoreCheckpoint(_checkpointID?: string): Promise<void> { + throw new Error('Method not implemented.'); + } + public listCheckpoints(): Promise<import('@jupyterlab/services').Contents.ICheckpointModel[]> { + throw new Error('Method not implemented.'); + } + public dispose(): void { + throw new Error('Method not implemented.'); + } +} diff --git a/src/ipywidgets/src/embed.ts b/src/ipywidgets/src/embed.ts new file mode 100644 index 000000000000..ecafca9e9810 --- /dev/null +++ b/src/ipywidgets/src/embed.ts @@ -0,0 +1,112 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +// tslint:disable: no-var-requires no-require-imports no-any +import * as libembed from './libembed'; +import * as wm from './manager'; + +let cdn = 'https://unpkg.com/'; +let onlyCDN = false; + +// find the data-cdn for any script tag, assuming it is only used for embed-amd.js +const scripts = document.getElementsByTagName('script'); +Array.prototype.forEach.call(scripts, (script: HTMLScriptElement) => { + cdn = script.getAttribute('data-jupyter-widgets-cdn') || cdn; + onlyCDN = onlyCDN || script.hasAttribute('data-jupyter-widgets-cdn-only'); +}); + +/** + * Load a package using requirejs and return a promise + * + * @param pkg Package name or names to load + */ +// tslint:disable-next-line: no-function-expression +const requirePromise = function (pkg: string | string[]): Promise<any> { + return new Promise((resolve, reject) => { + const require = (window as any).requirejs; + if (require === undefined) { + reject('Requirejs is needed, please ensure it is loaded on the page.'); + } else { + // tslint:disable-next-line: non-literal-require + require(pkg, resolve, reject); + } + }); +}; + +function moduleNameToCDNUrl(moduleName: string, moduleVersion: string): string { + let packageName = moduleName; + let fileName = 'index'; // default filename + // if a '/' is present, like 'foo/bar', packageName is changed to 'foo', and path to 'bar' + // We first find the first '/' + let index = moduleName.indexOf('/'); + if (index !== -1 && moduleName[0] === '@') { + // if we have a namespace, it's a different story + // @foo/bar/baz should translate to @foo/bar and baz + // so we find the 2nd '/' + index = moduleName.indexOf('/', index + 1); + } + if (index !== -1) { + fileName = moduleName.substr(index + 1); + packageName = moduleName.substr(0, index); + } + return `${cdn}${packageName}@${moduleVersion}/dist/${fileName}`; +} + +/** + * Load an amd module locally and fall back to specified CDN if unavailable. + * + * @param moduleName The name of the module to load.. + * @param moduleVersion The semver range for the module, if loaded from a CDN. + * + * By default, the CDN service used is unpkg.com. However, this default can be + * overriden by specifying another URL via the HTML attribute + * "data-jupyter-widgets-cdn" on a script tag of the page. + * + * The semver range is only used with the CDN. + */ +export function requireLoader(moduleName: string, moduleVersion: string): Promise<any> { + const require = (window as any).requirejs; + if (require === undefined) { + throw new Error('Requirejs is needed, please ensure it is loaded on the page.'); + } + function loadFromCDN(): Promise<any> { + const conf: { paths: { [key: string]: string } } = { paths: {} }; + conf.paths[moduleName] = moduleNameToCDNUrl(moduleName, moduleVersion); + require.config(conf); + return requirePromise([`${moduleName}`]); + } + if (onlyCDN) { + window.console.log(`Loading from ${cdn} for ${moduleName}@${moduleVersion}`); + return loadFromCDN(); + } + return requirePromise([`${moduleName}`]).catch((err) => { + const failedId = err.requireModules && err.requireModules[0]; + if (failedId) { + require.undef(failedId); + window.console.log(`Falling back to ${cdn} for ${moduleName}@${moduleVersion}`); + loadFromCDN().catch((x) => { + window.console.error(x); + }); + } + }); +} + +/** + * Render widgets in a given element. + * + * @param element (default document.documentElement) The element containing widget state and views. + * @param loader (default requireLoader) The function used to look up the modules containing + * the widgets' models and views classes. (The default loader looks them up on unpkg.com) + */ +export function renderWidgets(element = document.documentElement): void { + const managerFactory = (): any => { + return new wm.WidgetManager(undefined, element, { + widgetsRegisteredInRequireJs: new Set<string>(), + errorHandler: () => 'Error loading widget.', + loadWidgetScript: (_moduleName: string, _moduleVersion: string) => Promise.resolve(), + successHandler: () => 'Success' + }); + }; + libembed.renderWidgets(managerFactory, element).catch((x) => { + window.console.error(x); + }); +} diff --git a/src/ipywidgets/src/index.ts b/src/ipywidgets/src/index.ts new file mode 100644 index 000000000000..f428e7dbe8f8 --- /dev/null +++ b/src/ipywidgets/src/index.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export { WidgetManager } from './manager'; +import * as base from '@jupyter-widgets/base'; +import * as widgets from '@jupyter-widgets/controls'; +import * as outputWidgets from '@jupyter-widgets/jupyterlab-manager/lib/output'; +import * as embed from './embed'; +import './widgets.css'; + +// Export the following for `requirejs`. +// tslint:disable-next-line: no-any no-function-expression no-empty +const define = (window as any).define || function () {}; +define('@jupyter-widgets/controls', () => widgets); +define('@jupyter-widgets/base', () => base); +define('@jupyter-widgets/output', () => outputWidgets); + +// Render existing widgets without a kernel and pull in the correct css files +// This is not done yet. See this issue here: https://github.com/microsoft/vscode-python/issues/10794 +// Likely we'll do this in a different spot. +if (document.readyState === 'complete') { + embed.renderWidgets(); +} else { + window.addEventListener('load', () => { + embed.renderWidgets(); + }); +} diff --git a/src/ipywidgets/src/libembed.ts b/src/ipywidgets/src/libembed.ts new file mode 100644 index 000000000000..8fcc765f08c2 --- /dev/null +++ b/src/ipywidgets/src/libembed.ts @@ -0,0 +1,91 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +declare let __webpack_public_path__: string; +// tslint:disable: no-var-requires no-require-imports no-any +// eslint-disable-next-line prefer-const +__webpack_public_path__ = (window as any).__jupyter_widgets_assets_path__ || __webpack_public_path__; + +import '@phosphor/widgets/style/index.css'; +import 'font-awesome/css/font-awesome.css'; + +import '@jupyter-widgets/controls/css/widgets.css'; + +// Load json schema validator +const Ajv = require('ajv'); +import { WidgetManager } from './manager'; +const widget_state_schema = require('@jupyter-widgets/schema').v2.state; +const widget_view_schema = require('@jupyter-widgets/schema').v2.view; + +const ajv = new Ajv(); +const model_validate = ajv.compile(widget_state_schema); +const view_validate = ajv.compile(widget_view_schema); + +/** + * Render the inline widgets inside a DOM element. + * + * @param managerFactory A function that returns a new WidgetManager + * @param element (default document.documentElement) The document element in which to process for widget state. + */ +export async function renderWidgets( + managerFactory: () => WidgetManager, + element: HTMLElement = document.documentElement +): Promise<void> { + const tags = element.querySelectorAll('script[type="application/vnd.jupyter.widget-state+json"]'); + await Promise.all( + Array.from(tags).map(async (t) => renderManager(element, JSON.parse(t.innerHTML), managerFactory)) + ); +} + +/** + * Create a widget manager for a given widget state. + * + * @param element The DOM element to search for widget view state script tags + * @param widgetState The widget manager state + * @param managerFactory The widget manager factory + * + * #### Notes + * + * Widget view state should be in script tags with type + * "application/vnd.jupyter.widget-view+json". Any such script tag containing a + * model id the manager knows about is replaced with a rendered view. + * Additionally, if the script tag has a prior img sibling with class + * 'jupyter-widget', then that img tag is deleted. + */ +async function renderManager( + element: HTMLElement, + widgetState: any, + managerFactory: () => WidgetManager +): Promise<void> { + const valid = model_validate(widgetState); + if (!valid) { + throw new Error(`Model state has errors: ${model_validate.errors}`); + } + const manager = managerFactory(); + const models = await manager.set_state(widgetState); + const tags = element.querySelectorAll('script[type="application/vnd.jupyter.widget-view+json"]'); + await Promise.all( + Array.from(tags).map(async (viewtag) => { + const widgetViewObject = JSON.parse(viewtag.innerHTML); + const valid2 = view_validate(widgetViewObject); + if (!valid2) { + throw new Error(`View state has errors: ${view_validate.errors}`); + } + const model_id: string = widgetViewObject.model_id; + const model = models.find((item) => item.model_id === model_id); + if (model !== undefined && viewtag.parentElement !== null) { + const prev = viewtag.previousElementSibling; + if (prev && prev.tagName === 'img' && prev.classList.contains('jupyter-widget')) { + viewtag.parentElement.removeChild(prev); + } + const widgetTag = document.createElement('div'); + widgetTag.className = 'widget-subarea'; + viewtag.parentElement.insertBefore(widgetTag, viewtag); + const view = await manager.create_view(model, { node: widgetTag }); + manager.display_view('display_view', view, {}).catch((x) => { + window.console.error(x); + }); + } + }) + ); +} diff --git a/src/ipywidgets/src/manager.ts b/src/ipywidgets/src/manager.ts new file mode 100644 index 000000000000..b2a2a2a5d5b7 --- /dev/null +++ b/src/ipywidgets/src/manager.ts @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { shims } from '@jupyter-widgets/base'; +import * as jupyterlab from '@jupyter-widgets/jupyterlab-manager'; +import { RenderMimeRegistry, standardRendererFactories } from '@jupyterlab/rendermime'; +import { Kernel } from '@jupyterlab/services'; +import { Widget } from '@phosphor/widgets'; +import { DocumentContext } from './documentContext'; +import { requireLoader } from './widgetLoader'; + +export const WIDGET_MIMETYPE = 'application/vnd.jupyter.widget-view+json'; + +// tslint:disable: no-any +// Source borrowed from https://github.com/jupyter-widgets/ipywidgets/blob/master/examples/web3/src/manager.ts + +// These widgets can always be loaded from requirejs (as it is bundled). +const widgetsRegisteredInRequireJs = ['@jupyter-widgets/controls', '@jupyter-widgets/base', '@jupyter-widgets/output']; + +export class WidgetManager extends jupyterlab.WidgetManager { + public kernel: Kernel.IKernelConnection; + public el: HTMLElement; + + constructor( + kernel: Kernel.IKernelConnection, + el: HTMLElement, + private readonly scriptLoader: { + readonly widgetsRegisteredInRequireJs: Readonly<Set<string>>; + errorHandler(className: string, moduleName: string, moduleVersion: string, error: any): void; + loadWidgetScript(moduleName: string, moduleVersion: string): Promise<void>; + successHandler(className: string, moduleName: string, moduleVersion: string): void; + } + ) { + super( + new DocumentContext(kernel), + new RenderMimeRegistry({ + initialFactories: standardRendererFactories + }), + { saveState: false } + ); + this.kernel = kernel; + this.el = el; + this.rendermime.addFactory( + { + safe: false, + mimeTypes: [WIDGET_MIMETYPE], + createRenderer: (options) => new jupyterlab.WidgetRenderer(options, this) + }, + 0 + ); + + kernel.registerCommTarget(this.comm_target_name, async (comm, msg) => { + const oldComm = new shims.services.Comm(comm); + return this.handle_comm_open(oldComm, msg) as Promise<any>; + }); + } + + /** + * Create a comm. + */ + public async _create_comm( + target_name: string, + model_id: string, + data?: any, + metadata?: any + ): Promise<shims.services.Comm> { + const comm = this.kernel.connectToComm(target_name, model_id); + if (data || metadata) { + comm.open(data, metadata); + } + return Promise.resolve(new shims.services.Comm(comm)); + } + + /** + * Get the currently-registered comms. + */ + public _get_comm_info(): Promise<any> { + return this.kernel + .requestCommInfo({ target: this.comm_target_name }) + .then((reply) => (reply.content as any).comms); + } + public async display_view(msg: any, view: Backbone.View<Backbone.Model>, options: any): Promise<Widget> { + const widget = await super.display_view(msg, view, options); + const element = options.node ? (options.node as HTMLElement) : this.el; + // When do we detach? + if (element) { + Widget.attach(widget, element); + } + return widget; + } + public async restoreWidgets(): Promise<void> { + // Disabled for now. + // This throws errors if enabled, can be added later. + } + + // @ts-ignore https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-rc/#properties-overridding-accessors-and-vice-versa-is-an-error + public get onUnhandledIOPubMessage() { + return super.onUnhandledIOPubMessage; + } + + protected async loadClass(className: string, moduleName: string, moduleVersion: string): Promise<any> { + // Call the base class to try and load. If that fails, look locally + window.console.log(`WidgetManager: Loading class ${className}:${moduleName}:${moduleVersion}`); + // tslint:disable-next-line: no-unnecessary-local-variable + const result = await super + .loadClass(className, moduleName, moduleVersion) + .then((r) => { + this.sendSuccess(className, moduleName, moduleVersion); + return r; + }) + .catch(async (originalException) => { + try { + const loadModuleFromRequirejs = + widgetsRegisteredInRequireJs.includes(moduleName) || + this.scriptLoader.widgetsRegisteredInRequireJs.has(moduleName); + + if (!loadModuleFromRequirejs) { + // If not loading from requirejs, then check if we can. + // Notify the script loader that we need to load the widget module. + // If possible the loader will locate and register that in requirejs for things to start working. + await this.scriptLoader.loadWidgetScript(moduleName, moduleVersion); + } + const m = await requireLoader(moduleName); + if (m && m[className]) { + this.sendSuccess(className, moduleName, moduleVersion); + return m[className]; + } + throw originalException; + } catch (ex) { + this.sendError(className, moduleName, moduleVersion, originalException); + throw originalException; + } + }); + + return result; + } + private sendSuccess(className: string, moduleName: string, moduleVersion: string) { + try { + this.scriptLoader.successHandler(className, moduleName, moduleVersion); + } catch { + // Don't let script loader failures cause a break + } + } + + private sendError(className: string, moduleName: string, moduleVersion: string, originalException: Error) { + try { + this.scriptLoader.errorHandler(className, moduleName, moduleVersion, originalException); + } catch { + // Don't let script loader failures cause a break + } + } +} diff --git a/src/ipywidgets/src/signal.ts b/src/ipywidgets/src/signal.ts new file mode 100644 index 000000000000..ea7063145c37 --- /dev/null +++ b/src/ipywidgets/src/signal.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import { ISignal, Slot } from '@phosphor/signaling'; + +export class Signal<T, S> implements ISignal<T, S> { + private slots: Set<Slot<T, S>> = new Set<Slot<T, S>>(); + + // tslint:disable-next-line: no-any + public connect(slot: Slot<T, S>, thisArg?: any): boolean { + const bound = thisArg ? slot.bind(thisArg) : slot; + this.slots.add(bound); + return true; + } + // tslint:disable-next-line: no-any + public disconnect(slot: Slot<T, S>, thisArg?: any): boolean { + const bound = thisArg ? slot.bind(thisArg) : slot; + this.slots.delete(bound); + return true; + } + + public fire(sender: T, args: S): void { + this.slots.forEach((s) => s(sender, args)); + } +} diff --git a/src/ipywidgets/src/widgetLoader.ts b/src/ipywidgets/src/widgetLoader.ts new file mode 100644 index 000000000000..112e7c5b40cf --- /dev/null +++ b/src/ipywidgets/src/widgetLoader.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable-next-line: no-any +async function requirePromise(pkg: string | string[]): Promise<any> { + return new Promise((resolve, reject) => { + // tslint:disable-next-line: no-any + const requirejs = (window as any).requirejs; + if (requirejs === undefined) { + reject('Requirejs is needed, please ensure it is loaded on the page.'); + } else { + requirejs(pkg, resolve, reject); + } + }); +} +export function requireLoader(moduleName: string) { + return requirePromise([`${moduleName}`]); +} diff --git a/src/ipywidgets/src/widgets.css b/src/ipywidgets/src/widgets.css new file mode 100644 index 000000000000..1186770feb07 --- /dev/null +++ b/src/ipywidgets/src/widgets.css @@ -0,0 +1,9 @@ +/* Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +/* + * This example shows how to customize the theming and still compile the CSS + * down to something that all browsers support. + */ +@import "@jupyter-widgets/controls/css/widgets.css"; diff --git a/src/ipywidgets/tsconfig.json b/src/ipywidgets/tsconfig.json new file mode 100644 index 000000000000..bc5348c1c7d8 --- /dev/null +++ b/src/ipywidgets/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "esModuleInterop": true, + "incremental": true, + "moduleResolution": "node", + "noEmitOnError": true, + "preserveWatchOutput": true, + "resolveJsonModule": true, + "types": [], + "module": "esnext", + "target": "es2017", + "rootDir": "src", + "lib": ["es6", "es2018", "dom"], + "jsx": "react", + "sourceMap": true, + "outDir": "../../out/ipywidgets", + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "exclude": ["node_modules", "../node_modules"] +} diff --git a/src/ipywidgets/types/index.d.ts b/src/ipywidgets/types/index.d.ts new file mode 100644 index 000000000000..70343d7fc002 --- /dev/null +++ b/src/ipywidgets/types/index.d.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Added to allow compilation of backbone types pulled in from ipywidgets (@jupyterlab/widgets). +declare module JQuery { + type TriggeredEvent = any; +} diff --git a/src/ipywidgets/types/require.js.d.ts b/src/ipywidgets/types/require.js.d.ts new file mode 100644 index 000000000000..1ea978810180 --- /dev/null +++ b/src/ipywidgets/types/require.js.d.ts @@ -0,0 +1,414 @@ +// Type definitions for RequireJS 2.1.20 +// Project: http://requirejs.org/ +// Definitions by: Josh Baldwin <https://github.com/jbaldwin> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +/* +require-2.1.8.d.ts may be freely distributed under the MIT license. + +Copyright (c) 2013 Josh Baldwin https://github.com/jbaldwin/require.d.ts + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +*/ + +declare module 'module' { + var mod: { + config: () => any; + id: string; + uri: string; + }; + export = mod; +} + +interface RequireError extends Error { + /** + * The error ID that maps to an ID on a web page. + **/ + requireType: string; + + /** + * Required modules. + **/ + requireModules: string[] | null; + + /** + * The original error, if there is one (might be null). + **/ + originalError: Error; +} + +interface RequireShim { + /** + * List of dependencies. + **/ + deps?: string[]; + + /** + * Name the module will be exported as. + **/ + exports?: string; + + /** + * Initialize function with all dependcies passed in, + * if the function returns a value then that value is used + * as the module export value instead of the object + * found via the 'exports' string. + * @param dependencies + * @return + **/ + init?: (...dependencies: any[]) => any; +} + +interface RequireConfig { + /** + * The root path to use for all module lookups. + */ + baseUrl?: string; + + /** + * Path mappings for module names not found directly under + * baseUrl. + */ + paths?: { [key: string]: any }; + + /** + * Dictionary of Shim's. + * Can be of type RequireShim or string[] of dependencies + */ + shim?: { [key: string]: RequireShim | string[] }; + + /** + * For the given module prefix, instead of loading the + * module with the given ID, substitude a different + * module ID. + * + * @example + * requirejs.config({ + * map: { + * 'some/newmodule': { + * 'foo': 'foo1.2' + * }, + * 'some/oldmodule': { + * 'foo': 'foo1.0' + * } + * } + * }); + **/ + map?: { + [id: string]: { + [id: string]: string; + }; + }; + + /** + * Allows pointing multiple module IDs to a module ID that contains a bundle of modules. + * + * @example + * requirejs.config({ + * bundles: { + * 'primary': ['main', 'util', 'text', 'text!template.html'], + * 'secondary': ['text!secondary.html'] + * } + * }); + **/ + bundles?: { [key: string]: string[] }; + + /** + * AMD configurations, use module.config() to access in + * define() functions + **/ + config?: { [id: string]: {} }; + + /** + * Configures loading modules from CommonJS packages. + **/ + packages?: {}; + + /** + * The number of seconds to wait before giving up on loading + * a script. The default is 7 seconds. + **/ + waitSeconds?: number; + + /** + * A name to give to a loading context. This allows require.js + * to load multiple versions of modules in a page, as long as + * each top-level require call specifies a unique context string. + **/ + context?: string; + + /** + * An array of dependencies to load. + **/ + deps?: string[]; + + /** + * A function to pass to require that should be require after + * deps have been loaded. + * @param modules + **/ + callback?: (...modules: any[]) => void; + + /** + * If set to true, an error will be thrown if a script loads + * that does not call define() or have shim exports string + * value that can be checked. + **/ + enforceDefine?: boolean; + + /** + * If set to true, document.createElementNS() will be used + * to create script elements. + **/ + xhtml?: boolean; + + /** + * Extra query string arguments appended to URLs that RequireJS + * uses to fetch resources. Most useful to cache bust when + * the browser or server is not configured correctly. + * + * @example + * urlArgs: "bust= + (new Date()).getTime() + * + * As of RequireJS 2.2.0, urlArgs can be a function. If a + * function, it will receive the module ID and the URL as + * parameters, and it should return a string that will be added + * to the end of the URL. Return an empty string if no args. + * Be sure to take care of adding the '?' or '&' depending on + * the existing state of the URL. + * + * @example + + * requirejs.config({ + * urlArgs: function(id, url) { + * var args = 'v=1'; + * if (url.indexOf('view.html') !== -1) { + * args = 'v=2' + * } + * + * return (url.indexOf('?') === -1 ? '?' : '&') + args; + * } + * }); + **/ + urlArgs?: string | ((id: string, url: string) => string); + + /** + * Specify the value for the type="" attribute used for script + * tags inserted into the document by RequireJS. Default is + * "text/javascript". To use Firefox's JavasScript 1.8 + * features, use "text/javascript;version=1.8". + **/ + scriptType?: string; + + /** + * If set to true, skips the data-main attribute scanning done + * to start module loading. Useful if RequireJS is embedded in + * a utility library that may interact with other RequireJS + * library on the page, and the embedded version should not do + * data-main loading. + **/ + skipDataMain?: boolean; + + /** + * Allow extending requirejs to support Subresource Integrity + * (SRI). + **/ + onNodeCreated?: (node: HTMLScriptElement, config: RequireConfig, moduleName: string, url: string) => void; +} + +// todo: not sure what to do with this guy +interface RequireModule { + /** + * + **/ + config(): {}; +} + +/** + * + **/ +interface RequireMap { + /** + * + **/ + prefix: string; + + /** + * + **/ + name: string; + + /** + * + **/ + parentMap: RequireMap; + + /** + * + **/ + url: string; + + /** + * + **/ + originalName: string; + + /** + * + **/ + fullName: string; +} + +interface Require { + /** + * Configure require.js + **/ + config(config: RequireConfig): Require; + + /** + * CommonJS require call + * @param module Module to load + * @return The loaded module + */ + (module: string): any; + + /** + * Start the main app logic. + * Callback is optional. + * Can alternatively use deps and callback. + * @param modules Required modules to load. + **/ + (modules: string[]): void; + + /** + * @see Require() + * @param ready Called when required modules are ready. + **/ + (modules: string[], ready: Function): void; + + /** + * @see http://requirejs.org/docs/api.html#errbacks + * @param ready Called when required modules are ready. + **/ + (modules: string[], ready: Function, errback: Function): void; + + /** + * Generate URLs from require module + * @param module Module to URL + * @return URL string + **/ + toUrl(module: string): string; + + /** + * Returns true if the module has already been loaded and defined. + * @param module Module to check + **/ + defined(module: string): boolean; + + /** + * Returns true if the module has already been requested or is in the process of loading and should be available at some point. + * @param module Module to check + **/ + specified(module: string): boolean; + + /** + * On Error override + * @param err + **/ + onError(err: RequireError, errback?: (err: RequireError) => void): void; + + /** + * Undefine a module + * @param module Module to undefine. + **/ + undef(module: string): void; + + /** + * Semi-private function, overload in special instance of undef() + **/ + onResourceLoad(context: Object, map: RequireMap, depArray: RequireMap[]): void; +} + +interface RequireDefine { + /** + * Define Simple Name/Value Pairs + * @param config Dictionary of Named/Value pairs for the config. + **/ + (config: { [key: string]: any }): void; + + /** + * Define function. + * @param func: The function module. + **/ + (func: () => any): void; + + /** + * Define function with dependencies. + * @param deps List of dependencies module IDs. + * @param ready Callback function when the dependencies are loaded. + * callback param deps module dependencies + * callback return module definition + **/ + (deps: string[], ready: Function): void; + + /** + * Define module with simplified CommonJS wrapper. + * @param ready + * callback require requirejs instance + * callback exports exports object + * callback module module + * callback return module definition + **/ + (ready: (require: Require, exports: { [key: string]: any }, module: RequireModule) => any): void; + + /** + * Define a module with a name and dependencies. + * @param name The name of the module. + * @param deps List of dependencies module IDs. + * @param ready Callback function when the dependencies are loaded. + * callback deps module dependencies + * callback return module definition + **/ + (name: string, deps: string[], ready: Function): void; + + /** + * Define a module with a name. + * @param name The name of the module. + * @param ready Callback function when the dependencies are loaded. + * callback return module definition + **/ + (name: string, ready: Function): void; + + /** + * Used to allow a clear indicator that a global define function (as needed for script src browser loading) conforms + * to the AMD API, any global define function SHOULD have a property called "amd" whose value is an object. + * This helps avoid conflict with any other existing JavaScript code that could have defined a define() function + * that does not conform to the AMD API. + * define.amd.jQuery is specific to jQuery and indicates that the loader is able to account for multiple version + * of jQuery being loaded simultaneously. + */ + amd: Object; +} + +// Ambient declarations for 'require' and 'define' +declare var requirejs: Require; +declare var require: Require; +declare var define: RequireDefine; diff --git a/src/ipywidgets/webpack.config.js b/src/ipywidgets/webpack.config.js new file mode 100644 index 000000000000..420925a1fcf6 --- /dev/null +++ b/src/ipywidgets/webpack.config.js @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// Copied from https://github.com/jupyter-widgets/ipywidgets/blob/master/packages/html-manager/webpack.config.js + +const postcss = require('postcss'); +const webpack_bundle_analyzer = require('webpack-bundle-analyzer'); +const common = require('../../build/webpack/common'); +const path = require('path'); +const constants = require('../../build/constants'); +const outDir = path.join(__dirname, '..', '..', 'out', 'ipywidgets'); +const version = require(path.join( + __dirname, + '..', + '..', + 'node_modules', + '@jupyter-widgets', + 'jupyterlab-manager', + 'package.json' +)).version; +// Any build on the CI is considered production mode. +const isProdBuild = constants.isCI || process.argv.includes('--mode'); +const publicPath = 'https://unpkg.com/@jupyter-widgets/jupyterlab-manager@' + version + '/dist/'; +const rules = [ + { test: /\.css$/, use: ['style-loader', 'css-loader'] }, + // jquery-ui loads some images + { test: /\.(jpg|png|gif)$/, use: 'file-loader' }, + // required to load font-awesome + { + test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, + use: { + loader: 'url-loader', + options: { + limit: 10000, + mimetype: 'application/font-woff' + } + } + }, + { + test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, + use: { + loader: 'url-loader', + options: { + limit: 10000, + mimetype: 'application/font-woff' + } + } + }, + { + test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, + use: { + loader: 'url-loader', + options: { + limit: 10000, + mimetype: 'application/octet-stream' + } + } + }, + { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader' }, + { + test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, + use: { + loader: 'url-loader', + options: { + limit: 10000, + mimetype: 'image/svg+xml' + } + } + } +]; + +module.exports = [ + { + mode: isProdBuild ? 'production' : 'development', + devtool: isProdBuild ? 'source-map' : 'inline-source-map', + entry: path.join(outDir, 'index.js'), + output: { + filename: 'ipywidgets.js', + path: path.resolve(outDir, 'dist'), + publicPath: 'built/', + library: 'vscIPyWidgets', + libraryTarget: 'window' + }, + plugins: [...common.getDefaultPlugins('ipywidgets')], + module: { + rules: [ + { + test: /\.css$/, + use: [ + 'style-loader', + 'css-loader', + { + loader: 'postcss-loader', + options: { + plugins: [ + postcss.plugin('delete-tilde', function () { + return function (css) { + css.walkAtRules('import', function (rule) { + rule.params = rule.params.replace('~', ''); + }); + }; + }), + postcss.plugin('prepend', function () { + return function (css) { + css.prepend("@import '@jupyter-widgets/controls/css/labvariables.css';"); + }; + }), + require('postcss-import')(), + require('postcss-cssnext')() + ] + } + } + ] + }, + // jquery-ui loads some images + { test: /\.(jpg|png|gif)$/, use: 'file-loader' }, + // required to load font-awesome + { + test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, + use: { + loader: 'url-loader', + options: { + limit: 10000, + mimetype: 'application/font-woff' + } + } + }, + { + test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, + use: { + loader: 'url-loader', + options: { + limit: 10000, + mimetype: 'application/font-woff' + } + } + }, + { + test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, + use: { + loader: 'url-loader', + options: { + limit: 10000, + mimetype: 'application/octet-stream' + } + } + }, + { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader' }, + { + test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, + use: { + loader: 'url-loader', + options: { + limit: 10000, + mimetype: 'image/svg+xml' + } + } + } + ] + } + } +]; diff --git a/src/server/.gitignore b/src/server/.gitignore deleted file mode 100644 index 8e5962ee7274..000000000000 --- a/src/server/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -out -node_modules \ No newline at end of file diff --git a/src/server/.vscode/launch.json b/src/server/.vscode/launch.json deleted file mode 100644 index 46bbcce77423..000000000000 --- a/src/server/.vscode/launch.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": "0.1.0", - // List of configurations. Add new configurations or edit existing ones. - "configurations": [ - { - "name": "Attach", - "type": "node", - "request": "attach", - "port": 6004, - "sourceMaps": true, - "outDir": "../../out/server" - } - ] -} \ No newline at end of file diff --git a/src/server/.vscode/tasks.json b/src/server/.vscode/tasks.json deleted file mode 100644 index 6a159d6a5fa4..000000000000 --- a/src/server/.vscode/tasks.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "version": "0.1.0", - "command": "npm", - "isShellCommand": true, - "showOutput": "silent", - "args": ["run", "watch"], - "isWatching": true, - "problemMatcher": "$tsc-watch" -} \ No newline at end of file diff --git a/src/server/linters/linter.ts b/src/server/linters/linter.ts deleted file mode 100644 index 5756f4c19179..000000000000 --- a/src/server/linters/linter.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {Diagnostic} from 'vscode-languageserver'; - -export interface ILinter { - run(filePath: string): Promise<Diagnostic[]>; -} \ No newline at end of file diff --git a/src/server/linters/pylint.ts b/src/server/linters/pylint.ts deleted file mode 100644 index 6b4fd160c63f..000000000000 --- a/src/server/linters/pylint.ts +++ /dev/null @@ -1,153 +0,0 @@ -/*--------------------------------------------------------- - ** Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ -'use strict'; - -import * as child_process from 'child_process'; -import {ILinter} from './linter'; -import {ITextDocument, Diagnostic, DiagnosticSeverity, Range, Position} from 'vscode-languageserver'; -import * as path from 'path'; -import { exec } from 'child_process'; - -// const REGEX = '(?<line>\\d+),(?<col>\\d+),(?<type>\\w+),(\\w\\d+):(?<message>.*)\\r?(\\n|$)'; -var NamedRegexp = null; - -interface IPylintMessage { - line: number - column: number - code: string - message: string - type: string -} -function matchNamedRegEx(data, regex): IPylintMessage { - if (NamedRegexp === null) { - NamedRegexp = require('named-js-regexp'); - compiledRegexp = NamedRegexp(regex, "g"); - } - - var rawMatch = compiledRegexp.exec(data); - if (rawMatch !== null) { - return rawMatch.groups() - } - - return null; -} - -var compiledRegexp; - -const REGEX = '(?<line>\\d+),(?<column>\\d+),(?<type>\\w+),(?<code>\\w\\d+):(?<message>.*)\\r?(\\n|$)'; -const FILE_PROTOCOL = "file:///" -const PYLINT_COMMANDLINE = " --msg-template='{line},{column},{category},{msg_id}:{msg}' --reports=n --output-format=text"; -const PEP_COMMANDLINE = " --format='%(row)d,%(col)d,%(code)s,%(code)s:%(text)s'"; - -const PYLINT_CATEGORY_MAPPING = {}; -PYLINT_CATEGORY_MAPPING["refactor"] = DiagnosticSeverity.Hint; -PYLINT_CATEGORY_MAPPING["convention"] = DiagnosticSeverity.Hint; -PYLINT_CATEGORY_MAPPING["warning"] = DiagnosticSeverity.Warning; -PYLINT_CATEGORY_MAPPING["error"] = DiagnosticSeverity.Error; -PYLINT_CATEGORY_MAPPING["fatal"] = DiagnosticSeverity.Error; - -function uriToPath(pathValue: string): string { - if (pathValue.startsWith(FILE_PROTOCOL)) { - pathValue = pathValue.substring(FILE_PROTOCOL.length); - } - - return path.normalize(decodeURIComponent(pathValue)); -} -function getEnumValue(name:string):DiagnosticSeverity{ - return DiagnosticSeverity[name]; -} - -export class Linter { - public constructor(){ - - } - run(textDocument: ITextDocument, settings: any, isPep8:boolean): Promise<Diagnostic[]> { - PYLINT_CATEGORY_MAPPING["convention"] = getEnumValue(settings.linting.pylintCategorySeverity.convention); - PYLINT_CATEGORY_MAPPING["refactor"] = getEnumValue(settings.linting.pylintCategorySeverity.refactor); - PYLINT_CATEGORY_MAPPING["warning"] = getEnumValue(settings.linting.pylintCategorySeverity.warning); - PYLINT_CATEGORY_MAPPING["error"] = getEnumValue(settings.linting.pylintCategorySeverity.error); - PYLINT_CATEGORY_MAPPING["fatal"] = getEnumValue(settings.linting.pylintCategorySeverity.fatal); - - var filePath = uriToPath(textDocument.uri); - var txtDocumentLines = textDocument.getText().split(/\r?\n/g); - var dir = path.dirname(filePath); - var maxNumberOfProblems = settings.linting.maxNumberOfProblems; - var lintPath = isPep8 ? settings.linting.pep8Path : settings.linting.pylintPath; - var lintArgs = isPep8 ? PEP_COMMANDLINE : PYLINT_COMMANDLINE; - - var cmd: string = `${lintPath} ${lintArgs} ${filePath}`; - if (NamedRegexp === null) { - NamedRegexp = require('named-js-regexp') - compiledRegexp = NamedRegexp(REGEX, "g"); - } - - return new Promise<Diagnostic[]>((resolve, reject) => { - var dir = path.dirname(filePath); - //var cmd: string = `${PYLINT_COMMANDLINE} ${filePath}`; - exec(cmd,{cwd:dir}, (error: Error, stdout: string, stderr: string) => { - var outputLines = (stdout || "").split(/\r?\n/g); - if (outputLines.length === 0 || outputLines[0] === ""){ - var errorMessages = []; - if (error && error.message){ - errorMessages.push(`Error Message (${error.name}) : ${error.message}`); - } - if (stderr && stderr.length > 0){ - errorMessages.push(stderr + ''); - } - if (errorMessages.length === 0){ - return resolve([]) - } - var msg = ""; - if (isPep8){ - msg = "If Pep8 isn't used an not installed, you can turn its usage off from the setting 'python.linting.pep8Enabled'." + - "If Pep8 is used, but cannot be located, then configure the path in 'python.linting.pep8Path'"; - } - else { - msg = "If Pep8 isn't used an not installed, you can turn its usage off from the setting 'python.linting.pep8Enabled'." + - "If Pep8 is used, but cannot be located, then configure the path in 'python.linting.pep8Path'"; - - } - msg = msg + "\n" + errorMessages.join("\n"); - console.error(msg); - return resolve([]); - } - var diagnostics: Diagnostic[] = []; - outputLines.forEach(line=> { - if (diagnostics.length >= maxNumberOfProblems) { - return; - } - var match = matchNamedRegEx(line, REGEX); - if (match == null) { - return; - } - - try { - match.line = parseInt(<any>match.line); - match.column = parseInt(<any>match.column); - - var sourceLine = txtDocumentLines[match.line - 1]; - var sourceStart = sourceLine.substring(match.column - 1); - var endCol = txtDocumentLines[match.line - 1].length; - - //try to get the first word from the startig position - var possibleProblemWords = sourceStart.match(/\w+/g); - if (possibleProblemWords != null && possibleProblemWords.length > 0 && sourceStart.startsWith(possibleProblemWords[0])) { - endCol = match.column + possibleProblemWords[0].length; - } - - var range = Range.create(Position.create(match.line - 1, match.column), Position.create(match.line - 1, endCol)); - - var severity = isPep8? DiagnosticSeverity.Information : PYLINT_CATEGORY_MAPPING[match.type]; - diagnostics.push(Diagnostic.create(range, match.code + ":" + match.message, severity)); - } - catch (ex) { - var y = ""; - } - }); - - resolve(diagnostics); - }); - }); - } -} \ No newline at end of file diff --git a/src/server/package.json b/src/server/package.json deleted file mode 100644 index 07765fb9371c..000000000000 --- a/src/server/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "langauge-server-example", - "description": "Example implementation of a language server in node.", - "version": "0.0.1", - "author": "Don Jayamanne", - "publisher": "donjayamanne", - "license": "MIT", - "engines": { - "node": "*" - }, - "dependencies": { - "named-js-regexp": "^1.3.1", - "vscode-languageserver": "^1.1.0" - }, - "devDependencies": { - "typescript": "^1.6.2" - }, - "scripts": { - "compile": "tsc -p . && installServerIntoExtension ../../out ./package.json ./tsconfig.json && tsc -p .", - "watch": " tsc -p . && installServerIntoExtension ../../out ./package.json ./tsconfig.json && tsc --watch -p ." - } -} diff --git a/src/server/server.ts b/src/server/server.ts deleted file mode 100644 index 0323a6f306ff..000000000000 --- a/src/server/server.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* -------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - * ------------------------------------------------------------------------------------------ */ -'use strict'; - -import { -IPCMessageReader, IPCMessageWriter, -createConnection, IConnection, TextDocumentSyncKind, -TextDocuments, ITextDocument, Diagnostic, DiagnosticSeverity, -InitializeParams, InitializeResult, TextDocumentIdentifier, -CompletionItem, CompletionItemKind, PublishDiagnosticsParams -} from 'vscode-languageserver'; -import * as vscodeLanguageServer from 'vscode-languageserver'; -import * as pylinter from './linters/pylint'; - -// Create a connection for the server. The connection uses -// stdin / stdout for message passing -let connection: IConnection = createConnection(new IPCMessageReader(process), new IPCMessageWriter(process)); -var itHappened = ""; -// Create a simple text document manager. The text document manager -// supports full document sync only -let documents: TextDocuments = new TextDocuments(); -// Make the text document manager listen on the connection -// for open, change and close text document events -documents.listen(connection); - -// After the server has started the client sends an initilize request. The server receives -// in the passed params the rootPath of the workspace plus the client capabilites. -let workspaceRoot: string; -connection.onInitialize((params): InitializeResult => { - workspaceRoot = params.rootPath; - return { - capabilities: { - // Tell the client that the server works in FULL text document sync mode - textDocumentSync: documents.syncKind, - // Tell the client that the server support code complete - completionProvider: { - resolveProvider: true - } - } - } -}); - -// The content of a text document has changed. This event is emitted -// when the text document first opened or when its content has changed. -documents.onDidChangeContent((change) => { - validateTextDocument(change.document); -}); - -// The settings interface describe the server relevant settings part -interface Settings { - python: PythonSettings; -} - -// These are the example settings we defined in the client's package.json -// file -interface PythonSettings { - maxNumberOfProblems: number; -} -var pythonSettings: any = {}; -// The settings have changed. Is send on server activation -// as well. -connection.onDidChangeConfiguration((change) => { - let settings = <Settings>change.settings; - pythonSettings = settings.python; - // Revalidate any open text documents - documents.all().forEach(validateTextDocument); - -}); - -function validateTextDocument(textDocument: ITextDocument): void { - let diagnostics: Diagnostic[] = []; - var isWin = /^win/.test(process.platform); - if (!isWin) { - return; - } - if (!pythonSettings.linting.enabled || (!pythonSettings.linting.pylintEnabled && !pythonSettings.linting.pep8Enabled)) { - return; - } - - var pep8Messages = []; - var pylintMessages = []; - var pep8Done = false; - var pylintDone = false; - if (pythonSettings.linting.pylintEnabled) { - new pylinter.Linter().run(textDocument, pythonSettings, false).then((d) => { - pylintDone = true; - if (pythonSettings.linting.pep8Enabled) { - d.forEach(d=> d.message = d.message + " (pylint)"); - if (pep8Done) { - d.forEach(d=> pep8Messages.push(d)); - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: pep8Messages }); - } - else { - pylintMessages = d; - } - } - else { - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: d }); - } - }); - } - if (pythonSettings.linting.pep8Enabled) { - new pylinter.Linter().run(textDocument, pythonSettings, false).then((d) => { - pylintDone = true; - if (pythonSettings.linting.pylintEnabled) { - d.forEach(d=> d.message = d.message + " (pep8)"); - if (pylintDone) { - d.forEach(d=> pylintMessages.push(d)); - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: pylintMessages }); - } - else { - pep8Messages = d; - } - } - else { - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: d }); - } - }); - } -} -// - -connection.onDidChangeWatchedFiles((change) => { - // Monitored files have change in VSCode - // connection.console.log('We recevied an file change event'); -}); - -// -// // This handler provides the initial list of the completion items. -// connection.onCompletion((textDocumentPosition: TextDocumentIdentifier): CompletionItem[] => { -// // The pass parameter contains the position of the text document in -// // which code complete got requested. For the example we ignore this -// // info and always provide the same completion items. -// return []; -// // { -// // label: 'TypeScript', -// // kind: CompletionItemKind.Text, -// // data: 1 -// // }, -// // { -// // label: 'JavaScript', -// // kind: CompletionItemKind.Text, -// // data: 2 -// // } -// // ] -// }); -// -// // This handler resolve additional information for the item selected in -// // the completion list. -// connection.onCompletionResolve((item: CompletionItem): CompletionItem => { -// // if (item.data === 1) { -// // item.detail = 'TypeScript details', -// // item.documentation = 'TypeScript documentation' -// // } else if (item.data === 2) { -// // item.detail = 'JavaScript details', -// // item.documentation = 'JavaScript documentation' -// // } -// // return item; -// return item; -// }); - -/* -connection.onDidOpenTextDocument((params) => { - // A text document got opened in VSCode. - // params.uri uniquely identifies the document. For documents store on disk this is a file URI. - // params.text the initial full content of the document. - connection.console.log(`${params.uri} opened.`); -}); - -connection.onDidChangeTextDocument((params) => { - // The content of a text document did change in VSCode. - // params.uri uniquely identifies the document. - // params.contentChanges describe the content changes to the document. - connection.console.log(`${params.uri} changed: ${JSON.stringify(params.contentChanges)}`); -}); - -connection.onDidCloseTextDocument((params) => { - // A text document got closed in VSCode. - // params.uri uniquely identifies the document. - connection.console.log(`${params.uri} closed.`); -}); -*/ - -// Listen on the connection -connection.listen(); \ No newline at end of file diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json deleted file mode 100644 index 688775823941..000000000000 --- a/src/server/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "module": "commonjs", - "moduleResolution": "node", - "sourceMap": true, - "outDir": "../../out/server" - }, - "exclude": [ - "node_modules" - ] -} \ No newline at end of file diff --git a/src/server/typings/node.d.ts b/src/server/typings/node.d.ts deleted file mode 100644 index 4260ab2f6e36..000000000000 --- a/src/server/typings/node.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference path="../../../node_modules/vscode/typings/node.d.ts" /> \ No newline at end of file diff --git a/src/server/typings/vscode-typings.d.ts b/src/server/typings/vscode-typings.d.ts deleted file mode 100644 index 63c80890cbb0..000000000000 --- a/src/server/typings/vscode-typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference path="../../../node_modules/vscode/typings/index.d.ts" /> diff --git a/src/test/.vscode/launch.json b/src/test/.vscode/launch.json new file mode 100644 index 000000000000..a139754d2c07 --- /dev/null +++ b/src/test/.vscode/launch.json @@ -0,0 +1,37 @@ +{ + "version": "0.1.0", + "configurations": [ + { + "name": "launch a file", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, + { + "name": "attach to a local port", + "type": "python", + "request": "attach", + "port": 5678, + "host": "localhost", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ] + }, + { + "name": "attach to a local PID", + "type": "python", + "request": "attach", + "processId": "${env:CI_DEBUGPY_PROCESS_ID}", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ] + } + ] +} diff --git a/src/test/.vscode/launch.json.README b/src/test/.vscode/launch.json.README new file mode 100644 index 000000000000..644e7e47253a --- /dev/null +++ b/src/test/.vscode/launch.json.README @@ -0,0 +1,3 @@ +// These configs are used in full-stack integration tests. +// They mostly borrow from the code in src/client/debugger/extension/configuration/providers. + diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json new file mode 100644 index 000000000000..3d68e9e0284c --- /dev/null +++ b/src/test/.vscode/settings.json @@ -0,0 +1,31 @@ +{ + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": false, + "python.workspaceSymbols.enabled": false, + "python.testing.nosetestArgs": [], + "python.testing.pytestArgs": [], + "python.testing.unittestArgs": [ + "-s=./tests", + "-p=test_*.py", + "-v", + "-s", + ".", + "-p", + "*test*.py" + ], + "python.sortImports.args": [], + "python.linting.lintOnSave": false, + "python.linting.enabled": true, + "python.linting.pycodestyleEnabled": false, + "python.linting.prospectorEnabled": false, + "python.linting.pydocstyleEnabled": false, + "python.linting.pylamaEnabled": false, + "python.linting.mypyEnabled": false, + "python.linting.banditEnabled": false, + "python.formatting.provider": "yapf", + "python.linting.pylintUseMinimalCheckers": false, + // Do not set this to "Microsoft", else it will result in LS being downloaded on CI + // and that slows down tests significantly. We have other tests on CI for testing + // downloading of LS with this setting enabled. + "python.languageServer": "Jedi" +} diff --git a/src/test/.vscode/tags b/src/test/.vscode/tags new file mode 100644 index 000000000000..e4dc3f827c89 --- /dev/null +++ b/src/test/.vscode/tags @@ -0,0 +1,721 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +A ..\\pythonFiles\\autocomp\\pep526.py /^class A:$/;" kind:class line:13 +A ..\\pythonFiles\\definition\\await.test.py /^class A:$/;" kind:class line:3 +B ..\\pythonFiles\\autocomp\\pep526.py /^class B:$/;" kind:class line:17 +B ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^class B(Exception):$/;" kind:class line:19 +B ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^class B(Exception):$/;" kind:class line:19 +B ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^class B(Exception):$/;" kind:class line:19 +BaseRefactoring ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class BaseRefactoring(object):$/;" kind:class line:54 +BoundedQueue ..\\pythonFiles\\autocomp\\misc.py /^ class BoundedQueue(_Verbose):$/;" kind:class line:1250 +BoundedSemaphore ..\\pythonFiles\\autocomp\\misc.py /^def BoundedSemaphore(*args, **kwargs):$/;" kind:function line:497 +C ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^class C(B):$/;" kind:class line:22 +C ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^class C(B):$/;" kind:class line:22 +C ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^class C(B):$/;" kind:class line:22 +Change ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class Change():$/;" kind:class line:41 +ChangeType ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class ChangeType():$/;" kind:class line:32 +Child2Class ..\\pythonFiles\\symbolFiles\\childFile.py /^class Child2Class(object):$/;" kind:class line:5 +Class1 ..\\pythonFiles\\autocomp\\one.py /^class Class1(object):$/;" kind:class line:6 +Class1 ..\\pythonFiles\\definition\\one.py /^class Class1(object):$/;" kind:class line:6 +Condition ..\\pythonFiles\\autocomp\\misc.py /^def Condition(*args, **kwargs):$/;" kind:function line:242 +ConsumerThread ..\\pythonFiles\\autocomp\\misc.py /^ class ConsumerThread(Thread):$/;" kind:class line:1298 +D ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^class D(C):$/;" kind:class line:25 +D ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^class D(C):$/;" kind:class line:25 +D ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^class D(C):$/;" kind:class line:25 +DELETE ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ DELETE = 2$/;" kind:variable line:38 +DELETE ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ DELETE = 2$/;" kind:variable line:46 +Decorator ..\\pythonFiles\\autocomp\\deco.py /^class Decorator(metaclass=abc.ABCMeta):$/;" kind:class line:3 +DoSomething ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^class DoSomething():$/;" kind:class line:200 +DoSomething ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^class DoSomething():$/;" kind:class line:200 +DoSomething ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^class DoSomething():$/;" kind:class line:200 +EDIT ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ EDIT = 0$/;" kind:variable line:36 +EDIT ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ EDIT = 0$/;" kind:variable line:44 +Event ..\\pythonFiles\\autocomp\\misc.py /^def Event(*args, **kwargs):$/;" kind:function line:542 +Example3 ..\\pythonFiles\\formatting\\fileToFormat.py /^class Example3( object ):$/;" kind:class line:12 +ExtractMethodRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class ExtractMethodRefactor(ExtractVariableRefactor):$/;" kind:class line:144 +ExtractVariableRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class ExtractVariableRefactor(BaseRefactoring):$/;" kind:class line:120 +Foo ..\\multiRootWkspc\\disableLinters\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\parent\\child\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\workspace1\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\workspace2\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\multiRootWkspc\\workspace3\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\autocomp\\four.py /^class Foo(object):$/;" kind:class line:7 +Foo ..\\pythonFiles\\definition\\four.py /^class Foo(object):$/;" kind:class line:7 +Foo ..\\pythonFiles\\linting\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\flake8config\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\linting\\pylintconfig\\file.py /^class Foo(object):$/;" kind:class line:5 +Foo ..\\pythonFiles\\symbolFiles\\file.py /^class Foo(object):$/;" kind:class line:5 +Gaussian ..\\pythonFiles\\jupyter\\cells.py /^class Gaussian(object):$/;" kind:class line:100 +Lock ..\\pythonFiles\\autocomp\\misc.py /^Lock = _allocate_lock$/;" kind:variable line:112 +N ..\\pythonFiles\\jupyter\\cells.py /^N = 50$/;" kind:variable line:42 +NEW ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ NEW = 1$/;" kind:variable line:37 +NEW ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ NEW = 1$/;" kind:variable line:45 +PEP_484_style ..\\pythonFiles\\autocomp\\pep526.py /^PEP_484_style = SOMETHING # type: str$/;" kind:variable line:5 +PEP_526_style ..\\pythonFiles\\autocomp\\pep526.py /^PEP_526_style: str = "hello world"$/;" kind:variable line:3 +ProducerThread ..\\pythonFiles\\autocomp\\misc.py /^ class ProducerThread(Thread):$/;" kind:class line:1282 +RLock ..\\pythonFiles\\autocomp\\misc.py /^def RLock(*args, **kwargs):$/;" kind:function line:114 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:18 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\sorting\\noconfig\\after.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:12 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\sorting\\noconfig\\before.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:9 +ROPE_PROJECT_FOLDER ..\\pythonFiles\\sorting\\noconfig\\original.py /^ROPE_PROJECT_FOLDER = sys.argv[2]$/;" kind:variable line:9 +Random ..\\pythonFiles\\autocomp\\misc.py /^class Random(_random.Random):$/;" kind:class line:1331 +RefactorProgress ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class RefactorProgress():$/;" kind:class line:21 +RenameRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class RenameRefactor(BaseRefactoring):$/;" kind:class line:101 +RopeRefactoring ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^class RopeRefactoring(object):$/;" kind:class line:162 +Semaphore ..\\pythonFiles\\autocomp\\misc.py /^def Semaphore(*args, **kwargs):$/;" kind:function line:412 +TOOLS ..\\pythonFiles\\jupyter\\cells.py /^TOOLS = "pan,wheel_zoom,box_zoom,reset,save,box_select"$/;" kind:variable line:68 +Test_CheckMyApp ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^class Test_CheckMyApp:$/;" kind:class line:6 +Test_CheckMyApp ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^class Test_CheckMyApp:$/;" kind:class line:6 +Test_CheckMyApp ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^class Test_CheckMyApp:$/;" kind:class line:6 +Test_Current_Working_Directory ..\\pythonFiles\\testFiles\\cwd\\src\\tests\\test_cwd.py /^class Test_Current_Working_Directory(unittest.TestCase):$/;" kind:class line:6 +Test_NestedClassA ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ class Test_NestedClassA:$/;" kind:class line:13 +Test_NestedClassA ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^ class Test_NestedClassA:$/;" kind:class line:13 +Test_NestedClassA ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^ class Test_NestedClassA:$/;" kind:class line:13 +Test_Root_test1 ..\\pythonFiles\\testFiles\\single\\test_root.py /^class Test_Root_test1(unittest.TestCase):$/;" kind:class line:6 +Test_Root_test1 ..\\pythonFiles\\testFiles\\standard\\test_root.py /^class Test_Root_test1(unittest.TestCase):$/;" kind:class line:6 +Test_Root_test1 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\test_root.py /^class Test_Root_test1(unittest.TestCase):$/;" kind:class line:6 +Test_nested_classB_Of_A ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ class Test_nested_classB_Of_A:$/;" kind:class line:16 +Test_nested_classB_Of_A ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^ class Test_nested_classB_Of_A:$/;" kind:class line:16 +Test_nested_classB_Of_A ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^ class Test_nested_classB_Of_A:$/;" kind:class line:16 +Test_test1 ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test1 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test1 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_unittest_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test1 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 +Test_test2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^class Test_test2(unittest.TestCase):$/;" kind:class line:3 +Test_test2 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py /^class Test_test2(unittest.TestCase):$/;" kind:class line:3 +Test_test2a ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^class Test_test2a(unittest.TestCase):$/;" kind:class line:17 +Test_test2a ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py /^class Test_test2a(unittest.TestCase):$/;" kind:class line:17 +Test_test2a1 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ class Test_test2a1(unittest.TestCase):$/;" kind:class line:24 +Test_test2a1 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py /^ class Test_test2a1(unittest.TestCase):$/;" kind:class line:24 +Test_test3 ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py /^class Test_test3(unittest.TestCase):$/;" kind:class line:4 +Test_test3 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\unittest_three_test.py /^class Test_test3(unittest.TestCase):$/;" kind:class line:4 +Test_test_one_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^class Test_test_one_1(unittest.TestCase):$/;" kind:class line:3 +Test_test_one_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^class Test_test_one_2(unittest.TestCase):$/;" kind:class line:14 +Test_test_two_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^class Test_test_two_1(unittest.TestCase):$/;" kind:class line:3 +Test_test_two_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^class Test_test_two_2(unittest.TestCase):$/;" kind:class line:14 +Thread ..\\pythonFiles\\autocomp\\misc.py /^class Thread(_Verbose):$/;" kind:class line:640 +ThreadError ..\\pythonFiles\\autocomp\\misc.py /^ThreadError = thread.error$/;" kind:variable line:38 +Timer ..\\pythonFiles\\autocomp\\misc.py /^def Timer(*args, **kwargs):$/;" kind:function line:1046 +VERSION ..\\pythonFiles\\autocomp\\misc.py /^ VERSION = 3 # used by getstate\/setstate$/;" kind:variable line:1345 +WORKSPACE_ROOT ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:17 +WORKSPACE_ROOT ..\\pythonFiles\\sorting\\noconfig\\after.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:11 +WORKSPACE_ROOT ..\\pythonFiles\\sorting\\noconfig\\before.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:8 +WORKSPACE_ROOT ..\\pythonFiles\\sorting\\noconfig\\original.py /^WORKSPACE_ROOT = sys.argv[1]$/;" kind:variable line:8 +Workspace2Class ..\\pythonFiles\\symbolFiles\\workspace2File.py /^class Workspace2Class(object):$/;" kind:class line:5 +_BoundedSemaphore ..\\pythonFiles\\autocomp\\misc.py /^class _BoundedSemaphore(_Semaphore):$/;" kind:class line:515 +_Condition ..\\pythonFiles\\autocomp\\misc.py /^class _Condition(_Verbose):$/;" kind:class line:255 +_DummyThread ..\\pythonFiles\\autocomp\\misc.py /^class _DummyThread(Thread):$/;" kind:class line:1128 +_Event ..\\pythonFiles\\autocomp\\misc.py /^class _Event(_Verbose):$/;" kind:class line:552 +_MainThread ..\\pythonFiles\\autocomp\\misc.py /^class _MainThread(Thread):$/;" kind:class line:1088 +_RLock ..\\pythonFiles\\autocomp\\misc.py /^class _RLock(_Verbose):$/;" kind:class line:125 +_Semaphore ..\\pythonFiles\\autocomp\\misc.py /^class _Semaphore(_Verbose):$/;" kind:class line:423 +_Timer ..\\pythonFiles\\autocomp\\misc.py /^class _Timer(Thread):$/;" kind:class line:1058 +_VERBOSE ..\\pythonFiles\\autocomp\\misc.py /^_VERBOSE = False$/;" kind:variable line:53 +_Verbose ..\\pythonFiles\\autocomp\\misc.py /^ class _Verbose(object):$/;" kind:class line:57 +_Verbose ..\\pythonFiles\\autocomp\\misc.py /^ class _Verbose(object):$/;" kind:class line:79 +__all__ ..\\pythonFiles\\autocomp\\misc.py /^__all__ = ['activeCount', 'active_count', 'Condition', 'currentThread',$/;" kind:variable line:30 +__bootstrap ..\\pythonFiles\\autocomp\\misc.py /^ def __bootstrap(self):$/;" kind:member line:769 +__bootstrap_inner ..\\pythonFiles\\autocomp\\misc.py /^ def __bootstrap_inner(self):$/;" kind:member line:792 +__delete ..\\pythonFiles\\autocomp\\misc.py /^ def __delete(self):$/;" kind:member line:876 +__enter__ ..\\pythonFiles\\autocomp\\misc.py /^ __enter__ = acquire$/;" kind:variable line:185 +__enter__ ..\\pythonFiles\\autocomp\\misc.py /^ __enter__ = acquire$/;" kind:variable line:477 +__enter__ ..\\pythonFiles\\autocomp\\misc.py /^ def __enter__(self):$/;" kind:member line:285 +__exc_clear ..\\pythonFiles\\autocomp\\misc.py /^ __exc_clear = _sys.exc_clear$/;" kind:variable line:654 +__exc_info ..\\pythonFiles\\autocomp\\misc.py /^ __exc_info = _sys.exc_info$/;" kind:variable line:651 +__exit__ ..\\pythonFiles\\autocomp\\misc.py /^ def __exit__(self, *args):$/;" kind:member line:288 +__exit__ ..\\pythonFiles\\autocomp\\misc.py /^ def __exit__(self, t, v, tb):$/;" kind:member line:215 +__exit__ ..\\pythonFiles\\autocomp\\misc.py /^ def __exit__(self, t, v, tb):$/;" kind:member line:493 +__getstate__ ..\\pythonFiles\\autocomp\\misc.py /^ def __getstate__(self): # for pickle$/;" kind:member line:1422 +__init__ ..\\multiRootWkspc\\disableLinters\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\parent\\child\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\workspace1\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\workspace2\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\multiRootWkspc\\workspace3\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, limit):$/;" kind:member line:1252 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, queue, count):$/;" kind:member line:1300 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, queue, quota):$/;" kind:member line:1284 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:59 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:80 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self):$/;" kind:member line:1090 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self):$/;" kind:member line:1130 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, group=None, target=None, name=None,$/;" kind:member line:656 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, interval, function, args=[], kwargs={}):$/;" kind:member line:1067 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, lock=None, verbose=None):$/;" kind:member line:260 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, value=1, verbose=None):$/;" kind:member line:433 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, value=1, verbose=None):$/;" kind:member line:521 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:132 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, verbose=None):$/;" kind:member line:561 +__init__ ..\\pythonFiles\\autocomp\\misc.py /^ def __init__(self, x=None):$/;" kind:member line:1347 +__init__ ..\\pythonFiles\\autocomp\\one.py /^ def __init__(self, file_path=None, file_contents=None):$/;" kind:member line:14 +__init__ ..\\pythonFiles\\definition\\await.test.py /^ def __init__(self):$/;" kind:member line:4 +__init__ ..\\pythonFiles\\definition\\one.py /^ def __init__(self, file_path=None, file_contents=None):$/;" kind:member line:14 +__init__ ..\\pythonFiles\\formatting\\fileToFormat.py /^ def __init__ ( self, bar ):$/;" kind:member line:13 +__init__ ..\\pythonFiles\\jupyter\\cells.py /^ def __init__(self, mean=0.0, std=1, size=1000):$/;" kind:member line:104 +__init__ ..\\pythonFiles\\linting\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\flake8config\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self):$/;" kind:member line:164 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, filePath, fileMode=ChangeType.EDIT, diff=""):$/;" kind:member line:48 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, name='Task Name', message=None, percent=0):$/;" kind:member line:26 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Extract Method", progressCallback=None, startOff/;" kind:member line:146 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Extract Variable", progressCallback=None, startO/;" kind:member line:122 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Refactor", progressCallback=None):$/;" kind:member line:59 +__init__ ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def __init__(self, project, resource, name="Rename", progressCallback=None, startOffset=None/;" kind:member line:103 +__init__ ..\\pythonFiles\\symbolFiles\\childFile.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\symbolFiles\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\pythonFiles\\symbolFiles\\workspace2File.py /^ def __init__(self):$/;" kind:member line:8 +__init__.py ..\\pythonFiles\\autoimport\\two\\__init__.py 1;" kind:file line:1 +__initialized ..\\pythonFiles\\autocomp\\misc.py /^ __initialized = False$/;" kind:variable line:646 +__reduce__ ..\\pythonFiles\\autocomp\\misc.py /^ def __reduce__(self):$/;" kind:member line:1428 +__repr__ ..\\pythonFiles\\autocomp\\misc.py /^ def __repr__(self):$/;" kind:member line:138 +__repr__ ..\\pythonFiles\\autocomp\\misc.py /^ def __repr__(self):$/;" kind:member line:291 +__repr__ ..\\pythonFiles\\autocomp\\misc.py /^ def __repr__(self):$/;" kind:member line:713 +__revision__ ..\\multiRootWkspc\\disableLinters\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\parent\\child\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\workspace1\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\workspace2\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\multiRootWkspc\\workspace3\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\flake8config\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\linting\\pylintconfig\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\symbolFiles\\childFile.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\symbolFiles\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\pythonFiles\\symbolFiles\\workspace2File.py /^__revision__ = None$/;" kind:variable line:3 +__setstate__ ..\\pythonFiles\\autocomp\\misc.py /^ def __setstate__(self, state): # for pickle$/;" kind:member line:1425 +__stop ..\\pythonFiles\\autocomp\\misc.py /^ def __stop(self):$/;" kind:member line:866 +_acquire_restore ..\\pythonFiles\\autocomp\\misc.py /^ def _acquire_restore(self, count_owner):$/;" kind:member line:220 +_acquire_restore ..\\pythonFiles\\autocomp\\misc.py /^ def _acquire_restore(self, x):$/;" kind:member line:297 +_active ..\\pythonFiles\\autocomp\\misc.py /^_active = {} # maps thread id to Thread object$/;" kind:variable line:634 +_active_limbo_lock ..\\pythonFiles\\autocomp\\misc.py /^_active_limbo_lock = _allocate_lock()$/;" kind:variable line:633 +_after_fork ..\\pythonFiles\\autocomp\\misc.py /^def _after_fork():$/;" kind:function line:1211 +_allocate_lock ..\\pythonFiles\\autocomp\\misc.py /^_allocate_lock = thread.allocate_lock$/;" kind:variable line:36 +_block ..\\pythonFiles\\autocomp\\misc.py /^ def _block(self):$/;" kind:member line:705 +_count ..\\pythonFiles\\autocomp\\misc.py /^from itertools import count as _count$/;" kind:unknown line:14 +_counter ..\\pythonFiles\\autocomp\\misc.py /^_counter = _count().next$/;" kind:variable line:627 +_deque ..\\pythonFiles\\autocomp\\misc.py /^from collections import deque as _deque$/;" kind:unknown line:13 +_deserialize ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _deserialize(self, request):$/;" kind:member line:204 +_enumerate ..\\pythonFiles\\autocomp\\misc.py /^def _enumerate():$/;" kind:function line:1179 +_exitfunc ..\\pythonFiles\\autocomp\\misc.py /^ def _exitfunc(self):$/;" kind:member line:1100 +_extractMethod ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _extractMethod(self, filePath, start, end, newName):$/;" kind:member line:183 +_extractVariable ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _extractVariable(self, filePath, start, end, newName):$/;" kind:member line:168 +_figure_data ..\\pythonFiles\\jupyter\\cells.py /^ def _figure_data(self, format):$/;" kind:member line:112 +_format_exc ..\\pythonFiles\\autocomp\\misc.py /^from traceback import format_exc as _format_exc$/;" kind:unknown line:16 +_get_ident ..\\pythonFiles\\autocomp\\misc.py /^_get_ident = thread.get_ident$/;" kind:variable line:37 +_is_owned ..\\pythonFiles\\autocomp\\misc.py /^ def _is_owned(self):$/;" kind:member line:238 +_is_owned ..\\pythonFiles\\autocomp\\misc.py /^ def _is_owned(self):$/;" kind:member line:300 +_limbo ..\\pythonFiles\\autocomp\\misc.py /^_limbo = {}$/;" kind:variable line:635 +_newname ..\\pythonFiles\\autocomp\\misc.py /^def _newname(template="Thread-%d"):$/;" kind:function line:629 +_note ..\\pythonFiles\\autocomp\\misc.py /^ def _note(self, *args):$/;" kind:member line:82 +_note ..\\pythonFiles\\autocomp\\misc.py /^ def _note(self, format, *args):$/;" kind:member line:64 +_pickSomeNonDaemonThread ..\\pythonFiles\\autocomp\\misc.py /^def _pickSomeNonDaemonThread():$/;" kind:function line:1113 +_process_request ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _process_request(self, request):$/;" kind:member line:215 +_profile_hook ..\\pythonFiles\\autocomp\\misc.py /^_profile_hook = None$/;" kind:variable line:87 +_randbelow ..\\pythonFiles\\autocomp\\misc.py /^ def _randbelow(self, n, int=int, maxsize=1<<BPF, type=type,$/;" kind:member line:1483 +_release_save ..\\pythonFiles\\autocomp\\misc.py /^ def _release_save(self):$/;" kind:member line:228 +_release_save ..\\pythonFiles\\autocomp\\misc.py /^ def _release_save(self):$/;" kind:member line:294 +_repr_latex_ ..\\pythonFiles\\jupyter\\cells.py /^ def _repr_latex_(self):$/;" kind:member line:128 +_repr_png_ ..\\pythonFiles\\jupyter\\cells.py /^ def _repr_png_(self):$/;" kind:member line:123 +_reset_internal_locks ..\\pythonFiles\\autocomp\\misc.py /^ def _reset_internal_locks(self):$/;" kind:member line:566 +_reset_internal_locks ..\\pythonFiles\\autocomp\\misc.py /^ def _reset_internal_locks(self):$/;" kind:member line:697 +_serialize ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _serialize(self, identifier, results):$/;" kind:member line:198 +_set_daemon ..\\pythonFiles\\autocomp\\misc.py /^ def _set_daemon(self):$/;" kind:member line:1097 +_set_daemon ..\\pythonFiles\\autocomp\\misc.py /^ def _set_daemon(self):$/;" kind:member line:1143 +_set_daemon ..\\pythonFiles\\autocomp\\misc.py /^ def _set_daemon(self):$/;" kind:member line:709 +_set_ident ..\\pythonFiles\\autocomp\\misc.py /^ def _set_ident(self):$/;" kind:member line:789 +_shutdown ..\\pythonFiles\\autocomp\\misc.py /^_shutdown = _MainThread()._exitfunc$/;" kind:variable line:1200 +_sleep ..\\pythonFiles\\autocomp\\misc.py /^from time import time as _time, sleep as _sleep$/;" kind:unknown line:15 +_start_new_thread ..\\pythonFiles\\autocomp\\misc.py /^_start_new_thread = thread.start_new_thread$/;" kind:variable line:35 +_sys ..\\pythonFiles\\autocomp\\misc.py /^import sys as _sys$/;" kind:namespace line:3 +_test ..\\pythonFiles\\autocomp\\misc.py /^def _test():$/;" kind:function line:1248 +_time ..\\pythonFiles\\autocomp\\misc.py /^from time import time as _time, sleep as _sleep$/;" kind:unknown line:15 +_trace_hook ..\\pythonFiles\\autocomp\\misc.py /^_trace_hook = None$/;" kind:variable line:88 +_update_progress ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _update_progress(self):$/;" kind:member line:67 +_write_response ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def _write_response(self, response):$/;" kind:member line:230 +a ..\\pythonFiles\\autocomp\\pep526.py /^ a = 0$/;" kind:variable line:14 +a ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine2.py /^ a = 2$/;" kind:variable line:2 +a ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine4.py /^ a = 2$/;" kind:variable line:2 +a ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLineTab.py /^ a = 2$/;" kind:variable line:2 +acquire ..\\pythonFiles\\autocomp\\misc.py /^ def acquire(self, blocking=1):$/;" kind:member line:147 +acquire ..\\pythonFiles\\autocomp\\misc.py /^ def acquire(self, blocking=1):$/;" kind:member line:440 +activeCount ..\\pythonFiles\\autocomp\\misc.py /^def activeCount():$/;" kind:function line:1167 +active_count ..\\pythonFiles\\autocomp\\misc.py /^active_count = activeCount$/;" kind:variable line:1177 +add ..\\pythonFiles\\autocomp\\pep484.py /^def add(num1, num2) -> int:$/;" kind:function line:6 +after.py ..\\pythonFiles\\sorting\\noconfig\\after.py 1;" kind:file line:1 +after.py ..\\pythonFiles\\sorting\\withconfig\\after.py 1;" kind:file line:1 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:member line:263 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:function line:124 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:member line:263 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:function line:124 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:member line:263 +ask_ok ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):$/;" kind:function line:124 +await.test.py ..\\pythonFiles\\definition\\await.test.py 1;" kind:file line:1 +ax ..\\pythonFiles\\jupyter\\cells.py /^fig, ax = plt.subplots(subplot_kw=dict(axisbg='#EEEEEE'))$/;" kind:variable line:39 +b ..\\pythonFiles\\autocomp\\pep526.py /^ b: int = 0$/;" kind:variable line:18 +b ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine2.py /^ b = 3$/;" kind:variable line:3 +b ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine4.py /^ b = 3$/;" kind:variable line:3 +b ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLineTab.py /^ b = 3$/;" kind:variable line:3 +bar ..\\pythonFiles\\autocomp\\four.py /^ def bar():$/;" kind:member line:11 +bar ..\\pythonFiles\\definition\\four.py /^ def bar():$/;" kind:member line:11 +before.1.py ..\\pythonFiles\\sorting\\withconfig\\before.1.py 1;" kind:file line:1 +before.py ..\\pythonFiles\\sorting\\noconfig\\before.py 1;" kind:file line:1 +before.py ..\\pythonFiles\\sorting\\withconfig\\before.py 1;" kind:file line:1 +betavariate ..\\pythonFiles\\autocomp\\misc.py /^ def betavariate(self, alpha, beta):$/;" kind:member line:1862 +calculate_cash_flows ..\\pythonFiles\\definition\\decorators.py /^def calculate_cash_flows(remaining_loan_term, remaining_io_term,$/;" kind:function line:20 +cancel ..\\pythonFiles\\autocomp\\misc.py /^ def cancel(self):$/;" kind:member line:1075 +cells.py ..\\pythonFiles\\jupyter\\cells.py 1;" kind:file line:1 +childFile.py ..\\pythonFiles\\symbolFiles\\childFile.py 1;" kind:file line:1 +choice ..\\pythonFiles\\autocomp\\misc.py /^ def choice(self, seq):$/;" kind:member line:1513 +clear ..\\pythonFiles\\autocomp\\misc.py /^ def clear(self):$/;" kind:member line:590 +content ..\\pythonFiles\\autocomp\\doc.py /^ content = line.upper()$/;" kind:variable line:6 +ct ..\\pythonFiles\\autocomp\\two.py /^class ct:$/;" kind:class line:1 +ct ..\\pythonFiles\\definition\\two.py /^class ct:$/;" kind:class line:1 +currentThread ..\\pythonFiles\\autocomp\\misc.py /^def currentThread():$/;" kind:function line:1152 +current_thread ..\\pythonFiles\\autocomp\\misc.py /^current_thread = currentThread$/;" kind:variable line:1165 +daemon ..\\pythonFiles\\autocomp\\misc.py /^ def daemon(self):$/;" kind:member line:1009 +daemon ..\\pythonFiles\\autocomp\\misc.py /^ def daemon(self, daemonic):$/;" kind:member line:1025 +deco.py ..\\pythonFiles\\autocomp\\deco.py 1;" kind:file line:1 +decorators.py ..\\pythonFiles\\definition\\decorators.py 1;" kind:file line:1 +description ..\\pythonFiles\\autocomp\\one.py /^ description = "Run isort on modules registered in setuptools"$/;" kind:variable line:11 +description ..\\pythonFiles\\definition\\one.py /^ description = "Run isort on modules registered in setuptools"$/;" kind:variable line:11 +df ..\\pythonFiles\\jupyter\\cells.py /^df = df.cumsum()$/;" kind:variable line:87 +df ..\\pythonFiles\\jupyter\\cells.py /^df = pd.DataFrame(np.random.randn(1000, 4), index=ts.index,$/;" kind:variable line:85 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def divide(x, y):$/;" kind:member line:329 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def divide(x, y):$/;" kind:function line:190 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def divide(x, y):$/;" kind:member line:329 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def divide(x, y):$/;" kind:function line:190 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def divide(x, y):$/;" kind:member line:329 +divide ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def divide(x, y):$/;" kind:function line:190 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def divide(x, y):$/;" kind:function line:188 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def divide(x, y):$/;" kind:function line:199 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def divide(x, y):$/;" kind:function line:188 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def divide(x, y):$/;" kind:function line:199 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def divide(x, y):$/;" kind:function line:188 +divide ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def divide(x, y):$/;" kind:function line:199 +doc.py ..\\pythonFiles\\autocomp\\doc.py 1;" kind:file line:1 +dummy.py ..\\pythonFiles\\dummy.py 1;" kind:file line:1 +elseBlocks2.py ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py 1;" kind:file line:1 +elseBlocks4.py ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py 1;" kind:file line:1 +elseBlocksFirstLine2.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine2.py 1;" kind:file line:1 +elseBlocksFirstLine4.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLine4.py 1;" kind:file line:1 +elseBlocksFirstLineTab.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksFirstLineTab.py 1;" kind:file line:1 +elseBlocksTab.py ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py 1;" kind:file line:1 +enumerate ..\\pythonFiles\\autocomp\\misc.py /^def enumerate():$/;" kind:function line:1183 +example1 ..\\pythonFiles\\formatting\\fileToFormat.py /^def example1():$/;" kind:function line:3 +example2 ..\\pythonFiles\\formatting\\fileToFormat.py /^def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key(''));$/;" kind:function line:11 +expovariate ..\\pythonFiles\\autocomp\\misc.py /^ def expovariate(self, lambd):$/;" kind:member line:1670 +fig ..\\pythonFiles\\jupyter\\cells.py /^fig, ax = plt.subplots(subplot_kw=dict(axisbg='#EEEEEE'))$/;" kind:variable line:39 +file.py ..\\multiRootWkspc\\disableLinters\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\parent\\child\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\workspace1\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\workspace2\\file.py 1;" kind:file line:1 +file.py ..\\multiRootWkspc\\workspace3\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\flake8config\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\linting\\pylintconfig\\file.py 1;" kind:file line:1 +file.py ..\\pythonFiles\\symbolFiles\\file.py 1;" kind:file line:1 +fileToFormat.py ..\\pythonFiles\\formatting\\fileToFormat.py 1;" kind:file line:1 +five.py ..\\pythonFiles\\autocomp\\five.py 1;" kind:file line:1 +five.py ..\\pythonFiles\\definition\\five.py 1;" kind:file line:1 +four.py ..\\pythonFiles\\autocomp\\four.py 1;" kind:file line:1 +four.py ..\\pythonFiles\\definition\\four.py 1;" kind:file line:1 +fun ..\\pythonFiles\\autocomp\\two.py /^ def fun():$/;" kind:member line:2 +fun ..\\pythonFiles\\definition\\two.py /^ def fun():$/;" kind:member line:2 +function1 ..\\pythonFiles\\definition\\one.py /^def function1():$/;" kind:function line:33 +function2 ..\\pythonFiles\\definition\\one.py /^def function2():$/;" kind:function line:37 +function3 ..\\pythonFiles\\definition\\one.py /^def function3():$/;" kind:function line:40 +function4 ..\\pythonFiles\\definition\\one.py /^def function4():$/;" kind:function line:43 +gammavariate ..\\pythonFiles\\autocomp\\misc.py /^ def gammavariate(self, alpha, beta):$/;" kind:member line:1737 +gauss ..\\pythonFiles\\autocomp\\misc.py /^ def gauss(self, mu, sigma):$/;" kind:member line:1809 +get ..\\pythonFiles\\autocomp\\misc.py /^ def get(self):$/;" kind:member line:1271 +getName ..\\pythonFiles\\autocomp\\misc.py /^ def getName(self):$/;" kind:member line:1038 +getstate ..\\pythonFiles\\autocomp\\misc.py /^ def getstate(self):$/;" kind:member line:1388 +greeting ..\\pythonFiles\\autocomp\\pep484.py /^def greeting(name: str) -> str:$/;" kind:function line:2 +hoverTest.py ..\\pythonFiles\\autocomp\\hoverTest.py 1;" kind:file line:1 +ident ..\\pythonFiles\\autocomp\\misc.py /^ def ident(self):$/;" kind:member line:984 +identity ..\\pythonFiles\\definition\\decorators.py /^def identity(ob):$/;" kind:function line:1 +imp.py ..\\pythonFiles\\autocomp\\imp.py 1;" kind:file line:1 +instant_print ..\\pythonFiles\\autocomp\\lamb.py /^instant_print = lambda x: [print(x), sys.stdout.flush(), sys.stderr.flush()]$/;" kind:function line:1 +isAlive ..\\pythonFiles\\autocomp\\misc.py /^ def isAlive(self):$/;" kind:member line:995 +isDaemon ..\\pythonFiles\\autocomp\\misc.py /^ def isDaemon(self):$/;" kind:member line:1032 +isSet ..\\pythonFiles\\autocomp\\misc.py /^ def isSet(self):$/;" kind:member line:570 +is_alive ..\\pythonFiles\\autocomp\\misc.py /^ is_alive = isAlive$/;" kind:variable line:1006 +is_set ..\\pythonFiles\\autocomp\\misc.py /^ is_set = isSet$/;" kind:variable line:574 +join ..\\pythonFiles\\autocomp\\misc.py /^ def join(self, timeout=None):$/;" kind:member line:1146 +join ..\\pythonFiles\\autocomp\\misc.py /^ def join(self, timeout=None):$/;" kind:member line:911 +lamb.py ..\\pythonFiles\\autocomp\\lamb.py 1;" kind:file line:1 +local ..\\pythonFiles\\autocomp\\misc.py /^ from thread import _local as local$/;" kind:unknown line:1206 +lognormvariate ..\\pythonFiles\\autocomp\\misc.py /^ def lognormvariate(self, mu, sigma):$/;" kind:member line:1658 +meth1 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfChild ..\\pythonFiles\\symbolFiles\\childFile.py /^ def meth1OfChild(self, arg):$/;" kind:member line:11 +meth1OfWorkspace2 ..\\pythonFiles\\symbolFiles\\workspace2File.py /^ def meth1OfWorkspace2(self, arg):$/;" kind:member line:11 +meth2 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth2 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth3 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth4 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth5 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth6 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth7 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\multiRootWkspc\\disableLinters\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\parent\\child\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\workspace1\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\workspace2\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\multiRootWkspc\\workspace3\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\flake8config\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\pycodestyleconfig\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\pydocstyleconfig27\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\linting\\pylintconfig\\file.py /^ def meth8(self):$/;" kind:member line:80 +meth8 ..\\pythonFiles\\symbolFiles\\file.py /^ def meth8(self):$/;" kind:member line:80 +method1 ..\\pythonFiles\\autocomp\\one.py /^ def method1(self):$/;" kind:member line:18 +method1 ..\\pythonFiles\\definition\\one.py /^ def method1(self):$/;" kind:member line:18 +method2 ..\\pythonFiles\\autocomp\\one.py /^ def method2(self):$/;" kind:member line:24 +method2 ..\\pythonFiles\\definition\\one.py /^ def method2(self):$/;" kind:member line:24 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def minus():$/;" kind:member line:287 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def minus():$/;" kind:function line:148 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def minus():$/;" kind:member line:287 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def minus():$/;" kind:function line:148 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def minus():$/;" kind:member line:287 +minus ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def minus():$/;" kind:function line:148 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def minus():$/;" kind:function line:100 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def minus():$/;" kind:function line:91 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def minus():$/;" kind:function line:100 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def minus():$/;" kind:function line:91 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def minus():$/;" kind:function line:100 +minus ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def minus():$/;" kind:function line:91 +misc.py ..\\pythonFiles\\autocomp\\misc.py 1;" kind:file line:1 +mpl ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib as mpl$/;" kind:namespace line:4 +mpl ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib as mpl$/;" kind:namespace line:94 +myfunc ..\\pythonFiles\\definition\\decorators.py /^def myfunc():$/;" kind:function line:5 +name ..\\pythonFiles\\autocomp\\misc.py /^ def name(self):$/;" kind:member line:968 +name ..\\pythonFiles\\autocomp\\misc.py /^ def name(self, name):$/;" kind:member line:979 +non_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:10 +non_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:33 +non_parametrized_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:33 +non_parametrized_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_another_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:10 +non_parametrized_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:33 +normalvariate ..\\pythonFiles\\autocomp\\misc.py /^ def normalvariate(self, mu, sigma):$/;" kind:member line:1633 +notify ..\\pythonFiles\\autocomp\\misc.py /^ def notify(self, n=1):$/;" kind:member line:373 +notifyAll ..\\pythonFiles\\autocomp\\misc.py /^ def notifyAll(self):$/;" kind:member line:400 +notify_all ..\\pythonFiles\\autocomp\\misc.py /^ notify_all = notifyAll$/;" kind:variable line:409 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:34 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:5 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:63 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:78 +np ..\\pythonFiles\\jupyter\\cells.py /^import numpy as np$/;" kind:namespace line:97 +obj ..\\pythonFiles\\autocomp\\one.py /^obj = Class1()$/;" kind:variable line:30 +obj ..\\pythonFiles\\definition\\one.py /^obj = Class1()$/;" kind:variable line:30 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:109 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:131 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:149 +onRefactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def onRefactor(self):$/;" kind:member line:94 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def one():$/;" kind:function line:134 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def one():$/;" kind:function line:150 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def one():$/;" kind:function line:134 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def one():$/;" kind:function line:150 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def one():$/;" kind:function line:134 +one ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def one():$/;" kind:function line:150 +one.py ..\\pythonFiles\\autocomp\\one.py 1;" kind:file line:1 +one.py ..\\pythonFiles\\autoimport\\one.py 1;" kind:file line:1 +one.py ..\\pythonFiles\\definition\\one.py 1;" kind:file line:1 +one.py ..\\pythonFiles\\docstrings\\one.py 1;" kind:file line:1 +original.1.py ..\\pythonFiles\\sorting\\withconfig\\original.1.py 1;" kind:file line:1 +original.py ..\\pythonFiles\\sorting\\noconfig\\original.py 1;" kind:file line:1 +original.py ..\\pythonFiles\\sorting\\withconfig\\original.py 1;" kind:file line:1 +p1 ..\\pythonFiles\\jupyter\\cells.py /^p1 = figure(title="Legend Example", tools=TOOLS)$/;" kind:variable line:70 +parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def parametrized_username():$/;" kind:function line:6 +parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def parametrized_username():$/;" kind:function line:29 +parametrized_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^def parametrized_username():$/;" kind:function line:29 +parametrized_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_another_pytest.py /^def parametrized_username():$/;" kind:function line:6 +parametrized_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^def parametrized_username():$/;" kind:function line:29 +paretovariate ..\\pythonFiles\\autocomp\\misc.py /^ def paretovariate(self, alpha):$/;" kind:member line:1880 +pd ..\\pythonFiles\\jupyter\\cells.py /^import pandas as pd$/;" kind:namespace line:77 +pep484.py ..\\pythonFiles\\autocomp\\pep484.py 1;" kind:file line:1 +pep526.py ..\\pythonFiles\\autocomp\\pep526.py 1;" kind:file line:1 +plain.py ..\\pythonFiles\\shebang\\plain.py 1;" kind:file line:1 +plt ..\\pythonFiles\\jupyter\\cells.py /^from matplotlib import pyplot as plt$/;" kind:unknown line:80 +plt ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib.pyplot as plt$/;" kind:namespace line:3 +plt ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib.pyplot as plt$/;" kind:namespace line:33 +plt ..\\pythonFiles\\jupyter\\cells.py /^import matplotlib.pyplot as plt$/;" kind:namespace line:93 +print_hello ..\\pythonFiles\\hover\\stringFormat.py /^def print_hello(name):$/;" kind:function line:2 +put ..\\pythonFiles\\autocomp\\misc.py /^ def put(self, item):$/;" kind:member line:1260 +randint ..\\pythonFiles\\autocomp\\misc.py /^ def randint(self, a, b):$/;" kind:member line:1477 +randrange ..\\pythonFiles\\autocomp\\misc.py /^ def randrange(self, start, stop=None, step=1, _int=int):$/;" kind:member line:1433 +refactor ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def refactor(self):$/;" kind:member line:87 +refactor.py ..\\pythonFiles\\refactoring\\standAlone\\refactor.py 1;" kind:file line:1 +release ..\\pythonFiles\\autocomp\\misc.py /^ def release(self):$/;" kind:member line:187 +release ..\\pythonFiles\\autocomp\\misc.py /^ def release(self):$/;" kind:member line:479 +release ..\\pythonFiles\\autocomp\\misc.py /^ def release(self):$/;" kind:member line:525 +rnd ..\\pythonFiles\\autocomp\\hoverTest.py /^rnd = random.Random()$/;" kind:variable line:7 +rnd2 ..\\pythonFiles\\autocomp\\hoverTest.py /^rnd2 = misc.Random()$/;" kind:variable line:12 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:1289 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:1305 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:1079 +run ..\\pythonFiles\\autocomp\\misc.py /^ def run(self):$/;" kind:member line:752 +sample ..\\pythonFiles\\autocomp\\misc.py /^ def sample(self, population, k):$/;" kind:member line:1543 +scatter ..\\pythonFiles\\jupyter\\cells.py /^scatter = ax.scatter(np.random.normal(size=N),$/;" kind:variable line:43 +seed ..\\pythonFiles\\autocomp\\misc.py /^ def seed(self, a=None, version=2):$/;" kind:member line:1356 +set ..\\pythonFiles\\autocomp\\misc.py /^ def set(self):$/;" kind:member line:576 +setDaemon ..\\pythonFiles\\autocomp\\misc.py /^ def setDaemon(self, daemonic):$/;" kind:member line:1035 +setName ..\\pythonFiles\\autocomp\\misc.py /^ def setName(self, name):$/;" kind:member line:1041 +setprofile ..\\pythonFiles\\autocomp\\misc.py /^def setprofile(func):$/;" kind:function line:90 +setstate ..\\pythonFiles\\autocomp\\misc.py /^ def setstate(self, state):$/;" kind:member line:1392 +settrace ..\\pythonFiles\\autocomp\\misc.py /^def settrace(func):$/;" kind:function line:100 +shebang.py ..\\pythonFiles\\shebang\\shebang.py 1;" kind:file line:1 +shebangEnv.py ..\\pythonFiles\\shebang\\shebangEnv.py 1;" kind:file line:1 +shebangInvalid.py ..\\pythonFiles\\shebang\\shebangInvalid.py 1;" kind:file line:1 +showMessage ..\\pythonFiles\\autocomp\\four.py /^def showMessage():$/;" kind:function line:19 +showMessage ..\\pythonFiles\\definition\\four.py /^def showMessage():$/;" kind:function line:19 +shuffle ..\\pythonFiles\\autocomp\\misc.py /^ def shuffle(self, x, random=None):$/;" kind:member line:1521 +start ..\\pythonFiles\\autocomp\\misc.py /^ def start(self):$/;" kind:member line:726 +stop ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def stop(self):$/;" kind:member line:84 +stringFormat.py ..\\pythonFiles\\hover\\stringFormat.py 1;" kind:file line:1 +t ..\\pythonFiles\\autocomp\\hoverTest.py /^t = misc.Thread()$/;" kind:variable line:15 +test ..\\pythonFiles\\definition\\await.test.py /^ async def test(self):$/;" kind:member line:7 +test ..\\pythonFiles\\sorting\\noconfig\\after.py /^def test():$/;" kind:function line:15 +test ..\\pythonFiles\\sorting\\noconfig\\before.py /^def test():$/;" kind:function line:12 +test ..\\pythonFiles\\sorting\\noconfig\\original.py /^def test():$/;" kind:function line:12 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def test():$/;" kind:member line:201 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def test():$/;" kind:function line:62 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def test():$/;" kind:member line:201 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def test():$/;" kind:function line:62 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def test():$/;" kind:member line:201 +test ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def test():$/;" kind:function line:62 +test2 ..\\pythonFiles\\definition\\await.test.py /^ async def test2(self):$/;" kind:member line:10 +test_1_1_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_1_1(self):$/;" kind:member line:4 +test_1_1_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_1_1_1(self):$/;" kind:member line:4 +test_1_1_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_1_2(self):$/;" kind:member line:7 +test_1_1_2 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_1_1_2(self):$/;" kind:member line:7 +test_1_1_3 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_1_3(self):$/;" kind:member line:11 +test_1_1_3 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_1_1_3(self):$/;" kind:member line:11 +test_1_2_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py /^ def test_1_2_1(self):$/;" kind:member line:15 +test_222A2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222A2(self):$/;" kind:member line:18 +test_222A2 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222A2(self):$/;" kind:member line:18 +test_222A2wow ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222A2wow(self):$/;" kind:member line:25 +test_222A2wow ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222A2wow(self):$/;" kind:member line:25 +test_222B2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222B2(self):$/;" kind:member line:21 +test_222B2 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222B2(self):$/;" kind:member line:21 +test_222B2wow ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_222B2wow(self):$/;" kind:member line:28 +test_222B2wow ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py /^ def test_222B2wow(self):$/;" kind:member line:28 +test_2_1_1 ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py /^ def test_2_1_1(self):$/;" kind:member line:15 +test_A ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py /^ def test_A(self):$/;" kind:member line:5 +test_A ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_unittest_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_one.py /^ def test_A(self):$/;" kind:member line:7 +test_A ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\unittest_three_test.py /^ def test_A(self):$/;" kind:member line:5 +test_A2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_A2(self):$/;" kind:member line:4 +test_A2 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py /^ def test_A2(self):$/;" kind:member line:4 +test_B ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py /^ def test_B(self):$/;" kind:member line:8 +test_B ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_unittest_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_one.py /^ def test_B(self):$/;" kind:member line:10 +test_B ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\unittest_three_test.py /^ def test_B(self):$/;" kind:member line:8 +test_B2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_B2(self):$/;" kind:member line:7 +test_B2 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py /^ def test_B2(self):$/;" kind:member line:7 +test_C2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_C2(self):$/;" kind:member line:10 +test_C2 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py /^ def test_C2(self):$/;" kind:member line:10 +test_D2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py /^ def test_D2(self):$/;" kind:member line:13 +test_D2 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py /^ def test_D2(self):$/;" kind:member line:13 +test_Root_A ..\\pythonFiles\\testFiles\\single\\test_root.py /^ def test_Root_A(self):$/;" kind:member line:7 +test_Root_A ..\\pythonFiles\\testFiles\\standard\\test_root.py /^ def test_Root_A(self):$/;" kind:member line:7 +test_Root_A ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\test_root.py /^ def test_Root_A(self):$/;" kind:member line:7 +test_Root_B ..\\pythonFiles\\testFiles\\single\\test_root.py /^ def test_Root_B(self):$/;" kind:member line:10 +test_Root_B ..\\pythonFiles\\testFiles\\standard\\test_root.py /^ def test_Root_B(self):$/;" kind:member line:10 +test_Root_B ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\test_root.py /^ def test_Root_B(self):$/;" kind:member line:10 +test_Root_c ..\\pythonFiles\\testFiles\\single\\test_root.py /^ def test_Root_c(self):$/;" kind:member line:14 +test_Root_c ..\\pythonFiles\\testFiles\\standard\\test_root.py /^ def test_Root_c(self):$/;" kind:member line:14 +test_Root_c ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\test_root.py /^ def test_Root_c(self):$/;" kind:member line:14 +test_another_pytest.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py 1;" kind:file line:1 +test_another_pytest.py ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_another_pytest.py 1;" kind:file line:1 +test_c ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py /^ def test_c(self):$/;" kind:member line:14 +test_c ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py /^ def test_c(self):$/;" kind:member line:14 +test_c ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_unittest_one.py /^ def test_c(self):$/;" kind:member line:14 +test_c ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_one.py /^ def test_c(self):$/;" kind:member line:14 +test_complex_check ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_complex_check(self):$/;" kind:member line:10 +test_complex_check ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^ def test_complex_check(self):$/;" kind:member line:10 +test_complex_check ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^ def test_complex_check(self):$/;" kind:member line:10 +test_complex_check2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_complex_check2(self):$/;" kind:member line:24 +test_complex_check2 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^ def test_complex_check2(self):$/;" kind:member line:24 +test_complex_check2 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^ def test_complex_check2(self):$/;" kind:member line:24 +test_cwd ..\\pythonFiles\\testFiles\\cwd\\src\\tests\\test_cwd.py /^ def test_cwd(self):$/;" kind:member line:7 +test_cwd.py ..\\pythonFiles\\testFiles\\cwd\\src\\tests\\test_cwd.py 1;" kind:file line:1 +test_d ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_d(self):$/;" kind:member line:17 +test_d ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^ def test_d(self):$/;" kind:member line:17 +test_d ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^ def test_d(self):$/;" kind:member line:17 +test_nested_class_methodB ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_nested_class_methodB(self):$/;" kind:member line:14 +test_nested_class_methodB ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^ def test_nested_class_methodB(self):$/;" kind:member line:14 +test_nested_class_methodB ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^ def test_nested_class_methodB(self):$/;" kind:member line:14 +test_nested_class_methodC ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_nested_class_methodC(self):$/;" kind:member line:19 +test_nested_class_methodC ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^ def test_nested_class_methodC(self):$/;" kind:member line:19 +test_nested_class_methodC ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^ def test_nested_class_methodC(self):$/;" kind:member line:19 +test_one.py ..\\pythonFiles\\testFiles\\single\\tests\\test_one.py 1;" kind:file line:1 +test_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:16 +test_parametrized_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:39 +test_parametrized_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:39 +test_parametrized_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_another_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:16 +test_parametrized_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:39 +test_pytest.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py 1;" kind:file line:1 +test_pytest.py ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py 1;" kind:file line:1 +test_pytest.py ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py 1;" kind:file line:1 +test_root.py ..\\pythonFiles\\testFiles\\single\\test_root.py 1;" kind:file line:1 +test_root.py ..\\pythonFiles\\testFiles\\standard\\test_root.py 1;" kind:file line:1 +test_root.py ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\test_root.py 1;" kind:file line:1 +test_simple_check ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_simple_check(self):$/;" kind:member line:8 +test_simple_check ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^ def test_simple_check(self):$/;" kind:member line:8 +test_simple_check ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^ def test_simple_check(self):$/;" kind:member line:8 +test_simple_check2 ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^ def test_simple_check2(self):$/;" kind:member line:22 +test_simple_check2 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^ def test_simple_check2(self):$/;" kind:member line:22 +test_simple_check2 ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^ def test_simple_check2(self):$/;" kind:member line:22 +test_unittest_one.py ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_one.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_one.py ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_one.py ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_one.py 1;" kind:file line:1 +test_unittest_two.py ..\\pythonFiles\\testFiles\\specificTest\\tests\\test_unittest_two.py 1;" kind:file line:1 +test_unittest_two.py ..\\pythonFiles\\testFiles\\standard\\tests\\test_unittest_two.py 1;" kind:file line:1 +test_unittest_two.py ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_unittest_two.py 1;" kind:file line:1 +test_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_another_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:13 +test_username ..\\pythonFiles\\testFiles\\standard\\tests\\test_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:36 +test_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\other\\test_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:36 +test_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_another_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:13 +test_username ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\test_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:36 +testthis ..\\pythonFiles\\definition\\await.test.py /^async def testthis():$/;" kind:function line:13 +three.py ..\\pythonFiles\\autocomp\\three.py 1;" kind:file line:1 +three.py ..\\pythonFiles\\autoimport\\two\\three.py 1;" kind:file line:1 +three.py ..\\pythonFiles\\definition\\three.py 1;" kind:file line:1 +triangular ..\\pythonFiles\\autocomp\\misc.py /^ def triangular(self, low=0.0, high=1.0, mode=None):$/;" kind:member line:1611 +tryBlocks2.py ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py 1;" kind:file line:1 +tryBlocks4.py ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py 1;" kind:file line:1 +tryBlocksTab.py ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py 1;" kind:file line:1 +ts ..\\pythonFiles\\jupyter\\cells.py /^ts = pd.Series(np.random.randn(1000),$/;" kind:variable line:82 +ts ..\\pythonFiles\\jupyter\\cells.py /^ts = ts.cumsum()$/;" kind:variable line:84 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^ def two():$/;" kind:member line:308 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^def two():$/;" kind:function line:169 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ def two():$/;" kind:member line:308 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^def two():$/;" kind:function line:169 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ def two():$/;" kind:member line:308 +two ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^def two():$/;" kind:function line:169 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def two():$/;" kind:function line:166 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def two():$/;" kind:function line:177 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def two():$/;" kind:function line:166 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def two():$/;" kind:function line:177 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def two():$/;" kind:function line:166 +two ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def two():$/;" kind:function line:177 +two.py ..\\pythonFiles\\autocomp\\two.py 1;" kind:file line:1 +two.py ..\\pythonFiles\\definition\\two.py 1;" kind:file line:1 +uniform ..\\pythonFiles\\autocomp\\misc.py /^ def uniform(self, a, b):$/;" kind:member line:1605 +unittest_three_test.py ..\\pythonFiles\\testFiles\\standard\\tests\\unittest_three_test.py 1;" kind:file line:1 +unittest_three_test.py ..\\pythonFiles\\testFiles\\unittestsWithConfigs\\tests\\unittest_three_test.py 1;" kind:file line:1 +user_options ..\\pythonFiles\\autocomp\\one.py /^ user_options = []$/;" kind:variable line:12 +user_options ..\\pythonFiles\\definition\\one.py /^ user_options = []$/;" kind:variable line:12 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:1 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:15 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:29 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:339 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks2.py /^var = 100$/;" kind:variable line:353 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^ var = 100$/;" kind:variable line:339 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^var = 100$/;" kind:variable line:1 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^var = 100$/;" kind:variable line:15 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocks4.py /^var = 100$/;" kind:variable line:29 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^ var = 100$/;" kind:variable line:339 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^var = 100$/;" kind:variable line:1 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^var = 100$/;" kind:variable line:15 +var ..\\pythonFiles\\typeFormatFiles\\elseBlocksTab.py /^var = 100$/;" kind:variable line:29 +vonmisesvariate ..\\pythonFiles\\autocomp\\misc.py /^ def vonmisesvariate(self, mu, kappa):$/;" kind:member line:1689 +wait ..\\pythonFiles\\autocomp\\misc.py /^ def wait(self, timeout=None):$/;" kind:member line:309 +wait ..\\pythonFiles\\autocomp\\misc.py /^ def wait(self, timeout=None):$/;" kind:member line:603 +watch ..\\pythonFiles\\refactoring\\standAlone\\refactor.py /^ def watch(self):$/;" kind:member line:234 +weibullvariate ..\\pythonFiles\\autocomp\\misc.py /^ def weibullvariate(self, alpha, beta):$/;" kind:member line:1889 +workspace2File.py ..\\pythonFiles\\symbolFiles\\workspace2File.py 1;" kind:file line:1 +x ..\\pythonFiles\\jupyter\\cells.py /^x = Gaussian(2.0, 1.0)$/;" kind:variable line:131 +x ..\\pythonFiles\\jupyter\\cells.py /^x = np.linspace(0, 20, 100)$/;" kind:variable line:7 +x ..\\pythonFiles\\jupyter\\cells.py /^x = np.linspace(0, 4 * np.pi, 100)$/;" kind:variable line:65 +y ..\\pythonFiles\\jupyter\\cells.py /^y = np.sin(x)$/;" kind:variable line:66 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def zero():$/;" kind:function line:110 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks2.py /^def zero():$/;" kind:function line:122 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def zero():$/;" kind:function line:110 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocks4.py /^def zero():$/;" kind:function line:122 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def zero():$/;" kind:function line:110 +zero ..\\pythonFiles\\typeFormatFiles\\tryBlocksTab.py /^def zero():$/;" kind:function line:122 diff --git a/src/test/activation/aaTesting.unit.test.ts b/src/test/activation/aaTesting.unit.test.ts new file mode 100644 index 000000000000..97cba28f3e03 --- /dev/null +++ b/src/test/activation/aaTesting.unit.test.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as TypeMoq from 'typemoq'; +import { AATesting } from '../../client/activation/aaTesting'; +import { ValidateABTesting } from '../../client/common/experiments/groups'; +import { IExperimentsManager } from '../../client/common/types'; + +suite('A/A Testing', () => { + let experiments: TypeMoq.IMock<IExperimentsManager>; + let aaTesting: AATesting; + setup(() => { + experiments = TypeMoq.Mock.ofType<IExperimentsManager>(); + aaTesting = new AATesting(experiments.object); + }); + + test('Send telemetry corresponding to the experiment user is in', async () => { + experiments + .setup((exp) => exp.sendTelemetryIfInExperiment(ValidateABTesting.experiment)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + experiments + .setup((exp) => exp.sendTelemetryIfInExperiment(ValidateABTesting.control)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + await aaTesting.activate(); + experiments.verifyAll(); + }); +}); diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts new file mode 100644 index 000000000000..c4fc941ed17b --- /dev/null +++ b/src/test/activation/activationManager.unit.test.ts @@ -0,0 +1,620 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, reset, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { TextDocument, Uri } from 'vscode'; +import { ExtensionActivationManager } from '../../client/activation/activationManager'; +import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../../client/activation/types'; +import { IApplicationDiagnostics } from '../../client/application/types'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { IActiveResourceService, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { PYTHON_LANGUAGE } from '../../client/common/constants'; +import { DeprecatePythonPath } from '../../client/common/experiments/groups'; +import { ExperimentsManager } from '../../client/common/experiments/manager'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IDisposable, IExperimentsManager, IInterpreterPathService } from '../../client/common/types'; +import { createDeferred, createDeferredFromPromise } from '../../client/common/utils/async'; +import { InterpreterSecurityService } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService'; +import { + IInterpreterAutoSelectionService, + IInterpreterSecurityService +} from '../../client/interpreter/autoSelection/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { InterpreterService } from '../../client/interpreter/interpreterService'; +import * as EnvFileTelemetry from '../../client/telemetry/envFileTelemetry'; +import { sleep } from '../core'; + +suite('Activation Manager', () => { + // tslint:disable:max-func-body-length no-any + suite('Language Server Activation - ActivationManager', () => { + class ExtensionActivationManagerTest extends ExtensionActivationManager { + // tslint:disable-next-line:no-unnecessary-override + public addHandlers() { + return super.addHandlers(); + } + // tslint:disable-next-line:no-unnecessary-override + public async initialize() { + return super.initialize(); + } + // tslint:disable-next-line:no-unnecessary-override + public addRemoveDocOpenedHandlers() { + super.addRemoveDocOpenedHandlers(); + } + } + let managerTest: ExtensionActivationManagerTest; + let workspaceService: IWorkspaceService; + let appDiagnostics: typemoq.IMock<IApplicationDiagnostics>; + let autoSelection: typemoq.IMock<IInterpreterAutoSelectionService>; + let interpreterService: IInterpreterService; + let activeResourceService: IActiveResourceService; + let documentManager: typemoq.IMock<IDocumentManager>; + let interpreterSecurityService: IInterpreterSecurityService; + let interpreterPathService: typemoq.IMock<IInterpreterPathService>; + let experiments: IExperimentsManager; + let activationService1: IExtensionActivationService; + let activationService2: IExtensionActivationService; + let fileSystem: IFileSystem; + setup(() => { + interpreterSecurityService = mock(InterpreterSecurityService); + experiments = mock(ExperimentsManager); + interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>(); + workspaceService = mock(WorkspaceService); + activeResourceService = mock(ActiveResourceService); + appDiagnostics = typemoq.Mock.ofType<IApplicationDiagnostics>(); + autoSelection = typemoq.Mock.ofType<IInterpreterAutoSelectionService>(); + interpreterService = mock(InterpreterService); + documentManager = typemoq.Mock.ofType<IDocumentManager>(); + activationService1 = mock(LanguageServerExtensionActivationService); + activationService2 = mock(LanguageServerExtensionActivationService); + fileSystem = mock(FileSystem); + interpreterPathService + .setup((i) => i.onDidChange(typemoq.It.isAny())) + .returns(() => typemoq.Mock.ofType<IDisposable>().object); + managerTest = new ExtensionActivationManagerTest( + [instance(activationService1), instance(activationService2)], + [], + documentManager.object, + instance(interpreterService), + autoSelection.object, + appDiagnostics.object, + instance(workspaceService), + instance(fileSystem), + instance(activeResourceService), + instance(experiments), + interpreterPathService.object, + instance(interpreterSecurityService) + ); + + sinon.stub(EnvFileTelemetry, 'sendActivationTelemetry').resolves(); + managerTest.evaluateAutoSelectedInterpreterSafety = () => Promise.resolve(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Initialize will add event handlers and will dispose them when running dispose', async () => { + const disposable = typemoq.Mock.ofType<IDisposable>(); + const disposable2 = typemoq.Mock.ofType<IDisposable>(); + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(() => disposable.object); + when(workspaceService.workspaceFolders).thenReturn([1 as any, 2 as any]); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + const eventDef = () => disposable2.object; + documentManager + .setup((d) => d.onDidOpenTextDocument) + .returns(() => eventDef) + .verifiable(typemoq.Times.once()); + + await managerTest.initialize(); + + verify(workspaceService.workspaceFolders).once(); + verify(workspaceService.hasWorkspaceFolders).once(); + verify(workspaceService.onDidChangeWorkspaceFolders).once(); + + documentManager.verifyAll(); + + disposable.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); + disposable2.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); + + managerTest.dispose(); + + disposable.verifyAll(); + disposable2.verifyAll(); + }); + test('Remove text document opened handler if there is only one workspace', async () => { + const disposable = typemoq.Mock.ofType<IDisposable>(); + const disposable2 = typemoq.Mock.ofType<IDisposable>(); + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(() => disposable.object); + when(workspaceService.workspaceFolders).thenReturn([1 as any, 2 as any]); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + const eventDef = () => disposable2.object; + documentManager + .setup((d) => d.onDidOpenTextDocument) + .returns(() => eventDef) + .verifiable(typemoq.Times.once()); + disposable.setup((d) => d.dispose()); + disposable2.setup((d) => d.dispose()); + + await managerTest.initialize(); + + verify(workspaceService.workspaceFolders).once(); + verify(workspaceService.hasWorkspaceFolders).once(); + verify(workspaceService.onDidChangeWorkspaceFolders).once(); + documentManager.verifyAll(); + disposable.verify((d) => d.dispose(), typemoq.Times.never()); + disposable2.verify((d) => d.dispose(), typemoq.Times.never()); + + when(workspaceService.workspaceFolders).thenReturn([]); + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + + await managerTest.initialize(); + + verify(workspaceService.hasWorkspaceFolders).twice(); + disposable.verify((d) => d.dispose(), typemoq.Times.never()); + disposable2.verify((d) => d.dispose(), typemoq.Times.once()); + + managerTest.dispose(); + + disposable.verify((d) => d.dispose(), typemoq.Times.atLeast(1)); + disposable2.verify((d) => d.dispose(), typemoq.Times.once()); + }); + test('Activate workspace specific to the resource in case of Multiple workspaces when a file is opened', async () => { + const disposable1 = typemoq.Mock.ofType<IDisposable>(); + const disposable2 = typemoq.Mock.ofType<IDisposable>(); + let fileOpenedHandler!: (e: TextDocument) => Promise<void>; + let workspaceFoldersChangedHandler!: Function; + const documentUri = Uri.file('a'); + const document = typemoq.Mock.ofType<TextDocument>(); + document.setup((d) => d.uri).returns(() => documentUri); + document.setup((d) => d.languageId).returns(() => PYTHON_LANGUAGE); + + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn((cb) => { + workspaceFoldersChangedHandler = cb; + return disposable1.object; + }); + documentManager + .setup((w) => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((cb) => (fileOpenedHandler = cb)) + .returns(() => disposable2.object) + .verifiable(typemoq.Times.once()); + + const resource = Uri.parse('two'); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: resource, index: 2 }; + when(workspaceService.getWorkspaceFolderIdentifier(anything(), anything())).thenReturn('one'); + when(workspaceService.workspaceFolders).thenReturn([folder1, folder2]); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + when(workspaceService.getWorkspaceFolder(document.object.uri)).thenReturn(folder2); + + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(folder2); + when(activationService1.activate(resource)).thenResolve(); + when(activationService2.activate(resource)).thenResolve(); + when(interpreterService.getInterpreters(anything())).thenResolve(); + autoSelection + .setup((a) => a.autoSelectInterpreter(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + appDiagnostics + .setup((a) => a.performPreStartupHealthCheck(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + // Add workspaceFoldersChangedHandler + managerTest.addHandlers(); + expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set'); + + // Add fileOpenedHandler + workspaceFoldersChangedHandler.call(managerTest); + expect(fileOpenedHandler).not.to.be.equal(undefined, 'Handler not set'); + + // Check if activate workspace is called on opening a file + await fileOpenedHandler.call(managerTest, document.object); + await sleep(1); + + documentManager.verifyAll(); + verify(workspaceService.onDidChangeWorkspaceFolders).once(); + verify(workspaceService.workspaceFolders).atLeast(1); + verify(workspaceService.hasWorkspaceFolders).once(); + verify(workspaceService.getWorkspaceFolder(anything())).atLeast(1); + verify(activationService1.activate(resource)).once(); + verify(activationService2.activate(resource)).once(); + }); + + test('Function activateWorkspace() will be filtered to current resource', async () => { + const resource = Uri.parse('two'); + when(activationService1.activate(resource)).thenResolve(); + when(activationService2.activate(resource)).thenResolve(); + when(interpreterService.getInterpreters(anything())).thenResolve(); + autoSelection + .setup((a) => a.autoSelectInterpreter(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + appDiagnostics + .setup((a) => a.performPreStartupHealthCheck(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await managerTest.activateWorkspace(resource); + + verify(activationService1.activate(resource)).once(); + verify(activationService2.activate(resource)).once(); + }); + + test('If in Deprecate PythonPath experiment, method activateWorkspace() will copy old interpreter storage values to new', async () => { + const resource = Uri.parse('two'); + when(activationService1.activate(resource)).thenResolve(); + when(activationService2.activate(resource)).thenResolve(); + when(interpreterService.getInterpreters(anything())).thenResolve(); + when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + interpreterPathService + .setup((i) => i.copyOldInterpreterStorageValuesToNew(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + autoSelection + .setup((a) => a.autoSelectInterpreter(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + appDiagnostics + .setup((a) => a.performPreStartupHealthCheck(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await managerTest.activateWorkspace(resource); + + interpreterPathService.verifyAll(); + verify(activationService1.activate(resource)).once(); + verify(activationService2.activate(resource)).once(); + }); + + test("The same workspace isn't activated more than once", async () => { + const resource = Uri.parse('two'); + when(activationService1.activate(resource)).thenResolve(); + when(activationService2.activate(resource)).thenResolve(); + when(interpreterService.getInterpreters(anything())).thenResolve(); + autoSelection + .setup((a) => a.autoSelectInterpreter(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + appDiagnostics + .setup((a) => a.performPreStartupHealthCheck(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await managerTest.activateWorkspace(resource); + await managerTest.activateWorkspace(resource); + + verify(activationService1.activate(resource)).once(); + verify(activationService2.activate(resource)).once(); + autoSelection.verifyAll(); + appDiagnostics.verifyAll(); + }); + + test('If doc opened is not python, return', async () => { + const doc = { + uri: Uri.parse('doc'), + languageId: 'NOT PYTHON' + }; + + managerTest.onDocOpened(doc as any); + verify(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).never(); + }); + + test('If we have opened a doc that does not belong to workspace, then do nothing', async () => { + const doc = { + uri: Uri.parse('doc'), + languageId: PYTHON_LANGUAGE + }; + when(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).thenReturn(''); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + + managerTest.onDocOpened(doc as any); + + verify(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).once(); + verify(workspaceService.getWorkspaceFolder(doc.uri)).never(); + }); + + test('If workspace corresponding to the doc has already been activated, then do nothing', async () => { + const doc = { + uri: Uri.parse('doc'), + languageId: PYTHON_LANGUAGE + }; + when(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).thenReturn('key'); + managerTest.activatedWorkspaces.add('key'); + + managerTest.onDocOpened(doc as any); + + verify(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).once(); + verify(workspaceService.getWorkspaceFolder(doc.uri)).never(); + }); + + test('List of activated workspaces is updated & Handler docOpenedHandler is disposed in case no. of workspace folders decreases to one', async () => { + const disposable1 = typemoq.Mock.ofType<IDisposable>(); + const disposable2 = typemoq.Mock.ofType<IDisposable>(); + let docOpenedHandler!: (e: TextDocument) => Promise<void>; + let workspaceFoldersChangedHandler!: Function; + const documentUri = Uri.file('a'); + const document = typemoq.Mock.ofType<TextDocument>(); + document.setup((d) => d.uri).returns(() => documentUri); + + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn((cb) => { + workspaceFoldersChangedHandler = cb; + return disposable1.object; + }); + documentManager + .setup((w) => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((cb) => (docOpenedHandler = cb)) + .returns(() => disposable2.object) + .verifiable(typemoq.Times.once()); + + const resource = Uri.parse('two'); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: resource, index: 2 }; + when(workspaceService.workspaceFolders).thenReturn([folder1, folder2]); + + when(workspaceService.getWorkspaceFolderIdentifier(folder1.uri, anything())).thenReturn('one'); + when(workspaceService.getWorkspaceFolderIdentifier(folder2.uri, anything())).thenReturn('two'); + // Assume the two workspaces are already activated, so their keys will be present in `activatedWorkspaces` set + managerTest.activatedWorkspaces.add('one'); + managerTest.activatedWorkspaces.add('two'); + + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + // Add workspaceFoldersChangedHandler + managerTest.addHandlers(); + expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set'); + + // Add docOpenedHandler + workspaceFoldersChangedHandler.call(managerTest); + expect(docOpenedHandler).not.to.be.equal(undefined, 'Handler not set'); + + documentManager.verifyAll(); + verify(workspaceService.onDidChangeWorkspaceFolders).once(); + verify(workspaceService.workspaceFolders).atLeast(1); + verify(workspaceService.hasWorkspaceFolders).once(); + + //Removed no. of folders to one + when(workspaceService.workspaceFolders).thenReturn([folder1]); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + disposable2.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); + + workspaceFoldersChangedHandler.call(managerTest); + + verify(workspaceService.workspaceFolders).atLeast(1); + verify(workspaceService.hasWorkspaceFolders).twice(); + disposable2.verifyAll(); + + assert.deepEqual(Array.from(managerTest.activatedWorkspaces.keys()), ['one']); + }); + }); + + suite('Language Server Activation - activate()', () => { + let workspaceService: IWorkspaceService; + let appDiagnostics: typemoq.IMock<IApplicationDiagnostics>; + let autoSelection: typemoq.IMock<IInterpreterAutoSelectionService>; + let interpreterService: IInterpreterService; + let activeResourceService: IActiveResourceService; + let documentManager: typemoq.IMock<IDocumentManager>; + let interpreterSecurityService: IInterpreterSecurityService; + let activationService1: IExtensionActivationService; + let activationService2: IExtensionActivationService; + let fileSystem: IFileSystem; + let singleActivationService: typemoq.IMock<IExtensionSingleActivationService>; + let initialize: sinon.SinonStub<any>; + let activateWorkspace: sinon.SinonStub<any>; + let managerTest: ExtensionActivationManager; + const resource = Uri.parse('a'); + let interpreterPathService: typemoq.IMock<IInterpreterPathService>; + let experiments: IExperimentsManager; + + setup(() => { + interpreterSecurityService = mock(InterpreterSecurityService); + experiments = mock(ExperimentsManager); + workspaceService = mock(WorkspaceService); + activeResourceService = mock(ActiveResourceService); + appDiagnostics = typemoq.Mock.ofType<IApplicationDiagnostics>(); + autoSelection = typemoq.Mock.ofType<IInterpreterAutoSelectionService>(); + interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>(); + interpreterService = mock(InterpreterService); + documentManager = typemoq.Mock.ofType<IDocumentManager>(); + activationService1 = mock(LanguageServerExtensionActivationService); + activationService2 = mock(LanguageServerExtensionActivationService); + fileSystem = mock(FileSystem); + singleActivationService = typemoq.Mock.ofType<IExtensionSingleActivationService>(); + initialize = sinon.stub(ExtensionActivationManager.prototype, 'initialize'); + initialize.resolves(); + activateWorkspace = sinon.stub(ExtensionActivationManager.prototype, 'activateWorkspace'); + activateWorkspace.resolves(); + interpreterPathService + .setup((i) => i.onDidChange(typemoq.It.isAny())) + .returns(() => typemoq.Mock.ofType<IDisposable>().object); + managerTest = new ExtensionActivationManager( + [instance(activationService1), instance(activationService2)], + [singleActivationService.object], + documentManager.object, + instance(interpreterService), + autoSelection.object, + appDiagnostics.object, + instance(workspaceService), + instance(fileSystem), + instance(activeResourceService), + instance(experiments), + interpreterPathService.object, + instance(interpreterSecurityService) + ); + managerTest.evaluateAutoSelectedInterpreterSafety = () => Promise.resolve(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Execution goes as expected if there are no errors', async () => { + singleActivationService + .setup((s) => s.activate()) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + autoSelection + .setup((a) => a.autoSelectInterpreter(undefined)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + when(activeResourceService.getActiveResource()).thenReturn(resource); + await managerTest.activate(); + assert.ok(initialize.calledOnce); + assert.ok(activateWorkspace.calledOnce); + singleActivationService.verifyAll(); + autoSelection.verifyAll(); + }); + + test('Throws error if execution fails', async () => { + singleActivationService + .setup((s) => s.activate()) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + autoSelection + .setup((a) => a.autoSelectInterpreter(undefined)) + .returns(() => Promise.reject(new Error('Kaboom'))) + .verifiable(typemoq.Times.once()); + when(activeResourceService.getActiveResource()).thenReturn(resource); + const promise = managerTest.activate(); + await expect(promise).to.eventually.be.rejectedWith('Kaboom'); + }); + }); + + suite('Selected Python Activation - evaluateIfAutoSelectedInterpreterIsSafe()', () => { + let workspaceService: IWorkspaceService; + let appDiagnostics: typemoq.IMock<IApplicationDiagnostics>; + let autoSelection: typemoq.IMock<IInterpreterAutoSelectionService>; + let interpreterService: IInterpreterService; + let activeResourceService: IActiveResourceService; + let documentManager: typemoq.IMock<IDocumentManager>; + let activationService1: IExtensionActivationService; + let activationService2: IExtensionActivationService; + let fileSystem: IFileSystem; + let managerTest: ExtensionActivationManager; + const resource = Uri.parse('a'); + let interpreterSecurityService: IInterpreterSecurityService; + let interpreterPathService: IInterpreterPathService; + let experiments: IExperimentsManager; + setup(() => { + interpreterSecurityService = mock(InterpreterSecurityService); + experiments = mock(ExperimentsManager); + fileSystem = mock(FileSystem); + interpreterPathService = mock(InterpreterPathService); + workspaceService = mock(WorkspaceService); + activeResourceService = mock(ActiveResourceService); + appDiagnostics = typemoq.Mock.ofType<IApplicationDiagnostics>(); + autoSelection = typemoq.Mock.ofType<IInterpreterAutoSelectionService>(); + interpreterService = mock(InterpreterService); + documentManager = typemoq.Mock.ofType<IDocumentManager>(); + activationService1 = mock(LanguageServerExtensionActivationService); + activationService2 = mock(LanguageServerExtensionActivationService); + when(experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).thenReturn(undefined); + managerTest = new ExtensionActivationManager( + [instance(activationService1), instance(activationService2)], + [], + documentManager.object, + instance(interpreterService), + autoSelection.object, + appDiagnostics.object, + instance(workspaceService), + instance(fileSystem), + instance(activeResourceService), + instance(experiments), + instance(interpreterPathService), + instance(interpreterSecurityService) + ); + }); + + test(`If in Deprecate PythonPath experiment, and setting is not set, fetch autoselected interpreter but don't evaluate it if it equals 'undefined'`, async () => { + const interpreter = undefined; + when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); + autoSelection + .setup((a) => a.getAutoSelectedInterpreter(resource)) + .returns(() => interpreter as any) + .verifiable(typemoq.Times.once()); + when(interpreterPathService.get(resource)).thenReturn('python'); + when( + interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource) + ).thenResolve(); + await managerTest.evaluateAutoSelectedInterpreterSafety(resource); + autoSelection.verifyAll(); + verify(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).never(); + }); + + ['', 'python'].forEach((setting) => { + test(`If in Deprecate PythonPath experiment, and setting equals '${setting}', fetch autoselected interpreter and evaluate it`, async () => { + const interpreter = { path: 'pythonPath' }; + when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); + autoSelection + .setup((a) => a.getAutoSelectedInterpreter(resource)) + .returns(() => interpreter as any) + .verifiable(typemoq.Times.once()); + when(interpreterPathService.get(resource)).thenReturn(setting); + when( + interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource) + ).thenResolve(); + await managerTest.evaluateAutoSelectedInterpreterSafety(resource); + autoSelection.verifyAll(); + verify( + interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource) + ).once(); + }); + }); + + test(`If in Deprecate PythonPath experiment, and setting is not set, fetch autoselected interpreter but don't evaluate it if it equals 'undefined'`, async () => { + const interpreter = undefined; + when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); + autoSelection + .setup((a) => a.getAutoSelectedInterpreter(resource)) + .returns(() => interpreter as any) + .verifiable(typemoq.Times.once()); + when(interpreterPathService.get(resource)).thenReturn('python'); + when( + interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource) + ).thenResolve(); + await managerTest.evaluateAutoSelectedInterpreterSafety(resource); + autoSelection.verifyAll(); + verify(interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource)).never(); + }); + + test(`If in Deprecate PythonPath experiment, and setting is set, simply return`, async () => { + when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); + autoSelection.setup((a) => a.getAutoSelectedInterpreter(resource)).verifiable(typemoq.Times.never()); + when(interpreterPathService.get(resource)).thenReturn('settingSetToSomePath'); + await managerTest.evaluateAutoSelectedInterpreterSafety(resource); + autoSelection.verifyAll(); + }); + + test(`If in Deprecate PythonPath experiment, if setting is set during evaluation, don't wait for the evaluation to finish to resolve method promise`, async () => { + const interpreter = { path: 'pythonPath' }; + const evaluateIfInterpreterIsSafeDeferred = createDeferred<void>(); + when(experiments.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(workspaceService.getWorkspaceFolderIdentifier(resource)).thenReturn('1'); + autoSelection.setup((a) => a.getAutoSelectedInterpreter(resource)).returns(() => interpreter as any); + when(interpreterPathService.get(resource)).thenReturn('python'); + when( + interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource) + ).thenReturn(evaluateIfInterpreterIsSafeDeferred.promise); + const deferredPromise = createDeferredFromPromise( + managerTest.evaluateAutoSelectedInterpreterSafety(resource) + ); + expect(deferredPromise.completed).to.equal(false, 'Promise should not be resolved yet'); + reset(interpreterPathService); + when(interpreterPathService.get(resource)).thenReturn('settingSetToSomePath'); + await managerTest.evaluateAutoSelectedInterpreterSafety(resource); + await sleep(1); + expect(deferredPromise.completed).to.equal(true, 'Promise should be resolved'); + }); + }); +}); diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts new file mode 100644 index 000000000000..b85b27917aba --- /dev/null +++ b/src/test/activation/activationService.unit.test.ts @@ -0,0 +1,964 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationChangeEvent, Disposable, EventEmitter, Uri, WorkspaceConfiguration } from 'vscode'; + +import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; +import { + FolderVersionPair, + IExtensionActivationService, + ILanguageServerActivator, + ILanguageServerFolderService, + LanguageServerType +} from '../../client/activation/types'; +import { LSNotSupportedDiagnosticServiceId } from '../../client/application/diagnostics/checks/lsNotSupported'; +import { IDiagnostic, IDiagnosticsService } from '../../client/application/diagnostics/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; +import { IPlatformService } from '../../client/common/platform/types'; +import { + IConfigurationService, + IDisposable, + IDisposableRegistry, + IExtensions, + IOutputChannel, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, + Resource +} from '../../client/common/types'; +import { noop } from '../../client/common/utils/misc'; +import { Architecture } from '../../client/common/utils/platform'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { IServiceContainer } from '../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; + +// tslint:disable:max-func-body-length no-any + +suite('Language Server Activation - ActivationService', () => { + [LanguageServerType.Jedi, LanguageServerType.Microsoft].forEach((languageServerType) => { + suite( + `Test activation - ${ + languageServerType === LanguageServerType.Jedi ? 'Jedi is enabled' : 'Jedi is disabled' + }`, + () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let cmdManager: TypeMoq.IMock<ICommandManager>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let platformService: TypeMoq.IMock<IPlatformService>; + let lsNotSupportedDiagnosticService: TypeMoq.IMock<IDiagnosticsService>; + let stateFactory: TypeMoq.IMock<IPersistentStateFactory>; + let state: TypeMoq.IMock<IPersistentState<boolean | undefined>>; + let workspaceConfig: TypeMoq.IMock<WorkspaceConfiguration>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let interpreterChangedHandler!: Function; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + cmdManager = TypeMoq.Mock.ofType<ICommandManager>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + state = TypeMoq.Mock.ofType<IPersistentState<boolean | undefined>>(); + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + const langFolderServiceMock = TypeMoq.Mock.ofType<ILanguageServerFolderService>(); + const extensionsMock = TypeMoq.Mock.ofType<IExtensions>(); + const folderVer: FolderVersionPair = { + path: '', + version: new SemVer('1.2.3') + }; + lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType<IDiagnosticsService>(); + workspaceService.setup((w) => w.hasWorkspaceFolders).returns(() => false); + workspaceService.setup((w) => w.workspaceFolders).returns(() => []); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + const disposable = TypeMoq.Mock.ofType<IDisposable>(); + interpreterService + .setup((i) => i.onDidChangeInterpreter(TypeMoq.It.isAny())) + .returns((cb) => { + interpreterChangedHandler = cb; + return disposable.object; + }); + langFolderServiceMock + .setup((l) => l.getCurrentLanguageServerDirectory()) + .returns(() => Promise.resolve(folderVer)); + stateFactory + .setup((f) => + f.createGlobalPersistentState( + TypeMoq.It.isValue('SWITCH_LS'), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => state.object); + state.setup((s) => s.value).returns(() => undefined); + state.setup((s) => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny())) + .returns(() => workspaceConfig.object); + const output = TypeMoq.Mock.ofType<IOutputChannel>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) + .returns(() => output.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => appShell.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))) + .returns(() => cmdManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) + .returns(() => langFolderServiceMock.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IExtensions))) + .returns(() => extensionsMock.object); + serviceContainer + .setup((s) => + s.get( + TypeMoq.It.isValue(IDiagnosticsService), + TypeMoq.It.isValue(LSNotSupportedDiagnosticServiceId) + ) + ) + .returns(() => lsNotSupportedDiagnosticService.object); + }); + + async function testActivation( + activationService: IExtensionActivationService, + activator: TypeMoq.IMock<ILanguageServerActivator>, + lsSupported: boolean = true, + activatorName: LanguageServerType = LanguageServerType.Jedi + ) { + activator + .setup((a) => a.start(undefined, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator.setup((a) => a.activate()).verifiable(TypeMoq.Times.once()); + + if ( + activatorName !== LanguageServerType.None && + lsSupported && + activatorName !== LanguageServerType.Jedi + ) { + activatorName = LanguageServerType.Microsoft; + } + + let diagnostics: IDiagnostic[]; + if (!lsSupported && activatorName !== LanguageServerType.Jedi) { + diagnostics = [TypeMoq.It.isAny()]; + } else { + diagnostics = []; + } + + lsNotSupportedDiagnosticService + .setup((l) => l.diagnose(undefined)) + .returns(() => Promise.resolve(diagnostics)); + lsNotSupportedDiagnosticService + .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) + .returns(() => Promise.resolve()); + serviceContainer + .setup((c) => + c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isValue(activatorName)) + ) + .returns(() => activator.object) + .verifiable(TypeMoq.Times.once()); + + await activationService.activate(undefined); + + activator.verifyAll(); + serviceContainer.verifyAll(); + } + + async function testReloadMessage(settingName: string): Promise<void> { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise<void>; + workspaceService + .setup((w) => + w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .callback((cb) => (callbackHandler = cb)) + .returns(() => TypeMoq.Mock.ofType<Disposable>().object) + .verifiable(TypeMoq.Times.once()); + + pythonSettings.setup((p) => p.languageServer).returns(() => languageServerType); + const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + + workspaceService.verifyAll(); + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); + event + .setup((e) => + e.affectsConfiguration(TypeMoq.It.isValue(`python.${settingName}`), TypeMoq.It.isAny()) + ) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve('Reload')) + .verifiable(TypeMoq.Times.once()); + cmdManager + .setup((c) => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) + .verifiable(TypeMoq.Times.once()); + + // Toggle the value in the setting and invoke the callback. + languageServerType = + languageServerType === LanguageServerType.Jedi + ? LanguageServerType.Microsoft + : LanguageServerType.Jedi; + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + } + + test('LS is supported', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + + await testActivation(activationService, activator, true); + }); + test('LS is not supported', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + + await testActivation(activationService, activator, false); + }); + + test('Activator must be activated', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + + await testActivation(activationService, activator); + }); + test('Activator must be deactivated', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + + await testActivation(activationService, activator); + + activator.setup((a) => a.dispose()).verifiable(TypeMoq.Times.once()); + + activationService.dispose(); + activator.verifyAll(); + }); + test('No language service', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.None); + const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + await testActivation(activationService, activator, false, LanguageServerType.None); + }); + test('Prompt user to reload VS Code and reload, when languageServer setting is toggled', async () => { + await testReloadMessage('languageServer'); + }); + test('Do not prompt user to reload VS Code when setting is not changed', async () => { + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise<void>; + workspaceService + .setup((w) => + w.onDidChangeConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .callback((cb) => (callbackHandler = cb)) + .returns(() => TypeMoq.Mock.ofType<Disposable>().object) + .verifiable(TypeMoq.Times.once()); + + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + + workspaceService.verifyAll(); + await testActivation(activationService, activator); + + const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); + event + .setup((e) => + e.affectsConfiguration(TypeMoq.It.isValue('python.languageServer'), TypeMoq.It.isAny()) + ) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + cmdManager + .setup((c) => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) + .verifiable(TypeMoq.Times.never()); + + // Invoke the config changed callback. + await callbackHandler(event.object); + + event.verifyAll(); + appShell.verifyAll(); + cmdManager.verifyAll(); + }); + test('More than one LS is created for multiple interpreters if LS is "Microsoft"', async () => { + const interpreter1: PythonEnvironment = { + path: '/foo/bar/python', + sysPrefix: '1', + envName: '1', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + envType: EnvironmentType.Unknown + }; + const interpreter2: PythonEnvironment = { + path: '/foo/baz/python', + sysPrefix: '1', + envName: '2', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + envType: EnvironmentType.Unknown + }; + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + activator + .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator + .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator.setup((a) => a.deactivate()).verifiable(TypeMoq.Times.never()); + activator.setup((a) => a.activate()).verifiable(TypeMoq.Times.never()); + activator + .setup((a) => a.dispose()) + .returns(noop) + .verifiable(TypeMoq.Times.exactly(2)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isAny())) + .returns(() => activator.object); + + lsNotSupportedDiagnosticService + .setup((l) => l.diagnose(undefined)) + .returns(() => Promise.resolve([])); + lsNotSupportedDiagnosticService + .setup((l) => l.handle(TypeMoq.It.isValue([]))) + .returns(() => Promise.resolve()); + + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + const ls1 = await activationService.get(folder1.uri, interpreter1); + const ls2 = await activationService.get(folder1.uri, interpreter2); + expect(ls1).not.to.be.equal(ls2, 'Interpreter does not create new LS'); + const ls3 = await activationService.get(undefined, interpreter1); + expect(ls1).to.be.equal(ls3, 'Interpreter does return same LS'); + ls3.dispose(); + ls1.dispose(); + ls2.dispose(); + activator.verifyAll(); + }); + test('Changing interpreter will activate a new LS if it is "Microsoft"', async () => { + const interpreter1: PythonEnvironment = { + path: '/foo/bar/python', + sysPrefix: '1', + envName: '1', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + envType: EnvironmentType.Unknown + }; + const interpreter2: PythonEnvironment = { + path: '/foo/baz/python', + sysPrefix: '1', + envName: '2', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + envType: EnvironmentType.Unknown + }; + let getActiveCount = 0; + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => { + if (getActiveCount % 2 === 0) { + getActiveCount += 1; + return Promise.resolve(interpreter1); + } + getActiveCount += 1; + return Promise.resolve(interpreter2); + }); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + activator + .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter1))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator + .setup((a) => a.start(TypeMoq.It.isValue(folder1.uri), TypeMoq.It.isValue(interpreter2))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + let connectCount = 0; + activator + .setup((a) => a.activate()) + .returns(() => { + connectCount = connectCount + 1; + }); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isAny())) + .returns(() => activator.object); + const diagnostics: IDiagnostic[] = [TypeMoq.It.isAny()]; + lsNotSupportedDiagnosticService + .setup((l) => l.diagnose(undefined)) + .returns(() => Promise.resolve(diagnostics)); + lsNotSupportedDiagnosticService + .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) + .returns(() => Promise.resolve()); + + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + await activationService.activate(folder1.uri); + await interpreterChangedHandler(); + activator.verifyAll(); + + // Hold onto the second item and switch two more times. Verify that + // reconnect happens + const server = await activationService.get(folder1.uri); + await interpreterChangedHandler(); + expect(connectCount).to.be.equal(3, 'Reconnect is not happening'); + await interpreterChangedHandler(); + expect(connectCount).to.be.equal(4, 'Reconnect is not happening'); + server.dispose(); + }); + if (languageServerType !== LanguageServerType.Jedi) { + test('Revert to jedi when LS activation fails', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + const activatorLS = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + const activatorJedi = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + const diagnostics: IDiagnostic[] = []; + lsNotSupportedDiagnosticService + .setup((l) => l.diagnose(undefined)) + .returns(() => Promise.resolve(diagnostics)); + lsNotSupportedDiagnosticService + .setup((l) => l.handle(TypeMoq.It.isValue(diagnostics))) + .returns(() => Promise.resolve()); + serviceContainer + .setup((c) => + c.get( + TypeMoq.It.isValue(ILanguageServerActivator), + TypeMoq.It.isValue(LanguageServerType.Microsoft) + ) + ) + .returns(() => activatorLS.object) + .verifiable(TypeMoq.Times.once()); + activatorLS + .setup((a) => a.start(undefined, undefined)) + .returns(() => Promise.reject(new Error(''))) + .verifiable(TypeMoq.Times.once()); + serviceContainer + .setup((c) => + c.get( + TypeMoq.It.isValue(ILanguageServerActivator), + TypeMoq.It.isValue(LanguageServerType.Jedi) + ) + ) + .returns(() => activatorJedi.object) + .verifiable(TypeMoq.Times.once()); + activatorJedi + .setup((a) => a.start(undefined, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatorJedi + .setup((a) => a.activate()) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await activationService.activate(undefined); + + activatorLS.verifyAll(); + activatorJedi.verifyAll(); + serviceContainer.verifyAll(); + }); + async function testActivationOfResource( + activationService: IExtensionActivationService, + activator: TypeMoq.IMock<ILanguageServerActivator>, + resource: Resource + ) { + activator + .setup((a) => a.start(TypeMoq.It.isValue(resource), undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activator.setup((a) => a.activate()).verifiable(TypeMoq.Times.once()); + lsNotSupportedDiagnosticService + .setup((l) => l.diagnose(undefined)) + .returns(() => Promise.resolve([])); + lsNotSupportedDiagnosticService + .setup((l) => l.handle(TypeMoq.It.isValue([]))) + .returns(() => Promise.resolve()); + serviceContainer + .setup((c) => + c.get( + TypeMoq.It.isValue(ILanguageServerActivator), + TypeMoq.It.isValue(LanguageServerType.Microsoft) + ) + ) + .returns(() => activator.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.getWorkspaceFolderIdentifier(resource, '')) + .returns(() => resource!.fsPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + + await activationService.activate(resource); + + activator.verifyAll(); + serviceContainer.verifyAll(); + workspaceService.verifyAll(); + } + test('Activator is disposed if activated workspace is removed and LS is "Microsoft"', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Microsoft); + let workspaceFoldersChangedHandler!: Function; + workspaceService + .setup((w) => w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((cb) => (workspaceFoldersChangedHandler = cb)) + .returns(() => TypeMoq.Mock.ofType<IDisposable>().object) + .verifiable(TypeMoq.Times.once()); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + workspaceService.verifyAll(); + expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set'); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; + const folder3 = { name: 'three', uri: Uri.parse('three'), index: 3 }; + + const activator1 = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + await testActivationOfResource(activationService, activator1, folder1.uri); + const activator2 = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + await testActivationOfResource(activationService, activator2, folder2.uri); + const activator3 = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + await testActivationOfResource(activationService, activator3, folder3.uri); + + //Now remove folder3 + workspaceService.reset(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + workspaceService + .setup((w) => w.getWorkspaceFolderIdentifier(folder1.uri, '')) + .returns(() => folder1.uri.fsPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.getWorkspaceFolderIdentifier(folder2.uri, '')) + .returns(() => folder2.uri.fsPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + activator1.setup((d) => d.dispose()).verifiable(TypeMoq.Times.never()); + activator2.setup((d) => d.dispose()).verifiable(TypeMoq.Times.never()); + activator3.setup((d) => d.dispose()).verifiable(TypeMoq.Times.once()); + await workspaceFoldersChangedHandler.call(activationService); + workspaceService.verifyAll(); + activator3.verifyAll(); + }); + } else { + test('Jedi is only started once', async () => { + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); + const activator1 = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; + serviceContainer + .setup((c) => + c.get( + TypeMoq.It.isValue(ILanguageServerActivator), + TypeMoq.It.isValue(LanguageServerType.Jedi) + ) + ) + .returns(() => activator1.object) + .verifiable(TypeMoq.Times.once()); + activator1 + .setup((a) => a.start(folder1.uri, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + await activationService.activate(folder1.uri); + activator1.verifyAll(); + activator1.verify((a) => a.activate(), TypeMoq.Times.once()); + serviceContainer.verifyAll(); + + const activator2 = TypeMoq.Mock.ofType<ILanguageServerActivator>(); + serviceContainer + .setup((c) => + c.get( + TypeMoq.It.isValue(ILanguageServerActivator), + TypeMoq.It.isValue(LanguageServerType.Jedi) + ) + ) + .returns(() => activator2.object) + .verifiable(TypeMoq.Times.once()); + activator2 + .setup((a) => a.start(folder2.uri, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + activator2.setup((a) => a.activate()).verifiable(TypeMoq.Times.never()); + await activationService.activate(folder2.uri); + serviceContainer.verifyAll(); + activator1.verifyAll(); + activator1.verify((a) => a.activate(), TypeMoq.Times.exactly(2)); + activator2.verifyAll(); + }); + } + } + ); + }); + + suite('Test sendTelemetryForChosenLanguageServer()', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let cmdManager: TypeMoq.IMock<ICommandManager>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let platformService: TypeMoq.IMock<IPlatformService>; + let lsNotSupportedDiagnosticService: TypeMoq.IMock<IDiagnosticsService>; + let stateFactory: TypeMoq.IMock<IPersistentStateFactory>; + let state: TypeMoq.IMock<IPersistentState<LanguageServerType | undefined>>; + let workspaceConfig: TypeMoq.IMock<WorkspaceConfiguration>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + cmdManager = TypeMoq.Mock.ofType<ICommandManager>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + state = TypeMoq.Mock.ofType<IPersistentState<LanguageServerType | undefined>>(); + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + const e = new EventEmitter<void>(); + interpreterService.setup((i) => i.onDidChangeInterpreter).returns(() => e.event); + const langFolderServiceMock = TypeMoq.Mock.ofType<ILanguageServerFolderService>(); + const extensionsMock = TypeMoq.Mock.ofType<IExtensions>(); + const folderVer: FolderVersionPair = { + path: '', + version: new SemVer('1.2.3') + }; + lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType<IDiagnosticsService>(); + workspaceService.setup((w) => w.hasWorkspaceFolders).returns(() => false); + workspaceService.setup((w) => w.workspaceFolders).returns(() => []); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + langFolderServiceMock + .setup((l) => l.getCurrentLanguageServerDirectory()) + .returns(() => Promise.resolve(folderVer)); + stateFactory + .setup((f) => + f.createGlobalPersistentState( + TypeMoq.It.isValue('SWITCH_LS'), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => state.object); + state.setup((s) => s.value).returns(() => undefined); + state.setup((s) => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny())) + .returns(() => workspaceConfig.object); + const output = TypeMoq.Mock.ofType<IOutputChannel>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) + .returns(() => output.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => cmdManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) + .returns(() => langFolderServiceMock.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IExtensions))).returns(() => extensionsMock.object); + serviceContainer + .setup((s) => + s.get( + TypeMoq.It.isValue(IDiagnosticsService), + TypeMoq.It.isValue(LSNotSupportedDiagnosticServiceId) + ) + ) + .returns(() => lsNotSupportedDiagnosticService.object); + }); + + test('Track current LS usage for first usage', async () => { + state.reset(); + state + .setup((s) => s.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.exactly(2)); + state + .setup((s) => s.updateValue(TypeMoq.It.isValue(LanguageServerType.Jedi))) + .returns(() => { + state.setup((s) => s.value).returns(() => LanguageServerType.Jedi); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.once()); + + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi); + + state.verifyAll(); + }); + test('Track switch to LS', async () => { + state.reset(); + state + .setup((s) => s.value) + .returns(() => LanguageServerType.Jedi) + .verifiable(TypeMoq.Times.exactly(2)); + state + .setup((s) => s.updateValue(TypeMoq.It.isValue(LanguageServerType.Microsoft))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Microsoft); + + state.verifyAll(); + }); + test('Track switch to Jedi', async () => { + state.reset(); + state + .setup((s) => s.value) + .returns(() => LanguageServerType.Microsoft) + .verifiable(TypeMoq.Times.exactly(2)); + state + .setup((s) => s.updateValue(TypeMoq.It.isValue(LanguageServerType.Jedi))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi); + + state.verifyAll(); + }); + test('Track startup value', async () => { + state.reset(); + state + .setup((s) => s.value) + .returns(() => LanguageServerType.Jedi) + .verifiable(TypeMoq.Times.exactly(2)); + state + .setup((s) => s.updateValue(TypeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + await activationService.sendTelemetryForChosenLanguageServer(LanguageServerType.Jedi); + + state.verifyAll(); + }); + }); + + suite('Function isJediUsingDefaultConfiguration()', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let cmdManager: TypeMoq.IMock<ICommandManager>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let platformService: TypeMoq.IMock<IPlatformService>; + let lsNotSupportedDiagnosticService: TypeMoq.IMock<IDiagnosticsService>; + let stateFactory: TypeMoq.IMock<IPersistentStateFactory>; + let state: TypeMoq.IMock<IPersistentState<boolean | undefined>>; + let workspaceConfig: TypeMoq.IMock<WorkspaceConfiguration>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + cmdManager = TypeMoq.Mock.ofType<ICommandManager>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + state = TypeMoq.Mock.ofType<IPersistentState<boolean | undefined>>(); + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + const e = new EventEmitter<void>(); + interpreterService.setup((i) => i.onDidChangeInterpreter).returns(() => e.event); + const langFolderServiceMock = TypeMoq.Mock.ofType<ILanguageServerFolderService>(); + const extensionsMock = TypeMoq.Mock.ofType<IExtensions>(); + const folderVer: FolderVersionPair = { + path: '', + version: new SemVer('1.2.3') + }; + lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType<IDiagnosticsService>(); + workspaceService.setup((w) => w.hasWorkspaceFolders).returns(() => false); + workspaceService.setup((w) => w.workspaceFolders).returns(() => []); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + langFolderServiceMock + .setup((l) => l.getCurrentLanguageServerDirectory()) + .returns(() => Promise.resolve(folderVer)); + stateFactory + .setup((f) => + f.createGlobalPersistentState( + TypeMoq.It.isValue('SWITCH_LS'), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => state.object); + state.setup((s) => s.value).returns(() => undefined); + state.setup((s) => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny())) + .returns(() => workspaceConfig.object); + const output = TypeMoq.Mock.ofType<IOutputChannel>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) + .returns(() => output.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => cmdManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) + .returns(() => langFolderServiceMock.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IExtensions))).returns(() => extensionsMock.object); + serviceContainer + .setup((s) => + s.get( + TypeMoq.It.isValue(IDiagnosticsService), + TypeMoq.It.isValue(LSNotSupportedDiagnosticServiceId) + ) + ) + .returns(() => lsNotSupportedDiagnosticService.object); + }); + const value = [undefined, true, false]; // Possible values of settings + const index = [0, 1, 2]; // Index associated with each value + const expectedResults: boolean[][][] = Array(3) // Initializing a 3D array with default value `false` + .fill(false) + .map(() => + Array(3) + .fill(false) + .map(() => Array(3).fill(false)) + ); + expectedResults[0][0][0] = true; + for (const globalIndex of index) { + for (const workspaceIndex of index) { + for (const workspaceFolderIndex of index) { + const expectedResult = expectedResults[globalIndex][workspaceIndex][workspaceFolderIndex]; + const settings = { + globalValue: value[globalIndex], + workspaceValue: value[workspaceIndex], + workspaceFolderValue: value[workspaceFolderIndex] + }; + const testName = `Returns ${expectedResult} for setting = ${JSON.stringify(settings)}`; + test(testName, async () => { + workspaceConfig.reset(); + workspaceConfig + .setup((c) => c.inspect<LanguageServerType>('languageServer')) + .returns(() => settings as any) + .verifiable(TypeMoq.Times.once()); + + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + const result = activationService.isJediUsingDefaultConfiguration(Uri.parse('a')); + expect(result).to.equal(expectedResult); + + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + } + } + } + test('Returns false for settings = undefined', async () => { + workspaceConfig.reset(); + workspaceConfig + .setup((c) => c.inspect<LanguageServerType>('languageServer')) + .returns(() => undefined as any) + .verifiable(TypeMoq.Times.once()); + + const activationService = new LanguageServerExtensionActivationService( + serviceContainer.object, + stateFactory.object + ); + const result = activationService.isJediUsingDefaultConfiguration(Uri.parse('a')); + expect(result).to.equal(false, 'Return value should be false'); + + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + }); +}); diff --git a/src/test/activation/activeResource.unit.test.ts b/src/test/activation/activeResource.unit.test.ts new file mode 100644 index 000000000000..ca203155078c --- /dev/null +++ b/src/test/activation/activeResource.unit.test.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; + +// tslint:disable-next-line: max-func-body-length +suite('Active resource service', () => { + let documentManager: IDocumentManager; + let workspaceService: IWorkspaceService; + let activeResourceService: ActiveResourceService; + setup(() => { + documentManager = mock(DocumentManager); + workspaceService = mock(WorkspaceService); + activeResourceService = new ActiveResourceService(instance(documentManager), instance(workspaceService)); + }); + + test('Return document uri if the active document is not new (has been saved)', async () => { + const activeTextEditor = { + document: { + isUntitled: false, + uri: Uri.parse('a') + } + }; + // tslint:disable-next-line:no-any + when(documentManager.activeTextEditor).thenReturn(activeTextEditor as any); + + const activeResource = activeResourceService.getActiveResource(); + + assert.deepEqual(activeResource, activeTextEditor.document.uri); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspaceService.workspaceFolders).never(); + }); + + test("Don't return document uri if the active document is new (still unsaved)", async () => { + const activeTextEditor = { + document: { + isUntitled: true, + uri: Uri.parse('a') + } + }; + // tslint:disable-next-line:no-any + when(documentManager.activeTextEditor).thenReturn(activeTextEditor as any); + when(workspaceService.workspaceFolders).thenReturn([]); + + const activeResource = activeResourceService.getActiveResource(); + + assert.notDeepEqual(activeResource, activeTextEditor.document.uri); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspaceService.workspaceFolders).atLeast(1); + }); + + test('If no document is currently opened & the workspace opened contains workspace folders, return the uri of the first workspace folder', async () => { + const workspaceFolders = [ + { + uri: Uri.parse('a') + }, + { + uri: Uri.parse('b') + } + ]; + when(documentManager.activeTextEditor).thenReturn(undefined); + // tslint:disable-next-line:no-any + when(workspaceService.workspaceFolders).thenReturn(workspaceFolders as any); + + const activeResource = activeResourceService.getActiveResource(); + + assert.deepEqual(activeResource, workspaceFolders[0].uri); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspaceService.workspaceFolders).atLeast(1); + }); + + test('If no document is currently opened & no folder is opened, return undefined', async () => { + when(documentManager.activeTextEditor).thenReturn(undefined); + when(workspaceService.workspaceFolders).thenReturn(undefined); + + const activeResource = activeResourceService.getActiveResource(); + + assert.deepEqual(activeResource, undefined); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspaceService.workspaceFolders).atLeast(1); + }); + + test('If no document is currently opened & workspace contains no workspace folders, return undefined', async () => { + when(documentManager.activeTextEditor).thenReturn(undefined); + when(workspaceService.workspaceFolders).thenReturn([]); + + const activeResource = activeResourceService.getActiveResource(); + + assert.deepEqual(activeResource, undefined); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspaceService.workspaceFolders).atLeast(1); + }); +}); diff --git a/src/test/activation/extensionSurvey.unit.test.ts b/src/test/activation/extensionSurvey.unit.test.ts new file mode 100644 index 000000000000..d49c566df0bf --- /dev/null +++ b/src/test/activation/extensionSurvey.unit.test.ts @@ -0,0 +1,546 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ExtensionSurveyPrompt, extensionSurveyStateKeys } from '../../client/activation/extensionSurvey'; +import { IApplicationEnvironment, IApplicationShell } from '../../client/common/application/types'; +import { ShowExtensionSurveyPrompt } from '../../client/common/experiments/groups'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { IPlatformService } from '../../client/common/platform/types'; +import { + IBrowserService, + IExperimentsManager, + IPersistentState, + IPersistentStateFactory, + IRandom +} from '../../client/common/types'; +import { createDeferred } from '../../client/common/utils/async'; +import { Common, ExtensionSurveyBanner } from '../../client/common/utils/localize'; +import { OSType } from '../../client/common/utils/platform'; +import { sleep } from '../core'; + +// tslint:disable:no-any + +// tslint:disable-next-line:max-func-body-length +suite('Extension survey prompt - shouldShowBanner()', () => { + let appShell: TypeMoq.IMock<IApplicationShell>; + let browserService: TypeMoq.IMock<IBrowserService>; + let random: TypeMoq.IMock<IRandom>; + let persistentStateFactory: IPersistentStateFactory; + let experiments: TypeMoq.IMock<IExperimentsManager>; + let platformService: TypeMoq.IMock<IPlatformService>; + let appEnvironment: TypeMoq.IMock<IApplicationEnvironment>; + let disableSurveyForTime: TypeMoq.IMock<IPersistentState<any>>; + let doNotShowAgain: TypeMoq.IMock<IPersistentState<any>>; + let extensionSurveyPrompt: ExtensionSurveyPrompt; + setup(() => { + experiments = TypeMoq.Mock.ofType<IExperimentsManager>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + browserService = TypeMoq.Mock.ofType<IBrowserService>(); + random = TypeMoq.Mock.ofType<IRandom>(); + persistentStateFactory = mock(PersistentStateFactory); + disableSurveyForTime = TypeMoq.Mock.ofType<IPersistentState<any>>(); + doNotShowAgain = TypeMoq.Mock.ofType<IPersistentState<any>>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + when( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything() + ) + ).thenReturn(disableSurveyForTime.object); + when( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + ).thenReturn(doNotShowAgain.object); + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + 10 + ); + }); + test('Returns false if do not show again is clicked', async () => { + random + .setup((r) => r.getRandomInt(0, 100)) + .returns(() => 10) + .verifiable(TypeMoq.Times.never()); + doNotShowAgain.setup((d) => d.value).returns(() => true); + + const result = extensionSurveyPrompt.shouldShowBanner(); + + expect(result).to.equal(false, 'Banner should not be shown'); + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything() + ) + ).never(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + ).once(); + random.verifyAll(); + }); + test('Returns false if prompt is disabled for a while', async () => { + random + .setup((r) => r.getRandomInt(0, 100)) + .returns(() => 10) + .verifiable(TypeMoq.Times.never()); + disableSurveyForTime.setup((d) => d.value).returns(() => true); + doNotShowAgain.setup((d) => d.value).returns(() => false); + + const result = extensionSurveyPrompt.shouldShowBanner(); + + expect(result).to.equal(false, 'Banner should not be shown'); + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything() + ) + ).once(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + ).once(); + random.verifyAll(); + }); + test('Returns false if user is not in the random sampling', async () => { + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + // Default sample size is 10 + for (let i = 10; i < 100; i = i + 1) { + random.setup((r) => r.getRandomInt(0, 100)).returns(() => i); + const result = extensionSurveyPrompt.shouldShowBanner(); + expect(result).to.equal(false, 'Banner should not be shown'); + } + random.verifyAll(); + }); + test('Returns true if user is in the random sampling', async () => { + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + // Default sample size is 10 + for (let i = 0; i < 10; i = i + 1) { + random.setup((r) => r.getRandomInt(0, 100)).returns(() => i); + const result = extensionSurveyPrompt.shouldShowBanner(); + expect(result).to.equal(true, 'Banner should be shown'); + } + }); + + test('Always return true if sample size is 100', async () => { + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + 100 + ); + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + for (let i = 0; i < 100; i = i + 1) { + random.setup((r) => r.getRandomInt(0, 100)).returns(() => i); + const result = extensionSurveyPrompt.shouldShowBanner(); + expect(result).to.equal(true, 'Banner should be shown'); + } + }); + + test('Always return false if sample size is 0', async () => { + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + 0 + ); + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + for (let i = 0; i < 100; i = i + 1) { + random.setup((r) => r.getRandomInt(0, 100)).returns(() => i); + const result = extensionSurveyPrompt.shouldShowBanner(); + expect(result).to.equal(false, 'Banner should not be shown'); + } + random.verifyAll(); + }); +}); + +// tslint:disable-next-line: max-func-body-length +suite('Extension survey prompt - showSurvey()', () => { + let experiments: TypeMoq.IMock<IExperimentsManager>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let browserService: TypeMoq.IMock<IBrowserService>; + let random: TypeMoq.IMock<IRandom>; + let persistentStateFactory: IPersistentStateFactory; + let disableSurveyForTime: TypeMoq.IMock<IPersistentState<any>>; + let doNotShowAgain: TypeMoq.IMock<IPersistentState<any>>; + let platformService: TypeMoq.IMock<IPlatformService>; + let appEnvironment: TypeMoq.IMock<IApplicationEnvironment>; + let extensionSurveyPrompt: ExtensionSurveyPrompt; + setup(() => { + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + browserService = TypeMoq.Mock.ofType<IBrowserService>(); + random = TypeMoq.Mock.ofType<IRandom>(); + persistentStateFactory = mock(PersistentStateFactory); + disableSurveyForTime = TypeMoq.Mock.ofType<IPersistentState<any>>(); + doNotShowAgain = TypeMoq.Mock.ofType<IPersistentState<any>>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + when( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything() + ) + ).thenReturn(disableSurveyForTime.object); + when( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + ).thenReturn(doNotShowAgain.object); + experiments = TypeMoq.Mock.ofType<IExperimentsManager>(); + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + 10 + ); + }); + + test("Launch survey if 'Yes' option is clicked", async () => { + const packageJson = { + version: 'extensionVersion' + }; + const prompts = [ + ExtensionSurveyBanner.bannerLabelYes(), + ExtensionSurveyBanner.maybeLater(), + Common.doNotShowAgain() + ]; + const expectedUrl = `https://aka.ms/AA5rjx5?o=Windows&v=vscodeVersion&e=extensionVersion&m=sessionId`; + appEnvironment + .setup((a) => a.packageJson) + .returns(() => packageJson) + .verifiable(TypeMoq.Times.once()); + appEnvironment + .setup((a) => a.vscodeVersion) + .returns(() => 'vscodeVersion') + .verifiable(TypeMoq.Times.once()); + appEnvironment + .setup((a) => a.sessionId) + .returns(() => 'sessionId') + .verifiable(TypeMoq.Times.once()); + platformService + .setup((a) => a.osType) + .returns(() => OSType.Windows) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage(), ...prompts)) + .returns(() => Promise.resolve(ExtensionSurveyBanner.bannerLabelYes())) + .verifiable(TypeMoq.Times.once()); + browserService + .setup((s) => s.launch(expectedUrl)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + disableSurveyForTime + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + doNotShowAgain + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await extensionSurveyPrompt.showSurvey(); + + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything() + ) + ).once(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + ).never(); + appShell.verifyAll(); + browserService.verifyAll(); + disableSurveyForTime.verifyAll(); + doNotShowAgain.verifyAll(); + appEnvironment.verifyAll(); + platformService.verifyAll(); + }); + + test("Do nothing if 'Maybe later' option is clicked", async () => { + const prompts = [ + ExtensionSurveyBanner.bannerLabelYes(), + ExtensionSurveyBanner.maybeLater(), + Common.doNotShowAgain() + ]; + platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); + appShell + .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage(), ...prompts)) + .returns(() => Promise.resolve(ExtensionSurveyBanner.maybeLater())) + .verifiable(TypeMoq.Times.once()); + browserService + .setup((s) => s.launch(TypeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + disableSurveyForTime + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + doNotShowAgain + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await extensionSurveyPrompt.showSurvey(); + + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything() + ) + ).never(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + ).never(); + appShell.verifyAll(); + browserService.verifyAll(); + disableSurveyForTime.verifyAll(); + doNotShowAgain.verifyAll(); + platformService.verifyAll(); + }); + + test('Do nothing if no option is clicked', async () => { + const prompts = [ + ExtensionSurveyBanner.bannerLabelYes(), + ExtensionSurveyBanner.maybeLater(), + Common.doNotShowAgain() + ]; + platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); + appShell + .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage(), ...prompts)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + browserService + .setup((s) => s.launch(TypeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + disableSurveyForTime + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + doNotShowAgain + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await extensionSurveyPrompt.showSurvey(); + + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything() + ) + ).never(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + ).never(); + appShell.verifyAll(); + browserService.verifyAll(); + disableSurveyForTime.verifyAll(); + doNotShowAgain.verifyAll(); + platformService.verifyAll(); + }); + + test("Disable prompt if 'Do not show again' option is clicked", async () => { + const prompts = [ + ExtensionSurveyBanner.bannerLabelYes(), + ExtensionSurveyBanner.maybeLater(), + Common.doNotShowAgain() + ]; + platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); + appShell + .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage(), ...prompts)) + .returns(() => Promise.resolve(Common.doNotShowAgain())) + .verifiable(TypeMoq.Times.once()); + browserService + .setup((s) => s.launch(TypeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + disableSurveyForTime + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + doNotShowAgain + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await extensionSurveyPrompt.showSurvey(); + + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything() + ) + ).never(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + ).once(); + appShell.verifyAll(); + browserService.verifyAll(); + disableSurveyForTime.verifyAll(); + doNotShowAgain.verifyAll(); + platformService.verifyAll(); + }); +}); + +// tslint:disable-next-line: max-func-body-length +suite('Extension survey prompt - activate()', () => { + let appShell: TypeMoq.IMock<IApplicationShell>; + let browserService: TypeMoq.IMock<IBrowserService>; + let random: TypeMoq.IMock<IRandom>; + let persistentStateFactory: IPersistentStateFactory; + let shouldShowBanner: sinon.SinonStub<any>; + let showSurvey: sinon.SinonStub<any>; + let experiments: TypeMoq.IMock<IExperimentsManager>; + let extensionSurveyPrompt: ExtensionSurveyPrompt; + let platformService: TypeMoq.IMock<IPlatformService>; + let appEnvironment: TypeMoq.IMock<IApplicationEnvironment>; + setup(() => { + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + browserService = TypeMoq.Mock.ofType<IBrowserService>(); + random = TypeMoq.Mock.ofType<IRandom>(); + persistentStateFactory = mock(PersistentStateFactory); + experiments = TypeMoq.Mock.ofType<IExperimentsManager>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + }); + + teardown(() => { + sinon.restore(); + }); + + test("If user is not in 'ShowExtensionPrompt' experiment, send telemetry if in control group & return", async () => { + shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner'); + shouldShowBanner.callsFake(() => false); + showSurvey = sinon.stub(ExtensionSurveyPrompt.prototype, 'showSurvey'); + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + 10 + ); + experiments + .setup((exp) => exp.inExperiment(ShowExtensionSurveyPrompt.enabled)) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + experiments + .setup((exp) => exp.sendTelemetryIfInExperiment(ShowExtensionSurveyPrompt.control)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + await extensionSurveyPrompt.activate(); + assert.ok(shouldShowBanner.notCalled); + experiments.verifyAll(); + }); + + test("No survey is shown if shouldShowBanner() returns false and user is in 'ShowExtensionPrompt' experiment", async () => { + const deferred = createDeferred<true>(); + shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner'); + shouldShowBanner.callsFake(() => false); + showSurvey = sinon.stub(ExtensionSurveyPrompt.prototype, 'showSurvey'); + showSurvey.callsFake(() => { + deferred.resolve(true); + return Promise.resolve(); + }); + // waitTimeToShowSurvey = 50 ms + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + 10, + 50 + ); + experiments + .setup((exp) => exp.inExperiment(ShowExtensionSurveyPrompt.enabled)) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + experiments + .setup((exp) => exp.sendTelemetryIfInExperiment(TypeMoq.It.isAny())) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + await extensionSurveyPrompt.activate(); + assert.ok(shouldShowBanner.calledOnce); + + const doesSurveyShowUp = await Promise.race([deferred.promise, sleep(100).then(() => false)]); + assert.ok(showSurvey.notCalled); + expect(doesSurveyShowUp).to.equal(false, 'Survey should not appear'); + experiments.verifyAll(); + }); + + test("Survey is shown after waitTimeToShowSurvey if shouldShowBanner() returns true and user is in 'ShowExtensionPrompt' experiment", async () => { + const deferred = createDeferred<true>(); + shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner'); + shouldShowBanner.callsFake(() => true); + showSurvey = sinon.stub(ExtensionSurveyPrompt.prototype, 'showSurvey'); + showSurvey.callsFake(() => { + deferred.resolve(true); + return Promise.resolve(); + }); + // waitTimeToShowSurvey = 50 ms + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + 10, + 50 + ); + experiments + .setup((exp) => exp.inExperiment(ShowExtensionSurveyPrompt.enabled)) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + experiments + .setup((exp) => exp.sendTelemetryIfInExperiment(TypeMoq.It.isAny())) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + await extensionSurveyPrompt.activate(); + assert.ok(shouldShowBanner.calledOnce); + + const doesSurveyShowUp = await Promise.race([deferred.promise, sleep(200).then(() => false)]); + expect(doesSurveyShowUp).to.equal(true, 'Survey should appear'); + assert.ok(showSurvey.calledOnce); + experiments.verifyAll(); + }); +}); diff --git a/src/test/activation/languageServer/activator.unit.test.ts b/src/test/activation/languageServer/activator.unit.test.ts new file mode 100644 index 000000000000..13452eac1356 --- /dev/null +++ b/src/test/activation/languageServer/activator.unit.test.ts @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { DotNetLanguageServerActivator } from '../../../client/activation/languageServer/activator'; +import { DotNetLanguageServerManager } from '../../../client/activation/languageServer/manager'; +import { + ILanguageServerDownloader, + ILanguageServerFolderService, + ILanguageServerManager +} from '../../../client/activation/types'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IConfigurationService, IPythonExtensionBanner, IPythonSettings } from '../../../client/common/types'; +import { createDeferred } from '../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { sleep } from '../../core'; + +// tslint:disable:max-func-body-length + +suite('Microsoft Language Server - Activator', () => { + let activator: DotNetLanguageServerActivator; + let workspaceService: IWorkspaceService; + let manager: ILanguageServerManager; + let fs: IFileSystem; + let lsDownloader: ILanguageServerDownloader; + let lsFolderService: ILanguageServerFolderService; + let configuration: IConfigurationService; + let settings: IPythonSettings; + let banner: IPythonExtensionBanner; + setup(() => { + manager = mock(DotNetLanguageServerManager); + workspaceService = mock<IWorkspaceService>(); + fs = mock<IFileSystem>(); + lsDownloader = mock<ILanguageServerDownloader>(); + lsFolderService = mock<ILanguageServerFolderService>(); + configuration = mock<IConfigurationService>(); + settings = mock<IPythonSettings>(); + banner = mock<IPythonExtensionBanner>(); + when(configuration.getSettings(anything())).thenReturn(instance(settings)); + activator = new DotNetLanguageServerActivator( + instance(manager), + instance(workspaceService), + instance(fs), + instance(lsDownloader), + instance(lsFolderService), + instance(configuration), + instance(banner) + ); + }); + test('Manager must be started without any workspace', async () => { + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(manager.start(undefined, undefined)).thenResolve(); + when(settings.downloadLanguageServer).thenReturn(false); + + await activator.start(undefined); + + verify(manager.start(undefined, undefined)).once(); + verify(workspaceService.hasWorkspaceFolders).once(); + }); + test('Manager must be disposed', async () => { + activator.dispose(); + verify(manager.dispose()).once(); + }); + test('Server should be disconnected but be started', async () => { + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + await activator.start(undefined); + + verify(manager.start(undefined, undefined)).once(); + verify(manager.connect()).never(); + }); + test('Do not download LS if not required', async () => { + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(manager.start(undefined, undefined)).thenResolve(); + when(settings.downloadLanguageServer).thenReturn(false); + + await activator.start(undefined); + + verify(manager.start(undefined, undefined)).once(); + verify(workspaceService.hasWorkspaceFolders).once(); + verify(lsFolderService.getLanguageServerFolderName(anything())).never(); + verify(lsDownloader.downloadLanguageServer(anything(), anything())).never(); + }); + test('Do not download LS if not required', async () => { + const languageServerFolder = 'Some folder name'; + const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); + const mscorlib = path.join(languageServerFolderPath, 'mscorlib.dll'); + + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(manager.start(undefined, undefined)).thenResolve(); + when(settings.downloadLanguageServer).thenReturn(true); + when(lsFolderService.getLanguageServerFolderName(anything())).thenResolve(languageServerFolder); + when(fs.fileExists(mscorlib)).thenResolve(true); + + await activator.start(undefined); + + verify(manager.start(undefined, undefined)).once(); + verify(workspaceService.hasWorkspaceFolders).once(); + verify(lsFolderService.getLanguageServerFolderName(anything())).once(); + verify(lsDownloader.downloadLanguageServer(anything(), anything())).never(); + }); + test('Start language server after downloading', async () => { + const deferred = createDeferred<void>(); + const languageServerFolder = 'Some folder name'; + const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); + const mscorlib = path.join(languageServerFolderPath, 'mscorlib.dll'); + + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(manager.start(undefined, undefined)).thenResolve(); + when(settings.downloadLanguageServer).thenReturn(true); + when(lsFolderService.getLanguageServerFolderName(anything())).thenResolve(languageServerFolder); + when(fs.fileExists(mscorlib)).thenResolve(false); + when(lsDownloader.downloadLanguageServer(languageServerFolderPath, undefined)).thenReturn(deferred.promise); + + const promise = activator.start(undefined); + await sleep(1); + verify(workspaceService.hasWorkspaceFolders).once(); + verify(lsFolderService.getLanguageServerFolderName(anything())).once(); + verify(lsDownloader.downloadLanguageServer(anything(), undefined)).once(); + + verify(manager.start(undefined, undefined)).never(); + + deferred.resolve(); + await sleep(1); + verify(manager.start(undefined, undefined)).once(); + + await promise; + }); + test('Manager must be started with resource for first available workspace', async () => { + const uri = Uri.file(__filename); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + when(workspaceService.workspaceFolders).thenReturn([{ index: 0, name: '', uri }]); + when(manager.start(uri, undefined)).thenResolve(); + when(settings.downloadLanguageServer).thenReturn(false); + + await activator.start(undefined); + + verify(manager.start(uri, undefined)).once(); + verify(workspaceService.hasWorkspaceFolders).once(); + verify(workspaceService.workspaceFolders).once(); + }); + + test('Download and check if ICU config exists', async () => { + const languageServerFolder = 'Some folder name'; + const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); + const mscorlib = path.join(languageServerFolderPath, 'mscorlib.dll'); + const targetJsonFile = path.join( + languageServerFolderPath, + 'Microsoft.Python.LanguageServer.runtimeconfig.json' + ); + + when(settings.downloadLanguageServer).thenReturn(true); + when(lsFolderService.getLanguageServerFolderName(undefined)).thenResolve(languageServerFolder); + when(fs.fileExists(mscorlib)).thenResolve(false); + when(lsDownloader.downloadLanguageServer(languageServerFolderPath, undefined)).thenResolve(); + when(fs.fileExists(targetJsonFile)).thenResolve(false); + + await activator.ensureLanguageServerIsAvailable(undefined); + + verify(lsFolderService.getLanguageServerFolderName(undefined)).once(); + verify(lsDownloader.downloadLanguageServer(anything(), undefined)).once(); + verify(fs.fileExists(targetJsonFile)).once(); + }); + test('Download if contents of ICU config is not as expected', async () => { + const languageServerFolder = 'Some folder name'; + const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); + const mscorlib = path.join(languageServerFolderPath, 'mscorlib.dll'); + const targetJsonFile = path.join( + languageServerFolderPath, + 'Microsoft.Python.LanguageServer.runtimeconfig.json' + ); + const jsonContents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': false } } }; + + when(settings.downloadLanguageServer).thenReturn(true); + when(lsFolderService.getLanguageServerFolderName(undefined)).thenResolve(languageServerFolder); + when(fs.fileExists(mscorlib)).thenResolve(false); + when(lsDownloader.downloadLanguageServer(languageServerFolderPath, undefined)).thenResolve(); + when(fs.fileExists(targetJsonFile)).thenResolve(true); + when(fs.readFile(targetJsonFile)).thenResolve(JSON.stringify(jsonContents)); + + await activator.ensureLanguageServerIsAvailable(undefined); + + verify(lsFolderService.getLanguageServerFolderName(undefined)).once(); + verify(lsDownloader.downloadLanguageServer(anything(), undefined)).once(); + verify(fs.fileExists(targetJsonFile)).once(); + verify(fs.readFile(targetJsonFile)).once(); + }); + test('JSON file is created to ensure LS can start without ICU', async () => { + const targetJsonFile = path.join('some folder', 'Microsoft.Python.LanguageServer.runtimeconfig.json'); + const contents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': true } } }; + when(fs.fileExists(targetJsonFile)).thenResolve(false); + when(fs.writeFile(targetJsonFile, JSON.stringify(contents))).thenResolve(); + + await activator.prepareLanguageServerForNoICU('some folder'); + + verify(fs.fileExists(targetJsonFile)).atLeast(1); + verify(fs.writeFile(targetJsonFile, JSON.stringify(contents))).once(); + }); + test('JSON file is not created if it already exists with the right content', async () => { + const targetJsonFile = path.join('some folder', 'Microsoft.Python.LanguageServer.runtimeconfig.json'); + const contents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': true } } }; + const existingContents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': true } } }; + when(fs.fileExists(targetJsonFile)).thenResolve(true); + when(fs.readFile(targetJsonFile)).thenResolve(JSON.stringify(existingContents)); + + await activator.prepareLanguageServerForNoICU('some folder'); + + verify(fs.fileExists(targetJsonFile)).atLeast(1); + verify(fs.writeFile(targetJsonFile, JSON.stringify(contents))).never(); + verify(fs.readFile(targetJsonFile)).once(); + }); + test('JSON file is created if it already exists but with the wrong file content', async () => { + const targetJsonFile = path.join('some folder', 'Microsoft.Python.LanguageServer.runtimeconfig.json'); + const contents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': true } } }; + const existingContents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': false } } }; + when(fs.fileExists(targetJsonFile)).thenResolve(true); + when(fs.readFile(targetJsonFile)).thenResolve(JSON.stringify(existingContents)); + + await activator.prepareLanguageServerForNoICU('some folder'); + + verify(fs.fileExists(targetJsonFile)).atLeast(1); + verify(fs.writeFile(targetJsonFile, JSON.stringify(contents))).once(); + verify(fs.readFile(targetJsonFile)).once(); + }); +}); diff --git a/src/test/activation/languageServer/analysisOptions.unit.test.ts b/src/test/activation/languageServer/analysisOptions.unit.test.ts new file mode 100644 index 000000000000..34d12c454939 --- /dev/null +++ b/src/test/activation/languageServer/analysisOptions.unit.test.ts @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { expect } from 'chai'; +import { instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { ConfigurationChangeEvent, Uri, WorkspaceFolder } from 'vscode'; +import { DocumentFilter } from 'vscode-languageclient/node'; + +import { DotNetLanguageServerAnalysisOptions } from '../../../client/activation/languageServer/analysisOptions'; +import { DotNetLanguageServerFolderService } from '../../../client/activation/languageServer/languageServerFolderService'; +import { ILanguageServerFolderService, ILanguageServerOutputChannel } from '../../../client/activation/types'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { PYTHON_LANGUAGE } from '../../../client/common/constants'; +import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { + IConfigurationService, + IDisposable, + IExtensionContext, + IOutputChannel, + IPathUtils +} from '../../../client/common/types'; +import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { sleep } from '../../core'; + +// tslint:disable:no-unnecessary-override no-any chai-vague-errors no-unused-expression max-func-body-length + +suite('Language Server - Analysis Options', () => { + class TestClass extends DotNetLanguageServerAnalysisOptions { + public getDocumentFilters(workspaceFolder?: WorkspaceFolder): DocumentFilter[] { + return super.getDocumentFilters(workspaceFolder); + } + public getExcludedFiles(): string[] { + return super.getExcludedFiles(); + } + public getVsCodeExcludeSection(setting: string, list: string[]): void { + return super.getVsCodeExcludeSection(setting, list); + } + public getPythonExcludeSection(list: string[]): void { + return super.getPythonExcludeSection(list); + } + public getTypeshedPaths(): string[] { + return super.getTypeshedPaths(); + } + public onSettingsChanged(): void { + return super.onSettingsChanged(); + } + public async notifyIfValuesHaveChanged(oldArray: string[], newArray: string[]): Promise<void> { + return super.notifyIfValuesHaveChanged(oldArray, newArray); + } + } + let analysisOptions: TestClass; + let context: typemoq.IMock<IExtensionContext>; + let envVarsProvider: IEnvironmentVariablesProvider; + let configurationService: IConfigurationService; + let workspace: IWorkspaceService; + let outputChannel: IOutputChannel; + let lsOutputChannel: typemoq.IMock<ILanguageServerOutputChannel>; + let pathUtils: IPathUtils; + let lsFolderService: ILanguageServerFolderService; + setup(() => { + context = typemoq.Mock.ofType<IExtensionContext>(); + envVarsProvider = mock(EnvironmentVariablesProvider); + configurationService = mock(ConfigurationService); + workspace = mock(WorkspaceService); + outputChannel = typemoq.Mock.ofType<IOutputChannel>().object; + lsOutputChannel = typemoq.Mock.ofType<ILanguageServerOutputChannel>(); + lsOutputChannel.setup((l) => l.channel).returns(() => outputChannel); + pathUtils = mock(PathUtils); + lsFolderService = mock(DotNetLanguageServerFolderService); + analysisOptions = new TestClass( + context.object, + instance(envVarsProvider), + instance(configurationService), + instance(workspace), + lsOutputChannel.object, + instance(pathUtils), + instance(lsFolderService) + ); + }); + test('Initialize will add event handlers and will dispose them when running dispose', async () => { + const disposable1 = typemoq.Mock.ofType<IDisposable>(); + const disposable3 = typemoq.Mock.ofType<IDisposable>(); + when(workspace.onDidChangeConfiguration).thenReturn(() => disposable1.object); + when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn(() => disposable3.object); + + await analysisOptions.initialize(undefined, undefined); + + verify(workspace.onDidChangeConfiguration).once(); + verify(envVarsProvider.onDidEnvironmentVariablesChange).once(); + + disposable1.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); + disposable3.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); + + analysisOptions.dispose(); + + disposable1.verifyAll(); + disposable3.verifyAll(); + }); + test('Changes to settings or interpreter will be debounced', async () => { + const disposable1 = typemoq.Mock.ofType<IDisposable>(); + const disposable3 = typemoq.Mock.ofType<IDisposable>(); + let configChangedHandler!: Function; + when(workspace.onDidChangeConfiguration).thenReturn((cb) => { + configChangedHandler = cb; + return disposable1.object; + }); + when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn(() => disposable3.object); + let settingsChangedInvokedCount = 0; + analysisOptions.onDidChange(() => (settingsChangedInvokedCount += 1)); + + await analysisOptions.initialize(undefined, undefined); + expect(configChangedHandler).to.not.be.undefined; + + for (let i = 0; i < 100; i += 1) { + configChangedHandler.call(analysisOptions); + } + expect(settingsChangedInvokedCount).to.be.equal(0); + + await sleep(10); + + expect(settingsChangedInvokedCount).to.be.equal(1); + }); + test('If there are no changes then no events will be fired', async () => { + analysisOptions.getExcludedFiles = () => []; + analysisOptions.getTypeshedPaths = () => []; + + let eventFired = false; + analysisOptions.onDidChange(() => (eventFired = true)); + + analysisOptions.onSettingsChanged(); + await sleep(10); + + expect(eventFired).to.be.equal(false); + }); + test('Event must be fired if excluded files are different', async () => { + analysisOptions.getExcludedFiles = () => ['1']; + analysisOptions.getTypeshedPaths = () => []; + + let eventFired = false; + analysisOptions.onDidChange(() => (eventFired = true)); + + analysisOptions.onSettingsChanged(); + await sleep(10); + + expect(eventFired).to.be.equal(true); + }); + test('Event must be fired if typeshed files are different', async () => { + analysisOptions.getExcludedFiles = () => []; + analysisOptions.getTypeshedPaths = () => ['1']; + + let eventFired = false; + analysisOptions.onDidChange(() => (eventFired = true)); + + analysisOptions.onSettingsChanged(); + await sleep(10); + + expect(eventFired).to.be.equal(true); + }); + test('Event must be fired if interpreter info is different', async () => { + let eventFired = false; + analysisOptions.onDidChange(() => (eventFired = true)); + + analysisOptions.onSettingsChanged(); + await sleep(10); + + expect(eventFired).to.be.equal(true); + }); + test('Changes to settings will be filtered to current resource', async () => { + const uri = Uri.file(__filename); + const disposable1 = typemoq.Mock.ofType<IDisposable>(); + const disposable3 = typemoq.Mock.ofType<IDisposable>(); + let configChangedHandler!: Function; + let envVarChangedHandler!: Function; + when(workspace.onDidChangeConfiguration).thenReturn((cb) => { + configChangedHandler = cb; + return disposable1.object; + }); + when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn((cb) => { + envVarChangedHandler = cb; + return disposable3.object; + }); + let settingsChangedInvokedCount = 0; + + analysisOptions.onDidChange(() => (settingsChangedInvokedCount += 1)); + await analysisOptions.initialize(uri, undefined); + expect(configChangedHandler).to.not.be.undefined; + expect(envVarChangedHandler).to.not.be.undefined; + + for (let i = 0; i < 100; i += 1) { + const event = typemoq.Mock.ofType<ConfigurationChangeEvent>(); + event + .setup((e) => e.affectsConfiguration(typemoq.It.isValue('python'), typemoq.It.isValue(uri))) + .returns(() => true) + .verifiable(typemoq.Times.once()); + configChangedHandler.call(analysisOptions, event.object); + + event.verifyAll(); + } + expect(settingsChangedInvokedCount).to.be.equal(0); + + await sleep(10); + + expect(settingsChangedInvokedCount).to.be.equal(1); + }); + test('Ensure search pattern is not provided when there are no workspaces', () => { + when(workspace.workspaceFolders).thenReturn([]); + + const expectedSelector = [ + { scheme: 'file', language: PYTHON_LANGUAGE }, + { scheme: 'untitled', language: PYTHON_LANGUAGE }, + { scheme: 'vscode-notebook', language: PYTHON_LANGUAGE }, + { scheme: 'vscode-notebook-cell', language: PYTHON_LANGUAGE } + ]; + + const selector = analysisOptions.getDocumentFilters(); + + expect(selector).to.deep.equal(expectedSelector); + }); + test('Ensure search pattern is not provided in single-root workspaces', () => { + const workspaceFolder: WorkspaceFolder = { name: '', index: 0, uri: Uri.file(__dirname) }; + when(workspace.workspaceFolders).thenReturn([workspaceFolder]); + + const expectedSelector = [ + { scheme: 'file', language: PYTHON_LANGUAGE }, + { scheme: 'untitled', language: PYTHON_LANGUAGE }, + { scheme: 'vscode-notebook', language: PYTHON_LANGUAGE }, + { scheme: 'vscode-notebook-cell', language: PYTHON_LANGUAGE } + ]; + + const selector = analysisOptions.getDocumentFilters(workspaceFolder); + + expect(selector).to.deep.equal(expectedSelector); + }); + test('Ensure search pattern is provided in a multi-root workspace', () => { + const workspaceFolder1 = { name: '1', index: 0, uri: Uri.file(__dirname) }; + const workspaceFolder2 = { name: '2', index: 1, uri: Uri.file(__dirname) }; + when(workspace.workspaceFolders).thenReturn([workspaceFolder1, workspaceFolder2]); + + const expectedSelector = [ + { scheme: 'file', language: PYTHON_LANGUAGE, pattern: `${workspaceFolder1.uri.fsPath}/**/*` }, + { scheme: 'untitled', language: PYTHON_LANGUAGE }, + { scheme: 'vscode-notebook', language: PYTHON_LANGUAGE }, + { scheme: 'vscode-notebook-cell', language: PYTHON_LANGUAGE } + ]; + + const selector = analysisOptions.getDocumentFilters(workspaceFolder1); + + expect(selector).to.deep.equal(expectedSelector); + }); +}); diff --git a/src/test/activation/languageServer/downloadChannelRules.unit.test.ts b/src/test/activation/languageServer/downloadChannelRules.unit.test.ts new file mode 100644 index 000000000000..2b7c09eddb9e --- /dev/null +++ b/src/test/activation/languageServer/downloadChannelRules.unit.test.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import * as typeMoq from 'typemoq'; +import { + DownloadBetaChannelRule, + DownloadDailyChannelRule, + DownloadStableChannelRule +} from '../../../client/activation/common/downloadChannelRules'; +import { IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Microsoft Language Server Download Channel Rules', () => { + [undefined, path.join('a', 'b')].forEach((currentFolderPath) => { + const currentFolder = currentFolderPath ? { path: currentFolderPath, version: new SemVer('0.0.0') } : undefined; + const testSuffix = ` (${currentFolderPath ? 'with' : 'without'} an existing language server Folder`; + + test(`Daily channel should always download ${testSuffix}`, async () => { + const rule = new DownloadDailyChannelRule(); + expect(await rule.shouldLookForNewLanguageServer(currentFolder)).to.be.equal(true, 'invalid value'); + }); + + test(`Stable channel should be download only if folder doesn't exist ${testSuffix}`, async () => { + const rule = new DownloadStableChannelRule(); + const hasExistingLSFolder = currentFolderPath ? false : true; + expect(await rule.shouldLookForNewLanguageServer(currentFolder)).to.be.equal( + hasExistingLSFolder, + 'invalid value' + ); + }); + + suite('Betal channel', () => { + let serviceContainer: typeMoq.IMock<IServiceContainer>; + let stateFactory: typeMoq.IMock<IPersistentStateFactory>; + let state: typeMoq.IMock<IPersistentState<Boolean>>; + + setup(() => { + serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + stateFactory = typeMoq.Mock.ofType<IPersistentStateFactory>(); + state = typeMoq.Mock.ofType<IPersistentState<Boolean>>(); + stateFactory + .setup((s) => + s.createGlobalPersistentState(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny()) + ) + .returns(() => state.object) + .verifiable(typeMoq.Times.once()); + + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IPersistentStateFactory))) + .returns(() => stateFactory.object); + }); + function setupStateValue(value: boolean) { + state + .setup((s) => s.value) + .returns(() => value) + .verifiable(typeMoq.Times.atLeastOnce()); + } + test(`Should be download only if not checked previously ${testSuffix}`, async () => { + const rule = new DownloadBetaChannelRule(serviceContainer.object); + setupStateValue(true); + expect(await rule.shouldLookForNewLanguageServer(currentFolder)).to.be.equal(true, 'invalid value'); + }); + test(`Should be download only if checked previously ${testSuffix}`, async () => { + const rule = new DownloadBetaChannelRule(serviceContainer.object); + setupStateValue(false); + const shouldDownload = currentFolderPath ? false : true; + expect(await rule.shouldLookForNewLanguageServer(currentFolder)).to.be.equal( + shouldDownload, + 'invalid value' + ); + }); + }); + }); +}); diff --git a/src/test/activation/languageServer/downloader.unit.test.ts b/src/test/activation/languageServer/downloader.unit.test.ts new file mode 100644 index 000000000000..fa72ee5e8cf9 --- /dev/null +++ b/src/test/activation/languageServer/downloader.unit.test.ts @@ -0,0 +1,391 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { SemVer } from 'semver'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { Uri, WorkspaceConfiguration } from 'vscode'; +import { LanguageServerDownloader } from '../../../client/activation/common/downloader'; +import { DotNetLanguageServerFolderService } from '../../../client/activation/languageServer/languageServerFolderService'; +import { + ILanguageServerFolderService, + ILanguageServerOutputChannel, + IPlatformData +} from '../../../client/activation/types'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { FileDownloader } from '../../../client/common/net/fileDownloader'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IFileDownloader, IOutputChannel, Resource } from '../../../client/common/types'; +import { Common, LanguageService } from '../../../client/common/utils/localize'; +import { noop } from '../../core'; +import { MockOutputChannel } from '../../mockClasses'; + +use(chaiAsPromised); + +// tslint:disable-next-line:max-func-body-length +suite('Language Server Activation - Downloader', () => { + let languageServerDownloader: LanguageServerDownloader; + let folderService: TypeMoq.IMock<ILanguageServerFolderService>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let resource: Resource; + let outputChannel: IOutputChannel; + let lsOutputChannel: TypeMoq.IMock<ILanguageServerOutputChannel>; + setup(() => { + outputChannel = mock(MockOutputChannel); + lsOutputChannel = TypeMoq.Mock.ofType<ILanguageServerOutputChannel>(); + lsOutputChannel.setup((l) => l.channel).returns(() => instance(outputChannel)); + folderService = TypeMoq.Mock.ofType<ILanguageServerFolderService>(undefined, TypeMoq.MockBehavior.Strict); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(undefined, TypeMoq.MockBehavior.Strict); + resource = Uri.file(__dirname); + languageServerDownloader = new LanguageServerDownloader( + lsOutputChannel.object, + undefined as any, + folderService.object, + undefined as any, + undefined as any, + workspaceService.object, + undefined as any + ); + }); + + test('Get download info - HTTPS with resource', async () => { + const cfg = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Strict); + cfg.setup((c) => c.get('proxyStrictSSL', true)) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('http'), TypeMoq.It.isValue(resource))) + .returns(() => cfg.object) + .verifiable(TypeMoq.Times.once()); + + const pkg = makePkgInfo('ls', 'https://a.b.com/x/y/z/ls.nupkg'); + folderService + .setup((f) => f.getLatestLanguageServerVersion(resource)) + .returns(() => Promise.resolve(pkg)) + .verifiable(TypeMoq.Times.once()); + + const [uri, version, name] = await languageServerDownloader.getDownloadInfo(resource); + + folderService.verifyAll(); + workspaceService.verifyAll(); + expect(uri).to.equal(pkg.uri); + expect(version).to.equal(pkg.version.raw); + expect(name).to.equal('ls'); + }); + + test('Get download info - HTTPS without resource', async () => { + const cfg = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Strict); + cfg.setup((c) => c.get('proxyStrictSSL', true)) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('http'), undefined)) + .returns(() => cfg.object) + .verifiable(TypeMoq.Times.once()); + + const pkg = makePkgInfo('ls', 'https://a.b.com/x/y/z/ls.nupkg'); + folderService + .setup((f) => f.getLatestLanguageServerVersion(undefined)) + .returns(() => Promise.resolve(pkg)) + .verifiable(TypeMoq.Times.once()); + + const [uri, version, name] = await languageServerDownloader.getDownloadInfo(undefined); + + folderService.verifyAll(); + workspaceService.verifyAll(); + expect(uri).to.equal(pkg.uri); + expect(version).to.equal(pkg.version.raw); + expect(name).to.equal('ls'); + }); + + test('Get download info - HTTPS disabled', async () => { + const cfg = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Strict); + cfg.setup((c) => c.get('proxyStrictSSL', true)) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('http'), TypeMoq.It.isValue(resource))) + .returns(() => cfg.object) + .verifiable(TypeMoq.Times.once()); + + const pkg = makePkgInfo('ls', 'https://a.b.com/x/y/z/ls.nupkg'); + folderService + .setup((f) => f.getLatestLanguageServerVersion(resource)) + .returns(() => Promise.resolve(pkg)) + .verifiable(TypeMoq.Times.once()); + + const [uri, version, name] = await languageServerDownloader.getDownloadInfo(resource); + + folderService.verifyAll(); + workspaceService.verifyAll(); + // tslint:disable-next-line:no-http-string + expect(uri).to.deep.equal('http://a.b.com/x/y/z/ls.nupkg'); + expect(version).to.equal(pkg.version.raw); + expect(name).to.equal('ls'); + }); + + test('Get download info - HTTP', async () => { + // tslint:disable-next-line:no-http-string + const pkg = makePkgInfo('ls', 'http://a.b.com/x/y/z/ls.nupkg'); + folderService + .setup((f) => f.getLatestLanguageServerVersion(resource)) + .returns(() => Promise.resolve(pkg)) + .verifiable(TypeMoq.Times.once()); + + const [uri, version, name] = await languageServerDownloader.getDownloadInfo(resource); + + folderService.verifyAll(); + workspaceService.verifyAll(); + expect(uri).to.equal(pkg.uri); + expect(version).to.equal(pkg.version.raw); + expect(name).to.equal('ls'); + }); + + test('Get download info - bogus URL', async () => { + const pkg = makePkgInfo('ls', 'xyz'); + folderService + .setup((f) => f.getLatestLanguageServerVersion(resource)) + .returns(() => Promise.resolve(pkg)) + .verifiable(TypeMoq.Times.once()); + + const [uri, version, name] = await languageServerDownloader.getDownloadInfo(resource); + + folderService.verifyAll(); + workspaceService.verifyAll(); + expect(uri).to.equal(pkg.uri); + expect(version).to.equal(pkg.version.raw); + expect(name).to.equal('ls'); + }); + + suite('Test LanguageServerDownloader.downloadFile', () => { + let lsDownloader: LanguageServerDownloader; + let outputChannelDownload: IOutputChannel; + let fileDownloader: IFileDownloader; + let lsOutputChannelDownload: TypeMoq.IMock<ILanguageServerOutputChannel>; + // tslint:disable-next-line: no-http-string + const downloadUri = 'http://wow.com/file.txt'; + const downloadTitle = 'Downloadimg file.txt'; + setup(() => { + outputChannelDownload = mock(MockOutputChannel); + fileDownloader = mock(FileDownloader); + const lsFolderService = mock(DotNetLanguageServerFolderService); + const appShell = mock(ApplicationShell); + const fs = mock(FileSystem); + // tslint:disable-next-line: no-shadowed-variable + const workspaceService = mock(WorkspaceService); + lsOutputChannelDownload = TypeMoq.Mock.ofType<ILanguageServerOutputChannel>(); + lsOutputChannelDownload.setup((l) => l.channel).returns(() => instance(outputChannelDownload)); + + lsDownloader = new LanguageServerDownloader( + lsOutputChannelDownload.object, + instance(fileDownloader), + instance(lsFolderService), + instance(appShell), + instance(fs), + instance(workspaceService), + undefined as any + ); + }); + + test('Downloaded file name must be returned from file downloader and right args passed', async () => { + const downloadedFile = 'This is the downloaded file'; + when(fileDownloader.downloadFile(anything(), anything())).thenResolve(downloadedFile); + const expectedDownloadOptions = { + extension: '.nupkg', + outputChannel: instance(outputChannelDownload), + progressMessagePrefix: downloadTitle + }; + + const file = await lsDownloader.downloadFile(downloadUri, downloadTitle); + + expect(file).to.be.equal(downloadedFile); + verify(fileDownloader.downloadFile(anything(), anything())).once(); + verify(fileDownloader.downloadFile(downloadUri, deepEqual(expectedDownloadOptions))).once(); + }); + test('If download succeeds then log completion message', async () => { + when(fileDownloader.downloadFile(anything(), anything())).thenResolve(); + + await lsDownloader.downloadFile(downloadUri, downloadTitle); + + verify(fileDownloader.downloadFile(anything(), anything())).once(); + verify(outputChannelDownload.appendLine(LanguageService.extractionCompletedOutputMessage())).once(); + }); + test('If download fails do not log completion message', async () => { + const ex = new Error('kaboom'); + when(fileDownloader.downloadFile(anything(), anything())).thenReject(ex); + + const promise = lsDownloader.downloadFile(downloadUri, downloadTitle); + await promise.catch(noop); + + verify(outputChannelDownload.appendLine(LanguageService.extractionCompletedOutputMessage())).never(); + expect(promise).to.eventually.be.rejectedWith('kaboom'); + }); + }); + + // tslint:disable-next-line:max-func-body-length + suite('Test LanguageServerDownloader.downloadLanguageServer', () => { + const failure = new Error('kaboom'); + + class LanguageServerDownloaderTest extends LanguageServerDownloader { + // tslint:disable-next-line:no-unnecessary-override + public async downloadLanguageServer(destinationFolder: string, res?: Resource): Promise<void> { + return super.downloadLanguageServer(destinationFolder, res); + } + public async downloadFile(_uri: string, _title: string): Promise<string> { + throw failure; + } + } + class LanguageServerExtractorTest extends LanguageServerDownloader { + // tslint:disable-next-line:no-unnecessary-override + public async downloadLanguageServer(destinationFolder: string, res?: Resource): Promise<void> { + return super.downloadLanguageServer(destinationFolder, res); + } + // tslint:disable-next-line:no-unnecessary-override + public async getDownloadInfo(res?: Resource) { + return super.getDownloadInfo(res); + } + public async downloadFile() { + return 'random'; + } + protected async unpackArchive(_extensionPath: string, _tempFilePath: string): Promise<void> { + throw failure; + } + } + class LanguageServeBundledTest extends LanguageServerDownloader { + // tslint:disable-next-line:no-unnecessary-override + public async downloadLanguageServer(destinationFolder: string, res?: Resource): Promise<void> { + return super.downloadLanguageServer(destinationFolder, res); + } + // tslint:disable-next-line:no-unnecessary-override + public async getDownloadInfo(_res?: Resource): Promise<string[]> { + throw failure; + } + public async downloadFile(): Promise<string> { + throw failure; + } + protected async unpackArchive(_extensionPath: string, _tempFilePath: string): Promise<void> { + throw failure; + } + } + let output: TypeMoq.IMock<IOutputChannel>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let fs: TypeMoq.IMock<IFileSystem>; + let platformData: TypeMoq.IMock<IPlatformData>; + let languageServerDownloaderTest: LanguageServerDownloaderTest; + let languageServerExtractorTest: LanguageServerExtractorTest; + let languageServerBundledTest: LanguageServeBundledTest; + setup(() => { + appShell = TypeMoq.Mock.ofType<IApplicationShell>(undefined, TypeMoq.MockBehavior.Strict); + folderService = TypeMoq.Mock.ofType<ILanguageServerFolderService>(undefined, TypeMoq.MockBehavior.Strict); + output = TypeMoq.Mock.ofType<IOutputChannel>(); + fs = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); + platformData = TypeMoq.Mock.ofType<IPlatformData>(undefined, TypeMoq.MockBehavior.Strict); + lsOutputChannel = TypeMoq.Mock.ofType<ILanguageServerOutputChannel>(); + lsOutputChannel.setup((l) => l.channel).returns(() => output.object); + + languageServerDownloaderTest = new LanguageServerDownloaderTest( + lsOutputChannel.object, + undefined as any, + folderService.object, + appShell.object, + fs.object, + workspaceService.object, + undefined as any + ); + languageServerExtractorTest = new LanguageServerExtractorTest( + lsOutputChannel.object, + undefined as any, + folderService.object, + appShell.object, + fs.object, + workspaceService.object, + undefined as any + ); + languageServerBundledTest = new LanguageServeBundledTest( + lsOutputChannel.object, + undefined as any, + folderService.object, + appShell.object, + fs.object, + workspaceService.object, + undefined as any + ); + }); + test('Display error message if LS downloading fails', async () => { + folderService.setup((f) => f.skipDownload()).returns(async () => false); + const pkg = makePkgInfo('ls', 'xyz'); + folderService.setup((f) => f.getLatestLanguageServerVersion(resource)).returns(() => Promise.resolve(pkg)); + output.setup((o) => o.appendLine(LanguageService.downloadFailedOutputMessage())); + output.setup((o) => o.appendLine((failure as unknown) as string)); + appShell + .setup((a) => a.showErrorMessage(LanguageService.lsFailedToDownload(), Common.openOutputPanel())) + .returns(() => Promise.resolve(undefined)); + + let actualFailure: Error | undefined; + try { + await languageServerDownloaderTest.downloadLanguageServer('', resource); + } catch (err) { + actualFailure = err; + } + + expect(actualFailure).to.not.equal(undefined, 'error not thrown'); + folderService.verifyAll(); + output.verifyAll(); + appShell.verifyAll(); + fs.verifyAll(); + platformData.verifyAll(); + }); + test('Display error message if LS extraction fails', async () => { + folderService.setup((f) => f.skipDownload()).returns(async () => false); + const pkg = makePkgInfo('ls', 'xyz'); + folderService.setup((f) => f.getLatestLanguageServerVersion(resource)).returns(() => Promise.resolve(pkg)); + output.setup((o) => o.appendLine(LanguageService.extractionFailedOutputMessage())); + output.setup((o) => o.appendLine((failure as unknown) as string)); + appShell + .setup((a) => a.showErrorMessage(LanguageService.lsFailedToExtract(), Common.openOutputPanel())) + .returns(() => Promise.resolve(undefined)); + + let actualFailure: Error | undefined; + try { + await languageServerExtractorTest.downloadLanguageServer('', resource); + } catch (err) { + actualFailure = err; + } + + expect(actualFailure).to.not.equal(undefined, 'error not thrown'); + folderService.verifyAll(); + output.verifyAll(); + appShell.verifyAll(); + fs.verifyAll(); + platformData.verifyAll(); + }); + test('No download if bundled', async () => { + folderService.setup((f) => f.skipDownload()).returns(async () => true); + + await languageServerBundledTest.downloadLanguageServer('', resource); + + folderService.verifyAll(); + output.verifyAll(); + appShell.verifyAll(); + fs.verifyAll(); + platformData.verifyAll(); + }); + }); +}); + +function makePkgInfo(name: string, uri: string, version: string = '0.0.0') { + return { + package: name, + uri: uri, + version: new SemVer(version) + } as any; +} diff --git a/src/test/activation/languageServer/languageClientFactory.unit.test.ts b/src/test/activation/languageServer/languageClientFactory.unit.test.ts new file mode 100644 index 000000000000..5b1b73ffe03e --- /dev/null +++ b/src/test/activation/languageServer/languageClientFactory.unit.test.ts @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +//tslint:disable:no-require-imports no-require-imports no-var-requires no-any no-unnecessary-class max-func-body-length match-default-export-name + +import { expect } from 'chai'; +import * as path from 'path'; +import rewiremock from 'rewiremock'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node'; +import { + DotNetDownloadedLanguageClientFactory, + DotNetLanguageClientFactory, + DotNetSimpleLanguageClientFactory +} from '../../../client/activation/languageServer/languageClientFactory'; +import { DotNetLanguageServerFolderService } from '../../../client/activation/languageServer/languageServerFolderService'; +import { PlatformData } from '../../../client/activation/languageServer/platformData'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; +import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; + +const dotNetCommand = 'dotnet'; +const languageClientName = 'Python Tools'; + +suite('Language Server - LanguageClient Factory', () => { + let configurationService: IConfigurationService; + let settings: IPythonSettings; + setup(() => { + configurationService = mock(ConfigurationService); + settings = mock(PythonSettings); + when(configurationService.getSettings(anything())).thenReturn(instance(settings)); + }); + teardown(() => { + rewiremock.disable(); + }); + + test('Download factory is used when required to download the LS', async () => { + const downloadFactory = mock(DotNetDownloadedLanguageClientFactory); + const simpleFactory = mock(DotNetSimpleLanguageClientFactory); + const envVarProvider = mock(EnvironmentVariablesProvider); + const activationService = mock(EnvironmentActivationService); + const factory = new DotNetLanguageClientFactory( + instance(configurationService), + instance(envVarProvider), + instance(activationService), + undefined as any, + undefined as any, + instance(downloadFactory), + instance(simpleFactory) + ); + const uri = Uri.file(__filename); + const options = typemoq.Mock.ofType<LanguageClientOptions>().object; + const env = { FOO: 'bar' }; + when(settings.downloadLanguageServer).thenReturn(true); + when(envVarProvider.getEnvironmentVariables(uri)).thenReturn(Promise.resolve(env)); + when(activationService.getActivatedEnvironmentVariables(uri)).thenReturn(); + + await factory.createLanguageClient(uri, undefined, options); + + verify(configurationService.getSettings(uri)).once(); + verify(downloadFactory.createLanguageClient(uri, undefined, options, env)).once(); + verify(simpleFactory.createLanguageClient(uri, undefined, options, env)).never(); + }); + test('Simple factory is used when not required to download the LS', async () => { + const downloadFactory = mock(DotNetDownloadedLanguageClientFactory); + const simpleFactory = mock(DotNetSimpleLanguageClientFactory); + const envVarProvider = mock(EnvironmentVariablesProvider); + const activationService = mock(EnvironmentActivationService); + const factory = new DotNetLanguageClientFactory( + instance(configurationService), + instance(envVarProvider), + instance(activationService), + undefined as any, + undefined as any, + instance(downloadFactory), + instance(simpleFactory) + ); + const uri = Uri.file(__filename); + const options = typemoq.Mock.ofType<LanguageClientOptions>().object; + const env = { FOO: 'bar' }; + when(settings.downloadLanguageServer).thenReturn(false); + when(envVarProvider.getEnvironmentVariables(uri)).thenReturn(Promise.resolve(env)); + when(activationService.getActivatedEnvironmentVariables(uri)).thenReturn(); + + await factory.createLanguageClient(uri, undefined, options); + + verify(configurationService.getSettings(uri)).once(); + verify(downloadFactory.createLanguageClient(uri, undefined, options, env)).never(); + verify(simpleFactory.createLanguageClient(uri, undefined, options, env)).once(); + }); + test('Download factory will make use of the language server folder name and client will be created', async () => { + const platformData = mock(PlatformData); + const lsFolderService = mock(DotNetLanguageServerFolderService); + const factory = new DotNetDownloadedLanguageClientFactory(instance(platformData), instance(lsFolderService)); + const uri = Uri.file(__filename); + const options = typemoq.Mock.ofType<LanguageClientOptions>().object; + const languageServerFolder = 'some folder name'; + const engineDllName = 'xyz.dll'; + when(lsFolderService.getLanguageServerFolderName(anything())).thenResolve(languageServerFolder); + when(platformData.engineExecutableName).thenReturn(engineDllName); + + const serverModule = path.join(EXTENSION_ROOT_DIR, languageServerFolder, engineDllName); + const expectedServerOptions = { + run: { command: serverModule, args: [], options: { stdio: 'pipe', env: { FOO: 'bar' } } }, + debug: { command: serverModule, args: ['--debug'], options: { stdio: 'pipe', env: { FOO: 'bar' } } } + }; + rewiremock.enable(); + + class MockClass { + constructor( + language: string, + name: string, + serverOptions: ServerOptions, + clientOptions: LanguageClientOptions + ) { + expect(language).to.be.equal('python'); + expect(name).to.be.equal(languageClientName); + expect(clientOptions).to.be.deep.equal(options); + expect(serverOptions).to.be.deep.equal(expectedServerOptions); + } + } + rewiremock('vscode-languageclient/node').with({ LanguageClient: MockClass }); + + const client = await factory.createLanguageClient(uri, undefined, options, { FOO: 'bar' }); + + verify(lsFolderService.getLanguageServerFolderName(anything())).once(); + verify(platformData.engineExecutableName).atLeast(1); + verify(platformData.engineDllName).never(); + verify(platformData.platformName).never(); + expect(client).to.be.instanceOf(MockClass); + }); + test('Simple factory will make use of the language server folder name and client will be created', async () => { + const platformData = mock(PlatformData); + const lsFolderService = mock(DotNetLanguageServerFolderService); + const factory = new DotNetSimpleLanguageClientFactory(instance(platformData), instance(lsFolderService)); + const uri = Uri.file(__filename); + const options = typemoq.Mock.ofType<LanguageClientOptions>().object; + const languageServerFolder = 'some folder name'; + const engineDllName = 'xyz.dll'; + when(lsFolderService.getLanguageServerFolderName(anything())).thenResolve(languageServerFolder); + when(platformData.engineDllName).thenReturn(engineDllName); + + const serverModule = path.join(EXTENSION_ROOT_DIR, languageServerFolder, engineDllName); + const expectedServerOptions = { + run: { command: dotNetCommand, args: [serverModule], options: { stdio: 'pipe', env: { FOO: 'bar' } } }, + debug: { + command: dotNetCommand, + args: [serverModule, '--debug'], + options: { stdio: 'pipe', env: { FOO: 'bar' } } + } + }; + rewiremock.enable(); + + class MockClass { + constructor( + language: string, + name: string, + serverOptions: ServerOptions, + clientOptions: LanguageClientOptions + ) { + expect(language).to.be.equal('python'); + expect(name).to.be.equal(languageClientName); + expect(clientOptions).to.be.deep.equal(options); + expect(serverOptions).to.be.deep.equal(expectedServerOptions); + } + } + rewiremock('vscode-languageclient/node').with({ LanguageClient: MockClass }); + + const client = await factory.createLanguageClient(uri, undefined, options, { FOO: 'bar' }); + + verify(lsFolderService.getLanguageServerFolderName(anything())).once(); + verify(platformData.engineExecutableName).never(); + verify(platformData.engineDllName).once(); + verify(platformData.platformName).never(); + expect(client).to.be.instanceOf(MockClass); + }); +}); diff --git a/src/test/activation/languageServer/languageServer.unit.test.ts b/src/test/activation/languageServer/languageServer.unit.test.ts new file mode 100644 index 000000000000..ba13b59f7c1f --- /dev/null +++ b/src/test/activation/languageServer/languageServer.unit.test.ts @@ -0,0 +1,310 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { Disposable, LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; +import { DotNetLanguageClientFactory } from '../../../client/activation/languageServer/languageClientFactory'; +import { DotNetLanguageServerProxy } from '../../../client/activation/languageServer/languageServerProxy'; +import { ILanguageClientFactory } from '../../../client/activation/types'; +import { ICommandManager } from '../../../client/common/application/types'; +import '../../../client/common/extensions'; +import { IConfigurationService, IDisposable, IPythonSettings } from '../../../client/common/types'; +import { sleep } from '../../../client/common/utils/async'; +import { UnitTestManagementService } from '../../../client/testing/main'; +import { ITestManagementService } from '../../../client/testing/types'; + +//tslint:disable:no-require-imports no-require-imports no-var-requires no-any no-unnecessary-class max-func-body-length + +suite('Language Server - LanguageServer', () => { + class LanguageServerTest extends DotNetLanguageServerProxy { + // tslint:disable-next-line:no-unnecessary-override + public async registerTestServices() { + return super.registerTestServices(); + } + } + let clientFactory: ILanguageClientFactory; + let server: LanguageServerTest; + let client: typemoq.IMock<LanguageClient>; + let testManager: ITestManagementService; + let configService: typemoq.IMock<IConfigurationService>; + let commandManager: typemoq.IMock<ICommandManager>; + setup(() => { + client = typemoq.Mock.ofType<LanguageClient>(); + clientFactory = mock(DotNetLanguageClientFactory); + testManager = mock(UnitTestManagementService); + configService = typemoq.Mock.ofType<IConfigurationService>(); + + commandManager = typemoq.Mock.ofType<ICommandManager>(); + commandManager + .setup((c) => c.registerCommand(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => { + return typemoq.Mock.ofType<Disposable>().object; + }); + server = new LanguageServerTest(instance(clientFactory), instance(testManager), configService.object); + }); + teardown(() => { + client.setup((c) => c.stop()).returns(() => Promise.resolve()); + server.dispose(); + }); + test('Loading extension will not throw an error if not activated', () => { + expect(() => server.loadExtension()).not.throw(); + }); + test('Loading extension will not throw an error if not activated but after it loads message will be sent', async () => { + const loadExtensionArgs = { x: 1 }; + + expect(() => server.loadExtension({ a: '2' })).not.throw(); + + client.verify((c) => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + + const uri = Uri.file(__filename); + const options = typemoq.Mock.ofType<LanguageClientOptions>().object; + + const pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); + pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true); + configService.setup((c) => c.getSettings(uri)).returns(() => pythonSettings.object); + + const onTelemetryDisposable = typemoq.Mock.ofType<IDisposable>(); + client.setup((c) => c.onTelemetry(typemoq.It.isAny())).returns(() => onTelemetryDisposable.object); + + client.setup((c) => (c as any).then).returns(() => undefined); + when(clientFactory.createLanguageClient(uri, undefined, options)).thenResolve(client.object); + const startDisposable = typemoq.Mock.ofType<IDisposable>(); + client.setup((c) => c.stop()).returns(() => Promise.resolve()); + client + .setup((c) => c.start()) + .returns(() => startDisposable.object) + .verifiable(typemoq.Times.once()); + client + .setup((c) => + c.sendRequest(typemoq.It.isValue('python/loadExtension'), typemoq.It.isValue(loadExtensionArgs)) + ) + .returns(() => Promise.resolve(undefined) as any); + + expect(() => server.loadExtension(loadExtensionArgs)).not.throw(); + client.verify((c) => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + client + .setup((c) => c.initializeResult) + .returns(() => false as any) + .verifiable(typemoq.Times.once()); + + server.start(uri, undefined, options).ignoreErrors(); + + // Even though server has started request should not yet be sent out. + // Not until language client has initialized. + expect(() => server.loadExtension(loadExtensionArgs)).not.throw(); + client.verify((c) => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + + // // Initialize language client and verify that the request was sent out. + client + .setup((c) => c.initializeResult) + .returns(() => true as any) + .verifiable(typemoq.Times.once()); + await sleep(120); + + verify(testManager.activate(anything())).once(); + client.verify((c) => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.atLeast(2)); + }); + test('Send telemetry when LS has started and disposes appropriately', async () => { + const loadExtensionArgs = { x: 1 }; + const uri = Uri.file(__filename); + const options = typemoq.Mock.ofType<LanguageClientOptions>().object; + + const pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); + pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true); + configService.setup((c) => c.getSettings(uri)).returns(() => pythonSettings.object); + + const onTelemetryDisposable = typemoq.Mock.ofType<IDisposable>(); + client.setup((c) => c.onTelemetry(typemoq.It.isAny())).returns(() => onTelemetryDisposable.object); + + client.setup((c) => (c as any).then).returns(() => undefined); + when(clientFactory.createLanguageClient(uri, undefined, options)).thenResolve(client.object); + const startDisposable = typemoq.Mock.ofType<IDisposable>(); + client.setup((c) => c.stop()).returns(() => Promise.resolve()); + client + .setup((c) => c.start()) + .returns(() => startDisposable.object) + .verifiable(typemoq.Times.once()); + client + .setup((c) => + c.sendRequest(typemoq.It.isValue('python/loadExtension'), typemoq.It.isValue(loadExtensionArgs)) + ) + .returns(() => Promise.resolve(undefined) as any); + + expect(() => server.loadExtension(loadExtensionArgs)).not.throw(); + client.verify((c) => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + client + .setup((c) => c.initializeResult) + .returns(() => false as any) + .verifiable(typemoq.Times.once()); + + const promise = server.start(uri, undefined, options); + + // Even though server has started request should not yet be sent out. + // Not until language client has initialized. + expect(() => server.loadExtension(loadExtensionArgs)).not.throw(); + client.verify((c) => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + + // // Initialize language client and verify that the request was sent out. + client + .setup((c) => c.initializeResult) + .returns(() => true as any) + .verifiable(typemoq.Times.once()); + await sleep(120); + + verify(testManager.activate(anything())).once(); + expect(() => server.loadExtension(loadExtensionArgs)).to.not.throw(); + client.verify((c) => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); + client.verify((c) => c.stop(), typemoq.Times.never()); + + await promise; + server.dispose(); + + client.verify((c) => c.stop(), typemoq.Times.once()); + startDisposable.verify((d) => d.dispose(), typemoq.Times.once()); + }); + test('Ensure Errors raised when starting test manager are not bubbled up', async () => { + await server.registerTestServices(); + }); + test('Register telemetry handler if LS was downloadeded', async () => { + client.verify((c) => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + + const uri = Uri.file(__filename); + const options = typemoq.Mock.ofType<LanguageClientOptions>().object; + + const pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); + pythonSettings + .setup((p) => p.downloadLanguageServer) + .returns(() => true) + .verifiable(typemoq.Times.once()); + configService + .setup((c) => c.getSettings(uri)) + .returns(() => pythonSettings.object) + .verifiable(typemoq.Times.once()); + + const onTelemetryDisposable = typemoq.Mock.ofType<IDisposable>(); + client + .setup((c) => c.onTelemetry(typemoq.It.isAny())) + .returns(() => onTelemetryDisposable.object) + .verifiable(typemoq.Times.once()); + + client.setup((c) => (c as any).then).returns(() => undefined); + when(clientFactory.createLanguageClient(uri, undefined, options)).thenResolve(client.object); + const startDisposable = typemoq.Mock.ofType<IDisposable>(); + client.setup((c) => c.stop()).returns(() => Promise.resolve()); + client + .setup((c) => c.start()) + .returns(() => startDisposable.object) + .verifiable(typemoq.Times.once()); + + server.start(uri, undefined, options).ignoreErrors(); + + // Initialize language client and verify that the request was sent out. + client + .setup((c) => c.initializeResult) + .returns(() => true as any) + .verifiable(typemoq.Times.once()); + await sleep(120); + + verify(testManager.activate(anything())).once(); + + client.verify((c) => c.onTelemetry(typemoq.It.isAny()), typemoq.Times.once()); + pythonSettings.verifyAll(); + configService.verifyAll(); + }); + test('Do not register telemetry handler if LS was not downloadeded', async () => { + client.verify((c) => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + + const uri = Uri.file(__filename); + const options = typemoq.Mock.ofType<LanguageClientOptions>().object; + + const pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); + pythonSettings + .setup((p) => p.downloadLanguageServer) + .returns(() => false) + .verifiable(typemoq.Times.once()); + configService + .setup((c) => c.getSettings(uri)) + .returns(() => pythonSettings.object) + .verifiable(typemoq.Times.once()); + + const onTelemetryDisposable = typemoq.Mock.ofType<IDisposable>(); + client + .setup((c) => c.onTelemetry(typemoq.It.isAny())) + .returns(() => onTelemetryDisposable.object) + .verifiable(typemoq.Times.once()); + + client.setup((c) => (c as any).then).returns(() => undefined); + when(clientFactory.createLanguageClient(uri, undefined, options)).thenResolve(client.object); + const startDisposable = typemoq.Mock.ofType<IDisposable>(); + client.setup((c) => c.stop()).returns(() => Promise.resolve()); + client + .setup((c) => c.start()) + .returns(() => startDisposable.object) + .verifiable(typemoq.Times.once()); + + server.start(uri, undefined, options).ignoreErrors(); + + // Initialize language client and verify that the request was sent out. + client + .setup((c) => c.initializeResult) + .returns(() => true as any) + .verifiable(typemoq.Times.once()); + await sleep(120); + + verify(testManager.activate(anything())).once(); + + client.verify((c) => c.onTelemetry(typemoq.It.isAny()), typemoq.Times.never()); + pythonSettings.verifyAll(); + configService.verifyAll(); + }); + test('Do not register services if languageClient is disposed while waiting for it to start', async () => { + const uri = Uri.file(__filename); + const options = typemoq.Mock.ofType<LanguageClientOptions>().object; + + const pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); + pythonSettings + .setup((p) => p.downloadLanguageServer) + .returns(() => false) + .verifiable(typemoq.Times.never()); + configService + .setup((c) => c.getSettings(uri)) + .returns(() => pythonSettings.object) + .verifiable(typemoq.Times.never()); + + client.setup((c) => (c as any).then).returns(() => undefined); + client + .setup((c) => c.initializeResult) + .returns(() => undefined) + .verifiable(typemoq.Times.atLeastOnce()); + when(clientFactory.createLanguageClient(uri, undefined, options)).thenResolve(client.object); + const startDisposable = typemoq.Mock.ofType<IDisposable>(); + client.setup((c) => c.stop()).returns(() => Promise.resolve()); + client + .setup((c) => c.start()) + .returns(() => startDisposable.object) + .verifiable(typemoq.Times.once()); + + const promise = server.start(uri, undefined, options); + // Wait until we start ls client and check if it is ready. + await sleep(200); + // Confirm we checked if it is ready. + client.verifyAll(); + // Now dispose the language client. + server.dispose(); + // Wait until we check if it is ready. + await sleep(500); + + // Promise should resolve without any errors. + await promise; + + verify(testManager.activate(anything())).never(); + client.verify((c) => c.onTelemetry(typemoq.It.isAny()), typemoq.Times.never()); + pythonSettings.verifyAll(); + configService.verifyAll(); + }); +}); diff --git a/src/test/activation/languageServer/languageServerCompatibilityService.unit.test.ts b/src/test/activation/languageServer/languageServerCompatibilityService.unit.test.ts new file mode 100644 index 000000000000..8ea004ed6225 --- /dev/null +++ b/src/test/activation/languageServer/languageServerCompatibilityService.unit.test.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { LanguageServerCompatibilityService } from '../../../client/activation/languageServer/languageServerCompatibilityService'; +import { ILanguageServerCompatibilityService } from '../../../client/activation/types'; +import { IDotNetCompatibilityService } from '../../../client/common/dotnet/types'; + +suite('Language Server Support', () => { + let compatService: typeMoq.IMock<IDotNetCompatibilityService>; + let service: ILanguageServerCompatibilityService; + setup(() => { + compatService = typeMoq.Mock.ofType<IDotNetCompatibilityService>(); + service = new LanguageServerCompatibilityService(compatService.object); + }); + test('Not supported if there are errors ', async () => { + compatService.setup((c) => c.isSupported()).returns(() => Promise.reject(new Error('kaboom'))); + const supported = await service.isSupported(); + expect(supported).to.equal(false, 'incorrect'); + }); + test('Not supported if there are not errors ', async () => { + compatService.setup((c) => c.isSupported()).returns(() => Promise.resolve(false)); + const supported = await service.isSupported(); + expect(supported).to.equal(false, 'incorrect'); + }); + test('Support if there are not errors ', async () => { + compatService.setup((c) => c.isSupported()).returns(() => Promise.resolve(true)); + const supported = await service.isSupported(); + expect(supported).to.equal(true, 'incorrect'); + }); +}); diff --git a/src/test/activation/languageServer/languageServerExtension.unit.test.ts b/src/test/activation/languageServer/languageServerExtension.unit.test.ts new file mode 100644 index 000000000000..d18bacd66e3d --- /dev/null +++ b/src/test/activation/languageServer/languageServerExtension.unit.test.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { LanguageServerExtension } from '../../../client/activation/languageServer/languageServerExtension'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { ICommandManager } from '../../../client/common/application/types'; +import { IDisposable } from '../../../client/common/types'; + +use(chaiAsPromised); + +// tslint:disable:max-func-body-length no-any chai-vague-errors no-unused-expression + +const loadExtensionCommand = 'python._loadLanguageServerExtension'; + +suite('Language Server - Language Server Extension', () => { + class LanguageServerExtensionTest extends LanguageServerExtension { + // tslint:disable-next-line:no-unnecessary-override + public async register(): Promise<void> { + return super.register(); + } + public clearLoadExtensionArgs() { + super.loadExtensionArgs = undefined; + } + } + let extension: LanguageServerExtensionTest; + let cmdManager: ICommandManager; + let commandRegistrationDisposable: typemoq.IMock<IDisposable>; + setup(() => { + cmdManager = mock(CommandManager); + commandRegistrationDisposable = typemoq.Mock.ofType<IDisposable>(); + extension = new LanguageServerExtensionTest(instance(cmdManager)); + extension.clearLoadExtensionArgs(); + }); + test('Must register command handler', async () => { + when(cmdManager.registerCommand(loadExtensionCommand, anything())).thenReturn( + commandRegistrationDisposable.object + ); + await extension.register(); + verify(cmdManager.registerCommand(loadExtensionCommand, anything())).once(); + extension.dispose(); + commandRegistrationDisposable.verify((d) => d.dispose(), typemoq.Times.once()); + }); +}); diff --git a/src/test/activation/languageServer/languageServerFolderService.unit.test.ts b/src/test/activation/languageServer/languageServerFolderService.unit.test.ts new file mode 100644 index 000000000000..381ff514c4ad --- /dev/null +++ b/src/test/activation/languageServer/languageServerFolderService.unit.test.ts @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as semver from 'semver'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { DotNetLanguageServerFolderService } from '../../../client/activation/languageServer/languageServerFolderService'; +import { IDownloadChannelRule, ILanguageServerPackageService } from '../../../client/activation/types'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable:max-func-body-length + +suite('Language Server Folder Service', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let languageServerFolderService: DotNetLanguageServerFolderService; + const resource = Uri.parse('a'); + + // tslint:disable-next-line:max-func-body-length + suite('Method getLanguageServerFolderName()', async () => { + // tslint:disable-next-line: no-any + let shouldLookForNewLS: sinon.SinonStub<any>; + // tslint:disable-next-line: no-any + let getCurrentLanguageServerDirectory: sinon.SinonStub<any>; + const currentLSDirectory = { + path: 'path/to/currentLSDirectoryName', + version: new semver.SemVer('1.2.3') + }; + let languageServerPackageService: TypeMoq.IMock<ILanguageServerPackageService>; + setup(() => { + languageServerPackageService = TypeMoq.Mock.ofType<ILanguageServerPackageService>(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer + .setup((s) => s.get<ILanguageServerPackageService>(ILanguageServerPackageService)) + .returns(() => languageServerPackageService.object); + }); + teardown(() => { + sinon.restore(); + }); + + test('Returns current Language server directory name if rule says we should not look for new LS', async () => { + shouldLookForNewLS = sinon.stub( + DotNetLanguageServerFolderService.prototype, + 'shouldLookForNewLanguageServer' + ); + shouldLookForNewLS.resolves(false); + getCurrentLanguageServerDirectory = sinon.stub( + DotNetLanguageServerFolderService.prototype, + 'getCurrentLanguageServerDirectory' + ); + getCurrentLanguageServerDirectory.resolves(currentLSDirectory); + languageServerFolderService = new DotNetLanguageServerFolderService(serviceContainer.object); + const lsFolderName = await languageServerFolderService.getLanguageServerFolderName(resource); + expect(lsFolderName).to.equal('currentLSDirectoryName'); + }); + + test('Returns current Language server directory name if fetching latest LS version returns undefined', async () => { + shouldLookForNewLS = sinon.stub( + DotNetLanguageServerFolderService.prototype, + 'shouldLookForNewLanguageServer' + ); + shouldLookForNewLS.resolves(true); + languageServerPackageService + .setup((l) => l.getLatestNugetPackageVersion(resource)) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(undefined) as any); + getCurrentLanguageServerDirectory = sinon.stub( + DotNetLanguageServerFolderService.prototype, + 'getCurrentLanguageServerDirectory' + ); + getCurrentLanguageServerDirectory.resolves(currentLSDirectory); + languageServerFolderService = new DotNetLanguageServerFolderService(serviceContainer.object); + const lsFolderName = await languageServerFolderService.getLanguageServerFolderName(resource); + expect(lsFolderName).to.equal('currentLSDirectoryName'); + }); + + test('Returns current Language server directory name if fetched latest LS version is less than the current LS version', async () => { + shouldLookForNewLS = sinon.stub( + DotNetLanguageServerFolderService.prototype, + 'shouldLookForNewLanguageServer' + ); + shouldLookForNewLS.resolves(true); + const nugetPackage = { + package: 'packageName', + version: new semver.SemVer('1.1.3'), + uri: 'nugetUri' + }; + languageServerPackageService + .setup((l) => l.getLatestNugetPackageVersion(resource)) + .returns(() => Promise.resolve(nugetPackage)); + getCurrentLanguageServerDirectory = sinon.stub( + DotNetLanguageServerFolderService.prototype, + 'getCurrentLanguageServerDirectory' + ); + getCurrentLanguageServerDirectory.resolves(currentLSDirectory); + languageServerFolderService = new DotNetLanguageServerFolderService(serviceContainer.object); + const lsFolderName = await languageServerFolderService.getLanguageServerFolderName(resource); + expect(lsFolderName).to.equal('currentLSDirectoryName'); + }); + + test('Returns expected Language server directory name otherwise', async () => { + shouldLookForNewLS = sinon.stub( + DotNetLanguageServerFolderService.prototype, + 'shouldLookForNewLanguageServer' + ); + shouldLookForNewLS.resolves(true); + const nugetPackage = { + package: 'packageName', + version: new semver.SemVer('1.3.2'), + uri: 'nugetUri' + }; + languageServerPackageService + .setup((l) => l.getLatestNugetPackageVersion(resource, '0.0.0')) + .returns(() => Promise.resolve(nugetPackage)); + getCurrentLanguageServerDirectory = sinon.stub( + DotNetLanguageServerFolderService.prototype, + 'getCurrentLanguageServerDirectory' + ); + getCurrentLanguageServerDirectory.resolves(currentLSDirectory); + languageServerFolderService = new DotNetLanguageServerFolderService(serviceContainer.object); + const lsFolderName = await languageServerFolderService.getLanguageServerFolderName(resource); + expect(lsFolderName).to.equal('languageServer.1.3.2'); + }); + }); + + suite('Method shouldLookForNewLanguageServer()', async () => { + let configurationService: TypeMoq.IMock<IConfigurationService>; + let lsPackageService: TypeMoq.IMock<ILanguageServerPackageService>; + let downloadChannelRule: TypeMoq.IMock<IDownloadChannelRule>; + const currentLSDirectory = { + path: 'path/to/currentLSDirectoryName', + version: new semver.SemVer('1.2.3') + }; + setup(() => { + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + lsPackageService = TypeMoq.Mock.ofType<ILanguageServerPackageService>(); + downloadChannelRule = TypeMoq.Mock.ofType<IDownloadChannelRule>(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer + .setup((s) => s.get<IConfigurationService>(IConfigurationService)) + .returns(() => configurationService.object); + serviceContainer + .setup((s) => s.get<IDownloadChannelRule>(IDownloadChannelRule, 'beta')) + .returns(() => downloadChannelRule.object); + serviceContainer + .setup((s) => s.get<ILanguageServerPackageService>(ILanguageServerPackageService)) + .returns(() => lsPackageService.object); + lsPackageService.setup((l) => l.getLanguageServerDownloadChannel()).returns(() => 'beta'); + languageServerFolderService = new DotNetLanguageServerFolderService(serviceContainer.object); + }); + + test('Returns false if current folder is provided and setting `python.downloadLanguageServer` is set to false', async () => { + const settings = { + downloadLanguageServer: false, + autoUpdateLanguageServer: true + }; + configurationService + .setup((c) => c.getSettings()) + // tslint:disable-next-line: no-any + .returns(() => settings as any); + const result = await languageServerFolderService.shouldLookForNewLanguageServer(currentLSDirectory); + expect(result).to.equal(false, 'Should be false'); + }); + + test('Returns false if current folder is provided and setting `python.autoUpdateLanguageServer` is set to false', async () => { + const settings = { + downloadLanguageServer: true, + autoUpdateLanguageServer: false + }; + configurationService + .setup((c) => c.getSettings()) + // tslint:disable-next-line: no-any + .returns(() => settings as any); + const result = await languageServerFolderService.shouldLookForNewLanguageServer(currentLSDirectory); + expect(result).to.equal(false, 'Should be false'); + }); + + test('Returns whatever the rule to infer LS returns otherwise', async () => { + const settings = { + downloadLanguageServer: true, + autoUpdateLanguageServer: false + }; + configurationService + .setup((c) => c.getSettings()) + // tslint:disable-next-line: no-any + .returns(() => settings as any); + downloadChannelRule + .setup((d) => d.shouldLookForNewLanguageServer(undefined)) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + const result = await languageServerFolderService.shouldLookForNewLanguageServer(); + expect(result).to.equal(true, 'Should be true'); + downloadChannelRule.verifyAll(); + }); + }); + + suite('Method getCurrentLanguageServerDirectory()', async () => { + let configurationService: TypeMoq.IMock<IConfigurationService>; + let fs: TypeMoq.IMock<IFileSystem>; + setup(() => { + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + fs = TypeMoq.Mock.ofType<IFileSystem>(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer + .setup((s) => s.get<IConfigurationService>(IConfigurationService)) + .returns(() => configurationService.object); + serviceContainer.setup((s) => s.get<IFileSystem>(IFileSystem)).returns(() => fs.object); + languageServerFolderService = new DotNetLanguageServerFolderService(serviceContainer.object); + }); + + test('Returns the expected LS directory if setting `python.downloadLanguageServer` is set to false', async () => { + const settings = { + downloadLanguageServer: false + }; + const expectedLSDirectory = { + path: 'languageServer', + version: new semver.SemVer('0.0.0') + }; + configurationService + .setup((c) => c.getSettings()) + // tslint:disable-next-line: no-any + .returns(() => settings as any); + const result = await languageServerFolderService.getCurrentLanguageServerDirectory(); + assert.deepEqual(result, expectedLSDirectory); + }); + + test('Returns `undefined` if no LS directory exists', async () => { + const settings = { + downloadLanguageServer: true + }; + const directories = ['path/to/directory1', 'path/to/directory2']; + configurationService + .setup((c) => c.getSettings()) + // tslint:disable-next-line: no-any + .returns(() => settings as any); + fs.setup((f) => f.getSubDirectories(TypeMoq.It.isAny())).returns(() => Promise.resolve(directories)); + const result = await languageServerFolderService.getCurrentLanguageServerDirectory(); + assert.deepEqual(result, undefined); + }); + + test('Returns the LS directory with highest version if multiple LS directories exists', async () => { + const settings = { + downloadLanguageServer: true + }; + const directories = [ + 'path/to/languageServer', + 'path/to/languageServer.0.9.3', + 'path/to/languageServer.1.0.7', + 'path/to/languageServer.1.2.3', + 'path/to/languageServer.1.2.3.5' + ]; + const expectedLSDirectory = { + path: 'path/to/languageServer.1.2.3', + version: semver.parse('1.2.3', true)! + }; + configurationService + .setup((c) => c.getSettings()) + // tslint:disable-next-line: no-any + .returns(() => settings as any); + fs.setup((f) => f.getSubDirectories(TypeMoq.It.isAny())).returns(() => Promise.resolve(directories)); + const result = await languageServerFolderService.getCurrentLanguageServerDirectory(); + assert.deepEqual(result, expectedLSDirectory); + }); + }); + + suite('Method skipDownload()', () => { + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + languageServerFolderService = new DotNetLanguageServerFolderService(serviceContainer.object); + }); + + test('skipDownload is false', async () => { + const skipDownload = await languageServerFolderService.skipDownload(); + expect(skipDownload).to.be.equal(false, 'skipDownload should be false'); + }); + }); +}); diff --git a/src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts b/src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts new file mode 100644 index 000000000000..e32bc7acbf82 --- /dev/null +++ b/src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { LanguageServerDownloadChannel } from '../../../client/activation/common/packageRepository'; +import { + BetaDotNetLanguageServerPackageRepository, + DailyDotNetLanguageServerPackageRepository, + StableDotNetLanguageServerPackageRepository +} from '../../../client/activation/languageServer/languageServerPackageRepository'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Language Server Download Channels', () => { + let serviceContainer: typeMoq.IMock<IServiceContainer>; + setup(() => { + serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + }); + + function getPackageInfo(channel: LanguageServerDownloadChannel) { + let classToCreate = StableDotNetLanguageServerPackageRepository; + switch (channel) { + case LanguageServerDownloadChannel.stable: { + classToCreate = StableDotNetLanguageServerPackageRepository; + break; + } + case LanguageServerDownloadChannel.beta: { + classToCreate = BetaDotNetLanguageServerPackageRepository; + break; + } + case LanguageServerDownloadChannel.daily: { + classToCreate = DailyDotNetLanguageServerPackageRepository; + break; + } + default: { + throw new Error('Unknown download channel'); + } + } + const instance = new (class extends classToCreate { + constructor() { + super(serviceContainer.object); + } + public get storageAccount() { + return this.azureCDNBlobStorageAccount; + } + public get storageContainer() { + return this.azureBlobStorageContainer; + } + })(); + + return [instance.storageAccount, instance.storageContainer]; + } + test('Stable', () => { + expect(getPackageInfo(LanguageServerDownloadChannel.stable)).to.be.deep.equal([ + 'https://pvsc.azureedge.net', + 'python-language-server-stable' + ]); + }); + test('Beta', () => { + expect(getPackageInfo(LanguageServerDownloadChannel.beta)).to.be.deep.equal([ + 'https://pvsc.azureedge.net', + 'python-language-server-beta' + ]); + }); + test('Daily', () => { + expect(getPackageInfo(LanguageServerDownloadChannel.daily)).to.be.deep.equal([ + 'https://pvsc.azureedge.net', + 'python-language-server-daily' + ]); + }); +}); diff --git a/src/test/activation/languageServer/languageServerPackageService.test.ts b/src/test/activation/languageServer/languageServerPackageService.test.ts new file mode 100644 index 000000000000..1101876781e2 --- /dev/null +++ b/src/test/activation/languageServer/languageServerPackageService.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-this max-func-body-length + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { WorkspaceConfiguration } from 'vscode'; +import { DotNetLanguageServerMinVersionKey } from '../../../client/activation/languageServer/languageServerFolderService'; +import { DotNetLanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService'; +import { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types'; +import { AzureBlobStoreNugetRepository } from '../../../client/common/nuget/azureBlobStoreNugetRepository'; +import { NugetService } from '../../../client/common/nuget/nugetService'; +import { INugetRepository, INugetService } from '../../../client/common/nuget/types'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +const azureBlobStorageAccount = 'https://pvsc.blob.core.windows.net'; +const azureCDNBlobStorageAccount = 'https://pvsc.azureedge.net'; + +suite('Language Server Package Service', () => { + let serviceContainer: typeMoq.IMock<IServiceContainer>; + setup(() => { + serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + }); + test('Ensure new Major versions of language server is accounted for (azure blob)', async () => { + const nugetService = new NugetService(); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(INugetService))).returns(() => nugetService); + const platformService = new PlatformService(); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IPlatformService))).returns(() => platformService); + const workspace = typeMoq.Mock.ofType<IWorkspaceService>(); + const cfg = typeMoq.Mock.ofType<WorkspaceConfiguration>(); + cfg.setup((c) => c.get('proxyStrictSSL', true)).returns(() => true); + workspace.setup((w) => w.getConfiguration('http', undefined)).returns(() => cfg.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IWorkspaceService))).returns(() => workspace.object); + const defaultStorageChannel = 'python-language-server-daily'; + const nugetRepo = new AzureBlobStoreNugetRepository( + serviceContainer.object, + azureBlobStorageAccount, + defaultStorageChannel, + azureCDNBlobStorageAccount + ); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(INugetRepository))).returns(() => nugetRepo); + const appEnv = typeMoq.Mock.ofType<IApplicationEnvironment>(); + const packageJson = { [DotNetLanguageServerMinVersionKey]: '0.0.1' }; + appEnv.setup((e) => e.packageJson).returns(() => packageJson); + const platform = typeMoq.Mock.ofType<IPlatformService>(); + const lsPackageService = new DotNetLanguageServerPackageService( + serviceContainer.object, + appEnv.object, + platform.object + ); + const packageName = lsPackageService.getNugetPackageName(); + const packages = await nugetRepo.getPackages(packageName, undefined); + + const latestReleases = packages + .filter((item) => nugetService.isReleaseVersion(item.version)) + .sort((a, b) => a.version.compare(b.version)); + const latestRelease = latestReleases[latestReleases.length - 1]; + + expect(packages).to.be.length.greaterThan(0, 'No packages returned.'); + expect(latestReleases).to.be.length.greaterThan(0, 'No release packages returned.'); + expect(latestRelease.version.major).to.be.equal( + lsPackageService.maxMajorVersion, + 'New Major version of Language server has been released, we need to update it at our end.' + ); + }); +}); diff --git a/src/test/activation/languageServer/languageServerPackageService.unit.test.ts b/src/test/activation/languageServer/languageServerPackageService.unit.test.ts new file mode 100644 index 000000000000..358b48f63c41 --- /dev/null +++ b/src/test/activation/languageServer/languageServerPackageService.unit.test.ts @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-this max-func-body-length + +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import * as typeMoq from 'typemoq'; +import { + azureCDNBlobStorageAccount, + LanguageServerDownloadChannel +} from '../../../client/activation/common/packageRepository'; +import { DotNetLanguageServerMinVersionKey } from '../../../client/activation/languageServer/languageServerFolderService'; +import { DotNetLanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService'; +import { PlatformName } from '../../../client/activation/types'; +import { IApplicationEnvironment } from '../../../client/common/application/types'; +import { NugetService } from '../../../client/common/nuget/nugetService'; +import { INugetRepository, INugetService, NugetPackage } from '../../../client/common/nuget/types'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { OSType } from '../../../client/common/utils/platform'; +import { IServiceContainer } from '../../../client/ioc/types'; + +const downloadBaseFileName = 'Python-Language-Server'; + +suite('Language Server - Package Service', () => { + let serviceContainer: typeMoq.IMock<IServiceContainer>; + let platform: typeMoq.IMock<IPlatformService>; + let lsPackageService: DotNetLanguageServerPackageService; + let appVersion: typeMoq.IMock<IApplicationEnvironment>; + setup(() => { + serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + platform = typeMoq.Mock.ofType<IPlatformService>(); + appVersion = typeMoq.Mock.ofType<IApplicationEnvironment>(); + lsPackageService = new DotNetLanguageServerPackageService( + serviceContainer.object, + appVersion.object, + platform.object + ); + lsPackageService.getLanguageServerDownloadChannel = () => 'stable'; + }); + function setMinVersionOfLs(version: string) { + const packageJson = { [DotNetLanguageServerMinVersionKey]: version }; + appVersion.setup((e) => e.packageJson).returns(() => packageJson); + } + [true, false].forEach((is64Bit) => { + const bitness = is64Bit ? '64bit' : '32bit'; + test(`Get Package name for Windows (${bitness})`, async () => { + platform.setup((p) => p.osType).returns(() => OSType.Windows); + platform.setup((p) => p.is64bit).returns(() => is64Bit); + const expectedName = is64Bit + ? `${downloadBaseFileName}-${PlatformName.Windows64Bit}` + : `${downloadBaseFileName}-${PlatformName.Windows32Bit}`; + + const name = lsPackageService.getNugetPackageName(); + + platform.verifyAll(); + expect(name).to.be.equal(expectedName); + }); + test(`Get Package name for Mac (${bitness})`, async () => { + platform.setup((p) => p.osType).returns(() => OSType.OSX); + const expectedName = `${downloadBaseFileName}-${PlatformName.Mac64Bit}`; + + const name = lsPackageService.getNugetPackageName(); + + platform.verifyAll(); + expect(name).to.be.equal(expectedName); + }); + test(`Get Package name for Linux (${bitness})`, async () => { + platform.setup((p) => p.osType).returns(() => OSType.Linux); + const expectedName = `${downloadBaseFileName}-${PlatformName.Linux64Bit}`; + + const name = lsPackageService.getNugetPackageName(); + + platform.verifyAll(); + expect(name).to.be.equal(expectedName); + }); + }); + test('Get latest nuget package version', async () => { + const packageName = 'packageName'; + lsPackageService.getNugetPackageName = () => packageName; + lsPackageService.maxMajorVersion = 3; + setMinVersionOfLs('0.0.1'); + const packages: NugetPackage[] = [ + { package: '', uri: '', version: new SemVer('1.1.1') }, + { package: '', uri: '', version: new SemVer('3.4.1') }, + { package: '', uri: '', version: new SemVer('3.1.1') }, + { package: '', uri: '', version: new SemVer('2.1.1') } + ]; + const expectedPackage = packages[1]; + const repo = typeMoq.Mock.ofType<INugetRepository>(); + const nuget = typeMoq.Mock.ofType<INugetService>(); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(INugetRepository), typeMoq.It.isAny())) + .returns(() => repo.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(INugetService))).returns(() => nuget.object); + + repo.setup((n) => n.getPackages(typeMoq.It.isValue(packageName), typeMoq.It.isAny())) + .returns(() => Promise.resolve(packages)) + .verifiable(typeMoq.Times.once()); + nuget + .setup((n) => n.isReleaseVersion(typeMoq.It.isAny())) + .returns(() => true) + .verifiable(typeMoq.Times.atLeastOnce()); + + const info = await lsPackageService.getLatestNugetPackageVersion(undefined); + + repo.verifyAll(); + nuget.verifyAll(); + expect(info).to.deep.equal(expectedPackage); + }); + test('Get latest nuget package version (excluding non-release)', async () => { + setMinVersionOfLs('0.0.1'); + const packageName = 'packageName'; + lsPackageService.getNugetPackageName = () => packageName; + lsPackageService.maxMajorVersion = 1; + const packages: NugetPackage[] = [ + { package: '', uri: '', version: new SemVer('1.1.1') }, + { package: '', uri: '', version: new SemVer('1.3.1-alpha') }, + { package: '', uri: '', version: new SemVer('1.4.1-preview') }, + { package: '', uri: '', version: new SemVer('1.2.1-internal') } + ]; + const expectedPackage = packages[0]; + const repo = typeMoq.Mock.ofType<INugetRepository>(); + const nuget = new NugetService(); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(INugetRepository), typeMoq.It.isAny())) + .returns(() => repo.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(INugetService))).returns(() => nuget); + + repo.setup((n) => n.getPackages(typeMoq.It.isValue(packageName), typeMoq.It.isAny())) + .returns(() => Promise.resolve(packages)) + .verifiable(typeMoq.Times.once()); + + const info = await lsPackageService.getLatestNugetPackageVersion(undefined); + + repo.verifyAll(); + expect(info).to.deep.equal(expectedPackage); + }); + test('Ensure minimum version of package is used', async () => { + const minimumVersion = '0.1.50'; + setMinVersionOfLs(minimumVersion); + const packageName = 'packageName'; + lsPackageService.getNugetPackageName = () => packageName; + lsPackageService.maxMajorVersion = 0; + const packages: NugetPackage[] = [ + { package: '', uri: '', version: new SemVer('0.1.48') }, + { package: '', uri: '', version: new SemVer('0.1.49') } + ]; + const repo = typeMoq.Mock.ofType<INugetRepository>(); + const nuget = new NugetService(); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(INugetRepository), typeMoq.It.isAny())) + .returns(() => repo.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(INugetService))).returns(() => nuget); + + repo.setup((n) => n.getPackages(typeMoq.It.isValue(packageName), typeMoq.It.isAny())) + .returns(() => Promise.resolve(packages)) + .verifiable(typeMoq.Times.once()); + + const info = await lsPackageService.getLatestNugetPackageVersion(undefined, minimumVersion); + + repo.verifyAll(); + const expectedPackage: NugetPackage = { + version: new SemVer(minimumVersion), + package: LanguageServerDownloadChannel.stable, + uri: `${azureCDNBlobStorageAccount}/${LanguageServerDownloadChannel.stable}/${packageName}.${minimumVersion}.nupkg` + }; + expect(info).to.deep.equal(expectedPackage); + }); +}); +suite('Language Server Package Service - getLanguageServerDownloadChannel()', () => { + let serviceContainer: typeMoq.IMock<IServiceContainer>; + let platform: typeMoq.IMock<IPlatformService>; + let lsPackageService: DotNetLanguageServerPackageService; + let appVersion: typeMoq.IMock<IApplicationEnvironment>; + let configService: typeMoq.IMock<IConfigurationService>; + setup(() => { + serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + platform = typeMoq.Mock.ofType<IPlatformService>(); + appVersion = typeMoq.Mock.ofType<IApplicationEnvironment>(); + configService = typeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer.setup((s) => s.get(IConfigurationService)).returns(() => configService.object); + lsPackageService = new DotNetLanguageServerPackageService( + serviceContainer.object, + appVersion.object, + platform.object + ); + lsPackageService.isAlphaVersionOfExtension = () => true; + }); + test("If 'python.analysis.downloadChannel' setting is specified, return the value of the setting", async () => { + const settings = { + analysis: { + downloadChannel: 'someValue' + } + }; + configService.setup((c) => c.getSettings()).returns(() => settings as any); + + lsPackageService.isAlphaVersionOfExtension = () => { + throw new Error('Should not be here'); + }; + const downloadChannel = lsPackageService.getLanguageServerDownloadChannel(); + + expect(downloadChannel).to.be.equal('someValue'); + }); + + test("If 'python.analysis.downloadChannel' setting is not specified and insiders channel is 'weekly', return 'beta'", async () => { + const settings = { + analysis: {}, + insidersChannel: 'weekly' + }; + configService.setup((c) => c.getSettings()).returns(() => settings as any); + + lsPackageService.isAlphaVersionOfExtension = () => { + throw new Error('Should not be here'); + }; + const downloadChannel = lsPackageService.getLanguageServerDownloadChannel(); + + expect(downloadChannel).to.be.equal('beta'); + }); + + test("If 'python.analysis.downloadChannel' setting is not specified and insiders channel is 'daily', return 'beta'", async () => { + const settings = { + analysis: {}, + insidersChannel: 'daily' + }; + configService.setup((c) => c.getSettings()).returns(() => settings as any); + + lsPackageService.isAlphaVersionOfExtension = () => { + throw new Error('Should not be here'); + }; + const downloadChannel = lsPackageService.getLanguageServerDownloadChannel(); + + expect(downloadChannel).to.be.equal('beta'); + }); + + test("If 'python.analysis.downloadChannel' setting is not specified, user is not using insiders, and extension has Alpha version, return 'beta'", async () => { + const settings = { + analysis: {}, + insidersChannel: 'off' + }; + configService.setup((c) => c.getSettings()).returns(() => settings as any); + + lsPackageService.isAlphaVersionOfExtension = () => true; + const downloadChannel = lsPackageService.getLanguageServerDownloadChannel(); + + expect(downloadChannel).to.be.equal('beta'); + }); + + test("If 'python.analysis.downloadChannel' setting is not specified, user is not using insiders, and extension does not have Alpha version, return 'stable'", async () => { + const settings = { + analysis: {}, + insidersChannel: 'off' + }; + configService.setup((c) => c.getSettings()).returns(() => settings as any); + + lsPackageService.isAlphaVersionOfExtension = () => false; + const downloadChannel = lsPackageService.getLanguageServerDownloadChannel(); + + expect(downloadChannel).to.be.equal('stable'); + }); +}); diff --git a/src/test/activation/languageServer/manager.unit.test.ts b/src/test/activation/languageServer/manager.unit.test.ts new file mode 100644 index 000000000000..09a24a0df6d0 --- /dev/null +++ b/src/test/activation/languageServer/manager.unit.test.ts @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Disposable, Uri } from 'vscode'; +import { LanguageClientOptions } from 'vscode-languageclient/node'; +import { Commands } from '../../../client/activation/commands'; +import { DotNetLanguageServerAnalysisOptions } from '../../../client/activation/languageServer/analysisOptions'; +import { LanguageServerExtension } from '../../../client/activation/languageServer/languageServerExtension'; +import { DotNetLanguageServerFolderService } from '../../../client/activation/languageServer/languageServerFolderService'; +import { DotNetLanguageServerProxy } from '../../../client/activation/languageServer/languageServerProxy'; +import { DotNetLanguageServerManager } from '../../../client/activation/languageServer/manager'; +import { + ILanguageServerAnalysisOptions, + ILanguageServerExtension, + ILanguageServerFolderService, + ILanguageServerProxy +} from '../../../client/activation/types'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { ICommandManager } from '../../../client/common/application/types'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { ExperimentsManager } from '../../../client/common/experiments/manager'; +import { IConfigurationService, IExperimentsManager } from '../../../client/common/types'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { sleep } from '../../core'; + +use(chaiAsPromised); + +// tslint:disable:max-func-body-length no-any chai-vague-errors no-unused-expression + +suite('Language Server - Manager', () => { + let manager: DotNetLanguageServerManager; + let serviceContainer: IServiceContainer; + let analysisOptions: ILanguageServerAnalysisOptions; + let languageServer: ILanguageServerProxy; + let lsExtension: ILanguageServerExtension; + let onChangeAnalysisHandler: Function; + let folderService: ILanguageServerFolderService; + let experimentsManager: IExperimentsManager; + let configService: IConfigurationService; + let commandManager: ICommandManager; + const languageClientOptions = ({ x: 1 } as any) as LanguageClientOptions; + setup(() => { + serviceContainer = mock(ServiceContainer); + analysisOptions = mock(DotNetLanguageServerAnalysisOptions); + languageServer = mock(DotNetLanguageServerProxy); + lsExtension = mock(LanguageServerExtension); + folderService = mock(DotNetLanguageServerFolderService); + experimentsManager = mock(ExperimentsManager); + configService = mock(ConfigurationService); + + commandManager = mock(CommandManager); + const disposable = mock(Disposable); + when(commandManager.registerCommand(Commands.RestartLS, anything())).thenReturn(instance(disposable)); + + manager = new DotNetLanguageServerManager( + instance(serviceContainer), + instance(analysisOptions), + instance(lsExtension), + instance(folderService), + instance(experimentsManager), + instance(configService), + instance(commandManager) + ); + }); + + [undefined, Uri.file(__filename)].forEach((resource) => { + async function startLanguageServer() { + let invoked = false; + const lsExtensionChangeFn = (_handler: Function) => { + invoked = true; + }; + when(lsExtension.invoked).thenReturn(lsExtensionChangeFn as any); + + let analysisHandlerRegistered = false; + const analysisChangeFn = (handler: Function) => { + analysisHandlerRegistered = true; + onChangeAnalysisHandler = handler; + }; + when(analysisOptions.initialize(resource, undefined)).thenResolve(); + when(analysisOptions.getAnalysisOptions()).thenResolve(languageClientOptions); + when(analysisOptions.onDidChange).thenReturn(analysisChangeFn as any); + when(serviceContainer.get<ILanguageServerProxy>(ILanguageServerProxy)).thenReturn(instance(languageServer)); + when(languageServer.start(resource, undefined, languageClientOptions)).thenResolve(); + + await manager.start(resource, undefined); + + verify(analysisOptions.initialize(resource, undefined)).once(); + verify(analysisOptions.getAnalysisOptions()).once(); + verify(serviceContainer.get<ILanguageServerProxy>(ILanguageServerProxy)).once(); + verify(languageServer.start(resource, undefined, languageClientOptions)).once(); + expect(invoked).to.be.true; + expect(analysisHandlerRegistered).to.be.true; + verify(languageServer.dispose()).never(); + } + test('Start must register handlers and initialize analysis options', async () => { + await startLanguageServer(); + + manager.dispose(); + + verify(languageServer.dispose()).once(); + }); + test('Attempting to start LS will throw an exception', async () => { + await startLanguageServer(); + + await expect(manager.start(resource, undefined)).to.eventually.be.rejectedWith( + 'Language server already started' + ); + }); + test('Changes in analysis options must restart LS', async () => { + await startLanguageServer(); + + await onChangeAnalysisHandler.call(manager); + await sleep(1); + + verify(languageServer.dispose()).once(); + + verify(analysisOptions.getAnalysisOptions()).twice(); + verify(serviceContainer.get<ILanguageServerProxy>(ILanguageServerProxy)).twice(); + verify(languageServer.start(resource, undefined, languageClientOptions)).twice(); + }); + test('Changes in analysis options must throttled when restarting LS', async () => { + await startLanguageServer(); + + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await Promise.all([ + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager) + ]); + await sleep(1); + + verify(languageServer.dispose()).once(); + + verify(analysisOptions.getAnalysisOptions()).twice(); + verify(serviceContainer.get<ILanguageServerProxy>(ILanguageServerProxy)).twice(); + verify(languageServer.start(resource, undefined, languageClientOptions)).twice(); + }); + test('Multiple changes in analysis options must restart LS twice', async () => { + await startLanguageServer(); + + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await Promise.all([ + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager) + ]); + await sleep(1); + + verify(languageServer.dispose()).once(); + + verify(analysisOptions.getAnalysisOptions()).twice(); + verify(serviceContainer.get<ILanguageServerProxy>(ILanguageServerProxy)).twice(); + verify(languageServer.start(resource, undefined, languageClientOptions)).twice(); + + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await onChangeAnalysisHandler.call(manager); + await Promise.all([ + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager), + onChangeAnalysisHandler.call(manager) + ]); + await sleep(1); + + verify(languageServer.dispose()).twice(); + + verify(analysisOptions.getAnalysisOptions()).thrice(); + verify(serviceContainer.get<ILanguageServerProxy>(ILanguageServerProxy)).thrice(); + verify(languageServer.start(resource, undefined, languageClientOptions)).thrice(); + }); + test('Must load extension when command was been sent before starting LS', async () => { + const args = { x: 1 }; + when(lsExtension.loadExtensionArgs).thenReturn(args as any); + + await startLanguageServer(); + + verify(languageServer.loadExtension(args)).once(); + }); + }); +}); diff --git a/src/test/activation/languageServer/outputChannel.unit.test.ts b/src/test/activation/languageServer/outputChannel.unit.test.ts new file mode 100644 index 000000000000..c120cfdc45ca --- /dev/null +++ b/src/test/activation/languageServer/outputChannel.unit.test.ts @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { LanguageServerOutputChannel } from '../../../client/activation/languageServer/outputChannel'; +import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; +import { IOutputChannel } from '../../../client/common/types'; +import { sleep } from '../../../client/common/utils/async'; +import { OutputChannelNames } from '../../../client/common/utils/localize'; + +// tslint:disable-next-line:max-func-body-length +suite('Language Server Output Channel', () => { + let appShell: TypeMoq.IMock<IApplicationShell>; + let languageServerOutputChannel: LanguageServerOutputChannel; + let commandManager: TypeMoq.IMock<ICommandManager>; + let output: TypeMoq.IMock<IOutputChannel>; + setup(() => { + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + output = TypeMoq.Mock.ofType<IOutputChannel>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + languageServerOutputChannel = new LanguageServerOutputChannel(appShell.object, commandManager.object); + }); + + test('Create output channel if one does not exist before and return it', async () => { + appShell + .setup((a) => a.createOutputChannel(OutputChannelNames.languageServer())) + .returns(() => output.object) + .verifiable(TypeMoq.Times.once()); + const channel = languageServerOutputChannel.channel; + appShell.verifyAll(); + expect(channel).to.not.equal(undefined, 'Channel should not be undefined'); + }); + + test('Do not create output channel if one already exists', async () => { + languageServerOutputChannel.output = output.object; + appShell + .setup((a) => a.createOutputChannel(TypeMoq.It.isAny())) + .returns(() => output.object) + .verifiable(TypeMoq.Times.never()); + const channel = languageServerOutputChannel.channel; + appShell.verifyAll(); + expect(channel).to.not.equal(undefined, 'Channel should not be undefined'); + }); + test('Register Command to display output panel', async () => { + appShell + .setup((a) => a.createOutputChannel(TypeMoq.It.isAny())) + .returns(() => output.object) + .verifiable(TypeMoq.Times.once()); + commandManager + .setup((c) => + c.executeCommand( + TypeMoq.It.isValue('setContext'), + TypeMoq.It.isValue('python.hasLanguageServerOutputChannel'), + TypeMoq.It.isValue(true) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + commandManager + .setup((c) => c.registerCommand(TypeMoq.It.isValue('python.viewLanguageServerOutput'), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.once()); + + // Doesn't matter how many times we access channel propery. + let channel = languageServerOutputChannel.channel; + channel = languageServerOutputChannel.channel; + channel = languageServerOutputChannel.channel; + + await sleep(1); + + appShell.verifyAll(); + commandManager.verifyAll(); + expect(channel).to.not.equal(undefined, 'Channel should not be undefined'); + }); + test('Display panel when invoking command python.viewLanguageServerOutput', async () => { + let cmdCallback: Function | undefined; + appShell + .setup((a) => a.createOutputChannel(TypeMoq.It.isAny())) + .returns(() => output.object) + .verifiable(TypeMoq.Times.once()); + commandManager + .setup((c) => + c.executeCommand( + TypeMoq.It.isValue('setContext'), + TypeMoq.It.isValue('python.hasLanguageServerOutputChannel'), + TypeMoq.It.isValue(true) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + commandManager + .setup((c) => c.registerCommand(TypeMoq.It.isValue('python.viewLanguageServerOutput'), TypeMoq.It.isAny())) + .callback((_: string, callback: Function) => (cmdCallback = callback)) + .verifiable(TypeMoq.Times.once()); + output.setup((o) => o.show(true)).verifiable(TypeMoq.Times.never()); + // Doesn't matter how many times we access channel propery. + let channel = languageServerOutputChannel.channel; + channel = languageServerOutputChannel.channel; + channel = languageServerOutputChannel.channel; + + await sleep(1); + + appShell.verifyAll(); + commandManager.verifyAll(); + output.verifyAll(); + expect(channel).to.not.equal(undefined, 'Channel should not be undefined'); + expect(cmdCallback).to.not.equal(undefined, 'Command handler should not be undefined'); + + // Confirm panel is displayed when command handler is invoked. + output.reset(); + output.setup((o) => o.show(true)).verifiable(TypeMoq.Times.once()); + + // Invoke callback. + cmdCallback!(); + + output.verifyAll(); + }); +}); diff --git a/src/test/activation/languageServer/platformData.unit.test.ts b/src/test/activation/languageServer/platformData.unit.test.ts new file mode 100644 index 000000000000..d424aa63ceac --- /dev/null +++ b/src/test/activation/languageServer/platformData.unit.test.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unused-variable +import * as assert from 'assert'; +import * as TypeMoq from 'typemoq'; +import { PlatformData, PlatformLSExecutables } from '../../../client/activation/languageServer/platformData'; +import { IPlatformService } from '../../../client/common/platform/types'; + +const testDataWinMac = [ + { isWindows: true, is64Bit: true, expectedName: 'win-x64' }, + { isWindows: true, is64Bit: false, expectedName: 'win-x86' }, + { isWindows: false, is64Bit: true, expectedName: 'osx-x64' } +]; + +const testDataLinux = [ + { name: 'centos', expectedName: 'linux-x64' }, + { name: 'debian', expectedName: 'linux-x64' }, + { name: 'fedora', expectedName: 'linux-x64' }, + { name: 'ol', expectedName: 'linux-x64' }, + { name: 'opensuse', expectedName: 'linux-x64' }, + { name: 'rhel', expectedName: 'linux-x64' }, + { name: 'ubuntu', expectedName: 'linux-x64' } +]; + +const testDataModuleName = [ + { isWindows: true, isMac: false, isLinux: false, expectedName: PlatformLSExecutables.Windows }, + { isWindows: false, isMac: true, isLinux: false, expectedName: PlatformLSExecutables.MacOS }, + { isWindows: false, isMac: false, isLinux: true, expectedName: PlatformLSExecutables.Linux } +]; + +// tslint:disable-next-line:max-func-body-length +suite('Language Server Activation - platform data', () => { + test('Name and hash (Windows/Mac)', async () => { + for (const t of testDataWinMac) { + const platformService = TypeMoq.Mock.ofType<IPlatformService>(); + platformService.setup((x) => x.isWindows).returns(() => t.isWindows); + platformService.setup((x) => x.isMac).returns(() => !t.isWindows); + platformService.setup((x) => x.is64bit).returns(() => t.is64Bit); + + const pd = new PlatformData(platformService.object); + + const actual = pd.platformName; + assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); + } + }); + test('Name and hash (Linux)', async () => { + for (const t of testDataLinux) { + const platformService = TypeMoq.Mock.ofType<IPlatformService>(); + platformService.setup((x) => x.isWindows).returns(() => false); + platformService.setup((x) => x.isMac).returns(() => false); + platformService.setup((x) => x.isLinux).returns(() => true); + platformService.setup((x) => x.is64bit).returns(() => true); + + const pd = new PlatformData(platformService.object); + + const actual = pd.platformName; + assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); + } + }); + test('Module name', async () => { + for (const t of testDataModuleName) { + const platformService = TypeMoq.Mock.ofType<IPlatformService>(); + platformService.setup((x) => x.isWindows).returns(() => t.isWindows); + platformService.setup((x) => x.isLinux).returns(() => t.isLinux); + platformService.setup((x) => x.isMac).returns(() => t.isMac); + + const pd = new PlatformData(platformService.object); + + const actual = pd.engineExecutableName; + assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); + } + }); +}); diff --git a/src/test/activation/node/activator.unit.test.ts b/src/test/activation/node/activator.unit.test.ts new file mode 100644 index 000000000000..3b87588774eb --- /dev/null +++ b/src/test/activation/node/activator.unit.test.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter, Extension, Uri } from 'vscode'; +import { NodeLanguageServerActivator } from '../../../client/activation/node/activator'; +import { NodeLanguageServerManager } from '../../../client/activation/node/manager'; +import { ILanguageServerManager } from '../../../client/activation/types'; +import { + IApplicationEnvironment, + IApplicationShell, + IWorkspaceService +} from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IConfigurationService, IExtensions, IPythonSettings } from '../../../client/common/types'; +import { Common, Pylance } from '../../../client/common/utils/localize'; + +// tslint:disable:max-func-body-length + +suite('Pylance Language Server - Activator', () => { + let activator: NodeLanguageServerActivator; + let workspaceService: IWorkspaceService; + let manager: ILanguageServerManager; + let fs: IFileSystem; + let configuration: IConfigurationService; + let settings: IPythonSettings; + let extensions: IExtensions; + let appShell: IApplicationShell; + let appEnv: IApplicationEnvironment; + let extensionsChangedEvent: EventEmitter<void>; + + // tslint:disable-next-line: no-any + let pylanceExtension: Extension<any>; + setup(() => { + manager = mock(NodeLanguageServerManager); + workspaceService = mock(WorkspaceService); + fs = mock(FileSystem); + configuration = mock(ConfigurationService); + settings = mock(PythonSettings); + extensions = mock<IExtensions>(); + appShell = mock<IApplicationShell>(); + appEnv = mock<IApplicationEnvironment>(); + when(appEnv.uriScheme).thenReturn('scheme'); + + // tslint:disable-next-line: no-any + pylanceExtension = mock<Extension<any>>(); + when(configuration.getSettings(anything())).thenReturn(instance(settings)); + when(appEnv.uriScheme).thenReturn('scheme'); + + extensionsChangedEvent = new EventEmitter<void>(); + when(extensions.onDidChange).thenReturn(extensionsChangedEvent.event); + + activator = new NodeLanguageServerActivator( + instance(manager), + instance(workspaceService), + instance(fs), + instance(configuration), + instance(extensions), + instance(appShell), + instance(appEnv) + ); + }); + teardown(() => { + extensionsChangedEvent.dispose(); + }); + + test('Manager must be started without any workspace', async () => { + when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(pylanceExtension)); + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(manager.start(undefined, undefined)).thenResolve(); + + await activator.start(undefined); + verify(manager.start(undefined, undefined)).once(); + verify(workspaceService.hasWorkspaceFolders).once(); + }); + + test('Manager must be disposed', async () => { + activator.dispose(); + verify(manager.dispose()).once(); + }); + + test('Activator should check if Pylance is installed', async () => { + when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(pylanceExtension)); + await activator.start(undefined); + verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).once(); + }); + + test('Activator should not check if Pylance is installed in development mode', async () => { + when(settings.downloadLanguageServer).thenReturn(false); + await activator.start(undefined); + verify(extensions.getExtension(PYLANCE_EXTENSION_ID)).never(); + }); + + test('When Pylance is not installed activator should show install prompt ', async () => { + when( + appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(Common.bannerLabelNo())); + + try { + await activator.start(undefined); + // tslint:disable-next-line: no-empty + } catch {} + verify( + appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).once(); + verify(appShell.openUrl(`scheme:extension/${PYLANCE_EXTENSION_ID}`)).never(); + }); + + test('When Pylance is not installed activator should open Pylance install page if users clicks Yes', async () => { + when( + appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(Common.bannerLabelYes())); + + try { + await activator.start(undefined); + // tslint:disable-next-line: no-empty + } catch {} + verify(appShell.openUrl(`scheme:extension/${PYLANCE_EXTENSION_ID}`)).once(); + }); + + test('Activator should throw if Pylance is not installed', async () => { + expect(activator.start(undefined)) + .to.eventually.be.rejectedWith(Pylance.pylanceNotInstalledMessage()) + .and.be.an.instanceOf(Error); + }); + + test('Manager must be started with resource for first available workspace', async () => { + const uri = Uri.file(__filename); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + when(workspaceService.workspaceFolders).thenReturn([{ index: 0, name: '', uri }]); + when(manager.start(uri, undefined)).thenResolve(); + when(settings.downloadLanguageServer).thenReturn(false); + + await activator.start(undefined); + + verify(manager.start(uri, undefined)).once(); + verify(workspaceService.hasWorkspaceFolders).once(); + verify(workspaceService.workspaceFolders).once(); + }); +}); diff --git a/src/test/activation/node/languageServerChangeHandler.unit.test.ts b/src/test/activation/node/languageServerChangeHandler.unit.test.ts new file mode 100644 index 000000000000..94b7ef180311 --- /dev/null +++ b/src/test/activation/node/languageServerChangeHandler.unit.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anyString, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter, Extension } from 'vscode'; +import { LanguageServerChangeHandler } from '../../../client/activation/common/languageServerChangeHandler'; +import { LanguageServerType } from '../../../client/activation/types'; +import { IApplicationEnvironment, IApplicationShell, ICommandManager } from '../../../client/common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants'; +import { IExtensions } from '../../../client/common/types'; +import { Common, LanguageService, Pylance } from '../../../client/common/utils/localize'; + +suite('Language Server - Change Handler', () => { + let extensions: IExtensions; + let appShell: IApplicationShell; + let appEnv: IApplicationEnvironment; + let commands: ICommandManager; + let extensionsChangedEvent: EventEmitter<void>; + let handler: LanguageServerChangeHandler; + + // tslint:disable-next-line: no-any + let pylanceExtension: Extension<any>; + setup(() => { + extensions = mock<IExtensions>(); + appShell = mock<IApplicationShell>(); + appEnv = mock<IApplicationEnvironment>(); + commands = mock<ICommandManager>(); + + // tslint:disable-next-line: no-any + pylanceExtension = mock<Extension<any>>(); + when(appEnv.uriScheme).thenReturn('scheme'); + + extensionsChangedEvent = new EventEmitter<void>(); + when(extensions.onDidChange).thenReturn(extensionsChangedEvent.event); + }); + teardown(() => { + extensionsChangedEvent.dispose(); + handler?.dispose(); + }); + + [undefined, LanguageServerType.None, LanguageServerType.Microsoft, LanguageServerType.Node].forEach(async (t) => { + test(`Handler should do nothing if language server is ${t} and did not change`, async () => { + handler = makeHandler(t); + await handler.handleLanguageServerChange(t); + + verify(extensions.getExtension(anyString())).once(); + verify(appShell.openUrl(anyString())).never(); + verify(appShell.showInformationMessage(anyString(), anyString())).never(); + verify(appShell.showWarningMessage(anyString(), anyString())).never(); + verify(commands.executeCommand(anyString())).never(); + }); + }); + + [LanguageServerType.None, LanguageServerType.Microsoft, LanguageServerType.Node].forEach(async (t) => { + test(`Handler should prompt for reload when language server type changes to ${t}, Pylance is installed ans user clicks Reload`, async () => { + when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(pylanceExtension)); + when( + appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange(), Common.reload()) + ).thenReturn(Promise.resolve(Common.reload())); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(t); + + verify( + appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange(), Common.reload()) + ).once(); + verify(commands.executeCommand('workbench.action.reloadWindow')).once(); + }); + }); + + [LanguageServerType.None, LanguageServerType.Microsoft, LanguageServerType.Node].forEach(async (t) => { + test(`Handler should not prompt for reload when language server type changes to ${t}, Pylance is installed ans user does not clicks Reload`, async () => { + when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(instance(pylanceExtension)); + when( + appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange(), Common.reload()) + ).thenReturn(Promise.resolve(undefined)); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(t); + + verify( + appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange(), Common.reload()) + ).once(); + verify(commands.executeCommand('workbench.action.reloadWindow')).never(); + }); + }); + + test('Handler should prompt for install when language server changes to Pylance and Pylance is not installed', async () => { + when( + appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(undefined)); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(LanguageServerType.Node); + + verify( + appShell.showInformationMessage(LanguageService.reloadAfterLanguageServerChange(), Common.reload()) + ).never(); + verify( + appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).once(); + }); + + test('Handler should open Pylance store page when language server changes to Pylance, Pylance is not installed and user clicks Yes', async () => { + when( + appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(Common.bannerLabelYes())); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(LanguageServerType.Node); + + verify(appShell.openUrl(`scheme:extension/${PYLANCE_EXTENSION_ID}`)).once(); + verify(commands.executeCommand('workbench.action.reloadWindow')).never(); + }); + + test('Handler should not open Pylance store page when language server changes to Pylance, Pylance is not installed and user clicks No', async () => { + when( + appShell.showWarningMessage( + Pylance.installPylanceMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(Common.bannerLabelNo())); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(LanguageServerType.Node); + + verify(appShell.openUrl(`scheme:extension/${PYLANCE_EXTENSION_ID}`)).never(); + verify(commands.executeCommand('workbench.action.reloadWindow')).never(); + }); + + test('If Pylance was not installed and now it is, reload should be called if user agreed to it', async () => { + when( + appShell.showWarningMessage( + Pylance.pylanceInstalledReloadPromptMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(Common.bannerLabelYes())); + handler = makeHandler(LanguageServerType.Node); + + when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(pylanceExtension); + extensionsChangedEvent.fire(); + + await handler.pylanceInstallCompleted; + verify(commands.executeCommand('workbench.action.reloadWindow')).once(); + }); + + test('If Pylance was not installed and now it is, reload should not be called if user refused it', async () => { + when( + appShell.showWarningMessage( + Pylance.pylanceInstalledReloadPromptMessage(), + Common.bannerLabelYes(), + Common.bannerLabelNo() + ) + ).thenReturn(Promise.resolve(Common.bannerLabelNo())); + handler = makeHandler(LanguageServerType.Node); + + when(extensions.getExtension(PYLANCE_EXTENSION_ID)).thenReturn(pylanceExtension); + extensionsChangedEvent.fire(); + + await handler.pylanceInstallCompleted; + verify(commands.executeCommand('workbench.action.reloadWindow')).never(); + }); + + function makeHandler(initialLSType: LanguageServerType | undefined): LanguageServerChangeHandler { + return new LanguageServerChangeHandler( + initialLSType, + instance(extensions), + instance(appShell), + instance(appEnv), + instance(commands) + ); + } +}); diff --git a/src/test/activation/node/languageServerFolderService.unit.test.ts b/src/test/activation/node/languageServerFolderService.unit.test.ts new file mode 100644 index 000000000000..b944dde8ffb6 --- /dev/null +++ b/src/test/activation/node/languageServerFolderService.unit.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Extension, Uri, WorkspaceConfiguration } from 'vscode'; +import { + ILanguageServerFolder, + ILSExtensionApi, + NodeLanguageServerFolderService +} from '../../../client/activation/node/languageServerFolderService'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants'; +import { IConfigurationService, IExtensions, IPythonSettings } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable:max-func-body-length + +suite('Node Language Server Folder Service', () => { + const resource = Uri.parse('a'); + + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let configService: TypeMoq.IMock<IConfigurationService>; + let workspaceConfiguration: TypeMoq.IMock<WorkspaceConfiguration>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let extensions: TypeMoq.IMock<IExtensions>; + + class TestService extends NodeLanguageServerFolderService { + // tslint:disable-next-line: no-unnecessary-override + public languageServerFolder(): Promise<ILanguageServerFolder | undefined> { + return super.languageServerFolder(); + } + } + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + configService.setup((c) => c.getSettings(undefined)).returns(() => pythonSettings.object); + workspaceConfiguration = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspaceService + .setup((ws) => ws.getConfiguration('python', TypeMoq.It.isAny())) + .returns(() => workspaceConfiguration.object); + extensions = TypeMoq.Mock.ofType<IExtensions>(); + }); + + test('With packageName set', async () => { + pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true); + workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => 'somePackageName'); + + const folderService = new TestService( + serviceContainer.object, + configService.object, + workspaceService.object, + extensions.object + ); + + const lsf = await folderService.languageServerFolder(); + expect(lsf).to.be.equal(undefined, 'expected languageServerFolder to be undefined'); + expect(await folderService.skipDownload()).to.be.equal(false, 'skipDownload should be false'); + }); + + test('Invalid version', async () => { + pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true); + workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined); + + const folderService = new TestService( + serviceContainer.object, + configService.object, + workspaceService.object, + extensions.object + ); + + const lsf = await folderService.languageServerFolder(); + expect(lsf).to.be.equal(undefined, 'expected languageServerFolder to be undefined'); + expect(await folderService.skipDownload()).to.be.equal(false, 'skipDownload should be false'); + }); + + test('downloadLanguageServer set to false', async () => { + pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => false); + workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined); + + const folderService = new TestService( + serviceContainer.object, + configService.object, + workspaceService.object, + extensions.object + ); + + const lsf = await folderService.languageServerFolder(); + expect(lsf).to.be.equal(undefined, 'expected languageServerFolder to be undefined'); + expect(await folderService.skipDownload()).to.be.equal(false, 'skipDownload should be false'); + }); + + test('lsExtensionName is undefined', async () => { + pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true); + workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined); + + const folderService = new TestService( + serviceContainer.object, + configService.object, + workspaceService.object, + extensions.object + ); + + const lsf = await folderService.languageServerFolder(); + expect(lsf).to.be.equal(undefined, 'expected languageServerFolder to be undefined'); + expect(await folderService.skipDownload()).to.be.equal(false, 'skipDownload should be false'); + }); + + test('lsExtension not installed', async () => { + pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true); + workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined); + extensions.setup((e) => e.getExtension(PYLANCE_EXTENSION_ID)).returns(() => undefined); + + const folderService = new TestService( + serviceContainer.object, + configService.object, + workspaceService.object, + extensions.object + ); + + const lsf = await folderService.languageServerFolder(); + expect(lsf).to.be.equal(undefined, 'expected languageServerFolder to be undefined'); + expect(await folderService.skipDownload()).to.be.equal(false, 'skipDownload should be false'); + }); + + suite('Valid configuration', () => { + const lsPath = '/some/absolute/path'; + const lsVersion = '0.0.1-test'; + const extensionApi: ILSExtensionApi = { + languageServerFolder: async () => ({ + path: lsPath, + version: lsVersion + }) + }; + + let folderService: TestService; + let extension: TypeMoq.IMock<Extension<ILSExtensionApi>>; + + setup(() => { + extension = TypeMoq.Mock.ofType<Extension<ILSExtensionApi>>(); + extension.setup((e) => e.activate()).returns(() => Promise.resolve(extensionApi)); + extension.setup((e) => e.exports).returns(() => extensionApi); + pythonSettings.setup((p) => p.downloadLanguageServer).returns(() => true); + workspaceConfiguration.setup((wc) => wc.get('packageName')).returns(() => undefined); + extensions.setup((e) => e.getExtension(PYLANCE_EXTENSION_ID)).returns(() => extension.object); + folderService = new TestService( + serviceContainer.object, + configService.object, + workspaceService.object, + extensions.object + ); + }); + + test('skipDownload is true', async () => { + const skipDownload = await folderService.skipDownload(); + expect(skipDownload).to.be.equal(true, 'skipDownload should be true'); + }); + + test('Parsed version is correct', async () => { + const lsf = await folderService.languageServerFolder(); + assert(lsf); + expect(lsf!.version.format()).to.be.equal(lsVersion); + expect(lsf!.path).to.be.equal(lsPath); + }); + + test('getLanguageServerFolderName', async () => { + const folderName = await folderService.getLanguageServerFolderName(resource); + expect(folderName).to.be.equal(lsPath); + }); + + test('getLatestLanguageServerVersion', async () => { + const pkg = await folderService.getLatestLanguageServerVersion(resource); + expect(pkg).to.equal(undefined, 'expected latest version to be undefined'); + }); + + test('Method getCurrentLanguageServerDirectory()', async () => { + const dir = await folderService.getCurrentLanguageServerDirectory(); + assert(dir); + expect(dir!.path).to.equal(lsPath); + expect(dir!.version.format()).to.be.equal(lsVersion); + }); + }); +}); diff --git a/src/test/activation/serviceRegistry.unit.test.ts b/src/test/activation/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..51219665453e --- /dev/null +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { instance, mock, verify } from 'ts-mockito'; + +import { AATesting } from '../../client/activation/aaTesting'; +import { ExtensionActivationManager } from '../../client/activation/activationManager'; +import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; +import { DownloadBetaChannelRule, DownloadDailyChannelRule } from '../../client/activation/common/downloadChannelRules'; +import { LanguageServerDownloader } from '../../client/activation/common/downloader'; +import { LanguageServerDownloadChannel } from '../../client/activation/common/packageRepository'; +import { ExtensionSurveyPrompt } from '../../client/activation/extensionSurvey'; +import { JediExtensionActivator } from '../../client/activation/jedi'; +import { DotNetLanguageServerActivator } from '../../client/activation/languageServer/activator'; +import { DotNetLanguageServerAnalysisOptions } from '../../client/activation/languageServer/analysisOptions'; +import { DotNetLanguageClientFactory } from '../../client/activation/languageServer/languageClientFactory'; +import { LanguageServerCompatibilityService } from '../../client/activation/languageServer/languageServerCompatibilityService'; +import { LanguageServerExtension } from '../../client/activation/languageServer/languageServerExtension'; +import { DotNetLanguageServerFolderService } from '../../client/activation/languageServer/languageServerFolderService'; +import { + BetaDotNetLanguageServerPackageRepository, + DailyDotNetLanguageServerPackageRepository, + StableDotNetLanguageServerPackageRepository +} from '../../client/activation/languageServer/languageServerPackageRepository'; +import { DotNetLanguageServerPackageService } from '../../client/activation/languageServer/languageServerPackageService'; +import { DotNetLanguageServerProxy } from '../../client/activation/languageServer/languageServerProxy'; +import { DotNetLanguageServerManager } from '../../client/activation/languageServer/manager'; +import { LanguageServerOutputChannel } from '../../client/activation/languageServer/outputChannel'; +import { PlatformData } from '../../client/activation/languageServer/platformData'; +import { registerTypes } from '../../client/activation/serviceRegistry'; +import { + IDownloadChannelRule, + IExtensionActivationManager, + IExtensionSingleActivationService, + ILanguageClientFactory, + ILanguageServerActivator, + ILanguageServerAnalysisOptions, + ILanguageServerCache, + ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, + ILanguageServerDownloader, + ILanguageServerExtension, + ILanguageServerFolderService, + ILanguageServerManager, + ILanguageServerOutputChannel, + ILanguageServerPackageService, + ILanguageServerProxy, + IPlatformData, + LanguageServerType +} from '../../client/activation/types'; +import { INugetRepository } from '../../client/common/nuget/types'; +import { + BANNER_NAME_DS_SURVEY, + BANNER_NAME_INTERACTIVE_SHIFTENTER, + BANNER_NAME_PROPOSE_LS, + IPythonExtensionBanner +} from '../../client/common/types'; +import { DataScienceSurveyBanner } from '../../client/datascience/dataScienceSurveyBanner'; +import { InteractiveShiftEnterBanner } from '../../client/datascience/shiftEnterBanner'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceManager } from '../../client/ioc/types'; +import { ProposePylanceBanner } from '../../client/languageServices/proposeLanguageServerBanner'; + +// tslint:disable:max-func-body-length + +suite('Unit Tests - Language Server Activation Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + // tslint:disable-next-line: max-func-body-length + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager), LanguageServerType.Microsoft); + + verify( + serviceManager.addSingleton<ILanguageServerCache>( + ILanguageServerCache, + LanguageServerExtensionActivationService + ) + ).once(); + verify( + serviceManager.addSingleton<ILanguageServerExtension>(ILanguageServerExtension, LanguageServerExtension) + ).once(); + verify( + serviceManager.add<IExtensionActivationManager>(IExtensionActivationManager, ExtensionActivationManager) + ).once(); + verify( + serviceManager.add<ILanguageServerActivator>( + ILanguageServerActivator, + JediExtensionActivator, + LanguageServerType.Jedi + ) + ).once(); + verify( + serviceManager.add<ILanguageServerActivator>( + ILanguageServerActivator, + DotNetLanguageServerActivator, + LanguageServerType.Microsoft + ) + ).once(); + verify( + serviceManager.addSingleton<IPythonExtensionBanner>( + IPythonExtensionBanner, + ProposePylanceBanner, + BANNER_NAME_PROPOSE_LS + ) + ).once(); + verify( + serviceManager.addSingleton<IPythonExtensionBanner>( + IPythonExtensionBanner, + DataScienceSurveyBanner, + BANNER_NAME_DS_SURVEY + ) + ).once(); + verify( + serviceManager.addSingleton<IPythonExtensionBanner>( + IPythonExtensionBanner, + InteractiveShiftEnterBanner, + BANNER_NAME_INTERACTIVE_SHIFTENTER + ) + ).once(); + verify( + serviceManager.addSingleton<ILanguageServerFolderService>( + ILanguageServerFolderService, + DotNetLanguageServerFolderService + ) + ).once(); + verify( + serviceManager.addSingleton<ILanguageServerPackageService>( + ILanguageServerPackageService, + DotNetLanguageServerPackageService + ) + ).once(); + verify( + serviceManager.addSingleton<INugetRepository>( + INugetRepository, + StableDotNetLanguageServerPackageRepository, + LanguageServerDownloadChannel.stable + ) + ).once(); + verify( + serviceManager.addSingleton<INugetRepository>( + INugetRepository, + BetaDotNetLanguageServerPackageRepository, + LanguageServerDownloadChannel.beta + ) + ).once(); + verify( + serviceManager.addSingleton<INugetRepository>( + INugetRepository, + DailyDotNetLanguageServerPackageRepository, + LanguageServerDownloadChannel.daily + ) + ).once(); + verify( + serviceManager.addSingleton<IDownloadChannelRule>( + IDownloadChannelRule, + DownloadDailyChannelRule, + LanguageServerDownloadChannel.daily + ) + ).once(); + verify( + serviceManager.addSingleton<IDownloadChannelRule>( + IDownloadChannelRule, + DownloadBetaChannelRule, + LanguageServerDownloadChannel.beta + ) + ).once(); + verify( + serviceManager.addSingleton<IDownloadChannelRule>( + IDownloadChannelRule, + DownloadBetaChannelRule, + LanguageServerDownloadChannel.stable + ) + ).once(); + verify( + serviceManager.addSingleton<ILanagueServerCompatibilityService>( + ILanagueServerCompatibilityService, + LanguageServerCompatibilityService + ) + ).once(); + verify( + serviceManager.addSingleton<ILanguageClientFactory>(ILanguageClientFactory, DotNetLanguageClientFactory) + ).once(); + verify( + serviceManager.addSingleton<ILanguageServerDownloader>(ILanguageServerDownloader, LanguageServerDownloader) + ).once(); + verify(serviceManager.addSingleton<IPlatformData>(IPlatformData, PlatformData)).once(); + verify( + serviceManager.add<ILanguageServerAnalysisOptions>( + ILanguageServerAnalysisOptions, + DotNetLanguageServerAnalysisOptions, + LanguageServerType.Microsoft + ) + ).once(); + verify(serviceManager.add<ILanguageServerProxy>(ILanguageServerProxy, DotNetLanguageServerProxy)).once(); + verify(serviceManager.add<ILanguageServerManager>(ILanguageServerManager, DotNetLanguageServerManager)).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>(IExtensionSingleActivationService, AATesting) + ).once(); + verify( + serviceManager.addSingleton<ILanguageServerOutputChannel>( + ILanguageServerOutputChannel, + LanguageServerOutputChannel + ) + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ExtensionSurveyPrompt + ) + ).once(); + }); +}); diff --git a/src/test/analysisEngineTest.ts b/src/test/analysisEngineTest.ts new file mode 100644 index 000000000000..5a68406b9f32 --- /dev/null +++ b/src/test/analysisEngineTest.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-console no-require-imports no-var-requires +import * as path from 'path'; + +process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'test'); +process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; +process.env.VSC_PYTHON_LANGUAGE_SERVER = '1'; +process.env.TEST_FILES_SUFFIX = 'ls.test'; + +function start() { + console.log('*'.repeat(100)); + console.log('Start language server tests'); + require('../../node_modules/vscode/bin/test'); +} +start(); diff --git a/src/test/api.functional.test.ts b/src/test/api.functional.test.ts new file mode 100644 index 000000000000..58a32defc402 --- /dev/null +++ b/src/test/api.functional.test.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { assert, expect } from 'chai'; +import * as path from 'path'; +import { instance, mock, when } from 'ts-mockito'; +import * as Typemoq from 'typemoq'; +import { Event, Uri } from 'vscode'; +import { buildApi } from '../client/api'; +import { ConfigurationService } from '../client/common/configuration/service'; +import { EXTENSION_ROOT_DIR } from '../client/common/constants'; +import { IConfigurationService } from '../client/common/types'; +import { IInterpreterService } from '../client/interpreter/contracts'; +import { InterpreterService } from '../client/interpreter/interpreterService'; +import { ServiceContainer } from '../client/ioc/container'; +import { ServiceManager } from '../client/ioc/serviceManager'; +import { IServiceContainer, IServiceManager } from '../client/ioc/types'; + +suite('Extension API', () => { + const debuggerPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy'); + const debuggerHost = 'somehost'; + const debuggerPort = 12345; + + let serviceContainer: IServiceContainer; + let serviceManager: IServiceManager; + let configurationService: IConfigurationService; + let interpreterService: IInterpreterService; + + setup(() => { + serviceContainer = mock(ServiceContainer); + serviceManager = mock(ServiceManager); + configurationService = mock(ConfigurationService); + interpreterService = mock(InterpreterService); + + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn( + instance(configurationService) + ); + when(serviceContainer.get<IInterpreterService>(IInterpreterService)).thenReturn(instance(interpreterService)); + }); + + test('Execution details settings API returns expected object if interpreter is set', async () => { + const resource = Uri.parse('a'); + when(configurationService.getSettings(resource)).thenReturn({ pythonPath: 'settingValue' } as any); + + const execDetails = buildApi( + Promise.resolve(), + instance(serviceManager), + instance(serviceContainer) + ).settings.getExecutionDetails(resource); + + assert.deepEqual(execDetails, { execCommand: ['settingValue'] }); + }); + + test('Execution details settings API returns `undefined` if interpreter is set', async () => { + const resource = Uri.parse('a'); + when(configurationService.getSettings(resource)).thenReturn({ pythonPath: '' } as any); + + const execDetails = buildApi( + Promise.resolve(), + instance(serviceManager), + instance(serviceContainer) + ).settings.getExecutionDetails(resource); + + assert.deepEqual(execDetails, { execCommand: undefined }); + }); + + test('Provide a callback which is called when interpreter setting changes', async () => { + const expectedEvent = Typemoq.Mock.ofType<Event<Uri | undefined>>().object; + when(interpreterService.onDidChangeInterpreterConfiguration).thenReturn(expectedEvent); + + const result = buildApi(Promise.resolve(), instance(serviceManager), instance(serviceContainer)).settings + .onDidChangeExecutionDetails; + + assert.deepEqual(result, expectedEvent); + }); + + test('Test debug launcher args (no-wait)', async () => { + const waitForAttach = false; + + const args = await buildApi( + Promise.resolve(), + instance(serviceManager), + instance(serviceContainer) + ).debug.getRemoteLauncherCommand(debuggerHost, debuggerPort, waitForAttach); + const expectedArgs = [debuggerPath.fileToCommandArgument(), '--listen', `${debuggerHost}:${debuggerPort}`]; + + expect(args).to.be.deep.equal(expectedArgs); + }); + + test('Test debug launcher args (wait)', async () => { + const waitForAttach = true; + + const args = await buildApi( + Promise.resolve(), + instance(serviceManager), + instance(serviceContainer) + ).debug.getRemoteLauncherCommand(debuggerHost, debuggerPort, waitForAttach); + const expectedArgs = [ + debuggerPath.fileToCommandArgument(), + '--listen', + `${debuggerHost}:${debuggerPort}`, + '--wait-for-client' + ]; + + expect(args).to.be.deep.equal(expectedArgs); + }); + + test('Test debugger package path', async () => { + const pkgPath = await buildApi( + Promise.resolve(), + instance(serviceManager), + instance(serviceContainer) + ).debug.getDebuggerPackagePath(); + + assert.equal(pkgPath, debuggerPath); + }); +}); diff --git a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts new file mode 100644 index 000000000000..997a9fdd3725 --- /dev/null +++ b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:insecure-random no-any + +import * as assert from 'assert'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { DiagnosticSeverity } from 'vscode'; +import { ApplicationDiagnostics } from '../../../client/application/diagnostics/applicationDiagnostics'; +import { EnvironmentPathVariableDiagnosticsService } from '../../../client/application/diagnostics/checks/envPathVariable'; +import { InvalidPythonInterpreterService } from '../../../client/application/diagnostics/checks/pythonInterpreter'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticsService, + ISourceMapSupportService +} from '../../../client/application/diagnostics/types'; +import { IApplicationDiagnostics } from '../../../client/application/types'; +import { STANDARD_OUTPUT_CHANNEL } from '../../../client/common/constants'; +import { IOutputChannel } from '../../../client/common/types'; +import { createDeferred, createDeferredFromPromise } from '../../../client/common/utils/async'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { sleep } from '../../common'; + +// tslint:disable-next-line:max-func-body-length +suite('Application Diagnostics - ApplicationDiagnostics', () => { + let serviceContainer: typemoq.IMock<IServiceContainer>; + let envHealthCheck: typemoq.IMock<IDiagnosticsService>; + let lsNotSupportedCheck: typemoq.IMock<IDiagnosticsService>; + let pythonInterpreterCheck: typemoq.IMock<IDiagnosticsService>; + let outputChannel: typemoq.IMock<IOutputChannel>; + let appDiagnostics: IApplicationDiagnostics; + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + + setup(() => { + delete process.env.VSC_PYTHON_UNIT_TEST; + delete process.env.VSC_PYTHON_CI_TEST; + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + envHealthCheck = typemoq.Mock.ofType<IDiagnosticsService>(); + envHealthCheck.setup((service) => service.runInBackground).returns(() => true); + lsNotSupportedCheck = typemoq.Mock.ofType<IDiagnosticsService>(); + lsNotSupportedCheck.setup((service) => service.runInBackground).returns(() => false); + pythonInterpreterCheck = typemoq.Mock.ofType<IDiagnosticsService>(); + pythonInterpreterCheck.setup((service) => service.runInBackground).returns(() => false); + outputChannel = typemoq.Mock.ofType<IOutputChannel>(); + + serviceContainer + .setup((d) => d.getAll(typemoq.It.isValue(IDiagnosticsService))) + .returns(() => [envHealthCheck.object, lsNotSupportedCheck.object, pythonInterpreterCheck.object]); + serviceContainer + .setup((d) => d.get(typemoq.It.isValue(IOutputChannel), typemoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) + .returns(() => outputChannel.object); + + appDiagnostics = new ApplicationDiagnostics(serviceContainer.object, outputChannel.object); + }); + + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + }); + + test('Register should register source maps', () => { + const sourceMapService = typemoq.Mock.ofType<ISourceMapSupportService>(); + sourceMapService.setup((s) => s.register()).verifiable(typemoq.Times.once()); + + serviceContainer + .setup((d) => d.get(typemoq.It.isValue(ISourceMapSupportService), typemoq.It.isAny())) + .returns(() => sourceMapService.object); + + appDiagnostics.register(); + + sourceMapService.verifyAll(); + }); + + test('Performing Pre Startup Health Check must diagnose all validation checks', async () => { + envHealthCheck + .setup((e) => e.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + lsNotSupportedCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + pythonInterpreterCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + + await appDiagnostics.performPreStartupHealthCheck(undefined); + + envHealthCheck.verifyAll(); + lsNotSupportedCheck.verifyAll(); + pythonInterpreterCheck.verifyAll(); + }); + + test('Performing Pre Startup Health Check must handles all validation checks only once either in background or foreground', async () => { + const diagnostic: IDiagnostic = { + code: 'Error' as any, + message: 'Error', + scope: undefined, + severity: undefined, + resource: undefined, + invokeHandler: 'default' + } as any; + envHealthCheck + .setup((e) => e.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([diagnostic])) + .verifiable(typemoq.Times.once()); + envHealthCheck + .setup((p) => p.handle(typemoq.It.isValue([diagnostic]))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + lsNotSupportedCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([diagnostic])) + .verifiable(typemoq.Times.once()); + lsNotSupportedCheck + .setup((p) => p.handle(typemoq.It.isValue([diagnostic]))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + pythonInterpreterCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([diagnostic])) + .verifiable(typemoq.Times.once()); + pythonInterpreterCheck + .setup((p) => p.handle(typemoq.It.isValue([diagnostic]))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await appDiagnostics.performPreStartupHealthCheck(undefined); + await sleep(1); + + pythonInterpreterCheck.verifyAll(); + lsNotSupportedCheck.verifyAll(); + envHealthCheck.verifyAll(); + }); + + test('Diagnostics Returned by Pre Startup Health Checks must be logged', async () => { + const diagnostics: IDiagnostic[] = []; + for (let i = 0; i <= Math.random() * 10; i += 1) { + const diagnostic: IDiagnostic = { + code: `Error${i}` as any, + message: `Error${i}`, + scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, + severity: DiagnosticSeverity.Error, + resource: undefined, + invokeHandler: 'default' + }; + diagnostics.push(diagnostic); + } + for (let i = 0; i <= Math.random() * 10; i += 1) { + const diagnostic: IDiagnostic = { + code: `Warning${i}` as any, + message: `Warning${i}`, + scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, + severity: DiagnosticSeverity.Warning, + resource: undefined, + invokeHandler: 'default' + }; + diagnostics.push(diagnostic); + } + for (let i = 0; i <= Math.random() * 10; i += 1) { + const diagnostic: IDiagnostic = { + code: `Info${i}` as any, + message: `Info${i}`, + scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, + severity: DiagnosticSeverity.Information, + resource: undefined, + invokeHandler: 'default' + }; + diagnostics.push(diagnostic); + } + + for (const diagnostic of diagnostics) { + const message = `Diagnostic Code: ${diagnostic.code}, Message: ${diagnostic.message}`; + switch (diagnostic.severity) { + case DiagnosticSeverity.Error: { + outputChannel.setup((o) => o.appendLine(message)).verifiable(typemoq.Times.once()); + break; + } + case DiagnosticSeverity.Warning: { + outputChannel.setup((o) => o.appendLine(message)).verifiable(typemoq.Times.once()); + break; + } + default: { + break; + } + } + } + + envHealthCheck + .setup((e) => e.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve(diagnostics)) + .verifiable(typemoq.Times.once()); + lsNotSupportedCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + pythonInterpreterCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + + await appDiagnostics.performPreStartupHealthCheck(undefined); + await sleep(1); + + envHealthCheck.verifyAll(); + lsNotSupportedCheck.verifyAll(); + pythonInterpreterCheck.verifyAll(); + outputChannel.verifyAll(); + }); + test('Ensure diagnostics run in foreground and background', async () => { + const foreGroundService = mock(InvalidPythonInterpreterService); + const backGroundService = mock(EnvironmentPathVariableDiagnosticsService); + const svcContainer = mock(ServiceContainer); + const foreGroundDeferred = createDeferred<IDiagnostic[]>(); + const backgroundGroundDeferred = createDeferred<IDiagnostic[]>(); + + when(svcContainer.getAll<IDiagnosticsService>(IDiagnosticsService)).thenReturn([ + instance(foreGroundService), + instance(backGroundService) + ]); + when(foreGroundService.runInBackground).thenReturn(false); + when(backGroundService.runInBackground).thenReturn(true); + + when(foreGroundService.diagnose(anything())).thenReturn(foreGroundDeferred.promise); + when(backGroundService.diagnose(anything())).thenReturn(backgroundGroundDeferred.promise); + + const service = new ApplicationDiagnostics(instance(svcContainer), outputChannel.object); + + const promise = service.performPreStartupHealthCheck(undefined); + const deferred = createDeferredFromPromise(promise); + await sleep(1); + + verify(foreGroundService.runInBackground).atLeast(1); + verify(backGroundService.runInBackground).atLeast(1); + + assert.equal(deferred.completed, false); + foreGroundDeferred.resolve([]); + await sleep(1); + + assert.equal(deferred.completed, true); + + backgroundGroundDeferred.resolve([]); + await sleep(1); + verify(foreGroundService.diagnose(anything())).once(); + verify(backGroundService.diagnose(anything())).once(); + }); +}); diff --git a/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts b/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts new file mode 100644 index 000000000000..e0e37dd6c774 --- /dev/null +++ b/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import { DiagnosticSeverity } from 'vscode'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { EnvironmentPathVariableDiagnosticsService } from '../../../../client/application/diagnostics/checks/envPathVariable'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService +} from '../../../../client/application/diagnostics/types'; +import { IApplicationEnvironment, IWorkspaceService } from '../../../../client/common/application/types'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { ICurrentProcess, IPathUtils } from '../../../../client/common/types'; +import { EnvironmentVariables } from '../../../../client/common/variables/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +// tslint:disable:max-func-body-length no-any +suite('Application Diagnostics - Checks Env Path Variable', () => { + let diagnosticService: IDiagnosticsService; + let platformService: typemoq.IMock<IPlatformService>; + let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let filterService: typemoq.IMock<IDiagnosticFilterService>; + let procEnv: typemoq.IMock<EnvironmentVariables>; + let appEnv: typemoq.IMock<IApplicationEnvironment>; + let commandFactory: typemoq.IMock<IDiagnosticsCommandFactory>; + const pathVariableName = 'Path'; + const pathDelimiter = ';'; + const extensionName = 'Some Extension Name'; + setup(() => { + const serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + platformService = typemoq.Mock.ofType<IPlatformService>(); + platformService.setup((p) => p.pathVariableName).returns(() => pathVariableName); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) + ) + ) + .returns(() => messageHandler.object); + + appEnv = typemoq.Mock.ofType<IApplicationEnvironment>(); + appEnv.setup((a) => a.extensionName).returns(() => extensionName); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IApplicationEnvironment))).returns(() => appEnv.object); + + filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + + commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + + const currentProc = typemoq.Mock.ofType<ICurrentProcess>(); + procEnv = typemoq.Mock.ofType<EnvironmentVariables>(); + currentProc.setup((p) => p.env).returns(() => procEnv.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(ICurrentProcess))).returns(() => currentProc.object); + + const pathUtils = typemoq.Mock.ofType<IPathUtils>(); + pathUtils.setup((p) => p.delimiter).returns(() => pathDelimiter); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + workspaceService.setup((w) => w.getWorkspaceFolder(typemoq.It.isAny())).returns(() => undefined); + + diagnosticService = new (class extends EnvironmentPathVariableDiagnosticsService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })(serviceContainer.object, []); + (diagnosticService as any)._clear(); + }); + + test('Can handle EnvPathVariable diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(true, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Can not handle non-EnvPathVariable diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Should return empty diagnostics for Mac', async () => { + platformService.setup((p) => p.isMac).returns(() => true); + platformService.setup((p) => p.isLinux).returns(() => false); + platformService.setup((p) => p.isWindows).returns(() => false); + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + }); + test('Should return empty diagnostics for Linux', async () => { + platformService.setup((p) => p.isMac).returns(() => false); + platformService.setup((p) => p.isLinux).returns(() => true); + platformService.setup((p) => p.isWindows).returns(() => false); + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + }); + test('Should return empty diagnostics for Windows if path variable is valid', async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const paths = [path.join('one', 'two', 'three'), path.join('one', 'two', 'four')].join(pathDelimiter); + procEnv.setup((env) => env[pathVariableName]).returns(() => paths); + + const diagnostics = await diagnosticService.diagnose(undefined); + + expect(diagnostics).to.be.deep.equal([]); + }); + // Note: On windows, when a path contains a `;` then Windows encloses the path within `"`. + test("Should return single diagnostics for Windows if path contains '\"'", async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const paths = [path.join('one', 'two', 'three"'), path.join('one', 'two', 'four')].join(pathDelimiter); + procEnv.setup((env) => env[pathVariableName]).returns(() => paths); + + const diagnostics = await diagnosticService.diagnose(undefined); + + expect(diagnostics).to.be.lengthOf(1); + expect(diagnostics[0].code).to.be.equal(DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic); + expect(diagnostics[0].message).to.contain(extensionName); + expect(diagnostics[0].message).to.contain(pathVariableName); + expect(diagnostics[0].severity).to.be.equal(DiagnosticSeverity.Warning); + expect(diagnostics[0].scope).to.be.equal(DiagnosticScope.Global); + }); + test('Should not return diagnostics for Windows if path ends with delimiter', async () => { + const paths = + [path.join('one', 'two', 'three'), path.join('one', 'two', 'four')].join(pathDelimiter) + pathDelimiter; + platformService.setup((p) => p.isWindows).returns(() => true); + procEnv.setup((env) => env[pathVariableName]).returns(() => paths); + + const diagnostics = await diagnosticService.diagnose(undefined); + + expect(diagnostics).to.be.lengthOf(0); + }); + test('Should display three options in message displayed with 2 commands', async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + const alwaysIgnoreCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ + type: 'ignore', + options: DiagnosticScope.Global + }) + ) + ) + .returns(() => alwaysIgnoreCommand.object) + .verifiable(typemoq.Times.once()); + const launchBrowserCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }) + ) + ) + .returns(() => launchBrowserCommand.object) + .verifiable(typemoq.Times.once()); + messageHandler.setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic.object]); + + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + }); + test('Should not display a message if the diagnostic code has been ignored', async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + + filterService + .setup((f) => + f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic)) + ) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + messageHandler.setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic.object]); + + filterService.verifyAll(); + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts b/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts new file mode 100644 index 000000000000..3c27c5561935 --- /dev/null +++ b/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts @@ -0,0 +1,523 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + InvalidLaunchJsonDebuggerDiagnostic, + InvalidLaunchJsonDebuggerService +} from '../../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; +import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; +import { + IDiagnostic, + IDiagnosticHandlerService, + IDiagnosticsService +} from '../../../../client/application/diagnostics/types'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { Diagnostics } from '../../../../client/common/utils/localize'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +// tslint:disable:max-func-body-length no-any +suite('Application Diagnostics - Checks if launch.json is invalid', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let diagnosticService: IDiagnosticsService; + let commandFactory: TypeMoq.IMock<IDiagnosticsCommandFactory>; + let fs: TypeMoq.IMock<IFileSystem>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let baseWorkspaceService: TypeMoq.IMock<IWorkspaceService>; + let messageHandler: TypeMoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let workspaceFolder: WorkspaceFolder; + setup(() => { + workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + commandFactory = TypeMoq.Mock.ofType<IDiagnosticsCommandFactory>(); + fs = TypeMoq.Mock.ofType<IFileSystem>(); + messageHandler = TypeMoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + baseWorkspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => baseWorkspaceService.object); + + diagnosticService = new (class extends InvalidLaunchJsonDebuggerService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + public async fixLaunchJson(code: DiagnosticCodes) { + await super.fixLaunchJson(code); + } + })(serviceContainer.object, fs.object, [], workspaceService.object, messageHandler.object); + (diagnosticService as any)._clear(); + }); + + test('Can handle all InvalidLaunchJsonDebugger diagnostics', async () => { + for (const code of [ + DiagnosticCodes.InvalidDebuggerTypeDiagnostic, + DiagnosticCodes.JustMyCodeDiagnostic, + DiagnosticCodes.ConsoleTypeDiagnostic + ]) { + const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => code) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); + diagnostic.verifyAll(); + } + }); + + test('Can not handle non-InvalidLaunchJsonDebugger diagnostics', async () => { + const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + + test('Should return empty diagnostics if there are no workspace folders', async () => { + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + workspaceService.verifyAll(); + }); + + test('Should return empty diagnostics if file launch.json does not exist', async () => { + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.getWorkspaceFolder(undefined)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + workspaceService.verifyAll(); + fs.verifyAll(); + }); + + test('Should return empty diagnostics if file launch.json does not contain strings "pythonExperimental" and "debugStdLib" ', async () => { + const fileContents = 'Hello I am launch.json, although I am not very jsony'; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(fileContents)) + .verifiable(TypeMoq.Times.once()); + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + workspaceService.verifyAll(); + fs.verifyAll(); + }); + + test('Should return InvalidDebuggerTypeDiagnostic if file launch.json contains string "pythonExperimental"', async () => { + const fileContents = 'Hello I am launch.json, I contain string "pythonExperimental"'; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(fileContents)) + .verifiable(TypeMoq.Times.once()); + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, undefined)], + 'Diagnostics returned are not as expected' + ); + workspaceService.verifyAll(); + fs.verifyAll(); + }); + + test('Should return JustMyCodeDiagnostic if file launch.json contains string "debugStdLib"', async () => { + const fileContents = 'Hello I am launch.json, I contain string "debugStdLib"'; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(fileContents)) + .verifiable(TypeMoq.Times.once()); + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined)], + 'Diagnostics returned are not as expected' + ); + workspaceService.verifyAll(); + fs.verifyAll(); + }); + + test('Should return ConfigPythonPathDiagnostic if file launch.json contains string "{config:python.pythonPath}"', async () => { + const fileContents = 'Hello I am launch.json, I contain string {config:python.pythonPath}'; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(fileContents)) + .verifiable(TypeMoq.Times.once()); + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, undefined, false)], + 'Diagnostics returned are not as expected' + ); + workspaceService.verifyAll(); + fs.verifyAll(); + }); + + test('Should return ConfigPythonPathDiagnostic if file launch.json contains string "{config:python.interpreterPath}"', async () => { + const fileContents = 'Hello I am launch.json, I contain string {config:python.interpreterPath}'; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(fileContents)) + .verifiable(TypeMoq.Times.once()); + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.ConfigPythonPathDiagnostic, undefined, false)], + 'Diagnostics returned are not as expected' + ); + workspaceService.verifyAll(); + fs.verifyAll(); + }); + + test('Should return both diagnostics if file launch.json contains string "debugStdLib" and "pythonExperimental"', async () => { + const fileContents = 'Hello I am launch.json, I contain both "debugStdLib" and "pythonExperimental"'; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(fileContents)) + .verifiable(TypeMoq.Times.once()); + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [ + new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, undefined), + new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined) + ], + 'Diagnostics returned are not as expected' + ); + workspaceService.verifyAll(); + fs.verifyAll(); + }); + + test('All InvalidLaunchJsonDebugger diagnostics with `shouldShowPrompt` set to `true` should display a prompt with 2 buttons where clicking the first button will invoke a command', async () => { + for (const code of [ + DiagnosticCodes.InvalidDebuggerTypeDiagnostic, + DiagnosticCodes.JustMyCodeDiagnostic, + DiagnosticCodes.ConsoleTypeDiagnostic + ]) { + const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); + let options: MessageCommandPrompt | undefined; + diagnostic + .setup((d) => d.code) + .returns(() => code) + .verifiable(TypeMoq.Times.atLeastOnce()); + diagnostic + .setup((d) => d.shouldShowPrompt) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + messageHandler + .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_, opts: MessageCommandPrompt) => (options = opts)) + .verifiable(TypeMoq.Times.atLeastOnce()); + baseWorkspaceService + .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) + .returns(() => workspaceFolder) + .verifiable(TypeMoq.Times.atLeastOnce()); + + await diagnosticService.handle([diagnostic.object]); + + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + baseWorkspaceService.verifyAll(); + expect(options!.commandPrompts).to.be.lengthOf(2); + expect(options!.commandPrompts[0].prompt).to.be.equal(Diagnostics.yesUpdateLaunch()); + expect(options!.commandPrompts[0].command).not.to.be.equal(undefined, 'Command not set'); + } + }); + + test('All InvalidLaunchJsonDebugger diagnostics with `shouldShowPrompt` set to `false` should directly fix launch.json', async () => { + for (const code of [DiagnosticCodes.ConfigPythonPathDiagnostic]) { + let called = false; + (diagnosticService as any).fixLaunchJson = () => { + called = true; + }; + const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => code) + .verifiable(TypeMoq.Times.atLeastOnce()); + diagnostic + .setup((d) => d.shouldShowPrompt) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + messageHandler + .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.never()); + baseWorkspaceService + .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) + .returns(() => workspaceFolder) + .verifiable(TypeMoq.Times.atLeastOnce()); + + await diagnosticService.handle([diagnostic.object]); + + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + baseWorkspaceService.verifyAll(); + expect(called).to.equal(true, ''); + } + }); + + test('All InvalidLaunchJsonDebugger diagnostics should display message twice if invoked twice', async () => { + for (const code of [ + DiagnosticCodes.InvalidDebuggerTypeDiagnostic, + DiagnosticCodes.JustMyCodeDiagnostic, + DiagnosticCodes.ConsoleTypeDiagnostic + ]) { + const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => code) + .verifiable(TypeMoq.Times.atLeastOnce()); + diagnostic + .setup((d) => d.invokeHandler) + .returns(() => 'always') + .verifiable(TypeMoq.Times.atLeastOnce()); + messageHandler.reset(); + messageHandler + .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.exactly(2)); + baseWorkspaceService + .setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())) + .returns(() => workspaceFolder) + .verifiable(TypeMoq.Times.never()); + + await diagnosticService.handle([diagnostic.object]); + await diagnosticService.handle([diagnostic.object]); + + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + baseWorkspaceService.verifyAll(); + } + }); + + test('Function fixLaunchJson() returns if there are no workspace folders', async () => { + for (const code of [ + DiagnosticCodes.InvalidDebuggerTypeDiagnostic, + DiagnosticCodes.JustMyCodeDiagnostic, + DiagnosticCodes.ConsoleTypeDiagnostic + ]) { + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.never()); + await (diagnosticService as any).fixLaunchJson(code); + workspaceService.verifyAll(); + } + }); + + test('Function fixLaunchJson() returns if file launch.json does not exist', async () => { + for (const code of [ + DiagnosticCodes.InvalidDebuggerTypeDiagnostic, + DiagnosticCodes.JustMyCodeDiagnostic, + DiagnosticCodes.ConsoleTypeDiagnostic + ]) { + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.atLeastOnce()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.atLeastOnce()); + fs.setup((w) => w.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve('')) + .verifiable(TypeMoq.Times.never()); + await (diagnosticService as any).fixLaunchJson(code); + workspaceService.verifyAll(); + fs.verifyAll(); + } + }); + + test('File launch.json is fixed correctly when code equals JustMyCodeDiagnostic ', async () => { + const launchJson = '{"debugStdLib": true, "debugStdLib": false}'; + const correctedlaunchJson = '{"justMyCode": false, "justMyCode": true}'; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(launchJson)) + .verifiable(TypeMoq.Times.atLeastOnce()); + fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.JustMyCodeDiagnostic); + workspaceService.verifyAll(); + fs.verifyAll(); + }); + + test('File launch.json is fixed correctly when code equals InvalidDebuggerTypeDiagnostic ', async () => { + const launchJson = '{"Python Experimental: task" "pythonExperimental"}'; + const correctedlaunchJson = '{"Python: task" "python"}'; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(launchJson)) + .verifiable(TypeMoq.Times.atLeastOnce()); + fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.InvalidDebuggerTypeDiagnostic); + workspaceService.verifyAll(); + fs.verifyAll(); + }); + + test('File launch.json is fixed correctly when code equals ConsoleTypeDiagnostic ', async () => { + const launchJson = '{"console": "none"}'; + const correctedlaunchJson = '{"console": "internalConsole"}'; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(launchJson)) + .verifiable(TypeMoq.Times.atLeastOnce()); + fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.ConsoleTypeDiagnostic); + workspaceService.verifyAll(); + fs.verifyAll(); + }); + + test('File launch.json is fixed correctly when code equals ConfigPythonPathDiagnostic ', async () => { + const launchJson = 'This string contains {config:python.pythonPath} & {config:python.interpreterPath}'; + const correctedlaunchJson = + 'This string contains {command:python.interpreterPath} & {command:python.interpreterPath}'; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fs.setup((w) => w.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(launchJson)) + .verifiable(TypeMoq.Times.atLeastOnce()); + fs.setup((w) => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.ConfigPythonPathDiagnostic); + workspaceService.verifyAll(); + fs.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts b/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts new file mode 100644 index 000000000000..28e415be967e --- /dev/null +++ b/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts @@ -0,0 +1,419 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-template-strings max-func-body-length no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { InvalidPythonPathInDebuggerService } from '../../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../../../client/application/diagnostics/promptHandler'; +import { + IDiagnostic, + IDiagnosticCommand, + IDiagnosticHandlerService, + IInvalidPythonPathInDebuggerService +} from '../../../../client/application/diagnostics/types'; +import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; +import { IDocumentManager, IWorkspaceService } from '../../../../client/common/application/types'; +import { IConfigurationService, IPythonSettings } from '../../../../client/common/types'; +import { PythonPathSource } from '../../../../client/debugger/extension/types'; +import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Checks Python Path in debugger', () => { + let diagnosticService: IInvalidPythonPathInDebuggerService; + let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let commandFactory: typemoq.IMock<IDiagnosticsCommandFactory>; + let configService: typemoq.IMock<IConfigurationService>; + let helper: typemoq.IMock<IInterpreterHelper>; + let workspaceService: typemoq.IMock<IWorkspaceService>; + let docMgr: typemoq.IMock<IDocumentManager>; + setup(() => { + const serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) + ) + ) + .returns(() => messageHandler.object); + commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); + docMgr = typemoq.Mock.ofType<IDocumentManager>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + configService = typemoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + helper = typemoq.Mock.ofType<IInterpreterHelper>(); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + + diagnosticService = new (class extends InvalidPythonPathInDebuggerService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })( + serviceContainer.object, + workspaceService.object, + commandFactory.object, + helper.object, + docMgr.object, + configService.object, + [], + messageHandler.object + ); + (diagnosticService as any)._clear(); + }); + + test('Can handle InvalidPythonPathInDebugger diagnostics', async () => { + for (const code of [ + DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, + DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic + ]) { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => code) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); + diagnostic.verifyAll(); + } + }); + test('Can not handle non-InvalidPythonPathInDebugger diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Should return empty diagnostics', async () => { + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + }); + test('InvalidPythonPathInDebuggerSettings diagnostic should display one option to with a command', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + const interpreterSelectionCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand' + }) + ) + ) + .returns(() => interpreterSelectionCommand.object) + .verifiable(typemoq.Times.once()); + messageHandler.setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic.object]); + + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + }); + test('InvalidPythonPathInDebuggerSettings diagnostic should display message once if invoked twice', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + diagnostic + .setup((d) => d.invokeHandler) + .returns(() => 'default') + .verifiable(typemoq.Times.atLeastOnce()); + const interpreterSelectionCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand' + }) + ) + ) + .returns(() => interpreterSelectionCommand.object) + .verifiable(typemoq.Times.exactly(1)); + messageHandler + .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.exactly(1)); + + await diagnosticService.handle([diagnostic.object]); + await diagnosticService.handle([diagnostic.object]); + + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + }); + test('InvalidPythonPathInDebuggerSettings diagnostic should display message twice if invoked twice', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + diagnostic + .setup((d) => d.invokeHandler) + .returns(() => 'always') + .verifiable(typemoq.Times.atLeastOnce()); + const interpreterSelectionCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand' + }) + ) + ) + .returns(() => interpreterSelectionCommand.object) + .verifiable(typemoq.Times.exactly(2)); + messageHandler + .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.exactly(2)); + + await diagnosticService.handle([diagnostic.object]); + await diagnosticService.handle([diagnostic.object]); + + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + }); + test('InvalidPythonPathInDebuggerLaunch diagnostic should display one option to with a command', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + let options: MessageCommandPrompt | undefined; + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + messageHandler + .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_, opts: MessageCommandPrompt) => (options = opts)) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic.object]); + + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + expect(options!.commandPrompts).to.be.lengthOf(1); + expect(options!.commandPrompts[0].prompt).to.be.equal('Open launch.json'); + }); + test('Ensure we get python path from config when path = ${command:python.interpreterPath}', async () => { + const pythonPath = '${command:python.interpreterPath}'; + + const settings = typemoq.Mock.ofType<IPythonSettings>(); + settings + .setup((s) => s.pythonPath) + .returns(() => 'p') + .verifiable(typemoq.Times.once()); + configService + .setup((c) => c.getSettings(typemoq.It.isAny())) + .returns(() => settings.object) + .verifiable(typemoq.Times.once()); + helper + .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) + .returns(() => Promise.resolve({})) + .verifiable(typemoq.Times.once()); + + const valid = await diagnosticService.validatePythonPath(pythonPath); + + settings.verifyAll(); + configService.verifyAll(); + helper.verifyAll(); + expect(valid).to.be.equal(true, 'not valid'); + }); + test('Ensure ${workspaceFolder} is not expanded when a resource is not passed', async () => { + const pythonPath = '${workspaceFolder}/venv/bin/python'; + + workspaceService + .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) + .returns(() => undefined) + .verifiable(typemoq.Times.never()); + helper + .setup((h) => h.getInterpreterInformation(typemoq.It.isAny())) + .returns(() => Promise.resolve({})) + .verifiable(typemoq.Times.once()); + + await diagnosticService.validatePythonPath(pythonPath); + + configService.verifyAll(); + helper.verifyAll(); + }); + test('Ensure ${workspaceFolder} is expanded', async () => { + const pythonPath = '${workspaceFolder}/venv/bin/python'; + + const workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; + const expectedPath = `${workspaceFolder.uri.fsPath}/venv/bin/python`; + + workspaceService + .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) + .returns(() => workspaceFolder) + .verifiable(typemoq.Times.once()); + helper + .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(expectedPath))) + .returns(() => Promise.resolve({})) + .verifiable(typemoq.Times.once()); + + const valid = await diagnosticService.validatePythonPath( + pythonPath, + PythonPathSource.settingsJson, + Uri.parse('something') + ); + + configService.verifyAll(); + helper.verifyAll(); + expect(valid).to.be.equal(true, 'not valid'); + }); + test('Ensure ${env:XYZ123} is expanded', async () => { + const pythonPath = '${env:XYZ123}/venv/bin/python'; + + process.env.XYZ123 = 'something/else'; + const expectedPath = `${process.env.XYZ123}/venv/bin/python`; + workspaceService + .setup((c) => c.getWorkspaceFolder(typemoq.It.isAny())) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + helper + .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(expectedPath))) + .returns(() => Promise.resolve({})) + .verifiable(typemoq.Times.once()); + + const valid = await diagnosticService.validatePythonPath(pythonPath); + + configService.verifyAll(); + helper.verifyAll(); + expect(valid).to.be.equal(true, 'not valid'); + }); + test('Ensure we get python path from config when path = undefined', async () => { + const pythonPath = undefined; + + const settings = typemoq.Mock.ofType<IPythonSettings>(); + settings + .setup((s) => s.pythonPath) + .returns(() => 'p') + .verifiable(typemoq.Times.once()); + configService + .setup((c) => c.getSettings(typemoq.It.isAny())) + .returns(() => settings.object) + .verifiable(typemoq.Times.once()); + helper + .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) + .returns(() => Promise.resolve({})) + .verifiable(typemoq.Times.once()); + + const valid = await diagnosticService.validatePythonPath(pythonPath); + + settings.verifyAll(); + configService.verifyAll(); + helper.verifyAll(); + expect(valid).to.be.equal(true, 'not valid'); + }); + test('Ensure we do not get python path from config when path is provided', async () => { + const pythonPath = path.join('a', 'b'); + + const settings = typemoq.Mock.ofType<IPythonSettings>(); + configService + .setup((c) => c.getSettings(typemoq.It.isAny())) + .returns(() => settings.object) + .verifiable(typemoq.Times.never()); + helper + .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve({})) + .verifiable(typemoq.Times.once()); + + const valid = await diagnosticService.validatePythonPath(pythonPath); + + configService.verifyAll(); + helper.verifyAll(); + expect(valid).to.be.equal(true, 'not valid'); + }); + test('Ensure InvalidPythonPathInDebuggerLaunch diagnostic is handled when path is invalid in launch.json', async () => { + const pythonPath = path.join('a', 'b'); + const settings = typemoq.Mock.ofType<IPythonSettings>(); + configService + .setup((c) => c.getSettings(typemoq.It.isAny())) + .returns(() => settings.object) + .verifiable(typemoq.Times.never()); + let handleInvoked = false; + diagnosticService.handle = (diagnostics) => { + if ( + diagnostics.length !== 0 && + diagnostics[0].code === DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic + ) { + handleInvoked = true; + } + return Promise.resolve(); + }; + helper + .setup((h) => h.getInterpreterInformation(typemoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.once()); + + const valid = await diagnosticService.validatePythonPath(pythonPath, PythonPathSource.launchJson); + + helper.verifyAll(); + expect(valid).to.be.equal(false, 'should be invalid'); + expect(handleInvoked).to.be.equal(true, 'should be invoked'); + }); + test('Ensure InvalidPythonPathInDebuggerSettings diagnostic is handled when path is invalid in settings.json', async () => { + const pythonPath = undefined; + const settings = typemoq.Mock.ofType<IPythonSettings>(); + settings + .setup((s) => s.pythonPath) + .returns(() => 'p') + .verifiable(typemoq.Times.once()); + configService + .setup((c) => c.getSettings(typemoq.It.isAny())) + .returns(() => settings.object) + .verifiable(typemoq.Times.once()); + let handleInvoked = false; + diagnosticService.handle = (diagnostics) => { + if ( + diagnostics.length !== 0 && + diagnostics[0].code === DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic + ) { + handleInvoked = true; + } + return Promise.resolve(); + }; + helper + .setup((h) => h.getInterpreterInformation(typemoq.It.isValue('p'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.once()); + + const valid = await diagnosticService.validatePythonPath(pythonPath, PythonPathSource.settingsJson); + + helper.verifyAll(); + expect(valid).to.be.equal(false, 'should be invalid'); + expect(handleInvoked).to.be.equal(true, 'should be invoked'); + }); +}); diff --git a/src/test/application/diagnostics/checks/lsNotSupported.unit.test.ts b/src/test/application/diagnostics/checks/lsNotSupported.unit.test.ts new file mode 100644 index 000000000000..4f537b62c5bf --- /dev/null +++ b/src/test/application/diagnostics/checks/lsNotSupported.unit.test.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { ILanguageServerCompatibilityService } from '../../../../client/activation/types'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { LSNotSupportedDiagnosticService } from '../../../../client/application/diagnostics/checks/lsNotSupported'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService +} from '../../../../client/application/diagnostics/types'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +// tslint:disable:max-func-body-length no-any +suite('Application Diagnostics - Checks LS not supported', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let diagnosticService: IDiagnosticsService; + let filterService: TypeMoq.IMock<IDiagnosticFilterService>; + let commandFactory: TypeMoq.IMock<IDiagnosticsCommandFactory>; + let messageHandler: TypeMoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let lsCompatibility: TypeMoq.IMock<ILanguageServerCompatibilityService>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + filterService = TypeMoq.Mock.ofType<IDiagnosticFilterService>(); + commandFactory = TypeMoq.Mock.ofType<IDiagnosticsCommandFactory>(); + messageHandler = TypeMoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + lsCompatibility = TypeMoq.Mock.ofType<ILanguageServerCompatibilityService>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + serviceContainer + .setup((s) => + s.get( + TypeMoq.It.isValue(IDiagnosticHandlerService), + TypeMoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) + ) + ) + .returns(() => messageHandler.object); + const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + workspaceService.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + + diagnosticService = new (class extends LSNotSupportedDiagnosticService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })(serviceContainer.object, lsCompatibility.object, messageHandler.object, []); + (diagnosticService as any)._clear(); + }); + + test('Should display two options in message displayed with 2 commands', async () => { + let options: MessageCommandPrompt | undefined; + const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.LSNotSupportedDiagnostic) + .verifiable(TypeMoq.Times.atLeastOnce()); + const launchBrowserCommand = TypeMoq.Mock.ofType<IDiagnosticCommand>(); + commandFactory + .setup((f) => + f.createCommand( + TypeMoq.It.isAny(), + TypeMoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }) + ) + ) + .returns(() => launchBrowserCommand.object) + .verifiable(TypeMoq.Times.once()); + const alwaysIgnoreCommand = TypeMoq.Mock.ofType<IDiagnosticCommand>(); + commandFactory + .setup((f) => + f.createCommand( + TypeMoq.It.isAny(), + TypeMoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ + type: 'ignore', + options: DiagnosticScope.Global + }) + ) + ) + .returns(() => alwaysIgnoreCommand.object) + .verifiable(TypeMoq.Times.once()); + messageHandler + .setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_, opts: MessageCommandPrompt) => (options = opts)) + .verifiable(TypeMoq.Times.once()); + + await diagnosticService.handle([diagnostic.object]); + + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + expect(options!.commandPrompts).to.be.lengthOf(2); + expect(options!.commandPrompts[0].prompt).to.be.equal('More Info'); + }); + test('Should not display a message if the diagnostic code has been ignored', async () => { + const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); + + filterService + .setup((f) => f.shouldIgnoreDiagnostic(TypeMoq.It.isValue(DiagnosticCodes.LSNotSupportedDiagnostic))) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.LSNotSupportedDiagnostic) + .verifiable(TypeMoq.Times.atLeastOnce()); + commandFactory + .setup((f) => f.createCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.never()); + messageHandler.setup((m) => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())).verifiable(TypeMoq.Times.never()); + + await diagnosticService.handle([diagnostic.object]); + + filterService.verifyAll(); + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + }); + + test('LSNotSupportedDiagnosticService can handle LSNotSupported diagnostics', async () => { + const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.LSNotSupportedDiagnostic) + .verifiable(TypeMoq.Times.atLeastOnce()); + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(true, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('LSNotSupportedDiagnosticService can not handle non-LSNotSupported diagnostics', async () => { + const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) + .verifiable(TypeMoq.Times.atLeastOnce()); + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts new file mode 100644 index 000000000000..48bf504eab11 --- /dev/null +++ b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts @@ -0,0 +1,765 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any max-classes-per-file + +import { assert, expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { ConfigurationChangeEvent, Uri } from 'vscode'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + InvalidMacPythonInterpreterDiagnostic, + InvalidMacPythonInterpreterService +} from '../../../../client/application/diagnostics/checks/macPythonInterpreter'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService +} from '../../../../client/application/diagnostics/types'; +import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { DeprecatePythonPath } from '../../../../client/common/experiments/groups'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentsManager, + IInterpreterPathService, + InterpreterConfigurationScope, + IPythonSettings, + Resource +} from '../../../../client/common/types'; +import { sleep } from '../../../../client/common/utils/async'; +import { noop } from '../../../../client/common/utils/misc'; +import { IInterpreterHelper, IInterpreterService } from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; + +suite('Application Diagnostics - Checks Mac Python Interpreter', () => { + let diagnosticService: IDiagnosticsService; + let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let commandFactory: typemoq.IMock<IDiagnosticsCommandFactory>; + let settings: typemoq.IMock<IPythonSettings>; + let interpreterService: typemoq.IMock<IInterpreterService>; + let platformService: typemoq.IMock<IPlatformService>; + let helper: typemoq.IMock<IInterpreterHelper>; + let filterService: typemoq.IMock<IDiagnosticFilterService>; + const pythonPath = 'My Python Path in Settings'; + let serviceContainer: typemoq.IMock<IServiceContainer>; + function createContainer() { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) + ) + ) + .returns(() => messageHandler.object); + commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + settings = typemoq.Mock.ofType<IPythonSettings>(); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + const configService = typemoq.Mock.ofType<IConfigurationService>(); + configService.setup((c) => c.getSettings(typemoq.It.isAny())).returns(() => settings.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + interpreterService = typemoq.Mock.ofType<IInterpreterService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + platformService = typemoq.Mock.ofType<IPlatformService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + helper = typemoq.Mock.ofType<IInterpreterHelper>(); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); + filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + + platformService + .setup((p) => p.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + return serviceContainer.object; + } + suite('Diagnostics', () => { + setup(() => { + diagnosticService = new (class extends InvalidMacPythonInterpreterService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + protected addPythonPathChangedHandler() { + noop(); + } + })(createContainer(), interpreterService.object, [], platformService.object, helper.object); + (diagnosticService as any)._clear(); + }); + + test('Can handle InvalidPythonPathInterpreter diagnostics', async () => { + for (const code of [ + DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, + DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic + ]) { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => code) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); + diagnostic.verifyAll(); + } + }); + test('Can not handle non-InvalidPythonPathInterpreter diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Should return empty diagnostics if not a Mac', async () => { + platformService.reset(); + platformService + .setup((p) => p.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + platformService.verifyAll(); + }); + test('Should return empty diagnostics if installer check is disabled', async () => { + settings + .setup((s) => s.disableInstallationChecks) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + settings.verifyAll(); + platformService.verifyAll(); + }); + test('Should return empty diagnostics if there are interpreters, one is selected, and platform is not mac', async () => { + settings + .setup((s) => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.hasInterpreters) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.getInterpreters(typemoq.It.isAny())) + .returns(() => Promise.resolve([{} as any])) + .verifiable(typemoq.Times.never()); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve({ envType: EnvironmentType.Unknown } as any); + }) + .verifiable(typemoq.Times.once()); + platformService + .setup((i) => i.isMac) + .returns(() => false) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + settings.verifyAll(); + interpreterService.verifyAll(); + platformService.verifyAll(); + }); + test('Should return empty diagnostics if there are interpreters, platform is mac and selected interpreter is not default mac interpreter', async () => { + settings + .setup((s) => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.hasInterpreters) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.getInterpreters(typemoq.It.isAny())) + .returns(() => Promise.resolve([{} as any])) + .verifiable(typemoq.Times.never()); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve({ envType: EnvironmentType.Unknown } as any); + }) + .verifiable(typemoq.Times.once()); + platformService + .setup((i) => i.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + helper + .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isAny())) + .returns(() => false) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + settings.verifyAll(); + interpreterService.verifyAll(); + platformService.verifyAll(); + helper.verifyAll(); + }); + test('Should return diagnostic if there are no other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { + settings + .setup((s) => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.getInterpreters(typemoq.It.isAny())) + .returns(() => Promise.resolve([{ path: pythonPath } as any, { path: pythonPath } as any])) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve({ envType: EnvironmentType.Unknown } as any); + }) + .verifiable(typemoq.Times.once()); + platformService + .setup((i) => i.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + helper + .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) + .returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [ + new InvalidMacPythonInterpreterDiagnostic( + DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, + undefined + ) + ], + 'not the same' + ); + settings.verifyAll(); + interpreterService.verifyAll(); + platformService.verifyAll(); + helper.verifyAll(); + }); + test('Should return diagnostic if there are other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { + const nonMacStandardInterpreter = 'Non Mac Std Interpreter'; + settings + .setup((s) => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.getInterpreters(typemoq.It.isAny())) + .returns(() => + Promise.resolve([ + { path: pythonPath } as any, + { path: pythonPath } as any, + { path: nonMacStandardInterpreter } as any + ]) + ) + .verifiable(typemoq.Times.once()); + platformService + .setup((i) => i.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + helper + .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) + .returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + helper + .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isValue(nonMacStandardInterpreter))) + .returns(() => false) + .verifiable(typemoq.Times.atLeastOnce()); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve({ envType: EnvironmentType.Unknown } as any); + }) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [ + new InvalidMacPythonInterpreterDiagnostic( + DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, + undefined + ) + ], + 'not the same' + ); + settings.verifyAll(); + interpreterService.verifyAll(); + platformService.verifyAll(); + helper.verifyAll(); + }); + test('Handling no interpreters diagnostic should return select interpreter cmd', async () => { + const diagnostic = new InvalidMacPythonInterpreterDiagnostic( + DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, + undefined + ); + const cmd = ({} as any) as IDiagnosticCommand; + const cmdIgnore = ({} as any) as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand' + }) + ) + ) + .returns(() => cmd) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ + type: 'ignore', + options: DiagnosticScope.Global + }) + ) + ) + .returns(() => cmdIgnore) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([ + { prompt: 'Select Python Interpreter', command: cmd }, + { prompt: 'Do not show again', command: cmdIgnore } + ]); + }); + test('Handling no interpreters diagnostisc should return 3 commands', async () => { + const diagnostic = new InvalidMacPythonInterpreterDiagnostic( + DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, + undefined + ); + const cmdDownload = ({} as any) as IDiagnosticCommand; + const cmdLearn = ({} as any) as IDiagnosticCommand; + const cmdIgnore = ({} as any) as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ + type: 'launch', + options: 'https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites' + }) + ) + ) + .returns(() => cmdLearn) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ + type: 'launch', + options: 'https://www.python.org/downloads' + }) + ) + ) + .returns(() => cmdDownload) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ + type: 'ignore', + options: DiagnosticScope.Global + }) + ) + ) + .returns(() => cmdIgnore) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([ + { prompt: 'Learn more', command: cmdLearn }, + { prompt: 'Download', command: cmdDownload }, + { prompt: 'Do not show again', command: cmdIgnore } + ]); + }); + test('Should not display a message if No Interpreters diagnostic has been ignored', async () => { + const diagnostic = new InvalidMacPythonInterpreterDiagnostic( + DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, + undefined + ); + + filterService + .setup((f) => + f.shouldIgnoreDiagnostic( + typemoq.It.isValue(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic) + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + messageHandler + .setup((f) => f.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + filterService.verifyAll(); + commandFactory.verifyAll(); + }); + }); + + suite('Change Handlers.', () => { + test('Add PythonPath handler is invoked', async () => { + let invoked = false; + diagnosticService = new (class extends InvalidMacPythonInterpreterService { + protected addPythonPathChangedHandler() { + invoked = true; + } + })(createContainer(), interpreterService.object, [], platformService.object, helper.object); + + expect(invoked).to.be.equal(true, 'Not invoked'); + }); + test('Event Handler is registered and invoked', async () => { + let invoked = false; + let callbackHandler!: (e: ConfigurationChangeEvent) => Promise<void>; + const workspaceService = { + onDidChangeConfiguration: (cb: (e: ConfigurationChangeEvent) => Promise<void>) => (callbackHandler = cb) + } as any; + const serviceContainerObject = createContainer(); + + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); + const experiments = typemoq.Mock.ofType<IExperimentsManager>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + diagnosticService = new (class extends InvalidMacPythonInterpreterService { + protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { + invoked = true; + } + })(serviceContainerObject, undefined as any, [], undefined as any, undefined as any); + + await callbackHandler({} as any); + expect(invoked).to.be.equal(true, 'Not invoked'); + }); + test('Event Handler is registered and not invoked', async () => { + let invoked = false; + const workspaceService = { onDidChangeConfiguration: noop } as any; + const serviceContainerObject = createContainer(); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); + const experiments = typemoq.Mock.ofType<IExperimentsManager>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + diagnosticService = new (class extends InvalidMacPythonInterpreterService { + protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { + invoked = true; + } + })(serviceContainerObject, undefined as any, [], undefined as any, undefined as any); + + expect(invoked).to.be.equal(false, 'Not invoked'); + }); + test('Diagnostics are checked with Config change event uri when path changes and event is passed', async () => { + const event = typemoq.Mock.ofType<ConfigurationChangeEvent>(); + const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + const serviceContainerObject = createContainer(); + let diagnoseInvocationCount = 0; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typemoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [{ uri: '' }] as any) + .verifiable(typemoq.Times.once()); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + const experiments = typemoq.Mock.ofType<IExperimentsManager>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { + constructor( + arg1: IServiceContainer, + arg2: IInterpreterService, + arg3: IPlatformService, + arg4: IInterpreterHelper + ) { + super(arg1, arg2, [], arg3, arg4); + this.changeThrottleTimeout = 1; + } + public onDidChangeConfigurationEx = (e: ConfigurationChangeEvent) => super.onDidChangeConfiguration(e); + public diagnose(): Promise<any> { + diagnoseInvocationCount += 1; + return Promise.resolve(); + } + })( + serviceContainerObject, + typemoq.Mock.ofType<IInterpreterService>().object, + typemoq.Mock.ofType<IPlatformService>().object, + typemoq.Mock.ofType<IInterpreterHelper>().object + ); + + event + .setup((e) => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) + .returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + event.verifyAll(); + await sleep(100); + expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); + + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await sleep(100); + expect(diagnoseInvocationCount).to.be.equal(2, 'Not invoked'); + }); + + test('Diagnostics are checked with correct interpreter config uri when path changes and only config uri is passed', async () => { + const configUri = Uri.parse('i'); + const interpreterConfigurationScope = typemoq.Mock.ofType<InterpreterConfigurationScope>(); + interpreterConfigurationScope.setup((i) => i.uri).returns(() => Uri.parse('i')); + const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + const serviceContainerObject = createContainer(); + let diagnoseInvocationCount = 0; + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + const experiments = typemoq.Mock.ofType<IExperimentsManager>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { + constructor( + arg1: IServiceContainer, + arg2: IInterpreterService, + arg3: IPlatformService, + arg4: IInterpreterHelper + ) { + super(arg1, arg2, [], arg3, arg4); + this.changeThrottleTimeout = 1; + } + public onDidChangeConfigurationEx = (e?: ConfigurationChangeEvent, i?: InterpreterConfigurationScope) => + super.onDidChangeConfiguration(e, i); + public diagnose(resource: Resource): Promise<any> { + diagnoseInvocationCount += 1; + assert.deepEqual(resource, configUri); + return Promise.resolve(); + } + })( + serviceContainerObject, + typemoq.Mock.ofType<IInterpreterService>().object, + typemoq.Mock.ofType<IPlatformService>().object, + typemoq.Mock.ofType<IInterpreterHelper>().object + ); + + await diagnosticSvc.onDidChangeConfigurationEx(undefined, interpreterConfigurationScope.object); + await sleep(100); + expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); + + await diagnosticSvc.onDidChangeConfigurationEx(undefined, interpreterConfigurationScope.object); + await sleep(100); + expect(diagnoseInvocationCount).to.be.equal(2, 'Not invoked'); + }); + + test('Diagnostics throws error when none of config uri or config change event uri is passed', async () => { + const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + const serviceContainerObject = createContainer(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + const experiments = typemoq.Mock.ofType<IExperimentsManager>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { + constructor( + arg1: IServiceContainer, + arg2: IInterpreterService, + arg3: IPlatformService, + arg4: IInterpreterHelper + ) { + super(arg1, arg2, [], arg3, arg4); + this.changeThrottleTimeout = 1; + } + public onDidChangeConfigurationEx = (e?: ConfigurationChangeEvent, i?: InterpreterConfigurationScope) => + super.onDidChangeConfiguration(e, i); + })( + serviceContainerObject, + typemoq.Mock.ofType<IInterpreterService>().object, + typemoq.Mock.ofType<IPlatformService>().object, + typemoq.Mock.ofType<IInterpreterHelper>().object + ); + + await expect(diagnosticSvc.onDidChangeConfigurationEx(undefined, undefined)).to.eventually.be.rejectedWith( + Error + ); + }); + + test('Diagnostics are checked and throttled when path changes', async () => { + const event = typemoq.Mock.ofType<ConfigurationChangeEvent>(); + const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + const serviceContainerObject = createContainer(); + let diagnoseInvocationCount = 0; + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typemoq.Times.once()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [{ uri: '' }] as any) + .verifiable(typemoq.Times.once()); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + const experiments = typemoq.Mock.ofType<IExperimentsManager>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { + constructor( + arg1: IServiceContainer, + arg2: IInterpreterService, + arg3: IPlatformService, + arg4: IInterpreterHelper + ) { + super(arg1, arg2, [], arg3, arg4); + this.changeThrottleTimeout = 100; + } + public onDidChangeConfigurationEx = (e: ConfigurationChangeEvent) => super.onDidChangeConfiguration(e); + public diagnose(): Promise<any> { + diagnoseInvocationCount += 1; + return Promise.resolve(); + } + })( + serviceContainerObject, + typemoq.Mock.ofType<IInterpreterService>().object, + typemoq.Mock.ofType<IPlatformService>().object, + typemoq.Mock.ofType<IInterpreterHelper>().object + ); + + event + .setup((e) => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) + .returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await sleep(500); + event.verifyAll(); + expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); + }); + + test('Ensure event Handler is registered correctly if in Deprecate Python path experiment', async () => { + let interpreterPathServiceHandler: Function; + const workspaceService = { onDidChangeConfiguration: noop } as any; + const serviceContainerObject = createContainer(); + + const interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>(); + interpreterPathService + .setup((d) => d.onDidChange(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((cb) => (interpreterPathServiceHandler = cb)) + .returns(() => { + return { dispose: noop }; + }); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IInterpreterPathService))) + .returns(() => interpreterPathService.object); + + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))).returns(() => workspaceService); + const experiments = typemoq.Mock.ofType<IExperimentsManager>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experiments.object); + experiments.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experiments + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + diagnosticService = new (class extends InvalidMacPythonInterpreterService {})( + serviceContainerObject, + undefined as any, + [], + undefined as any, + undefined as any + ); + + expect(interpreterPathServiceHandler!).to.not.equal(undefined, 'Handler not set'); + }); + }); +}); diff --git a/src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts b/src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts new file mode 100644 index 000000000000..fc5a1bb6a35e --- /dev/null +++ b/src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { PowerShellActivationHackDiagnosticsService } from '../../../../client/application/diagnostics/checks/powerShellActivation'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService +} from '../../../../client/application/diagnostics/types'; +import { IApplicationEnvironment, IWorkspaceService } from '../../../../client/common/application/types'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { ICurrentProcess, IPathUtils } from '../../../../client/common/types'; +import { EnvironmentVariables } from '../../../../client/common/variables/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +// tslint:disable:max-func-body-length no-any +suite('Application Diagnostics - PowerShell Activation', () => { + let diagnosticService: IDiagnosticsService; + let platformService: typemoq.IMock<IPlatformService>; + let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let filterService: typemoq.IMock<IDiagnosticFilterService>; + let procEnv: typemoq.IMock<EnvironmentVariables>; + let appEnv: typemoq.IMock<IApplicationEnvironment>; + let commandFactory: typemoq.IMock<IDiagnosticsCommandFactory>; + const pathVariableName = 'Path'; + const pathDelimiter = ';'; + const extensionName = 'Some Extension Name'; + setup(() => { + const serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + platformService = typemoq.Mock.ofType<IPlatformService>(); + platformService.setup((p) => p.pathVariableName).returns(() => pathVariableName); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) + ) + ) + .returns(() => messageHandler.object); + + appEnv = typemoq.Mock.ofType<IApplicationEnvironment>(); + appEnv.setup((a) => a.extensionName).returns(() => extensionName); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IApplicationEnvironment))).returns(() => appEnv.object); + + filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + + commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + + const currentProc = typemoq.Mock.ofType<ICurrentProcess>(); + procEnv = typemoq.Mock.ofType<EnvironmentVariables>(); + currentProc.setup((p) => p.env).returns(() => procEnv.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(ICurrentProcess))).returns(() => currentProc.object); + + const pathUtils = typemoq.Mock.ofType<IPathUtils>(); + pathUtils.setup((p) => p.delimiter).returns(() => pathDelimiter); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + + const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + workspaceService.setup((w) => w.getWorkspaceFolder(typemoq.It.isAny())).returns(() => undefined); + + diagnosticService = new (class extends PowerShellActivationHackDiagnosticsService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })(serviceContainer.object, []); + (diagnosticService as any)._clear(); + }); + + test('Can handle PowerShell diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(true, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Can not handle non-EnvPathVariable diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Must return empty diagnostics', async () => { + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + }); + test('Should display three options in message displayed with 4 commands', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + let options: MessageCommandPrompt | undefined; + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + const alwaysIgnoreCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ + type: 'ignore', + options: DiagnosticScope.Global + }) + ) + ) + .returns(() => alwaysIgnoreCommand.object) + .verifiable(typemoq.Times.once()); + const launchBrowserCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }) + ) + ) + .returns(() => launchBrowserCommand.object) + .verifiable(typemoq.Times.once()); + messageHandler + .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_, opts: MessageCommandPrompt) => (options = opts)) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic.object]); + + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + expect(options!.commandPrompts).to.be.lengthOf(4); + expect(options!.commandPrompts[0].prompt).to.be.equal('Use Command Prompt'); + }); +}); diff --git a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts new file mode 100644 index 000000000000..78b9920014bf --- /dev/null +++ b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -0,0 +1,424 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any max-classes-per-file + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { InvalidLaunchJsonDebuggerDiagnostic } from '../../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; +import { + InvalidPythonInterpreterDiagnostic, + InvalidPythonInterpreterService +} from '../../../../client/application/diagnostics/checks/pythonInterpreter'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../../../client/application/diagnostics/promptHandler'; +import { + IDiagnostic, + IDiagnosticCommand, + IDiagnosticHandlerService, + IDiagnosticsService +} from '../../../../client/application/diagnostics/types'; +import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; +import { noop } from '../../../../client/common/utils/misc'; +import { IInterpreterHelper, IInterpreterService } from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +suite('Application Diagnostics - Checks Python Interpreter', () => { + let diagnosticService: IDiagnosticsService; + let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let commandFactory: typemoq.IMock<IDiagnosticsCommandFactory>; + let settings: typemoq.IMock<IPythonSettings>; + let interpreterService: typemoq.IMock<IInterpreterService>; + let platformService: typemoq.IMock<IPlatformService>; + let helper: typemoq.IMock<IInterpreterHelper>; + const pythonPath = 'My Python Path in Settings'; + let serviceContainer: typemoq.IMock<IServiceContainer>; + function createContainer() { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) + ) + ) + .returns(() => messageHandler.object); + commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + settings = typemoq.Mock.ofType<IPythonSettings>(); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + const configService = typemoq.Mock.ofType<IConfigurationService>(); + configService.setup((c) => c.getSettings(typemoq.It.isAny())).returns(() => settings.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + interpreterService = typemoq.Mock.ofType<IInterpreterService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + platformService = typemoq.Mock.ofType<IPlatformService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + helper = typemoq.Mock.ofType<IInterpreterHelper>(); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); + return serviceContainer.object; + } + suite('Diagnostics', () => { + setup(() => { + diagnosticService = new (class extends InvalidPythonInterpreterService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + protected addPythonPathChangedHandler() { + noop(); + } + })(createContainer(), []); + (diagnosticService as any)._clear(); + }); + + test('Can handle InvalidPythonPathInterpreter diagnostics', async () => { + for (const code of [ + DiagnosticCodes.NoPythonInterpretersDiagnostic, + DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic + ]) { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => code) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); + diagnostic.verifyAll(); + } + }); + test('Can not handle non-InvalidPythonPathInterpreter diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Should return empty diagnostics if installer check is disabled', async () => { + settings + .setup((s) => s.disableInstallationChecks) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + settings.verifyAll(); + }); + test('Should return diagnostics if there are no interpreters after double-checking', async () => { + settings + .setup((s) => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.hasInterpreters) + .returns(() => Promise.resolve(false)) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.getInterpreters(undefined)) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoPythonInterpretersDiagnostic, undefined)], + 'not the same' + ); + settings.verifyAll(); + interpreterService.verifyAll(); + }); + test('Should return empty diagnostics if there are interpreters after double-checking', async () => { + const interpreter: PythonEnvironment = { envType: EnvironmentType.Unknown } as any; + + settings + .setup((s) => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.hasInterpreters) + .returns(() => Promise.resolve(false)) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.getInterpreters(undefined)) + .returns(() => Promise.resolve([interpreter])) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve(interpreter); + }) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + + expect(diagnostics).to.be.deep.equal([], 'not the same'); + settings.verifyAll(); + interpreterService.verifyAll(); + }); + test('Should return invalid diagnostics if there are interpreters but no current interpreter', async () => { + settings + .setup((s) => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.hasInterpreters) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve(undefined); + }) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [ + new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, + undefined + ) + ], + 'not the same' + ); + settings.verifyAll(); + interpreterService.verifyAll(); + }); + test('Should return empty diagnostics if there are interpreters and a current interpreter', async () => { + settings + .setup((s) => s.disableInstallationChecks) + .returns(() => false) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.hasInterpreters) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve({ envType: EnvironmentType.Unknown } as any); + }) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([], 'not the same'); + settings.verifyAll(); + interpreterService.verifyAll(); + }); + test('Handling no interpreters diagnostic should return download link', async () => { + const diagnostic = new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoPythonInterpretersDiagnostic, + undefined + ); + const cmd = ({} as any) as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }) + ) + ) + .returns(() => cmd) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([{ prompt: 'Download', command: cmd }]); + expect(messagePrompt!.onClose).to.not.be.equal(undefined, 'onClose handler should be set.'); + }); + test('Handling no currently selected interpreter diagnostic should show select interpreter message', async () => { + const diagnostic = new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, + undefined + ); + const cmd = ({} as any) as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand' + }) + ) + ) + .returns(() => cmd) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.onClose).be.equal(undefined, 'onClose handler should not be set.'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([ + { prompt: 'Select Python Interpreter', command: cmd } + ]); + }); + test('Handling no interpreters diagnostic should return select interpreter cmd', async () => { + const diagnostic = new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, + undefined + ); + const cmd = ({} as any) as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand' + }) + ) + ) + .returns(() => cmd) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.onClose).be.equal(undefined, 'onClose handler should not be set.'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([ + { prompt: 'Select Python Interpreter', command: cmd } + ]); + }); + test('Handling an empty diagnostic should not show a message nor return a command', async () => { + const diagnostics: IDiagnostic[] = []; + const cmd = ({} as any) as IDiagnosticCommand; + + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand' + }) + ) + ) + .returns(() => cmd) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle(diagnostics); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + }); + test('Handling an unsupported diagnostic code should not show a message nor return a command', async () => { + const diagnostic = new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, + undefined + ); + const cmd = ({} as any) as IDiagnosticCommand; + const diagnosticServiceMock = (typemoq.Mock.ofInstance(diagnosticService) as any) as typemoq.IMock< + InvalidPythonInterpreterService + >; + + diagnosticServiceMock.setup((f) => f.canHandle(typemoq.It.isAny())).returns(() => Promise.resolve(false)); + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand' + }) + ) + ) + .returns(() => cmd) + .verifiable(typemoq.Times.never()); + + await diagnosticServiceMock.object.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + }); + test('Getting command prompts for an unsupported diagnostic code should throw an error', async () => { + const diagnostic = new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined); + const cmd = ({} as any) as IDiagnosticCommand; + + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand' + }) + ) + ) + .returns(() => cmd) + .verifiable(typemoq.Times.never()); + + try { + await diagnosticService.handle([diagnostic]); + } catch (err) { + expect(err.message).to.be.equal( + "Invalid diagnostic for 'InvalidPythonInterpreterService'", + 'Error message is different' + ); + } + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + }); + }); +}); diff --git a/src/test/application/diagnostics/checks/pythonPathDeprecated.unit.test.ts b/src/test/application/diagnostics/checks/pythonPathDeprecated.unit.test.ts new file mode 100644 index 000000000000..7fab9fb2de27 --- /dev/null +++ b/src/test/application/diagnostics/checks/pythonPathDeprecated.unit.test.ts @@ -0,0 +1,394 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any max-classes-per-file + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { ConfigurationTarget, DiagnosticSeverity, Uri, WorkspaceConfiguration } from 'vscode'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + PythonPathDeprecatedDiagnostic, + PythonPathDeprecatedDiagnosticService +} from '../../../../client/application/diagnostics/checks/pythonPathDeprecated'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService +} from '../../../../client/application/diagnostics/types'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { STANDARD_OUTPUT_CHANNEL } from '../../../../client/common/constants'; +import { DeprecatePythonPath } from '../../../../client/common/experiments/groups'; +import { IDisposableRegistry, IExperimentsManager, IOutputChannel, Resource } from '../../../../client/common/types'; +import { Common, Diagnostics } from '../../../../client/common/utils/localize'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Python Path Deprecated', () => { + const resource = Uri.parse('a'); + let diagnosticService: PythonPathDeprecatedDiagnosticService; + let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let commandFactory: typemoq.IMock<IDiagnosticsCommandFactory>; + let workspaceService: typemoq.IMock<IWorkspaceService>; + let filterService: typemoq.IMock<IDiagnosticFilterService>; + let experimentsManager: typemoq.IMock<IExperimentsManager>; + let output: typemoq.IMock<IOutputChannel>; + let serviceContainer: typemoq.IMock<IServiceContainer>; + function createContainer() { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); + output = typemoq.Mock.ofType<IOutputChannel>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IOutputChannel), STANDARD_OUTPUT_CHANNEL)) + .returns(() => output.object); + experimentsManager = typemoq.Mock.ofType<IExperimentsManager>(); + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) + ) + ) + .returns(() => messageHandler.object); + commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experimentsManager.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); + return serviceContainer.object; + } + suite('Diagnostics', () => { + setup(() => { + diagnosticService = new (class extends PythonPathDeprecatedDiagnosticService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })(createContainer(), messageHandler.object, []); + (diagnosticService as any)._clear(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Can handle PythonPathDeprecatedDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.PythonPathDeprecatedDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal( + true, + `Should be able to handle ${DiagnosticCodes.PythonPathDeprecatedDiagnostic}` + ); + diagnostic.verifyAll(); + }); + test('Can not handle non-PythonPathDeprecatedDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Should not display a message if the diagnostic code has been ignored', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + + filterService + .setup((f) => + f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.PythonPathDeprecatedDiagnostic)) + ) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.PythonPathDeprecatedDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + messageHandler + .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic.object]); + + filterService.verifyAll(); + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + }); + test('Python Path Deprecated Diagnostic is handled as expected', async () => { + const diagnostic = new PythonPathDeprecatedDiagnostic('message', resource); + const ignoreCmd = ({ cmd: 'ignoreCmd' } as any) as IDiagnosticCommand; + filterService + .setup((f) => + f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.PythonPathDeprecatedDiagnostic)) + ) + .returns(() => Promise.resolve(false)); + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ type: 'ignore' }) + ) + ) + .returns(() => ignoreCmd) + .verifiable(typemoq.Times.once()); + const _removePythonPathFromWorkspaceSettings = sinon.stub( + PythonPathDeprecatedDiagnosticService.prototype, + '_removePythonPathFromWorkspaceSettings' + ); + _removePythonPathFromWorkspaceSettings.resolves(); + + await diagnosticService.handle([diagnostic]); + + assert(_removePythonPathFromWorkspaceSettings.calledOnceWith(resource)); + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts.length).to.equal(2, 'Incorrect length'); + expect(messagePrompt!.commandPrompts[0].command).not.be.equal(undefined, 'Command not set'); + expect(messagePrompt!.commandPrompts[0].command!.diagnostic).to.be.deep.equal(diagnostic); + expect(messagePrompt!.commandPrompts[0].prompt).to.be.deep.equal(Common.openOutputPanel()); + expect(messagePrompt!.commandPrompts[1]).to.be.deep.equal({ + prompt: Common.doNotShowAgain(), + command: ignoreCmd + }); + + output + .setup((o) => o.show(true)) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + await messagePrompt!.commandPrompts[0].command!.invoke(); + output.verifyAll(); + }); + test('Handling an empty diagnostic should not show a message nor return a command', async () => { + const diagnostics: IDiagnostic[] = []; + + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle(diagnostics); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + }); + test('Handling an unsupported diagnostic code should not show a message nor return a command', async () => { + const diagnostic = new (class SomeRandomDiagnostic extends BaseDiagnostic { + constructor(message: string, uri: Resource) { + super( + 'SomeRandomDiagnostic' as any, + message, + DiagnosticSeverity.Information, + DiagnosticScope.WorkspaceFolder, + uri + ); + } + })('message', undefined); + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + }); + + test('If not in DeprecatePythonPath experiment, empty diagnostics is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const workspaceConfig = typemoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((w) => w.getConfiguration('python', resource)) + .returns(() => workspaceConfig.object) + .verifiable(typemoq.Times.never()); + workspaceConfig + .setup((w) => w.inspect('pythonPath')) + .returns( + () => + ({ + workspaceFolderValue: 'workspaceFolderValue', + workspaceValue: 'workspaceValue' + } as any) + ); + + const diagnostics = await diagnosticService.diagnose(resource); + assert.deepEqual(diagnostics, []); + + workspaceService.verifyAll(); + }); + + test('If a workspace is opened and only workspace value is set, diagnostic with appropriate message is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const workspaceConfig = typemoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.workspaceFile).returns(() => Uri.parse('path/to/workspaceFile')); + workspaceService + .setup((w) => w.getConfiguration('python', resource)) + .returns(() => workspaceConfig.object) + .verifiable(typemoq.Times.once()); + workspaceConfig + .setup((w) => w.inspect('pythonPath')) + .returns( + () => + ({ + workspaceValue: 'workspaceValue' + } as any) + ); + + const diagnostics = await diagnosticService.diagnose(resource); + expect(diagnostics.length).to.equal(1); + expect(diagnostics[0].message).to.equal(Diagnostics.removedPythonPathFromSettings()); + expect(diagnostics[0].resource).to.equal(resource); + + workspaceService.verifyAll(); + }); + + test('If folder is directly opened and workspace folder value is set, diagnostic with appropriate message is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const workspaceConfig = typemoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + workspaceService + .setup((w) => w.getConfiguration('python', resource)) + .returns(() => workspaceConfig.object) + .verifiable(typemoq.Times.once()); + workspaceConfig + .setup((w) => w.inspect('pythonPath')) + .returns( + () => + ({ + workspaceValue: 'workspaceValue', + workspaceFolderValue: 'workspaceFolderValue' + } as any) + ); + + const diagnostics = await diagnosticService.diagnose(resource); + expect(diagnostics.length).to.equal(1); + expect(diagnostics[0].message).to.equal(Diagnostics.removedPythonPathFromSettings()); + expect(diagnostics[0].resource).to.equal(resource); + + workspaceService.verifyAll(); + }); + + test('If a workspace is opened and both workspace folder value & workspace value is set, diagnostic with appropriate message is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const workspaceConfig = typemoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.workspaceFile).returns(() => Uri.parse('path/to/workspaceFile')); + workspaceService + .setup((w) => w.getConfiguration('python', resource)) + .returns(() => workspaceConfig.object) + .verifiable(typemoq.Times.once()); + workspaceConfig + .setup((w) => w.inspect('pythonPath')) + .returns( + () => + ({ + workspaceValue: 'workspaceValue', + workspaceFolderValue: 'workspaceFolderValue' + } as any) + ); + + const diagnostics = await diagnosticService.diagnose(resource); + expect(diagnostics.length).to.equal(1); + expect(diagnostics[0].message).to.equal(Diagnostics.removedPythonPathFromSettings()); + expect(diagnostics[0].resource).to.equal(resource); + + workspaceService.verifyAll(); + }); + + test('Otherwise an empty diagnostic is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const workspaceConfig = typemoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.workspaceFile).returns(() => Uri.parse('path/to/workspaceFile')); + workspaceService + .setup((w) => w.getConfiguration('python', resource)) + .returns(() => workspaceConfig.object) + .verifiable(typemoq.Times.once()); + workspaceConfig.setup((w) => w.inspect('pythonPath')).returns(() => ({} as any)); + + const diagnostics = await diagnosticService.diagnose(resource); + assert.deepEqual(diagnostics, []); + + workspaceService.verifyAll(); + }); + + test('Method _removePythonPathFromWorkspaceSettings() removes `python.pythonPath` setting from Workspace & Workspace Folder', async () => { + const workspaceConfig = typemoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.workspaceFile).returns(() => Uri.parse('path/to/workspaceFile')); + workspaceService.setup((w) => w.getConfiguration('python', resource)).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.update('pythonPath', undefined, ConfigurationTarget.Workspace)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + workspaceConfig + .setup((w) => w.update('pythonPath', undefined, ConfigurationTarget.WorkspaceFolder)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await diagnosticService._removePythonPathFromWorkspaceSettings(resource); + + workspaceConfig.verifyAll(); + }); + }); +}); diff --git a/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts b/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts new file mode 100644 index 000000000000..039086a404a3 --- /dev/null +++ b/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { ApplicationEnvironment } from '../../../../client/common/application/applicationEnvironment'; +import { IApplicationEnvironment, IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentState } from '../../../../client/common/types'; +import { UpdateTestSettingService } from '../../../../client/testing/common/updateTestSettings'; + +// tslint:disable:max-func-body-length no-invalid-this no-any +suite('Application Diagnostics - Check Test Settings', () => { + let diagnosticService: UpdateTestSettingService; + let fs: IFileSystem; + let appEnv: IApplicationEnvironment; + let storage: IPersistentState<string[]>; + let workspace: IWorkspaceService; + const sandbox = sinon.createSandbox(); + setup(() => { + fs = mock(FileSystem); + appEnv = mock(ApplicationEnvironment); + storage = mock(PersistentState); + workspace = mock(WorkspaceService); + const stateFactory = mock(PersistentStateFactory); + + when(stateFactory.createGlobalPersistentState('python.unitTest.Settings', anything())).thenReturn( + instance(storage) + ); + + diagnosticService = new UpdateTestSettingService(instance(fs), instance(appEnv), instance(workspace)); + }); + teardown(() => { + sandbox.restore(); + }); + [Uri.file(__filename), undefined].forEach((resource) => { + const resourceTitle = resource ? '(with a resource)' : '(without a resource)'; + + test(`activate method invokes UpdateTestSettings ${resourceTitle}`, async () => { + const updateTestSettings = sandbox.stub(UpdateTestSettingService.prototype, 'updateTestSettings'); + updateTestSettings.resolves(); + diagnosticService = new UpdateTestSettingService(instance(fs), instance(appEnv), instance(workspace)); + + await diagnosticService.activate(resource); + + assert.ok(updateTestSettings.calledOnce); + }); + + test(`activate method invokes UpdateTestSettings and ignores errors raised by UpdateTestSettings ${resourceTitle}`, async () => { + const updateTestSettings = sandbox.stub(UpdateTestSettingService.prototype, 'updateTestSettings'); + updateTestSettings.rejects(new Error('Kaboom')); + diagnosticService = new UpdateTestSettingService(instance(fs), instance(appEnv), instance(workspace)); + + await diagnosticService.activate(resource); + + assert.ok(updateTestSettings.calledOnce); + }); + + test(`When there are no workspaces, then return just the user settings file ${resourceTitle}`, async () => { + when(workspace.getWorkspaceFolder(anything())).thenReturn(); + when(appEnv.userSettingsFile).thenReturn('user.json'); + + const files = diagnosticService.getSettingsFiles(resource); + + assert.deepEqual(files, ['user.json']); + verify(workspace.getWorkspaceFolder(resource)).once(); + }); + test(`When there are no workspaces & no user file, then return an empty array ${resourceTitle}`, async () => { + when(workspace.getWorkspaceFolder(anything())).thenReturn(); + when(appEnv.userSettingsFile).thenReturn(); + + const files = diagnosticService.getSettingsFiles(resource); + + assert.deepEqual(files, []); + verify(workspace.getWorkspaceFolder(resource)).once(); + }); + test(`When there is a workspace folder, then return the user settings file & workspace file ${resourceTitle}`, async function () { + if (!resource) { + return this.skip(); + } + when(workspace.getWorkspaceFolder(resource)).thenReturn({ name: '1', uri: Uri.file('folder1'), index: 0 }); + when(appEnv.userSettingsFile).thenReturn('user.json'); + + const files = diagnosticService.getSettingsFiles(resource); + + assert.deepEqual(files, ['user.json', path.join(Uri.file('folder1').fsPath, '.vscode', 'settings.json')]); + verify(workspace.getWorkspaceFolder(resource)).once(); + }); + test(`When there is a workspace folder & no user file, then workspace file ${resourceTitle}`, async function () { + if (!resource) { + return this.skip(); + } + when(workspace.getWorkspaceFolder(resource)).thenReturn({ name: '1', uri: Uri.file('folder1'), index: 0 }); + when(appEnv.userSettingsFile).thenReturn(); + + const files = diagnosticService.getSettingsFiles(resource); + + assert.deepEqual(files, [path.join(Uri.file('folder1').fsPath, '.vscode', 'settings.json')]); + verify(workspace.getWorkspaceFolder(resource)).once(); + }); + test(`Return an empty array if there are no files ${resourceTitle}`, async () => { + const getSettingsFiles = sandbox.stub(UpdateTestSettingService.prototype, 'getSettingsFiles'); + getSettingsFiles.returns([]); + diagnosticService = new UpdateTestSettingService(instance(fs), instance(appEnv), instance(workspace)); + + const files = await diagnosticService.getFilesToBeFixed(resource); + + expect(files).to.deep.equal([]); + }); + test(`Filter files based on whether they need to be fixed ${resourceTitle}`, async () => { + const getSettingsFiles = sandbox.stub(UpdateTestSettingService.prototype, 'getSettingsFiles'); + const filterFiles = sandbox.stub(UpdateTestSettingService.prototype, 'doesFileNeedToBeFixed'); + filterFiles.callsFake((file) => Promise.resolve(file === 'file_a' || file === 'file_c')); + getSettingsFiles.returns(['file_a', 'file_b', 'file_c', 'file_d']); + + diagnosticService = new UpdateTestSettingService(instance(fs), instance(appEnv), instance(workspace)); + + const files = await diagnosticService.getFilesToBeFixed(resource); + + expect(files).to.deep.equal(['file_a', 'file_c']); + }); + }); + [ + { + testTitle: 'Should fix file if contents contains python.unitTest.', + expectedValue: true, + contents: '{"python.pythonPath":"1234", "python.unitTest.unitTestArgs":[]}' + }, + { + testTitle: 'Should fix file if contents contains python.pyTest.', + expectedValue: true, + contents: '{"python.pythonPath":"1234", "python.pyTestArgs":[]}' + }, + { + testTitle: 'Should fix file if contents contains python.pyTest. & python.unitTest.', + expectedValue: true, + contents: '{"python.pythonPath":"1234", "python.testing.pyTestArgs":[], "python.unitTest.unitTestArgs":[]}' + }, + { + testTitle: 'Should not fix file if contents does not contain python.unitTest. and python.pyTest', + expectedValue: false, + contents: '{"python.pythonPath":"1234", "python.unittest.unitTestArgs":[]}' + } + ].forEach((item) => { + test(item.testTitle, async () => { + when(fs.readFile(__filename)).thenResolve(item.contents); + + const needsToBeFixed = await diagnosticService.doesFileNeedToBeFixed(__filename); + + expect(needsToBeFixed).to.equal(item.expectedValue); + verify(fs.readFile(__filename)).once(); + }); + }); + test("File should not be fixed if there's an error in reading the file", async () => { + when(fs.readFile(__filename)).thenReject(new Error('Kaboom')); + + const needsToBeFixed = await diagnosticService.doesFileNeedToBeFixed(__filename); + + assert.ok(!needsToBeFixed); + verify(fs.readFile(__filename)).once(); + }); + test('Verify `python.jediEnabled` is found in user settings', async () => { + when(fs.readFile(__filename)).thenResolve('"python.jediEnabled": false'); + const needsToBeFixed = await diagnosticService.doesFileNeedToBeFixed(__filename); + assert.ok(needsToBeFixed); + verify(fs.readFile(__filename)).once(); + }); + + [ + { + testTitle: 'Should replace python.unitTest.', + contents: '{"python.pythonPath":"1234", "python.unitTest.unitTestArgs":[]}', + expectedContents: '{"python.pythonPath":"1234", "python.testing.unitTestArgs":[]}' + }, + { + testTitle: 'Should replace python.unitTest.pyTest.', + contents: + '{"python.pythonPath":"1234", "python.unitTest.pyTestArgs":[], "python.unitTest.pyTestArgs":[], "python.unitTest.pyTestPath":[]}', + expectedContents: + '{"python.pythonPath":"1234", "python.testing.pytestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestPath":[]}' + }, + { + testTitle: 'Should replace python.testing.pyTest.', + contents: + '{"python.pythonPath":"1234", "python.testing.pyTestArgs":[], "python.testing.pyTestArgs":[], "python.testing.pyTestPath":[]}', + expectedContents: + '{"python.pythonPath":"1234", "python.testing.pytestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestPath":[]}' + }, + { + testTitle: 'Should not make any changes to the file', + contents: + '{"python.pythonPath":"1234", "python.unittest.unitTestArgs":[], "python.unitTest.pytestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestPath":[]}', + expectedContents: + '{"python.pythonPath":"1234", "python.unittest.unitTestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestPath":[]}' + } + ].forEach((item) => { + test(item.testTitle, async () => { + when(fs.readFile(__filename)).thenResolve(item.contents); + when(fs.writeFile(__filename, anything())).thenResolve(); + + const actualContent = await diagnosticService.fixSettingInFile(__filename, false); + + verify(fs.readFile(__filename)).once(); + verify(fs.writeFile(__filename, anyString())).once(); + expect(actualContent).to.be.equal(item.expectedContents); + }); + }); + + [ + { + testTitle: 'No jediEnabled setting', + contents: '{}', + expectedContent: '{}' + }, + { + testTitle: 'jediEnabled setting in comment', + contents: '{\n // "python.jediEnabled": true\n }', + expectedContent: '{\n // "python.jediEnabled": true\n }' + }, + { + testTitle: 'jediEnabled: true, no languageServer setting', + contents: '{ "python.jediEnabled": true }', + expectedContent: '{ "python.jediEnabled": true, "python.languageServer": "Jedi"}' + }, + { + testTitle: 'jediEnabled: true, languageServer setting present', + contents: '{ "python.jediEnabled": true }', + expectedContent: '{ "python.jediEnabled": true, "python.languageServer": "Jedi"}' + }, + { + testTitle: 'jediEnabled: false, no languageServer setting', + contents: '{ "python.jediEnabled": false }', + expectedContent: '{ "python.jediEnabled": false, "python.languageServer": "Microsoft"}' + }, + { + testTitle: 'jediEnabled: false, languageServer is Microsoft', + contents: '{ "python.jediEnabled": false, "python.languageServer": "Microsoft" }', + expectedContent: '{ "python.jediEnabled": false, "python.languageServer": "Microsoft"}' + }, + { + testTitle: 'jediEnabled: false, languageServer is None', + contents: '{ "python.jediEnabled": false, "python.languageServer": "None" }', + expectedContent: '{ "python.jediEnabled": false, "python.languageServer": "None"}' + }, + { + testTitle: 'jediEnabled: false, languageServer is Jedi', + contents: '{ "python.jediEnabled": false, "python.languageServer": "Jedi" }', + expectedContent: '{ "python.jediEnabled": false, "python.languageServer": "Jedi"}' + }, + { + testTitle: 'jediEnabled not present, languageServer is Microsoft', + contents: '{ "python.languageServer": "Microsoft" }', + expectedContent: '{ "python.languageServer": "Microsoft" }' + } + ].forEach((item) => { + test(item.testTitle, async () => { + when(fs.readFile(__filename)).thenResolve(item.contents); + + const actualContent = await diagnosticService.fixSettingInFile(__filename); + + expect(nows(actualContent)).to.equal(nows(item.expectedContent)); + verify(fs.readFile(__filename)).once(); + }); + }); + + function nows(s: string): string { + return s.replace(/\s*/g, ''); + } +}); diff --git a/src/test/application/diagnostics/checks/upgradeCodeRunner.unit.test.ts b/src/test/application/diagnostics/checks/upgradeCodeRunner.unit.test.ts new file mode 100644 index 000000000000..e0f220f41080 --- /dev/null +++ b/src/test/application/diagnostics/checks/upgradeCodeRunner.unit.test.ts @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any max-classes-per-file + +import { assert, expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { DiagnosticSeverity, Extension, Uri, WorkspaceConfiguration } from 'vscode'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + UpgradeCodeRunnerDiagnostic, + UpgradeCodeRunnerDiagnosticService +} from '../../../../client/application/diagnostics/checks/upgradeCodeRunner'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService +} from '../../../../client/application/diagnostics/types'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { CODE_RUNNER_EXTENSION_ID } from '../../../../client/common/constants'; +import { DeprecatePythonPath } from '../../../../client/common/experiments/groups'; +import { IDisposableRegistry, IExperimentsManager, IExtensions, Resource } from '../../../../client/common/types'; +import { Common, Diagnostics } from '../../../../client/common/utils/localize'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Upgrade Code Runner', () => { + const resource = Uri.parse('a'); + let diagnosticService: UpgradeCodeRunnerDiagnosticService; + let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let commandFactory: typemoq.IMock<IDiagnosticsCommandFactory>; + let workspaceService: typemoq.IMock<IWorkspaceService>; + let filterService: typemoq.IMock<IDiagnosticFilterService>; + let serviceContainer: typemoq.IMock<IServiceContainer>; + let experimentsManager: typemoq.IMock<IExperimentsManager>; + let extensions: typemoq.IMock<IExtensions>; + function createContainer() { + extensions = typemoq.Mock.ofType<IExtensions>(); + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + experimentsManager = typemoq.Mock.ofType<IExperimentsManager>(); + filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) + ) + ) + .returns(() => messageHandler.object); + commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experimentsManager.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); + return serviceContainer.object; + } + suite('Diagnostics', () => { + setup(() => { + diagnosticService = new (class extends UpgradeCodeRunnerDiagnosticService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })(createContainer(), messageHandler.object, [], extensions.object); + (diagnosticService as any)._clear(); + }); + + test('Can handle UpgradeCodeRunnerDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.UpgradeCodeRunnerDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal( + true, + `Should be able to handle ${DiagnosticCodes.UpgradeCodeRunnerDiagnostic}` + ); + diagnostic.verifyAll(); + }); + + test('Can not handle non-UpgradeCodeRunnerDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + + test('Should not display a message if the diagnostic code has been ignored', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + + filterService + .setup((f) => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.UpgradeCodeRunnerDiagnostic))) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.UpgradeCodeRunnerDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + messageHandler + .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic.object]); + + filterService.verifyAll(); + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + }); + + test('UpgradeCodeRunnerDiagnostic is handled as expected', async () => { + const diagnostic = new UpgradeCodeRunnerDiagnostic('message', resource); + const ignoreCmd = ({ cmd: 'ignoreCmd' } as any) as IDiagnosticCommand; + filterService + .setup((f) => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.UpgradeCodeRunnerDiagnostic))) + .returns(() => Promise.resolve(false)); + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ type: 'ignore' }) + ) + ) + .returns(() => ignoreCmd) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts.length).to.equal(1, 'Incorrect length'); + expect(messagePrompt!.commandPrompts[0]).to.be.deep.equal({ + prompt: Common.doNotShowAgain(), + command: ignoreCmd + }); + }); + + test('Handling an empty diagnostic should not show a message nor return a command', async () => { + const diagnostics: IDiagnostic[] = []; + + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle(diagnostics); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + }); + + test('Handling an unsupported diagnostic code should not show a message nor return a command', async () => { + const diagnostic = new (class SomeRandomDiagnostic extends BaseDiagnostic { + constructor(message: string, uri: Resource) { + super( + 'SomeRandomDiagnostic' as any, + message, + DiagnosticSeverity.Information, + DiagnosticScope.WorkspaceFolder, + uri + ); + } + })('message', undefined); + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + }); + + test('If a diagnostic has already been returned, empty diagnostics is returned', async () => { + diagnosticService._diagnosticReturned = false; + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + }); + + test('If not in DeprecatePythonPath experiment, empty diagnostics is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + }); + + test('If Code Runner extension is not installed, empty diagnostics is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + extensions.setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)).returns(() => undefined); + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + }); + + test('If Code Runner extension is installed but the appropriate feature flag is set in package.json, empty diagnostics is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const extension = typemoq.Mock.ofType<Extension<any>>(); + extensions.setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)).returns(() => extension.object); + extension + .setup((e) => e.packageJSON) + .returns(() => ({ + featureFlags: { + usingNewPythonInterpreterPathApiV2: true + } + })); + workspaceService + .setup((w) => w.getConfiguration('code-runner', resource)) + .verifiable(typemoq.Times.never()); + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + workspaceService.verifyAll(); + }); + + test('If old version of Code Runner extension is installed but setting `code-runner.executorMap.python` is not set, empty diagnostics is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const workspaceConfig = typemoq.Mock.ofType<WorkspaceConfiguration>(); + const extension = typemoq.Mock.ofType<Extension<any>>(); + extensions.setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)).returns(() => extension.object); + extension.setup((e) => e.packageJSON).returns(() => undefined); + workspaceService + .setup((w) => w.getConfiguration('code-runner', resource)) + .returns(() => workspaceConfig.object) + .verifiable(typemoq.Times.once()); + workspaceConfig.setup((w) => w.get<string>('executorMap.python')).returns(() => undefined); + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + workspaceService.verifyAll(); + }); + + test('If old version of Code Runner extension is installed but $pythonPath is not being used, empty diagnostics is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const workspaceConfig = typemoq.Mock.ofType<WorkspaceConfiguration>(); + const extension = typemoq.Mock.ofType<Extension<any>>(); + extensions.setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)).returns(() => extension.object); + extension.setup((e) => e.packageJSON).returns(() => undefined); + workspaceService + .setup((w) => w.getConfiguration('code-runner', resource)) + .returns(() => workspaceConfig.object) + .verifiable(typemoq.Times.once()); + workspaceConfig.setup((w) => w.get<string>('executorMap.python')).returns(() => 'Random string'); + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + workspaceService.verifyAll(); + }); + + test('If old version of Code Runner extension is installed and $pythonPath is being used, diagnostic with appropriate message is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const workspaceConfig = typemoq.Mock.ofType<WorkspaceConfiguration>(); + const extension = typemoq.Mock.ofType<Extension<any>>(); + extensions.setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)).returns(() => extension.object); + extension.setup((e) => e.packageJSON).returns(() => undefined); + workspaceService + .setup((w) => w.getConfiguration('code-runner', resource)) + .returns(() => workspaceConfig.object) + .verifiable(typemoq.Times.once()); + workspaceConfig + .setup((w) => w.get<string>('executorMap.python')) + .returns(() => 'This string contains $pythonPath'); + + const diagnostics = await diagnosticService.diagnose(resource); + + expect(diagnostics.length).to.equal(1); + expect(diagnostics[0].message).to.equal(Diagnostics.upgradeCodeRunner()); + expect(diagnostics[0].resource).to.equal(resource); + expect(diagnosticService._diagnosticReturned).to.equal(true, ''); + + workspaceService.verifyAll(); + }); + }); +}); diff --git a/src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts b/src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts new file mode 100644 index 000000000000..56f9e51e0051 --- /dev/null +++ b/src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { ExecuteVSCCommand } from '../../../../client/application/diagnostics/commands/execVSCCommand'; +import { DiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/factory'; +import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { IDiagnostic } from '../../../../client/application/diagnostics/types'; +import { ICommandManager } from '../../../../client/common/application/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Exec VSC Commands', () => { + let commandFactory: IDiagnosticsCommandFactory; + let commandManager: typemoq.IMock<ICommandManager>; + setup(() => { + const serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + commandManager = typemoq.Mock.ofType<ICommandManager>(); + serviceContainer + .setup((svc) => svc.get<ICommandManager>(typemoq.It.isValue(ICommandManager), typemoq.It.isAny())) + .returns(() => commandManager.object); + commandFactory = new DiagnosticsCommandFactory(serviceContainer.object); + }); + + test('Test creation of VSC Command', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + + const command = commandFactory.createCommand(diagnostic.object, { + type: 'executeVSCCommand', + options: 'editor.action.formatDocument' + }); + expect(command).to.be.instanceOf(ExecuteVSCCommand); + }); + + test('Test execution of VSC Command', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + commandManager + .setup((cmd) => cmd.executeCommand('editor.action.formatDocument')) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.once()); + + const command = commandFactory.createCommand(diagnostic.object, { + type: 'executeVSCCommand', + options: 'editor.action.formatDocument' + }); + await command.invoke(); + + expect(command).to.be.instanceOf(ExecuteVSCCommand); + commandManager.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/commands/factory.unit.test.ts b/src/test/application/diagnostics/commands/factory.unit.test.ts new file mode 100644 index 000000000000..a8e534408ad6 --- /dev/null +++ b/src/test/application/diagnostics/commands/factory.unit.test.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { DiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/factory'; +import { IgnoreDiagnosticCommand } from '../../../../client/application/diagnostics/commands/ignore'; +import { LaunchBrowserCommand } from '../../../../client/application/diagnostics/commands/launchBrowser'; +import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticScope, IDiagnostic } from '../../../../client/application/diagnostics/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Commands Factory', () => { + let commandFactory: IDiagnosticsCommandFactory; + setup(() => { + const serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + commandFactory = new DiagnosticsCommandFactory(serviceContainer.object); + }); + + test('Test creation of Ignore Command', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + + const command = commandFactory.createCommand(diagnostic.object, { + type: 'ignore', + options: DiagnosticScope.Global + }); + expect(command).to.be.instanceOf(IgnoreDiagnosticCommand); + }); + + test('Test creation of Launch Browser Command', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + + const command = commandFactory.createCommand(diagnostic.object, { type: 'launch', options: 'x' }); + expect(command).to.be.instanceOf(LaunchBrowserCommand); + }); +}); diff --git a/src/test/application/diagnostics/commands/ignore.unit.test.ts b/src/test/application/diagnostics/commands/ignore.unit.test.ts new file mode 100644 index 000000000000..65eb73c16d12 --- /dev/null +++ b/src/test/application/diagnostics/commands/ignore.unit.test.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as typemoq from 'typemoq'; +import { IgnoreDiagnosticCommand } from '../../../../client/application/diagnostics/commands/ignore'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService +} from '../../../../client/application/diagnostics/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +// tslint:disable:no-any + +suite('Application Diagnostics - Commands Ignore', () => { + let ignoreCommand: IDiagnosticCommand; + let serviceContainer: typemoq.IMock<IServiceContainer>; + let diagnostic: typemoq.IMock<IDiagnostic>; + setup(() => { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + + diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + ignoreCommand = new IgnoreDiagnosticCommand(diagnostic.object, serviceContainer.object, DiagnosticScope.Global); + }); + + test('Invoking Command should invoke the filter Service', async () => { + const filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object) + .verifiable(typemoq.Times.once()); + diagnostic + .setup((d) => d.code) + .returns(() => 'xyz' as any) + .verifiable(typemoq.Times.once()); + filterService + .setup((s) => s.ignoreDiagnostic(typemoq.It.isValue('xyz'), typemoq.It.isValue(DiagnosticScope.Global))) + .verifiable(typemoq.Times.once()); + + await ignoreCommand.invoke(); + serviceContainer.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/commands/launchBrowser.unit.test.ts b/src/test/application/diagnostics/commands/launchBrowser.unit.test.ts new file mode 100644 index 000000000000..5b85621971fc --- /dev/null +++ b/src/test/application/diagnostics/commands/launchBrowser.unit.test.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as typemoq from 'typemoq'; +import { LaunchBrowserCommand } from '../../../../client/application/diagnostics/commands/launchBrowser'; +import { IDiagnostic, IDiagnosticCommand } from '../../../../client/application/diagnostics/types'; +import { IBrowserService } from '../../../../client/common/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Commands Launch Browser', () => { + let cmd: IDiagnosticCommand; + let serviceContainer: typemoq.IMock<IServiceContainer>; + let diagnostic: typemoq.IMock<IDiagnostic>; + const url = 'xyz://abc'; + setup(() => { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + cmd = new LaunchBrowserCommand(diagnostic.object, serviceContainer.object, url); + }); + + test('Invoking Command should launch the browser', async () => { + const browser = typemoq.Mock.ofType<IBrowserService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IBrowserService))) + .returns(() => browser.object) + .verifiable(typemoq.Times.once()); + browser.setup((s) => s.launch(typemoq.It.isValue(url))).verifiable(typemoq.Times.once()); + + await cmd.invoke(); + serviceContainer.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/filter.unit.test.ts b/src/test/application/diagnostics/filter.unit.test.ts new file mode 100644 index 000000000000..fbd23e7390da --- /dev/null +++ b/src/test/application/diagnostics/filter.unit.test.ts @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { DiagnosticFilterService, FilterKeys } from '../../../client/application/diagnostics/filter'; +import { DiagnosticScope, IDiagnosticFilterService } from '../../../client/application/diagnostics/types'; +import { IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Application Diagnostics - Filter', () => { + let globalState: typemoq.IMock<IPersistentState<string[]>>; + let workspaceState: typemoq.IMock<IPersistentState<string[]>>; + + [ + { name: 'Global', scope: DiagnosticScope.Global, state: () => globalState, otherState: () => workspaceState }, + { + name: 'Workspace', + scope: DiagnosticScope.WorkspaceFolder, + state: () => workspaceState, + otherState: () => globalState + } + ].forEach((item) => { + let serviceContainer: typemoq.IMock<IServiceContainer>; + let filterService: IDiagnosticFilterService; + + setup(() => { + globalState = typemoq.Mock.ofType<IPersistentState<string[]>>(); + workspaceState = typemoq.Mock.ofType<IPersistentState<string[]>>(); + + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + const stateFactory = typemoq.Mock.ofType<IPersistentStateFactory>(); + + stateFactory + .setup((f) => + f.createGlobalPersistentState<string[]>( + typemoq.It.isValue(FilterKeys.GlobalDiagnosticFilter), + typemoq.It.isValue([]) + ) + ) + .returns(() => globalState.object); + stateFactory + .setup((f) => + f.createWorkspacePersistentState<string[]>( + typemoq.It.isValue(FilterKeys.WorkspaceDiagnosticFilter), + typemoq.It.isValue([]) + ) + ) + .returns(() => workspaceState.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IPersistentStateFactory))) + .returns(() => stateFactory.object); + + filterService = new DiagnosticFilterService(serviceContainer.object); + }); + + test(`ignoreDiagnostic must save codes in ${item.name} Persistent State`, async () => { + const code = 'xyz'; + item.state() + .setup((g) => g.value) + .returns(() => []) + .verifiable(typemoq.Times.once()); + item.state() + .setup((g) => g.updateValue(typemoq.It.isValue([code]))) + .verifiable(typemoq.Times.once()); + + item.otherState() + .setup((g) => g.value) + .verifiable(typemoq.Times.never()); + item.otherState() + .setup((g) => g.updateValue(typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await filterService.ignoreDiagnostic(code, item.scope); + + item.state().verifyAll(); + }); + test("shouldIgnoreDiagnostic should return 'false' when code does not exist in any State", async () => { + const code = 'xyz'; + item.state() + .setup((g) => g.value) + .returns(() => []) + .verifiable(typemoq.Times.once()); + item.otherState() + .setup((g) => g.value) + .returns(() => []) + .verifiable(typemoq.Times.once()); + + const ignore = await filterService.shouldIgnoreDiagnostic(code); + + expect(ignore).to.be.equal(false, 'Incorrect value'); + item.state().verifyAll(); + }); + test(`shouldIgnoreDiagnostic should return \'true\' when code exist in ${item.name} State`, async () => { + const code = 'xyz'; + item.state() + .setup((g) => g.value) + .returns(() => ['a', 'b', 'c', code]) + .verifiable(typemoq.Times.once()); + item.otherState() + .setup((g) => g.value) + .returns(() => []) + .verifiable(typemoq.Times.once()); + + const ignore = await filterService.shouldIgnoreDiagnostic(code); + + expect(ignore).to.be.equal(true, 'Incorrect value'); + item.state().verifyAll(); + }); + + test("shouldIgnoreDiagnostic should return 'true' when code exist in any State", async () => { + const code = 'xyz'; + item.state() + .setup((g) => g.value) + .returns(() => []) + .verifiable(typemoq.Times.atLeast(0)); + item.otherState() + .setup((g) => g.value) + .returns(() => ['a', 'b', 'c', code]) + .verifiable(typemoq.Times.atLeast(0)); + + const ignore = await filterService.shouldIgnoreDiagnostic(code); + + expect(ignore).to.be.equal(true, 'Incorrect value'); + item.state().verifyAll(); + }); + + test(`ignoreDiagnostic must append codes in ${item.name} Persistent State`, async () => { + const code = 'xyz'; + const currentState = ['a', 'b', 'c']; + item.state() + .setup((g) => g.value) + .returns(() => currentState) + .verifiable(typemoq.Times.atLeastOnce()); + item.state() + .setup((g) => g.updateValue(typemoq.It.isAny())) + .callback((value) => { + expect(value).to.deep.equal(currentState.concat([code])); + }) + .verifiable(typemoq.Times.atLeastOnce()); + + item.otherState() + .setup((g) => g.value) + .verifiable(typemoq.Times.never()); + item.otherState() + .setup((g) => g.updateValue(typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await filterService.ignoreDiagnostic(code, item.scope); + + item.state().verifyAll(); + }); + }); +}); diff --git a/src/test/application/diagnostics/promptHandler.unit.test.ts b/src/test/application/diagnostics/promptHandler.unit.test.ts new file mode 100644 index 000000000000..0f9b33017a73 --- /dev/null +++ b/src/test/application/diagnostics/promptHandler.unit.test.ts @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:insecure-random max-func-body-length no-any + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { DiagnosticSeverity } from 'vscode'; +import { + DiagnosticCommandPromptHandlerService, + MessageCommandPrompt +} from '../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticHandlerService +} from '../../../client/application/diagnostics/types'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Application Diagnostics - PromptHandler', () => { + let serviceContainer: typemoq.IMock<IServiceContainer>; + let appShell: typemoq.IMock<IApplicationShell>; + let promptHandler: IDiagnosticHandlerService<MessageCommandPrompt>; + + setup(() => { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + appShell = typemoq.Mock.ofType<IApplicationShell>(); + + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + + promptHandler = new DiagnosticCommandPromptHandlerService(serviceContainer.object); + }); + + getNamesAndValues<DiagnosticSeverity>(DiagnosticSeverity).forEach((severity) => { + test(`Handling a diagnositic of severity '${severity.name}' should display a message without any buttons`, async () => { + const diagnostic: IDiagnostic = { + code: '1' as any, + message: 'one', + scope: DiagnosticScope.Global, + severity: severity.value, + resource: undefined, + invokeHandler: 'default' + }; + switch (severity.value) { + case DiagnosticSeverity.Error: { + appShell + .setup((a) => a.showErrorMessage(typemoq.It.isValue(diagnostic.message))) + .verifiable(typemoq.Times.once()); + break; + } + case DiagnosticSeverity.Warning: { + appShell + .setup((a) => a.showWarningMessage(typemoq.It.isValue(diagnostic.message))) + .verifiable(typemoq.Times.once()); + break; + } + default: { + appShell + .setup((a) => a.showInformationMessage(typemoq.It.isValue(diagnostic.message))) + .verifiable(typemoq.Times.once()); + break; + } + } + + await promptHandler.handle(diagnostic); + appShell.verifyAll(); + }); + test(`Handling a diagnositic of severity '${severity.name}' should invoke the onClose handler`, async () => { + const diagnostic: IDiagnostic = { + code: '1' as any, + message: 'one', + scope: DiagnosticScope.Global, + severity: severity.value, + resource: undefined, + invokeHandler: 'default' + }; + let onCloseHandlerInvoked = false; + const options: MessageCommandPrompt = { + commandPrompts: [{ prompt: 'Yes' }, { prompt: 'No' }], + message: 'Custom Message', + onClose: () => { + onCloseHandlerInvoked = true; + } + }; + + switch (severity.value) { + case DiagnosticSeverity.Error: { + appShell + .setup((a) => + a.showErrorMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No') + ) + ) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + case DiagnosticSeverity.Warning: { + appShell + .setup((a) => + a.showWarningMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No') + ) + ) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + default: { + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No') + ) + ) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + } + + await promptHandler.handle(diagnostic, options); + appShell.verifyAll(); + expect(onCloseHandlerInvoked).to.equal(true, 'onClose handler should be called.'); + }); + test(`Handling a diagnositic of severity '${severity.name}' should display a custom message with buttons`, async () => { + const diagnostic: IDiagnostic = { + code: '1' as any, + message: 'one', + scope: DiagnosticScope.Global, + severity: severity.value, + resource: undefined, + invokeHandler: 'default' + }; + const options: MessageCommandPrompt = { + commandPrompts: [{ prompt: 'Yes' }, { prompt: 'No' }], + message: 'Custom Message' + }; + + switch (severity.value) { + case DiagnosticSeverity.Error: { + appShell + .setup((a) => + a.showErrorMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No') + ) + ) + .verifiable(typemoq.Times.once()); + break; + } + case DiagnosticSeverity.Warning: { + appShell + .setup((a) => + a.showWarningMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No') + ) + ) + .verifiable(typemoq.Times.once()); + break; + } + default: { + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No') + ) + ) + .verifiable(typemoq.Times.once()); + break; + } + } + + await promptHandler.handle(diagnostic, options); + appShell.verifyAll(); + }); + test(`Handling a diagnositic of severity '${severity.name}' should display a custom message with buttons and invoke selected command`, async () => { + const diagnostic: IDiagnostic = { + code: '1' as any, + message: 'one', + scope: DiagnosticScope.Global, + severity: severity.value, + resource: undefined, + invokeHandler: 'default' + }; + const command = typemoq.Mock.ofType<IDiagnosticCommand>(); + const options: MessageCommandPrompt = { + commandPrompts: [ + { prompt: 'Yes', command: command.object }, + { prompt: 'No', command: command.object } + ], + message: 'Custom Message' + }; + command.setup((c) => c.invoke()).verifiable(typemoq.Times.once()); + + switch (severity.value) { + case DiagnosticSeverity.Error: { + appShell + .setup((a) => + a.showErrorMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No') + ) + ) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + case DiagnosticSeverity.Warning: { + appShell + .setup((a) => + a.showWarningMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No') + ) + ) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + default: { + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No') + ) + ) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + } + + await promptHandler.handle(diagnostic, options); + appShell.verifyAll(); + command.verifyAll(); + }); + }); +}); diff --git a/src/test/application/diagnostics/serviceRegistry.unit.test.ts b/src/test/application/diagnostics/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..f174d1591ca1 --- /dev/null +++ b/src/test/application/diagnostics/serviceRegistry.unit.test.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionSingleActivationService, LanguageServerType } from '../../../client/activation/types'; +import { ApplicationDiagnostics } from '../../../client/application/diagnostics/applicationDiagnostics'; +import { + EnvironmentPathVariableDiagnosticsService, + EnvironmentPathVariableDiagnosticsServiceId +} from '../../../client/application/diagnostics/checks/envPathVariable'; +import { + InvalidLaunchJsonDebuggerService, + InvalidLaunchJsonDebuggerServiceId +} from '../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; +import { + InvalidPythonPathInDebuggerService, + InvalidPythonPathInDebuggerServiceId +} from '../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; +import { + LSNotSupportedDiagnosticService, + LSNotSupportedDiagnosticServiceId +} from '../../../client/application/diagnostics/checks/lsNotSupported'; +import { + InvalidMacPythonInterpreterService, + InvalidMacPythonInterpreterServiceId +} from '../../../client/application/diagnostics/checks/macPythonInterpreter'; +import { + PowerShellActivationHackDiagnosticsService, + PowerShellActivationHackDiagnosticsServiceId +} from '../../../client/application/diagnostics/checks/powerShellActivation'; +import { + InvalidPythonInterpreterService, + InvalidPythonInterpreterServiceId +} from '../../../client/application/diagnostics/checks/pythonInterpreter'; +import { + PythonPathDeprecatedDiagnosticService, + PythonPathDeprecatedDiagnosticServiceId +} from '../../../client/application/diagnostics/checks/pythonPathDeprecated'; +import { + UpgradeCodeRunnerDiagnosticService, + UpgradeCodeRunnerDiagnosticServiceId +} from '../../../client/application/diagnostics/checks/upgradeCodeRunner'; +import { DiagnosticsCommandFactory } from '../../../client/application/diagnostics/commands/factory'; +import { IDiagnosticsCommandFactory } from '../../../client/application/diagnostics/commands/types'; +import { DiagnosticFilterService } from '../../../client/application/diagnostics/filter'; +import { + DiagnosticCommandPromptHandlerService, + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../../client/application/diagnostics/promptHandler'; +import { registerTypes } from '../../../client/application/diagnostics/serviceRegistry'; +import { + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService +} from '../../../client/application/diagnostics/types'; +import { JoinMailingListPrompt } from '../../../client/application/misc/joinMailingListPrompt'; +import { IApplicationDiagnostics } from '../../../client/application/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Application Diagnostics - Register classes in IOC Container', () => { + let serviceManager: IServiceManager; + setup(() => { + serviceManager = mock(ServiceManager); + }); + test('Register Classes', () => { + registerTypes(instance(serviceManager), LanguageServerType.Microsoft); + + verify( + serviceManager.addSingleton<IDiagnosticFilterService>(IDiagnosticFilterService, DiagnosticFilterService) + ); + verify( + serviceManager.addSingleton<IDiagnosticHandlerService<MessageCommandPrompt>>( + IDiagnosticHandlerService, + DiagnosticCommandPromptHandlerService, + DiagnosticCommandPromptHandlerServiceId + ) + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + EnvironmentPathVariableDiagnosticsService, + EnvironmentPathVariableDiagnosticsServiceId + ) + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + InvalidLaunchJsonDebuggerService, + InvalidLaunchJsonDebuggerServiceId + ) + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + UpgradeCodeRunnerDiagnosticService, + UpgradeCodeRunnerDiagnosticServiceId + ) + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + InvalidPythonInterpreterService, + InvalidPythonInterpreterServiceId + ) + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + InvalidPythonPathInDebuggerService, + InvalidPythonPathInDebuggerServiceId + ) + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + LSNotSupportedDiagnosticService, + LSNotSupportedDiagnosticServiceId + ) + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + PowerShellActivationHackDiagnosticsService, + PowerShellActivationHackDiagnosticsServiceId + ) + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + InvalidMacPythonInterpreterService, + InvalidMacPythonInterpreterServiceId + ) + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + PythonPathDeprecatedDiagnosticService, + PythonPathDeprecatedDiagnosticServiceId + ) + ); + verify( + serviceManager.addSingleton<IDiagnosticsCommandFactory>( + IDiagnosticsCommandFactory, + DiagnosticsCommandFactory + ) + ); + verify(serviceManager.addSingleton<IApplicationDiagnostics>(IApplicationDiagnostics, ApplicationDiagnostics)); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + JoinMailingListPrompt + ) + ); + }); +}); diff --git a/src/test/application/diagnostics/sourceMapSupportService.unit.test.ts b/src/test/application/diagnostics/sourceMapSupportService.unit.test.ts new file mode 100644 index 000000000000..4556b63d38e7 --- /dev/null +++ b/src/test/application/diagnostics/sourceMapSupportService.unit.test.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { anyFunction, anything, instance, mock, verify, when } from 'ts-mockito'; +import { ConfigurationTarget } from 'vscode'; +import { SourceMapSupportService } from '../../../client/application/diagnostics/surceMapSupportService'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { Commands } from '../../../client/common/constants'; +import { Diagnostics } from '../../../client/common/utils/localize'; + +suite('Diagnostisc - Source Maps', () => { + test('Command is registered', async () => { + const commandManager = mock(CommandManager); + const service = new SourceMapSupportService(instance(commandManager), [], undefined as any, undefined as any); + service.register(); + verify(commandManager.registerCommand(Commands.Enable_SourceMap_Support, anyFunction(), service)).once(); + }); + test('Setting is turned on and vsc reloaded', async () => { + const commandManager = mock(CommandManager); + const configService = mock(ConfigurationService); + const service = new SourceMapSupportService( + instance(commandManager), + [], + instance(configService), + undefined as any + ); + when( + configService.updateSetting('diagnostics.sourceMapsEnabled', true, undefined, ConfigurationTarget.Global) + ).thenResolve(); + when(commandManager.executeCommand('workbench.action.reloadWindow')).thenResolve(); + + await service.enable(); + + verify( + configService.updateSetting('diagnostics.sourceMapsEnabled', true, undefined, ConfigurationTarget.Global) + ).once(); + verify(commandManager.executeCommand('workbench.action.reloadWindow')).once(); + }); + test('Display prompt and do not enable', async () => { + const shell = mock(ApplicationShell); + const service = new (class extends SourceMapSupportService { + public async enable() { + throw new Error('Should not be invokved'); + } + public async onEnable() { + await super.onEnable(); + } + })(undefined as any, [], undefined as any, instance(shell)); + when(shell.showWarningMessage(anything(), anything())).thenResolve(); + + await service.onEnable(); + }); + test('Display prompt and must enable', async () => { + const commandManager = mock(CommandManager); + const configService = mock(ConfigurationService); + const shell = mock(ApplicationShell); + const service = new (class extends SourceMapSupportService { + public async onEnable() { + await super.onEnable(); + } + })(instance(commandManager), [], instance(configService), instance(shell)); + + when( + configService.updateSetting('diagnostics.sourceMapsEnabled', true, undefined, ConfigurationTarget.Global) + ).thenResolve(); + when(shell.showWarningMessage(anything(), anything())).thenResolve( + Diagnostics.enableSourceMapsAndReloadVSC() as any + ); + when(commandManager.executeCommand('workbench.action.reloadWindow')).thenResolve(); + + await service.onEnable(); + + verify( + configService.updateSetting('diagnostics.sourceMapsEnabled', true, undefined, ConfigurationTarget.Global) + ).once(); + verify(commandManager.executeCommand('workbench.action.reloadWindow')).once(); + }); +}); diff --git a/src/test/application/misc/joinMailingListPrompt.unit.test.ts b/src/test/application/misc/joinMailingListPrompt.unit.test.ts new file mode 100644 index 000000000000..825baf978af9 --- /dev/null +++ b/src/test/application/misc/joinMailingListPrompt.unit.test.ts @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { ImportMock } from 'ts-mock-imports'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { JoinMailingListPrompt } from '../../../client/application/misc/joinMailingListPrompt'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { IApplicationEnvironment, IApplicationShell } from '../../../client/common/application/types'; +import { JoinMailingListPromptVariants } from '../../../client/common/experiments/groups'; +import { ExperimentService } from '../../../client/common/experiments/service'; +import { BrowserService } from '../../../client/common/net/browser'; +import { PersistentState, PersistentStateFactory } from '../../../client/common/persistentState'; +import { IBrowserService, IExperimentService, IPersistentState } from '../../../client/common/types'; +import { Common } from '../../../client/common/utils/localize'; +import * as telemetry from '../../../client/telemetry'; +import { EventName } from '../../../client/telemetry/constants'; + +suite('Interpreters - Interpreter Selection Tip', () => { + let joinMailingList: JoinMailingListPrompt; + let appShell: IApplicationShell; + let storage: IPersistentState<boolean>; + let experimentService: IExperimentService; + let browserService: IBrowserService; + let applicationEnvironment: IApplicationEnvironment; + let sendTelemetryStub: sinon.SinonStub; + setup(() => { + const factory = mock(PersistentStateFactory); + storage = mock(PersistentState); + appShell = mock(ApplicationShell); + experimentService = mock(ExperimentService); + browserService = mock(BrowserService); + applicationEnvironment = mock(IApplicationEnvironment); + + when(factory.createGlobalPersistentState('JoinMailingListPrompt', false)).thenReturn(instance(storage)); + when(applicationEnvironment.sessionId).thenReturn('test.sessionId'); + + joinMailingList = new JoinMailingListPrompt( + instance(appShell), + instance(factory), + instance(experimentService), + instance(browserService), + instance(applicationEnvironment) + ); + + sendTelemetryStub = ImportMock.mockFunction(telemetry, 'sendTelemetryEvent'); + }); + teardown(() => { + sendTelemetryStub.restore(); + }); + test('Do not show notification if already shown', async () => { + when(storage.value).thenReturn(true); + + await joinMailingList.activate(); + + verify(appShell.showInformationMessage(anything(), anything())).never(); + }); + test('Do not show notification if in neither experiments', async () => { + when(storage.value).thenReturn(false); + when(experimentService.inExperiment(anything())).thenResolve(false); + + await joinMailingList.activate(); + + verify(appShell.showInformationMessage(anything(), anything())).never(); + verify(storage.updateValue(true)).once(); + }); + test('Show prompt if in variant 1 experiment', async () => { + when(storage.value).thenReturn(false); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant1)).thenResolve(true); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant2)).thenResolve(false); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant3)).thenResolve(false); + when(experimentService.getExperimentValue(JoinMailingListPromptVariants.variant1)).thenResolve('Sample value'); + + await joinMailingList.activate(); + + verify(appShell.showInformationMessage(anything(), Common.bannerLabelYes(), Common.bannerLabelNo())).once(); + verify(storage.updateValue(true)).once(); + }); + test('Show prompt if in variant 2 experiment', async () => { + when(storage.value).thenReturn(false); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant1)).thenResolve(false); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant2)).thenResolve(true); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant3)).thenResolve(false); + when(experimentService.getExperimentValue(JoinMailingListPromptVariants.variant2)).thenResolve('Sample value'); + + await joinMailingList.activate(); + + verify(appShell.showInformationMessage(anything(), Common.bannerLabelYes(), Common.bannerLabelNo())).once(); + verify(storage.updateValue(true)).once(); + }); + test('Show prompt if in variant 3 experiment', async () => { + when(storage.value).thenReturn(false); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant1)).thenResolve(false); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant2)).thenResolve(false); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant3)).thenResolve(true); + when(experimentService.getExperimentValue(JoinMailingListPromptVariants.variant3)).thenResolve('Sample value'); + + await joinMailingList.activate(); + + verify(appShell.showInformationMessage(anything(), Common.bannerLabelYes(), Common.bannerLabelNo())).once(); + verify(storage.updateValue(true)).once(); + }); + test('Show any variant, but user clicks "Yes"', async () => { + when(storage.value).thenReturn(false); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant1)).thenResolve(true); + when(experimentService.getExperimentValue(JoinMailingListPromptVariants.variant1)).thenResolve('Sample value'); + + when(appShell.showInformationMessage(anything(), Common.bannerLabelYes(), Common.bannerLabelNo())).thenResolve( + // tslint:disable-next-line: no-any + Common.bannerLabelYes() as any + ); + + await joinMailingList.activate(); + + verify(browserService.launch('https://aka.ms/python-vscode-mailinglist?m=test.sessionId')).once(); + verify(storage.updateValue(true)).once(); + assert.ok( + sendTelemetryStub.calledWithExactly(EventName.JOIN_MAILING_LIST_PROMPT, undefined, { selection: 'Yes' }) + ); + }); + test('Show any variant, but user clicks "No"', async () => { + when(storage.value).thenReturn(false); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant1)).thenResolve(true); + when(experimentService.getExperimentValue(JoinMailingListPromptVariants.variant1)).thenResolve('Sample value'); + + when(appShell.showInformationMessage(anything(), Common.bannerLabelYes(), Common.bannerLabelNo())).thenResolve( + // tslint:disable-next-line: no-any + Common.bannerLabelNo() as any + ); + + await joinMailingList.activate(); + + verify(storage.updateValue(true)).once(); + assert.ok( + sendTelemetryStub.calledWithExactly(EventName.JOIN_MAILING_LIST_PROMPT, undefined, { selection: 'No' }) + ); + }); + test('Show any variant, but user clicks close', async () => { + when(storage.value).thenReturn(false); + when(experimentService.inExperiment(JoinMailingListPromptVariants.variant1)).thenResolve(true); + when(experimentService.getExperimentValue(JoinMailingListPromptVariants.variant1)).thenResolve('Sample value'); + + when(appShell.showInformationMessage(anything(), Common.bannerLabelYes(), Common.bannerLabelNo())).thenResolve( + undefined + ); + + await joinMailingList.activate(); + + verify(storage.updateValue(true)).once(); + assert.ok( + sendTelemetryStub.calledWithExactly(EventName.JOIN_MAILING_LIST_PROMPT, undefined, { selection: undefined }) + ); + }); +}); diff --git a/src/test/ciConstants.ts b/src/test/ciConstants.ts new file mode 100644 index 000000000000..7bc24e3d2afa --- /dev/null +++ b/src/test/ciConstants.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// +// Constants that pertain to CI processes/tests only. No dependencies on vscode! +// +export const PYTHON_VIRTUAL_ENVS_LOCATION = process.env.PYTHON_VIRTUAL_ENVS_LOCATION; +export const IS_APPVEYOR = process.env.APPVEYOR === 'true'; +export const IS_TRAVIS = process.env.TRAVIS === 'true'; +export const IS_VSTS = process.env.TF_BUILD !== undefined; +export const IS_GITHUB_ACTIONS = process.env.GITHUB_ACTIONS === 'true'; +export const IS_CI_SERVER = IS_TRAVIS || IS_APPVEYOR || IS_VSTS || IS_GITHUB_ACTIONS; + +// Control JUnit-style output logging for reporting purposes. +let reportJunit: boolean = false; +if (IS_CI_SERVER && process.env.MOCHA_REPORTER_JUNIT !== undefined) { + reportJunit = process.env.MOCHA_REPORTER_JUNIT.toLowerCase() === 'true'; +} +export const MOCHA_REPORTER_JUNIT: boolean = reportJunit; +export const IS_CI_SERVER_TEST_DEBUGGER = process.env.IS_CI_SERVER_TEST_DEBUGGER === '1'; diff --git a/src/test/common.ts b/src/test/common.ts new file mode 100644 index 000000000000..3083337fc469 --- /dev/null +++ b/src/test/common.ts @@ -0,0 +1,711 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// tslint:disable:no-console no-require-imports no-var-requires + +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import * as glob from 'glob'; +import * as path from 'path'; +import { coerce, SemVer } from 'semver'; +import { ConfigurationTarget, Event, TextDocument, Uri } from 'vscode'; +import { IExtensionApi } from '../client/api'; +import { IProcessService } from '../client/common/process/types'; +import { IDisposable, IPythonSettings, Resource } from '../client/common/types'; +import { IServiceContainer, IServiceManager } from '../client/ioc/types'; +import { PythonEnvironment } from '../client/pythonEnvironments/info'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_MULTI_ROOT_TEST, IS_PERF_TEST, IS_SMOKE_TEST } from './constants'; +import { noop, sleep } from './core'; + +const StreamZip = require('node-stream-zip'); + +export { sleep } from './core'; + +// tslint:disable:no-invalid-this no-any + +const fileInNonRootWorkspace = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'pythonFiles', 'dummy.py'); +export const rootWorkspaceUri = getWorkspaceRoot(); + +export const PYTHON_PATH = getPythonPath(); + +const arch = require('arch'); +export const IS_64_BIT = arch() === 'x64'; + +export enum OSType { + Unknown = 'Unknown', + Windows = 'Windows', + OSX = 'OSX', + Linux = 'Linux' +} + +export type PythonSettingKeys = + | 'workspaceSymbols.enabled' + | 'pythonPath' + | 'languageServer' + | 'linting.lintOnSave' + | 'linting.enabled' + | 'linting.pylintEnabled' + | 'linting.flake8Enabled' + | 'linting.pycodestyleEnabled' + | 'linting.pylamaEnabled' + | 'linting.prospectorEnabled' + | 'linting.pydocstyleEnabled' + | 'linting.mypyEnabled' + | 'linting.banditEnabled' + | 'testing.nosetestArgs' + | 'testing.pytestArgs' + | 'testing.unittestArgs' + | 'formatting.provider' + | 'sortImports.args' + | 'testing.nosetestsEnabled' + | 'testing.pytestEnabled' + | 'testing.unittestEnabled' + | 'envFile' + | 'linting.ignorePatterns' + | 'terminal.activateEnvironment'; + +async function disposePythonSettings() { + if (!IS_SMOKE_TEST) { + const configSettings = await import('../client/common/configSettings'); + configSettings.PythonSettings.dispose(); + } +} + +export async function updateSetting( + setting: PythonSettingKeys, + value: {} | undefined, + resource: Uri | undefined, + configTarget: ConfigurationTarget +) { + const vscode = require('vscode') as typeof import('vscode'); + const settings = vscode.workspace.getConfiguration('python', resource || null); + const currentValue = settings.inspect(setting); + if ( + currentValue !== undefined && + ((configTarget === vscode.ConfigurationTarget.Global && currentValue.globalValue === value) || + (configTarget === vscode.ConfigurationTarget.Workspace && currentValue.workspaceValue === value) || + (configTarget === vscode.ConfigurationTarget.WorkspaceFolder && + currentValue.workspaceFolderValue === value)) + ) { + await disposePythonSettings(); + return; + } + await settings.update(setting, value, configTarget); + + // We've experienced trouble with .update in the past, where VSC returns stale data even + // after invoking the update method. This issue has regressed a few times as well. This + // delay is merely a backup to ensure it extension doesn't break the tests due to similar + // regressions in VSC: + // await sleep(2000); + // ... please see issue #2356 and PR #2332 for a discussion on the matter + + await disposePythonSettings(); +} + +export async function clearPythonPathInWorkspaceFolder(resource: string | Uri) { + const vscode = require('vscode') as typeof import('vscode'); + return retryAsync(setPythonPathInWorkspace)(resource, vscode.ConfigurationTarget.WorkspaceFolder); +} + +export async function setPythonPathInWorkspaceRoot(pythonPath: string) { + const vscode = require('vscode') as typeof import('vscode'); + return retryAsync(setPythonPathInWorkspace)(undefined, vscode.ConfigurationTarget.Workspace, pythonPath); +} + +export async function restorePythonPathInWorkspaceRoot() { + const vscode = require('vscode') as typeof import('vscode'); + return retryAsync(setPythonPathInWorkspace)(undefined, vscode.ConfigurationTarget.Workspace, PYTHON_PATH); +} + +export async function setGlobalInterpreterPath(pythonPath: string) { + return retryAsync(setGlobalPathToInterpreter)(pythonPath); +} + +export const resetGlobalInterpreterPathSetting = async () => retryAsync(restoreGlobalInterpreterPathSetting)(); + +async function restoreGlobalInterpreterPathSetting(): Promise<void> { + const vscode = require('vscode') as typeof import('vscode'); + const pythonConfig = vscode.workspace.getConfiguration('python', (null as any) as Uri); + await pythonConfig.update('defaultInterpreterPath', undefined, true); + await disposePythonSettings(); +} +async function setGlobalPathToInterpreter(pythonPath?: string): Promise<void> { + const vscode = require('vscode') as typeof import('vscode'); + const pythonConfig = vscode.workspace.getConfiguration('python', (null as any) as Uri); + await pythonConfig.update('defaultInterpreterPath', pythonPath, true); + await disposePythonSettings(); +} +export const resetGlobalPythonPathSetting = async () => retryAsync(restoreGlobalPythonPathSetting)(); + +export async function setAutoSaveDelayInWorkspaceRoot(delayinMS: number) { + const vscode = require('vscode') as typeof import('vscode'); + return retryAsync(setAutoSaveDelay)(undefined, vscode.ConfigurationTarget.Workspace, delayinMS); +} + +function getWorkspaceRoot() { + if (IS_SMOKE_TEST || IS_PERF_TEST) { + return; + } + const vscode = require('vscode') as typeof import('vscode'); + if (!Array.isArray(vscode.workspace.workspaceFolders) || vscode.workspace.workspaceFolders.length === 0) { + return vscode.Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test')); + } + if (vscode.workspace.workspaceFolders.length === 1) { + return vscode.workspace.workspaceFolders[0].uri; + } + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(fileInNonRootWorkspace)); + return workspaceFolder ? workspaceFolder.uri : vscode.workspace.workspaceFolders[0].uri; +} + +export function getExtensionSettings(resource: Uri | undefined): IPythonSettings { + const vscode = require('vscode') as typeof import('vscode'); + class AutoSelectionService { + get onDidChangeAutoSelectedInterpreter(): Event<void> { + return new vscode.EventEmitter<void>().event; + } + public autoSelectInterpreter(_resource: Resource): Promise<void> { + return Promise.resolve(); + } + public getAutoSelectedInterpreter(_resource: Resource): PythonEnvironment | undefined { + return; + } + public async setWorkspaceInterpreter( + _resource: Uri, + _interpreter: PythonEnvironment | undefined + ): Promise<void> { + return; + } + } + const pythonSettings = require('../client/common/configSettings') as typeof import('../client/common/configSettings'); + return pythonSettings.PythonSettings.getInstance(resource, new AutoSelectionService()); +} +export function retryAsync(this: any, wrapped: Function, retryCount: number = 2) { + return async (...args: any[]) => { + return new Promise((resolve, reject) => { + const reasons: any[] = []; + + const makeCall = () => { + wrapped.call(this as Function, ...args).then(resolve, (reason: any) => { + reasons.push(reason); + if (reasons.length >= retryCount) { + reject(reasons); + } else { + // If failed once, lets wait for some time before trying again. + setTimeout(makeCall, 500); + } + }); + }; + + makeCall(); + }); + }; +} + +async function setAutoSaveDelay(resource: string | Uri | undefined, config: ConfigurationTarget, delayinMS: number) { + const vscode = require('vscode') as typeof import('vscode'); + if (config === vscode.ConfigurationTarget.WorkspaceFolder && !IS_MULTI_ROOT_TEST) { + return; + } + const resourceUri = typeof resource === 'string' ? vscode.Uri.file(resource) : resource; + const settings = vscode.workspace.getConfiguration('files', resourceUri || null); + const value = settings.inspect<number>('autoSaveDelay'); + const prop: 'workspaceFolderValue' | 'workspaceValue' = + config === vscode.ConfigurationTarget.Workspace ? 'workspaceValue' : 'workspaceFolderValue'; + if (value && value[prop] !== delayinMS) { + await settings.update('autoSaveDelay', delayinMS, config); + await settings.update('autoSave', 'afterDelay'); + } +} + +async function setPythonPathInWorkspace( + resource: string | Uri | undefined, + config: ConfigurationTarget, + pythonPath?: string +) { + const vscode = require('vscode') as typeof import('vscode'); + if (config === vscode.ConfigurationTarget.WorkspaceFolder && !IS_MULTI_ROOT_TEST) { + return; + } + const resourceUri = typeof resource === 'string' ? vscode.Uri.file(resource) : resource; + const settings = vscode.workspace.getConfiguration('python', resourceUri || null); + const value = settings.inspect<string>('pythonPath'); + const prop: 'workspaceFolderValue' | 'workspaceValue' = + config === vscode.ConfigurationTarget.Workspace ? 'workspaceValue' : 'workspaceFolderValue'; + if (value && value[prop] !== pythonPath) { + await settings.update('pythonPath', pythonPath, config); + await disposePythonSettings(); + } +} +async function restoreGlobalPythonPathSetting(): Promise<void> { + const vscode = require('vscode') as typeof import('vscode'); + const pythonConfig = vscode.workspace.getConfiguration('python', (null as any) as Uri); + await Promise.all([ + pythonConfig.update('pythonPath', undefined, true), + pythonConfig.update('defaultInterpreterPath', undefined, true) + ]); + await disposePythonSettings(); +} + +export async function deleteDirectory(dir: string) { + const exists = await fs.pathExists(dir); + if (exists) { + await fs.remove(dir); + } +} + +export async function deleteFile(file: string) { + const exists = await fs.pathExists(file); + if (exists) { + await fs.remove(file); + } +} + +export async function deleteFiles(globPattern: string) { + const items = await new Promise<string[]>((resolve, reject) => { + glob(globPattern, (ex, files) => (ex ? reject(ex) : resolve(files))); + }); + + return Promise.all(items.map((item) => fs.remove(item).catch(noop))); +} +function getPythonPath(): string { + if (process.env.CI_PYTHON_PATH && fs.existsSync(process.env.CI_PYTHON_PATH)) { + return process.env.CI_PYTHON_PATH; + } + // tslint:disable-next-line:no-suspicious-comment + // TODO: Change this to python3. + // See https://github.com/microsoft/vscode-python/issues/10910. + return 'python'; +} + +/** + * Determine if the current platform is included in a list of platforms. + * + * @param {OSes} OSType[] List of operating system Ids to check within. + * @return true if the current OS matches one from the list, false otherwise. + */ +export function isOs(...OSes: OSType[]): boolean { + // get current OS + const currentOS: OSType = getOSType(); + // compare and return + if (OSes.indexOf(currentOS) === -1) { + return false; + } + return true; +} + +export function getOSType(platform: string = process.platform): OSType { + if (/^win/.test(platform)) { + return OSType.Windows; + } else if (/^darwin/.test(platform)) { + return OSType.OSX; + } else if (/^linux/.test(platform)) { + return OSType.Linux; + } else { + return OSType.Unknown; + } +} + +/** + * Update a string that represents a path in any OS to the string representation of + * that same path in a different OS. Note: Does not handle drive letter if the path + * is intended for a root. + * + * @param pathToCorrect The string representation of a path from a specific OS. + * @param os The OS representation to switch to - if left undefined the current OS is used. + */ +export function correctPathForOsType(pathToCorrect: string, os?: OSType): string { + if (os === undefined) { + os = getOSType(); + } + const pathSep: string = os === OSType.Windows ? '\\' : '/'; + const replacePathSepRegex: RegExp = os === OSType.Windows ? /\//g : /\\/g; + + return pathToCorrect.replace(replacePathSepRegex, pathSep); +} + +/** + * Get the current Python interpreter version. + * + * @param {procService} IProcessService Optionally specify the IProcessService implementation to use to execute with. + * @return `SemVer` version of the Python interpreter, or `undefined` if an error occurs. + */ +export async function getPythonSemVer(procService?: IProcessService): Promise<SemVer | undefined> { + const decoder = await import('../client/common/process/decoder'); + const proc = await import('../client/common/process/proc'); + + const pythonProcRunner = procService ? procService : new proc.ProcessService(new decoder.BufferDecoder()); + const pyVerArgs = ['-c', 'import sys;print("{0}.{1}.{2}".format(*sys.version_info[:3]))']; + + return pythonProcRunner + .exec(PYTHON_PATH, pyVerArgs) + .then((strVersion) => new SemVer(strVersion.stdout.trim())) + .catch((err) => { + // if the call fails this should make it loudly apparent. + console.error('Failed to get Python Version in getPythonSemVer', err); + return undefined; + }); +} + +/** + * Match a given semver version specification with a list of loosely defined + * version strings. + * + * Specify versions by their major version at minimum - the minor and patch + * version numbers are optional. + * + * '3', '3.6', '3.6.6', are all vald and only the portions specified will be matched + * against the current running Python interpreter version. + * + * Example scenarios: + * '3' will match version 3.5.6, 3.6.4, 3.6.6, and 3.7.0. + * '3.6' will match version 3.6.4 and 3.6.6. + * '3.6.4' will match version 3.6.4 only. + * + * @param {version} SemVer the version to look for. + * @param {searchVersions} string[] List of loosely-specified versions to match against. + */ +export function isVersionInList(version: SemVer, ...searchVersions: string[]): boolean { + // see if the major/minor version matches any member of the skip-list. + const isPresent = searchVersions.findIndex((ver) => { + const semverChecker = coerce(ver); + if (semverChecker) { + if (semverChecker.compare(version) === 0) { + return true; + } else { + // compare all the parts of the version that we have, we know we have + // at minimum the major version or semverChecker would be 'null'... + const versionParts = ver.split('.'); + let matches = parseInt(versionParts[0], 10) === version.major; + + if (matches && versionParts.length >= 2) { + matches = parseInt(versionParts[1], 10) === version.minor; + } + + if (matches && versionParts.length >= 3) { + matches = parseInt(versionParts[2], 10) === version.patch; + } + + return matches; + } + } + return false; + }); + + if (isPresent >= 0) { + return true; + } + return false; +} + +/** + * Determine if the Python interpreter version running in a given `IProcessService` + * is in a selection of versions. + * + * You can specify versions by specifying the major version at minimum - the minor and + * patch version numbers are optional. + * + * '3', '3.6', '3.6.6', are all vald and only the portions specified will be matched + * against the current running Python interpreter version. + * + * Example scenarios: + * '3' will match version 3.5.6, 3.6.4, 3.6.6, and 3.7.0. + * '3.6' will match version 3.6.4 and 3.6.6. + * '3.6.4' will match version 3.6.4 only. + * + * If you don't need to specify the environment (ie. the workspace) that the Python + * interpreter is running under, use the simpler `isPythonVersion` instead. + * + * @param {procService} IProcessService Optionally, use this process service to call out to python with. + * @param {versions} string[] Python versions to test for, specified as described above. + * @return true if the current Python version matches a version in the skip list, false otherwise. + */ +export async function isPythonVersionInProcess(procService?: IProcessService, ...versions: string[]): Promise<boolean> { + // get the current python version major/minor + const currentPyVersion = await getPythonSemVer(procService); + if (currentPyVersion) { + return isVersionInList(currentPyVersion, ...versions); + } else { + console.error( + `Failed to determine the current Python version when comparing against list [${versions.join(', ')}].` + ); + return false; + } +} + +/** + * Determine if the current interpreter version is in a given selection of versions. + * + * You can specify versions by using up to the first three semver parts of a python + * version. + * + * '3', '3.6', '3.6.6', are all vald and only the portions specified will be matched + * against the current running Python interpreter version. + * + * Example scenarios: + * '3' will match version 3.5.6, 3.6.4, 3.6.6, and 3.7.0. + * '3.6' will match version 3.6.4 and 3.6.6. + * '3.6.4' will match version 3.6.4 only. + * + * If you need to specify the environment (ie. the workspace) that the Python + * interpreter is running under, use `isPythonVersionInProcess` instead. + * + * @param {versions} string[] List of versions of python that are to be skipped. + * @param {resource} vscode.Uri Current workspace resource Uri or undefined. + * @return true if the current Python version matches a version in the skip list, false otherwise. + */ +export async function isPythonVersion(...versions: string[]): Promise<boolean> { + const currentPyVersion = await getPythonSemVer(); + if (currentPyVersion) { + return isVersionInList(currentPyVersion, ...versions); + } else { + console.error( + `Failed to determine the current Python version when comparing against list [${versions.join(', ')}].` + ); + return false; + } +} + +export interface IExtensionTestApi extends IExtensionApi { + serviceContainer: IServiceContainer; + serviceManager: IServiceManager; +} + +export async function unzip(zipFile: string, targetFolder: string): Promise<void> { + await fs.ensureDir(targetFolder); + return new Promise<void>((resolve, reject) => { + const zip = new StreamZip({ + file: zipFile, + storeEntries: true + }); + zip.on('ready', async () => { + zip.extract('extension', targetFolder, (err: any) => { + if (err) { + reject(err); + } else { + resolve(); + } + zip.close(); + }); + }); + }); +} +/** + * Wait for a condition to be fulfilled within a timeout. + * + * @export + * @param {() => Promise<boolean>} condition + * @param {number} timeoutMs + * @param {string} errorMessage + * @returns {Promise<void>} + */ +export async function waitForCondition( + condition: () => Promise<boolean>, + timeoutMs: number, + errorMessage: string +): Promise<void> { + return new Promise<void>(async (resolve, reject) => { + const timeout = setTimeout(() => { + clearTimeout(timeout); + // tslint:disable-next-line: no-use-before-declare + clearTimeout(timer); + reject(new Error(errorMessage)); + }, timeoutMs); + const timer = setInterval(async () => { + if (!(await condition().catch(() => false))) { + return; + } + clearTimeout(timeout); + clearTimeout(timer); + resolve(); + }, 10); + }); +} + +/** + * Execute a method until it executes without any exceptions. + */ +export async function retryIfFail<T>(fn: () => Promise<T>, timeoutMs: number = 60_000): Promise<T> { + let lastEx: Error | undefined; + const started = new Date().getTime(); + while (timeoutMs > new Date().getTime() - started) { + try { + // tslint:disable-next-line: no-unnecessary-local-variable + const result = await fn(); + // Capture result, if no exceptions return that. + return result; + } catch (ex) { + lastEx = ex; + } + await sleep(10); + } + if (lastEx) { + throw lastEx; + } + throw new Error('Timeout waiting for function to complete without any errors'); +} + +export async function openFile(file: string): Promise<TextDocument> { + const vscode = require('vscode') as typeof import('vscode'); + const textDocument = await vscode.workspace.openTextDocument(file); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + return textDocument; +} + +/** + * Fakes for timers in nodejs when testing, using `lolex`. + * An alternative to `sinon.useFakeTimers` (which in turn uses `lolex`, but doesn't expose the `async` methods). + * Use this class when you have tests with `setTimeout` and which to avoid them for faster tests. + * + * For further information please refer: + * - https://www.npmjs.com/package/lolex + * - https://sinonjs.org/releases/v1.17.6/fake-timers/ + * + * @class FakeClock + */ +export class FakeClock { + // tslint:disable-next-line:no-any + private clock?: any; + /** + * Creates an instance of FakeClock. + * @param {number} [advacenTimeMs=10_000] Default `timeout` value. Defaults to 10s. Assuming we do not have anything bigger. + * @memberof FakeClock + */ + constructor(private readonly advacenTimeMs: number = 10_000) {} + public install() { + // tslint:disable-next-line:no-require-imports + const lolex = require('lolex'); + this.clock = lolex.install(); + } + public uninstall() { + this.clock?.uninstall(); + } + /** + * Wait for timers to kick in, and then wait for all of them to complete. + * + * @returns {Promise<void>} + * @memberof FakeClock + */ + public async wait(): Promise<void> { + await this.waitForTimersToStart(); + await this.waitForTimersToFinish(); + } + + /** + * Wait for timers to start. + * + * @returns {Promise<void>} + * @memberof FakeClock + */ + private async waitForTimersToStart(): Promise<void> { + if (!this.clock) { + throw new Error('Fake clock not installed'); + } + while (this.clock.countTimers() === 0) { + // Relinquish control to event loop, so other timer code will run. + // We want to wait for `setTimeout` to kick in. + await new Promise((resolve) => process.nextTick(resolve)); + } + } + /** + * Wait for timers to finish. + * + * @returns {Promise<void>} + * @memberof FakeClock + */ + private async waitForTimersToFinish(): Promise<void> { + if (!this.clock) { + throw new Error('Fake clock not installed'); + } + while (this.clock.countTimers()) { + // Advance clock by 10s (can be anything to ensure the next scheduled block of code executes). + // Assuming we do not have timers > 10s + // This will ensure any such such as `setTimeout(..., 10)` will get executed. + this.clock.tick(this.advacenTimeMs); + + // Wait for the timer code to run to completion (incase they are promises). + await this.clock.runAllAsync(); + } + } +} + +/** + * Helper class to test events. + * + * Usage: Assume xyz.onDidSave is the event we want to test. + * const handler = new TestEventHandler(xyz.onDidSave); + * // Do something that would trigger the event. + * assert.ok(handler.fired) + * assert.equal(handler.first, 'Args Passed to first onDidSave') + * assert.equal(handler.count, 1)// Only one should have been fired. + */ +export class TestEventHandler<T extends void | any = any> implements IDisposable { + public get fired() { + return this.handledEvents.length > 0; + } + public get first(): T { + return this.handledEvents[0]; + } + public get second(): T { + return this.handledEvents[1]; + } + public get last(): T { + return this.handledEvents[this.handledEvents.length - 1]; + } + public get count(): number { + return this.handledEvents.length; + } + public get all(): T[] { + return this.handledEvents; + } + private readonly handler: IDisposable; + // tslint:disable-next-line: no-any + private readonly handledEvents: any[] = []; + constructor(event: Event<T>, private readonly eventNameForErrorMessages: string, disposables: IDisposable[] = []) { + disposables.push(this); + this.handler = event(this.listener, this); + } + public reset() { + while (this.handledEvents.length) { + this.handledEvents.pop(); + } + } + public async assertFired(waitPeriod: number = 100): Promise<void> { + await waitForCondition(async () => this.fired, waitPeriod, `${this.eventNameForErrorMessages} event not fired`); + } + public async assertFiredExactly(numberOfTimesFired: number, waitPeriod: number = 2_000): Promise<void> { + await waitForCondition( + async () => this.count === numberOfTimesFired, + waitPeriod, + `${this.eventNameForErrorMessages} event fired ${this.count}, expected ${numberOfTimesFired}` + ); + } + public async assertFiredAtLeast(numberOfTimesFired: number, waitPeriod: number = 2_000): Promise<void> { + await waitForCondition( + async () => this.count >= numberOfTimesFired, + waitPeriod, + `${this.eventNameForErrorMessages} event fired ${this.count}, expected at least ${numberOfTimesFired}.` + ); + } + public atIndex(index: number): T { + return this.handledEvents[index]; + } + + public dispose() { + this.handler.dispose(); + } + + private listener(e: T) { + this.handledEvents.push(e); + } +} + +export function createEventHandler<T, K extends keyof T>( + obj: T, + eventName: K, + dispoables: IDisposable[] = [] +): T[K] extends Event<infer TArgs> ? TestEventHandler<TArgs> : TestEventHandler<void> { + // tslint:disable-next-line: no-any + return new TestEventHandler(obj[eventName] as any, eventName as string, dispoables) as any; +} diff --git a/src/test/common/application/commands/reloadCommand.unit.test.ts b/src/test/common/application/commands/reloadCommand.unit.test.ts new file mode 100644 index 000000000000..2606cf18da7d --- /dev/null +++ b/src/test/common/application/commands/reloadCommand.unit.test.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../../client/common/application/commandManager'; +import { ReloadVSCodeCommandHandler } from '../../../../client/common/application/commands/reloadCommand'; +import { IApplicationShell, ICommandManager } from '../../../../client/common/application/types'; +import { Common } from '../../../../client/common/utils/localize'; + +// Defines a Mocha test suite to group tests of similar kind together +suite('Common Commands ReloadCommand', () => { + let reloadCommandHandler: ReloadVSCodeCommandHandler; + let appShell: IApplicationShell; + let cmdManager: ICommandManager; + setup(async () => { + appShell = mock(ApplicationShell); + cmdManager = mock(CommandManager); + reloadCommandHandler = new ReloadVSCodeCommandHandler(instance(cmdManager), instance(appShell)); + when(cmdManager.executeCommand(anything())).thenResolve(); + await reloadCommandHandler.activate(); + }); + + test('Confirm command handler is added', async () => { + verify(cmdManager.registerCommand('python.reloadVSCode', anything(), anything())).once(); + }); + test('Display prompt to reload VS Code with message passed into command', async () => { + const message = 'Hello World!'; + // tslint:disable-next-line: no-any + const commandHandler = capture(cmdManager.registerCommand as any).first()[1] as Function; + + await commandHandler.call(reloadCommandHandler, message); + + verify(appShell.showInformationMessage(message, Common.reload())).once(); + }); + test('Do not reload VS Code if user selects `Reload` option', async () => { + const message = 'Hello World!'; + // tslint:disable-next-line: no-any + const commandHandler = capture(cmdManager.registerCommand as any).first()[1] as Function; + // tslint:disable-next-line: no-any + when(appShell.showInformationMessage(message, Common.reload())).thenResolve(Common.reload() as any); + + await commandHandler.call(reloadCommandHandler, message); + + verify(appShell.showInformationMessage(message, Common.reload())).once(); + verify(cmdManager.executeCommand('workbench.action.reloadWindow')).once(); + }); + test('Do not reload VS Code if user does not select `Reload` option', async () => { + const message = 'Hello World!'; + // tslint:disable-next-line: no-any + const commandHandler = capture(cmdManager.registerCommand as any).first()[1] as Function; + when(appShell.showInformationMessage(message, Common.reload())).thenResolve(); + + await commandHandler.call(reloadCommandHandler, message); + + verify(appShell.showInformationMessage(message, Common.reload())).once(); + verify(cmdManager.executeCommand('workbench.action.reloadWindow')).never(); + }); +}); diff --git a/src/test/common/asyncDump.ts b/src/test/common/asyncDump.ts new file mode 100644 index 000000000000..b92b0231ced9 --- /dev/null +++ b/src/test/common/asyncDump.ts @@ -0,0 +1,9 @@ +'use strict'; + +//tslint:disable:no-require-imports no-var-requires +const log = require('why-is-node-running'); + +// Call this function to debug async hangs. It should print out stack traces of still running promises. +export function asyncDump() { + log(); +} diff --git a/src/test/common/configSettings.test.ts b/src/test/common/configSettings.test.ts new file mode 100644 index 000000000000..fb50c191e434 --- /dev/null +++ b/src/test/common/configSettings.test.ts @@ -0,0 +1,60 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { IWorkspaceSymbolSettings } from '../../client/common/types'; +import { SystemVariables } from '../../client/common/variables/systemVariables'; +import { getExtensionSettings } from '../common'; +import { initialize } from './../initialize'; + +const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); + +// Defines a Mocha test suite to group tests of similar kind together +suite('Configuration Settings', () => { + setup(initialize); + + test('Check Values', (done) => { + const systemVariables: SystemVariables = new SystemVariables(undefined, workspaceRoot); + // tslint:disable-next-line:no-any + const pythonConfig = vscode.workspace.getConfiguration('python', (null as any) as vscode.Uri); + const pythonSettings = getExtensionSettings(vscode.Uri.file(workspaceRoot)); + Object.keys(pythonSettings).forEach((key) => { + let settingValue = pythonConfig.get(key, 'Not a config'); + if (settingValue === 'Not a config') { + return; + } + if (settingValue) { + settingValue = systemVariables.resolve(settingValue); + } + // tslint:disable-next-line:no-any + const pythonSettingValue = (pythonSettings as any)[key] as string; + if (key.endsWith('Path') && IS_WINDOWS) { + assert.equal( + settingValue.toUpperCase(), + pythonSettingValue.toUpperCase(), + `Setting ${key} not the same` + ); + } else if (key === 'workspaceSymbols' && IS_WINDOWS) { + const workspaceSettings = (pythonSettingValue as {}) as IWorkspaceSymbolSettings; + const workspaceSttings = (settingValue as {}) as IWorkspaceSymbolSettings; + assert.equal( + workspaceSettings.tagFilePath.toUpperCase(), + workspaceSttings.tagFilePath.toUpperCase(), + `Setting ${key} not the same` + ); + + const workspaceSettingsWithoutPath = { ...workspaceSettings }; + workspaceSettingsWithoutPath.tagFilePath = ''; + const pythonSettingValueWithoutPath = { ...((pythonSettingValue as {}) as IWorkspaceSymbolSettings) }; + pythonSettingValueWithoutPath.tagFilePath = ''; + assert.deepEqual( + workspaceSettingsWithoutPath, + pythonSettingValueWithoutPath, + `Setting ${key} not the same` + ); + } + }); + + done(); + }); +}); diff --git a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts new file mode 100644 index 000000000000..e83a69742aee --- /dev/null +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-require-imports no-var-requires max-func-body-length no-unnecessary-override no-invalid-template-strings no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { DeprecatePythonPath } from '../../../client/common/experiments/groups'; +import { IExperimentsManager, IInterpreterPathService } from '../../../client/common/types'; +import { noop } from '../../../client/common/utils/misc'; +import { IInterpreterSecurityService } from '../../../client/interpreter/autoSelection/types'; +import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; +import { MockAutoSelectionService } from '../../mocks/autoSelector'; +const untildify = require('untildify'); + +suite('Python Settings - pythonPath', () => { + class CustomPythonSettings extends PythonSettings { + public update(settings: WorkspaceConfiguration) { + return super.update(settings); + } + protected getPythonExecutable(pythonPath: string) { + return pythonPath; + } + protected initialize() { + noop(); + } + } + let configSettings: CustomPythonSettings; + let workspaceService: typemoq.IMock<IWorkspaceService>; + let experimentsManager: typemoq.IMock<IExperimentsManager>; + let interpreterPathService: typemoq.IMock<IInterpreterPathService>; + let pythonSettings: typemoq.IMock<WorkspaceConfiguration>; + setup(() => { + pythonSettings = typemoq.Mock.ofType<WorkspaceConfiguration>(); + sinon.stub(EnvFileTelemetry, 'sendSettingTelemetry').returns(); + interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>(); + experimentsManager = typemoq.Mock.ofType<IExperimentsManager>(); + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + pythonSettings.setup((p) => p.get(typemoq.It.isValue('defaultInterpreterPath'))).returns(() => 'python'); + pythonSettings.setup((p) => p.get('logging')).returns(() => ({ level: 'error' })); + }); + teardown(() => { + if (configSettings) { + configSettings.dispose(); + } + sinon.restore(); + }); + + test('Python Path from settings.json is used', () => { + configSettings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); + const pythonPath = 'This is the python Path'; + pythonSettings + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeast(1)); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + }); + test("Python Path from settings.json is used and relative path starting with '~' will be resolved from home directory", () => { + configSettings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); + const pythonPath = `~${path.sep}This is the python Path`; + pythonSettings + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeast(1)); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(untildify(pythonPath)); + }); + test("Python Path from settings.json is used and relative path starting with '.' will be resolved from workspace folder", () => { + const workspaceFolderUri = Uri.file(__dirname); + configSettings = new CustomPythonSettings(workspaceFolderUri, new MockAutoSelectionService()); + const pythonPath = `.${path.sep}This is the python Path`; + pythonSettings + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeast(1)); + + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(path.resolve(workspaceFolderUri.fsPath, pythonPath)); + }); + test('Python Path from settings.json is used and ${workspacecFolder} value will be resolved from workspace folder', () => { + const workspaceFolderUri = Uri.file(__dirname); + configSettings = new CustomPythonSettings(workspaceFolderUri, new MockAutoSelectionService()); + const workspaceFolderToken = '${workspaceFolder}'; + const pythonPath = `${workspaceFolderToken}${path.sep}This is the python Path`; + pythonSettings + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeast(1)); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(path.join(workspaceFolderUri.fsPath, 'This is the python Path')); + }); + test("If we don't have a custom python path and no auto selected interpreters, then use default", () => { + const workspaceFolderUri = Uri.file(__dirname); + const selectionService = mock(MockAutoSelectionService); + configSettings = new CustomPythonSettings(workspaceFolderUri, instance(selectionService)); + const pythonPath = 'python'; + pythonSettings + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeast(1)); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal('python'); + }); + test("If we don't have a custom python path and we do have an auto selected interpreter, then use it", () => { + const pythonPath = path.join(__dirname, 'this is a python path that was auto selected'); + const interpreter: any = { path: pythonPath }; + const workspaceFolderUri = Uri.file(__dirname); + const selectionService = mock(MockAutoSelectionService); + when(selectionService.getAutoSelectedInterpreter(workspaceFolderUri)).thenReturn(interpreter); + when(selectionService.setWorkspaceInterpreter(workspaceFolderUri, anything())).thenResolve(); + configSettings = new CustomPythonSettings(workspaceFolderUri, instance(selectionService)); + pythonSettings + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => 'python') + .verifiable(typemoq.Times.atLeast(1)); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + verify(selectionService.getAutoSelectedInterpreter(workspaceFolderUri)).once(); + }); + test("If user is in Deprecate Python Path experiment and we don't have a custom python path, get the autoselected interpreter and use it if it's safe", () => { + const resource = Uri.parse('a'); + const pythonPath = path.join(__dirname, 'this is a python path that was auto selected'); + const interpreter: any = { path: pythonPath }; + const selectionService = mock(MockAutoSelectionService); + const interpreterSecurityService = typemoq.Mock.ofType<IInterpreterSecurityService>(); + when(selectionService.getAutoSelectedInterpreter(resource)).thenReturn(interpreter); + interpreterSecurityService.setup((i) => i.isSafe(interpreter)).returns(() => true); + when(selectionService.setWorkspaceInterpreter(resource, anything())).thenResolve(); + configSettings = new CustomPythonSettings( + resource, + instance(selectionService), + workspaceService.object, + experimentsManager.object, + interpreterPathService.object, + interpreterSecurityService.object + ); + experimentsManager + .setup((e) => e.inExperiment(DeprecatePythonPath.experiment)) + .returns(() => true) + .verifiable(typemoq.Times.once()); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + interpreterPathService.setup((i) => i.get(resource)).returns(() => 'python'); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + experimentsManager.verifyAll(); + interpreterPathService.verifyAll(); + pythonSettings.verifyAll(); + verify(selectionService.getAutoSelectedInterpreter(resource)).once(); + }); + test("If user is in Deprecate Python Path experiment and we don't have a custom python path, get the autoselected interpreter and but don't use it if it's not safe", () => { + const resource = Uri.parse('a'); + const pythonPath = path.join(__dirname, 'this is a python path that was auto selected'); + const interpreter: any = { path: pythonPath }; + const selectionService = mock(MockAutoSelectionService); + const interpreterSecurityService = typemoq.Mock.ofType<IInterpreterSecurityService>(); + when(selectionService.getAutoSelectedInterpreter(resource)).thenReturn(interpreter); + interpreterSecurityService.setup((i) => i.isSafe(interpreter)).returns(() => false); + when(selectionService.setWorkspaceInterpreter(resource, anything())).thenResolve(); + configSettings = new CustomPythonSettings( + resource, + instance(selectionService), + workspaceService.object, + experimentsManager.object, + interpreterPathService.object, + interpreterSecurityService.object + ); + experimentsManager + .setup((e) => e.inExperiment(DeprecatePythonPath.experiment)) + .returns(() => true) + .verifiable(typemoq.Times.once()); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + interpreterPathService.setup((i) => i.get(resource)).returns(() => 'python'); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal('a'); + experimentsManager.verifyAll(); + interpreterPathService.verifyAll(); + pythonSettings.verifyAll(); + verify(selectionService.getAutoSelectedInterpreter(resource)).once(); + }); + test('If user is in Deprecate Python Path experiment, use the new API to fetch Python Path', () => { + const resource = Uri.parse('a'); + configSettings = new CustomPythonSettings( + resource, + new MockAutoSelectionService(), + workspaceService.object, + experimentsManager.object, + interpreterPathService.object + ); + const pythonPath = 'This is the new API python Path'; + pythonSettings.setup((p) => p.get(typemoq.It.isValue('pythonPath'))).verifiable(typemoq.Times.never()); + experimentsManager + .setup((e) => e.inExperiment(DeprecatePythonPath.experiment)) + .returns(() => true) + .verifiable(typemoq.Times.once()); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + interpreterPathService + .setup((i) => i.get(resource)) + .returns(() => pythonPath) + .verifiable(typemoq.Times.once()); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + experimentsManager.verifyAll(); + interpreterPathService.verifyAll(); + pythonSettings.verifyAll(); + }); + test('If user is not in Deprecate Python Path experiment, use the settings to fetch Python Path', () => { + const resource = Uri.parse('a'); + configSettings = new CustomPythonSettings( + resource, + new MockAutoSelectionService(), + workspaceService.object, + experimentsManager.object, + interpreterPathService.object + ); + const pythonPath = 'This is the settings python Path'; + pythonSettings + .setup((p) => p.get(typemoq.It.isValue('pythonPath'))) + .returns(() => pythonPath) + .verifiable(typemoq.Times.atLeastOnce()); + experimentsManager + .setup((e) => e.inExperiment(DeprecatePythonPath.experiment)) + .returns(() => false) + .verifiable(typemoq.Times.once()); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + interpreterPathService.setup((i) => i.get(resource)).verifiable(typemoq.Times.never()); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + experimentsManager.verifyAll(); + interpreterPathService.verifyAll(); + pythonSettings.verifyAll(); + }); +}); diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts new file mode 100644 index 000000000000..c8e1b92042b5 --- /dev/null +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -0,0 +1,355 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { assert, expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +// tslint:disable-next-line:no-require-imports +import untildify = require('untildify'); +import { WorkspaceConfiguration } from 'vscode'; +import { LanguageServerType } from '../../../client/activation/types'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { + IAnalysisSettings, + IAutoCompleteSettings, + IDataScienceSettings, + IExperiments, + IFormattingSettings, + ILintingSettings, + ILoggingSettings, + ISortImportSettings, + ITerminalSettings, + ITestingSettings, + IWorkspaceSymbolSettings +} from '../../../client/common/types'; +import { noop } from '../../../client/common/utils/misc'; +import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; +import { MockAutoSelectionService } from '../../mocks/autoSelector'; + +// tslint:disable-next-line:max-func-body-length +suite('Python Settings', async () => { + class CustomPythonSettings extends PythonSettings { + // tslint:disable-next-line:no-unnecessary-override + public update(pythonSettings: WorkspaceConfiguration) { + return super.update(pythonSettings); + } + protected initialize() { + noop(); + } + } + let config: TypeMoq.IMock<WorkspaceConfiguration>; + let expected: CustomPythonSettings; + let settings: CustomPythonSettings; + setup(() => { + sinon.stub(EnvFileTelemetry, 'sendSettingTelemetry').returns(); + config = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Loose); + expected = new CustomPythonSettings(undefined, new MockAutoSelectionService()); + settings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); + expected.defaultInterpreterPath = 'python'; + }); + + teardown(() => { + sinon.restore(); + }); + + function initializeConfig(sourceSettings: PythonSettings) { + // string settings + for (const name of [ + 'pythonPath', + 'venvPath', + 'condaPath', + 'pipenvPath', + 'envFile', + 'poetryPath', + 'insidersChannel', + 'defaultInterpreterPath', + 'jediPath' + ]) { + config + .setup((c) => c.get<string>(name)) + // tslint:disable-next-line:no-any + .returns(() => (sourceSettings as any)[name]); + } + for (const name of ['venvFolders']) { + config + .setup((c) => c.get<string[]>(name)) + // tslint:disable-next-line:no-any + .returns(() => (sourceSettings as any)[name]); + } + + // boolean settings + for (const name of ['downloadLanguageServer', 'autoUpdateLanguageServer']) { + config + .setup((c) => c.get<boolean>(name, true)) + // tslint:disable-next-line:no-any + .returns(() => (sourceSettings as any)[name]); + } + for (const name of ['disableInstallationCheck', 'globalModuleInstallation']) { + config + .setup((c) => c.get<boolean>(name)) + // tslint:disable-next-line:no-any + .returns(() => (sourceSettings as any)[name]); + } + + // number settings + config.setup((c) => c.get<number>('jediMemoryLimit')).returns(() => sourceSettings.jediMemoryLimit); + // Language server type settings + config.setup((c) => c.get<LanguageServerType>('languageServer')).returns(() => sourceSettings.languageServer); + + // "any" settings + // tslint:disable-next-line:no-any + config.setup((c) => c.get<any[]>('devOptions')).returns(() => sourceSettings.devOptions); + + // complex settings + config.setup((c) => c.get<ILoggingSettings>('logging')).returns(() => sourceSettings.logging); + config.setup((c) => c.get<ILintingSettings>('linting')).returns(() => sourceSettings.linting); + config.setup((c) => c.get<IAnalysisSettings>('analysis')).returns(() => sourceSettings.analysis); + config.setup((c) => c.get<ISortImportSettings>('sortImports')).returns(() => sourceSettings.sortImports); + config.setup((c) => c.get<IFormattingSettings>('formatting')).returns(() => sourceSettings.formatting); + config.setup((c) => c.get<IAutoCompleteSettings>('autoComplete')).returns(() => sourceSettings.autoComplete); + config + .setup((c) => c.get<IWorkspaceSymbolSettings>('workspaceSymbols')) + .returns(() => sourceSettings.workspaceSymbols); + config.setup((c) => c.get<ITestingSettings>('testing')).returns(() => sourceSettings.testing); + config.setup((c) => c.get<ITerminalSettings>('terminal')).returns(() => sourceSettings.terminal); + config.setup((c) => c.get<IDataScienceSettings>('dataScience')).returns(() => sourceSettings.datascience); + config.setup((c) => c.get<IExperiments>('experiments')).returns(() => sourceSettings.experiments); + } + + function testIfValueIsUpdated(settingName: string, value: any) { + test(`${settingName} updated`, async () => { + expected.pythonPath = 'python3'; + (expected as any)[settingName] = value; + initializeConfig(expected); + + settings.update(config.object); + + expect((settings as any)[settingName]).to.be.equal((expected as any)[settingName]); + config.verifyAll(); + }); + } + + suite('String settings', async () => { + [ + 'pythonPath', + 'venvPath', + 'condaPath', + 'pipenvPath', + 'envFile', + 'poetryPath', + 'insidersChannel', + 'defaultInterpreterPath' + ].forEach(async (settingName) => { + testIfValueIsUpdated(settingName, 'stringValue'); + }); + }); + + suite('Boolean settings', async () => { + ['downloadLanguageServer', 'autoUpdateLanguageServer', 'globalModuleInstallation'].forEach( + async (settingName) => { + testIfValueIsUpdated(settingName, true); + } + ); + }); + + suite('Number settings', async () => { + ['jediMemoryLimit'].forEach(async (settingName) => { + testIfValueIsUpdated(settingName, 1001); + }); + }); + + test('condaPath updated', () => { + expected.pythonPath = 'python3'; + expected.condaPath = 'spam'; + initializeConfig(expected); + config + .setup((c) => c.get<string>('condaPath')) + .returns(() => expected.condaPath) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + + expect(settings.condaPath).to.be.equal(expected.condaPath); + config.verifyAll(); + }); + + test('condaPath (relative to home) updated', async () => { + expected.pythonPath = 'python3'; + expected.condaPath = path.join('~', 'anaconda3', 'bin', 'conda'); + initializeConfig(expected); + config + .setup((c) => c.get<string>('condaPath')) + .returns(() => expected.condaPath) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + + expect(settings.condaPath).to.be.equal(untildify(expected.condaPath)); + config.verifyAll(); + }); + + function testLanguageServer(languageServer: LanguageServerType, expectedValue: LanguageServerType) { + test(languageServer, () => { + expected.pythonPath = 'python3'; + expected.languageServer = languageServer; + initializeConfig(expected); + config + .setup((c) => c.get<LanguageServerType>('languageServer')) + .returns(() => expected.languageServer) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + + expect(settings.languageServer).to.be.equal(expectedValue); + config.verifyAll(); + }); + } + + suite('languageServer settings', async () => { + Object.values(LanguageServerType).forEach(async (languageServer) => { + testLanguageServer(languageServer, languageServer); + }); + + testLanguageServer('invalid' as LanguageServerType, LanguageServerType.Jedi); + }); + + function testExperiments(enabled: boolean) { + expected.pythonPath = 'python3'; + // tslint:disable-next-line:no-any + expected.experiments = { + enabled, + optInto: [], + optOutFrom: [] + }; + initializeConfig(expected); + config + .setup((c) => c.get<IExperiments>('experiments')) + .returns(() => expected.experiments) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + + for (const key of Object.keys(expected.experiments)) { + // tslint:disable-next-line:no-any + expect((settings.experiments as any)[key]).to.be.deep.equal((expected.experiments as any)[key]); + } + config.verifyAll(); + } + test('Experiments (not enabled)', () => testExperiments(false)); + + test('Experiments (enabled)', () => testExperiments(true)); + + test('Formatter Paths and args', () => { + expected.pythonPath = 'python3'; + // tslint:disable-next-line:no-any + expected.formatting = { + autopep8Args: ['1', '2'], + autopep8Path: 'one', + blackArgs: ['3', '4'], + blackPath: 'two', + yapfArgs: ['5', '6'], + yapfPath: 'three', + provider: '' + }; + expected.formatting.blackPath = 'spam'; + initializeConfig(expected); + config + .setup((c) => c.get<IFormattingSettings>('formatting')) + .returns(() => expected.formatting) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + + for (const key of Object.keys(expected.formatting)) { + // tslint:disable-next-line:no-any + expect((settings.formatting as any)[key]).to.be.deep.equal((expected.formatting as any)[key]); + } + config.verifyAll(); + }); + test('Formatter Paths (paths relative to home)', () => { + expected.pythonPath = 'python3'; + // tslint:disable-next-line:no-any + expected.formatting = { + autopep8Args: [], + autopep8Path: path.join('~', 'one'), + blackArgs: [], + blackPath: path.join('~', 'two'), + yapfArgs: [], + yapfPath: path.join('~', 'three'), + provider: '' + }; + expected.formatting.blackPath = 'spam'; + initializeConfig(expected); + config + .setup((c) => c.get<IFormattingSettings>('formatting')) + .returns(() => expected.formatting) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + + for (const key of Object.keys(expected.formatting)) { + if (!key.endsWith('path')) { + continue; + } + // tslint:disable-next-line:no-any + const expectedPath = untildify((expected.formatting as any)[key]); + // tslint:disable-next-line:no-any + expect((settings.formatting as any)[key]).to.be.equal(expectedPath); + } + config.verifyAll(); + }); + + test('File env variables remain in settings', () => { + expected.datascience = { + allowImportFromNotebook: true, + alwaysTrustNotebooks: true, + jupyterLaunchTimeout: 20000, + jupyterLaunchRetries: 3, + enabled: true, + jupyterServerURI: 'local', + // tslint:disable-next-line: no-invalid-template-strings + notebookFileRoot: '${fileDirname}', + changeDirOnImportExport: true, + useDefaultConfigForJupyter: true, + jupyterInterruptTimeout: 10000, + searchForJupyter: true, + showCellInputCode: true, + collapseCellInputCodeByDefault: true, + allowInput: true, + maxOutputSize: 400, + enableScrollingForCellOutputs: true, + errorBackgroundColor: '#FFFFFF', + sendSelectionToInteractiveWindow: false, + variableExplorerExclude: 'module;function;builtin_function_or_method', + codeRegularExpression: '', + markdownRegularExpression: '', + enablePlotViewer: true, + runStartupCommands: '', + debugJustMyCode: true, + variableQueries: [], + jupyterCommandLineArguments: [], + widgetScriptSources: [], + interactiveWindowMode: 'single' + }; + expected.pythonPath = 'python3'; + // tslint:disable-next-line:no-any + expected.experiments = { + enabled: false, + optInto: [], + optOutFrom: [] + }; + initializeConfig(expected); + config + .setup((c) => c.get<IExperiments>('experiments')) + .returns(() => expected.experiments) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + assert.equal(expected.datascience.notebookFileRoot, settings.datascience.notebookFileRoot); + }); +}); diff --git a/src/test/common/configuration/service.test.ts b/src/test/common/configuration/service.test.ts new file mode 100644 index 000000000000..5bbd24a9fa8d --- /dev/null +++ b/src/test/common/configuration/service.test.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { expect } from 'chai'; +import { workspace } from 'vscode'; +import { IAsyncDisposableRegistry, IConfigurationService } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { getExtensionSettings } from '../../common'; +import { initialize } from '../../initialize'; + +// tslint:disable-next-line:max-func-body-length +suite('Configuration Service', () => { + let serviceContainer: IServiceContainer; + suiteSetup(async () => { + serviceContainer = (await initialize()).serviceContainer; + }); + + test('Ensure same instance of settings return', () => { + const workspaceUri = workspace.workspaceFolders![0].uri; + const settings = serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(workspaceUri); + const instanceIsSame = settings === getExtensionSettings(workspaceUri); + expect(instanceIsSame).to.be.equal(true, 'Incorrect settings'); + }); + + test('Ensure async registry works', async () => { + const asyncRegistry = serviceContainer.get<IAsyncDisposableRegistry>(IAsyncDisposableRegistry); + let disposed = false; + const disposable = { + dispose(): Promise<void> { + disposed = true; + return Promise.resolve(); + } + }; + asyncRegistry.push(disposable); + await asyncRegistry.dispose(); + expect(disposed).to.be.equal(true, "Didn't dispose during async registry cleanup"); + }); +}); diff --git a/src/test/common/configuration/service.unit.test.ts b/src/test/common/configuration/service.unit.test.ts new file mode 100644 index 000000000000..887a0779acf7 --- /dev/null +++ b/src/test/common/configuration/service.unit.test.ts @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { DeprecatePythonPath } from '../../../client/common/experiments/groups'; +import { IExperimentsManager, IInterpreterPathService } from '../../../client/common/types'; +import { + IInterpreterAutoSeletionProxyService, + IInterpreterSecurityService +} from '../../../client/interpreter/autoSelection/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Configuration Service', () => { + const resource = Uri.parse('a'); + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let interpreterPathService: TypeMoq.IMock<IInterpreterPathService>; + let experimentsManager: TypeMoq.IMock<IExperimentsManager>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let interpreterSecurityService: TypeMoq.IMock<IInterpreterSecurityService>; + let configService: ConfigurationService; + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + interpreterSecurityService = TypeMoq.Mock.ofType<IInterpreterSecurityService>(); + workspaceService + .setup((w) => w.getWorkspaceFolder(resource)) + .returns(() => ({ + uri: resource, + index: 0, + name: '0' + })); + interpreterPathService = TypeMoq.Mock.ofType<IInterpreterPathService>(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + experimentsManager = TypeMoq.Mock.ofType<IExperimentsManager>(); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + serviceContainer.setup((s) => s.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); + serviceContainer.setup((s) => s.get(IExperimentsManager)).returns(() => experimentsManager.object); + configService = new ConfigurationService(serviceContainer.object); + }); + + function setupConfigProvider(): TypeMoq.IMock<WorkspaceConfiguration> { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) + .returns(() => workspaceConfig.object); + return workspaceConfig; + } + + test('Fetching settings goes as expected', () => { + const interpreterAutoSelectionProxyService = TypeMoq.Mock.ofType<IInterpreterAutoSeletionProxyService>(); + serviceContainer + .setup((s) => s.get(IInterpreterSecurityService)) + .returns(() => interpreterSecurityService.object) + .verifiable(TypeMoq.Times.once()); + serviceContainer + .setup((s) => s.get(IInterpreterAutoSeletionProxyService)) + .returns(() => interpreterAutoSelectionProxyService.object) + .verifiable(TypeMoq.Times.once()); + const settings = configService.getSettings(); + expect(settings).to.be.instanceOf(PythonSettings); + }); + + test('Do not update global settings if global value is already equal to the new value', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig.setup((w) => w.inspect('setting')).returns(() => ({ globalValue: 'globalValue' } as any)); + workspaceConfig + .setup((w) => w.update('setting', 'globalValue', ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await configService.updateSetting('setting', 'globalValue', resource, ConfigurationTarget.Global); + + workspaceConfig.verifyAll(); + }); + + test('Update global settings if global value is not equal to the new value', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig.setup((w) => w.inspect('setting')).returns(() => ({ globalValue: 'globalValue' } as any)); + workspaceConfig + .setup((w) => w.update('setting', 'newGlobalValue', ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await configService.updateSetting('setting', 'newGlobalValue', resource, ConfigurationTarget.Global); + + workspaceConfig.verifyAll(); + }); + + test('Do not update workspace settings if workspace value is already equal to the new value', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig.setup((w) => w.inspect('setting')).returns(() => ({ workspaceValue: 'workspaceValue' } as any)); + workspaceConfig + .setup((w) => w.update('setting', 'workspaceValue', ConfigurationTarget.Workspace)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await configService.updateSetting('setting', 'workspaceValue', resource, ConfigurationTarget.Workspace); + + workspaceConfig.verifyAll(); + }); + + test('Update workspace settings if workspace value is not equal to the new value', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig.setup((w) => w.inspect('setting')).returns(() => ({ workspaceValue: 'workspaceValue' } as any)); + workspaceConfig + .setup((w) => w.update('setting', 'newWorkspaceValue', ConfigurationTarget.Workspace)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await configService.updateSetting('setting', 'newWorkspaceValue', resource, ConfigurationTarget.Workspace); + + workspaceConfig.verifyAll(); + }); + + test('Do not update workspace folder settings if workspace folder value is already equal to the new value', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + workspaceConfig + .setup((w) => w.inspect('setting')) + // tslint:disable-next-line: no-any + .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue' } as any)); + workspaceConfig + .setup((w) => w.update('setting', 'workspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await configService.updateSetting( + 'setting', + 'workspaceFolderValue', + resource, + ConfigurationTarget.WorkspaceFolder + ); + + workspaceConfig.verifyAll(); + }); + + test('Update workspace folder settings if workspace folder value is not equal to the new value', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + workspaceConfig + .setup((w) => w.inspect('setting')) + // tslint:disable-next-line: no-any + .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue' } as any)); + workspaceConfig + .setup((w) => w.update('setting', 'newWorkspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await configService.updateSetting( + 'setting', + 'newWorkspaceFolderValue', + resource, + ConfigurationTarget.WorkspaceFolder + ); + + workspaceConfig.verifyAll(); + }); + + test('If in Deprecate PythonPath experiment & setting to update is `python.pythonPath`, update settings using new API if stored value is not equal to the new value', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + interpreterPathService + .setup((w) => w.inspect(resource)) + // tslint:disable-next-line: no-any + .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue' } as any)); + interpreterPathService + .setup((w) => w.update(resource, ConfigurationTarget.WorkspaceFolder, 'newWorkspaceFolderValue')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await configService.updateSetting( + 'pythonPath', + 'newWorkspaceFolderValue', + resource, + ConfigurationTarget.WorkspaceFolder + ); + + interpreterPathService.verifyAll(); + }); +}); diff --git a/src/test/common/crypto.unit.test.ts b/src/test/common/crypto.unit.test.ts new file mode 100644 index 000000000000..878e2b1ff0d4 --- /dev/null +++ b/src/test/common/crypto.unit.test.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { CryptoUtils } from '../../client/common/crypto'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; + +const RANDOM_WORDS = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'common', 'randomWords.txt'); + +// tslint:disable-next-line: max-func-body-length +suite('Crypto Utils', async () => { + let crypto: CryptoUtils; + let wordsText: string; + suiteSetup(async () => { + wordsText = await fs.readFile(RANDOM_WORDS, 'utf8'); + }); + setup(() => { + crypto = new CryptoUtils(); + }); + async function getRandomWords(): Promise<string[]> { + return wordsText.split('\n'); + } + + test('If hashFormat equals `number`, method createHash() returns a number', async () => { + const hash = crypto.createHash('blabla', 'number'); + assert.typeOf(hash, 'number', 'Type should be a number'); + }); + test('If hashFormat equals `string`, method createHash() returns a string', async () => { + const hash = crypto.createHash('blabla', 'string'); + assert.typeOf(hash, 'string', 'Type should be a string'); + }); + test('Hashes must be same for same strings (sha256)', async () => { + const hash1 = crypto.createHash('blabla', 'string', 'SHA256'); + const hash2 = crypto.createHash('blabla', 'string', 'SHA256'); + assert.equal(hash1, hash2); + }); + test('Hashes must be different for different strings (sha256)', async () => { + const hash1 = crypto.createHash('Hello', 'string', 'SHA256'); + const hash2 = crypto.createHash('World', 'string', 'SHA256'); + assert.notEqual(hash1, hash2); + }); + test('If hashFormat equals `number`, the hash should not be NaN', async () => { + let hash = crypto.createHash('test', 'number'); + assert.isNotNaN(hash, 'Number hash should not be NaN'); + hash = crypto.createHash('hash', 'number'); + assert.isNotNaN(hash, 'Number hash should not be NaN'); + hash = crypto.createHash('HASH1', 'number'); + assert.isNotNaN(hash, 'Number hash should not be NaN'); + }); + test('If hashFormat equals `string`, the hash should not be undefined', async () => { + let hash = crypto.createHash('test', 'string'); + assert.isDefined(hash, 'String hash should not be undefined'); + hash = crypto.createHash('hash', 'string'); + assert.isDefined(hash, 'String hash should not be undefined'); + hash = crypto.createHash('HASH1', 'string'); + assert.isDefined(hash, 'String hash should not be undefined'); + }); + test('If hashFormat equals `number`, hashes with different data should return different number hashes', async () => { + const hash1 = crypto.createHash('hash1', 'number'); + const hash2 = crypto.createHash('hash2', 'number'); + assert.notEqual(hash1, hash2, 'Hashes should be different numbers'); + }); + test('If hashFormat equals `string`, hashes with different data should return different string hashes', async () => { + const hash1 = crypto.createHash('hash1', 'string'); + const hash2 = crypto.createHash('hash2', 'string'); + assert.notEqual(hash1, hash2, 'Hashes should be different strings'); + }); + test('If hashFormat equals `number`, ensure numbers are uniformly distributed on scale from 0 to 100', async () => { + const wordList = await getRandomWords(); + const buckets: number[] = Array(100).fill(0); + const hashes = Array(10).fill(0); + for (const w of wordList) { + for (let i = 0; i < 10; i += 1) { + const word = `${w}${i}`; + const hash = crypto.createHash(word, 'number'); + buckets[hash % 100] += 1; + hashes[i] = hash % 100; + } + } + // Total number of words = wordList.length * 10, because we added ten variants of each word above. + const expectedHitsPerBucket = (wordList.length * 10) / 100; + for (const hit of buckets) { + expect(hit).to.be.lessThan(1.25 * expectedHitsPerBucket); + expect(hit).to.be.greaterThan(0.75 * expectedHitsPerBucket); + } + }); + test('If hashFormat equals `number`, on a scale of 0 to 100, small difference in the input on average produce large differences (about 33) in the output ', async () => { + const wordList = await getRandomWords(); + const buckets: number[] = Array(100).fill(0); + let hashes: number[] = []; + let totalDifference = 0; + // We are only iterating over the first 10 words for purposes of this test + for (const w of wordList.slice(0, 10)) { + hashes = []; + totalDifference = 0; + if (w.length === 0) { + continue; + } + for (let i = 0; i < 10; i += 1) { + const word = `${w}${i}`; + const hash = crypto.createHash(word, 'number'); + buckets[hash % 100] += 1; + hashes.push(hash % 100); + } + for (let i = 0; i < 10; i += 1) { + const word = `${i}${w}`; + const hash = crypto.createHash(word, 'number'); + buckets[hash % 100] += 1; + hashes.push(hash % 100); + } + // Iterating over ASCII alphabets 'a' to 'z' and appending to the word + for (let i = 0; i < 26; i += 1) { + const word = `${String.fromCharCode(97 + i)}${w}`; + const hash = crypto.createHash(word, 'number'); + buckets[hash % 100] += 1; + hashes.push(hash % 100); + } + // Iterating over ASCII alphabets 'a' to 'z' and prepending to the word + for (let i = 0; i < 26; i += 1) { + const word = `${w}${String.fromCharCode(97 + i)}`; + const hash = crypto.createHash(word, 'number'); + buckets[hash % 100] += 1; + hashes.push(hash % 100); + } + // tslint:disable: prefer-for-of + for (let i = 0; i < hashes.length; i += 1) { + for (let j = 0; j < hashes.length; j += 1) { + if (hashes[i] > hashes[j]) { + totalDifference += hashes[i] - hashes[j]; + } else { + totalDifference += hashes[j] - hashes[i]; + } + } + } + const averageDifference = totalDifference / hashes.length / hashes.length; + expect(averageDifference).to.be.lessThan(1.25 * 33); + expect(averageDifference).to.be.greaterThan(0.75 * 33); + } + }); +}); diff --git a/src/test/common/dotnet/compatibilityService.unit.test.ts b/src/test/common/dotnet/compatibilityService.unit.test.ts new file mode 100644 index 000000000000..eb6289ec41c5 --- /dev/null +++ b/src/test/common/dotnet/compatibilityService.unit.test.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { instance, mock, when } from 'ts-mockito'; +import { DotNetCompatibilityService } from '../../../client/common/dotnet/compatibilityService'; +import { UnknownOSDotNetCompatibilityService } from '../../../client/common/dotnet/services/unknownOsCompatibilityService'; +import { IOSDotNetCompatibilityService } from '../../../client/common/dotnet/types'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { OSType } from '../../../client/common/utils/platform'; + +suite('DOT.NET', () => { + getNamesAndValues<OSType>(OSType).forEach((osType) => { + [true, false].forEach((supported) => { + test(`Test ${osType.name} support = ${supported}`, async () => { + const unknownService = mock(UnknownOSDotNetCompatibilityService); + const macService = mock(UnknownOSDotNetCompatibilityService); + const winService = mock(UnknownOSDotNetCompatibilityService); + const linuxService = mock(UnknownOSDotNetCompatibilityService); + const platformService = mock(PlatformService); + + const mappedServices = new Map<OSType, IOSDotNetCompatibilityService>(); + mappedServices.set(OSType.Unknown, unknownService); + mappedServices.set(OSType.OSX, macService); + mappedServices.set(OSType.Windows, winService); + mappedServices.set(OSType.Linux, linuxService); + + const service = new DotNetCompatibilityService( + instance(unknownService), + instance(macService), + instance(winService), + instance(linuxService), + instance(platformService) + ); + + when(platformService.osType).thenReturn(osType.value); + const osService = mappedServices.get(osType.value)!; + when(osService.isSupported()).thenResolve(supported); + + const result = await service.isSupported(); + expect(result).to.be.equal(supported, 'Invalid value'); + }); + }); + }); +}); diff --git a/src/test/common/dotnet/serviceRegistry.unit.test.ts b/src/test/common/dotnet/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..d03d6113d7cc --- /dev/null +++ b/src/test/common/dotnet/serviceRegistry.unit.test.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { DotNetCompatibilityService } from '../../../client/common/dotnet/compatibilityService'; +import { registerTypes } from '../../../client/common/dotnet/serviceRegistry'; +import { LinuxDotNetCompatibilityService } from '../../../client/common/dotnet/services/linuxCompatibilityService'; +import { MacDotNetCompatibilityService } from '../../../client/common/dotnet/services/macCompatibilityService'; +import { UnknownOSDotNetCompatibilityService } from '../../../client/common/dotnet/services/unknownOsCompatibilityService'; +import { WindowsDotNetCompatibilityService } from '../../../client/common/dotnet/services/windowsCompatibilityService'; +import { IDotNetCompatibilityService, IOSDotNetCompatibilityService } from '../../../client/common/dotnet/types'; +import { OSType } from '../../../client/common/utils/platform'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Common Dotnet Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify( + serviceManager.addSingleton<IDotNetCompatibilityService>( + IDotNetCompatibilityService, + DotNetCompatibilityService + ) + ).once(); + verify( + serviceManager.addSingleton<IOSDotNetCompatibilityService>( + IOSDotNetCompatibilityService, + MacDotNetCompatibilityService, + OSType.OSX + ) + ).once(); + verify( + serviceManager.addSingleton<IOSDotNetCompatibilityService>( + IOSDotNetCompatibilityService, + WindowsDotNetCompatibilityService, + OSType.Windows + ) + ).once(); + verify( + serviceManager.addSingleton<IOSDotNetCompatibilityService>( + IOSDotNetCompatibilityService, + LinuxDotNetCompatibilityService, + OSType.Linux + ) + ).once(); + verify( + serviceManager.addSingleton<IOSDotNetCompatibilityService>( + IOSDotNetCompatibilityService, + UnknownOSDotNetCompatibilityService, + OSType.Unknown + ) + ).once(); + }); +}); diff --git a/src/test/common/dotnet/services/linuxCompatibilityService.unit.test.ts b/src/test/common/dotnet/services/linuxCompatibilityService.unit.test.ts new file mode 100644 index 000000000000..4e945d865edf --- /dev/null +++ b/src/test/common/dotnet/services/linuxCompatibilityService.unit.test.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { instance, mock, when } from 'ts-mockito'; +import { LinuxDotNetCompatibilityService } from '../../../../client/common/dotnet/services/linuxCompatibilityService'; +import { PlatformService } from '../../../../client/common/platform/platformService'; + +suite('DOT.NET', () => { + suite('Linux', () => { + async function testSupport(expectedValueForIsSupported: boolean, is64Bit: boolean) { + const platformService = mock(PlatformService); + const service = new LinuxDotNetCompatibilityService(instance(platformService)); + + when(platformService.is64bit).thenReturn(is64Bit); + + const result = await service.isSupported(); + expect(result).to.be.equal(expectedValueForIsSupported, 'Invalid value'); + } + test('Linux 64 bit is supported', async () => { + await testSupport(true, true); + }); + test('Linux 64 bit is not supported', async () => { + await testSupport(false, false); + }); + }); +}); diff --git a/src/test/common/dotnet/services/macCompatibilityService.unit.test.ts b/src/test/common/dotnet/services/macCompatibilityService.unit.test.ts new file mode 100644 index 000000000000..ed3f9557101f --- /dev/null +++ b/src/test/common/dotnet/services/macCompatibilityService.unit.test.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { instance, mock, when } from 'ts-mockito'; +import { MacDotNetCompatibilityService } from '../../../../client/common/dotnet/services/macCompatibilityService'; +import { PlatformService } from '../../../../client/common/platform/platformService'; + +suite('DOT.NET', () => { + suite('Mac', () => { + async function testSupport(version: string, expectedValueForIsSupported: boolean) { + const platformService = mock(PlatformService); + const service = new MacDotNetCompatibilityService(instance(platformService)); + + when(platformService.getVersion()).thenResolve(new SemVer(version)); + + const result = await service.isSupported(); + expect(result).to.be.equal(expectedValueForIsSupported, 'Invalid value'); + } + test('Supported on 16.0.0', () => testSupport('16.0.0', true)); + test('Supported on 16.0.0', () => testSupport('16.0.1', true)); + test('Supported on 16.0.0', () => testSupport('16.1.0', true)); + test('Supported on 16.0.0', () => testSupport('17.0.0', true)); + + test('Supported on 16.0.0', () => testSupport('15.0.0', false)); + test('Supported on 16.0.0', () => testSupport('15.9.9', false)); + test('Supported on 16.0.0', () => testSupport('14.0.0', false)); + test('Supported on 16.0.0', () => testSupport('10.12.0', false)); + }); +}); diff --git a/src/test/common/dotnet/services/unknownOsCompatibilityService.unit.test.ts b/src/test/common/dotnet/services/unknownOsCompatibilityService.unit.test.ts new file mode 100644 index 000000000000..7ab997b440b5 --- /dev/null +++ b/src/test/common/dotnet/services/unknownOsCompatibilityService.unit.test.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { UnknownOSDotNetCompatibilityService } from '../../../../client/common/dotnet/services/unknownOsCompatibilityService'; + +suite('DOT.NET', () => { + suite('Unknown', () => { + test('Not supported', async () => { + const service = new UnknownOSDotNetCompatibilityService(); + const result = await service.isSupported(); + expect(result).to.be.equal(false, 'Invalid value'); + }); + }); +}); diff --git a/src/test/common/dotnet/services/winCompatibilityService.unit.test.ts b/src/test/common/dotnet/services/winCompatibilityService.unit.test.ts new file mode 100644 index 000000000000..bf7455b16708 --- /dev/null +++ b/src/test/common/dotnet/services/winCompatibilityService.unit.test.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { WindowsDotNetCompatibilityService } from '../../../../client/common/dotnet/services/windowsCompatibilityService'; + +suite('DOT.NET', () => { + suite('Windows', () => { + test('Windows is Supported', async () => { + const service = new WindowsDotNetCompatibilityService(); + const result = await service.isSupported(); + expect(result).to.be.equal(true, 'Invalid value'); + }); + }); +}); diff --git a/src/test/common/exitCIAfterTestReporter.ts b/src/test/common/exitCIAfterTestReporter.ts new file mode 100644 index 000000000000..602d6eefd1ed --- /dev/null +++ b/src/test/common/exitCIAfterTestReporter.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// Custom reporter to ensure Mocha process exits when we're done with tests. +// This is a hack, however for some reason the process running the tests do not exit. +// The hack is to force it to die when tests are done, if this doesn't work we've got a bigger problem on our hands. + +// tslint:disable:no-var-requires no-require-imports no-any no-console no-unnecessary-class no-default-export +import * as fs from 'fs-extra'; +import * as net from 'net'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; +import { noop } from '../core'; + +let client: net.Socket | undefined; +const mochaTests: any = require('mocha'); +const { EVENT_RUN_BEGIN, EVENT_RUN_END } = mochaTests.Runner.constants; + +async function connectToServer() { + const portFile = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'port.txt'); + if (!(await fs.pathExists(portFile))) { + return; + } + const port = parseInt(await fs.readFile(portFile, 'utf-8'), 10); + console.log(`Need to connect to port ${port}`); + return new Promise((resolve) => { + try { + client = new net.Socket(); + client.connect({ port }, () => { + console.log(`Connected to port ${port}`); + resolve(); + }); + } catch { + console.error('Failed to connect to socket server to notify completion of tests'); + resolve(); + } + }); +} +function notifyCompleted(hasFailures: boolean) { + if (!client || client.destroyed || !client.writable) { + console.error('No client to write from'); + return; + } + try { + const exitCode = hasFailures ? 1 : 0; + console.log(`Notify server of test completion with code ${exitCode}`); + // If there are failures, send a code of 1 else 0. + client.write(exitCode.toString()); + client.end(); + console.log('Notified server of test completion'); + } catch (ex) { + console.error('Socket client error', ex); + } +} + +class ExitReporter { + constructor(runner: any) { + console.log('Initialize Exit Reporter for Mocha (PVSC).'); + connectToServer().catch(noop); + const stats = runner.stats; + runner + .once(EVENT_RUN_BEGIN, () => { + console.info('Start Exit Reporter for Mocha.'); + }) + .once(EVENT_RUN_END, async () => { + notifyCompleted(stats.failures > 0); + console.info('End Exit Reporter for Mocha.'); + }); + } +} + +module.exports = ExitReporter; diff --git a/src/test/common/experiments/manager.unit.test.ts b/src/test/common/experiments/manager.unit.test.ts new file mode 100644 index 000000000000..aa5801464f29 --- /dev/null +++ b/src/test/common/experiments/manager.unit.test.ts @@ -0,0 +1,1083 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ApplicationEnvironment } from '../../../client/common/application/applicationEnvironment'; +import { IApplicationEnvironment } from '../../../client/common/application/types'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { CryptoUtils } from '../../../client/common/crypto'; +import { NotebookEditorSupport } from '../../../client/common/experiments/groups'; +import { + configUri, + downloadedExperimentStorageKey, + ExperimentsManager, + experimentStorageKey, + isDownloadedStorageValidKey, + oldExperimentSalts +} from '../../../client/common/experiments/manager'; +import { HttpClient } from '../../../client/common/net/httpClient'; +import { PersistentStateFactory } from '../../../client/common/persistentState'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { + ICryptoUtils, + IExperiments, + IHttpClient, + IOutputChannel, + IPersistentState, + IPersistentStateFactory +} from '../../../client/common/types'; +import { createDeferred, createDeferredFromPromise } from '../../../client/common/utils/async'; +import { sleep } from '../../common'; +import { noop } from '../../core'; + +// tslint:disable: max-func-body-length + +suite('A/B experiments', () => { + let httpClient: IHttpClient; + let crypto: ICryptoUtils; + let appEnvironment: IApplicationEnvironment; + let persistentStateFactory: IPersistentStateFactory; + let isDownloadedStorageValid: TypeMoq.IMock<IPersistentState<boolean>>; + let experimentStorage: TypeMoq.IMock<IPersistentState<any>>; + let downloadedExperimentsStorage: TypeMoq.IMock<IPersistentState<any>>; + let output: TypeMoq.IMock<IOutputChannel>; + let fs: IFileSystem; + let expManager: ExperimentsManager; + let configurationService: ConfigurationService; + let experiments: TypeMoq.IMock<IExperiments>; + setup(() => { + httpClient = mock(HttpClient); + crypto = mock(CryptoUtils); + appEnvironment = mock(ApplicationEnvironment); + persistentStateFactory = mock(PersistentStateFactory); + isDownloadedStorageValid = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + experimentStorage = TypeMoq.Mock.ofType<IPersistentState<any>>(); + downloadedExperimentsStorage = TypeMoq.Mock.ofType<IPersistentState<any>>(); + output = TypeMoq.Mock.ofType<IOutputChannel>(); + configurationService = mock(ConfigurationService); + experiments = TypeMoq.Mock.ofType<IExperiments>(); + const settings = mock(PythonSettings); + when(settings.experiments).thenReturn(experiments.object); + experiments.setup((e) => e.optInto).returns(() => []); + experiments.setup((e) => e.optOutFrom).returns(() => []); + when(configurationService.getSettings(undefined)).thenReturn(instance(settings)); + fs = mock(FileSystem); + when( + persistentStateFactory.createGlobalPersistentState(isDownloadedStorageValidKey, false, anything()) + ).thenReturn(isDownloadedStorageValid.object); + when(persistentStateFactory.createGlobalPersistentState(experimentStorageKey, undefined as any)).thenReturn( + experimentStorage.object + ); + when( + persistentStateFactory.createGlobalPersistentState(downloadedExperimentStorageKey, undefined as any) + ).thenReturn(downloadedExperimentsStorage.object); + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService) + ); + }); + + teardown(() => { + sinon.restore(); + }); + + async function testInitialization( + downloadError: boolean = false, + experimentsDownloaded: any = [{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }] + ) { + if (downloadError) { + when(httpClient.getJSON(configUri, false)).thenReject(new Error('Kaboom')); + } else { + when(httpClient.getJSON(configUri, false)).thenResolve(experimentsDownloaded); + } + + try { + await expManager.initializeInBackground(); + // tslint:disable-next-line:no-empty + } catch {} + + isDownloadedStorageValid.verifyAll(); + } + + test('Initializing experiments does not download experiments if storage is valid and contains experiments', async () => { + isDownloadedStorageValid + .setup((n) => n.value) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + + await testInitialization(); + + verify(httpClient.getJSON(configUri, false)).never(); + }); + + test('If storage has expired, initializing experiments downloads the experiments, but does not store them if they are invalid or incomplete', async () => { + const abExperiments = [{ name: 'experiment1', salt: 'salt', max: 100 }]; + isDownloadedStorageValid + .setup((n) => n.value) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + isDownloadedStorageValid + .setup((n) => n.updateValue(true)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + downloadedExperimentsStorage + .setup((n) => n.updateValue(abExperiments)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + // downloadError = false, experimentsDownloaded = experiments + await testInitialization(false, abExperiments); + + verify(httpClient.getJSON(configUri, false)).once(); + }); + + test('If storage has expired, initializing experiments downloads the experiments, and stores them if they are valid', async () => { + isDownloadedStorageValid + .setup((n) => n.value) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + isDownloadedStorageValid + .setup((n) => n.updateValue(true)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + downloadedExperimentsStorage + .setup((n) => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + await testInitialization(); + + verify(httpClient.getJSON(configUri, false)).once(); + }); + + test('If downloading experiments fails with error, the storage is left as it is', async () => { + isDownloadedStorageValid + .setup((n) => n.value) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + isDownloadedStorageValid + .setup((n) => n.updateValue(true)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + downloadedExperimentsStorage + .setup((n) => n.updateValue(anything())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + // downloadError = true + await testInitialization(true); + + verify(httpClient.getJSON(configUri, false)).once(); + }); + + async function testEnablingExperiments(enabled: boolean) { + const updateExperimentStorage = sinon.stub(ExperimentsManager.prototype, 'updateExperimentStorage'); + updateExperimentStorage.callsFake(() => Promise.resolve()); + const populateUserExperiments = sinon.stub(ExperimentsManager.prototype, 'populateUserExperiments'); + populateUserExperiments.callsFake(() => Promise.resolve()); + const initializeInBackground = sinon.stub(ExperimentsManager.prototype, 'initializeInBackground'); + initializeInBackground.callsFake(() => Promise.resolve()); + experiments + .setup((e) => e.enabled) + .returns(() => enabled) + .verifiable(TypeMoq.Times.atLeastOnce()); + + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService) + ); + await expManager.activate(); + + // If experiments are disabled, then none of these methods will be invoked & vice versa. + assert.equal(updateExperimentStorage.callCount, enabled ? 1 : 0); + assert.equal(populateUserExperiments.callCount, enabled ? 1 : 0); + assert.equal(initializeInBackground.callCount, enabled ? 1 : 0); + + experiments.verifyAll(); + } + test('Ensure experiments are not initialized when it is disabled', async () => testEnablingExperiments(false)); + + test('Ensure experiments are initialized when it is enabled', async () => testEnablingExperiments(true)); + + async function testEnablingExperimentsToCheckIfInExperiment(enabled: boolean) { + const sendTelemetry = sinon.stub(ExperimentsManager.prototype, 'sendTelemetryIfInExperiment'); + sendTelemetry.callsFake((_: string) => noop()); + + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService) + ); + + expManager._enabled = enabled; + expManager.userExperiments.push({ name: 'this should be in experiment', max: 0, min: 0, salt: '' }); + + // If experiments are disabled, then `inExperiment` will return false & vice versa. + assert.equal(expManager.inExperiment('this should be in experiment'), enabled); + // This experiment does not exist, hence `inExperiment` will always be `false` for this. + assert.equal(expManager.inExperiment('this should never be in experiment'), false); + + experiments.verifyAll(); + } + test('Ensure inExperiment is true when experiments are enabled', async () => + testEnablingExperimentsToCheckIfInExperiment(true)); + + test('Ensure inExperiment is false when experiments are disabled', async () => + testEnablingExperimentsToCheckIfInExperiment(false)); + + test('Ensure experiments can only be activated once', async () => { + const updateExperimentStorage = sinon.stub(ExperimentsManager.prototype, 'updateExperimentStorage'); + updateExperimentStorage.callsFake(() => Promise.resolve()); + const populateUserExperiments = sinon.stub(ExperimentsManager.prototype, 'populateUserExperiments'); + populateUserExperiments.callsFake(() => Promise.resolve()); + const initializeInBackground = sinon.stub(ExperimentsManager.prototype, 'initializeInBackground'); + initializeInBackground.callsFake(() => Promise.resolve()); + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService) + ); + + assert.isFalse(expManager._activated()); + await expManager.activate(); + + // Ensure activated flag is set + assert.isTrue(expManager._activated()); + }); + + test('Ensure experiments are reliably downloaded in the background', async () => { + const experimentsDeferred = createDeferred<void>(); + const updateExperimentStorage = sinon.stub(ExperimentsManager.prototype, 'updateExperimentStorage'); + updateExperimentStorage.callsFake(() => Promise.resolve()); + const populateUserExperiments = sinon.stub(ExperimentsManager.prototype, 'populateUserExperiments'); + populateUserExperiments.callsFake(() => Promise.resolve()); + const initializeInBackground = sinon.stub(ExperimentsManager.prototype, 'initializeInBackground'); + initializeInBackground.callsFake(() => experimentsDeferred.promise); + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService) + ); + + const promise = expManager.activate(); + const deferred = createDeferredFromPromise(promise); + await sleep(1); + + // Ensure activate() function has completed while experiments are still being downloaded + assert.equal(deferred.completed, true); + + experimentsDeferred.resolve(); + await sleep(1); + + assert.ok(initializeInBackground.calledOnce); + }); + + test('Ensure experiment storage is updated to contain the latest downloaded experiments', async () => { + downloadedExperimentsStorage + .setup((n) => n.value) + .returns(() => [{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }]) + .verifiable(TypeMoq.Times.atLeastOnce()); + downloadedExperimentsStorage + .setup((n) => n.updateValue(undefined)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + experimentStorage + .setup((n) => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + await expManager.updateExperimentStorage(); + + experimentStorage.verifyAll(); + downloadedExperimentsStorage.verifyAll(); + }); + + test('When latest experiments are not available, but experiment storage contains experiments, then experiment storage is not updated', async () => { + const doBestEffortToPopulateExperiments = sinon.stub( + ExperimentsManager.prototype, + 'doBestEffortToPopulateExperiments' + ); + doBestEffortToPopulateExperiments.callsFake(() => Promise.resolve(false)); + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService) + ); + + downloadedExperimentsStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + downloadedExperimentsStorage + .setup((n) => n.updateValue(undefined)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + experimentStorage + .setup((n) => n.value) + .returns(() => [{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }]) + .verifiable(TypeMoq.Times.once()); + experimentStorage + .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + await expManager.updateExperimentStorage(); + + assert.ok(doBestEffortToPopulateExperiments.notCalled); + experimentStorage.verifyAll(); + downloadedExperimentsStorage.verifyAll(); + }); + + test('When best effort to populate experiments succeeds, function updateStorage() returns', async () => { + const doBestEffortToPopulateExperiments = sinon.stub( + ExperimentsManager.prototype, + 'doBestEffortToPopulateExperiments' + ); + doBestEffortToPopulateExperiments.callsFake(() => Promise.resolve(true)); + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService) + ); + + downloadedExperimentsStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + downloadedExperimentsStorage + .setup((n) => n.updateValue(undefined)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + experimentStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + await expManager.updateExperimentStorage(); + + assert.ok(doBestEffortToPopulateExperiments.calledOnce); + experimentStorage.verifyAll(); + downloadedExperimentsStorage.verifyAll(); + }); + + test('When latest experiments are not available, experiment storage is empty, but if local experiments file is not valid, experiment storage is not updated', async () => { + const doBestEffortToPopulateExperiments = sinon.stub( + ExperimentsManager.prototype, + 'doBestEffortToPopulateExperiments' + ); + doBestEffortToPopulateExperiments.callsFake(() => Promise.resolve(false)); + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService) + ); + downloadedExperimentsStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + downloadedExperimentsStorage + .setup((n) => n.updateValue(undefined)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + // tslint:disable-next-line:no-multiline-string + const fileContent = ` + // Yo! I am a JSON file with comments as well as trailing commas! + + [{ "name": "experiment1", "salt": "salt", "min": 90, },] + `; + + when(fs.fileExists(anything())).thenResolve(true); + when(fs.readFile(anything())).thenResolve(fileContent); + + experimentStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + experimentStorage + .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + await expManager.updateExperimentStorage(); + + verify(fs.fileExists(anything())).once(); + verify(fs.readFile(anything())).once(); + experimentStorage.verifyAll(); + downloadedExperimentsStorage.verifyAll(); + }); + + test('When latest experiments are not available, and experiment storage is empty, then experiment storage is updated using local experiments file given experiments are valid', async () => { + const doBestEffortToPopulateExperiments = sinon.stub( + ExperimentsManager.prototype, + 'doBestEffortToPopulateExperiments' + ); + doBestEffortToPopulateExperiments.callsFake(() => Promise.resolve(false)); + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService) + ); + downloadedExperimentsStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + downloadedExperimentsStorage + .setup((n) => n.updateValue(undefined)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + // tslint:disable-next-line:no-multiline-string + const fileContent = ` + // Yo! I am a JSON file with comments as well as trailing commas! + + [{ "name": "experiment1", "salt": "salt", "min": 90, "max": 100, },] + `; + + when(fs.fileExists(anything())).thenResolve(true); + when(fs.readFile(anything())).thenResolve(fileContent); + + experimentStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + experimentStorage + .setup((n) => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + await expManager.updateExperimentStorage(); + + verify(fs.fileExists(anything())).once(); + verify(fs.readFile(anything())).once(); + experimentStorage.verifyAll(); + downloadedExperimentsStorage.verifyAll(); + }); + + suite( + 'When latest experiments are not available, and experiment storage is empty, then function updateExperimentStorage() stops execution and returns', + () => { + setup(() => { + const doBestEffortToPopulateExperiments = sinon.stub( + ExperimentsManager.prototype, + 'doBestEffortToPopulateExperiments' + ); + doBestEffortToPopulateExperiments.callsFake(() => Promise.resolve(false)); + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService) + ); + }); + test('If checking the existence of config file fails', async () => { + downloadedExperimentsStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + downloadedExperimentsStorage + .setup((n) => n.updateValue(undefined)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + const error = new Error('Kaboom'); + when(fs.fileExists(anything())).thenThrow(error); + when(fs.readFile(anything())).thenResolve('fileContent'); + + experimentStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + experimentStorage + .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + await expManager.updateExperimentStorage(); + + verify(fs.fileExists(anything())).once(); + verify(fs.readFile(anything())).never(); + experimentStorage.verifyAll(); + downloadedExperimentsStorage.verifyAll(); + }); + + test('If reading config file fails', async () => { + downloadedExperimentsStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + downloadedExperimentsStorage + .setup((n) => n.updateValue(undefined)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + const error = new Error('Kaboom'); + when(fs.fileExists(anything())).thenResolve(true); + when(fs.readFile(anything())).thenThrow(error); + + experimentStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + experimentStorage + .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + await expManager.updateExperimentStorage(); + + verify(fs.fileExists(anything())).once(); + verify(fs.readFile(anything())).once(); + experimentStorage.verifyAll(); + downloadedExperimentsStorage.verifyAll(); + }); + + test('If config file does not exist', async () => { + downloadedExperimentsStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + downloadedExperimentsStorage + .setup((n) => n.updateValue(undefined)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + when(fs.fileExists(anything())).thenResolve(false); + when(fs.readFile(anything())).thenResolve('fileContent'); + + experimentStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + experimentStorage + .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + await expManager.updateExperimentStorage(); + + verify(fs.fileExists(anything())).once(); + verify(fs.readFile(anything())).never(); + experimentStorage.verifyAll(); + downloadedExperimentsStorage.verifyAll(); + }); + + test('If parsing file or updating storage fails', async () => { + downloadedExperimentsStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + downloadedExperimentsStorage + .setup((n) => n.updateValue(undefined)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + // tslint:disable-next-line:no-multiline-string + const fileContent = ` + // Yo! I am a JSON file with comments as well as trailing commas! + + [{ "name": "experiment1", "salt": "salt", "min": 90, "max": 100 },] + `; + const error = new Error('Kaboom'); + when(fs.fileExists(anything())).thenResolve(true); + when(fs.readFile(anything())).thenResolve(fileContent); + + experimentStorage + .setup((n) => n.value) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + experimentStorage + .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .returns(() => Promise.reject(error)) + .verifiable(TypeMoq.Times.once()); + + await expManager.updateExperimentStorage(); + + verify(fs.fileExists(anything())).once(); + verify(fs.readFile(anything())).once(); + experimentStorage.verifyAll(); + downloadedExperimentsStorage.verifyAll(); + }); + } + ); + + const testsForInExperiment = [ + { + testName: "If experiment's name is not in user experiment list, user is not in experiment", + experimentName: 'imaginary experiment', + userExperiments: [ + { name: 'experiment1', salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2', salt: 'salt', min: 19, max: 30 } + ], + expectedResult: false + }, + { + testName: + "If experiment's name is in user experiment list and hash modulo output is in range, user is in experiment", + experimentName: 'experiment1', + userExperiments: [ + { name: 'experiment1', salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2', salt: 'salt', min: 19, max: 30 } + ], + expectedResult: true + } + ]; + + testsForInExperiment.forEach((testParams) => { + test(testParams.testName, async () => { + expManager.userExperiments = testParams.userExperiments; + expect(expManager.inExperiment(testParams.experimentName)).to.equal( + testParams.expectedResult, + 'Incorrectly identified' + ); + }); + }); + + const testsForIsUserInRange = [ + // Note min equals 79 and max equals 94 + { + testName: 'Returns true if hash modulo output is in range', + hash: 1181, + expectedResult: true + }, + { + testName: 'Returns false if hash modulo is less than min', + hash: 967, + expectedResult: false + }, + { + testName: 'Returns false if hash modulo is more than max', + hash: 3297, + expectedResult: false + }, + { + testName: 'If checking if user is in range fails with error, throw error', + hash: 3297, + error: true, + expectedResult: false + }, + { + testName: 'If machine ID is bogus, throw error', + hash: 3297, + machineIdError: true, + expectedResult: false + } + ]; + + suite('Function IsUserInRange()', () => { + testsForIsUserInRange.forEach((testParams) => { + test(testParams.testName, async () => { + when(appEnvironment.machineId).thenReturn('101'); + if (testParams.machineIdError) { + when(appEnvironment.machineId).thenReturn(undefined as any); + expect(() => expManager.isUserInRange(79, 94, 'salt')).to.throw(); + } else if (testParams.error) { + const error = new Error('Kaboom'); + when(crypto.createHash(anything(), 'number', anything())).thenThrow(error); + expect(() => expManager.isUserInRange(79, 94, 'salt')).to.throw(error); + } else { + when(crypto.createHash(anything(), 'number', anything())).thenReturn(testParams.hash); + expect(expManager.isUserInRange(79, 94, 'salt')).to.equal( + testParams.expectedResult, + 'Incorrectly identified' + ); + } + }); + }); + test('If experiment salt belongs to an old experiment, keep using `SHA512` algorithm', async () => { + when(appEnvironment.machineId).thenReturn('101'); + when(crypto.createHash(anything(), 'number', 'SHA512')).thenReturn(644); + when(crypto.createHash(anything(), anything(), 'FNV')).thenReturn(1293); + // 'ShowPlayIcon' is one of the old experiments + expManager.isUserInRange(79, 94, 'ShowPlayIcon'); + verify(crypto.createHash(anything(), 'number', 'SHA512')).once(); + verify(crypto.createHash(anything(), anything(), 'FNV')).never(); + }); + test('If experiment salt does not belong to an old experiment, use `FNV` algorithm', async () => { + when(appEnvironment.machineId).thenReturn('101'); + when(crypto.createHash(anything(), anything(), 'SHA512')).thenReturn(644); + when(crypto.createHash(anything(), 'number', 'FNV')).thenReturn(1293); + expManager.isUserInRange(79, 94, 'NewExperimentSalt'); + verify(crypto.createHash(anything(), anything(), 'SHA512')).never(); + verify(crypto.createHash(anything(), 'number', 'FNV')).once(); + }); + test('Use the expected list of old experiments', async () => { + const expectedOldExperimentSalts = [ + 'ShowExtensionSurveyPrompt', + 'ShowPlayIcon', + 'AlwaysDisplayTestExplorer', + 'LS' + ]; + assert.deepEqual(expectedOldExperimentSalts, oldExperimentSalts); + }); + }); + + const testsForPopulateUserExperiments = [ + { + testName: 'User experiments list is empty if experiment storage value is not an array', + experimentStorageValue: undefined, + expectedResult: [] + }, + { + testName: 'User experiments list is empty if experiment storage value is an empty array', + experimentStorageValue: [], + expectedResult: [] + }, + { + testName: + 'User experiments list does not contain any experiments if user has requested to opt out of all experiments', + experimentStorageValue: [ + { name: 'experiment1 - control', salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2 - control', salt: 'salt', min: 80, max: 90 } + ], + hash: 8187, + experimentsOptedOutFrom: ['All'], + expectedResult: [] + }, + { + testName: + 'User experiments list contains all experiments if user has requested to opt into all experiments', + experimentStorageValue: [ + { name: 'experiment1 - control', salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2 - control', salt: 'salt', min: 80, max: 90 } + ], + hash: 8187, + experimentsOptedInto: ['All'], + expectedResult: [ + { name: 'experiment1 - control', salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2 - control', salt: 'salt', min: 80, max: 90 } + ] + }, + { + testName: + 'User experiments list contains the experiment if user has requested to opt in a control group but is not in experiment range', + experimentStorageValue: [{ name: 'experiment2 - control', salt: 'salt', min: 19, max: 30 }], + hash: 8187, + experimentsOptedInto: ['experiment2 - control'], + expectedResult: [] + }, + { + testName: + 'User experiments list contains the experiment if user has requested to opt out of a control group but user is in experiment range', + experimentStorageValue: [ + { name: 'experiment1 - control', salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2 - control', salt: 'salt', min: 19, max: 30 } + ], + hash: 8187, + experimentsOptedOutFrom: ['experiment1 - control'], + expectedResult: [{ name: 'experiment1 - control', salt: 'salt', min: 79, max: 94 }] + }, + { + testName: + 'User experiments list does not contains the experiment if user has opted out of experiment even though user is in experiment range', + experimentStorageValue: [ + { name: 'experiment1', salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2', salt: 'salt', min: 19, max: 30 } + ], + hash: 8187, + experimentsOptedOutFrom: ['experiment1'], + expectedResult: [] + }, + { + testName: + 'User experiments list contains the experiment if user has opted into the experiment even though user is not in experiment range', + experimentStorageValue: [ + { name: 'experiment1', salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2', salt: 'salt', min: 19, max: 30 } + ], + hash: 8187, + experimentsOptedInto: ['experiment1'], + expectedResult: [{ name: 'experiment1', salt: 'salt', min: 79, max: 94 }] + }, + { + testName: + 'User experiments list contains the experiment user has opened into and not the control experiment even if user is in the control experiment range', + experimentStorageValue: [ + { name: 'control', salt: 'salt', min: 0, max: 100 }, + { name: 'experiment', salt: 'salt', min: 0, max: 0 } + ], + hash: 8187, + experimentsOptedInto: ['experiment'], + expectedResult: [{ name: 'experiment', salt: 'salt', min: 0, max: 0 }] + }, + { + testName: + 'User experiments list does not contain the experiment if user has both opted in and out of an experiment', + experimentStorageValue: [ + { name: 'experiment1', salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2', salt: 'salt', min: 19, max: 30 } + ], + hash: 8187, + experimentsOptedInto: ['experiment1'], + experimentsOptedOutFrom: ['experiment1'], + expectedResult: [] + }, + { + testName: 'Otherwise user experiments list contains the experiment if user is in experiment range', + experimentStorageValue: [ + { name: 'experiment1', salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2', salt: 'salt', min: 19, max: 30 } + ], + hash: 8187, + expectedResult: [{ name: 'experiment1', salt: 'salt', min: 79, max: 94 }] + } + ]; + + suite('Function populateUserExperiments', async () => { + testsForPopulateUserExperiments.forEach((testParams) => { + test(testParams.testName, async () => { + experimentStorage.setup((n) => n.value).returns(() => testParams.experimentStorageValue); + when(appEnvironment.machineId).thenReturn('101'); + if (testParams.hash) { + when(crypto.createHash(anything(), 'number', anything())).thenReturn(testParams.hash); + } + if (testParams.experimentsOptedInto) { + expManager._experimentsOptedInto = testParams.experimentsOptedInto; + } + if (testParams.experimentsOptedOutFrom) { + expManager._experimentsOptedOutFrom = testParams.experimentsOptedOutFrom; + } + expManager.populateUserExperiments(); + assert.deepEqual(expManager.userExperiments, testParams.expectedResult); + }); + }); + test('NativeNotebook Experiment are not loaded in VSC Insiders', async () => { + const storageValue = [ + { name: NotebookEditorSupport.control, salt: 'salt', min: 0, max: 0 }, + { name: NotebookEditorSupport.customEditorExperiment, salt: 'salt', min: 0, max: 0 }, + { name: NotebookEditorSupport.nativeNotebookExperiment, salt: 'salt', min: 0, max: 100 } + ]; + experimentStorage.setup((n) => n.value).returns(() => storageValue); + when(appEnvironment.machineId).thenReturn('101'); + when(appEnvironment.channel).thenReturn('stable'); + when(crypto.createHash(anything(), 'number', anything())).thenReturn(8187); + expManager.populateUserExperiments(); + assert.deepEqual(expManager.userExperiments, []); + }); + test('NativeNotebook Experiment is loaded in VSC Insiders', async () => { + const storageValue = [ + { name: NotebookEditorSupport.control, salt: 'salt', min: 0, max: 0 }, + { name: NotebookEditorSupport.customEditorExperiment, salt: 'salt', min: 0, max: 0 }, + { name: NotebookEditorSupport.nativeNotebookExperiment, salt: 'salt', min: 0, max: 100 } + ]; + experimentStorage.setup((n) => n.value).returns(() => storageValue); + when(appEnvironment.machineId).thenReturn('101'); + when(appEnvironment.channel).thenReturn('insiders'); + when(crypto.createHash(anything(), 'number', anything())).thenReturn(8187); + expManager.populateUserExperiments(); + assert.deepEqual(expManager.userExperiments, [ + { + min: 0, + max: 100, + name: 'NativeNotebook - experiment', + salt: 'salt' + } + ]); + }); + }); + + const testsForAreExperimentsValid = [ + { + testName: 'If experiments are not an array, return false', + experiments: undefined, + expectedResult: false + }, + { + testName: 'If any experiment have `min` field missing, return false', + experiments: [ + { name: 'experiment1', salt: 'salt', max: 94 }, + { name: 'experiment2', salt: 'salt', min: 19, max: 30 } + ], + expectedResult: false + }, + { + testName: 'If any experiment have `max` field missing, return false', + experiments: [ + { name: 'experiment1', salt: 'salt', min: 79 }, + { name: 'experiment2', salt: 'salt', min: 19, max: 30 } + ], + expectedResult: false + }, + { + testName: 'If any experiment have `salt` field missing, return false', + experiments: [ + { name: 'experiment1', min: 79, max: 94 }, + { name: 'experiment2', salt: 'salt', min: 19, max: 30 } + ], + expectedResult: false + }, + { + testName: 'If any experiment have `name` field missing, return false', + experiments: [ + { salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2', salt: 'salt', min: 19, max: 30 } + ], + expectedResult: false + }, + { + testName: 'If all experiments contain all the fields in type `ABExperiment`, return true', + experiments: [ + { name: 'experiment1', salt: 'salt', min: 79, max: 94 }, + { name: 'experiment2', salt: 'salt', min: 19, max: 30 } + ], + expectedResult: true + } + ]; + + suite('Function areExperimentsValid()', () => { + testsForAreExperimentsValid.forEach((testParams) => { + test(testParams.testName, () => { + expect(expManager.areExperimentsValid(testParams.experiments as any)).to.equal( + testParams.expectedResult + ); + }); + }); + }); + + suite('Function doBestEffortToPopulateExperiments()', async () => { + let downloadAndStoreExperiments: sinon.SinonStub<any>; + + test('If attempt to download experiments within timeout suceeds, return true', async () => { + downloadAndStoreExperiments = sinon.stub(ExperimentsManager.prototype, 'downloadAndStoreExperiments'); + const timeout = 150; + const downloadExperimentsDeferred = createDeferred<void>(); + downloadAndStoreExperiments.callsFake(() => downloadExperimentsDeferred.promise); + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService), + timeout + ); + + // Download set to complete in 50 ms, timeout is of 150 ms, i.e download will complete within timeout + const timer = setTimeout(() => downloadExperimentsDeferred.resolve(), 50); + const result = await expManager.doBestEffortToPopulateExperiments(); + expect(result).to.equal(true, 'Expected value is true'); + assert.ok(downloadAndStoreExperiments.calledOnce); + clearTimeout(timer); + }); + + test('If downloading experiments fails to complete within timeout, return false', async () => { + downloadAndStoreExperiments = sinon.stub(ExperimentsManager.prototype, 'downloadAndStoreExperiments'); + const timeout = 100; + const downloadExperimentsDeferred = createDeferred<void>(); + downloadAndStoreExperiments.callsFake(() => downloadExperimentsDeferred.promise); + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService), + timeout + ); + + // Download set to complete in 200 ms, timeout is of 100 ms, i.e download will complete within timeout + const timer = setTimeout(() => downloadExperimentsDeferred.resolve(), 200); + const result = await expManager.doBestEffortToPopulateExperiments(); + expect(result).to.equal(false, 'Expected value is false'); + assert.ok(downloadAndStoreExperiments.calledOnce); + clearTimeout(timer); + }); + + test('If downloading experiments fails with error, return false', async () => { + downloadAndStoreExperiments = sinon.stub(ExperimentsManager.prototype, 'downloadAndStoreExperiments'); + downloadAndStoreExperiments.callsFake(() => Promise.reject('Kaboom')); + expManager = new ExperimentsManager( + instance(persistentStateFactory), + instance(httpClient), + instance(crypto), + instance(appEnvironment), + output.object, + instance(fs), + instance(configurationService) + ); + + const result = await expManager.doBestEffortToPopulateExperiments(); + expect(result).to.equal(false, 'Expected value is false'); + assert.ok(downloadAndStoreExperiments.calledOnce); + }); + }); + + test('If storage as parameter is passed in as argument to function downloadAndStoreExperiments(), download experiments into that storage', async () => { + downloadedExperimentsStorage + .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + experimentStorage + .setup((n) => n.updateValue(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + isDownloadedStorageValid + .setup((n) => n.updateValue(true)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + when(httpClient.getJSON(configUri, false)).thenResolve([ + { name: 'experiment1', salt: 'salt', min: 90, max: 100 } + ]); + + await expManager.downloadAndStoreExperiments(experimentStorage.object); + + verify(httpClient.getJSON(configUri, false)).once(); + isDownloadedStorageValid.verifyAll(); + experimentStorage.verifyAll(); + downloadedExperimentsStorage.verifyAll(); + }); +}); diff --git a/src/test/common/experiments/service.unit.test.ts b/src/test/common/experiments/service.unit.test.ts new file mode 100644 index 000000000000..3f9f055274a3 --- /dev/null +++ b/src/test/common/experiments/service.unit.test.ts @@ -0,0 +1,374 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, when } from 'ts-mockito'; +import * as tasClient from 'vscode-tas-client'; +import { ApplicationEnvironment } from '../../../client/common/application/applicationEnvironment'; +import { Channel, IApplicationEnvironment } from '../../../client/common/application/types'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { ExperimentService } from '../../../client/common/experiments/service'; +import { IConfigurationService } from '../../../client/common/types'; +import { Experiments } from '../../../client/common/utils/localize'; +import * as Telemetry from '../../../client/telemetry'; +import { EventName } from '../../../client/telemetry/constants'; +import { PVSC_EXTENSION_ID_FOR_TESTS } from '../../constants'; +import { MockOutputChannel } from '../../mockClasses'; +import { MockMemento } from '../../mocks/mementos'; + +suite('Experimentation service', () => { + const extensionVersion = '1.2.3'; + + let configurationService: IConfigurationService; + let appEnvironment: IApplicationEnvironment; + let globalMemento: MockMemento; + let outputChannel: MockOutputChannel; + + setup(() => { + configurationService = mock(ConfigurationService); + appEnvironment = mock(ApplicationEnvironment); + globalMemento = new MockMemento(); + outputChannel = new MockOutputChannel(''); + }); + + teardown(() => { + sinon.restore(); + Telemetry._resetSharedProperties(); + }); + + function configureSettings(enabled: boolean, optInto: string[], optOutFrom: string[]) { + when(configurationService.getSettings(undefined)).thenReturn({ + experiments: { + enabled, + optInto, + optOutFrom + } + // tslint:disable-next-line: no-any + } as any); + } + + function configureApplicationEnvironment(channel: Channel, version: string) { + when(appEnvironment.extensionChannel).thenReturn(channel); + when(appEnvironment.extensionName).thenReturn(PVSC_EXTENSION_ID_FOR_TESTS); + when(appEnvironment.packageJson).thenReturn({ version }); + } + + suite('Initialization', () => { + test('Users with a release version of the extension should be in the Public target population', () => { + const getExperimentationServiceStub = sinon.stub(tasClient, 'getExperimentationService'); + + configureSettings(true, [], []); + configureApplicationEnvironment('stable', extensionVersion); + + // tslint:disable-next-line: no-unused-expression + new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + + sinon.assert.calledWithExactly( + getExperimentationServiceStub, + PVSC_EXTENSION_ID_FOR_TESTS, + extensionVersion, + tasClient.TargetPopulation.Public, + sinon.match.any, + globalMemento + ); + }); + + test('Users with an Insiders version of the extension should be the Insiders target population', () => { + const getExperimentationServiceStub = sinon.stub(tasClient, 'getExperimentationService'); + + configureSettings(true, [], []); + configureApplicationEnvironment('insiders', extensionVersion); + + // tslint:disable-next-line: no-unused-expression + new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + + sinon.assert.calledWithExactly( + getExperimentationServiceStub, + PVSC_EXTENSION_ID_FOR_TESTS, + extensionVersion, + tasClient.TargetPopulation.Insiders, + sinon.match.any, + globalMemento + ); + }); + + test('Users can only opt into experiment groups', () => { + sinon.stub(tasClient, 'getExperimentationService'); + + configureSettings(true, ['Foo - experiment', 'Bar - control'], []); + configureApplicationEnvironment('stable', extensionVersion); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + + assert.deepEqual(experimentService._optInto, ['Foo - experiment']); + }); + + test('Users can only opt out of experiment groups', () => { + sinon.stub(tasClient, 'getExperimentationService'); + configureSettings(true, [], ['Foo - experiment', 'Bar - control']); + configureApplicationEnvironment('stable', extensionVersion); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + + assert.deepEqual(experimentService._optOutFrom, ['Foo - experiment']); + }); + + test('Experiment data in Memento storage should be logged if it starts with "python"', () => { + const experiments = ['ExperimentOne', 'pythonExperiment']; + globalMemento = mock(MockMemento); + configureSettings(true, [], []); + configureApplicationEnvironment('stable', extensionVersion); + // tslint:disable-next-line: no-any + when(globalMemento.get(anything(), anything())).thenReturn({ features: experiments } as any); + + // tslint:disable-next-line: no-unused-expression + new ExperimentService( + instance(configurationService), + instance(appEnvironment), + instance(globalMemento), + outputChannel + ); + const output = `${Experiments.inGroup().format('pythonExperiment')}\n`; + + assert.equal(outputChannel.output, output); + }); + }); + + suite('In-experiment check', () => { + const experiment = 'Test Experiment - experiment'; + let telemetryEvents: { eventName: string; properties: object }[] = []; + let isCachedFlightEnabledStub: sinon.SinonStub; + let sendTelemetryEventStub: sinon.SinonStub; + + setup(() => { + sendTelemetryEventStub = sinon + .stub(Telemetry, 'sendTelemetryEvent') + .callsFake((eventName: string, _, properties: object) => { + const telemetry = { eventName, properties }; + telemetryEvents.push(telemetry); + }); + + isCachedFlightEnabledStub = sinon.stub().returns(Promise.resolve(true)); + sinon.stub(tasClient, 'getExperimentationService').returns({ + isCachedFlightEnabled: isCachedFlightEnabledStub + // tslint:disable-next-line: no-any + } as any); + + configureApplicationEnvironment('stable', extensionVersion); + }); + + teardown(() => { + telemetryEvents = []; + }); + + test('If the opt-in and opt-out arrays are empty, return the value from the experimentation framework for a given experiment', async () => { + configureSettings(true, [], []); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + const result = await experimentService.inExperiment(experiment); + + assert.isTrue(result); + sinon.assert.notCalled(sendTelemetryEventStub); + sinon.assert.calledOnce(isCachedFlightEnabledStub); + }); + + test('If the experiment setting is disabled, inExperiment should return false', async () => { + configureSettings(false, [], []); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + const result = await experimentService.inExperiment(experiment); + + assert.isFalse(result); + sinon.assert.notCalled(sendTelemetryEventStub); + sinon.assert.notCalled(isCachedFlightEnabledStub); + }); + + test('If the opt-in setting contains "All", inExperiment should return true', async () => { + configureSettings(true, ['All'], []); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + const result = await experimentService.inExperiment(experiment); + + assert.isTrue(result); + assert.equal(telemetryEvents.length, 1); + assert.deepEqual(telemetryEvents[0], { + eventName: EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT, + properties: { expNameOptedInto: experiment } + }); + sinon.assert.notCalled(isCachedFlightEnabledStub); + }); + + test('If the opt-in setting contains the experiment name, inExperiment should return true', async () => { + configureSettings(true, [experiment], []); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + const result = await experimentService.inExperiment(experiment); + + assert.isTrue(result); + assert.equal(telemetryEvents.length, 1); + assert.deepEqual(telemetryEvents[0], { + eventName: EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT, + properties: { expNameOptedInto: experiment } + }); + sinon.assert.notCalled(isCachedFlightEnabledStub); + }); + + test('If the opt-out setting contains "All", inExperiment should return false', async () => { + configureSettings(true, [], ['All']); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + const result = await experimentService.inExperiment(experiment); + + assert.isFalse(result); + assert.equal(telemetryEvents.length, 1); + assert.deepEqual(telemetryEvents[0], { + eventName: EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT, + properties: { expNameOptedOutOf: experiment } + }); + sinon.assert.notCalled(isCachedFlightEnabledStub); + }); + + test('If the opt-out setting contains the experiment name, inExperiment should return false', async () => { + configureSettings(true, [], [experiment]); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + const result = await experimentService.inExperiment(experiment); + + assert.isFalse(result); + assert.equal(telemetryEvents.length, 1); + assert.deepEqual(telemetryEvents[0], { + eventName: EventName.PYTHON_EXPERIMENTS_OPT_IN_OUT, + properties: { expNameOptedOutOf: experiment } + }); + sinon.assert.notCalled(isCachedFlightEnabledStub); + }); + }); + + suite('Experiment value retrieval', () => { + const experiment = 'Test Experiment - experiment'; + let getTreatmentVariableAsyncStub: sinon.SinonStub; + + setup(() => { + getTreatmentVariableAsyncStub = sinon.stub().returns(Promise.resolve('value')); + sinon.stub(tasClient, 'getExperimentationService').returns({ + getTreatmentVariableAsync: getTreatmentVariableAsyncStub + // tslint:disable-next-line: no-any + } as any); + + configureApplicationEnvironment('stable', extensionVersion); + }); + + test('If the service is enabled and the opt-out array is empty,return the value from the experimentation framework for a given experiment', async () => { + configureSettings(true, [], []); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + const result = await experimentService.getExperimentValue(experiment); + + assert.equal(result, 'value'); + sinon.assert.calledOnce(getTreatmentVariableAsyncStub); + }); + + test('If the experiment setting is disabled, getExperimentValue should return undefined', async () => { + configureSettings(false, [], []); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + const result = await experimentService.getExperimentValue(experiment); + + assert.isUndefined(result); + sinon.assert.notCalled(getTreatmentVariableAsyncStub); + }); + + test('If the opt-out setting contains "All", getExperimentValue should return undefined', async () => { + configureSettings(true, [], ['All']); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + const result = await experimentService.getExperimentValue(experiment); + + assert.isUndefined(result); + sinon.assert.notCalled(getTreatmentVariableAsyncStub); + }); + + test('If the opt-out setting contains the experiment name, igetExperimentValue should return undefined', async () => { + configureSettings(true, [], [experiment]); + + const experimentService = new ExperimentService( + instance(configurationService), + instance(appEnvironment), + globalMemento, + outputChannel + ); + const result = await experimentService.getExperimentValue(experiment); + + assert.isUndefined(result); + sinon.assert.notCalled(getTreatmentVariableAsyncStub); + }); + }); +}); diff --git a/src/test/common/experiments/telemetry.unit.test.ts b/src/test/common/experiments/telemetry.unit.test.ts new file mode 100644 index 000000000000..d49ba5599c71 --- /dev/null +++ b/src/test/common/experiments/telemetry.unit.test.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { ExperimentationTelemetry } from '../../../client/common/experiments/telemetry'; +import * as Telemetry from '../../../client/telemetry'; + +suite('Experimentation telemetry', () => { + const event = 'SomeEventName'; + + let telemetryEvents: { eventName: string; properties: object }[] = []; + let sendTelemetryEventStub: sinon.SinonStub; + let setSharedPropertyStub: sinon.SinonStub; + let experimentTelemetry: ExperimentationTelemetry; + let eventProperties: Map<string, string>; + + setup(() => { + sendTelemetryEventStub = sinon + .stub(Telemetry, 'sendTelemetryEvent') + .callsFake((eventName: string, _, properties: object) => { + const telemetry = { eventName, properties }; + telemetryEvents.push(telemetry); + }); + setSharedPropertyStub = sinon.stub(Telemetry, 'setSharedProperty'); + + eventProperties = new Map<string, string>(); + eventProperties.set('foo', 'one'); + eventProperties.set('bar', 'two'); + + experimentTelemetry = new ExperimentationTelemetry(); + }); + + teardown(() => { + telemetryEvents = []; + sinon.restore(); + }); + + test('Calling postEvent should send a telemetry event', () => { + experimentTelemetry.postEvent(event, eventProperties); + + sinon.assert.calledOnce(sendTelemetryEventStub); + assert.equal(telemetryEvents.length, 1); + assert.deepEqual(telemetryEvents[0], { + eventName: event, + properties: { + foo: 'one', + bar: 'two' + } + }); + }); + + test('Shared properties should be set for all telemetry events', () => { + const shared = { key: 'shared', value: 'three' }; + + experimentTelemetry.setSharedProperty(shared.key, shared.value); + + sinon.assert.calledOnce(setSharedPropertyStub); + }); +}); diff --git a/src/test/common/extensions.unit.test.ts b/src/test/common/extensions.unit.test.ts new file mode 100644 index 000000000000..dcd392dbd695 --- /dev/null +++ b/src/test/common/extensions.unit.test.ts @@ -0,0 +1,86 @@ +import { expect } from 'chai'; +import '../../client/common/extensions'; + +// Defines a Mocha test suite to group tests of similar kind together +suite('String Extensions', () => { + test('Should return empty string for empty arg', () => { + const argTotest = ''; + expect(argTotest.toCommandArgument()).to.be.equal(''); + }); + test('Should quote an empty space', () => { + const argTotest = ' '; + expect(argTotest.toCommandArgument()).to.be.equal('" "'); + }); + test('Should not quote command arguments without spaces', () => { + const argTotest = 'one.two.three'; + expect(argTotest.toCommandArgument()).to.be.equal(argTotest); + }); + test('Should quote command arguments with spaces', () => { + const argTotest = 'one two three'; + expect(argTotest.toCommandArgument()).to.be.equal(`"${argTotest}"`); + }); + test('Should return empty string for empty path', () => { + const fileToTest = ''; + expect(fileToTest.fileToCommandArgument()).to.be.equal(''); + }); + test('Should not quote file argument without spaces', () => { + const fileToTest = 'users/test/one'; + expect(fileToTest.fileToCommandArgument()).to.be.equal(fileToTest); + }); + test('Should quote file argument with spaces', () => { + const fileToTest = 'one two three'; + expect(fileToTest.fileToCommandArgument()).to.be.equal(`"${fileToTest}"`); + }); + test('Should replace all back slashes with forward slashes (irrespective of OS)', () => { + const fileToTest = 'c:\\users\\user\\conda\\scripts\\python.exe'; + expect(fileToTest.fileToCommandArgument()).to.be.equal(fileToTest.replace(/\\/g, '/')); + }); + test('Should replace all back slashes with forward slashes (irrespective of OS) and quoted when file has spaces', () => { + const fileToTest = 'c:\\users\\user namne\\conda path\\scripts\\python.exe'; + expect(fileToTest.fileToCommandArgument()).to.be.equal(`"${fileToTest.replace(/\\/g, '/')}"`); + }); + test('Should replace all back slashes with forward slashes (irrespective of OS) and quoted when file has spaces', () => { + const fileToTest = 'c:\\users\\user namne\\conda path\\scripts\\python.exe'; + expect(fileToTest.fileToCommandArgument()).to.be.equal(`"${fileToTest.replace(/\\/g, '/')}"`); + }); + test('Should leave string unchanged', () => { + expect('something {0}'.format()).to.be.equal('something {0}'); + }); + test('String should be formatted to contain first argument', () => { + const formatString = 'something {0}'; + const expectedString = 'something one'; + expect(formatString.format('one')).to.be.equal(expectedString); + }); + test('String should be formatted to contain first argument even with too many args', () => { + const formatString = 'something {0}'; + const expectedString = 'something one'; + expect(formatString.format('one', 'two')).to.be.equal(expectedString); + }); + test('String should be formatted to contain second argument', () => { + const formatString = 'something {1}'; + const expectedString = 'something two'; + expect(formatString.format('one', 'two')).to.be.equal(expectedString); + }); + test('String should be formatted to contain second argument even with too many args', () => { + const formatString = 'something {1}'; + const expectedString = 'something two'; + expect(formatString.format('one', 'two', 'three')).to.be.equal(expectedString); + }); + test('String should be formatted with multiple args', () => { + const formatString = 'something {1}, {0}'; + const expectedString = 'something two, one'; + expect(formatString.format('one', 'two', 'three')).to.be.equal(expectedString); + }); + test('String should remove quotes', () => { + //tslint:disable:no-multiline-string + const quotedString = `'foo is "bar" is foo' is bar'`; + const quotedString2 = `foo is "bar" is foo' is bar'`; + const quotedString3 = `foo is "bar" is foo' is bar`; + const quotedString4 = `"foo is "bar" is foo' is bar"`; + const expectedString = `foo is "bar" is foo' is bar`; + expect(quotedString.trimQuotes()).to.be.equal(expectedString); + expect(quotedString2.trimQuotes()).to.be.equal(expectedString); + expect(quotedString3.trimQuotes()).to.be.equal(expectedString); + expect(quotedString4.trimQuotes()).to.be.equal(expectedString); + }); +}); diff --git a/src/test/common/featureDeprecationManager.unit.test.ts b/src/test/common/featureDeprecationManager.unit.test.ts new file mode 100644 index 000000000000..2f3b48a61eb2 --- /dev/null +++ b/src/test/common/featureDeprecationManager.unit.test.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Disposable, WorkspaceConfiguration } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; +import { FeatureDeprecationManager } from '../../client/common/featureDeprecationManager'; +import { DeprecatedSettingAndValue, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; + +suite('Feature Deprecation Manager Tests', () => { + test('Ensure deprecated command Build_Workspace_Symbols registers its popup', () => { + const persistentState: TypeMoq.IMock<IPersistentStateFactory> = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + const persistentBool: TypeMoq.IMock<IPersistentState<boolean>> = TypeMoq.Mock.ofType< + IPersistentState<boolean> + >(); + persistentBool.setup((a) => a.value).returns(() => true); + persistentBool.setup((a) => a.updateValue(TypeMoq.It.isValue(false))).returns(() => Promise.resolve()); + persistentState + .setup((a) => + a.createGlobalPersistentState( + TypeMoq.It.isValue('SHOW_DEPRECATED_FEATURE_PROMPT_BUILD_WORKSPACE_SYMBOLS'), + TypeMoq.It.isValue(true) + ) + ) + .returns(() => persistentBool.object) + .verifiable(TypeMoq.Times.once()); + const popupMgr: TypeMoq.IMock<IApplicationShell> = TypeMoq.Mock.ofType<IApplicationShell>(); + popupMgr + .setup((p) => + p.showInformationMessage(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString()) + ) + .returns( + (_val) => + new Promise<string>((resolve, _reject) => { + resolve('Learn More'); + }) + ); + const cmdDisposable: TypeMoq.IMock<Disposable> = TypeMoq.Mock.ofType<Disposable>(); + const cmdManager: TypeMoq.IMock<ICommandManager> = TypeMoq.Mock.ofType<ICommandManager>(); + cmdManager + .setup((c) => + c.registerCommand( + TypeMoq.It.isValue('python.buildWorkspaceSymbols'), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => cmdDisposable.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + const workspaceConfig: TypeMoq.IMock<WorkspaceConfiguration> = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceConfig + .setup((ws) => ws.has(TypeMoq.It.isAnyString())) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + const workspace: TypeMoq.IMock<IWorkspaceService> = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspace + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isAny())) + .returns(() => workspaceConfig.object); + const featureDepMgr: FeatureDeprecationManager = new FeatureDeprecationManager( + persistentState.object, + cmdManager.object, + workspace.object, + popupMgr.object + ); + + featureDepMgr.initialize(); + }); + test('Ensure setting is checked', () => { + const pythonConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + const deprecatedSetting: DeprecatedSettingAndValue = { setting: 'autoComplete.preloadModules' }; + // tslint:disable-next-line:no-any + const _ = {} as any; + const featureDepMgr = new FeatureDeprecationManager(_, _, _, _); + + pythonConfig + .setup((p) => p.has(TypeMoq.It.isValue(deprecatedSetting.setting))) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + + let isUsed = featureDepMgr.isDeprecatedSettingAndValueUsed(pythonConfig.object, deprecatedSetting); + pythonConfig.verifyAll(); + expect(isUsed).to.be.equal(false, 'Setting should not be used'); + + type TestConfigs = { valueInSetting: any; expectedValue: boolean; valuesToLookFor?: any[] }; + let testConfigs: TestConfigs[] = [ + { valueInSetting: [], expectedValue: false }, + { valueInSetting: ['1'], expectedValue: true }, + { valueInSetting: [1], expectedValue: true }, + { valueInSetting: [{}], expectedValue: true } + ]; + + for (const config of testConfigs) { + pythonConfig.reset(); + pythonConfig + .setup((p) => p.has(TypeMoq.It.isValue(deprecatedSetting.setting))) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + pythonConfig + .setup((p) => p.get(TypeMoq.It.isValue(deprecatedSetting.setting))) + .returns(() => config.valueInSetting); + + isUsed = featureDepMgr.isDeprecatedSettingAndValueUsed(pythonConfig.object, deprecatedSetting); + + pythonConfig.verifyAll(); + expect(isUsed).to.be.equal(config.expectedValue, `Failed for config = ${JSON.stringify(config)}`); + } + + testConfigs = [ + { valueInSetting: 'true', expectedValue: true, valuesToLookFor: ['true', true] }, + { valueInSetting: true, expectedValue: true, valuesToLookFor: ['true', true] }, + { valueInSetting: 'false', expectedValue: true, valuesToLookFor: ['false', false] }, + { valueInSetting: false, expectedValue: true, valuesToLookFor: ['false', false] } + ]; + + for (const config of testConfigs) { + pythonConfig.reset(); + pythonConfig + .setup((p) => p.has(TypeMoq.It.isValue(deprecatedSetting.setting))) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + pythonConfig + .setup((p) => p.get(TypeMoq.It.isValue(deprecatedSetting.setting))) + .returns(() => config.valueInSetting); + + deprecatedSetting.values = config.valuesToLookFor; + isUsed = featureDepMgr.isDeprecatedSettingAndValueUsed(pythonConfig.object, deprecatedSetting); + + pythonConfig.verifyAll(); + expect(isUsed).to.be.equal(config.expectedValue, `Failed for config = ${JSON.stringify(config)}`); + } + }); +}); diff --git a/src/test/common/helpers.test.ts b/src/test/common/helpers.test.ts new file mode 100644 index 000000000000..45e14d9146a1 --- /dev/null +++ b/src/test/common/helpers.test.ts @@ -0,0 +1,23 @@ +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import { isNotInstalledError } from '../../client/common/helpers'; + +// Defines a Mocha test suite to group tests of similar kind together +suite('helpers', () => { + test('isNotInstalledError', (done) => { + const error = new Error('something is not installed'); + assert.equal(isNotInstalledError(error), false, 'Standard error'); + + // tslint:disable-next-line:no-any + (error as any).code = 'ENOENT'; + assert.equal(isNotInstalledError(error), true, 'ENOENT error code not detected'); + + // tslint:disable-next-line:no-any + (error as any).code = 127; + assert.equal(isNotInstalledError(error), true, '127 error code not detected'); + + done(); + }); +}); diff --git a/src/test/common/insidersBuild/downloadChannelRules.unit.test.ts b/src/test/common/insidersBuild/downloadChannelRules.unit.test.ts new file mode 100644 index 000000000000..285e0df9d422 --- /dev/null +++ b/src/test/common/insidersBuild/downloadChannelRules.unit.test.ts @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { assert } from 'chai'; +import { instance, mock, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { + ExtensionInsidersDailyChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionInsidersWeeklyChannelRule, + frequencyForDailyInsidersCheck, + frequencyForWeeklyInsidersCheck, + lastLookUpTimeKey +} from '../../../client/common/insidersBuild/downloadChannelRules'; +import { PersistentStateFactory } from '../../../client/common/persistentState'; +import { IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; + +suite('Download channel rules - ExtensionInsidersOffChannelRule', () => { + let stableChannelRule: ExtensionInsidersOffChannelRule; + setup(() => { + stableChannelRule = new ExtensionInsidersOffChannelRule(); + }); + + test('Never look for insiders build', async () => { + const result = await stableChannelRule.shouldLookForInsidersBuild(); + assert.equal(result, false, 'Not looking for the correct build'); + }); +}); + +suite('Download channel rules - ExtensionInsidersDailyChannelRule', () => { + let persistentStateFactory: IPersistentStateFactory; + let lastLookUpTime: TypeMoq.IMock<IPersistentState<number>>; + let insidersDailyChannelRule: ExtensionInsidersDailyChannelRule; + setup(() => { + persistentStateFactory = mock(PersistentStateFactory); + lastLookUpTime = TypeMoq.Mock.ofType<IPersistentState<number>>(); + when(persistentStateFactory.createGlobalPersistentState(lastLookUpTimeKey, -1)).thenReturn( + lastLookUpTime.object + ); + insidersDailyChannelRule = new ExtensionInsidersDailyChannelRule(instance(persistentStateFactory)); + }); + + test('If insiders channel rule is new, update look up time and return installer for insiders build', async () => { + lastLookUpTime + .setup((l) => l.updateValue(TypeMoq.It.isAnyNumber())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + const result = await insidersDailyChannelRule.shouldLookForInsidersBuild(true); + lastLookUpTime.verifyAll(); + assert.equal(result, true, 'Not looking for the correct build'); + }); + suite('If insiders channel rule is not new', async () => { + test('Update look up time and return installer for insiders build if looking for insiders the first time', async () => { + lastLookUpTime + .setup((l) => l.updateValue(TypeMoq.It.isAnyNumber())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + lastLookUpTime + .setup((l) => l.value) + .returns(() => -1) + .verifiable(TypeMoq.Times.atLeastOnce()); + const result = await insidersDailyChannelRule.shouldLookForInsidersBuild(false); + lastLookUpTime.verifyAll(); + assert.equal(result, true, 'Not looking for the correct build'); + }); + test('Update look up time and return installer for insiders build if looking for insiders after 24 hrs of last lookup time', async () => { + lastLookUpTime + .setup((l) => l.updateValue(TypeMoq.It.isAnyNumber())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + lastLookUpTime + .setup((l) => l.value) + .returns(() => Date.now() - 2 * frequencyForDailyInsidersCheck) // Looking after 2 days + .verifiable(TypeMoq.Times.atLeastOnce()); + const result = await insidersDailyChannelRule.shouldLookForInsidersBuild(false); + lastLookUpTime.verifyAll(); + assert.equal(result, true, 'Not looking for the correct build'); + }); + test('Do not update look up time or return any installer if looking for insiders within 24 hrs of last lookup time', async () => { + lastLookUpTime + .setup((l) => l.updateValue(TypeMoq.It.isAnyNumber())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + lastLookUpTime + .setup((l) => l.value) + .returns(() => Date.now() - frequencyForDailyInsidersCheck / 2) // Looking after half a day + .verifiable(TypeMoq.Times.atLeastOnce()); + const result = await insidersDailyChannelRule.shouldLookForInsidersBuild(false); + lastLookUpTime.verifyAll(); + assert.equal(result, false, 'Not looking for the correct build'); + }); + }); +}); + +suite('Download channel rules - ExtensionInsidersWeeklyChannelRule', () => { + let persistentStateFactory: IPersistentStateFactory; + let lastLookUpTime: TypeMoq.IMock<IPersistentState<number>>; + let insidersWeeklyChannelRule: ExtensionInsidersWeeklyChannelRule; + setup(() => { + persistentStateFactory = mock(PersistentStateFactory); + lastLookUpTime = TypeMoq.Mock.ofType<IPersistentState<number>>(); + when(persistentStateFactory.createGlobalPersistentState(lastLookUpTimeKey, -1)).thenReturn( + lastLookUpTime.object + ); + insidersWeeklyChannelRule = new ExtensionInsidersWeeklyChannelRule(instance(persistentStateFactory)); + }); + + test('If insiders channel rule is new, update look up time and return installer for insiders build', async () => { + lastLookUpTime + .setup((l) => l.updateValue(TypeMoq.It.isAnyNumber())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + const result = await insidersWeeklyChannelRule.shouldLookForInsidersBuild(true); + lastLookUpTime.verifyAll(); + assert.equal(result, true, 'Not looking for the correct build'); + }); + suite('If insiders channel rule is not new', async () => { + test('Update look up time and return installer for insiders build if looking for insiders the first time', async () => { + lastLookUpTime + .setup((l) => l.updateValue(TypeMoq.It.isAnyNumber())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + lastLookUpTime + .setup((l) => l.value) + .returns(() => -1) + .verifiable(TypeMoq.Times.atLeastOnce()); + const result = await insidersWeeklyChannelRule.shouldLookForInsidersBuild(false); + lastLookUpTime.verifyAll(); + assert.equal(result, true, 'Not looking for the correct build'); + }); + test('Update look up time and return installer for insiders build if looking for insiders after a week of last lookup time', async () => { + lastLookUpTime + .setup((l) => l.updateValue(TypeMoq.It.isAnyNumber())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + lastLookUpTime + .setup((l) => l.value) + .returns(() => Date.now() - 2 * frequencyForWeeklyInsidersCheck) // Looking after 2 weeks + .verifiable(TypeMoq.Times.atLeastOnce()); + const result = await insidersWeeklyChannelRule.shouldLookForInsidersBuild(false); + lastLookUpTime.verifyAll(); + assert.equal(result, true, 'Not looking for the correct build'); + }); + test('Do not update look up time or return any installer if looking for insiders within one week of last lookup time', async () => { + lastLookUpTime + .setup((l) => l.updateValue(TypeMoq.It.isAnyNumber())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + lastLookUpTime + .setup((l) => l.value) + .returns(() => Date.now() - frequencyForWeeklyInsidersCheck / 2) // Looking after half a week + .verifiable(TypeMoq.Times.atLeastOnce()); + const result = await insidersWeeklyChannelRule.shouldLookForInsidersBuild(false); + lastLookUpTime.verifyAll(); + assert.equal(result, false, 'Not looking for the correct build'); + }); + }); +}); diff --git a/src/test/common/insidersBuild/downloadChannelService.unit.test.ts b/src/test/common/insidersBuild/downloadChannelService.unit.test.ts new file mode 100644 index 000000000000..f3cce07d5c56 --- /dev/null +++ b/src/test/common/insidersBuild/downloadChannelService.unit.test.ts @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect } from 'chai'; +import { instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationChangeEvent, ConfigurationTarget, EventEmitter, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { + ExtensionChannelService, + insidersChannelSetting +} from '../../../client/common/insidersBuild/downloadChannelService'; +import { ExtensionChannels } from '../../../client/common/insidersBuild/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { createDeferred } from '../../../client/common/utils/async'; +import { sleep } from '../../../test/common'; + +// tslint:disable-next-line:max-func-body-length +suite('Download channel service', () => { + let configService: IConfigurationService; + let workspaceService: IWorkspaceService; + let channelService: ExtensionChannelService; + let configChangeEvent: EventEmitter<ConfigurationChangeEvent>; + setup(() => { + configService = mock(ConfigurationService); + workspaceService = mock(WorkspaceService); + configChangeEvent = new EventEmitter<ConfigurationChangeEvent>(); + when(workspaceService.onDidChangeConfiguration).thenReturn(configChangeEvent.event); + channelService = new ExtensionChannelService(instance(configService), instance(workspaceService), []); + }); + + teardown(() => { + configChangeEvent.dispose(); + }); + + [ + { + testName: "Get channel returns 'off' if settings value is set to 'off'", + settings: 'off', + expectedResult: 'off' + }, + { + testName: "Get channel returns 'weekly' if settings value is set to 'weekly'", + settings: 'weekly', + expectedResult: 'weekly' + }, + { + testName: "Get channel returns 'daily' if settings value is set to 'daily'", + settings: 'daily', + expectedResult: 'daily' + } + ].forEach((testParams) => { + test(testParams.testName, async () => { + when(configService.getSettings()).thenReturn({ + insidersChannel: testParams.settings as ExtensionChannels + } as any); + const result = channelService.getChannel(); + expect(result).to.equal(testParams.expectedResult); + verify(configService.getSettings()).once(); + }); + }); + + test('Function isChannelUsingDefaultConfiguration() returns false if setting is set', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + const settings = { globalValue: 'off' }; + + when(workspaceService.getConfiguration('python')).thenReturn(workspaceConfig.object); + workspaceConfig + .setup((c) => c.inspect<ExtensionChannels>(insidersChannelSetting)) + .returns(() => settings as any) + .verifiable(TypeMoq.Times.once()); + expect(channelService.isChannelUsingDefaultConfiguration).to.equal(false, 'Incorrect value'); + workspaceConfig.verifyAll(); + }); + + test('Function isChannelUsingDefaultConfiguration() returns true if setting is not set', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + const settings = { globalValue: undefined }; + + when(workspaceService.getConfiguration('python')).thenReturn(workspaceConfig.object); + workspaceConfig + .setup((c) => c.inspect<ExtensionChannels>(insidersChannelSetting)) + .returns(() => settings as any) + .verifiable(TypeMoq.Times.once()); + expect(channelService.isChannelUsingDefaultConfiguration).to.equal(true, 'Incorrect value'); + workspaceConfig.verifyAll(); + }); + + test('Function isChannelUsingDefaultConfiguration() throws error if not setting is found', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + const settings = undefined; + + when(workspaceService.getConfiguration('python')).thenReturn(workspaceConfig.object); + workspaceConfig + .setup((c) => c.inspect<ExtensionChannels>(insidersChannelSetting)) + .returns(() => settings as any) + .verifiable(TypeMoq.Times.once()); + expect(() => channelService.isChannelUsingDefaultConfiguration).to.throw(); + workspaceConfig.verifyAll(); + }); + + test('Update channel updates configuration settings', async () => { + const value = 'Random'; + when( + configService.updateSetting(insidersChannelSetting, value, undefined, ConfigurationTarget.Global) + ).thenResolve(undefined); + await channelService.updateChannel(value as any); + verify( + configService.updateSetting(insidersChannelSetting, value, undefined, ConfigurationTarget.Global) + ).once(); + }); + + test('Update channel throws error when updates configuration settings fails', async () => { + const value = 'Random'; + when( + configService.updateSetting(insidersChannelSetting, value, undefined, ConfigurationTarget.Global) + ).thenThrow(new Error('Kaboom')); + const promise = channelService.updateChannel(value as any); + await expect(promise).to.eventually.be.rejectedWith('Kaboom'); + }); + + test('If insidersChannelSetting is changed, an event is fired', async () => { + const _onDidChannelChange = TypeMoq.Mock.ofType<EventEmitter<ExtensionChannels>>(); + const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); + const settings = { insidersChannel: 'off' }; + event + .setup((e) => e.affectsConfiguration(`python.${insidersChannelSetting}`)) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + when(configService.getSettings()).thenReturn(settings as any); + channelService._onDidChannelChange = _onDidChannelChange.object; + _onDidChannelChange + .setup((emitter) => emitter.fire(TypeMoq.It.isValue(settings.insidersChannel as any))) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + await channelService.onDidChangeConfiguration(event.object); + _onDidChannelChange.verifyAll(); + event.verifyAll(); + verify(configService.getSettings()).once(); + }); + + test('If some other setting changed, no event is fired', async () => { + const _onDidChannelChange = TypeMoq.Mock.ofType<EventEmitter<ExtensionChannels>>(); + const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); + const settings = { insidersChannel: 'off' }; + event + .setup((e) => e.affectsConfiguration(`python.${insidersChannelSetting}`)) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + when(configService.getSettings()).thenReturn(settings as any); + channelService._onDidChannelChange = _onDidChannelChange.object; + _onDidChannelChange + .setup((emitter) => emitter.fire(TypeMoq.It.isValue(settings.insidersChannel as any))) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + await channelService.onDidChangeConfiguration(event.object); + _onDidChannelChange.verifyAll(); + event.verifyAll(); + verify(configService.getSettings()).never(); + }); + + test('Ensure on channel change captures the fired event with the correct arguments', async () => { + const deferred = createDeferred<true>(); + const settings = { insidersChannel: 'off' }; + channelService.onDidChannelChange((channel) => { + expect(channel).to.equal(settings.insidersChannel); + deferred.resolve(true); + }); + channelService._onDidChannelChange.fire(settings.insidersChannel as any); + const eventCaptured = await Promise.race([deferred.promise, sleep(1000).then(() => false)]); + expect(eventCaptured).to.equal(true, 'Event should be captured'); + }); +}); diff --git a/src/test/common/insidersBuild/insidersExtensionPrompt.unit.test.ts b/src/test/common/insidersBuild/insidersExtensionPrompt.unit.test.ts new file mode 100644 index 000000000000..df7fcf45f332 --- /dev/null +++ b/src/test/common/insidersBuild/insidersExtensionPrompt.unit.test.ts @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; +import { ExtensionChannelService } from '../../../client/common/insidersBuild/downloadChannelService'; +import { + InsidersExtensionPrompt, + insidersPromptStateKey +} from '../../../client/common/insidersBuild/insidersExtensionPrompt'; +import { ExtensionChannel, IExtensionChannelService } from '../../../client/common/insidersBuild/types'; +import { PersistentStateFactory } from '../../../client/common/persistentState'; +import { IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { Common, DataScienceSurveyBanner, ExtensionChannels } from '../../../client/common/utils/localize'; + +// tslint:disable-next-line: max-func-body-length +suite('Insiders Extension prompt', () => { + let appShell: IApplicationShell; + let extensionChannelService: IExtensionChannelService; + let cmdManager: ICommandManager; + let persistentState: IPersistentStateFactory; + let hasUserBeenNotifiedState: TypeMoq.IMock<IPersistentState<boolean>>; + let insidersPrompt: InsidersExtensionPrompt; + setup(() => { + extensionChannelService = mock(ExtensionChannelService); + appShell = mock(ApplicationShell); + persistentState = mock(PersistentStateFactory); + cmdManager = mock(CommandManager); + hasUserBeenNotifiedState = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + when(persistentState.createGlobalPersistentState(insidersPromptStateKey, false)).thenReturn( + hasUserBeenNotifiedState.object + ); + insidersPrompt = new InsidersExtensionPrompt( + instance(appShell), + instance(extensionChannelService), + instance(cmdManager), + instance(persistentState) + ); + }); + + test("Channel is set to 'daily' if 'Yes, daily' option is selected", async () => { + const prompts = [ + ExtensionChannels.yesWeekly(), + ExtensionChannels.yesDaily(), + DataScienceSurveyBanner.bannerLabelNo() + ]; + when(appShell.showInformationMessage(ExtensionChannels.promptMessage(), ...prompts)).thenResolve( + ExtensionChannels.yesDaily() as any + ); + when(cmdManager.executeCommand('workbench.action.reloadWindow')).thenResolve(); + when(extensionChannelService.updateChannel(ExtensionChannel.daily)).thenResolve(); + hasUserBeenNotifiedState + .setup((u) => u.updateValue(true)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + await insidersPrompt.promptToInstallInsiders(); + verify(appShell.showInformationMessage(ExtensionChannels.promptMessage(), ...prompts)).once(); + verify(extensionChannelService.updateChannel(ExtensionChannel.daily)).once(); + hasUserBeenNotifiedState.verifyAll(); + verify(cmdManager.executeCommand('workbench.action.reloadWindow')).never(); + }); + + test("Channel is set to 'weekly' if 'Yes, weekly' option is selected", async () => { + const prompts = [ + ExtensionChannels.yesWeekly(), + ExtensionChannels.yesDaily(), + DataScienceSurveyBanner.bannerLabelNo() + ]; + when(appShell.showInformationMessage(ExtensionChannels.promptMessage(), ...prompts)).thenResolve( + ExtensionChannels.yesWeekly() as any + ); + when(cmdManager.executeCommand('workbench.action.reloadWindow')).thenResolve(); + when(extensionChannelService.updateChannel(ExtensionChannel.weekly)).thenResolve(); + hasUserBeenNotifiedState + .setup((u) => u.updateValue(true)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + await insidersPrompt.promptToInstallInsiders(); + verify(appShell.showInformationMessage(ExtensionChannels.promptMessage(), ...prompts)).once(); + verify(extensionChannelService.updateChannel(ExtensionChannel.weekly)).once(); + hasUserBeenNotifiedState.verifyAll(); + verify(cmdManager.executeCommand('workbench.action.reloadWindow')).never(); + }); + + test("No channel is set if 'No, thanks' option is selected", async () => { + const prompts = [ + ExtensionChannels.yesWeekly(), + ExtensionChannels.yesDaily(), + DataScienceSurveyBanner.bannerLabelNo() + ]; + when(appShell.showInformationMessage(ExtensionChannels.promptMessage(), ...prompts)).thenResolve( + DataScienceSurveyBanner.bannerLabelNo() as any + ); + when(cmdManager.executeCommand('workbench.action.reloadWindow')).thenResolve(); + when(extensionChannelService.updateChannel(anything())).thenResolve(); + hasUserBeenNotifiedState + .setup((u) => u.updateValue(true)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + await insidersPrompt.promptToInstallInsiders(); + verify(appShell.showInformationMessage(ExtensionChannels.promptMessage(), ...prompts)).once(); + verify(extensionChannelService.updateChannel(anything())).never(); + hasUserBeenNotifiedState.verifyAll(); + verify(cmdManager.executeCommand('workbench.action.reloadWindow')).never(); + }); + + test('No channel is set if no option is selected', async () => { + const prompts = [ + ExtensionChannels.yesWeekly(), + ExtensionChannels.yesDaily(), + DataScienceSurveyBanner.bannerLabelNo() + ]; + when(appShell.showInformationMessage(ExtensionChannels.promptMessage(), ...prompts)).thenResolve( + undefined as any + ); + when(cmdManager.executeCommand('workbench.action.reloadWindow')).thenResolve(); + when(extensionChannelService.updateChannel(anything())).thenResolve(); + hasUserBeenNotifiedState + .setup((u) => u.updateValue(true)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + await insidersPrompt.promptToInstallInsiders(); + verify(appShell.showInformationMessage(ExtensionChannels.promptMessage(), ...prompts)).once(); + verify(extensionChannelService.updateChannel(anything())).never(); + hasUserBeenNotifiedState.verifyAll(); + verify(cmdManager.executeCommand('workbench.action.reloadWindow')).never(); + }); + + test('Do not do anything if no option is selected in the reload prompt', async () => { + when( + appShell.showInformationMessage(ExtensionChannels.reloadToUseInsidersMessage(), Common.reload()) + ).thenResolve(undefined); + when(cmdManager.executeCommand('workbench.action.reloadWindow')).thenResolve(); + await insidersPrompt.promptToReload(); + verify(appShell.showInformationMessage(ExtensionChannels.reloadToUseInsidersMessage(), Common.reload())).once(); + verify(cmdManager.executeCommand('workbench.action.reloadWindow')).never(); + }); + + test("Reload windows if 'Reload' option is selected in the reload prompt", async () => { + when( + appShell.showInformationMessage(ExtensionChannels.reloadToUseInsidersMessage(), Common.reload()) + ).thenResolve(Common.reload() as any); + when(cmdManager.executeCommand('workbench.action.reloadWindow')).thenResolve(); + await insidersPrompt.promptToReload(); + verify(appShell.showInformationMessage(ExtensionChannels.reloadToUseInsidersMessage(), Common.reload())).once(); + verify(cmdManager.executeCommand('workbench.action.reloadWindow')).once(); + }); +}); diff --git a/src/test/common/insidersBuild/insidersExtensionService.unit.test.ts b/src/test/common/insidersBuild/insidersExtensionService.unit.test.ts new file mode 100644 index 000000000000..a0d96984e168 --- /dev/null +++ b/src/test/common/insidersBuild/insidersExtensionService.unit.test.ts @@ -0,0 +1,567 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { EventEmitter } from 'vscode'; +import { ApplicationEnvironment } from '../../../client/common/application/applicationEnvironment'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { Channel, IApplicationEnvironment, ICommandManager } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import { ExtensionChannelService } from '../../../client/common/insidersBuild/downloadChannelService'; +import { InsidersExtensionPrompt } from '../../../client/common/insidersBuild/insidersExtensionPrompt'; +import { InsidersExtensionService } from '../../../client/common/insidersBuild/insidersExtensionService'; +import { + ExtensionChannels, + IExtensionChannelRule, + IExtensionChannelService, + IInsiderExtensionPrompt +} from '../../../client/common/insidersBuild/types'; +import { InsidersBuildInstaller } from '../../../client/common/installer/extensionBuildInstaller'; +import { IExtensionBuildInstaller } from '../../../client/common/installer/types'; +import { PersistentState } from '../../../client/common/persistentState'; +import { IDisposable, IPersistentState } from '../../../client/common/types'; +import { createDeferred, createDeferredFromPromise } from '../../../client/common/utils/async'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { sleep } from '../../../test/core'; + +suite('Insiders Extension Service - Handle channel', () => { + let appEnvironment: IApplicationEnvironment; + let serviceContainer: IServiceContainer; + let extensionChannelService: IExtensionChannelService; + let cmdManager: ICommandManager; + let insidersPrompt: IInsiderExtensionPrompt; + let insidersInstaller: IExtensionBuildInstaller; + let insidersExtensionService: InsidersExtensionService; + setup(() => { + extensionChannelService = mock(ExtensionChannelService); + appEnvironment = mock(ApplicationEnvironment); + cmdManager = mock(CommandManager); + serviceContainer = mock(ServiceContainer); + insidersPrompt = mock(InsidersExtensionPrompt); + insidersInstaller = mock(InsidersBuildInstaller); + insidersExtensionService = new InsidersExtensionService( + instance(extensionChannelService), + instance(insidersPrompt), + instance(appEnvironment), + instance(cmdManager), + instance(serviceContainer), + instance(insidersInstaller), + [] + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('If insiders is not be installed, handling channel does not do anything and simply returns', async () => { + const channelRule = TypeMoq.Mock.ofType<IExtensionChannelRule>(); + when(serviceContainer.get<IExtensionChannelRule>(IExtensionChannelRule, 'off')).thenReturn(channelRule.object); + channelRule + .setup((c) => c.shouldLookForInsidersBuild(false)) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + when(insidersInstaller.install()).thenResolve(undefined); + await insidersExtensionService.handleChannel('off'); + verify(insidersInstaller.install()).never(); + channelRule.verifyAll(); + }); + + test('If insiders is required to be installed, handling channel installs the build and prompts user', async () => { + const channelRule = TypeMoq.Mock.ofType<IExtensionChannelRule>(); + when(serviceContainer.get<IExtensionChannelRule>(IExtensionChannelRule, 'weekly')).thenReturn( + channelRule.object + ); + channelRule + .setup((c) => c.shouldLookForInsidersBuild(false)) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + when(insidersInstaller.install()).thenResolve(undefined); + when(insidersPrompt.promptToReload()).thenResolve(undefined); + await insidersExtensionService.handleChannel('weekly'); + verify(insidersInstaller.install()).once(); + verify(insidersPrompt.promptToReload()).once(); + channelRule.verifyAll(); + }); +}); + +// tslint:disable-next-line: max-func-body-length +suite('Insiders Extension Service - Activation', () => { + let appEnvironment: IApplicationEnvironment; + let serviceContainer: IServiceContainer; + let extensionChannelService: IExtensionChannelService; + let cmdManager: ICommandManager; + let insidersPrompt: IInsiderExtensionPrompt; + let registerCommandsAndHandlers: sinon.SinonStub<any>; + let handleChannel: sinon.SinonStub<any>; + let handleEdgeCases: sinon.SinonStub<any>; + let insidersInstaller: IExtensionBuildInstaller; + let insidersExtensionService: InsidersExtensionService; + let envUITEST_DISABLE_INSIDERSExists = false; + setup(() => { + envUITEST_DISABLE_INSIDERSExists = process.env.UITEST_DISABLE_INSIDERS !== undefined; + delete process.env.UITEST_DISABLE_INSIDERS; + extensionChannelService = mock(ExtensionChannelService); + insidersInstaller = mock(InsidersBuildInstaller); + appEnvironment = mock(ApplicationEnvironment); + cmdManager = mock(CommandManager); + serviceContainer = mock(ServiceContainer); + insidersPrompt = mock(InsidersExtensionPrompt); + handleEdgeCases = sinon.stub(InsidersExtensionService.prototype, 'handleEdgeCases'); + registerCommandsAndHandlers = sinon.stub(InsidersExtensionService.prototype, 'registerCommandsAndHandlers'); + registerCommandsAndHandlers.callsFake(() => Promise.resolve()); + }); + + teardown(() => { + if (envUITEST_DISABLE_INSIDERSExists) { + process.env.UITEST_DISABLE_INSIDERS = '1'; + } + sinon.restore(); + }); + + test('If install channel is handled in the edge cases, do not handle it again using the general way', async () => { + handleChannel = sinon.stub(InsidersExtensionService.prototype, 'handleChannel'); + handleChannel.callsFake(() => Promise.resolve()); + handleEdgeCases.callsFake(() => Promise.resolve(true)); + insidersExtensionService = new InsidersExtensionService( + instance(extensionChannelService), + instance(insidersPrompt), + instance(appEnvironment), + instance(cmdManager), + instance(serviceContainer), + instance(insidersInstaller), + [] + ); + when(extensionChannelService.getChannel()).thenReturn('daily'); + when(extensionChannelService.isChannelUsingDefaultConfiguration).thenReturn(false); + + await insidersExtensionService.activate(); + + verify(extensionChannelService.getChannel()).once(); + verify(extensionChannelService.isChannelUsingDefaultConfiguration).once(); + assert.ok(registerCommandsAndHandlers.calledOnce); + assert.ok(handleEdgeCases.calledOnce); + assert.ok(handleEdgeCases.calledWith('daily', false)); + assert.ok(handleChannel.notCalled); + }); + + test('If install channel is not handled in the edge cases, handle it using the general way', async () => { + handleChannel = sinon.stub(InsidersExtensionService.prototype, 'handleChannel'); + handleChannel.callsFake(() => Promise.resolve()); + handleEdgeCases.callsFake(() => Promise.resolve(false)); + insidersExtensionService = new InsidersExtensionService( + instance(extensionChannelService), + instance(insidersPrompt), + instance(appEnvironment), + instance(cmdManager), + instance(serviceContainer), + instance(insidersInstaller), + [] + ); + when(extensionChannelService.getChannel()).thenReturn('daily'); + when(extensionChannelService.isChannelUsingDefaultConfiguration).thenReturn(false); + + await insidersExtensionService.activate(); + + verify(extensionChannelService.getChannel()).once(); + verify(extensionChannelService.isChannelUsingDefaultConfiguration).once(); + assert.ok(registerCommandsAndHandlers.calledOnce); + assert.ok(handleEdgeCases.calledOnce); + assert.ok(handleEdgeCases.calledWith('daily', false)); + assert.ok(handleChannel.calledOnce); + }); + + test('Ensure channels are reliably handled in the background', async () => { + const handleChannelsDeferred = createDeferred<void>(); + handleChannel = sinon.stub(InsidersExtensionService.prototype, 'handleChannel'); + handleChannel.callsFake(() => handleChannelsDeferred.promise); + handleEdgeCases.callsFake(() => Promise.resolve(false)); + insidersExtensionService = new InsidersExtensionService( + instance(extensionChannelService), + instance(insidersPrompt), + instance(appEnvironment), + instance(cmdManager), + instance(serviceContainer), + instance(insidersInstaller), + [] + ); + when(extensionChannelService.getChannel()).thenReturn('daily'); + when(extensionChannelService.isChannelUsingDefaultConfiguration).thenReturn(false); + + const promise = insidersExtensionService.activate(); + const deferred = createDeferredFromPromise(promise); + await sleep(1); + + // Ensure activate() function has completed while handleChannel is still running + assert.equal(deferred.completed, true); + + handleChannelsDeferred.resolve(); + await sleep(1); + + assert.ok(registerCommandsAndHandlers.calledOnce); + assert.ok(handleEdgeCases.calledOnce); + assert.ok(handleChannel.calledOnce); + assert.ok(handleEdgeCases.calledWith('daily', false)); + }); +}); + +// tslint:disable-next-line: max-func-body-length +suite('Insiders Extension Service - Function handleEdgeCases()', () => { + let appEnvironment: TypeMoq.IMock<IApplicationEnvironment>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let extensionChannelService: TypeMoq.IMock<IExtensionChannelService>; + let cmdManager: TypeMoq.IMock<ICommandManager>; + let insidersPrompt: TypeMoq.IMock<IInsiderExtensionPrompt>; + let hasUserBeenNotifiedState: IPersistentState<boolean>; + let insidersInstaller: TypeMoq.IMock<IExtensionBuildInstaller>; + + let insidersExtensionService: InsidersExtensionService; + + function setupCommon() { + extensionChannelService = TypeMoq.Mock.ofType<IExtensionChannelService>(undefined, TypeMoq.MockBehavior.Strict); + insidersInstaller = TypeMoq.Mock.ofType<IExtensionBuildInstaller>(undefined, TypeMoq.MockBehavior.Strict); + appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(undefined, TypeMoq.MockBehavior.Strict); + cmdManager = TypeMoq.Mock.ofType<ICommandManager>(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(undefined, TypeMoq.MockBehavior.Strict); + insidersPrompt = TypeMoq.Mock.ofType<IInsiderExtensionPrompt>(undefined, TypeMoq.MockBehavior.Strict); + hasUserBeenNotifiedState = mock(PersistentState); + + insidersExtensionService = new InsidersExtensionService( + extensionChannelService.object, + insidersPrompt.object, + appEnvironment.object, + cmdManager.object, + serviceContainer.object, + insidersInstaller.object, + [] + ); + + insidersPrompt + .setup((p) => p.hasUserBeenNotified) + .returns(() => instance(hasUserBeenNotifiedState)) + // Basically means "we don't care" (necessary for strict mocks). + .verifiable(TypeMoq.Times.atLeast(0)); + } + + function verifyAll() { + // the most important ones: + insidersPrompt.verifyAll(); + insidersInstaller.verifyAll(); + extensionChannelService.verifyAll(); + // the other used interfaces: + appEnvironment.verifyAll(); + serviceContainer.verifyAll(); + cmdManager.verifyAll(); + } + + type TestInfo = { + vscodeChannel?: Channel; + extensionChannel?: Channel; + installChannel: ExtensionChannels; + isChannelUsingDefaultConfiguration?: boolean; + hasUserBeenNotified?: boolean; + }; + + function setState(info: TestInfo, checkPromptEnroll: boolean, checkDisable: boolean) { + if (info.vscodeChannel) { + appEnvironment.setup((e) => e.channel).returns(() => info.vscodeChannel!); + } + if (info.extensionChannel) { + appEnvironment.setup((e) => e.extensionChannel).returns(() => info.extensionChannel!); + } + + if (checkDisable) { + extensionChannelService.setup((ec) => ec.updateChannel('off')).returns(() => Promise.resolve()); + } + if (info.hasUserBeenNotified !== undefined) { + when(hasUserBeenNotifiedState.value).thenReturn(info.hasUserBeenNotified!); + } + if (checkPromptEnroll) { + insidersPrompt.setup((p) => p.promptToInstallInsiders()).returns(() => Promise.resolve()); + } + } + + suite('Case II - Verify Insiders Install Prompt is displayed when conditions are met', async () => { + const testsForHandleEdgeCaseII: TestInfo[] = [ + { + installChannel: 'daily', + // prompt to enroll + vscodeChannel: 'insiders', + hasUserBeenNotified: false, + isChannelUsingDefaultConfiguration: true + }, + { + installChannel: 'off', + // prompt to enroll + vscodeChannel: 'insiders', + hasUserBeenNotified: false, + isChannelUsingDefaultConfiguration: true + } + ]; + + setup(() => { + setupCommon(); + }); + + testsForHandleEdgeCaseII.forEach((testParams) => { + const testName = `Insiders Install Prompt is displayed when vscode channel = '${ + testParams.vscodeChannel + }', extension channel = '${testParams.extensionChannel}', install channel = '${ + testParams.installChannel + }', ${ + !testParams.hasUserBeenNotified + ? 'user has not been notified to install insiders' + : 'user has already been notified to install insiders' + }, isChannelUsingDefaultConfiguration = ${testParams.isChannelUsingDefaultConfiguration}`; + test(testName, async () => { + setState(testParams, true, false); + + await insidersExtensionService.handleEdgeCases( + testParams.installChannel, + testParams.isChannelUsingDefaultConfiguration! + ); + + verifyAll(); + verify(hasUserBeenNotifiedState.value).once(); + }); + }); + }); + + suite('Case III - Verify Insiders channel is set to off when conditions are met', async () => { + const testsForHandleEdgeCaseIII: TestInfo[] = [ + { + installChannel: 'daily', + // skip enroll + vscodeChannel: 'stable', + // disable + // with installChannel from above + extensionChannel: 'stable' + }, + { + installChannel: 'weekly', + // skip enroll + vscodeChannel: 'stable', + // disable + // with installChannel from above + extensionChannel: 'stable' + } + ]; + + setup(() => { + setupCommon(); + }); + + testsForHandleEdgeCaseIII.forEach((testParams) => { + const testName = `Insiders channel is set to off when vscode channel = '${ + testParams.vscodeChannel + }', extension channel = '${testParams.extensionChannel}', install channel = '${ + testParams.installChannel + }', ${ + !testParams.hasUserBeenNotified + ? 'user has not been notified to install insiders' + : 'user has already been notified to install insiders' + }, isChannelUsingDefaultConfiguration = ${testParams.isChannelUsingDefaultConfiguration}`; + test(testName, async () => { + setState(testParams, false, true); + + await insidersExtensionService.handleEdgeCases( + testParams.installChannel, + false // isDefault + ); + + verifyAll(); + verify(hasUserBeenNotifiedState.value).never(); + }); + }); + }); + + suite('Case IV - Verify no operation is performed if none of the case conditions are met', async () => { + const testsForHandleEdgeCaseIV: TestInfo[] = [ + { + installChannel: 'daily', + // skip enroll + vscodeChannel: 'insiders', + hasUserBeenNotified: true, + // skip disable + extensionChannel: 'insiders' + }, + { + installChannel: 'daily', + // skip enroll + vscodeChannel: 'insiders', + hasUserBeenNotified: false, + isChannelUsingDefaultConfiguration: false, + // skip disable + extensionChannel: 'insiders' + }, + { + installChannel: 'daily', + // skip enroll + vscodeChannel: 'stable', + // skip disable + extensionChannel: 'insiders' + }, + { + installChannel: 'off', + // skip enroll + vscodeChannel: 'insiders', + hasUserBeenNotified: true + }, + { + installChannel: 'off', + isChannelUsingDefaultConfiguration: true, + // skip enroll + vscodeChannel: 'insiders', + hasUserBeenNotified: true + }, + { + // skip re-enroll + installChannel: 'off', + isChannelUsingDefaultConfiguration: true, + // skip enroll + vscodeChannel: 'stable' + } + ]; + + setup(() => { + setupCommon(); + }); + + testsForHandleEdgeCaseIV.forEach((testParams) => { + const testName = `No operation is performed when vscode channel = '${ + testParams.vscodeChannel + }', extension channel = '${testParams.extensionChannel}', install channel = '${ + testParams.installChannel + }', ${ + !testParams.hasUserBeenNotified + ? 'user has not been notified to install insiders' + : 'user has already been notified to install insiders' + }, isChannelUsingDefaultConfiguration = ${testParams.isChannelUsingDefaultConfiguration}`; + test(testName, async () => { + setState(testParams, false, false); + + await insidersExtensionService.handleEdgeCases( + testParams.installChannel, + testParams.isChannelUsingDefaultConfiguration || testParams.installChannel === 'off' + ); + + verifyAll(); + if (testParams.hasUserBeenNotified === undefined) { + verify(hasUserBeenNotifiedState.value).never(); + } else { + verify(hasUserBeenNotifiedState.value).once(); + } + }); + }); + }); +}); + +// tslint:disable-next-line: max-func-body-length +suite('Insiders Extension Service - Function registerCommandsAndHandlers()', () => { + let appEnvironment: IApplicationEnvironment; + let serviceContainer: IServiceContainer; + let extensionChannelService: IExtensionChannelService; + let cmdManager: ICommandManager; + let insidersPrompt: IInsiderExtensionPrompt; + let channelChangeEvent: EventEmitter<ExtensionChannels>; + let handleChannel: sinon.SinonStub<any>; + let insidersExtensionService: InsidersExtensionService; + let insidersInstaller: IExtensionBuildInstaller; + setup(() => { + extensionChannelService = mock(ExtensionChannelService); + insidersInstaller = mock(InsidersBuildInstaller); + appEnvironment = mock(ApplicationEnvironment); + cmdManager = mock(CommandManager); + serviceContainer = mock(ServiceContainer); + insidersPrompt = mock(InsidersExtensionPrompt); + channelChangeEvent = new EventEmitter<ExtensionChannels>(); + handleChannel = sinon.stub(InsidersExtensionService.prototype, 'handleChannel'); + handleChannel.callsFake(() => Promise.resolve()); + insidersExtensionService = new InsidersExtensionService( + instance(extensionChannelService), + instance(insidersPrompt), + instance(appEnvironment), + instance(cmdManager), + instance(serviceContainer), + instance(insidersInstaller), + [] + ); + }); + + teardown(() => { + sinon.restore(); + channelChangeEvent.dispose(); + }); + + test('Ensure commands and handlers get registered, and disposables returned are in the disposable list', async () => { + const disposable1 = TypeMoq.Mock.ofType<IDisposable>(); + const disposable2 = TypeMoq.Mock.ofType<IDisposable>(); + const disposable3 = TypeMoq.Mock.ofType<IDisposable>(); + const disposable4 = TypeMoq.Mock.ofType<IDisposable>(); + when(extensionChannelService.onDidChannelChange).thenReturn(() => disposable1.object); + when(cmdManager.registerCommand(Commands.SwitchOffInsidersChannel, anything())).thenReturn(disposable2.object); + when(cmdManager.registerCommand(Commands.SwitchToInsidersDaily, anything())).thenReturn(disposable3.object); + when(cmdManager.registerCommand(Commands.SwitchToInsidersWeekly, anything())).thenReturn(disposable4.object); + + insidersExtensionService.registerCommandsAndHandlers(); + + expect(insidersExtensionService.disposables.length).to.equal(4); + verify(extensionChannelService.onDidChannelChange).once(); + verify(cmdManager.registerCommand(Commands.SwitchOffInsidersChannel, anything())).once(); + verify(cmdManager.registerCommand(Commands.SwitchToInsidersDaily, anything())).once(); + verify(cmdManager.registerCommand(Commands.SwitchToInsidersWeekly, anything())).once(); + }); + + test('Ensure commands and handlers get registered with the correct callback handlers', async () => { + const disposable1 = TypeMoq.Mock.ofType<IDisposable>(); + const disposable2 = TypeMoq.Mock.ofType<IDisposable>(); + const disposable3 = TypeMoq.Mock.ofType<IDisposable>(); + const disposable4 = TypeMoq.Mock.ofType<IDisposable>(); + let channelChangedHandler!: Function; + let switchTooffHandler!: Function; + let switchToInsidersDailyHandler!: Function; + let switchToweeklyHandler!: Function; + when(extensionChannelService.onDidChannelChange).thenReturn((cb) => { + channelChangedHandler = cb; + return disposable1.object; + }); + when(cmdManager.registerCommand(Commands.SwitchOffInsidersChannel, anything())).thenCall((_, cb) => { + switchTooffHandler = cb; + return disposable2.object; + }); + when(cmdManager.registerCommand(Commands.SwitchToInsidersDaily, anything())).thenCall((_, cb) => { + switchToInsidersDailyHandler = cb; + return disposable3.object; + }); + when(cmdManager.registerCommand(Commands.SwitchToInsidersWeekly, anything())).thenCall((_, cb) => { + switchToweeklyHandler = cb; + return disposable4.object; + }); + + insidersExtensionService.registerCommandsAndHandlers(); + + channelChangedHandler('Some channel'); + assert.ok(handleChannel.calledOnce); + + when(extensionChannelService.updateChannel('off')).thenResolve(); + await switchTooffHandler(); + verify(extensionChannelService.updateChannel('off')).once(); + + when(extensionChannelService.updateChannel('daily')).thenResolve(); + await switchToInsidersDailyHandler(); + verify(extensionChannelService.updateChannel('daily')).once(); + + when(extensionChannelService.updateChannel('weekly')).thenResolve(); + await switchToweeklyHandler(); + verify(extensionChannelService.updateChannel('weekly')).once(); + }); +}); diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts new file mode 100644 index 000000000000..04dd3f713b2a --- /dev/null +++ b/src/test/common/installer.test.ts @@ -0,0 +1,407 @@ +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; +import { ClipboardService } from '../../client/common/application/clipboard'; +import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; +import { CustomEditorService } from '../../client/common/application/customEditorService'; +import { DebugService } from '../../client/common/application/debugService'; +import { DebugSessionTelemetry } from '../../client/common/application/debugSessionTelemetry'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { Extensions } from '../../client/common/application/extensions'; +import { + IActiveResourceService, + IApplicationEnvironment, + IApplicationShell, + IClipboard, + ICommandManager, + ICustomEditorService, + IDebugService, + IDocumentManager, + ILiveShareApi, + IWorkspaceService +} from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { CryptoUtils } from '../../client/common/crypto'; +import { EditorUtils } from '../../client/common/editor'; +import { ExperimentsManager } from '../../client/common/experiments/manager'; +import { ExperimentService } from '../../client/common/experiments/service'; +import { FeatureDeprecationManager } from '../../client/common/featureDeprecationManager'; +import { + ExtensionInsidersDailyChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionInsidersWeeklyChannelRule +} from '../../client/common/insidersBuild/downloadChannelRules'; +import { ExtensionChannelService } from '../../client/common/insidersBuild/downloadChannelService'; +import { InsidersExtensionPrompt } from '../../client/common/insidersBuild/insidersExtensionPrompt'; +import { InsidersExtensionService } from '../../client/common/insidersBuild/insidersExtensionService'; +import { + ExtensionChannel, + IExtensionChannelRule, + IExtensionChannelService, + IInsiderExtensionPrompt +} from '../../client/common/insidersBuild/types'; +import { InstallationChannelManager } from '../../client/common/installer/channelManager'; +import { ProductInstaller } from '../../client/common/installer/productInstaller'; +import { + CTagsProductPathService, + DataScienceProductPathService, + FormatterProductPathService, + LinterProductPathService, + RefactoringLibraryProductPathService, + TestFrameworkProductPathService +} from '../../client/common/installer/productPath'; +import { ProductService } from '../../client/common/installer/productService'; +import { + IInstallationChannelManager, + IModuleInstaller, + IProductPathService, + IProductService +} from '../../client/common/installer/types'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; +import { BrowserService } from '../../client/common/net/browser'; +import { FileDownloader } from '../../client/common/net/fileDownloader'; +import { HttpClient } from '../../client/common/net/httpClient'; +import { NugetService } from '../../client/common/nuget/nugetService'; +import { INugetService } from '../../client/common/nuget/types'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { PathUtils } from '../../client/common/platform/pathUtils'; +import { CurrentProcess } from '../../client/common/process/currentProcess'; +import { ProcessLogger } from '../../client/common/process/logger'; +import { IProcessLogger, IProcessServiceFactory } from '../../client/common/process/types'; +import { TerminalActivator } from '../../client/common/terminal/activator'; +import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; +import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; +import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; +import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; +import { TerminalServiceFactory } from '../../client/common/terminal/factory'; +import { TerminalHelper } from '../../client/common/terminal/helper'; +import { SettingsShellDetector } from '../../client/common/terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalActivationHandler, + ITerminalActivator, + ITerminalHelper, + ITerminalServiceFactory, + TerminalActivationProviders +} from '../../client/common/terminal/types'; +import { + IAsyncDisposableRegistry, + IBrowserService, + IConfigurationService, + ICryptoUtils, + ICurrentProcess, + IEditorUtils, + IExperimentService, + IExperimentsManager, + IExtensions, + IFeatureDeprecationManager, + IFileDownloader, + IHttpClient, + IInstaller, + IInterpreterPathService, + IPathUtils, + IPersistentStateFactory, + IRandom, + IsWindows, + ModuleNamePurpose, + Product, + ProductType +} from '../../client/common/types'; +import { createDeferred } from '../../client/common/utils/async'; +import { getNamesAndValues } from '../../client/common/utils/enum'; +import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; +import { Random } from '../../client/common/utils/random'; +import { LiveShareApi } from '../../client/datascience/liveshare/liveshare'; +import { INotebookExecutionLogger } from '../../client/datascience/types'; +import { ImportTracker } from '../../client/telemetry/importTracker'; +import { IImportTracker } from '../../client/telemetry/types'; +import { rootWorkspaceUri, updateSetting } from '../common'; +import { MockModuleInstaller } from '../mocks/moduleInstaller'; +import { MockProcessService } from '../mocks/proc'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; +import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; + +// tslint:disable-next-line:max-func-body-length +suite('Installer', () => { + let ioc: UnitTestIocContainer; + const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); + const resource = IS_MULTI_ROOT_TEST ? workspaceUri : undefined; + suiteSetup(initializeTest); + setup(async () => { + await initializeTest(); + await resetSettings(); + initializeDI(); + }); + suiteTeardown(async () => { + await closeActiveWindows(); + await resetSettings(); + }); + teardown(async () => { + await ioc.dispose(); + await closeActiveWindows(); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerUnitTestTypes(); + ioc.registerFileSystemTypes(); + ioc.registerVariableTypes(); + ioc.registerLinterTypes(); + ioc.registerFormatterTypes(); + ioc.registerInterpreterStorageTypes(); + + ioc.serviceManager.addSingleton<IPersistentStateFactory>(IPersistentStateFactory, PersistentStateFactory); + ioc.serviceManager.addSingleton<IInstaller>(IInstaller, ProductInstaller); + ioc.serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); + ioc.serviceManager.addSingleton<IProcessLogger>(IProcessLogger, ProcessLogger); + ioc.serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess); + ioc.serviceManager.addSingleton<IInstallationChannelManager>( + IInstallationChannelManager, + InstallationChannelManager + ); + ioc.serviceManager.addSingletonInstance<ICommandManager>( + ICommandManager, + TypeMoq.Mock.ofType<ICommandManager>().object + ); + + ioc.serviceManager.addSingletonInstance<IApplicationShell>( + IApplicationShell, + TypeMoq.Mock.ofType<IApplicationShell>().object + ); + ioc.serviceManager.addSingleton<IConfigurationService>(IConfigurationService, ConfigurationService); + ioc.serviceManager.addSingleton<IWorkspaceService>(IWorkspaceService, WorkspaceService); + + ioc.registerMockInterpreterTypes(); + ioc.registerMockProcessTypes(); + ioc.serviceManager.addSingletonInstance<boolean>(IsWindows, false); + ioc.serviceManager.addSingletonInstance<IProductService>(IProductService, new ProductService()); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + CTagsProductPathService, + ProductType.WorkspaceSymbols + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + FormatterProductPathService, + ProductType.Formatter + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + LinterProductPathService, + ProductType.Linter + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + TestFrameworkProductPathService, + ProductType.TestFramework + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + RefactoringLibraryProductPathService, + ProductType.RefactoringLibrary + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + DataScienceProductPathService, + ProductType.DataScience + ); + + ioc.serviceManager.addSingleton<IActiveResourceService>(IActiveResourceService, ActiveResourceService); + ioc.serviceManager.addSingleton<IInterpreterPathService>(IInterpreterPathService, InterpreterPathService); + ioc.serviceManager.addSingleton<IExtensions>(IExtensions, Extensions); + ioc.serviceManager.addSingleton<IRandom>(IRandom, Random); + ioc.serviceManager.addSingleton<ITerminalServiceFactory>(ITerminalServiceFactory, TerminalServiceFactory); + ioc.serviceManager.addSingleton<IClipboard>(IClipboard, ClipboardService); + ioc.serviceManager.addSingleton<IDocumentManager>(IDocumentManager, DocumentManager); + ioc.serviceManager.addSingleton<IDebugService>(IDebugService, DebugService); + ioc.serviceManager.addSingleton<IApplicationEnvironment>(IApplicationEnvironment, ApplicationEnvironment); + ioc.serviceManager.addSingleton<IBrowserService>(IBrowserService, BrowserService); + ioc.serviceManager.addSingleton<IHttpClient>(IHttpClient, HttpClient); + ioc.serviceManager.addSingleton<IFileDownloader>(IFileDownloader, FileDownloader); + ioc.serviceManager.addSingleton<IEditorUtils>(IEditorUtils, EditorUtils); + ioc.serviceManager.addSingleton<INugetService>(INugetService, NugetService); + ioc.serviceManager.addSingleton<ITerminalActivator>(ITerminalActivator, TerminalActivator); + ioc.serviceManager.addSingleton<ITerminalActivationHandler>( + ITerminalActivationHandler, + PowershellTerminalActivationFailedHandler + ); + ioc.serviceManager.addSingleton<ILiveShareApi>(ILiveShareApi, LiveShareApi); + ioc.serviceManager.addSingleton<ICryptoUtils>(ICryptoUtils, CryptoUtils); + ioc.serviceManager.addSingleton<IExperimentsManager>(IExperimentsManager, ExperimentsManager); + ioc.serviceManager.addSingleton<IExperimentService>(IExperimentService, ExperimentService); + + ioc.serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + Bash, + TerminalActivationProviders.bashCShellFish + ); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell + ); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + PyEnvActivationCommandProvider, + TerminalActivationProviders.pyenv + ); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + CondaActivationCommandProvider, + TerminalActivationProviders.conda + ); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + PipEnvActivationCommandProvider, + TerminalActivationProviders.pipenv + ); + ioc.serviceManager.addSingleton<IFeatureDeprecationManager>( + IFeatureDeprecationManager, + FeatureDeprecationManager + ); + + ioc.serviceManager.addSingleton<IAsyncDisposableRegistry>(IAsyncDisposableRegistry, AsyncDisposableRegistry); + ioc.serviceManager.addSingleton<IMultiStepInputFactory>(IMultiStepInputFactory, MultiStepInputFactory); + ioc.serviceManager.addSingleton<IImportTracker>(IImportTracker, ImportTracker); + ioc.serviceManager.addBinding(IImportTracker, IExtensionSingleActivationService); + ioc.serviceManager.addBinding(IImportTracker, INotebookExecutionLogger); + ioc.serviceManager.addSingleton<IShellDetector>(IShellDetector, TerminalNameShellDetector); + ioc.serviceManager.addSingleton<IShellDetector>(IShellDetector, SettingsShellDetector); + ioc.serviceManager.addSingleton<IShellDetector>(IShellDetector, UserEnvironmentShellDetector); + ioc.serviceManager.addSingleton<IShellDetector>(IShellDetector, VSCEnvironmentShellDetector); + ioc.serviceManager.addSingleton<IInsiderExtensionPrompt>(IInsiderExtensionPrompt, InsidersExtensionPrompt); + ioc.serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + InsidersExtensionService + ); + ioc.serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ReloadVSCodeCommandHandler + ); + ioc.serviceManager.addSingleton<IExtensionChannelService>(IExtensionChannelService, ExtensionChannelService); + ioc.serviceManager.addSingleton<IExtensionChannelRule>( + IExtensionChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionChannel.off + ); + ioc.serviceManager.addSingleton<IExtensionChannelRule>( + IExtensionChannelRule, + ExtensionInsidersDailyChannelRule, + ExtensionChannel.daily + ); + ioc.serviceManager.addSingleton<IExtensionChannelRule>( + IExtensionChannelRule, + ExtensionInsidersWeeklyChannelRule, + ExtensionChannel.weekly + ); + ioc.serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + DebugSessionTelemetry + ); + ioc.serviceManager.addSingleton<ICustomEditorService>(ICustomEditorService, CustomEditorService); + } + async function resetSettings() { + await updateSetting('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); + } + + async function testCheckingIfProductIsInstalled(product: Product) { + const installer = ioc.serviceContainer.get<IInstaller>(IInstaller); + const processService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + const checkInstalledDef = createDeferred<boolean>(); + processService.onExec((_file, args, _options, callback) => { + const moduleName = installer.translateProductToModuleName(product, ModuleNamePurpose.run); + // args[0] is pyvsc-run-isolated.py. + if (args.length > 1 && args[1] === '-c' && args[2] === `import ${moduleName}`) { + checkInstalledDef.resolve(true); + } + callback({ stdout: '' }); + }); + await installer.isInstalled(product, resource); + await checkInstalledDef.promise; + } + getNamesAndValues<Product>(Product).forEach((prod) => { + test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async () => { + ioc.serviceManager.addSingletonInstance<IModuleInstaller>( + IModuleInstaller, + new MockModuleInstaller('one', false) + ); + ioc.serviceManager.addSingletonInstance<IModuleInstaller>( + IModuleInstaller, + new MockModuleInstaller('two', true) + ); + ioc.serviceManager.addSingletonInstance<ITerminalHelper>(ITerminalHelper, instance(mock(TerminalHelper))); + if ( + prod.value === Product.ctags || + prod.value === Product.unittest || + prod.value === Product.isort || + prod.value === Product.jupyter || + prod.value === Product.notebook || + prod.value === Product.pandas || + prod.value === Product.kernelspec || + prod.value === Product.nbconvert || + prod.value === Product.ipykernel + ) { + return; + } + await testCheckingIfProductIsInstalled(prod.value); + }); + }); + + async function testInstallingProduct(product: Product) { + const installer = ioc.serviceContainer.get<IInstaller>(IInstaller); + const checkInstalledDef = createDeferred<boolean>(); + const moduleInstallers = ioc.serviceContainer.getAll<MockModuleInstaller>(IModuleInstaller); + const moduleInstallerOne = moduleInstallers.find((item) => item.displayName === 'two')!; + + moduleInstallerOne.on('installModule', (moduleName) => { + const installName = installer.translateProductToModuleName(product, ModuleNamePurpose.install); + if (installName === moduleName) { + checkInstalledDef.resolve(); + } + }); + await installer.install(product); + await checkInstalledDef.promise; + } + getNamesAndValues<Product>(Product).forEach((prod) => { + test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async () => { + ioc.serviceManager.addSingletonInstance<IModuleInstaller>( + IModuleInstaller, + new MockModuleInstaller('one', false) + ); + ioc.serviceManager.addSingletonInstance<IModuleInstaller>( + IModuleInstaller, + new MockModuleInstaller('two', true) + ); + ioc.serviceManager.addSingletonInstance<ITerminalHelper>(ITerminalHelper, instance(mock(TerminalHelper))); + if ( + prod.value === Product.unittest || + prod.value === Product.ctags || + prod.value === Product.isort || + prod.value === Product.jupyter || + prod.value === Product.notebook || + prod.value === Product.pandas || + prod.value === Product.ipykernel || + prod.value === Product.kernelspec || + prod.value === Product.nbconvert + ) { + return; + } + await testInstallingProduct(prod.value); + }); + }); +}); diff --git a/src/test/common/installer/channelManager.unit.test.ts b/src/test/common/installer/channelManager.unit.test.ts new file mode 100644 index 000000000000..678dc86205d5 --- /dev/null +++ b/src/test/common/installer/channelManager.unit.test.ts @@ -0,0 +1,367 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { InstallationChannelManager } from '../../../client/common/installer/channelManager'; +import { IModuleInstaller } from '../../../client/common/installer/types'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { Product } from '../../../client/common/types'; +import { Installer } from '../../../client/common/utils/localize'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType } from '../../../client/pythonEnvironments/info'; + +// tslint:disable-next-line: max-func-body-length +suite('InstallationChannelManager - getInstallationChannel()', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let appShell: TypeMoq.IMock<IApplicationShell>; + // tslint:disable-next-line:no-any + let getInstallationChannels: sinon.SinonStub<any>; + // tslint:disable-next-line:no-any + let showNoInstallersMessage: sinon.SinonStub<any>; + const resource = Uri.parse('a'); + let installChannelManager: InstallationChannelManager; + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceContainer.setup((s) => s.get<IApplicationShell>(IApplicationShell)).returns(() => appShell.object); + }); + + teardown(() => { + sinon.restore(); + }); + + test('If there is exactly one installation channel, return it', async () => { + const moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller.setup((m) => m.name).returns(() => 'singleChannel'); + moduleInstaller + // tslint:disable-next-line:no-any + .setup((m) => (m as any).then) + .returns(() => undefined); + getInstallationChannels = sinon.stub(InstallationChannelManager.prototype, 'getInstallationChannels'); + getInstallationChannels.resolves([moduleInstaller.object]); + showNoInstallersMessage = sinon.stub(InstallationChannelManager.prototype, 'showNoInstallersMessage'); + showNoInstallersMessage.resolves(); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + // tslint:disable-next-line:no-any + const channel = await installChannelManager.getInstallationChannel(undefined as any, resource); + expect(channel).to.not.equal(undefined, 'Channel should be set'); + expect(channel!.name).to.equal('singleChannel'); + }); + + test('If no channels are returned by the resource, show no installer message and return', async () => { + getInstallationChannels = sinon.stub(InstallationChannelManager.prototype, 'getInstallationChannels'); + getInstallationChannels.resolves([]); + showNoInstallersMessage = sinon.stub(InstallationChannelManager.prototype, 'showNoInstallersMessage'); + showNoInstallersMessage.resolves(); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + // tslint:disable-next-line:no-any + const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + expect(channel).to.equal(undefined, 'should be undefined'); + assert.ok(showNoInstallersMessage.calledOnceWith(resource)); + }); + + test('If no channel is selected in the quickpick, return undefined', async () => { + const moduleInstaller1 = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller1.setup((m) => m.displayName).returns(() => 'moduleInstaller1'); + moduleInstaller1 + // tslint:disable-next-line:no-any + .setup((m) => (m as any).then) + .returns(() => undefined); + const moduleInstaller2 = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller2.setup((m) => m.displayName).returns(() => 'moduleInstaller2'); + moduleInstaller2 + // tslint:disable-next-line:no-any + .setup((m) => (m as any).then) + .returns(() => undefined); + appShell + .setup((a) => a.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + getInstallationChannels = sinon.stub(InstallationChannelManager.prototype, 'getInstallationChannels'); + getInstallationChannels.resolves([moduleInstaller1.object, moduleInstaller2.object]); + showNoInstallersMessage = sinon.stub(InstallationChannelManager.prototype, 'showNoInstallersMessage'); + showNoInstallersMessage.resolves(); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + // tslint:disable-next-line:no-any + const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + assert.ok(showNoInstallersMessage.notCalled); + appShell.verifyAll(); + expect(channel).to.equal(undefined, 'Channel should not be set'); + }); + + test('If multiple channels are returned by the resource, show quick pick of the channel names and return the selected channel installer', async () => { + const moduleInstaller1 = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller1.setup((m) => m.displayName).returns(() => 'moduleInstaller1'); + moduleInstaller1 + // tslint:disable-next-line:no-any + .setup((m) => (m as any).then) + .returns(() => undefined); + const moduleInstaller2 = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller2.setup((m) => m.displayName).returns(() => 'moduleInstaller2'); + moduleInstaller2 + // tslint:disable-next-line:no-any + .setup((m) => (m as any).then) + .returns(() => undefined); + const selection = { + label: 'some label', + description: '', + installer: moduleInstaller2.object + }; + appShell + .setup((a) => a.showQuickPick<typeof selection>(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(selection)) + .verifiable(TypeMoq.Times.once()); + getInstallationChannels = sinon.stub(InstallationChannelManager.prototype, 'getInstallationChannels'); + getInstallationChannels.resolves([moduleInstaller1.object, moduleInstaller2.object]); + showNoInstallersMessage = sinon.stub(InstallationChannelManager.prototype, 'showNoInstallersMessage'); + showNoInstallersMessage.resolves(); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + // tslint:disable-next-line:no-any + const channel = await installChannelManager.getInstallationChannel(Product.autopep8, resource); + assert.ok(showNoInstallersMessage.notCalled); + appShell.verifyAll(); + expect(channel).to.not.equal(undefined, 'Channel should be set'); + expect(channel!.displayName).to.equal('moduleInstaller2'); + }); +}); + +suite('InstallationChannelManager - getInstallationChannels()', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + const resource = Uri.parse('a'); + let installChannelManager: InstallationChannelManager; + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + }); + + test('If no installers are returned by serviceContainer, return an empty list', async () => { + serviceContainer.setup((s) => s.getAll<IModuleInstaller>(IModuleInstaller)).returns(() => []); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + const channel = await installChannelManager.getInstallationChannels(resource); + assert.deepEqual(channel, []); + }); + + test('Return highest priority supported installers', async () => { + const moduleInstallers: IModuleInstaller[] = []; + // Setup 2 installers with priority 1, where one is supported and other is not + for (let i = 0; i < 2; i = i + 1) { + const moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller + // tslint:disable-next-line:no-any + .setup((m) => (m as any).then) + .returns(() => undefined); + moduleInstaller.setup((m) => m.priority).returns(() => 1); + moduleInstaller.setup((m) => m.isSupported(resource)).returns(() => Promise.resolve(i % 2 === 0)); + moduleInstallers.push(moduleInstaller.object); + } + // Setup 3 installers with priority 2, where two are supported and other is not + for (let i = 2; i < 5; i = i + 1) { + const moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller + // tslint:disable-next-line:no-any + .setup((m) => (m as any).then) + .returns(() => undefined); + moduleInstaller.setup((m) => m.priority).returns(() => 2); + moduleInstaller.setup((m) => m.isSupported(resource)).returns(() => Promise.resolve(i % 2 === 0)); + moduleInstallers.push(moduleInstaller.object); + } + // Setup 2 installers with priority 3, but none are supported + for (let i = 5; i < 7; i = i + 1) { + const moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller + // tslint:disable-next-line:no-any + .setup((m) => (m as any).then) + .returns(() => undefined); + moduleInstaller.setup((m) => m.priority).returns(() => 3); + moduleInstaller.setup((m) => m.isSupported(resource)).returns(() => Promise.resolve(false)); + moduleInstallers.push(moduleInstaller.object); + } + serviceContainer.setup((s) => s.getAll<IModuleInstaller>(IModuleInstaller)).returns(() => moduleInstallers); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + const channels = await installChannelManager.getInstallationChannels(resource); + // Verify that highest supported priority is 2, so number of installers supported with that priority is 2 + expect(channels.length).to.equal(2); + for (let i = 0; i < 2; i = i + 1) { + expect(channels[i].priority).to.equal(2); + } + }); +}); + +// tslint:disable-next-line: max-func-body-length +suite('InstallationChannelManager - showNoInstallersMessage()', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + const resource = Uri.parse('a'); + let installChannelManager: InstallationChannelManager; + + setup(() => { + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + }); + + test('If no active interpreter is returned, simply return', async () => { + serviceContainer + .setup((s) => s.get<IInterpreterService>(IInterpreterService)) + .returns(() => interpreterService.object); + serviceContainer.setup((s) => s.get<IApplicationShell>(IApplicationShell)).verifiable(TypeMoq.Times.never()); + interpreterService.setup((i) => i.getActiveInterpreter(resource)).returns(() => Promise.resolve(undefined)); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + await installChannelManager.showNoInstallersMessage(resource); + serviceContainer.verifyAll(); + }); + + test('If active interpreter is Conda, show conda prompt', async () => { + const activeInterpreter = { + envType: EnvironmentType.Conda + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceContainer + .setup((s) => s.get<IInterpreterService>(IInterpreterService)) + .returns(() => interpreterService.object); + serviceContainer + .setup((s) => s.get<IApplicationShell>(IApplicationShell)) + .returns(() => appShell.object) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((i) => i.getActiveInterpreter(resource)) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(activeInterpreter as any)); + appShell + .setup((a) => a.showErrorMessage(Installer.noCondaOrPipInstaller(), Installer.searchForHelp())) + .verifiable(TypeMoq.Times.once()); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + await installChannelManager.showNoInstallersMessage(resource); + serviceContainer.verifyAll(); + appShell.verifyAll(); + }); + + test('If active interpreter is not Conda, show pip prompt', async () => { + const activeInterpreter = { + envType: EnvironmentType.Pipenv + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceContainer + .setup((s) => s.get<IInterpreterService>(IInterpreterService)) + .returns(() => interpreterService.object); + serviceContainer + .setup((s) => s.get<IApplicationShell>(IApplicationShell)) + .returns(() => appShell.object) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((i) => i.getActiveInterpreter(resource)) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(activeInterpreter as any)); + appShell + .setup((a) => a.showErrorMessage(Installer.noPipInstaller(), Installer.searchForHelp())) + .verifiable(TypeMoq.Times.once()); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + await installChannelManager.showNoInstallersMessage(resource); + serviceContainer.verifyAll(); + appShell.verifyAll(); + }); + + [EnvironmentType.Conda, EnvironmentType.Pipenv].forEach((interpreterType) => { + [ + { + osName: 'Windows', + isWindows: true, + isMac: false + }, + { + osName: 'Linux', + isWindows: false, + isMac: false + }, + { + osName: 'MacOS', + isWindows: false, + isMac: true + } + ].forEach((testParams) => { + const expectedURL = `https://www.bing.com/search?q=Install Pip ${testParams.osName} ${ + interpreterType === EnvironmentType.Conda ? 'Conda' : '' + }`; + test(`If \'Search for help\' is selected in error prompt, open correct URL for ${ + testParams.osName + } when Interpreter type is ${ + interpreterType === EnvironmentType.Conda ? 'Conda' : 'not Conda' + }`, async () => { + const activeInterpreter = { + envType: interpreterType + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + const platformService = TypeMoq.Mock.ofType<IPlatformService>(); + serviceContainer + .setup((s) => s.get<IInterpreterService>(IInterpreterService)) + .returns(() => interpreterService.object); + serviceContainer + .setup((s) => s.get<IApplicationShell>(IApplicationShell)) + .returns(() => appShell.object) + .verifiable(TypeMoq.Times.once()); + serviceContainer + .setup((s) => s.get<IPlatformService>(IPlatformService)) + .returns(() => platformService.object) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((i) => i.getActiveInterpreter(resource)) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(activeInterpreter as any)); + platformService.setup((p) => p.isWindows).returns(() => testParams.isWindows); + platformService.setup((p) => p.isMac).returns(() => testParams.isMac); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAny(), Installer.searchForHelp())) + .returns(() => Promise.resolve(Installer.searchForHelp())) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.openUrl(expectedURL)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + await installChannelManager.showNoInstallersMessage(resource); + serviceContainer.verifyAll(); + appShell.verifyAll(); + }); + }); + }); + test("If 'Search for help' is not selected in error prompt, don't open URL", async () => { + const activeInterpreter = { + envType: EnvironmentType.Conda + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + const platformService = TypeMoq.Mock.ofType<IPlatformService>(); + serviceContainer + .setup((s) => s.get<IInterpreterService>(IInterpreterService)) + .returns(() => interpreterService.object); + serviceContainer + .setup((s) => s.get<IApplicationShell>(IApplicationShell)) + .returns(() => appShell.object) + .verifiable(TypeMoq.Times.once()); + serviceContainer + .setup((s) => s.get<IPlatformService>(IPlatformService)) + .returns(() => platformService.object) + .verifiable(TypeMoq.Times.never()); + interpreterService + .setup((i) => i.getActiveInterpreter(resource)) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(activeInterpreter as any)); + platformService.setup((p) => p.isWindows).returns(() => true); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), Installer.searchForHelp())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.openUrl(TypeMoq.It.isAny())) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + await installChannelManager.showNoInstallersMessage(resource); + serviceContainer.verifyAll(); + appShell.verifyAll(); + }); +}); diff --git a/src/test/common/installer/condaInstaller.unit.test.ts b/src/test/common/installer/condaInstaller.unit.test.ts new file mode 100644 index 000000000000..28f955e03a7d --- /dev/null +++ b/src/test/common/installer/condaInstaller.unit.test.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { instance, mock, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { CondaInstaller } from '../../../client/common/installer/condaInstaller'; +import { InterpreterUri } from '../../../client/common/installer/types'; +import { ExecutionInfo, IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { ICondaService } from '../../../client/interpreter/contracts'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CondaEnvironmentInfo } from '../../../client/pythonEnvironments/discovery/locators/services/conda'; +import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; + +// tslint:disable-next-line: max-func-body-length +suite('Common - Conda Installer', () => { + let installer: CondaInstallerTest; + let serviceContainer: IServiceContainer; + let condaService: ICondaService; + let configService: IConfigurationService; + class CondaInstallerTest extends CondaInstaller { + // tslint:disable-next-line: no-unnecessary-override + public async getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise<ExecutionInfo> { + return super.getExecutionInfo(moduleName, resource); + } + } + setup(() => { + serviceContainer = mock(ServiceContainer); + condaService = mock(CondaService); + configService = mock(ConfigurationService); + when(serviceContainer.get<ICondaService>(ICondaService)).thenReturn(instance(condaService)); + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(configService)); + installer = new CondaInstallerTest(instance(serviceContainer)); + }); + test('Name and priority', async () => { + assert.equal(installer.displayName, 'Conda'); + assert.equal(installer.name, 'Conda'); + assert.equal(installer.priority, 0); + }); + test('Installer is not supported when conda is available variable is set to false', async () => { + const uri = Uri.file(__filename); + installer._isCondaAvailable = false; + + const supported = await installer.isSupported(uri); + + assert.equal(supported, false); + }); + test('Installer is not supported when conda is not available', async () => { + const uri = Uri.file(__filename); + when(condaService.isCondaAvailable()).thenResolve(false); + + const supported = await installer.isSupported(uri); + + assert.equal(supported, false); + }); + test('Installer is not supported when current env is not a conda env', async () => { + const uri = Uri.file(__filename); + const settings: IPythonSettings = mock(PythonSettings); + const pythonPath = 'my py path'; + + when(settings.pythonPath).thenReturn(pythonPath); + when(condaService.isCondaAvailable()).thenResolve(true); + when(configService.getSettings(uri)).thenReturn(instance(settings)); + when(condaService.isCondaEnvironment(pythonPath)).thenResolve(false); + + const supported = await installer.isSupported(uri); + + assert.equal(supported, false); + }); + test('Installer is supported when current env is a conda env', async () => { + const uri = Uri.file(__filename); + const settings: IPythonSettings = mock(PythonSettings); + const pythonPath = 'my py path'; + + when(settings.pythonPath).thenReturn(pythonPath); + when(condaService.isCondaAvailable()).thenResolve(true); + when(configService.getSettings(uri)).thenReturn(instance(settings)); + when(condaService.isCondaEnvironment(pythonPath)).thenResolve(true); + + const supported = await installer.isSupported(uri); + + assert.equal(supported, true); + }); + test('Include name of environment', async () => { + const uri = Uri.file(__filename); + const settings: IPythonSettings = mock(PythonSettings); + const pythonPath = 'my py path'; + const condaPath = 'some Conda Path'; + const condaEnv: CondaEnvironmentInfo = { + name: 'Hello', + path: 'Some Path' + }; + + when(configService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.pythonPath).thenReturn(pythonPath); + when(condaService.getCondaFile()).thenResolve(condaPath); + when(condaService.getCondaEnvironment(pythonPath)).thenResolve(condaEnv); + + const execInfo = await installer.getExecutionInfo('abc', uri); + + assert.deepEqual(execInfo, { args: ['install', '--name', condaEnv.name, 'abc', '-y'], execPath: condaPath }); + }); + test('Include path of environment', async () => { + const uri = Uri.file(__filename); + const settings: IPythonSettings = mock(PythonSettings); + const pythonPath = 'my py path'; + const condaPath = 'some Conda Path'; + const condaEnv: CondaEnvironmentInfo = { + name: '', + path: 'Some Path' + }; + + when(configService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.pythonPath).thenReturn(pythonPath); + when(condaService.getCondaFile()).thenResolve(condaPath); + when(condaService.getCondaEnvironment(pythonPath)).thenResolve(condaEnv); + + const execInfo = await installer.getExecutionInfo('abc', uri); + + assert.deepEqual(execInfo, { + args: ['install', '--prefix', condaEnv.path.fileToCommandArgument(), 'abc', '-y'], + execPath: condaPath + }); + }); +}); diff --git a/src/test/common/installer/extensionBuildInstaller.unit.test.ts b/src/test/common/installer/extensionBuildInstaller.unit.test.ts new file mode 100644 index 000000000000..13676a9658f8 --- /dev/null +++ b/src/test/common/installer/extensionBuildInstaller.unit.test.ts @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length no-invalid-this + +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Progress, Uri } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; +import { PVSC_EXTENSION_ID } from '../../../client/common/constants'; +import { + developmentBuildUri, + InsidersBuildInstaller, + StableBuildInstaller, + vsixFileExtension +} from '../../../client/common/installer/extensionBuildInstaller'; +import { FileDownloader } from '../../../client/common/net/fileDownloader'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { DownloadOptions, IFileDownloader, IOutputChannel } from '../../../client/common/types'; +import { ExtensionChannels } from '../../../client/common/utils/localize'; +import { MockOutputChannel } from '../../../test/mockClasses'; + +type ProgressReporterData = { message?: string; increment?: number }; + +suite('Extension build installer - Stable build installer', async () => { + let output: IOutputChannel; + let cmdManager: ICommandManager; + let appShell: IApplicationShell; + let stableBuildInstaller: StableBuildInstaller; + let progressReporter: Progress<ProgressReporterData>; + let progressReportStub: sinon.SinonStub; + setup(() => { + output = mock(MockOutputChannel); + cmdManager = mock(CommandManager); + appShell = mock(ApplicationShell); + stableBuildInstaller = new StableBuildInstaller(instance(output), instance(cmdManager), instance(appShell)); + progressReportStub = sinon.stub(); + progressReporter = { report: progressReportStub }; + }); + test('Installing stable build logs progress and installs stable', async () => { + when(output.append(ExtensionChannels.installingStableMessage())).thenReturn(); + when(output.appendLine(ExtensionChannels.installationCompleteMessage())).thenReturn(); + when(cmdManager.executeCommand('workbench.extensions.installExtension', PVSC_EXTENSION_ID)).thenResolve( + undefined + ); + when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); + await stableBuildInstaller.install(); + verify(output.append(ExtensionChannels.installingStableMessage())).once(); + verify(output.appendLine(ExtensionChannels.installationCompleteMessage())).once(); + verify(appShell.withProgressCustomIcon(anything(), anything())); + expect(progressReportStub.callCount).to.equal(1); + verify(cmdManager.executeCommand('workbench.extensions.installExtension', PVSC_EXTENSION_ID)).once(); + }); +}); + +suite('Extension build installer - Insiders build installer', async () => { + let output: IOutputChannel; + let cmdManager: ICommandManager; + let fileDownloader: IFileDownloader; + let fs: IFileSystem; + let appShell: IApplicationShell; + let insidersBuildInstaller: InsidersBuildInstaller; + let progressReporter: Progress<ProgressReporterData>; + let progressReportStub: sinon.SinonStub; + setup(() => { + output = mock(MockOutputChannel); + fileDownloader = mock(FileDownloader); + fs = mock(FileSystem); + cmdManager = mock(CommandManager); + appShell = mock(ApplicationShell); + progressReportStub = sinon.stub(); + progressReporter = { report: progressReportStub }; + insidersBuildInstaller = new InsidersBuildInstaller( + instance(output), + instance(fileDownloader), + instance(fs), + instance(cmdManager), + instance(appShell) + ); + }); + test('Installing Insiders build downloads and installs Insiders', async () => { + const vsixFilePath = 'path/to/vsix'; + const options = { + extension: vsixFileExtension, + outputChannel: output, + progressMessagePrefix: ExtensionChannels.downloadingInsidersMessage() + }; + when(output.append(ExtensionChannels.installingInsidersMessage())).thenReturn(); + when(output.appendLine(ExtensionChannels.startingDownloadOutputMessage())).thenReturn(); + when(output.appendLine(ExtensionChannels.downloadCompletedOutputMessage())).thenReturn(); + when(output.appendLine(ExtensionChannels.installationCompleteMessage())).thenReturn(); + when(fileDownloader.downloadFile(developmentBuildUri, anything())).thenCall( + (_, downloadOptions: DownloadOptions) => { + expect(downloadOptions.extension).to.equal(options.extension, 'Incorrect file extension'); + expect(downloadOptions.progressMessagePrefix).to.equal(options.progressMessagePrefix); + return Promise.resolve(vsixFilePath); + } + ); + when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); + when(cmdManager.executeCommand('workbench.extensions.installExtension', anything())).thenCall((_, cb) => { + assert.deepEqual(cb, Uri.file(vsixFilePath), 'Wrong VSIX installed'); + }); + when(fs.deleteFile(vsixFilePath)).thenResolve(); + + await insidersBuildInstaller.install(); + + verify(output.append(ExtensionChannels.installingInsidersMessage())).once(); + verify(output.appendLine(ExtensionChannels.startingDownloadOutputMessage())).once(); + verify(output.appendLine(ExtensionChannels.downloadCompletedOutputMessage())).once(); + verify(output.appendLine(ExtensionChannels.installationCompleteMessage())).once(); + verify(appShell.withProgressCustomIcon(anything(), anything())); + expect(progressReportStub.callCount).to.equal(1); + verify(cmdManager.executeCommand('workbench.extensions.installExtension', anything())).once(); + verify(fs.deleteFile(vsixFilePath)).once(); + }); +}); diff --git a/src/test/common/installer/installer.invalidPath.unit.test.ts b/src/test/common/installer/installer.invalidPath.unit.test.ts new file mode 100644 index 000000000000..fedf01322985 --- /dev/null +++ b/src/test/common/installer/installer.invalidPath.unit.test.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; +import '../../../client/common/extensions'; +import { ProductInstaller } from '../../../client/common/installer/productInstaller'; +import { ProductService } from '../../../client/common/installer/productService'; +import { IProductPathService, IProductService } from '../../../client/common/installer/types'; +import { IPersistentState, IPersistentStateFactory, Product } from '../../../client/common/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +use(chaiAsPromised); + +// tslint:disable: max-func-body-length +suite('Module Installer - Invalid Paths', () => { + [undefined, Uri.file('resource')].forEach((resource) => { + ['moduleName', path.join('users', 'dev', 'tool', 'executable')].forEach((pathToExecutable) => { + const isExecutableAModule = path.basename(pathToExecutable) === pathToExecutable; + + getNamesAndValues<Product>(Product).forEach((product) => { + let installer: ProductInstaller; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let app: TypeMoq.IMock<IApplicationShell>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let productPathService: TypeMoq.IMock<IProductPathService>; + let persistentState: TypeMoq.IMock<IPersistentStateFactory>; + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + const outputChannel = TypeMoq.Mock.ofType<OutputChannel>(); + + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) + .returns(() => new ProductService()); + app = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) + .returns(() => app.object); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) + .returns(() => workspaceService.object); + + productPathService = TypeMoq.Mock.ofType<IProductPathService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), TypeMoq.It.isAny())) + .returns(() => productPathService.object); + + const interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + + const pythonInterpreter = TypeMoq.Mock.ofType<PythonEnvironment>(); + // tslint:disable-next-line:no-any + pythonInterpreter.setup((i) => (i as any).then).returns(() => undefined); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonInterpreter.object)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) + .returns(() => interpreterService.object); + + persistentState = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())) + .returns(() => persistentState.object); + + installer = new ProductInstaller(serviceContainer.object, outputChannel.object); + }); + + switch (product.value) { + case Product.isort: + case Product.ctags: + case Product.rope: + case Product.unittest: + case Product.ipykernel: + case Product.kernelspec: + case Product.nbconvert: + case Product.notebook: + case Product.pandas: + case Product.jupyter: { + return; + } + default: { + test(`Ensure invalid path message is ${isExecutableAModule ? 'not displayed' : 'displayed'} ${ + product.name + } (${resource ? 'With a resource' : 'without a resource'})`, async () => { + // If the path to executable is a module, then we won't display error message indicating path is invalid. + + productPathService + .setup((p) => + p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource)) + ) + .returns(() => pathToExecutable) + .verifiable(TypeMoq.Times.atLeast(isExecutableAModule ? 0 : 1)); + productPathService + .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) + .returns(() => isExecutableAModule) + .verifiable(TypeMoq.Times.atLeastOnce()); + const anyParams = [0, 1, 2, 3, 4, 5].map(() => TypeMoq.It.isAny()); + app.setup((a) => a.showErrorMessage(TypeMoq.It.isAny(), ...anyParams)) + .callback((message) => { + if (!isExecutableAModule) { + expect(message).contains(pathToExecutable); + } + }) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.exactly(1)); + const persistValue = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + persistValue.setup((pv) => pv.value).returns(() => false); + persistValue.setup((pv) => pv.updateValue(TypeMoq.It.isValue(true))); + persistentState + .setup((ps) => + ps.createGlobalPersistentState<boolean>( + TypeMoq.It.isAnyString(), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => persistValue.object); + await installer.promptToInstall(product.value, resource); + productPathService.verifyAll(); + }); + } + } + }); + }); + }); +}); diff --git a/src/test/common/installer/installer.unit.test.ts b/src/test/common/installer/installer.unit.test.ts new file mode 100644 index 000000000000..929b34b98c6a --- /dev/null +++ b/src/test/common/installer/installer.unit.test.ts @@ -0,0 +1,1087 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length no-invalid-this messages-must-be-localized no-any + +import { assert, expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import { instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { Disposable, OutputChannel, Uri, WorkspaceFolder } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { Commands } from '../../../client/common/constants'; +import '../../../client/common/extensions'; +import { + CTagsInsllationScript, + CTagsInstaller, + FormatterInstaller, + LinterInstaller, + ProductInstaller +} from '../../../client/common/installer/productInstaller'; +import { ProductNames } from '../../../client/common/installer/productNames'; +import { ProductService } from '../../../client/common/installer/productService'; +import { + IInstallationChannelManager, + IModuleInstaller, + IProductPathService, + IProductService +} from '../../../client/common/installer/types'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { + ExecutionResult, + IProcessService, + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonExecutionService +} from '../../../client/common/process/types'; +import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { + IConfigurationService, + IDisposableRegistry, + InstallerResponse, + IOutputChannel, + IPersistentState, + IPersistentStateFactory, + ModuleNamePurpose, + Product, + ProductType +} from '../../../client/common/types'; +import { createDeferred, Deferred } from '../../../client/common/utils/async'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { sleep } from '../../common'; + +use(chaiAsPromised); + +suite('Module Installer only', () => { + [undefined, Uri.file('resource')].forEach((resource) => { + // tslint:disable-next-line: cyclomatic-complexity + getNamesAndValues<Product>(Product) + .concat([{ name: 'Unkown product', value: 404 }]) + // tslint:disable-next-line: cyclomatic-complexity + .forEach((product) => { + let disposables: Disposable[] = []; + let installer: ProductInstaller; + let installationChannel: TypeMoq.IMock<IInstallationChannelManager>; + let moduleInstaller: TypeMoq.IMock<IModuleInstaller>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let app: TypeMoq.IMock<IApplicationShell>; + let promptDeferred: Deferred<string>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let persistentStore: TypeMoq.IMock<IPersistentStateFactory>; + let outputChannel: TypeMoq.IMock<OutputChannel>; + let productPathService: TypeMoq.IMock<IProductPathService>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + const productService = new ProductService(); + + setup(() => { + promptDeferred = createDeferred<string>(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + outputChannel = TypeMoq.Mock.ofType<OutputChannel>(); + + disposables = []; + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) + .returns(() => disposables); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) + .returns(() => productService); + installationChannel = TypeMoq.Mock.ofType<IInstallationChannelManager>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInstallationChannelManager), TypeMoq.It.isAny())) + .returns(() => installationChannel.object); + app = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) + .returns(() => app.object); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) + .returns(() => workspaceService.object); + persistentStore = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())) + .returns(() => persistentStore.object); + + moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + // tslint:disable-next-line:no-any + moduleInstaller.setup((x: any) => x.then).returns(() => undefined); + installationChannel + .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(moduleInstaller.object)); + installationChannel + .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(moduleInstaller.object)); + + productPathService = TypeMoq.Mock.ofType<IProductPathService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProductPathService), TypeMoq.It.isAny())) + .returns(() => productPathService.object); + productPathService + .setup((p) => p.getExecutableNameFromSettings(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) + .returns(() => 'xyz'); + productPathService + .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) + .returns(() => true); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + const pythonInterpreter = TypeMoq.Mock.ofType<PythonEnvironment>(); + // tslint:disable-next-line:no-any + pythonInterpreter.setup((i) => (i as any).then).returns(() => undefined); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonInterpreter.object)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) + .returns(() => interpreterService.object); + installer = new ProductInstaller(serviceContainer.object, outputChannel.object); + }); + teardown(() => { + // This must be resolved, else all subsequent tests will fail (as this same promise will be used for other tests). + promptDeferred.resolve(); + disposables.forEach((disposable) => { + if (disposable) { + disposable.dispose(); + } + }); + sinon.restore(); + }); + + switch (product.value) { + case 404: { + test(`If product type is not recognized, throw error (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + app.setup((a) => + a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ).verifiable(TypeMoq.Times.never()); + const getProductType = sinon.stub(ProductService.prototype, 'getProductType'); + // tslint:disable-next-line: no-any + getProductType.returns('random' as any); + const promise = installer.promptToInstall(product.value, resource); + await expect(promise).to.eventually.be.rejectedWith(`Unknown product ${product.value}`); + app.verifyAll(); + assert.ok(getProductType.calledOnce); + }); + return; + } + case Product.isort: { + return; + } + case Product.ctags: { + test(`If platform is Windows, for module installer ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + }), print the instructions to install Ctags into the output channel`, async () => { + const platformService = TypeMoq.Mock.ofType<IPlatformService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + platformService.setup((p) => p.isWindows).returns(() => true); + outputChannel.setup((o) => o.appendLine(TypeMoq.It.isAny())).returns(() => undefined); + outputChannel + .setup((o) => + o.appendLine( + 'Install Universal Ctags Win32 to enable support for Workspace Symbols' + ) + ) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + outputChannel + .setup((o) => o.show()) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + const response = await installer.install(product.value, resource); + expect(response).to.be.equal(InstallerResponse.Ignore); + outputChannel.verifyAll(); + }); + test(`If platform is not Windows, for module installer ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + }), install Ctags using the corresponding script`, async () => { + const platformService = TypeMoq.Mock.ofType<IPlatformService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + platformService.setup((p) => p.isWindows).returns(() => false); + const termianlService = TypeMoq.Mock.ofType<ITerminalService>(); + const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))) + .returns(() => terminalServiceFactory.object); + terminalServiceFactory + .setup((p) => p.getTerminalService(resource)) + .returns(() => termianlService.object); + termianlService + .setup((t) => t.sendCommand(CTagsInsllationScript, [])) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + const response = await installer.install(product.value, resource); + expect(response).to.be.equal(InstallerResponse.Ignore); + termianlService.verifyAll(); + }); + test(`If platform is not Windows, for module installer ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + }), but installing Ctags fails with Error, log error and return`, async () => { + const platformService = TypeMoq.Mock.ofType<IPlatformService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + platformService.setup((p) => p.isWindows).returns(() => false); + const termianlService = TypeMoq.Mock.ofType<ITerminalService>(); + const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))) + .returns(() => terminalServiceFactory.object); + terminalServiceFactory + .setup((p) => p.getTerminalService(resource)) + .returns(() => termianlService.object); + termianlService + .setup((t) => t.sendCommand(CTagsInsllationScript, [])) + .returns(() => Promise.reject('Kaboom')) + .verifiable(TypeMoq.Times.once()); + const response = await installer.install(product.value, resource); + expect(response).to.be.equal(InstallerResponse.Ignore); + termianlService.verifyAll(); + }); + test(`If 'Yes' is selected on the install prompt for the the module installer ${ + product.name + } (${ + resource ? 'With a resource' : 'without a resource' + }), install module and return response`, async () => { + app.setup((a) => + a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve('Yes')) + .verifiable(TypeMoq.Times.once()); + const install = sinon.stub(CTagsInstaller.prototype, 'install'); + install.resolves(InstallerResponse.Installed); + const response = await installer.promptToInstall(product.value, resource); + expect(response).to.be.equal(InstallerResponse.Installed); + app.verifyAll(); + assert.ok(install.calledOnceWith(product.value, resource)); + }); + test(`If 'No' is selected on the install prompt for the module installer ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + }), return ignore response`, async () => { + app.setup((a) => + a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve('No')) + .verifiable(TypeMoq.Times.once()); + const install = sinon.stub(CTagsInstaller.prototype, 'install'); + install.resolves(InstallerResponse.Installed); + const response = await installer.promptToInstall(product.value, resource); + expect(response).to.be.equal(InstallerResponse.Ignore); + app.verifyAll(); + assert.ok(install.notCalled); + }); + return; + } + case Product.unittest: { + test(`Ensure resource info is passed into the module installer ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + const response = await installer.install(product.value, resource); + expect(response).to.be.equal(InstallerResponse.Installed); + }); + test(`Ensure resource info is passed into the module installer (created using ProductInstaller) ${ + product.name + } (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const response = await installer.install(product.value, resource); + expect(response).to.be.equal(InstallerResponse.Installed); + }); + break; + } + case Product.jupyter: + case Product.pandas: + case Product.nbconvert: + case Product.ipykernel: + case Product.kernelspec: + case Product.notebook: + { + test(`Ensure resource info is passed into the module installer ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + const moduleName = installer.translateProductToModuleName( + product.value, + ModuleNamePurpose.install + ); + + moduleInstaller + .setup((m) => + m.installModule( + TypeMoq.It.isValue(moduleName), + TypeMoq.It.isValue(resource), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => Promise.reject(new Error('UnitTesting'))); + + try { + await installer.install(product.value, resource); + } catch (ex) { + expect(ex.message).to.be.equal( + 'All data science packages require an interpreter be passed in' + ); + } + }); + } + break; + + default: + { + test(`Ensure the prompt is displayed only once, until the prompt is closed, ${ + product.name + } (${resource ? 'With a resource' : 'without a resource'})`, async () => { + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) + .returns(() => TypeMoq.Mock.ofType<WorkspaceFolder>().object) + .verifiable(TypeMoq.Times.exactly(resource ? 5 : 0)); + app.setup((a) => + a.showErrorMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => { + return promptDeferred.promise; + }) + .verifiable(TypeMoq.Times.once()); + const persistVal = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + persistVal.setup((p) => p.value).returns(() => false); + persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); + persistentStore + .setup((ps) => + ps.createGlobalPersistentState<boolean>( + TypeMoq.It.isAnyString(), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => persistVal.object); + + // Display first prompt. + installer.promptToInstall(product.value, resource).ignoreErrors(); + await sleep(1); + + // Display a few more prompts. + installer.promptToInstall(product.value, resource).ignoreErrors(); + await sleep(1); + installer.promptToInstall(product.value, resource).ignoreErrors(); + await sleep(1); + installer.promptToInstall(product.value, resource).ignoreErrors(); + await sleep(1); + installer.promptToInstall(product.value, resource).ignoreErrors(); + await sleep(1); + + app.verifyAll(); + workspaceService.verifyAll(); + }); + test(`Ensure the prompt is displayed again when previous prompt has been closed, ${ + product.name + } (${resource ? 'With a resource' : 'without a resource'})`, async () => { + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) + .returns(() => TypeMoq.Mock.ofType<WorkspaceFolder>().object) + .verifiable(TypeMoq.Times.exactly(resource ? 3 : 0)); + app.setup((a) => + a.showErrorMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.exactly(3)); + const persistVal = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + persistVal.setup((p) => p.value).returns(() => false); + persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); + persistentStore + .setup((ps) => + ps.createGlobalPersistentState<boolean>( + TypeMoq.It.isAnyString(), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => persistVal.object); + + await installer.promptToInstall(product.value, resource); + await installer.promptToInstall(product.value, resource); + await installer.promptToInstall(product.value, resource); + + app.verifyAll(); + workspaceService.verifyAll(); + }); + + if (product.value === Product.pylint) { + test(`Ensure the install prompt is not displayed when the user requests it not be shown again, ${ + product.name + } (${resource ? 'With a resource' : 'without a resource'})`, async () => { + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) + .returns(() => TypeMoq.Mock.ofType<WorkspaceFolder>().object) + .verifiable(TypeMoq.Times.exactly(resource ? 2 : 0)); + app.setup((a) => + a.showErrorMessage( + TypeMoq.It.isAnyString(), + TypeMoq.It.isValue('Install'), + TypeMoq.It.isValue('Select Linter'), + TypeMoq.It.isValue('Do not show again') + ) + ) + .returns(async () => { + return 'Do not show again'; + }) + .verifiable(TypeMoq.Times.once()); + const persistVal = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + let mockPersistVal = false; + persistVal + .setup((p) => p.value) + .returns(() => { + return mockPersistVal; + }); + persistVal + .setup((p) => p.updateValue(TypeMoq.It.isValue(true))) + .returns(() => { + mockPersistVal = true; + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.once()); + persistentStore + .setup((ps) => + ps.createGlobalPersistentState<boolean>( + TypeMoq.It.isAnyString(), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => { + return persistVal.object; + }) + .verifiable(TypeMoq.Times.exactly(3)); + + // Display first prompt. + const initialResponse = await installer.promptToInstall(product.value, resource); + + // Display a second prompt. + const secondResponse = await installer.promptToInstall(product.value, resource); + + expect(initialResponse).to.be.equal(InstallerResponse.Ignore); + expect(secondResponse).to.be.equal(InstallerResponse.Ignore); + + app.verifyAll(); + workspaceService.verifyAll(); + persistentStore.verifyAll(); + persistVal.verifyAll(); + }); + } else if (productService.getProductType(product.value) === ProductType.Linter) { + test(`Ensure the 'do not show again' prompt isn't shown for non-pylint linters, ${ + product.name + } (${resource ? 'With a resource' : 'without a resource'})`, async () => { + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) + .returns(() => TypeMoq.Mock.ofType<WorkspaceFolder>().object); + app.setup((a) => + a.showErrorMessage( + TypeMoq.It.isAnyString(), + TypeMoq.It.isValue('Install'), + TypeMoq.It.isValue('Select Linter') + ) + ) + .returns(async () => { + return undefined; + }) + .verifiable(TypeMoq.Times.once()); + app.setup((a) => + a.showErrorMessage( + TypeMoq.It.isAnyString(), + TypeMoq.It.isValue('Install'), + TypeMoq.It.isValue('Select Linter'), + TypeMoq.It.isValue('Do not show again') + ) + ) + .returns(async () => { + return undefined; + }) + .verifiable(TypeMoq.Times.never()); + const persistVal = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + let mockPersistVal = false; + persistVal + .setup((p) => p.value) + .returns(() => { + return mockPersistVal; + }); + persistVal + .setup((p) => p.updateValue(TypeMoq.It.isValue(true))) + .returns(() => { + mockPersistVal = true; + return Promise.resolve(); + }); + persistentStore + .setup((ps) => + ps.createGlobalPersistentState<boolean>( + TypeMoq.It.isAnyString(), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => { + return persistVal.object; + }); + + // Display the prompt. + await installer.promptToInstall(product.value, resource); + + // we're just ensuring the 'disable pylint' prompt never appears... + app.verifyAll(); + }); + } + } + + test(`Ensure resource info is passed into the module installer ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + const moduleName = installer.translateProductToModuleName( + product.value, + ModuleNamePurpose.install + ); + + moduleInstaller + .setup((m) => + m.installModule( + TypeMoq.It.isValue(moduleName), + TypeMoq.It.isValue(resource), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => Promise.reject(new Error('UnitTesting'))); + + try { + await installer.install(product.value, resource); + } catch (ex) { + moduleInstaller.verify( + (m) => + m.installModule( + TypeMoq.It.isValue(moduleName), + TypeMoq.It.isValue(resource), + TypeMoq.It.isValue(undefined) + ), + TypeMoq.Times.once() + ); + } + }); + + test(`Return InstallerResponse.Ignore for the module installer ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + }) if installation channel is not defined`, async () => { + const moduleName = installer.translateProductToModuleName( + product.value, + ModuleNamePurpose.install + ); + + moduleInstaller + .setup((m) => + m.installModule( + TypeMoq.It.isValue(moduleName), + TypeMoq.It.isValue(resource), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => Promise.reject(new Error('UnitTesting'))); + installationChannel.reset(); + installationChannel + .setup((i) => i.getInstallationChannel(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + try { + const response = await installer.install(product.value, resource); + expect(response).to.equal(InstallerResponse.Ignore); + } catch (ex) { + assert(false, `Should not throw errors, ${ex}`); + } + }); + test(`Ensure resource info is passed into the module installer (created using ProductInstaller) ${ + product.name + } (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const moduleName = installer.translateProductToModuleName( + product.value, + ModuleNamePurpose.install + ); + + moduleInstaller + .setup((m) => + m.installModule( + TypeMoq.It.isValue(moduleName), + TypeMoq.It.isValue(resource), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => Promise.reject(new Error('UnitTesting'))); + + try { + await installer.install(product.value, resource); + } catch (ex) { + moduleInstaller.verify( + (m) => + m.installModule( + TypeMoq.It.isValue(moduleName), + TypeMoq.It.isValue(resource), + TypeMoq.It.isValue(undefined) + ), + TypeMoq.Times.once() + ); + } + }); + } + // Test isInstalled() + if (product.value === Product.unittest) { + test(`Method isInstalled() returns true for module installer ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + const result = await installer.isInstalled(product.value, resource); + expect(result).to.equal(true, 'Should be true'); + }); + } else { + test(`Method isInstalled() returns true if module is installed for the module installer ${ + product.name + } (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + const pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))) + .returns(() => pythonExecutionFactory.object); + pythonExecutionFactory + .setup((p) => p.createActivatedEnvironment(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + pythonExecutionService + // tslint:disable-next-line: no-any + .setup((p) => (p as any).then) + .returns(() => undefined); + pythonExecutionService + .setup((p) => p.isModuleInstalled(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + const response = await installer.isInstalled(product.value, resource); + expect(response).to.equal(true, 'Should be true'); + pythonExecutionService.verifyAll(); + }); + test(`Method isInstalled() returns false if module is not installed for the module installer ${ + product.name + } (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + const pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))) + .returns(() => pythonExecutionFactory.object); + pythonExecutionFactory + .setup((p) => p.createActivatedEnvironment(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + pythonExecutionService + // tslint:disable-next-line: no-any + .setup((p) => (p as any).then) + .returns(() => undefined); + pythonExecutionService + .setup((p) => p.isModuleInstalled(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + + const response = await installer.isInstalled(product.value, resource); + expect(response).to.equal(false, 'Should be false'); + + pythonExecutionService.verifyAll(); + }); + test(`Method isInstalled() returns true if running 'path/to/module_executable --version' succeeds for the module installer ${ + product.name + } (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + const processService = TypeMoq.Mock.ofType<IProcessService>(); + serviceContainer + .setup((c) => c.get<IProcessServiceFactory>(IProcessServiceFactory)) + .returns(() => processServiceFactory.object); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + processService + // tslint:disable-next-line: no-any + .setup((p) => (p as any).then) + .returns(() => undefined); + const executionResult: ExecutionResult<string> = { + stdout: 'output' + }; + processService + .setup((p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(executionResult)) + .verifiable(TypeMoq.Times.once()); + + productPathService.reset(); + productPathService + .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) + .returns(() => false); + + const response = await installer.isInstalled(product.value, resource); + expect(response).to.equal(true, 'Should be true'); + + processService.verifyAll(); + }); + test(`Method isInstalled() returns false if running 'path/to/module_executable --version' fails for the module installer ${ + product.name + } (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + const processService = TypeMoq.Mock.ofType<IProcessService>(); + serviceContainer + .setup((c) => c.get<IProcessServiceFactory>(IProcessServiceFactory)) + .returns(() => processServiceFactory.object); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + processService + // tslint:disable-next-line: no-any + .setup((p) => (p as any).then) + .returns(() => undefined); + processService + .setup((p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.reject('Kaboom')) + .verifiable(TypeMoq.Times.once()); + + productPathService.reset(); + productPathService + .setup((p) => p.isExecutableAModule(TypeMoq.It.isAny(), TypeMoq.It.isValue(resource))) + .returns(() => false); + + const response = await installer.isInstalled(product.value, resource); + expect(response).to.equal(false, 'Should be false'); + + processService.verifyAll(); + }); + } + + // Test promptToInstall() when no interpreter is selected + test(`If no interpreter is selected, promptToInstall() doesn't prompt for product ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) + .returns(() => TypeMoq.Mock.ofType<WorkspaceFolder>().object) + .verifiable(TypeMoq.Times.never()); + app.setup((a) => + a.showErrorMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + const persistVal = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + persistVal.setup((p) => p.value).returns(() => false); + persistVal.setup((p) => p.updateValue(TypeMoq.It.isValue(true))); + persistentStore + .setup((ps) => + ps.createGlobalPersistentState<boolean>( + TypeMoq.It.isAnyString(), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => persistVal.object); + + interpreterService.reset(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + await installer.promptToInstall(product.value, resource); + + app.verifyAll(); + interpreterService.verifyAll(); + workspaceService.verifyAll(); + }); + }); + + suite('Test LinterInstaller.promptToInstallImplementation', () => { + class LinterInstallerTest extends LinterInstaller { + // tslint:disable-next-line:no-unnecessary-override + public async promptToInstallImplementation(product: Product, uri?: Uri): Promise<InstallerResponse> { + return super.promptToInstallImplementation(product, uri); + } + protected getStoredResponse(_key: string) { + return false; + } + protected isExecutableAModule(_product: Product, _resource?: Uri) { + return true; + } + } + let installer: LinterInstallerTest; + let appShell: IApplicationShell; + let configService: IConfigurationService; + let workspaceService: IWorkspaceService; + let productService: IProductService; + let cmdManager: ICommandManager; + setup(() => { + const serviceContainer = mock(ServiceContainer); + appShell = mock(ApplicationShell); + configService = mock(ConfigurationService); + workspaceService = mock(WorkspaceService); + productService = mock(ProductService); + cmdManager = mock(CommandManager); + const outputChannel = TypeMoq.Mock.ofType<IOutputChannel>(); + + when(serviceContainer.get<IApplicationShell>(IApplicationShell)).thenReturn(instance(appShell)); + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn( + instance(configService) + ); + when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspaceService)); + when(serviceContainer.get<IProductService>(IProductService)).thenReturn(instance(productService)); + when(serviceContainer.get<ICommandManager>(ICommandManager)).thenReturn(instance(cmdManager)); + + installer = new LinterInstallerTest(instance(serviceContainer), outputChannel.object); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Ensure 3 options for pylint', async () => { + const product = Product.pylint; + const options = ['Select Linter', 'Do not show again']; + const productName = ProductNames.get(product)!; + await installer.promptToInstallImplementation(product, resource); + verify( + appShell.showErrorMessage( + `Linter ${productName} is not installed.`, + 'Install', + options[0], + options[1] + ) + ).once(); + }); + test('Ensure select linter command is invoked', async () => { + const product = Product.pylint; + const options = ['Select Linter', 'Do not show again']; + const productName = ProductNames.get(product)!; + when( + appShell.showErrorMessage( + `Linter ${productName} is not installed.`, + 'Install', + options[0], + options[1] + ) + // tslint:disable-next-line:no-any + ).thenResolve('Select Linter' as any); + when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); + + const response = await installer.promptToInstallImplementation(product, resource); + + verify( + appShell.showErrorMessage( + `Linter ${productName} is not installed.`, + 'Install', + options[0], + options[1] + ) + ).once(); + verify(cmdManager.executeCommand(Commands.Set_Linter)).once(); + expect(response).to.be.equal(InstallerResponse.Ignore); + }); + test('If install button is selected, install linter and return response', async () => { + const product = Product.pylint; + const options = ['Select Linter', 'Do not show again']; + const productName = ProductNames.get(product)!; + when( + appShell.showErrorMessage( + `Linter ${productName} is not installed.`, + 'Install', + options[0], + options[1] + ) + // tslint:disable-next-line:no-any + ).thenResolve('Install' as any); + when(cmdManager.executeCommand(Commands.Set_Linter)).thenResolve(undefined); + const install = sinon.stub(LinterInstaller.prototype, 'install'); + install.resolves(InstallerResponse.Installed); + + const response = await installer.promptToInstallImplementation(product, resource); + + expect(response).to.be.equal(InstallerResponse.Installed); + assert.ok(install.calledOnceWith(product, resource, undefined)); + }); + }); + + suite('Test FormatterInstaller.promptToInstallImplementation', () => { + class FormatterInstallerTest extends FormatterInstaller { + // tslint:disable-next-line:no-unnecessary-override + public async promptToInstallImplementation(product: Product, uri?: Uri): Promise<InstallerResponse> { + return super.promptToInstallImplementation(product, uri); + } + protected getStoredResponse(_key: string) { + return false; + } + protected isExecutableAModule(_product: Product, _resource?: Uri) { + return true; + } + } + let installer: FormatterInstallerTest; + let appShell: IApplicationShell; + let configService: IConfigurationService; + let workspaceService: IWorkspaceService; + let productService: IProductService; + let cmdManager: ICommandManager; + setup(() => { + const serviceContainer = mock(ServiceContainer); + appShell = mock(ApplicationShell); + configService = mock(ConfigurationService); + workspaceService = mock(WorkspaceService); + productService = mock(ProductService); + cmdManager = mock(CommandManager); + const outputChannel = TypeMoq.Mock.ofType<IOutputChannel>(); + + when(serviceContainer.get<IApplicationShell>(IApplicationShell)).thenReturn(instance(appShell)); + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn( + instance(configService) + ); + when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspaceService)); + when(serviceContainer.get<IProductService>(IProductService)).thenReturn(instance(productService)); + when(serviceContainer.get<ICommandManager>(ICommandManager)).thenReturn(instance(cmdManager)); + + installer = new FormatterInstallerTest(instance(serviceContainer), outputChannel.object); + }); + + teardown(() => { + sinon.restore(); + }); + + test('If nothing is selected, return Ignore as response', async () => { + const product = Product.autopep8; + when( + appShell.showErrorMessage( + `Formatter autopep8 is not installed. Install?`, + 'Yes', + 'Use black', + 'Use yapf' + ) + // tslint:disable-next-line: no-any + ).thenReturn(undefined as any); + + const response = await installer.promptToInstallImplementation(product, resource); + + verify( + appShell.showErrorMessage( + `Formatter autopep8 is not installed. Install?`, + 'Yes', + 'Use black', + 'Use yapf' + ) + ).once(); + expect(response).to.equal(InstallerResponse.Ignore); + }); + + test('If `Yes` is selected, install product', async () => { + const product = Product.autopep8; + const install = sinon.stub(FormatterInstaller.prototype, 'install'); + install.resolves(InstallerResponse.Installed); + + when( + appShell.showErrorMessage( + `Formatter autopep8 is not installed. Install?`, + 'Yes', + 'Use black', + 'Use yapf' + ) + // tslint:disable-next-line: no-any + ).thenReturn('Yes' as any); + const response = await installer.promptToInstallImplementation(product, resource); + + verify( + appShell.showErrorMessage( + `Formatter autopep8 is not installed. Install?`, + 'Yes', + 'Use black', + 'Use yapf' + ) + ).once(); + expect(response).to.equal(InstallerResponse.Installed); + assert.ok(install.calledOnceWith(product, resource, undefined)); + }); + + test('If `Use black` is selected, install black formatter', async () => { + const product = Product.autopep8; + const install = sinon.stub(FormatterInstaller.prototype, 'install'); + install.resolves(InstallerResponse.Installed); + + when( + appShell.showErrorMessage( + `Formatter autopep8 is not installed. Install?`, + 'Yes', + 'Use black', + 'Use yapf' + ) + // tslint:disable-next-line: no-any + ).thenReturn('Use black' as any); + when(configService.updateSetting('formatting.provider', 'black', resource)).thenResolve(); + + const response = await installer.promptToInstallImplementation(product, resource); + + verify( + appShell.showErrorMessage( + `Formatter autopep8 is not installed. Install?`, + 'Yes', + 'Use black', + 'Use yapf' + ) + ).once(); + expect(response).to.equal(InstallerResponse.Installed); + verify(configService.updateSetting('formatting.provider', 'black', resource)).once(); + assert.ok(install.calledOnceWith(Product.black, resource, undefined)); + }); + + test('If `Use yapf` is selected, install black formatter', async () => { + const product = Product.autopep8; + const install = sinon.stub(FormatterInstaller.prototype, 'install'); + install.resolves(InstallerResponse.Installed); + + when( + appShell.showErrorMessage( + `Formatter autopep8 is not installed. Install?`, + 'Yes', + 'Use black', + 'Use yapf' + ) + // tslint:disable-next-line: no-any + ).thenReturn('Use yapf' as any); + when(configService.updateSetting('formatting.provider', 'yapf', resource)).thenResolve(); + + const response = await installer.promptToInstallImplementation(product, resource); + + verify( + appShell.showErrorMessage( + `Formatter autopep8 is not installed. Install?`, + 'Yes', + 'Use black', + 'Use yapf' + ) + ).once(); + expect(response).to.equal(InstallerResponse.Installed); + verify(configService.updateSetting('formatting.provider', 'yapf', resource)).once(); + assert.ok(install.calledOnceWith(Product.yapf, resource, undefined)); + }); + }); + }); +}); diff --git a/src/test/common/installer/moduleInstaller.unit.test.ts b/src/test/common/installer/moduleInstaller.unit.test.ts new file mode 100644 index 000000000000..93594c050e46 --- /dev/null +++ b/src/test/common/installer/moduleInstaller.unit.test.ts @@ -0,0 +1,617 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length no-invalid-this + +import { assert } from 'chai'; +import * as path from 'path'; +// tslint:disable-next-line: match-default-export-name +import rewiremock from 'rewiremock'; +import { SemVer } from 'semver'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { + CancellationTokenSource, + Disposable, + OutputChannel, + ProgressLocation, + Uri, + WorkspaceConfiguration +} from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; +import { STANDARD_OUTPUT_CHANNEL } from '../../../client/common/constants'; +import { CondaInstaller } from '../../../client/common/installer/condaInstaller'; +import { ModuleInstaller } from '../../../client/common/installer/moduleInstaller'; +import { PipEnvInstaller, pipenvName } from '../../../client/common/installer/pipEnvInstaller'; +import { PipInstaller } from '../../../client/common/installer/pipInstaller'; +import { ProductInstaller } from '../../../client/common/installer/productInstaller'; +import { IInstallationChannelManager, IModuleInstaller, InterpreterUri } from '../../../client/common/installer/types'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { + ExecutionInfo, + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + IPythonSettings, + ModuleNamePurpose, + Product +} from '../../../client/common/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { Products } from '../../../client/common/utils/localize'; +import { noop } from '../../../client/common/utils/misc'; +import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; + +const isolated = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'pyvsc-run-isolated.py').replace(/\\/g, '/'); + +/* Complex test to ensure we cover all combinations: +We could have written separate tests for each installer, but we'd be replicate code. +Both approaches have their benefits. + +Combinations of: +1. With and without a workspace. +2. Http Proxy configuration. +3. All products. +4. Different versions of Python. +5. With and without conda. +6. Conda environments with names and without names. +7. All installers. +*/ +suite('Module Installer', () => { + class TestModuleInstaller extends ModuleInstaller { + public get priority(): number { + return 0; + } + public get name(): string { + return ''; + } + public get displayName(): string { + return ''; + } + public isSupported(_resource?: InterpreterUri): Promise<boolean> { + return Promise.resolve(false); + } + public getExecutionInfo(_moduleName: string, _resource?: InterpreterUri): Promise<ExecutionInfo> { + return Promise.resolve({ moduleName: 'executionInfo', args: [] }); + } + // tslint:disable-next-line: no-unnecessary-override + public elevatedInstall(execPath: string, args: string[]) { + return super.elevatedInstall(execPath, args); + } + } + let outputChannel: TypeMoq.IMock<IOutputChannel>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + const pythonPath = path.join(__dirname, 'python'); + + suite('Method _elevatedInstall()', async () => { + let installer: TestModuleInstaller; + const execPath = 'execPath'; + const args = ['1', '2']; + const command = `"${execPath.replace(/\\/g, '/')}" ${args.join(' ')}`; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + outputChannel = TypeMoq.Mock.ofType<IOutputChannel>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) + .returns(() => outputChannel.object); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + installer = new TestModuleInstaller(serviceContainer.object); + }); + teardown(() => { + rewiremock.disable(); + }); + + test('Show error message if sudo exec fails with error', async () => { + const error = 'Error message'; + const sudoPromptMock = { + exec: (_command: any, _options: any, callBackFn: Function) => callBackFn(error, 'stdout', 'stderr') + }; + rewiremock.enable(); + rewiremock('sudo-prompt').with(sudoPromptMock); + appShell + .setup((a) => a.showErrorMessage(error)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + outputChannel + // tslint:disable-next-line: messages-must-be-localized + .setup((o) => o.appendLine(`[Elevated] ${command}`)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + installer.elevatedInstall(execPath, args); + appShell.verifyAll(); + outputChannel.verifyAll(); + }); + + test('Show stdout if sudo exec succeeds', async () => { + const stdout = 'stdout'; + const sudoPromptMock = { + exec: (_command: any, _options: any, callBackFn: Function) => callBackFn(undefined, stdout, undefined) + }; + rewiremock.enable(); + rewiremock('sudo-prompt').with(sudoPromptMock); + outputChannel + .setup((o) => o.show()) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + outputChannel + // tslint:disable-next-line: messages-must-be-localized + .setup((o) => o.appendLine(`[Elevated] ${command}`)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + outputChannel + .setup((o) => o.append(stdout)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + installer.elevatedInstall(execPath, args); + outputChannel.verifyAll(); + }); + + test('Show stderr if sudo exec gives a warning with stderr', async () => { + const stderr = 'stderr'; + const sudoPromptMock = { + exec: (_command: any, _options: any, callBackFn: Function) => callBackFn(undefined, undefined, stderr) + }; + rewiremock.enable(); + rewiremock('sudo-prompt').with(sudoPromptMock); + outputChannel + // tslint:disable-next-line: messages-must-be-localized + .setup((o) => o.appendLine(`[Elevated] ${command}`)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + outputChannel + .setup((o) => o.show()) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + outputChannel + // tslint:disable-next-line: messages-must-be-localized + .setup((o) => o.append(`Warning: ${stderr}`)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + installer.elevatedInstall(execPath, args); + outputChannel.verifyAll(); + }); + }); + + [CondaInstaller, PipInstaller, PipEnvInstaller, TestModuleInstaller].forEach((installerClass) => { + // Proxy info is relevant only for PipInstaller. + const proxyServers = installerClass === PipInstaller ? ['', 'proxy:1234'] : ['']; + proxyServers.forEach((proxyServer) => { + [undefined, Uri.file('/users/dev/xyz')].forEach((resource) => { + // Conda info is relevant only for CondaInstaller. + const condaEnvs = + installerClass === CondaInstaller + ? [ + { name: 'My-Env01', path: '' }, + { name: '', path: path.join('conda', 'path') }, + { name: 'My-Env01 With Spaces', path: '' }, + { name: '', path: path.join('conda with spaces', 'path') } + ] + : []; + [undefined, ...condaEnvs].forEach((condaEnvInfo) => { + const testProxySuffix = proxyServer.length === 0 ? 'without proxy info' : 'with proxy info'; + const testCondaEnv = condaEnvInfo + ? condaEnvInfo.name + ? 'without conda name' + : 'with conda path' + : 'without conda'; + const testSuite = [testProxySuffix, testCondaEnv].filter((item) => item.length > 0).join(', '); + suite(`${installerClass.name} (${testSuite})`, () => { + let disposables: Disposable[] = []; + let installationChannel: TypeMoq.IMock<IInstallationChannelManager>; + let terminalService: TypeMoq.IMock<ITerminalService>; + let configService: TypeMoq.IMock<IConfigurationService>; + let fs: TypeMoq.IMock<IFileSystem>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let installer: IModuleInstaller; + const condaExecutable = 'my.exe'; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => appShell.object); + + fs = TypeMoq.Mock.ofType<IFileSystem>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))) + .returns(() => fs.object); + + disposables = []; + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) + .returns(() => disposables); + + installationChannel = TypeMoq.Mock.ofType<IInstallationChannelManager>(); + serviceContainer + .setup((c) => + c.get(TypeMoq.It.isValue(IInstallationChannelManager), TypeMoq.It.isAny()) + ) + .returns(() => installationChannel.object); + + const condaService = TypeMoq.Mock.ofType<ICondaService>(); + condaService.setup((c) => c.getCondaFile()).returns(() => Promise.resolve(condaExecutable)); + condaService + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(condaEnvInfo)); + + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) + .returns(() => configService.object); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + pythonSettings.setup((p) => p.pythonPath).returns(() => pythonPath); + configService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns(() => pythonSettings.object); + + terminalService = TypeMoq.Mock.ofType<ITerminalService>(); + const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>(); + terminalServiceFactory + .setup((f) => f.getTerminalService(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => terminalService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalServiceFactory), TypeMoq.It.isAny())) + .returns(() => terminalServiceFactory.object); + + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) + .returns(() => interpreterService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICondaService), TypeMoq.It.isAny())) + .returns(() => condaService.object); + + const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) + .returns(() => workspaceService.object); + const http = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + http.setup((h) => h.get(TypeMoq.It.isValue('proxy'), TypeMoq.It.isAny())).returns( + () => proxyServer + ); + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('http'))) + .returns(() => http.object); + + installer = new installerClass(serviceContainer.object); + }); + teardown(() => { + disposables.forEach((disposable) => { + if (disposable) { + disposable.dispose(); + } + }); + sinon.restore(); + }); + function setActiveInterpreter(activeInterpreter?: PythonEnvironment) { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(activeInterpreter)) + .verifiable(TypeMoq.Times.atLeastOnce()); + } + getModuleNamesForTesting().forEach((product) => { + const moduleName = product.moduleName; + async function installModuleAndVerifyCommand(command: string, expectedArgs: string[]) { + terminalService + .setup((t) => + t.sendCommand( + TypeMoq.It.isValue(command), + TypeMoq.It.isValue(expectedArgs), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await installer.installModule(moduleName, resource); + terminalService.verifyAll(); + } + + if (product.value === Product.pylint) { + // tslint:disable-next-line:no-shadowed-variable + generatePythonInterpreterVersions().forEach((interpreterInfo) => { + const majorVersion = interpreterInfo.version ? interpreterInfo.version.major : 0; + if (majorVersion === 2) { + const testTitle = `Ensure install arg is \'pylint<2.0.0\' in ${ + interpreterInfo.version ? interpreterInfo.version.raw : '' + }`; + if (installerClass === PipInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const proxyArgs = + proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; + const expectedArgs = [ + isolated, + 'pip', + ...proxyArgs, + 'install', + '-U', + '"pylint<2.0.0"' + ]; + await installModuleAndVerifyCommand(pythonPath, expectedArgs); + }); + } + if (installerClass === PipEnvInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const expectedArgs = ['install', '"pylint<2.0.0"', '--dev']; + await installModuleAndVerifyCommand(pipenvName, expectedArgs); + }); + } + if (installerClass === CondaInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const expectedArgs = ['install']; + if (condaEnvInfo && condaEnvInfo.name) { + expectedArgs.push('--name'); + expectedArgs.push(condaEnvInfo.name.toCommandArgument()); + } else if (condaEnvInfo && condaEnvInfo.path) { + expectedArgs.push('--prefix'); + expectedArgs.push(condaEnvInfo.path.fileToCommandArgument()); + } + expectedArgs.push('"pylint<2.0.0"'); + expectedArgs.push('-y'); + await installModuleAndVerifyCommand(condaExecutable, expectedArgs); + }); + } + } else { + const testTitle = `Ensure install arg is \'pylint\' in ${ + interpreterInfo.version ? interpreterInfo.version.raw : '' + }`; + if (installerClass === PipInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const proxyArgs = + proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; + const expectedArgs = [ + isolated, + 'pip', + ...proxyArgs, + 'install', + '-U', + 'pylint' + ]; + await installModuleAndVerifyCommand(pythonPath, expectedArgs); + }); + } + if (installerClass === PipEnvInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const expectedArgs = ['install', 'pylint', '--dev']; + await installModuleAndVerifyCommand(pipenvName, expectedArgs); + }); + } + if (installerClass === CondaInstaller) { + test(testTitle, async () => { + setActiveInterpreter(interpreterInfo); + const expectedArgs = ['install']; + if (condaEnvInfo && condaEnvInfo.name) { + expectedArgs.push('--name'); + expectedArgs.push(condaEnvInfo.name.toCommandArgument()); + } else if (condaEnvInfo && condaEnvInfo.path) { + expectedArgs.push('--prefix'); + expectedArgs.push(condaEnvInfo.path.fileToCommandArgument()); + } + expectedArgs.push('pylint'); + expectedArgs.push('-y'); + await installModuleAndVerifyCommand(condaExecutable, expectedArgs); + }); + } + } + }); + return; + } + + if (installerClass === TestModuleInstaller) { + suite(`If interpreter type is Unknown (${product.name})`, async () => { + test(`If 'python.globalModuleInstallation' is set to true and pythonPath directory is read only, do an elevated install`, async () => { + const info = TypeMoq.Mock.ofType<PythonEnvironment>(); + info.setup((t: any) => t.then).returns(() => undefined); + info.setup((t) => t.envType).returns(() => EnvironmentType.Unknown); + info.setup((t) => t.version).returns(() => new SemVer('3.5.0-final')); + setActiveInterpreter(info.object); + pythonSettings.setup((p) => p.globalModuleInstallation).returns(() => true); + const elevatedInstall = sinon.stub( + TestModuleInstaller.prototype, + 'elevatedInstall' + ); + elevatedInstall.returns(); + fs.setup((f) => f.isDirReadonly(path.dirname(pythonPath))).returns(() => + Promise.resolve(true) + ); + try { + await installer.installModule(product.name, resource); + } catch (ex) { + noop(); + } + const args = [isolated, 'executionInfo']; + assert.ok(elevatedInstall.calledOnceWith(pythonPath, args)); + interpreterService.verifyAll(); + }); + test(`If 'python.globalModuleInstallation' is set to true and pythonPath directory is not read only, send command to terminal`, async () => { + const info = TypeMoq.Mock.ofType<PythonEnvironment>(); + info.setup((t: any) => t.then).returns(() => undefined); + info.setup((t) => t.envType).returns(() => EnvironmentType.Unknown); + info.setup((t) => t.version).returns(() => new SemVer('3.5.0-final')); + setActiveInterpreter(info.object); + pythonSettings.setup((p) => p.globalModuleInstallation).returns(() => true); + fs.setup((f) => f.isDirReadonly(path.dirname(pythonPath))).returns(() => + Promise.resolve(false) + ); + const args = [isolated, 'executionInfo']; + terminalService + .setup((t) => t.sendCommand(pythonPath, args, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + try { + await installer.installModule(product.name, resource); + } catch (ex) { + noop(); + } + interpreterService.verifyAll(); + terminalService.verifyAll(); + }); + test(`If 'python.globalModuleInstallation' is not set to true, concatenate arguments with '--user' flag and send command to terminal`, async () => { + const info = TypeMoq.Mock.ofType<PythonEnvironment>(); + info.setup((t: any) => t.then).returns(() => undefined); + info.setup((t) => t.envType).returns(() => EnvironmentType.Unknown); + info.setup((t) => t.version).returns(() => new SemVer('3.5.0-final')); + setActiveInterpreter(info.object); + pythonSettings.setup((p) => p.globalModuleInstallation).returns(() => false); + const args = [isolated, 'executionInfo', '--user']; + terminalService + .setup((t) => t.sendCommand(pythonPath, args, undefined)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + try { + await installer.installModule(product.name, resource); + } catch (ex) { + noop(); + } + interpreterService.verifyAll(); + terminalService.verifyAll(); + }); + test(`ignores failures in IFileSystem.isDirReadonly()`, async () => { + const info = TypeMoq.Mock.ofType<PythonEnvironment>(); + info.setup((t: any) => t.then).returns(() => undefined); + info.setup((t) => t.envType).returns(() => EnvironmentType.Unknown); + info.setup((t) => t.version).returns(() => new SemVer('3.5.0-final')); + setActiveInterpreter(info.object); + pythonSettings.setup((p) => p.globalModuleInstallation).returns(() => true); + const elevatedInstall = sinon.stub( + TestModuleInstaller.prototype, + 'elevatedInstall' + ); + elevatedInstall.returns(); + const err = new Error('oops!'); + fs.setup((f) => f.isDirReadonly(path.dirname(pythonPath))).returns(() => + Promise.reject(err) + ); + + try { + await installer.installModule(product.name, resource); + } catch (ex) { + noop(); + } + const args = [isolated, 'executionInfo']; + assert.ok(elevatedInstall.calledOnceWith(pythonPath, args)); + interpreterService.verifyAll(); + }); + test('If cancellation token is provided, install while showing progress', async () => { + const options = { + location: ProgressLocation.Notification, + cancellable: true, + title: Products.installingModule().format(product.name) + }; + appShell + .setup((a) => a.withProgress(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((expected, _) => assert.deepEqual(expected, options)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + try { + await installer.installModule( + product.name, + resource, + new CancellationTokenSource().token + ); + } catch (ex) { + noop(); + } + interpreterService.verifyAll(); + appShell.verifyAll(); + }); + }); + } + + if (installerClass === PipInstaller) { + test(`Ensure getActiveInterpreter is used in PipInstaller (${product.name})`, async () => { + setActiveInterpreter(); + try { + await installer.installModule(product.name, resource); + } catch { + noop(); + } + interpreterService.verifyAll(); + }); + } + if (installerClass === PipInstaller) { + test(`Test Args (${product.name})`, async () => { + setActiveInterpreter(); + const proxyArgs = proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; + const expectedArgs = [isolated, 'pip', ...proxyArgs, 'install', '-U', moduleName]; + await installModuleAndVerifyCommand(pythonPath, expectedArgs); + interpreterService.verifyAll(); + }); + } + if (installerClass === PipEnvInstaller) { + test(`Test args (${product.name})`, async () => { + setActiveInterpreter(); + const expectedArgs = ['install', moduleName, '--dev']; + if (moduleName === 'black') { + expectedArgs.push('--pre'); + } + await installModuleAndVerifyCommand(pipenvName, expectedArgs); + }); + } + if (installerClass === CondaInstaller) { + test(`Test args (${product.name})`, async () => { + setActiveInterpreter(); + const expectedArgs = ['install']; + if (condaEnvInfo && condaEnvInfo.name) { + expectedArgs.push('--name'); + expectedArgs.push(condaEnvInfo.name.toCommandArgument()); + } else if (condaEnvInfo && condaEnvInfo.path) { + expectedArgs.push('--prefix'); + expectedArgs.push(condaEnvInfo.path.fileToCommandArgument()); + } + expectedArgs.push(moduleName); + expectedArgs.push('-y'); + await installModuleAndVerifyCommand(condaExecutable, expectedArgs); + }); + } + }); + }); + }); + }); + }); + }); +}); + +function generatePythonInterpreterVersions() { + const versions: SemVer[] = ['2.7.0-final', '3.4.0-final', '3.5.0-final', '3.6.0-final', '3.7.0-final'].map( + (ver) => new SemVer(ver) + ); + return versions.map((version) => { + const info = TypeMoq.Mock.ofType<PythonEnvironment>(); + info.setup((t: any) => t.then).returns(() => undefined); + info.setup((t) => t.envType).returns(() => EnvironmentType.VirtualEnv); + info.setup((t) => t.version).returns(() => version); + return info.object; + }); +} + +function getModuleNamesForTesting(): { name: string; value: Product; moduleName: string }[] { + return getNamesAndValues<Product>(Product) + .map((product) => { + let moduleName = ''; + const mockSvc = TypeMoq.Mock.ofType<IServiceContainer>().object; + const mockOutChnl = TypeMoq.Mock.ofType<OutputChannel>().object; + try { + const prodInstaller = new ProductInstaller(mockSvc, mockOutChnl); + moduleName = prodInstaller.translateProductToModuleName(product.value, ModuleNamePurpose.install); + return { name: product.name, value: product.value, moduleName }; + } catch { + return; + } + }) + .filter((item) => item !== undefined) as { name: string; value: Product; moduleName: string }[]; +} diff --git a/src/test/common/installer/pipEnvInstaller.unit.test.ts b/src/test/common/installer/pipEnvInstaller.unit.test.ts new file mode 100644 index 000000000000..fc3a0b1d94bf --- /dev/null +++ b/src/test/common/installer/pipEnvInstaller.unit.test.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { PipEnvInstaller } from '../../../client/common/installer/pipEnvInstaller'; +import { IInterpreterLocatorService, PIPENV_SERVICE } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +// tslint:disable-next-line: max-func-body-length +suite('PipEnv installer', async () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let locatorService: TypeMoq.IMock<IInterpreterLocatorService>; + let pipEnvInstaller: PipEnvInstaller; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + locatorService = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterLocatorService), TypeMoq.It.isValue(PIPENV_SERVICE))) + .returns(() => locatorService.object); + pipEnvInstaller = new PipEnvInstaller(serviceContainer.object); + }); + + test('Installer name is pipenv', () => { + expect(pipEnvInstaller.name).to.equal('pipenv'); + }); + + test('Installer priority is 10', () => { + expect(pipEnvInstaller.priority).to.equal(10); + }); + + test('If InterpreterUri is Pipenv interpreter, method isSupported() returns true', async () => { + const interpreter = { + envType: EnvironmentType.Pipenv + }; + // tslint:disable-next-line: no-any + const result = await pipEnvInstaller.isSupported(interpreter as any); + expect(result).to.equal(true, 'Should be true'); + }); + + test('If InterpreterUri is Python interpreter but not of type Pipenv, method isSupported() returns false', async () => { + const interpreter = { + envType: EnvironmentType.Conda + }; + // tslint:disable-next-line: no-any + const result = await pipEnvInstaller.isSupported(interpreter as any); + expect(result).to.equal(false, 'Should be false'); + }); + + test('If InterpreterUri is Resource, and if resource contains pipEnv interpreters, return true', async () => { + const resource = Uri.parse('a'); + locatorService + .setup((p) => p.getInterpreters(resource)) + .returns(() => + Promise.resolve([ + TypeMoq.Mock.ofType<PythonEnvironment>().object, + TypeMoq.Mock.ofType<PythonEnvironment>().object + ]) + ); + const result = await pipEnvInstaller.isSupported(resource); + expect(result).to.equal(true, 'Should be true'); + }); + + test('If InterpreterUri is Resource, and if resource does not contain pipEnv interpreters, return false', async () => { + const resource = Uri.parse('a'); + locatorService.setup((p) => p.getInterpreters(resource)).returns(() => Promise.resolve([])); + const result = await pipEnvInstaller.isSupported(resource); + expect(result).to.equal(false, 'Should be false'); + }); +}); diff --git a/src/test/common/installer/pipInstaller.unit.test.ts b/src/test/common/installer/pipInstaller.unit.test.ts new file mode 100644 index 000000000000..de27c0b4e938 --- /dev/null +++ b/src/test/common/installer/pipInstaller.unit.test.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { PipInstaller } from '../../../client/common/installer/pipInstaller'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable-next-line: max-func-body-length +suite('Pip installer', async () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>; + let pipInstaller: PipInstaller; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))) + .returns(() => pythonExecutionFactory.object); + pipInstaller = new PipInstaller(serviceContainer.object); + }); + + test('Installer name is Pip', () => { + expect(pipInstaller.name).to.equal('Pip'); + }); + + test('Installer priority is 0', () => { + expect(pipInstaller.priority).to.equal(0); + }); + + test('If InterpreterUri is Python interpreter, Python execution factory is called with the correct arguments', async () => { + const pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + const interpreter = { + path: 'pythonPath' + }; + pythonExecutionFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .callback((options) => { + assert.deepEqual(options, { resource: undefined, pythonPath: interpreter.path }); + }) + .returns(() => Promise.resolve(pythonExecutionService.object)) + .verifiable(TypeMoq.Times.once()); + pythonExecutionService + // tslint:disable-next-line: no-any + .setup((p) => (p as any).then) + .returns(() => undefined); + + // tslint:disable-next-line: no-any + await pipInstaller.isSupported(interpreter as any); + + pythonExecutionFactory.verifyAll(); + }); + + test('If InterpreterUri is Resource, Python execution factory is called with the correct arguments', async () => { + const pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + const resource = Uri.parse('a'); + pythonExecutionFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .callback((options) => { + assert.deepEqual(options, { resource, pythonPath: undefined }); + }) + .returns(() => Promise.resolve(pythonExecutionService.object)) + .verifiable(TypeMoq.Times.once()); + pythonExecutionService + // tslint:disable-next-line: no-any + .setup((p) => (p as any).then) + .returns(() => undefined); + + await pipInstaller.isSupported(resource); + + pythonExecutionFactory.verifyAll(); + }); + + test('Method isSupported() returns true if pip module is installed', async () => { + const pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + const resource = Uri.parse('a'); + pythonExecutionFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + pythonExecutionService + // tslint:disable-next-line: no-any + .setup((p) => (p as any).then) + .returns(() => undefined); + pythonExecutionService.setup((p) => p.isModuleInstalled('pip')).returns(() => Promise.resolve(true)); + + const expected = await pipInstaller.isSupported(resource); + + expect(expected).to.equal(true, 'Should be true'); + }); + + test('Method isSupported() returns false if pip module is not installed', async () => { + const pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + const resource = Uri.parse('a'); + pythonExecutionFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + pythonExecutionService + // tslint:disable-next-line: no-any + .setup((p) => (p as any).then) + .returns(() => undefined); + pythonExecutionService.setup((p) => p.isModuleInstalled('pip')).returns(() => Promise.resolve(false)); + + const expected = await pipInstaller.isSupported(resource); + + expect(expected).to.equal(false, 'Should be false'); + }); + + test('Method isSupported() returns false if checking if pip module is installed fails with error', async () => { + const pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + const resource = Uri.parse('a'); + pythonExecutionFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + pythonExecutionService + // tslint:disable-next-line: no-any + .setup((p) => (p as any).then) + .returns(() => undefined); + pythonExecutionService + .setup((p) => p.isModuleInstalled('pip')) + .returns(() => Promise.reject('Unable to check if module is installed')); + + const expected = await pipInstaller.isSupported(resource); + + expect(expected).to.equal(false, 'Should be false'); + }); +}); diff --git a/src/test/common/installer/poetryInstaller.unit.test.ts b/src/test/common/installer/poetryInstaller.unit.test.ts new file mode 100644 index 000000000000..2f72b40e97dc --- /dev/null +++ b/src/test/common/installer/poetryInstaller.unit.test.ts @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { PoetryInstaller } from '../../../client/common/installer/poetryInstaller'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { ProcessService } from '../../../client/common/process/proc'; +import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { ExecutionInfo, IConfigurationService } from '../../../client/common/types'; +import { ServiceContainer } from '../../../client/ioc/container'; + +// tslint:disable-next-line:max-func-body-length +suite('Module Installer - Poetry', () => { + class TestInstaller extends PoetryInstaller { + // tslint:disable-next-line:no-unnecessary-override + public getExecutionInfo(moduleName: string, resource?: Uri): Promise<ExecutionInfo> { + return super.getExecutionInfo(moduleName, resource); + } + } + let poetryInstaller: TestInstaller; + let workspaceService: IWorkspaceService; + let configurationService: IConfigurationService; + let fileSystem: IFileSystem; + let processServiceFactory: IProcessServiceFactory; + setup(() => { + const serviceContainer = mock(ServiceContainer); + workspaceService = mock(WorkspaceService); + configurationService = mock(ConfigurationService); + fileSystem = mock(FileSystem); + processServiceFactory = mock(ProcessServiceFactory); + + poetryInstaller = new TestInstaller( + instance(serviceContainer), + instance(workspaceService), + instance(configurationService), + instance(fileSystem), + instance(processServiceFactory) + ); + }); + + test('Installer name is poetry', () => { + expect(poetryInstaller.name).to.equal('poetry'); + }); + + test('Installer priority is 10', () => { + expect(poetryInstaller.priority).to.equal(10); + }); + + test('Installer display name is poetry', () => { + expect(poetryInstaller.displayName).to.equal('poetry'); + }); + + test('Is not supported when there is no resource', async () => { + const supported = await poetryInstaller.isSupported(); + assert.equal(supported, false); + }); + test('Is not supported when there is no workspace', async () => { + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(); + + const supported = await poetryInstaller.isSupported(Uri.file(__filename)); + + assert.equal(supported, false); + }); + test('Is not supported when the poetry file does not exists', async () => { + const uri = Uri.file(__dirname); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); + when(fileSystem.fileExists(anything())).thenResolve(false); + + const supported = await poetryInstaller.isSupported(Uri.file(__filename)); + + assert.equal(supported, false); + }); + test('Is not supported when the poetry is not available (with stderr)', async () => { + const uri = Uri.file(__dirname); + const processService = mock(ProcessService); + const settings = mock(PythonSettings); + + when(configurationService.getSettings(anything())).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry'); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); + when(fileSystem.fileExists(anything())).thenResolve(true); + when(processServiceFactory.create(anything())).thenResolve(instance(processService)); + when(processService.exec(anything(), anything(), anything())).thenResolve({ stderr: 'Kaboom', stdout: '' }); + + const supported = await poetryInstaller.isSupported(Uri.file(__filename)); + + assert.equal(supported, false); + }); + test('Is not supported when the poetry is not available (with error running poetry)', async () => { + const uri = Uri.file(__dirname); + const processService = mock(ProcessService); + const settings = mock(PythonSettings); + + when(configurationService.getSettings(anything())).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry'); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); + when(fileSystem.fileExists(anything())).thenResolve(true); + when(processServiceFactory.create(anything())).thenResolve(instance(processService)); + when(processService.exec(anything(), anything(), anything())).thenReject(new Error('Kaboom')); + + const supported = await poetryInstaller.isSupported(Uri.file(__filename)); + + assert.equal(supported, false); + }); + test('Is supported', async () => { + const uri = Uri.file(__dirname); + const processService = mock(ProcessService); + const settings = mock(PythonSettings); + + when(configurationService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry path'); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); + when(fileSystem.fileExists(anything())).thenResolve(true); + when(processServiceFactory.create(uri)).thenResolve(instance(processService)); + when(processService.exec('poetry path', anything(), anything())).thenResolve({ stderr: '', stdout: '' }); + + const supported = await poetryInstaller.isSupported(Uri.file(__filename)); + + assert.equal(supported, true); + }); + test('Get Executable info', async () => { + const uri = Uri.file(__dirname); + const settings = mock(PythonSettings); + + when(configurationService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry path'); + + const info = await poetryInstaller.getExecutionInfo('something', uri); + + assert.deepEqual(info, { args: ['add', '--dev', 'something'], execPath: 'poetry path' }); + }); + test('Get executable info when installing black', async () => { + const uri = Uri.file(__dirname); + const settings = mock(PythonSettings); + + when(configurationService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry path'); + + const info = await poetryInstaller.getExecutionInfo('black', uri); + + assert.deepEqual(info, { args: ['add', '--dev', 'black', '--allow-prereleases'], execPath: 'poetry path' }); + }); +}); diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts new file mode 100644 index 000000000000..fc2d8c7e2b7b --- /dev/null +++ b/src/test/common/installer/productPath.unit.test.ts @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-invalid-this + +import { fail } from 'assert'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as TypeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; +import '../../../client/common/extensions'; +import { ProductInstaller } from '../../../client/common/installer/productInstaller'; +import { + BaseProductPathsService, + CTagsProductPathService, + DataScienceProductPathService, + FormatterProductPathService, + LinterProductPathService, + RefactoringLibraryProductPathService, + TestFrameworkProductPathService +} from '../../../client/common/installer/productPath'; +import { ProductService } from '../../../client/common/installer/productService'; +import { IProductService } from '../../../client/common/installer/types'; +import { + IConfigurationService, + IFormattingSettings, + IInstaller, + IPythonSettings, + ITestingSettings, + IWorkspaceSymbolSettings, + ModuleNamePurpose, + Product, + ProductType +} from '../../../client/common/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { IFormatterHelper } from '../../../client/formatters/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ILinterInfo, ILinterManager } from '../../../client/linters/types'; +import { ITestsHelper } from '../../../client/testing/common/types'; + +use(chaiAsPromised); + +suite('Product Path', () => { + [undefined, Uri.file('resource')].forEach((resource) => { + getNamesAndValues<Product>(Product).forEach((product) => { + class TestBaseProductPathsService extends BaseProductPathsService { + public getExecutableNameFromSettings(_: Product, _resource?: Uri): string { + return ''; + } + } + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let formattingSettings: TypeMoq.IMock<IFormattingSettings>; + let unitTestSettings: TypeMoq.IMock<ITestingSettings>; + let workspaceSymnbolSettings: TypeMoq.IMock<IWorkspaceSymbolSettings>; + let configService: TypeMoq.IMock<IConfigurationService>; + let productInstaller: ProductInstaller; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + formattingSettings = TypeMoq.Mock.ofType<IFormattingSettings>(); + unitTestSettings = TypeMoq.Mock.ofType<ITestingSettings>(); + workspaceSymnbolSettings = TypeMoq.Mock.ofType<IWorkspaceSymbolSettings>(); + + productInstaller = new ProductInstaller( + serviceContainer.object, + TypeMoq.Mock.ofType<OutputChannel>().object + ); + const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + pythonSettings.setup((p) => p.formatting).returns(() => formattingSettings.object); + pythonSettings.setup((p) => p.testing).returns(() => unitTestSettings.object); + pythonSettings.setup((p) => p.workspaceSymbols).returns(() => workspaceSymnbolSettings.object); + configService + .setup((s) => s.getSettings(TypeMoq.It.isValue(resource))) + .returns(() => pythonSettings.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) + .returns(() => configService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IInstaller), TypeMoq.It.isAny())) + .returns(() => productInstaller); + + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())) + .returns(() => new ProductService()); + }); + + if (product.value === Product.isort) { + return; + } + suite('Method isExecutableAModule()', () => { + if (product.value === Product.ipykernel) { + test('Returns true if product is ipykernel', () => { + const productPathService = new TestBaseProductPathsService(serviceContainer.object); + expect(productPathService.isExecutableAModule(product.value)).to.equal(true, 'Should be true'); + }); + } else if (product.value === Product.nbconvert) { + test('Returns true if product is nbconvert', () => { + const productPathService = new TestBaseProductPathsService(serviceContainer.object); + expect(productPathService.isExecutableAModule(product.value)).to.equal(true, 'Should be true'); + }); + } else if (product.value === Product.kernelspec) { + test('Returns true if product is kernelspec', () => { + const productPathService = new TestBaseProductPathsService(serviceContainer.object); + expect(productPathService.isExecutableAModule(product.value)).to.equal( + false, + 'Should be false' + ); + }); + } else { + test('Returns true if User has customized the executable name', () => { + productInstaller.translateProductToModuleName = () => 'moduleName'; + const productPathService = new TestBaseProductPathsService(serviceContainer.object); + productPathService.getExecutableNameFromSettings = () => 'executableName'; + expect(productPathService.isExecutableAModule(product.value)).to.equal(true, 'Should be true'); + }); + test('Returns false if User has customized the full path to executable', () => { + productInstaller.translateProductToModuleName = () => 'moduleName'; + const productPathService = new TestBaseProductPathsService(serviceContainer.object); + productPathService.getExecutableNameFromSettings = () => 'path/to/executable'; + expect(productPathService.isExecutableAModule(product.value)).to.equal( + false, + 'Should be false' + ); + }); + test('Returns false if translating product to module name fails with error', () => { + productInstaller.translateProductToModuleName = () => { + // tslint:disable-next-line: no-any + return new Error('Kaboom') as any; + }; + const productPathService = new TestBaseProductPathsService(serviceContainer.object); + productPathService.getExecutableNameFromSettings = () => 'executableName'; + expect(productPathService.isExecutableAModule(product.value)).to.equal( + false, + 'Should be false' + ); + }); + } + }); + const productType = new ProductService().getProductType(product.value); + switch (productType) { + case ProductType.Formatter: { + test(`Ensure path is returned for ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + const productPathService = new FormatterProductPathService(serviceContainer.object); + const formatterHelper = TypeMoq.Mock.ofType<IFormatterHelper>(); + const expectedPath = 'Some Path'; + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IFormatterHelper), TypeMoq.It.isAny())) + .returns(() => formatterHelper.object); + formattingSettings + .setup((f) => f.autopep8Path) + .returns(() => expectedPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + formatterHelper + .setup((f) => f.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) + .returns(() => { + return { + pathName: 'autopep8Path', + argsName: 'autopep8Args' + }; + }) + .verifiable(TypeMoq.Times.once()); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + expect(value).to.be.equal(expectedPath); + formattingSettings.verifyAll(); + formatterHelper.verifyAll(); + }); + break; + } + case ProductType.Linter: { + test(`Ensure path is returned for ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + const productPathService = new LinterProductPathService(serviceContainer.object); + const linterManager = TypeMoq.Mock.ofType<ILinterManager>(); + const linterInfo = TypeMoq.Mock.ofType<ILinterInfo>(); + const expectedPath = 'Some Path'; + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) + .returns(() => linterManager.object); + linterInfo + .setup((l) => l.pathName(TypeMoq.It.isValue(resource))) + .returns(() => expectedPath) + .verifiable(TypeMoq.Times.once()); + linterManager + .setup((l) => l.getLinterInfo(TypeMoq.It.isValue(product.value))) + .returns(() => linterInfo.object) + .verifiable(TypeMoq.Times.once()); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + expect(value).to.be.equal(expectedPath); + linterInfo.verifyAll(); + linterManager.verifyAll(); + }); + break; + } + case ProductType.RefactoringLibrary: { + test(`Ensure path is returned for ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + const productPathService = new RefactoringLibraryProductPathService(serviceContainer.object); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + const moduleName = productInstaller.translateProductToModuleName( + product.value, + ModuleNamePurpose.run + ); + expect(value).to.be.equal(moduleName); + }); + break; + } + case ProductType.WorkspaceSymbols: { + test(`Ensure path is returned for ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + const productPathService = new CTagsProductPathService(serviceContainer.object); + const expectedPath = 'Some Path'; + workspaceSymnbolSettings + .setup((w) => w.ctagsPath) + .returns(() => expectedPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + expect(value).to.be.equal(expectedPath); + workspaceSymnbolSettings.verifyAll(); + }); + break; + } + case ProductType.TestFramework: { + test(`Ensure path is returned for ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + const productPathService = new TestFrameworkProductPathService(serviceContainer.object); + const testHelper = TypeMoq.Mock.ofType<ITestsHelper>(); + const expectedPath = 'Some Path'; + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) + .returns(() => testHelper.object); + testHelper + .setup((t) => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) + .returns(() => { + return { + argsName: 'autoTestDiscoverOnSaveEnabled', + enabledName: 'autoTestDiscoverOnSaveEnabled', + pathName: 'nosetestPath' + }; + }) + .verifiable(TypeMoq.Times.once()); + unitTestSettings + .setup((u) => u.nosetestPath) + .returns(() => expectedPath) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + expect(value).to.be.equal(expectedPath); + testHelper.verifyAll(); + unitTestSettings.verifyAll(); + }); + test(`Ensure module name is returned for ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + const productPathService = new TestFrameworkProductPathService(serviceContainer.object); + const testHelper = TypeMoq.Mock.ofType<ITestsHelper>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) + .returns(() => testHelper.object); + testHelper + .setup((t) => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) + .returns(() => { + return { + argsName: 'autoTestDiscoverOnSaveEnabled', + enabledName: 'autoTestDiscoverOnSaveEnabled', + pathName: undefined + }; + }) + .verifiable(TypeMoq.Times.once()); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + const moduleName = productInstaller.translateProductToModuleName( + product.value, + ModuleNamePurpose.run + ); + expect(value).to.be.equal(moduleName); + testHelper.verifyAll(); + }); + break; + } + case ProductType.DataScience: { + test(`Ensure path is returned for ${product.name} (${ + resource ? 'With a resource' : 'without a resource' + })`, async () => { + const productPathService = new DataScienceProductPathService(serviceContainer.object); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + const moduleName = productInstaller.translateProductToModuleName( + product.value, + ModuleNamePurpose.run + ); + expect(value).to.be.equal(moduleName); + }); + break; + } + default: { + test(`No tests for Product Path of this Product Type ${product.name}`, () => { + fail('No tests for Product Path of this Product Type'); + }); + } + } + }); + }); +}); diff --git a/src/test/common/installer/serviceRegistry.unit.test.ts b/src/test/common/installer/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..e000fb209e25 --- /dev/null +++ b/src/test/common/installer/serviceRegistry.unit.test.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { IWebviewPanelProvider } from '../../../client/common/application/types'; +import { WebviewPanelProvider } from '../../../client/common/application/webviewPanels/webviewPanelProvider'; +import { InstallationChannelManager } from '../../../client/common/installer/channelManager'; +import { CondaInstaller } from '../../../client/common/installer/condaInstaller'; +import { InsidersBuildInstaller, StableBuildInstaller } from '../../../client/common/installer/extensionBuildInstaller'; +import { PipEnvInstaller } from '../../../client/common/installer/pipEnvInstaller'; +import { PipInstaller } from '../../../client/common/installer/pipInstaller'; +import { PoetryInstaller } from '../../../client/common/installer/poetryInstaller'; +import { + CTagsProductPathService, + DataScienceProductPathService, + FormatterProductPathService, + LinterProductPathService, + RefactoringLibraryProductPathService, + TestFrameworkProductPathService +} from '../../../client/common/installer/productPath'; +import { ProductService } from '../../../client/common/installer/productService'; +import { registerTypes } from '../../../client/common/installer/serviceRegistry'; +import { + IExtensionBuildInstaller, + IInstallationChannelManager, + IModuleInstaller, + INSIDERS_INSTALLER, + IProductPathService, + IProductService, + STABLE_INSTALLER +} from '../../../client/common/installer/types'; +import { ProductType } from '../../../client/common/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Common installer Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify(serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, CondaInstaller)).once(); + verify(serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipInstaller)).once(); + verify(serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipEnvInstaller)).once(); + verify(serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PoetryInstaller)).once(); + verify( + serviceManager.addSingleton<IInstallationChannelManager>( + IInstallationChannelManager, + InstallationChannelManager + ) + ).once(); + verify( + serviceManager.addSingleton<IExtensionBuildInstaller>( + IExtensionBuildInstaller, + StableBuildInstaller, + STABLE_INSTALLER + ) + ).once(); + verify( + serviceManager.addSingleton<IExtensionBuildInstaller>( + IExtensionBuildInstaller, + InsidersBuildInstaller, + INSIDERS_INSTALLER + ) + ).once(); + + verify(serviceManager.addSingleton<IProductService>(IProductService, ProductService)).once(); + verify( + serviceManager.addSingleton<IProductPathService>( + IProductPathService, + CTagsProductPathService, + ProductType.WorkspaceSymbols + ) + ).once(); + verify( + serviceManager.addSingleton<IProductPathService>( + IProductPathService, + FormatterProductPathService, + ProductType.Formatter + ) + ).once(); + verify( + serviceManager.addSingleton<IProductPathService>( + IProductPathService, + LinterProductPathService, + ProductType.Linter + ) + ).once(); + verify( + serviceManager.addSingleton<IProductPathService>( + IProductPathService, + TestFrameworkProductPathService, + ProductType.TestFramework + ) + ).once(); + verify( + serviceManager.addSingleton<IProductPathService>( + IProductPathService, + RefactoringLibraryProductPathService, + ProductType.RefactoringLibrary + ) + ).once(); + verify( + serviceManager.addSingleton<IProductPathService>( + IProductPathService, + DataScienceProductPathService, + ProductType.DataScience + ) + ).once(); + verify(serviceManager.addSingleton<IWebviewPanelProvider>(IWebviewPanelProvider, WebviewPanelProvider)).once(); + }); +}); diff --git a/src/test/common/interpreterPathService.unit.test.ts b/src/test/common/interpreterPathService.unit.test.ts new file mode 100644 index 000000000000..ad4aa91b1e4c --- /dev/null +++ b/src/test/common/interpreterPathService.unit.test.ts @@ -0,0 +1,653 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { + ConfigurationChangeEvent, + ConfigurationTarget, + Event, + EventEmitter, + Uri, + WorkspaceConfiguration +} from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { + defaultInterpreterPathSetting, + InterpreterPathService, + isGlobalSettingCopiedKey, + workspaceFolderKeysForWhichTheCopyIsDone_Key, + workspaceKeysForWhichTheCopyIsDone_Key +} from '../../client/common/interpreterPathService'; +import { FileSystemPaths } from '../../client/common/platform/fs-paths'; +import { InterpreterConfigurationScope, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; +import { createDeferred, sleep } from '../../client/common/utils/async'; + +suite('Interpreter Path Service', async () => { + let interpreterPathService: InterpreterPathService; + let persistentStateFactory: TypeMoq.IMock<IPersistentStateFactory>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + const resource = Uri.parse('a'); + const resourceOutsideOfWorkspace = Uri.parse('b'); + const interpreterPath = 'path/to/interpreter'; + const fs = FileSystemPaths.withDefaults(); + setup(() => { + const event = TypeMoq.Mock.ofType<Event<ConfigurationChangeEvent>>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspaceService + .setup((w) => w.getWorkspaceFolder(resource)) + .returns(() => ({ + uri: resource, + name: 'Workspacefolder', + index: 0 + })); + workspaceService.setup((w) => w.getWorkspaceFolder(resourceOutsideOfWorkspace)).returns(() => undefined); + persistentStateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + workspaceService.setup((w) => w.onDidChangeConfiguration).returns(() => event.object); + interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Ensure execution of method copyOldInterpreterStorageValuesToNew() goes as expected', async () => { + const _copyWorkspaceFolderValueToNewStorage = sinon.stub( + InterpreterPathService.prototype, + '_copyWorkspaceFolderValueToNewStorage' + ); + const _copyWorkspaceValueToNewStorage = sinon.stub( + InterpreterPathService.prototype, + '_copyWorkspaceValueToNewStorage' + ); + const _moveGlobalSettingValueToNewStorage = sinon.stub( + InterpreterPathService.prototype, + '_moveGlobalSettingValueToNewStorage' + ); + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.getConfiguration('python', resource)).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('pythonPath')) + .returns( + () => + ({ + globalValue: 'globalPythonPath', + workspaceFolderValue: 'workspaceFolderPythonPath', + workspaceValue: 'workspacePythonPath' + // tslint:disable-next-line: no-any + } as any) + ); + + interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + await interpreterPathService.copyOldInterpreterStorageValuesToNew(resource); + + assert(_copyWorkspaceFolderValueToNewStorage.calledWith(resource, 'workspaceFolderPythonPath')); + assert(_copyWorkspaceValueToNewStorage.calledWith(resource, 'workspacePythonPath')); + assert(_moveGlobalSettingValueToNewStorage.calledWith('globalPythonPath')); + }); + + test('If the one-off transfer to new storage has not happened yet for the workspace folder, do it and record the transfer', async () => { + const update = sinon.stub(InterpreterPathService.prototype, 'update'); + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string[]>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource, '')).returns(() => resource.fsPath); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string[]>(workspaceFolderKeysForWhichTheCopyIsDone_Key, [])) + .returns(() => persistentState.object); + persistentState.setup((p) => p.value).returns(() => ['...storedWorkspaceFolderKeys']); + persistentState + .setup((p) => p.updateValue([resource.fsPath, '...storedWorkspaceFolderKeys'])) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + await interpreterPathService._copyWorkspaceFolderValueToNewStorage(resource, 'workspaceFolderPythonPath'); + + assert(update.calledWith(resource, ConfigurationTarget.WorkspaceFolder, 'workspaceFolderPythonPath')); + persistentState.verifyAll(); + }); + + test('If the one-off transfer to new storage has already happened for the workspace folder, do not update and simply return', async () => { + const update = sinon.stub(InterpreterPathService.prototype, 'update'); + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string[]>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource, '')).returns(() => resource.fsPath); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string[]>(workspaceFolderKeysForWhichTheCopyIsDone_Key, [])) + .returns(() => persistentState.object); + persistentState.setup((p) => p.value).returns(() => [resource.fsPath, '...storedWorkspaceKeys']); + persistentState.setup((p) => p.updateValue(TypeMoq.It.isAny())).verifiable(TypeMoq.Times.never()); + + interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + await interpreterPathService._copyWorkspaceFolderValueToNewStorage(resource, 'workspaceFolderPythonPath'); + + assert(update.notCalled); + persistentState.verifyAll(); + }); + + test('If no folder is opened, do not do the one-off transfer to new storage for the workspace folder', async () => { + const update = sinon.stub(InterpreterPathService.prototype, 'update'); + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string[]>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource, '')).returns(() => ''); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string[]>(workspaceFolderKeysForWhichTheCopyIsDone_Key, [])) + .returns(() => persistentState.object); + persistentState.setup((p) => p.value).returns(() => ['...storedWorkspaceKeys']); + persistentState.setup((p) => p.updateValue(TypeMoq.It.isAny())).verifiable(TypeMoq.Times.never()); + + interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + await interpreterPathService._copyWorkspaceFolderValueToNewStorage(resource, 'workspaceFolderPythonPath'); + + assert(update.notCalled); + persistentState.verifyAll(); + }); + + test('If the one-off transfer to new storage has not happened yet for the workspace, do it and record the transfer', async () => { + const workspaceFileUri = Uri.parse('path/to/workspaceFile'); + const expectedWorkspaceKey = fs.normCase(workspaceFileUri.fsPath); + const update = sinon.stub(InterpreterPathService.prototype, 'update'); + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string[]>>(); + workspaceService.setup((w) => w.workspaceFile).returns(() => workspaceFileUri); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string[]>(workspaceKeysForWhichTheCopyIsDone_Key, [])) + .returns(() => persistentState.object); + persistentState.setup((p) => p.value).returns(() => ['...storedWorkspaceKeys']); + persistentState + .setup((p) => p.updateValue([expectedWorkspaceKey, '...storedWorkspaceKeys'])) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + await interpreterPathService._copyWorkspaceValueToNewStorage(resource, 'workspacePythonPath'); + + assert(update.calledWith(resource, ConfigurationTarget.Workspace, 'workspacePythonPath')); + persistentState.verifyAll(); + }); + + test('If the one-off transfer to new storage has already happened for the workspace, do not update and simply return', async () => { + const workspaceFileUri = Uri.parse('path/to/workspaceFile'); + const expectedWorkspaceKey = fs.normCase(workspaceFileUri.fsPath); + const update = sinon.stub(InterpreterPathService.prototype, 'update'); + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string[]>>(); + workspaceService.setup((w) => w.workspaceFile).returns(() => workspaceFileUri); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string[]>(workspaceKeysForWhichTheCopyIsDone_Key, [])) + .returns(() => persistentState.object); + persistentState.setup((p) => p.value).returns(() => [expectedWorkspaceKey, '...storedWorkspaceKeys']); + persistentState.setup((p) => p.updateValue(TypeMoq.It.isAny())).verifiable(TypeMoq.Times.never()); + + interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + await interpreterPathService._copyWorkspaceValueToNewStorage(resource, 'workspacePythonPath'); + + assert(update.notCalled); + persistentState.verifyAll(); + }); + + test('Do not update workspace settings and if a folder is directly opened', async () => { + const update = sinon.stub(InterpreterPathService.prototype, 'update'); + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string[]>>(); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string[]>(workspaceKeysForWhichTheCopyIsDone_Key, [])) + .returns(() => persistentState.object); + persistentState.setup((p) => p.value).verifiable(TypeMoq.Times.never()); + persistentState.setup((p) => p.updateValue(TypeMoq.It.isAny())).verifiable(TypeMoq.Times.never()); + + interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + await interpreterPathService._copyWorkspaceValueToNewStorage(resource, 'workspacePythonPath'); + + assert(update.notCalled); + persistentState.verifyAll(); + }); + + test('If the one-off transfer to new storage has not happened yet for the user setting, do it, record the transfer and remove the original user setting', async () => { + const update = sinon.stub(InterpreterPathService.prototype, 'update'); + const persistentState = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<boolean>(isGlobalSettingCopiedKey, false)) + .returns(() => persistentState.object); + persistentState.setup((p) => p.value).returns(() => false); + persistentState.setup((p) => p.updateValue(true)).verifiable(TypeMoq.Times.once()); + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.update('pythonPath', undefined, ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + await interpreterPathService._moveGlobalSettingValueToNewStorage('globalPythonPath'); + + assert(update.calledWith(undefined, ConfigurationTarget.Global, 'globalPythonPath')); + persistentState.verifyAll(); + workspaceConfig.verifyAll(); + }); + + test('If the one-off transfer to new storage has already happened for the user setting, do not update and simply return', async () => { + const update = sinon.stub(InterpreterPathService.prototype, 'update'); + const persistentState = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<boolean>(isGlobalSettingCopiedKey, false)) + .returns(() => persistentState.object); + persistentState.setup((p) => p.value).returns(() => true); + persistentState.setup((p) => p.updateValue(TypeMoq.It.isAny())).verifiable(TypeMoq.Times.never()); + + interpreterPathService = new InterpreterPathService(persistentStateFactory.object, workspaceService.object, []); + await interpreterPathService._moveGlobalSettingValueToNewStorage('globalPythonPath'); + + assert(update.notCalled); + persistentState.verifyAll(); + }); + + test('Global settings are not updated if stored value is same as new value', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: interpreterPath + // tslint:disable-next-line: no-any + } as any) + ); + workspaceConfig + .setup((w) => w.update('defaultInterpreterPath', interpreterPath, true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await interpreterPathService.update(resource, ConfigurationTarget.Global, interpreterPath); + + workspaceConfig.verifyAll(); + }); + + test('Global settings are correctly updated otherwise', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: 'storedValue' + // tslint:disable-next-line: no-any + } as any) + ); + workspaceConfig + .setup((w) => w.update('defaultInterpreterPath', interpreterPath, true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Global, interpreterPath); + + workspaceConfig.verifyAll(); + }); + + test('Workspace settings are not updated if stored value is same as new value', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState.setup((p) => p.value).returns(() => interpreterPath); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await interpreterPathService.update(resource, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Workspace settings are correctly updated if a folder is directly opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Ensure the correct event is fired if Workspace settings are updated', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object); + persistentState.setup((p) => p.updateValue(interpreterPath)).returns(() => Promise.resolve()); + + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType<EventEmitter<InterpreterConfigurationScope>>(); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup((emitter) => emitter.fire({ uri: resource, configTarget: ConfigurationTarget.Workspace })) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Workspace, interpreterPath); + + _didChangeInterpreterEmitter.verifyAll(); + }); + + test('Workspace settings are correctly updated in case of multiroot folders', async () => { + const workspaceFileUri = Uri.parse('path/to/workspaceFile'); + const expectedSettingKey = `WORKSPACE_INTERPRETER_PATH_${fs.normCase(workspaceFileUri.fsPath)}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.workspaceFile).returns(() => workspaceFileUri); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Workspace folder settings are correctly updated in case of multiroot folders', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Ensure the correct event is fired if Workspace folder settings are updated', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType<EventEmitter<InterpreterConfigurationScope>>(); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup((emitter) => emitter.fire({ uri: resource, configTarget: ConfigurationTarget.WorkspaceFolder })) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, interpreterPath); + + _didChangeInterpreterEmitter.verifyAll(); + }); + + test('Updating workspace settings simply returns if no workspace is opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.never()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await interpreterPathService.update(resourceOutsideOfWorkspace, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Updating workspace folder settings simply returns if no workspace is opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.never()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await interpreterPathService.update(resourceOutsideOfWorkspace, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Inspecting settings returns as expected if no workspace is opened', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: 'default/path/to/interpreter' + // tslint:disable-next-line: no-any + } as any) + ); + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.never()); + + const settings = interpreterPathService.inspect(resourceOutsideOfWorkspace); + assert.deepEqual(settings, { + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: undefined, + workspaceValue: undefined + }); + + persistentStateFactory.verifyAll(); + }); + + test('Inspecting settings returns as expected if a folder is directly opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + // No workspace file is present if a folder is directly opened + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: 'default/path/to/interpreter' + // tslint:disable-next-line: no-any + } as any) + ); + const workspaceFolderPersistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => workspaceFolderPersistentState.object); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => workspaceFolderPersistentState.object); + workspaceFolderPersistentState.setup((p) => p.value).returns(() => 'workspaceFolderValue'); + + const settings = interpreterPathService.inspect(resource); + + assert.deepEqual(settings, { + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: 'workspaceFolderValue', + workspaceValue: 'workspaceFolderValue' + }); + }); + + test('Inspecting settings returns as expected in case of multiroot folders', async () => { + const workspaceFileUri = Uri.parse('path/to/workspaceFile'); + const expectedWorkspaceSettingKey = `WORKSPACE_INTERPRETER_PATH_${fs.normCase(workspaceFileUri.fsPath)}`; + const expectedWorkspaceFolderSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + // A workspace file is present in case of multiroot workspace folders + workspaceService.setup((w) => w.workspaceFile).returns(() => workspaceFileUri); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: 'default/path/to/interpreter' + // tslint:disable-next-line: no-any + } as any) + ); + const workspaceFolderPersistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + const workspacePersistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup((p) => + p.createGlobalPersistentState<string | undefined>(expectedWorkspaceFolderSettingKey, undefined) + ) + .returns(() => workspaceFolderPersistentState.object); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedWorkspaceSettingKey, undefined)) + .returns(() => workspacePersistentState.object); + workspaceFolderPersistentState.setup((p) => p.value).returns(() => 'workspaceFolderValue'); + workspacePersistentState.setup((p) => p.value).returns(() => 'workspaceValue'); + + const settings = interpreterPathService.inspect(resource); + + assert.deepEqual(settings, { + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: 'workspaceFolderValue', + workspaceValue: 'workspaceValue' + }); + }); + + test(`Getting setting value returns workspace folder value if it's defined`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: 'workspaceFolderValue', + workspaceValue: 'workspaceValue' + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('workspaceFolderValue'); + }); + + test(`Getting setting value returns workspace value if workspace folder value is 'undefined'`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: undefined, + workspaceValue: 'workspaceValue' + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('workspaceValue'); + }); + + test(`Getting setting value returns workspace value if workspace folder value is 'undefined'`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: undefined, + workspaceValue: 'workspaceValue' + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('workspaceValue'); + }); + + test(`Getting setting value returns global value if workspace folder & workspace value are 'undefined'`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: undefined, + workspaceValue: undefined + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('default/path/to/interpreter'); + }); + + test(`Getting setting value returns 'python' if all workspace folder, workspace, and global value are 'undefined'`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: undefined, + workspaceFolderValue: undefined, + workspaceValue: undefined + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('python'); + }); + + test('If defaultInterpreterPathSetting is changed, an event is fired', async () => { + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType<EventEmitter<InterpreterConfigurationScope>>(); + const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); + event + .setup((e) => e.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup((emitter) => emitter.fire({ uri: undefined, configTarget: ConfigurationTarget.Global })) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + await interpreterPathService.onDidChangeConfiguration(event.object); + _didChangeInterpreterEmitter.verifyAll(); + event.verifyAll(); + }); + + test('If some other setting changed, no event is fired', async () => { + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType<EventEmitter<InterpreterConfigurationScope>>(); + const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); + event + .setup((e) => e.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup((emitter) => emitter.fire(TypeMoq.It.isAny())) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + await interpreterPathService.onDidChangeConfiguration(event.object); + _didChangeInterpreterEmitter.verifyAll(); + event.verifyAll(); + }); + + test('Ensure on interpreter change captures the fired event with the correct arguments', async () => { + const deferred = createDeferred<true>(); + const interpreterConfigurationScope = { uri: undefined, configTarget: ConfigurationTarget.Global }; + interpreterPathService.onDidChange((i) => { + expect(i).to.equal(interpreterConfigurationScope); + deferred.resolve(true); + }); + interpreterPathService._didChangeInterpreterEmitter.fire(interpreterConfigurationScope); + const eventCaptured = await Promise.race([deferred.promise, sleep(1000).then(() => false)]); + expect(eventCaptured).to.equal(true, 'Event should be captured'); + }); +}); diff --git a/src/test/common/misc.test.ts b/src/test/common/misc.test.ts new file mode 100644 index 000000000000..370668d40e7e --- /dev/null +++ b/src/test/common/misc.test.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { isTestExecution } from '../../client/common/constants'; + +// Defines a Mocha test suite to group tests of similar kind together +suite('Common - Misc', () => { + test("Ensure its identified that we're running unit tests", () => { + expect(isTestExecution()).to.be.equal(true, 'incorrect'); + }); +}); diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts new file mode 100644 index 000000000000..a5bccb537869 --- /dev/null +++ b/src/test/common/moduleInstaller.test.ts @@ -0,0 +1,628 @@ +// tslint:disable:max-func-body-length + +import { expect, should as chai_should, use as chai_use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { instance, mock } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { ClipboardService } from '../../client/common/application/clipboard'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; +import { CustomEditorService } from '../../client/common/application/customEditorService'; +import { DebugService } from '../../client/common/application/debugService'; +import { DebugSessionTelemetry } from '../../client/common/application/debugSessionTelemetry'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { Extensions } from '../../client/common/application/extensions'; +import { + IActiveResourceService, + IApplicationEnvironment, + IApplicationShell, + IClipboard, + ICommandManager, + ICustomEditorService, + IDebugService, + IDocumentManager, + ILiveShareApi, + IWorkspaceService +} from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { CryptoUtils } from '../../client/common/crypto'; +import { EditorUtils } from '../../client/common/editor'; +import { ExperimentsManager } from '../../client/common/experiments/manager'; +import { ExperimentService } from '../../client/common/experiments/service'; +import { FeatureDeprecationManager } from '../../client/common/featureDeprecationManager'; +import { + ExtensionInsidersDailyChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionInsidersWeeklyChannelRule +} from '../../client/common/insidersBuild/downloadChannelRules'; +import { ExtensionChannelService } from '../../client/common/insidersBuild/downloadChannelService'; +import { InsidersExtensionPrompt } from '../../client/common/insidersBuild/insidersExtensionPrompt'; +import { InsidersExtensionService } from '../../client/common/insidersBuild/insidersExtensionService'; +import { + ExtensionChannel, + IExtensionChannelRule, + IExtensionChannelService, + IInsiderExtensionPrompt +} from '../../client/common/insidersBuild/types'; +import { CondaInstaller } from '../../client/common/installer/condaInstaller'; +import { PipEnvInstaller } from '../../client/common/installer/pipEnvInstaller'; +import { PipInstaller } from '../../client/common/installer/pipInstaller'; +import { ProductInstaller } from '../../client/common/installer/productInstaller'; +import { IModuleInstaller } from '../../client/common/installer/types'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; +import { BrowserService } from '../../client/common/net/browser'; +import { FileDownloader } from '../../client/common/net/fileDownloader'; +import { HttpClient } from '../../client/common/net/httpClient'; +import { NugetService } from '../../client/common/nuget/nugetService'; +import { INugetService } from '../../client/common/nuget/types'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { PathUtils } from '../../client/common/platform/pathUtils'; +import { PlatformService } from '../../client/common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { CurrentProcess } from '../../client/common/process/currentProcess'; +import { ProcessLogger } from '../../client/common/process/logger'; +import { IProcessLogger, IProcessServiceFactory, IPythonExecutionFactory } from '../../client/common/process/types'; +import { TerminalActivator } from '../../client/common/terminal/activator'; +import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; +import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; +import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; +import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; +import { TerminalHelper } from '../../client/common/terminal/helper'; +import { SettingsShellDetector } from '../../client/common/terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalActivationHandler, + ITerminalActivator, + ITerminalHelper, + ITerminalService, + ITerminalServiceFactory, + TerminalActivationProviders +} from '../../client/common/terminal/types'; +import { + IAsyncDisposableRegistry, + IBrowserService, + IConfigurationService, + ICryptoUtils, + ICurrentProcess, + IEditorUtils, + IExperimentService, + IExperimentsManager, + IExtensions, + IFeatureDeprecationManager, + IFileDownloader, + IHttpClient, + IInstaller, + IInterpreterPathService, + IPathUtils, + IPersistentStateFactory, + IPythonSettings, + IRandom, + IsWindows +} from '../../client/common/types'; +import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; +import { Architecture } from '../../client/common/utils/platform'; +import { Random } from '../../client/common/utils/random'; +import { LiveShareApi } from '../../client/datascience/liveshare/liveshare'; +import { INotebookExecutionLogger } from '../../client/datascience/types'; +import { + ICondaService, + IInterpreterLocatorService, + IInterpreterService, + INTERPRETER_LOCATOR_SERVICE, + PIPENV_SERVICE +} from '../../client/interpreter/contracts'; +import { IServiceContainer } from '../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { ImportTracker } from '../../client/telemetry/importTracker'; +import { IImportTracker } from '../../client/telemetry/types'; +import { getExtensionSettings, PYTHON_PATH, rootWorkspaceUri } from '../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; +import { MockModuleInstaller } from '../mocks/moduleInstaller'; +import { MockProcessService } from '../mocks/proc'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; +import { closeActiveWindows, initializeTest } from './../initialize'; + +chai_use(chaiAsPromised); + +const isolated = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'pyvsc-run-isolated.py').replace(/\\/g, '/'); + +const info: PythonEnvironment = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + envType: EnvironmentType.Unknown, + version: new SemVer('0.0.0-alpha'), + sysPrefix: '', + sysVersion: '' +}; + +suite('Module Installer', () => { + [undefined, Uri.file(__filename)].forEach((resource) => { + let ioc: UnitTestIocContainer; + let mockTerminalService: TypeMoq.IMock<ITerminalService>; + let condaService: TypeMoq.IMock<ICondaService>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let mockTerminalFactory: TypeMoq.IMock<ITerminalServiceFactory>; + + const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); + suiteSetup(initializeTest); + setup(async () => { + chai_should(); + initializeDI(); + await initializeTest(); + await resetSettings(); + }); + suiteTeardown(async () => { + await closeActiveWindows(); + }); + teardown(async () => { + await ioc.dispose(); + await closeActiveWindows(); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + ioc.registerLinterTypes(); + ioc.registerFormatterTypes(); + ioc.registerInterpreterStorageTypes(); + + ioc.serviceManager.addSingleton<IPersistentStateFactory>(IPersistentStateFactory, PersistentStateFactory); + ioc.serviceManager.addSingleton<IProcessLogger>(IProcessLogger, ProcessLogger); + ioc.serviceManager.addSingleton<IInstaller>(IInstaller, ProductInstaller); + + mockTerminalService = TypeMoq.Mock.ofType<ITerminalService>(); + mockTerminalFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>(); + mockTerminalFactory + .setup((t) => t.getTerminalService(TypeMoq.It.isValue(resource))) + .returns(() => mockTerminalService.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + // If resource is provided, then ensure we do not invoke without the resource. + mockTerminalFactory + .setup((t) => t.getTerminalService(TypeMoq.It.isAny())) + .callback((passedInResource) => expect(passedInResource).to.be.equal(resource)) + .returns(() => mockTerminalService.object); + ioc.serviceManager.addSingletonInstance<ITerminalServiceFactory>( + ITerminalServiceFactory, + mockTerminalFactory.object + ); + + ioc.serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipInstaller); + ioc.serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, CondaInstaller); + ioc.serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipEnvInstaller); + + ioc.serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); + ioc.serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess); + ioc.serviceManager.addSingleton<IFileSystem>(IFileSystem, FileSystem); + ioc.serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService); + ioc.serviceManager.addSingleton<IConfigurationService>(IConfigurationService, ConfigurationService); + + ioc.serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, new WorkspaceService()); + + ioc.registerMockProcessTypes(); + ioc.serviceManager.addSingletonInstance<boolean>(IsWindows, false); + + ioc.registerMockInterpreterTypes(); + condaService = TypeMoq.Mock.ofType<ICondaService>(); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, condaService.object); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + ioc.serviceManager.rebindInstance<IInterpreterService>(IInterpreterService, interpreterService.object); + + ioc.serviceManager.addSingleton<IActiveResourceService>(IActiveResourceService, ActiveResourceService); + ioc.serviceManager.addSingleton<IInterpreterPathService>(IInterpreterPathService, InterpreterPathService); + ioc.serviceManager.addSingleton<IExtensions>(IExtensions, Extensions); + ioc.serviceManager.addSingleton<IRandom>(IRandom, Random); + ioc.serviceManager.addSingleton<IApplicationShell>(IApplicationShell, ApplicationShell); + ioc.serviceManager.addSingleton<IClipboard>(IClipboard, ClipboardService); + ioc.serviceManager.addSingleton<ICommandManager>(ICommandManager, CommandManager); + ioc.serviceManager.addSingleton<IDocumentManager>(IDocumentManager, DocumentManager); + ioc.serviceManager.addSingleton<IDebugService>(IDebugService, DebugService); + ioc.serviceManager.addSingleton<IApplicationEnvironment>(IApplicationEnvironment, ApplicationEnvironment); + ioc.serviceManager.addSingleton<IBrowserService>(IBrowserService, BrowserService); + ioc.serviceManager.addSingleton<IHttpClient>(IHttpClient, HttpClient); + ioc.serviceManager.addSingleton<IFileDownloader>(IFileDownloader, FileDownloader); + ioc.serviceManager.addSingleton<IEditorUtils>(IEditorUtils, EditorUtils); + ioc.serviceManager.addSingleton<INugetService>(INugetService, NugetService); + ioc.serviceManager.addSingleton<ITerminalActivator>(ITerminalActivator, TerminalActivator); + ioc.serviceManager.addSingleton<ITerminalActivationHandler>( + ITerminalActivationHandler, + PowershellTerminalActivationFailedHandler + ); + ioc.serviceManager.addSingleton<ILiveShareApi>(ILiveShareApi, LiveShareApi); + ioc.serviceManager.addSingleton<ICryptoUtils>(ICryptoUtils, CryptoUtils); + ioc.serviceManager.addSingleton<IExperimentsManager>(IExperimentsManager, ExperimentsManager); + ioc.serviceManager.addSingleton<IExperimentService>(IExperimentService, ExperimentService); + + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + Bash, + TerminalActivationProviders.bashCShellFish + ); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell + ); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + PyEnvActivationCommandProvider, + TerminalActivationProviders.pyenv + ); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + CondaActivationCommandProvider, + TerminalActivationProviders.conda + ); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + PipEnvActivationCommandProvider, + TerminalActivationProviders.pipenv + ); + ioc.serviceManager.addSingleton<IFeatureDeprecationManager>( + IFeatureDeprecationManager, + FeatureDeprecationManager + ); + + ioc.serviceManager.addSingleton<IAsyncDisposableRegistry>( + IAsyncDisposableRegistry, + AsyncDisposableRegistry + ); + ioc.serviceManager.addSingleton<IMultiStepInputFactory>(IMultiStepInputFactory, MultiStepInputFactory); + ioc.serviceManager.addSingleton<IImportTracker>(IImportTracker, ImportTracker); + ioc.serviceManager.addBinding(IImportTracker, IExtensionSingleActivationService); + ioc.serviceManager.addBinding(IImportTracker, INotebookExecutionLogger); + ioc.serviceManager.addSingleton<IShellDetector>(IShellDetector, TerminalNameShellDetector); + ioc.serviceManager.addSingleton<IShellDetector>(IShellDetector, SettingsShellDetector); + ioc.serviceManager.addSingleton<IShellDetector>(IShellDetector, UserEnvironmentShellDetector); + ioc.serviceManager.addSingleton<IShellDetector>(IShellDetector, VSCEnvironmentShellDetector); + ioc.serviceManager.addSingleton<IInsiderExtensionPrompt>(IInsiderExtensionPrompt, InsidersExtensionPrompt); + ioc.serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + InsidersExtensionService + ); + ioc.serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ReloadVSCodeCommandHandler + ); + ioc.serviceManager.addSingleton<IExtensionChannelService>( + IExtensionChannelService, + ExtensionChannelService + ); + ioc.serviceManager.addSingleton<IExtensionChannelRule>( + IExtensionChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionChannel.off + ); + ioc.serviceManager.addSingleton<IExtensionChannelRule>( + IExtensionChannelRule, + ExtensionInsidersDailyChannelRule, + ExtensionChannel.daily + ); + ioc.serviceManager.addSingleton<IExtensionChannelRule>( + IExtensionChannelRule, + ExtensionInsidersWeeklyChannelRule, + ExtensionChannel.weekly + ); + ioc.serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + DebugSessionTelemetry + ); + ioc.serviceManager.addSingleton<ICustomEditorService>(ICustomEditorService, CustomEditorService); + } + async function resetSettings(): Promise<void> { + const configService = ioc.serviceManager.get<IConfigurationService>(IConfigurationService); + await configService.updateSetting( + 'linting.pylintEnabled', + true, + rootWorkspaceUri, + ConfigurationTarget.Workspace + ); + } + async function getCurrentPythonPath(): Promise<string> { + const pythonPath = getExtensionSettings(workspaceUri).pythonPath; + if (path.basename(pythonPath) === pythonPath) { + const pythonProc = await ioc.serviceContainer + .get<IPythonExecutionFactory>(IPythonExecutionFactory) + .create({ resource: workspaceUri }); + return pythonProc.getExecutablePath().catch(() => pythonPath); + } else { + return pythonPath; + } + } + test('Ensure pip is supported and conda is not', async () => { + ioc.serviceManager.addSingletonInstance<IModuleInstaller>( + IModuleInstaller, + new MockModuleInstaller('mock', true) + ); + const mockInterpreterLocator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + mockInterpreterLocator + .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([])); + ioc.serviceManager.rebindInstance<IInterpreterLocatorService>( + IInterpreterLocatorService, + mockInterpreterLocator.object, + INTERPRETER_LOCATOR_SERVICE + ); + ioc.serviceManager.rebindInstance<IInterpreterLocatorService>( + IInterpreterLocatorService, + TypeMoq.Mock.ofType<IInterpreterLocatorService>().object, + PIPENV_SERVICE + ); + ioc.serviceManager.addSingletonInstance<ITerminalHelper>(ITerminalHelper, instance(mock(TerminalHelper))); + + const processService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + processService.onExec((file, args, _options, callback) => { + if (args.length > 1 && args[0] === '-c' && args[1] === 'import pip') { + callback({ stdout: '' }); + } + if (args.length > 0 && args[0] === '--version' && file === 'conda') { + callback({ stdout: '', stderr: 'not available' }); + } + }); + const moduleInstallers = ioc.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); + expect(moduleInstallers).length(4, 'Incorrect number of installers'); + + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); + + const condaInstaller = moduleInstallers.find((item) => item.displayName === 'Conda')!; + expect(condaInstaller).not.to.be.an('undefined', 'Conda installer not found'); + await expect(condaInstaller.isSupported()).to.eventually.equal(false, 'Conda is supported'); + + const mockInstaller = moduleInstallers.find((item) => item.displayName === 'mock')!; + expect(mockInstaller).not.to.be.an('undefined', 'mock installer not found'); + await expect(mockInstaller.isSupported()).to.eventually.equal(true, 'mock is not supported'); + }); + + test('Ensure pip is supported', async () => { + ioc.serviceManager.addSingletonInstance<IModuleInstaller>( + IModuleInstaller, + new MockModuleInstaller('mock', true) + ); + const pythonPath = await getCurrentPythonPath(); + const mockInterpreterLocator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + mockInterpreterLocator + .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) + .returns(() => + Promise.resolve([ + { + ...info, + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: pythonPath, + envType: EnvironmentType.Conda, + version: new SemVer('1.0.0') + } + ]) + ); + ioc.serviceManager.rebindInstance<IInterpreterLocatorService>( + IInterpreterLocatorService, + mockInterpreterLocator.object, + INTERPRETER_LOCATOR_SERVICE + ); + ioc.serviceManager.rebindInstance<IInterpreterLocatorService>( + IInterpreterLocatorService, + TypeMoq.Mock.ofType<IInterpreterLocatorService>().object, + PIPENV_SERVICE + ); + ioc.serviceManager.addSingletonInstance<ITerminalHelper>(ITerminalHelper, instance(mock(TerminalHelper))); + + const processService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + processService.onExec((file, args, _options, callback) => { + if (args.length > 1 && args[0] === '-c' && args[1] === 'import pip') { + callback({ stdout: '' }); + } + if (args.length > 0 && args[0] === '--version' && file === 'conda') { + callback({ stdout: '' }); + } + }); + const moduleInstallers = ioc.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); + expect(moduleInstallers).length(4, 'Incorrect number of installers'); + + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); + }); + test('Ensure conda is supported', async () => { + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + const pythonPath = 'pythonABC'; + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); + condaService.setup((c) => c.isCondaAvailable()).returns(() => Promise.resolve(true)); + condaService + .setup((c) => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)); + + const condaInstaller = new CondaInstaller(serviceContainer.object); + await expect(condaInstaller.isSupported()).to.eventually.equal(true, 'Conda is not supported'); + }); + test('Ensure conda is not supported even if conda is available', async () => { + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + const pythonPath = 'pythonABC'; + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); + condaService.setup((c) => c.isCondaAvailable()).returns(() => Promise.resolve(true)); + condaService + .setup((c) => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(false)); + + const condaInstaller = new CondaInstaller(serviceContainer.object); + await expect(condaInstaller.isSupported()).to.eventually.equal(false, 'Conda should not be supported'); + }); + + const resourceTestNameSuffix = resource ? ' with a resource' : ' without a resource'; + test(`Validate pip install arguments ${resourceTestNameSuffix}`, async () => { + const interpreterPath = await getCurrentPythonPath(); + const mockInterpreterLocator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + mockInterpreterLocator + .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([{ ...info, path: interpreterPath, envType: EnvironmentType.Unknown }])); + ioc.serviceManager.rebindInstance<IInterpreterLocatorService>( + IInterpreterLocatorService, + mockInterpreterLocator.object, + INTERPRETER_LOCATOR_SERVICE + ); + ioc.serviceManager.rebindInstance<IInterpreterLocatorService>( + IInterpreterLocatorService, + TypeMoq.Mock.ofType<IInterpreterLocatorService>().object, + PIPENV_SERVICE + ); + + const interpreter: PythonEnvironment = { + ...info, + envType: EnvironmentType.Unknown, + path: PYTHON_PATH + }; + interpreterService + .setup((x) => x.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(interpreter)); + + const moduleName = 'xyz'; + + const moduleInstallers = ioc.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; + + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + + let argsSent: string[] = []; + mockTerminalService + .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((_cmd: string, args: string[]) => { + argsSent = args; + return Promise.resolve(void 0); + }); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + // tslint:disable-next-line:no-any + .returns(() => Promise.resolve({ envType: EnvironmentType.Unknown } as any)); + + await pipInstaller.installModule(moduleName, resource); + + mockTerminalFactory.verifyAll(); + expect(argsSent.join(' ')).equal( + `${isolated} pip install -U ${moduleName} --user`, + 'Invalid command sent to terminal for installation.' + ); + }); + + test(`Validate Conda install arguments ${resourceTestNameSuffix}`, async () => { + const interpreterPath = await getCurrentPythonPath(); + const mockInterpreterLocator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + mockInterpreterLocator + .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([{ ...info, path: interpreterPath, envType: EnvironmentType.Conda }])); + ioc.serviceManager.rebindInstance<IInterpreterLocatorService>( + IInterpreterLocatorService, + mockInterpreterLocator.object, + INTERPRETER_LOCATOR_SERVICE + ); + ioc.serviceManager.rebindInstance<IInterpreterLocatorService>( + IInterpreterLocatorService, + TypeMoq.Mock.ofType<IInterpreterLocatorService>().object, + PIPENV_SERVICE + ); + + const moduleName = 'xyz'; + + const moduleInstallers = ioc.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; + + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + + let argsSent: string[] = []; + mockTerminalService + .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((_cmd: string, args: string[]) => { + argsSent = args; + return Promise.resolve(void 0); + }); + + await pipInstaller.installModule(moduleName, resource); + + mockTerminalFactory.verifyAll(); + expect(argsSent.join(' ')).equal( + `${isolated} pip install -U ${moduleName}`, + 'Invalid command sent to terminal for installation.' + ); + }); + + test(`Validate pipenv install arguments ${resourceTestNameSuffix}`, async () => { + const mockInterpreterLocator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + mockInterpreterLocator + .setup((p) => p.getInterpreters(TypeMoq.It.isAny())) + .returns(() => + Promise.resolve([{ ...info, path: 'interpreterPath', envType: EnvironmentType.VirtualEnv }]) + ); + ioc.serviceManager.rebindInstance<IInterpreterLocatorService>( + IInterpreterLocatorService, + mockInterpreterLocator.object, + PIPENV_SERVICE + ); + + const moduleName = 'xyz'; + const moduleInstallers = ioc.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'pipenv')!; + + expect(pipInstaller).not.to.be.an('undefined', 'pipenv installer not found'); + + let argsSent: string[] = []; + let command: string | undefined; + mockTerminalService + .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((cmd: string, args: string[]) => { + argsSent = args; + command = cmd; + return Promise.resolve(void 0); + }); + + await pipInstaller.installModule(moduleName, resource); + + mockTerminalFactory.verifyAll(); + expect(command!).equal('pipenv', 'Invalid command sent to terminal for installation.'); + expect(argsSent.join(' ')).equal( + `install ${moduleName} --dev`, + 'Invalid command arguments sent to terminal for installation.' + ); + }); + }); +}); diff --git a/src/test/common/net/fileDownloader.unit.test.ts b/src/test/common/net/fileDownloader.unit.test.ts new file mode 100644 index 000000000000..5d9a50733003 --- /dev/null +++ b/src/test/common/net/fileDownloader.unit.test.ts @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable: no-var-requires no-require-imports max-func-body-length no-any match-default-export-name +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as fsExtra from 'fs-extra'; +import * as nock from 'nock'; +import * as path from 'path'; +import rewiremock from 'rewiremock'; +import * as sinon from 'sinon'; +import { Readable, Writable } from 'stream'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Progress } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { FileDownloader } from '../../../client/common/net/fileDownloader'; +import { HttpClient } from '../../../client/common/net/httpClient'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IHttpClient } from '../../../client/common/types'; +import { Http } from '../../../client/common/utils/localize'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { noop } from '../../core'; +import { MockOutputChannel } from '../../mockClasses'; +const requestProgress = require('request-progress'); +const request = require('request'); + +type ProgressReporterData = { message?: string; increment?: number }; + +/** + * Writable stream that'll throw an error when written to. + * (used to mimick errors thrown when writing to a file). + * + * @class ErroringMemoryStream + * @extends {Writable} + */ +class ErroringMemoryStream extends Writable { + constructor(private readonly errorMessage: string) { + super(); + } + public _write(_chunk: any, _encoding: any, callback: any) { + super.emit('error', new Error(this.errorMessage)); + return callback(); + } +} +/** + * Readable stream that's slow to return data. + * (used to mimic slow file downloads). + * + * @class DelayedReadMemoryStream + * @extends {Readable} + */ +class DelayedReadMemoryStream extends Readable { + private readCounter = 0; + constructor( + private readonly totalKb: number, + private readonly delayMs: number, + private readonly kbPerIteration: number + ) { + super(); + } + // @ts-ignore https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-rc/#properties-overridding-accessors-and-vice-versa-is-an-error + public get readableLength() { + return 1024 * 10; + } + public _read() { + // Delay reading data, mimicking slow file downloads. + setTimeout(() => this.sendMesage(), this.delayMs); + } + public sendMesage() { + const i = (this.readCounter += 1); + if (i > this.totalKb / this.kbPerIteration) { + this.push(null); + } else { + this.push(Buffer.from('a'.repeat(this.kbPerIteration), 'ascii')); + } + } +} + +suite('File Downloader', () => { + let fileDownloader: FileDownloader; + let httpClient: IHttpClient; + let fs: IFileSystem; + let appShell: IApplicationShell; + suiteTeardown(() => { + rewiremock.disable(); + sinon.restore(); + }); + suite('File Downloader (real)', () => { + const uri = 'https://python.extension/package.json'; + const packageJsonFile = path.join(EXTENSION_ROOT_DIR, 'package.json'); + setup(() => { + rewiremock.disable(); + httpClient = mock(HttpClient); + appShell = mock(ApplicationShell); + when(httpClient.downloadFile(anything())).thenCall(request); + fs = new FileSystem(); + }); + teardown(() => { + rewiremock.disable(); + sinon.restore(); + }); + test('File gets downloaded', async () => { + // When downloading a uri, point it to package.json file. + nock('https://python.extension') + .get('/package.json') + .reply(200, () => fsExtra.createReadStream(packageJsonFile)); + const progressReportStub = sinon.stub(); + const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub }; + const tmpFilePath = await fs.createTemporaryFile('.json'); + when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); + + fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); + await fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', tmpFilePath.filePath); + + // Confirm the package.json file gets downloaded + const expectedFileContents = fsExtra.readFileSync(packageJsonFile).toString(); + assert.equal(fsExtra.readFileSync(tmpFilePath.filePath).toString(), expectedFileContents); + }); + test('Error is throw for http Status !== 200', async () => { + // When downloading a uri, throw status 500 error. + nock('https://python.extension').get('/package.json').reply(500); + const progressReportStub = sinon.stub(); + const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub }; + when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); + const tmpFilePath = await fs.createTemporaryFile('.json'); + + fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); + const promise = fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', tmpFilePath.filePath); + + await expect(promise).to.eventually.be.rejectedWith( + 'Failed with status 500, null, Uri https://python.extension/package.json' + ); + }); + test('Error is throw if unable to write to the file stream', async () => { + // When downloading a uri, point it to package.json file. + nock('https://python.extension') + .get('/package.json') + .reply(200, () => fsExtra.createReadStream(packageJsonFile)); + const progressReportStub = sinon.stub(); + const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub }; + when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); + + // Use bogus files that cannot be created (on windows, invalid drives, on mac & linux use invalid home directories). + const invalidFileName = new PlatformService().isWindows + ? 'abcd:/bogusFile/one.txt' + : '/bogus file path/.txt'; + fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); + const promise = fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', invalidFileName); + + // Things should fall over. + await expect(promise).to.eventually.be.rejected; + }); + test('Error is throw if file stream throws an error', async () => { + // When downloading a uri, point it to package.json file. + nock('https://python.extension') + .get('/package.json') + .reply(200, () => fsExtra.createReadStream(packageJsonFile)); + const progressReportStub = sinon.stub(); + const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub }; + when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); + // Create a file stream that will throw an error when written to (use ErroringMemoryStream). + const tmpFilePath = 'bogus file'; + const fileSystem = mock(FileSystem); + const fileStream = new ErroringMemoryStream('kaboom from fs'); + when(fileSystem.createWriteStream(tmpFilePath)).thenReturn(fileStream as any); + + fileDownloader = new FileDownloader(instance(httpClient), instance(fileSystem), instance(appShell)); + const promise = fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', tmpFilePath); + + // Confirm error from FS is bubbled up. + await expect(promise).to.eventually.be.rejectedWith('kaboom from fs'); + }); + test('Report progress as file gets downloaded', async () => { + const totalKb = 50; + // When downloading a uri, point it to stream that's slow. + // We'll return data from this stream slowly, mimicking a slow download. + // When the download is slow, we can test progress. + nock('https://python.extension') + .get('/package.json') + .reply(200, () => [ + 200, + new DelayedReadMemoryStream(1024 * totalKb, 5, 1024 * 10), + { 'content-length': 1024 * totalKb } + ]); + const progressReportStub = sinon.stub(); + const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub }; + when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); + const tmpFilePath = await fs.createTemporaryFile('.json'); + // Mock request-progress to throttle 1ms, so we can get progress messages. + // I.e. report progress every 1ms. (however since download is delayed to 10ms, + // we'll get progress reported every 10ms. We use 1ms, to ensure its guaranteed + // to be reported. Else changing it to 10ms could result in it being reported in 12ms + rewiremock.enable(); + rewiremock('request-progress').with((reqUri: string) => requestProgress(reqUri, { throttle: 1 })); + + fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); + await fileDownloader.downloadFileWithStatusBarProgress(uri, 'Downloading-something', tmpFilePath.filePath); + + // Since we are throttling the progress notifications for ever 1ms, + // and we're delaying downloading by every 10ms, we'll have progress reported for every 10ms. + // So we'll have progress reported for every 10kb of data downloaded, for a total of 5 times. + expect(progressReportStub.callCount).to.equal(5); + expect(progressReportStub.args[0][0].message).to.equal(getProgressMessage(10, 20)); + expect(progressReportStub.args[1][0].message).to.equal(getProgressMessage(20, 40)); + expect(progressReportStub.args[2][0].message).to.equal(getProgressMessage(30, 60)); + expect(progressReportStub.args[3][0].message).to.equal(getProgressMessage(40, 80)); + expect(progressReportStub.args[4][0].message).to.equal(getProgressMessage(50, 100)); + + function getProgressMessage(downloadedKb: number, percentage: number) { + return Http.downloadingFileProgress().format( + 'Downloading-something', + downloadedKb.toFixed(), + totalKb.toFixed(), + percentage.toString() + ); + } + }); + }); + suite('File Downloader (mocks)', () => { + let downloadWithProgressStub: sinon.SinonStub<any>; + setup(() => { + httpClient = mock(HttpClient); + fs = mock(FileSystem); + appShell = mock(ApplicationShell); + downloadWithProgressStub = sinon.stub(FileDownloader.prototype, 'displayDownloadProgress'); + downloadWithProgressStub.callsFake(() => Promise.resolve()); + }); + teardown(() => { + sinon.restore(); + }); + test('Create temporary file and return path to that file', async () => { + const tmpFile = { filePath: 'my temp file', dispose: noop }; + when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); + fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); + + const file = await fileDownloader.downloadFile('file', { progressMessagePrefix: '', extension: '.pdf' }); + + verify(fs.createTemporaryFile('.pdf')).once(); + assert.equal(file, 'my temp file'); + }); + test('Display progress message in output channel', async () => { + const outputChannel = mock(MockOutputChannel); + const tmpFile = { filePath: 'my temp file', dispose: noop }; + when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); + fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); + + await fileDownloader.downloadFile('file to download', { + progressMessagePrefix: '', + extension: '.pdf', + outputChannel: outputChannel + }); + + verify(outputChannel.appendLine(Http.downloadingFile().format('file to download'))); + }); + test('Display progress when downloading', async () => { + const tmpFile = { filePath: 'my temp file', dispose: noop }; + when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); + const statusBarProgressStub = sinon.stub(FileDownloader.prototype, 'downloadFileWithStatusBarProgress'); + statusBarProgressStub.callsFake(() => Promise.resolve()); + fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); + + await fileDownloader.downloadFile('file', { progressMessagePrefix: '', extension: '.pdf' }); + + assert.ok(statusBarProgressStub.calledOnce); + }); + test('Dispose temp file and bubble error thrown by status progress', async () => { + const disposeStub = sinon.stub(); + const tmpFile = { filePath: 'my temp file', dispose: disposeStub }; + when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); + const statusBarProgressStub = sinon.stub(FileDownloader.prototype, 'downloadFileWithStatusBarProgress'); + statusBarProgressStub.callsFake(() => Promise.reject(new Error('kaboom'))); + fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); + + const promise = fileDownloader.downloadFile('file', { progressMessagePrefix: '', extension: '.pdf' }); + + await expect(promise).to.eventually.be.rejectedWith('kaboom'); + assert.ok(statusBarProgressStub.calledOnce); + assert.ok(disposeStub.calledOnce); + }); + }); +}); diff --git a/src/test/common/net/httpClient.unit.test.ts b/src/test/common/net/httpClient.unit.test.ts new file mode 100644 index 000000000000..33f30c4a1176 --- /dev/null +++ b/src/test/common/net/httpClient.unit.test.ts @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import * as assert from 'assert'; +import { expect } from 'chai'; +// tslint:disable-next-line: match-default-export-name +import rewiremock from 'rewiremock'; +import * as TypeMoq from 'typemoq'; +import { WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { HttpClient } from '../../../client/common/net/httpClient'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable-next-line: max-func-body-length +suite('Http Client', () => { + const proxy = 'https://myproxy.net:4242'; + let config: TypeMoq.IMock<WorkspaceConfiguration>; + let workSpaceService: TypeMoq.IMock<IWorkspaceService>; + let container: TypeMoq.IMock<IServiceContainer>; + let httpClient: HttpClient; + setup(() => { + container = TypeMoq.Mock.ofType<IServiceContainer>(); + workSpaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + config = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + config + .setup((c) => c.get(TypeMoq.It.isValue('proxy'), TypeMoq.It.isValue(''))) + .returns(() => proxy) + .verifiable(TypeMoq.Times.once()); + workSpaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('http'))) + .returns(() => config.object) + .verifiable(TypeMoq.Times.once()); + container.setup((a) => a.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workSpaceService.object); + + httpClient = new HttpClient(container.object); + }); + test('Get proxy info', async () => { + expect(httpClient.requestOptions).to.deep.equal({ proxy: proxy }); + config.verifyAll(); + workSpaceService.verifyAll(); + }); + suite('Test getJSON()', async () => { + teardown(() => { + rewiremock.disable(); + }); + [ + { + name: 'Throw error if request returns with download error', + returnedArgs: ['downloadError', { statusCode: 201 }, undefined], + expectedErrorMessage: 'downloadError' + }, + { + name: 'Throw error if request does not return with status code 200', + returnedArgs: [undefined, { statusCode: 201, statusMessage: 'wrongStatus' }, undefined], + expectedErrorMessage: 'Failed with status 201, wrongStatus, Uri downloadUri' + }, + { + name: 'If strict is set to true, and parsing fails, throw error', + returnedArgs: [undefined, { statusCode: 200 }, '[{ "strictJSON" : true,, }]'], + strict: true + } + ].forEach(async (testParams) => { + test(testParams.name, async () => { + const requestMock = (_uri: any, _requestOptions: any, callBackFn: Function) => + callBackFn(...testParams.returnedArgs); + rewiremock.enable(); + rewiremock('request').with(requestMock); + let rejected = true; + try { + await httpClient.getJSON('downloadUri', testParams.strict); + rejected = false; + } catch (ex) { + if (testParams.expectedErrorMessage) { + // Compare error messages + if (ex.message) { + ex = ex.message; + } + expect(ex).to.equal( + testParams.expectedErrorMessage, + 'Promise rejected with the wrong error message' + ); + } + } + assert(rejected === true, 'Promise should be rejected'); + }); + }); + + [ + { + name: + "If strict is set to false, and jsonc parsing returns error codes, then log errors and don't throw, return json", + returnedArgs: [undefined, { statusCode: 200 }, '[{ "strictJSON" : false,, }]'], + strict: false, + expectedJSON: [{ strictJSON: false }] + }, + { + name: 'Return expected json if strict is set to true and parsing is successful', + returnedArgs: [undefined, { statusCode: 200 }, '[{ "strictJSON" : true }]'], + strict: true, + expectedJSON: [{ strictJSON: true }] + }, + { + name: 'Return expected json if strict is set to false and parsing is successful', + returnedArgs: [undefined, { statusCode: 200 }, '[{ //Comment \n "strictJSON" : false }]'], + strict: false, + expectedJSON: [{ strictJSON: false }] + } + ].forEach(async (testParams) => { + test(testParams.name, async () => { + const requestMock = (_uri: any, _requestOptions: any, callBackFn: Function) => + callBackFn(...testParams.returnedArgs); + rewiremock.enable(); + rewiremock('request').with(requestMock); + let json; + try { + json = await httpClient.getJSON('downloadUri', testParams.strict); + } catch (ex) { + assert(false, 'Promise should not be rejected'); + } + assert.deepEqual(json, testParams.expectedJSON, 'Unexpected JSON returned'); + }); + }); + }); +}); diff --git a/src/test/common/nuget/azureBobStoreRepository.functional.test.ts b/src/test/common/nuget/azureBobStoreRepository.functional.test.ts new file mode 100644 index 000000000000..2afb1b94b680 --- /dev/null +++ b/src/test/common/nuget/azureBobStoreRepository.functional.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import * as typeMoq from 'typemoq'; +import { WorkspaceConfiguration } from 'vscode'; +import { DotNetLanguageServerMinVersionKey } from '../../../client/activation/languageServer/languageServerFolderService'; +import { DotNetLanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService'; +import { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types'; +import { AzureBlobStoreNugetRepository } from '../../../client/common/nuget/azureBlobStoreNugetRepository'; +import { INugetService } from '../../../client/common/nuget/types'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IHttpClient } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +const azureBlobStorageAccount = 'https://pvsc.blob.core.windows.net'; +const azureCDNBlobStorageAccount = 'https://pvsc.azureedge.net'; + +suite('Nuget Azure Storage Repository', () => { + let serviceContainer: typeMoq.IMock<IServiceContainer>; + let httpClient: typeMoq.IMock<IHttpClient>; + let workspace: typeMoq.IMock<IWorkspaceService>; + let cfg: typeMoq.IMock<WorkspaceConfiguration>; + let repo: AzureBlobStoreNugetRepository; + setup(() => { + serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + httpClient = typeMoq.Mock.ofType<IHttpClient>(); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IHttpClient))).returns(() => httpClient.object); + cfg = typeMoq.Mock.ofType<WorkspaceConfiguration>(); + cfg.setup((c) => c.get('proxyStrictSSL', true)).returns(() => true); + workspace = typeMoq.Mock.ofType<IWorkspaceService>(); + workspace.setup((w) => w.getConfiguration('http', undefined)).returns(() => cfg.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IWorkspaceService))).returns(() => workspace.object); + + const nugetService = typeMoq.Mock.ofType<INugetService>(); + nugetService + .setup((n) => n.getVersionFromPackageFileName(typeMoq.It.isAny())) + .returns(() => new SemVer('1.1.1')); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(INugetService))).returns(() => nugetService.object); + const defaultStorageChannel = 'python-language-server-stable'; + + repo = new AzureBlobStoreNugetRepository( + serviceContainer.object, + azureBlobStorageAccount, + defaultStorageChannel, + azureCDNBlobStorageAccount + ); + }); + + test('Get all packages', async function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(15000); + const platformService = new PlatformService(); + const packageJson = { [DotNetLanguageServerMinVersionKey]: '0.0.1' }; + const appEnv = typeMoq.Mock.ofType<IApplicationEnvironment>(); + appEnv.setup((e) => e.packageJson).returns(() => packageJson); + const lsPackageService = new DotNetLanguageServerPackageService( + serviceContainer.object, + appEnv.object, + platformService + ); + const packageName = lsPackageService.getNugetPackageName(); + const packages = await repo.getPackages(packageName, undefined); + + expect(packages).to.be.length.greaterThan(0); + }); +}); diff --git a/src/test/common/nuget/azureBobStoreRepository.unit.test.ts b/src/test/common/nuget/azureBobStoreRepository.unit.test.ts new file mode 100644 index 000000000000..f29e12e42dcc --- /dev/null +++ b/src/test/common/nuget/azureBobStoreRepository.unit.test.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-http-string + +import { BlobService, ErrorOrResult } from 'azure-storage'; +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import * as typeMoq from 'typemoq'; +import { WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { AzureBlobStoreNugetRepository } from '../../../client/common/nuget/azureBlobStoreNugetRepository'; +import { INugetService } from '../../../client/common/nuget/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Nuget Azure Storage Repository', () => { + const packageName = 'Python-Language-Server-???'; + + let serviceContainer: typeMoq.IMock<IServiceContainer>; + let workspace: typeMoq.IMock<IWorkspaceService>; + let nugetService: typeMoq.IMock<INugetService>; + let cfg: typeMoq.IMock<WorkspaceConfiguration>; + + setup(() => { + serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(undefined, typeMoq.MockBehavior.Strict); + workspace = typeMoq.Mock.ofType<IWorkspaceService>(undefined, typeMoq.MockBehavior.Strict); + nugetService = typeMoq.Mock.ofType<INugetService>(undefined, typeMoq.MockBehavior.Strict); + cfg = typeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, typeMoq.MockBehavior.Strict); + + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(INugetService))).returns(() => nugetService.object); + }); + + class FakeBlobStore { + // tslint:disable-next-line:no-any + public calls: [string, string, any][] = []; + public results?: BlobService.BlobResult[]; + public error?: Error; + public contructor() { + this.calls = []; + } + public listBlobsSegmentedWithPrefix( + c: string, + p: string, + // tslint:disable-next-line:no-any + t: any, + cb: ErrorOrResult<BlobService.ListBlobsResult> + ) { + this.calls.push([c, p, t]); + const result: BlobService.ListBlobsResult = { entries: this.results! }; + // tslint:disable-next-line:no-any + cb(this.error as Error, result, undefined as any); + } + } + + const tests: [string, boolean, string][] = [ + ['https://az', true, 'https://az'], + ['https://az', false, 'http://az'], + ['http://az', true, 'http://az'], + ['http://az', false, 'http://az'] + ]; + for (const [uri, setting, expected] of tests) { + test(`Get all packages ("${uri}" / ${setting})`, async () => { + if (uri.startsWith('https://')) { + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspace.object); + workspace.setup((w) => w.getConfiguration('http', undefined)).returns(() => cfg.object); + cfg.setup((c) => c.get('proxyStrictSSL', true)).returns(() => setting); + } + const blobstore = new FakeBlobStore(); + // tslint:disable:no-object-literal-type-assertion + blobstore.results = [ + { name: 'Azarath' } as BlobService.BlobResult, + { name: 'Metrion' } as BlobService.BlobResult, + { name: 'Zinthos' } as BlobService.BlobResult + ]; + // tslint:enable:no-object-literal-type-assertion + const version = new SemVer('1.1.1'); + blobstore.results.forEach((r) => { + nugetService.setup((n) => n.getVersionFromPackageFileName(r.name)).returns(() => version); + }); + let actualURI = ''; + const repo = new AzureBlobStoreNugetRepository( + serviceContainer.object, + uri, + 'spam', + 'eggs', + async (uriArg) => { + actualURI = uriArg; + return blobstore; + } + ); + + const packages = await repo.getPackages(packageName, undefined); + + expect(packages).to.deep.equal([ + { package: 'Azarath', uri: 'eggs/spam/Azarath', version: version }, + { package: 'Metrion', uri: 'eggs/spam/Metrion', version: version }, + { package: 'Zinthos', uri: 'eggs/spam/Zinthos', version: version } + ]); + expect(actualURI).to.equal(expected); + expect(blobstore.calls).to.deep.equal([['spam', packageName, undefined]], 'failed'); + serviceContainer.verifyAll(); + workspace.verifyAll(); + cfg.verifyAll(); + }); + } +}); diff --git a/src/test/common/nuget/nugetRepository.unit.test.ts b/src/test/common/nuget/nugetRepository.unit.test.ts new file mode 100644 index 000000000000..1370e824b2da --- /dev/null +++ b/src/test/common/nuget/nugetRepository.unit.test.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import * as typeMoq from 'typemoq'; +import { NugetRepository } from '../../../client/common/nuget/nugetRepository'; +import { IHttpClient } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Nuget on Nuget Repo', () => { + let serviceContainer: typeMoq.IMock<IServiceContainer>; + let httpClient: typeMoq.IMock<IHttpClient>; + let nugetRepo: NugetRepository; + setup(() => { + serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + httpClient = typeMoq.Mock.ofType<IHttpClient>(); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IHttpClient))).returns(() => httpClient.object); + + nugetRepo = new NugetRepository(serviceContainer.object); + }); + + test('Get all package versions', async () => { + const packageBaseAddress = 'a'; + const packageName = 'b'; + const resp = { versions: ['1.1.1', '1.2.1'] }; + const expectedUri = `${packageBaseAddress}/${packageName.toLowerCase().trim()}/index.json`; + + httpClient + .setup((h) => h.getJSON(typeMoq.It.isValue(expectedUri))) + .returns(() => Promise.resolve(resp)) + .verifiable(typeMoq.Times.once()); + + const versions = await nugetRepo.getVersions(packageBaseAddress, packageName); + + httpClient.verifyAll(); + expect(versions).to.be.lengthOf(2); + expect(versions.map((item) => item.raw)).to.deep.equal(resp.versions); + }); + + test('Get package uri', async () => { + const packageBaseAddress = 'a'; + const packageName = 'b'; + const version = '1.1.3'; + const expectedUri = `${packageBaseAddress}/${packageName}/${version}/${packageName}.${version}.nupkg`; + + const packageUri = nugetRepo.getNugetPackageUri(packageBaseAddress, packageName, new SemVer(version)); + + httpClient.verifyAll(); + expect(packageUri).to.equal(expectedUri); + }); + + test('Get packages', async () => { + const versions = ['1.1.1', '1.2.1', '2.2.2', '2.5.4', '2.9.5-release', '2.7.4-beta', '2.0.2', '3.5.4']; + nugetRepo.getVersions = () => Promise.resolve(versions.map((v) => new SemVer(v))); + nugetRepo.getNugetPackageUri = () => 'uri'; + + const packages = await nugetRepo.getPackages('packageName'); + + expect(packages).to.be.lengthOf(versions.length); + expect(packages.map((item) => item.version.raw)).to.be.deep.equal(versions); + expect(packages.map((item) => item.uri)).to.be.deep.equal(versions.map(() => 'uri')); + expect(packages.map((item) => item.package)).to.be.deep.equal(versions.map(() => 'packageName')); + }); +}); diff --git a/src/test/common/nuget/nugetService.unit.test.ts b/src/test/common/nuget/nugetService.unit.test.ts new file mode 100644 index 000000000000..3437db24178d --- /dev/null +++ b/src/test/common/nuget/nugetService.unit.test.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { parse } from 'semver'; +import { NugetService } from '../../../client/common/nuget/nugetService'; + +suite('Nuget Service', () => { + test('Identifying release versions', async () => { + const service = new NugetService(); + + expect(service.isReleaseVersion(parse('0.1.1')!)).to.be.equal(true, 'incorrect'); + expect(service.isReleaseVersion(parse('0.1.1-1')!)).to.be.equal(false, 'incorrect'); + expect(service.isReleaseVersion(parse('0.1.1-release')!)).to.be.equal(false, 'incorrect'); + expect(service.isReleaseVersion(parse('0.1.1-preview')!)).to.be.equal(false, 'incorrect'); + }); + + test('Get package version', async () => { + const service = new NugetService(); + expect(service.getVersionFromPackageFileName('Something-xyz.0.0.1.nupkg').compare(parse('0.0.1')!)).to.equal( + 0, + 'incorrect' + ); + expect( + service.getVersionFromPackageFileName('Something-xyz.0.0.1.1234.nupkg').compare(parse('0.0.1-1234')!) + ).to.equal(0, 'incorrect'); + expect( + service.getVersionFromPackageFileName('Something-xyz.0.0.1-preview.nupkg').compare(parse('0.0.1-preview')!) + ).to.equal(0, 'incorrect'); + }); +}); diff --git a/src/test/common/platform/errors.unit.test.ts b/src/test/common/platform/errors.unit.test.ts new file mode 100644 index 000000000000..d565bc817218 --- /dev/null +++ b/src/test/common/platform/errors.unit.test.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import * as vscode from 'vscode'; +import { + isFileExistsError, + isFileIsDirError, + isFileNotFoundError, + isNoPermissionsError, + isNotDirError +} from '../../../client/common/platform/errors'; +import { SystemError } from './utils'; + +suite('FileSystem - errors', () => { + const filename = 'spam'; + + suite('isFileNotFoundError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileNotFound(filename), true], + [vscode.FileSystemError.FileExists(filename), false], + [new SystemError('ENOENT', 'stat', '<msg>'), true], + [new SystemError('EEXIST', '???', '<msg>'), false], + [new Error(filename), undefined] + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isFileNotFoundError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isFileExistsError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileExists(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('EEXIST', '???', '<msg>'), true], + [new SystemError('ENOENT', 'stat', '<msg>'), false], + [new Error(filename), undefined] + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isFileExistsError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isFileIsDirError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileIsADirectory(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('EISDIR', '???', '<msg>'), true], + [new SystemError('ENOENT', 'stat', '<msg>'), false], + [new Error(filename), undefined] + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isFileIsDirError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isNotDirError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileNotADirectory(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('ENOTDIR', '???', '<msg>'), true], + [new SystemError('ENOENT', 'stat', '<msg>'), false], + [new Error(filename), undefined] + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isNotDirError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isNoPermissionsError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.NoPermissions(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('EACCES', '???', '<msg>'), true], + [new SystemError('ENOENT', 'stat', '<msg>'), false], + [new Error(filename), undefined] + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isNoPermissionsError(err); + + expect(matches).to.equal(expected); + }); + }); + }); +}); diff --git a/src/test/common/platform/filesystem.functional.test.ts b/src/test/common/platform/filesystem.functional.test.ts new file mode 100644 index 000000000000..55e96b7aaa5d --- /dev/null +++ b/src/test/common/platform/filesystem.functional.test.ts @@ -0,0 +1,793 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length chai-vague-errors + +import { expect, use } from 'chai'; +import * as fs from 'fs-extra'; +import { convertStat, FileSystem, FileSystemUtils, RawFileSystem } from '../../../client/common/platform/fileSystem'; +import { FileSystemPaths, FileSystemPathUtils } from '../../../client/common/platform/fs-paths'; +import { FileType } from '../../../client/common/platform/types'; +import { createDeferred, sleep } from '../../../client/common/utils/async'; +import { noop } from '../../../client/common/utils/misc'; +import { + assertDoesNotExist, + assertFileText, + DOES_NOT_EXIST, + fixPath, + FSFixture, + SUPPORTS_SOCKETS, + SUPPORTS_SYMLINKS, + WINDOWS +} from './utils'; + +// tslint:disable:no-require-imports no-var-requires +const assertArrays = require('chai-arrays'); +use(require('chai-as-promised')); +use(assertArrays); + +suite('FileSystem - raw', () => { + let fileSystem: RawFileSystem; + let fix: FSFixture; + setup(async () => { + fileSystem = RawFileSystem.withDefaults(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + await fix.ensureDeleted(DOES_NOT_EXIST); + }); + + suite('lstat', () => { + test('for symlinks, gives the link info', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + const rawStat = await fs.lstat(symlink); + const expected = convertStat(rawStat, FileType.SymbolicLink); + + const stat = await fileSystem.lstat(symlink); + + expect(stat).to.deep.equal(expected); + }); + + test('for normal files, gives the file info', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + // Ideally we would compare to the result of + // fileSystem.stat(). However, we do not have access + // to the VS Code API here. + const rawStat = await fs.lstat(filename); + const expected = convertStat(rawStat, FileType.File); + + const stat = await fileSystem.lstat(filename); + + expect(stat).to.deep.equal(expected); + }); + + test('fails if the file does not exist', async () => { + const promise = fileSystem.lstat(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('chmod (non-Windows)', () => { + suiteSetup(function () { + // On Windows, chmod won't have any effect on the file itself. + if (WINDOWS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + + async function checkMode(filename: string, expected: number) { + const stat = await fs.stat(filename); + expect(stat.mode & 0o777).to.equal(expected); + } + + test('the file mode gets updated (string)', async () => { + const filename = await fix.createFile('spam.py', '...'); + await fs.chmod(filename, 0o644); + + await fileSystem.chmod(filename, '755'); + + await checkMode(filename, 0o755); + }); + + test('the file mode gets updated (number)', async () => { + const filename = await fix.createFile('spam.py', '...'); + await fs.chmod(filename, 0o644); + + await fileSystem.chmod(filename, 0o755); + + await checkMode(filename, 0o755); + }); + + test('the file mode gets updated for a directory', async () => { + const dirname = await fix.createDirectory('spam'); + await fs.chmod(dirname, 0o755); + + await fileSystem.chmod(dirname, 0o700); + + await checkMode(dirname, 0o700); + }); + + test('nothing happens if the file mode already matches', async () => { + const filename = await fix.createFile('spam.py', '...'); + await fs.chmod(filename, 0o644); + + await fileSystem.chmod(filename, 0o644); + + await checkMode(filename, 0o644); + }); + + test('fails if the file does not exist', async () => { + const promise = fileSystem.chmod(DOES_NOT_EXIST, 0o755); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('appendText', () => { + test('existing file', async () => { + const orig = 'spamspamspam\n\n'; + const dataToAppend = `Some Data\n${new Date().toString()}\nAnd another line`; + const filename = await fix.createFile('spam.txt', orig); + const expected = `${orig}${dataToAppend}`; + + await fileSystem.appendText(filename, dataToAppend); + + const actual = await fs.readFile(filename, 'utf8'); + expect(actual).to.be.equal(expected); + }); + + test('existing empty file', async () => { + const filename = await fix.createFile('spam.txt'); + const dataToAppend = `Some Data\n${new Date().toString()}\nAnd another line`; + const expected = dataToAppend; + + await fileSystem.appendText(filename, dataToAppend); + + const actual = await fs.readFile(filename, 'utf8'); + expect(actual).to.be.equal(expected); + }); + + test('creates the file if it does not already exist', async () => { + await fileSystem.appendText(DOES_NOT_EXIST, 'spam'); + + const actual = await fs.readFile(DOES_NOT_EXIST, 'utf8'); + expect(actual).to.be.equal('spam'); + }); + + test('fails if not a file', async () => { + const dirname = await fix.createDirectory('spam'); + + const promise = fileSystem.appendText(dirname, 'spam'); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + // non-async + + suite('statSync', () => { + test('for normal files, gives the file info', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + // Ideally we would compare to the result of + // fileSystem.stat(). However, we do not have access + // to the VS Code API here. + const rawStat = await fs.stat(filename); + const expected = convertStat(rawStat, FileType.File); + + const stat = fileSystem.statSync(filename); + + expect(stat).to.deep.equal(expected); + }); + + test('for symlinks, gives the linked info', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + const rawStat = await fs.stat(filename); + const expected = convertStat(rawStat, FileType.SymbolicLink | FileType.File); + + const stat = fileSystem.statSync(symlink); + + expect(stat).to.deep.equal(expected); + }); + + test('fails if the file does not exist', async () => { + expect(() => { + fileSystem.statSync(DOES_NOT_EXIST); + }).to.throw(); + }); + }); + + suite('readTextSync', () => { + test('returns contents of a file', async () => { + const expected = '<some text>'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const text = fileSystem.readTextSync(filename); + + expect(text).to.be.equal(expected); + }); + + test('always UTF-8', async () => { + const expected = '... 😁 ...'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const text = fileSystem.readTextSync(filename); + + expect(text).to.equal(expected); + }); + + test('throws an exception if file does not exist', () => { + expect(() => { + fileSystem.readTextSync(DOES_NOT_EXIST); + }).to.throw(Error); + }); + }); + + suite('createReadStream', () => { + setup(function () { + // tslint:disable-next-line: no-suspicious-comment + // TODO: This appears to be producing + // false negative test results, so we're skipping + // it for now. + // See https://github.com/microsoft/vscode-python/issues/10031. + // tslint:disable-next-line:no-invalid-this + this.skip(); + }); + + test('returns the correct ReadStream', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const expected = fs.createReadStream(filename); + expected.destroy(); + + const stream = fileSystem.createReadStream(filename); + stream.destroy(); + + expect(stream.path).to.deep.equal(expected.path); + }); + + // Missing tests: + // * creation fails if the file does not exist + // * .read() works as expected + // * .pipe() works as expected + }); + + suite('createWriteStream', () => { + setup(function () { + // tslint:disable-next-line: no-suspicious-comment + // TODO This appears to be producing + // false negative test results, so we're skipping + // it for now. + // See https://github.com/microsoft/vscode-python/issues/10031. + // tslint:disable-next-line:no-invalid-this + this.skip(); + }); + + async function writeToStream(filename: string, write: (str: fs.WriteStream) => void) { + const closeDeferred = createDeferred(); + const stream = fileSystem.createWriteStream(filename); + stream.on('close', () => closeDeferred.resolve()); + write(stream); + stream.end(); + stream.close(); + stream.destroy(); + await closeDeferred.promise; + return stream; + } + + test('returns the correct WriteStream', async () => { + const filename = await fix.resolve('x/y/z/spam.py'); + const expected = fs.createWriteStream(filename); + expected.destroy(); + + const stream = await writeToStream(filename, noop); + + expect(stream.path).to.deep.equal(expected.path); + }); + + test('creates the file if missing', async () => { + const filename = await fix.resolve('x/y/z/spam.py'); + await assertDoesNotExist(filename); + const data = 'line1\nline2\n'; + + await writeToStream(filename, (s) => s.write(data)); + + await assertFileText(filename, data); + }); + + test('always UTF-8', async () => { + const filename = await fix.resolve('x/y/z/spam.py'); + const data = '... 😁 ...'; + + await writeToStream(filename, (s) => s.write(data)); + + await assertFileText(filename, data); + }); + + test('overwrites existing file', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const data = 'line1\nline2\n'; + + await writeToStream(filename, (s) => s.write(data)); + + await assertFileText(filename, data); + }); + }); +}); + +suite('FileSystem - utils', () => { + let utils: FileSystemUtils; + let fix: FSFixture; + setup(async () => { + utils = FileSystemUtils.withDefaults(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + await fix.ensureDeleted(DOES_NOT_EXIST); + }); + + suite('getFileHash', () => { + // Since getFileHash() relies on timestamps, we have to take + // into account filesystem timestamp resolution. For instance + // on FAT and HFS it is 1 second. + // See: https://nodejs.org/api/fs.html#fs_stat_time_values + + test('Getting hash for a file should return non-empty string', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const hash = await utils.getFileHash(filename); + + expect(hash).to.not.equal(''); + }); + + test('the returned hash is stable', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const hash1 = await utils.getFileHash(filename); + const hash2 = await utils.getFileHash(filename); + await sleep(2_000); // just in case + const hash3 = await utils.getFileHash(filename); + + expect(hash1).to.equal(hash2); + expect(hash1).to.equal(hash3); + expect(hash2).to.equal(hash3); + }); + + test('the returned hash changes with modification', async () => { + const filename = await fix.createFile('x/y/z/spam.py', 'original text'); + + const hash1 = await utils.getFileHash(filename); + await sleep(2_000); // for filesystems with 1s resolution + await fs.writeFile(filename, 'new text'); + const hash2 = await utils.getFileHash(filename); + + expect(hash1).to.not.equal(hash2); + }); + + test('the returned hash is unique', async () => { + const file1 = await fix.createFile('spam.py'); + await sleep(2_000); // for filesystems with 1s resolution + const file2 = await fix.createFile('x/y/z/spam.py'); + await sleep(2_000); // for filesystems with 1s resolution + const file3 = await fix.createFile('eggs.py'); + + const hash1 = await utils.getFileHash(file1); + const hash2 = await utils.getFileHash(file2); + const hash3 = await utils.getFileHash(file3); + + expect(hash1).to.not.equal(hash2); + expect(hash1).to.not.equal(hash3); + expect(hash2).to.not.equal(hash3); + }); + + test('Getting hash for non existent file should throw error', async () => { + const promise = utils.getFileHash(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('search', () => { + test('found matches', async () => { + const pattern = await fix.resolve(`x/y/z/spam.*`); + const expected: string[] = [ + await fix.createFile('x/y/z/spam.py'), + await fix.createFile('x/y/z/spam.pyc'), + await fix.createFile('x/y/z/spam.so'), + await fix.createDirectory('x/y/z/spam.data') + ]; + // non-matches + await fix.createFile('x/spam.py'); + await fix.createFile('x/y/z/eggs.py'); + await fix.createFile('x/y/z/spam-all.py'); + await fix.createFile('x/y/z/spam'); + await fix.createFile('x/spam.py'); + + let files = await utils.search(pattern); + + // For whatever reason, on Windows "search()" is + // returning filenames with forward slasshes... + files = files.map(fixPath); + expect(files.sort()).to.deep.equal(expected.sort()); + }); + + test('no matches', async () => { + const pattern = await fix.resolve(`x/y/z/spam.*`); + + const files = await utils.search(pattern); + + expect(files).to.deep.equal([]); + }); + }); + + suite('fileExistsSync', () => { + test('want file, got file', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = utils.fileExistsSync(filename); + + expect(exists).to.equal(true); + }); + + test('want file, not file', async () => { + const filename = await fix.createDirectory('x/y/z/spam.py'); + + const exists = utils.fileExistsSync(filename); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + + const exists = utils.fileExistsSync(symlink); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = utils.fileExistsSync(sockFile); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + }); +}); + +suite('FileSystem', () => { + let fileSystem: FileSystem; + let fix: FSFixture; + setup(async () => { + fileSystem = new FileSystem(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + await fix.ensureDeleted(DOES_NOT_EXIST); + }); + + suite('path-related', () => { + const paths = FileSystemPaths.withDefaults(); + const pathUtils = FileSystemPathUtils.withDefaults(paths); + + suite('directorySeparatorChar', () => { + // tested fully in the FileSystemPaths tests. + + test('matches wrapped object', () => { + const expected = paths.sep; + + const sep = fileSystem.directorySeparatorChar; + + expect(sep).to.equal(expected); + }); + }); + + suite('arePathsSame', () => { + // tested fully in the FileSystemPathUtils tests. + + test('matches wrapped object', () => { + const file1 = fixPath('a/b/c/spam.py'); + const file2 = fixPath('a/b/c/Spam.py'); + const expected = pathUtils.arePathsSame(file1, file2); + + const areSame = fileSystem.arePathsSame(file1, file2); + + expect(areSame).to.equal(expected); + }); + }); + }); + + suite('raw', () => { + suite('appendFile', () => { + test('wraps the low-level impl', async () => { + const filename = await fix.createFile('spam.txt'); + const dataToAppend = `Some Data\n${new Date().toString()}\nAnd another line`; + const expected = dataToAppend; + + await fileSystem.appendFile(filename, dataToAppend); + + const actual = await fs.readFile(filename, 'utf8'); + expect(actual).to.be.equal(expected); + }); + }); + + suite('chmod (non-Windows)', () => { + suiteSetup(function () { + // On Windows, chmod won't have any effect on the file itself. + if (WINDOWS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + + test('wraps the low-level impl', async () => { + const filename = await fix.createFile('spam.py', '...'); + await fs.chmod(filename, 0o644); + + await fileSystem.chmod(filename, '755'); + + const stat = await fs.stat(filename); + expect(stat.mode & 0o777).to.equal(0o755); + }); + }); + + //============================= + // sync methods + + suite('readFileSync', () => { + test('wraps the low-level impl', async () => { + const expected = '<some text>'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const text = fileSystem.readFileSync(filename); + + expect(text).to.be.equal(expected); + }); + }); + + suite('createReadStream', () => { + test('wraps the low-level impl', async function () { + // This test seems to randomly fail. + // tslint:disable-next-line: no-invalid-this + this.skip(); + + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const expected = fs.createReadStream(filename); + expected.destroy(); + + const stream = fileSystem.createReadStream(filename); + stream.destroy(); + + expect(stream.path).to.deep.equal(expected.path); + }); + }); + + suite('createWriteStream', () => { + test('wraps the low-level impl', async function () { + // This test seems to randomly fail. + // tslint:disable-next-line: no-invalid-this + this.skip(); + + const filename = await fix.resolve('x/y/z/spam.py'); + const expected = fs.createWriteStream(filename); + expected.destroy(); + + const stream = fileSystem.createWriteStream(filename); + stream.destroy(); + + expect(stream.path).to.deep.equal(expected.path); + }); + }); + }); + + suite('utils', () => { + suite('getFileHash', () => { + // Since getFileHash() relies on timestamps, we have to take + // into account filesystem timestamp resolution. For instance + // on FAT and HFS it is 1 second. + // See: https://nodejs.org/api/fs.html#fs_stat_time_values + + test('Getting hash for a file should return non-empty string', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const hash = await fileSystem.getFileHash(filename); + + expect(hash).to.not.equal(''); + }); + + test('the returned hash is stable', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const hash1 = await fileSystem.getFileHash(filename); + const hash2 = await fileSystem.getFileHash(filename); + await sleep(2_000); // just in case + const hash3 = await fileSystem.getFileHash(filename); + + expect(hash1).to.equal(hash2); + expect(hash1).to.equal(hash3); + expect(hash2).to.equal(hash3); + }); + + test('the returned hash changes with modification', async () => { + const filename = await fix.createFile('x/y/z/spam.py', 'original text'); + + const hash1 = await fileSystem.getFileHash(filename); + await sleep(2_000); // for filesystems with 1s resolution + await fs.writeFile(filename, 'new text'); + const hash2 = await fileSystem.getFileHash(filename); + + expect(hash1).to.not.equal(hash2); + }); + + test('the returned hash is unique', async () => { + const file1 = await fix.createFile('spam.py'); + await sleep(2_000); // for filesystems with 1s resolution + const file2 = await fix.createFile('x/y/z/spam.py'); + await sleep(2_000); // for filesystems with 1s resolution + const file3 = await fix.createFile('eggs.py'); + + const hash1 = await fileSystem.getFileHash(file1); + const hash2 = await fileSystem.getFileHash(file2); + const hash3 = await fileSystem.getFileHash(file3); + + expect(hash1).to.not.equal(hash2); + expect(hash1).to.not.equal(hash3); + expect(hash2).to.not.equal(hash3); + }); + + test('Getting hash for non existent file should throw error', async () => { + const promise = fileSystem.getFileHash(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('search', () => { + test('found matches', async () => { + const pattern = await fix.resolve(`x/y/z/spam.*`); + const expected: string[] = [ + await fix.createFile('x/y/z/spam.py'), + await fix.createFile('x/y/z/spam.pyc'), + await fix.createFile('x/y/z/spam.so'), + await fix.createDirectory('x/y/z/spam.data') + ]; + // non-matches + await fix.createFile('x/spam.py'); + await fix.createFile('x/y/z/eggs.py'); + await fix.createFile('x/y/z/spam-all.py'); + await fix.createFile('x/y/z/spam'); + await fix.createFile('x/spam.py'); + await fix.createFile('x/y/z/.net.py'); + let files = await fileSystem.search(pattern); + + // For whatever reason, on Windows "search()" is + // returning filenames with forward slasshes... + files = files.map(fixPath); + expect(files.sort()).to.deep.equal(expected.sort()); + }); + test('found dot matches', async () => { + const dir = await fix.resolve(`x/y/z`); + const expected: string[] = [ + await fix.createFile('x/y/z/spam.py'), + await fix.createFile('x/y/z/.net.py') + ]; + // non-matches + await fix.createFile('x/spam.py'); + await fix.createFile('x/y/z/spam'); + await fix.createFile('x/spam.py'); + let files = await fileSystem.search(`${dir}/**/*.py`, undefined, true); + + // For whatever reason, on Windows "search()" is + // returning filenames with forward slasshes... + files = files.map(fixPath); + expect(files.sort()).to.deep.equal(expected.sort()); + }); + + test('no matches', async () => { + const pattern = await fix.resolve(`x/y/z/spam.*`); + + const files = await fileSystem.search(pattern); + + expect(files).to.deep.equal([]); + }); + }); + + //============================= + // sync methods + + suite('fileExistsSync', () => { + test('want file, got file', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = fileSystem.fileExistsSync(filename); + + expect(exists).to.equal(true); + }); + + test('want file, not file', async () => { + const filename = await fix.createDirectory('x/y/z/spam.py'); + + const exists = fileSystem.fileExistsSync(filename); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + + const exists = fileSystem.fileExistsSync(symlink); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = fileSystem.fileExistsSync(sockFile); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + }); + }); +}); diff --git a/src/test/common/platform/filesystem.test.ts b/src/test/common/platform/filesystem.test.ts new file mode 100644 index 000000000000..1f11200e1e77 --- /dev/null +++ b/src/test/common/platform/filesystem.test.ts @@ -0,0 +1,1311 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length chai-vague-errors +// tslint:disable:no-suspicious-comment + +import { expect } from 'chai'; +import * as fsextra from 'fs-extra'; +import * as path from 'path'; +import { convertStat, FileSystem, FileSystemUtils, RawFileSystem } from '../../../client/common/platform/fileSystem'; +import { FileType, IFileSystem, IFileSystemUtils, IRawFileSystem } from '../../../client/common/platform/types'; +import { + assertDoesNotExist, + assertExists, + assertFileText, + DOES_NOT_EXIST, + FSFixture, + SUPPORTS_SOCKETS, + SUPPORTS_SYMLINKS, + WINDOWS +} from './utils'; + +// Note: all functional tests that do not trigger the VS Code "fs" API +// are found in filesystem.functional.test.ts. + +suite('FileSystem - raw', () => { + let filesystem: IRawFileSystem; + let fix: FSFixture; + setup(async () => { + filesystem = RawFileSystem.withDefaults(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + }); + + suite('stat', () => { + setup(function () { + // https://github.com/microsoft/vscode-python/issues/10294 + // tslint:disable-next-line: no-invalid-this + this.skip(); + }); + test('gets the info for an existing file', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const old = await fsextra.stat(filename); + const expected = convertStat(old, FileType.File); + + const stat = await filesystem.stat(filename); + + expect(stat).to.deep.equal(expected); + }); + + test('gets the info for an existing directory', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + const old = await fsextra.stat(dirname); + const expected = convertStat(old, FileType.Directory); + + const stat = await filesystem.stat(dirname); + + expect(stat).to.deep.equal(expected); + }); + + test('for symlinks, gets the info for the linked file', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + const old = await fsextra.stat(filename); + const expected = convertStat(old, FileType.SymbolicLink | FileType.File); + + const stat = await filesystem.stat(symlink); + + expect(stat).to.deep.equal(expected); + }); + + test('gets the info for a socket', async function () { + if (!SUPPORTS_SOCKETS) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + const sock = await fix.createSocket('x/spam.sock'); + const old = await fsextra.stat(sock); + const expected = convertStat(old, FileType.Unknown); + + const stat = await filesystem.stat(sock); + + expect(stat).to.deep.equal(expected); + }); + + test('fails if the file does not exist', async () => { + const promise = filesystem.stat(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('move', () => { + test('rename file', async () => { + const source = await fix.createFile('spam.py', '<text>'); + const target = await fix.resolve('eggs-txt'); + await assertDoesNotExist(target); + + await filesystem.move(source, target); + + await assertExists(target); + const text = await fsextra.readFile(target, 'utf8'); + expect(text).to.equal('<text>'); + await assertDoesNotExist(source); + }); + + test('rename directory', async () => { + const source = await fix.createDirectory('spam'); + await fix.createFile('spam/data.json', '<text>'); + const target = await fix.resolve('eggs'); + const filename = await fix.resolve('eggs/data.json', false); + await assertDoesNotExist(target); + + await filesystem.move(source, target); + + await assertExists(filename); + const text = await fsextra.readFile(filename, 'utf8'); + expect(text).to.equal('<text>'); + await assertDoesNotExist(source); + }); + + test('rename symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('spam.py'); + const symlink = await fix.createSymlink('spam.lnk', filename); + const target = await fix.resolve('eggs'); + await assertDoesNotExist(target); + + await filesystem.move(symlink, target); + + await assertExists(target); + const linked = await fsextra.readlink(target); + expect(linked).to.equal(filename); + await assertDoesNotExist(symlink); + }); + + test('move file', async () => { + const source = await fix.createFile('spam.py', '<text>'); + await fix.createDirectory('eggs'); + const target = await fix.resolve('eggs/spam.py'); + await assertDoesNotExist(target); + + await filesystem.move(source, target); + + await assertExists(target); + const text = await fsextra.readFile(target, 'utf8'); + expect(text).to.equal('<text>'); + await assertDoesNotExist(source); + }); + + test('move directory', async () => { + const source = await fix.createDirectory('spam/spam/spam/eggs/spam'); + await fix.createFile('spam/spam/spam/eggs/spam/data.json', '<text>'); + await fix.createDirectory('spam/spam/spam/hash'); + const target = await fix.resolve('spam/spam/spam/hash/spam'); + const filename = await fix.resolve('spam/spam/spam/hash/spam/data.json', false); + await assertDoesNotExist(target); + + await filesystem.move(source, target); + + await assertExists(filename); + const text = await fsextra.readFile(filename, 'utf8'); + expect(text).to.equal('<text>'); + await assertDoesNotExist(source); + }); + + test('move symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('spam.py'); + const symlink = await fix.createSymlink('w/spam.lnk', filename); + const target = await fix.resolve('x/spam.lnk'); + await assertDoesNotExist(target); + + await filesystem.move(symlink, target); + + await assertExists(target); + const linked = await fsextra.readlink(target); + expect(linked).to.equal(filename); + await assertDoesNotExist(symlink); + }); + + test('file target already exists', async () => { + const source = await fix.createFile('spam.py', '<text>'); + const target = await fix.createFile('eggs-txt', '<other>'); + + await filesystem.move(source, target); + + await assertDoesNotExist(source); + await assertExists(target); + const text2 = await fsextra.readFile(target, 'utf8'); + expect(text2).to.equal('<text>'); + }); + + test('directory target already exists', async () => { + const source = await fix.createDirectory('spam'); + const file3 = await fix.createFile('spam/data.json', '<text>'); + const target = await fix.createDirectory('eggs'); + const file1 = await fix.createFile('eggs/spam.py', '<code>'); + const file2 = await fix.createFile('eggs/data.json', '<other>'); + + const promise = filesystem.move(source, target); + + await expect(promise).to.eventually.be.rejected; + // Make sure nothing changed. + const text1 = await fsextra.readFile(file1, 'utf8'); + expect(text1).to.equal('<code>'); + const text2 = await fsextra.readFile(file2, 'utf8'); + expect(text2).to.equal('<other>'); + const text3 = await fsextra.readFile(file3, 'utf8'); + expect(text3).to.equal('<text>'); + }); + + test('fails if the file does not exist', async () => { + const source = await fix.resolve(DOES_NOT_EXIST); + const target = await fix.resolve('spam.py'); + + const promise = filesystem.move(source, target); + + await expect(promise).to.eventually.be.rejected; + // Make sure nothing changed. + await assertDoesNotExist(target); + }); + + test('fails if the target directory does not exist', async () => { + const source = await fix.createFile('x/spam.py', '<text>'); + const target = await fix.resolve('w/spam.py', false); + await assertDoesNotExist(path.dirname(target)); + + const promise = filesystem.move(source, target); + + await expect(promise).to.eventually.be.rejected; + // Make sure nothing changed. + await assertExists(source); + await assertDoesNotExist(target); + }); + }); + + suite('readData', () => { + test('returns contents of a file', async () => { + const text = '<some text>'; + const expected = Buffer.from(text, 'utf8'); + const filename = await fix.createFile('x/y/z/spam.py', text); + + const content = await filesystem.readData(filename); + + expect(content).to.deep.equal(expected); + }); + + test('throws an exception if file does not exist', async () => { + const promise = filesystem.readData(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('readText', () => { + test('returns contents of a file', async () => { + const expected = '<some text>'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const content = await filesystem.readText(filename); + + expect(content).to.be.equal(expected); + }); + + test('always UTF-8', async () => { + const expected = '... 😁 ...'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const text = await filesystem.readText(filename); + + expect(text).to.equal(expected); + }); + + test('returns garbage if encoding is UCS-2', async () => { + const filename = await fix.resolve('spam.py'); + // There are probably cases where this would fail too. + // However, the extension never has to deal with non-UTF8 + // cases, so it doesn't matter too much. + const original = '... 😁 ...'; + await fsextra.writeFile(filename, original, { encoding: 'ucs2' }); + + const text = await filesystem.readText(filename); + + expect(text).to.equal('.\u0000.\u0000.\u0000 \u0000=�\u0001� \u0000.\u0000.\u0000.\u0000'); + }); + + test('throws an exception if file does not exist', async () => { + const promise = filesystem.readText(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('writeText', () => { + test('creates the file if missing', async () => { + const filename = await fix.resolve('x/y/z/spam.py'); + await assertDoesNotExist(filename); + const data = 'line1\nline2\n'; + + await filesystem.writeText(filename, data); + + await assertFileText(filename, data); + }); + + test('always UTF-8', async () => { + const filename = await fix.resolve('x/y/z/spam.py'); + const data = '... 😁 ...'; + + await filesystem.writeText(filename, data); + + await assertFileText(filename, data); + }); + + test('overwrites existing file', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const data = 'line1\nline2\n'; + + await filesystem.writeText(filename, data); + + await assertFileText(filename, data); + }); + }); + + suite('copyFile', () => { + test('the source file gets copied (same directory)', async () => { + const data = '<content>'; + const src = await fix.createFile('x/y/z/spam.py', data); + const dest = await fix.resolve('x/y/z/spam.py.bak'); + await assertDoesNotExist(dest); + + await filesystem.copyFile(src, dest); + + await assertFileText(dest, data); + await assertFileText(src, data); // Make sure src wasn't changed. + }); + + test('the source file gets copied (different directory)', async () => { + const data = '<content>'; + const src = await fix.createFile('x/y/z/spam.py', data); + const dest = await fix.resolve('x/y/eggs.py'); + await assertDoesNotExist(dest); + + await filesystem.copyFile(src, dest); + + await assertFileText(dest, data); + await assertFileText(src, data); // Make sure src wasn't changed. + }); + + test('fails if the source does not exist', async () => { + const dest = await fix.resolve('x/spam.py'); + + const promise = filesystem.copyFile(DOES_NOT_EXIST, dest); + + await expect(promise).to.eventually.be.rejected; + }); + + test('fails if the target parent directory does not exist', async () => { + const src = await fix.createFile('x/spam.py', '...'); + const dest = await fix.resolve('y/eggs.py', false); + await assertDoesNotExist(path.dirname(dest)); + + const promise = filesystem.copyFile(src, dest); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('rmfile', () => { + test('deletes the file', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + await assertExists(filename); + + await filesystem.rmfile(filename); + + await assertDoesNotExist(filename); + }); + + test('fails if the file does not exist', async () => { + const promise = filesystem.rmfile(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('rmdir', () => { + test('deletes the directory if empty', async () => { + const dirname = await fix.createDirectory('x'); + await assertExists(dirname); + + await filesystem.rmdir(dirname); + + await assertDoesNotExist(dirname); + }); + + test('fails if the directory is not empty', async () => { + const dirname = await fix.createDirectory('x'); + const filename = await fix.createFile('x/y/z/spam.py'); + await assertExists(filename); + + const promise = filesystem.rmdir(dirname); + + await expect(promise).to.eventually.be.rejected; + }); + + test('fails if the directory does not exist', async () => { + const promise = filesystem.rmdir(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('rmtree', () => { + test('deletes the directory if empty', async () => { + const dirname = await fix.createDirectory('x'); + await assertExists(dirname); + + await filesystem.rmtree(dirname); + + await assertDoesNotExist(dirname); + }); + + test('deletes the directory if not empty', async () => { + const dirname = await fix.createDirectory('x'); + const filename = await fix.createFile('x/y/z/spam.py'); + await assertExists(filename); + + await filesystem.rmtree(dirname); + + await assertDoesNotExist(dirname); + }); + + test('fails if the directory does not exist', async () => { + const promise = filesystem.rmtree(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('mkdirp', () => { + test('creates the directory and all missing parents', async () => { + await fix.createDirectory('x'); + // x/y, x/y/z, and x/y/z/spam are all missing. + const dirname = await fix.resolve('x/y/z/spam', false); + await assertDoesNotExist(dirname); + + await filesystem.mkdirp(dirname); + + await assertExists(dirname); + }); + + test('works if the directory already exists', async () => { + const dirname = await fix.createDirectory('spam'); + await assertExists(dirname); + + await filesystem.mkdirp(dirname); + + await assertExists(dirname); + }); + }); + + suite('listdir', () => { + test('mixed', async function () { + // https://github.com/microsoft/vscode-python/issues/10240 + // tslint:disable-next-line: no-invalid-this + return this.skip(); + // Create the target directory and its contents. + const dirname = await fix.createDirectory('x/y/z'); + const file1 = await fix.createFile('x/y/z/__init__.py', ''); + const script = await fix.createFile('x/y/z/__main__.py', '<script here>'); + const file2 = await fix.createFile('x/y/z/spam.py', '...'); + const file3 = await fix.createFile('x/y/z/eggs.py', '"""..."""'); + const subdir = await fix.createDirectory('x/y/z/w'); + const expected = [ + [file1, FileType.File], + [script, FileType.File], + [file3, FileType.File], + [file2, FileType.File], + [subdir, FileType.Directory] + ]; + if (SUPPORTS_SYMLINKS) { + // a symlink to a file (source not directly in listed dir) + const symlink1 = await fix.createSymlink( + 'x/y/z/info.py', + // Link to an ignored file. + await fix.createFile('x/_info.py', '<info here>') // source + ); + expected.push([symlink1, FileType.SymbolicLink | FileType.File]); + + // a symlink to a directory (source not directly in listed dir) + const symlink4 = await fix.createSymlink( + 'x/y/z/static_files', + await fix.resolve('x/y/z/w/data') // source + ); + expected.push([symlink4, FileType.SymbolicLink | FileType.Directory]); + + // a broken symlink + // TODO (https://github.com/microsoft/vscode/issues/90031): + // VS Code ignores broken symlinks currently... + //const symlink2 = await fix.createSymlink( + // 'x/y/z/broken', + // DOES_NOT_EXIST // source + //); + //expected.push([symlink2, FileType.SymbolicLink]); + } + if (SUPPORTS_SOCKETS) { + // a socket + const sock = await fix.createSocket('x/y/z/ipc.sock'); + expected.push([sock, FileType.Unknown]); + + if (SUPPORTS_SYMLINKS) { + // a symlink to a socket + const symlink3 = await fix.createSymlink( + 'x/y/z/ipc.sck', + sock // source + ); + expected.push( + // TODO (https://github.com/microsoft/vscode/issues/90032): + // VS Code gets symlinks to "unknown" files wrong: + [symlink3, FileType.SymbolicLink | FileType.File] + //[symlink3, FileType.SymbolicLink] + ); + } + } + // Create other files and directories (should be ignored). + await fix.createFile('x/__init__.py', ''); + await fix.createFile('x/y/__init__.py', ''); + await fix.createDirectory('x/y/z/w/data'); + await fix.createFile('x/y/z/w/data/v1.json'); + if (SUPPORTS_SYMLINKS) { + // a broken symlink + // TODO (https://github.com/microsoft/vscode/issues/90031): + // VS Code ignores broken symlinks currently... + await fix.createSymlink( + 'x/y/z/broken', + DOES_NOT_EXIST // source + ); + + // a symlink outside the listed dir (to a file inside the dir) + await fix.createSymlink( + 'my-script.py', + // Link to a listed file. + script // source (__main__.py) + ); + + // a symlink in a subdir (to a file outside the dir) + await fix.createSymlink( + 'x/y/z/w/__init__.py', + await fix.createFile('x/__init__.py', '') // source + ); + } + + const entries = await filesystem.listdir(dirname); + + expect(entries.sort()).to.deep.equal(expected.sort()); + }); + + test('empty', async () => { + const dirname = await fix.createDirectory('x/y/z/eggs'); + + const entries = await filesystem.listdir(dirname); + + expect(entries).to.deep.equal([]); + }); + + test('fails if the directory does not exist', async () => { + const promise = filesystem.listdir(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); +}); + +suite('FileSystem - utils', () => { + let utils: IFileSystemUtils; + let fix: FSFixture; + setup(async () => { + utils = FileSystemUtils.withDefaults(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + }); + + suite('createDirectory', () => { + test('wraps the low-level impl', async () => { + await fix.createDirectory('x'); + // x/y, x/y/z, and x/y/z/spam are all missing. + const dirname = await fix.resolve('x/spam', false); + await assertDoesNotExist(dirname); + + await utils.createDirectory(dirname); + + await assertExists(dirname); + }); + }); + + suite('deleteDirectory', () => { + test('wraps the low-level impl', async () => { + const dirname = await fix.createDirectory('x'); + await assertExists(dirname); + + await utils.deleteDirectory(dirname); + + await assertDoesNotExist(dirname); + }); + }); + + suite('deleteFile', () => { + test('wraps the low-level impl', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + await assertExists(filename); + + await utils.deleteFile(filename); + + await assertDoesNotExist(filename); + }); + }); + + suite('pathExists', () => { + test('exists (without type)', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await utils.pathExists(filename); + + expect(exists).to.equal(true); + }); + + test('does not exist (without type)', async () => { + const exists = await utils.pathExists(DOES_NOT_EXIST); + + expect(exists).to.equal(false); + }); + + test('matches (type: file)', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await utils.pathExists(filename, FileType.File); + + expect(exists).to.equal(true); + }); + + test('mismatch (type: file)', async () => { + const filename = await fix.createDirectory('x/y/z/spam.py'); + + const exists = await utils.pathExists(filename, FileType.File); + + expect(exists).to.equal(false); + }); + + test('matches (type: directory)', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + + const exists = await utils.pathExists(dirname, FileType.Directory); + + expect(exists).to.equal(true); + }); + + test('mismatch (type: directory)', async () => { + const dirname = await fix.createFile('x/y/z/spam'); + + const exists = await utils.pathExists(dirname, FileType.Directory); + + expect(exists).to.equal(false); + }); + + test('symlinks are followed', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + + const exists = await utils.pathExists(symlink, FileType.SymbolicLink); + const destIsFile = await utils.pathExists(symlink, FileType.File); + const destIsDir = await utils.pathExists(symlink, FileType.Directory); + + expect(exists).to.equal(true); + expect(destIsFile).to.equal(true); + expect(destIsDir).to.equal(false); + }); + + test('mismatch (type: symlink)', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await utils.pathExists(filename, FileType.SymbolicLink); + + expect(exists).to.equal(false); + }); + + test('matches (type: unknown)', async function () { + if (!SUPPORTS_SOCKETS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = await utils.pathExists(sockFile, FileType.Unknown); + + expect(exists).to.equal(true); + }); + + test('mismatch (type: unknown)', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await utils.pathExists(filename, FileType.Unknown); + + expect(exists).to.equal(false); + }); + }); + + suite('fileExists', () => { + test('want file, got file', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await utils.fileExists(filename); + + expect(exists).to.equal(true); + }); + + test('want file, not file', async () => { + const filename = await fix.createDirectory('x/y/z/spam.py'); + + const exists = await utils.fileExists(filename); + + expect(exists).to.equal(false); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + + const exists = await utils.fileExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = await utils.fileExists(sockFile); + + expect(exists).to.equal(false); + }); + + test('failure in stat()', async function () { + if (WINDOWS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const dirname = await fix.createDirectory('x/y/z'); + const filename = await fix.createFile('x/y/z/spam.py', '...'); + await fsextra.chmod(dirname, 0o400); + + let exists: boolean; + try { + exists = await utils.fileExists(filename); + } finally { + await fsextra.chmod(dirname, 0o755); + } + + expect(exists).to.equal(false); + }); + }); + + suite('directoryExists', () => { + test('want directory, got directory', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + + const exists = await utils.directoryExists(dirname); + + expect(exists).to.equal(true); + }); + + test('want directory, not directory', async () => { + const dirname = await fix.createFile('x/y/z/spam'); + + const exists = await utils.directoryExists(dirname); + + expect(exists).to.equal(false); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const dirname = await fix.createDirectory('x/y/z/spam'); + const symlink = await fix.createSymlink('x/y/z/eggs', dirname); + + const exists = await utils.directoryExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = await utils.directoryExists(sockFile); + + expect(exists).to.equal(false); + }); + + test('failure in stat()', async function () { + if (WINDOWS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const parentdir = await fix.createDirectory('x/y/z'); + const dirname = await fix.createDirectory('x/y/z/spam'); + await fsextra.chmod(parentdir, 0o400); + + let exists: boolean; + try { + exists = await utils.fileExists(dirname); + } finally { + await fsextra.chmod(parentdir, 0o755); + } + + expect(exists).to.equal(false); + }); + }); + + suite('listdir', () => { + test('wraps the low-level impl', async () => { + test('mixed', async () => { + // Create the target directory and its contents. + const dirname = await fix.createDirectory('x/y/z'); + const file = await fix.createFile('x/y/z/__init__.py', ''); + const subdir = await fix.createDirectory('x/y/z/w'); + + const entries = await utils.listdir(dirname); + + expect(entries.sort()).to.deep.equal([ + [file, FileType.File], + [subdir, FileType.Directory] + ]); + }); + }); + }); + + suite('getSubDirectories', () => { + test('empty if the directory does not exist', async () => { + const entries = await utils.getSubDirectories(DOES_NOT_EXIST); + + expect(entries).to.deep.equal([]); + }); + }); + + suite('getFiles', () => { + test('empty if the directory does not exist', async () => { + const entries = await utils.getFiles(DOES_NOT_EXIST); + + expect(entries).to.deep.equal([]); + }); + }); + + suite('isDirReadonly', () => { + suite('non-Windows', () => { + suiteSetup(function () { + if (WINDOWS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + + // On Windows, chmod won't have any effect on the file itself. + test('is readonly', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + await fsextra.chmod(dirname, 0o444); + + const isReadonly = await utils.isDirReadonly(dirname); + + expect(isReadonly).to.equal(true); + }); + }); + + test('is not readonly', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + + const isReadonly = await utils.isDirReadonly(dirname); + + expect(isReadonly).to.equal(false); + }); + + test('fail if the directory does not exist', async () => { + const promise = utils.isDirReadonly(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); +}); + +suite('FileSystem', () => { + let filesystem: IFileSystem; + let fix: FSFixture; + setup(async () => { + filesystem = new FileSystem(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + }); + + suite('raw', () => { + suite('stat', () => { + setup(function () { + // https://github.com/microsoft/vscode-python/issues/10294 + // tslint:disable-next-line: no-invalid-this + this.skip(); + }); + test('gets the info for an existing file', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const old = await fsextra.stat(filename); + const expected = convertStat(old, FileType.File); + + const stat = await filesystem.stat(filename); + + expect(stat).to.deep.equal(expected); + }); + + test('gets the info for an existing directory', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + const old = await fsextra.stat(dirname); + const expected = convertStat(old, FileType.Directory); + + const stat = await filesystem.stat(dirname); + + expect(stat).to.deep.equal(expected); + }); + + test('for symlinks, gets the info for the linked file', async function () { + // https://github.com/microsoft/vscode-python/issues/10294 + // tslint:disable-next-line:no-invalid-this + this.skip(); + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + const old = await fsextra.stat(filename); + const expected = convertStat(old, FileType.SymbolicLink | FileType.File); + + const stat = await filesystem.stat(symlink); + + expect(stat).to.deep.equal(expected); + }); + + test('gets the info for a socket', async function () { + if (!SUPPORTS_SOCKETS) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + const sock = await fix.createSocket('x/spam.sock'); + const old = await fsextra.stat(sock); + const expected = convertStat(old, FileType.Unknown); + + const stat = await filesystem.stat(sock); + + expect(stat).to.deep.equal(expected); + }); + + test('fails if the file does not exist', async () => { + const promise = filesystem.stat(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('createDirectory', () => { + test('wraps the low-level impl', async () => { + await fix.createDirectory('x'); + // x/y, x/y/z, and x/y/z/spam are all missing. + const dirname = await fix.resolve('x/spam', false); + await assertDoesNotExist(dirname); + + await filesystem.createDirectory(dirname); + + await assertExists(dirname); + }); + }); + + suite('deleteDirectory', () => { + test('wraps the low-level impl', async () => { + const dirname = await fix.createDirectory('x'); + await assertExists(dirname); + + await filesystem.deleteDirectory(dirname); + + await assertDoesNotExist(dirname); + }); + }); + + suite('listdir', () => { + test('wraps the low-level impl', async () => { + test('mixed', async () => { + // Create the target directory and its contents. + const dirname = await fix.createDirectory('x/y/z'); + const file = await fix.createFile('x/y/z/__init__.py', ''); + const subdir = await fix.createDirectory('x/y/z/w'); + + const entries = await filesystem.listdir(dirname); + + expect(entries.sort()).to.deep.equal([ + [file, FileType.File], + [subdir, FileType.Directory] + ]); + }); + }); + }); + + suite('readFile', () => { + test('wraps the low-level impl', async () => { + const expected = '<some text>'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const content = await filesystem.readFile(filename); + + expect(content).to.be.equal(expected); + }); + }); + + suite('readData', () => { + test('wraps the low-level impl', async () => { + const text = '<some text>'; + const expected = Buffer.from(text, 'utf8'); + const filename = await fix.createFile('x/y/z/spam.py', text); + + const content = await filesystem.readData(filename); + + expect(content).to.deep.equal(expected); + }); + }); + + suite('writeFile', () => { + test('wraps the low-level impl', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const data = 'line1\nline2\n'; + + await filesystem.writeFile(filename, data); + + await assertFileText(filename, data); + }); + }); + + suite('copyFile', () => { + test('wraps the low-level impl', async () => { + const data = '<content>'; + const src = await fix.createFile('x/y/z/spam.py', data); + const dest = await fix.resolve('x/y/z/spam.py.bak'); + await assertDoesNotExist(dest); + + await filesystem.copyFile(src, dest); + + await assertFileText(dest, data); + await assertFileText(src, data); // Make sure src wasn't changed. + }); + }); + + suite('move', () => { + test('wraps the low-level impl', async () => { + const source = await fix.createFile('spam.py', '<text>'); + const target = await fix.resolve('eggs-txt'); + await assertDoesNotExist(target); + + await filesystem.move(source, target); + + await assertExists(target); + const text = await fsextra.readFile(target, 'utf8'); + expect(text).to.equal('<text>'); + await assertDoesNotExist(source); + }); + }); + }); + + suite('utils', () => { + suite('fileExists', () => { + test('want file, got file', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await filesystem.fileExists(filename); + + expect(exists).to.equal(true); + }); + + test('want file, not file', async () => { + const filename = await fix.createDirectory('x/y/z/spam.py'); + + const exists = await filesystem.fileExists(filename); + + expect(exists).to.equal(false); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + + const exists = await filesystem.fileExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = await filesystem.fileExists(sockFile); + + expect(exists).to.equal(false); + }); + }); + + suite('directoryExists', () => { + test('want directory, got directory', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + + const exists = await filesystem.directoryExists(dirname); + + expect(exists).to.equal(true); + }); + + test('want directory, not directory', async () => { + const dirname = await fix.createFile('x/y/z/spam'); + + const exists = await filesystem.directoryExists(dirname); + + expect(exists).to.equal(false); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const dirname = await fix.createDirectory('x/y/z/spam'); + const symlink = await fix.createSymlink('x/y/z/eggs', dirname); + + const exists = await filesystem.directoryExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = await filesystem.directoryExists(sockFile); + + expect(exists).to.equal(false); + }); + }); + + suite('getSubDirectories', () => { + test('mixed types', async () => { + // Create the target directory and its subdirs. + const dirname = await fix.createDirectory('x/y/z/scripts'); + const expected = [ + await fix.createDirectory('x/y/z/scripts/w'), // subdir1 + await fix.createDirectory('x/y/z/scripts/v') // subdir2 + ]; + if (SUPPORTS_SYMLINKS) { + // a symlink to a directory (source is outside listed dir) + const symlinkDirSource = await fix.createDirectory('x/data'); + const symlink = await fix.createSymlink('x/y/z/scripts/datadir', symlinkDirSource); + expected.push(symlink); + } + // Create files in the directory (should be ignored). + await fix.createFile('x/y/z/scripts/spam.py'); + await fix.createFile('x/y/z/scripts/eggs.py'); + await fix.createFile('x/y/z/scripts/data.json'); + if (SUPPORTS_SYMLINKS) { + // a symlink to a file (source outside listed dir) + const symlinkFileSource = await fix.createFile('x/info.py'); + await fix.createSymlink('x/y/z/scripts/other', symlinkFileSource); + } + if (SUPPORTS_SOCKETS) { + // a plain socket + await fix.createSocket('x/y/z/scripts/spam.sock'); + } + + const results = await filesystem.getSubDirectories(dirname); + + expect(results.sort()).to.deep.equal(expected.sort()); + }); + + test('empty if the directory does not exist', async () => { + const entries = await filesystem.getSubDirectories(DOES_NOT_EXIST); + + expect(entries).to.deep.equal([]); + }); + }); + + suite('getFiles', () => { + test('mixed types', async () => { + // Create the target directory and its files. + const dirname = await fix.createDirectory('x/y/z/scripts'); + const expected = [ + await fix.createFile('x/y/z/scripts/spam.py'), // file1 + await fix.createFile('x/y/z/scripts/eggs.py'), // file2 + await fix.createFile('x/y/z/scripts/data.json') // file3 + ]; + if (SUPPORTS_SYMLINKS) { + const symlinkFileSource = await fix.createFile('x/info.py'); + const symlink = await fix.createSymlink('x/y/z/scripts/other', symlinkFileSource); + expected.push(symlink); + } + // Create subdirs, sockets, etc. in the directory (should be ignored). + await fix.createDirectory('x/y/z/scripts/w'); + await fix.createDirectory('x/y/z/scripts/v'); + if (SUPPORTS_SYMLINKS) { + const symlinkDirSource = await fix.createDirectory('x/data'); + await fix.createSymlink('x/y/z/scripts/datadir', symlinkDirSource); + } + if (SUPPORTS_SOCKETS) { + await fix.createSocket('x/y/z/scripts/spam.sock'); + } + + const results = await filesystem.getFiles(dirname); + + expect(results.sort()).to.deep.equal(expected.sort()); + }); + + test('empty if the directory does not exist', async () => { + const entries = await filesystem.getFiles(DOES_NOT_EXIST); + + expect(entries).to.deep.equal([]); + }); + }); + + suite('isDirReadonly', () => { + suite('non-Windows', () => { + suiteSetup(function () { + if (WINDOWS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + + // On Windows, chmod won't have any effect on the file itself. + test('is readonly', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + await fsextra.chmod(dirname, 0o444); + + const isReadonly = await filesystem.isDirReadonly(dirname); + + expect(isReadonly).to.equal(true); + }); + }); + + test('is not readonly', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + + const isReadonly = await filesystem.isDirReadonly(dirname); + + expect(isReadonly).to.equal(false); + }); + + test('fail if the directory does not exist', async () => { + const promise = filesystem.isDirReadonly(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + }); +}); diff --git a/src/test/common/platform/filesystem.unit.test.ts b/src/test/common/platform/filesystem.unit.test.ts new file mode 100644 index 000000000000..00e0ca55c9e3 --- /dev/null +++ b/src/test/common/platform/filesystem.unit.test.ts @@ -0,0 +1,1529 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as fsextra from 'fs-extra'; +import * as TypeMoq from 'typemoq'; +import * as vscode from 'vscode'; +import { FileSystemUtils, RawFileSystem } from '../../../client/common/platform/fileSystem'; +import { + FileStat, + FileType, + // These interfaces are needed for FileSystemUtils deps. + IFileSystemPaths, + IFileSystemPathUtils, + IRawFileSystem, + ITempFileSystem, + ReadStream, + WriteStream +} from '../../../client/common/platform/types'; + +// tslint:disable:max-func-body-length chai-vague-errors + +function Uri(filename: string): vscode.Uri { + return vscode.Uri.file(filename); +} + +function createDummyStat(filetype: FileType): FileStat { + //tslint:disable-next-line:no-any + return { type: filetype } as any; +} + +function copyStat(stat: FileStat, old: TypeMoq.IMock<fsextra.Stats>) { + old.setup((s) => s.size) // plug in the original value + .returns(() => stat.size); + old.setup((s) => s.ctimeMs) // plug in the original value + .returns(() => stat.ctime); + old.setup((s) => s.mtimeMs) // plug in the original value + .returns(() => stat.mtime); +} + +interface IPaths { + // fs paths (IFileSystemPaths) + sep: string; + dirname(filename: string): string; + join(...paths: string[]): string; +} + +interface IRawFS extends IPaths { + // vscode.workspace.fs + copy(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable<void>; + createDirectory(uri: vscode.Uri): Thenable<void>; + delete(uri: vscode.Uri, options?: { recursive: boolean; useTrash: boolean }): Thenable<void>; + readDirectory(uri: vscode.Uri): Thenable<[string, FileType][]>; + readFile(uri: vscode.Uri): Thenable<Uint8Array>; + rename(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable<void>; + stat(uri: vscode.Uri): Thenable<FileStat>; + writeFile(uri: vscode.Uri, content: Uint8Array): Thenable<void>; + + // "fs-extra" + lstat(filename: string): Promise<fs.Stats>; + chmod(filePath: string, mode: string | number): Promise<void>; + appendFile(filename: string, data: {}): Promise<void>; + lstatSync(filename: string): fs.Stats; + statSync(filename: string): fs.Stats; + readFileSync(path: string, encoding: string): string; + createReadStream(filename: string): ReadStream; + createWriteStream(filename: string): WriteStream; +} + +suite('Raw FileSystem', () => { + let raw: TypeMoq.IMock<IRawFS>; + let oldStats: TypeMoq.IMock<fs.Stats>[]; + let filesystem: RawFileSystem; + setup(() => { + raw = TypeMoq.Mock.ofType<IRawFS>(undefined, TypeMoq.MockBehavior.Strict); + oldStats = []; + filesystem = new RawFileSystem( + // Since it's a mock we can just use it for all 3 values. + raw.object, + raw.object, + raw.object + ); + }); + function verifyAll() { + raw.verifyAll(); + oldStats.forEach((stat) => { + stat.verifyAll(); + }); + } + function createMockLegacyStat(): TypeMoq.IMock<fsextra.Stats> { + const stat = TypeMoq.Mock.ofType<fsextra.Stats>(undefined, TypeMoq.MockBehavior.Strict); + // This is necessary because passing "mock.object" to + // Promise.resolve() triggers the lookup. + //tslint:disable-next-line:no-any + stat.setup((s: any) => s.then) + .returns(() => undefined) + .verifiable(TypeMoq.Times.atLeast(0)); + oldStats.push(stat); + return stat; + } + function setupStatFileType(stat: TypeMoq.IMock<fs.Stats>, filetype: FileType) { + // This mirrors the logic in convertFileType(). + if (filetype === FileType.File) { + stat.setup((s) => s.isFile()) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + } else if (filetype === FileType.Directory) { + stat.setup((s) => s.isFile()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + stat.setup((s) => s.isDirectory()) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + } else if ((filetype & FileType.SymbolicLink) > 0) { + stat.setup((s) => s.isFile()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + stat.setup((s) => s.isDirectory()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + stat.setup((s) => s.isSymbolicLink()) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + } else if (filetype === FileType.Unknown) { + stat.setup((s) => s.isFile()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + stat.setup((s) => s.isDirectory()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + stat.setup((s) => s.isSymbolicLink()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + } else { + throw Error(`unsupported file type ${filetype}`); + } + } + + suite('stat', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + const expected = createDummyStat(FileType.File); + raw.setup((r) => r.stat(Uri(filename))) // expect the specific filename + .returns(() => Promise.resolve(expected)); + + const stat = await filesystem.stat(filename); + + expect(stat).to.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.stat('spam.py'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('lstat', () => { + [ + { kind: 'file', filetype: FileType.File }, + { kind: 'dir', filetype: FileType.Directory }, + { kind: 'symlink', filetype: FileType.SymbolicLink }, + { kind: 'unknown', filetype: FileType.Unknown } + ].forEach((testData) => { + test(`wraps the low-level function (filetype: ${testData.kind}`, async () => { + const filename = 'x/y/z/spam.py'; + const expected: FileStat = { + type: testData.filetype, + size: 10, + ctime: 101, + mtime: 102 + //tslint:disable-next-line:no-any + } as any; + const old = createMockLegacyStat(); + setupStatFileType(old, testData.filetype); + copyStat(expected, old); + raw.setup((r) => r.lstat(filename)) // expect the specific filename + .returns(() => Promise.resolve(old.object)); + + const stat = await filesystem.lstat(filename); + + expect(stat).to.deep.equal(expected); + verifyAll(); + }); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.lstat(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.lstat('spam.py'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('chmod', () => { + test('passes through a string mode', async () => { + const filename = 'x/y/z/spam.py'; + const mode = '755'; + raw.setup((r) => r.chmod(filename, mode)) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.chmod(filename, mode); + + verifyAll(); + }); + + test('passes through an int mode', async () => { + const filename = 'x/y/z/spam.py'; + const mode = 0o755; + raw.setup((r) => r.chmod(filename, mode)) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.chmod(filename, mode); + + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.chmod(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.chmod('spam.py', 755); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('move', () => { + test('move a file (target does not exist)', async () => { + const src = 'x/y/z/spam.py'; + const tgt = 'x/y/spam.py'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y'); + raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.move(src, tgt); + + verifyAll(); + }); + + test('move a file (target exists)', async () => { + const src = 'x/y/z/spam.py'; + const tgt = 'x/y/spam.py'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y'); + raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + const err = vscode.FileSystemError.FileExists('...'); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename + .returns(() => Promise.reject(err)); + raw.setup((r) => r.stat(Uri(tgt))) // It's a file. + .returns(() => Promise.resolve(({ type: FileType.File } as unknown) as FileStat)); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: true })) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.move(src, tgt); + + verifyAll(); + }); + + test('move a directory (target does not exist)', async () => { + const src = 'x/y/z/spam'; + const tgt = 'x/y/spam'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y'); + raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.move(src, tgt); + + verifyAll(); + }); + + test('moving a directory fails if target exists', async () => { + const src = 'x/y/z/spam.py'; + const tgt = 'x/y/spam.py'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y'); + raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + const err = vscode.FileSystemError.FileExists('...'); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename + .returns(() => Promise.reject(err)); + raw.setup((r) => r.stat(Uri(tgt))) // It's a directory. + .returns(() => Promise.resolve(({ type: FileType.Directory } as unknown) as FileStat)); + + const promise = filesystem.move(src, tgt); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('move a symlink to a directory (target exists)', async () => { + const src = 'x/y/z/spam'; + const tgt = 'x/y/spam.lnk'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y'); + raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + const err = vscode.FileSystemError.FileExists('...'); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename + .returns(() => Promise.reject(err)); + raw.setup((r) => r.stat(Uri(tgt))) // It's a symlink. + .returns(() => + Promise.resolve(({ type: FileType.SymbolicLink | FileType.Directory } as unknown) as FileStat) + ); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: true })) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.move(src, tgt); + + verifyAll(); + }); + + test('fails if the target parent dir does not exist', async () => { + raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. + .returns(() => ''); + const err = vscode.FileSystemError.FileNotFound('...'); + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir does not exist. + .returns(() => Promise.reject(err)); + + const promise = filesystem.move('spam', 'eggs'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. + .returns(() => ''); + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + const err = new Error('oops!'); + raw.setup((r) => r.rename(TypeMoq.It.isAny(), TypeMoq.It.isAny(), { overwrite: false })) // We don't care about the filename. + .throws(err); + + const promise = filesystem.move('spam', 'eggs'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('readData', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + const expected = Buffer.from('<data>'); + raw.setup((r) => r.readFile(Uri(filename))) // expect the specific filename + .returns(() => Promise.resolve(expected)); + + const data = await filesystem.readData(filename); + + expect(data).to.deep.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.readFile(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.readData('spam.py'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('readText', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + const expected = '<text>'; + const data = Buffer.from(expected); + raw.setup((r) => r.readFile(Uri(filename))) // expect the specific filename + .returns(() => Promise.resolve(data)); + + const text = await filesystem.readText(filename); + + expect(text).to.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.readFile(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.readText('spam.py'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('writeText', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + const text = '<text>'; + const data = Buffer.from(text); + raw.setup((r) => r.writeFile(Uri(filename), data)) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.writeText(filename, text); + + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.writeFile(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.writeText('spam.py', '<text>'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('appendText', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + const text = '<text>'; + raw.setup((r) => r.appendFile(filename, text)) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.appendText(filename, text); + + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.appendFile(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.appendText('spam.py', '<text>'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('copyFile', () => { + test('wraps the low-level function', async () => { + const src = 'x/y/z/spam.py'; + const tgt = 'x/y/z/eggs.py'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y/z'); + raw.setup((r) => r.stat(Uri('x/y/z'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.copy(Uri(src), Uri(tgt), { overwrite: true })) // Expect the specific args. + .returns(() => Promise.resolve()); + + await filesystem.copyFile(src, tgt); + + verifyAll(); + }); + + test('fails if target parent does not exist', async () => { + raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. + .returns(() => ''); + const err = vscode.FileSystemError.FileNotFound('...'); + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir exists. + .returns(() => Promise.reject(err)); + + const promise = filesystem.copyFile('spam', 'eggs'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. + .returns(() => ''); + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.copy(TypeMoq.It.isAny(), TypeMoq.It.isAny(), { overwrite: true })) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.copyFile('spam', 'eggs'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('rmFile', () => { + const opts = { + recursive: false, + useTrash: false + }; + + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + raw.setup((r) => r.delete(Uri(filename), opts)) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.rmfile(filename); + + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.delete(TypeMoq.It.isAny(), opts)) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.rmfile('spam.py'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('mkdirp', () => { + test('wraps the low-level function', async () => { + const dirname = 'x/y/z/spam'; + raw.setup((r) => r.createDirectory(Uri(dirname))) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.mkdirp(dirname); + + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.createDirectory(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.mkdirp('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('rmdir', () => { + const opts = { + recursive: true, + useTrash: false + }; + + test('directory is empty', async () => { + const dirname = 'x/y/z/spam'; + raw.setup((r) => r.readDirectory(Uri(dirname))) // The dir is empty. + .returns(() => Promise.resolve([])); + raw.setup((r) => r.delete(Uri(dirname), opts)) // Expect the specific args. + .returns(() => Promise.resolve()); + + await filesystem.rmdir(dirname); + + verifyAll(); + }); + + test('fails if readDirectory() fails (e.g. is a file)', async () => { + raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // It's not a directory. + .throws(new Error('is a file')); + + const promise = filesystem.rmdir('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('fails if not empty', async () => { + const entries: [string, FileType][] = [ + ['dev1', FileType.Unknown], + ['w', FileType.Directory], + ['spam.py', FileType.File], + ['other', FileType.SymbolicLink | FileType.File] + ]; + raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // The dir is not empty. + .returns(() => Promise.resolve(entries)); + + const promise = filesystem.rmdir('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // The "file" exists. + .returns(() => Promise.resolve([])); + raw.setup((r) => r.delete(TypeMoq.It.isAny(), opts)) // We don't care about the filename. + .throws(new Error('oops!')); + + const promise = filesystem.rmdir('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('rmtree', () => { + const opts = { + recursive: true, + useTrash: false + }; + + test('wraps the low-level function', async () => { + const dirname = 'x/y/z/spam'; + raw.setup((r) => r.stat(Uri(dirname))) // The dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.delete(Uri(dirname), opts)) // Expect the specific dirname. + .returns(() => Promise.resolve()); + + await filesystem.rmtree(dirname); + + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The "file" exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.delete(TypeMoq.It.isAny(), opts)) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.rmtree('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('listdir', () => { + test('mixed', async () => { + const dirname = 'x/y/z/spam'; + const actual: [string, FileType][] = [ + ['dev1', FileType.Unknown], + ['w', FileType.Directory], + ['spam.py', FileType.File], + ['other', FileType.SymbolicLink | FileType.File] + ]; + const expected = actual.map(([basename, filetype]) => { + const filename = `x/y/z/spam/${basename}`; + raw.setup((r) => r.join(dirname, basename)) // Expect the specific basename. + .returns(() => filename); + return [filename, filetype] as [string, FileType]; + }); + raw.setup((r) => r.readDirectory(Uri(dirname))) // Expect the specific filename. + .returns(() => Promise.resolve(actual)); + + const entries = await filesystem.listdir(dirname); + + expect(entries).to.deep.equal(expected); + verifyAll(); + }); + + test('empty', async () => { + const dirname = 'x/y/z/spam'; + const expected: [string, FileType][] = []; + raw.setup((r) => r.readDirectory(Uri(dirname))) // expect the specific filename + .returns(() => Promise.resolve([])); + + const entries = await filesystem.listdir(dirname); + + expect(entries).to.deep.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.listdir('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('statSync', () => { + test('wraps the low-level function (filetype: unknown)', async () => { + const filename = 'x/y/z/spam.py'; + const expected: FileStat = { + type: FileType.Unknown, + size: 10, + ctime: 101, + mtime: 102 + //tslint:disable-next-line:no-any + } as any; + const lstat = createMockLegacyStat(); + setupStatFileType(lstat, FileType.Unknown); + copyStat(expected, lstat); + raw.setup((r) => r.lstatSync(filename)) // expect the specific filename + .returns(() => lstat.object); + + const stat = filesystem.statSync(filename); + + expect(stat).to.deep.equal(expected); + verifyAll(); + }); + + [ + { kind: 'file', filetype: FileType.File }, + { kind: 'dir', filetype: FileType.Directory } + ].forEach((testData) => { + test(`wraps the low-level function (filetype: ${testData.kind})`, async () => { + const filename = 'x/y/z/spam.py'; + const expected: FileStat = { + type: testData.filetype, + size: 10, + ctime: 101, + mtime: 102 + //tslint:disable-next-line:no-any + } as any; + const lstat = createMockLegacyStat(); + lstat + .setup((s) => s.isSymbolicLink()) // not a symlink + .returns(() => false); + setupStatFileType(lstat, testData.filetype); + copyStat(expected, lstat); + raw.setup((r) => r.lstatSync(filename)) // expect the specific filename + .returns(() => lstat.object); + + const stat = filesystem.statSync(filename); + + expect(stat).to.deep.equal(expected); + verifyAll(); + }); + }); + + [ + { kind: 'file', filetype: FileType.File }, + { kind: 'dir', filetype: FileType.Directory }, + { kind: 'unknown', filetype: FileType.Unknown } + ].forEach((testData) => { + test(`wraps the low-level function (filetype: ${testData.kind} symlink)`, async () => { + const filename = 'x/y/z/spam.py'; + const expected: FileStat = { + type: testData.filetype | FileType.SymbolicLink, + size: 10, + ctime: 101, + mtime: 102 + //tslint:disable-next-line:no-any + } as any; + const lstat = createMockLegacyStat(); + lstat + .setup((s) => s.isSymbolicLink()) // not a symlink + .returns(() => true); + raw.setup((r) => r.lstatSync(filename)) // expect the specific filename + .returns(() => lstat.object); + const old = createMockLegacyStat(); + setupStatFileType(old, testData.filetype); + copyStat(expected, old); + raw.setup((r) => r.statSync(filename)) // expect the specific filename + .returns(() => old.object); + + const stat = filesystem.statSync(filename); + + expect(stat).to.deep.equal(expected); + verifyAll(); + }); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.lstatSync(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + expect(() => { + filesystem.statSync('spam.py'); + }).to.throw(); + verifyAll(); + }); + }); + + suite('readTextSync', () => { + test('wraps the low-level function', () => { + const filename = 'x/y/z/spam.py'; + const expected = '<text>'; + raw.setup((r) => r.readFileSync(filename, 'utf8')) // expect the specific filename + .returns(() => expected); + + const text = filesystem.readTextSync(filename); + + expect(text).to.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.readFileSync(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + expect(() => filesystem.readTextSync('spam.py')).to.throw(); + + verifyAll(); + }); + }); + + suite('createReadStream', () => { + test('wraps the low-level function', () => { + const filename = 'x/y/z/spam.py'; + //tslint:disable-next-line:no-any + const expected = {} as any; + raw.setup((r) => r.createReadStream(filename)) // expect the specific filename + .returns(() => expected); + + const stream = filesystem.createReadStream(filename); + + expect(stream).to.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.createReadStream(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + expect(() => filesystem.createReadStream('spam.py')).to.throw(); + + verifyAll(); + }); + }); + + suite('createWriteStream', () => { + test('wraps the low-level function', () => { + const filename = 'x/y/z/spam.py'; + //tslint:disable-next-line:no-any + const expected = {} as any; + raw.setup((r) => r.createWriteStream(filename)) // expect the specific filename + .returns(() => expected); + + const stream = filesystem.createWriteStream(filename); + + expect(stream).to.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.createWriteStream(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + expect(() => filesystem.createWriteStream('spam.py')).to.throw(); + + verifyAll(); + }); + }); +}); + +interface IUtilsDeps extends IRawFileSystem, IFileSystemPaths, IFileSystemPathUtils, ITempFileSystem { + // helpers + getHash(data: string): string; + globFile(pat: string, options?: { cwd: string }): Promise<string[]>; +} + +suite('FileSystemUtils', () => { + let deps: TypeMoq.IMock<IUtilsDeps>; + let stats: TypeMoq.IMock<FileStat>[]; + let utils: FileSystemUtils; + setup(() => { + deps = TypeMoq.Mock.ofType<IUtilsDeps>(undefined, TypeMoq.MockBehavior.Strict); + + stats = []; + utils = new FileSystemUtils( + // Since it's a mock we can just use it for all 3 values. + deps.object, // rawFS + deps.object, // pathUtils + deps.object, // paths + deps.object, // tempFS + (data: string) => deps.object.getHash(data), + (pat: string, options?: { cwd: string }) => deps.object.globFile(pat, options) + ); + }); + function verifyAll() { + deps.verifyAll(); + stats.forEach((stat) => { + stat.verifyAll(); + }); + } + function createMockStat(): TypeMoq.IMock<FileStat> { + const stat = TypeMoq.Mock.ofType<FileStat>(undefined, TypeMoq.MockBehavior.Strict); + // This is necessary because passing "mock.object" to + // Promise.resolve() triggers the lookup. + //tslint:disable-next-line:no-any + stat.setup((s: any) => s.then) + .returns(() => undefined) + .verifiable(TypeMoq.Times.atLeast(0)); + stats.push(stat); + return stat; + } + + suite('createDirectory', () => { + test('wraps the low-level function', async () => { + const dirname = 'x/y/z/spam'; + deps.setup((d) => d.mkdirp(dirname)) // expect the specific filename + .returns(() => Promise.resolve()); + + await utils.createDirectory(dirname); + + verifyAll(); + }); + }); + + suite('deleteDirectory', () => { + test('wraps the low-level function', async () => { + const dirname = 'x/y/z/spam'; + deps.setup((d) => d.rmdir(dirname)) // expect the specific filename + .returns(() => Promise.resolve()); + + await utils.deleteDirectory(dirname); + + verifyAll(); + }); + }); + + suite('deleteFile', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + deps.setup((d) => d.rmfile(filename)) // expect the specific filename + .returns(() => Promise.resolve()); + + await utils.deleteFile(filename); + + verifyAll(); + }); + }); + + suite('pathExists', () => { + test('exists (without type)', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(filename); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('does not exist', async () => { + const filename = 'x/y/z/spam.py'; + const err = vscode.FileSystemError.FileNotFound(filename); + deps.setup((d) => d.stat(filename)) // The file does not exist. + .throws(err); + + const exists = await utils.pathExists(filename); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('ignores errors from stat()', async () => { + const filename = 'x/y/z/spam.py'; + deps.setup((d) => d.stat(filename)) // It's broken. + .returns(() => Promise.reject(new Error('oops!'))); + + const exists = await utils.pathExists(filename); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('matches (type: undefined)', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(filename); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('matches (type: file)', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a file. + .returns(() => FileType.File); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(filename, FileType.File); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('mismatch (type: file)', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a directory. + .returns(() => FileType.Directory); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(filename, FileType.File); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('matches (type: directory)', async () => { + const dirname = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a directory. + .returns(() => FileType.Directory); + deps.setup((d) => d.stat(dirname)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(dirname, FileType.Directory); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('mismatch (type: directory)', async () => { + const dirname = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a file. + .returns(() => FileType.File); + deps.setup((d) => d.stat(dirname)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(dirname, FileType.Directory); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('symlinks are followed', async () => { + const symlink = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a symlink to a file. + .returns(() => FileType.File | FileType.SymbolicLink) + .verifiable(TypeMoq.Times.exactly(3)); + deps.setup((d) => d.stat(symlink)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)) + .verifiable(TypeMoq.Times.exactly(3)); + + const exists = await utils.pathExists(symlink, FileType.SymbolicLink); + const destIsFile = await utils.pathExists(symlink, FileType.File); + const destIsDir = await utils.pathExists(symlink, FileType.Directory); + + expect(exists).to.equal(true); + expect(destIsFile).to.equal(true); + expect(destIsDir).to.equal(false); + verifyAll(); + }); + + test('mismatch (type: symlink)', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a file. + .returns(() => FileType.File); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(filename, FileType.SymbolicLink); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('matches (type: unknown)', async () => { + const sockFile = 'x/y/z/ipc.sock'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a socket. + .returns(() => FileType.Unknown); + deps.setup((d) => d.stat(sockFile)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(sockFile, FileType.Unknown); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('mismatch (type: unknown)', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a file. + .returns(() => FileType.File); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(filename, FileType.Unknown); + + expect(exists).to.equal(false); + verifyAll(); + }); + }); + + suite('fileExists', () => { + test('want file, got file', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a File. + .returns(() => FileType.File); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.fileExists(filename); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('want file, not file', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a directory. + .returns(() => FileType.Directory); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.fileExists(filename); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('symlink', async () => { + const symlink = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a symlink to a File. + .returns(() => FileType.File | FileType.SymbolicLink); + deps.setup((d) => d.stat(symlink)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.fileExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + verifyAll(); + }); + + test('unknown', async () => { + const sockFile = 'x/y/z/ipc.sock'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a socket. + .returns(() => FileType.Unknown); + deps.setup((d) => d.stat(sockFile)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.fileExists(sockFile); + + expect(exists).to.equal(false); + verifyAll(); + }); + }); + + suite('directoryExists', () => { + test('want directory, got directory', async () => { + const dirname = 'x/y/z/spam'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a directory. + .returns(() => FileType.Directory); + deps.setup((d) => d.stat(dirname)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.directoryExists(dirname); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('want directory, not directory', async () => { + const dirname = 'x/y/z/spam'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a file. + .returns(() => FileType.File); + deps.setup((d) => d.stat(dirname)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.directoryExists(dirname); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('symlink', async () => { + const symlink = 'x/y/z/spam'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a symlink to a directory. + .returns(() => FileType.Directory | FileType.SymbolicLink); + deps.setup((d) => d.stat(symlink)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.directoryExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + verifyAll(); + }); + + test('unknown', async () => { + const sockFile = 'x/y/z/ipc.sock'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a socket. + .returns(() => FileType.Unknown); + deps.setup((d) => d.stat(sockFile)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.directoryExists(sockFile); + + expect(exists).to.equal(false); + verifyAll(); + }); + }); + + suite('listdir', () => { + test('wraps the raw call on success', async () => { + const dirname = 'x/y/z/spam'; + const expected: [string, FileType][] = [ + ['x/y/z/spam/dev1', FileType.Unknown], + ['x/y/z/spam/w', FileType.Directory], + ['x/y/z/spam/spam.py', FileType.File], + ['x/y/z/spam/other', FileType.SymbolicLink | FileType.File] + ]; + deps.setup((d) => d.listdir(dirname)) // Full results get returned from RawFileSystem.listdir(). + .returns(() => Promise.resolve(expected)); + + const entries = await utils.listdir(dirname); + + expect(entries).to.deep.equal(expected); + verifyAll(); + }); + + test('returns [] if the directory does not exist', async () => { + const dirname = 'x/y/z/spam'; + const err = vscode.FileSystemError.FileNotFound(dirname); + deps.setup((d) => d.listdir(dirname)) // The "file" does not exist. + .returns(() => Promise.reject(err)); + deps.setup((d) => d.stat(dirname)) // The "file" does not exist. + .returns(() => Promise.reject(err)); + + const entries = await utils.listdir(dirname); + + expect(entries).to.deep.equal([]); + verifyAll(); + }); + + test('fails if not a directory', async () => { + const dirname = 'x/y/z/spam'; + const err = vscode.FileSystemError.FileNotADirectory(dirname); + deps.setup((d) => d.listdir(dirname)) // Fail (async) with not-a-directory. + .returns(() => Promise.reject(err)); + const stat = createMockStat(); + deps.setup((d) => d.stat(dirname)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const promise = utils.listdir(dirname); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('fails if the raw call promise fails', async () => { + const dirname = 'x/y/z/spam'; + const err = new Error('oops!'); + deps.setup((d) => d.listdir(dirname)) // Fail (async) with an arbitrary error. + .returns(() => Promise.reject(err)); + deps.setup((d) => d.stat(dirname)) // Fail with file-not-found. + .throws(vscode.FileSystemError.FileNotFound(dirname)); + + const entries = await utils.listdir(dirname); + + expect(entries).to.deep.equal([]); + verifyAll(); + }); + + test('fails if the raw call fails', async () => { + const dirname = 'x/y/z/spam'; + const err = new Error('oops!'); + deps.setup((d) => d.listdir(dirname)) // Fail with an arbirary error. + .throws(err); + const stat = createMockStat(); + deps.setup((d) => d.stat(dirname)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const promise = utils.listdir(dirname); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('getSubDirectories', () => { + test('filters out non-subdirs', async () => { + const dirname = 'x/y/z/spam'; + const entries: [string, FileType][] = [ + ['x/y/z/spam/dev1', FileType.Unknown], + ['x/y/z/spam/w', FileType.Directory], + ['x/y/z/spam/spam.py', FileType.File], + ['x/y/z/spam/v', FileType.Directory], + ['x/y/z/spam/eggs.py', FileType.File], + ['x/y/z/spam/other1', FileType.SymbolicLink | FileType.File], + ['x/y/z/spam/other2', FileType.SymbolicLink | FileType.Directory] + ]; + const expected = [ + // only entries with FileType.Directory + 'x/y/z/spam/w', + 'x/y/z/spam/v', + 'x/y/z/spam/other2' + ]; + deps.setup((d) => d.listdir(dirname)) // Full results get returned from RawFileSystem.listdir(). + .returns(() => Promise.resolve(entries)); + + const filtered = await utils.getSubDirectories(dirname); + + expect(filtered).to.deep.equal(expected); + verifyAll(); + }); + }); + + suite('getFiles', () => { + test('filters out non-files', async () => { + const filename = 'x/y/z/spam'; + const entries: [string, FileType][] = [ + ['x/y/z/spam/dev1', FileType.Unknown], + ['x/y/z/spam/w', FileType.Directory], + ['x/y/z/spam/spam.py', FileType.File], + ['x/y/z/spam/v', FileType.Directory], + ['x/y/z/spam/eggs.py', FileType.File], + ['x/y/z/spam/other1', FileType.SymbolicLink | FileType.File], + ['x/y/z/spam/other2', FileType.SymbolicLink | FileType.Directory] + ]; + const expected = [ + // only entries with FileType.File + 'x/y/z/spam/spam.py', + 'x/y/z/spam/eggs.py', + 'x/y/z/spam/other1' + ]; + deps.setup((d) => d.listdir(filename)) // Full results get returned from RawFileSystem.listdir(). + .returns(() => Promise.resolve(entries)); + + const filtered = await utils.getFiles(filename); + + expect(filtered).to.deep.equal(expected); + verifyAll(); + }); + }); + + suite('isDirReadonly', () => { + setup(() => { + deps.setup((d) => d.sep) // The value really doesn't matter. + .returns(() => '/'); + }); + + test('is not readonly', async () => { + const dirname = 'x/y/z/spam'; + const filename = `${dirname}/___vscpTest___`; + deps.setup((d) => d.stat(dirname)) // Success! + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + deps.setup((d) => d.writeText(filename, '')) // Success! + .returns(() => Promise.resolve()); + deps.setup((d) => d.rmfile(filename)) // Success! + .returns(() => Promise.resolve()); + + const isReadonly = await utils.isDirReadonly(dirname); + + expect(isReadonly).to.equal(false); + verifyAll(); + }); + + test('is readonly', async () => { + const dirname = 'x/y/z/spam'; + const filename = `${dirname}/___vscpTest___`; + const err = new Error('not permitted'); + // tslint:disable-next-line:no-any + (err as any).code = 'EACCES'; // errno + deps.setup((d) => d.stat(dirname)) // Success! + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + deps.setup((d) => d.writeText(filename, '')) // not permitted + .returns(() => Promise.reject(err)); + + const isReadonly = await utils.isDirReadonly(dirname); + + expect(isReadonly).to.equal(true); + verifyAll(); + }); + + test('fails if the directory does not exist', async () => { + const dirname = 'x/y/z/spam'; + const err = new Error('not found'); + // tslint:disable-next-line:no-any + (err as any).code = 'ENOENT'; // errno + deps.setup((d) => d.stat(dirname)) // file-not-found + .returns(() => Promise.reject(err)); + + const promise = utils.isDirReadonly(dirname); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('getFileHash', () => { + test('Getting hash for a file should return non-empty string', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.ctime) // created + .returns(() => 100); + stat.setup((s) => s.mtime) // modified + .returns(() => 120); + deps.setup((d) => d.lstat(filename)) // file exists + .returns(() => Promise.resolve(stat.object)); + deps.setup((d) => d.getHash('100-120')) // built from ctime and mtime + .returns(() => 'deadbeef'); + + const hash = await utils.getFileHash(filename); + + expect(hash).to.equal('deadbeef'); + verifyAll(); + }); + + test('Getting hash for non existent file should throw error', async () => { + const filename = 'x/y/z/spam.py'; + const err = vscode.FileSystemError.FileNotFound(filename); + deps.setup((d) => d.lstat(filename)) // file-not-found + .returns(() => Promise.reject(err)); + + const promise = utils.getFileHash(filename); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('search', () => { + test('found matches (without cwd)', async () => { + const pattern = `x/y/z/spam.*`; + const expected: string[] = [ + // We can pretend that there were other files + // that were ignored. + 'x/y/z/spam.py', + 'x/y/z/spam.pyc', + 'x/y/z/spam.so', + 'x/y/z/spam.data' + ]; + deps.setup((d) => d.globFile(pattern, undefined)) // found some + .returns(() => Promise.resolve(expected)); + + const files = await utils.search(pattern); + + expect(files).to.deep.equal(expected); + verifyAll(); + }); + + test('found matches (with cwd)', async () => { + const pattern = `x/y/z/spam.*`; + const cwd = 'a/b/c'; + const expected: string[] = [ + // We can pretend that there were other files + // that were ignored. + 'x/y/z/spam.py', + 'x/y/z/spam.pyc', + 'x/y/z/spam.so', + 'x/y/z/spam.data' + ]; + deps.setup((d) => d.globFile(pattern, { cwd: cwd })) // found some + .returns(() => Promise.resolve(expected)); + + const files = await utils.search(pattern, cwd); + + expect(files).to.deep.equal(expected); + verifyAll(); + }); + + test('no matches (empty)', async () => { + const pattern = `x/y/z/spam.*`; + deps.setup((d) => d.globFile(pattern, undefined)) // found none + .returns(() => Promise.resolve([])); + + const files = await utils.search(pattern); + + expect(files).to.deep.equal([]); + verifyAll(); + }); + + test('no matches (undefined)', async () => { + const pattern = `x/y/z/spam.*`; + deps.setup((d) => d.globFile(pattern, undefined)) // found none + .returns(() => Promise.resolve((undefined as unknown) as string[])); + + const files = await utils.search(pattern); + + expect(files).to.deep.equal([]); + verifyAll(); + }); + }); + + suite('fileExistsSync', () => { + test('file exists', async () => { + const filename = 'x/y/z/spam.py'; + deps.setup((d) => d.statSync(filename)) // The file exists. + .returns(() => (undefined as unknown) as FileStat); + + const exists = utils.fileExistsSync(filename); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('file does not exist', async () => { + const filename = 'x/y/z/spam.py'; + const err = vscode.FileSystemError.FileNotFound('...'); + deps.setup((d) => d.statSync(filename)) // The file does not exist. + .throws(err); + + const exists = utils.fileExistsSync(filename); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('fails if low-level call fails', async () => { + const filename = 'x/y/z/spam.py'; + const err = new Error('oops!'); + deps.setup((d) => d.statSync(filename)) // big badda boom + .throws(err); + + expect(() => utils.fileExistsSync(filename)).to.throw(err); + verifyAll(); + }); + }); +}); diff --git a/src/test/common/platform/fs-paths.functional.test.ts b/src/test/common/platform/fs-paths.functional.test.ts new file mode 100644 index 000000000000..3e3925a0ea0c --- /dev/null +++ b/src/test/common/platform/fs-paths.functional.test.ts @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length chai-vague-errors + +import { expect } from 'chai'; +import * as os from 'os'; +import * as path from 'path'; +import { Executables, FileSystemPaths, FileSystemPathUtils } from '../../../client/common/platform/fs-paths'; +import { WINDOWS as IS_WINDOWS } from './utils'; + +suite('FileSystem - Paths', () => { + let paths: FileSystemPaths; + setup(() => { + paths = FileSystemPaths.withDefaults(); + }); + + suite('separator', () => { + test('matches node', () => { + expect(paths.sep).to.be.equal(path.sep); + }); + }); + + suite('dirname', () => { + test('with dirname', () => { + const filename = path.join('spam', 'eggs', 'spam.py'); + const expected = path.join('spam', 'eggs'); + + const basename = paths.dirname(filename); + + expect(basename).to.equal(expected); + }); + + test('without dirname', () => { + const filename = 'spam.py'; + const expected = '.'; + + const basename = paths.dirname(filename); + + expect(basename).to.equal(expected); + }); + }); + + suite('basename', () => { + test('with dirname', () => { + const filename = path.join('spam', 'eggs', 'spam.py'); + const expected = 'spam.py'; + + const basename = paths.basename(filename); + + expect(basename).to.equal(expected); + }); + + test('without dirname', () => { + const filename = 'spam.py'; + const expected = filename; + + const basename = paths.basename(filename); + + expect(basename).to.equal(expected); + }); + }); + + suite('normalize', () => { + test('noop', () => { + const filename = path.join('spam', 'eggs', 'spam.py'); + const expected = filename; + + const norm = paths.normalize(filename); + + expect(norm).to.equal(expected); + }); + + test('pathological', () => { + const filename = path.join(path.sep, 'spam', '..', 'eggs', '.', 'spam.py'); + const expected = path.join(path.sep, 'eggs', 'spam.py'); + + const norm = paths.normalize(filename); + + expect(norm).to.equal(expected); + }); + + test('relative to CWD', () => { + const filename = path.join('..', 'spam', 'eggs', 'spam.py'); + const expected = filename; + + const norm = paths.normalize(filename); + + expect(norm).to.equal(expected); + }); + + test('parent of root fails', () => { + const filename = path.join(path.sep, '..'); + const expected = filename; + + const norm = paths.normalize(filename); + + expect(norm).to.equal(expected); + }); + }); + + suite('join', () => { + test('parts get joined by path.sep', () => { + const expected = path.join('x', 'y', 'z', 'spam.py'); + + const result = paths.join( + 'x', + // Be explicit here to ensure our assumptions are correct + // about the relationship between "sep" and "join()". + path.sep === '\\' ? 'y\\z' : 'y/z', + 'spam.py' + ); + + expect(result).to.equal(expected); + }); + }); + + suite('normCase', () => { + test('forward-slash', () => { + const filename = 'X/Y/Z/SPAM.PY'; + const expected = IS_WINDOWS ? 'X\\Y\\Z\\SPAM.PY' : filename; + + const result = paths.normCase(filename); + + expect(result).to.equal(expected); + }); + + test('backslash is not changed', () => { + const filename = 'X\\Y\\Z\\SPAM.PY'; + const expected = filename; + + const result = paths.normCase(filename); + + expect(result).to.equal(expected); + }); + + test('lower-case', () => { + const filename = 'x\\y\\z\\spam.py'; + const expected = IS_WINDOWS ? 'X\\Y\\Z\\SPAM.PY' : filename; + + const result = paths.normCase(filename); + + expect(result).to.equal(expected); + }); + + test('upper-case stays upper-case', () => { + const filename = 'X\\Y\\Z\\SPAM.PY'; + const expected = 'X\\Y\\Z\\SPAM.PY'; + + const result = paths.normCase(filename); + + expect(result).to.equal(expected); + }); + }); +}); + +suite('FileSystem - Executables', () => { + let execs: Executables; + setup(() => { + execs = Executables.withDefaults(); + }); + + suite('delimiter', () => { + test('matches node', () => { + expect(execs.delimiter).to.be.equal(path.delimiter); + }); + }); + + suite('getPathVariableName', () => { + const expected = IS_WINDOWS ? 'Path' : 'PATH'; + + test('matches platform', () => { + expect(execs.envVar).to.equal(expected); + }); + }); +}); + +suite('FileSystem - Path Utils', () => { + let utils: FileSystemPathUtils; + setup(() => { + utils = FileSystemPathUtils.withDefaults(); + }); + + suite('arePathsSame', () => { + test('identical', () => { + const filename = 'x/y/z/spam.py'; + + const result = utils.arePathsSame(filename, filename); + + expect(result).to.equal(true); + }); + + test('not the same', () => { + const file1 = 'x/y/z/spam.py'; + const file2 = 'a/b/c/spam.py'; + + const result = utils.arePathsSame(file1, file2); + + expect(result).to.equal(false); + }); + + test('with different separators', () => { + const file1 = 'x/y/z/spam.py'; + const file2 = 'x\\y\\z\\spam.py'; + const expected = IS_WINDOWS; + + const result = utils.arePathsSame(file1, file2); + + expect(result).to.equal(expected); + }); + + test('with different case', () => { + const file1 = 'x/y/z/spam.py'; + const file2 = 'x/Y/z/Spam.py'; + const expected = IS_WINDOWS; + + const result = utils.arePathsSame(file1, file2); + + expect(result).to.equal(expected); + }); + }); + + suite('getDisplayName', () => { + const relname = path.join('spam', 'eggs', 'spam.py'); + const cwd = path.resolve(path.sep, 'x', 'y', 'z'); + + test('filename matches CWD', () => { + const filename = path.join(cwd, relname); + const expected = `.${path.sep}${relname}`; + + const display = utils.getDisplayName(filename, cwd); + + expect(display).to.equal(expected); + }); + + test('filename does not match CWD', () => { + const filename = path.resolve(cwd, '..', relname); + const expected = filename; + + const display = utils.getDisplayName(filename, cwd); + + expect(display).to.equal(expected); + }); + + test('filename matches home dir, not cwd', () => { + const filename = path.join(os.homedir(), relname); + const expected = path.join('~', relname); + + const display = utils.getDisplayName(filename, cwd); + + expect(display).to.equal(expected); + }); + + test('filename matches home dir', () => { + const filename = path.join(os.homedir(), relname); + const expected = path.join('~', relname); + + const display = utils.getDisplayName(filename); + + expect(display).to.equal(expected); + }); + + test('filename does not match home dir', () => { + const filename = relname; + const expected = filename; + + const display = utils.getDisplayName(filename); + + expect(display).to.equal(expected); + }); + }); +}); diff --git a/src/test/common/platform/fs-paths.unit.test.ts b/src/test/common/platform/fs-paths.unit.test.ts new file mode 100644 index 000000000000..bfd588efdd49 --- /dev/null +++ b/src/test/common/platform/fs-paths.unit.test.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { FileSystemPathUtils } from '../../../client/common/platform/fs-paths'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { OSType } from '../../../client/common/utils/platform'; + +interface IUtilsDeps { + // executables + delimiter: string; + envVar: string; + // paths + readonly sep: string; + join(...filenames: string[]): string; + dirname(filename: string): string; + basename(filename: string, suffix?: string): string; + normalize(filename: string): string; + normCase(filename: string): string; + // node "path" + relative(relpath: string, rootpath: string): string; +} + +suite('FileSystem - Path Utils', () => { + let deps: TypeMoq.IMock<IUtilsDeps>; + let utils: FileSystemPathUtils; + setup(() => { + deps = TypeMoq.Mock.ofType<IUtilsDeps>(undefined, TypeMoq.MockBehavior.Strict); + utils = new FileSystemPathUtils( + 'my-home', + // It's simpler to just use one mock for all 3 dependencies. + deps.object, + deps.object, + deps.object + ); + }); + function verifyAll() { + deps.verifyAll(); + } + + suite('path-related', () => { + const caseInsensitive = [OSType.Windows]; + + suite('arePathsSame', () => { + getNamesAndValues<OSType>(OSType).forEach((item) => { + const osType = item.value; + + function setNormCase(filename: string, numCalls = 1): string { + let norm = filename; + if (osType === OSType.Windows) { + norm = path.normalize(filename).toUpperCase(); + } + deps.setup((d) => d.normCase(filename)) + .returns(() => norm) + .verifiable(TypeMoq.Times.exactly(numCalls)); + return filename; + } + + [ + // no upper-case + 'c:\\users\\peter smith\\my documents\\test.txt', + // some upper-case + 'c:\\USERS\\Peter Smith\\my documents\\test.TXT' + ].forEach((path1) => { + test(`True if paths are identical (type: ${item.name}) - ${path1}`, () => { + path1 = setNormCase(path1, 2); + + const areSame = utils.arePathsSame(path1, path1); + + expect(areSame).to.be.equal(true, 'file paths do not match'); + verifyAll(); + }); + }); + + test(`False if paths are completely different (type: ${item.name})`, () => { + const path1 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.txt'); + const path2 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.exe'); + + const areSame = utils.arePathsSame(path1, path2); + + expect(areSame).to.be.equal(false, 'file paths do not match'); + verifyAll(); + }); + + if (caseInsensitive.includes(osType)) { + test(`True if paths only differ by case (type: ${item.name})`, () => { + const path1 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.txt'); + const path2 = setNormCase('c:\\USERS\\Peter Smith\\my documents\\test.TXT'); + + const areSame = utils.arePathsSame(path1, path2); + + expect(areSame).to.be.equal(true, 'file paths match'); + verifyAll(); + }); + } else { + test(`False if paths only differ by case (type: ${item.name})`, () => { + const path1 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.txt'); + const path2 = setNormCase('c:\\USERS\\Peter Smith\\my documents\\test.TXT'); + + const areSame = utils.arePathsSame(path1, path2); + + expect(areSame).to.be.equal(false, 'file paths do not match'); + verifyAll(); + }); + } + + // Missing tests: + // * exercize normalization + }); + }); + }); +}); diff --git a/src/test/common/platform/fs-temp.functional.test.ts b/src/test/common/platform/fs-temp.functional.test.ts new file mode 100644 index 000000000000..e6338b604462 --- /dev/null +++ b/src/test/common/platform/fs-temp.functional.test.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length chai-vague-errors + +import { expect, use } from 'chai'; +import * as fs from 'fs-extra'; +import { TemporaryFileSystem } from '../../../client/common/platform/fs-temp'; +import { TemporaryFile } from '../../../client/common/platform/types'; +import { assertDoesNotExist, assertExists, FSFixture, WINDOWS } from './utils'; + +// tslint:disable:no-require-imports no-var-requires +const assertArrays = require('chai-arrays'); +use(require('chai-as-promised')); +use(assertArrays); + +suite('FileSystem - TemporaryFileSystem', () => { + let tmpfs: TemporaryFileSystem; + let fix: FSFixture; + setup(async () => { + tmpfs = TemporaryFileSystem.withDefaults(); + fix = new FSFixture(); + }); + teardown(async () => { + await fix.cleanUp(); + }); + + suite('createFile', () => { + async function createFile(suffix: string): Promise<TemporaryFile> { + const tempfile = await tmpfs.createFile(suffix); + fix.addFSCleanup(tempfile.filePath, tempfile.dispose); + return tempfile; + } + + test('TemporaryFile is created properly', async () => { + const tempfile = await tmpfs.createFile('.tmp'); + fix.addFSCleanup(tempfile.filePath, tempfile.dispose); + await assertExists(tempfile.filePath); + + expect(tempfile.filePath.endsWith('.tmp')).to.equal(true, `bad suffix on ${tempfile.filePath}`); + }); + + test('TemporaryFile is disposed properly', async () => { + const tempfile = await createFile('.tmp'); + await assertExists(tempfile.filePath); + + tempfile.dispose(); + + await assertDoesNotExist(tempfile.filePath); + }); + + test('Ensure creating a temporary file results in a unique temp file path', async () => { + const tempFile = await createFile('.tmp'); + const tempFile2 = await createFile('.tmp'); + + const filename1 = tempFile.filePath; + const filename2 = tempFile2.filePath; + + expect(filename1).to.not.equal(filename2); + }); + + test('Ensure writing to a temp file is supported via file stream', async function () { + if (WINDOWS) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const tempfile = await createFile('.tmp'); + const stream = fs.createWriteStream(tempfile.filePath); + fix.addCleanup(() => stream.destroy()); + const data = '...'; + + stream.write(data, 'utf8'); + + const actual = await fs.readFile(tempfile.filePath, 'utf8'); + expect(actual).to.equal(data); + }); + + test('Ensure chmod works against a temporary file', async () => { + // Note that on Windows chmod is a noop. + const tempfile = await createFile('.tmp'); + + const promise = fs.chmod(tempfile.filePath, '7777'); + + await expect(promise).to.not.eventually.be.rejected; + }); + }); +}); diff --git a/src/test/common/platform/fs-temp.unit.test.ts b/src/test/common/platform/fs-temp.unit.test.ts new file mode 100644 index 000000000000..62d5d8932498 --- /dev/null +++ b/src/test/common/platform/fs-temp.unit.test.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { TemporaryFileSystem } from '../../../client/common/platform/fs-temp'; + +interface IDeps { + // tmp module + file( + config: { postfix?: string; mode?: number }, + // tslint:disable-next-line:no-any + callback?: (err: any, path: string, fd: number, cleanupCallback: () => void) => void + ): void; +} + +suite('FileSystem - temp files', () => { + let deps: TypeMoq.IMock<IDeps>; + let temp: TemporaryFileSystem; + setup(() => { + deps = TypeMoq.Mock.ofType<IDeps>(undefined, TypeMoq.MockBehavior.Strict); + temp = new TemporaryFileSystem(deps.object); + }); + function verifyAll() { + deps.verifyAll(); + } + + suite('createFile', () => { + test(`fails if the raw call fails`, async () => { + const failure = new Error('oops'); + deps.setup((d) => d.file({ postfix: '.tmp', mode: undefined }, TypeMoq.It.isAny())) + // fail with an arbitrary error + .throws(failure); + + const promise = temp.createFile('.tmp'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test(`fails if the raw call "returns" an error`, async () => { + const failure = new Error('oops'); + deps.setup((d) => d.file({ postfix: '.tmp', mode: undefined }, TypeMoq.It.isAny())) + // tslint:disable-next-line:no-empty + .callback((_cfg, cb) => cb(failure, '...', -1, () => {})); + + const promise = temp.createFile('.tmp'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); +}); diff --git a/src/test/common/platform/pathUtils.functional.test.ts b/src/test/common/platform/pathUtils.functional.test.ts new file mode 100644 index 000000000000..7a7cc9b88e92 --- /dev/null +++ b/src/test/common/platform/pathUtils.functional.test.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import { FileSystemPathUtils } from '../../../client/common/platform/fs-paths'; +import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { WINDOWS as IS_WINDOWS } from './utils'; + +suite('FileSystem - PathUtils', () => { + let utils: PathUtils; + let wrapped: FileSystemPathUtils; + setup(() => { + utils = new PathUtils(IS_WINDOWS); + wrapped = FileSystemPathUtils.withDefaults(); + }); + + suite('home', () => { + test('matches wrapped object', () => { + const expected = wrapped.home; + + expect(utils.home).to.equal(expected); + }); + }); + + suite('delimiter', () => { + test('matches wrapped object', () => { + const expected = wrapped.executables.delimiter; + + expect(utils.delimiter).to.be.equal(expected); + }); + }); + + suite('separator', () => { + test('matches wrapped object', () => { + const expected = wrapped.paths.sep; + + expect(utils.separator).to.be.equal(expected); + }); + }); + + suite('getPathVariableName', () => { + test('matches wrapped object', () => { + const expected = wrapped.executables.envVar; + + const envVar = utils.getPathVariableName(); + + expect(envVar).to.equal(expected); + }); + }); + + suite('getDisplayName', () => { + test('matches wrapped object', () => { + const filename = 'spam.py'; + const expected = wrapped.getDisplayName(filename); + + const display = utils.getDisplayName(filename); + + expect(display).to.equal(expected); + }); + }); + + suite('basename', () => { + test('matches wrapped object', () => { + const filename = 'spam.py'; + const expected = wrapped.paths.basename(filename); + + const basename = utils.basename(filename); + + expect(basename).to.equal(expected); + }); + }); +}); diff --git a/src/test/common/platform/platformService.test.ts b/src/test/common/platform/platformService.test.ts new file mode 100644 index 000000000000..0f803e38095e --- /dev/null +++ b/src/test/common/platform/platformService.test.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as os from 'os'; +import { parse } from 'semver'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { OSType } from '../../../client/common/utils/platform'; + +use(chaiAsPromised); + +// tslint:disable-next-line:max-func-body-length +suite('PlatformService', () => { + const osType = getOSType(); + test('pathVariableName', async () => { + const expected = osType === OSType.Windows ? 'Path' : 'PATH'; + const svc = new PlatformService(); + const result = svc.pathVariableName; + + expect(result).to.be.equal(expected, 'invalid value'); + }); + + test('virtualEnvBinName - Windows', async () => { + const expected = osType === OSType.Windows ? 'Scripts' : 'bin'; + const svc = new PlatformService(); + const result = svc.virtualEnvBinName; + + expect(result).to.be.equal(expected, 'invalid value'); + }); + + test('isWindows', async () => { + const expected = osType === OSType.Windows; + const svc = new PlatformService(); + const result = svc.isWindows; + + expect(result).to.be.equal(expected, 'invalid value'); + }); + + test('isMac', async () => { + const expected = osType === OSType.OSX; + const svc = new PlatformService(); + const result = svc.isMac; + + expect(result).to.be.equal(expected, 'invalid value'); + }); + + test('isLinux', async () => { + const expected = osType === OSType.Linux; + const svc = new PlatformService(); + const result = svc.isLinux; + + expect(result).to.be.equal(expected, 'invalid value'); + }); + + test('osRelease', async () => { + const expected = os.release(); + const svc = new PlatformService(); + const result = svc.osRelease; + + expect(result).to.be.equal(expected, 'invalid value'); + }); + + test('is64bit', async () => { + // tslint:disable-next-line:no-require-imports + const arch = require('arch'); + + const hostReports64Bit = arch() === 'x64'; + const svc = new PlatformService(); + const result = svc.is64bit; + + expect(result).to.be.equal( + hostReports64Bit, + `arch() reports '${arch()}', PlatformService.is64bit reports ${result}.` + ); + }); + + test('getVersion on Mac/Windows', async function () { + if (osType === OSType.Linux) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + const expectedVersion = parse(os.release())!; + const svc = new PlatformService(); + const result = await svc.getVersion(); + + expect(result.compare(expectedVersion)).to.be.equal(0, 'invalid value'); + }); + test('getVersion on Linux shoud throw an exception', async function () { + if (osType !== OSType.Linux) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + const svc = new PlatformService(); + + await expect(svc.getVersion()).to.eventually.be.rejectedWith('Not Supported'); + }); +}); + +function getOSType(platform: string = process.platform): OSType { + if (/^win/.test(platform)) { + return OSType.Windows; + } else if (/^darwin/.test(platform)) { + return OSType.OSX; + } else if (/^linux/.test(platform)) { + return OSType.Linux; + } else { + return OSType.Unknown; + } +} diff --git a/src/test/common/platform/serviceRegistry.unit.test.ts b/src/test/common/platform/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..109a633e0489 --- /dev/null +++ b/src/test/common/platform/serviceRegistry.unit.test.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { RegistryImplementation } from '../../../client/common/platform/registry'; +import { registerTypes } from '../../../client/common/platform/serviceRegistry'; +import { IFileSystem, IPlatformService, IRegistry } from '../../../client/common/platform/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Common Platform Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify(serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService)).once(); + verify(serviceManager.addSingleton<IFileSystem>(IFileSystem, FileSystem)).once(); + verify(serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation)).once(); + }); +}); diff --git a/src/test/common/platform/utils.ts b/src/test/common/platform/utils.ts new file mode 100644 index 000000000000..89d98e61274f --- /dev/null +++ b/src/test/common/platform/utils.ts @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-console + +import { expect } from 'chai'; +import * as fsextra from 'fs-extra'; +import * as net from 'net'; +import * as path from 'path'; +import * as tmpMod from 'tmp'; +import { CleanupFixture } from '../../fixtures'; + +// Note: all functional tests that trigger the VS Code "fs" API are +// found in filesystem.test.ts. + +export const WINDOWS = /^win/.test(process.platform); +export const OSX = /^darwin/.test(process.platform); + +export const SUPPORTS_SYMLINKS = (() => { + const source = fsextra.readdirSync('.')[0]; + const symlink = `${source}.symlink`; + try { + fsextra.symlinkSync(source, symlink); + } catch { + return false; + } + fsextra.unlinkSync(symlink); + return true; +})(); +export const SUPPORTS_SOCKETS = (() => { + if (WINDOWS) { + // Windows requires named pipes to have a specific path under + // the local domain ("\\.\pipe\*"). This makes them relatively + // useless in our functional tests, where we want to use them + // to exercise FileType.Unknown. + return false; + } + const tmp = tmpMod.dirSync({ + prefix: 'pyvsc-test-', + unsafeCleanup: true // for non-empty dir + }); + const filename = path.join(tmp.name, 'test.sock'); + try { + const srv = net.createServer(); + try { + srv.listen(filename); + } finally { + srv.close(); + } + } catch { + return false; + } finally { + tmp.removeCallback(); + } + return true; +})(); + +export const DOES_NOT_EXIST = 'this file does not exist'; + +export async function assertDoesNotExist(filename: string) { + const promise = fsextra.stat(filename); + await expect(promise).to.eventually.be.rejected; +} + +export async function assertExists(filename: string) { + const promise = fsextra.stat(filename); + await expect(promise).to.not.eventually.be.rejected; +} + +export async function assertFileText(filename: string, expected: string): Promise<string> { + const data = await fsextra.readFile(filename); + const text = data.toString(); + expect(text).to.equal(expected); + return text; +} + +export function fixPath(filename: string): string { + return path.normalize(filename); +} + +export class SystemError extends Error { + public code: string; + public errno: number; + public syscall: string; + public info?: string; + public path?: string; + public address?: string; + public dest?: string; + public port?: string; + constructor(code: string, syscall: string, message: string) { + super(`${code}: ${message} ${syscall} '...'`); + this.code = code; + this.errno = 0; // Don't bother until we actually need it. + this.syscall = syscall; + } +} + +export class FSFixture extends CleanupFixture { + private tempDir: string | undefined; + private sockServer: net.Server | undefined; + + public addFSCleanup(filename: string, dispose?: () => void) { + this.addCleanup(() => this.ensureDeleted(filename, dispose)); + } + + public async resolve(relname: string, mkdirs = true): Promise<string> { + const tempDir = this.ensureTempDir(); + relname = path.normalize(relname); + const filename = path.join(tempDir, relname); + if (mkdirs) { + const dirname = path.dirname(filename); + await fsextra.mkdirp(dirname); + } + return filename; + } + + public async createFile(relname: string, text = ''): Promise<string> { + const filename = await this.resolve(relname); + await fsextra.writeFile(filename, text); + return filename; + } + + public async createDirectory(relname: string): Promise<string> { + const dirname = await this.resolve(relname); + await fsextra.mkdir(dirname); + return dirname; + } + + public async createSymlink(relname: string, source: string): Promise<string> { + if (!SUPPORTS_SYMLINKS) { + throw Error('this platform does not support symlinks'); + } + const symlink = await this.resolve(relname); + // We cannot use fsextra.ensureSymlink() because it requires + // that "source" exist. + await fsextra.symlink(source, symlink); + return symlink; + } + + public async createSocket(relname: string): Promise<string> { + const srv = this.ensureSocketServer(); + const filename = await this.resolve(relname); + await new Promise((resolve) => srv!.listen(filename, 0, resolve)); + return filename; + } + + public async ensureDeleted(filename: string, dispose?: () => void) { + if (dispose) { + try { + dispose(); + return; // Trust that dispose() did what it's supposed to. + } catch (err) { + // For temp directories, the "unsafeCleanup: true" + // option of the "tmp" module is supposed to support + // a non-empty directory, but apparently that isn't + // always the case. + // (see #8804) + if (!(await fsextra.pathExists(filename))) { + return; + } + console.log(`failure during dispose() for ${filename}: ${err}`); + console.log('...manually deleting'); + // Fall back to fsextra. + } + } + + try { + await fsextra.remove(filename); + } catch (err) { + console.log(`failure while deleting ${filename}: ${err}`); + } + } + + private ensureTempDir(): string { + if (this.tempDir) { + return this.tempDir; + } + + const tempDir = tmpMod.dirSync({ + prefix: 'pyvsc-fs-tests-', + unsafeCleanup: true + }); + this.tempDir = tempDir.name; + + this.addFSCleanup(tempDir.name, async () => { + if (!this.tempDir) { + return; + } + this.tempDir = undefined; + + await this.ensureDeleted(tempDir.name, tempDir.removeCallback); + //try { + // tempDir.removeCallback(); + //} catch { + // // The "unsafeCleanup: true" option is supposed + // // to support a non-empty directory, but apparently + // // that isn't always the case. (see #8804) + // await fsextra.remove(tempDir.name); + //} + }); + return tempDir.name; + } + + private ensureSocketServer(): net.Server { + if (this.sockServer) { + return this.sockServer; + } + + const srv = net.createServer(); + this.sockServer = srv; + this.addCleanup(async () => { + try { + await new Promise((resolve) => srv.close(resolve)); + } catch (err) { + console.log(`failure while closing socket server: ${err}`); + } + }); + return srv; + } +} diff --git a/src/test/common/process/currentProcess.test.ts b/src/test/common/process/currentProcess.test.ts new file mode 100644 index 000000000000..d37077d05a03 --- /dev/null +++ b/src/test/common/process/currentProcess.test.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { CurrentProcess } from '../../../client/common/process/currentProcess'; +import { ICurrentProcess } from '../../../client/common/types'; + +suite('Current Process', () => { + let currentProcess: ICurrentProcess; + setup(() => { + currentProcess = new CurrentProcess(); + }); + + test('Current process argv is returned', () => { + expect(currentProcess.argv).to.deep.equal(process.argv); + }); + + test('Current process env is returned', () => { + expect(currentProcess.env).to.deep.equal(process.env); + }); + + test('Current process stdin is returned', () => { + expect(currentProcess.stdin).to.deep.equal(process.stdin); + }); + + test('Current process stdout is returned', () => { + expect(currentProcess.stdout).to.deep.equal(process.stdout); + }); +}); diff --git a/src/test/common/process/decoder.test.ts b/src/test/common/process/decoder.test.ts new file mode 100644 index 000000000000..92d43f8aadf4 --- /dev/null +++ b/src/test/common/process/decoder.test.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { encode, encodingExists } from 'iconv-lite'; +import { BufferDecoder } from '../../../client/common/process/decoder'; +import { initialize } from './../../initialize'; + +suite('Decoder', () => { + setup(initialize); + teardown(initialize); + + test('Test decoding utf8 strings', () => { + const value = 'Sample input string Сделать это'; + const buffer = encode(value, 'utf8'); + const decoder = new BufferDecoder(); + const decodedValue = decoder.decode([buffer]); + expect(decodedValue).equal(value, 'Decoded string is incorrect'); + }); + + test('Test decoding cp932 strings', function () { + if (!encodingExists('cp866')) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + const value = 'Sample input string Сделать это'; + const buffer = encode(value, 'cp866'); + const decoder = new BufferDecoder(); + let decodedValue = decoder.decode([buffer]); + expect(decodedValue).not.equal(value, 'Decoded string is the same'); + + decodedValue = decoder.decode([buffer], 'cp866'); + expect(decodedValue).equal(value, 'Decoded string is incorrect'); + }); +}); diff --git a/src/test/common/process/execFactory.test.ts b/src/test/common/process/execFactory.test.ts new file mode 100644 index 000000000000..d1d0400c55c3 --- /dev/null +++ b/src/test/common/process/execFactory.test.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { InterpreterVersionService } from '../../../client/interpreter/interpreterVersion'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('PythonExecutableService', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let configService: TypeMoq.IMock<IConfigurationService>; + let procService: TypeMoq.IMock<IProcessService>; + let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + const envVarsProvider = TypeMoq.Mock.ofType<IEnvironmentVariablesProvider>(); + procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + procService = TypeMoq.Mock.ofType<IProcessService>(); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + const fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + fileSystem.setup((f) => f.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider))) + .returns(() => envVarsProvider.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory))) + .returns(() => procServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + procService.setup((x: any) => x.then).returns(() => undefined); + procServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(procService.object)); + envVarsProvider.setup((v) => v.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); + + const envActivationService = TypeMoq.Mock.ofType<IEnvironmentActivationService>(); + envActivationService + .setup((e) => e.getActivatedEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + envActivationService + .setup((e) => e.getActivatedEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + envActivationService + .setup((e) => + e.getActivatedEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(undefined)); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IEnvironmentActivationService), TypeMoq.It.isAny())) + .returns(() => envActivationService.object); + }); + test('Ensure resource is used when getting configuration service settings (undefined resource)', async () => { + const pythonPath = `Python_Path_${new Date().toString()}`; + const pythonVersion = `Python_Version_${new Date().toString()}`; + const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + pythonSettings.setup((p) => p.pythonPath).returns(() => pythonPath); + configService.setup((c) => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => pythonSettings.object); + procService + .setup((p) => p.exec(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: pythonVersion })); + + const versionService = new InterpreterVersionService(procServiceFactory.object); + const version = await versionService.getVersion(pythonPath, ''); + + expect(version).to.be.equal(pythonVersion); + }); + test('Ensure resource is used when getting configuration service settings (defined resource)', async () => { + const resource = Uri.file('abc'); + const pythonPath = `Python_Path_${new Date().toString()}`; + const pythonVersion = `Python_Version_${new Date().toString()}`; + const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + pythonSettings.setup((p) => p.pythonPath).returns(() => pythonPath); + configService.setup((c) => c.getSettings(TypeMoq.It.isValue(resource))).returns(() => pythonSettings.object); + procService + .setup((p) => p.exec(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: pythonVersion })); + + const versionService = new InterpreterVersionService(procServiceFactory.object); + const version = await versionService.getVersion(pythonPath, ''); + + expect(version).to.be.equal(pythonVersion); + }); +}); diff --git a/src/test/common/process/logger.unit.test.ts b/src/test/common/process/logger.unit.test.ts new file mode 100644 index 000000000000..d6c0706c57c7 --- /dev/null +++ b/src/test/common/process/logger.unit.test.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +// tslint:disable-next-line:no-require-imports +import untildify = require('untildify'); + +import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { ProcessLogger } from '../../../client/common/process/logger'; +import { IOutputChannel } from '../../../client/common/types'; +import { Logging } from '../../../client/common/utils/localize'; +import { getOSType, OSType } from '../../common'; + +// tslint:disable: max-func-body-length +suite('ProcessLogger suite', () => { + let outputChannel: TypeMoq.IMock<IOutputChannel>; + let pathUtils: PathUtils; + let outputResult: string; + + suiteSetup(() => { + outputChannel = TypeMoq.Mock.ofType<IOutputChannel>(); + pathUtils = new PathUtils(getOSType() === OSType.Windows); + }); + + setup(() => { + outputResult = ''; + outputChannel + .setup((o) => o.appendLine(TypeMoq.It.isAnyString())) + .returns((s: string) => (outputResult += `${s}\n`)); + }); + + teardown(() => { + outputChannel.reset(); + }); + + test('Logger displays the process command, arguments and current working directory in the output channel', async () => { + const options = { cwd: path.join('debug', 'path') }; + const logger = new ProcessLogger(outputChannel.object, pathUtils); + logger.logProcess('test', ['--foo', '--bar'], options); + + const expectedResult = `> test --foo --bar\n${Logging.currentWorkingDirectory()} ${options.cwd}\n`; + expect(outputResult).to.equal(expectedResult, 'Output string is incorrect - String built incorrectly'); + + outputChannel.verify((o) => o.appendLine(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(2)); + }); + + test('Logger adds quotes around arguments if they contain spaces', async () => { + const options = { cwd: path.join('debug', 'path') }; + const logger = new ProcessLogger(outputChannel.object, pathUtils); + logger.logProcess('test', ['--foo', '--bar', 'import test'], options); + + const expectedResult = `> test --foo --bar "import test"\n${Logging.currentWorkingDirectory()} ${path.join( + 'debug', + 'path' + )}\n`; + expect(outputResult).to.equal(expectedResult, 'Output string is incorrect: Home directory is not tildified'); + }); + + test('Logger preserves quotes around arguments if they contain spaces', async () => { + const options = { cwd: path.join('debug', 'path') }; + const logger = new ProcessLogger(outputChannel.object, pathUtils); + logger.logProcess('test', ['--foo', '--bar', "'import test'"], options); + + const expectedResult = `> test --foo --bar \'import test\'\n${Logging.currentWorkingDirectory()} ${path.join( + 'debug', + 'path' + )}\n`; + expect(outputResult).to.equal(expectedResult, 'Output string is incorrect: Home directory is not tildified'); + }); + + test('Logger replaces the path/to/home with ~ in the current working directory', async () => { + const options = { cwd: path.join(untildify('~'), 'debug', 'path') }; + const logger = new ProcessLogger(outputChannel.object, pathUtils); + logger.logProcess('test', ['--foo', '--bar'], options); + + const expectedResult = `> test --foo --bar\n${Logging.currentWorkingDirectory()} ${path.join( + '~', + 'debug', + 'path' + )}\n`; + expect(outputResult).to.equal(expectedResult, 'Output string is incorrect: Home directory is not tildified'); + }); + + test('Logger replaces the path/to/home with ~ in the command path', async () => { + const options = { cwd: path.join('debug', 'path') }; + const logger = new ProcessLogger(outputChannel.object, pathUtils); + logger.logProcess(path.join(untildify('~'), 'test'), ['--foo', '--bar'], options); + + const expectedResult = `> ${path.join('~', 'test')} --foo --bar\n${Logging.currentWorkingDirectory()} ${ + options.cwd + }\n`; + expect(outputResult).to.equal(expectedResult, 'Output string is incorrect: Home directory is not tildified'); + }); + + test("Logger doesn't display the working directory line if there is no options parameter", async () => { + const logger = new ProcessLogger(outputChannel.object, pathUtils); + logger.logProcess(path.join(untildify('~'), 'test'), ['--foo', '--bar']); + + const expectedResult = `> ${path.join('~', 'test')} --foo --bar\n`; + expect(outputResult).to.equal( + expectedResult, + 'Output string is incorrect: Working directory line should not be displayed' + ); + }); + + test("Logger doesn't display the working directory line if there is no cwd key in the options parameter", async () => { + const options = {}; + const logger = new ProcessLogger(outputChannel.object, pathUtils); + logger.logProcess(path.join(untildify('~'), 'test'), ['--foo', '--bar'], options); + + const expectedResult = `> ${path.join('~', 'test')} --foo --bar\n`; + expect(outputResult).to.equal( + expectedResult, + 'Output string is incorrect: Working directory line should not be displayed' + ); + }); +}); diff --git a/src/test/common/process/proc.exec.test.ts b/src/test/common/process/proc.exec.test.ts new file mode 100644 index 000000000000..816a6db55ac4 --- /dev/null +++ b/src/test/common/process/proc.exec.test.ts @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { CancellationTokenSource } from 'vscode'; +import { BufferDecoder } from '../../../client/common/process/decoder'; +import { ProcessService } from '../../../client/common/process/proc'; +import { StdErrError } from '../../../client/common/process/types'; +import { OSType } from '../../../client/common/utils/platform'; +import { getExtensionSettings, isOs, isPythonVersion } from '../../common'; +import { initialize } from './../../initialize'; + +use(chaiAsPromised); + +// tslint:disable-next-line:max-func-body-length +suite('ProcessService Observable', () => { + let pythonPath: string; + suiteSetup(() => { + pythonPath = getExtensionSettings(undefined).pythonPath; + return initialize(); + }); + setup(initialize); + teardown(initialize); + + test('exec should output print statements', async () => { + const procService = new ProcessService(new BufferDecoder()); + const printOutput = '1234'; + const result = await procService.exec(pythonPath, ['-c', `print("${printOutput}")`]); + + expect(result).not.to.be.an('undefined', 'result is undefined'); + expect(result.stdout.trim()).to.be.equal(printOutput, 'Invalid output'); + expect(result.stderr).to.equal(undefined, 'stderr not undefined'); + }); + + test('exec should output print unicode characters', async function () { + // This test has not been working for many months in Python 2.7 under + // Windows. Tracked by #2546. (unicode under Py2.7 is tough!) + if (isOs(OSType.Windows) && (await isPythonVersion('2.7'))) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + + const procService = new ProcessService(new BufferDecoder()); + const printOutput = 'öä'; + const result = await procService.exec(pythonPath, ['-c', `print("${printOutput}")`]); + + expect(result).not.to.be.an('undefined', 'result is undefined'); + expect(result.stdout.trim()).to.be.equal(printOutput, 'Invalid output'); + expect(result.stderr).to.equal(undefined, 'stderr not undefined'); + }); + + test('exec should wait for completion of program with new lines', async function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(5000); + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'print("3")' + ]; + const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')]); + const outputs = ['1', '2', '3']; + + expect(result).not.to.be.an('undefined', 'result is undefined'); + const values = result.stdout + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); + expect(values).to.deep.equal(outputs, 'Output values are incorrect'); + expect(result.stderr).to.equal(undefined, 'stderr not undefined'); + }); + + test('exec should wait for completion of program without new lines', async function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(5000); + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = [ + 'import sys', + 'import time', + 'sys.stdout.write("1")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stdout.write("2")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stdout.write("3")' + ]; + const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')]); + const outputs = ['123']; + + expect(result).not.to.be.an('undefined', 'result is undefined'); + const values = result.stdout + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); + expect(values).to.deep.equal(outputs, 'Output values are incorrect'); + expect(result.stderr).to.equal(undefined, 'stderr not undefined'); + }); + + test('exec should end when cancellationToken is cancelled', async function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(15000); + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(10)', + 'print("2")', + 'sys.stdout.flush()' + ]; + const cancellationToken = new CancellationTokenSource(); + setTimeout(() => cancellationToken.cancel(), 3000); + + const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')], { + token: cancellationToken.token + }); + + expect(result).not.to.be.an('undefined', 'result is undefined'); + const values = result.stdout + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); + expect(values).to.deep.equal(['1'], 'Output values are incorrect'); + expect(result.stderr).to.equal(undefined, 'stderr not undefined'); + }); + + test('exec should stream stdout and stderr separately', async function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(7000); + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("a")', + 'sys.stderr.flush()', + 'time.sleep(1)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("b")', + 'sys.stderr.flush()', + 'time.sleep(1)', + 'print("3")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("c")', + 'sys.stderr.flush()' + ]; + const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')]); + const expectedStdout = ['1', '2', '3']; + const expectedStderr = ['abc']; + + expect(result).not.to.be.an('undefined', 'result is undefined'); + const stdouts = result.stdout + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); + expect(stdouts).to.deep.equal(expectedStdout, 'stdout values are incorrect'); + const stderrs = result + .stderr!.split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); + expect(stderrs).to.deep.equal(expectedStderr, 'stderr values are incorrect'); + }); + + test('exec should merge stdout and stderr streams', async function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(7000); + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = [ + 'import sys', + 'import time', + 'sys.stdout.write("1")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("a")', + 'sys.stderr.flush()', + 'time.sleep(1)', + 'sys.stdout.write("2")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("b")', + 'sys.stderr.flush()', + 'time.sleep(1)', + 'sys.stdout.write("3")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("c")', + 'sys.stderr.flush()' + ]; + const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')], { mergeStdOutErr: true }); + const expectedOutput = ['1a2b3c']; + + expect(result).not.to.be.an('undefined', 'result is undefined'); + const outputs = result.stdout + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); + expect(outputs).to.deep.equal(expectedOutput, 'Output values are incorrect'); + }); + + test('exec should throw an error with stderr output', async () => { + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = ['import sys', 'sys.stderr.write("a")', 'sys.stderr.flush()']; + const result = procService.exec(pythonPath, ['-c', pythonCode.join(';')], { throwOnStdErr: true }); + + await expect(result).to.eventually.be.rejectedWith(StdErrError, 'a', 'Expected error to be thrown'); + }); + + test('exec should throw an error when spawn file not found', async () => { + const procService = new ProcessService(new BufferDecoder()); + const result = procService.exec(Date.now().toString(), []); + + await expect(result).to.eventually.be.rejected.and.to.have.property('code', 'ENOENT', 'Invalid error code'); + }); + + test('exec should exit without no output', async () => { + const procService = new ProcessService(new BufferDecoder()); + const result = await procService.exec(pythonPath, ['-c', 'import sys', 'sys.exit()']); + + expect(result.stdout).equals('', 'stdout is invalid'); + expect(result.stderr).equals(undefined, 'stderr is invalid'); + }); + test('shellExec should be able to run python too', async () => { + const procService = new ProcessService(new BufferDecoder()); + const printOutput = '1234'; + const result = await procService.shellExec(`"${pythonPath}" -c "print('${printOutput}')"`); + + expect(result).not.to.be.an('undefined', 'result is undefined'); + expect(result.stderr).to.equal(undefined, 'stderr not empty'); + expect(result.stdout.trim()).to.be.equal(printOutput, 'Invalid output'); + }); + test('shellExec should fail on invalid command', async () => { + const procService = new ProcessService(new BufferDecoder()); + const result = procService.shellExec('invalid command'); + await expect(result).to.eventually.be.rejectedWith(Error, 'a', 'Expected error to be thrown'); + }); + test('variables can be changed after the fact', async () => { + const procService = new ProcessService(new BufferDecoder(), process.env); + let result = await procService.exec(pythonPath, ['-c', `import os;print(os.environ.get("MY_TEST_VARIABLE"))`], { + extraVariables: { MY_TEST_VARIABLE: 'foo' } + }); + + expect(result).not.to.be.an('undefined', 'result is undefined'); + expect(result.stdout.trim()).to.be.equal('foo', 'Invalid output'); + expect(result.stderr).to.equal(undefined, 'stderr not undefined'); + + result = await procService.exec(pythonPath, ['-c', `import os;print(os.environ.get("MY_TEST_VARIABLE"))`]); + expect(result).not.to.be.an('undefined', 'result is undefined'); + expect(result.stdout.trim()).to.be.equal('None', 'Invalid output'); + expect(result.stderr).to.equal(undefined, 'stderr not undefined'); + }); +}); diff --git a/src/test/common/process/proc.observable.test.ts b/src/test/common/process/proc.observable.test.ts new file mode 100644 index 000000000000..8ebfdec3148b --- /dev/null +++ b/src/test/common/process/proc.observable.test.ts @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { CancellationTokenSource } from 'vscode'; + +import { BufferDecoder } from '../../../client/common/process/decoder'; +import { ProcessService } from '../../../client/common/process/proc'; +import { createDeferred } from '../../../client/common/utils/async'; +import { getExtensionSettings, isOs, OSType } from '../../common'; +import { initialize } from './../../initialize'; + +use(chaiAsPromised); + +// tslint:disable-next-line:max-func-body-length +suite('ProcessService', () => { + let pythonPath: string; + suiteSetup(() => { + pythonPath = getExtensionSettings(undefined).pythonPath; + return initialize(); + }); + setup(initialize); + teardown(initialize); + + test('execObservable should stream output with new lines', function (done) { + // tslint:disable-next-line:no-invalid-this + this.timeout(10000); + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'print("3")', + 'sys.stdout.flush()', + 'time.sleep(2)' + ]; + const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')]); + const outputs = ['1', '2', '3']; + + expect(result).not.to.be.an('undefined', 'result is undefined'); + result.out.subscribe( + (output) => { + // Ignore line breaks. + if (output.out.trim().length === 0) { + return; + } + const expectedValue = outputs.shift(); + if (expectedValue !== output.out.trim() && expectedValue === output.out) { + done(`Received value ${output.out} is not same as the expectd value ${expectedValue}`); + } + if (output.source !== 'stdout') { + done(`Source is not stdout. Value received is ${output.source}`); + } + }, + done, + done + ); + }); + + test('execObservable should stream output without new lines', function (done) { + // Skipping to get nightly build to pass. Opened this issue: + // https://github.com/microsoft/vscode-python/issues/7411 + // tslint:disable-next-line: no-invalid-this + this.skip(); + + // tslint:disable-next-line:no-invalid-this + this.timeout(10000); + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = [ + 'import sys', + 'import time', + 'sys.stdout.write("1")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'sys.stdout.write("2")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'sys.stdout.write("3")', + 'sys.stdout.flush()', + 'time.sleep(2)' + ]; + const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')]); + const outputs = ['1', '2', '3']; + + expect(result).not.to.be.an('undefined', 'result is undefined'); + result.out.subscribe( + (output) => { + // Ignore line breaks. + if (output.out.trim().length === 0) { + return; + } + const expectedValue = outputs.shift(); + if (expectedValue !== output.out) { + done(`Received value ${output.out} is not same as the expectd value ${expectedValue}`); + } + if (output.source !== 'stdout') { + done(`Source is not stdout. Value received is ${output.source}`); + } + }, + done, + done + ); + }); + + test('execObservable should end when cancellationToken is cancelled', function (done) { + // tslint:disable-next-line:no-invalid-this + this.timeout(15000); + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(10)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(2)' + ]; + const cancellationToken = new CancellationTokenSource(); + const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { + token: cancellationToken.token + }); + + const def = createDeferred(); + def.promise.then(done).catch(done); + expect(result).not.to.be.an('undefined', 'result is undefined'); + result.out.subscribe( + (output) => { + const value = output.out.trim(); + if (value === '1') { + cancellationToken.cancel(); + } else { + if (!def.completed) { + def.reject("Output received when we shouldn't have."); + } + } + }, + done, + () => { + if (def.completed) { + return; + } + if (cancellationToken.token.isCancellationRequested) { + def.resolve(); + } else { + def.reject('Program terminated even before cancelling it.'); + } + } + ); + }); + + test('execObservable should end when process is killed', function (done) { + // tslint:disable-next-line:no-invalid-this + this.timeout(15000); + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(10)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(2)' + ]; + const cancellationToken = new CancellationTokenSource(); + const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { + token: cancellationToken.token + }); + let procKilled = false; + + expect(result).not.to.be.an('undefined', 'result is undefined'); + result.out.subscribe( + (output) => { + const value = output.out.trim(); + // Ignore line breaks. + if (value.length === 0) { + return; + } + if (value === '1') { + procKilled = true; + if (result.proc) { + result.proc.kill(); + } + } else { + done("Output received when we shouldn't have."); + } + }, + done, + () => { + const errorMsg = procKilled ? undefined : 'Program terminated even before killing it.'; + done(errorMsg); + } + ); + }); + + test('execObservable should stream stdout and stderr separately', function (done) { + // tslint:disable-next-line:no-invalid-this + this.timeout(20000); + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'sys.stderr.write("a")', + 'sys.stderr.flush()', + 'time.sleep(2)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'sys.stderr.write("b")', + 'sys.stderr.flush()', + 'time.sleep(2)', + 'print("3")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'sys.stderr.write("c")', + 'sys.stderr.flush()', + 'time.sleep(2)' + ]; + const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')]); + const outputs = [ + { out: '1', source: 'stdout' }, + { out: 'a', source: 'stderr' }, + { out: '2', source: 'stdout' }, + { out: 'b', source: 'stderr' }, + { out: '3', source: 'stdout' }, + { out: 'c', source: 'stderr' } + ]; + + expect(result).not.to.be.an('undefined', 'result is undefined'); + result.out.subscribe( + (output) => { + const value = output.out.trim(); + // Ignore line breaks. + if (value.length === 0) { + return; + } + const expectedOutput = outputs.shift()!; + + expect(value).to.be.equal(expectedOutput.out, 'Expected output is incorrect'); + expect(output.source).to.be.equal(expectedOutput.source, 'Expected sopurce is incorrect'); + }, + done, + done + ); + }); + + test('execObservable should send stdout and stderr streams separately', async function () { + // This test is failing on Windows. Tracked by GH #4755. + if (isOs(OSType.Windows)) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + }); + + test('execObservable should throw an error with stderr output', (done) => { + const procService = new ProcessService(new BufferDecoder()); + const pythonCode = ['import sys', 'sys.stderr.write("a")', 'sys.stderr.flush()']; + const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { throwOnStdErr: true }); + + expect(result).not.to.be.an('undefined', 'result is undefined.'); + result.out.subscribe( + (_output) => { + done("Output received, when we're expecting an error to be thrown."); + }, + (ex: Error) => { + expect(ex).to.have.property('message', 'a', 'Invalid error thrown'); + done(); + }, + () => { + done("Completed, when we're expecting an error to be thrown."); + } + ); + }); + + test('execObservable should throw an error when spawn file not found', (done) => { + const procService = new ProcessService(new BufferDecoder()); + const result = procService.execObservable(Date.now().toString(), []); + + expect(result).not.to.be.an('undefined', 'result is undefined.'); + result.out.subscribe( + (_output) => { + done("Output received, when we're expecting an error to be thrown."); + }, + (ex) => { + expect(ex).to.have.property('code', 'ENOENT', 'Invalid error code'); + done(); + }, + () => { + done("Completed, when we're expecting an error to be thrown."); + } + ); + }); + + test('execObservable should exit without no output', (done) => { + const procService = new ProcessService(new BufferDecoder()); + const result = procService.execObservable(pythonPath, ['-c', 'import sys', 'sys.exit()']); + + expect(result).not.to.be.an('undefined', 'result is undefined.'); + result.out.subscribe( + (output) => { + done(`Output received, when we\'re not expecting any, ${JSON.stringify(output)}`); + }, + done, + done + ); + }); +}); diff --git a/src/test/common/process/proc.unit.test.ts b/src/test/common/process/proc.unit.test.ts new file mode 100644 index 000000000000..0f1f8fa1dc3a --- /dev/null +++ b/src/test/common/process/proc.unit.test.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length no-invalid-this max-classes-per-file + +import { expect } from 'chai'; +import { ChildProcess, spawn } from 'child_process'; +import { ProcessService } from '../../../client/common/process/proc'; +import { createDeferred, Deferred } from '../../../client/common/utils/async'; +import { PYTHON_PATH } from '../../common'; + +interface IProcData { + proc: ChildProcess; + exited: Deferred<Boolean>; +} + +suite('Process - Process Service', function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(5000); + const procsToKill: IProcData[] = []; + teardown(() => { + procsToKill.forEach((p) => { + if (!p.exited.resolved) { + p.proc.kill(); + } + }); + }); + + function spawnProc(): IProcData { + const proc = spawn(PYTHON_PATH, ['-c', 'while(True): import time;time.sleep(0.5);print(1)']); + const exited = createDeferred<Boolean>(); + proc.on('exit', () => exited.resolve(true)); + procsToKill.push({ proc, exited }); + + return procsToKill[procsToKill.length - 1]; + } + + test('Process is killed', async () => { + const proc = spawnProc(); + + ProcessService.kill(proc.proc.pid); + + expect(await proc.exited.promise).to.equal(true, 'process did not die'); + }); + test('Process is alive', async () => { + const proc = spawnProc(); + + expect(ProcessService.isAlive(proc.proc.pid)).to.equal(true, 'process is not alive'); + }); +}); diff --git a/src/test/common/process/processFactory.unit.test.ts b/src/test/common/process/processFactory.unit.test.ts new file mode 100644 index 000000000000..5c124ebab808 --- /dev/null +++ b/src/test/common/process/processFactory.unit.test.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { expect } from 'chai'; +import { instance, mock, verify, when } from 'ts-mockito'; +import { Disposable, Uri } from 'vscode'; + +import { BufferDecoder } from '../../../client/common/process/decoder'; +import { ProcessLogger } from '../../../client/common/process/logger'; +import { ProcessService } from '../../../client/common/process/proc'; +import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; +import { IBufferDecoder, IProcessLogger } from '../../../client/common/process/types'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; + +suite('Process - ProcessServiceFactory', () => { + let factory: ProcessServiceFactory; + let envVariablesProvider: IEnvironmentVariablesProvider; + let bufferDecoder: IBufferDecoder; + let processLogger: IProcessLogger; + let processService: ProcessService; + let disposableRegistry: IDisposableRegistry; + + setup(() => { + bufferDecoder = mock(BufferDecoder); + envVariablesProvider = mock(EnvironmentVariablesProvider); + processLogger = mock(ProcessLogger); + when(processLogger.logProcess('', [], {})).thenReturn(); + processService = mock(ProcessService); + when( + processService.on('exec', () => { + return; + }) + ).thenReturn(processService); + disposableRegistry = []; + factory = new ProcessServiceFactory( + instance(envVariablesProvider), + instance(processLogger), + instance(bufferDecoder), + disposableRegistry + ); + }); + + teardown(() => { + (disposableRegistry as Disposable[]).forEach((d) => d.dispose()); + }); + + [Uri.parse('test'), undefined].forEach((resource) => { + test(`Ensure ProcessService is created with an ${resource ? 'existing' : 'undefined'} resource`, async () => { + when(envVariablesProvider.getEnvironmentVariables(resource)).thenResolve({ x: 'test' }); + + const proc = await factory.create(resource); + verify(envVariablesProvider.getEnvironmentVariables(resource)).once(); + + const disposables = disposableRegistry as Disposable[]; + expect(disposables.length).equal(1); + expect(proc).instanceOf(ProcessService); + }); + }); +}); diff --git a/src/test/common/process/pythonDaemon.functional.test.ts b/src/test/common/process/pythonDaemon.functional.test.ts new file mode 100644 index 000000000000..f7ac134e0c08 --- /dev/null +++ b/src/test/common/process/pythonDaemon.functional.test.ts @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect, use } from 'chai'; +import * as chaiPromised from 'chai-as-promised'; +import { ChildProcess, spawn, spawnSync } from 'child_process'; +import * as dedent from 'dedent'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import { + createMessageConnection, + MessageConnection, + RequestType, + StreamMessageReader, + StreamMessageWriter +} from 'vscode-jsonrpc/node'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { PythonDaemonExecutionService } from '../../../client/common/process/pythonDaemon'; +import { IPythonExecutionService } from '../../../client/common/process/types'; +import { IDisposable } from '../../../client/common/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { PythonVersionInfo } from '../../../client/pythonEnvironments/info'; +import { parsePythonVersion } from '../../../client/pythonEnvironments/info/pythonVersion'; +import { isPythonVersion, PYTHON_PATH } from '../../common'; +import { createTemporaryFile } from '../../utils/fs'; +use(chaiPromised); + +// tslint:disable-next-line: max-func-body-length +suite('Daemon', () => { + // Set PYTHONPATH to pickup our module and the jsonrpc modules. + const envPythonPath = `${path.join(EXTENSION_ROOT_DIR, 'pythonFiles')}${path.delimiter}${path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'lib', + 'python' + )}`; + const env = { PYTHONPATH: envPythonPath, PYTHONUNBUFFERED: '1' }; + let pythonProc: ChildProcess; + let connection: MessageConnection; + let fullyQualifiedPythonPath: string = PYTHON_PATH; + let pythonDaemon: PythonDaemonExecutionService; + let pythonExecutionService: IPythonExecutionService; + let platformService: IPlatformService; + let disposables: IDisposable[] = []; + suiteSetup(() => { + // When running locally. + if (PYTHON_PATH.toLowerCase() === 'python') { + fullyQualifiedPythonPath = spawnSync(PYTHON_PATH, ['-c', 'import sys;print(sys.executable)']) + .stdout.toString() + .trim(); + } + }); + setup(async function () { + if (isPythonVersion('2.7')) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + // Enable the following to log everything going on at pyton end. + // pythonProc = spawn(fullyQualifiedPythonPath, ['-m', 'vscode_datascience_helpers.daemon', '-v', `--log-file=${path.join(EXTENSION_ROOT_DIR, 'test.log')}`], { env }); + pythonProc = spawn(fullyQualifiedPythonPath, ['-m', 'vscode_datascience_helpers.daemon'], { env }); + connection = createMessageConnection( + new StreamMessageReader(pythonProc.stdout), + new StreamMessageWriter(pythonProc.stdin) + ); + connection.listen(); + pythonExecutionService = mock<IPythonExecutionService>(); + platformService = mock<IPlatformService>(); + pythonDaemon = new PythonDaemonExecutionService( + instance(pythonExecutionService), + instance(platformService), + fullyQualifiedPythonPath, + pythonProc, + connection + ); + }); + teardown(() => { + pythonProc?.kill(); + if (connection) { + connection.dispose(); + } + pythonDaemon?.dispose(); + disposables.forEach((item) => item.dispose()); + disposables = []; + }); + + async function createPythonFile(source: string): Promise<string> { + const tmpFile = await createTemporaryFile('.py'); + disposables.push({ dispose: () => tmpFile.cleanupCallback() }); + await fs.writeFile(tmpFile.filePath, source, { encoding: 'utf8' }); + return tmpFile.filePath; + } + + test('Ping', async () => { + const data = 'Hello World'; + const request = new RequestType<{ data: string }, { pong: string }, void, void>('ping'); + const result = await connection.sendRequest(request, { data }); + assert.equal(result.pong, data); + }); + + test('Ping with Unicode', async () => { + const data = 'Hello World-₹-😄'; + const request = new RequestType<{ data: string }, { pong: string }, void, void>('ping'); + const result = await connection.sendRequest(request, { data }); + assert.equal(result.pong, data); + }); + + test('Interpreter Information', async () => { + type InterpreterInfo = { + versionInfo: PythonVersionInfo; + sysPrefix: string; + sysVersion: string; + is64Bit: boolean; + }; + const json: InterpreterInfo = JSON.parse( + spawnSync(fullyQualifiedPythonPath, [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'interpreterInfo.py')]) + .stdout.toString() + .trim() + ); + const versionValue = + json.versionInfo.length === 4 + ? `${json.versionInfo.slice(0, 3).join('.')}-${json.versionInfo[3]}` + : json.versionInfo.join('.'); + const expectedVersion = { + architecture: json.is64Bit ? Architecture.x64 : Architecture.x86, + path: fullyQualifiedPythonPath, + version: parsePythonVersion(versionValue), + sysVersion: json.sysVersion, + sysPrefix: json.sysPrefix + }; + + const version = await pythonDaemon.getInterpreterInformation(); + + assert.deepEqual(version, expectedVersion); + }); + + test('Executable path', async () => { + const execPath = await pythonDaemon.getExecutablePath(); + + assert.deepEqual(execPath, fullyQualifiedPythonPath); + }); + + async function testModuleInstalled(moduleName: string, expectedToBeInstalled: boolean) { + await assert.eventually.equal(pythonDaemon.isModuleInstalled(moduleName), expectedToBeInstalled); + } + + test("'pip' module is installed", async () => testModuleInstalled('pip', true)); + test("'unittest' module is installed", async () => testModuleInstalled('unittest', true)); + test("'VSCode-Python-Rocks' module is not Installed", async () => + testModuleInstalled('VSCode-Python-Rocks', false)); + + test('Execute a file and capture stdout (with unicode)', async () => { + const source = dedent` + import sys + sys.stdout.write("HELLO WORLD-₹-😄") + `; + const fileToExecute = await createPythonFile(source); + const output = await pythonDaemon.exec([fileToExecute], {}); + + assert.isUndefined(output.stderr); + assert.deepEqual(output.stdout, 'HELLO WORLD-₹-😄'); + }); + + test('Execute a file and capture stderr (with unicode)', async () => { + const source = dedent` + import sys + sys.stderr.write("HELLO WORLD-₹-😄") + `; + const fileToExecute = await createPythonFile(source); + const output = await pythonDaemon.exec([fileToExecute], {}); + + assert.isUndefined(output.stdout); + assert.deepEqual(output.stderr, 'HELLO WORLD-₹-😄'); + }); + + test('Execute a file with arguments', async () => { + const source = dedent` + import sys + sys.stdout.write(sys.argv[1]) + `; + const fileToExecute = await createPythonFile(source); + const output = await pythonDaemon.exec([fileToExecute, 'HELLO WORLD'], {}); + + assert.isUndefined(output.stderr); + assert.equal(output.stdout, 'HELLO WORLD'); + }); + + test('Execute a file with custom cwd', async () => { + const source = dedent` + import os + print(os.getcwd()) + `; + const fileToExecute = await createPythonFile(source); + const output1 = await pythonDaemon.exec([fileToExecute, 'HELLO WORLD'], { cwd: EXTENSION_ROOT_DIR }); + + assert.isUndefined(output1.stderr); + assert.equal(output1.stdout.trim(), EXTENSION_ROOT_DIR); + + const output2 = await pythonDaemon.exec([fileToExecute, 'HELLO WORLD'], { cwd: __dirname }); + + assert.isUndefined(output2.stderr); + assert.equal(output2.stdout.trim(), __dirname); + }); + + test('Execute a file and capture stdout & stderr', async () => { + const source = dedent` + import sys + sys.stdout.write("HELLO WORLD-₹-😄") + sys.stderr.write("FOO BAR-₹-😄") + `; + const fileToExecute = await createPythonFile(source); + const output = await pythonDaemon.exec([fileToExecute, 'HELLO WORLD'], {}); + + assert.equal(output.stdout, 'HELLO WORLD-₹-😄'); + assert.equal(output.stderr, 'FOO BAR-₹-😄'); + }); + + test('Execute a file and handle error', async () => { + const source = dedent` + import sys + raise Exception("KABOOM") + `; + const fileToExecute = await createPythonFile(source); + const promise = pythonDaemon.exec([fileToExecute], {}); + await expect(promise).to.eventually.be.rejectedWith('KABOOM'); + }); + + test('Execute a file with custom env variable', async () => { + const source = dedent` + import os + print(os.getenv("VSC_HELLO_CUSTOM", "NONE")) + `; + const fileToExecute = await createPythonFile(source); + + const output1 = await pythonDaemon.exec([fileToExecute], {}); + + // Confirm there's no custom variable. + assert.equal(output1.stdout.trim(), 'NONE'); + + // Confirm setting the varible works. + const output2 = await pythonDaemon.exec([fileToExecute], { env: { VSC_HELLO_CUSTOM: 'wow' } }); + assert.equal(output2.stdout.trim(), 'wow'); + }); + + test('Execute simple module', async () => { + const pipVersion = spawnSync(fullyQualifiedPythonPath, ['-c', 'import pip;print(pip.__version__)']) + .stdout.toString() + .trim(); + + const output = await pythonDaemon.execModule('pip', ['--version'], {}); + + assert.isUndefined(output.stderr); + assert.equal(output.stdout.trim(), pipVersion); + }); + + test('Execute a file and stream output', async () => { + const source = dedent` + import sys + import time + for i in range(5): + print(i) + time.sleep(1) + `; + const fileToExecute = await createPythonFile(source); + const output = pythonDaemon.execObservable([fileToExecute], {}); + const outputsReceived: string[] = []; + await new Promise((resolve, reject) => { + output.out.subscribe((out) => outputsReceived.push(out.out.trim()), reject, resolve); + }); + assert.deepEqual( + outputsReceived.filter((item) => item.length > 0), + ['0', '1', '2', '3', '4'] + ); + }).timeout(10_000); + + test('Execute a file and throw exception if stderr is not empty', async () => { + const fileToExecute = await createPythonFile(['import sys', 'sys.stderr.write("KABOOM")'].join(os.EOL)); + const promise = pythonDaemon.exec([fileToExecute], { throwOnStdErr: true }); + await expect(promise).to.eventually.be.rejectedWith('KABOOM'); + }); + + test('Execute a file and throw exception if stderr is not empty when streaming output', async () => { + const source = dedent` + import sys + import time + time.sleep(1) + sys.stderr.write("KABOOM") + sys.stderr.flush() + time.sleep(1) + `; + const fileToExecute = await createPythonFile(source); + const output = pythonDaemon.execObservable([fileToExecute], { throwOnStdErr: true }); + const outputsReceived: string[] = []; + const promise = new Promise((resolve, reject) => { + output.out.subscribe((out) => outputsReceived.push(out.out.trim()), reject, resolve); + }); + await expect(promise).to.eventually.be.rejectedWith('KABOOM'); + }).timeout(3_000); +}); diff --git a/src/test/common/process/pythonDaemonPool.functional.test.ts b/src/test/common/process/pythonDaemonPool.functional.test.ts new file mode 100644 index 000000000000..a0eb331178b5 --- /dev/null +++ b/src/test/common/process/pythonDaemonPool.functional.test.ts @@ -0,0 +1,472 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect, use } from 'chai'; +import * as chaiPromised from 'chai-as-promised'; +import { spawn, spawnSync } from 'child_process'; +import * as dedent from 'dedent'; +import { EventEmitter } from 'events'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import * as sinon from 'sinon'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { ProcessLogger } from '../../../client/common/process/logger'; +import { PythonDaemonExecutionServicePool } from '../../../client/common/process/pythonDaemonPool'; +import { + IProcessLogger, + IPythonDaemonExecutionService, + IPythonExecutionService, + ObservableExecutionResult, + Output +} from '../../../client/common/process/types'; +import { IDisposable } from '../../../client/common/types'; +import { sleep } from '../../../client/common/utils/async'; +import { noop } from '../../../client/common/utils/misc'; +import { Architecture } from '../../../client/common/utils/platform'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { JupyterDaemonModule } from '../../../client/datascience/constants'; +import { PythonVersionInfo } from '../../../client/pythonEnvironments/info'; +import { parsePythonVersion } from '../../../client/pythonEnvironments/info/pythonVersion'; +import { isPythonVersion, PYTHON_PATH, waitForCondition } from '../../common'; +import { createTemporaryFile } from '../../utils/fs'; +use(chaiPromised); + +// tslint:disable: max-func-body-length +suite('Daemon - Python Daemon Pool', () => { + // Set PYTHONPATH to pickup our module and the jsonrpc modules. + const envPythonPath = `${path.join(EXTENSION_ROOT_DIR, 'pythonFiles')}${path.delimiter}${path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'lib', + 'python' + )}`; + const env = { PYTHONPATH: envPythonPath, PYTHONUNBUFFERED: '1' }; + let fullyQualifiedPythonPath: string = PYTHON_PATH; + let pythonDaemonPool: PythonDaemonExecutionServicePool; + let pythonExecutionService: IPythonExecutionService; + let platformService: IPlatformService; + let disposables: IDisposable[] = []; + let createDaemonServicesSpy: sinon.SinonSpy<[], Promise<IPythonDaemonExecutionService | IDisposable>>; + let logger: IProcessLogger; + class DaemonPool extends PythonDaemonExecutionServicePool { + // tslint:disable-next-line: no-unnecessary-override + public createDaemonService<T extends IPythonDaemonExecutionService | IDisposable>(): Promise<T> { + return super.createDaemonService(); + } + } + suiteSetup(() => { + // When running locally. + if (PYTHON_PATH.toLowerCase() === 'python') { + fullyQualifiedPythonPath = spawnSync(PYTHON_PATH, ['-c', 'import sys;print(sys.executable)']) + .stdout.toString() + .trim(); + } + }); + setup(async function () { + if (isPythonVersion('2.7')) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + logger = mock(ProcessLogger); + createDaemonServicesSpy = sinon.spy(DaemonPool.prototype, 'createDaemonService'); + pythonExecutionService = mock<IPythonExecutionService>(); + platformService = mock<IPlatformService>(); + when( + pythonExecutionService.execModuleObservable('vscode_datascience_helpers.daemon', anything(), anything()) + ).thenCall(() => { + const pythonProc = spawn(fullyQualifiedPythonPath, ['-m', 'vscode_datascience_helpers.daemon'], { env }); + const connection = createMessageConnection( + new StreamMessageReader(pythonProc.stdout), + new StreamMessageWriter(pythonProc.stdin) + ); + connection.listen(); + disposables.push({ dispose: () => pythonProc.kill() }); + disposables.push({ dispose: () => connection.dispose() }); + // tslint:disable-next-line: no-any + return { proc: pythonProc, dispose: noop, out: undefined as any }; + }); + const options = { + pythonPath: fullyQualifiedPythonPath, + daemonModule: JupyterDaemonModule, + daemonCount: 2, + observableDaemonCount: 1 + }; + pythonDaemonPool = new DaemonPool( + logger, + [], + options, + instance(pythonExecutionService), + instance(platformService), + {}, + 100 + ); + await pythonDaemonPool.initialize(); + disposables.push(pythonDaemonPool); + }); + teardown(() => { + sinon.restore(); + disposables.forEach((item) => item.dispose()); + disposables = []; + }); + async function getStdOutFromObservable(output: ObservableExecutionResult<string>) { + return new Promise<string>((resolve, reject) => { + const data: string[] = []; + output.out.subscribe( + (out) => data.push(out.out.trim()), + reject, + () => resolve(data.join('')) + ); + }); + } + + async function createPythonFile(source: string): Promise<string> { + const tmpFile = await createTemporaryFile('.py'); + disposables.push({ dispose: () => tmpFile.cleanupCallback() }); + await fs.writeFile(tmpFile.filePath, source, { encoding: 'utf8' }); + return tmpFile.filePath; + } + + test('Interpreter Information', async () => { + type InterpreterInfo = { + versionInfo: PythonVersionInfo; + sysPrefix: string; + sysVersion: string; + is64Bit: boolean; + }; + const json: InterpreterInfo = JSON.parse( + spawnSync(fullyQualifiedPythonPath, [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'interpreterInfo.py')]) + .stdout.toString() + .trim() + ); + const versionValue = + json.versionInfo.length === 4 + ? `${json.versionInfo.slice(0, 3).join('.')}-${json.versionInfo[3]}` + : json.versionInfo.join('.'); + const expectedVersion = { + architecture: json.is64Bit ? Architecture.x64 : Architecture.x86, + path: fullyQualifiedPythonPath, + version: parsePythonVersion(versionValue), + sysVersion: json.sysVersion, + sysPrefix: json.sysPrefix + }; + + const version = await pythonDaemonPool.getInterpreterInformation(); + + assert.deepEqual(version, expectedVersion); + }); + + test('Executable path', async () => { + const execPath = await pythonDaemonPool.getExecutablePath(); + + assert.deepEqual(execPath, fullyQualifiedPythonPath); + }); + + async function testModuleInstalled(moduleName: string, expectedToBeInstalled: boolean) { + await assert.eventually.equal(pythonDaemonPool.isModuleInstalled(moduleName), expectedToBeInstalled); + } + + test("'pip' module is installed", async () => testModuleInstalled('pip', true)); + test("'unittest' module is installed", async () => testModuleInstalled('unittest', true)); + test("'VSCode-Python-Rocks' module is not Installed", async () => + testModuleInstalled('VSCode-Python-Rocks', false)); + + test('Execute a file and capture stdout (with unicode)', async () => { + const source = dedent` + import sys + sys.stdout.write("HELLO WORLD-₹-😄") + `; + const fileToExecute = await createPythonFile(source); + const output = await pythonDaemonPool.exec([fileToExecute], {}); + + assert.isUndefined(output.stderr); + assert.deepEqual(output.stdout, 'HELLO WORLD-₹-😄'); + }); + + test('Execute a file and capture stderr (with unicode)', async () => { + const source = dedent` + import sys + sys.stderr.write("HELLO WORLD-₹-😄") + `; + const fileToExecute = await createPythonFile(source); + const output = await pythonDaemonPool.exec([fileToExecute], {}); + + assert.isUndefined(output.stdout); + assert.deepEqual(output.stderr, 'HELLO WORLD-₹-😄'); + }); + + test('Execute a file with arguments', async () => { + const source = dedent` + import sys + sys.stdout.write(sys.argv[1]) + `; + const fileToExecute = await createPythonFile(source); + const output = await pythonDaemonPool.exec([fileToExecute, 'HELLO WORLD'], {}); + + assert.isUndefined(output.stderr); + assert.equal(output.stdout, 'HELLO WORLD'); + }); + + test('Execute a file with custom cwd', async () => { + const source = dedent` + import os + print(os.getcwd()) + `; + const fileToExecute = await createPythonFile(source); + const output1 = await pythonDaemonPool.exec([fileToExecute, 'HELLO WORLD'], { cwd: EXTENSION_ROOT_DIR }); + + assert.isUndefined(output1.stderr); + assert.equal(output1.stdout.trim(), EXTENSION_ROOT_DIR); + + const output2 = await pythonDaemonPool.exec([fileToExecute, 'HELLO WORLD'], { cwd: __dirname }); + + assert.isUndefined(output2.stderr); + assert.equal(output2.stdout.trim(), __dirname); + }); + + test('Execute a file and capture stdout & stderr', async () => { + const source = dedent` + import sys + sys.stdout.write("HELLO WORLD-₹-😄") + sys.stderr.write("FOO BAR-₹-😄") + `; + const fileToExecute = await createPythonFile(source); + const output = await pythonDaemonPool.exec([fileToExecute, 'HELLO WORLD'], {}); + + assert.equal(output.stdout, 'HELLO WORLD-₹-😄'); + assert.equal(output.stderr, 'FOO BAR-₹-😄'); + }); + + test('Execute a file and handle error', async () => { + const source = dedent` + import sys + raise Exception("KABOOM") + `; + const fileToExecute = await createPythonFile(source); + const promise = pythonDaemonPool.exec([fileToExecute], {}); + await expect(promise).to.eventually.be.rejectedWith('KABOOM'); + }); + + test('Execute a file with custom env variable', async () => { + const source = dedent` + import os + print(os.getenv("VSC_HELLO_CUSTOM", "NONE")) + `; + const fileToExecute = await createPythonFile(source); + + const output1 = await pythonDaemonPool.exec([fileToExecute], {}); + + // Confirm there's no custom variable. + assert.equal(output1.stdout.trim(), 'NONE'); + + // Confirm setting the varible works. + const output2 = await pythonDaemonPool.exec([fileToExecute], { env: { VSC_HELLO_CUSTOM: 'wow' } }); + assert.equal(output2.stdout.trim(), 'wow'); + }); + + test('Execute simple module', async () => { + const pipVersion = spawnSync(fullyQualifiedPythonPath, ['-c', 'import pip;print(pip.__version__)']) + .stdout.toString() + .trim(); + + const output = await pythonDaemonPool.execModule('pip', ['--version'], {}); + + assert.isUndefined(output.stderr); + assert.equal(output.stdout.trim(), pipVersion); + }); + + test('Execute a file and stream output', async () => { + const source = dedent` + import sys + import time + for i in range(5): + print(i) + time.sleep(0.1) + `; + const fileToExecute = await createPythonFile(source); + const output = pythonDaemonPool.execObservable([fileToExecute], {}); + const outputsReceived: string[] = []; + await new Promise((resolve, reject) => { + output.out.subscribe((out) => outputsReceived.push(out.out.trim()), reject, resolve); + }); + assert.deepEqual( + outputsReceived.filter((item) => item.length > 0), + ['0', '1', '2', '3', '4'] + ); + }).timeout(5_000); + + test('Execute a file and throw exception if stderr is not empty', async () => { + const fileToExecute = await createPythonFile(['import sys', 'sys.stderr.write("KABOOM")'].join(os.EOL)); + const promise = pythonDaemonPool.exec([fileToExecute], { throwOnStdErr: true }); + await expect(promise).to.eventually.be.rejectedWith('KABOOM'); + }); + + test('Execute a file and throw exception if stderr is not empty when streaming output', async () => { + const source = dedent` + import sys + import time + time.sleep(0.1) + sys.stderr.write("KABOOM") + sys.stderr.flush() + time.sleep(0.1) + `; + const fileToExecute = await createPythonFile(source); + const output = pythonDaemonPool.execObservable([fileToExecute], { throwOnStdErr: true }); + const outputsReceived: string[] = []; + const promise = new Promise((resolve, reject) => { + output.out.subscribe((out) => outputsReceived.push(out.out.trim()), reject, resolve); + }); + await expect(promise).to.eventually.be.rejectedWith('KABOOM'); + }).timeout(5_000); + test('If executing a file takes time, then ensure we use another daemon', async () => { + const source = dedent` + import os + import time + time.sleep(0.2) + print(os.getpid()) + `; + const fileToExecute = await createPythonFile(source); + // When using the python execution service, return a bogus value. + when(pythonExecutionService.execObservable(deepEqual([fileToExecute]), anything())).thenCall(() => { + const observable = new Observable<Output<string>>((s) => { + s.next({ out: 'mypid', source: 'stdout' }); + s.complete(); + }); + // tslint:disable-next-line: no-any + return { proc: new EventEmitter() as any, dispose: noop, out: observable }; + }); + // This will use a damon. + const output1 = pythonDaemonPool.execObservable([fileToExecute], {}); + // These two will use a python execution service. + const output2 = pythonDaemonPool.execObservable([fileToExecute], {}); + const output3 = pythonDaemonPool.execObservable([fileToExecute], {}); + const [result1, result2, result3] = await Promise.all([ + getStdOutFromObservable(output1), + getStdOutFromObservable(output2), + getStdOutFromObservable(output3) + ]); + + // Two process ids are used to run the code (one process for a daemon, another for bogus puthon process). + expect(result1).to.not.equal('mypid'); + expect(result2).to.equal('mypid'); + expect(result3).to.equal('mypid'); + verify(pythonExecutionService.execObservable(deepEqual([fileToExecute]), anything())).twice(); + }).timeout(3_000); + test('Ensure to re-use the same daemon & it goes back into the pool (for observables)', async () => { + const source = dedent` + import os + print(os.getpid()) + `; + const fileToExecute = await createPythonFile(source); + // This will use a damon. + const output1 = await getStdOutFromObservable(pythonDaemonPool.execObservable([fileToExecute], {})); + // Wait for daemon to go into the pool. + await sleep(100); + // This will use a damon. + const output2 = await getStdOutFromObservable(pythonDaemonPool.execObservable([fileToExecute], {})); + // Wait for daemon to go into the pool. + await sleep(100); + // This will use a damon. + const output3 = await getStdOutFromObservable(pythonDaemonPool.execObservable([fileToExecute], {})); + + // The pid for all processes is the same. + // This means we're re-using the same daemon (process). + expect(output1).to.equal(output2); + expect(output1).to.equal(output3); + }).timeout(3_000); + test('Ensure two different daemons are used to execute code', async () => { + const source = dedent` + import os + import time + time.sleep(0.2) + print(os.getpid()) + `; + const fileToExecute = await createPythonFile(source); + + const [output1, output2] = await Promise.all([ + pythonDaemonPool.exec([fileToExecute], {}), + pythonDaemonPool.exec([fileToExecute], {}) + ]); + + // The pid for both processes will be different. + // This means we're running both in two separate daemons. + expect(output1.stdout).to.not.equal(output2.stdout); + }); + test('Ensure to create a new daemon if one dies', async () => { + // Get pids of the 2 daemons. + const daemonsCreated = createDaemonServicesSpy.callCount; + const source1 = dedent` + import os + import time + time.sleep(0.1) + print(os.getpid()) + `; + const fileToExecute1 = await createPythonFile(source1); + + let [pid1, pid2] = await Promise.all([ + pythonDaemonPool.exec([fileToExecute1], {}).then((out) => out.stdout.trim()), + pythonDaemonPool.exec([fileToExecute1], {}).then((out) => out.stdout.trim()) + ]); + + const processesUsedToRunCode = new Set<string>(); + processesUsedToRunCode.add(pid1); + processesUsedToRunCode.add(pid2); + + // We should have two distinct process ids, that was used to run our code. + expect(processesUsedToRunCode.size).to.equal(2); + + // Ok, wait for daemons to go back into the pool. + await sleep(1); + + // Kill one of the daemons (let it die while running some code). + const source2 = dedent` + import os + os.kill(os.getpid(), 1) + `; + const fileToExecute2 = await createPythonFile(source2); + [pid1, pid2] = await Promise.all([ + pythonDaemonPool + .exec([fileToExecute1], {}) + .then((out) => out.stdout.trim()) + .catch(() => 'FAILED'), + pythonDaemonPool + .exec([fileToExecute2], {}) + .then((out) => out.stdout.trim()) + .catch(() => 'FAILED') + ]); + + // Confirm that one of the executions failed due to an error. + expect(pid1 === 'FAILED' ? pid1 : pid2).to.equal('FAILED'); + // Keep track of the process that worked. + processesUsedToRunCode.add(pid1 === 'FAILED' ? pid2 : pid1); + // We should still have two distinct process ids (one of the eralier processes died). + expect(processesUsedToRunCode.size).to.equal(2); + + // Wait for a new daemon to be created. + await waitForCondition( + async () => createDaemonServicesSpy.callCount - daemonsCreated === 1, + 5_000, + 'Failed to create a new daemon' + ); + + // Confirm we have two daemons by checking the Pids again. + // One of them will be new. + [pid1, pid2] = await Promise.all([ + pythonDaemonPool.exec([fileToExecute1], {}).then((out) => out.stdout.trim()), + pythonDaemonPool.exec([fileToExecute1], {}).then((out) => out.stdout.trim()) + ]); + + // Keep track of the pids. + processesUsedToRunCode.add(pid1); + processesUsedToRunCode.add(pid2); + + // Confirm we have a total of three process ids (for 3 daemons). + // 2 for earlier, then one died and a new one was created. + expect(processesUsedToRunCode.size).to.be.greaterThan(2); + }).timeout(10_000); +}); diff --git a/src/test/common/process/pythonDaemonPool.unit.test.ts b/src/test/common/process/pythonDaemonPool.unit.test.ts new file mode 100644 index 000000000000..faa29f075530 --- /dev/null +++ b/src/test/common/process/pythonDaemonPool.unit.test.ts @@ -0,0 +1,313 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fakeTimers from '@sinonjs/fake-timers'; +import { expect, use } from 'chai'; +import * as chaiPromised from 'chai-as-promised'; +import { ChildProcess } from 'child_process'; +import { EventEmitter } from 'events'; +import { Observable } from 'rxjs/Observable'; +import * as sinon from 'sinon'; +import { anything, instance, mock, reset, verify, when } from 'ts-mockito'; +import { MessageConnection } from 'vscode-jsonrpc'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { ProcessLogger } from '../../../client/common/process/logger'; +import { PythonDaemonExecutionService } from '../../../client/common/process/pythonDaemon'; +import { PythonDaemonExecutionServicePool } from '../../../client/common/process/pythonDaemonPool'; +import { IProcessLogger, IPythonExecutionService, Output } from '../../../client/common/process/types'; +import { sleep } from '../../../client/common/utils/async'; +import { InterpreterInformation } from '../../../client/pythonEnvironments/info'; +import { noop } from '../../core'; +import { asyncDump } from '../asyncDump'; +use(chaiPromised); + +// tslint:disable: no-any max-func-body-length +suite('Daemon - Python Daemon Pool', () => { + class DaemonPool extends PythonDaemonExecutionServicePool { + // tslint:disable-next-line: no-unnecessary-override + public createConnection(proc: ChildProcess) { + return super.createConnection(proc); + } + } + // tslint:disable-next-line: no-any use-default-type-parameter + let sendRequestStub: sinon.SinonStub<any[], any>; + // tslint:disable-next-line: no-any use-default-type-parameter + let listenStub: sinon.SinonStub<any[], any>; + let pythonExecService: IPythonExecutionService; + let platformService: IPlatformService; + let logger: IProcessLogger; + let clock: fakeTimers.InstalledClock; + setup(() => { + logger = instance(mock(ProcessLogger)); + pythonExecService = mock<IPythonExecutionService>(); + platformService = mock<IPlatformService>(); + (instance(pythonExecService) as any).then = undefined; + sendRequestStub = sinon.stub(); + listenStub = sinon.stub(); + listenStub.returns(undefined); + sendRequestStub.returns({ pong: 'hello' }); + }); + teardown(function () { + // tslint:disable-next-line: no-invalid-this + if (this.currentTest && this.currentTest.state === 'failed') { + asyncDump(); + } + if (clock) { + clock.uninstall(); + } + sinon.restore(); + }); + + async function setupDaemon(daemonPoolService: DaemonPool) { + const mockMessageConnection = ({ + sendRequest: sendRequestStub, + listen: listenStub, + onClose: noop, + onDispose: noop, + onError: noop, + onNotification: noop, + onUnhandledNotification: noop + } as any) as MessageConnection; + const daemonProc = (new EventEmitter() as any) as ChildProcess; + daemonProc.killed = false; + daemonProc.pid = process.pid; + daemonProc.kill = noop; + daemonProc.stdout = new EventEmitter() as any; + daemonProc.stderr = new EventEmitter() as any; + + when( + pythonExecService.execModuleObservable('vscode_datascience_helpers.daemon', anything(), anything()) + ).thenReturn({ + proc: daemonProc, + dispose: noop, + out: undefined as any + }); + + // Create and initialize the pool. + daemonPoolService.createConnection = () => mockMessageConnection; + await daemonPoolService.initialize(); + } + test('Create daemons when initializing', async () => { + // Create and initialize the pool. + const pool = new DaemonPool( + logger, + [], + { pythonPath: 'py.exe' }, + instance(pythonExecService), + instance(platformService), + undefined + ); + await setupDaemon(pool); + + // 2 = 2 for standard daemon + 1 observable daemon. + expect(sendRequestStub.callCount).equal(3); + expect(listenStub.callCount).equal(3); + }).timeout(5000); + test('Create specific number of daemons when initializing', async () => { + // Create and initialize the pool. + const pool = new DaemonPool( + logger, + [], + { daemonCount: 5, observableDaemonCount: 3, pythonPath: 'py.exe' }, + instance(pythonExecService), + instance(platformService), + undefined + ); + await setupDaemon(pool); + + // 3 = 2 for standard daemon + 1 observable daemon. + expect(sendRequestStub.callCount).equal(8); + expect(listenStub.callCount).equal(8); + }).timeout(5000); + test('Throw error if daemon does not respond to ping within 5s', async () => { + clock = fakeTimers.install(); + sendRequestStub.reset(); + sendRequestStub.returns(sleep(6_000).then({ pong: 'hello' } as any)); + // Create and initialize the pool. + const pool = new DaemonPool( + logger, + [], + { daemonCount: 5, observableDaemonCount: 3, pythonPath: 'py.exe' }, + instance(pythonExecService), + instance(platformService), + undefined + ); + const promise = setupDaemon(pool); + + // Ensure all exceptions are handled. + promise.catch(noop); + + // Move time forward to trigger timeout error (the limit is 5s). + await clock.tickAsync(5_000); + await clock.runAllAsync(); + + await expect(promise).to.eventually.be.rejectedWith('Timeout'); + }).timeout(5000); + test('If executing python is fast, then use the daemon', async () => { + const getInterpreterInformationStub = sinon.stub( + PythonDaemonExecutionService.prototype, + 'getInterpreterInformation' + ); + const interpreterInfoFromDaemon: InterpreterInformation = { pythonPath: 1 } as any; + // Delay returning interpreter info for 2 seconds. + getInterpreterInformationStub.resolves(interpreterInfoFromDaemon); + + // Create and initialize the pool. + const pool = new DaemonPool( + logger, + [], + { daemonCount: 1, observableDaemonCount: 1, pythonPath: 'py.exe' }, + instance(pythonExecService), + instance(platformService), + undefined + ); + await setupDaemon(pool); + + // 3 = 2 for standard daemon + 1 observable daemon. + expect(sendRequestStub.callCount).equal(2); + expect(listenStub.callCount).equal(2); + + const [info1, info2, info3] = await Promise.all([ + pool.getInterpreterInformation(), + pool.getInterpreterInformation(), + pool.getInterpreterInformation() + ]); + + // Verify we used the daemon. + expect(getInterpreterInformationStub.callCount).to.equal(3); + // Verify we used the python execution service. + verify(pythonExecService.getInterpreterInformation()).never(); + + expect(info1).to.deep.equal(interpreterInfoFromDaemon); + expect(info2).to.deep.equal(interpreterInfoFromDaemon); + expect(info3).to.deep.equal(interpreterInfoFromDaemon); + }).timeout(5000); + test('If executing python code takes too long (> 1s), then return standard PythonExecutionService', async () => { + clock = fakeTimers.install(); + const getInterpreterInformationStub = sinon.stub( + PythonDaemonExecutionService.prototype, + 'getInterpreterInformation' + ); + const interpreterInfoFromDaemon: InterpreterInformation = { pythonPath: 1 } as any; + const interpreterInfoFromPythonProc: InterpreterInformation = { pythonPath: 2 } as any; + + try { + let daemonsBusyExecutingCode = 0; + let daemonsExecuted = 0; + // Delay returning interpreter info for 5 seconds. + getInterpreterInformationStub.value(async () => { + daemonsBusyExecutingCode += 1; + // Add an artificial delay to cause daemon to be busy. + await sleep(5_000); + daemonsExecuted += 1; + return interpreterInfoFromDaemon; + }); + when(pythonExecService.getInterpreterInformation()).thenResolve(interpreterInfoFromPythonProc); + + // Create and initialize the pool. + const pool = new DaemonPool( + logger, + [], + { daemonCount: 2, observableDaemonCount: 1, pythonPath: 'py.exe' }, + instance(pythonExecService), + instance(platformService), + undefined + ); + + await setupDaemon(pool); + + // 3 = 2 for standard daemon + 1 observable daemon. + expect(sendRequestStub.callCount).equal(3); + expect(listenStub.callCount).equal(3); + + // Lets get interpreter information. + // As we have 2 daemons in the pool, 2 of the requests will be processed by the two daemons. + // As getting interpreter information will take 1.5s (see above), the daemon pool will + // end up using standard process code to serve the other 2 requests. + // 4 requests = 2 served by daemons, and other 2 served by standard processes. + const promises = Promise.all([ + pool.getInterpreterInformation(), + pool.getInterpreterInformation(), + pool.getInterpreterInformation(), + pool.getInterpreterInformation() + ]); + + // Daemon pool will wait for 1s, after 500ms, it is still waiting for daemons to get free. + await clock.tickAsync(500); + // Confirm the fact that we didn't use standard processes to get interpreter info. + verify(pythonExecService.getInterpreterInformation()).never(); + + // Confirm the fact that daemon is still busy. + expect(daemonsBusyExecutingCode).to.equal(2); // Started. + expect(daemonsExecuted).to.equal(0); // Not yet finished. + expect(getInterpreterInformationStub.callCount).to.equal(0); // Not yet finished. + + // Daemon pool will wait for 1s, after which it will resort to using standard processes. + // Move time forward by 1s & then daemon pool will resort to using standard processes. + await clock.tickAsync(1000); + + // Confirm standard process was used. + verify(pythonExecService.getInterpreterInformation()).twice(); + + // Confirm the fact that daemon is still busy. + expect(daemonsBusyExecutingCode).to.equal(2); // Started. + expect(daemonsExecuted).to.equal(0); // Not yet finished. + expect(getInterpreterInformationStub.callCount).to.equal(0); // Not yet finished. + + // We know getting interpreter info from daemon will take 5seconds. + // Lets let that complete. + await clock.tickAsync(5_000); + await clock.runAllAsync(); + + const [info1, info2, info3, info4] = await promises; + + // Verify the fact that the first 2 requests were served by daemons. + expect(info1).to.deep.equal(interpreterInfoFromDaemon); + expect(info2).to.deep.equal(interpreterInfoFromDaemon); + expect(daemonsExecuted).to.equal(2); // 2 daemons called this. + + // Verify the fact that the seconds 2 requests were served by standard processes. + expect(info3).to.deep.equal(interpreterInfoFromPythonProc); + expect(info4).to.deep.equal(interpreterInfoFromPythonProc); + verify(pythonExecService.getInterpreterInformation()).twice(); // 2 standard processes called this. + } finally { + // Make sure to remove the stub or other tests will take too long. + getInterpreterInformationStub.restore(); + } + }).timeout(5000); + test('If executing python is fast, then use the daemon (for observables)', async () => { + const execModuleObservable = sinon.stub(PythonDaemonExecutionService.prototype, 'execModuleObservable'); + const out = new Observable<Output<string>>((s) => { + s.next({ source: 'stdout', out: '' }); + s.complete(); + }); + execModuleObservable.returns({ out } as any); + + // Create and initialize the pool. + const pool = new DaemonPool( + logger, + [], + { daemonCount: 1, observableDaemonCount: 1, pythonPath: 'py.exe' }, + instance(pythonExecService), + instance(platformService), + undefined + ); + await setupDaemon(pool); + + // 3 = 2 for standard daemon + 1 observable daemon. + expect(sendRequestStub.callCount).equal(2); + expect(listenStub.callCount).equal(2); + + // Invoke the execModuleObservable method twice (one to use daemon, other will use python exec service). + reset(pythonExecService); + when(pythonExecService.execModuleObservable(anything(), anything(), anything())).thenReturn({ out } as any); + await Promise.all([pool.execModuleObservable('x', [], {}), pool.execModuleObservable('x', [], {})]); + + // Verify we used the daemon. + expect(execModuleObservable.callCount).to.equal(1); + // Verify we used the python execution service. + verify(pythonExecService.execModuleObservable(anything(), anything(), anything())).once(); + }).timeout(5000); +}); diff --git a/src/test/common/process/pythonEnvironment.unit.test.ts b/src/test/common/process/pythonEnvironment.unit.test.ts new file mode 100644 index 000000000000..a4035e1c1af8 --- /dev/null +++ b/src/test/common/process/pythonEnvironment.unit.test.ts @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable: max-func-body-length + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { + createCondaEnv, + createPythonEnv, + createWindowsStoreEnv +} from '../../../client/common/process/pythonEnvironment'; +import { IProcessService, StdErrError } from '../../../client/common/process/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; + +const isolated = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'pyvsc-run-isolated.py'); + +use(chaiAsPromised); + +suite('PythonEnvironment', () => { + let processService: TypeMoq.IMock<IProcessService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + const pythonPath = 'path/to/python'; + + setup(() => { + processService = TypeMoq.Mock.ofType<IProcessService>(undefined, TypeMoq.MockBehavior.Strict); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); + }); + + test('getInterpreterInformation should return an object if the python path is valid', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate'], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true + }; + + processService + .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getInterpreterInformation(); + const expectedResult = { + architecture: Architecture.x64, + path: pythonPath, + version: new SemVer('3.7.5-candidate'), + sysPrefix: json.sysPrefix, + sysVersion: undefined + }; + + expect(result).to.deep.equal(expectedResult, 'Incorrect value returned by getInterpreterInformation().'); + }); + + test('getInterpreterInformation should return an object if the version info contains less than 4 items', async () => { + const json = { + versionInfo: [3, 7, 5], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true + }; + + processService + .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getInterpreterInformation(); + const expectedResult = { + architecture: Architecture.x64, + path: pythonPath, + version: new SemVer('3.7.5'), + sysPrefix: json.sysPrefix, + sysVersion: undefined + }; + + expect(result).to.deep.equal( + expectedResult, + 'Incorrect value returned by getInterpreterInformation() with truncated versionInfo.' + ); + }); + + test('getInterpreterInformation should return an object with the architecture value set to x86 if json.is64bit is not 64bit', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate'], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: false + }; + + processService + .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getInterpreterInformation(); + const expectedResult = { + architecture: Architecture.x86, + path: pythonPath, + version: new SemVer('3.7.5-candidate'), + sysPrefix: json.sysPrefix, + sysVersion: undefined + }; + + expect(result).to.deep.equal( + expectedResult, + 'Incorrect value returned by getInterpreterInformation() for x86b architecture.' + ); + }); + + test('getInterpreterInformation should error out if interpreterInfo.py times out', async () => { + processService + .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + // tslint:disable-next-line: no-any + .returns(() => Promise.reject(new Error('timed out'))); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getInterpreterInformation(); + + expect(result).to.equal( + undefined, + 'getInterpreterInfo() should return undefined because interpreterInfo timed out.' + ); + }); + + test('getInterpreterInformation should return undefined if the json value returned by interpreterInfo.py is not valid', async () => { + processService + .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'bad json' })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getInterpreterInformation(); + + expect(result).to.equal(undefined, 'getInterpreterInfo() should return undefined because of bad json.'); + }); + + test('getExecutablePath should return pythonPath if pythonPath is a file', async () => { + fileSystem.setup((f) => f.fileExists(pythonPath)).returns(() => Promise.resolve(true)); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getExecutablePath(); + + expect(result).to.equal(pythonPath, "getExecutablePath() sbould return pythonPath if it's a file"); + }); + + test('getExecutablePath should not return pythonPath if pythonPath is not a file', async () => { + const executablePath = 'path/to/dummy/executable'; + fileSystem.setup((f) => f.fileExists(pythonPath)).returns(() => Promise.resolve(false)); + const argv = [isolated, '-c', 'import sys;print(sys.executable)']; + processService + .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .returns(() => Promise.resolve({ stdout: executablePath })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getExecutablePath(); + + expect(result).to.equal(executablePath, "getExecutablePath() sbould not return pythonPath if it's not a file"); + }); + + test('getExecutablePath should throw if the result of exec() writes to stderr', async () => { + const stderr = 'bar'; + fileSystem.setup((f) => f.fileExists(pythonPath)).returns(() => Promise.resolve(false)); + const argv = [isolated, '-c', 'import sys;print(sys.executable)']; + processService + .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .returns(() => Promise.reject(new StdErrError(stderr))); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = env.getExecutablePath(); + + await expect(result).to.eventually.be.rejectedWith(stderr); + }); + + test('isModuleInstalled should call processService.exec()', async () => { + const moduleName = 'foo'; + const argv = [isolated, '-c', `import ${moduleName}`]; + processService + .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .returns(() => Promise.resolve({ stdout: '' })) + .verifiable(TypeMoq.Times.once()); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + await env.isModuleInstalled(moduleName); + + processService.verifyAll(); + }); + + test('isModuleInstalled should return true when processService.exec() succeeds', async () => { + const moduleName = 'foo'; + const argv = [isolated, '-c', `import ${moduleName}`]; + processService + .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .returns(() => Promise.resolve({ stdout: '' })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.isModuleInstalled(moduleName); + + expect(result).to.equal(true, 'isModuleInstalled() should return true if the module exists'); + }); + + test('isModuleInstalled should return false when processService.exec() throws', async () => { + const moduleName = 'foo'; + const argv = [isolated, '-c', `import ${moduleName}`]; + processService + .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .returns(() => Promise.reject(new StdErrError('bar'))); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.isModuleInstalled(moduleName); + + expect(result).to.equal(false, 'isModuleInstalled() should return false if the module does not exist'); + }); + + test('getExecutionInfo should return pythonPath and the execution arguments as is', () => { + const args = ['-a', 'b', '-c']; + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = env.getExecutionInfo(args); + + expect(result).to.deep.equal( + { command: pythonPath, args, python: [pythonPath], pythonExecutable: pythonPath }, + 'getExecutionInfo should return pythonPath and the command and execution arguments as is' + ); + }); +}); + +suite('CondaEnvironment', () => { + let processService: TypeMoq.IMock<IProcessService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + const args = ['-a', 'b', '-c']; + const pythonPath = 'path/to/python'; + const condaFile = 'path/to/conda'; + + setup(() => { + processService = TypeMoq.Mock.ofType<IProcessService>(undefined, TypeMoq.MockBehavior.Strict); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); + }); + + test('getExecutionInfo with a named environment should return execution info using the environment name', () => { + const condaInfo = { name: 'foo', path: 'bar' }; + const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); + + const result = env.getExecutionInfo(args); + + expect(result).to.deep.equal({ + command: condaFile, + args: ['run', '-n', condaInfo.name, 'python', ...args], + python: [condaFile, 'run', '-n', condaInfo.name, 'python'], + pythonExecutable: 'python' + }); + }); + + test('getExecutionInfo with a non-named environment should return execution info using the environment path', () => { + const condaInfo = { name: '', path: 'bar' }; + const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); + + const result = env.getExecutionInfo(args); + + expect(result).to.deep.equal({ + command: condaFile, + args: ['run', '-p', condaInfo.path, 'python', ...args], + python: [condaFile, 'run', '-p', condaInfo.path, 'python'], + pythonExecutable: 'python' + }); + }); + + test('getExecutionObservableInfo with a named environment should return execution info using pythonPath only', () => { + const expected = { command: pythonPath, args, python: [pythonPath], pythonExecutable: pythonPath }; + const condaInfo = { name: 'foo', path: 'bar' }; + const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); + + const result = env.getExecutionObservableInfo(args); + + expect(result).to.deep.equal(expected); + }); + + test('getExecutionObservableInfo with a non-named environment should return execution info using pythonPath only', () => { + const expected = { command: pythonPath, args, python: [pythonPath], pythonExecutable: pythonPath }; + const condaInfo = { name: '', path: 'bar' }; + const env = createCondaEnv(condaFile, condaInfo, pythonPath, processService.object, fileSystem.object); + + const result = env.getExecutionObservableInfo(args); + + expect(result).to.deep.equal(expected); + }); +}); + +suite('WindowsStoreEnvironment', () => { + let processService: TypeMoq.IMock<IProcessService>; + const pythonPath = 'foo'; + + setup(() => { + processService = TypeMoq.Mock.ofType<IProcessService>(undefined, TypeMoq.MockBehavior.Strict); + }); + + test('Should return pythonPath if it is the path to the windows store interpreter', async () => { + const env = createWindowsStoreEnv(pythonPath, processService.object); + + const executablePath = await env.getExecutablePath(); + + expect(executablePath).to.equal(pythonPath); + processService.verifyAll(); + }); +}); diff --git a/src/test/common/process/pythonExecutionFactory.unit.test.ts b/src/test/common/process/pythonExecutionFactory.unit.test.ts new file mode 100644 index 000000000000..28a0756fa2b1 --- /dev/null +++ b/src/test/common/process/pythonExecutionFactory.unit.test.ts @@ -0,0 +1,526 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; +import { expect } from 'chai'; +import { parse, SemVer } from 'semver'; +import * as sinon from 'sinon'; +import { anyString, anything, instance, mock, reset, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; + +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { BufferDecoder } from '../../../client/common/process/decoder'; +import { ProcessLogger } from '../../../client/common/process/logger'; +import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; +import { PythonDaemonExecutionServicePool } from '../../../client/common/process/pythonDaemonPool'; +import { CONDA_RUN_VERSION, PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; +import { + ExecutionFactoryCreationOptions, + IBufferDecoder, + IProcessLogger, + IProcessService, + IProcessServiceFactory, + IPythonExecutionService +} from '../../../client/common/process/types'; +import { IConfigurationService, IDisposableRegistry } from '../../../client/common/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { IWindowsStoreInterpreter } from '../../../client/interpreter/locators/types'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { WindowsStoreInterpreter } from '../../../client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +// tslint:disable:no-any max-func-body-length chai-vague-errors + +const pythonInterpreter: PythonEnvironment = { + path: '/foo/bar/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 +}; + +function title(resource?: Uri, interpreter?: PythonEnvironment) { + return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; +} + +async function verifyCreateActivated( + factory: PythonExecutionFactory, + activationHelper: IEnvironmentActivationService, + resource?: Uri, + interpreter?: PythonEnvironment +): Promise<IPythonExecutionService> { + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve(); + + const service = await factory.createActivatedEnvironment({ resource, interpreter }); + + verify(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).once(); + + return service; +} + +suite('Process - PythonExecutionFactory', () => { + [ + { resource: undefined, interpreter: undefined }, + { resource: undefined, interpreter: pythonInterpreter }, + { resource: Uri.parse('x'), interpreter: undefined }, + { resource: Uri.parse('x'), interpreter: pythonInterpreter } + ].forEach((item) => { + const resource = item.resource; + const interpreter = item.interpreter; + suite(title(resource, interpreter), () => { + let factory: PythonExecutionFactory; + let activationHelper: IEnvironmentActivationService; + let bufferDecoder: IBufferDecoder; + let processFactory: IProcessServiceFactory; + let configService: IConfigurationService; + let condaService: ICondaService; + let processLogger: IProcessLogger; + let processService: typemoq.IMock<IProcessService>; + let windowsStoreInterpreter: IWindowsStoreInterpreter; + let interpreterService: IInterpreterService; + let executionService: typemoq.IMock<IPythonExecutionService>; + let platformService: IPlatformService; + setup(() => { + bufferDecoder = mock(BufferDecoder); + activationHelper = mock(EnvironmentActivationService); + processFactory = mock(ProcessServiceFactory); + configService = mock(ConfigurationService); + condaService = mock(CondaService); + processLogger = mock(ProcessLogger); + platformService = mock<IPlatformService>(); + windowsStoreInterpreter = mock(WindowsStoreInterpreter); + executionService = typemoq.Mock.ofType<IPythonExecutionService>(); + executionService.setup((p: any) => p.then).returns(() => undefined); + when(processLogger.logProcess('', [], {})).thenReturn(); + processService = typemoq.Mock.ofType<IProcessService>(); + processService + .setup((p) => + p.on('exec', () => { + return; + }) + ) + .returns(() => processService.object); + processService.setup((p: any) => p.then).returns(() => undefined); + interpreterService = mock(InterpreterService); + when(interpreterService.getInterpreterDetails(anything())).thenResolve({ + version: { major: 3 } + } as any); + const serviceContainer = mock(ServiceContainer); + when(serviceContainer.get<IDisposableRegistry>(IDisposableRegistry)).thenReturn([]); + when(serviceContainer.get<IProcessLogger>(IProcessLogger)).thenReturn(processLogger); + when(serviceContainer.get<IInterpreterService>(IInterpreterService)).thenReturn( + instance(interpreterService) + ); + when(serviceContainer.tryGet<IInterpreterService>(IInterpreterService)).thenReturn( + instance(interpreterService) + ); + factory = new PythonExecutionFactory( + instance(serviceContainer), + instance(activationHelper), + instance(processFactory), + instance(configService), + instance(condaService), + instance(bufferDecoder), + instance(windowsStoreInterpreter), + instance(platformService) + ); + }); + teardown(() => sinon.restore()); + test('Ensure PythonExecutionService is created', async () => { + const pythonSettings = mock(PythonSettings); + when(processFactory.create(resource)).thenResolve(processService.object); + when(activationHelper.getActivatedEnvironmentVariables(resource)).thenResolve({ x: '1' }); + when(pythonSettings.pythonPath).thenReturn('HELLO'); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + + const service = await factory.create({ resource }); + + expect(service).to.not.equal(undefined); + verify(processFactory.create(resource)).once(); + verify(pythonSettings.pythonPath).once(); + }); + test('Ensure we use an existing `create` method if there are no environment variables for the activated env', async () => { + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + + let createInvoked = false; + const mockExecService = 'something'; + factory.create = async (_options: ExecutionFactoryCreationOptions) => { + createInvoked = true; + return Promise.resolve((mockExecService as any) as IPythonExecutionService); + }; + + const service = await verifyCreateActivated(factory, activationHelper, resource, interpreter); + assert.deepEqual(service, mockExecService); + assert.equal(createInvoked, true); + }); + test('Ensure we use an existing `create` method if there are no environment variables (0 length) for the activated env', async () => { + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + + let createInvoked = false; + const mockExecService = 'something'; + factory.create = async (_options: ExecutionFactoryCreationOptions) => { + createInvoked = true; + return Promise.resolve((mockExecService as any) as IPythonExecutionService); + }; + + const service = await verifyCreateActivated(factory, activationHelper, resource, interpreter); + assert.deepEqual(service, mockExecService); + assert.equal(createInvoked, true); + }); + test('PythonExecutionService is created', async () => { + let createInvoked = false; + const mockExecService = 'something'; + factory.create = async (_options: ExecutionFactoryCreationOptions) => { + createInvoked = true; + return Promise.resolve((mockExecService as any) as IPythonExecutionService); + }; + + const pythonSettings = mock(PythonSettings); + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ + x: '1' + }); + when(pythonSettings.pythonPath).thenReturn('HELLO'); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + const service = await factory.createActivatedEnvironment({ resource, interpreter }); + + expect(service).to.not.equal(undefined); + verify(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).once(); + if (!interpreter) { + verify(pythonSettings.pythonPath).once(); + } + assert.equal(createInvoked, false); + }); + + test("Ensure `create` returns a WindowsStorePythonProcess instance if it's a windows store intepreter path", async () => { + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + when(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).thenReturn(true); + + const service = await factory.create({ resource }); + + expect(service).to.not.equal(undefined); + verify(processFactory.create(resource)).once(); + verify(pythonSettings.pythonPath).once(); + verify(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).once(); + }); + + test('Ensure `create` returns a CondaExecutionService instance if createCondaExecutionService() returns a valid object', async function () { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + + when(interpreterService.hasInterpreters).thenResolve(true); + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); + when(condaService.getCondaEnvironment(pythonPath)).thenResolve({ + name: 'foo', + path: 'path/to/foo/env' + }); + when(condaService.getCondaFile()).thenResolve('conda'); + + const service = await factory.create({ resource }); + + expect(service).to.not.equal(undefined); + verify(processFactory.create(resource)).once(); + verify(pythonSettings.pythonPath).once(); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + }); + + test('Ensure `create` returns a PythonExecutionService instance if createCondaExecutionService() returns undefined', async function () { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + when(condaService.getCondaVersion()).thenResolve(new SemVer('1.0.0')); + when(interpreterService.hasInterpreters).thenResolve(true); + + const service = await factory.create({ resource }); + + expect(service).to.not.equal(undefined); + verify(processFactory.create(resource)).once(); + verify(pythonSettings.pythonPath).once(); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + }); + + test('Ensure `createActivatedEnvironment` returns a CondaExecutionService instance if createCondaExecutionService() returns a valid object', async function () { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + + const pythonPath = 'path/to/python'; + const pythonSettings = mock(PythonSettings); + + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ + x: '1' + }); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); + when(condaService.getCondaEnvironment(anyString())).thenResolve({ + name: 'foo', + path: 'path/to/foo/env' + }); + when(condaService.getCondaFile()).thenResolve('conda'); + + const service = await factory.createActivatedEnvironment({ resource, interpreter }); + + expect(service).to.not.equal(undefined); + verify(condaService.getCondaFile()).once(); + if (!interpreter) { + verify(pythonSettings.pythonPath).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + } else { + // @ts-ignore + verify(condaService.getCondaEnvironment(interpreter.path)).once(); + } + }); + + test('Ensure `createActivatedEnvironment` returns a PythonExecutionService instance if createCondaExecutionService() returns undefined', async function () { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + + let createInvoked = false; + const pythonPath = 'path/to/python'; + const mockExecService = 'mockService'; + factory.create = async (_options: ExecutionFactoryCreationOptions) => { + createInvoked = true; + return Promise.resolve((mockExecService as any) as IPythonExecutionService); + }; + + const pythonSettings = mock(PythonSettings); + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ + x: '1' + }); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + when(condaService.getCondaVersion()).thenResolve(new SemVer('1.0.0')); + + const service = await factory.createActivatedEnvironment({ resource, interpreter }); + + expect(service).to.not.equal(undefined); + verify(condaService.getCondaFile()).once(); + verify(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).once(); + verify(condaService.getCondaVersion()).once(); + if (!interpreter) { + verify(pythonSettings.pythonPath).once(); + } + + assert.equal(createInvoked, false); + }); + + test('Ensure `createCondaExecutionService` creates a CondaExecutionService instance if there is a conda environment', async () => { + const pythonPath = 'path/to/python'; + when(condaService.getCondaEnvironment(pythonPath)).thenResolve({ + name: 'foo', + path: 'path/to/foo/env' + }); + when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); + when(condaService.getCondaFile()).thenResolve('conda'); + + const result = await factory.createCondaExecutionService(pythonPath, processService.object, resource); + + expect(result).to.not.equal(undefined); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + }); + + test('Ensure `createCondaExecutionService` instantiates a ProcessService instance if the process argument is undefined', async () => { + const pythonPath = 'path/to/python'; + when(processFactory.create(resource)).thenResolve(processService.object); + when(condaService.getCondaEnvironment(pythonPath)).thenResolve({ + name: 'foo', + path: 'path/to/foo/env' + }); + when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); + when(condaService.getCondaFile()).thenResolve('conda'); + + const result = await factory.createCondaExecutionService(pythonPath, undefined, resource); + + expect(result).to.not.equal(undefined); + verify(processFactory.create(resource)).once(); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + }); + + test('Ensure `createCondaExecutionService` returns undefined if there is no conda environment', async () => { + const pythonPath = 'path/to/python'; + when(condaService.getCondaEnvironment(pythonPath)).thenResolve(undefined); + when(condaService.getCondaVersion()).thenResolve(new SemVer(CONDA_RUN_VERSION)); + + const result = await factory.createCondaExecutionService(pythonPath, processService.object); + + expect(result).to.be.equal( + undefined, + 'createCondaExecutionService should return undefined if not in a conda environment' + ); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + }); + + test('Ensure `createCondaExecutionService` returns undefined if the conda version does not support conda run', async () => { + const pythonPath = 'path/to/python'; + when(condaService.getCondaVersion()).thenResolve(new SemVer('1.0.0')); + + const result = await factory.createCondaExecutionService(pythonPath, processService.object); + + expect(result).to.be.equal( + undefined, + 'createCondaExecutionService should return undefined if not in a conda environment' + ); + verify(condaService.getCondaVersion()).once(); + verify(condaService.getCondaEnvironment(pythonPath)).once(); + verify(condaService.getCondaFile()).once(); + }); + test('Create Daemon Service an invoke initialize', async () => { + const pythonSettings = mock(PythonSettings); + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ + x: '1' + }); + when(pythonSettings.pythonPath).thenReturn('HELLO'); + when(configService.getSettings(anything())).thenReturn(instance(pythonSettings)); + factory.createActivatedEnvironment = () => Promise.resolve(executionService.object); + + const initialize = sinon.stub(PythonDaemonExecutionServicePool.prototype, 'initialize'); + initialize.returns(Promise.resolve()); + + const daemon = await factory.createDaemon({ resource, pythonPath: item.interpreter?.path }); + + expect(daemon).instanceOf(PythonDaemonExecutionServicePool); + expect(initialize.callCount).to.equal(1); + }); + test('Do not create Daemon Service for Python 2.7', async () => { + const pythonSettings = mock(PythonSettings); + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ + x: '1' + }); + when(pythonSettings.pythonPath).thenReturn('HELLO'); + when(configService.getSettings(anything())).thenReturn(instance(pythonSettings)); + reset(interpreterService); + when(interpreterService.getInterpreterDetails(anything(), anything())).thenResolve({ + version: parse('2.7.14') + } as any); + factory.createActivatedEnvironment = () => Promise.resolve(executionService.object); + + const initialize = sinon.stub(PythonDaemonExecutionServicePool.prototype, 'initialize'); + initialize.returns(Promise.resolve()); + + const daemon = await factory.createDaemon({ resource, pythonPath: item.interpreter?.path }); + + expect(daemon).not.instanceOf(PythonDaemonExecutionServicePool); + expect(initialize.callCount).to.equal(0); + }); + test('Create Daemon Service should return the same daemon when created one after another', async () => { + const pythonSettings = mock(PythonSettings); + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ + x: '1' + }); + when(pythonSettings.pythonPath).thenReturn('HELLO'); + when(configService.getSettings(anything())).thenReturn(instance(pythonSettings)); + factory.createActivatedEnvironment = () => Promise.resolve(executionService.object); + + const initialize = sinon.stub(PythonDaemonExecutionServicePool.prototype, 'initialize'); + initialize.returns(Promise.resolve()); + + const daemon1 = await factory.createDaemon({ resource, pythonPath: item.interpreter?.path }); + const daemon2 = await factory.createDaemon({ resource, pythonPath: item.interpreter?.path }); + + expect(daemon1).to.equal(daemon2); + }); + test('Create Daemon Service should return two different daemons (if python path is different)', async () => { + const pythonSettings = mock(PythonSettings); + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ + x: '1' + }); + when(pythonSettings.pythonPath).thenReturn('HELLO'); + when(configService.getSettings(anything())).thenReturn(instance(pythonSettings)); + factory.createActivatedEnvironment = () => Promise.resolve(executionService.object); + + const initialize = sinon.stub(PythonDaemonExecutionServicePool.prototype, 'initialize'); + initialize.returns(Promise.resolve()); + + const daemon1 = await factory.createDaemon({ resource }); + + when(pythonSettings.pythonPath).thenReturn('HELLO2'); + const daemon2 = await factory.createDaemon({ resource }); + + expect(daemon1).to.not.equal(daemon2); + }); + test('Create Daemon Service should return the same daemon when created in parallel', async () => { + const pythonSettings = mock(PythonSettings); + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ + x: '1' + }); + when(pythonSettings.pythonPath).thenReturn('HELLO'); + when(configService.getSettings(anything())).thenReturn(instance(pythonSettings)); + factory.createActivatedEnvironment = () => Promise.resolve(executionService.object); + + const initialize = sinon.stub(PythonDaemonExecutionServicePool.prototype, 'initialize'); + initialize.returns(Promise.resolve()); + + const [daemon1, daemon2] = await Promise.all([ + factory.createDaemon({ resource, pythonPath: item.interpreter?.path }), + factory.createDaemon({ resource, pythonPath: item.interpreter?.path }) + ]); + + expect(daemon1).to.equal(daemon2); + }); + test('Failure to create Daemon Service should return PythonExecutionService', async () => { + const pythonSettings = mock(PythonSettings); + const pythonExecService = ({ dummy: 1 } as any) as IPythonExecutionService; + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve({ + x: '1' + }); + when(pythonSettings.pythonPath).thenReturn('HELLO'); + when(configService.getSettings(anything())).thenReturn(instance(pythonSettings)); + factory.createActivatedEnvironment = () => Promise.resolve(pythonExecService); + + const initialize = sinon.stub(PythonDaemonExecutionServicePool.prototype, 'initialize'); + initialize.returns(Promise.reject(new Error('Kaboom'))); + + const daemon = await factory.createDaemon({ resource, pythonPath: item.interpreter?.path }); + + expect(daemon).not.instanceOf(PythonDaemonExecutionServicePool); + expect(initialize.callCount).to.equal(1); + expect(daemon).equal(pythonExecService); + }); + }); + }); +}); diff --git a/src/test/common/process/pythonProc.simple.multiroot.test.ts b/src/test/common/process/pythonProc.simple.multiroot.test.ts new file mode 100644 index 000000000000..7df62d4a805a --- /dev/null +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { execFile } from 'child_process'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import * as path from 'path'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IPythonExecutionFactory, StdErrError } from '../../../client/common/process/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { clearCache } from '../../../client/common/utils/cacheUtils'; +import { OSType } from '../../../client/common/utils/platform'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { clearPythonPathInWorkspaceFolder, getExtensionSettings, isOs, isPythonVersion } from '../../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; + +use(chaiAsPromised); + +const multirootPath = path.join(__dirname, '..', '..', '..', '..', 'src', 'testMultiRootWkspc'); +const workspace4Path = Uri.file(path.join(multirootPath, 'workspace4')); +const workspace4PyFile = Uri.file(path.join(workspace4Path.fsPath, 'one.py')); + +// tslint:disable-next-line:max-func-body-length +suite('PythonExecutableService', () => { + let serviceContainer: IServiceContainer; + let configService: IConfigurationService; + let pythonExecFactory: IPythonExecutionFactory; + + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await clearPythonPathInWorkspaceFolder(workspace4Path); + serviceContainer = (await initialize()).serviceContainer; + }); + setup(async () => { + configService = serviceContainer.get<IConfigurationService>(IConfigurationService); + pythonExecFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); + + await configService.updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + clearCache(); + return initializeTest(); + }); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await clearPythonPathInWorkspaceFolder(workspace4Path); + await configService.updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + await initializeTest(); + clearCache(); + }); + + test('Importing without a valid PYTHONPATH should fail', async () => { + await configService.updateSetting( + 'envFile', + 'someInvalidFile.env', + workspace4PyFile, + ConfigurationTarget.WorkspaceFolder + ); + pythonExecFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); + const promise = pythonExecService.exec([workspace4PyFile.fsPath], { + cwd: path.dirname(workspace4PyFile.fsPath), + throwOnStdErr: true + }); + + await expect(promise).to.eventually.be.rejectedWith(StdErrError); + }); + + test('Importing with a valid PYTHONPATH from .env file should succeed', async function () { + // This test has not been working for many months in Python 2.7 under + // Windows. Tracked by #2547. + if (isOs(OSType.Windows) && (await isPythonVersion('2.7'))) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + + await configService.updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); + const promise = pythonExecService.exec([workspace4PyFile.fsPath], { + cwd: path.dirname(workspace4PyFile.fsPath), + throwOnStdErr: true + }); + + await expect(promise).to.eventually.have.property('stdout', `Hello${EOL}`); + }); + + test("Known modules such as 'os' and 'sys' should be deemed 'installed'", async () => { + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); + const osModuleIsInstalled = pythonExecService.isModuleInstalled('os'); + const sysModuleIsInstalled = pythonExecService.isModuleInstalled('sys'); + await expect(osModuleIsInstalled).to.eventually.equal(true, 'os module is not installed'); + await expect(sysModuleIsInstalled).to.eventually.equal(true, 'sys module is not installed'); + }); + + test("Unknown modules such as 'xyzabc123' be deemed 'not installed'", async () => { + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); + const randomModuleName = `xyz123${new Date().getSeconds()}`; + const randomModuleIsInstalled = pythonExecService.isModuleInstalled(randomModuleName); + await expect(randomModuleIsInstalled).to.eventually.equal( + false, + `Random module '${randomModuleName}' is installed` + ); + }); + + test('Ensure correct path to executable is returned', async () => { + const pythonPath = getExtensionSettings(workspace4Path).pythonPath; + let expectedExecutablePath: string; + if (await fs.pathExists(pythonPath)) { + expectedExecutablePath = pythonPath; + } else { + expectedExecutablePath = await new Promise<string>((resolve) => { + execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_error, stdout, _stdErr) => { + resolve(stdout.trim()); + }); + }); + } + const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); + const executablePath = await pythonExecService.getExecutablePath(); + expect(executablePath).to.equal(expectedExecutablePath, 'Executable paths are not the same'); + }); +}); diff --git a/src/test/common/process/pythonProcess.unit.test.ts b/src/test/common/process/pythonProcess.unit.test.ts new file mode 100644 index 000000000000..cc0a847e7e7e --- /dev/null +++ b/src/test/common/process/pythonProcess.unit.test.ts @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { createPythonEnv } from '../../../client/common/process/pythonEnvironment'; +import { createPythonProcessService } from '../../../client/common/process/pythonProcess'; +import { IProcessService, StdErrError } from '../../../client/common/process/types'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { noop } from '../../core'; + +const isolated = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'pyvsc-run-isolated.py').replace(/\\/g, '/'); + +use(chaiAsPromised); + +// tslint:disable-next-line: max-func-body-length +suite('PythonProcessService', () => { + let processService: TypeMoq.IMock<IProcessService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + const pythonPath = 'path/to/python'; + + setup(() => { + processService = TypeMoq.Mock.ofType<IProcessService>(undefined, TypeMoq.MockBehavior.Strict); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); + }); + + test('execObservable should call processService.execObservable', () => { + const args = ['-a', 'b', '-c']; + const options = {}; + const observable = { + proc: undefined, + // tslint:disable-next-line: no-any + out: {} as any, + dispose: () => { + noop(); + } + }; + processService.setup((p) => p.execObservable(pythonPath, args, options)).returns(() => observable); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + const procs = createPythonProcessService(processService.object, env); + + const result = procs.execObservable(args, options); + + processService.verify((p) => p.execObservable(pythonPath, args, options), TypeMoq.Times.once()); + expect(result).to.be.equal(observable, 'execObservable should return an observable'); + }); + + test('execModuleObservable should call processService.execObservable with the -m argument', () => { + const args = ['-a', 'b', '-c']; + const moduleName = 'foo'; + const expectedArgs = [isolated, moduleName, ...args]; + const options = {}; + const observable = { + proc: undefined, + // tslint:disable-next-line: no-any + out: {} as any, + dispose: () => { + noop(); + } + }; + processService.setup((p) => p.execObservable(pythonPath, expectedArgs, options)).returns(() => observable); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + const procs = createPythonProcessService(processService.object, env); + + const result = procs.execModuleObservable(moduleName, args, options); + + processService.verify((p) => p.execObservable(pythonPath, expectedArgs, options), TypeMoq.Times.once()); + expect(result).to.be.equal(observable, 'execModuleObservable should return an observable'); + }); + + test('exec should call processService.exec', async () => { + const args = ['-a', 'b', '-c']; + const options = {}; + const stdout = 'foo'; + processService.setup((p) => p.exec(pythonPath, args, options)).returns(() => Promise.resolve({ stdout })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + const procs = createPythonProcessService(processService.object, env); + + const result = await procs.exec(args, options); + + processService.verify((p) => p.exec(pythonPath, args, options), TypeMoq.Times.once()); + expect(result.stdout).to.be.equal(stdout, 'exec should return the content of stdout'); + }); + + test('execModule should call processService.exec with the -m argument', async () => { + const args = ['-a', 'b', '-c']; + const moduleName = 'foo'; + const expectedArgs = [isolated, moduleName, ...args]; + const options = {}; + const stdout = 'bar'; + processService + .setup((p) => p.exec(pythonPath, expectedArgs, options)) + .returns(() => Promise.resolve({ stdout })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + const procs = createPythonProcessService(processService.object, env); + + const result = await procs.execModule(moduleName, args, options); + + processService.verify((p) => p.exec(pythonPath, expectedArgs, options), TypeMoq.Times.once()); + expect(result.stdout).to.be.equal(stdout, 'exec should return the content of stdout'); + }); + + test('execModule should throw an error if the module is not installed', async () => { + const args = ['-a', 'b', '-c']; + const moduleName = 'foo'; + const expectedArgs = [isolated, moduleName, ...args]; + const options = {}; + processService + .setup((p) => p.exec(pythonPath, expectedArgs, options)) + .returns(() => Promise.resolve({ stdout: 'bar', stderr: `Error: No module named ${moduleName}` })); + processService + .setup((p) => p.exec(pythonPath, [isolated, '-c', `import ${moduleName}`], { throwOnStdErr: true })) + .returns(() => Promise.reject(new StdErrError('not installed'))); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + const procs = createPythonProcessService(processService.object, env); + + const result = procs.execModule(moduleName, args, options); + + expect(result).to.eventually.be.rejectedWith(`Module '${moduleName}' not installed`); + }); +}); diff --git a/src/test/common/process/pythonToolService.unit.test.ts b/src/test/common/process/pythonToolService.unit.test.ts new file mode 100644 index 000000000000..ff83e1ead8f7 --- /dev/null +++ b/src/test/common/process/pythonToolService.unit.test.ts @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { ProcessService } from '../../../client/common/process/proc'; +import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; +import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; +import { PythonToolExecutionService } from '../../../client/common/process/pythonToolService'; +import { + ExecutionResult, + IProcessService, + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonExecutionService, + ObservableExecutionResult +} from '../../../client/common/process/types'; +import { ExecutionInfo } from '../../../client/common/types'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { noop } from '../../core'; + +use(chaiAsPromised); + +// tslint:disable-next-line: max-func-body-length +suite('Process - Python tool execution service', () => { + const resource = Uri.parse('one'); + const observable: ObservableExecutionResult<string> = { + proc: undefined, + // tslint:disable-next-line: no-any + out: {} as any, + dispose: () => { + noop(); + } + }; + const executionResult: ExecutionResult<string> = { + stdout: 'output' + }; + + let pythonService: IPythonExecutionService; + let executionFactory: IPythonExecutionFactory; + let processService: IProcessService; + let processFactory: IProcessServiceFactory; + + let executionService: PythonToolExecutionService; + + setup(() => { + pythonService = mock<IPythonExecutionService>(); + when(pythonService.execModuleObservable(anything(), anything(), anything())).thenReturn(observable); + when(pythonService.execModule(anything(), anything(), anything())).thenResolve(executionResult); + const pythonServiceInstance = instance(pythonService); + // tslint:disable-next-line: no-any + (pythonServiceInstance as any).then = undefined; + + executionFactory = mock(PythonExecutionFactory); + when(executionFactory.create(anything())).thenResolve(pythonServiceInstance); + + processService = mock(ProcessService); + when(processService.execObservable(anything(), anything(), anything())).thenReturn(observable); + when(processService.exec(anything(), anything(), anything())).thenResolve(executionResult); + + processFactory = mock(ProcessServiceFactory); + when(processFactory.create(anything())).thenResolve(instance(processService)); + + const serviceContainer = mock(ServiceContainer); + when(serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory)).thenReturn( + instance(executionFactory) + ); + when(serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory)).thenReturn(instance(processFactory)); + + executionService = new PythonToolExecutionService(instance(serviceContainer)); + }); + + test('When calling execObservable, throw an error if environment variables are passed to the options parameter', () => { + const options = { env: { envOne: 'envOne' } }; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: 'moduleOne', + args: ['-a', 'b', '-c'] + }; + + const promise = executionService.execObservable(executionInfo, options, resource); + + expect(promise).to.eventually.be.rejectedWith('Environment variables are not supported'); + }); + + test('When calling execObservable, use a python execution service if a module name is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: 'moduleOne', + args: ['-a', 'b', '-c'] + }; + + const result = await executionService.execObservable(executionInfo, options, resource); + + assert.deepEqual(result, observable); + verify(pythonService.execModuleObservable(executionInfo.moduleName!, executionInfo.args, options)).once(); + }); + + test('When calling execObservable, use a process service if an empty module name string is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: '', + args: ['-a', 'b', '-c'] + }; + + const result = await executionService.execObservable(executionInfo, options, resource); + + assert.deepEqual(result, observable); + verify(processService.execObservable(executionInfo.execPath!, executionInfo.args, anything())).once(); + }); + + test('When calling execObservable, use a process service if no module name is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + args: ['-a', 'b', '-c'] + }; + + const result = await executionService.execObservable(executionInfo, options, resource); + + assert.deepEqual(result, observable); + verify(processService.execObservable(executionInfo.execPath!, executionInfo.args, anything())).once(); + }); + + test('When calling exec, throw an error if environment variables are passed to the options parameter', () => { + const options = { env: { envOne: 'envOne' } }; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: 'moduleOne', + args: ['-a', 'b', '-c'] + }; + + const promise = executionService.exec(executionInfo, options, resource); + + expect(promise).to.eventually.be.rejectedWith('Environment variables are not supported'); + }); + + test('When calling exec, use a python execution service if a module name is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: 'moduleOne', + args: ['-a', 'b', '-c'] + }; + + const result = await executionService.exec(executionInfo, options, resource); + + assert.deepEqual(result, executionResult); + verify(pythonService.execModule(executionInfo.moduleName!, executionInfo.args, options)).once(); + }); + + test('When calling exec, use a process service if an empty module name string is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: '', + args: ['-a', 'b', '-c'] + }; + + const result = await executionService.exec(executionInfo, options, resource); + + assert.deepEqual(result, executionResult); + verify(processService.exec(executionInfo.execPath!, executionInfo.args, anything())).once(); + }); + + test('When calling exec, use a process service if no module name is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + args: ['-a', 'b', '-c'] + }; + + const result = await executionService.exec(executionInfo, options, resource); + + assert.deepEqual(result, executionResult); + verify(processService.exec(executionInfo.execPath!, executionInfo.args, anything())).once(); + }); +}); diff --git a/src/test/common/process/serviceRegistry.unit.test.ts b/src/test/common/process/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..6f8b3d63d902 --- /dev/null +++ b/src/test/common/process/serviceRegistry.unit.test.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { BufferDecoder } from '../../../client/common/process/decoder'; +import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; +import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; +import { PythonToolExecutionService } from '../../../client/common/process/pythonToolService'; +import { registerTypes } from '../../../client/common/process/serviceRegistry'; +import { + IBufferDecoder, + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonToolExecutionService +} from '../../../client/common/process/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Common Process Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify(serviceManager.addSingleton<IBufferDecoder>(IBufferDecoder, BufferDecoder)).once(); + verify( + serviceManager.addSingleton<IProcessServiceFactory>(IProcessServiceFactory, ProcessServiceFactory) + ).once(); + verify( + serviceManager.addSingleton<IPythonExecutionFactory>(IPythonExecutionFactory, PythonExecutionFactory) + ).once(); + verify( + serviceManager.addSingleton<IPythonToolExecutionService>( + IPythonToolExecutionService, + PythonToolExecutionService + ) + ).once(); + }); +}); diff --git a/src/test/common/randomWords.txt b/src/test/common/randomWords.txt new file mode 100644 index 000000000000..56066eaa9576 --- /dev/null +++ b/src/test/common/randomWords.txt @@ -0,0 +1,2000 @@ +screw +passenger +zesty +concerned +rustic +store +disagreeable +own +tranquil +modern +tickle +ceaseless +responsible +exclusive +harass +book +attach +squeak +amount +describe +deer +burst +women +influence +undesirable +jewel +inject +balance +dysfunctional +dog +recess +caption +abusive +hallowed +fabulous +maniacal +sweltering +adventurous +glorious +shut +carpenter +sun +kneel +impartial +ashamed +joke +therapeutic +friendly +wood +comfortable +repeat +pencil +agonizing +pricey +territory +scream +shrill +fry +invite +color +strange +zippy +plate +exist +succinct +wholesale +macabre +jam +cloudy +design +stone +apologise +snotty +ruddy +penitent +ban +eager +marry +neat +stale +angry +historical +park +club +cumbersome +table +kitty +parsimonious +sidewalk +dress +truck +ants +odd +worry +roll +stupid +jeans +desert +drop +nod +disastrous +gate +dreary +twist +plane +sky +piquant +naughty +complete +house +add +fool +hate +owe +stuff +humorous +kill +strip +dust +bump +moldy +separate +chalk +fly +third +guarded +sand +three +structure +tease +dispensable +beneficial +comb +attack +undress +bath +scarecrow +gusty +incredible +quaint +dream +wait +rainy +accept +tan +brass +sad +delay +ducks +joyous +trucks +tidy +redundant +unpack +square +north +belligerent +enthusiastic +utopian +last +zinc +shoe +reminiscent +offbeat +army +help +ear +draconian +religion +spark +yarn +spotty +moaning +polish +bite-sized +crayon +mess up +smile +endurable +nut +pedal +root +synonymous +complete +rotten +obedient +flippant +potato +twist +gratis +fresh +vague +slim +empty +grain +uttermost +warm +violet +harm +dad +crack +strap +animated +detect +aback +death +jail +announce +spooky +watch +wonder +unbecoming +zealous +gentle +quiver +royal +shade +attractive +crazy +live +courageous +zoo +solid +rice +applaud +willing +leather +friend +permit +plant +destroy +typical +tight +change +rabbit +behavior +oil +eyes +malicious +axiomatic +exercise +lunchroom +rod +spot +different +delightful +tire +ragged +juicy +tacky +corn +painstaking +tangible +gigantic +ground +curved +ablaze +messy +thick +truculent +paste +mellow +bashful +recognise +join +pull +obsolete +name +price +mixed +overwrought +plan +lick +five +creature +protect +daily +frequent +cynical +icicle +lock +insidious +rough +grubby +credit +challenge +descriptive +wet +introduce +notice +boil +zip +stop +gamy +star +wine +slap +measure +impossible +realise +concentrate +swim +drink +texture +calm +run +rhetorical +whine +page +mark +confused +ill-informed +diligent +good +ball +pause +befitting +toothbrush +bee +fancy +flower +elegant +rule +deafening +heartbreaking +purple +temper +scrape +plant +number +drain +arm +youthful +shame +snow +chop +event +advertisement +wiry +bikes +bat +mate +coach +nifty +parallel +degree +romantic +wanting +battle +meaty +full +unit +blade +wrestle +hook +wakeful +foolish +place +gaze +precede +volatile +replace +chivalrous +adjustment +idea +agree +eye +skinny +reward +grandfather +apparatus +pat +private +square +chief +brick +bomb +bulb +melt +form +snails +tent +giant +treatment +pail +shape +spiky +thoughtful +mean +disillusioned +sophisticated +lively +murky +tank +needle +harbor +gaping +subdued +momentous +dirty +married +secretive +frightened +easy +consider +scarce +absorbed +hammer +icky +metal +stocking +pathetic +son +car +crook +stiff +look +familiar +quirky +numerous +calendar +green +aunt +aromatic +air +complex +reply +health +current +observation +nation +burly +cannon +regret +listen +rings +door +level +sniff +unsightly +alert +doctor +false +desire +support +hammer +maddening +tasteless +secretary +special +earthy +argue +connection +hurry +smell +tour +cows +room +string +placid +confess +true +hypnotic +meal +caring +sore +swift +cup +fretful +peep +stay +scandalous +disarm +leg +material +arrange +strong +man +scorch +swing +society +quiet +peace +dynamic +flowers +helpful +breath +young +kindhearted +wind +glossy +knot +cooing +vegetable +idiotic +aboard +nutty +near +claim +bite +trick +preserve +mountainous +imagine +uninterested +enter +rat +gainful +prickly +coach +alluring +money +mouth +sip +fear +plantation +conscious +unequaled +jaded +appreciate +shivering +bake +weigh +labored +feigned +straight +end +act +consist +victorious +mountain +sleep +dinosaurs +trip +grate +last +regular +tiresome +whistle +gather +maid +pretend +weather +abandoned +drag +enjoy +foregoing +glove +boundary +weight +smelly +tug +butter +fit +manage +unarmed +steady +sassy +depressed +secret +abject +loud +fear +government +blood +alive +soak +wicked +bright +note +touch +innate +walk +jump +use +supreme +suspect +haunt +lethal +needy +seashore +colour +available +curious +brush +loose +cellar +push +evanescent +trace +cultured +ubiquitous +plot +wild +seemly +enchanting +milky +cure +lake +hospital +evasive +puzzling +woozy +lowly +acoustics +wax +madly +distance +bare +jump +van +decision +cheese +suggest +salt +houses +bury +spray +value +woman +raise +nest +fortunate +pass +efficient +stretch +interest +tray +pop +bounce +aspiring +busy +enormous +porter +yawn +frogs +immense +water +late +size +man +instinctive +whistle +skin +pot +zany +surprise +bubble +seal +nervous +lunch +combative +dazzling +feeble +enchanted +analyse +unique +provide +great +high-pitched +scare +arrive +push +signal +business +approve +steel +eight +common +windy +marked +sloppy +warm +fair +remain +sigh +knowing +frog +oceanic +tub +spectacular +knot +prepare +cover +tremendous +silent +stitch +shock +moon +calculate +representative +gleaming +dramatic +top +freezing +inquisitive +round +knife +oval +pack +fairies +taboo +ad hoc +abundant +unadvised +verse +condemned +tall +tap +confuse +sleet +peck +long +holiday +veil +lucky +produce +cautious +yoke +dear +industrious +present +political +mix +rejoice +lively +river +serve +cars +lush +zebra +loutish +sink +flavor +finger +flowery +yellow +marble +jealous +clip +dashing +pleasant +likeable +difficult +scene +quilt +forgetful +devilish +point +acrid +awake +imaginary +trouble +excuse +mourn +hat +town +puzzled +null +warlike +real +toothpaste +sleepy +lopsided +clumsy +uneven +lonely +harmonious +hospitable +temporary +avoid +trade +rock +deadpan +stranger +request +acidic +bone +actor +chilly +wheel +tie +rub +wall +chew +grab +clear +splendid +ghost +attraction +board +tip +aquatic +shop +orange +agreeable +branch +glass +line +increase +hulking +order +decorous +basketball +spot +monkey +cloistered +dust +ocean +scientific +camp +minor +skillful +worm +mom +divergent +pick +meeting +turn +expert +holistic +grey +sort +blushing +tart +touch +rainstorm +crush +field +upbeat +tangy +wish +handy +spotless +steep +afford +salty +snobbish +groan +uptight +question +drain +glistening +discover +wash +unkempt +funny +waste +fix +poison +allow +decisive +arrogant +robust +elastic +turkey +red +calculating +edge +old-fashioned +inexpensive +ticket +route +wail +dry +basin +squeamish +frighten +cart +elderly +murder +lavish +best +brown +glow +slave +tail +towering +scale +flame +alert +please +slope +direful +cactus +second-hand +macho +prevent +release +quarter +excite +party +trousers +test +defeated +long +throne +irritating +zoom +chase +afternoon +suffer +train +balance +station +force +smile +elfin +breakable +cheat +toes +tame +cute +medical +shallow +well-to-do +flimsy +smoke +blue-eyed +cloth +notebook +lettuce +ray +low +productive +wobble +existence +cow +wink +energetic +disappear +boy +lamp +distinct +illegal +addicted +aboriginal +yam +clean +black +hate +plug +comparison +rush +next +volleyball +distribution +sneaky +concern +precious +self +building +wound +psychotic +flap +same +swanky +quixotic +jail +oven +jumbled +past +spill +home +drown +impulse +imminent +sweet +helpless +didactic +turn +savory +ski +opposite +plant +stop +mint +wandering +appliance +repair +fluffy +eminent +lewd +physical +pig +regret +stomach +extra-small +stain +ugliest +cake +separate +partner +water +ring +end +envious +futuristic +itch +rifle +unite +puny +horn +reading +embarrassed +halting +channel +watery +wonderful +gruesome +point +shocking +relax +subtract +economic +luxuriant +parcel +radiate +wary +rich +stare +wasteful +six +nasty +quick +creepy +highfalutin +spotted +cobweb +explode +subsequent +blink +activity +plough +report +rabid +eggs +bouncy +quarrelsome +produce +street +spy +frantic +steadfast +strengthen +head +sour +unused +matter +jazzy +slow +tearful +nebulous +accidental +wool +impolite +simplistic +quicksand +spoil +meat +intelligent +scary +scarf +permissible +command +kettle +grade +animal +purring +crash +annoying +vein +duck +elbow +step +slippery +juvenile +war +ignorant +fuel +pigs +smash +peaceful +astonishing +lock +questionable +obtainable +stupendous +income +hour +love +sick +rate +compete +bent +servant +melted +blind +thing +obeisant +flesh +coherent +wooden +arch +cause +flow +understood +earn +gifted +wave +straw +skirt +boring +extra-large +daffy +detailed +tired +dogs +blue +possible +fish +makeshift +attack +bang +peel +magic +debonair +receive +orange +wise +ill-fated +striped +nail +belief +furniture +group +motionless +sugar +surround +tested +coal +question +well-off +squeal +waggish +wrist +actually +trains +bruise +show +ill +equal +earth +volcano +rambunctious +unusual +pocket +language +thought +parched +camera +pastoral +aggressive +land +learn +hurried +quizzical +bit +ignore +front +fade +discussion +mice +ambitious +abaft +suit +various +ink +dance +reduce +screeching +apparel +delicate +faithful +decide +low +staking +probable +curve +delicious +trade +drab +steer +argument +collect +sheep +anxious +search +brake +card +squealing +sprout +amazing +tree +farm +narrow +tense +books +gullible +alike +pumped +melodic +satisfying +shop +improve +button +irate +big +thin +drop +curl +umbrella +talk +marvelous +level +stir +tenuous +yummy +arithmetic +overt +pull +gray +large +rule +teeny-tiny +shelter +scared +judge +wacky +cakes +merciful +testy +shave +limping +power +abiding +bless +dapper +internal +division +taste +donkey +airplane +dolls +ethereal +spiteful +smoke +bear +knowledgeable +like +delight +picayune +toe +apathetic +wealthy +sponge +sail +crow +slip +loss +weak +pointless +queen +ship +letters +pollution +upset +aberrant +yard +bumpy +pin +pushy +waste +expand +vacuous +fierce +determined +discreet +lip +paint +stingy +vest +amusing +two +nappy +hungry +wilderness +offer +kindly +connect +employ +neighborly +dare +open +planes +cat +office +voyage +float +festive +cracker +adaptable +ludicrous +omniscient +guiltless +heavenly +even +name +appear +crowded +homely +kaput +stick +spiffy +classy +disgusting +heat +thirsty +nimble +invincible +shiny +paper +songs +able +tasteful +open +mighty +chemical +trot +flag +sincere +wren +known +tempt +afraid +squirrel +exultant +ordinary +quill +sound +thunder +haircut +lame +beef +airport +cut +vigorous +boat +prefer +disagree +race +bubble +sore +famous +baby +accessible +tumble +callous +whirl +rob +lackadaisical +view +bike +seed +mother +jar +used +risk +move +yell +groovy +vast +protest +normal +wide-eyed +paddle +bell +charming +nerve +delirious +overconfident +teeny +choke +pleasure +elite +capricious +sin +snore +mine +lie +call +resolute +bathe +dry +dock +careful +program +birds +mere +neck +second +scratch +spiritual +little +x-ray +greasy +cattle +ripe +property +snakes +crooked +aware +cooperative +plastic +observant +expansion +sedate +class +geese +first +industry +knee +change +hard-to-find +intend +icy +scent +obsequious +hum +form +happy +relation +detail +person +science +reign +addition +shade +possess +mysterious +sister +teeth +remember +telling +outstanding +repulsive +soothe +succeed +scrub +rebel +morning +crawl +hobbies +alleged +middle +old +absurd +nose +polite +anger +erratic +part +memory +alcoholic +picture +vanish +small +fire +mass +obscene +tendency +daughter +decay +drunk +rain +muddle +sudden +hover +pen +poor +embarrass +judge +carriage +cool +land +cheap +error +damage +periodic +thumb +guitar +engine +waiting +fertile +unaccountable +correct +fetch +skip +base +educate +nonchalant +racial +double +continue +painful +type +cave +steam +roasted +clean +cycle +borrow +rapid +automatic +bait +tin +saw +development +walk +suggestion +judicious +time +bird +clap +deeply +inconclusive +vulgar +cast +sneeze +bleach +nosy +explain +settle +military +trashy +ruthless +cemetery +book +cluttered +pets +unable +mark +thoughtless +fork +thankful +foamy +seat +smell +writing +eggnog +care +shaky +breezy +unruly +lying +chunky +hope +brother +shirt +panoramic +truthful +education +condition +psychedelic +extend +deliver +miniature +rain +oatmeal +voiceless +hot +mammoth +finger +empty +smart +guide +direction +gorgeous +position +friends +trap +zonked +oranges +adhesive +order +boundless +public +telephone +fascinated +noxious +rhythm +zephyr +tongue +organic +tense +knowledge +fold +vengeful +authority +faulty +head +dusty +bow +ambiguous +sneeze +broken +sharp +spell +poised +egg +fragile +stamp +company +load +ancient +somber +believe +fearless +thread +kick +compare +beam +interest +sordid +hard +infamous +impress +earthquake +action +ready +superficial +contain +spring +colorful +humdrum +certain +tricky +bitter +scatter +laugh +greedy +silly +join +prick +four +crate +jittery +bead +giraffe +whip +kick +needless +rinse +rot +history +roll +boot +hellish +instrument +object +lovely +tame +trite +majestic +rescue +superb +ten +frail +stage +spicy +crib +brake +pies +sign +flood +gun +trust +preach +ugly +abrupt +unhealthy +wave +drawer +grass +bloody +shock +hanging +versed +window +workable +suit +sulky +mindless +few +disgusted +achiever +art +verdant +lacking +flagrant +materialistic +grandmother +frame +save +thrill +tiny +reflect +nonstop +jog +wrathful +advise +righteous +massive +numberless +magnificent +cheerful +left +protective +talk +lace +nauseating +fearful +month +obnoxious +selfish +soda +plain +meddle +can +absorbing +rock +hollow +weary +cable +beautiful +awesome +glib +harmony +frightening +ladybug +occur +abhorrent +dress +powder +example +carry +experience +dizzy +noise +mushy +baseball +cross +jelly +heavy +hose +entertaining +store +moan +ahead +changeable +unknown +drum +hand +pale +mature +work +grip +control +grape +jam +sweater +nippy +muddled +lazy +whole +useless +start +fast +advice +simple +want +tremble +many +learned +terrific +bag +symptomatic +pray +tiger +outrageous +theory +resonant +sack +hushed +hysterical +match +care +support +cabbage +beginner +committee +voracious +spurious +miss +silky +profit +whisper +noisy +thundering +horse +tacit +sail +scissors +thaw +domineering +trouble +box +discovery +childlike +cuddly +perpetual +husky +fruit +scold +elated +godly +guarantee +nutritious +hesitant +doubt +cherries +curly +cough +move +bottle +clear +ratty +stretch +stormy +overflow +puffy +tick +harsh +female +test +illustrious +expensive +muscle +attend +stereotyped +payment +deep +afterthought +pear +quiet +launch +suppose +examine +worried +selective +flower +motion +divide +wriggle +warn +flashy +hateful +milk +hideous +post +unbiased +rural +remind +transport +fancy +list +day +reaction +thinkable +absent +grieving +increase +cream +thank +interrupt +bewildered +aftermath +misty +mind +grease +cover +overjoyed +develop +deceive +growth +treat +complain +pine +wish +twig +box +heady +hall +previous +liquid +aloof +dull +trees +present +wipe +key +jobless +careless +week +mute +curvy +imported +need +puncture +whip +title +finicky +pancake +unwritten +suck +acceptable +valuable +play +quack +wretched +magenta +shoes +wry +vacation +deserve +coil +grotesque +wide +fixed +womanly +rare +wire +heap +badge +honorable +irritate +bawdy +supply +sheet +erect +frame +hilarious +colossal +bed +girl +pet +crabby +cry +deranged +wistful +plucky +pump +cold +shake +satisfy +safe +handsomely +faded +follow +serious +dangerous +insect +annoy +loaf +soap +taste +mitten +lyrical +substantial +fog +wrench +destruction +lighten +wrap +soggy +hot +terrible +bedroom +fanatical +receipt diff --git a/src/test/common/serviceRegistry.unit.test.ts b/src/test/common/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..d40edb5e5378 --- /dev/null +++ b/src/test/common/serviceRegistry.unit.test.ts @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable: no-any + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { DebugService } from '../../client/common/application/debugService'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { Extensions } from '../../client/common/application/extensions'; +import { LanguageService } from '../../client/common/application/languageService'; +import { TerminalManager } from '../../client/common/application/terminalManager'; +import { + IActiveResourceService, + IApplicationEnvironment, + IApplicationShell, + ICommandManager, + IDebugService, + IDocumentManager, + ILanguageService, + ILiveShareApi, + ITerminalManager, + IWorkspaceService +} from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { CryptoUtils } from '../../client/common/crypto'; +import { EditorUtils } from '../../client/common/editor'; +import { ExperimentsManager } from '../../client/common/experiments/manager'; +import { FeatureDeprecationManager } from '../../client/common/featureDeprecationManager'; +import { + ExtensionInsidersDailyChannelRule, + ExtensionInsidersOffChannelRule, + ExtensionInsidersWeeklyChannelRule +} from '../../client/common/insidersBuild/downloadChannelRules'; +import { ExtensionChannelService } from '../../client/common/insidersBuild/downloadChannelService'; +import { InsidersExtensionPrompt } from '../../client/common/insidersBuild/insidersExtensionPrompt'; +import { InsidersExtensionService } from '../../client/common/insidersBuild/insidersExtensionService'; +import { + ExtensionChannel, + IExtensionChannelRule, + IExtensionChannelService, + IInsiderExtensionPrompt +} from '../../client/common/insidersBuild/types'; +import { ProductInstaller } from '../../client/common/installer/productInstaller'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; +import { BrowserService } from '../../client/common/net/browser'; +import { HttpClient } from '../../client/common/net/httpClient'; +import { NugetService } from '../../client/common/nuget/nugetService'; +import { INugetService } from '../../client/common/nuget/types'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { PathUtils } from '../../client/common/platform/pathUtils'; +import { CurrentProcess } from '../../client/common/process/currentProcess'; +import { registerTypes } from '../../client/common/serviceRegistry'; +import { TerminalActivator } from '../../client/common/terminal/activator'; +import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; +import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; +import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; +import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; +import { TerminalServiceFactory } from '../../client/common/terminal/factory'; +import { TerminalHelper } from '../../client/common/terminal/helper'; +import { SettingsShellDetector } from '../../client/common/terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalActivationHandler, + ITerminalActivator, + ITerminalHelper, + ITerminalServiceFactory, + TerminalActivationProviders +} from '../../client/common/terminal/types'; +import { + IAsyncDisposableRegistry, + IBrowserService, + IConfigurationService, + ICryptoUtils, + ICurrentProcess, + IEditorUtils, + IExperimentsManager, + IExtensions, + IFeatureDeprecationManager, + IHttpClient, + IInstaller, + IInterpreterPathService, + IPathUtils, + IPersistentStateFactory, + IRandom +} from '../../client/common/types'; +import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; +import { Random } from '../../client/common/utils/random'; +import { LiveShareApi } from '../../client/datascience/liveshare/liveshare'; +import { IServiceManager } from '../../client/ioc/types'; +import { ImportTracker } from '../../client/telemetry/importTracker'; +import { IImportTracker } from '../../client/telemetry/types'; + +suite('Common - Service Registry', () => { + test('Registrations', () => { + const serviceManager = typemoq.Mock.ofType<IServiceManager>(); + + [ + [IActiveResourceService, ActiveResourceService], + [IInterpreterPathService, InterpreterPathService], + [IExtensions, Extensions], + [IRandom, Random], + [IPersistentStateFactory, PersistentStateFactory], + [ITerminalServiceFactory, TerminalServiceFactory], + [IPathUtils, PathUtils], + [IApplicationShell, ApplicationShell], + [ICurrentProcess, CurrentProcess], + [IInstaller, ProductInstaller], + [ICommandManager, CommandManager], + [IConfigurationService, ConfigurationService], + [IWorkspaceService, WorkspaceService], + [IDocumentManager, DocumentManager], + [ITerminalManager, TerminalManager], + [IDebugService, DebugService], + [IApplicationEnvironment, ApplicationEnvironment], + [ILanguageService, LanguageService], + [IBrowserService, BrowserService], + [IHttpClient, HttpClient], + [IEditorUtils, EditorUtils], + [INugetService, NugetService], + [ITerminalActivator, TerminalActivator], + [ITerminalActivationHandler, PowershellTerminalActivationFailedHandler], + [ILiveShareApi, LiveShareApi], + [ICryptoUtils, CryptoUtils], + [IExperimentsManager, ExperimentsManager], + [ITerminalHelper, TerminalHelper], + [ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, TerminalActivationProviders.pyenv], + [ITerminalActivationCommandProvider, Bash, TerminalActivationProviders.bashCShellFish], + [ + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell + ], + [ITerminalActivationCommandProvider, CondaActivationCommandProvider, TerminalActivationProviders.conda], + [ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv], + [IFeatureDeprecationManager, FeatureDeprecationManager], + [IAsyncDisposableRegistry, AsyncDisposableRegistry], + [IMultiStepInputFactory, MultiStepInputFactory], + [IImportTracker, ImportTracker], + [IShellDetector, TerminalNameShellDetector], + [IShellDetector, SettingsShellDetector], + [IShellDetector, UserEnvironmentShellDetector], + [IShellDetector, VSCEnvironmentShellDetector], + [IInsiderExtensionPrompt, InsidersExtensionPrompt], + [IExtensionSingleActivationService, InsidersExtensionService], + [IExtensionChannelService, ExtensionChannelService], + [IExtensionChannelRule, ExtensionInsidersOffChannelRule, ExtensionChannel.off], + [IExtensionChannelRule, ExtensionInsidersDailyChannelRule, ExtensionChannel.daily], + [IExtensionChannelRule, ExtensionInsidersWeeklyChannelRule, ExtensionChannel.weekly] + ].forEach((mapping) => { + if (mapping.length === 2) { + serviceManager + .setup((s) => + s.addSingleton( + typemoq.It.isValue(mapping[0] as any), + typemoq.It.is((value) => mapping[1] === value) + ) + ) + .verifiable(typemoq.Times.atLeastOnce()); + } else { + serviceManager + .setup((s) => + s.addSingleton( + typemoq.It.isValue(mapping[0] as any), + typemoq.It.isAny(), + typemoq.It.isValue(mapping[2] as any) + ) + ) + .callback((_, cls) => expect(cls).to.equal(mapping[1])) + .verifiable(typemoq.Times.once()); + } + }); + + registerTypes(serviceManager.object); + serviceManager.verifyAll(); + }); +}); diff --git a/src/test/common/socketCallbackHandler.test.ts b/src/test/common/socketCallbackHandler.test.ts new file mode 100644 index 000000000000..70b5b3f04c74 --- /dev/null +++ b/src/test/common/socketCallbackHandler.test.ts @@ -0,0 +1,341 @@ +// tslint:disable:no-any max-classes-per-file max-func-body-length no-stateless-class no-require-imports no-var-requires no-empty + +import { expect } from 'chai'; +import * as getFreePort from 'get-port'; +import * as net from 'net'; +import { SocketCallbackHandler } from '../../client/common/net/socket/socketCallbackHandler'; +import { SocketServer } from '../../client/common/net/socket/socketServer'; +import { SocketStream } from '../../client/common/net/socket/SocketStream'; +import { createDeferred, Deferred } from '../../client/common/utils/async'; + +const uint64be = require('uint64be'); + +// tslint:disable-next-line:no-unnecessary-class +class Commands { + public static ExitCommandBytes: Buffer = new Buffer('exit'); + public static PingBytes: Buffer = new Buffer('ping'); +} + +namespace ResponseCommands { + export const Pong = 'PONG'; + export const ListKernels = 'LSTK'; + export const Error = 'EROR'; +} + +const GUID = 'This is the Guid'; +const PID = 1234; + +class MockSocketCallbackHandler extends SocketCallbackHandler { + private pid?: number; + private guid?: string; + constructor(socketServer: SocketServer) { + super(socketServer); + this.registerCommandHandler(ResponseCommands.Pong, this.onPong.bind(this)); + this.registerCommandHandler(ResponseCommands.Error, this.onError.bind(this)); + } + public ping(message: string) { + this.SendRawCommand(Commands.PingBytes); + + const stringBuffer = new Buffer(message); + const buffer = Buffer.concat([ + Buffer.concat([new Buffer('U'), uint64be.encode(stringBuffer.byteLength)]), + stringBuffer + ]); + this.stream.Write(buffer); + } + protected handleHandshake(): boolean { + if (!this.guid) { + this.guid = this.stream.readStringInTransaction(); + if (typeof this.guid !== 'string') { + return false; + } + } + + if (!this.pid) { + this.pid = this.stream.readInt32InTransaction(); + if (typeof this.pid !== 'number') { + return false; + } + } + + if (this.guid !== GUID) { + this.emit('error', this.guid, GUID, 'Guids not the same'); + return true; + } + if (this.pid !== PID) { + this.emit('error', this.pid, PID, 'pids not the same'); + return true; + } + + this.emit('handshake'); + return true; + } + private onError() { + const message = this.stream.readStringInTransaction(); + if (typeof message !== 'string') { + return; + } + this.emit('error', '', '', message); + } + private onPong() { + const message = this.stream.readStringInTransaction(); + if (typeof message !== 'string') { + return; + } + this.emit('pong', message); + } +} +class MockSocketClient { + private socket?: net.Socket; + private socketStream?: SocketStream; + private def?: Deferred<any>; + constructor(private port: number) {} + public get SocketStream(): SocketStream { + if (this.socketStream === undefined) { + throw Error('not listening'); + } + return this.socketStream; + } + public start(): Promise<any> { + this.def = createDeferred<any>(); + this.socket = net.connect(this.port as any, this.connectionListener.bind(this)); + return this.def.promise; + } + private connectionListener() { + if (this.socket === undefined || this.def === undefined) { + throw Error('not started'); + } + this.socketStream = new SocketStream(this.socket, new Buffer('')); + this.def.resolve(); + this.socket.on('error', () => {}); + this.socket.on('data', (data: Buffer) => { + try { + this.SocketStream.Append(data); + // We can only receive ping messages + this.SocketStream.BeginTransaction(); + const cmdIdBytes: number[] = []; + for (let counter = 0; counter < 4; counter += 1) { + const byte = this.SocketStream.ReadByte(); + if (typeof byte !== 'number') { + this.SocketStream.RollBackTransaction(); + return; + } + cmdIdBytes.push(byte); + } + const cmdId = new Buffer(cmdIdBytes).toString(); + const message = this.SocketStream.ReadString(); + if (typeof message !== 'string') { + this.SocketStream.RollBackTransaction(); + return; + } + + this.SocketStream.EndTransaction(); + + if (cmdId !== 'ping') { + this.SocketStream.Write(new Buffer(ResponseCommands.Error)); + + const errorMessage = `Received unknown command '${cmdId}'`; + const errorBuffer = Buffer.concat([ + Buffer.concat([new Buffer('A'), uint64be.encode(errorMessage.length)]), + new Buffer(errorMessage) + ]); + this.SocketStream.Write(errorBuffer); + return; + } + + this.SocketStream.Write(new Buffer(ResponseCommands.Pong)); + + const messageBuffer = new Buffer(message); + const pongBuffer = Buffer.concat([ + Buffer.concat([new Buffer('U'), uint64be.encode(messageBuffer.byteLength)]), + messageBuffer + ]); + this.SocketStream.Write(pongBuffer); + } catch (ex) { + this.SocketStream.Write(new Buffer(ResponseCommands.Error)); + + const errorMessage = `Fatal error in handling data at socket client. Error: ${ex.message}`; + const errorBuffer = Buffer.concat([ + Buffer.concat([new Buffer('A'), uint64be.encode(errorMessage.length)]), + new Buffer(errorMessage) + ]); + this.SocketStream.Write(errorBuffer); + } + }); + } +} + +// Defines a Mocha test suite to group tests of similar kind together +suite('SocketCallbackHandler', () => { + let socketServer: SocketServer; + setup(() => (socketServer = new SocketServer())); + teardown(() => socketServer.Stop()); + + test('Succesfully starts without any specific host or port', async () => { + const port = await socketServer.Start(); + expect(port).to.be.greaterThan(0); + }); + test('Succesfully starts with port=0 and no host', async () => { + const port = await socketServer.Start({ port: 0 }); + expect(port).to.be.greaterThan(0); + }); + test('Succesfully starts with port=0 and host=localhost', async () => { + const port = await socketServer.Start({ port: 0, host: 'localhost' }); + expect(port).to.be.greaterThan(0); + }); + test('Succesfully starts with host=127.0.0.1', async () => { + const port = await socketServer.Start({ host: '127.0.0.1' }); + expect(port).to.be.greaterThan(0); + }); + test('Succesfully starts with port=0 and host=127.0.0.1', async () => { + const port = await socketServer.Start({ port: 0, host: '127.0.0.1' }); + expect(port).to.be.greaterThan(0); + }); + test('Succesfully starts with specific port', async () => { + const availablePort = await getFreePort({ host: 'localhost' }); + const port = await socketServer.Start({ port: availablePort, host: 'localhost' }); + expect(port).to.be.equal(availablePort); + }); + test('Succesful Handshake', async () => { + const port = await socketServer.Start(); + const callbackHandler = new MockSocketCallbackHandler(socketServer); + const socketClient = new MockSocketClient(port); + await socketClient.start(); + const def = createDeferred<any>(); + + callbackHandler.on('handshake', () => { + def.resolve(); + }); + callbackHandler.on('error', (actual: string, expected: string, message: string) => { + if (!def.completed) { + def.reject({ actual: actual, expected: expected, message: message }); + } + }); + + // Client has connected, now send information to the callback handler via sockets + const guidBuffer = Buffer.concat([new Buffer('A'), uint64be.encode(GUID.length), new Buffer(GUID)]); + socketClient.SocketStream.Write(guidBuffer); + socketClient.SocketStream.WriteInt32(PID); + await def.promise; + }); + test('Unsuccesful Handshake', async () => { + const port = await socketServer.Start(); + const callbackHandler = new MockSocketCallbackHandler(socketServer); + const socketClient = new MockSocketClient(port); + await socketClient.start(); + + const def = createDeferred<any>(); + let timeOut: NodeJS.Timer | undefined | number = setTimeout(() => { + def.reject('Handshake not completed in allocated time'); + }, 5000); + + callbackHandler.on('handshake', () => { + if (timeOut) { + clearTimeout(timeOut as any); + timeOut = undefined; + } + def.reject('handshake should fail, but it succeeded!'); + }); + callbackHandler.on('error', (actual: string | number, expected: string, message: string) => { + if (timeOut) { + clearTimeout(timeOut as any); + timeOut = undefined; + } + if (actual === 0 && message === 'pids not the same') { + def.resolve(); + } else { + def.reject({ actual: actual, expected: expected, message: message }); + } + }); + + // Client has connected, now send information to the callback handler via sockets + const guidBuffer = Buffer.concat([new Buffer('A'), uint64be.encode(GUID.length), new Buffer(GUID)]); + socketClient.SocketStream.Write(guidBuffer); + + // Send the wrong pid + socketClient.SocketStream.WriteInt32(0); + await def.promise; + }); + test('Ping with message', async () => { + const port = await socketServer.Start(); + const callbackHandler = new MockSocketCallbackHandler(socketServer); + const socketClient = new MockSocketClient(port); + await socketClient.start(); + + const def = createDeferred<any>(); + const PING_MESSAGE = 'This is the Ping Message - Функция проверки ИНН и КПП - 说明'; + + callbackHandler.on('handshake', () => { + // Send a custom message (only after handshake has been done) + callbackHandler.ping(PING_MESSAGE); + }); + callbackHandler.on('pong', (message: string) => { + try { + expect(message).to.be.equal(PING_MESSAGE); + def.resolve(); + } catch (ex) { + def.reject(ex); + } + }); + callbackHandler.on('error', (actual: string, expected: string, message: string) => { + if (!def.completed) { + def.reject({ actual: actual, expected: expected, message: message }); + } + }); + + // Client has connected, now send information to the callback handler via sockets + const guidBuffer = Buffer.concat([new Buffer('A'), uint64be.encode(GUID.length), new Buffer(GUID)]); + socketClient.SocketStream.Write(guidBuffer); + + // Send the wrong pid + socketClient.SocketStream.WriteInt32(PID); + await def.promise; + }); + test('Succesful Handshake with port=0 and host=localhost', async () => { + const port = await socketServer.Start({ port: 0, host: 'localhost' }); + const callbackHandler = new MockSocketCallbackHandler(socketServer); + const socketClient = new MockSocketClient(port); + await socketClient.start(); + + const def = createDeferred<any>(); + + callbackHandler.on('handshake', () => def.resolve()); + callbackHandler.on('error', (actual: string, expected: string, message: string) => { + if (!def.completed) { + def.reject({ actual: actual, expected: expected, message: message }); + } + }); + + // Client has connected, now send information to the callback handler via sockets + const guidBuffer = Buffer.concat([new Buffer('A'), uint64be.encode(GUID.length), new Buffer(GUID)]); + socketClient.SocketStream.Write(guidBuffer); + socketClient.SocketStream.WriteInt32(PID); + await def.promise; + }); + test('Succesful Handshake with specific port', async () => { + const availablePort = await new Promise<number>((resolve, reject) => + getFreePort({ host: 'localhost' }).then(resolve, reject) + ); + const port = await socketServer.Start({ port: availablePort, host: 'localhost' }); + + expect(port).to.be.equal(availablePort, 'Server is not listening on the provided port number'); + const callbackHandler = new MockSocketCallbackHandler(socketServer); + const socketClient = new MockSocketClient(port); + await socketClient.start(); + + const def = createDeferred<any>(); + + callbackHandler.on('handshake', () => def.resolve()); + callbackHandler.on('error', (actual: string, expected: string, message: string) => { + if (!def.completed) { + def.reject({ actual: actual, expected: expected, message: message }); + } + }); + + // Client has connected, now send information to the callback handler via sockets + const guidBuffer = Buffer.concat([new Buffer('A'), uint64be.encode(GUID.length), new Buffer(GUID)]); + socketClient.SocketStream.Write(guidBuffer); + socketClient.SocketStream.WriteInt32(PID); + await def.promise; + }); +}); diff --git a/src/test/common/socketStream.test.ts b/src/test/common/socketStream.test.ts new file mode 100644 index 000000000000..13d3082dcd96 --- /dev/null +++ b/src/test/common/socketStream.test.ts @@ -0,0 +1,188 @@ +// +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. +// + +// Place this right on top +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as net from 'net'; +import { SocketStream } from '../../client/common/net/socket/SocketStream'; +// tslint:disable:no-require-imports no-var-requires +const uint64be = require('uint64be'); + +class MockSocket { + private _data: string; + // tslint:disable-next-line:no-any + private _rawDataWritten: any; + constructor() { + this._data = ''; + } + public get dataWritten(): string { + return this._data; + } + // tslint:disable-next-line:no-any + public get rawDataWritten(): any { + return this._rawDataWritten; + } + // tslint:disable-next-line:no-any + public write(data: any) { + this._data = `${data}` + ''; + this._rawDataWritten = data; + } +} +// Defines a Mocha test suite to group tests of similar kind together +// tslint:disable-next-line:max-func-body-length +suite('SocketStream', () => { + test('Read Byte', (done) => { + const buffer = new Buffer('X'); + const byteValue = buffer[0]; + const socket = new MockSocket(); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + + assert.equal(stream.ReadByte(), byteValue); + done(); + }); + test('Read Int32', (done) => { + const num = 1234; + const socket = new MockSocket(); + const buffer = uint64be.encode(num); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + + assert.equal(stream.ReadInt32(), num); + done(); + }); + test('Read Int64', (done) => { + const num = 9007199254740993; + const socket = new MockSocket(); + const buffer = uint64be.encode(num); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + + assert.equal(stream.ReadInt64(), num); + done(); + }); + test('Read Ascii String', (done) => { + const message = 'Hello World'; + const socket = new MockSocket(); + const buffer = Buffer.concat([new Buffer('A'), uint64be.encode(message.length), new Buffer(message)]); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + + assert.equal(stream.ReadString(), message); + done(); + }); + test('Read Unicode String', (done) => { + const message = 'Hello World - Функция проверки ИНН и КПП - 说明'; + const socket = new MockSocket(); + const stringBuffer = new Buffer(message); + const buffer = Buffer.concat([ + Buffer.concat([new Buffer('U'), uint64be.encode(stringBuffer.byteLength)]), + stringBuffer + ]); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + + assert.equal(stream.ReadString(), message); + done(); + }); + test('Read RollBackTransaction', (done) => { + const message = 'Hello World'; + const socket = new MockSocket(); + let buffer = Buffer.concat([new Buffer('A'), uint64be.encode(message.length), new Buffer(message)]); + + // Write part of a second message + const partOfSecondMessage = Buffer.concat([new Buffer('A'), uint64be.encode(message.length)]); + buffer = Buffer.concat([buffer, partOfSecondMessage]); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + + stream.BeginTransaction(); + assert.equal(stream.ReadString(), message, 'First message not read properly'); + stream.ReadString(); + assert.equal(stream.HasInsufficientDataForReading, true, 'Should not have sufficient data for reading'); + stream.RollBackTransaction(); + assert.equal(stream.ReadString(), message, 'First message not read properly after rolling back transaction'); + done(); + }); + test('Read EndTransaction', (done) => { + const message = 'Hello World'; + const socket = new MockSocket(); + let buffer = Buffer.concat([new Buffer('A'), uint64be.encode(message.length), new Buffer(message)]); + + // Write part of a second message + const partOfSecondMessage = Buffer.concat([new Buffer('A'), uint64be.encode(message.length)]); + buffer = Buffer.concat([buffer, partOfSecondMessage]); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + + stream.BeginTransaction(); + assert.equal(stream.ReadString(), message, 'First message not read properly'); + stream.ReadString(); + assert.equal(stream.HasInsufficientDataForReading, true, 'Should not have sufficient data for reading'); + stream.EndTransaction(); + stream.RollBackTransaction(); + assert.notEqual(stream.ReadString(), message, 'First message cannot be read after commit transaction'); + done(); + }); + test('Write Buffer', (done) => { + const message = 'Hello World'; + const buffer = new Buffer(''); + const socket = new MockSocket(); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + stream.Write(new Buffer(message)); + + assert.equal(socket.dataWritten, message); + done(); + }); + test('Write Int32', (done) => { + const num = 1234; + const buffer = new Buffer(''); + const socket = new MockSocket(); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + stream.WriteInt32(num); + + assert.equal(uint64be.decode(socket.rawDataWritten), num); + done(); + }); + test('Write Int64', (done) => { + const num = 9007199254740993; + const buffer = new Buffer(''); + const socket = new MockSocket(); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + stream.WriteInt64(num); + + assert.equal(uint64be.decode(socket.rawDataWritten), num); + done(); + }); + test('Write Ascii String', (done) => { + const message = 'Hello World'; + const buffer = new Buffer(''); + const socket = new MockSocket(); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + stream.WriteString(message); + + assert.equal(socket.dataWritten, message); + done(); + }); + test('Write Unicode String', (done) => { + const message = 'Hello World - Функция проверки ИНН и КПП - 说明'; + const buffer = new Buffer(''); + const socket = new MockSocket(); + // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); + stream.WriteString(message); + + assert.equal(socket.dataWritten, message); + done(); + }); +}); diff --git a/src/test/common/terminals/activation.bash.unit.test.ts b/src/test/common/terminals/activation.bash.unit.test.ts new file mode 100644 index 000000000000..b14415e3aab2 --- /dev/null +++ b/src/test/common/terminals/activation.bash.unit.test.ts @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import '../../../client/common/extensions'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; +import { TerminalShellType } from '../../../client/common/terminal/types'; +import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable: max-func-body-length + +suite('Terminal Environment Activation (bash)', () => { + [ + 'usr/bin/python', + 'usr/bin/env with spaces/env more/python', + 'c:\\users\\windows paths\\conda\\python.exe' + ].forEach((pythonPath) => { + const hasSpaces = pythonPath.indexOf(' ') > 0; + const suiteTitle = hasSpaces + ? 'and there are spaces in the script file (pythonpath),' + : 'and there are no spaces in the script file (pythonpath),'; + suite(suiteTitle, () => { + ['activate', 'activate.sh', 'activate.csh', 'activate.fish', 'activate.bat', 'Activate.ps1'].forEach( + (scriptFileName) => { + suite(`and script file is ${scriptFileName}`, () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + serviceContainer.setup((c) => c.get(IFileSystem)).returns(() => fileSystem.object); + + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + configService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns(() => settings.object); + }); + + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { + let isScriptFileSupported = false; + switch (shellType.value) { + case TerminalShellType.zsh: + case TerminalShellType.ksh: + case TerminalShellType.wsl: + case TerminalShellType.gitbash: + case TerminalShellType.bash: { + isScriptFileSupported = ['activate', 'activate.sh'].indexOf(scriptFileName) >= 0; + break; + } + case TerminalShellType.fish: { + isScriptFileSupported = ['activate.fish'].indexOf(scriptFileName) >= 0; + break; + } + case TerminalShellType.tcshell: + case TerminalShellType.cshell: { + isScriptFileSupported = ['activate.csh'].indexOf(scriptFileName) >= 0; + break; + } + default: { + isScriptFileSupported = false; + } + } + const titleTitle = isScriptFileSupported + ? `Ensure bash Activation command returns activation command (Shell: ${shellType.name})` + : `Ensure bash Activation command returns undefined (Shell: ${shellType.name})`; + + test(titleTitle, async () => { + const bash = new Bash(serviceContainer.object); + + const supported = bash.isShellSupported(shellType.value); + switch (shellType.value) { + case TerminalShellType.wsl: + case TerminalShellType.zsh: + case TerminalShellType.ksh: + case TerminalShellType.bash: + case TerminalShellType.gitbash: + case TerminalShellType.tcshell: + case TerminalShellType.cshell: + case TerminalShellType.fish: { + expect(supported).to.be.equal( + true, + `${shellType.name} shell not supported (it should be)` + ); + break; + } + default: { + expect(supported).to.be.equal( + false, + `${shellType.name} incorrectly supported (should not be)` + ); + // No point proceeding with other tests. + return; + } + } + + const pathToScriptFile = path.join(path.dirname(pythonPath), scriptFileName); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(undefined, shellType.value); + + if (isScriptFileSupported) { + // Ensure the script file is of the following form: + // source "<path to script file>" <environment name> + // Ensure the path is quoted if it contains any spaces. + // Ensure it contains the name of the environment as an argument to the script file. + + expect(command).to.be.deep.equal( + [`source ${pathToScriptFile.fileToCommandArgument()}`.trim()], + 'Invalid command' + ); + } else { + expect(command).to.be.equal(undefined, 'Command should be undefined'); + } + }); + }); + }); + } + ); + }); + }); +}); diff --git a/src/test/common/terminals/activation.commandPrompt.unit.test.ts b/src/test/common/terminals/activation.commandPrompt.unit.test.ts new file mode 100644 index 000000000000..56d1239caf72 --- /dev/null +++ b/src/test/common/terminals/activation.commandPrompt.unit.test.ts @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { TerminalShellType } from '../../../client/common/terminal/types'; +import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Terminal Environment Activation (cmd/powershell)', () => { + [ + 'c:/programfiles/python/python', + 'c:/program files/python/python', + 'c:\\users\\windows paths\\conda\\python.exe' + ].forEach((pythonPath) => { + const hasSpaces = pythonPath.indexOf(' ') > 0; + const resource = Uri.file('a'); + + const suiteTitle = hasSpaces + ? 'and there are spaces in the script file (pythonpath),' + : 'and there are no spaces in the script file (pythonpath),'; + suite(suiteTitle, () => { + ['activate', 'activate.sh', 'activate.csh', 'activate.fish', 'activate.bat', 'Activate.ps1'].forEach( + (scriptFileName) => { + suite(`and script file is ${scriptFileName}`, () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + serviceContainer.setup((c) => c.get(IFileSystem)).returns(() => fileSystem.object); + + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + configService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns(() => settings.object); + }); + + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { + const isScriptFileSupported = ['activate.bat', 'Activate.ps1'].indexOf(scriptFileName) >= 0; + const titleTitle = isScriptFileSupported + ? `Ensure terminal type is supported (Shell: ${shellType.name})` + : `Ensure terminal type is not supported (Shell: ${shellType.name})`; + + test(titleTitle, async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + const supported = bash.isShellSupported(shellType.value); + switch (shellType.value) { + case TerminalShellType.commandPrompt: + case TerminalShellType.powershellCore: + case TerminalShellType.powershell: { + expect(supported).to.be.equal( + true, + `${shellType.name} shell not supported (it should be)` + ); + break; + } + default: { + expect(supported).to.be.equal( + false, + `${shellType.name} incorrectly supported (should not be)` + ); + } + } + }); + }); + }); + } + ); + + suite('and script file is activate.bat', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let platform: TypeMoq.IMock<IPlatformService>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + platform = TypeMoq.Mock.ofType<IPlatformService>(); + serviceContainer.setup((c) => c.get(IFileSystem)).returns(() => fileSystem.object); + serviceContainer.setup((c) => c.get(IPlatformService)).returns(() => platform.object); + + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + }); + + test('Ensure batch files are supported by command prompt', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const commands = await bash.getActivationCommands(resource, TerminalShellType.commandPrompt); + + // Ensure the script file is of the following form: + // source "<path to script file>" <environment name> + // Ensure the path is quoted if it contains any spaces. + // Ensure it contains the name of the environment as an argument to the script file. + + expect(commands).to.be.deep.equal([pathToScriptFile.fileToCommandArgument()], 'Invalid command'); + }); + + test('Ensure batch files are not supported by powershell (on windows)', async () => { + const batch = new CommandPromptAndPowerShell(serviceContainer.object); + + platform.setup((p) => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await batch.getActivationCommands(resource, TerminalShellType.powershell); + + expect(command).to.be.equal(undefined, 'Invalid'); + }); + + test('Ensure batch files are not supported by powershell core (on windows)', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + platform.setup((p) => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); + + expect(command).to.be.equal(undefined, 'Invalid'); + }); + + test('Ensure batch files are not supported by powershell (on non-windows)', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + platform.setup((p) => p.isWindows).returns(() => false); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.powershell); + + expect(command).to.be.equal(undefined, 'Invalid command'); + }); + + test('Ensure batch files are not supported by powershell core (on non-windows)', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + platform.setup((p) => p.isWindows).returns(() => false); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); + + expect(command).to.be.equal(undefined, 'Invalid command'); + }); + }); + + suite('and script file is Activate.ps1', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let platform: TypeMoq.IMock<IPlatformService>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + platform = TypeMoq.Mock.ofType<IPlatformService>(); + serviceContainer.setup((c) => c.get(IFileSystem)).returns(() => fileSystem.object); + serviceContainer.setup((c) => c.get(IPlatformService)).returns(() => platform.object); + + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + }); + + test('Ensure powershell files are not supported by command prompt', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + platform.setup((p) => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'Activate.ps1'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.commandPrompt); + + expect(command).to.be.deep.equal( + [], + 'Invalid command (running powershell files are not supported on command prompt)' + ); + }); + + test('Ensure powershell files are supported by powershell', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + platform.setup((p) => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'Activate.ps1'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.powershell); + + expect(command).to.be.deep.equal( + [`& ${pathToScriptFile.fileToCommandArgument()}`.trim()], + 'Invalid command' + ); + }); + + test('Ensure powershell files are supported by powershell core', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); + + platform.setup((p) => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'Activate.ps1'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); + + expect(command).to.be.deep.equal( + [`& ${pathToScriptFile.fileToCommandArgument()}`.trim()], + 'Invalid command' + ); + }); + }); + }); + }); +}); diff --git a/src/test/common/terminals/activation.conda.unit.test.ts b/src/test/common/terminals/activation.conda.unit.test.ts new file mode 100644 index 000000000000..6d3483cd50e0 --- /dev/null +++ b/src/test/common/terminals/activation.conda.unit.test.ts @@ -0,0 +1,639 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import { parse } from 'semver'; +import { anything, instance, mock, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { Disposable } from 'vscode'; +import { TerminalManager } from '../../../client/common/application/terminalManager'; +import '../../../client/common/extensions'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { CondaActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; +import { PipEnvActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; +import { PyEnvActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; +import { TerminalHelper } from '../../../client/common/terminal/helper'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../../../client/common/terminal/types'; +import { + IConfigurationService, + IDisposableRegistry, + IPythonSettings, + ITerminalSettings +} from '../../../client/common/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { ICondaService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Terminal Environment Activation conda', () => { + let terminalHelper: TerminalHelper; + let disposables: Disposable[] = []; + let terminalSettings: TypeMoq.IMock<ITerminalSettings>; + let platformService: TypeMoq.IMock<IPlatformService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let processService: TypeMoq.IMock<IProcessService>; + let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; + let condaService: TypeMoq.IMock<ICondaService>; + let configService: TypeMoq.IMock<IConfigurationService>; + let conda: string; + let bash: ITerminalActivationCommandProvider; + + setup(() => { + conda = 'conda'; + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + disposables = []; + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) + .returns(() => disposables); + + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + processService = TypeMoq.Mock.ofType<IProcessService>(); + condaService = TypeMoq.Mock.ofType<ICondaService>(); + condaService.setup((c) => c.getCondaFile()).returns(() => Promise.resolve(conda)); + bash = mock(Bash); + + processService.setup((x: any) => x.then).returns(() => undefined); + procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + procServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())) + .returns(() => platformService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) + .returns(() => fileSystem.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())) + .returns(() => procServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICondaService), TypeMoq.It.isAny())) + .returns(() => condaService.object); + + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + + terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>(); + pythonSettings.setup((s) => s.terminal).returns(() => terminalSettings.object); + + terminalHelper = new TerminalHelper( + platformService.object, + instance(mock(TerminalManager)), + condaService.object, + instance(mock(InterpreterService)), + configService.object, + new CondaActivationCommandProvider(condaService.object, platformService.object, configService.object), + instance(bash), + mock(CommandPromptAndPowerShell), + mock(PyEnvActivationCommandProvider), + mock(PipEnvActivationCommandProvider), + [] + ); + }); + teardown(() => { + disposables.forEach((disposable) => { + if (disposable) { + disposable.dispose(); + } + }); + }); + + test('Conda activation for fish escapes spaces in conda filename', async () => { + conda = 'path to conda'; + const envName = 'EnvA'; + const pythonPath = 'python3'; + platformService.setup((p) => p.isWindows).returns(() => false); + condaService + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ name: envName, path: path.dirname(pythonPath) })); + const expected = ['"path to conda" activate EnvA']; + + const provider = new CondaActivationCommandProvider( + condaService.object, + platformService.object, + configService.object + ); + const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.fish); + + expect(activationCommands).to.deep.equal(expected, 'Incorrect Activation command'); + }); + + test('Conda activation on bash uses "source" before 4.4.0', async () => { + const envName = 'EnvA'; + const pythonPath = 'python3'; + const condaPath = path.join('a', 'b', 'c', 'conda'); + platformService.setup((p) => p.isWindows).returns(() => false); + condaService.reset(); + condaService + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + name: envName, + path: path.dirname(pythonPath) + }) + ); + condaService.setup((c) => c.getCondaFile()).returns(() => Promise.resolve(condaPath)); + condaService.setup((c) => c.getCondaVersion()).returns(() => Promise.resolve(parse('4.3.1', true)!)); + const expected = [`source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgument()} EnvA`]; + + const provider = new CondaActivationCommandProvider( + condaService.object, + platformService.object, + configService.object + ); + const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash); + + expect(activationCommands).to.deep.equal(expected, 'Incorrect Activation command'); + }); + + test('Conda activation on bash uses "conda" after 4.4.0', async () => { + const envName = 'EnvA'; + const pythonPath = 'python3'; + const condaPath = path.join('a', 'b', 'c', 'conda'); + platformService.setup((p) => p.isWindows).returns(() => false); + condaService.reset(); + condaService + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + name: envName, + path: path.dirname(pythonPath) + }) + ); + condaService.setup((c) => c.getCondaFile()).returns(() => Promise.resolve(condaPath)); + condaService.setup((c) => c.getCondaVersion()).returns(() => Promise.resolve(parse('4.4.0', true)!)); + const expected = [`source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgument()} EnvA`]; + + const provider = new CondaActivationCommandProvider( + condaService.object, + platformService.object, + configService.object + ); + const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash); + + expect(activationCommands).to.deep.equal(expected, 'Incorrect Activation command'); + }); + + const interpreterPath = path.join('path', 'to', 'interpreter'); + const environmentName = 'Env'; + const environmentNameHasSpaces = 'Env with spaces'; + const testsForActivationUsingInterpreterPath = [ + { + testName: + 'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, with no spaces in env name', + envName: environmentName, + expectedResult: ['path/to/activate', 'conda activate Env'], + isWindows: true + }, + { + testName: + 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, with no spaces in env name', + envName: environmentName, + expectedResult: ['source path/to/activate', 'conda activate Env'], + isWindows: false + }, + { + testName: + 'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, with spaces in env name', + envName: environmentNameHasSpaces, + expectedResult: ['path/to/activate', 'conda activate "Env with spaces"'], + isWindows: true + }, + { + testName: + 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, with spaces in env name', + envName: environmentNameHasSpaces, + expectedResult: ['source path/to/activate', 'conda activate "Env with spaces"'], + isWindows: false + }, + { + testName: + 'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, and no env name', + envName: '', + expectedResult: ['path/to/activate', `conda activate .`], + isWindows: true + }, + { + testName: + 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, and no env name', + envName: '', + expectedResult: ['source path/to/activate', `conda activate .`], + isWindows: false + } + ]; + + testsForActivationUsingInterpreterPath.forEach((testParams) => { + test(testParams.testName, async () => { + const pythonPath = 'python3'; + platformService.setup((p) => p.isWindows).returns(() => testParams.isWindows); + condaService.reset(); + condaService + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + name: testParams.envName, + path: path.dirname(pythonPath) + }) + ); + condaService.setup((c) => c.getCondaVersion()).returns(() => Promise.resolve(parse('4.4.0', true)!)); + condaService + .setup((c) => c.getCondaFileFromInterpreter(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(interpreterPath)); + + const provider = new CondaActivationCommandProvider( + condaService.object, + platformService.object, + configService.object + ); + const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash); + + expect(activationCommands).to.deep.equal(testParams.expectedResult, 'Incorrect Activation command'); + }); + }); + + async function testCondaActivationCommands( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + pythonPath: string, + shellType: TerminalShellType, + envName: string + ) { + platformService.setup((p) => p.isLinux).returns(() => isLinux); + platformService.setup((p) => p.isWindows).returns(() => isWindows); + platformService.setup((p) => p.isMac).returns(() => isOsx); + condaService.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); + condaService + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ name: envName, path: path.dirname(pythonPath) })); + + const activationCommands = await new CondaActivationCommandProvider( + condaService.object, + platformService.object, + configService.object + ).getActivationCommands(undefined, shellType); + let expectedActivationCommand: string[] | undefined; + const expectEnvActivatePath = path.dirname(pythonPath); + switch (shellType) { + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + case TerminalShellType.fish: { + if (envName !== '') { + expectedActivationCommand = [`conda activate ${envName.toCommandArgument()}`]; + } else { + expectedActivationCommand = [`conda activate ${expectEnvActivatePath}`]; + } + break; + } + default: { + if (envName !== '') { + expectedActivationCommand = isWindows + ? [`activate ${envName.toCommandArgument()}`] + : [`source activate ${envName.toCommandArgument()}`]; + } else { + expectedActivationCommand = isWindows + ? [`activate ${expectEnvActivatePath}`] + : [`source activate ${expectEnvActivatePath}`]; + } + break; + } + } + if (expectedActivationCommand) { + expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command'); + } else { + expect(activationCommands).to.equal(undefined, 'Incorrect Activation command'); + } + } + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { + test(`Conda activation command for shell ${shellType.name} on (windows)`, async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); + await testCondaActivationCommands(true, false, false, pythonPath, shellType.value, 'Env'); + }); + + test(`Conda activation command for shell ${shellType.name} on (linux)`, async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await testCondaActivationCommands(false, false, true, pythonPath, shellType.value, 'Env'); + }); + + test(`Conda activation command for shell ${shellType.name} on (mac)`, async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await testCondaActivationCommands(false, true, false, pythonPath, shellType.value, 'Env'); + }); + }); + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { + test(`Conda activation command for shell ${shellType.name} on (windows), containing spaces in environment name`, async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); + await testCondaActivationCommands(true, false, false, pythonPath, shellType.value, 'Env A'); + }); + + test(`Conda activation command for shell ${shellType.name} on (linux), containing spaces in environment name`, async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await testCondaActivationCommands(false, false, true, pythonPath, shellType.value, 'Env A'); + }); + + test(`Conda activation command for shell ${shellType.name} on (mac), containing spaces in environment name`, async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await testCondaActivationCommands(false, true, false, pythonPath, shellType.value, 'Env A'); + }); + }); + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { + test(`Conda activation command for shell ${shellType.name} on (windows), containing no environment name`, async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); + await testCondaActivationCommands(true, false, false, pythonPath, shellType.value, ''); + }); + + test(`Conda activation command for shell ${shellType.name} on (linux), containing no environment name`, async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await testCondaActivationCommands(false, false, true, pythonPath, shellType.value, ''); + }); + + test(`Conda activation command for shell ${shellType.name} on (mac), containing no environment name`, async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await testCondaActivationCommands(false, true, false, pythonPath, shellType.value, ''); + }); + }); + async function expectCondaActivationCommand( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + pythonPath: string + ) { + platformService.setup((p) => p.isLinux).returns(() => isLinux); + platformService.setup((p) => p.isWindows).returns(() => isWindows); + platformService.setup((p) => p.isMac).returns(() => isOsx); + condaService.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); + condaService + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ name: 'EnvA', path: path.dirname(pythonPath) })); + + const expectedActivationCommand = isWindows ? ['activate EnvA'] : ['source activate EnvA']; + const activationCommands = await terminalHelper.getEnvironmentActivationCommands( + TerminalShellType.bash, + undefined + ); + expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command'); + } + + test('If environment is a conda environment, ensure conda activation command is sent (windows)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await expectCondaActivationCommand(true, false, false, pythonPath); + }); + + test('If environment is a conda environment, ensure conda activation command is sent (linux)', async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + fileSystem + .setup((f) => + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))) + ) + .returns(() => Promise.resolve(true)); + await expectCondaActivationCommand(false, false, true, pythonPath); + }); + + test('If environment is a conda environment, ensure conda activation command is sent (osx)', async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + fileSystem + .setup((f) => + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))) + ) + .returns(() => Promise.resolve(true)); + await expectCondaActivationCommand(false, true, false, pythonPath); + }); + + test('Get activation script command if environment is not a conda environment', async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + condaService.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); + + const mockProvider = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); + serviceContainer + .setup((c) => c.getAll(TypeMoq.It.isValue(ITerminalActivationCommandProvider), TypeMoq.It.isAny())) + .returns(() => [mockProvider.object]); + mockProvider.setup((p) => p.isShellSupported(TypeMoq.It.isAny())).returns(() => true); + mockProvider + .setup((p) => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(['mock command'])); + + const expectedActivationCommand = ['mock command']; + when(bash.isShellSupported(anything())).thenReturn(true); + when(bash.getActivationCommands(anything(), TerminalShellType.bash)).thenResolve(expectedActivationCommand); + + const activationCommands = await terminalHelper.getEnvironmentActivationCommands( + TerminalShellType.bash, + undefined + ); + + expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command'); + }); + async function expectActivationCommandIfCondaDetectionFails( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + pythonPath: string + ) { + platformService.setup((p) => p.isLinux).returns(() => isLinux); + platformService.setup((p) => p.isWindows).returns(() => isWindows); + platformService.setup((p) => p.isMac).returns(() => isOsx); + condaService.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + condaService.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); + + when(bash.isShellSupported(anything())).thenReturn(true); + when(bash.getActivationCommands(anything(), TerminalShellType.bash)).thenResolve(['mock command']); + + const expectedActivationCommand = ['mock command']; + const activationCommands = await terminalHelper.getEnvironmentActivationCommands( + TerminalShellType.bash, + undefined + ); + expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command'); + } + + test('If environment is a conda environment and environment detection fails, ensure activatino of script is sent (windows)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await expectActivationCommandIfCondaDetectionFails(true, false, false, pythonPath); + }); + + test('If environment is a conda environment and environment detection fails, ensure activatino of script is sent (osx)', async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'python'); + fileSystem + .setup((f) => + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))) + ) + .returns(() => Promise.resolve(true)); + await expectActivationCommandIfCondaDetectionFails(false, true, false, pythonPath); + }); + + test('If environment is a conda environment and environment detection fails, ensure activatino of script is sent (linux)', async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'python'); + fileSystem + .setup((f) => + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))) + ) + .returns(() => Promise.resolve(true)); + await expectActivationCommandIfCondaDetectionFails(false, false, true, pythonPath); + }); + + test('Return undefined if unable to get activation command', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); + + condaService.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + + pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); + + const mockProvider = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); + serviceContainer + .setup((c) => c.getAll(TypeMoq.It.isValue(ITerminalActivationCommandProvider), TypeMoq.It.isAny())) + .returns(() => [mockProvider.object]); + mockProvider.setup((p) => p.isShellSupported(TypeMoq.It.isAny())).returns(() => true); + mockProvider + .setup((p) => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + const activationCommands = await terminalHelper.getEnvironmentActivationCommands( + TerminalShellType.bash, + undefined + ); + expect(activationCommands).to.equal(undefined, 'Incorrect Activation command'); + }); + + const windowsTestPath = 'C:\\path\\to'; + const windowsTestPathSpaces = 'C:\\the path\\to the command'; + + type WindowsActivationTestParams = { + testName: string; + basePath: string; + envName: string; + expectedResult: string[] | undefined; + expectedRawCmd: string; + terminalKind: TerminalShellType; + }; + + const testsForWindowsActivation: WindowsActivationTestParams[] = [ + { + testName: 'Activation uses full path on windows for powershell', + basePath: windowsTestPath, + envName: 'TesterEnv', + expectedResult: ['conda activate TesterEnv'], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.powershell + }, + { + testName: 'Activation uses full path with spaces on windows for powershell', + basePath: windowsTestPathSpaces, + envName: 'TesterEnv', + expectedResult: ['conda activate TesterEnv'], + expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, + terminalKind: TerminalShellType.powershell + }, + { + testName: 'Activation uses full path on windows under powershell, environment name has spaces', + basePath: windowsTestPath, + envName: 'The Tester Environment', + expectedResult: ['conda activate "The Tester Environment"'], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.powershell + }, + { + testName: 'Activation uses full path on windows for powershell-core', + basePath: windowsTestPath, + envName: 'TesterEnv', + expectedResult: ['conda activate TesterEnv'], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.powershellCore + }, + { + testName: 'Activation uses full path with spaces on windows for powershell-core', + basePath: windowsTestPathSpaces, + envName: 'TesterEnv', + expectedResult: ['conda activate TesterEnv'], + expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, + terminalKind: TerminalShellType.powershellCore + }, + { + testName: 'Activation uses full path on windows for powershell-core, environment name has spaces', + basePath: windowsTestPath, + envName: 'The Tester Environment', + expectedResult: ['conda activate "The Tester Environment"'], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.powershellCore + }, + { + testName: 'Activation uses full path on windows for cmd.exe', + basePath: windowsTestPath, + envName: 'TesterEnv', + expectedResult: [`${path.join(windowsTestPath, 'activate')} TesterEnv`], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.commandPrompt + }, + { + testName: 'Activation uses full path with spaces on windows for cmd.exe', + basePath: windowsTestPathSpaces, + envName: 'TesterEnv', + expectedResult: [`"${path.join(windowsTestPathSpaces, 'activate')}" TesterEnv`], + expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, + terminalKind: TerminalShellType.commandPrompt + }, + { + testName: 'Activation uses full path on windows for cmd.exe, environment name has spaces', + basePath: windowsTestPath, + envName: 'The Tester Environment', + expectedResult: [`${path.join(windowsTestPath, 'activate')} "The Tester Environment"`], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.commandPrompt + } + ]; + + testsForWindowsActivation.forEach((testParams: WindowsActivationTestParams) => { + test(testParams.testName, async () => { + // each test simply tests the base windows activate command, + // and then the specific result from the terminal selected. + const servCnt = TypeMoq.Mock.ofType<IServiceContainer>(); + const condaSrv = TypeMoq.Mock.ofType<ICondaService>(); + condaSrv + .setup((c) => c.getCondaFile()) + .returns(async () => { + return path.join(testParams.basePath, 'conda.exe'); + }); + servCnt + .setup((s) => s.get(TypeMoq.It.isValue(ICondaService), TypeMoq.It.isAny())) + .returns(() => condaSrv.object); + + const tstCmdProvider = new CondaActivationCommandProvider( + condaSrv.object, + platformService.object, + configService.object + ); + + let result: string[] | undefined; + + if (testParams.terminalKind === TerminalShellType.commandPrompt) { + result = await tstCmdProvider.getWindowsCommands(testParams.envName); + } else { + result = await tstCmdProvider.getPowershellCommands(testParams.envName); + } + expect(result).to.deep.equal(testParams.expectedResult, 'Specific terminal command is incorrect.'); + }); + }); +}); diff --git a/src/test/common/terminals/activation.unit.test.ts b/src/test/common/terminals/activation.unit.test.ts new file mode 100644 index 000000000000..b8ef12607395 --- /dev/null +++ b/src/test/common/terminals/activation.unit.test.ts @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import { expect } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { Terminal, Uri } from 'vscode'; +import { ActiveResourceService } from '../../../client/common/application/activeResource'; +import { TerminalManager } from '../../../client/common/application/terminalManager'; +import { IActiveResourceService, ITerminalManager } from '../../../client/common/application/types'; +import { TerminalActivator } from '../../../client/common/terminal/activator'; +import { ITerminalActivator } from '../../../client/common/terminal/types'; +import { IDisposable } from '../../../client/common/types'; +import { TerminalAutoActivation } from '../../../client/terminals/activation'; +import { ITerminalAutoActivation } from '../../../client/terminals/types'; +import { noop } from '../../core'; + +// tslint:disable-next-line: max-func-body-length +suite('Terminal Auto Activation', () => { + let activator: ITerminalActivator; + let terminalManager: ITerminalManager; + let terminalAutoActivation: ITerminalAutoActivation; + let activeResourceService: IActiveResourceService; + const resource = Uri.parse('a'); + let terminal: Terminal; + + setup(() => { + terminal = { + dispose: noop, + hide: noop, + name: 'Python', + creationOptions: {}, + processId: Promise.resolve(0), + sendText: noop, + show: noop, + exitStatus: { code: 0 } + }; + terminalManager = mock(TerminalManager); + activator = mock(TerminalActivator); + activeResourceService = mock(ActiveResourceService); + + terminalAutoActivation = new TerminalAutoActivation( + instance(terminalManager), + [], + instance(activator), + instance(activeResourceService) + ); + }); + + test('New Terminals should be activated', async () => { + type EventHandler = (e: Terminal) => void; + let handler: undefined | EventHandler; + const handlerDisposable = TypeMoq.Mock.ofType<IDisposable>(); + const onDidOpenTerminal = (cb: EventHandler) => { + handler = cb; + return handlerDisposable.object; + }; + when(activeResourceService.getActiveResource()).thenReturn(resource); + when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); + + terminalAutoActivation.register(); + + expect(handler).not.to.be.an('undefined', 'event handler not initialized'); + + handler!.bind(terminalAutoActivation)(terminal); + + verify(activator.activateEnvironmentInTerminal(terminal, anything())).once(); + }); + test('New Terminals should not be activated if hidden from user', async () => { + terminal = { + dispose: noop, + hide: noop, + name: 'Python', + creationOptions: { hideFromUser: true }, + processId: Promise.resolve(0), + sendText: noop, + show: noop, + exitStatus: { code: 0 } + }; + type EventHandler = (e: Terminal) => void; + let handler: undefined | EventHandler; + const handlerDisposable = TypeMoq.Mock.ofType<IDisposable>(); + const onDidOpenTerminal = (cb: EventHandler) => { + handler = cb; + return handlerDisposable.object; + }; + when(activeResourceService.getActiveResource()).thenReturn(resource); + when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); + + terminalAutoActivation.register(); + + expect(handler).not.to.be.an('undefined', 'event handler not initialized'); + + handler!.bind(terminalAutoActivation)(terminal); + + verify(activator.activateEnvironmentInTerminal(terminal, anything())).never(); + }); + test('New Terminals should be activated with resource of single workspace', async () => { + type EventHandler = (e: Terminal) => void; + let handler: undefined | EventHandler; + const handlerDisposable = TypeMoq.Mock.ofType<IDisposable>(); + const onDidOpenTerminal = (cb: EventHandler) => { + handler = cb; + return handlerDisposable.object; + }; + when(activeResourceService.getActiveResource()).thenReturn(resource); + when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); + + terminalAutoActivation.register(); + + expect(handler).not.to.be.an('undefined', 'event handler not initialized'); + + handler!.bind(terminalAutoActivation)(terminal); + + verify(activator.activateEnvironmentInTerminal(terminal, anything())).once(); + }); + test('New Terminals should be activated with resource of main workspace', async () => { + type EventHandler = (e: Terminal) => void; + let handler: undefined | EventHandler; + const handlerDisposable = TypeMoq.Mock.ofType<IDisposable>(); + const onDidOpenTerminal = (cb: EventHandler) => { + handler = cb; + return handlerDisposable.object; + }; + when(activeResourceService.getActiveResource()).thenReturn(resource); + when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); + terminalAutoActivation.register(); + + expect(handler).not.to.be.an('undefined', 'event handler not initialized'); + + handler!.bind(terminalAutoActivation)(terminal); + + verify(activator.activateEnvironmentInTerminal(terminal, anything())).once(); + }); +}); diff --git a/src/test/common/terminals/activator/base.unit.test.ts b/src/test/common/terminals/activator/base.unit.test.ts new file mode 100644 index 000000000000..79f1c121cdb4 --- /dev/null +++ b/src/test/common/terminals/activator/base.unit.test.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Terminal } from 'vscode'; +import { BaseTerminalActivator } from '../../../../client/common/terminal/activator/base'; +import { ITerminalActivator, ITerminalHelper } from '../../../../client/common/terminal/types'; +import { noop } from '../../../../client/common/utils/misc'; + +// tslint:disable:max-func-body-length no-any +suite('Terminal Base Activator', () => { + let activator: ITerminalActivator; + let helper: TypeMoq.IMock<ITerminalHelper>; + + setup(() => { + helper = TypeMoq.Mock.ofType<ITerminalHelper>(); + activator = (new (class extends BaseTerminalActivator { + public waitForCommandToProcess() { + noop(); + return Promise.resolve(); + } + })(helper.object) as any) as ITerminalActivator; + }); + [ + { commandCount: 1, preserveFocus: false }, + { commandCount: 2, preserveFocus: false }, + { commandCount: 1, preserveFocus: true }, + { commandCount: 1, preserveFocus: true } + ].forEach((item) => { + const titleSuffix = `(${item.commandCount} activation command, and preserve focus in terminal is ${item.preserveFocus})`; + const activationCommands = item.commandCount === 1 ? ['CMD1'] : ['CMD1', 'CMD2']; + test(`Terminal is activated ${titleSuffix}`, async () => { + helper + .setup((h) => + h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(activationCommands)); + const terminal = TypeMoq.Mock.ofType<Terminal>(); + + terminal + .setup((t) => t.show(TypeMoq.It.isValue(item.preserveFocus))) + .returns(() => undefined) + .verifiable(TypeMoq.Times.exactly(activationCommands.length)); + activationCommands.forEach((cmd) => { + terminal + .setup((t) => t.sendText(TypeMoq.It.isValue(cmd))) + .returns(() => undefined) + .verifiable(TypeMoq.Times.exactly(1)); + }); + + await activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }); + + terminal.verifyAll(); + }); + test(`Terminal is activated only once ${titleSuffix}`, async () => { + helper + .setup((h) => + h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(activationCommands)); + const terminal = TypeMoq.Mock.ofType<Terminal>(); + + terminal + .setup((t) => t.show(TypeMoq.It.isValue(item.preserveFocus))) + .returns(() => undefined) + .verifiable(TypeMoq.Times.exactly(activationCommands.length)); + activationCommands.forEach((cmd) => { + terminal + .setup((t) => t.sendText(TypeMoq.It.isValue(cmd))) + .returns(() => undefined) + .verifiable(TypeMoq.Times.exactly(1)); + }); + + await activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }); + await activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }); + await activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }); + + terminal.verifyAll(); + }); + test(`Terminal is activated only once ${titleSuffix} (even when not waiting)`, async () => { + helper + .setup((h) => + h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(activationCommands)); + const terminal = TypeMoq.Mock.ofType<Terminal>(); + + terminal + .setup((t) => t.show(TypeMoq.It.isValue(item.preserveFocus))) + .returns(() => undefined) + .verifiable(TypeMoq.Times.exactly(activationCommands.length)); + activationCommands.forEach((cmd) => { + terminal + .setup((t) => t.sendText(TypeMoq.It.isValue(cmd))) + .returns(() => undefined) + .verifiable(TypeMoq.Times.exactly(1)); + }); + + const activated = await Promise.all([ + activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }), + activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }), + activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }) + ]); + + terminal.verifyAll(); + expect(activated).to.deep.equal([true, true, true], 'Invalid values'); + }); + }); +}); diff --git a/src/test/common/terminals/activator/index.unit.test.ts b/src/test/common/terminals/activator/index.unit.test.ts new file mode 100644 index 000000000000..e9b1a1e0050e --- /dev/null +++ b/src/test/common/terminals/activator/index.unit.test.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Terminal } from 'vscode'; +import { TerminalActivator } from '../../../../client/common/terminal/activator'; +import { + ITerminalActivationHandler, + ITerminalActivator, + ITerminalHelper +} from '../../../../client/common/terminal/types'; +import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../../client/common/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal Activator', () => { + let activator: TerminalActivator; + let baseActivator: TypeMoq.IMock<ITerminalActivator>; + let handler1: TypeMoq.IMock<ITerminalActivationHandler>; + let handler2: TypeMoq.IMock<ITerminalActivationHandler>; + let terminalSettings: TypeMoq.IMock<ITerminalSettings>; + setup(() => { + baseActivator = TypeMoq.Mock.ofType<ITerminalActivator>(); + terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>(); + handler1 = TypeMoq.Mock.ofType<ITerminalActivationHandler>(); + handler2 = TypeMoq.Mock.ofType<ITerminalActivationHandler>(); + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + configService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns(() => { + return ({ + terminal: terminalSettings.object + } as unknown) as IPythonSettings; + }); + activator = new (class extends TerminalActivator { + protected initialize() { + this.baseActivator = baseActivator.object; + } + })(TypeMoq.Mock.ofType<ITerminalHelper>().object, [handler1.object, handler2.object], configService.object); + }); + async function testActivationAndHandlers( + activationSuccessful: boolean, + activateEnvironmentSetting: boolean, + hidden: boolean = false + ) { + terminalSettings + .setup((b) => b.activateEnvironment) + .returns(() => activateEnvironmentSetting) + .verifiable(TypeMoq.Times.once()); + baseActivator + .setup((b) => b.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(activationSuccessful)) + .verifiable(TypeMoq.Times.exactly(activationSuccessful ? 1 : 0)); + handler1 + .setup((h) => + h.handleActivation( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isValue(activationSuccessful) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.exactly(activationSuccessful ? 1 : 0)); + handler2 + .setup((h) => + h.handleActivation( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isValue(activationSuccessful) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.exactly(activationSuccessful ? 1 : 0)); + + const terminal = TypeMoq.Mock.ofType<Terminal>(); + const activated = await activator.activateEnvironmentInTerminal(terminal.object, { + preserveFocus: activationSuccessful, + hideFromUser: hidden + }); + + assert.equal(activated, activationSuccessful); + baseActivator.verifyAll(); + handler1.verifyAll(); + handler2.verifyAll(); + } + test('Terminal is activated and handlers are invoked', () => testActivationAndHandlers(true, true)); + test('Terminal is not activated if auto-activate setting is set to true but terminal is hidden', () => + testActivationAndHandlers(false, true, true)); + test('Terminal is not activated and handlers are invoked', () => testActivationAndHandlers(false, false)); +}); diff --git a/src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts b/src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts new file mode 100644 index 000000000000..0ca229c07dee --- /dev/null +++ b/src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as TypeMoq from 'typemoq'; +import { Terminal } from 'vscode'; +import { IDiagnosticsService } from '../../../../client/application/diagnostics/types'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { PowershellTerminalActivationFailedHandler } from '../../../../client/common/terminal/activator/powershellFailedHandler'; +import { + ITerminalActivationHandler, + ITerminalHelper, + TerminalShellType +} from '../../../../client/common/terminal/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal Activation Powershell Failed Handler', () => { + let psHandler: ITerminalActivationHandler; + let helper: TypeMoq.IMock<ITerminalHelper>; + let platform: TypeMoq.IMock<IPlatformService>; + let diagnosticService: TypeMoq.IMock<IDiagnosticsService>; + + async function testDiagnostics( + mustHandleDiagnostics: boolean, + isWindows: boolean, + activatedSuccessfully: boolean, + shellType: TerminalShellType, + cmdPromptHasActivationCommands: boolean + ) { + platform.setup((p) => p.isWindows).returns(() => isWindows); + helper.setup((p) => p.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => shellType); + const cmdPromptCommands = cmdPromptHasActivationCommands ? ['a'] : []; + helper + .setup((h) => + h.getEnvironmentActivationCommands( + TypeMoq.It.isValue(TerminalShellType.commandPrompt), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(cmdPromptCommands)); + + diagnosticService + .setup((d) => d.handle(TypeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.exactly(mustHandleDiagnostics ? 1 : 0)); + await psHandler.handleActivation( + TypeMoq.Mock.ofType<Terminal>().object, + undefined, + false, + activatedSuccessfully + ); + } + + [true, false].forEach((isWindows) => { + suite(`OS is ${isWindows ? 'Windows' : 'Non-Widows'}`, () => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shell) => { + suite(`Shell is ${shell.name}`, () => { + [true, false].forEach((hasCommandPromptActivations) => { + hasCommandPromptActivations = + isWindows && hasCommandPromptActivations && shell.value !== TerminalShellType.commandPrompt; + suite( + `${ + hasCommandPromptActivations + ? 'Can activate with Command Prompt' + : "Can't activate with Command Prompt" + }`, + () => { + [true, false].forEach((activatedSuccessfully) => { + suite( + `Terminal Activation is ${activatedSuccessfully ? 'successful' : 'has failed'}`, + () => { + setup(() => { + helper = TypeMoq.Mock.ofType<ITerminalHelper>(); + platform = TypeMoq.Mock.ofType<IPlatformService>(); + diagnosticService = TypeMoq.Mock.ofType<IDiagnosticsService>(); + psHandler = new PowershellTerminalActivationFailedHandler( + helper.object, + platform.object, + diagnosticService.object + ); + }); + const isPs = + shell.value === TerminalShellType.powershell || + shell.value === TerminalShellType.powershellCore; + const mustHandleDiagnostics = + isPs && !activatedSuccessfully && hasCommandPromptActivations; + test(`Diagnostic must ${ + mustHandleDiagnostics ? 'be' : 'not be' + } handled`, async () => { + await testDiagnostics( + mustHandleDiagnostics, + isWindows, + activatedSuccessfully, + shell.value, + hasCommandPromptActivations + ); + helper.verifyAll(); + diagnosticService.verifyAll(); + }); + } + ); + }); + } + ); + }); + }); + }); + }); + }); +}); diff --git a/src/test/common/terminals/commandPrompt.unit.test.ts b/src/test/common/terminals/commandPrompt.unit.test.ts new file mode 100644 index 000000000000..2a240967d2b8 --- /dev/null +++ b/src/test/common/terminals/commandPrompt.unit.test.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget } from 'vscode'; +import { + getCommandPromptLocation, + useCommandPromptAsDefaultShell +} from '../../../client/common/terminal/commandPrompt'; +import { IConfigurationService, ICurrentProcess } from '../../../client/common/types'; + +suite('Terminal Command Prompt', () => { + let currentProc: TypeMoq.IMock<ICurrentProcess>; + let configService: TypeMoq.IMock<IConfigurationService>; + + setup(() => { + currentProc = TypeMoq.Mock.ofType<ICurrentProcess>(); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + }); + + test('Getting Path Command Prompt executable (32 on 64Win)', async () => { + const env = { windir: 'windir' }; + currentProc + .setup((p) => p.env) + .returns(() => env) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const cmdPath = getCommandPromptLocation(currentProc.object); + + expect(cmdPath).to.be.deep.equal(path.join('windir', 'System32', 'cmd.exe')); + currentProc.verifyAll(); + }); + test('Getting Path Command Prompt executable (not 32 on 64Win)', async () => { + const env = { PROCESSOR_ARCHITEW6432: 'x', windir: 'windir' }; + currentProc + .setup((p) => p.env) + .returns(() => env) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const cmdPath = getCommandPromptLocation(currentProc.object); + + expect(cmdPath).to.be.deep.equal(path.join('windir', 'Sysnative', 'cmd.exe')); + currentProc.verifyAll(); + }); + test('Use command prompt as default shell', async () => { + const env = { windir: 'windir' }; + currentProc + .setup((p) => p.env) + .returns(() => env) + .verifiable(TypeMoq.Times.atLeastOnce()); + const cmdPromptPath = path.join('windir', 'System32', 'cmd.exe'); + configService + .setup((c) => + c.updateSectionSetting( + TypeMoq.It.isValue('terminal'), + TypeMoq.It.isValue('integrated.shell.windows'), + TypeMoq.It.isValue(cmdPromptPath), + TypeMoq.It.isAny(), + TypeMoq.It.isValue(ConfigurationTarget.Global) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await useCommandPromptAsDefaultShell(currentProc.object, configService.object); + configService.verifyAll(); + currentProc.verifyAll(); + }); +}); diff --git a/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts b/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts new file mode 100644 index 000000000000..effae6004575 --- /dev/null +++ b/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { instance, mock, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { PipEnvActivationCommandProvider } from '../../../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../../../../client/common/terminal/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { IInterpreterService, IPipEnvService } from '../../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../../client/interpreter/interpreterService'; +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; + +// tslint:disable:no-any + +suite('Terminals Activation - Pipenv', () => { + [undefined, Uri.parse('x')].forEach((resource) => { + suite(resource ? 'With a resource' : 'Without a resource', () => { + let pipenvExecFile = 'pipenv'; + let activationProvider: ITerminalActivationCommandProvider; + let interpreterService: IInterpreterService; + let pipenvService: TypeMoq.IMock<IPipEnvService>; + let workspaceService: IWorkspaceService; + let fs: IFileSystem; + setup(() => { + interpreterService = mock(InterpreterService); + fs = mock(FileSystem); + workspaceService = mock(WorkspaceService); + interpreterService = mock(InterpreterService); + pipenvService = TypeMoq.Mock.ofType<IPipEnvService>(); + activationProvider = new PipEnvActivationCommandProvider( + instance(interpreterService), + pipenvService.object, + instance(workspaceService), + instance(fs) + ); + + pipenvService.setup((p) => p.executable).returns(() => pipenvExecFile); + }); + + test('No commands for no interpreter', async () => { + when(interpreterService.getActiveInterpreter(resource)).thenResolve(); + + for (const shell of getNamesAndValues<TerminalShellType>(TerminalShellType)) { + const cmd = await activationProvider.getActivationCommands(resource, shell.value); + + assert.equal(cmd, undefined); + } + }); + test('No commands for an interpreter that is not Pipenv', async () => { + const nonPipInterpreterTypes = getNamesAndValues<EnvironmentType>(EnvironmentType).filter( + (t) => t.value !== EnvironmentType.Pipenv + ); + for (const interpreterType of nonPipInterpreterTypes) { + when(interpreterService.getActiveInterpreter(resource)).thenResolve({ + type: interpreterType + } as any); + + for (const shell of getNamesAndValues<TerminalShellType>(TerminalShellType)) { + const cmd = await activationProvider.getActivationCommands(resource, shell.value); + + assert.equal(cmd, undefined); + } + } + }); + test('pipenv shell is returned for pipenv interpeter', async () => { + when(interpreterService.getActiveInterpreter(resource)).thenResolve({ + envType: EnvironmentType.Pipenv + } as any); + + for (const shell of getNamesAndValues<TerminalShellType>(TerminalShellType)) { + const cmd = await activationProvider.getActivationCommands(resource, shell.value); + + assert.deepEqual(cmd, ['pipenv shell']); + } + }); + test('pipenv is properly escaped', async () => { + pipenvExecFile = 'my pipenv'; + when(interpreterService.getActiveInterpreter(resource)).thenResolve({ + envType: EnvironmentType.Pipenv + } as any); + + for (const shell of getNamesAndValues<TerminalShellType>(TerminalShellType)) { + const cmd = await activationProvider.getActivationCommands(resource, shell.value); + + assert.deepEqual(cmd, ['"my pipenv" shell']); + } + }); + }); + }); +}); diff --git a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts new file mode 100644 index 000000000000..e986708ac163 --- /dev/null +++ b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { DeprecatePythonPath } from '../../../../client/common/experiments/groups'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IExperimentsManager } from '../../../../client/common/types'; +import { PYTHON_VIRTUAL_ENVS_LOCATION } from '../../../ciConstants'; +import { + PYTHON_PATH, + resetGlobalInterpreterPathSetting, + restorePythonPathInWorkspaceRoot, + setGlobalInterpreterPath, + setPythonPathInWorkspaceRoot, + updateSetting, + waitForCondition +} from '../../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, TEST_TIMEOUT } from '../../../constants'; +import { sleep } from '../../../core'; +import { initialize, initializeTest } from '../../../initialize'; + +// tslint:disable:max-func-body-length no-any +suite('Activation of Environments in Terminal', () => { + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'testExecInTerminal.py' + ); + let outputFile = ''; + let outputFileCounter = 0; + const fileSystem = new FileSystem(); + const outputFilesCreated: string[] = []; + const envsLocation = + PYTHON_VIRTUAL_ENVS_LOCATION !== undefined + ? path.join(EXTENSION_ROOT_DIR_FOR_TESTS, PYTHON_VIRTUAL_ENVS_LOCATION) + : path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'tmp', 'envPaths.json'); + const waitTimeForActivation = 5000; + type EnvPath = { + condaExecPath: string; + condaPath: string; + venvPath: string; + pipenvPath: string; + virtualEnvPath: string; + }; + let envPaths: EnvPath; + const defaultShell = { + Windows: '', + Linux: '', + MacOS: '' + }; + let terminalSettings: any; + let pythonSettings: any; + let experiments: IExperimentsManager; + suiteSetup(async () => { + envPaths = await fs.readJson(envsLocation); + terminalSettings = vscode.workspace.getConfiguration('terminal', vscode.workspace.workspaceFolders![0].uri); + pythonSettings = vscode.workspace.getConfiguration('python', vscode.workspace.workspaceFolders![0].uri); + defaultShell.Windows = terminalSettings.inspect('integrated.shell.windows').globalValue; + defaultShell.Linux = terminalSettings.inspect('integrated.shell.linux').globalValue; + await terminalSettings.update('integrated.shell.linux', '/bin/bash', vscode.ConfigurationTarget.Global); + experiments = (await initialize()).serviceContainer.get<IExperimentsManager>(IExperimentsManager); + }); + + setup(async () => { + await initializeTest(); + outputFile = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + `testExecInTerminal_${outputFileCounter}.log` + ); + outputFileCounter += 1; + outputFilesCreated.push(outputFile); + }); + + suiteTeardown(async function () { + // tslint:disable-next-line: no-invalid-this + this.timeout(TEST_TIMEOUT * 2); + await revertSettings(); + + // remove all created log files. + outputFilesCreated.forEach(async (filePath: string) => { + if (await fs.pathExists(filePath)) { + await fs.unlink(filePath); + } + }); + }); + + async function revertSettings() { + await updateSetting( + 'terminal.activateEnvironment', + undefined, + vscode.workspace.workspaceFolders![0].uri, + vscode.ConfigurationTarget.WorkspaceFolder + ); + await terminalSettings.update( + 'integrated.shell.windows', + defaultShell.Windows, + vscode.ConfigurationTarget.Global + ); + await terminalSettings.update('integrated.shell.linux', defaultShell.Linux, vscode.ConfigurationTarget.Global); + await pythonSettings.update('condaPath', undefined, vscode.ConfigurationTarget.Workspace); + if (experiments.inExperiment(DeprecatePythonPath.experiment)) { + await resetGlobalInterpreterPathSetting(); + } else { + await restorePythonPathInWorkspaceRoot(); + } + } + + /** + * Open a terminal and issue a python `pythonFile` command, expecting it to + * create a file `logfile`, with timeout limits. + * + * @param pythonFile The python script to run. + * @param logFile The logfile that the python script will produce. + * @param consoleInitWaitMs How long to wait for the console to initialize. + * @param logFileCreationWaitMs How long to wait for the output file to be produced. + */ + async function openTerminalAndAwaitCommandContent( + consoleInitWaitMs: number, + pythonFile: string, + logFile: string, + logFileCreationWaitMs: number + ): Promise<string> { + const terminal = vscode.window.createTerminal(); + await sleep(consoleInitWaitMs); + terminal.sendText(`python ${pythonFile} ${logFile}`, true); + await waitForCondition(() => fs.pathExists(logFile), logFileCreationWaitMs, `${logFile} file not created.`); + + return fs.readFile(logFile, 'utf-8'); + } + + /** + * Turn on `terminal.activateEnvironment`, produce a shell, run a python script + * that outputs the path to the active python interpreter. + * + * Note: asserts that the envPath given matches the envPath returned by the script. + * + * @param envPath Python environment path to activate in the terminal (via vscode config) + */ + async function testActivation(envPath: string) { + await updateSetting( + 'terminal.activateEnvironment', + true, + vscode.workspace.workspaceFolders![0].uri, + vscode.ConfigurationTarget.WorkspaceFolder + ); + if (experiments.inExperiment(DeprecatePythonPath.experiment)) { + await setGlobalInterpreterPath(envPath); + } else { + await setPythonPathInWorkspaceRoot(envPath); + } + const content = await openTerminalAndAwaitCommandContent(waitTimeForActivation, file, outputFile, 5_000); + expect(fileSystem.arePathsSame(content, envPath)).to.equal(true, 'Environment not activated'); + } + + test('Should not activate', async () => { + await updateSetting( + 'terminal.activateEnvironment', + false, + vscode.workspace.workspaceFolders![0].uri, + vscode.ConfigurationTarget.WorkspaceFolder + ); + const content = await openTerminalAndAwaitCommandContent(waitTimeForActivation, file, outputFile, 5_000); + expect(fileSystem.arePathsSame(content, PYTHON_PATH)).to.equal(false, 'Environment not activated'); + }); + + test('Should activate with venv', async function () { + if (process.env.CI_PYTHON_VERSION && process.env.CI_PYTHON_VERSION.startsWith('2.')) { + // tslint:disable-next-line: no-invalid-this + this.skip(); + } + await testActivation(envPaths.venvPath); + }); + test('Should activate with pipenv', async () => { + await testActivation(envPaths.pipenvPath); + }); + test('Should activate with virtualenv', async () => { + await testActivation(envPaths.virtualEnvPath); + }); + test('Should activate with conda', async () => { + await terminalSettings.update( + 'integrated.shell.windows', + 'C:\\Windows\\System32\\cmd.exe', + vscode.ConfigurationTarget.Global + ); + await pythonSettings.update('condaPath', envPaths.condaExecPath, vscode.ConfigurationTarget.Workspace); + await testActivation(envPaths.condaPath); + }).timeout(TEST_TIMEOUT * 2); +}); diff --git a/src/test/common/terminals/factory.unit.test.ts b/src/test/common/terminals/factory.unit.test.ts new file mode 100644 index 000000000000..1aeee9263f53 --- /dev/null +++ b/src/test/common/terminals/factory.unit.test.ts @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { TerminalServiceFactory } from '../../../client/common/terminal/factory'; +import { TerminalService } from '../../../client/common/terminal/service'; +import { SynchronousTerminalService } from '../../../client/common/terminal/syncTerminalService'; +import { ITerminalHelper, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal Service Factory', () => { + let factory: ITerminalServiceFactory; + let disposables: Disposable[] = []; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let fs: TypeMoq.IMock<IFileSystem>; + setup(() => { + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + const interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + fs = TypeMoq.Mock.ofType<IFileSystem>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) + .returns(() => interpreterService.object); + disposables = []; + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) + .returns(() => disposables); + const terminalHelper = TypeMoq.Mock.ofType<ITerminalHelper>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalHelper), TypeMoq.It.isAny())) + .returns(() => terminalHelper.object); + const terminalManager = TypeMoq.Mock.ofType<ITerminalManager>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalManager), TypeMoq.It.isAny())) + .returns(() => terminalManager.object); + factory = new TerminalServiceFactory(serviceContainer.object, fs.object, interpreterService.object); + + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) + .returns(() => workspaceService.object); + }); + teardown(() => { + disposables.forEach((disposable) => { + if (disposable) { + disposable.dispose(); + } + }); + }); + + test('Ensure same instance of terminal service is returned', () => { + const instance = factory.getTerminalService() as SynchronousTerminalService; + const sameInstance = + (factory.getTerminalService() as SynchronousTerminalService).terminalService === instance.terminalService; + expect(sameInstance).to.equal(true, 'Instances are not the same'); + + const differentInstance = factory.getTerminalService(undefined, 'New Title'); + const notTheSameInstance = differentInstance === instance; + expect(notTheSameInstance).not.to.equal(true, 'Instances are the same'); + }); + + test('Ensure different instance of terminal service is returned when title is provided', () => { + const defaultInstance = factory.getTerminalService(); + expect(defaultInstance instanceof SynchronousTerminalService).to.equal( + true, + 'Not an instance of Terminal service' + ); + + const notSameAsDefaultInstance = factory.getTerminalService(undefined, 'New Title') === defaultInstance; + expect(notSameAsDefaultInstance).to.not.equal(true, 'Instances are the same as default instance'); + + const instance = factory.getTerminalService(undefined, 'New Title') as SynchronousTerminalService; + const sameInstance = + (factory.getTerminalService(undefined, 'New Title') as SynchronousTerminalService).terminalService === + instance.terminalService; + expect(sameInstance).to.equal(true, 'Instances are not the same'); + + const differentInstance = factory.getTerminalService(undefined, 'Another New Title'); + const notTheSameInstance = differentInstance === instance; + expect(notTheSameInstance).not.to.equal(true, 'Instances are the same'); + }); + + test('Ensure different instance of terminal services are created', () => { + const instance1 = factory.createTerminalService(); + expect(instance1 instanceof TerminalService).to.equal(true, 'Not an instance of Terminal service'); + + const notSameAsFirstInstance = factory.createTerminalService() === instance1; + expect(notSameAsFirstInstance).to.not.equal(true, 'Instances are the same'); + + const instance2 = factory.createTerminalService(Uri.file('a'), 'Title'); + const notSameAsSecondInstance = instance1 === instance2; + expect(notSameAsSecondInstance).to.not.equal(true, 'Instances are the same'); + + const instance3 = factory.createTerminalService(Uri.file('a'), 'Title'); + const notSameAsThirdInstance = instance2 === instance3; + expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same'); + }); + + test('Ensure same terminal is returned when using resources from the same workspace', () => { + const file1A = Uri.file('1a'); + const file2A = Uri.file('2a'); + const fileB = Uri.file('b'); + const workspaceUriA = Uri.file('A'); + const workspaceUriB = Uri.file('B'); + const workspaceFolderA = TypeMoq.Mock.ofType<WorkspaceFolder>(); + workspaceFolderA.setup((w) => w.uri).returns(() => workspaceUriA); + const workspaceFolderB = TypeMoq.Mock.ofType<WorkspaceFolder>(); + workspaceFolderB.setup((w) => w.uri).returns(() => workspaceUriB); + + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file1A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(file2A))) + .returns(() => workspaceFolderA.object); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(fileB))) + .returns(() => workspaceFolderB.object); + + const terminalForFile1A = factory.getTerminalService(file1A) as SynchronousTerminalService; + const terminalForFile2A = factory.getTerminalService(file2A) as SynchronousTerminalService; + const terminalForFileB = factory.getTerminalService(fileB) as SynchronousTerminalService; + + const terminalsAreSameForWorkspaceA = terminalForFile1A.terminalService === terminalForFile2A.terminalService; + expect(terminalsAreSameForWorkspaceA).to.equal(true, 'Instances are not the same for Workspace A'); + + const terminalsForWorkspaceABAreDifferent = + terminalForFile1A.terminalService === terminalForFileB.terminalService; + expect(terminalsForWorkspaceABAreDifferent).to.equal( + false, + 'Instances should be different for different workspaces' + ); + }); +}); diff --git a/src/test/common/terminals/helper.unit.test.ts b/src/test/common/terminals/helper.unit.test.ts new file mode 100644 index 000000000000..8393f59b50fa --- /dev/null +++ b/src/test/common/terminals/helper.unit.test.ts @@ -0,0 +1,401 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import * as sinon from 'sinon'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { Terminal, Uri } from 'vscode'; +import { TerminalManager } from '../../../client/common/application/terminalManager'; +import { ITerminalManager } from '../../../client/common/application/types'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { CondaActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; +import { PipEnvActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; +import { PyEnvActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; +import { TerminalHelper } from '../../../client/common/terminal/helper'; +import { ShellDetector } from '../../../client/common/terminal/shellDetector'; +import { TerminalNameShellDetector } from '../../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + TerminalShellType +} from '../../../client/common/terminal/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { Architecture, OSType } from '../../../client/common/utils/platform'; +import { ICondaService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +// tslint:disable:max-func-body-length no-any + +suite('Terminal Service helpers', () => { + let helper: TerminalHelper; + let terminalManager: ITerminalManager; + let platformService: IPlatformService; + let condaService: ICondaService; + let configurationService: IConfigurationService; + let condaActivationProvider: ITerminalActivationCommandProvider; + let bashActivationProvider: ITerminalActivationCommandProvider; + let cmdActivationProvider: ITerminalActivationCommandProvider; + let pyenvActivationProvider: ITerminalActivationCommandProvider; + let pipenvActivationProvider: ITerminalActivationCommandProvider; + let pythonSettings: PythonSettings; + let shellDetectorIdentifyTerminalShell: sinon.SinonStub<[(Terminal | undefined)?], TerminalShellType>; + let mockDetector: IShellDetector; + const pythonInterpreter: PythonEnvironment = { + path: '/foo/bar/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 + }; + + function doSetup() { + mockDetector = mock(TerminalNameShellDetector); + terminalManager = mock(TerminalManager); + platformService = mock(PlatformService); + condaService = mock(CondaService); + configurationService = mock(ConfigurationService); + condaActivationProvider = mock(CondaActivationCommandProvider); + bashActivationProvider = mock(Bash); + cmdActivationProvider = mock(CommandPromptAndPowerShell); + pyenvActivationProvider = mock(PyEnvActivationCommandProvider); + pipenvActivationProvider = mock(PipEnvActivationCommandProvider); + pythonSettings = mock(PythonSettings); + shellDetectorIdentifyTerminalShell = sinon.stub(ShellDetector.prototype, 'identifyTerminalShell'); + helper = new TerminalHelper( + instance(platformService), + instance(terminalManager), + instance(condaService), + instance(mock(InterpreterService)), + instance(configurationService), + instance(condaActivationProvider), + instance(bashActivationProvider), + instance(cmdActivationProvider), + instance(pyenvActivationProvider), + instance(pipenvActivationProvider), + [instance(mockDetector)] + ); + } + teardown(() => shellDetectorIdentifyTerminalShell.restore()); + suite('Misc', () => { + setup(doSetup); + + test('Create terminal without a title', () => { + const terminal = 'Terminal Created'; + when(terminalManager.createTerminal(anything())).thenReturn(terminal as any); + + const term = helper.createTerminal(); + + verify(terminalManager.createTerminal(anything())).once(); + const args = capture(terminalManager.createTerminal).first()[0]; + expect(term).to.be.deep.equal(terminal); + expect(args.name).to.be.deep.equal(undefined, 'name should be undefined'); + }); + test('Create terminal with a title', () => { + const theTitle = 'Hello'; + const terminal = 'Terminal Created'; + when(terminalManager.createTerminal(anything())).thenReturn(terminal as any); + + const term = helper.createTerminal(theTitle); + + verify(terminalManager.createTerminal(anything())).once(); + const args = capture(terminalManager.createTerminal).first()[0]; + expect(term).to.be.deep.equal(terminal); + expect(args.name).to.be.deep.equal(theTitle); + }); + test('Ensure spaces in command is quoted', async () => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((item) => { + const command = 'c:\\python 3.7.exe'; + const args = ['1', '2']; + const commandPrefix = + item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore + ? '& ' + : ''; + const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgument()} 1 2`; + + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); + }); + }); + test('Ensure spaces in args are quoted', async () => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((item) => { + const command = 'python3.7.exe'; + const args = ['a file.py', '1', '2']; + const commandPrefix = + item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore + ? '& ' + : ''; + const expectedTerminalCommand = `${commandPrefix}${command} "a file.py" 1 2`; + + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); + }); + }); + + test('Ensure empty args are ignored', async () => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((item) => { + const command = 'python3.7.exe'; + const args: string[] = []; + const commandPrefix = + item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore + ? '& ' + : ''; + const expectedTerminalCommand = `${commandPrefix}${command}`; + + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell '${item.name}'`); + }); + }); + + test('Ensure empty args are ignored with s in command', async () => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((item) => { + const command = 'c:\\python 3.7.exe'; + const args: string[] = []; + const commandPrefix = + item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore + ? '& ' + : ''; + const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgument()}`; + + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); + }); + }); + }); + + function title(resource?: Uri, interpreter?: PythonEnvironment) { + return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; + } + + suite('Activation', () => { + [undefined, Uri.parse('a')].forEach((resource) => { + suite(title(resource), () => { + setup(() => { + doSetup(); + when(configurationService.getSettings(resource)).thenReturn(instance(pythonSettings)); + }); + function ensureCondaIsSupported( + isSupported: boolean, + pythonPath: string, + condaActivationCommands: string[] + ) { + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(pythonSettings.terminal).thenReturn({ activateEnvironment: true } as any); + when(condaService.isCondaEnvironment(pythonPath)).thenResolve(isSupported); + when(condaActivationProvider.getActivationCommands(resource, anything())).thenResolve( + condaActivationCommands + ); + } + test('Activation command must return conda activation command if interpreter is conda', async () => { + const pythonPath = 'some python Path value'; + const condaActivationCommands = ['Hello', '1']; + ensureCondaIsSupported(true, pythonPath, condaActivationCommands); + + const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); + + expect(cmd).to.equal(condaActivationCommands); + verify(pythonSettings.pythonPath).once(); + verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(condaActivationProvider.getActivationCommands(resource, anything())).once(); + }); + test('Activation command must return undefined if none of the proivders support the shell', async () => { + const pythonPath = 'some python Path value'; + ensureCondaIsSupported(false, pythonPath, []); + + when(bashActivationProvider.isShellSupported(anything())).thenReturn(false); + when(cmdActivationProvider.isShellSupported(anything())).thenReturn(false); + when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); + when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); + + const cmd = await helper.getEnvironmentActivationCommands( + ('someShell' as any) as TerminalShellType, + resource + ); + + expect(cmd).to.equal(undefined, 'Command must be undefined'); + verify(pythonSettings.pythonPath).once(); + verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + }); + test('Activation command must return command from bash if that is supported and others are not', async () => { + const pythonPath = 'some python Path value'; + const expectCommand = ['one', 'two']; + ensureCondaIsSupported(false, pythonPath, []); + + when(bashActivationProvider.getActivationCommands(resource, anything())).thenResolve(expectCommand); + + when(bashActivationProvider.isShellSupported(anything())).thenReturn(true); + when(cmdActivationProvider.isShellSupported(anything())).thenReturn(false); + when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); + when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); + + const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); + + expect(cmd).to.deep.equal(expectCommand); + verify(pythonSettings.pythonPath).once(); + verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); + verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + }); + test('Activation command must return command from pipenv if that is supported and even if others are supported', async () => { + const pythonPath = 'some python Path value'; + const expectCommand = ['one', 'two']; + ensureCondaIsSupported(false, pythonPath, []); + + when(pipenvActivationProvider.getActivationCommands(resource, anything())).thenResolve( + expectCommand + ); + when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(true); + + [bashActivationProvider, cmdActivationProvider, pyenvActivationProvider].forEach((provider) => { + when(provider.getActivationCommands(resource, anything())).thenResolve(['Something']); + when(provider.isShellSupported(anything())).thenReturn(true); + }); + + const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); + + expect(cmd).to.deep.equal(expectCommand); + verify(pythonSettings.pythonPath).once(); + verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(bashActivationProvider.getActivationCommands(resource, anything())).never(); + verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(pipenvActivationProvider.getActivationCommands(resource, anything())).atLeast(1); + verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + }); + test('Activation command must return command from Command Prompt if that is supported and others are not', async () => { + const pythonPath = 'some python Path value'; + const expectCommand = ['one', 'two']; + ensureCondaIsSupported(false, pythonPath, []); + + when(cmdActivationProvider.getActivationCommands(resource, anything())).thenResolve(expectCommand); + + when(bashActivationProvider.isShellSupported(anything())).thenReturn(false); + when(cmdActivationProvider.isShellSupported(anything())).thenReturn(true); + when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); + when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); + + const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); + + expect(cmd).to.deep.equal(expectCommand); + verify(pythonSettings.pythonPath).once(); + verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(cmdActivationProvider.getActivationCommands(resource, anything())).once(); + verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + }); + test('Activation command must return command from Command Prompt if that is supported, and so is bash but no commands are returned', async () => { + const pythonPath = 'some python Path value'; + const expectCommand = ['one', 'two']; + ensureCondaIsSupported(false, pythonPath, []); + + when(cmdActivationProvider.getActivationCommands(resource, anything())).thenResolve(expectCommand); + when(bashActivationProvider.getActivationCommands(resource, anything())).thenResolve([]); + + when(bashActivationProvider.isShellSupported(anything())).thenReturn(true); + when(cmdActivationProvider.isShellSupported(anything())).thenReturn(true); + when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); + when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); + + const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); + + expect(cmd).to.deep.equal(expectCommand); + verify(pythonSettings.pythonPath).once(); + verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); + verify(cmdActivationProvider.getActivationCommands(resource, anything())).once(); + verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + }); + [undefined, pythonInterpreter].forEach((interpreter) => { + test('Activation command for Shell must be empty for unknown os', async () => { + when(platformService.osType).thenReturn(OSType.Unknown); + + for (const item of getNamesAndValues<TerminalShellType>(TerminalShellType)) { + const cmd = await helper.getEnvironmentActivationShellCommands( + resource, + item.value, + interpreter + ); + expect(cmd).to.equal(undefined, 'Command must be undefined'); + } + }); + }); + [undefined, pythonInterpreter].forEach((interpreter) => { + [OSType.Linux, OSType.OSX, OSType.Windows].forEach((osType) => { + test(`Activation command for Shell must never use pipenv nor pyenv (${osType})`, async () => { + const pythonPath = 'some python Path value'; + const shellToExpect = + osType === OSType.Windows ? TerminalShellType.commandPrompt : TerminalShellType.bash; + ensureCondaIsSupported(false, pythonPath, []); + + shellDetectorIdentifyTerminalShell.returns(shellToExpect); + when(platformService.osType).thenReturn(osType); + when(bashActivationProvider.isShellSupported(shellToExpect)).thenReturn(false); + when(cmdActivationProvider.isShellSupported(shellToExpect)).thenReturn(false); + + const cmd = await helper.getEnvironmentActivationShellCommands( + resource, + shellToExpect, + interpreter + ); + + expect(cmd).to.equal(undefined, 'Command must be undefined'); + verify(pythonSettings.pythonPath).times(interpreter ? 0 : 1); + verify(condaService.isCondaEnvironment(pythonPath)).times(interpreter ? 0 : 1); + verify(bashActivationProvider.isShellSupported(shellToExpect)).atLeast(1); + verify(pyenvActivationProvider.isShellSupported(anything())).never(); + verify(pipenvActivationProvider.isShellSupported(anything())).never(); + verify(cmdActivationProvider.isShellSupported(shellToExpect)).atLeast(1); + }); + }); + }); + }); + }); + }); + + suite('Identify Terminal Shell', () => { + setup(doSetup); + test('Use shell detector to identify terminal shells', () => { + const terminal = {} as any; + const expectedShell = TerminalShellType.ksh; + shellDetectorIdentifyTerminalShell.returns(expectedShell); + + const shell = helper.identifyTerminalShell(terminal); + expect(shell).to.be.equal(expectedShell); + expect(shellDetectorIdentifyTerminalShell.callCount).to.equal(1); + expect(shellDetectorIdentifyTerminalShell.args[0]).deep.equal([terminal]); + }); + test('Detector passed throught constructor is used by shell detector class', () => { + const terminal = {} as any; + const expectedShell = TerminalShellType.ksh; + shellDetectorIdentifyTerminalShell.callThrough(); + when(mockDetector.identify(anything(), terminal)).thenReturn(expectedShell); + + const shell = helper.identifyTerminalShell(terminal); + + expect(shell).to.be.equal(expectedShell); + expect(shellDetectorIdentifyTerminalShell.callCount).to.equal(1); + verify(mockDetector.identify(anything(), terminal)).once(); + }); + }); +}); diff --git a/src/test/common/terminals/pyenvActivationProvider.unit.test.ts b/src/test/common/terminals/pyenvActivationProvider.unit.test.ts new file mode 100644 index 000000000000..4da4479b3662 --- /dev/null +++ b/src/test/common/terminals/pyenvActivationProvider.unit.test.ts @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import '../../../client/common/extensions'; +import { PyEnvActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../../../client/common/terminal/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { Architecture } from '../../../client/common/utils/platform'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +suite('Terminal Environment Activation pyenv', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let activationProvider: ITerminalActivationCommandProvider; + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) + .returns(() => interpreterService.object); + + activationProvider = new PyEnvActivationCommandProvider(serviceContainer.object); + }); + + test('All shells should be supported', async () => { + for (const item of getNamesAndValues<TerminalShellType>(TerminalShellType)) { + expect(activationProvider.isShellSupported(item.value)).to.equal(true, 'All shells should be supported'); + } + }); + + test('Ensure no activation commands are returned if intrepreter info is not found', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + const activationCommands = await activationProvider.getActivationCommands(undefined, TerminalShellType.bash); + expect(activationCommands).to.equal(undefined, 'Activation commands should be undefined'); + }); + + test('Ensure no activation commands are returned if intrepreter is not pyenv', async () => { + const intepreterInfo: PythonEnvironment = { + architecture: Architecture.Unknown, + path: '', + sysPrefix: '', + version: new SemVer('1.1.1-alpha'), + sysVersion: '', + envType: EnvironmentType.Unknown + }; + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(intepreterInfo)) + .verifiable(TypeMoq.Times.once()); + + const activationCommands = await activationProvider.getActivationCommands(undefined, TerminalShellType.bash); + expect(activationCommands).to.equal(undefined, 'Activation commands should be undefined'); + }); + + test('Ensure no activation commands are returned if intrepreter envName is empty', async () => { + const intepreterInfo: PythonEnvironment = { + architecture: Architecture.Unknown, + path: '', + sysPrefix: '', + version: new SemVer('1.1.1-alpha'), + sysVersion: '', + envType: EnvironmentType.Pyenv + }; + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(intepreterInfo)) + .verifiable(TypeMoq.Times.once()); + + const activationCommands = await activationProvider.getActivationCommands(undefined, TerminalShellType.bash); + expect(activationCommands).to.equal(undefined, 'Activation commands should be undefined'); + }); + + test('Ensure activation command is returned', async () => { + const intepreterInfo: PythonEnvironment = { + architecture: Architecture.Unknown, + path: '', + sysPrefix: '', + version: new SemVer('1.1.1-alpha'), + sysVersion: '', + envType: EnvironmentType.Pyenv, + envName: 'my env name' + }; + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(intepreterInfo)) + .verifiable(TypeMoq.Times.once()); + + const activationCommands = await activationProvider.getActivationCommands(undefined, TerminalShellType.bash); + expect(activationCommands).to.deep.equal( + [`pyenv shell "${intepreterInfo.envName}"`], + 'Invalid Activation command' + ); + }); +}); diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts new file mode 100644 index 000000000000..901c0eb63697 --- /dev/null +++ b/src/test/common/terminals/service.unit.test.ts @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Terminal as VSCodeTerminal, WorkspaceConfiguration } from 'vscode'; +import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { TerminalService } from '../../../client/common/terminal/service'; +import { ITerminalActivator, ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal Service', () => { + let service: TerminalService; + let terminal: TypeMoq.IMock<VSCodeTerminal>; + let terminalManager: TypeMoq.IMock<ITerminalManager>; + let terminalHelper: TypeMoq.IMock<ITerminalHelper>; + let terminalActivator: TypeMoq.IMock<ITerminalActivator>; + let platformService: TypeMoq.IMock<IPlatformService>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let disposables: Disposable[] = []; + let mockServiceContainer: TypeMoq.IMock<IServiceContainer>; + setup(() => { + terminal = TypeMoq.Mock.ofType<VSCodeTerminal>(); + terminalManager = TypeMoq.Mock.ofType<ITerminalManager>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + terminalHelper = TypeMoq.Mock.ofType<ITerminalHelper>(); + terminalActivator = TypeMoq.Mock.ofType<ITerminalActivator>(); + disposables = []; + + mockServiceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + mockServiceContainer.setup((c) => c.get(ITerminalManager)).returns(() => terminalManager.object); + mockServiceContainer.setup((c) => c.get(ITerminalHelper)).returns(() => terminalHelper.object); + mockServiceContainer.setup((c) => c.get(IPlatformService)).returns(() => platformService.object); + mockServiceContainer.setup((c) => c.get(IDisposableRegistry)).returns(() => disposables); + mockServiceContainer.setup((c) => c.get(IWorkspaceService)).returns(() => workspaceService.object); + mockServiceContainer.setup((c) => c.get(ITerminalActivator)).returns(() => terminalActivator.object); + }); + teardown(() => { + if (service) { + // tslint:disable-next-line:no-any + service.dispose(); + } + disposables.filter((item) => !!item).forEach((item) => item.dispose()); + }); + + test('Ensure terminal is disposed', async () => { + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + const os: string = 'windows'; + service = new TerminalService(mockServiceContainer.object); + const shellPath = 'powershell.exe'; + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('terminal.integrated.shell'))) + .returns(() => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceConfig.setup((c) => c.get(os)).returns(() => shellPath); + return workspaceConfig.object; + }); + + platformService.setup((p) => p.isWindows).returns(() => os === 'windows'); + platformService.setup((p) => p.isLinux).returns(() => os === 'linux'); + platformService.setup((p) => p.isMac).returns(() => os === 'osx'); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + terminalHelper + .setup((h) => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => 'dummy text'); + + // Sending a command will cause the terminal to be created + await service.sendCommand('', []); + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); + service.dispose(); + terminal.verify((t) => t.dispose(), TypeMoq.Times.exactly(1)); + }); + + test('Ensure command is sent to terminal and it is shown', async () => { + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + const commandToSend = 'SomeCommand'; + const args = ['1', '2']; + const commandToExpect = [commandToSend].concat(args).join(' '); + terminalHelper + .setup((h) => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => commandToExpect); + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.sendCommand(commandToSend, args); + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); + terminal.verify( + (t) => t.sendText(TypeMoq.It.isValue(commandToExpect), TypeMoq.It.isValue(true)), + TypeMoq.Times.exactly(1) + ); + }); + + test('Ensure text is sent to terminal and it is shown', async () => { + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.sendText(textToSend); + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure terminal is not shown if `hideFromUser` option is set to `true`', async () => { + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object, { hideFromUser: true }); + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.show(); + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.never()); + }); + + test('Ensure terminal shown otherwise', async () => { + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.show(); + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); + }); + + test('Ensure terminal shown and focus is set to the Terminal', async () => { + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.show(false); + + terminal.verify((t) => t.show(TypeMoq.It.isValue(false)), TypeMoq.Times.exactly(2)); + }); + + test('Ensure terminal is activated once after creation', async () => { + service = new TerminalService(mockServiceContainer.object); + terminalActivator + .setup((h) => h.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + terminalManager + .setup((t) => t.createTerminal(TypeMoq.It.isAny())) + .returns(() => terminal.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + + await service.show(); + await service.show(); + await service.show(); + await service.show(); + + terminalHelper.verifyAll(); + terminalActivator.verifyAll(); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); + }); + + test('Ensure terminal is activated once before sending text', async () => { + service = new TerminalService(mockServiceContainer.object); + const textToSend = 'Some Text'; + terminalActivator + .setup((h) => h.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + terminalManager + .setup((t) => t.createTerminal(TypeMoq.It.isAny())) + .returns(() => terminal.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + + await service.sendText(textToSend); + await service.sendText(textToSend); + await service.sendText(textToSend); + await service.sendText(textToSend); + + terminalHelper.verifyAll(); + terminalActivator.verifyAll(); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); + }); + + test('Ensure close event is not fired when another terminal is closed', async () => { + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + let eventFired = false; + let eventHandler: undefined | (() => void); + terminalManager + .setup((m) => m.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((handler) => { + eventHandler = handler; + // tslint:disable-next-line:no-empty + return { dispose: () => {} }; + }); + service = new TerminalService(mockServiceContainer.object); + service.onDidCloseTerminal(() => (eventFired = true), service); + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + // This will create the terminal. + await service.sendText('blah'); + + expect(eventHandler).not.to.be.an('undefined', 'event handler not initialized'); + eventHandler!.bind(service)(); + expect(eventFired).to.be.equal(false, 'Event fired'); + }); + + test('Ensure close event is not fired when terminal is closed', async () => { + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + let eventFired = false; + let eventHandler: undefined | ((t: VSCodeTerminal) => void); + terminalManager + .setup((m) => m.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((handler) => { + eventHandler = handler; + // tslint:disable-next-line:no-empty + return { dispose: () => {} }; + }); + service = new TerminalService(mockServiceContainer.object); + service.onDidCloseTerminal(() => (eventFired = true)); + + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + // This will create the terminal. + await service.sendText('blah'); + + expect(eventHandler).not.to.be.an('undefined', 'event handler not initialized'); + eventHandler!.bind(service)(terminal.object); + expect(eventFired).to.be.equal(true, 'Event not fired'); + }); +}); diff --git a/src/test/common/terminals/serviceRegistry.unit.test.ts b/src/test/common/terminals/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..8bc7e1670016 --- /dev/null +++ b/src/test/common/terminals/serviceRegistry.unit.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; +import { ExtensionActivationForTerminalActivation, TerminalAutoActivation } from '../../../client/terminals/activation'; +import { CodeExecutionManager } from '../../../client/terminals/codeExecution/codeExecutionManager'; +import { DjangoShellCodeExecutionProvider } from '../../../client/terminals/codeExecution/djangoShellCodeExecution'; +import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; +import { ReplProvider } from '../../../client/terminals/codeExecution/repl'; +import { TerminalCodeExecutionProvider } from '../../../client/terminals/codeExecution/terminalCodeExecution'; +import { registerTypes } from '../../../client/terminals/serviceRegistry'; +import { + ICodeExecutionHelper, + ICodeExecutionManager, + ICodeExecutionService, + ITerminalAutoActivation +} from '../../../client/terminals/types'; + +suite('Common Terminal Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify(serviceManager.addSingleton<ICodeExecutionHelper>(ICodeExecutionHelper, CodeExecutionHelper)).once(); + + verify(serviceManager.addSingleton<ICodeExecutionManager>(ICodeExecutionManager, CodeExecutionManager)).once(); + + verify( + serviceManager.addSingleton<ICodeExecutionService>( + ICodeExecutionService, + DjangoShellCodeExecutionProvider, + 'djangoShell' + ) + ).once(); + verify( + serviceManager.addSingleton<ICodeExecutionService>( + ICodeExecutionService, + TerminalCodeExecutionProvider, + 'standard' + ) + ).once(); + verify(serviceManager.addSingleton<ICodeExecutionService>(ICodeExecutionService, ReplProvider, 'repl')).once(); + + verify( + serviceManager.addSingleton<ITerminalAutoActivation>(ITerminalAutoActivation, TerminalAutoActivation) + ).once(); + + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ExtensionActivationForTerminalActivation + ) + ).once(); + }); +}); diff --git a/src/test/common/terminals/shellDetector.unit.test.ts b/src/test/common/terminals/shellDetector.unit.test.ts new file mode 100644 index 000000000000..8afa2b18ba4a --- /dev/null +++ b/src/test/common/terminals/shellDetector.unit.test.ts @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { ApplicationEnvironment } from '../../../client/common/application/applicationEnvironment'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { ShellDetector } from '../../../client/common/terminal/shellDetector'; +import { SettingsShellDetector } from '../../../client/common/terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from '../../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from '../../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from '../../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; +import { TerminalShellType } from '../../../client/common/terminal/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { OSType } from '../../../client/common/utils/platform'; +import { MockProcess } from '../../../test/mocks/process'; + +// tslint:disable:max-func-body-length no-any + +suite('Shell Detector', () => { + let platformService: IPlatformService; + const defaultOSShells = { + [OSType.Linux]: TerminalShellType.bash, + [OSType.OSX]: TerminalShellType.bash, + [OSType.Windows]: TerminalShellType.commandPrompt, + [OSType.Unknown]: TerminalShellType.other + }; + const sandbox = sinon.createSandbox(); + setup(() => (platformService = mock(PlatformService))); + teardown(() => sandbox.restore()); + + getNamesAndValues<OSType>(OSType).forEach((os) => { + const testSuffix = `(OS ${os.name})`; + test('Test identification of Terminal Shells in order of priority', async () => { + const callOrder: string[] = []; + const nameDetectorIdentify = sandbox.stub(TerminalNameShellDetector.prototype, 'identify'); + nameDetectorIdentify.callsFake(() => { + callOrder.push('calledFirst'); + return undefined; + }); + const vscEnvDetectorIdentify = sandbox.stub(VSCEnvironmentShellDetector.prototype, 'identify'); + vscEnvDetectorIdentify.callsFake(() => { + callOrder.push('calledSecond'); + return undefined; + }); + const userEnvDetectorIdentify = sandbox.stub(UserEnvironmentShellDetector.prototype, 'identify'); + userEnvDetectorIdentify.callsFake(() => { + callOrder.push('calledLast'); + return undefined; + }); + const settingsDetectorIdentify = sandbox.stub(SettingsShellDetector.prototype, 'identify'); + settingsDetectorIdentify.callsFake(() => { + callOrder.push('calledThird'); + return undefined; + }); + + when(platformService.osType).thenReturn(os.value); + const nameDetector = new TerminalNameShellDetector(); + const vscEnvDetector = new VSCEnvironmentShellDetector(instance(mock(ApplicationEnvironment))); + const userEnvDetector = new UserEnvironmentShellDetector(mock(MockProcess), instance(platformService)); + const settingsDetector = new SettingsShellDetector( + instance(mock(WorkspaceService)), + instance(platformService) + ); + const detectors = [settingsDetector, userEnvDetector, nameDetector, vscEnvDetector]; + const shellDetector = new ShellDetector(instance(platformService), detectors); + + shellDetector.identifyTerminalShell(); + + expect(callOrder).to.deep.equal(['calledFirst', 'calledSecond', 'calledThird', 'calledLast']); + }); + test(`Use default shell based on OS if there are no shell detectors ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + when(platformService.osType).thenReturn(os.value); + const shellDetector = new ShellDetector(instance(platformService), []); + + const shell = shellDetector.identifyTerminalShell(); + + expect(shell).to.be.equal(defaultOSShells[os.value]); + }); + test(`Use default shell based on OS if there are no shell detectors (when a terminal is provided) ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + const shellDetector = new ShellDetector(instance(platformService), []); + + const shell = shellDetector.identifyTerminalShell({ name: 'bash' } as any); + + expect(shell).to.be.equal(defaultOSShells[os.value]); + }); + test(`Use shell provided by detector ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + const detector = mock(UserEnvironmentShellDetector); + const detectedShell = TerminalShellType.xonsh; + when(detector.identify(anything(), anything())).thenReturn(detectedShell); + const shellDetector = new ShellDetector(instance(platformService), [instance(detector)]); + + const shell = shellDetector.identifyTerminalShell(); + + expect(shell).to.be.equal(detectedShell); + verify(detector.identify(anything(), undefined)).once(); + }); + test(`Use shell provided by detector (when a terminal is provided) ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + const terminal = { name: 'bash' } as any; + const detector = mock(UserEnvironmentShellDetector); + const detectedShell = TerminalShellType.xonsh; + when(detector.identify(anything(), anything())).thenReturn(detectedShell); + const shellDetector = new ShellDetector(instance(platformService), [instance(detector)]); + + const shell = shellDetector.identifyTerminalShell(terminal); + + expect(shell).to.be.equal(detectedShell); + verify(detector.identify(anything(), terminal)).once(); + }); + test(`Use shell provided by detector with highest priority ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + const detector1 = mock(UserEnvironmentShellDetector); + const detector2 = mock(UserEnvironmentShellDetector); + const detector3 = mock(UserEnvironmentShellDetector); + const detectedShell = TerminalShellType.xonsh; + when(detector1.priority).thenReturn(0); + when(detector2.priority).thenReturn(2); + when(detector3.priority).thenReturn(1); + when(detector1.identify(anything(), anything())).thenReturn(TerminalShellType.tcshell); + when(detector2.identify(anything(), anything())).thenReturn(detectedShell); + when(detector3.identify(anything(), anything())).thenReturn(TerminalShellType.fish); + const shellDetector = new ShellDetector(instance(platformService), [ + instance(detector1), + instance(detector2), + instance(detector3) + ]); + + const shell = shellDetector.identifyTerminalShell(); + + expect(shell).to.be.equal(detectedShell); + verify(detector1.identify(anything(), anything())).never(); + verify(detector2.identify(anything(), undefined)).once(); + verify(detector3.identify(anything(), anything())).never(); + }); + test(`Fall back to detectors that can identify a shell ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + const detector1 = mock(UserEnvironmentShellDetector); + const detector2 = mock(UserEnvironmentShellDetector); + const detector3 = mock(UserEnvironmentShellDetector); + const detector4 = mock(UserEnvironmentShellDetector); + const detectedShell = TerminalShellType.xonsh; + when(detector1.priority).thenReturn(1); + when(detector2.priority).thenReturn(2); + when(detector3.priority).thenReturn(3); + when(detector4.priority).thenReturn(4); + when(detector1.identify(anything(), anything())).thenReturn(TerminalShellType.ksh); + when(detector2.identify(anything(), anything())).thenReturn(detectedShell); + when(detector3.identify(anything(), anything())).thenReturn(undefined); + when(detector4.identify(anything(), anything())).thenReturn(undefined); + const shellDetector = new ShellDetector(instance(platformService), [ + instance(detector1), + instance(detector2), + instance(detector3), + instance(detector4) + ]); + + const shell = shellDetector.identifyTerminalShell(); + + expect(shell).to.be.equal(detectedShell); + verify(detector1.identify(anything(), anything())).never(); + verify(detector2.identify(anything(), undefined)).once(); + verify(detector3.identify(anything(), anything())).once(); + verify(detector4.identify(anything(), anything())).once(); + }); + test(`Fall back to detectors that can identify a shell ${testSuffix} (even if detected shell is other)`, () => { + when(platformService.osType).thenReturn(os.value); + const detector1 = mock(UserEnvironmentShellDetector); + const detector2 = mock(UserEnvironmentShellDetector); + const detector3 = mock(UserEnvironmentShellDetector); + const detector4 = mock(UserEnvironmentShellDetector); + const detectedShell = TerminalShellType.xonsh; + when(detector1.priority).thenReturn(1); + when(detector2.priority).thenReturn(2); + when(detector3.priority).thenReturn(3); + when(detector4.priority).thenReturn(4); + when(detector1.identify(anything(), anything())).thenReturn(TerminalShellType.ksh); + when(detector2.identify(anything(), anything())).thenReturn(detectedShell); + when(detector3.identify(anything(), anything())).thenReturn(TerminalShellType.other); + when(detector4.identify(anything(), anything())).thenReturn(TerminalShellType.other); + const shellDetector = new ShellDetector(instance(platformService), [ + instance(detector1), + instance(detector2), + instance(detector3), + instance(detector4) + ]); + + const shell = shellDetector.identifyTerminalShell(); + + expect(shell).to.be.equal(detectedShell); + verify(detector1.identify(anything(), anything())).never(); + verify(detector2.identify(anything(), undefined)).once(); + verify(detector3.identify(anything(), anything())).once(); + verify(detector4.identify(anything(), anything())).once(); + }); + }); +}); diff --git a/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts b/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts new file mode 100644 index 000000000000..7abd0c28d74e --- /dev/null +++ b/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { instance, mock, when } from 'ts-mockito'; +import { ApplicationEnvironment } from '../../../../client/common/application/applicationEnvironment'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { CurrentProcess } from '../../../../client/common/process/currentProcess'; +import { SettingsShellDetector } from '../../../../client/common/terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from '../../../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from '../../../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from '../../../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; +import { ShellIdentificationTelemetry, TerminalShellType } from '../../../../client/common/terminal/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { OSType } from '../../../../client/common/utils/platform'; + +// tslint:disable:max-func-body-length no-any + +suite('Shell Detectors', () => { + let platformService: IPlatformService; + let currentProcess: CurrentProcess; + let workspaceService: WorkspaceService; + let appEnv: ApplicationEnvironment; + + // Dummy data for testing. + const shellPathsAndIdentification = new Map<string, TerminalShellType>(); + shellPathsAndIdentification.set('c:\\windows\\system32\\cmd.exe', TerminalShellType.commandPrompt); + shellPathsAndIdentification.set('c:\\windows\\system32\\bash.exe', TerminalShellType.bash); + shellPathsAndIdentification.set('c:\\windows\\system32\\wsl.exe', TerminalShellType.wsl); + shellPathsAndIdentification.set('c:\\windows\\system32\\gitbash.exe', TerminalShellType.gitbash); + shellPathsAndIdentification.set('/usr/bin/bash', TerminalShellType.bash); + shellPathsAndIdentification.set('/usr/bin/zsh', TerminalShellType.zsh); + shellPathsAndIdentification.set('/usr/bin/ksh', TerminalShellType.ksh); + shellPathsAndIdentification.set('c:\\windows\\system32\\powershell.exe', TerminalShellType.powershell); + shellPathsAndIdentification.set('c:\\windows\\system32\\pwsh.exe', TerminalShellType.powershellCore); + shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/powershell', TerminalShellType.powershell); + shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/pwsh', TerminalShellType.powershellCore); + shellPathsAndIdentification.set('/usr/bin/fish', TerminalShellType.fish); + shellPathsAndIdentification.set('c:\\windows\\system32\\shell.exe', TerminalShellType.other); + shellPathsAndIdentification.set('/usr/bin/shell', TerminalShellType.other); + shellPathsAndIdentification.set('/usr/bin/csh', TerminalShellType.cshell); + shellPathsAndIdentification.set('/usr/bin/tcsh', TerminalShellType.tcshell); + shellPathsAndIdentification.set('/usr/bin/xonsh', TerminalShellType.xonsh); + shellPathsAndIdentification.set('/usr/bin/xonshx', TerminalShellType.other); + + const telemetryProperties: ShellIdentificationTelemetry = { + failed: true, + shellIdentificationSource: 'default', + terminalProvided: false, + hasCustomShell: undefined, + hasShellInEnv: undefined + }; + + setup(() => { + platformService = mock(PlatformService); + workspaceService = mock(WorkspaceService); + currentProcess = mock(CurrentProcess); + appEnv = mock(ApplicationEnvironment); + }); + test('Test Priority of detectors', async () => { + expect(new TerminalNameShellDetector().priority).to.equal(4); + expect(new VSCEnvironmentShellDetector(instance(appEnv)).priority).to.equal(3); + expect(new SettingsShellDetector(instance(workspaceService), instance(platformService)).priority).to.equal(2); + expect(new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)).priority).to.equal( + 1 + ); + }); + test('Test identification of Terminal Shells (base class method)', async () => { + const shellDetector = new TerminalNameShellDetector(); + shellPathsAndIdentification.forEach((shellType, shellPath) => { + expect(shellDetector.identifyShellFromShellPath(shellPath)).to.equal( + shellType, + `Incorrect Shell Type for path '${shellPath}'` + ); + }); + }); + test('Identify shell based on name of terminal', async () => { + const shellDetector = new TerminalNameShellDetector(); + shellPathsAndIdentification.forEach((shellType, shellPath) => { + expect(shellDetector.identify(telemetryProperties, { name: shellPath } as any)).to.equal( + shellType, + `Incorrect Shell Type for name '${shellPath}'` + ); + }); + + expect(shellDetector.identify(telemetryProperties, undefined)).to.equal( + undefined, + 'Should be undefined when there is no temrinal' + ); + }); + test('Identify shell based on VSC Environment', async () => { + const shellDetector = new VSCEnvironmentShellDetector(instance(appEnv)); + shellPathsAndIdentification.forEach((shellType, shellPath) => { + when(appEnv.shell).thenReturn(shellPath); + expect(shellDetector.identify(telemetryProperties, { name: shellPath } as any)).to.equal( + shellType, + `Incorrect Shell Type from identifyShellByTerminalName, for path '${shellPath}'` + ); + }); + + when(appEnv.shell).thenReturn(undefined as any); + expect(shellDetector.identify(telemetryProperties, undefined)).to.equal( + undefined, + 'Should be undefined when vscode.env.shell is undefined' + ); + }); + test('Identify shell based on VSC Settings', async () => { + const shellDetector = new SettingsShellDetector(instance(workspaceService), instance(platformService)); + shellPathsAndIdentification.forEach((shellType, shellPath) => { + // Assume the same paths are stored in user settings, we should still be able to identify the shell. + shellDetector.getTerminalShellPath = () => shellPath; + expect(shellDetector.identify(telemetryProperties, {} as any)).to.equal( + shellType, + `Incorrect Shell Type for path '${shellPath}'` + ); + }); + }); + getNamesAndValues<OSType>(OSType).forEach((os) => { + test(`Get shell path from settings (OS ${os.name})`, async () => { + const shellPathInSettings = 'some value'; + const shellDetector = new SettingsShellDetector(instance(workspaceService), instance(platformService)); + const getStub = sinon.stub(); + const config = { get: getStub } as any; + getStub.returns(shellPathInSettings); + when(workspaceService.getConfiguration('terminal.integrated.shell')).thenReturn(config); + when(platformService.osType).thenReturn(os.value); + + const shellPath = shellDetector.getTerminalShellPath(); + + expect(shellPath).to.equal(os.value === OSType.Unknown ? '' : shellPathInSettings); + expect(getStub.callCount).to.equal(os.value === OSType.Unknown ? 0 : 1); + if (os.value !== OSType.Unknown) { + expect(getStub.args[0][0]).to.equal(os.name.toLowerCase()); + } + }); + }); + test('Identify shell based on user environment variables', async () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + shellPathsAndIdentification.forEach((shellType, shellPath) => { + // Assume the same paths are defined in user environment variables, we should still be able to identify the shell. + shellDetector.getDefaultPlatformShell = () => shellPath; + expect(shellDetector.identify(telemetryProperties, {} as any)).to.equal( + shellType, + `Incorrect Shell Type for path '${shellPath}'` + ); + }); + }); + test('Default shell on Windows < 10 is cmd.exe', () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.Windows); + when(platformService.osRelease).thenReturn('7'); + when(currentProcess.env).thenReturn({}); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('cmd.exe'); + }); + test('Default shell on Windows >= 10 32bit is powershell.exe', () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.Windows); + when(platformService.osRelease).thenReturn('10'); + when(currentProcess.env).thenReturn({ windir: 'WindowsDir', PROCESSOR_ARCHITEW6432: '', comspec: 'hello.exe' }); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('WindowsDir\\Sysnative\\WindowsPowerShell\\v1.0\\powershell.exe'); + }); + test('Default shell on Windows >= 10 64bit is powershell.exe', () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.Windows); + when(platformService.osRelease).thenReturn('10'); + when(currentProcess.env).thenReturn({ windir: 'WindowsDir', comspec: 'hello.exe' }); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('WindowsDir\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'); + }); + test('Default shell on Windows < 10 is what ever is defined in env.comspec', () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.Windows); + when(platformService.osRelease).thenReturn('7'); + when(currentProcess.env).thenReturn({ comspec: 'hello.exe' }); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('hello.exe'); + }); + [OSType.OSX, OSType.Linux].forEach((osType) => { + test(`Default shell on ${osType} is /bin/bash`, () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.OSX); + when(currentProcess.env).thenReturn({}); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('/bin/bash'); + }); + test(`Default shell on ${osType} is what ever is in env.SHELL`, () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.OSX); + when(currentProcess.env).thenReturn({ SHELL: 'hello terminal.app' }); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('hello terminal.app'); + }); + test(`Default shell on ${osType} is what ever is /bin/bash if env.SHELL == /bin/false`, () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.OSX); + when(currentProcess.env).thenReturn({ SHELL: '/bin/false' }); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('/bin/bash'); + }); + }); +}); diff --git a/src/test/common/terminals/synchronousTerminalService.unit.test.ts b/src/test/common/terminals/synchronousTerminalService.unit.test.ts new file mode 100644 index 000000000000..75b0e27bb63a --- /dev/null +++ b/src/test/common/terminals/synchronousTerminalService.unit.test.ts @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as path from 'path'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { CancellationTokenSource } from 'vscode'; +import { CancellationError } from '../../../client/common/cancellation'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { TerminalService } from '../../../client/common/terminal/service'; +import { SynchronousTerminalService } from '../../../client/common/terminal/syncTerminalService'; +import { createDeferredFrom } from '../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { noop, sleep } from '../../core'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal Service (synchronous)', () => { + let service: SynchronousTerminalService; + let fs: IFileSystem; + let interpreterService: IInterpreterService; + let terminalService: TerminalService; + setup(() => { + fs = mock(FileSystem); + interpreterService = mock(InterpreterService); + terminalService = mock(TerminalService); + service = new SynchronousTerminalService(instance(fs), instance(interpreterService), instance(terminalService)); + }); + suite('Show, sendText and dispose should invoke corressponding methods in wrapped TerminalService', () => { + test('Show should invoke show in terminal', async () => { + when(terminalService.show(anything())).thenResolve(); + await service.show(); + verify(terminalService.show(undefined)).once(); + }); + test('Show should invoke show in terminal (without chaning focus)', async () => { + when(terminalService.show(anything())).thenResolve(); + await service.show(false); + verify(terminalService.show(false)).once(); + }); + test('Show should invoke show in terminal (without chaning focus)', async () => { + when(terminalService.show(anything())).thenResolve(); + await service.show(false); + verify(terminalService.show(false)).once(); + }); + test('Show should invoke show in terminal (without chaning focus)', async () => { + when(terminalService.show(anything())).thenResolve(); + await service.show(false); + verify(terminalService.show(false)).once(); + }); + test('Dispose should dipose the wrapped TerminalService', async () => { + service.dispose(); + verify(terminalService.dispose()).once(); + }); + test('sendText should invokeSendText in wrapped TerminalService', async () => { + when(terminalService.sendText('Blah')).thenResolve(); + await service.sendText('Blah'); + verify(terminalService.sendText('Blah')).once(); + }); + test('sendText should invokeSendText in wrapped TerminalService (errors should be bubbled up)', async () => { + when(terminalService.sendText('Blah')).thenReject(new Error('kaboom')); + const promise = service.sendText('Blah'); + + await assert.isRejected(promise, 'kaboom'); + verify(terminalService.sendText('Blah')).once(); + }); + }); + suite('sendCommand', () => { + const isolated = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'pyvsc-run-isolated.py').replace(/\\/g, '/'); + const shellExecFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'shell_exec.py'); + + test('run sendCommand in terminalService if there is no cancellation token', async () => { + when(terminalService.sendCommand('cmd', deepEqual(['1', '2']))).thenResolve(); + await service.sendCommand('cmd', ['1', '2']); + verify(terminalService.sendCommand('cmd', deepEqual(['1', '2']))).once(); + }); + test('run sendCommand in terminalService should be cancelled', async () => { + const cancel = new CancellationTokenSource(); + const tmpFile = { filePath: 'tmp with spaces', dispose: noop }; + when(terminalService.sendCommand(anything(), anything())).thenResolve(); + when(interpreterService.getActiveInterpreter(undefined)).thenResolve(undefined); + when(fs.createTemporaryFile('.log')).thenResolve(tmpFile); + when(fs.readFile(anything())).thenResolve(''); + + // Send the necessary commands to the terminal. + const promise = service.sendCommand('cmd', ['1', '2'], cancel.token).catch((ex) => Promise.reject(ex)); + + const deferred = createDeferredFrom(promise); + // required to shutup node (we must handled exceptions). + deferred.promise.ignoreErrors(); + + // Should not have completed. + assert.isFalse(deferred.completed); + + // Wait for some time, and it should still not be completed + // Should complete only after command has executed successfully or been cancelled. + await sleep(500); + assert.isFalse(deferred.completed); + + // If cancelled, then throw cancellation error. + cancel.cancel(); + + await assert.isRejected(promise, new CancellationError().message); + verify(fs.createTemporaryFile('.log')).once(); + verify(fs.readFile(tmpFile.filePath)).atLeast(1); + verify( + terminalService.sendCommand( + 'python', + deepEqual([isolated, shellExecFile, 'cmd', '1', '2', tmpFile.filePath.fileToCommandArgument()]) + ) + ).once(); + }).timeout(1_000); + test('run sendCommand in terminalService should complete when command completes', async () => { + const cancel = new CancellationTokenSource(); + const tmpFile = { filePath: 'tmp with spaces', dispose: noop }; + when(terminalService.sendCommand(anything(), anything())).thenResolve(); + when(interpreterService.getActiveInterpreter(undefined)).thenResolve(undefined); + when(fs.createTemporaryFile('.log')).thenResolve(tmpFile); + when(fs.readFile(anything())).thenResolve(''); + + // Send the necessary commands to the terminal. + const promise = service.sendCommand('cmd', ['1', '2'], cancel.token).catch((ex) => Promise.reject(ex)); + + const deferred = createDeferredFrom(promise); + // required to shutup node (we must handled exceptions). + deferred.promise.ignoreErrors(); + + // Should not have completed. + assert.isFalse(deferred.completed); + + // Wait for some time, and it should still not be completed + // Should complete only after command has executed successfully or been cancelled. + await sleep(500); + assert.isFalse(deferred.completed); + + // Write `END` into file, to trigger completion of the command. + when(fs.readFile(anything())).thenResolve('END'); + + await promise; + verify(fs.createTemporaryFile('.log')).once(); + verify(fs.readFile(tmpFile.filePath)).atLeast(1); + verify( + terminalService.sendCommand( + 'python', + deepEqual([isolated, shellExecFile, 'cmd', '1', '2', tmpFile.filePath.fileToCommandArgument()]) + ) + ).once(); + }).timeout(2_000); + }); +}); diff --git a/src/test/common/utils/async.unit.test.ts b/src/test/common/utils/async.unit.test.ts new file mode 100644 index 000000000000..9202035a625f --- /dev/null +++ b/src/test/common/utils/async.unit.test.ts @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { chain, createDeferred, flattenIterator } from '../../../client/common/utils/async'; + +suite('Deferred', () => { + test('Resolve', (done) => { + const valueToSent = new Date().getTime(); + const def = createDeferred<number>(); + def.promise + .then((value) => { + assert.equal(value, valueToSent); + assert.equal(def.resolved, true, 'resolved property value is not `true`'); + }) + .then(done) + .catch(done); + + assert.equal(def.resolved, false, 'Promise is resolved even when it should not be'); + assert.equal(def.rejected, false, 'Promise is rejected even when it should not be'); + assert.equal(def.completed, false, 'Promise is completed even when it should not be'); + + def.resolve(valueToSent); + + assert.equal(def.resolved, true, 'Promise is not resolved even when it should not be'); + assert.equal(def.rejected, false, 'Promise is rejected even when it should not be'); + assert.equal(def.completed, true, 'Promise is not completed even when it should not be'); + }); + test('Reject', (done) => { + const errorToSend = new Error('Something'); + const def = createDeferred<number>(); + def.promise + .then((value) => { + assert.fail(value, 'Error', 'Was expecting promise to get rejected, however it was resolved', ''); + done(); + }) + .catch((reason) => { + assert.equal(reason, errorToSend, 'Error received is not the same'); + done(); + }) + .catch(done); + + assert.equal(def.resolved, false, 'Promise is resolved even when it should not be'); + assert.equal(def.rejected, false, 'Promise is rejected even when it should not be'); + assert.equal(def.completed, false, 'Promise is completed even when it should not be'); + + def.reject(errorToSend); + + assert.equal(def.resolved, false, 'Promise is resolved even when it should not be'); + assert.equal(def.rejected, true, 'Promise is not rejected even when it should not be'); + assert.equal(def.completed, true, 'Promise is not completed even when it should not be'); + }); +}); + +suite('chain async iterators', () => { + const flatten = flattenIterator; + + test('no iterators', async () => { + const expected: string[] = []; + + const results = await flatten(chain([])); + + assert.deepEqual(results, expected); + }); + + test('one iterator, one item', async () => { + const expected = ['foo']; + const it = (async function* () { + yield 'foo'; + })(); + + const results = await flatten(chain([it])); + + assert.deepEqual(results, expected); + }); + + test('one iterator, many items', async () => { + const expected = ['foo', 'bar', 'baz']; + const it = (async function* () { + yield* expected; + })(); + + const results = await flatten(chain([it])); + + assert.deepEqual(results, expected); + }); + + test('one iterator, no items', async () => { + const deferred = createDeferred<void>(); + const it = (async function* () { + deferred.resolve(); + })(); + + const results = await flatten(chain([it])); + + assert.deepEqual(results, []); + // Make sure chain() actually used up the iterator, + // even through it didn't yield anything. + assert.ok(deferred.resolved); + }); + + test('many iterators, one item each', async () => { + // For deterministic results we must control when each iterator starts. + const deferred12 = createDeferred<void>(); + const deferred23 = createDeferred<void>(); + const expected = ['a', 'b', 'c']; + const it1 = (async function* () { + yield 'a'; + deferred12.resolve(); + })(); + const it2 = (async function* () { + await deferred12.promise; + yield 'b'; + deferred23.resolve(); + })(); + const it3 = (async function* () { + await deferred23.promise; + yield 'c'; + })(); + + const results = await flatten(chain([it1, it2, it3])); + + assert.deepEqual(results, expected); + }); + + test('many iterators, many items each', async () => { + // For deterministic results we must control when each iterator starts. + const deferred12 = createDeferred<void>(); + const deferred23 = createDeferred<void>(); + const expected = ['a1', 'a2', 'a3', 'b1', 'b2', 'b3', 'c1', 'c2', 'c3']; + const it1 = (async function* () { + yield 'a1'; + yield 'a2'; + yield 'a3'; + deferred12.resolve(); + })(); + const it2 = (async function* () { + await deferred12.promise; + yield 'b1'; + yield 'b2'; + yield 'b3'; + deferred23.resolve(); + })(); + const it3 = (async function* () { + await deferred23.promise; + yield 'c1'; + yield 'c2'; + yield 'c3'; + })(); + + const results = await flatten(chain([it1, it2, it3])); + + assert.deepEqual(results, expected); + }); + + test('many iterators, one empty', async () => { + // For deterministic results we must control when each iterator starts. + const deferred12 = createDeferred<void>(); + const deferred23 = createDeferred<void>(); + const expected = ['a', 'c']; + const it1 = (async function* () { + yield 'a'; + deferred12.resolve(); + })(); + const it2 = (async function* () { + await deferred12.promise; + // We do not yield anything. + deferred23.resolve(); + })(); + const it3 = (async function* () { + await deferred23.promise; + yield 'c'; + })(); + const empty = it2; + + const results = await flatten(chain([it1, empty, it3])); + + assert.deepEqual(results, expected); + }); + + test('Results are yielded as soon as ready, regardless of source iterator.', async () => { + // For deterministic results we must control when each iterator starts. + const deferred24 = createDeferred<void>(); + const deferred41 = createDeferred<void>(); + const deferred13 = createDeferred<void>(); + const deferred35 = createDeferred<void>(); + const deferred56 = createDeferred<void>(); + const expected = ['b', 'd', 'a', 'c', 'e', 'f']; + const it1 = (async function* () { + await deferred41.promise; + yield 'a'; + deferred13.resolve(); + })(); + const it2 = (async function* () { + yield 'b'; + deferred24.resolve(); + })(); + const it3 = (async function* () { + await deferred13.promise; + yield 'c'; + deferred35.resolve(); + })(); + const it4 = (async function* () { + await deferred24.promise; + yield 'd'; + deferred41.resolve(); + })(); + const it5 = (async function* () { + await deferred35.promise; + yield 'e'; + deferred56.resolve(); + })(); + const it6 = (async function* () { + await deferred56.promise; + yield 'f'; + })(); + + const results = await flatten(chain([it1, it2, it3, it4, it5, it6])); + + assert.deepEqual(results, expected); + }); + + test('A failed iterator does not block the others, with onError.', async () => { + // For deterministic results we must control when each iterator starts. + const deferred12 = createDeferred<void>(); + const deferred23 = createDeferred<void>(); + const expected = ['a', 'b', 'c']; + const it1 = (async function* () { + yield 'a'; + deferred12.resolve(); + })(); + const failure = new Error('uh-oh!'); + const it2 = (async function* () { + await deferred12.promise; + yield 'b'; + throw failure; + })(); + const it3 = (async function* () { + await deferred23.promise; + yield 'c'; + })(); + const fails = it2; + let gotErr: { err: Error; index: number } | undefined; + async function onError(err: Error, index: number) { + gotErr = { err, index }; + deferred23.resolve(); + } + + const results = await flatten(chain([it1, fails, it3], onError)); + + assert.deepEqual(results, expected); + assert.deepEqual(gotErr, { err: failure, index: 1 }); + }); + + test('A failed iterator does not block the others, without onError.', async () => { + // If this test fails then it will likely fail intermittently. + // For (mostly) deterministic results we must control when each iterator starts. + const deferred12 = createDeferred<void>(); + const deferred23 = createDeferred<void>(); + const expected = ['a', 'b', 'c']; + const it1 = (async function* () { + yield 'a'; + deferred12.resolve(); + })(); + const failure = new Error('uh-oh!'); + const it2 = (async function* () { + await deferred12.promise; + yield 'b'; + deferred23.resolve(); + // This is ignored by chain() since we did not provide onError(). + throw failure; + })(); + const it3 = (async function* () { + await deferred23.promise; + yield 'c'; + })(); + const fails = it2; + + const results = await flatten(chain([it1, fails, it3])); + + assert.deepEqual(results, expected); + }); + + test('int results', async () => { + const expected = [42, 7, 11, 13]; + const it = (async function* () { + yield 42; + yield* [7, 11, 13]; + })(); + + const results = await flatten(chain([it])); + + assert.deepEqual(results, expected); + }); + + test('object results', async () => { + type Result = { + value: string; + }; + const expected: Result[] = [ + // We don't need anything special here. + { value: 'foo' }, + { value: 'bar' }, + { value: 'baz' } + ]; + const it = (async function* () { + yield* expected; + })(); + + const results = await flatten(chain([it])); + + assert.deepEqual(results, expected); + }); +}); diff --git a/src/test/common/utils/cacheUtils.unit.test.ts b/src/test/common/utils/cacheUtils.unit.test.ts new file mode 100644 index 000000000000..b6859a82342d --- /dev/null +++ b/src/test/common/utils/cacheUtils.unit.test.ts @@ -0,0 +1,307 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import { clearCache, InMemoryCache, InMemoryInterpreterSpecificCache } from '../../../client/common/utils/cacheUtils'; + +type CacheUtilsTestScenario = { + scenarioDesc: string; + // tslint:disable-next-line:no-any + dataToStore: any; +}; + +const scenariosToTest: CacheUtilsTestScenario[] = [ + { + scenarioDesc: 'simple string', + dataToStore: 'hello' + }, + { + scenarioDesc: 'undefined', + dataToStore: undefined + }, + { + scenarioDesc: 'object', + dataToStore: { date: new Date(), hello: 1234 } + } +]; + +class TestInMemoryInterpreterSpecificCache extends InMemoryInterpreterSpecificCache< + string | undefined | { date: number; hello: number } +> { + public elapsed: number = 0; + + public set simulatedElapsedMs(value: number) { + this.elapsed = value; + } + + protected calculateExpiry(): number { + return this.expiryDurationMs; + } + + protected hasExpired(expiry: number): boolean { + return expiry < this.elapsed; + } +} + +// tslint:disable:no-any max-func-body-length +suite('Common Utils - CacheUtils', () => { + suite('InMemory Cache', () => { + let clock: sinon.SinonFakeTimers; + setup(() => { + clock = sinon.useFakeTimers(); + }); + teardown(() => clock.restore()); + test('Cached item should exist', () => { + const cache = new InMemoryCache(5_000); + cache.data = 'Hello World'; + + assert.equal(cache.data, 'Hello World'); + assert.isOk(cache.hasData); + }); + test('Cached item can be updated and should exist', () => { + const cache = new InMemoryCache(5_000); + cache.data = 'Hello World'; + + assert.equal(cache.data, 'Hello World'); + assert.isOk(cache.hasData); + + cache.data = 'Bye'; + + assert.equal(cache.data, 'Bye'); + assert.isOk(cache.hasData); + }); + test('Cached item should not exist after time expires', () => { + const cache = new InMemoryCache(5_000); + cache.data = 'Hello World'; + + assert.equal(cache.data, 'Hello World'); + assert.isTrue(cache.hasData); + + // Should not expire after 4.999s. + clock.tick(4_999); + + assert.equal(cache.data, 'Hello World'); + assert.isTrue(cache.hasData); + + // Should expire after 5s (previous 4999ms + 1ms). + clock.tick(1); + + assert.equal(cache.data, undefined); + assert.isFalse(cache.hasData); + }); + }); + suite('Interpreter Specific Cache', () => { + teardown(() => { + clearCache(); + }); + function createMockVSC(pythonPath: string): typeof import('vscode') { + return { + workspace: { + getConfiguration: () => { + return { + get: () => { + return pythonPath; + }, + inspect: () => { + return { globalValue: pythonPath }; + } + }; + }, + getWorkspaceFolder: () => { + return; + } + }, + Uri: Uri + } as any; + } + scenariosToTest.forEach((scenario: CacheUtilsTestScenario) => { + test(`Data is stored in cache (without workspaces): ${scenario.scenarioDesc}`, () => { + const pythonPath = 'Some Python Path'; + const vsc = createMockVSC(pythonPath); + const resource = Uri.parse('a'); + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); + + expect(cache.hasData).to.be.equal(false, 'Must not have any data'); + + cache.data = scenario.dataToStore; + + expect(cache.hasData).to.be.equal(true, 'Must have data'); + expect(cache.data).to.be.deep.equal(scenario.dataToStore); + }); + test(`Data is stored in cache must be cleared when clearing globally: ${scenario.scenarioDesc}`, () => { + const pythonPath = 'Some Python Path'; + const vsc = createMockVSC(pythonPath); + const resource = Uri.parse('a'); + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); + + expect(cache.hasData).to.be.equal(false, 'Must not have any data'); + + cache.data = scenario.dataToStore; + + expect(cache.hasData).to.be.equal(true, 'Must have data'); + expect(cache.data).to.be.deep.equal(scenario.dataToStore); + + clearCache(); + expect(cache.hasData).to.be.equal(false, 'Must not have data'); + expect(cache.data).to.be.deep.equal(undefined, 'Must not have data'); + }); + test(`Data is stored in cache must be cleared: ${scenario.scenarioDesc}`, () => { + const pythonPath = 'Some Python Path'; + const vsc = createMockVSC(pythonPath); + const resource = Uri.parse('a'); + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); + + expect(cache.hasData).to.be.equal(false, 'Must not have any data'); + + cache.data = scenario.dataToStore; + + expect(cache.hasData).to.be.equal(true, 'Must have data'); + expect(cache.data).to.be.deep.equal(scenario.dataToStore); + + cache.clear(); + expect(cache.hasData).to.be.equal(false, 'Must not have data'); + expect(cache.data).to.be.deep.equal(undefined, 'Must not have data'); + }); + test(`Data is stored in cache and expired data is not returned: ${scenario.scenarioDesc}`, async () => { + const pythonPath = 'Some Python Path'; + const vsc = createMockVSC(pythonPath); + const resource = Uri.parse('a'); + const cache = new TestInMemoryInterpreterSpecificCache('Something', 100, [resource], undefined, vsc); + + expect(cache.hasData).to.be.equal(false, 'Must not have any data before caching.'); + cache.data = scenario.dataToStore; + expect(cache.hasData).to.be.equal(true, 'Must have data after setting the first time.'); + expect(cache.data).to.be.deep.equal(scenario.dataToStore); + + cache.simulatedElapsedMs = 10; + expect(cache.hasData).to.be.equal(true, 'Must have data after waiting for 10ms'); + expect(cache.data).to.be.deep.equal( + scenario.dataToStore, + 'Data should be intact and unchanged in cache after 10ms' + ); + + cache.simulatedElapsedMs = 50; + expect(cache.hasData).to.be.equal(true, 'Must have data after waiting 50ms'); + expect(cache.data).to.be.deep.equal( + scenario.dataToStore, + 'Data should be intact and unchanged in cache after 50ms' + ); + + cache.simulatedElapsedMs = 110; + expect(cache.hasData).to.be.equal(false, 'Must not have data after waiting 110ms'); + expect(cache.data).to.be.deep.equal( + undefined, + 'Must not have data stored after 100ms timeout expires.' + ); + }); + test(`Data is stored in cache (with workspaces): ${scenario.scenarioDesc}`, () => { + const pythonPath = 'Some Python Path'; + const vsc = createMockVSC(pythonPath); + const resource = Uri.parse('a'); + (vsc.workspace as any).workspaceFolders = [{ index: 0, name: '1', uri: Uri.parse('wkfolder') }]; + vsc.workspace.getWorkspaceFolder = () => vsc.workspace.workspaceFolders![0]; + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); + + expect(cache.hasData).to.be.equal(false, 'Must not have any data'); + + cache.data = scenario.dataToStore; + + expect(cache.hasData).to.be.equal(true, 'Must have data'); + expect(cache.data).to.be.deep.equal(scenario.dataToStore); + }); + test(`Data is stored in cache and different resources point to same storage location (without workspaces): ${scenario.scenarioDesc}`, () => { + const pythonPath = 'Some Python Path'; + const vsc = createMockVSC(pythonPath); + const resource = Uri.parse('a'); + const anotherResource = Uri.parse('b'); + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); + const cache2 = new InMemoryInterpreterSpecificCache( + 'Something', + 10000, + [anotherResource], + undefined, + vsc + ); + + expect(cache.hasData).to.be.equal(false, 'Must not have any data'); + expect(cache2.hasData).to.be.equal(false, 'Must not have any data'); + + cache.data = scenario.dataToStore; + + expect(cache.hasData).to.be.equal(true, 'Must have data'); + expect(cache2.hasData).to.be.equal(true, 'Must have data'); + expect(cache.data).to.be.deep.equal(scenario.dataToStore); + expect(cache2.data).to.be.deep.equal(scenario.dataToStore); + }); + test(`Data is stored in cache and different resources point to same storage location (with workspaces): ${scenario.scenarioDesc}`, () => { + const pythonPath = 'Some Python Path'; + const vsc = createMockVSC(pythonPath); + const resource = Uri.parse('a'); + const anotherResource = Uri.parse('b'); + (vsc.workspace as any).workspaceFolders = [{ index: 0, name: '1', uri: Uri.parse('wkfolder') }]; + vsc.workspace.getWorkspaceFolder = () => vsc.workspace.workspaceFolders![0]; + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); + const cache2 = new InMemoryInterpreterSpecificCache( + 'Something', + 10000, + [anotherResource], + undefined, + vsc + ); + + expect(cache.hasData).to.be.equal(false, 'Must not have any data'); + expect(cache2.hasData).to.be.equal(false, 'Must not have any data'); + + cache.data = scenario.dataToStore; + + expect(cache.hasData).to.be.equal(true, 'Must have data'); + expect(cache2.hasData).to.be.equal(true, 'Must have data'); + expect(cache.data).to.be.deep.equal(scenario.dataToStore); + expect(cache2.data).to.be.deep.equal(scenario.dataToStore); + }); + test(`Data is stored in cache and different resources do not point to same storage location (with multiple workspaces): ${scenario.scenarioDesc}`, () => { + const pythonPath = 'Some Python Path'; + const vsc = createMockVSC(pythonPath); + const resource = Uri.parse('a'); + const anotherResource = Uri.parse('b'); + (vsc.workspace as any).workspaceFolders = [ + { index: 0, name: '1', uri: Uri.parse('wkfolder1') }, + { index: 1, name: '2', uri: Uri.parse('wkfolder2') } + ]; + vsc.workspace.getWorkspaceFolder = (res) => { + const index = res.fsPath === resource.fsPath ? 0 : 1; + return vsc.workspace.workspaceFolders![index]; + }; + const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], undefined, vsc); + const cache2 = new InMemoryInterpreterSpecificCache( + 'Something', + 10000, + [anotherResource], + undefined, + vsc + ); + + expect(cache.hasData).to.be.equal(false, 'Must not have any data'); + expect(cache2.hasData).to.be.equal(false, 'Must not have any data'); + + cache.data = scenario.dataToStore; + + expect(cache.hasData).to.be.equal(true, 'Must have data'); + expect(cache2.hasData).to.be.equal(false, 'Must not have any data'); + expect(cache.data).to.be.deep.equal(scenario.dataToStore); + expect(cache2.data).to.be.deep.equal(undefined, 'Must not have any data'); + + cache2.data = 'Store some other data'; + + expect(cache.hasData).to.be.equal(true, 'Must have data'); + expect(cache2.hasData).to.be.equal(true, 'Must have'); + expect(cache.data).to.be.deep.equal(scenario.dataToStore); + expect(cache2.data).to.be.deep.equal('Store some other data', 'Must have data'); + }); + }); + }); +}); diff --git a/src/test/common/utils/decorators.unit.test.ts b/src/test/common/utils/decorators.unit.test.ts new file mode 100644 index 000000000000..8ff3c81c0ca6 --- /dev/null +++ b/src/test/common/utils/decorators.unit.test.ts @@ -0,0 +1,336 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiPromise from 'chai-as-promised'; +import { clearCache } from '../../../client/common/utils/cacheUtils'; +import { cache, makeDebounceAsyncDecorator, makeDebounceDecorator } from '../../../client/common/utils/decorators'; +import { sleep } from '../../core'; +use(chaiPromise); + +// tslint:disable:no-any max-func-body-length no-unnecessary-class +suite('Common Utils - Decorators', function () { + // For some reason, sometimes we have timeouts on CI. + // Note: setTimeout and similar functions are not guaranteed to execute + // at the precise time prescribed. + // tslint:disable-next-line: no-invalid-this + this.retries(3); + suite('Cache Decorator', () => { + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + + setup(() => { + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + }); + + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + clearCache(); + }); + class TestClass { + public invoked = false; + @cache(1000) + public async doSomething(a: number, b: number): Promise<number> { + this.invoked = true; + return a + b; + } + } + + test('Result should be cached for 1s', async () => { + const cls = new TestClass(); + expect(cls.invoked).to.equal(false, 'Wrong initialization value'); + await expect(cls.doSomething(1, 2)).to.eventually.equal(3); + expect(cls.invoked).to.equal(true, 'Should have been invoked'); + + // Reset and ensure it is not updated. + cls.invoked = false; + await expect(cls.doSomething(1, 2)).to.eventually.equal(3); + expect(cls.invoked).to.equal(false, 'Should not have been invoked'); + await expect(cls.doSomething(1, 2)).to.eventually.equal(3); + expect(cls.invoked).to.equal(false, 'Should not have been invoked'); + + // Cache should expire. + await sleep(2000); + + await expect(cls.doSomething(1, 2)).to.eventually.equal(3); + expect(cls.invoked).to.equal(true, 'Should have been invoked'); + // Reset and ensure it is not updated. + cls.invoked = false; + await expect(cls.doSomething(1, 2)).to.eventually.equal(3); + expect(cls.invoked).to.equal(false, 'Should not have been invoked'); + }).timeout(3000); + }); + + suite('Debounce', () => { + /* + * Time in milliseconds (from some arbitrary point in time for current process). + * Don't use new Date().getTime() to calculate differences in times. + * Similarly setTimeout doesn't always trigger at prescribed time (accuracy isn't guaranteed). + * This has an accuracy of around 2-20ms. + * However we're dealing with tests that need accuracy of 1ms. + * Use API that'll give us better accuracy when dealing with elapsed times. + * + * @returns {number} + */ + function getHighPrecisionTime(): number { + const currentTime = process.hrtime(); + // Convert seconds to ms and nanoseconds to ms. + return currentTime[0] * 1000 + currentTime[1] / 1000_000; + } + + /** + * setTimeout doesn't always trigger at prescribed time (accuracy isn't guaranteed). + * Allow a discrepancy of +-5%. + * Here's a simple test to prove this (this has been reported by others too): + * ```js + * // Execute the following around 100 times, you'll see at least one where elapsed time is < 100. + * const startTime = .... + * await new Promise(resolve = setTimeout(resolve, 100)) + * console.log(currentTime - startTijme) + * ``` + * + * @param {number} actualDelay + * @param {number} expectedDelay + */ + function assertElapsedTimeWithinRange(actualDelay: number, expectedDelay: number) { + const difference = actualDelay - expectedDelay; + if (difference >= 0) { + return; + } + expect(Math.abs(difference)).to.be.lessThan( + expectedDelay * 0.05, + `Actual delay ${actualDelay}, expected delay ${expectedDelay}, not within 5% of accuracy` + ); + } + // tslint:disable-next-line: max-classes-per-file + class Base { + public created: number; + public calls: string[]; + public timestamps: number[]; + constructor() { + this.created = getHighPrecisionTime(); + this.calls = []; + this.timestamps = []; + } + protected _addCall(funcname: string, timestamp?: number): void { + if (!timestamp) { + timestamp = getHighPrecisionTime(); + } + this.calls.push(funcname); + this.timestamps.push(timestamp); + } + } + async function waitForCalls(timestamps: number[], count: number, delay = 10, timeout = 1000) { + const steps = timeout / delay; + for (let i = 0; i < steps; i += 1) { + if (timestamps.length >= count) { + return; + } + await sleep(delay); + } + if (timestamps.length < count) { + throw Error(`timed out after ${timeout}ms`); + } + } + test('Debounce: one sync call', async () => { + const wait = 100; + // tslint:disable-next-line:max-classes-per-file + class One extends Base { + @makeDebounceDecorator(wait) + public run(): void { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + one.run(); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + }); + test('Debounce: one async call & no wait', async () => { + const wait = 100; + // tslint:disable-next-line:max-classes-per-file + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + let errored = false; + one.run().catch(() => (errored = true)); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + expect(errored).to.be.equal(false, "Exception raised when there shouldn't have been any"); + }); + test('Debounce: one async call', async () => { + const wait = 100; + // tslint:disable-next-line:max-classes-per-file + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + await one.run(); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + }); + test('Debounce: one async call and ensure exceptions are re-thrown', async () => { + const wait = 100; + // tslint:disable-next-line:max-classes-per-file + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + throw new Error('Kaboom'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + let capturedEx: Error | undefined; + await one.run().catch((ex) => (capturedEx = ex)); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + expect(capturedEx).to.not.be.equal(undefined, 'Exception not re-thrown'); + }); + test('Debounce: multiple async calls', async () => { + const wait = 100; + // tslint:disable-next-line:max-classes-per-file + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + let errored = false; + one.run().catch(() => (errored = true)); + one.run().catch(() => (errored = true)); + one.run().catch(() => (errored = true)); + one.run().catch(() => (errored = true)); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + expect(errored).to.be.equal(false, "Exception raised when there shouldn't have been any"); + }); + test('Debounce: multiple async calls when awaiting on all', async function () { + const wait = 100; + // tslint:disable-next-line:max-classes-per-file + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + await Promise.all([one.run(), one.run(), one.run(), one.run()]); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + }); + test('Debounce: multiple async calls & wait on some', async () => { + const wait = 100; + // tslint:disable-next-line:max-classes-per-file + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + let errored = false; + one.run().catch(() => (errored = true)); + await one.run(); + one.run().catch(() => (errored = true)); + one.run().catch(() => (errored = true)); + await waitForCalls(one.timestamps, 2); + const delay = one.timestamps[1] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run', 'run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + expect(errored).to.be.equal(false, "Exception raised when there shouldn't have been any"); + }); + test('Debounce: multiple calls grouped', async () => { + const wait = 100; + // tslint:disable-next-line:max-classes-per-file + class One extends Base { + @makeDebounceDecorator(wait) + public run(): void { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + one.run(); + one.run(); + one.run(); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + }); + test('Debounce: multiple calls spread', async () => { + const wait = 100; + // tslint:disable-next-line:max-classes-per-file + class One extends Base { + @makeDebounceDecorator(wait) + public run(): void { + this._addCall('run'); + } + } + const one = new One(); + + one.run(); + await sleep(wait); + one.run(); + await waitForCalls(one.timestamps, 2); + + expect(one.calls).to.deep.equal(['run', 'run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + }); + }); +}); diff --git a/src/test/common/utils/localize.functional.test.ts b/src/test/common/utils/localize.functional.test.ts new file mode 100644 index 000000000000..8ccd3cf58926 --- /dev/null +++ b/src/test/common/utils/localize.functional.test.ts @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import * as localize from '../../../client/common/utils/localize'; + +const defaultNLSFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); + +// Defines a Mocha test suite to group tests of similar kind together +suite('Localization', () => { + // Note: We use package.nls.json by default for tests. Use the + // setLocale() helper to switch to a different locale. + + let localeFiles: string[]; + let nls_orig: string | undefined; + + setup(() => { + localeFiles = []; + + nls_orig = process.env.VSCODE_NLS_CONFIG; + setLocale('en-us'); + + // Ensure each test starts fresh. + localize._resetCollections(); + }); + + teardown(() => { + if (nls_orig) { + process.env.VSCODE_NLS_CONFIG = nls_orig; + } else { + delete process.env.VSCODE_NLS_CONFIG; + } + + const filenames = localeFiles; + localeFiles = []; + for (const filename of filenames) { + fs.unlinkSync(filename); + } + }); + + function addLocale(locale: string, nls: Record<string, string>) { + const filename = addLocaleFile(locale, nls); + localeFiles.push(filename); + } + + test('keys', (done) => { + const val = localize.ExtensionSurveyBanner.bannerMessage(); + assert.equal( + val, + 'Can you please take 2 minutes to tell us how the Python extension is working for you?', + 'LanguageService string doesnt match' + ); + done(); + }); + + test('keys italian', (done) => { + // Force a config change + setLocale('it'); + + const val = localize.ExtensionSurveyBanner.bannerLabelYes(); + assert.equal(val, 'Sì, prenderò il sondaggio ora', 'bannerLabelYes is not being translated'); + done(); + }); + + test('key found for locale', (done) => { + addLocale('spam', { + 'debug.selectConfigurationTitle': '???', + 'Common.gotIt': '!!!' + }); + setLocale('spam'); + + const title = localize.DebugConfigStrings.selectConfiguration.title(); + const gotIt = localize.Common.gotIt(); + + assert.equal(title, '???', 'not used'); + assert.equal(gotIt, '!!!', 'not used'); + done(); + }); + + test('key not found for locale (default used)', (done) => { + addLocale('spam', { + 'debug.selectConfigurationTitle': '???' + }); + setLocale('spam'); + + const gotIt = localize.Common.gotIt(); + + assert.equal(gotIt, 'Got it!', `default not used (got ${gotIt})`); + done(); + }); + + test('keys exist', (done) => { + // Read in the JSON object for the package.nls.json + const nlsCollection = getDefaultCollection(); + + // Now match all of our namespace entries to our nls entries + useEveryLocalization(localize); + + // Now verify all of the asked for keys exist + const askedFor = localize._getAskedForCollection(); + const missing: Record<string, string> = {}; + Object.keys(askedFor).forEach((key: string) => { + // Now check that this key exists somewhere in the nls collection + if (!nlsCollection[key]) { + missing[key] = askedFor[key]; + } + }); + + // If any missing keys, output an error + const missingKeys = Object.keys(missing); + if (missingKeys && missingKeys.length > 0) { + let message = 'Missing keys. Add the following to package.nls.json:\n'; + missingKeys.forEach((k: string) => { + message = message.concat(`\t"${k}" : "${missing[k]}",\n`); + }); + assert.fail(message); + } + + done(); + }); + + test('all keys used', function (done) { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Unused keys need to be cleaned up. + // tslint:disable-next-line:no-invalid-this + this.skip(); + //test('all keys used', done => { + const nlsCollection = getDefaultCollection(); + useEveryLocalization(localize); + + // Now verify all of the asked for keys exist + const askedFor = localize._getAskedForCollection(); + const extra: Record<string, string> = {}; + Object.keys(nlsCollection).forEach((key: string) => { + // Now check that this key exists somewhere in the nls collection + if (askedFor[key]) { + return; + } + if (key.toLowerCase().indexOf('datascience') >= 0) { + return; + } + extra[key] = nlsCollection[key]; + }); + + // If any missing keys, output an error + const extraKeys = Object.keys(extra); + if (extraKeys && extraKeys.length > 0) { + let message = 'Unused keys. Remove the following from package.nls.json:\n'; + extraKeys.forEach((k: string) => { + message = message.concat(`\t"${k}" : "${extra[k]}",\n`); + }); + assert.fail(message); + } + + done(); + }); +}); + +function addLocaleFile(locale: string, nls: Record<string, string>) { + const filename = path.join(EXTENSION_ROOT_DIR, `package.nls.${locale}.json`); + if (fs.existsSync(filename)) { + throw Error(`NLS file ${filename} already exists`); + } + const contents = JSON.stringify(nls); + fs.writeFileSync(filename, contents); + return filename; +} + +function setLocale(locale: string) { + let nls: Record<string, string>; + if (process.env.VSCODE_NLS_CONFIG) { + nls = JSON.parse(process.env.VSCODE_NLS_CONFIG); + nls.locale = locale; + } else { + nls = { locale: locale }; + } + process.env.VSCODE_NLS_CONFIG = JSON.stringify(nls); +} + +function getDefaultCollection() { + if (!fs.existsSync(defaultNLSFile)) { + throw Error('package.nls.json is missing'); + } + const contents = fs.readFileSync(defaultNLSFile, 'utf8'); + return JSON.parse(contents); +} + +// tslint:disable-next-line:no-any +function useEveryLocalization(topns: any) { + // Read all of the namespaces from the localize import. + const entries = Object.keys(topns); + + // Now match all of our namespace entries to our nls entries. + entries.forEach((e: string) => { + // @ts-ignore + if (typeof topns[e] === 'function') { + return; + } + // It must be a namespace. + useEveryLocalizationInNS(topns[e]); + }); +} + +// tslint:disable-next-line:no-any +function useEveryLocalizationInNS(ns: any) { + // The namespace should have functions inside of it. + // @ts-ignore + const props = Object.keys(ns); + + // Run every function and cover every sub-namespace. + // This should fill up our "asked-for keys" collection. + props.forEach((key: string) => { + if (typeof ns[key] === 'function') { + const func = ns[key]; + func(); + } else { + useEveryLocalizationInNS(ns[key]); + } + }); +} diff --git a/src/test/common/utils/regexp.unit.test.ts b/src/test/common/utils/regexp.unit.test.ts new file mode 100644 index 000000000000..26613230d7ac --- /dev/null +++ b/src/test/common/utils/regexp.unit.test.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-multiline-string + +import { expect } from 'chai'; + +import { verboseRegExp } from '../../../client/common/utils/regexp'; + +suite('Utils for regular expressions - verboseRegExp()', () => { + test('whitespace removed in multiline pattern (example of typical usage)', () => { + const regex = verboseRegExp(` + ^ + (?: + spam \\b .* + ) | + (?: + eggs \\b .* + ) + $ + `); + + expect(regex.source).to.equal('^(?:spam\\b.*)|(?:eggs\\b.*)$', 'mismatch'); + }); + + const whitespaceTests = [ + ['spam eggs', 'spameggs'], + [ + `spam + eggs`, + 'spameggs' + ], + // empty + [' ', '(?:)'], + [ + ` + `, + '(?:)' + ] + ]; + for (const [pat, expected] of whitespaceTests) { + test(`whitespace removed ("${pat}")`, () => { + const regex = verboseRegExp(pat); + + expect(regex.source).to.equal(expected, 'mismatch'); + }); + } + + const noopPatterns = ['^(?:spam\\b.*)$', 'spam', '^spam$', 'spam$', '^spam']; + for (const pat of noopPatterns) { + test(`pattern not changed ("${pat}")`, () => { + const regex = verboseRegExp(pat); + + expect(regex.source).to.equal(pat, 'mismatch'); + }); + } + + const emptyPatterns = [ + '', + ` + `, + ' ' + ]; + for (const pat of emptyPatterns) { + test(`no pattern ("${pat}")`, () => { + const regex = verboseRegExp(pat); + + expect(regex.source).to.equal('(?:)', 'mismatch'); + }); + } +}); diff --git a/src/test/common/utils/text.unit.test.ts b/src/test/common/utils/text.unit.test.ts new file mode 100644 index 000000000000..a26a1fbeadb0 --- /dev/null +++ b/src/test/common/utils/text.unit.test.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any no-require-imports no-var-requires + +import { expect } from 'chai'; +import { Position, Range } from 'vscode'; +import { parsePosition, parseRange } from '../../../client/common/utils/text'; + +suite('parseRange()', () => { + test('valid strings', async () => { + const tests: [string, Range][] = [ + ['1:5-3:5', new Range(new Position(1, 5), new Position(3, 5))], + ['1:5-3:3', new Range(new Position(1, 5), new Position(3, 3))], + ['1:3-3:5', new Range(new Position(1, 3), new Position(3, 5))], + ['1-3:5', new Range(new Position(1, 0), new Position(3, 5))], + ['1-3', new Range(new Position(1, 0), new Position(3, 0))], + ['1-1', new Range(new Position(1, 0), new Position(1, 0))], + ['1', new Range(new Position(1, 0), new Position(1, 0))], + [ + '1:3-', + new Range( + new Position(1, 3), + new Position(0, 0) // ??? + ) + ], + ['1:3', new Range(new Position(1, 3), new Position(1, 3))], + ['', new Range(new Position(0, 0), new Position(0, 0))], + ['3-1', new Range(new Position(3, 0), new Position(1, 0))] + ]; + for (const [raw, expected] of tests) { + const result = parseRange(raw); + + expect(result).to.deep.equal(expected); + } + }); + test('valid numbers', async () => { + const tests: [number, Range][] = [[1, new Range(new Position(1, 0), new Position(1, 0))]]; + for (const [raw, expected] of tests) { + const result = parseRange(raw); + + expect(result).to.deep.equal(expected); + } + }); + test('bad strings', async () => { + const tests: string[] = [ + '1-2-3', + '1:4-2-3', + '1-2:4-3', + '1-2-3:4', + + '1:2:3', + '1:2:3-4', + '1-2:3:4', + '1:2:3-4:5:6', + + '1-a', + '1:2-a', + '1-a:2', + '1:2-a:2', + 'a-1', + 'a-b', + 'a', + 'a:1', + 'a:b' + ]; + for (const raw of tests) { + expect(() => parseRange(raw)).to.throw(); + } + }); +}); + +suite('parsePosition()', () => { + test('valid strings', async () => { + const tests: [string, Position][] = [ + ['1:5', new Position(1, 5)], + ['1', new Position(1, 0)], + ['', new Position(0, 0)] + ]; + for (const [raw, expected] of tests) { + const result = parsePosition(raw); + + expect(result).to.deep.equal(expected); + } + }); + test('valid numbers', async () => { + const tests: [number, Position][] = [[1, new Position(1, 0)]]; + for (const [raw, expected] of tests) { + const result = parsePosition(raw); + + expect(result).to.deep.equal(expected); + } + }); + test('bad strings', async () => { + const tests: string[] = ['1:2:3', '1:a', 'a']; + for (const raw of tests) { + expect(() => parsePosition(raw)).to.throw(); + } + }); +}); diff --git a/src/test/common/utils/version.unit.test.ts b/src/test/common/utils/version.unit.test.ts new file mode 100644 index 000000000000..2a2878dcd6eb --- /dev/null +++ b/src/test/common/utils/version.unit.test.ts @@ -0,0 +1,350 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; + +import { + getVersionString, + isVersionInfoEmpty, + normalizeVersionInfo, + ParseResult, + parseVersionInfo, + validateVersionInfo, + VersionInfo +} from '../../../client/common/utils/version'; + +const NOT_USED = {}; + +type Unnormalized = { + major: string; + minor: string; + micro: string; +}; + +function ver( + // tslint:disable:no-any + major: any, + minor: any = NOT_USED, + micro: any = NOT_USED, + // tslint:enable:no-any + unnormalized?: Unnormalized +): VersionInfo { + if (minor === NOT_USED) { + minor = -1; + } + if (micro === NOT_USED) { + micro = -1; + } + const info = { + major: (major as unknown) as number, + minor: (minor as unknown) as number, + micro: (micro as unknown) as number, + raw: undefined + }; + if (unnormalized !== undefined) { + // tslint:disable-next-line:no-any + ((info as unknown) as any).unnormalized = unnormalized; + } + return info; +} + +function unnorm(major: string, minor: string, micro: string): Unnormalized { + return { major, minor, micro }; +} + +function res( + // These go into the VersionInfo: + major: number, + minor: number, + micro: number, + // These are the remainder of the ParseResult: + before: string, + after: string +): ParseResult<VersionInfo> { + return { + before, + after, + version: ver(major, minor, micro) + }; +} + +const VERSIONS: [VersionInfo, string][] = [ + [ver(2, 7, 0), '2.7.0'], + [ver(2, 7, -1), '2.7'], + [ver(2, -1, -1), '2'], + [ver(-1, -1, -1), ''], + [ver(2, 7, 11), '2.7.11'], + [ver(3, 11, 1), '3.11.1'], + [ver(0, 0, 0), '0.0.0'] +]; +const INVALID: VersionInfo[] = [ + ver(undefined, undefined, undefined), + ver(null, null, null), + ver({}, {}, {}), + ver('x', 'y', 'z') +]; + +suite('common utils - getVersionString', () => { + VERSIONS.forEach((data) => { + const [info, expected] = data; + test(`${expected}`, () => { + const result = getVersionString(info); + + assert.equal(result, expected); + }); + }); +}); + +suite('common utils - isVersionEmpty', () => { + [ + ver(-1, -1, -1), + // normalization failed: + ver(-1, -1, -1, unnorm('oops', 'uh-oh', "I've got a bad feeling about this")), + // not normalized by still empty + ver(-10, -10, -10) + ].forEach((data: VersionInfo) => { + const info = data; + test(`empty: ${info}`, () => { + const result = isVersionInfoEmpty(info); + + assert.ok(result); + }); + }); + + [ + // clearly not empty: + ver(3, 4, 5), + ver(3, 4, -1), + ver(3, -1, -1), + // 0 is not empty: + ver(0, 0, 0), + ver(0, 0, -1), + ver(0, -1, -1) + ].forEach((data: VersionInfo) => { + const info = data; + test(`not empty: ${info.major}.${info.minor}.${info.micro}`, () => { + const result = isVersionInfoEmpty(info); + + assert.equal(result, false); + }); + }); + + INVALID.forEach((data: VersionInfo) => { + const info = data; + test(`bogus: ${info.major}`, () => { + const result = isVersionInfoEmpty(info); + + assert.equal(result, false); + }); + }); +}); + +suite('common utils - normalizeVersionInfo', () => { + suite('valid', () => { + test(`noop`, () => { + const info = ver(1, 2, 3); + info.raw = '1.2.3'; + // tslint:disable-next-line:no-any + ((info as unknown) as any).unnormalized = unnorm('', '', ''); + const expected = info; + + const normalized = normalizeVersionInfo(info); + + assert.deepEqual(normalized, expected); + }); + + test(`same`, () => { + const info = ver(1, 2, 3); + info.raw = '1.2.3'; + // tslint:disable-next-line:no-any + const expected: any = { ...info }; + expected.unnormalized = unnorm('', '', ''); + + const normalized = normalizeVersionInfo(info); + + assert.deepEqual(normalized, expected); + }); + + [ + [ver(3, 4, 5), ver(3, 4, 5)], + [ver(3, 4, 1), ver(3, 4, 1)], + [ver(3, 4, 0), ver(3, 4, 0)], + [ver(3, 4, -1), ver(3, 4, -1)], + [ver(3, 4, -5), ver(3, 4, -1)], + // empty + [ver(-1, -1, -1), ver(-1, -1, -1)], + [ver(-3, -4, -5), ver(-1, -1, -1)], + // numeric permutations + [ver(1, 5, 10), ver(1, 5, 10)], + [ver(1, 5, -10), ver(1, 5, -1)], + [ver(1, -5, -10), ver(1, -1, -1)], + [ver(-1, -5, -10), ver(-1, -1, -1)], + [ver(1, -5, 10), ver(1, -1, 10)], + [ver(-1, -5, 10), ver(-1, -1, 10)], + // coerced + [ver(3, 4, '5'), ver(3, 4, 5)], + [ver(3, 4, '1'), ver(3, 4, 1)], + [ver(3, 4, '0'), ver(3, 4, 0)], + [ver(3, 4, '-1'), ver(3, 4, -1)], + [ver(3, 4, '-5'), ver(3, 4, -1)] + ].forEach((data) => { + const [info, expected] = data; + // tslint:disable-next-line:no-any + ((expected as unknown) as any).unnormalized = unnorm('', '', ''); + expected.raw = ''; + test(`[${info.major}, ${info.minor}, ${info.micro}]`, () => { + const normalized = normalizeVersionInfo(info); + + assert.deepEqual(normalized, expected); + }); + }); + }); + + suite('partially "invalid"', () => { + ([ + [ver(undefined, 4, 5), unnorm('missing', '', '')], + [ver(3, null, 5), unnorm('', 'missing', '')], + [ver(3, 4, NaN), unnorm('', '', 'missing')], + [ver(3, 4, ''), unnorm('', '', 'string not numeric')], + [ver(3, 4, ' '), unnorm('', '', 'string not numeric')], + [ver(3, 4, 'foo'), unnorm('', '', 'string not numeric')], + [ver(3, 4, {}), unnorm('', '', 'unsupported type')], + [ver(3, 4, []), unnorm('', '', 'unsupported type')] + ] as [VersionInfo, Unnormalized][]).forEach((data) => { + const [info, unnormalized] = data; + const expected = { ...info }; + if (info.major !== 3) { + expected.major = -1; + } else if (info.minor !== 4) { + expected.minor = -1; + } else { + expected.micro = -1; + } + // tslint:disable-next-line:no-any + ((expected as unknown) as any).unnormalized = unnormalized; + expected.raw = ''; + test(`[${info.major}, ${info.minor}, ${info.micro}]`, () => { + const normalized = normalizeVersionInfo(info); + + assert.deepEqual(normalized, expected); + }); + }); + }); +}); + +suite('common utils - validateVersionInfo', () => { + suite('valid', () => { + [ + ver(3, 4, 5), + ver(3, 4, -1), + ver(3, -1, -1), + // unnormalized but still valid: + ver(3, -7, -11) + ].forEach((info) => { + test(`as-is: [${info.major}, ${info.minor}, ${info.micro}]`, () => { + validateVersionInfo(info); + }); + }); + + test('normalization worked', () => { + const raw = unnorm('', '', ''); + const info = ver(3, 8, -1, raw); + + validateVersionInfo(info); + }); + }); + + suite('invalid', () => { + [ + // missing major: + ver(-1, -1, -1), + ver(-1, -1, 5), + ver(-1, 4, -1), + ver(-1, 4, 5), + // missing minor: + ver(3, -1, 5) + ].forEach((info) => { + test(`missing parts: [${info.major}.${info.minor}.${info.micro}]`, () => { + assert.throws(() => validateVersionInfo(info)); + }); + }); + + [ + // These are all error messages that will be used in the unnormalized property. + 'string not numeric', + 'missing', + 'unsupported type', + 'oops!' + ].forEach((errMsg) => { + const raw = unnorm('', '', errMsg); + const info = ver(3, 4, -1, raw); + test(`normalization failed: ${errMsg}`, () => { + assert.throws(() => validateVersionInfo(info)); + }); + }); + + // We expect only numbers, so NaN nor any of the items + // in INVALID need to be tested. + }); +}); + +suite('common utils - parseVersionInfo', () => { + suite('invalid versions', () => { + const BOGUS = [ + // Note that some of these are *almost* valid. + '2.', + '.2', + '.2.7', + 'a', + '2.a', + '2.b7', + '2-b.7', + '2.7rc1', + '' + ]; + for (const verStr of BOGUS) { + test(`invalid - '${verStr}'`, () => { + const result = parseVersionInfo(verStr); + + assert.equal(result, undefined); + }); + } + }); + + suite('valid versions', () => { + ([ + // plain + ...VERSIONS.map(([v, s]) => [s, { version: v, before: '', after: '' }]), + ['02.7', res(2, 7, -1, '', '')], + ['2.07', res(2, 7, -1, '', '')], + ['2.7.01', res(2, 7, 1, '', '')], + // with before/after + [' 2.7.9 ', res(2, 7, 9, ' ', ' ')], + ['2.7.9-3.2.7', res(2, 7, 9, '', '-3.2.7')], + ['python2.7.exe', res(2, 7, -1, 'python', '.exe')], + ['1.2.3.4.5-x2.2', res(1, 2, 3, '', '.4.5-x2.2')], + ['3.8.1a2', res(3, 8, 1, '', 'a2')], + ['3.8.1-alpha2', res(3, 8, 1, '', '-alpha2')], + [ + '3.7.5 (default, Nov 7 2019, 10:50:52) \\n[GCC 8.3.0]', + res(3, 7, 5, '', ' (default, Nov 7 2019, 10:50:52) \\n[GCC 8.3.0]') + ], + ['python2', res(2, -1, -1, 'python', '')], + // without the "before" the following won't match. + ['python2.a', res(2, -1, -1, 'python', '.a')], + ['python2.b7', res(2, -1, -1, 'python', '.b7')] + ] as [string, ParseResult<VersionInfo>][]).forEach((data) => { + const [verStr, result] = data; + if (verStr === '') { + return; + } + const expected = { ...result, version: { ...result.version } }; + expected.version.raw = verStr; + test(`valid - '${verStr}'`, () => { + const parsed = parseVersionInfo(verStr); + + assert.deepEqual(parsed, expected); + }); + }); + }); +}); diff --git a/src/test/common/utils/workerPool.functional.test.ts b/src/test/common/utils/workerPool.functional.test.ts new file mode 100644 index 000000000000..dfd68d559f55 --- /dev/null +++ b/src/test/common/utils/workerPool.functional.test.ts @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { createWorkerPool, QueuePosition } from '../../../client/common/utils/workerPool'; + +suite('Process Queue', () => { + test('Run two workers to calculate square', async () => { + const workerPool = createWorkerPool<number, number>(async (i) => Promise.resolve(i * i)); + const promises: Promise<number>[] = []; + const results: number[] = []; + [2, 3, 4, 5, 6, 7, 8, 9].forEach((i) => promises.push(workerPool.addToQueue(i))); + await Promise.all(promises).then((r) => { + results.push(...r); + }); + assert.deepEqual(results, [4, 9, 16, 25, 36, 49, 64, 81]); + }); + + test('Run, wait for result, run again', async () => { + const workerPool = createWorkerPool<number, number>((i) => Promise.resolve(i * i)); + let promises: Promise<number>[] = []; + let results: number[] = []; + [2, 3, 4].forEach((i) => promises.push(workerPool.addToQueue(i))); + await Promise.all(promises).then((r) => { + results.push(...r); + }); + assert.deepEqual(results, [4, 9, 16]); + + promises = []; + results = []; + [5, 6, 7, 8].forEach((i) => promises.push(workerPool.addToQueue(i))); + await Promise.all(promises).then((r) => { + results.push(...r); + }); + assert.deepEqual(results, [25, 36, 49, 64]); + }); + test('Run two workers and stop in between', async () => { + const workerPool = createWorkerPool<number, number>(async (i) => { + if (i === 4) { + workerPool.stop(); + } + return Promise.resolve(i * i); + }); + const promises: Promise<number>[] = []; + const results: number[] = []; + const reasons: Error[] = []; + [2, 3, 4, 5, 6].forEach((i) => promises.push(workerPool.addToQueue(i))); + for (const v of promises) { + try { + results.push(await v); + } catch (reason) { + reasons.push(reason); + } + } + assert.deepEqual(results, [4, 9]); + assert.deepEqual(reasons, [ + Error('Queue stopped processing'), + Error('Queue stopped processing'), + Error('Queue stopped processing') + ]); + }); + + test('Add to a stopped queue', async () => { + const workerPool = createWorkerPool<number, number>((i) => Promise.resolve(i * i)); + workerPool.stop(); + const reasons: Error[] = []; + try { + await workerPool.addToQueue(2); + } catch (reason) { + reasons.push(reason); + } + assert.deepEqual(reasons, [Error('Queue is stopped')]); + }); + + test('Worker function fails', async () => { + const workerPool = createWorkerPool<number, number>((i) => { + if (i === 4) { + throw Error('Bad input'); + } + return Promise.resolve(i * i); + }); + const promises: Promise<number>[] = []; + const results: number[] = []; + const reasons: string[] = []; + [2, 3, 4, 5, 6].forEach((i) => promises.push(workerPool.addToQueue(i))); + for (const v of promises) { + try { + results.push(await v); + } catch (reason) { + reasons.push(reason); + } + } + assert.deepEqual(reasons, [Error('Bad input')]); + assert.deepEqual(results, [4, 9, 25, 36]); + }); + + test('Add to the front of the queue', async () => { + const processOrder: number[] = []; + const workerPool = createWorkerPool<number, number>((i) => { + processOrder.push(i); + return Promise.resolve(i * i); + }); + + const promises: Promise<number>[] = []; + const results: number[] = []; + [1, 2, 3, 4, 5, 6].forEach((i) => { + if (i === 4) { + promises.push(workerPool.addToQueue(i, QueuePosition.Front)); + } else { + promises.push(workerPool.addToQueue(i)); + } + }); + await Promise.all(promises).then((r) => { + results.push(...r); + }); + + assert.deepEqual(processOrder, [1, 2, 4, 3, 5, 6]); + assert.deepEqual(results, [1, 4, 9, 16, 25, 36]); + }); +}); diff --git a/src/test/common/variables/envVarsProvider.multiroot.test.ts b/src/test/common/variables/envVarsProvider.multiroot.test.ts new file mode 100644 index 000000000000..5035c9316d20 --- /dev/null +++ b/src/test/common/variables/envVarsProvider.multiroot.test.ts @@ -0,0 +1,352 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { ConfigurationTarget, Disposable, Uri, workspace } from 'vscode'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { + IS_WINDOWS, + NON_WINDOWS_PATH_VARIABLE_NAME, + WINDOWS_PATH_VARIABLE_NAME +} from '../../../client/common/platform/constants'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IDisposableRegistry, IPathUtils } from '../../../client/common/types'; +import { clearCache } from '../../../client/common/utils/cacheUtils'; +import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; +import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; +import { EnvironmentVariables } from '../../../client/common/variables/types'; +import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { IInterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection/types'; +import { clearPythonPathInWorkspaceFolder, isOs, OSType, updateSetting } from '../../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../../initialize'; +import { MockAutoSelectionService } from '../../mocks/autoSelector'; +import { MockProcess } from '../../mocks/process'; +import { UnitTestIocContainer } from '../../testing/serviceRegistry'; + +use(chaiAsPromised); + +const multirootPath = path.join(__dirname, '..', '..', '..', '..', 'src', 'testMultiRootWkspc'); +const workspace4Path = Uri.file(path.join(multirootPath, 'workspace4')); +const workspace4PyFile = Uri.file(path.join(workspace4Path.fsPath, 'one.py')); + +// tslint:disable-next-line:max-func-body-length +suite('Multiroot Environment Variables Provider', () => { + let ioc: UnitTestIocContainer; + const pathVariableName = IS_WINDOWS ? WINDOWS_PATH_VARIABLE_NAME : NON_WINDOWS_PATH_VARIABLE_NAME; + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + await clearPythonPathInWorkspaceFolder(workspace4Path); + await updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + await initialize(); + }); + setup(() => { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + ioc.registerInterpreterStorageTypes(); + ioc.registerMockInterpreterTypes(); + const mockEnvironmentActivationService = mock(EnvironmentActivationService); + when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); + when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenResolve(); + when( + mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve(); + ioc.serviceManager.rebindInstance<IEnvironmentActivationService>( + IEnvironmentActivationService, + instance(mockEnvironmentActivationService) + ); + clearCache(); + return initializeTest(); + }); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await ioc.dispose(); + await closeActiveWindows(); + await clearPythonPathInWorkspaceFolder(workspace4Path); + await updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + await initializeTest(); + clearCache(); + }); + + function getVariablesProvider(mockVariables: EnvironmentVariables = { ...process.env }) { + const pathUtils = ioc.serviceContainer.get<IPathUtils>(IPathUtils); + const fs = ioc.serviceContainer.get<IFileSystem>(IFileSystem); + const mockProcess = new MockProcess(mockVariables); + const variablesService = new EnvironmentVariablesService(pathUtils, fs); + const disposables = ioc.serviceContainer.get<Disposable[]>(IDisposableRegistry); + ioc.serviceManager.addSingletonInstance(IInterpreterAutoSelectionService, new MockAutoSelectionService()); + const cfgService = new ConfigurationService(ioc.serviceContainer); + const workspaceService = new WorkspaceService(); + return new EnvironmentVariablesProvider( + variablesService, + disposables, + new PlatformService(), + workspaceService, + cfgService, + mockProcess, + ioc.serviceContainer + ); + } + + test('Custom variables should not be undefined without an env file', async () => { + await updateSetting('envFile', 'someInvalidFile.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const envProvider = getVariablesProvider(); + const vars = envProvider.getEnvironmentVariables(workspace4PyFile); + await expect(vars).to.eventually.not.equal(undefined, 'Variables is not undefiend'); + }); + + test('Custom variables should be parsed from env file', async () => { + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const processVariables = { ...process.env }; + if (processVariables.PYTHONPATH) { + delete processVariables.PYTHONPATH; + } + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + }); + + test('All process environment variables should be included in variables returned', async () => { + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const processVariables = { ...process.env }; + if (processVariables.PYTHONPATH) { + delete processVariables.PYTHONPATH; + } + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + + Object.keys(processVariables).forEach((variable) => { + expect(vars).to.have.property(variable); + // On CI, it was seen that processVariable[variable] can contain spaces at the end, which causes tests to fail. So trim the strings before comparing. + expect(vars[variable]?.trim()).to.equal( + processVariables[variable]?.trim(), + 'Value of the variable is incorrect' + ); + }); + }); + + test('Variables from file should take precedence over variables in process', async () => { + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const processVariables = { ...process.env }; + if (processVariables.PYTHONPATH) { + delete processVariables.PYTHONPATH; + } + processVariables.X1234PYEXTUNITTESTVAR = 'abcd'; + processVariables.ABCD = 'abcd'; + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('ABCD', 'abcd', 'ABCD value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + }); + + test('PYTHONPATH from process variables should be merged with that in env file', async () => { + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const processVariables = { ...process.env }; + processVariables.PYTHONPATH = '/usr/one/TWO'; + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + const expectedPythonPath = `../workspace5${path.delimiter}${processVariables.PYTHONPATH}`; + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + }); + + test('PATH from process variables should be included in in variables returned (mock variables)', async () => { + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const processVariables = { ...process.env }; + processVariables.PYTHONPATH = '/usr/one/TWO'; + processVariables[pathVariableName] = '/usr/one/THREE'; + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + const expectedPythonPath = `../workspace5${path.delimiter}${processVariables.PYTHONPATH}`; + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property(pathVariableName, processVariables[pathVariableName], 'PATH value is invalid'); + }); + + test('PATH from process variables should be included in in variables returned', async function () { + // this test is flaky on windows (likely the value of the path property + // has incorrect path separator chars). Tracked by GH #4756 + if (isOs(OSType.Windows)) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const processVariables = { ...process.env }; + processVariables.PYTHONPATH = '/usr/one/TWO'; + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + const expectedPythonPath = `../workspace5${path.delimiter}${processVariables.PYTHONPATH}`; + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property(pathVariableName, processVariables[pathVariableName], 'PATH value is invalid'); + }); + + test('PYTHONPATH and PATH from process variables should be merged with that in env file', async () => { + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env5', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const processVariables = { ...process.env }; + processVariables.PYTHONPATH = '/usr/one/TWO'; + processVariables[pathVariableName] = '/usr/one/THREE'; + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + const expectedPythonPath = `/usr/one/three:/usr/one/four${path.delimiter}${processVariables.PYTHONPATH}`; + const expectedPath = `/usr/x:/usr/y${path.delimiter}${processVariables[pathVariableName]}`; + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X', '1', 'X value is invalid'); + expect(vars).to.have.property('Y', '2', 'Y value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property(pathVariableName, expectedPath, 'PATH value is invalid'); + }); + + test('PATH and PYTHONPATH from env file should be returned as is', async () => { + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env5', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const processVariables = { ...process.env }; + if (processVariables.PYTHONPATH) { + delete processVariables.PYTHONPATH; + } + if (processVariables[pathVariableName]) { + delete processVariables[pathVariableName]; + } + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + const expectedPythonPath = '/usr/one/three:/usr/one/four'; + const expectedPath = '/usr/x:/usr/y'; + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X', '1', 'X value is invalid'); + expect(vars).to.have.property('Y', '2', 'Y value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property(pathVariableName, expectedPath, 'PATH value is invalid'); + }); + + test('PYTHONPATH and PATH from process variables should be included in variables returned', async () => { + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env2', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const processVariables = { ...process.env }; + processVariables.PYTHONPATH = '/usr/one/TWO'; + processVariables[pathVariableName] = '/usr/one/THREE'; + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X12345PYEXTUNITTESTVAR', '12345', 'X12345PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', processVariables.PYTHONPATH, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property(pathVariableName, processVariables[pathVariableName], 'PATH value is invalid'); + }); + + test('PYTHONPATH should not exist in variables returned', async () => { + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env2', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const processVariables = { ...process.env }; + if (processVariables.PYTHONPATH) { + delete processVariables.PYTHONPATH; + } + processVariables[pathVariableName] = '/usr/one/THREE'; + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X12345PYEXTUNITTESTVAR', '12345', 'X12345PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.not.have.property('PYTHONPATH'); + expect(vars).to.have.property(pathVariableName, processVariables[pathVariableName], 'PATH value is invalid'); + }); + + test('Custom variables should not be merged with process environment varaibles', async () => { + const randomEnvVariable = `UNIT_TEST_PYTHON_EXT_RANDOM_VARIABLE_${new Date().getSeconds()}`; + const processVariables = { ...process.env }; + processVariables[randomEnvVariable] = '1234'; + if (processVariables.PYTHONPATH) { + delete processVariables.PYTHONPATH; + } + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + expect(vars).to.not.to.have.property(randomEnvVariable, undefined, 'Yikes process variable has leaked'); + }); + + test('Custom variables should be merged with process environment varaibles', async () => { + const randomEnvVariable = `UNIT_TEST_PYTHON_EXT_RANDOM_VARIABLE_${new Date().getSeconds()}`; + const processVariables = { ...process.env }; + processVariables[randomEnvVariable] = '1234'; + if (processVariables.PYTHONPATH) { + delete processVariables.PYTHONPATH; + } + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + expect(vars).to.have.property(randomEnvVariable, '1234', 'Yikes process variable has leaked'); + }); + + test('Custom variables will be refreshed when settings points to a different env file', async function () { + // https://github.com/microsoft/vscode-python/issues/12563 + // tslint:disable-next-line: no-invalid-this + return this.skip(); + // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + const processVariables = { ...process.env }; + if (processVariables.PYTHONPATH) { + delete processVariables.PYTHONPATH; + } + const envProvider = getVariablesProvider(processVariables); + const vars = await envProvider.getEnvironmentVariables(workspace4PyFile); + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + + const settings = workspace.getConfiguration('python', workspace4PyFile); + // tslint:disable-next-line:no-invalid-template-strings + await settings.update('envFile', '${workspaceRoot}/.env2', ConfigurationTarget.WorkspaceFolder); + + // Wait for settings to get refreshed. + await new Promise((resolve) => setTimeout(resolve, 5000)); + + const newVars = await envProvider.getEnvironmentVariables(workspace4PyFile); + expect(newVars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(newVars).to.have.property('X12345PYEXTUNITTESTVAR', '12345', 'X12345PYEXTUNITTESTVAR value is invalid'); + expect(newVars).to.not.to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + }); +}); diff --git a/src/test/common/variables/envVarsService.functional.test.ts b/src/test/common/variables/envVarsService.functional.test.ts new file mode 100644 index 000000000000..6feac75ee532 --- /dev/null +++ b/src/test/common/variables/envVarsService.functional.test.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { IPathUtils } from '../../../client/common/types'; +import { OSType } from '../../../client/common/utils/platform'; +import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; +import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; +import { getOSType } from '../../common'; + +use(chaiAsPromised); + +// Functional tests that run code using the VS Code API are found +// in envVarsService.test.ts. + +suite('Environment Variables Service', () => { + let pathUtils: IPathUtils; + let variablesService: IEnvironmentVariablesService; + setup(() => { + pathUtils = new PathUtils(getOSType() === OSType.Windows); + const fs = new FileSystem(); + variablesService = new EnvironmentVariablesService(pathUtils, fs); + }); + + suite('parseFile()', () => { + test('Custom variables should be undefined with no argument', async () => { + const vars = await variablesService.parseFile(undefined); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + }); + }); +}); diff --git a/src/test/common/variables/envVarsService.test.ts b/src/test/common/variables/envVarsService.test.ts new file mode 100644 index 000000000000..dbacdb4887fe --- /dev/null +++ b/src/test/common/variables/envVarsService.test.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { IPathUtils } from '../../../client/common/types'; +import { OSType } from '../../../client/common/utils/platform'; +import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; +import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; +import { getOSType } from '../../common'; + +use(chaiAsPromised); + +const envFilesFolderPath = path.join(__dirname, '..', '..', '..', '..', 'src', 'testMultiRootWkspc', 'workspace4'); + +// Functional tests that do not run code using the VS Code API are found +// in envVarsService.test.ts. + +suite('Environment Variables Service', () => { + let pathUtils: IPathUtils; + let variablesService: IEnvironmentVariablesService; + setup(() => { + pathUtils = new PathUtils(getOSType() === OSType.Windows); + const fs = new FileSystem(); + variablesService = new EnvironmentVariablesService(pathUtils, fs); + }); + + suite('parseFile()', () => { + test('Custom variables should be undefined with no argument', async () => { + const vars = await variablesService.parseFile(undefined); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + }); + + test('Custom variables should be undefined with non-existent files', async () => { + const vars = await variablesService.parseFile(path.join(envFilesFolderPath, 'abcd')); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + }); + + test('Custom variables should be undefined when folder name is passed instead of a file name', async () => { + const vars = await variablesService.parseFile(envFilesFolderPath); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + }); + + test('Custom variables should be not undefined with a valid environment file', async () => { + const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env')); + expect(vars).to.not.equal(undefined, 'Variables should be undefined'); + }); + + test('Custom variables should be parsed from env file', async () => { + const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env')); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + }); + + test('PATH and PYTHONPATH from env file should be returned as is', async () => { + const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env5')); + const expectedPythonPath = '/usr/one/three:/usr/one/four'; + const expectedPath = '/usr/x:/usr/y'; + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); + expect(vars).to.have.property('X', '1', 'X value is invalid'); + expect(vars).to.have.property('Y', '2', 'Y value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property('PATH', expectedPath, 'PATH value is invalid'); + }); + + test('Simple variable substitution is supported', async () => { + const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env6'), { + BINDIR: '/usr/bin' + }); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(3, 'Incorrect number of variables'); + expect(vars).to.have.property('REPO', '/home/user/git/foobar', 'value is invalid'); + expect(vars).to.have.property( + 'PYTHONPATH', + '/home/user/git/foobar/foo:/home/user/git/foobar/bar', + 'value is invalid' + ); + expect(vars).to.have.property('PYTHON', '/usr/bin/python3', 'value is invalid'); + }); + }); +}); diff --git a/src/test/common/variables/envVarsService.unit.test.ts b/src/test/common/variables/envVarsService.unit.test.ts new file mode 100644 index 000000000000..0e1ae863478d --- /dev/null +++ b/src/test/common/variables/envVarsService.unit.test.ts @@ -0,0 +1,654 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IPathUtils } from '../../../client/common/types'; +import { EnvironmentVariablesService, parseEnvFile } from '../../../client/common/variables/environment'; + +use(chaiAsPromised); + +type PathVar = 'Path' | 'PATH'; +const PATHS = [ + 'Path', // Windows + 'PATH' // non-Windows +]; + +suite('Environment Variables Service', () => { + const filename = 'x/y/z/.env'; + let pathUtils: TypeMoq.IMock<IPathUtils>; + let fs: TypeMoq.IMock<IFileSystem>; + let variablesService: EnvironmentVariablesService; + setup(() => { + pathUtils = TypeMoq.Mock.ofType<IPathUtils>(undefined, TypeMoq.MockBehavior.Strict); + fs = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); + variablesService = new EnvironmentVariablesService( + // This is the only place that the mocks are used. + pathUtils.object, + fs.object + ); + }); + function verifyAll() { + pathUtils.verifyAll(); + fs.verifyAll(); + } + function setFile(fileName: string, text: string) { + fs.setup((f) => f.fileExists(fileName)) // Handle the specific file. + .returns(() => Promise.resolve(true)); // The file exists. + fs.setup((f) => f.readFile(fileName)) // Handle the specific file. + .returns(() => Promise.resolve(text)); // Pretend to read from the file. + } + + suite('parseFile()', () => { + test('Custom variables should be undefined with no argument', async () => { + const vars = await variablesService.parseFile(undefined); + + expect(vars).to.equal(undefined, 'Variables should be undefined'); + verifyAll(); + }); + + test('Custom variables should be undefined with non-existent files', async () => { + fs.setup((f) => f.fileExists(filename)) // Handle the specific file. + .returns(() => Promise.resolve(false)); // The file is missing. + + const vars = await variablesService.parseFile(filename); + + expect(vars).to.equal(undefined, 'Variables should be undefined'); + verifyAll(); + }); + + test('Custom variables should be undefined when folder name is passed instead of a file name', async () => { + const dirname = 'x/y/z'; + fs.setup((f) => f.fileExists(dirname)) // Handle the specific "file". + .returns(() => Promise.resolve(false)); // It isn't a "regular" file. + + const vars = await variablesService.parseFile(dirname); + + expect(vars).to.equal(undefined, 'Variables should be undefined'); + verifyAll(); + }); + + test('Custom variables should be not undefined with a valid environment file', async () => { + setFile(filename, '...'); + + const vars = await variablesService.parseFile(filename); + + expect(vars).to.not.equal(undefined, 'Variables should be undefined'); + verifyAll(); + }); + + test('Custom variables should be parsed from env file', async () => { + // src/testMultiRootWkspc/workspace4/.env + setFile( + filename, + ` +X1234PYEXTUNITTESTVAR=1234 +PYTHONPATH=../workspace5 + ` + ); + + const vars = await variablesService.parseFile(filename); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + verifyAll(); + }); + + test('PATH and PYTHONPATH from env file should be returned as is', async () => { + const expectedPythonPath = '/usr/one/three:/usr/one/four'; + const expectedPath = '/usr/x:/usr/y'; + // src/testMultiRootWkspc/workspace4/.env + setFile( + filename, + ` +X=1 +Y=2 +PYTHONPATH=/usr/one/three:/usr/one/four +# Unix PATH variable +PATH=/usr/x:/usr/y +# Windows Path variable +Path=/usr/x:/usr/y + ` + ); + + const vars = await variablesService.parseFile(filename); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); + expect(vars).to.have.property('X', '1', 'X value is invalid'); + expect(vars).to.have.property('Y', '2', 'Y value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property('PATH', expectedPath, 'PATH value is invalid'); + verifyAll(); + }); + + test('Simple variable substitution is supported', async () => { + // src/testMultiRootWkspc/workspace4/.env + setFile( + filename, + // tslint:disable:no-invalid-template-strings + '\ +REPO=/home/user/git/foobar\n\ +PYTHONPATH=${REPO}/foo:${REPO}/bar\n\ +PYTHON=${BINDIR}/python3\n\ + ' + // tslint:enable:no-invalid-template-strings + ); + + const vars = await variablesService.parseFile(filename, { BINDIR: '/usr/bin' }); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(3, 'Incorrect number of variables'); + expect(vars).to.have.property('REPO', '/home/user/git/foobar', 'value is invalid'); + expect(vars).to.have.property( + 'PYTHONPATH', + '/home/user/git/foobar/foo:/home/user/git/foobar/bar', + 'value is invalid' + ); + expect(vars).to.have.property('PYTHON', '/usr/bin/python3', 'value is invalid'); + verifyAll(); + }); + }); + + PATHS.map((pathVariable) => { + suite(`mergeVariables() (path var: ${pathVariable})`, () => { + setup(() => { + pathUtils + .setup((pu) => pu.getPathVariableName()) // This always gets called. + .returns(() => pathVariable as PathVar); // Pretend we're on a specific platform. + }); + + test('Ensure variables are merged', async () => { + const vars1 = { ONE: '1', TWO: 'TWO' }; + const vars2 = { ONE: 'ONE', THREE: '3' }; + + variablesService.mergeVariables(vars1, vars2); + + expect(Object.keys(vars1)).lengthOf(2, 'Source variables modified'); + expect(Object.keys(vars2)).lengthOf(3, 'Variables not merged'); + expect(vars2).to.have.property('ONE', 'ONE', 'Variable overwritten'); + expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); + expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); + verifyAll(); + }); + + test('Ensure path variabnles variables are not merged into target', async () => { + const vars1 = { ONE: '1', TWO: 'TWO', PYTHONPATH: 'PYTHONPATH' }; + // tslint:disable-next-line:no-any + (vars1 as any)[pathVariable] = 'PATH'; + const vars2 = { ONE: 'ONE', THREE: '3' }; + + variablesService.mergeVariables(vars1, vars2); + + expect(Object.keys(vars1)).lengthOf(4, 'Source variables modified'); + expect(Object.keys(vars2)).lengthOf(3, 'Variables not merged'); + expect(vars2).to.have.property('ONE', 'ONE', 'Variable overwritten'); + expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); + expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); + verifyAll(); + }); + + test('Ensure path variables variables in target are left untouched', async () => { + const vars1 = { ONE: '1', TWO: 'TWO' }; + const vars2 = { ONE: 'ONE', THREE: '3', PYTHONPATH: 'PYTHONPATH' }; + // tslint:disable-next-line:no-any + (vars2 as any)[pathVariable] = 'PATH'; + + variablesService.mergeVariables(vars1, vars2); + + expect(Object.keys(vars1)).lengthOf(2, 'Source variables modified'); + expect(Object.keys(vars2)).lengthOf(5, 'Variables not merged'); + expect(vars2).to.have.property('ONE', 'ONE', 'Variable overwritten'); + expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); + expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); + expect(vars2).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); + expect(vars2).to.have.property(pathVariable, 'PATH', 'Incorrect value'); + verifyAll(); + }); + }); + }); + + PATHS.map((pathVariable) => { + suite(`appendPath() (path var: ${pathVariable})`, () => { + setup(() => { + pathUtils + .setup((pu) => pu.getPathVariableName()) // This always gets called. + .returns(() => pathVariable as PathVar); // Pretend we're on a specific platform. + }); + + test('Ensure appending PATH has no effect if an undefined value or empty string is provided and PATH does not exist in vars object', async () => { + const vars = { ONE: '1' }; + + variablesService.appendPath(vars); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + variablesService.appendPath(vars, ''); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + variablesService.appendPath(vars, ' ', ''); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + verifyAll(); + }); + + test(`Ensure appending PATH has no effect if an empty string is provided and path does not exist in vars object (${pathVariable})`, async () => { + const vars = { ONE: '1' }; + // tslint:disable-next-line:no-any + (vars as any)[pathVariable] = 'PATH'; + + variablesService.appendPath(vars); + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); + + variablesService.appendPath(vars, ''); + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); + + variablesService.appendPath(vars, ' ', ''); + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); + + verifyAll(); + }); + + test(`Ensure PATH is appeneded (${pathVariable})`, async () => { + const vars = { ONE: '1' }; + // tslint:disable-next-line:no-any + (vars as any)[pathVariable] = 'PATH'; + const pathToAppend = `/usr/one${path.delimiter}/usr/three`; + + variablesService.appendPath(vars, pathToAppend); + + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property(pathVariable, `PATH${path.delimiter}${pathToAppend}`, 'Incorrect value'); + verifyAll(); + }); + }); + }); + + suite('appendPythonPath()', () => { + test('Ensure appending PYTHONPATH has no effect if an undefined value or empty string is provided and PYTHONPATH does not exist in vars object', async () => { + const vars = { ONE: '1' }; + + variablesService.appendPythonPath(vars); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + variablesService.appendPythonPath(vars, ''); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + variablesService.appendPythonPath(vars, ' ', ''); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + verifyAll(); + }); + + test('Ensure appending PYTHONPATH has no effect if an empty string is provided and PYTHONPATH does not exist in vars object', async () => { + const vars = { ONE: '1', PYTHONPATH: 'PYTHONPATH' }; + + variablesService.appendPythonPath(vars); + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); + + variablesService.appendPythonPath(vars, ''); + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); + + variablesService.appendPythonPath(vars, ' ', ''); + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); + + verifyAll(); + }); + + test('Ensure appending PYTHONPATH has no effect if an empty string is provided and PYTHONPATH does not exist in vars object', async () => { + const vars = { ONE: '1', PYTHONPATH: 'PYTHONPATH' }; + const pathToAppend = `/usr/one${path.delimiter}/usr/three`; + + variablesService.appendPythonPath(vars, pathToAppend); + + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property( + 'PYTHONPATH', + `PYTHONPATH${path.delimiter}${pathToAppend}`, + 'Incorrect value' + ); + verifyAll(); + }); + }); +}); + +suite('Parsing Environment Variables Files', () => { + suite('parseEnvFile()', () => { + test('Custom variables should be parsed from env file', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile(` +X1234PYEXTUNITTESTVAR=1234 +PYTHONPATH=../workspace5 + `); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + }); + + test('PATH and PYTHONPATH from env file should be returned as is', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile(` +X=1 +Y=2 +PYTHONPATH=/usr/one/three:/usr/one/four +# Unix PATH variable +PATH=/usr/x:/usr/y +# Windows Path variable +Path=/usr/x:/usr/y + `); + + const expectedPythonPath = '/usr/one/three:/usr/one/four'; + const expectedPath = '/usr/x:/usr/y'; + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); + expect(vars).to.have.property('X', '1', 'X value is invalid'); + expect(vars).to.have.property('Y', '2', 'Y value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property('PATH', expectedPath, 'PATH value is invalid'); + }); + + test('Variable names must be alpha + alnum/underscore', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile(` +SPAM=1234 +ham=5678 +Eggs=9012 +_bogus1=... +1bogus2=... +bogus 3=... +bogus.4=... +bogus-5=... +bogus~6=... +VAR1=3456 +VAR_2=7890 + `); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('ham', '5678', 'value is invalid'); + expect(vars).to.have.property('Eggs', '9012', 'value is invalid'); + expect(vars).to.have.property('VAR1', '3456', 'value is invalid'); + expect(vars).to.have.property('VAR_2', '7890', 'value is invalid'); + }); + + test('Empty values become empty string', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile(` +SPAM= + `); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '', 'value is invalid'); + }); + + test('Outer quotation marks are removed', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile(` +SPAM=1234 +HAM='5678' +EGGS="9012" +FOO='"3456"' +BAR="'7890'" +BAZ="\"ABCD" +VAR1="EFGH +VAR2=IJKL" +VAR3='MN'OP' +VAR4="QR"ST" + `); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(10, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('HAM', '5678', 'value is invalid'); + expect(vars).to.have.property('EGGS', '9012', 'value is invalid'); + expect(vars).to.have.property('FOO', '"3456"', 'value is invalid'); + expect(vars).to.have.property('BAR', "'7890'", 'value is invalid'); + expect(vars).to.have.property('BAZ', '"ABCD', 'value is invalid'); + expect(vars).to.have.property('VAR1', '"EFGH', 'value is invalid'); + expect(vars).to.have.property('VAR2', 'IJKL"', 'value is invalid'); + // tslint:disable-next-line:no-suspicious-comment + // TODO: Should the outer marks be left? + expect(vars).to.have.property('VAR3', "MN'OP", 'value is invalid'); + expect(vars).to.have.property('VAR4', 'QR"ST', 'value is invalid'); + }); + + test('Whitespace is ignored', () => { + // tslint:disable:no-trailing-whitespace + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile(` +SPAM=1234 +HAM =5678 +EGGS= 9012 +FOO = 3456 + BAR=7890 + BAZ = ABCD +VAR1=EFGH ... +VAR2=IJKL +VAR3=' MNOP ' + `); + // tslint:enable:no-trailing-whitespace + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(9, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('HAM', '5678', 'value is invalid'); + expect(vars).to.have.property('EGGS', '9012', 'value is invalid'); + expect(vars).to.have.property('FOO', '3456', 'value is invalid'); + expect(vars).to.have.property('BAR', '7890', 'value is invalid'); + expect(vars).to.have.property('BAZ', 'ABCD', 'value is invalid'); + expect(vars).to.have.property('VAR1', 'EFGH ...', 'value is invalid'); + expect(vars).to.have.property('VAR2', 'IJKL', 'value is invalid'); + expect(vars).to.have.property('VAR3', ' MNOP ', 'value is invalid'); + }); + + test('Blank lines are ignored', () => { + // tslint:disable:no-trailing-whitespace + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile(` + +SPAM=1234 + +HAM=5678 + + + `); + // tslint:enable:no-trailing-whitespace + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('HAM', '5678', 'value is invalid'); + }); + + test('Comments are ignored', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile(` +# step 1 +SPAM=1234 + # step 2 +HAM=5678 +#step 3 +EGGS=9012 # ... +# done + `); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(3, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('HAM', '5678', 'value is invalid'); + expect(vars).to.have.property('EGGS', '9012 # ...', 'value is invalid'); + }); + + suite('variable substitution', () => { + // tslint:disable:no-invalid-template-strings + + test('Basic substitution syntax', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile( + '\ +REPO=/home/user/git/foobar \n\ +PYTHONPATH=${REPO}/foo:${REPO}/bar \n\ + ' + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('REPO', '/home/user/git/foobar', 'value is invalid'); + expect(vars).to.have.property( + 'PYTHONPATH', + '/home/user/git/foobar/foo:/home/user/git/foobar/bar', + 'value is invalid' + ); + }); + + test('Curly braces are required for substitution', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile('\ +SPAM=1234 \n\ +EGGS=$SPAM \n\ + '); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('EGGS', '$SPAM', 'value is invalid'); + }); + + test('Nested substitution is not supported', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile( + '\ +SPAM=EGGS \n\ +EGGS=??? \n\ +HAM1="-- ${${SPAM}} --"\n\ +abcEGGSxyz=!!! \n\ +HAM2="-- ${abc${SPAM}xyz} --"\n\ +HAM3="-- ${${SPAM} --"\n\ +HAM4="-- ${${SPAM}} ${EGGS} --"\n\ + ' + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(7, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', 'EGGS', 'value is invalid'); + expect(vars).to.have.property('EGGS', '???', 'value is invalid'); + expect(vars).to.have.property('HAM1', '-- ${${SPAM}} --', 'value is invalid'); + expect(vars).to.have.property('abcEGGSxyz', '!!!', 'value is invalid'); + expect(vars).to.have.property('HAM2', '-- ${abc${SPAM}xyz} --', 'value is invalid'); + expect(vars).to.have.property('HAM3', '-- ${${SPAM} --', 'value is invalid'); + expect(vars).to.have.property('HAM4', '-- ${${SPAM}} ${EGGS} --', 'value is invalid'); + }); + + test('Other bad substitution syntax', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile( + '\ +SPAM=EGGS \n\ +EGGS=??? \n\ +HAM1=${} \n\ +HAM2=${ \n\ +HAM3=${SPAM+EGGS} \n\ +HAM4=$SPAM \n\ + ' + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(6, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', 'EGGS', 'value is invalid'); + expect(vars).to.have.property('EGGS', '???', 'value is invalid'); + expect(vars).to.have.property('HAM1', '${}', 'value is invalid'); + expect(vars).to.have.property('HAM2', '${', 'value is invalid'); + expect(vars).to.have.property('HAM3', '${SPAM+EGGS}', 'value is invalid'); + expect(vars).to.have.property('HAM4', '$SPAM', 'value is invalid'); + }); + + test('Recursive substitution is allowed', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile( + '\ +REPO=/home/user/git/foobar \n\ +PYTHONPATH=${REPO}/foo \n\ +PYTHONPATH=${PYTHONPATH}:${REPO}/bar \n\ + ' + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('REPO', '/home/user/git/foobar', 'value is invalid'); + expect(vars).to.have.property( + 'PYTHONPATH', + '/home/user/git/foobar/foo:/home/user/git/foobar/bar', + 'value is invalid' + ); + }); + + test('Substitution may be escaped', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile( + '\ +SPAM=1234 \n\ +EGGS=\\${SPAM}/foo:\\${SPAM}/bar \n\ +HAM=$ ... $$ \n\ + ' + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(3, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('EGGS', '${SPAM}/foo:${SPAM}/bar', 'value is invalid'); + expect(vars).to.have.property('HAM', '$ ... $$', 'value is invalid'); + }); + + test('base substitution variables', () => { + // tslint:disable-next-line:no-multiline-string + const vars = parseEnvFile('\ +PYTHONPATH=${REPO}/foo:${REPO}/bar \n\ + ', { + REPO: '/home/user/git/foobar' + }); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property( + 'PYTHONPATH', + '/home/user/git/foobar/foo:/home/user/git/foobar/bar', + 'value is invalid' + ); + }); + + // tslint:enable:no-invalid-template-strings + }); + }); +}); diff --git a/src/test/common/variables/environmentVariablesProvider.unit.test.ts b/src/test/common/variables/environmentVariablesProvider.unit.test.ts new file mode 100644 index 000000000000..5faa09400240 --- /dev/null +++ b/src/test/common/variables/environmentVariablesProvider.unit.test.ts @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { ConfigurationChangeEvent, FileSystemWatcher, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { CurrentProcess } from '../../../client/common/process/currentProcess'; +import { IConfigurationService, ICurrentProcess, IPythonSettings } from '../../../client/common/types'; +import { sleep } from '../../../client/common/utils/async'; +import { clearCache } from '../../../client/common/utils/cacheUtils'; +import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; +import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; +import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; +import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; +import { noop } from '../../core'; + +// tslint:disable:no-any max-func-body-length +suite('Multiroot Environment Variables Provider', () => { + let provider: EnvironmentVariablesProvider; + let envVarsService: IEnvironmentVariablesService; + let platform: IPlatformService; + let workspace: IWorkspaceService; + let configuration: IConfigurationService; + let currentProcess: ICurrentProcess; + let settings: IPythonSettings; + let serviceContainer: IServiceContainer; + + setup(() => { + envVarsService = mock(EnvironmentVariablesService); + platform = mock(PlatformService); + workspace = mock(WorkspaceService); + configuration = mock(ConfigurationService); + currentProcess = mock(CurrentProcess); + settings = mock(PythonSettings); + serviceContainer = mock(ServiceContainer); + + when(configuration.getSettings(anything())).thenReturn(instance(settings)); + when(workspace.onDidChangeConfiguration).thenReturn(noop as any); + provider = new EnvironmentVariablesProvider( + instance(envVarsService), + [], + instance(platform), + instance(workspace), + instance(configuration), + instance(currentProcess), + instance(serviceContainer) + ); + + sinon.stub(EnvFileTelemetry, 'sendFileCreationTelemetry').returns(); + + clearCache(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Event is fired when there are changes to settings', () => { + let affectedWorkspace: Uri | undefined; + const workspaceFolder1Uri = Uri.file('workspace1'); + const workspaceFolder2Uri = Uri.file('workspace2'); + + provider.trackedWorkspaceFolders.add(workspaceFolder1Uri.fsPath); + provider.trackedWorkspaceFolders.add(workspaceFolder2Uri.fsPath); + provider.onDidEnvironmentVariablesChange((uri) => (affectedWorkspace = uri)); + const changedEvent: ConfigurationChangeEvent = { + affectsConfiguration(setting: string, uri?: Uri) { + return setting === 'python.envFile' && uri!.fsPath === workspaceFolder1Uri.fsPath; + } + }; + + provider.configurationChanged(changedEvent); + + assert.ok(affectedWorkspace); + assert.equal(affectedWorkspace!.fsPath, workspaceFolder1Uri.fsPath); + }); + test('Event is not fired when there are not changes to settings', () => { + let affectedWorkspace: Uri | undefined; + const workspaceFolderUri = Uri.file('workspace1'); + + provider.trackedWorkspaceFolders.add(workspaceFolderUri.fsPath); + provider.onDidEnvironmentVariablesChange((uri) => (affectedWorkspace = uri)); + const changedEvent: ConfigurationChangeEvent = { + affectsConfiguration(_setting: string, _uri?: Uri) { + return false; + } + }; + + provider.configurationChanged(changedEvent); + + assert.equal(affectedWorkspace, undefined); + }); + test('Event is not fired when workspace is not tracked', () => { + let affectedWorkspace: Uri | undefined; + provider.onDidEnvironmentVariablesChange((uri) => (affectedWorkspace = uri)); + const changedEvent: ConfigurationChangeEvent = { + affectsConfiguration(_setting: string, _uri?: Uri) { + return true; + } + }; + + provider.configurationChanged(changedEvent); + + assert.equal(affectedWorkspace, undefined); + }); + [undefined, Uri.file('workspace')].forEach((workspaceUri) => { + const workspaceTitle = workspaceUri ? '(with a workspace)' : '(without a workspace)'; + test(`Event is fired when the environment file is modified ${workspaceTitle}`, () => { + let affectedWorkspace: Uri | undefined = Uri.file('dummy value'); + const envFile = path.join('a', 'b', 'env.file'); + const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + + let onChangeHandler: undefined | ((resource?: Uri) => Function); + + fileSystemWatcher + .setup((fs) => fs.onDidChange(typemoq.It.isAny())) + .callback((cb) => (onChangeHandler = cb)) + .verifiable(typemoq.Times.once()); + when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); + provider.onDidEnvironmentVariablesChange((uri) => (affectedWorkspace = uri)); + + provider.createFileWatcher(envFile, workspaceUri); + + fileSystemWatcher.verifyAll(); + assert.ok(onChangeHandler); + + onChangeHandler!(); + + assert.equal(affectedWorkspace, workspaceUri); + }); + test(`Event is fired when the environment file is deleted ${workspaceTitle}`, () => { + let affectedWorkspace: Uri | undefined = Uri.file('dummy value'); + const envFile = path.join('a', 'b', 'env.file'); + const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + + let onDeleted: undefined | ((resource?: Uri) => Function); + + fileSystemWatcher + .setup((fs) => fs.onDidDelete(typemoq.It.isAny())) + .callback((cb) => (onDeleted = cb)) + .verifiable(typemoq.Times.once()); + when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); + provider.onDidEnvironmentVariablesChange((uri) => (affectedWorkspace = uri)); + + provider.createFileWatcher(envFile, workspaceUri); + + fileSystemWatcher.verifyAll(); + assert.ok(onDeleted); + + onDeleted!(); + + assert.equal(affectedWorkspace, workspaceUri); + }); + test(`Event is fired when the environment file is created ${workspaceTitle}`, () => { + let affectedWorkspace: Uri | undefined = Uri.file('dummy value'); + const envFile = path.join('a', 'b', 'env.file'); + const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + + let onCreated: undefined | ((resource?: Uri) => Function); + + fileSystemWatcher + .setup((fs) => fs.onDidCreate(typemoq.It.isAny())) + .callback((cb) => (onCreated = cb)) + .verifiable(typemoq.Times.once()); + when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); + provider.onDidEnvironmentVariablesChange((uri) => (affectedWorkspace = uri)); + + provider.createFileWatcher(envFile, workspaceUri); + + fileSystemWatcher.verifyAll(); + assert.ok(onCreated); + + onCreated!(); + + assert.equal(affectedWorkspace, workspaceUri); + }); + test(`File system watcher event handlers are added once ${workspaceTitle}`, () => { + const envFile = path.join('a', 'b', 'env.file'); + const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + + fileSystemWatcher.setup((fs) => fs.onDidChange(typemoq.It.isAny())).verifiable(typemoq.Times.once()); + fileSystemWatcher.setup((fs) => fs.onDidCreate(typemoq.It.isAny())).verifiable(typemoq.Times.once()); + fileSystemWatcher.setup((fs) => fs.onDidDelete(typemoq.It.isAny())).verifiable(typemoq.Times.once()); + when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); + + provider.createFileWatcher(envFile); + provider.createFileWatcher(envFile); + provider.createFileWatcher(envFile, workspaceUri); + provider.createFileWatcher(envFile, workspaceUri); + provider.createFileWatcher(envFile, workspaceUri); + + fileSystemWatcher.verifyAll(); + verify(workspace.createFileSystemWatcher(envFile)).once(); + }); + + test(`Getting environment variables (without an envfile, without PATH in current env, without PYTHONPATH in current env) & ${workspaceTitle}`, async () => { + const envFile = path.join('a', 'b', 'env.file'); + const workspaceFolder = workspaceUri ? { name: '', index: 0, uri: workspaceUri } : undefined; + const currentProcEnv = { SOMETHING: 'wow' }; + + when(currentProcess.env).thenReturn(currentProcEnv); + when(settings.envFile).thenReturn(envFile); + when(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(undefined); + when(platform.pathVariableName).thenReturn('PATH'); + + const vars = await provider.getEnvironmentVariables(workspaceUri); + + verify(currentProcess.env).atLeast(1); + verify(settings.envFile).atLeast(1); + verify(envVarsService.parseFile(envFile, currentProcEnv)).atLeast(1); + verify(envVarsService.mergeVariables(deepEqual(currentProcEnv), deepEqual({}))).once(); + verify(platform.pathVariableName).atLeast(1); + assert.deepEqual(vars, {}); + }); + test(`Getting environment variables (with an envfile, without PATH in current env, without PYTHONPATH in current env) & ${workspaceTitle}`, async () => { + const envFile = path.join('a', 'b', 'env.file'); + const workspaceFolder = workspaceUri ? { name: '', index: 0, uri: workspaceUri } : undefined; + const currentProcEnv = { SOMETHING: 'wow' }; + const envFileVars = { MY_FILE: '1234' }; + + when(currentProcess.env).thenReturn(currentProcEnv); + when(settings.envFile).thenReturn(envFile); + when(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(envFileVars); + when(platform.pathVariableName).thenReturn('PATH'); + + const vars = await provider.getEnvironmentVariables(workspaceUri); + + verify(currentProcess.env).atLeast(1); + verify(settings.envFile).atLeast(1); + verify(envVarsService.parseFile(envFile, currentProcEnv)).atLeast(1); + verify(envVarsService.mergeVariables(deepEqual(currentProcEnv), deepEqual(envFileVars))).once(); + verify(platform.pathVariableName).atLeast(1); + assert.deepEqual(vars, envFileVars); + }); + test(`Getting environment variables (with an envfile, with PATH in current env, with PYTHONPATH in current env) & ${workspaceTitle}`, async () => { + const envFile = path.join('a', 'b', 'env.file'); + const workspaceFolder = workspaceUri ? { name: '', index: 0, uri: workspaceUri } : undefined; + const currentProcEnv = { SOMETHING: 'wow', PATH: 'some path value', PYTHONPATH: 'some python path value' }; + const envFileVars = { MY_FILE: '1234' }; + + when(currentProcess.env).thenReturn(currentProcEnv); + when(settings.envFile).thenReturn(envFile); + when(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(envFileVars); + when(platform.pathVariableName).thenReturn('PATH'); + + const vars = await provider.getEnvironmentVariables(workspaceUri); + + verify(currentProcess.env).atLeast(1); + verify(settings.envFile).atLeast(1); + verify(envVarsService.parseFile(envFile, currentProcEnv)).atLeast(1); + verify(envVarsService.mergeVariables(deepEqual(currentProcEnv), deepEqual(envFileVars))).once(); + verify(envVarsService.appendPath(deepEqual(envFileVars), currentProcEnv.PATH)).once(); + verify(envVarsService.appendPythonPath(deepEqual(envFileVars), currentProcEnv.PYTHONPATH)).once(); + verify(platform.pathVariableName).atLeast(1); + assert.deepEqual(vars, envFileVars); + }); + + test(`Getting environment variables which are already cached does not reinvoke the method ${workspaceTitle}`, async () => { + const envFile = path.join('a', 'b', 'env.file'); + const workspaceFolder = workspaceUri ? { name: '', index: 0, uri: workspaceUri } : undefined; + const currentProcEnv = { SOMETHING: 'wow' }; + + when(currentProcess.env).thenReturn(currentProcEnv); + when(settings.envFile).thenReturn(envFile); + when(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(undefined); + when(platform.pathVariableName).thenReturn('PATH'); + + const vars = await provider.getEnvironmentVariables(workspaceUri); + + assert.deepEqual(vars, {}); + + await provider.getEnvironmentVariables(workspaceUri); + + // Verify that the contents of `_getEnvironmentVariables()` method are only invoked once + verify(configuration.getSettings(anything())).once(); + assert.deepEqual(vars, {}); + }); + + test(`Cache result must be cleared when cache expires ${workspaceTitle}`, async () => { + const envFile = path.join('a', 'b', 'env.file'); + const workspaceFolder = workspaceUri ? { name: '', index: 0, uri: workspaceUri } : undefined; + const currentProcEnv = { SOMETHING: 'wow' }; + + when(currentProcess.env).thenReturn(currentProcEnv); + when(settings.envFile).thenReturn(envFile); + when(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(undefined); + when(platform.pathVariableName).thenReturn('PATH'); + + provider = new EnvironmentVariablesProvider( + instance(envVarsService), + [], + instance(platform), + instance(workspace), + instance(configuration), + instance(currentProcess), + instance(serviceContainer), + 100 + ); + const vars = await provider.getEnvironmentVariables(workspaceUri); + + assert.deepEqual(vars, {}); + + await sleep(110); + await provider.getEnvironmentVariables(workspaceUri); + + // Verify that the contents of `_getEnvironmentVariables()` method are invoked twice + verify(configuration.getSettings(anything())).twice(); + assert.deepEqual(vars, {}); + }); + }); +}); diff --git a/src/test/common/variables/serviceRegistry.unit.test.ts b/src/test/common/variables/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..55a3bbc2f2a4 --- /dev/null +++ b/src/test/common/variables/serviceRegistry.unit.test.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; +import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; +import { registerTypes } from '../../../client/common/variables/serviceRegistry'; +import { IEnvironmentVariablesProvider, IEnvironmentVariablesService } from '../../../client/common/variables/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Common variables Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify( + serviceManager.addSingleton<IEnvironmentVariablesService>( + IEnvironmentVariablesService, + EnvironmentVariablesService + ) + ).once(); + verify( + serviceManager.addSingleton<IEnvironmentVariablesProvider>( + IEnvironmentVariablesProvider, + EnvironmentVariablesProvider + ) + ).once(); + }); +}); diff --git a/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts new file mode 100644 index 000000000000..1e247f71a9ba --- /dev/null +++ b/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; +import { Interpreters } from '../../../../client/common/utils/localize'; +import { ResetInterpreterCommand } from '../../../../client/interpreter/configuration/interpreterSelector/commands/resetInterpreter'; +import { IPythonPathUpdaterServiceManager } from '../../../../client/interpreter/configuration/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Reset Interpreter Command', () => { + let workspace: TypeMoq.IMock<IWorkspaceService>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let pythonPathUpdater: TypeMoq.IMock<IPythonPathUpdaterServiceManager>; + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; + + let resetInterpreterCommand: ResetInterpreterCommand; + + setup(() => { + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + pythonPathUpdater = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + + resetInterpreterCommand = new ResetInterpreterCommand( + pythonPathUpdater.object, + commandManager.object, + appShell.object, + workspace.object + ); + }); + + // tslint:disable-next-line: max-func-body-length + suite('Test method resetInterpreter()', async () => { + test('Update Global settings when there are no workspaces', async () => { + workspace.setup((w) => w.workspaceFolders).returns(() => undefined); + + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.Global), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await resetInterpreterCommand.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update workspace folder settings when there is one workspace folder and no workspace file', async () => { + const folder = { name: 'one', uri: Uri.parse('one'), index: 0 }; + workspace.setup((w) => w.workspaceFolders).returns(() => [folder]); + workspace.setup((w) => w.workspaceFile).returns(() => undefined); + + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder.uri) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await resetInterpreterCommand.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update selected workspace folder settings when there is more than one workspace folder', async () => { + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + appShell + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }) + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder2.uri) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await resetInterpreterCommand.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update entire workspace settings when there is more than one workspace folder and `Entire workspace` is selected', async () => { + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + appShell + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + label: Interpreters.entireWorkspace(), + uri: folder1.uri + }) + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.Workspace), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder1.uri) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await resetInterpreterCommand.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Do not update anything when user does not select a workspace folder and there is more than one workspace folder', async () => { + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + + appShell + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await resetInterpreterCommand.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + }); +}); diff --git a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts new file mode 100644 index 000000000000..64928d8fe24d --- /dev/null +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -0,0 +1,620 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, QuickPickItem, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; +import { PathUtils } from '../../../../client/common/platform/pathUtils'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { IConfigurationService, IPythonSettings } from '../../../../client/common/types'; +import { InterpreterQuickPickList, Interpreters } from '../../../../client/common/utils/localize'; +import { IMultiStepInput, IMultiStepInputFactory } from '../../../../client/common/utils/multiStepInput'; +import { + InterpreterStateArgs, + SetInterpreterCommand +} from '../../../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter'; +import { + IInterpreterQuickPickItem, + IInterpreterSelector, + IPythonPathUpdaterServiceManager +} from '../../../../client/interpreter/configuration/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Set Interpreter Command', () => { + let workspace: TypeMoq.IMock<IWorkspaceService>; + let interpreterSelector: TypeMoq.IMock<IInterpreterSelector>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let pythonPathUpdater: TypeMoq.IMock<IPythonPathUpdaterServiceManager>; + let configurationService: TypeMoq.IMock<IConfigurationService>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let platformService: TypeMoq.IMock<IPlatformService>; + let multiStepInputFactory: TypeMoq.IMock<IMultiStepInputFactory>; + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; + + let setInterpreterCommand: SetInterpreterCommand; + + setup(() => { + interpreterSelector = TypeMoq.Mock.ofType<IInterpreterSelector>(); + multiStepInputFactory = TypeMoq.Mock.ofType<IMultiStepInputFactory>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + pythonPathUpdater = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + configurationService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + + setInterpreterCommand = new SetInterpreterCommand( + appShell.object, + new PathUtils(false), + pythonPathUpdater.object, + configurationService.object, + commandManager.object, + multiStepInputFactory.object, + platformService.object, + interpreterSelector.object, + workspace.object + ); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('Test method _pickInterpreter()', async () => { + // tslint:disable-next-line: no-any + let _enterOrBrowseInterpreterPath: sinon.SinonStub<any>; + const item: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + // tslint:disable-next-line: no-any + interpreter: {} as any + }; + const expectedEnterInterpreterPathSuggestion = { + label: InterpreterQuickPickList.enterPath.label(), + detail: InterpreterQuickPickList.enterPath.detail(), + alwaysShow: true + }; + const currentPythonPath = 'python'; + setup(() => { + _enterOrBrowseInterpreterPath = sinon.stub( + SetInterpreterCommand.prototype, + '_enterOrBrowseInterpreterPath' + ); + _enterOrBrowseInterpreterPath.resolves(); + interpreterSelector + .setup((i) => i.getSuggestions(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([item])); + pythonSettings.setup((p) => p.pythonPath).returns(() => currentPythonPath); + setInterpreterCommand = new SetInterpreterCommand( + appShell.object, + new PathUtils(false), + pythonPathUpdater.object, + configurationService.object, + commandManager.object, + multiStepInputFactory.object, + platformService.object, + interpreterSelector.object, + workspace.object + ); + }); + teardown(() => { + sinon.restore(); + }); + + test('Existing state path must be removed before displaying picker', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(undefined as any)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + expect(state.path).to.equal(undefined, ''); + }); + + test('Picker should be displayed with expected items', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + const suggestions = [expectedEnterInterpreterPathSuggestion, item]; + const expectedParameters = { + placeholder: InterpreterQuickPickList.quickPickListPlaceholder().format(currentPythonPath), + items: suggestions, + activeItem: item, + matchOnDetail: true, + matchOnDescription: true + }; + multiStepInput + .setup((i) => i.showQuickPick(expectedParameters)) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(undefined as any)) + .verifiable(TypeMoq.Times.once()); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + multiStepInput.verifyAll(); + }); + + test('If an item is selected, update state and return', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(item as any)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + expect(state.path).to.equal(item.path, ''); + }); + + test('If `Enter or browse...` option is selected, call the corresponding method with correct arguments', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(expectedEnterInterpreterPathSuggestion as any)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + assert( + _enterOrBrowseInterpreterPath.calledOnceWith(multiStepInput.object, { + path: undefined, + workspace: undefined + }) + ); + }); + }); + + suite('Test method _enterOrBrowseInterpreterPath()', async () => { + const items: QuickPickItem[] = [ + { + label: InterpreterQuickPickList.browsePath.label(), + detail: InterpreterQuickPickList.browsePath.detail() + } + ]; + const expectedParameters = { + placeholder: InterpreterQuickPickList.enterPath.placeholder(), + items, + acceptFilterBoxTextAsSelection: true + }; + + test('Picker should be displayed with expected items', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(expectedParameters)) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(undefined as any)) + .verifiable(TypeMoq.Times.once()); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + + multiStepInput.verifyAll(); + }); + + test('If user enters path to interpreter in the filter box, get path and update state', async () => { + const state: InterpreterStateArgs = { path: undefined, workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve('enteredPath' as any)); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + + expect(state.path).to.equal('enteredPath', ''); + }); + + test('If `Browse...` is selected, open the file browser to get path and update state', async () => { + const state: InterpreterStateArgs = { path: undefined, workspace: undefined }; + const expectedPathUri = Uri.parse('browsed path'); + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(items[0] as any)); + appShell + .setup((a) => a.showOpenDialog(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([expectedPathUri])); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + + expect(state.path).to.equal(expectedPathUri.fsPath, ''); + }); + + test('If `Browse...` option is selected on Windows, file browser is opened using expected parameters', async () => { + const state: InterpreterStateArgs = { path: undefined, workspace: undefined }; + const filtersKey = 'Executables'; + const filtersObject: { [name: string]: string[] } = {}; + filtersObject[filtersKey] = ['exe']; + const expectedParams = { + filters: filtersObject, + openLabel: InterpreterQuickPickList.browsePath.openButtonLabel(), + canSelectMany: false, + title: InterpreterQuickPickList.browsePath.title() + }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(items[0] as any)); + appShell + // tslint:disable-next-line: no-any + .setup((a) => a.showOpenDialog(expectedParams as any)) + .verifiable(TypeMoq.Times.once()); + platformService.setup((p) => p.isWindows).returns(() => true); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + + appShell.verifyAll(); + }); + + test('If `Browse...` option is selected on non-Windows, file browser is opened using expected parameters', async () => { + const state: InterpreterStateArgs = { path: undefined, workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + const expectedParams = { + filters: undefined, + openLabel: InterpreterQuickPickList.browsePath.openButtonLabel(), + canSelectMany: false, + title: InterpreterQuickPickList.browsePath.title() + }; + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + // tslint:disable-next-line: no-any + .returns(() => Promise.resolve(items[0] as any)); + appShell.setup((a) => a.showOpenDialog(expectedParams)).verifiable(TypeMoq.Times.once()); + platformService.setup((p) => p.isWindows).returns(() => false); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + + appShell.verifyAll(); + }); + }); + // tslint:disable-next-line: max-func-body-length + suite('Test method setInterpreter()', async () => { + test('Update Global settings when there are no workspaces', async () => { + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + // tslint:disable-next-line: no-any + interpreter: {} as any + }; + + workspace.setup((w) => w.workspaceFolders).returns(() => undefined); + + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + const multiStepInput = { + // tslint:disable-next-line: no-any + run: (_: any, state: InterpreterStateArgs) => { + state.path = selectedItem.path; + return Promise.resolve(); + } + }; + multiStepInputFactory + .setup((f) => f.create()) + // tslint:disable-next-line: no-any + .returns(() => multiStepInput as any); + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.Global), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await setInterpreterCommand.setInterpreter(); + + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update workspace folder settings when there is one workspace folder and no workspace file', async () => { + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + workspace.setup((w) => w.workspaceFile).returns(() => undefined); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + // tslint:disable-next-line: no-any + interpreter: {} as any + }; + + const folder = { name: 'one', uri: Uri.parse('one'), index: 0 }; + workspace.setup((w) => w.workspaceFolders).returns(() => [folder]); + + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + + const multiStepInput = { + // tslint:disable-next-line: no-any + run: (_: any, state: InterpreterStateArgs) => { + state.path = selectedItem.path; + return Promise.resolve(); + } + }; + multiStepInputFactory + .setup((f) => f.create()) + // tslint:disable-next-line: no-any + .returns(() => multiStepInput as any); + + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder.uri) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await setInterpreterCommand.setInterpreter(); + + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update selected workspace folder settings when there is more than one workspace folder', async () => { + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + // tslint:disable-next-line: no-any + interpreter: {} as any + }; + + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + + const multiStepInput = { + // tslint:disable-next-line: no-any + run: (_: any, state: InterpreterStateArgs) => { + state.path = selectedItem.path; + return Promise.resolve(); + } + }; + multiStepInputFactory + .setup((f) => f.create()) + // tslint:disable-next-line: no-any + .returns(() => multiStepInput as any); + appShell + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }) + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder2.uri) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await setInterpreterCommand.setInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update entire workspace settings when there is more than one workspace folder and `Entire workspace` is selected', async () => { + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + // tslint:disable-next-line: no-any + interpreter: {} as any + }; + + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + + interpreterSelector + .setup((i) => i.getSuggestions(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([selectedItem])); + const multiStepInput = { + // tslint:disable-next-line: no-any + run: (_: any, state: InterpreterStateArgs) => { + state.path = selectedItem.path; + return Promise.resolve(); + } + }; + multiStepInputFactory + .setup((f) => f.create()) + // tslint:disable-next-line: no-any + .returns(() => multiStepInput as any); + appShell + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + label: Interpreters.entireWorkspace(), + uri: folder1.uri + }) + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.Workspace), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder1.uri) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await setInterpreterCommand.setInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Do not update anything when user does not select a workspace folder and there is more than one workspace folder', async () => { + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + multiStepInputFactory + .setup((f) => f.create()) + // tslint:disable-next-line: no-any + .verifiable(TypeMoq.Times.never()); + + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri + }, + { + label: Interpreters.entireWorkspace(), + uri: folder1.uri + } + ]; + + appShell + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await setInterpreterCommand.setInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + multiStepInputFactory.verifyAll(); + }); + test('Make sure multiStepInput.run is called with the correct arguments', async () => { + const pickInterpreter = sinon.stub(SetInterpreterCommand.prototype, '_pickInterpreter'); + setInterpreterCommand = new SetInterpreterCommand( + appShell.object, + new PathUtils(false), + pythonPathUpdater.object, + configurationService.object, + commandManager.object, + multiStepInputFactory.object, + platformService.object, + interpreterSelector.object, + workspace.object + ); + let inputStep!: Function; + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + // tslint:disable-next-line: no-any + interpreter: {} as any + }; + + workspace.setup((w) => w.workspaceFolders).returns(() => undefined); + + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + const multiStepInput = { + // tslint:disable-next-line: no-any + run: (inputStepArg: any, state: InterpreterStateArgs) => { + inputStep = inputStepArg; + state.path = selectedItem.path; + return Promise.resolve(); + } + }; + multiStepInputFactory + .setup((f) => f.create()) + // tslint:disable-next-line: no-any + .returns(() => multiStepInput as any); + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.Global), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(undefined) + ) + ) + .returns(() => Promise.resolve()); + + await setInterpreterCommand.setInterpreter(); + + expect(inputStep).to.not.equal(undefined, ''); + + assert(pickInterpreter.notCalled); + await inputStep(); + assert(pickInterpreter.calledOnce); + }); + }); +}); diff --git a/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts b/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts new file mode 100644 index 000000000000..020f8e6544da --- /dev/null +++ b/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { DeprecatePythonPath } from '../../../client/common/experiments/groups'; +import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IExperimentsManager } from '../../../client/common/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { IInterpreterSecurityService } from '../../../client/interpreter/autoSelection/types'; +import { InterpreterSelector } from '../../../client/interpreter/configuration/interpreterSelector/interpreterSelector'; +import { IInterpreterComparer, IInterpreterQuickPickItem } from '../../../client/interpreter/configuration/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +const info: PythonEnvironment = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + envType: EnvironmentType.Unknown, + version: new SemVer('1.0.0-alpha'), + sysPrefix: '', + sysVersion: '' +}; + +class InterpreterQuickPickItem implements IInterpreterQuickPickItem { + public path: string; + public label: string; + public description!: string; + public detail?: string; + // tslint:disable-next-line: no-any + public interpreter = {} as any; + constructor(l: string, p: string) { + this.path = p; + this.label = l; + } +} + +// tslint:disable-next-line:max-func-body-length +suite('Interpreters - selector', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let comparer: TypeMoq.IMock<IInterpreterComparer>; + let experimentsManager: TypeMoq.IMock<IExperimentsManager>; + let interpreterSecurityService: TypeMoq.IMock<IInterpreterSecurityService>; + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + + class TestInterpreterSelector extends InterpreterSelector { + // tslint:disable-next-line:no-unnecessary-override + public async suggestionToQuickPickItem( + suggestion: PythonEnvironment, + workspaceUri?: Uri + ): Promise<IInterpreterQuickPickItem> { + return super.suggestionToQuickPickItem(suggestion, workspaceUri); + } + } + + let selector: TestInterpreterSelector; + + setup(() => { + experimentsManager = TypeMoq.Mock.ofType<IExperimentsManager>(); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + interpreterSecurityService = TypeMoq.Mock.ofType<IInterpreterSecurityService>(); + comparer = TypeMoq.Mock.ofType<IInterpreterComparer>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + fileSystem + .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) + .returns((a: string, b: string) => a === b); + + comparer.setup((c) => c.compare(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => 0); + selector = new TestInterpreterSelector( + interpreterService.object, + comparer.object, + experimentsManager.object, + interpreterSecurityService.object, + new PathUtils(false) + ); + }); + + [true, false].forEach((isWindows) => { + test(`Suggestions (${isWindows ? 'Windows' : 'Non-Windows'})`, async () => { + selector = new TestInterpreterSelector( + interpreterService.object, + comparer.object, + experimentsManager.object, + interpreterSecurityService.object, + new PathUtils(isWindows) + ); + + const initial: PythonEnvironment[] = [ + { displayName: '1', path: 'c:/path1/path1', envType: EnvironmentType.Unknown }, + { displayName: '2', path: 'c:/path1/path1', envType: EnvironmentType.Unknown }, + { displayName: '2', path: 'c:/path2/path2', envType: EnvironmentType.Unknown }, + { displayName: '2 (virtualenv)', path: 'c:/path2/path2', envType: EnvironmentType.VirtualEnv }, + { displayName: '3', path: 'c:/path2/path2', envType: EnvironmentType.Unknown }, + { displayName: '4', path: 'c:/path4/path4', envType: EnvironmentType.Conda } + ].map((item) => { + return { ...info, ...item }; + }); + interpreterService + .setup((x) => x.getInterpreters(TypeMoq.It.isAny(), { onSuggestion: true })) + .returns(() => new Promise((resolve) => resolve(initial))); + + const actual = await selector.getSuggestions(undefined); + + const expected: InterpreterQuickPickItem[] = [ + new InterpreterQuickPickItem('1', 'c:/path1/path1'), + new InterpreterQuickPickItem('2', 'c:/path1/path1'), + new InterpreterQuickPickItem('2', 'c:/path2/path2'), + new InterpreterQuickPickItem('2 (virtualenv)', 'c:/path2/path2'), + new InterpreterQuickPickItem('3', 'c:/path2/path2'), + new InterpreterQuickPickItem('4', 'c:/path4/path4') + ]; + + assert.equal(actual.length, expected.length, 'Suggestion lengths are different.'); + for (let i = 0; i < expected.length; i += 1) { + assert.equal( + actual[i].label, + expected[i].label, + `Suggestion label is different at ${i}: exected '${expected[i].label}', found '${actual[i].label}'.` + ); + assert.equal( + actual[i].path, + expected[i].path, + `Suggestion path is different at ${i}: exected '${expected[i].path}', found '${actual[i].path}'.` + ); + } + }); + }); + + test('When in Deprecate PythonPath experiment, remove unsafe interpreters from the suggested interpreters list', async () => { + // tslint:disable-next-line: no-any + const interpreterList = ['interpreter1', 'interpreter2', 'interpreter3'] as any; + interpreterService + .setup((i) => i.getInterpreters(folder1.uri, { onSuggestion: true })) + .returns(() => interpreterList); + // tslint:disable-next-line: no-any + interpreterSecurityService.setup((i) => i.isSafe('interpreter1' as any)).returns(() => true); + // tslint:disable-next-line: no-any + interpreterSecurityService.setup((i) => i.isSafe('interpreter2' as any)).returns(() => false); + // tslint:disable-next-line: no-any + interpreterSecurityService.setup((i) => i.isSafe('interpreter3' as any)).returns(() => undefined); + experimentsManager.reset(); + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + // tslint:disable-next-line: no-any + selector.suggestionToQuickPickItem = (item, _) => Promise.resolve(item as any); + const suggestion = await selector.getSuggestions(folder1.uri); + assert.deepEqual(suggestion, ['interpreter1', 'interpreter3']); + }); +}); diff --git a/src/test/constants.ts b/src/test/constants.ts new file mode 100644 index 000000000000..2e255ecec9d5 --- /dev/null +++ b/src/test/constants.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { IS_CI_SERVER, IS_CI_SERVER_TEST_DEBUGGER } from './ciConstants'; + +// Activating extension for Multiroot and Debugger CI tests for Windows takes just over 2 minutes sometimes, so 3 minutes seems like a safe margin +export const MAX_EXTENSION_ACTIVATION_TIME = 180_000; +export const TEST_TIMEOUT = 25000; +export const TEST_RETRYCOUNT = 3; +export const IS_SMOKE_TEST = process.env.VSC_PYTHON_SMOKE_TEST === '1'; +export const IS_PERF_TEST = process.env.VSC_PYTHON_PERF_TEST === '1'; +export const IS_MULTI_ROOT_TEST = isMultitrootTest(); + +// If running on CI server, then run debugger tests ONLY if the corresponding flag is enabled. +export const TEST_DEBUGGER = IS_CI_SERVER ? IS_CI_SERVER_TEST_DEBUGGER : true; + +function isMultitrootTest() { + // No need to run smoke nor perf tests in a multi-root environment. + if (IS_SMOKE_TEST || IS_PERF_TEST) { + return false; + } + try { + // tslint:disable-next-line:no-require-imports + const vscode = require('vscode'); + const workspace = vscode.workspace; + return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1; + } catch { + // being accessed, when VS Code hasn't been launched. + return false; + } +} + +export const EXTENSION_ROOT_DIR_FOR_TESTS = path.join(__dirname, '..', '..'); +export const PVSC_EXTENSION_ID_FOR_TESTS = 'ms-python.python'; + +export const SMOKE_TEST_EXTENSIONS_DIR = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'tmp', + 'ext', + 'smokeTestExtensionsFolder' +); diff --git a/src/test/core.ts b/src/test/core.ts new file mode 100644 index 000000000000..8db9d06665ec --- /dev/null +++ b/src/test/core.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// File without any dependencies on VS Code. + +export async function sleep(milliseconds: number) { + return new Promise<void>((resolve) => setTimeout(resolve, milliseconds)); +} + +// tslint:disable-next-line:no-empty +export function noop() {} + +export const isWindows = /^win/.test(process.platform); diff --git a/src/test/datascience/.vscode/settings.json b/src/test/datascience/.vscode/settings.json new file mode 100644 index 000000000000..90f4ac4dc843 --- /dev/null +++ b/src/test/datascience/.vscode/settings.json @@ -0,0 +1,29 @@ +{ + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": false, + "python.workspaceSymbols.enabled": false, + "python.linting.lintOnSave": false, + "python.linting.enabled": true, + "python.linting.pycodestyleEnabled": false, + "python.linting.prospectorEnabled": false, + "python.linting.pydocstyleEnabled": false, + "python.linting.pylamaEnabled": false, + "python.linting.mypyEnabled": false, + "python.linting.banditEnabled": false, + "python.formatting.provider": "yapf", + "python.pythonPath": "python", + "python.experiments.optInto": [ + "LocalZMQKernel - experiment", + "NativeNotebook - experiment" + ], + // Do not set this to "Microsoft", else it will result in LS being downloaded on CI + // and that slows down tests significantly. We have other tests on CI for testing + // downloading of LS with this setting enabled. + "python.languageServer": "Jedi", + // Ensure auto save is off. + "files.autoSave": "off", + // We don't want jupyter to start when testing (DS functionality or anything else). + "python.dataScience.disableJupyterAutoStart": true, + "python.logging.level": "debug", + "python.dataScience.alwaysTrustNotebooks": true // In UI tests we don't want prompts for `Do you want to trust thie nb...` +} diff --git a/src/test/datascience/DefaultSalesReport.csv b/src/test/datascience/DefaultSalesReport.csv new file mode 100644 index 000000000000..02a53318cf00 --- /dev/null +++ b/src/test/datascience/DefaultSalesReport.csv @@ -0,0 +1,278 @@ +Product,Customer,Qtr 1,Qtr 2,Qtr 3,Qtr 4 +Alice Mutton,ANTON, $- , $702.00 , $- , $- +Alice Mutton,BERGS, $312.00 , $- , $- , $- +Alice Mutton,BOLID, $- , $- , $- ," $1,170.00 " +Alice Mutton,BOTTM," $1,170.00 ", $- , $- , $- +Alice Mutton,ERNSH," $1,123.20 ", $- , $- ," $2,607.15 " +Alice Mutton,GODOS, $- , $280.80 , $- , $- +Alice Mutton,HUNGC, $62.40 , $- , $- , $- +Alice Mutton,PICCO, $- ," $1,560.00 ", $936.00 , $- +Alice Mutton,RATTC, $- , $592.80 , $- , $- +Alice Mutton,REGGC, $- , $- , $- , $741.00 +Alice Mutton,SAVEA, $- , $- ," $3,900.00 ", $789.75 +Alice Mutton,SEVES, $- , $877.50 , $- , $- +Alice Mutton,WHITC, $- , $- , $- , $780.00 +Aniseed Syrup,ALFKI, $- , $- , $- , $60.00 +Aniseed Syrup,BOTTM, $- , $- , $- , $200.00 +Aniseed Syrup,ERNSH, $- , $- , $- , $180.00 +Aniseed Syrup,LINOD, $544.00 , $- , $- , $- +Aniseed Syrup,QUICK, $- , $600.00 , $- , $- +Aniseed Syrup,VAFFE, $- , $- , $140.00 , $- +Boston Crab Meat,ANTON, $- , $165.60 , $- , $- +Boston Crab Meat,BERGS, $- , $920.00 , $- , $- +Boston Crab Meat,BONAP, $- , $248.40 , $524.40 , $- +Boston Crab Meat,BOTTM, $551.25 , $- , $- , $- +Boston Crab Meat,BSBEV, $147.00 , $- , $- , $- +Boston Crab Meat,FRANS, $- , $- , $- , $18.40 +Boston Crab Meat,HILAA, $- , $92.00 ," $1,104.00 ", $- +Boston Crab Meat,LAZYK, $147.00 , $- , $- , $- +Boston Crab Meat,LEHMS, $- , $515.20 , $- , $- +Boston Crab Meat,MAGAA, $- , $- , $- , $55.20 +Boston Crab Meat,OTTIK, $- , $- , $368.00 , $- +Boston Crab Meat,PERIC, $308.70 , $- , $- , $- +Boston Crab Meat,QUEEN, $26.46 , $- , $419.52 , $110.40 +Boston Crab Meat,QUICK, $- , $- ," $1,223.60 ", $- +Boston Crab Meat,RANCH, $294.00 , $- , $- , $- +Boston Crab Meat,SAVEA, $- , $- , $772.80 , $736.00 +Boston Crab Meat,TRAIH, $- , $36.80 , $- , $- +Boston Crab Meat,VAFFE, $294.00 , $- , $- , $736.00 +Camembert Pierrot,ANATR, $- , $- , $340.00 , $- +Camembert Pierrot,AROUT, $- , $- , $- , $510.00 +Camembert Pierrot,BERGS, $- , $- , $680.00 , $- +Camembert Pierrot,BOTTM, $- , $- , $- ," $1,700.00 " +Camembert Pierrot,CHOPS, $- , $323.00 , $- , $- +Camembert Pierrot,FAMIA, $- , $346.80 , $- , $- +Camembert Pierrot,FRANK, $- , $- , $612.00 , $- +Camembert Pierrot,FURIB, $544.00 , $- , $- , $- +Camembert Pierrot,GOURL, $- , $- , $- , $340.00 +Camembert Pierrot,LEHMS, $- , $892.50 , $- , $- +Camembert Pierrot,MEREP, $- , $- ," $2,261.00 ", $- +Camembert Pierrot,OTTIK, $- , $- ," $1,020.00 ", $- +Camembert Pierrot,QUEEN, $- , $- , $- , $510.00 +Camembert Pierrot,QUICK, $- ," $2,427.60 "," $1,776.50 ", $- +Camembert Pierrot,RICAR," $1,088.00 ", $- , $- , $- +Camembert Pierrot,RICSU," $1,550.40 ", $- , $- , $- +Camembert Pierrot,SAVEA, $- , $- ," $2,380.00 ", $- +Camembert Pierrot,WARTH, $- , $693.60 , $- , $- +Camembert Pierrot,WOLZA, $- , $- , $510.00 , $- +Chef Anton's Cajun Seasoning,BERGS, $- , $- , $237.60 , $- +Chef Anton's Cajun Seasoning,BONAP, $- , $935.00 , $- , $- +Chef Anton's Cajun Seasoning,EASTC, $- , $- , $- , $550.00 +Chef Anton's Cajun Seasoning,FOLKO, $- ," $1,045.00 ", $- , $- +Chef Anton's Cajun Seasoning,FURIB, $225.28 , $- , $- , $- +Chef Anton's Cajun Seasoning,MAGAA, $- , $- , $198.00 , $- +Chef Anton's Cajun Seasoning,QUEEN, $- , $- , $- , $132.00 +Chef Anton's Cajun Seasoning,QUICK, $- , $990.00 , $- , $- +Chef Anton's Cajun Seasoning,TRADH, $- , $- , $352.00 , $- +Chef Anton's Cajun Seasoning,WARTH, $- , $- , $550.00 , $- +Chef Anton's Gumbo Mix,MAGAA, $- , $- , $288.22 , $- +Chef Anton's Gumbo Mix,THEBI, $- , $- , $- , $85.40 +Filo Mix,AROUT, $- , $210.00 , $- , $56.00 +Filo Mix,BERGS, $- , $- , $- , $175.00 +Filo Mix,BLONP, $112.00 , $- , $- , $- +Filo Mix,DUMON, $- , $- , $63.00 , $- +Filo Mix,FAMIA, $- , $- , $- , $28.00 +Filo Mix,LAUGB, $- , $- , $35.00 , $- +Filo Mix,NORTS, $- , $42.00 , $- , $- +Filo Mix,OLDWO, $- , $- , $168.00 , $- +Filo Mix,REGGC, $- , $- , $23.80 , $- +Filo Mix,RICAR, $- , $490.00 , $- , $- +Filo Mix,RICSU, $- , $- , $- , $420.00 +Filo Mix,TOMSP, $75.60 , $- , $- , $- +Filo Mix,VAFFE, $- , $- , $- , $99.75 +Filo Mix,VINET, $- , $- , $- , $126.00 +Gorgonzola Telino,AROUT, $- , $- , $- , $625.00 +Gorgonzola Telino,BLONP, $- , $593.75 , $- , $- +Gorgonzola Telino,BONAP, $- , $- , $- , $35.62 +Gorgonzola Telino,CACTU, $- , $- , $- , $12.50 +Gorgonzola Telino,ERNSH, $- , $- , $- , $890.00 +Gorgonzola Telino,FOLKO, $- , $- , $- , $18.75 +Gorgonzola Telino,GOURL, $140.00 , $- , $- , $- +Gorgonzola Telino,HANAR, $- , $- , $- , $125.00 +Gorgonzola Telino,HILAA, $- , $- , $- , $250.00 +Gorgonzola Telino,HUNGO, $- , $600.00 , $- , $- +Gorgonzola Telino,LEHMS, $- , $250.00 , $- , $- +Gorgonzola Telino,OLDWO, $- , $- , $187.50 , $- +Gorgonzola Telino,PICCO, $- , $- , $- , $100.00 +Gorgonzola Telino,QUEEN, $- , $- , $237.50 , $- +Gorgonzola Telino,QUICK, $- , $584.37 , $- , $- +Gorgonzola Telino,RATTC, $- , $421.25 , $- , $- +Gorgonzola Telino,RICSU, $- , $375.00 , $- , $- +Gorgonzola Telino,SAVEA, $- , $- , $- , $625.00 +Gorgonzola Telino,SUPRD, $297.50 , $- , $- , $- +Gorgonzola Telino,TOMSP, $27.00 , $- , $- , $- +Gorgonzola Telino,TORTU, $- , $250.00 , $- , $- +Gorgonzola Telino,TRADH, $- , $190.00 , $- , $- +Gorgonzola Telino,WANDK, $- , $- , $90.00 , $- +Gorgonzola Telino,WARTH, $- , $375.00 , $- , $- +Grandma's Boysenberry Spread,GOURL, $- , $- , $- , $750.00 +Grandma's Boysenberry Spread,MEREP, $- , $- ," $1,750.00 ", $- +Ipoh Coffee,ANTON, $- , $586.50 , $- , $- +Ipoh Coffee,BERGS, $- ," $2,760.00 ", $- , $- +Ipoh Coffee,FURIB, $110.40 , $- , $- , $- +Ipoh Coffee,KOENE, $552.00 , $- , $- , $- +Ipoh Coffee,MAISD, $- , $- , $- ," $1,035.00 " +Ipoh Coffee,OLDWO, $- , $- , $- ," $1,104.00 " +Ipoh Coffee,PICCO, $- ," $1,150.00 ", $- , $- +Ipoh Coffee,QUICK, $- , $- , $- ," $1,840.00 " +Ipoh Coffee,SUPRD, $736.00 , $- , $- , $- +Ipoh Coffee,WELLI, $- , $- , $920.00 , $- +Ipoh Coffee,WILMK, $- , $- , $276.00 , $- +Jack's New England Clam Chowder,AROUT, $- , $- , $- , $135.10 +Jack's New England Clam Chowder,BERGS, $231.00 , $- , $- , $96.50 +Jack's New England Clam Chowder,BLONP, $- , $110.01 , $- , $- +Jack's New England Clam Chowder,BOTTM, $154.00 , $- , $- , $- +Jack's New England Clam Chowder,CACTU, $- , $96.50 , $- , $- +Jack's New England Clam Chowder,FAMIA, $- , $- , $- , $115.80 +Jack's New England Clam Chowder,FRANK, $- , $- , $- , $183.35 +Jack's New England Clam Chowder,GOURL, $- , $- , $38.60 , $- +Jack's New England Clam Chowder,HUNGO, $- , $694.80 , $- , $- +Jack's New England Clam Chowder,LAUGB, $- , $154.00 , $- , $- +Jack's New England Clam Chowder,OTTIK, $- , $82.51 , $- , $- +Jack's New England Clam Chowder,PICCO, $- , $- , $- , $337.75 +Jack's New England Clam Chowder,REGGC, $- , $- , $154.40 , $- +Jack's New England Clam Chowder,SAVEA, $- , $- ," $1,389.60 ", $405.30 +Jack's New England Clam Chowder,SEVES, $- , $52.11 , $- , $- +Jack's New England Clam Chowder,TOMSP, $- , $135.10 , $- , $- +Jack's New England Clam Chowder,VAFFE, $- , $- , $- , $275.02 +Jack's New England Clam Chowder,VINET, $- , $- , $- , $115.80 +Laughing Lumberjack Lager,FRANK, $- , $- , $350.00 , $- +Laughing Lumberjack Lager,LONEP, $- , $98.00 , $- , $- +Laughing Lumberjack Lager,PERIC, $- , $420.00 , $- , $- +Laughing Lumberjack Lager,THECR, $- , $- , $- , $42.00 +Longlife Tofu,FRANS, $- , $- , $- , $50.00 +Longlife Tofu,HILAA, $128.00 , $- , $- , $- +Longlife Tofu,MEREP, $240.00 , $- , $- , $- +Longlife Tofu,QUICK, $120.00 , $- , $- , $- +Longlife Tofu,VICTE, $- , $- , $- , $112.50 +Longlife Tofu,WARTH, $- , $- , $- , $350.00 +Louisiana Fiery Hot Pepper Sauce,BONAP, $- , $- , $- , $199.97 +Louisiana Fiery Hot Pepper Sauce,ERNSH, $- , $820.95 , $- ," $1,299.84 " +Louisiana Fiery Hot Pepper Sauce,FRANR, $- , $- , $252.60 , $- +Louisiana Fiery Hot Pepper Sauce,FURIB, $- , $- , $268.39 , $- +Louisiana Fiery Hot Pepper Sauce,HANAR, $- , $682.02 , $- , $- +Louisiana Fiery Hot Pepper Sauce,HUNGO, $- , $421.00 , $- , $842.00 +Louisiana Fiery Hot Pepper Sauce,LAMAI, $- , $226.80 , $- , $- +Louisiana Fiery Hot Pepper Sauce,LINOD, $- , $- , $442.05 , $- +Louisiana Fiery Hot Pepper Sauce,OTTIK, $- , $599.92 , $- , $- +Louisiana Fiery Hot Pepper Sauce,PICCO, $- , $- , $202.08 , $- +Louisiana Fiery Hot Pepper Sauce,QUICK, $423.36 , $- , $- ," $1,515.60 " +Louisiana Fiery Hot Pepper Sauce,RATTC, $336.00 , $- , $- , $- +Louisiana Fiery Hot Pepper Sauce,RICAR, $588.00 , $- , $- , $- +Louisiana Fiery Hot Pepper Sauce,RICSU, $- , $- , $210.50 , $- +Louisiana Fiery Hot Pepper Sauce,VICTE, $- , $- , $- , $42.10 +Louisiana Hot Spiced Okra,ANTON, $- , $- , $68.00 , $- +Louisiana Hot Spiced Okra,EASTC, $- , $408.00 , $- , $- +Louisiana Hot Spiced Okra,ERNSH, $816.00 , $- , $- , $- +Louisiana Hot Spiced Okra,FOLKO, $- , $- , $- , $850.00 +Louisiana Hot Spiced Okra,LAMAI, $- , $122.40 , $- , $- +Louisiana Hot Spiced Okra,SUPRD, $693.60 , $- , $- , $- +Mozzarella di Giovanni,BOTTM, $- , $- , $- ," $1,218.00 " +Mozzarella di Giovanni,BSBEV, $- , $34.80 , $- , $- +Mozzarella di Giovanni,CONSH, $278.00 , $- , $- , $- +Mozzarella di Giovanni,FOLKO, $- , $835.20 , $- , $- +Mozzarella di Giovanni,GREAL, $- , $313.20 , $- , $- +Mozzarella di Giovanni,ISLAT, $- , $- , $- , $348.00 +Mozzarella di Giovanni,LEHMS, $- , $695.00 , $- , $- +Mozzarella di Giovanni,LINOD, $- , $- ," $2,088.00 ", $- +Mozzarella di Giovanni,MAGAA, $- , $- , $- , $887.40 +Mozzarella di Giovanni,MAISD, $- , $- , $522.00 , $- +Mozzarella di Giovanni,MORGK, $- ," $1,044.00 ", $- , $- +Mozzarella di Giovanni,QUICK, $- , $- , $- , $243.60 +Mozzarella di Giovanni,RICSU, $- , $730.80 , $- , $- +Mozzarella di Giovanni,SAVEA, $- , $- , $417.60 , $- +Mozzarella di Giovanni,SIMOB, $- , $835.20 , $- , $- +Mozzarella di Giovanni,VICTE," $1,112.00 ", $- , $- , $- +Northwoods Cranberry Sauce,BONAP, $- , $340.00 , $- , $- +Northwoods Cranberry Sauce,GOURL, $- , $- , $- ," $1,600.00 " +Northwoods Cranberry Sauce,LEHMS, $- , $960.00 , $- , $- +Northwoods Cranberry Sauce,QUEEN, $- , $- , $- , $960.00 +Northwoods Cranberry Sauce,WILMK, $- , $- , $- , $400.00 +Ravioli Angelo,ANTON, $- , $87.75 , $- , $- +Ravioli Angelo,AROUT, $- , $- , $- , $780.00 +Ravioli Angelo,BLAUS, $- , $78.00 , $- , $- +Ravioli Angelo,BONAP, $- , $- , $- , $204.75 +Ravioli Angelo,BSBEV, $- , $117.00 , $- , $- +Ravioli Angelo,PICCO, $- , $- , $390.00 , $- +Ravioli Angelo,TOMSP, $187.20 , $- , $- , $- +Ravioli Angelo,WARTH, $312.00 , $- , $- , $- +Sasquatch Ale,ANTON, $- , $560.00 , $- , $- +Sasquatch Ale,SAVEA, $- , $- , $- , $554.40 +Sasquatch Ale,THEBI, $- , $- , $- , $140.00 +Sasquatch Ale,TOMSP, $179.20 , $105.00 , $- , $- +Sasquatch Ale,VAFFE, $- , $- , $- , $196.00 +Sasquatch Ale,WHITC, $372.40 , $- , $- , $- +Sir Rodney's Marmalade,ERNSH, $- ," $3,159.00 ", $- , $- +Sir Rodney's Marmalade,HUNGC, $- , $- ," $1,701.00 ", $- +Sir Rodney's Marmalade,LEHMS, $- , $- ," $1,360.80 ", $- +Sir Rodney's Marmalade,SEVES, $- ," $1,093.50 ", $- , $- +Sir Rodney's Scones,BLAUS, $- , $- , $80.00 , $- +Sir Rodney's Scones,BSBEV, $112.00 , $150.00 , $- , $- +Sir Rodney's Scones,CHOPS, $- , $- , $- , $380.00 +Sir Rodney's Scones,DUMON, $- , $- , $60.00 , $- +Sir Rodney's Scones,ERNSH, $400.00 , $- , $- , $- +Sir Rodney's Scones,FOLIG, $- , $- , $- , $400.00 +Sir Rodney's Scones,FRANK, $- , $- , $225.00 , $304.00 +Sir Rodney's Scones,GODOS, $- , $54.00 , $- , $- +Sir Rodney's Scones,GREAL, $- , $- , $108.00 , $- +Sir Rodney's Scones,KOENE, $272.00 , $- , $- , $- +Sir Rodney's Scones,LILAS, $240.00 , $- , $- , $- +Sir Rodney's Scones,LINOD, $- , $- , $- , $300.00 +Sir Rodney's Scones,MEREP, $- , $- , $420.00 , $- +Sir Rodney's Scones,OCEAN, $96.00 , $- , $- , $- +Sir Rodney's Scones,PRINI, $126.00 , $- , $- , $- +Sir Rodney's Scones,QUEEN, $216.00 , $- , $- , $- +Sir Rodney's Scones,QUICK, $- , $- , $600.00 , $- +Sir Rodney's Scones,RANCH, $- , $- , $- , $50.00 +Sir Rodney's Scones,SIMOB, $- , $- , $240.00 , $- +Sir Rodney's Scones,WANDK, $- , $320.00 , $- , $- +Sir Rodney's Scones,WHITC, $- , $120.00 , $- , $- +Steeleye Stout,BERGS, $115.20 , $- , $- , $- +Steeleye Stout,BSBEV, $- , $360.00 , $- , $- +Steeleye Stout,CACTU, $- , $54.00 , $- , $- +Steeleye Stout,EASTC, $504.00 , $- , $- , $- +Steeleye Stout,ERNSH, $- , $- , $405.00 , $- +Steeleye Stout,FOLIG, $- , $- , $- , $270.00 +Steeleye Stout,FRANK, $- , $- , $486.00 , $- +Steeleye Stout,FURIB, $- , $306.00 , $- , $- +Steeleye Stout,GREAL, $- , $- , $72.00 , $- +Steeleye Stout,LINOD, $- , $- , $- , $121.50 +Steeleye Stout,MEREP, $691.20 , $- , $- , $- +Steeleye Stout,QUEDE, $- , $- , $360.00 , $378.00 +Steeleye Stout,VICTE, $- , $540.00 , $- , $- +Steeleye Stout,WARTH, $- , $108.00 , $- , $- +Steeleye Stout,WHITC, $- , $- , $- , $504.00 +Teatime Chocolate Biscuits,FAMIA, $124.83 , $- , $- , $- +Teatime Chocolate Biscuits,FRANK, $- , $- , $124.20 , $- +Teatime Chocolate Biscuits,FRANS, $- , $- , $- , $46.00 +Teatime Chocolate Biscuits,GODOS, $- , $92.00 , $- , $- +Teatime Chocolate Biscuits,GREAL, $- , $- , $248.40 , $- +Teatime Chocolate Biscuits,ISLAT, $- , $- , $46.00 , $- +Teatime Chocolate Biscuits,LINOD, $- , $- , $- , $48.30 +Teatime Chocolate Biscuits,QUEDE, $24.82 , $- , $276.00 , $- +Teatime Chocolate Biscuits,QUEEN, $36.50 , $- , $- , $- +Teatime Chocolate Biscuits,QUICK, $- , $- , $- , $437.00 +Teatime Chocolate Biscuits,RICAR, $292.00 , $- , $- , $- +Teatime Chocolate Biscuits,SAVEA, $- , $257.60 , $- , $110.40 +Teatime Chocolate Biscuits,SUPRD, $153.30 , $- , $- , $- +Teatime Chocolate Biscuits,TOMSP, $166.44 , $- , $- , $- +Teatime Chocolate Biscuits,TORTU, $- , $- , $64.40 , $- +Teatime Chocolate Biscuits,WANDK, $- , $- , $82.80 , $- +Teatime Chocolate Biscuits,WARTH, $146.00 , $- , $- , $- +Teatime Chocolate Biscuits,WELLI, $- , $- , $- , $209.76 +Uncle Bob's Organic Dried Pears,BONAP, $- ," $1,275.00 ", $- , $- +Uncle Bob's Organic Dried Pears,BSBEV, $720.00 , $- , $- , $- +Uncle Bob's Organic Dried Pears,FOLIG, $- , $- ," $1,050.00 ", $- +Uncle Bob's Organic Dried Pears,GOURL, $- , $- , $- , $76.50 +Uncle Bob's Organic Dried Pears,OTTIK, $- , $- , $- ," $1,050.00 " +Uncle Bob's Organic Dried Pears,QUICK, $- , $- , $- ," $2,700.00 " +Uncle Bob's Organic Dried Pears,SAVEA, $- , $- ," $1,350.00 ", $- +Uncle Bob's Organic Dried Pears,VAFFE, $- , $- , $300.00 , $- +Uncle Bob's Organic Dried Pears,VICTE, $364.80 , $300.00 , $- , $- +Veggie-spread,ALFKI, $- , $- , $- , $878.00 +Veggie-spread,ERNSH," $2,281.50 ", $- , $- , $- +Veggie-spread,FOLIG, $- , $- , $- ," $1,317.00 " +Veggie-spread,HUNGO, $921.37 , $- , $- , $- +Veggie-spread,MORGK, $- , $263.40 , $- , $- +Veggie-spread,PICCO, $- , $- , $- , $395.10 +Veggie-spread,WHITC, $- , $- , $842.88 , $- diff --git a/src/test/datascience/activation.unit.test.ts b/src/test/datascience/activation.unit.test.ts new file mode 100644 index 000000000000..afc58dc74840 --- /dev/null +++ b/src/test/datascience/activation.unit.test.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter } from 'vscode'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; +import { IPythonExecutionFactory } from '../../client/common/process/types'; +import { sleep } from '../../client/common/utils/async'; +import { Activation } from '../../client/datascience/activation'; +import { JupyterDaemonModule } from '../../client/datascience/constants'; +import { ActiveEditorContextService } from '../../client/datascience/context/activeEditorContext'; +import { NativeEditor } from '../../client/datascience/interactive-ipynb/nativeEditor'; +import { JupyterInterpreterService } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterService'; +import { KernelDaemonPreWarmer } from '../../client/datascience/kernel-launcher/kernelDaemonPreWarmer'; +import { NativeEditorProvider } from '../../client/datascience/notebookStorage/nativeEditorProvider'; +import { + INotebookAndInteractiveWindowUsageTracker, + INotebookEditor, + INotebookEditorProvider +} from '../../client/datascience/types'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { FakeClock } from '../common'; +import { createPythonInterpreter } from '../utils/interpreters'; + +suite('DataScience - Activation', () => { + let activator: IExtensionSingleActivationService; + let notebookEditorProvider: INotebookEditorProvider; + let jupyterInterpreterService: JupyterInterpreterService; + let executionFactory: IPythonExecutionFactory; + let openedEventEmitter: EventEmitter<INotebookEditor>; + let interpreterEventEmitter: EventEmitter<PythonEnvironment>; + let contextService: ActiveEditorContextService; + let fakeTimer: FakeClock; + const interpreter = createPythonInterpreter(); + + setup(async () => { + fakeTimer = new FakeClock(); + openedEventEmitter = new EventEmitter<INotebookEditor>(); + interpreterEventEmitter = new EventEmitter<PythonEnvironment>(); + const tracker = mock<INotebookAndInteractiveWindowUsageTracker>(); + + notebookEditorProvider = mock(NativeEditorProvider); + jupyterInterpreterService = mock(JupyterInterpreterService); + executionFactory = mock(PythonExecutionFactory); + contextService = mock(ActiveEditorContextService); + const daemonPool = mock(KernelDaemonPreWarmer); + when(notebookEditorProvider.onDidOpenNotebookEditor).thenReturn(openedEventEmitter.event); + when(jupyterInterpreterService.onDidChangeInterpreter).thenReturn(interpreterEventEmitter.event); + when(executionFactory.createDaemon(anything())).thenResolve(); + when(contextService.activate()).thenResolve(); + when(daemonPool.activate(anything())).thenResolve(); + activator = new Activation( + instance(notebookEditorProvider), + instance(jupyterInterpreterService), + instance(executionFactory), + [], + instance(contextService), + instance(daemonPool), + instance(tracker) + ); + when(jupyterInterpreterService.getSelectedInterpreter()).thenResolve(interpreter); + when(jupyterInterpreterService.getSelectedInterpreter(anything())).thenResolve(interpreter); + when(jupyterInterpreterService.setInitialInterpreter()).thenResolve(interpreter); + await activator.activate(); + }); + teardown(() => fakeTimer.uninstall()); + async function testCreatingDaemonWhenOpeningANotebook() { + fakeTimer.install(); + const notebook: INotebookEditor = mock(NativeEditor); + + // Open a notebook, (fire the event). + openedEventEmitter.fire(notebook); + + // Wait for debounce to complete. + await fakeTimer.wait(); + + verify(executionFactory.createDaemon(anything())).once(); + verify( + executionFactory.createDaemon( + deepEqual({ daemonModule: JupyterDaemonModule, pythonPath: interpreter.path }) + ) + ).once(); + } + + test('Create a daemon when a notebook is opened', async () => testCreatingDaemonWhenOpeningANotebook()); + + test('Create a daemon when changing interpreter after a notebook has beeen opened', async () => { + await testCreatingDaemonWhenOpeningANotebook(); + + // Trigger changes to interpreter. + interpreterEventEmitter.fire(interpreter); + + // Wait for debounce to complete. + await fakeTimer.wait(); + + verify( + executionFactory.createDaemon( + deepEqual({ daemonModule: JupyterDaemonModule, pythonPath: interpreter.path }) + ) + ).twice(); + }); + test('Changing interpreter without opening a notebook does not result in a daemon being created', async () => { + // Trigger changes to interpreter. + interpreterEventEmitter.fire(interpreter); + + // Assume a debounce is required and wait. + await sleep(10); + + verify(executionFactory.createDaemon(anything())).never(); + }); +}); diff --git a/src/test/datascience/cellFactory.unit.test.ts b/src/test/datascience/cellFactory.unit.test.ts new file mode 100644 index 000000000000..46dff8b23e26 --- /dev/null +++ b/src/test/datascience/cellFactory.unit.test.ts @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert } from 'chai'; +import { generateCells } from '../../client/datascience/cellFactory'; +import { removeLinesFromFrontAndBack, stripComments } from '../../datascience-ui/common'; + +// tslint:disable: max-func-body-length +suite('DataScience CellFactory', () => { + test('parsing cells', () => { + let cells = generateCells(undefined, '#%%\na=1\na', 'foo', 0, true, '1'); + assert.equal(cells.length, 1, 'Simple cell, not right number found'); + cells = generateCells(undefined, '#%% [markdown]\na=1\na', 'foo', 0, true, '1'); + assert.equal(cells.length, 2, 'Split cell, not right number found'); + cells = generateCells(undefined, '#%% [markdown]\n# #a=1\n#a', 'foo', 0, true, '1'); + assert.equal(cells.length, 1, 'Markdown split wrong'); + assert.equal(cells[0].data.cell_type, 'markdown', 'Markdown cell not generated'); + cells = generateCells(undefined, "#%% [markdown]\n'''\n# a\nb\n'''", 'foo', 0, true, '1'); + assert.equal(cells.length, 1, 'Markdown cell multline failed'); + assert.equal(cells[0].data.cell_type, 'markdown', 'Markdown cell not generated'); + assert.equal(cells[0].data.source.length, 2, 'Lines for markdown not emitted'); + cells = generateCells(undefined, '#%% [markdown]\n"""\n# a\nb\n"""', 'foo', 0, true, '1'); + assert.equal(cells.length, 1, 'Markdown cell multline failed'); + assert.equal(cells[0].data.cell_type, 'markdown', 'Markdown cell not generated'); + assert.equal(cells[0].data.source.length, 2, 'Lines for markdown not emitted'); + cells = generateCells(undefined, '#%% \n"""\n# a\nb\n"""', 'foo', 0, true, '1'); + assert.equal(cells.length, 1, 'Code cell multline failed'); + assert.equal(cells[0].data.cell_type, 'code', 'Code cell not generated'); + assert.equal(cells[0].data.source.length, 5, 'Lines for cell not emitted'); + cells = generateCells(undefined, '#%% [markdown] \n"""# a\nb\n"""', 'foo', 0, true, '1'); + assert.equal(cells.length, 1, 'Markdown cell multline failed'); + assert.equal(cells[0].data.cell_type, 'markdown', 'Markdown cell not generated'); + assert.equal(cells[0].data.source.length, 2, 'Lines for cell not emitted'); + + // tslint:disable-next-line: no-multiline-string + const multilineCode = `#%% +myvar = """ # Lorem Ipsum +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Nullam eget varius ligula, eget fermentum mauris. +Cras ultrices, enim sit amet iaculis ornare, nisl nibh aliquet elit, sed ultrices velit ipsum dignissim nisl. +Nunc quis orci ante. Vivamus vel blandit velit. +Sed mattis dui diam, et blandit augue mattis vestibulum. +Suspendisse ornare interdum velit. Suspendisse potenti. +Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. +"""`; + // tslint:disable-next-line: no-multiline-string + const multilineTwo = `#%% +""" # Lorem Ipsum +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Nullam eget varius ligula, eget fermentum mauris. +Cras ultrices, enim sit amet iaculis ornare, nisl nibh aliquet elit, sed ultrices velit ipsum dignissim nisl. +Nunc quis orci ante. Vivamus vel blandit velit. +Sed mattis dui diam, et blandit augue mattis vestibulum. +Suspendisse ornare interdum velit. Suspendisse potenti. +Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. +""" print('bob')`; + + cells = generateCells(undefined, multilineCode, 'foo', 0, true, '1'); + assert.equal(cells.length, 1, 'code cell multline failed'); + assert.equal(cells[0].data.cell_type, 'code', 'Code cell not generated'); + assert.equal(cells[0].data.source.length, 10, 'Lines for cell not emitted'); + cells = generateCells(undefined, multilineTwo, 'foo', 0, true, '1'); + assert.equal(cells.length, 1, 'code cell multline failed'); + assert.equal(cells[0].data.cell_type, 'code', 'Code cell not generated'); + assert.equal(cells[0].data.source.length, 10, 'Lines for cell not emitted'); + // tslint:disable-next-line: no-multiline-string + assert.equal(cells[0].data.source[9], `""" print('bob')`, 'Lines for cell not emitted'); + // tslint:disable-next-line: no-multiline-string + const multilineMarkdown = `#%% [markdown] +# ## Block of Interest +# +# ### Take a look +# +# +# 1. Item 1 +# +# - Item 1-a +# 1. Item 1-a-1 +# - Item 1-a-1-a +# - Item 1-a-1-b +# 2. Item 1-a-2 +# - Item 1-a-2-a +# - Item 1-a-2-b +# 3. Item 1-a-3 +# - Item 1-a-3-a +# - Item 1-a-3-b +# - Item 1-a-3-c +# +# 2. Item 2`; + cells = generateCells(undefined, multilineMarkdown, 'foo', 0, true, '1'); + assert.equal(cells.length, 1, 'markdown cell multline failed'); + assert.equal(cells[0].data.cell_type, 'markdown', 'markdown cell not generated'); + assert.equal(cells[0].data.source.length, 20, 'Lines for cell not emitted'); + assert.equal(cells[0].data.source[17], ' - Item 1-a-3-c\n', 'Lines for markdown not emitted'); + + // tslint:disable-next-line: no-multiline-string + const multilineQuoteWithOtherDelimiter = `#%% [markdown] +''' +### Take a look + 2. Item 2 +""" Not a comment delimiter +''' +`; + cells = generateCells(undefined, multilineQuoteWithOtherDelimiter, 'foo', 0, true, '1'); + assert.equal(cells.length, 1, 'markdown cell multline failed'); + assert.equal(cells[0].data.cell_type, 'markdown', 'markdown cell not generated'); + assert.equal(cells[0].data.source.length, 3, 'Lines for cell not emitted'); + assert.equal(cells[0].data.source[2], '""" Not a comment delimiter', 'Lines for markdown not emitted'); + + // tslint:disable-next-line: no-multiline-string + const multilineQuoteInFunc = `#%% +import requests +def download(url, filename): + """ utility function to download a file """ + response = requests.get(url, stream=True) + with open(filename, "wb") as handle: + for data in response.iter_content(): + handle.write(data) +`; + cells = generateCells(undefined, multilineQuoteInFunc, 'foo', 0, true, '1'); + assert.equal(cells.length, 1, 'cell multline failed'); + assert.equal(cells[0].data.cell_type, 'code', 'code cell not generated'); + assert.equal(cells[0].data.source.length, 9, 'Lines for cell not emitted'); + assert.equal( + cells[0].data.source[3], + ' """ utility function to download a file """\n', + 'Lines for cell not emitted' + ); + + // tslint:disable-next-line: no-multiline-string + const multilineMarkdownWithCell = `#%% [markdown] +# # Define a simple class +class Pizza(object): + def __init__(self, size, toppings, price, rating): + self.size = size + self.toppings = toppings + self.price = price + self.rating = rating + `; + + cells = generateCells(undefined, multilineMarkdownWithCell, 'foo', 0, true, '1'); + assert.equal(cells.length, 2, 'cell split failed'); + assert.equal(cells[0].data.cell_type, 'markdown', 'markdown cell not generated'); + assert.equal(cells[0].data.source.length, 1, 'Lines for markdown not emitted'); + assert.equal(cells[1].data.cell_type, 'code', 'code cell not generated'); + assert.equal(cells[1].data.source.length, 7, 'Lines for code not emitted'); + assert.equal(cells[1].data.source[3], ' self.toppings = toppings\n', 'Lines for cell not emitted'); + + // Non comments tests + let nonComments = stripComments(multilineCode); + assert.ok(nonComments.startsWith('myvar = """ # Lorem Ipsum'), 'Variable set to multiline string not working'); + nonComments = stripComments(multilineTwo); + assert.equal(nonComments, '', 'Multline comment is not being stripped'); + nonComments = stripComments(multilineQuoteInFunc); + assert.equal(nonComments.splitLines().length, 6, 'Splitting quote in func wrong number of lines'); + }); + + test('Line removal', () => { + const entry1 = `# %% CELL + +first line`; + const expected1 = `# %% CELL +first line`; + const entry2 = `# %% CELL + +first line + +`; + const expected2 = `# %% CELL +first line`; + const entry3 = `# %% CELL + +first line + +second line + +`; + const expected3 = `# %% CELL +first line + +second line`; + + const entry4 = ` + +if (foo): + print('stuff') + +print('some more') + +`; + const expected4 = `if (foo): + print('stuff') + +print('some more')`; + let removed = removeLinesFromFrontAndBack(entry1); + assert.equal(removed, expected1); + removed = removeLinesFromFrontAndBack(entry2); + assert.equal(removed, expected2); + removed = removeLinesFromFrontAndBack(entry3); + assert.equal(removed, expected3); + removed = removeLinesFromFrontAndBack(entry4); + assert.equal(removed, expected4); + }); +}); diff --git a/src/test/datascience/cellMatcher.unit.test.ts b/src/test/datascience/cellMatcher.unit.test.ts new file mode 100644 index 000000000000..8bdf6855314f --- /dev/null +++ b/src/test/datascience/cellMatcher.unit.test.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert } from 'chai'; +import { IDataScienceSettings } from '../../client/common/types'; +import { CellMatcher } from '../../client/datascience/cellMatcher'; +import { defaultDataScienceSettings } from './helpers'; + +suite('DataScience CellMatcher', () => { + test('CellMatcher', () => { + const settings: IDataScienceSettings = defaultDataScienceSettings(); + const matcher1 = new CellMatcher(settings); + assert.ok(matcher1.isCode('# %%'), 'Base code is wrong'); + assert.ok(matcher1.isMarkdown('# %% [markdown]'), 'Base markdown is wrong'); + assert.equal(matcher1.exec('# %% TITLE'), 'TITLE', 'Title not found'); + + settings.defaultCellMarker = '# %% CODE HERE'; + const matcher2 = new CellMatcher(settings); + assert.ok(matcher2.isCode('# %%'), 'Code not found'); + assert.ok(matcher2.isCode('# %% CODE HERE'), 'Code not found'); + assert.ok(matcher2.isCode('# %% CODE HERE TOO'), 'Code not found'); + assert.ok(matcher2.isMarkdown('# %% [markdown]'), 'Base markdown is wrong'); + assert.equal(matcher2.exec('# %% CODE HERE'), '', 'Should not have a title'); + assert.equal(matcher2.exec('# %% CODE HERE FOO'), 'FOO', 'Should have a title'); + }); +}); diff --git a/src/test/datascience/color.test.ts b/src/test/datascience/color.test.ts new file mode 100644 index 000000000000..95c12c218d9f --- /dev/null +++ b/src/test/datascience/color.test.ts @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { WorkspaceConfiguration } from 'vscode'; + +import { Extensions } from '../../client/common/application/extensions'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { PythonSettings } from '../../client/common/configSettings'; +import { CurrentProcess } from '../../client/common/process/currentProcess'; +import { IConfigurationService } from '../../client/common/types'; +import { CodeCssGenerator } from '../../client/datascience/codeCssGenerator'; +import { DataScienceFileSystem } from '../../client/datascience/dataScienceFileSystem'; +import { ThemeFinder } from '../../client/datascience/themeFinder'; +import { IThemeFinder } from '../../client/datascience/types'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; + +// tslint:disable:max-func-body-length +suite('Theme colors', () => { + let themeFinder: ThemeFinder; + let extensions: Extensions; + let currentProcess: CurrentProcess; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let workspaceConfig: TypeMoq.IMock<WorkspaceConfiguration>; + let cssGenerator: CodeCssGenerator; + let configService: TypeMoq.IMock<IConfigurationService>; + const settings: PythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); + + setup(() => { + extensions = new Extensions(); + currentProcess = new CurrentProcess(); + const fs = new DataScienceFileSystem(); + themeFinder = new ThemeFinder(extensions, currentProcess, fs); + + workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceConfig + .setup((ws) => ws.has(TypeMoq.It.isAnyString())) + .returns(() => { + return false; + }); + workspaceConfig + .setup((ws) => ws.get(TypeMoq.It.isAnyString())) + .returns(() => { + return undefined; + }); + workspaceConfig + .setup((ws) => ws.get(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) + .returns((_s, d) => { + return d; + }); + + settings.datascience = { + allowImportFromNotebook: true, + alwaysTrustNotebooks: true, + jupyterLaunchTimeout: 20000, + jupyterLaunchRetries: 3, + enabled: true, + jupyterServerURI: 'local', + notebookFileRoot: 'WORKSPACE', + changeDirOnImportExport: true, + useDefaultConfigForJupyter: true, + jupyterInterruptTimeout: 10000, + searchForJupyter: true, + showCellInputCode: true, + collapseCellInputCodeByDefault: true, + allowInput: true, + maxOutputSize: 400, + enableScrollingForCellOutputs: true, + errorBackgroundColor: '#FFFFFF', + sendSelectionToInteractiveWindow: false, + variableExplorerExclude: 'module;function;builtin_function_or_method', + codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', + markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', + enablePlotViewer: true, + runStartupCommands: '', + debugJustMyCode: true, + variableQueries: [], + jupyterCommandLineArguments: [], + widgetScriptSources: [], + interactiveWindowMode: 'single' + }; + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings); + + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspaceService.setup((c) => c.getConfiguration(TypeMoq.It.isAny())).returns(() => workspaceConfig.object); + workspaceService + .setup((c) => c.getConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => workspaceConfig.object); + + cssGenerator = new CodeCssGenerator(workspaceService.object, themeFinder, configService.object, fs); + }); + + function runTest(themeName: string, isDark: boolean, shouldExist: boolean) { + test(themeName, async () => { + const json = await themeFinder.findThemeRootJson(themeName); + if (shouldExist) { + assert.ok(json, `Cannot find theme ${themeName}`); + const actuallyDark = await themeFinder.isThemeDark(themeName); + assert.equal(actuallyDark, isDark, `Theme ${themeName} darkness is not ${isDark}`); + workspaceConfig.reset(); + workspaceConfig + .setup((ws) => ws.get<string>(TypeMoq.It.isValue('colorTheme'))) + .returns(() => { + return themeName; + }); + workspaceConfig + .setup((ws) => ws.get<string>(TypeMoq.It.isValue('fontFamily'))) + .returns(() => { + return 'Arial'; + }); + workspaceConfig + .setup((ws) => ws.get<number>(TypeMoq.It.isValue('fontSize'))) + .returns(() => { + return 16; + }); + workspaceConfig + .setup((ws) => ws.get(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) + .returns((_s, d) => { + return d; + }); + const theme = await cssGenerator.generateMonacoTheme(undefined, isDark, themeName); + assert.ok(theme, `Cannot find monaco theme for ${themeName}`); + const colors = await cssGenerator.generateThemeCss(undefined, isDark, themeName); + assert.ok(colors, 'Cannot find theme colors for Kimbie Dark'); + + // Make sure we have a string value that is not set to a variable + // (that would be the default and all themes have a string color) + assert.ok(theme.rules, 'No rules found in monaco theme'); + // tslint:disable-next-line: no-any + const commentPunctuation = (theme.rules as any[]).findIndex( + (r) => r.token === 'punctuation.definition.comment' + ); + assert.ok(commentPunctuation >= 0, 'No punctuation.comment found'); + } else { + assert.notOk(json, `Found ${themeName} when not expected`); + } + }); + } + + // One test per known theme + runTest('Light (Visual Studio)', false, true); + runTest('Light+ (default light)', false, true); + runTest('Quiet Light', false, true); + runTest('Solarized Light', false, true); + runTest('Abyss', true, true); + runTest('Dark (Visual Studio)', true, true); + runTest('Dark+ (default dark)', true, true); + runTest('Kimbie Dark', true, true); + runTest('Monokai', true, true); + runTest('Monokai Dimmed', true, true); + runTest('Red', true, true); + runTest('Solarized Dark', true, true); + runTest('Tomorrow Night Blue', true, true); + + // One test to make sure unknown themes don't return a value. + runTest('Knight Rider', true, false); + + // Test for when theme's json can't be found. + test('Missing json theme', async () => { + const mockThemeFinder = TypeMoq.Mock.ofType<IThemeFinder>(); + mockThemeFinder.setup((m) => m.isThemeDark(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(false)); + mockThemeFinder + .setup((m) => m.findThemeRootJson(TypeMoq.It.isAnyString())) + .returns(() => Promise.resolve(undefined)); + + const fs = new DataScienceFileSystem(); + cssGenerator = new CodeCssGenerator(workspaceService.object, mockThemeFinder.object, configService.object, fs); + + const colors = await cssGenerator.generateThemeCss(undefined, false, 'Kimbie Dark'); + assert.ok(colors, 'Cannot find theme colors for Kimbie Dark'); + + // Make sure we have a string value that is not set to a variable + // (that would be the default and all themes have a string color) + const matches = /--code-string-color\:\s(.*?);/gm.exec(colors); + assert.ok(matches, 'No matches found for string color'); + assert.equal(matches!.length, 2, 'Wrong number of matches for for string color'); + assert.ok(matches![1].includes('#'), 'String color not found'); + }); +}); diff --git a/src/test/datascience/commands/commandRegistry.unit.test.ts b/src/test/datascience/commands/commandRegistry.unit.test.ts new file mode 100644 index 000000000000..c6ee1f605ed0 --- /dev/null +++ b/src/test/datascience/commands/commandRegistry.unit.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { anything, instance, mock, verify } from 'ts-mockito'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { DebugService } from '../../../client/common/application/debugService'; +import { DocumentManager } from '../../../client/common/application/documentManager'; +import { ICommandManager } from '../../../client/common/application/types'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { StartPage } from '../../../client/common/startPage/startPage'; +import { JupyterCommandLineSelectorCommand } from '../../../client/datascience/commands/commandLineSelector'; +import { CommandRegistry } from '../../../client/datascience/commands/commandRegistry'; +import { ExportCommands } from '../../../client/datascience/commands/exportCommands'; +import { NotebookCommands } from '../../../client/datascience/commands/notebookCommands'; +import { JupyterServerSelectorCommand } from '../../../client/datascience/commands/serverSelector'; +import { Commands } from '../../../client/datascience/constants'; +import { DataScienceFileSystem } from '../../../client/datascience/dataScienceFileSystem'; +import { DataScienceCodeLensProvider } from '../../../client/datascience/editor-integration/codelensprovider'; +import { NativeEditorProvider } from '../../../client/datascience/notebookStorage/nativeEditorProvider'; +import { MockOutputChannel } from '../../mockClasses'; + +// tslint:disable: max-func-body-length +suite('DataScience - Commands', () => { + let kernelSwitcherCommand: NotebookCommands; + let serverSelectorCommand: JupyterServerSelectorCommand; + let commandLineCommand: JupyterCommandLineSelectorCommand; + let commandRegistry: CommandRegistry; + let commandManager: ICommandManager; + setup(() => { + kernelSwitcherCommand = mock(NotebookCommands); + serverSelectorCommand = mock(JupyterServerSelectorCommand); + commandLineCommand = mock(JupyterCommandLineSelectorCommand); + + const codeLensProvider = mock(DataScienceCodeLensProvider); + const notebookEditorProvider = mock(NativeEditorProvider); + const debugService = mock(DebugService); + const documentManager = mock(DocumentManager); + commandManager = mock(CommandManager); + const configService = mock(ConfigurationService); + const appShell = mock(ApplicationShell); + const startPage = mock(StartPage); + const exportCommand = mock(ExportCommands); + const fileSystem = mock(DataScienceFileSystem); + + commandRegistry = new CommandRegistry( + documentManager, + instance(codeLensProvider), + [], + instance(commandManager), + instance(serverSelectorCommand), + instance(kernelSwitcherCommand), + instance(commandLineCommand), + instance(notebookEditorProvider), + instance(debugService), + instance(configService), + instance(appShell), + new MockOutputChannel('Jupyter'), + instance(startPage), + instance(exportCommand), + instance(fileSystem) + ); + }); + + suite('Register', () => { + setup(() => { + commandRegistry.register(); + }); + + test('Should register server Selector Command', () => { + verify(serverSelectorCommand.register()).once(); + }); + test('Should register server kernelSwitcher Command', () => { + verify(kernelSwitcherCommand.register()).once(); + }); + [ + Commands.RunAllCells, + Commands.RunCell, + Commands.RunCurrentCell, + Commands.RunCurrentCellAdvance, + Commands.ExecSelectionInInteractiveWindow, + Commands.RunAllCellsAbove, + Commands.RunCellAndAllBelow, + Commands.RunAllCellsAbovePalette, + Commands.RunCellAndAllBelowPalette, + Commands.RunToLine, + Commands.RunFromLine, + Commands.RunFileInInteractiveWindows, + Commands.DebugFileInInteractiveWindows, + Commands.AddCellBelow, + Commands.RunCurrentCellAndAddBelow, + Commands.DebugCell, + Commands.DebugStepOver, + Commands.DebugContinue, + Commands.DebugStop, + Commands.DebugCurrentCellPalette, + Commands.CreateNewNotebook, + Commands.ViewJupyterOutput + ].forEach((command) => { + test(`Should register Command ${command}`, () => { + // tslint:disable-next-line: no-any + verify(commandManager.registerCommand(command as any, anything(), commandRegistry)).once(); + }); + }); + }); +}); diff --git a/src/test/datascience/commands/notebookCommands.functional.test.ts b/src/test/datascience/commands/notebookCommands.functional.test.ts new file mode 100644 index 000000000000..493415132f7e --- /dev/null +++ b/src/test/datascience/commands/notebookCommands.functional.test.ts @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import type { Kernel } from '@jupyterlab/services/lib/kernel/kernel'; +import { assert } from 'chai'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter, Uri } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { ICommandManager } from '../../../client/common/application/types'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { Architecture } from '../../../client/common/utils/platform'; +import { NotebookCommands } from '../../../client/datascience/commands/notebookCommands'; +import { Commands } from '../../../client/datascience/constants'; +import { NotebookProvider } from '../../../client/datascience/interactive-common/notebookProvider'; +import { InteractiveWindowProvider } from '../../../client/datascience/interactive-window/interactiveWindowProvider'; +import { JupyterNotebookBase } from '../../../client/datascience/jupyter/jupyterNotebook'; +import { JupyterSessionManagerFactory } from '../../../client/datascience/jupyter/jupyterSessionManagerFactory'; +import { KernelDependencyService } from '../../../client/datascience/jupyter/kernels/kernelDependencyService'; +import { KernelSelectionProvider } from '../../../client/datascience/jupyter/kernels/kernelSelections'; +import { KernelSelector } from '../../../client/datascience/jupyter/kernels/kernelSelector'; +import { KernelService } from '../../../client/datascience/jupyter/kernels/kernelService'; +import { KernelSwitcher } from '../../../client/datascience/jupyter/kernels/kernelSwitcher'; +import { + IKernelSpecQuickPickItem, + KernelSpecConnectionMetadata, + LiveKernelConnectionMetadata, + PythonKernelConnectionMetadata +} from '../../../client/datascience/jupyter/kernels/types'; +import { IKernelFinder } from '../../../client/datascience/kernel-launcher/types'; +import { NativeEditorProvider } from '../../../client/datascience/notebookStorage/nativeEditorProvider'; +import { IInteractiveWindowProvider, INotebookEditorProvider } from '../../../client/datascience/types'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { EnvironmentType } from '../../../client/pythonEnvironments/info'; + +// tslint:disable: max-func-body-length no-any +suite('DataScience - Notebook Commands', () => { + let notebookCommands: NotebookCommands; + let commandManager: ICommandManager; + let interactiveWindowProvider: IInteractiveWindowProvider; + let notebookEditorProvider: INotebookEditorProvider; + let notebookProvider: NotebookProvider; + let kernelSelectionProvider: KernelSelectionProvider; + const remoteKernel = { + lastActivityTime: new Date(), + name: 'CurrentKernel', + numberOfConnections: 0, + id: '2232', + // tslint:disable-next-line: no-any + session: {} as any + }; + const localKernel = { + name: 'CurrentKernel', + language: 'python', + path: 'python', + display_name: 'CurrentKernel', + env: {}, + argv: [] + }; + const selectedInterpreter = { + path: '', + envType: EnvironmentType.Conda, + architecture: Architecture.Unknown, + sysPrefix: '', + sysVersion: '' + }; + const remoteSelections: IKernelSpecQuickPickItem<LiveKernelConnectionMetadata>[] = [ + { + label: 'foobar', + selection: { + kernelModel: remoteKernel, + interpreter: undefined, + kind: 'connectToLiveKernel' + } + } + ]; + const localSelections: IKernelSpecQuickPickItem<KernelSpecConnectionMetadata | PythonKernelConnectionMetadata>[] = [ + { + label: 'foobar', + selection: { + kernelSpec: localKernel, + kernelModel: undefined, + interpreter: undefined, + kind: 'startUsingKernelSpec' + } + }, + { + label: 'foobaz', + selection: { + kernelSpec: undefined, + interpreter: selectedInterpreter, + kind: 'startUsingPythonInterpreter' + } + } + ]; + + [true, false].forEach((isLocalConnection) => { + // tslint:disable-next-line: max-func-body-length + suite(isLocalConnection ? 'Local Connection' : 'Remote Connection', () => { + setup(() => { + interactiveWindowProvider = mock(InteractiveWindowProvider); + notebookEditorProvider = mock(NativeEditorProvider); + notebookProvider = mock(NotebookProvider); + commandManager = mock(CommandManager); + + const kernelDependencyService = mock(KernelDependencyService); + const kernelService = mock(KernelService); + kernelSelectionProvider = mock(KernelSelectionProvider); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + anything(), + anything(), + anything() + ) + ).thenResolve(localSelections); + when( + kernelSelectionProvider.getKernelSelectionsForRemoteSession(anything(), anything(), anything()) + ).thenResolve(remoteSelections); + const appShell = mock(ApplicationShell); + const dependencyService = mock(KernelDependencyService); + const interpreterService = mock(InterpreterService); + const kernelFinder = mock<IKernelFinder>(); + const jupyterSessionManagerFactory = mock(JupyterSessionManagerFactory); + const dummySessionEvent = new EventEmitter<Kernel.IKernelConnection>(); + when(jupyterSessionManagerFactory.onRestartSessionCreated).thenReturn(dummySessionEvent.event); + when(jupyterSessionManagerFactory.onRestartSessionUsed).thenReturn(dummySessionEvent.event); + when(appShell.showQuickPick(anything(), anything(), anything())).thenCall(() => { + return isLocalConnection ? localSelections[0] : remoteSelections[0]; + }); + when(appShell.withProgress(anything(), anything())).thenCall((_o, t) => { + return t(); + }); + when(notebookProvider.connect(anything())).thenResolve( + isLocalConnection ? ({ type: 'raw' } as any) : ({ type: 'jupyter' } as any) + ); + + const configService = mock(ConfigurationService); + // tslint:disable-next-line: no-http-string + const settings = { datascience: { jupyterServerURI: isLocalConnection ? 'local' : 'http://foobar' } }; + when(configService.getSettings(anything())).thenReturn(settings as any); + + const kernelSelector = new KernelSelector( + instance(kernelSelectionProvider), + instance(appShell), + instance(kernelService), + instance(interpreterService), + instance(dependencyService), + instance(kernelFinder), + instance(jupyterSessionManagerFactory), + instance(configService), + [] + ); + + const kernelSwitcher = new KernelSwitcher( + instance(configService), + instance(appShell), + instance(kernelDependencyService), + kernelSelector + ); + + notebookCommands = new NotebookCommands( + instance(commandManager), + instance(notebookEditorProvider), + instance(interactiveWindowProvider), + instance(notebookProvider), + kernelSelector, + kernelSwitcher + ); + }); + + function createNotebookMock() { + const obj = mock(JupyterNotebookBase); + when((obj as any).then).thenReturn(undefined); + return obj; + } + function verifyCallToSetKernelSpec(notebook: JupyterNotebookBase) { + verify(notebook.setKernelConnection(anything(), anything())).once(); + + const kernelConnection = capture(notebook.setKernelConnection).first()[0]; + if (isLocalConnection) { + assert.equal(kernelConnection.kind, 'startUsingKernelSpec'); + const kernelSpec = + kernelConnection.kind !== 'connectToLiveKernel' ? kernelConnection.kernelSpec : undefined; + assert.equal(kernelSpec?.name, localKernel.name); + } else { + assert.equal(kernelConnection.kind, 'connectToLiveKernel'); + const kernelModel = + kernelConnection.kind === 'connectToLiveKernel' ? kernelConnection.kernelModel : undefined; + assert.equal(kernelModel?.name, remoteKernel.name); + } + } + + test('Register Command', () => { + notebookCommands.register(); + + verify( + commandManager.registerCommand(Commands.SwitchJupyterKernel, anything(), notebookCommands) + ).once(); + }); + suite('Command Handler', () => { + // tslint:disable-next-line: no-any + let commandHandler: Function; + setup(() => { + notebookCommands.register(); + // tslint:disable-next-line: no-any + commandHandler = capture(commandManager.registerCommand as any).first()[1] as Function; + commandHandler = commandHandler.bind(notebookCommands); + }); + test('Should not switch if no identity', async () => { + await commandHandler.bind(notebookCommands)(); + verify( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + anything(), + anything(), + anything() + ) + ).never(); + }); + test('Should switch kernel using the provided notebookxxx', async () => { + const notebook = createNotebookMock(); + when((notebook as any).then).thenReturn(undefined); + const uri = Uri.file('test.ipynb'); + when(notebookProvider.getOrCreateNotebook(anything())).thenCall(async () => { + return instance(notebook); + }); + + await commandHandler.bind(notebookCommands)({ identity: uri }); + + verifyCallToSetKernelSpec(notebook); + }); + test('Should switch kernel using the active Native Editor', async () => { + const nativeEditor = createNotebookMock(); + const uri = Uri.file('test.ipynb'); + // tslint:disable-next-line: no-any + when(notebookEditorProvider.activeEditor).thenReturn({ + file: uri, + model: { metadata: undefined } + } as any); + when(notebookProvider.getOrCreateNotebook(anything())).thenResolve(instance(nativeEditor)); + + await commandHandler.bind(notebookCommands)(); + + verifyCallToSetKernelSpec(nativeEditor); + }); + test('Should switch kernel using the active Interactive Window', async () => { + const interactiveWindow = createNotebookMock(); + const uri = Uri.parse('history://foobar'); + // tslint:disable-next-line: no-any + when(interactiveWindowProvider.activeWindow).thenReturn({ + identity: uri + } as any); + when(notebookProvider.getOrCreateNotebook(anything())).thenResolve(instance(interactiveWindow)); + + await commandHandler.bind(notebookCommands)(); + + verifyCallToSetKernelSpec(interactiveWindow); + }); + test('Should switch kernel using the active Native editor even if an Interactive Window is available', async () => { + const uri1 = Uri.parse('history://foobar'); + const nativeEditor = createNotebookMock(); + const uri2 = Uri.parse('test.ipynb'); + when(notebookEditorProvider.activeEditor).thenReturn({ + file: uri2, + model: { metadata: undefined } + } as any); + when(interactiveWindowProvider.activeWindow).thenReturn({ + identity: uri1 + } as any); + when(notebookProvider.getOrCreateNotebook(anything())).thenCall(async (o) => { + if (o.identity === uri2) { + return instance(nativeEditor); + } + }); + + await commandHandler.bind(notebookCommands)(); + + verifyCallToSetKernelSpec(nativeEditor); + }); + test('With no notebook, should still fire change', async () => { + when(notebookProvider.getOrCreateNotebook(anything())).thenResolve(undefined); + const uri = Uri.parse('history://foobar'); + await commandHandler.bind(notebookCommands)({ identity: uri }); + verify(notebookProvider.firePotentialKernelChanged(anything(), anything())).once(); + }); + }); + }); + }); +}); diff --git a/src/test/datascience/commands/serverSelector.unit.test.ts b/src/test/datascience/commands/serverSelector.unit.test.ts new file mode 100644 index 000000000000..4cec791a08a4 --- /dev/null +++ b/src/test/datascience/commands/serverSelector.unit.test.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { anything, capture, instance, mock, verify } from 'ts-mockito'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { ICommandManager } from '../../../client/common/application/types'; +import { JupyterServerSelectorCommand } from '../../../client/datascience/commands/serverSelector'; +import { Commands } from '../../../client/datascience/constants'; +import { JupyterServerSelector } from '../../../client/datascience/jupyter/serverSelector'; + +// tslint:disable: max-func-body-length +suite('DataScience - Server Selector Command', () => { + let serverSelectorCommand: JupyterServerSelectorCommand; + let commandManager: ICommandManager; + let serverSelector: JupyterServerSelector; + + setup(() => { + commandManager = mock(CommandManager); + serverSelector = mock(JupyterServerSelector); + + serverSelectorCommand = new JupyterServerSelectorCommand(instance(commandManager), instance(serverSelector)); + }); + + test('Register Command', () => { + serverSelectorCommand.register(); + + verify(commandManager.registerCommand(Commands.SelectJupyterURI, anything(), instance(serverSelector))).once(); + }); + + test('Command Handler should invoke ServerSelector', () => { + serverSelectorCommand.register(); + // tslint:disable-next-line: no-any + const handler = (capture(commandManager.registerCommand as any).first()[1] as Function).bind( + serverSelectorCommand + ); + + handler(); + + verify(serverSelector.selectJupyterURI(true)).once(); + }); +}); diff --git a/src/test/datascience/common.unit.test.ts b/src/test/datascience/common.unit.test.ts new file mode 100644 index 000000000000..e9dff1eab730 --- /dev/null +++ b/src/test/datascience/common.unit.test.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert } from 'chai'; +import { formatStreamText } from '../../datascience-ui/common'; + +suite('DataScience Common Tests', () => { + test('formatting stream text', async () => { + assert.equal(formatStreamText('\rExecute\rExecute 1'), 'Execute 1'); + assert.equal(formatStreamText('\rExecute\r\nExecute 2'), 'Execute\nExecute 2'); + assert.equal(formatStreamText('\rExecute\rExecute\r\nExecute 3'), 'Execute\nExecute 3'); + assert.equal(formatStreamText('\rExecute\rExecute\nExecute 4'), 'Execute\nExecute 4'); + assert.equal(formatStreamText('\rExecute\r\r \r\rExecute\nExecute 5'), 'Execute\nExecute 5'); + assert.equal(formatStreamText('\rExecute\rExecute\nExecute 6\rExecute 7'), 'Execute\nExecute 7'); + assert.equal(formatStreamText('\rExecute\rExecute\nExecute 8\rExecute 9\r\r'), 'Execute\n'); + assert.equal(formatStreamText('\rExecute\rExecute\nExecute 10\rExecute 11\r\n'), 'Execute\nExecute 11\n'); + }); +}); diff --git a/src/test/datascience/crossProcessLock.unit.test.ts b/src/test/datascience/crossProcessLock.unit.test.ts new file mode 100644 index 000000000000..46c757c0f12f --- /dev/null +++ b/src/test/datascience/crossProcessLock.unit.test.ts @@ -0,0 +1,27 @@ +import { assert } from 'chai'; +import { sleep } from '../../client/common/utils/async'; +import { CrossProcessLock } from '../../client/datascience/crossProcessLock'; + +suite('Cross process lock', async () => { + let mutex1: CrossProcessLock; + let mutex2: CrossProcessLock; + + suiteSetup(() => { + // Create two named mutexes with the same name + mutex1 = new CrossProcessLock('crossProcessLockUnitTest'); + mutex2 = new CrossProcessLock('crossProcessLockUnitTest'); + }); + + suiteTeardown(async () => { + // Delete the lockfile so it's clean for the next run + // Note that mutex2 should not have been acquired so there's no need to unlock it + await mutex1.unlock(); + }); + + test('Lock guarantees in-process mutual exclusion', async () => { + const result1 = await mutex1.lock(); + assert.equal(result1, true); // Expect to successfully acquire the lock since it's not held + const result2 = await Promise.race([mutex2.lock(), sleep(1000)]); + assert.equal(result2, 1000); // Expect the sleep to resolve before the mutex is acquired + }).timeout(10000); +}); diff --git a/src/test/datascience/data-viewing/dataViewer.unit.test.ts b/src/test/datascience/data-viewing/dataViewer.unit.test.ts new file mode 100644 index 000000000000..05cc037ad1e0 --- /dev/null +++ b/src/test/datascience/data-viewing/dataViewer.unit.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { ConfigurationChangeEvent, EventEmitter } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { IApplicationShell, IWebviewPanelProvider, IWorkspaceService } from '../../../client/common/application/types'; +import { WebviewPanelProvider } from '../../../client/common/application/webviewPanels/webviewPanelProvider'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { IConfigurationService } from '../../../client/common/types'; +import { CodeCssGenerator } from '../../../client/datascience/codeCssGenerator'; +import { DataViewer } from '../../../client/datascience/data-viewing/dataViewer'; +import { JupyterVariableDataProvider } from '../../../client/datascience/data-viewing/jupyterVariableDataProvider'; +import { IDataViewer, IDataViewerDataProvider } from '../../../client/datascience/data-viewing/types'; +import { ThemeFinder } from '../../../client/datascience/themeFinder'; +import { ICodeCssGenerator, IThemeFinder } from '../../../client/datascience/types'; + +suite('DataScience - DataViewer', () => { + let dataViewer: IDataViewer; + let webPanelProvider: IWebviewPanelProvider; + let configService: IConfigurationService; + let codeCssGenerator: ICodeCssGenerator; + let themeFinder: IThemeFinder; + let workspaceService: IWorkspaceService; + let applicationShell: IApplicationShell; + let dataProvider: IDataViewerDataProvider; + const title: string = 'Data Viewer - Title'; + + setup(async () => { + webPanelProvider = mock(WebviewPanelProvider); + configService = mock(ConfigurationService); + codeCssGenerator = mock(CodeCssGenerator); + themeFinder = mock(ThemeFinder); + workspaceService = mock(WorkspaceService); + applicationShell = mock(ApplicationShell); + dataProvider = mock(JupyterVariableDataProvider); + const settings = mock(PythonSettings); + const settingsChangedEvent = new EventEmitter<void>(); + + when(settings.onDidChange).thenReturn(settingsChangedEvent.event); + when(configService.getSettings(anything())).thenReturn(instance(settings)); + + const configChangeEvent = new EventEmitter<ConfigurationChangeEvent>(); + when(workspaceService.onDidChangeConfiguration).thenReturn(configChangeEvent.event); + + dataViewer = new DataViewer( + instance(webPanelProvider), + instance(configService), + instance(codeCssGenerator), + instance(themeFinder), + instance(workspaceService), + instance(applicationShell), + false + ); + }); + test('Data viewer showData calls gets dataFrame info from data provider', async () => { + await dataViewer.showData(instance(dataProvider), title); + + verify(dataProvider.getDataFrameInfo()).once(); + }); + test('Data viewer calls data provider dispose', async () => { + await dataViewer.showData(instance(dataProvider), title); + dataViewer.dispose(); + + verify(dataProvider.dispose()).once(); + }); +}); diff --git a/src/test/datascience/data-viewing/dataViewerPDependencyService.unit.test.ts b/src/test/datascience/data-viewing/dataViewerPDependencyService.unit.test.ts new file mode 100644 index 000000000000..c2f1cdf6dc09 --- /dev/null +++ b/src/test/datascience/data-viewing/dataViewerPDependencyService.unit.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { ProductInstaller } from '../../../client/common/installer/productInstaller'; +import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; +import { IInstaller, Product } from '../../../client/common/types'; +import { Common, DataScience } from '../../../client/common/utils/localize'; +import { Architecture } from '../../../client/common/utils/platform'; +import { DataViewerDependencyService } from '../../../client/datascience/data-viewing/dataViewerDependencyService'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +suite('DataScience - DataViewerDependencyService', () => { + let dependencyService: DataViewerDependencyService; + let appShell: IApplicationShell; + let pythonExecFactory: IPythonExecutionFactory; + let installer: IInstaller; + let interpreter: PythonEnvironment; + let pythonExecService: IPythonExecutionService; + setup(async () => { + interpreter = { + architecture: Architecture.Unknown, + displayName: '', + path: path.join('users', 'python', 'bin', 'python.exe'), + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown, + version: new SemVer('3.3.3') + }; + pythonExecService = mock<IPythonExecutionService>(); + installer = mock(ProductInstaller); + appShell = mock(ApplicationShell); + pythonExecFactory = mock(PythonExecutionFactory); + dependencyService = new DataViewerDependencyService( + instance(appShell), + instance(installer), + instance(pythonExecFactory) + ); + + // tslint:disable-next-line: no-any + (instance(pythonExecService) as any).then = undefined; + // tslint:disable-next-line: no-any + (pythonExecService as any).then = undefined; + when(pythonExecFactory.createActivatedEnvironment(anything())).thenResolve(instance(pythonExecService)); + }); + test('All ok, if pandas is installed and version is > 1.20', async () => { + when( + pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) + ).thenResolve({ stdout: '0.30.0' }); + + await dependencyService.checkAndInstallMissingDependencies(interpreter); + }); + test('Throw exception if pandas is installed and version is = 0.20', async () => { + when( + pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) + ).thenResolve({ stdout: '0.20.0' }); + + const promise = dependencyService.checkAndInstallMissingDependencies(interpreter); + + await assert.isRejected(promise, DataScience.pandasTooOldForViewingFormat().format('0.20.')); + }); + test('Throw exception if pandas is installed and version is < 0.20', async () => { + when( + pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) + ).thenResolve({ stdout: '0.10.0' }); + + const promise = dependencyService.checkAndInstallMissingDependencies(interpreter); + + await assert.isRejected(promise, DataScience.pandasTooOldForViewingFormat().format('0.10.')); + }); + test('Prompt to install pandas and install pandas', async () => { + when( + pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) + ).thenReject(new Error('Not Found')); + // tslint:disable-next-line: no-any + when(appShell.showErrorMessage(anything(), anything())).thenResolve(Common.install() as any); + when(installer.install(Product.pandas, interpreter, anything())).thenResolve(); + + await dependencyService.checkAndInstallMissingDependencies(interpreter); + + verify(appShell.showErrorMessage(DataScience.pandasRequiredForViewing(), Common.install())).once(); + verify(installer.install(Product.pandas, interpreter, anything())).once(); + }); + test('Prompt to install pandas and throw error if user does not install pandas', async () => { + when( + pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) + ).thenReject(new Error('Not Found')); + // tslint:disable-next-line: no-any + when(appShell.showErrorMessage(anything(), anything())).thenResolve(); + + const promise = dependencyService.checkAndInstallMissingDependencies(interpreter); + + await assert.isRejected(promise, DataScience.pandasRequiredForViewing()); + verify(appShell.showErrorMessage(DataScience.pandasRequiredForViewing(), Common.install())).once(); + verify(installer.install(anything(), anything(), anything())).never(); + }); +}); diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts new file mode 100644 index 000000000000..ee37f5f2b8ea --- /dev/null +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -0,0 +1,1581 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +//tslint:disable:trailing-comma no-any +import * as child_process from 'child_process'; +import { ReactWrapper } from 'enzyme'; +import * as fs from 'fs-extra'; +import * as glob from 'glob'; +import { interfaces } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { anyString, anything, instance, mock, reset, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { + CancellationTokenSource, + ConfigurationChangeEvent, + Disposable, + Event, + EventEmitter, + FileSystemWatcher, + Memento, + Uri, + WindowState, + WorkspaceFolder, + WorkspaceFoldersChangeEvent +} from 'vscode'; +import * as vsls from 'vsls/vscode'; +import { KernelDaemonPool } from '../../client/datascience/kernel-launcher/kernelDaemonPool'; + +import { promisify } from 'util'; +import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; +import { LanguageServerDownloader } from '../../client/activation/common/downloader'; +import { JediExtensionActivator } from '../../client/activation/jedi'; +import { DotNetLanguageServerActivator } from '../../client/activation/languageServer/activator'; +import { LanguageServerCompatibilityService } from '../../client/activation/languageServer/languageServerCompatibilityService'; +import { LanguageServerExtension } from '../../client/activation/languageServer/languageServerExtension'; +import { DotNetLanguageServerFolderService } from '../../client/activation/languageServer/languageServerFolderService'; +import { DotNetLanguageServerPackageService } from '../../client/activation/languageServer/languageServerPackageService'; +import { DotNetLanguageServerManager } from '../../client/activation/languageServer/manager'; +import { NodeLanguageServerActivator } from '../../client/activation/node/activator'; +import { NodeLanguageServerManager } from '../../client/activation/node/manager'; +import { + IExtensionSingleActivationService, + ILanguageServerActivator, + ILanguageServerAnalysisOptions, + ILanguageServerCache, + ILanguageServerCompatibilityService, + ILanguageServerDownloader, + ILanguageServerExtension, + ILanguageServerFolderService, + ILanguageServerManager, + ILanguageServerPackageService, + ILanguageServerProxy, + LanguageServerType +} from '../../client/activation/types'; +import { + LSNotSupportedDiagnosticService, + LSNotSupportedDiagnosticServiceId +} from '../../client/application/diagnostics/checks/lsNotSupported'; +import { DiagnosticFilterService } from '../../client/application/diagnostics/filter'; +import { + DiagnosticCommandPromptHandlerService, + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../client/application/diagnostics/promptHandler'; +import { + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService +} from '../../client/application/diagnostics/types'; +import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { ClipboardService } from '../../client/common/application/clipboard'; +import { VSCodeNotebook } from '../../client/common/application/notebook'; +import { TerminalManager } from '../../client/common/application/terminalManager'; +import { + IApplicationEnvironment, + IApplicationShell, + IClipboard, + ICommandManager, + ICustomEditorService, + IDebugService, + IDocumentManager, + ILiveShareApi, + ILiveShareTestingApi, + ITerminalManager, + IVSCodeNotebook, + IWebviewPanelOptions, + IWebviewPanelProvider, + IWorkspaceService +} from '../../client/common/application/types'; +import { WebviewPanelProvider } from '../../client/common/application/webviewPanels/webviewPanelProvider'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; +import { PythonSettings } from '../../client/common/configSettings'; +import { + EXTENSION_ROOT_DIR, + UseCustomEditorApi, + UseProposedApi, + UseVSCodeNotebookEditorApi +} from '../../client/common/constants'; +import { CryptoUtils } from '../../client/common/crypto'; +import { DotNetCompatibilityService } from '../../client/common/dotnet/compatibilityService'; +import { IDotNetCompatibilityService } from '../../client/common/dotnet/types'; +import { LocalZMQKernel } from '../../client/common/experiments/groups'; +import { ExperimentsManager } from '../../client/common/experiments/manager'; +import { ExperimentService } from '../../client/common/experiments/service'; +import { InstallationChannelManager } from '../../client/common/installer/channelManager'; +import { ProductInstaller } from '../../client/common/installer/productInstaller'; +import { + CTagsProductPathService, + DataScienceProductPathService, + FormatterProductPathService, + LinterProductPathService, + RefactoringLibraryProductPathService, + TestFrameworkProductPathService +} from '../../client/common/installer/productPath'; +import { ProductService } from '../../client/common/installer/productService'; +import { IInstallationChannelManager, IProductPathService, IProductService } from '../../client/common/installer/types'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; +import { traceError, traceInfo } from '../../client/common/logger'; +import { BrowserService } from '../../client/common/net/browser'; +import { HttpClient } from '../../client/common/net/httpClient'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { PathUtils } from '../../client/common/platform/pathUtils'; +import { RegistryImplementation } from '../../client/common/platform/registry'; +import { IRegistry } from '../../client/common/platform/types'; +import { CurrentProcess } from '../../client/common/process/currentProcess'; +import { BufferDecoder } from '../../client/common/process/decoder'; +import { ProcessLogger } from '../../client/common/process/logger'; +import { ProcessServiceFactory } from '../../client/common/process/processFactory'; +import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; +import { + IBufferDecoder, + IProcessLogger, + IProcessServiceFactory, + IPythonExecutionFactory +} from '../../client/common/process/types'; +import { StartPage } from '../../client/common/startPage/startPage'; +import { IStartPage } from '../../client/common/startPage/types'; +import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { CondaActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; +import { PipEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; +import { PyEnvActivationCommandProvider } from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; +import { TerminalHelper } from '../../client/common/terminal/helper'; +import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalHelper, + TerminalActivationProviders +} from '../../client/common/terminal/types'; +import { + BANNER_NAME_PROPOSE_LS, + GLOBAL_MEMENTO, + IAsyncDisposableRegistry, + IBrowserService, + IConfigurationService, + ICryptoUtils, + ICurrentProcess, + IDataScienceSettings, + IDisposable, + IExperimentService, + IExperimentsManager, + IExtensionContext, + IExtensions, + IHttpClient, + IInstaller, + IInterpreterPathService, + IMemento, + IOutputChannel, + IPathUtils, + IPersistentStateFactory, + IPythonExtensionBanner, + IPythonSettings, + IsWindows, + ProductType, + Resource, + WORKSPACE_MEMENTO +} from '../../client/common/types'; +import { sleep } from '../../client/common/utils/async'; +import { noop } from '../../client/common/utils/misc'; +import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; +import { Architecture } from '../../client/common/utils/platform'; +import { EnvironmentVariablesService } from '../../client/common/variables/environment'; +import { EnvironmentVariablesProvider } from '../../client/common/variables/environmentVariablesProvider'; +import { IEnvironmentVariablesProvider, IEnvironmentVariablesService } from '../../client/common/variables/types'; +import { CodeCssGenerator } from '../../client/datascience/codeCssGenerator'; +import { JupyterCommandLineSelectorCommand } from '../../client/datascience/commands/commandLineSelector'; +import { CommandRegistry } from '../../client/datascience/commands/commandRegistry'; +import { ExportCommands } from '../../client/datascience/commands/exportCommands'; +import { NotebookCommands } from '../../client/datascience/commands/notebookCommands'; +import { JupyterServerSelectorCommand } from '../../client/datascience/commands/serverSelector'; +import { DataScienceStartupTime, Identifiers, JUPYTER_OUTPUT_CHANNEL } from '../../client/datascience/constants'; +import { ActiveEditorContextService } from '../../client/datascience/context/activeEditorContext'; +import { DataViewer } from '../../client/datascience/data-viewing/dataViewer'; +import { DataViewerDependencyService } from '../../client/datascience/data-viewing/dataViewerDependencyService'; +import { DataViewerFactory } from '../../client/datascience/data-viewing/dataViewerFactory'; +import { JupyterVariableDataProvider } from '../../client/datascience/data-viewing/jupyterVariableDataProvider'; +import { JupyterVariableDataProviderFactory } from '../../client/datascience/data-viewing/jupyterVariableDataProviderFactory'; +import { IDataViewer, IDataViewerFactory } from '../../client/datascience/data-viewing/types'; +import { DebugLocationTrackerFactory } from '../../client/datascience/debugLocationTrackerFactory'; +import { CellHashProvider } from '../../client/datascience/editor-integration/cellhashprovider'; +import { CodeLensFactory } from '../../client/datascience/editor-integration/codeLensFactory'; +import { DataScienceCodeLensProvider } from '../../client/datascience/editor-integration/codelensprovider'; +import { CodeWatcher } from '../../client/datascience/editor-integration/codewatcher'; +import { HoverProvider } from '../../client/datascience/editor-integration/hoverProvider'; +import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/errorHandler'; +import { ExportBase } from '../../client/datascience/export/exportBase'; +import { ExportDependencyChecker } from '../../client/datascience/export/exportDependencyChecker'; +import { ExportFileOpener } from '../../client/datascience/export/exportFileOpener'; +import { ExportManager } from '../../client/datascience/export/exportManager'; +import { ExportManagerFilePicker } from '../../client/datascience/export/exportManagerFilePicker'; +import { ExportToHTML } from '../../client/datascience/export/exportToHTML'; +import { ExportToPDF } from '../../client/datascience/export/exportToPDF'; +import { ExportToPython } from '../../client/datascience/export/exportToPython'; +import { ExportUtil } from '../../client/datascience/export/exportUtil'; +import { ExportFormat, IExport, IExportManager, IExportManagerFilePicker } from '../../client/datascience/export/types'; +import { GatherListener } from '../../client/datascience/gather/gatherListener'; +import { GatherLogger } from '../../client/datascience/gather/gatherLogger'; +import { IntellisenseProvider } from '../../client/datascience/interactive-common/intellisense/intellisenseProvider'; +import { NotebookProvider } from '../../client/datascience/interactive-common/notebookProvider'; +import { NotebookServerProvider } from '../../client/datascience/interactive-common/notebookServerProvider'; +import { AutoSaveService } from '../../client/datascience/interactive-ipynb/autoSaveService'; +import { DigestStorage } from '../../client/datascience/interactive-ipynb/digestStorage'; +import { NativeEditorCommandListener } from '../../client/datascience/interactive-ipynb/nativeEditorCommandListener'; +import { NativeEditorRunByLineListener } from '../../client/datascience/interactive-ipynb/nativeEditorRunByLineListener'; +import { NativeEditorSynchronizer } from '../../client/datascience/interactive-ipynb/nativeEditorSynchronizer'; +import { TrustService } from '../../client/datascience/interactive-ipynb/trustService'; +import { InteractiveWindowCommandListener } from '../../client/datascience/interactive-window/interactiveWindowCommandListener'; +import { IPyWidgetHandler } from '../../client/datascience/ipywidgets/ipywidgetHandler'; +import { IPyWidgetMessageDispatcherFactory } from '../../client/datascience/ipywidgets/ipyWidgetMessageDispatcherFactory'; +import { IPyWidgetScriptSource } from '../../client/datascience/ipywidgets/ipyWidgetScriptSource'; +import { JupyterCommandLineSelector } from '../../client/datascience/jupyter/commandLineSelector'; +import { DebuggerVariableRegistration } from '../../client/datascience/jupyter/debuggerVariableRegistration'; +import { DebuggerVariables } from '../../client/datascience/jupyter/debuggerVariables'; +import { JupyterCommandFactory } from '../../client/datascience/jupyter/interpreter/jupyterCommand'; +import { JupyterInterpreterDependencyService } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService'; +import { JupyterInterpreterOldCacheStateStore } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterOldCacheStateStore'; +import { JupyterInterpreterSelectionCommand } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand'; +import { JupyterInterpreterSelector } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterSelector'; +import { JupyterInterpreterService } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterService'; +import { JupyterInterpreterStateStore } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterStateStore'; +import { JupyterInterpreterSubCommandExecutionService } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService'; +import { JupyterDebugger } from '../../client/datascience/jupyter/jupyterDebugger'; +import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; +import { JupyterExporter } from '../../client/datascience/jupyter/jupyterExporter'; +import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter'; +import { JupyterNotebookProvider } from '../../client/datascience/jupyter/jupyterNotebookProvider'; +import { JupyterPasswordConnect } from '../../client/datascience/jupyter/jupyterPasswordConnect'; +import { JupyterServerWrapper } from '../../client/datascience/jupyter/jupyterServerWrapper'; +import { JupyterSessionManagerFactory } from '../../client/datascience/jupyter/jupyterSessionManagerFactory'; +import { JupyterVariables } from '../../client/datascience/jupyter/jupyterVariables'; +import { KernelDependencyService } from '../../client/datascience/jupyter/kernels/kernelDependencyService'; +import { KernelSelectionProvider } from '../../client/datascience/jupyter/kernels/kernelSelections'; +import { KernelSelector } from '../../client/datascience/jupyter/kernels/kernelSelector'; +import { KernelService } from '../../client/datascience/jupyter/kernels/kernelService'; +import { KernelSwitcher } from '../../client/datascience/jupyter/kernels/kernelSwitcher'; +import { KernelVariables } from '../../client/datascience/jupyter/kernelVariables'; +import { NotebookStarter } from '../../client/datascience/jupyter/notebookStarter'; +import { OldJupyterVariables } from '../../client/datascience/jupyter/oldJupyterVariables'; +import { ServerPreload } from '../../client/datascience/jupyter/serverPreload'; +import { JupyterServerSelector } from '../../client/datascience/jupyter/serverSelector'; +import { JupyterDebugService } from '../../client/datascience/jupyterDebugService'; +import { JupyterUriProviderRegistration } from '../../client/datascience/jupyterUriProviderRegistration'; +import { KernelDaemonPreWarmer } from '../../client/datascience/kernel-launcher/kernelDaemonPreWarmer'; +import { KernelFinder } from '../../client/datascience/kernel-launcher/kernelFinder'; +import { KernelLauncher } from '../../client/datascience/kernel-launcher/kernelLauncher'; +import { IKernelFinder, IKernelLauncher } from '../../client/datascience/kernel-launcher/types'; +import { NotebookAndInteractiveWindowUsageTracker } from '../../client/datascience/notebookAndInteractiveTracker'; +import { NotebookModelFactory } from '../../client/datascience/notebookStorage/factory'; +import { NativeEditorStorage } from '../../client/datascience/notebookStorage/nativeEditorStorage'; +import { + INotebookStorageProvider, + NotebookStorageProvider +} from '../../client/datascience/notebookStorage/notebookStorageProvider'; +import { INotebookModelFactory } from '../../client/datascience/notebookStorage/types'; +import { PlotViewer } from '../../client/datascience/plotting/plotViewer'; +import { PlotViewerProvider } from '../../client/datascience/plotting/plotViewerProvider'; +import { ProgressReporter } from '../../client/datascience/progress/progressReporter'; +import { RawNotebookProviderWrapper } from '../../client/datascience/raw-kernel/rawNotebookProviderWrapper'; +import { RawNotebookSupportedService } from '../../client/datascience/raw-kernel/rawNotebookSupportedService'; +import { StatusProvider } from '../../client/datascience/statusProvider'; +import { ThemeFinder } from '../../client/datascience/themeFinder'; +import { + ICellHashListener, + ICellHashProvider, + ICodeCssGenerator, + ICodeLensFactory, + ICodeWatcher, + IDataScience, + IDataScienceCodeLensProvider, + IDataScienceCommandListener, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IDebugLocationTracker, + IDigestStorage, + IGatherLogger, + IInteractiveWindow, + IInteractiveWindowListener, + IInteractiveWindowProvider, + IJupyterCommandFactory, + IJupyterDebugger, + IJupyterDebugService, + IJupyterExecution, + IJupyterInterpreterDependencyManager, + IJupyterNotebookProvider, + IJupyterPasswordConnect, + IJupyterServerProvider, + IJupyterSessionManagerFactory, + IJupyterSubCommandExecutionService, + IJupyterUriProviderRegistration, + IJupyterVariableDataProvider, + IJupyterVariableDataProviderFactory, + IJupyterVariables, + IKernelDependencyService, + INotebookAndInteractiveWindowUsageTracker, + INotebookEditor, + INotebookEditorProvider, + INotebookExecutionLogger, + INotebookExporter, + INotebookImporter, + INotebookProvider, + INotebookServer, + INotebookStorage, + IPlotViewer, + IPlotViewerProvider, + IRawNotebookProvider, + IRawNotebookSupportedService, + IStatusProvider, + IThemeFinder, + ITrustService +} from '../../client/datascience/types'; +import { ProtocolParser } from '../../client/debugger/extension/helpers/protocolParser'; +import { IProtocolParser } from '../../client/debugger/extension/types'; +import { + EnvironmentActivationService, + EnvironmentActivationServiceCache +} from '../../client/interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { InterpreterEvaluation } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation'; +import { InterpreterSecurityService } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService'; +import { InterpreterSecurityStorage } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage'; +import { + IInterpreterEvaluation, + IInterpreterSecurityService, + IInterpreterSecurityStorage +} from '../../client/interpreter/autoSelection/types'; +import { InterpreterComparer } from '../../client/interpreter/configuration/interpreterComparer'; +import { InterpreterSelector } from '../../client/interpreter/configuration/interpreterSelector/interpreterSelector'; +import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { + IInterpreterComparer, + IInterpreterSelector, + IPythonPathUpdaterServiceFactory, + IPythonPathUpdaterServiceManager +} from '../../client/interpreter/configuration/types'; +import { + ICondaService, + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterService, + IInterpreterVersionService, + IShebangCodeLensProvider +} from '../../client/interpreter/contracts'; +import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; +import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion'; +import { IInterpreterHashProviderFactory } from '../../client/interpreter/locators/types'; +import { registerInterpreterTypes } from '../../client/interpreter/serviceRegistry'; +import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; +import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; +import { ProposePylanceBanner } from '../../client/languageServices/proposeLanguageServerBanner'; +import { CacheableLocatorPromiseCache } from '../../client/pythonEnvironments/discovery/locators/services/cacheableLocatorService'; +import { InterpeterHashProviderFactory } from '../../client/pythonEnvironments/discovery/locators/services/hashProviderFactory'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { registerForIOC } from '../../client/pythonEnvironments/legacyIOC'; +import { CodeExecutionHelper } from '../../client/terminals/codeExecution/helper'; +import { ICodeExecutionHelper } from '../../client/terminals/types'; +import { MockOutputChannel } from '../mockClasses'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; +import { MockCommandManager } from './mockCommandManager'; +import { MockCustomEditorService } from './mockCustomEditorService'; +import { MockDebuggerService } from './mockDebugService'; +import { MockDocumentManager } from './mockDocumentManager'; +import { MockExtensions } from './mockExtensions'; +import { MockFileSystem } from './mockFileSystem'; +import { MockJupyterManager, SupportedCommands } from './mockJupyterManager'; +import { MockJupyterManagerFactory } from './mockJupyterManagerFactory'; +import { MockLanguageServerAnalysisOptions } from './mockLanguageServerAnalysisOptions'; +import { MockLanguageServerProxy } from './mockLanguageServerProxy'; +import { MockLiveShareApi } from './mockLiveShare'; +import { MockPythonSettings } from './mockPythonSettings'; +import { MockWorkspaceConfiguration } from './mockWorkspaceConfig'; +import { MockWorkspaceFolder } from './mockWorkspaceFolder'; +import { IMountedWebView } from './mountedWebView'; +import { IMountedWebViewFactory, MountedWebViewFactory } from './mountedWebViewFactory'; +import { TestExecutionLogger } from './testexecutionLogger'; +import { TestInteractiveWindowProvider } from './testInteractiveWindowProvider'; +import { + ITestNativeEditorProvider, + TestNativeEditorProvider, + TestNativeEditorProviderOld +} from './testNativeEditorProvider'; +import { TestPersistentStateFactory } from './testPersistentStateFactory'; +import { WebBrowserPanelProvider } from './uiTests/webBrowserPanelProvider'; + +export class DataScienceIocContainer extends UnitTestIocContainer { + public get workingInterpreter() { + return this.workingPython; + } + + public get workingInterpreter2() { + return this.workingPython2; + } + + public get onContextSet(): Event<{ name: string; value: boolean }> { + return this.contextSetEvent.event; + } + + public get mockJupyter(): MockJupyterManager | undefined { + return this.jupyterMock ? this.jupyterMock.getManager() : undefined; + } + + public get kernelService() { + return this.kernelServiceMock; + } + private static jupyterInterpreters: PythonEnvironment[] = []; + private static foundPythonPath: string | undefined; + public applicationShell!: ApplicationShell; + // tslint:disable-next-line:no-any + public datascience!: TypeMoq.IMock<IDataScience>; + public shouldMockJupyter: boolean; + private commandManager: MockCommandManager = new MockCommandManager(); + private setContexts: Record<string, boolean> = {}; + private contextSetEvent: EventEmitter<{ name: string; value: boolean }> = new EventEmitter<{ + name: string; + value: boolean; + }>(); + private jupyterMock: MockJupyterManagerFactory | undefined; + private asyncRegistry: AsyncDisposableRegistry; + private configChangeEvent = new EventEmitter<ConfigurationChangeEvent>(); + private worksaceFoldersChangedEvent = new EventEmitter<WorkspaceFoldersChangeEvent>(); + private documentManager = new MockDocumentManager(); + private workingPython: PythonEnvironment = { + path: '/foo/bar/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + displayName: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 + }; + private workingPython2: PythonEnvironment = { + path: '/foo/baz/python.exe', + version: new SemVer('3.6.7-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + displayName: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 + }; + + private webPanelProvider = mock(WebviewPanelProvider); + private settingsMap = new Map<string, any>(); + private configMap = new Map<string, MockWorkspaceConfiguration>(); + private emptyConfig = new MockWorkspaceConfiguration(); + private workspaceFolders: MockWorkspaceFolder[] = []; + private defaultPythonPath: string | undefined; + private kernelServiceMock = mock(KernelService); + private disposed = false; + private experimentState = new Map<string, boolean>(); + private extensionRootPath: string | undefined; + private languageServerType: LanguageServerType = LanguageServerType.Microsoft; + private pendingWebPanel: IMountedWebView | undefined; + + constructor(private readonly uiTest: boolean = false) { + super(); + this.useVSCodeAPI = false; + const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; + this.shouldMockJupyter = !isRollingBuild; + this.asyncRegistry = new AsyncDisposableRegistry(); + } + + public async dispose(): Promise<void> { + // Make sure to disable all command handling during dispose. Don't want + // anything to startup again. + this.commandManager.dispose(); + try { + // Make sure to delete any temp files written by native editor storage + const globPr = promisify(glob); + const tempLocation = os.tmpdir; + const tempFiles = await globPr(`${tempLocation}/*.ipynb`); + if (tempFiles && tempFiles.length) { + await Promise.all(tempFiles.map((t) => fs.remove(t))); + } + } catch (exc) { + // tslint:disable-next-line: no-console + console.log(`Exception on cleanup: ${exc}`); + } + await this.asyncRegistry.dispose(); + await super.dispose(); + this.disposed = true; + + if (!this.uiTest) { + // Blur window focus so we don't have editors polling + // tslint:disable-next-line: no-require-imports + const reactHelpers = require('./reactHelpers') as typeof import('./reactHelpers'); + reactHelpers.blurWindow(); + } + + // Bounce this so that our editor has time to shutdown + await sleep(150); + + if (!this.uiTest) { + // Clear out the monaco global services. Some of these services are preventing shutdown. + // tslint:disable: no-require-imports + const services = require('monaco-editor/esm/vs/editor/standalone/browser/standaloneServices') as any; + if (services.StaticServices) { + const keys = Object.keys(services.StaticServices); + keys.forEach((k) => { + const service = services.StaticServices[k] as any; + if (service && service._value && service._value.dispose) { + if (typeof service._value.dispose === 'function') { + service._value.dispose(); + } + } + }); + } + // This file doesn't have an export so we can't force a dispose. Instead it has a 5 second timeout + const config = require('monaco-editor/esm/vs/editor/browser/config/configuration') as any; + if (config.getCSSBasedConfiguration) { + config.getCSSBasedConfiguration().dispose(); + } + } + + // Because there are outstanding promises holding onto this object, clear out everything we can + this.workspaceFolders = []; + this.settingsMap.clear(); + this.configMap.clear(); + this.setContexts = {}; + reset(this.webPanelProvider); + + // Turn off the static maps for the environment and conda services. Otherwise this + // can mess up tests that don't depend upon them + CacheableLocatorPromiseCache.forceUseNormal(); + EnvironmentActivationServiceCache.forceUseNormal(); + } + + //tslint:disable:max-func-body-length + public registerDataScienceTypes( + useCustomEditor: boolean = false, + languageServerType: LanguageServerType = LanguageServerType.Microsoft + ) { + this.serviceManager.addSingletonInstance<number>(DataScienceStartupTime, Date.now()); + this.serviceManager.addSingletonInstance<DataScienceIocContainer>(DataScienceIocContainer, this); + + // Save our language server type + this.languageServerType = languageServerType; + + // Inform the cacheable locator service to use a static map so that it stays in memory in between tests + CacheableLocatorPromiseCache.forceUseStatic(); + + // Do the same thing for the environment variable activation service. + EnvironmentActivationServiceCache.forceUseStatic(); + + // Make sure the default python path is set. + this.defaultPythonPath = this.findPythonPath(); + + // Create the workspace service first as it's used to set config values. + this.createWorkspaceService(); + + // Setup our webpanel provider to create our dummy web panel + when(this.webPanelProvider.create(anything())).thenCall(this.onCreateWebPanel.bind(this)); + if (this.uiTest) { + this.serviceManager.addSingleton<IWebviewPanelProvider>(IWebviewPanelProvider, WebBrowserPanelProvider); + this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, IPyWidgetScriptSource); + this.serviceManager.addSingleton<IHttpClient>(IHttpClient, HttpClient); + } else { + this.serviceManager.addSingletonInstance<IWebviewPanelProvider>( + IWebviewPanelProvider, + instance(this.webPanelProvider) + ); + } + this.serviceManager.addSingleton<IInterpreterHashProviderFactory>( + IInterpreterHashProviderFactory, + InterpeterHashProviderFactory + ); + this.serviceManager.addSingleton<IExportManager>(IExportManager, ExportManager); + this.serviceManager.addSingleton<ExportDependencyChecker>(ExportDependencyChecker, ExportDependencyChecker); + this.serviceManager.addSingleton<ExportFileOpener>(ExportFileOpener, ExportFileOpener); + this.serviceManager.addSingleton<IExport>(IExport, ExportToPDF, ExportFormat.pdf); + this.serviceManager.addSingleton<IExport>(IExport, ExportToHTML, ExportFormat.html); + this.serviceManager.addSingleton<IExport>(IExport, ExportToPython, ExportFormat.python); + this.serviceManager.addSingleton<IExport>(IExport, ExportBase, 'Export Base'); + this.serviceManager.addSingleton<ExportUtil>(ExportUtil, ExportUtil); + this.serviceManager.addSingleton<ExportCommands>(ExportCommands, ExportCommands); + this.serviceManager.addSingleton<IExportManagerFilePicker>(IExportManagerFilePicker, ExportManagerFilePicker); + + this.serviceManager.addSingleton<INotebookModelFactory>(INotebookModelFactory, NotebookModelFactory); + this.serviceManager.addSingleton<IMountedWebViewFactory>(IMountedWebViewFactory, MountedWebViewFactory); + this.registerFileSystemTypes(); + this.serviceManager.addSingletonInstance<IDataScienceFileSystem>(IDataScienceFileSystem, new MockFileSystem()); + this.serviceManager.addSingleton<IJupyterExecution>(IJupyterExecution, JupyterExecutionFactory); + this.serviceManager.addSingleton<IInteractiveWindowProvider>( + IInteractiveWindowProvider, + TestInteractiveWindowProvider + ); + this.serviceManager.addSingletonInstance(UseProposedApi, false); + this.serviceManager.addSingletonInstance(UseCustomEditorApi, useCustomEditor); + this.serviceManager.addSingletonInstance(UseVSCodeNotebookEditorApi, false); + this.serviceManager.addSingleton<IDataViewerFactory>(IDataViewerFactory, DataViewerFactory); + this.serviceManager.add<IJupyterVariableDataProvider>( + IJupyterVariableDataProvider, + JupyterVariableDataProvider + ); + this.serviceManager.addSingleton<IJupyterVariableDataProviderFactory>( + IJupyterVariableDataProviderFactory, + JupyterVariableDataProviderFactory + ); + this.serviceManager.addSingleton<IPlotViewerProvider>(IPlotViewerProvider, PlotViewerProvider); + this.serviceManager.add<IDataViewer>(IDataViewer, DataViewer); + this.serviceManager.add<IPlotViewer>(IPlotViewer, PlotViewer); + this.serviceManager.add<IStartPage>(IStartPage, StartPage); + + const experimentService = mock(ExperimentService); + this.serviceManager.addSingletonInstance<IExperimentService>(IExperimentService, instance(experimentService)); + + this.serviceManager.addSingleton<IApplicationEnvironment>(IApplicationEnvironment, ApplicationEnvironment); + this.serviceManager.add<INotebookImporter>(INotebookImporter, JupyterImporter); + this.serviceManager.add<INotebookExporter>(INotebookExporter, JupyterExporter); + this.serviceManager.addSingleton<ILiveShareApi>(ILiveShareApi, MockLiveShareApi); + this.serviceManager.addSingleton<IExtensions>(IExtensions, MockExtensions); + this.serviceManager.add<INotebookServer>(INotebookServer, JupyterServerWrapper); + this.serviceManager.add<IJupyterCommandFactory>(IJupyterCommandFactory, JupyterCommandFactory); + this.serviceManager.addSingleton<IRawNotebookProvider>(IRawNotebookProvider, RawNotebookProviderWrapper); + this.serviceManager.addSingleton<IRawNotebookSupportedService>( + IRawNotebookSupportedService, + RawNotebookSupportedService + ); + this.serviceManager.addSingleton<IThemeFinder>(IThemeFinder, ThemeFinder); + this.serviceManager.addSingleton<ICodeCssGenerator>(ICodeCssGenerator, CodeCssGenerator); + this.serviceManager.addSingleton<IStatusProvider>(IStatusProvider, StatusProvider); + this.serviceManager.addSingleton<IInterpreterPathService>(IInterpreterPathService, InterpreterPathService); + this.serviceManager.addSingleton<IBrowserService>(IBrowserService, BrowserService); + this.serviceManager.addSingletonInstance<IAsyncDisposableRegistry>( + IAsyncDisposableRegistry, + this.asyncRegistry + ); + this.serviceManager.addSingleton<IEnvironmentActivationService>( + IEnvironmentActivationService, + EnvironmentActivationService + ); + this.serviceManager.add<ICodeWatcher>(ICodeWatcher, CodeWatcher); + this.serviceManager.add<IDataScienceCodeLensProvider>( + IDataScienceCodeLensProvider, + DataScienceCodeLensProvider + ); + this.serviceManager.add<ICodeExecutionHelper>(ICodeExecutionHelper, CodeExecutionHelper); + this.serviceManager.add<IDataScienceCommandListener>( + IDataScienceCommandListener, + InteractiveWindowCommandListener + ); + this.serviceManager.addSingleton<IDataScienceErrorHandler>(IDataScienceErrorHandler, DataScienceErrorHandler); + this.serviceManager.add<IInstallationChannelManager>(IInstallationChannelManager, InstallationChannelManager); + this.serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + DebuggerVariableRegistration + ); + this.serviceManager.addSingleton<IJupyterVariables>( + IJupyterVariables, + JupyterVariables, + Identifiers.ALL_VARIABLES + ); + this.serviceManager.addSingleton<IJupyterVariables>( + IJupyterVariables, + OldJupyterVariables, + Identifiers.OLD_VARIABLES + ); + this.serviceManager.addSingleton<IJupyterVariables>( + IJupyterVariables, + KernelVariables, + Identifiers.KERNEL_VARIABLES + ); + this.serviceManager.addSingleton<IJupyterVariables>( + IJupyterVariables, + DebuggerVariables, + Identifiers.DEBUGGER_VARIABLES + ); + this.serviceManager.addSingleton<IJupyterDebugger>(IJupyterDebugger, JupyterDebugger, undefined, [ + ICellHashListener + ]); + this.serviceManager.addSingleton<IDebugLocationTracker>(IDebugLocationTracker, DebugLocationTrackerFactory); + this.serviceManager.addSingleton<INotebookEditorProvider>( + INotebookEditorProvider, + useCustomEditor ? TestNativeEditorProvider : TestNativeEditorProviderOld + ); + this.serviceManager.addSingleton<DataViewerDependencyService>( + DataViewerDependencyService, + DataViewerDependencyService + ); + + this.serviceManager.addSingleton<IDataScienceCommandListener>( + IDataScienceCommandListener, + NativeEditorCommandListener + ); + this.serviceManager.addSingletonInstance<IOutputChannel>( + IOutputChannel, + mock(MockOutputChannel), + JUPYTER_OUTPUT_CHANNEL + ); + this.serviceManager.addSingleton<ICryptoUtils>(ICryptoUtils, CryptoUtils); + this.serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ServerPreload + ); + const mockExtensionContext = TypeMoq.Mock.ofType<IExtensionContext>(); + mockExtensionContext.setup((m) => m.globalStoragePath).returns(() => os.tmpdir()); + mockExtensionContext.setup((m) => m.extensionPath).returns(() => this.extensionRootPath || os.tmpdir()); + this.serviceManager.addSingletonInstance<IExtensionContext>(IExtensionContext, mockExtensionContext.object); + + const mockServerSelector = mock(JupyterServerSelector); + this.serviceManager.addSingletonInstance<JupyterServerSelector>( + JupyterServerSelector, + instance(mockServerSelector) + ); + + this.serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper); + this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + Bash, + TerminalActivationProviders.bashCShellFish + ); + this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell + ); + this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + PyEnvActivationCommandProvider, + TerminalActivationProviders.pyenv + ); + this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + CondaActivationCommandProvider, + TerminalActivationProviders.conda + ); + this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + PipEnvActivationCommandProvider, + TerminalActivationProviders.pipenv + ); + this.serviceManager.addSingleton<ITerminalManager>(ITerminalManager, TerminalManager); + this.serviceManager.addSingleton<ILanguageServerProxy>(ILanguageServerProxy, MockLanguageServerProxy); + this.serviceManager.addSingleton<ILanguageServerCache>( + ILanguageServerCache, + LanguageServerExtensionActivationService + ); + this.serviceManager.addSingleton<ILanguageServerExtension>(ILanguageServerExtension, LanguageServerExtension); + + this.serviceManager.add<ILanguageServerActivator>( + ILanguageServerActivator, + JediExtensionActivator, + LanguageServerType.Jedi + ); + this.serviceManager.addSingleton<ILanguageServerAnalysisOptions>( + ILanguageServerAnalysisOptions, + MockLanguageServerAnalysisOptions + ); + if (languageServerType === LanguageServerType.Microsoft) { + this.serviceManager.add<ILanguageServerActivator>( + ILanguageServerActivator, + DotNetLanguageServerActivator, + LanguageServerType.Microsoft + ); + this.serviceManager.add<ILanguageServerManager>(ILanguageServerManager, DotNetLanguageServerManager); + this.serviceManager.add<IPythonExtensionBanner>( + IPythonExtensionBanner, + ProposePylanceBanner, + BANNER_NAME_PROPOSE_LS + ); + } else if (languageServerType === LanguageServerType.Node) { + this.serviceManager.add<ILanguageServerActivator>( + ILanguageServerActivator, + NodeLanguageServerActivator, + LanguageServerType.Node + ); + this.serviceManager.add<ILanguageServerManager>(ILanguageServerManager, NodeLanguageServerManager); + } + + this.serviceManager.addSingleton<INotebookProvider>(INotebookProvider, NotebookProvider); + this.serviceManager.addSingleton<IJupyterNotebookProvider>(IJupyterNotebookProvider, JupyterNotebookProvider); + this.serviceManager.addSingleton<IJupyterServerProvider>(IJupyterServerProvider, NotebookServerProvider); + + this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, IntellisenseProvider); + this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, AutoSaveService); + this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, GatherListener); + this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, NativeEditorRunByLineListener); + this.serviceManager.addSingleton<IPyWidgetMessageDispatcherFactory>( + IPyWidgetMessageDispatcherFactory, + IPyWidgetMessageDispatcherFactory + ); + if (this.uiTest) { + this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, IPyWidgetHandler); + } + this.serviceManager.add<IProtocolParser>(IProtocolParser, ProtocolParser); + this.serviceManager.addSingleton<IJupyterDebugService>( + IJupyterDebugService, + JupyterDebugService, + Identifiers.RUN_BY_LINE_DEBUGSERVICE + ); + const mockDebugService = new MockDebuggerService( + this.serviceManager.get<IJupyterDebugService>(IJupyterDebugService, Identifiers.RUN_BY_LINE_DEBUGSERVICE) + ); + this.serviceManager.addSingletonInstance<IDebugService>(IDebugService, mockDebugService); + this.serviceManager.addSingletonInstance<IJupyterDebugService>( + IJupyterDebugService, + mockDebugService, + Identifiers.MULTIPLEXING_DEBUGSERVICE + ); + this.serviceManager.add<ICellHashProvider>(ICellHashProvider, CellHashProvider, undefined, [ + INotebookExecutionLogger + ]); + this.serviceManager.addSingleton<INotebookExecutionLogger>(INotebookExecutionLogger, HoverProvider); + this.serviceManager.add<IGatherLogger>(IGatherLogger, GatherLogger, undefined, [INotebookExecutionLogger]); + this.serviceManager.add<INotebookExecutionLogger>(INotebookExecutionLogger, TestExecutionLogger); + this.serviceManager.addSingleton<ICodeLensFactory>(ICodeLensFactory, CodeLensFactory, undefined, [ + IInteractiveWindowListener + ]); + this.serviceManager.addSingleton<IShellDetector>(IShellDetector, TerminalNameShellDetector); + this.serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + LSNotSupportedDiagnosticService, + LSNotSupportedDiagnosticServiceId + ); + this.serviceManager.addSingleton<ILanguageServerCompatibilityService>( + ILanguageServerCompatibilityService, + LanguageServerCompatibilityService + ); + this.serviceManager.addSingleton<IDiagnosticHandlerService<MessageCommandPrompt>>( + IDiagnosticHandlerService, + DiagnosticCommandPromptHandlerService, + DiagnosticCommandPromptHandlerServiceId + ); + this.serviceManager.addSingleton<IDiagnosticFilterService>(IDiagnosticFilterService, DiagnosticFilterService); + this.serviceManager.addSingleton<NotebookStarter>(NotebookStarter, NotebookStarter); + this.serviceManager.addSingleton<KernelSelector>(KernelSelector, KernelSelector); + this.serviceManager.addSingleton<KernelSelectionProvider>(KernelSelectionProvider, KernelSelectionProvider); + this.serviceManager.addSingleton<KernelSwitcher>(KernelSwitcher, KernelSwitcher); + this.serviceManager.addSingleton<IKernelDependencyService>(IKernelDependencyService, KernelDependencyService); + this.serviceManager.addSingleton<INotebookAndInteractiveWindowUsageTracker>( + INotebookAndInteractiveWindowUsageTracker, + NotebookAndInteractiveWindowUsageTracker + ); + this.serviceManager.addSingleton<IProductService>(IProductService, ProductService); + this.serviceManager.addSingleton<KernelDaemonPool>(KernelDaemonPool, KernelDaemonPool); + this.serviceManager.addSingleton<KernelDaemonPreWarmer>(KernelDaemonPreWarmer, KernelDaemonPreWarmer); + this.serviceManager.addSingleton<IVSCodeNotebook>(IVSCodeNotebook, VSCodeNotebook); + this.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + CTagsProductPathService, + ProductType.WorkspaceSymbols + ); + this.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + FormatterProductPathService, + ProductType.Formatter + ); + this.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + LinterProductPathService, + ProductType.Linter + ); + this.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + TestFrameworkProductPathService, + ProductType.TestFramework + ); + this.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + RefactoringLibraryProductPathService, + ProductType.RefactoringLibrary + ); + this.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + DataScienceProductPathService, + ProductType.DataScience + ); + this.serviceManager.addSingleton<IMultiStepInputFactory>(IMultiStepInputFactory, MultiStepInputFactory); + + // No need of reporting progress. + const progressReporter = mock(ProgressReporter); + when(progressReporter.createProgressIndicator(anything())).thenReturn({ + dispose: noop, + token: new CancellationTokenSource().token + }); + this.serviceManager.addSingletonInstance<ProgressReporter>(ProgressReporter, instance(progressReporter)); + + // Don't check for dot net compatibility + const dotNetCompability = mock(DotNetCompatibilityService); + when(dotNetCompability.isSupported()).thenResolve(true); + this.serviceManager.addSingletonInstance<IDotNetCompatibilityService>( + IDotNetCompatibilityService, + instance(dotNetCompability) + ); + + // Don't allow the download to happen + const downloader = mock(LanguageServerDownloader); + this.serviceManager.addSingletonInstance<ILanguageServerDownloader>( + ILanguageServerDownloader, + instance(downloader) + ); + + const folderService = mock(DotNetLanguageServerFolderService); + const packageService = mock(DotNetLanguageServerPackageService); + this.serviceManager.addSingletonInstance<ILanguageServerFolderService>( + ILanguageServerFolderService, + instance(folderService) + ); + this.serviceManager.addSingletonInstance<ILanguageServerPackageService>( + ILanguageServerPackageService, + instance(packageService) + ); + + // Turn off experiments. + const experimentManager = mock(ExperimentsManager); + when(experimentManager.inExperiment(anything())).thenCall((exp) => { + const setState = this.experimentState.get(exp); + if (setState === undefined) { + if (this.shouldMockJupyter) { + // RawKernel doesn't currently have a mock layer + return exp !== LocalZMQKernel.experiment; + } else { + // All experiments to true by default if not mocking jupyter + return true; + } + } + return setState; + }); + when(experimentManager.activate()).thenResolve(); + this.serviceManager.addSingletonInstance<IExperimentsManager>(IExperimentsManager, instance(experimentManager)); + + // Setup our command list + this.commandManager.registerCommand('setContext', (name: string, value: boolean) => { + this.setContexts[name] = value; + this.contextSetEvent.fire({ name: name, value: value }); + }); + this.serviceManager.addSingletonInstance<ICommandManager>(ICommandManager, this.commandManager); + + // Mock the app shell + this.applicationShell = mock(ApplicationShell); + const configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + + configurationService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(this.getSettings.bind(this)); + + this.serviceManager.addSingleton<IEnvironmentVariablesProvider>( + IEnvironmentVariablesProvider, + EnvironmentVariablesProvider + ); + + this.serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, instance(this.applicationShell)); + this.serviceManager.addSingleton<IClipboard>(IClipboard, ClipboardService); + this.serviceManager.addSingletonInstance<IDocumentManager>(IDocumentManager, this.documentManager); + this.serviceManager.addSingletonInstance<IConfigurationService>( + IConfigurationService, + configurationService.object + ); + + this.datascience = TypeMoq.Mock.ofType<IDataScience>(); + this.serviceManager.addSingletonInstance<IDataScience>(IDataScience, this.datascience.object); + this.serviceManager.addSingleton<JupyterCommandLineSelector>( + JupyterCommandLineSelector, + JupyterCommandLineSelector + ); + this.serviceManager.addSingleton<JupyterCommandLineSelectorCommand>( + JupyterCommandLineSelectorCommand, + JupyterCommandLineSelectorCommand + ); + + this.serviceManager.addSingleton<JupyterServerSelectorCommand>( + JupyterServerSelectorCommand, + JupyterServerSelectorCommand + ); + this.serviceManager.addSingleton<NotebookCommands>(NotebookCommands, NotebookCommands); + + this.serviceManager.addSingleton<CommandRegistry>(CommandRegistry, CommandRegistry); + this.serviceManager.addSingleton<IBufferDecoder>(IBufferDecoder, BufferDecoder); + this.serviceManager.addSingleton<IEnvironmentVariablesService>( + IEnvironmentVariablesService, + EnvironmentVariablesService + ); + this.serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); + this.serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS); + + const globalStorage = this.serviceManager.get<Memento>(IMemento, GLOBAL_MEMENTO); + const localStorage = this.serviceManager.get<Memento>(IMemento, WORKSPACE_MEMENTO); + + // Create a custom persistent state factory that remembers specific things between tests + this.serviceManager.addSingletonInstance<IPersistentStateFactory>( + IPersistentStateFactory, + new TestPersistentStateFactory(globalStorage, localStorage) + ); + + const currentProcess = new CurrentProcess(); + this.serviceManager.addSingletonInstance<ICurrentProcess>(ICurrentProcess, currentProcess); + this.serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation); + + this.serviceManager.addSingleton<JupyterInterpreterStateStore>( + JupyterInterpreterStateStore, + JupyterInterpreterStateStore + ); + this.serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + JupyterInterpreterSelectionCommand + ); + this.serviceManager.addSingleton<JupyterInterpreterSelector>( + JupyterInterpreterSelector, + JupyterInterpreterSelector + ); + this.serviceManager.addSingleton<JupyterInterpreterDependencyService>( + JupyterInterpreterDependencyService, + JupyterInterpreterDependencyService + ); + this.serviceManager.addSingleton<JupyterInterpreterService>( + JupyterInterpreterService, + JupyterInterpreterService + ); + this.serviceManager.addSingleton<JupyterInterpreterOldCacheStateStore>( + JupyterInterpreterOldCacheStateStore, + JupyterInterpreterOldCacheStateStore + ); + this.serviceManager.addSingleton<ActiveEditorContextService>( + ActiveEditorContextService, + ActiveEditorContextService + ); + this.serviceManager.addSingleton<IKernelLauncher>(IKernelLauncher, KernelLauncher); + this.serviceManager.addSingleton<IKernelFinder>(IKernelFinder, KernelFinder); + + this.serviceManager.addSingleton<IJupyterSubCommandExecutionService>( + IJupyterSubCommandExecutionService, + JupyterInterpreterSubCommandExecutionService + ); + this.serviceManager.addSingleton<IJupyterInterpreterDependencyManager>( + IJupyterInterpreterDependencyManager, + JupyterInterpreterSubCommandExecutionService + ); + + const interpreterDisplay = TypeMoq.Mock.ofType<IInterpreterDisplay>(); + interpreterDisplay.setup((i) => i.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + + this.serviceManager.add<INotebookStorage>(INotebookStorage, NativeEditorStorage); + this.serviceManager.addSingleton<INotebookStorageProvider>(INotebookStorageProvider, NotebookStorageProvider); + this.serviceManager.addSingleton<ICustomEditorService>(ICustomEditorService, MockCustomEditorService); + + // Create our jupyter mock if necessary + if (this.shouldMockJupyter) { + this.jupyterMock = new MockJupyterManagerFactory(this.serviceManager); + // When using mocked Jupyter, default to using default kernel. + when(this.kernelServiceMock.searchAndRegisterKernel(anything(), anything())).thenResolve(undefined); + when(this.kernelServiceMock.getKernelSpecs(anything(), anything())).thenResolve([]); + this.serviceManager.addSingletonInstance<KernelService>(KernelService, instance(this.kernelServiceMock)); + + registerForIOC(this.serviceManager, this.serviceContainer); + + this.serviceManager.addSingleton<IInterpreterSecurityService>( + IInterpreterSecurityService, + InterpreterSecurityService + ); + this.serviceManager.addSingleton<IInterpreterSecurityStorage>( + IInterpreterSecurityStorage, + InterpreterSecurityStorage + ); + this.serviceManager.addSingleton<IInterpreterEvaluation>(IInterpreterEvaluation, InterpreterEvaluation); + + this.serviceManager.addSingleton<IInterpreterHelper>(IInterpreterHelper, InterpreterHelper); + + this.serviceManager.addSingleton<IInterpreterComparer>(IInterpreterComparer, InterpreterComparer); + this.serviceManager.addSingleton<IInterpreterVersionService>( + IInterpreterVersionService, + InterpreterVersionService + ); + + this.serviceManager.addSingleton<IInterpreterSelector>(IInterpreterSelector, InterpreterSelector); + this.serviceManager.addSingleton<IShebangCodeLensProvider>( + IShebangCodeLensProvider, + ShebangCodeLensProvider + ); + this.serviceManager.addSingleton<IPythonPathUpdaterServiceFactory>( + IPythonPathUpdaterServiceFactory, + PythonPathUpdaterServiceFactory + ); + this.serviceManager.addSingleton<IPythonPathUpdaterServiceManager>( + IPythonPathUpdaterServiceManager, + PythonPathUpdaterService + ); + + // Don't use conda at all when mocking + const condaService = TypeMoq.Mock.ofType<ICondaService>(); + this.serviceManager.rebindInstance<ICondaService>(ICondaService, condaService.object); + condaService.setup((c) => c.isCondaAvailable()).returns(() => Promise.resolve(false)); + condaService.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + condaService.setup((c) => c.condaEnvironmentsFile).returns(() => undefined); + + this.serviceManager.addSingleton<IVirtualEnvironmentManager>( + IVirtualEnvironmentManager, + VirtualEnvironmentManager + ); + + this.serviceManager.addSingletonInstance<IInterpreterDisplay>( + IInterpreterDisplay, + interpreterDisplay.object + ); + } else { + this.serviceManager.addSingleton<IInstaller>(IInstaller, ProductInstaller); + this.serviceManager.addSingleton<KernelService>(KernelService, KernelService); + this.serviceManager.addSingleton<IProcessServiceFactory>(IProcessServiceFactory, ProcessServiceFactory); + this.serviceManager.addSingleton<IPythonExecutionFactory>(IPythonExecutionFactory, PythonExecutionFactory); + + // Make sure full interpreter services are available. + registerInterpreterTypes(this.serviceManager); + registerForIOC(this.serviceManager, this.serviceContainer); + + // Rebind the interpreter display as we don't want to use the real one + this.serviceManager.rebindInstance<IInterpreterDisplay>(IInterpreterDisplay, interpreterDisplay.object); + + this.serviceManager.addSingleton<IJupyterSessionManagerFactory>( + IJupyterSessionManagerFactory, + JupyterSessionManagerFactory + ); + this.serviceManager.addSingleton<IJupyterPasswordConnect>(IJupyterPasswordConnect, JupyterPasswordConnect); + this.serviceManager.addSingleton<IProcessLogger>(IProcessLogger, ProcessLogger); + } + this.serviceManager.addSingleton<NativeEditorSynchronizer>(NativeEditorSynchronizer, NativeEditorSynchronizer); + this.serviceManager.addSingleton<ITrustService>(ITrustService, TrustService); + this.serviceManager.addSingleton<IDigestStorage>(IDigestStorage, DigestStorage); + // Disable syncrhonizing edits + this.serviceContainer.get<NativeEditorSynchronizer>(NativeEditorSynchronizer).disable(); + const dummyDisposable = { + dispose: () => { + return; + } + }; + + when(this.applicationShell.showErrorMessage(anyString())).thenReturn(Promise.resolve('')); + when(this.applicationShell.showErrorMessage(anyString(), anything())).thenReturn(Promise.resolve('')); + when(this.applicationShell.showErrorMessage(anyString(), anything(), anything())).thenReturn( + Promise.resolve('') + ); + when(this.applicationShell.showInformationMessage(anyString())).thenReturn(Promise.resolve('')); + when(this.applicationShell.showInformationMessage(anyString(), anything())).thenReturn(Promise.resolve('')); + when( + this.applicationShell.showInformationMessage(anyString(), anything(), anything()) + ).thenCall((_a1, a2, _a3) => Promise.resolve(a2)); + when(this.applicationShell.showInformationMessage(anyString(), anything(), anything(), anything())).thenCall( + (_a1, a2, _a3, a4) => { + if (typeof a2 === 'string') { + return Promise.resolve(a2); + } else { + return Promise.resolve(a4); + } + } + ); + when(this.applicationShell.showWarningMessage(anyString())).thenReturn(Promise.resolve('')); + when(this.applicationShell.showWarningMessage(anyString(), anything())).thenReturn(Promise.resolve('')); + when(this.applicationShell.showWarningMessage(anyString(), anything(), anything())).thenCall((_a1, a2, _a3) => + Promise.resolve(a2) + ); + when(this.applicationShell.showWarningMessage(anyString(), anything(), anything(), anything())).thenCall( + (_a1, a2, _a3, a4) => { + if (typeof a2 === 'string') { + return Promise.resolve(a2); + } else { + return Promise.resolve(a4); + } + } + ); + when(this.applicationShell.showSaveDialog(anything())).thenReturn(Promise.resolve(Uri.file('test.ipynb'))); + when(this.applicationShell.setStatusBarMessage(anything())).thenReturn(dummyDisposable); + when(this.applicationShell.showInputBox(anything())).thenReturn(Promise.resolve('Python')); + const eventCallback = ( + _listener: (e: WindowState) => any, + _thisArgs?: any, + _disposables?: IDisposable[] | Disposable + ) => { + return { + dispose: noop + }; + }; + when(this.applicationShell.onDidChangeWindowState).thenReturn(eventCallback); + when(this.applicationShell.withProgress(anything(), anything())).thenCall((_o, c) => c()); + + const interpreterManager = this.serviceContainer.get<IInterpreterService>(IInterpreterService); + interpreterManager.initialize(); + + if (this.mockJupyter) { + this.addInterpreter(this.workingPython2, SupportedCommands.all); + this.addInterpreter(this.workingPython, SupportedCommands.all); + } + this.serviceManager.addSingleton<IJupyterUriProviderRegistration>( + IJupyterUriProviderRegistration, + JupyterUriProviderRegistration + ); + } + public setFileContents(uri: Uri, contents: string) { + const fileSystem = this.serviceManager.get<IDataScienceFileSystem>(IDataScienceFileSystem) as MockFileSystem; + fileSystem.addFileContents(uri.fsPath, contents); + } + + public async activate(): Promise<void> { + // Activate all of the extension activation services + const activationServices = this.serviceManager.getAll<IExtensionSingleActivationService>( + IExtensionSingleActivationService + ); + + await Promise.all(activationServices.map((a) => a.activate())); + + // Make sure the command registry registers all commands + this.get<CommandRegistry>(CommandRegistry).register(); + + // Then force our interpreter to be one that supports jupyter (unless in a mock state when we don't have to) + if (!this.mockJupyter) { + const interpreterService = this.serviceManager.get<IInterpreterService>(IInterpreterService); + const activeInterpreter = await interpreterService.getActiveInterpreter(); + if (!activeInterpreter || !(await this.hasFunctionalDependencies(activeInterpreter))) { + const list = await this.getFunctionalTestInterpreters(); + if (list.length) { + this.forceSettingsChanged(undefined, list[0].path); + + // Log this all the time. Useful in determining why a test may not pass. + const message = `Setting interpreter to ${list[0].displayName || list[0].path} -> ${list[0].path}`; + traceInfo(message); + // tslint:disable-next-line: no-console + console.log(message); + + // Also set this as the interpreter to use for jupyter + await this.serviceManager + .get<JupyterInterpreterService>(JupyterInterpreterService) + .setAsSelectedInterpreter(list[0]); + } else { + throw new Error( + 'No jupyter capable interpreter found. Make sure you install all of the functional requirements before running a test' + ); + } + } + } + } + + // tslint:disable:any + public createWebView( + mount: () => ReactWrapper<any, Readonly<{}>, React.Component>, + id: string, + role: vsls.Role = vsls.Role.None + ) { + // Force the container to mock actual live share if necessary + if (role !== vsls.Role.None) { + const liveShareTest = this.get<ILiveShareApi>(ILiveShareApi) as ILiveShareTestingApi; + liveShareTest.forceRole(role); + } + + // We need to mount the react control before we even create an interactive window object. Otherwise the mount will miss rendering some parts + this.pendingWebPanel = this.get<IMountedWebViewFactory>(IMountedWebViewFactory).create(id, mount); + return this.pendingWebPanel; + } + + public getWrapper(type: 'notebook' | 'interactive') { + if (type === 'notebook') { + return this.getNativeWebPanel(undefined).wrapper; + } else { + return this.getInteractiveWebPanel(undefined).wrapper; + } + } + + public getInteractiveWebPanel(window: IInteractiveWindow | undefined) { + return this.get<TestInteractiveWindowProvider>(IInteractiveWindowProvider).getMountedWebView(window); + } + + public getNativeWebPanel(window: INotebookEditor | undefined) { + return this.get<ITestNativeEditorProvider>(INotebookEditorProvider).getMountedWebView(window); + } + + public getContext(name: string): boolean { + if (this.setContexts.hasOwnProperty(name)) { + return this.setContexts[name]; + } + + return false; + } + + public getSettings(resource?: Uri): IPythonSettings { + const key = this.getResourceKey(resource); + let setting = this.settingsMap.get(key); + if (!setting && !this.disposed) { + // Make sure we have the default config for this resource first. + this.getWorkspaceConfig('python', resource); + setting = new MockPythonSettings( + resource, + new MockAutoSelectionService(), + this.serviceManager.get<IWorkspaceService>(IWorkspaceService) + ); + this.settingsMap.set(key, setting); + } else if (this.disposed) { + setting = this.generatePythonSettings(this.languageServerType); + } + return setting; + } + + public forceDataScienceSettingsChanged(dataScienceSettings: Partial<IDataScienceSettings>) { + this.forceSettingsChanged(undefined, this.getSettings().pythonPath, { + ...this.getSettings().datascience, + ...dataScienceSettings + }); + } + + public forceSettingsChanged(resource: Resource, newPath: string, datascienceSettings?: IDataScienceSettings) { + const settings = this.getSettings(resource) as any; + settings.pythonPath = newPath; + settings.datascience = datascienceSettings ? datascienceSettings : settings.datascience; + + // The workspace config must be updated too as a config change event will cause the data to be reread from + // the config. + const config = this.getWorkspaceConfig('python', resource); + config.update('pythonPath', newPath).ignoreErrors(); + config.update('dataScience', settings.datascience).ignoreErrors(); + settings.fireChangeEvent(); + this.configChangeEvent.fire({ + affectsConfiguration(_s: string, _r?: Uri): boolean { + return true; + } + }); + } + + public setExtensionRootPath(newRoot: string) { + this.extensionRootPath = newRoot; + } + + public async getJupyterCapableInterpreter(): Promise<PythonEnvironment | undefined> { + const list = await this.getFunctionalTestInterpreters(); + return list ? list[0] : undefined; + } + + public async getFunctionalTestInterpreters(): Promise<PythonEnvironment[]> { + // This should be cacheable as we don't install new interpreters during tests + if (DataScienceIocContainer.jupyterInterpreters.length > 0) { + return DataScienceIocContainer.jupyterInterpreters; + } + const list = await this.get<IInterpreterService>(IInterpreterService).getInterpreters(undefined); + const promises = list.map((f) => this.hasFunctionalDependencies(f).then((b) => (b ? f : undefined))); + const resolved = await Promise.all(promises); + DataScienceIocContainer.jupyterInterpreters = resolved.filter((r) => r) as PythonEnvironment[]; + return DataScienceIocContainer.jupyterInterpreters; + } + + public addWorkspaceFolder(folderPath: string) { + const workspaceFolder = new MockWorkspaceFolder(folderPath, this.workspaceFolders.length); + this.workspaceFolders.push(workspaceFolder); + return workspaceFolder; + } + + public addResourceToFolder(resource: Uri, folderPath: string) { + let folder = this.workspaceFolders.find((f) => f.uri.fsPath === folderPath); + if (!folder) { + folder = this.addWorkspaceFolder(folderPath); + } + folder.ownedResources.add(resource.toString()); + } + + public get<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, name?: string | number | symbol): T { + return this.serviceManager.get<T>(serviceIdentifier, name); + } + + public getAll<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, name?: string | number | symbol): T[] { + return this.serviceManager.getAll<T>(serviceIdentifier, name); + } + + public addDocument(code: string, file: string) { + return this.documentManager.addDocument(code, file); + } + + public addInterpreter(newInterpreter: PythonEnvironment, commands: SupportedCommands) { + if (this.mockJupyter) { + this.mockJupyter.addInterpreter(newInterpreter, commands); + } + } + + public getWorkspaceConfig(section: string | undefined, resource?: Resource): MockWorkspaceConfiguration { + if (!section || section !== 'python') { + return this.emptyConfig; + } + const key = this.getResourceKey(resource); + let result = this.configMap.get(key); + if (!result) { + result = this.generatePythonWorkspaceConfig(this.languageServerType); + this.configMap.set(key, result); + } + return result; + } + + public setExperimentState(experimentName: string, enabled: boolean) { + this.experimentState.set(experimentName, enabled); + } + + private async onCreateWebPanel(options: IWebviewPanelOptions) { + if (!this.pendingWebPanel) { + throw new Error('Creating web panel without a mount'); + } + const panel = this.pendingWebPanel; + panel.attach(options); + return panel; + } + + private generatePythonSettings(languageServerType: LanguageServerType) { + // Create a dummy settings just to setup the workspace config + const pythonSettings = new MockPythonSettings(undefined, new MockAutoSelectionService()); + pythonSettings.pythonPath = this.defaultPythonPath!; + pythonSettings.datascience = { + allowImportFromNotebook: true, + alwaysTrustNotebooks: true, + jupyterLaunchTimeout: 120000, + jupyterLaunchRetries: 3, + enabled: true, + jupyterServerURI: 'local', + // tslint:disable-next-line: no-invalid-template-strings + notebookFileRoot: '${fileDirname}', + changeDirOnImportExport: false, + useDefaultConfigForJupyter: true, + jupyterInterruptTimeout: 10000, + searchForJupyter: true, + showCellInputCode: true, + collapseCellInputCodeByDefault: true, + allowInput: true, + maxOutputSize: 400, + enableScrollingForCellOutputs: true, + errorBackgroundColor: '#FFFFFF', + sendSelectionToInteractiveWindow: false, + codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', + markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', + variableExplorerExclude: 'module;function;builtin_function_or_method', + liveShareConnectionTimeout: 100, + enablePlotViewer: true, + stopOnFirstLineWhileDebugging: true, + stopOnError: true, + addGotoCodeLenses: true, + enableCellCodeLens: true, + runStartupCommands: '', + debugJustMyCode: true, + variableQueries: [], + jupyterCommandLineArguments: [], + disableJupyterAutoStart: false, + widgetScriptSources: ['jsdelivr.com', 'unpkg.com'], + interactiveWindowMode: 'single' + }; + pythonSettings.downloadLanguageServer = false; + const folders = ['Envs', '.virtualenvs']; + pythonSettings.venvFolders = folders; + pythonSettings.venvPath = path.join('~', 'foo'); + pythonSettings.terminal = { + executeInFileDir: false, + launchArgs: [], + activateEnvironment: true, + activateEnvInCurrentTerminal: false + }; + pythonSettings.languageServer = languageServerType; + return pythonSettings; + } + + private generatePythonWorkspaceConfig(languageServerType: LanguageServerType): MockWorkspaceConfiguration { + const pythonSettings = this.generatePythonSettings(languageServerType); + + // Use these settings to default all of the settings in a python configuration + return new MockWorkspaceConfiguration(pythonSettings); + } + + private createWorkspaceService() { + class MockFileSystemWatcher implements FileSystemWatcher { + public ignoreCreateEvents: boolean = false; + public ignoreChangeEvents: boolean = false; + public ignoreDeleteEvents: boolean = false; + //tslint:disable-next-line:no-any + public onDidChange(_listener: (e: Uri) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { + return { dispose: noop }; + } + //tslint:disable-next-line:no-any + public onDidDelete(_listener: (e: Uri) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { + return { dispose: noop }; + } + //tslint:disable-next-line:no-any + public onDidCreate(_listener: (e: Uri) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { + return { dispose: noop }; + } + public dispose() { + noop(); + } + } + + const workspaceService = mock(WorkspaceService); + this.serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, instance(workspaceService)); + when(workspaceService.onDidChangeConfiguration).thenReturn(this.configChangeEvent.event); + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(this.worksaceFoldersChangedEvent.event); + + // Create another config for other parts of the workspace config. + when(workspaceService.getConfiguration(anything())).thenCall(this.getWorkspaceConfig.bind(this)); + when(workspaceService.getConfiguration(anything(), anything())).thenCall(this.getWorkspaceConfig.bind(this)); + const testWorkspaceFolder = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); + + when(workspaceService.createFileSystemWatcher(anything(), anything(), anything(), anything())).thenReturn( + new MockFileSystemWatcher() + ); + when(workspaceService.createFileSystemWatcher(anything())).thenReturn(new MockFileSystemWatcher()); + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + when(workspaceService.workspaceFolders).thenReturn(this.workspaceFolders); + when(workspaceService.rootPath).thenReturn(testWorkspaceFolder); + when(workspaceService.getWorkspaceFolder(anything())).thenCall(this.getWorkspaceFolder.bind(this)); + this.addWorkspaceFolder(testWorkspaceFolder); + return workspaceService; + } + + private getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined { + if (uri) { + return this.workspaceFolders.find((w) => w.ownedResources.has(uri.toString())); + } + return undefined; + } + + private getResourceKey(resource: Resource): string { + if (!this.disposed) { + const workspace = this.serviceManager.get<IWorkspaceService>(IWorkspaceService); + const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri; + return workspaceFolderUri ? workspaceFolderUri.fsPath : ''; + } + return ''; + } + + private async hasFunctionalDependencies(interpreter: PythonEnvironment): Promise<boolean | undefined> { + try { + traceInfo(`Checking ${interpreter.path} for functional dependencies ...`); + const dependencyChecker = this.serviceManager.get<JupyterInterpreterDependencyService>( + JupyterInterpreterDependencyService + ); + if (await dependencyChecker.areDependenciesInstalled(interpreter)) { + // Functional tests require livelossplot too. Make sure this interpreter has that value as well + const pythonProcess = await this.serviceContainer + .get<IPythonExecutionFactory>(IPythonExecutionFactory) + .createActivatedEnvironment({ + resource: undefined, + interpreter, + allowEnvironmentFetchExceptions: true + }); + const result = pythonProcess.isModuleInstalled('livelossplot'); // Should we check all dependencies? + traceInfo(`${interpreter.path} has jupyter with livelossplot indicating : ${result}`); + return result; + } else { + traceInfo(`${JSON.stringify(interpreter)} is missing jupyter.`); + } + } catch (ex) { + traceError(`Exception attempting dependency list for ${interpreter.path}: `, ex); + return false; + } + } + + private findPythonPath(): string { + try { + // Use a static variable so we don't have to recompute this on subsequenttests + if (!DataScienceIocContainer.foundPythonPath) { + // Give preference to the CI test python (could also be set in launch.json for debugging). + const output = child_process.execFileSync( + process.env.CI_PYTHON_PATH || 'python', + ['-c', 'import sys;print(sys.executable)'], + { encoding: 'utf8' } + ); + DataScienceIocContainer.foundPythonPath = output.replace(/\r?\n/g, ''); + } + return DataScienceIocContainer.foundPythonPath; + } catch (ex) { + return 'python'; + } + } +} diff --git a/src/test/datascience/datascience.unit.test.ts b/src/test/datascience/datascience.unit.test.ts new file mode 100644 index 000000000000..db20c2c28534 --- /dev/null +++ b/src/test/datascience/datascience.unit.test.ts @@ -0,0 +1,245 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { nbformat } from '@jupyterlab/coreutils'; +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { PythonSettings } from '../../client/common/configSettings'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { IConfigurationService, IPythonSettings } from '../../client/common/types'; +import { CommandRegistry } from '../../client/datascience/commands/commandRegistry'; +import { pruneCell } from '../../client/datascience/common'; +import { DataScience } from '../../client/datascience/datascience'; +import { DataScienceCodeLensProvider } from '../../client/datascience/editor-integration/codelensprovider'; +import { IDataScienceCodeLensProvider } from '../../client/datascience/types'; + +// tslint:disable: max-func-body-length +suite('DataScience Tests', () => { + let dataScience: DataScience; + let cmdManager: CommandManager; + let codeLensProvider: IDataScienceCodeLensProvider; + let configService: IConfigurationService; + let docManager: IDocumentManager; + let workspaceService: IWorkspaceService; + let cmdRegistry: CommandRegistry; + let settings: IPythonSettings; + let onDidChangeSettings: sinon.SinonStub; + let onDidChangeActiveTextEditor: sinon.SinonStub; + setup(() => { + cmdManager = mock(CommandManager); + codeLensProvider = mock(DataScienceCodeLensProvider); + configService = mock(ConfigurationService); + workspaceService = mock(WorkspaceService); + cmdRegistry = mock(CommandRegistry); + docManager = mock(DocumentManager); + settings = mock(PythonSettings); + + dataScience = new DataScience( + instance(cmdManager), + // tslint:disable-next-line: no-any + [] as any, + // tslint:disable-next-line: no-any + { subscriptions: [] } as any, + instance(codeLensProvider), + instance(configService), + instance(docManager), + instance(workspaceService), + instance(cmdRegistry) + ); + + onDidChangeSettings = sinon.stub(); + onDidChangeActiveTextEditor = sinon.stub(); + when(configService.getSettings(anything())).thenReturn(instance(settings)); + when(settings.onDidChange).thenReturn(onDidChangeSettings); + // tslint:disable-next-line: no-any + when(settings.datascience).thenReturn({} as any); + when(docManager.onDidChangeActiveTextEditor).thenReturn(onDidChangeActiveTextEditor); + }); + + suite('Activate', () => { + setup(async () => { + await dataScience.activate(); + }); + + test('Should register commands', async () => { + verify(cmdRegistry.register()).once(); + }); + test('Should add handler for Settings Changed', async () => { + assert.ok(onDidChangeSettings.calledOnce); + }); + test('Should add handler for ActiveTextEditorChanged', async () => { + assert.ok(onDidChangeActiveTextEditor.calledOnce); + }); + }); + + suite('Cell pruning', () => { + test('Remove output and execution count from non code', () => { + const cell: nbformat.ICell = { + cell_type: 'markdown', + outputs: [], + execution_count: '23', + source: 'My markdown', + metadata: {} + }; + const result = pruneCell(cell); + assert.equal(Object.keys(result).indexOf('outputs'), -1, 'Outputs inside markdown'); + assert.equal(Object.keys(result).indexOf('execution_count'), -1, 'Execution count inside markdown'); + }); + test('Outputs dont contain extra data', () => { + const cell: nbformat.ICell = { + cell_type: 'code', + outputs: [ + { + output_type: 'display_data', + extra: {} + } + ], + execution_count: '23', + source: 'My source', + metadata: {} + }; + const result = pruneCell(cell); + // tslint:disable-next-line: no-any + assert.equal((result.outputs as any).length, 1, 'Outputs were removed'); + assert.equal(result.execution_count, '23', 'Output execution count removed'); + const output = (result.outputs as nbformat.IOutput[])[0]; + assert.equal(Object.keys(output).indexOf('extra'), -1, 'Output still has extra data'); + assert.notEqual(Object.keys(output).indexOf('output_type'), -1, 'Output is missing output_type'); + }); + test('Display outputs still have their data', () => { + const cell: nbformat.ICell = { + cell_type: 'code', + execution_count: 2, + metadata: {}, + outputs: [ + { + output_type: 'display_data', + data: { + 'text/plain': "Box(children=(Label(value='My label'),))", + 'application/vnd.jupyter.widget-view+json': { + version_major: 2, + version_minor: 0, + model_id: '90c99248d7bb490ca132427de6d1e235' + } + }, + metadata: { bob: 'youruncle' } + } + ], + source: ["line = widgets.Label('My label')\n", 'box = widgets.Box([line])\n', 'box'] + }; + + const result = pruneCell(cell); + // tslint:disable-next-line: no-any + assert.equal((result.outputs as any).length, 1, 'Outputs were removed'); + assert.equal(result.execution_count, 2, 'Output execution count removed'); + assert.deepEqual(result.outputs, cell.outputs, 'Outputs were modified'); + }); + test('Stream outputs still have their data', () => { + const cell: nbformat.ICell = { + cell_type: 'code', + execution_count: 2, + metadata: {}, + outputs: [ + { + output_type: 'stream', + name: 'stdout', + text: 'foobar' + } + ], + source: ["line = widgets.Label('My label')\n", 'box = widgets.Box([line])\n', 'box'] + }; + + const result = pruneCell(cell); + // tslint:disable-next-line: no-any + assert.equal((result.outputs as any).length, 1, 'Outputs were removed'); + assert.equal(result.execution_count, 2, 'Output execution count removed'); + assert.deepEqual(result.outputs, cell.outputs, 'Outputs were modified'); + }); + test('Errors outputs still have their data', () => { + const cell: nbformat.ICell = { + cell_type: 'code', + execution_count: 2, + metadata: {}, + outputs: [ + { + output_type: 'error', + ename: 'stdout', + evalue: 'stdout is a value', + traceback: ['more'] + } + ], + source: ["line = widgets.Label('My label')\n", 'box = widgets.Box([line])\n', 'box'] + }; + + const result = pruneCell(cell); + // tslint:disable-next-line: no-any + assert.equal((result.outputs as any).length, 1, 'Outputs were removed'); + assert.equal(result.execution_count, 2, 'Output execution count removed'); + assert.deepEqual(result.outputs, cell.outputs, 'Outputs were modified'); + }); + test('Execute result outputs still have their data', () => { + const cell: nbformat.ICell = { + cell_type: 'code', + execution_count: 2, + metadata: {}, + outputs: [ + { + output_type: 'execute_result', + execution_count: '4', + data: { + 'text/plain': "Box(children=(Label(value='My label'),))", + 'application/vnd.jupyter.widget-view+json': { + version_major: 2, + version_minor: 0, + model_id: '90c99248d7bb490ca132427de6d1e235' + } + }, + metadata: { foo: 'bar' } + } + ], + source: ["line = widgets.Label('My label')\n", 'box = widgets.Box([line])\n', 'box'] + }; + + const result = pruneCell(cell); + // tslint:disable-next-line: no-any + assert.equal((result.outputs as any).length, 1, 'Outputs were removed'); + assert.equal(result.execution_count, 2, 'Output execution count removed'); + assert.deepEqual(result.outputs, cell.outputs, 'Outputs were modified'); + }); + test('Unrecognized outputs still have their data', () => { + const cell: nbformat.ICell = { + cell_type: 'code', + execution_count: 2, + metadata: {}, + outputs: [ + { + output_type: 'unrecognized', + execution_count: '4', + data: { + 'text/plain': "Box(children=(Label(value='My label'),))", + 'application/vnd.jupyter.widget-view+json': { + version_major: 2, + version_minor: 0, + model_id: '90c99248d7bb490ca132427de6d1e235' + } + }, + metadata: {} + } + ], + source: ["line = widgets.Label('My label')\n", 'box = widgets.Box([line])\n', 'box'] + }; + + const result = pruneCell(cell); + // tslint:disable-next-line: no-any + assert.equal((result.outputs as any).length, 1, 'Outputs were removed'); + assert.equal(result.execution_count, 2, 'Output execution count removed'); + assert.deepEqual(result.outputs, cell.outputs, 'Outputs were modified'); + }); + }); +}); diff --git a/src/test/datascience/datascienceSurveyBanner.unit.test.ts b/src/test/datascience/datascienceSurveyBanner.unit.test.ts new file mode 100644 index 000000000000..fab0eb8d2f4a --- /dev/null +++ b/src/test/datascience/datascienceSurveyBanner.unit.test.ts @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { expect } from 'chai'; +import { instance, mock, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { EventEmitter } from 'vscode'; +import { IApplicationShell } from '../../client/common/application/types'; +import { IBrowserService, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; +import { DataScienceSurveyBanner, DSSurveyStateKeys } from '../../client/datascience/dataScienceSurveyBanner'; +import { NativeEditorProvider } from '../../client/datascience/notebookStorage/nativeEditorProvider'; +import { INotebookEditor } from '../../client/datascience/types'; + +suite('DataScience Survey Banner', () => { + let appShell: typemoq.IMock<IApplicationShell>; + let browser: typemoq.IMock<IBrowserService>; + const targetUri: string = 'https://microsoft.com'; + + const message = + 'Can you please take 2 minutes to tell us how the Python Data Science features are working for you?'; + const yes = 'Yes, take survey now'; + const no = 'No, thanks'; + + setup(() => { + appShell = typemoq.Mock.ofType<IApplicationShell>(); + browser = typemoq.Mock.ofType<IBrowserService>(); + }); + test('DataScience banner should be enabled after we hit our execution count', async () => { + const enabledValue: boolean = true; + const executionCount: number = 1000; + const testBanner: DataScienceSurveyBanner = preparePopup( + executionCount, + 0, + enabledValue, + appShell.object, + browser.object, + targetUri + ); + const expectedUri: string = targetUri; + let receivedUri: string = ''; + browser + .setup((b) => + b.launch( + typemoq.It.is((a: string) => { + receivedUri = a; + return a === expectedUri; + }) + ) + ) + .verifiable(typemoq.Times.once()); + await testBanner.launchSurvey(); + // This is technically not necessary, but it gives + // better output than the .verifyAll messages do. + expect(receivedUri).is.equal(expectedUri, 'Uri given to launch mock is incorrect.'); + + // verify that the calls expected were indeed made. + browser.verifyAll(); + browser.reset(); + }); + + test('DataScience banner should be enabled after we hit our notebook count', async () => { + const enabledValue: boolean = true; + const testBanner: DataScienceSurveyBanner = preparePopup( + 0, + 15, + enabledValue, + appShell.object, + browser.object, + targetUri + ); + const expectedUri: string = targetUri; + let receivedUri: string = ''; + browser + .setup((b) => + b.launch( + typemoq.It.is((a: string) => { + receivedUri = a; + return a === expectedUri; + }) + ) + ) + .verifiable(typemoq.Times.once()); + await testBanner.launchSurvey(); + // This is technically not necessary, but it gives + // better output than the .verifyAll messages do. + expect(receivedUri).is.equal(expectedUri, 'Uri given to launch mock is incorrect.'); + + // verify that the calls expected were indeed made. + browser.verifyAll(); + browser.reset(); + }); + + test('Do not show data science banner when it is disabled', () => { + appShell + .setup((a) => + a.showInformationMessage(typemoq.It.isValue(message), typemoq.It.isValue(yes), typemoq.It.isValue(no)) + ) + .verifiable(typemoq.Times.never()); + const enabledValue: boolean = false; + const executionCount: number = 0; + const notebookCount: number = 200; + const testBanner: DataScienceSurveyBanner = preparePopup( + executionCount, + notebookCount, + enabledValue, + appShell.object, + browser.object, + targetUri + ); + testBanner.showBanner().ignoreErrors(); + }); + test('Do not show data science banner if we have not hit our execution count or our notebook count', () => { + appShell + .setup((a) => + a.showInformationMessage(typemoq.It.isValue(message), typemoq.It.isValue(yes), typemoq.It.isValue(no)) + ) + .verifiable(typemoq.Times.never()); + const enabledValue: boolean = true; + const testBanner: DataScienceSurveyBanner = preparePopup( + 99, + 4, + enabledValue, + appShell.object, + browser.object, + targetUri + ); + testBanner.showBanner().ignoreErrors(); + }); +}); + +function preparePopup( + executionCount: number, + initialOpenCount: number, + enabledValue: boolean, + appShell: IApplicationShell, + browser: IBrowserService, + targetUri: string +): DataScienceSurveyBanner { + let openCount = 0; + const myfactory: typemoq.IMock<IPersistentStateFactory> = typemoq.Mock.ofType<IPersistentStateFactory>(); + const enabledValState: typemoq.IMock<IPersistentState<boolean>> = typemoq.Mock.ofType<IPersistentState<boolean>>(); + const executionCountState: typemoq.IMock<IPersistentState<number>> = typemoq.Mock.ofType< + IPersistentState<number> + >(); + const openCountState: typemoq.IMock<IPersistentState<number>> = typemoq.Mock.ofType<IPersistentState<number>>(); + const provider = mock(NativeEditorProvider); + (instance(provider) as any).then = undefined; + const openedEventEmitter = new EventEmitter<INotebookEditor>(); + when(provider.onDidOpenNotebookEditor).thenReturn(openedEventEmitter.event); + enabledValState + .setup((a) => a.updateValue(typemoq.It.isValue(true))) + .returns(() => { + enabledValue = true; + return Promise.resolve(); + }); + enabledValState + .setup((a) => a.updateValue(typemoq.It.isValue(false))) + .returns(() => { + enabledValue = false; + return Promise.resolve(); + }); + + executionCountState + .setup((a) => a.updateValue(typemoq.It.isAnyNumber())) + .returns(() => { + executionCount += 1; + return Promise.resolve(); + }); + openCountState + .setup((a) => a.updateValue(typemoq.It.isAnyNumber())) + .returns((v) => { + openCount = v; + return Promise.resolve(); + }); + + enabledValState.setup((a) => a.value).returns(() => enabledValue); + executionCountState.setup((a) => a.value).returns(() => executionCount); + openCountState.setup((a) => a.value).returns(() => openCount); + + myfactory + .setup((a) => + a.createGlobalPersistentState(typemoq.It.isValue(DSSurveyStateKeys.ShowBanner), typemoq.It.isValue(true)) + ) + .returns(() => { + return enabledValState.object; + }); + myfactory + .setup((a) => + a.createGlobalPersistentState(typemoq.It.isValue(DSSurveyStateKeys.ShowBanner), typemoq.It.isValue(false)) + ) + .returns(() => { + return enabledValState.object; + }); + myfactory + .setup((a) => + a.createGlobalPersistentState( + typemoq.It.isValue(DSSurveyStateKeys.ExecutionCount), + typemoq.It.isAnyNumber() + ) + ) + .returns(() => { + return executionCountState.object; + }); + myfactory + .setup((a) => + a.createGlobalPersistentState( + typemoq.It.isValue(DSSurveyStateKeys.OpenNotebookCount), + typemoq.It.isAnyNumber() + ) + ) + .returns(() => { + return openCountState.object; + }); + const result = new DataScienceSurveyBanner(appShell, myfactory.object, browser, instance(provider), targetUri); + + // Fire the number of opens specifed so that it behaves like the real editor + for (let i = 0; i < initialOpenCount; i += 1) { + openedEventEmitter.fire({} as any); + } + + return result; +} diff --git a/src/test/datascience/dataviewer.functional.test.tsx b/src/test/datascience/dataviewer.functional.test.tsx new file mode 100644 index 000000000000..d10c18e81f6c --- /dev/null +++ b/src/test/datascience/dataviewer.functional.test.tsx @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +import '../../client/common/extensions'; + +import { nbformat } from '@jupyterlab/coreutils'; +import * as assert from 'assert'; +import { mount, ReactWrapper } from 'enzyme'; +import { parse } from 'node-html-parser'; +import * as React from 'react'; +import * as uuid from 'uuid/v4'; +import { Disposable } from 'vscode'; + +import { Identifiers } from '../../client/datascience/constants'; +import { + DataViewerMessages, + IDataViewer, + IDataViewerDataProvider, + IDataViewerFactory +} from '../../client/datascience/data-viewing/types'; +import { getDefaultInteractiveIdentity } from '../../client/datascience/interactive-window/identity'; +import { + IJupyterVariable, + IJupyterVariableDataProviderFactory, + INotebook, + INotebookProvider +} from '../../client/datascience/types'; +import { MainPanel } from '../../datascience-ui/data-explorer/mainPanel'; +import { ReactSlickGrid } from '../../datascience-ui/data-explorer/reactSlickGrid'; +import { noop, sleep } from '../core'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { takeSnapshot, writeDiffSnapshot } from './helpers'; +import { IMountedWebView } from './mountedWebView'; + +// import { asyncDump } from '../common/asyncDump'; +suite('DataScience DataViewer tests', () => { + const disposables: Disposable[] = []; + let dataViewerFactory: IDataViewerFactory; + let jupyterVariableDataProviderFactory: IJupyterVariableDataProviderFactory; + let ioc: DataScienceIocContainer; + let notebook: INotebook | undefined; + const snapshot = takeSnapshot(); + + suiteSetup(function () { + // DataViewer tests require jupyter to run. Othewrise can't + // run any of our variable execution code + const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; + if (!isRollingBuild) { + // tslint:disable-next-line:no-console + console.log('Skipping DataViewer tests. Requires python environment'); + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + + suiteTeardown(() => { + writeDiffSnapshot(snapshot, 'DataViewer'); + }); + + setup(async () => { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(); + return ioc.activate(); + }); + + function mountWebView() { + // Setup our webview panel + const mounted = ioc.createWebView( + () => mount(<MainPanel skipDefault={true} baseTheme={'vscode-light'} testMode={true} />), + 'default' + ); + + // Make sure the data explorer provider and execution factory in the container is created (the extension does this on startup in the extension) + dataViewerFactory = ioc.get<IDataViewerFactory>(IDataViewerFactory); + jupyterVariableDataProviderFactory = ioc.get<IJupyterVariableDataProviderFactory>( + IJupyterVariableDataProviderFactory + ); + + return mounted; + } + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + await ioc.dispose(); + delete (global as any).ascquireVsCodeApi; + }); + + suiteTeardown(() => { + // asyncDump(); + }); + + function createJupyterVariable(variable: string, type: string): IJupyterVariable { + return { + name: variable, + value: '', + supportsDataExplorer: true, + type, + size: 0, + truncated: true, + shape: '', + count: 0 + }; + } + + async function createJupyterVariableDataProvider( + jupyterVariable: IJupyterVariable + ): Promise<IDataViewerDataProvider> { + return jupyterVariableDataProviderFactory.create(jupyterVariable, notebook!); + } + + async function createDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise<IDataViewer> { + return dataViewerFactory.create(dataProvider, title); + } + + async function createJupyterVariableDataViewer(variable: string, type: string): Promise<IDataViewer> { + const jupyterVariable: IJupyterVariable = createJupyterVariable(variable, type); + const jupyterVariableDataProvider: IDataViewerDataProvider = await createJupyterVariableDataProvider( + jupyterVariable + ); + return createDataViewer(jupyterVariableDataProvider, jupyterVariable.name); + } + + async function injectCode(code: string): Promise<void> { + const notebookProvider = ioc.get<INotebookProvider>(INotebookProvider); + notebook = await notebookProvider.getOrCreateNotebook({ + identity: getDefaultInteractiveIdentity() + }); + if (notebook) { + const cells = await notebook.execute(code, Identifiers.EmptyFileName, 0, uuid()); + assert.equal(cells.length, 1, `Wrong number of cells returned`); + assert.equal(cells[0].data.cell_type, 'code', `Wrong type of cell returned`); + const cell = cells[0].data as nbformat.ICodeCell; + if (cell.outputs.length > 0) { + const error = cell.outputs[0].evalue; + if (error) { + assert.fail(`Unexpected error: ${error}`); + } + } + } + } + + function getCompletedPromise(mountedWebView: IMountedWebView): Promise<void> { + return mountedWebView.waitForMessage(DataViewerMessages.CompletedData); + } + + // tslint:disable-next-line:no-any + function runMountedTest(name: string, testFunc: (mount: IMountedWebView) => Promise<void>) { + test(name, async () => { + const wrapper = mountWebView(); + try { + await testFunc(wrapper); + } finally { + // Make sure to unmount the wrapper or it will interfere with other tests + wrapper.dispose(); + } + }); + } + + function sortRows( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + sortCol: string, + sortAsc: boolean + ): void { + // Cause our sort + const mainPanelWrapper = wrapper.find(MainPanel); + assert.ok(mainPanelWrapper && mainPanelWrapper.length > 0, 'Grid not found to sort on'); + const mainPanel = mainPanelWrapper.instance() as MainPanel; + assert.ok(mainPanel, 'Main panel instance not found'); + const reactGrid = (mainPanel as any).grid.current as ReactSlickGrid; + assert.ok(reactGrid, 'Grid control not found'); + if (reactGrid.state.grid) { + const cols = reactGrid.state.grid.getColumns(); + const col = cols.find((c) => c.field === sortCol); + assert.ok(col, `${sortCol} is not a column of the grid`); + reactGrid.sort(new Slick.EventData(), { + sortCol: col, + sortAsc, + multiColumnSort: false, + grid: reactGrid.state.grid + }); + } + } + + async function filterRows( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + filterCol: string, + filterText: string + ): Promise<void> { + // Cause our sort + const mainPanelWrapper = wrapper.find(MainPanel); + assert.ok(mainPanelWrapper && mainPanelWrapper.length > 0, 'Grid not found to sort on'); + const mainPanel = mainPanelWrapper.instance() as MainPanel; + assert.ok(mainPanel, 'Main panel instance not found'); + const reactGrid = (mainPanel as any).grid.current as ReactSlickGrid; + assert.ok(reactGrid, 'Grid control not found'); + if (reactGrid.state.grid) { + const cols = reactGrid.state.grid.getColumns(); + const col = cols.find((c) => c.field === filterCol); + assert.ok(col, `${filterCol} is not a column of the grid`); + reactGrid.filterChanged(filterText, col!); + await sleep(100); + wrapper.update(); + } + } + + function verifyRows(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, rows: (string | number)[]) { + const mainPanel = wrapper.find('.main-panel'); + assert.ok(mainPanel.length >= 1, "Didn't find any cells being rendered"); + wrapper.update(); + + // Force the main panel to actually render. + const html = mainPanel.html(); + const root = parse(html) as any; + const cells = root.querySelectorAll('.react-grid-cell') as HTMLElement[]; + assert.ok(cells, 'No cells found'); + assert.ok(cells.length >= rows.length, 'Not enough cells found'); + // Cells should be an array that matches up to the values we expect. + for (let i = 0; i < rows.length; i += 1) { + // Span should have our value (based on the CellFormatter's output) + const span = cells[i].querySelector('div.cell-formatter span') as HTMLSpanElement; + assert.ok(span, `Span ${i} not found`); + const val = rows[i].toString(); + assert.equal(val, span.innerHTML, `Row ${i} not matching. ${span.innerHTML} !== ${val}`); + } + } + + runMountedTest('Data Frame', async (wrapper) => { + await injectCode('import pandas as pd\r\ndf = pd.DataFrame([0, 1, 2, 3])'); + const gotAllRows = getCompletedPromise(wrapper); + const dv = await createJupyterVariableDataViewer('df', 'DataFrame'); + assert.ok(dv, 'DataViewer not created'); + await gotAllRows; + + verifyRows(wrapper.wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); + }); + + runMountedTest('List', async (wrapper) => { + await injectCode('ls = [0, 1, 2, 3]'); + const gotAllRows = getCompletedPromise(wrapper); + const dv = await createJupyterVariableDataViewer('ls', 'list'); + assert.ok(dv, 'DataViewer not created'); + await gotAllRows; + + verifyRows(wrapper.wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); + }); + + runMountedTest('Series', async (wrapper) => { + await injectCode('import pandas as pd\r\ns = pd.Series([0, 1, 2, 3])'); + const gotAllRows = getCompletedPromise(wrapper); + const dv = await createJupyterVariableDataViewer('s', 'Series'); + assert.ok(dv, 'DataViewer not created'); + await gotAllRows; + + verifyRows(wrapper.wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); + }); + + runMountedTest('np.array', async (wrapper) => { + await injectCode('import numpy as np\r\nx = np.array([0, 1, 2, 3])'); + const gotAllRows = getCompletedPromise(wrapper); + const dv = await createJupyterVariableDataViewer('x', 'ndarray'); + assert.ok(dv, 'DataViewer not created'); + await gotAllRows; + + verifyRows(wrapper.wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); + }); + + runMountedTest('Failure', async (_wrapper) => { + await injectCode('import numpy as np\r\nx = np.array([0, 1, 2, 3])'); + try { + await createJupyterVariableDataViewer('unknown variable', 'ndarray'); + assert.fail('Exception should have been thrown'); + } catch { + noop(); + } + }); + + runMountedTest('Sorting', async (wrapper) => { + await injectCode('import numpy as np\r\nx = np.array([0, 1, 2, 3])'); + const gotAllRows = getCompletedPromise(wrapper); + const dv = await createJupyterVariableDataViewer('x', 'ndarray'); + assert.ok(dv, 'DataViewer not created'); + await gotAllRows; + + verifyRows(wrapper.wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); + sortRows(wrapper.wrapper, '0', false); + verifyRows(wrapper.wrapper, [3, 3, 2, 2, 1, 1, 0, 0]); + }); + + runMountedTest('Filter', async (wrapper) => { + await injectCode('import numpy as np\r\nx = np.array([0, 1, 2, 3])'); + const gotAllRows = getCompletedPromise(wrapper); + const dv = await createJupyterVariableDataViewer('x', 'ndarray'); + assert.ok(dv, 'DataViewer not created'); + await gotAllRows; + + verifyRows(wrapper.wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); + await filterRows(wrapper.wrapper, '0', '> 1'); + verifyRows(wrapper.wrapper, [2, 2, 3, 3]); + await filterRows(wrapper.wrapper, '0', '0'); + verifyRows(wrapper.wrapper, [0, 0]); + }); +}); diff --git a/src/test/datascience/debugLocationTracker.unit.test.ts b/src/test/datascience/debugLocationTracker.unit.test.ts new file mode 100644 index 000000000000..f712d9d0f189 --- /dev/null +++ b/src/test/datascience/debugLocationTracker.unit.test.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +//tslint:disable:max-func-body-length match-default-export-name no-any no-multiline-string no-trailing-whitespace +import { expect } from 'chai'; + +import { DebugLocationTracker } from '../../client/datascience/debugLocationTracker'; +import { IDebugLocation } from '../../client/datascience/types'; + +suite('Debug Location Tracker', () => { + let debugTracker: DebugLocationTracker; + + setup(() => { + debugTracker = new DebugLocationTracker('1'); + }); + + test('Check debug location', async () => { + expect(debugTracker.debugLocation).to.be.equal(undefined, 'Initial location is empty'); + + debugTracker.onDidSendMessage(makeStopMessage()); + + expect(debugTracker.debugLocation).to.be.equal(undefined, 'After stop location is empty'); + + debugTracker.onDidSendMessage(makeStackTraceMessage()); + + const testLocation: IDebugLocation = { lineNumber: 1, column: 1, fileName: 'testpath' }; + expect(debugTracker.debugLocation).to.be.deep.equal(testLocation, 'Source location is incorrect'); + + debugTracker.onDidSendMessage(makeContinueMessage()); + + expect(debugTracker.debugLocation).to.be.equal(undefined, 'After continue location is empty'); + }); +}); + +function makeStopMessage(): any { + return { type: 'event', event: 'stopped' }; +} + +function makeContinueMessage(): any { + return { type: 'event', event: 'continue' }; +} + +function makeStackTraceMessage(): any { + return { + type: 'response', + command: 'stackTrace', + body: { + stackFrames: [{ line: 1, column: 1, source: { path: 'testpath' } }] + } + }; +} diff --git a/src/test/datascience/debugger.functional.test.tsx b/src/test/datascience/debugger.functional.test.tsx new file mode 100644 index 000000000000..435fa3e01701 --- /dev/null +++ b/src/test/datascience/debugger.functional.test.tsx @@ -0,0 +1,448 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import * as uuid from 'uuid/v4'; +import { CodeLens, Disposable, Position, Range, SourceBreakpoint, Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; + +import { expect } from 'chai'; +import { IApplicationShell, IDocumentManager } from '../../client/common/application/types'; +import { RunByLine } from '../../client/common/experiments/groups'; +import { createDeferred, waitForPromise } from '../../client/common/utils/async'; +import { noop } from '../../client/common/utils/misc'; +import { EXTENSION_ROOT_DIR } from '../../client/constants'; +import { Commands, Identifiers } from '../../client/datascience/constants'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { + IDataScienceCodeLensProvider, + IDebugLocationTracker, + IInteractiveWindowProvider, + IJupyterDebugService, + IJupyterExecution +} from '../../client/datascience/types'; +import { DebugState } from '../../datascience-ui/interactive-common/mainState'; +import { ImageButton } from '../../datascience-ui/react-common/imageButton'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { takeSnapshot, writeDiffSnapshot } from './helpers'; +import { getInteractiveCellResults, getOrCreateInteractiveWindow } from './interactiveWindowTestHelpers'; +import { MockDocument } from './mockDocument'; +import { MockDocumentManager } from './mockDocumentManager'; +import { addCell, createNewEditor } from './nativeEditorTestHelpers'; +import { getLastOutputCell, openVariableExplorer, runInteractiveTest, runNativeTest } from './testHelpers'; +import { verifyVariables } from './variableTestHelpers'; + +//import { asyncDump } from '../common/asyncDump'; +// tslint:disable-next-line:max-func-body-length no-any +suite('DataScience Debugger tests', () => { + const disposables: Disposable[] = []; + const postDisposables: Disposable[] = []; + let ioc: DataScienceIocContainer; + let lastErrorMessage: string | undefined; + let jupyterDebuggerService: IJupyterDebugService | undefined; + // tslint:disable-next-line: no-any + let snapshot: any; + + suiteSetup(function () { + snapshot = takeSnapshot(); + + // Debugger tests require jupyter to run. Othewrise can't not really testing them + const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; + + if (!isRollingBuild) { + // tslint:disable-next-line:no-console + console.log('Skipping Debugger tests. Requires python environment'); + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + + suiteTeardown(() => { + writeDiffSnapshot(snapshot, 'Debugger'); + }); + + setup(async () => { + ioc = new DataScienceIocContainer(); + }); + + async function createIOC() { + ioc.registerDataScienceTypes(); + jupyterDebuggerService = ioc.serviceManager.get<IJupyterDebugService>( + IJupyterDebugService, + Identifiers.MULTIPLEXING_DEBUGSERVICE + ); + // Rebind the appshell so we can change what happens on an error + const dummyDisposable = { + dispose: () => { + return; + } + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((e) => (lastErrorMessage = e)); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve('')); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2)); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(Uri.file('test.ipynb'))); + appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + + ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); + + // Make sure the history provider and execution factory in the container is created (the extension does this on startup in the extension) + // This is necessary to get the appropriate live share services up and running. + ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); + ioc.get<IJupyterExecution>(IJupyterExecution); + ioc.get<IDebugLocationTracker>(IDebugLocationTracker); + + await ioc.activate(); + return ioc; + } + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + await ioc.dispose(); + lastErrorMessage = undefined; + for (const disposable of postDisposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + }); + + suiteTeardown(() => { + // asyncDump(); + }); + + async function debugCell( + type: 'notebook' | 'interactive', + code: string, + breakpoint?: Range, + breakpointFile?: string, + expectError?: boolean, + stepAndVerify?: () => void + ): Promise<void> { + // Create a dummy document with just this code + const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + const fileName = path.join(EXTENSION_ROOT_DIR, 'foo.py'); + docManager.addDocument(code, fileName); + + if (breakpoint) { + const sourceFile = breakpointFile ? path.join(EXTENSION_ROOT_DIR, breakpointFile) : fileName; + const sb: SourceBreakpoint = { + location: { + uri: Uri.file(sourceFile), + range: breakpoint + }, + id: uuid(), + enabled: true + }; + jupyterDebuggerService!.addBreakpoints([sb]); + } + + // Start the jupyter server + const history = await getOrCreateInteractiveWindow(ioc); + + const expectedBreakLine = breakpoint && !breakpointFile ? breakpoint.start.line : 2; // 2 because of the 'breakpoint()' that gets added + + // Debug this code. We should either hit the breakpoint or stop on entry + const resultPromise = getInteractiveCellResults(ioc, async () => { + let breakPromise = createDeferred<void>(); + + // Make sure that our code lens provider has signaled a change before we check the lenses + let newLensPromise = createDeferred<void>(); + let newLensDispose; + const codeLensProvider = ioc.serviceManager.get<IDataScienceCodeLensProvider>(IDataScienceCodeLensProvider); + if (codeLensProvider.onDidChangeCodeLenses) { + newLensDispose = codeLensProvider.onDidChangeCodeLenses(() => newLensPromise.resolve()); + } + + disposables.push(jupyterDebuggerService!.onBreakpointHit(() => breakPromise.resolve())); + const done = history.window.debugCode(code, Uri.file(fileName), 0, docManager.activeTextEditor); + await waitForPromise(Promise.race([done, breakPromise.promise]), 60000); + if (expectError) { + assert.ok(lastErrorMessage, 'Error did not occur when expected'); + throw Error('Exiting cell results'); + } else { + assert.ok(breakPromise.resolved, 'Breakpoint event did not fire'); + assert.ok(!lastErrorMessage, `Error occurred ${lastErrorMessage}`); + const stackFrames = await jupyterDebuggerService!.getStack(); + assert.ok(stackFrames, 'Stack trace not computable'); + assert.ok(stackFrames.length >= 1, 'Not enough frames'); + assert.equal(stackFrames[0].line, expectedBreakLine, 'Stopped on wrong line number'); + + await waitForPromise(newLensPromise.promise, 10_000); + + verifyCodeLenses(expectedBreakLine); + newLensDispose?.dispose(); + + // Step if allowed + if (stepAndVerify && ioc.getWrapper(type) && !ioc.mockJupyter) { + // Verify variables work. Native editor should already open the variable explorer + // automatically + if (type === 'interactive') { + openVariableExplorer(ioc.getWrapper(type)); + } + const mountedWebPanel = + type === 'notebook' ? ioc.getNativeWebPanel(undefined) : ioc.getInteractiveWebPanel(undefined); + breakPromise = createDeferred<void>(); + await jupyterDebuggerService?.step(); + await breakPromise.promise; + await mountedWebPanel.waitForMessage(InteractiveWindowMessages.VariablesComplete); + const variableRefresh = mountedWebPanel.waitForMessage(InteractiveWindowMessages.VariablesComplete); + await jupyterDebuggerService?.requestVariables(); + await variableRefresh; + + // Force an update so we render whatever the current state is + ioc.getWrapper(type).update(); + + // Then verify results. + stepAndVerify(); + } + + newLensPromise = createDeferred<void>(); + if (codeLensProvider.onDidChangeCodeLenses) { + newLensDispose = codeLensProvider.onDidChangeCodeLenses(() => newLensPromise.resolve()); + } + + // Verify break location + await jupyterDebuggerService!.continue(); + + await waitForPromise(newLensPromise.promise, 10_000); + + verifyCodeLenses(undefined); + newLensDispose?.dispose(); + } + }); + + if (!expectError) { + const cellResults = await resultPromise; + assert.ok(cellResults, 'No cell results after finishing debugging'); + } else { + try { + await resultPromise; + } catch { + noop(); + } + } + await history.window.dispose(); + } + + function verifyCodeLenses(expectedBreakLine: number | undefined) { + // We should have three debug code lenses which should all contain the break line + const codeLenses = getCodeLenses(); + + if (expectedBreakLine) { + assert.equal(codeLenses.length, 3, 'Incorrect number of debug code lenses stop'); + expect(codeLenses[0].command!.command).to.be.equal(Commands.DebugContinue); + expect(codeLenses[1].command!.command).to.be.equal(Commands.DebugStop); + expect(codeLenses[2].command!.command).to.be.equal(Commands.DebugStepOver); + codeLenses.forEach((codeLens) => { + assert.ok(codeLens.range.contains(new Position(expectedBreakLine - 1, 0))); + }); + } else { + // Two options, either we are in Debug-Run mode and expect no lenses. + // Or we are in Design mode and expect run - run below - debug cell - goto + if (codeLenses.length !== 4) { + assert.equal(codeLenses.length, 0, 'Incorrect number of debug code lenses debug - run'); + } else { + assert.equal(codeLenses.length, 4, 'Incorrect number of debug code lenses design after debug'); + expect(codeLenses[0].command!.command).to.be.equal(Commands.RunCell); + expect(codeLenses[1].command!.command).to.be.equal(Commands.RunCellAndAllBelow); + expect(codeLenses[2].command!.command).to.be.equal(Commands.DebugCell); + expect(codeLenses[3].command!.command).to.be.equal(Commands.ScrollToCell); + } + } + } + + function getCodeLenses(): CodeLens[] { + const documentManager = ioc.serviceManager.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + const codeLensProvider = ioc.serviceManager.get<IDataScienceCodeLensProvider>(IDataScienceCodeLensProvider); + const doc = documentManager.textDocuments[0]; + const result = codeLensProvider.provideCodeLenses(doc, CancellationToken.None); + // tslint:disable-next-line:no-any + if ((result as any).length) { + return result as CodeLens[]; + } + return []; + } + + runInteractiveTest( + 'Debug cell without breakpoint', + async () => { + await debugCell('interactive', '#%%\nprint("bar")'); + }, + createIOC + ); + runInteractiveTest( + 'Check variables', + async () => { + ioc.setExperimentState(RunByLine.experiment, true); + await debugCell('interactive', '#%%\nx = [4, 6]\nx = 5', undefined, undefined, false, () => { + const targetResult = { + name: 'x', + value: '[4, 6]', + supportsDataExplorer: true, + type: 'list', + size: 0, + shape: '', + count: 2, + truncated: false + }; + verifyVariables(ioc!.getWrapper('interactive')!, [targetResult]); + }); + }, + createIOC + ); + + runInteractiveTest( + 'Debug temporary file', + async () => { + const code = '#%%\nprint("bar")'; + + // Create a dummy document with just this code + const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + const fileName = 'Untitled-1'; + docManager.addDocument(code, fileName); + const mockDoc = docManager.textDocuments[0] as MockDocument; + mockDoc.forceUntitled(); + + // Start the jupyter server + const history = await getOrCreateInteractiveWindow(ioc); + const expectedBreakLine = 2; // 2 because of the 'breakpoint()' that gets added + + // Debug this code. We should either hit the breakpoint or stop on entry + const resultPromise = getInteractiveCellResults(ioc, async () => { + const breakPromise = createDeferred<void>(); + disposables.push(jupyterDebuggerService!.onBreakpointHit(() => breakPromise.resolve())); + const targetUri = Uri.file(fileName); + const done = history.window.debugCode(code, targetUri, 0, docManager.activeTextEditor); + await waitForPromise( + Promise.race([done, breakPromise.promise]), + ioc.getSettings().datascience.jupyterLaunchTimeout * 2 // Give restarts a chance + ); + assert.ok(breakPromise.resolved, 'Breakpoint event did not fire'); + assert.ok(!lastErrorMessage, `Error occurred ${lastErrorMessage}`); + const stackFrames = await jupyterDebuggerService!.getStack(); + assert.ok(stackFrames, 'Stack trace not computable'); + assert.ok(stackFrames.length >= 1, 'Not enough frames'); + assert.equal(stackFrames[0].line, expectedBreakLine, 'Stopped on wrong line number'); + assert.ok( + stackFrames[0].source!.path!.includes('baz.py'), + 'Stopped on wrong file name. Name should have been saved' + ); + // Verify break location + await jupyterDebuggerService!.continue(); + }); + + const cellResults = await resultPromise; + assert.ok(cellResults, 'No cell results after finishing debugging'); + await history.window.dispose(); + }, + createIOC + ); + + runNativeTest( + 'Run by line', + async () => { + // Create an editor so something is listening to messages + const ne = await createNewEditor(ioc); + const wrapper = ne.mount.wrapper; + + // Add a cell into the UI and wait for it to render and submit it. + await addCell(ne.mount, 'a=1\na', true); + + // Step into this cell using the button + let cell = getLastOutputCell(wrapper, 'NativeCell'); + let ImageButtons = cell.find(ImageButton); + assert.equal(ImageButtons.length, 7, 'Cell buttons not found'); + const runByLineButton = ImageButtons.at(3); + // tslint:disable-next-line: no-any + assert.equal((runByLineButton.instance().props as any).tooltip, 'Run by line'); + + const promise = ne.mount.waitForMessage(InteractiveWindowMessages.ShowingIp); + runByLineButton.simulate('click'); + await promise; + + // We should be in the break state. See if buttons indicate that or not + cell = getLastOutputCell(wrapper, 'NativeCell'); + ImageButtons = cell.find(ImageButton); + assert.equal(ImageButtons.length, 5, 'Cell buttons wrong number'); + }, + () => { + ioc.setExperimentState(RunByLine.experiment, true); + return createIOC(); + } + ); + runNativeTest( + 'Run by line state check', + async () => { + // Create an editor so something is listening to messages + const ne = await createNewEditor(ioc); + const wrapper = ne.mount.wrapper; + + // Add a cell into the UI and wait for it to render and submit it. + await addCell(ne.mount, 'a=1\na=2\na=3', true); + + // Step into this cell using the button + let cell = getLastOutputCell(wrapper, 'NativeCell'); + let ImageButtons = cell.find(ImageButton); + assert.equal(ImageButtons.length, 7, 'Cell buttons not found'); + const runByLineButton = ImageButtons.at(3); + // tslint:disable-next-line: no-any + assert.equal((runByLineButton.instance().props as any).tooltip, 'Run by line'); + + const promise = ne.mount.waitForMessage(InteractiveWindowMessages.DebugStateChange, { + withPayload: (p) => { + return p.oldState === DebugState.Design && p.newState === DebugState.Run; + } + }); + runByLineButton.simulate('click'); + await promise; + + // We should be running, is the run by line button disabled? + cell = getLastOutputCell(wrapper, 'NativeCell'); + ImageButtons = cell.find(ImageButton); + // tslint:disable-next-line: no-any + let runByLineButtonProps = ImageButtons.at(3).instance().props as any; + expect(runByLineButtonProps.disabled).to.equal(true, 'Run by line button not disabled when running'); + + // Now wait for break mode + const breakPromise = ne.mount.waitForMessage(InteractiveWindowMessages.DebugStateChange, { + withPayload: (p) => { + return p.oldState === DebugState.Run && p.newState === DebugState.Break; + } + }); + await breakPromise; + + cell = getLastOutputCell(wrapper, 'NativeCell'); + ImageButtons = cell.find(ImageButton); + // tslint:disable-next-line: no-any + runByLineButtonProps = ImageButtons.at(3).instance().props as any; + expect(runByLineButtonProps.disabled).to.equal(false, 'Run by line button not active in break mode'); + }, + () => { + ioc.setExperimentState(RunByLine.experiment, true); + return createIOC(); + } + ); +}); diff --git a/src/test/datascience/dsTestSetup.ts b/src/test/datascience/dsTestSetup.ts new file mode 100644 index 000000000000..d043a88ab276 --- /dev/null +++ b/src/test/datascience/dsTestSetup.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; +/** + * Modify package.json to ensure VSC Notebooks have been setup so tests can run. + * This is required because we modify package.json during runtime, hence we need to do the same thing for tests. + */ + +const packageJsonFile = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'package.json'); +const content = JSON.parse(fs.readFileSync(packageJsonFile).toString()); + +// This code is temporary. +if ( + !content.enableProposedApi || + !Array.isArray(content.contributes.notebookOutputRenderer) || + !Array.isArray(content.contributes.notebookProvider) +) { + content.enableProposedApi = true; + content.contributes.notebookOutputRenderer = [ + { + viewType: 'jupyter-notebook-renderer', + displayName: 'Jupyter Notebook Renderer', + mimeTypes: [ + 'application/geo+json', + 'application/vdom.v1+json', + 'application/vnd.dataresource+json', + 'application/vnd.plotly.v1+json', + 'application/vnd.vega.v2+json', + 'application/vnd.vega.v3+json', + 'application/vnd.vega.v4+json', + 'application/vnd.vega.v5+json', + 'application/vnd.vegalite.v1+json', + 'application/vnd.vegalite.v2+json', + 'application/vnd.vegalite.v3+json', + 'application/vnd.vegalite.v4+json', + 'application/x-nteract-model-debug+json', + 'image/gif', + 'image/png', + 'image/jpeg', + 'text/latex', + 'text/vnd.plotly.v1+html' + ] + } + ]; + content.contributes.notebookProvider = [ + { + viewType: 'jupyter-notebook', + displayName: 'Jupyter Notebook', + selector: [ + { + filenamePattern: '*.ipynb' + } + ] + } + ]; +} + +// Update package.json to pick experiments from our custom settings.json file. +content.contributes.configuration.properties['python.experiments.optInto'].scope = 'resource'; +content.contributes.configuration.properties['python.logging.level'].scope = 'resource'; + +fs.writeFileSync(packageJsonFile, JSON.stringify(content, undefined, 4)); diff --git a/src/test/datascience/editor-integration/cellhashprovider.unit.test.ts b/src/test/datascience/editor-integration/cellhashprovider.unit.test.ts new file mode 100644 index 000000000000..d59c2f92ca9b --- /dev/null +++ b/src/test/datascience/editor-integration/cellhashprovider.unit.test.ts @@ -0,0 +1,559 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Position, Range, Uri } from 'vscode'; + +import { IDebugService } from '../../../client/common/application/types'; +import { IConfigurationService, IDataScienceSettings, IPythonSettings } from '../../../client/common/types'; +import { CellHashProvider } from '../../../client/datascience/editor-integration/cellhashprovider'; +import { + CellState, + ICell, + ICellHashListener, + IDataScienceFileSystem, + IFileHashes +} from '../../../client/datascience/types'; +import { MockDocumentManager } from '../mockDocumentManager'; + +class HashListener implements ICellHashListener { + public lastHashes: IFileHashes[] = []; + + public async hashesUpdated(hashes: IFileHashes[]): Promise<void> { + this.lastHashes = hashes; + } +} + +// tslint:disable-next-line: max-func-body-length +suite('CellHashProvider Unit Tests', () => { + let hashProvider: CellHashProvider; + let documentManager: MockDocumentManager; + let configurationService: TypeMoq.IMock<IConfigurationService>; + let dataScienceSettings: TypeMoq.IMock<IDataScienceSettings>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let debugService: TypeMoq.IMock<IDebugService>; + let fileSystem: TypeMoq.IMock<IDataScienceFileSystem>; + const hashListener: HashListener = new HashListener(); + setup(() => { + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + dataScienceSettings = TypeMoq.Mock.ofType<IDataScienceSettings>(); + debugService = TypeMoq.Mock.ofType<IDebugService>(); + fileSystem = TypeMoq.Mock.ofType<IDataScienceFileSystem>(); + dataScienceSettings.setup((d) => d.enabled).returns(() => true); + pythonSettings.setup((p) => p.datascience).returns(() => dataScienceSettings.object); + configurationService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + debugService.setup((d) => d.activeDebugSession).returns(() => undefined); + fileSystem + .setup((d) => d.areLocalPathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) + .returns(() => true); + documentManager = new MockDocumentManager(); + hashProvider = new CellHashProvider( + documentManager, + configurationService.object, + debugService.object, + fileSystem.object, + [hashListener] + ); + }); + + function addSingleChange(file: string, range: Range, newText: string) { + documentManager.changeDocument(file, [{ range, newText }]); + } + + function sendCode(code: string, line: number, file?: string): Promise<void> { + const cell: ICell = { + file: Uri.file(file ? file : 'foo.py').fsPath, + line, + data: { + source: code, + cell_type: 'code', + metadata: {}, + outputs: [], + execution_count: 1 + }, + id: '1', + state: CellState.init + }; + return hashProvider.preExecute(cell, false); + } + + test('Add a cell and edit it', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; + const code = '#%%\r\nprint("bar")'; + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(code, 2); + + // We should have a single hash + let hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Edit the first cell, removing it + addSingleChange('foo.py', new Range(new Position(0, 0), new Position(2, 0)), ''); + + // Get our hashes again. The line number should change + // We should have a single hash + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 2, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 2, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + }); + + test('Add a cell, delete it, and recreate it', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; + const code = '#%%\r\nprint("bar")'; + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(code, 2); + + // We should have a single hash + let hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Change the second cell + addSingleChange('foo.py', new Range(new Position(3, 0), new Position(3, 0)), 'print ("bob")\r\n'); + + // Should be no hashes now + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 0, 'Hash should be gone'); + + // Undo the last change + addSingleChange('foo.py', new Range(new Position(3, 0), new Position(4, 0)), ''); + + // Hash should reappear + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + }); + + test('Delete code below', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")\r\n#%%\r\nprint("baz")'; + const code = '#%%\r\nprint("bar")'; + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(code, 2); + + // We should have a single hash + let hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Change the third cell + addSingleChange('foo.py', new Range(new Position(5, 0), new Position(5, 0)), 'print ("bob")\r\n'); + + // Should be the same hashes + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Delete the first cell + addSingleChange('foo.py', new Range(new Position(0, 0), new Position(2, 0)), ''); + + // Hash should move + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 2, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 2, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + }); + + test('Modify code after sending twice', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")\r\n#%%\r\nprint("baz")'; + const code = '#%%\r\nprint("bar")'; + const thirdCell = '#%%\r\nprint ("bob")\r\nprint("baz")'; + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(code, 2); + + // We should have a single hash + let hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Change the third cell + addSingleChange('foo.py', new Range(new Position(5, 0), new Position(5, 0)), 'print ("bob")\r\n'); + + // Send the third cell + await sendCode(thirdCell, 4); + + // Should be two hashes + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 2, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + assert.equal(hashes[0].hashes[1].line, 6, 'Wrong start line'); + assert.equal(hashes[0].hashes[1].endLine, 7, 'Wrong end line'); + assert.equal(hashes[0].hashes[1].executionCount, 2, 'Wrong execution count'); + + // Delete the first cell + addSingleChange('foo.py', new Range(new Position(0, 0), new Position(2, 0)), ''); + + // Hashes should move + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 2, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 2, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 2, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + assert.equal(hashes[0].hashes[1].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[1].endLine, 5, 'Wrong end line'); + assert.equal(hashes[0].hashes[1].executionCount, 2, 'Wrong execution count'); + }); + + test('Run same cell twice', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")\r\n#%%\r\nprint("baz")'; + const code = '#%%\r\nprint("bar")'; + const thirdCell = '#%%\r\nprint ("bob")\r\nprint("baz")'; + + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(code, 2); + + // Add a second cell + await sendCode(thirdCell, 4); + + // Add this code a second time + await sendCode(code, 2); + + // Execution count should go up, but still only have two cells. + const hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 2, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 3, 'Wrong execution count'); + assert.equal(hashes[0].hashes[1].line, 6, 'Wrong start line'); + assert.equal(hashes[0].hashes[1].endLine, 6, 'Wrong end line'); + assert.equal(hashes[0].hashes[1].executionCount, 2, 'Wrong execution count'); + }); + + test('Two files with same cells', async () => { + const file1 = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")\r\n#%%\r\nprint("baz")'; + const file2 = file1; + const code = '#%%\r\nprint("bar")'; + const thirdCell = '#%%\r\nprint ("bob")\r\nprint("baz")'; + + // Create our documents + documentManager.addDocument(file1, 'foo.py'); + documentManager.addDocument(file2, 'bar.py'); + + // Add this code + await sendCode(code, 2); + await sendCode(code, 2, 'bar.py'); + + // Add a second cell + await sendCode(thirdCell, 4); + + // Add this code a second time + await sendCode(code, 2); + + // Execution count should go up, but still only have two cells. + const hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 2, 'Wrong number of hashes'); + const fooHash = hashes.find((h) => h.file === Uri.file('foo.py').fsPath); + const barHash = hashes.find((h) => h.file === Uri.file('bar.py').fsPath); + assert.ok(fooHash, 'No hash for foo.py'); + assert.ok(barHash, 'No hash for bar.py'); + assert.equal(fooHash!.hashes.length, 2, 'Not enough hashes found'); + assert.equal(fooHash!.hashes[0].line, 4, 'Wrong start line'); + assert.equal(fooHash!.hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(fooHash!.hashes[0].executionCount, 4, 'Wrong execution count'); + assert.equal(fooHash!.hashes[1].line, 6, 'Wrong start line'); + assert.equal(fooHash!.hashes[1].endLine, 6, 'Wrong end line'); + assert.equal(fooHash!.hashes[1].executionCount, 3, 'Wrong execution count'); + assert.equal(barHash!.hashes.length, 1, 'Not enough hashes found'); + assert.equal(barHash!.hashes[0].line, 4, 'Wrong start line'); + assert.equal(barHash!.hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(barHash!.hashes[0].executionCount, 2, 'Wrong execution count'); + }); + + test('Delete cell with dupes in code, put cell back', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")\r\n#%%\r\nprint("baz")'; + const code = '#%%\r\nprint("foo")'; + + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(code, 2); + + // We should have a single hash + let hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Modify the code + addSingleChange('foo.py', new Range(new Position(3, 0), new Position(3, 1)), ''); + + // Should have zero hashes + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 0, 'Too many hashes found'); + + // Put back the original cell + addSingleChange('foo.py', new Range(new Position(3, 0), new Position(3, 0)), 'p'); + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Modify the code + addSingleChange('foo.py', new Range(new Position(3, 0), new Position(3, 1)), ''); + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 0, 'Too many hashes found'); + + // Remove the first cell + addSingleChange('foo.py', new Range(new Position(0, 0), new Position(2, 0)), ''); + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 0, 'Too many hashes found'); + + // Put back the original cell + addSingleChange('foo.py', new Range(new Position(1, 0), new Position(1, 0)), 'p'); + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 2, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 2, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + }); + + test('Add a cell and edit different parts of it', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; + const code = '#%%\r\nprint("bar")'; + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(code, 2); + + // We should have a single hash + const hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Edit the cell we added + addSingleChange('foo.py', new Range(new Position(2, 0), new Position(2, 0)), '#'); + assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); + addSingleChange('foo.py', new Range(new Position(2, 0), new Position(2, 1)), ''); + assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); + addSingleChange('foo.py', new Range(new Position(2, 0), new Position(2, 1)), ''); + assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); + addSingleChange('foo.py', new Range(new Position(2, 0), new Position(2, 0)), '#'); + assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); + addSingleChange('foo.py', new Range(new Position(2, 1), new Position(2, 2)), ''); + assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); + addSingleChange('foo.py', new Range(new Position(2, 1), new Position(2, 1)), '%'); + assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); + addSingleChange('foo.py', new Range(new Position(2, 2), new Position(2, 3)), ''); + assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); + addSingleChange('foo.py', new Range(new Position(2, 2), new Position(2, 2)), '%'); + assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); + addSingleChange('foo.py', new Range(new Position(2, 3), new Position(2, 4)), ''); + assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); + addSingleChange('foo.py', new Range(new Position(2, 3), new Position(2, 3)), '\r'); + assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); + addSingleChange('foo.py', new Range(new Position(2, 4), new Position(2, 5)), ''); + assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); + addSingleChange('foo.py', new Range(new Position(2, 4), new Position(2, 4)), '\n'); + assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); + }); + + test('Add a cell and edit it to be exactly the same', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; + const code = '#%%\r\nprint("bar")'; + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(code, 2); + + // We should have a single hash + let hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Replace with the same cell + addSingleChange('foo.py', new Range(new Position(0, 0), new Position(4, 0)), file); + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); + }); + + test('Add a cell and edit it to not be exactly the same', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; + const file2 = '#%%\r\nprint("fooze")\r\n#%%\r\nprint("bar")'; + const code = '#%%\r\nprint("bar")'; + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(code, 2); + + // We should have a single hash + let hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Replace with the new code + addSingleChange('foo.py', new Range(new Position(0, 0), new Position(4, 0)), file2); + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 0, 'Hashes should be gone'); + + // Put back old code + addSingleChange('foo.py', new Range(new Position(0, 0), new Position(4, 0)), file); + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + }); + + test('Apply multiple edits at once', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; + const code = '#%%\r\nprint("bar")'; + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(code, 2); + + // We should have a single hash + let hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Apply a couple of edits at once + documentManager.changeDocument('foo.py', [ + { + range: new Range(new Position(0, 0), new Position(0, 0)), + newText: '#%%\r\nprint("new cell")\r\n' + }, + { + range: new Range(new Position(0, 0), new Position(0, 0)), + newText: '#%%\r\nprint("new cell")\r\n' + } + ]); + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 8, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 8, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + documentManager.changeDocument('foo.py', [ + { + range: new Range(new Position(0, 0), new Position(0, 0)), + newText: '#%%\r\nprint("new cell")\r\n' + }, + { + range: new Range(new Position(0, 0), new Position(2, 0)), + newText: '' + } + ]); + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 8, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 8, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + }); + + test('Restart kernel', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; + const code = '#%%\r\nprint("bar")'; + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(code, 2); + + // We should have a single hash + let hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + + // Restart the kernel + hashProvider.onKernelRestarted(); + + hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 0, 'Restart should have cleared'); + }); + + test('More than one cell in range', async () => { + const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; + // Create our document + documentManager.addDocument(file, 'foo.py'); + + // Add this code + await sendCode(file, 0); + + // We should have a single hash + const hashes = hashProvider.getHashes(); + assert.equal(hashes.length, 1, 'No hashes found'); + assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); + assert.equal(hashes[0].hashes[0].line, 2, 'Wrong start line'); + assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); + assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); + }); +}); diff --git a/src/test/datascience/editor-integration/codelensprovider.unit.test.ts b/src/test/datascience/editor-integration/codelensprovider.unit.test.ts new file mode 100644 index 000000000000..3733e518ea41 --- /dev/null +++ b/src/test/datascience/editor-integration/codelensprovider.unit.test.ts @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as TypeMoq from 'typemoq'; +import { CancellationTokenSource, Disposable, TextDocument, Uri } from 'vscode'; + +import { + ICommandManager, + IDebugService, + IDocumentManager, + IVSCodeNotebook +} from '../../../client/common/application/types'; +import { IConfigurationService, IDataScienceSettings, IPythonSettings } from '../../../client/common/types'; +import { DataScienceCodeLensProvider } from '../../../client/datascience/editor-integration/codelensprovider'; +import { + ICodeWatcher, + IDataScienceCodeLensProvider, + IDataScienceFileSystem, + IDebugLocationTracker +} from '../../../client/datascience/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable-next-line: max-func-body-length +suite('DataScienceCodeLensProvider Unit Tests', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let configurationService: TypeMoq.IMock<IConfigurationService>; + let codeLensProvider: IDataScienceCodeLensProvider; + let dataScienceSettings: TypeMoq.IMock<IDataScienceSettings>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let documentManager: TypeMoq.IMock<IDocumentManager>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let debugService: TypeMoq.IMock<IDebugService>; + let debugLocationTracker: TypeMoq.IMock<IDebugLocationTracker>; + let fileSystem: TypeMoq.IMock<IDataScienceFileSystem>; + let tokenSource: CancellationTokenSource; + let vscodeNotebook: TypeMoq.IMock<IVSCodeNotebook>; + const disposables: Disposable[] = []; + + setup(() => { + tokenSource = new CancellationTokenSource(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + debugService = TypeMoq.Mock.ofType<IDebugService>(); + debugLocationTracker = TypeMoq.Mock.ofType<IDebugLocationTracker>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + dataScienceSettings = TypeMoq.Mock.ofType<IDataScienceSettings>(); + fileSystem = TypeMoq.Mock.ofType<IDataScienceFileSystem>(); + vscodeNotebook = TypeMoq.Mock.ofType<IVSCodeNotebook>(); + dataScienceSettings.setup((d) => d.enabled).returns(() => true); + pythonSettings.setup((p) => p.datascience).returns(() => dataScienceSettings.object); + configurationService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + vscodeNotebook.setup((c) => c.activeNotebookEditor).returns(() => undefined); + commandManager + .setup((c) => c.executeCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve()); + debugService.setup((d) => d.activeDebugSession).returns(() => undefined); + fileSystem + .setup((f) => f.areLocalPathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((a, b) => { + return a.toLowerCase() === b.toLowerCase(); + }); + + codeLensProvider = new DataScienceCodeLensProvider( + serviceContainer.object, + debugLocationTracker.object, + documentManager.object, + configurationService.object, + commandManager.object, + disposables, + debugService.object, + fileSystem.object, + vscodeNotebook.object + ); + }); + + test('Initialize Code Lenses one document', () => { + // Create our document + const document = TypeMoq.Mock.ofType<TextDocument>(); + document.setup((d) => d.fileName).returns(() => 'test.py'); + document.setup((d) => d.version).returns(() => 1); + + const targetCodeWatcher = TypeMoq.Mock.ofType<ICodeWatcher>(); + targetCodeWatcher + .setup((tc) => tc.getCodeLenses()) + .returns(() => []) + .verifiable(TypeMoq.Times.once()); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICodeWatcher))) + .returns(() => targetCodeWatcher.object) + .verifiable(TypeMoq.Times.once()); + documentManager.setup((d) => d.textDocuments).returns(() => [document.object]); + + codeLensProvider.provideCodeLenses(document.object, tokenSource.token); + + targetCodeWatcher.verifyAll(); + serviceContainer.verifyAll(); + }); + + test('Initialize Code Lenses same doc called', () => { + // Create our document + const document = TypeMoq.Mock.ofType<TextDocument>(); + const uri = Uri.file('test.py'); + document.setup((d) => d.fileName).returns(() => uri.fsPath); + document.setup((d) => d.version).returns(() => 1); + + const targetCodeWatcher = TypeMoq.Mock.ofType<ICodeWatcher>(); + targetCodeWatcher + .setup((tc) => tc.getCodeLenses()) + .returns(() => []) + .verifiable(TypeMoq.Times.exactly(2)); + targetCodeWatcher.setup((tc) => tc.uri).returns(() => uri); + targetCodeWatcher.setup((tc) => tc.getVersion()).returns(() => 1); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICodeWatcher))) + .returns(() => { + return targetCodeWatcher.object; + }) + .verifiable(TypeMoq.Times.once()); + documentManager.setup((d) => d.textDocuments).returns(() => [document.object]); + + codeLensProvider.provideCodeLenses(document.object, tokenSource.token); + codeLensProvider.provideCodeLenses(document.object, tokenSource.token); + + // getCodeLenses should be called twice, but getting the code watcher only once due to same doc + targetCodeWatcher.verifyAll(); + serviceContainer.verifyAll(); + }); + test('Should not Initialize Code Lenses when a Native Notebook is open', () => { + // Create our document + const document = TypeMoq.Mock.ofType<TextDocument>(); + document.setup((d) => d.fileName).returns(() => 'test.py'); + document.setup((d) => d.version).returns(() => 1); + vscodeNotebook.reset(); + // tslint:disable-next-line: no-any + vscodeNotebook.setup((c) => c.activeNotebookEditor).returns(() => ({} as any)); + + const targetCodeWatcher = TypeMoq.Mock.ofType<ICodeWatcher>(); + targetCodeWatcher + .setup((tc) => tc.getCodeLenses()) + .returns(() => []) + .verifiable(TypeMoq.Times.never()); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICodeWatcher))) + .returns(() => targetCodeWatcher.object) + .verifiable(TypeMoq.Times.never()); + documentManager.setup((d) => d.textDocuments).returns(() => [document.object]); + + codeLensProvider.provideCodeLenses(document.object, tokenSource.token); + codeLensProvider.provideCodeLenses(document.object, tokenSource.token); + + // getCodeLenses should be called twice, but getting the code watcher only once due to same doc + targetCodeWatcher.verifyAll(); + serviceContainer.verifyAll(); + }); + + test('Initialize Code Lenses new name / version', () => { + // Create our document + const document = TypeMoq.Mock.ofType<TextDocument>(); + document.setup((d) => d.fileName).returns(() => 'test.py'); + document.setup((d) => d.version).returns(() => 1); + + const document2 = TypeMoq.Mock.ofType<TextDocument>(); + document2.setup((d) => d.fileName).returns(() => 'test2.py'); + document2.setup((d) => d.version).returns(() => 1); + + const document3 = TypeMoq.Mock.ofType<TextDocument>(); + document3.setup((d) => d.fileName).returns(() => 'test.py'); + document3.setup((d) => d.version).returns(() => 2); + + const targetCodeWatcher = TypeMoq.Mock.ofType<ICodeWatcher>(); + targetCodeWatcher + .setup((tc) => tc.getCodeLenses()) + .returns(() => []) + .verifiable(TypeMoq.Times.exactly(3)); + targetCodeWatcher.setup((tc) => tc.uri).returns(() => Uri.file('test.py')); + targetCodeWatcher.setup((tc) => tc.getVersion()).returns(() => 1); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICodeWatcher))) + .returns(() => targetCodeWatcher.object) + .verifiable(TypeMoq.Times.exactly(3)); + documentManager + .setup((d) => d.textDocuments) + .returns(() => [document.object, document2.object, document3.object]); + + codeLensProvider.provideCodeLenses(document.object, tokenSource.token); + codeLensProvider.provideCodeLenses(document2.object, tokenSource.token); + codeLensProvider.provideCodeLenses(document3.object, tokenSource.token); + + // service container get should be called three times as the names and versions don't match + targetCodeWatcher.verifyAll(); + serviceContainer.verifyAll(); + }); +}); diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts new file mode 100644 index 000000000000..a632b46c14a4 --- /dev/null +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -0,0 +1,2263 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +// tslint:disable:max-func-body-length no-trailing-whitespace no-multiline-string chai-vague-errors no-unused-expression +// Disable whitespace / multiline as we use that to pass in our fake file strings +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { CancellationTokenSource, CodeLens, Disposable, EventEmitter, Range, Selection, TextEditor, Uri } from 'vscode'; + +import { instance, mock, when } from 'ts-mockito'; +import { + ICommandManager, + IDebugService, + IDocumentManager, + IVSCodeNotebook +} from '../../../client/common/application/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { Commands, EditorContexts } from '../../../client/datascience/constants'; +import { CodeLensFactory } from '../../../client/datascience/editor-integration/codeLensFactory'; +import { DataScienceCodeLensProvider } from '../../../client/datascience/editor-integration/codelensprovider'; +import { CodeWatcher } from '../../../client/datascience/editor-integration/codewatcher'; +import { NotebookProvider } from '../../../client/datascience/interactive-common/notebookProvider'; +import { + ICodeWatcher, + IDataScienceErrorHandler, + IDataScienceFileSystem, + IDebugLocationTracker, + IInteractiveWindow, + IInteractiveWindowProvider, + INotebook +} from '../../../client/datascience/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ICodeExecutionHelper } from '../../../client/terminals/types'; +import { MockAutoSelectionService } from '../../mocks/autoSelector'; +import { MockDocumentManager } from '../mockDocumentManager'; +import { MockPythonSettings } from '../mockPythonSettings'; +import { MockEditor } from '../mockTextEditor'; +import { createDocument } from './helpers'; + +//tslint:disable:no-any + +function initializeMockTextEditor( + codeWatcher: CodeWatcher, + documentManager: TypeMoq.IMock<IDocumentManager>, + inputText: string +): MockEditor { + const fileName = Uri.file('test.py').fsPath; + const version = 1; + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); + codeWatcher.setDocument(document.object); + + // For this test we need to set up a document selection point + // TypeMoq does not play well with setting properties on editor + const mockDocumentManager = new MockDocumentManager(); + const mockDocument = mockDocumentManager.addDocument(inputText, fileName); + const mockTextEditor = new MockEditor(mockDocumentManager, mockDocument); + documentManager.reset(); + documentManager.setup((dm) => dm.activeTextEditor).returns(() => mockTextEditor); + mockTextEditor.selection = new Selection(0, 0, 0, 0); + return mockTextEditor; +} + +suite('DataScience Code Watcher Unit Tests', () => { + let codeWatcher: CodeWatcher; + let interactiveWindowProvider: TypeMoq.IMock<IInteractiveWindowProvider>; + let activeInteractiveWindow: TypeMoq.IMock<IInteractiveWindow>; + let documentManager: TypeMoq.IMock<IDocumentManager>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let textEditor: TypeMoq.IMock<TextEditor>; + let fileSystem: TypeMoq.IMock<IDataScienceFileSystem>; + let configService: TypeMoq.IMock<IConfigurationService>; + let dataScienceErrorHandler: TypeMoq.IMock<IDataScienceErrorHandler>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let helper: TypeMoq.IMock<ICodeExecutionHelper>; + let tokenSource: CancellationTokenSource; + let debugService: TypeMoq.IMock<IDebugService>; + let debugLocationTracker: TypeMoq.IMock<IDebugLocationTracker>; + let vscodeNotebook: TypeMoq.IMock<IVSCodeNotebook>; + const contexts: Map<string, boolean> = new Map<string, boolean>(); + const pythonSettings = new MockPythonSettings(undefined, new MockAutoSelectionService()); + const disposables: Disposable[] = []; + + setup(() => { + tokenSource = new CancellationTokenSource(); + interactiveWindowProvider = TypeMoq.Mock.ofType<IInteractiveWindowProvider>(); + activeInteractiveWindow = createTypeMoq<IInteractiveWindow>('history'); + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + textEditor = TypeMoq.Mock.ofType<TextEditor>(); + fileSystem = TypeMoq.Mock.ofType<IDataScienceFileSystem>(); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + debugLocationTracker = TypeMoq.Mock.ofType<IDebugLocationTracker>(); + helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + debugService = TypeMoq.Mock.ofType<IDebugService>(); + vscodeNotebook = TypeMoq.Mock.ofType<IVSCodeNotebook>(); + + // Setup default settings + pythonSettings.datascience = { + allowImportFromNotebook: true, + alwaysTrustNotebooks: true, + jupyterLaunchTimeout: 20000, + jupyterLaunchRetries: 3, + enabled: true, + jupyterServerURI: 'local', + notebookFileRoot: 'WORKSPACE', + changeDirOnImportExport: true, + useDefaultConfigForJupyter: true, + jupyterInterruptTimeout: 10000, + searchForJupyter: true, + showCellInputCode: true, + collapseCellInputCodeByDefault: true, + allowInput: true, + maxOutputSize: 400, + enableScrollingForCellOutputs: true, + errorBackgroundColor: '#FFFFFF', + sendSelectionToInteractiveWindow: false, + variableExplorerExclude: 'module;function;builtin_function_or_method', + codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', + markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', + enableCellCodeLens: true, + enablePlotViewer: true, + runStartupCommands: '', + debugJustMyCode: true, + variableQueries: [], + jupyterCommandLineArguments: [], + widgetScriptSources: [], + interactiveWindowMode: 'single' + }; + debugService.setup((d) => d.activeDebugSession).returns(() => undefined); + vscodeNotebook.setup((d) => d.activeNotebookEditor).returns(() => undefined); + + // Setup the service container to return code watchers + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + + // Setup the file system + fileSystem.setup((f) => f.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => true); + + // Setup config service + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings); + + const dummyEvent = new EventEmitter<{ identity: Uri; notebook: INotebook }>(); + const notebookProvider = mock(NotebookProvider); + when((notebookProvider as any).then).thenReturn(undefined); + when(notebookProvider.onNotebookCreated).thenReturn(dummyEvent.event); + + const codeLensFactory = new CodeLensFactory( + configService.object, + instance(notebookProvider), + fileSystem.object, + documentManager.object + ); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICodeWatcher))) + .returns( + () => + new CodeWatcher( + interactiveWindowProvider.object, + fileSystem.object, + configService.object, + documentManager.object, + helper.object, + dataScienceErrorHandler.object, + codeLensFactory + ) + ); + + // Setup our error handler + dataScienceErrorHandler = TypeMoq.Mock.ofType<IDataScienceErrorHandler>(); + + // Setup our active history instance + interactiveWindowProvider + .setup((h) => h.getOrCreate(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(activeInteractiveWindow.object)); + + // Setup our active text editor + documentManager.setup((dm) => dm.activeTextEditor).returns(() => textEditor.object); + + commandManager + .setup((c) => c.executeCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((c, n, v) => { + if (c === 'setContext') { + contexts.set(n, v); + } + return Promise.resolve(); + }); + + codeWatcher = new CodeWatcher( + interactiveWindowProvider.object, + fileSystem.object, + configService.object, + documentManager.object, + helper.object, + dataScienceErrorHandler.object, + codeLensFactory + ); + }); + + function createTypeMoq<T>(tag: string): TypeMoq.IMock<T> { + // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class + // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 + const result: TypeMoq.IMock<T> = TypeMoq.Mock.ofType<T>(); + (result as any).tag = tag; + result.setup((x: any) => x.then).returns(() => undefined); + return result; + } + + function verifyCodeLensesAtPosition( + codeLenses: CodeLens[], + startLensIndex: number, + targetRange: Range, + firstCell: boolean = false, + markdownCell: boolean = false + ) { + if (codeLenses[startLensIndex].command) { + expect(codeLenses[startLensIndex].command!.command).to.be.equal( + Commands.RunCell, + 'Run Cell code lens command incorrect' + ); + } + expect(codeLenses[startLensIndex].range).to.be.deep.equal(targetRange, 'Run Cell code lens range incorrect'); + + if (!firstCell) { + if (codeLenses[startLensIndex + 1].command) { + expect(codeLenses[startLensIndex + 1].command!.command).to.be.equal( + Commands.RunAllCellsAbove, + 'Run Above code lens command incorrect' + ); + } + expect(codeLenses[startLensIndex + 1].range).to.be.deep.equal( + targetRange, + 'Run Above code lens range incorrect' + ); + } + + if (!markdownCell) { + const indexAdd = 2; + if (codeLenses[startLensIndex + indexAdd].command) { + expect(codeLenses[startLensIndex + indexAdd].command!.command).to.be.equal( + Commands.DebugCell, + 'Debug command incorrect' + ); + } + expect(codeLenses[startLensIndex + indexAdd].range).to.be.deep.equal( + targetRange, + 'Debug code lens range incorrect' + ); + + // Debugger mode commands + if (codeLenses[startLensIndex + indexAdd + 1].command) { + expect(codeLenses[startLensIndex + indexAdd + 1].command!.command).to.be.equal( + Commands.DebugContinue, + 'Debug command incorrect' + ); + } + expect(codeLenses[startLensIndex + indexAdd + 1].range).to.be.deep.equal( + targetRange, + 'Debug code lens range incorrect' + ); + if (codeLenses[startLensIndex + indexAdd + 2].command) { + expect(codeLenses[startLensIndex + indexAdd + 2].command!.command).to.be.equal( + Commands.DebugStop, + 'Debug command incorrect' + ); + } + expect(codeLenses[startLensIndex + indexAdd + 2].range).to.be.deep.equal( + targetRange, + 'Debug code lens range incorrect' + ); + if (codeLenses[startLensIndex + indexAdd + 3].command) { + expect(codeLenses[startLensIndex + indexAdd + 3].command!.command).to.be.equal( + Commands.DebugStepOver, + 'Debug command incorrect' + ); + } + expect(codeLenses[startLensIndex + indexAdd + 3].range).to.be.deep.equal( + targetRange, + 'Debug code lens range incorrect' + ); + } + } + + test('Add a file with just a #%% mark to a code watcher', () => { + const fileName = Uri.file('test.py').fsPath; + const version = 1; + const inputText = `#%%`; + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Verify meta data + expect(codeWatcher.uri?.fsPath).to.be.equal(fileName, 'File name of CodeWatcher does not match'); + expect(codeWatcher.getVersion()).to.be.equal(version, 'File version of CodeWatcher does not match'); + + // Verify code lenses + const codeLenses = codeWatcher.getCodeLenses(); + expect(codeLenses.length).to.be.equal(6, 'Incorrect count of code lenses'); + verifyCodeLensesAtPosition(codeLenses, 0, new Range(0, 0, 0, 3), true); + + // Verify function calls + document.verifyAll(); + }); + + test('Add a file without a mark to a code watcher', () => { + const fileName = Uri.file('test.py').fsPath; + const version = 1; + const inputText = `dummy`; + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Verify meta data + expect(codeWatcher.uri?.fsPath).to.be.equal(fileName, 'File name of CodeWatcher does not match'); + expect(codeWatcher.getVersion()).to.be.equal(version, 'File version of CodeWatcher does not match'); + + // Verify code lenses + const codeLenses = codeWatcher.getCodeLenses(); + expect(codeLenses.length).to.be.equal(0, 'Incorrect count of code lenses'); + + // Verify function calls + document.verifyAll(); + }); + + test('Add a file with multiple marks to a code watcher', () => { + const fileName = Uri.file('test.py').fsPath; + const version = 1; + const inputText = `first line +second line + +#%% +third line + +#%% +fourth line`; + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Verify meta data + expect(codeWatcher.uri?.fsPath).to.be.equal(fileName, 'File name of CodeWatcher does not match'); + expect(codeWatcher.getVersion()).to.be.equal(version, 'File version of CodeWatcher does not match'); + + // Verify code lenses + const codeLenses = codeWatcher.getCodeLenses(); + expect(codeLenses.length).to.be.equal(12, 'Incorrect count of code lenses'); + + verifyCodeLensesAtPosition(codeLenses, 0, new Range(3, 0, 5, 0), true); + verifyCodeLensesAtPosition(codeLenses, 6, new Range(6, 0, 7, 11)); + + // Verify function calls + document.verifyAll(); + }); + + test('Add a file with custom marks to a code watcher', () => { + const fileName = Uri.file('test.py').fsPath; + const version = 1; + const inputText = `first line +second line + +# <foobar> +third line + +# <baz> +fourth line + +# <mymarkdown> +# fifth line`; + pythonSettings.datascience.codeRegularExpression = '(#\\s*\\<foobar\\>|#\\s*\\<baz\\>)'; + pythonSettings.datascience.markdownRegularExpression = '(#\\s*\\<markdowncell\\>|#\\s*\\<mymarkdown\\>)'; + + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Verify meta data + expect(codeWatcher.uri?.fsPath).to.be.equal(fileName, 'File name of CodeWatcher does not match'); + expect(codeWatcher.getVersion()).to.be.equal(version, 'File version of CodeWatcher does not match'); + + // Verify code lenses + const codeLenses = codeWatcher.getCodeLenses(); + expect(codeLenses.length).to.be.equal(14, 'Incorrect count of code lenses'); + + verifyCodeLensesAtPosition(codeLenses, 0, new Range(3, 0, 5, 0), true); + verifyCodeLensesAtPosition(codeLenses, 6, new Range(6, 0, 8, 0)); + verifyCodeLensesAtPosition(codeLenses, 12, new Range(9, 0, 10, 12), false, true); + + // Verify function calls + document.verifyAll(); + }); + + test('Make sure invalid regex from a user still work', () => { + const fileName = Uri.file('test.py').fsPath; + const version = 1; + const inputText = `first line +second line + +# <codecell> +third line + +# <codecell> +fourth line + +# <mymarkdown> +# fifth line`; + pythonSettings.datascience.codeRegularExpression = '# * code cell)'; + pythonSettings.datascience.markdownRegularExpression = '(#\\s*\\<markdowncell\\>|#\\s*\\<mymarkdown\\>)'; + + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Verify meta data + expect(codeWatcher.uri?.fsPath).to.be.equal(fileName, 'File name of CodeWatcher does not match'); + expect(codeWatcher.getVersion()).to.be.equal(version, 'File version of CodeWatcher does not match'); + + // Verify code lenses + const codeLenses = codeWatcher.getCodeLenses(); + expect(codeLenses.length).to.be.equal(14, 'Incorrect count of code lenses'); + + verifyCodeLensesAtPosition(codeLenses, 0, new Range(3, 0, 5, 0), true); + verifyCodeLensesAtPosition(codeLenses, 6, new Range(6, 0, 8, 0)); + verifyCodeLensesAtPosition(codeLenses, 12, new Range(9, 0, 10, 12), false, true); + + // Verify function calls + document.verifyAll(); + }); + + test('Test the RunCell command', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const testString = '#%%\ntesting'; + const document = createDocument(testString, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + const testRange = new Range(0, 0, 1, 7); + + codeWatcher.setDocument(document.object); + + // Set up our expected call to add code + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue(testString), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + // Try our RunCell command + await codeWatcher.runCell(testRange); + + // Verify function calls + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test the RunFileInteractive command', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = `#%% +testing1 +#%% +testing2`; + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce()); + + document + .setup((doc) => doc.getText()) + .returns(() => inputText) + .verifiable(TypeMoq.Times.exactly(1)); + + codeWatcher.setDocument(document.object); + + // Set up our expected calls to add code + // RunFileInteractive should run the entire file in one block, not cell by cell like RunAllCells + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue(inputText), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + await codeWatcher.runFileInteractive(); + + // Verify function calls + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test the RunAllCells command', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = `testing0 +#%% +testing1 +#%% +testing2`; + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Set up our expected calls to add code + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue('testing0\n#%%\ntesting1'), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue('#%%\ntesting2'), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(3), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + await codeWatcher.runAllCells(); + + // Verify function calls + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test the RunCurrentCell command', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = `#%% +testing1 +#%% +testing2`; + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Set up our expected calls to add code + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue('#%%\ntesting2'), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + // For this test we need to set up a document selection point + textEditor.setup((te) => te.selection).returns(() => new Selection(2, 0, 2, 0)); + + await codeWatcher.runCurrentCell(); + + // Verify function calls + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test the RunCellAndAllBelow command', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = `#%% +testing1 +#%% +testing2 +#%% +testing3`; + const targetText1 = `#%% +testing2`; + + const targetText2 = `#%% +testing3`; + + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Set up our expected calls to add code + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue(targetText1), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue(targetText2), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(4), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + await codeWatcher.runCellAndAllBelow(2, 0); + + // Verify function calls + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test the RunAllCellsAbove command', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = `testing0 +#%% +testing1 +#%% +testing2 +#%% +testing3`; + const targetText1 = `testing0 +#%% +testing1`; + + const targetText2 = `#%% +testing2`; + + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Set up our expected calls to add code + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue(targetText1), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(1), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue(targetText2), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(3), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + await codeWatcher.runAllCellsAbove(4, 0); + + // Verify function calls + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test the RunToLine command', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = `#%% +testing1 +#%% +testing2 +#%% +testing3`; + const targetText = `#%% +testing1`; + + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Set up our expected calls to add code + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue(targetText), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + await codeWatcher.runToLine(2); + + // Verify function calls + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test the RunToLine command with nothing on the lines', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = ` + +print('testing')`; + + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // If adding empty lines nothing should be added and history should not be started + interactiveWindowProvider + .setup((h) => h.getOrCreate(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(activeInteractiveWindow.object)) + .verifiable(TypeMoq.Times.never()); + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isAny(), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isAnyNumber(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.never()); + + await codeWatcher.runToLine(2); + + // Verify function calls + interactiveWindowProvider.verifyAll(); + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test the RunFromLine command', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = `#%% +testing1 +#%% +testing2 +#%% +testing3`; + const targetText = `#%% +testing2 +#%% +testing3`; + + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Set up our expected calls to add code + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue(targetText), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + // Try our RunCell command with the first selection point + await codeWatcher.runFromLine(2); + + // Verify function calls + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test the RunSelection command', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = `#%% +testing1 +#%% +testing2`; + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + helper + .setup((h) => + h.getSelectedTextToExecute( + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }) + ) + ) + .returns(() => Promise.resolve('testing2')); + helper.setup((h) => h.normalizeLines(TypeMoq.It.isAny())).returns(() => Promise.resolve('testing2')); + + // Set up our expected calls to add code + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue('testing2'), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(3), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + // For this test we need to set up a document selection point + textEditor.setup((te) => te.document).returns(() => document.object); + textEditor.setup((te) => te.selection).returns(() => new Selection(3, 0, 3, 0)); + + // Try our RunCell command with the first selection point + await codeWatcher.runSelectionOrLine(textEditor.object); + + // Verify function calls + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test the RunCellAndAdvance command with next cell', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = `#%% +testing1 +#%% +testing2`; + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Set up our expected calls to add code + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue('#%%\ntesting1'), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + // For this test we need to set up a document selection point + const selection = new Selection(0, 0, 0, 0); + textEditor.setup((te) => te.selection).returns(() => selection); + + //textEditor.setup(te => te.selection = TypeMoq.It.isAny()).verifiable(TypeMoq.Times.once()); + //textEditor.setup(te => te.selection = TypeMoq.It.isAnyObject<Selection>(Selection)); + // Would be good to check that selection was set, but TypeMoq doesn't seem to like + // both getting and setting an object property. isAnyObject is not valid for this class + // and is or isAny overwrite the previous property getter if used. Will verify selection set + // in functional test + // https://github.com/florinn/typemoq/issues/107 + + // To get around this, override the advanceToRange function called from within runCurrentCellAndAdvance + // this will tell us if we are calling the correct range + (codeWatcher as any).advanceToRange = (targetRange: Range) => { + expect(targetRange.start.line).is.equal(2, 'Incorrect range in run cell and advance'); + expect(targetRange.start.character).is.equal(0, 'Incorrect range in run cell and advance'); + expect(targetRange.end.line).is.equal(3, 'Incorrect range in run cell and advance'); + expect(targetRange.end.character).is.equal(8, 'Incorrect range in run cell and advance'); + }; + + await codeWatcher.runCurrentCellAndAdvance(); + + // Verify function calls + textEditor.verifyAll(); + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('CodeLens returned after settings changed is different', () => { + // Create our document + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = '#%% foobar'; + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce()); + document.setup((doc) => doc.getText()).returns(() => inputText); + documentManager.setup((d) => d.textDocuments).returns(() => [document.object]); + const codeLensProvider = new DataScienceCodeLensProvider( + serviceContainer.object, + debugLocationTracker.object, + documentManager.object, + configService.object, + commandManager.object, + disposables, + debugService.object, + fileSystem.object, + vscodeNotebook.object + ); + + let result = codeLensProvider.provideCodeLenses(document.object, tokenSource.token); + expect(result, 'result not okay').to.be.ok; + let codeLens = result as CodeLens[]; + expect(codeLens.length).to.equal(3, 'Code lens wrong length - initial'); + + expect(contexts.get(EditorContexts.HasCodeCells)).to.be.equal(true, 'Code cells context not set'); + + // Change settings + pythonSettings.datascience.codeRegularExpression = '#%%%.*dude'; + pythonSettings.fireChangeEvent(); + result = codeLensProvider.provideCodeLenses(document.object, tokenSource.token); + expect(result, 'result not okay').to.be.ok; + codeLens = result as CodeLens[]; + expect(codeLens.length).to.equal(0, 'Code lens wrong length'); + + expect(contexts.get(EditorContexts.HasCodeCells)).to.be.equal(false, 'Code cells context not set'); + + // Change settings to empty + pythonSettings.datascience.codeRegularExpression = ''; + pythonSettings.fireChangeEvent(); + result = codeLensProvider.provideCodeLenses(document.object, tokenSource.token); + expect(result, 'result not okay').to.be.ok; + codeLens = result as CodeLens[]; + expect(codeLens.length).to.equal(3, 'Code lens wrong length - final'); + }); + + test('Test the RunAllCellsAbove command with an error', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = `#%% +testing1 +#%% +testing2 +#%% +testing3`; + const targetText1 = `#%% +testing1`; + + const targetText2 = `#%% +testing2`; + + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Set up our expected calls to add code + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue(targetText1), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue(targetText2), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.never()); + + await codeWatcher.runAllCellsAbove(4, 0); + + // Verify function calls + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test the RunAllCells command with an error', async () => { + const fileName = Uri.file('test.py'); + const version = 1; + const inputText = `#%% +testing1 +#%% +testing2`; // Command tests override getText, so just need the ranges here + const document = createDocument(inputText, fileName.fsPath, version, TypeMoq.Times.atLeastOnce(), true); + + codeWatcher.setDocument(document.object); + + // Set up our expected calls to add code + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue('#%%\ntesting1'), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + + activeInteractiveWindow + .setup((h) => + h.addCode( + TypeMoq.It.isValue('#%%\ntesting2'), + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.never()); + + await codeWatcher.runAllCells(); + + // Verify function calls + activeInteractiveWindow.verifyAll(); + document.verifyAll(); + }); + + test('Test insert cell below position', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(0, 4, 0, 4); + + codeWatcher.insertCellBelowPosition(); + + expect(mockTextEditor.document.getText()).to.equal(`testing0 +# %% + +#%% +testing1 +#%% +testing2`); + expect(mockTextEditor.selection.start.line).to.equal(2); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(2); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); + + test('Test insert cell below position at end', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +#%% +testing2` + ); + + // end selection at bottom of document + mockTextEditor.selection = new Selection(1, 4, 5, 8); + + codeWatcher.insertCellBelowPosition(); + + expect(mockTextEditor.document.getText()).to.equal(`testing0 +#%% +testing1 +#%% +testing2 +# %% +`); + expect(mockTextEditor.selection.start.line).to.equal(7); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(7); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); + + test('Test insert cell below', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(2, 4, 2, 4); + + codeWatcher.insertCellBelow(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing0 +#%% +testing1 +testing1a +# %% + +#%% +testing2` + ); + expect(mockTextEditor.selection.start.line).to.equal(5); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(5); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); + + test('Test insert cell below but above any cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(0, 4, 0, 4); + + codeWatcher.insertCellBelow(); + + expect(mockTextEditor.document.getText()).to.equal(`testing0 +# %% + +#%% +testing1 +#%% +testing2`); + expect(mockTextEditor.selection.start.line).to.equal(2); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(2); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); + + test('Test insert cell below selection range', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + // range crossing multiple cells.Insert below bottom of range. + mockTextEditor.selection = new Selection(0, 4, 2, 4); + + codeWatcher.insertCellBelow(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing0 +#%% +testing1 +testing1a +# %% + +#%% +testing2` + ); + expect(mockTextEditor.selection.start.line).to.equal(5); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(5); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); + + test('Test insert cell above first cell of range', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + // above the first cell of the range + mockTextEditor.selection = new Selection(3, 4, 5, 4); + + codeWatcher.insertCellAbove(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing0 +# %% + +#%% +testing1 +testing1a +#%% +testing2` + ); + expect(mockTextEditor.selection.start.line).to.equal(2); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(2); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); + + test('Test insert cell above and above cells', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(0, 3, 0, 4); + + codeWatcher.insertCellAbove(); + + expect(mockTextEditor.document.getText()).to.equal( + `# %% + +testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + expect(mockTextEditor.selection.start.line).to.equal(1); + expect(mockTextEditor.selection.start.character).to.equal(0); + expect(mockTextEditor.selection.end.line).to.equal(1); + expect(mockTextEditor.selection.end.character).to.equal(0); + }); + + test('Delete single cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(3, 4, 3, 4); + + codeWatcher.deleteCells(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing0 +#%% +testing2` + ); + }); + + test('Delete multiple cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(3, 4, 5, 4); + + codeWatcher.deleteCells(); + + expect(mockTextEditor.document.getText()).to.equal(`testing0`); + }); + + test('Delete cell no cells in selection', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(0, 1, 0, 4); + + codeWatcher.deleteCells(); + + expect(mockTextEditor.document.getText()).to.equal(`testing0 +#%% +testing1 +testing1a +#%% +testing2`); + }); + + test('Select cell single', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(2, 1, 2, 1); + + codeWatcher.selectCell(); + + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(3); + expect(mockTextEditor.selection.active.character).to.equal(9); + }); + + test('Select cell multiple', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(2, 1, 4, 1); + + codeWatcher.selectCell(); + + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(5); + expect(mockTextEditor.selection.active.character).to.equal(8); + }); + + test('Select cell multiple reversed', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(4, 1, 2, 1); + + codeWatcher.selectCell(); + + expect(mockTextEditor.selection.active.line).to.equal(1); + expect(mockTextEditor.selection.active.character).to.equal(0); + expect(mockTextEditor.selection.anchor.line).to.equal(5); + expect(mockTextEditor.selection.anchor.character).to.equal(8); + }); + + test('Select cell above cells unchanged', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(0, 1, 0, 4); + + codeWatcher.selectCell(); + + expect(mockTextEditor.selection.start.line).to.equal(0); + expect(mockTextEditor.selection.start.character).to.equal(1); + expect(mockTextEditor.selection.end.line).to.equal(0); + expect(mockTextEditor.selection.end.character).to.equal(4); + }); + + test('Select cell contents', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(3, 4, 3, 4); + + codeWatcher.selectCellContents(); + + expect(mockTextEditor.selections.length).to.equal(1); + + const selection = mockTextEditor.selections[0]; + expect(selection.anchor.line).to.equal(2); + expect(selection.anchor.character).to.equal(0); + expect(selection.active.line).to.equal(3); + expect(selection.active.character).to.equal(9); + }); + + test('Select cell contents multi cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(3, 4, 5, 4); + + codeWatcher.selectCellContents(); + + expect(mockTextEditor.selections.length).to.equal(2); + + let selection: Selection; + selection = mockTextEditor.selections[0]; + expect(selection.anchor.line).to.equal(2); + expect(selection.anchor.character).to.equal(0); + expect(selection.active.line).to.equal(3); + expect(selection.active.character).to.equal(9); + + selection = mockTextEditor.selections[1]; + expect(selection.anchor.line).to.equal(5); + expect(selection.anchor.character).to.equal(0); + expect(selection.active.line).to.equal(5); + expect(selection.active.character).to.equal(8); + }); + + test('Select cell contents multi cell reversed', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing0 +#%% +testing1 +testing1a +#%% +testing2` + ); + + mockTextEditor.selection = new Selection(5, 4, 3, 4); + + codeWatcher.selectCellContents(); + + expect(mockTextEditor.selections.length).to.equal(2); + + let selection: Selection; + selection = mockTextEditor.selections[0]; + expect(selection.active.line).to.equal(2); + expect(selection.active.character).to.equal(0); + expect(selection.anchor.line).to.equal(3); + expect(selection.anchor.character).to.equal(9); + + selection = mockTextEditor.selections[1]; + expect(selection.active.line).to.equal(5); + expect(selection.active.character).to.equal(0); + expect(selection.anchor.line).to.equal(5); + expect(selection.anchor.character).to.equal(8); + }); + + test('Extend selection by cell above initial select', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 5, 2); + + codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(6); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(4); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above initial range in cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 6, 4); + + codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(6); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(4); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above initial range in cell opposite direction', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 4, 5, 2); + + codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(6); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(4); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above initial range below cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 8, 2); + + codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell above initial range above cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(8, 2, 5, 2); + + codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(8); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(4); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above expand above', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 10, 4, 0); + + codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(6); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(1); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above contract below', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(1, 0, 6, 10); + + codeWatcher.extendSelectionByCellAbove(); + + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(3); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below initial select', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 5, 2); + + codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below initial range in cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 6, 4); + + codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below initial range in cell opposite direction', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 4, 5, 2); + + codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below initial range below cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(3, 2, 6, 2); + + codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below initial range above cell', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 2, 3, 2); + + codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below expand below', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 10, 4, 0); + + codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(8); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Extend selection by cell below contract above', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(6, 10, 1, 0); + + codeWatcher.extendSelectionByCellBelow(); + + expect(mockTextEditor.selection.anchor.line).to.equal(6); + expect(mockTextEditor.selection.anchor.character).to.equal(10); + expect(mockTextEditor.selection.active.line).to.equal(4); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Extend selection by cell above and below', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 2, 6, 2); + + codeWatcher.extendSelectionByCellAbove(); // select full cell + codeWatcher.extendSelectionByCellAbove(); // select cell above + codeWatcher.extendSelectionByCellAbove(); // top cell no change + codeWatcher.extendSelectionByCellAbove(); // top cell no change + codeWatcher.extendSelectionByCellBelow(); // contract by cell + codeWatcher.extendSelectionByCellBelow(); // expand by cell below + codeWatcher.extendSelectionByCellBelow(); // last cell no change + codeWatcher.extendSelectionByCellAbove(); // Original cell + + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(6); + expect(mockTextEditor.selection.active.character).to.equal(10); + }); + + test('Move cells up', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 5, 5, 5); + + await codeWatcher.moveCellsUp(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L5 +testing_L6 +# %% +testing_L2 +testing_L3 +# %% +testing_L8` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(2); + expect(mockTextEditor.selection.anchor.character).to.equal(5); + expect(mockTextEditor.selection.active.line).to.equal(2); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + + test('Move cells up multiple cells', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(8, 8, 5, 5); + + await codeWatcher.moveCellsUp(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L5 +testing_L6 +# %% +testing_L8 +# %% +testing_L2 +testing_L3` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(5); + expect(mockTextEditor.selection.anchor.character).to.equal(8); + expect(mockTextEditor.selection.active.line).to.equal(2); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + + test('Move cells up first cell no change', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(1, 2, 5, 5); + + await codeWatcher.moveCellsUp(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(2); + expect(mockTextEditor.selection.active.line).to.equal(5); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + + test('Move cells down', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 5, 5, 5); + + await codeWatcher.moveCellsDown(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L8 +# %% +testing_L5 +testing_L6` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(7); + expect(mockTextEditor.selection.anchor.character).to.equal(5); + expect(mockTextEditor.selection.active.line).to.equal(7); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + + test('Move cells down multiple cells', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(2, 2, 5, 5); + + await codeWatcher.moveCellsDown(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L8 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(4); + expect(mockTextEditor.selection.anchor.character).to.equal(2); + expect(mockTextEditor.selection.active.line).to.equal(7); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + + test('Move cells down last cell no change', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + + mockTextEditor.selection = new Selection(5, 5, 8, 5); + + await codeWatcher.moveCellsDown(); + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% +testing_L5 +testing_L6 +# %% +testing_L8` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(5); + expect(mockTextEditor.selection.anchor.character).to.equal(5); + expect(mockTextEditor.selection.active.line).to.equal(8); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + + test('Change cell to markdown', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% +testing_L2 +testing_L3 +# %% extra +# # testing_L5 +testing_L6 + +` + ); + + mockTextEditor.selection = new Selection(1, 2, 5, 5); + + codeWatcher.changeCellToMarkdown(); + + // NOTE: When running the function in real environment there + // are comment lines added in addition to the [markdown] definition. + // It is unclear with TypeMoq how to test this particular behavior because + // the external `commands.executeCommmands` is being proxied along with + // all subsequent calls. Essentially, I must rely on those functions + // being unit tested. + /* + actual expected = `testing_L0 +# %% +testing_L2 +testing_L3 +# %% [markdown] extra +# # testing_L5 +# testing_L6 + +` + */ + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% [markdown] +testing_L2 +testing_L3 +# %% [markdown] extra +# # testing_L5 +testing_L6 + +` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(5); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(8); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Change cell to markdown no change', async () => { + const text = `testing_L0 +# %% [markdown] +testing_L2 +testing_L3 +# %% [markdown] extra +# # testing_L5 +testing_L6 + +`; + const mockTextEditor = initializeMockTextEditor(codeWatcher, documentManager, text); + + mockTextEditor.selection = new Selection(1, 2, 5, 5); + + codeWatcher.changeCellToMarkdown(); + + expect(mockTextEditor.document.getText()).to.equal(text); + + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(2); + expect(mockTextEditor.selection.active.line).to.equal(5); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); + + test('Change cell to code', async () => { + const mockTextEditor = initializeMockTextEditor( + codeWatcher, + documentManager, + `testing_L0 +# %% [markdown] +# testing_L2 +# testing_L3 +# %% [markdown] extra +# # testing_L5 +# testing_L6 + +` + ); + + mockTextEditor.selection = new Selection(1, 2, 5, 5); + + codeWatcher.changeCellToCode(); + + // NOTE: When running the function in real environment there + // are comment lines added in addition to the [markdown] definition. + // It is unclear with TypeMoq how to test this particular behavior because + // the external `commands.executeCommmands` is being proxied along with + // all subsequent calls. Essentially, I must rely on those functions + // being unit tested. + /* + actual expected = `testing_L0 +# %% +testing_L2 +testing_L3 +# %% [markdown] extra +# # testing_L5 +# testing_L6 + +` + */ + + expect(mockTextEditor.document.getText()).to.equal( + `testing_L0 +# %% +# testing_L2 +# testing_L3 +# %% extra +# # testing_L5 +# testing_L6 + +` + ); + expect(mockTextEditor.selection.anchor.line).to.equal(5); + expect(mockTextEditor.selection.anchor.character).to.equal(0); + expect(mockTextEditor.selection.active.line).to.equal(8); + expect(mockTextEditor.selection.active.character).to.equal(0); + }); + + test('Change cell to code no change', async () => { + const text = `testing_L0 +# %% +# testing_L2 +# testing_L3 +# %% extra +# # testing_L5 +# testing_L6 + +`; + const mockTextEditor = initializeMockTextEditor(codeWatcher, documentManager, text); + + mockTextEditor.selection = new Selection(1, 2, 5, 5); + + codeWatcher.changeCellToCode(); + + expect(mockTextEditor.document.getText()).to.equal(text); + + expect(mockTextEditor.selection.anchor.line).to.equal(1); + expect(mockTextEditor.selection.anchor.character).to.equal(2); + expect(mockTextEditor.selection.active.line).to.equal(5); + expect(mockTextEditor.selection.active.character).to.equal(5); + }); +}); diff --git a/src/test/datascience/editor-integration/gotocell.functional.test.ts b/src/test/datascience/editor-integration/gotocell.functional.test.ts new file mode 100644 index 000000000000..13867648355b --- /dev/null +++ b/src/test/datascience/editor-integration/gotocell.functional.test.ts @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert } from 'chai'; +import { ChildProcess } from 'child_process'; +import * as path from 'path'; +import * as uuid from 'uuid/v4'; +import { CodeLens, Disposable, Position, Range, TextDocument } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; + +import { range } from 'lodash'; +import { IDocumentManager } from '../../../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { traceError } from '../../../client/common/logger'; +import { IDataScienceSettings } from '../../../client/common/types'; +import * as CellFactory from '../../../client/datascience/cellFactory'; +import { Commands } from '../../../client/datascience/constants'; +import { InteractiveWindowMessages } from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { getDefaultInteractiveIdentity } from '../../../client/datascience/interactive-window/identity'; +import { + ICell, + ICodeLensFactory, + IDataScienceCodeLensProvider, + IInteractiveWindowListener, + INotebook, + INotebookProvider +} from '../../../client/datascience/types'; +import { DataScienceIocContainer } from '../dataScienceIocContainer'; +import { MockDocumentManager } from '../mockDocumentManager'; + +// tslint:disable:no-any no-multiline-string max-func-body-length no-console max-classes-per-file trailing-comma +suite('DataScience gotocell tests', () => { + const disposables: Disposable[] = []; + let codeLensProvider: IDataScienceCodeLensProvider; + let codeLensFactory: ICodeLensFactory; + let notebookProvider: INotebookProvider; + let ioc: DataScienceIocContainer; + let documentManager: MockDocumentManager; + let visibleCells: ICell[] = []; + + setup(async () => { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(); + codeLensProvider = ioc.serviceManager.get<IDataScienceCodeLensProvider>(IDataScienceCodeLensProvider); + notebookProvider = ioc.serviceManager.get<INotebookProvider>(INotebookProvider); + documentManager = ioc.serviceManager.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + codeLensFactory = ioc.serviceManager.get<ICodeLensFactory>(ICodeLensFactory); + await ioc.activate(); + }); + + teardown(async () => { + try { + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < disposables.length; i += 1) { + const disposable = disposables[i]; + if (disposable) { + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + } + await ioc.dispose(); + } catch (e) { + traceError(e); + } + visibleCells = []; + }); + + function runTest(name: string, func: () => Promise<void>, _notebookProc?: ChildProcess) { + test(name, async () => { + console.log(`Starting test ${name} ...`); + return func(); + }); + } + + async function createNotebook(expectFailure: boolean = false): Promise<INotebook | undefined> { + // Catch exceptions. Throw a specific assertion if the promise fails + try { + const uri = getDefaultInteractiveIdentity(); + const nb = await notebookProvider.getOrCreateNotebook({ identity: uri }); + const listener = (codeLensFactory as any) as IInteractiveWindowListener; + listener.onMessage(InteractiveWindowMessages.NotebookIdentity, { + resource: uri, + type: 'interactive' + }); + listener.onMessage(InteractiveWindowMessages.NotebookExecutionActivated, uri); + return nb; + } catch (exc) { + if (!expectFailure) { + assert.ok(false, `Expected server to be created, but got ${exc}`); + } + } + } + + function addMockData(code: string, result: string | number, mimeType?: string, cellType?: string) { + if (ioc.mockJupyter) { + if (cellType && cellType === 'error') { + ioc.mockJupyter.addError(code, result.toString()); + } else { + ioc.mockJupyter.addCell(code, result, mimeType); + } + } + } + + function addDocument(cells: { code: string; result: any; cellType?: string }[], filePath: string) { + let docText = ''; + cells.forEach((c) => { + addMockData(c.code, c.result, c.cellType); + docText = docText.concat(c.code, '\n'); + }); + return documentManager.addDocument(docText, filePath); + } + + function srcDirectory() { + return path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); + } + + function getCodeLenses(): CodeLens[] { + const doc = documentManager.textDocuments[0]; + const result = codeLensProvider.provideCodeLenses(doc, CancellationToken.None); + if ((result as any).length) { + return result as CodeLens[]; + } + return []; + } + + async function executeCell(pos: number, notebook: INotebook): Promise<number> { + // Not using the interactive window, so we need to execute directly. + const doc = documentManager.textDocuments[0]; + + // However use the code lens to figure out the code to execute + const codeLenses = getCodeLenses(); + assert.ok(codeLenses && codeLenses.length > 0, 'No cell code lenses found'); + if (codeLenses.length) { + const runLens = codeLenses.filter((c) => c.command && c.command.command === Commands.RunCell); + assert.ok(runLens && runLens.length > pos, 'No run cell code lenses found'); + const codeLens = runLens[pos]; + const code = doc.getText(codeLens.range); + const startLine = codeLens.range.start.line; + const output = await notebook.execute(code, doc.fileName, startLine, uuid()); + visibleCells = visibleCells.concat(output); + // Trick the codeLensFactory into having the cells + const listener = (codeLensFactory as any) as IInteractiveWindowListener; + listener.onMessage(InteractiveWindowMessages.FinishCell, { + cell: output[0], + notebookIdentity: notebook.identity + }); + } + + return visibleCells.length; + } + + function verifyNoGoto(startLine: number) { + // See what code lens we have for the document + const codeLenses = getCodeLenses(); + + // There should be one with the ScrollTo command + const scrollTo = codeLenses.find( + (c) => c.command && c.command.command === Commands.ScrollToCell && c.range.start.line === startLine + ); + assert.equal(scrollTo, undefined, 'Goto cell code lens should not be found'); + } + + function verifyGoto(count: string, startLine: number) { + // See what code lens we have for the document + const codeLenses = getCodeLenses(); + + // There should be one with the ScrollTo command + const scrollTo = codeLenses.find( + (c) => c.command && c.command.command === Commands.ScrollToCell && c.range.start.line === startLine + ); + assert.ok(scrollTo, 'Goto cell code lens not found'); + + // It should have the same number as the execution count + assert.ok(scrollTo!.command!.title.includes(count), 'Wrong goto on cell'); + } + + function addSingleChange(r: Range, newText: string) { + const filePath = path.join(srcDirectory(), 'foo.py'); + documentManager.changeDocument(filePath, [{ range: r, newText }]); + } + + runTest('Basic execution', async () => { + addDocument( + [ + { + code: `#%%\na=1\na`, + result: 1 + }, + { + code: `#%%\na+=1\na`, + result: 2 + }, + { + code: `#%%\na+=4\na`, + result: 6 + } + ], + path.join(srcDirectory(), 'foo.py') + ); + + const server = await createNotebook(true); + assert.ok(server, 'No server created'); + + // Verify we don't have a goto + const codeLenses = getCodeLenses(); + const scrollTo = codeLenses.find((c) => c.command && c.command.command === Commands.ScrollToCell); + assert.equal(scrollTo, undefined, 'Goto cell code lens should not be found'); + + // Execute the first cell + await executeCell(0, server!); + + // Verify it now has a goto + verifyGoto('1', 0); + }); + + runTest('Basic edit', async () => { + const filePath = path.join(srcDirectory(), 'foo.py'); + addDocument( + [ + { + code: `#%%\na=1\na`, + result: 1 + }, + { + code: `#%%\na+=1\na`, + result: 2 + }, + { + code: `#%%\na+=4\na`, + result: 6 + }, + { + code: `#%%\n`, + result: undefined + } + ], + filePath + ); + + const server = await createNotebook(true); + assert.ok(server, 'No server created'); + + // Execute the second cell + await executeCell(1, server!); + + // verify we have an execute + verifyGoto('1', 3); + + // Execute the first cell and check same thing + await executeCell(0, server!); + + // verify we have an execute + verifyGoto('2', 0); + + // Delete the first cell and make sure the second cell still has an execute + addSingleChange(new Range(new Position(0, 0), new Position(3, 0)), ''); + + // verify we have an execute (start should have moved though) + verifyGoto('1', 0); + + // Run the last cell. It should not generate a code lens as it has no code + await executeCell(2, server!); + verifyNoGoto(6); + + // Put back the cell we deleted + addSingleChange(new Range(new Position(0, 0), new Position(0, 0)), '#%%\na=1\na\n'); + + // Our 2nd execute should show up again + verifyGoto('2', 0); + + // Our 1st execute should have moved + verifyGoto('1', 3); + }); + + runTest('Verify not recreating code lenses when not necessary', async () => { + // Override the function that generates cell ranges. We want to count how many times this is called + let generateCount = 0; + const oldGenerateRanges = (CellFactory as any).generateCellRangesFromDocument; + (CellFactory as any).generateCellRangesFromDocument = ( + document: TextDocument, + settings?: IDataScienceSettings + ) => { + generateCount = generateCount + 1; + return oldGenerateRanges(document, settings); + }; + + for (const i of range(0, 10)) { + const filePath = path.join(srcDirectory(), `foo${i}.py`); + const doc = addDocument( + [ + { + code: `#%%\na=1\na`, + result: 1 + }, + { + code: `#%%\na+=1\na`, + result: 2 + }, + { + code: `#%%\na+=4\na`, + result: 6 + }, + { + code: `#%%\n`, + result: undefined + } + ], + filePath + ); + codeLensFactory.createCodeLenses(doc); + } + + const server = await createNotebook(true); + assert.ok(server, 'No server created'); + const currentGenerateCount = generateCount; + + // Execute the second cell + await executeCell(1, server!); + + // verify we did not generate any new cell ranges + assert.equal(generateCount, currentGenerateCount, 'Should not be regenerating cell ranges on execute'); + }); +}); diff --git a/src/test/datascience/editor-integration/helpers.ts b/src/test/datascience/editor-integration/helpers.ts new file mode 100644 index 000000000000..b84c0a07f969 --- /dev/null +++ b/src/test/datascience/editor-integration/helpers.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as TypeMoq from 'typemoq'; +import { Range, TextDocument, TextLine, Uri } from 'vscode'; + +// tslint:disable:max-func-body-length no-trailing-whitespace no-multiline-string +// Disable whitespace / multiline as we use that to pass in our fake file strings + +// Helper function to create a document and get line count and lines +export function createDocument( + inputText: string, + fileName: string, + fileVersion: number, + times: TypeMoq.Times, + implementGetText?: boolean +): TypeMoq.IMock<TextDocument> { + const document = TypeMoq.Mock.ofType<TextDocument>(); + + // Split our string on newline chars + const inputLines = inputText.split(/\r?\n/); + + document.setup((d) => d.languageId).returns(() => 'python'); + + // First set the metadata + document + .setup((d) => d.fileName) + .returns(() => Uri.file(fileName).fsPath) + .verifiable(times); + document + .setup((d) => d.version) + .returns(() => fileVersion) + .verifiable(times); + document.setup((d) => d.uri).returns(() => Uri.file(fileName)); + + // Next add the lines in + document.setup((d) => d.lineCount).returns(() => inputLines.length); + + const textLines = inputLines.map((line, index) => { + const textLine = TypeMoq.Mock.ofType<TextLine>(); + const testRange = new Range(index, 0, index, line.length); + textLine.setup((l) => l.text).returns(() => line); + textLine.setup((l) => l.range).returns(() => testRange); + textLine.setup((l) => l.isEmptyOrWhitespace).returns(() => line.trim().length === 0); + return textLine; + }); + document.setup((d) => d.lineAt(TypeMoq.It.isAnyNumber())).returns((index: number) => textLines[index].object); + + // Get text is a bit trickier + if (implementGetText) { + document.setup((d) => d.getText()).returns(() => inputText); + document + .setup((d) => d.getText(TypeMoq.It.isAny())) + .returns((r: Range) => { + let results = ''; + if (r) { + for (let line = r.start.line; line <= r.end.line && line < inputLines.length; line += 1) { + const startIndex = line === r.start.line ? r.start.character : 0; + const endIndex = line === r.end.line ? r.end.character : inputLines[line].length - 1; + results += inputLines[line].slice(startIndex, endIndex + 1); + if (line !== r.end.line) { + results += '\n'; + } + } + } else { + results = inputText; + } + return results; + }); + } + + return document; +} diff --git a/src/test/datascience/errorHandler.functional.test.tsx b/src/test/datascience/errorHandler.functional.test.tsx new file mode 100644 index 000000000000..ce686aaa5914 --- /dev/null +++ b/src/test/datascience/errorHandler.functional.test.tsx @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { IDocumentManager } from '../../client/common/application/types'; +import { LocalZMQKernel } from '../../client/common/experiments/groups'; +import { JupyterInterpreterSubCommandExecutionService } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService'; +import { JupyterInstallError } from '../../client/datascience/jupyter/jupyterInstallError'; +import { ICodeWatcher, IInteractiveWindowProvider, IJupyterExecution } from '../../client/datascience/types'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { MockDocumentManager } from './mockDocumentManager'; + +suite('DataScience Error Handler Functional Tests', () => { + let ioc: DataScienceIocContainer; + let stubbedInstallMissingDependencies: sinon.SinonStub<[(JupyterInstallError | undefined)?], Promise<void>>; + setup(async () => { + stubbedInstallMissingDependencies = sinon.stub( + JupyterInterpreterSubCommandExecutionService.prototype, + 'installMissingDependencies' + ); + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(); + ioc = modifyContainer(); + return ioc.activate(); + }); + + teardown(async () => { + sinon.restore(); + await ioc.dispose(); + }); + + function modifyContainer(): DataScienceIocContainer { + const jupyterExecution = TypeMoq.Mock.ofType<IJupyterExecution>(); + + jupyterExecution.setup((jup) => jup.getUsableJupyterPython()).returns(() => Promise.resolve(undefined)); + ioc.serviceManager.rebindInstance<IJupyterExecution>(IJupyterExecution, jupyterExecution.object); + + ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); + ioc.get<IJupyterExecution>(IJupyterExecution); + stubbedInstallMissingDependencies.resolves(); + return ioc; + } + + test('Jupyter not installed', async () => { + // Turn off raw kernel for this test as it's testing jupyter install state + ioc.setExperimentState(LocalZMQKernel.experiment, false); + ioc.addDocument('#%%\ntesting', 'test.py'); + + const cw = ioc.serviceManager.get<ICodeWatcher>(ICodeWatcher); + const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + + cw.setDocument(docManager.textDocuments[0]); + await cw.runAllCells(); + + assert.isOk(stubbedInstallMissingDependencies.callCount, 'installMissingDependencies not invoked'); + await ioc.dispose(); + }); +}); diff --git a/src/test/datascience/errorHandler.unit.test.ts b/src/test/datascience/errorHandler.unit.test.ts new file mode 100644 index 000000000000..ab855ccee836 --- /dev/null +++ b/src/test/datascience/errorHandler.unit.test.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { IApplicationShell } from '../../client/common/application/types'; +import { IInstallationChannelManager, IModuleInstaller } from '../../client/common/installer/types'; +import * as localize from '../../client/common/utils/localize'; +import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/errorHandler'; +import { JupyterInstallError } from '../../client/datascience/jupyter/jupyterInstallError'; +import { JupyterSelfCertsError } from '../../client/datascience/jupyter/jupyterSelfCertsError'; +import { JupyterZMQBinariesNotFoundError } from '../../client/datascience/jupyter/jupyterZMQBinariesNotFoundError'; +import { JupyterServerSelector } from '../../client/datascience/jupyter/serverSelector'; +import { IJupyterInterpreterDependencyManager } from '../../client/datascience/types'; + +suite('DataScience Error Handler Unit Tests', () => { + let applicationShell: typemoq.IMock<IApplicationShell>; + let channels: typemoq.IMock<IInstallationChannelManager>; + let dataScienceErrorHandler: DataScienceErrorHandler; + let dependencyManager: IJupyterInterpreterDependencyManager; + const serverSelector = mock(JupyterServerSelector); + + setup(() => { + applicationShell = typemoq.Mock.ofType<IApplicationShell>(); + channels = typemoq.Mock.ofType<IInstallationChannelManager>(); + dependencyManager = mock<IJupyterInterpreterDependencyManager>(); + when(dependencyManager.installMissingDependencies(anything())).thenResolve(); + dataScienceErrorHandler = new DataScienceErrorHandler( + applicationShell.object, + instance(dependencyManager), + instance(serverSelector) + ); + }); + const message = 'Test error message.'; + + test('Default error', async () => { + applicationShell + .setup((app) => app.showErrorMessage(typemoq.It.isAny())) + .returns(() => Promise.resolve(message)) + .verifiable(typemoq.Times.once()); + + const err = new Error(message); + await dataScienceErrorHandler.handleError(err); + + applicationShell.verifyAll(); + }); + + test('Jupyter Self Certificates Error', async () => { + applicationShell + .setup((app) => app.showErrorMessage(typemoq.It.isAny())) + .returns(() => Promise.resolve(message)) + .verifiable(typemoq.Times.never()); + + const err = new JupyterSelfCertsError(message); + await dataScienceErrorHandler.handleError(err); + + applicationShell.verifyAll(); + }); + + test('Jupyter Install Error', async () => { + applicationShell + .setup((app) => + app.showInformationMessage( + typemoq.It.isAny(), + typemoq.It.isValue(localize.DataScience.jupyterInstall()), + typemoq.It.isValue(localize.DataScience.notebookCheckForImportNo()), + typemoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(localize.DataScience.jupyterInstall())) + .verifiable(typemoq.Times.once()); + + const installers: IModuleInstaller[] = [ + { + name: 'Pip', + displayName: 'Pip', + priority: 0, + isSupported: () => Promise.resolve(true), + installModule: () => Promise.resolve() + }, + { + name: 'Conda', + displayName: 'Conda', + priority: 0, + isSupported: () => Promise.resolve(true), + installModule: () => Promise.resolve() + } + ]; + + channels + .setup((ch) => ch.getInstallationChannels()) + .returns(() => Promise.resolve(installers)) + .verifiable(typemoq.Times.once()); + + const err = new JupyterInstallError(message, 'test.com'); + await dataScienceErrorHandler.handleError(err); + + verify(dependencyManager.installMissingDependencies(err)).once(); + }); + + test('ZMQ Install Error', async () => { + applicationShell + .setup((app) => + app.showErrorMessage(typemoq.It.isAny(), typemoq.It.isValue(localize.DataScience.selectNewServer())) + ) + .returns(() => Promise.resolve(localize.DataScience.selectNewServer())) + .verifiable(typemoq.Times.once()); + when(serverSelector.selectJupyterURI(anything())).thenCall(() => Promise.resolve()); + const err = new JupyterZMQBinariesNotFoundError('Not found'); + await dataScienceErrorHandler.handleError(err); + verify(serverSelector.selectJupyterURI(anything())).once(); + applicationShell.verifyAll(); + }); +}); diff --git a/src/test/datascience/execution.unit.test.ts b/src/test/datascience/execution.unit.test.ts new file mode 100644 index 000000000000..78203130f51f --- /dev/null +++ b/src/test/datascience/execution.unit.test.ts @@ -0,0 +1,1104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert } from 'chai'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import { SemVer } from 'semver'; +import { anything, instance, match, mock, reset, when } from 'ts-mockito'; +import { Matcher } from 'ts-mockito/lib/matcher/type/Matcher'; +import * as TypeMoq from 'typemoq'; +import * as uuid from 'uuid/v4'; +import { CancellationTokenSource, ConfigurationChangeEvent, Disposable, EventEmitter } from 'vscode'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { PythonSettings } from '../../client/common/configSettings'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { PYTHON_LANGUAGE } from '../../client/common/constants'; +import { PersistentState, PersistentStateFactory } from '../../client/common/persistentState'; +import { ProcessServiceFactory } from '../../client/common/process/processFactory'; +import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; +import { + ExecutionResult, + IProcessService, + IProcessServiceFactory, + IPythonDaemonExecutionService, + IPythonExecutionFactory, + IPythonExecutionService, + ObservableExecutionResult, + Output +} from '../../client/common/process/types'; +import { + IAsyncDisposableRegistry, + IConfigurationService, + IOutputChannel, + IPathUtils, + Product +} from '../../client/common/types'; +import { Architecture } from '../../client/common/utils/platform'; +import { EXTENSION_ROOT_DIR } from '../../client/constants'; +import { DataScienceFileSystem } from '../../client/datascience/dataScienceFileSystem'; +import { JupyterInterpreterDependencyService } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService'; +import { JupyterInterpreterOldCacheStateStore } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterOldCacheStateStore'; +import { JupyterInterpreterService } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterService'; +import { JupyterInterpreterSubCommandExecutionService } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService'; +import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; +import { KernelSelector } from '../../client/datascience/jupyter/kernels/kernelSelector'; +import { NotebookStarter } from '../../client/datascience/jupyter/notebookStarter'; +import { LiveShareApi } from '../../client/datascience/liveshare/liveshare'; +import { + IDataScienceFileSystem, + IJupyterKernelSpec, + IJupyterSubCommandExecutionService, + INotebookServer +} from '../../client/datascience/types'; +import { EnvironmentActivationService } from '../../client/interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { InterpreterService } from '../../client/interpreter/interpreterService'; +import { ServiceContainer } from '../../client/ioc/container'; +import { KnownSearchPathsForInterpreters } from '../../client/pythonEnvironments/discovery/locators/services/KnownPathsService'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { getOSType, OSType } from '../common'; +import { noop } from '../core'; +import { MockOutputChannel } from '../mockClasses'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; +import { MockJupyterServer } from './mockJupyterServer'; + +// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length +class DisposableRegistry implements IAsyncDisposableRegistry { + private disposables: Disposable[] = []; + + public push = (disposable: Disposable) => this.disposables.push(disposable); + + public dispose = async (): Promise<void> => { + for (const disposable of this.disposables) { + if (!disposable) { + continue; + } + const val = disposable.dispose(); + if (val instanceof Promise) { + const promise = val as Promise<void>; + await promise; + } + } + this.disposables = []; + }; +} + +suite('Jupyter Execution', async () => { + const interpreterService = mock(InterpreterService); + const jupyterOutputChannel = new MockOutputChannel(''); + const executionFactory = mock(PythonExecutionFactory); + const liveShare = mock(LiveShareApi); + const configService = mock(ConfigurationService); + const application = mock(ApplicationShell); + const processServiceFactory = mock(ProcessServiceFactory); + const knownSearchPaths = mock(KnownSearchPathsForInterpreters); + const fileSystem = mock(DataScienceFileSystem); + const activationHelper = mock(EnvironmentActivationService); + const serviceContainer = mock(ServiceContainer); + const workspaceService = mock(WorkspaceService); + const disposableRegistry = new DisposableRegistry(); + const dummyEvent = new EventEmitter<void>(); + const configChangeEvent = new EventEmitter<ConfigurationChangeEvent>(); + const pythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); + const jupyterOnPath = getOSType() === OSType.Windows ? '/foo/bar/jupyter.exe' : '/foo/bar/jupyter'; + let ipykernelInstallCount = 0; + let kernelSelector: KernelSelector; + let notebookStarter: NotebookStarter; + const workingPython: PythonEnvironment = { + path: '/foo/bar/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 + }; + + const missingKernelPython: PythonEnvironment = { + path: '/foo/baz/python.exe', + version: new SemVer('3.1.1-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 + }; + + const missingNotebookPython: PythonEnvironment = { + path: '/bar/baz/python.exe', + version: new SemVer('2.1.1-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 + }; + + const missingNotebookPython2: PythonEnvironment = { + path: '/two/baz/python.exe', + version: new SemVer('2.1.1'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 + }; + + let workingKernelSpec: string; + + suiteSetup(() => { + noop(); + }); + suiteTeardown(() => { + noop(); + }); + + setup(() => { + workingKernelSpec = createTempSpec(workingPython.path); + ipykernelInstallCount = 0; + // tslint:disable-next-line:no-invalid-this + }); + + teardown(() => { + reset(fileSystem); + return cleanupDisposables(); + }); + + function cleanupDisposables(): Promise<void> { + return disposableRegistry.dispose(); + } + + // tslint:disable-next-line: max-classes-per-file + class FunctionMatcher extends Matcher { + private func: (obj: any) => boolean; + constructor(func: (obj: any) => boolean) { + super(); + this.func = func; + } + public match(value: Object): boolean { + return this.func(value); + } + public toString(): string { + return 'FunctionMatcher'; + } + } + + function createTempSpec(pythonPath: string): string { + const tempDir = os.tmpdir(); + const subDir = uuid(); + const filePath = path.join(tempDir, subDir, 'kernel.json'); + fs.ensureDirSync(path.dirname(filePath)); + fs.writeJSONSync(filePath, { + display_name: 'Python 3', + language: 'python', + argv: [pythonPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}'] + }); + return filePath; + } + + function argThat(func: (obj: any) => boolean): any { + return new FunctionMatcher(func); + } + + function createTypeMoq<T>(tag: string): TypeMoq.IMock<T> { + // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class + // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 + const result: TypeMoq.IMock<T> = TypeMoq.Mock.ofType<T>(); + (result as any).tag = tag; + result.setup((x: any) => x.then).returns(() => undefined); + return result; + } + + function argsMatch(matchers: (string | RegExp)[], args: string[]): boolean { + if (matchers.length === args.length) { + return args.every((s, i) => { + const r = matchers[i] as RegExp; + return r && r.test ? r.test(s) : s === matchers[i]; + }); + } + return false; + } + + function setupPythonService( + service: TypeMoq.IMock<IPythonExecutionService>, + module: string | undefined, + args: (string | RegExp)[], + result: Promise<ExecutionResult<string>> + ) { + if (module) { + service + .setup((x) => + x.execModule( + TypeMoq.It.isValue(module), + TypeMoq.It.is((a) => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) + .returns(() => result); + const withModuleArgs = ['-m', module, ...args]; + service + .setup((x) => + x.exec( + TypeMoq.It.is((a) => argsMatch(withModuleArgs, a)), + TypeMoq.It.isAny() + ) + ) + .returns(() => result); + } else { + service + .setup((x) => + x.exec( + TypeMoq.It.is((a) => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) + .returns(() => result); + } + } + + function setupPythonServiceWithFunc( + service: TypeMoq.IMock<IPythonExecutionService>, + module: string, + args: (string | RegExp)[], + result: () => Promise<ExecutionResult<string>> + ) { + service + .setup((x) => + x.execModule( + TypeMoq.It.isValue(module), + TypeMoq.It.is((a) => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) + .returns(result); + const withModuleArgs = ['-m', module, ...args]; + service + .setup((x) => + x.exec( + TypeMoq.It.is((a) => argsMatch(withModuleArgs, a)), + TypeMoq.It.isAny() + ) + ) + .returns(result); + service + .setup((x) => + x.execModule( + TypeMoq.It.isValue(module), + TypeMoq.It.is((a) => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) + .returns(result); + } + + function setupPythonServiceExecObservable( + service: TypeMoq.IMock<IPythonExecutionService>, + module: string, + args: (string | RegExp)[], + stderr: string[], + stdout: string[] + ) { + const result: ObservableExecutionResult<string> = { + proc: undefined, + out: new Observable<Output<string>>((subscriber) => { + stderr.forEach((s) => subscriber.next({ source: 'stderr', out: s })); + stdout.forEach((s) => subscriber.next({ source: 'stderr', out: s })); + }), + dispose: () => { + noop(); + } + }; + + service + .setup((x) => + x.execModuleObservable( + TypeMoq.It.isValue(module), + TypeMoq.It.is((a) => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) + .returns(() => result); + const withModuleArgs = ['-m', module, ...args]; + service + .setup((x) => + x.execObservable( + TypeMoq.It.is((a) => argsMatch(withModuleArgs, a)), + TypeMoq.It.isAny() + ) + ) + .returns(() => result); + } + + function setupProcessServiceExec( + service: TypeMoq.IMock<IProcessService>, + file: string, + args: (string | RegExp)[], + result: Promise<ExecutionResult<string>> + ) { + service + .setup((x) => + x.exec( + TypeMoq.It.isValue(file), + TypeMoq.It.is((a) => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) + .returns(() => result); + } + + function setupProcessServiceExecWithFunc( + service: TypeMoq.IMock<IProcessService>, + file: string, + args: (string | RegExp)[], + result: () => Promise<ExecutionResult<string>> + ) { + service + .setup((x) => + x.exec( + TypeMoq.It.isValue(file), + TypeMoq.It.is((a) => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) + .returns(result); + } + + function setupProcessServiceExecObservable( + service: TypeMoq.IMock<IProcessService>, + file: string, + args: (string | RegExp)[], + stderr: string[], + stdout: string[] + ) { + const result: ObservableExecutionResult<string> = { + proc: undefined, + out: new Observable<Output<string>>((subscriber) => { + stderr.forEach((s) => subscriber.next({ source: 'stderr', out: s })); + stdout.forEach((s) => subscriber.next({ source: 'stderr', out: s })); + }), + dispose: () => { + noop(); + } + }; + + service + .setup((x) => + x.execObservable( + TypeMoq.It.isValue(file), + TypeMoq.It.is((a) => argsMatch(args, a)), + TypeMoq.It.isAny() + ) + ) + .returns(() => result); + } + + function createKernelSpecs(specs: { name: string; resourceDir: string }[]): Record<string, any> { + const models: Record<string, any> = {}; + specs.forEach((spec) => { + models[spec.name] = { + resource_dir: spec.resourceDir, + spec: { + name: spec.name, + display_name: spec.name, + language: 'python' + } + }; + }); + return models; + } + function setupWorkingPythonService( + service: TypeMoq.IMock<IPythonExecutionService>, + notebookStdErr?: string[], + runInDocker?: boolean + ) { + setupPythonService(service, 'ipykernel', ['--version'], Promise.resolve({ stdout: '1.1.1.1' })); + setupPythonService(service, 'jupyter', ['nbconvert', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); + setupPythonService(service, 'jupyter', ['notebook', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); + setupPythonService(service, 'jupyter', ['kernelspec', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); + service.setup((x) => x.getInterpreterInformation()).returns(() => Promise.resolve(workingPython)); + + // Don't mind the goofy path here. It's supposed to not find the item. It's just testing the internal regex works + setupPythonServiceWithFunc(service, 'jupyter', ['kernelspec', 'list', '--json'], () => { + // Return different results after we install our kernel + if (ipykernelInstallCount > 0) { + const kernelSpecs = createKernelSpecs([ + { name: 'working', resourceDir: path.dirname(workingKernelSpec) }, + { + name: '0e8519db-0895-416c-96df-fa80131ecea0', + resourceDir: + 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' + } + ]); + return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); + } else { + const kernelSpecs = createKernelSpecs([ + { + name: '0e8519db-0895-416c-96df-fa80131ecea0', + resourceDir: + 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' + } + ]); + return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); + } + }); + const kernelSpecs2 = createKernelSpecs([ + { name: 'working', resourceDir: path.dirname(workingKernelSpec) }, + { + name: '0e8519db-0895-416c-96df-fa80131ecea0', + resourceDir: + 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' + } + ]); + setupPythonService( + service, + 'jupyter', + ['kernelspec', 'list', '--json'], + Promise.resolve({ stdout: JSON.stringify(kernelSpecs2) }) + ); + setupPythonServiceWithFunc( + service, + 'ipykernel', + ['install', '--user', '--name', /\w+-\w+-\w+-\w+-\w+/, '--display-name', `'Python Interactive'`], + () => { + ipykernelInstallCount += 1; + const kernelSpecs = createKernelSpecs([ + { name: 'somename', resourceDir: path.dirname(workingKernelSpec) } + ]); + return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); + } + ); + const getServerInfoPath = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'getServerInfo.py' + ); + setupPythonService( + service, + undefined, + [getServerInfoPath], + Promise.resolve({ stdout: 'failure to get server infos' }) + ); + setupPythonServiceExecObservable(service, 'jupyter', ['kernelspec', 'list', '--json'], [], []); + const dockerArgs = runInDocker ? ['--ip', '127.0.0.1'] : []; + setupPythonServiceExecObservable( + service, + 'jupyter', + [ + 'notebook', + '--no-browser', + /--notebook-dir=.*/, + /--config=.*/, + '--NotebookApp.iopub_data_rate_limit=10000000000.0', + ...dockerArgs + ], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); + } + + function setupMissingKernelPythonService( + service: TypeMoq.IMock<IPythonExecutionService>, + notebookStdErr?: string[] + ) { + setupPythonService(service, 'jupyter', ['notebook', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); + setupPythonService(service, 'jupyter', ['kernelspec', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); + service.setup((x) => x.getInterpreterInformation()).returns(() => Promise.resolve(missingKernelPython)); + const kernelSpecs = createKernelSpecs([{ name: 'working', resourceDir: path.dirname(workingKernelSpec) }]); + setupPythonService( + service, + 'jupyter', + ['kernelspec', 'list', '--json'], + Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }) + ); + const getServerInfoPath = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'getServerInfo.py' + ); + setupPythonService( + service, + undefined, + [getServerInfoPath], + Promise.resolve({ stdout: 'failure to get server infos' }) + ); + setupPythonServiceExecObservable(service, 'jupyter', ['kernelspec', 'list', '--json'], [], []); + setupPythonServiceExecObservable( + service, + 'jupyter', + [ + 'notebook', + '--no-browser', + /--notebook-dir=.*/, + /--config=.*/, + '--NotebookApp.iopub_data_rate_limit=10000000000.0' + ], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); + } + + function setupMissingNotebookPythonService(service: TypeMoq.IMock<IPythonExecutionService>) { + service + .setup((x) => x.execModule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((_v) => { + return Promise.reject('cant exec'); + }); + service + .setup((x) => x.execModuleObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + throw new Error('Not supported'); + }); + service.setup((x) => x.getInterpreterInformation()).returns(() => Promise.resolve(missingNotebookPython)); + } + + function setupWorkingProcessService(service: TypeMoq.IMock<IProcessService>, notebookStdErr?: string[]) { + // Don't mind the goofy path here. It's supposed to not find the item. It's just testing the internal regex works + setupProcessServiceExecWithFunc( + service, + workingPython.path, + ['-m', 'jupyter', 'kernelspec', 'list', '--json'], + () => { + // Return different results after we install our kernel + if (ipykernelInstallCount > 0) { + const kernelSpecs = createKernelSpecs([ + { name: 'working', resourceDir: path.dirname(workingKernelSpec) }, + { + name: '0e8519db-0895-416c-96df-fa80131ecea0', + resourceDir: + 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' + } + ]); + return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); + } else { + const kernelSpecs = createKernelSpecs([ + { + name: '0e8519db-0895-416c-96df-fa80131ecea0', + resourceDir: + 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' + } + ]); + return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); + } + } + ); + const kernelSpecs2 = createKernelSpecs([ + { name: 'working', resourceDir: path.dirname(workingKernelSpec) }, + { + name: '0e8519db-0895-416c-96df-fa80131ecea0', + resourceDir: + 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' + } + ]); + setupProcessServiceExec( + service, + workingPython.path, + ['-m', 'jupyter', 'kernelspec', 'list', '--json'], + Promise.resolve({ stdout: JSON.stringify(kernelSpecs2) }) + ); + setupProcessServiceExecWithFunc( + service, + workingPython.path, + [ + '-m', + 'ipykernel', + 'install', + '--user', + '--name', + /\w+-\w+-\w+-\w+-\w+/, + '--display-name', + `'Python Interactive'` + ], + () => { + ipykernelInstallCount += 1; + const kernelSpecs = createKernelSpecs([ + { name: 'somename', resourceDir: path.dirname(workingKernelSpec) } + ]); + return Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }); + } + ); + const getServerInfoPath = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'getServerInfo.py' + ); + setupProcessServiceExec( + service, + workingPython.path, + [getServerInfoPath], + Promise.resolve({ stdout: 'failure to get server infos' }) + ); + setupProcessServiceExecObservable( + service, + workingPython.path, + ['-m', 'jupyter', 'kernelspec', 'list', '--json'], + [], + [] + ); + setupProcessServiceExecObservable( + service, + workingPython.path, + [ + '-m', + 'jupyter', + 'notebook', + '--no-browser', + /--notebook-dir=.*/, + /--config=.*/, + '--NotebookApp.iopub_data_rate_limit=10000000000.0' + ], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); + } + + function setupMissingKernelProcessService(service: TypeMoq.IMock<IProcessService>, notebookStdErr?: string[]) { + const kernelSpecs = createKernelSpecs([{ name: 'working', resourceDir: path.dirname(workingKernelSpec) }]); + setupProcessServiceExec( + service, + missingKernelPython.path, + ['-m', 'jupyter', 'kernelspec', 'list', '--json'], + Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }) + ); + const getServerInfoPath = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'getServerInfo.py' + ); + setupProcessServiceExec( + service, + missingKernelPython.path, + [getServerInfoPath], + Promise.resolve({ stdout: 'failure to get server infos' }) + ); + setupProcessServiceExecObservable( + service, + missingKernelPython.path, + ['-m', 'jupyter', 'kernelspec', 'list', '--json'], + [], + [] + ); + setupProcessServiceExecObservable( + service, + missingKernelPython.path, + [ + '-m', + 'jupyter', + 'notebook', + '--no-browser', + /--notebook-dir=.*/, + /--config=.*/, + '--NotebookApp.iopub_data_rate_limit=10000000000.0' + ], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); + } + + function setupPathProcessService( + jupyterPath: string, + service: TypeMoq.IMock<IProcessService>, + notebookStdErr?: string[] + ) { + const kernelSpecs = createKernelSpecs([{ name: 'working', resourceDir: path.dirname(workingKernelSpec) }]); + setupProcessServiceExec( + service, + jupyterPath, + ['kernelspec', 'list', '--json'], + Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }) + ); + setupProcessServiceExecObservable(service, jupyterPath, ['kernelspec', 'list', '--json'], [], []); + setupProcessServiceExec(service, jupyterPath, ['--version'], Promise.resolve({ stdout: '1.1.1.1' })); + setupProcessServiceExec( + service, + jupyterPath, + ['notebook', '--version'], + Promise.resolve({ stdout: '1.1.1.1' }) + ); + setupProcessServiceExec( + service, + jupyterPath, + ['kernelspec', '--version'], + Promise.resolve({ stdout: '1.1.1.1' }) + ); + setupProcessServiceExecObservable( + service, + jupyterPath, + [ + 'notebook', + '--no-browser', + /--notebook-dir=.*/, + /--config=.*/, + '--NotebookApp.iopub_data_rate_limit=10000000000.0' + ], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); + + // WE also check for existence with just the key jupyter + setupProcessServiceExec(service, 'jupyter', ['--version'], Promise.resolve({ stdout: '1.1.1.1' })); + setupProcessServiceExec(service, 'jupyter', ['notebook', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); + setupProcessServiceExec( + service, + 'jupyter', + ['kernelspec', '--version'], + Promise.resolve({ stdout: '1.1.1.1' }) + ); + } + + function createExecution( + activeInterpreter: PythonEnvironment, + notebookStdErr?: string[], + skipSearch?: boolean + ): JupyterExecutionFactory { + return createExecutionAndReturnProcessService(activeInterpreter, notebookStdErr, skipSearch) + .jupyterExecutionFactory; + } + function createExecutionAndReturnProcessService( + activeInterpreter: PythonEnvironment, + notebookStdErr?: string[], + skipSearch?: boolean, + runInDocker?: boolean + ): { + executionService: IPythonExecutionService; + jupyterExecutionFactory: JupyterExecutionFactory; + } { + // Setup defaults + when(interpreterService.onDidChangeInterpreter).thenReturn(dummyEvent.event); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(activeInterpreter); + when(interpreterService.getInterpreters(anything())).thenResolve([ + workingPython, + missingKernelPython, + missingNotebookPython + ]); + when(interpreterService.getInterpreterDetails(match('/foo/bar/python.exe'))).thenResolve(workingPython); // Mockito is stupid. Matchers have to use literals. + when(interpreterService.getInterpreterDetails(match('/foo/baz/python.exe'))).thenResolve(missingKernelPython); + when(interpreterService.getInterpreterDetails(match('/bar/baz/python.exe'))).thenResolve(missingNotebookPython); + when(interpreterService.getInterpreterDetails(argThat((o) => !o.includes || !o.includes('python')))).thenReject( + ('Unknown interpreter' as any) as Error + ); + if (runInDocker) { + when(fileSystem.readLocalFile('/proc/self/cgroup')).thenResolve('hello docker world'); + } + // Create our working python and process service. + const workingService = createTypeMoq<IPythonExecutionService>('working'); + setupWorkingPythonService(workingService, notebookStdErr, runInDocker); + const missingKernelService = createTypeMoq<IPythonExecutionService>('missingKernel'); + setupMissingKernelPythonService(missingKernelService, notebookStdErr); + const missingNotebookService = createTypeMoq<IPythonExecutionService>('missingNotebook'); + setupMissingNotebookPythonService(missingNotebookService); + const missingNotebookService2 = createTypeMoq<IPythonExecutionService>('missingNotebook2'); + setupMissingNotebookPythonService(missingNotebookService2); + const processService = createTypeMoq<IProcessService>('working process'); + setupWorkingProcessService(processService, notebookStdErr); + setupMissingKernelProcessService(processService, notebookStdErr); + setupPathProcessService(jupyterOnPath, processService, notebookStdErr); + when(executionFactory.create(argThat((o) => o.pythonPath && o.pythonPath === workingPython.path))).thenResolve( + workingService.object + ); + when( + executionFactory.create(argThat((o) => o.pythonPath && o.pythonPath === missingKernelPython.path)) + ).thenResolve(missingKernelService.object); + when( + executionFactory.create(argThat((o) => o.pythonPath && o.pythonPath === missingNotebookPython.path)) + ).thenResolve(missingNotebookService.object); + when( + executionFactory.create(argThat((o) => o.pythonPath && o.pythonPath === missingNotebookPython2.path)) + ).thenResolve(missingNotebookService2.object); + + when( + executionFactory.createDaemon(argThat((o) => o.pythonPath && o.pythonPath === workingPython.path)) + ).thenResolve((workingService.object as unknown) as IPythonDaemonExecutionService); + + when( + executionFactory.createDaemon(argThat((o) => o.pythonPath && o.pythonPath === missingKernelPython.path)) + ).thenResolve((missingKernelService.object as unknown) as IPythonDaemonExecutionService); + + when( + executionFactory.createDaemon(argThat((o) => o.pythonPath && o.pythonPath === missingNotebookPython.path)) + ).thenResolve((missingNotebookService.object as unknown) as IPythonDaemonExecutionService); + + when( + executionFactory.createDaemon(argThat((o) => o.pythonPath && o.pythonPath === missingNotebookPython2.path)) + ).thenResolve((missingNotebookService2.object as unknown) as IPythonDaemonExecutionService); + + let activeService = workingService; + if (activeInterpreter === missingKernelPython) { + activeService = missingKernelService; + } else if (activeInterpreter === missingNotebookPython) { + activeService = missingNotebookService; + } else if (activeInterpreter === missingNotebookPython2) { + activeService = missingNotebookService2; + } + when(executionFactory.create(argThat((o) => !o || !o.pythonPath))).thenResolve(activeService.object); + when( + executionFactory.createActivatedEnvironment(argThat((o) => !o || o.interpreter === activeInterpreter)) + ).thenResolve(activeService.object); + when( + executionFactory.createActivatedEnvironment(argThat((o) => o && o.interpreter.path === workingPython.path)) + ).thenResolve(workingService.object); + when( + executionFactory.createActivatedEnvironment( + argThat((o) => o && o.interpreter.path === missingKernelPython.path) + ) + ).thenResolve(missingKernelService.object); + when( + executionFactory.createActivatedEnvironment( + argThat((o) => o && o.interpreter.path === missingNotebookPython.path) + ) + ).thenResolve(missingNotebookService.object); + when( + executionFactory.createActivatedEnvironment( + argThat((o) => o && o.interpreter.path === missingNotebookPython2.path) + ) + ).thenResolve(missingNotebookService2.object); + when(processServiceFactory.create()).thenResolve(processService.object); + + when(liveShare.getApi()).thenResolve(null); + + // Service container needs logger, file system, and config service + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(configService)); + when(serviceContainer.get<IDataScienceFileSystem>(IDataScienceFileSystem)).thenReturn(instance(fileSystem)); + when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspaceService)); + when(serviceContainer.get<IApplicationShell>(IApplicationShell)).thenReturn(instance(application)); + when(configService.getSettings(anything())).thenReturn(pythonSettings); + when(workspaceService.onDidChangeConfiguration).thenReturn(configChangeEvent.event); + when(application.withProgress(anything(), anything())).thenCall( + (_, cb: (_: any, token: any) => Promise<any>) => { + return new Promise((resolve, reject) => { + cb({ report: noop }, new CancellationTokenSource().token).then(resolve).catch(reject); + }); + } + ); + + // Setup default settings + pythonSettings.datascience = { + allowImportFromNotebook: true, + alwaysTrustNotebooks: true, + jupyterLaunchTimeout: 10, + jupyterLaunchRetries: 3, + enabled: true, + jupyterServerURI: 'local', + // tslint:disable-next-line: no-invalid-template-strings + notebookFileRoot: '${fileDirname}', + changeDirOnImportExport: true, + useDefaultConfigForJupyter: true, + jupyterInterruptTimeout: 10000, + searchForJupyter: !skipSearch, + showCellInputCode: true, + collapseCellInputCodeByDefault: true, + allowInput: true, + maxOutputSize: 400, + enableScrollingForCellOutputs: true, + errorBackgroundColor: '#FFFFFF', + sendSelectionToInteractiveWindow: false, + variableExplorerExclude: 'module;function;builtin_function_or_method', + codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', + markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', + allowLiveShare: false, + enablePlotViewer: true, + runStartupCommands: '', + debugJustMyCode: true, + variableQueries: [], + jupyterCommandLineArguments: [], + widgetScriptSources: [], + interactiveWindowMode: 'single' + }; + + // Service container also needs to generate jupyter servers. However we can't use a mock as that messes up returning + // this object from a promise + when(serviceContainer.get<INotebookServer>(INotebookServer)).thenReturn(new MockJupyterServer()); + + when(knownSearchPaths.getSearchPaths()).thenReturn(['/foo/bar']); + + // We also need a file system + const tempFile = { + dispose: () => { + return undefined; + }, + filePath: '/foo/bar/baz.py' + }; + when(fileSystem.createTemporaryLocalFile(anything())).thenResolve(tempFile); + when(fileSystem.createLocalDirectory(anything())).thenResolve(); + when(fileSystem.deleteLocalDirectory(anything())).thenResolve(); + when(fileSystem.localFileExists(workingKernelSpec)).thenResolve(true); + when(fileSystem.readLocalFile(workingKernelSpec)).thenResolve( + '{"display_name":"Python 3","language":"python","argv":["/foo/bar/python.exe","-m","ipykernel_launcher","-f","{connection_file}"]}' + ); + + const persistentSateFactory = mock(PersistentStateFactory); + const persistentState = mock(PersistentState); + when(persistentState.updateValue(anything())).thenResolve(); + when(persistentSateFactory.createGlobalPersistentState(anything())).thenReturn(instance(persistentState)); + when(persistentSateFactory.createGlobalPersistentState(anything(), anything())).thenReturn( + instance(persistentState) + ); + when(persistentSateFactory.createWorkspacePersistentState(anything())).thenReturn(instance(persistentState)); + when(persistentSateFactory.createWorkspacePersistentState(anything(), anything())).thenReturn( + instance(persistentState) + ); + when(serviceContainer.get<IInterpreterService>(IInterpreterService)).thenReturn(instance(interpreterService)); + when(serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory)).thenReturn( + instance(processServiceFactory) + ); + when(serviceContainer.get<IEnvironmentActivationService>(IEnvironmentActivationService)).thenReturn( + instance(activationHelper) + ); + when(serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory)).thenReturn( + instance(executionFactory) + ); + kernelSelector = mock(KernelSelector); + const kernelSpec: IJupyterKernelSpec = { + argv: [], + display_name: 'hello', + language: PYTHON_LANGUAGE, + name: 'hello', + path: '', + env: undefined + }; + when( + kernelSelector.getPreferredKernelForLocalConnection( + anything(), + anything(), + anything(), + anything(), + anything(), + anything() + ) + ).thenResolve({ + kernelSpec, + kind: 'startUsingKernelSpec' + }); + + const dependencyService = mock(JupyterInterpreterDependencyService); + when(dependencyService.areDependenciesInstalled(anything(), anything())).thenCall( + async (interpreter: PythonEnvironment) => { + if (interpreter === missingNotebookPython) { + return false; + } + return true; + } + ); + when(dependencyService.isExportSupported(anything(), anything())).thenCall( + async (interpreter: PythonEnvironment) => { + if (interpreter === missingNotebookPython) { + return false; + } + return true; + } + ); + when(dependencyService.getDependenciesNotInstalled(anything(), anything())).thenCall( + async (interpreter: PythonEnvironment) => { + if (interpreter === missingNotebookPython) { + return [Product.jupyter]; + } + return []; + } + ); + const oldStore = mock(JupyterInterpreterOldCacheStateStore); + when(oldStore.getCachedInterpreterPath()).thenReturn(); + const jupyterInterpreterService = mock(JupyterInterpreterService); + when(jupyterInterpreterService.getSelectedInterpreter(anything())).thenResolve(activeInterpreter); + const jupyterCmdExecutionService = new JupyterInterpreterSubCommandExecutionService( + instance(jupyterInterpreterService), + instance(interpreterService), + instance(dependencyService), + instance(fileSystem), + instance(executionFactory), + instance(mock<IOutputChannel>()), + instance(mock<IPathUtils>()) + ); + when(serviceContainer.get<IJupyterSubCommandExecutionService>(IJupyterSubCommandExecutionService)).thenReturn( + jupyterCmdExecutionService + ); + notebookStarter = new NotebookStarter( + jupyterCmdExecutionService, + instance(fileSystem), + instance(serviceContainer), + instance(jupyterOutputChannel) + ); + when(serviceContainer.get<KernelSelector>(KernelSelector)).thenReturn(instance(kernelSelector)); + when(serviceContainer.get<NotebookStarter>(NotebookStarter)).thenReturn(notebookStarter); + return { + executionService: activeService.object, + jupyterExecutionFactory: new JupyterExecutionFactory( + instance(liveShare), + instance(interpreterService), + (disposableRegistry as unknown) as any[], + disposableRegistry, + instance(fileSystem), + instance(workspaceService), + instance(configService), + instance(kernelSelector), + notebookStarter, + instance(application), + instance(jupyterOutputChannel), + instance(serviceContainer) + ) + }; + } + + test('Working notebook and commands found', async () => { + const jupyterExecutionFactory = createExecution(workingPython); + + await assert.eventually.equal(jupyterExecutionFactory.isNotebookSupported(), true, 'Notebook not supported'); + await assert.eventually.equal(jupyterExecutionFactory.isImportSupported(), true, 'Import not supported'); + const usableInterpreter = await jupyterExecutionFactory.getUsableJupyterPython(); + assert.isOk(usableInterpreter, 'Usable interpreter not found'); + await assert.isFulfilled(jupyterExecutionFactory.connectToNotebookServer(), 'Should be able to start a server'); + }).timeout(10000); + + test('Includes correct args for running in docker', async () => { + const { jupyterExecutionFactory } = createExecutionAndReturnProcessService( + workingPython, + undefined, + undefined, + true + ); + + await assert.eventually.equal(jupyterExecutionFactory.isNotebookSupported(), true, 'Notebook not supported'); + await assert.eventually.equal(jupyterExecutionFactory.isImportSupported(), true, 'Import not supported'); + const usableInterpreter = await jupyterExecutionFactory.getUsableJupyterPython(); + assert.isOk(usableInterpreter, 'Usable interpreter not found'); + await assert.isFulfilled(jupyterExecutionFactory.connectToNotebookServer(), 'Should be able to start a server'); + }).timeout(10000); + + test('Failing notebook throws exception', async () => { + const execution = createExecution(missingNotebookPython); + when(interpreterService.getInterpreters(anything())).thenResolve([missingNotebookPython]); + await assert.isRejected(execution.connectToNotebookServer(), 'Data Science library jupyter is not installed.'); + }).timeout(10000); + + test('Missing kernel python still finds interpreter', async () => { + const execution = createExecution(missingKernelPython); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(missingKernelPython); + await assert.eventually.equal(execution.isNotebookSupported(), true, 'Notebook not supported'); + const usableInterpreter = await execution.getUsableJupyterPython(); + assert.isOk(usableInterpreter, 'Usable interpreter not found'); + if (usableInterpreter) { + // Linter + assert.equal(usableInterpreter.path, missingKernelPython.path); + assert.equal( + usableInterpreter.version!.major, + missingKernelPython.version!.major, + 'Found interpreter should match on major' + ); + assert.equal( + usableInterpreter.version!.minor, + missingKernelPython.version!.minor, + 'Found interpreter should match on minor' + ); + } + }).timeout(10000); + + test('If active interpreter does not support notebooks then no support for notebooks', async () => { + const execution = createExecution(missingNotebookPython); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(missingNotebookPython); + await assert.eventually.equal(execution.isNotebookSupported(), false); + }); +}); diff --git a/src/test/datascience/executionServiceMock.ts b/src/test/datascience/executionServiceMock.ts new file mode 100644 index 000000000000..0d91a51a5850 --- /dev/null +++ b/src/test/datascience/executionServiceMock.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { SemVer } from 'semver'; + +import { ErrorUtils } from '../../client/common/errors/errorUtils'; +import { ModuleNotInstalledError } from '../../client/common/errors/moduleNotInstalledError'; +import { BufferDecoder } from '../../client/common/process/decoder'; +import { ProcessService } from '../../client/common/process/proc'; +import { + ExecutionResult, + IPythonExecutionService, + ObservableExecutionResult, + SpawnOptions +} from '../../client/common/process/types'; +import { Architecture } from '../../client/common/utils/platform'; +import { buildPythonExecInfo } from '../../client/pythonEnvironments/exec'; +import { InterpreterInformation } from '../../client/pythonEnvironments/info'; + +export class MockPythonExecutionService implements IPythonExecutionService { + private procService: ProcessService; + private pythonPath: string = 'python'; + + constructor() { + this.procService = new ProcessService(new BufferDecoder()); + } + + public getInterpreterInformation(): Promise<InterpreterInformation> { + return Promise.resolve({ + path: '', + version: new SemVer('3.6.0-beta'), + sysVersion: '1.0', + sysPrefix: '1.0', + architecture: Architecture.x64 + }); + } + + public getExecutablePath(): Promise<string> { + return Promise.resolve(this.pythonPath); + } + public isModuleInstalled(moduleName: string): Promise<boolean> { + return this.procService + .exec(this.pythonPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true }) + .then(() => true) + .catch(() => false); + } + public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult<string> { + const opts: SpawnOptions = { ...options }; + return this.procService.execObservable(this.pythonPath, args, opts); + } + public execModuleObservable( + moduleName: string, + args: string[], + options: SpawnOptions + ): ObservableExecutionResult<string> { + const opts: SpawnOptions = { ...options }; + return this.procService.execObservable(this.pythonPath, ['-m', moduleName, ...args], opts); + } + public exec(args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> { + const opts: SpawnOptions = { ...options }; + return this.procService.exec(this.pythonPath, args, opts); + } + public async execModule( + moduleName: string, + args: string[], + options: SpawnOptions + ): Promise<ExecutionResult<string>> { + const opts: SpawnOptions = { ...options }; + const result = await this.procService.exec(this.pythonPath, ['-m', moduleName, ...args], opts); + + // If a module is not installed we'll have something in stderr. + if (moduleName && ErrorUtils.outputHasModuleNotInstalledError(moduleName!, result.stderr)) { + const isInstalled = await this.isModuleInstalled(moduleName!); + if (!isInstalled) { + throw new ModuleNotInstalledError(moduleName!); + } + } + + return result; + } + public getExecutionInfo(args: string[]) { + return buildPythonExecInfo(this.pythonPath, args); + } +} diff --git a/src/test/datascience/export/exportFileOpener.unit.test.ts b/src/test/datascience/export/exportFileOpener.unit.test.ts new file mode 100644 index 000000000000..90b01e936468 --- /dev/null +++ b/src/test/datascience/export/exportFileOpener.unit.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { TextEditor, Uri } from 'vscode'; +import { IApplicationShell, IDocumentManager } from '../../../client/common/application/types'; +import { IBrowserService, IDisposable } from '../../../client/common/types'; +import { ExportFileOpener } from '../../../client/datascience/export/exportFileOpener'; +import { ExportFormat } from '../../../client/datascience/export/types'; +import { ProgressReporter } from '../../../client/datascience/progress/progressReporter'; +import { IDataScienceFileSystem } from '../../../client/datascience/types'; +import { getLocString } from '../../../datascience-ui/react-common/locReactSide'; + +suite('DataScience - Export File Opener', () => { + let fileOpener: ExportFileOpener; + let documentManager: IDocumentManager; + let fileSystem: IDataScienceFileSystem; + let applicationShell: IApplicationShell; + let browserService: IBrowserService; + setup(async () => { + documentManager = mock<IDocumentManager>(); + fileSystem = mock<IDataScienceFileSystem>(); + applicationShell = mock<IApplicationShell>(); + browserService = mock<IBrowserService>(); + const reporter = mock(ProgressReporter); + const editor = mock<TextEditor>(); + // tslint:disable-next-line: no-any + (instance(editor) as any).then = undefined; + // tslint:disable-next-line: no-any + when(reporter.createProgressIndicator(anything())).thenReturn(instance(mock<IDisposable>()) as any); + when(documentManager.openTextDocument(anything())).thenResolve(); + when(documentManager.showTextDocument(anything())).thenReturn(Promise.resolve(instance(editor))); + when(fileSystem.readFile(anything())).thenResolve(); + fileOpener = new ExportFileOpener( + instance(documentManager), + instance(fileSystem), + instance(applicationShell), + instance(browserService) + ); + }); + + test('Python File is opened if exported', async () => { + const uri = Uri.file('test.python'); + await fileOpener.openFile(ExportFormat.python, uri); + + verify(documentManager.showTextDocument(anything())).once(); + }); + test('HTML File opened if yes button pressed', async () => { + const uri = Uri.file('test.html'); + when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn( + Promise.resolve(getLocString('DataScience.openExportFileYes', 'Yes')) + ); + + await fileOpener.openFile(ExportFormat.html, uri); + + verify(browserService.launch(anything())).once(); + }); + test('HTML File not opened if no button button pressed', async () => { + const uri = Uri.file('test.html'); + when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn( + Promise.resolve(getLocString('DataScience.openExportFileNo', 'No')) + ); + + await fileOpener.openFile(ExportFormat.html, uri); + + verify(browserService.launch(anything())).never(); + }); + test('PDF File opened if yes button pressed', async () => { + const uri = Uri.file('test.pdf'); + when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn( + Promise.resolve(getLocString('DataScience.openExportFileYes', 'Yes')) + ); + + await fileOpener.openFile(ExportFormat.pdf, uri); + + verify(browserService.launch(anything())).once(); + }); + test('PDF File not opened if no button button pressed', async () => { + const uri = Uri.file('test.pdf'); + when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn( + Promise.resolve(getLocString('DataScience.openExportFileNo', 'No')) + ); + + await fileOpener.openFile(ExportFormat.pdf, uri); + + verify(browserService.launch(anything())).never(); + }); +}); diff --git a/src/test/datascience/export/exportManager.test.ts b/src/test/datascience/export/exportManager.test.ts new file mode 100644 index 000000000000..5ed02e7fd743 --- /dev/null +++ b/src/test/datascience/export/exportManager.test.ts @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { IDisposable } from '../../../client/common/types'; +import { ExportDependencyChecker } from '../../../client/datascience/export/exportDependencyChecker'; +import { ExportFileOpener } from '../../../client/datascience/export/exportFileOpener'; +import { ExportManager } from '../../../client/datascience/export/exportManager'; +import { ExportUtil } from '../../../client/datascience/export/exportUtil'; +import { ExportFormat, IExport, IExportManagerFilePicker } from '../../../client/datascience/export/types'; +import { ProgressReporter } from '../../../client/datascience/progress/progressReporter'; +import { IDataScienceFileSystem, INotebookModel } from '../../../client/datascience/types'; + +suite('DataScience - Export Manager', () => { + let exporter: ExportManager; + let exportPython: IExport; + let exportHtml: IExport; + let exportPdf: IExport; + let fileSystem: IDataScienceFileSystem; + let exportUtil: ExportUtil; + let filePicker: IExportManagerFilePicker; + let appShell: IApplicationShell; + let exportFileOpener: ExportFileOpener; + let exportDependencyChecker: ExportDependencyChecker; + const model = mock<INotebookModel>(); + setup(async () => { + exportUtil = mock<ExportUtil>(); + const reporter = mock(ProgressReporter); + filePicker = mock<IExportManagerFilePicker>(); + fileSystem = mock<IDataScienceFileSystem>(); + exportPython = mock<IExport>(); + exportHtml = mock<IExport>(); + exportPdf = mock<IExport>(); + appShell = mock<IApplicationShell>(); + exportFileOpener = mock<ExportFileOpener>(); + exportDependencyChecker = mock<ExportDependencyChecker>(); + // tslint:disable-next-line: no-any + when(filePicker.getExportFileLocation(anything(), anything(), anything())).thenReturn( + Promise.resolve(Uri.file('test.pdf')) + ); + // tslint:disable-next-line: no-empty + when(appShell.showErrorMessage(anything())).thenResolve(); + // tslint:disable-next-line: no-empty + when(exportUtil.generateTempDir()).thenResolve({ path: 'test', dispose: () => {} }); + when(exportUtil.makeFileInDirectory(anything(), anything(), anything())).thenResolve('foo'); + // tslint:disable-next-line: no-empty + when(fileSystem.createTemporaryLocalFile(anything())).thenResolve({ filePath: 'test', dispose: () => {} }); + when(exportPdf.export(anything(), anything(), anything())).thenResolve(); + when(filePicker.getExportFileLocation(anything(), anything())).thenResolve(Uri.file('foo')); + when(exportDependencyChecker.checkDependencies(anything())).thenResolve(); + when(exportFileOpener.openFile(anything(), anything())).thenResolve(); + // tslint:disable-next-line: no-any + when(reporter.createProgressIndicator(anything(), anything())).thenReturn(instance(mock<IDisposable>()) as any); + exporter = new ExportManager( + instance(exportPdf), + instance(exportHtml), + instance(exportPython), + instance(fileSystem), + instance(filePicker), + instance(reporter), + instance(exportUtil), + instance(appShell), + instance(exportFileOpener), + instance(exportDependencyChecker) + ); + }); + + test('Remove svg is called when exporting to PDF', async () => { + await exporter.export(ExportFormat.pdf, model); + verify(exportUtil.removeSvgs(anything())).once(); + }); + test('Erorr message is shown if export fails', async () => { + when(exportHtml.export(anything(), anything(), anything())).thenThrow(new Error('failed...')); + await exporter.export(ExportFormat.html, model); + verify(appShell.showErrorMessage(anything())).once(); + verify(exportFileOpener.openFile(anything(), anything())).never(); + }); + test('Export to PDF is called when export method is PDF', async () => { + await exporter.export(ExportFormat.pdf, model); + verify(exportPdf.export(anything(), anything(), anything())).once(); + verify(exportFileOpener.openFile(ExportFormat.pdf, anything())).once(); + }); + test('Export to HTML is called when export method is HTML', async () => { + await exporter.export(ExportFormat.html, model); + verify(exportHtml.export(anything(), anything(), anything())).once(); + verify(exportFileOpener.openFile(ExportFormat.html, anything())).once(); + }); + test('Export to Python is called when export method is Python', async () => { + await exporter.export(ExportFormat.python, model); + verify(exportPython.export(anything(), anything(), anything())).once(); + verify(exportFileOpener.openFile(ExportFormat.python, anything())).once(); + }); +}); diff --git a/src/test/datascience/export/exportToHTML.test.ts b/src/test/datascience/export/exportToHTML.test.ts new file mode 100644 index 000000000000..d179aa49a714 --- /dev/null +++ b/src/test/datascience/export/exportToHTML.test.ts @@ -0,0 +1,50 @@ +// Licensed under the MIT License. +// Copyright (c) Microsoft Corporation. All rights reserved. + +// tslint:disable: no-var-requires no-require-imports no-invalid-this no-any +import { assert } from 'chai'; +import * as path from 'path'; +import { CancellationTokenSource, Uri } from 'vscode'; +import { ExportFormat, IExport } from '../../../client/datascience/export/types'; +import { IDataScienceFileSystem } from '../../../client/datascience/types'; +import { IExtensionTestApi } from '../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { closeActiveWindows, initialize } from '../../initialize'; + +suite('DataScience - Export HTML', () => { + let api: IExtensionTestApi; + suiteSetup(async function () { + this.timeout(10_000); + api = await initialize(); + // Export to HTML tests require jupyter to run. Othewrise can't + // run any of our variable execution code + const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; + if (!isRollingBuild) { + // tslint:disable-next-line:no-console + console.log('Skipping Export to HTML tests. Requires python environment'); + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + teardown(closeActiveWindows); + suiteTeardown(closeActiveWindows); + test('Export To HTML', async () => { + const fileSystem = api.serviceContainer.get<IDataScienceFileSystem>(IDataScienceFileSystem); + const exportToHTML = api.serviceContainer.get<IExport>(IExport, ExportFormat.html); + const file = await fileSystem.createTemporaryLocalFile('.html'); + const target = Uri.file(file.filePath); + await file.dispose(); + const token = new CancellationTokenSource(); + await exportToHTML.export( + Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'export', 'test.ipynb')), + target, + token.token + ); + + assert.equal(await fileSystem.localFileExists(target.fsPath), true); + const fileContents = await fileSystem.readLocalFile(target.fsPath); + assert.include(fileContents, '<!DOCTYPE html>'); + // this is the content of a cell + assert.include(fileContents, 'f6886df81f3d4023a2122cc3f55fdbec'); + }); +}); diff --git a/src/test/datascience/export/exportToPython.test.ts b/src/test/datascience/export/exportToPython.test.ts new file mode 100644 index 000000000000..8845c6d890b4 --- /dev/null +++ b/src/test/datascience/export/exportToPython.test.ts @@ -0,0 +1,46 @@ +// Licensed under the MIT License. +// Copyright (c) Microsoft Corporation. All rights reserved. + +// tslint:disable: no-var-requires no-require-imports no-invalid-this no-any +import { assert } from 'chai'; +import * as path from 'path'; +import { CancellationTokenSource, Uri } from 'vscode'; +import { IDocumentManager } from '../../../client/common/application/types'; +import { ExportFormat, IExport } from '../../../client/datascience/export/types'; +import { IDataScienceFileSystem } from '../../../client/datascience/types'; +import { IExtensionTestApi } from '../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { closeActiveWindows, initialize } from '../../initialize'; + +suite('DataScience - Export Python', () => { + let api: IExtensionTestApi; + suiteSetup(async function () { + this.timeout(10_000); + api = await initialize(); + // Export to Python tests require jupyter to run. Othewrise can't + // run any of our variable execution code + const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; + if (!isRollingBuild) { + // tslint:disable-next-line:no-console + console.log('Skipping Export to Python tests. Requires python environment'); + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + teardown(closeActiveWindows); + suiteTeardown(closeActiveWindows); + test('Export To Python', async () => { + const fileSystem = api.serviceContainer.get<IDataScienceFileSystem>(IDataScienceFileSystem); + const exportToPython = api.serviceContainer.get<IExport>(IExport, ExportFormat.python); + const target = Uri.file((await fileSystem.createTemporaryLocalFile('.py')).filePath); + const token = new CancellationTokenSource(); + await exportToPython.export( + Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'export', 'test.ipynb')), + target, + token.token + ); + + const documentManager = api.serviceContainer.get<IDocumentManager>(IDocumentManager); + assert.include(documentManager.activeTextEditor!.document.getText(), 'tim = 1'); + }); +}); diff --git a/src/test/datascience/export/exportUtil.test.ts b/src/test/datascience/export/exportUtil.test.ts new file mode 100644 index 000000000000..c59d133f3feb --- /dev/null +++ b/src/test/datascience/export/exportUtil.test.ts @@ -0,0 +1,60 @@ +// Licensed under the MIT License. +// Copyright (c) Microsoft Corporation. All rights reserved. + +// tslint:disable: no-var-requires no-require-imports no-invalid-this no-any +import { nbformat } from '@jupyterlab/coreutils'; +import { assert } from 'chai'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { ExportUtil } from '../../../client/datascience/export/exportUtil'; +import { INotebookStorage } from '../../../client/datascience/types'; +import { IExtensionTestApi } from '../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { closeActiveWindows, initialize } from '../../initialize'; + +suite('DataScience - Export Util', () => { + let api: IExtensionTestApi; + suiteSetup(async function () { + this.timeout(10_000); + api = await initialize(); + // Export Util tests require jupyter to run. Othewrise can't + // run any of our variable execution code + const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; + if (!isRollingBuild) { + // tslint:disable-next-line:no-console + console.log('Skipping Export Util tests. Requires python environment'); + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + teardown(closeActiveWindows); + suiteTeardown(closeActiveWindows); + test('Remove svgs from model', async () => { + const exportUtil = api.serviceContainer.get<ExportUtil>(ExportUtil); + const notebookStorage = api.serviceContainer.get<INotebookStorage>(INotebookStorage); + const file = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'export', 'testPDF.ipynb') + ); + + await exportUtil.removeSvgs(file); + const model = await notebookStorage.getOrCreateModel(file); + + // make sure no svg exists in model + const SVG = 'image/svg+xml'; + const PNG = 'image/png'; + for (const cell of model.cells) { + const outputs = cell.data.outputs; + if (outputs as nbformat.IOutput[]) { + for (const output of outputs as nbformat.IOutput[]) { + if (output.data as nbformat.IMimeBundle) { + const data = output.data as nbformat.IMimeBundle; + if (PNG in data) { + // we only remove svgs if there is a pdf available + assert.equal(SVG in data, false); + } + } + } + } + } + }); +}); diff --git a/src/test/datascience/export/test.ipynb b/src/test/datascience/export/test.ipynb new file mode 100644 index 000000000000..f9a7d41fbd1f --- /dev/null +++ b/src/test/datascience/export/test.ipynb @@ -0,0 +1,39 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tim = 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"f6886df81f3d4023a2122cc3f55fdbec\")" + ] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "orig_nbformat": 2 + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/src/test/datascience/export/testPDF.ipynb b/src/test/datascience/export/testPDF.ipynb new file mode 100644 index 000000000000..050d9ac5e97d --- /dev/null +++ b/src/test/datascience/export/testPDF.ipynb @@ -0,0 +1,62 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": "[<matplotlib.lines.Line2D at 0x1a0eb999460>]" + }, + "metadata": {}, + "execution_count": 1 + }, + { + "output_type": "display_data", + "data": { + "text/plain": "<Figure size 432x288 with 1 Axes>", + "image/svg+xml": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\r\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<!-- Created with matplotlib (https://matplotlib.org/) -->\r\n<svg height=\"248.518125pt\" version=\"1.1\" viewBox=\"0 0 372.103125 248.518125\" width=\"372.103125pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\r\n <defs>\r\n <style type=\"text/css\">\r\n*{stroke-linecap:butt;stroke-linejoin:round;}\r\n </style>\r\n </defs>\r\n <g id=\"figure_1\">\r\n <g id=\"patch_1\">\r\n <path d=\"M 0 248.518125 \r\nL 372.103125 248.518125 \r\nL 372.103125 0 \r\nL 0 0 \r\nz\r\n\" style=\"fill:none;\"/>\r\n </g>\r\n <g id=\"axes_1\">\r\n <g id=\"patch_2\">\r\n <path d=\"M 30.103125 224.64 \r\nL 364.903125 224.64 \r\nL 364.903125 7.2 \r\nL 30.103125 7.2 \r\nz\r\n\" style=\"fill:#ffffff;\"/>\r\n </g>\r\n <g id=\"matplotlib.axis_1\">\r\n <g id=\"xtick_1\">\r\n <g id=\"line2d_1\">\r\n <defs>\r\n <path d=\"M 0 0 \r\nL 0 3.5 \r\n\" id=\"m53b1dcb76a\" style=\"stroke:#000000;stroke-width:0.8;\"/>\r\n </defs>\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"45.321307\" xlink:href=\"#m53b1dcb76a\" y=\"224.64\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_1\">\r\n <!-- 1.0 -->\r\n <defs>\r\n <path d=\"M 12.40625 8.296875 \r\nL 28.515625 8.296875 \r\nL 28.515625 63.921875 \r\nL 10.984375 60.40625 \r\nL 10.984375 69.390625 \r\nL 28.421875 72.90625 \r\nL 38.28125 72.90625 \r\nL 38.28125 8.296875 \r\nL 54.390625 8.296875 \r\nL 54.390625 0 \r\nL 12.40625 0 \r\nz\r\n\" id=\"DejaVuSans-49\"/>\r\n <path d=\"M 10.6875 12.40625 \r\nL 21 12.40625 \r\nL 21 0 \r\nL 10.6875 0 \r\nz\r\n\" id=\"DejaVuSans-46\"/>\r\n <path d=\"M 31.78125 66.40625 \r\nQ 24.171875 66.40625 20.328125 58.90625 \r\nQ 16.5 51.421875 16.5 36.375 \r\nQ 16.5 21.390625 20.328125 13.890625 \r\nQ 24.171875 6.390625 31.78125 6.390625 \r\nQ 39.453125 6.390625 43.28125 13.890625 \r\nQ 47.125 21.390625 47.125 36.375 \r\nQ 47.125 51.421875 43.28125 58.90625 \r\nQ 39.453125 66.40625 31.78125 66.40625 \r\nz\r\nM 31.78125 74.21875 \r\nQ 44.046875 74.21875 50.515625 64.515625 \r\nQ 56.984375 54.828125 56.984375 36.375 \r\nQ 56.984375 17.96875 50.515625 8.265625 \r\nQ 44.046875 -1.421875 31.78125 -1.421875 \r\nQ 19.53125 -1.421875 13.0625 8.265625 \r\nQ 6.59375 17.96875 6.59375 36.375 \r\nQ 6.59375 54.828125 13.0625 64.515625 \r\nQ 19.53125 74.21875 31.78125 74.21875 \r\nz\r\n\" id=\"DejaVuSans-48\"/>\r\n </defs>\r\n <g transform=\"translate(37.369744 239.238438)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-49\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"xtick_2\">\r\n <g id=\"line2d_2\">\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"106.194034\" xlink:href=\"#m53b1dcb76a\" y=\"224.64\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_2\">\r\n <!-- 1.2 -->\r\n <defs>\r\n <path d=\"M 19.1875 8.296875 \r\nL 53.609375 8.296875 \r\nL 53.609375 0 \r\nL 7.328125 0 \r\nL 7.328125 8.296875 \r\nQ 12.9375 14.109375 22.625 23.890625 \r\nQ 32.328125 33.6875 34.8125 36.53125 \r\nQ 39.546875 41.84375 41.421875 45.53125 \r\nQ 43.3125 49.21875 43.3125 52.78125 \r\nQ 43.3125 58.59375 39.234375 62.25 \r\nQ 35.15625 65.921875 28.609375 65.921875 \r\nQ 23.96875 65.921875 18.8125 64.3125 \r\nQ 13.671875 62.703125 7.8125 59.421875 \r\nL 7.8125 69.390625 \r\nQ 13.765625 71.78125 18.9375 73 \r\nQ 24.125 74.21875 28.421875 74.21875 \r\nQ 39.75 74.21875 46.484375 68.546875 \r\nQ 53.21875 62.890625 53.21875 53.421875 \r\nQ 53.21875 48.921875 51.53125 44.890625 \r\nQ 49.859375 40.875 45.40625 35.40625 \r\nQ 44.1875 33.984375 37.640625 27.21875 \r\nQ 31.109375 20.453125 19.1875 8.296875 \r\nz\r\n\" id=\"DejaVuSans-50\"/>\r\n </defs>\r\n <g transform=\"translate(98.242472 239.238438)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-49\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"xtick_3\">\r\n <g id=\"line2d_3\">\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"167.066761\" xlink:href=\"#m53b1dcb76a\" y=\"224.64\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_3\">\r\n <!-- 1.4 -->\r\n <defs>\r\n <path d=\"M 37.796875 64.3125 \r\nL 12.890625 25.390625 \r\nL 37.796875 25.390625 \r\nz\r\nM 35.203125 72.90625 \r\nL 47.609375 72.90625 \r\nL 47.609375 25.390625 \r\nL 58.015625 25.390625 \r\nL 58.015625 17.1875 \r\nL 47.609375 17.1875 \r\nL 47.609375 0 \r\nL 37.796875 0 \r\nL 37.796875 17.1875 \r\nL 4.890625 17.1875 \r\nL 4.890625 26.703125 \r\nz\r\n\" id=\"DejaVuSans-52\"/>\r\n </defs>\r\n <g transform=\"translate(159.115199 239.238438)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-49\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-52\"/>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"xtick_4\">\r\n <g id=\"line2d_4\">\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"227.939489\" xlink:href=\"#m53b1dcb76a\" y=\"224.64\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_4\">\r\n <!-- 1.6 -->\r\n <defs>\r\n <path d=\"M 33.015625 40.375 \r\nQ 26.375 40.375 22.484375 35.828125 \r\nQ 18.609375 31.296875 18.609375 23.390625 \r\nQ 18.609375 15.53125 22.484375 10.953125 \r\nQ 26.375 6.390625 33.015625 6.390625 \r\nQ 39.65625 6.390625 43.53125 10.953125 \r\nQ 47.40625 15.53125 47.40625 23.390625 \r\nQ 47.40625 31.296875 43.53125 35.828125 \r\nQ 39.65625 40.375 33.015625 40.375 \r\nz\r\nM 52.59375 71.296875 \r\nL 52.59375 62.3125 \r\nQ 48.875 64.0625 45.09375 64.984375 \r\nQ 41.3125 65.921875 37.59375 65.921875 \r\nQ 27.828125 65.921875 22.671875 59.328125 \r\nQ 17.53125 52.734375 16.796875 39.40625 \r\nQ 19.671875 43.65625 24.015625 45.921875 \r\nQ 28.375 48.1875 33.59375 48.1875 \r\nQ 44.578125 48.1875 50.953125 41.515625 \r\nQ 57.328125 34.859375 57.328125 23.390625 \r\nQ 57.328125 12.15625 50.6875 5.359375 \r\nQ 44.046875 -1.421875 33.015625 -1.421875 \r\nQ 20.359375 -1.421875 13.671875 8.265625 \r\nQ 6.984375 17.96875 6.984375 36.375 \r\nQ 6.984375 53.65625 15.1875 63.9375 \r\nQ 23.390625 74.21875 37.203125 74.21875 \r\nQ 40.921875 74.21875 44.703125 73.484375 \r\nQ 48.484375 72.75 52.59375 71.296875 \r\nz\r\n\" id=\"DejaVuSans-54\"/>\r\n </defs>\r\n <g transform=\"translate(219.987926 239.238438)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-49\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-54\"/>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"xtick_5\">\r\n <g id=\"line2d_5\">\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"288.812216\" xlink:href=\"#m53b1dcb76a\" y=\"224.64\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_5\">\r\n <!-- 1.8 -->\r\n <defs>\r\n <path d=\"M 31.78125 34.625 \r\nQ 24.75 34.625 20.71875 30.859375 \r\nQ 16.703125 27.09375 16.703125 20.515625 \r\nQ 16.703125 13.921875 20.71875 10.15625 \r\nQ 24.75 6.390625 31.78125 6.390625 \r\nQ 38.8125 6.390625 42.859375 10.171875 \r\nQ 46.921875 13.96875 46.921875 20.515625 \r\nQ 46.921875 27.09375 42.890625 30.859375 \r\nQ 38.875 34.625 31.78125 34.625 \r\nz\r\nM 21.921875 38.8125 \r\nQ 15.578125 40.375 12.03125 44.71875 \r\nQ 8.5 49.078125 8.5 55.328125 \r\nQ 8.5 64.0625 14.71875 69.140625 \r\nQ 20.953125 74.21875 31.78125 74.21875 \r\nQ 42.671875 74.21875 48.875 69.140625 \r\nQ 55.078125 64.0625 55.078125 55.328125 \r\nQ 55.078125 49.078125 51.53125 44.71875 \r\nQ 48 40.375 41.703125 38.8125 \r\nQ 48.828125 37.15625 52.796875 32.3125 \r\nQ 56.78125 27.484375 56.78125 20.515625 \r\nQ 56.78125 9.90625 50.3125 4.234375 \r\nQ 43.84375 -1.421875 31.78125 -1.421875 \r\nQ 19.734375 -1.421875 13.25 4.234375 \r\nQ 6.78125 9.90625 6.78125 20.515625 \r\nQ 6.78125 27.484375 10.78125 32.3125 \r\nQ 14.796875 37.15625 21.921875 38.8125 \r\nz\r\nM 18.3125 54.390625 \r\nQ 18.3125 48.734375 21.84375 45.5625 \r\nQ 25.390625 42.390625 31.78125 42.390625 \r\nQ 38.140625 42.390625 41.71875 45.5625 \r\nQ 45.3125 48.734375 45.3125 54.390625 \r\nQ 45.3125 60.0625 41.71875 63.234375 \r\nQ 38.140625 66.40625 31.78125 66.40625 \r\nQ 25.390625 66.40625 21.84375 63.234375 \r\nQ 18.3125 60.0625 18.3125 54.390625 \r\nz\r\n\" id=\"DejaVuSans-56\"/>\r\n </defs>\r\n <g transform=\"translate(280.860653 239.238438)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-49\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-56\"/>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"xtick_6\">\r\n <g id=\"line2d_6\">\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"349.684943\" xlink:href=\"#m53b1dcb76a\" y=\"224.64\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_6\">\r\n <!-- 2.0 -->\r\n <g transform=\"translate(341.733381 239.238438)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-50\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\r\n </g>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"matplotlib.axis_2\">\r\n <g id=\"ytick_1\">\r\n <g id=\"line2d_7\">\r\n <defs>\r\n <path d=\"M 0 0 \r\nL -3.5 0 \r\n\" id=\"mbac6a827fa\" style=\"stroke:#000000;stroke-width:0.8;\"/>\r\n </defs>\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"30.103125\" xlink:href=\"#mbac6a827fa\" y=\"214.756364\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_7\">\r\n <!-- 1.0 -->\r\n <g transform=\"translate(7.2 218.555582)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-49\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"ytick_2\">\r\n <g id=\"line2d_8\">\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"30.103125\" xlink:href=\"#mbac6a827fa\" y=\"175.221818\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_8\">\r\n <!-- 1.2 -->\r\n <g transform=\"translate(7.2 179.021037)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-49\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"ytick_3\">\r\n <g id=\"line2d_9\">\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"30.103125\" xlink:href=\"#mbac6a827fa\" y=\"135.687273\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_9\">\r\n <!-- 1.4 -->\r\n <g transform=\"translate(7.2 139.486491)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-49\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-52\"/>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"ytick_4\">\r\n <g id=\"line2d_10\">\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"30.103125\" xlink:href=\"#mbac6a827fa\" y=\"96.152727\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_10\">\r\n <!-- 1.6 -->\r\n <g transform=\"translate(7.2 99.951946)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-49\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-54\"/>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"ytick_5\">\r\n <g id=\"line2d_11\">\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"30.103125\" xlink:href=\"#mbac6a827fa\" y=\"56.618182\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_11\">\r\n <!-- 1.8 -->\r\n <g transform=\"translate(7.2 60.417401)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-49\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-56\"/>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"ytick_6\">\r\n <g id=\"line2d_12\">\r\n <g>\r\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"30.103125\" xlink:href=\"#mbac6a827fa\" y=\"17.083636\"/>\r\n </g>\r\n </g>\r\n <g id=\"text_12\">\r\n <!-- 2.0 -->\r\n <g transform=\"translate(7.2 20.882855)scale(0.1 -0.1)\">\r\n <use xlink:href=\"#DejaVuSans-50\"/>\r\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\r\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\r\n </g>\r\n </g>\r\n </g>\r\n </g>\r\n <g id=\"line2d_13\">\r\n <path clip-path=\"url(#p5811507758)\" d=\"M 45.321307 214.756364 \r\nL 349.684943 17.083636 \r\n\" style=\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/>\r\n </g>\r\n <g id=\"patch_3\">\r\n <path d=\"M 30.103125 224.64 \r\nL 30.103125 7.2 \r\n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\r\n </g>\r\n <g id=\"patch_4\">\r\n <path d=\"M 364.903125 224.64 \r\nL 364.903125 7.2 \r\n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\r\n </g>\r\n <g id=\"patch_5\">\r\n <path d=\"M 30.103125 224.64 \r\nL 364.903125 224.64 \r\n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\r\n </g>\r\n <g id=\"patch_6\">\r\n <path d=\"M 30.103125 7.2 \r\nL 364.903125 7.2 \r\n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\r\n </g>\r\n </g>\r\n </g>\r\n <defs>\r\n <clipPath id=\"p5811507758\">\r\n <rect height=\"217.44\" width=\"334.8\" x=\"30.103125\" y=\"7.2\"/>\r\n </clipPath>\r\n </defs>\r\n</svg>\r\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXQAAAD4CAYAAAD8Zh1EAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deXQV9f3/8ecHSICEEJawhxD2LQkKgYgrLq2AuCBaq9YNFbV2/bYSBBUUq4i1akuV4oZUq60kLKLiioIKKqhkJRDCFrYQAknIQpb7+f2R/HqoAgnk3kzu3NfjHA65meHOawi8zmQy8x5jrUVERPxfM6cDiIiId6jQRURcQoUuIuISKnQREZdQoYuIuEQLpzYcERFho6Ojndq8iIhf2rBhQ761ttPxljlW6NHR0axfv96pzYuI+CVjzI4TLdMpFxERl1Chi4i4hApdRMQlVOgiIi6hQhcRcYk6C90Y09MYs8oYk2mMSTfG/PY46xhjzF+NMdnGmBRjzHDfxBURkROpz2WLVcAfrLXfGmPCgA3GmA+ttRnHrDMO6F/7KwF4vvZ3ERFpJHUeoVtr91prv639uBjIBHr8YLUrgUW2xjqgnTGmm9fTioj4scpqD899ms3GXYd98v6ndA7dGBMNnAl89YNFPYBdx7zO5celjzFmijFmvTFm/YEDB04tqYiIH0vbXchVf/+CuSuzeC9tn0+2Ue87RY0xbYAk4HfW2qIfLj7OH/nRkzOstQuABQDx8fF6soaIuF55ZTV/+2QL8z/LoX1IMM/fOJxxsb45gVGvQjfGBFFT5q9ba5OPs0ou0POY15HAnobHExHxX+u3FzA1KYWcAyVcOyKSBy4bQnhIkM+2V2ehG2MM8BKQaa39ywlWWw78yhjzJjU/DC201u71XkwREf9x5GgVT67cxKJ1O+ge3ppFk0dx/oDjztPyqvocoZ8D3ASkGmO+r/3cdCAKwFo7H3gXGA9kA6XAbd6PKiLS9H22+QDTk1PZU1jGLaOjue/SgYS2bJw5iHVuxVr7Occ/R37sOha411uhRET8zeHSCmavyCTp21z6dgrlrbtGEx/doVEzODY+V0TELd5L3cuDy9I5VFrBry7sx68u6keroOaNnkOFLiJymvKKynloWTor0/cR06Mtr04eydDu4Y7lUaGLiJwiay1vbcjl0RUZlFd5SBw7iDvP602L5s6Ox1Khi4icgl0FpUxfksqaLfmMiu7AnEmx9OnUxulYgApdRKReqj2WRWu38+T7WRhg9pVDuTGhF82anfSakUalQhcRqUN2XjGJSals2HGICwZ04rGrY+nRrrXTsX5EhS4icgKV1R7+8dlW/vpxNiEtm/P0dcO46owe1Nxv2fSo0EVEjiM1t5CpSSlk7i3isrhuPHzFUCLatHQ61kmp0EVEjlFeWc0zH23hhTU5dAwN5h83jeDSoV2djlUvKnQRkVpf5RxkWnIq2/JLuC6+J9MvG0x4a98N0/I2FbqIBLzi8krmrszin+t20LNDa16/I4Fz+kU4HeuUqdBFJKCtyspjRnIqe4vKmXxOb/546QBCgv2zGv0ztYhIAx0qqWD2igySv9tN/85tSLrnbIZHtXc6VoOo0EUkoFhreSd1LzOXpVNYVslvLu7PvRf2pWWLxh+m5W0qdBEJGPuLynlgaRofZuwnLjKc1+5IYHC3tk7H8hoVuoi4nrWW/6zfxaPvZFJR5WH6+EFMPsf5YVrepkIXEVfbebCUackpfLn1IAm9O/DEpDiiI0KdjuUTKnQRcaVqj2Xhl9v58/tZNG9m+NPEGK4fGdWkhml5mwpdRFxn8/5ipi5O4ftdh7loUGf+NDGGbuFNb5iWt6nQRcQ1Kqo8PP/pVuat2kJYqyCe/fkZXDGse5MdpuVtKnQRcYWNuw6TmJTCpn3FXDGsOzMvH0LHJj5My9tU6CLi18oqqnn6o828uCaHzmGtePHmeC4Z0sXpWI5QoYuI31q79SD3J6ew/WAp14+K4v7xg2jbyn+GaXmbCl1E/E5ReSVz3tvEv77aSa+OIfzrzgTO7ut/w7S8TYUuIn7l48z9zFiSRl5xOVPO78PvLxlA62D/v23fG1ToIuIXDh45ysNvZ7B84x4Gdglj/k0jOKNnO6djNSkqdBFp0qy1LN+4h4ffzqC4vJLfXzKAe8b0JbiFu27b9wYVuog0WXsLy3hgSRofb8pjWM92zJ0Ux8CuYU7HarJU6CLS5Hg8lje/2cXj72ZS6fHwwGWDue2c3jR38W373qBCF5EmZXt+CdOSU1iXU8DZfTvy+NWx9OrozmFa3qZCF5Emoaraw8tfbOOpDzYT3LwZc66O5bqRPQPmtn1vqLPQjTEvAxOAPGttzHGWhwOvAVG17/dna+0r3g4qIu61aV8RiYtT2JhbyCWDu/DoVTF0DW/ldCy/U58j9IXAPGDRCZbfC2RYay83xnQCsowxr1trK7yUUURc6mhVNX9ftZXnVmUT3jqIv11/JhPiuumo/DTVWejW2tXGmOiTrQKEmZqvQBugAKjySjoRca3vdh4iMSmFzfuPMPHMHjw4YQgdQoOdjuXXvHEOfR6wHNgDhAHXWWs9x1vRGDMFmAIQFRXlhU2LiL8prajiqQ828/IX2+jathUv3xrPRYMCc5iWt3mj0C8FvgcuAvoCHxpj1lhri364orV2AbAAID4+3nph2yLiR77Mzmdacio7C0r5xVlRJI4dRFgAD9PyNm8U+m3AHGutBbKNMduAQcDXXnhvEXGBwrJKHn83kze/2UXviFD+PeUsEvp0dDqW63ij0HcCFwNrjDFdgIFAjhfeV0Rc4IP0fTywNI38I0e564KaYVqtgjRMyxfqc9niG8AYIMIYkwvMBIIArLXzgdnAQmNMKmCARGttvs8Si4hfyD9ylFnL01mRspdBXcN48ZZ44iI1TMuX6nOVy/V1LN8D/NRriUTEr1lrWfr9bh5+O4PSo9X84ScDuHtMX4Kaa5iWr+lOURHxmj2Hy5ixJJVVWQc4M6pmmFb/Lhqm1VhU6CLSYB6P5fWvdzLn3Uw8FmZePoSbR0drmFYjU6GLSIPkHDjCtKRUvt5ewLn9Inj86lh6dghxOlZAUqGLyGmpqvbw4ufbePrDzbRs0Yy518Rx7YhI3bbvIBW6iJyyjD1FTE3aSNruIi4d2oXZV8bQua2GaTlNhS4i9Xa0qpp5n2Tz/KdbaRcSxHM3DmdcTFcdlTcRKnQRqZcNOwpITEolO+8Ik4ZH8sBlg2mvYVpNigpdRE6q5GgVT76fxatrt9M9vDWvTh7FBQM6OR1LjkOFLiIntGbLAe5PTiX3UBm3jO7FfWMH0aalaqOp0ldGRH6ksLSSR9/J4K0NufTpFMpbd49mZHQHp2NJHVToIvI/Vqbt48FlaRSUVPDLMX35zcX9NUzLT6jQRQSAvOJyZi1P593UfQzp1pZXbh1JTI9wp2PJKVChiwQ4ay1J3+5m9ooMyiqrue/SgUw5v4+GafkhFbpIAMs9VMr0JWms3nyA+F7tmTMpjn6d2zgdS06TCl0kAHk8ln+u28ETKzcB8PAVQ7nprF400zAtv6ZCFwkwWw8cIXFxCut3HOL8AZ14bGIMke01TMsNVOgiAaKy2sOC1Tk8+/EWWgc158/XDmPS8B66bd9FVOgiASBtdyFTF6eQsbeI8bFdmXXFUDqHaZiW26jQRVysvLKaZz/ewoLVOXQIDWb+L4YzNqab07HER1ToIi71zfYCEhenkJNfwrUjInngsiGEhwQ5HUt8SIUu4jJHjlYxd+UmFq3dQWT71vzz9lGc11/DtAKBCl3ERT7bfIDpyansKSzj1rOjue/SgYRqmFbA0FdaxAUOl1bwyIoMkr/dTd9OoSy+ezQjemmYVqBRoYv4MWst76Xt46FlaRwureRXF/bjVxf10zCtAKVCF/FTeUXlPLgsjffT9xPToy2vTh7F0O4aphXIVOgifsZay1sbcnl0RQZHqzxMGzeIO87tTQsN0wp4KnQRP7KroJT7k1P5PDufUdEdmDMplj6dNExLaqjQRfxAtceyaO125q7MopmB2VfFcOOoKA3Tkv+hQhdp4rbsLyYxKYVvdx5mzMBO/GliLD3atXY6ljRBKnSRJqqy2sP8T7fyt0+yCW3ZnKevG8ZVZ2iYlpxYnYVujHkZmADkWWtjTrDOGOAZIAjIt9Ze4M2QIoEmNbeQ+xZvZNO+YibEdWPWFUOJaNPS6VjSxNXnCH0hMA9YdLyFxph2wHPAWGvtTmNMZ+/FEwks5ZXVPP3RZl5YnUNEm5YsuGkEPx3a1elY4ifqLHRr7WpjTPRJVrkBSLbW7qxdP8870UQCy1c5B5mWnMq2/BJ+PrIn948fTHhrDdOS+vPGOfQBQJAx5lMgDHjWWnuio/kpwBSAqKgoL2xaxP8Vl1fyxMpNvLZuJz07tOb1OxI4p1+E07HED3mj0FsAI4CLgdbAWmPMOmvt5h+uaK1dACwAiI+Pt17YtohfW7Upj+lLUtlXVM7t5/bmDz8dQEiwrlWQ0+ONfzm51PwgtAQoMcasBoYBPyp0EalRUFLBI2+ns/T7PfTv3Iake85meFR7p2OJn/NGoS8D5hljWgDBQALwtBfeV8R1rLWsSNnLrOXpFJZV8tuL+/PLC/vSsoWGaUnD1eeyxTeAMUCEMSYXmEnN5YlYa+dbazONMSuBFMADvGitTfNdZBH/tL+onBlL0vgocz9xkeG8fmcCg7q2dTqWuEh9rnK5vh7rPAk86ZVEIi5jreXf3+ziT+9mUlHlYcb4wdx2TrSGaYnX6acvIj6042AJ9yen8uXWgyT07sATk+KIjgh1Opa4lApdxAeqPZZXvtjGnz/IokWzZjw2MZafj+ypYVriUyp0ES/L2lfM1KQUNu46zMWDOvPoxBi6hWuYlvieCl3ESyqqPDz3aTZ/X5VNWKsgnv35GVwxrLuGaUmjUaGLeMHGXYeZujiFrP3FXHlGdx6aMISOGqYljUyFLtIAZRXV/OXDLF76fBudw1rx4s3xXDKki9OxJECp0EVO05db87k/OZUdB0u5ISGKaeMG0baVhmmJc1ToIqeoqLySx9/dxBtf76RXxxD+dWcCZ/fVMC1xngpd5BR8lLGfGUtTOVB8lCnn9+H3lwygdbBu25emQYUuUg8Hjxzl4bczWL5xD4O6hrHgpniG9WzndCyR/6FCFzkJay3LN+5h1vJ0jhyt4veXDOCeMX0JbqHb9qXpUaGLnMDewjIeWJLGx5vyOKNnO+ZeE8eALmFOxxI5IRW6yA94PJY3vtnJ4+9uosrj4YHLBnPbOb1prtv2pYlToYscY1t+CdOSUvhqWwFn9+3InKvjiOoY4nQskXpRoYsAVdUeXv5iG099sJngFs14YlIsP4vvqdv2xa+o0CXgZe4tIjEphZTcQn4ypAuPXhVDl7atnI4lcspU6BKwjlZV8/dVW3luVTbhrYOYd8OZXBbbTUfl4rdU6BKQvt15iMTFKWzJO8LEM3vw0IQhtA8NdjqWSIOo0CWglFZU8ef3N/PKl9vo2rYVr9w6kgsHdXY6lohXqNAlYHyRnc+05BR2FZRx01m9mDp2IGEapiUuokIX1yssq+SxdzL59/pd9I4I5d9TziKhT0enY4l4nQpdXO2D9H08sDSNgyUV3H1BX353SX9aBWmYlriTCl1c6UDxUWa9nc47KXsZ3K0tL90yktjIcKdjifiUCl1cxVrLku9288iKDEqPVvPHnw7grgv6EtRcw7TE/VTo4hq7D5cxY0kqn2YdYHhUzTCtfp01TEsChwpd/J7HY3n9qx3MeW8THgszLx/CzaOjNUxLAo4KXfxazoEjTEtK5evtBZzXP4LHJsbSs4OGaUlgUqGLX6qq9vDCmm08/dFmWrVoxpPXxHHNiEjdti8BTYUufid9TyGJSSmk7S7i0qFdmH1lDJ01TEtEhS7+o7yymr99soX5n+XQPiSY528czrjYbk7HEmkyVOjiFzbsKGDq4hS2Hihh0vBIHpwwmHYhGqYlcqw6L841xrxsjMkzxqTVsd5IY0y1MeYa78WTQFdytIpZy9O5Zv5ayis9vDp5FE/9bJjKXOQ46nOEvhCYByw60QrGmObAE8D73oklAqs3H+D+5FT2FJZx81m9uG/sINq01DeVIidS5/8Oa+1qY0x0Hav9GkgCRnohkwS4wtJKZr+TweINufTpFMp/7hrNyOgOTscSafIafLhjjOkBTAQuoo5CN8ZMAaYAREVFNXTT4kIr0/by4LJ0Ckoq+OWYvvzmYg3TEqkvb3z/+gyQaK2trusaYGvtAmABQHx8vPXCtsUl8orLmbksnffS9jGkW1teuXUkMT00TEvkVHij0OOBN2vLPAIYb4ypstYu9cJ7i8tZa1m8IZdH38mkrLKaqWMHcud5fTRMS+Q0NLjQrbW9///HxpiFwAqVudTHroJSpi9JZc2WfEZGt2fOpDj6dmrjdCwRv1VnoRtj3gDGABHGmFxgJhAEYK2d79N04koej2XR2u3MfT8LAzxy5VB+kdCLZhqmJdIg9bnK5fr6vpm19tYGpRHXy847wrSkFNbvOMT5Azrx2MQYIttrmJaIN+iiXmkUldUeFqzO4dmPttA6uDlPXTuMq4f30DAtES9SoYvPpe0uZOriFDL2FjE+tisPXxFDp7CWTscScR0VuvhMeWU1z368hQWrc+gQGsz8X4xgbExXp2OJuJYKXXzim+0FJC5OISe/hJ/FRzJj/BDCQ4KcjiXiaip08aojR6uYu3ITi9buILJ9a167PYFz+0c4HUskIKjQxWtWZeUxIzmVvUXl3HZONH/86UBCNUxLpNHof5s02KGSCmavyCD5u93069yGxXefzYhe7Z2OJRJwVOhy2qy1vJu6j5nL0zhcWsmvL+rHry7qR8sWGqYl4gQVupyWvKJyHliaxgcZ+4ntEc6iyQkM6d7W6VgiAU2FLqfEWstb63OZ/U4GFVUe7h83iNvP7U0LDdMScZwKXeptV0Ep9yen8nl2PqN6d2DO1bH00TAtkSZDhS51qvZYXv1yO0++n0XzZoZHr4rhhlFRGqYl0sSo0OWktuwvZmpSCt/tPMyYgZ14bGIs3du1djqWiByHCl2Oq6LKw/zPtjLvk2xCWzbnmevO4MozumuYlkgTpkKXH0nJPczUxSls2lfM5cO6M/PyIUS00TAtkaZOhS7/VV5ZzdMfbuaFNTl0CmvJCzfH85MhXZyOJSL1pEIXANblHGRaUgrbD5Zy/aieTBs3mPDWGqYl4k9U6AGuuLySOe9t4vWvdhLVIYR/3ZHA2f00TEvEH6nQA9gnm/YzY0ka+4vKuePc3vzfTwcQEqx/EiL+Sv97A1BBSQWPvJ3O0u/3MKBLG5678WzOjNIwLRF/p0IPINZa3k7Zy6zl6RSXV/Lbi/tz74X9CG6h2/ZF3ECFHiD2FdYM0/oocz/DIsN54poEBnXVMC0RN1Ghu5y1lje/2cVj72RS6fEwY/xgJp/bm+a6bV/EdVToLrbjYAnTklJZm3OQs/p0YM7VcURHhDodS0R8RIXuQtUeyytfbOPPH2QR1KwZj02M5ecje2qYlojLqdBdJmtfzTCtjbsOc/Ggzjw6MYZu4RqmJRIIVOguUVHl4blPs/n7qmzCWgXx1+vP5PK4bhqmJRJAVOgu8P2uwyQuTiFrfzFXntGdmZcPpUNosNOxRKSRqdD9WFlFNU99kMXLX2yjc1grXrolnosHa5iWSKBSofupL7fmMy0plZ0FpdyQEMW0cYNo20rDtEQCWZ2Fbox5GZgA5FlrY46z/EYgsfblEeAea+1Gr6aU/yoqr+TxdzN54+td9OoYwht3nsXovh2djiUiTUB9jtAXAvOARSdYvg24wFp7yBgzDlgAJHgnnhzro4z9zFiayoHio9x1fh9+d8kAWgc3dzqWiDQRdRa6tXa1MSb6JMu/POblOiCy4bHkWAePHGXW2xm8vXEPg7qG8cLN8cRFtnM6log0Md4+h3478N6JFhpjpgBTAKKiory8afex1rLs+z08/HY6R45W8X8/GcDdF/TVMC0ROS6vFbox5kJqCv3cE61jrV1AzSkZ4uPjrbe27UZ7DpfxwNI0PtmUxxk92zH3mjgGdAlzOpaINGFeKXRjTBzwIjDOWnvQG+8ZqDwey7++3smc9zZR7bE8OGEIt54drWFaIlKnBhe6MSYKSAZustZubnikwLUtv4RpSSl8ta2Ac/p15PGJcUR1DHE6loj4ifpctvgGMAaIMMbkAjOBIABr7XzgIaAj8FztbeZV1tp4XwV2o6pqDy99vo2/fLiZ4BbNmDspjmvjI3Xbvoickvpc5XJ9HcvvAO7wWqIAk7GniMSkFFJ3F/KTIV149KoYurRt5XQsEfFDulPUIUerqpn3STbPf7qVdiFB/P2G4YyP7aqjchE5bSp0B2zYcYjEpBSy845w9Zk9eHDCENprmJaINJAKvRGVVlTx5PtZLPxyO93atuKV20Zy4cDOTscSEZdQoTeSz7fkMy05hdxDZdx0Vi+mjh1ImIZpiYgXqdB9rLCskj+9k8F/1ufSOyKU/9w1mlG9OzgdS0RcSIXuQ++n7+PBpWkcLKngnjF9+e3F/WkVpGFaIuIbKnQfOFB8lFnL03kndS+Du7XlpVtGEhsZ7nQsEXE5FboXWWtJ/nY3j6zIoKyimvsuHciU8/sQ1FzDtETE91ToXrL7cBnTk1P5bPMBhkfVDNPq11nDtESk8ajQG8jjsbz21Q6eeG8TFph1+RBuGq1hWiLS+FToDbD1wBGmJaXwzfZDnNc/gscmxtKzg4ZpiYgzVOinobLawwtrcnjmoy20atGMJ6+J45oRGqYlIs5SoZ+itN2FJCalkL6niLFDu/LIVUPpHKZhWiLiPBV6PZVXVvO3T7Yw/7Mc2ocE8/yNwxkX283pWCIi/6VCr4f12wuYmpRCzoESJg2P5MEJg2kXomFaItK0qNBPouRozTCtV9dup3t4a16dPIoLBnRyOpaIyHGp0E/gs80HmJ6cyp7CMm4ZHc19lw4ktKX+ukSk6VJD/cDh0gpmr8gk6dtc+nQK5a27RhMfrWFaItL0qdCP8V7qXh5cls6h0gruvbAvv75Iw7RExH+o0IG8onIeWpbOyvR9DO3ellcnj2Rodw3TEhH/EtCFbq1l8YZcZq/IoLzKQ+LYQdxxXm8N0xIRvxSwhb6roJTpS1JZsyWfkdHtmTMpjr6d2jgdS0TktAVcoVd7LP9cu52572dhgNlXDuXGhF400zAtEfFzAVXo2XnFJCalsmHHIS4Y0Ik/TYwhsr2GaYmIOwREoVdWe/jHZ1v568fZhLRszl9+NoyJZ/bQMC0RcRXXF3ra7kLuW5xC5t4iLovtxqwrhtIprKXTsUREvM61hV5eWc0zH23hhTU5dAgNZv4vRjA2pqvTsUREfMaVhf71tgKmJaWQk1/CdfE9mT5+MOEhQU7HEhHxKVcVenF5JXNXZvHPdTuIbN+a125P4Nz+EU7HEhFpFK4p9FVZecxITmVvUTmTz+nNHy8dQEiwa3ZPRKROft94h0oqmL0ig+TvdtOvcxsW3302I3q1dzqWiEijq7PQjTEvAxOAPGttzHGWG+BZYDxQCtxqrf3W20F/yFrLO6l7mbksncKySn5zUT/uvagfLVtomJaIBKb6HKEvBOYBi06wfBzQv/ZXAvB87e8+s7+onAeXpvFBxn5ie4Tz2h0JDO7W1pebFBFp8uosdGvtamNM9ElWuRJYZK21wDpjTDtjTDdr7V4vZfwfqzbl8Zs3v6OiysP94wZx+7m9aaFhWiIiXjmH3gPYdczr3NrP/ajQjTFTgCkAUVFRp7Wx3hGhDI9qz6wrhtI7IvS03kNExI28cWh7vPvn7fFWtNYusNbGW2vjO3U6vWdzRkeE8urkUSpzEZEf8Eah5wI9j3kdCezxwvuKiMgp8EahLwduNjXOAgp9df5cREROrD6XLb4BjAEijDG5wEwgCMBaOx94l5pLFrOpuWzxNl+FFRGRE6vPVS7X17HcAvd6LZGIiJwWXe8nIuISKnQREZdQoYuIuIQKXUTEJUzNzzQd2LAxB4Adp/nHI4B8L8bxB9rnwKB9DgwN2ede1trj3pnpWKE3hDFmvbU23ukcjUn7HBi0z4HBV/usUy4iIi6hQhcRcQl/LfQFTgdwgPY5MGifA4NP9tkvz6GLiMiP+esRuoiI/IAKXUTEJZp0oRtjXjbG5Blj0k6w3Bhj/mqMyTbGpBhjhjd2Rm+qx/7eWLufKcaYL40xwxo7o7fVtc/HrDfSGFNtjLmmsbL5Sn322RgzxhjzvTEm3RjzWWPm84V6/NsON8a8bYzZWLvPfj+11RjT0xizyhiTWbtPvz3OOl7tsCZd6NQ8oHrsSZYf+4DqKdQ8oNqfLeTk+7sNuMBaGwfMxh0/TFrIyfcZY0xz4Ang/cYI1AgWcpJ9Nsa0A54DrrDWDgWubaRcvrSQk3+d7wUyrLXDqBnX/ZQxJrgRcvlSFfAHa+1g4CzgXmPMkB+s49UOa9KFbq1dDRScZJX/PqDaWrsOaGeM6dY46byvrv211n5prT1U+3IdNU+H8mv1+BoD/BpIAvJ8n8j36rHPNwDJ1tqdtev7/X7XY58tEGaMMUCb2nWrGiObr1hr91prv639uBjIpOZ5y8fyaoc16UKvhxM9oDoQ3A6853QIXzPG9AAmAvOdztKIBgDtjTGfGmM2GGNudjpQI5gHDKbm8ZWpwG+ttR5nI3mPMSYaOBP46geLvNphdT7goomr9wOq3cQYcyE1hX6u01kawTNAorW2uubgLSC0AEYAFwOtgbXGmHXW2s3OxvKpS4HvgYuAvsCHxpg11toiZ2M1nDGmDTXfYf7uOPvj1Q7z90IPuAdUG2PigBeBcdbag07naQTxwJu1ZR4BjDfGVFlrlzoby6dygXxrbQlQYoxZDQwD3FzotwFzap+Alm2M2QYMAr52NlbDGGOCqCnz1621ycdZxasd5u+nXALqAdXGmCggGbjJ5Udr/2Wt7W2tjbbWRgOLgV+6vMwBlgHnGWNaGGNCgARqzr+62U5qviPBGNMFGAjkOJqogWp/HvASkGmt/csJVvPOKC8AAACLSURBVPNqhzXpI/RAe0B1Pfb3IaAj8FztEWuVv0+pq8c+u05d+2ytzTTGrARSAA/worX2pJd1NnX1+DrPBhYaY1KpOQ2RaK3195G65wA3AanGmO9rPzcdiALfdJhu/RcRcQl/P+UiIiK1VOgiIi6hQhcRcQkVuoiIS6jQRURcQoUuIuISKnQREZf4f3qgoL7LBuDFAAAAAElFTkSuQmCC\n" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "plt.plot([1,2], [1,2])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "orig_nbformat": 2, + "kernelspec": { + "name": "python_defaultSpec_1594150811981", + "display_name": "Python 3.8.2 64-bit" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.eslintrc.js b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.eslintrc.js new file mode 100644 index 000000000000..f660e395fe25 --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.eslintrc.js @@ -0,0 +1,20 @@ +/**@type {import('eslint').Linter.Config} */ +// eslint-disable-next-line no-undef +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: [ + '@typescript-eslint', + ], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + ], + rules: { + 'semi': [2, "always"], + '@typescript-eslint/no-unused-vars': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/explicit-module-boundary-types': 0, + '@typescript-eslint/no-non-null-assertion': 0, + } +}; \ No newline at end of file diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.gitignore b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.gitignore new file mode 100644 index 000000000000..eaf5a2953c34 --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.gitignore @@ -0,0 +1,3 @@ +dist +node_modules +*.vsix diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscode/extensions.json b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscode/extensions.json new file mode 100644 index 000000000000..af515502dfd1 --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + "dbaeumer.vscode-eslint" + ] +} \ No newline at end of file diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscode/launch.json b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscode/launch.json new file mode 100644 index 000000000000..e0a96d6afd7e --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscode/launch.json @@ -0,0 +1,33 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ], + "preLaunchTask": "npm: webpack" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "outFiles": [ + "${workspaceFolder}/out/test/**/*.js" + ], + "preLaunchTask": "npm: test-compile" + } + ] +} diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscodeignore b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscodeignore new file mode 100644 index 000000000000..c8b0c2086cfe --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/.vscodeignore @@ -0,0 +1,6 @@ +.vscode +node_modules +src/** +package-json.lock +tsconfig.json +webpack.config.js diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/README.md b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/README.md new file mode 100644 index 000000000000..a822212b683e --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/README.md @@ -0,0 +1,18 @@ +# AI Tools Extension + +This extension is used for testing the extensibility of the ms-python.python extension + +# Testing with this extension + +You can use this extension to test the python extension's API. To do so, follow these steps: + +1. Create an azure compute node +1. Open .\src\serverPicker.ts +1. Change the code in serverPicker.ts to match your compute node +1. Switch to the directory that the README.md is in +1. Run npm install +1. Run npm run package +1. Install the VSIX created +1. Debug or run the ms-python.python package +1. Pick the 'Specify local or remote Jupyter server for connections' +1. This extension should then load and show the 'Azure Compute' entry in the picker that opens. diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package-lock.json b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package-lock.json new file mode 100644 index 000000000000..df1dbfc9841c --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package-lock.json @@ -0,0 +1,4657 @@ +{ + "name": "ms-ai-tools-test", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", + "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.3" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", + "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", + "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.3", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/jquery": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.0.tgz", + "integrity": "sha512-C7qQUjpMWDUNYQRTXsP5nbYYwCwwgy84yPgoTT7fPN69NH92wLeCtFaMsWeolJD1AF/6uQw3pYt62rzv83sMmw==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, + "@types/json-schema": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", + "dev": true + }, + "@types/node": { + "version": "12.12.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.47.tgz", + "integrity": "sha512-yzBInQFhdY8kaZmqoL2+3U5dSTMrKaYcb561VU+lDzAYvqt+2lojvBEy+hmpSNuXnPTx7m9+04CzWYOUqWME2A==", + "dev": true + }, + "@types/sizzle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", + "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", + "dev": true + }, + "@types/vscode": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.46.0.tgz", + "integrity": "sha512-8m9wPEB2mcRqTWNKs9A9Eqs8DrQZt0qNFO8GkxBOnyW6xR//3s77SoMgb/nY1ctzACsZXwZj3YRTDsn4bAoaUw==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.4.0.tgz", + "integrity": "sha512-wfkpiqaEVhZIuQRmudDszc01jC/YR7gMSxa6ulhggAe/Hs0KVIuo9wzvFiDbG3JD5pRFQoqnf4m7REDsUvBnMQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "3.4.0", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.4.0.tgz", + "integrity": "sha512-rHPOjL43lOH1Opte4+dhC0a/+ks+8gOBwxXnyrZ/K4OTAChpSjP76fbI8Cglj7V5GouwVAGaK+xVwzqTyE/TPw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "3.4.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.4.0.tgz", + "integrity": "sha512-ZUGI/de44L5x87uX5zM14UYcbn79HSXUR+kzcqU42gH0AgpdB/TjuJy3m4ezI7Q/jk3wTQd755mxSDLhQP79KA==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.4.0", + "@typescript-eslint/typescript-estree": "3.4.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.4.0.tgz", + "integrity": "sha512-zKwLiybtt4uJb4mkG5q2t6+W7BuYx2IISiDNV+IY68VfoGwErDx/RfVI7SWL4gnZ2t1A1ytQQwZ+YOJbHHJ2rw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "@webassemblyjs/ast": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "dev": true + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "acorn": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", + "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-keywords": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.0.tgz", + "integrity": "sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw==", + "dev": true + }, + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "optional": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true, + "optional": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true, + "optional": true + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "bn.js": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.2.tgz", + "integrity": "sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "browserify-sign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.0.tgz", + "integrity": "sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA==", + "dev": true, + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.2", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", + "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz", + "integrity": "sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + } + }, + "enquirer": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.5.tgz", + "integrity": "sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA==", + "dev": true, + "requires": { + "ansi-colors": "^3.2.1" + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "requires": { + "prr": "~1.0.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.3.1.tgz", + "integrity": "sha512-cQC/xj9bhWUcyi/RuMbRtC3I0eW8MH0jhRELSvpKYkWep3C6YZ2OkvcvJVUeO6gcunABmzptbXBuDoXsjHmfTA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.0", + "eslint-utils": "^2.0.0", + "eslint-visitor-keys": "^1.2.0", + "espree": "^7.1.0", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.1.0.tgz", + "integrity": "sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw==", + "dev": true, + "requires": { + "acorn": "^7.2.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.2.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "events": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", + "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "dev": true + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "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-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "requires": { + "global-prefix": "^3.0.0" + }, + "dependencies": { + "global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "requires": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "requires": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "requires": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-asn1": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", + "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true, + "optional": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "pbkdf2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "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" + } + }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "dev": true, + "optional": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true, + "optional": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "dependencies": { + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "^1.1.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "dev": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", + "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + } + }, + "tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true + }, + "terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz", + "integrity": "sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA==", + "dev": true, + "requires": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^3.1.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "timers-browserify": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", + "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-loader": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.5.tgz", + "integrity": "sha512-zXypEIT6k3oTc+OZNx/cqElrsbBtYqDknf48OZos0NQ3RTt045fBIU8RRSu+suObBzYB355aIPGOe/3kj9h7Ig==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typescript": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz", + "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "optional": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz", + "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==" + }, + "v8-compile-cache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", + "dev": true + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "watchpack": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz", + "integrity": "sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g==", + "dev": true, + "requires": { + "chokidar": "^3.4.0", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.0" + } + }, + "watchpack-chokidar2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz", + "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==", + "dev": true, + "optional": true, + "requires": { + "chokidar": "^2.1.8" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "optional": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "optional": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "webpack": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.43.0.tgz", + "integrity": "sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.6.1", + "webpack-sources": "^1.4.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "webpack-cli": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz", + "integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "cross-spawn": "^6.0.5", + "enhanced-resolve": "^4.1.1", + "findup-sync": "^3.0.0", + "global-modules": "^2.0.0", + "import-local": "^2.0.0", + "interpret": "^1.4.0", + "loader-utils": "^1.4.0", + "supports-color": "^6.1.0", + "v8-compile-cache": "^2.1.1", + "yargs": "^13.3.2" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "requires": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } +} diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package.json b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package.json new file mode 100644 index 000000000000..da0796dd4018 --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/package.json @@ -0,0 +1,51 @@ +{ + "name": "ms-ai-tools-test", + "displayName": "AI Tools Test Extension", + "description": "Extension for testing the API for talking to the ms-python.python extension", + "version": "0.0.1", + "publisher": "ms-python", + "engines": { + "vscode": "^1.32.0" + }, + "license": "MIT", + "homepage": "https://github.com/Microsoft/vscode-python", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-python" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-python/issues" + }, + "qna": "https://stackoverflow.com/questions/tagged/visual-studio-code+python", + "categories": [ + "Other" + ], + "activationEvents": [], + "main": "./dist/extension", + "contributes": { + "pythonRemoteServerProvider": [ {"id": "RemoteServerPickerExample"} ] + }, + "scripts": { + "vscode:prepublish": "webpack --mode production", + "webpack": "webpack --mode development", + "webpack-dev": "webpack --mode development --watch", + "test-compile": "tsc -p ./", + "lint": "eslint . --ext .ts,.tsx", + "package": "npm run vscode:prepublish && vsce package -o ms-ai-tools-test.vsix" + }, + "devDependencies": { + "@types/jquery": "^3.5.0", + "@types/node": "^12.12.0", + "@types/vscode": "^1.32.0", + "@typescript-eslint/eslint-plugin": "^3.0.2", + "@typescript-eslint/parser": "^3.0.2", + "eslint": "^7.1.0", + "ts-loader": "^7.0.5", + "typescript": "^3.9.4", + "webpack": "^4.43.0", + "webpack-cli": "^3.3.11" + }, + "dependencies": { + "uuid": "^8.2.0" + } +} diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/extension.ts b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/extension.ts new file mode 100644 index 000000000000..645c62b26ea5 --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/extension.ts @@ -0,0 +1,22 @@ +// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below +import * as vscode from 'vscode'; +import { RemoteServerPickerExample } from './serverPicker'; +import { IPythonExtensionApi } from './typings/python'; + +// Register our URI picker +export async function activate(_context: vscode.ExtensionContext) { + const python = vscode.extensions.getExtension<IPythonExtensionApi>('ms-python.python'); + if (python) { + if (!python.isActive) { + await python.activate(); + await python.exports.ready; + } + python.exports.datascience.registerRemoteServerProvider(new RemoteServerPickerExample()); + } +} + +// this method is called when your extension is deactivated +export function deactivate() { + // Don't do anything at the moment here. +} diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/serverPicker.ts b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/serverPicker.ts new file mode 100644 index 000000000000..ddfc530dd761 --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/serverPicker.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { exec } from 'child_process'; +import * as vscode from 'vscode'; +import { IJupyterServerUri, IJupyterUriProvider, JupyterServerUriHandle } from './typings/python'; + +// This is an example of how to implement the IJupyterUriQuickPicker. Replace +// the machine name and server URI below with your own version +const Compute_Name = 'rchiodocom'; +const Compute_Name_NotWorking = 'rchiodonw'; +const Compute_ServerUri = 'https://rchiodocom2.westus.instances.azureml.net'; + +export class RemoteServerPickerExample implements IJupyterUriProvider { + public get id() { + return 'RemoteServerPickerExample'; // This should be a unique constant + } + public getQuickPickEntryItems(): vscode.QuickPickItem[] { + return [ + { + label: '$(clone) Azure COMPUTE', + detail: 'Use Azure COMPUTE to run your notebooks' + } + ]; + } + public handleQuickPick( + _item: vscode.QuickPickItem, + back: boolean + ): Promise<JupyterServerUriHandle | 'back' | undefined> { + // Show a quick pick list to start off. + const quickPick = vscode.window.createQuickPick(); + quickPick.title = 'Pick a compute instance'; + quickPick.placeholder = 'Choose instance'; + quickPick.buttons = back ? [vscode.QuickInputButtons.Back] : []; + quickPick.items = [{ label: Compute_Name }, { label: Compute_Name_NotWorking }]; + let resolved = false; + const result = new Promise<JupyterServerUriHandle | 'back' | undefined>((resolve, _reject) => { + quickPick.onDidTriggerButton((b) => { + if (b === vscode.QuickInputButtons.Back) { + resolved = true; + resolve('back'); + quickPick.hide(); + } + }); + quickPick.onDidChangeSelection((s) => { + resolved = true; + if (s && s[0].label === Compute_Name) { + resolve(Compute_Name); + } else { + resolve(undefined); + } + quickPick.hide(); + }); + quickPick.onDidHide(() => { + if (!resolved) { + resolve(undefined); + } + }); + }); + quickPick.show(); + return result; + } + + public getServerUri(_handle: JupyterServerUriHandle): Promise<IJupyterServerUri> { + return new Promise((resolve, reject) => { + exec( + 'az account get-access-token', + { + windowsHide: true, + encoding: 'utf-8' + }, + (_e, stdout, _stderr) => { + // Stdout (if it worked) should have something like so: + // accessToken: bearerToken value + // tokenType: Bearer + // some other stuff + if (stdout) { + const output = JSON.parse(stdout.toString()); + const currentDate = new Date(); + resolve({ + baseUrl: Compute_ServerUri, + token: '', //output.accessToken, + authorizationHeader: { Authorization: `Bearer ${output.accessToken}` }, + expiration: new Date( + currentDate.getFullYear(), + currentDate.getMonth(), + undefined, + currentDate.getHours(), + currentDate.getMinutes() + 1 // Expire after one minute + ) + }); + } else { + reject('Unable to get az token'); + } + } + ); + }); + } +} diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/typings/python.d.ts b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/typings/python.d.ts new file mode 100644 index 000000000000..f98ce2d07166 --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/src/typings/python.d.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { QuickPickItem, QuickInputButton } from 'vscode'; + +// Typings for the code in the python extension +export interface IPythonExtensionApi { + /** + * Promise indicating whether all parts of the extension have completed loading or not. + * @type {Promise<void>} + * @memberof IExtensionApi + */ + ready: Promise<void>; + datascience: { + /** + * Launches Data Viewer component. + * @param {IDataViewerDataProvider} dataProvider Instance that will be used by the Data Viewer component to fetch data. + * @param {string} title Data Viewer title + */ + showDataViewer(dataProvider: IDataViewerDataProvider, title: string): Promise<void>; + /** + * Registers a remote server provider component that's used to pick remote jupyter server URIs + * @param serverProvider object called back when picking jupyter server URI + */ + registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void; + }; +} + +export interface IDataFrameInfo { + columns?: { key: string; type: ColumnType }[]; + indexColumn?: string; + rowCount?: number; +} + +export interface IDataViewerDataProvider { + dispose(): void; + getDataFrameInfo(): Promise<IDataFrameInfo>; + getAllRows(): Promise<IRowsResponse>; + getRows(start: number, end: number): Promise<IRowsResponse>; +} + +export enum ColumnType { + String = 'string', + Number = 'number', + Bool = 'bool' +} + +// tslint:disable-next-line: no-any +export type IRowsResponse = any[]; + +export interface IJupyterServerUri { + baseUrl: string; + token: string; + // tslint:disable-next-line: no-any + authorizationHeader: any; // JSON object for authorization header. + expiration?: Date; // Date/time when header expires and should be refreshed. +} + +export type JupyterServerUriHandle = string; + +export interface IJupyterUriProvider { + readonly id: string; // Should be a unique string (like a guid) + getQuickPickEntryItems(): QuickPickItem[]; + handleQuickPick(item: QuickPickItem, backEnabled: boolean): Promise<JupyterServerUriHandle | 'back' | undefined>; + getServerUri(handle: JupyterServerUriHandle): Promise<IJupyterServerUri>; +} diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/tsconfig.json b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/tsconfig.json new file mode 100644 index 000000000000..287f63f07dfb --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": ["types/*"] + }, + "module": "commonjs", + "target": "es2018", + "outDir": "dist", + "lib": ["es6", "es2018", "dom", "ES2019"], + "jsx": "react", + "sourceMap": true, + "rootDir": "src", + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "exclude": [ + "node_modules", + ".vscode-test", + ".vscode test", + "build", + "out", + ] +} diff --git a/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/webpack.config.js b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/webpack.config.js new file mode 100644 index 000000000000..423b78185bbb --- /dev/null +++ b/src/test/datascience/extensionapi/exampleextension/ms-ai-tools-test/webpack.config.js @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const path = require('path'); + +/**@type {import('webpack').Configuration}*/ +const config = { + target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ + + entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ + output: { // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ + path: path.resolve(__dirname, 'dist'), + filename: 'extension.js', + libraryTarget: "commonjs2", + devtoolModuleFilenameTemplate: "../[resource-path]", + }, + devtool: 'source-map', + externals: { + vscode: "commonjs vscode" // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ + }, + resolve: { // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader + extensions: ['.ts', '.js'] + }, + module: { + rules: [{ + test: /\.ts$/, + exclude: /node_modules/, + use: [{ + loader: 'ts-loader', + options: { + compilerOptions: { + "module": "es6" // override `tsconfig.json` so that TypeScript emits native JavaScript modules. + } + } + }] + }] + }, +} + +module.exports = config; diff --git a/src/test/datascience/foo.py b/src/test/datascience/foo.py new file mode 100644 index 000000000000..17da214da465 --- /dev/null +++ b/src/test/datascience/foo.py @@ -0,0 +1 @@ +# Dummy file just to find a file for use in jupyter execution diff --git a/src/test/datascience/helpers.ts b/src/test/datascience/helpers.ts new file mode 100644 index 000000000000..e4a9791f3cc3 --- /dev/null +++ b/src/test/datascience/helpers.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { noop } from 'lodash'; +import { IDataScienceSettings } from '../../client/common/types'; + +// The default base set of data science settings to use +export function defaultDataScienceSettings(): IDataScienceSettings { + return { + allowImportFromNotebook: true, + alwaysTrustNotebooks: true, + jupyterLaunchTimeout: 10, + jupyterLaunchRetries: 3, + enabled: true, + jupyterServerURI: 'local', + // tslint:disable-next-line: no-invalid-template-strings + notebookFileRoot: '${fileDirname}', + changeDirOnImportExport: false, + useDefaultConfigForJupyter: true, + jupyterInterruptTimeout: 10000, + searchForJupyter: true, + showCellInputCode: true, + collapseCellInputCodeByDefault: true, + allowInput: true, + maxOutputSize: 400, + enableScrollingForCellOutputs: true, + errorBackgroundColor: '#FFFFFF', + sendSelectionToInteractiveWindow: false, + variableExplorerExclude: 'module;function;builtin_function_or_method', + codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', + markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', + enablePlotViewer: true, + runStartupCommands: '', + debugJustMyCode: true, + variableQueries: [], + jupyterCommandLineArguments: [], + widgetScriptSources: [], + interactiveWindowMode: 'single' + }; +} + +export function takeSnapshot() { + // If you're investigating memory leaks in the tests, using the node-memwatch + // code below can be helpful. It will at least write out what objects are taking up the most + // memory. + // Alternatively, using the test:functional:memleak task and sticking breakpoints here and in + // writeDiffSnapshot can be used as convenient locations to create heap snapshots and diff them. + // tslint:disable-next-line: no-require-imports + //const memwatch = require('@raghb1/node-memwatch'); + return {}; //new memwatch.HeapDiff(); +} + +//let snapshotCounter = 1; +// tslint:disable-next-line: no-any +export function writeDiffSnapshot(_snapshot: any, _prefix: string) { + noop(); // Stick breakpoint here when generating heap snapshots + // const diff = snapshot.end(); + // const file = path.join(EXTENSION_ROOT_DIR, 'tmp', `SD-${snapshotCounter}-${prefix}.json`); + // snapshotCounter += 1; + // fs.writeFile(file, JSON.stringify(diff), { encoding: 'utf-8' }).ignoreErrors(); +} diff --git a/src/test/datascience/inputHistory.unit.test.ts b/src/test/datascience/inputHistory.unit.test.ts new file mode 100644 index 000000000000..a7919dca5d69 --- /dev/null +++ b/src/test/datascience/inputHistory.unit.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert } from 'chai'; +import { InputHistory } from '../../datascience-ui/interactive-common/inputHistory'; + +suite('DataScience InputHistory', () => { + test('input history', async () => { + let history = new InputHistory(); + history.add('1', true); + history.add('2', true); + history.add('3', true); + history.add('4', true); + assert.equal(history.completeDown('5'), '5'); + history.add('5', true); + assert.equal(history.completeUp(''), '5'); + history.add('5', false); + assert.equal(history.completeUp('5'), '5'); + assert.equal(history.completeUp('4'), '4'); + assert.equal(history.completeUp('2'), '3'); + assert.equal(history.completeUp('1'), '2'); + assert.equal(history.completeUp(''), '1'); + + // Add should reset position. + history.add('6', true); + assert.equal(history.completeUp(''), '6'); + assert.equal(history.completeUp(''), '5'); + assert.equal(history.completeUp(''), '4'); + assert.equal(history.completeUp(''), '3'); + assert.equal(history.completeUp(''), '2'); + assert.equal(history.completeUp(''), '1'); + history = new InputHistory(); + history.add('1', true); + history.add('2', true); + history.add('3', true); + history.add('4', true); + assert.equal(history.completeDown('5'), '5'); + assert.equal(history.completeDown(''), ''); + assert.equal(history.completeUp('1'), '4'); + assert.equal(history.completeDown('4'), '4'); + assert.equal(history.completeDown('4'), '4'); + assert.equal(history.completeUp('1'), '3'); + assert.equal(history.completeUp('4'), '2'); + assert.equal(history.completeDown('3'), '3'); + assert.equal(history.completeDown(''), '4'); + assert.equal(history.completeUp(''), '3'); + assert.equal(history.completeUp(''), '2'); + assert.equal(history.completeUp(''), '1'); + assert.equal(history.completeUp(''), ''); + assert.equal(history.completeUp('1'), '1'); + assert.equal(history.completeDown('1'), '2'); + assert.equal(history.completeDown('2'), '3'); + assert.equal(history.completeDown('3'), '4'); + assert.equal(history.completeDown(''), ''); + history.add('5', true); + assert.equal(history.completeUp('1'), '5'); + assert.equal(history.completeUp('1'), '4'); + assert.equal(history.completeUp('1'), '3'); + history.add('3', false); + assert.equal(history.completeUp('1'), '3'); + assert.equal(history.completeUp('1'), '2'); + assert.equal(history.completeUp('1'), '1'); + assert.equal(history.completeDown('1'), '2'); + assert.equal(history.completeUp('1'), '1'); + assert.equal(history.completeDown('1'), '2'); + assert.equal(history.completeDown('1'), '3'); + assert.equal(history.completeDown('1'), '4'); + assert.equal(history.completeDown('1'), '5'); + assert.equal(history.completeDown('1'), '3'); + }); +}); diff --git a/src/test/datascience/intellisense.functional.test.tsx b/src/test/datascience/intellisense.functional.test.tsx new file mode 100644 index 000000000000..daa822cb5adf --- /dev/null +++ b/src/test/datascience/intellisense.functional.test.tsx @@ -0,0 +1,485 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; +import { ReactWrapper } from 'enzyme'; +import { IDisposable } from 'monaco-editor'; +import { Disposable } from 'vscode'; + +import { nbformat } from '@jupyterlab/coreutils'; +import { LanguageServerType } from '../../client/activation/types'; +import { createDeferred } from '../../client/common/utils/async'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { INotebookEditorProvider } from '../../client/datascience/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { MonacoEditor } from '../../datascience-ui/react-common/monacoEditor'; +import { noop } from '../core'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { takeSnapshot, writeDiffSnapshot } from './helpers'; +import * as InteractiveHelpers from './interactiveWindowTestHelpers'; +import * as NativeHelpers from './nativeEditorTestHelpers'; +import { addMockData, enterEditorKey, getInteractiveEditor, getNativeEditor, typeCode } from './testHelpers'; +import { ITestNativeEditorProvider } from './testNativeEditorProvider'; + +// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +[LanguageServerType.Microsoft, LanguageServerType.Node].forEach((languageServerType) => { + suite(`DataScience Intellisense tests with ${languageServerType} LanguageServer mocked`, () => { + const disposables: Disposable[] = []; + let ioc: DataScienceIocContainer; + let snapshot: any; + + suiteSetup(() => { + snapshot = takeSnapshot(); + }); + + setup(async () => { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(false, languageServerType); + return ioc.activate(); + }); + + suiteTeardown(() => { + writeDiffSnapshot(snapshot, 'Intellisense'); + }); + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + await ioc.dispose(); + }); + + // suiteTeardown(() => { + // asyncDump(); + // }); + + function getIntellisenseTextLines(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>): string[] { + assert.ok(wrapper); + const editor = getInteractiveEditor(wrapper); + assert.ok(editor); + const domNode = editor.getDOMNode(); + assert.ok(domNode); + const nodes = domNode!.getElementsByClassName('monaco-list-row'); + assert.ok(nodes && nodes.length); + const innerTexts: string[] = []; + for (let i = 0; i < nodes.length; i += 1) { + const node = nodes.item(i) as HTMLElement; + const content = node.textContent; + if (content) { + innerTexts.push(content); + } + } + return innerTexts; + } + + function getHoverText( + type: 'Interactive' | 'Native', + wrapper: ReactWrapper<any, Readonly<{}>, React.Component> + ): string { + assert.ok(wrapper); + const editor = type === 'Interactive' ? getInteractiveEditor(wrapper) : getNativeEditor(wrapper, 0); + assert.ok(editor); + const domNode = editor?.getDOMNode(); + assert.ok(domNode); + const nodes = domNode!.getElementsByClassName('hover-contents'); + assert.ok(nodes && nodes.length); + const innerTexts: string[] = []; + for (let i = 0; i < nodes.length; i += 1) { + const node = nodes.item(i) as HTMLElement; + const content = node.textContent; + if (content) { + innerTexts.push(content); + } + } + return innerTexts.join(''); + } + + function verifyIntellisenseVisible( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + expectedSpan: string + ) { + wrapper.update(); + const innerTexts = getIntellisenseTextLines(wrapper); + assert.ok(innerTexts.includes(expectedSpan), 'Intellisense row not matching'); + } + + function verifyIntellisenseNotVisible( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + expectedSpan: string + ) { + const innerTexts = getIntellisenseTextLines(wrapper); + assert.ok(!innerTexts.includes(expectedSpan), 'Intellisense row is showing'); + } + + function verifyHoverVisible( + type: 'Interactive' | 'Native', + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + expectedSpan: string + ) { + const innerText = getHoverText(type, wrapper); + assert.ok(innerText.includes(expectedSpan), `${innerText} not matching ${expectedSpan}`); + } + + // Note: If suggestions are hanging, verify suggestion results are returning by + // sticking a breakpoint here: node_modules\monaco-editor\esm\vs\editor\contrib\suggest\suggestModel.js#337 or so + function waitForSuggestion( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component> + ): { disposable: IDisposable; promise: Promise<void> } { + const editorEnzyme = getInteractiveEditor(wrapper); + const reactEditor = editorEnzyme.instance() as MonacoEditor; + const editor = reactEditor.state.editor; + if (editor) { + // The suggest controller has a suggest model on it. It has an event + // that fires when the suggest controller is opened. + const suggest = editor.getContribution('editor.contrib.suggestController') as any; + if (suggest && suggest._model) { + const promise = createDeferred<void>(); + const disposable = suggest._model.onDidSuggest(() => { + promise.resolve(); + }); + return { + disposable, + promise: promise.promise + }; + } + } + + return { + disposable: { + dispose: noop + }, + promise: Promise.resolve() + }; + } + + function waitForHover( + type: 'Interactive' | 'Native', + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + line: number, + column: number + ): Promise<void> { + wrapper.update(); + const editorEnzyme = type === 'Interactive' ? getInteractiveEditor(wrapper) : getNativeEditor(wrapper, 0); + const reactEditor = editorEnzyme?.instance() as MonacoEditor; + const editor = reactEditor.state.editor; + if (editor) { + // The hover controller has a hover model on it. It has an event + // that fires when the hover controller is opened. + const hover = editor.getContribution('editor.contrib.hover') as any; + if (hover && hover.contentWidget) { + const promise = createDeferred<void>(); + const timer = setTimeout(() => { + promise.reject(new Error('Timed out waiting for hover')); + }, 10000); + const originalShowAt = hover.contentWidget.showAt.bind(hover.contentWidget); + hover.contentWidget.showAt = (p: any, r: any, f: any) => { + clearTimeout(timer); + promise.resolve(); + hover.contentWidget.showAt = originalShowAt; + originalShowAt(p, r, f); + }; + hover.contentWidget.startShowingAt( + { startLineNumber: line, endLineNumber: line, startColumn: column, endColumn: column }, + 0, + false + ); + return promise.promise; + } + } + + return Promise.reject(new Error('Hover not found')); + } + + function clearEditor(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) { + const editor = getInteractiveEditor(wrapper); + const inst = editor.instance() as MonacoEditor; + inst.state.model!.setValue(''); + } + + InteractiveHelpers.runTest( + 'Simple autocomplete', + async () => { + // Create an interactive window so that it listens to the results. + const { mount } = await InteractiveHelpers.getOrCreateInteractiveWindow(ioc); + + // Then enter some code. Don't submit, we're just testing that autocomplete appears + const suggestion = waitForSuggestion(mount.wrapper); + typeCode(getInteractiveEditor(mount.wrapper), 'print'); + await suggestion.promise; + suggestion.disposable.dispose(); + verifyIntellisenseVisible(mount.wrapper, 'print'); + + // Force suggestion box to disappear so that shutdown doesn't try to generate suggestions + // while we're destroying the editor. + clearEditor(mount.wrapper); + }, + () => { + return ioc; + } + ); + + InteractiveHelpers.runTest( + 'Multiple interpreters', + async () => { + // Create an interactive window so that it listens to the results. + const { mount } = await InteractiveHelpers.getOrCreateInteractiveWindow(ioc); + + // Then enter some code. Don't submit, we're just testing that autocomplete appears + let suggestion = waitForSuggestion(mount.wrapper); + typeCode(getInteractiveEditor(mount.wrapper), 'print'); + await suggestion.promise; + suggestion.disposable.dispose(); + verifyIntellisenseVisible(mount.wrapper, 'print'); + + // Clear the code + const editor = getInteractiveEditor(mount.wrapper); + const inst = editor.instance() as MonacoEditor; + inst.state.model!.setValue(''); + + // Then change our current interpreter + const interpreterService = ioc.get<IInterpreterService>(IInterpreterService); + const oldActive = await interpreterService.getActiveInterpreter(undefined); + const interpreters = await interpreterService.getInterpreters(undefined); + if (interpreters.length > 1 && oldActive) { + const firstOther = interpreters.filter((i) => i.path !== oldActive.path); + ioc.forceSettingsChanged(undefined, firstOther[0].path); + const active = await interpreterService.getActiveInterpreter(undefined); + assert.notDeepEqual(active, oldActive, 'Should have changed interpreter'); + } + + // Type in again, make sure it works (should use the current interpreter in the server) + suggestion = waitForSuggestion(mount.wrapper); + typeCode(getInteractiveEditor(mount.wrapper), 'print'); + await suggestion.promise; + suggestion.disposable.dispose(); + verifyIntellisenseVisible(mount.wrapper, 'print'); + + // Force suggestion box to disappear so that shutdown doesn't try to generate suggestions + // while we're destroying the editor. + inst.state.model!.setValue(''); + }, + () => { + return ioc; + } + ); + + InteractiveHelpers.runTest( + 'Jupyter autocomplete', + async () => { + if (ioc.mockJupyter) { + // This test only works when mocking. + + // Create an interactive window so that it listens to the results. + const { mount } = await InteractiveHelpers.getOrCreateInteractiveWindow(ioc); + + // Then enter some code. Don't submit, we're just testing that autocomplete appears + const suggestion = waitForSuggestion(mount.wrapper); + typeCode(getInteractiveEditor(mount.wrapper), 'print'); + await suggestion.promise; + suggestion.disposable.dispose(); + verifyIntellisenseVisible(mount.wrapper, 'printly'); + + // Force suggestion box to disappear so that shutdown doesn't try to generate suggestions + // while we're destroying the editor. + clearEditor(mount.wrapper); + } + }, + () => { + return ioc; + } + ); + + InteractiveHelpers.runTest( + 'Jupyter autocomplete not timeout', + async () => { + if (ioc.mockJupyter) { + // This test only works when mocking. + + // Create an interactive window so that it listens to the results. + const { mount } = await InteractiveHelpers.getOrCreateInteractiveWindow(ioc); + + // Force a timeout on the jupyter completions so that it takes some amount of time + ioc.mockJupyter.getCurrentSession()!.setCompletionTimeout(100); + + // Then enter some code. Don't submit, we're just testing that autocomplete appears + const suggestion = waitForSuggestion(mount.wrapper); + typeCode(getInteractiveEditor(mount.wrapper), 'print'); + await suggestion.promise; + suggestion.disposable.dispose(); + verifyIntellisenseVisible(mount.wrapper, 'printly'); + + // Force suggestion box to disappear so that shutdown doesn't try to generate suggestions + // while we're destroying the editor. + clearEditor(mount.wrapper); + } + }, + () => { + return ioc; + } + ); + + InteractiveHelpers.runTest( + 'Filtered Jupyter autocomplete, verify magic commands appear', + async () => { + if (ioc.mockJupyter) { + // This test only works when mocking. + + // Create an interactive window so that it listens to the results. + const { mount } = await InteractiveHelpers.getOrCreateInteractiveWindow(ioc); + + // Then enter some code. Don't submit, we're just testing that autocomplete appears + const suggestion = waitForSuggestion(mount.wrapper); + typeCode(getInteractiveEditor(mount.wrapper), 'print'); + enterEditorKey(mount.wrapper, { code: ' ', ctrlKey: true }); + await suggestion.promise; + suggestion.disposable.dispose(); + verifyIntellisenseNotVisible(mount.wrapper, '%%bash'); + + // Force suggestion box to disappear so that shutdown doesn't try to generate suggestions + // while we're destroying the editor. + clearEditor(mount.wrapper); + } + }, + () => { + return ioc; + } + ); + + InteractiveHelpers.runTest( + 'Filtered Jupyter autocomplete, verify magic commands are filtered', + async () => { + if (ioc.mockJupyter) { + // This test only works when mocking. + + // Create an interactive window so that it listens to the results. + const { mount } = await InteractiveHelpers.getOrCreateInteractiveWindow(ioc); + + // Then enter some code. Don't submit, we're just testing that autocomplete appears + const suggestion = waitForSuggestion(mount.wrapper); + typeCode(getInteractiveEditor(mount.wrapper), ' '); + enterEditorKey(mount.wrapper, { code: ' ', ctrlKey: true }); + await suggestion.promise; + suggestion.disposable.dispose(); + verifyIntellisenseVisible(mount.wrapper, '%%bash'); + + // Force suggestion box to disappear so that shutdown doesn't try to generate suggestions + // while we're destroying the editor. + clearEditor(mount.wrapper); + } + }, + () => { + return ioc; + } + ); + const notebookJSON: nbformat.INotebookContent = { + nbformat: 4, + nbformat_minor: 2, + cells: [ + { + cell_type: 'code', + execution_count: 1, + metadata: { + collapsed: true + }, + outputs: [ + { + data: { + 'text/plain': ['1'] + }, + output_type: 'execute_result', + execution_count: 1, + metadata: {} + } + ], + source: ['a=1\n', 'a'] + }, + { + cell_type: 'code', + execution_count: 2, + metadata: {}, + outputs: [ + { + data: { + 'text/plain': ['2'] + }, + output_type: 'execute_result', + execution_count: 2, + metadata: {} + } + ], + source: ['b=2\n', 'b'] + }, + { + cell_type: 'code', + execution_count: 3, + metadata: {}, + outputs: [ + { + data: { + 'text/plain': ['3'] + }, + output_type: 'execute_result', + execution_count: 3, + metadata: {} + } + ], + source: ['c=3\n', 'c'] + } + ], + metadata: { + orig_nbformat: 4, + kernelspec: { + display_name: 'JUNK', + name: 'JUNK' + }, + language_info: { + name: 'python', + version: '1.2.3' + } + } + }; + NativeHelpers.runMountedTest('Hover on notebook', async () => { + // Create an notebook so that it listens to the results. + const kernelIdle = ioc + .get<ITestNativeEditorProvider>(INotebookEditorProvider) + .waitForMessage(undefined, InteractiveWindowMessages.KernelIdle); + const ne = await NativeHelpers.openEditor(ioc, JSON.stringify(notebookJSON)); + await ne.editor.show(); + await kernelIdle; + + // Cause a hover event over the first character + await waitForHover('Native', ne.mount.wrapper, 1, 1); + verifyHoverVisible('Native', ne.mount.wrapper, 'a=1\na'); + await NativeHelpers.closeNotebook(ioc, ne.editor); + }); + + InteractiveHelpers.runTest( + 'Hover on interactive', + async () => { + // Create an interactive window so that it listens to the results. + const { window, mount } = await InteractiveHelpers.getOrCreateInteractiveWindow(ioc); + addMockData(ioc, 'a=1\na', 1); + addMockData(ioc, 'b=2\nb', 2); + + await InteractiveHelpers.addCode(ioc, 'a=1\na'); + await InteractiveHelpers.addCode(ioc, 'b=2\nb'); + + // Cause a hover event over the first character + await waitForHover('Interactive', mount.wrapper, 1, 1); + verifyHoverVisible('Interactive', mount.wrapper, 'a=1\na\nb=2\nb'); + + await InteractiveHelpers.closeInteractiveWindow(ioc, window); + }, + () => { + return ioc; + } + ); + }); +}); diff --git a/src/test/datascience/intellisense.unit.test.ts b/src/test/datascience/intellisense.unit.test.ts new file mode 100644 index 000000000000..8089af05eeb1 --- /dev/null +++ b/src/test/datascience/intellisense.unit.test.ts @@ -0,0 +1,619 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import * as uuid from 'uuid/v4'; + +import { instance, mock } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { PythonSettings } from '../../client/common/configSettings'; +import { IConfigurationService } from '../../client/common/types'; +import { Identifiers } from '../../client/datascience/constants'; +import { IntellisenseDocument } from '../../client/datascience/interactive-common/intellisense/intellisenseDocument'; +import { IntellisenseProvider } from '../../client/datascience/interactive-common/intellisense/intellisenseProvider'; +import { + IEditorContentChange, + IInteractiveWindowMapping, + InteractiveWindowMessages +} from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { JupyterVariables } from '../../client/datascience/jupyter/jupyterVariables'; +import { ICell, IDataScienceFileSystem, INotebookProvider } from '../../client/datascience/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { createEmptyCell, generateTestCells } from '../../datascience-ui/interactive-common/mainState'; +import { generateReverseChange, IMonacoTextModel } from '../../datascience-ui/react-common/monacoHelpers'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; +import { MockLanguageServerCache } from './mockLanguageServerCache'; + +// tslint:disable:no-any unified-signatures +const TestCellContents = `myvar = """ # Lorem Ipsum + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Nullam eget varius ligula, eget fermentum mauris. +Cras ultrices, enim sit amet iaculis ornare, nisl nibh aliquet elit, sed ultrices velit ipsum dignissim nisl. +Nunc quis orci ante. Vivamus vel blandit velit. +","Sed mattis dui diam, et blandit augue mattis vestibulum. +Suspendisse ornare interdum velit. Suspendisse potenti. +Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. +""" +df +df +`; + +// tslint:disable-next-line: max-func-body-length +suite('DataScience Intellisense Unit Tests', () => { + let intellisenseProvider: IntellisenseProvider; + let intellisenseDocument: IntellisenseDocument; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let languageServerCache: MockLanguageServerCache; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let configService: TypeMoq.IMock<IConfigurationService>; + let fileSystem: TypeMoq.IMock<IDataScienceFileSystem>; + let notebookProvider: TypeMoq.IMock<INotebookProvider>; + let cells: ICell[] = [createEmptyCell(Identifiers.EditCellId, null)]; + const pythonSettings = new (class extends PythonSettings { + public fireChangeEvent() { + this.changed.fire(); + } + })(undefined, new MockAutoSelectionService()); + + setup(async () => { + languageServerCache = new MockLanguageServerCache(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + fileSystem = TypeMoq.Mock.ofType<IDataScienceFileSystem>(); + notebookProvider = TypeMoq.Mock.ofType<INotebookProvider>(); + const variableProvider = mock(JupyterVariables); + + pythonSettings.languageServer = LanguageServerType.Microsoft; + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings); + workspaceService.setup((w) => w.rootPath).returns(() => '/foo/bar'); + fileSystem + .setup((f) => f.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((f1: Uri, f2: Uri) => { + return f1?.fsPath?.toLowerCase() === f2.fsPath?.toLowerCase(); + }); + + intellisenseProvider = new IntellisenseProvider( + workspaceService.object, + fileSystem.object, + notebookProvider.object, + interpreterService.object, + languageServerCache, + instance(variableProvider) + ); + intellisenseDocument = await intellisenseProvider.getDocument(); + }); + + function sendMessage<M extends IInteractiveWindowMapping, T extends keyof M>( + type: T, + payload?: M[T] + ): Promise<void> { + const result = languageServerCache.getMockServer().waitForNotification(); + intellisenseProvider.onMessage(type.toString(), payload); + return result; + } + + function addCell(code: string, id: string): Promise<void> { + const cell = createEmptyCell(id, null); + cell.data.source = code; + const result = sendMessage(InteractiveWindowMessages.UpdateModel, { + source: 'user', + kind: 'add', + oldDirty: false, + newDirty: true, + fullText: code, + currentText: code, + cell + }); + cells.splice(cells.length - 1, 0, cell); + return result; + } + + function generateModel(doc: IntellisenseDocument): IMonacoTextModel { + const code = doc.getText(); + return { + id: '1', + getValue: () => code, + getValueLength: () => code.length, + getVersionId: () => doc.version, + getPositionAt: (o: number) => { + const p = doc.positionAt(o); + return { lineNumber: p.line + 1, column: p.character + 1 }; + } + }; + } + + function sendUpdate( + id: string, + oldText: string, + doc: IntellisenseDocument, + change: IEditorContentChange, + source: 'user' | 'undo' | 'redo' = 'user' + ) { + const reverse = { + ...generateReverseChange(oldText, generateModel(doc), change), + position: { lineNumber: 1, column: 1 } + }; + return sendMessage(InteractiveWindowMessages.UpdateModel, { + source, + kind: 'edit', + oldDirty: false, + newDirty: true, + forward: [change], + reverse: [reverse], + id + }); + } + + function updateCell( + newCode: string, + oldCode: string, + id: string, + source: 'user' | 'undo' | 'redo' = 'user' + ): Promise<void> { + const oldSplit = oldCode.split('\n'); + const change: IEditorContentChange = { + range: { + startLineNumber: 1, + startColumn: 1, + endLineNumber: oldSplit.length, + endColumn: oldSplit[oldSplit.length - 1].length + 1 + }, + rangeOffset: 0, + rangeLength: oldCode.length, + text: newCode, + position: { + column: 1, + lineNumber: 1 + } + }; + return sendUpdate(id, oldCode, getDocument(), change, source); + } + + function addCode(code: string, line: number, pos: number, offset: number): Promise<void> { + if (!line || !pos) { + throw new Error('Invalid line or position data'); + } + const change: IEditorContentChange = { + range: { + startLineNumber: line, + startColumn: pos, + endLineNumber: line, + endColumn: pos + }, + rangeOffset: offset, + rangeLength: 0, + text: code, + position: { + column: 1, + lineNumber: 1 + } + }; + return sendMessage(InteractiveWindowMessages.UpdateModel, { + source: 'user', + kind: 'edit', + oldDirty: false, + newDirty: true, + forward: [change], + reverse: [change], + id: cells[cells.length - 1].id + }); + } + + function removeCode(line: number, startPos: number, endPos: number, length: number): Promise<void> { + if (!line || !startPos || !endPos) { + throw new Error('Invalid line or position data'); + } + const change: IEditorContentChange = { + range: { + startLineNumber: line, + startColumn: startPos, + endLineNumber: line, + endColumn: endPos + }, + rangeOffset: startPos, + rangeLength: length, + text: '', + position: { + column: 1, + lineNumber: 1 + } + }; + return sendUpdate(cells[cells.length - 1].id, '', getDocument(), change); + } + + async function removeCell( + cell: ICell | undefined, + oldIndex: number = -1, + source: 'user' | 'undo' | 'redo' = 'user' + ): Promise<number> { + if (cell) { + let index = cells.findIndex((c) => c.id === cell.id); + if (index < 0) { + index = oldIndex; + } else { + cells.splice(index, 1); + } + await sendMessage(InteractiveWindowMessages.UpdateModel, { + source, + kind: 'remove', + oldDirty: false, + newDirty: true, + cell, + index + }); + return index; + } + return -1; + } + + function removeAllCells(source: 'user' | 'undo' | 'redo' = 'user', oldCells: ICell[] = cells): Promise<void> { + return sendMessage(InteractiveWindowMessages.UpdateModel, { + source, + kind: 'remove_all', + oldDirty: false, + newDirty: true, + oldCells, + newCellId: uuid() + }); + } + + function swapCells(id1: string, id2: string, source: 'user' | 'undo' | 'redo' = 'user'): Promise<void> { + return sendMessage(InteractiveWindowMessages.UpdateModel, { + source, + kind: 'swap', + oldDirty: false, + newDirty: true, + firstCellId: id1, + secondCellId: id2 + }); + } + + function insertCell( + id: string, + code: string, + codeCellAbove?: string, + source: 'user' | 'undo' | 'redo' = 'user', + end?: boolean + ): Promise<void> { + const cell = createEmptyCell(id, null); + cell.data.source = code; + const index = codeCellAbove ? cells.findIndex((c) => c.id === codeCellAbove) : end ? cells.length : 0; + if (source === 'undo') { + cells = cells.filter((c) => c.id !== id); + } else { + cells.splice(index, 0, cell); + } + return sendMessage(InteractiveWindowMessages.UpdateModel, { + source, + kind: 'insert', + oldDirty: false, + newDirty: true, + codeCellAboveId: codeCellAbove, + cell, + index + }); + } + + function loadAllCells(allCells: ICell[]): Promise<void> { + cells = allCells; + intellisenseProvider.onMessage(InteractiveWindowMessages.NotebookIdentity, { + resource: Uri.parse('file:///foo.ipynb'), + type: 'native' + }); + + // Load all cells will actually respond with a notification, NotebookIdentity won't so don't wait for it. + return sendMessage(InteractiveWindowMessages.LoadAllCellsComplete, { cells }); + } + + function getDocumentContents(): string { + return languageServerCache.getMockServer().getDocumentContents(); + } + + function getDocument(): IntellisenseDocument { + return intellisenseDocument; + } + + test('Add a single cell', async () => { + await addCell('import sys\n\n', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n\n\n', 'Document not set'); + }); + + test('Add two cells', async () => { + await addCell('import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + await addCell('import sys', '2'); + expect(getDocumentContents()).to.be.eq('import sys\nimport sys\n', 'Document not set after double'); + }); + + test('Add a cell and edit', async () => { + await addCell('import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + await addCode('i', 1, 1, 0); + expect(getDocumentContents()).to.be.eq('import sys\ni', 'Document not set after edit'); + await addCode('m', 1, 2, 1); + expect(getDocumentContents()).to.be.eq('import sys\nim', 'Document not set after edit'); + await addCode('\n', 1, 3, 2); + expect(getDocumentContents()).to.be.eq('import sys\nim\n', 'Document not set after edit'); + }); + + test('Add a cell and remove', async () => { + await addCell('import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + await addCode('i', 1, 1, 0); + expect(getDocumentContents()).to.be.eq('import sys\ni', 'Document not set after edit'); + await removeCode(1, 1, 2, 1); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set after edit'); + await addCode('\n', 1, 1, 0); + expect(getDocumentContents()).to.be.eq('import sys\n\n', 'Document not set after edit'); + }); + + test('Remove a section in the middle', async () => { + await addCell('import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + await addCode('import os', 1, 1, 0); + expect(getDocumentContents()).to.be.eq('import sys\nimport os', 'Document not set after edit'); + await removeCode(1, 4, 7, 4); + expect(getDocumentContents()).to.be.eq('import sys\nimp os', 'Document not set after edit'); + }); + + test('Remove a bunch in a row', async () => { + await addCell('import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + await addCode('p', 1, 1, 0); + await addCode('r', 1, 2, 1); + await addCode('i', 1, 3, 2); + await addCode('n', 1, 4, 3); + await addCode('t', 1, 5, 4); + expect(getDocumentContents()).to.be.eq('import sys\nprint', 'Document not set after edit'); + await removeCode(1, 5, 6, 1); + await removeCode(1, 4, 5, 1); + await removeCode(1, 3, 4, 1); + await removeCode(1, 2, 3, 1); + await removeCode(1, 1, 2, 1); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set after edit'); + }); + test('Remove from a line', async () => { + await addCell('import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + await addCode('s', 1, 1, 0); + await addCode('y', 1, 2, 1); + await addCode('s', 1, 3, 2); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); + await addCode('\n', 1, 4, 3); + expect(getDocumentContents()).to.be.eq('import sys\nsys\n', 'Document not set after edit'); + await addCode('s', 2, 1, 3); + await addCode('y', 2, 2, 4); + await addCode('s', 2, 3, 5); + expect(getDocumentContents()).to.be.eq('import sys\nsys\nsys', 'Document not set after edit'); + await removeCode(1, 3, 4, 1); + expect(getDocumentContents()).to.be.eq('import sys\nsy\nsys', 'Document not set after edit'); + }); + + test('Add cell after adding code', async () => { + await addCell('import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + await addCode('s', 1, 1, 0); + await addCode('y', 1, 2, 1); + await addCode('s', 1, 3, 2); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); + await addCell('import sys', '2'); + expect(getDocumentContents()).to.be.eq('import sys\nimport sys\nsys', 'Adding a second cell broken'); + }); + + test('Collapse expand cell', async () => { + await addCell('import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + await updateCell('import sys\nsys.version_info', 'import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Readding a cell broken'); + await updateCell('import sys', 'import sys\nsys.version_info', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Collapsing a cell broken'); + await updateCell('import sys', 'import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Updating a cell broken'); + }); + + test('Collapse expand cell after adding code', async () => { + await addCell('import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + await addCode('s', 1, 1, 0); + await addCode('y', 1, 2, 1); + await addCode('s', 1, 3, 2); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); + await updateCell('import sys\nsys.version_info', 'import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Readding a cell broken'); + await updateCell('import sys', 'import sys\nsys.version_info', '1'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Collapsing a cell broken'); + await updateCell('import sys', 'import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Updating a cell broken'); + }); + + test('Add a cell and remove it', async () => { + await addCell('import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); + await addCode('s', 1, 1, 0); + await addCode('y', 1, 2, 1); + await addCode('s', 1, 3, 2); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); + await removeCell(cells.find((c) => c.id === '1')); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Removing a cell broken'); + await addCell('import sys', '2'); + expect(getDocumentContents()).to.be.eq('import sys\nimport sys\nsys', 'Adding a cell broken'); + await addCell('import bar', '3'); + expect(getDocumentContents()).to.be.eq('import sys\nimport sys\nimport bar\nsys', 'Adding a cell broken'); + await removeCell(cells.find((c) => c.id === '1')); + expect(getDocumentContents()).to.be.eq('import sys\nimport sys\nimport bar\nsys', 'Removing a cell broken'); + }); + + test('Add a bunch of cells and remove them', async () => { + await addCode('s', 1, 1, 0); + await addCode('y', 1, 2, 1); + await addCode('s', 1, 3, 2); + expect(getDocumentContents()).to.be.eq('sys', 'Document not set after edit'); + await addCell('import sys', '1'); + expect(getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set'); + await addCell('import foo', '2'); + expect(getDocumentContents()).to.be.eq('import sys\nimport foo\nsys', 'Document not set'); + await addCell('import bar', '3'); + expect(getDocumentContents()).to.be.eq('import sys\nimport foo\nimport bar\nsys', 'Document not set'); + await removeAllCells(); + expect(getDocumentContents()).to.be.eq('import sys\nimport foo\nimport bar\nsys', 'Removing all cells broken'); + await addCell('import baz', '3'); + expect(getDocumentContents()).to.be.eq( + 'import sys\nimport foo\nimport bar\nimport baz\nsys', + 'Document not set' + ); + }); + + test('Load remove and insert', async () => { + const test = generateTestCells('foo.py', 1); + await loadAllCells(test); + expect(getDocumentContents()).to.be.eq(TestCellContents, 'Load all cells is failing'); + await removeAllCells(); + expect(getDocumentContents()).to.be.eq('', 'Remove all cells is failing'); + await insertCell('6', 'foo'); + expect(getDocumentContents()).to.be.eq('foo\n', 'Insert after remove'); + await insertCell('7', 'bar', '6'); + expect(getDocumentContents()).to.be.eq('foo\nbar\n', 'Double insert after remove'); + }); + + test('Swap cells around', async () => { + const test = generateTestCells('foo.py', 1); + await loadAllCells(test); + await swapCells('0', '1'); // 2nd cell is markdown + expect(getDocumentContents()).to.be.eq(TestCellContents, 'Swap cells should skip swapping on markdown'); + await swapCells('0', '2'); + const afterSwap = `df +myvar = """ # Lorem Ipsum + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Nullam eget varius ligula, eget fermentum mauris. +Cras ultrices, enim sit amet iaculis ornare, nisl nibh aliquet elit, sed ultrices velit ipsum dignissim nisl. +Nunc quis orci ante. Vivamus vel blandit velit. +","Sed mattis dui diam, et blandit augue mattis vestibulum. +Suspendisse ornare interdum velit. Suspendisse potenti. +Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. +""" +df +`; + expect(getDocumentContents()).to.be.eq(afterSwap, 'Swap cells failed'); + await swapCells('0', '2'); + expect(getDocumentContents()).to.be.eq(TestCellContents, 'Swap cells back failed'); + }); + + test('Insert and swap', async () => { + const test = generateTestCells('foo.py', 1); + await loadAllCells(test); + expect(getDocumentContents()).to.be.eq(TestCellContents, 'Load all cells is failing'); + await insertCell('6', 'foo'); + const afterInsert = `foo +myvar = """ # Lorem Ipsum + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Nullam eget varius ligula, eget fermentum mauris. +Cras ultrices, enim sit amet iaculis ornare, nisl nibh aliquet elit, sed ultrices velit ipsum dignissim nisl. +Nunc quis orci ante. Vivamus vel blandit velit. +","Sed mattis dui diam, et blandit augue mattis vestibulum. +Suspendisse ornare interdum velit. Suspendisse potenti. +Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. +""" +df +df +`; + expect(getDocumentContents()).to.be.eq(afterInsert, 'Insert cell failed'); + await insertCell('7', 'foo', '0'); + const afterInsert2 = `foo +myvar = """ # Lorem Ipsum + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Nullam eget varius ligula, eget fermentum mauris. +Cras ultrices, enim sit amet iaculis ornare, nisl nibh aliquet elit, sed ultrices velit ipsum dignissim nisl. +Nunc quis orci ante. Vivamus vel blandit velit. +","Sed mattis dui diam, et blandit augue mattis vestibulum. +Suspendisse ornare interdum velit. Suspendisse potenti. +Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. +""" +foo +df +df +`; + expect(getDocumentContents()).to.be.eq(afterInsert2, 'Insert2 cell failed'); + await removeCell(cells.find((c) => c.id === '7')); + expect(getDocumentContents()).to.be.eq(afterInsert, 'Remove 2 cell failed'); + await swapCells('0', '2'); + const afterSwap = `foo +df +myvar = """ # Lorem Ipsum + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Nullam eget varius ligula, eget fermentum mauris. +Cras ultrices, enim sit amet iaculis ornare, nisl nibh aliquet elit, sed ultrices velit ipsum dignissim nisl. +Nunc quis orci ante. Vivamus vel blandit velit. +","Sed mattis dui diam, et blandit augue mattis vestibulum. +Suspendisse ornare interdum velit. Suspendisse potenti. +Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. +""" +df +`; + expect(getDocumentContents()).to.be.eq(afterSwap, 'Swap cell failed'); + }); + + test('Edit and undo', async () => { + const loadable = [createEmptyCell('0', null), createEmptyCell('1', null)]; + loadable[0].data.source = 'a=1\na'; + loadable[1].data.source = 'b=2\nb'; + await loadAllCells(loadable); + const startContent = `a=1 +a +b=2 +b +`; + expect(getDocumentContents()).to.be.eq(startContent, 'Load all cells is failing'); + await swapCells('0', '1'); + const afterSwap = `b=2 +b +a=1 +a +`; + expect(getDocumentContents()).to.be.eq(afterSwap, 'Swap cell failed'); + await swapCells('0', '1', 'undo'); + expect(getDocumentContents()).to.be.eq(startContent, 'Swap cell undo failed'); + await updateCell('a=4\na', 'a=1\na', '0'); + const afterUpdate = `a=4 +a +b=2 +b +`; + expect(getDocumentContents()).to.be.eq(afterUpdate, 'Edit cell failed'); + await updateCell('a=4\na', 'a=1\na', '0', 'undo'); + expect(getDocumentContents()).to.be.eq(startContent, 'Edit undo cell failed'); + + const afterInsert = `a=1 +a +b=2 +b +c=5 +c +`; + await insertCell('2', 'c=5\nc', undefined, 'user', true); + expect(getDocumentContents()).to.be.eq(afterInsert, 'Insert cell failed'); + await insertCell('2', 'c=5\nc', undefined, 'undo', true); + expect(getDocumentContents()).to.be.eq(startContent, 'Insert cell update failed'); + const oldCells = [...cells]; + await removeAllCells(); + expect(getDocumentContents()).to.be.eq('', 'Remove all failed'); + await removeAllCells('undo', oldCells); + expect(getDocumentContents()).to.be.eq(startContent, 'Remove all undo failed'); + const cell = cells.find((c) => c.id === '1'); + const oldIndex = await removeCell(cell); + const afterRemove = `a=1 +a +`; + expect(getDocumentContents()).to.be.eq(afterRemove, 'Remove failed'); + await removeCell(cell, oldIndex, 'undo'); + expect(getDocumentContents()).to.be.eq(startContent, 'Remove undo failed'); + }); +}); diff --git a/src/test/datascience/interactive-common/notebookProvider.unit.test.ts b/src/test/datascience/interactive-common/notebookProvider.unit.test.ts new file mode 100644 index 000000000000..015dfccaedff --- /dev/null +++ b/src/test/datascience/interactive-common/notebookProvider.unit.test.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { expect } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import * as vscode from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { IDataScienceSettings, IDisposableRegistry, IPythonSettings } from '../../../client/common/types'; +import { NotebookProvider } from '../../../client/datascience/interactive-common/notebookProvider'; +import { IJupyterNotebookProvider, INotebook, IRawNotebookProvider } from '../../../client/datascience/types'; + +function Uri(filename: string): vscode.Uri { + return vscode.Uri.file(filename); +} + +// tslint:disable:no-any +function createTypeMoq<T>(tag: string): typemoq.IMock<T> { + // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class + // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 + const result = typemoq.Mock.ofType<T>(); + (result as any).tag = tag; + result.setup((x: any) => x.then).returns(() => undefined); + return result; +} + +// tslint:disable: max-func-body-length +suite('DataScience - NotebookProvider', () => { + let notebookProvider: NotebookProvider; + let disposableRegistry: IDisposableRegistry; + let jupyterNotebookProvider: IJupyterNotebookProvider; + let rawNotebookProvider: IRawNotebookProvider; + let pythonSettings: IPythonSettings; + let dataScienceSettings: IDataScienceSettings; + + setup(() => { + disposableRegistry = mock<IDisposableRegistry>(); + jupyterNotebookProvider = mock<IJupyterNotebookProvider>(); + rawNotebookProvider = mock<IRawNotebookProvider>(); + const workspaceService = mock<IWorkspaceService>(); + + // Set up our settings + pythonSettings = mock<IPythonSettings>(); + dataScienceSettings = mock<IDataScienceSettings>(); + when(pythonSettings.datascience).thenReturn(instance(dataScienceSettings)); + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(dataScienceSettings.jupyterServerURI).thenReturn('local'); + when(dataScienceSettings.useDefaultConfigForJupyter).thenReturn(true); + when(rawNotebookProvider.supported).thenReturn(() => Promise.resolve(false)); + + notebookProvider = new NotebookProvider( + instance(disposableRegistry), + instance(rawNotebookProvider), + instance(jupyterNotebookProvider), + instance(workspaceService) + ); + }); + + test('NotebookProvider getOrCreateNotebook jupyter provider has notebook already', async () => { + const notebookMock = createTypeMoq<INotebook>('jupyter notebook'); + when(jupyterNotebookProvider.getNotebook(anything())).thenResolve(notebookMock.object); + + const notebook = await notebookProvider.getOrCreateNotebook({ identity: Uri('C:\\\\foo.py') }); + expect(notebook).to.not.equal(undefined, 'Provider should return a notebook'); + }); + + test('NotebookProvider getOrCreateNotebook jupyter provider does not have notebook already', async () => { + const notebookMock = createTypeMoq<INotebook>('jupyter notebook'); + when(jupyterNotebookProvider.getNotebook(anything())).thenResolve(undefined); + when(jupyterNotebookProvider.createNotebook(anything())).thenResolve(notebookMock.object); + when(jupyterNotebookProvider.connect(anything())).thenResolve({} as any); + + const notebook = await notebookProvider.getOrCreateNotebook({ identity: Uri('C:\\\\foo.py') }); + expect(notebook).to.not.equal(undefined, 'Provider should return a notebook'); + }); + + test('NotebookProvider getOrCreateNotebook second request should return the notebook already cached', async () => { + const notebookMock = createTypeMoq<INotebook>('jupyter notebook'); + when(jupyterNotebookProvider.getNotebook(anything())).thenResolve(undefined); + when(jupyterNotebookProvider.createNotebook(anything())).thenResolve(notebookMock.object); + when(jupyterNotebookProvider.connect(anything())).thenResolve({} as any); + + const notebook = await notebookProvider.getOrCreateNotebook({ identity: Uri('C:\\\\foo.py') }); + expect(notebook).to.not.equal(undefined, 'Server should return a notebook'); + + const notebook2 = await notebookProvider.getOrCreateNotebook({ identity: Uri('C:\\\\foo.py') }); + expect(notebook2).to.equal(notebook); + }); +}); diff --git a/src/test/datascience/interactive-common/notebookServerProvider.unit.test.ts b/src/test/datascience/interactive-common/notebookServerProvider.unit.test.ts new file mode 100644 index 000000000000..415f96c01dd6 --- /dev/null +++ b/src/test/datascience/interactive-common/notebookServerProvider.unit.test.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { IConfigurationService, IDataScienceSettings, IPythonSettings } from '../../../client/common/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { NotebookServerProvider } from '../../../client/datascience/interactive-common/notebookServerProvider'; +import { ProgressReporter } from '../../../client/datascience/progress/progressReporter'; +import { IJupyterExecution, INotebookServer } from '../../../client/datascience/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +// tslint:disable:no-any +function createTypeMoq<T>(tag: string): typemoq.IMock<T> { + // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class + // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 + const result = typemoq.Mock.ofType<T>(); + (result as any).tag = tag; + result.setup((x: any) => x.then).returns(() => undefined); + return result; +} + +// tslint:disable: max-func-body-length +suite('DataScience - NotebookServerProvider', () => { + let serverProvider: NotebookServerProvider; + let progressReporter: ProgressReporter; + let configurationService: IConfigurationService; + let jupyterExecution: IJupyterExecution; + let applicationShell: IApplicationShell; + let interpreterService: IInterpreterService; + let pythonSettings: IPythonSettings; + let dataScienceSettings: IDataScienceSettings; + const workingPython: PythonEnvironment = { + path: '/foo/bar/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 + }; + + setup(() => { + progressReporter = mock(ProgressReporter); + configurationService = mock<IConfigurationService>(); + jupyterExecution = mock<IJupyterExecution>(); + applicationShell = mock<IApplicationShell>(); + interpreterService = mock<IInterpreterService>(); + + // Set up our settings + pythonSettings = mock<IPythonSettings>(); + dataScienceSettings = mock<IDataScienceSettings>(); + when(pythonSettings.datascience).thenReturn(instance(dataScienceSettings)); + when(dataScienceSettings.jupyterServerURI).thenReturn('local'); + when(dataScienceSettings.useDefaultConfigForJupyter).thenReturn(true); + when(configurationService.getSettings(anything())).thenReturn(instance(pythonSettings)); + + // Create the server provider + serverProvider = new NotebookServerProvider( + instance(progressReporter), + instance(configurationService), + instance(jupyterExecution), + instance(applicationShell), + instance(interpreterService) + ); + }); + + test('NotebookServerProvider - Get Only - no server', async () => { + when(jupyterExecution.getServer(anything())).thenResolve(undefined); + + const server = await serverProvider.getOrCreateServer({ getOnly: true }); + expect(server).to.equal(undefined, 'Server expected to be undefined'); + verify(jupyterExecution.getServer(anything())).once(); + }); + + test('NotebookServerProvider - Get Only - server', async () => { + const notebookServer = mock<INotebookServer>(); + when(jupyterExecution.getServer(anything())).thenResolve(instance(notebookServer)); + + const server = serverProvider.getOrCreateServer({ getOnly: true }); + expect(server).to.not.equal(undefined, 'Server expected to be defined'); + verify(jupyterExecution.getServer(anything())).once(); + }); + + test('NotebookServerProvider - Get Or Create', async () => { + when(jupyterExecution.getUsableJupyterPython()).thenResolve(workingPython); + const notebookServer = createTypeMoq<INotebookServer>('jupyter server'); + when(jupyterExecution.connectToNotebookServer(anything(), anything())).thenResolve(notebookServer.object); + + // Disable UI just lets us skip mocking the progress reporter + const server = await serverProvider.getOrCreateServer({ getOnly: false, disableUI: true }); + expect(server).to.not.equal(undefined, 'Server expected to be defined'); + }); +}); diff --git a/src/test/datascience/interactive-common/trustCommandHandler.unit.test.ts b/src/test/datascience/interactive-common/trustCommandHandler.unit.test.ts new file mode 100644 index 000000000000..7a4c1c05b021 --- /dev/null +++ b/src/test/datascience/interactive-common/trustCommandHandler.unit.test.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as fakeTimers from '@sinonjs/fake-timers'; +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; +import { ContextKey } from '../../../client/common/contextKey'; +import { CryptoUtils } from '../../../client/common/crypto'; +import { IDisposable } from '../../../client/common/types'; +import { DataScience } from '../../../client/common/utils/localize'; +import { Commands } from '../../../client/datascience/constants'; +import { TrustCommandHandler } from '../../../client/datascience/interactive-ipynb/trustCommandHandler'; +import { TrustService } from '../../../client/datascience/interactive-ipynb/trustService'; +import { INotebookStorageProvider } from '../../../client/datascience/notebookStorage/notebookStorageProvider'; +import { VSCodeNotebookModel } from '../../../client/datascience/notebookStorage/vscNotebookModel'; +import { INotebookEditorProvider, INotebookModel, ITrustService } from '../../../client/datascience/types'; +import { noop } from '../../core'; +import { MockMemento } from '../../mocks/mementos'; +import { createNotebookDocument, createNotebookModel, disposeAllDisposables } from '../notebook/helper'; + +// tslint:disable: no-any + +suite('DataScience - Trust Command Handler', () => { + let trustCommandHandler: IExtensionSingleActivationService; + let trustService: ITrustService; + let editorProvider: INotebookEditorProvider; + let storageProvider: INotebookStorageProvider; + let commandManager: ICommandManager; + let applicationShell: IApplicationShell; + let disposables: IDisposable[] = []; + let clock: fakeTimers.InstalledClock; + let contextKeySet: sinon.SinonStub<[boolean], Promise<void>>; + let model: INotebookModel; + let trustNotebookCommandCallback: (uri: Uri) => Promise<void>; + let testIndex = 0; + setup(() => { + trustService = mock<TrustService>(); + editorProvider = mock<INotebookEditorProvider>(); + storageProvider = mock<INotebookStorageProvider>(); + commandManager = mock<ICommandManager>(); + applicationShell = mock<IApplicationShell>(); + const crypto = mock(CryptoUtils); + testIndex += 1; + when(crypto.createHash(anything(), 'string')).thenReturn(`${testIndex}`); + model = createNotebookModel(false, Uri.file('a'), new MockMemento(), instance(crypto)); + createNotebookDocument(model as VSCodeNotebookModel); + when(storageProvider.getOrCreateModel(anything())).thenResolve(model); + disposables = []; + + when(trustService.trustNotebook(anything(), anything())).thenResolve(); + when(commandManager.registerCommand(anything(), anything(), anything())).thenCall(() => ({ dispose: noop })); + when(commandManager.registerCommand(Commands.TrustNotebook, anything(), anything())).thenCall((_, cb) => { + trustNotebookCommandCallback = cb.bind(trustCommandHandler); + return { dispose: noop }; + }); + + trustCommandHandler = new TrustCommandHandler( + instance(trustService), + instance(editorProvider), + instance(storageProvider), + instance(commandManager), + instance(applicationShell), + disposables + ); + + clock = fakeTimers.install(); + + contextKeySet = sinon.stub(ContextKey.prototype, 'set'); + contextKeySet.resolves(); + }); + teardown(() => { + sinon.restore(); + disposeAllDisposables(disposables); + clock.uninstall(); + }); + + test('Executing command will not update trust after dismissing the prompt', async () => { + when(applicationShell.showErrorMessage(anything(), anything(), anything(), anything())).thenResolve( + undefined as any + ); + + await trustCommandHandler.activate(); + await clock.runAllAsync(); + await trustNotebookCommandCallback(Uri.file('a')); + + verify(applicationShell.showErrorMessage(anything(), anything(), anything(), anything())).once(); + verify(trustService.trustNotebook(anything(), anything())).never(); + assert.isFalse(model.isTrusted); + }); + test('Executing command will update trust', async () => { + when(applicationShell.showErrorMessage(anything(), anything(), anything(), anything())).thenResolve( + DataScience.trustNotebook() as any + ); + + assert.isFalse(model.isTrusted); + await trustCommandHandler.activate(); + await clock.runAllAsync(); + await trustNotebookCommandCallback(Uri.file('a')); + + verify(applicationShell.showErrorMessage(anything(), anything(), anything(), anything())).once(); + verify(trustService.trustNotebook(anything(), anything())).once(); + assert.isTrue(model.isTrusted); + }); +}); diff --git a/src/test/datascience/interactive-common/trustService.unit.test.ts b/src/test/datascience/interactive-common/trustService.unit.test.ts new file mode 100644 index 000000000000..84c687bc8d6c --- /dev/null +++ b/src/test/datascience/interactive-common/trustService.unit.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { assert } from 'chai'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import { anything, instance, mock, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { IExtensionContext } from '../../../client/common/types'; +import { DataScienceFileSystem } from '../../../client/datascience/dataScienceFileSystem'; +import { DigestStorage } from '../../../client/datascience/interactive-ipynb/digestStorage'; +import { TrustService } from '../../../client/datascience/interactive-ipynb/trustService'; + +suite('DataScience - TrustService', () => { + let trustService: TrustService; + let alwaysTrustNotebooks: boolean = false; + setup(() => { + alwaysTrustNotebooks = false; + const configService = mock(ConfigurationService); + const fileSystem = mock(DataScienceFileSystem); + const context = typemoq.Mock.ofType<IExtensionContext>(); + context.setup((c) => c.globalStoragePath).returns(() => os.tmpdir()); + when(configService.getSettings()).thenCall(() => { + // tslint:disable-next-line: no-any + return { datascience: { alwaysTrustNotebooks } } as any; + }); + when(fileSystem.appendLocalFile(anything(), anything())).thenCall((f, c) => fs.appendFile(f, c)); + when(fileSystem.readLocalFile(anything())).thenCall((f) => fs.readFile(f)); + when(fileSystem.createLocalDirectory(anything())).thenCall((d) => fs.mkdir(d)); + when(fileSystem.localDirectoryExists(anything())).thenCall((d) => fs.pathExists(d)); + const digestStorage = new DigestStorage(instance(fileSystem), context.object); + trustService = new TrustService(digestStorage, instance(configService)); + }); + + test('Trusting a notebook', async () => { + const uri = Uri.file('foo.ipynb'); + await trustService.trustNotebook(uri, 'foobar'); + assert.ok(await trustService.isNotebookTrusted(uri, 'foobar'), 'Notebook is not trusted'); + assert.notOk(await trustService.isNotebookTrusted(uri, 'foobaz')); + }); + test('Trusting a notebook with same path', async () => { + const uri = Uri.file('foo.ipynb'); + const uri2 = Uri.file('FOO.ipynb'); + if (os.platform() === 'win32') { + await trustService.trustNotebook(uri, 'foobar'); + assert.ok(await trustService.isNotebookTrusted(uri2, 'foobar'), 'Notebook is not trusted'); + } + }); + test('Always trusting notebooks', async () => { + alwaysTrustNotebooks = true; + const uri = Uri.file('foo.ipynb'); + assert.ok(await trustService.isNotebookTrusted(uri, 'foobar'), 'Notebook is not trusted when all should be'); + }); +}); diff --git a/src/test/datascience/interactive-ipynb/nativeEditorProvider.functional.test.ts b/src/test/datascience/interactive-ipynb/nativeEditorProvider.functional.test.ts new file mode 100644 index 000000000000..c5e088449700 --- /dev/null +++ b/src/test/datascience/interactive-ipynb/nativeEditorProvider.functional.test.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable: no-any + +import { expect } from 'chai'; +import { Uri } from 'vscode'; +import { CancellationToken } from 'vscode-languageclient/node'; +import { INotebookStorageProvider } from '../../../client/datascience/notebookStorage/notebookStorageProvider'; +import { INotebookEditorProvider, INotebookModel } from '../../../client/datascience/types'; +import { concatMultilineString } from '../../../datascience-ui/common'; +import { createEmptyCell } from '../../../datascience-ui/interactive-common/mainState'; +import { DataScienceIocContainer } from '../dataScienceIocContainer'; +import { TestNativeEditorProvider } from '../testNativeEditorProvider'; + +// tslint:disable: max-func-body-length +suite('DataScience - Native Editor Provider', () => { + let ioc: DataScienceIocContainer; + setup(async () => { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(); + }); + + function createNotebookProvider() { + return ioc.get<TestNativeEditorProvider>(INotebookEditorProvider); + } + + test('Opening a notebook', async () => { + const provider = createNotebookProvider(); + const n = await provider.open(Uri.file('foo.ipynb')); + expect(n.file.fsPath).to.be.include('foo.ipynb'); + }); + + test('Multiple new notebooks have new names', async () => { + const provider = createNotebookProvider(); + const n1 = await provider.createNew(); + expect(n1.file.fsPath).to.be.include('Untitled-1'); + const n2 = await provider.createNew(); + expect(n2.file.fsPath).to.be.include('Untitled-2'); + }); + + test('Untitled files changing', async () => { + const provider = createNotebookProvider(); + const n1 = await provider.createNew(); + expect(n1.file.fsPath).to.be.include('Untitled-1'); + await n1.dispose(); + const n2 = await provider.createNew(); + expect(n2.file.fsPath).to.be.include('Untitled-2'); + await n2.dispose(); + const n3 = await provider.createNew(); + expect(n3.file.fsPath).to.be.include('Untitled-3'); + }); + + function insertCell(nbm: INotebookModel, index: number, code: string) { + const cell = createEmptyCell(undefined, 1); + cell.data.source = code; + return nbm.update({ + source: 'user', + kind: 'insert', + oldDirty: nbm.isDirty, + newDirty: true, + cell, + index + }); + } + + test('Untitled files reopening with changed contents', async () => { + let provider = createNotebookProvider(); + const n1 = await provider.createNew(); + let cells = n1.model!.cells; + expect(cells).to.be.lengthOf(1); + insertCell(n1.model!, 0, 'a=1'); + await ioc.get<INotebookStorageProvider>(INotebookStorageProvider).backup(n1.model!, CancellationToken.None); + const uri = n1.file; + + // Act like a reboot + provider = createNotebookProvider(); + const n2 = await provider.open(uri); + cells = n2.model!.cells; + expect(cells).to.be.lengthOf(2); + expect(concatMultilineString(cells[0].data.source)).to.be.eq('a=1'); + + // Act like another reboot but create a new file + provider = createNotebookProvider(); + const n3 = await provider.createNew(); + cells = n3.model!.cells; + expect(cells).to.be.lengthOf(1); + }); +}); diff --git a/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts b/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts new file mode 100644 index 000000000000..ad5093ca7361 --- /dev/null +++ b/src/test/datascience/interactive-ipynb/nativeEditorStorage.unit.test.ts @@ -0,0 +1,615 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Matcher } from 'ts-mockito/lib/matcher/type/Matcher'; +import * as typemoq from 'typemoq'; +import { ConfigurationChangeEvent, EventEmitter, FileType, TextEditor, Uri } from 'vscode'; + +import { CancellationToken } from 'vscode-jsonrpc'; +import { DocumentManager } from '../../../client/common/application/documentManager'; +import { + IDocumentManager, + IWebviewPanelMessageListener, + IWebviewPanelProvider, + IWorkspaceService +} from '../../../client/common/application/types'; +import { WebviewPanel } from '../../../client/common/application/webviewPanels/webviewPanel'; +import { WebviewPanelProvider } from '../../../client/common/application/webviewPanels/webviewPanelProvider'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { CryptoUtils } from '../../../client/common/crypto'; +import { IConfigurationService, ICryptoUtils, IDisposable, IExtensionContext } from '../../../client/common/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { + IEditorContentChange, + InteractiveWindowMessages +} from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { TrustService } from '../../../client/datascience/interactive-ipynb/trustService'; +import { JupyterExecutionFactory } from '../../../client/datascience/jupyter/jupyterExecutionFactory'; +import { NotebookModelFactory } from '../../../client/datascience/notebookStorage/factory'; +import { NativeEditorStorage } from '../../../client/datascience/notebookStorage/nativeEditorStorage'; +import { NotebookStorageProvider } from '../../../client/datascience/notebookStorage/notebookStorageProvider'; +import { + ICell, + IDataScienceFileSystem, + IJupyterExecution, + INotebookModel, + INotebookServerOptions, + ITrustService +} from '../../../client/datascience/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { concatMultilineString } from '../../../datascience-ui/common'; +import { createEmptyCell } from '../../../datascience-ui/interactive-common/mainState'; +import { MockMemento } from '../../mocks/mementos'; +import { MockWorkspaceConfiguration } from '../mockWorkspaceConfig'; + +// tslint:disable: no-any chai-vague-errors no-unused-expression + +// tslint:disable: max-func-body-length +suite('DataScience - Native Editor Storage', () => { + let workspace: IWorkspaceService; + let configService: IConfigurationService; + let fileSystem: typemoq.IMock<IDataScienceFileSystem>; + let docManager: IDocumentManager; + let interpreterService: IInterpreterService; + let webPanelProvider: IWebviewPanelProvider; + let executionProvider: IJupyterExecution; + let globalMemento: MockMemento; + let localMemento: MockMemento; + let trustService: ITrustService; + let context: typemoq.IMock<IExtensionContext>; + let crypto: ICryptoUtils; + let lastWriteFileValue: any; + let wroteToFileEvent: EventEmitter<string> = new EventEmitter<string>(); + let filesConfig: MockWorkspaceConfiguration | undefined; + let testIndex = 0; + let model: INotebookModel; + let storage: NotebookStorageProvider; + const disposables: IDisposable[] = []; + const baseUri = Uri.parse('file:///foo.ipynb'); + const untiledUri = Uri.parse('untitled:///untitled-1.ipynb'); + const baseFile = `{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a=1\\n", + "a" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b=2\\n", + "b" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c=3\\n", + "c" + ] + } + ], + "metadata": { + "file_extension": ".py", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.4" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +}`; + + const differentFile = `{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b=2\\n", + "b" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c=3\\n", + "c" + ] + } + ], + "metadata": { + "file_extension": ".py", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.4" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 + }`; + + setup(() => { + context = typemoq.Mock.ofType<IExtensionContext>(); + crypto = mock(CryptoUtils); + globalMemento = new MockMemento(); + localMemento = new MockMemento(); + configService = mock(ConfigurationService); + fileSystem = typemoq.Mock.ofType<IDataScienceFileSystem>(); + docManager = mock(DocumentManager); + workspace = mock(WorkspaceService); + interpreterService = mock(InterpreterService); + webPanelProvider = mock(WebviewPanelProvider); + executionProvider = mock(JupyterExecutionFactory); + trustService = mock(TrustService); + const settings = mock(PythonSettings); + const settingsChangedEvent = new EventEmitter<void>(); + + context + .setup((c) => c.globalStoragePath) + .returns(() => path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience', 'WorkspaceDir')); + + when(settings.onDidChange).thenReturn(settingsChangedEvent.event); + when(configService.getSettings()).thenReturn(instance(settings)); + + const configChangeEvent = new EventEmitter<ConfigurationChangeEvent>(); + when(workspace.onDidChangeConfiguration).thenReturn(configChangeEvent.event); + filesConfig = new MockWorkspaceConfiguration(); + when(workspace.getConfiguration('files', anything())).thenReturn(filesConfig); + + const interprerterChangeEvent = new EventEmitter<void>(); + when(interpreterService.onDidChangeInterpreter).thenReturn(interprerterChangeEvent.event); + + const editorChangeEvent = new EventEmitter<TextEditor | undefined>(); + when(docManager.onDidChangeActiveTextEditor).thenReturn(editorChangeEvent.event); + + const serverStartedEvent = new EventEmitter<INotebookServerOptions>(); + when(executionProvider.serverStarted).thenReturn(serverStartedEvent.event); + + when(trustService.isNotebookTrusted(anything(), anything())).thenReturn(Promise.resolve(true)); + when(trustService.trustNotebook(anything(), anything())).thenCall(() => { + return Promise.resolve(); + }); + + testIndex += 1; + when(crypto.createHash(anything(), 'string')).thenReturn(`${testIndex}`); + + let listener: IWebviewPanelMessageListener; + const webPanel = mock(WebviewPanel); + const startTime = Date.now(); + class WebPanelCreateMatcher extends Matcher { + public match(value: any) { + listener = value.listener; + listener.onMessage(InteractiveWindowMessages.Started, undefined); + return true; + } + public toString() { + return ''; + } + } + const matcher = (): any => { + return new WebPanelCreateMatcher(); + }; + when(webPanelProvider.create(matcher())).thenResolve(instance(webPanel)); + lastWriteFileValue = baseFile; + wroteToFileEvent = new EventEmitter<string>(); + fileSystem + .setup((f) => f.writeFile(typemoq.It.isAny(), typemoq.It.isAny())) + .returns((a1, a2) => { + if (a1.fsPath && a1.fsPath.includes(`${testIndex}.ipynb`)) { + lastWriteFileValue = a2; + wroteToFileEvent.fire(a2); + } + return Promise.resolve(); + }); + fileSystem + .setup((f) => f.writeLocalFile(typemoq.It.isAny(), typemoq.It.isAny())) + .returns((a1, a2) => { + if (a1 && a1.includes(`${testIndex}.ipynb`)) { + lastWriteFileValue = a2; + wroteToFileEvent.fire(a2); + } + return Promise.resolve(); + }); + fileSystem + .setup((f) => f.readFile(typemoq.It.isAny())) + .returns((_a1) => { + return Promise.resolve(lastWriteFileValue); + }); + fileSystem + .setup((f) => f.readLocalFile(typemoq.It.isAny())) + .returns((_a1) => { + return Promise.resolve(lastWriteFileValue); + }); + fileSystem + .setup((f) => f.stat(typemoq.It.isAny())) + .returns((_a1) => { + return Promise.resolve({ mtime: startTime, type: FileType.File, ctime: startTime, size: 100 }); + }); + storage = createStorage(); + }); + + function createStorage() { + const notebookStorage = new NativeEditorStorage( + instance(executionProvider), + fileSystem.object, // Use typemoq so can save values in returns + instance(crypto), + context.object, + globalMemento, + localMemento, + instance(trustService), + new NotebookModelFactory(false) + ); + + return new NotebookStorageProvider(notebookStorage, [], instance(workspace)); + } + + teardown(() => { + globalMemento.clear(); + sinon.reset(); + disposables.forEach((d) => d.dispose()); + }); + + function insertCell(index: number, code: string) { + const cell = createEmptyCell(undefined, 1); + cell.data.source = code; + return model.update({ + source: 'user', + kind: 'insert', + oldDirty: model.isDirty, + newDirty: true, + cell, + index + }); + } + + function swapCells(first: string, second: string) { + return model.update({ + source: 'user', + kind: 'swap', + oldDirty: model.isDirty, + newDirty: true, + firstCellId: first, + secondCellId: second + }); + } + + function editCell(changes: IEditorContentChange[], cell: ICell, _newCode: string) { + return model.update({ + source: 'user', + kind: 'edit', + oldDirty: model.isDirty, + newDirty: true, + forward: changes, + reverse: changes, + id: cell.id + }); + } + + function removeCell(index: number, cell: ICell) { + return model.update({ + source: 'user', + kind: 'remove', + oldDirty: model.isDirty, + newDirty: true, + index, + cell + }); + } + + function deleteAllCells() { + return model.update({ + source: 'user', + kind: 'remove_all', + oldDirty: model.isDirty, + newDirty: true, + oldCells: [...model.cells], + newCellId: '1' + }); + } + + test('Create new editor and add some cells', async () => { + model = await storage.getOrCreateModel(baseUri); + insertCell(0, '1'); + const cells = model.cells; + expect(cells).to.be.lengthOf(4); + expect(model.isDirty).to.be.equal(true, 'Editor should be dirty'); + expect(cells[0].id).to.be.match(/1/); + }); + + test('Move cells around', async () => { + model = await storage.getOrCreateModel(baseUri); + swapCells('NotebookImport#0', 'NotebookImport#1'); + const cells = model.cells; + expect(cells).to.be.lengthOf(3); + expect(model.isDirty).to.be.equal(true, 'Editor should be dirty'); + expect(cells[0].id).to.be.match(/NotebookImport#1/); + }); + + test('Edit/delete cells', async () => { + model = await storage.getOrCreateModel(baseUri); + expect(model.isDirty).to.be.equal(false, 'Editor should not be dirty'); + editCell( + [ + { + range: { + startLineNumber: 2, + startColumn: 1, + endLineNumber: 2, + endColumn: 1 + }, + rangeOffset: 4, + rangeLength: 0, + text: 'a', + position: { + lineNumber: 1, + column: 1 + } + } + ], + model.cells[1], + 'a' + ); + let cells = model.cells; + expect(cells).to.be.lengthOf(3); + expect(cells[1].id).to.be.match(/NotebookImport#1/); + expect(concatMultilineString(cells[1].data.source)).to.be.equals('b=2\nab'); + expect(model.isDirty).to.be.equal(true, 'Editor should be dirty'); + removeCell(0, cells[0]); + cells = model.cells; + expect(cells).to.be.lengthOf(2); + expect(cells[0].id).to.be.match(/NotebookImport#1/); + deleteAllCells(); + cells = model.cells; + expect(cells).to.be.lengthOf(1); + }); + + test('Editing a file and closing will keep contents', async () => { + await filesConfig?.update('autoSave', 'off'); + + model = await storage.getOrCreateModel(baseUri); + expect(model.isDirty).to.be.equal(false, 'Editor should not be dirty'); + editCell( + [ + { + range: { + startLineNumber: 2, + startColumn: 1, + endLineNumber: 2, + endColumn: 1 + }, + rangeOffset: 4, + rangeLength: 0, + text: 'a', + position: { + lineNumber: 1, + column: 1 + } + } + ], + model.cells[1], + 'a' + ); + + // Force a backup + await storage.backup(model, CancellationToken.None); + + // Recreate + storage = createStorage(); + model = await storage.getOrCreateModel(baseUri); + + const cells = model.cells; + expect(cells).to.be.lengthOf(3); + expect(cells[1].id).to.be.match(/NotebookImport#1/); + expect(concatMultilineString(cells[1].data.source)).to.be.equals('b=2\nab'); + expect(model.isDirty).to.be.equal(true, 'Editor should be dirty'); + }); + + test('Editing a new file and closing will keep contents', async () => { + model = await storage.getOrCreateModel(untiledUri, undefined, true); + expect(model.isDirty).to.be.equal(false, 'Editor should not be dirty'); + insertCell(0, 'a=1'); + + // Wait for backup + await storage.backup(model, CancellationToken.None); + + // Recreate + storage = createStorage(); + model = await storage.getOrCreateModel(untiledUri); + + const cells = model.cells; + expect(cells).to.be.lengthOf(2); + expect(concatMultilineString(cells[0].data.source)).to.be.equals('a=1'); + expect(model.isDirty).to.be.equal(true, 'Editor should be dirty'); + }); + + test('Opening file with local storage but no global will still open with old contents', async () => { + await filesConfig?.update('autoSave', 'off'); + // This test is really for making sure when a user upgrades to a new extension, we still have their old storage + const file = Uri.parse('file:///foo.ipynb'); + + // Initially nothing in memento + expect(globalMemento.get(`notebook-storage-${file.toString()}`)).to.be.undefined; + expect(localMemento.get(`notebook-storage-${file.toString()}`)).to.be.undefined; + + // Put the regular file into the local storage + await localMemento.update(`notebook-storage-${file.toString()}`, differentFile); + model = await storage.getOrCreateModel(file); + + // It should load with that value + const cells = model.cells; + expect(cells).to.be.lengthOf(2); + }); + + test('Opening file with global storage but no global file will still open with old contents', async () => { + // This test is really for making sure when a user upgrades to a new extension, we still have their old storage + const file = Uri.parse('file:///foo.ipynb'); + fileSystem.setup((f) => f.stat(typemoq.It.isAny())).returns(() => Promise.resolve({ mtime: 1 } as any)); + + // Initially nothing in memento + expect(globalMemento.get(`notebook-storage-${file.toString()}`)).to.be.undefined; + expect(localMemento.get(`notebook-storage-${file.toString()}`)).to.be.undefined; + + // Put the regular file into the global storage + await globalMemento.update(`notebook-storage-${file.toString()}`, { + contents: differentFile, + lastModifiedTimeMs: Date.now() + }); + model = await storage.getOrCreateModel(file); + + // It should load with that value + const cells = model.cells; + expect(cells).to.be.lengthOf(2); + }); + + test('Opening file with global storage will clear all global storage', async () => { + await filesConfig?.update('autoSave', 'off'); + + // This test is really for making sure when a user upgrades to a new extension, we still have their old storage + const file = Uri.parse('file:///foo.ipynb'); + fileSystem.setup((f) => f.stat(typemoq.It.isAny())).returns(() => Promise.resolve({ mtime: 1 } as any)); + + // Initially nothing in memento + expect(globalMemento.get(`notebook-storage-${file.toString()}`)).to.be.undefined; + expect(localMemento.get(`notebook-storage-${file.toString()}`)).to.be.undefined; + + // Put the regular file into the global storage + await globalMemento.update(`notebook-storage-${file.toString()}`, { + contents: differentFile, + lastModifiedTimeMs: Date.now() + }); + + // Put another file into the global storage + await globalMemento.update(`notebook-storage-file::///bar.ipynb`, { + contents: differentFile, + lastModifiedTimeMs: Date.now() + }); + + model = await storage.getOrCreateModel(file); + + // It should load with that value + const cells = model.cells; + expect(cells).to.be.lengthOf(2); + + // And global storage should be empty + expect(globalMemento.get(`notebook-storage-${file.toString()}`)).to.be.undefined; + expect(globalMemento.get(`notebook-storage-file::///bar.ipynb`)).to.be.undefined; + expect(localMemento.get(`notebook-storage-${file.toString()}`)).to.be.undefined; + }); +}); diff --git a/src/test/datascience/interactivePanel.functional.test.tsx b/src/test/datascience/interactivePanel.functional.test.tsx new file mode 100644 index 000000000000..0ef60f1e23cd --- /dev/null +++ b/src/test/datascience/interactivePanel.functional.test.tsx @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as React from 'react'; +import { PYTHON_LANGUAGE } from '../../client/common/constants'; +import { CellState } from '../../client/datascience/types'; +import { InteractiveCellComponent } from '../../datascience-ui/history-react/interactiveCell'; +import { IInteractivePanelProps, InteractivePanel } from '../../datascience-ui/history-react/interactivePanel'; +import { CursorPos, DebugState, ServerStatus } from '../../datascience-ui/interactive-common/mainState'; +import { TrustMessage } from '../../datascience-ui/interactive-common/trustMessage'; +import { noop } from '../core'; +import { mountComponent } from './testHelpers'; + +// tslint:disable: no-any + +suite('DataScience Interactive Panel', () => { + const noopAny: any = noop; + let props: IInteractivePanelProps; + setup(() => { + props = { + baseTheme: '', + busy: false, + cellVMs: [], + clickCell: noopAny, + codeCreated: noopAny, + codeTheme: '', + collapseAll: noopAny, + copyCellCode: noopAny, + currentExecutionCount: 0, + debugging: false, + deleteAllCells: noopAny, + deleteCell: noopAny, + dirty: false, + editCell: noopAny, + editCellVM: { + cell: { + file: '', + id: '', + line: 0, + state: CellState.finished, + data: { + cell_type: 'code', + execution_count: 0, + metadata: {}, + outputs: [{ data: '', execution_count: 1, metadata: {}, output_type: 'text' }], + source: '' + } + }, + cursorPos: CursorPos.Current, + editable: true, + focused: false, + hasBeenRun: true, + hideOutput: false, + inputBlockCollapseNeeded: false, + inputBlockOpen: false, + inputBlockShow: true, + inputBlockText: '', + scrollCount: 0, + selected: false, + runningByLine: DebugState.Design, + gathering: false + }, + editorLoaded: noopAny, + editorUnmounted: noopAny, + expandAll: noopAny, + export: noopAny, + exportAs: noopAny, + focusInput: noopAny, + focusPending: 0, + font: { family: '', size: 1 }, + gatherCell: noopAny, + gatherCellToScript: noopAny, + getVariableData: noopAny, + gotoCell: noopAny, + interruptKernel: noopAny, + isAtBottom: false, + kernel: { + displayName: '', + jupyterServerStatus: ServerStatus.Busy, + localizedUri: '', + language: PYTHON_LANGUAGE + }, + knownDark: false, + linkClick: noopAny, + loaded: true, + monacoReady: true, + openSettings: noopAny, + redo: noopAny, + redoStack: [], + restartKernel: noopAny, + scroll: noopAny, + selectKernel: noopAny, + selectServer: noopAny, + showDataViewer: noopAny, + showPlot: noopAny, + submitInput: noopAny, + submittedText: noopAny, + toggleInputBlock: noopAny, + toggleVariableExplorer: noopAny, + undo: noopAny, + undoStack: noopAny, + unfocus: noopAny, + widgetFailed: noopAny, + variableState: { + currentExecutionCount: 0, + pageSize: 0, + sortAscending: true, + sortColumn: '', + variables: [], + visible: true, + containerHeight: 0, + gridHeight: 200, + refreshCount: 0, + showVariablesOnDebug: false + }, + setVariableExplorerHeight: noopAny, + editorOptions: {}, + settings: { showCellInputCode: true, allowInput: true, extraSettings: { editor: {} } } as any, + isNotebookTrusted: true + }; + }); + test('Input Cell is displayed', () => { + props.settings!.allowInput = true; + + const wrapper = mountComponent('interactive', <InteractivePanel {...props}></InteractivePanel>); + + assert.equal(wrapper.find(InteractiveCellComponent).length, 1); + }); + test('Input Cell is not displayed', () => { + props.settings!.allowInput = false; + + const wrapper = mountComponent('interactive', <InteractivePanel {...props}></InteractivePanel>); + + assert.equal(wrapper.find(InteractiveCellComponent).length, 0); + }); + test('Trust message is not displayed', () => { + const wrapper = mountComponent('interactive', <InteractivePanel {...props}></InteractivePanel>); + + assert.equal(wrapper.find(TrustMessage).length, 0); + }); +}); diff --git a/src/test/datascience/interactiveWindow.functional.test.tsx b/src/test/datascience/interactiveWindow.functional.test.tsx new file mode 100644 index 000000000000..504e3499f401 --- /dev/null +++ b/src/test/datascience/interactiveWindow.functional.test.tsx @@ -0,0 +1,1314 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import { parse } from 'node-html-parser'; +import * as os from 'os'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Memento, Selection, TextDocument, TextEditor, Uri } from 'vscode'; + +import { ReactWrapper } from 'enzyme'; +import { anything, when } from 'ts-mockito'; +import { IApplicationShell, IDocumentManager } from '../../client/common/application/types'; +import { GLOBAL_MEMENTO, IDataScienceSettings, IMemento } from '../../client/common/types'; +import { createDeferred, sleep, waitForPromise } from '../../client/common/utils/async'; +import { noop } from '../../client/common/utils/misc'; +import { EXTENSION_ROOT_DIR } from '../../client/constants'; +import { generateCellsFromDocument } from '../../client/datascience/cellFactory'; +import { EditorContexts } from '../../client/datascience/constants'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; +import { AskedForPerFileSettingKey } from '../../client/datascience/interactive-window/interactiveWindowProvider'; +import { IInteractiveWindowProvider } from '../../client/datascience/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { concatMultilineString } from '../../datascience-ui/common'; +import { InteractivePanel } from '../../datascience-ui/history-react/interactivePanel'; +import { IKeyboardEvent } from '../../datascience-ui/react-common/event'; +import { ImageButton } from '../../datascience-ui/react-common/imageButton'; +import { MonacoEditor } from '../../datascience-ui/react-common/monacoEditor'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { createDocument } from './editor-integration/helpers'; +import { defaultDataScienceSettings, takeSnapshot, writeDiffSnapshot } from './helpers'; +import { + addCode, + closeInteractiveWindow, + createCodeWatcher, + getInteractiveCellResults, + getOrCreateInteractiveWindow, + runCodeLens, + runTest +} from './interactiveWindowTestHelpers'; +import { MockDocumentManager } from './mockDocumentManager'; +import { MockEditor } from './mockTextEditor'; +import { addCell, createNewEditor } from './nativeEditorTestHelpers'; +import { + addContinuousMockData, + addInputMockData, + addMockData, + CellInputState, + CellPosition, + enterEditorKey, + enterInput, + escapePath, + findButton, + getInteractiveEditor, + getLastOutputCell, + srcDirectory, + submitInput, + toggleCellExpansion, + typeCode, + verifyHtmlOnCell, + verifyLastCellInputState +} from './testHelpers'; +import { ITestInteractiveWindowProvider } from './testInteractiveWindowProvider'; + +// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +suite('DataScience Interactive Window output tests', () => { + const disposables: Disposable[] = []; + let ioc: DataScienceIocContainer; + const defaultCellMarker = '# %%'; + let snapshot: any; + + suiteSetup(() => { + snapshot = takeSnapshot(); + }); + setup(async () => { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(); + return ioc.activate(); + }); + + suiteTeardown(() => { + writeDiffSnapshot(snapshot, 'Interactive Window'); + }); + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + await ioc.dispose(); + }); + + async function forceSettingsChange(newSettings: IDataScienceSettings) { + const { mount } = await getOrCreateInteractiveWindow(ioc); + const update = mount.waitForMessage(InteractiveWindowMessages.SettingsUpdated); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath, newSettings); + return update; + } + + function simulateKeyPressOnEditor( + editorControl: ReactWrapper<any, Readonly<{}>, React.Component> | undefined, + keyboardEvent: Partial<IKeyboardEvent> & { code: string } + ) { + enterEditorKey(editorControl, keyboardEvent); + } + + function verifyHtmlOnInteractiveCell(html: string | undefined | RegExp, cellIndex: number | CellPosition) { + const iw = ioc.getInteractiveWebPanel(undefined).wrapper; + iw.update(); + verifyHtmlOnCell(iw, 'InteractiveCell', html, cellIndex); + } + + // Uncomment this to debug hangs on exit + // suiteTeardown(() => { + // asyncDump(); + // }); + + runTest( + 'Simple text', + async () => { + await addCode(ioc, 'a=1\na'); + + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.Last); + }, + () => { + return ioc; + } + ); + + runTest( + 'Clear output', + async () => { + const text = `from IPython.display import clear_output +for i in range(10): + clear_output() + print("Hello World {0}!".format(i)) +`; + addContinuousMockData(ioc, text, async (_c) => { + return { + result: 'Hello World 9!', + haveMore: false + }; + }); + await addCode(ioc, text); + + verifyHtmlOnInteractiveCell('<div>Hello World 9!', CellPosition.Last); + }, + () => { + return ioc; + } + ); + + runTest( + 'Hide inputs', + async () => { + await forceSettingsChange({ ...defaultDataScienceSettings(), showCellInputCode: false }); + + await addCode(ioc, 'a=1\na'); + + verifyLastCellInputState(ioc.getWrapper('interactive'), 'InteractiveCell', CellInputState.Hidden); + + // Add a cell without output, this cell should not show up at all + addMockData(ioc, 'a=1', undefined, 'text/plain'); + await addCode(ioc, 'a=1'); + + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.First); + verifyHtmlOnInteractiveCell(undefined, CellPosition.Last); + }, + () => { + return ioc; + } + ); + + runTest( + 'Show inputs', + async () => { + await forceSettingsChange({ ...defaultDataScienceSettings() }); + + await addCode(ioc, 'a=1\na'); + + verifyLastCellInputState(ioc.getWrapper('interactive'), 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(ioc.getWrapper('interactive'), 'InteractiveCell', CellInputState.Collapsed); + }, + () => { + return ioc; + } + ); + + runTest( + 'Expand inputs', + async () => { + await forceSettingsChange({ ...defaultDataScienceSettings(), collapseCellInputCodeByDefault: false }); + await addCode(ioc, 'a=1\na'); + + verifyLastCellInputState(ioc.getWrapper('interactive'), 'InteractiveCell', CellInputState.Expanded); + }, + () => { + return ioc; + } + ); + + runTest( + 'Ctrl + 1/Ctrl + 2', + async () => { + // Create an interactive window so that it listens to the results. + const { mount } = await getOrCreateInteractiveWindow(ioc); + + // Type in the input box + const editor = getInteractiveEditor(mount.wrapper); + typeCode(editor, 'a=1\na'); + + // Give focus to a random div + const reactDiv = mount.wrapper.find('div').first().getDOMNode(); + + const domDiv = reactDiv.querySelector('div'); + + if (domDiv && mount.wrapper) { + domDiv.tabIndex = -1; + domDiv.focus(); + + // send the ctrl + 1/2 message, this should put focus back on the input box + mount.postMessage({ type: InteractiveWindowMessages.Activate, payload: undefined }); + + // Then enter press shift + enter on the active element + const activeElement = document.activeElement; + if (activeElement) { + await submitInput(mount, activeElement as HTMLTextAreaElement); + } + } + + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.Last); + }, + () => { + return ioc; + } + ); + + runTest( + 'Escape/Ctrl+U', + async () => { + // Create an interactive window so that it listens to the results. + const { mount } = await getOrCreateInteractiveWindow(ioc); + + // Type in the input box + const editor = getInteractiveEditor(mount.wrapper); + typeCode(editor, 'a=1\na'); + + // Check code is what we think it is + const reactEditor = editor.instance() as MonacoEditor; + assert.equal(reactEditor.state.model?.getValue().replace(/\r/g, ''), 'a=1\na'); + + // Send escape + simulateKeyPressOnEditor(editor, { code: 'Escape' }); + assert.equal(reactEditor.state.model?.getValue().replace(/\r/g, ''), ''); + + typeCode(editor, 'a=1\na'); + assert.equal(reactEditor.state.model?.getValue().replace(/\r/g, ''), 'a=1\na'); + + simulateKeyPressOnEditor(editor, { code: 'KeyU', ctrlKey: true }); + assert.equal(reactEditor.state.model?.getValue().replace(/\r/g, ''), ''); + }, + () => { + return ioc; + } + ); + + runTest( + 'Click outside cells sets focus to input box', + async () => { + // Create an interactive window so that it listens to the results. + const { mount } = await getOrCreateInteractiveWindow(ioc); + + // Type in the input box + const editor = getInteractiveEditor(mount.wrapper); + typeCode(editor, 'a=1\na'); + + // Give focus to a random div + const reactDiv = mount.wrapper.find('div').first().getDOMNode(); + + const domDiv = reactDiv.querySelector('div'); + + if (domDiv) { + domDiv.tabIndex = -1; + domDiv.focus(); + + mount.wrapper.find('section#main-panel-footer').first().simulate('click'); + + // Then enter press shift + enter on the active element + const activeElement = document.activeElement; + if (activeElement) { + await submitInput(mount, activeElement as HTMLTextAreaElement); + } + } + + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.Last); + }, + () => { + return ioc; + } + ); + + runTest( + 'Collapse / expand cell', + async () => { + await forceSettingsChange({ ...defaultDataScienceSettings() }); + await addCode(ioc, 'a=1\na'); + const wrapper = ioc.getWrapper('interactive'); + + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Collapsed); + + toggleCellExpansion(wrapper, 'InteractiveCell'); + + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Expanded); + + toggleCellExpansion(wrapper, 'InteractiveCell'); + + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Collapsed); + }, + () => { + return ioc; + } + ); + + runTest( + 'Hide / show cell', + async () => { + await forceSettingsChange({ ...defaultDataScienceSettings() }); + await addCode(ioc, 'a=1\na'); + + const wrapper = ioc.getWrapper('interactive'); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Collapsed); + + // Hide the inputs and verify + await forceSettingsChange({ ...defaultDataScienceSettings(), showCellInputCode: false }); + + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Hidden); + + // Show the inputs and verify + await forceSettingsChange({ ...defaultDataScienceSettings(), showCellInputCode: true }); + + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Visible); + verifyLastCellInputState(wrapper, 'InteractiveCell', CellInputState.Collapsed); + }, + () => { + return ioc; + } + ); + + runTest( + 'Mime Types', + async () => { + const badPanda = `import pandas as pd +df = pd.read("${escapePath(path.join(srcDirectory(), 'DefaultSalesReport.csv'))}") +df.head()`; + const goodPanda = `import pandas as pd +df = pd.read_csv("${escapePath(path.join(srcDirectory(), 'DefaultSalesReport.csv'))}") +df.head()`; + const matPlotLib = + 'import matplotlib.pyplot as plt\r\nimport numpy as np\r\nx = np.linspace(0,20,100)\r\nplt.plot(x, np.sin(x))\r\nplt.show()'; + const matPlotLibResults = 'img'; + const spinningCursor = `import sys +import time + +def spinning_cursor(): + while True: + for cursor in '|/-\\\\': + yield cursor + +spinner = spinning_cursor() +for _ in range(50): + sys.stdout.write(next(spinner)) + sys.stdout.flush() + time.sleep(0.1) + sys.stdout.write('\\r')`; + + addMockData(ioc, badPanda, `pandas has no attribute 'read'`, 'text/html', 'error'); + addMockData(ioc, goodPanda, `<td>A table</td>`, 'text/html'); + addMockData(ioc, matPlotLib, matPlotLibResults, 'text/html'); + const cursors = ['|', '/', '-', '\\']; + let cursorPos = 0; + let loops = 3; + addContinuousMockData(ioc, spinningCursor, async (_c) => { + const result = `${cursors[cursorPos]}\r`; + cursorPos += 1; + if (cursorPos >= cursors.length) { + cursorPos = 0; + loops -= 1; + } + return Promise.resolve({ result: result, haveMore: loops > 0 }); + }); + + await addCode(ioc, badPanda, true); + verifyHtmlOnInteractiveCell(`has no attribute 'read'`, CellPosition.Last); + + await addCode(ioc, goodPanda); + verifyHtmlOnInteractiveCell(`<td>`, CellPosition.Last); + + await addCode(ioc, matPlotLib); + verifyHtmlOnInteractiveCell(/img|Figure/, CellPosition.Last); + + await addCode(ioc, spinningCursor); + verifyHtmlOnInteractiveCell('<div>', CellPosition.Last); + + addContinuousMockData(ioc, 'len?', async (_c) => { + return Promise.resolve({ + result: `Signature: len(obj, /) +Docstring: Return the number of items in a container. +Type: builtin_function_or_method`, + haveMore: false + }); + }); + await addCode(ioc, 'len?'); + verifyHtmlOnInteractiveCell('len', CellPosition.Last); + }, + () => { + return ioc; + } + ); + + runTest( + 'Undo/redo commands', + async () => { + const { window } = await getOrCreateInteractiveWindow(ioc); + + // Get a cell into the list + await addCode(ioc, 'a=1\na'); + + // Now verify if we undo, we have no cells + let afterUndo = await getInteractiveCellResults( + ioc, + () => { + window.undoCells(); + return Promise.resolve(); + }, + window + ); + + assert.equal(afterUndo.length, 1, `Undo should remove cells + ${afterUndo.debug()}`); + + // Redo should put the cells back + const afterRedo = await getInteractiveCellResults( + ioc, + () => { + window.redoCells(); + return Promise.resolve(); + }, + window + ); + assert.equal(afterRedo.length, 2, 'Redo should put cells back'); + + // Get another cell into the list + const afterAdd = await addCode(ioc, 'a=1\na'); + assert.equal(afterAdd.length, 3, 'Second cell did not get added'); + + // Clear everything + const afterClear = await getInteractiveCellResults(ioc, () => { + window.removeAllCells(); + return Promise.resolve(); + }); + assert.equal(afterClear.length, 1, "Clear didn't work"); + + // Undo should put them back + afterUndo = await getInteractiveCellResults(ioc, () => { + window.undoCells(); + return Promise.resolve(); + }); + + assert.equal(afterUndo.length, 3, `Undo should put cells back`); + }, + () => { + return ioc; + } + ); + + runTest( + 'Click buttons', + async () => { + // Goto source should cause the visible editor to be picked as long as its filename matches + const showedEditor = createDeferred(); + const textEditors: TextEditor[] = []; + const docManager = TypeMoq.Mock.ofType<IDocumentManager>(); + const visibleEditor = TypeMoq.Mock.ofType<TextEditor>(); + const dummyDocument = TypeMoq.Mock.ofType<TextDocument>(); + dummyDocument.setup((d) => d.fileName).returns(() => Uri.file('foo.py').fsPath); + visibleEditor.setup((v) => v.show()).returns(() => showedEditor.resolve()); + visibleEditor.setup((v) => v.revealRange(TypeMoq.It.isAny())).returns(noop); + visibleEditor.setup((v) => v.document).returns(() => dummyDocument.object); + textEditors.push(visibleEditor.object); + docManager.setup((a) => a.visibleTextEditors).returns(() => textEditors); + ioc.serviceManager.rebindInstance<IDocumentManager>(IDocumentManager, docManager.object); + + // Get a cell into the list + await addCode(ioc, 'a=1\na'); + + // 'Click' the buttons in the react control + const undo = findButton(ioc.getWrapper('interactive'), InteractivePanel, 2); + const redo = findButton(ioc.getWrapper('interactive'), InteractivePanel, 1); + const clear = findButton(ioc.getWrapper('interactive'), InteractivePanel, 0); + + // Now verify if we undo, we have no cells + let afterUndo = await getInteractiveCellResults(ioc, () => { + undo!.simulate('click'); + return Promise.resolve(); + }); + + assert.equal(afterUndo.length, 1, `Undo should remove cells`); + + // Redo should put the cells back + const afterRedo = await getInteractiveCellResults(ioc, async () => { + redo!.simulate('click'); + return Promise.resolve(); + }); + assert.equal(afterRedo.length, 2, 'Redo should put cells back'); + + // Get another cell into the list + const afterAdd = await addCode(ioc, 'a=1\na'); + assert.equal(afterAdd.length, 3, 'Second cell did not get added'); + + // Clear everything + const afterClear = await getInteractiveCellResults(ioc, async () => { + clear!.simulate('click'); + return Promise.resolve(); + }); + assert.equal(afterClear.length, 1, "Clear didn't work"); + + // Undo should put them back + afterUndo = await getInteractiveCellResults(ioc, async () => { + undo!.simulate('click'); + return Promise.resolve(); + }); + + assert.equal(afterUndo.length, 3, `Undo should put cells back`); + + // find the buttons on the cell itself + const ImageButtons = afterUndo.at(afterUndo.length - 2).find(ImageButton); + assert.equal(ImageButtons.length, 4, 'Cell buttons not found'); + + const goto = ImageButtons.at(1); + const deleteButton = ImageButtons.at(3); + + // Make sure goto works + goto.simulate('click'); + await waitForPromise(showedEditor.promise, 1000); + assert.ok(showedEditor.resolved, 'Goto source is not jumping to editor'); + + // Make sure delete works + const afterDelete = await getInteractiveCellResults(ioc, async () => { + deleteButton.simulate('click'); + return Promise.resolve(); + }); + assert.equal(afterDelete.length, 2, `Delete should remove a cell`); + }, + () => { + return ioc; + } + ); + + const interruptCode = ` +import time +for i in range(0, 100): + try: + time.sleep(0.5) + except KeyboardInterrupt: + time.sleep(0.5)`; + + runTest( + 'Interrupt double', + async (context: Mocha.Context) => { + if (ioc.mockJupyter) { + let interruptedKernel = false; + const { window, mount } = await getOrCreateInteractiveWindow(ioc); + window.notebook?.onKernelInterrupted(() => (interruptedKernel = true)); + + let timerCount = 0; + addContinuousMockData(ioc, interruptCode, async (_c) => { + timerCount += 1; + await sleep(0.5); + return Promise.resolve({ result: '', haveMore: timerCount < 100 }); + }); + + addMockData(ioc, interruptCode, undefined, 'text/plain'); + + // Run the interrupt code and then interrupt it twice to make sure we can interrupt twice + const waitForAdd = addCode(ioc, interruptCode); + + // 'Click' the button in the react control. We need to verify we can + // click it more than once. + const interrupt = findButton(mount.wrapper, InteractivePanel, 4); + interrupt?.simulate('click'); + await sleep(0.1); + interrupt?.simulate('click'); + + // We should get out of the wait for add + await waitForAdd; + + // We should have also fired an interrupt + assert.ok(interruptedKernel, 'Kernel was not interrupted'); + } else { + // Timing is too iffy for real jupyter. However we really just + // want to make sure double interrupt is supported so keep the test. + context.skip(); + } + }, + () => { + return ioc; + } + ); + + runTest( + 'Export', + async () => { + // Export should cause the export dialog to come up. Remap appshell so we can check + const dummyDisposable = { + dispose: () => { + return; + } + }; + let exportCalled = false; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())) + .returns((e) => { + throw e; + }); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve('')); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => { + exportCalled = true; + return Promise.resolve(undefined); + }); + appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); + + // Make sure to create the interactive window after the rebind or it gets the wrong application shell. + await addCode(ioc, 'a=1\na'); + const { window, mount } = await getOrCreateInteractiveWindow(ioc); + + // Export should cause exportCalled to change to true + const exportPromise = mount.waitForMessage(InteractiveWindowMessages.ReturnAllCells); + window.exportCells(); + await exportPromise; + await sleep(100); // Give time for appshell to come up + assert.equal(exportCalled, true, 'Export is not being called during export'); + + // Remove the cell + const exportButton = findButton(mount.wrapper, InteractivePanel, 6); + const undo = findButton(mount.wrapper, InteractivePanel, 2); + + // Now verify if we undo, we have no cells + const afterUndo = await getInteractiveCellResults(ioc, () => { + undo!.simulate('click'); + return Promise.resolve(); + }); + + assert.equal(afterUndo.length, 1, 'Undo should remove cells'); + + // Then verify we cannot click the button (it should be disabled) + exportCalled = false; + exportButton!.simulate('click'); + await sleep(100); + assert.equal(exportCalled, false, 'Export should not be called when no cells visible'); + }, + () => { + return ioc; + } + ); + + runTest( + 'Multiple Interpreters', + async (context) => { + if (!ioc.mockJupyter) { + const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); + const interpreterService = ioc.get<IInterpreterService>(IInterpreterService); + const interpreters = await ioc.getFunctionalTestInterpreters(); + if (interpreters.length < 2) { + // tslint:disable-next-line: no-console + console.log( + 'Multiple interpreters skipped because local machine does not have more than one jupyter environment' + ); + context.skip(); + return; + } + const window = (await interactiveWindowProvider.getOrCreate(undefined)) as InteractiveWindow; + await addCode(ioc, 'a=1\na'); + const activeInterpreter = await interpreterService.getActiveInterpreter(window.owningResource); + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.Last); + assert.equal( + window.notebook!.getMatchingInterpreter()?.path, + activeInterpreter?.path, + 'Active intrepreter not used to launch notebook' + ); + await closeInteractiveWindow(ioc, window); + + // Add another python path + const secondUri = Uri.file('bar.py'); + ioc.addResourceToFolder(secondUri, path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience2')); + ioc.forceSettingsChanged( + secondUri, + interpreters.filter((i) => i.path !== activeInterpreter?.path)[0].path + ); + + // Then open a second time and make sure it uses this new path + const secondPath = await interpreterService.getActiveInterpreter(secondUri); + assert.notEqual(secondPath?.path, activeInterpreter?.path, 'Second path was not set'); + const newWindow = (await interactiveWindowProvider.getOrCreate(secondUri)) as InteractiveWindow; + await addCode(ioc, 'a=1\na', false, secondUri); + assert.notEqual( + newWindow.notebook!.getMatchingInterpreter()?.path, + activeInterpreter?.path, + 'Active intrepreter used to launch second notebook when it should not have' + ); + verifyHtmlOnCell(ioc.getWrapper('interactive'), 'InteractiveCell', '<span>1</span>', CellPosition.Last); + } else { + context.skip(); + } + }, + () => { + return ioc; + } + ); + + runTest( + 'Dispose test', + async () => { + // tslint:disable-next-line:no-any + const { window } = await getOrCreateInteractiveWindow(ioc); + await window.dispose(); + // tslint:disable-next-line:no-any + const h2 = await getOrCreateInteractiveWindow(ioc); + // Check equal and then dispose so the test goes away + const equal = Object.is(window, h2.window); + assert.ok(!equal, 'Disposing is not removing the active interactive window'); + }, + () => { + return ioc; + } + ); + + runTest( + 'Editor Context', + async () => { + // Before we have any cells, verify our contexts are not set + assert.equal( + ioc.getContext(EditorContexts.HaveInteractive), + false, + 'Should not have interactive before starting' + ); + assert.equal( + ioc.getContext(EditorContexts.HaveInteractiveCells), + false, + 'Should not have interactive cells before starting' + ); + assert.equal( + ioc.getContext(EditorContexts.HaveRedoableCells), + false, + 'Should not have redoable before starting' + ); + + // Verify we can send different commands to the UI and it will respond + const { window, mount } = await getOrCreateInteractiveWindow(ioc); + + // Get an update promise so we can wait for the add code + const updatePromise = mount.waitForMessage(InteractiveWindowMessages.ExecutionRendered); + + // Send some code to the interactive window + await window.addCode('a=1\na', Uri.file('foo.py'), 2); + + // Wait for the render to go through + await updatePromise; + + // Now we should have the 3 editor contexts + assert.equal( + ioc.getContext(EditorContexts.HaveInteractive), + true, + 'Should have interactive after starting' + ); + assert.equal( + ioc.getContext(EditorContexts.HaveInteractiveCells), + true, + 'Should have interactive cells after starting' + ); + assert.equal( + ioc.getContext(EditorContexts.HaveRedoableCells), + false, + 'Should not have redoable after starting' + ); + + // Setup a listener for context change events. We have 3 separate contexts, so we have to wait for all 3. + let count = 0; + let deferred = createDeferred<boolean>(); + const eventDispose = ioc.onContextSet((_a) => { + count += 1; + if (count >= 3) { + deferred.resolve(); + } + }); + disposables.push(eventDispose); + + // Create a method that resets the waiting + const resetWaiting = () => { + count = 0; + deferred = createDeferred<boolean>(); + }; + + // Now send an undo command. This should change the state, so use our waitForInfo promise instead + resetWaiting(); + window.undoCells(); + await waitForPromise(deferred.promise, 2000); + assert.ok(deferred.resolved, 'Never got update to state'); + assert.equal( + ioc.getContext(EditorContexts.HaveInteractiveCells), + false, + 'Should not have interactive cells after undo as sysinfo is ignored' + ); + assert.equal(ioc.getContext(EditorContexts.HaveRedoableCells), true, 'Should have redoable after undo'); + + resetWaiting(); + window.redoCells(); + await waitForPromise(deferred.promise, 2000); + assert.ok(deferred.resolved, 'Never got update to state'); + assert.equal( + ioc.getContext(EditorContexts.HaveInteractiveCells), + true, + 'Should have interactive cells after redo' + ); + assert.equal( + ioc.getContext(EditorContexts.HaveRedoableCells), + false, + 'Should not have redoable after redo' + ); + + resetWaiting(); + window.removeAllCells(); + await waitForPromise(deferred.promise, 2000); + assert.ok(deferred.resolved, 'Never got update to state'); + assert.equal( + ioc.getContext(EditorContexts.HaveInteractiveCells), + false, + 'Should not have interactive cells after delete' + ); + }, + () => { + return ioc; + } + ); + + runTest( + 'Simple input', + async () => { + // Create an interactive window so that it listens to the results. + const { mount } = await getOrCreateInteractiveWindow(ioc); + + // Then enter some code. + await enterInput(mount, 'a=1\na', 'InteractiveCell'); + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.Last); + }, + () => { + return ioc; + } + ); + + runTest( + 'Copy to source input', + async () => { + const showedEditor = createDeferred(); + ioc.addDocument('# No cells here', 'foo.py'); + const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + const editor = (await docManager.showTextDocument(docManager.textDocuments[0])) as MockEditor; + editor.setRevealCallback(() => showedEditor.resolve()); + + // Create an interactive window so that it listens to the results. + const { mount } = await getOrCreateInteractiveWindow(ioc); + + // Then enter some code. + await enterInput(mount, 'a=1\na', 'InteractiveCell'); + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.Last); + const ImageButtons = getLastOutputCell(mount.wrapper, 'InteractiveCell').find(ImageButton); + assert.equal(ImageButtons.length, 4, 'Cell buttons not found'); + const copyToSource = ImageButtons.at(2); + + // Then click the copy to source button + copyToSource.simulate('click'); + await waitForPromise(showedEditor.promise, 100); + assert.ok(showedEditor.resolved, 'Copy to source is not adding code to the editor'); + }, + () => { + return ioc; + } + ); + + runTest( + 'Multiple input', + async () => { + // Create an interactive window so that it listens to the results. + const { mount } = await getOrCreateInteractiveWindow(ioc); + + // Then enter some code. + await enterInput(mount, 'a=1\na', 'InteractiveCell'); + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.Last); + + // Then delete the node + const lastCell = getLastOutputCell(mount.wrapper, 'InteractiveCell'); + const ImageButtons = lastCell.find(ImageButton); + assert.equal(ImageButtons.length, 4, 'Cell buttons not found'); + const deleteButton = ImageButtons.at(3); + + // Make sure delete works + const afterDelete = await getInteractiveCellResults(ioc, async () => { + deleteButton.simulate('click'); + return Promise.resolve(); + }); + assert.equal(afterDelete.length, 1, `Delete should remove a cell`); + + // Should be able to enter again + await enterInput(mount, 'a=1\na', 'InteractiveCell'); + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.Last); + + // Try a 3rd time with some new input + addMockData(ioc, 'print("hello")', 'hello'); + await enterInput(mount, 'print("hello', 'InteractiveCell'); + verifyHtmlOnInteractiveCell('hello', CellPosition.Last); + + // Verify auto indent is working + const editor = getInteractiveEditor(mount.wrapper); + typeCode(editor, 'if (True):\n'); + typeCode(editor, 'print("true")'); + const reactEditor = editor.instance() as MonacoEditor; + assert.equal(reactEditor.state.model?.getValue().replace(/\r/g, ''), `if (True):\n print("true")`); + }, + () => { + return ioc; + } + ); + + runTest( + 'Restart with session failure', + async () => { + // Prime the pump + await addCode(ioc, 'a=1\na'); + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.Last); + + // Then something that could possibly timeout + addContinuousMockData(ioc, 'import time\r\ntime.sleep(1000)', (_c) => { + return Promise.resolve({ result: '', haveMore: true }); + }); + + // Then get our mock session and force it to not restart ever. + if (ioc.mockJupyter) { + const currentSession = ioc.mockJupyter.getCurrentSession(); + if (currentSession) { + currentSession.prolongRestarts(); + } + } + + // Then try executing our long running cell and restarting in the middle + const { window } = await getOrCreateInteractiveWindow(ioc); + const executed = createDeferred(); + // We have to wait until the execute goes through before we reset. + window.onExecutedCode(() => executed.resolve()); + const added = window.addCode('import time\r\ntime.sleep(1000)', Uri.file('foo'), 0); + await executed.promise; + await window.restartKernel(); + await added; + + // Now see if our wrapper still works. Interactive window should have forced a restart + await window.addCode('a=1\na', Uri.file('foo'), 0); + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.Last); + }, + () => { + return ioc; + } + ); + + runTest( + 'LiveLossPlot', + async () => { + // Only run this test when not mocking. Too complicated to mimic otherwise + if (!ioc.mockJupyter) { + // Load all of our cells + const testFile = path.join(srcDirectory(), 'liveloss.py'); + const version = 1; + const inputText = await fs.readFile(testFile, 'utf-8'); + const document = createDocument(inputText, testFile, version, TypeMoq.Times.atLeastOnce(), true); + const cells = generateCellsFromDocument(document.object); + assert.ok(cells, 'No cells generated'); + assert.equal(cells.length, 2, 'Not enough cells generated'); + + // Run the first cell + await addCode(ioc, concatMultilineString(cells[0].data.source)); + + // Last cell should generate a series of updates. Verify we end up with a single image + await addCode(ioc, concatMultilineString(cells[1].data.source)); + const cell = getLastOutputCell(ioc.getInteractiveWebPanel(undefined).wrapper, 'InteractiveCell'); + + const output = cell!.find('div.cell-output'); + assert.ok(output.length > 0, 'No output cell found'); + const outHtml = output.html(); + + const root = parse(outHtml) as any; + const png = root.querySelectorAll('img') as HTMLElement[]; + assert.ok(png, 'No pngs found'); + assert.equal(png.length, 1, 'Wrong number of pngs'); + } + }, + () => { + return ioc; + } + ); + + runTest( + 'Gather code run from text editor', + async () => { + ioc.getSettings().datascience.gatherToScript = true; + // Enter some code. + const code = `${defaultCellMarker}\na=1\na`; + await addCode(ioc, code); + addMockData(ioc, code, undefined); + const mount = ioc.getInteractiveWebPanel(undefined); + const ImageButtons = getLastOutputCell(mount.wrapper, 'InteractiveCell').find(ImageButton); // This isn't rendering correctly + assert.equal(ImageButtons.length, 4, 'Cell buttons not found'); + const gatherCode = ImageButtons.at(0); + + // Then click the gather code button + const gatherPromise = mount.waitForMessage(InteractiveWindowMessages.GatherCodeToScript); + gatherCode.simulate('click'); + await gatherPromise; + const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + assert.notEqual(docManager.activeTextEditor, undefined); + if (docManager.activeTextEditor) { + assert.notEqual( + docManager.activeTextEditor.document + .getText() + .trim() + .search('# This file was generated by the Gather Extension'), + -1 + ); + + // Basic unit test does not need to have Gather available in the build. + assert.notEqual( + docManager.activeTextEditor.document.getText().trim().search('Gather internal error'), + -1 + ); + } + }, + () => { + return ioc; + } + ); + + runTest( + 'Gather code run from input box', + async () => { + ioc.getSettings().datascience.gatherToScript = true; + // Create an interactive window so that it listens to the results. + const { mount } = await getOrCreateInteractiveWindow(ioc); + + // Then enter some code. + await enterInput(mount, 'a=1\na', 'InteractiveCell'); + verifyHtmlOnInteractiveCell('<span>1</span>', CellPosition.Last); + const ImageButtons = getLastOutputCell(mount.wrapper, 'InteractiveCell').find(ImageButton); + assert.equal(ImageButtons.length, 4, 'Cell buttons not found'); + const gatherCode = ImageButtons.at(0); + + // Then click the gather code button + const gatherPromise = mount.waitForMessage(InteractiveWindowMessages.GatherCodeToScript); + gatherCode.simulate('click'); + await gatherPromise; + const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + assert.notEqual(docManager.activeTextEditor, undefined); + + if (docManager.activeTextEditor) { + // Just check key parts of the document, not the whole thing. + assert.notEqual( + docManager.activeTextEditor.document + .getText() + .trim() + .search('# This file was generated by the Gather Extension'), + -1 + ); + + // Basic unit test does not need to have Gather available in the build. + assert.notEqual( + docManager.activeTextEditor.document.getText().trim().search('Gather internal error'), + -1 + ); + } + }, + () => { + return ioc; + } + ); + + runTest( + 'Copy back to source', + async (_wrapper) => { + ioc.addDocument(`${defaultCellMarker}${os.EOL}print("bar")`, 'foo.py'); + const docManager = ioc.get<IDocumentManager>(IDocumentManager); + docManager.showTextDocument(docManager.textDocuments[0]); + const { window } = await getOrCreateInteractiveWindow(ioc); + const interactiveWindow = window as InteractiveWindow; + await interactiveWindow.copyCode({ source: 'print("baz")' }); + assert.equal( + docManager.textDocuments[0].getText(), + `${defaultCellMarker}${os.EOL}print("baz")${os.EOL}${defaultCellMarker}${os.EOL}print("bar")`, + 'Text not inserted' + ); + const activeEditor = docManager.activeTextEditor as MockEditor; + activeEditor.selection = new Selection(1, 2, 1, 2); + await interactiveWindow.copyCode({ source: 'print("baz")' }); + assert.equal( + docManager.textDocuments[0].getText(), + `${defaultCellMarker}${os.EOL}${defaultCellMarker}${os.EOL}print("baz")${os.EOL}${defaultCellMarker}${os.EOL}print("baz")${os.EOL}${defaultCellMarker}${os.EOL}print("bar")`, + 'Text not inserted' + ); + }, + () => { + return ioc; + } + ); + + runTest( + 'Limit text output', + async () => { + ioc.getSettings().datascience.textOutputLimit = 8; + + // Output should be trimmed to just two lines of output + const code = `print("hello\\nworld\\nhow\\nare\\nyou")`; + addMockData(ioc, code, 'are\nyou\n'); + await addCode(ioc, code); + + verifyHtmlOnInteractiveCell('>are\nyou', CellPosition.Last); + }, + () => { + return ioc; + } + ); + + runTest( + 'Type in input', + async () => { + when(ioc.applicationShell.showInputBox(anything())).thenReturn(Promise.resolve('typed input')); + // Send in some special input + const code = `b = input('Test')\nb`; + addInputMockData(ioc, code, 'typed input'); + await addCode(ioc, code); + + verifyHtmlOnInteractiveCell('typed input', CellPosition.Last); + }, + () => { + return ioc; + } + ); + runTest( + 'Update display data', + async (context) => { + if (ioc.mockJupyter) { + context.skip(); + } else { + // Create 3 cells. Last cell should update the second + await addCode(ioc, 'dh = display(display_id=True)'); + await addCode(ioc, 'dh.display("Hello")'); + verifyHtmlOnInteractiveCell('Hello', CellPosition.Last); + await addCode(ioc, 'dh.update("Goodbye")'); + verifyHtmlOnInteractiveCell('<div></div>', CellPosition.Last); + verifyHtmlOnInteractiveCell('Goodbye', 1); + } + }, + () => { + return ioc; + } + ); + + test('Open notebook and interactive at the same time', async () => { + addMockData(ioc, 'a=1\na', 1, 'text/plain'); + addMockData(ioc, 'b=2\nb', 2, 'text/plain'); + + // Mount two different webviews + const ne = await createNewEditor(ioc); + let iw = await getOrCreateInteractiveWindow(ioc); + + // Run code in both + await addCode(ioc, 'a=1\na'); + await addCell(ne.mount, 'a=1\na', true); + + // Make sure both are correct + verifyHtmlOnCell(iw.mount.wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', '<span>1</span>', CellPosition.Last); + + // Close the interactive editor. + await closeInteractiveWindow(ioc, iw.window); + + // Run another cell and make sure it works in the notebook + await addCell(ne.mount, 'b=2\nb', true); + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', '<span>2</span>', CellPosition.Last); + + // Rerun the interactive window + iw = await getOrCreateInteractiveWindow(ioc); + await addCode(ioc, 'a=1\na'); + + verifyHtmlOnCell(iw.mount.wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + }); + test('Multiple interactive windows', async () => { + ioc.forceDataScienceSettingsChanged({ interactiveWindowMode: 'multiple' }); + const pair1 = await getOrCreateInteractiveWindow(ioc); + const pair2 = await getOrCreateInteractiveWindow(ioc); + assert.notEqual(pair1.window.title, pair2.window.title, 'Two windows were not created.'); + assert.notEqual(pair1.mount.wrapper, pair2.mount.wrapper, 'Two windows were not created.'); + }); + const fooCode = `x = 'foo'\nx`; + const barCode = `y = 'bar'\ny`; + test('Multiple executes go to last active window', async () => { + addMockData(ioc, fooCode, 'foo'); + addMockData(ioc, barCode, 'bar'); + + ioc.forceDataScienceSettingsChanged({ interactiveWindowMode: 'multiple' }); + const globalMemento = ioc.get<Memento>(IMemento, GLOBAL_MEMENTO); + await globalMemento.update(AskedForPerFileSettingKey, true); + + const pair1 = await getOrCreateInteractiveWindow(ioc); + + // Run a cell from a document + const fooWatcher = createCodeWatcher(`# %%\n${fooCode}`, 'foo.py', ioc); + const lenses = fooWatcher?.getCodeLenses(); + assert.equal(lenses?.length, 6, 'No code lenses found'); + await runCodeLens(fooWatcher!.uri!, lenses ? lenses[0] : undefined, ioc); + verifyHtmlOnCell(pair1.mount.wrapper, 'InteractiveCell', 'foo', CellPosition.Last); + + // Create another window, run a cell again + const pair2 = await getOrCreateInteractiveWindow(ioc); + await runCodeLens(fooWatcher!.uri!, lenses ? lenses[0] : undefined, ioc); + verifyHtmlOnCell(pair2.mount.wrapper, 'InteractiveCell', 'foo', CellPosition.Last); + + // Make the first window active + pair2.mount.changeViewState(false, false); + pair1.mount.changeViewState(true, true); + + // Run another file + const barWatcher = createCodeWatcher(`# %%\n${barCode}`, 'bar.py', ioc); + const lenses2 = barWatcher?.getCodeLenses(); + assert.equal(lenses2?.length, 6, 'No code lenses found'); + await runCodeLens(barWatcher!.uri!, lenses2 ? lenses2[0] : undefined, ioc); + verifyHtmlOnCell(pair1.mount.wrapper, 'InteractiveCell', 'bar', CellPosition.Last); + }); + test('Per file', async () => { + addMockData(ioc, fooCode, 'foo'); + addMockData(ioc, barCode, 'bar'); + ioc.forceDataScienceSettingsChanged({ interactiveWindowMode: 'perFile' }); + const interactiveWindowProvider = ioc.get<ITestInteractiveWindowProvider>(IInteractiveWindowProvider); + + // Run a cell from a document + const fooWatcher = createCodeWatcher(`# %%\n${fooCode}`, 'foo.py', ioc); + const lenses = fooWatcher?.getCodeLenses(); + assert.equal(lenses?.length, 6, 'No code lenses found'); + await runCodeLens(fooWatcher!.uri!, lenses ? lenses[0] : undefined, ioc); + assert.equal(interactiveWindowProvider.windows.length, 1, 'Interactive window not created'); + const mounted1 = interactiveWindowProvider.getMountedWebView(interactiveWindowProvider.windows[0]); + verifyHtmlOnCell(mounted1.wrapper, 'InteractiveCell', 'foo', CellPosition.Last); + + // Create another window, run a cell again + const barWatcher = createCodeWatcher(`# %%\n${barCode}`, 'bar.py', ioc); + const lenses2 = barWatcher?.getCodeLenses(); + await runCodeLens(barWatcher!.uri!, lenses2 ? lenses2[0] : undefined, ioc); + assert.equal(interactiveWindowProvider.windows.length, 2, 'Interactive window not created'); + const mounted2 = interactiveWindowProvider.getMountedWebView( + interactiveWindowProvider.windows.find((w) => w.title.includes('bar')) + ); + verifyHtmlOnCell(mounted2.wrapper, 'InteractiveCell', 'bar', CellPosition.Last); + }); + test('Per file asks and changes titles', async () => { + addMockData(ioc, fooCode, 'foo'); + addMockData(ioc, barCode, 'bar'); + ioc.forceDataScienceSettingsChanged({ interactiveWindowMode: 'multiple' }); + const interactiveWindowProvider = ioc.get<ITestInteractiveWindowProvider>(IInteractiveWindowProvider); + const globalMemento = ioc.get<Memento>(IMemento, GLOBAL_MEMENTO); + await globalMemento.update(AskedForPerFileSettingKey, false); + + // Run a cell from a document + const fooWatcher = createCodeWatcher(`# %%\n${fooCode}`, 'foo.py', ioc); + const lenses = fooWatcher?.getCodeLenses(); + assert.equal(lenses?.length, 6, 'No code lenses found'); + await runCodeLens(fooWatcher!.uri!, lenses ? lenses[0] : undefined, ioc); + assert.equal(interactiveWindowProvider.windows.length, 1, 'Interactive window not created'); + const mounted1 = interactiveWindowProvider.getMountedWebView(interactiveWindowProvider.windows[0]); + verifyHtmlOnCell(mounted1.wrapper, 'InteractiveCell', 'foo', CellPosition.Last); + + // Create another window, run a cell again + const barWatcher = createCodeWatcher(`# %%\n${barCode}`, 'bar.py', ioc); + const lenses2 = barWatcher?.getCodeLenses(); + await runCodeLens(barWatcher!.uri!, lenses2 ? lenses2[0] : undefined, ioc); + assert.equal(interactiveWindowProvider.windows.length, 2, 'Interactive window not created'); + const mounted2 = interactiveWindowProvider.getMountedWebView( + interactiveWindowProvider.windows.find((w) => w.title.includes('bar')) + ); + verifyHtmlOnCell(mounted2.wrapper, 'InteractiveCell', 'bar', CellPosition.Last); + + // First window should now have foo in the title too + assert.ok(interactiveWindowProvider.windows[0].title.includes('foo'), 'Title of first window did not change'); + }); +}); diff --git a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts new file mode 100644 index 000000000000..377a0bb0300b --- /dev/null +++ b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts @@ -0,0 +1,330 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; +import { assert } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Matcher } from 'ts-mockito/lib/matcher/type/Matcher'; +import * as TypeMoq from 'typemoq'; +import * as uuid from 'uuid/v4'; +import { EventEmitter, Uri } from 'vscode'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../client/common/application/types'; +import { PythonSettings } from '../../client/common/configSettings'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { IConfigurationService, IDisposable } from '../../client/common/types'; +import * as localize from '../../client/common/utils/localize'; +import { generateCells } from '../../client/datascience/cellFactory'; +import { Commands } from '../../client/datascience/constants'; +import { DataScienceFileSystem } from '../../client/datascience/dataScienceFileSystem'; +import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/errorHandler'; +import { ExportFormat, IExportManager } from '../../client/datascience/export/types'; +import { NotebookProvider } from '../../client/datascience/interactive-common/notebookProvider'; +import { InteractiveWindowCommandListener } from '../../client/datascience/interactive-window/interactiveWindowCommandListener'; +import { InteractiveWindowProvider } from '../../client/datascience/interactive-window/interactiveWindowProvider'; +import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; +import { JupyterExporter } from '../../client/datascience/jupyter/jupyterExporter'; +import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter'; +import { NativeEditorProvider } from '../../client/datascience/notebookStorage/nativeEditorProvider'; +import { NotebookStorageProvider } from '../../client/datascience/notebookStorage/notebookStorageProvider'; +import { + IDataScienceFileSystem, + IInteractiveWindow, + IJupyterExecution, + INotebook, + INotebookEditorProvider, + INotebookServer +} from '../../client/datascience/types'; +import { InterpreterService } from '../../client/interpreter/interpreterService'; +import { ServiceContainer } from '../../client/ioc/container'; +import { KnownSearchPathsForInterpreters } from '../../client/pythonEnvironments/discovery/locators/services/KnownPathsService'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; +import { MockCommandManager } from './mockCommandManager'; +import { MockDocumentManager } from './mockDocumentManager'; +import { MockStatusProvider } from './mockStatusProvider'; + +// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length + +function createTypeMoq<T>(tag: string): TypeMoq.IMock<T> { + // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class + // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 + const result = TypeMoq.Mock.ofType<T>(); + (result as any).tag = tag; + result.setup((x: any) => x.then).returns(() => undefined); + return result; +} + +// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length +suite('Interactive window command listener', async () => { + const interpreterService = mock(InterpreterService); + const configService = mock(ConfigurationService); + const knownSearchPaths = mock(KnownSearchPathsForInterpreters); + const fileSystem = mock(DataScienceFileSystem); + const serviceContainer = mock(ServiceContainer); + const dummyEvent = new EventEmitter<void>(); + const pythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); + const disposableRegistry: IDisposable[] = []; + const interactiveWindowProvider = mock(InteractiveWindowProvider); + const dataScienceErrorHandler = mock(DataScienceErrorHandler); + const notebookImporter = mock(JupyterImporter); + const notebookExporter = mock(JupyterExporter); + let applicationShell: IApplicationShell; + let jupyterExecution: IJupyterExecution; + const interactiveWindow = createTypeMoq<IInteractiveWindow>('Interactive Window'); + const documentManager = new MockDocumentManager(); + const statusProvider = new MockStatusProvider(); + const commandManager = new MockCommandManager(); + const exportManager = mock<IExportManager>(); + const notebookStorageProvider = mock(NotebookStorageProvider); + let notebookEditorProvider: INotebookEditorProvider; + const server = createTypeMoq<INotebookServer>('jupyter server'); + let lastFileContents: any; + + teardown(() => { + documentManager.activeTextEditor = undefined; + lastFileContents = undefined; + }); + + class FunctionMatcher extends Matcher { + private func: (obj: any) => boolean; + constructor(func: (obj: any) => boolean) { + super(); + this.func = func; + } + public match(value: Object): boolean { + return this.func(value); + } + public toString(): string { + return 'FunctionMatcher'; + } + } + + function argThat(func: (obj: any) => boolean): any { + return new FunctionMatcher(func); + } + + function createCommandListener(): InteractiveWindowCommandListener { + notebookEditorProvider = mock(NativeEditorProvider); + jupyterExecution = mock(JupyterExecutionFactory); + applicationShell = mock(ApplicationShell); + + // Setup defaults + when(interpreterService.onDidChangeInterpreter).thenReturn(dummyEvent.event); + when(interpreterService.getInterpreterDetails(argThat((o) => !o.includes || !o.includes('python')))).thenReject( + ('Unknown interpreter' as any) as Error + ); + + // Service container needs logger, file system, and config service + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(configService)); + when(serviceContainer.get<IDataScienceFileSystem>(IDataScienceFileSystem)).thenReturn(instance(fileSystem)); + when(configService.getSettings(anything())).thenReturn(pythonSettings); + + // Setup default settings + pythonSettings.datascience = { + allowImportFromNotebook: true, + alwaysTrustNotebooks: true, + jupyterLaunchTimeout: 10, + jupyterLaunchRetries: 3, + enabled: true, + jupyterServerURI: '', + changeDirOnImportExport: false, + // tslint:disable-next-line: no-invalid-template-strings + notebookFileRoot: '${fileDirname}', + useDefaultConfigForJupyter: true, + jupyterInterruptTimeout: 10000, + searchForJupyter: true, + showCellInputCode: true, + collapseCellInputCodeByDefault: true, + allowInput: true, + maxOutputSize: 400, + enableScrollingForCellOutputs: true, + errorBackgroundColor: '#FFFFFF', + sendSelectionToInteractiveWindow: false, + variableExplorerExclude: 'module;function;builtin_function_or_method', + codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', + markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', + enablePlotViewer: true, + runStartupCommands: '', + debugJustMyCode: true, + variableQueries: [], + jupyterCommandLineArguments: [], + widgetScriptSources: [], + interactiveWindowMode: 'single' + }; + + when(knownSearchPaths.getSearchPaths()).thenReturn(['/foo/bar']); + + // We also need a file system + const tempFile = { + dispose: () => { + return undefined; + }, + filePath: '/foo/bar/baz.py' + }; + when(fileSystem.createTemporaryLocalFile(anything())).thenResolve(tempFile); + when(fileSystem.deleteLocalDirectory(anything())).thenResolve(); + when( + fileSystem.writeFile( + anything(), + argThat((o) => { + lastFileContents = o; + return true; + }) + ) + ).thenResolve(); + when(fileSystem.arePathsSame(anything(), anything())).thenReturn(true); + + when(interactiveWindowProvider.getOrCreate(anything())).thenResolve(interactiveWindow.object); + when(notebookImporter.importFromFile(anything())).thenResolve('imported'); + const metadata: nbformat.INotebookMetadata = { + language_info: { + name: 'python', + codemirror_mode: { + name: 'ipython', + version: 3 + } + }, + orig_nbformat: 2, + file_extension: '.py', + mimetype: 'text/x-python', + name: 'python', + npconvert_exporter: 'python', + pygments_lexer: `ipython${3}`, + version: 3 + }; + when(notebookExporter.translateToNotebook(anything())).thenResolve({ + cells: [], + nbformat: 4, + nbformat_minor: 2, + metadata: metadata + }); + + when(jupyterExecution.isNotebookSupported()).thenResolve(true); + + documentManager.addDocument('#%%\r\nprint("code")', 'bar.ipynb'); + + when(applicationShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve('moo')); + when(applicationShell.showInformationMessage(anything())).thenReturn(Promise.resolve('moo')); + + const notebookProvider = mock(NotebookProvider); + + const result = new InteractiveWindowCommandListener( + disposableRegistry, + instance(interactiveWindowProvider), + instance(notebookExporter), + instance(jupyterExecution), + instance(notebookProvider), + documentManager, + instance(applicationShell), + instance(fileSystem), + instance(configService), + statusProvider, + instance(dataScienceErrorHandler), + instance(notebookEditorProvider), + instance(exportManager), + instance(notebookStorageProvider) + ); + result.register(commandManager); + + return result; + } + + test('Import', async () => { + createCommandListener(); + when(applicationShell.showOpenDialog(argThat((o) => o.openLabel && o.openLabel.includes('Import')))).thenReturn( + Promise.resolve([Uri.file('foo')]) + ); + await commandManager.executeCommand(Commands.ImportNotebook, undefined, undefined); + verify(exportManager.export(ExportFormat.python, anything())).once(); + }); + test('Import File', async () => { + createCommandListener(); + await commandManager.executeCommand(Commands.ImportNotebook, Uri.file('bar.ipynb'), undefined); + verify(exportManager.export(ExportFormat.python, anything())).twice(); + }); + test('Export File', async () => { + createCommandListener(); + const doc = await documentManager.openTextDocument('bar.ipynb'); + await documentManager.showTextDocument(doc); + when(applicationShell.showSaveDialog(argThat((o) => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn( + Promise.resolve(Uri.file('foo')) + ); + when(applicationShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve('moo')); + when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn( + Promise.resolve('moo') + ); + when(jupyterExecution.isSpawnSupported()).thenResolve(true); + + await commandManager.executeCommand(Commands.ExportFileAsNotebook, Uri.file('bar.ipynb'), undefined); + + assert.ok(lastFileContents, 'Export file was not written to'); + verify( + applicationShell.showInformationMessage( + anything(), + localize.DataScience.exportOpenQuestion1(), + localize.DataScience.exportOpenQuestion() + ) + ).once(); + }); + test('Export File and output', async () => { + createCommandListener(); + const doc = await documentManager.openTextDocument('bar.ipynb'); + await documentManager.showTextDocument(doc); + when(jupyterExecution.connectToNotebookServer(anything(), anything())).thenResolve(server.object); + const notebook = createTypeMoq<INotebook>('jupyter notebook'); + server + .setup((s) => s.createNotebook(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(notebook.object)); + notebook + .setup((n) => + n.execute( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAnyNumber(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => { + return Promise.resolve(generateCells(undefined, 'a=1', 'bar.py', 0, false, uuid())); + }); + + when(applicationShell.showSaveDialog(argThat((o) => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn( + Promise.resolve(Uri.file('foo')) + ); + when(applicationShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve('moo')); + when(applicationShell.showInformationMessage(anything(), anything(), anything())).thenReturn( + Promise.resolve('moo') + ); + when(jupyterExecution.isSpawnSupported()).thenResolve(true); + + await commandManager.executeCommand(Commands.ExportFileAndOutputAsNotebook, Uri.file('bar.ipynb')); + + assert.ok(lastFileContents, 'Export file was not written to'); + verify( + applicationShell.showInformationMessage( + anything(), + localize.DataScience.exportOpenQuestion1(), + localize.DataScience.exportOpenQuestion() + ) + ).once(); + }); + test('Export skipped on no file', async () => { + createCommandListener(); + when(applicationShell.showSaveDialog(argThat((o) => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn( + Promise.resolve(Uri.file('foo')) + ); + await commandManager.executeCommand(Commands.ExportFileAndOutputAsNotebook, Uri.file('bar.ipynb')); + assert.notExists(lastFileContents, 'Export file was written to'); + }); + test('Export happens on no file', async () => { + createCommandListener(); + const doc = await documentManager.openTextDocument('bar.ipynb'); + await documentManager.showTextDocument(doc); + when(applicationShell.showSaveDialog(argThat((o) => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn( + Promise.resolve(Uri.file('foo')) + ); + await commandManager.executeCommand(Commands.ExportFileAsNotebook, undefined, undefined); + assert.ok(lastFileContents, 'Export file was not written to'); + }); +}); diff --git a/src/test/datascience/interactiveWindowTestHelpers.tsx b/src/test/datascience/interactiveWindowTestHelpers.tsx new file mode 100644 index 000000000000..cb472248a076 --- /dev/null +++ b/src/test/datascience/interactiveWindowTestHelpers.tsx @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; +import { ReactWrapper } from 'enzyme'; +import * as React from 'react'; +import { CodeLens, Uri } from 'vscode'; + +import { ICommandManager, IDocumentManager } from '../../client/common/application/types'; +import { Resource } from '../../client/common/types'; +import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; +import { + ICodeWatcher, + IDataScienceCodeLensProvider, + IInteractiveWindow, + IInteractiveWindowProvider, + IJupyterExecution +} from '../../client/datascience/types'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { MockDocumentManager } from './mockDocumentManager'; +import { IMountedWebView } from './mountedWebView'; +import { addMockData, getCellResults } from './testHelpers'; +import { TestInteractiveWindowProvider } from './testInteractiveWindowProvider'; + +export async function getInteractiveCellResults( + ioc: DataScienceIocContainer, + updater: () => Promise<void>, + window?: IInteractiveWindow | undefined +): Promise<ReactWrapper> { + const mountedWebView = ioc.get<TestInteractiveWindowProvider>(IInteractiveWindowProvider).getMountedWebView(window); + return getCellResults(mountedWebView, 'InteractiveCell', updater); +} + +export async function getOrCreateInteractiveWindow( + ioc: DataScienceIocContainer, + owner?: Resource +): Promise<{ window: IInteractiveWindow; mount: IMountedWebView }> { + const interactiveWindowProvider = ioc.get<TestInteractiveWindowProvider>(IInteractiveWindowProvider); + const window = (await interactiveWindowProvider.getOrCreate(owner)) as InteractiveWindow; + const mount = interactiveWindowProvider.getMountedWebView(window); + await window.show(); + return { window, mount }; +} + +export function createCodeWatcher( + docText: string, + docName: string, + ioc: DataScienceIocContainer +): ICodeWatcher | undefined { + const doc = ioc.addDocument(docText, docName); + const codeLensProvider = ioc.get<IDataScienceCodeLensProvider>(IDataScienceCodeLensProvider); + return codeLensProvider.getCodeWatcher(doc); +} + +export async function runCodeLens( + uri: Uri, + codeLens: CodeLens | undefined, + ioc: DataScienceIocContainer +): Promise<void> { + const documentManager = ioc.get<MockDocumentManager>(IDocumentManager); + await documentManager.showTextDocument(uri); + const commandManager = ioc.get<ICommandManager>(ICommandManager); + if (codeLens && codeLens.command) { + // tslint:disable-next-line: no-any + await commandManager.executeCommand(codeLens.command.command as any, ...codeLens.command.arguments); + } +} + +export function closeInteractiveWindow(ioc: DataScienceIocContainer, window: IInteractiveWindow) { + const promise = window.dispose(); + ioc.get<TestInteractiveWindowProvider>(IInteractiveWindowProvider).getMountedWebView(window).dispose(); + return promise; +} + +export function runTest( + name: string, + // tslint:disable-next-line:no-any + testFunc: (context: Mocha.Context) => Promise<void>, + getIOC: () => DataScienceIocContainer +) { + test(name, async function () { + const ioc = getIOC(); + const jupyterExecution = ioc.get<IJupyterExecution>(IJupyterExecution); + if (await jupyterExecution.isNotebookSupported()) { + addMockData(ioc, 'a=1\na', 1); + // tslint:disable-next-line: no-invalid-this + await testFunc(this); + } else { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); +} + +export async function addCode( + ioc: DataScienceIocContainer, + code: string, + expectError: boolean = false, + uri: Uri = Uri.file('foo.py') + // tslint:disable-next-line: no-any +): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { + const { window } = await getOrCreateInteractiveWindow(ioc); + return getInteractiveCellResults(ioc, async () => { + const success = await window.addCode(code, uri, 2); + if (expectError) { + assert.equal(success, false, `${code} did not produce an error`); + } + }); +} diff --git a/src/test/datascience/ipywidgets/cdnWidgetScriptSourceProvider.unit.test.ts b/src/test/datascience/ipywidgets/cdnWidgetScriptSourceProvider.unit.test.ts new file mode 100644 index 000000000000..cdf974021e46 --- /dev/null +++ b/src/test/datascience/ipywidgets/cdnWidgetScriptSourceProvider.unit.test.ts @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert } from 'chai'; +import * as fs from 'fs-extra'; +import { sha256 } from 'hash.js'; +import { shutdown } from 'log4js'; +import * as nock from 'nock'; +import * as path from 'path'; +import { Readable } from 'stream'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter, Uri } from 'vscode'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { HttpClient } from '../../../client/common/net/httpClient'; +import { IConfigurationService, IHttpClient, WidgetCDNs } from '../../../client/common/types'; +import { noop } from '../../../client/common/utils/misc'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { DataScienceFileSystem } from '../../../client/datascience/dataScienceFileSystem'; +import { CDNWidgetScriptSourceProvider } from '../../../client/datascience/ipywidgets/cdnWidgetScriptSourceProvider'; +import { IPyWidgetScriptSource } from '../../../client/datascience/ipywidgets/ipyWidgetScriptSource'; +import { IWidgetScriptSourceProvider } from '../../../client/datascience/ipywidgets/types'; +import { JupyterNotebookBase } from '../../../client/datascience/jupyter/jupyterNotebook'; +import { + IDataScienceFileSystem, + IJupyterConnection, + ILocalResourceUriConverter, + INotebook +} from '../../../client/datascience/types'; + +// tslint:disable: no-var-requires no-require-imports max-func-body-length no-any match-default-export-name no-console +const request = require('request'); +const sanitize = require('sanitize-filename'); + +const unpgkUrl = 'https://unpkg.com/'; +const jsdelivrUrl = 'https://cdn.jsdelivr.net/npm/'; + +// tslint:disable: max-func-body-length no-any +suite('DataScience - ipywidget - CDN', () => { + let scriptSourceProvider: IWidgetScriptSourceProvider; + let notebook: INotebook; + let configService: IConfigurationService; + let httpClient: IHttpClient; + let settings: PythonSettings; + let fileSystem: IDataScienceFileSystem; + let webviewUriConverter: ILocalResourceUriConverter; + let tempFileCount = 0; + suiteSetup(function () { + // Nock seems to fail randomly on CI builds. See bug + // https://github.com/microsoft/vscode-python/issues/11442 + // tslint:disable-next-line: no-invalid-this + return this.skip(); + }); + setup(() => { + notebook = mock(JupyterNotebookBase); + configService = mock(ConfigurationService); + httpClient = mock(HttpClient); + fileSystem = mock(DataScienceFileSystem); + webviewUriConverter = mock(IPyWidgetScriptSource); + settings = { datascience: { widgetScriptSources: [] } } as any; + when(configService.getSettings(anything())).thenReturn(settings as any); + when(httpClient.downloadFile(anything())).thenCall(request); + when(fileSystem.localFileExists(anything())).thenCall((f) => fs.pathExists(f)); + + when(fileSystem.createTemporaryLocalFile(anything())).thenCall(createTemporaryFile); + when(fileSystem.createLocalWriteStream(anything())).thenCall((p) => fs.createWriteStream(p)); + when(fileSystem.createDirectory(anything())).thenCall((d) => fs.ensureDir(d)); + when(webviewUriConverter.rootScriptFolder).thenReturn( + Uri.file(path.join(EXTENSION_ROOT_DIR, 'tmp', 'scripts')) + ); + when(webviewUriConverter.asWebviewUri(anything())).thenCall((u) => u); + scriptSourceProvider = new CDNWidgetScriptSourceProvider( + instance(configService), + instance(httpClient), + instance(webviewUriConverter), + instance(fileSystem) + ); + }); + + shutdown(() => { + clearDiskCache(); + }); + + function createStreamFromString(str: string) { + const readable = new Readable(); + readable._read = noop; + readable.push(str); + readable.push(null); + return readable; + } + + function createTemporaryFile(ext: string) { + tempFileCount += 1; + + // Put temp files next to extension so we can clean them up later + const filePath = path.join( + EXTENSION_ROOT_DIR, + 'tmp', + 'tempfile_loc', + `tempfile_for_test${tempFileCount}.${ext}` + ); + fs.createFileSync(filePath); + return { + filePath, + dispose: () => { + fs.removeSync(filePath); + } + }; + } + + function generateScriptName(moduleName: string, moduleVersion: string) { + const hash = sanitize(sha256().update(`${moduleName}${moduleVersion}`).digest('hex')); + return Uri.file(path.join(EXTENSION_ROOT_DIR, 'tmp', 'scripts', hash, 'index.js')).toString(); + } + + function clearDiskCache() { + try { + fs.removeSync(path.join(EXTENSION_ROOT_DIR, 'tmp', 'scripts')); + fs.removeSync(path.join(EXTENSION_ROOT_DIR, 'tmp', 'tempfile_loc')); + } catch { + // Swallow any errors here + } + } + + [true, false].forEach((localLaunch) => { + suite(localLaunch ? 'Local Jupyter Server' : 'Remote Jupyter Server', () => { + setup(() => { + const connection: IJupyterConnection = { + type: 'jupyter', + baseUrl: '', + localProcExitCode: undefined, + valid: true, + displayName: '', + disconnected: new EventEmitter<number>().event, + dispose: noop, + hostName: '', + localLaunch, + token: '', + rootDirectory: '' + }; + when(notebook.connection).thenReturn(connection); + }); + test('Script source will be empty if CDN is not a configured source of widget scripts in settings', async () => { + const value = await scriptSourceProvider.getWidgetScriptSource('HelloWorld', '1'); + + assert.deepEqual(value, { moduleName: 'HelloWorld' }); + // Should not make any http calls. + verify(httpClient.exists(anything())).never(); + }); + function updateCDNSettings(...values: WidgetCDNs[]) { + settings.datascience.widgetScriptSources = values; + } + (['unpkg.com', 'jsdelivr.com'] as WidgetCDNs[]).forEach((cdn) => { + suite(cdn, () => { + const moduleName = 'HelloWorld'; + const moduleVersion = '1'; + let baseUrl = ''; + let scriptUri = ''; + setup(() => { + baseUrl = cdn === 'unpkg.com' ? unpgkUrl : jsdelivrUrl; + scriptUri = generateScriptName(moduleName, moduleVersion); + }); + teardown(() => { + clearDiskCache(); + scriptSourceProvider.dispose(); + nock.cleanAll(); + }); + test('Ensure widget script is downloaded once and cached', async () => { + updateCDNSettings(cdn); + let downloadCount = 0; + nock(baseUrl) + .log(console.log) + + .get(/.*/) + .reply(200, () => { + downloadCount += 1; + return createStreamFromString('foo'); + }); + + const value = await scriptSourceProvider.getWidgetScriptSource(moduleName, moduleVersion); + + assert.deepEqual(value, { + moduleName: 'HelloWorld', + scriptUri, + source: 'cdn' + }); + + const value2 = await scriptSourceProvider.getWidgetScriptSource(moduleName, moduleVersion); + + assert.deepEqual(value2, { + moduleName: 'HelloWorld', + scriptUri, + source: 'cdn' + }); + + assert.equal(downloadCount, 1, 'Downloaded more than once'); + }); + test('No script source if package does not exist on CDN', async () => { + updateCDNSettings(cdn); + nock(baseUrl).log(console.log).get(/.*/).replyWithError('404'); + + const value = await scriptSourceProvider.getWidgetScriptSource(moduleName, moduleVersion); + + assert.deepEqual(value, { + moduleName: 'HelloWorld' + }); + }); + test('Script source if package does not exist on both CDNs', async () => { + // Add the other cdn (the opposite of the working one) + const cdns = + cdn === 'unpkg.com' + ? ([cdn, 'jsdelivr.com'] as WidgetCDNs[]) + : ([cdn, 'unpkg.com'] as WidgetCDNs[]); + updateCDNSettings(cdns[0], cdns[1]); + // Make only one cdn available + when(httpClient.exists(anything())).thenCall((a) => { + if (a.includes(cdn[0])) { + return true; + } + return false; + }); + nock(baseUrl) + .get(/.*/) + .reply(200, () => { + return createStreamFromString('foo'); + }); + const value = await scriptSourceProvider.getWidgetScriptSource(moduleName, moduleVersion); + + assert.deepEqual(value, { + moduleName: 'HelloWorld', + scriptUri, + source: 'cdn' + }); + }); + + test('Retry if busy', async () => { + let retryCount = 0; + updateCDNSettings(cdn); + when(httpClient.exists(anything())).thenResolve(true); + nock(baseUrl).log(console.log).get(/.*/).twice().replyWithError('Not found'); + nock(baseUrl) + .log(console.log) + .get(/.*/) + .thrice() + .reply(200, () => { + retryCount = 3; + return createStreamFromString('foo'); + }); + + // Then see if we can get it still. + const value = await scriptSourceProvider.getWidgetScriptSource(moduleName, moduleVersion); + + assert.deepEqual(value, { + moduleName: 'HelloWorld', + scriptUri, + source: 'cdn' + }); + assert.equal(retryCount, 3, 'Did not actually retry'); + }); + test('Script source already on disk', async () => { + updateCDNSettings(cdn); + // Make nobody available + when(httpClient.exists(anything())).thenResolve(true); + + // Write to where the file should eventually end up + const filePath = Uri.parse(scriptUri).fsPath; + await fs.createFile(filePath); + await fs.writeFile(filePath, 'foo'); + + // Then see if we can get it still. + const value = await scriptSourceProvider.getWidgetScriptSource(moduleName, moduleVersion); + + assert.deepEqual(value, { + moduleName: 'HelloWorld', + scriptUri, + source: 'cdn' + }); + }); + }); + }); + }); + }); +}); diff --git a/src/test/datascience/ipywidgets/incompatibleWidgetHandler.unit.test.ts b/src/test/datascience/ipywidgets/incompatibleWidgetHandler.unit.test.ts new file mode 100644 index 000000000000..d87ada1dd0e2 --- /dev/null +++ b/src/test/datascience/ipywidgets/incompatibleWidgetHandler.unit.test.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert } from 'chai'; +import { warnAboutWidgetVersionsThatAreNotSupported } from '../../../datascience-ui/ipywidgets/incompatibleWidgetHandler'; + +// tslint:disable: max-func-body-length no-any +suite('DataScience - Incompatible Widgets', () => { + suite('Using qgrid widget with CDN turned on', () => { + async function testLoadingQgrid(versionToLoad: string, warningExpectedToBeDisplayed: boolean) { + let warningDisplayed = false; + warnAboutWidgetVersionsThatAreNotSupported( + { moduleName: 'qgrid' }, + versionToLoad, + true, + () => (warningDisplayed = true) + ); + + assert.equal(warningDisplayed, warningExpectedToBeDisplayed); + } + test('Widget script is not found for qgrid@1.1.0, then do not display a warning', async () => { + // This test just ensures we never display warnings for 1.1.0. + // This will never happen as the file exists on CDN. + // Hence gurantees that we'll not display when not required. + await testLoadingQgrid('1.1.0', false); + }); + test('Widget script is not found for qgrid@1.1.1, then do not display a warning', async () => { + // This test just ensures we never display warnings for 1.1.0. + // This will never happen as the file exists on CDN. + // Hence gurantees that we'll not display when not required. + await testLoadingQgrid('1.1.1', false); + }); + test('Widget script is not found for qgrid@1.1.2, then display a warning', async () => { + // We know there are no scripts on CDN for > 1.1.1 + await testLoadingQgrid('1.1.2', true); + }); + test('Widget script is not found for qgrid@^1.1.2, then display a warning', async () => { + // We know there are no scripts on CDN for > 1.1.1 + await testLoadingQgrid('^1.1.2', true); + }); + test('Widget script is not found for qgrid@1.3.0, then display a warning', async () => { + // We know there are no scripts on CDN for > 1.1.1 + await testLoadingQgrid('1.3.0', true); + }); + test('Widget script is not found for qgrid@^1.3.0, then display a warning', async () => { + // We know there are no scripts on CDN for > 1.1.1 + await testLoadingQgrid('^1.3.0', true); + }); + }); +}); diff --git a/src/test/datascience/ipywidgets/ipyWidgetScriptSourceProvider.unit.test.ts b/src/test/datascience/ipywidgets/ipyWidgetScriptSourceProvider.unit.test.ts new file mode 100644 index 000000000000..5d549fa4178f --- /dev/null +++ b/src/test/datascience/ipywidgets/ipyWidgetScriptSourceProvider.unit.test.ts @@ -0,0 +1,320 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { ConfigurationChangeEvent, ConfigurationTarget, EventEmitter } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { HttpClient } from '../../../client/common/net/httpClient'; +import { PersistentState, PersistentStateFactory } from '../../../client/common/persistentState'; +import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { Common, DataScience } from '../../../client/common/utils/localize'; +import { noop } from '../../../client/common/utils/misc'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { DataScienceFileSystem } from '../../../client/datascience/dataScienceFileSystem'; +import { CDNWidgetScriptSourceProvider } from '../../../client/datascience/ipywidgets/cdnWidgetScriptSourceProvider'; +import { IPyWidgetScriptSourceProvider } from '../../../client/datascience/ipywidgets/ipyWidgetScriptSourceProvider'; +import { LocalWidgetScriptSourceProvider } from '../../../client/datascience/ipywidgets/localWidgetScriptSourceProvider'; +import { RemoteWidgetScriptSourceProvider } from '../../../client/datascience/ipywidgets/remoteWidgetScriptSourceProvider'; +import { JupyterNotebookBase } from '../../../client/datascience/jupyter/jupyterNotebook'; +import { IJupyterConnection, ILocalResourceUriConverter, INotebook } from '../../../client/datascience/types'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; + +// tslint:disable: no-any no-invalid-this + +suite('DataScience - ipywidget - Widget Script Source Provider', () => { + let scriptSourceProvider: IPyWidgetScriptSourceProvider; + let notebook: INotebook; + let configService: IConfigurationService; + let settings: IPythonSettings; + let appShell: IApplicationShell; + let workspaceService: IWorkspaceService; + let onDidChangeWorkspaceSettings: EventEmitter<ConfigurationChangeEvent>; + let userSelectedOkOrDoNotShowAgainInPrompt: PersistentState<boolean>; + setup(() => { + notebook = mock(JupyterNotebookBase); + configService = mock(ConfigurationService); + appShell = mock(ApplicationShell); + workspaceService = mock(WorkspaceService); + onDidChangeWorkspaceSettings = new EventEmitter<ConfigurationChangeEvent>(); + when(workspaceService.onDidChangeConfiguration).thenReturn(onDidChangeWorkspaceSettings.event); + const httpClient = mock(HttpClient); + const resourceConverter = mock<ILocalResourceUriConverter>(); + const fs = mock(DataScienceFileSystem); + const interpreterService = mock(InterpreterService); + const stateFactory = mock(PersistentStateFactory); + userSelectedOkOrDoNotShowAgainInPrompt = mock<PersistentState<boolean>>(); + + when(stateFactory.createGlobalPersistentState(anything(), anything())).thenReturn( + instance(userSelectedOkOrDoNotShowAgainInPrompt) + ); + settings = { datascience: { widgetScriptSources: [] } } as any; + when(configService.getSettings(anything())).thenReturn(settings as any); + when(userSelectedOkOrDoNotShowAgainInPrompt.value).thenReturn(false); + when(userSelectedOkOrDoNotShowAgainInPrompt.updateValue(anything())).thenResolve(); + scriptSourceProvider = new IPyWidgetScriptSourceProvider( + instance(notebook), + instance(resourceConverter), + instance(fs), + instance(interpreterService), + instance(appShell), + instance(configService), + instance(workspaceService), + instance(stateFactory), + instance(httpClient) + ); + }); + teardown(() => sinon.restore()); + + [true, false].forEach((localLaunch) => { + suite(localLaunch ? 'Local Jupyter Server' : 'Remote Jupyter Server', () => { + setup(() => { + const connection: IJupyterConnection = { + type: 'jupyter', + valid: true, + displayName: '', + baseUrl: '', + localProcExitCode: undefined, + disconnected: new EventEmitter<number>().event, + dispose: noop, + hostName: '', + localLaunch, + token: '', + rootDirectory: EXTENSION_ROOT_DIR + }; + when(notebook.connection).thenReturn(connection); + }); + test('Prompt to use CDN', async () => { + when(appShell.showInformationMessage(anything(), anything(), anything(), anything())).thenResolve(); + + await scriptSourceProvider.getWidgetScriptSource('HelloWorld', '1'); + + verify( + appShell.showInformationMessage( + DataScience.useCDNForWidgets(), + Common.ok(), + Common.cancel(), + Common.doNotShowAgain() + ) + ).once(); + }); + test('Do not prompt to use CDN if user has chosen not to use a CDN', async () => { + when(appShell.showInformationMessage(anything(), anything(), anything(), anything())).thenResolve(); + when(userSelectedOkOrDoNotShowAgainInPrompt.value).thenReturn(true); + + await scriptSourceProvider.getWidgetScriptSource('HelloWorld', '1'); + + verify( + appShell.showInformationMessage( + DataScience.useCDNForWidgets(), + Common.ok(), + Common.cancel(), + Common.doNotShowAgain() + ) + ).never(); + }); + function verifyNoCDNUpdatedInSettings() { + // Confirm message was displayed. + verify( + appShell.showInformationMessage( + DataScience.useCDNForWidgets(), + Common.ok(), + Common.cancel(), + Common.doNotShowAgain() + ) + ).once(); + + // Confirm settings were updated. + verify( + configService.updateSetting( + 'dataScience.widgetScriptSources', + deepEqual([]), + undefined, + ConfigurationTarget.Global + ) + ).once(); + } + test('Do not update if prompt is dismissed', async () => { + when(appShell.showInformationMessage(anything(), anything(), anything(), anything())).thenResolve(); + + await scriptSourceProvider.getWidgetScriptSource('HelloWorld', '1'); + + verify(configService.updateSetting(anything(), anything(), anything(), anything())).never(); + verify(userSelectedOkOrDoNotShowAgainInPrompt.updateValue(true)).never(); + }); + test('Do not update settings if Cancel is clicked in prompt', async () => { + when(appShell.showInformationMessage(anything(), anything(), anything(), anything())).thenResolve( + Common.cancel() as any + ); + + await scriptSourceProvider.getWidgetScriptSource('HelloWorld', '1'); + + verify(configService.updateSetting(anything(), anything(), anything(), anything())).never(); + verify(userSelectedOkOrDoNotShowAgainInPrompt.updateValue(true)).never(); + }); + test('Update settings to not use CDN if `Do Not Show Again` is clicked in prompt', async () => { + when(appShell.showInformationMessage(anything(), anything(), anything(), anything())).thenResolve( + Common.doNotShowAgain() as any + ); + + await scriptSourceProvider.getWidgetScriptSource('HelloWorld', '1'); + + verifyNoCDNUpdatedInSettings(); + verify(userSelectedOkOrDoNotShowAgainInPrompt.updateValue(true)).once(); + }); + test('Update settings to use CDN based on prompt', async () => { + when(appShell.showInformationMessage(anything(), anything(), anything(), anything())).thenResolve( + Common.ok() as any + ); + + await scriptSourceProvider.getWidgetScriptSource('HelloWorld', '1'); + + // Confirm message was displayed. + verify( + appShell.showInformationMessage( + DataScience.useCDNForWidgets(), + Common.ok(), + Common.cancel(), + Common.doNotShowAgain() + ) + ).once(); + // Confirm settings were updated. + verify(userSelectedOkOrDoNotShowAgainInPrompt.updateValue(true)).once(); + verify( + configService.updateSetting( + 'dataScience.widgetScriptSources', + deepEqual(['jsdelivr.com', 'unpkg.com']), + undefined, + ConfigurationTarget.Global + ) + ).once(); + }); + test('Attempt to get widget source from all providers', async () => { + settings.datascience.widgetScriptSources = ['jsdelivr.com', 'unpkg.com']; + const localOrRemoteSource = localLaunch + ? sinon.stub(LocalWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource') + : sinon.stub(RemoteWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource'); + const cdnSource = sinon.stub(CDNWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource'); + + localOrRemoteSource.resolves({ moduleName: 'HelloWorld' }); + cdnSource.resolves({ moduleName: 'HelloWorld' }); + + scriptSourceProvider.initialize(); + const value = await scriptSourceProvider.getWidgetScriptSource('HelloWorld', '1'); + + assert.deepEqual(value, { moduleName: 'HelloWorld' }); + assert.isTrue(localOrRemoteSource.calledOnce); + assert.isTrue(cdnSource.calledOnce); + }); + test('Widget sources should respect changes to configuration settings', async () => { + // 1. Search CDN then local/remote juptyer. + settings.datascience.widgetScriptSources = ['jsdelivr.com', 'unpkg.com']; + const localOrRemoteSource = localLaunch + ? sinon.stub(LocalWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource') + : sinon.stub(RemoteWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource'); + const cdnSource = sinon.stub(CDNWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource'); + cdnSource.resolves({ moduleName: 'moduleCDN', scriptUri: '1', source: 'cdn' }); + + scriptSourceProvider.initialize(); + let values = await scriptSourceProvider.getWidgetScriptSource('ModuleName', '`'); + + assert.deepEqual(values, { moduleName: 'moduleCDN', scriptUri: '1', source: 'cdn' }); + assert.isFalse(localOrRemoteSource.calledOnce); + assert.isTrue(cdnSource.calledOnce); + + // 2. Update settings to remove the use of CDNs + localOrRemoteSource.reset(); + cdnSource.reset(); + localOrRemoteSource.resolves({ moduleName: 'moduleLocal', scriptUri: '1', source: 'local' }); + settings.datascience.widgetScriptSources = []; + onDidChangeWorkspaceSettings.fire({ affectsConfiguration: () => true }); + + values = await scriptSourceProvider.getWidgetScriptSource('ModuleName', '`'); + assert.deepEqual(values, { moduleName: 'moduleLocal', scriptUri: '1', source: 'local' }); + assert.isTrue(localOrRemoteSource.calledOnce); + assert.isFalse(cdnSource.calledOnce); + }); + test('Widget source should support fall back search', async () => { + // 1. Search CDN and if that fails then get from local/remote. + settings.datascience.widgetScriptSources = ['jsdelivr.com', 'unpkg.com']; + const localOrRemoteSource = localLaunch + ? sinon.stub(LocalWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource') + : sinon.stub(RemoteWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource'); + const cdnSource = sinon.stub(CDNWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource'); + localOrRemoteSource.resolves({ moduleName: 'moduleLocal', scriptUri: '1', source: 'local' }); + cdnSource.resolves({ moduleName: 'moduleCDN' }); + + scriptSourceProvider.initialize(); + const value = await scriptSourceProvider.getWidgetScriptSource('', ''); + + // 1. Confirm CDN was first searched, then local/remote + assert.deepEqual(value, { moduleName: 'moduleLocal', scriptUri: '1', source: 'local' }); + assert.isTrue(localOrRemoteSource.calledOnce); + assert.isTrue(cdnSource.calledOnce); + // Confirm we first searched CDN before going to local/remote. + cdnSource.calledBefore(localOrRemoteSource); + }); + test('Widget sources from CDN should be given prefernce', async () => { + settings.datascience.widgetScriptSources = ['jsdelivr.com', 'unpkg.com']; + const localOrRemoteSource = localLaunch + ? sinon.stub(LocalWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource') + : sinon.stub(RemoteWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource'); + const cdnSource = sinon.stub(CDNWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource'); + + localOrRemoteSource.resolves({ moduleName: 'module1' }); + cdnSource.resolves({ moduleName: 'module1', scriptUri: '1', source: 'cdn' }); + + scriptSourceProvider.initialize(); + const values = await scriptSourceProvider.getWidgetScriptSource('ModuleName', '1'); + + assert.deepEqual(values, { moduleName: 'module1', scriptUri: '1', source: 'cdn' }); + assert.isFalse(localOrRemoteSource.calledOnce); + assert.isTrue(cdnSource.calledOnce); + verify(appShell.showWarningMessage(anything(), anything(), anything(), anything())).never(); + }); + test('When CDN is turned on and widget script is not found, then display a warning about script not found on CDN', async () => { + settings.datascience.widgetScriptSources = ['jsdelivr.com', 'unpkg.com']; + const localOrRemoteSource = localLaunch + ? sinon.stub(LocalWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource') + : sinon.stub(RemoteWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource'); + const cdnSource = sinon.stub(CDNWidgetScriptSourceProvider.prototype, 'getWidgetScriptSource'); + + localOrRemoteSource.resolves({ moduleName: 'module1' }); + cdnSource.resolves({ moduleName: 'module1' }); + + scriptSourceProvider.initialize(); + let values = await scriptSourceProvider.getWidgetScriptSource('module1', '1'); + + assert.deepEqual(values, { moduleName: 'module1' }); + assert.isTrue(localOrRemoteSource.calledOnce); + assert.isTrue(cdnSource.calledOnce); + verify( + appShell.showWarningMessage( + DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork().format('module1'), + anything(), + anything(), + anything() + ) + ).once(); + + // Ensure message is not displayed more than once. + values = await scriptSourceProvider.getWidgetScriptSource('module1', '1'); + + assert.deepEqual(values, { moduleName: 'module1' }); + assert.isTrue(localOrRemoteSource.calledTwice); + assert.isTrue(cdnSource.calledTwice); + verify( + appShell.showWarningMessage( + DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork().format('module1'), + anything(), + anything(), + anything() + ) + ).once(); + }); + }); + }); +}); diff --git a/src/test/datascience/ipywidgets/localWidgetScriptSourceProvider.unit.test.ts b/src/test/datascience/ipywidgets/localWidgetScriptSourceProvider.unit.test.ts new file mode 100644 index 000000000000..9e2d182892ab --- /dev/null +++ b/src/test/datascience/ipywidgets/localWidgetScriptSourceProvider.unit.test.ts @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { PYTHON_LANGUAGE } from '../../../client/common/constants'; +import { DataScienceFileSystem } from '../../../client/datascience/dataScienceFileSystem'; +import { LocalWidgetScriptSourceProvider } from '../../../client/datascience/ipywidgets/localWidgetScriptSourceProvider'; +import { IWidgetScriptSourceProvider } from '../../../client/datascience/ipywidgets/types'; +import { JupyterNotebookBase } from '../../../client/datascience/jupyter/jupyterNotebook'; +import { IDataScienceFileSystem, ILocalResourceUriConverter, INotebook } from '../../../client/datascience/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; + +// tslint:disable: max-func-body-length no-any +suite('DataScience - ipywidget - Local Widget Script Source', () => { + let scriptSourceProvider: IWidgetScriptSourceProvider; + let notebook: INotebook; + let resourceConverter: ILocalResourceUriConverter; + let fs: IDataScienceFileSystem; + let interpreterService: IInterpreterService; + const filesToLookSearchFor = `*${path.sep}index.js`; + function asVSCodeUri(uri: Uri) { + return `vscodeUri://${uri.fsPath}`; + } + setup(() => { + notebook = mock(JupyterNotebookBase); + resourceConverter = mock<ILocalResourceUriConverter>(); + fs = mock(DataScienceFileSystem); + interpreterService = mock(InterpreterService); + when(resourceConverter.asWebviewUri(anything())).thenCall((uri) => Promise.resolve(asVSCodeUri(uri))); + scriptSourceProvider = new LocalWidgetScriptSourceProvider( + instance(notebook), + instance(resourceConverter), + instance(fs), + instance(interpreterService) + ); + }); + test('No script source when there is no kernel associated with notebook', async () => { + when(notebook.getKernelConnection()).thenReturn(); + + const value = await scriptSourceProvider.getWidgetScriptSource('ModuleName', '1'); + + assert.deepEqual(value, { moduleName: 'ModuleName' }); + }); + test('No script source when there are no widgets', async () => { + when(notebook.getKernelConnection()).thenReturn({ + kernelSpec: { metadata: { interpreter: { sysPrefix: 'sysPrefix', path: 'pythonPath' } } } + } as any); + when(fs.searchLocal(anything(), anything())).thenResolve([]); + + const value = await scriptSourceProvider.getWidgetScriptSource('ModuleName', '1'); + + assert.deepEqual(value, { moduleName: 'ModuleName' }); + + // Ensure we searched the directories. + verify(fs.searchLocal(anything(), anything())).once(); + }); + test('Look for widgets in sysPath of interpreter defined in kernel metadata', async () => { + const sysPrefix = 'sysPrefix Of Python in Metadata'; + const searchDirectory = path.join(sysPrefix, 'share', 'jupyter', 'nbextensions'); + + when(notebook.getKernelConnection()).thenReturn({ + kernelSpec: { metadata: { interpreter: { sysPrefix, path: 'pythonPath' } } } + } as any); + when(fs.searchLocal(anything(), anything())).thenResolve([]); + + const value = await scriptSourceProvider.getWidgetScriptSource('ModuleName', '1'); + + assert.deepEqual(value, { moduleName: 'ModuleName' }); + + // Ensure we look for the right things in the right place. + verify(fs.searchLocal(filesToLookSearchFor, searchDirectory)).once(); + }); + test('Look for widgets in sysPath of kernel', async () => { + const sysPrefix = 'sysPrefix Of Kernel'; + const kernelPath = 'kernel Path.exe'; + when(interpreterService.getInterpreterDetails(kernelPath)).thenResolve({ sysPrefix } as any); + const searchDirectory = path.join(sysPrefix, 'share', 'jupyter', 'nbextensions'); + + when(notebook.getKernelConnection()).thenReturn({ + kernelSpec: { path: kernelPath, language: PYTHON_LANGUAGE } + } as any); + when(fs.searchLocal(anything(), anything())).thenResolve([]); + + const value = await scriptSourceProvider.getWidgetScriptSource('ModuleName', '1'); + + assert.deepEqual(value, { moduleName: 'ModuleName' }); + + // Ensure we look for the right things in the right place. + verify(fs.searchLocal(filesToLookSearchFor, searchDirectory)).once(); + }); + test('Ensure we cache the list of widgets source (when nothing is found)', async () => { + when(notebook.getKernelConnection()).thenReturn({ + kernelSpec: { metadata: { interpreter: { sysPrefix: 'sysPrefix', path: 'pythonPath' } } } + } as any); + when(fs.searchLocal(anything(), anything())).thenResolve([]); + + const value = await scriptSourceProvider.getWidgetScriptSource('ModuleName', '1'); + assert.deepEqual(value, { moduleName: 'ModuleName' }); + const value1 = await scriptSourceProvider.getWidgetScriptSource('ModuleName', '1'); + assert.deepEqual(value1, { moduleName: 'ModuleName' }); + const value2 = await scriptSourceProvider.getWidgetScriptSource('ModuleName', '1'); + assert.deepEqual(value2, { moduleName: 'ModuleName' }); + + // Ensure we search directories once. + verify(fs.searchLocal(anything(), anything())).once(); + }); + test('Ensure we search directory only once (cache results)', async () => { + const sysPrefix = 'sysPrefix Of Python in Metadata'; + const searchDirectory = path.join(sysPrefix, 'share', 'jupyter', 'nbextensions'); + when(notebook.getKernelConnection()).thenReturn({ + kernelSpec: { metadata: { interpreter: { sysPrefix, path: 'pythonPath' } } } + } as any); + when(fs.searchLocal(anything(), anything())).thenResolve([ + // In order to match the real behavior, don't use join here + 'widget1/index.js', + 'widget2/index.js', + 'widget3/index.js' + ]); + + const value = await scriptSourceProvider.getWidgetScriptSource('widget2', '1'); + assert.deepEqual(value, { + moduleName: 'widget2', + source: 'local', + scriptUri: asVSCodeUri(Uri.file(path.join(searchDirectory, 'widget2', 'index.js'))) + }); + const value1 = await scriptSourceProvider.getWidgetScriptSource('widget2', '1'); + assert.deepEqual(value1, value); + const value2 = await scriptSourceProvider.getWidgetScriptSource('widget2', '1'); + assert.deepEqual(value2, value); + + // Ensure we look for the right things in the right place. + verify(fs.searchLocal(filesToLookSearchFor, searchDirectory)).once(); + }); + test('Get source for a specific widget & search in the right place', async () => { + const sysPrefix = 'sysPrefix Of Python in Metadata'; + const searchDirectory = path.join(sysPrefix, 'share', 'jupyter', 'nbextensions'); + when(notebook.getKernelConnection()).thenReturn({ + kernelSpec: { metadata: { interpreter: { sysPrefix, path: 'pythonPath' } } } + } as any); + when(fs.searchLocal(anything(), anything())).thenResolve([ + // In order to match the real behavior, don't use join here + 'widget1/index.js', + 'widget2/index.js', + 'widget3/index.js' + ]); + + const value = await scriptSourceProvider.getWidgetScriptSource('widget1', '1'); + + // Ensure the script paths are properly converted to be used within notebooks. + assert.deepEqual(value, { + moduleName: 'widget1', + source: 'local', + scriptUri: asVSCodeUri(Uri.file(path.join(searchDirectory, 'widget1', 'index.js'))) + }); + + // Ensure we look for the right things in the right place. + verify(fs.searchLocal(filesToLookSearchFor, searchDirectory)).once(); + }); + test('Return empty source for widgets that cannot be found', async () => { + const sysPrefix = 'sysPrefix Of Python in Metadata'; + const searchDirectory = path.join(sysPrefix, 'share', 'jupyter', 'nbextensions'); + when(notebook.getKernelConnection()).thenReturn({ + kernelSpec: { metadata: { interpreter: { sysPrefix, path: 'pythonPath' } } } + } as any); + when(fs.searchLocal(anything(), anything())).thenResolve([ + // In order to match the real behavior, don't use join here + 'widget1/index.js', + 'widget2/index.js', + 'widget3/index.js' + ]); + + const value = await scriptSourceProvider.getWidgetScriptSource('widgetNotFound', '1'); + assert.deepEqual(value, { + moduleName: 'widgetNotFound' + }); + const value1 = await scriptSourceProvider.getWidgetScriptSource('widgetNotFound', '1'); + assert.isOk(value1); + const value2 = await scriptSourceProvider.getWidgetScriptSource('widgetNotFound', '1'); + assert.deepEqual(value2, value1); + // We should ignore version numbers (when getting widget sources from local fs). + const value3 = await scriptSourceProvider.getWidgetScriptSource('widgetNotFound', '1234'); + assert.deepEqual(value3, value1); + + // Ensure we look for the right things in the right place. + // Also ensure we call once (& cache for subsequent searches). + verify(fs.searchLocal(filesToLookSearchFor, searchDirectory)).once(); + }); +}); diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts new file mode 100644 index 000000000000..5663af080ac5 --- /dev/null +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../../../client/common/application/types'; +import { ProductInstaller } from '../../../../client/common/installer/productInstaller'; +import { IInstaller, InstallerResponse, Product } from '../../../../client/common/types'; +import { DataScience } from '../../../../client/common/utils/localize'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { + InterpreterJupyterKernelSpecCommand, + JupyterCommandFactory +} from '../../../../client/datascience/jupyter/interpreter/jupyterCommand'; +import { + JupyterInterpreterDependencyResponse, + JupyterInterpreterDependencyService +} from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService'; +import { IJupyterCommand, IJupyterCommandFactory } from '../../../../client/datascience/types'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +// tslint:disable: max-func-body-length no-any + +suite('DataScience - Jupyter Interpreter Configuration', () => { + let configuration: JupyterInterpreterDependencyService; + let appShell: IApplicationShell; + let installer: IInstaller; + let commandFactory: IJupyterCommandFactory; + let command: IJupyterCommand; + const pythonInterpreter: PythonEnvironment = { + path: '', + architecture: Architecture.Unknown, + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown + }; + setup(() => { + appShell = mock(ApplicationShell); + installer = mock(ProductInstaller); + commandFactory = mock(JupyterCommandFactory); + command = mock(InterpreterJupyterKernelSpecCommand); + instance(commandFactory as any).then = undefined; + instance(command as any).then = undefined; + when( + commandFactory.createInterpreterCommand(anything(), anything(), anything(), anything(), anything()) + ).thenReturn(instance(command)); + when(command.exec(anything(), anything())).thenResolve({ stdout: '' }); + + configuration = new JupyterInterpreterDependencyService( + instance(appShell), + instance(installer), + instance(commandFactory) + ); + }); + test('Return ok if all dependencies are installed', async () => { + when(installer.isInstalled(Product.jupyter, pythonInterpreter)).thenResolve(true); + when(installer.isInstalled(Product.notebook, pythonInterpreter)).thenResolve(true); + + const response = await configuration.installMissingDependencies(pythonInterpreter); + + assert.equal(response, JupyterInterpreterDependencyResponse.ok); + }); + async function testPromptIfModuleNotInstalled( + jupyterInstalled: boolean, + notebookInstalled: boolean + ): Promise<void> { + when(installer.isInstalled(Product.jupyter, pythonInterpreter)).thenResolve(jupyterInstalled); + when(installer.isInstalled(Product.notebook, pythonInterpreter)).thenResolve(notebookInstalled); + when(appShell.showErrorMessage(anything(), anything(), anything(), anything())).thenResolve(); + + const response = await configuration.installMissingDependencies(pythonInterpreter); + + verify( + appShell.showErrorMessage( + anything(), + DataScience.jupyterInstall(), + DataScience.selectDifferentJupyterInterpreter(), + DataScience.pythonInteractiveHelpLink() + ) + ).once(); + assert.equal(response, JupyterInterpreterDependencyResponse.cancel); + } + test('Prompt to install if Jupyter is not installed', async () => testPromptIfModuleNotInstalled(false, true)); + test('Prompt to install if notebook is not installed', async () => testPromptIfModuleNotInstalled(true, false)); + test('Prompt to install if jupyter & notebook is not installed', async () => + testPromptIfModuleNotInstalled(false, false)); + test('Reinstall Jupyter if jupyter and notebook are installed but kernelspec is not found', async () => { + when(installer.isInstalled(Product.jupyter, pythonInterpreter)).thenResolve(true); + when(installer.isInstalled(Product.notebook, pythonInterpreter)).thenResolve(true); + when(appShell.showErrorMessage(anything(), anything(), anything(), anything())).thenResolve( + // tslint:disable-next-line: no-any + DataScience.jupyterInstall() as any + ); + when(command.exec(anything(), anything())).thenReject(new Error('Not found')); + when(installer.install(anything(), anything(), anything())).thenResolve(InstallerResponse.Installed); + + const response = await configuration.installMissingDependencies(pythonInterpreter); + + // Jupyter must be installed & not kernelspec or anything else. + verify(installer.install(Product.jupyter, anything(), anything())).once(); + verify(installer.install(anything(), anything(), anything())).once(); + verify( + appShell.showErrorMessage( + anything(), + DataScience.jupyterInstall(), + DataScience.selectDifferentJupyterInterpreter(), + anything() + ) + ).once(); + assert.equal(response, JupyterInterpreterDependencyResponse.cancel); + }); + + async function testInstallationOfJupyter( + installerResponse: InstallerResponse, + expectedConfigurationReponse: JupyterInterpreterDependencyResponse + ): Promise<void> { + when(installer.isInstalled(Product.jupyter, pythonInterpreter)).thenResolve(false); + when(installer.isInstalled(Product.notebook, pythonInterpreter)).thenResolve(true); + when(appShell.showErrorMessage(anything(), anything(), anything(), anything())).thenResolve( + // tslint:disable-next-line: no-any + DataScience.jupyterInstall() as any + ); + when(installer.install(anything(), anything(), anything())).thenResolve(installerResponse); + + const response = await configuration.installMissingDependencies(pythonInterpreter); + + verify(installer.install(Product.jupyter, pythonInterpreter, anything())).once(); + assert.equal(response, expectedConfigurationReponse); + } + async function testInstallationOfJupyterAndNotebook( + jupyterInstallerResponse: InstallerResponse, + notebookInstallationResponse: InstallerResponse, + expectedConfigurationReponse: JupyterInterpreterDependencyResponse + ): Promise<void> { + when(installer.isInstalled(Product.jupyter, pythonInterpreter)).thenResolve(false); + when(installer.isInstalled(Product.notebook, pythonInterpreter)).thenResolve(false); + when(appShell.showErrorMessage(anything(), anything(), anything(), anything())).thenResolve( + // tslint:disable-next-line: no-any + DataScience.jupyterInstall() as any + ); + when(installer.install(Product.jupyter, anything(), anything())).thenResolve(jupyterInstallerResponse); + when(installer.install(Product.notebook, anything(), anything())).thenResolve(notebookInstallationResponse); + + const response = await configuration.installMissingDependencies(pythonInterpreter); + + verify(installer.install(Product.jupyter, pythonInterpreter, anything())).once(); + verify(installer.install(Product.notebook, pythonInterpreter, anything())).once(); + assert.equal(response, expectedConfigurationReponse); + } + test('Install Jupyter and return ok if installed successfully', async () => + testInstallationOfJupyter(InstallerResponse.Installed, JupyterInterpreterDependencyResponse.ok)); + test('Install Jupyter & notebook and return ok if both are installed successfully', async () => + testInstallationOfJupyterAndNotebook( + InstallerResponse.Installed, + InstallerResponse.Installed, + JupyterInterpreterDependencyResponse.ok + )); + test('Install Jupyter & notebook and return cancel if notebook is not installed', async () => + testInstallationOfJupyterAndNotebook( + InstallerResponse.Installed, + InstallerResponse.Ignore, + JupyterInterpreterDependencyResponse.cancel + )); + test('Install Jupyter and return cancel if installation is disabled', async () => + testInstallationOfJupyter(InstallerResponse.Disabled, JupyterInterpreterDependencyResponse.cancel)); + test('Install Jupyter and return cancel if installation is ignored', async () => + testInstallationOfJupyter(InstallerResponse.Ignore, JupyterInterpreterDependencyResponse.cancel)); +}); diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand.unit.test.ts new file mode 100644 index 000000000000..9b40580acacf --- /dev/null +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand.unit.test.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Disposable } from 'vscode'; +import { IExtensionSingleActivationService } from '../../../../client/activation/types'; +import { CommandManager } from '../../../../client/common/application/commandManager'; +import { ICommandManager } from '../../../../client/common/application/types'; +import { IDisposableRegistry } from '../../../../client/common/types'; +import { JupyterInterpreterSelectionCommand } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterSelectionCommand'; +import { JupyterInterpreterService } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterService'; + +suite('DataScience - Jupyter Interpreter Command', () => { + let interpreterCommand: IExtensionSingleActivationService; + let disposableRegistry: IDisposableRegistry; + let commandManager: ICommandManager; + let interpreterService: JupyterInterpreterService; + setup(() => { + interpreterService = mock(JupyterInterpreterService); + commandManager = mock(CommandManager); + disposableRegistry = []; + when(interpreterService.selectInterpreter()).thenResolve(); + interpreterCommand = new JupyterInterpreterSelectionCommand( + instance(interpreterService), + instance(commandManager), + disposableRegistry + ); + }); + test('Activation should register command', async () => { + const disposable = mock(Disposable); + when(commandManager.registerCommand('python.datascience.selectJupyterInterpreter', anything())).thenReturn( + instance(disposable) + ); + + await interpreterCommand.activate(); + + verify(commandManager.registerCommand('python.datascience.selectJupyterInterpreter', anything())).once(); + }); + test('Command handler must be jupyter interpreter selection', async () => { + const disposable = mock(Disposable); + let handler: Function | undefined; + when(commandManager.registerCommand('python.datascience.selectJupyterInterpreter', anything())).thenCall( + (_, cb: Function) => { + handler = cb; + return instance(disposable); + } + ); + + await interpreterCommand.activate(); + + verify(commandManager.registerCommand('python.datascience.selectJupyterInterpreter', anything())).once(); + assert.isFunction(handler); + + // Invoking handler must select jupyter interpreter. + handler!(); + + verify(interpreterService.selectInterpreter()).once(); + }); +}); diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelector.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelector.unit.test.ts new file mode 100644 index 000000000000..55685e69afe1 --- /dev/null +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelector.unit.test.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { IApplicationShell, IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { PythonSettings } from '../../../../client/common/configSettings'; +import { PathUtils } from '../../../../client/common/platform/pathUtils'; +import { IDataScienceSettings, IPathUtils } from '../../../../client/common/types'; +import { JupyterInterpreterSelector } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterSelector'; +import { JupyterInterpreterStateStore } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterStateStore'; +import { InterpreterSelector } from '../../../../client/interpreter/configuration/interpreterSelector/interpreterSelector'; +import { IInterpreterSelector } from '../../../../client/interpreter/configuration/types'; + +suite('DataScience - Jupyter Interpreter Picker', () => { + let picker: JupyterInterpreterSelector; + let interpreterSelector: IInterpreterSelector; + let appShell: IApplicationShell; + let interpreterSelectionState: JupyterInterpreterStateStore; + let workspace: IWorkspaceService; + let pathUtils: IPathUtils; + let dsSettings: IDataScienceSettings; + + setup(() => { + interpreterSelector = mock(InterpreterSelector); + interpreterSelectionState = mock(JupyterInterpreterStateStore); + appShell = mock(ApplicationShell); + workspace = mock(WorkspaceService); + pathUtils = mock(PathUtils); + const pythonSettings = mock(PythonSettings); + // tslint:disable-next-line: no-any + dsSettings = {} as any; + when(pythonSettings.datascience).thenReturn(dsSettings); + picker = new JupyterInterpreterSelector( + instance(interpreterSelector), + instance(appShell), + instance(interpreterSelectionState), + instance(workspace), + instance(pathUtils) + ); + }); + + test('Should display the list of interpreters', async () => { + // tslint:disable-next-line: no-any + const interpreters = ['something'] as any[]; + when(interpreterSelector.getSuggestions(undefined)).thenResolve(interpreters); + when(appShell.showQuickPick(anything(), anything())).thenResolve(); + + await picker.selectInterpreter(); + + verify(interpreterSelector.getSuggestions(undefined)).once(); + verify(appShell.showQuickPick(anything(), anything())).once(); + }); + test('Selected interpreter must be returned', async () => { + // tslint:disable-next-line: no-any + const interpreters = ['something'] as any[]; + // tslint:disable-next-line: no-any + const interpreter = {} as any; + when(interpreterSelector.getSuggestions(undefined)).thenResolve(interpreters); + // tslint:disable-next-line: no-any + when(appShell.showQuickPick(anything(), anything())).thenResolve({ interpreter } as any); + + const selected = await picker.selectInterpreter(); + + assert.isOk(selected === interpreter, 'Not the same instance'); + }); + test('Should display current interpreter path in the picker', async () => { + // tslint:disable-next-line: no-any + const interpreters = ['something'] as any[]; + const displayPath = 'Display Path'; + when(interpreterSelectionState.selectedPythonPath).thenReturn('jupyter.exe'); + when(pathUtils.getDisplayName('jupyter.exe', anything())).thenReturn(displayPath); + when(interpreterSelector.getSuggestions(undefined)).thenResolve(interpreters); + when(appShell.showQuickPick(anything(), anything())).thenResolve(); + + await picker.selectInterpreter(); + + assert.equal(capture(appShell.showQuickPick).first()[1]?.placeHolder, `current: ${displayPath}`); + }); +}); diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterService.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterService.unit.test.ts new file mode 100644 index 000000000000..1c2b1ea270b8 --- /dev/null +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterService.unit.test.ts @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; +import { Memento } from 'vscode'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { + JupyterInterpreterDependencyResponse, + JupyterInterpreterDependencyService +} from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService'; +import { JupyterInterpreterOldCacheStateStore } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterOldCacheStateStore'; +import { JupyterInterpreterSelector } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterSelector'; +import { JupyterInterpreterService } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterService'; +import { JupyterInterpreterStateStore } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterStateStore'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../../client/interpreter/interpreterService'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { MockMemento } from '../../../mocks/mementos'; +import { createPythonInterpreter } from '../../../utils/interpreters'; + +// tslint:disable: max-func-body-length + +suite('DataScience - Jupyter Interpreter Service', () => { + let jupyterInterpreterService: JupyterInterpreterService; + let interpreterSelector: JupyterInterpreterSelector; + let interpreterConfiguration: JupyterInterpreterDependencyService; + let interpreterService: IInterpreterService; + let selectedInterpreterEventArgs: PythonEnvironment | undefined; + let memento: Memento; + let interpreterSelectionState: JupyterInterpreterStateStore; + let oldVersionCacheStateStore: JupyterInterpreterOldCacheStateStore; + const selectedJupyterInterpreter = createPythonInterpreter({ displayName: 'JupyterInterpreter' }); + const pythonInterpreter: PythonEnvironment = { + path: 'some path', + architecture: Architecture.Unknown, + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown + }; + const secondPythonInterpreter: PythonEnvironment = { + path: 'second interpreter path', + architecture: Architecture.Unknown, + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown + }; + + setup(() => { + interpreterSelector = mock(JupyterInterpreterSelector); + interpreterConfiguration = mock(JupyterInterpreterDependencyService); + interpreterService = mock(InterpreterService); + memento = mock(MockMemento); + interpreterSelectionState = mock(JupyterInterpreterStateStore); + oldVersionCacheStateStore = mock(JupyterInterpreterOldCacheStateStore); + jupyterInterpreterService = new JupyterInterpreterService( + instance(oldVersionCacheStateStore), + instance(interpreterSelectionState), + instance(interpreterSelector), + instance(interpreterConfiguration), + instance(interpreterService) + ); + when(interpreterService.getInterpreterDetails(pythonInterpreter.path, undefined)).thenResolve( + pythonInterpreter + ); + when(interpreterService.getInterpreterDetails(secondPythonInterpreter.path, undefined)).thenResolve( + secondPythonInterpreter + ); + when(memento.update(anything(), anything())).thenResolve(); + jupyterInterpreterService.onDidChangeInterpreter((e) => (selectedInterpreterEventArgs = e)); + when(interpreterSelector.selectInterpreter()).thenResolve(pythonInterpreter); + }); + + test('Cancelling interpreter configuration is same as cancelling selection of an interpreter', async () => { + when( + interpreterConfiguration.installMissingDependencies(pythonInterpreter, anything(), anything()) + ).thenResolve(JupyterInterpreterDependencyResponse.cancel); + + const response = await jupyterInterpreterService.selectInterpreter(); + + assert.equal(response, undefined); + assert.isUndefined(selectedInterpreterEventArgs); + }); + test('Once selected interpreter must be stored in settings and event fired', async () => { + when( + interpreterConfiguration.installMissingDependencies(pythonInterpreter, anything(), anything()) + ).thenResolve(JupyterInterpreterDependencyResponse.ok); + + const response = await jupyterInterpreterService.selectInterpreter(); + + verify(interpreterConfiguration.installMissingDependencies(pythonInterpreter, anything(), anything())).once(); + assert.equal(response, pythonInterpreter); + assert.equal(selectedInterpreterEventArgs, pythonInterpreter); + + // Selected interpreter should be returned. + const selectedInterpreter = await jupyterInterpreterService.selectInterpreter(); + + assert.equal(selectedInterpreter, pythonInterpreter); + }); + test('Select another interpreter if user opts to not install dependencies', async () => { + when( + interpreterConfiguration.installMissingDependencies(pythonInterpreter, anything(), anything()) + ).thenResolve(JupyterInterpreterDependencyResponse.selectAnotherInterpreter); + when( + interpreterConfiguration.installMissingDependencies(secondPythonInterpreter, anything(), anything()) + ).thenResolve(JupyterInterpreterDependencyResponse.ok); + let interpreterSelection = 0; + when(interpreterSelector.selectInterpreter()).thenCall(() => { + // When selecting intererpter for first time, return first interpreter + // When selected interpretre + interpreterSelection += 1; + return interpreterSelection === 1 ? pythonInterpreter : secondPythonInterpreter; + }); + + const response = await jupyterInterpreterService.selectInterpreter(); + + verify(interpreterSelector.selectInterpreter()).twice(); + assert.equal(response, secondPythonInterpreter); + assert.equal(selectedInterpreterEventArgs, secondPythonInterpreter); + + // Selected interpreter should be the second interpreter. + const selectedInterpreter = await jupyterInterpreterService.selectInterpreter(); + + assert.equal(selectedInterpreter, secondPythonInterpreter); + }); + test('setInitialInterpreter if older version is set should use and clear', async () => { + when(oldVersionCacheStateStore.getCachedInterpreterPath()).thenReturn(pythonInterpreter.path); + when(oldVersionCacheStateStore.clearCache()).thenResolve(); + when(interpreterConfiguration.areDependenciesInstalled(pythonInterpreter, anything())).thenResolve(true); + const initialInterpreter = await jupyterInterpreterService.setInitialInterpreter(undefined); + verify(oldVersionCacheStateStore.clearCache()).once(); + assert.equal(initialInterpreter, pythonInterpreter); + }); + test('setInitialInterpreter use saved interpreter if valid', async () => { + when(oldVersionCacheStateStore.getCachedInterpreterPath()).thenReturn(undefined); + when(interpreterSelectionState.selectedPythonPath).thenReturn(pythonInterpreter.path); + when(interpreterConfiguration.areDependenciesInstalled(pythonInterpreter, anything())).thenResolve(true); + const initialInterpreter = await jupyterInterpreterService.setInitialInterpreter(undefined); + assert.equal(initialInterpreter, pythonInterpreter); + }); + test('setInitialInterpreter saved interpreter invalid, clear it and use active interpreter', async () => { + when(oldVersionCacheStateStore.getCachedInterpreterPath()).thenReturn(undefined); + when(interpreterSelectionState.selectedPythonPath).thenReturn(secondPythonInterpreter.path); + when(interpreterConfiguration.areDependenciesInstalled(secondPythonInterpreter, anything())).thenResolve(false); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(pythonInterpreter); + when(interpreterConfiguration.areDependenciesInstalled(pythonInterpreter, anything())).thenResolve(true); + const initialInterpreter = await jupyterInterpreterService.setInitialInterpreter(undefined); + assert.equal(initialInterpreter, pythonInterpreter); + // Make sure we set our saved interpreter to the new active interpreter + // it should have been cleared to undefined, then set to a new value + verify(interpreterSelectionState.updateSelectedPythonPath(undefined)).once(); + verify(interpreterSelectionState.updateSelectedPythonPath(anyString())).once(); + }); + test('Install missing dependencies into active interpreter', async () => { + when(interpreterService.getActiveInterpreter(anything())).thenResolve(pythonInterpreter); + await jupyterInterpreterService.installMissingDependencies(undefined); + verify(interpreterConfiguration.installMissingDependencies(pythonInterpreter, undefined)).once(); + }); + test('Install missing dependencies into jupyter interpreter', async () => { + when(interpreterService.getActiveInterpreter(anything())).thenResolve(undefined); + when(interpreterSelector.selectInterpreter()).thenResolve(selectedJupyterInterpreter); + when( + interpreterConfiguration.installMissingDependencies(selectedJupyterInterpreter, anything(), anything()) + ).thenResolve(JupyterInterpreterDependencyResponse.ok); + // First select our interpreter + await jupyterInterpreterService.selectInterpreter(); + await jupyterInterpreterService.installMissingDependencies(undefined); + verify(interpreterConfiguration.installMissingDependencies(selectedJupyterInterpreter, undefined)).once(); + }); + test('Display picker if no interpreters are selected', async () => { + when(interpreterService.getActiveInterpreter(undefined)).thenResolve(undefined); + when(interpreterSelector.selectInterpreter()).thenResolve(selectedJupyterInterpreter); + when( + interpreterConfiguration.installMissingDependencies(selectedJupyterInterpreter, anything(), anything()) + ).thenResolve(JupyterInterpreterDependencyResponse.ok); + await jupyterInterpreterService.installMissingDependencies(undefined); + verify(interpreterSelector.selectInterpreter()).once(); + }); +}); diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterStateStore.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterStateStore.unit.test.ts new file mode 100644 index 000000000000..4b1d880e25ca --- /dev/null +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterStateStore.unit.test.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { EventEmitter, Memento } from 'vscode'; +import { JupyterInterpreterService } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterService'; +import { JupyterInterpreterStateStore } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterStateStore'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { MockMemento } from '../../../mocks/mementos'; + +suite('DataScience - Jupyter Interpreter State', () => { + let selected: JupyterInterpreterStateStore; + let memento: Memento; + let interpreterService: JupyterInterpreterService; + let interpreterSelectedEventEmitter: EventEmitter<PythonEnvironment>; + + setup(() => { + memento = mock(MockMemento); + interpreterService = mock(JupyterInterpreterService); + when(memento.update(anything(), anything())).thenResolve(); + interpreterSelectedEventEmitter = new EventEmitter<PythonEnvironment>(); + when(interpreterService.onDidChangeInterpreter).thenReturn(interpreterSelectedEventEmitter.event); + selected = new JupyterInterpreterStateStore(instance(memento)); + }); + + test('Interpeter should not be set for fresh installs', async () => { + when(memento.get(anything(), false)).thenReturn(false); + + assert.isFalse(selected.interpreterSetAtleastOnce); + }); + test('If memento is set (for subsequent sesssions), return true', async () => { + when(memento.get<string | undefined>(anything(), undefined)).thenReturn('jupyter.exe'); + + assert.isOk(selected.interpreterSetAtleastOnce); + }); + test('Get python path from memento', async () => { + when(memento.get<string | undefined>(anything(), undefined)).thenReturn('jupyter.exe'); + + assert.equal(selected.selectedPythonPath, 'jupyter.exe'); + }); +}); diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.unit.test.ts new file mode 100644 index 000000000000..a95b8b8f79a4 --- /dev/null +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.unit.test.ts @@ -0,0 +1,432 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect, use } from 'chai'; +import * as chaiPromise from 'chai-as-promised'; +import * as path from 'path'; +import { Subject } from 'rxjs/Subject'; +import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { PYTHON_LANGUAGE } from '../../../../client/common/constants'; +import { ProductNames } from '../../../../client/common/installer/productNames'; +import { PathUtils } from '../../../../client/common/platform/pathUtils'; +import { PythonExecutionFactory } from '../../../../client/common/process/pythonExecutionFactory'; +import { + IPythonDaemonExecutionService, + ObservableExecutionResult, + Output +} from '../../../../client/common/process/types'; +import { Product } from '../../../../client/common/types'; +import { DataScience } from '../../../../client/common/utils/localize'; +import { noop } from '../../../../client/common/utils/misc'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { JupyterDaemonModule } from '../../../../client/datascience/constants'; +import { DataScienceFileSystem } from '../../../../client/datascience/dataScienceFileSystem'; +import { JupyterInterpreterDependencyService } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService'; +import { JupyterInterpreterService } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterService'; +import { JupyterInterpreterSubCommandExecutionService } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService'; +import { JupyterServerInfo } from '../../../../client/datascience/jupyter/jupyterConnection'; +import { IDataScienceFileSystem } from '../../../../client/datascience/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../../client/interpreter/interpreterService'; +import { MockOutputChannel } from '../../../mockClasses'; +import { createPythonInterpreter } from '../../../utils/interpreters'; +use(chaiPromise); + +// tslint:disable: max-func-body-length + +suite('DataScience - Jupyter InterpreterSubCommandExecutionService', () => { + let jupyterInterpreter: JupyterInterpreterService; + let interperterService: IInterpreterService; + let jupyterDependencyService: JupyterInterpreterDependencyService; + let fs: IDataScienceFileSystem; + let execService: IPythonDaemonExecutionService; + let jupyterInterpreterExecutionService: JupyterInterpreterSubCommandExecutionService; + const selectedJupyterInterpreter = createPythonInterpreter({ displayName: 'JupyterInterpreter' }); + const activePythonInterpreter = createPythonInterpreter({ displayName: 'activePythonInterpreter' }); + let notebookStartResult: ObservableExecutionResult<string>; + setup(() => { + interperterService = mock(InterpreterService); + jupyterInterpreter = mock(JupyterInterpreterService); + jupyterDependencyService = mock(JupyterInterpreterDependencyService); + fs = mock(DataScienceFileSystem); + const execFactory = mock(PythonExecutionFactory); + execService = mock<IPythonDaemonExecutionService>(); + when( + execFactory.createDaemon( + deepEqual({ daemonModule: JupyterDaemonModule, pythonPath: selectedJupyterInterpreter.path }) + ) + ).thenResolve(instance(execService)); + when(execFactory.createActivatedEnvironment(anything())).thenResolve(instance(execService)); + // tslint:disable-next-line: no-any + (instance(execService) as any).then = undefined; + const output = new MockOutputChannel(''); + const pathUtils = mock(PathUtils); + notebookStartResult = { + dispose: noop, + proc: undefined, + out: new Subject<Output<string>>().asObservable() + }; + jupyterInterpreterExecutionService = new JupyterInterpreterSubCommandExecutionService( + instance(jupyterInterpreter), + instance(interperterService), + instance(jupyterDependencyService), + instance(fs), + instance(execFactory), + output, + instance(pathUtils) + ); + + when(execService.execModuleObservable('jupyter', anything(), anything())).thenResolve( + // tslint:disable-next-line: no-any + notebookStartResult as any + ); + when(interperterService.getActiveInterpreter()).thenResolve(activePythonInterpreter); + when(interperterService.getActiveInterpreter(undefined)).thenResolve(activePythonInterpreter); + }); + // tslint:disable-next-line: max-func-body-length + suite('Interpreter is not selected', () => { + setup(() => { + when(jupyterInterpreter.getSelectedInterpreter()).thenResolve(undefined); + when(jupyterInterpreter.getSelectedInterpreter(anything())).thenResolve(undefined); + }); + test('Returns selected interpreter', async () => { + const interpreter = await jupyterInterpreterExecutionService.getSelectedInterpreter(undefined); + assert.isUndefined(interpreter); + }); + test('Notebook is not supported', async () => { + const isSupported = await jupyterInterpreterExecutionService.isNotebookSupported(undefined); + assert.isFalse(isSupported); + }); + test('Export is not supported', async () => { + const isSupported = await jupyterInterpreterExecutionService.isExportSupported(undefined); + assert.isFalse(isSupported); + }); + test('Jupyter cannot be started because no interpreter has been selected', async () => { + when(interperterService.getActiveInterpreter(undefined)).thenResolve(undefined); + const reason = await jupyterInterpreterExecutionService.getReasonForJupyterNotebookNotBeingSupported( + undefined + ); + assert.equal(reason, DataScience.selectJupyterInterpreter()); + }); + test('Jupyter cannot be started because jupyter is not installed', async () => { + const expectedReason = DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter().format( + activePythonInterpreter.displayName!, + ProductNames.get(Product.jupyter)! + ); + when(jupyterDependencyService.getDependenciesNotInstalled(activePythonInterpreter, undefined)).thenResolve([ + Product.jupyter + ]); + const reason = await jupyterInterpreterExecutionService.getReasonForJupyterNotebookNotBeingSupported( + undefined + ); + assert.equal(reason, expectedReason); + }); + test('Jupyter cannot be started because notebook is not installed', async () => { + const expectedReason = DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter().format( + activePythonInterpreter.displayName!, + ProductNames.get(Product.notebook)! + ); + when(jupyterDependencyService.getDependenciesNotInstalled(activePythonInterpreter, undefined)).thenResolve([ + Product.notebook + ]); + const reason = await jupyterInterpreterExecutionService.getReasonForJupyterNotebookNotBeingSupported( + undefined + ); + assert.equal(reason, expectedReason); + }); + test('Cannot start notebook', async () => { + const promise = jupyterInterpreterExecutionService.startNotebook([], {}); + when(jupyterDependencyService.getDependenciesNotInstalled(activePythonInterpreter, undefined)).thenResolve([ + Product.notebook + ]); + + await expect(promise).to.eventually.be.rejectedWith( + DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter().format( + activePythonInterpreter.displayName!, + ProductNames.get(Product.notebook)! + ) + ); + }); + test('Cannot launch notebook file in jupyter notebook', async () => { + const promise = jupyterInterpreterExecutionService.openNotebook('some.ipynb'); + when(jupyterDependencyService.getDependenciesNotInstalled(activePythonInterpreter, undefined)).thenResolve([ + Product.notebook + ]); + + await expect(promise).to.eventually.be.rejectedWith( + DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter().format( + activePythonInterpreter.displayName!, + ProductNames.get(Product.notebook)! + ) + ); + }); + test('Cannot export notebook to python', async () => { + const promise = jupyterInterpreterExecutionService.exportNotebookToPython(Uri.file('somefile.ipynb')); + when(jupyterDependencyService.getDependenciesNotInstalled(activePythonInterpreter, undefined)).thenResolve([ + Product.notebook + ]); + + await expect(promise).to.eventually.be.rejectedWith( + DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter().format( + activePythonInterpreter.displayName!, + ProductNames.get(Product.notebook)! + ) + ); + }); + test('Cannot get a list of running jupyter servers', async () => { + const promise = jupyterInterpreterExecutionService.getRunningJupyterServers(undefined); + when(jupyterDependencyService.getDependenciesNotInstalled(activePythonInterpreter, undefined)).thenResolve([ + Product.notebook + ]); + + await expect(promise).to.eventually.be.rejectedWith( + DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter().format( + activePythonInterpreter.displayName!, + ProductNames.get(Product.notebook)! + ) + ); + }); + test('Cannot get kernelspecs', async () => { + const promise = jupyterInterpreterExecutionService.getKernelSpecs(undefined); + when(jupyterDependencyService.getDependenciesNotInstalled(activePythonInterpreter, undefined)).thenResolve([ + Product.notebook + ]); + + await expect(promise).to.eventually.be.rejectedWith( + DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter().format( + activePythonInterpreter.displayName!, + ProductNames.get(Product.notebook)! + ) + ); + }); + }); + // tslint:disable-next-line: max-func-body-length + suite('Interpreter is selected', () => { + setup(() => { + when(jupyterInterpreter.getSelectedInterpreter()).thenResolve(selectedJupyterInterpreter); + when(jupyterInterpreter.getSelectedInterpreter(anything())).thenResolve(selectedJupyterInterpreter); + }); + test('Returns selected interpreter', async () => { + const interpreter = await jupyterInterpreterExecutionService.getSelectedInterpreter(undefined); + + assert.deepEqual(interpreter, selectedJupyterInterpreter); + }); + test('If ds dependencies are not installed, then notebook is not supported', async () => { + when(jupyterDependencyService.areDependenciesInstalled(selectedJupyterInterpreter, anything())).thenResolve( + false + ); + + const isSupported = await jupyterInterpreterExecutionService.isNotebookSupported(undefined); + + assert.isFalse(isSupported); + }); + test('If ds dependencies are installed, then notebook is supported', async () => { + when(jupyterInterpreter.getSelectedInterpreter(anything())).thenResolve(selectedJupyterInterpreter); + when(jupyterDependencyService.areDependenciesInstalled(selectedJupyterInterpreter, anything())).thenResolve( + true + ); + + const isSupported = await jupyterInterpreterExecutionService.isNotebookSupported(undefined); + + assert.isOk(isSupported); + }); + test('Jupyter cannot be started because jupyter is not installed', async () => { + const expectedReason = DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter().format( + selectedJupyterInterpreter.displayName!, + ProductNames.get(Product.jupyter)! + ); + when( + jupyterDependencyService.getDependenciesNotInstalled(selectedJupyterInterpreter, undefined) + ).thenResolve([Product.jupyter]); + + const reason = await jupyterInterpreterExecutionService.getReasonForJupyterNotebookNotBeingSupported( + undefined + ); + + assert.equal(reason, expectedReason); + }); + test('Jupyter cannot be started because notebook is not installed', async () => { + const expectedReason = DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter().format( + selectedJupyterInterpreter.displayName!, + ProductNames.get(Product.notebook)! + ); + when( + jupyterDependencyService.getDependenciesNotInstalled(selectedJupyterInterpreter, undefined) + ).thenResolve([Product.notebook]); + + const reason = await jupyterInterpreterExecutionService.getReasonForJupyterNotebookNotBeingSupported( + undefined + ); + + assert.equal(reason, expectedReason); + }); + test('Jupyter cannot be started because kernelspec is not available', async () => { + when( + jupyterDependencyService.getDependenciesNotInstalled(selectedJupyterInterpreter, undefined) + ).thenResolve([Product.kernelspec]); + + const reason = await jupyterInterpreterExecutionService.getReasonForJupyterNotebookNotBeingSupported( + undefined + ); + + assert.equal(reason, DataScience.jupyterKernelSpecModuleNotFound().format(selectedJupyterInterpreter.path)); + }); + test('Can start jupyer notebook', async () => { + const output = await jupyterInterpreterExecutionService.startNotebook([], {}); + + assert.isOk(output === notebookStartResult); + const moduleName = capture(execService.execModuleObservable).first()[0]; + const args = capture(execService.execModuleObservable).first()[1]; + assert.equal(moduleName, 'jupyter'); + assert.equal(args[0], 'notebook'); + }); + test('Can launch notebook file in jupyter notebook', async () => { + const file = 'somefile.ipynb'; + when(execService.execModule('jupyter', anything(), anything())).thenResolve(); + + await jupyterInterpreterExecutionService.openNotebook(file); + + verify( + execService.execModule( + 'jupyter', + deepEqual(['notebook', `--NotebookApp.file_to_run=${file}`]), + anything() + ) + ).once(); + }); + test('Cannot export notebook to python if module is not installed', async () => { + const file = 'somefile.ipynb'; + when(jupyterDependencyService.isExportSupported(selectedJupyterInterpreter, anything())).thenResolve(false); + + const promise = jupyterInterpreterExecutionService.exportNotebookToPython(Uri.file(file)); + + await expect(promise).to.eventually.be.rejectedWith(DataScience.jupyterNbConvertNotSupported()); + }); + test('Export notebook to python', async () => { + const file = 'somefile.ipynb'; + const uri = Uri.file(file); + const convertOutput = 'converted'; + when(jupyterDependencyService.isExportSupported(selectedJupyterInterpreter, anything())).thenResolve(true); + when( + execService.execModule( + 'jupyter', + deepEqual(['nbconvert', uri.fsPath, '--to', 'python', '--stdout']), + anything() + ) + ).thenResolve({ stdout: convertOutput }); + + const output = await jupyterInterpreterExecutionService.exportNotebookToPython(uri); + + assert.equal(output, convertOutput); + }); + test('Return list of running jupyter servers', async () => { + const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'vscode_datascience_helpers', 'getServerInfo.py'); + const expectedServers: JupyterServerInfo[] = [ + { + base_url: '1', + hostname: '111', + notebook_dir: 'a', + password: true, + pid: 1, + port: 1243, + secure: false, + token: 'wow', + url: 'url' + }, + { + base_url: '2', + hostname: '22', + notebook_dir: 'b', + password: false, + pid: 13, + port: 4444, + secure: true, + token: 'wow2', + url: 'url2' + } + ]; + when(execService.exec(deepEqual([file]), anything())).thenResolve({ + stdout: JSON.stringify(expectedServers) + }); + + const servers = await jupyterInterpreterExecutionService.getRunningJupyterServers(undefined); + + assert.deepEqual(servers, expectedServers); + }); + test('Return list of kernelspecs (from daemon)', async () => { + const kernelSpecs = { + K1: { + resource_dir: 'dir1', + spec: { + argv: [], + display_name: 'disp1', + language: PYTHON_LANGUAGE, + metadata: { interpreter: { path: 'Some Path', envName: 'MyEnvName' } } + } + }, + K2: { + resource_dir: 'dir2', + spec: { + argv: [], + display_name: 'disp2', + language: PYTHON_LANGUAGE, + metadata: { interpreter: { path: 'Some Path2', envName: 'MyEnvName2' } } + } + } + }; + when(fs.localFileExists(anything())).thenResolve(true); + when( + execService.execModule('jupyter', deepEqual(['kernelspec', 'list', '--json']), anything()) + ).thenResolve({ stdout: JSON.stringify({ kernelspecs: kernelSpecs }) }); + when(execService.exec(anything(), anything())).thenResolve({ stdout: '' }); + + const specs = await jupyterInterpreterExecutionService.getKernelSpecs(undefined); + + assert.equal(specs.length, 2); + assert.equal(specs[0].name, 'K1'); + assert.equal(specs[0].display_name, kernelSpecs.K1.spec.display_name); + assert.equal(specs[1].name, 'K2'); + assert.equal(specs[1].display_name, kernelSpecs.K2.spec.display_name); + }); + test('Return list of kernelspecs (from process exec)', async () => { + const kernelSpecs = { + K1: { + resource_dir: 'dir1', + spec: { + argv: [], + display_name: 'disp1', + language: PYTHON_LANGUAGE, + metadata: { interpreter: { path: 'Some Path', envName: 'MyEnvName' } } + } + }, + K2: { + resource_dir: 'dir2', + spec: { + argv: [], + display_name: 'disp2', + language: PYTHON_LANGUAGE, + metadata: { interpreter: { path: 'Some Path2', envName: 'MyEnvName2' } } + } + } + }; + when(fs.localFileExists(anything())).thenResolve(true); + when(execService.execModule('jupyter', deepEqual(['kernelspec', 'list', '--json']), anything())).thenReject( + new Error('kaboom') + ); + when(execService.exec(anything(), anything())).thenResolve({ + stdout: JSON.stringify({ kernelspecs: kernelSpecs }) + }); + + const specs = await jupyterInterpreterExecutionService.getKernelSpecs(undefined); + + assert.equal(specs.length, 2); + assert.equal(specs[0].name, 'K1'); + assert.equal(specs[0].display_name, kernelSpecs.K1.spec.display_name); + assert.equal(specs[1].name, 'K2'); + assert.equal(specs[1].display_name, kernelSpecs.K2.spec.display_name); + }); + }); +}); diff --git a/src/test/datascience/jupyter/jupyterCellOutputMimeTypeTracker.unit.test.ts b/src/test/datascience/jupyter/jupyterCellOutputMimeTypeTracker.unit.test.ts new file mode 100644 index 000000000000..9c10b3080736 --- /dev/null +++ b/src/test/datascience/jupyter/jupyterCellOutputMimeTypeTracker.unit.test.ts @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { nbformat } from '@jupyterlab/coreutils'; +import * as fakeTimers from '@sinonjs/fake-timers'; +import { expect } from 'chai'; +import { sha256 } from 'hash.js'; +// tslint:disable-next-line: match-default-export-name +import rewiremock from 'rewiremock'; +import { instance, mock, when } from 'ts-mockito'; +import { EventEmitter, Uri } from 'vscode'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { Telemetry } from '../../../client/datascience/constants'; +import { NativeEditor } from '../../../client/datascience/interactive-ipynb/nativeEditor'; +import { CellOutputMimeTypeTracker } from '../../../client/datascience/jupyter/jupyterCellOutputMimeTypeTracker'; +import { NativeEditorProvider } from '../../../client/datascience/notebookStorage/nativeEditorProvider'; +import { CellState, ICell, INotebookEditor, INotebookModel } from '../../../client/datascience/types'; + +suite('DataScience - Cell Output Mimetype Tracker', () => { + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + let outputMimeTypeTracker: CellOutputMimeTypeTracker; + let nativeProvider: NativeEditorProvider; + let openedNotebookEmitter: EventEmitter<INotebookEditor>; + let clock: fakeTimers.InstalledClock; + class Reporter { + public static telemetrySent: [string, Record<string, string>][] = []; + public static expectHashes(props: {}[]) { + const mimeTypeTelemetry = Reporter.telemetrySent.filter( + (item) => item[0] === Telemetry.HashedCellOutputMimeType + ); + expect(mimeTypeTelemetry).to.be.lengthOf(props.length, 'Incorrect number of telemetry messages sent'); + + expect(mimeTypeTelemetry).to.deep.equal( + props.map((prop) => [Telemetry.HashedCellOutputMimeType, prop]), + 'Contents in telemetry do not match' + ); + } + + public sendTelemetryEvent(eventName: string, properties?: {}, _measures?: {}) { + Reporter.telemetrySent.push([eventName, properties!]); + } + } + + setup(async () => { + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + + openedNotebookEmitter = new EventEmitter<INotebookEditor>(); + nativeProvider = mock(NativeEditorProvider); + when(nativeProvider.onDidOpenNotebookEditor).thenReturn(openedNotebookEmitter.event); + when(nativeProvider.editors).thenReturn([]); + + rewiremock.enable(); + rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + + outputMimeTypeTracker = new CellOutputMimeTypeTracker(instance(nativeProvider)); + clock = fakeTimers.install(); + await outputMimeTypeTracker.activate(); + }); + teardown(() => { + clock.uninstall(); + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + Reporter.telemetrySent = []; + rewiremock.disable(); + }); + + function emitNotebookEvent(cells: ICell[]) { + const notebook = mock(NativeEditor); + const model = mock<INotebookModel>(); + + when(notebook.file).thenReturn(Uri.file('wow')); + when(notebook.model).thenReturn(instance(model)); + when(model.cells).thenReturn(cells); + + openedNotebookEmitter.fire(instance(notebook)); + } + + function generateCellWithOutput(outputs: nbformat.IOutput[]): ICell { + return { + data: { + cell_type: 'code', + source: '', + execution_count: 1, + metadata: {}, + outputs + }, + file: new Date().getTime().toString(), + id: new Date().getTime().toString(), + line: 1, + state: CellState.init + }; + } + function generateTextOutput(output_type: string) { + return { data: { 'text/html': '' }, output_type }; + } + function generateErrorOutput() { + return { output_type: 'error' }; + } + function generateStreamedOutput() { + return { output_type: 'stream' }; + } + function generateSvgOutput(output_type: string) { + return { data: { 'application/svg+xml': '' }, output_type }; + } + function generatePlotlyOutput(output_type: string) { + return { data: { 'application/vnd.plotly.v1+json': '' }, output_type }; + } + function generatePlotlyWithTextOutput(output_type: string) { + return { data: { 'application/vnd.plotly.v1+json': '', 'text/html': '' }, output_type }; + } + function generateTelemetry(mimeType: string) { + const hashedName = sha256().update(mimeType).digest('hex'); + + const lowerMimeType = mimeType.toLowerCase(); + return { + hashedName, + hasText: lowerMimeType.includes('text').toString(), + hasLatex: lowerMimeType.includes('latex').toString(), + hasHtml: lowerMimeType.includes('html').toString(), + hasSvg: lowerMimeType.includes('svg').toString(), + hasXml: lowerMimeType.includes('xml').toString(), + hasJson: lowerMimeType.includes('json').toString(), + hasImage: lowerMimeType.includes('image').toString(), + hasGeo: lowerMimeType.includes('geo').toString(), + hasPlotly: lowerMimeType.includes('plotly').toString(), + hasVega: lowerMimeType.includes('vega').toString(), + hasWidget: lowerMimeType.includes('widget').toString(), + hasJupyter: lowerMimeType.includes('jupyter').toString(), + hasVnd: lowerMimeType.includes('vnd').toString() + }; + } + test('Send telemetry for cell with streamed output', async () => { + const expectedTelemetry = generateTelemetry('stream'); + const cellTextOutput = generateCellWithOutput([generateStreamedOutput()]); + + emitNotebookEvent([cellTextOutput]); + + await clock.runAllAsync(); + Reporter.expectHashes([expectedTelemetry]); + }); + test('Send telemetry even if output type is unknown', async () => { + const expectedTelemetry = generateTelemetry('unrecognized_cell_output'); + const cellTextOutput = generateCellWithOutput([generateTextOutput('unknown_output_type')]); + + emitNotebookEvent([cellTextOutput]); + + await clock.runAllAsync(); + Reporter.expectHashes([expectedTelemetry]); + }); + test('Send telemetry if output type is markdown', async () => { + const expectedTelemetry = generateTelemetry('markdown'); + const cellTextOutput = generateCellWithOutput([generateStreamedOutput()]); + cellTextOutput.data.cell_type = 'markdown'; + + emitNotebookEvent([cellTextOutput]); + + await clock.runAllAsync(); + Reporter.expectHashes([expectedTelemetry]); + }); + suite('No telemetry sent', () => { + test('If cell has error output', async () => { + const cellTextOutput = generateCellWithOutput([generateErrorOutput()]); + + emitNotebookEvent([cellTextOutput]); + + await clock.runAllAsync(); + Reporter.expectHashes([]); + }); + test('If cell type is not code', async () => { + const cellTextOutput = generateCellWithOutput([generateStreamedOutput()]); + cellTextOutput.data.cell_type = 'messages'; + + emitNotebookEvent([cellTextOutput]); + + await clock.runAllAsync(); + Reporter.expectHashes([]); + }); + test('If there is no output', async () => { + const cellTextOutput = generateCellWithOutput([]); + + emitNotebookEvent([cellTextOutput]); + + await clock.runAllAsync(); + Reporter.expectHashes([]); + }); + [CellState.editing, CellState.error, CellState.executing].forEach((cellState) => { + const cellStateValues = getNamesAndValues(CellState); + test(`If cell state is '${cellStateValues.find((item) => item.value === cellState)?.name}'`, async () => { + const cellTextOutput = generateCellWithOutput([generateStreamedOutput()]); + cellTextOutput.state = cellState; + + emitNotebookEvent([cellTextOutput]); + + await clock.runAllAsync(); + Reporter.expectHashes([]); + }); + }); + }); + test('Send telemetry once for multiple cells with multiple outputs', async () => { + // Even if we have 2,3 cells, each with multiple text output, send telemetry once for each mime type. + const expectedTelemetry = [ + generateTelemetry('text/html'), + generateTelemetry('application/svg+xml'), + generateTelemetry('application/vnd.plotly.v1+json'), + generateTelemetry('stream') + ]; + const cell1 = generateCellWithOutput([ + generateTextOutput('display_data'), + generateSvgOutput('update_display_data'), + generatePlotlyWithTextOutput('execute_result') + ]); + const cell2 = generateCellWithOutput([generateErrorOutput()]); + const cell3 = generateCellWithOutput([]); + const cell4 = generateCellWithOutput([generateStreamedOutput()]); + + emitNotebookEvent([cell1, cell2, cell3, cell4]); + + await clock.runAllAsync(); + Reporter.expectHashes(expectedTelemetry); + }); + ['display_data', 'update_display_data', 'execute_result'].forEach((outputType) => { + suite(`Send Telemetry for Output Type = ${outputType}`, () => { + test('MimeType text/html', async () => { + const expectedTelemetry = generateTelemetry('text/html'); + const cellTextOutput = generateCellWithOutput([generateTextOutput(outputType)]); + + emitNotebookEvent([cellTextOutput]); + + await clock.runAllAsync(); + Reporter.expectHashes([expectedTelemetry]); + }); + test('MimeType plotly', async () => { + const expectedTelemetry = generateTelemetry('application/vnd.plotly.v1+json'); + const cellTextOutput = generateCellWithOutput([generatePlotlyOutput(outputType)]); + + emitNotebookEvent([cellTextOutput]); + + await clock.runAllAsync(); + Reporter.expectHashes([expectedTelemetry]); + }); + test('Multiple mime types', async () => { + const expectedTelemetry = generateTelemetry('application/vnd.plotly.v1+json'); + const expectedPlainTextTelemetry = generateTelemetry('text/html'); + const cellTextOutput = generateCellWithOutput([generatePlotlyWithTextOutput(outputType)]); + + emitNotebookEvent([cellTextOutput]); + + await clock.runAllAsync(); + Reporter.expectHashes([expectedTelemetry, expectedPlainTextTelemetry]); + }); + test('Multiple cells and multiple text/html', async () => { + // Even if we have 2,3 cells, each with multiple text output, send telemetry once for each mime type. + const expectedTelemetry = generateTelemetry('text/html'); + const cellTextOutput = generateCellWithOutput([ + generateTextOutput(outputType), + generateTextOutput(outputType) + ]); + + emitNotebookEvent([cellTextOutput, cellTextOutput]); + + await clock.runAllAsync(); + Reporter.expectHashes([expectedTelemetry]); + }); + }); + }); +}); diff --git a/src/test/datascience/jupyter/jupyterConnection.unit.test.ts b/src/test/datascience/jupyter/jupyterConnection.unit.test.ts new file mode 100644 index 000000000000..9c0ba31f988b --- /dev/null +++ b/src/test/datascience/jupyter/jupyterConnection.unit.test.ts @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert } from 'chai'; +import * as events from 'events'; +import { Subject } from 'rxjs/Subject'; +import * as sinon from 'sinon'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { CancellationToken } from 'vscode'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { ObservableExecutionResult, Output } from '../../../client/common/process/types'; +import { IConfigurationService, IDataScienceSettings } from '../../../client/common/types'; +import { DataScience } from '../../../client/common/utils/localize'; +import { noop } from '../../../client/common/utils/misc'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { DataScienceFileSystem } from '../../../client/datascience/dataScienceFileSystem'; +import { JupyterConnectionWaiter, JupyterServerInfo } from '../../../client/datascience/jupyter/jupyterConnection'; +import { IDataScienceFileSystem } from '../../../client/datascience/types'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable: max-func-body-length +suite('DataScience - JupyterConnection', () => { + let observableOutput: Subject<Output<string>>; + let launchResult: ObservableExecutionResult<string>; + let getServerInfoStub: sinon.SinonStub<[CancellationToken | undefined], JupyterServerInfo[] | undefined>; + let configService: IConfigurationService; + let fs: IDataScienceFileSystem; + let serviceContainer: IServiceContainer; + // tslint:disable-next-line: no-any + const dsSettings: IDataScienceSettings = { jupyterLaunchTimeout: 10_000 } as any; + const childProc = new events.EventEmitter(); + const notebookDir = 'someDir'; + const dummyServerInfos: JupyterServerInfo[] = [ + { + base_url: '1', + hostname: '111', + notebook_dir: 'a', + password: true, + pid: 1, + port: 1243, + secure: false, + token: 'wow', + url: 'url' + }, + { + base_url: '2', + hostname: '22', + notebook_dir: notebookDir, + password: false, + pid: 13, + port: 4444, + secure: true, + token: 'wow2', + url: 'url2' + }, + { + base_url: '22', + hostname: '33', + notebook_dir: 'c', + password: false, + pid: 15, + port: 555, + secure: true, + token: 'wow3', + url: 'url23' + } + ]; + const expectedServerInfo = dummyServerInfos[1]; + + setup(() => { + observableOutput = new Subject<Output<string>>(); + launchResult = { + dispose: noop, + out: observableOutput, + // tslint:disable-next-line: no-any + proc: childProc as any + }; + getServerInfoStub = sinon.stub<[CancellationToken | undefined], JupyterServerInfo[] | undefined>(); + serviceContainer = mock(ServiceContainer); + fs = mock(DataScienceFileSystem); + configService = mock(ConfigurationService); + const settings = mock(PythonSettings); + getServerInfoStub.resolves(dummyServerInfos); + when(fs.areLocalPathsSame(anything(), anything())).thenCall((path1, path2) => path1 === path2); + when(settings.datascience).thenReturn(dsSettings); + when(configService.getSettings(anything())).thenReturn(instance(settings)); + when(serviceContainer.get<IDataScienceFileSystem>(IDataScienceFileSystem)).thenReturn(instance(fs)); + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(configService)); + }); + + function createConnectionWaiter(cancelToken?: CancellationToken) { + return new JupyterConnectionWaiter( + launchResult, + notebookDir, + EXTENSION_ROOT_DIR, + // tslint:disable-next-line: no-any + getServerInfoStub as any, + instance(serviceContainer), + cancelToken + ); + } + test('Successfully gets connection info', async () => { + dsSettings.jupyterLaunchTimeout = 10_000; + const waiter = createConnectionWaiter(); + observableOutput.next({ source: 'stderr', out: 'Jupyter listening on http://123.123.123:8888' }); + + const connection = await waiter.waitForConnection(); + + assert.equal(connection.localLaunch, true); + assert.equal(connection.localProcExitCode, undefined); + assert.equal(connection.baseUrl, expectedServerInfo.url); + assert.equal(connection.hostName, expectedServerInfo.hostname); + assert.equal(connection.token, expectedServerInfo.token); + }); + test('Disconnect event is fired in connection', async () => { + dsSettings.jupyterLaunchTimeout = 10_000; + const waiter = createConnectionWaiter(); + observableOutput.next({ source: 'stderr', out: 'Jupyter listening on http://123.123.123:8888' }); + let disconnected = false; + + const connection = await waiter.waitForConnection(); + connection.disconnected(() => (disconnected = true)); + + childProc.emit('exit', 999); + + assert.isTrue(disconnected); + assert.equal(connection.localProcExitCode, 999); + }); + test('Throw timeout error', async () => { + dsSettings.jupyterLaunchTimeout = 10; + const waiter = createConnectionWaiter(); + + const promise = waiter.waitForConnection(); + + await assert.isRejected(promise, DataScience.jupyterLaunchTimedOut()); + }); + test('Throw crashed error', async () => { + const exitCode = 999; + const waiter = createConnectionWaiter(); + + const promise = waiter.waitForConnection(); + childProc.emit('exit', exitCode); + observableOutput.complete(); + + await assert.isRejected(promise, DataScience.jupyterServerCrashed().format(exitCode.toString())); + }); +}); diff --git a/src/test/datascience/jupyter/jupyterSession.unit.test.ts b/src/test/datascience/jupyter/jupyterSession.unit.test.ts new file mode 100644 index 000000000000..5d29085b51d7 --- /dev/null +++ b/src/test/datascience/jupyter/jupyterSession.unit.test.ts @@ -0,0 +1,381 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { + ContentsManager, + Kernel, + KernelMessage, + ServerConnection, + Session, + SessionManager +} from '@jupyterlab/services'; +import { DefaultKernel } from '@jupyterlab/services/lib/kernel/default'; +import { DefaultSession } from '@jupyterlab/services/lib/session/default'; +import { ISignal, Signal } from '@phosphor/commands/node_modules/@phosphor/signaling'; +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; + +import { traceInfo } from '../../../client/common/logger'; +import { createDeferred, Deferred } from '../../../client/common/utils/async'; +import { DataScience } from '../../../client/common/utils/localize'; +import { noop } from '../../../client/common/utils/misc'; +import { JupyterSession } from '../../../client/datascience/jupyter/jupyterSession'; +import { KernelConnectionMetadata, LiveKernelModel } from '../../../client/datascience/jupyter/kernels/types'; +import { IJupyterConnection, IJupyterKernelSpec } from '../../../client/datascience/types'; +import { MockOutputChannel } from '../../mockClasses'; + +// tslint:disable: max-func-body-length no-any +suite('DataScience - JupyterSession', () => { + type ISession = Session.ISession & { + /** + * Whether this is a remote session that we attached to. + * + * @type {boolean} + */ + isRemoteSession?: boolean; + }; + interface IKernelChangedArgs { + /** + * The old kernel. + */ + oldValue: Kernel.IKernelConnection | null; + /** + * The new kernel. + */ + newValue: Kernel.IKernelConnection | null; + } + + let jupyterSession: JupyterSession; + let restartSessionCreatedEvent: Deferred<void>; + let restartSessionUsedEvent: Deferred<void>; + let connection: IJupyterConnection; + let serverSettings: typemoq.IMock<ServerConnection.ISettings>; + let mockKernelSpec: typemoq.IMock<KernelConnectionMetadata>; + let sessionManager: SessionManager; + let contentsManager: ContentsManager; + let session: ISession; + let kernel: Kernel.IKernelConnection; + let statusChangedSignal: ISignal<Session.ISession, Kernel.Status>; + let kernelChangedSignal: ISignal<Session.ISession, IKernelChangedArgs>; + + setup(() => { + restartSessionCreatedEvent = createDeferred(); + restartSessionUsedEvent = createDeferred(); + connection = mock<IJupyterConnection>(); + serverSettings = typemoq.Mock.ofType<ServerConnection.ISettings>(); + mockKernelSpec = typemoq.Mock.ofType<KernelConnectionMetadata>(); + session = mock(DefaultSession); + kernel = mock(DefaultKernel); + when(session.kernel).thenReturn(instance(kernel)); + statusChangedSignal = mock(Signal); + kernelChangedSignal = mock(Signal); + const ioPubSignal = mock(Signal); + when(session.statusChanged).thenReturn(instance(statusChangedSignal)); + when(session.kernelChanged).thenReturn(instance(kernelChangedSignal)); + when(session.iopubMessage).thenReturn(instance(ioPubSignal)); + when(session.kernel).thenReturn(instance(kernel)); + when(kernel.status).thenReturn('idle'); + when(connection.rootDirectory).thenReturn(''); + const channel = new MockOutputChannel('JUPYTER'); + // tslint:disable-next-line: no-any + (instance(session) as any).then = undefined; + sessionManager = mock(SessionManager); + contentsManager = mock(ContentsManager); + jupyterSession = new JupyterSession( + instance(connection), + serverSettings.object, + mockKernelSpec.object, + instance(sessionManager), + instance(contentsManager), + channel, + () => { + restartSessionCreatedEvent.resolve(); + }, + () => { + restartSessionUsedEvent.resolve(); + }, + '', + 60_000 + ); + }); + + async function connect() { + const nbFile = 'file path'; + // tslint:disable-next-line: no-any + when(contentsManager.newUntitled(anything())).thenResolve({ path: nbFile } as any); + // tslint:disable-next-line: no-any + when(contentsManager.rename(anything(), anything())).thenResolve({ path: nbFile } as any); + when(contentsManager.delete(anything())).thenResolve(); + when(sessionManager.startNew(anything())).thenResolve(instance(session)); + const specOrModel = { name: 'some name', id: undefined } as any; + (mockKernelSpec as any).setup((k: any) => k.kernelModel).returns(() => specOrModel); + (mockKernelSpec as any).setup((k: any) => k.kernelSpec).returns(() => specOrModel); + + await jupyterSession.connect(100); + } + + test('Start a session when connecting', async () => { + await connect(); + + assert.isTrue(jupyterSession.isConnected); + verify(sessionManager.startNew(anything())).once(); + verify(contentsManager.newUntitled(anything())).once(); + }); + + test('Shutdown when disposing', async () => { + const shutdown = sinon.stub(jupyterSession, 'shutdown'); + shutdown.resolves(); + + await jupyterSession.dispose(); + + assert.isTrue(shutdown.calledOnce); + }); + + suite('After connecting', () => { + setup(connect); + test('Interrupting will result in kernel being interrupted', async () => { + when(kernel.interrupt()).thenResolve(); + + await jupyterSession.interrupt(1000); + + verify(kernel.interrupt()).once(); + }); + suite('Shutdown', () => { + test('Remote session', async () => { + when(connection.localLaunch).thenReturn(false); + when(sessionManager.refreshRunning()).thenResolve(); + when(session.isRemoteSession).thenReturn(true); + when(session.shutdown()).thenResolve(); + when(session.dispose()).thenReturn(); + + await jupyterSession.shutdown(); + + verify(sessionManager.refreshRunning()).never(); + verify(contentsManager.delete(anything())).once(); + // With remote sessions, do not shutdown the remote session. + verify(session.shutdown()).never(); + // With remote sessions, we should not shut the session, but dispose it. + verify(session.dispose()).once(); + }); + test('Local session', async () => { + when(connection.localLaunch).thenReturn(true); + when(session.isRemoteSession).thenReturn(false); + when(session.isDisposed).thenReturn(false); + when(session.shutdown()).thenResolve(); + when(session.dispose()).thenReturn(); + await jupyterSession.shutdown(); + + verify(sessionManager.refreshRunning()).never(); + verify(contentsManager.delete(anything())).once(); + // always kill the sessions. + verify(session.shutdown()).once(); + verify(session.dispose()).once(); + }); + }); + suite('Wait for session idle', () => { + test('Will timeout', async () => { + when(kernel.status).thenReturn('unknown'); + + const promise = jupyterSession.waitForIdle(100); + + await assert.isRejected(promise, DataScience.jupyterLaunchTimedOut()); + }); + test('Will succeed', async () => { + when(kernel.status).thenReturn('idle'); + + await jupyterSession.waitForIdle(100); + + verify(kernel.status).atLeast(1); + }); + }); + suite('Remote Sessions', async () => { + let restartCount = 0; + const newActiveRemoteKernel: LiveKernelModel = { + argv: [], + display_name: 'new kernel', + language: 'python', + name: 'newkernel', + path: 'path', + lastActivityTime: new Date(), + numberOfConnections: 1, + session: { + statusChanged: { + connect: noop, + disconnect: noop + }, + kernelChanged: { + connect: noop, + disconnect: noop + }, + iopubMessage: { + connect: noop, + disconnect: noop + }, + kernel: { + status: 'idle', + restart: () => (restartCount = restartCount + 1), + registerCommTarget: noop + }, + shutdown: () => Promise.resolve(), + isRemoteSession: false + // tslint:disable-next-line: no-any + } as any, + id: 'liveKernel' + }; + let remoteSession: ISession; + let remoteKernel: Kernel.IKernelConnection; + let remoteSessionInstance: ISession; + setup(() => { + remoteSession = mock(DefaultSession); + remoteKernel = mock(DefaultKernel); + remoteSessionInstance = instance(remoteSession); + remoteSessionInstance.isRemoteSession = false; + when(remoteSession.kernel).thenReturn(instance(remoteKernel)); + when(remoteKernel.registerCommTarget(anything(), anything())).thenReturn(); + when(sessionManager.startNew(anything())).thenCall(() => { + return Promise.resolve(instance(remoteSession)); + }); + }); + suite('Switching kernels', () => { + setup(async () => { + const signal = mock(Signal); + when(remoteSession.statusChanged).thenReturn(instance(signal)); + verify(sessionManager.startNew(anything())).once(); + when(sessionManager.connectTo(newActiveRemoteKernel.session)).thenReturn( + // tslint:disable-next-line: no-any + newActiveRemoteKernel.session as any + ); + + assert.isFalse(remoteSessionInstance.isRemoteSession); + await jupyterSession.changeKernel( + { kernelModel: newActiveRemoteKernel, kind: 'connectToLiveKernel' }, + 10000 + ); + }); + test('Will shutdown to old session', async () => { + verify(session.shutdown()).once(); + }); + test('Will connect to existing session', async () => { + verify(sessionManager.connectTo(newActiveRemoteKernel.session)).once(); + }); + test('Will flag new session as being remote', async () => { + // Confirm the new session is flagged as remote + assert.isTrue(newActiveRemoteKernel.session.isRemoteSession); + }); + test('Will not create a new session', async () => { + verify(sessionManager.startNew(anything())).once(); + }); + test('Restart should restart the new remote kernel', async () => { + when(remoteKernel.restart()).thenResolve(); + + await jupyterSession.restart(0); + + // We should restart the kernel, not the session. + assert.equal(restartCount, 1, 'Did not restart the kernel'); + verify(remoteSession.shutdown()).never(); + verify(remoteSession.dispose()).never(); + }); + }); + }); + suite('Local Sessions', async () => { + let newSession: Session.ISession; + let newKernelConnection: Kernel.IKernelConnection; + let newStatusChangedSignal: ISignal<Session.ISession, Kernel.Status>; + let newKernelChangedSignal: ISignal<Session.ISession, IKernelChangedArgs>; + let newSessionCreated: Deferred<void>; + setup(async () => { + newSession = mock(DefaultSession); + newKernelConnection = mock(DefaultKernel); + newStatusChangedSignal = mock(Signal); + newKernelChangedSignal = mock(Signal); + const newIoPubSignal = mock(Signal); + restartSessionCreatedEvent = createDeferred(); + restartSessionUsedEvent = createDeferred(); + when(newSession.statusChanged).thenReturn(instance(newStatusChangedSignal)); + when(newSession.kernelChanged).thenReturn(instance(newKernelChangedSignal)); + when(newSession.iopubMessage).thenReturn(instance(newIoPubSignal)); + // tslint:disable-next-line: no-any + (instance(newSession) as any).then = undefined; + newSessionCreated = createDeferred(); + when(session.isRemoteSession).thenReturn(false); + when(session.isDisposed).thenReturn(false); + when(newKernelConnection.id).thenReturn('restartId'); + when(newKernelConnection.clientId).thenReturn('restartClientId'); + when(newKernelConnection.status).thenReturn('idle'); + when(newSession.kernel).thenReturn(instance(newKernelConnection)); + when(sessionManager.startNew(anything())).thenCall(() => { + newSessionCreated.resolve(); + return Promise.resolve(instance(newSession)); + }); + }); + teardown(() => { + verify(sessionManager.connectTo(anything())).never(); + }); + test('Switching kernels will kill current session and start a new one', async () => { + verify(sessionManager.startNew(anything())).once(); + + const newKernel: IJupyterKernelSpec = { + argv: [], + display_name: 'new kernel', + language: 'python', + name: 'newkernel', + path: 'path', + env: undefined + }; + + await jupyterSession.changeKernel({ kernelSpec: newKernel, kind: 'startUsingKernelSpec' }, 10000); + + // Wait untill a new session has been started. + await newSessionCreated.promise; + // One original, one new session. + verify(sessionManager.startNew(anything())).twice(); + }); + suite('Executing user code', async () => { + setup(executeUserCode); + + async function executeUserCode() { + const future = mock< + Kernel.IFuture<KernelMessage.IShellControlMessage, KernelMessage.IShellControlMessage> + >(); + // tslint:disable-next-line: no-any + when(future.done).thenReturn(Promise.resolve(undefined as any)); + // tslint:disable-next-line: no-any + when(kernel.requestExecute(anything(), anything(), anything())).thenReturn(instance(future) as any); + + const result = jupyterSession.requestExecute({ code: '', allow_stdin: false, silent: false }); + + assert.isOk(result); + await result!.done; + } + + test('Restart should create a new session & kill old session', async () => { + const oldSessionShutDown = createDeferred(); + when(connection.localLaunch).thenReturn(true); + when(session.isRemoteSession).thenReturn(false); + when(session.isDisposed).thenReturn(false); + when(session.shutdown()).thenCall(() => { + oldSessionShutDown.resolve(); + return Promise.resolve(); + }); + when(session.dispose()).thenCall(() => { + traceInfo('Shutting down'); + return Promise.resolve(); + }); + const sessionServerSettings: ServerConnection.ISettings = mock<ServerConnection.ISettings>(); + when(session.serverSettings).thenReturn(instance(sessionServerSettings)); + + await jupyterSession.restart(0); + + // We should kill session and switch to new session, startig a new restart session. + await restartSessionCreatedEvent.promise; + await oldSessionShutDown.promise; + verify(session.shutdown()).once(); + verify(session.dispose()).once(); + // Confirm kernel isn't restarted. + verify(kernel.restart()).never(); + }); + }); + }); + }); +}); diff --git a/src/test/datascience/jupyter/kernels/kernelDependencyService.unit.test.ts b/src/test/datascience/jupyter/kernels/kernelDependencyService.unit.test.ts new file mode 100644 index 000000000000..6002f48bcaa6 --- /dev/null +++ b/src/test/datascience/jupyter/kernels/kernelDependencyService.unit.test.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { IApplicationShell } from '../../../../client/common/application/types'; +import { IInstaller, InstallerResponse, Product } from '../../../../client/common/types'; +import { Common } from '../../../../client/common/utils/localize'; +import { KernelDependencyService } from '../../../../client/datascience/jupyter/kernels/kernelDependencyService'; +import { KernelInterpreterDependencyResponse } from '../../../../client/datascience/types'; +import { createPythonInterpreter } from '../../../utils/interpreters'; + +// tslint:disable: no-any + +// tslint:disable-next-line: max-func-body-length +suite('DataScience - Kernel Dependency Service', () => { + let dependencyService: KernelDependencyService; + let appShell: IApplicationShell; + let installer: IInstaller; + const interpreter = createPythonInterpreter(); + setup(() => { + appShell = mock<IApplicationShell>(); + installer = mock<IInstaller>(); + dependencyService = new KernelDependencyService(instance(appShell), instance(installer)); + }); + test('Check if ipykernel is installed', async () => { + when(installer.isInstalled(Product.ipykernel, interpreter)).thenResolve(true); + + const response = await dependencyService.installMissingDependencies(interpreter); + + assert.equal(response, KernelInterpreterDependencyResponse.ok); + verify(installer.isInstalled(Product.ipykernel, interpreter)).once(); + verify(installer.isInstalled(anything(), anything())).once(); + }); + test('Do not prompt if if ipykernel is installed', async () => { + when(installer.isInstalled(Product.ipykernel, interpreter)).thenResolve(true); + + const response = await dependencyService.installMissingDependencies(interpreter); + + assert.equal(response, KernelInterpreterDependencyResponse.ok); + verify(appShell.showErrorMessage(anything(), anything(), anything())).never(); + }); + test('Prompt if if ipykernel is not installed', async () => { + when(installer.isInstalled(Product.ipykernel, interpreter)).thenResolve(false); + when(appShell.showErrorMessage(anything(), anything())).thenResolve(Common.install() as any); + + const response = await dependencyService.installMissingDependencies(interpreter); + + assert.equal(response, KernelInterpreterDependencyResponse.cancel); + verify(appShell.showErrorMessage(anything(), anything(), anything())).never(); + }); + test('Install ipykernel', async () => { + when(installer.isInstalled(Product.ipykernel, interpreter)).thenResolve(false); + when(installer.install(Product.ipykernel, interpreter, anything())).thenResolve(InstallerResponse.Installed); + when(appShell.showErrorMessage(anything(), anything())).thenResolve(Common.install() as any); + + const response = await dependencyService.installMissingDependencies(interpreter); + + assert.equal(response, KernelInterpreterDependencyResponse.ok); + }); + test('Bubble installation errors', async () => { + when(installer.isInstalled(Product.ipykernel, interpreter)).thenResolve(false); + when(installer.install(Product.ipykernel, interpreter, anything())).thenReject( + new Error('Install failed - kaboom') + ); + when(appShell.showErrorMessage(anything(), anything())).thenResolve(Common.install() as any); + + const promise = dependencyService.installMissingDependencies(interpreter); + + await assert.isRejected(promise, 'Install failed - kaboom'); + }); +}); diff --git a/src/test/datascience/jupyter/kernels/kernelSelections.unit.test.ts b/src/test/datascience/jupyter/kernels/kernelSelections.unit.test.ts new file mode 100644 index 000000000000..784438dfec82 --- /dev/null +++ b/src/test/datascience/jupyter/kernels/kernelSelections.unit.test.ts @@ -0,0 +1,347 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { PYTHON_LANGUAGE } from '../../../../client/common/constants'; +import { PathUtils } from '../../../../client/common/platform/pathUtils'; +import { IPathUtils } from '../../../../client/common/types'; +import * as localize from '../../../../client/common/utils/localize'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { DataScienceFileSystem } from '../../../../client/datascience/dataScienceFileSystem'; +import { JupyterSessionManager } from '../../../../client/datascience/jupyter/jupyterSessionManager'; +import { KernelSelectionProvider } from '../../../../client/datascience/jupyter/kernels/kernelSelections'; +import { KernelService } from '../../../../client/datascience/jupyter/kernels/kernelService'; +import { IKernelSpecQuickPickItem } from '../../../../client/datascience/jupyter/kernels/types'; +import { IKernelFinder } from '../../../../client/datascience/kernel-launcher/types'; +import { + IDataScienceFileSystem, + IJupyterKernel, + IJupyterKernelSpec, + IJupyterSessionManager +} from '../../../../client/datascience/types'; +import { InterpreterSelector } from '../../../../client/interpreter/configuration/interpreterSelector/interpreterSelector'; +import { IInterpreterQuickPickItem, IInterpreterSelector } from '../../../../client/interpreter/configuration/types'; +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; + +// tslint:disable-next-line: max-func-body-length +suite('DataScience - KernelSelections', () => { + let kernelSelectionProvider: KernelSelectionProvider; + let kernelService: KernelService; + let kernelFinder: IKernelFinder; + let interpreterSelector: IInterpreterSelector; + let pathUtils: IPathUtils; + let fs: IDataScienceFileSystem; + let sessionManager: IJupyterSessionManager; + const activePython1KernelModel = { + lastActivityTime: new Date(2011, 11, 10, 12, 15, 0, 0), + numberOfConnections: 10, + name: 'py1' + }; + const activeJuliaKernelModel = { + lastActivityTime: new Date(2001, 1, 1, 12, 15, 0, 0), + numberOfConnections: 10, + name: 'julia' + }; + const python1KernelSpecModel = { + argv: [], + display_name: 'Python display name', + language: PYTHON_LANGUAGE, + name: 'py1', + path: 'somePath', + metadata: {}, + env: {} + }; + const python3KernelSpecModel = { + argv: [], + display_name: 'Python3', + language: PYTHON_LANGUAGE, + name: 'py3', + path: 'somePath3', + metadata: {}, + env: {} + }; + const juliaKernelSpecModel = { + argv: [], + display_name: 'Julia display name', + language: 'julia', + name: 'julia', + path: 'j', + metadata: {}, + env: {} + }; + const rKernelSpecModel = { + argv: [], + display_name: 'R', + language: 'r', + name: 'r', + path: 'r', + metadata: {}, + env: {} + }; + + const allSpecs: IJupyterKernelSpec[] = [ + python1KernelSpecModel, + python3KernelSpecModel, + juliaKernelSpecModel, + rKernelSpecModel + ]; + + const allInterpreters: IInterpreterQuickPickItem[] = [ + { + label: 'Hello1', + interpreter: { + architecture: Architecture.Unknown, + path: 'p1', + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Conda, + displayName: 'Hello1' + }, + path: 'p1', + detail: '<user friendly path>', + description: '' + }, + { + label: 'Hello1', + interpreter: { + architecture: Architecture.Unknown, + path: 'p2', + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Conda, + displayName: 'Hello2' + }, + path: 'p1', + detail: '<user friendly path>', + description: '' + }, + { + label: 'Hello1', + interpreter: { + architecture: Architecture.Unknown, + path: 'p3', + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Conda, + displayName: 'Hello3' + }, + path: 'p1', + detail: '<user friendly path>', + description: '' + } + ]; + + setup(() => { + interpreterSelector = mock(InterpreterSelector); + sessionManager = mock(JupyterSessionManager); + kernelService = mock(KernelService); + kernelFinder = mock<IKernelFinder>(); + fs = mock(DataScienceFileSystem); + pathUtils = mock(PathUtils); + when(pathUtils.getDisplayName(anything())).thenReturn('<user friendly path>'); + when(pathUtils.getDisplayName(anything(), anything())).thenReturn('<user friendly path>'); + kernelSelectionProvider = new KernelSelectionProvider( + instance(kernelService), + instance(interpreterSelector), + instance(fs), + instance(pathUtils), + instance(kernelFinder) + ); + }); + + test('Should return an empty list for remote kernels if there are none', async () => { + when(kernelService.getKernelSpecs(instance(sessionManager), anything())).thenResolve([]); + when(sessionManager.getRunningKernels()).thenResolve([]); + when(sessionManager.getRunningSessions()).thenResolve([]); + + const items = await kernelSelectionProvider.getKernelSelectionsForRemoteSession( + undefined, + instance(sessionManager) + ); + + assert.equal(items.length, 0); + }); + test('Should return a list with the proper details in the quick pick for remote connections', async () => { + const activeKernels: IJupyterKernel[] = [activePython1KernelModel, activeJuliaKernelModel]; + const sessions = activeKernels.map((item, index) => { + return { + id: `sessionId${index}`, + name: 'someSession', + // tslint:disable-next-line: no-any + kernel: { id: `sessionId${index}`, ...(item as any) }, + type: '', + path: '' + }; + }); + when(kernelService.getKernelSpecs(instance(sessionManager), anything())).thenResolve([]); + when(sessionManager.getRunningKernels()).thenResolve(activeKernels); + when(sessionManager.getRunningSessions()).thenResolve(sessions); + when(sessionManager.getKernelSpecs()).thenResolve(allSpecs); + + // Quick pick must contain + // - kernel spec display name + // - selection = kernel model + kernel spec + // - description = last activity and # of connections. + const expectedItems: IKernelSpecQuickPickItem[] = [ + { + label: python1KernelSpecModel.display_name, + // tslint:disable-next-line: no-any + selection: { + interpreter: undefined, + kernelModel: { + ...activePython1KernelModel, + ...python1KernelSpecModel, + id: 'sessionId0', + session: { + id: 'sessionId0', + name: 'someSession', + // tslint:disable-next-line: no-any + kernel: { id: 'sessionId0', ...(activeKernels[0] as any) }, + type: '', + path: '' + // tslint:disable-next-line: no-any + } as any + }, + kind: 'connectToLiveKernel' + }, + detail: '<user friendly path>', + description: localize.DataScience.jupyterSelectURIRunningDetailFormat().format( + activePython1KernelModel.lastActivityTime.toLocaleString(), + activePython1KernelModel.numberOfConnections.toString() + ) + }, + { + label: juliaKernelSpecModel.display_name, + // tslint:disable-next-line: no-any + selection: { + interpreter: undefined, + kernelModel: { + ...activeJuliaKernelModel, + ...juliaKernelSpecModel, + id: 'sessionId1', + session: { + id: 'sessionId1', + name: 'someSession', + // tslint:disable-next-line: no-any + kernel: { id: 'sessionId1', ...(activeKernels[1] as any) }, + type: '', + path: '' + // tslint:disable-next-line: no-any + } as any + }, + kind: 'connectToLiveKernel' + }, + detail: '<user friendly path>', + description: localize.DataScience.jupyterSelectURIRunningDetailFormat().format( + activeJuliaKernelModel.lastActivityTime.toLocaleString(), + activeJuliaKernelModel.numberOfConnections.toString() + ) + } + ]; + expectedItems.sort((a, b) => (a.label === b.label ? 0 : a.label > b.label ? 1 : -1)); + + const items = await kernelSelectionProvider.getKernelSelectionsForRemoteSession( + undefined, + instance(sessionManager) + ); + + verify(sessionManager.getRunningKernels()).once(); + verify(sessionManager.getKernelSpecs()).once(); + assert.deepEqual(items, expectedItems); + }); + test('Should return a list of Local Kernels + Interpreters for local raw connection', async () => { + when(kernelFinder.listKernelSpecs(anything())).thenResolve(allSpecs); + when(interpreterSelector.getSuggestions(undefined)).thenResolve(allInterpreters); + + // Quick pick must contain + // - kernel spec display name + // - selection = kernel model + kernel spec + // - description = last activity and # of connections. + const expectedKernelItems: IKernelSpecQuickPickItem[] = allSpecs.map((item) => { + return { + label: item.display_name, + detail: '<user friendly path>', + selection: { + interpreter: undefined, + kernelModel: undefined, + kernelSpec: item, + kind: 'startUsingKernelSpec' + } + }; + }); + const expectedInterpreterItems: IKernelSpecQuickPickItem[] = allInterpreters.map((item) => { + return { + ...item, + label: item.label, + detail: '<user friendly path>', + description: '', + selection: { + kernelModel: undefined, + interpreter: item.interpreter, + kernelSpec: undefined, + kind: 'startUsingPythonInterpreter' + } + }; + }); + const expectedList = [...expectedKernelItems, ...expectedInterpreterItems]; + expectedList.sort((a, b) => (a.label === b.label ? 0 : a.label > b.label ? 1 : -1)); + + const items = await kernelSelectionProvider.getKernelSelectionsForLocalSession( + undefined, + 'raw', + instance(sessionManager) + ); + + assert.deepEqual(items, expectedList); + }); + test('Should return a list of Local Kernels + Interpreters for local jupyter connection', async () => { + when(sessionManager.getKernelSpecs()).thenResolve(allSpecs); + when(kernelService.getKernelSpecs(anything(), anything())).thenResolve(allSpecs); + when(interpreterSelector.getSuggestions(undefined)).thenResolve(allInterpreters); + + // Quick pick must contain + // - kernel spec display name + // - selection = kernel model + kernel spec + // - description = last activity and # of connections. + const expectedKernelItems: IKernelSpecQuickPickItem[] = allSpecs.map((item) => { + return { + label: item.display_name, + detail: '<user friendly path>', + selection: { + interpreter: undefined, + kernelModel: undefined, + kernelSpec: item, + kind: 'startUsingKernelSpec' + } + }; + }); + const expectedInterpreterItems: IKernelSpecQuickPickItem[] = allInterpreters.map((item) => { + return { + ...item, + label: item.label, + detail: '<user friendly path>', + description: '', + selection: { + kernelModel: undefined, + interpreter: item.interpreter, + kernelSpec: undefined, + kind: 'startUsingPythonInterpreter' + } + }; + }); + const expectedList = [...expectedKernelItems, ...expectedInterpreterItems]; + expectedList.sort((a, b) => (a.label === b.label ? 0 : a.label > b.label ? 1 : -1)); + + const items = await kernelSelectionProvider.getKernelSelectionsForLocalSession( + undefined, + 'jupyter', + instance(sessionManager) + ); + + verify(kernelService.getKernelSpecs(anything(), anything())).once(); + assert.deepEqual(items, expectedList); + }); +}); diff --git a/src/test/datascience/jupyter/kernels/kernelSelector.unit.test.ts b/src/test/datascience/jupyter/kernels/kernelSelector.unit.test.ts new file mode 100644 index 000000000000..515e6c4984de --- /dev/null +++ b/src/test/datascience/jupyter/kernels/kernelSelector.unit.test.ts @@ -0,0 +1,851 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { nbformat } from '@jupyterlab/coreutils'; +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { CancellationToken } from 'vscode-jsonrpc'; + +import type { Kernel } from '@jupyterlab/services'; +import { EventEmitter } from 'vscode'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../../../client/common/application/types'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { PYTHON_LANGUAGE } from '../../../../client/common/constants'; +import { Resource } from '../../../../client/common/types'; +import * as localize from '../../../../client/common/utils/localize'; +import { noop } from '../../../../client/common/utils/misc'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { StopWatch } from '../../../../client/common/utils/stopWatch'; +import { JupyterSessionManager } from '../../../../client/datascience/jupyter/jupyterSessionManager'; +import { JupyterSessionManagerFactory } from '../../../../client/datascience/jupyter/jupyterSessionManagerFactory'; +import { defaultKernelSpecName } from '../../../../client/datascience/jupyter/kernels/helpers'; +import { KernelDependencyService } from '../../../../client/datascience/jupyter/kernels/kernelDependencyService'; +import { KernelSelectionProvider } from '../../../../client/datascience/jupyter/kernels/kernelSelections'; +import { KernelSelector } from '../../../../client/datascience/jupyter/kernels/kernelSelector'; +import { KernelService } from '../../../../client/datascience/jupyter/kernels/kernelService'; +import { + IKernelSpecQuickPickItem, + KernelSpecConnectionMetadata, + LiveKernelConnectionMetadata, + LiveKernelModel +} from '../../../../client/datascience/jupyter/kernels/types'; +import { IKernelFinder } from '../../../../client/datascience/kernel-launcher/types'; +import { IJupyterSessionManager } from '../../../../client/datascience/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../../client/interpreter/interpreterService'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +// tslint:disable: max-func-body-length no-unused-expression no-any + +suite('DataScience - KernelSelector', () => { + let kernelSelectionProvider: KernelSelectionProvider; + let kernelService: KernelService; + let sessionManager: IJupyterSessionManager; + let kernelSelector: KernelSelector; + let interpreterService: IInterpreterService; + let appShell: IApplicationShell; + let dependencyService: KernelDependencyService; + let kernelFinder: IKernelFinder; + const kernelSpec = { + argv: [], + display_name: 'Something', + dispose: async () => noop(), + language: PYTHON_LANGUAGE, + name: 'SomeName', + path: 'somePath', + env: {} + }; + const interpreter: PythonEnvironment = { + displayName: 'Something', + architecture: Architecture.Unknown, + path: 'somePath', + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Conda, + version: { raw: '3.7.1.1', major: 3, minor: 7, patch: 1, build: ['1'], prerelease: [] } + }; + + setup(() => { + sessionManager = mock(JupyterSessionManager); + kernelService = mock(KernelService); + kernelSelectionProvider = mock(KernelSelectionProvider); + appShell = mock(ApplicationShell); + dependencyService = mock(KernelDependencyService); + interpreterService = mock(InterpreterService); + kernelFinder = mock<IKernelFinder>(); + const jupyterSessionManagerFactory = mock(JupyterSessionManagerFactory); + const dummySessionEvent = new EventEmitter<Kernel.IKernelConnection>(); + when(jupyterSessionManagerFactory.onRestartSessionCreated).thenReturn(dummySessionEvent.event); + when(jupyterSessionManagerFactory.onRestartSessionUsed).thenReturn(dummySessionEvent.event); + const configService = mock(ConfigurationService); + kernelSelector = new KernelSelector( + instance(kernelSelectionProvider), + instance(appShell), + instance(kernelService), + instance(interpreterService), + instance(dependencyService), + instance(kernelFinder), + instance(jupyterSessionManagerFactory), + instance(configService), + [] + ); + }); + teardown(() => sinon.restore()); + suite('Select Remote Kernel', () => { + test('Should display quick pick and return nothing when nothing is selected (remote sessions)', async () => { + when( + kernelSelectionProvider.getKernelSelectionsForRemoteSession( + anything(), + instance(sessionManager), + anything() + ) + ).thenResolve([]); + when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve(); + + const kernel = await kernelSelector.selectRemoteKernel( + undefined, + new StopWatch(), + instance(sessionManager) + ); + + assert.isUndefined(kernel); + verify( + kernelSelectionProvider.getKernelSelectionsForRemoteSession( + anything(), + instance(sessionManager), + anything() + ) + ).once(); + verify(appShell.showQuickPick(anything(), anything(), anything())).once(); + }); + test('Should display quick pick and return nothing when nothing is selected (local sessions)', async () => { + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + 'jupyter', + instance(sessionManager), + anything() + ) + ).thenResolve([]); + when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve(); + + const kernel = await kernelSelector.selectLocalKernel( + undefined, + 'jupyter', + new StopWatch(), + instance(sessionManager) + ); + + assert.isUndefined(kernel); + verify( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + 'jupyter', + instance(sessionManager), + anything() + ) + ).once(); + verify(appShell.showQuickPick(anything(), anything(), anything())).once(); + }); + test('Should return the selected remote kernelspec along with a matching interpreter', async () => { + when( + kernelSelectionProvider.getKernelSelectionsForRemoteSession( + anything(), + instance(sessionManager), + anything() + ) + ).thenResolve([]); + when(kernelService.findMatchingInterpreter(kernelSpec, anything())).thenResolve(interpreter); + when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve({ + selection: { kernelSpec } + } as any); + + const kernel = await kernelSelector.selectRemoteKernel( + undefined, + new StopWatch(), + instance(sessionManager) + ); + + assert.deepEqual((kernel as any)?.kernelSpec, kernelSpec); + assert.deepEqual(kernel?.interpreter, interpreter); + verify( + kernelSelectionProvider.getKernelSelectionsForRemoteSession( + anything(), + instance(sessionManager), + anything() + ) + ).once(); + verify(appShell.showQuickPick(anything(), anything(), anything())).once(); + verify(kernelService.findMatchingInterpreter(kernelSpec, anything())).once(); + }); + }); + suite('Hide kernels from Remote & Local Kernel', () => { + test('Should hide kernel from remote sessions', async () => { + const kernelModels: LiveKernelModel[] = [ + { + lastActivityTime: new Date(), + name: '1one', + numberOfConnections: 1, + id: 'id1', + display_name: '1', + session: {} as any + }, + { + lastActivityTime: new Date(), + name: '2two', + numberOfConnections: 1, + id: 'id2', + display_name: '2', + session: {} as any + }, + { + lastActivityTime: new Date(), + name: '3three', + numberOfConnections: 1, + id: 'id3', + display_name: '3', + session: {} as any + }, + { + lastActivityTime: new Date(), + name: '4four', + numberOfConnections: 1, + id: 'id4', + display_name: '4', + session: {} as any + } + ]; + const quickPickItems: IKernelSpecQuickPickItem< + LiveKernelConnectionMetadata | KernelSpecConnectionMetadata + >[] = kernelModels.map((kernelModel) => { + return { + label: '', + selection: { + kernelModel, + kernelSpec: undefined, + interpreter: undefined, + kind: 'connectToLiveKernel' + } + }; + }); + + when( + kernelSelectionProvider.getKernelSelectionsForRemoteSession( + anything(), + instance(sessionManager), + anything() + ) + ).thenResolve(quickPickItems); + when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve(undefined); + + kernelSelector.addKernelToIgnoreList({ id: 'id2' } as any); + kernelSelector.addKernelToIgnoreList({ clientId: 'id4' } as any); + const kernel = await kernelSelector.selectRemoteKernel( + undefined, + new StopWatch(), + instance(sessionManager) + ); + + assert.isUndefined(kernel); + verify( + kernelSelectionProvider.getKernelSelectionsForRemoteSession( + anything(), + instance(sessionManager), + anything() + ) + ).once(); + verify(appShell.showQuickPick(anything(), anything(), anything())).once(); + const suggestions = capture(appShell.showQuickPick).first()[0] as IKernelSpecQuickPickItem[]; + assert.deepEqual( + suggestions, + quickPickItems.filter((item) => !['id2', 'id4'].includes(item.selection?.kernelModel?.id || '')) + ); + }); + }); + suite('Select Local Kernel', () => { + test('Should return the selected local kernelspec along with a matching interpreter', async () => { + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + 'jupyter', + instance(sessionManager), + anything() + ) + ).thenResolve([]); + when(kernelService.findMatchingInterpreter(kernelSpec, anything())).thenResolve(interpreter); + when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve({ + selection: { kernelSpec } + } as any); + + const kernel = await kernelSelector.selectLocalKernel( + undefined, + 'jupyter', + new StopWatch(), + instance(sessionManager) + ); + + assert.deepEqual((kernel as any)?.kernelSpec, kernelSpec); + assert.deepEqual(kernel?.interpreter, interpreter); + verify( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + 'jupyter', + instance(sessionManager), + anything() + ) + ).once(); + verify(appShell.showQuickPick(anything(), anything(), anything())).once(); + verify(kernelService.findMatchingInterpreter(kernelSpec, anything())).once(); + }); + test('If selected interpreter has ipykernel installed, then return matching kernelspec and interpreter', async () => { + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(true); + when(kernelService.findMatchingKernelSpec(interpreter, instance(sessionManager), anything())).thenResolve( + kernelSpec + ); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + 'jupyter', + instance(sessionManager), + anything() + ) + ).thenResolve([]); + when( + appShell.showInformationMessage(localize.DataScience.fallbackToUseActiveInterpreterAsKernel()) + ).thenResolve(); + when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve({ + selection: { interpreter, kernelSpec } + } as any); + + const kernel = await kernelSelector.selectLocalKernel( + undefined, + 'jupyter', + new StopWatch(), + instance(sessionManager) + ); + + assert.deepEqual((kernel as any)?.kernelSpec, kernelSpec); + verify(dependencyService.areDependenciesInstalled(interpreter, anything())).once(); + verify(kernelService.findMatchingKernelSpec(interpreter, instance(sessionManager), anything())).once(); + verify( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + 'jupyter', + instance(sessionManager), + anything() + ) + ).once(); + verify(appShell.showQuickPick(anything(), anything(), anything())).once(); + verify(kernelService.registerKernel(anything(), anything())).never(); + verify( + appShell.showInformationMessage(localize.DataScience.fallbackToUseActiveInterpreterAsKernel()) + ).never(); + verify( + appShell.showInformationMessage(localize.DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel()) + ).never(); + }); + test('If selected interpreter has ipykernel installed and there is no matching kernelSpec, then register a new kernel and return the new kernelspec and interpreter', async () => { + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(true); + when(kernelService.findMatchingKernelSpec(interpreter, instance(sessionManager), anything())).thenResolve(); + when(kernelService.registerKernel(interpreter, anything(), anything())).thenResolve(kernelSpec); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + 'jupyter', + instance(sessionManager), + anything() + ) + ).thenResolve([]); + when( + appShell.showInformationMessage(localize.DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel()) + ).thenResolve(); + when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve({ + selection: { interpreter, kernelSpec } + } as any); + + const kernel = await kernelSelector.selectLocalKernel( + undefined, + 'jupyter', + new StopWatch(), + instance(sessionManager) + ); + + assert.deepEqual((kernel as any)?.kernelSpec, kernelSpec); + assert.deepEqual(kernel?.interpreter, interpreter); + verify(dependencyService.areDependenciesInstalled(interpreter, anything())).once(); + verify(kernelService.findMatchingKernelSpec(interpreter, instance(sessionManager), anything())).once(); + verify( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + 'jupyter', + instance(sessionManager), + anything() + ) + ).twice(); // Once for caching. + verify(appShell.showQuickPick(anything(), anything(), anything())).once(); + verify( + appShell.showInformationMessage(localize.DataScience.fallbackToUseActiveInterpreterAsKernel()) + ).never(); + verify( + appShell.showInformationMessage(localize.DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel()) + ).never(); + }); + test('If selected interpreter does not have ipykernel installed and there is no matching kernelspec, then register a new kernel and return the new kernelspec and interpreter', async () => { + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(false); + when(kernelService.registerKernel(interpreter, anything(), anything())).thenResolve(kernelSpec); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + 'jupyter', + instance(sessionManager), + anything() + ) + ).thenResolve([]); + when( + appShell.showInformationMessage(localize.DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel()) + ).thenResolve(); + when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve({ + selection: { interpreter, kernelSpec } + } as any); + + const kernel = await kernelSelector.selectLocalKernel( + undefined, + 'jupyter', + new StopWatch(), + instance(sessionManager) + ); + + assert.deepEqual((kernel as any)?.kernelSpec, kernelSpec); + verify(dependencyService.areDependenciesInstalled(interpreter, anything())).once(); + verify( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + 'jupyter', + instance(sessionManager), + anything() + ) + ).twice(); // once for caching. + verify(appShell.showQuickPick(anything(), anything(), anything())).once(); + verify(kernelService.findMatchingKernelSpec(interpreter, instance(sessionManager), anything())).never(); + verify(kernelService.registerKernel(interpreter, anything(), anything())).once(); + verify(appShell.showInformationMessage(anything(), anything(), anything())).never(); + verify( + appShell.showInformationMessage(localize.DataScience.fallbackToUseActiveInterpreterAsKernel()) + ).never(); + verify( + appShell.showInformationMessage(localize.DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel()) + ).never(); + }); + test('For a raw connection, if an interpreter is selected return it along with a default kernelspec', async () => { + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(true); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession(anything(), 'raw', anything(), anything()) + ).thenResolve([]); + when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve({ + selection: { interpreter, kernelSpec: undefined } + } as any); + + const kernel = await kernelSelector.selectLocalKernel(undefined, 'raw', new StopWatch()); + + assert.deepEqual(kernel?.interpreter, interpreter); + expect((kernel as any)?.kernelSpec, 'Should have kernelspec').to.not.be.undefined; + expect((kernel as any)?.kernelSpec!.name, 'Spec should have default name').to.include( + defaultKernelSpecName + ); + }); + test('For a raw connection, if a kernel spec is selected return it with the interpreter', async () => { + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(true); + when(kernelService.findMatchingInterpreter(kernelSpec, anything())).thenResolve(interpreter); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession(anything(), 'raw', anything(), anything()) + ).thenResolve([]); + when(appShell.showQuickPick(anything(), anything(), anything())).thenResolve({ + selection: { interpreter: undefined, kernelSpec } + } as any); + const kernel = await kernelSelector.selectLocalKernel(undefined, 'raw', new StopWatch()); + assert.deepEqual((kernel as any)?.kernelSpec, kernelSpec); + assert.deepEqual(kernel?.interpreter, interpreter); + }); + }); + // tslint:disable-next-line: max-func-body-length + suite('Get a kernel for local sessions', () => { + let nbMetadataKernelSpec: nbformat.IKernelspecMetadata = {} as any; + let nbMetadata: nbformat.INotebookMetadata = {} as any; + let selectLocalKernelStub: sinon.SinonStub< + [ + Resource, + 'raw' | 'jupyter' | 'noConnection', + StopWatch, + (IJupyterSessionManager | undefined)?, + (CancellationToken | undefined)?, + string? + ], + Promise<any> + >; + setup(() => { + nbMetadataKernelSpec = { + display_name: interpreter.displayName!, + name: kernelSpec.name + }; + nbMetadata = { + kernelspec: nbMetadataKernelSpec as any, + orig_nbformat: 4, + language_info: { name: PYTHON_LANGUAGE } + }; + selectLocalKernelStub = sinon.stub(KernelSelector.prototype, 'selectLocalKernel'); + selectLocalKernelStub.resolves({ kernelSpec, interpreter }); + }); + teardown(() => sinon.restore()); + test('Raw kernel connection finds a valid kernel spec and interpreter', async () => { + when(kernelFinder.findKernelSpec(anything(), anything(), anything(), anything())).thenResolve(kernelSpec); + when(kernelService.findMatchingInterpreter(kernelSpec, anything())).thenResolve(interpreter); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + anything(), + anything(), + anything() + ) + ).thenResolve(); + + const kernel = await kernelSelector.getPreferredKernelForLocalConnection( + anything(), + 'raw', + undefined, + nbMetadata + ); + + assert.deepEqual((kernel as any).kernelSpec, kernelSpec); + assert.deepEqual(kernel?.interpreter, interpreter); + }); + test('If metadata contains kernel information, then return a matching kernel and a matching interpreter', async () => { + when( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).thenResolve(kernelSpec); + when(kernelService.findMatchingInterpreter(kernelSpec, anything())).thenResolve(interpreter); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + anything(), + anything(), + anything() + ) + ).thenResolve(); + + const kernel = await kernelSelector.getPreferredKernelForLocalConnection( + anything(), + 'jupyter', + instance(sessionManager), + nbMetadata + ); + + assert.deepEqual((kernel as any).kernelSpec, kernelSpec); + assert.deepEqual(kernel?.interpreter, interpreter); + assert.isOk(selectLocalKernelStub.notCalled); + verify( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).once(); + verify(kernelService.findMatchingInterpreter(kernelSpec, anything())).once(); + verify(appShell.showQuickPick(anything(), anything(), anything())).never(); + verify(kernelService.registerKernel(anything(), anything(), anything())).never(); + }); + test('If metadata contains kernel information, then return a matching kernel (even if there is no matching interpreter)', async () => { + when( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).thenResolve(kernelSpec); + when(kernelService.findMatchingInterpreter(kernelSpec, anything())).thenResolve(); + when(interpreterService.getActiveInterpreter(undefined)).thenResolve(interpreter); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + anything(), + anything(), + anything() + ) + ).thenResolve(); + + const kernel = await kernelSelector.getPreferredKernelForLocalConnection( + undefined, + 'jupyter', + instance(sessionManager), + nbMetadata + ); + + assert.deepEqual((kernel as any).kernelSpec, kernelSpec); + assert.isOk(kernel?.interpreter); + assert.isOk(selectLocalKernelStub.notCalled); + verify( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).once(); + verify(kernelService.findMatchingInterpreter(kernelSpec, anything())).once(); + verify(appShell.showQuickPick(anything(), anything(), anything())).never(); + verify(kernelService.registerKernel(anything(), anything(), anything())).never(); + }); + test('If metadata contains kernel information, and there is matching kernelspec, then use current interpreter as a kernel', async () => { + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(false); + when( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).thenResolve(undefined); + when(interpreterService.getActiveInterpreter(undefined)).thenResolve(interpreter); + when(kernelService.registerKernel(anything(), anything(), anything())).thenResolve(kernelSpec); + when( + appShell.showInformationMessage(localize.DataScience.fallbackToUseActiveInterpreterAsKernel()) + ).thenResolve(); + when( + appShell.showInformationMessage( + localize.DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel().format( + nbMetadata.kernelspec?.display_name! + ) + ) + ).thenResolve(); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + anything(), + anything(), + anything() + ) + ).thenResolve(); + + const kernel = await kernelSelector.getPreferredKernelForLocalConnection( + undefined, + 'jupyter', + instance(sessionManager), + nbMetadata + ); + + assert.deepEqual((kernel as any)?.kernelSpec, kernelSpec); + assert.deepEqual(kernel?.interpreter, interpreter); + assert.isOk(selectLocalKernelStub.notCalled); + verify( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).once(); + verify(kernelService.updateKernelEnvironment(interpreter, anything(), anything())).never(); + verify(kernelService.findMatchingInterpreter(kernelSpec, anything())).never(); + verify(appShell.showQuickPick(anything(), anything(), anything())).never(); + verify(kernelService.registerKernel(anything(), anything(), anything())).once(); + verify( + appShell.showInformationMessage( + localize.DataScience.fallBackToPromptToUseActiveInterpreterOrSelectAKernel() + ) + ).never(); + verify( + appShell.showInformationMessage( + localize.DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel().format( + nbMetadata.kernelspec?.display_name! + ) + ) + ).once(); + }); + test('If metadata is empty, then use active interpreter and find a kernel matching active interpreter', async () => { + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(false); + when( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).thenResolve(undefined); + when(interpreterService.getActiveInterpreter(undefined)).thenResolve(interpreter); + when(kernelService.searchAndRegisterKernel(interpreter, anything(), anything())).thenResolve(kernelSpec); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession( + anything(), + anything(), + anything(), + anything() + ) + ).thenResolve(); + + const kernel = await kernelSelector.getPreferredKernelForLocalConnection( + undefined, + 'jupyter', + instance(sessionManager), + undefined + ); + + assert.deepEqual(kernel?.kernelSpec, kernelSpec); + assert.deepEqual(kernel?.interpreter, interpreter); + assert.isOk(selectLocalKernelStub.notCalled); + verify(appShell.showInformationMessage(anything(), anything(), anything())).never(); + verify(kernelService.searchAndRegisterKernel(interpreter, anything(), anything())).once(); + verify( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).never(); + verify(kernelService.findMatchingInterpreter(kernelSpec, anything())).never(); + verify(appShell.showQuickPick(anything(), anything(), anything())).never(); + verify(kernelService.registerKernel(anything(), anything())).never(); + }); + test('Remote search works', async () => { + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(false); + when( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).thenResolve(undefined); + when(kernelService.getKernelSpecs(anything(), anything())).thenResolve([ + { + name: 'bar', + display_name: 'foo', + language: 'c#', + path: '/foo/dotnet', + argv: [], + env: {} + }, + { + name: 'python3', + display_name: 'foo', + language: 'python', + path: '/foo/python', + argv: [], + env: {} + } + ]); + when(interpreterService.getActiveInterpreter(undefined)).thenResolve(interpreter); + when(kernelService.searchAndRegisterKernel(interpreter, anything(), anything())).thenResolve(kernelSpec); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession(anything(), anything(), anything()) + ).thenResolve(); + + const kernel = await kernelSelector.getPreferredKernelForRemoteConnection( + undefined, + instance(sessionManager), + undefined + ); + + assert.ok((kernel as any)?.kernelSpec, 'No kernel spec found for remote'); + assert.equal((kernel as any)?.kernelSpec?.display_name, 'foo', 'Did not find the python kernel spec'); + assert.deepEqual(kernel?.interpreter, interpreter); + assert.isOk(selectLocalKernelStub.notCalled); + verify(appShell.showInformationMessage(anything(), anything(), anything())).never(); + verify(kernelService.searchAndRegisterKernel(interpreter, anything(), anything())).never(); + verify( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).never(); + verify(kernelService.findMatchingInterpreter(kernelSpec, anything())).never(); + verify(appShell.showQuickPick(anything(), anything(), anything())).never(); + verify(kernelService.registerKernel(anything(), anything(), anything())).never(); + }); + test('Remote search prefers same name as long as it is python', async () => { + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(false); + when( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).thenResolve(undefined); + when(kernelService.getKernelSpecs(anything(), anything())).thenResolve([ + { + name: 'bar', + display_name: 'foo', + language: 'CSharp', + path: '/foo/dotnet', + argv: [], + env: {} + }, + { + name: 'foo', + display_name: 'zip', + language: 'Python', + path: '/foo/python', + argv: [], + env: undefined + }, + { + name: 'foo', + display_name: 'foo', + language: 'Python', + path: '/foo/python', + argv: [], + env: undefined + } + ]); + when(interpreterService.getActiveInterpreter(undefined)).thenResolve(interpreter); + when(kernelService.searchAndRegisterKernel(interpreter, anything())).thenResolve(kernelSpec); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession(anything(), anything(), anything()) + ).thenResolve(); + + const kernel = await kernelSelector.getPreferredKernelForRemoteConnection( + undefined, + instance(sessionManager), + { + orig_nbformat: 4, + kernelspec: { display_name: 'foo', name: 'foo' } + } + ); + + assert.ok((kernel as any).kernelSpec, 'No kernel spec found for remote'); + assert.equal( + (kernel as any).kernelSpec?.display_name, + 'foo', + 'Did not find the preferred python kernel spec' + ); + assert.deepEqual(kernel?.interpreter, interpreter); + assert.isOk(selectLocalKernelStub.notCalled); + verify(appShell.showInformationMessage(anything(), anything(), anything())).never(); + verify(kernelService.searchAndRegisterKernel(interpreter, anything())).never(); + verify( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).never(); + verify(kernelService.findMatchingInterpreter(kernelSpec, anything())).never(); + verify(appShell.showQuickPick(anything(), anything(), anything())).never(); + verify(kernelService.registerKernel(anything(), anything())).never(); + }); + test('Remote search prefers same version', async () => { + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(false); + when( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).thenResolve(undefined); + when(kernelService.getKernelSpecs(anything(), anything())).thenResolve([ + { + name: 'bar', + display_name: 'fod', + language: 'CSharp', + path: '/foo/dotnet', + argv: [], + env: {} + }, + { + name: 'python2', + display_name: 'zip', + language: 'Python', + path: '/foo/python', + argv: [], + env: undefined + }, + { + name: 'python3', + display_name: 'foo', + language: 'Python', + path: '/foo/python', + argv: [], + env: undefined + } + ]); + when(interpreterService.getActiveInterpreter(undefined)).thenResolve(interpreter); + when(kernelService.searchAndRegisterKernel(interpreter, anything())).thenResolve(kernelSpec); + when( + kernelSelectionProvider.getKernelSelectionsForLocalSession(anything(), anything(), anything()) + ).thenResolve(); + + const kernel = await kernelSelector.getPreferredKernelForRemoteConnection( + undefined, + instance(sessionManager), + { + orig_nbformat: 4, + kernelspec: { display_name: 'foo', name: 'foo' } + } + ); + + assert.ok((kernel as any).kernelSpec, 'No kernel spec found for remote'); + assert.equal( + (kernel as any).kernelSpec?.display_name, + 'foo', + 'Did not find the preferred python kernel spec' + ); + assert.deepEqual(kernel?.interpreter, interpreter); + assert.isOk(selectLocalKernelStub.notCalled); + verify(appShell.showInformationMessage(anything(), anything(), anything())).never(); + verify(kernelService.searchAndRegisterKernel(interpreter, anything())).never(); + verify( + kernelService.findMatchingKernelSpec(nbMetadataKernelSpec, instance(sessionManager), anything()) + ).never(); + verify(kernelService.findMatchingInterpreter(kernelSpec, anything())).never(); + verify(appShell.showQuickPick(anything(), anything(), anything())).never(); + verify(kernelService.registerKernel(anything(), anything())).never(); + }); + }); +}); diff --git a/src/test/datascience/jupyter/kernels/kernelService.unit.test.ts b/src/test/datascience/jupyter/kernels/kernelService.unit.test.ts new file mode 100644 index 000000000000..fc7eaafec158 --- /dev/null +++ b/src/test/datascience/jupyter/kernels/kernelService.unit.test.ts @@ -0,0 +1,608 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Kernel } from '@jupyterlab/services'; +import { assert } from 'chai'; +import { cloneDeep } from 'lodash'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { CancellationToken } from 'vscode'; +import { PYTHON_LANGUAGE } from '../../../../client/common/constants'; +import { PythonExecutionFactory } from '../../../../client/common/process/pythonExecutionFactory'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; +import { ReadWrite } from '../../../../client/common/types'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { DataScienceFileSystem } from '../../../../client/datascience/dataScienceFileSystem'; +import { JupyterSessionManager } from '../../../../client/datascience/jupyter/jupyterSessionManager'; +import { JupyterKernelSpec } from '../../../../client/datascience/jupyter/kernels/jupyterKernelSpec'; +import { KernelDependencyService } from '../../../../client/datascience/jupyter/kernels/kernelDependencyService'; +import { KernelService } from '../../../../client/datascience/jupyter/kernels/kernelService'; +import { + IDataScienceFileSystem, + IJupyterKernelSpec, + IJupyterSessionManager, + IJupyterSubCommandExecutionService, + KernelInterpreterDependencyResponse +} from '../../../../client/datascience/types'; +import { EnvironmentActivationService } from '../../../../client/interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../../../client/interpreter/activation/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../../client/interpreter/interpreterService'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { FakeClock } from '../../../common'; + +// tslint:disable-next-line: max-func-body-length +suite('DataScience - KernelService', () => { + let kernelService: KernelService; + let interperterService: IInterpreterService; + let fs: IDataScienceFileSystem; + let sessionManager: IJupyterSessionManager; + let execFactory: IPythonExecutionFactory; + let execService: IPythonExecutionService; + let activationHelper: IEnvironmentActivationService; + let dependencyService: KernelDependencyService; + let jupyterInterpreterExecutionService: IJupyterSubCommandExecutionService; + + function initialize() { + interperterService = mock(InterpreterService); + fs = mock(DataScienceFileSystem); + sessionManager = mock(JupyterSessionManager); + activationHelper = mock(EnvironmentActivationService); + execFactory = mock(PythonExecutionFactory); + execService = mock<IPythonExecutionService>(); + dependencyService = mock(KernelDependencyService); + jupyterInterpreterExecutionService = mock<IJupyterSubCommandExecutionService>(); + when(execFactory.createActivatedEnvironment(anything())).thenResolve(instance(execService)); + // tslint:disable-next-line: no-any + (instance(execService) as any).then = undefined; + + kernelService = new KernelService( + instance(jupyterInterpreterExecutionService), + instance(execFactory), + instance(interperterService), + instance(dependencyService), + instance(fs), + instance(activationHelper) + ); + } + setup(initialize); + teardown(() => sinon.restore()); + + test('Should not return a matching spec from a session for a given kernelspec', async () => { + const activeKernelSpecs: IJupyterKernelSpec[] = [ + { + argv: [], + language: PYTHON_LANGUAGE, + name: '1', + path: '', + display_name: '1', + metadata: {}, + env: undefined + }, + { + argv: [], + language: PYTHON_LANGUAGE, + name: '2', + path: '', + display_name: '2', + metadata: {}, + env: undefined + } + ]; + when(sessionManager.getKernelSpecs()).thenResolve(activeKernelSpecs); + + const matchingKernel = await kernelService.findMatchingKernelSpec( + { name: 'A', display_name: 'A' }, + instance(sessionManager) + ); + + assert.isUndefined(matchingKernel); + verify(sessionManager.getKernelSpecs()).once(); + }); + test('Should not return a matching spec from a session for a given interpeter', async () => { + const activeKernelSpecs: IJupyterKernelSpec[] = [ + { + argv: [], + language: PYTHON_LANGUAGE, + name: '1', + path: '', + display_name: '1', + metadata: {}, + env: undefined + }, + { + argv: [], + language: PYTHON_LANGUAGE, + name: '2', + path: '', + display_name: '2', + metadata: {}, + env: undefined + } + ]; + when(sessionManager.getKernelSpecs()).thenResolve(activeKernelSpecs); + const interpreter: PythonEnvironment = { + path: 'some Path', + displayName: 'Hello World', + envName: 'Hello', + envType: EnvironmentType.Conda + // tslint:disable-next-line: no-any + } as any; + + const matchingKernel = await kernelService.findMatchingKernelSpec(interpreter, instance(sessionManager)); + + assert.isUndefined(matchingKernel); + verify(sessionManager.getKernelSpecs()).once(); + }); + test('Should not return a matching spec from a jupyter process for a given kernelspec', async () => { + when(jupyterInterpreterExecutionService.getKernelSpecs(anything())).thenResolve([]); + + const matchingKernel = await kernelService.findMatchingKernelSpec({ name: 'A', display_name: 'A' }, undefined); + + assert.isUndefined(matchingKernel); + }); + test('Should not return a matching spec from a jupyter process for a given interpreter', async () => { + when(jupyterInterpreterExecutionService.getKernelSpecs(anything())).thenResolve([]); + + const interpreter: PythonEnvironment = { + path: 'some Path', + displayName: 'Hello World', + envName: 'Hello', + envType: EnvironmentType.Conda + // tslint:disable-next-line: no-any + } as any; + + const matchingKernel = await kernelService.findMatchingKernelSpec(interpreter, undefined); + + assert.isUndefined(matchingKernel); + }); + test('Should return a matching spec from a session for a given kernelspec', async () => { + const activeKernelSpecs: IJupyterKernelSpec[] = [ + { + argv: [], + language: PYTHON_LANGUAGE, + name: '1', + path: 'Path1', + display_name: 'Disp1', + metadata: {}, + env: undefined + }, + { + argv: [], + language: PYTHON_LANGUAGE, + name: '2', + path: 'Path2', + display_name: 'Disp2', + metadata: {}, + env: undefined + } + ]; + when(sessionManager.getKernelSpecs()).thenResolve(activeKernelSpecs); + + const matchingKernel = await kernelService.findMatchingKernelSpec( + { name: '2', display_name: 'Disp2' }, + instance(sessionManager) + ); + + assert.isOk(matchingKernel); + assert.equal(matchingKernel?.display_name, 'Disp2'); + assert.equal(matchingKernel?.name, '2'); + assert.equal(matchingKernel?.path, 'Path2'); + assert.equal(matchingKernel?.language, PYTHON_LANGUAGE); + verify(sessionManager.getKernelSpecs()).once(); + }); + test('Should return a matching spec from a session for a given interpreter', async () => { + const activeKernelSpecs: IJupyterKernelSpec[] = [ + { + argv: [], + language: PYTHON_LANGUAGE, + name: '1', + path: 'Path1', + display_name: 'Disp1', + metadata: {}, + env: undefined + }, + { + argv: [], + language: PYTHON_LANGUAGE, + name: '2', + path: 'Path2', + display_name: 'Disp2', + metadata: { interpreter: { path: 'myPath2' } }, + env: undefined + }, + { + argv: [], + language: PYTHON_LANGUAGE, + name: '3', + path: 'Path3', + display_name: 'Disp3', + metadata: { interpreter: { path: 'myPath3' } }, + env: undefined + } + ]; + when(sessionManager.getKernelSpecs()).thenResolve(activeKernelSpecs); + when(fs.areLocalPathsSame('myPath2', 'myPath2')).thenReturn(true); + const interpreter: PythonEnvironment = { + displayName: 'Disp2', + path: 'myPath2', + sysPrefix: 'xyz', + envType: EnvironmentType.Conda, + sysVersion: '', + architecture: Architecture.Unknown + }; + + const matchingKernel = await kernelService.findMatchingKernelSpec(interpreter, instance(sessionManager)); + + assert.isOk(matchingKernel); + assert.equal(matchingKernel?.display_name, 'Disp2'); + assert.equal(matchingKernel?.name, '2'); + assert.equal(matchingKernel?.path, 'Path2'); + assert.deepEqual(matchingKernel?.metadata, activeKernelSpecs[1].metadata); + assert.equal(matchingKernel?.language, PYTHON_LANGUAGE); + verify(sessionManager.getKernelSpecs()).once(); + }); + test('Should return a matching spec from a jupyter process for a given kernelspec', async () => { + const kernelSpecs = [ + new JupyterKernelSpec( + { + name: 'K1', + argv: [], + display_name: 'disp1', + language: PYTHON_LANGUAGE, + resources: {}, + metadata: { interpreter: { path: 'Some Path', envName: 'MyEnvName' } } + }, + path.join('dir1', 'kernel.json') + ), + new JupyterKernelSpec( + { + name: 'K2', + argv: [], + display_name: 'disp2', + language: PYTHON_LANGUAGE, + resources: {}, + metadata: { interpreter: { path: 'Some Path2', envName: 'MyEnvName2' } } + }, + path.join('dir2', 'kernel.json') + ) + ]; + when(jupyterInterpreterExecutionService.getKernelSpecs(anything())).thenResolve(kernelSpecs); + const matchingKernel = await kernelService.findMatchingKernelSpec( + { name: 'K2', display_name: 'disp2' }, + undefined + ); + + assert.isOk(matchingKernel); + assert.equal(matchingKernel?.display_name, 'disp2'); + assert.equal(matchingKernel?.name, 'K2'); + assert.equal(matchingKernel?.metadata?.interpreter?.path, 'Some Path2'); + assert.equal(matchingKernel?.metadata?.interpreter?.envName, 'MyEnvName2'); + assert.equal(matchingKernel?.language, PYTHON_LANGUAGE); + }); + test('Should return a matching spec from a jupyter process for a given interpreter', async () => { + const kernelSpecs = [ + new JupyterKernelSpec( + { + name: 'K1', + argv: [], + display_name: 'disp1', + language: PYTHON_LANGUAGE, + resources: {}, + metadata: { interpreter: { path: 'Some Path', envName: 'MyEnvName' } } + }, + path.join('dir1', 'kernel.json') + ), + new JupyterKernelSpec( + { + name: 'K2', + argv: [], + display_name: 'disp2', + language: PYTHON_LANGUAGE, + resources: {}, + metadata: { interpreter: { path: 'Some Path2', envName: 'MyEnvName2' } } + }, + path.join('dir2', 'kernel.json') + ) + ]; + when(jupyterInterpreterExecutionService.getKernelSpecs(anything())).thenResolve(kernelSpecs); + when(fs.areLocalPathsSame('Some Path2', 'Some Path2')).thenReturn(true); + when(fs.localFileExists(path.join('dir2', 'kernel.json'))).thenResolve(true); + const interpreter: PythonEnvironment = { + displayName: 'disp2', + path: 'Some Path2', + sysPrefix: 'xyz', + envType: EnvironmentType.Conda, + sysVersion: '', + architecture: Architecture.Unknown + }; + + const matchingKernel = await kernelService.findMatchingKernelSpec(interpreter, undefined); + + assert.isOk(matchingKernel); + assert.equal(matchingKernel?.display_name, 'disp2'); + assert.equal(matchingKernel?.name, 'K2'); + assert.equal(matchingKernel?.metadata?.interpreter?.path, 'Some Path2'); + assert.equal(matchingKernel?.metadata?.interpreter?.envName, 'MyEnvName2'); + assert.equal(matchingKernel?.language, PYTHON_LANGUAGE); + assert.deepEqual(matchingKernel?.metadata, kernelSpecs[1].metadata); + }); + // tslint:disable-next-line: max-func-body-length + suite('Registering Interpreters as Kernels', () => { + let findMatchingKernelSpecStub: sinon.SinonStub< + [PythonEnvironment, IJupyterSessionManager?, (CancellationToken | undefined)?], + Promise<IJupyterKernelSpec | undefined> + >; + let fakeTimer: FakeClock; + const interpreter: PythonEnvironment = { + architecture: Architecture.Unknown, + path: path.join('interpreter', 'python'), + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Conda, + displayName: 'Hello' + }; + // Marked as readonly, to ensure we do not update this in tests. + const kernelSpecModel: Readonly<Kernel.ISpecModel> = { + argv: ['python', '-m', 'ipykernel'], + display_name: interpreter.displayName!, + language: PYTHON_LANGUAGE, + name: 'somme name', + resources: {}, + env: {}, + metadata: { + something: '1', + interpreter: { + path: interpreter.path, + type: interpreter.envType + } + } + }; + const userKernelSpecModel: Readonly<Kernel.ISpecModel> = { + argv: ['python', '-m', 'ipykernel'], + display_name: interpreter.displayName!, + language: PYTHON_LANGUAGE, + name: 'somme name', + resources: {}, + env: {}, + metadata: { + something: '1' + } + }; + const kernelJsonFile = path.join('someFile', 'kernel.json'); + + setup(() => { + findMatchingKernelSpecStub = sinon.stub(KernelService.prototype, 'findMatchingKernelSpec'); + fakeTimer = new FakeClock(); + initialize(); + }); + + teardown(() => fakeTimer.uninstall()); + + test('Fail if interpreter does not have a display name', async () => { + const invalidInterpreter: PythonEnvironment = { + architecture: Architecture.Unknown, + path: '', + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Conda + }; + + const promise = kernelService.registerKernel(invalidInterpreter); + + await assert.isRejected(promise, 'Interpreter does not have a display name'); + }); + test('Fail if installed kernel cannot be found', async () => { + when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' }); + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(true); + findMatchingKernelSpecStub.resolves(undefined); + fakeTimer.install(); + + const promise = kernelService.registerKernel(interpreter); + + await fakeTimer.wait(); + await assert.isRejected(promise); + verify(execService.execModule('ipykernel', anything(), anything())).once(); + const installArgs = capture(execService.execModule).first()[1] as string[]; + const kernelName = installArgs[3]; + assert.deepEqual(installArgs, [ + 'install', + '--user', + '--name', + kernelName, + '--display-name', + interpreter.displayName + ]); + await assert.isRejected( + promise, + `Kernel not created with the name ${kernelName}, display_name ${interpreter.displayName}. Output is ` + ); + }); + test('If ipykernel is not installed, then prompt to install ipykernel', async () => { + when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' }); + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(false); + when(dependencyService.installMissingDependencies(anything(), anything())).thenResolve( + KernelInterpreterDependencyResponse.ok + ); + findMatchingKernelSpecStub.resolves(undefined); + fakeTimer.install(); + + const promise = kernelService.registerKernel(interpreter); + + await fakeTimer.wait(); + await assert.isRejected(promise); + verify(execService.execModule('ipykernel', anything(), anything())).once(); + const installArgs = capture(execService.execModule).first()[1] as string[]; + const kernelName = installArgs[3]; + assert.deepEqual(installArgs, [ + 'install', + '--user', + '--name', + kernelName, + '--display-name', + interpreter.displayName + ]); + await assert.isRejected( + promise, + `Kernel not created with the name ${kernelName}, display_name ${interpreter.displayName}. Output is ` + ); + verify(dependencyService.installMissingDependencies(anything(), anything())).once(); + }); + test('If ipykernel is not installed, and ipykerne installation is canclled, then do not reigster kernel', async () => { + when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' }); + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(false); + when(dependencyService.installMissingDependencies(anything(), anything())).thenResolve( + KernelInterpreterDependencyResponse.cancel + ); + findMatchingKernelSpecStub.resolves(undefined); + + const kernel = await kernelService.registerKernel(interpreter); + + assert.isUndefined(kernel); + verify(execService.execModule('ipykernel', anything(), anything())).never(); + verify(dependencyService.installMissingDependencies(anything(), anything())).once(); + }); + test('Fail if installed kernel is not an instance of JupyterKernelSpec', async () => { + when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' }); + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(true); + // tslint:disable-next-line: no-any + findMatchingKernelSpecStub.resolves({} as any); + + const promise = kernelService.registerKernel(interpreter); + + await assert.isRejected(promise); + verify(execService.execModule('ipykernel', anything(), anything())).once(); + const installArgs = capture(execService.execModule).first()[1] as string[]; + const kernelName = installArgs[3]; + await assert.isRejected( + promise, + `Kernel not registered locally, created with the name ${kernelName}, display_name ${interpreter.displayName}. Output is ` + ); + }); + test('Fail if installed kernel spec does not have a specFile setup', async () => { + when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' }); + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(true); + // tslint:disable-next-line: no-any + const kernel = new JupyterKernelSpec({} as any); + findMatchingKernelSpecStub.resolves(kernel); + + const promise = kernelService.registerKernel(interpreter); + + await assert.isRejected(promise); + verify(execService.execModule('ipykernel', anything(), anything())).once(); + const installArgs = capture(execService.execModule).first()[1] as string[]; + const kernelName = installArgs[3]; + await assert.isRejected( + promise, + `kernel.json not created with the name ${kernelName}, display_name ${interpreter.displayName}. Output is ` + ); + }); + test('Kernel is installed and spec file is updated with interpreter information in metadata and interpreter path in argv', async () => { + when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' }); + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(true); + const kernel = new JupyterKernelSpec(kernelSpecModel, kernelJsonFile); + when(fs.readLocalFile(kernelJsonFile)).thenResolve(JSON.stringify(kernelSpecModel)); + when(fs.writeLocalFile(kernelJsonFile, anything())).thenResolve(); + when(activationHelper.getActivatedEnvironmentVariables(undefined, interpreter, true)).thenResolve( + undefined + ); + findMatchingKernelSpecStub.resolves(kernel); + const expectedKernelJsonContent: ReadWrite<Kernel.ISpecModel> = cloneDeep(kernelSpecModel); + // Fully qualified path must be injected into `argv`. + expectedKernelJsonContent.argv = [interpreter.path, '-m', 'ipykernel']; + // tslint:disable-next-line: no-any + expectedKernelJsonContent.metadata!.interpreter = interpreter as any; + + const installedKernel = await kernelService.registerKernel(interpreter); + + // tslint:disable-next-line: no-any + assert.deepEqual(kernel, installedKernel as any); + verify(fs.writeLocalFile(kernelJsonFile, anything())).once(); + // Verify the contents of JSON written to the file match as expected. + assert.deepEqual(JSON.parse(capture(fs.writeLocalFile).first()[1] as string), expectedKernelJsonContent); + }); + test('Kernel is installed and spec file is updated with interpreter information in metadata along with environment variables', async () => { + when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' }); + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(true); + const kernel = new JupyterKernelSpec(kernelSpecModel, kernelJsonFile); + when(fs.readLocalFile(kernelJsonFile)).thenResolve(JSON.stringify(kernelSpecModel)); + when(fs.writeLocalFile(kernelJsonFile, anything())).thenResolve(); + const envVariables = { MYVAR: '1' }; + when(activationHelper.getActivatedEnvironmentVariables(undefined, interpreter, true)).thenResolve( + envVariables + ); + findMatchingKernelSpecStub.resolves(kernel); + const expectedKernelJsonContent: ReadWrite<Kernel.ISpecModel> = cloneDeep(kernelSpecModel); + // Fully qualified path must be injected into `argv`. + expectedKernelJsonContent.argv = [interpreter.path, '-m', 'ipykernel']; + // tslint:disable-next-line: no-any + expectedKernelJsonContent.metadata!.interpreter = interpreter as any; + // tslint:disable-next-line: no-any + expectedKernelJsonContent.env = envVariables as any; + + const installedKernel = await kernelService.registerKernel(interpreter); + + // tslint:disable-next-line: no-any + assert.deepEqual(kernel, installedKernel as any); + verify(fs.writeLocalFile(kernelJsonFile, anything())).once(); + // Verify the contents of JSON written to the file match as expected. + assert.deepEqual(JSON.parse(capture(fs.writeLocalFile).first()[1] as string), expectedKernelJsonContent); + }); + test('Kernel is found and spec file is updated with interpreter information in metadata along with environment variables', async () => { + when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' }); + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(true); + const kernel = new JupyterKernelSpec(kernelSpecModel, kernelJsonFile); + when(jupyterInterpreterExecutionService.getKernelSpecs(anything())).thenResolve([kernel]); + when(fs.readLocalFile(kernelJsonFile)).thenResolve(JSON.stringify(kernelSpecModel)); + when(fs.writeLocalFile(kernelJsonFile, anything())).thenResolve(); + const envVariables = { MYVAR: '1' }; + when(activationHelper.getActivatedEnvironmentVariables(undefined, interpreter, true)).thenResolve( + envVariables + ); + findMatchingKernelSpecStub.resolves(kernel); + const expectedKernelJsonContent: ReadWrite<Kernel.ISpecModel> = cloneDeep(kernelSpecModel); + // Fully qualified path must be injected into `argv`. + expectedKernelJsonContent.argv = [interpreter.path, '-m', 'ipykernel']; + // tslint:disable-next-line: no-any + expectedKernelJsonContent.metadata!.interpreter = interpreter as any; + // tslint:disable-next-line: no-any + expectedKernelJsonContent.env = envVariables as any; + + const installedKernel = await kernelService.searchAndRegisterKernel(interpreter, true); + + // tslint:disable-next-line: no-any + assert.deepEqual(kernel, installedKernel as any); + verify(fs.writeLocalFile(kernelJsonFile, anything())).once(); + // Verify the contents of JSON written to the file match as expected. + assert.deepEqual(JSON.parse(capture(fs.writeLocalFile).first()[1] as string), expectedKernelJsonContent); + }); + test('Kernel is found and spec file is not updated with interpreter information when user spec file', async () => { + when(execService.execModule('ipykernel', anything(), anything())).thenResolve({ stdout: '' }); + when(dependencyService.areDependenciesInstalled(interpreter, anything())).thenResolve(true); + const kernel = new JupyterKernelSpec(userKernelSpecModel, kernelJsonFile); + when(jupyterInterpreterExecutionService.getKernelSpecs(anything())).thenResolve([kernel]); + when(fs.readLocalFile(kernelJsonFile)).thenResolve(JSON.stringify(userKernelSpecModel)); + let contents: string | undefined; + when(fs.writeLocalFile(kernelJsonFile, anything())).thenCall((_f, c) => { + contents = c; + return Promise.resolve(); + }); + const envVariables = { MYVAR: '1' }; + when(activationHelper.getActivatedEnvironmentVariables(undefined, interpreter, true)).thenResolve( + envVariables + ); + findMatchingKernelSpecStub.resolves(kernel); + + const installedKernel = await kernelService.searchAndRegisterKernel(interpreter, true); + + // tslint:disable-next-line: no-any + assert.deepEqual(kernel, installedKernel as any); + assert.ok(contents, 'Env not updated'); + const obj = JSON.parse(contents!); + assert.notOk(obj.metadata.interpreter, 'MetaData should not have been written'); + }); + }); +}); diff --git a/src/test/datascience/jupyter/kernels/kernelSwitcher.unit.test.ts b/src/test/datascience/jupyter/kernels/kernelSwitcher.unit.test.ts new file mode 100644 index 000000000000..9ad8e634e443 --- /dev/null +++ b/src/test/datascience/jupyter/kernels/kernelSwitcher.unit.test.ts @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter } from 'vscode'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../../../client/common/application/types'; +import { PythonSettings } from '../../../../client/common/configSettings'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { IConfigurationService, IPythonSettings } from '../../../../client/common/types'; +import { Common } from '../../../../client/common/utils/localize'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { JupyterSessionStartError } from '../../../../client/datascience/baseJupyterSession'; +import { NotebookProvider } from '../../../../client/datascience/interactive-common/notebookProvider'; +import { JupyterNotebookBase } from '../../../../client/datascience/jupyter/jupyterNotebook'; +import { KernelDependencyService } from '../../../../client/datascience/jupyter/kernels/kernelDependencyService'; +import { KernelSelector } from '../../../../client/datascience/jupyter/kernels/kernelSelector'; +import { KernelSwitcher } from '../../../../client/datascience/jupyter/kernels/kernelSwitcher'; +import { KernelConnectionMetadata, LiveKernelModel } from '../../../../client/datascience/jupyter/kernels/types'; +import { IJupyterConnection, IJupyterKernelSpec, INotebook } from '../../../../client/datascience/types'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { noop } from '../../../core'; + +// tslint:disable: max-func-body-length no-any +suite('DataScience - Kernel Switcher', () => { + let kernelSwitcher: KernelSwitcher; + let configService: IConfigurationService; + let kernelSelector: KernelSelector; + let appShell: IApplicationShell; + let notebook: INotebook; + let connection: IJupyterConnection; + let currentKernel: IJupyterKernelSpec | LiveKernelModel; + let selectedInterpreter: PythonEnvironment; + let settings: IPythonSettings; + let newKernelConnection: KernelConnectionMetadata; + setup(() => { + connection = mock<IJupyterConnection>(); + settings = mock(PythonSettings); + currentKernel = { + lastActivityTime: new Date(), + name: 'CurrentKernel', + numberOfConnections: 0, + // tslint:disable-next-line: no-any + session: {} as any + }; + selectedInterpreter = { + path: '', + envType: EnvironmentType.Conda, + architecture: Architecture.Unknown, + sysPrefix: '', + sysVersion: '' + }; + newKernelConnection = { + kernelModel: currentKernel, + interpreter: selectedInterpreter, + kind: 'connectToLiveKernel' + }; + notebook = mock(JupyterNotebookBase); + configService = mock(ConfigurationService); + kernelSelector = mock(KernelSelector); + appShell = mock(ApplicationShell); + const notebookProvider = mock(NotebookProvider); + when(notebookProvider.type).thenReturn('jupyter'); + + // tslint:disable-next-line: no-any + when(settings.datascience).thenReturn({} as any); + when(notebook.connection).thenReturn(instance(connection)); + when(configService.getSettings(anything())).thenReturn(instance(settings)); + kernelSwitcher = new KernelSwitcher( + instance(configService), + instance(appShell), + instance(mock(KernelDependencyService)), + instance(kernelSelector) + ); + when(appShell.withProgress(anything(), anything())).thenCall(async (_, cb: () => Promise<void>) => { + await cb(); + }); + }); + + [true, false].forEach((isLocalConnection) => { + // tslint:disable-next-line: max-func-body-length + suite(isLocalConnection ? 'Local Connection' : 'Remote Connection', () => { + setup(() => { + const jupyterConnection: IJupyterConnection = { + type: 'jupyter', + localLaunch: isLocalConnection, + baseUrl: '', + disconnected: new EventEmitter<number>().event, + hostName: '', + token: '', + localProcExitCode: 0, + valid: true, + displayName: '', + dispose: noop, + rootDirectory: EXTENSION_ROOT_DIR + }; + when(notebook.connection).thenReturn(jupyterConnection); + }); + teardown(function () { + // tslint:disable-next-line: no-invalid-this + if (this.runnable().state) { + // We should have checked if it was a local connection. + verify(notebook.connection).atLeast(1); + } + }); + + [ + { title: 'Without an existing kernel', currentKernel: undefined }, + { title: 'With an existing kernel', currentKernel } + ].forEach((currentKernelInfo) => { + suite(currentKernelInfo.title, () => { + setup(() => { + when(notebook.getKernelConnection()).thenReturn({ + kernelSpec: currentKernelInfo.currentKernel as any, + kind: 'startUsingKernelSpec' + }); + }); + + test('Switch to new kernel', async () => { + await kernelSwitcher.switchKernelWithRetry(instance(notebook), newKernelConnection); + verify(notebook.setKernelConnection(anything(), anything())).once(); + }); + test('Switch to new kernel with error', async () => { + const ex = new JupyterSessionStartError(new Error('Kaboom')); + when(notebook.setKernelConnection(anything(), anything())).thenReject(ex); + when(appShell.showErrorMessage(anything(), anything(), anything())).thenResolve( + // tslint:disable-next-line: no-any + Common.cancel() as any + ); + + // This wouldn't normally fail for remote because sessions should always start if + // the remote server is up but both should throw + try { + await kernelSwitcher.switchKernelWithRetry(instance(notebook), newKernelConnection); + assert.fail('Should throw exception'); + } catch { + // This is expected + } + if (isLocalConnection) { + verify(kernelSelector.askForLocalKernel(anything(), anything(), anything())).once(); + } + }); + }); + }); + }); + }); +}); diff --git a/src/test/datascience/jupyter/serverCache.unit.test.ts b/src/test/datascience/jupyter/serverCache.unit.test.ts new file mode 100644 index 000000000000..1d37c555c67b --- /dev/null +++ b/src/test/datascience/jupyter/serverCache.unit.test.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { CancellationToken } from 'vscode'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { sleep } from '../../../client/common/utils/async'; +import { DataScienceFileSystem } from '../../../client/datascience/dataScienceFileSystem'; +import { ServerCache } from '../../../client/datascience/jupyter/liveshare/serverCache'; +import { INotebookServerOptions } from '../../../client/datascience/types'; +import { MockAutoSelectionService } from '../../mocks/autoSelector'; +import { MockJupyterServer } from '../mockJupyterServer'; + +// tslint:disable: max-func-body-length +suite('DataScience - ServerCache', () => { + let serverCache: ServerCache; + const fileSystem = mock(DataScienceFileSystem); + const workspaceService = mock(WorkspaceService); + const configService = mock(ConfigurationService); + const server = new MockJupyterServer(); + const pythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); + + setup(() => { + // Setup default settings + pythonSettings.datascience = { + allowImportFromNotebook: true, + alwaysTrustNotebooks: true, + jupyterLaunchTimeout: 10, + jupyterLaunchRetries: 3, + enabled: true, + jupyterServerURI: 'local', + // tslint:disable-next-line: no-invalid-template-strings + notebookFileRoot: '${fileDirname}', + changeDirOnImportExport: true, + useDefaultConfigForJupyter: true, + jupyterInterruptTimeout: 10000, + searchForJupyter: false, + showCellInputCode: true, + collapseCellInputCodeByDefault: true, + allowInput: true, + maxOutputSize: 400, + enableScrollingForCellOutputs: true, + errorBackgroundColor: '#FFFFFF', + sendSelectionToInteractiveWindow: false, + variableExplorerExclude: 'module;function;builtin_function_or_method', + codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', + markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', + allowLiveShare: false, + enablePlotViewer: true, + runStartupCommands: '', + debugJustMyCode: true, + variableQueries: [], + jupyterCommandLineArguments: [], + widgetScriptSources: [], + interactiveWindowMode: 'single' + }; + when(configService.getSettings(anything())).thenReturn(pythonSettings); + serverCache = new ServerCache(instance(configService), instance(workspaceService), instance(fileSystem)); + }); + + test('Cache works on second get', async () => { + const options: INotebookServerOptions = { + purpose: 'test', + allowUI: () => false + }; + const func = () => { + return Promise.resolve(server); + }; + const result = await serverCache.getOrCreate(func, options); + assert.ok(result, 'first get did not work'); + const r2 = await serverCache.get(options); + assert.equal(result, r2, 'Second get did not work'); + }); + + test('Cache with UI will not cancel original get', async () => { + let token: CancellationToken | undefined; + let allowUI = false; + const callback = () => { + return allowUI; + }; + const options: INotebookServerOptions = { + purpose: 'test', + allowUI: callback + }; + serverCache + .getOrCreate(async (_o, t) => { + token = t; + await sleep(500); + return Promise.resolve(server); + }, options) + .ignoreErrors(); + allowUI = true; + const result2 = await serverCache.getOrCreate(async () => { + return Promise.resolve(server); + }, options); + assert.ok(result2, 'Second did not work'); + assert.notOk(token?.isCancellationRequested, 'First request should not be canceled'); + }); +}); diff --git a/src/test/datascience/jupyter/serverSelector.unit.test.ts b/src/test/datascience/jupyter/serverSelector.unit.test.ts new file mode 100644 index 000000000000..5fd6bdd72f0b --- /dev/null +++ b/src/test/datascience/jupyter/serverSelector.unit.test.ts @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; + +import * as sinon from 'sinon'; +import { QuickPickItem } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { ClipboardService } from '../../../client/common/application/clipboard'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { IClipboard, ICommandManager } from '../../../client/common/application/types'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { IDataScienceSettings } from '../../../client/common/types'; +import { DataScience } from '../../../client/common/utils/localize'; +import { noop } from '../../../client/common/utils/misc'; +import { MultiStepInput, MultiStepInputFactory } from '../../../client/common/utils/multiStepInput'; +import { addToUriList } from '../../../client/datascience/common'; +import { Settings } from '../../../client/datascience/constants'; +import { JupyterServerSelector } from '../../../client/datascience/jupyter/serverSelector'; +import { JupyterUriProviderRegistration } from '../../../client/datascience/jupyterUriProviderRegistration'; +import { MockMemento } from '../../mocks/mementos'; +import { MockInputBox } from '../mockInputBox'; +import { MockQuickPick } from '../mockQuickPick'; + +// tslint:disable: max-func-body-length +suite('DataScience - Jupyter Server URI Selector', () => { + let quickPick: MockQuickPick | undefined; + let cmdManager: ICommandManager; + let dsSettings: IDataScienceSettings; + let clipboard: IClipboard; + + function createDataScienceObject( + quickPickSelection: string, + inputSelection: string, + updateCallback: (val: string) => void, + mockStorage?: MockMemento + ): JupyterServerSelector { + dsSettings = { + jupyterServerURI: Settings.JupyterServerLocalLaunch + // tslint:disable-next-line: no-any + } as any; + clipboard = mock(ClipboardService); + const configService = mock(ConfigurationService); + const applicationShell = mock(ApplicationShell); + const picker = mock(JupyterUriProviderRegistration); + cmdManager = mock(CommandManager); + const storage = mockStorage ? mockStorage : new MockMemento(); + quickPick = new MockQuickPick(quickPickSelection); + const input = new MockInputBox(inputSelection); + when(cmdManager.executeCommand(anything(), anything())).thenResolve(); + when(applicationShell.createQuickPick()).thenReturn(quickPick!); + when(applicationShell.createInputBox()).thenReturn(input); + const multiStepFactory = new MultiStepInputFactory(instance(applicationShell)); + // tslint:disable-next-line: no-any + when(configService.getSettings(anything())).thenReturn({ datascience: dsSettings } as any); + when(configService.updateSetting('dataScience.jupyterServerURI', anything(), anything(), anything())).thenCall( + (_a1, a2, _a3, _a4) => { + updateCallback(a2); + return Promise.resolve(); + } + ); + + return new JupyterServerSelector( + storage, + instance(clipboard), + multiStepFactory, + instance(configService), + instance(cmdManager), + instance(picker) + ); + } + + teardown(() => sinon.restore()); + + test('Local pick server uri', async () => { + let value = ''; + const ds = createDataScienceObject('$(zap) Default', '', (v) => (value = v)); + await ds.selectJupyterURI(true); + assert.equal(value, Settings.JupyterServerLocalLaunch, 'Default should pick local launch'); + + // Try a second time. + await ds.selectJupyterURI(true); + assert.equal(value, Settings.JupyterServerLocalLaunch, 'Default should pick local launch'); + + // Verify active items + assert.equal(quickPick?.items.length, 2, 'Wrong number of items in the quick pick'); + }); + + test('Quick pick MRU tests', async () => { + const mockStorage = new MockMemento(); + const ds = createDataScienceObject( + '$(zap) Default', + '', + () => { + noop(); + }, + mockStorage + ); + + await ds.selectJupyterURI(true); + // Verify initial default items + assert.equal(quickPick?.items.length, 2, 'Wrong number of items in the quick pick'); + + // Add in a new server + const serverA1 = { uri: 'ServerA', time: 1, date: new Date(1) }; + addToUriList(mockStorage, serverA1.uri, serverA1.time, serverA1.uri); + + await ds.selectJupyterURI(true); + assert.equal(quickPick?.items.length, 3, 'Wrong number of items in the quick pick'); + quickPickCheck(quickPick?.items[2], serverA1); + + // Add in a second server, the newer server should be higher in the list due to newer time + const serverB1 = { uri: 'ServerB', time: 2, date: new Date(2) }; + addToUriList(mockStorage, serverB1.uri, serverB1.time, serverB1.uri); + await ds.selectJupyterURI(true); + assert.equal(quickPick?.items.length, 4, 'Wrong number of items in the quick pick'); + quickPickCheck(quickPick?.items[2], serverB1); + quickPickCheck(quickPick?.items[3], serverA1); + + // Reconnect to server A with a new time, it should now be higher in the list + const serverA3 = { uri: 'ServerA', time: 3, date: new Date(3) }; + addToUriList(mockStorage, serverA3.uri, serverA3.time, serverA3.uri); + await ds.selectJupyterURI(true); + assert.equal(quickPick?.items.length, 4, 'Wrong number of items in the quick pick'); + quickPickCheck(quickPick?.items[3], serverB1); + quickPickCheck(quickPick?.items[2], serverA1); + + // Verify that we stick to our settings limit + for (let i = 0; i < Settings.JupyterServerUriListMax + 10; i = i + 1) { + addToUriList(mockStorage, i.toString(), i, i.toString()); + } + + await ds.selectJupyterURI(true); + // Need a plus 2 here for the two default items + assert.equal( + quickPick?.items.length, + Settings.JupyterServerUriListMax + 2, + 'Wrong number of items in the quick pick' + ); + }); + + function quickPickCheck(item: QuickPickItem | undefined, expected: { uri: string; time: Number; date: Date }) { + assert.isOk(item, 'Quick pick item not defined'); + if (item) { + assert.equal(item.label, expected.uri, 'Wrong URI value in quick pick'); + assert.equal( + item.detail, + DataScience.jupyterSelectURIMRUDetail().format(expected.date.toLocaleString()), + 'Wrong detail value in quick pick' + ); + } + } + + test('Remote server uri', async () => { + let value = ''; + const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', (v) => (value = v)); + await ds.selectJupyterURI(true); + assert.equal(value, 'http://localhost:1111', 'Already running should end up with the user inputed value'); + }); + + test('Remote server uri no local', async () => { + let value = ''; + const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', (v) => (value = v)); + await ds.selectJupyterURI(false); + assert.equal(value, 'http://localhost:1111', 'Already running should end up with the user inputed value'); + }); + + test('Remote server uri (reload VSCode if there is a change in settings)', async () => { + let value = ''; + const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', (v) => (value = v)); + await ds.selectJupyterURI(true); + assert.equal(value, 'http://localhost:1111', 'Already running should end up with the user inputed value'); + verify(cmdManager.executeCommand(anything(), anything())).once(); + }); + + test('Remote server uri (do not reload VSCode if there is no change in settings)', async () => { + let value = ''; + const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', (v) => (value = v)); + dsSettings.jupyterServerURI = 'http://localhost:1111'; + await ds.selectJupyterURI(true); + assert.equal(value, 'http://localhost:1111', 'Already running should end up with the user inputed value'); + verify(cmdManager.executeCommand(anything(), anything())).never(); + }); + + test('Invalid server uri', async () => { + let value = ''; + const ds = createDataScienceObject('$(server) Existing', 'httx://localhost:1111', (v) => (value = v)); + await ds.selectJupyterURI(true); + assert.notEqual(value, 'httx://localhost:1111', 'Already running should validate'); + assert.equal(value, '', 'Validation failed'); + }); + + suite('Default Uri when selecting remote uri', () => { + const defaultUri = 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe'; + + async function testDefaultUri(expectedDefaultUri: string, clipboardValue?: string) { + const showInputBox = sinon.spy(MultiStepInput.prototype, 'showInputBox'); + const ds = createDataScienceObject('$(server) Existing', 'http://localhost:1111', noop); + when(clipboard.readText()).thenResolve(clipboardValue || ''); + + await ds.selectJupyterURI(true); + + assert.equal(showInputBox.firstCall.args[0].value, expectedDefaultUri); + } + + test('Display default uri', async () => { + await testDefaultUri(defaultUri); + }); + test('Display default uri if clipboard is empty', async () => { + await testDefaultUri(defaultUri, ''); + }); + test('Display default uri if clipboard contains invalid uri, display default uri', async () => { + await testDefaultUri(defaultUri, 'Hello World!'); + }); + test('Display default uri if clipboard contains invalid file uri, display default uri', async () => { + await testDefaultUri(defaultUri, 'file://test.pdf'); + }); + test('Display default uri if clipboard contains a valid uri, display uri from clipboard', async () => { + const validUri = 'https://wow:0909/?password=1234'; + + await testDefaultUri(validUri, validUri); + }); + }); +}); diff --git a/src/test/datascience/jupyterHelpers.ts b/src/test/datascience/jupyterHelpers.ts new file mode 100644 index 000000000000..fd60a39737ce --- /dev/null +++ b/src/test/datascience/jupyterHelpers.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// IP = * format is a bit different from localhost format +export function getIPConnectionInfo(output: string): string | undefined { + // String format: http://(NAME or IP):PORT/ + const nameAndPortRegEx = /(https?):\/\/\(([^\s]*) or [0-9.]*\):([0-9]*)\/(?:\?token=)?([a-zA-Z0-9]*)?/; + + const urlMatch = nameAndPortRegEx.exec(output); + if (urlMatch && !urlMatch[4]) { + return `${urlMatch[1]}://${urlMatch[2]}:${urlMatch[3]}/`; + } else if (urlMatch && urlMatch.length === 5) { + return `${urlMatch[1]}://${urlMatch[2]}:${urlMatch[3]}/?token=${urlMatch[4]}`; + } + + // In Notebook 6.0 instead of the above format it returns a single valid web address so just return that + return getConnectionInfo(output); +} + +export function getConnectionInfo(output: string): string | undefined { + const UrlPatternRegEx = /(https?:\/\/[^\s]+)/; + + const urlMatch = UrlPatternRegEx.exec(output); + if (urlMatch) { + return urlMatch[0]; + } + return undefined; +} diff --git a/src/test/datascience/jupyterPasswordConnect.unit.test.ts b/src/test/datascience/jupyterPasswordConnect.unit.test.ts new file mode 100644 index 000000000000..1c90275cc011 --- /dev/null +++ b/src/test/datascience/jupyterPasswordConnect.unit.test.ts @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import * as nodeFetch from 'node-fetch'; +import * as typemoq from 'typemoq'; + +import { anything, instance, mock, when } from 'ts-mockito'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; +import { JupyterPasswordConnect } from '../../client/datascience/jupyter/jupyterPasswordConnect'; +import { MockInputBox } from './mockInputBox'; +import { MockQuickPick } from './mockQuickPick'; + +// tslint:disable:no-any max-func-body-length no-http-string +suite('JupyterPasswordConnect', () => { + let jupyterPasswordConnect: JupyterPasswordConnect; + let appShell: ApplicationShell; + let configService: ConfigurationService; + + const xsrfValue: string = '12341234'; + const sessionName: string = 'sessionName'; + const sessionValue: string = 'sessionValue'; + + setup(() => { + appShell = mock(ApplicationShell); + when(appShell.showInputBox(anything())).thenReturn(Promise.resolve('Python')); + const multiStepFactory = new MultiStepInputFactory(instance(appShell)); + const mockDisposableRegistry = mock(AsyncDisposableRegistry); + configService = mock(ConfigurationService); + + jupyterPasswordConnect = new JupyterPasswordConnect( + instance(appShell), + multiStepFactory, + instance(mockDisposableRegistry), + instance(configService) + ); + }); + + function createMockSetup(secure: boolean, ok: boolean) { + const dsSettings = { + allowUnauthorizedRemoteConnection: secure + // tslint:disable-next-line: no-any + } as any; + when(configService.getSettings(anything())).thenReturn({ datascience: dsSettings } as any); + when(configService.updateSetting('dataScience.jupyterServerURI', anything(), anything(), anything())).thenCall( + (_a1, _a2, _a3, _a4) => { + return Promise.resolve(); + } + ); + + // Set up our fake node fetch + const fetchMock: typemoq.IMock<typeof nodeFetch.default> = typemoq.Mock.ofInstance(nodeFetch.default); + const rootUrl = secure ? 'https://TESTNAME:8888/' : 'http://TESTNAME:8888/'; + + // Mock our first call to get xsrf cookie + const mockXsrfResponse = typemoq.Mock.ofType(nodeFetch.Response); + const mockXsrfHeaders = typemoq.Mock.ofType(nodeFetch.Headers); + mockXsrfHeaders + .setup((mh) => mh.raw()) + .returns(() => { + return { 'set-cookie': [`_xsrf=${xsrfValue}`] }; + }); + mockXsrfResponse.setup((mr) => mr.ok).returns(() => ok); + mockXsrfResponse.setup((mr) => mr.status).returns(() => 302); + mockXsrfResponse.setup((mr) => mr.headers).returns(() => mockXsrfHeaders.object); + + const mockHubResponse = typemoq.Mock.ofType(nodeFetch.Response); + mockHubResponse.setup((mr) => mr.ok).returns(() => false); + mockHubResponse.setup((mr) => mr.status).returns(() => 404); + + fetchMock + .setup((fm) => + fm( + `${rootUrl}login?`, + typemoq.It.isObjectWith({ + method: 'get', + headers: { Connection: 'keep-alive' } + }) + ) + ) + .returns(() => Promise.resolve(mockXsrfResponse.object)); + fetchMock + .setup((fm) => + fm( + `${rootUrl}tree?`, + typemoq.It.isObjectWith({ + method: 'get', + headers: { Connection: 'keep-alive' } + }) + ) + ) + .returns(() => Promise.resolve(mockXsrfResponse.object)); + fetchMock + .setup((fm) => + fm( + `${rootUrl}hub/api`, + typemoq.It.isObjectWith({ + method: 'get', + headers: { Connection: 'keep-alive' } + }) + ) + ) + .returns(() => Promise.resolve(mockHubResponse.object)); + + return { fetchMock, mockXsrfHeaders, mockXsrfResponse }; + } + + test('getPasswordConnectionInfo', async () => { + const { fetchMock, mockXsrfHeaders, mockXsrfResponse } = createMockSetup(false, true); + + // Mock our second call to get session cookie + const mockSessionResponse = typemoq.Mock.ofType(nodeFetch.Response); + const mockSessionHeaders = typemoq.Mock.ofType(nodeFetch.Headers); + mockSessionHeaders + .setup((mh) => mh.raw()) + .returns(() => { + return { + 'set-cookie': [`${sessionName}=${sessionValue}`] + }; + }); + mockSessionResponse.setup((mr) => mr.status).returns(() => 302); + mockSessionResponse.setup((mr) => mr.headers).returns(() => mockSessionHeaders.object); + + // typemoq doesn't love this comparison, so generalize it a bit + fetchMock + .setup((fm) => + fm( + 'http://TESTNAME:8888/login?', + typemoq.It.isObjectWith({ + method: 'post', + headers: { + Cookie: `_xsrf=${xsrfValue}`, + Connection: 'keep-alive', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' + } + }) + ) + ) + .returns(() => Promise.resolve(mockSessionResponse.object)); + + const result = await jupyterPasswordConnect.getPasswordConnectionInfo( + 'http://TESTNAME:8888/', + fetchMock.object + ); + assert(result, 'Failed to get password'); + if (result) { + // tslint:disable-next-line: no-cookies + assert.ok((result.requestHeaders as any).Cookie, 'No cookie'); + } + + // Verfiy calls + mockXsrfHeaders.verifyAll(); + mockSessionHeaders.verifyAll(); + mockXsrfResponse.verifyAll(); + mockSessionResponse.verifyAll(); + fetchMock.verifyAll(); + }); + + test('getPasswordConnectionInfo allowUnauthorized', async () => { + const { fetchMock, mockXsrfHeaders, mockXsrfResponse } = createMockSetup(true, true); + + // Mock our second call to get session cookie + const mockSessionResponse = typemoq.Mock.ofType(nodeFetch.Response); + const mockSessionHeaders = typemoq.Mock.ofType(nodeFetch.Headers); + mockSessionHeaders + .setup((mh) => mh.raw()) + .returns(() => { + return { + 'set-cookie': [`${sessionName}=${sessionValue}`] + }; + }); + mockSessionResponse.setup((mr) => mr.status).returns(() => 302); + mockSessionResponse.setup((mr) => mr.headers).returns(() => mockSessionHeaders.object); + + // typemoq doesn't love this comparison, so generalize it a bit + fetchMock + .setup((fm) => + fm( + 'https://TESTNAME:8888/login?', + typemoq.It.isObjectWith({ + method: 'post', + headers: { + Cookie: `_xsrf=${xsrfValue}`, + Connection: 'keep-alive', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' + } + }) + ) + ) + .returns(() => Promise.resolve(mockSessionResponse.object)); + + const result = await jupyterPasswordConnect.getPasswordConnectionInfo( + 'https://TESTNAME:8888/', + fetchMock.object + ); + assert(result, 'Failed to get password'); + if (result) { + // tslint:disable-next-line: no-cookies + assert.ok((result.requestHeaders as any).Cookie, 'No cookie'); + } + + // Verfiy calls + mockXsrfHeaders.verifyAll(); + mockSessionHeaders.verifyAll(); + mockXsrfResponse.verifyAll(); + mockSessionResponse.verifyAll(); + fetchMock.verifyAll(); + }); + + test('getPasswordConnectionInfo failure', async () => { + const { fetchMock, mockXsrfHeaders, mockXsrfResponse } = createMockSetup(false, false); + + const result = await jupyterPasswordConnect.getPasswordConnectionInfo( + 'http://TESTNAME:8888/', + fetchMock.object + ); + assert(!result); + + // Verfiy calls + mockXsrfHeaders.verifyAll(); + mockXsrfResponse.verifyAll(); + fetchMock.verifyAll(); + }); + + function createJupyterHubSetup() { + const dsSettings = { + allowUnauthorizedRemoteConnection: false + // tslint:disable-next-line: no-any + } as any; + when(configService.getSettings(anything())).thenReturn({ datascience: dsSettings } as any); + when(configService.updateSetting('dataScience.jupyterServerURI', anything(), anything(), anything())).thenCall( + (_a1, _a2, _a3, _a4) => { + return Promise.resolve(); + } + ); + + const quickPick = new MockQuickPick(''); + const input = new MockInputBox('test'); + when(appShell.createQuickPick()).thenReturn(quickPick!); + when(appShell.createInputBox()).thenReturn(input); + + const hubActiveResponse = mock(nodeFetch.Response); + when(hubActiveResponse.ok).thenReturn(true); + when(hubActiveResponse.status).thenReturn(200); + const invalidResponse = mock(nodeFetch.Response); + when(invalidResponse.ok).thenReturn(false); + when(invalidResponse.status).thenReturn(404); + const loginResponse = mock(nodeFetch.Response); + const loginHeaders = mock(nodeFetch.Headers); + when(loginHeaders.raw()).thenReturn({ 'set-cookie': ['super-cookie-login=foobar'] }); + when(loginResponse.ok).thenReturn(true); + when(loginResponse.status).thenReturn(302); + when(loginResponse.headers).thenReturn(instance(loginHeaders)); + const tokenResponse = mock(nodeFetch.Response); + when(tokenResponse.ok).thenReturn(true); + when(tokenResponse.status).thenReturn(200); + when(tokenResponse.json()).thenResolve({ + token: 'foobar', + id: '1' + }); + + instance(hubActiveResponse as any).then = undefined; + instance(invalidResponse as any).then = undefined; + instance(loginResponse as any).then = undefined; + instance(tokenResponse as any).then = undefined; + + return async (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => { + const urlString = url.toString().toLowerCase(); + if (urlString === 'http://testname:8888/hub/api') { + return instance(hubActiveResponse); + } else if (urlString === 'http://testname:8888/hub/login?next=') { + return instance(loginResponse); + } else if ( + urlString === 'http://testname:8888/hub/api/users/test/tokens' && + init && + init.method === 'POST' && + (init.headers as any).Referer === 'http://testname:8888/hub/login' && + (init.headers as any).Cookie === ';super-cookie-login=foobar' + ) { + return instance(tokenResponse); + } + return instance(invalidResponse); + }; + } + test('getPasswordConnectionInfo jupyter hub', async () => { + const fetchMock = createJupyterHubSetup(); + + const result = await jupyterPasswordConnect.getPasswordConnectionInfo('http://TESTNAME:8888/', fetchMock); + assert.ok(result, 'No hub connection info'); + assert.equal(result?.remappedBaseUrl, 'http://testname:8888/user/test', 'Url not remapped'); + assert.equal(result?.remappedToken, 'foobar', 'Token should be returned in URL'); + assert.ok(result?.requestHeaders, 'No request headers returned for jupyter hub'); + }); +}); diff --git a/src/test/datascience/jupyterUriProviderRegistration.functional.test.ts b/src/test/datascience/jupyterUriProviderRegistration.functional.test.ts new file mode 100644 index 000000000000..185c53ccc0b9 --- /dev/null +++ b/src/test/datascience/jupyterUriProviderRegistration.functional.test.ts @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; + +import { Event, EventEmitter, Extension, ExtensionKind, QuickPickItem, Uri } from 'vscode'; +import { IExtensions } from '../../client/common/types'; +import { sleep } from '../../client/common/utils/async'; +import { Identifiers } from '../../client/datascience/constants'; +import { + IJupyterExecution, + IJupyterServerUri, + IJupyterUriProvider, + IJupyterUriProviderRegistration +} from '../../client/datascience/types'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; + +const TestUriProviderId = 'TestUriProvider_Id'; +const TestUriHandle = 'TestUriHandle'; + +class TestUriProvider implements IJupyterUriProvider { + public id: string = TestUriProviderId; + public currentBearer = 1; + public getQuickPickEntryItems(): QuickPickItem[] { + throw new Error('Method not implemented.'); + } + public handleQuickPick(_item: QuickPickItem, _backEnabled: boolean): Promise<string | undefined> { + throw new Error('Method not implemented.'); + } + public async getServerUri(handle: string): Promise<IJupyterServerUri> { + if (handle === TestUriHandle) { + setTimeout(() => (this.currentBearer += 1), 300); + return { + // tslint:disable-next-line: no-http-string + baseUrl: 'http://foobar:3000', + displayName: 'test', + token: '', + authorizationHeader: { Bearer: this.currentBearer.toString() }, + expiration: new Date(Date.now() + 300) // Expire after 300 milliseconds + }; + } + + throw new Error('Invalid server uri handle'); + } +} + +// tslint:disable: no-any +class TestUriProviderExtension implements Extension<any> { + public id: string = '1'; + public extensionUri: Uri = Uri.parse('foo'); + public extensionPath: string = 'foo'; + public isActive: boolean = false; + public packageJSON: any = { + contributes: { + pythonRemoteServerProvider: [] + } + }; + public extensionKind: ExtensionKind = ExtensionKind.Workspace; + public exports: any = {}; + constructor(private ioc: DataScienceIocContainer) {} + public async activate() { + this.ioc + .get<IJupyterUriProviderRegistration>(IJupyterUriProviderRegistration) + .registerProvider(new TestUriProvider()); + this.isActive = true; + return {}; + } +} + +class UriMockExtensions implements IExtensions { + public all: Extension<any>[] = []; + private changeEvent = new EventEmitter<void>(); + constructor(ioc: DataScienceIocContainer) { + this.all.push(new TestUriProviderExtension(ioc)); + } + public getExtension<T>(_extensionId: string): Extension<T> | undefined { + return undefined; + } + + public get onDidChange(): Event<void> { + return this.changeEvent.event; + } +} + +// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +suite(`DataScience JupyterServerUriProvider tests`, () => { + let ioc: DataScienceIocContainer; + + setup(async () => { + ioc = new DataScienceIocContainer(); + // Force to always be a mock run. Real will try to connect to the dummy URI + ioc.shouldMockJupyter = true; + ioc.registerDataScienceTypes(false); + ioc.serviceManager.rebindInstance<IExtensions>(IExtensions, new UriMockExtensions(ioc)); + return ioc.activate(); + }); + + teardown(async () => { + await ioc.dispose(); + }); + + test('Expiration', async function () { + // Issue #13668 filed to investigate / reactivate. Was failing randomly in some PR runs. + // tslint:disable-next-line: no-invalid-this + return this.skip(); + + // Set the URI to id value. + const uri = `${Identifiers.REMOTE_URI}?${Identifiers.REMOTE_URI_ID_PARAM}=${TestUriProviderId}&${Identifiers.REMOTE_URI_HANDLE_PARAM}=${TestUriHandle}`; + ioc.forceDataScienceSettingsChanged({ + jupyterServerURI: uri + }); + + // Start a notebook server (should not actually start anything as it's remote) + const jupyterExecution = ioc.get<IJupyterExecution>(IJupyterExecution); + const server = await jupyterExecution.connectToNotebookServer({ + uri, + purpose: 'history', + allowUI: () => false + }); + + // Verify URI is our expected one + // tslint:disable-next-line: no-http-string + assert.equal(server?.getConnectionInfo()?.baseUrl, `http://foobar:3000`, 'Base URI is invalid'); + let authHeader = server?.getConnectionInfo()?.getAuthHeader?.call(undefined); + assert.deepEqual(authHeader, { Bearer: '1' }, 'Bearer token invalid'); + + // Wait a bit + await sleep(500); + + authHeader = server?.getConnectionInfo()?.getAuthHeader?.call(undefined); + + // Auth header should have updated + assert.notEqual(authHeader.Bearer, '1', 'Bearer token did not update'); + }); +}); diff --git a/src/test/datascience/jupyterUriProviderRegistration.unit.test.ts b/src/test/datascience/jupyterUriProviderRegistration.unit.test.ts new file mode 100644 index 000000000000..0eda6343606d --- /dev/null +++ b/src/test/datascience/jupyterUriProviderRegistration.unit.test.ts @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { assert } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import * as vscode from 'vscode'; +import { DataScienceFileSystem } from '../../client/datascience/dataScienceFileSystem'; +import { JupyterUriProviderRegistration } from '../../client/datascience/jupyterUriProviderRegistration'; +import { IJupyterServerUri, IJupyterUriProvider, JupyterServerUriHandle } from '../../client/datascience/types'; +import { MockExtensions } from './mockExtensions'; + +class MockProvider implements IJupyterUriProvider { + public get id() { + return this._id; + } + private currentBearer = 1; + private result: string = '1'; + constructor(private readonly _id: string) { + // Id should be readonly + } + public getQuickPickEntryItems(): vscode.QuickPickItem[] { + return [{ label: 'Foo' }]; + } + public async handleQuickPick( + _item: vscode.QuickPickItem, + back: boolean + ): Promise<JupyterServerUriHandle | 'back' | undefined> { + return back ? 'back' : this.result; + } + public async getServerUri(handle: string): Promise<IJupyterServerUri> { + if (handle === '1') { + const currentDate = new Date(); + return { + // tslint:disable-next-line: no-http-string + baseUrl: 'http://foobar:3000', + token: '', + displayName: 'dummy', + authorizationHeader: { Bearer: this.currentBearer.toString() }, + expiration: new Date( + currentDate.getFullYear(), + currentDate.getMonth(), + undefined, + currentDate.getHours(), + currentDate.getMinutes() + 1 // Expire after one minute + ) + }; + } + + throw new Error('Invalid server uri handle'); + } +} + +// tslint:disable: max-func-body-length no-any +suite('DataScience URI Picker', () => { + function createRegistration(providerIds: string[]) { + let registration: JupyterUriProviderRegistration | undefined; + const extensions = mock(MockExtensions); + const extensionList: vscode.Extension<any>[] = []; + const fileSystem = mock(DataScienceFileSystem); + when(fileSystem.localFileExists(anything())).thenResolve(false); + providerIds.forEach((id) => { + const extension = TypeMoq.Mock.ofType<vscode.Extension<any>>(); + const packageJson = TypeMoq.Mock.ofType<any>(); + const contributes = TypeMoq.Mock.ofType<any>(); + extension.setup((e) => e.packageJSON).returns(() => packageJson.object); + packageJson.setup((p) => p.contributes).returns(() => contributes.object); + contributes.setup((p) => p.pythonRemoteServerProvider).returns(() => [{ d: '' }]); + extension + .setup((e) => e.activate()) + .returns(() => { + registration?.registerProvider(new MockProvider(id)); + return Promise.resolve(); + }); + extension.setup((e) => e.isActive).returns(() => false); + extensionList.push(extension.object); + }); + when(extensions.all).thenReturn(extensionList); + registration = new JupyterUriProviderRegistration(instance(extensions), instance(fileSystem)); + return registration; + } + + test('Simple', async () => { + const registration = createRegistration(['1']); + const pickers = await registration.getProviders(); + assert.equal(pickers.length, 1, 'Default picker should be there'); + const quickPick = pickers[0].getQuickPickEntryItems(); + assert.equal(quickPick.length, 1, 'No quick pick items added'); + const handle = await pickers[0].handleQuickPick(quickPick[0], false); + assert.ok(handle, 'Handle not set'); + const uri = await registration.getJupyterServerUri('1', handle!); + // tslint:disable-next-line: no-http-string + assert.equal(uri.baseUrl, 'http://foobar:3000', 'Base URL not found'); + assert.equal(uri.displayName, 'dummy', 'Display name not found'); + }); + test('Back', async () => { + const registration = createRegistration(['1']); + const pickers = await registration.getProviders(); + assert.equal(pickers.length, 1, 'Default picker should be there'); + const quickPick = pickers[0].getQuickPickEntryItems(); + assert.equal(quickPick.length, 1, 'No quick pick items added'); + const handle = await pickers[0].handleQuickPick(quickPick[0], true); + assert.equal(handle, 'back', 'Should be sending back'); + }); + test('Error', async () => { + const registration = createRegistration(['1']); + const pickers = await registration.getProviders(); + assert.equal(pickers.length, 1, 'Default picker should be there'); + const quickPick = pickers[0].getQuickPickEntryItems(); + assert.equal(quickPick.length, 1, 'No quick pick items added'); + try { + await registration.getJupyterServerUri('1', 'foobar'); + // tslint:disable-next-line: no-http-string + assert.fail('Should not get here'); + } catch { + // This means test passed. + } + }); + test('No picker call', async () => { + const registration = createRegistration(['1']); + const uri = await registration.getJupyterServerUri('1', '1'); + // tslint:disable-next-line: no-http-string + assert.equal(uri.baseUrl, 'http://foobar:3000', 'Base URL not found'); + }); + test('Two pickers', async () => { + const registration = createRegistration(['1', '2']); + let uri = await registration.getJupyterServerUri('1', '1'); + // tslint:disable-next-line: no-http-string + assert.equal(uri.baseUrl, 'http://foobar:3000', 'Base URL not found'); + uri = await registration.getJupyterServerUri('2', '1'); + // tslint:disable-next-line: no-http-string + assert.equal(uri.baseUrl, 'http://foobar:3000', 'Base URL not found'); + }); + test('Two pickers with same id', async () => { + const registration = createRegistration(['1', '1']); + try { + await registration.getJupyterServerUri('1', '1'); + // tslint:disable-next-line: no-http-string + assert.fail('Should have failed if calling with same picker'); + } catch { + // This means it passed + } + }); +}); diff --git a/src/test/datascience/jupyterUtils.unit.test.ts b/src/test/datascience/jupyterUtils.unit.test.ts new file mode 100644 index 000000000000..487c0205a729 --- /dev/null +++ b/src/test/datascience/jupyterUtils.unit.test.ts @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import type { KernelMessage } from '@jupyterlab/services'; +import { assert } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { DebugService } from '../../client/common/application/debugService'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { DataScienceFileSystem } from '../../client/datascience/dataScienceFileSystem'; +import { CellHashProvider } from '../../client/datascience/editor-integration/cellhashprovider'; +import { expandWorkingDir } from '../../client/datascience/jupyter/jupyterUtils'; +import { createEmptyCell } from '../../datascience-ui/interactive-common/mainState'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; +import { MockDocument } from './mockDocument'; +import { MockDocumentManager } from './mockDocumentManager'; +import { MockPythonSettings } from './mockPythonSettings'; + +suite('DataScience JupyterUtils', () => { + const workspaceService = mock(WorkspaceService); + const configService = mock(ConfigurationService); + const debugService = mock(DebugService); + const fileSystem = mock(DataScienceFileSystem); + const docManager = new MockDocumentManager(); + const dummySettings = new MockPythonSettings(undefined, new MockAutoSelectionService()); + when(configService.getSettings(anything())).thenReturn(dummySettings); + when(fileSystem.getDisplayName(anything())).thenCall((a) => `${a}tastic`); + when(fileSystem.areLocalPathsSame(anything(), anything())).thenCall((a, b) => + a.replace(/\\/g, '/').includes(b.replace(/\\/g, '/')) + ); + const hashProvider = new CellHashProvider( + docManager, + instance(configService), + instance(debugService), + instance(fileSystem), + [] + ); + + // tslint:disable: no-invalid-template-strings + test('expanding file variables', async function () { + // tslint:disable-next-line: no-invalid-this + this.timeout(10000); + const uri = Uri.file('test/bar'); + const folder = { index: 0, name: '', uri }; + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + when(workspaceService.workspaceFolders).thenReturn([folder]); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(folder); + const inst = instance(workspaceService); + const relativeFilePath = IS_WINDOWS ? '..\\xyz\\bip\\foo.baz' : '../xyz/bip/foo.baz'; + const relativeFileDir = IS_WINDOWS ? '..\\xyz\\bip' : '../xyz/bip'; + + assert.equal(expandWorkingDir(undefined, 'bar/foo.baz', inst), 'bar'); + assert.equal(expandWorkingDir(undefined, 'bar/bip/foo.baz', inst), 'bar/bip'); + assert.equal(expandWorkingDir('${file}', 'bar/bip/foo.baz', inst), Uri.file('bar/bip/foo.baz').fsPath); + assert.equal(expandWorkingDir('${fileDirname}', 'bar/bip/foo.baz', inst), Uri.file('bar/bip').fsPath); + assert.equal(expandWorkingDir('${relativeFile}', 'test/xyz/bip/foo.baz', inst), relativeFilePath); + assert.equal(expandWorkingDir('${relativeFileDirname}', 'test/xyz/bip/foo.baz', inst), relativeFileDir); + assert.equal(expandWorkingDir('${cwd}', 'test/xyz/bip/foo.baz', inst), Uri.file('test/bar').fsPath); + assert.equal(expandWorkingDir('${workspaceFolder}', 'test/xyz/bip/foo.baz', inst), Uri.file('test/bar').fsPath); + assert.equal( + expandWorkingDir('${cwd}-${file}', 'bar/bip/foo.baz', inst), + `${Uri.file('test/bar').fsPath}-${Uri.file('bar/bip/foo.baz').fsPath}` + ); + }); + + function modifyTraceback(trace: string[]): string[] { + // Pass onto the hash provider + const dummyMessage: KernelMessage.IErrorMsg = { + channel: 'iopub', + content: { + ename: 'foo', + evalue: 'foo', + traceback: trace + }, + header: { + msg_type: 'error', + msg_id: '1', + date: '1', + session: '1', + username: '1', + version: '1' + }, + parent_header: {}, + metadata: {} + }; + + // tslint:disable-next-line: no-any + return (hashProvider.preHandleIOPub(dummyMessage).content as any).traceback; + } + + function addCell(code: string, file: string, line: number) { + const doc = docManager.textDocuments.find((d) => d.fileName === file) as MockDocument; + if (doc) { + doc.addContent(code); + } else { + // Create a number of emptyish lines above the line + const emptyLines = Array.from('x'.repeat(line)).join('\n'); + const docCode = `${emptyLines}\n${code}`; + docManager.addDocument(docCode, file); + } + const cell = createEmptyCell(undefined, null); + cell.file = file; + cell.line = line; + cell.data.source = code; + return hashProvider.preExecute(cell, false); + } + + test('modifying traceback', async () => { + await addCell('sys.', 'foo.py', 60); + const trace1 = [ + '"\u001b[1;36m File \u001b[1;32mfoo.pytastic\u001b[1;36m, line \u001b[1;32m599999\u001b[0m\n\u001b[1;33m sys.\u001b[0m\n\u001b[1;37m ^\u001b[0m\n\u001b[1;31mSyntaxError\u001b[0m\u001b[1;31m:\u001b[0m invalid syntax\n"' + ]; + const after1 = [ + `"\u001b[1;36m File \u001b[1;32mfoo.pytastic\u001b[1;36m, line \u001b[1;32m<a href='file://foo.py?line=600058'>600059</a>\u001b[0m\n\u001b[1;33m sys.\u001b[0m\n\u001b[1;37m ^\u001b[0m\n\u001b[1;31mSyntaxError\u001b[0m\u001b[1;31m:\u001b[0m invalid syntax\n"` + ]; + // Use a join after to make the assert show the results + assert.equal(after1.join('\n'), modifyTraceback(trace1).join('\n'), 'Syntax error failure'); + + await addCell( + `for i in trange(100): + time.sleep(0.01) + raise Exception('spam')`, + 'd:\\Training\\SnakePython\\manualTestFile.py', + 1 + ); + const trace2 = [ + '\u001b[1;31m---------------------------------------------------------------------------\u001b[0m', + '\u001b[1;31mException\u001b[0m Traceback (most recent call last)', + "\u001b[1;32md:\\Training\\SnakePython\\manualTestFile.pytastic\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m 3\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mtrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m100\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[0mtime\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m0.01\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 5\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'spam'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + '\u001b[1;31mException\u001b[0m: spam' + ]; + const after2 = [ + '\u001b[1;31m---------------------------------------------------------------------------\u001b[0m', + '\u001b[1;31mException\u001b[0m Traceback (most recent call last)', + `\u001b[1;32md:\\Training\\SnakePython\\manualTestFile.pytastic\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m <a href='file://d:\\Training\\SnakePython\\manualTestFile.py?line=3'>4</a>\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mtrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m100\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m <a href='file://d:\\Training\\SnakePython\\manualTestFile.py?line=4'>5</a>\u001b[0m \u001b[0mtime\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m0.01\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> <a href='file://d:\\Training\\SnakePython\\manualTestFile.py?line=5'>6</a>\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'spam'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m`, + '\u001b[1;31mException\u001b[0m: spam' + ]; + assert.equal(after2.join('\n'), modifyTraceback(trace2).join('\n'), 'Exception failure'); + + when(fileSystem.getDisplayName(anything())).thenReturn('~/Test/manualTestFile.py'); + await addCell( + ` + +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt`, + '/home/rich/Test/manualTestFile.py', + 19 + ); + const trace3 = [ + '\u001b[0;31m---------------------------------------------------------------------------\u001b[0m', + '\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)', + '\u001b[0;32m~/Test/manualTestFile.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 4\u001b[0;31m \u001b[0;32mimport\u001b[0m \u001b[0mnumpy\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 5\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mpandas\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mpd\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mmatplotlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpyplot\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mplt\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n', + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'numpy'" + ]; + const after3 = [ + '\u001b[0;31m---------------------------------------------------------------------------\u001b[0m', + '\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)', + "\u001b[0;32m~/Test/manualTestFile.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> <a href='file:///home/rich/Test/manualTestFile.py?line=24'>25</a>\u001b[0;31m \u001b[0;32mimport\u001b[0m \u001b[0mnumpy\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m <a href='file:///home/rich/Test/manualTestFile.py?line=25'>26</a>\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mpandas\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mpd\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m <a href='file:///home/rich/Test/manualTestFile.py?line=26'>27</a>\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mmatplotlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpyplot\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mplt\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'numpy'" + ]; + assert.equal(after3.join('\n'), modifyTraceback(trace3).join('\n'), 'Exception unix failure'); + when(fileSystem.getDisplayName(anything())).thenReturn('d:\\Training\\SnakePython\\foo.py'); + + await addCell( + `# %% + +def cause_error(): + print('start') + print('error') + print('now') + + print( 1 / 0) +`, + 'd:\\Training\\SnakePython\\foo.py', + 133 + ); + await addCell( + `# %% +print('some more') + +cause_error()`, + 'd:\\Training\\SnakePython\\foo.py', + 142 + ); + const trace4 = [ + '\u001b[1;31m---------------------------------------------------------------------------\u001b[0m', + '\u001b[1;31mZeroDivisionError\u001b[0m Traceback (most recent call last)', + "\u001b[1;32md:\\Training\\SnakePython\\foo.py\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'some more'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 2\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 3\u001b[1;33m \u001b[0mcause_error\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[1;32md:\\Training\\SnakePython\\foo.py\u001b[0m in \u001b[0;36mcause_error\u001b[1;34m()\u001b[0m\n\u001b[0;32m 4\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'now'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m \u001b[1;36m1\u001b[0m \u001b[1;33m/\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m" + ]; + const after4 = [ + '\u001b[1;31m---------------------------------------------------------------------------\u001b[0m', + '\u001b[1;31mZeroDivisionError\u001b[0m Traceback (most recent call last)', + "\u001b[1;32md:\\Training\\SnakePython\\foo.py\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m <a href='file://d:\\Training\\SnakePython\\foo.py?line=143'>144</a>\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'some more'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m <a href='file://d:\\Training\\SnakePython\\foo.py?line=144'>145</a>\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> <a href='file://d:\\Training\\SnakePython\\foo.py?line=145'>146</a>\u001b[1;33m \u001b[0mcause_error\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[1;32md:\\Training\\SnakePython\\foo.py\u001b[0m in \u001b[0;36mcause_error\u001b[1;34m()\u001b[0m\n\u001b[0;32m <a href='file://d:\\Training\\SnakePython\\foo.py?line=138'>139</a>\u001b[0m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'now'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m <a href='file://d:\\Training\\SnakePython\\foo.py?line=139'>140</a>\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> <a href='file://d:\\Training\\SnakePython\\foo.py?line=140'>141</a>\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m \u001b[1;36m1\u001b[0m \u001b[1;33m/\u001b[0m \u001b[1;36m0\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m" + ]; + assert.equal(after4.join('\n'), modifyTraceback(trace4).join('\n'), 'Multiple levels'); + }); +}); diff --git a/src/test/datascience/kernel-launcher/kernelDaemonPool.unit.test.ts b/src/test/datascience/kernel-launcher/kernelDaemonPool.unit.test.ts new file mode 100644 index 000000000000..a8e3c65997e2 --- /dev/null +++ b/src/test/datascience/kernel-launcher/kernelDaemonPool.unit.test.ts @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { DaemonExecutionFactoryCreationOptions, IPythonExecutionFactory } from '../../../client/common/process/types'; +import { ReadWrite, Resource } from '../../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { KernelDaemonPool } from '../../../client/datascience/kernel-launcher/kernelDaemonPool'; +import { IPythonKernelDaemon } from '../../../client/datascience/kernel-launcher/types'; +import { + IDataScienceFileSystem, + IJupyterKernelSpec, + IKernelDependencyService +} from '../../../client/datascience/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { sleep } from '../../core'; +import { createPythonInterpreter } from '../../utils/interpreters'; + +// tslint:disable: max-func-body-length no-any +suite('DataScience - Kernel Daemon Pool', () => { + const interpreter1 = createPythonInterpreter({ path: 'interpreter1' }); + const interpreter2 = createPythonInterpreter({ path: 'interpreter2' }); + const interpreter3 = createPythonInterpreter({ path: 'interpreter3' }); + const workspace1 = Uri.file('1'); + const workspace2 = Uri.file('2'); + const workspace3 = Uri.file('3'); + let didEnvVarsChange: EventEmitter<Resource>; + let didChangeInterpreter: EventEmitter<void>; + let daemon1: IPythonKernelDaemon; + let daemon2: IPythonKernelDaemon; + let daemon3: IPythonKernelDaemon; + let daemonPool: KernelDaemonPool; + let worksapceService: IWorkspaceService; + let kernelDependencyService: IKernelDependencyService; + let pythonExecutionFactory: IPythonExecutionFactory; + let envVars: IEnvironmentVariablesProvider; + let fs: IDataScienceFileSystem; + let interpeterService: IInterpreterService; + let kernelSpec: ReadWrite<IJupyterKernelSpec>; + let interpretersPerWorkspace: Map<string | undefined, PythonEnvironment>; + + setup(() => { + didEnvVarsChange = new EventEmitter<Resource>(); + didChangeInterpreter = new EventEmitter<void>(); + worksapceService = mock<IWorkspaceService>(); + kernelDependencyService = mock<IKernelDependencyService>(); + daemon1 = mock<IPythonKernelDaemon>(); + daemon2 = mock<IPythonKernelDaemon>(); + daemon3 = mock<IPythonKernelDaemon>(); + pythonExecutionFactory = mock<IPythonExecutionFactory>(); + envVars = mock<IEnvironmentVariablesProvider>(); + fs = mock<IDataScienceFileSystem>(); + interpeterService = mock<IInterpreterService>(); + interpretersPerWorkspace = new Map<string | undefined, PythonEnvironment>(); + interpretersPerWorkspace.set(workspace1.fsPath, interpreter1); + interpretersPerWorkspace.set(workspace2.fsPath, interpreter2); + interpretersPerWorkspace.set(workspace3.fsPath, interpreter3); + + (instance(daemon1) as any).then = undefined; + (instance(daemon2) as any).then = undefined; + (instance(daemon3) as any).then = undefined; + when(daemon1.preWarm()).thenResolve(); + when(daemon2.preWarm()).thenResolve(); + when(daemon3.preWarm()).thenResolve(); + when(daemon1.dispose()).thenResolve(); + when(daemon2.dispose()).thenResolve(); + when(daemon3.dispose()).thenResolve(); + + when(envVars.onDidEnvironmentVariablesChange).thenReturn(didEnvVarsChange.event); + when(interpeterService.onDidChangeInterpreter).thenReturn(didChangeInterpreter.event); + when(interpeterService.getActiveInterpreter(anything())).thenCall((uri?: Uri) => + interpretersPerWorkspace.get(uri?.fsPath) + ); + const daemonsCreatedForEachInterpreter = new Set<string>(); + when(pythonExecutionFactory.createDaemon(anything())).thenCall( + async (options: DaemonExecutionFactoryCreationOptions) => { + // Don't re-use daemons, just return a new one (else it stuffs up tests). + // I.e. we created a daemon once, then next time return a new daemon object. + if (daemonsCreatedForEachInterpreter.has(options.pythonPath!)) { + const newDaemon = mock<IPythonKernelDaemon>(); + (instance(newDaemon) as any).then = undefined; + return instance(newDaemon); + } + + daemonsCreatedForEachInterpreter.add(options.pythonPath!); + switch (options.pythonPath) { + case interpreter1.path: + return instance(daemon1); + case interpreter2.path: + return instance(daemon2); + case interpreter3.path: + return instance(daemon3); + default: + const newDaemon = mock<IPythonKernelDaemon>(); + (instance(newDaemon) as any).then = undefined; + return instance(newDaemon); + } + } + ); + when(kernelDependencyService.areDependenciesInstalled(anything())).thenResolve(true); + when(worksapceService.getWorkspaceFolderIdentifier(anything())).thenCall((uri: Uri) => uri.fsPath); + daemonPool = new KernelDaemonPool( + instance(worksapceService), + instance(envVars), + instance(fs), + instance(interpeterService), + instance(pythonExecutionFactory), + instance(kernelDependencyService) + ); + kernelSpec = { + argv: ['python', '-m', 'ipkernel_launcher', '-f', 'file.json'], + display_name: '', + env: undefined, + language: 'python', + name: '', + path: '' + }; + }); + test('Confirm we get pre-warmed daemons instead of creating new ones', async () => { + when(worksapceService.workspaceFolders).thenReturn([ + { index: 0, name: '', uri: workspace1 }, + { index: 0, name: '', uri: workspace2 } + ]); + await daemonPool.preWarmKernelDaemons(); + + // Verify we only created 2 daemons. + assert.equal(daemonPool.daemons, 2); + + let daemon = await daemonPool.get(workspace1, kernelSpec, interpreter1); + assert.equal(daemon, instance(daemon1)); + // Verify this daemon was pre-warmed. + verify(daemon1.preWarm()).atLeast(1); + + daemon = await daemonPool.get(workspace2, kernelSpec, interpreter2); + assert.equal(daemon, instance(daemon2)); + // Verify this daemon was pre-warmed. + verify(daemon1.preWarm()).atLeast(1); + + // Wait for background async to complete. + await sleep(1); + // Verify we created 2 more daemons. + assert.equal(daemonPool.daemons, 2); + }); + test('Pre-warming multiple times has no affect', async () => { + when(worksapceService.workspaceFolders).thenReturn([ + { index: 0, name: '', uri: workspace1 }, + { index: 0, name: '', uri: workspace2 } + ]); + await daemonPool.preWarmKernelDaemons(); + + // Verify we only created 2 daemons. + assert.equal(daemonPool.daemons, 2); + + // attempting to pre-warm again should be a noop. + await daemonPool.preWarmKernelDaemons(); + await daemonPool.preWarmKernelDaemons(); + await daemonPool.preWarmKernelDaemons(); + + // Verify we only created 2 daemons. + assert.equal(daemonPool.daemons, 2); + }); + test('Disposing daemonpool should kill all daemons in the pool', async () => { + when(worksapceService.workspaceFolders).thenReturn([ + { index: 0, name: '', uri: workspace1 }, + { index: 0, name: '', uri: workspace2 } + ]); + await daemonPool.preWarmKernelDaemons(); + + // Verify we only created 2 daemons. + assert.equal(daemonPool.daemons, 2); + + // Confirm daemons have been craeted. + verify(daemon1.preWarm()).once(); + verify(daemon2.preWarm()).once(); + + daemonPool.dispose(); + + // Confirm daemons have been disposed. + verify(daemon1.dispose()).once(); + verify(daemon2.dispose()).once(); + }); + test('Create new daemons even when not prewarmed', async () => { + const daemon = await daemonPool.get(workspace1, kernelSpec, interpreter1); + assert.equal(daemon, instance(daemon1)); + // Verify this daemon was not pre-warmed. + verify(daemon1.preWarm()).never(); + + // Wait for background async to complete. + await sleep(1); + assert.equal(daemonPool.daemons, 0); + }); + test('Create a new daemon if we do not have a pre-warmed daemon', async () => { + when(worksapceService.workspaceFolders).thenReturn([ + { index: 0, name: '', uri: workspace1 }, + { index: 0, name: '', uri: workspace2 } + ]); + await daemonPool.preWarmKernelDaemons(); + + // Verify we only created 2 daemons. + assert.equal(daemonPool.daemons, 2); + + const daemon = await daemonPool.get(workspace3, kernelSpec, interpreter3); + assert.equal(daemon, instance(daemon3)); + // Verify this daemon was not pre-warmed. + verify(daemon3.preWarm()).never(); + }); + test('Create a new daemon if our kernelspec has environment variables (will not use one from the pool of daemons)', async () => { + when(worksapceService.workspaceFolders).thenReturn([ + { index: 0, name: '', uri: workspace1 }, + { index: 0, name: '', uri: workspace2 } + ]); + await daemonPool.preWarmKernelDaemons(); + // Verify we created just 2 daemons. + verify(pythonExecutionFactory.createDaemon(anything())).twice(); + + kernelSpec.env = { HELLO: '1' }; + const daemon = await daemonPool.get(workspace3, kernelSpec, interpreter3); + assert.equal(daemon, instance(daemon3)); + // Verify this daemon was not pre-warmed. + verify(daemon3.preWarm()).never(); + + // Wait for background async to complete. + await sleep(1); + // Verify we created just 1 extra new daemon (2 previously prewarmed, one for the new damone). + verify(pythonExecutionFactory.createDaemon(anything())).times(3); + }); + test('After updating env varialbes we will always create new daemons, and not use the ones from the daemon pool', async () => { + when(worksapceService.workspaceFolders).thenReturn([ + { index: 0, name: '', uri: workspace1 }, + { index: 0, name: '', uri: workspace2 } + ]); + await daemonPool.preWarmKernelDaemons(); + // Verify we created just 2 daemons. + assert.equal(daemonPool.daemons, 2); + + // Update env vars for worksapce 1. + didEnvVarsChange.fire(workspace1); + // Wait for background async to complete. + await sleep(1); + + const daemon = await daemonPool.get(workspace1, kernelSpec, interpreter1); + // Verify it is a whole new daemon. + assert.notEqual(daemon, instance(daemon1)); + assert.notEqual(daemon, instance(daemon2)); + assert.notEqual(daemon, instance(daemon3)); + // Verify the pre-warmed daemon for workspace 1 was disposed. + verify(daemon1.dispose()).once(); + }); + test('After selecting a new interpreter we will always create new daemons, and not use the ones from the daemon pool', async () => { + when(worksapceService.workspaceFolders).thenReturn([ + { index: 0, name: '', uri: workspace1 }, + { index: 0, name: '', uri: workspace2 } + ]); + await daemonPool.preWarmKernelDaemons(); + // Verify we created just 2 daemons. + assert.equal(daemonPool.daemons, 2); + + // Update interpreter for workespace1. + when(interpeterService.getActiveInterpreter(anything())).thenCall((uri?: Uri) => { + if (uri?.fsPath === workspace1.fsPath) { + return createPythonInterpreter({ path: 'New' }); + } + interpretersPerWorkspace.get(uri?.fsPath); + }); + didChangeInterpreter.fire(); + // Wait for background async to complete. + await sleep(1); + + const daemon = await daemonPool.get(workspace1, kernelSpec, interpreter1); + // Verify it is a whole new daemon. + assert.notEqual(daemon, instance(daemon1)); + assert.notEqual(daemon, instance(daemon2)); + assert.notEqual(daemon, instance(daemon3)); + // Verify the pre-warmed daemon for workspace 1 was disposed. + verify(daemon1.dispose()).once(); + }); +}); diff --git a/src/test/datascience/kernel-launcher/kernelDaemonPoolPreWarmer.unit.test.ts b/src/test/datascience/kernel-launcher/kernelDaemonPoolPreWarmer.unit.test.ts new file mode 100644 index 000000000000..ec9600b11015 --- /dev/null +++ b/src/test/datascience/kernel-launcher/kernelDaemonPoolPreWarmer.unit.test.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { IConfigurationService, IExperimentsManager, IPythonSettings } from '../../../client/common/types'; +import { KernelDaemonPool } from '../../../client/datascience/kernel-launcher/kernelDaemonPool'; +import { KernelDaemonPreWarmer } from '../../../client/datascience/kernel-launcher/kernelDaemonPreWarmer'; +import { + IInteractiveWindowProvider, + INotebookAndInteractiveWindowUsageTracker, + INotebookEditorProvider, + IRawNotebookSupportedService +} from '../../../client/datascience/types'; + +// tslint:disable: max-func-body-length no-any +suite('DataScience - Kernel Daemon Pool PreWarmer', () => { + let prewarmer: KernelDaemonPreWarmer; + let notebookEditorProvider: INotebookEditorProvider; + let interactiveProvider: IInteractiveWindowProvider; + let usageTracker: INotebookAndInteractiveWindowUsageTracker; + let rawNotebookSupported: IRawNotebookSupportedService; + let configService: IConfigurationService; + let daemonPool: KernelDaemonPool; + let settings: IPythonSettings; + setup(() => { + notebookEditorProvider = mock<INotebookEditorProvider>(); + interactiveProvider = mock<IInteractiveWindowProvider>(); + usageTracker = mock<INotebookAndInteractiveWindowUsageTracker>(); + daemonPool = mock<KernelDaemonPool>(); + rawNotebookSupported = mock<IRawNotebookSupportedService>(); + configService = mock<IConfigurationService>(); + const experiment = mock<IExperimentsManager>(); + when(experiment.inExperiment(anything())).thenReturn(true); + + // Set up our config settings + settings = mock(PythonSettings); + when(configService.getSettings()).thenReturn(instance(settings)); + // tslint:disable-next-line: no-any + when(settings.datascience).thenReturn({} as any); + + prewarmer = new KernelDaemonPreWarmer( + instance(notebookEditorProvider), + instance(interactiveProvider), + [], + instance(usageTracker), + instance(daemonPool), + instance(rawNotebookSupported), + instance(configService) + ); + }); + test('Should not pre-warm daemon pool if ds was never used', async () => { + when(rawNotebookSupported.supported()).thenResolve(true); + when(usageTracker.lastInteractiveWindowOpened).thenReturn(undefined); + when(usageTracker.lastNotebookOpened).thenReturn(undefined); + + await prewarmer.activate(undefined); + + verify(daemonPool.preWarmKernelDaemons()).never(); + }); + + test('Should not pre-warm daemon pool raw kernel is not supported', async () => { + when(rawNotebookSupported.supported()).thenResolve(false); + + await prewarmer.activate(undefined); + + verify(daemonPool.preWarmKernelDaemons()).never(); + }); + + test('Prewarm if supported and the date works', async () => { + when(rawNotebookSupported.supported()).thenResolve(true); + when(usageTracker.lastInteractiveWindowOpened).thenReturn(new Date()); + when(usageTracker.lastNotebookOpened).thenReturn(new Date()); + + await prewarmer.activate(undefined); + + verify(daemonPool.preWarmKernelDaemons()).once(); + }); +}); diff --git a/src/test/datascience/kernel-launcher/kernelLauncherDaemon.unit.test.ts b/src/test/datascience/kernel-launcher/kernelLauncherDaemon.unit.test.ts new file mode 100644 index 000000000000..6adb9a49047d --- /dev/null +++ b/src/test/datascience/kernel-launcher/kernelLauncherDaemon.unit.test.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import { anything, deepEqual, instance, mock, when } from 'ts-mockito'; +import { IPythonExecutionService, ObservableExecutionResult } from '../../../client/common/process/types'; +import { ReadWrite } from '../../../client/common/types'; +import { KernelDaemonPool } from '../../../client/datascience/kernel-launcher/kernelDaemonPool'; +import { PythonKernelLauncherDaemon } from '../../../client/datascience/kernel-launcher/kernelLauncherDaemon'; +import { IPythonKernelDaemon } from '../../../client/datascience/kernel-launcher/types'; +import { IJupyterKernelSpec } from '../../../client/datascience/types'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { createPythonInterpreter } from '../../utils/interpreters'; + +// tslint:disable: max-func-body-length no-any +suite('DataScience - Kernel Launcher Daemon', () => { + let launcher: PythonKernelLauncherDaemon; + let daemonPool: KernelDaemonPool; + let interpreter: PythonEnvironment; + let kernelSpec: ReadWrite<IJupyterKernelSpec>; + let kernelDaemon: IPythonKernelDaemon; + let observableOutputForDaemon: ObservableExecutionResult<string>; + setup(() => { + kernelSpec = { + argv: ['python', '-m', 'ipkernel_launcher', '-f', 'file.json'], + display_name: '', + env: { hello: '1' }, + language: 'python', + name: '', + path: '' + }; + interpreter = createPythonInterpreter(); + daemonPool = mock(KernelDaemonPool); + observableOutputForDaemon = mock<ObservableExecutionResult<string>>(); + kernelDaemon = mock<IPythonKernelDaemon>(); + // Else ts-mockit doesn't allow us to return an instance of a mock as a return value from an async function. + (instance(kernelDaemon) as any).then = undefined; + // Else ts-mockit doesn't allow us to return an instance of a mock as a return value from an async function. + (instance(observableOutputForDaemon) as any).then = undefined; + + when(daemonPool.get(anything(), anything(), anything())).thenResolve(instance(kernelDaemon)); + when(observableOutputForDaemon.proc).thenResolve({} as any); + when(kernelDaemon.start('ipkernel_launcher', deepEqual(['-f', 'file.json']), anything())).thenResolve( + instance(observableOutputForDaemon) + ); + launcher = new PythonKernelLauncherDaemon(instance(daemonPool)); + }); + test('Does not support launching kernels if there is no -m in argv', async () => { + kernelSpec.argv = ['wow']; + const promise = launcher.launch(undefined, '', kernelSpec, interpreter); + + await assert.isRejected(promise, /^Unsupported KernelSpec file. args must be/g); + }); + test('Creates and returns a daemon', async () => { + const daemonCreationOutput = await launcher.launch(undefined, '', kernelSpec, interpreter); + + assert.isDefined(daemonCreationOutput); + + if (daemonCreationOutput) { + assert.equal(daemonCreationOutput.observableOutput, instance(observableOutputForDaemon)); + assert.equal(daemonCreationOutput.daemon, instance(kernelDaemon)); + } + }); + test('If our daemon pool returns an execution service, then use it and return the daemon as undefined', async () => { + const executionService = mock<IPythonExecutionService>(); + when( + executionService.execModuleObservable('ipkernel_launcher', deepEqual(['-f', 'file.json']), anything()) + ).thenReturn(instance(observableOutputForDaemon)); + // Else ts-mockit doesn't allow us to return an instance of a mock as a return value from an async function. + (instance(executionService) as any).then = undefined; + when(daemonPool.get(anything(), anything(), anything())).thenResolve(instance(executionService) as any); + const daemonCreationOutput = await launcher.launch(undefined, '', kernelSpec, interpreter); + + assert.equal(daemonCreationOutput.observableOutput, instance(observableOutputForDaemon)); + assert.isUndefined(daemonCreationOutput.daemon); + }); +}); diff --git a/src/test/datascience/kernelFinder.unit.test.ts b/src/test/datascience/kernelFinder.unit.test.ts new file mode 100644 index 000000000000..696864406153 --- /dev/null +++ b/src/test/datascience/kernelFinder.unit.test.ts @@ -0,0 +1,603 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { assert, expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; + +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { IPlatformService } from '../../client/common/platform/types'; +import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; +import { IExtensionContext, IInstaller, IPathUtils, Resource } from '../../client/common/types'; +import { Architecture } from '../../client/common/utils/platform'; +import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; +import { defaultKernelSpecName } from '../../client/datascience/jupyter/kernels/helpers'; +import { JupyterKernelSpec } from '../../client/datascience/jupyter/kernels/jupyterKernelSpec'; +import { KernelFinder } from '../../client/datascience/kernel-launcher/kernelFinder'; +import { IKernelFinder } from '../../client/datascience/kernel-launcher/types'; +import { IDataScienceFileSystem, IJupyterKernelSpec } from '../../client/datascience/types'; +import { IInterpreterLocatorService, IInterpreterService } from '../../client/interpreter/contracts'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; + +suite('Kernel Finder', () => { + let interpreterService: typemoq.IMock<IInterpreterService>; + let interpreterLocator: typemoq.IMock<IInterpreterLocatorService>; + let fileSystem: typemoq.IMock<IDataScienceFileSystem>; + let platformService: typemoq.IMock<IPlatformService>; + let pathUtils: typemoq.IMock<IPathUtils>; + let context: typemoq.IMock<IExtensionContext>; + let envVarsProvider: typemoq.IMock<IEnvironmentVariablesProvider>; + let installer: IInstaller; + let workspaceService: IWorkspaceService; + let kernelFinder: IKernelFinder; + let activeInterpreter: PythonEnvironment; + let interpreters: PythonEnvironment[] = []; + let resource: Resource; + const kernelName = 'testKernel'; + const testKernelMetadata = { name: 'testKernel', display_name: 'Test Display Name' }; + const cacheFile = 'kernelSpecPathCache.json'; + const kernel: JupyterKernelSpec = { + name: 'testKernel', + language: 'python', + path: '<python path>', + display_name: 'Python 3', + metadata: {}, + env: {}, + argv: ['<python path>', '-m', 'ipykernel_launcher', '-f', '{connection_file}'], + specFile: path.join('1', 'share', 'jupyter', 'kernels', kernelName, 'kernel.json') + }; + // Change this to your actual JUPYTER_PATH value and see it appearing on the paths in the kernelFinder + let JupyterPathEnvVar = ''; + + function setupFileSystem() { + fileSystem + .setup((fs) => fs.writeLocalFile(typemoq.It.isAnyString(), typemoq.It.isAnyString())) + .returns(() => Promise.resolve()); + // fileSystem.setup((fs) => fs.getSubDirectories(typemoq.It.isAnyString())).returns(() => Promise.resolve([''])); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), typemoq.It.isAnyString(), typemoq.It.isAny())) + .returns(() => + Promise.resolve([ + path.join(kernel.name, 'kernel.json'), + path.join('kernelA', 'kernel.json'), + path.join('kernelB', 'kernel.json') + ]) + ); + } + + function setupFindFileSystem() { + fileSystem + .setup((fs) => fs.writeLocalFile(typemoq.It.isAnyString(), typemoq.It.isAnyString())) + .returns(() => Promise.resolve()); + // fileSystem.setup((fs) => fs.getSubDirectories(typemoq.It.isAnyString())).returns(() => Promise.resolve([''])); + } + + setup(() => { + pathUtils = typemoq.Mock.ofType<IPathUtils>(); + pathUtils.setup((pu) => pu.home).returns(() => './'); + + context = typemoq.Mock.ofType<IExtensionContext>(); + context.setup((c) => c.globalStoragePath).returns(() => './'); + fileSystem = typemoq.Mock.ofType<IDataScienceFileSystem>(); + + installer = mock<IInstaller>(); + when(installer.isInstalled(anything(), anything())).thenResolve(true); + + platformService = typemoq.Mock.ofType<IPlatformService>(); + platformService.setup((ps) => ps.isWindows).returns(() => true); + platformService.setup((ps) => ps.isMac).returns(() => true); + + envVarsProvider = typemoq.Mock.ofType<IEnvironmentVariablesProvider>(); + envVarsProvider + .setup((e) => e.getEnvironmentVariables(typemoq.It.isAny())) + .returns(() => Promise.resolve({ JUPYTER_PATH: JupyterPathEnvVar })); + }); + + suite('listKernelSpecs', () => { + let activeKernelA: IJupyterKernelSpec; + let activeKernelB: IJupyterKernelSpec; + let interpreter0Kernel: IJupyterKernelSpec; + let interpreter1Kernel: IJupyterKernelSpec; + let globalKernel: IJupyterKernelSpec; + let jupyterPathKernelA: IJupyterKernelSpec; + let jupyterPathKernelB: IJupyterKernelSpec; + let loadError = false; + setup(() => { + JupyterPathEnvVar = `Users/testuser/jupyterPathDirA${path.delimiter}Users/testuser/jupyterPathDirB`; + + activeInterpreter = { + path: context.object.globalStoragePath, + displayName: 'activeInterpreter', + sysPrefix: 'active', + envName: '1', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + envType: EnvironmentType.Unknown + }; + interpreters = []; + for (let i = 0; i < 2; i += 1) { + interpreters.push({ + path: `${context.object.globalStoragePath}_${i}`, + sysPrefix: `Interpreter${i}`, + envName: '1', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + envType: EnvironmentType.Unknown + }); + } + + // Our defaultresource + resource = Uri.file('abc'); + + // Set our active interpreter + interpreterService = typemoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((is) => is.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => Promise.resolve(activeInterpreter)); + + // Set our workspace interpreters + interpreterLocator = typemoq.Mock.ofType<IInterpreterLocatorService>(); + interpreterLocator + .setup((il) => il.getInterpreters(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(interpreters)); + + activeKernelA = { + name: 'activeKernelA', + language: 'python', + path: '<python path>', + display_name: 'Python 3', + metadata: {}, + env: {}, + argv: ['<python path>', '-m', 'ipykernel_launcher', '-f', '{connection_file}'] + }; + + activeKernelB = { + name: 'activeKernelB', + language: 'python', + path: '<python path>', + display_name: 'Python 3', + metadata: {}, + env: {}, + argv: ['<python path>', '-m', 'ipykernel_launcher', '-f', '{connection_file}'] + }; + + interpreter0Kernel = { + name: 'interpreter0Kernel', + language: 'python', + path: '<python path>', + display_name: 'Python 3', + metadata: {}, + env: {}, + argv: ['<python path>', '-m', 'ipykernel_launcher', '-f', '{connection_file}'] + }; + + interpreter1Kernel = { + name: 'interpreter1Kernel', + language: 'python', + path: '<python path>', + display_name: 'Python 3', + metadata: {}, + env: {}, + argv: ['<python path>', '-m', 'ipykernel_launcher', '-f', '{connection_file}'] + }; + + globalKernel = { + name: 'globalKernel', + language: 'python', + path: '<python path>', + display_name: 'Python 3', + metadata: {}, + env: {}, + argv: ['<python path>', '-m', 'ipykernel_launcher', '-f', '{connection_file}'] + }; + + jupyterPathKernelA = { + name: 'jupyterPathKernelA', + language: 'python', + path: '<python path>', + display_name: 'Python 3', + metadata: {}, + env: {}, + argv: ['<python path>', '-m', 'ipykernel_launcher', '-f', '{connection_file}'] + }; + + jupyterPathKernelB = { + name: 'jupyterPathKernelB', + language: 'python', + path: '<python path>', + display_name: 'Python 3', + metadata: {}, + env: {}, + argv: ['<python path>', '-m', 'ipykernel_launcher', '-f', '{connection_file}'] + }; + + platformService.reset(); + platformService.setup((ps) => ps.isWindows).returns(() => false); + platformService.setup((ps) => ps.isMac).returns(() => true); + + workspaceService = mock<IWorkspaceService>(); + when(workspaceService.getWorkspaceFolderIdentifier(anything(), resource.fsPath)).thenReturn( + resource.fsPath + ); + + // Setup file system + const activePath = path.join('active', 'share', 'jupyter', 'kernels'); + const activePathA = path.join(activePath, activeKernelA.name, 'kernel.json'); + const activePathB = path.join(activePath, activeKernelB.name, 'kernel.json'); + fileSystem + .setup((fs) => fs.writeLocalFile(typemoq.It.isAnyString(), typemoq.It.isAnyString())) + .returns(() => Promise.resolve()); + // fileSystem + // .setup((fs) => fs.getSubDirectories(typemoq.It.isAnyString())) + // .returns(() => Promise.resolve([''])); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), activePath, typemoq.It.isAny())) + .returns(() => + Promise.resolve([ + path.join(activeKernelA.name, 'kernel.json'), + path.join(activeKernelB.name, 'kernel.json') + ]) + ); + const interpreter0Path = path.join('Interpreter0', 'share', 'jupyter', 'kernels'); + const interpreter0FullPath = path.join(interpreter0Path, interpreter0Kernel.name, 'kernel.json'); + const interpreter1Path = path.join('Interpreter1', 'share', 'jupyter', 'kernels'); + const interpreter1FullPath = path.join(interpreter1Path, interpreter1Kernel.name, 'kernel.json'); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), interpreter0Path, typemoq.It.isAny())) + .returns(() => Promise.resolve([path.join(interpreter0Kernel.name, 'kernel.json')])); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), interpreter1Path, typemoq.It.isAny())) + .returns(() => Promise.resolve([path.join(interpreter1Kernel.name, 'kernel.json')])); + + // Global path setup + const globalPath = path.join('usr', 'share', 'jupyter', 'kernels'); + const globalFullPath = path.join(globalPath, globalKernel.name, 'kernel.json'); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), globalPath, typemoq.It.isAny())) + .returns(() => Promise.resolve([path.join(globalKernel.name, 'kernel.json')])); + + // Empty global paths + const globalAPath = path.join('usr', 'local', 'share', 'jupyter', 'kernels'); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), globalAPath, typemoq.It.isAny())) + .returns(() => Promise.resolve([])); + const globalBPath = path.join('Library', 'Jupyter', 'kernels'); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), globalBPath, typemoq.It.isAny())) + .returns(() => Promise.resolve([])); + + // Jupyter path setup + const jupyterPathKernelAPath = path.join('Users', 'testuser', 'jupyterPathDirA', 'kernels'); + const jupyterPathKernelAFullPath = path.join( + jupyterPathKernelAPath, + jupyterPathKernelA.name, + 'kernel.json' + ); + const jupyterPathKernelBPath = path.join('Users', 'testuser', 'jupyterPathDirB', 'kernels'); + const jupyterPathKernelBFullPath = path.join( + jupyterPathKernelBPath, + jupyterPathKernelB.name, + 'kernel.json' + ); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), jupyterPathKernelAPath, typemoq.It.isAny())) + .returns(() => Promise.resolve([path.join(jupyterPathKernelA.name, 'kernel.json')])); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), jupyterPathKernelBPath, typemoq.It.isAny())) + .returns(() => Promise.resolve([path.join(jupyterPathKernelB.name, 'kernel.json')])); + + // Set the file system to return our kernelspec json + fileSystem + .setup((fs) => fs.readLocalFile(typemoq.It.isAnyString())) + .returns((param: string) => { + switch (param) { + case activePathA: + if (!loadError) { + return Promise.resolve(JSON.stringify(activeKernelA)); + } else { + return Promise.resolve(''); + } + case activePathB: + return Promise.resolve(JSON.stringify(activeKernelB)); + case interpreter0FullPath: + return Promise.resolve(JSON.stringify(interpreter0Kernel)); + case interpreter1FullPath: + return Promise.resolve(JSON.stringify(interpreter1Kernel)); + case globalFullPath: + return Promise.resolve(JSON.stringify(globalKernel)); + case jupyterPathKernelAFullPath: + return Promise.resolve(JSON.stringify(jupyterPathKernelA)); + case jupyterPathKernelBFullPath: + return Promise.resolve(JSON.stringify(jupyterPathKernelB)); + default: + return Promise.resolve(''); + } + }); + + const executionFactory = mock(PythonExecutionFactory); + + kernelFinder = new KernelFinder( + interpreterService.object, + interpreterLocator.object, + platformService.object, + fileSystem.object, + pathUtils.object, + instance(installer), + context.object, + instance(workspaceService), + instance(executionFactory), + envVarsProvider.object + ); + }); + + test('Basic listKernelSpecs', async () => { + setupFindFileSystem(); + const specs = await kernelFinder.listKernelSpecs(resource); + expect(specs[0]).to.deep.include(activeKernelA); + expect(specs[1]).to.deep.include(activeKernelB); + expect(specs[2]).to.deep.include(interpreter0Kernel); + expect(specs[3]).to.deep.include(interpreter1Kernel); + expect(specs[4]).to.deep.include(jupyterPathKernelA); + expect(specs[5]).to.deep.include(jupyterPathKernelB); + expect(specs[6]).to.deep.include(globalKernel); + fileSystem.reset(); + }); + + test('listKernelSpecs load error', async () => { + setupFindFileSystem(); + loadError = true; + const specs = await kernelFinder.listKernelSpecs(resource); + expect(specs[0]).to.deep.include(activeKernelB); + expect(specs[1]).to.deep.include(interpreter0Kernel); + expect(specs[2]).to.deep.include(interpreter1Kernel); + expect(specs[3]).to.deep.include(jupyterPathKernelA); + expect(specs[4]).to.deep.include(jupyterPathKernelB); + expect(specs[5]).to.deep.include(globalKernel); + fileSystem.reset(); + }); + }); + + suite('findKernelSpec', () => { + setup(() => { + interpreterService = typemoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((is) => is.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => Promise.resolve(activeInterpreter)); + interpreterService + .setup((is) => is.getInterpreterDetails(typemoq.It.isAny())) + .returns(() => Promise.resolve(activeInterpreter)); + + interpreterLocator = typemoq.Mock.ofType<IInterpreterLocatorService>(); + interpreterLocator + .setup((il) => il.getInterpreters(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(interpreters)); + + fileSystem = typemoq.Mock.ofType<IDataScienceFileSystem>(); + + activeInterpreter = { + path: context.object.globalStoragePath, + displayName: 'activeInterpreter', + sysPrefix: '1', + envName: '1', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + envType: EnvironmentType.Unknown + }; + for (let i = 0; i < 10; i += 1) { + interpreters.push({ + path: `${context.object.globalStoragePath}_${i}`, + sysPrefix: '1', + envName: '1', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + envType: EnvironmentType.Unknown + }); + } + interpreters.push(activeInterpreter); + resource = Uri.file(context.object.globalStoragePath); + + workspaceService = mock<IWorkspaceService>(); + const executionFactory = mock(PythonExecutionFactory); + + kernelFinder = new KernelFinder( + interpreterService.object, + interpreterLocator.object, + platformService.object, + fileSystem.object, + pathUtils.object, + instance(installer), + context.object, + instance(workspaceService), + instance(executionFactory), + envVarsProvider.object + ); + }); + + test('KernelSpec is in cache', async () => { + setupFileSystem(); + fileSystem + .setup((fs) => fs.readLocalFile(typemoq.It.isAnyString())) + .returns((param: string) => { + if (param.includes(cacheFile)) { + return Promise.resolve(`["${kernel.name}"]`); + } + return Promise.resolve(JSON.stringify(kernel)); + }); + const spec = await kernelFinder.findKernelSpec(resource, testKernelMetadata); + assert.deepEqual(spec, kernel, 'The found kernel spec is not the same.'); + fileSystem.reset(); + }); + + test('KernelSpec is in the active interpreter', async () => { + setupFileSystem(); + fileSystem + .setup((fs) => fs.readLocalFile(typemoq.It.isAnyString())) + .returns((pathParam: string) => { + if (pathParam.includes(cacheFile)) { + return Promise.resolve('[]'); + } + return Promise.resolve(JSON.stringify(kernel)); + }); + const spec = await kernelFinder.findKernelSpec(resource, testKernelMetadata); + expect(spec).to.deep.include(kernel); + fileSystem.reset(); + }); + + test('No kernel name given, then return undefined.', async () => { + setupFileSystem(); + + // Create a second active interpreter to return on the second call + const activeInterpreter2 = { + path: context.object.globalStoragePath, + displayName: 'activeInterpreter2', + sysPrefix: '1', + envName: '1', + sysVersion: '3.1.1.1', + architecture: Architecture.x64, + envType: EnvironmentType.Unknown + }; + // Record a second call to getActiveInterpreter, will play after the first + interpreterService + .setup((is) => is.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => Promise.resolve(activeInterpreter2)); + + fileSystem + .setup((fs) => fs.readLocalFile(typemoq.It.isAnyString())) + .returns((pathParam: string) => { + if (pathParam.includes(cacheFile)) { + return Promise.resolve('[]'); + } + return Promise.resolve(JSON.stringify(kernel)); + }); + const spec = await kernelFinder.findKernelSpec(resource); + assert.isUndefined(spec); + fileSystem.reset(); + }); + + test('KernelSpec is in the interpreters', async () => { + setupFileSystem(); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), typemoq.It.isAnyString(), typemoq.It.isAny())) + .returns(() => Promise.resolve([])); + fileSystem + .setup((fs) => fs.readLocalFile(typemoq.It.isAnyString())) + .returns((pathParam: string) => { + if (pathParam.includes(cacheFile)) { + return Promise.resolve('[]'); + } + return Promise.resolve(JSON.stringify(kernel)); + }); + const spec = await kernelFinder.findKernelSpec(activeInterpreter, testKernelMetadata); + expect(spec).to.deep.include(kernel); + fileSystem.reset(); + }); + + test('KernelSpec is in disk', async () => { + setupFileSystem(); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), typemoq.It.isAnyString(), typemoq.It.isAny())) + .returns(() => Promise.resolve([kernelName])); + fileSystem + .setup((fs) => fs.readLocalFile(typemoq.It.isAnyString())) + .returns((pathParam: string) => { + if (pathParam.includes(cacheFile)) { + return Promise.resolve('[]'); + } + return Promise.resolve(JSON.stringify(kernel)); + }); + interpreterService + .setup((is) => is.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + const spec = await kernelFinder.findKernelSpec(activeInterpreter, testKernelMetadata); + expect(spec).to.deep.include(kernel); + fileSystem.reset(); + }); + + test('KernelSpec not found, returning undefined', async () => { + setupFileSystem(); + fileSystem + .setup((fs) => fs.readLocalFile(typemoq.It.isAnyString())) + .returns((pathParam: string) => { + if (pathParam.includes(cacheFile)) { + return Promise.resolve('[]'); + } + return Promise.resolve('{}'); + }); + // get default kernel + const spec = await kernelFinder.findKernelSpec(resource); + assert.isUndefined(spec); + fileSystem.reset(); + }); + + test('Kernel metadata already has a default spec, and kernel spec not found, then return undefined', async () => { + setupFileSystem(); + fileSystem + .setup((fs) => fs.readLocalFile(typemoq.It.isAnyString())) + .returns((pathParam: string) => { + if (pathParam.includes(cacheFile)) { + return Promise.resolve('[]'); + } + return Promise.resolve('{}'); + }); + // get default kernel + const spec = await kernelFinder.findKernelSpec(resource, { + name: defaultKernelSpecName, + display_name: 'TargetDisplayName' + }); + assert.isUndefined(spec); + fileSystem.reset(); + }); + + test('Look for KernelA with no cache, find KernelA and KenelB, then search for KernelB and find it in cache', async () => { + setupFileSystem(); + fileSystem + .setup((fs) => fs.readLocalFile(typemoq.It.isAnyString())) + .returns((pathParam: string) => { + if (pathParam.includes(cacheFile)) { + return Promise.resolve('[]'); + } else if (pathParam.includes('kernelA')) { + const specA = { + ...kernel, + name: 'kernelA' + }; + return Promise.resolve(JSON.stringify(specA)); + } + return Promise.resolve(''); + }); + + const spec = await kernelFinder.findKernelSpec(resource, { name: 'kernelA', display_name: '' }); + assert.equal(spec!.name.includes('kernelA'), true); + fileSystem.reset(); + + setupFileSystem(); + fileSystem + .setup((fs) => fs.searchLocal(typemoq.It.isAnyString(), typemoq.It.isAnyString(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); // this never executing means the kernel was found in cache + fileSystem + .setup((fs) => fs.readLocalFile(typemoq.It.isAnyString())) + .returns((pathParam: string) => { + if (pathParam.includes(cacheFile)) { + return Promise.resolve( + JSON.stringify([ + path.join('kernels', kernel.name, 'kernel.json'), + path.join('kernels', 'kernelA', 'kernel.json'), + path.join('kernels', 'kernelB', 'kernel.json') + ]) + ); + } else if (pathParam.includes('kernelB')) { + const specB = { + ...kernel, + name: 'kernelB' + }; + return Promise.resolve(JSON.stringify(specB)); + } + return Promise.resolve('{}'); + }); + const spec2 = await kernelFinder.findKernelSpec(resource, { name: 'kernelB', display_name: '' }); + assert.equal(spec2!.name.includes('kernelB'), true); + }); + }); +}); diff --git a/src/test/datascience/kernelLauncher.functional.test.ts b/src/test/datascience/kernelLauncher.functional.test.ts new file mode 100644 index 000000000000..d74d66c425fd --- /dev/null +++ b/src/test/datascience/kernelLauncher.functional.test.ts @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { assert, use } from 'chai'; + +import { KernelMessage } from '@jupyterlab/services'; +import * as uuid from 'uuid/v4'; +import { IProcessServiceFactory } from '../../client/common/process/types'; +import { createDeferred } from '../../client/common/utils/async'; +import { JupyterZMQBinariesNotFoundError } from '../../client/datascience/jupyter/jupyterZMQBinariesNotFoundError'; +import { KernelDaemonPool } from '../../client/datascience/kernel-launcher/kernelDaemonPool'; +import { KernelLauncher } from '../../client/datascience/kernel-launcher/kernelLauncher'; +import { IKernelConnection, IKernelFinder } from '../../client/datascience/kernel-launcher/types'; +import { createRawKernel } from '../../client/datascience/raw-kernel/rawKernel'; +import { IDataScienceFileSystem, IJupyterKernelSpec } from '../../client/datascience/types'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { sleep, waitForCondition } from '../common'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { takeSnapshot, writeDiffSnapshot } from './helpers'; +import { MockKernelFinder } from './mockKernelFinder'; +import { requestExecute } from './raw-kernel/rawKernelTestHelpers'; + +// Chai as promised is not part of this file +import * as chaiAsPromised from 'chai-as-promised'; +use(chaiAsPromised); + +suite('DataScience - Kernel Launcher', () => { + let ioc: DataScienceIocContainer; + let kernelLauncher: KernelLauncher; + let pythonInterpreter: PythonEnvironment | undefined; + let kernelSpec: IJupyterKernelSpec; + let kernelFinder: MockKernelFinder; + // tslint:disable-next-line: no-any + let snapshot: any; + + suiteSetup(() => { + snapshot = takeSnapshot(); + }); + + setup(async () => { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(); + kernelFinder = new MockKernelFinder(ioc.get<IKernelFinder>(IKernelFinder)); + const processServiceFactory = ioc.get<IProcessServiceFactory>(IProcessServiceFactory); + const daemonPool = ioc.get<KernelDaemonPool>(KernelDaemonPool); + const fileSystem = ioc.get<IDataScienceFileSystem>(IDataScienceFileSystem); + kernelLauncher = new KernelLauncher(processServiceFactory, fileSystem, daemonPool); + await ioc.activate(); + if (!ioc.mockJupyter) { + pythonInterpreter = await ioc.getJupyterCapableInterpreter(); + kernelSpec = { + argv: [pythonInterpreter!.path, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], + display_name: 'new kernel', + language: 'python', + name: 'newkernel', + path: 'path', + env: undefined + }; + } + }); + + suiteTeardown(() => { + writeDiffSnapshot(snapshot, 'KernelLauncher'); + }); + + test('Launch from kernelspec', async function () { + if (!process.env.VSCODE_PYTHON_ROLLING) { + // tslint:disable-next-line: no-invalid-this + this.skip(); + } else { + let exitExpected = false; + const deferred = createDeferred<boolean>(); + const kernel = await kernelLauncher.launch( + { kernelSpec, kind: 'startUsingKernelSpec' }, + undefined, + process.cwd() + ); + kernel.exited(() => { + if (exitExpected) { + deferred.resolve(true); + } else { + deferred.reject(new Error('Kernel exited prematurely')); + } + }); + + assert.isOk<IKernelConnection | undefined>(kernel.connection, 'Connection not found'); + + // It should not exit. + await assert.isRejected( + waitForCondition(() => deferred.promise, 2_000, 'Timeout'), + 'Timeout' + ); + + // Upon disposing, we should get an exit event within 100ms or less. + // If this happens, then we know a process existed. + exitExpected = true; + await kernel.dispose(); + await deferred.promise; + } + }).timeout(10_000); + + test('Launch with environment', async function () { + if (!process.env.VSCODE_PYTHON_ROLLING || !pythonInterpreter) { + // tslint:disable-next-line: no-invalid-this + this.skip(); + } else { + const spec: IJupyterKernelSpec = { + name: 'foo', + language: 'python', + path: pythonInterpreter.path, + display_name: pythonInterpreter.displayName || 'foo', + argv: [pythonInterpreter.path, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], + env: { + TEST_VAR: '1' + } + }; + kernelFinder.addKernelSpec(pythonInterpreter.path, spec); + + const kernel = await kernelLauncher.launch( + { kernelSpec: spec, kind: 'startUsingKernelSpec' }, + undefined, + process.cwd() + ); + const exited = new Promise<boolean>((resolve) => kernel.exited(() => resolve(true))); + + assert.isOk<IKernelConnection | undefined>(kernel.connection, 'Connection not found'); + + // Send a request to print out the env vars + const rawKernel = createRawKernel(kernel, uuid()); + + const result = await requestExecute(rawKernel, 'import os\nprint(os.getenv("TEST_VAR"))'); + assert.ok(result, 'No result returned'); + // Should have a stream output message + const output = result.find((r) => r.header.msg_type === 'stream') as KernelMessage.IStreamMsg; + assert.ok(output, 'no stream output'); + assert.equal(output.content.text, '1\n', 'Wrong content found on message'); + + // Upon disposing, we should get an exit event within 100ms or less. + // If this happens, then we know a process existed. + await kernel.dispose(); + assert.isRejected( + waitForCondition(() => exited, 100, 'Timeout'), + 'Timeout' + ); + } + }).timeout(10_000); + + test('Bind with ZMQ', async function () { + if (!process.env.VSCODE_PYTHON_ROLLING) { + // tslint:disable-next-line: no-invalid-this + this.skip(); + } else { + const kernel = await kernelLauncher.launch( + { kernelSpec, kind: 'startUsingKernelSpec' }, + undefined, + process.cwd() + ); + + try { + const zmq = await import('zeromq'); + const sock = new zmq.Pull(); + + sock.connect(`tcp://${kernel.connection!.ip}:${kernel.connection!.stdin_port}`); + sock.receive().ignoreErrors(); // This will never return unless the kenrel process sends something. Just used for testing the API is available + await sleep(50); + sock.close(); + } catch (e) { + throw new JupyterZMQBinariesNotFoundError(e.toString()); + } finally { + await kernel.dispose(); + } + } + }); +}); diff --git a/src/test/datascience/liveloss.py b/src/test/datascience/liveloss.py new file mode 100644 index 000000000000..a8187e717761 --- /dev/null +++ b/src/test/datascience/liveloss.py @@ -0,0 +1,18 @@ +#%% +from time import sleep +import numpy as np + +from livelossplot import PlotLosses + +#%% +liveplot = PlotLosses() + +for i in range(10): + liveplot.update({ + 'accuracy': 1 - np.random.rand() / (i + 2.), + 'val_accuracy': 1 - np.random.rand() / (i + 0.5), + 'mse': 1. / (i + 2.), + 'val_mse': 1. / (i + 0.5) + }) + liveplot.draw() + sleep(1.) diff --git a/src/test/datascience/liveshare.functional.test.tsx b/src/test/datascience/liveshare.functional.test.tsx new file mode 100644 index 000000000000..0a8b6a0fa3dc --- /dev/null +++ b/src/test/datascience/liveshare.functional.test.tsx @@ -0,0 +1,442 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; +import { ReactWrapper } from 'enzyme'; +import * as React from 'react'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Uri } from 'vscode'; +import * as vsls from 'vsls/vscode'; + +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + ILiveShareApi, + ILiveShareTestingApi +} from '../../client/common/application/types'; +import { LocalZMQKernel } from '../../client/common/experiments/groups'; +import { Resource } from '../../client/common/types'; +import { Commands } from '../../client/datascience/constants'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; +import { + ICodeWatcher, + IDataScienceCommandListener, + IDataScienceFileSystem, + IInteractiveWindowProvider, + IJupyterExecution +} from '../../client/datascience/types'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { createDocument } from './editor-integration/helpers'; +import { MockFileSystem } from './mockFileSystem'; +import { addMockData, CellPosition, mountConnectedMainPanel, verifyHtmlOnCell } from './testHelpers'; +import { TestInteractiveWindowProvider } from './testInteractiveWindowProvider'; +//import { asyncDump } from '../common/asyncDump'; +//tslint:disable:trailing-comma no-any no-multiline-string + +// tslint:disable-next-line:max-func-body-length no-any +suite('DataScience LiveShare tests', () => { + const disposables: Disposable[] = []; + let hostContainer: DataScienceIocContainer; + let guestContainer: DataScienceIocContainer; + let lastErrorMessage: string | undefined; + + setup(async () => { + hostContainer = createContainer(vsls.Role.Host); + guestContainer = createContainer(vsls.Role.Guest); + return Promise.all([hostContainer.activate(), guestContainer.activate()]); + }); + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + if (hostContainer) { + await hostContainer.dispose(); + } + if (guestContainer) { + await guestContainer.dispose(); + } + lastErrorMessage = undefined; + }); + + suiteTeardown(() => { + //asyncDump(); + }); + + function createContainer(role: vsls.Role): DataScienceIocContainer { + const result = new DataScienceIocContainer(); + result.registerDataScienceTypes(); + + // Rebind the appshell so we can change what happens on an error + const dummyDisposable = { + dispose: () => { + return; + } + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((e) => (lastErrorMessage = e)); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve('')); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2)); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(Uri.file('test.ipynb'))); + appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + + result.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); + + // Setup our webview panel + result.createWebView(() => mountConnectedMainPanel('interactive'), 'default', role); + + // Make sure the history provider and execution factory in the container is created (the extension does this on startup in the extension) + // This is necessary to get the appropriate live share services up and running. + result.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); + result.get<IJupyterExecution>(IJupyterExecution); + return result; + } + + async function getOrCreateInteractiveWindow(role: vsls.Role, owner?: Resource) { + // Get the container to use based on the role. + const container = role === vsls.Role.Host ? hostContainer : guestContainer; + const interactiveWindowProvider = container.get<TestInteractiveWindowProvider>(IInteractiveWindowProvider); + const window = (await interactiveWindowProvider.getOrCreate(owner)) as InteractiveWindow; + const mount = interactiveWindowProvider.getMountedWebView(window); + await window.show(); + return { window, mount }; + } + + function isSessionStarted(role: vsls.Role): boolean { + const container = role === vsls.Role.Host ? hostContainer : guestContainer; + const api = container!.get<ILiveShareApi>(ILiveShareApi) as ILiveShareTestingApi; + return api.isSessionStarted; + } + + async function waitForResults( + role: vsls.Role, + resultGenerator: (both: boolean) => Promise<void> + ): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { + const container = role === vsls.Role.Host ? hostContainer : guestContainer; + + // If just the host session has started or nobody, just run the host. + const guestStarted = isSessionStarted(vsls.Role.Guest); + if (!guestStarted) { + // NOTE: These tests aren't going to work unless there's more than just 'notebook' and 'default' + const hostRenderPromise = hostContainer + .get<TestInteractiveWindowProvider>(IInteractiveWindowProvider) + .waitForMessage(undefined, InteractiveWindowMessages.ExecutionRendered); + + // Generate our results + await resultGenerator(false); + + // Wait for all of the renders to go through + await hostRenderPromise; + } else { + // Otherwise more complicated. We have to wait for renders on both + + // Get a render promise with the expected number of renders for both wrappers + const hostRenderPromise = hostContainer + .get<TestInteractiveWindowProvider>(IInteractiveWindowProvider) + .waitForMessage(undefined, InteractiveWindowMessages.ExecutionRendered); + const guestRenderPromise = guestContainer + .get<TestInteractiveWindowProvider>(IInteractiveWindowProvider) + .waitForMessage(undefined, InteractiveWindowMessages.ExecutionRendered); + + // Generate our results + await resultGenerator(true); + + // Wait for all of the renders to go through. Guest may have been shutdown by now. + await Promise.all([ + hostRenderPromise, + isSessionStarted(vsls.Role.Guest) ? guestRenderPromise : Promise.resolve() + ]); + } + return container.getInteractiveWebPanel(undefined).wrapper; + } + + async function addCodeToRole( + role: vsls.Role, + code: string + ): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { + return waitForResults(role, async (both: boolean) => { + if (!both) { + const history = await getOrCreateInteractiveWindow(role); + await history.window.addCode(code, Uri.file('foo.py'), 2); + } else { + // Add code to the apropriate container + const host = await getOrCreateInteractiveWindow(vsls.Role.Host); + + // Make sure guest is still creatable + if (isSessionStarted(vsls.Role.Guest)) { + const guest = await getOrCreateInteractiveWindow(vsls.Role.Guest); + role === vsls.Role.Host + ? await host.window.addCode(code, Uri.file('foo.py'), 2) + : await guest.window.addCode(code, Uri.file('foo.py'), 2); + } else { + await host.window.addCode(code, Uri.file('foo.py'), 2); + } + } + }); + } + + function startSession(role: vsls.Role): Promise<void> { + const container = role === vsls.Role.Host ? hostContainer : guestContainer; + const api = container!.get<ILiveShareApi>(ILiveShareApi) as ILiveShareTestingApi; + return api.startSession(); + } + + function stopSession(role: vsls.Role): Promise<void> { + const container = role === vsls.Role.Host ? hostContainer : guestContainer; + const api = container!.get<ILiveShareApi>(ILiveShareApi) as ILiveShareTestingApi; + return api.stopSession(); + } + + function disableGuestChecker(role: vsls.Role) { + const container = role === vsls.Role.Host ? hostContainer : guestContainer; + const api = container!.get<ILiveShareApi>(ILiveShareApi) as ILiveShareTestingApi; + api.disableGuestChecker(); + } + + test('Host alone', async () => { + // Should only need mock data in host + addMockData(hostContainer!, 'a=1\na', 1); + + // Start the host session first + await startSession(vsls.Role.Host); + + // Just run some code in the host + const wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + }); + + test('Host & Guest Simple', async function () { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + // Should only need mock data in host + addMockData(hostContainer!, 'a=1\na', 1); + + // Create the host history and then the guest history + await getOrCreateInteractiveWindow(vsls.Role.Host); + await startSession(vsls.Role.Host); + await getOrCreateInteractiveWindow(vsls.Role.Guest); + await startSession(vsls.Role.Guest); + + // Send code through the host + const wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + + // Verify it ended up on the guest too + assert.ok(guestContainer.getInteractiveWebPanel(undefined), 'Guest wrapper not created'); + verifyHtmlOnCell( + guestContainer.getInteractiveWebPanel(undefined).wrapper, + 'InteractiveCell', + '<span>1</span>', + CellPosition.Last + ); + }); + + test('Host starts LiveShare after starting Jupyter', async function () { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + addMockData(hostContainer!, 'a=1\na', 1); + addMockData(hostContainer!, 'b=2\nb', 2); + await getOrCreateInteractiveWindow(vsls.Role.Host); + let wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + + await startSession(vsls.Role.Host); + await getOrCreateInteractiveWindow(vsls.Role.Guest); + await startSession(vsls.Role.Guest); + + wrapper = await addCodeToRole(vsls.Role.Host, 'b=2\nb'); + + assert.ok(guestContainer.getInteractiveWebPanel(undefined), 'Guest wrapper not created'); + verifyHtmlOnCell( + guestContainer.getInteractiveWebPanel(undefined).wrapper, + 'InteractiveCell', + '<span>2</span>', + CellPosition.Last + ); + }); + + test('Host Shutdown and Run', async () => { + // Should only need mock data in host + addMockData(hostContainer!, 'a=1\na', 1); + + // Create the host history and then the guest history + await getOrCreateInteractiveWindow(vsls.Role.Host); + await startSession(vsls.Role.Host); + + // Send code through the host + let wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + + // Stop the session + await stopSession(vsls.Role.Host); + + // Send code again. It should still work. + wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + }); + + test('Host startup and guest restart', async function () { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + // Should only need mock data in host + addMockData(hostContainer!, 'a=1\na', 1); + + // Start the host, and add some data + const host = await getOrCreateInteractiveWindow(vsls.Role.Host); + await startSession(vsls.Role.Host); + + // Send code through the host + let wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + + // Shutdown the host + host.window.dispose(); + + // Startup a guest and run some code. + await startSession(vsls.Role.Guest); + wrapper = await addCodeToRole(vsls.Role.Guest, 'a=1\na'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + + assert.ok(hostContainer.getInteractiveWebPanel(undefined), 'Host wrapper not created'); + verifyHtmlOnCell( + hostContainer.getInteractiveWebPanel(undefined).wrapper, + 'InteractiveCell', + '<span>1</span>', + CellPosition.Last + ); + }); + + test('Going through codewatcher', async () => { + // Currently keeping the liveshare tests via jupyter connect + hostContainer.setExperimentState(LocalZMQKernel.experiment, false); + guestContainer.setExperimentState(LocalZMQKernel.experiment, false); + + // Should only need mock data in host + addMockData(hostContainer!, '#%%\na=1\na', 1); + + // Start both the host and the guest + await startSession(vsls.Role.Host); + await startSession(vsls.Role.Guest); + + // Setup a document and text + const fileName = 'test.py'; + const version = 1; + const inputText = '#%%\na=1\na'; + const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); + document.setup((doc) => doc.getText(TypeMoq.It.isAny())).returns(() => inputText); + + const codeWatcher = guestContainer!.get<ICodeWatcher>(ICodeWatcher); + codeWatcher.setDocument(document.object); + + // Send code using a codewatcher instead (we're sending it through the guest) + const wrapper = await waitForResults(vsls.Role.Guest, async (both: boolean) => { + // Should always be both + assert.ok(both, 'Expected both guest and host to be used'); + await codeWatcher.runAllCells(); + }); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + assert.ok(hostContainer.getInteractiveWebPanel(undefined), 'Host wrapper not created for some reason'); + verifyHtmlOnCell( + hostContainer.getInteractiveWebPanel(undefined).wrapper, + 'InteractiveCell', + '<span>1</span>', + CellPosition.Last + ); + }); + + test('Export from guest', async () => { + // Currently keeping the liveshare tests via jupyter connect + hostContainer.setExperimentState(LocalZMQKernel.experiment, false); + guestContainer.setExperimentState(LocalZMQKernel.experiment, false); + + const originalFileSystem = guestContainer.get<IDataScienceFileSystem>(IDataScienceFileSystem) as MockFileSystem; + + // Should only need mock data in host + addMockData(hostContainer!, '#%%\na=1\na', 1); + + // Remap the fileSystem so we control the write for the notebook. Have to do this + // before the listener is created so that it uses this file system. + let outputContents: string | undefined; + const fileSystem = TypeMoq.Mock.ofType<IDataScienceFileSystem>(); + guestContainer!.serviceManager.rebindInstance<IDataScienceFileSystem>( + IDataScienceFileSystem, + fileSystem.object + ); + fileSystem + .setup((f) => f.writeFile(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((_f, c) => { + outputContents = c.toString(); + + // Tell the mock file system that a certain file exists + originalFileSystem.addFileContents(Uri.file('test.ipynb').fsPath, outputContents!); + + return Promise.resolve(); + }); + fileSystem.setup((f) => f.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => true); + // fileSystem.setup((f) => f.getSubDirectories(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + fileSystem.setup((f) => f.localDirectoryExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + + // Need to register commands as our extension isn't actually loading. + const listeners = guestContainer!.getAll<IDataScienceCommandListener>(IDataScienceCommandListener); + const guestCommandManager = guestContainer!.get<ICommandManager>(ICommandManager); + listeners.forEach((f) => f.register(guestCommandManager)); + + // Start both the host and the guest + await startSession(vsls.Role.Host); + await startSession(vsls.Role.Guest); + + // Create a document on the guest + const file = Uri.file('foo.py'); + guestContainer!.addDocument('#%%\na=1\na', file.fsPath); + guestContainer!.get<IDocumentManager>(IDocumentManager).showTextDocument(file); + + // Attempt to export a file from the guest by running an ExportFileAndOutputAsNotebook + const executePromise = guestCommandManager.executeCommand( + Commands.ExportFileAndOutputAsNotebook, + file + ) as Promise<Uri>; + assert.ok(executePromise, 'Export file did not return a promise'); + const savedUri = await executePromise; + assert.ok(savedUri, 'Uri not returned from export'); + assert.equal(savedUri.fsPath, Uri.file('test.ipynb').fsPath, 'Export did not work'); + assert.ok(outputContents, 'Output not exported'); + assert.ok(outputContents!.includes('data'), 'Output is empty'); + }); + + test('Guest does not have extension', async () => { + // Should only need mock data in host + addMockData(hostContainer!, '#%%\na=1\na', 1); + + // Start just the host and verify it works + await startSession(vsls.Role.Host); + let wrapper = await addCodeToRole(vsls.Role.Host, '#%%\na=1\na'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + + // Disable guest checking on the guest (same as if the guest doesn't have the python extension) + await startSession(vsls.Role.Guest); + disableGuestChecker(vsls.Role.Guest); + + // Host should now be in a state that if any code runs, the session should end. However + // the code should still run + wrapper = await addCodeToRole(vsls.Role.Host, '#%%\na=1\na'); + verifyHtmlOnCell(wrapper, 'InteractiveCell', '<span>1</span>', CellPosition.Last); + assert.equal(isSessionStarted(vsls.Role.Host), false, 'Host should have exited session'); + assert.equal(isSessionStarted(vsls.Role.Guest), false, 'Guest should have exited session'); + assert.ok(lastErrorMessage, 'Error was not set during session shutdown'); + }); +}); diff --git a/src/test/datascience/mainState.unit.test.ts b/src/test/datascience/mainState.unit.test.ts new file mode 100644 index 000000000000..d6dd7ff3738f --- /dev/null +++ b/src/test/datascience/mainState.unit.test.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert } from 'chai'; +import { IDataScienceSettings } from '../../client/common/types'; +import { + createEmptyCell, + CursorPos, + DebugState, + extractInputText, + ICellViewModel +} from '../../datascience-ui/interactive-common/mainState'; +import { defaultDataScienceSettings } from './helpers'; + +// tslint:disable: max-func-body-length +suite('DataScience MainState', () => { + function cloneVM(cvm: ICellViewModel, newCode: string, debugging?: boolean): ICellViewModel { + const result = { + ...cvm, + cell: { + ...cvm.cell, + data: { + ...cvm.cell.data, + source: newCode + } + }, + inputBlockText: newCode, + runDuringDebug: debugging + }; + + // Typecast so that the build works. ICell.MetaData doesn't like reassigning + // tslint:disable-next-line: no-any + return (result as any) as ICellViewModel; + } + + test('ExtractInputText', () => { + const settings: IDataScienceSettings = defaultDataScienceSettings(); + settings.stopOnFirstLineWhileDebugging = true; + const cvm: ICellViewModel = { + cell: createEmptyCell('1', null), + inputBlockCollapseNeeded: false, + inputBlockText: '', + inputBlockOpen: false, + inputBlockShow: false, + editable: false, + focused: false, + selected: false, + scrollCount: 0, + cursorPos: CursorPos.Current, + hasBeenRun: false, + runningByLine: DebugState.Design, + gathering: false + }; + assert.equal(extractInputText(cloneVM(cvm, '# %%\na=1'), settings), 'a=1', 'Cell marker not removed'); + assert.equal( + extractInputText(cloneVM(cvm, '# %%\nbreakpoint()\na=1'), settings), + 'breakpoint()\na=1', + 'Cell marker not removed' + ); + assert.equal( + extractInputText(cloneVM(cvm, '# %%\nbreakpoint()\na=1', true), settings), + 'a=1', + 'Cell marker not removed' + ); + }); +}); diff --git a/src/test/datascience/manualTestFiles/manualTestFile.py b/src/test/datascience/manualTestFiles/manualTestFile.py new file mode 100644 index 000000000000..b6009d6f04d0 --- /dev/null +++ b/src/test/datascience/manualTestFiles/manualTestFile.py @@ -0,0 +1,75 @@ +# To run this file either conda or pip install the following: jupyter, numpy, matplotlib, pandas, tqdm, bokeh, vega_datasets, altair, vega, plotly + +# %% Basic Imports +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +# %% Matplotlib Plot +x = np.linspace(0, 20, 100) +plt.plot(x, np.sin(x)) +plt.show() + +# %% Bokeh Plot +from bokeh.io import output_notebook, show +from bokeh.plotting import figure +output_notebook() +p = figure(plot_width=400, plot_height=400) +p.circle([1,2,3,4,5], [6,7,2,4,5], size=15, line_color="navy", fill_color="orange", fill_alpha=0.5) +show(p) + +# %% Progress bar +from tqdm import trange +import time +for i in trange(100): + time.sleep(0.01) + +# %% [markdown] +# # Heading +# ## Sub-heading +# *bold*,_italic_,`monospace` +# Horizontal rule +# --- +# Bullet List +# * Apples +# * Pears +# Numbered List +# 1. ??? +# 2. Profit +# +# [Link](http://www.microsoft.com) + +# %% Magics +%whos + +# %% Some extra variable types for the variable explorer +myNparray = np.array([['Bob', 1, 2, 3], ['Alice', 4, 5, 6], ['Gina', 7, 8, 9]]) +myDataFrame = pd.DataFrame(myNparray, columns=['name', 'b', 'c', 'd']) +mySeries = myDataFrame['name'] +myList = [x ** 2 for x in range(0, 100000)] +myString = 'testing testing testing' + +# %% Latex +%%latex +\begin{align} +\nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\ +\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\ +\nabla \cdot \vec{\mathbf{B}} & = 0 +\end{align} + +# %% Altair (vega) +import altair as alt +from vega_datasets import data + +iris = data.iris() + +alt.Chart(iris).mark_point().encode( + x='petalLength', + y='petalWidth', + color='species' +) + +# %% Plotly +import plotly.graph_objects as go +fig = go.Figure(data=go.Bar(y=[2, 3, 1, 5])) +fig.show() \ No newline at end of file diff --git a/src/test/datascience/manualTestFiles/manualTestFileNoCells.py b/src/test/datascience/manualTestFiles/manualTestFileNoCells.py new file mode 100644 index 000000000000..87de1fd22327 --- /dev/null +++ b/src/test/datascience/manualTestFiles/manualTestFileNoCells.py @@ -0,0 +1,22 @@ +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +# Matplotlib Plot +x = np.linspace(0, 20, 100) +plt.plot(x, np.sin(x)) +plt.show() + +# Bokeh Plot +from bokeh.io import output_notebook, show +from bokeh.plotting import figure +output_notebook() +p = figure(plot_width=400, plot_height=400) +p.circle([1,2,3,4,5], [6,7,2,4,5], size=15, line_color="navy", fill_color="orange", fill_alpha=0.5) +show(p) + +# Progress bar +from tqdm import trange +import time +for i in trange(100): + time.sleep(0.01) \ No newline at end of file diff --git a/src/test/datascience/markdownManipulation.unit.test.ts b/src/test/datascience/markdownManipulation.unit.test.ts new file mode 100644 index 000000000000..385165d55610 --- /dev/null +++ b/src/test/datascience/markdownManipulation.unit.test.ts @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { expect } from 'chai'; +import { fixMarkdown } from '../../datascience-ui/interactive-common/markdownManipulation'; + +// tslint:disable: max-func-body-length +suite('DataScience - Markdown Manipulation', () => { + const markdown1 = `\\begin{align} +\\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\ +\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\ +\\nabla \\cdot \\vec{\\mathbf{B}} & = 0 +\\end{align} +sample text`; + + const output1 = ` +$$ +\\begin{align} +\\nabla \\cdot \\vec{\\mathbf{E}} & = 4 \\pi \\rho \\\\ +\\nabla \\times \\vec{\\mathbf{E}}\\, +\\, \\frac1c\\, \\frac{\\partial\\vec{\\mathbf{B}}}{\\partial t} & = \\vec{\\mathbf{0}} \\\\ +\\nabla \\cdot \\vec{\\mathbf{B}} & = 0 +\\end{align} +$$ + +sample text`; + + const markdown2 = `$\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*}$ +sample text +$\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*}$ +sample text`; + + const markdown3 = `\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +sample text +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +sample text +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +sample text + +sample text +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*}`; + + const output3 = ` +$$ +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +$$ + +sample text + +$$ +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +$$ + +sample text + +$$ +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +$$ + +sample text + +sample text + +$$ +\\begin{align*} +(a+b)^2 = a^2+2ab+b^2 +\\end{align*} +$$ +`; + + const markdown4 = ` +$$ +\\begin{equation*} +\\mathbf{V}_1 \\times \\mathbf{V}_2 = \\begin{vmatrix} +\\mathbf{i} & \\mathbf{j} & \\mathbf{k} \\\\ +\\frac{\\partial X}{\\partial u} & \\frac{\\partial Y}{\\partial u} & 0 \\\\ +\\frac{\\partial X}{\\partial v} & \\frac{\\partial Y}{\\partial v} & 0 +\\end{vmatrix} +\\end{equation*} +$$ +`; + + const markdown5 = ` +\\begin{equation*} +P(E) = {n \\choose k} p^k (1-p)^{ n-k} +\\end{equation*} + +This expression $\\sqrt{3x-1}+(1+x)^2$ is an example of a TeX inline equation in a [Markdown-formatted](https://daringfireball.net/projects/markdown/) sentence. +`; + const output5 = ` + +$$ +\\begin{equation*} +P(E) = {n \\choose k} p^k (1-p)^{ n-k} +\\end{equation*} +$$ + + +This expression $\\sqrt{3x-1}+(1+x)^2$ is an example of a TeX inline equation in a [Markdown-formatted](https://daringfireball.net/projects/markdown/) sentence. +`; + + const markdown6 = `$$ +\\begin{aligned} +\\frac{\\partial}{\\partial\\omega_j}C(\\omega) &= \\frac1m\\sum_{i=1}^m\\varphi_j\\left(x^i\\right)\\left(\\varphi^T\\left(x^i\\right)\\omega-t^i\\right) += 0 +\\end{aligned} +$$ +$$ +\\begin{pmatrix} +\\varphi_j\\left(x^1\\right) & \\dots & \\varphi_j\\left(x^m\\right) +\\end{pmatrix} +\\begin{pmatrix} +\\varphi_1\\left(x^1\\right) & \\dots & \\varphi_n\\left(x^1\\right)\\\\ +\\vdots & \\ddots & \\vdots\\\\ +\\varphi_1\\left(x^m\\right) & \\dots & \\varphi_n\\left(x^m\\right) +\\end{pmatrix} +\\begin{pmatrix} +\\omega_1\\\\ +\\vdots\\\\ +\\omega_n +\\end{pmatrix} += +\\begin{pmatrix} +\\varphi_j\\left(x^1\\right) & \\dots & \\varphi_j\\left(x^m\\right) +\\end{pmatrix} +\\begin{pmatrix} +t^1\\\\ +\\vdots\\\\ +t^m +\\end{pmatrix} +$$ + +Assuming that $T = (t^1, t^2, ..., t^m)^T$,$X = \\left(\\varphi(x^1), \\varphi(x^2), ..., \\varphi(x^m)\\right)^T$, then +$$ +X^TX\\omega = X^TT +$$`; + + const output6 = `$$ +\\begin{aligned} +\\frac{\\partial}{\\partial\\omega_j}C(\\omega) &= \\frac1m\\sum_{i=1}^m\\varphi_j\\left(x^i\\right)\\left(\\varphi^T\\left(x^i\\right)\\omega-t^i\\right) += 0 +\\end{aligned} +$$ +$$ +\\begin{pmatrix} +\\varphi_j\\left(x^1\\right) & \\dots & \\varphi_j\\left(x^m\\right) +\\end{pmatrix} +\\begin{pmatrix} +\\varphi_1\\left(x^1\\right) & \\dots & \\varphi_n\\left(x^1\\right)\\\\ +\\vdots & \\ddots & \\vdots\\\\ +\\varphi_1\\left(x^m\\right) & \\dots & \\varphi_n\\left(x^m\\right) +\\end{pmatrix} +\\begin{pmatrix} +\\omega_1\\\\ +\\vdots\\\\ +\\omega_n +\\end{pmatrix} += +\\begin{pmatrix} +\\varphi_j\\left(x^1\\right) & \\dots & \\varphi_j\\left(x^m\\right) +\\end{pmatrix} +\\begin{pmatrix} +t^1\\\\ +\\vdots\\\\ +t^m +\\end{pmatrix} +$$ + +Assuming that $$T = (t^1, t^2, ..., t^m)^T$$,$$X = \\left(\\varphi(x^1), \\varphi(x^2), ..., \\varphi(x^m)\\right)^T$$, then +$$ +X^TX\\omega = X^TT +$$`; + + const output6_nonSingle = `$$ +\\begin{aligned} +\\frac{\\partial}{\\partial\\omega_j}C(\\omega) &= \\frac1m\\sum_{i=1}^m\\varphi_j\\left(x^i\\right)\\left(\\varphi^T\\left(x^i\\right)\\omega-t^i\\right) += 0 +\\end{aligned} +$$ +$$ +\\begin{pmatrix} +\\varphi_j\\left(x^1\\right) & \\dots & \\varphi_j\\left(x^m\\right) +\\end{pmatrix} +\\begin{pmatrix} +\\varphi_1\\left(x^1\\right) & \\dots & \\varphi_n\\left(x^1\\right)\\\\ +\\vdots & \\ddots & \\vdots\\\\ +\\varphi_1\\left(x^m\\right) & \\dots & \\varphi_n\\left(x^m\\right) +\\end{pmatrix} +\\begin{pmatrix} +\\omega_1\\\\ +\\vdots\\\\ +\\omega_n +\\end{pmatrix} += +\\begin{pmatrix} +\\varphi_j\\left(x^1\\right) & \\dots & \\varphi_j\\left(x^m\\right) +\\end{pmatrix} +\\begin{pmatrix} +t^1\\\\ +\\vdots\\\\ +t^m +\\end{pmatrix} +$$ + +Assuming that $T = (t^1, t^2, ..., t^m)^T$,$X = \\left(\\varphi(x^1), \\varphi(x^2), ..., \\varphi(x^m)\\right)^T$, then +$$ +X^TX\\omega = X^TT +$$`; + + test("Latex - Equations don't have $$", () => { + const result = fixMarkdown(markdown1); + expect(result).to.be.equal(output1, 'Result is incorrect'); + }); + + test('Latex - Equations have $', () => { + const result = fixMarkdown(markdown2); + expect(result).to.be.equal(markdown2, 'Result is incorrect'); + }); + + test("Latex - Multiple equations don't have $$", () => { + const result = fixMarkdown(markdown3); + expect(result).to.be.equal(output3, 'Result is incorrect'); + }); + + test('Latex - All on the same line', () => { + const line = '\\begin{matrix}1 & 0\\0 & 1\\end{matrix}'; + const after = '\n$$\n\\begin{matrix}1 & 0\\0 & 1\\end{matrix}\n$$\n'; + const result = fixMarkdown(line); + expect(result).to.be.equal(after, 'Result is incorrect'); + }); + + test('Latex - Invalid', () => { + const invalid = '\n\\begin{eq*}do stuff\\end{eq}'; + const result = fixMarkdown(invalid); + expect(result).to.be.equal(invalid, 'Result should not have changed'); + }); + + test('Latex - $$ already present', () => { + const result = fixMarkdown(markdown4); + expect(result).to.be.equal(markdown4, 'Result should not have changed'); + }); + + test('Latex - Multiple types', () => { + const result = fixMarkdown(markdown5); + expect(result).to.be.equal(output5, 'Result is incorrect'); + }); + + test('Latex - Multiple /begins inside $$', () => { + const result = fixMarkdown(markdown6, true); + expect(result).to.be.equal(output6, 'Result is incorrect'); + const result2 = fixMarkdown(markdown6, false); + expect(result2).to.be.equal(output6_nonSingle, 'Result is incorrect'); + }); + + test('Links - Change HTML links to Markdown links', () => { + // tag with single quotes + const result = fixMarkdown(`<a href='https://aka.ms/AA8dqti'>link</a>`); + expect(result).to.be.equal(`[link](https://aka.ms/AA8dqti)`, 'Result is incorrect'); + + // tag with double quotes + const result2 = fixMarkdown(`<a href="https://aka.ms/AA8dqti">link <a</a>`); + expect(result2).to.be.equal(`[link <a](https://aka.ms/AA8dqti)`, 'Result is incorrect'); + + // tag with space in href and two endings + const result3 = fixMarkdown(`<a href = "https://aka.ms/AA8dqti">link </a></a>`); + expect(result3).to.be.equal(`[link ](https://aka.ms/AA8dqti)</a>`, 'Result is incorrect'); + + // mal formed tag + const result4 = fixMarkdown(`<a href = "https://aka.ms/AA8dqti" link </a></a>`); + expect(result4).to.be.equal(`<a href = "https://aka.ms/AA8dqti" link </a></a>`, 'Result is incorrect'); + }); +}); diff --git a/src/test/datascience/matplotlib.txt b/src/test/datascience/matplotlib.txt new file mode 100644 index 000000000000..9b106e836b8e --- /dev/null +++ b/src/test/datascience/matplotlib.txt @@ -0,0 +1 @@ +<img alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYYAAAD8CAYAAABzTgP2AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4xLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvDW2N/gAAIABJREFUeJztvXl4ZNV16PtbVZpVmqeW1FKrNfRI0w2IBmzDxRgwnoAkHiCDSeKElxf75SV+N9f45l7Hz0m+2PHNJTf5nNgkHvCNHwY7tsEOMQYMHhi7oedBraHV3WpNpblKs6r2++Oco64WUmuo4Qy1f99Xn6rOUGdp1zl77TXstUUphUaj0Wg0Fj67BdBoNBqNs9CKQaPRaDSXoRWDRqPRaC5DKwaNRqPRXIZWDBqNRqO5DK0YNBqNRnMZWjFoNBqN5jK0YtBoNBrNZWjFoNFoNJrLyLBbgI1QXl6uGhoa7BZDo9FoXMUbb7wxpJSqWO04VyqGhoYGDh48aLcYGo1G4ypE5NxajtOuJI1Go9FchlYMGo1Go7kMrRg0Go1GcxlaMWg0Go3mMrRi0Gg0Gs1lJEQxiMjXRGRQRI6vsF9E5O9FpENEjorItTH7HhCRdvP1QCLk0Wg0Gs3GSZTF8A3grivsfw/QYr4eBP4JQERKgT8HbgD2A38uIiUJkkmj0Wg0GyAhikEp9XNg5AqH3AN8Uxm8ChSLSDXwbuBZpdSIUmoUeJYrKxhHMzY1x4+O9jIxM2+3KGlHJKr48fE+3jh3pdtQkyxO9k7w2OvnmY9E7RZFkwBSNcGtFrgQ87nH3LbS9rcgIg9iWBvU19cnR8oN8tzJAZ44eIEX2gaZjyh2VRfyzY/tpzyQbbdonmchEuWHR3v5h5920BWcxO8T/vv7dvLA2xoQEbvF8zyTswv83XNn+NpL3USiisdeP8/ffmgvLVUFdoumiYNUBZ+Xe0LVFba/daNSjyilWpVSrRUVq87oThlPHenl9755kEMXxnjgpga+8Gt76BoK85GvvELf+LTd4nmaSFRx3yOv8iePHyHL7+N/3bePd26v5LM/PMmn/u0oswsRu0X0NIfOj3Lnwz/nn39xlg+31vHwR/bSMzrN+/7hl3z1l2ftFk8TB6myGHqAupjPm4Fec/utS7a/mCKZ4iY0M89f/ugke2qL+P4fvo0Mv6Fnt5YH+N1vHOBDX36Fbz94I5tL8myW1Jt85+AFDp4b5bMf2MVHb2rA5xM+cHUNf/fcGf7+px2MT8/zld9qtVtMTxKJKv70u0dRSvGdP7iJ6xtKAXhHcwUP/dtR/uJHJ9lVXchNTWU2S6rZCKmyGJ4CPmpmJ90IjCul+oBngDtFpMQMOt9pbnMF/+u5doLhWf7i3qsWlQLA/q2l/H+/fwOjk3N88Zk2GyX0LuHZBf7HT85w3ZYSHniboRQAfD7hk3du549vb+GZEwMc6xm3WVJv8oNDF+kYDPPf379rUSkAVBRk86XfuJbKgmwefu4MSi3rANA4nESlqz4GvAJsF5EeEfmYiPyBiPyBecjTQBfQAfwz8IcASqkR4C+AA+brc+Y2x3O6f4Kvv9zNfdfXs6+u+C37r95czP376/nR0T56x7RLKdF85WedDIVn+W/v27lsLOF337GVgpwM/vHFDhuk8zZzC1Eefu4Me2qLuOuqTW/Zn5Pp5+PvbOb1syO81DFsg4SaeElUVtL9SqlqpVSmUmqzUuqrSqkvK6W+bO5XSqmPK6WalFJ7lFIHY879mlKq2Xx9PRHyJBulFJ/5wQkKczL4L+/evuJxv/OOrQB8/SXtb00kvWPTPPLzLu7eW8M19ctnNxfmZPLATQ38+EQ/HYOhFEvobR4/cJ6e0Wn+87u3rxjgv29/HdVFOfzPZ9u01eBC9MznDfDimSCvd4/wqbt2UJKfteJxtcW5vG9PNY+9fkGnsCaQ//FMGwr4L3etrJTBsBpyMvz844udqREsDZiei/D3P+1g/9ZSbmkpX/G47Aw/n7itmTfPj/HimWAKJdQkAq0YNsCThy5SlJvJr167edVjf//mRsKzCzz++oVVj9WsTjA0yw8OX+SjN25ZNahfmp/F/fvrefJwLxdGplIkobf55ivdBEOz/OkVrAWLD11Xx+aSXB5+VscaEkE0qohGU9OOWjGsk+m5CD85OcB792wiK2P15tuzuYgbG0v5+ktn9eSfBPDjE/1EFXywdXWlDPDgLY34BL78M201JILvvNHD/obSywLOK5GV4ePj72zmaM84b54fS4F03uaXHUPc+NfPc7p/IunX0ophnTx/eoCpuQgf2Fuz5nMevKWR3vEZnj7Wl0TJ0oOnj/bRWJHP9jVOoNpUlMPde2t58nAvcwtaMcdDx2CIjsEw77u6es3nvO/qajL9wk9O9CdRsvTg2ZMDhGYWaCjLT/q1tGJYJ08d7qWyIJsbtq49P/vWbZXUFufywyO9SZTM+wRDs7x2dpj376le16zm91y1ifDsAq+d1Rky8fDMiQEA7txdteZzCnMyuampnGdO9Gt3UhwopXju1AA3t5STk+lP+vW0YlgH49PzvNgW5P1X1+D3rb1j8vmE23dW8suOIWbm9WzcjWK5kd67jhErwNuby8nJ9PHcyYEkSZYe/Ph4P/vqiqkuyl3Xee/eXUX38BRnBsJJksz7HL84Qd/4DHfsWrtSjgetGNbBMyf6mYtEuXvf2t1IFu/cUcnMfJRXuvSodaM8fbSPpnW4kSxys/zc3FLBc6cG9ah1g/SMTnHs4jjvWWbewmrcsasKEeP50WyMZ0/24xN4106tGBzHD4/0sqUsj72bi9Z97o2NZeRm+nnh9GASJPM+lhvpfet0I1ncsbOKi2PTnOrTcxo2guVGevfu9SuGyoIcrqkr1oohDn5ycoDWLaWUXiE9PpFoxbBGhsKzvNw5zAeurtlQx5ST6eftzeU8r0etG2KjbiSLd+6oRASeO6XdSRvhmeP97NhUQEP5xgKf7969iRO9EzpteANcGJnidH9oXbGdeNGKYY08f2qASFTx/r0b65gAbttRycWxadoHta91vWzUjWRRUZDNNXXFWjFsgGBolgPnRpYtf7FWLEvjWR3nWTc/MdssVfEF0IphzbzaNUJ5IHvDHRMYigHgp9qdtC7Gp+Z57eww792gG8ni9l1VHO0Zp398JoHSeZ+fnOxHKeJSDA3lhlLX7qT18+zJfrZVBdiSgjRVC60Y1oBSilc6h7mxsTSujmlTUQ67qgv56SmtGNbDge4RosrILoqHO8zA3fOn9ah1PTx7coAtZXlxDYrAyE460D3CcHg2QZJ5n7GpOQ50j6bUWgCtGNbEueEp+idmuLEx/try79pZyRvnRxmf0rWT1srr3SNk+X3LVrFdD82VAbaU5em01XUQiSoOdo/yjubyuFfEu21nFVGFzsxbBy+2BYlEFXfs2ri1thG0YlgDr5o3ciIUwzt3VBKJKn7WrguLrZXXuobZV1cc98QeEeGd2yt5uXNYz4JeI6f6JgjPLrB/6+olMFZjd00huZl+DnaPJkCy9OC1s8MU5mRwde36MyHjQSuGNfBq1zDlgWyaKuL38e3dXExpfhY/a9OKYS2EZxc43jvBDY3xd0xgLKI0uxDlRK9ewGctvH7WWB5lLbWRViPT7+PaLcWL36lZnQPdo1y3pWRxIapUkaiFeu4SkTYR6RCRh5bZ/7CIHDZfZ0RkLGZfJGbfU4mQJ5EopXi1ayTu+IKF3ye0binhzfN61LQW3jg3SiSqEjJiBWjdUrL4vZrVOdA9Qm1xLjXF65vtvBKtW0o53T+hy9CvgdHJOToGw7QmQCmvl7gVg4j4gS8B7wF2AfeLyK7YY5RSf6KU2qeU2gf8A/C9mN3T1j6l1N3xypNoEhlfsLhuSwlnhyZ1EG4NvNY1TIZPuG7L8gvyrJfKwhzqSnO1O2MNKKU40D2SMKUMhsUWVXBIV1tdFWvw0pqge389JMJi2A90KKW6lFJzwLeBe65w/P3AYwm4bkqwAmWJXNT8WvOH1qWIV+f1syNcVVtEXlZGwr6zdUspB8+N6omGq3B2aJKh8FxC3EgW++qK8fuEA9qdtCoHzo2Q6Rf2xpl0sRESoRhqgdhVaHrMbW9BRLYAW4GfxmzOEZGDIvKqiNybAHkSyqtdw1QUZNO4wRmfy7GntohMv2h30ipMz0U40jOWsPiCxXVbShgKz3Jez8K9Ige6jc57/9bEjVjzszPYXVO4+N2alTnYPcqe2qKUVFNdSiIUw3KO95WGYvcB31VKxZYYrVdKtQK/DvydiDQtexGRB00FcjAYTE3g1ogvDHNjY1lC4gsWOZl+dtcUaT/3Khy6MMp8RHHjOkqcr4XWBqOj0+6kK/P62VFK87Noqggk9Huvbyjl8IUxZhd0peGVmJmPcKxn3Jb4AiRGMfQAdTGfNwMrLTxwH0vcSEqpXvNvF/AicM1yJyqlHlFKtSqlWisqKuKVeU10D08xMDHLjQkesYIxaj1yYUyv6nYFXusawSdwXUNifazbKgsoyMngoFbMV+RA9wjXN5QkdFAEcH1DCbMLUY5fTP5KZG7l2MVx5iJRW+ILkBjFcABoEZGtIpKF0fm/JbtIRLYDJcArMdtKRCTbfF8OvB04mQCZEsJrZnxhPYvyrJVr642H42SvfjhW4rWzw+yqKaQwJzOh3+vzCdfWl/DGOe3OWImBiRnOj0wlNL5gYY2CtTtpZSxrNlFJF+slbsWglFoAPgE8A5wCnlBKnRCRz4lIbJbR/cC31eURv53AQRE5ArwAfF4p5RjFcKRnnMKcjITMX1jKtVuMgJKOMyzP7EKEQ+fH2N+QeKUMRqbHmYEwY1NzSfl+t2PNNUhkRpJFecCI2R3UimFFDnaP0FiRT1kg25brJyTVQyn1NPD0km2fWfL5s8uc9zKwJxEyJINjF8e4enNxwk1pgOqiXGqLc3nj3Ci/8/atCf9+t3O6L8TsQnQxHpBorFHrm+dHuW1HauvQuIED3SPkZ/nZVV2YlO9vbSjhmRMDRKMq5ZO3nE40qjh4bpS7NrD2RaLQM59XYGY+Qlt/iD0bWJRnrVxTX8yb2s+9LMcuGjOT9ySpFMC+umIyfKID0CtwsHuUa+pLyPAnp4u4vqGU8el5XYJ+GTqDYcan5xMeW1sPWjGsQFt/iPmISlrHBIb/sHd8ht6x6aRdw60cvzhOUW4mm0sSM+N2KblZfnbXFOoA9DLMLkQ4MxDi6iQOiqy5PEcu6Lk8SzlgDlaSEd9ZK1oxrECyR6xwKbCk4wxv5djFcfbUFiXFjWdx3ZZSnRm2DG39IRaiiquSeO9vLcsnP8vPcV2z6i0c7RmjOC+ThrI822TQimEFjvWMU5KXvBErwM7qQnIyfXo+wxKsEWsyOyaAPZsLmV2I0hnU7oxYrDTSZA6KfD5hd00RJ3RW3ls40TvB7prCpA6KVkMrhhU4enGcPUkKPFtk+n3sqS3iWI8eNcWSCjcewFU1xvef0Pn0l3G818jGS+agCGBXTSEneyeIRHVpEov5SJS2/tDivWkXWjEsw8y86WNNQQ303TVFnOqbIKofjkVS4cYDaKwIkJPp06PWJZy4OM5VSXbjAVxVW8T0fISzQ9pis2gfCDMXibKrJjnZYGtFK4ZlONlnjGKSmZFksau6kMm5COd03Z5FrMBzXWlyR6x+n7BjU6FemyGG+UiUU/3Jd+MBXFVrdH56BvQlrHtxt7YYnIfl2klmVoaFNTLQM6AvkYrAs8XumkJO9k3oSqsmHYNh5hai7E7BiLW5IkB2ho/jF7VitjjRO0Felp+tCSzauRG0YliGoz3jlAey2VSYk/RrtVQFyPCJHrWazC4Y80dSMWIFY2QWmlngwohOGQYWO+lUtH+G38eO6kKdmRTDid5xdlYX4rd50p9WDMtw/OI4V29OzYg1O8NPc2WAk33aYgA40x9OSeDZwhoZa8VscPziOPlZfraWpWbEelVNISd6tcUGxoznk70TXGVzfAG0YngLU3MLtA+GUtYxwaXsDE3qAs8W2zcV4PeJDkCbHO+dYFdNYcrKVGiL7RLdw5NMzkVsjy+AVgxv4WTvBFGVuo4JjIdjMDRLMKSX+jyWosCzRU6mn+aKgLYYgIg5Yk1lx7QYgNbtvzg4sTsjCbRieAtHzcBzKjKSLKxCZdqdZLgyrqpN7eSe3aY7I905OxRmej6SsvgOwLaqAjJ8ogPQGMox0y9sqyqwWxStGJZyqm+C8kAWVSkIPFssKoY075zmFqIpDTxb7Kop1BYbqZnxvJScTD8tVQUcT/N7H4znf1tVAVkZ9nfL9kvgMNoGQmzflFqNXWSW3kh3d0b7YIi5SDTlsz4t10m6t//xi+NkZ/iSsv7IlbiqppATF8fTOgCtlOJE74TtM54ttGKIIRJVnBkIsb0q9T6+XdWFae9KausPAbCzOrWKeddiZlJ6t/+xi0aqZLJKba/EVbVFDE/OMTCRvhZb3/gMI5Nz7K61P74ACVIMInKXiLSJSIeIPLTM/t8WkaCIHDZfvxez7wERaTdfDyRCno1yfmSKmfkoO1JsMYDROZ0dmmRqbiHl13YKbf0hsvw+GlKUKmlhBbvT2ZWnlOJU34QtgU8rZfhYGscZrEFJKiYWroW4FYOI+IEvAe8BdgH3i8iuZQ59XCm1z3z9i3luKfDnwA3AfuDPRcS21Sna+o0fZ0eKR6xguDOUglN9oZRf2ym0DYRoqgykfMQKsLu6KK1dSQMTs0zMLNgyKNphxtis5y8dOdE7johRcdkJJOIJ3A90KKW6lFJzwLeBe9Z47ruBZ5VSI0qpUeBZ4K4EyLQhTveHEIGWSnssBkjvzKS2/pAtHRMYI7Xu4SlCM/O2XN9u2gaMAYkdGTGBbKOSa9tA+hbTO90XoqEsn7yshKy2HDeJUAy1wIWYzz3mtqX8mogcFZHvikjdOs9NCW39xo+Tm+VP+bVrinIoys1MW3fG+PQ8feMzKQ/8W1ij1nRdatIarW+3KVVye1UB7QPpay2fGQyxrSpgtxiLJEIxLJdwvjS94IdAg1LqauA54NF1nGscKPKgiBwUkYPBYHDDwl6Jtv6QbQ+GiKR1APqM2SnY1f7WQ5munVNbf5jKgmxK8rNsuf62TQV0BsNpuZrezHyE7qFJ2+795UiEYugB6mI+bwZ6Yw9QSg0rpayUg38GrlvruTHf8YhSqlUp1VpRUZEAsS9nZj5C9/CkbSNWMMozdAyE0jJt77SZkWRX+9eV5JGT6aOtPz0thjM2pGnHsr2qgPmIonto0jYZ7KIzGCaqDOXoFBKhGA4ALSKyVUSygPuAp2IPEJHqmI93A6fM988Ad4pIiRl0vtPclnLaB4wfxy4fNxiVVifnIlwcS7+6MWf6QxTkZFBdlLqJhbH4fEJLZQHtg+lnMUSiivbBkK0zbltMi60tDS22djO24imLQSm1AHwCo0M/BTyhlDohIp8TkbvNw/5IRE6IyBHgj4DfNs8dAf4CQ7kcAD5nbks5pywfq42KwXow29MwCGe58exc53ZbVcHiXIp04oKZpm1nx9RUEcAnxgAh3WgbCJHpFxpsXoMhloSEwJVSTwNPL9n2mZj3nwY+vcK5XwO+lgg54qGtP0ROpo8tKc6hj2WbmQ11ZiDEO3dU2iZHqlFK0TYQ4n1XV69+cBLZVhXg397sYWxqjuI8e3ztdrCYkWTjoCgn009DeX5aWgxn+kM0lgfItCFNeyWcI4nNtPWHaKkssHWBjKK8TCoLsjmTZhbDwMQs49Pztrrx4JLFlm7tb43SWyrtzYrZXlWQdm0PZkaSg+ILoBXDIqdtzKGPZVtV+vm5T5tuPLurSloP55k0G7W2DYSoK80lP9veHPptVQV0D08yMx+xVY5UMjlrrEWxzWalvBStGIDh8CxD4Vlb4wsWLVUBIxAeTZ/MJLtTVS1qinIIZGekn2KwMU07lu2bClDKWHc6XbD+V20xOBAr4Lhjk/3T0bdVFTA9n16ZSaf7Q7bm0FuICC1VgbRSDLMLEc4O2ZumbWFZjOmUANDmkEHRUrRiwP4c+lisiVbp1DnZnUMfy7bKgrTKCjs7NMlCVNnuxgNoKMsjy+9Lr3u/P0R2ho+60jy7RbkMrRgwOqbS/CwqCrLtFoXmyvQKgEaiivaBsCPiO2CY9MOTcwyF06MEdJuDBkUZfh9NlYG0ykw6MximpSpga9LLcmjFgOHna3ZI8KcoN5NNhTlpM2rqHp5kdiHqiBErpJ/FdmYgRIZPaCx3xv2/vSqQVnMZzvTbO7FwJdJeMSilaHeQYgDSys/dbmNVz+WwfL3p0jm19YfZWp7viOUkwbDYesdnmEiDKrfj0/P0T8w45t6PxRl3g40MhecYn563PYc7lm1VBXQMhomkQWaSlZXhFMVcUZBNUW4mZ9IkM+bMgLNy6Lcvzv73vmJud2jgGbRicFzHBIY7Y3YhyoWRKbtFSTodg2FqinJsz6G3EBFjolUaWAzTcxEujE4tzrh3Apcyk7yvmJ0w43wltGIIOk8xtFSlz0SrjmCYJge1PVxy5Xm9ym1nMIxSzrr3a4tzyc30p8VchjP9IQLZGdTYVDjySmjFMGD8OJsKnfPjWG4try8aE40qOgcnHdUxgZGhMzGz4PnF6TsdOCjy+YSmyvzFAZuXsQZFdhaOXAmtGBz44xTkZFJbnOt5i6F3fJrp+YijOiaA5gpDHq+PWjsHw/gEGsqdlUPfVBGg0+NtD2Y2ZIWz7n2LtFcM7QPO/HEMd4a3H47F+I7D2t9SVJ0eH7V2BMPUl+aRnZH6pWyvRHNFgItj00zOLtgtStKYmJlnYGLWcYMii7RWDBMz8wyGnPnjNFcE6Ap6u2aSEwP/YGQmFWRneN5icNL8nVgsmc56eDW3rqDxvzVVOGcNhljSWjFYD76TUlUtmiqNzCQv10zqDIYpycukLGD/jPNYRISmyoCnLYaFSJSzQ5OOC/zDJcXgZcXs1EGRRUIUg4jcJSJtItIhIg8ts/+TInJSRI6KyPMisiVmX0REDpuvp5aem0w6Bpz74yw+HB7unJw6YgXDz+3ljun8yBTzEeU4Nx7AlrJ8/D7xdPt3DIbJ9Av1DquRZBG3YhARP/Al4D3ALuB+Edm15LBDQKtS6mrgu8DfxOybVkrtM193k0I6gmGyHFjACoyOCfB0EM7JiqG5MsBgaNazM3A7TVeGE9s/K8PHltI8T1tsncEwDWX5ZDho1bZYEiHVfqBDKdWllJoDvg3cE3uAUuoFpZQ1W+tVYHMCrhs3HYNhGsvzHVfACqA0P4vS/CzPPhzD4VlGp+YXiwY6jcUAtEcVszUad6IrCQy5vGwxdDp4UASJUQy1wIWYzz3mtpX4GPAfMZ9zROSgiLwqIveudJKIPGgedzAYDMYnsUn7YMjRP05TRT6dg94MwDndx2oFBb3aOXUMhqksyKYwJ9NuUZaluTJA9/AkC5Go3aIknLmFKOdGpha9Ak4kEYphueH2sqk0IvKbQCvwxZjN9UqpVuDXgb8TkablzlVKPaKUalVKtVZUVMQrMzPzEXpGpx3bMYGZz+1Ri8GJM85jqS/NI9Mviy4Xr9ERdPaItakiwHxEcc6DZWHODU8SiSpHt38iFEMPUBfzeTPQu/QgEbkd+DPgbqXU4pRSpVSv+bcLeBG4JgEyrYoTywEspbkywPDkHKOTc3aLknA6BsPkZfkdWQ4AjLUBGsryPWkxKKUc78rwsivP6dYyJEYxHABaRGSriGQB9wGXZReJyDXAVzCUwmDM9hIRyTbflwNvB04mQKZVuZSq6kwfN8QEoD1oNXQMhmmqcNaM86U0VxpzSbzGYGiW8OyCozumRVeeB9vf6nsaHTqHARKgGJRSC8AngGeAU8ATSqkTIvI5EbGyjL4IBIDvLElL3QkcFJEjwAvA55VSKVMMTiwHEIvXFYOTOyYw2v/cyBRzC97yczt1xnksBTnGglVetNg6g2Fqi3PJy3JGReHlSIhkSqmngaeXbPtMzPvbVzjvZWBPImRYLx2DYbaU5TuuHEAstSW5ZGf4PPdwhGcX6BufcbxiaK4MEIkquocnHbmYykZxgysDoKky35uupGDY0dYCpPHM567gpGOno1v4fcLW8nzPBUCth93JWRng3bkkHYNhCrIzHLHG+ZVorgjQGZz0VPlzp1YUXkpaKoZIVBnlABzeMQGeLM3gxHLPy9Ho0ZTVjkHnVRRejubKAOFZb5U/75uYYXo+4vi+Jy0VQ8/oFHORqOPNOTBGTRdGppiZj9gtSsLoDIbJ8Albypwb3wHINxdR8ZpidnqqqoU1+c5Litktbry0VAyXKhs6+8cB4+GIKuge9o47qSs4ac4TcP7t11QZ8FRmzMTMPMHQrCvu/WYPJl90asXgXKwbzQ0PhxUH8dIM6M5gmEYXtD0YD3Dn4KRnyp93ObhG0lIqCrIpyPFW+fOOYJii3EzK8rPsFuWKpK1iKMnLpMThPw5AY3kAEe+Y05GoontoiqZK57vxwBg8TM9H6JuYsVuUhNDpghx6CxGhsSJA15A37n0w2r+pIt/x8Z00VQzuCDwD5Gb5qS3O9Yw5bcV3msrd0f5ey0zqGjLiO04t97yUpor8RSvHC7il70lLxdAVDLvix7Fo9lBm0mJ8xy0WgymnV2ZAdw5OUl/mjvgOGIq5b3zGE8t8jk/PMxSedWxF21jccXckkPGpeYbCc64wpS2sYnpe8HNbCq7RJRZDRcBY5tMrc0m6htw1KLJibF5Y5rNr8d53ft+Tdoqhc8g9gWeLxop8ZuajnvBzdwbDlOZnuSK+A6afu9Ibfm4rvuOmQVGjhzKTLlnLzu970k8xOHyBkuWwlJgX3BmdwUlXjJhiaSr3xroYi/EdFw2KtpTl4RM8YbFZ83fcEN9JO8XQNTRJpl+oK8m1W5Q107iYsup+xeC2+A4Yg4j+iRnCLvdzX0rTdo9izs7wU+eRZT67gu6J7zhfwgTTaRbPc+paq8th+bm7XO5nteI7bgk8W1gWzlmXj1otV4Zb4jsWTRUBT2QmdQbDrml79/SOCaIzGHbViAli/Nwufzis+I5bHg4Ly+3o9jiD2+I7Fo3l+ZwdcnfyxUIkyrnhKdf0PWmlGOYjUc6K/HxsAAAgAElEQVSPTLlm1m0sTeX5rjen3RR8i2XRz+1yV16nCyoKL0dTZYCZ+Si949N2i7JhekanXRXfSSvFcGFkivmIcs2PE0tTpfvzuTuDYdfFdyDGz+1yV16Xi1wZsViuPDcHoC1r0y0ZYQlRDCJyl4i0iUiHiDy0zP5sEXnc3P+aiDTE7Pu0ub1NRN6dCHlW4lLxPHf8OLEs+rld3Dl1Bd0X37FoLHf3ojFuje9AjCvPxRazldXmlkFp3E+oiPiBLwHvAXYB94vIriWHfQwYVUo1Aw8DXzDP3YWxRvRu4C7gH83vSwqLk6tc8uPEYj0cbnYnuTFV1aKxIsDZIfcW03NrfAegLD+LwpwMV9/7XUPuqc8GibEY9gMdSqkupdQc8G3gniXH3AM8ar7/LvAuMapI3QN8Wyk1q5Q6C3SY35cUOoNhygPZFOVmJusSScPt+dxG8G3SdfEFi6aKALMLUS6OudPP7db4DhjJF00uT75wS40ki0QohlrgQsznHnPbsscopRaAcaBsjecCICIPishBETkYDAY3JOjkXIRtVe75cWLJzvCzuSTPteb0hdFp18Z34JJv2K0pw26N71g0lru7XliXC9Z5jiURimG5+rFL7e2VjlnLucZGpR5RSrUqpVorKirWKaLBl379Wv71Yzds6Fwn0FTh3vWf3VTueTncPvvczfEdMIoZDkzMunKS4WJ8x0WDokTcJT1AXcznzUDvSseISAZQBIys8dyE4vM5uw76lTD83O7M57ayMtxSbnsp5YEsClzs53ZzfAcuxUbcqJgX4ztpphgOAC0islVEsjCCyU8tOeYp4AHz/QeBnyqllLn9PjNraSvQAryeAJk8SVNFwLXF9DoHJ434Tp774jtg+rldOgPX7fEdgObF8ufua383ZkPGrRjMmMEngGeAU8ATSqkTIvI5EbnbPOyrQJmIdACfBB4yzz0BPAGcBH4MfFwp5Z1V7xOMm2smdbrMx7ocjRXunGRoxXfcbDHUl+bj94kr298qnlfnguJ5FhmJ+BKl1NPA00u2fSbm/QzwoRXO/SvgrxIhh9eJ9XPfsm1jcRa76AyGueuqarvFiIumigDfe/Mi4dkFAtkJeXRSghsrCi8lK8NHfWmeSy2GMFtcUjzPwj2SamL83O56OEYm5xidmneVKb0clvxu83MvVlV1aXzHosmlFltncNJV8QXQisFVLPq5XVbMzepI3TxihZj1n13WOXUF3R3fsTDu/UkiLkq+mLfiO1oxaJJJY4X7Fo3xyoi1viwPv09c585wY0Xh5WiqCDC3EOXiqHsmGV6qz+au9teKwWU0Vbhv0Ziu4CRZGT5qXTq5yiI7w0+9CxeN6QyGXW+tAYt1ntzU/p0unXGuFYPLsExSNy0aYyxQYmSVuJ0ml1lsl+I77uqYlsOay+AuxeBOa1krBpdh5XN3BEM2S7J2jOCbu0zplWgyi+m5xc99qXCk+9u/JD+L0vwsdymGwbAr4ztaMbiMxXxul4xa5xaMxZG8MGIF088didIzOmW3KGvCSlVt9kz7u8ti6xpy5+JIWjG4jKwMH1tc5Oc+P2KMrj2jGFzm5+4MhsnO8FFT7O74jkVThXuK6Sml6Bh0Z3xHKwYX0lTpnoejw2ULlKzGop/bJaPWruAkWz0S3wHjPhqenGNsas5uUVZlZHKO8Wl3xne0YnAhTRUBuoemWIhE7RZlVSwFttWF5vRylORnUeYiP7dXMpIsLllszlfMnS6skWShFYMLaarIZy4S5YIL8rk7g2E2Fea4qoTEarjFnTG7EPFUfAfcNclwMSPJhe2vFYMLWVzm0wXF9LqCk65cZ/hKNFXm0+GCtj83PEVUuXPEuhKbS/LI8vvcoRgGw+Rk+qh1YXxHKwYX0uSSfG6llDnr1n0jpivRVBFgdGqekUln+7kXi+d5qP39PmFruTsyk4z5OwFXrgGjFYMLKcrLpDyQ7XjFEAzPEppZcHW55+VwiztjMb7jsfZvrMh3RSHDzqB718DQisGlNFc6f5lPa1Tn1odjJRYVg8PdSV3BSWqKcsj3UHwHjPY/NzLF3IJzky9m5iNcGJ1yrRsvLsUgIqUi8qyItJt/S5Y5Zp+IvCIiJ0TkqIh8JGbfN0TkrIgcNl/74pEnnWiqCNAxGMZYCM+ZLC7n6SFXBkBtSS5ZGc73c3stI8miqTKfSFRxfsS5A6Pu4UmUcu+9H6/F8BDwvFKqBXje/LyUKeCjSqndwF3A34lIccz+P1VK7TNfh+OUJ21oqggwPj3PsIP93B2DYfKy/GwqzLFblITi9wmN5c622Iz4jvvKPa8F63/qcHCcodPl83fiVQz3AI+a7x8F7l16gFLqjFKq3XzfCwwC7lp+zIG4ITOpYzBMc6U7g2+r4fSU1cHQLOHZBde6Mq5EowtiPJ3BMCLuje/EqxiqlFJ9AObfyisdLCL7gSygM2bzX5kupodFJDtOedIG64F38qi1YzDsmRo9S2mqyOfCyBQz885cotxKp3XbymFrIZCdwabCHMcrhtriXHKz/HaLsiFWVQwi8pyIHF/mdc96LiQi1cD/Bn5HKWVFjT4N7ACuB0qBT13h/AdF5KCIHAwGg+u5tCepKcolN9Pv2IcjNDNP3/gMzVXe65jAsNiiypgr4ETaB4zquy0ebf/myoCjrWW3p2mvqhiUUrcrpa5a5vUkMGB2+FbHP7jcd4hIIfDvwH9TSr0a8919ymAW+Dqw/wpyPKKUalVKtVZUaE+UzyfGam4OVQwdHqvquZRLfm5ntn/7YJii3EwqAt40wpsrA7Q7NPkiGlV0Dro7vhOvK+kp4AHz/QPAk0sPEJEs4PvAN5VS31myz1IqghGfOB6nPGmFlZnkRCy5WqoKbJYkOVjrGzi1/dsHw7RUBjAeLe/RUhVgai5C7/iM3aK8hYtj00zPR9jmYmstXsXweeAOEWkH7jA/IyKtIvIv5jEfBm4BfnuZtNRvicgx4BhQDvxlnPKkFU0VAeMmnHOen7tjMExWho86ly/nuRJ5WRnUlebSPujMBZM6BsOedSMBtFQaAw7LZeYkrHvCze0f18wXpdQw8K5lth8Efs98/6/Av65w/m3xXD/daakKoJThz7yqtshucS6jfdBYzjPD7905lC2VBbQPOM9iGA7PMjI5R3OlN601gJbKS668W7dfMecl5Vj3RHOFe9vfu09tGmA9HE4ctVqpql6mpSpA11DYceXP2634jofbvyQ/i/JAliMVc/tgmMoC9y3nGYtWDC6moTyfTL9wxmEPh1UOwMsdExgWw3xEcW7EWZlJlmJo8Xj7GwFo5w2K2gdCrnYjgVYMribT72Nreb7j/KydwTBKXfIDe5VFi81p7T8YJj/LT3WRt2acL6WlssBxmUlKKTPw7+57XysGl2M9HE6iIw1cGXDp/3OaO6N9MERzVYFnM5IsWqoChGYWGAzN2i3KIr3jM0zNRbTFoLGXlqoA50emHJWZ1D4Qxu8TGsrz7BYlqeRnZ1BbnOs4xdw+EPa8GwmcqZgXJxZqi0FjJ9uqChYzk5xCx2CYLWV5ZGe4sxzAethWFXCUYhifmmcwNJsWimExZdVBcYYOj8R3tGJwOdYkmjMO8nO3D4Y8O+N5KS1VBXQGw0SizvBzdwTdn0O/VsoDWRTnZToq+aJ9IEx5IIuS/Cy7RYkLrRhczpYyIzPJKaPWuYUo3cNTadExgeHOmFuIct4hmUmWW8Xtroy1ICK0VAbocJDFcGYw5Im214rB5TgtM+nc8CSRqPJ84NliW5WzZuC2u3gB+o3QXFnAmQFnZCYppegY8MaMc60YPEBLVYFjzOlLOfTuHzWthcUAqEMstvZBo6qnF9fAWI6WSmPBqqGw/QtWDUzMEppdcH18AbRi8ATbKgu4MOqMzKRL6wC4c4GS9RKwMpMcYjF0DIQ80TGtFWt07oQAtCWDF0qRaMXgAWJrJtlNW3+IutJc8rK8tQD9lbBKQNtNaGae3vEZz1a0XQ7LMnVCldvF+I52JWmcgJMyk073T7BjU6HdYqQUIwBqf2aStZqfm9cBWC9VhdkUZGc4Yi5D+2CI0vwsyj2wBoZWDB7AykyyO84wMx/h7NAkOzelz4gVjAD07EKUnlF7M5Pa+icA2JFG7S8iNFcFHDEoah/wTuFIrRg8gJWZZHfaXsdgmKiCHdXpZTFYy5faPWo91RciL8tPfam3Z5wvZcemQk73h2zNTLJqJGnFoHEUTshMOtVnjFi3p9GIFS7Ncm2zedR6un+C7ZsK0iYjyWJndQHj0/P0T9i3mlvf+Azj0/OesZbjUgwiUioiz4pIu/m3ZIXjIjGrtz0Vs32riLxmnv+4uQyoZgM4ITPpdH+I7AwfDWXpkZFkUZCTyeaS3EXFaAdKKU71hdIuvgMs/s+n++xTzNZvv9Mj1nK8FsNDwPNKqRbgefPzckwrpfaZr7tjtn8BeNg8fxT4WJzypC3bNxmZSXb6Wtv6Q2yrKsCfZiNWMDoEOxVD/4Q5Yq32xoh1Peww/+eTNra/9dt7xY0ar2K4B3jUfP8ocO9aTxSjJvBtwHc3cr7mcqyRip2dk5GRlH4dExjtf3Zokpl5eyw2a7ScjhZDYU4mtcW5nO630WLoD1Ffmkcg2xtp2vEqhiqlVB+A+XelxVdzROSgiLwqIlbnXwaMKaUWzM89QG2c8qQtdSXGTWnXqCkYmmUoPOeZEdN62VVdQFQZVpMdnOpPz/iOhd0W26k+bw2KVlVvIvIcsGmZXX+2juvUK6V6RaQR+KmIHAOW+xVXTCsQkQeBBwHq6+vXcen0wOcTdlYXcLLXnofD6hC99HCsh1iLbW9dccqvf7ovRG1xLkW57l1nOB52Vhfw09MDzMxHyMlMbbn36bkI3UOTfODqmpReN5msajEopW5XSl21zOtJYEBEqgHMv4MrfEev+bcLeBG4BhgCikXEUk6bgd4ryPGIUqpVKdVaUVGxjn8xfdhZbaTtRW2YaHU6DXPoY6krySM/y2/bqPV0/0RaxhcsdmwqJKrsmQHdNhAiqrwTeIb4XUlPAQ+Y7x8Anlx6gIiUiEi2+b4ceDtwUhlJxy8AH7zS+Zq1s6u6kPDsAhdsmGh1uj9EeSCbMg/M+twIPp+wo7qQUzZkxswuROgMTqZlfMFip40BaGswsEsrhkU+D9whIu3AHeZnRKRVRP7FPGYncFBEjmAogs8rpU6a+z4FfFJEOjBiDl+NU560ZleNcWPa4U5K9xErGJ3Tqf6JlE+0ah8wynHsSOP231KWT06mz5aU1dN9E+Rn+dlc4p1S53GF0JVSw8C7ltl+EPg98/3LwJ4Vzu8C9scjg+YS26oK8IkxgnnPnuqUXXchEqV9IMxHb9qSsms6kZ3Vhfzrq+fpGZ2mLoWzj0/3p29GkoXfJ2zfZE8A+lRfiB3VhZ6aWKhnPnuInEw/TRWBlJvT3cNTzC5E2Z7GHRPYlzJ8um/CnFiYXqUwlrJzUwGnU2yxKaU45UFrWSsGj7GrJvV+7nTPSLLYsakAEVLe/qfNiYUZ/vR+nHdsKmB0ap7B0GzKrtkzOk1oZsFTgWfQisFz7Kwu5OLYNGNTqVvR6nT/BH6feKaA2EbJy8qgoSw/9RaDB0esG8HqnFNpMS/OePaYtawVg8fYZcPDcaJ3gsby/JTnjzsRKwCdKhYnFnqsY9oIdtRMOt0fQsR71rJWDB7jkp87NQ+HUoqjPWNcvTn1k7qcyM5NhZwbniI8u7D6wQng0ojVWx3TRijKs0pjpNZi2FKaR75HSmFYaMXgMSoKsqkoyE5Zymrv+AxD4Tn21hWl5HpOx1LMbSnqnI5dHAdgd41ufzAU5IkUpmuf6pvwXHwBtGLwJLuqC1PmSjrWMwbAnlrdMQHstOaSpMhiO3JhjMbyfIry0rMUxlKu3lxMZzBMaGY+6dcKzcxzbmTKk248rRg8yK6aQjoGQ8wtRJN+rSM942T4xJOjpo1QU5RDSV4mx3vGU3K9Iz1jttRmcip764pQ6pIllUyO9YyjFJ60lrVi8CC7qguZj6iUrM1wtGeMHdUFOvBsIiLsqyvm0IXRpF+rf3yGgYlZrt7svY5po+wzleSRC8lXDIcujF12TS+hFYMH2WsGgo+Ybp5kEY0qjvaM68DzEvbVldA+mHx3xmGzY9IWwyWK87JoKMvjcAoU8+ELY2wtz6c4z3sLT2rF4EHqSnMpy8/izXPJVQzdw5OEZhbYq0esl3FNfTFKwdEku5OO9IyR4RNPFW9LBHvripNuMSilOHxhzJPWAmjF4ElEhGvqS5LuzrA6Pm0xXI41grdG9MniaM8YO6sLtRtvCXs3F9M/MUP/+EzSrtE7PkMwNKsVg8ZdXFNfTFdwMqkzoI/2jJOT6aMlzWc8L6UoN5PGinwOnU+eYohGFUcvjHsy8Bkv++qT70o9fN678QXQisGzXFtfAlwKkCWDoz1j7K4pSvsaPctxTV0Jhy+MJq2gW9fQJKHZhcV4kuYSu6oLyfAJR5J47x/pGSMrw+fZbDz9RHuUqzcX4RM4dC457qSFSJTjveM6I2YF9tUXMxSeo2d0Oinff0QHnlckJ9PPzurCpLryDp8fY3dNIVkZ3uxCvflfacjPzmDHpsKkWQztg2Fm5qN6xLoC15gddrLa/2jPGPlZRpl1zVvZV1fM0Z7xpCxzuxCJcuziuKfv/bgUg4iUisizItJu/i1Z5ph3isjhmNeMiNxr7vuGiJyN2bcvHnk0l3NNfTGHz48l5eE4avpvtcWwPNs3FZCT6Vv0RSeawz3j7NlchN9Di8Mkkr11xYRnF+gaSvwa0G0DIabnI1xTrxXDSjwEPK+UagGeNz9fhlLqBaXUPqXUPuA2YAr4Scwhf2rtV0odjlMeTQzX1pcQml2gI5j4h+NIzzgFOUaZac1byfT72FNblJR8+tmFCKd6J7Qb6QrsM4Pyh5OQtnrYwxPbLOJVDPcAj5rvHwXuXeX4DwL/oZRK/Wr1aYg1ojl0PvGd0xvdo+yrK/bUcoaJZl9dMcd7JxJemuR0X4i5SJR9HnZlxEtjeYCC7IykKObD58cozc+iPoXLt6aaeBVDlVKqD8D8W7nK8fcBjy3Z9lciclREHhaR7JVOFJEHReSgiBwMBoPxSZ0mGLMyMxM+0S0YmqVtIMTbmsoT+r1e45r6EuYWoglfuOdNU9Fri2FlfD7h6rqipASgj/SMsXdzESLeHRStqhhE5DkROb7M6571XEhEqoE9wDMxmz8N7ACuB0qBT610vlLqEaVUq1KqtaKiYj2XTltEhGuSULfn1a5hAN7WVJbQ7/Ua+5I00e2ljmG2lOVRU5yb0O/1GtdtKeVk7wTj04krTTIxM0/7YJh9dW8Jp3qKVRWDUup2pdRVy7yeBAbMDt/q+Aev8FUfBr6vlFr8lZRSfcpgFvg6sD++f0ezlGvrjbo9Ewms2/Ny5zAFORnsrvFmDneiqC7KYVNhDq+fHUnYdy5EorzaNczbm7W1thrvaC4nqi4NZBLBq53DKAU3NJYm7DudSLyupKeAB8z3DwBPXuHY+1niRopRKoIRnzgepzyaJVxTX4JS8EYC5zO80jnEDVvL9MS2VRAR3tFSzi87hogkKDPsSM844dkF3qEVw6rsqysmL8vPL9uHEvadv2gfIi/LvziB1KvE+2R/HrhDRNqBO8zPiEiriPyLdZCINAB1wM+WnP8tETkGHAPKgb+MUx7NElobSsjO8PHzM4mJy1wcm6Z7eEq7kdbIzS3ljE/PJ2x9gJc6hhCBmxp1+69GVoaPGxvL+GVHIhVDkJsayzw7sc0irv9OKTWslHqXUqrF/Dtibj+olPq9mOO6lVK1SqnokvNvU0rtMV1Tv6mUSnxeZZqTk+nnxsYyftaWGMXwSqdhlt+kFcOauLmlAhESpph/2THEVTVFlOR7r9RzMnh7czlnhybpGY0/EfLCyBTdw1O8o8X71pq31Z4GgFu3V9A1NMn54fgfjpc7hyjNz2J7lV58fi2U5mexp7YoIYphcnaBQ+dHeVuzVspr5WazE38pAVbDL0yX1M0t3k9+0YohDbh1u5FF/LMzV8oNWB2lFK90DnNTY5mev7AObm4p59CFsbgTAF7vHmE+onR8YR20VAaoLMjmlx3xB6B/0R6kpiiHpgrvT+rUiiENaCjLo740jxfjdCd1D0/RNz6j3Ujr5JaWCiJRxctxdk4vtQ+RleHj+gZvZ8QkEhHhHc3lvNQxFFdpmIVIlJc6hkzXoPcHRVoxpAEiwq3bK3i5c5iZ+ciGv+flTsOU1oHn9XHtlhIC2Rn8vD0+xfxS5zCtW0r0wjzr5B0t5YxMznEyjomGRy+OMzGzwM3b0sNa04ohTbh1ewXT8xEOdG88p/7ljmE2Feawtdz7pnQiyfT7uKmpjJ+fCW54fYah8Cyn+ib0/IUNYLVZPHGGX5wxssHeniaz/bViSBNuaiwnK8O3YXfS1NwCL7QN8p+2pYcpnWhuaSmnZ9RI9d0IVqemFcP6qSrMYVtVIK601V+0B9lTmz7ZYFoxpAm5WX5u2FrKi20bC0A/e3KAqbkI915Tm2DJ0oNbthmZLBvNTvrR0T7KA9nsqdVlzjfCzS0VvHZ2ZEPlMSZm5jl0YWwxwykd0Iohjbh1eyWdwUkujKx/1Pq9Ny9SW5zLDVt14HMjbCnLp6Esj2dO9K/73KHwLC+cHuRXr63V6y9skHv21TC3EOWpI73rPvffj/YRiSretbMqCZI5E60Y0ojbdhhpqz862reu8wZDM/yiPcg9+2p0mmocfPC6zbzcOczZocl1nfeDQxdZiCo+dN3mJEnmffbUFrFjUwFPHLiw7nMfe/0826sKFlflSwe0Ykgjtpbnc2NjKf/66rl11e556nAvUQW/ot1IcfHh1joyfMJjr59f8zlKKb77Rg9764pp0ZMKN4yI8JHr6zh2cZyTvWvPTjp+cZyjPePcv78urWJrWjGkGb/9tgYujk3z3KmBNZ/zg8MXuaq2UHdMcVJZmMMdu6r4zsELa04bPn5xgtP9IW0tJIB799WS5ffxxMG1Ww3fPnCe7Awfv3JNerW/Vgxpxu07q6gtzuUbL3Wv6fj2gRDHL06k3YORLH7jhi2MTs3z4+NrizV8540LZGf4+MDemiRL5n1K8rO4c3cV3z90cU2KeWpugR8c6uV9e6opystMgYTOQSuGNCPD7+M3b9zCK13DtPWHVj3+e4cu4vcJd+uOKSG8ramMhrI8vvXauVWPnZmP8OThXt69exNFuenVMSWLj1xfx/j0PD85ubrF/KOjfYRnF7j/hvoUSOYstGJIQ+67vo7sDB+PvtJ9xeNGJ+d44sAFbm4pp6JgxVVXNevA5xN+/YZ6DnSPrqqYnz05wPj0PB9q1dZaonh7Uzm1xblrCkI/9vp5misDtG7x9toLy6EVQxpSkp/Fvftq+f6bFxmfWjmv+y9+dJLx6Xn+9N3bUyid9/ngdXVk+X184+WzKx4zPjXPXz99isbyfL22dgLx+Ywg9C87hvjZFeaUvNY1zKHzY9y/vz6tgs4WcSkGEfmQiJwQkaiItF7huLtEpE1EOkTkoZjtW0XkNRFpF5HHRSQ9phU6gAfe1sD0fIS/evrksmUafnp6gO8dusgf3trE7ho9qSqRlOZn8es31PPY6xf44TJ59Uop/usPjjEYmuXhj+zTcxcSzO/f3Mj2qgI++fhhBiZm3rJ/cGKGTzx2iIayPD6cptZavBbDceBXgZ+vdICI+IEvAe8BdgH3i8guc/cXgIeVUi3AKPCxOOXRrJFdNYX8X7c188TBHr7w47bL9k3MzPNfv3ecbVUBPn5bs00SeptPv3cH1zeU8J+/c4SjPWOX7fvOGz38+9E+PnnnNvamUe58qsjN8vOl37iGqbkIf/TYIRYil9YPm1uI8n9+600mZxf4ym+1UpCTnrGdeFdwO6WUalvlsP1Ah1KqSyk1B3wbuMdc5/k24LvmcY9irPusSRGfvGMbv3XjFr78s06+/LNOpuYWeLlziP/niSMMhmb4mw/uJTtDV/JMBtkZfv7pN6+jPJDN73/zIOeHp7gwMsXPzwT57FMnuLGxlP/jlia7xfQszZUF/OW9V/Ha2RG++EwbZ4cm6R+f4XM/OsEb50b5mw9ezfZN6ZuenZGCa9QCsZGeHuAGoAwYU0otxGzXM6hSiIjw/969m7HpeT7/H6f54jNtixPf/uT2bezTo9WkUh7I5p8/2soHv/wyt3zxhcXtxXmZ2oWUAn7tus280jXMV37exVd+3rW4/cFbGnn/1emdhbeqYhCR54BNy+z6M6XUk2u4xnJ3t7rC9pXkeBB4EKC+Pv3Sx5KFzyf87Yf2srU8n2hUcd2WEq6tL0m7vG272FVTyGO/fyMvdQ5RHsimsiCb3TVFOgssRfz1r+7hvXs2MTG9wPR8hLwsP+/bU223WLazqmJQSt0e5zV6gLqYz5uBXmAIKBaRDNNqsLavJMcjwCMAra2tG1+KSfMWsjJ8fPKObXaLkbbsrSvWsQSbyPT7uG1H+hTHWyupSFc9ALSYGUhZwH3AU8pIhXkB+KB53APAWiwQjUaj0SSReNNVf0VEeoCbgH8XkWfM7TUi8jSAaQ18AngGOAU8oZQ6YX7Fp4BPikgHRszhq/HIo9FoNJr4kY0uNWgnra2t6uDBg3aLodFoNK5CRN5QSq0458xCz3zWaDQazWVoxaDRaDSay9CKQaPRaDSXoRWDRqPRaC5DKwaNRqPRXIYrs5JEJAisvtLJ8pRjTK5zGlqu9aHlWh9arvXhVbm2KKUqVjvIlYohHkTk4FrStVKNlmt9aLnWh5ZrfaS7XNqVpNFoNJrL0IpBo9FoNJeRjorhEbsFWAEt1/rQcq0PLdf6SGu50i7GoNFoNJork44Wg0aj0WiugGcVg4jcJSJtItIhIg8tsz9bRB43978mIg0pkKlORF4QkVMickJE/u9ljrlVRPybzMsAAAS3SURBVMZF5LD5+kyy5TKv2y0ix8xrvqVCoRj8vdleR0Xk2hTItD2mHQ6LyISI/PGSY1LSXiLyNREZFJHjMdtKReRZEWk3/5ascO4D5jHtIvJACuT6ooicNn+n74vIsos9rPabJ0Guz4rIxZjf6r0rnHvFZzcJcj0eI1O3iBxe4dxktteyfYNt95hSynMvwA90Ao1AFnAE2LXkmD8Evmy+vw94PAVyVQPXmu8LgDPLyHUr8CMb2qwbKL/C/vcC/4Gx8t6NwGs2/Kb9GHnYKW8v4BbgWuB4zLa/AR4y3z8EfGGZ80qBLvNvifm+JMly3QlkmO+/sJxca/nNkyDXZ4H/vIbf+YrPbqLlWrL/b4HP2NBey/YNdt1jXrUY9gMdSqkupdQc8G3gniXH3AM8ar7/LvAuEUnqIrtKqT6l1Jvm+xDG+hRuWef6HuCbyuBVjNX3UrkG4ruATqXURic2xoVS6ufAyJLNsffQo8C9y5z6buBZpdSIUmoUeBa4K5lyKaV+oi6tpf4qxuqIKWWF9loLa3l2kyKX+fx/GHgsUddbK1foG2y5x7yqGGqBCzGfe3hrB7x4jPkQjWMsFpQSTNfVNcBry+y+SUSOiMh/iMjuFImkgJ+IyBtirK+9lLW0aTK5j5UfWDvaC6BKKdUHxoMNVC5zjN3t9rsYlt5yrPabJ4NPmC6ur63gFrGzvW4GBpRS7SvsT0l7LekbbLnHvKoYlhv5L02/WssxSUFEAsC/AX+slJpYsvtNDHfJXuAfgB+kQibg7Uqpa4H3AB8XkVuW7LezvbKAu4HvLLPbrvZaK3a2258BC8C3Vjhktd880fwT0ATsA/ow3DZLsa29gPu5srWQ9PZapW9Y8bRltsXVZl5VDD1AXcznzUDvSseISAZQxMZM33UhIpkYP/y3lFLfW7pfKTWhlAqb758GMkWkPNlyKaV6zb+DwPcxTPpY1tKmyeI9wJtKqYGlO+xqL5MBy51m/h1c5hhb2s0MQL4f+A1lOqKXsobfPKEopQaUUhGlVBT45xWuZ1d7ZQC/Cjy+0jHJbq8V+gZb7jGvKoYDQIuIbDVHm/cBTy055inAit5/EPjpSg9QojB9mF8FTiml/ucKx2yyYh0ish/jNxpOslz5IlJgvccIXh5fcthTwEfF4EZg3DJxU8CKIzk72iuG2HvoAeDJZY55BrhTREpM18md5rakISJ3YaynfrdSamqFY9bymydartiY1K+scL21PLvJ4HbgtFKqZ7mdyW6vK/QN9txjyYiwO+GFkUVzBiPD4c/MbZ/DeFgAcjBcEx3A60BjCmR6B4aJdxQ4bL7eC/wB8AfmMZ8ATmBkY7wKvC0FcjWa1ztiXttqr1i5BPiS2Z7HgNYU/Y55GB19Ucy2lLcXhmLqA+YxRmgfw4hJPQ+0m39LzWNbgX+JOfd3zfusA/idFMjVgeFztu4xK/uuBnj6Sr95kuX63+a9cxSjw6teKpf5+S3PbjLlMrd/w7qnYo5NZXut1DfYco/pmc8ajUajuQyvupI0Go1Gs0G0YtBoNBrNZWjFoNFoNJrL0IpBo9FoNJehFYNGo9FoLkMrBo1Go9FchlYMGo1Go7kMrRg0Go1Gcxn/Px8G3YTg49XbAAAAAElFTkSuQmCC&#10;"> diff --git a/src/test/datascience/mockCode2ProtocolConverter.ts b/src/test/datascience/mockCode2ProtocolConverter.ts new file mode 100644 index 000000000000..cff4a5f647d1 --- /dev/null +++ b/src/test/datascience/mockCode2ProtocolConverter.ts @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as code from 'vscode'; +import { Code2ProtocolConverter } from 'vscode-languageclient/node'; +import * as proto from 'vscode-languageserver-protocol'; + +// tslint:disable:no-any unified-signatures +export class MockCode2ProtocolConverter implements Code2ProtocolConverter { + public asTextDocumentIdentifier(textDocument: code.TextDocument): proto.TextDocumentIdentifier { + return { uri: textDocument.uri.toString() }; + } + public asVersionedTextDocumentIdentifier(textDocument: code.TextDocument): proto.VersionedTextDocumentIdentifier { + return { uri: textDocument.uri.toString(), version: textDocument.version }; + } + public asOpenTextDocumentParams(textDocument: code.TextDocument): proto.DidOpenTextDocumentParams { + return { + textDocument: { + uri: textDocument.uri.toString(), + languageId: 'PYTHON', + version: textDocument.version, + text: textDocument.getText() + } + }; + } + + public asChangeTextDocumentParams(textDocument: code.TextDocument): proto.DidChangeTextDocumentParams; + public asChangeTextDocumentParams(event: code.TextDocumentChangeEvent): proto.DidChangeTextDocumentParams; + public asChangeTextDocumentParams(arg: any): proto.DidChangeTextDocumentParams { + if (this.isTextDocument(arg)) { + return { + textDocument: { + uri: arg.uri.toString(), + version: arg.version + }, + contentChanges: [{ text: arg.getText() }] + }; + } else if (this.isTextDocumentChangeEvent(arg)) { + const document = arg.document; + return { + textDocument: { + uri: document.uri.toString(), + version: document.version + }, + contentChanges: arg.contentChanges.map( + (change): proto.TextDocumentContentChangeEvent => { + const range = change.range; + return { + range: { + start: { line: range.start.line, character: range.start.character }, + end: { line: range.end.line, character: range.end.character } + }, + rangeLength: change.rangeLength, + text: change.text + }; + } + ) + }; + } else { + throw Error('Unsupported text document change parameter'); + } + } + public asCloseTextDocumentParams(_textDocument: code.TextDocument): proto.DidCloseTextDocumentParams { + throw new Error('Method not implemented.'); + } + public asSaveTextDocumentParams( + _textDocument: code.TextDocument, + _includeContent?: boolean | undefined + ): proto.DidSaveTextDocumentParams { + throw new Error('Method not implemented.'); + } + public asWillSaveTextDocumentParams(_event: code.TextDocumentWillSaveEvent): proto.WillSaveTextDocumentParams { + throw new Error('Method not implemented.'); + } + public asTextDocumentPositionParams( + _textDocument: code.TextDocument, + _position: code.Position + ): proto.TextDocumentPositionParams { + return { + textDocument: { + uri: _textDocument.uri.fsPath + }, + position: { + line: _position.line, + character: _position.character + } + }; + } + public asCompletionParams( + _textDocument: code.TextDocument, + _position: code.Position, + _context: code.CompletionContext + ): proto.CompletionParams { + const triggerKind = _context.triggerKind as number; + return { + textDocument: { + uri: _textDocument.uri.fsPath + }, + position: { + line: _position.line, + character: _position.character + }, + context: { + triggerCharacter: _context.triggerCharacter, + triggerKind: triggerKind as proto.CompletionTriggerKind + } + }; + } + public asWorkerPosition(_position: code.Position): proto.Position { + throw new Error('Method not implemented.'); + } + public asPosition(value: code.Position): proto.Position; + public asPosition(value: undefined): undefined; + public asPosition(value: null): null; + public asPosition(value: code.Position | null | undefined): proto.Position | null | undefined; + public asPosition(_value: any): any { + if (_value === undefined || _value === null) { + return _value; + } + return { line: _value.line, character: _value.character }; + } + public asRange(value: code.Range): proto.Range; + public asRange(value: undefined): undefined; + public asRange(value: null): null; + public asRange(value: code.Range | null | undefined): proto.Range | null | undefined; + public asRange(_value: any): any { + throw new Error('Method not implemented.'); + } + public asDiagnosticSeverity(_value: code.DiagnosticSeverity): number { + throw new Error('Method not implemented.'); + } + public asDiagnostic(_item: code.Diagnostic): proto.Diagnostic { + throw new Error('Method not implemented.'); + } + public asDiagnostics(_items: code.Diagnostic[]): proto.Diagnostic[] { + throw new Error('Method not implemented.'); + } + public asCompletionItem(_item: code.CompletionItem): proto.CompletionItem { + throw new Error('Method not implemented.'); + } + public asTextEdit(_edit: code.TextEdit): proto.TextEdit { + throw new Error('Method not implemented.'); + } + public asReferenceParams( + _textDocument: code.TextDocument, + _position: code.Position, + _options: { includeDeclaration: boolean } + ): proto.ReferenceParams { + throw new Error('Method not implemented.'); + } + public asCodeActionContext(_context: code.CodeActionContext): proto.CodeActionContext { + throw new Error('Method not implemented.'); + } + public asCommand(_item: code.Command): proto.Command { + throw new Error('Method not implemented.'); + } + public asCodeLens(_item: code.CodeLens): proto.CodeLens { + throw new Error('Method not implemented.'); + } + public asFormattingOptions(_item: code.FormattingOptions): proto.FormattingOptions { + throw new Error('Method not implemented.'); + } + public asDocumentSymbolParams(_textDocument: code.TextDocument): proto.DocumentSymbolParams { + throw new Error('Method not implemented.'); + } + public asCodeLensParams(_textDocument: code.TextDocument): proto.CodeLensParams { + throw new Error('Method not implemented.'); + } + public asDocumentLink(_item: code.DocumentLink): proto.DocumentLink { + throw new Error('Method not implemented.'); + } + public asDocumentLinkParams(_textDocument: code.TextDocument): proto.DocumentLinkParams { + throw new Error('Method not implemented.'); + } + public asSignatureHelpParams( + _textDocument: code.TextDocument, + _position: code.Position, + _context: code.SignatureHelpContext + ): proto.SignatureHelpParams { + throw new Error('Method not implemented.'); + } + public asPositions(_value: code.Position[]): proto.Position[] { + throw new Error('Method not implemented.'); + } + public asLocation(value: code.Location): proto.Location; + public asLocation(value: undefined): undefined; + public asLocation(value: null): null; + public asLocation(value: code.Location | undefined | null): proto.Location | undefined | null; + public asLocation(_value: any): any { + throw new Error('Method not implemented.'); + } + public asDiagnosticTag(_value: code.DiagnosticTag): number | undefined { + throw new Error('Method not implemented.'); + } + public asSymbolKind(_item: code.SymbolKind): proto.SymbolKind { + throw new Error('Method not implemented.'); + } + public asSymbolTag(_item: code.SymbolTag): 1 { + throw new Error('Method not implemented.'); + } + public asSymbolTags(_items: readonly code.SymbolTag[]): 1[] { + throw new Error('Method not implemented.'); + } + public asUri(_uri: code.Uri): string { + throw new Error('Method not implemented.'); + } + public asCallHierarchyItem(_value: code.CallHierarchyItem): proto.CallHierarchyItem { + throw new Error('Method not implemented.'); + } + private isTextDocumentChangeEvent(value: any): value is code.TextDocumentChangeEvent { + const candidate = <code.TextDocumentChangeEvent>value; + return !!candidate.document && !!candidate.contentChanges; + } + + private isTextDocument(value: any): value is code.TextDocument { + const candidate = <code.TextDocument>value; + return !!candidate.uri && !!candidate.version; + } +} diff --git a/src/test/datascience/mockCommandManager.ts b/src/test/datascience/mockCommandManager.ts new file mode 100644 index 000000000000..91cd18311c8e --- /dev/null +++ b/src/test/datascience/mockCommandManager.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { noop } from 'lodash'; +import { Disposable, TextEditor, TextEditorEdit } from 'vscode'; + +import { ICommandNameArgumentTypeMapping } from '../../client/common/application/commands'; +import { ICommandManager } from '../../client/common/application/types'; + +// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length +export class MockCommandManager implements ICommandManager { + private commands: Map<string, (...args: any[]) => any> = new Map<string, (...args: any[]) => any>(); + + public dispose() { + this.commands.clear(); + } + public registerCommand< + E extends keyof ICommandNameArgumentTypeMapping, + U extends ICommandNameArgumentTypeMapping[E] + >(command: E, callback: (...args: U) => any, thisArg?: any): Disposable { + this.commands.set(command, thisArg ? (callback.bind(thisArg) as any) : (callback as any)); + return { + dispose: () => { + noop(); + } + }; + } + + public registerTextEditorCommand( + _command: string, + _callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, + _thisArg?: any + ): Disposable { + throw new Error('Method not implemented.'); + } + public executeCommand< + T, + E extends keyof ICommandNameArgumentTypeMapping, + U extends ICommandNameArgumentTypeMapping[E] + >(command: E, ...rest: U): Thenable<T | undefined> { + const func = this.commands.get(command); + if (func) { + const result = func(...rest); + const tPromise = result as Promise<T>; + if (tPromise) { + return tPromise; + } + return Promise.resolve(result); + } + return Promise.resolve(undefined); + } + + public getCommands(_filterInternal?: boolean): Thenable<string[]> { + const keys = Object.keys(this.commands); + return Promise.resolve(keys); + } +} diff --git a/src/test/datascience/mockCustomEditorService.ts b/src/test/datascience/mockCustomEditorService.ts new file mode 100644 index 000000000000..03ceee2b8c25 --- /dev/null +++ b/src/test/datascience/mockCustomEditorService.ts @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { inject, injectable } from 'inversify'; +import { CancellationTokenSource, Disposable, Uri, WebviewPanel, WebviewPanelOptions } from 'vscode'; +import { CancellationToken } from 'vscode-languageclient/node'; +import { + CustomDocument, + CustomEditorProvider, + ICommandManager, + ICustomEditorService +} from '../../client/common/application/types'; +import { IDisposableRegistry } from '../../client/common/types'; +import { noop } from '../../client/common/utils/misc'; +import { NotebookModelChange } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { NativeEditorProvider } from '../../client/datascience/notebookStorage/nativeEditorProvider'; +import { NativeEditorNotebookModel } from '../../client/datascience/notebookStorage/notebookModel'; +import { INotebookStorageProvider } from '../../client/datascience/notebookStorage/notebookStorageProvider'; +import { INotebookEditor, INotebookEditorProvider } from '../../client/datascience/types'; +import { createTemporaryFile } from '../utils/fs'; + +@injectable() +export class MockCustomEditorService implements ICustomEditorService { + private provider: CustomEditorProvider | undefined; + private resolvedList = new Map<string, Thenable<void> | void>(); + private undoStack = new Map<string, unknown[]>(); + private redoStack = new Map<string, unknown[]>(); + + constructor( + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(INotebookStorageProvider) private readonly storage: INotebookStorageProvider + ) { + disposableRegistry.push( + commandManager.registerCommand('workbench.action.files.save', this.onFileSave.bind(this)) + ); + disposableRegistry.push( + commandManager.registerCommand('workbench.action.files.saveAs', this.onFileSaveAs.bind(this)) + ); + } + + public registerCustomEditorProvider( + _viewType: string, + provider: CustomEditorProvider, + _options?: { + readonly webviewOptions?: WebviewPanelOptions; + + /** + * Only applies to `CustomReadonlyEditorProvider | CustomEditorProvider`. + * + * Indicates that the provider allows multiple editor instances to be open at the same time for + * the same resource. + * + * If not set, VS Code only allows one editor instance to be open at a time for each resource. If the + * user tries to open a second editor instance for the resource, the first one is instead moved to where + * the second one was to be opened. + * + * When set, users can split and create copies of the custom editor. The custom editor must make sure it + * can properly synchronize the states of all editor instances for a resource so that they are consistent. + */ + readonly supportsMultipleEditorsPerDocument?: boolean; + } + ): Disposable { + // Only support one view type, so just save the provider + this.provider = provider; + + // Sign up for close so we can clear our resolved map + // tslint:disable-next-line: no-any + ((this.provider as any) as INotebookEditorProvider).onDidCloseNotebookEditor(this.closedEditor.bind(this)); + // tslint:disable-next-line: no-any + ((this.provider as any) as INotebookEditorProvider).onDidOpenNotebookEditor(this.openedEditor.bind(this)); + + return { dispose: noop }; + } + public async openEditor(file: Uri, _viewType: string): Promise<void> { + if (!this.provider) { + throw new Error('Opening before registering'); + } + + // Make sure not to resolve more than once for the same file. At least in testing. + let resolved = this.resolvedList.get(file.toString()); + if (!resolved) { + // Pass undefined as the webview panel. This will make the editor create a new one + resolved = this.provider.resolveCustomEditor( + this.createDocument(file), + // tslint:disable-next-line: no-any + (undefined as any) as WebviewPanel, + CancellationToken.None + ); + this.resolvedList.set(file.toString(), resolved); + } + + await resolved; + } + + public undo(file: Uri) { + this.popAndApply(file, this.undoStack, this.redoStack, (e) => { + this.getModel(file) + .then((m) => m?.undoEdits([e as NotebookModelChange])) + .ignoreErrors(); + }); + } + + public redo(file: Uri) { + this.popAndApply(file, this.redoStack, this.undoStack, (e) => { + this.getModel(file) + .then((m) => m?.applyEdits([e as NotebookModelChange])) + .ignoreErrors(); + }); + } + + private popAndApply( + file: Uri, + from: Map<string, unknown[]>, + to: Map<string, unknown[]>, + apply: (element: unknown) => void + ) { + const key = file.toString(); + const fromStack = from.get(key); + if (fromStack) { + const element = fromStack.pop(); + apply(element); + let toStack = to.get(key); + if (toStack === undefined) { + toStack = []; + to.set(key, toStack); + } + toStack.push(element); + } + } + + private createDocument(file: Uri): CustomDocument { + return { + uri: file, + dispose: noop + }; + } + + private async getModel(file: Uri): Promise<NativeEditorNotebookModel | undefined> { + const nativeProvider = this.provider as NativeEditorProvider; + if (nativeProvider) { + return (nativeProvider.loadModel(file) as unknown) as Promise<NativeEditorNotebookModel | undefined>; + } + return undefined; + } + + private async onFileSave(file: Uri) { + const model = await this.getModel(file); + if (model) { + await this.storage.save(model, new CancellationTokenSource().token); + } + } + + private async onFileSaveAs(file: Uri) { + const model = await this.getModel(file); + if (model) { + const tmp = await createTemporaryFile('.ipynb'); + await this.storage.saveAs(model, Uri.file(tmp.filePath)); + } + } + + private closedEditor(editor: INotebookEditor) { + this.resolvedList.delete(editor.file.toString()); + } + + private openedEditor(editor: INotebookEditor) { + // Listen for model changes + this.getModel(editor.file) + .then((m) => m?.onDidEdit(this.onEditChange.bind(this, editor.file))) + .ignoreErrors(); + } + + private onEditChange(file: Uri, e: unknown) { + let stack = this.undoStack.get(file.toString()); + if (stack === undefined) { + stack = []; + this.undoStack.set(file.toString(), stack); + } + stack.push(e); + } +} diff --git a/src/test/datascience/mockDebugService.ts b/src/test/datascience/mockDebugService.ts new file mode 100644 index 000000000000..30f993b744fb --- /dev/null +++ b/src/test/datascience/mockDebugService.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { inject, injectable, named } from 'inversify'; +import { + Breakpoint, + BreakpointsChangeEvent, + DebugAdapterDescriptorFactory, + DebugAdapterTrackerFactory, + DebugConfiguration, + DebugConfigurationProvider, + DebugConsole, + DebugSession, + DebugSessionCustomEvent, + Disposable, + Event, + WorkspaceFolder +} from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { Identifiers } from '../../client/datascience/constants'; +import { IJupyterDebugService } from '../../client/datascience/types'; + +@injectable() +export class MockDebuggerService implements IJupyterDebugService { + constructor( + @inject(IJupyterDebugService) + @named(Identifiers.RUN_BY_LINE_DEBUGSERVICE) + private jupyterDebugService: IJupyterDebugService + ) {} + public get activeDebugSession(): DebugSession | undefined { + return this.activeService.activeDebugSession; + } + + public get activeDebugConsole(): DebugConsole { + return this.activeService.activeDebugConsole; + } + public get breakpoints(): Breakpoint[] { + return this.activeService.breakpoints; + } + public get onDidChangeActiveDebugSession(): Event<DebugSession | undefined> { + return this.activeService.onDidChangeActiveDebugSession; + } + public get onDidStartDebugSession(): Event<DebugSession> { + return this.activeService.onDidStartDebugSession; + } + public get onDidReceiveDebugSessionCustomEvent(): Event<DebugSessionCustomEvent> { + return this.activeService.onDidReceiveDebugSessionCustomEvent; + } + public get onDidTerminateDebugSession(): Event<DebugSession> { + return this.activeService.onDidTerminateDebugSession; + } + public get onDidChangeBreakpoints(): Event<BreakpointsChangeEvent> { + return this.onDidChangeBreakpoints; + } + public get onBreakpointHit(): Event<void> { + return this.activeService.onBreakpointHit; + } + public startRunByLine(config: DebugConfiguration): Thenable<boolean> { + return this.jupyterDebugService.startRunByLine(config); + } + public registerDebugConfigurationProvider(debugType: string, provider: DebugConfigurationProvider): Disposable { + return this.jupyterDebugService.registerDebugConfigurationProvider(debugType, provider); + } + public registerDebugAdapterDescriptorFactory( + debugType: string, + factory: DebugAdapterDescriptorFactory + ): Disposable { + return this.jupyterDebugService.registerDebugAdapterDescriptorFactory(debugType, factory); + } + public registerDebugAdapterTrackerFactory(debugType: string, factory: DebugAdapterTrackerFactory): Disposable { + return this.jupyterDebugService.registerDebugAdapterTrackerFactory(debugType, factory); + } + public startDebugging( + folder: WorkspaceFolder | undefined, + nameOrConfiguration: string | DebugConfiguration, + parentSession?: DebugSession | undefined + ): Thenable<boolean> { + return this.activeService.startDebugging(folder, nameOrConfiguration, parentSession); + } + public addBreakpoints(breakpoints: Breakpoint[]): void { + return this.activeService.addBreakpoints(breakpoints); + } + public removeBreakpoints(breakpoints: Breakpoint[]): void { + return this.activeService.removeBreakpoints(breakpoints); + } + public getStack(): Promise<DebugProtocol.StackFrame[]> { + return this.activeService.getStack(); + } + public step(): Promise<void> { + return this.activeService.step(); + } + public continue(): Promise<void> { + return this.activeService.continue(); + } + public requestVariables(): Promise<void> { + return this.activeService.requestVariables(); + } + public stop(): void { + return this.activeService.stop(); + } + private get activeService(): IJupyterDebugService { + return this.jupyterDebugService; + } +} diff --git a/src/test/datascience/mockDocument.ts b/src/test/datascience/mockDocument.ts new file mode 100644 index 000000000000..f19264910e0c --- /dev/null +++ b/src/test/datascience/mockDocument.ts @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { EndOfLine, Position, Range, TextDocument, TextDocumentContentChangeEvent, TextLine, Uri } from 'vscode'; + +import { + DefaultWordPattern, + ensureValidWordDefinition, + getWordAtText, + regExpLeadsToEndlessLoop +} from '../../client/datascience/interactive-common/intellisense/wordHelper'; + +class MockLine implements TextLine { + private _range: Range; + private _rangeWithLineBreak: Range; + private _firstNonWhitespaceIndex: number | undefined; + private _isEmpty: boolean | undefined; + + constructor(private _contents: string, private _line: number, private _offset: number) { + this._range = new Range(new Position(_line, 0), new Position(_line, _contents.length)); + this._rangeWithLineBreak = new Range(this.range.start, new Position(_line, _contents.length + 1)); + } + + public get offset(): number { + return this._offset; + } + public get lineNumber(): number { + return this._line; + } + public get text(): string { + return this._contents; + } + public get range(): Range { + return this._range; + } + public get rangeIncludingLineBreak(): Range { + return this._rangeWithLineBreak; + } + public get firstNonWhitespaceCharacterIndex(): number { + if (this._firstNonWhitespaceIndex === undefined) { + this._firstNonWhitespaceIndex = this._contents.trimLeft().length - this._contents.length; + } + return this._firstNonWhitespaceIndex; + } + public get isEmptyOrWhitespace(): boolean { + if (this._isEmpty === undefined) { + this._isEmpty = this._contents.length === 0 || this._contents.trim().length === 0; + } + return this._isEmpty; + } +} + +export class MockDocument implements TextDocument { + private _uri: Uri; + private _version: number = 0; + private _lines: MockLine[] = []; + private _contents: string = ''; + private _isUntitled = false; + private _isDirty = false; + private _onSave: (doc: TextDocument) => Promise<boolean>; + + constructor(contents: string, fileName: string, onSave: (doc: TextDocument) => Promise<boolean>) { + this._uri = Uri.file(fileName); + this._contents = contents; + this._lines = this.createLines(); + this._onSave = onSave; + } + + public setContent(contents: string) { + this._contents = contents; + this._lines = this.createLines(); + } + + public addContent(contents: string) { + this.setContent(`${this._contents}\n${contents}`); + } + + public forceUntitled(): void { + this._isUntitled = true; + this._isDirty = true; + } + + public get uri(): Uri { + return this._uri; + } + public get fileName(): string { + return this._uri.fsPath; + } + + public get isUntitled(): boolean { + return this._isUntitled; + } + public get languageId(): string { + return 'python'; + } + public get version(): number { + return this._version; + } + public get isDirty(): boolean { + return this._isDirty; + } + public get isClosed(): boolean { + return false; + } + public save(): Thenable<boolean> { + return this._onSave(this); + } + public get eol(): EndOfLine { + return EndOfLine.LF; + } + public get lineCount(): number { + return this._lines.length; + } + public lineAt(position: Position | number): TextLine { + if (typeof position === 'number') { + return this._lines[position as number]; + } else { + return this._lines[position.line]; + } + } + public offsetAt(position: Position): number { + return this.convertToOffset(position); + } + public positionAt(offset: number): Position { + let line = 0; + let ch = 0; + while (line + 1 < this._lines.length && this._lines[line + 1].offset <= offset) { + line += 1; + } + if (line < this._lines.length) { + ch = offset - this._lines[line].offset; + } + return new Position(line, ch); + } + public getText(range?: Range | undefined): string { + if (!range) { + return this._contents; + } else { + const startOffset = this.convertToOffset(range.start); + const endOffset = this.convertToOffset(range.end); + return this._contents.substr(startOffset, endOffset - startOffset); + } + } + public getWordRangeAtPosition(position: Position, regexp?: RegExp | undefined): Range | undefined { + if (!regexp) { + // use default when custom-regexp isn't provided + regexp = DefaultWordPattern; + } else if (regExpLeadsToEndlessLoop(regexp)) { + // use default when custom-regexp is bad + console.warn( + `[getWordRangeAtPosition]: ignoring custom regexp '${regexp.source}' because it matches the empty string.` + ); + regexp = DefaultWordPattern; + } + + const wordAtText = getWordAtText( + position.character + 1, + ensureValidWordDefinition(regexp), + this._lines[position.line].text, + 0 + ); + + if (wordAtText) { + return new Range(position.line, wordAtText.startColumn - 1, position.line, wordAtText.endColumn - 1); + } + return undefined; + } + public validateRange(range: Range): Range { + return range; + } + public validatePosition(position: Position): Position { + return position; + } + + public edit(c: TextDocumentContentChangeEvent): void { + this._version += 1; + const before = this._contents.substr(0, c.rangeOffset); + const after = this._contents.substr(c.rangeOffset + c.rangeLength); + this._contents = `${before}${c.text}${after}`; + this._lines = this.createLines(); + } + + private createLines(): MockLine[] { + const split = this._contents.split('\n'); + let prevLine: MockLine | undefined; + return split.map((s, i) => { + const nextLine = this.createTextLine(s, i, prevLine); + prevLine = nextLine; + return nextLine; + }); + } + + private createTextLine(line: string, index: number, prevLine: MockLine | undefined): MockLine { + return new MockLine( + line, + index, + prevLine ? prevLine.offset + prevLine.rangeIncludingLineBreak.end.character : 0 + ); + } + + private convertToOffset(pos: Position): number { + if (pos.line < this._lines.length) { + return ( + this._lines[pos.line].offset + + Math.min(this._lines[pos.line].rangeIncludingLineBreak.end.character, pos.character) + ); + } + return this._contents.length; + } +} diff --git a/src/test/datascience/mockDocumentManager.ts b/src/test/datascience/mockDocumentManager.ts new file mode 100644 index 000000000000..20e711c1d9d4 --- /dev/null +++ b/src/test/datascience/mockDocumentManager.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as path from 'path'; +import { + DecorationRenderOptions, + Event, + EventEmitter, + Range, + TextDocument, + TextDocumentChangeEvent, + TextDocumentShowOptions, + TextEditor, + TextEditorDecorationType, + TextEditorOptionsChangeEvent, + TextEditorSelectionChangeEvent, + TextEditorViewColumnChangeEvent, + Uri, + ViewColumn, + WorkspaceEdit +} from 'vscode'; + +import { IDocumentManager } from '../../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../client/constants'; +import { MockDocument } from './mockDocument'; +import { MockEditor } from './mockTextEditor'; + +// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length + +export class MockDocumentManager implements IDocumentManager { + public textDocuments: TextDocument[] = []; + public activeTextEditor: TextEditor | undefined; + public visibleTextEditors: TextEditor[] = []; + public didChangeActiveTextEditorEmitter = new EventEmitter<TextEditor>(); + private didOpenEmitter = new EventEmitter<TextDocument>(); + private didChangeVisibleEmitter = new EventEmitter<TextEditor[]>(); + private didChangeTextEditorSelectionEmitter = new EventEmitter<TextEditorSelectionChangeEvent>(); + private didChangeTextEditorOptionsEmitter = new EventEmitter<TextEditorOptionsChangeEvent>(); + private didChangeTextEditorViewColumnEmitter = new EventEmitter<TextEditorViewColumnChangeEvent>(); + private didCloseEmitter = new EventEmitter<TextDocument>(); + private didSaveEmitter = new EventEmitter<TextDocument>(); + private didChangeTextDocumentEmitter = new EventEmitter<TextDocumentChangeEvent>(); + public get onDidChangeActiveTextEditor(): Event<TextEditor | undefined> { + return this.didChangeActiveTextEditorEmitter.event; + } + public get onDidChangeTextDocument(): Event<TextDocumentChangeEvent> { + return this.didChangeTextDocumentEmitter.event; + } + public get onDidOpenTextDocument(): Event<TextDocument> { + return this.didOpenEmitter.event; + } + public get onDidChangeVisibleTextEditors(): Event<TextEditor[]> { + return this.didChangeVisibleEmitter.event; + } + public get onDidChangeTextEditorSelection(): Event<TextEditorSelectionChangeEvent> { + return this.didChangeTextEditorSelectionEmitter.event; + } + public get onDidChangeTextEditorOptions(): Event<TextEditorOptionsChangeEvent> { + return this.didChangeTextEditorOptionsEmitter.event; + } + public get onDidChangeTextEditorViewColumn(): Event<TextEditorViewColumnChangeEvent> { + return this.didChangeTextEditorViewColumnEmitter.event; + } + public get onDidCloseTextDocument(): Event<TextDocument> { + return this.didCloseEmitter.event; + } + public get onDidSaveTextDocument(): Event<TextDocument> { + return this.didSaveEmitter.event; + } + public showTextDocument( + _document: TextDocument, + _column?: ViewColumn, + _preserveFocus?: boolean + ): Thenable<TextEditor>; + public showTextDocument(_document: TextDocument | Uri, _options?: TextDocumentShowOptions): Thenable<TextEditor>; + public showTextDocument(document: any, _column?: any, _preserveFocus?: any): Thenable<TextEditor> { + this.visibleTextEditors.push(document); + const mockEditor = new MockEditor(this, this.lastDocument as MockDocument); + this.activeTextEditor = mockEditor; + this.didChangeActiveTextEditorEmitter.fire(this.activeTextEditor); + return Promise.resolve(mockEditor); + } + public openTextDocument(_fileName: string | Uri): Thenable<TextDocument>; + public openTextDocument(_options?: { language?: string; content?: string }): Thenable<TextDocument>; + public openTextDocument(_options?: any): Thenable<TextDocument> { + if (_options && _options.content) { + const doc = new MockDocument(_options.content, 'Untitled-1', this.saveDocument); + this.textDocuments.push(doc); + } + return Promise.resolve(this.lastDocument); + } + public applyEdit(_edit: WorkspaceEdit): Thenable<boolean> { + throw new Error('Method not implemented.'); + } + + public addDocument(code: string, file: string) { + let existing = this.textDocuments.find((d) => d.uri.fsPath === file) as MockDocument; + if (existing) { + existing.setContent(code); + } else { + existing = new MockDocument(code, file, this.saveDocument); + this.textDocuments.push(existing); + } + return existing; + } + + public changeDocument(file: string, changes: { range: Range; newText: string }[]) { + const doc = this.textDocuments.find((d) => d.uri.fsPath === Uri.file(file).fsPath) as MockDocument; + if (doc) { + const contentChanges = changes.map((c) => { + const startOffset = doc.offsetAt(c.range.start); + const endOffset = doc.offsetAt(c.range.end); + return { + range: c.range, + rangeOffset: startOffset, + rangeLength: endOffset - startOffset, + text: c.newText + }; + }); + const ev: TextDocumentChangeEvent = { + document: doc, + contentChanges + }; + // Changes are applied to the doc before it's sent. + ev.contentChanges.forEach(doc.edit.bind(doc)); + this.didChangeTextDocumentEmitter.fire(ev); + } + } + + public createTextEditorDecorationType(_options: DecorationRenderOptions): TextEditorDecorationType { + throw new Error('Method not implemented'); + } + + private get lastDocument(): TextDocument { + if (this.textDocuments.length > 0) { + return this.textDocuments[this.textDocuments.length - 1]; + } + throw new Error('No documents in MockDocumentManager'); + } + + private saveDocument = (doc: TextDocument): Promise<boolean> => { + // Create a new document with the contents of the doc passed in + this.addDocument(doc.getText(), path.join(EXTENSION_ROOT_DIR, 'baz.py')); + return Promise.resolve(true); + }; +} diff --git a/src/test/datascience/mockExtensions.ts b/src/test/datascience/mockExtensions.ts new file mode 100644 index 000000000000..9b25c811fd7e --- /dev/null +++ b/src/test/datascience/mockExtensions.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { injectable } from 'inversify'; +import { Event, Extension, extensions } from 'vscode'; + +import { IExtensions } from '../../client/common/types'; + +// tslint:disable:no-any unified-signatures + +@injectable() +export class MockExtensions implements IExtensions { + public all: Extension<any>[] = []; + public getExtension<T>(_extensionId: string): Extension<T> | undefined { + return undefined; + } + + public get onDidChange(): Event<void> { + return extensions.onDidChange; + } +} diff --git a/src/test/datascience/mockFileSystem.ts b/src/test/datascience/mockFileSystem.ts new file mode 100644 index 000000000000..9d359b30890d --- /dev/null +++ b/src/test/datascience/mockFileSystem.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { Uri } from 'vscode'; +import { DataScienceFileSystem } from '../../client/datascience/dataScienceFileSystem'; +import { FakeVSCodeFileSystemAPI } from '../serviceRegistry'; + +export class MockFileSystem extends DataScienceFileSystem { + private contentOverloads = new Map<string, string>(); + + constructor() { + super(); + this.vscfs = new FakeVSCodeFileSystemAPI(); + } + public async readLocalFile(filePath: string): Promise<string> { + const contents = this.contentOverloads.get(filePath); + if (contents) { + return contents; + } + return super.readLocalFile(filePath); + } + public async readFile(filePath: Uri): Promise<string> { + const contents = this.contentOverloads.get(filePath.fsPath); + if (contents) { + return contents; + } + return super.readFile(filePath); + } + public addFileContents(filePath: string, contents: string): void { + this.contentOverloads.set(filePath, contents); + } +} diff --git a/src/test/datascience/mockInputBox.ts b/src/test/datascience/mockInputBox.ts new file mode 100644 index 000000000000..3523093ef62c --- /dev/null +++ b/src/test/datascience/mockInputBox.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Event, EventEmitter, InputBox, QuickInputButton } from 'vscode'; + +export class MockInputBox implements InputBox { + public value: string = ''; + public placeholder: string | undefined; + public password: boolean = false; + public buttons: QuickInputButton[] = []; + public prompt: string | undefined; + public validationMessage: string | undefined; + public title: string | undefined; + public step: number | undefined; + public totalSteps: number | undefined; + public enabled: boolean = true; + public busy: boolean = false; + public ignoreFocusOut: boolean = true; + private didChangeValueEmitter: EventEmitter<string> = new EventEmitter<string>(); + private didAcceptEmitter: EventEmitter<void> = new EventEmitter<void>(); + private didHideEmitter: EventEmitter<void> = new EventEmitter<void>(); + private didTriggerButtonEmitter: EventEmitter<QuickInputButton> = new EventEmitter<QuickInputButton>(); + private _value: string; + constructor(value: string) { + this._value = value; + } + public get onDidChangeValue(): Event<string> { + return this.didChangeValueEmitter.event; + } + public get onDidAccept(): Event<void> { + return this.didAcceptEmitter.event; + } + public get onDidTriggerButton(): Event<QuickInputButton> { + return this.didTriggerButtonEmitter.event; + } + public get onDidHide(): Event<void> { + return this.didHideEmitter.event; + } + public show(): void { + // After 10 ms set the value, then accept it + setTimeout(() => { + this.value = this._value; + this.didChangeValueEmitter.fire(this._value); + setTimeout(() => { + if (this.validationMessage) { + this.value = this.validationMessage; + this.didHideEmitter.fire(); + } else { + this.didAcceptEmitter.fire(); + } + }, 10); + }, 10); + } + public hide(): void { + // Do nothing + } + public dispose(): void { + // Do nothing + } +} diff --git a/src/test/datascience/mockJupyterManager.ts b/src/test/datascience/mockJupyterManager.ts new file mode 100644 index 000000000000..e690c6ef6a22 --- /dev/null +++ b/src/test/datascience/mockJupyterManager.ts @@ -0,0 +1,898 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { nbformat } from '@jupyterlab/coreutils'; +import { ChildProcess } from 'child_process'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import * as tmp from 'tmp'; +import * as TypeMoq from 'typemoq'; +import * as uuid from 'uuid/v4'; +import { EventEmitter, Uri } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; + +import type { Kernel, Session } from '@jupyterlab/services'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Cancellation } from '../../client/common/cancellation'; +import { ProductInstaller } from '../../client/common/installer/productInstaller'; +import { + ExecutionResult, + IProcessServiceFactory, + IPythonDaemonExecutionService, + IPythonExecutionFactory, + Output +} from '../../client/common/process/types'; +import { IConfigurationService, IInstaller, Product } from '../../client/common/types'; +import { EXTENSION_ROOT_DIR } from '../../client/constants'; +import { generateCells } from '../../client/datascience/cellFactory'; +import { CellMatcher } from '../../client/datascience/cellMatcher'; +import { CodeSnippets, Identifiers } from '../../client/datascience/constants'; +import { KernelConnectionMetadata } from '../../client/datascience/jupyter/kernels/types'; +import { + ICell, + IJupyterConnection, + IJupyterKernel, + IJupyterKernelSpec, + IJupyterSession, + IJupyterSessionManager +} from '../../client/datascience/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { IServiceManager } from '../../client/ioc/types'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { concatMultilineString } from '../../datascience-ui/common'; +import { noop, sleep } from '../core'; +import { MockJupyterSession } from './mockJupyterSession'; +import { MockProcessService } from './mockProcessService'; +import { MockPythonService } from './mockPythonService'; + +// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length + +const MockJupyterTimeDelay = 10; +const LineFeedRegEx = /(\r\n|\n)/g; + +export enum SupportedCommands { + none = 0, + ipykernel = 1, + nbconvert = 2, + notebook = 4, + kernelspec = 8, + all = 0xffff +} + +function createKernelSpecs(specs: { name: string; resourceDir: string }[]): Record<string, any> { + const models: Record<string, any> = {}; + specs.forEach((spec) => { + models[spec.name] = { + resource_dir: spec.resourceDir, + spec: { + name: spec.name, + display_name: spec.name, + language: 'python' + } + }; + }); + return models; +} + +// This class is used to mock talking to jupyter. It mocks +// the process services, the interpreter services, the python services, and the jupyter session +export class MockJupyterManager implements IJupyterSessionManager { + public readonly productInstaller: IInstaller; + private restartSessionCreatedEvent = new EventEmitter<Kernel.IKernelConnection>(); + private restartSessionUsedEvent = new EventEmitter<Kernel.IKernelConnection>(); + private pythonExecutionFactory = this.createTypeMoq<IPythonExecutionFactory>('Python Exec Factory'); + private processServiceFactory = this.createTypeMoq<IProcessServiceFactory>('Process Exec Factory'); + private processService: MockProcessService = new MockProcessService(); + private interpreterService = this.createTypeMoq<IInterpreterService>('Interpreter Service'); + private changedInterpreterEvent: EventEmitter<void> = new EventEmitter<void>(); + private installedInterpreters: PythonEnvironment[] = []; + private pythonServices: MockPythonService[] = []; + private activeInterpreter: PythonEnvironment | undefined; + private sessionTimeout: number | undefined; + private cellDictionary: Record<string, ICell> = {}; + private kernelSpecs: { name: string; dir: string }[] = []; + private currentSession: MockJupyterSession | undefined; + private connInfo: IJupyterConnection | undefined; + private cleanTemp: (() => void) | undefined; + private pendingSessionFailure = false; + private pendingKernelChangeFailure = false; + + constructor(serviceManager: IServiceManager) { + // Make our process service factory always return this item + this.processServiceFactory.setup((p) => p.create()).returns(() => Promise.resolve(this.processService)); + this.productInstaller = mock(ProductInstaller); + // Setup our interpreter service + this.interpreterService + .setup((i) => i.onDidChangeInterpreter) + .returns(() => this.changedInterpreterEvent.event); + this.interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(this.activeInterpreter)); + this.interpreterService + .setup((i) => i.getInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(this.installedInterpreters)); + this.interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAnyString())) + .returns((p) => { + const found = this.installedInterpreters.find((i) => i.path === p); + if (found) { + return Promise.resolve(found); + } + return Promise.reject('Unknown interpreter'); + }); + // Listen to configuration changes like the real interpreter service does so that we fire our settings changed event + const configService = serviceManager.get<IConfigurationService>(IConfigurationService); + if (configService && configService !== null) { + configService.getSettings(undefined).onDidChange(this.onConfigChanged.bind(this, configService)); + } + + // Stick our services into the service manager + serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, this.interpreterService.object); + serviceManager.addSingletonInstance<IPythonExecutionFactory>( + IPythonExecutionFactory, + this.pythonExecutionFactory.object + ); + serviceManager.addSingletonInstance<IProcessServiceFactory>( + IProcessServiceFactory, + this.processServiceFactory.object + ); + serviceManager.addSingletonInstance<IInstaller>(IInstaller, instance(this.productInstaller)); + + // Setup our default kernel spec (this is just a dummy value) + // tslint:disable-next-line:no-octal-literal + this.kernelSpecs.push({ + name: '0e8519db-0895-416c-96df-fa80131ecea0', + dir: 'C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0' + }); + + // Setup our default cells that happen for everything + this.addCell(CodeSnippets.MatplotLibInitSvg); + this.addCell(CodeSnippets.MatplotLibInitPng); + this.addCell(CodeSnippets.ConfigSvg); + this.addCell(CodeSnippets.ConfigPng); + this.addCell(CodeSnippets.UpdateCWDAndPath.format(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'))); + this.addCell( + CodeSnippets.UpdateCWDAndPath.format( + Uri.file(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience')).fsPath + ) + ); + tmp.file((_e, p, _fd, cleanup) => { + this.addCell(CodeSnippets.UpdateCWDAndPath.format(path.dirname(p))); + this.cleanTemp = cleanup; + }); + this.addCell(`import sys\r\nsys.path.append('undefined')\r\nsys.path`); + this.addCell(`import debugpy;debugpy.listen(('localhost', 0))`); + this.addCell("matplotlib.style.use('dark_background')"); + this.addCell(`matplotlib.rcParams.update(${Identifiers.MatplotLibDefaultParams})`); + this.addCell(`%cd "${path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience')}"`); + // When we have windows file names, we replace `\` with `\\`. + // Code is as follows `await this.notebook.execute(`__file__ = '${file.replace(/\\/g, '\\\\')}'`, file, line, uuid(), undefined, true); + // Found in src\client\datascience\interactive-common\interactiveBase.ts. + this.addCell(`%cd "${Uri.file(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience')).fsPath}`); + // New root dir should be in the temp folder. + tmp.file((_e, p, _fd, cleanup) => { + this.addCell(`%cd "${path.dirname(p).toLowerCase()}"`); + this.cleanTemp = cleanup; + }); + this.addCell('import sys\r\nsys.version', '1.1.1.1'); + this.addCell('import sys\r\nsys.executable', 'python'); + this.addCell('import notebook\r\nnotebook.version_info', '1.1.1.1'); + + this.addCell(`__file__ = '${Uri.file('foo.py').fsPath}'`); + this.addCell(`__file__ = '${Uri.file('bar.py').fsPath}'`); + this.addCell(`__file__ = '${Uri.file('foo').fsPath}'`); + this.addCell(`__file__ = '${Uri.file('test.py').fsPath}'`); + + // When we have windows file names, we replace `\` with `\\`. + // Code is as follows `await this.notebook.execute(`__file__ = '${file.replace(/\\/g, '\\\\')}'`, file, line, uuid(), undefined, true); + // Found in src\client\datascience\interactive-common\interactiveBase.ts. + this.addCell(`__file__ = '${Uri.file('foo.py').fsPath.replace(/\\/g, '\\\\')}'`); + this.addCell(`__file__ = '${Uri.file('bar.py').fsPath.replace(/\\/g, '\\\\')}'`); + this.addCell(`__file__ = '${Uri.file('foo').fsPath.replace(/\\/g, '\\\\')}'`); + this.addCell(`__file__ = '${Uri.file('test.py').fsPath.replace(/\\/g, '\\\\')}'`); + this.addCell('import os\nos.getcwd()', `'${path.join(EXTENSION_ROOT_DIR)}'`); + this.addCell('import sys\nsys.path[0]', `'${path.join(EXTENSION_ROOT_DIR)}'`); + + // Default cell used for a lot of tests. + this.addCell('a=1\na', 1); + + // Default used for variables + this.addCell('_rwho_ls = %who_ls\nprint(_rwho_ls)', ''); + } + + public get onRestartSessionCreated() { + return this.restartSessionCreatedEvent.event; + } + + public get onRestartSessionUsed() { + return this.restartSessionUsedEvent.event; + } + public getConnInfo(): IJupyterConnection { + return this.connInfo!; + } + + public makeActive(interpreter: PythonEnvironment) { + this.activeInterpreter = interpreter; + } + + public getCurrentSession(): MockJupyterSession | undefined { + return this.currentSession; + } + + public forcePendingIdleFailure() { + this.pendingSessionFailure = true; + } + + public forcePendingKernelChangeFailure() { + this.pendingKernelChangeFailure = true; + } + + public getRunningKernels(): Promise<IJupyterKernel[]> { + return Promise.resolve([]); + } + + public getRunningSessions(): Promise<Session.IModel[]> { + return Promise.resolve([]); + } + + public setProcessDelay(timeout: number | undefined) { + this.processService.setDelay(timeout); + this.pythonServices.forEach((p) => p.setDelay(timeout)); + } + + public addInterpreter( + interpreter: PythonEnvironment, + supportedCommands: SupportedCommands, + notebookStdErr?: string[], + notebookProc?: ChildProcess + ) { + this.installedInterpreters.push(interpreter); + + // Add the python calls first. + const pythonService = new MockPythonService(interpreter); + this.pythonServices.push(pythonService); + this.pythonExecutionFactory + .setup((f) => + f.create( + TypeMoq.It.is((o) => { + return o && o.pythonPath ? o.pythonPath === interpreter.path : false; + }) + ) + ) + .returns(() => Promise.resolve(pythonService)); + this.pythonExecutionFactory + .setup((f) => + f.createDaemon( + TypeMoq.It.is((o) => { + return o && o.pythonPath ? o.pythonPath === interpreter.path : false; + }) + ) + ) + .returns(() => Promise.resolve((pythonService as unknown) as IPythonDaemonExecutionService)); + this.pythonExecutionFactory + .setup((f) => + f.createActivatedEnvironment( + TypeMoq.It.is((o) => { + return !o || JSON.stringify(o.interpreter) === JSON.stringify(interpreter); + }) + ) + ) + .returns(() => Promise.resolve(pythonService)); + this.setupSupportedPythonService(pythonService, interpreter, supportedCommands, notebookStdErr, notebookProc); + + // Then the process calls + this.setupSupportedProcessService(interpreter, supportedCommands, notebookStdErr); + + // Default to being the new active + this.makeActive(interpreter); + } + + public addError(code: string, message: string) { + // Turn the message into an nbformat.IError + const result: nbformat.IError = { + output_type: 'error', + ename: message, + evalue: message, + traceback: [message] + }; + + this.addCell(code, result); + } + + public addContinuousOutputCell( + code: string, + resultGenerator: (cancelToken: CancellationToken) => Promise<{ result: string; haveMore: boolean }> + ) { + const cells = generateCells(undefined, code, Uri.file('foo.py').fsPath, 1, true, uuid()); + cells.forEach((c) => { + const key = concatMultilineString(c.data.source).replace(LineFeedRegEx, '').toLowerCase(); + if (c.data.cell_type === 'code') { + const taggedResult = { + output_type: 'generator' + }; + const data: nbformat.ICodeCell = c.data as nbformat.ICodeCell; + data.outputs = [...data.outputs, taggedResult]; + + // Tag on our extra data + (taggedResult as any).resultGenerator = async (t: CancellationToken) => { + const result = await resultGenerator(t); + return { + result: this.createStreamResult(result.result), + haveMore: result.haveMore + }; + }; + + // Save in the cell. + c.data = data; + } + + // Save each in our dictionary for future use. + // Note: Our entire setup is recreated each test so this dictionary + // should be unique per test + this.cellDictionary[key] = c; + }); + } + + public addInputCell( + code: string, + result?: + | undefined + | string + | number + | nbformat.IUnrecognizedOutput + | nbformat.IExecuteResult + | nbformat.IDisplayData + | nbformat.IStream + | nbformat.IError, + mimeType?: string + ) { + const cells = generateCells(undefined, code, Uri.file('foo.py').fsPath, 1, true, uuid()); + cells.forEach((c) => { + const key = concatMultilineString(c.data.source).replace(LineFeedRegEx, '').toLowerCase(); + if (c.data.cell_type === 'code') { + const taggedResult = { + output_type: 'input' + }; + const massagedResult = this.massageCellResult(result, mimeType); + const data: nbformat.ICodeCell = c.data as nbformat.ICodeCell; + if (result) { + data.outputs = [...data.outputs, taggedResult, massagedResult]; + } else { + data.outputs = [...data.outputs, taggedResult]; + } + // Save in the cell. + c.data = data; + } + + // Save each in our dictionary for future use. + // Note: Our entire setup is recreated each test so this dictionary + // should be unique per test + this.cellDictionary[key] = c; + }); + } + + public addCell( + code: string, + result?: + | undefined + | string + | number + | nbformat.IUnrecognizedOutput + | nbformat.IExecuteResult + | nbformat.IDisplayData + | nbformat.IStream + | nbformat.IError + | string[], + mimeType?: string | string[] + ) { + const cells = generateCells(undefined, code, Uri.file('foo.py').fsPath, 1, true, uuid()); + cells.forEach((c) => { + const cellMatcher = new CellMatcher(); + const key = cellMatcher + .stripFirstMarker(concatMultilineString(c.data.source)) + .replace(LineFeedRegEx, '') + .toLowerCase(); + if (c.data.cell_type === 'code') { + if (mimeType && Array.isArray(mimeType) && Array.isArray(result)) { + for (let i = 0; i < mimeType.length; i = i + 1) { + this.addCellOutput(c, result[i], mimeType[i]); + } + } else if (!Array.isArray(result) && !Array.isArray(mimeType)) { + this.addCellOutput(c, result, mimeType); + } + } + + // Save each in our dictionary for future use. + // Note: Our entire setup is recreated each test so this dictionary + // should be unique per test + this.cellDictionary[key] = c; + }); + } + + public setWaitTime(timeout: number | undefined) { + this.sessionTimeout = timeout; + } + + public async dispose(): Promise<void> { + if (this.cleanTemp) { + this.cleanTemp(); + } + } + + public async initialize(connInfo: IJupyterConnection): Promise<void> { + this.connInfo = connInfo; + } + + public startNew( + _kernelConnection: KernelConnectionMetadata | undefined, + _workingDirectory: string, + cancelToken?: CancellationToken + ): Promise<IJupyterSession> { + if (this.sessionTimeout && cancelToken) { + const localTimeout = this.sessionTimeout; + return Cancellation.race(async () => { + await sleep(localTimeout); + return this.createNewSession(); + }, cancelToken); + } else { + return Promise.resolve(this.createNewSession()); + } + } + + public getKernelSpecs(): Promise<IJupyterKernelSpec[]> { + return Promise.resolve([]); + } + + public changeWorkingDirectory(workingDir: string) { + this.addCell(CodeSnippets.UpdateCWDAndPath.format(workingDir)); + this.addCell('import os\nos.getcwd()', path.join(workingDir)); + this.addCell('import sys\nsys.path[0]', path.join(workingDir)); + } + + private addCellOutput( + cell: ICell, + result?: + | undefined + | string + | number + | nbformat.IUnrecognizedOutput + | nbformat.IExecuteResult + | nbformat.IDisplayData + | nbformat.IStream + | nbformat.IError, + mimeType?: string + ) { + const massagedResult = this.massageCellResult(result, mimeType); + const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; + if (result) { + data.outputs = [...data.outputs, massagedResult]; + } else { + data.outputs = [...data.outputs]; + } + cell.data = data; + } + + private onConfigChanged(configService: IConfigurationService) { + const pythonPath = configService.getSettings().pythonPath; + if (this.activeInterpreter === undefined || pythonPath !== this.activeInterpreter.path) { + this.activeInterpreter = this.installedInterpreters.filter((f) => f.path === pythonPath)[0]; + if (!this.activeInterpreter) { + this.activeInterpreter = this.installedInterpreters[0]; + } + this.changedInterpreterEvent.fire(); + } + } + + private createNewSession(): MockJupyterSession { + const sessionFailure = this.pendingSessionFailure; + const kernelChangeFailure = this.pendingKernelChangeFailure; + this.pendingSessionFailure = false; + this.pendingKernelChangeFailure = false; + this.currentSession = new MockJupyterSession( + this.cellDictionary, + MockJupyterTimeDelay, + sessionFailure, + kernelChangeFailure + ); + return this.currentSession; + } + + private createStreamResult(str: string): nbformat.IStream { + return { + output_type: 'stream', + name: 'stdout', + text: str + }; + } + + private massageCellResult( + result: + | undefined + | string + | number + | nbformat.IUnrecognizedOutput + | nbformat.IExecuteResult + | nbformat.IDisplayData + | nbformat.IStream + | nbformat.IError, + mimeType?: string + ): + | nbformat.IUnrecognizedOutput + | nbformat.IExecuteResult + | nbformat.IDisplayData + | nbformat.IStream + | nbformat.IError { + // See if undefined or string or number + if (!result) { + // This is an empty execute result + return { + output_type: 'execute_result', + execution_count: 1, + data: {}, + metadata: {} + }; + } else if (mimeType && mimeType === 'clear_true') { + return { + output_type: 'clear_true' + }; + } else if (mimeType && mimeType === 'stream') { + return { + output_type: 'stream', + text: result, + name: 'stdout' + }; + } else if (typeof result === 'string') { + const data = {}; + (data as any)[mimeType ? mimeType : 'text/plain'] = result; + return { + output_type: 'execute_result', + execution_count: 1, + data: data, + metadata: {} + }; + } else if (typeof result === 'number') { + return { + output_type: 'execute_result', + execution_count: 1, + data: { 'text/plain': result.toString() }, + metadata: {} + }; + } else { + return result; + } + } + + private createTempSpec(pythonPath: string): string { + const tempDir = os.tmpdir(); + const subDir = uuid(); + const filePath = path.join(tempDir, subDir, 'kernel.json'); + fs.ensureDirSync(path.dirname(filePath)); + fs.writeJSONSync(filePath, { + display_name: 'Python 3', + language: 'python', + argv: [pythonPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}'] + }); + return filePath; + } + + private createTypeMoq<T>(tag: string): TypeMoq.IMock<T> { + // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class + // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 + const result = TypeMoq.Mock.ofType<T>(); + (result as any).tag = tag; + result.setup((x: any) => x.then).returns(() => undefined); + return result; + } + + private setupPythonServiceExec( + service: MockPythonService, + module: string, + args: (string | RegExp)[], + result: () => Promise<ExecutionResult<string>> + ) { + service.addExecResult(['-m', module, ...args], result); + service.addExecModuleResult(module, args, result); + } + + private setupPythonServiceExecObservable( + service: MockPythonService, + module: string, + args: (string | RegExp)[], + stderr: string[], + stdout: string[], + proc?: ChildProcess + ) { + const result = { + proc, + out: new Observable<Output<string>>((subscriber) => { + stderr.forEach((s) => subscriber.next({ source: 'stderr', out: s })); + stdout.forEach((s) => subscriber.next({ source: 'stderr', out: s })); + }), + dispose: () => { + noop(); + } + }; + + service.addExecObservableResult(['-m', module, ...args], () => result); + service.addExecModuleObservableResult(module, args, () => result); + } + + private setupProcessServiceExec( + service: MockProcessService, + file: string, + args: (string | RegExp)[], + result: () => Promise<ExecutionResult<string>> + ) { + service.addExecResult(file, args, result); + } + + private setupProcessServiceExecObservable( + service: MockProcessService, + file: string, + args: (string | RegExp)[], + stderr: string[], + stdout: string[] + ) { + service.addExecObservableResult(file, args, () => { + return { + proc: undefined, + out: new Observable<Output<string>>((subscriber) => { + stderr.forEach((s) => subscriber.next({ source: 'stderr', out: s })); + stdout.forEach((s) => subscriber.next({ source: 'stderr', out: s })); + }), + dispose: () => { + noop(); + } + }; + }); + } + + private setupSupportedPythonService( + service: MockPythonService, + workingPython: PythonEnvironment, + supportedCommands: SupportedCommands, + notebookStdErr?: string[], + notebookProc?: ChildProcess + ) { + when(this.productInstaller.isInstalled(anything())).thenResolve(true); + when(this.productInstaller.isInstalled(anything(), anything())).thenResolve(true); + if ((supportedCommands & SupportedCommands.ipykernel) === SupportedCommands.ipykernel) { + this.setupPythonServiceExec(service, 'ipykernel', ['--version'], () => + Promise.resolve({ stdout: '1.1.1.1' }) + ); + this.setupPythonServiceExec( + service, + 'ipykernel', + ['install', '--user', '--name', /\w+-\w+-\w+-\w+-\w+/, '--display-name', `'Python Interactive'`], + () => { + const spec = this.addKernelSpec(workingPython.path); + return Promise.resolve({ stdout: `somename ${path.dirname(spec)}` }); + } + ); + } else { + when(this.productInstaller.isInstalled(Product.ipykernel)).thenResolve(false); + when(this.productInstaller.isInstalled(Product.ipykernel, anything())).thenResolve(false); + } + if ((supportedCommands & SupportedCommands.nbconvert) === SupportedCommands.nbconvert) { + this.setupPythonServiceExec(service, 'jupyter', ['nbconvert', '--version'], () => + Promise.resolve({ stdout: '1.1.1.1' }) + ); + this.setupPythonServiceExec( + service, + 'jupyter', + ['nbconvert', /.*/, '--to', 'python', '--stdout', '--template', /.*/], + () => { + return Promise.resolve({ + stdout: '#%%\r\nimport os\r\nos.chdir()\r\n#%%\r\na=1' + }); + } + ); + } else { + when(this.productInstaller.isInstalled(Product.nbconvert)).thenResolve(false); + when(this.productInstaller.isInstalled(Product.nbconvert, anything())).thenResolve(false); + } + if ((supportedCommands & SupportedCommands.notebook) === SupportedCommands.notebook) { + this.setupPythonServiceExec(service, 'jupyter', ['notebook', '--version'], () => + Promise.resolve({ stdout: '1.1.1.1' }) + ); + this.setupPythonServiceExecObservable( + service, + 'jupyter', + [ + 'notebook', + '--no-browser', + /--notebook-dir=.*/, + /.*/, + '--NotebookApp.iopub_data_rate_limit=10000000000.0' + ], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'], + notebookProc + ); + this.setupPythonServiceExecObservable( + service, + 'jupyter', + ['notebook', '--no-browser', /--notebook-dir=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'], + notebookProc + ); + } else { + when(this.productInstaller.isInstalled(Product.notebook)).thenResolve(false); + when(this.productInstaller.isInstalled(Product.notebook, anything())).thenResolve(false); + } + if ((supportedCommands & SupportedCommands.kernelspec) === SupportedCommands.kernelspec) { + this.setupPythonServiceExec(service, 'jupyter', ['kernelspec', '--version'], () => + Promise.resolve({ stdout: '1.1.1.1' }) + ); + this.setupPythonServiceExec(service, 'jupyter', ['kernelspec', 'list', '--json'], () => { + const kernels = this.kernelSpecs.map((k) => ({ name: k.name, resourceDir: k.dir })); + return Promise.resolve({ stdout: JSON.stringify(createKernelSpecs(kernels)) }); + }); + } else { + when(this.productInstaller.isInstalled(Product.kernelspec)).thenResolve(false); + when(this.productInstaller.isInstalled(Product.kernelspec, anything())).thenResolve(false); + } + } + + private addKernelSpec(pythonPath: string): string { + const spec = this.createTempSpec(pythonPath); + this.kernelSpecs.push({ name: `${this.kernelSpecs.length}Spec`, dir: `${path.dirname(spec)}` }); + return spec; + } + + private setupSupportedProcessService( + workingPython: PythonEnvironment, + supportedCommands: SupportedCommands, + notebookStdErr?: string[] + ) { + if ((supportedCommands & SupportedCommands.ipykernel) === SupportedCommands.ipykernel) { + // Don't mind the goofy path here. It's supposed to not find the item on your box. It's just testing the internal regex works + this.setupProcessServiceExec( + this.processService, + workingPython.path, + ['-m', 'jupyter', 'kernelspec', 'list', '--json'], + () => { + const kernels = this.kernelSpecs.map((k) => ({ name: k.name, resourceDir: k.dir })); + return Promise.resolve({ stdout: JSON.stringify(createKernelSpecs(kernels)) }); + } + ); + this.setupProcessServiceExec( + this.processService, + workingPython.path, + [ + '-m', + 'ipykernel', + 'install', + '--user', + '--name', + /\w+-\w+-\w+-\w+-\w+/, + '--display-name', + `'Python Interactive'` + ], + () => { + const spec = this.addKernelSpec(workingPython.path); + return Promise.resolve({ + stdout: JSON.stringify( + createKernelSpecs([{ name: 'somename', resourceDir: path.dirname(spec) }]) + ) + }); + } + ); + const getServerInfoPath = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'getServerInfo.py' + ); + this.setupProcessServiceExec(this.processService, workingPython.path, [getServerInfoPath], () => + Promise.resolve({ stdout: 'failure to get server infos' }) + ); + this.setupProcessServiceExecObservable( + this.processService, + workingPython.path, + ['-m', 'jupyter', 'kernelspec', 'list', '--json'], + [], + [] + ); + this.setupProcessServiceExecObservable( + this.processService, + workingPython.path, + [ + '-m', + 'jupyter', + 'notebook', + '--no-browser', + /--notebook-dir=.*/, + /.*/, + '--NotebookApp.iopub_data_rate_limit=10000000000.0' + ], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); + this.setupProcessServiceExecObservable( + this.processService, + workingPython.path, + [ + '-m', + 'jupyter', + 'notebook', + '--no-browser', + /--notebook-dir=.*/, + '--NotebookApp.iopub_data_rate_limit=10000000000.0' + ], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); + } else if ((supportedCommands & SupportedCommands.notebook) === SupportedCommands.notebook) { + this.setupProcessServiceExec( + this.processService, + workingPython.path, + ['-m', 'jupyter', 'kernelspec', 'list', '--json'], + () => { + const kernels = this.kernelSpecs.map((k) => ({ name: k.name, resourceDir: k.dir })); + return Promise.resolve({ stdout: JSON.stringify(createKernelSpecs(kernels)) }); + } + ); + const getServerInfoPath = path.join( + EXTENSION_ROOT_DIR, + 'pythonFiles', + 'vscode_datascience_helpers', + 'getServerInfo.py' + ); + this.setupProcessServiceExec(this.processService, workingPython.path, [getServerInfoPath], () => + Promise.resolve({ stdout: 'failure to get server infos' }) + ); + this.setupProcessServiceExecObservable( + this.processService, + workingPython.path, + ['-m', 'jupyter', 'kernelspec', 'list', '--json'], + [], + [] + ); + this.setupProcessServiceExecObservable( + this.processService, + workingPython.path, + [ + '-m', + 'jupyter', + 'notebook', + '--no-browser', + /--notebook-dir=.*/, + /.*/, + '--NotebookApp.iopub_data_rate_limit=10000000000.0' + ], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); + this.setupProcessServiceExecObservable( + this.processService, + workingPython.path, + [ + '-m', + 'jupyter', + 'notebook', + '--no-browser', + /--notebook-dir=.*/, + '--NotebookApp.iopub_data_rate_limit=10000000000.0' + ], + [], + notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'] + ); + } + if ((supportedCommands & SupportedCommands.nbconvert) === SupportedCommands.nbconvert) { + this.setupProcessServiceExec( + this.processService, + workingPython.path, + ['-m', 'jupyter', 'nbconvert', /.*/, '--to', 'python', '--stdout', '--template', /.*/], + () => { + return Promise.resolve({ + stdout: '#%%\r\nimport os\r\nos.chdir()' + }); + } + ); + } + } +} diff --git a/src/test/datascience/mockJupyterManagerFactory.ts b/src/test/datascience/mockJupyterManagerFactory.ts new file mode 100644 index 000000000000..fec36809d20f --- /dev/null +++ b/src/test/datascience/mockJupyterManagerFactory.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { + IJupyterConnection, + IJupyterSessionManager, + IJupyterSessionManagerFactory +} from '../../client/datascience/types'; +import { IServiceManager } from '../../client/ioc/types'; +import { MockJupyterManager } from './mockJupyterManager'; + +// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length + +// This class is used to mock talking to jupyter. It mocks +// the process services, the interpreter services, the python services, and the jupyter session +export class MockJupyterManagerFactory implements IJupyterSessionManagerFactory { + private mockJupyterManager: MockJupyterManager; + + constructor(serviceManager: IServiceManager) { + serviceManager.addSingletonInstance<IJupyterSessionManagerFactory>(IJupyterSessionManagerFactory, this); + this.mockJupyterManager = new MockJupyterManager(serviceManager); + } + + public create(_connInfo: IJupyterConnection, _failOnPassword?: boolean): Promise<IJupyterSessionManager> { + return Promise.resolve(this.mockJupyterManager); + } + + public getManager(): MockJupyterManager { + return this.mockJupyterManager; + } + + public get onRestartSessionCreated() { + return this.mockJupyterManager.onRestartSessionCreated; + } + + public get onRestartSessionUsed() { + return this.mockJupyterManager.onRestartSessionUsed; + } +} diff --git a/src/test/datascience/mockJupyterNotebook.ts b/src/test/datascience/mockJupyterNotebook.ts new file mode 100644 index 000000000000..3e98d4f6e86f --- /dev/null +++ b/src/test/datascience/mockJupyterNotebook.ts @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Kernel, KernelMessage } from '@jupyterlab/services'; +import { JSONObject } from '@phosphor/coreutils/lib/json'; +import { Observable } from 'rxjs/Observable'; +import { CancellationToken, Event, EventEmitter, Uri } from 'vscode'; +import { Resource } from '../../client/common/types'; +import { getDefaultInteractiveIdentity } from '../../client/datascience/interactive-window/identity'; +import { KernelConnectionMetadata } from '../../client/datascience/jupyter/kernels/types'; +import { + ICell, + ICellHashProvider, + IJupyterSession, + INotebook, + INotebookCompletion, + INotebookExecutionLogger, + INotebookProviderConnection, + InterruptResult, + KernelSocketInformation +} from '../../client/datascience/types'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { ServerStatus } from '../../datascience-ui/interactive-common/mainState'; +import { noop } from '../core'; + +export class MockJupyterNotebook implements INotebook { + public get connection(): INotebookProviderConnection | undefined { + return this.providerConnection; + } + public get identity(): Uri { + return getDefaultInteractiveIdentity(); + } + public get onSessionStatusChanged(): Event<ServerStatus> { + if (!this.onStatusChangedEvent) { + this.onStatusChangedEvent = new EventEmitter<ServerStatus>(); + } + return this.onStatusChangedEvent.event; + } + + public get status(): ServerStatus { + return ServerStatus.Idle; + } + public get session(): IJupyterSession { + throw new Error('Method not implemented'); + } + + public get resource(): Resource { + return Uri.file('foo.py'); + } + public get onKernelInterrupted(): Event<void> { + return this.kernelInterrupted.event; + } + public kernelSocket = new Observable<KernelSocketInformation | undefined>(); + public onKernelChanged = new EventEmitter<KernelConnectionMetadata>().event; + public onDisposed = new EventEmitter<void>().event; + public onKernelRestarted = new EventEmitter<void>().event; + public readonly disposed: boolean = false; + private kernelInterrupted = new EventEmitter<void>(); + private onStatusChangedEvent: EventEmitter<ServerStatus> | undefined; + + constructor(private providerConnection: INotebookProviderConnection | undefined) { + noop(); + } + public registerIOPubListener(_listener: (msg: KernelMessage.IIOPubMessage, requestId: string) => void): void { + noop(); + } + public getCellHashProvider(): ICellHashProvider | undefined { + throw new Error('Method not implemented.'); + } + + public clear(_id: string): void { + noop(); + } + public executeObservable(_code: string, _f: string, _line: number): Observable<ICell[]> { + throw new Error('Method not implemented'); + } + + public inspect(_code: string, _offsetInCode = 0, _cancelToken?: CancellationToken): Promise<JSONObject> { + return Promise.resolve({}); + } + + public async getCompletion( + _cellCode: string, + _offsetInCode: number, + _cancelToken?: CancellationToken + ): Promise<INotebookCompletion> { + throw new Error('Method not implemented'); + } + public execute(_code: string, _f: string, _line: number): Promise<ICell[]> { + throw new Error('Method not implemented'); + } + public restartKernel(): Promise<void> { + throw new Error('Method not implemented'); + } + public translateToNotebook(_cells: ICell[]): Promise<JSONObject> { + throw new Error('Method not implemented'); + } + public waitForIdle(): Promise<void> { + throw new Error('Method not implemented'); + } + public setLaunchingFile(_file: string): Promise<void> { + throw new Error('Method not implemented'); + } + + public async setMatplotLibStyle(_useDark: boolean): Promise<void> { + noop(); + } + + public addLogger(_logger: INotebookExecutionLogger): void { + noop(); + } + + public getSysInfo(): Promise<ICell | undefined> { + return Promise.resolve(undefined); + } + + public interruptKernel(_timeout: number): Promise<InterruptResult> { + throw new Error('Method not implemented'); + } + + public async dispose(): Promise<void> { + if (this.onStatusChangedEvent) { + this.onStatusChangedEvent.dispose(); + } + return Promise.resolve(); + } + + public getMatchingInterpreter(): PythonEnvironment | undefined { + return; + } + + public setInterpreter(_inter: PythonEnvironment) { + noop(); + } + + public getKernelConnection(): KernelConnectionMetadata | undefined { + return; + } + + public setKernelConnection(_spec: KernelConnectionMetadata, _timeout: number): Promise<void> { + return Promise.resolve(); + } + + public getLoggers(): INotebookExecutionLogger[] { + return []; + } + + public registerCommTarget( + _targetName: string, + _callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike<void> + ) { + noop(); + } + + public sendCommMessage( + buffers: (ArrayBuffer | ArrayBufferView)[], + content: { comm_id: string; data: JSONObject; target_name: string | undefined }, + // tslint:disable-next-line: no-any + metadata: any, + // tslint:disable-next-line: no-any + msgId: any + ): Kernel.IShellFuture< + KernelMessage.IShellMessage<'comm_msg'>, + KernelMessage.IShellMessage<KernelMessage.ShellMessageType> + > { + const shellMessage = KernelMessage.createMessage<KernelMessage.ICommMsgMsg<'shell'>>({ + // tslint:disable-next-line: no-any + msgType: 'comm_msg', + channel: 'shell', + buffers, + content, + metadata, + msgId, + session: '1', + username: '1' + }); + + return { + done: Promise.resolve(undefined), + msg: shellMessage, + onReply: noop, + onIOPub: noop, + onStdin: noop, + registerMessageHook: noop, + removeMessageHook: noop, + sendInputReply: noop, + isDisposed: false, + dispose: noop + }; + } + + public requestCommInfo( + _content: KernelMessage.ICommInfoRequestMsg['content'] + ): Promise<KernelMessage.ICommInfoReplyMsg> { + const shellMessage = KernelMessage.createMessage<KernelMessage.ICommInfoReplyMsg>({ + msgType: 'comm_info_reply', + channel: 'shell', + content: { + status: 'ok' + // tslint:disable-next-line: no-any + } as any, + metadata: {}, + session: '1', + username: '1' + }); + + return Promise.resolve(shellMessage); + } + public registerMessageHook( + _msgId: string, + _hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean> + ): void { + noop(); + } + public removeMessageHook( + _msgId: string, + _hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean> + ): void { + noop(); + } +} diff --git a/src/test/datascience/mockJupyterRequest.ts b/src/test/datascience/mockJupyterRequest.ts new file mode 100644 index 000000000000..392a6fa6799b --- /dev/null +++ b/src/test/datascience/mockJupyterRequest.ts @@ -0,0 +1,351 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { nbformat } from '@jupyterlab/coreutils'; +import { Kernel, KernelMessage } from '@jupyterlab/services'; +import { CancellationToken } from 'vscode-jsonrpc'; + +import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { noop } from '../../client/common/utils/misc'; +import { ICell } from '../../client/datascience/types'; +import { concatMultilineString } from '../../datascience-ui/common'; + +//tslint:disable:no-any +interface IMessageResult { + message: KernelMessage.IIOPubMessage | KernelMessage.IInputRequestMsg | KernelMessage.IMessage; + haveMore: boolean; +} + +interface IMessageProducer { + produceNextMessage(): Promise<IMessageResult>; + receiveInput(value: string): void; +} + +class SimpleMessageProducer implements IMessageProducer { + private type: KernelMessage.IOPubMessageType | KernelMessage.ShellMessageType; + private result: any; + private channel: string = 'iopub'; + + constructor( + type: KernelMessage.IOPubMessageType | KernelMessage.ShellMessageType, + result: any, + channel: string = 'iopub' + ) { + this.type = type; + this.result = result; + this.channel = channel; + } + + public produceNextMessage(): Promise<IMessageResult> { + return new Promise<IMessageResult>((resolve, _reject) => { + const message = + this.channel === 'iopub' + ? this.generateIOPubMessage(this.type as KernelMessage.IOPubMessageType, this.result) + : this.generateShellMessage(this.type as KernelMessage.ShellMessageType, this.result); + resolve({ message: message, haveMore: false }); + }); + } + + public receiveInput(_value: string): void { + noop(); + } + + protected generateIOPubMessage(msgType: KernelMessage.IOPubMessageType, result: any): KernelMessage.IIOPubMessage { + return { + channel: 'iopub', + header: { + username: 'foo', + version: '1.1', + session: '1111111111', + msg_id: '1.1', + msg_type: msgType, + date: '' + }, + parent_header: {}, + metadata: {}, + content: result + }; + } + + protected generateShellMessage( + msgType: KernelMessage.ShellMessageType, + result: any + ): KernelMessage.IShellControlMessage { + return { + channel: 'shell', + header: { + username: 'foo', + version: '1.1', + session: '1111111111', + msg_id: '1.1', + msg_type: msgType, + date: '' + }, + parent_header: {}, + metadata: {}, + content: result + }; + } + + protected generateInputMessage(): KernelMessage.IInputRequestMsg { + return { + channel: 'stdin', + header: { + username: 'foo', + version: '1.1', + session: '1111111111', + msg_id: '1.1', + msg_type: 'stdin' as any, + date: '' + }, + parent_header: {}, + metadata: {}, + content: { + prompt: 'Type Something', + password: false + } + }; + } + + protected generateClearMessage(wait: boolean): KernelMessage.IClearOutputMsg { + return { + channel: 'iopub', + header: { + username: 'foo', + version: '1.1', + session: '1111111111', + msg_id: '1.1', + msg_type: 'clear_output', + date: '' + }, + parent_header: {}, + metadata: {}, + content: { + wait + } + }; + } +} + +class OutputMessageProducer extends SimpleMessageProducer { + private output: nbformat.IOutput; + private cancelToken: CancellationToken; + private waitingForInput: Deferred<string> | undefined; + + constructor(output: nbformat.IOutput, cancelToken: CancellationToken) { + super(output.output_type as KernelMessage.IOPubMessageType, output); + this.output = output; + this.cancelToken = cancelToken; + } + + public async produceNextMessage(): Promise<IMessageResult> { + // Special case the 'generator' cell that returns a function + // to generate output. + if (this.output.output_type === 'generator') { + const resultEntry = <any>this.output.resultGenerator; + const resultGenerator = resultEntry as ( + t: CancellationToken + ) => Promise<{ result: nbformat.IStream; haveMore: boolean }>; + if (resultGenerator) { + const streamResult = await resultGenerator(this.cancelToken); + return { + message: this.generateIOPubMessage(streamResult.result.output_type, streamResult.result), + haveMore: streamResult.haveMore + }; + } + } else if (this.output.output_type === 'input') { + if (this.waitingForInput) { + await this.waitingForInput.promise; + this.waitingForInput = undefined; + return { + message: this.generateDummyMessage(), + haveMore: false + }; + } else { + this.waitingForInput = createDeferred<string>(); + return { + message: this.generateInputMessage(), + haveMore: this.waitingForInput !== undefined + }; + } + } else if (this.output.output_type === 'clear_true') { + // Generate a clear message + return { + message: this.generateClearMessage(true), + haveMore: false + }; + } + + return super.produceNextMessage(); + } + + public receiveInput(value: string) { + if (this.waitingForInput) { + this.waitingForInput.resolve(value); + } + } + + private generateDummyMessage(): KernelMessage.IMessage { + return { + channel: 'shell', + header: { + username: 'foo', + version: '1.1', + session: '1111111111', + msg_id: '1.1', + msg_type: 'stdin' as any + }, + parent_header: {}, + metadata: {}, + content: {} + } as any; + } +} + +// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length +export class MockJupyterRequest implements Kernel.IFuture<any, any> { + public msg: KernelMessage.IShellMessage; + public onReply: (msg: KernelMessage.IShellMessage) => void | PromiseLike<void>; + public onStdin: (msg: KernelMessage.IStdinMessage) => void | PromiseLike<void>; + public onIOPub: (msg: KernelMessage.IIOPubMessage) => void | PromiseLike<void>; + public isDisposed: boolean = false; + + private deferred: Deferred<KernelMessage.IShellMessage> = createDeferred<KernelMessage.IShellMessage>(); + private executionCount: number; + private cell: ICell; + private cancelToken: CancellationToken; + private currentProducer: IMessageProducer | undefined; + + constructor(cell: ICell, delay: number, executionCount: number, cancelToken: CancellationToken) { + // Save our execution count, this is like our id + this.executionCount = executionCount; + this.cell = cell; + this.cancelToken = cancelToken; + + // Because the base type was implemented without undefined on unset items, we + // need to set all items for hygiene to work. + this.msg = { + channel: 'shell', + header: { + username: 'foo', + version: '1.1', + session: '1111111111', + msg_id: '1.1', + msg_type: ('shell' as any) as KernelMessage.ShellMessageType, + date: '' + }, + parent_header: {}, + metadata: {}, + content: {} + }; + this.onIOPub = noop; + this.onReply = noop; + this.onStdin = noop; + + // Start our sequence of events that is our cell running + this.executeRequest(delay); + } + + public get done(): Promise<KernelMessage.IShellMessage> { + return this.deferred.promise; + } + public registerMessageHook(_hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void { + noop(); + } + public removeMessageHook(_hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void { + noop(); + } + public sendInputReply(content: KernelMessage.IInputReply): void { + if (this.currentProducer) { + this.currentProducer.receiveInput(content.value); + } + } + public dispose(): void { + if (!this.isDisposed) { + this.isDisposed = true; + } + } + + private executeRequest(delay: number) { + // The order of messages should be: + // 1 - Status busy + // 2 - Execute input + // 3 - N - Results/output + // N + 1 - Status idle + + // Create message producers for output first. + const outputs = this.cell.data.outputs as nbformat.IOutput[]; + const outputProducers = outputs.map( + (o) => new OutputMessageProducer({ ...o, execution_count: this.executionCount }, this.cancelToken) + ); + + // Then combine those into an array of producers for the rest of the messages + const producers = [ + new SimpleMessageProducer('status', { execution_state: 'busy' }), + new SimpleMessageProducer('execute_input', { + code: concatMultilineString(this.cell.data.source), + execution_count: this.executionCount + }), + ...outputProducers, + new SimpleMessageProducer('status', { execution_state: 'idle' }) + ]; + + // Then send these until we're done + this.sendMessages(producers, delay); + } + + private sendMessages(producers: IMessageProducer[], delay: number) { + if (producers && producers.length > 0) { + // We have another producer, after a delay produce the next + // message + const producer = producers[0]; + this.currentProducer = producer; + setTimeout(() => { + // Produce the next message + producer + .produceNextMessage() + .then((r) => { + // If there's a message, send it. + if (r.message && r.message.channel === 'iopub' && this.onIOPub) { + this.onIOPub(r.message as KernelMessage.IIOPubMessage); + } else if (r.message && r.message.channel === 'stdin' && this.onStdin) { + this.onStdin(r.message as KernelMessage.IStdinMessage); + } + + // Move onto the next producer if allowed + if (!this.cancelToken.isCancellationRequested) { + if (r.haveMore) { + this.sendMessages(producers, delay); + } else { + this.sendMessages(producers.slice(1), delay); + } + } + }) + .ignoreErrors(); + }, delay); + } else { + this.currentProducer = undefined; + // No more messages, send the execute reply message + const replyProducer = new SimpleMessageProducer( + 'execute_reply', + { execution_count: this.executionCount }, + 'shell' + ); + replyProducer + .produceNextMessage() + .then((r) => { + this.onReply((<any>r.message) as KernelMessage.IShellMessage); + }) + .ignoreErrors(); + + // Then the done message + const shellProducer = new SimpleMessageProducer('done' as any, { status: 'success' }, 'shell'); + shellProducer + .produceNextMessage() + .then((r) => { + this.deferred.resolve((<any>r.message) as KernelMessage.IShellMessage); + }) + .ignoreErrors(); + } + } +} diff --git a/src/test/datascience/mockJupyterServer.ts b/src/test/datascience/mockJupyterServer.ts new file mode 100644 index 000000000000..3a83f3e82847 --- /dev/null +++ b/src/test/datascience/mockJupyterServer.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as uuid from 'uuid/v4'; +import { Uri } from 'vscode'; +import { TemporaryFile } from '../../client/common/platform/types'; +import { noop } from '../../client/common/utils/misc'; +import { getNameOfKernelConnection } from '../../client/datascience/jupyter/kernels/helpers'; +import { + IJupyterConnection, + INotebook, + INotebookServer, + INotebookServerLaunchInfo +} from '../../client/datascience/types'; +import { MockJupyterNotebook } from './mockJupyterNotebook'; + +export class MockJupyterServer implements INotebookServer { + private launchInfo: INotebookServerLaunchInfo | undefined; + private notebookFile: TemporaryFile | undefined; + private _id = uuid(); + + public get id(): string { + return this._id; + } + public connect(launchInfo: INotebookServerLaunchInfo): Promise<void> { + if (launchInfo && launchInfo.connectionInfo && launchInfo.kernelConnectionMetadata) { + this.launchInfo = launchInfo; + + // Validate connection info and kernel spec + const name = getNameOfKernelConnection(launchInfo.kernelConnectionMetadata); + if (launchInfo.connectionInfo.baseUrl && name && /[a-z,A-Z,0-9,-,.,_]+/.test(name)) { + return Promise.resolve(); + } + } + return Promise.reject('invalid server startup'); + } + + public async createNotebook(_resource: Uri): Promise<INotebook> { + return new MockJupyterNotebook(this.getConnectionInfo()); + } + + public async getNotebook(_resource: Uri): Promise<INotebook | undefined> { + return new MockJupyterNotebook(this.getConnectionInfo()); + } + + public async setMatplotLibStyle(_useDark: boolean): Promise<void> { + noop(); + } + public getConnectionInfo(): IJupyterConnection | undefined { + return this.launchInfo ? this.launchInfo.connectionInfo : undefined; + } + public waitForConnect(): Promise<INotebookServerLaunchInfo | undefined> { + throw new Error('Method not implemented'); + } + public async shutdown() { + return Promise.resolve(); + } + + public async dispose(): Promise<void> { + if (this.launchInfo) { + this.launchInfo.connectionInfo.dispose(); // This should kill the process that's running + this.launchInfo = undefined; + } + if (this.notebookFile) { + this.notebookFile.dispose(); // This destroy any unwanted kernel specs if necessary + this.notebookFile = undefined; + } + } +} diff --git a/src/test/datascience/mockJupyterSession.ts b/src/test/datascience/mockJupyterSession.ts new file mode 100644 index 000000000000..886158338dce --- /dev/null +++ b/src/test/datascience/mockJupyterSession.ts @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Kernel, KernelMessage } from '@jupyterlab/services'; +import { JSONObject } from '@phosphor/coreutils/lib/json'; +import { CancellationTokenSource, Event, EventEmitter } from 'vscode'; + +import { Observable } from 'rxjs/Observable'; +import { noop } from '../../client/common/utils/misc'; +import { JupyterInvalidKernelError } from '../../client/datascience/jupyter/jupyterInvalidKernelError'; +import { JupyterWaitForIdleError } from '../../client/datascience/jupyter/jupyterWaitForIdleError'; +import { JupyterKernelPromiseFailedError } from '../../client/datascience/jupyter/kernels/jupyterKernelPromiseFailedError'; +import { KernelConnectionMetadata } from '../../client/datascience/jupyter/kernels/types'; +import { ICell, IJupyterSession, KernelSocketInformation } from '../../client/datascience/types'; +import { ServerStatus } from '../../datascience-ui/interactive-common/mainState'; +import { sleep } from '../core'; +import { MockJupyterRequest } from './mockJupyterRequest'; + +const LineFeedRegEx = /(\r\n|\n)/g; + +// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length +export class MockJupyterSession implements IJupyterSession { + public readonly workingDirectory = ''; + public readonly kernelSocket = new Observable<KernelSocketInformation | undefined>(); + private dict: Record<string, ICell>; + private restartedEvent: EventEmitter<void> = new EventEmitter<void>(); + private onStatusChangedEvent: EventEmitter<ServerStatus> = new EventEmitter<ServerStatus>(); + private onIoPubMessageEvent: EventEmitter<KernelMessage.IIOPubMessage> = new EventEmitter< + KernelMessage.IIOPubMessage + >(); + private timedelay: number; + private executionCount: number = 0; + private outstandingRequestTokenSources: CancellationTokenSource[] = []; + private executes: string[] = []; + private forceRestartTimeout: boolean = false; + private completionTimeout: number = 1; + private lastRequest: MockJupyterRequest | undefined; + private _status = ServerStatus.Busy; + constructor( + cellDictionary: Record<string, ICell>, + timedelay: number, + private pendingIdleFailure: boolean = false, + private pendingKernelChangeFailure: boolean = false + ) { + this.dict = cellDictionary; + this.timedelay = timedelay; + // Switch to idle after a timeout + setTimeout(() => this.changeStatus(ServerStatus.Idle), 100); + } + + public get onRestarted(): Event<void> { + return this.restartedEvent.event; + } + + public get onSessionStatusChanged(): Event<ServerStatus> { + if (!this.onStatusChangedEvent) { + this.onStatusChangedEvent = new EventEmitter<ServerStatus>(); + } + return this.onStatusChangedEvent.event; + } + public get onIoPubMessage(): Event<KernelMessage.IIOPubMessage> { + return this.onIoPubMessageEvent.event; + } + + public get status(): ServerStatus { + return this._status; + } + + public async restart(_timeout: number): Promise<void> { + // For every outstanding request, switch them to fail mode + const requests = [...this.outstandingRequestTokenSources]; + requests.forEach((r) => r.cancel()); + + if (this.forceRestartTimeout) { + throw new JupyterKernelPromiseFailedError('Forcing restart timeout'); + } + + return sleep(this.timedelay); + } + public interrupt(_timeout: number): Promise<void> { + const requests = [...this.outstandingRequestTokenSources]; + requests.forEach((r) => r.cancel()); + return sleep(this.timedelay); + } + public waitForIdle(_timeout: number): Promise<void> { + if (this.pendingIdleFailure) { + this.pendingIdleFailure = false; + return Promise.reject(new JupyterWaitForIdleError('Kernel is dead')); + } + return sleep(this.timedelay); + } + + public prolongRestarts() { + this.forceRestartTimeout = true; + } + public requestExecute( + content: KernelMessage.IExecuteRequestMsg['content'], + _disposeOnDone?: boolean, + _metadata?: JSONObject + ): Kernel.IFuture<any, any> { + // Content should have the code + const cell = this.findCell(content.code); + if (cell) { + this.executes.push(content.code); + } + + // Create a new dummy request + this.executionCount += content.store_history && content.code.trim().length > 0 ? 1 : 0; + const tokenSource = new CancellationTokenSource(); + const request = new MockJupyterRequest(cell, this.timedelay, this.executionCount, tokenSource.token); + this.outstandingRequestTokenSources.push(tokenSource); + + // When it finishes, it should not be an outstanding request anymore + const removeHandler = () => { + this.outstandingRequestTokenSources = this.outstandingRequestTokenSources.filter((f) => f !== tokenSource); + if (this.lastRequest === request) { + this.lastRequest = undefined; + } + }; + request.done.then(removeHandler).catch(removeHandler); + this.lastRequest = request; + return request; + } + + public requestInspect( + _content: KernelMessage.IInspectRequestMsg['content'] + ): Promise<KernelMessage.IInspectReplyMsg> { + return Promise.resolve({ + content: { + status: 'ok', + metadata: {}, + found: true, + data: {} // Could add variable values here? + }, + channel: 'shell', + header: { + date: 'foo', + version: '1', + session: '1', + msg_id: '1', + msg_type: 'inspect_reply', + username: 'foo' + }, + parent_header: { + date: 'foo', + version: '1', + session: '1', + msg_id: '1', + msg_type: 'inspect_request', + username: 'foo' + }, + metadata: {} + }); + } + + public sendInputReply(content: string) { + if (this.lastRequest) { + this.lastRequest.sendInputReply({ value: content, status: 'ok' }); + } + } + + public async requestComplete( + _content: KernelMessage.ICompleteRequestMsg['content'] + ): Promise<KernelMessage.ICompleteReplyMsg | undefined> { + await sleep(this.completionTimeout); + + return { + content: { + matches: ['printly', '%%bash'], // This keeps this in the intellisense when the editor pairs down results + cursor_start: 0, + cursor_end: 7, + status: 'ok', + metadata: {} + }, + channel: 'shell', + header: { + username: 'foo', + version: '1', + session: '1', + msg_id: '1', + msg_type: 'complete' as any, + date: '' + }, + parent_header: {}, + metadata: {} + } as any; + } + + public dispose(): Promise<void> { + return sleep(10); + } + + public getExecutes(): string[] { + return this.executes; + } + + public setCompletionTimeout(timeout: number) { + this.completionTimeout = timeout; + } + + public changeKernel(kernelConnection: KernelConnectionMetadata, _timeoutMS: number): Promise<void> { + if (this.pendingKernelChangeFailure) { + this.pendingKernelChangeFailure = false; + return Promise.reject(new JupyterInvalidKernelError(kernelConnection)); + } + return Promise.resolve(); + } + + public registerCommTarget( + _targetName: string, + _callback: (comm: Kernel.IComm, msg: KernelMessage.ICommOpenMsg) => void | PromiseLike<void> + ) { + noop(); + } + + public sendCommMessage( + buffers: (ArrayBuffer | ArrayBufferView)[], + content: { comm_id: string; data: JSONObject; target_name: string | undefined }, + // tslint:disable-next-line: no-any + metadata: any, + // tslint:disable-next-line: no-any + msgId: any + ): Kernel.IShellFuture< + KernelMessage.IShellMessage<'comm_msg'>, + KernelMessage.IShellMessage<KernelMessage.ShellMessageType> + > { + const shellMessage = KernelMessage.createMessage<KernelMessage.ICommMsgMsg<'shell'>>({ + // tslint:disable-next-line: no-any + msgType: 'comm_msg', + channel: 'shell', + buffers, + content, + metadata, + msgId, + session: '1', + username: '1' + }); + + return { + done: Promise.resolve(undefined), + msg: shellMessage, + onReply: noop, + onIOPub: noop, + onStdin: noop, + registerMessageHook: noop, + removeMessageHook: noop, + sendInputReply: noop, + isDisposed: false, + dispose: noop + }; + } + + public requestCommInfo( + _content: KernelMessage.ICommInfoRequestMsg['content'] + ): Promise<KernelMessage.ICommInfoReplyMsg> { + const shellMessage = KernelMessage.createMessage<KernelMessage.ICommInfoReplyMsg>({ + msgType: 'comm_info_reply', + channel: 'shell', + content: { + status: 'ok' + // tslint:disable-next-line: no-any + } as any, + metadata: {}, + session: '1', + username: '1' + }); + + return Promise.resolve(shellMessage); + } + public registerMessageHook( + _msgId: string, + _hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean> + ): void { + noop(); + } + public removeMessageHook( + _msgId: string, + _hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean> + ): void { + noop(); + } + + private changeStatus(newStatus: ServerStatus) { + this._status = newStatus; + this.onStatusChangedEvent.fire(newStatus); + } + + private findCell = (code: string): ICell => { + // Match skipping line separators + const withoutLines = code.replace(LineFeedRegEx, '').toLowerCase(); + + if (this.dict.hasOwnProperty(withoutLines)) { + return this.dict[withoutLines] as ICell; + } + // tslint:disable-next-line:no-console + console.log(`Cell '${code}' not found in mock`); + // tslint:disable-next-line:no-console + console.log(`Dict has these keys ${Object.keys(this.dict).join('","')}`); + throw new Error(`Cell '${code}' not found in mock`); + }; +} diff --git a/src/test/datascience/mockKernelFinder.ts b/src/test/datascience/mockKernelFinder.ts new file mode 100644 index 000000000000..c3c33f6f018e --- /dev/null +++ b/src/test/datascience/mockKernelFinder.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import type { nbformat } from '@jupyterlab/coreutils'; +import { InterpreterUri } from '../../client/common/installer/types'; +import { IKernelFinder } from '../../client/datascience/kernel-launcher/types'; +import { IJupyterKernelSpec } from '../../client/datascience/types'; + +export class MockKernelFinder implements IKernelFinder { + private dummySpecs = new Map<string, IJupyterKernelSpec>(); + + constructor(private readonly realFinder: IKernelFinder) {} + + public async findKernelSpec( + interpreterUri: InterpreterUri, + kernelSpecMetadata?: nbformat.IKernelspecMetadata + ): Promise<IJupyterKernelSpec | undefined> { + const spec = interpreterUri?.path + ? this.dummySpecs.get(interpreterUri.path) + : this.dummySpecs.get((interpreterUri || '').toString()); + if (spec) { + return spec; + } + return this.realFinder.findKernelSpec(interpreterUri, kernelSpecMetadata); + } + + public async listKernelSpecs(): Promise<IJupyterKernelSpec[]> { + throw new Error('Not yet implemented'); + } + + public addKernelSpec(pythonPathOrResource: string, spec: IJupyterKernelSpec) { + this.dummySpecs.set(pythonPathOrResource, spec); + } +} diff --git a/src/test/datascience/mockLanguageClient.ts b/src/test/datascience/mockLanguageClient.ts new file mode 100644 index 000000000000..4b0df44df306 --- /dev/null +++ b/src/test/datascience/mockLanguageClient.ts @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { CancellationToken, DiagnosticCollection, Disposable, Event, Hover, OutputChannel } from 'vscode'; +import { + Code2ProtocolConverter, + CompletionItem, + DynamicFeature, + ErrorHandler, + GenericNotificationHandler, + GenericRequestHandler, + InitializeResult, + LanguageClient, + LanguageClientOptions, + MessageSignature, + MessageTransports, + NotificationHandler, + NotificationHandler0, + NotificationType, + NotificationType0, + Position, + Protocol2CodeConverter, + Range, + RequestHandler, + RequestHandler0, + RequestType, + RequestType0, + ServerOptions, + StateChangeEvent, + StaticFeature, + TextDocumentContentChangeEvent, + TextDocumentItem, + TextDocumentSyncKind, + Trace, + VersionedTextDocumentIdentifier +} from 'vscode-languageclient/node'; + +import { LanguageServerType } from '../../client/activation/types'; +import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { IntellisenseLine } from '../../client/datascience/interactive-common/intellisense/intellisenseLine'; +import { noop } from '../core'; +import { MockCode2ProtocolConverter } from './mockCode2ProtocolConverter'; +import { MockProtocol2CodeConverter } from './mockProtocol2CodeConverter'; + +// tslint:disable:no-any unified-signatures +export class MockLanguageClient extends LanguageClient { + private notificationPromise: Deferred<void> | undefined; + private contents: string; + private versionId: number | null; + private code2Protocol: MockCode2ProtocolConverter; + private protocol2Code: MockProtocol2CodeConverter; + private initResult: InitializeResult; + + public constructor( + name: string, + serverOptions: ServerOptions, + clientOptions: LanguageClientOptions, + forceDebug?: boolean + ) { + (LanguageClient.prototype as any).checkVersion = noop; + super(name, serverOptions, clientOptions, forceDebug); + this.contents = ''; + this.versionId = 0; + this.code2Protocol = new MockCode2ProtocolConverter(); + this.protocol2Code = new MockProtocol2CodeConverter(); + + // Vary our initialize result based on the name + if (name === LanguageServerType.Microsoft) { + this.initResult = { + capabilities: { + textDocumentSync: TextDocumentSyncKind.Incremental + } + }; + } else { + this.initResult = { + capabilities: { + textDocumentSync: TextDocumentSyncKind.Full + } + }; + } + } + public waitForNotification(): Promise<void> { + this.notificationPromise = createDeferred(); + return this.notificationPromise.promise; + } + + // Returns the current contents of the document being built by the completion provider calls + public getDocumentContents(): string { + return this.contents; + } + + public getVersionId(): number | null { + return this.versionId; + } + + public stop(): Promise<void> { + throw new Error('Method not implemented.'); + } + public registerProposedFeatures(): void { + throw new Error('Method not implemented.'); + } + public get initializeResult(): InitializeResult | undefined { + return this.initResult; + } + public sendRequest<R, E, RO>(type: RequestType0<R, E, RO>, token?: CancellationToken): Promise<R>; + public sendRequest<P, R, E, RO>(type: RequestType<P, R, E, RO>, params: P, token?: CancellationToken): Promise<R>; + public sendRequest<R>(method: string, token?: CancellationToken): Promise<R>; + public sendRequest<R>(method: string, param: any, token?: CancellationToken): Promise<R>; + public sendRequest(_method: any, _param?: any, _token?: any): Promise<any> { + switch (_method.method) { + case 'textDocument/completion': + // Just return one for each line of our contents + return Promise.resolve(this.getDocumentCompletions()); + + case 'textDocument/hover': + // Just return a simple hover + return Promise.resolve(this.getHover()); + default: + break; + } + return Promise.resolve(); + } + public onRequest<R, E, RO>(type: RequestType0<R, E, RO>, handler: RequestHandler0<R, E>): void; + public onRequest<P, R, E, RO>(type: RequestType<P, R, E, RO>, handler: RequestHandler<P, R, E>): void; + public onRequest<R, E>(method: string, handler: GenericRequestHandler<R, E>): void; + public onRequest(_method: any, _handler: any) { + throw new Error('Method not implemented.'); + } + public sendNotification<RO>(type: NotificationType0<RO>): void; + public sendNotification<P, RO>(type: NotificationType<P, RO>, params?: P | undefined): void; + public sendNotification(method: string): void; + public sendNotification(method: string, params: any): void; + public sendNotification(method: any, params?: any) { + switch (method.method) { + case 'textDocument/didOpen': + const item = params.textDocument as TextDocumentItem; + if (item) { + this.contents = item.text; + this.versionId = item.version; + } + break; + + case 'textDocument/didChange': + const id = params.textDocument as VersionedTextDocumentIdentifier; + const changes = params.contentChanges as TextDocumentContentChangeEvent[]; + if (id && changes) { + this.applyChanges(changes); + this.versionId = id.version; + } + break; + + default: + if (this.notificationPromise) { + this.notificationPromise.reject(new Error(`Unknown notification ${method.method}`)); + } + break; + } + if (this.notificationPromise && !this.notificationPromise.resolved) { + this.notificationPromise.resolve(); + } + } + public onNotification<RO>(type: NotificationType0<RO>, handler: NotificationHandler0): void; + public onNotification<P, RO>(type: NotificationType<P, RO>, handler: NotificationHandler<P>): void; + public onNotification(method: string, handler: GenericNotificationHandler): void; + public onNotification(_method: any, _handler: any) { + throw new Error('Method not implemented.'); + } + public get clientOptions(): LanguageClientOptions { + throw new Error('Method not implemented.'); + } + public get protocol2CodeConverter(): Protocol2CodeConverter { + return this.protocol2Code; + } + public get code2ProtocolConverter(): Code2ProtocolConverter { + return this.code2Protocol; + } + public get onTelemetry(): Event<any> { + throw new Error('Method not implemented.'); + } + public get onDidChangeState(): Event<StateChangeEvent> { + throw new Error('Method not implemented.'); + } + public get outputChannel(): OutputChannel { + throw new Error('Method not implemented.'); + } + public get diagnostics(): DiagnosticCollection | undefined { + throw new Error('Method not implemented.'); + } + public createDefaultErrorHandler(): ErrorHandler { + throw new Error('Method not implemented.'); + } + public get trace(): Trace { + throw new Error('Method not implemented.'); + } + public info(_message: string, _data?: any): void { + throw new Error('Method not implemented.'); + } + public warn(_message: string, _data?: any): void { + throw new Error('Method not implemented.'); + } + public error(_message: string, _data?: any): void { + throw new Error('Method not implemented.'); + } + public needsStart(): boolean { + throw new Error('Method not implemented.'); + } + public needsStop(): boolean { + throw new Error('Method not implemented.'); + } + public onReady(): Promise<void> { + throw new Error('Method not implemented.'); + } + public start(): Disposable { + throw new Error('Method not implemented.'); + } + public registerFeatures(_features: (StaticFeature | DynamicFeature<any>)[]): void { + throw new Error('Method not implemented.'); + } + public registerFeature(_feature: StaticFeature | DynamicFeature<any>): void { + throw new Error('Method not implemented.'); + } + public handleFailedRequest<T>(_type: MessageSignature, _error: any, _defaultValue: T): T { + throw new Error('Method not implemented.'); + } + protected handleConnectionClosed(): void { + throw new Error('Method not implemented.'); + } + protected createMessageTransports(_encoding: string): Promise<MessageTransports> { + throw new Error('Method not implemented.'); + } + protected registerBuiltinFeatures(): void { + noop(); + } + + private applyChanges(changes: TextDocumentContentChangeEvent[]) { + if (this.initResult.capabilities.textDocumentSync === TextDocumentSyncKind.Incremental) { + changes.forEach((change: TextDocumentContentChangeEvent) => { + const c = change as { range: Range; rangeLength?: number; text: string }; + if (c.range) { + const offset = c.range ? this.getOffset(c.range.start) : 0; + const before = this.contents.substr(0, offset); + const after = c.rangeLength ? this.contents.substr(offset + c.rangeLength) : ''; + this.contents = `${before}${c.text}${after}`; + } + }); + } else { + changes.forEach((c: TextDocumentContentChangeEvent) => { + this.contents = c.text; + }); + } + } + + private getDocumentCompletions(): CompletionItem[] { + const lines = this.contents.splitLines(); + return lines.map((l) => { + return { + label: l, + insertText: l, + sortText: l + }; + }); + } + + private getHover(): Hover { + return { + contents: [this.contents] + }; + } + + private createLines(): IntellisenseLine[] { + const split = this.contents.splitLines({ trim: false, removeEmptyEntries: false }); + let prevLine: IntellisenseLine | undefined; + return split.map((s, i) => { + const nextLine = this.createTextLine(s, i, prevLine); + prevLine = nextLine; + return nextLine; + }); + } + + private createTextLine(line: string, index: number, prevLine: IntellisenseLine | undefined): IntellisenseLine { + return new IntellisenseLine( + line, + index, + prevLine ? prevLine.offset + prevLine.rangeIncludingLineBreak.end.character : 0 + ); + } + + private getOffset(position: Position): number { + const lines = this.createLines(); + if (position.line >= 0 && position.line < lines.length) { + return lines[position.line].offset + position.character; + } + return 0; + } +} diff --git a/src/test/datascience/mockLanguageServer.ts b/src/test/datascience/mockLanguageServer.ts new file mode 100644 index 000000000000..9f7f29086519 --- /dev/null +++ b/src/test/datascience/mockLanguageServer.ts @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { + CancellationToken, + CodeLens, + CompletionContext, + CompletionItem, + CompletionList, + DocumentSymbol, + Hover, + Location, + LocationLink, + Position, + ProviderResult, + ReferenceContext, + SignatureHelp, + SignatureHelpContext, + SymbolInformation, + TextDocument, + TextDocumentContentChangeEvent, + WorkspaceEdit +} from 'vscode'; + +import { ILanguageServer } from '../../client/activation/types'; +import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { noop } from '../../client/common/utils/misc'; + +// tslint:disable:no-any unified-signatures +export class MockLanguageServer implements ILanguageServer { + private notificationPromise: Deferred<void> | undefined; + private contents = ''; + private versionId: number = 0; + + public waitForNotification(): Promise<void> { + this.notificationPromise = createDeferred(); + return this.notificationPromise.promise; + } + + public getDocumentContents(): string { + return this.contents; + } + + public getVersionId(): number | null { + return this.versionId; + } + + public handleChanges(document: TextDocument, changes: TextDocumentContentChangeEvent[]) { + this.versionId = document.version; + this.applyChanges(changes); + this.resolveNotificationPromise(); + } + + public handleOpen(_document: TextDocument) { + noop(); + } + + public provideRenameEdits( + _document: TextDocument, + _position: Position, + _newName: string, + _token: CancellationToken + ): ProviderResult<WorkspaceEdit> { + this.resolveNotificationPromise(); + return null; + } + public provideDefinition( + _document: TextDocument, + _position: Position, + _token: CancellationToken + ): ProviderResult<Location | Location[] | LocationLink[]> { + this.resolveNotificationPromise(); + return null; + } + public provideHover( + _document: TextDocument, + _position: Position, + _token: CancellationToken + ): ProviderResult<Hover> { + this.resolveNotificationPromise(); + return null; + } + public provideReferences( + _document: TextDocument, + _position: Position, + _context: ReferenceContext, + _token: CancellationToken + ): ProviderResult<Location[]> { + this.resolveNotificationPromise(); + return null; + } + public provideCompletionItems( + _document: TextDocument, + _position: Position, + _token: CancellationToken, + _context: CompletionContext + ): ProviderResult<CompletionItem[] | CompletionList> { + this.resolveNotificationPromise(); + return null; + } + public provideCodeLenses(_document: TextDocument, _token: CancellationToken): ProviderResult<CodeLens[]> { + this.resolveNotificationPromise(); + return null; + } + public provideDocumentSymbols( + _document: TextDocument, + _token: CancellationToken + ): ProviderResult<SymbolInformation[] | DocumentSymbol[]> { + this.resolveNotificationPromise(); + return null; + } + public provideSignatureHelp( + _document: TextDocument, + _position: Position, + _token: CancellationToken, + _context: SignatureHelpContext + ): ProviderResult<SignatureHelp> { + this.resolveNotificationPromise(); + return null; + } + public dispose(): void { + noop(); + } + + public disconnect(): void { + noop(); + } + + public reconnect(): void { + noop(); + } + + private applyChanges(changes: TextDocumentContentChangeEvent[]) { + changes.forEach((c) => { + const before = this.contents.substr(0, c.rangeOffset); + const after = this.contents.substr(c.rangeOffset + c.rangeLength); + this.contents = `${before}${c.text}${after}`; + }); + this.versionId = this.versionId + 1; + } + + private resolveNotificationPromise() { + if (this.notificationPromise) { + this.notificationPromise.resolve(); + this.notificationPromise = undefined; + } + } +} diff --git a/src/test/datascience/mockLanguageServerAnalysisOptions.ts b/src/test/datascience/mockLanguageServerAnalysisOptions.ts new file mode 100644 index 000000000000..f030728d8ab8 --- /dev/null +++ b/src/test/datascience/mockLanguageServerAnalysisOptions.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { injectable } from 'inversify'; +import { Event, EventEmitter } from 'vscode'; +import { LanguageClientOptions } from 'vscode-languageclient/node'; + +import { ILanguageServerAnalysisOptions } from '../../client/activation/types'; +import { Resource } from '../../client/common/types'; +import { noop } from '../core'; + +// tslint:disable:no-any unified-signatures +@injectable() +export class MockLanguageServerAnalysisOptions implements ILanguageServerAnalysisOptions { + private onDidChangeEmitter: EventEmitter<void> = new EventEmitter<void>(); + + public get onDidChange(): Event<void> { + return this.onDidChangeEmitter.event; + } + + public initialize(_resource: Resource): Promise<void> { + return Promise.resolve(); + } + public getAnalysisOptions(): Promise<LanguageClientOptions> { + return Promise.resolve({}); + } + public dispose(): void | undefined { + noop(); + } +} diff --git a/src/test/datascience/mockLanguageServerCache.ts b/src/test/datascience/mockLanguageServerCache.ts new file mode 100644 index 000000000000..a3a5bba3761c --- /dev/null +++ b/src/test/datascience/mockLanguageServerCache.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { injectable } from 'inversify'; +import { Uri } from 'vscode'; + +import { ILanguageServer, ILanguageServerCache } from '../../client/activation/types'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { MockLanguageServer } from './mockLanguageServer'; + +// tslint:disable:no-any unified-signatures +@injectable() +export class MockLanguageServerCache implements ILanguageServerCache { + private mockLanguageServer = new MockLanguageServer(); + + public get(_resource: Uri | undefined, _interpreter?: PythonEnvironment | undefined): Promise<ILanguageServer> { + return Promise.resolve(this.mockLanguageServer); + } + + public getMockServer(): MockLanguageServer { + return this.mockLanguageServer; + } +} diff --git a/src/test/datascience/mockLanguageServerProxy.ts b/src/test/datascience/mockLanguageServerProxy.ts new file mode 100644 index 000000000000..dbb4fadf5b27 --- /dev/null +++ b/src/test/datascience/mockLanguageServerProxy.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; + +import { ILanguageServerProxy } from '../../client/activation/types'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { MockLanguageClient } from './mockLanguageClient'; + +// tslint:disable:no-any unified-signatures +@injectable() +export class MockLanguageServerProxy implements ILanguageServerProxy { + private mockLanguageClient: MockLanguageClient | undefined; + + public get languageClient(): LanguageClient | undefined { + if (!this.mockLanguageClient) { + this.mockLanguageClient = new MockLanguageClient('mockLanguageClient', { module: 'dummy' }, {}); + } + return this.mockLanguageClient; + } + + public start( + _resource: Uri | undefined, + _interpreter: PythonEnvironment | undefined, + _options: LanguageClientOptions + ): Promise<void> { + if (!this.mockLanguageClient) { + this.mockLanguageClient = new MockLanguageClient('mockLanguageClient', { module: 'dummy' }, {}); + } + return Promise.resolve(); + } + public loadExtension(_args?: {} | undefined): void { + throw new Error('Method not implemented.'); + } + public dispose(): void | undefined { + this.mockLanguageClient = undefined; + } +} diff --git a/src/test/datascience/mockLiveShare.ts b/src/test/datascience/mockLiveShare.ts new file mode 100644 index 000000000000..aacf6b4546e7 --- /dev/null +++ b/src/test/datascience/mockLiveShare.ts @@ -0,0 +1,446 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import * as uuid from 'uuid/v4'; +import { + CancellationToken, + CancellationTokenSource, + Disposable, + Event, + EventEmitter, + TreeDataProvider, + Uri +} from 'vscode'; +import * as vsls from 'vsls/vscode'; + +import { IApplicationShell, ILiveShareTestingApi } from '../../client/common/application/types'; +import { IConfigurationService, IDisposable, IDisposableRegistry } from '../../client/common/types'; +import { noop } from '../../client/common/utils/misc'; +import { LiveShare } from '../../client/datascience/constants'; +import { LiveShareProxy } from '../../client/datascience/liveshare/liveshareProxy'; + +// tslint:disable:no-any unified-signatures max-classes-per-file + +class MockLiveService implements vsls.SharedService, vsls.SharedServiceProxy { + public isServiceAvailable: boolean = true; + private changeIsServiceAvailableEmitter: EventEmitter<boolean> = new EventEmitter<boolean>(); + private requestHandlers: Map<string, vsls.RequestHandler> = new Map<string, vsls.RequestHandler>(); + private notifyHandlers: Map<string, vsls.NotifyHandler> = new Map<string, vsls.NotifyHandler>(); + private defaultCancellationSource = new CancellationTokenSource(); + private sibling: MockLiveService | undefined; + + constructor(public readonly role: vsls.Role) {} + + public setSibling(sibling: MockLiveService) { + this.sibling = sibling; + } + + public get onDidChangeIsServiceAvailable(): Event<boolean> { + return this.changeIsServiceAvailableEmitter.event; + } + public request(name: string, args: any[], cancellation?: CancellationToken): Promise<any> { + // See if any handlers. + const handler = this.sibling ? this.sibling.requestHandlers.get(name) : undefined; + if (handler) { + return handler(args, cancellation ? cancellation : this.defaultCancellationSource.token); + } + return Promise.resolve(); + } + public onRequest(name: string, handler: vsls.RequestHandler): void { + this.requestHandlers.set(name, handler); + } + public onNotify(name: string, handler: vsls.NotifyHandler): void { + this.notifyHandlers.set(name, handler); + } + public notify(name: string, args: object): void { + // See if any handlers. + const handler = this.sibling ? this.sibling.notifyHandlers.get(name) : undefined; + if (handler) { + handler(args); + } + } + + public clearHandlers(): void { + this.requestHandlers.clear(); + this.notifyHandlers.clear(); + } +} + +type ArgumentType = 'boolean' | 'number' | 'string' | 'object' | 'function' | 'array' | 'uri'; + +function checkArg(value: any, name: string, type?: ArgumentType) { + if (!value) { + throw new Error(`Argument \'${name}\' is required.`); + } else if (type) { + if (type === 'array') { + if (!Array.isArray(value)) { + throw new Error(`Argument \'${name}\' must be an array.`); + } + } else if (type === 'uri') { + if (!(value instanceof Uri)) { + throw new Error(`Argument \'${name}\' must be a Uri object.`); + } + } else if (type === 'object' && Array.isArray(value)) { + throw new Error(`Argument \'${name}\' must be a a non-array object.`); + } else if (typeof value !== type) { + throw new Error(`Argument \'${name}\' must be type \'' + type + '\'.`); + } + } +} + +type Listener = [Function, any] | Function; + +class Emitter<T> { + private _event: Event<T> | undefined; + private _disposed: boolean = false; + private _deliveryQueue: { listener: Listener; event?: T }[] = []; + private _listeners: Listener[] = []; + + get event(): Event<T> { + if (!this._event) { + this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]) => { + this._listeners.push(!thisArgs ? listener : [listener, thisArgs]); + let result: IDisposable; + result = { + dispose: () => { + result.dispose = noop; + if (!this._disposed) { + this._listeners = this._listeners.filter((l) => l !== listener); + } + } + }; + if (Array.isArray(disposables)) { + disposables.push(result); + } + + return result; + }; + } + return this._event; + } + + public async fire(event?: T): Promise<void> { + if (this._listeners) { + // put all [listener,event]-pairs into delivery queue + // then emit all event. an inner/nested event might be + // the driver of this + + if (!this._deliveryQueue) { + this._deliveryQueue = []; + } + + for (const l of this._listeners) { + this._deliveryQueue.push({ listener: l, event }); + } + + while (this._deliveryQueue.length > 0) { + const item = this._deliveryQueue.shift(); + let result: any; + try { + if (item && item.listener) { + if (typeof item.listener === 'function') { + result = item.listener.call(undefined, item.event); + } else { + const func = item.listener[0]; + if (func) { + result = func.call(item.listener[1], item.event); + } + } + } + } catch (e) { + // Do nothinga + } + if (result) { + const promise = result as Promise<void>; + if (promise) { + await promise; + } + } + } + } + } + + public dispose() { + if (this._listeners) { + this._listeners = []; + } + if (this._deliveryQueue) { + this._deliveryQueue = []; + } + this._disposed = true; + } +} + +class MockLiveShare implements vsls.LiveShare, vsls.Session, vsls.Peer, IDisposable { + private static others: MockLiveShare[] = []; + private static services: Map<string, MockLiveService[]> = new Map<string, MockLiveService[]>(); + private changeSessionEmitter = new Emitter<vsls.SessionChangeEvent>(); + private changePeersEmitter = new EventEmitter<vsls.PeersChangeEvent>(); + private currentPeers: vsls.Peer[] = []; + private _id = uuid(); + private _peerNumber = 0; + private _visibleRole = vsls.Role.None; + constructor(private _role: vsls.Role) { + this._peerNumber = _role === vsls.Role.Host ? 0 : 1; + MockLiveShare.others.push(this); + } + + public onPeerConnected(peer: MockLiveShare) { + if (peer.role !== this.role) { + this.currentPeers.push(peer); + this.changePeersEmitter.fire({ added: [peer], removed: [] }); + } + } + + public dispose() { + MockLiveShare.others = MockLiveShare.others.filter((o) => o._id !== this._id); + } + + public get session(): vsls.Session { + return this; + } + + public async start(): Promise<void> { + this._visibleRole = this._role; + + // Special case, we need to wait for the fire to finish. This means + // the real product can have a race condition between starting the session and registering commands? + // Nope, because the guest side can't do anything until the session starts up. + await this.changeSessionEmitter.fire({ session: this }); + if (this._role === vsls.Role.Guest) { + for (const o of MockLiveShare.others) { + if (o._id !== this._id) { + o.onPeerConnected(this); + } + } + } + } + + public async stop(): Promise<void> { + this._visibleRole = vsls.Role.None; + const existingPeers = this.currentPeers; + this.currentPeers = []; + this.changePeersEmitter.fire({ added: [], removed: existingPeers }); + await this.changeSessionEmitter.fire({ session: this }); + } + + public removeHandlers(serviceName: string) { + const services = MockLiveShare.services.get(serviceName); + if (!services) { + throw new Error(`${serviceName} failure to add service to map`); + } + + // Remove just the one corresponding to the role of this api + if (this.role === vsls.Role.Guest) { + services[1].clearHandlers(); + } else { + services[0].clearHandlers(); + } + } + + public getContacts(_emails: string[]): Promise<vsls.ContactsCollection> { + throw new Error('Method not implemented.'); + } + + public get role(): vsls.Role { + return this._visibleRole; + } + public get id(): string { + return this._id; + } + public get peerNumber(): number { + return this._peerNumber; + } + public get user(): vsls.UserInfo { + return { + displayName: 'Test', + emailAddress: 'Test@Microsoft.Com', + userName: 'Test', + id: '0' + }; + } + public get access(): vsls.Access { + return vsls.Access.None; + } + + public get onDidChangeSession(): Event<vsls.SessionChangeEvent> { + return this.changeSessionEmitter.event; + } + public get peers(): vsls.Peer[] { + return this.currentPeers; + } + public get onDidChangePeers(): Event<vsls.PeersChangeEvent> { + return this.changePeersEmitter.event; + } + public share(_options?: vsls.ShareOptions): Promise<Uri> { + throw new Error('Method not implemented.'); + } + public join(_link: Uri, _options?: vsls.JoinOptions): Promise<void> { + throw new Error('Method not implemented.'); + } + public async end(): Promise<void> { + // If we're the guest, just stop ourselves. If we're the host, stop everybody + if (this._role === vsls.Role.Guest) { + await this.stop(); + } else { + await Promise.all(MockLiveShare.others.map((p) => p.stop())); + } + } + public shareService(name: string): Promise<vsls.SharedService> { + if (!MockLiveShare.services.has(name)) { + MockLiveShare.services.set(name, this.generateServicePair()); + } + const services = MockLiveShare.services.get(name); + if (!services) { + throw new Error(`${name} failure to add service to map`); + } + + // Host is always the first + return Promise.resolve(services[0]); + } + public unshareService(name: string): Promise<void> { + MockLiveShare.services.delete(name); + return Promise.resolve(); + } + public getSharedService(name: string): Promise<vsls.SharedServiceProxy> { + if (!MockLiveShare.services.has(name)) { + // Don't wait for the host to start. It shouldn't be necessary anyway. + MockLiveShare.services.set(name, this.generateServicePair()); + } + const services = MockLiveShare.services.get(name); + if (!services) { + throw new Error(`${name} failure to add service to map`); + } + + // Guest is always the second one + return Promise.resolve(services[1]); + } + public convertLocalUriToShared(localUri: Uri): Uri { + // Do the same checking that liveshare does + checkArg(localUri, 'localUri', 'uri'); + + if (this.session.role !== vsls.Role.Host) { + throw new Error('Only the host role can convert shared URIs.'); + } + + const scheme = 'vsls'; + if (localUri.scheme === scheme) { + throw new Error(`URI is already a ${scheme} URI: ${localUri}`); + } + + if (localUri.scheme !== 'file') { + throw new Error(`Not a workspace file URI: ${localUri}`); + } + + return Uri.parse(`vsls:${localUri.fsPath}`); + } + public convertSharedUriToLocal(sharedUri: Uri): Uri { + checkArg(sharedUri, 'sharedUri', 'uri'); + + if (this.session.role !== vsls.Role.Host) { + throw new Error('Only the host role can convert shared URIs.'); + } + + const scheme = 'vsls'; + if (sharedUri.scheme !== scheme) { + throw new Error(`Not a shared URI: ${sharedUri}`); + } + + return Uri.file(sharedUri.fsPath); + } + public registerCommand(_command: string, _isEnabled?: () => boolean, _thisArg?: any): Disposable { + throw new Error('Method not implemented.'); + } + public registerTreeDataProvider<T>(_viewId: vsls.View, _treeDataProvider: TreeDataProvider<T>): Disposable { + throw new Error('Method not implemented.'); + } + public registerContactServiceProvider( + _name: string, + _contactServiceProvider: vsls.ContactServiceProvider + ): Disposable { + throw new Error('Method not implemented.'); + } + public shareServer(_server: vsls.Server): Promise<Disposable> { + // Ignore for now. We don't need to port forward during a test + return Promise.resolve({ dispose: noop }); + } + + private generateServicePair(): MockLiveService[] { + const hostService = new MockLiveService(vsls.Role.Host); + const guestService = new MockLiveService(vsls.Role.Guest); + hostService.setSibling(guestService); + guestService.setSibling(hostService); + // Host is always first + return [hostService, guestService]; + } +} + +@injectable() +export class MockLiveShareApi implements ILiveShareTestingApi { + private currentRole: vsls.Role = vsls.Role.None; + private internalApi: MockLiveShare | null = null; + private externalProxy: vsls.LiveShare | null = null; + private sessionStarted = false; + + constructor( + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, + @inject(IApplicationShell) private appShell: IApplicationShell, + @inject(IConfigurationService) private config: IConfigurationService + ) {} + + public getApi(): Promise<vsls.LiveShare | null> { + return Promise.resolve(this.externalProxy); + } + + public forceRole(role: vsls.Role) { + // Force a role on our live share api + if (role !== this.currentRole) { + this.internalApi = new MockLiveShare(role); + this.externalProxy = new LiveShareProxy( + this.appShell, + this.config.getSettings().datascience.liveShareConnectionTimeout, + this.internalApi + ); + this.internalApi.onDidChangeSession(this.onInternalSessionChanged, this); + this.currentRole = role; + this.disposables.push(this.internalApi); + } + } + + public async startSession(): Promise<void> { + if (this.internalApi) { + await this.internalApi.start(); + this.sessionStarted = true; + } else { + throw Error('Cannot start session without a role.'); + } + } + + public async stopSession(): Promise<void> { + if (this.internalApi) { + await this.internalApi.stop(); + this.sessionStarted = false; + } else { + throw Error('Cannot start session without a role.'); + } + } + + public disableGuestChecker() { + // Remove the handlers for the guest checker notification + if (this.internalApi) { + this.internalApi.removeHandlers(LiveShare.GuestCheckerService); + } + this.externalProxy = null; + } + + public get isSessionStarted(): boolean { + return this.sessionStarted; + } + + private onInternalSessionChanged(_ev: vsls.SessionChangeEvent) { + if (this.internalApi) { + this.sessionStarted = this.internalApi.role !== vsls.Role.None; + } + } +} diff --git a/src/test/datascience/mockProcessService.ts b/src/test/datascience/mockProcessService.ts new file mode 100644 index 000000000000..7b795902a141 --- /dev/null +++ b/src/test/datascience/mockProcessService.ts @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Observable } from 'rxjs/Observable'; + +import { Cancellation, CancellationError } from '../../client/common/cancellation'; +import { + ExecutionResult, + IProcessService, + ObservableExecutionResult, + Output, + ShellOptions, + SpawnOptions +} from '../../client/common/process/types'; +import { noop, sleep } from '../core'; + +export class MockProcessService implements IProcessService { + private execResults: { file: string; args: (string | RegExp)[]; result(): Promise<ExecutionResult<string>> }[] = []; + private execObservableResults: { + file: string; + args: (string | RegExp)[]; + result(): ObservableExecutionResult<string>; + }[] = []; + private timeDelay: number | undefined; + + public execObservable(file: string, args: string[], _options: SpawnOptions): ObservableExecutionResult<string> { + const match = this.execObservableResults.find((f) => this.argsMatch(f.args, args) && f.file === file); + if (match) { + return match.result(); + } + + return this.defaultObservable([file, ...args]); + } + + public async exec(file: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> { + const match = this.execResults.find((f) => this.argsMatch(f.args, args) && f.file === file); + if (match) { + // Might need a delay before executing to mimic it taking a while. + if (this.timeDelay) { + try { + const localTime = this.timeDelay; + await Cancellation.race((_t) => sleep(localTime), options.token); + } catch (exc) { + if (exc instanceof CancellationError) { + return this.defaultExecutionResult([file, ...args]); + } + } + } + return match.result(); + } + + return this.defaultExecutionResult([file, ...args]); + } + + public shellExec(command: string, _options: ShellOptions): Promise<ExecutionResult<string>> { + // Not supported + return this.defaultExecutionResult([command]); + } + + public addExecResult(file: string, args: (string | RegExp)[], result: () => Promise<ExecutionResult<string>>) { + this.execResults.splice(0, 0, { file: file, args: args, result: result }); + } + + public addExecObservableResult( + file: string, + args: (string | RegExp)[], + result: () => ObservableExecutionResult<string> + ) { + this.execObservableResults.splice(0, 0, { file: file, args: args, result: result }); + } + + public setDelay(timeout: number | undefined) { + this.timeDelay = timeout; + } + + public on() { + return this; + } + + public dispose() { + return; + } + + private argsMatch(matchers: (string | RegExp)[], args: string[]): boolean { + if (matchers.length === args.length) { + return args.every((s, i) => { + const r = matchers[i] as RegExp; + return r && r.test ? r.test(s) : s === matchers[i]; + }); + } + return false; + } + + private defaultObservable(args: string[]): ObservableExecutionResult<string> { + const output = new Observable<Output<string>>((subscriber) => { + subscriber.next({ out: `Invalid call to ${args.join(' ')}`, source: 'stderr' }); + }); + return { + proc: undefined, + out: output, + dispose: () => noop + }; + } + + private defaultExecutionResult(args: string[]): Promise<ExecutionResult<string>> { + return Promise.resolve({ stderr: `Invalid call to ${args.join(' ')}`, stdout: '' }); + } +} diff --git a/src/test/datascience/mockProtocol2CodeConverter.ts b/src/test/datascience/mockProtocol2CodeConverter.ts new file mode 100644 index 000000000000..d6dab6b66450 --- /dev/null +++ b/src/test/datascience/mockProtocol2CodeConverter.ts @@ -0,0 +1,453 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as code from 'vscode'; +// tslint:disable-next-line: import-name +import ProtocolCompletionItem from 'vscode-languageclient/lib/common/protocolCompletionItem'; +import { Protocol2CodeConverter } from 'vscode-languageclient/node'; +import * as proto from 'vscode-languageserver-protocol'; + +// tslint:disable:no-any unified-signatures +export class MockProtocol2CodeConverter implements Protocol2CodeConverter { + public asUri(_value: string): code.Uri { + throw new Error('Method not implemented.'); + } + + public asDiagnostic(_diagnostic: proto.Diagnostic): code.Diagnostic { + throw new Error('Method not implemented.'); + } + public asDiagnostics(_diagnostics: proto.Diagnostic[]): code.Diagnostic[] { + throw new Error('Method not implemented.'); + } + + public asPosition(value: proto.Position): code.Position; + public asPosition(value: undefined): undefined; + public asPosition(value: null): null; + public asPosition(value: proto.Position | null | undefined): code.Position | null | undefined; + public asPosition(value: any): any { + if (!value) { + return undefined; + } + return new code.Position(value.line, value.character); + } + public asRange(value: proto.Range): code.Range; + public asRange(value: undefined): undefined; + public asRange(value: null): null; + public asRange(value: proto.Range | null | undefined): code.Range | null | undefined; + public asRange(value: any): any { + if (!value) { + return undefined; + } + return new code.Range( + this.asPosition(value.start as proto.Position), + this.asPosition(value.end as proto.Position) + ); + } + public asDiagnosticSeverity(_value: number | null | undefined): code.DiagnosticSeverity { + throw new Error('Method not implemented.'); + } + public asHover(hover: proto.Hover): code.Hover; + public asHover(hover: null | undefined): undefined; + public asHover(hover: proto.Hover | null | undefined): code.Hover | undefined; + public asHover(hover: any): any { + if (!hover) { + return undefined; + } + return hover; + } + public asCompletionResult(result: proto.CompletionList): code.CompletionList; + public asCompletionResult(result: proto.CompletionItem[]): code.CompletionItem[]; + public asCompletionResult(result: null | undefined): undefined; + public asCompletionResult( + result: proto.CompletionList | proto.CompletionItem[] | null | undefined + ): code.CompletionList | code.CompletionItem[] | undefined; + public asCompletionResult(result: any): any { + if (!result) { + return undefined; + } + if (Array.isArray(result)) { + const items = <proto.CompletionItem[]>result; + return items.map(this.asCompletionItem.bind(this)); + } + const list = <proto.CompletionList>result; + return new code.CompletionList(list.items.map(this.asCompletionItem.bind(this)), list.isIncomplete); + } + public asCompletionItem(item: proto.CompletionItem): ProtocolCompletionItem { + const result = new ProtocolCompletionItem(item.label); + if (item.detail) { + result.detail = item.detail; + } + if (item.documentation) { + result.documentation = item.documentation.toString(); + result.documentationFormat = '$string'; + } + if (item.filterText) { + result.filterText = item.filterText; + } + const insertText = this.asCompletionInsertText(item); + if (insertText) { + result.insertText = insertText.text; + result.range = insertText.range; + result.fromEdit = insertText.fromEdit; + } + if (typeof item.kind === 'number') { + const [itemKind, original] = this.asCompletionItemKind(item.kind); + result.kind = itemKind; + if (original) { + result.originalItemKind = original; + } + } + if (item.sortText) { + result.sortText = item.sortText; + } + if (item.additionalTextEdits) { + result.additionalTextEdits = this.asTextEdits(item.additionalTextEdits); + } + if (this.isStringArray(item.commitCharacters)) { + result.commitCharacters = item.commitCharacters.slice(); + } + if (item.command) { + result.command = this.asCommand(item.command); + } + if (item.deprecated === true || item.deprecated === false) { + result.deprecated = item.deprecated; + } + if (item.preselect === true || item.preselect === false) { + result.preselect = item.preselect; + } + if (item.data !== undefined) { + result.data = item.data; + } + return result; + } + public asTextEdit(edit: null | undefined): undefined; + public asTextEdit(edit: proto.TextEdit): code.TextEdit; + public asTextEdit(edit: proto.TextEdit | null | undefined): code.TextEdit | undefined; + public asTextEdit(_edit: any): any { + throw new Error('Method not implemented.'); + } + public asTextEdits(items: proto.TextEdit[]): code.TextEdit[]; + public asTextEdits(items: null | undefined): undefined; + public asTextEdits(items: proto.TextEdit[] | null | undefined): code.TextEdit[] | undefined; + public asTextEdits(_items: any): any { + throw new Error('Method not implemented.'); + } + public asSignatureHelp(item: null | undefined): undefined; + public asSignatureHelp(item: proto.SignatureHelp): code.SignatureHelp; + public asSignatureHelp(item: proto.SignatureHelp | null | undefined): code.SignatureHelp | undefined; + public asSignatureHelp(_item: any): any { + throw new Error('Method not implemented.'); + } + public asSignatureInformation(_item: proto.SignatureInformation): code.SignatureInformation { + throw new Error('Method not implemented.'); + } + public asSignatureInformations(_items: proto.SignatureInformation[]): code.SignatureInformation[] { + throw new Error('Method not implemented.'); + } + public asParameterInformation(_item: proto.ParameterInformation): code.ParameterInformation { + throw new Error('Method not implemented.'); + } + public asParameterInformations(_item: proto.ParameterInformation[]): code.ParameterInformation[] { + throw new Error('Method not implemented.'); + } + public asLocation(item: proto.Location): code.Location; + public asLocation(item: null | undefined): undefined; + public asLocation(item: proto.Location | null | undefined): code.Location | undefined; + public asLocation(_item: any): any { + throw new Error('Method not implemented.'); + } + public asDeclarationResult(item: proto.Declaration): code.Location | code.Location[]; + public asDeclarationResult(item: proto.LocationLink[]): code.LocationLink[]; + public asDeclarationResult(item: null | undefined): undefined; + public asDeclarationResult( + item: proto.Location | proto.Location[] | proto.LocationLink[] | null | undefined + ): code.Location | code.Location[] | code.LocationLink[] | undefined; + public asDeclarationResult(_item: any): any { + throw new Error('Method not implemented.'); + } + public asDefinitionResult(item: proto.Definition): code.Definition; + public asDefinitionResult(item: proto.LocationLink[]): code.LocationLink[]; + public asDefinitionResult(item: null | undefined): undefined; + public asDefinitionResult( + item: proto.Location | proto.LocationLink[] | proto.Location[] | null | undefined + ): code.Location | code.LocationLink[] | code.Location[] | undefined; + public asDefinitionResult(_item: any): any { + throw new Error('Method not implemented.'); + } + public asReferences(values: proto.Location[]): code.Location[]; + public asReferences(values: null | undefined): code.Location[] | undefined; + public asReferences(values: proto.Location[] | null | undefined): code.Location[] | undefined; + public asReferences(_values: any): any { + throw new Error('Method not implemented.'); + } + public asDocumentHighlightKind(_item: number): code.DocumentHighlightKind { + throw new Error('Method not implemented.'); + } + public asDocumentHighlight(_item: proto.DocumentHighlight): code.DocumentHighlight { + throw new Error('Method not implemented.'); + } + public asDocumentHighlights(values: proto.DocumentHighlight[]): code.DocumentHighlight[]; + public asDocumentHighlights(values: null | undefined): undefined; + public asDocumentHighlights( + values: proto.DocumentHighlight[] | null | undefined + ): code.DocumentHighlight[] | undefined; + public asDocumentHighlights(_values: any): any { + throw new Error('Method not implemented.'); + } + public asSymbolInformation(_item: proto.SymbolInformation, _uri?: code.Uri | undefined): code.SymbolInformation { + throw new Error('Method not implemented.'); + } + public asSymbolInformations( + values: proto.SymbolInformation[], + uri?: code.Uri | undefined + ): code.SymbolInformation[]; + public asSymbolInformations(values: null | undefined, uri?: code.Uri | undefined): undefined; + public asSymbolInformations( + values: proto.SymbolInformation[] | null | undefined, + uri?: code.Uri | undefined + ): code.SymbolInformation[] | undefined; + public asSymbolInformations(_values: any, _uri?: any): any { + throw new Error('Method not implemented.'); + } + public asDocumentSymbol(_value: proto.DocumentSymbol): code.DocumentSymbol { + throw new Error('Method not implemented.'); + } + public asDocumentSymbols(value: null | undefined): undefined; + public asDocumentSymbols(value: proto.DocumentSymbol[]): code.DocumentSymbol[]; + public asDocumentSymbols(value: proto.DocumentSymbol[] | null | undefined): code.DocumentSymbol[] | undefined; + public asDocumentSymbols(_value: any): any { + throw new Error('Method not implemented.'); + } + public asCommand(_item: proto.Command): code.Command { + throw new Error('Method not implemented.'); + } + public asCommands(items: proto.Command[]): code.Command[]; + public asCommands(items: null | undefined): undefined; + public asCommands(items: proto.Command[] | null | undefined): code.Command[] | undefined; + public asCommands(_items: any): any { + throw new Error('Method not implemented.'); + } + public asCodeAction(item: proto.CodeAction): code.CodeAction; + public asCodeAction(item: null | undefined): undefined; + public asCodeAction(item: proto.CodeAction | null | undefined): code.CodeAction | undefined; + public asCodeAction(_item: any): any { + throw new Error('Method not implemented.'); + } + public asCodeActionKind(item: null | undefined): undefined; + public asCodeActionKind(item: string): code.CodeActionKind; + public asCodeActionKind(item: string | null | undefined): code.CodeActionKind | undefined; + public asCodeActionKind(_item: any): any { + throw new Error('Method not implemented.'); + } + public asCodeActionKinds(item: null | undefined): undefined; + public asCodeActionKinds(items: string[]): code.CodeActionKind[]; + public asCodeActionKinds(item: string[] | null | undefined): code.CodeActionKind[] | undefined; + public asCodeActionKinds(_item: any): any { + throw new Error('Method not implemented.'); + } + public asCodeLens(item: proto.CodeLens): code.CodeLens; + public asCodeLens(item: null | undefined): undefined; + public asCodeLens(item: proto.CodeLens | null | undefined): code.CodeLens | undefined; + public asCodeLens(_item: any): any { + throw new Error('Method not implemented.'); + } + public asCodeLenses(items: proto.CodeLens[]): code.CodeLens[]; + public asCodeLenses(items: null | undefined): undefined; + public asCodeLenses(items: proto.CodeLens[] | null | undefined): code.CodeLens[] | undefined; + public asCodeLenses(_items: any): any { + throw new Error('Method not implemented.'); + } + public asWorkspaceEdit(item: proto.WorkspaceEdit): code.WorkspaceEdit; + public asWorkspaceEdit(item: null | undefined): undefined; + public asWorkspaceEdit(item: proto.WorkspaceEdit | null | undefined): code.WorkspaceEdit | undefined; + public asWorkspaceEdit(_item: any): any { + throw new Error('Method not implemented.'); + } + public asDocumentLink(_item: proto.DocumentLink): code.DocumentLink { + throw new Error('Method not implemented.'); + } + public asDocumentLinks(items: proto.DocumentLink[]): code.DocumentLink[]; + public asDocumentLinks(items: null | undefined): undefined; + public asDocumentLinks(items: proto.DocumentLink[] | null | undefined): code.DocumentLink[] | undefined; + public asDocumentLinks(_items: any): any { + throw new Error('Method not implemented.'); + } + public asColor(_color: proto.Color): code.Color { + throw new Error('Method not implemented.'); + } + public asColorInformation(_ci: proto.ColorInformation): code.ColorInformation { + throw new Error('Method not implemented.'); + } + public asColorInformations(colorPresentations: proto.ColorInformation[]): code.ColorInformation[]; + public asColorInformations(colorPresentations: null | undefined): undefined; + public asColorInformations(colorInformation: proto.ColorInformation[] | null | undefined): code.ColorInformation[]; + public asColorInformations(_colorInformation: any): any { + throw new Error('Method not implemented.'); + } + public asColorPresentation(_cp: proto.ColorPresentation): code.ColorPresentation { + throw new Error('Method not implemented.'); + } + public asColorPresentations(colorPresentations: proto.ColorPresentation[]): code.ColorPresentation[]; + public asColorPresentations(colorPresentations: null | undefined): undefined; + public asColorPresentations(colorPresentations: proto.ColorPresentation[] | null | undefined): undefined; + public asColorPresentations(_colorPresentations: any): any { + throw new Error('Method not implemented.'); + } + public asFoldingRangeKind(_kind: string | undefined): code.FoldingRangeKind | undefined { + throw new Error('Method not implemented.'); + } + public asFoldingRange(_r: proto.FoldingRange): code.FoldingRange { + throw new Error('Method not implemented.'); + } + public asFoldingRanges(foldingRanges: proto.FoldingRange[]): code.FoldingRange[]; + public asFoldingRanges(foldingRanges: null | undefined): undefined; + public asFoldingRanges(foldingRanges: proto.FoldingRange[] | null | undefined): code.FoldingRange[] | undefined; + public asFoldingRanges(foldingRanges: proto.FoldingRange[] | null | undefined): code.FoldingRange[] | undefined; + public asFoldingRanges(_foldingRanges: any): any { + throw new Error('Method not implemented.'); + } + public asRanges(_values: proto.Range[]): code.Range[] { + throw new Error('Method not implemented.'); + } + public asDiagnosticTag(_tag: proto.InsertTextFormat): code.DiagnosticTag | undefined { + throw new Error('Method not implemented.'); + } + public asSymbolKind(_item: proto.SymbolKind): code.SymbolKind { + throw new Error('Method not implemented.'); + } + public asSymbolTag(_item: 1): code.SymbolTag { + throw new Error('Method not implemented.'); + } + public asSymbolTags(items: null | undefined): undefined; + public asSymbolTags(items: readonly 1[]): code.SymbolTag[]; + public asSymbolTags(items: readonly 1[] | null | undefined): code.SymbolTag[] | undefined; + public asSymbolTags(_items: any): any { + throw new Error('Method not implemented.'); + } + public asSelectionRange(_selectionRange: proto.SelectionRange): code.SelectionRange { + throw new Error('Method not implemented.'); + } + public asSelectionRanges(selectionRanges: proto.SelectionRange[]): code.SelectionRange[]; + public asSelectionRanges(selectionRanges: null | undefined): undefined; + public asSelectionRanges( + selectionRanges: proto.SelectionRange[] | null | undefined + ): code.SelectionRange[] | undefined; + public asSelectionRanges( + selectionRanges: proto.SelectionRange[] | null | undefined + ): code.SelectionRange[] | undefined; + public asSelectionRanges(_selectionRanges: any): any { + throw new Error('Method not implemented.'); + } + public asSemanticTokensLegend(_value: proto.SemanticTokensLegend): code.SemanticTokensLegend { + throw new Error('Method not implemented.'); + } + public asSemanticTokens(value: proto.SemanticTokens): code.SemanticTokens; + public asSemanticTokens(value: undefined | null): undefined; + public asSemanticTokens(value: proto.SemanticTokens | undefined | null): code.SemanticTokens | undefined; + public asSemanticTokens(value: proto.SemanticTokens | undefined | null): code.SemanticTokens | undefined; + public asSemanticTokens(_value: any): code.SemanticTokens | undefined { + throw new Error('Method not implemented.'); + } + public asSemanticTokensEdit(_value: proto.SemanticTokensEdit): code.SemanticTokensEdit { + throw new Error('Method not implemented.'); + } + public asSemanticTokensEdits(value: proto.SemanticTokensDelta): code.SemanticTokensEdits; + public asSemanticTokensEdits(value: undefined | null): undefined; + public asSemanticTokensEdits( + value: proto.SemanticTokensDelta | undefined | null + ): code.SemanticTokensEdits | undefined; + public asSemanticTokensEdits( + value: proto.SemanticTokensDelta | undefined | null + ): code.SemanticTokensEdits | undefined; + public asSemanticTokensEdits(_value: any): code.SemanticTokensEdits | undefined { + throw new Error('Method not implemented.'); + } + public asCallHierarchyItem(item: null): undefined; + public asCallHierarchyItem(item: proto.CallHierarchyItem): code.CallHierarchyItem; + public asCallHierarchyItem(item: proto.CallHierarchyItem | null): code.CallHierarchyItem | undefined; + public asCallHierarchyItem(item: proto.CallHierarchyItem | null): code.CallHierarchyItem | undefined; + public asCallHierarchyItem(_item: any): code.CallHierarchyItem | undefined { + throw new Error('Method not implemented.'); + } + public asCallHierarchyItems(items: null): undefined; + public asCallHierarchyItems(items: proto.CallHierarchyItem[]): code.CallHierarchyItem[]; + public asCallHierarchyItems(items: proto.CallHierarchyItem[] | null): code.CallHierarchyItem[] | undefined; + public asCallHierarchyItems(items: proto.CallHierarchyItem[] | null): code.CallHierarchyItem[] | undefined; + public asCallHierarchyItems(_items: any): code.CallHierarchyItem[] | undefined { + throw new Error('Method not implemented.'); + } + public asCallHierarchyIncomingCall(_item: proto.CallHierarchyIncomingCall): code.CallHierarchyIncomingCall { + throw new Error('Method not implemented.'); + } + public asCallHierarchyIncomingCalls(items: null): undefined; + public asCallHierarchyIncomingCalls( + items: ReadonlyArray<proto.CallHierarchyIncomingCall> + ): code.CallHierarchyIncomingCall[]; + public asCallHierarchyIncomingCalls( + items: ReadonlyArray<proto.CallHierarchyIncomingCall> | null + ): code.CallHierarchyIncomingCall[] | undefined; + public asCallHierarchyIncomingCalls( + items: ReadonlyArray<proto.CallHierarchyIncomingCall> | null + ): code.CallHierarchyIncomingCall[] | undefined; + public asCallHierarchyIncomingCalls(_items: any): code.CallHierarchyIncomingCall[] | undefined { + throw new Error('Method not implemented.'); + } + public asCallHierarchyOutgoingCall(_item: proto.CallHierarchyOutgoingCall): code.CallHierarchyOutgoingCall { + throw new Error('Method not implemented.'); + } + public asCallHierarchyOutgoingCalls(items: null): undefined; + public asCallHierarchyOutgoingCalls( + items: ReadonlyArray<proto.CallHierarchyOutgoingCall> + ): code.CallHierarchyOutgoingCall[]; + public asCallHierarchyOutgoingCalls( + items: ReadonlyArray<proto.CallHierarchyOutgoingCall> | null + ): code.CallHierarchyOutgoingCall[] | undefined; + public asCallHierarchyOutgoingCalls( + items: ReadonlyArray<proto.CallHierarchyOutgoingCall> | null + ): code.CallHierarchyOutgoingCall[] | undefined; + public asCallHierarchyOutgoingCalls(_items: any): code.CallHierarchyOutgoingCall[] | undefined { + throw new Error('Method not implemented.'); + } + + private asCompletionItemKind( + value: proto.CompletionItemKind + ): [code.CompletionItemKind, proto.CompletionItemKind | undefined] { + // Protocol item kind is 1 based, codes item kind is zero based. + if (proto.CompletionItemKind.Text <= value && value <= proto.CompletionItemKind.TypeParameter) { + return [value - 1, undefined]; + } + return [code.CompletionItemKind.Text, value]; + } + + private isStringArray(value: any): value is string[] { + return Array.isArray(value) && (<any[]>value).every((elem) => typeof elem === 'string'); + } + + private asCompletionInsertText( + item: proto.CompletionItem + ): { text: string | code.SnippetString; range?: code.Range; fromEdit: boolean } | undefined { + if (item.textEdit) { + if (item.insertTextFormat === proto.InsertTextFormat.Snippet) { + return { + text: new code.SnippetString(item.textEdit.newText), + range: this.asRange((item.textEdit as code.TextEdit).range), + fromEdit: true + }; + } else { + return { + text: item.textEdit.newText, + range: this.asRange((item.textEdit as code.TextEdit).range), + fromEdit: true + }; + } + } else if (item.insertText) { + if (item.insertTextFormat === proto.InsertTextFormat.Snippet) { + return { text: new code.SnippetString(item.insertText), fromEdit: false }; + } else { + return { text: item.insertText, fromEdit: false }; + } + } else { + return undefined; + } + } +} diff --git a/src/test/datascience/mockPythonService.ts b/src/test/datascience/mockPythonService.ts new file mode 100644 index 000000000000..d4bd33c20875 --- /dev/null +++ b/src/test/datascience/mockPythonService.ts @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { + ExecutionResult, + IPythonExecutionService, + ObservableExecutionResult, + SpawnOptions +} from '../../client/common/process/types'; +import { buildPythonExecInfo } from '../../client/pythonEnvironments/exec'; +import { InterpreterInformation, PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { MockProcessService } from './mockProcessService'; + +export class MockPythonService implements IPythonExecutionService { + private interpreter: PythonEnvironment; + private procService: MockProcessService = new MockProcessService(); + + constructor(interpreter: PythonEnvironment) { + this.interpreter = interpreter; + } + + public getInterpreterInformation(): Promise<InterpreterInformation> { + return Promise.resolve(this.interpreter); + } + + public getExecutablePath(): Promise<string> { + return Promise.resolve(this.interpreter.path); + } + + public isModuleInstalled(_moduleName: string): Promise<boolean> { + return Promise.resolve(false); + } + + public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult<string> { + return this.procService.execObservable(this.interpreter.path, args, options); + } + public execModuleObservable( + moduleName: string, + args: string[], + options: SpawnOptions + ): ObservableExecutionResult<string> { + return this.procService.execObservable(this.interpreter.path, ['-m', moduleName, ...args], options); + } + public exec(args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> { + return this.procService.exec(this.interpreter.path, args, options); + } + + public execModule(moduleName: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> { + return this.procService.exec(this.interpreter.path, ['-m', moduleName, ...args], options); + } + + public addExecResult(args: (string | RegExp)[], result: () => Promise<ExecutionResult<string>>) { + this.procService.addExecResult(this.interpreter.path, args, result); + } + + public addExecModuleResult( + moduleName: string, + args: (string | RegExp)[], + result: () => Promise<ExecutionResult<string>> + ) { + this.procService.addExecResult(this.interpreter.path, ['-m', moduleName, ...args], result); + } + + public addExecObservableResult(args: (string | RegExp)[], result: () => ObservableExecutionResult<string>) { + this.procService.addExecObservableResult(this.interpreter.path, args, result); + } + + public addExecModuleObservableResult( + moduleName: string, + args: (string | RegExp)[], + result: () => ObservableExecutionResult<string> + ) { + this.procService.addExecObservableResult(this.interpreter.path, ['-m', moduleName, ...args], result); + } + + public setDelay(timeout: number | undefined) { + this.procService.setDelay(timeout); + } + + public getExecutionInfo(args: string[]) { + return buildPythonExecInfo(this.interpreter.path, args); + } +} diff --git a/src/test/datascience/mockPythonSettings.ts b/src/test/datascience/mockPythonSettings.ts new file mode 100644 index 000000000000..956d9ec5d65c --- /dev/null +++ b/src/test/datascience/mockPythonSettings.ts @@ -0,0 +1,37 @@ +import { IWorkspaceService } from '../../client/common/application/types'; +import { PythonSettings } from '../../client/common/configSettings'; +import { IExperimentsManager, IInterpreterPathService, Resource } from '../../client/common/types'; +import { + IInterpreterAutoSeletionProxyService, + IInterpreterSecurityService +} from '../../client/interpreter/autoSelection/types'; + +export class MockPythonSettings extends PythonSettings { + constructor( + workspaceFolder: Resource, + interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, + workspace?: IWorkspaceService, + experimentsManager?: IExperimentsManager, + interpreterPathService?: IInterpreterPathService, + interpreterSecurityService?: IInterpreterSecurityService + ) { + super( + workspaceFolder, + interpreterAutoSelectionService, + workspace, + experimentsManager, + interpreterPathService, + interpreterSecurityService + ); + } + + public fireChangeEvent() { + this.changed.fire(); + } + + protected getPythonExecutable(v: string) { + // Don't validate python paths during tests. On windows this can take 4 or 5 seconds + // and slow down every test + return v; + } +} diff --git a/src/test/datascience/mockQuickPick.ts b/src/test/datascience/mockQuickPick.ts new file mode 100644 index 000000000000..ad9d85059542 --- /dev/null +++ b/src/test/datascience/mockQuickPick.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Event, EventEmitter, QuickInputButton, QuickPick, QuickPickItem } from 'vscode'; + +export class MockQuickPick implements QuickPick<QuickPickItem> { + public value: string = ''; + public placeholder: string | undefined; + public title: string | undefined = 'foo'; + public step: number | undefined; + public totalSteps: number | undefined; + public enabled: boolean = true; + public busy: boolean = false; + public ignoreFocusOut: boolean = true; + public items: QuickPickItem[] = []; + public canSelectMany: boolean = false; + public matchOnDescription: boolean = false; + public matchOnDetail: boolean = false; + public buttons: QuickInputButton[] = []; + private didChangeValueEmitter: EventEmitter<string> = new EventEmitter<string>(); + private didAcceptEmitter: EventEmitter<void> = new EventEmitter<void>(); + private didTriggerButtonEmitter: EventEmitter<QuickInputButton> = new EventEmitter<QuickInputButton>(); + private didChangeActiveEmitter: EventEmitter<QuickPickItem[]> = new EventEmitter<QuickPickItem[]>(); + private didChangeSelectedEmitter: EventEmitter<QuickPickItem[]> = new EventEmitter<QuickPickItem[]>(); + private didHideEmitter: EventEmitter<void> = new EventEmitter<void>(); + private _activeItems: QuickPickItem[] = []; + private _pickedItem: string; + constructor(pickedItem: string) { + this._pickedItem = pickedItem; + } + + public get onDidChangeValue(): Event<string> { + return this.didChangeValueEmitter.event; + } + public get onDidAccept(): Event<void> { + return this.didAcceptEmitter.event; + } + public get onDidTriggerButton(): Event<QuickInputButton> { + return this.didTriggerButtonEmitter.event; + } + public get activeItems(): QuickPickItem[] { + return this._activeItems; + } + public set activeItems(items: QuickPickItem[]) { + this._activeItems = items; + this.didChangeActiveEmitter.fire(items); + } + public get onDidChangeActive(): Event<QuickPickItem[]> { + return this.didChangeActiveEmitter.event; + } + public get selectedItems(): readonly QuickPickItem[] { + return []; + } + public get onDidChangeSelection(): Event<QuickPickItem[]> { + return this.didChangeSelectedEmitter.event; + } + public get onDidHide(): Event<void> { + return this.didHideEmitter.event; + } + public show(): void { + // After a timeout select the item + setTimeout(() => { + const item = this.items.find((a) => a.label === this._pickedItem); + if (item) { + this.didChangeSelectedEmitter.fire([item]); + } else { + this.didHideEmitter.fire(); + } + }, 1); + } + public hide(): void { + // Do nothing. + } + public dispose(): void { + // Do nothing. + } +} diff --git a/src/test/datascience/mockStatusProvider.ts b/src/test/datascience/mockStatusProvider.ts new file mode 100644 index 000000000000..7f96760e1aa4 --- /dev/null +++ b/src/test/datascience/mockStatusProvider.ts @@ -0,0 +1,27 @@ +import { Disposable } from 'vscode'; +import { IInteractiveBase, IStatusProvider } from '../../client/datascience/types'; +import { noop } from '../core'; +export class MockStatusProvider implements IStatusProvider { + public set( + _message: string, + _inweb: boolean, + _timeout?: number, + _cancel?: () => void, + _panel?: IInteractiveBase + ): Disposable { + return { + dispose: noop + }; + } + + public waitWithStatus<T>( + promise: () => Promise<T>, + _message: string, + _inweb: boolean, + _timeout?: number, + _canceled?: () => void, + _panel?: IInteractiveBase + ): Promise<T> { + return promise(); + } +} diff --git a/src/test/datascience/mockTextEditor.ts b/src/test/datascience/mockTextEditor.ts new file mode 100644 index 000000000000..88a8019c0bc4 --- /dev/null +++ b/src/test/datascience/mockTextEditor.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { + DecorationOptions, + EndOfLine, + Position, + Range, + Selection, + SnippetString, + TextDocument, + TextEditor, + TextEditorDecorationType, + TextEditorEdit, + TextEditorOptions, + TextEditorRevealType, + ViewColumn +} from 'vscode'; + +import { noop } from '../../client/common/utils/misc'; +import { MockDocument } from './mockDocument'; +import { MockDocumentManager } from './mockDocumentManager'; + +class MockEditorEdit implements TextEditorEdit { + constructor(private _documentManager: MockDocumentManager, private _document: MockDocument) {} + + public replace(location: Selection | Range | Position, value: string): void { + this._documentManager.changeDocument(this._document.fileName, [ + { + range: location as Range, + newText: value + } + ]); + } + + public insert(location: Position, value: string): void { + this._documentManager.changeDocument(this._document.fileName, [ + { + range: new Range(location, location), + newText: value + } + ]); + } + public delete(_location: Selection | Range): void { + throw new Error('Method not implemented.'); + } + public setEndOfLine(_endOfLine: EndOfLine): void { + throw new Error('Method not implemented.'); + } +} + +export class MockEditor implements TextEditor { + public selection: Selection; + public selections: Selection[] = []; + private _revealCallback: () => void; + + constructor(private _documentManager: MockDocumentManager, private _document: MockDocument) { + this.selection = new Selection(0, 0, 0, 0); + this._revealCallback = noop; + } + + public get document(): TextDocument { + return this._document; + } + public get visibleRanges(): Range[] { + return []; + } + public get options(): TextEditorOptions { + return {}; + } + public get viewColumn(): ViewColumn | undefined { + return undefined; + } + public edit( + callback: (editBuilder: TextEditorEdit) => void, + _options?: { undoStopBefore: boolean; undoStopAfter: boolean } | undefined + ): Thenable<boolean> { + return new Promise((r) => { + const editor = new MockEditorEdit(this._documentManager, this._document); + callback(editor); + r(true); + }); + } + public insertSnippet( + _snippet: SnippetString, + _location?: Range | Position | Range[] | Position[] | undefined, + _options?: { undoStopBefore: boolean; undoStopAfter: boolean } | undefined + ): Thenable<boolean> { + throw new Error('Method not implemented.'); + } + public setDecorations( + _decorationType: TextEditorDecorationType, + _rangesOrOptions: Range[] | DecorationOptions[] + ): void { + throw new Error('Method not implemented.'); + } + public revealRange(_range: Range, _revealType?: TextEditorRevealType | undefined): void { + this._revealCallback(); + } + public show(_column?: ViewColumn | undefined): void { + throw new Error('Method not implemented.'); + } + public hide(): void { + throw new Error('Method not implemented.'); + } + + public setRevealCallback(callback: () => void) { + this._revealCallback = callback; + } +} diff --git a/src/test/datascience/mockWorkspaceConfig.ts b/src/test/datascience/mockWorkspaceConfig.ts new file mode 100644 index 000000000000..12d5b8d3c268 --- /dev/null +++ b/src/test/datascience/mockWorkspaceConfig.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ConfigurationTarget, WorkspaceConfiguration } from 'vscode'; + +export class MockWorkspaceConfiguration implements WorkspaceConfiguration { + // tslint:disable: no-any + private values = new Map<string, any>(); + + constructor(defaultSettings?: any) { + if (defaultSettings) { + const keys = [...Object.keys(defaultSettings)]; + keys.forEach((k) => this.values.set(k, defaultSettings[k])); + } + + // Special case python path (not in the object) + if (defaultSettings && defaultSettings.pythonPath) { + this.values.set('pythonPath', defaultSettings.pythonPath); + } + + // Special case datascience. Not the same case + if (defaultSettings && defaultSettings.datascience) { + this.values.set('dataScience', defaultSettings.datascience); + } + } + + public get<T>(key: string, defaultValue?: T): T | undefined { + // tslint:disable-next-line: use-named-parameter + if (this.values.has(key)) { + return this.values.get(key); + } + + return arguments.length > 1 ? defaultValue : (undefined as any); + } + public has(section: string): boolean { + return this.values.has(section); + } + public inspect<T>( + _section: string + ): + | { + key: string; + defaultValue?: T | undefined; + globalValue?: T | undefined; + workspaceValue?: T | undefined; + workspaceFolderValue?: T | undefined; + } + | undefined { + return; + } + public update( + section: string, + value: any, + _configurationTarget?: boolean | ConfigurationTarget | undefined + ): Promise<void> { + this.values.set(section, value); + return Promise.resolve(); + } +} diff --git a/src/test/datascience/mockWorkspaceConfiguration.ts b/src/test/datascience/mockWorkspaceConfiguration.ts new file mode 100644 index 000000000000..2237c9c2b02a --- /dev/null +++ b/src/test/datascience/mockWorkspaceConfiguration.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { ConfigurationTarget, WorkspaceConfiguration } from 'vscode'; + +// tslint:disable: no-any +export class MockWorkspaceConfiguration implements WorkspaceConfiguration { + private map: Map<string, any> = new Map<string, any>(); + + // tslint:disable: no-any + public get(key: string): any; + public get<T>(section: string): T | undefined; + public get<T>(section: string, defaultValue: T): T; + public get(section: any, defaultValue?: any): any; + public get(section: string, defaultValue?: any): any { + if (this.map.has(section)) { + return this.map.get(section); + } + return arguments.length > 1 ? defaultValue : (undefined as any); + } + public has(_section: string): boolean { + return false; + } + public inspect<T>( + _section: string + ): + | { + key: string; + defaultValue?: T | undefined; + globalValue?: T | undefined; + workspaceValue?: T | undefined; + workspaceFolderValue?: T | undefined; + } + | undefined { + return; + } + public update( + section: string, + value: any, + _configurationTarget?: boolean | ConfigurationTarget | undefined + ): Promise<void> { + this.map.set(section, value); + return Promise.resolve(); + } +} diff --git a/src/test/datascience/mockWorkspaceFolder.ts b/src/test/datascience/mockWorkspaceFolder.ts new file mode 100644 index 000000000000..3b56a9860bf5 --- /dev/null +++ b/src/test/datascience/mockWorkspaceFolder.ts @@ -0,0 +1,12 @@ +import { Uri, WorkspaceFolder } from 'vscode'; + +export class MockWorkspaceFolder implements WorkspaceFolder { + public uri: Uri; + public name: string; + public ownedResources = new Set<string>(); + + constructor(folder: string, public index: number) { + this.uri = Uri.file(folder); + this.name = folder; + } +} diff --git a/src/test/datascience/mountedWebView.ts b/src/test/datascience/mountedWebView.ts new file mode 100644 index 000000000000..7ef57e71a4db --- /dev/null +++ b/src/test/datascience/mountedWebView.ts @@ -0,0 +1,259 @@ +import { ReactWrapper } from 'enzyme'; +import { noop } from 'lodash'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { + IWebviewPanel, + IWebviewPanelMessageListener, + IWebviewPanelOptions, + WebviewMessage +} from '../../client/common/application/types'; +import { traceError, traceInfo } from '../../client/common/logger'; +import { IDisposable } from '../../client/common/types'; +import { createDeferred } from '../../client/common/utils/async'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { IVsCodeApi } from '../../datascience-ui/react-common/postOffice'; + +export type WaitForMessageOptions = { + /** + * Timeout for waiting for message. + * Defaults to 65_000ms. + * + * @type {number} + */ + timeoutMs?: number; + /** + * Number of times the message should be received. + * Defaults to 1. + * + * @type {number} + */ + numberOfTimes?: number; + + // Optional check for the payload of the message + // will only return (or count) message if this returns true + // tslint:disable-next-line: no-any + withPayload?(payload: any): boolean; +}; + +// tslint:disable: no-any +export interface IMountedWebView extends IWebviewPanel, IDisposable { + readonly id: string; + readonly wrapper: ReactWrapper<any, Readonly<{}>, React.Component>; + readonly onDisposed: Event<void>; + postMessage(ev: WebviewMessage): void; + changeViewState(active: boolean, visible: boolean): void; + addMessageListener(callback: (m: string, p: any) => void): void; + removeMessageListener(callback: (m: string, p: any) => void): void; + attach(options: IWebviewPanelOptions): void; + waitForMessage(message: string, options?: WaitForMessageOptions): Promise<void>; +} + +export class MountedWebView implements IMountedWebView, IDisposable { + public wrapper: ReactWrapper<any, Readonly<{}>, React.Component>; + private missedMessages: any[] = []; + private webPanelListener: IWebviewPanelMessageListener | undefined; + private reactMessageCallback: ((ev: MessageEvent) => void) | undefined; + private extraListeners: ((m: string, p: any) => void)[] = []; + private disposed = false; + private active = true; + private visible = true; + private disposedEvent = new EventEmitter<void>(); + private loadFailedEmitter = new EventEmitter<void>(); + + constructor(mount: () => ReactWrapper<any, Readonly<{}>, React.Component>, public readonly id: string) { + // Setup the acquireVsCodeApi. The react control will cache this value when it's mounted. + const globalAcquireVsCodeApi = (): IVsCodeApi => { + return { + // tslint:disable-next-line:no-any + postMessage: (msg: any) => { + this.postMessageToWebPanel(msg); + }, + // tslint:disable-next-line:no-any no-empty + setState: (_msg: any) => {}, + // tslint:disable-next-line:no-any no-empty + getState: () => { + return {}; + } + }; + }; + // tslint:disable-next-line:no-string-literal + (global as any)['acquireVsCodeApi'] = globalAcquireVsCodeApi; + + // Remap event handlers to point to the container. + const oldListener = window.addEventListener; + window.addEventListener = (event: string, cb: any) => { + if (event === 'message') { + this.reactMessageCallback = cb; + } + }; + + // Mount our main panel. This will make the global api be cached and have the event handler registered + this.wrapper = mount(); + + // We can remove the global api and event listener now. + delete (global as any).acquireVsCodeApi; + window.addEventListener = oldListener; + } + + public get onDisposed() { + return this.disposedEvent.event; + } + public get loadFailed(): Event<void> { + return this.loadFailedEmitter.event; + } + public attach(options: IWebviewPanelOptions) { + this.webPanelListener = options.listener; + + // Send messages that were already posted but were missed. + // During normal operation, the react control will not be created before + // the webPanelListener + if (this.missedMessages.length && this.webPanelListener) { + // This needs to be async because we are being called in the ctor of the webpanel. It can't + // handle some messages during the ctor. + setTimeout(() => { + this.missedMessages.forEach((m) => + this.webPanelListener ? this.webPanelListener.onMessage(m.type, m.payload) : noop() + ); + this.missedMessages = []; + }, 0); + } + } + + public async waitForMessage(message: string, options?: WaitForMessageOptions): Promise<void> { + const timeoutMs = options && options.timeoutMs ? options.timeoutMs : undefined; + const numberOfTimes = options && options.numberOfTimes ? options.numberOfTimes : 1; + // Wait for the mounted web panel to send a message back to the data explorer + const promise = createDeferred<void>(); + traceInfo(`Waiting for message ${message} with timeout of ${timeoutMs}`); + let handler: (m: string, p: any) => void; + const timer = timeoutMs + ? setTimeout(() => { + if (!promise.resolved) { + promise.reject(new Error(`Waiting for ${message} timed out`)); + } + }, timeoutMs) + : undefined; + let timesMessageReceived = 0; + const dispatchedAction = `DISPATCHED_ACTION_${message}`; + handler = (m: string, payload: any) => { + if (m === message || m === dispatchedAction) { + // First verify the payload matches + if (options?.withPayload) { + if (!options.withPayload(payload)) { + return; + } + } + + timesMessageReceived += 1; + if (timesMessageReceived < numberOfTimes) { + return; + } + if (timer) { + clearTimeout(timer); + } + this.removeMessageListener(handler); + // Make sure to rerender current state. + if (this.wrapper) { + this.wrapper.update(); + } + if (m === message) { + promise.resolve(); + } else { + // It could a redux dispatched message. + // Wait for 10ms, wait for other stuff to finish. + // We can wait for 100ms or 1s. But thats too long. + // The assumption is that currently we do not have any setTimeouts + // in UI code that's in the magnitude of 100ms or more. + // We do have a couple of setTiemout's, but they wait for 1ms, not 100ms. + // 10ms more than sufficient for all the UI timeouts. + setTimeout(() => promise.resolve(), 10); + } + } + }; + + this.addMessageListener(handler); + return promise.promise; + } + + public asWebviewUri(localResource: Uri): Uri { + return localResource; + } + public setTitle(_val: string): void { + noop(); + } + public async show(_preserveFocus: boolean): Promise<void> { + noop(); + } + public isVisible(): boolean { + return this.visible; + } + public postMessage(m: WebviewMessage): void { + // Actually send to the UI + if (this.reactMessageCallback) { + // tslint:disable-next-line: no-require-imports + const reactHelpers = require('./reactHelpers') as typeof import('./reactHelpers'); + const message = reactHelpers.createMessageEvent(m); + this.reactMessageCallback(message); + if (m.payload) { + delete m.payload; + } + } + } + public close(): void { + noop(); + } + public isActive(): boolean { + return this.active; + } + public updateCwd(_cwd: string): void { + noop(); + } + public dispose() { + if (!this.disposed) { + this.disposed = true; + if (this.wrapper.length) { + this.wrapper.unmount(); + } + this.disposedEvent.fire(); + } + } + + public changeViewState(active: boolean, visible: boolean) { + this.active = active; + this.visible = visible; + if (this.webPanelListener) { + this.webPanelListener.onChangeViewState(this); + } + } + public addMessageListener(callback: (m: string, p: any) => void) { + this.extraListeners.push(callback); + } + + public removeMessageListener(callback: (m: string, p: any) => void) { + const index = this.extraListeners.indexOf(callback); + if (index >= 0) { + this.extraListeners.splice(index, 1); + } + } + private postMessageToWebPanel(msg: any) { + if (this.disposed && !msg.type.startsWith(`DISPATCHED`)) { + traceError(`Posting to disposed mount.`); + } + if (this.webPanelListener) { + this.webPanelListener.onMessage(msg.type, msg.payload); + } else { + this.missedMessages.push({ type: msg.type, payload: msg.payload }); + } + if (this.extraListeners.length) { + this.extraListeners.forEach((e) => e(msg.type, msg.payload)); + } + + // Clear out msg payload + delete msg.payload; + + // unmount ourselves if this is the close message + if (msg.type === InteractiveWindowMessages.NotebookClose) { + this.dispose(); + } + } +} diff --git a/src/test/datascience/mountedWebViewFactory.ts b/src/test/datascience/mountedWebViewFactory.ts new file mode 100644 index 000000000000..107a2c7d404f --- /dev/null +++ b/src/test/datascience/mountedWebViewFactory.ts @@ -0,0 +1,44 @@ +import { ReactWrapper } from 'enzyme'; +import { inject, injectable } from 'inversify'; +import { IDisposable, IDisposableRegistry } from '../../client/common/types'; +import { IMountedWebView, MountedWebView } from './mountedWebView'; + +export const IMountedWebViewFactory = Symbol('IMountedWebViewFactory'); + +export interface IMountedWebViewFactory { + get(id: string): IMountedWebView; + // tslint:disable-next-line: no-any + create(id: string, mount: () => ReactWrapper<any, Readonly<{}>, React.Component>): IMountedWebView; +} + +@injectable() +export class MountedWebViewFactory implements IMountedWebViewFactory, IDisposable { + private map = new Map<string, MountedWebView>(); + + constructor(@inject(IDisposableRegistry) readonly disposables: IDisposableRegistry) { + disposables.push(this); + } + + public dispose() { + this.map.forEach((v) => v.dispose()); + this.map.clear(); + } + public get(id: string): IMountedWebView { + const obj = this.map.get(id); + if (!obj) { + throw new Error(`No mounted web view found for id ${id}`); + } + return obj; + } + + // tslint:disable-next-line: no-any + public create(id: string, mount: () => ReactWrapper<any, Readonly<{}>, React.Component>): IMountedWebView { + if (this.map.has(id)) { + throw new Error(`Mounted web view already exists for id ${id}`); + } + const obj = new MountedWebView(mount, id); + obj.onDisposed(() => this.map.delete(id)); + this.map.set(id, obj); + return obj; + } +} diff --git a/src/test/datascience/nativeEditor.functional.test.tsx b/src/test/datascience/nativeEditor.functional.test.tsx new file mode 100644 index 000000000000..041b71198905 --- /dev/null +++ b/src/test/datascience/nativeEditor.functional.test.tsx @@ -0,0 +1,2771 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { nbformat } from '@jupyterlab/coreutils'; +import { assert, expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as dedent from 'dedent'; +import { ReactWrapper } from 'enzyme'; +import * as fs from 'fs-extra'; +import { IDisposable } from 'monaco-editor'; +import * as os from 'os'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { anything, objectContaining, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { CustomEditorProvider, Disposable, TextDocument, TextEditor, Uri, WindowState } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { + IApplicationShell, + ICommandManager, + ICustomEditorService, + IDocumentManager, + IWorkspaceService +} from '../../client/common/application/types'; +import { LocalZMQKernel } from '../../client/common/experiments/groups'; +import { ICryptoUtils, IExtensionContext } from '../../client/common/types'; +import { createDeferred, sleep, waitForPromise } from '../../client/common/utils/async'; +import { noop } from '../../client/common/utils/misc'; +import { Commands, Identifiers } from '../../client/datascience/constants'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { NativeEditor as NativeEditorWebView } from '../../client/datascience/interactive-ipynb/nativeEditor'; +import { IKernelSpecQuickPickItem } from '../../client/datascience/jupyter/kernels/types'; +import { KeyPrefix } from '../../client/datascience/notebookStorage/nativeEditorStorage'; +import { + ICell, + IDataScienceErrorHandler, + IJupyterExecution, + INotebookEditor, + INotebookEditorProvider, + INotebookExporter, + ITrustService +} from '../../client/datascience/types'; +import { concatMultilineString } from '../../datascience-ui/common'; +import { Editor } from '../../datascience-ui/interactive-common/editor'; +import { ExecutionCount } from '../../datascience-ui/interactive-common/executionCount'; +import { CommonActionType } from '../../datascience-ui/interactive-common/redux/reducers/types'; +import { NativeCell } from '../../datascience-ui/native-editor/nativeCell'; +import { NativeEditor } from '../../datascience-ui/native-editor/nativeEditor'; +import { IKeyboardEvent } from '../../datascience-ui/react-common/event'; +import { ImageButton } from '../../datascience-ui/react-common/imageButton'; +import { IMonacoEditorState, MonacoEditor } from '../../datascience-ui/react-common/monacoEditor'; +import { waitForCondition } from '../common'; +import { createTemporaryFile } from '../utils/fs'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { takeSnapshot, writeDiffSnapshot } from './helpers'; +import { MockCustomEditorService } from './mockCustomEditorService'; +import { MockDocumentManager } from './mockDocumentManager'; +import { IMountedWebView, WaitForMessageOptions } from './mountedWebView'; +import { + addCell, + closeNotebook, + createNewEditor, + getNativeCellResults, + openEditor, + runMountedTest +} from './nativeEditorTestHelpers'; +import { createPythonService, startRemoteServer } from './remoteTestHelpers'; +import { + addContinuousMockData, + addMockData, + CellPosition, + enterEditorKey, + escapePath, + findButton, + getLastOutputCell, + getNativeFocusedEditor, + getOutputCell, + injectCode, + isCellFocused, + isCellMarkdown, + isCellSelected, + srcDirectory, + typeCode, + verifyCellIndex, + verifyCellSource, + verifyHtmlOnCell +} from './testHelpers'; +import { ITestNativeEditorProvider } from './testNativeEditorProvider'; + +use(chaiAsPromised); + +// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +async function updateFileConfig(ioc: DataScienceIocContainer, key: string, value: any) { + return ioc.get<IWorkspaceService>(IWorkspaceService).getConfiguration('file').update(key, value); +} +function waitForMessage(ioc: DataScienceIocContainer, message: string, options?: WaitForMessageOptions): Promise<void> { + return ioc.getNativeWebPanel(undefined).waitForMessage(message, options); +} +suite('DataScience Native Editor', () => { + const originalPlatform = window.navigator.platform; + Object.defineProperty( + window.navigator, + 'platform', + ((value: string) => { + return { + get: () => value, + set: (v: string) => (value = v) + }; + })(originalPlatform) + ); + + [false, true].forEach((useCustomEditorApi) => { + //import { asyncDump } from '../common/asyncDump'; + let snapshot: any; + suite(`${useCustomEditorApi ? 'With' : 'Without'} Custom Editor API`, () => { + function createFileCell(cell: any, data: any): ICell { + const newCell = { + type: 'preview', + id: 'FakeID', + file: Identifiers.EmptyFileName, + line: 0, + state: 2, + ...cell + }; + newCell.data = { + cell_type: 'code', + execution_count: null, + metadata: {}, + outputs: [], + source: '', + ...data + }; + + return newCell; + } + suiteSetup(() => { + snapshot = takeSnapshot(); + }); + suiteTeardown(() => { + writeDiffSnapshot(snapshot, `Native ${useCustomEditorApi}`); + }); + suite('Editor tests', () => { + const disposables: Disposable[] = []; + let ioc: DataScienceIocContainer; + let tempNotebookFile: { + filePath: string; + cleanupCallback: Function; + }; + + setup(async () => { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(useCustomEditorApi); + await ioc.activate(); + + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())) + .returns((_e) => Promise.resolve('')); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve('')); + appShell + .setup((a) => + a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2)); + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns((_a1: string, _a2: any, _a3: string, a4: string) => Promise.resolve(a4)); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(Uri.file('foo.ipynb'))); + ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); + tempNotebookFile = await createTemporaryFile('.ipynb'); + // Stub trustService.isNotebookTrusted. Some tests do not write to storage, + // so explicitly calling trustNotebook on the tempNotebookFile doesn't work + try { + sinon + .stub(ioc.serviceContainer.get<ITrustService>(ITrustService), 'isNotebookTrusted') + .resolves(true); + } catch (e) { + // tslint:disable-next-line: no-console + console.log(`Stub failure ${e}`); + } + }); + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + await ioc.dispose(); + try { + tempNotebookFile.cleanupCallback(); + } catch { + noop(); + } + }); + + // Uncomment this to debug hangs on exit + // suiteTeardown(() => { + // asyncDump(); + // }); + + runMountedTest('Simple text', async () => { + // Create an editor so something is listening to messages + const { mount } = await createNewEditor(ioc); + + // Add a cell into the UI and wait for it to render + await addCell(mount, 'a=1\na'); + + verifyHtmlOnCell(mount.wrapper, 'NativeCell', '<span>1</span>', 1); + }); + + runMountedTest('Invalid session still runs', async (context) => { + if (ioc.mockJupyter) { + // Can only do this with the mock. Have to force the first call to waitForIdle on the + // the jupyter session to fail + ioc.mockJupyter.forcePendingIdleFailure(); + + // Create an editor so something is listening to messages + const { mount } = await createNewEditor(ioc); + + // Run the first cell. Should fail but then ask for another + await addCell(mount, 'a=1\na'); + + verifyHtmlOnCell(mount.wrapper, 'NativeCell', '<span>1</span>', 1); + } else { + context.skip(); + } + }); + + runMountedTest('Save on close', async (_context) => { + // Close should cause the save as to come up. Remap appshell so we can check + const dummyDisposable = { + dispose: () => { + return; + } + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())) + .returns((e) => { + throw e; + }); + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns((_a1, _a2, a3, _a4) => Promise.resolve(a3)); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve(Uri.file(tempNotebookFile.filePath)); + }); + appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); + + // Create an editor + const ne = await createNewEditor(ioc); + + // Add a cell + await addCell(ne.mount, 'a=1\na'); + + // Close the editor. It should ask for save as (if not custom editor) + if (useCustomEditorApi) { + // For custom editor do what VS code would do on close + const notebookEditorProvider = ioc.get<ITestNativeEditorProvider>(INotebookEditorProvider); + const customDoc = notebookEditorProvider.getCustomDocument(ne.editor.file); + assert.ok(customDoc, 'No custom document for new notebook'); + const customEditorProvider = (notebookEditorProvider as any) as CustomEditorProvider; + await customEditorProvider.saveCustomDocumentAs( + customDoc!, + Uri.file(tempNotebookFile.filePath), + CancellationToken.None + ); + } + await ne.editor.dispose(); + + // Open the temp file to make sure it has the new cell + const opened = await openEditor(ioc, '', tempNotebookFile.filePath); + + verifyCellSource(opened.mount.wrapper, 'NativeCell', 'a=1\na', CellPosition.Last); + }); + + function getHashedFileName(file: Uri): string { + const crypto = ioc.get<ICryptoUtils>(ICryptoUtils); + const context = ioc.get<IExtensionContext>(IExtensionContext); + const key = `${KeyPrefix}${file.toString()}`; + const name = `${crypto.createHash(key, 'string')}.ipynb`; + return path.join(context.globalStoragePath, name); + } + + runMountedTest('Save on shutdown', async (context) => { + // Skip this test is using custom editor. VS code handles this situation + if (useCustomEditorApi) { + context.skip(); + } else { + // When we dispose act like user wasn't able to hit anything + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())) + .returns((e) => { + throw e; + }); + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns((_a1, _a2, _a3, _a4) => Promise.resolve(undefined)); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve(Uri.file(tempNotebookFile.filePath)); + }); + ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); + + // Turn off auto save so that backup works. + await updateFileConfig(ioc, 'autoSave', 'off'); + + // Create an editor with a specific path + const ne = await openEditor(ioc, '', tempNotebookFile.filePath); + + // Figure out the backup file name + const deferred = createDeferred<boolean>(); + const backupFileName = getHashedFileName(Uri.file(tempNotebookFile.filePath)); + fs.watchFile(backupFileName, (c, p) => { + if (p.mtime < c.mtime) { + deferred.resolve(true); + } + }); + + try { + // Add a cell + await addCell(ne.mount, 'a=1\na'); + + // Wait for write. It should have written to backup + const result = await waitForPromise(deferred.promise, 5000); + assert.ok(result, 'Backup file did not write'); + + // Prevent reopen (we want to act like shutdown) + (ne.editor as any).reopen = noop; + await closeNotebook(ioc, ne.editor); + } finally { + fs.unwatchFile(backupFileName); + } + + // Reopen and verify + const opened = await openEditor(ioc, '', tempNotebookFile.filePath); + verifyCellSource(opened.mount.wrapper, 'NativeCell', 'a=1\na', CellPosition.Last); + } + }); + + runMountedTest('Invalid kernel still runs', async (context) => { + if (ioc.mockJupyter) { + const kernelDesc = { + name: 'foobar', + display_name: 'foobar' + }; + const invalidKernel = { + name: 'foobar', + display_name: 'foobar', + language: 'python', + path: '/foo/bar/python', + argv: [], + env: undefined + }; + + // Allow the invalid kernel to be used + const kernelServiceMock = ioc.kernelService; + when( + kernelServiceMock.findMatchingKernelSpec( + objectContaining(kernelDesc), + anything(), + anything() + ) + ).thenResolve(invalidKernel); + + // Can only do this with the mock. Have to force the first call to changeKernel on the + // the jupyter session to fail + ioc.mockJupyter.forcePendingKernelChangeFailure(); + + // Create an editor so something is listening to messages + const ne = await createNewEditor(ioc); + + // Force an update to the editor so that it has a new kernel + const editor = (ne.editor as any) as NativeEditorWebView; + await editor.updateNotebookOptions({ kernelSpec: invalidKernel, kind: 'startUsingKernelSpec' }); + + // Run the first cell. Should fail but then ask for another + await addCell(ne.mount, 'a=1\na'); + + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', '<span>1</span>', 1); + } else { + context.skip(); + } + }); + + runMountedTest('Invalid kernel can be switched', async (context) => { + if (ioc.mockJupyter) { + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath, { + ...ioc.getSettings().datascience, + jupyterLaunchRetries: 1, + disableJupyterAutoStart: true + }); + + // Can only do this with the mock. Have to force the first call to idle on the + // the jupyter session to fail + ioc.mockJupyter.forcePendingIdleFailure(); + + // Create an editor so something is listening to messages + const ne = await createNewEditor(ioc); + + // Run a cell. It should fail. + await addCell(ne.mount, 'a=1\na'); + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', undefined, 1); + + // Now switch to another kernel + ((ne.editor as any) as NativeEditorWebView).onMessage( + InteractiveWindowMessages.SelectKernel, + undefined + ); + + // Verify we picked the valid kernel. + await addCell(ne.mount, 'a=1\na'); + + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', '<span>1</span>', 2); + } else { + context.skip(); + } + }); + + runMountedTest('Remote kernel can be switched and remembered', async () => { + // Turn off raw kernel for this test as it's testing remote + ioc.setExperimentState(LocalZMQKernel.experiment, false); + + const pythonService = await createPythonService(ioc, 2); + + // Skip test for older python and raw kernel and mac + if (pythonService && os.platform() !== 'darwin' && !ioc.mockJupyter) { + const uri = await startRemoteServer(ioc, pythonService, [ + '-m', + 'jupyter', + 'notebook', + '--NotebookApp.open_browser=False', + '--NotebookApp.ip=*', + '--NotebookApp.port=9999' + ]); + + // Set this as the URI to use when connecting + ioc.forceDataScienceSettingsChanged({ jupyterServerURI: uri }); + + // Create a notebook and run a cell. + const notebook = await createNewEditor(ioc); + await addCell(notebook.mount, 'a=12\na', true); + verifyHtmlOnCell(notebook.mount.wrapper, 'NativeCell', '12', CellPosition.Last); + + // Create another notebook and connect it to the already running kernel of the other one + when(ioc.applicationShell.showQuickPick(anything(), anything(), anything())).thenCall( + async (o: IKernelSpecQuickPickItem[]) => { + const existing = o.filter( + (s) => + s.selection.kind === 'connectToLiveKernel' && + s.selection.kernelModel.numberOfConnections + ); + + // Might be more than one. Get the oldest one. It has the actual activity. + const sorted = existing.sort((a, b) => { + if ( + a.selection.kind !== 'connectToLiveKernel' || + b.selection.kind !== 'connectToLiveKernel' + ) { + return 0; + } + return ( + b.selection.kernelModel.lastActivityTime.getTime() - + a.selection.kernelModel.lastActivityTime.getTime() + ); + }); + if (sorted && sorted.length) { + return sorted[0]; + } + } + ); + const n2 = await openEditor(ioc, '', 'kernel_share.ipynb'); + + // Have to do this by sending the switch kernel command + await ioc.get<ICommandManager>(ICommandManager).executeCommand(Commands.SwitchJupyterKernel, { + identity: n2.editor.file, + resource: n2.editor.file, + currentKernelDisplayName: undefined + }); + + // Execute a cell that should indicate using the same kernel as the first notebook + await addCell(n2.mount, 'a', true); + verifyHtmlOnCell(n2.mount.wrapper, 'NativeCell', '12', CellPosition.Last); + + // Now close the notebook and reopen. Should still be using the same kernel + await closeNotebook(ioc, n2.editor); + const n3 = await openEditor(ioc, '', 'kernel_share.ipynb'); + await addCell(n3.mount, 'a', true); + verifyHtmlOnCell(n3.mount.wrapper, 'NativeCell', '12', CellPosition.Last); + } + }); + + runMountedTest('Mime Types', async () => { + // Create an editor so something is listening to messages + await createNewEditor(ioc); + + const badPanda = `import pandas as pd +df = pd.read("${escapePath(path.join(srcDirectory(), 'DefaultSalesReport.csv'))}") +df.head()`; + const goodPanda = `import pandas as pd +df = pd.read_csv("${escapePath(path.join(srcDirectory(), 'DefaultSalesReport.csv'))}") +df.head()`; + const matPlotLib = + 'import matplotlib.pyplot as plt\r\nimport numpy as np\r\nx = np.linspace(0,20,100)\r\nplt.plot(x, np.sin(x))\r\nplt.show()'; + const matPlotLibResults = 'img'; + const spinningCursor = dedent`import sys + import time + def spinning_cursor(): + while True: + for cursor in '|/-\\\\': + yield cursor + spinner = spinning_cursor() + for _ in range(50): + sys.stdout.write(next(spinner)) + sys.stdout.flush() + time.sleep(0.1) + sys.stdout.write('\\r')`; + const alternating = `from IPython.display import display\r\nprint('foo')\r\ndisplay('foo')\r\nprint('bar')\r\ndisplay('bar')`; + const alternatingResults = ['foo\n', 'foo', 'bar\n', 'bar']; + + const clearalternating = `from IPython.display import display, clear_output\r\nprint('foo')\r\ndisplay('foo')\r\nclear_output(True)\r\nprint('bar')\r\ndisplay('bar')`; + const clearalternatingResults = ['foo\n', 'foo', '', 'bar\n', 'bar']; + + addMockData(ioc, badPanda, `pandas has no attribute 'read'`, 'text/html', 'error'); + addMockData(ioc, goodPanda, `<td>A table</td>`, 'text/html'); + addMockData(ioc, matPlotLib, matPlotLibResults, 'text/html'); + addMockData(ioc, alternating, alternatingResults, ['text/plain', 'stream', 'text/plain', 'stream']); + addMockData(ioc, clearalternating, clearalternatingResults, [ + 'text/plain', + 'stream', + 'clear_true', + 'text/plain', + 'stream' + ]); + const cursors = ['|', '/', '-', '\\']; + let cursorPos = 0; + let loops = 3; + addContinuousMockData(ioc, spinningCursor, async (_c) => { + const result = `${cursors[cursorPos]}\r`; + cursorPos += 1; + if (cursorPos >= cursors.length) { + cursorPos = 0; + loops -= 1; + } + return Promise.resolve({ result: result, haveMore: loops > 0 }); + }); + + const mount = ioc.getNativeWebPanel(undefined); + const wrapper = mount.wrapper; + await addCell(mount, badPanda, true); + verifyHtmlOnCell(wrapper, 'NativeCell', `has no attribute 'read'`, CellPosition.Last); + + await addCell(mount, goodPanda, true); + verifyHtmlOnCell(wrapper, 'NativeCell', `<td>`, CellPosition.Last); + + await addCell(mount, matPlotLib, true); + verifyHtmlOnCell(wrapper, 'NativeCell', /img|Figure/, CellPosition.Last); + + await addCell(mount, spinningCursor, true); + verifyHtmlOnCell(wrapper, 'NativeCell', '<div>', CellPosition.Last); + + await addCell(mount, alternating, true); + verifyHtmlOnCell(wrapper, 'NativeCell', /.*foo\n.*foo.*bar\n.*bar/m, CellPosition.Last); + await addCell(mount, clearalternating, true); + verifyHtmlOnCell(wrapper, 'NativeCell', /.*bar\n.*bar/m, CellPosition.Last); + }); + + runMountedTest('Click buttons', async () => { + // Goto source should cause the visible editor to be picked as long as its filename matches + const showedEditor = createDeferred(); + const textEditors: TextEditor[] = []; + const docManager = TypeMoq.Mock.ofType<IDocumentManager>(); + const visibleEditor = TypeMoq.Mock.ofType<TextEditor>(); + const dummyDocument = TypeMoq.Mock.ofType<TextDocument>(); + dummyDocument.setup((d) => d.fileName).returns(() => Uri.file('foo.py').fsPath); + visibleEditor.setup((v) => v.show()).returns(() => showedEditor.resolve()); + visibleEditor.setup((v) => v.revealRange(TypeMoq.It.isAny())).returns(noop); + visibleEditor.setup((v) => v.document).returns(() => dummyDocument.object); + textEditors.push(visibleEditor.object); + docManager.setup((a) => a.visibleTextEditors).returns(() => textEditors); + ioc.serviceManager.rebindInstance<IDocumentManager>(IDocumentManager, docManager.object); + // Create an editor so something is listening to messages + const ne = await createNewEditor(ioc); + const wrapper = ne.mount.wrapper; + + // Get a cell into the list + await addCell(ne.mount, 'a=1\na'); + + // find the buttons on the cell itself + let cell = getLastOutputCell(wrapper, 'NativeCell'); + let ImageButtons = cell.find(ImageButton); + assert.equal(ImageButtons.length, 7, 'Cell buttons not found'); // Note, run by line is there as a button, it's just disabled. + let deleteButton = ImageButtons.at(6); + + // Make sure delete works + let afterDelete = await getNativeCellResults(ne.mount, async () => { + deleteButton.simulate('click'); + return Promise.resolve(); + }); + assert.equal(afterDelete.length, 1, `Delete should remove a cell`); + + // Secondary delete should NOT delete the cell as there should ALWAYS be at + // least one cell in the file. + cell = getLastOutputCell(wrapper, 'NativeCell'); + ImageButtons = cell.find(ImageButton); + assert.equal(ImageButtons.length, 7, 'Cell buttons not found'); + deleteButton = ImageButtons.at(6); + + afterDelete = await getNativeCellResults( + ne.mount, + async () => { + deleteButton.simulate('click'); + return Promise.resolve(); + }, + () => Promise.resolve() + ); + assert.equal(afterDelete.length, 1, `Delete should NOT remove the last cell`); + }); + + runMountedTest('Select Jupyter Server', async () => { + // tslint:disable-next-line: no-console + console.log('Test skipped until user can change jupyter server selection again'); + // let selectorCalled = false; + + // ioc.datascience.setup(ds => ds.selectJupyterURI()).returns(() => { + // selectorCalled = true; + // return Promise.resolve(); + // }); + + // await createNewEditor(ioc); + // const editor = wrapper.find(NativeEditor); + // const kernelSelectionUI = editor.find(KernelSelection); + // const buttons = kernelSelectionUI.find('div'); + // buttons!.at(1).simulate('click'); + + // assert.equal(selectorCalled, true, 'Server Selector should have been called'); + }); + + runMountedTest('Select Jupyter Kernel', async (_wrapper) => { + // tslint:disable-next-line: no-console + console.log('Tests skipped, as we need better tests'); + // let selectorCalled = false; + + // ioc.datascience.setup(ds => ds.selectLocalJupyterKernel()).returns(() => { + // selectorCalled = true; + // const spec: kernelConnectionMetadata = {}; + // return Promise.resolve(spec); + // }); + + // await createNewEditor(ioc); + // // Create an editor so something is listening to messages + // await createNewEditor(ioc); + + // // Add a cell into the UI and wait for it to render + // await addCell(mount, 'a=1\na'); + + // const editor = wrapper.find(NativeEditor); + // const kernelSelectionUI = editor.find(KernelSelection); + // const buttons = kernelSelectionUI.find('div'); + // buttons!.at(4).simulate('click'); + + // assert.equal(selectorCalled, true, 'Kernel Selector should have been called'); + }); + + runMountedTest('Server already loaded', async (context) => { + if (ioc.mockJupyter) { + await ioc.activate(); + ioc.forceDataScienceSettingsChanged({ + disableJupyterAutoStart: false + }); + + // Create an editor so something is listening to messages + const editor = await createNewEditor(ioc); + + // Wait a bit to let async activation to work + await sleep(2000); + + // Make sure it has a server + assert.ok(editor.editor.notebook, 'Notebook did not start with a server'); + } else { + context.skip(); + } + }); + + runMountedTest('Server load skipped', async (context) => { + if (ioc.mockJupyter) { + ioc.getSettings().datascience.disableJupyterAutoStart = true; + await ioc.activate(); + + // Create an editor so something is listening to messages + const editor = await createNewEditor(ioc); + + // Wait a bit to let async activation to work + await sleep(500); + + // Make sure it does not have a server + assert.notOk(editor.editor.notebook, 'Notebook should not start with a server'); + } else { + context.skip(); + } + }); + + runMountedTest('Convert to python', async () => { + // Export should cause the export dialog to come up. Remap appshell so we can check + const dummyDisposable = { + dispose: () => { + return; + } + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())) + .returns((e) => { + throw e; + }); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve('')); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve(Uri.file(tempNotebookFile.filePath)); + }); + appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); + + // Make sure to create the interactive window after the rebind or it gets the wrong application shell. + const ne = await createNewEditor(ioc); + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + await addCell(ne.mount, 'a=1\na'); + await dirtyPromise; + + // Export should cause exportCalled to change to true + const saveButton = findButton(ne.mount.wrapper, NativeEditor, 8); + const saved = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + saveButton!.simulate('click'); + await saved; + + // Click export and wait for a document to change + const commandFired = createDeferred(); + const commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + const editor = TypeMoq.Mock.ofType<INotebookEditorProvider>().object.activeEditor; + const model = editor!.model!; + ioc.serviceManager.rebindInstance<ICommandManager>(ICommandManager, commandManager.object); + commandManager + .setup((cmd) => cmd.executeCommand(Commands.Export, model, undefined)) + .returns(() => { + commandFired.resolve(); + return Promise.resolve(); + }); + + const exportButton = findButton(ne.mount.wrapper, NativeEditor, 9); + exportButton!.simulate('click'); + + // This can be slow, hence wait for a max of 60. + await waitForPromise(commandFired.promise, 60_000); + }); + + runMountedTest('Save As', async () => { + if (useCustomEditorApi) { + return; + } + const initialFileContents = (await fs.readFile(tempNotebookFile.filePath, 'utf8')).toString(); + // Export should cause the export dialog to come up. Remap appshell so we can check + const dummyDisposable = { + dispose: () => { + return; + } + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())) + .returns((e) => { + throw e; + }); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve('')); + appShell + .setup((a) => a.showSaveDialog(TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve(Uri.file(tempNotebookFile.filePath)); + }); + appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); + ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); + + // Make sure to create the interactive window after the rebind or it gets the wrong application shell. + const ne = await createNewEditor(ioc); + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + await addCell(ne.mount, 'a=1\na'); + await dirtyPromise; + + // Export should cause exportCalled to change to true + const saveButton = findButton(ne.mount.wrapper, NativeEditor, 8); + const saved = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + saveButton!.simulate('click'); + await saved; + + const newFileContents = (await fs.readFile(tempNotebookFile.filePath, 'utf8')).toString(); + // File should have been modified. + assert.notEqual(initialFileContents, newFileContents); + // Should be a valid json with 2 cells. + const nbContent = JSON.parse(newFileContents) as nbformat.INotebookContent; + assert.equal(nbContent.cells.length, 2); + }); + + runMountedTest('RunAllCells', async () => { + // Make sure we don't write to storage for the notebook. It messes up other tests + await updateFileConfig(ioc, 'autoSave', 'onFocusChange'); + addMockData(ioc, 'print(1)\na=1', 1); + addMockData(ioc, 'a=a+1\nprint(a)', 2); + addMockData(ioc, 'print(a+1)', 3); + + const baseFile = [ + { id: 'NotebookImport#0', data: { source: 'print(1)\na=1' } }, + { id: 'NotebookImport#1', data: { source: 'a=a+1\nprint(a)' } }, + { id: 'NotebookImport#2', data: { source: 'print(a+1)' } } + ]; + const runAllCells = baseFile.map((cell) => { + return createFileCell(cell, cell.data); + }); + const notebook = await ioc + .get<INotebookExporter>(INotebookExporter) + .translateToNotebook(runAllCells, undefined); + const ne = await openEditor(ioc, JSON.stringify(notebook)); + + const runAllButton = findButton(ne.mount.wrapper, NativeEditor, 0); + // The render method needs to be executed 3 times for three cells. + const threeCellsUpdated = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { + numberOfTimes: 3 + }); + runAllButton!.simulate('click'); + await threeCellsUpdated; + + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', `1`, 0); + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', `2`, 1); + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', `3`, 2); + }); + + runMountedTest('Startup and shutdown', async () => { + // Turn off raw kernel for this test as it's testing jupyterserver start / shutdown + ioc.setExperimentState(LocalZMQKernel.experiment, false); + addMockData(ioc, 'b=2\nb', 2); + addMockData(ioc, 'c=3\nc', 3); + + const baseFile = [ + { id: 'NotebookImport#0', data: { source: 'a=1\na' } }, + { id: 'NotebookImport#1', data: { source: 'b=2\nb' } }, + { id: 'NotebookImport#2', data: { source: 'c=3\nc' } } + ]; + const runAllCells = baseFile.map((cell) => { + return createFileCell(cell, cell.data); + }); + const notebook = await ioc + .get<INotebookExporter>(INotebookExporter) + .translateToNotebook(runAllCells, undefined); + let editor = await openEditor(ioc, JSON.stringify(notebook)); + + // Run everything + let threeCellsUpdated = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { + numberOfTimes: 3 + }); + let runAllButton = findButton(editor.mount.wrapper, NativeEditor, 0); + runAllButton!.simulate('click'); + await threeCellsUpdated; + + // Close editor. Should still have the server up + await closeNotebook(ioc, editor.editor); + const jupyterExecution = ioc.serviceManager.get<IJupyterExecution>(IJupyterExecution); + const server = await jupyterExecution.getServer({ + allowUI: () => false, + purpose: Identifiers.HistoryPurpose + }); + assert.ok(server, 'Server was destroyed on notebook shutdown'); + + // Reopen, and rerun + editor = await openEditor(ioc, JSON.stringify(notebook)); + + threeCellsUpdated = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { + numberOfTimes: 3 + }); + runAllButton = findButton(editor.mount.wrapper, NativeEditor, 0); + runAllButton!.simulate('click'); + await threeCellsUpdated; + verifyHtmlOnCell(editor.mount.wrapper, 'NativeCell', `1`, 0); + }); + + test('Failure', async () => { + let fail = true; + const errorThrownDeferred = createDeferred<Error>(); + + // Turn off raw kernel for this test as it's testing jupyter usable error + ioc.setExperimentState(LocalZMQKernel.experiment, false); + + // REmap the functions in the execution and error handler. Note, we can't rebind them as + // they've already been injected into the INotebookProvider + const execution = ioc.serviceManager.get<IJupyterExecution>(IJupyterExecution); + const errorHandler = ioc.serviceManager.get<IDataScienceErrorHandler>(IDataScienceErrorHandler); + const originalGetUsable = execution.getUsableJupyterPython.bind(execution); + execution.getUsableJupyterPython = () => { + if (fail) { + return Promise.resolve(undefined); + } + return originalGetUsable(); + }; + errorHandler.handleError = (exc: Error) => { + errorThrownDeferred.resolve(exc); + return Promise.resolve(); + }; + + addMockData(ioc, 'a=1\na', 1); + const ne = await createNewEditor(ioc); + const result = await Promise.race([addCell(ne.mount, 'a=1\na', true), errorThrownDeferred.promise]); + assert.ok(result, 'Error not found'); + assert.ok(result instanceof Error, 'Error not found'); + + // Fix failure and try again + fail = false; + const cell = getOutputCell(ne.mount.wrapper, 'NativeCell', 1); + assert.ok(cell, 'Cannot find the first cell'); + const imageButtons = cell!.find(ImageButton); + assert.equal(imageButtons.length, 7, 'Cell buttons not found'); + const runButton = imageButtons.findWhere((w) => w.props().tooltip === 'Run cell'); + assert.equal(runButton.length, 1, 'No run button found'); + const update = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { + numberOfTimes: 3 + }); + runButton.simulate('click'); + await update; + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', `1`, 1); + }); + }); + + suite('Editor tests', () => { + let wrapper: ReactWrapper<any, Readonly<{}>, React.Component>; + let mount: IMountedWebView; + const disposables: Disposable[] = []; + let ioc: DataScienceIocContainer; + const baseFile = ` +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a=1\\n", + "a" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b=2\\n", + "b" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c=3\\n", + "c" + ] + } + ], + "metadata": { + "file_extension": ".py", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.4" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +}`; + const addedJSON = JSON.parse(baseFile); + addedJSON.cells.splice(3, 0, { + cell_type: 'code', + execution_count: null, + metadata: {}, + outputs: [], + source: ['a'] + }); + + const addedJSONFile = JSON.stringify(addedJSON, null, ' '); + + let notebookFile: { + filePath: string; + cleanupCallback: Function; + }; + function initIoc() { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(useCustomEditorApi); + return ioc.activate(); + } + async function setupFunction(this: Mocha.Context, fileContents?: any) { + addMockData(ioc, 'b=2\nb', 2); + addMockData(ioc, 'c=3\nc', 3); + // Use a real file so we can save notebook to a file. + // This is used in some tests (saving). + notebookFile = await createTemporaryFile('.ipynb'); + await fs.writeFile(notebookFile.filePath, fileContents ? fileContents : baseFile); + const ne = await openEditor(ioc, fileContents ? fileContents : baseFile, notebookFile.filePath); + wrapper = ne.mount.wrapper; + mount = ne.mount; + } + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + if (ioc) { + await ioc.dispose(); + } + try { + notebookFile.cleanupCallback(); + } catch { + noop(); + } + }); + + function clickCell(cellIndex: number) { + wrapper.update(); + wrapper.find(NativeCell).at(cellIndex).simulate('click'); + wrapper.update(); + } + + function simulateKeyPressOnCell( + cellIndex: number, + keyboardEvent: Partial<IKeyboardEvent> & { code: string } + ) { + // Check to see if we have an active focused editor + const editor = getNativeFocusedEditor(wrapper); + + // If we do have one, send the input there, otherwise send it to the outer cell + if (editor) { + simulateKeyPressOnEditor(editor, keyboardEvent); + } else { + simulateKeyPressOnCellInner(cellIndex, keyboardEvent); + } + } + + async function addMarkdown(code: string): Promise<void> { + const totalCells = wrapper.find('NativeCell').length; + const newCellIndex = totalCells; + await addCell(mount, code, false); + assert.equal(wrapper.find('NativeCell').length, totalCells + 1); + + // First lose focus + clickCell(newCellIndex); + let update = waitForMessage(ioc, InteractiveWindowMessages.UnfocusedCellEditor); + simulateKeyPressOnCell(1, { code: 'Escape' }); + await update; + + // Switch to markdown + update = waitForMessage(ioc, CommonActionType.CHANGE_CELL_TYPE); + simulateKeyPressOnCell(newCellIndex, { code: 'm' }); + await update; + + clickCell(newCellIndex); + + // Monaco editor should be rendered and the cell should be markdown + assert.ok(!isCellFocused(wrapper, 'NativeCell', newCellIndex)); + assert.ok(isCellMarkdown(wrapper, 'NativeCell', newCellIndex)); + } + + function simulateKeyPressOnEditor( + editorControl: ReactWrapper<any, Readonly<{}>, React.Component> | undefined, + keyboardEvent: Partial<IKeyboardEvent> & { code: string } + ) { + enterEditorKey(editorControl, keyboardEvent); + } + + function simulateKeyPressOnCellInner( + cellIndex: number, + keyboardEvent: Partial<IKeyboardEvent> & { code: string } + ) { + wrapper.update(); + let nativeCell = wrapper.find(NativeCell).at(cellIndex); + if (nativeCell.exists()) { + nativeCell.simulate('keydown', { + key: keyboardEvent.code, + shiftKey: keyboardEvent.shiftKey, + ctrlKey: keyboardEvent.ctrlKey, + altKey: keyboardEvent.altKey, + metaKey: keyboardEvent.metaKey + }); + } + wrapper.update(); + // Requery for our cell as something like a 'dd' keydown command can delete it before the press and up + nativeCell = wrapper.find(NativeCell).at(cellIndex); + if (nativeCell.exists()) { + nativeCell.simulate('keypress', { + key: keyboardEvent.code, + shiftKey: keyboardEvent.shiftKey, + ctrlKey: keyboardEvent.ctrlKey, + altKey: keyboardEvent.altKey, + metaKey: keyboardEvent.metaKey + }); + } + nativeCell = wrapper.find(NativeCell).at(cellIndex); + wrapper.update(); + if (nativeCell.exists()) { + nativeCell.simulate('keyup', { + key: keyboardEvent.code, + shiftKey: keyboardEvent.shiftKey, + ctrlKey: keyboardEvent.ctrlKey, + altKey: keyboardEvent.altKey, + metaKey: keyboardEvent.metaKey + }); + } + wrapper.update(); + } + + suite('Selection/Focus', () => { + setup(async function () { + await initIoc(); + // tslint:disable-next-line: no-invalid-this + await setupFunction.call(this); + }); + test('None of the cells are selected by default', async () => { + assert.ok(!isCellSelected(wrapper, 'NativeCell', 0)); + assert.ok(!isCellSelected(wrapper, 'NativeCell', 1)); + assert.ok(!isCellSelected(wrapper, 'NativeCell', 2)); + }); + + test('None of the cells are not focused by default', async () => { + assert.ok(!isCellFocused(wrapper, 'NativeCell', 0)); + assert.ok(!isCellFocused(wrapper, 'NativeCell', 1)); + assert.ok(!isCellFocused(wrapper, 'NativeCell', 2)); + }); + + test('Select cells by clicking them', async () => { + // Click first cell, then second, then third. + clickCell(0); + assert.ok(isCellSelected(wrapper, 'NativeCell', 0)); + assert.equal(isCellSelected(wrapper, 'NativeCell', 1), false); + assert.equal(isCellSelected(wrapper, 'NativeCell', 2), false); + + clickCell(1); + assert.ok(isCellSelected(wrapper, 'NativeCell', 1)); + assert.equal(isCellSelected(wrapper, 'NativeCell', 0), false); + assert.equal(isCellSelected(wrapper, 'NativeCell', 2), false); + + clickCell(2); + assert.ok(isCellSelected(wrapper, 'NativeCell', 2)); + assert.equal(isCellSelected(wrapper, 'NativeCell', 0), false); + assert.equal(isCellSelected(wrapper, 'NativeCell', 1), false); + }); + + test('Markdown saved when selecting another cell', async () => { + clickCell(0); + + // Switch to markdown + let update = waitForMessage(ioc, CommonActionType.CHANGE_CELL_TYPE); + simulateKeyPressOnCell(0, { code: 'm' }); + await update; + + // Monaco editor should be rendered and the cell should be markdown + assert.ok(!isCellFocused(wrapper, 'NativeCell', 0)); + assert.ok(isCellMarkdown(wrapper, 'NativeCell', 0)); + + // Focus the cell. + update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + simulateKeyPressOnCell(0, { code: 'Enter', editorInfo: undefined }); + await update; + + assert.ok(isCellFocused(wrapper, 'NativeCell', 0)); + assert.equal(wrapper.find(NativeCell).at(0).find(MonacoEditor).length, 1); + + // Verify cell content + const currentEditor = getNativeFocusedEditor(wrapper); + const reactEditor = currentEditor!.instance() as MonacoEditor; + const editor = reactEditor.state.editor; + if (editor) { + assert.equal( + editor.getModel()!.getValue(), + 'a=1\na', + 'Incorrect editor text in markdown cell' + ); + } + + typeCode(currentEditor, 'world'); + + if (editor) { + assert.equal( + editor.getModel()!.getValue(), + 'worlda=1\na', + 'Incorrect editor text in markdown cell' + ); + } + + // Now get the editor for the next cell and click it + update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + clickCell(1); + await update; + + // Look back at the output for the first cell, not focused, not selected, text saved in output + assert.equal(isCellSelected(wrapper, 'NativeCell', 0), false); + assert.equal(isCellFocused(wrapper, 'NativeCell', 0), false); + + verifyHtmlOnCell(wrapper, 'NativeCell', '<p>worlda=1\na</p>', 0); + }); + }); + + suite('Model updates', () => { + setup(async function () { + await initIoc(); + // tslint:disable-next-line: no-invalid-this + await setupFunction.call(this); + }); + async function undo(): Promise<void> { + const uri = Uri.file(notebookFile.filePath); + const update = waitForMessage(ioc, InteractiveWindowMessages.ReceivedUpdateModel); + const editorService = ioc.serviceManager.get<ICustomEditorService>( + ICustomEditorService + ) as MockCustomEditorService; + editorService.undo(uri); + return update; + } + async function redo(): Promise<void> { + const uri = Uri.file(notebookFile.filePath); + const update = waitForMessage(ioc, InteractiveWindowMessages.ReceivedUpdateModel); + const editorService = ioc.serviceManager.get<ICustomEditorService>( + ICustomEditorService + ) as MockCustomEditorService; + editorService.redo(uri); + return update; + } + test('Add a cell and undo', async () => { + // Add empty cell, else adding text is yet another thing that needs to be undone, + // we have tests for that. + await addCell(mount, '', false); + + // Should have 4 cells + assert.equal(wrapper.find('NativeCell').length, 4, 'Cell not added'); + + // Send undo through the custom editor + await undo(); + + // Should have 3 + assert.equal(wrapper.find('NativeCell').length, 3, 'Cell not removed'); + }); + test('Edit a cell and undo', async () => { + await addCell(mount, '', false); + + // Should have 4 cells + assert.equal(wrapper.find('NativeCell').length, 4, 'Cell not added'); + + // Change the contents of the cell + const editorEnzyme = getNativeFocusedEditor(wrapper); + + // Type in something with brackets + typeCode(editorEnzyme, 'some more'); + + // Verify cell content + const reactEditor = editorEnzyme!.instance() as MonacoEditor; + const editor = reactEditor.state.editor; + if (editor) { + assert.equal(editor.getModel()!.getValue(), 'some more', 'Text does not match'); + } + + // Add a new cell + await addCell(mount, '', false); + + // Send undo a bunch of times. Should undo the add and the edits + await undo(); + await undo(); + await undo(); + + // Should have four again + assert.equal(wrapper.find('NativeCell').length, 4, 'Cell not removed on undo'); + + // Should have different content + if (editor) { + assert.equal(editor.getModel()!.getValue(), 'some mo', 'Text does not match after undo'); + } + + // Send redo to see if goes back + await redo(); + if (editor) { + assert.equal(editor.getModel()!.getValue(), 'some mor', 'Text does not match'); + } + + // Send redo to see if goes back + await redo(); + await redo(); + assert.equal(wrapper.find('NativeCell').length, 5, 'Cell not readded on redo'); + }); + test('Remove, move, and undo', async () => { + await addCell(mount, '', false); + + // Should have 4 cells + assert.equal(wrapper.find('NativeCell').length, 4, 'Cell not added'); + + // Delete the cell + let cell = getLastOutputCell(wrapper, 'NativeCell'); + let imageButtons = cell.find(ImageButton); + assert.equal(imageButtons.length, 7, 'Cell buttons not found'); + const deleteButton = imageButtons.at(6); + const afterDelete = await getNativeCellResults(mount, async () => { + deleteButton.simulate('click'); + return Promise.resolve(); + }); + // Should have 3 cells + assert.equal(afterDelete.length, 3, 'Cell not deleted'); + + // Undo the delete + await undo(); + + // Should have 4 cells again + assert.equal(wrapper.find('NativeCell').length, 4, 'Cell delete not undone'); + + // Redo the delete + await redo(); + + // Should have 3 cells again + assert.equal(wrapper.find('NativeCell').length, 3, 'Cell delete not redone'); + + // Move some cells around + cell = getLastOutputCell(wrapper, 'NativeCell'); + imageButtons = cell.find(ImageButton); + assert.equal(imageButtons.length, 7, 'Cell buttons not found'); + const moveUpButton = imageButtons.at(0); + const afterMove = await getNativeCellResults(mount, async () => { + moveUpButton.simulate('click'); + return Promise.resolve(); + }); + + let foundCell = getOutputCell(afterMove, 'NativeCell', 2)?.instance() as NativeCell; + assert.equal(foundCell.props.cellVM.cell.id, 'NotebookImport#1', 'Cell did not move'); + await undo(); + foundCell = getOutputCell(wrapper, 'NativeCell', 2)?.instance() as NativeCell; + assert.equal(foundCell.props.cellVM.cell.id, 'NotebookImport#2', 'Cell did not move back'); + }); + + test('Update as user types into editor (update redux store and model)', async () => { + const cellIndex = 3; + await addCell(mount, '', false); + assert.ok(isCellFocused(wrapper, 'NativeCell', cellIndex)); + assert.equal(wrapper.find('NativeCell').length, 4, 'Cell not added'); + + const notebookEditorProvider = ioc.get<INotebookEditorProvider>(INotebookEditorProvider); + const model = (notebookEditorProvider.editors[0] as NativeEditorWebView).model; + + // This is the string the user will type in a character at a time into the editor. + const stringToType = 'Hi! Bob!'; + + // We are expecting to receive multiple edits to the model in the backend/extension from react, one for each character. + // Lets create deferreds that we can await on, and each will be resolved with the edit it received. + // For first edit, we'll expect `H`, then `i`, then `!` + const modelEditsInExtension = stringToType.split('').map(createDeferred); + model?.changed((e) => { + if (e.kind === 'edit') { + // Find the first deferred that's no completed. + const deferred = modelEditsInExtension.find((d) => !d.completed); + // Resolve promise with the character/string it received as edit. + deferred?.resolve(e.forward.map((m) => m.text).join('')); + } + }); + + for (let index = 0; index < stringToType.length; index += 1) { + // Single character to be typed into the editor. + const characterToTypeIntoEditor = stringToType.substring(index, index + 1); + + // Type a character into the editor. + const editorEnzyme = getNativeFocusedEditor(wrapper); + typeCode(editorEnzyme, characterToTypeIntoEditor); + + const reactEditor = editorEnzyme!.instance() as MonacoEditor; + const editorValue = reactEditor.state.editor!.getModel()!.getValue(); + const expectedString = stringToType.substring(0, index + 1); + + // 1. Validate the value in the monaco editor. + // Confirms value in the editor is as expected. + assert.equal(editorValue, expectedString, 'Text does not match'); + + // 2. Validate the value in the redux state (props - update in redux, will push through to props). + // Confirms value in the props is as expected. + assert.equal(reactEditor.props.value, expectedString, 'Text does not match'); + + // 3. Validate the edit received by the extension from the react side. + // When user types `H`, then we'll expect to see `H` edit received in the model, then `i`, `!` & so on. + const expectedModelEditInExtension = modelEditsInExtension[index]; + // Verify against the character the user typed. + await assert.eventually.equal( + expectedModelEditInExtension.promise, + characterToTypeIntoEditor + ); + } + }); + test('Updates are not lost when switching to markdown (update redux store and model)', async () => { + const cellIndex = 3; + await addCell(mount, '', false); + assert.ok(isCellFocused(wrapper, 'NativeCell', cellIndex)); + assert.equal(wrapper.find('NativeCell').length, 4, 'Cell not added'); + + const notebookEditorProvider = ioc.get<INotebookEditorProvider>(INotebookEditorProvider); + const model = (notebookEditorProvider.editors[0] as NativeEditorWebView).model; + + // This is the string the user will type in a character at a time into the editor. + const stringToType = 'Hi Bob!'; + + // We are expecting to receive multiple edits to the model in the backend/extension from react, one for each character. + // Lets create deferreds that we can await on, and each will be resolved with the edit it received. + // For first edit, we'll expect `H`, then `i`, then `!` + const modelEditsInExtension = createDeferred(); + // Create deferred to detect changes to cellType. + const modelCellChangedInExtension = createDeferred(); + model?.changed((e) => { + // Resolve promise when we receive last edit (the last character `!`). + if (e.kind === 'edit' && e.forward.map((m) => m.text).join('') === '!') { + modelEditsInExtension.resolve(); + } + if (e.kind === 'changeCellType') { + modelCellChangedInExtension.resolve(); + } + }); + + // Type into new cell in one go (e.g. a paste operation) + const editorEnzyme = getNativeFocusedEditor(wrapper); + typeCode(editorEnzyme, stringToType); + + // Verify cell content + const reactEditor = editorEnzyme!.instance() as MonacoEditor; + const editorValue = reactEditor.state.editor!.getModel()!.getValue(); + + // 1. Validate the value in the monaco editor. + // Confirms value in the editor is as expected. + assert.equal(editorValue, stringToType, 'Text does not match'); + + // 2. Validate the value in the monaco editor state (redux state). + // Ensures we are keeping redux upto date. + assert.equal(reactEditor.props.value, stringToType, 'Text does not match'); + + // 3. Validate the edit received by the extension from the react side. + await modelEditsInExtension.promise; + assert.equal(concatMultilineString(model?.cells[3].data.source!), stringToType); + + // Now hit escape. + let update = waitForMessage(ioc, InteractiveWindowMessages.UnfocusedCellEditor); + simulateKeyPressOnCell(cellIndex, { code: 'Escape' }); + await update; + + // Confirm it is no longer focused, and it is selected. + assert.equal(isCellSelected(wrapper, 'NativeCell', cellIndex), true); + assert.equal(isCellFocused(wrapper, 'NativeCell', cellIndex), false); + + // Switch to markdown + update = waitForMessage(ioc, CommonActionType.CHANGE_CELL_TYPE); + simulateKeyPressOnCell(cellIndex, { code: 'm' }); + await update; + + // Monaco editor should be rendered and the cell should be markdown + assert.ok(!isCellFocused(wrapper, 'NativeCell', cellIndex), 'cell is not focused'); + assert.ok(isCellMarkdown(wrapper, 'NativeCell', cellIndex), 'cell is not markdown'); + + // Confirm cell has been changed in model. + await modelCellChangedInExtension.promise; + // Verify the cell type. + assert.equal(model?.cells[3].data.cell_type, 'markdown'); + // Verify that changing cell type didn't result in a loss of data. + assert.equal(concatMultilineString(model?.cells[3].data.source!), stringToType); + }); + }); + + suite('Keyboard Shortcuts', () => { + setup(async function () { + (window.navigator as any).platform = originalPlatform; + await initIoc(); + // tslint:disable-next-line: no-invalid-this + await setupFunction.call(this); + }); + teardown(() => ((window.navigator as any).platform = originalPlatform)); + test('Traverse cells by using ArrowUp and ArrowDown, k and j', async () => { + const keyCodesAndPositions = [ + // When we press arrow down in the first cell, then second cell gets selected. + { keyCode: 'ArrowDown', cellIndexToPressKeysOn: 0, expectedSelectedCell: 1 }, + { keyCode: 'ArrowDown', cellIndexToPressKeysOn: 1, expectedSelectedCell: 2 }, + // Arrow down on last cell is a noop. + { keyCode: 'ArrowDown', cellIndexToPressKeysOn: 2, expectedSelectedCell: 2 }, + // When we press arrow up in the last cell, then second cell (from bottom) gets selected. + { keyCode: 'ArrowUp', cellIndexToPressKeysOn: 2, expectedSelectedCell: 1 }, + { keyCode: 'ArrowUp', cellIndexToPressKeysOn: 1, expectedSelectedCell: 0 }, + // Arrow up on last cell is a noop. + { keyCode: 'ArrowUp', cellIndexToPressKeysOn: 0, expectedSelectedCell: 0 }, + + // Same tests as above with k and j. + { keyCode: 'j', cellIndexToPressKeysOn: 0, expectedSelectedCell: 1 }, + { keyCode: 'j', cellIndexToPressKeysOn: 1, expectedSelectedCell: 2 }, + // Arrow down on last cell is a noop. + { keyCode: 'j', cellIndexToPressKeysOn: 2, expectedSelectedCell: 2 }, + { keyCode: 'k', cellIndexToPressKeysOn: 2, expectedSelectedCell: 1 }, + { keyCode: 'k', cellIndexToPressKeysOn: 1, expectedSelectedCell: 0 }, + // Arrow up on last cell is a noop. + { keyCode: 'k', cellIndexToPressKeysOn: 0, expectedSelectedCell: 0 } + ]; + + // keypress on first cell, then second, then third. + // Test navigation through all cells, by traversing up and down. + for (const testItem of keyCodesAndPositions) { + simulateKeyPressOnCell(testItem.cellIndexToPressKeysOn, { code: testItem.keyCode }); + + // Check if it is selected. + // Only the cell at the index should be selected, as that's what we click. + assert.ok(isCellSelected(wrapper, 'NativeCell', testItem.expectedSelectedCell) === true); + } + }); + + test('Traverse cells by using ArrowUp and ArrowDown, k and j', async () => { + const keyCodesAndPositions = [ + // When we press arrow down in the first cell, then second cell gets selected. + { keyCode: 'ArrowDown', cellIndexToPressKeysOn: 0, expectedIndex: 1 }, + { keyCode: 'ArrowDown', cellIndexToPressKeysOn: 1, expectedIndex: 2 }, + // Arrow down on last cell is a noop. + { keyCode: 'ArrowDown', cellIndexToPressKeysOn: 2, expectedIndex: 2 }, + // When we press arrow up in the last cell, then second cell (from bottom) gets selected. + { keyCode: 'ArrowUp', cellIndexToPressKeysOn: 2, expectedIndex: 1 }, + { keyCode: 'ArrowUp', cellIndexToPressKeysOn: 1, expectedIndex: 0 }, + // Arrow up on last cell is a noop. + { keyCode: 'ArrowUp', cellIndexToPressKeysOn: 0, expectedIndex: 0 } + ]; + + // keypress on first cell, then second, then third. + // Test navigation through all cells, by traversing up and down. + for (const testItem of keyCodesAndPositions) { + simulateKeyPressOnCell(testItem.cellIndexToPressKeysOn, { code: testItem.keyCode }); + + // Check if it is selected. + // Only the cell at the index should be selected, as that's what we click. + assert.ok(isCellSelected(wrapper, 'NativeCell', testItem.expectedIndex) === true); + } + }); + + test("Pressing 'Enter' on a selected cell, results in focus being set to the code", async () => { + // For some reason we cannot allow setting focus to monaco editor. + // Tests are known to fall over if allowed. + wrapper.update(); + const editor = wrapper.find(NativeCell).at(1).find(Editor).first(); + (editor.instance() as Editor).giveFocus = () => editor.props().focused!(); + + const update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + clickCell(1); + simulateKeyPressOnCell(1, { code: 'Enter', editorInfo: undefined }); + await update; + + // The second cell should be selected. + assert.ok(isCellFocused(wrapper, 'NativeCell', 1)); + }); + + test("Pressing 'Escape' on a focused cell results in the cell being selected", async () => { + // First focus the cell. + let update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + clickCell(1); + simulateKeyPressOnCell(1, { code: 'Enter', editorInfo: undefined }); + await update; + + // The second cell should be selected. + assert.equal(isCellSelected(wrapper, 'NativeCell', 1), false); + assert.equal(isCellFocused(wrapper, 'NativeCell', 1), true); + + // Now hit escape. + update = waitForMessage(ioc, InteractiveWindowMessages.UnfocusedCellEditor); + simulateKeyPressOnCell(1, { code: 'Escape' }); + await update; + + // Confirm it is no longer focused, and it is selected. + assert.equal(isCellSelected(wrapper, 'NativeCell', 1), true); + assert.equal(isCellFocused(wrapper, 'NativeCell', 1), false); + }).retries(3); + + test("Pressing 'Shift+Enter' on a selected cell executes the cell and advances to the next cell", async () => { + let update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + clickCell(1); + simulateKeyPressOnCell(1, { code: 'Enter', editorInfo: undefined }); + await update; + + // The 2nd cell should be focused + assert.ok(isCellFocused(wrapper, 'NativeCell', 1)); + + update = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered); + simulateKeyPressOnCell(1, { code: 'Enter', shiftKey: true, editorInfo: undefined }); + await update; + wrapper.update(); + + // Ensure cell was executed. + verifyHtmlOnCell(wrapper, 'NativeCell', '<span>2</span>', 1); + + // The third cell should be selected. + assert.ok(isCellSelected(wrapper, 'NativeCell', 2)); + + // The third cell should not be focused + assert.ok(!isCellFocused(wrapper, 'NativeCell', 2)); + + // Shift+enter on the last cell, it should behave differently. It should be selected and focused + + // First focus the cell. + update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + clickCell(2); + simulateKeyPressOnCell(2, { code: 'Enter', editorInfo: undefined }); + await update; + + // The 3rd cell should be focused + assert.ok(isCellFocused(wrapper, 'NativeCell', 2)); + + update = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered); + simulateKeyPressOnCell(2, { code: 'Enter', shiftKey: true, editorInfo: undefined }); + await update; + wrapper.update(); + + // The fourth cell should be focused and not selected. + assert.ok(!isCellSelected(wrapper, 'NativeCell', 3)); + + // The fourth cell should be focused + assert.ok(isCellFocused(wrapper, 'NativeCell', 3)); + }); + + test("Pressing 'Ctrl+Enter' on a selected cell executes the cell and cell selection is not changed", async () => { + const update = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered); + clickCell(1); + simulateKeyPressOnCell(1, { code: 'Enter', ctrlKey: true, editorInfo: undefined }); + await update; + + // Ensure cell was executed. + verifyHtmlOnCell(wrapper, 'NativeCell', '<span>2</span>', 1); + + // The first cell should be selected. + assert.ok(isCellSelected(wrapper, 'NativeCell', 1)); + }); + + test("Pressing 'Alt+Enter' on a selected cell adds a new cell below it", async () => { + // Initially 3 cells. + wrapper.update(); + assert.equal(wrapper.find('NativeCell').length, 3); + + const update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + clickCell(1); + simulateKeyPressOnCell(1, { code: 'Enter', altKey: true, editorInfo: undefined }); + await update; + + // The second cell should be focused. + assert.ok(isCellFocused(wrapper, 'NativeCell', 2)); + // There should be 4 cells. + assert.equal(wrapper.find('NativeCell').length, 4); + }); + + test('Auto brackets work', async () => { + wrapper.update(); + // Initially 3 cells. + assert.equal(wrapper.find('NativeCell').length, 3); + + // Give focus + let update = waitForMessage(ioc, InteractiveWindowMessages.SelectedCell); + clickCell(1); + await update; + update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + simulateKeyPressOnCell(1, { code: 'Enter', editorInfo: undefined }); + await update; + + // The first cell should be focused. + assert.ok(isCellFocused(wrapper, 'NativeCell', 1)); + + // Add cell + await addCell(mount, '', false); + assert.equal(wrapper.find('NativeCell').length, 4); + + // New cell should have focus + assert.ok(isCellFocused(wrapper, 'NativeCell', 2)); + + const editorEnzyme = getNativeFocusedEditor(wrapper); + + // Type in something with brackets + typeCode(editorEnzyme, 'a('); + + // Verify cell content + const reactEditor = editorEnzyme!.instance() as MonacoEditor; + const editor = reactEditor.state.editor; + if (editor) { + assert.equal(editor.getModel()!.getValue(), 'a()', 'Text does not have brackets'); + } + }); + + test('Navigating cells using up/down keys while focus is set to editor', async () => { + wrapper.update(); + + const firstCell = 0; + const secondCell = 1; + + // Set focus to the first cell. + let update = waitForMessage(ioc, InteractiveWindowMessages.SelectedCell); + clickCell(firstCell); + await update; + update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + simulateKeyPressOnCell(firstCell, { code: 'Enter' }); + await update; + assert.ok(isCellFocused(wrapper, 'NativeCell', firstCell)); + + // Now press the down arrow, and focus should go to the next cell. + update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + let monacoEditor = getNativeFocusedEditor(wrapper)!.instance() as MonacoEditor; + monacoEditor.getCurrentVisibleLine = () => 0; + monacoEditor.getVisibleLineCount = () => 1; + simulateKeyPressOnCell(firstCell, { code: 'ArrowDown' }); + await update; + + // The next cell must be focused, but not selected. + assert.isFalse( + isCellFocused(wrapper, 'NativeCell', firstCell), + 'First new cell must not be focused' + ); + assert.isTrue( + isCellFocused(wrapper, 'NativeCell', secondCell), + 'Second new cell must be focused' + ); + assert.isFalse( + isCellSelected(wrapper, 'NativeCell', firstCell), + 'First new cell must not be selected' + ); + assert.isFalse( + isCellSelected(wrapper, 'NativeCell', secondCell), + 'Second new cell must not be selected' + ); + + // Now press the up arrow, and focus should go back to the first cell. + update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + monacoEditor = getNativeFocusedEditor(wrapper)!.instance() as MonacoEditor; + monacoEditor.getCurrentVisibleLine = () => 0; + monacoEditor.getVisibleLineCount = () => 1; + simulateKeyPressOnCell(firstCell, { code: 'ArrowUp' }); + await update; + + // The first cell must be focused, but not selected. + assert.isTrue( + isCellFocused(wrapper, 'NativeCell', firstCell), + 'First new cell must not be focused' + ); + assert.isFalse( + isCellFocused(wrapper, 'NativeCell', secondCell), + 'Second new cell must be focused' + ); + assert.isFalse( + isCellSelected(wrapper, 'NativeCell', firstCell), + 'First new cell must not be selected' + ); + assert.isFalse( + isCellSelected(wrapper, 'NativeCell', secondCell), + 'Second new cell must not be selected' + ); + }); + + test('Navigating cells using up/down keys through code & markdown cells, while focus is set to editor', async () => { + // Previously when pressing ArrowDown with mixture of markdown and code cells, + // the cursor would not go past a markdown cell (i.e. markdown editor will not get focus for ArrowDown to work). + + wrapper.update(); + + // Add a markdown cell at the end. + await addMarkdown('4'); + await addCell(mount, '5', false); + await addMarkdown('6'); + await addCell(mount, '7', false); + + // Access the code in the cells. + const notebookEditorProvider = ioc.get<INotebookEditorProvider>(INotebookEditorProvider); + const model = (notebookEditorProvider.editors[0] as NativeEditorWebView).model; + + // Set focus to the first cell. + let update = waitForMessage(ioc, InteractiveWindowMessages.SelectedCell); + clickCell(0); + await update; + update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + simulateKeyPressOnCell(0, { code: 'Enter' }); + await update; + assert.ok(isCellFocused(wrapper, 'NativeCell', 0)); + + for (let index = 0; index < 5; index += 1) { + // 1. Now press the down arrow, and focus should go to the next cell. + update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + const monacoEditor = getNativeFocusedEditor(wrapper)!.instance() as MonacoEditor; + monacoEditor.getCurrentVisibleLine = () => 0; + monacoEditor.getVisibleLineCount = () => 1; + simulateKeyPressOnCell(index, { code: 'ArrowDown' }); + await update; + + // Next cell. + const expectedActiveCell = model?.cells[index + 1]; + // The editor has focus, confirm the value in the active element/editor is the code. + const codeInActiveElement = ((document.activeElement as any).value as string).trim(); + const expectedCode = concatMultilineString(expectedActiveCell!.data.source!).trim(); + assert.equal(codeInActiveElement, expectedCode); + } + }); + + test("Pressing 'd' on a selected cell twice deletes the cell", async () => { + // Initially 3 cells. + wrapper.update(); + assert.equal(wrapper.find('NativeCell').length, 3); + + clickCell(2); + simulateKeyPressOnCell(2, { code: 'd' }); + simulateKeyPressOnCell(2, { code: 'd' }); + + // There should be 2 cells. + assert.equal(wrapper.find('NativeCell').length, 2); + }); + + test("Pressing 'a' on a selected cell adds a cell at the current position", async () => { + // Initially 3 cells. + wrapper.update(); + assert.equal(wrapper.find('NativeCell').length, 3); + + clickCell(0); + const addedCell = waitForMessage(ioc, CommonActionType.INSERT_ABOVE_AND_FOCUS_NEW_CELL); + const update = waitForMessage(ioc, InteractiveWindowMessages.SelectedCell); + simulateKeyPressOnCell(0, { code: 'a' }); + await Promise.all([update, addedCell]); + + // There should be 4 cells. + assert.equal(wrapper.find('NativeCell').length, 4); + + // Verify cell indexes of old items. + verifyCellIndex(wrapper, 'div[id="NotebookImport#0"]', 1); + verifyCellIndex(wrapper, 'div[id="NotebookImport#1"]', 2); + verifyCellIndex(wrapper, 'div[id="NotebookImport#2"]', 3); + }); + + test("Pressing 'b' on a selected cell adds a cell after the current position", async () => { + // Initially 3 cells. + wrapper.update(); + assert.equal(wrapper.find('NativeCell').length, 3); + + clickCell(1); + const addedCell = waitForMessage(ioc, CommonActionType.INSERT_BELOW_AND_FOCUS_NEW_CELL); + const update = waitForMessage(ioc, InteractiveWindowMessages.SelectedCell); + simulateKeyPressOnCell(1, { code: 'b' }); + await Promise.all([update, addedCell]); + + // There should be 4 cells. + assert.equal(wrapper.find('NativeCell').length, 4); + + // Verify cell indexes of old items. + verifyCellIndex(wrapper, 'div[id="NotebookImport#0"]', 0); + verifyCellIndex(wrapper, 'div[id="NotebookImport#1"]', 1); + verifyCellIndex(wrapper, 'div[id="NotebookImport#2"]', 3); + }); + + test('Toggle visibility of output', async () => { + // First execute contents of last cell. + let update = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered); + clickCell(2); + simulateKeyPressOnCell(2, { code: 'Enter', ctrlKey: true, editorInfo: undefined }); + await update; + + // Ensure cell was executed. + verifyHtmlOnCell(wrapper, 'NativeCell', '<span>3</span>', 2); + + // Hide the output + update = waitForMessage(ioc, InteractiveWindowMessages.OutputToggled); + simulateKeyPressOnCell(2, { code: 'o' }); + await update; + + // Ensure cell output is hidden (looking for cell results will throw an exception). + assert.throws(() => verifyHtmlOnCell(wrapper, 'NativeCell', '<span>3</span>', 2)); + + // Display the output + update = waitForMessage(ioc, InteractiveWindowMessages.OutputToggled); + simulateKeyPressOnCell(2, { code: 'o' }); + await update; + + // Ensure cell output is visible again. + verifyHtmlOnCell(wrapper, 'NativeCell', '<span>3</span>', 2); + }); + + test("Toggle line numbers using the 'l' key", async () => { + clickCell(1); + + const monacoEditorComponent = wrapper.find(NativeCell).at(1).find(MonacoEditor).first(); + const editor = (monacoEditorComponent.instance().state as IMonacoEditorState).editor!; + const optionsUpdated = sinon.spy(editor, 'updateOptions'); + + // Display line numbers. + simulateKeyPressOnCell(1, { code: 'l' }); + // Confirm monaco editor got updated with line numbers set to turned on. + assert.equal(optionsUpdated.lastCall.args[0].lineNumbers, 'on'); + + // toggle the display of line numbers. + simulateKeyPressOnCell(1, { code: 'l' }); + // Confirm monaco editor got updated with line numbers set to turned ff. + assert.equal(optionsUpdated.lastCall.args[0].lineNumbers, 'off'); + }); + + test("Toggle markdown and code modes using 'y' and 'm' keys (cells should not be focused)", async () => { + clickCell(1); + // Switch to markdown + let update = waitForMessage(ioc, CommonActionType.CHANGE_CELL_TYPE); + simulateKeyPressOnCell(1, { code: 'm' }); + await update; + + // Monaco editor should be rendered and the cell should be markdown + assert.ok(!isCellFocused(wrapper, 'NativeCell', 1), '1st cell is not focused'); + assert.ok(isCellMarkdown(wrapper, 'NativeCell', 1), '1st cell is not markdown'); + + // Switch to code + update = waitForMessage(ioc, CommonActionType.CHANGE_CELL_TYPE); + simulateKeyPressOnCell(1, { code: 'y' }); + await update; + + assert.ok(!isCellFocused(wrapper, 'NativeCell', 1), '1st cell is not focused 2nd time'); + assert.ok(!isCellMarkdown(wrapper, 'NativeCell', 1), '1st cell is markdown second time'); + }); + + test("Toggle markdown and code modes using 'y' and 'm' keys & ensure changes to cells is preserved", async () => { + clickCell(1); + // Switch to markdown + let update = waitForMessage(ioc, CommonActionType.CHANGE_CELL_TYPE); + simulateKeyPressOnCell(1, { code: 'm' }); + await update; + + // Monaco editor should be rendered and the cell should be markdown + assert.ok(!isCellFocused(wrapper, 'NativeCell', 1), '1st cell is not focused'); + assert.ok(isCellMarkdown(wrapper, 'NativeCell', 1), '1st cell is not markdown'); + + // Focus the cell. + update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + simulateKeyPressOnCell(1, { code: 'Enter', editorInfo: undefined }); + await update; + + assert.ok(isCellFocused(wrapper, 'NativeCell', 1)); + assert.equal(wrapper.find(NativeCell).at(1).find(MonacoEditor).length, 1); + + // Change the markdown + let editor = getNativeFocusedEditor(wrapper); + injectCode(editor, 'foo'); + + // Switch back to code mode. + // First lose focus + update = waitForMessage(ioc, InteractiveWindowMessages.UnfocusedCellEditor); + simulateKeyPressOnCell(1, { code: 'Escape' }); + await update; + + // Confirm markdown output is rendered + assert.ok(!isCellFocused(wrapper, 'NativeCell', 1), '1st cell is focused'); + assert.ok(isCellMarkdown(wrapper, 'NativeCell', 1), '1st cell is not markdown'); + assert.equal(wrapper.find(NativeCell).at(1).find(MonacoEditor).length, 0); + + // Switch to code + update = waitForMessage(ioc, CommonActionType.CHANGE_CELL_TYPE); + simulateKeyPressOnCell(1, { code: 'y' }); + await update; + + assert.ok(!isCellFocused(wrapper, 'NativeCell', 1), '1st cell is not focused 2nd time'); + assert.ok(!isCellMarkdown(wrapper, 'NativeCell', 1), '1st cell is markdown second time'); + + // Focus the cell. + update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + simulateKeyPressOnCell(1, { code: 'Enter', editorInfo: undefined }); + await update; + + // Confirm editor still has the same text + editor = getNativeFocusedEditor(wrapper); + const monacoEditor = editor!.instance() as MonacoEditor; + assert.equal('foo', monacoEditor.state.editor!.getValue(), 'Changing cell type lost input'); + }); + + test("Test undo using the key 'z'", async function () { + if (useCustomEditorApi) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + clickCell(0); + + // Add, then undo, keep doing at least 3 times and confirm it works as expected. + for (let i = 0; i < 3; i += 1) { + // Add a new cell + let update = waitForMessage(ioc, InteractiveWindowMessages.SelectedCell); + simulateKeyPressOnCell(0, { code: 'a' }); + await update; + + // Wait a bit for the time out to try and set focus a second time (this will be + // fixed when we switch to redux) + await sleep(100); + + // There should be 4 cells and first cell is selected. + assert.equal(isCellSelected(wrapper, 'NativeCell', 0), true); + assert.equal(isCellSelected(wrapper, 'NativeCell', 1), false); + assert.equal(isCellFocused(wrapper, 'NativeCell', 0), false); + assert.equal(isCellFocused(wrapper, 'NativeCell', 1), false); + assert.equal(wrapper.find('NativeCell').length, 4); + + // Press 'ctrl+z'. This should do nothing + simulateKeyPressOnCell(0, { code: 'z', ctrlKey: true }); + await sleep(100); + + // There should be 4 cells and first cell is selected. + assert.equal(isCellSelected(wrapper, 'NativeCell', 0), true); + assert.equal(isCellSelected(wrapper, 'NativeCell', 1), false); + assert.equal(isCellFocused(wrapper, 'NativeCell', 0), false); + assert.equal(isCellFocused(wrapper, 'NativeCell', 1), false); + assert.equal(wrapper.find('NativeCell').length, 4); + + // Press 'meta+z'. This should do nothing + simulateKeyPressOnCell(0, { code: 'z', metaKey: true }); + await sleep(100); + + // There should be 4 cells and first cell is selected. + assert.equal(isCellSelected(wrapper, 'NativeCell', 0), true); + assert.equal(isCellSelected(wrapper, 'NativeCell', 1), false); + assert.equal(isCellFocused(wrapper, 'NativeCell', 0), false); + assert.equal(isCellFocused(wrapper, 'NativeCell', 1), false); + assert.equal(wrapper.find('NativeCell').length, 4); + + // Press 'z' to undo. + // Technically not really rendering, but it fires when the cell count changes + update = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered); + simulateKeyPressOnCell(0, { code: 'z' }); + await update; + + // There should be 3 cells and first cell is selected & nothing focused. + assert.equal(isCellSelected(wrapper, 'NativeCell', 0), true); + assert.equal(isCellSelected(wrapper, 'NativeCell', 1), false); + assert.equal(wrapper.find('NativeCell').length, 3); + + // Press 'shift+z' to redo + update = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered); + simulateKeyPressOnCell(0, { code: 'z', shiftKey: true }); + await update; + + // There should be 4 cells and first cell is selected. + assert.equal(isCellSelected(wrapper, 'NativeCell', 0), true); + assert.equal(isCellSelected(wrapper, 'NativeCell', 1), false); + assert.equal(isCellFocused(wrapper, 'NativeCell', 0), false); + assert.equal(isCellFocused(wrapper, 'NativeCell', 1), false); + assert.equal(wrapper.find('NativeCell').length, 4); + + // Press 'z' to undo. + update = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered); + simulateKeyPressOnCell(0, { code: 'z' }); + await update; + + // There should be 3 cells and first cell is selected & nothing focused. + assert.equal(isCellSelected(wrapper, 'NativeCell', 0), true); + assert.equal(isCellSelected(wrapper, 'NativeCell', 1), false); + assert.equal(wrapper.find('NativeCell').length, 3); + } + }); + + test("Test save using the key 'ctrl+s' on Windows", async function () { + if (useCustomEditorApi) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + (window.navigator as any).platform = 'Win'; + clickCell(0); + + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + await addCell(mount, 'a=1\na', true); + await dirtyPromise; + + const notebookEditorProvider = ioc.get<INotebookEditorProvider>(INotebookEditorProvider); + const editor = notebookEditorProvider.editors[0]; + assert.ok(editor, 'No editor when saving'); + const savedPromise = createDeferred(); + editor.saved(() => savedPromise.resolve()); + + const clean = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + simulateKeyPressOnCell(1, { code: 's', ctrlKey: true }); + await waitForCondition( + () => savedPromise.promise.then(() => true).catch(() => false), + 10_000, + 'Timedout' + ); + await clean; + assert.ok(!editor!.isDirty, 'Editor should not be dirty after saving'); + }); + + test("Test save using the key 'ctrl+s' on Mac", async function () { + if (useCustomEditorApi) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + (window.navigator as any).platform = 'Mac'; + clickCell(0); + + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + await addCell(mount, 'a=1\na', true); + await dirtyPromise; + + const notebookEditorProvider = ioc.get<INotebookEditorProvider>(INotebookEditorProvider); + const editor = notebookEditorProvider.editors[0]; + assert.ok(editor, 'No editor when saving'); + const savedPromise = createDeferred(); + editor.saved(() => savedPromise.resolve()); + + simulateKeyPressOnCell(1, { code: 's', ctrlKey: true }); + + await expect( + waitForCondition( + () => savedPromise.promise.then(() => true).catch(() => false), + 1_000, + 'Timedout' + ) + ).to.eventually.be.rejected; + + assert.ok(editor!.isDirty, 'Editor be dirty as nothing got saved'); + }); + + test("Test save using the key 'cmd+s' on a Mac", async function () { + if (useCustomEditorApi) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + (window.navigator as any).platform = 'Mac'; + + clickCell(0); + + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + await addCell(mount, 'a=1\na', true); + await dirtyPromise; + + const notebookEditorProvider = ioc.get<INotebookEditorProvider>(INotebookEditorProvider); + const editor = notebookEditorProvider.editors[0]; + assert.ok(editor, 'No editor when saving'); + const savedPromise = createDeferred(); + editor.saved(() => savedPromise.resolve()); + + simulateKeyPressOnCell(1, { code: 's', metaKey: true }); + + const clean = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + await waitForCondition( + () => savedPromise.promise.then(() => true).catch(() => false), + 1_000, + 'Timedout' + ); + await clean; + + assert.ok(!editor!.isDirty, 'Editor should not be dirty after saving'); + }); + test("Test save using the key 'cmd+s' on a Windows", async function () { + if (useCustomEditorApi) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + (window.navigator as any).platform = 'Win'; + + clickCell(0); + + await addCell(mount, 'a=1\na', true); + + const notebookEditorProvider = ioc.get<INotebookEditorProvider>(INotebookEditorProvider); + const editor = notebookEditorProvider.editors[0]; + assert.ok(editor, 'No editor when saving'); + const savedPromise = createDeferred(); + editor.saved(() => savedPromise.resolve()); + + // CMD+s won't work on Windows. + simulateKeyPressOnCell(1, { code: 's', metaKey: true }); + + await expect( + waitForCondition( + () => savedPromise.promise.then(() => true).catch(() => false), + 1_000, + 'Timedout' + ) + ).to.eventually.be.rejected; + + assert.ok(editor!.isDirty, 'Editor be dirty as nothing got saved'); + }); + }); + + suite('Auto Save', () => { + let windowStateChangeHandlers: ((e: WindowState) => any)[] = []; + setup(async function () { + if (useCustomEditorApi) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + await initIoc(); + + const eventCallback = ( + listener: (e: WindowState) => any, + _thisArgs?: any, + _disposables?: IDisposable[] | Disposable + ) => { + windowStateChangeHandlers.push(listener); + return { + dispose: noop + }; + }; + windowStateChangeHandlers = []; + // Keep track of all handlers for the onDidChangeWindowState event. + when(ioc.applicationShell.onDidChangeWindowState).thenReturn(eventCallback); + + // tslint:disable-next-line: no-invalid-this + await setupFunction.call(this); + }); + teardown(() => sinon.restore()); + + /** + * Make some kind of a change to the notebook. + * + * @param {number} cellIndex + */ + async function modifyNotebook() { + // (Add a cell into the UI) + await addCell(mount, 'a', false); + } + + test('Auto save notebook every 1s', async () => { + // Configure notebook to save automatically ever 1s. + await updateFileConfig(ioc, 'autoSave', 'afterDelay'); + await updateFileConfig(ioc, 'autoSaveDelay', 1_000); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); + + /** + * Make some changes to a cell of a notebook, then verify the notebook is auto saved. + * + * @param {number} cellIndex + */ + async function makeChangesAndConfirmFileIsUpdated() { + const notebookFileContents = await fs.readFile(notebookFile.filePath, 'utf8'); + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + const cleanPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + + await modifyNotebook(); + await dirtyPromise; + + // At this point a message should be sent to extension asking it to save. + // After the save, the extension should send a message to react letting it know that it was saved successfully. + await cleanPromise; + + // Confirm file has been updated as well. + const newFileContents = await fs.readFile(notebookFile.filePath, 'utf8'); + assert.notEqual(newFileContents, notebookFileContents); + } + + // Make changes & validate (try a couple of times). + await makeChangesAndConfirmFileIsUpdated(); + await makeChangesAndConfirmFileIsUpdated(); + await makeChangesAndConfirmFileIsUpdated(); + }).retries(2); + + test('File saved with same format', async () => { + // Configure notebook to save automatically ever 1s. + await updateFileConfig(ioc, 'autoSave', 'afterDelay'); + await updateFileConfig(ioc, 'autoSaveDelay', 2_000); + + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); + const notebookFileContents = await fs.readFile(notebookFile.filePath, 'utf8'); + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + const cleanPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + + await modifyNotebook(); + await dirtyPromise; + + // At this point a message should be sent to extension asking it to save. + // After the save, the extension should send a message to react letting it know that it was saved successfully. + await cleanPromise; + + // Confirm file is not the same. There should be a single cell that's been added + const newFileContents = await fs.readFile(notebookFile.filePath, 'utf8'); + assert.notEqual(newFileContents, notebookFileContents); + assert.equal(newFileContents, addedJSONFile); + }); + + test('Should not auto save notebook, ever', async () => { + const notebookFileContents = await fs.readFile(notebookFile.filePath, 'utf8'); + + // Configure notebook to to never save. + await updateFileConfig(ioc, 'autoSave', 'off'); + await updateFileConfig(ioc, 'autoSaveDelay', 1_000); + + // Update the settings and wait for the component to receive it and process it. + const promise = waitForMessage(ioc, InteractiveWindowMessages.SettingsUpdated); + ioc.forceDataScienceSettingsChanged({ + showCellInputCode: false + }); + await promise; + + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + const cleanPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean, { + timeoutMs: 5_000 + }); + + await modifyNotebook(); + await dirtyPromise; + + // Now that the notebook is dirty, change the active editor. + const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + docManager.didChangeActiveTextEditorEmitter.fire({} as any); + // Also, send notification about changes to window state. + windowStateChangeHandlers.forEach((item) => item({ focused: false })); + windowStateChangeHandlers.forEach((item) => item({ focused: true })); + + // Confirm the message is not clean, trying to wait for it to get saved will timeout (i.e. rejected). + await expect(cleanPromise).to.eventually.be.rejected; + // Confirm file has not been updated as well. + assert.equal(await fs.readFile(notebookFile.filePath, 'utf8'), notebookFileContents); + }).timeout(10_000); + + async function testAutoSavingWhenEditorFocusChanges(newEditor: TextEditor | undefined) { + const notebookFileContents = await fs.readFile(notebookFile.filePath, 'utf8'); + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + const cleanPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + + await modifyNotebook(); + await dirtyPromise; + + // Configure notebook to save when active editor changes. + await updateFileConfig(ioc, 'autoSave', 'onFocusChange'); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); + + // Now that the notebook is dirty, change the active editor. + const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + docManager.didChangeActiveTextEditorEmitter.fire(newEditor!); + + // At this point a message should be sent to extension asking it to save. + // After the save, the extension should send a message to react letting it know that it was saved successfully. + await cleanPromise; + + // Confirm file has been updated as well. + assert.notEqual(await fs.readFile(notebookFile.filePath, 'utf8'), notebookFileContents); + } + + test('Auto save notebook when focus changes from active editor to none', () => + testAutoSavingWhenEditorFocusChanges(undefined)); + + test('Auto save notebook when focus changes from active editor to something else', () => + testAutoSavingWhenEditorFocusChanges(TypeMoq.Mock.ofType<TextEditor>().object)); + + test('Should not auto save notebook when active editor changes', async () => { + const notebookFileContents = await fs.readFile(notebookFile.filePath, 'utf8'); + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + const cleanPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean, { + timeoutMs: 5_000 + }); + + await modifyNotebook(); + await dirtyPromise; + + // Configure notebook to save when window state changes. + await updateFileConfig(ioc, 'autoSave', 'onWindowChange'); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); + + // Now that the notebook is dirty, change the active editor. + // This should not trigger a save of notebook (as its configured to save only when window state changes). + const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + docManager.didChangeActiveTextEditorEmitter.fire({} as any); + + // Confirm the message is not clean, trying to wait for it to get saved will timeout (i.e. rejected). + await expect(cleanPromise).to.eventually.be.rejected; + // Confirm file has not been updated as well. + assert.equal(await fs.readFile(notebookFile.filePath, 'utf8'), notebookFileContents); + }).timeout(10_000); + + async function testAutoSavingWithChangesToWindowState( + configSetting: 'onFocusChange' | 'onWindowChange', + focused: boolean + ) { + const notebookFileContents = await fs.readFile(notebookFile.filePath, 'utf8'); + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + const cleanPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + + await modifyNotebook(); + await dirtyPromise; + + // Configure notebook to save when active editor changes. + await updateFileConfig(ioc, 'autoSave', configSetting); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); + + // Now that the notebook is dirty, send notification about changes to window state. + windowStateChangeHandlers.forEach((item) => item({ focused })); + + // At this point a message should be sent to extension asking it to save. + // After the save, the extension should send a message to react letting it know that it was saved successfully. + await cleanPromise; + + // Confirm file has been updated as well. + assert.notEqual(await fs.readFile(notebookFile.filePath, 'utf8'), notebookFileContents); + } + + test('Auto save notebook when window state changes to being not focused', async () => + testAutoSavingWithChangesToWindowState('onWindowChange', false)); + test('Auto save notebook when window state changes to being focused', async () => + testAutoSavingWithChangesToWindowState('onWindowChange', true)); + test('Auto save notebook when window state changes to being focused for focusChange', async () => + testAutoSavingWithChangesToWindowState('onFocusChange', true)); + test('Auto save notebook when window state changes to being not focused for focusChange', async () => + testAutoSavingWithChangesToWindowState('onFocusChange', false)); + + test('Auto save notebook when view state changes', async () => { + const notebookFileContents = await fs.readFile(notebookFile.filePath, 'utf8'); + const dirtyPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + const cleanPromise = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + + await modifyNotebook(); + await dirtyPromise; + + // Configure notebook to save when active editor changes. + await updateFileConfig(ioc, 'autoSave', 'onFocusChange'); + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath); + + // Force a view state change + mount.changeViewState(true, false); + + // At this point a message should be sent to extension asking it to save. + // After the save, the extension should send a message to react letting it know that it was saved successfully. + await cleanPromise; + + // Confirm file has been updated as well. + assert.notEqual(await fs.readFile(notebookFile.filePath, 'utf8'), notebookFileContents); + }); + }); + + const oldJson: nbformat.INotebookContent = { + nbformat: 4, + nbformat_minor: 2, + cells: [ + { + cell_type: 'code', + execution_count: 1, + metadata: { + collapsed: true + }, + outputs: [ + { + data: { + 'text/plain': ['1'] + }, + output_type: 'execute_result', + execution_count: 1, + metadata: {} + } + ], + source: ['a=1\n', 'a'] + }, + { + cell_type: 'code', + execution_count: 2, + metadata: {}, + outputs: [ + { + data: { + 'text/plain': ['2'] + }, + output_type: 'execute_result', + execution_count: 2, + metadata: {} + } + ], + source: ['b=2\n', 'b'] + }, + { + cell_type: 'code', + execution_count: 3, + metadata: {}, + outputs: [ + { + data: { + 'text/plain': ['3'] + }, + output_type: 'execute_result', + execution_count: 3, + metadata: {} + } + ], + source: ['c=3\n', 'c'] + } + ], + metadata: { + orig_nbformat: 4, + kernelspec: { + display_name: 'JUNK', + name: 'JUNK' + }, + language_info: { + name: 'python', + version: '1.2.3' + } + } + }; + + suite('Stop On Error', () => { + let notebookEditor: { editor: INotebookEditor; mount: IMountedWebView }; + setup(async () => { + await initIoc(); + + // Set up a file where the second cell throws an exception + addMockData(ioc, 'print("hello")', 'hello'); + addMockData(ioc, 'raise Exception("stop")', undefined, undefined, 'error'); + addMockData(ioc, 'print("world")', 'world'); + + const errorFile = [ + { id: 'NotebookImport#0', data: { source: 'print("hello")' } }, + { id: 'NotebookImport#1', data: { source: 'raise Exception("stop")' } }, + { id: 'NotebookImport#2', data: { source: 'print("world")' } } + ]; + const runAllCells = errorFile.map((cell) => { + return createFileCell(cell, cell.data); + }); + const notebook = await ioc + .get<INotebookExporter>(INotebookExporter) + .translateToNotebook(runAllCells, undefined); + notebookEditor = await openEditor(ioc, JSON.stringify(notebook)); + }); + + test('Stop On Error On', async () => { + const ne = notebookEditor; + + const runAllButton = findButton(ne.mount.wrapper, NativeEditor, 0); + // The render method needs to be executed 3 times for three cells. + const threeCellsUpdated = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { + numberOfTimes: 3 + }); + runAllButton!.simulate('click'); + await threeCellsUpdated; + + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', `hello`, 0); + // There should be no output on the third cell as it's blocked by the exception on the second cell + assert.throws(() => verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', `world`, 2)); + }); + + test('Stop On Error Off', async () => { + const ne = notebookEditor; + + // Force our settings to not stop on error + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath, { + ...ioc.getSettings().datascience, + stopOnError: false + }); + + const runAllButton = findButton(ne.mount.wrapper, NativeEditor, 0); + // The render method needs to be executed 3 times for three cells. + const threeCellsUpdated = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { + numberOfTimes: 3 + }); + runAllButton!.simulate('click'); + await threeCellsUpdated; + + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', `hello`, 0); + // There should be output on the third cell, even with an error on the second + verifyHtmlOnCell(ne.mount.wrapper, 'NativeCell', `world`, 2); + }); + }); + + suite('Update Metadata', () => { + setup(async function () { + await initIoc(); + // tslint:disable-next-line: no-invalid-this + await setupFunction.call(this, JSON.stringify(oldJson)); + }); + + test('Update notebook metadata on execution', async () => { + const notebookEditorProvider = ioc.get<INotebookEditorProvider>(INotebookEditorProvider); + const editor = notebookEditorProvider.editors[0]; + assert.ok(editor, 'No editor when saving'); + + // add cells, run them and save + await addCell(mount, 'a=1\na'); + const runAllButton = findButton(wrapper, NativeEditor, 0); + const threeCellsUpdated = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { + numberOfTimes: 3 + }); + runAllButton!.simulate('click'); + await threeCellsUpdated; + + const saveButton = findButton(wrapper, NativeEditor, 8); + const saved = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + saveButton!.simulate('click'); + await saved; + + // the file has output and execution count + const fileContent = await fs.readFile(notebookFile.filePath, 'utf8'); + const fileObject = JSON.parse(fileContent); + + // First cell should still have the 'collapsed' metadata + assert.ok(fileObject.cells[0].metadata.collapsed, 'Metadata erased during execution'); + + // Old language info should be changed by the new execution + assert.notEqual(fileObject.metadata.language_info.version, '1.2.3'); + + // Some tests don't have a kernelspec, in which case we should remove it + // If there is a spec, we should update the name and display name + const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; + if (isRollingBuild && fileObject.metadata.kernelspec) { + assert.notEqual(fileObject.metadata.kernelspec.display_name, 'JUNK'); + assert.notEqual(fileObject.metadata.kernelspec.name, 'JUNK'); + } + }); + }); + + suite('Clear Outputs', () => { + setup(async function () { + await initIoc(); + // tslint:disable-next-line: no-invalid-this + await setupFunction.call(this, JSON.stringify(oldJson)); + }); + + function verifyExecutionCount(cellIndex: number, executionCountContent: string) { + assert.equal(wrapper.find(ExecutionCount).at(cellIndex).props().count, executionCountContent); + } + + test('Clear Outputs in WebView', async () => { + const runAllButton = findButton(wrapper, NativeEditor, 0); + const threeCellsUpdated = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { + numberOfTimes: 3 + }); + runAllButton!.simulate('click'); + await threeCellsUpdated; + + verifyExecutionCount(0, '1'); + verifyExecutionCount(1, '2'); + verifyExecutionCount(2, '3'); + + // Press clear all outputs + const clearAllOutput = waitForMessage(ioc, InteractiveWindowMessages.ClearAllOutputs); + const clearAllOutputButton = findButton(wrapper, NativeEditor, 6); + clearAllOutputButton!.simulate('click'); + await clearAllOutput; + + verifyExecutionCount(0, '-'); + verifyExecutionCount(1, '-'); + verifyExecutionCount(2, '-'); + }); + + test('Clear execution_count and outputs in notebook', async () => { + const notebookEditorProvider = ioc.get<INotebookEditorProvider>(INotebookEditorProvider); + const editor = notebookEditorProvider.editors[0]; + assert.ok(editor, 'No editor when saving'); + // add cells, run them and save + // await addCell(mount, 'a=1\na'); + const runAllButton = findButton(wrapper, NativeEditor, 0); + const threeCellsUpdated = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { + numberOfTimes: 3 + }); + runAllButton!.simulate('click'); + await threeCellsUpdated; + + const saveButton = findButton(wrapper, NativeEditor, 8); + let saved = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + saveButton!.simulate('click'); + await saved; + + // press clear all outputs, and save + const cleared = waitForMessage(ioc, InteractiveWindowMessages.NotebookDirty); + const clearAllOutputButton = findButton(wrapper, NativeEditor, 6); + clearAllOutputButton!.simulate('click'); + await cleared; + + saved = waitForMessage(ioc, InteractiveWindowMessages.NotebookClean); + saveButton!.simulate('click'); + await saved; + await sleep(1000); // Make sure file finishes writing. + + const nb = JSON.parse( + await fs.readFile(notebookFile.filePath, 'utf8') + ) as nbformat.INotebookContent; + assert.equal(nb.cells[0].execution_count, null); + assert.equal(nb.cells[1].execution_count, null); + assert.equal(nb.cells[2].execution_count, null); + expect(nb.cells[0].outputs).to.be.lengthOf(0); + expect(nb.cells[1].outputs).to.be.lengthOf(0); + expect(nb.cells[2].outputs).to.be.lengthOf(0); + }); + }); + }); + }); + }); +}); diff --git a/src/test/datascience/nativeEditor.toolbar.functional.test.tsx b/src/test/datascience/nativeEditor.toolbar.functional.test.tsx new file mode 100644 index 000000000000..b1e86f026daa --- /dev/null +++ b/src/test/datascience/nativeEditor.toolbar.functional.test.tsx @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import { ReactWrapper } from 'enzyme'; +import * as React from 'react'; +import * as sinon from 'sinon'; +import { PYTHON_LANGUAGE } from '../../client/common/constants'; +import { getNamesAndValues } from '../../client/common/utils/enum'; +import { ServerStatus } from '../../datascience-ui/interactive-common/mainState'; +import { TrustMessage } from '../../datascience-ui/interactive-common/trustMessage'; +import { INativeEditorToolbarProps, Toolbar } from '../../datascience-ui/native-editor/toolbar'; +import { ImageButton } from '../../datascience-ui/react-common/imageButton'; +import { noop } from '../core'; +import { mountComponent } from './testHelpers'; + +// tslint:disable: no-any use-default-type-parameter + +enum Button { + RunAll = 0, + RunAbove = 1, + RunBelow = 2, + RestartKernel = 3, + InterruptKernel = 4, + AddCell = 5, + ClearAllOutput = 6, + VariableExplorer = 7, + Save = 8, + Export = 9 +} +const allowList: Button[] = []; // List of buttons to be enabled while a notebook is untrusted + +suite('DataScience Native Toolbar', () => { + const noopAny: any = noop; + let props: INativeEditorToolbarProps; + let wrapper: ReactWrapper<any, Readonly<{}>, React.Component>; + setup(() => { + props = { + baseTheme: '', + busy: false, + cellCount: 0, + dirty: false, + export: sinon.stub(), + exportAs: sinon.stub(), + font: { family: '', size: 1 }, + interruptKernel: sinon.stub(), + kernel: { + displayName: '', + jupyterServerStatus: ServerStatus.Busy, + localizedUri: '', + language: PYTHON_LANGUAGE + }, + restartKernel: sinon.stub(), + selectKernel: noopAny, + selectServer: noopAny, + addCell: sinon.stub(), + clearAllOutputs: sinon.stub(), + executeAbove: sinon.stub(), + executeAllCells: sinon.stub(), + executeCellAndBelow: sinon.stub(), + save: sinon.stub(), + selectionFocusedInfo: {}, + sendCommand: noopAny, + toggleVariableExplorer: sinon.stub(), + setVariableExplorerHeight: sinon.stub(), + launchNotebookTrustPrompt: sinon.stub(), + variablesVisible: false, + isNotebookTrusted: true + }; + }); + function mountToolbar() { + wrapper = mountComponent('native', <Toolbar {...props}></Toolbar>); + } + function getToolbarButton(button: Button) { + return wrapper.find(ImageButton).at(button); + } + function getTrustMessage() { + return wrapper.find(TrustMessage); + } + function clickTrustMessage() { + const handler = getTrustMessage().props().launchNotebookTrustPrompt; + if (handler) { + handler(); + } + } + function assertEnabled(button: Button) { + assert.isFalse(getToolbarButton(button).props().disabled); + } + function assertDisabled(button: Button) { + assert.isTrue(getToolbarButton(button).props().disabled); + } + function clickButton(button: Button) { + const handler = getToolbarButton(button).props().onClick; + if (handler) { + handler(); + } + } + suite('Run All', () => { + test('When not busy it is enabled', () => { + props.busy = false; + mountToolbar(); + assertEnabled(Button.RunAll); + }); + test('When busy it is disabled', () => { + props.busy = true; + mountToolbar(); + assertDisabled(Button.RunAll); + }); + test('When clicked dispatches executeAllCells', () => { + mountToolbar(); + clickButton(Button.RunAll); + assert.isTrue(((props.executeAllCells as any) as sinon.SinonStub).calledOnce); + }); + }); + suite('Run Above', () => { + test('If not busy and there are no selected cells, then disabled', () => { + props.selectionFocusedInfo.selectedCellIndex = undefined; + props.busy = false; + mountToolbar(); + assertDisabled(Button.RunAbove); + }); + test('If not busy and selected cell is first cell, then disabled', () => { + props.selectionFocusedInfo.selectedCellIndex = 0; + props.busy = false; + mountToolbar(); + assertDisabled(Button.RunAbove); + }); + test('If not busy and selected cell is second cell, then enabled', () => { + props.selectionFocusedInfo.selectedCellIndex = 1; + props.busy = false; + mountToolbar(); + assertEnabled(Button.RunAbove); + }); + test('When busy it is disabled', () => { + props.busy = true; + mountToolbar(); + assertDisabled(Button.RunAbove); + }); + test('When clicked dispatches executeAbove', () => { + props.selectionFocusedInfo.selectedCellId = 'My_Selected_CellId'; + props.selectionFocusedInfo.selectedCellIndex = 5; + mountToolbar(); + clickButton(Button.RunAbove); + assert.isTrue(((props.executeAbove as any) as sinon.SinonStub).calledOnce); + assert.equal(((props.executeAbove as any) as sinon.SinonStub).firstCall.args[0], 'My_Selected_CellId'); + }); + }); + suite('Run Below', () => { + test('If not busy and there are no selected cells, then disabled', () => { + props.selectionFocusedInfo.selectedCellIndex = undefined; + props.selectionFocusedInfo.selectedCellId = undefined; + props.busy = false; + mountToolbar(); + assertDisabled(Button.RunBelow); + }); + test('If not busy and selected cell is last cell, then disabled', () => { + props.selectionFocusedInfo.selectedCellIndex = undefined; + props.selectionFocusedInfo.selectedCellIndex = 10; + props.cellCount = 11; + props.busy = false; + mountToolbar(); + assertDisabled(Button.RunBelow); + }); + test('If not busy and selected cell is other than last cell, then enabled', () => { + props.selectionFocusedInfo.selectedCellId = 'My_Selected_CellId'; + props.selectionFocusedInfo.selectedCellIndex = 5; + props.cellCount = 11; + props.busy = false; + mountToolbar(); + assertEnabled(Button.RunBelow); + }); + test('When busy it is disabled', () => { + props.busy = true; + mountToolbar(); + assertDisabled(Button.RunBelow); + }); + test('When clicked dispatches executeBelow', () => { + props.selectionFocusedInfo.selectedCellId = 'My_Selected_CellId'; + props.selectionFocusedInfo.selectedCellIndex = 5; + props.cellCount = 11; + mountToolbar(); + clickButton(Button.RunBelow); + assert.isTrue(((props.executeCellAndBelow as any) as sinon.SinonStub).calledOnce); + assert.equal( + ((props.executeCellAndBelow as any) as sinon.SinonStub).firstCall.args[0], + 'My_Selected_CellId' + ); + }); + }); + suite('Restart & Interrupt Kernel', () => { + getNamesAndValues<ServerStatus>(ServerStatus).forEach((status) => { + // Should always be disabled if busy. + if (status.name === ServerStatus.NotStarted) { + // Should be disabled if not busy and status === 'Not Started'. + test(`If Kernel status is ${ServerStatus.NotStarted} and not busy, both are disabled`, () => { + props.kernel.jupyterServerStatus = ServerStatus.NotStarted; + props.busy = false; + mountToolbar(); + assertDisabled(Button.RestartKernel); + assertDisabled(Button.InterruptKernel); + }); + } else { + // Should be enabled if busy and status != 'Not Started'. + test(`If Kernel status is ${status.name}, both are enabled`, () => { + props.kernel.jupyterServerStatus = status.name as any; + props.busy = true; + mountToolbar(); + assertEnabled(Button.RestartKernel); + assertEnabled(Button.InterruptKernel); + }); + } + }); + test('When clicked dispatches restartKernel', () => { + mountToolbar(); + clickButton(Button.RestartKernel); + assert.isTrue(((props.restartKernel as any) as sinon.SinonStub).calledOnce); + }); + test('When clicked dispatches interruptKernel', () => { + mountToolbar(); + clickButton(Button.InterruptKernel); + assert.isTrue(((props.interruptKernel as any) as sinon.SinonStub).calledOnce); + }); + }); + suite('When trusted', () => { + test('Trust message shows "Trusted"', () => { + mountToolbar(); + const message = getTrustMessage(); + assert.equal(message.text(), 'Trusted'); + }); + test('Clicking trust message does nothing', () => { + mountToolbar(); + clickTrustMessage(); + assert.isTrue(((props.launchNotebookTrustPrompt as any) as sinon.SinonStub).notCalled); + }); + }); + suite('When untrusted', () => { + setup(() => { + props = { + baseTheme: '', + busy: false, + cellCount: 0, + dirty: false, + export: sinon.stub(), + exportAs: sinon.stub(), + font: { family: '', size: 1 }, + interruptKernel: sinon.stub(), + kernel: { + displayName: '', + jupyterServerStatus: ServerStatus.Busy, + localizedUri: '', + language: PYTHON_LANGUAGE + }, + restartKernel: sinon.stub(), + selectKernel: noopAny, + selectServer: noopAny, + addCell: sinon.stub(), + clearAllOutputs: sinon.stub(), + executeAbove: sinon.stub(), + executeAllCells: sinon.stub(), + executeCellAndBelow: sinon.stub(), + save: sinon.stub(), + selectionFocusedInfo: {}, + sendCommand: noopAny, + toggleVariableExplorer: sinon.stub(), + setVariableExplorerHeight: sinon.stub(), + launchNotebookTrustPrompt: sinon.stub(), + variablesVisible: false, + isNotebookTrusted: false + }; + }); + test('All toolbar buttons are disabled unless explicitly added to allowlist', () => { + mountToolbar(); + const buttons = wrapper.find(ImageButton); + let index = 0; + buttons.forEach((_b) => { + if (!allowList.includes(index)) { + assertDisabled(index); + } + index += 1; + }); + }); + test('Trust message shows "Not Trusted"', () => { + mountToolbar(); + const message = getTrustMessage(); + assert.equal(message.text(), 'Not Trusted'); + }); + test('Clicking trust message dispatches launchNotebookTrustPrompt', () => { + mountToolbar(); + clickTrustMessage(); + assert.isTrue(((props.launchNotebookTrustPrompt as any) as sinon.SinonStub).calledOnce); + }); + }); +}); diff --git a/src/test/datascience/nativeEditorTestHelpers.tsx b/src/test/datascience/nativeEditorTestHelpers.tsx new file mode 100644 index 000000000000..6a7d7347f462 --- /dev/null +++ b/src/test/datascience/nativeEditorTestHelpers.tsx @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; +import { ReactWrapper } from 'enzyme'; +import * as React from 'react'; +import { Uri } from 'vscode'; + +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { INotebookEditor, INotebookEditorProvider } from '../../client/datascience/types'; +import { CursorPos } from '../../datascience-ui/interactive-common/mainState'; +import { NativeCell } from '../../datascience-ui/native-editor/nativeCell'; +import { ImageButton } from '../../datascience-ui/react-common/imageButton'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { IMountedWebView } from './mountedWebView'; +import { getCellResults, getNativeFocusedEditor, injectCode, simulateKey } from './testHelpers'; +import { ITestNativeEditorProvider } from './testNativeEditorProvider'; + +// tslint:disable: no-any + +async function getOrCreateNativeEditor(ioc: DataScienceIocContainer, uri?: Uri) { + const notebookEditorProvider = ioc.get<ITestNativeEditorProvider>(INotebookEditorProvider); + let editor: INotebookEditor | undefined; + const messageWaiter = notebookEditorProvider.waitForMessage(uri, InteractiveWindowMessages.LoadAllCellsComplete); + if (uri) { + editor = await notebookEditorProvider.open(uri); + } else { + editor = await notebookEditorProvider.createNew(); + } + if (editor) { + await messageWaiter; + } + + return { editor, mount: notebookEditorProvider.getMountedWebView(editor) }; +} + +export async function createNewEditor(ioc: DataScienceIocContainer) { + return getOrCreateNativeEditor(ioc); +} + +export async function openEditor( + ioc: DataScienceIocContainer, + contents: string, + filePath: string = '/usr/home/test.ipynb' +) { + const uri = Uri.file(filePath); + ioc.setFileContents(uri, contents); + return getOrCreateNativeEditor(ioc, uri); +} + +// tslint:disable-next-line: no-any +export function getNativeCellResults( + mounted: IMountedWebView, + updater: () => Promise<void>, + renderPromiseGenerator?: () => Promise<void> +): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { + return getCellResults(mounted, 'NativeCell', updater, renderPromiseGenerator); +} + +// tslint:disable-next-line:no-any +export function runMountedTest(name: string, testFunc: (context: Mocha.Context) => Promise<void>) { + test(name, async function () { + // tslint:disable-next-line: no-invalid-this + await testFunc(this); + }); +} + +export function focusCell(mounted: IMountedWebView, index: number): Promise<void> { + const cell = mounted.wrapper.find(NativeCell).at(index); + if (cell) { + const vm = cell.props().cellVM; + if (!vm.focused) { + const focusChange = mounted.waitForMessage(InteractiveWindowMessages.FocusedCellEditor); + cell.props().focusCell(vm.cell.id, CursorPos.Current); + return focusChange; + } + } + return Promise.resolve(); +} + +// tslint:disable-next-line: no-any +export async function addCell(mounted: IMountedWebView, code: string, submit: boolean = true): Promise<void> { + // First get the main toolbar. We'll use this to add a cell. + const toolbar = mounted.wrapper.find('#main-panel-toolbar'); + assert.ok(toolbar, 'Cannot find the main panel toolbar during adding a cell'); + const ImageButtons = toolbar.find(ImageButton); + assert.equal(ImageButtons.length, 10, 'Toolbar buttons not found'); + const addButton = ImageButtons.at(5); + let update = mounted.waitForMessage(InteractiveWindowMessages.FocusedCellEditor); + addButton.simulate('click'); + + await update; + + let textArea: HTMLTextAreaElement | null; + if (code) { + // Type in the code + const editorEnzyme = getNativeFocusedEditor(mounted.wrapper); + textArea = injectCode(editorEnzyme, code); + } + + if (submit) { + // Then run the cell (use ctrl+enter so we don't add another cell) + update = mounted.waitForMessage(InteractiveWindowMessages.ExecutionRendered); + simulateKey(textArea!, 'Enter', false, true); + return update; + } +} + +export function closeNotebook(ioc: DataScienceIocContainer, editor: INotebookEditor): Promise<void> { + const promise = editor.dispose(); + ioc.getNativeWebPanel(editor).dispose(); + return promise; +} diff --git a/src/test/datascience/nativeEditorViewTracker.unit.test.ts b/src/test/datascience/nativeEditorViewTracker.unit.test.ts new file mode 100644 index 000000000000..7f73a03a9c3a --- /dev/null +++ b/src/test/datascience/nativeEditorViewTracker.unit.test.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { expect } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { EventEmitter, Memento, Uri } from 'vscode'; +import { NotebookModelChange } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { NativeEditor } from '../../client/datascience/interactive-ipynb/nativeEditor'; +import { NativeEditorViewTracker } from '../../client/datascience/interactive-ipynb/nativeEditorViewTracker'; +import { NativeEditorProvider } from '../../client/datascience/notebookStorage/nativeEditorProvider'; +import { NativeEditorNotebookModel } from '../../client/datascience/notebookStorage/notebookModel'; +import { INotebookEditor, INotebookEditorProvider, INotebookModel } from '../../client/datascience/types'; +import { MockMemento } from '../mocks/mementos'; + +suite('DataScience - View tracker', () => { + let editorProvider: INotebookEditorProvider; + let editor1: INotebookEditor; + let editor2: INotebookEditor; + let untitled1: INotebookEditor; + let untitledModel: INotebookModel; + let memento: Memento; + let openedList: string[]; + let editorList: INotebookEditor[]; + let openEvent: EventEmitter<INotebookEditor>; + let closeEvent: EventEmitter<INotebookEditor>; + let untitledChangeEvent: EventEmitter<NotebookModelChange>; + const file1 = Uri.file('foo.ipynb'); + const file2 = Uri.file('bar.ipynb'); + const untitledFile = Uri.parse('untitled://untitled.ipynb'); + setup(() => { + openEvent = new EventEmitter<INotebookEditor>(); + closeEvent = new EventEmitter<INotebookEditor>(); + untitledChangeEvent = new EventEmitter<NotebookModelChange>(); + openedList = []; + editorList = []; + editorProvider = mock(NativeEditorProvider); + untitledModel = mock(NativeEditorNotebookModel); + when(editorProvider.open(anything())).thenCall((f) => { + const key = f.toString(); + openedList.push(f.toString()); + // tslint:disable-next-line: no-unnecessary-initializer + let editorInstance: INotebookEditor | undefined = undefined; + if (key === file1.toString()) { + editorInstance = instance(editor1); + } + if (key === file2.toString()) { + editorInstance = instance(editor2); + } + if (key === untitledFile.toString()) { + editorInstance = instance(untitled1); + } + if (editorInstance) { + editorList.push(editorInstance); + openEvent.fire(editorInstance); + } + return Promise.resolve(); + }); + when(editorProvider.editors).thenReturn(editorList); + when(editorProvider.onDidCloseNotebookEditor).thenReturn(closeEvent.event); + when(editorProvider.onDidOpenNotebookEditor).thenReturn(openEvent.event); + editor1 = mock(NativeEditor); + when(editor1.file).thenReturn(file1); + when(editor1.isUntitled).thenReturn(false); + const model1 = mock<INotebookModel>(); + when(editor1.model).thenReturn(instance(model1)); + when(model1.isUntitled).thenReturn(false); + editor2 = mock(NativeEditor); + when(editor2.file).thenReturn(file2); + when(editor2.isUntitled).thenReturn(false); + const model2 = mock<INotebookModel>(); + when(editor2.model).thenReturn(instance(model2)); + when(model2.isUntitled).thenReturn(false); + untitled1 = mock(NativeEditor); + when(untitled1.file).thenReturn(untitledFile); + when(untitled1.model).thenReturn(instance(untitledModel)); + when(untitledModel.file).thenReturn(untitledFile); + when(untitledModel.changed).thenReturn(untitledChangeEvent.event); + when(untitledModel.isUntitled).thenReturn(true); + memento = new MockMemento(); + }); + + function activate(): Promise<void> { + openedList = []; + const viewTracker = new NativeEditorViewTracker(instance(editorProvider), memento, [], false); + return viewTracker.activate(); + } + + function close(editor: INotebookEditor) { + editorList = editorList.filter((f) => f.file.toString() !== editor.file.toString()); + closeEvent.fire(editor); + } + + function open(editor: INotebookEditor) { + editorList.push(editor); + openEvent.fire(editor); + } + test('Open a bunch of editors will reopen after shutdown', async () => { + await activate(); + open(instance(editor1)); + open(instance(editor2)); + await activate(); + expect(openedList).to.include(file1.toString(), 'First file not opened'); + expect(openedList).to.include(file2.toString(), 'Second file not opened'); + }); + test('Open a bunch of editors and close will not open after shutdown', async () => { + await activate(); + open(instance(editor1)); + open(instance(editor2)); + close(instance(editor1)); + close(instance(editor2)); + await activate(); + expect(openedList).to.not.include(file1.toString(), 'First file opened'); + expect(openedList).to.not.include(file2.toString(), 'Second file opened'); + }); + test('Untitled files open too', async () => { + await activate(); + open(instance(untitled1)); + open(instance(editor2)); + await activate(); + expect(openedList).to.not.include(untitledFile.toString(), 'First file should not open because not modified'); + expect(openedList).to.include(file2.toString(), 'Second file did not open'); + open(instance(untitled1)); + untitledChangeEvent.fire({ kind: 'clear', oldCells: [], oldDirty: false, newDirty: false, source: 'user' }); + await activate(); + expect(openedList).to.include(untitledFile.toString(), 'First file open because not modified'); + expect(openedList).to.include(file2.toString(), 'Second file did not open'); + }); + test('Opening more than once does not cause more than one open on reactivate', async () => { + await activate(); + open(instance(editor1)); + open(instance(editor1)); + await activate(); + expect(openedList.length).to.eq(1, 'Wrong length on reopen'); + }); +}); diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts new file mode 100644 index 000000000000..5e89e9f675bd --- /dev/null +++ b/src/test/datascience/notebook.functional.test.ts @@ -0,0 +1,1483 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { nbformat } from '@jupyterlab/coreutils'; +import { assert } from 'chai'; +import { ChildProcess } from 'child_process'; +import * as fs from 'fs-extra'; +import { injectable } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { Readable, Writable } from 'stream'; +import { anything, instance, mock, when } from 'ts-mockito'; +import * as uuid from 'uuid/v4'; +import { Disposable, Uri } from 'vscode'; +import { CancellationToken, CancellationTokenSource } from 'vscode-jsonrpc'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { Cancellation, CancellationError } from '../../client/common/cancellation'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { LocalZMQKernel } from '../../client/common/experiments/groups'; +import { traceError, traceInfo } from '../../client/common/logger'; +import { IPythonExecutionFactory } from '../../client/common/process/types'; +import { Product } from '../../client/common/types'; +import { createDeferred, waitForPromise } from '../../client/common/utils/async'; +import { noop } from '../../client/common/utils/misc'; +import { Architecture } from '../../client/common/utils/platform'; +import { getDefaultInteractiveIdentity } from '../../client/datascience/interactive-window/identity'; +import { getMessageForLibrariesNotInstalled } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService'; +import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; +import { JupyterKernelPromiseFailedError } from '../../client/datascience/jupyter/kernels/jupyterKernelPromiseFailedError'; +import { HostJupyterNotebook } from '../../client/datascience/jupyter/liveshare/hostJupyterNotebook'; +import { + CellState, + ICell, + IDataScienceFileSystem, + IJupyterConnection, + IJupyterExecution, + IJupyterKernelSpec, + INotebook, + INotebookExecutionLogger, + INotebookExporter, + INotebookImporter, + INotebookProvider, + InterruptResult +} from '../../client/datascience/types'; +import { IInterpreterService, IKnownSearchPathsForInterpreters } from '../../client/interpreter/contracts'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { concatMultilineString } from '../../datascience-ui/common'; +import { generateTestState, ICellViewModel } from '../../datascience-ui/interactive-common/mainState'; +import { sleep } from '../core'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { takeSnapshot, writeDiffSnapshot } from './helpers'; +import { SupportedCommands } from './mockJupyterManager'; +import { MockPythonService } from './mockPythonService'; +import { createPythonService, startRemoteServer } from './remoteTestHelpers'; + +// tslint:disable:no-any no-multiline-string max-func-body-length no-console max-classes-per-file trailing-comma +suite('DataScience notebook tests', () => { + [false, true].forEach((useRawKernel) => { + suite(`${useRawKernel ? 'With Direct Kernel' : 'With Jupyter Server'}`, () => { + const disposables: Disposable[] = []; + let notebookProvider: INotebookProvider; + + let ioc: DataScienceIocContainer; + let modifiedConfig = false; + const baseUri = Uri.file('foo.py'); + let snapshot: any; + + // tslint:disable-next-line: no-function-expression + setup(async function () { + ioc = new DataScienceIocContainer(); + if (ioc.shouldMockJupyter && useRawKernel) { + // tslint:disable-next-line: no-invalid-this + this.skip(); + return; + } else { + ioc.setExperimentState(LocalZMQKernel.experiment, useRawKernel); + } + ioc.registerDataScienceTypes(); + await ioc.activate(); + notebookProvider = ioc.get<INotebookProvider>(INotebookProvider); + }); + + suiteSetup(() => { + snapshot = takeSnapshot(); + }); + + suiteTeardown(() => { + writeDiffSnapshot(snapshot, `Notebook ${useRawKernel}`); + }); + + teardown(async () => { + try { + if (modifiedConfig) { + traceInfo('Attempting to put jupyter default config back'); + const procService = await createPythonService(ioc); + if (procService) { + await procService.exec(['-m', 'jupyter', 'notebook', '--generate-config', '-y'], {}); + } + } + traceInfo('Shutting down after test.'); + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < disposables.length; i += 1) { + const disposable = disposables[i]; + if (disposable) { + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + } + await ioc.dispose(); + traceInfo('Shutdown after test complete.'); + } catch (e) { + traceError(e); + } + if (process.env.PYTHONWARNINGS) { + delete process.env.PYTHONWARNINGS; + } + }); + + function escapePath(p: string) { + return p.replace(/\\/g, '\\\\'); + } + + function srcDirectory() { + return path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); + } + + function extractDataOutput(cell: ICell): any { + assert.equal(cell.data.cell_type, 'code', `Wrong type of cell returned`); + const codeCell = cell.data as nbformat.ICodeCell; + if (codeCell.outputs.length > 0) { + assert.equal(codeCell.outputs.length, 1, 'Cell length not correct'); + const data = codeCell.outputs[0].data; + const error = codeCell.outputs[0].evalue; + if (error) { + assert.fail(`Unexpected error: ${error}`); + } + assert.ok(data, `No data object on the cell`); + if (data) { + // For linter + assert.ok(data.hasOwnProperty('text/plain'), `Cell mime type not correct`); + assert.ok((data as any)['text/plain'], `Cell mime type not correct`); + return (data as any)['text/plain']; + } + } + } + + async function verifySimple( + notebook: INotebook | undefined, + code: string, + expectedValue: any, + pathVerify = false + ): Promise<void> { + const cells = await notebook!.execute(code, path.join(srcDirectory(), 'foo.py'), 2, uuid()); + assert.equal(cells.length, 1, `Wrong number of cells returned`); + const data = extractDataOutput(cells[0]); + if (pathVerify) { + // For a path comparison normalize output + const normalizedOutput = path.normalize(data).toUpperCase().replace(/'/g, ''); + const normalizedTarget = path.normalize(expectedValue).toUpperCase().replace(/'/g, ''); + assert.equal(normalizedOutput, normalizedTarget, 'Cell path values does not match'); + } else { + assert.equal(data, expectedValue, 'Cell value does not match'); + } + } + + async function verifyError( + notebook: INotebook | undefined, + code: string, + errorString: string + ): Promise<void> { + const cells = await notebook!.execute(code, path.join(srcDirectory(), 'foo.py'), 2, uuid()); + assert.equal(cells.length, 1, `Wrong number of cells returned`); + assert.equal(cells[0].data.cell_type, 'code', `Wrong type of cell returned`); + const cell = cells[0].data as nbformat.ICodeCell; + assert.equal(cell.outputs.length, 1, `Cell length not correct`); + const error = cell.outputs[0].evalue; + if (error) { + assert.ok(error, 'Error not found when expected'); + assert.equal(error, errorString, 'Unexpected error found'); + } + } + + async function verifyCell( + notebook: INotebook | undefined, + index: number, + code: string, + mimeType: string, + cellType: string, + verifyValue: (data: any) => void + ): Promise<void> { + // Verify results of an execute + const cells = await notebook!.execute(code, path.join(srcDirectory(), 'foo.py'), 2, uuid()); + assert.equal(cells.length, 1, `${index}: Wrong number of cells returned`); + if (cellType === 'code') { + assert.equal(cells[0].data.cell_type, cellType, `${index}: Wrong type of cell returned`); + const cell = cells[0].data as nbformat.ICodeCell; + assert.ok(cell.outputs.length >= 1, `${index}: Cell length not correct`); + const error = cell.outputs[0].evalue; + if (error) { + assert.ok(false, `${index}: Unexpected error: ${error}`); + } + const data = cell.outputs[0].data; + const text = cell.outputs[0].text; + assert.ok(data || text, `${index}: No data object on the cell for ${code}`); + if (data) { + // For linter + assert.ok( + data.hasOwnProperty(mimeType) || data.hasOwnProperty('text/plain'), + `${index}: Cell mime type not correct for ${JSON.stringify(data)}` + ); + const actualMimeType = data.hasOwnProperty(mimeType) ? mimeType : 'text/plain'; + assert.ok((data as any)[actualMimeType], `${index}: Cell mime type not correct`); + verifyValue((data as any)[actualMimeType]); + } + if (text) { + verifyValue(text); + } + } else if (cellType === 'markdown') { + assert.equal(cells[0].data.cell_type, cellType, `${index}: Wrong type of cell returned`); + const cell = cells[0].data as nbformat.IMarkdownCell; + const outputSource = concatMultilineString(cell.source); + verifyValue(outputSource); + } else if (cellType === 'error') { + const cell = cells[0].data as nbformat.ICodeCell; + assert.equal(cell.outputs.length, 1, `${index}: Cell length not correct`); + const error = cell.outputs[0].evalue; + assert.ok(error, 'Error not found when expected'); + verifyValue(error); + } + } + + function testMimeTypes( + types: { + markdownRegEx: string | undefined; + code: string; + mimeType: string; + result: any; + cellType: string; + verifyValue(data: any): void; + }[] + ) { + runTest('MimeTypes', async () => { + // Prefill with the output (This is only necessary for mocking) + types.forEach((t) => { + addMockData(t.code, t.result, t.mimeType, t.cellType); + }); + + // Test all mime types together so we don't have to startup and shutdown between + // each + const server = await createNotebook(); + if (server) { + for (let i = 0; i < types.length; i += 1) { + const markdownRegex = types[i].markdownRegEx ? types[i].markdownRegEx : ''; + ioc.getSettings().datascience.markdownRegularExpression = markdownRegex!; + await verifyCell( + server, + i, + types[i].code, + types[i].mimeType, + types[i].cellType, + types[i].verifyValue + ); + } + } + }); + } + + function runTest( + name: string, + func: (_this: Mocha.Context) => Promise<void>, + _notebookProc?: ChildProcess + ) { + test(name, async function () { + console.log(`Starting test ${name} ...`); + // tslint:disable-next-line: no-invalid-this + return func(this); + }); + } + + async function createNotebookWithNonDefaultConfig(): Promise<INotebook | undefined> { + const newSettings = { ...ioc.getSettings().datascience, useDefaultConfig: false }; + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath, newSettings); + return createNotebook(); + } + + async function createNotebook( + uri?: string, + launchingFile?: string, + expectFailure?: boolean + ): Promise<INotebook | undefined> { + // Catch exceptions. Throw a specific assertion if the promise fails + try { + if (uri) { + const newSettings = { ...ioc.getSettings().datascience, jupyterServerURI: uri }; + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath, newSettings); + } + launchingFile = launchingFile || path.join(srcDirectory(), 'foo.py'); + const notebook = await notebookProvider.getOrCreateNotebook({ + identity: getDefaultInteractiveIdentity() + }); + + if (notebook) { + await notebook.setLaunchingFile(launchingFile); + } + return notebook; + } catch (exc) { + if (!expectFailure) { + assert.ok(false, `Expected server to be created, but got ${exc}`); + } + } + } + + function addMockData( + code: string, + result: string | number | undefined, + mimeType?: string, + cellType?: string + ) { + if (ioc.mockJupyter) { + if (cellType && cellType === 'error') { + ioc.mockJupyter.addError(code, `${result}`); + } else { + ioc.mockJupyter.addCell(code, result, mimeType); + } + } + } + + function changeMockWorkingDirectory(workingDir: string) { + if (ioc.mockJupyter) { + ioc.mockJupyter.changeWorkingDirectory(workingDir); + } + } + + function addInterruptableMockData( + code: string, + resultGenerator: (c: CancellationToken) => Promise<{ result: string; haveMore: boolean }> + ) { + if (ioc.mockJupyter) { + ioc.mockJupyter.addContinuousOutputCell(code, resultGenerator); + } + } + + runTest('Remote Self Certs', async (_this: Mocha.Context) => { + const pythonService = await createPythonService(ioc, 2); + + // Skip test for older python and raw kernel and mac + if (pythonService && !useRawKernel && os.platform() !== 'darwin') { + // We will only connect if we allow for self signed cert connections + ioc.forceDataScienceSettingsChanged({ + allowUnauthorizedRemoteConnection: true, + jupyterLaunchTimeout: 60000 + }); + + const pemFile = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'datascience', + 'serverConfigFiles', + 'jcert.pem' + ); + const keyFile = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'datascience', + 'serverConfigFiles', + 'jkey.key' + ); + + const uri = await startRemoteServer(ioc, pythonService, [ + '-m', + 'jupyter', + 'notebook', + '--NotebookApp.open_browser=False', + '--NotebookApp.ip=*', + '--NotebookApp.port=9999', + `--certfile=${pemFile}`, + `--keyfile=${keyFile}` + ]); + + traceInfo('Waiting for notebook'); + // We have a connection string here, so try to connect jupyterExecution to the notebook server + const notebook = await createNotebook(uri); + if (!notebook) { + assert.fail(`Failed to connect to remote self cert server on ${uri}`); + } else { + await verifySimple(notebook, `a=1${os.EOL}a`, 1); + } + } else { + traceInfo('Remote Self Cert is not supported on 2.7'); + _this.skip(); + } + }); + + // Connect to a server that doesn't have a token or password, customers use this and we regressed it once + runTest( + 'Remote No Auth', + async () => { + const pythonService = await createPythonService(ioc); + + if (pythonService && !useRawKernel) { + const configFile = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'datascience', + 'serverConfigFiles', + 'remoteNoAuth.py' + ); + const uri = await startRemoteServer(ioc, pythonService, [ + '-m', + 'jupyter', + 'notebook', + `--config=${configFile}` + ]); + + // We have a connection string here, so try to connect jupyterExecution to the notebook server + const notebook = await createNotebook(uri); + if (!notebook) { + assert.fail('Failed to connect to remote password server'); + } else { + await verifySimple(notebook, `a=1${os.EOL}a`, 1); + } + } + }, + undefined + ); + + // For a connection to a remote machine that is not secure deny the connection and we should not connect + runTest( + 'Remote Deny Insecure', + async () => { + when( + ioc.applicationShell.showWarningMessage(anything(), anything(), anything(), anything()) + ).thenCall((_a1, _a2, a3, _a4) => { + return Promise.resolve(a3); + }); + + const pythonService = await createPythonService(ioc); + + if (pythonService && !useRawKernel) { + const configFile = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'datascience', + 'serverConfigFiles', + 'remoteNoAuth.py' + ); + const uri = await startRemoteServer(ioc, pythonService, [ + '-m', + 'jupyter', + 'notebook', + `--config=${configFile}` + ]); + + // To make sure we get an 'insecure' message, replace localhost with 127.0.0.1 + const replaced = uri.replace('localhost', '127.0.0.1'); + + // Try to create, we expect a failure here as we will deny the insecure connection + let madeItPast = false; + try { + await createNotebook(replaced, undefined); + madeItPast = true; + } catch (exc) { + assert.ok(exc.toString().includes('insecure'), `Invalid exception thrown: ${exc}`); + } + assert.notOk(madeItPast, 'Should have thrown an exception'); + } + }, + undefined + ); + runTest('Remote Password', async () => { + const pythonService = await createPythonService(ioc); + + if (pythonService && !useRawKernel && os.platform() !== 'darwin') { + const configFile = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'datascience', + 'serverConfigFiles', + 'remotePassword.py' + ); + const uri = await startRemoteServer(ioc, pythonService, [ + '-m', + 'jupyter', + 'notebook', + `--config=${configFile}` + ]); + + traceInfo('Waiting for notebook'); + + // We have a connection string here, so try to connect jupyterExecution to the notebook server + const notebook = await createNotebook(uri); + if (!notebook) { + assert.fail('Failed to connect to remote password server'); + } else { + await verifySimple(notebook, `a=1${os.EOL}a`, 1); + } + } + }); + + runTest('Remote', async () => { + const pythonService = await createPythonService(ioc); + + if (pythonService && !useRawKernel) { + const configFile = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'datascience', + 'serverConfigFiles', + 'remoteToken.py' + ); + + const uri = await startRemoteServer(ioc, pythonService, [ + '-m', + 'jupyter', + 'notebook', + `--config=${configFile}` + ]); + + // We have a connection string here, so try to connect jupyterExecution to the notebook server + const notebook = await createNotebook(uri); + if (!notebook) { + assert.fail('Failed to connect to remote server'); + } else { + await verifySimple(notebook, `a=1${os.EOL}a`, 1); + } + } + }); + + runTest('Creation', async () => { + await createNotebook(); + }); + + runTest('Failure', async (_this: Mocha.Context) => { + if (!useRawKernel) { + // Make a dummy class that will fail during launch + class FailedProcess extends JupyterExecutionFactory { + public isNotebookSupported = (): Promise<boolean> => { + return Promise.resolve(false); + }; + } + ioc.serviceManager.rebind<IJupyterExecution>(IJupyterExecution, FailedProcess); + await createNotebook(undefined, undefined, true); + } else { + // This test is useless for raw kernel. You can't fail to launch a python process + _this.skip(); + } + }); + + test('Not installed', async function () { + if (!useRawKernel) { + // Rewire our data we use to search for processes + @injectable() + class EmptyInterpreterService implements IInterpreterService { + public get hasInterpreters(): Promise<boolean> { + return Promise.resolve(true); + } + public onDidChangeInterpreterConfiguration(): Disposable { + return { dispose: noop }; + } + public onDidChangeInterpreter( + _listener: (e: void) => any, + _thisArgs?: any, + _disposables?: Disposable[] + ): Disposable { + return { dispose: noop }; + } + public onDidChangeInterpreterInformation( + _listener: (e: PythonEnvironment) => any, + _thisArgs?: any, + _disposables?: Disposable[] + ): Disposable { + return { dispose: noop }; + } + public getInterpreters(_resource?: Uri): Promise<PythonEnvironment[]> { + return Promise.resolve([]); + } + public autoSetInterpreter(): Promise<void> { + throw new Error('Method not implemented'); + } + public getActiveInterpreter(_resource?: Uri): Promise<PythonEnvironment | undefined> { + return Promise.resolve(undefined); + } + public getInterpreterDetails(_pythonPath: string, _resoure?: Uri): Promise<PythonEnvironment> { + throw new Error('Method not implemented'); + } + public refresh(_resource: Uri): Promise<void> { + throw new Error('Method not implemented'); + } + public initialize(): void { + throw new Error('Method not implemented'); + } + public getDisplayName(_interpreter: Partial<PythonEnvironment>): Promise<string> { + throw new Error('Method not implemented'); + } + public shouldAutoSetInterpreter(): Promise<boolean> { + throw new Error('Method not implemented'); + } + } + @injectable() + class EmptyPathService implements IKnownSearchPathsForInterpreters { + public getSearchPaths(): string[] { + return []; + } + } + ioc.serviceManager.rebind<IInterpreterService>(IInterpreterService, EmptyInterpreterService); + ioc.serviceManager.rebind<IKnownSearchPathsForInterpreters>( + IKnownSearchPathsForInterpreters, + EmptyPathService + ); + await createNotebook(undefined, undefined, true); + } else { + // tslint:disable-next-line: no-invalid-this + this.skip(); + } + }); + + runTest('Export/Import', async () => { + // Get a bunch of test cells (use our test cells from the react controls) + const testFolderPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); + const testState = generateTestState(testFolderPath); + const cells = testState.cellVMs.map((cellVM: ICellViewModel, _index: number) => { + return cellVM.cell; + }); + + // Translate this into a notebook + + // Make sure we have a change dir happening + const settings = { ...ioc.getSettings().datascience }; + settings.changeDirOnImportExport = true; + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath, settings); + + const exporter = ioc.serviceManager.get<INotebookExporter>(INotebookExporter); + const newFolderPath = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'datascience', + 'WorkspaceDir', + 'WorkspaceSubDir', + 'foo.ipynb' + ); + const notebook = await exporter.translateToNotebook(cells, newFolderPath); + assert.ok(notebook, 'Translate to notebook is failing'); + + // Make sure we added in our chdir + if (notebook) { + const nbcells = notebook.cells; + if (nbcells) { + const firstCellText: string = nbcells[0].source as string; + assert.ok(firstCellText.includes('os.chdir'), `${firstCellText} does not include 'os.chdir`); + } + } + + // Save to a temp file + const fileSystem = ioc.serviceManager.get<IDataScienceFileSystem>(IDataScienceFileSystem); + const importer = ioc.serviceManager.get<INotebookImporter>(INotebookImporter); + const temp = await fileSystem.createTemporaryLocalFile('.ipynb'); + + try { + await fs.writeFile(temp.filePath, JSON.stringify(notebook), 'utf8'); + // Try importing this. This should verify export works and that importing is possible + const results = await importer.importFromFile(Uri.file(temp.filePath)); + + // Make sure we have a single chdir in our results + const first = results.indexOf('os.chdir'); + assert.ok(first >= 0, 'No os.chdir in import'); + const second = results.indexOf('os.chdir', first + 1); + assert.equal(second, -1, 'More than one chdir in the import. It should be skipped'); + + // Make sure we have a cell in our results + assert.ok(/#\s*%%/.test(results), 'No cells in returned import'); + } finally { + importer.dispose(); + temp.dispose(); + } + }); + + // tslint:disable-next-line:no-invalid-template-strings + runTest('Verify ${fileDirname} working directory', async () => { + // Verify that the default ${fileDirname} setting sets the working directory to the file path + changeMockWorkingDirectory(`'${srcDirectory()}'`); + const notebook = await createNotebook(); + await verifySimple(notebook, 'import os\nos.getcwd()', srcDirectory(), true); + await verifySimple(notebook, 'import sys\nsys.path[0]', srcDirectory(), true); + }); + + runTest('Change Interpreter', async () => { + const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; + + // Real Jupyter doesn't help this test at all and is tricky to set up for it, so just skip it + if (!isRollingBuild) { + const server = await createNotebook(); + + // Create again, we should get the same server from the cache + const server2 = await createNotebook(); + // tslint:disable-next-line: triple-equals + assert.ok(server == server2, 'With no settings changed we should return the cached server'); + + // Create a new mock interpreter with a different path + const newPython: PythonEnvironment = { + path: '/foo/bar/baz/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 + }; + + // Add interpreter into mock jupyter service and set it as active + ioc.addInterpreter(newPython, SupportedCommands.all); + + // Create a new notebook, we should still be the same as interpreter is just saved for notebook creation + const server3 = await createNotebook(); + // tslint:disable-next-line: triple-equals + assert.ok(server == server3, 'With interpreter changed we should not return a new server'); + } else { + console.log(`Skipping Change Interpreter test in non-mocked Jupyter case`); + } + }); + + runTest('Restart kernel', async () => { + addMockData(`a=1${os.EOL}a`, 1); + addMockData(`a+=1${os.EOL}a`, 2); + addMockData(`a+=4${os.EOL}a`, 6); + addMockData('a', `name 'a' is not defined`, 'error'); + + const server = await createNotebook(); + + // Setup some state and verify output is correct + await verifySimple(server, `a=1${os.EOL}a`, 1); + await verifySimple(server, `a+=1${os.EOL}a`, 2); + await verifySimple(server, `a+=4${os.EOL}a`, 6); + + console.log('Waiting for idle'); + + // In unit tests we have to wait for status idle before restarting. Unit tests + // seem to be timing out if the restart throws any exceptions (even if they're caught) + await server!.waitForIdle(10000); + + console.log('Restarting kernel'); + try { + await server!.restartKernel(10000); + + console.log('Waiting for idle'); + await server!.waitForIdle(10000); + + console.log('Verifying restart'); + await verifyError(server, 'a', `name 'a' is not defined`); + } catch (exc) { + assert.ok( + exc instanceof JupyterKernelPromiseFailedError, + `Restarting did not timeout correctly for ${exc}` + ); + } + }); + + class TaggedCancellationTokenSource extends CancellationTokenSource { + public tag: string; + constructor(tag: string) { + super(); + this.tag = tag; + } + } + + async function testCancelableCall<T>( + method: (t: CancellationToken) => Promise<T>, + messageFormat: string, + timeout: number + ): Promise<boolean> { + const tokenSource = new TaggedCancellationTokenSource(messageFormat.format(timeout.toString())); + const disp = setTimeout( + (_s) => { + tokenSource.cancel(); + }, + timeout, + tokenSource.tag + ); + + try { + // tslint:disable-next-line:no-string-literal + (tokenSource.token as any)['tag'] = messageFormat.format(timeout.toString()); + await method(tokenSource.token); + } catch (exc) { + // This should happen. This means it was canceled. + assert.ok(exc instanceof CancellationError, `Non cancellation error found : ${exc.stack}`); + } finally { + clearTimeout(disp); + tokenSource.dispose(); + } + + return true; + } + + async function testCancelableMethod<T>( + method: (t: CancellationToken) => Promise<T>, + messageFormat: string, + short?: boolean + ): Promise<boolean> { + const timeouts = short ? [10, 20, 30, 100] : [300, 400, 500, 1000]; + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < timeouts.length; i += 1) { + await testCancelableCall(method, messageFormat, timeouts[i]); + } + + return true; + } + + runTest('Cancel execution', async (_this: Mocha.Context) => { + if (useRawKernel) { + // Not cancellable at the moment. Just starts a process + _this.skip(); + return; + } + if (ioc.mockJupyter) { + ioc.mockJupyter.setProcessDelay(2000); + addMockData(`a=1${os.EOL}a`, 1); + } + const jupyterExecution = ioc.get<IJupyterExecution>(IJupyterExecution); + + // Try different timeouts, canceling after the timeout on each + assert.ok( + await testCancelableMethod( + (t: CancellationToken) => jupyterExecution.connectToNotebookServer(undefined, t), + 'Cancel did not cancel start after {0}ms' + ) + ); + + if (ioc.mockJupyter) { + ioc.mockJupyter.setProcessDelay(undefined); + } + + // Make sure doing normal start still works + const nonCancelSource = new CancellationTokenSource(); + const server = await jupyterExecution.connectToNotebookServer(undefined, nonCancelSource.token); + const notebook = server + ? await server.createNotebook(baseUri, getDefaultInteractiveIdentity()) + : undefined; + assert.ok(notebook, 'Server not found with a cancel token that does not cancel'); + + // Make sure can run some code too + await verifySimple(notebook, `a=1${os.EOL}a`, 1); + + if (ioc.mockJupyter) { + ioc.mockJupyter.setProcessDelay(200); + } + + // Force a settings changed so that all of the cached data is cleared + ioc.forceSettingsChanged(undefined, '/usr/bin/test3/python'); + + assert.ok( + await testCancelableMethod( + (t: CancellationToken) => jupyterExecution.getUsableJupyterPython(t), + 'Cancel did not cancel getusable after {0}ms', + true + ) + ); + assert.ok( + await testCancelableMethod( + (t: CancellationToken) => jupyterExecution.isNotebookSupported(t), + 'Cancel did not cancel isNotebook after {0}ms', + true + ) + ); + assert.ok( + await testCancelableMethod( + (t: CancellationToken) => jupyterExecution.isImportSupported(t), + 'Cancel did not cancel isImport after {0}ms', + true + ) + ); + }); + + async function interruptExecute( + notebook: INotebook | undefined, + code: string, + interruptMs: number, + sleepMs: number + ): Promise<InterruptResult> { + let interrupted = false; + let finishedBefore = false; + const finishedPromise = createDeferred(); + let error; + const observable = notebook!.executeObservable(code, Uri.file('foo.py').fsPath, 0, uuid(), false); + observable.subscribe( + (c) => { + if (c.length > 0 && c[0].state === CellState.error) { + finishedBefore = !interrupted; + finishedPromise.resolve(); + } + if (c.length > 0 && c[0].state === CellState.finished) { + finishedBefore = !interrupted; + finishedPromise.resolve(); + } + }, + (err) => { + error = err; + finishedPromise.resolve(); + }, + () => finishedPromise.resolve() + ); + + // Then interrupt + interrupted = true; + const result = await notebook!.interruptKernel(interruptMs); + + // Then we should get our finish unless there was a restart + await waitForPromise(finishedPromise.promise, sleepMs); + assert.equal(finishedBefore, false, 'Finished before the interruption'); + assert.equal(error, undefined, 'Error thrown during interrupt'); + assert.ok( + finishedPromise.completed || + result === InterruptResult.TimedOut || + result === InterruptResult.Success, + `Interrupt restarted ${result} for: ${code}` + ); + + return result; + } + + runTest('Interrupt kernel', async (_this: Mocha.Context) => { + // Interrupt doesn't work yet for the raw kernel. + if (useRawKernel) { + _this.skip(); + return; + } + const returnable = `import signal +import _thread +import time + +keep_going = True +def handler(signum, frame): + global keep_going + print('signal') + keep_going = False + +signal.signal(signal.SIGINT, handler) + +while keep_going: + print(".") + time.sleep(.1)`; + const fourSecondSleep = `import time${os.EOL}time.sleep(4)${os.EOL}print("foo")`; + const kill = `import signal +import time +import os + +keep_going = True +def handler(signum, frame): + global keep_going + print('signal') + os._exit(-2) + +signal.signal(signal.SIGINT, handler) + +while keep_going: + print(".") + time.sleep(.1)`; + + // Add to our mock each of these, with each one doing something specific. + addInterruptableMockData(returnable, async (cancelToken: CancellationToken) => { + // This one goes forever until a cancellation happens + let haveMore = true; + try { + await Cancellation.race((_t) => sleep(100), cancelToken); + } catch { + haveMore = false; + } + return { result: '.', haveMore: haveMore }; + }); + addInterruptableMockData(fourSecondSleep, async (_cancelToken: CancellationToken) => { + // This one sleeps for four seconds and then it's done. + await sleep(4000); + return { result: 'foo', haveMore: false }; + }); + addInterruptableMockData(kill, async (cancelToken: CancellationToken) => { + // This one goes forever until a cancellation happens + let haveMore = true; + try { + await Cancellation.race((_t) => sleep(100), cancelToken); + } catch { + haveMore = false; + } + return { result: '.', haveMore: haveMore }; + }); + + const server = await createNotebook(); + + // Give some time for the server to finish. Otherwise our first interrupt will + // happen so fast, we'll interrupt startup. + await sleep(100); + + // Try with something we can interrupt + await interruptExecute(server, returnable, 1000, 1000); + + // Try again with something that doesn't return. However it should finish before + // we get to our own sleep. Note: We need the print so that the test knows something happened. + await interruptExecute(server, fourSecondSleep, 7000, 7000); + + // Try again with something that doesn't return. Make sure it times out + await interruptExecute(server, fourSecondSleep, 100, 7000); + + // The tough one, somethign that causes a kernel reset. + await interruptExecute(server, kill, 1000, 1000); + }); + + testMimeTypes([ + { + markdownRegEx: undefined, + code: `a=1 +a`, + mimeType: 'text/plain', + cellType: 'code', + result: 1, + verifyValue: (d) => assert.equal(d, 1, 'Plain text invalid') + }, + { + markdownRegEx: undefined, + code: `import pandas as pd +df = pd.read("${escapePath(path.join(srcDirectory(), 'DefaultSalesReport.csv'))}") +df.head()`, + mimeType: 'text/html', + result: `pd has no attribute 'read'`, + cellType: 'error', + // tslint:disable-next-line:quotemark + verifyValue: (d) => + assert.ok((d as string).includes("has no attribute 'read'"), 'Unexpected error result') + }, + { + markdownRegEx: undefined, + code: `import pandas as pd +df = pd.read_csv("${escapePath(path.join(srcDirectory(), 'DefaultSalesReport.csv'))}") +df.head()`, + mimeType: 'text/html', + result: `<td>A table</td>`, + cellType: 'code', + verifyValue: (d) => assert.ok(d.toString().includes('</td>'), 'Table not found') + }, + { + markdownRegEx: undefined, + code: `#%% [markdown]# +# #HEADER`, + mimeType: 'text/plain', + cellType: 'markdown', + result: '#HEADER', + verifyValue: (d) => assert.equal(d, ' #HEADER', 'Markdown incorrect') + }, + { + markdownRegEx: '\\s*#\\s*<markdowncell>', + code: `# <markdowncell> +# #HEADER`, + mimeType: 'text/plain', + cellType: 'markdown', + result: '#HEADER', + verifyValue: (d) => assert.equal(d, ' #HEADER', 'Markdown incorrect') + }, + { + // Test relative directories too. + markdownRegEx: undefined, + code: `import pandas as pd +df = pd.read_csv("./DefaultSalesReport.csv") +df.head()`, + mimeType: 'text/html', + cellType: 'code', + result: `<td>A table</td>`, + verifyValue: (d) => assert.ok(d.toString().includes('</td>'), 'Table not found') + }, + { + // Important to test as multiline cell magics only work if they are the first item in the cell + markdownRegEx: undefined, + code: `#%% +%%bash +echo 'hello'`, + mimeType: 'text/plain', + cellType: 'code', + result: 'hello', + verifyValue: (_d) => noop() // Anything is fine as long as it tries it. + }, + { + // Test shell command should work on PC / Mac / Linux + markdownRegEx: undefined, + code: `!echo world`, + mimeType: 'text/plain', + cellType: 'code', + result: 'world', + verifyValue: (d) => assert.ok(d.includes('world'), 'Cell command incorrect') + }, + { + // Plotly + markdownRegEx: undefined, + code: `import matplotlib.pyplot as plt +import matplotlib as mpl +import numpy as np +import pandas as pd +x = np.linspace(0, 20, 100) +plt.plot(x, np.sin(x)) +plt.show()`, + result: `00000`, + mimeType: 'image/svg+xml', + cellType: 'code', + verifyValue: (_d) => { + return; + } + } + ]); + + async function generateNonDefaultConfig() { + const usable = await ioc.getJupyterCapableInterpreter(); + assert.ok(usable, 'Cant find jupyter enabled python'); + + // Manually generate an invalid jupyter config + const procService = await createPythonService(ioc); + assert.ok(procService, 'Can not get a process service'); + const results = await procService!.exec(['-m', 'jupyter', 'notebook', '--generate-config', '-y'], {}); + + // Results should have our path to the config. + const match = /^.*\s+(.*jupyter_notebook_config.py)\s+.*$/m.exec(results.stdout); + assert.ok( + match && match !== null && match.length > 0, + 'Jupyter is not outputting the path to the config' + ); + const configPath = match !== null ? match[1] : ''; + const filesystem = ioc.serviceContainer.get<IDataScienceFileSystem>(IDataScienceFileSystem); + await filesystem.writeLocalFile(configPath, 'c.NotebookApp.password_required = True'); // This should make jupyter fail + modifiedConfig = true; + } + + runTest('Non default config fails', async () => { + if (!ioc.mockJupyter) { + await generateNonDefaultConfig(); + try { + await createNotebookWithNonDefaultConfig(); + assert.fail('Should not be able to connect to notebook server with bad config'); + } catch { + noop(); + } + } else { + // In the mock case, just make sure not using a config works + await createNotebookWithNonDefaultConfig(); + } + }); + + runTest('Non default config does not mess up default config', async () => { + if (!ioc.mockJupyter) { + await generateNonDefaultConfig(); + const server = await createNotebook(); + assert.ok(server, 'Never connected to a default server with a bad default config'); + + await verifySimple(server, `a=1${os.EOL}a`, 1); + } + }); + + runTest('Custom command line', async () => { + if (!ioc.mockJupyter && !useRawKernel) { + const tempDir = os.tmpdir(); + const settings = ioc.getSettings(); + settings.datascience.jupyterCommandLineArguments = [ + '--NotebookApp.port=9975', + `--notebook-dir=${tempDir}` + ]; + ioc.forceSettingsChanged(undefined, settings.pythonPath, settings.datascience); + const notebook = await createNotebook(); + assert.ok(notebook, 'Server should have started on port 9975'); + const hs = notebook as HostJupyterNotebook; + // Check port number. Should have at least started with the one specified. + if (hs.connection.type === 'jupyter') { + assert.ok(hs.connection.baseUrl.startsWith('http://localhost:99'), 'Port was not used'); + } + + await verifySimple(hs, `a=1${os.EOL}a`, 1); + } + }); + + runTest('Invalid kernel spec works', async () => { + if (ioc.mockJupyter && !useRawKernel) { + // Make a dummy class that will fail during launch + class FailedKernelSpec extends JupyterExecutionFactory { + protected async getMatchingKernelSpec( + _connection?: IJupyterConnection, + _cancelToken?: CancellationToken + ): Promise<IJupyterKernelSpec | undefined> { + return Promise.resolve(undefined); + } + } + ioc.serviceManager.rebind<IJupyterExecution>(IJupyterExecution, FailedKernelSpec); + addMockData(`a=1${os.EOL}a`, 1); + + const server = await createNotebook(); + assert.ok(server, 'Empty kernel spec messes up creating a server'); + + await verifySimple(server, `a=1${os.EOL}a`, 1); + } + }); + + runTest('Server cache working', async () => { + const s1 = await createNotebook(); + const s2 = await createNotebook(); + assert.ok(s1 === s2, 'Two servers not the same when they should be'); + await s1!.dispose(); + }); + + class DyingProcess implements ChildProcess { + public readonly exitCode: number | null = null; + public readonly signalCode: number | null = null; + public stdin: Writable; + public stdout: Readable; + public stderr: Readable; + public stdio: [Writable, Readable, Readable]; + public killed: boolean = false; + public pid: number = 1; + public connected: boolean = true; + constructor(private timeout: number) { + noop(); + this.stderr = this.stdout = new Readable(); + this.stdin = new Writable(); + this.stdio = [this.stdin, this.stdout, this.stderr]; + } + public kill(_signal?: string): void { + throw new Error('Method not implemented.'); + } + public send(_message: any, _sendHandle?: any, _options?: any, _callback?: any): any { + throw new Error('Method not implemented.'); + } + public disconnect(): void { + throw new Error('Method not implemented.'); + } + public unref(): void { + throw new Error('Method not implemented.'); + } + public ref(): void { + throw new Error('Method not implemented.'); + } + public addListener(_event: any, _listener: any): this { + throw new Error('Method not implemented.'); + } + public emit(_event: any, _message?: any, _sendHandle?: any, ..._rest: any[]): any { + throw new Error('Method not implemented.'); + } + public on(event: any, listener: any): this { + if (event === 'exit') { + setTimeout(() => listener(2), this.timeout); + } + return this; + } + public off(_event: string | symbol, _listener: (...args: any[]) => void): this { + throw new Error('Method not implemented.'); + } + public once(_event: any, _listener: any): this { + throw new Error('Method not implemented.'); + } + public prependListener(_event: any, _listener: any): this { + throw new Error('Method not implemented.'); + } + public prependOnceListener(_event: any, _listener: any): this { + throw new Error('Method not implemented.'); + } + public removeListener(_event: string | symbol, _listener: (...args: any[]) => void): this { + return this; + } + public removeAllListeners(_event?: string | symbol): this { + throw new Error('Method not implemented.'); + } + public setMaxListeners(_n: number): this { + throw new Error('Method not implemented.'); + } + public getMaxListeners(): number { + throw new Error('Method not implemented.'); + } + public listeners(_event: string | symbol): Function[] { + throw new Error('Method not implemented.'); + } + public rawListeners(_event: string | symbol): Function[] { + throw new Error('Method not implemented.'); + } + public eventNames(): (string | symbol)[] { + throw new Error('Method not implemented.'); + } + public listenerCount(_type: string | symbol): number { + throw new Error('Method not implemented.'); + } + } + + runTest( + 'Server death', + async () => { + if (ioc.mockJupyter) { + // Only run this test for mocks. We need to mock the server dying. + addMockData(`a=1${os.EOL}a`, 1); + const server = await createNotebook(); + assert.ok(server, 'Server died before running'); + + // Sleep for 100 ms so it crashes + await sleep(100); + + try { + await verifySimple(server, `a=1${os.EOL}a`, 1); + assert.ok(false, 'Exception should have been thrown'); + } catch { + noop(); + } + } + }, + new DyingProcess(100) + ); + + runTest('Execution logging', async () => { + const cellInputs: string[] = []; + const outputs: string[] = []; + @injectable() + class Logger implements INotebookExecutionLogger { + public onKernelRestarted() { + // Do nothing on restarted + } + public dispose() { + noop(); + } + public async preExecute(cell: ICell, silent: boolean): Promise<void> { + if (!silent) { + cellInputs.push(concatMultilineString(cell.data.source)); + } + } + public async postExecute(cell: ICell, silent: boolean): Promise<void> { + if (!silent) { + outputs.push(extractDataOutput(cell)); + } + } + } + ioc.serviceManager.add<INotebookExecutionLogger>(INotebookExecutionLogger, Logger); + addMockData(`a=1${os.EOL}a`, 1); + const server = await createNotebook(); + assert.ok(server, 'Server not created in logging case'); + await server!.execute(`a=1${os.EOL}a`, path.join(srcDirectory(), 'foo.py'), 2, uuid()); + assert.equal(cellInputs.length, 1, 'Not enough cell inputs'); + assert.ok(outputs.length >= 1, 'Not enough cell outputs'); + assert.equal(cellInputs[0], 'a=1\na', 'Cell inputs not captured'); + assert.equal(outputs[outputs.length - 1], '1', 'Cell outputs not captured'); + }); + + async function disableJupyter(pythonPath: string) { + const factory = ioc.serviceManager.get<IPythonExecutionFactory>(IPythonExecutionFactory); + const service = await factory.create({ pythonPath }); + const mockService = service as MockPythonService; + // Used by commands (can be removed when `src/client/datascience/jupyter/interpreter/jupyterCommand.ts` is deleted). + mockService.addExecResult(['-m', 'jupyter', 'notebook', '--version'], () => { + return Promise.resolve({ + stdout: '9.9.9.9', + stderr: 'Not supported' + }); + }); + + // Used by commands (can be removed when `src/client/datascience/jupyter/interpreter/jupyterCommand.ts` is deleted). + mockService.addExecResult(['-m', 'notebook', '--version'], () => { + return Promise.resolve({ + stdout: '', + stderr: 'Not supported' + }); + }); + // For new approach. + when(ioc.mockJupyter?.productInstaller.isInstalled(Product.jupyter)).thenResolve(false as any); + when(ioc.mockJupyter?.productInstaller.isInstalled(Product.notebook)).thenResolve(false as any); + when(ioc.mockJupyter?.productInstaller.isInstalled(Product.jupyter, anything())).thenResolve( + false as any + ); + when(ioc.mockJupyter?.productInstaller.isInstalled(Product.notebook, anything())).thenResolve( + false as any + ); + } + + test('Notebook launch failure', async function () { + if (!ioc.mockJupyter || useRawKernel) { + // tslint:disable-next-line: no-invalid-this + this.skip(); + } else { + const application = mock(ApplicationShell); + ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, instance(application)); + + const jupyterExecution = ioc.serviceManager.get<IJupyterExecution>(IJupyterExecution); + + // Change notebook command to fail with some goofy output + await disableJupyter(ioc.workingInterpreter.path); + await disableJupyter(ioc.workingInterpreter2.path); + + // Try creating a notebook + let threw = false; + try { + const testDir = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); + await jupyterExecution.connectToNotebookServer({ + usingDarkTheme: false, + workingDir: testDir, + purpose: '1', + allowUI: () => false + }); + } catch (e) { + threw = true; + // When using old command finder, the error is `Not Supported` (directly from stdout). - can be deprecated when jupyterCommandFinder.ts is deleted. + // When using new approach, we inform user that some packages are not installed. + const expectedErrorMsg = getMessageForLibrariesNotInstalled( + [Product.jupyter, Product.notebook], + 'Python' + ); + + assert.ok( + e.message.includes('Not supported') || e.message.includes(expectedErrorMsg), + `Wrong error thrown when notebook is created. Error is ${e.message}` + ); + } + + assert.ok(threw, 'No exception thrown during notebook creation'); + } + }); + + test('Notebook launch with PYTHONWARNINGS', async function () { + if (ioc.mockJupyter) { + // tslint:disable-next-line: no-invalid-this + this.skip(); + } else { + // Force python warnings to always + process.env[`PYTHONWARNINGS`] = 'always'; + + // Try creating a notebook + const server = await createNotebook(); + assert.ok(server, 'Server died before running'); + } + }); + + // tslint:disable-next-line: no-function-expression + runTest('Notebook launch retry', async function (_this: Mocha.Context) { + // Skipping for now. Re-enable to test idle timeouts + _this.skip(); + // ioc.getSettings().datascience.jupyterLaunchRetries = 1; + // ioc.getSettings().datascience.jupyterLaunchTimeout = 10000; + // ioc.getSettings().datascience.runStartupCommands = '%config Application.log_level="DEBUG"'; + // const log = `import logging + // logger = logging.getLogger() + // fhandler = logging.FileHandler(filename='D:\\Training\\mylog.log', mode='a') + // formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + // fhandler.setFormatter(formatter) + // logger.addHandler(fhandler) + // logger.setLevel(logging.DEBUG)`; + // for (let i = 0; i < 100; i += 1) { + // const notebook = await createNotebook(); + // assert.ok(notebook, 'did not create notebook'); + // await notebook!.dispose(); + // const exec = ioc.get<IJupyterExecution>(IJupyterExecution); + // await exec.dispose(); + // } + }); + + runTest('Startup commands', async () => { + ioc.getSettings().datascience.runStartupCommands = ['a=1', 'b=2']; + addMockData(`a=1\\nb=2`, undefined); + addMockData(`a`, 1); + addMockData(`b`, 2); + + const notebook = await createNotebook(); + assert.ok(notebook, 'did not create notebook'); + + await verifySimple(notebook, `a`, 1); + await verifySimple(notebook, `b`, 2); + }); + runTest('Current directory', async () => { + const rootFolder = ioc.get<IWorkspaceService>(IWorkspaceService).rootPath!; + const escapedPath = `'${rootFolder.replace(/\\/g, '\\\\')}'`; + addMockData(`import os\nos.getcwd()`, escapedPath); + const notebook = await notebookProvider.getOrCreateNotebook({ + identity: getDefaultInteractiveIdentity(), + resource: Uri.file(path.join(rootFolder, 'foo.ipynb')) + }); + + assert.ok(notebook, 'did not create notebook'); + await verifySimple(notebook, `import os\nos.getcwd()`, escapedPath, true); + }); + }); + }); +}); diff --git a/src/test/datascience/notebook/cellOutput.ds.test.ts b/src/test/datascience/notebook/cellOutput.ds.test.ts new file mode 100644 index 000000000000..e7a0c1dd0f63 --- /dev/null +++ b/src/test/datascience/notebook/cellOutput.ds.test.ts @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-require-imports no-var-requires +import { join } from 'path'; +import { Subject } from 'rxjs/Subject'; +import * as sinon from 'sinon'; +import { anything, instance, mock, reset, when } from 'ts-mockito'; +import { commands, Uri } from 'vscode'; +import { IVSCodeNotebook } from '../../../client/common/application/types'; +import { IDisposable } from '../../../client/common/types'; +import { + CellState, + ICell, + INotebook, + INotebookEditorProvider, + INotebookProvider +} from '../../../client/datascience/types'; +import { IExtensionTestApi } from '../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; +import { + assertHasExecutionCompletedSuccessfully, + assertHasExecutionCompletedWithErrors, + assertHasOutputInVSCell, + canRunTests, + closeNotebooksAndCleanUpAfterTests, + createTemporaryNotebook, + deleteAllCellsAndWait, + insertPythonCellAndWait, + trustAllNotebooks, + waitForExecutionCompletedSuccessfully, + waitForExecutionOrderInVSCCell, + waitForTextOutputInVSCode, + waitForVSCCellHasEmptyOutput, + waitForVSCCellIsRunning +} from './helper'; + +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); + +// tslint:disable: no-any no-invalid-this +suite('DataScience - VSCode Notebook - (fake execution) (Clearing Output)', function () { + this.timeout(10_000); + + let api: IExtensionTestApi; + let editorProvider: INotebookEditorProvider; + let vscodeNotebook: IVSCodeNotebook; + let notebookProvider: INotebookProvider; + let nb: INotebook; + let cellObservableResult: Subject<ICell[]>; + let cell2ObservableResult: Subject<ICell[]>; + + suiteSetup(async function () { + api = await initialize(); + if (!(await canRunTests())) { + return this.skip(); + } + await trustAllNotebooks(); + vscodeNotebook = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook); + notebookProvider = api.serviceContainer.get<INotebookProvider>(INotebookProvider); + editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + }); + suiteTeardown(() => closeNotebooksAndCleanUpAfterTests([])); + suite('Different notebooks in each test', () => { + const disposables2: IDisposable[] = []; + const templateIPynb = join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'datascience', + 'notebook', + 'with3CellsAndOutput.ipynb' + ); + suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables2)); + setup(async () => { + await trustAllNotebooks(); + const testIPynb = Uri.file(await createTemporaryNotebook(templateIPynb, disposables2)); + await editorProvider.open(testIPynb); + }); + test('Clearing output when not executing', async function () { + // tslint:disable-next-line: no-unused-expression + return this.skip(); + const cells = vscodeNotebook.activeNotebookEditor?.document.cells!; + + // Verify we have execution counts and output. + assertHasExecutionCompletedSuccessfully(cells[0]); + assertHasExecutionCompletedWithErrors(cells[1]); + assertHasExecutionCompletedSuccessfully(cells[2]); + assertHasOutputInVSCell(cells[0]); + assertHasOutputInVSCell(cells[1]); + assertHasOutputInVSCell(cells[2]); + + // Clear the cells + await commands.executeCommand('notebook.clearAllCellsOutputs'); + + for (let cellIndex = 0; cellIndex < 3; cellIndex += 1) { + // https://github.com/microsoft/vscode-python/issues/13159 + // await waitForExecutionOrderInVSCCell(cells[cellIndex], undefined); + + await waitForVSCCellHasEmptyOutput(cells[cellIndex]); + } + }); + }); + suite('Use same notebook for tests', () => { + suiteSetup(async () => { + await trustAllNotebooks(); + // Open a notebook and use this for all tests in this test suite. + await editorProvider.createNew(); + }); + setup(async () => { + sinon.restore(); + const getOrCreateNotebook = sinon.stub(notebookProvider, 'getOrCreateNotebook'); + nb = mock<INotebook>(); + (instance(nb) as any).then = undefined; + getOrCreateNotebook.resolves(instance(nb)); + + cellObservableResult = new Subject<ICell[]>(); + cell2ObservableResult = new Subject<ICell[]>(); + reset(nb); + when(nb.executeObservable(anything(), anything(), anything(), anything(), anything())).thenReturn( + cellObservableResult.asObservable() + ); + await deleteAllCellsAndWait(); + }); + teardown(() => { + cellObservableResult.unsubscribe(); + cell2ObservableResult.unsubscribe(); + }); + + test('Clear cell status, output and execution count before executing a cell', async function () { + // tslint:disable-next-line: no-unused-expression + return this.skip(); + + await insertPythonCellAndWait('# Some bogus cell', 0); + const vscCell = vscodeNotebook.activeNotebookEditor?.document.cells![0]!; + // Setup original state in cell. + vscCell.outputs = [{ outputKind: vscodeNotebookEnums.CellOutputKind.Text, text: 'Output1' }]; + vscCell.metadata.statusMessage = 'Error Message'; + vscCell.metadata.executionOrder = 999; + + // Once we execute the cell, the execution count & output should be cleared. + await commands.executeCommand('notebook.cell.execute'); + await waitForExecutionOrderInVSCCell(vscCell, undefined); + await waitForVSCCellHasEmptyOutput(vscCell); + await waitForVSCCellIsRunning(vscCell); + + // Now send some output. + const executionCount = 22; + cellObservableResult.next([ + { + data: { + cell_type: 'code', + execution_count: 22, + metadata: {}, + outputs: [{ output_type: 'stream', name: 'stdout', text: 'Hello' }], + source: '' + }, + file: '', + id: vscCell.uri.toString(), + line: 1, + state: CellState.executing + } + ]); + + // Confirm output was received by VS Code. + await waitForExecutionOrderInVSCCell(vscCell, executionCount); + await waitForTextOutputInVSCode(vscCell, 'Hello', 0); + + // Complete the execution. + cellObservableResult.complete(); + + // Confirm output is the same and status is a success. + await waitForExecutionCompletedSuccessfully(vscCell); + await waitForExecutionOrderInVSCCell(vscCell, executionCount); + await waitForTextOutputInVSCode(vscCell, 'Hello', 0); + }); + test('Clear cell output while executing will only clear output when executing a cell', async function () { + // tslint:disable-next-line: no-unused-expression + return this.skip(); + await insertPythonCellAndWait('# Some bogus cell', 0); + const vscCell = vscodeNotebook.activeNotebookEditor?.document.cells![0]!; + // Setup original state in cell. + vscCell.outputs = [{ outputKind: vscodeNotebookEnums.CellOutputKind.Text, text: 'Output1' }]; + vscCell.metadata.statusMessage = 'Error Message'; + vscCell.metadata.executionOrder = 999; + + // Once we execute the cell, the execution count & output should be cleared. + await commands.executeCommand('notebook.cell.execute'); + + await waitForExecutionOrderInVSCCell(vscCell, undefined); + await waitForVSCCellHasEmptyOutput(vscCell); + await waitForVSCCellIsRunning(vscCell); + + // Now send some output. + const executionCount = 22; + cellObservableResult.next([ + { + data: { + cell_type: 'code', + execution_count: 22, + metadata: {}, + outputs: [{ output_type: 'stream', name: 'stdout', text: 'Hello' }], + source: '' + }, + file: '', + id: vscCell.uri.toString(), + line: 1, + state: CellState.executing + } + ]); + + // Confirm output was received by VS Code. + await waitForExecutionOrderInVSCCell(vscCell, executionCount); + await waitForTextOutputInVSCode(vscCell, 'Hello', 0); + + // Clear output. + await commands.executeCommand('notebook.clearAllCellsOutputs'); + + // Confirm output was cleared & execution order has not been cleared & cell is still running. + await waitForVSCCellHasEmptyOutput(vscCell); + await waitForExecutionOrderInVSCCell(vscCell, executionCount); + await waitForVSCCellIsRunning(vscCell); + + // Complete the execution. + cellObservableResult.complete(); + + // Confirm output is the same and status is a success. + await waitForExecutionCompletedSuccessfully(vscCell); + await waitForExecutionOrderInVSCCell(vscCell, executionCount); + await waitForVSCCellHasEmptyOutput(vscCell); + }); + }); +}); diff --git a/src/test/datascience/notebook/contentProvider.ds.test.ts b/src/test/datascience/notebook/contentProvider.ds.test.ts new file mode 100644 index 000000000000..d2a64d11ef61 --- /dev/null +++ b/src/test/datascience/notebook/contentProvider.ds.test.ts @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-require-imports no-var-requires +import { nbformat } from '@jupyterlab/coreutils'; +import { assert } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { CellErrorOutput, Uri } from 'vscode'; +import { CellDisplayOutput } from '../../../../types/vscode-proposed'; +import { IVSCodeNotebook } from '../../../client/common/application/types'; +import { IDisposable } from '../../../client/common/types'; +import { INotebookStorageProvider } from '../../../client/datascience/notebookStorage/notebookStorageProvider'; +import { INotebookEditorProvider } from '../../../client/datascience/types'; +import { IExtensionTestApi } from '../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; +import { canRunTests, closeNotebooksAndCleanUpAfterTests, createTemporaryNotebook, trustAllNotebooks } from './helper'; +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); + +// tslint:disable: no-any no-invalid-this +suite('DataScience - VSCode Notebook - (Open)', function () { + this.timeout(15_000); + const templateIPynb = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'datascience', + 'notebook', + 'withOutput.ipynb' + ); + let api: IExtensionTestApi; + let testIPynb: Uri; + const disposables: IDisposable[] = []; + suiteSetup(async function () { + this.timeout(15_000); + api = await initialize(); + if (!(await canRunTests())) { + return this.skip(); + } + await trustAllNotebooks(); + }); + setup(async () => { + sinon.restore(); + // Don't use same file (due to dirty handling, we might save in dirty.) + // Cuz we won't save to file, hence extension will backup in dirty file and when u re-open it will open from dirty. + testIPynb = Uri.file(await createTemporaryNotebook(templateIPynb, disposables)); + }); + teardown(async () => closeNotebooksAndCleanUpAfterTests(disposables)); + test('Verify Notebook Json', async () => { + const storageProvider = api.serviceContainer.get<INotebookStorageProvider>(INotebookStorageProvider); + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'datascience', + 'notebook', + 'testJsonContents.ipynb' + ); + const model = await storageProvider.getOrCreateModel(Uri.file(file)); + disposables.push(model); + model.trust(); + const jsonStr = fs.readFileSync(file, { encoding: 'utf8' }); + + // JSON should be identical, before and after trusting a notebook. + assert.deepEqual(JSON.parse(jsonStr), JSON.parse(model.getContent())); + + model.trust(); + + assert.deepEqual(JSON.parse(jsonStr), JSON.parse(model.getContent())); + }); + test('Verify cells (content, metadata & output)', async () => { + const vscodeNotebook = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook); + const editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + const model = (await editorProvider.open(testIPynb))!.model!; + model.trust(); // We want to test the output as well. + + const notebook = vscodeNotebook.activeNotebookEditor?.document!; + + assert.equal(notebook.cells.length, model?.cells.length, 'Incorrect number of cells'); + assert.equal(notebook.cells.length, 6, 'Incorrect number of cells'); + + // Cell 1. + assert.equal(notebook.cells[0].cellKind, vscodeNotebookEnums.CellKind.Code, 'Cell1, type'); + assert.lengthOf(notebook.cells[0].outputs, 0, 'Cell1, outputs'); + assert.include(notebook.cells[0].document.getText(), 'a=1', 'Cell1, source'); + assert.isUndefined(notebook.cells[0].metadata.executionOrder, 'Cell1, execution count'); + assert.lengthOf(Object.keys(notebook.cells[0].metadata.custom || {}), 1, 'Cell1, metadata'); + assert.containsAllKeys(notebook.cells[0].metadata.custom || {}, { metadata: '' }, 'Cell1, metadata'); + + // Cell 2. + assert.equal(notebook.cells[1].cellKind, vscodeNotebookEnums.CellKind.Code, 'Cell2, type'); + assert.include(notebook.cells[1].document.getText(), 'pip list', 'Cell1, source'); + assert.lengthOf(notebook.cells[1].outputs, 1, 'Cell2, outputs'); + assert.equal(notebook.cells[1].outputs[0].outputKind, vscodeNotebookEnums.CellOutputKind.Rich, 'Cell2, output'); + assert.equal(notebook.cells[1].metadata.executionOrder, 3, 'Cell2, execution count'); + assert.lengthOf(Object.keys(notebook.cells[1].metadata.custom || {}), 1, 'Cell2, metadata'); + assert.deepEqual(notebook.cells[1].metadata.custom?.metadata.tags, ['WOW'], 'Cell2, metadata'); + + // Cell 3. + assert.equal(notebook.cells[2].cellKind, vscodeNotebookEnums.CellKind.Markdown, 'Cell3, type'); + assert.include(notebook.cells[2].document.getText(), '# HELLO WORLD', 'Cell3, source'); + assert.lengthOf(notebook.cells[2].outputs, 0, 'Cell3, outputs'); + assert.isUndefined(notebook.cells[2].metadata.executionOrder, 'Cell3, execution count'); + assert.lengthOf(Object.keys(notebook.cells[2].metadata.custom || {}), 1, 'Cell3, metadata'); + assert.isEmpty(notebook.cells[2].metadata.custom?.metadata, 'Cell3, metadata'); + + // Cell 4. + assert.equal(notebook.cells[3].cellKind, vscodeNotebookEnums.CellKind.Code, 'Cell4, type'); + assert.include(notebook.cells[3].document.getText(), 'with Error', 'Cell4, source'); + assert.lengthOf(notebook.cells[3].outputs, 1, 'Cell4, outputs'); + assert.equal( + notebook.cells[3].outputs[0].outputKind, + vscodeNotebookEnums.CellOutputKind.Error, + 'Cell4, output' + ); + const errorOutput = (notebook.cells[3].outputs[0] as unknown) as CellErrorOutput; + assert.equal(errorOutput.ename, 'SyntaxError', 'Cell4, output'); + assert.equal(errorOutput.evalue, 'invalid syntax (<ipython-input-1-8b7c24be1ec9>, line 1)', 'Cell3, output'); + assert.lengthOf(errorOutput.traceback, 1, 'Cell4, output'); + assert.include(errorOutput.traceback[0], 'invalid syntax', 'Cell4, output'); + assert.equal(notebook.cells[3].metadata.executionOrder, 1, 'Cell4, execution count'); + assert.lengthOf(Object.keys(notebook.cells[3].metadata.custom || {}), 1, 'Cell4, metadata'); + assert.isEmpty(notebook.cells[3].metadata.custom?.metadata, 'Cell4, metadata'); + + // Cell 5. + assert.equal(notebook.cells[4].cellKind, vscodeNotebookEnums.CellKind.Code, 'Cell5, type'); + assert.include(notebook.cells[4].document.getText(), 'import matplotlib', 'Cell5, source'); + assert.include(notebook.cells[4].document.getText(), 'plt.show()', 'Cell5, source'); + assert.lengthOf(notebook.cells[4].outputs, 1, 'Cell5, outputs'); + assert.equal(notebook.cells[4].outputs[0].outputKind, vscodeNotebookEnums.CellOutputKind.Rich, 'Cell5, output'); + const richOutput = (notebook.cells[4].outputs[0] as unknown) as CellDisplayOutput; + assert.containsAllKeys( + richOutput.data, + { 'text/plain': '', 'image/svg+xml': '', 'image/png': '' }, + 'Cell5, output' + ); + assert.deepEqual( + richOutput.metadata?.custom, + { + needs_background: 'light', + vscode: { + outputType: 'display_data' + } + }, + 'Cell5, output' + ); + + // Cell 6. + assert.equal(notebook.cells[5].cellKind, vscodeNotebookEnums.CellKind.Code, 'Cell6, type'); + assert.lengthOf(notebook.cells[5].outputs, 0, 'Cell6, outputs'); + assert.lengthOf(notebook.cells[5].document.getText(), 0, 'Cell6, source'); + assert.isUndefined(notebook.cells[5].metadata.executionOrder, 'Cell6, execution count'); + assert.lengthOf(Object.keys(notebook.cells[5].metadata.custom || {}), 1, 'Cell6, metadata'); + assert.containsAllKeys(notebook.cells[5].metadata.custom || {}, { metadata: '' }, 'Cell6, metadata'); + }); + test('Verify generation of NotebookJson', async () => { + const editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + const model = (await editorProvider.open(testIPynb))!.model!; + + const originalJsonStr = (await fs.readFile(templateIPynb, { encoding: 'utf8' })).trim(); + const originalJson: nbformat.INotebookContent = JSON.parse(originalJsonStr); + assert.deepEqual(JSON.parse(model.getContent()), originalJson, 'Untrusted notebook json content is invalid'); + // https://github.com/microsoft/vscode-python/issues/13155 + // assert.equal(model.getContent(), originalJsonStr, 'Untrusted notebook json not identical'); + + model.trust(); + assert.deepEqual(JSON.parse(model.getContent()), originalJson, 'Trusted notebook json content is invalid'); + // https://github.com/microsoft/vscode-python/issues/13155 + // assert.equal(model.getContent(), originalJsonStr, 'Trusted notebook json not identical'); + }); +}); diff --git a/src/test/datascience/notebook/contentProvider.unit.test.ts b/src/test/datascience/notebook/contentProvider.unit.test.ts new file mode 100644 index 000000000000..39fd8e5b3464 --- /dev/null +++ b/src/test/datascience/notebook/contentProvider.unit.test.ts @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { cloneDeep } from 'lodash'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Memento, Uri } from 'vscode'; +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); +import type { NotebookContentProvider as VSCodeNotebookContentProvider } from 'vscode-proposed'; +import { MARKDOWN_LANGUAGE, PYTHON_LANGUAGE } from '../../../client/common/constants'; +import { ICryptoUtils } from '../../../client/common/types'; +import { NotebookContentProvider } from '../../../client/datascience/notebook/contentProvider'; +import { NotebookEditorCompatibilitySupport } from '../../../client/datascience/notebook/notebookEditorCompatibilitySupport'; +import { INotebookStorageProvider } from '../../../client/datascience/notebookStorage/notebookStorageProvider'; +import { createNotebookModel } from './helper'; +// tslint:disable: no-any +suite('DataScience - NativeNotebook ContentProvider', () => { + let storageProvider: INotebookStorageProvider; + let contentProvider: VSCodeNotebookContentProvider; + const fileUri = Uri.file('a.ipynb'); + setup(async () => { + storageProvider = mock<INotebookStorageProvider>(); + const compatSupport = mock(NotebookEditorCompatibilitySupport); + when(compatSupport.canOpenWithOurNotebookEditor(anything())).thenReturn(true); + when(compatSupport.canOpenWithVSCodeNotebookEditor(anything())).thenReturn(true); + contentProvider = new NotebookContentProvider(instance(storageProvider), instance(compatSupport)); + }); + [true, false].forEach((isNotebookTrusted) => { + suite(isNotebookTrusted ? 'Trusted Notebook' : 'Un-trusted notebook', () => { + test('Return notebook with 2 cells', async () => { + const model = createNotebookModel( + isNotebookTrusted, + Uri.file('any'), + instance(mock<Memento>()), + instance(mock<ICryptoUtils>()), + { + cells: [ + { + cell_type: 'code', + execution_count: 10, + hasExecutionOrder: true, + outputs: [], + source: 'print(1)', + metadata: {} + }, + { + cell_type: 'markdown', + hasExecutionOrder: false, + source: '# HEAD', + metadata: {} + } + ] + } + ); + when(storageProvider.getOrCreateModel(anything(), anything(), anything(), anything())).thenResolve( + model + ); + + const notebook = await contentProvider.openNotebook(fileUri, {}); + + assert.isOk(notebook); + assert.deepEqual(notebook.languages, ['*']); + // ignore metadata we add. + const cellsWithoutCustomMetadata = notebook.cells.map((cell) => { + const cellToCompareWith = cloneDeep(cell); + delete cellToCompareWith.metadata?.custom; + return cellToCompareWith; + }); + + assert.equal(notebook.metadata.cellEditable, isNotebookTrusted); + assert.equal(notebook.metadata.cellRunnable, isNotebookTrusted); + assert.equal(notebook.metadata.editable, isNotebookTrusted); + assert.equal(notebook.metadata.runnable, isNotebookTrusted); + + assert.deepEqual(cellsWithoutCustomMetadata, [ + { + cellKind: (vscodeNotebookEnums as any).CellKind.Code, + language: PYTHON_LANGUAGE, + outputs: [], + source: 'print(1)', + metadata: { + editable: isNotebookTrusted, + executionOrder: 10, + hasExecutionOrder: true, + runState: (vscodeNotebookEnums as any).NotebookCellRunState.Success, + runnable: isNotebookTrusted + } + }, + { + cellKind: (vscodeNotebookEnums as any).CellKind.Markdown, + language: MARKDOWN_LANGUAGE, + outputs: [], + source: '# HEAD', + metadata: { + editable: isNotebookTrusted, + executionOrder: undefined, + hasExecutionOrder: false, + runnable: false + } + } + ]); + }); + + test('Return notebook with csharp language', async () => { + const model = createNotebookModel( + isNotebookTrusted, + Uri.file('any'), + instance(mock<Memento>()), + instance(mock<ICryptoUtils>()), + { + metadata: { + language_info: { + name: 'csharp' + }, + orig_nbformat: 5 + }, + cells: [ + { + cell_type: 'code', + execution_count: 10, + hasExecutionOrder: true, + outputs: [], + source: 'Console.WriteLine("1")', + metadata: {} + }, + { + cell_type: 'markdown', + hasExecutionOrder: false, + source: '# HEAD', + metadata: {} + } + ] + } + ); + when(storageProvider.getOrCreateModel(anything(), anything(), anything(), anything())).thenResolve( + model + ); + + const notebook = await contentProvider.openNotebook(fileUri, {}); + + assert.isOk(notebook); + assert.deepEqual(notebook.languages, ['*']); + + assert.equal(notebook.metadata.cellEditable, isNotebookTrusted); + assert.equal(notebook.metadata.cellRunnable, isNotebookTrusted); + assert.equal(notebook.metadata.editable, isNotebookTrusted); + assert.equal(notebook.metadata.runnable, isNotebookTrusted); + + // ignore metadata we add. + const cellsWithoutCustomMetadata = notebook.cells.map((cell) => { + const cellToCompareWith = cloneDeep(cell); + delete cellToCompareWith.metadata?.custom; + return cellToCompareWith; + }); + + assert.deepEqual(cellsWithoutCustomMetadata, [ + { + cellKind: (vscodeNotebookEnums as any).CellKind.Code, + language: 'csharp', + outputs: [], + source: 'Console.WriteLine("1")', + metadata: { + editable: isNotebookTrusted, + executionOrder: 10, + hasExecutionOrder: true, + runState: (vscodeNotebookEnums as any).NotebookCellRunState.Success, + runnable: isNotebookTrusted + } + }, + { + cellKind: (vscodeNotebookEnums as any).CellKind.Markdown, + language: MARKDOWN_LANGUAGE, + outputs: [], + source: '# HEAD', + metadata: { + editable: isNotebookTrusted, + executionOrder: undefined, + hasExecutionOrder: false, + runnable: false + } + } + ]); + }); + test('Verify mime types and order', () => { + // https://github.com/microsoft/vscode-python/issues/11880 + }); + }); + }); +}); diff --git a/src/test/datascience/notebook/edit.ds.test.ts b/src/test/datascience/notebook/edit.ds.test.ts new file mode 100644 index 000000000000..03ea4f780c86 --- /dev/null +++ b/src/test/datascience/notebook/edit.ds.test.ts @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable: no-var-requires no-require-imports no-invalid-this no-any + +import { assert } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { commands, Uri } from 'vscode'; +import { IDisposable } from '../../../client/common/types'; +import { ICell, INotebookEditorProvider, INotebookModel } from '../../../client/datascience/types'; +import { splitMultilineString } from '../../../datascience-ui/common'; +import { IExtensionTestApi, waitForCondition } from '../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { initialize } from '../../initialize'; +import { + canRunTests, + closeNotebooksAndCleanUpAfterTests, + createTemporaryNotebook, + deleteAllCellsAndWait, + deleteCell, + insertMarkdownCell, + insertMarkdownCellAndWait, + insertPythonCell, + insertPythonCellAndWait, + trustAllNotebooks +} from './helper'; + +suite('DataScience - VSCode Notebook (Edit)', function () { + this.timeout(10_000); + + const templateIPynb = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'datascience', + 'notebook', + 'test.ipynb' + ); + let testIPynb: Uri; + let api: IExtensionTestApi; + let editorProvider: INotebookEditorProvider; + const disposables: IDisposable[] = []; + suiteSetup(async function () { + this.timeout(10_000); + api = await initialize(); + if (!(await canRunTests())) { + return this.skip(); + } + await trustAllNotebooks(); + editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + }); + suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables)); + [true, false].forEach((isUntitled) => { + suite(isUntitled ? 'Untitled Notebook' : 'Existing Notebook', () => { + let model: INotebookModel; + setup(async () => { + sinon.restore(); + await trustAllNotebooks(); + // Don't use same file (due to dirty handling, we might save in dirty.) + // Cuz we won't save to file, hence extension will backup in dirty file and when u re-open it will open from dirty. + testIPynb = Uri.file(await createTemporaryNotebook(templateIPynb, disposables)); + + // Reset for tests, do this every time, as things can change due to config changes etc. + const editor = isUntitled ? await editorProvider.createNew() : await editorProvider.open(testIPynb); + model = editor.model!; + }); + teardown(() => closeNotebooksAndCleanUpAfterTests(disposables)); + async function assertTextInCell(cell: ICell, text: string) { + await waitForCondition( + async () => (cell.data.source as string[]).join('') === splitMultilineString(text).join(''), + 1_000, + `Text ${text} is not in ${(cell.data.source as string[]).join('')}` + ); + } + test('Insert and edit cell', async () => { + await deleteAllCellsAndWait(); + await insertPythonCellAndWait('HELLO'); + await assertTextInCell(model.cells[0], 'HELLO'); + }); + + test('Deleting a cell in an nb should update our NotebookModel', async () => { + // Delete first cell. + await deleteCell(0); + + // Verify model state is correct. + await waitForCondition(async () => model.cells.length === 0, 5_000, 'Not deleted'); + }); + test('Adding a markdown cell in an nb should update our NotebookModel', async () => { + await insertMarkdownCell('HELLO'); + + // Verify model has been updated + await waitForCondition(async () => model.cells.length === 2, 5_000, 'Not inserted'); + }); + test('Adding a markdown cell then deleting it should update our NotebookModel', async () => { + await insertMarkdownCell('HELLO'); + + // Verify events were fired. + await waitForCondition(async () => model.cells.length === 2, 5_000, 'Not inserted'); + + // Delete second cell. + await deleteCell(1); + + await waitForCondition(async () => model.cells.length === 1, 5_000, 'Not Deleted'); + }); + test('Adding a code cell in an nb should update our NotebookModel', async () => { + await insertPythonCell('HELLO'); + + await waitForCondition(async () => model.cells.length === 2, 5_000, 'Not Inserted'); + }); + test('Adding a code cell in specific position should update our NotebookModel', async () => { + await insertPythonCell('HELLO', 1); + + // Verify events were fired. + await waitForCondition(async () => model.cells.length === 2, 5_000, 'Not Inserted'); + assert.equal(model.cells.length, 2); + }); + function assertCodeCell(index: number, text: string) { + const cell = model.cells[index]; + assert.equal(cell.data.cell_type, 'code'); + assert.deepEqual(cell.data.source, text === '' ? [''] : splitMultilineString(text)); + return true; + } + function assertMarkdownCell(index: number, text?: string) { + const cell = model.cells[index]; + assert.equal(cell.data.cell_type, 'markdown'); + assert.deepEqual( + cell.data.source, + text === undefined ? [] : text === '' ? [''] : splitMultilineString(text) + ); + return true; + } + test('Change cell to markdown', async () => { + await deleteAllCellsAndWait(); + await insertPythonCellAndWait('HELLO'); + + await commands.executeCommand('notebook.cell.changeToMarkdown'); + + await waitForCondition(async () => assertMarkdownCell(0, 'HELLO'), 1_000, 'Not Changed'); + }); + test('Change cell to code', async function () { + this.timeout(10_000); + await deleteAllCellsAndWait(); + await insertMarkdownCellAndWait('HELLO'); + + await commands.executeCommand('notebook.cell.changeToCode'); + + await waitForCondition(async () => assertCodeCell(0, 'HELLO'), 1_000, 'Not Changed'); + }); + test('Toggle cells (code->markdown->code->markdown)', async () => { + await deleteAllCellsAndWait(); + await insertPythonCellAndWait('HELLO'); + + await commands.executeCommand('notebook.cell.changeToMarkdown'); + + await waitForCondition(async () => assertMarkdownCell(0, 'HELLO'), 1_000, 'Not Changed'); + + await commands.executeCommand('notebook.cell.changeToCode'); + + await waitForCondition(async () => assertCodeCell(0, 'HELLO'), 1_000, 'Not Changed'); + + await commands.executeCommand('notebook.cell.changeToMarkdown'); + + await waitForCondition(async () => assertMarkdownCell(0, 'HELLO'), 1_000, 'Not Changed'); + }); + test('Cut cell', async () => { + await commands.executeCommand('notebook.cell.cut'); + + await waitForCondition(async () => model.cells.length === 0, 5_000, 'Not Cut'); + }); + }); + }); +}); diff --git a/src/test/datascience/notebook/empty.ipynb b/src/test/datascience/notebook/empty.ipynb new file mode 100644 index 000000000000..6efc42a2c1ba --- /dev/null +++ b/src/test/datascience/notebook/empty.ipynb @@ -0,0 +1 @@ +{"cells":[],"nbformat":4,"nbformat_minor":2,"metadata":{"language_info":{"name":"python","codemirror_mode":{"name":"ipython","version":3}},"orig_nbformat":2,"file_extension":".py","mimetype":"text/x-python","name":"python","npconvert_exporter":"python","pygments_lexer":"ipython3","version":3}} diff --git a/src/test/datascience/notebook/empty.py b/src/test/datascience/notebook/empty.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/datascience/notebook/executionService.ds.test.ts b/src/test/datascience/notebook/executionService.ds.test.ts new file mode 100644 index 000000000000..a2165b660984 --- /dev/null +++ b/src/test/datascience/notebook/executionService.ds.test.ts @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-require-imports no-var-requires +import { assert, expect } from 'chai'; +import * as dedent from 'dedent'; +import * as sinon from 'sinon'; +import { CellDisplayOutput, commands } from 'vscode'; +import { CellErrorOutput } from '../../../../typings/vscode-proposed'; +import { IVSCodeNotebook } from '../../../client/common/application/types'; +import { IDisposable } from '../../../client/common/types'; +import { INotebookContentProvider } from '../../../client/datascience/notebook/types'; +import { INotebookEditorProvider } from '../../../client/datascience/types'; +import { createEventHandler, IExtensionTestApi, sleep, waitForCondition } from '../../common'; +import { initialize } from '../../initialize'; +import { + assertHasExecutionCompletedSuccessfully, + assertHasExecutionCompletedWithErrors, + assertHasTextOutputInVSCode, + assertNotHasTextOutputInVSCode, + assertVSCCellHasErrors, + canRunTests, + closeNotebooksAndCleanUpAfterTests, + deleteAllCellsAndWait, + executeActiveDocument, + executeCell, + insertPythonCellAndWait, + startJupyter, + trustAllNotebooks +} from './helper'; + +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); + +// tslint:disable: no-any no-invalid-this +suite('DataScience - VSCode Notebook - (Execution) (slow)', function () { + this.timeout(120_000); + + let api: IExtensionTestApi; + let editorProvider: INotebookEditorProvider; + const disposables: IDisposable[] = []; + let vscodeNotebook: IVSCodeNotebook; + suiteSetup(async function () { + this.timeout(120_000); + api = await initialize(); + if (!(await canRunTests())) { + return this.skip(); + } + await trustAllNotebooks(); + await startJupyter(false); // This should create a new notebook + sinon.restore(); + vscodeNotebook = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook); + editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + }); + setup(deleteAllCellsAndWait); + suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables)); + test('Execute cell using VSCode Kernel', async () => { + await insertPythonCellAndWait('print("Hello World")', 0); + const cell = vscodeNotebook.activeNotebookEditor?.document.cells![0]!; + + await executeCell(cell); + + // Wait till execution count changes and status is success. + await waitForCondition( + async () => assertHasExecutionCompletedSuccessfully(cell), + 15_000, + 'Cell did not get executed' + ); + }); + test('Executed events are triggered', async () => { + await insertPythonCellAndWait('print("Hello World")', 0); + const cell = vscodeNotebook.activeNotebookEditor?.document.cells![0]!; + + const executed = createEventHandler(editorProvider.activeEditor!, 'executed', disposables); + const codeExecuted = createEventHandler(editorProvider.activeEditor!, 'executed', disposables); + await executeCell(cell); + + // Wait till execution count changes and status is success. + await waitForCondition( + async () => assertHasExecutionCompletedSuccessfully(cell), + 15_000, + 'Cell did not get executed' + ); + + await executed.assertFired(1_000); + await codeExecuted.assertFired(1_000); + }); + test('Empty cell will not get executed', async () => { + await insertPythonCellAndWait('', 0); + const cell = vscodeNotebook.activeNotebookEditor?.document.cells![0]!; + await executeCell(cell); + + // After 2s, confirm status has remained unchanged. + await sleep(2_000); + assert.isUndefined(cell?.metadata.runState); + }); + test('Empty cells will not get executed when running whole document', async () => { + await insertPythonCellAndWait('', 0); + await insertPythonCellAndWait('print("Hello World")', 1); + const cells = vscodeNotebook.activeNotebookEditor?.document.cells!; + + await executeActiveDocument(); + + // Wait till execution count changes and status is success. + await waitForCondition( + async () => assertHasExecutionCompletedSuccessfully(cells[1]), + 15_000, + 'Cell did not get executed' + ); + assert.isUndefined(cells[0].metadata.runState); + }); + test('Execute cell should mark a notebook as being dirty', async () => { + await insertPythonCellAndWait('print("Hello World")', 0); + const contentProvider = api.serviceContainer.get<INotebookContentProvider>(INotebookContentProvider); + const cell = vscodeNotebook.activeNotebookEditor?.document.cells![0]!; + const changedEvent = createEventHandler(contentProvider, 'onDidChangeNotebook', disposables); + + await executeCell(cell); + + // Wait till execution count changes and status is success. + await waitForCondition( + async () => assertHasExecutionCompletedSuccessfully(cell), + 15_000, + 'Cell did not get executed' + ); + assert.ok(changedEvent.fired, 'Notebook should be dirty after executing a cell'); + }); + test('Verify Cell output, execution count and status', async () => { + await insertPythonCellAndWait('print("Hello World")', 0); + const cell = vscodeNotebook.activeNotebookEditor?.document.cells![0]!; + + await executeActiveDocument(); + + // Wait till execution count changes and status is success. + await waitForCondition( + async () => assertHasExecutionCompletedSuccessfully(cell), + 15_000, + 'Cell did not get executed' + ); + + // Verify output. + assertHasTextOutputInVSCode(cell, 'Hello World', 0); + + // Verify execution count. + assert.ok(cell.metadata.executionOrder, 'Execution count should be > 0'); + }); + test('Verify multiple cells get executed', async () => { + await insertPythonCellAndWait('print("Foo Bar")', 0); + await insertPythonCellAndWait('print("Hello World")', 1); + const cells = vscodeNotebook.activeNotebookEditor?.document.cells!; + + await executeActiveDocument(); + + // Wait till execution count changes and status is success. + await waitForCondition( + async () => + assertHasExecutionCompletedSuccessfully(cells[0]) && assertHasExecutionCompletedSuccessfully(cells[1]), + 15_000, + 'Cells did not get executed' + ); + + // Verify output. + assertHasTextOutputInVSCode(cells[0], 'Foo Bar', 0); + assertHasTextOutputInVSCode(cells[1], 'Hello World', 0); + + // Verify execution count. + assert.ok(cells[0].metadata.executionOrder, 'Execution count should be > 0'); + assert.equal(cells[1].metadata.executionOrder! - 1, cells[0].metadata.executionOrder!); + }); + test('Verify metadata for successfully executed cell', async () => { + await insertPythonCellAndWait('print("Foo Bar")', 0); + const cell = vscodeNotebook.activeNotebookEditor?.document.cells![0]!; + + await executeActiveDocument(); + + // Wait till execution count changes and status is success. + await waitForCondition( + async () => assertHasExecutionCompletedSuccessfully(cell), + 15_000, + 'Cell did not get executed' + ); + + expect(cell.metadata.executionOrder).to.be.greaterThan(0, 'Execution count should be > 0'); + expect(cell.metadata.runStartTime).to.be.greaterThan(0, 'Start time should be > 0'); + expect(cell.metadata.lastRunDuration).to.be.greaterThan(0, 'Duration should be > 0'); + assert.equal(cell.metadata.runState, vscodeNotebookEnums.NotebookCellRunState.Success, 'Incorrect State'); + assert.equal(cell.metadata.statusMessage, '', 'Incorrect Status message'); + }); + test('Verify output & metadata for executed cell with errors', async () => { + await insertPythonCellAndWait('print(abcd)', 0); + const cell = vscodeNotebook.activeNotebookEditor?.document.cells![0]!; + + await executeActiveDocument(); + + // Wait till execution count changes and status is error. + await waitForCondition( + async () => assertHasExecutionCompletedWithErrors(cell), + 15_000, + 'Cell did not get executed' + ); + + assert.lengthOf(cell.outputs, 1, 'Incorrect output'); + const errorOutput = cell.outputs[0] as CellErrorOutput; + assert.equal(errorOutput.outputKind, vscodeNotebookEnums.CellOutputKind.Error, 'Incorrect output'); + assert.equal(errorOutput.ename, 'NameError', 'Incorrect ename'); // As status contains ename, we don't want this displayed again. + assert.equal(errorOutput.evalue, "name 'abcd' is not defined", 'Incorrect evalue'); // As status contains ename, we don't want this displayed again. + assert.isNotEmpty(errorOutput.traceback, 'Incorrect traceback'); + expect(cell.metadata.executionOrder).to.be.greaterThan(0, 'Execution count should be > 0'); + expect(cell.metadata.runStartTime).to.be.greaterThan(0, 'Start time should be > 0'); + expect(cell.metadata.lastRunDuration).to.be.greaterThan(0, 'Duration should be > 0'); + assert.equal(cell.metadata.runState, vscodeNotebookEnums.NotebookCellRunState.Error, 'Incorrect State'); + assert.include(cell.metadata.statusMessage!, 'NameError', 'Must contain error message'); + assert.include(cell.metadata.statusMessage!, 'abcd', 'Must contain error message'); + }); + test('Updating display data', async () => { + await insertPythonCellAndWait('from IPython.display import Markdown\n', 0); + await insertPythonCellAndWait('dh = display(display_id=True)\n', 1); + await insertPythonCellAndWait('dh.update(Markdown("foo"))\n', 2); + const displayCell = vscodeNotebook.activeNotebookEditor?.document.cells![1]!; + const updateCell = vscodeNotebook.activeNotebookEditor?.document.cells![2]!; + + await executeActiveDocument(); + + // Wait till execution count changes and status is success. + await waitForCondition( + async () => assertHasExecutionCompletedSuccessfully(updateCell), + 15_000, + 'Cell did not get executed' + ); + + assert.lengthOf(displayCell.outputs, 1, 'Incorrect output'); + const markdownOutput = displayCell.outputs[0] as CellDisplayOutput; + assert.equal(markdownOutput.outputKind, vscodeNotebookEnums.CellOutputKind.Rich, 'Incorrect output'); + expect(displayCell.metadata.executionOrder).to.be.greaterThan(0, 'Execution count should be > 0'); + expect(displayCell.metadata.runStartTime).to.be.greaterThan(0, 'Start time should be > 0'); + expect(displayCell.metadata.lastRunDuration).to.be.greaterThan(0, 'Duration should be > 0'); + expect(markdownOutput.data['text/markdown']).to.be.equal('foo', 'Display cell did not update'); + }); + test('Clearing output while executing will ensure output is cleared', async function () { + // https://github.com/microsoft/vscode-python/issues/12302 + return this.skip(); + // Assume you are executing a cell that prints numbers 1-100. + // When printing number 50, you click clear. + // Cell output should now start printing output from 51 onwards, & not 1. + await insertPythonCellAndWait( + dedent` + print("Start") + import time + for i in range(100): + time.sleep(0.1) + print(i) + + print("End")`, + 0 + ); + const cell = vscodeNotebook.activeNotebookEditor?.document.cells![0]!; + + await executeActiveDocument(); + + // Wait till execution count changes and status is error. + await waitForCondition( + async () => assertHasTextOutputInVSCode(cell, 'Start', 0, false), + 15_000, + 'Cell did not get executed' + ); + + // Clear the cells + await commands.executeCommand('notebook.clearAllCellsOutputs'); + // Wait till execution count changes and status is error. + await waitForCondition( + async () => assertNotHasTextOutputInVSCode(cell, 'Start', 0, false), + 5_000, + 'Cell did not get cleared' + ); + + // Interrupt the kernel). + await commands.executeCommand('notebook.cancelExecution'); + await waitForCondition(async () => assertVSCCellHasErrors(cell), 1_000, 'Execution not cancelled'); + + // Verify that it hasn't got added (even after interrupting). + assertNotHasTextOutputInVSCode(cell, 'Start', 0, false); + }); +}); diff --git a/src/test/datascience/notebook/helper.ts b/src/test/datascience/notebook/helper.ts new file mode 100644 index 000000000000..125443ab63a1 --- /dev/null +++ b/src/test/datascience/notebook/helper.ts @@ -0,0 +1,457 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable: no-var-requires no-require-imports no-invalid-this no-any + +import { nbformat } from '@jupyterlab/coreutils'; +import { assert, expect } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as tmp from 'tmp'; +import { instance, mock } from 'ts-mockito'; +import { commands, Memento, TextDocument, Uri } from 'vscode'; +import { NotebookCell, NotebookDocument } from '../../../../types/vscode-proposed'; +import { CellDisplayOutput } from '../../../../typings/vscode-proposed'; +import { IApplicationEnvironment, IVSCodeNotebook } from '../../../client/common/application/types'; +import { MARKDOWN_LANGUAGE, PYTHON_LANGUAGE } from '../../../client/common/constants'; +import { IConfigurationService, ICryptoUtils, IDisposable } from '../../../client/common/types'; +import { noop, swallowExceptions } from '../../../client/common/utils/misc'; +import { Identifiers } from '../../../client/datascience/constants'; +import { JupyterNotebookView } from '../../../client/datascience/notebook/constants'; +import { createVSCNotebookCellDataFromCell } from '../../../client/datascience/notebook/helpers/helpers'; +import { INotebookContentProvider } from '../../../client/datascience/notebook/types'; +import { VSCodeNotebookModel } from '../../../client/datascience/notebookStorage/vscNotebookModel'; +import { + CellState, + ICell, + INotebookEditorProvider, + INotebookModel, + INotebookProvider +} from '../../../client/datascience/types'; +import { createEventHandler, waitForCondition } from '../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { closeActiveWindows, initialize } from '../../initialize'; +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); + +async function getServices() { + const api = await initialize(); + return { + contentProvider: api.serviceContainer.get<INotebookContentProvider>(INotebookContentProvider), + vscodeNotebook: api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook), + editorProvider: api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider) + }; +} + +export async function insertMarkdownCell(source: string, index: number = 0) { + const { vscodeNotebook } = await getServices(); + const vscEditor = vscodeNotebook.activeNotebookEditor; + await new Promise((resolve) => + vscEditor?.edit((builder) => { + builder.insert(index, source, MARKDOWN_LANGUAGE, vscodeNotebookEnums.CellKind.Markdown, [], undefined); + resolve(); + }) + ); + + await waitForCondition( + async () => vscEditor?.document.cells[index].document.getText().trim() === source.trim(), + 5_000, + 'Cell not inserted' + ); +} +export async function insertPythonCell(source: string, index: number = 0) { + const { vscodeNotebook } = await getServices(); + const vscEditor = vscodeNotebook.activeNotebookEditor; + await new Promise((resolve) => + vscEditor?.edit((builder) => { + builder.insert(index, source, PYTHON_LANGUAGE, vscodeNotebookEnums.CellKind.Code, [], undefined); + resolve(); + }) + ); + + await waitForCondition( + async () => vscEditor?.document.cells[index].document.getText().trim() === source.trim(), + 5_000, + 'Cell not inserted' + ); +} +export async function insertPythonCellAndWait(source: string, index: number = 0) { + await insertPythonCell(source, index); +} +export async function insertMarkdownCellAndWait(source: string, index: number = 0) { + await insertMarkdownCell(source, index); +} +export async function deleteCell(index: number = 0) { + const { vscodeNotebook } = await getServices(); + const activeEditor = vscodeNotebook.activeNotebookEditor; + await new Promise((resolve) => + activeEditor?.edit((builder) => { + builder.delete(index); + resolve(); + }) + ); +} +export async function deleteAllCellsAndWait(index: number = 0) { + const { vscodeNotebook } = await getServices(); + const activeEditor = vscodeNotebook.activeNotebookEditor; + if (!activeEditor) { + return; + } + const vscCells = activeEditor.document.cells!; + let previousCellOut = vscCells.length; + while (previousCellOut) { + await new Promise((resolve) => + activeEditor?.edit((builder) => { + builder.delete(index); + resolve(); + }) + ); + // Wait for cell to get deleted. + await waitForCondition(async () => vscCells.length === previousCellOut - 1, 1_000, 'Cell not deleted'); + previousCellOut = vscCells.length; + } +} + +export async function createTemporaryFile(options: { + templateFile: string; + dir: string; +}): Promise<{ file: string } & IDisposable> { + const extension = path.extname(options.templateFile); + const tempFile = tmp.tmpNameSync({ postfix: extension, dir: options.dir }); + await fs.copyFile(options.templateFile, tempFile); + return { file: tempFile, dispose: () => swallowExceptions(() => fs.unlinkSync(tempFile)) }; +} + +export async function createTemporaryNotebook(templateFile: string, disposables: IDisposable[]): Promise<string> { + const extension = path.extname(templateFile); + fs.ensureDirSync(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'tmp')); + const tempFile = tmp.tmpNameSync({ postfix: extension, dir: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'tmp') }); + await fs.copyFile(templateFile, tempFile); + disposables.push({ dispose: () => swallowExceptions(() => fs.unlinkSync(tempFile)) }); + return tempFile; +} + +export function disposeAllDisposables(disposables: IDisposable[]) { + while (disposables.length) { + disposables.pop()?.dispose(); // NOSONAR; + } +} + +export async function canRunTests() { + const api = await initialize(); + const appEnv = api.serviceContainer.get<IApplicationEnvironment>(IApplicationEnvironment); + return appEnv.extensionChannel !== 'stable'; +} + +/** + * We will be editing notebooks, to close notebooks them we need to ensure changes are saved. + * Else when we close notebooks as part of teardown in tests, things will not work as nbs are dirty. + * Solution - swallow saves this way when VSC fires save, we resolve and VSC thinks nb got saved and marked as not dirty. + */ +export async function swallowSavingOfNotebooks() { + const api = await initialize(); + // We will be editing notebooks, to close notebooks them we need to ensure changes are saved. + const contentProvider = api.serviceContainer.get<INotebookContentProvider>(INotebookContentProvider); + sinon.stub(contentProvider, 'saveNotebook').callsFake(noop as any); + sinon.stub(contentProvider, 'saveNotebookAs').callsFake(noop as any); +} + +export async function shutdownAllNotebooks() { + const api = await initialize(); + const notebookProvider = api.serviceContainer.get<INotebookProvider>(INotebookProvider); + await Promise.all(notebookProvider.activeNotebooks.map(async (item) => (await item).dispose())); +} + +let oldValueFor_alwaysTrustNotebooks: undefined | boolean; +export async function closeNotebooksAndCleanUpAfterTests(disposables: IDisposable[] = []) { + await closeActiveWindows(); + disposeAllDisposables(disposables); + await shutdownAllNotebooks(); + if (typeof oldValueFor_alwaysTrustNotebooks === 'boolean') { + const api = await initialize(); + const dsSettings = api.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings() + .datascience; + dsSettings.alwaysTrustNotebooks = oldValueFor_alwaysTrustNotebooks; + oldValueFor_alwaysTrustNotebooks = undefined; + } + + sinon.restore(); +} +export async function closeNotebooks(disposables: IDisposable[] = []) { + await closeActiveWindows(); + disposeAllDisposables(disposables); +} + +export async function trustAllNotebooks() { + const api = await initialize(); + const dsSettings = api.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings().datascience; + if (oldValueFor_alwaysTrustNotebooks !== undefined) { + oldValueFor_alwaysTrustNotebooks = dsSettings.alwaysTrustNotebooks; + } + dsSettings.alwaysTrustNotebooks = true; +} +export async function startJupyter(closeInitialEditor: boolean) { + const { editorProvider, vscodeNotebook } = await getServices(); + await closeActiveWindows(); + + const disposables: IDisposable[] = []; + try { + await editorProvider.createNew(); + await insertPythonCell('print("Hello World")', 0); + const cell = vscodeNotebook.activeNotebookEditor!.document.cells[0]!; + await executeActiveDocument(); + // Wait for Jupyter to start. + await waitForCondition(async () => cell.outputs.length > 0, 60_000, 'Cell not executed'); + + if (closeInitialEditor) { + await closeActiveWindows(); + } else { + await deleteCell(0); + } + } finally { + disposables.forEach((d) => d.dispose()); + } +} + +export function assertHasExecutionCompletedSuccessfully(cell: NotebookCell) { + return ( + (cell.metadata.executionOrder ?? 0) > 0 && + cell.metadata.runState === vscodeNotebookEnums.NotebookCellRunState.Success + ); +} +export async function waitForExecutionCompletedSuccessfully(cell: NotebookCell) { + await waitForCondition( + async () => assertHasExecutionCompletedSuccessfully(cell), + 1_000, + `Cell ${cell.notebook.cells.indexOf(cell) + 1} did not complete successfully` + ); +} +export function assertExecutionOrderInVSCCell(cell: NotebookCell, executionOrder?: number) { + assert.equal(cell.metadata.executionOrder, executionOrder); + return true; +} +export async function waitForExecutionOrderInVSCCell(cell: NotebookCell, executionOrder: number | undefined) { + await waitForCondition( + async () => assertExecutionOrderInVSCCell(cell, executionOrder), + 1_000, + `Execution count not '${executionOrder}' for Cell ${cell.notebook.cells.indexOf(cell) + 1}` + ); +} +export async function waitForExecutionOrderInCell(cell: NotebookCell, executionOrder: number | undefined) { + await waitForCondition( + async () => { + if (executionOrder === undefined || executionOrder === null) { + return cell.metadata.executionOrder === undefined; + } + return cell.metadata.executionOrder === executionOrder; + }, + 15_000, + `Execution count not '${executionOrder}' for Cell ${cell.notebook.cells.indexOf(cell)}` + ); +} +export function assertHasExecutionCompletedWithErrors(cell: NotebookCell) { + return ( + (cell.metadata.executionOrder ?? 0) > 0 && + cell.metadata.runState === vscodeNotebookEnums.NotebookCellRunState.Error + ); +} +export function assertHasOutputInVSCell(cell: NotebookCell) { + assert.ok(cell.outputs.length, `No output in Cell ${cell.notebook.cells.indexOf(cell) + 1}`); +} +export function assertHasOutputInICell(cell: ICell, model: INotebookModel) { + assert.ok((cell.data.outputs as nbformat.IOutput[]).length, `No output in ICell ${model.cells.indexOf(cell) + 1}`); +} +export function assertHasTextOutputInVSCode(cell: NotebookCell, text: string, index: number, isExactMatch = true) { + const cellOutputs = cell.outputs; + assert.ok(cellOutputs, 'No output'); + assert.equal(cellOutputs[index].outputKind, vscodeNotebookEnums.CellOutputKind.Rich, 'Incorrect output kind'); + const outputText = (cellOutputs[index] as CellDisplayOutput).data['text/plain'].trim(); + if (isExactMatch) { + assert.equal(outputText, text, 'Incorrect output'); + } else { + expect(outputText).to.include(text, 'Output does not contain provided text'); + } + return true; +} +export async function waitForTextOutputInVSCode( + cell: NotebookCell, + text: string, + index: number, + isExactMatch = true, + timeout = 1_000 +) { + await waitForCondition( + async () => assertHasTextOutputInVSCode(cell, text, index, isExactMatch), + timeout, + `Output does not contain provided text '${text}' for Cell ${cell.notebook.cells.indexOf(cell) + 1}` + ); +} +export function assertNotHasTextOutputInVSCode(cell: NotebookCell, text: string, index: number, isExactMatch = true) { + const cellOutputs = cell.outputs; + assert.ok(cellOutputs, 'No output'); + assert.equal(cellOutputs[index].outputKind, vscodeNotebookEnums.CellOutputKind.Rich, 'Incorrect output kind'); + const outputText = (cellOutputs[index] as CellDisplayOutput).data['text/plain'].trim(); + if (isExactMatch) { + assert.notEqual(outputText, text, 'Incorrect output'); + } else { + expect(outputText).to.not.include(text, 'Output does not contain provided text'); + } + return true; +} +export function assertHasTextOutputInICell(cell: ICell, text: string, index: number) { + const cellOutputs = cell.data.outputs as nbformat.IOutput[]; + assert.ok(cellOutputs, 'No output'); + assert.equal((cellOutputs[index].text as string).trim(), text, 'Incorrect output'); +} +export function assertVSCCellIsRunning(cell: NotebookCell) { + assert.equal(cell.metadata.runState, vscodeNotebookEnums.NotebookCellRunState.Running); + return true; +} +export async function waitForVSCCellHasEmptyOutput(cell: NotebookCell) { + await waitForCondition( + async () => cell.outputs.length === 0, + 1_000, + `Cell ${cell.notebook.cells.indexOf(cell) + 1} output did not get cleared` + ); +} +export async function waitForCellHasEmptyOutput(cell: ICell, model: INotebookModel) { + await waitForCondition( + async () => !Array.isArray(cell.data.outputs) || cell.data.outputs.length === 0, + 1_000, + `ICell ${model.cells.indexOf(cell) + 1} output did not get cleared` + ); +} +export async function waitForVSCCellIsRunning(cell: NotebookCell) { + await waitForCondition( + async () => assertVSCCellIsRunning(cell), + 1_000, + `Cell ${cell.notebook.cells.indexOf(cell) + 1} did not start` + ); +} +export function assertVSCCellIsNotRunning(cell: NotebookCell) { + assert.notEqual(cell.metadata.runState, vscodeNotebookEnums.NotebookCellRunState.Running); + return true; +} +export function assertVSCCellIsIdle(cell: NotebookCell) { + assert.equal(cell.metadata.runState, vscodeNotebookEnums.NotebookCellRunState.Idle); + return true; +} +export function assertVSCCellStateIsUndefined(cell: NotebookCell) { + assert.isUndefined(cell.metadata.runState); + return true; +} +export function assertVSCCellHasErrors(cell: NotebookCell) { + assert.equal(cell.metadata.runState, vscodeNotebookEnums.NotebookCellRunState.Error); + return true; +} +export function assertVSCCellHasErrorOutput(cell: NotebookCell) { + assert.ok( + cell.outputs.filter((output) => output.outputKind === vscodeNotebookEnums.CellOutputKind.Error).length, + 'No error output in cell' + ); + return true; +} + +export async function saveActiveNotebook(disposables: IDisposable[]) { + const api = await initialize(); + const editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + const savedEvent = createEventHandler(editorProvider.activeEditor!.model!, 'changed', disposables); + await commands.executeCommand('workbench.action.files.saveAll'); + + await waitForCondition(async () => savedEvent.all.some((e) => e.kind === 'save'), 5_000, 'Not saved'); +} + +export function createNotebookModel( + trusted: boolean, + uri: Uri, + globalMemento: Memento, + crypto: ICryptoUtils, + nb?: Partial<nbformat.INotebookContent> +) { + const nbJson: nbformat.INotebookContent = { + cells: [], + metadata: { + orig_nbformat: 4 + }, + nbformat: 4, + nbformat_minor: 4, + ...(nb || {}) + }; + + const cells = nbJson.cells.map((c, index) => { + return { + id: `NotebookImport#${index}`, + file: Identifiers.EmptyFileName, + line: 0, + state: CellState.finished, + data: c + }; + }); + return new VSCodeNotebookModel(trusted, uri, JSON.parse(JSON.stringify(cells)), globalMemento, crypto, nbJson); +} +export async function executeCell(cell: NotebookCell) { + const api = await initialize(); + const vscodeNotebook = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook); + await waitForCondition( + async () => !!vscodeNotebook.activeNotebookEditor?.kernel, + 60_000, // Validating kernel can take a while. + 'Timeout waiting for active kernel' + ); + if (!vscodeNotebook.activeNotebookEditor || !vscodeNotebook.activeNotebookEditor.kernel) { + throw new Error('No notebook or kernel'); + } + // Execute cells (it should throw an error). + vscodeNotebook.activeNotebookEditor.kernel.executeCell(cell.notebook, cell); +} +export async function executeActiveDocument() { + const api = await initialize(); + const vscodeNotebook = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook); + await waitForCondition( + async () => !!vscodeNotebook.activeNotebookEditor?.kernel, + 60_000, // Validating kernel can take a while. + 'Timeout waiting for active kernel' + ); + if (!vscodeNotebook.activeNotebookEditor || !vscodeNotebook.activeNotebookEditor.kernel) { + throw new Error('No notebook or kernel'); + } + // Execute cells (it should throw an error). + vscodeNotebook.activeNotebookEditor.kernel.executeAllCells(vscodeNotebook.activeNotebookEditor.document); +} +export function createNotebookDocument( + model: VSCodeNotebookModel, + viewType: string = JupyterNotebookView +): NotebookDocument { + const cells: NotebookCell[] = []; + const doc: NotebookDocument = { + cells, + version: 1, + fileName: model.file.fsPath, + isDirty: false, + languages: [], + uri: model.file, + isUntitled: false, + viewType, + metadata: { + cellEditable: model.isTrusted, + cellHasExecutionOrder: true, + cellRunnable: model.isTrusted, + editable: model.isTrusted, + runnable: model.isTrusted + } + }; + model.cells.forEach((cell, index) => { + const vscCell = createVSCNotebookCellDataFromCell(model, cell)!; + const vscDocumentCell: NotebookCell = { + cellKind: vscCell.cellKind, + language: vscCell.language, + metadata: vscCell.metadata || {}, + uri: model.file.with({ fragment: `cell${index}` }), + notebook: doc, + document: instance(mock<TextDocument>()), + outputs: vscCell.outputs + }; + cells.push(vscDocumentCell); + }); + model.associateNotebookDocument(doc); + return doc; +} diff --git a/src/test/datascience/notebook/helpers.unit.test.ts b/src/test/datascience/notebook/helpers.unit.test.ts new file mode 100644 index 000000000000..18597c3945c7 --- /dev/null +++ b/src/test/datascience/notebook/helpers.unit.test.ts @@ -0,0 +1,447 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { nbformat } from '@jupyterlab/coreutils'; +import { assert } from 'chai'; +import { cloneDeep } from 'lodash'; +import type { CellOutput } from 'vscode-proposed'; +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); +import { MARKDOWN_LANGUAGE, PYTHON_LANGUAGE } from '../../../client/common/constants'; +import { notebookModelToVSCNotebookData } from '../../../client/datascience/notebook/helpers/helpers'; +import { CellState, INotebookModel } from '../../../client/datascience/types'; + +suite('DataScience - NativeNotebook helpers', () => { + test('Convert NotebookModel to VSCode NotebookData', async () => { + const model: Partial<INotebookModel> = { + cells: [ + { + data: { + cell_type: 'code', + execution_count: 10, + outputs: [], + source: 'print(1)', + metadata: {} + }, + file: 'a.ipynb', + id: 'MyCellId1', + line: 0, + state: CellState.init + }, + { + data: { + cell_type: 'markdown', + source: '# HEAD', + metadata: {} + }, + file: 'a.ipynb', + id: 'MyCellId2', + line: 0, + state: CellState.init + } + ], + isTrusted: true + }; + + // tslint:disable-next-line: no-any + const notebook = notebookModelToVSCNotebookData(model as any); + + assert.isOk(notebook); + assert.deepEqual(notebook.languages, ['*']); + // ignore metadata we add. + const cellsWithoutCustomMetadata = notebook.cells.map((cell) => { + const cellToCompareWith = cloneDeep(cell); + delete cellToCompareWith.metadata?.custom; + return cellToCompareWith; + }); + assert.deepEqual(cellsWithoutCustomMetadata, [ + { + cellKind: vscodeNotebookEnums.CellKind.Code, + language: PYTHON_LANGUAGE, + outputs: [], + source: 'print(1)', + metadata: { + editable: true, + executionOrder: 10, + hasExecutionOrder: true, + runState: vscodeNotebookEnums.NotebookCellRunState.Success, + runnable: true + } + }, + { + cellKind: vscodeNotebookEnums.CellKind.Markdown, + language: MARKDOWN_LANGUAGE, + outputs: [], + source: '# HEAD', + metadata: { + editable: true, + executionOrder: undefined, + hasExecutionOrder: false, + runnable: false + } + } + ]); + }); + suite('Outputs', () => { + function validateCellOutputTranslation(outputs: nbformat.IOutput[], expectedOutputs: CellOutput[]) { + const model: Partial<INotebookModel> = { + cells: [ + { + data: { + cell_type: 'code', + execution_count: 10, + outputs, + source: 'print(1)', + metadata: {} + }, + file: 'a.ipynb', + id: 'MyCellId1', + line: 0, + state: CellState.init + } + ], + isTrusted: true + }; + // tslint:disable-next-line: no-any + const notebook = notebookModelToVSCNotebookData(model as any); + + assert.deepEqual(notebook.cells[0].outputs, expectedOutputs); + } + test('Empty output', () => { + validateCellOutputTranslation([], []); + }); + test('Stream output', () => { + validateCellOutputTranslation( + [ + { + output_type: 'stream', + name: 'stderr', + text: 'Error' + }, + { + output_type: 'stream', + name: 'stdout', + text: 'NoError' + } + ], + [ + { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { 'text/plain': 'Error' }, + metadata: { + custom: { + vscode: { + name: 'stderr', + outputType: 'stream' + } + } + } + }, + { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { 'text/plain': 'NoError' }, + metadata: { + custom: { + vscode: { + name: 'stdout', + outputType: 'stream' + } + } + } + } + ] + ); + }); + test('Streamed text with Ansi characters', async () => { + validateCellOutputTranslation( + [ + { + name: 'stderr', + text: '\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + output_type: 'stream' + } + ], + [ + { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { + 'text/plain': '\u001b[K\u001b[33m✅ \u001b[0m Loading\n' + }, + metadata: { + custom: { + vscode: { + name: 'stderr', + outputType: 'stream' + } + } + } + } + ] + ); + }); + test('Streamed text with angle bracket characters', async () => { + validateCellOutputTranslation( + [ + { + name: 'stderr', + text: '1 is < 2', + output_type: 'stream' + } + ], + [ + { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { + 'text/plain': '1 is < 2' + }, + metadata: { + custom: { + vscode: { + name: 'stderr', + outputType: 'stream' + } + } + } + } + ] + ); + }); + test('Streamed text with angle bracket characters and ansi chars', async () => { + validateCellOutputTranslation( + [ + { + name: 'stderr', + text: '1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + output_type: 'stream' + } + ], + [ + { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { + 'text/plain': '1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n' + }, + metadata: { + custom: { + vscode: { + name: 'stderr', + outputType: 'stream' + } + } + } + } + ] + ); + }); + test('Error', async () => { + validateCellOutputTranslation( + [ + { + ename: 'Error Name', + evalue: 'Error Value', + traceback: ['stack1', 'stack2', 'stack3'], + output_type: 'error' + } + ], + [ + { + outputKind: vscodeNotebookEnums.CellOutputKind.Error, + ename: 'Error Name', + evalue: 'Error Value', + traceback: ['stack1', 'stack2', 'stack3'] + } + ] + ); + }); + + ['display_data', 'execute_result'].forEach((output_type) => { + suite(`Rich output for output_type = ${output_type}`, () => { + test('Text mimeType output', async () => { + validateCellOutputTranslation( + [ + { + data: { + 'text/plain': 'Hello World!' + }, + output_type + } + ], + [ + { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { + 'text/plain': 'Hello World!' + }, + metadata: { + custom: { + vscode: { + outputType: output_type + } + } + } + } + ] + ); + }); + + test('png,jpeg images', async () => { + validateCellOutputTranslation( + [ + { + data: { + 'image/png': 'base64PNG', + 'image/jpeg': 'base64JPEG' + }, + output_type + } + ], + [ + { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { + 'image/png': 'base64PNG', + 'image/jpeg': 'base64JPEG' + }, + metadata: { + custom: { + vscode: { + outputType: output_type + } + } + } + } + ] + ); + }); + test('png image with a light background', async () => { + validateCellOutputTranslation( + [ + { + data: { + 'image/png': 'base64PNG' + }, + metadata: { + needs_background: 'light' + }, + output_type + } + ], + [ + { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { + 'image/png': 'base64PNG' + }, + metadata: { + custom: { + needs_background: 'light', + vscode: { + outputType: output_type + } + } + } + } + ] + ); + }); + test('png image with a dark background', async () => { + validateCellOutputTranslation( + [ + { + data: { + 'image/png': 'base64PNG' + }, + metadata: { + needs_background: 'dark' + }, + output_type + } + ], + [ + { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { + 'image/png': 'base64PNG' + }, + metadata: { + custom: { + needs_background: 'dark', + vscode: { + outputType: output_type + } + } + } + } + ] + ); + }); + test('png image with custom dimensions', async () => { + validateCellOutputTranslation( + [ + { + data: { + 'image/png': 'base64PNG' + }, + metadata: { + 'image/png': { height: '111px', width: '999px' } + }, + output_type + } + ], + [ + { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { + 'image/png': 'base64PNG' + }, + metadata: { + custom: { + 'image/png': { height: '111px', width: '999px' }, + vscode: { + outputType: output_type + } + } + } + } + ] + ); + }); + test('png allowed to scroll', async () => { + validateCellOutputTranslation( + [ + { + data: { + 'image/png': 'base64PNG' + }, + metadata: { + unconfined: true, + 'image/png': { width: '999px' } + }, + output_type + } + ], + [ + { + outputKind: vscodeNotebookEnums.CellOutputKind.Rich, + data: { + 'image/png': 'base64PNG' + }, + metadata: { + custom: { + unconfined: true, + 'image/png': { width: '999px' }, + vscode: { + outputType: output_type + } + } + } + } + ] + ); + }); + }); + }); + }); +}); diff --git a/src/test/datascience/notebook/interrupRestart.ds.test.ts b/src/test/datascience/notebook/interrupRestart.ds.test.ts new file mode 100644 index 000000000000..ca5e2b78ae34 --- /dev/null +++ b/src/test/datascience/notebook/interrupRestart.ds.test.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { commands, NotebookEditor as VSCNotebookEditor } from 'vscode'; +import { IApplicationShell, IVSCodeNotebook } from '../../../client/common/application/types'; +import { IDisposable } from '../../../client/common/types'; +import { createDeferredFromPromise } from '../../../client/common/utils/async'; +import { noop } from '../../../client/common/utils/misc'; +import { IKernelProvider } from '../../../client/datascience/jupyter/kernels/types'; +import { INotebookEditorProvider } from '../../../client/datascience/types'; +import { IExtensionTestApi, waitForCondition } from '../../common'; +import { initialize } from '../../initialize'; +import { + assertVSCCellHasErrors, + assertVSCCellIsNotRunning, + assertVSCCellIsRunning, + canRunTests, + closeNotebooks, + closeNotebooksAndCleanUpAfterTests, + deleteAllCellsAndWait, + executeActiveDocument, + insertPythonCellAndWait, + startJupyter, + trustAllNotebooks, + waitForTextOutputInVSCode +} from './helper'; +// tslint:disable-next-line: no-var-requires no-require-imports + +// tslint:disable: no-any no-invalid-this +/* + * This test focuses on interrupting, restarting kernels. + * We will not use actual kernels, just ensure the appropriate methods are invoked on the appropriate classes. + * This is done by stubbing out some methods. + */ +suite('DataScience - VSCode Notebook - Restart/Interrupt/Cancel/Errors (slow)', function () { + this.timeout(60_000); + + let api: IExtensionTestApi; + let editorProvider: INotebookEditorProvider; + const disposables: IDisposable[] = []; + let kernelProvider: IKernelProvider; + let vscEditor: VSCNotebookEditor; + let vscodeNotebook: IVSCodeNotebook; + const suiteDisposables: IDisposable[] = []; + suiteSetup(async function () { + this.timeout(60_000); + api = await initialize(); + if (!(await canRunTests())) { + return this.skip(); + } + await closeNotebooksAndCleanUpAfterTests(); + await startJupyter(true); + vscodeNotebook = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook); + editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + kernelProvider = api.serviceContainer.get<IKernelProvider>(IKernelProvider); + }); + setup(async () => { + sinon.restore(); + await trustAllNotebooks(); + // Open a notebook and use this for all tests in this test suite. + await editorProvider.createNew(); + assert.isOk(vscodeNotebook.activeNotebookEditor, 'No active notebook'); + vscEditor = vscodeNotebook.activeNotebookEditor!; + await deleteAllCellsAndWait(); + }); + teardown(() => closeNotebooks(disposables)); + suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables.concat(suiteDisposables))); + + test('Cancelling token will cancel cell execution', async () => { + await insertPythonCellAndWait('import time\nfor i in range(10000):\n print(i)\n time.sleep(0.1)', 0); + const cell = vscEditor.document.cells[0]; + const appShell = api.serviceContainer.get<IApplicationShell>(IApplicationShell); + const showInformationMessage = sinon.stub(appShell, 'showInformationMessage'); + showInformationMessage.resolves(); // Ignore message to restart kernel. + disposables.push({ dispose: () => showInformationMessage.restore() }); + await waitForCondition(async () => kernelProvider.get(cell.notebook.uri) !== undefined, 5_000, 'No kernel'); + const promise = kernelProvider.get(cell.notebook.uri)!.executeCell(cell); + const deferred = createDeferredFromPromise(promise); + + // Wait for cell to get busy. + await waitForCondition(async () => assertVSCCellIsRunning(cell), 15_000, 'Cell not being executed'); + + // Wait for ?s, and verify cell is still running. + assert.isFalse(deferred.completed); + assertVSCCellIsRunning(cell); + // Wait for some output. + await waitForTextOutputInVSCode(cell, '1', 0, false, 15_000); // Wait for 15 seconds for it to start (possibly kernel is still starting). + + // Interrupt the kernel. + kernelProvider.get(cell.notebook.uri)!.interrupt().catch(noop); + + // Wait for interruption or message prompting to restart kernel to be displayed. + // Interrupt can fail sometimes and then we display message prompting user to restart kernel. + await waitForCondition( + async () => deferred.completed || showInformationMessage.called, + 30_000, // Wait for completion or interrupt timeout. + 'Execution not cancelled' + ); + if (deferred.completed) { + assertVSCCellHasErrors(cell); + } + }); + test('Restarting kernel will cancel cell execution & we can re-run a cell', async () => { + await insertPythonCellAndWait('import time\nfor i in range(10000):\n print(i)\n time.sleep(0.1)', 0); + const cell = vscEditor.document.cells[0]; + + await executeActiveDocument(); + + // Wait for cell to get busy. + await waitForCondition(async () => assertVSCCellIsRunning(cell), 15_000, 'Cell not being executed'); + + // Wait for ?s, and verify cell is still running. + assertVSCCellIsRunning(cell); + // Wait for some output. + await waitForTextOutputInVSCode(cell, '1', 0, false, 15_000); // Wait for 15 seconds for it to start (possibly kernel is still starting). + + // Restart the kernel. + const restartPromise = commands.executeCommand('python.datascience.notebookeditor.restartkernel'); + + await waitForCondition(async () => assertVSCCellIsNotRunning(cell), 15_000, 'Execution not cancelled'); + + // Wait before we execute cells again. + await restartPromise; + + // Confirm we can execute a cell (using the new kernel session). + await executeActiveDocument(); + + // Wait for cell to get busy. + await waitForCondition(async () => assertVSCCellIsRunning(cell), 15_000, 'Cell not being executed'); + }); +}); diff --git a/src/test/datascience/notebook/notebookEditorProvider.ds.test.ts b/src/test/datascience/notebook/notebookEditorProvider.ds.test.ts new file mode 100644 index 000000000000..5ce23ffe65ec --- /dev/null +++ b/src/test/datascience/notebook/notebookEditorProvider.ds.test.ts @@ -0,0 +1,431 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { commands, Uri } from 'vscode'; +import { ICommandManager, IVSCodeNotebook } from '../../../client/common/application/types'; +import { IDisposable } from '../../../client/common/types'; +import { JupyterNotebookView } from '../../../client/datascience/notebook/constants'; +import { NotebookEditor } from '../../../client/datascience/notebook/notebookEditor'; +import { INotebookEditorProvider } from '../../../client/datascience/types'; +import { createEventHandler, IExtensionTestApi, waitForCondition } from '../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { closeActiveWindows, initialize } from '../../initialize'; +import { + canRunTests, + closeNotebooksAndCleanUpAfterTests, + createTemporaryNotebook, + insertMarkdownCellAndWait, + trustAllNotebooks +} from './helper'; + +suite('DataScience - VSCode Notebook (Editor Provider)', function () { + // tslint:disable: no-invalid-this no-any + this.timeout(5_000); + + let api: IExtensionTestApi; + let vscodeNotebook: IVSCodeNotebook; + let editorProvider: INotebookEditorProvider; + let commandManager: ICommandManager; + const templateIPynb = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'datascience', + 'notebook', + 'test.ipynb' + ); + const emptyPyFile = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'datascience', 'notebook', 'empty.py'); + let testIPynb: Uri; + const disposables: IDisposable[] = []; + suiteSetup(async function () { + api = await initialize(); + if (!(await canRunTests())) { + return this.skip(); + } + vscodeNotebook = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook); + editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + commandManager = api.serviceContainer.get<ICommandManager>(ICommandManager); + }); + setup(async () => { + sinon.restore(); + await trustAllNotebooks(); + // Don't use same file (due to dirty handling, we might save in dirty.) + // Cuz we won't save to file, hence extension will backup in dirty file and when u re-open it will open from dirty. + testIPynb = Uri.file(await createTemporaryNotebook(templateIPynb, disposables)); + }); + teardown(() => closeNotebooksAndCleanUpAfterTests(disposables)); + suiteTeardown(() => closeNotebooksAndCleanUpAfterTests(disposables)); + + test('No notebooks when opening VSC', async () => { + assert.isUndefined(vscodeNotebook.activeNotebookEditor); + assert.isUndefined(editorProvider.activeEditor); + assert.equal(editorProvider.editors.length, 0, 'Should not have any notebooks open'); + assert.equal(vscodeNotebook.notebookEditors.length, 0, 'Should not have any vsc notebooks'); + }); + test('Create empty notebook', async () => { + const editor = await editorProvider.createNew(); + + assert.isOk(editor); + assert.instanceOf(editor, NotebookEditor); + assert.isOk(vscodeNotebook.activeNotebookEditor); + }); + test('Create empty notebook using command', async () => { + await commandManager.executeCommand('python.datascience.createnewnotebook'); + + assert.isOk(vscodeNotebook.activeNotebookEditor); + }); + test('Create empty notebook using command & our editor is created', async () => { + await commandManager.executeCommand('python.datascience.createnewnotebook'); + + await waitForCondition(async () => !!editorProvider.activeEditor, 2_000, 'Editor not created'); + }); + test('Create empty notebook and we have active editor', async () => { + assert.isUndefined(editorProvider.activeEditor); + assert.equal(editorProvider.editors.length, 0); + + const editor = await editorProvider.createNew(); + + assert.equal(editorProvider.editors.length, 1); + assert.isOk(editor); + assert.isOk(vscodeNotebook.activeNotebookEditor); + assert.isOk(editorProvider.activeEditor); + assert.equal( + vscodeNotebook.activeNotebookEditor?.document.uri.fsPath.toLowerCase(), + editor.file.fsPath.toLowerCase() + ); + assert.equal(editorProvider.activeEditor?.file.fsPath.toLowerCase(), editor.file.fsPath.toLowerCase()); + }); + + test('Create empty notebook will fire necessary events', async () => { + const notebookOpened = createEventHandler(editorProvider, 'onDidOpenNotebookEditor', disposables); + const activeNotebookChanged = createEventHandler( + editorProvider, + 'onDidChangeActiveNotebookEditor', + disposables + ); + + await editorProvider.createNew(); + + await notebookOpened.assertFired(); + await activeNotebookChanged.assertFired(); + }); + test('Opening a non-notebooks will fire necessary events', async () => { + const notebookOpened = createEventHandler(editorProvider, 'onDidOpenNotebookEditor', disposables); + const activeNotebookChanged = createEventHandler( + editorProvider, + 'onDidChangeActiveNotebookEditor', + disposables + ); + const notebookClosed = createEventHandler(editorProvider, 'onDidCloseNotebookEditor', disposables); + + await editorProvider.createNew(); + + await notebookOpened.assertFired(); + await activeNotebookChanged.assertFired(); + assert.isOk(activeNotebookChanged.first, 'Active Editor is undefined'); + assert.isFalse(notebookClosed.fired, 'No notebook should be closed'); + + // Open a python file. + await commandManager.executeCommand('vscode.open', Uri.file(emptyPyFile)); + + await activeNotebookChanged.assertFired(); + assert.isTrue(notebookClosed.fired, 'Unpinned notebook should have been closed when opening another file'); + assert.isUndefined(activeNotebookChanged.second, 'Active Editor should be undefined'); + }); + test('Opening a non-notebook file and toggling between nb & non-notebook will fire necessary events', async () => { + const notebookOpened = createEventHandler(editorProvider, 'onDidOpenNotebookEditor', disposables); + const activeNotebookChanged = createEventHandler( + editorProvider, + 'onDidChangeActiveNotebookEditor', + disposables + ); + const notebookClosed = createEventHandler(editorProvider, 'onDidCloseNotebookEditor', disposables); + + const notebookEditor = await editorProvider.open(testIPynb); + await insertMarkdownCellAndWait('1'); // Make the file dirty (so it gets pinned). + await notebookOpened.assertFired(); + await activeNotebookChanged.assertFired(); + assert.equal(activeNotebookChanged.first, notebookEditor); + + // Open a python file. + await commandManager.executeCommand('vscode.open', Uri.file(emptyPyFile)); + + await activeNotebookChanged.assertFired(); + assert.isFalse(notebookClosed.fired, 'Notebook is dirty, hence pinned and not closed'); + assert.isUndefined(activeNotebookChanged.second, 'Active Editor should be undefined'); + + // Going to first notebook file should change the editor back to that. + await commands.executeCommand('workbench.action.nextEditor'); + await notebookOpened.assertFiredExactly(1); // Only 2, no new notebooks opened. + await activeNotebookChanged.assertFiredAtLeast(3); + assert.equal(activeNotebookChanged.last, notebookEditor); + + // Going to second python file should change the editor back to that. + await commands.executeCommand('workbench.action.nextEditor'); + await notebookOpened.assertFiredExactly(1); // Only 2, no new notebooks opened. + await activeNotebookChanged.assertFiredAtLeast(4); + assert.isUndefined(activeNotebookChanged.second, 'Active Editor should be undefined'); + + // Close the python file. + await commandManager.executeCommand('workbench.action.closeActiveEditor'); + assert.isFalse(notebookClosed.fired, 'Notebook is dirty, hence pinned and not closed'); + await activeNotebookChanged.assertFiredAtLeast(5); + assert.equal(activeNotebookChanged.last, notebookEditor); + + // Close the notebook. + await commands.executeCommand('workbench.action.files.saveAll'); // Save untitled changes (to prevent prompts). + await commandManager.executeCommand('workbench.action.closeActiveEditor'); + await notebookClosed.assertFiredExactly(1); + await activeNotebookChanged.assertFiredAtLeast(6); // Fired when there are no more documents open. + assert.isUndefined(activeNotebookChanged.last); + }); + test('Opening two notebooks and toggling between the two will fire necessary event', async () => { + const notebookOpened = createEventHandler(editorProvider, 'onDidOpenNotebookEditor', disposables); + const activeNotebookChanged = createEventHandler( + editorProvider, + 'onDidChangeActiveNotebookEditor', + disposables + ); + const notebookClosed = createEventHandler(editorProvider, 'onDidCloseNotebookEditor', disposables); + + const editor1 = await editorProvider.open(testIPynb); + await insertMarkdownCellAndWait('1'); // Make the file dirty (so it gets pinned). + await notebookOpened.assertFired(); + await activeNotebookChanged.assertFired(); + assert.equal(activeNotebookChanged.first, editor1); + assert.isFalse(notebookClosed.fired, 'No notebook should be closed'); + + // Open another notebook. + const testIPynb2 = Uri.file(await createTemporaryNotebook(templateIPynb, disposables)); + const editor2 = await editorProvider.open(testIPynb2); + await insertMarkdownCellAndWait('1'); // Make the file dirty (so it gets pinned). + + await notebookOpened.assertFiredExactly(2); + await activeNotebookChanged.assertFiredAtLeast(2); + assert.equal(activeNotebookChanged.last, editor2); + assert.isFalse(notebookClosed.fired, 'No notebook should be closed'); + + // Re-opening, first file should change the editor back to that. + await commands.executeCommand('workbench.action.nextEditor'); + await notebookOpened.assertFiredExactly(2); // Only 2, no new notebooks opened. + await activeNotebookChanged.assertFiredAtLeast(3); + assert.equal(activeNotebookChanged.last, editor1); + + // Close the first notebook. + await commands.executeCommand('workbench.action.files.saveAll'); // Save untitled changes (to prevent prompts). + await commandManager.executeCommand('workbench.action.closeActiveEditor'); + await notebookClosed.assertFired(); + await activeNotebookChanged.assertFiredAtLeast(4); + assert.equal(activeNotebookChanged.last, editor2); + + // Close the second notebook. + await commandManager.executeCommand('workbench.action.closeActiveEditor'); + await notebookClosed.assertFiredExactly(2); + await activeNotebookChanged.assertFiredAtLeast(5); // Fired when there are no more documents open. + assert.isUndefined(activeNotebookChanged.last); + }); + test('Closing a notebook will fire necessary events and clear state', async () => { + const notebookClosed = createEventHandler(editorProvider, 'onDidCloseNotebookEditor', disposables); + + await editorProvider.createNew(); + await closeActiveWindows(); + + assert.isUndefined(editorProvider.activeEditor); + assert.equal(editorProvider.editors.length, 0); + await notebookClosed.assertFired(); + }); + test('Closing nb should close our notebook editors as related resources', async () => { + await commandManager.executeCommand('python.datascience.createnewnotebook'); + await waitForCondition(async () => !!editorProvider.activeEditor, 2_000, 'Editor not created'); + + const editorClosed = createEventHandler(editorProvider.activeEditor!, 'closed', disposables); + const modelDisposed = createEventHandler(editorProvider.activeEditor!.model!, 'onDidDispose', disposables); + + await closeActiveWindows(); + + await waitForCondition(async () => !editorProvider.activeEditor, 1_000, 'Editor not closed'); + await editorClosed.assertFired(); + await modelDisposed.assertFired(); + }); + test('Opening an nb multiple times will result in a single (our) INotebookEditor being created', async () => { + await commandManager.executeCommand('vscode.openWith', Uri.file(templateIPynb), JupyterNotebookView); + await waitForCondition(async () => !!editorProvider.activeEditor, 2_000, 'Editor not created'); + + // Open a duplicate editor. + await commands.executeCommand('workbench.action.splitEditor', Uri.file(templateIPynb)); + await waitForCondition(async () => vscodeNotebook.notebookEditors.length === 2, 2_000, 'Duplicate not opened'); + + // Verify two VSC editors & single (our) INotebookEditor. + assert.equal(vscodeNotebook.notebookEditors.length, 2, 'Should have two editors'); + assert.lengthOf(editorProvider.editors, 1); + }); + test('Closing one of the duplicate notebooks will not dispose (our) INotebookEditor until all VSC Editors are closed', async () => { + await commandManager.executeCommand('vscode.openWith', Uri.file(templateIPynb), JupyterNotebookView); + await waitForCondition(async () => !!editorProvider.activeEditor, 2_000, 'Editor not created'); + + const editorDisposed = createEventHandler(editorProvider.activeEditor!, 'closed', disposables); + const modelDisposed = createEventHandler(editorProvider.activeEditor!.model!, 'onDidDispose', disposables); + + // Open a duplicate editor. + await commands.executeCommand('workbench.action.splitEditor', Uri.file(templateIPynb)); + await waitForCondition(async () => vscodeNotebook.notebookEditors.length === 2, 2_000, 'Duplicate not opened'); + + // Verify two VSC editors & single (our) INotebookEditor. + assert.equal(vscodeNotebook.notebookEditors.length, 2, 'Should have two editors'); + assert.lengthOf(editorProvider.editors, 1, 'Should have an editor opened'); + + // If we close one of the VS Code notebook editors, then it should not close our editor. + // Cuz we still have a VSC editor associated with the same file. + await commands.executeCommand('workbench.action.closeActiveEditor'); + + // Verify we have only one VSC Notebook & still have our INotebookEditor. + assert.equal(vscodeNotebook.notebookEditors.length, 1, 'Should have one VSC editor'); + assert.lengthOf(editorProvider.editors, 1, 'Should have an editor opened'); + + // Verify our notebook was not disposed. + assert.equal(editorDisposed.count, 0, 'Editor disposed'); + assert.equal(modelDisposed.count, 0, 'Model disposed'); + + // Close the last VSC editor & confirm our editor also got disposed. + await commands.executeCommand('workbench.action.closeActiveEditor'); + await waitForCondition(async () => !editorProvider.activeEditor, 2_000, 'Editor not disposed'); + + // Verify all editors have been closed. + assert.equal(vscodeNotebook.notebookEditors.length, 0, 'Should not have any VSC editors'); + assert.lengthOf(editorProvider.editors, 0, 'Should not have an editor opened'); + + // Verify our notebook was not disposed. + await editorDisposed.assertFired(); + await modelDisposed.assertFired(); + }); + test('Closing nb & re-opening it should create a new model & not re-use old model', async () => { + await editorProvider.open(Uri.file(templateIPynb)); + const firstEditor = editorProvider.activeEditor!; + const firstModel = firstEditor.model!; + + await closeActiveWindows(); + + await editorProvider.open(Uri.file(templateIPynb)); + + assert.notEqual(firstEditor, editorProvider.activeEditor!, 'Editor references must be different'); + assert.notEqual(firstModel, editorProvider.activeEditor!.model, 'Model references must be different'); + }); + test('Open a notebook using our API', async () => { + const editor = await editorProvider.open(testIPynb); + + assert.isOk(editor); + }); + test('Open a notebook using our API and we will have an active editor', async () => { + assert.isUndefined(editorProvider.activeEditor); + assert.equal(editorProvider.editors.length, 0); + + const editor = await editorProvider.open(testIPynb); + + assert.equal(editorProvider.editors.length, 1); + assert.isOk(editor); + assert.isOk(vscodeNotebook.activeNotebookEditor); + assert.isOk(editorProvider.activeEditor); + assert.equal( + vscodeNotebook.activeNotebookEditor?.document.uri.fsPath.toLowerCase(), + editor.file.fsPath.toLowerCase() + ); + assert.equal(editorProvider.activeEditor?.file.fsPath.toLowerCase(), editor.file.fsPath.toLowerCase()); + }); + test('Open a notebook using our API will fire necessary events', async () => { + const notebookOpened = createEventHandler(editorProvider, 'onDidChangeActiveNotebookEditor', disposables); + const activeNotebookChanged = createEventHandler(editorProvider, 'onDidOpenNotebookEditor', disposables); + + await editorProvider.open(testIPynb); + + await notebookOpened.assertFired(); + await activeNotebookChanged.assertFired(); + }); + test('Open a notebook using VS Code API', async () => { + assert.isUndefined(editorProvider.activeEditor); + assert.equal(editorProvider.editors.length, 0); + + await commandManager.executeCommand('vscode.openWith', testIPynb, JupyterNotebookView); + + assert.equal(editorProvider.editors.length, 1); + assert.isOk(vscodeNotebook.activeNotebookEditor); + assert.isOk(editorProvider.activeEditor); + assert.equal(editorProvider.activeEditor?.file.fsPath.toLowerCase(), testIPynb.fsPath.toLowerCase()); + }); + test('Open a notebook using VSC API then ours yields the same editor', async () => { + assert.isUndefined(editorProvider.activeEditor); + assert.equal(editorProvider.editors.length, 0); + + await commandManager.executeCommand('vscode.openWith', testIPynb, JupyterNotebookView); + + assert.equal(editorProvider.editors.length, 1); + assert.isOk(vscodeNotebook.activeNotebookEditor); + assert.equal( + vscodeNotebook.activeNotebookEditor?.document.uri.fsPath.toLowerCase(), + testIPynb.fsPath.toLowerCase() + ); + assert.equal(editorProvider.activeEditor?.file.fsPath.toLowerCase(), testIPynb.fsPath.toLowerCase()); + + // Opening again with our will do nothing (it will return the existing editor). + const editor = await editorProvider.open(testIPynb); + + assert.equal(editorProvider.editors.length, 1); + assert.equal(editor.file.fsPath.toLowerCase(), testIPynb.fsPath.toLowerCase()); + }); + test('Active notebook points to the currently active editor', async () => { + const editor1 = await editorProvider.createNew(); + + assert.isOk(vscodeNotebook.activeNotebookEditor); + assert.equal( + vscodeNotebook.activeNotebookEditor?.document.uri.fsPath.toLowerCase(), + editor1.file.fsPath.toLowerCase() + ); + assert.equal( + vscodeNotebook.activeNotebookEditor?.document.uri.fsPath.toLowerCase(), + editorProvider.activeEditor?.file.fsPath.toLowerCase() + ); + + const editor2 = await editorProvider.createNew(); + assert.equal(editorProvider.activeEditor?.file.fsPath.toLowerCase(), editor2.file.fsPath.toLowerCase()); + }); + test('Create two blank notebooks', async () => { + const editor1 = await editorProvider.createNew(); + + assert.equal(editor1.file.scheme, 'untitled'); + assert.equal( + vscodeNotebook.activeNotebookEditor?.document.uri.fsPath.toLowerCase(), + editor1.file.fsPath.toLowerCase() + ); + + const editor2 = await editorProvider.createNew(); + + assert.equal(editor2.file.scheme, 'untitled'); + assert.equal( + vscodeNotebook.activeNotebookEditor?.document.uri.fsPath.toLowerCase(), + editor2.file.fsPath.toLowerCase() + ); + assert.notEqual( + vscodeNotebook.activeNotebookEditor?.document.uri.fsPath.toLowerCase(), + editor1.file.fsPath.toLowerCase() + ); + }); + test('Active Notebook Editor event gets fired when opening multiple notebooks', async () => { + const notebookOpened = createEventHandler(editorProvider, 'onDidChangeActiveNotebookEditor', disposables); + const activeNotebookChanged = createEventHandler(editorProvider, 'onDidOpenNotebookEditor', disposables); + + await editorProvider.createNew(); + + await notebookOpened.assertFiredExactly(1); + await activeNotebookChanged.assertFiredExactly(1); + + // Open another notebook. + await commandManager.executeCommand('vscode.openWith', testIPynb, JupyterNotebookView); + + await notebookOpened.assertFiredAtLeast(2); + await activeNotebookChanged.assertFiredExactly(2); + }); +}); diff --git a/src/test/datascience/notebook/notebookStorage.unit.test.ts b/src/test/datascience/notebook/notebookStorage.unit.test.ts new file mode 100644 index 000000000000..df14b31c60ce --- /dev/null +++ b/src/test/datascience/notebook/notebookStorage.unit.test.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable-next-line: no-var-requires no-require-imports +import { assert } from 'chai'; +import { anyString, instance, mock, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { CryptoUtils } from '../../../client/common/crypto'; +import { sleep } from '../../../client/common/utils/async'; +import { NotebookModelChange } from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { + ActiveKernelIdList, + BaseNotebookModel, + MaximumKernelIdListSize +} from '../../../client/datascience/notebookStorage/baseModel'; +import { NativeEditorNotebookModel } from '../../../client/datascience/notebookStorage/notebookModel'; +import { MockMemento } from '../../mocks/mementos'; + +suite('DataScience - Notebook Storage', () => { + let globalMemento: MockMemento; + let crypto: CryptoUtils; + setup(() => { + globalMemento = new MockMemento(); + crypto = mock(CryptoUtils); + when(crypto.createHash(anyString(), 'string')).thenCall((a1, _a2) => a1); + }); + + function createModel(index: number): BaseNotebookModel { + const fileName = `foo${index}.ipynb`; + return new NativeEditorNotebookModel(true, Uri.file(fileName), [], globalMemento, instance(crypto)); + } + + function updateModelKernel(model: BaseNotebookModel, id: string) { + const kernelModel = { + name: 'foo', + // tslint:disable-next-line: no-any + session: {} as any, + lastActivityTime: new Date(), + numberOfConnections: 1, + id + }; + const change: NotebookModelChange = { + kind: 'version', + kernelConnection: { + kernelModel, + interpreter: undefined, + kind: 'connectToLiveKernel' + }, + oldDirty: false, + newDirty: true, + source: 'user' + }; + model.update(change); + } + + test('Verify live kernel id is saved', async () => { + const model = createModel(0); + updateModelKernel(model, '1'); + const kernelIds = globalMemento.get(ActiveKernelIdList, []); + assert.equal(kernelIds.length, 1, 'Kernel id not written'); + }); + test('Verify live kernel id maxes out at 40', async () => { + for (let i = 0; i < MaximumKernelIdListSize + 10; i += 1) { + const model = createModel(i); + updateModelKernel(model, `${i}`); + } + await sleep(100); // Give it time to update. + const kernelIds = globalMemento.get(ActiveKernelIdList, []); + assert.equal(kernelIds.length, MaximumKernelIdListSize, 'Kernel length is too many'); + }); +}); diff --git a/src/test/datascience/notebook/notebookTrust.ds.test.ts b/src/test/datascience/notebook/notebookTrust.ds.test.ts new file mode 100644 index 000000000000..d0069ba83f2f --- /dev/null +++ b/src/test/datascience/notebook/notebookTrust.ds.test.ts @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-require-imports no-var-requires +import { nbformat } from '@jupyterlab/coreutils'; +import { assert } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as uuid from 'uuid/v4'; +import { Uri } from 'vscode'; +import { NotebookDocument } from '../../../../types/vscode-proposed'; +import { IVSCodeNotebook } from '../../../client/common/application/types'; +import { IConfigurationService, IDataScienceSettings, IDisposable } from '../../../client/common/types'; +import { INotebookEditorProvider } from '../../../client/datascience/types'; +import { splitMultilineString } from '../../../datascience-ui/common'; +import { IExtensionTestApi } from '../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; +import { canRunTests, closeNotebooksAndCleanUpAfterTests, createTemporaryNotebook } from './helper'; +// tslint:disable-next-line: no-var-requires no-require-imports +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); + +// tslint:disable: no-any no-invalid-this +suite('DataScience - VSCode Notebook - (Trust)', function () { + this.timeout(15_000); + const templateIPynb = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'datascience', + 'notebook', + 'withOutputForTrust.ipynb' + ); + let api: IExtensionTestApi; + let testIPynb: Uri; + const disposables: IDisposable[] = []; + suiteSetup(async function () { + this.timeout(15_000); + api = await initialize(); + if (!(await canRunTests())) { + return this.skip(); + } + }); + let oldTrustSetting: boolean; + let dsSettings: IDataScienceSettings; + suiteSetup(() => { + const configService = api.serviceContainer.get<IConfigurationService>(IConfigurationService); + dsSettings = configService.getSettings(testIPynb).datascience; + oldTrustSetting = dsSettings.alwaysTrustNotebooks; + dsSettings.alwaysTrustNotebooks = false; + }); + setup(async () => { + sinon.restore(); + dsSettings.alwaysTrustNotebooks = false; + // Don't use same file (due to dirty handling, we might save in dirty.) + // Cuz we won't save to file, hence extension will backup in dirty file and when u re-open it will open from dirty. + testIPynb = Uri.file(await createTemporaryNotebook(templateIPynb, disposables)); + // Modify ipynb to have random text in code cell, so that it is untrusted. + const nb: nbformat.INotebookContent = JSON.parse(await fs.readFile(testIPynb.fsPath, { encoding: 'utf8' })); + nb.cells[0].source = splitMultilineString(`PRINT "${uuid()}"`); + await fs.writeFile(testIPynb.fsPath, JSON.stringify(nb, undefined, 4)); + }); + teardown(async () => closeNotebooksAndCleanUpAfterTests(disposables)); + suiteTeardown(() => (dsSettings.alwaysTrustNotebooks = oldTrustSetting === true)); + + function assertDocumentTrust(document: NotebookDocument, trusted: boolean) { + assert.equal(document.metadata.cellEditable, trusted); + assert.equal(document.metadata.cellRunnable, trusted); + assert.equal(document.metadata.editable, trusted); + assert.equal(document.metadata.runnable, trusted); + + document.cells.forEach((cell) => { + assert.equal(cell.metadata.editable, trusted); + if (cell.cellKind === vscodeNotebookEnums.CellKind.Code) { + assert.equal(cell.metadata.runnable, trusted); + // In our test all code cells have outputs. + if (trusted) { + assert.ok(cell.outputs.length, 'No output in trusted cell'); + } else { + assert.lengthOf(cell.outputs, 0, 'Cannot have output in non-trusted notebook'); + } + } + }); + } + + test('Cannot run/edit un-trusted notebooks, once trusted can edit/run', async () => { + const editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + const model = (await editorProvider.open(testIPynb))!.model!; + + const document = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook).activeNotebookEditor!.document; + assert.equal(model.isTrusted, false); + assertDocumentTrust(document, false); + + model.trust(); + assert.equal(model.isTrusted, true); + assertDocumentTrust(document, true); + }); +}); diff --git a/src/test/datascience/notebook/rendererExension.unit.test.ts b/src/test/datascience/notebook/rendererExension.unit.test.ts new file mode 100644 index 000000000000..559da209cd19 --- /dev/null +++ b/src/test/datascience/notebook/rendererExension.unit.test.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter, Extension, ExtensionKind, Uri } from 'vscode'; +import { NotebookDocument } from '../../../../types/vscode-proposed'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { VSCodeNotebook } from '../../../client/common/application/notebook'; +import { IApplicationEnvironment, IVSCodeNotebook } from '../../../client/common/application/types'; +import { IDisposable, IExtensions } from '../../../client/common/types'; +import { JupyterNotebookView, RendererExtensionId } from '../../../client/datascience/notebook/constants'; +import { RendererExtension } from '../../../client/datascience/notebook/rendererExtension'; +import { RendererExtensionDownloader } from '../../../client/datascience/notebook/rendererExtensionDownloader'; + +suite('DataScience - NativeNotebook Renderer Extension', () => { + let rendererExtension: IExtensionSingleActivationService; + let downloader: RendererExtensionDownloader; + let vscNotebook: IVSCodeNotebook; + let extensions: IExtensions; + let appEnv: IApplicationEnvironment; + let onDidOpenNotebookDocument: EventEmitter<NotebookDocument>; + const disposables: IDisposable[] = []; + const jupyterNotebook: NotebookDocument = { + cells: [], + version: 1, + uri: Uri.file('one.ipynb'), + fileName: '', + isDirty: false, + languages: [], + metadata: {}, + isUntitled: false, + viewType: JupyterNotebookView + }; + const nonJupyterNotebook: NotebookDocument = { + cells: [], + version: 1, + uri: Uri.file('one.xyz'), + fileName: '', + isUntitled: false, + isDirty: false, + languages: [], + metadata: {}, + viewType: 'somethingElse' + }; + const extension: Extension<{}> = { + activate: () => Promise.resolve({}), + exports: {}, + extensionKind: ExtensionKind.UI, + extensionPath: '', + extensionUri: Uri.file(__filename), + id: RendererExtensionId, + isActive: true, + packageJSON: {} + }; + setup(() => { + downloader = mock(RendererExtensionDownloader); + vscNotebook = mock(VSCodeNotebook); + extensions = mock<IExtensions>(); + appEnv = mock<IApplicationEnvironment>(); + rendererExtension = new RendererExtension( + instance(vscNotebook), + instance(downloader), + instance(extensions), + instance(appEnv), + disposables + ); + onDidOpenNotebookDocument = new EventEmitter<NotebookDocument>(); + when(vscNotebook.notebookDocuments).thenReturn([]); + when(vscNotebook.onDidOpenNotebookDocument).thenReturn(onDidOpenNotebookDocument.event); + when(downloader.downloadAndInstall()).thenResolve(); + when(extensions.getExtension(anything())).thenReturn(); + }); + suite('Extension has not been installed in VSC Stable', () => { + setup(() => { + when(extensions.getExtension(anything())).thenReturn(); + when(appEnv.channel).thenReturn('stable'); + }); + test('A jupyter notebook is already open', async () => { + when(vscNotebook.notebookDocuments).thenReturn([jupyterNotebook]); + await rendererExtension.activate(); + + verify(downloader.downloadAndInstall()).never(); + }); + test('A jupyter notebook is opened', async () => { + await rendererExtension.activate(); + onDidOpenNotebookDocument.fire(jupyterNotebook); + + verify(downloader.downloadAndInstall()).never(); + }); + }); + suite('Extension has not been installed', () => { + setup(() => { + when(extensions.getExtension(anything())).thenReturn(); + when(appEnv.channel).thenReturn('insiders'); + }); + test('Should not download extension', async () => { + await rendererExtension.activate(); + + verify(downloader.downloadAndInstall()).never(); + }); + test('A jupyter notebook is already open', async () => { + when(vscNotebook.notebookDocuments).thenReturn([jupyterNotebook]); + await rendererExtension.activate(); + + verify(downloader.downloadAndInstall()).once(); + }); + test('A jupyter notebook is opened', async () => { + await rendererExtension.activate(); + onDidOpenNotebookDocument.fire(jupyterNotebook); + + verify(downloader.downloadAndInstall()).once(); + }); + test('A non-jupyter notebook is already open', async () => { + when(vscNotebook.notebookDocuments).thenReturn([nonJupyterNotebook]); + await rendererExtension.activate(); + + verify(downloader.downloadAndInstall()).never(); + }); + test('A non-jupyter notebook is opened', async () => { + await rendererExtension.activate(); + onDidOpenNotebookDocument.fire(nonJupyterNotebook); + + verify(downloader.downloadAndInstall()).never(); + }); + }); + suite('Extension has already been installed', () => { + setup(() => { + when(extensions.getExtension(RendererExtensionId)).thenReturn(extension); + when(appEnv.channel).thenReturn('insiders'); + }); + test('A jupyter notebook is already open', async () => { + when(vscNotebook.notebookDocuments).thenReturn([jupyterNotebook]); + await rendererExtension.activate(); + + verify(downloader.downloadAndInstall()).never(); + }); + test('A jupyter notebook is opened', async () => { + await rendererExtension.activate(); + onDidOpenNotebookDocument.fire(jupyterNotebook); + + verify(downloader.downloadAndInstall()).never(); + }); + test('A non-jupyter notebook is already open', async () => { + when(vscNotebook.notebookDocuments).thenReturn([nonJupyterNotebook]); + await rendererExtension.activate(); + + verify(downloader.downloadAndInstall()).never(); + }); + test('A non-jupyter notebook is opened', async () => { + await rendererExtension.activate(); + onDidOpenNotebookDocument.fire(nonJupyterNotebook); + + verify(downloader.downloadAndInstall()).never(); + }); + }); +}); diff --git a/src/test/datascience/notebook/rendererExtensionDownloader.unit.test.ts b/src/test/datascience/notebook/rendererExtensionDownloader.unit.test.ts new file mode 100644 index 000000000000..02863950c268 --- /dev/null +++ b/src/test/datascience/notebook/rendererExtensionDownloader.unit.test.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; +import { IFileDownloader, IOutputChannel } from '../../../client/common/types'; +import { RendererExtensionDownloadUri } from '../../../client/datascience/notebook/constants'; +import { RendererExtensionDownloader } from '../../../client/datascience/notebook/rendererExtensionDownloader'; +import { IDataScienceFileSystem } from '../../../client/datascience/types'; +import { noop } from '../../core'; + +// tslint:disable: no-any +suite('DataScience - NativeNotebook Download Renderer Extension', () => { + let downloader: RendererExtensionDownloader; + let appShell: IApplicationShell; + let output: IOutputChannel; + let fs: IDataScienceFileSystem; + let fileDownloader: IFileDownloader; + let cmdManager: ICommandManager; + const downloadedFile = Uri.file('TempRendererExtensionVSIX.vsix'); + setup(() => { + appShell = mock<IApplicationShell>(); + output = mock<IOutputChannel>(); + fs = mock<IDataScienceFileSystem>(); + fileDownloader = mock<IFileDownloader>(); + cmdManager = mock<ICommandManager>(); + downloader = new RendererExtensionDownloader( + instance(output), + instance(appShell), + instance(cmdManager), + instance(fileDownloader), + instance(fs) + ); + + when(fileDownloader.downloadFile(anything(), anything())).thenResolve(downloadedFile.fsPath); + when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb({ report: noop })); + when(cmdManager.executeCommand(anything(), anything())).thenResolve(); + }); + teardown(() => verify(fs.deleteLocalFile(downloadedFile.fsPath)).once()); + test('Should download & install extension', async () => { + await downloader.downloadAndInstall(); + + verify(fileDownloader.downloadFile(RendererExtensionDownloadUri, anything())).once(); + verify(cmdManager.executeCommand('workbench.extensions.installExtension', anything())).once(); + const fileArg = capture(cmdManager.executeCommand as any).first()[1] as Uri; + assert.equal(fileArg.fsPath, downloadedFile.fsPath); + }); + test('Should download & install extension once', async () => { + await Promise.all([downloader.downloadAndInstall(), downloader.downloadAndInstall()]); + await downloader.downloadAndInstall(); + await downloader.downloadAndInstall(); + + verify(fileDownloader.downloadFile(RendererExtensionDownloadUri, anything())).once(); + verify(cmdManager.executeCommand('workbench.extensions.installExtension', anything())).once(); + const fileArg = capture(cmdManager.executeCommand as any).first()[1] as Uri; + assert.equal(fileArg.fsPath, downloadedFile.fsPath); + }); +}); diff --git a/src/test/datascience/notebook/saving.ds.test.ts b/src/test/datascience/notebook/saving.ds.test.ts new file mode 100644 index 000000000000..ff1fb04f2cc2 --- /dev/null +++ b/src/test/datascience/notebook/saving.ds.test.ts @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-require-imports no-var-requires +import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; +import { assert, expect } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { commands, Uri } from 'vscode'; +import { NotebookCell } from '../../../../typings/vscode-proposed'; +import { IVSCodeNotebook } from '../../../client/common/application/types'; +import { IDisposable } from '../../../client/common/types'; +import { sleep } from '../../../client/common/utils/async'; +import { INotebookContentProvider } from '../../../client/datascience/notebook/types'; +import { INotebookEditorProvider } from '../../../client/datascience/types'; +import { createEventHandler, IExtensionTestApi, waitForCondition } from '../../common'; +import { closeActiveWindows, EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; +import { + assertHasExecutionCompletedSuccessfully, + assertHasExecutionCompletedWithErrors, + assertHasTextOutputInVSCode, + assertVSCCellHasErrorOutput, + assertVSCCellStateIsUndefined, + canRunTests, + closeNotebooksAndCleanUpAfterTests, + createTemporaryNotebook, + executeActiveDocument, + insertPythonCellAndWait, + saveActiveNotebook, + startJupyter, + trustAllNotebooks +} from './helper'; +// tslint:disable-next-line:no-require-imports no-var-requires +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); + +// tslint:disable: no-any no-invalid-this +suite('DataScience - VSCode Notebook - (Saving)', function () { + this.timeout(60_000); + let api: IExtensionTestApi; + let editorProvider: INotebookEditorProvider; + const disposables: IDisposable[] = []; + let vscodeNotebook: IVSCodeNotebook; + suiteSetup(async function () { + this.timeout(60_000); + api = await initialize(); + if (!(await canRunTests())) { + return this.skip(); + } + await startJupyter(true); + vscodeNotebook = api.serviceContainer.get<IVSCodeNotebook>(IVSCodeNotebook); + editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + }); + setup(async () => { + sinon.restore(); + await trustAllNotebooks(); + }); + teardown(async () => closeNotebooksAndCleanUpAfterTests(disposables)); + test('Clearing output will mark document as dirty', async function () { + // https://github.com/microsoft/vscode-python/issues/13162 + return this.skip(); + const templateIPynb = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'datascience', + 'notebook', + 'test.ipynb' + ); + // Don't use same file (due to dirty handling, we might save in dirty.) + // Cuz we won't save to file, hence extension will backup in dirty file and when u re-open it will open from dirty. + const testIPynb = Uri.file(await createTemporaryNotebook(templateIPynb, disposables)); + await editorProvider.open(testIPynb); + const contentProvider = api.serviceContainer.get<INotebookContentProvider>(INotebookContentProvider); + const changedEvent = createEventHandler(contentProvider, 'onDidChangeNotebook', disposables); + + // Clear the output & then save the notebook. + await commands.executeCommand('notebook.clearAllCellsOutputs'); + + // Wait till execution count changes & it is marked as dirty + await changedEvent.assertFired(5_000); + }); + test('Saving after clearing should result in execution_count=null in ipynb file', async function () { + // https://github.com/microsoft/vscode-python/issues/13159 + return this.skip(); + const templateIPynb = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'datascience', + 'notebook', + 'test.ipynb' + ); + // Don't use same file (due to dirty handling, we might save in dirty.) + // Cuz we won't save to file, hence extension will backup in dirty file and when u re-open it will open from dirty. + const testIPynb = Uri.file(await createTemporaryNotebook(templateIPynb, disposables)); + await editorProvider.open(testIPynb); + const notebookDocument = vscodeNotebook.activeNotebookEditor?.document!; + const vscCells = notebookDocument.cells!; + const contentProvider = api.serviceContainer.get<INotebookContentProvider>(INotebookContentProvider); + const changedEvent = createEventHandler(contentProvider, 'onDidChangeNotebook', disposables); + + // Clear the output & then save the notebook. + await commands.executeCommand('notebook.clearAllCellsOutputs'); + + // Wait till execution count changes & it is marked as dirty + await waitForCondition( + async () => !vscCells[0].metadata.executionOrder && changedEvent.fired, + 5_000, + 'Cell did not get cleared' + ); + + await saveActiveNotebook(disposables); + + // Open nb json and validate execution_count = null. + const json = JSON.parse(fs.readFileSync(testIPynb.fsPath, { encoding: 'utf8' })) as nbformat.INotebookContent; + assert.ok(json.cells[0].execution_count === null); + }); + test('Verify output & metadata when re-opening (slow)', async () => { + const templateIPynb = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'datascience', + 'notebook', + 'empty.ipynb' + ); + // Don't use same file (due to dirty handling, we might save in dirty.) + // Cuz we won't save to file, hence extension will backup in dirty file and when u re-open it will open from dirty. + const testIPynb = Uri.file(await createTemporaryNotebook(templateIPynb, disposables)); + await editorProvider.open(testIPynb); + + await insertPythonCellAndWait('print(1)', 0); + await insertPythonCellAndWait('print(a)', 1); + await insertPythonCellAndWait('import time\nfor i in range(10000):\n print(i)\n time.sleep(0.1)', 2); + await insertPythonCellAndWait('import time\nfor i in range(10000):\n print(i)\n time.sleep(0.1)', 3); + let cell1: NotebookCell; + let cell2: NotebookCell; + let cell3: NotebookCell; + let cell4: NotebookCell; + + function initializeCells() { + cell1 = vscodeNotebook.activeNotebookEditor?.document.cells![0]!; + cell2 = vscodeNotebook.activeNotebookEditor?.document.cells![1]!; + cell3 = vscodeNotebook.activeNotebookEditor?.document.cells![2]!; + cell4 = vscodeNotebook.activeNotebookEditor?.document.cells![3]!; + } + initializeCells(); + await executeActiveDocument(); + await sleep(5_000); + // Wait till 1 & 2 finish & 3rd cell starts executing. + await waitForCondition( + async () => + assertHasExecutionCompletedSuccessfully(cell1) && + assertHasExecutionCompletedWithErrors(cell2) && + assertVSCCellStateIsUndefined(cell3) && + assertVSCCellStateIsUndefined(cell4), + 15_000, + 'Cells did not finish executing' + ); + + function verifyCelMetadata() { + assert.lengthOf(cell1.outputs, 1, 'Incorrect output for cell 1'); + assert.lengthOf(cell2.outputs, 1, 'Incorrect output for cell 2'); + assert.lengthOf(cell3.outputs, 0, 'Incorrect output for cell 3'); // stream and interrupt error. + assert.lengthOf(cell4.outputs, 0, 'Incorrect output for cell 4'); + + assert.equal( + cell1.metadata.runState, + vscodeNotebookEnums.NotebookCellRunState.Success, + 'Incorrect state 1' + ); + assert.equal(cell2.metadata.runState, vscodeNotebookEnums.NotebookCellRunState.Error, 'Incorrect state 2'); + assert.equal( + cell3.metadata.runState || vscodeNotebookEnums.NotebookCellRunState.Idle, + vscodeNotebookEnums.NotebookCellRunState.Idle, + 'Incorrect state 3' + ); + assert.equal( + cell4.metadata.runState || vscodeNotebookEnums.NotebookCellRunState.Idle, + vscodeNotebookEnums.NotebookCellRunState.Idle, + 'Incorrect state 4' + ); + + assertHasTextOutputInVSCode(cell1, '1', 0); + assertVSCCellHasErrorOutput(cell2); + + expect(cell1.metadata.executionOrder).to.be.greaterThan(0, 'Execution count should be > 0'); + expect(cell2.metadata.executionOrder).to.be.greaterThan( + cell1.metadata.executionOrder!, + 'Execution count > cell 1' + ); + assert.isUndefined(cell3.metadata.executionOrder, 'Execution count must be undefined for cell 3'); + assert.isUndefined(cell4.metadata.executionOrder, 'Execution count must be undefined for cell 4'); + + assert.isEmpty(cell1.metadata.statusMessage || '', 'Cell 1 status should be empty'); // No errors. + assert.isNotEmpty(cell2.metadata.statusMessage, 'Cell 1 status should be empty'); // Errors. + assert.isEmpty(cell3.metadata.statusMessage || '', 'Cell 3 status should be empty'); // Not executed. + assert.isEmpty(cell4.metadata.statusMessage || '', 'Cell 4 status should be empty'); // Not executed. + + // assert.isOk(cell1.metadata.runStartTime, 'Start time should be > 0'); // Flaky with VSC as we're using NB as source of truth. + // assert.isOk(cell1.metadata.lastRunDuration, 'Duration should be > 0'); // Flaky with VSC as we're using NB as source of truth. + // assert.isOk(cell2.metadata.runStartTime, 'Start time should be > 0'); // Flaky with VSC as we're using NB as source of truth. + // assert.isOk(cell2.metadata.lastRunDuration, 'Duration should be > 0'); // Flaky with VSC as we're using NB as source of truth. + assert.isUndefined(cell3.metadata.runStartTime, 'Cell 3 did should not have run'); + assert.isUndefined(cell3.metadata.lastRunDuration, 'Cell 3 did should not have run'); + assert.isUndefined(cell4.metadata.runStartTime, 'Cell 4 did should not have run'); + assert.isUndefined(cell4.metadata.lastRunDuration, 'Cell 4 did should not have run'); + } + + verifyCelMetadata(); + + // Save and close this nb. + await saveActiveNotebook(disposables); + await closeActiveWindows(); + + // Reopen the notebook & validate the metadata. + await editorProvider.open(testIPynb); + initializeCells(); + verifyCelMetadata(); + }); +}); diff --git a/src/test/datascience/notebook/survey.unit.test.ts b/src/test/datascience/notebook/survey.unit.test.ts new file mode 100644 index 000000000000..47299ade0613 --- /dev/null +++ b/src/test/datascience/notebook/survey.unit.test.ts @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fakeTimers from '@sinonjs/fake-timers'; +import { anything, deepEqual, instance, mock, reset, verify, when } from 'ts-mockito'; +import { EventEmitter } from 'vscode'; +import { NotebookDocument } from '../../../../types/vscode-proposed'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { IApplicationShell, IVSCodeNotebook, NotebookCellChangedEvent } from '../../../client/common/application/types'; +import { IBrowserService, IDisposable, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { CommonSurvey } from '../../../client/common/utils/localize'; +import { MillisecondsInADay } from '../../../client/constants'; +import { + NotebookSurveyBanner, + NotebookSurveyDataLogger, + NotebookSurveyUsageData +} from '../../../client/datascience/notebook/survey'; +import { INotebookEditor, INotebookEditorProvider } from '../../../client/datascience/types'; + +// tslint:disable: no-any +suite('DataScience - NativeNotebook Survey', () => { + let stateFactory: IPersistentStateFactory; + let stateService: IPersistentState<NotebookSurveyUsageData>; + let state: NotebookSurveyUsageData = {}; + let vscNotebook: IVSCodeNotebook; + let notebookEditorProvider: INotebookEditorProvider; + let browser: IBrowserService; + let shell: IApplicationShell; + let survey: IExtensionSingleActivationService; + const disposables: IDisposable[] = []; + let editor: INotebookEditor; + const mockDocument = instance(mock<NotebookDocument>()); + let onDidOpenNotebookEditor: EventEmitter<INotebookEditor>; + let onExecutedCode: EventEmitter<string>; + let onDidChangeNotebookDocument: EventEmitter<NotebookCellChangedEvent>; + let clock: fakeTimers.InstalledClock; + setup(async () => { + editor = mock<INotebookEditor>(); + onExecutedCode = new EventEmitter<string>(); + when(editor.onExecutedCode).thenReturn(onExecutedCode.event); + stateFactory = mock<IPersistentStateFactory>(); + stateService = mock<IPersistentState<NotebookSurveyUsageData>>(); + when(stateFactory.createGlobalPersistentState(anything(), anything())).thenReturn(instance(stateService)); + state = {}; + when(stateService.value).thenReturn(state); + when(stateService.updateValue(anything())).thenCall((newState) => { + Object.assign(state, newState); + }); + vscNotebook = mock<IVSCodeNotebook>(); + onDidChangeNotebookDocument = new EventEmitter<NotebookCellChangedEvent>(); + when(vscNotebook.onDidChangeNotebookDocument).thenReturn(onDidChangeNotebookDocument.event); + notebookEditorProvider = mock<INotebookEditorProvider>(); + onDidOpenNotebookEditor = new EventEmitter<INotebookEditor>(); + when(notebookEditorProvider.onDidOpenNotebookEditor).thenReturn(onDidOpenNotebookEditor.event); + shell = mock<IApplicationShell>(); + browser = mock<IBrowserService>(); + clock = fakeTimers.install(); + clock.setSystemTime(new Date(2020, 7, 1)); // Survey will work only after 1st August. + }); + async function loadAndActivateExtension() { + const surveyBanner = new NotebookSurveyBanner(instance(shell), instance(stateFactory), instance(browser)); + survey = new NotebookSurveyDataLogger( + instance(stateFactory), + instance(vscNotebook), + instance(notebookEditorProvider), + disposables, + surveyBanner + ); + await survey.activate(); + await clock.runAllAsync(); + } + teardown(() => { + clock.uninstall(); + while (disposables.length) { + disposables.pop()!.dispose(); + } + }); + async function performCellOperations(numberOfCellActions: number, numberOfCellRuns: number) { + for (let i = 0; i < numberOfCellRuns; i += 1) { + onExecutedCode.fire(''); + } + for (let i = 0; i < numberOfCellActions; i += 1) { + onDidChangeNotebookDocument.fire({ type: 'changeCells', changes: [], document: mockDocument }); + } + await clock.runAllAsync(); + } + test('No survey displayed when loading extension for first time', async () => { + await loadAndActivateExtension(); + + verify(browser.launch(anything())).never(); + }); + test('Display survey if user performs > 100 cell executions in a notebook', async () => { + when(shell.showInformationMessage(anything(), anything(), anything(), anything())).thenResolve( + CommonSurvey.yesLabel() as any + ); + await loadAndActivateExtension(); + + // Open nb. + when(editor.type).thenReturn('native'); + onDidOpenNotebookEditor.fire(instance(editor)); + + // Perform 100 actions, survey will not be displayed + await performCellOperations(0, 100); + + verify(browser.launch(anything())).never(); + + // After the 101st action, survey should be displayed. + await performCellOperations(1, 0); + + verify(browser.launch(anything())).once(); + + // Verify survey is disabled. + verify(stateService.updateValue(deepEqual({ surveyDisabled: true }))).once(); + }); + test('Remind if survey not taken & selected to remind again', async () => { + when(shell.showInformationMessage(anything(), anything(), anything(), anything())).thenResolve( + CommonSurvey.remindMeLaterLabel() as any + ); + await loadAndActivateExtension(); + + // Open nb. + when(editor.type).thenReturn('native'); + onDidOpenNotebookEditor.fire(instance(editor)); + + // Perform 120 actions, survey will be displayed. + await performCellOperations(60, 60); + verify(shell.showInformationMessage(anything(), anything(), anything(), anything())).once(); + verify(browser.launch(anything())).never(); + + // Open extension again & confirm prompt is displayed again. + await loadAndActivateExtension(); + + verify(shell.showInformationMessage(anything(), anything(), anything(), anything())).twice(); + }); + test('Do not display again if cancelled', async () => { + when(shell.showInformationMessage(anything(), anything(), anything(), anything())).thenResolve( + CommonSurvey.noLabel() as any + ); + await loadAndActivateExtension(); + + // Open nb. + when(editor.type).thenReturn('native'); + onDidOpenNotebookEditor.fire(instance(editor)); + + // Perform 120 actions, survey will be displayed. + await performCellOperations(60, 60); + verify(shell.showInformationMessage(anything(), anything(), anything(), anything())).once(); + verify(browser.launch(anything())).never(); + + // Perform more actions & should not be prompted again. + reset(shell); + reset(browser); + await performCellOperations(60, 60); + verify(shell.showInformationMessage(anything(), anything(), anything(), anything())).never(); + verify(browser.launch(anything())).never(); + + // Open extension again & confirm prompt is displayed again. + await loadAndActivateExtension(); + + verify(shell.showInformationMessage(anything(), anything(), anything(), anything())).never(); + verify(browser.launch(anything())).never(); + }); + test('Display survey if user performs > 100 cell actions in a notebook', async () => { + when(shell.showInformationMessage(anything(), anything(), anything(), anything())).thenResolve( + CommonSurvey.yesLabel() as any + ); + + await loadAndActivateExtension(); + + // Open nb. + when(editor.type).thenReturn('native'); + onDidOpenNotebookEditor.fire(instance(editor)); + + // Perform 100 actions, survey will not be displayed + await performCellOperations(50, 50); + verify(browser.launch(anything())).never(); + + // After the 101st action, survey should be displayed. + await performCellOperations(0, 1); + verify(browser.launch(anything())).once(); + + // Verify survey is disabled. + verify(stateService.updateValue(deepEqual({ surveyDisabled: true }))).once(); + + // No subsequent prompts (ever). + reset(browser); + await loadAndActivateExtension(); + await clock.runAllAsync(); + await performCellOperations(100, 100); + verify(browser.launch(anything())).never(); + }); + test('After 5 edits and 6 days of inactivity, display survey', async () => { + when(shell.showInformationMessage(anything(), anything(), anything(), anything())).thenResolve( + CommonSurvey.yesLabel() as any + ); + await loadAndActivateExtension(); + + // Open nb. + when(editor.type).thenReturn('native'); + onDidOpenNotebookEditor.fire(instance(editor)); + + // Perform 6 actions, survey will not be displayed + await performCellOperations(4, 2); + verify(browser.launch(anything())).never(); + + // Day 2, & confirm no survey prompts. + clock.tick(2 * MillisecondsInADay); + await loadAndActivateExtension(); + await clock.runAllAsync(); + verify(browser.launch(anything())).never(); + + // Day 3, & confirm no survey prompts. + clock.tick(3 * MillisecondsInADay); + await loadAndActivateExtension(); + await clock.runAllAsync(); + verify(browser.launch(anything())).never(); + + // Day 6, & confirm survey prompt is displayed. + clock.tick(6 * MillisecondsInADay); + await loadAndActivateExtension(); + await clock.runAllAsync(); + verify(browser.launch(anything())).once(); + verify(stateService.updateValue(deepEqual({ surveyDisabled: true }))).once(); + + // No subsequent prompts (ever). + reset(browser); + await loadAndActivateExtension(); + await clock.runAllAsync(); + await performCellOperations(100, 100); + verify(browser.launch(anything())).never(); + }); +}); diff --git a/src/test/datascience/notebook/test.ipynb b/src/test/datascience/notebook/test.ipynb new file mode 100644 index 000000000000..412b9c008e74 --- /dev/null +++ b/src/test/datascience/notebook/test.ipynb @@ -0,0 +1 @@ +{"cells":[{"source":["a=1\n","a"],"cell_type":"code","outputs":[{"output_type":"execute_result","data":{"text/plain":"1"},"metadata":{},"execution_count":1}],"metadata":{},"execution_count":1}],"nbformat":4,"nbformat_minor":2,"metadata":{"language_info":{"name":"python","codemirror_mode":{"name":"ipython","version":3}},"orig_nbformat":2,"file_extension":".py","mimetype":"text/x-python","name":"python","npconvert_exporter":"python","pygments_lexer":"ipython3","version":3}} \ No newline at end of file diff --git a/src/test/datascience/notebook/test.png b/src/test/datascience/notebook/test.png new file mode 100644 index 000000000000..4045d3a437f9 Binary files /dev/null and b/src/test/datascience/notebook/test.png differ diff --git a/src/test/datascience/notebook/testJsonContents.ipynb b/src/test/datascience/notebook/testJsonContents.ipynb new file mode 100644 index 000000000000..455546f6ca04 --- /dev/null +++ b/src/test/datascience/notebook/testJsonContents.ipynb @@ -0,0 +1,407 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "source": [ + "print(13333333)\n", + "print(13333333)\n", + "print(133333335)\n", + "\n", + "import os\n" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "13333333\n", + "13333333\n", + "133333335\n" + ] + } + ], + "metadata": { + "execution": { + "iopub.execute_input": "2020-06-03T17:28:38.310392Z", + "iopub.status.busy": "2020-06-03T17:28:38.310138Z", + "iopub.status.idle": "2020-06-03T17:28:38.314294Z", + "shell.execute_reply": "2020-06-03T17:28:38.313546Z", + "shell.execute_reply.started": "2020-06-03T17:28:38.310363Z" + }, + "tags": [], + "vscode": { + "end_execution_time": "2020-08-05T19:41:08.654Z", + "start_execution_time": "2020-08-05T19:40:58.908Z" + } + } + }, + { + "cell_type": "code", + "execution_count": 1, + "source": [ + "#dd\n", + "import sys\n", + "sys.executable\n" + ], + "outputs": [], + "metadata": { + "vscode": { + "end_execution_time": "2020-07-13T20:05:50.031Z", + "start_execution_time": "2020-07-13T20:05:50.006Z" + } + } + }, + { + "cell_type": "code", + "execution_count": 1, + "source": [ + "#dd\n", + "import sys\n", + "sys.executable\n", + "sys.executable\n", + "sys.executable\n", + "sys.executable" + ], + "outputs": [], + "metadata": { + "vscode": { + "end_execution_time": "2020-07-08T17:59:53.598Z", + "start_execution_time": "2020-07-08T17:59:53.557Z" + } + } + }, + { + "cell_type": "markdown", + "source": [ + "[HelloWorld](command:python.setInterpreter) " + ], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "%%timeit -n 1 -r 1\n", + "\n", + "testvec2 = [[1] * 20] * 200\n", + "for i, val in enumerate(testvec2):\n", + "\ta = 1\n", + " #print(f\"Using print: {val}\")\n", + " #display(val)\n", + " # display(i)" + ], + "outputs": [], + "metadata": { + "tags": [], + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": 2, + "source": [ + "a=1\n", + "\n", + "print(a)\n", + "import time\n", + "time.sleep(90.1)\n", + "print(2)" + ], + "outputs": [], + "metadata": { + "tags": [], + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "a=2" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "a=1\n", + "\n", + "print(a)\n", + "import time\n", + "time.sleep(.1)\n", + "print(2)" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "a=1\n", + "\n", + "print(a)\n", + "import time\n", + "time.sleep(.1)\n", + "print(2)" + ], + "outputs": [], + "metadata": { + "tags": [], + "vscode": {} + } + }, + { + "cell_type": "markdown", + "source": [ + "# Cell 3" + ], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "a=1" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "a" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "import time\n", + "time.sleep(.5)\n", + "print('a')" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "import time\n", + "time.sleep(.5)\n", + "print('b')" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "import time\n", + "for i in range(10000):\n", + "\ttime.sleep(0.1)\n", + "\tprint(i)\n", + "\n", + "print('b')" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "import sys\n", + "sys.executable" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "import ipywidgets as widgets\n", + "s = widgets.IntSlider()\n", + "s" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "import nglview\n", + "view = nglview.show_pdbid(\"3pqr\")\n", + "view" + ], + "outputs": [], + "metadata": { + "scrolled": true, + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "#pip install requests jupyter notebook" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "import matplotlib\n", + "import time\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "%matplotlib inline \n", + "x = np.linspace(0, 3*np.pi, 500)\n", + "plt.plot(x, np.sin(x**2))\n", + "plt.title('Plot1')\n", + "plt.show()\n", + "plt.show()\n", + "\n", + "#time.sleep(1)\n", + "plt.title('Plot2')\n", + "plt.show()" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "#%%\n", + "# To add a new cell, type '# %%'\n", + "# To add a new markdown cell, type '# %% [markdown]'\n", + "from pythreejs import *\n", + "import ipywidgets\n", + "from IPython.display import display" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "%%timeit -n 1 -r 1\n", + "\n", + "testvec2 = [[1] * 20] * 200\n", + "for i, val in enumerate(testvec2):\n", + " print(f\"Using print: {val}\")\n", + " display(val)" + ], + "outputs": [], + "metadata": { + "tags": [], + "vscode": {} + } + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "for i in range(100):\n", + "\timport time\n", + "\ttime.sleep(1)\n", + "\tprint(i)" + ], + "outputs": [], + "metadata": { + "vscode": {} + } + } + ], + "metadata": { + "file_extension": ".py", + "kernelspec": { + "display_name": ".NET (C#)", + "language": "python", + "name": ".net-csharp" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.2-final" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": false, + "sideBar": false, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": false, + "toc_window_display": false + }, + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/src/test/datascience/notebook/with3CellsAndOutput.ipynb b/src/test/datascience/notebook/with3CellsAndOutput.ipynb new file mode 100644 index 000000000000..630d2a58718e --- /dev/null +++ b/src/test/datascience/notebook/with3CellsAndOutput.ipynb @@ -0,0 +1,101 @@ +{ + "cells": [ + { + "source": [ + "a=1\n", + "a" + ], + "cell_type": "code", + "metadata": {}, + "execution_count": 1, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "Package Version \n-------------------- ----------\nadal 1.2.2 \naltair 4.1.0 \nappdirs 1.4.3 \nappnope 0.1.0 \nattrs 19.3.0 \nazure-common 1.1.25 \nazure-kusto-data 0.0.44 \nazure-kusto-ingest 0.0.44 \nazure-storage-blob 2.1.0 \nazure-storage-common 2.1.0 \nazure-storage-queue 2.1.0 \nbackcall 0.1.0 \nbeakerx 1.4.1 \nblack 19.10b0 \nbleach 3.1.4 \nbqplot 0.12.6 \nbranca 0.3.1 \ncertifi 2020.4.5.1\ncffi 1.14.0 \nchardet 3.0.4 \nclick 7.1.2 \ncryptography 2.9.2 \ncycler 0.10.0 \ndebugpy 1.0.0b7 \ndecorator 4.4.2 \ndefusedxml 0.6.0 \nentrypoints 0.3 \nidna 2.9 \nipydatawidgets 4.0.1 \nipykernel 5.2.1 \nipyleaflet 0.12.4 \nipython 7.13.0 \nipython-genutils 0.2.0 \nipyvolume 0.5.2 \nipywebrtc 0.5.0 \nipywidgets 7.5.1 \nisodate 0.6.0 \njedi 0.17.0 \nJinja2 2.11.2 \njson5 0.9.5 \njsonschema 3.2.0 \njupyter-client 6.1.3 \njupyter-core 4.6.3 \njupyterlab 2.1.3 \njupyterlab-server 1.1.5 \nK3D 2.7.4 \nkiwisolver 1.2.0 \nMarkupSafe 1.1.1 \nmatplotlib 3.2.1 \nmistune 0.8.4 \nmsrest 0.6.13 \nmsrestazure 0.6.3 \nnbconvert 5.6.1 \nnbformat 5.0.5 \nnglview 2.7.5 \nnotebook 6.0.3 \nnumpy 1.18.2 \noauthlib 3.1.0 \npandas 1.0.3 \npandocfilters 1.4.2 \nparso 0.7.0 \npathspec 0.8.0 \npexpect 4.8.0 \npickleshare 0.7.5 \nPillow 7.1.1 \npip 19.2.3 \nprometheus-client 0.7.1 \nprompt-toolkit 3.0.5 \nptyprocess 0.6.0 \npy4j 0.10.9 \npycparser 2.20 \nPygments 2.6.1 \nPyJWT 1.7.1 \npyparsing 2.4.7 \npyrsistent 0.16.0 \npython-dateutil 2.8.1 \npythreejs 2.2.0 \npytz 2019.3 \npyzmq 19.0.0 \nqgrid 1.1.1 \nregex 2020.4.4 \nrequests 2.23.0 \nrequests-oauthlib 1.3.0 \nSend2Trash 1.5.0 \nsetuptools 41.2.0 \nsix 1.14.0 \nterminado 0.8.3 \ntestpath 0.4.4 \ntoml 0.10.0 \ntoolz 0.10.0 \ntornado 6.0.4 \ntraitlets 4.3.3 \ntraittypes 0.2.1 \ntyped-ast 1.4.1 \nurllib3 1.25.9 \nvega-datasets 0.8.0 \nwcwidth 0.1.9 \nwebencodings 0.5.1 \nwidgetsnbextension 3.5.1 \nxarray 0.15.1 \nNote: you may need to restart the kernel to use updated packages.\n" + } + ], + "metadata": { + "tags": [] + } + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "output_type": "error", + "ename": "SyntaxError", + "evalue": "invalid syntax (<ipython-input-1-8b7c24be1ec9>, line 1)", + "traceback": [ + "\u001b[0;36m File \u001b[0;32m\"<ipython-input-1-8b7c24be1ec9>\"\u001b[0;36m, line \u001b[0;32m1\u001b[0m\n\u001b[0;31m with Error\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n" + ] + } + ], + "source": [ + "with Error" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "<Figure size 432x288 with 1 Axes>", + "image/svg+xml": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<!-- Created with matplotlib (https://matplotlib.org/) -->\n<svg height=\"277.314375pt\" version=\"1.1\" viewBox=\"0 0 392.14375 277.314375\" width=\"392.14375pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n <defs>\n <style type=\"text/css\">\n*{stroke-linecap:butt;stroke-linejoin:round;}\n </style>\n </defs>\n <g id=\"figure_1\">\n <g id=\"patch_1\">\n <path d=\"M 0 277.314375 \nL 392.14375 277.314375 \nL 392.14375 0 \nL 0 0 \nz\n\" style=\"fill:none;\"/>\n </g>\n <g id=\"axes_1\">\n <g id=\"patch_2\">\n <path d=\"M 50.14375 239.758125 \nL 384.94375 239.758125 \nL 384.94375 22.318125 \nL 50.14375 22.318125 \nz\n\" style=\"fill:#ffffff;\"/>\n </g>\n <g id=\"matplotlib.axis_1\">\n <g id=\"xtick_1\">\n <g id=\"line2d_1\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 65.361932 239.758125 \nL 65.361932 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_2\">\n <defs>\n <path d=\"M 0 0 \nL 0 3.5 \n\" id=\"m3ed7cd1696\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"65.361932\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_1\">\n <!-- 0.00 -->\n <defs>\n <path d=\"M 31.78125 66.40625 \nQ 24.171875 66.40625 20.328125 58.90625 \nQ 16.5 51.421875 16.5 36.375 \nQ 16.5 21.390625 20.328125 13.890625 \nQ 24.171875 6.390625 31.78125 6.390625 \nQ 39.453125 6.390625 43.28125 13.890625 \nQ 47.125 21.390625 47.125 36.375 \nQ 47.125 51.421875 43.28125 58.90625 \nQ 39.453125 66.40625 31.78125 66.40625 \nz\nM 31.78125 74.21875 \nQ 44.046875 74.21875 50.515625 64.515625 \nQ 56.984375 54.828125 56.984375 36.375 \nQ 56.984375 17.96875 50.515625 8.265625 \nQ 44.046875 -1.421875 31.78125 -1.421875 \nQ 19.53125 -1.421875 13.0625 8.265625 \nQ 6.59375 17.96875 6.59375 36.375 \nQ 6.59375 54.828125 13.0625 64.515625 \nQ 19.53125 74.21875 31.78125 74.21875 \nz\n\" id=\"DejaVuSans-48\"/>\n <path d=\"M 10.6875 12.40625 \nL 21 12.40625 \nL 21 0 \nL 10.6875 0 \nz\n\" id=\"DejaVuSans-46\"/>\n </defs>\n <g transform=\"translate(54.229119 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_2\">\n <g id=\"line2d_3\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 103.59857 239.758125 \nL 103.59857 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_4\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"103.59857\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_2\">\n <!-- 0.25 -->\n <defs>\n <path d=\"M 19.1875 8.296875 \nL 53.609375 8.296875 \nL 53.609375 0 \nL 7.328125 0 \nL 7.328125 8.296875 \nQ 12.9375 14.109375 22.625 23.890625 \nQ 32.328125 33.6875 34.8125 36.53125 \nQ 39.546875 41.84375 41.421875 45.53125 \nQ 43.3125 49.21875 43.3125 52.78125 \nQ 43.3125 58.59375 39.234375 62.25 \nQ 35.15625 65.921875 28.609375 65.921875 \nQ 23.96875 65.921875 18.8125 64.3125 \nQ 13.671875 62.703125 7.8125 59.421875 \nL 7.8125 69.390625 \nQ 13.765625 71.78125 18.9375 73 \nQ 24.125 74.21875 28.421875 74.21875 \nQ 39.75 74.21875 46.484375 68.546875 \nQ 53.21875 62.890625 53.21875 53.421875 \nQ 53.21875 48.921875 51.53125 44.890625 \nQ 49.859375 40.875 45.40625 35.40625 \nQ 44.1875 33.984375 37.640625 27.21875 \nQ 31.109375 20.453125 19.1875 8.296875 \nz\n\" id=\"DejaVuSans-50\"/>\n <path d=\"M 10.796875 72.90625 \nL 49.515625 72.90625 \nL 49.515625 64.59375 \nL 19.828125 64.59375 \nL 19.828125 46.734375 \nQ 21.96875 47.46875 24.109375 47.828125 \nQ 26.265625 48.1875 28.421875 48.1875 \nQ 40.625 48.1875 47.75 41.5 \nQ 54.890625 34.8125 54.890625 23.390625 \nQ 54.890625 11.625 47.5625 5.09375 \nQ 40.234375 -1.421875 26.90625 -1.421875 \nQ 22.3125 -1.421875 17.546875 -0.640625 \nQ 12.796875 0.140625 7.71875 1.703125 \nL 7.71875 11.625 \nQ 12.109375 9.234375 16.796875 8.0625 \nQ 21.484375 6.890625 26.703125 6.890625 \nQ 35.15625 6.890625 40.078125 11.328125 \nQ 45.015625 15.765625 45.015625 23.390625 \nQ 45.015625 31 40.078125 35.4375 \nQ 35.15625 39.890625 26.703125 39.890625 \nQ 22.75 39.890625 18.8125 39.015625 \nQ 14.890625 38.140625 10.796875 36.28125 \nz\n\" id=\"DejaVuSans-53\"/>\n </defs>\n <g transform=\"translate(92.465757 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_3\">\n <g id=\"line2d_5\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 141.835207 239.758125 \nL 141.835207 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_6\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"141.835207\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_3\">\n <!-- 0.50 -->\n <g transform=\"translate(130.702395 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_4\">\n <g id=\"line2d_7\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 180.071845 239.758125 \nL 180.071845 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_8\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"180.071845\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_4\">\n <!-- 0.75 -->\n <defs>\n <path d=\"M 8.203125 72.90625 \nL 55.078125 72.90625 \nL 55.078125 68.703125 \nL 28.609375 0 \nL 18.3125 0 \nL 43.21875 64.59375 \nL 8.203125 64.59375 \nz\n\" id=\"DejaVuSans-55\"/>\n </defs>\n <g transform=\"translate(168.939033 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_5\">\n <g id=\"line2d_9\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 218.308483 239.758125 \nL 218.308483 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_10\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"218.308483\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_5\">\n <!-- 1.00 -->\n <defs>\n <path d=\"M 12.40625 8.296875 \nL 28.515625 8.296875 \nL 28.515625 63.921875 \nL 10.984375 60.40625 \nL 10.984375 69.390625 \nL 28.421875 72.90625 \nL 38.28125 72.90625 \nL 38.28125 8.296875 \nL 54.390625 8.296875 \nL 54.390625 0 \nL 12.40625 0 \nz\n\" id=\"DejaVuSans-49\"/>\n </defs>\n <g transform=\"translate(207.17567 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_6\">\n <g id=\"line2d_11\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 256.54512 239.758125 \nL 256.54512 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_12\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"256.54512\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_6\">\n <!-- 1.25 -->\n <g transform=\"translate(245.412308 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_7\">\n <g id=\"line2d_13\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 294.781758 239.758125 \nL 294.781758 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_14\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"294.781758\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_7\">\n <!-- 1.50 -->\n <g transform=\"translate(283.648946 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_8\">\n <g id=\"line2d_15\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 333.018396 239.758125 \nL 333.018396 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_16\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"333.018396\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_8\">\n <!-- 1.75 -->\n <g transform=\"translate(321.885583 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_9\">\n <g id=\"line2d_17\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 371.255034 239.758125 \nL 371.255034 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_18\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"371.255034\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_9\">\n <!-- 2.00 -->\n <g transform=\"translate(360.122221 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"text_10\">\n <!-- time (s) -->\n <defs>\n <path d=\"M 18.3125 70.21875 \nL 18.3125 54.6875 \nL 36.8125 54.6875 \nL 36.8125 47.703125 \nL 18.3125 47.703125 \nL 18.3125 18.015625 \nQ 18.3125 11.328125 20.140625 9.421875 \nQ 21.96875 7.515625 27.59375 7.515625 \nL 36.8125 7.515625 \nL 36.8125 0 \nL 27.59375 0 \nQ 17.1875 0 13.234375 3.875 \nQ 9.28125 7.765625 9.28125 18.015625 \nL 9.28125 47.703125 \nL 2.6875 47.703125 \nL 2.6875 54.6875 \nL 9.28125 54.6875 \nL 9.28125 70.21875 \nz\n\" id=\"DejaVuSans-116\"/>\n <path d=\"M 9.421875 54.6875 \nL 18.40625 54.6875 \nL 18.40625 0 \nL 9.421875 0 \nz\nM 9.421875 75.984375 \nL 18.40625 75.984375 \nL 18.40625 64.59375 \nL 9.421875 64.59375 \nz\n\" id=\"DejaVuSans-105\"/>\n <path d=\"M 52 44.1875 \nQ 55.375 50.25 60.0625 53.125 \nQ 64.75 56 71.09375 56 \nQ 79.640625 56 84.28125 50.015625 \nQ 88.921875 44.046875 88.921875 33.015625 \nL 88.921875 0 \nL 79.890625 0 \nL 79.890625 32.71875 \nQ 79.890625 40.578125 77.09375 44.375 \nQ 74.3125 48.1875 68.609375 48.1875 \nQ 61.625 48.1875 57.5625 43.546875 \nQ 53.515625 38.921875 53.515625 30.90625 \nL 53.515625 0 \nL 44.484375 0 \nL 44.484375 32.71875 \nQ 44.484375 40.625 41.703125 44.40625 \nQ 38.921875 48.1875 33.109375 48.1875 \nQ 26.21875 48.1875 22.15625 43.53125 \nQ 18.109375 38.875 18.109375 30.90625 \nL 18.109375 0 \nL 9.078125 0 \nL 9.078125 54.6875 \nL 18.109375 54.6875 \nL 18.109375 46.1875 \nQ 21.1875 51.21875 25.484375 53.609375 \nQ 29.78125 56 35.6875 56 \nQ 41.65625 56 45.828125 52.96875 \nQ 50 49.953125 52 44.1875 \nz\n\" id=\"DejaVuSans-109\"/>\n <path d=\"M 56.203125 29.59375 \nL 56.203125 25.203125 \nL 14.890625 25.203125 \nQ 15.484375 15.921875 20.484375 11.0625 \nQ 25.484375 6.203125 34.421875 6.203125 \nQ 39.59375 6.203125 44.453125 7.46875 \nQ 49.3125 8.734375 54.109375 11.28125 \nL 54.109375 2.78125 \nQ 49.265625 0.734375 44.1875 -0.34375 \nQ 39.109375 -1.421875 33.890625 -1.421875 \nQ 20.796875 -1.421875 13.15625 6.1875 \nQ 5.515625 13.8125 5.515625 26.8125 \nQ 5.515625 40.234375 12.765625 48.109375 \nQ 20.015625 56 32.328125 56 \nQ 43.359375 56 49.78125 48.890625 \nQ 56.203125 41.796875 56.203125 29.59375 \nz\nM 47.21875 32.234375 \nQ 47.125 39.59375 43.09375 43.984375 \nQ 39.0625 48.390625 32.421875 48.390625 \nQ 24.90625 48.390625 20.390625 44.140625 \nQ 15.875 39.890625 15.1875 32.171875 \nz\n\" id=\"DejaVuSans-101\"/>\n <path id=\"DejaVuSans-32\"/>\n <path d=\"M 31 75.875 \nQ 24.46875 64.65625 21.28125 53.65625 \nQ 18.109375 42.671875 18.109375 31.390625 \nQ 18.109375 20.125 21.3125 9.0625 \nQ 24.515625 -2 31 -13.1875 \nL 23.1875 -13.1875 \nQ 15.875 -1.703125 12.234375 9.375 \nQ 8.59375 20.453125 8.59375 31.390625 \nQ 8.59375 42.28125 12.203125 53.3125 \nQ 15.828125 64.359375 23.1875 75.875 \nz\n\" id=\"DejaVuSans-40\"/>\n <path d=\"M 44.28125 53.078125 \nL 44.28125 44.578125 \nQ 40.484375 46.53125 36.375 47.5 \nQ 32.28125 48.484375 27.875 48.484375 \nQ 21.1875 48.484375 17.84375 46.4375 \nQ 14.5 44.390625 14.5 40.28125 \nQ 14.5 37.15625 16.890625 35.375 \nQ 19.28125 33.59375 26.515625 31.984375 \nL 29.59375 31.296875 \nQ 39.15625 29.25 43.1875 25.515625 \nQ 47.21875 21.78125 47.21875 15.09375 \nQ 47.21875 7.46875 41.1875 3.015625 \nQ 35.15625 -1.421875 24.609375 -1.421875 \nQ 20.21875 -1.421875 15.453125 -0.5625 \nQ 10.6875 0.296875 5.421875 2 \nL 5.421875 11.28125 \nQ 10.40625 8.6875 15.234375 7.390625 \nQ 20.0625 6.109375 24.8125 6.109375 \nQ 31.15625 6.109375 34.5625 8.28125 \nQ 37.984375 10.453125 37.984375 14.40625 \nQ 37.984375 18.0625 35.515625 20.015625 \nQ 33.0625 21.96875 24.703125 23.78125 \nL 21.578125 24.515625 \nQ 13.234375 26.265625 9.515625 29.90625 \nQ 5.8125 33.546875 5.8125 39.890625 \nQ 5.8125 47.609375 11.28125 51.796875 \nQ 16.75 56 26.8125 56 \nQ 31.78125 56 36.171875 55.265625 \nQ 40.578125 54.546875 44.28125 53.078125 \nz\n\" id=\"DejaVuSans-115\"/>\n <path d=\"M 8.015625 75.875 \nL 15.828125 75.875 \nQ 23.140625 64.359375 26.78125 53.3125 \nQ 30.421875 42.28125 30.421875 31.390625 \nQ 30.421875 20.453125 26.78125 9.375 \nQ 23.140625 -1.703125 15.828125 -13.1875 \nL 8.015625 -13.1875 \nQ 14.5 -2 17.703125 9.0625 \nQ 20.90625 20.125 20.90625 31.390625 \nQ 20.90625 42.671875 17.703125 53.65625 \nQ 14.5 64.65625 8.015625 75.875 \nz\n\" id=\"DejaVuSans-41\"/>\n </defs>\n <g transform=\"translate(198.152344 268.034687)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"39.208984\" xlink:href=\"#DejaVuSans-105\"/>\n <use x=\"66.992188\" xlink:href=\"#DejaVuSans-109\"/>\n <use x=\"164.404297\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"225.927734\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"257.714844\" xlink:href=\"#DejaVuSans-40\"/>\n <use x=\"296.728516\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"348.828125\" xlink:href=\"#DejaVuSans-41\"/>\n </g>\n </g>\n </g>\n <g id=\"matplotlib.axis_2\">\n <g id=\"ytick_1\">\n <g id=\"line2d_19\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 229.874489 \nL 384.94375 229.874489 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_20\">\n <defs>\n <path d=\"M 0 0 \nL -3.5 0 \n\" id=\"m6dd8a7d0d7\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"229.874489\"/>\n </g>\n </g>\n <g id=\"text_11\">\n <!-- 0.00 -->\n <g transform=\"translate(20.878125 233.673707)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_2\">\n <g id=\"line2d_21\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 205.165398 \nL 384.94375 205.165398 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_22\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"205.165398\"/>\n </g>\n </g>\n <g id=\"text_12\">\n <!-- 0.25 -->\n <g transform=\"translate(20.878125 208.964616)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_3\">\n <g id=\"line2d_23\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 180.456307 \nL 384.94375 180.456307 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_24\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"180.456307\"/>\n </g>\n </g>\n <g id=\"text_13\">\n <!-- 0.50 -->\n <g transform=\"translate(20.878125 184.255526)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_4\">\n <g id=\"line2d_25\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 155.747216 \nL 384.94375 155.747216 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_26\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"155.747216\"/>\n </g>\n </g>\n <g id=\"text_14\">\n <!-- 0.75 -->\n <g transform=\"translate(20.878125 159.546435)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_5\">\n <g id=\"line2d_27\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 131.038125 \nL 384.94375 131.038125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_28\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"131.038125\"/>\n </g>\n </g>\n <g id=\"text_15\">\n <!-- 1.00 -->\n <g transform=\"translate(20.878125 134.837344)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_6\">\n <g id=\"line2d_29\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 106.329034 \nL 384.94375 106.329034 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_30\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"106.329034\"/>\n </g>\n </g>\n <g id=\"text_16\">\n <!-- 1.25 -->\n <g transform=\"translate(20.878125 110.128253)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_7\">\n <g id=\"line2d_31\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 81.619943 \nL 384.94375 81.619943 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_32\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"81.619943\"/>\n </g>\n </g>\n <g id=\"text_17\">\n <!-- 1.50 -->\n <g transform=\"translate(20.878125 85.419162)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_8\">\n <g id=\"line2d_33\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 56.910852 \nL 384.94375 56.910852 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_34\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"56.910852\"/>\n </g>\n </g>\n <g id=\"text_18\">\n <!-- 1.75 -->\n <g transform=\"translate(20.878125 60.710071)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_9\">\n <g id=\"line2d_35\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 32.201761 \nL 384.94375 32.201761 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_36\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"32.201761\"/>\n </g>\n </g>\n <g id=\"text_19\">\n <!-- 2.00 -->\n <g transform=\"translate(20.878125 36.00098)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"text_20\">\n <!-- voltage (mV) -->\n <defs>\n <path d=\"M 2.984375 54.6875 \nL 12.5 54.6875 \nL 29.59375 8.796875 \nL 46.6875 54.6875 \nL 56.203125 54.6875 \nL 35.6875 0 \nL 23.484375 0 \nz\n\" id=\"DejaVuSans-118\"/>\n <path d=\"M 30.609375 48.390625 \nQ 23.390625 48.390625 19.1875 42.75 \nQ 14.984375 37.109375 14.984375 27.296875 \nQ 14.984375 17.484375 19.15625 11.84375 \nQ 23.34375 6.203125 30.609375 6.203125 \nQ 37.796875 6.203125 41.984375 11.859375 \nQ 46.1875 17.53125 46.1875 27.296875 \nQ 46.1875 37.015625 41.984375 42.703125 \nQ 37.796875 48.390625 30.609375 48.390625 \nz\nM 30.609375 56 \nQ 42.328125 56 49.015625 48.375 \nQ 55.71875 40.765625 55.71875 27.296875 \nQ 55.71875 13.875 49.015625 6.21875 \nQ 42.328125 -1.421875 30.609375 -1.421875 \nQ 18.84375 -1.421875 12.171875 6.21875 \nQ 5.515625 13.875 5.515625 27.296875 \nQ 5.515625 40.765625 12.171875 48.375 \nQ 18.84375 56 30.609375 56 \nz\n\" id=\"DejaVuSans-111\"/>\n <path d=\"M 9.421875 75.984375 \nL 18.40625 75.984375 \nL 18.40625 0 \nL 9.421875 0 \nz\n\" id=\"DejaVuSans-108\"/>\n <path d=\"M 34.28125 27.484375 \nQ 23.390625 27.484375 19.1875 25 \nQ 14.984375 22.515625 14.984375 16.5 \nQ 14.984375 11.71875 18.140625 8.90625 \nQ 21.296875 6.109375 26.703125 6.109375 \nQ 34.1875 6.109375 38.703125 11.40625 \nQ 43.21875 16.703125 43.21875 25.484375 \nL 43.21875 27.484375 \nz\nM 52.203125 31.203125 \nL 52.203125 0 \nL 43.21875 0 \nL 43.21875 8.296875 \nQ 40.140625 3.328125 35.546875 0.953125 \nQ 30.953125 -1.421875 24.3125 -1.421875 \nQ 15.921875 -1.421875 10.953125 3.296875 \nQ 6 8.015625 6 15.921875 \nQ 6 25.140625 12.171875 29.828125 \nQ 18.359375 34.515625 30.609375 34.515625 \nL 43.21875 34.515625 \nL 43.21875 35.40625 \nQ 43.21875 41.609375 39.140625 45 \nQ 35.0625 48.390625 27.6875 48.390625 \nQ 23 48.390625 18.546875 47.265625 \nQ 14.109375 46.140625 10.015625 43.890625 \nL 10.015625 52.203125 \nQ 14.9375 54.109375 19.578125 55.046875 \nQ 24.21875 56 28.609375 56 \nQ 40.484375 56 46.34375 49.84375 \nQ 52.203125 43.703125 52.203125 31.203125 \nz\n\" id=\"DejaVuSans-97\"/>\n <path d=\"M 45.40625 27.984375 \nQ 45.40625 37.75 41.375 43.109375 \nQ 37.359375 48.484375 30.078125 48.484375 \nQ 22.859375 48.484375 18.828125 43.109375 \nQ 14.796875 37.75 14.796875 27.984375 \nQ 14.796875 18.265625 18.828125 12.890625 \nQ 22.859375 7.515625 30.078125 7.515625 \nQ 37.359375 7.515625 41.375 12.890625 \nQ 45.40625 18.265625 45.40625 27.984375 \nz\nM 54.390625 6.78125 \nQ 54.390625 -7.171875 48.1875 -13.984375 \nQ 42 -20.796875 29.203125 -20.796875 \nQ 24.46875 -20.796875 20.265625 -20.09375 \nQ 16.0625 -19.390625 12.109375 -17.921875 \nL 12.109375 -9.1875 \nQ 16.0625 -11.328125 19.921875 -12.34375 \nQ 23.78125 -13.375 27.78125 -13.375 \nQ 36.625 -13.375 41.015625 -8.765625 \nQ 45.40625 -4.15625 45.40625 5.171875 \nL 45.40625 9.625 \nQ 42.625 4.78125 38.28125 2.390625 \nQ 33.9375 0 27.875 0 \nQ 17.828125 0 11.671875 7.65625 \nQ 5.515625 15.328125 5.515625 27.984375 \nQ 5.515625 40.671875 11.671875 48.328125 \nQ 17.828125 56 27.875 56 \nQ 33.9375 56 38.28125 53.609375 \nQ 42.625 51.21875 45.40625 46.390625 \nL 45.40625 54.6875 \nL 54.390625 54.6875 \nz\n\" id=\"DejaVuSans-103\"/>\n <path d=\"M 28.609375 0 \nL 0.78125 72.90625 \nL 11.078125 72.90625 \nL 34.1875 11.53125 \nL 57.328125 72.90625 \nL 67.578125 72.90625 \nL 39.796875 0 \nz\n\" id=\"DejaVuSans-86\"/>\n </defs>\n <g transform=\"translate(14.798438 163.502187)rotate(-90)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-118\"/>\n <use x=\"59.179688\" xlink:href=\"#DejaVuSans-111\"/>\n <use x=\"120.361328\" xlink:href=\"#DejaVuSans-108\"/>\n <use x=\"148.144531\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"187.353516\" xlink:href=\"#DejaVuSans-97\"/>\n <use x=\"248.632812\" xlink:href=\"#DejaVuSans-103\"/>\n <use x=\"312.109375\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"373.632812\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"405.419922\" xlink:href=\"#DejaVuSans-40\"/>\n <use x=\"444.433594\" xlink:href=\"#DejaVuSans-109\"/>\n <use x=\"541.845703\" xlink:href=\"#DejaVuSans-86\"/>\n <use x=\"610.253906\" xlink:href=\"#DejaVuSans-41\"/>\n </g>\n </g>\n </g>\n <g id=\"line2d_37\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 65.361932 131.038125 \nL 71.479794 106.458521 \nL 76.06819 88.955648 \nL 79.127121 78.078953 \nL 82.186052 68.037456 \nL 85.244983 58.989517 \nL 88.303914 51.077827 \nL 89.83338 47.587823 \nL 91.362845 44.427159 \nL 92.892311 41.608309 \nL 94.421776 39.142398 \nL 95.951242 37.039157 \nL 97.480708 35.306887 \nL 99.010173 33.952425 \nL 100.539639 32.981116 \nL 102.069104 32.396792 \nL 103.59857 32.201761 \nL 105.128035 32.396792 \nL 106.657501 32.981116 \nL 108.186966 33.952425 \nL 109.716432 35.306887 \nL 111.245897 37.039157 \nL 112.775363 39.142398 \nL 114.304828 41.608309 \nL 115.834294 44.427159 \nL 117.363759 47.587823 \nL 118.893225 51.077827 \nL 120.42269 54.883398 \nL 123.481621 63.379978 \nL 126.540552 72.943568 \nL 129.599483 83.423344 \nL 132.658414 94.654033 \nL 137.246811 112.518037 \nL 151.012 167.422217 \nL 154.070931 178.652906 \nL 157.129862 189.132682 \nL 160.188793 198.696272 \nL 163.247724 207.192852 \nL 164.77719 210.998423 \nL 166.306655 214.488427 \nL 167.836121 217.649091 \nL 169.365586 220.467941 \nL 170.895052 222.933852 \nL 172.424517 225.037093 \nL 173.953983 226.769363 \nL 175.483448 228.123825 \nL 177.012914 229.095134 \nL 178.54238 229.679458 \nL 180.071845 229.874489 \nL 181.601311 229.679458 \nL 183.130776 229.095134 \nL 184.660242 228.123825 \nL 186.189707 226.769363 \nL 187.719173 225.037093 \nL 189.248638 222.933852 \nL 190.778104 220.467941 \nL 192.307569 217.649091 \nL 193.837035 214.488427 \nL 195.3665 210.998423 \nL 196.895966 207.192852 \nL 199.954897 198.696272 \nL 203.013828 189.132682 \nL 206.072759 178.652906 \nL 209.13169 167.422217 \nL 213.720086 149.558213 \nL 227.485276 94.654033 \nL 230.544207 83.423344 \nL 233.603138 72.943568 \nL 236.662069 63.379978 \nL 239.721 54.883398 \nL 241.250465 51.077827 \nL 242.779931 47.587823 \nL 244.309396 44.427159 \nL 245.838862 41.608309 \nL 247.368327 39.142398 \nL 248.897793 37.039157 \nL 250.427258 35.306887 \nL 251.956724 33.952425 \nL 253.486189 32.981116 \nL 255.015655 32.396792 \nL 256.54512 32.201761 \nL 258.074586 32.396792 \nL 259.604052 32.981116 \nL 261.133517 33.952425 \nL 262.662983 35.306887 \nL 264.192448 37.039157 \nL 265.721914 39.142398 \nL 267.251379 41.608309 \nL 268.780845 44.427159 \nL 270.31031 47.587823 \nL 271.839776 51.077827 \nL 273.369241 54.883398 \nL 276.428172 63.379978 \nL 279.487103 72.943568 \nL 282.546034 83.423344 \nL 285.604965 94.654033 \nL 290.193362 112.518037 \nL 303.958551 167.422217 \nL 307.017482 178.652906 \nL 310.076413 189.132682 \nL 313.135344 198.696272 \nL 316.194275 207.192852 \nL 317.723741 210.998423 \nL 319.253206 214.488427 \nL 320.782672 217.649091 \nL 322.312137 220.467941 \nL 323.841603 222.933852 \nL 325.371068 225.037093 \nL 326.900534 226.769363 \nL 328.429999 228.123825 \nL 329.959465 229.095134 \nL 331.48893 229.679458 \nL 333.018396 229.874489 \nL 334.547861 229.679458 \nL 336.077327 229.095134 \nL 337.606792 228.123825 \nL 339.136258 226.769363 \nL 340.665724 225.037093 \nL 342.195189 222.933852 \nL 343.724655 220.467941 \nL 345.25412 217.649091 \nL 346.783586 214.488427 \nL 348.313051 210.998423 \nL 349.842517 207.192852 \nL 352.901448 198.696272 \nL 355.960379 189.132682 \nL 359.01931 178.652906 \nL 362.078241 167.422217 \nL 366.666637 149.558213 \nL 369.725568 137.244112 \nL 369.725568 137.244112 \n\" style=\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/>\n </g>\n <g id=\"patch_3\">\n <path d=\"M 50.14375 239.758125 \nL 50.14375 22.318125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_4\">\n <path d=\"M 384.94375 239.758125 \nL 384.94375 22.318125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_5\">\n <path d=\"M 50.14375 239.758125 \nL 384.94375 239.758125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_6\">\n <path d=\"M 50.14375 22.318125 \nL 384.94375 22.318125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"text_21\">\n <!-- About as simple as it gets, folks -->\n <defs>\n <path d=\"M 34.1875 63.1875 \nL 20.796875 26.90625 \nL 47.609375 26.90625 \nz\nM 28.609375 72.90625 \nL 39.796875 72.90625 \nL 67.578125 0 \nL 57.328125 0 \nL 50.6875 18.703125 \nL 17.828125 18.703125 \nL 11.1875 0 \nL 0.78125 0 \nz\n\" id=\"DejaVuSans-65\"/>\n <path d=\"M 48.6875 27.296875 \nQ 48.6875 37.203125 44.609375 42.84375 \nQ 40.53125 48.484375 33.40625 48.484375 \nQ 26.265625 48.484375 22.1875 42.84375 \nQ 18.109375 37.203125 18.109375 27.296875 \nQ 18.109375 17.390625 22.1875 11.75 \nQ 26.265625 6.109375 33.40625 6.109375 \nQ 40.53125 6.109375 44.609375 11.75 \nQ 48.6875 17.390625 48.6875 27.296875 \nz\nM 18.109375 46.390625 \nQ 20.953125 51.265625 25.265625 53.625 \nQ 29.59375 56 35.59375 56 \nQ 45.5625 56 51.78125 48.09375 \nQ 58.015625 40.1875 58.015625 27.296875 \nQ 58.015625 14.40625 51.78125 6.484375 \nQ 45.5625 -1.421875 35.59375 -1.421875 \nQ 29.59375 -1.421875 25.265625 0.953125 \nQ 20.953125 3.328125 18.109375 8.203125 \nL 18.109375 0 \nL 9.078125 0 \nL 9.078125 75.984375 \nL 18.109375 75.984375 \nz\n\" id=\"DejaVuSans-98\"/>\n <path d=\"M 8.5 21.578125 \nL 8.5 54.6875 \nL 17.484375 54.6875 \nL 17.484375 21.921875 \nQ 17.484375 14.15625 20.5 10.265625 \nQ 23.53125 6.390625 29.59375 6.390625 \nQ 36.859375 6.390625 41.078125 11.03125 \nQ 45.3125 15.671875 45.3125 23.6875 \nL 45.3125 54.6875 \nL 54.296875 54.6875 \nL 54.296875 0 \nL 45.3125 0 \nL 45.3125 8.40625 \nQ 42.046875 3.421875 37.71875 1 \nQ 33.40625 -1.421875 27.6875 -1.421875 \nQ 18.265625 -1.421875 13.375 4.4375 \nQ 8.5 10.296875 8.5 21.578125 \nz\nM 31.109375 56 \nz\n\" id=\"DejaVuSans-117\"/>\n <path d=\"M 18.109375 8.203125 \nL 18.109375 -20.796875 \nL 9.078125 -20.796875 \nL 9.078125 54.6875 \nL 18.109375 54.6875 \nL 18.109375 46.390625 \nQ 20.953125 51.265625 25.265625 53.625 \nQ 29.59375 56 35.59375 56 \nQ 45.5625 56 51.78125 48.09375 \nQ 58.015625 40.1875 58.015625 27.296875 \nQ 58.015625 14.40625 51.78125 6.484375 \nQ 45.5625 -1.421875 35.59375 -1.421875 \nQ 29.59375 -1.421875 25.265625 0.953125 \nQ 20.953125 3.328125 18.109375 8.203125 \nz\nM 48.6875 27.296875 \nQ 48.6875 37.203125 44.609375 42.84375 \nQ 40.53125 48.484375 33.40625 48.484375 \nQ 26.265625 48.484375 22.1875 42.84375 \nQ 18.109375 37.203125 18.109375 27.296875 \nQ 18.109375 17.390625 22.1875 11.75 \nQ 26.265625 6.109375 33.40625 6.109375 \nQ 40.53125 6.109375 44.609375 11.75 \nQ 48.6875 17.390625 48.6875 27.296875 \nz\n\" id=\"DejaVuSans-112\"/>\n <path d=\"M 11.71875 12.40625 \nL 22.015625 12.40625 \nL 22.015625 4 \nL 14.015625 -11.625 \nL 7.71875 -11.625 \nL 11.71875 4 \nz\n\" id=\"DejaVuSans-44\"/>\n <path d=\"M 37.109375 75.984375 \nL 37.109375 68.5 \nL 28.515625 68.5 \nQ 23.6875 68.5 21.796875 66.546875 \nQ 19.921875 64.59375 19.921875 59.515625 \nL 19.921875 54.6875 \nL 34.71875 54.6875 \nL 34.71875 47.703125 \nL 19.921875 47.703125 \nL 19.921875 0 \nL 10.890625 0 \nL 10.890625 47.703125 \nL 2.296875 47.703125 \nL 2.296875 54.6875 \nL 10.890625 54.6875 \nL 10.890625 58.5 \nQ 10.890625 67.625 15.140625 71.796875 \nQ 19.390625 75.984375 28.609375 75.984375 \nz\n\" id=\"DejaVuSans-102\"/>\n <path d=\"M 9.078125 75.984375 \nL 18.109375 75.984375 \nL 18.109375 31.109375 \nL 44.921875 54.6875 \nL 56.390625 54.6875 \nL 27.390625 29.109375 \nL 57.625 0 \nL 45.90625 0 \nL 18.109375 26.703125 \nL 18.109375 0 \nL 9.078125 0 \nz\n\" id=\"DejaVuSans-107\"/>\n </defs>\n <g transform=\"translate(121.998438 16.318125)scale(0.12 -0.12)\">\n <use xlink:href=\"#DejaVuSans-65\"/>\n <use x=\"68.408203\" xlink:href=\"#DejaVuSans-98\"/>\n <use x=\"131.884766\" xlink:href=\"#DejaVuSans-111\"/>\n <use x=\"193.066406\" xlink:href=\"#DejaVuSans-117\"/>\n <use x=\"256.445312\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"295.654297\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"327.441406\" xlink:href=\"#DejaVuSans-97\"/>\n <use x=\"388.720703\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"440.820312\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"472.607422\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"524.707031\" xlink:href=\"#DejaVuSans-105\"/>\n <use x=\"552.490234\" xlink:href=\"#DejaVuSans-109\"/>\n <use x=\"649.902344\" xlink:href=\"#DejaVuSans-112\"/>\n <use x=\"713.378906\" xlink:href=\"#DejaVuSans-108\"/>\n <use x=\"741.162109\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"802.685547\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"834.472656\" xlink:href=\"#DejaVuSans-97\"/>\n <use x=\"895.751953\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"947.851562\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"979.638672\" xlink:href=\"#DejaVuSans-105\"/>\n <use x=\"1007.421875\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"1046.630859\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"1078.417969\" xlink:href=\"#DejaVuSans-103\"/>\n <use x=\"1141.894531\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"1203.417969\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"1242.626953\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"1294.726562\" xlink:href=\"#DejaVuSans-44\"/>\n <use x=\"1326.513672\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"1358.300781\" xlink:href=\"#DejaVuSans-102\"/>\n <use x=\"1393.505859\" xlink:href=\"#DejaVuSans-111\"/>\n <use x=\"1454.6875\" xlink:href=\"#DejaVuSans-108\"/>\n <use x=\"1482.470703\" xlink:href=\"#DejaVuSans-107\"/>\n <use x=\"1540.380859\" xlink:href=\"#DejaVuSans-115\"/>\n </g>\n </g>\n </g>\n </g>\n <defs>\n <clipPath id=\"p2444f92044\">\n <rect height=\"217.44\" width=\"334.8\" x=\"50.14375\" y=\"22.318125\"/>\n </clipPath>\n </defs>\n</svg>\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYgAAAEWCAYAAAB8LwAVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nOydeXxU1fn/30/2lewJECAhIYCgCAbZFAW1gtVW29pWa1vtZq312721/fZX29rtu7bf9ms396+tSq1Vq9alLgRBCJuAsmcDwpoNyEaWSZ7fH/eOjjHLJJk7d2Zy3q/Xfc3MXc75zJ0z88x5nnPOI6qKwWAwGAx9iXJbgMFgMBhCE2MgDAaDwdAvxkAYDAaDoV+MgTAYDAZDvxgDYTAYDIZ+MQbCYDAYDP1iDIThbUTkQRH5qds6nEBEbhCRfzpUtqv3TUSWisg+t+ofKWLxgIicFJFNfpyvIjLNfh6xbTWUMAZiDCIiZfaXMj5I9RXaX+6YYNTXH6r6sKpe7lb9TqKqa1V1hve1iBwQkcucqEtElonI4QAVdyHwPmCSqi4IUJmGAGIMxBhDRAqBpYACH3RVjGGsUwAcUNU2t4UY+scYiLHHp4Fy4EHgxn6OZ4vISyLSIiJrRKTAe0BElojIZhE5bT8u8Tn2rn+tIvIjEfmz/fI1+/GUiLSKyOK+lYrIAhHZICKnROSYiNwlInH2MRGRX4lInYg0i8hbInJ2f29ORG4SkWpbf42I3OCzf53PeSoit4pIhX3uT0SkWETW23U85lP/MhE5LCL/KiIN9nu9YaAbLCJXich2+72sF5E5g5z7axGptevcKiJL+9yTLfaxEyLyywHKePtfvYj8CZgCPGPf6+8McM137Pt8VEQ+38d9Ey8i/yUih+x6/yAiiSKSDDwPTLTLbhWRif7q7FP/54B7gcV2OT+2939BRCpFpElEnhaRiX6UlSoiq0XkN3Zbeb+I7LY/1yMi8q2hyjAMgKqabQxtQCVwK1AKdAN5PsceBFqAi4B44NfAOvtYJnAS+BQQA1xvv86yjx8ALvMp60fAn+3nhVg9lphBdJUCi+yyC4E9wNfsYyuArUA6IMBZwIR+ykgGmoEZ9usJwGz7+U3e92K/VuDvwDhgNtAJvAIUAWnAbuBG+9xlgAf4pX1fLgbafOp5EPip/XweUAcsBKKxjPABIH6A9/1JIMt+398EjgMJ9rENwKfs5ynAogHKWAYc9nn9rs+in/NX2vXMBpKAP9v3Y5p9/FfA0/Znngo8A/yiv7qGo7MfHX0/k0uABuA8+z7/L/Ban8/Mq/FB4Kf2vdvkvf/2sWPAUvt5BnCe29+7cN1MD2IMISIXYnXrH1PVrUAV8Ik+p/1DVV9T1U7g+1j/8CYDVwIVqvonVfWo6qPAXuADgdCmqltVtdwu+wDwR6wfYrAMWSowExBV3aOqxwYoqhc4W0QSVfWYqu4apNr/UNVm+5ydwD9VtVpVT2P9U57X5/wfqGqnqq4B/gF8rJ8ybwb+qKobVbVHVf8Py/gsGuB9/1lVG+33/d9YP4zeeEI3ME1EslW1VVXLB3kvw+FjwAOquktV27GMOWD11uz38HVVbVLVFuDnwHWDlBconTcA96vqG3b7+x5W+ysc4PyJwBrgr6r6//romSUi41T1pKq+MUI9Yx5jIMYWN2L9CDbYrx/hvW6mWu8TVW0FmrC+iBOBg33OPQjkB0KYiEwXkWdF5LiINGP9KGXbOl4F7gJ+C9SJyN0iMq5vGWr5sj8O3AIcE5F/iMjMQao94fP8TD+vU3xen9R3+8oPYt2TvhQA37TdS6dE5BQweYBzEZFvicgesdx2p7B6L9n24c8B04G9Yrn0rhrkvQyHifh8zn2e52D1Krb66H/B3j8QgdL5rjZmt79GBm5jVwKJwB/67P8I8H7goFhu0ve4NA3+YQzEGEFEErH+OV5s/wgfB74OnCsi5/qcOtnnmhQsN8NReyvg3UwBjtjP27B+WLyM93nuz5LBv8fqkZSo6jjgX7HcSVYBqr9R1VJgFtaP0bf7K0RVX1TV92G5l/YC9/hRtz9k2D54L1Ow7klfaoGfqWq6z5Zk97jehR1v+A7W55KhqunAaez3raoVqno9kAv8O/B4Hw0DMdT9PgZM8nk92ed5A5ZxnO2jP01VvcbyPWWPQmdf3tXG7DKyeKeN9eUeLOP1nG99qrpZVa+29TwFPDYCLQaMgRhLXAP0YP3AzrW3s4C1WIFrL+8XkQvtAO1PgHJVrQWeA6aLyCdEJEZEPm6X9ax93XbgOhGJFZH5wLU+ZdZjuX6KBtGXihU/aLX/9X/Je0BEzheRhSISi2WIOuzy3oWI5InI1faPRSfQ2t95o+DHIhJn/7BfBfy1n3PuAW6x9YqIJIvIlSKS2s+5qVixjXogRkTuwIqJeN/PJ0UkR1V7gVP2bn/ezwkGv9ePAZ8RkbNEJAn4gfeAXdc9wK9EJNfWkS8iK3zKzhKRNH90ihXQv8kPzQCP2rrmijUE++fARtvlOBC3AfuwgvKJ9udzg4ikqWo3VpsKZBsYUxgDMXa4EcvvfEhVj3s3LNfNDfLOHIVHgB9iuZZKsYKoqGoj1o/iN7G6/d8BrvJxV/0AKMYKXP/YLgf72nbgZ8DrttuiP3/8t7DiIS1YP1B/8Tk2zt53EssF0Qj8Zz9lRAHfwPon2oQVw/hSP+eNhON2/UeBh4FbVHVv35NUdQvwBaz7ehJrUMBNA5T5ItY/4P1Y76uDd7t7VgK7RKQVa8DAdap6xg+tvwD+n32v3zOCR1WfB34DrLb1eWMGnfbj7d79trvvZey4iP2eHwWq7fInDqTT/pOR5VP+oKjqy1jt6G9YvZxiBo99oKqKFTM5jDXoIAFrIMUBW/stWLENwwgQ6/4aDIaBEJFlWCOyJg11bjgiImdhBenjVdUTwHIvBL5su58MYYjpQRgMYxAR+ZBY8x0ysOIGzwTSOACo6jpjHMIbYyAMhrHJF7Hma1RhxaYC5YozRBDGxWQwGAyGfjE9CIPBYDD0i2urazpBdna2FhYWjujatrY2kpNHMnTbWYyu4ROq2oyu4WF0DZ+RaNu6dWuDqvY/ETLYa3s4uZWWlupIWb169YivdRKja/iEqjaja3gYXcNnJNqALWrWYjIYDAbDcDAGwmAwGAz9YgyEwWAwGPrFGAiDwWAw9IsxEAaDwWDoF8cMhIhMttMA7haRXSLy1X7OETtNYKWIvCki5/kcu1GsdJAVItJfakyDwWAwOIiT8yA8wDdV9Q17qeOtIvKSqu72OecKoMTeFmLlBFgoIplYK4rOx1p/fquIPK2qJx3UazAYDAYfHDMQaqWEPGY/bxGRPViZoXwNxNXAQ/ZY3HIRSReRCVh5b19S1SYAEXkJa0nh9yRdiUTauzy8XtnIwcY2Kmq66cg+zoUl2aTER9S8RoOLtHZ6WFfRwKGmNqpquunKsdpYUpxpY4Z3CMpaTHZO2deAs1W12Wf/s8C/qeo6+/UrWGvRL8NK3P5Te/8PgDOq+l/9lH0z1nrw5OXlla5atWpEGltbW0lJSRn6RAc541GequxiTa2Hjp53H4uLhgvzY/hISRzJsdJ/AUEkFO7XQISqtlDQ1datPFHRxdojHrr6tLGEaLh4UgzXlMSRGGPa2ECEqi4Ymbbly5dvVdX5/R1z/O+Cnbbyb8DXfI1DoFDVu4G7AebPn6/Lli0bUTllZWWM9NpAsO3QSb738Bscb/Zwzdx8Pjp/ErMnpLHu9XXkTDuXv209zONvHObNph5+c/1clhRnD12og7h9vwYjVLW5rWtDVSPfenQbJ9t7uPa8yXykdBIzxqeybt06MorO4fEth3ly+xHePBXDb284j/OmZLimFdy/XwMRqrog8NocHcVkp4j8G/Cwqj7RzylHeHc+3En2voH2RyQv7z7B9feUExsdxd++tIRffdwyAGlJsSTHCgumZvLv187h71++gIykWG68fxNP7+gvHbLB0D/P7DjKjfdvIj0plr9/+QL+/do5LJiaSVqi1caWFGfzy4/P5YkvLSEuJopP3FPOy7tPuC3b4DJOjmIS4D5gj6r+coDTngY+bY9mWgSctmMXLwKXi0iGndDkcntfxLG+qoEvPbyVGXmpPHHrkkH/tZ2dn8bjtyxh3pQMvrZqG6/sMV9gw9C8sucEX121jbmT0/nbLUs4Oz9twHPnTcngiS8tYcb4cXzp4a2sr2oY8FxD5ONkD+ICrNywl4jIdnt7v4jcIiK32Oc8B1Rj5b+9B7gVwA5O/wTYbG93egPWkURlXQtf/NNWCrOSeeizC8lOiR/ymrSkWB646XxmT0zjtke2sfPI6SAoNYQrO4+c5rZHtjF7YhoPfOZ80pJih7wmKyWehz67gKnZyXzxoa1UnGgJglJDKOKYgVAr3aCo6hxVnWtvz6nqH1T1D/Y5qqpfVtViVT1HrYTv3uvvV9Vp9vaAUzrdoqO7h9se2UZcdBQPfnaBX19cL8nxMdx/0/lkJMXy5UfeoLUzoJkiDRFCa6eH2x55g/SkWO6/6XyShzEKLi0xlgc/s4D42Chue2QbHd09Q19kiDjMTGqX+Plze9h7vIX/+ti55KcnDvv6nNR4/ue6edQ2tXPHUzsdUGgId+54aieHmtr59XXzyEkdunfal4npifzXR89l34kWfvaPPQ4oNIQ6xkC4wOYDTTy04SCfvWAqy2fkjricBVMzue2SEp7YdoTV++oCqNAQ7qzeV8cT245w2yUlLJiaOeJyls3I5XMXTuVP5QfZVBNxXl7DEBgDEWS6PL18/8m3yE9P5Fsrpo+6vC8vL6Y4J5kfPLWTM30HthvGJGe6erjj7zspzknmy8uLR13eNy+fTn56It9/8i26PL0BUGgIF4yBCDL/t/4A+0+08uMPzg7IrNX4mGh+es05HD55ht+XVQZAoSHc+cOaKmqbzvDTa84hPiZ61OUlxcVw59Wzqahr5cH1NQFQaAgXjIEIIqfbu7lrdSUXT8/hsll5ASt3cXEWV86ZwD1ra6hr7ghYuYbwo66lg3vWVnPlnAksLs4KWLmXnpXHshk5/HZ1FafbuwNWriG0MQYiiPyurJLmjm6+e8XMgJf97ctn0N3Ty/+8UhHwsg3hw69frqDL08u3L58R8LJvXzmT5o5ufmd6qmMGYyCCRF1LBw+uP8CH5uZz1oRxAS+/MDuZGxZO4S+baznU2B7w8g2hT21TO6s21/KJhVMozE4OePlnTRjHh+bl88D6A6anOkYwBiJI3Lu2hu6eXr5yaYljddy6fBrRIvx+TZVjdRhCl9+vqSJahC8vn+ZYHV+5pARPTy/3rjOxiLGAMRBB4GRbF38uP8gHzp3oyD87L3njEvjo/Ek8vrWWY6fPOFaPIfQ4frqDx7cc5qPzJ5E3LsGxegqzk/nAuRP5c/lBTrZ1OVaPITQwBiIIPLj+AO1dPY7+s/Nyy8XFqFo9FsPY4d611fSocsvFox/WOhRfXj6N9q4eHlx/wPG6DO5iDITDdHT38PDGg1w6M5fpeamO1zc5M4kr50zgL5traekwo03GAq2dHv6yuZar5kxgcmaS4/VNz0vl0pm5PLzxoFmCI8IxBsJhntlxlIbWLj574dSg1fmZC6bS2unhr1sOB61Og3v8dUstLZ0ePnNB8NrYZy+cSkNrl1l2PsIxBsJBVJX7Xz/AjLxUlgRwTPpQzJ2cTmlBBg+uP0BPr/MZAw3u0dOrPLj+AKUFGcydnB60epcUZzEjL5X719UQjKyUBncwBsJBttWeYs+xZm5cUoiVHiN4fOaCQg41tfNaRX1Q6zUEl9cq6jnY2M6NSwqDWq+I8JkLCtl7vIWtB08GtW5D8DAGwkEe21xLYmw0Hzh3QtDrvnzWeLJT4nhk46Gg120IHo9uPERWchwrZ48Pet0fOHciKfExpo1FMMZAOERbp4dndhzlyjkTSE3wP9dDoIiLieLa0sm8ureO46fNpKZI5ERzB6/sreOj8ycTFxP8r3JyfAzXzJvIs28d41S7GfIaiTiZcvR+EakTkX6TFYjIt30yze0UkR4RybSPHRCRt+xjW/q7PtT5x1vHaOvq4ePnTx76ZIe4fsFkenqVx7bUuqbB4ByPba6lp1e5foF7bewTCwro8vTyxBsRmzJ+TOPk344HgZUDHVTV//RmmgO+B6zpk1Z0uX18voMaHeOxzbUU5SQzv2DgHNNOU5CVzJLiLJ5447AJJEYYqsoT246wuCiLgiznJl8OxayJ45gzKY0ntpkRc5GIkylHXwP8zTByPfCoU1qCTWVdK1sOnuRj8ycHPTjdl2vm5XOgsZ3ttadc1WEILDsOn6amoY0PnZfvthSumZvPziPNJnd1BCJO/rMUkULgWVU9e5BzkoDDwDRvD0JEaoCTgAJ/VNW7B7n+ZuBmgLy8vNJVq1aNSGtrayspKSkjurYvf9nXxYsHuvnlskTS40dng0erq71b+erqdi6aFMOnZg0/7aRTupwkVLUFUtefdnfy2mEPv16eRFLs6P6EjFbX6U7l62XtvH9qLNdOjxuVlkDqcopQ1QUj07Z8+fKtA3pqVNWxDSgEdg5xzseBZ/rsy7cfc4EdwEX+1FdaWqojZfXq1SO+1pcuT4+W/uQl/fz/bQ5IeYHQdevDW3Xuj1/ULk/P6AXZBOp+OUGoagtkG5t35z/11oe3BqS8QOj69H0bdckvXtGent7RC7KJ9M/RCUaiDdiiA/ymhsIopuvo415S1SP2Yx3wJLDABV0jYkNVIw2tnVxbOsltKW/z4Xn5nGzv5rX9Zk5EJLC2op6mti4+NNd995KXD5+Xz5FTZ9h8wOStjiRcNRAikgZcDPzdZ1+yiKR6nwOXA/2OhApFnn3zKKnxMVw8PcdtKW9z0fQcMpJieXKbGWkSCTy57SgZSbFcFEJt7H2z8kiKi+ap7aaNRRJODnN9FNgAzBCRwyLyORG5RURu8TntQ8A/VbXNZ18esE5EdgCbgH+o6gtO6QwkXZ5eXth5nPfNyiMhdvS5gANFbHQUHzh3Ii/tPkGzWcAvrGnp6Oafu45z1ZyJrsx9GIikuBhWzh7Ps28eMwv4RRBOjmK6XlUnqGqsqk5S1ftU9Q+q+gefcx5U1ev6XFetqufa22xV/ZlTGgPN65UNNHd4uHJO8GdOD8U18/LptA2YIXx5YedxOj29XDMvdNxLXq6Zl09Lh4fVe+vclmIIEKHzFyQCeObNo4xLiGFpSeh0/b3Mm5xOQVYSz5jVN8Oap3ccZUpmEudNCd7CfP6ypDiLnNR4s8JrBGEMRIDo6O7hpV0nWDF7fEh1/b2ICFecPYENVY2cbjdupnDkdHs3G6oaueKc8a7Pr+mPmOgoVszOo2xfPWe6jJspEgi9X7IwZW1FAy2doele8rLy7PF4epVX9p5wW4phBLyy9wSeXnVlYT5/WTl7Ame6e8wqwhGCMRAB4tk3j5KeFMsF07LdljIgc/LTmJCWYOIQYcqLu44zflwC504KPfeSl4VFmaQlxvLiLtPGIgFjIAJAR3cPL+8+wcrZ44mNDt1bGhUlrJg9njX762nv8rgtxzAM2rs8rNlfz4rZeURFhZ57yUtsdBSXnZXHy7tP0N3T67YcwygJ3V+zMOL1ygbaunq44pzQdS95uXx2Hp2eXtbsMy6AcOK1/fV0dPey4uzQdS95WTE7j+YOD+XVjW5LMYwSYyACwMt7TpASH8Oioky3pQzJgsJMMpJiecG4AMKKF3YeJyMplgWFod/GLpqeQ2JstHFlRgDGQIyS3l7l5T11XDw9h/iY0JkcNxAx0VG8b1Yer+6po8tjXADhQJenl1f21PG+WXnEhLAL00tCbDTLZ+bw4q4TJid6mBP6rS3EefPIaepbOrlsVq7bUvxm5dnjaen0sL6qwW0pBj9YX2WNkFsZBu4lLytmj6ehtZNth0y+6nDGGIhR8vLuE0RHCctnhI+BWFKcTUp8jBlpEia8uOs4KfExLCkO3RFyfblkZi5x0VHGzRTmGAMxSl7ec4LzCzNITwrcOvhOkxAbzcXTc3hlT53JNBfiqCqv2C7MUFrfayhSE2JZVJzFK2bZjbDGGIhRUNvUzt7jLVx2Vp7bUobN8pm51LV0sutos9tSDIOw62gzdS2dLJ8ZPj1UL5fMyKGmoY2ahrahTzaEJMZAjIKX91gzkt83K/wMxLIZOYhgFlYLcbyfz7IZobe+11BcMtP6Xpg2Fr4YAzEKXt5zgpLcFFeTxo+U7JR45kxK59V95ssbyry6r45zJ6WRnRK4dLHBYkpWEsU5yaw2bSxsMQZihDR3dLOxuonLwrD34GX5jBy2156isbXTbSmGfmhq62J77amwdC95WT4jl43VTbR1mpn74YgxECNkXUUDnl7l0jD+8l4yMxdVWGNSkYYka/bXoUpYjZDryyUzc+nq6eX1SjOkOhxxMqPc/SJSJyL9pgsVkWUiclpEttvbHT7HVorIPhGpFJHvOqVxNKzZV8+4hBjmTg7dhdOG4uyJluviVeMjDkle3VtPdko85+SnuS1lxMwvzCQlPsa4mcIUJ3sQDwIrhzhnrarOtbc7AUQkGvgtcAUwC7heRGY5qHPYqCpr9teztCQnLGa2DkRUlLBsRg6v7a/HYxZWCyk8Pb2s2VfHshk5Ib0431DExURx4bRsVu+tN0OqwxAnU46+BjSN4NIFQKWderQLWAVcHVBxo2T/iVaON3dwcQgljR8pl8zMpbnDwxuHTrktxeDDttpTNHd4wtq95OWSmbkcb+5gz7EWt6UYhkmMy/UvFpEdwFHgW6q6C8gHan3OOQwsHKgAEbkZuBkgLy+PsrKyEQlpbW31+9rna6yMbLGNFZSVVY2oPn8Zjq4R0a1ECzz4zy20z/B/sp/jukZBqGobjq6/7usiWkBO7KWsbF/I6BoJcZ1W7/S+58v5QHH4t7FQ1QUOaFNVxzagENg5wLFxQIr9/P1Ahf38WuBen/M+BdzlT32lpaU6UlavXu33uZ+4Z4Ou+NWaEdc1HIaja6Rc98fhv59g6BopoaptOLpW/s9r+rE/rHdOjA/BuF9X/WatfuR3rw/rmkj4HIPNSLQBW3SA31TXHOiq2qyqrfbz54BYEckGjgCTfU6dZO8LCdo6PWyuOclFEeBe8rJ0ejZ7j7dQ19zhthQDUNfSwZ5jzZHVxkqy2VZ7ipYOkw89nHDNQIjIeLEzr4vIAltLI7AZKBGRqSISB1wHPO2Wzr6UVzfS1dMbEfEHLxeVWO9lnRmKGBJ4h4R6P5dIYGlJDj29yoYqk0QonHBymOujwAZghogcFpHPicgtInKLfcq1wE47BvEb4Dq7x+MBbgNeBPYAj6kVmwgJ1uyvJzE2mvmFGW5LCRizJowjMzmOdRXGQIQCaysayEiKZfbEcW5LCRjnFaSTFBdt/oSEGY4FqVX1+iGO3wXcNcCx54DnnNA1Wtbsr2dJcVZYJAfyl6go4YJp2bxW0YCqYnfsDC6gqqytaODCkvAe3tqX+JhoFhVlsdb8CQkrwncQvwscaGjjYGM7F4fhwmlDsbQkm4bWTvYeN0MR3WTfiRbqWzpZWhI+uR/8ZWlJNjUNbdQ2tbstxeAnxkAMg7V293hpBPmGvXh/kNZWmGU33GTtfm8bi0wDAZheRBhhDMQwWF/ZQH56IoVZSW5LCTgT0hIpyU0xX16XWVvZwLTcFCakJbotJeAU56QwIS2BdZXmT0i4YAyEn/T2KhuqG1lSnBWxPvoLS7LZVNNER3eP21LGJB3dPWysbozI3gOAiLC0JJt1FQ309JplN8IBYyD8ZPexZk61d3PBtMj88oI1rLLT08vmAyNZIcUwWrYcOEmnpzeihrf2ZWlJDs0dHt48bJZ2CQeMgfAT79j0JcVZLitxjoVFmcRGi3EzucTaynpio4WFRZluS3GMC6ZlI2LiEOGCMRB+sq6ygZLcFHLHJbgtxTGS4mIoLcgwX16XWFfRQGlBBklxbi+R5hyZyXGcPTHNzLkJE4yB8INOTw+bDzRFtHvJy4XTstlzrJmmti63pYwpTrV3sftYMxcUR34bu2BaNttqT9LeZbLMhTrGQPjBtkOn6OjujWj3kpfF9nssrzZLIgSTTTVNqMKiMdLGunuULQdOui3FMATGQPjB+soGogQWFkX+l3fOJGtJhPVVxgUQTDZUN5IQG8WcSeGbPc5fzi/MICZKWG/WZQp5jIHwg9erGpkzKZ20xFi3pThObHQUC6ZmmkXVgkx5dROlBRkRtYTLQCTFxTBvSjobTC815DEGYghaOz3sqD3FBdMiv/fgZUlxFlX1bZwwy38HhVPtXew93syiqWOnjS0uzuatw6doNst/hzTGQAzBpppGPL06JoKHXhYXWe/V9CKCw0Y7/rB4DMQfvCwuyqJXYVO1mXMTygxqIERkkoh8S0T+LiKbReQ1EfmdiFwpImPCuKyraCQ+JorzCiJnee+hmDVxHOMSYkwcIkiUvx1/SHdbStCYNyWd+JgoE4cIcQYccC0iD2Dlh34W+HegDkgApgMrge+LyHdV9bVgCHWL9VUNzC/MICE28n3DXqKjhEVFWcZHHCQ2VDUyvyCTuJgx8Z8LgAQ7p4ppY6HNYC3yv1X1clX9jaquV9VKVd2pqk+o6r8Ay4CjA10sIveLSJ2I7Bzg+A0i8qaIvCUi60XkXJ9jB+z920Vky0jf3GjxLn+9ZAy5l7wsKc6itumMWZrZYU62dbH3eAuLInj29EAsKTZzbkKdwQzEFSIyaaCDqtqlqpWDXP8gVk9jIGqAi1X1HOAnwN19ji9X1bmqOn+QMhzF2/0dCxPk+rK42MQhgsHGGssHv2gMDKHui/c9mzk3octgBmIisEFE1orIrSIyrBXEbNfTgBEou1finSlTDgxojNxifWUDqQkxnJMf+WPT+zI9L4Ws5DgTh3CY8upGEmOjx1T8wcucSWkkmzk3IY2oDrzsrljrWl8EXAdcA+wAHgWeUNUhU4+JSCHwrKqePcR53wJmqurn7dc1wElAgT+qat/ehe+1NwM3A+Tl5ZWuWrVqKFn90traSkpKyrv2fXtNO5NSo/jqee6tv9SfrmDxu+0d7D/Zy6+WJb5niXM3dQ1FqGrrT060oJcAACAASURBVNcPXj/DuDj49vnu5X9w8379cmsH9e29/GLpe3OshNPnGCqMRNvy5cu3DuipUVW/NiAaWAFsA9r9vKYQ2DnEOcuBPUCWz758+zEXyyhd5E99paWlOlJWr179rteHT7Zrwe3P6n1rq0dcZiDoqyuYPFx+UAtuf1Yr61rec8xNXUMRqtr66mpq7dSC25/Vu16tcEeQjZv36+41VVpw+7N6/PSZ9xwLl88xlBiJNmCLDvCb6tewCRE5B7gT+C3QCXxvWCZq4HLnAPcCV6vq245IVT1iP9YBTwILAlHfcNho+0XHom/Yi3dcvhmK6Awba7xtbOwFqL1425iJdYUmAxoIESkRkR+IyC7gYaANuFxVF6nqr0dbsYhMAZ4APqWq+332J4tIqvc5cDnQ70goJymvbiQtMZaZ41ODXXXIUJiVxIS0BDYYH7EjlFc3kRgbzTn5Yy/+4OWsCeNIS4w1cYgQZbCF51/Aijd8XFWH/QMtIo9iDYXNFpHDwA+BWABV/QNwB5AF/M72b3vU8oPlAU/a+2KAR1T1heHWP1o21jSxYGomUVGRmV7UH0SExcVZlO2rp7dXx/S9cILy6kbmF2aMqfkPfbHm3GSa+RAhyoAGQlWLfV+LyDjf81V10Dnyqnr9EMc/D3y+n/3VwLnvvSJ4HD11hoON7Xx6caGbMkKCxUVZPPHGEfadaOGsCePclhMxNNnzHz5w7kS3pbjO4qIsXtx1gtqmdiZnvjdYbXCPIf+6iMgXReQ48Caw1d5cm7wWDIxv+B2W2HNATBwisGyqMTEuL942ZuIQoYc/fdtvAWeraqGqTrW3IqeFucnG6ibGJcQwc7z5x5yfnsiUzCQzmSnAbKjyzn8Ye3Ns+lKSa825MW0s9PDHQFQBY2q9hfLqRhZMzSLa+NwBqye1qaaJ3t6B58wYhkd5dRPzCzOIjR678QcvItbaX+XVjd5h7oYQwZ/W+T1gvYj8UUR+492cFuYWx06f4UBju3Ev+bCoKIvTZ7rZc7zZbSkRQWNrJ/tOtBj3kg+LijI5erqD2qYzbksx+DDYKCYvfwReBd4Cep2V4z4bq8fu2jgD8c6aOU3MnmhcIqNl0xhef2kgfNdlmpJlAtWhgj8GIlZVv+G4khBhY00jqQkxZsSODxPTEynIsuIQn7twqttywp7y6kaS4kz8wZdpuSlkp8SxobqRj50/2W05Bht/XEzPi8jNIjJBRDK9m+PKXKK8uomFUzNN/KEPi6ZmsbG6kR4Thxg1Vvwh08QffBARFpo4RMjhTwu9HjsOQYQPcz1+uoOahjbT9e+HRcWZNHd42HPMxCFGwzvxh4j9jzViFhVlcex0B4dMDpKQYUgXk6qOGZ+Cd/7DwjGUPN5fvPekvLqRs8fg8ueBYiznfxiKxbbRLK9upCAr2WU1Bhh8LaYLB7tQRMaJyKDLeIcb5dVNpMbHMGuiiT/05Z04hEkyPxq88YexmGNkKIpzrDiEaWOhw2A9iI+IyH9grcm0FajHykk9DWuJ7gLgm44rDCIbqxtZYOIPA7K4KIvn3jpm4hCjwFp/ycQf+sMbh9hQZeIQocKArVRVvw5cBRwDPoqVFvQbQAlWEp+LVHVzUFQGgZMdvVSb+MOgLCrKMnGIUdDcqew/0cpi08YGZHFRFsebOzjYaOIQocCgMQh7Qb577C2i2ddkTfFYaIKHA7LQx0c8zWUt4cjekz2AWeNrMHznQ4x3WYvBv1FMY4K9J3us+IOZ/zAgE9ISKcwy6zKNlL1NPSTHRZsg/yAU5ySTnRJv2liIYAyEzd6mHs6fmkmM8Q0PyqKiLDbWNNFrfMTDZm9Tj4k/DIG1LlMm5dVNJg4RApiWCtQ1d3C8TVk41XT9h2JxcRYtHR4ONUf8qisBpaG1k6OtamJcfrDIjkPUtRsD4Tb+5INIslOP3mO/LhGRq/wpXETuF5E6Eek3I51Y/EZEKkXkTRE5z+fYjSJSYW83+vuGRoIZm+4/3vkQe5uMgRgO76zxZf6EDIU3T/Weph6XlRj86UE8AHQCi+3XR4Cf+ln+g8DKQY5fgTUqqgS4Gfg9gL2Uxw+BhcAC4IcikuFnncOmvLqRhGiYbeY/DMn4tASmZiebL+8w8bYxM/9haIqyk8lJjWevaWOu44+BKFbV/wC6AVS1HfBrooCqvgYMNuvlauAhtSgH0kVkArACeElVm1T1JPASgxuaUVFe3cj0zGgTf/CTRUWZ7D/ZY+ZDDIMN1Y1MzzBtzB+8+SH2NvWaOITL+LOaa5eIJAIKICLFWD2KQJAP1Pq8PmzvG2j/exCRm7F6H+Tl5VFWVjYsAV09Cl0dTMvsGfa1waC1tTXkdI3r8HDGA3965lUK06LdlvMeQu2ene5UKuvaubpQQ0qXl1C7XwCZ3d2c6lT+8txqxieHllENxfvlJdDa/DEQP8SaTT1ZRB4GLgBuCpiCUaKqdwN3A8yfP1+XLVs27DIuvxTKysoYybVOE4q6zmru4I9vvkJ3xlSWXRR62WdD7Z49++ZRYBvnjk8MKV1eQu1+AUyub+X/dq9Bc6axbMEUt+W8i1C8X14CrW1I06yqLwEfxjIKjwLzVbUsQPUfAXwXf59k7xtovyEEyBuXwPgkMWPV/aS8upGU+BgKxoXWP+FQpig7mbR4YUOVaWNu4s8opvOw1l06BhwFpohIsYj40/sYiqeBT9ujmRYBp1X1GPAicLmIZNjB6cvtfYYQYWZmNJtqmkwcwg/Kq5s4vzDDrPE1DESEszKjTH4Il/HnL83vgHIsN849wAbgr8A+Ebl8sAtF5FH7/BkiclhEPicit4jILfYpzwHVQKVd9q3w9hIfPwE229ud9j5DiDAzM5qWTg+7jp52W0pIU9fSQWVdqxlCPQJmZkZT19JJTUOb21LGLP70Ao4Cn1PVXQAiMgu4E/gO8ATwz4EuVNXrBytYrb8GXx7g2P3A/X7oM7jAzEzrv0V5dSNzJqW7rCZ08c1xfrKqdoizDb7MzLQGQJRXN1GUk+KymtDltf31HGpq53oHYjX+9CCme40DgKruBmaqanXA1RjChvSEKIpyks3a/UPgjT+YOTbDJy9JyE016zINxaObDvH7sipHXJj+GIhdIvJ7EbnY3n4H7BaReOy5EYaxyaKiLDbXNOHpMbOqB6K8upHzCzPM/IcR4J0PscHEIQZEVdlY0+TYKtT+tNqbsGIEX7O3antfN1biIMMYZVFRFi2dHnab/BD9UtfSQVW9yTEyGhYXZ1Hf0km1iUP0S0VdK01tXY61MX9yUp8B/tve+tIacEWGsGHR1HfyQ5g4xHvxxh+8awsZho9vfohiE4d4D173m1NJqPwZ5loiIo+LyG4RqfZujqgxhBW54xIoykk2Y9UHYEN1o8kxMkoKs5LIGxdvYl0DsLG6iYlpCUzKSHSkfH8X6/s94MFyKT0E/NkRNYawY3FRFpsPnDRxiH4or240OUZGiTcOYeZDvBcr/tDIoqIsRJyZY+NPy01U1VcAUdWDqvoj4EpH1BjCjkVFWbR2eth11MQhfKlr7qC6vs0s7x0AFhVZcYiqehOH8KWqvpWG1i5H0yT7YyA6RSQKqBCR20TkQ4BxBhqAd+epNrxDuckxEjB84xCGd9hQ7Xwb88dAfBVIAr4ClAKfBD7tmCJDWJGbmkBxTrL58vah3MQfAkZhVhLjxyWYNtaH8upGJqQlMCUzybE6/DEQharaqqqHVfUzqvoRILSWVzS4yiITh3gP5VWNLDDxh4Bg8lS/F1VlY3UTC6dmOhZ/AP8MxPf83GcYoywutuIQO00cAoATzR1UN5j5D4FkUVEWDa0mDuGlqr6NhtZOx9vYgPMgROQK4P1Avoj8xufQOKwRTQYD8E6e6vLqRuZONvMhvK4QYyACh/debqhuZFquCYFurAlOGxusB3EU2Ap02I/e7WmslKAGAwA5qfFMy00xPmKbt+MPZv2lgFFg4hDvory6ibxx8RRkORd/gEF6EKq6A9ghIn9WVdNjMAzKoqJMnnzjCJ6e3jHvd99Q1cjCokyT/yGAiAiLi7NYW1GPqjrqdw91VJUNVQ1cOC3b8fsw4DdZRN4SkTeBN0Tkzb6bo6oMYceioizaunrGfBziyKkzHGhsZ3FxtttSIo5FRZk0tHZRVT+2V/ipqLPmPywJQhsbbC2mqxyv3RAxvO0jrhrbcQjvsiNLzPpLAeedOEQT03JTXVbjHt42Fow1vgbsQdizpg+q6kGsOMQ59nbG3jckIrJSRPaJSKWIfLef478Ske32tl9ETvkc6/E59vTw35ohmGSnxFNi4hBsqGokMzmOGXlj9wfMKaZkJjEhzcQh1lc1MDkzkckOzn/w4s9ifR8DNgEfBT4GbBSRa/24Lhr4LXAFMAu43s5G9zaq+nVVnauqc4H/xcpQ5+WM95iqftDvd2RwjUVFWWw50ET3GJ0P4fUNLyrKJMrEHwKOd12mjWN4XaaeXqW8usmx1Vv74k808fvA+ap6o6p+GlgA/MCP6xYAlaparapdwCrg6kHOvx541I9yDSHK23GII2MzT/XBxnaOnu4w8QcH8cYhKuvGZhxiz7FmTp/pDkr8AfzLSR2lqnU+rxvxz7DkA75JeA8DC/s7UUQKgKnAqz67E0RkC9aci39T1acGuPZm4GaAvLw8ysrK/JD2XlpbW0d8rZOEk66eTutf3SMvb+Z0UZwLqizcumdltVaCxZiGKsrKat5zPJw+y1CgP13SbvVOH3qxnEunxLqgyt379XyN1cb0xD7Kyireczzg2lR10A34T+BFrCxyNwHPA//ux3XXAvf6vP4UcNcA594O/G+fffn2YxFwACgeqs7S0lIdKatXrx7xtU4Sbrre98sy/fR9G4Mrpg9u3bMvP7xVF/zsJe3t7e33eLh9lm7Tn67e3l5d/POX9dY/bw2+IBs379dN92/US/5r4PpHog3YogP8pg7ZE1DVbwN/BObY292qersftucIMNnn9SR7X39cRx/3kqoesR+rgTJgnh91GlzGWpdp7MUhVJXy6kYWO7g2v2Fs54fo7ullU01TUDMU+hOk/gawUVW/YW9P+ln2ZqBERKaKSByWEXjPaCQRmQlkABt89mWISLz9PBu4ANjtZ70GF1lUlEV7Vw9vjbE4RDDHpo91FhVl0djWRcUYi0O8efg0bV09QW1j/sQSUoF/ishaOx9Enj8FqzX7+jYs99Qe4DFV3SUid4qI76ik64BV+u6/A2cBW0RkB7AaKwZhDEQYsGDq2MwPsb6yATD5p4PBWM0P4cYaX0MGqVX1x8CPRWQO8HFgjYgcVtXL/Lj2OeC5Pvvu6PP6R/1ctx5rzoUhzMhOiWd6Xgrl1U3cusxtNcFjfVVj0Mamj3UmZyaSn55IeXUjn15c6LacoLG+qoGzJowjMzl4A0CGs2hOHXAcaxRTrjNyDJHA4jE2H8Iam94YtLHpYx0RYeEYyw/R0d3DlgMng97G/IlB3CoiZcArQBbwBVWd47QwQ/gy1uIQe44109zhMfGHILKoKIumMRSH2HboFJ2e3qAv4eJPD2Iy8DVVna2qPzKxAMNQeOMQ3jVjIp31VSb+EGwW+6z9NRbYUN1IlMACOwd8sPBnmOv3VHV7MMQYIoOslHhm5KWOmSDi+qpGinOSyRuX4LaUMcOkjHfiEGOBDVUNnDMpnXEJwZ0cOLYX7jc4xqKiTLYcOBnxcQg3xqYb3olDbKxporc3suMQ7V0eth065UqMyxgIgyMsLs7iTHcPO2pPDX1yGLOj9hTtQR6bbrBYbMch9p1ocVuKo2yqacLTq64sIW8MhMERFhdlEyWwtqLBbSmOsraiARGT/8ENLiyxjPK6CG9j6yoaiIuJ4vzC4MYfwBgIg0OkJcUyZ1I6ayvq3ZbiKGsr6pkzKZ30JPcWJxyrTEhLZFpuCq9FfBtrYEFhJolx0UGv2xgIg2NcVJLN9tpTnD7T7bYURzh9ppvttae4qMS4l9xiaUk2m2qa6OjucVuKI5xo7mDfiRaWutTGjIEwOMbS6Tn0auQORdxQ1UivwoXTjIFwi4tKcuj09LLlwEm3pTiC1312oTEQhkhj7uR0UuJjItbNtLainuS4aOZNyXBbyphlYVEmsdES0W0sOyWOs8aPc6V+YyAMjhEbHcWioqyIDVSvrWhgcXEWcTHma+QWSXExlBZk8FoEtrHeXmVdZQMXTst2LYWtadkGR7loejaHmto52NjmtpSAcrCxjUNN7SwtyXFbyphnaUkOe441U9/S6baUgLL3eAsNrV2utjFjIAyO4vXPR1ovYq3LvmHDO3gDuK9XRlobs9xmbrYxYyAMjjI1O5n89MSI8xGvragnPz2Rouxkt6WMeWZPTCMjKTbihruurWhgRl6qq0u4OGogRGSliOwTkUoR+W4/x28SkXoR2W5vn/c5dqOIVNjbjU7qNDiHiHDR9GzWVzXiiZBlNzw9vayvamRpSbZJLxoCREcJF0zLZl1FQ8Qs/93R3cOmA02uDW/14piBEJFo4LfAFcAs4HoRmdXPqX9R1bn2dq99bSbwQ2AhsAD4oYiYoSJhytKSHFo6POw4HBnLbuw4fIqWDo9xL4UQF5XkUNfSGTHLbmysaaLL0+t6G3OyB7EAqFTValXtAlYBV/t57QrgJVVtUtWTwEvASod0GhzmgmnZREcJq/dGhgtg9d56oqOEpdNMgDpUuGi69VlEThurIyE2KqjpRftjyJSjoyAfqPV5fRirR9CXj4jIRcB+4OuqWjvAtfn9VSIiNwM3A+Tl5VFWVjYisa2trSO+1kkiRde0NOHpLdXMjz/mnCgbp+/Z01vOUJwmbNv0+rCui5TPMlgMV9eU1Cie3Lifs9710xF4nL5fqspz288wIz2K8tfXDuvagGtTVUc24FrgXp/XnwLu6nNOFhBvP/8i8Kr9/FvA//M57wfAt4aqs7S0VEfK6tWrR3ytk0SKrt+trtSC25/VY6fOOCPIByfv2bFTZ7Tg9mf1t6srhn1tpHyWwWK4uv7jhT1a9L1/6Km2LmcE2Th9vyrrWrTg9mf1ofU1w752JNqALTrAb6qTLqYjWNnovEyy972Nqjaqqnfw8r1Aqb/XGsKLS2ZaaczL9tW5rGR0ePV7348hdLhkZi49vRr2o5lW77Xa2PIQaGNOGojNQImITBWROOA64GnfE0Rkgs/LDwJ77OcvApeLSIYdnL7c3mcIU6bnpZCfnsire8PbQLy6t46JaQnMyEt1W4qhD3MnZ5CRFPv2D2y4snpfHdPzUpiUkeS2FOcMhKp6gNuwftj3AI+p6i4RuVNEPmif9hUR2SUiO4CvADfZ1zYBP8EyMpuBO+19hjBFRFg+M4d1lQ10esJz5c1OTw/rKhtYPjPXDG8NQaKjhIun51C2v56eMM0y19rpYVNNU0j0HsDheRCq+pyqTlfVYlX9mb3vDlV92n7+PVWdrarnqupyVd3rc+39qjrN3h5wUqchOCyfkUt7Vw+basLT1m+qaaK9q4flM0Ljy2t4L8tn5tLU1hW2Q6rXVdTT3aMh08bMTGpD0FhSnE18TFTYDkVcvbeeuJgolkwz2eNClYun5xAlUBambqbVe+tJTbAWIAwFjIEwBI3EuGgWF2exOkwD1av31bG4KIukOCdHhxtGQ3pSHOdNyeDVMGxjqsrqfXVcND2H2OjQ+GkODRWGMcPyGbnUNLRR0xBeq7t6NS+fYSbHhTrLZ+ay80gzdc0dbksZFruONlPX0hky7iUwBsIQZLzDQ1/efcJlJcPDq/fSs/JcVmIYirfb2J7w6kW8tPsEIrAshP6EGANhCCqTM5OYPXEcL+w67raUYfHCruPMmjCOyZnuDz00DM7M8akUZCWFXRt7cddxzi/MJDsl3m0pb2MMhCHorJw9nq0HT4aNC6CuuYOtB0+y8uzxbksx+IGIsHL2eNZXNnD6TLfbcvyipqGNvcdbWDk7tNqYMRCGoOP9oX0xTNxMXp3GQIQPK84ej6dXeXVvmLQxu7ezIsTamDEQhqAzLTeFopxkXtwZHi6AF3cepyg7mZLcFLelGPxk7qR08sbF80KYtLEXdh5nzqQ08tMT3ZbyLoyBMAQdrwtgQ3Ujp9q73JYzKKfauyivbmTF2ePN7OkwIipKWDF7PGv213OmK7Rn7h87fYbttadYEWLuJTAGwuASK2aPp6dXQ36kySt76vD0akh+eQ2Ds2L2eDq6e1mzP7QnZv5zl+UGC8U2ZgyEwRXmTEpjQlpCyLsAXth1nAlpCczJT3NbimGYLJiaSXpS7Nv+/VDlhZ3HmZabwrQQdGEaA2FwBRHLBfBaRT1tnR635fRLW6eH1/bXs2L2eKKijHsp3IiNjuKys/J4ec8JujyhmQ+9qa2LjTWNITd6yYsxEAbXWHn2eLo8vSG7BHjZvno6Pb1cPttMjgtXVs4eT0uHh9erGtyW0i8v7T5Or4amewmMgTC4yPmFmeSmxvP37UfdltIvT20/Qm5qPAunmsX5wpWl07MZlxDD06HaxrYdpTAribPzx7ktpV+MgTC4RnSUcPXciZTtq6OpLbRGM51s66JsXx1Xz51ItHEvhS3xMdFcOWciL+w8HnKuzKOnzlBe08g18/JDdoScMRAGV/nQvEl4epV/vHXMbSnv4h9vHaO7R7lmXr7bUgyj5EPz8jnT3cNLITYx8+kdR1G19IUqjhoIEVkpIvtEpFJEvtvP8W+IyG4ReVNEXhGRAp9jPSKy3d6e7nutITI4a0IqM/JSeWpbaKUcf3LbEabnpTBrQmh2/Q3+M78gg/z0RJ4IsTb21LYjnDclnYKsZLelDIhjBkJEooHfAlcAs4DrRWRWn9O2AfNVdQ7wOPAfPsfOqOpce/sghohERLhmXj5bD57kUGO723IAONTYztaDJ0O662/wn6go4Zp5E1lXUU9dS2is/7XnWDN7j7eEdO8BnO1BLAAqVbVaVbuAVcDVvieo6mpV9f4qlAOTHNRjCFGunjsRsILCoYBXxzVzQ/vLa/CfD83Lp1fhmR2h4cp8atsRYqKEK+dMdFvKoIiqM8m9ReRaYKWqft5+/SlgoareNsD5dwHHVfWn9msPsB3wAP+mqk8NcN3NwM0AeXl5patWrRqR3tbWVlJSQm+iyljR9W+bznCqQ/nF0sRR/2sfjTZV5Xtrz5CeIHx3QWDXxRkrn2WgCLSuH60/Yz0uGd3nOlpdvap8s+wMBeOi+Fppwqi09GUk2pYvX75VVef3e1BVHdmAa4F7fV5/CrhrgHM/idWDiPfZl28/FgEHgOKh6iwtLdWRsnr16hFf6yRjRdeqTQe14PZndduhk6MuazTath86qQW3P6urNh0ctY6+jJXPMlAEWte9a6u14PZnteJE86jKGa2u1yvqteD2Z/WZHUdGVU5/jEQbsEUH+E110sV0BJjs83qSve9diMhlwPeBD6pqp3e/qh6xH6uBMmCeg1oNLnPFOROIj4nisS21rur4y5Za4mOiWHn2BFd1GALPB8+dSEyU8NiWw67q+MuWWlITYrgsDLITOmkgNgMlIjJVROKA64B3jUYSkXnAH7GMQ53P/gwRibefZwMXALsd1GpwmXEJsVw1ZyJ/33bEtfHqbZ0e/r7tCFfNmUhaYqwrGgzOkZMaz2Vn5fH41sN0etxZ4bWprYvn3zrOh+flkxAb7YqG4eCYgVBVD3Ab8CKwB3hMVXeJyJ0i4h2V9J9ACvDXPsNZzwK2iMgOYDVWDMIYiAjnEwun0NbVw9M73Jn1+vSOo7R19fCJhVNcqd/gPJ9YOIWmti5e3OXOnIi/bT1MV08vn1hYMPTJIUCMk4Wr6nPAc3323eHz/LIBrlsPnOOkNkPocd6UdGaOT+XhjQe57vzJQR1iqqo8svEQM8enct6U9KDVawguF07LZnJmIg+XH+SD5wZ3BFFvr/LopkOUFmQwY3xqUOseKWYmtSFkEBFuWFTAziPNvHHoZFDrfuPQSd46cpobFk4xcx8imKgo4RMLCthY08SeY81BrXttZQPVDW3cEEY9VGMgDCHFR87LZ1xCDPevOxDUeu9fd4BxCTF8+DwzFSfSuX7BZBJjo3ng9Zqg1nv/uhpyUuO5KsTnPvhiDIQhpEiKi+H6hVN4fucxDp8MzszqwyfbeX7nMa5fOIXkeEe9roYQID0pjo+U5vPU9qM0tHYOfUEAqKxrYc3+ej69qIC4mPD52Q0fpYYxw42LCxGRoPUiHnj9ACLCpxcXBqU+g/vctGQqXZ5eHlp/ICj13bu2hviYqLAbAGEMhCHkmJieyDVz83lk00EaHf6H19jaycMbD3L13Inkpwd25rQhdJmWm8KK2Xk8uP4AzR3djtZ15NQZ/vbGYa47fzJZKfGO1hVojIEwhCS3Li+m09PLfeuc9RPft66GTk8vty6b5mg9htDjtuUlNHd4+NOGg47Wc/eaKlTh5ouLHa3HCYyBMIQkxTkpvP+cCTy04aBjyYSa2rp4aMNB3n/2hJBMGG9wlnMmpXHx9BzuW1dDi0O9iOOnO1i1uZaPnDcpLHuoxkAYQpavXVpCe5eHu16tdKT8u16tpL3Lw1cvK3GkfEPo8433TaeprYt7Xqt2pPxfvbQfVbjtkvDsoRoDYQhZSvJS+WjpZP5UfoDapsCOaKptaudP5Qe4tnQS0/PCY9KSIfCcOzmdK8+ZwD1rawKeK6LiRAt/3VrLJxcVMDkzKaBlBwtjIAwhzdffN53oKOFn/9gT0HJ//tweokT4+vumB7RcQ/jx7RUz6O7p5T9e2BewMlWVO5/dTXJcTNj2HsAYCEOIMz4tgX+5pIQXdh3n1b2BWT/n1b0neH7ncb5yaQkT0sLPL2wILIXZyXx+aRGPbz3MxurGgJT5zJvHWFvRwDcvn05mclxAynQDYyAMIc8XlhZRkpvCD57aResoV3pt7fRwx993UZKbwheWFgVIoSHc+eqlJUzKSORfn3yLGSFswgAADCtJREFUju7RrfR6qr2Lnzy7mzmT0vhUmM+tMQbCEPLExUTxbx85h2Onz3DHUztHVdYdf9/J0VNn+MWHzwmrGa0GZ0mMi+bnHzqHqvo2fv7cyN2Zqsp3Hn+TU+1d/PxD5xAdFd7replviCEsKC3I5CuXlvDEtiP8dYRJhR7fepgn3jjCVy4tYX5hZoAVGsKdi6bn8PkLp/LQhoM8/9bIclc/tOEg/9x9gttXzuTs/LQAKww+xkAYwoZ/uaSEJcVZ/OuTb7G+qmFY166vauB7T7zJ4qIsblsevkFDg7N8Z+VM5k5O5+uPbWfbMFcUfmXPCX78zC4unZnLZy+Y6pDC4GIMhCFsiI4Sfv/JUqZmJ/PFh7ayqabJr+s2H2jii3/aSmFWMn/4ZCkx0abZG/onLiaKe2+cT25qAp99cDNvHj7l13VrK+q57ZFtzJ6Yxm+un0dUmLuWvDj6TRGRlSKyT0QqReS7/RyPF5G/2Mc3ikihz7Hv2fv3icgKJ3Uawoe0xFge/MwCcsbF88n7NvK3rYex8q6/F1XliTcOc8O9G8lJjefBzy4gLcmkEjUMTnZKPA99dgHJ8TFcd3c5zw3iblJVHt54kM88sJmCrCTuv+n8iFoR2DEDISLRwG+BK4BZwPUiMqvPaZ8DTqrqNOBXwL/b187CymE9G1gJ/M4uz2BgYnoif7tlCXMnp/PNv+7gxgc2s66igd5ey1D0qvJ6ZQM3PrCZbzy2g7mT0vnbLUvCcqkDgzsUZifzxK1LKMlN4daH3+Dz/7eF8urGt9tYT69Stq+O6+8p5/tP7mRxcRaP3bKYnNTwWoxvKJw0dQuASlWtBhCRVcDVgG9u6auBH9nPHwfuEiud19XAKlXtBGpEpNIub4ODeg1hREZyHI9+YRF/2nCAX71cwSfv20h8TBTZKfHUNZ+hu3cjaYmx/PADs/j04sKwH01iCD65qQk8/qUl3Leuht++WsnLe06QEBtFcrTS8tILdPX0kpkcxy8+fA4fnz85YtxKvshA3fNRFyxyLbBSVT9vv/4UsFBVb/M5Z6d9zmH7dRWwEMtolKvqn+399wHPq+rj/dRzM3AzQF5eXumqVatGpLe1tZWUlNBbsM3oGpquHmV7XQ/Vp3s53dlLUpSH6dkJzMuNJi46dL60oXTPfDG6hqazR9l6ooeDzT00tXWTnRJHcVoUc3OjiQkhwzCSe7Z8+fKtqjq/v2Nh7yxT1buBuwHmz5+vy5YtG1E5ZWVljPRaJzG6/ONyn+ehps2L0TU8Qk2XNxAaarp8CbQ2J4PUR4DJPq8n2fv6PUdEYoA0oNHPaw0Gg8HgIE4aiM1AiYhMFZE4rKDz033OeRq40X5+LfCqWj6vp4Hr7FFOU4ESYJODWg0Gg8HQB8dcTKrqEZHbgBeBaOB+Vd0lIncCW1T1aeA+4E92ELoJy4hgn/cYVkDbA3xZVUe3QIrBYDAYhoWjMQhVfQ54rs++O3yedwAfHeDanwE/c1KfwWAwGAbGTCk1GAwGQ78YA2EwGAyGfjEGwmAwGAz9YgyEwWAwGPrFsZnUbiAi9cDBEV6eDQxvDengYHQNn1DVZnQND6Nr+IxEW4Gq5vR3IKIMxGgQkS0DTTd3E6Nr+ISqNqNreBhdwyfQ2oyLyWAwGAz9YgyEwWAwGPrFGIh3uNttAQNgdA2fUNVmdA0Po2v4BFSbiUEYDAaDoV9MD8JgMBgM/WIMhMFgMBj6JeINhIisFJF9IlIpIt/t53i8iPzFPr5RRAr/f3vnH2NXUcXxz1fENkUCLY2xIL9aJQ1FSlsErRVBTQo1UJSQlECkUqIVIRoDCaZJY0xUkv6hEjDGEIMkpghViUUxtlKBdNmSgm0XBEq7JWhDLFYobTDLr+Mfcx5Mr/e9fd2+ubtZzye52bnz4853zz3vzZ07u2eysm97/rOSFlbbNqDtW5L+JmmbpD9LOjkre0vSFj+qYdRL61oq6aWs/2uzsqslPefH1dW2hXX9MNO0XdIrWVlJe/1c0h7fIbGuXJJudd3bJM3NykraazhdV7qeAUl9kmZnZc97/hZJmxvWdb6kfdn9WpmVdfSBwrpuyjQ96T41xctK2utESRv8u+ApSd+oqVPGx8xs3B6kMOM7genA+4CtwOmVOtcBP/X0EuBXnj7d608ATvXrHNGwtguASZ7+Wkubnx8YRZstBW6raTsFGPSfkz09uSldlfo3kELMF7WXX/s8YC7wZJvyRcADgICPA5tK26tLXfNb/QEXtXT5+fPA1FGy1/nA/YfrA73WVal7MWn/mibsNQ2Y6+mjge01n8kiPjbeZxDnADvMbNDMXgfuBhZX6iwGfuHpNcBnJcnz7zazITPbBezw6zWmzcw2mNlrftpP2lmvNN3YrB0LgXVm9m8zexlYB1w4SrquAFb3qO+OmNnDpP1M2rEYuMsS/cCxkqZR1l7D6jKzPu8XmvOvbuzVjsPxzV7ratK/XjSzJzy9H3gaOKFSrYiPjfcB4gTg79n5P/hfw75Tx8zeBPYBx3XZtrS2nGWkJ4QWEyVtltQv6dJR0HWZT2XXSGptD1vSZl1f21/FnQo8mGWXslc3tNNe2scOhap/GfAnSY9L+soo6PmEpK2SHpA0y/PGhL0kTSJ9yf46y27EXkqvwOcAmypFRXys6IZBQW+QdBVwNvDpLPtkM9staTrwoKQBM9vZkKS1wGozG5L0VdIM7DMN9d0NS4A1dvAuhKNprzGNpAtIA8SCLHuB2+sDwDpJz/gTdhM8QbpfByQtAu4jbTs8VrgY2Ghm+WyjuL0kvZ80KH3TzF7t5bXbMd5nELuBE7PzD3lebR1J7wWOAfZ22ba0NiR9DlgBXGJmQ618M9vtPweBv5CeKhrRZWZ7My13APO6bVtSV8YSKtP/gvbqhnbaS/vYsEg6k3QPF5vZ3lZ+Zq89wG/p7evVjpjZq2Z2wNN/AI6UNJUxYC+nk38VsZekI0mDwy/N7Dc1Vcr4WIlFlbFykGZIg6TXDa1FrVmVOl/n4EXqezw9i4MXqQfp7SJ1N9rmkBblPlLJnwxM8PRU4Dl6tFjXpa5pWfoLQL+9uyC2y/VN9vSUpnR5vZmkBUM1Ya+sj1Nov+j6eQ5eQHystL261HUSaW1tfiX/KODoLN0HXNigrg+27h/pi/YFt11XPlBKl5cfQ1qnOKope/nvfhfwow51ivhYzww7Vg/S6v520hftCs/7LumJHGAicK9/UB4DpmdtV3i7Z4GLRkHbeuCfwBY/fuf584EB/4AMAMsa1vUD4CnvfwMwM2t7jdtyB/DlJnX5+XeAWyrtSttrNfAi8AbpHe8yYDmw3MsF3O66B4CzG7LXcLruAF7O/Guz5093W231+7yiYV3XZ/7VTzaA1flAU7q8zlLSH6/k7UrbawFpjWNbdq8WNeFjEWojCIIgqGW8r0EEQRAEIyQGiCAIgqCWGCCCIAiCWmKACIIgCGqJASIIgiCoJQaIIGiDpGMlXZedHy9pTaG+Ls2jltaUf1TSnSX6DoJ2xJ+5BkEbPO7N/WZ2RgN99ZH+n+NfHeqsB64xsxdK6wkCiBlEEHTiFmCGx/hfJemU1l4BSnti3Cdpne8FcL3S/h1/9YCArX0CZkj6owdxe0TSzGonkk4DhlqDg6TLfb+BrZLyeD5rSf/tHwSNEANEELTnZmCnmZ1lZjfVlJ8BfBH4GPA94DUzmwM8CnzJ6/wMuMHM5gE3Aj+puc4nSQHqWqwEFprZbOCSLH8z8KnD+H2C4JCIaK5BMHI2WIrPv1/SPtITPqRQB2d69M35wL1pixEgxfaqMg14KTvfCNwp6R4gD8y2Bzi+h/qDoCMxQATByBnK0m9n52+TPlvvAV4xs7OGuc5/SEHgADCz5ZLOJQVge1zSPEuRVid63SBohHjFFATt2U/a4nFEWIrZv0vS5fDOvsGza6o+DXy4dSJphpltMrOVpJlFK1zzaUDtfslBUIIYIIKgDf7UvtEXjFeN8DJXAssktSJ91m2R+TAwR+++h1olacAXxPtIUUIh7VH++xHqCIJDJv7MNQjGAJJ+DKw1s/VtyicAD5F2LnuzUXHB/y0xgwiCscH3gUkdyk8Cbo7BIWiSmEEEQRAEtcQMIgiCIKglBoggCIKglhgggiAIglpigAiCIAhqiQEiCIIgqOW/sMLhWL1Rt5wAAAAASUVORK5CYII=\n" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "source": [ + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "t = np.arange(0.0, 2.0, 0.01)\n", + "s = 1 + np.sin(2 * np.pi * t)\n", + "\n", + "fig, ax = plt.subplots()\n", + "ax.plot(t, s)\n", + "\n", + "ax.set(xlabel='time (s)', ylabel='voltage (mV)',\n", + " title='About as simple as it gets, folks')\n", + "\n", + "ax.grid()\n", + "\n", + "fig.savefig('test.png')\n", + "\n", + "plt.show()" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + } + }, + "orig_nbformat": 2, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3, + "kernelspec": { + "name": "python_defaultSpec_1591908362720", + "display_name": "Python 3.8.2 64-bit ('venvForWidgets': venv)" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/test/datascience/notebook/withOutput.ipynb b/src/test/datascience/notebook/withOutput.ipynb new file mode 100644 index 000000000000..2c5c4b3fcb5a --- /dev/null +++ b/src/test/datascience/notebook/withOutput.ipynb @@ -0,0 +1,1371 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a=1\n", + "a" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "tags": [ + "WOW" + ] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Package Version \n", + "-------------------- ----------\n", + "adal 1.2.2 \n", + "altair 4.1.0 \n", + "appdirs 1.4.3 \n", + "appnope 0.1.0 \n", + "attrs 19.3.0 \n", + "azure-common 1.1.25 \n", + "azure-kusto-data 0.0.44 \n", + "azure-kusto-ingest 0.0.44 \n", + "azure-storage-blob 2.1.0 \n", + "azure-storage-common 2.1.0 \n", + "azure-storage-queue 2.1.0 \n", + "backcall 0.1.0 \n", + "beakerx 1.4.1 \n", + "black 19.10b0 \n", + "bleach 3.1.4 \n", + "bqplot 0.12.6 \n", + "branca 0.3.1 \n", + "certifi 2020.4.5.1\n", + "cffi 1.14.0 \n", + "chardet 3.0.4 \n", + "click 7.1.2 \n", + "cryptography 2.9.2 \n", + "cycler 0.10.0 \n", + "debugpy 1.0.0b7 \n", + "decorator 4.4.2 \n", + "defusedxml 0.6.0 \n", + "entrypoints 0.3 \n", + "idna 2.9 \n", + "ipydatawidgets 4.0.1 \n", + "ipykernel 5.2.1 \n", + "ipyleaflet 0.12.4 \n", + "ipython 7.13.0 \n", + "ipython-genutils 0.2.0 \n", + "ipyvolume 0.5.2 \n", + "ipywebrtc 0.5.0 \n", + "ipywidgets 7.5.1 \n", + "isodate 0.6.0 \n", + "jedi 0.17.0 \n", + "Jinja2 2.11.2 \n", + "json5 0.9.5 \n", + "jsonschema 3.2.0 \n", + "jupyter-client 6.1.3 \n", + "jupyter-core 4.6.3 \n", + "jupyterlab 2.1.3 \n", + "jupyterlab-server 1.1.5 \n", + "K3D 2.7.4 \n", + "kiwisolver 1.2.0 \n", + "MarkupSafe 1.1.1 \n", + "matplotlib 3.2.1 \n", + "mistune 0.8.4 \n", + "msrest 0.6.13 \n", + "msrestazure 0.6.3 \n", + "nbconvert 5.6.1 \n", + "nbformat 5.0.5 \n", + "nglview 2.7.5 \n", + "notebook 6.0.3 \n", + "numpy 1.18.2 \n", + "oauthlib 3.1.0 \n", + "pandas 1.0.3 \n", + "pandocfilters 1.4.2 \n", + "parso 0.7.0 \n", + "pathspec 0.8.0 \n", + "pexpect 4.8.0 \n", + "pickleshare 0.7.5 \n", + "Pillow 7.1.1 \n", + "pip 19.2.3 \n", + "prometheus-client 0.7.1 \n", + "prompt-toolkit 3.0.5 \n", + "ptyprocess 0.6.0 \n", + "py4j 0.10.9 \n", + "pycparser 2.20 \n", + "Pygments 2.6.1 \n", + "PyJWT 1.7.1 \n", + "pyparsing 2.4.7 \n", + "pyrsistent 0.16.0 \n", + "python-dateutil 2.8.1 \n", + "pythreejs 2.2.0 \n", + "pytz 2019.3 \n", + "pyzmq 19.0.0 \n", + "qgrid 1.1.1 \n", + "regex 2020.4.4 \n", + "requests 2.23.0 \n", + "requests-oauthlib 1.3.0 \n", + "Send2Trash 1.5.0 \n", + "setuptools 41.2.0 \n", + "six 1.14.0 \n", + "terminado 0.8.3 \n", + "testpath 0.4.4 \n", + "toml 0.10.0 \n", + "toolz 0.10.0 \n", + "tornado 6.0.4 \n", + "traitlets 4.3.3 \n", + "traittypes 0.2.1 \n", + "typed-ast 1.4.1 \n", + "urllib3 1.25.9 \n", + "vega-datasets 0.8.0 \n", + "wcwidth 0.1.9 \n", + "webencodings 0.5.1 \n", + "widgetsnbextension 3.5.1 \n", + "xarray 0.15.1 \n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "pip list" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# HELLO WORLD" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "invalid syntax (<ipython-input-1-8b7c24be1ec9>, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m File \u001b[0;32m\"<ipython-input-1-8b7c24be1ec9>\"\u001b[0;36m, line \u001b[0;32m1\u001b[0m\n\u001b[0;31m with Error\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n" + ] + } + ], + "source": [ + "with Error" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYgAAAEWCAYAAAB8LwAVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nOydeXxU1fn/30/2lewJECAhIYCgCAbZFAW1gtVW29pWa1vtZq312721/fZX29rtu7bf9ms396+tSq1Vq9alLgRBCJuAsmcDwpoNyEaWSZ7fH/eOjjHLJJk7d2Zy3q/Xfc3MXc75zJ0z88x5nnPOI6qKwWAwGAx9iXJbgMFgMBhCE2MgDAaDwdAvxkAYDAaDoV+MgTAYDAZDvxgDYTAYDIZ+MQbCYDAYDP1iDIThbUTkQRH5qds6nEBEbhCRfzpUtqv3TUSWisg+t+ofKWLxgIicFJFNfpyvIjLNfh6xbTWUMAZiDCIiZfaXMj5I9RXaX+6YYNTXH6r6sKpe7lb9TqKqa1V1hve1iBwQkcucqEtElonI4QAVdyHwPmCSqi4IUJmGAGIMxBhDRAqBpYACH3RVjGGsUwAcUNU2t4UY+scYiLHHp4Fy4EHgxn6OZ4vISyLSIiJrRKTAe0BElojIZhE5bT8u8Tn2rn+tIvIjEfmz/fI1+/GUiLSKyOK+lYrIAhHZICKnROSYiNwlInH2MRGRX4lInYg0i8hbInJ2f29ORG4SkWpbf42I3OCzf53PeSoit4pIhX3uT0SkWETW23U85lP/MhE5LCL/KiIN9nu9YaAbLCJXich2+72sF5E5g5z7axGptevcKiJL+9yTLfaxEyLyywHKePtfvYj8CZgCPGPf6+8McM137Pt8VEQ+38d9Ey8i/yUih+x6/yAiiSKSDDwPTLTLbhWRif7q7FP/54B7gcV2OT+2939BRCpFpElEnhaRiX6UlSoiq0XkN3Zbeb+I7LY/1yMi8q2hyjAMgKqabQxtQCVwK1AKdAN5PsceBFqAi4B44NfAOvtYJnAS+BQQA1xvv86yjx8ALvMp60fAn+3nhVg9lphBdJUCi+yyC4E9wNfsYyuArUA6IMBZwIR+ykgGmoEZ9usJwGz7+U3e92K/VuDvwDhgNtAJvAIUAWnAbuBG+9xlgAf4pX1fLgbafOp5EPip/XweUAcsBKKxjPABIH6A9/1JIMt+398EjgMJ9rENwKfs5ynAogHKWAYc9nn9rs+in/NX2vXMBpKAP9v3Y5p9/FfA0/Znngo8A/yiv7qGo7MfHX0/k0uABuA8+z7/L/Ban8/Mq/FB4Kf2vdvkvf/2sWPAUvt5BnCe29+7cN1MD2IMISIXYnXrH1PVrUAV8Ik+p/1DVV9T1U7g+1j/8CYDVwIVqvonVfWo6qPAXuADgdCmqltVtdwu+wDwR6wfYrAMWSowExBV3aOqxwYoqhc4W0QSVfWYqu4apNr/UNVm+5ydwD9VtVpVT2P9U57X5/wfqGqnqq4B/gF8rJ8ybwb+qKobVbVHVf8Py/gsGuB9/1lVG+33/d9YP4zeeEI3ME1EslW1VVXLB3kvw+FjwAOquktV27GMOWD11uz38HVVbVLVFuDnwHWDlBconTcA96vqG3b7+x5W+ysc4PyJwBrgr6r6//romSUi41T1pKq+MUI9Yx5jIMYWN2L9CDbYrx/hvW6mWu8TVW0FmrC+iBOBg33OPQjkB0KYiEwXkWdF5LiINGP9KGXbOl4F7gJ+C9SJyN0iMq5vGWr5sj8O3AIcE5F/iMjMQao94fP8TD+vU3xen9R3+8oPYt2TvhQA37TdS6dE5BQweYBzEZFvicgesdx2p7B6L9n24c8B04G9Yrn0rhrkvQyHifh8zn2e52D1Krb66H/B3j8QgdL5rjZmt79GBm5jVwKJwB/67P8I8H7goFhu0ve4NA3+YQzEGEFEErH+OV5s/wgfB74OnCsi5/qcOtnnmhQsN8NReyvg3UwBjtjP27B+WLyM93nuz5LBv8fqkZSo6jjgX7HcSVYBqr9R1VJgFtaP0bf7K0RVX1TV92G5l/YC9/hRtz9k2D54L1Ow7klfaoGfqWq6z5Zk97jehR1v+A7W55KhqunAaez3raoVqno9kAv8O/B4Hw0DMdT9PgZM8nk92ed5A5ZxnO2jP01VvcbyPWWPQmdf3tXG7DKyeKeN9eUeLOP1nG99qrpZVa+29TwFPDYCLQaMgRhLXAP0YP3AzrW3s4C1WIFrL+8XkQvtAO1PgHJVrQWeA6aLyCdEJEZEPm6X9ax93XbgOhGJFZH5wLU+ZdZjuX6KBtGXihU/aLX/9X/Je0BEzheRhSISi2WIOuzy3oWI5InI1faPRSfQ2t95o+DHIhJn/7BfBfy1n3PuAW6x9YqIJIvIlSKS2s+5qVixjXogRkTuwIqJeN/PJ0UkR1V7gVP2bn/ezwkGv9ePAZ8RkbNEJAn4gfeAXdc9wK9EJNfWkS8iK3zKzhKRNH90ihXQv8kPzQCP2rrmijUE++fARtvlOBC3AfuwgvKJ9udzg4ikqWo3VpsKZBsYUxgDMXa4EcvvfEhVj3s3LNfNDfLOHIVHgB9iuZZKsYKoqGoj1o/iN7G6/d8BrvJxV/0AKMYKXP/YLgf72nbgZ8DrttuiP3/8t7DiIS1YP1B/8Tk2zt53EssF0Qj8Zz9lRAHfwPon2oQVw/hSP+eNhON2/UeBh4FbVHVv35NUdQvwBaz7ehJrUMBNA5T5ItY/4P1Y76uDd7t7VgK7RKQVa8DAdap6xg+tvwD+n32v3zOCR1WfB34DrLb1eWMGnfbj7d79trvvZey4iP2eHwWq7fInDqTT/pOR5VP+oKjqy1jt6G9YvZxiBo99oKqKFTM5jDXoIAFrIMUBW/stWLENwwgQ6/4aDIaBEJFlWCOyJg11bjgiImdhBenjVdUTwHIvBL5su58MYYjpQRgMYxAR+ZBY8x0ysOIGzwTSOACo6jpjHMIbYyAMhrHJF7Hma1RhxaYC5YozRBDGxWQwGAyGfjE9CIPBYDD0i2urazpBdna2FhYWjujatrY2kpNHMnTbWYyu4ROq2oyu4WF0DZ+RaNu6dWuDqvY/ETLYa3s4uZWWlupIWb169YivdRKja/iEqjaja3gYXcNnJNqALWrWYjIYDAbDcDAGwmAwGAz9YgyEwWAwGPrFGAiDwWAw9IsxEAaDwWDoF8cMhIhMttMA7haRXSLy1X7OETtNYKWIvCki5/kcu1GsdJAVItJfakyDwWAwOIiT8yA8wDdV9Q17qeOtIvKSqu72OecKoMTeFmLlBFgoIplYK4rOx1p/fquIPK2qJx3UazAYDAYfHDMQaqWEPGY/bxGRPViZoXwNxNXAQ/ZY3HIRSReRCVh5b19S1SYAEXkJa0nh9yRdiUTauzy8XtnIwcY2Kmq66cg+zoUl2aTER9S8RoOLtHZ6WFfRwKGmNqpquunKsdpYUpxpY4Z3CMpaTHZO2deAs1W12Wf/s8C/qeo6+/UrWGvRL8NK3P5Te/8PgDOq+l/9lH0z1nrw5OXlla5atWpEGltbW0lJSRn6RAc541GequxiTa2Hjp53H4uLhgvzY/hISRzJsdJ/AUEkFO7XQISqtlDQ1datPFHRxdojHrr6tLGEaLh4UgzXlMSRGGPa2ECEqi4Ymbbly5dvVdX5/R1z/O+Cnbbyb8DXfI1DoFDVu4G7AebPn6/Lli0bUTllZWWM9NpAsO3QSb738Bscb/Zwzdx8Pjp/ErMnpLHu9XXkTDuXv209zONvHObNph5+c/1clhRnD12og7h9vwYjVLW5rWtDVSPfenQbJ9t7uPa8yXykdBIzxqeybt06MorO4fEth3ly+xHePBXDb284j/OmZLimFdy/XwMRqrog8NocHcVkp4j8G/Cwqj7RzylHeHc+3En2voH2RyQv7z7B9feUExsdxd++tIRffdwyAGlJsSTHCgumZvLv187h71++gIykWG68fxNP7+gvHbLB0D/P7DjKjfdvIj0plr9/+QL+/do5LJiaSVqi1caWFGfzy4/P5YkvLSEuJopP3FPOy7tPuC3b4DJOjmIS4D5gj6r+coDTngY+bY9mWgSctmMXLwKXi0iGndDkcntfxLG+qoEvPbyVGXmpPHHrkkH/tZ2dn8bjtyxh3pQMvrZqG6/sMV9gw9C8sucEX121jbmT0/nbLUs4Oz9twHPnTcngiS8tYcb4cXzp4a2sr2oY8FxD5ONkD+ICrNywl4jIdnt7v4jcIiK32Oc8B1Rj5b+9B7gVwA5O/wTYbG93egPWkURlXQtf/NNWCrOSeeizC8lOiR/ymrSkWB646XxmT0zjtke2sfPI6SAoNYQrO4+c5rZHtjF7YhoPfOZ80pJih7wmKyWehz67gKnZyXzxoa1UnGgJglJDKOKYgVAr3aCo6hxVnWtvz6nqH1T1D/Y5qqpfVtViVT1HrYTv3uvvV9Vp9vaAUzrdoqO7h9se2UZcdBQPfnaBX19cL8nxMdx/0/lkJMXy5UfeoLUzoJkiDRFCa6eH2x55g/SkWO6/6XyShzEKLi0xlgc/s4D42Chue2QbHd09Q19kiDjMTGqX+Plze9h7vIX/+ti55KcnDvv6nNR4/ue6edQ2tXPHUzsdUGgId+54aieHmtr59XXzyEkdunfal4npifzXR89l34kWfvaPPQ4oNIQ6xkC4wOYDTTy04SCfvWAqy2fkjricBVMzue2SEp7YdoTV++oCqNAQ7qzeV8cT245w2yUlLJiaOeJyls3I5XMXTuVP5QfZVBNxXl7DEBgDEWS6PL18/8m3yE9P5Fsrpo+6vC8vL6Y4J5kfPLWTM30HthvGJGe6erjj7zspzknmy8uLR13eNy+fTn56It9/8i26PL0BUGgIF4yBCDL/t/4A+0+08uMPzg7IrNX4mGh+es05HD55ht+XVQZAoSHc+cOaKmqbzvDTa84hPiZ61OUlxcVw59Wzqahr5cH1NQFQaAgXjIEIIqfbu7lrdSUXT8/hsll5ASt3cXEWV86ZwD1ra6hr7ghYuYbwo66lg3vWVnPlnAksLs4KWLmXnpXHshk5/HZ1FafbuwNWriG0MQYiiPyurJLmjm6+e8XMgJf97ctn0N3Ty/+8UhHwsg3hw69frqDL08u3L58R8LJvXzmT5o5ufmd6qmMGYyCCRF1LBw+uP8CH5uZz1oRxAS+/MDuZGxZO4S+baznU2B7w8g2hT21TO6s21/KJhVMozE4OePlnTRjHh+bl88D6A6anOkYwBiJI3Lu2hu6eXr5yaYljddy6fBrRIvx+TZVjdRhCl9+vqSJahC8vn+ZYHV+5pARPTy/3rjOxiLGAMRBB4GRbF38uP8gHzp3oyD87L3njEvjo/Ek8vrWWY6fPOFaPIfQ4frqDx7cc5qPzJ5E3LsGxegqzk/nAuRP5c/lBTrZ1OVaPITQwBiIIPLj+AO1dPY7+s/Nyy8XFqFo9FsPY4d611fSocsvFox/WOhRfXj6N9q4eHlx/wPG6DO5iDITDdHT38PDGg1w6M5fpeamO1zc5M4kr50zgL5traekwo03GAq2dHv6yuZar5kxgcmaS4/VNz0vl0pm5PLzxoFmCI8IxBsJhntlxlIbWLj574dSg1fmZC6bS2unhr1sOB61Og3v8dUstLZ0ePnNB8NrYZy+cSkNrl1l2PsIxBsJBVJX7Xz/AjLxUlgRwTPpQzJ2cTmlBBg+uP0BPr/MZAw3u0dOrPLj+AKUFGcydnB60epcUZzEjL5X719UQjKyUBncwBsJBttWeYs+xZm5cUoiVHiN4fOaCQg41tfNaRX1Q6zUEl9cq6jnY2M6NSwqDWq+I8JkLCtl7vIWtB08GtW5D8DAGwkEe21xLYmw0Hzh3QtDrvnzWeLJT4nhk46Gg120IHo9uPERWchwrZ48Pet0fOHciKfExpo1FMMZAOERbp4dndhzlyjkTSE3wP9dDoIiLieLa0sm8ureO46fNpKZI5ERzB6/sreOj8ycTFxP8r3JyfAzXzJvIs28d41S7GfIaiTiZcvR+EakTkX6TFYjIt30yze0UkR4RybSPHRCRt+xjW/q7PtT5x1vHaOvq4ePnTx76ZIe4fsFkenqVx7bUuqbB4ByPba6lp1e5foF7bewTCwro8vTyxBsRmzJ+TOPk344HgZUDHVTV//RmmgO+B6zpk1Z0uX18voMaHeOxzbUU5SQzv2DgHNNOU5CVzJLiLJ5447AJJEYYqsoT246wuCiLgiznJl8OxayJ45gzKY0ntpkRc5GIkylHXwP8zTByPfCoU1qCTWVdK1sOnuRj8ycHPTjdl2vm5XOgsZ3ttadc1WEILDsOn6amoY0PnZfvthSumZvPziPNJnd1BCJO/rMUkULgWVU9e5BzkoDDwDRvD0JEaoCTgAJ/VNW7B7n+ZuBmgLy8vNJVq1aNSGtrayspKSkjurYvf9nXxYsHuvnlskTS40dng0erq71b+erqdi6aFMOnZg0/7aRTupwkVLUFUtefdnfy2mEPv16eRFLs6P6EjFbX6U7l62XtvH9qLNdOjxuVlkDqcopQ1QUj07Z8+fKtA3pqVNWxDSgEdg5xzseBZ/rsy7cfc4EdwEX+1FdaWqojZfXq1SO+1pcuT4+W/uQl/fz/bQ5IeYHQdevDW3Xuj1/ULk/P6AXZBOp+OUGoagtkG5t35z/11oe3BqS8QOj69H0bdckvXtGent7RC7KJ9M/RCUaiDdiiA/ymhsIopuvo415S1SP2Yx3wJLDABV0jYkNVIw2tnVxbOsltKW/z4Xn5nGzv5rX9Zk5EJLC2op6mti4+NNd995KXD5+Xz5FTZ9h8wOStjiRcNRAikgZcDPzdZ1+yiKR6nwOXA/2OhApFnn3zKKnxMVw8PcdtKW9z0fQcMpJieXKbGWkSCTy57SgZSbFcFEJt7H2z8kiKi+ap7aaNRRJODnN9FNgAzBCRwyLyORG5RURu8TntQ8A/VbXNZ18esE5EdgCbgH+o6gtO6QwkXZ5eXth5nPfNyiMhdvS5gANFbHQUHzh3Ii/tPkGzWcAvrGnp6Oafu45z1ZyJrsx9GIikuBhWzh7Ps28eMwv4RRBOjmK6XlUnqGqsqk5S1ftU9Q+q+gefcx5U1ev6XFetqufa22xV/ZlTGgPN65UNNHd4uHJO8GdOD8U18/LptA2YIXx5YedxOj29XDMvdNxLXq6Zl09Lh4fVe+vclmIIEKHzFyQCeObNo4xLiGFpSeh0/b3Mm5xOQVYSz5jVN8Oap3ccZUpmEudNCd7CfP6ypDiLnNR4s8JrBGEMRIDo6O7hpV0nWDF7fEh1/b2ICFecPYENVY2cbjdupnDkdHs3G6oaueKc8a7Pr+mPmOgoVszOo2xfPWe6jJspEgi9X7IwZW1FAy2doele8rLy7PF4epVX9p5wW4phBLyy9wSeXnVlYT5/WTl7Ame6e8wqwhGCMRAB4tk3j5KeFMsF07LdljIgc/LTmJCWYOIQYcqLu44zflwC504KPfeSl4VFmaQlxvLiLtPGIgFjIAJAR3cPL+8+wcrZ44mNDt1bGhUlrJg9njX762nv8rgtxzAM2rs8rNlfz4rZeURFhZ57yUtsdBSXnZXHy7tP0N3T67YcwygJ3V+zMOL1ygbaunq44pzQdS95uXx2Hp2eXtbsMy6AcOK1/fV0dPey4uzQdS95WTE7j+YOD+XVjW5LMYwSYyACwMt7TpASH8Oioky3pQzJgsJMMpJiecG4AMKKF3YeJyMplgWFod/GLpqeQ2JstHFlRgDGQIyS3l7l5T11XDw9h/iY0JkcNxAx0VG8b1Yer+6po8tjXADhQJenl1f21PG+WXnEhLAL00tCbDTLZ+bw4q4TJid6mBP6rS3EefPIaepbOrlsVq7bUvxm5dnjaen0sL6qwW0pBj9YX2WNkFsZBu4lLytmj6ehtZNth0y+6nDGGIhR8vLuE0RHCctnhI+BWFKcTUp8jBlpEia8uOs4KfExLCkO3RFyfblkZi5x0VHGzRTmGAMxSl7ec4LzCzNITwrcOvhOkxAbzcXTc3hlT53JNBfiqCqv2C7MUFrfayhSE2JZVJzFK2bZjbDGGIhRUNvUzt7jLVx2Vp7bUobN8pm51LV0sutos9tSDIOw62gzdS2dLJ8ZPj1UL5fMyKGmoY2ahrahTzaEJMZAjIKX91gzkt83K/wMxLIZOYhgFlYLcbyfz7IZobe+11BcMtP6Xpg2Fr4YAzEKXt5zgpLcFFeTxo+U7JR45kxK59V95ssbyry6r45zJ6WRnRK4dLHBYkpWEsU5yaw2bSxsMQZihDR3dLOxuonLwrD34GX5jBy2156isbXTbSmGfmhq62J77amwdC95WT4jl43VTbR1mpn74YgxECNkXUUDnl7l0jD+8l4yMxdVWGNSkYYka/bXoUpYjZDryyUzc+nq6eX1SjOkOhxxMqPc/SJSJyL9pgsVkWUiclpEttvbHT7HVorIPhGpFJHvOqVxNKzZV8+4hBjmTg7dhdOG4uyJluviVeMjDkle3VtPdko85+SnuS1lxMwvzCQlPsa4mcIUJ3sQDwIrhzhnrarOtbc7AUQkGvgtcAUwC7heRGY5qHPYqCpr9teztCQnLGa2DkRUlLBsRg6v7a/HYxZWCyk8Pb2s2VfHshk5Ib0431DExURx4bRsVu+tN0OqwxAnU46+BjSN4NIFQKWderQLWAVcHVBxo2T/iVaON3dwcQgljR8pl8zMpbnDwxuHTrktxeDDttpTNHd4wtq95OWSmbkcb+5gz7EWt6UYhkmMy/UvFpEdwFHgW6q6C8gHan3OOQwsHKgAEbkZuBkgLy+PsrKyEQlpbW31+9rna6yMbLGNFZSVVY2oPn8Zjq4R0a1ECzz4zy20z/B/sp/jukZBqGobjq6/7usiWkBO7KWsbF/I6BoJcZ1W7/S+58v5QHH4t7FQ1QUOaFNVxzagENg5wLFxQIr9/P1Ahf38WuBen/M+BdzlT32lpaU6UlavXu33uZ+4Z4Ou+NWaEdc1HIaja6Rc98fhv59g6BopoaptOLpW/s9r+rE/rHdOjA/BuF9X/WatfuR3rw/rmkj4HIPNSLQBW3SA31TXHOiq2qyqrfbz54BYEckGjgCTfU6dZO8LCdo6PWyuOclFEeBe8rJ0ejZ7j7dQ19zhthQDUNfSwZ5jzZHVxkqy2VZ7ipYOkw89nHDNQIjIeLEzr4vIAltLI7AZKBGRqSISB1wHPO2Wzr6UVzfS1dMbEfEHLxeVWO9lnRmKGBJ4h4R6P5dIYGlJDj29yoYqk0QonHBymOujwAZghogcFpHPicgtInKLfcq1wE47BvEb4Dq7x+MBbgNeBPYAj6kVmwgJ1uyvJzE2mvmFGW5LCRizJowjMzmOdRXGQIQCaysayEiKZfbEcW5LCRjnFaSTFBdt/oSEGY4FqVX1+iGO3wXcNcCx54DnnNA1Wtbsr2dJcVZYJAfyl6go4YJp2bxW0YCqYnfsDC6gqqytaODCkvAe3tqX+JhoFhVlsdb8CQkrwncQvwscaGjjYGM7F4fhwmlDsbQkm4bWTvYeN0MR3WTfiRbqWzpZWhI+uR/8ZWlJNjUNbdQ2tbstxeAnxkAMg7V293hpBPmGvXh/kNZWmGU33GTtfm8bi0wDAZheRBhhDMQwWF/ZQH56IoVZSW5LCTgT0hIpyU0xX16XWVvZwLTcFCakJbotJeAU56QwIS2BdZXmT0i4YAyEn/T2KhuqG1lSnBWxPvoLS7LZVNNER3eP21LGJB3dPWysbozI3gOAiLC0JJt1FQ309JplN8IBYyD8ZPexZk61d3PBtMj88oI1rLLT08vmAyNZIcUwWrYcOEmnpzeihrf2ZWlJDs0dHt48bJZ2CQeMgfAT79j0JcVZLitxjoVFmcRGi3EzucTaynpio4WFRZluS3GMC6ZlI2LiEOGCMRB+sq6ygZLcFHLHJbgtxTGS4mIoLcgwX16XWFfRQGlBBklxbi+R5hyZyXGcPTHNzLkJE4yB8INOTw+bDzRFtHvJy4XTstlzrJmmti63pYwpTrV3sftYMxcUR34bu2BaNttqT9LeZbLMhTrGQPjBtkOn6OjujWj3kpfF9nssrzZLIgSTTTVNqMKiMdLGunuULQdOui3FMATGQPjB+soGogQWFkX+l3fOJGtJhPVVxgUQTDZUN5IQG8WcSeGbPc5fzi/MICZKWG/WZQp5jIHwg9erGpkzKZ20xFi3pThObHQUC6ZmmkXVgkx5dROlBRkRtYTLQCTFxTBvSjobTC815DEGYghaOz3sqD3FBdMiv/fgZUlxFlX1bZwwy38HhVPtXew93syiqWOnjS0uzuatw6doNst/hzTGQAzBpppGPL06JoKHXhYXWe/V9CKCw0Y7/rB4DMQfvCwuyqJXYVO1mXMTygxqIERkkoh8S0T+LiKbReQ1EfmdiFwpImPCuKyraCQ+JorzCiJnee+hmDVxHOMSYkwcIkiUvx1/SHdbStCYNyWd+JgoE4cIcQYccC0iD2Dlh34W+HegDkgApgMrge+LyHdV9bVgCHWL9VUNzC/MICE28n3DXqKjhEVFWcZHHCQ2VDUyvyCTuJgx8Z8LgAQ7p4ppY6HNYC3yv1X1clX9jaquV9VKVd2pqk+o6r8Ay4CjA10sIveLSJ2I7Bzg+A0i8qaIvCUi60XkXJ9jB+z920Vky0jf3GjxLn+9ZAy5l7wsKc6itumMWZrZYU62dbH3eAuLInj29EAsKTZzbkKdwQzEFSIyaaCDqtqlqpWDXP8gVk9jIGqAi1X1HOAnwN19ji9X1bmqOn+QMhzF2/0dCxPk+rK42MQhgsHGGssHv2gMDKHui/c9mzk3octgBmIisEFE1orIrSIyrBXEbNfTgBEou1finSlTDgxojNxifWUDqQkxnJMf+WPT+zI9L4Ws5DgTh3CY8upGEmOjx1T8wcucSWkkmzk3IY2oDrzsrljrWl8EXAdcA+wAHgWeUNUhU4+JSCHwrKqePcR53wJmqurn7dc1wElAgT+qat/ehe+1NwM3A+Tl5ZWuWrVqKFn90traSkpKyrv2fXtNO5NSo/jqee6tv9SfrmDxu+0d7D/Zy6+WJb5niXM3dQ1FqGrrT060oJcAACAASURBVNcPXj/DuDj49vnu5X9w8379cmsH9e29/GLpe3OshNPnGCqMRNvy5cu3DuipUVW/NiAaWAFsA9r9vKYQ2DnEOcuBPUCWz758+zEXyyhd5E99paWlOlJWr179rteHT7Zrwe3P6n1rq0dcZiDoqyuYPFx+UAtuf1Yr61rec8xNXUMRqtr66mpq7dSC25/Vu16tcEeQjZv36+41VVpw+7N6/PSZ9xwLl88xlBiJNmCLDvCb6tewCRE5B7gT+C3QCXxvWCZq4HLnAPcCV6vq245IVT1iP9YBTwILAlHfcNho+0XHom/Yi3dcvhmK6Awba7xtbOwFqL1425iJdYUmAxoIESkRkR+IyC7gYaANuFxVF6nqr0dbsYhMAZ4APqWq+332J4tIqvc5cDnQ70goJymvbiQtMZaZ41ODXXXIUJiVxIS0BDYYH7EjlFc3kRgbzTn5Yy/+4OWsCeNIS4w1cYgQZbCF51/Aijd8XFWH/QMtIo9iDYXNFpHDwA+BWABV/QNwB5AF/M72b3vU8oPlAU/a+2KAR1T1heHWP1o21jSxYGomUVGRmV7UH0SExcVZlO2rp7dXx/S9cILy6kbmF2aMqfkPfbHm3GSa+RAhyoAGQlWLfV+LyDjf81V10Dnyqnr9EMc/D3y+n/3VwLnvvSJ4HD11hoON7Xx6caGbMkKCxUVZPPHGEfadaOGsCePclhMxNNnzHz5w7kS3pbjO4qIsXtx1gtqmdiZnvjdYbXCPIf+6iMgXReQ48Caw1d5cm7wWDIxv+B2W2HNATBwisGyqMTEuL942ZuIQoYc/fdtvAWeraqGqTrW3IqeFucnG6ibGJcQwc7z5x5yfnsiUzCQzmSnAbKjyzn8Ye3Ns+lKSa825MW0s9PDHQFQBY2q9hfLqRhZMzSLa+NwBqye1qaaJ3t6B58wYhkd5dRPzCzOIjR678QcvItbaX+XVjd5h7oYQwZ/W+T1gvYj8UUR+492cFuYWx06f4UBju3Ev+bCoKIvTZ7rZc7zZbSkRQWNrJ/tOtBj3kg+LijI5erqD2qYzbksx+DDYKCYvfwReBd4Cep2V4z4bq8fu2jgD8c6aOU3MnmhcIqNl0xhef2kgfNdlmpJlAtWhgj8GIlZVv+G4khBhY00jqQkxZsSODxPTEynIsuIQn7twqttywp7y6kaS4kz8wZdpuSlkp8SxobqRj50/2W05Bht/XEzPi8jNIjJBRDK9m+PKXKK8uomFUzNN/KEPi6ZmsbG6kR4Thxg1Vvwh08QffBARFpo4RMjhTwu9HjsOQYQPcz1+uoOahjbT9e+HRcWZNHd42HPMxCFGwzvxh4j9jzViFhVlcex0B4dMDpKQYUgXk6qOGZ+Cd/7DwjGUPN5fvPekvLqRs8fg8ueBYiznfxiKxbbRLK9upCAr2WU1Bhh8LaYLB7tQRMaJyKDLeIcb5dVNpMbHMGuiiT/05Z04hEkyPxq88YexmGNkKIpzrDiEaWOhw2A9iI+IyH9grcm0FajHykk9DWuJ7gLgm44rDCIbqxtZYOIPA7K4KIvn3jpm4hCjwFp/ycQf+sMbh9hQZeIQocKArVRVvw5cBRwDPoqVFvQbQAlWEp+LVHVzUFQGgZMdvVSb+MOgLCrKMnGIUdDcqew/0cpi08YGZHFRFsebOzjYaOIQocCgMQh7Qb577C2i2ddkTfFYaIKHA7LQx0c8zWUt4cjekz2AWeNrMHznQ4x3WYvBv1FMY4K9J3us+IOZ/zAgE9ISKcwy6zKNlL1NPSTHRZsg/yAU5ySTnRJv2liIYAyEzd6mHs6fmkmM8Q0PyqKiLDbWNNFrfMTDZm9Tj4k/DIG1LlMm5dVNJg4RApiWCtQ1d3C8TVk41XT9h2JxcRYtHR4ONUf8qisBpaG1k6OtamJcfrDIjkPUtRsD4Tb+5INIslOP3mO/LhGRq/wpXETuF5E6Eek3I51Y/EZEKkXkTRE5z+fYjSJSYW83+vuGRoIZm+4/3vkQe5uMgRgO76zxZf6EDIU3T/Weph6XlRj86UE8AHQCi+3XR4Cf+ln+g8DKQY5fgTUqqgS4Gfg9gL2Uxw+BhcAC4IcikuFnncOmvLqRhGiYbeY/DMn4tASmZiebL+8w8bYxM/9haIqyk8lJjWevaWOu44+BKFbV/wC6AVS1HfBrooCqvgYMNuvlauAhtSgH0kVkArACeElVm1T1JPASgxuaUVFe3cj0zGgTf/CTRUWZ7D/ZY+ZDDIMN1Y1MzzBtzB+8+SH2NvWaOITL+LOaa5eIJAIKICLFWD2KQJAP1Pq8PmzvG2j/exCRm7F6H+Tl5VFWVjYsAV09Cl0dTMvsGfa1waC1tTXkdI3r8HDGA3965lUK06LdlvMeQu2ene5UKuvaubpQQ0qXl1C7XwCZ3d2c6lT+8txqxieHllENxfvlJdDa/DEQP8SaTT1ZRB4GLgBuCpiCUaKqdwN3A8yfP1+XLVs27DIuvxTKysoYybVOE4q6zmru4I9vvkJ3xlSWXRR62WdD7Z49++ZRYBvnjk8MKV1eQu1+AUyub+X/dq9Bc6axbMEUt+W8i1C8X14CrW1I06yqLwEfxjIKjwLzVbUsQPUfAXwXf59k7xtovyEEyBuXwPgkMWPV/aS8upGU+BgKxoXWP+FQpig7mbR4YUOVaWNu4s8opvOw1l06BhwFpohIsYj40/sYiqeBT9ujmRYBp1X1GPAicLmIZNjB6cvtfYYQYWZmNJtqmkwcwg/Kq5s4vzDDrPE1DESEszKjTH4Il/HnL83vgHIsN849wAbgr8A+Ebl8sAtF5FH7/BkiclhEPicit4jILfYpzwHVQKVd9q3w9hIfPwE229ud9j5DiDAzM5qWTg+7jp52W0pIU9fSQWVdqxlCPQJmZkZT19JJTUOb21LGLP70Ao4Cn1PVXQAiMgu4E/gO8ATwz4EuVNXrBytYrb8GXx7g2P3A/X7oM7jAzEzrv0V5dSNzJqW7rCZ08c1xfrKqdoizDb7MzLQGQJRXN1GUk+KymtDltf31HGpq53oHYjX+9CCme40DgKruBmaqanXA1RjChvSEKIpyks3a/UPgjT+YOTbDJy9JyE016zINxaObDvH7sipHXJj+GIhdIvJ7EbnY3n4H7BaReOy5EYaxyaKiLDbXNOHpMbOqB6K8upHzCzPM/IcR4J0PscHEIQZEVdlY0+TYKtT+tNqbsGIEX7O3antfN1biIMMYZVFRFi2dHnab/BD9UtfSQVW9yTEyGhYXZ1Hf0km1iUP0S0VdK01tXY61MX9yUp8B/tve+tIacEWGsGHR1HfyQ5g4xHvxxh+8awsZho9vfohiE4d4D173m1NJqPwZ5loiIo+LyG4RqfZujqgxhBW54xIoykk2Y9UHYEN1o8kxMkoKs5LIGxdvYl0DsLG6iYlpCUzKSHSkfH8X6/s94MFyKT0E/NkRNYawY3FRFpsPnDRxiH4or240OUZGiTcOYeZDvBcr/tDIoqIsRJyZY+NPy01U1VcAUdWDqvoj4EpH1BjCjkVFWbR2eth11MQhfKlr7qC6vs0s7x0AFhVZcYiqehOH8KWqvpWG1i5H0yT7YyA6RSQKqBCR20TkQ4BxBhqAd+epNrxDuckxEjB84xCGd9hQ7Xwb88dAfBVIAr4ClAKfBD7tmCJDWJGbmkBxTrL58vah3MQfAkZhVhLjxyWYNtaH8upGJqQlMCUzybE6/DEQharaqqqHVfUzqvoRILSWVzS4yiITh3gP5VWNLDDxh4Bg8lS/F1VlY3UTC6dmOhZ/AP8MxPf83GcYoywutuIQO00cAoATzR1UN5j5D4FkUVEWDa0mDuGlqr6NhtZOx9vYgPMgROQK4P1Avoj8xufQOKwRTQYD8E6e6vLqRuZONvMhvK4QYyACh/debqhuZFquCYFurAlOGxusB3EU2Ap02I/e7WmslKAGAwA5qfFMy00xPmKbt+MPZv2lgFFg4hDvory6ibxx8RRkORd/gEF6EKq6A9ghIn9WVdNjMAzKoqJMnnzjCJ6e3jHvd99Q1cjCokyT/yGAiAiLi7NYW1GPqjrqdw91VJUNVQ1cOC3b8fsw4DdZRN4SkTeBN0Tkzb6bo6oMYceioizaunrGfBziyKkzHGhsZ3FxtttSIo5FRZk0tHZRVT+2V/ipqLPmPywJQhsbbC2mqxyv3RAxvO0jrhrbcQjvsiNLzPpLAeedOEQT03JTXVbjHt42Fow1vgbsQdizpg+q6kGsOMQ59nbG3jckIrJSRPaJSKWIfLef478Ske32tl9ETvkc6/E59vTw35ohmGSnxFNi4hBsqGokMzmOGXlj9wfMKaZkJjEhzcQh1lc1MDkzkckOzn/w4s9ifR8DNgEfBT4GbBSRa/24Lhr4LXAFMAu43s5G9zaq+nVVnauqc4H/xcpQ5+WM95iqftDvd2RwjUVFWWw50ET3GJ0P4fUNLyrKJMrEHwKOd12mjWN4XaaeXqW8usmx1Vv74k808fvA+ap6o6p+GlgA/MCP6xYAlaparapdwCrg6kHOvx541I9yDSHK23GII2MzT/XBxnaOnu4w8QcH8cYhKuvGZhxiz7FmTp/pDkr8AfzLSR2lqnU+rxvxz7DkA75JeA8DC/s7UUQKgKnAqz67E0RkC9aci39T1acGuPZm4GaAvLw8ysrK/JD2XlpbW0d8rZOEk66eTutf3SMvb+Z0UZwLqizcumdltVaCxZiGKsrKat5zPJw+y1CgP13SbvVOH3qxnEunxLqgyt379XyN1cb0xD7Kyireczzg2lR10A34T+BFrCxyNwHPA//ux3XXAvf6vP4UcNcA594O/G+fffn2YxFwACgeqs7S0lIdKatXrx7xtU4Sbrre98sy/fR9G4Mrpg9u3bMvP7xVF/zsJe3t7e33eLh9lm7Tn67e3l5d/POX9dY/bw2+IBs379dN92/US/5r4PpHog3YogP8pg7ZE1DVbwN/BObY292qersftucIMNnn9SR7X39cRx/3kqoesR+rgTJgnh91GlzGWpdp7MUhVJXy6kYWO7g2v2Fs54fo7ullU01TUDMU+hOk/gawUVW/YW9P+ln2ZqBERKaKSByWEXjPaCQRmQlkABt89mWISLz9PBu4ANjtZ70GF1lUlEV7Vw9vjbE4RDDHpo91FhVl0djWRcUYi0O8efg0bV09QW1j/sQSUoF/ishaOx9Enj8FqzX7+jYs99Qe4DFV3SUid4qI76ik64BV+u6/A2cBW0RkB7AaKwZhDEQYsGDq2MwPsb6yATD5p4PBWM0P4cYaX0MGqVX1x8CPRWQO8HFgjYgcVtXL/Lj2OeC5Pvvu6PP6R/1ctx5rzoUhzMhOiWd6Xgrl1U3cusxtNcFjfVVj0Mamj3UmZyaSn55IeXUjn15c6LacoLG+qoGzJowjMzl4A0CGs2hOHXAcaxRTrjNyDJHA4jE2H8Iam94YtLHpYx0RYeEYyw/R0d3DlgMng97G/IlB3CoiZcArQBbwBVWd47QwQ/gy1uIQe44109zhMfGHILKoKIumMRSH2HboFJ2e3qAv4eJPD2Iy8DVVna2qPzKxAMNQeOMQ3jVjIp31VSb+EGwW+6z9NRbYUN1IlMACOwd8sPBnmOv3VHV7MMQYIoOslHhm5KWOmSDi+qpGinOSyRuX4LaUMcOkjHfiEGOBDVUNnDMpnXEJwZ0cOLYX7jc4xqKiTLYcOBnxcQg3xqYb3olDbKxporc3suMQ7V0eth065UqMyxgIgyMsLs7iTHcPO2pPDX1yGLOj9hTtQR6bbrBYbMch9p1ocVuKo2yqacLTq64sIW8MhMERFhdlEyWwtqLBbSmOsraiARGT/8ENLiyxjPK6CG9j6yoaiIuJ4vzC4MYfwBgIg0OkJcUyZ1I6ayvq3ZbiKGsr6pkzKZ30JPcWJxyrTEhLZFpuCq9FfBtrYEFhJolx0UGv2xgIg2NcVJLN9tpTnD7T7bYURzh9ppvttae4qMS4l9xiaUk2m2qa6OjucVuKI5xo7mDfiRaWutTGjIEwOMbS6Tn0auQORdxQ1UivwoXTjIFwi4tKcuj09LLlwEm3pTiC1312oTEQhkhj7uR0UuJjItbNtLainuS4aOZNyXBbyphlYVEmsdES0W0sOyWOs8aPc6V+YyAMjhEbHcWioqyIDVSvrWhgcXEWcTHma+QWSXExlBZk8FoEtrHeXmVdZQMXTst2LYWtadkGR7loejaHmto52NjmtpSAcrCxjUNN7SwtyXFbyphnaUkOe441U9/S6baUgLL3eAsNrV2utjFjIAyO4vXPR1ovYq3LvmHDO3gDuK9XRlobs9xmbrYxYyAMjjI1O5n89MSI8xGvragnPz2Rouxkt6WMeWZPTCMjKTbihruurWhgRl6qq0u4OGogRGSliOwTkUoR+W4/x28SkXoR2W5vn/c5dqOIVNjbjU7qNDiHiHDR9GzWVzXiiZBlNzw9vayvamRpSbZJLxoCREcJF0zLZl1FQ8Qs/93R3cOmA02uDW/14piBEJFo4LfAFcAs4HoRmdXPqX9R1bn2dq99bSbwQ2AhsAD4oYiYoSJhytKSHFo6POw4HBnLbuw4fIqWDo9xL4UQF5XkUNfSGTHLbmysaaLL0+t6G3OyB7EAqFTValXtAlYBV/t57QrgJVVtUtWTwEvASod0GhzmgmnZREcJq/dGhgtg9d56oqOEpdNMgDpUuGi69VlEThurIyE2KqjpRftjyJSjoyAfqPV5fRirR9CXj4jIRcB+4OuqWjvAtfn9VSIiNwM3A+Tl5VFWVjYisa2trSO+1kkiRde0NOHpLdXMjz/mnCgbp+/Z01vOUJwmbNv0+rCui5TPMlgMV9eU1Cie3Lifs9710xF4nL5fqspz288wIz2K8tfXDuvagGtTVUc24FrgXp/XnwLu6nNOFhBvP/8i8Kr9/FvA//M57wfAt4aqs7S0VEfK6tWrR3ytk0SKrt+trtSC25/VY6fOOCPIByfv2bFTZ7Tg9mf1t6srhn1tpHyWwWK4uv7jhT1a9L1/6Km2LmcE2Th9vyrrWrTg9mf1ofU1w752JNqALTrAb6qTLqYjWNnovEyy972Nqjaqqnfw8r1Aqb/XGsKLS2ZaaczL9tW5rGR0ePV7348hdLhkZi49vRr2o5lW77Xa2PIQaGNOGojNQImITBWROOA64GnfE0Rkgs/LDwJ77OcvApeLSIYdnL7c3mcIU6bnpZCfnsire8PbQLy6t46JaQnMyEt1W4qhD3MnZ5CRFPv2D2y4snpfHdPzUpiUkeS2FOcMhKp6gNuwftj3AI+p6i4RuVNEPmif9hUR2SUiO4CvADfZ1zYBP8EyMpuBO+19hjBFRFg+M4d1lQ10esJz5c1OTw/rKhtYPjPXDG8NQaKjhIun51C2v56eMM0y19rpYVNNU0j0HsDheRCq+pyqTlfVYlX9mb3vDlV92n7+PVWdrarnqupyVd3rc+39qjrN3h5wUqchOCyfkUt7Vw+basLT1m+qaaK9q4flM0Ljy2t4L8tn5tLU1hW2Q6rXVdTT3aMh08bMTGpD0FhSnE18TFTYDkVcvbeeuJgolkwz2eNClYun5xAlUBambqbVe+tJTbAWIAwFjIEwBI3EuGgWF2exOkwD1av31bG4KIukOCdHhxtGQ3pSHOdNyeDVMGxjqsrqfXVcND2H2OjQ+GkODRWGMcPyGbnUNLRR0xBeq7t6NS+fYSbHhTrLZ+ay80gzdc0dbksZFruONlPX0hky7iUwBsIQZLzDQ1/efcJlJcPDq/fSs/JcVmIYirfb2J7w6kW8tPsEIrAshP6EGANhCCqTM5OYPXEcL+w67raUYfHCruPMmjCOyZnuDz00DM7M8akUZCWFXRt7cddxzi/MJDsl3m0pb2MMhCHorJw9nq0HT4aNC6CuuYOtB0+y8uzxbksx+IGIsHL2eNZXNnD6TLfbcvyipqGNvcdbWDk7tNqYMRCGoOP9oX0xTNxMXp3GQIQPK84ej6dXeXVvmLQxu7ezIsTamDEQhqAzLTeFopxkXtwZHi6AF3cepyg7mZLcFLelGPxk7qR08sbF80KYtLEXdh5nzqQ08tMT3ZbyLoyBMAQdrwtgQ3Ujp9q73JYzKKfauyivbmTF2ePN7OkwIipKWDF7PGv213OmK7Rn7h87fYbttadYEWLuJTAGwuASK2aPp6dXQ36kySt76vD0akh+eQ2Ds2L2eDq6e1mzP7QnZv5zl+UGC8U2ZgyEwRXmTEpjQlpCyLsAXth1nAlpCczJT3NbimGYLJiaSXpS7Nv+/VDlhZ3HmZabwrQQdGEaA2FwBRHLBfBaRT1tnR635fRLW6eH1/bXs2L2eKKijHsp3IiNjuKys/J4ec8JujyhmQ+9qa2LjTWNITd6yYsxEAbXWHn2eLo8vSG7BHjZvno6Pb1cPttMjgtXVs4eT0uHh9erGtyW0i8v7T5Or4amewmMgTC4yPmFmeSmxvP37UfdltIvT20/Qm5qPAunmsX5wpWl07MZlxDD06HaxrYdpTAribPzx7ktpV+MgTC4RnSUcPXciZTtq6OpLbRGM51s66JsXx1Xz51ItHEvhS3xMdFcOWciL+w8HnKuzKOnzlBe08g18/JDdoScMRAGV/nQvEl4epV/vHXMbSnv4h9vHaO7R7lmXr7bUgyj5EPz8jnT3cNLITYx8+kdR1G19IUqjhoIEVkpIvtEpFJEvtvP8W+IyG4ReVNEXhGRAp9jPSKy3d6e7nutITI4a0IqM/JSeWpbaKUcf3LbEabnpTBrQmh2/Q3+M78gg/z0RJ4IsTb21LYjnDclnYKsZLelDIhjBkJEooHfAlcAs4DrRWRWn9O2AfNVdQ7wOPAfPsfOqOpce/sghohERLhmXj5bD57kUGO723IAONTYztaDJ0O662/wn6go4Zp5E1lXUU9dS2is/7XnWDN7j7eEdO8BnO1BLAAqVbVaVbuAVcDVvieo6mpV9f4qlAOTHNRjCFGunjsRsILCoYBXxzVzQ/vLa/CfD83Lp1fhmR2h4cp8atsRYqKEK+dMdFvKoIiqM8m9ReRaYKWqft5+/SlgoareNsD5dwHHVfWn9msPsB3wAP+mqk8NcN3NwM0AeXl5patWrRqR3tbWVlJSQm+iyljR9W+bznCqQ/nF0sRR/2sfjTZV5Xtrz5CeIHx3QWDXxRkrn2WgCLSuH60/Yz0uGd3nOlpdvap8s+wMBeOi+Fppwqi09GUk2pYvX75VVef3e1BVHdmAa4F7fV5/CrhrgHM/idWDiPfZl28/FgEHgOKh6iwtLdWRsnr16hFf6yRjRdeqTQe14PZndduhk6MuazTath86qQW3P6urNh0ctY6+jJXPMlAEWte9a6u14PZnteJE86jKGa2u1yvqteD2Z/WZHUdGVU5/jEQbsEUH+E110sV0BJjs83qSve9diMhlwPeBD6pqp3e/qh6xH6uBMmCeg1oNLnPFOROIj4nisS21rur4y5Za4mOiWHn2BFd1GALPB8+dSEyU8NiWw67q+MuWWlITYrgsDLITOmkgNgMlIjJVROKA64B3jUYSkXnAH7GMQ53P/gwRibefZwMXALsd1GpwmXEJsVw1ZyJ/33bEtfHqbZ0e/r7tCFfNmUhaYqwrGgzOkZMaz2Vn5fH41sN0etxZ4bWprYvn3zrOh+flkxAb7YqG4eCYgVBVD3Ab8CKwB3hMVXeJyJ0i4h2V9J9ACvDXPsNZzwK2iMgOYDVWDMIYiAjnEwun0NbVw9M73Jn1+vSOo7R19fCJhVNcqd/gPJ9YOIWmti5e3OXOnIi/bT1MV08vn1hYMPTJIUCMk4Wr6nPAc3323eHz/LIBrlsPnOOkNkPocd6UdGaOT+XhjQe57vzJQR1iqqo8svEQM8enct6U9KDVawguF07LZnJmIg+XH+SD5wZ3BFFvr/LopkOUFmQwY3xqUOseKWYmtSFkEBFuWFTAziPNvHHoZFDrfuPQSd46cpobFk4xcx8imKgo4RMLCthY08SeY81BrXttZQPVDW3cEEY9VGMgDCHFR87LZ1xCDPevOxDUeu9fd4BxCTF8+DwzFSfSuX7BZBJjo3ng9Zqg1nv/uhpyUuO5KsTnPvhiDIQhpEiKi+H6hVN4fucxDp8MzszqwyfbeX7nMa5fOIXkeEe9roYQID0pjo+U5vPU9qM0tHYOfUEAqKxrYc3+ej69qIC4mPD52Q0fpYYxw42LCxGRoPUiHnj9ACLCpxcXBqU+g/vctGQqXZ5eHlp/ICj13bu2hviYqLAbAGEMhCHkmJieyDVz83lk00EaHf6H19jaycMbD3L13Inkpwd25rQhdJmWm8KK2Xk8uP4AzR3djtZ15NQZ/vbGYa47fzJZKfGO1hVojIEwhCS3Li+m09PLfeuc9RPft66GTk8vty6b5mg9htDjtuUlNHd4+NOGg47Wc/eaKlTh5ouLHa3HCYyBMIQkxTkpvP+cCTy04aBjyYSa2rp4aMNB3n/2hJBMGG9wlnMmpXHx9BzuW1dDi0O9iOOnO1i1uZaPnDcpLHuoxkAYQpavXVpCe5eHu16tdKT8u16tpL3Lw1cvK3GkfEPo8433TaeprYt7Xqt2pPxfvbQfVbjtkvDsoRoDYQhZSvJS+WjpZP5UfoDapsCOaKptaudP5Qe4tnQS0/PCY9KSIfCcOzmdK8+ZwD1rawKeK6LiRAt/3VrLJxcVMDkzKaBlBwtjIAwhzdffN53oKOFn/9gT0HJ//tweokT4+vumB7RcQ/jx7RUz6O7p5T9e2BewMlWVO5/dTXJcTNj2HsAYCEOIMz4tgX+5pIQXdh3n1b2BWT/n1b0neH7ncb5yaQkT0sLPL2wILIXZyXx+aRGPbz3MxurGgJT5zJvHWFvRwDcvn05mclxAynQDYyAMIc8XlhZRkpvCD57aResoV3pt7fRwx993UZKbwheWFgVIoSHc+eqlJUzKSORfn3yLGSFswgAADCtJREFUju7RrfR6qr2Lnzy7mzmT0vhUmM+tMQbCEPLExUTxbx85h2Onz3DHUztHVdYdf9/J0VNn+MWHzwmrGa0GZ0mMi+bnHzqHqvo2fv7cyN2Zqsp3Hn+TU+1d/PxD5xAdFd7replviCEsKC3I5CuXlvDEtiP8dYRJhR7fepgn3jjCVy4tYX5hZoAVGsKdi6bn8PkLp/LQhoM8/9bIclc/tOEg/9x9gttXzuTs/LQAKww+xkAYwoZ/uaSEJcVZ/OuTb7G+qmFY166vauB7T7zJ4qIsblsevkFDg7N8Z+VM5k5O5+uPbWfbMFcUfmXPCX78zC4unZnLZy+Y6pDC4GIMhCFsiI4Sfv/JUqZmJ/PFh7ayqabJr+s2H2jii3/aSmFWMn/4ZCkx0abZG/onLiaKe2+cT25qAp99cDNvHj7l13VrK+q57ZFtzJ6Yxm+un0dUmLuWvDj6TRGRlSKyT0QqReS7/RyPF5G/2Mc3ikihz7Hv2fv3icgKJ3Uawoe0xFge/MwCcsbF88n7NvK3rYex8q6/F1XliTcOc8O9G8lJjefBzy4gLcmkEjUMTnZKPA99dgHJ8TFcd3c5zw3iblJVHt54kM88sJmCrCTuv+n8iFoR2DEDISLRwG+BK4BZwPUiMqvPaZ8DTqrqNOBXwL/b187CymE9G1gJ/M4uz2BgYnoif7tlCXMnp/PNv+7gxgc2s66igd5ey1D0qvJ6ZQM3PrCZbzy2g7mT0vnbLUvCcqkDgzsUZifzxK1LKMlN4daH3+Dz/7eF8urGt9tYT69Stq+O6+8p5/tP7mRxcRaP3bKYnNTwWoxvKJw0dQuASlWtBhCRVcDVgG9u6auBH9nPHwfuEiud19XAKlXtBGpEpNIub4ODeg1hREZyHI9+YRF/2nCAX71cwSfv20h8TBTZKfHUNZ+hu3cjaYmx/PADs/j04sKwH01iCD65qQk8/qUl3Leuht++WsnLe06QEBtFcrTS8tILdPX0kpkcxy8+fA4fnz85YtxKvshA3fNRFyxyLbBSVT9vv/4UsFBVb/M5Z6d9zmH7dRWwEMtolKvqn+399wHPq+rj/dRzM3AzQF5eXumqVatGpLe1tZWUlNBbsM3oGpquHmV7XQ/Vp3s53dlLUpSH6dkJzMuNJi46dL60oXTPfDG6hqazR9l6ooeDzT00tXWTnRJHcVoUc3OjiQkhwzCSe7Z8+fKtqjq/v2Nh7yxT1buBuwHmz5+vy5YtG1E5ZWVljPRaJzG6/ONyn+ehps2L0TU8Qk2XNxAaarp8CbQ2J4PUR4DJPq8n2fv6PUdEYoA0oNHPaw0Gg8HgIE4aiM1AiYhMFZE4rKDz033OeRq40X5+LfCqWj6vp4Hr7FFOU4ESYJODWg0Gg8HQB8dcTKrqEZHbgBeBaOB+Vd0lIncCW1T1aeA+4E92ELoJy4hgn/cYVkDbA3xZVUe3QIrBYDAYhoWjMQhVfQ54rs++O3yedwAfHeDanwE/c1KfwWAwGAbGTCk1GAwGQ78YA2EwGAyGfjEGwmAwGAz9YgyEwWAwGPrFsZnUbiAi9cDBEV6eDQxvDengYHQNn1DVZnQND6Nr+IxEW4Gq5vR3IKIMxGgQkS0DTTd3E6Nr+ISqNqNreBhdwyfQ2oyLyWAwGAz9YgyEwWAwGPrFGIh3uNttAQNgdA2fUNVmdA0Po2v4BFSbiUEYDAaDoV9MD8JgMBgM/WIMhMFgMBj6JeINhIisFJF9IlIpIt/t53i8iPzFPr5RRAr/f3vnH2NXUcXxz1fENkUCLY2xIL9aJQ1FSlsErRVBTQo1UJSQlECkUqIVIRoDCaZJY0xUkv6hEjDGEIMkpghViUUxtlKBdNmSgm0XBEq7JWhDLFYobTDLr+Mfcx5Mr/e9fd2+ubtZzye52bnz4853zz3vzZ07u2eysm97/rOSFlbbNqDtW5L+JmmbpD9LOjkre0vSFj+qYdRL61oq6aWs/2uzsqslPefH1dW2hXX9MNO0XdIrWVlJe/1c0h7fIbGuXJJudd3bJM3NykraazhdV7qeAUl9kmZnZc97/hZJmxvWdb6kfdn9WpmVdfSBwrpuyjQ96T41xctK2utESRv8u+ApSd+oqVPGx8xs3B6kMOM7genA+4CtwOmVOtcBP/X0EuBXnj7d608ATvXrHNGwtguASZ7+Wkubnx8YRZstBW6raTsFGPSfkz09uSldlfo3kELMF7WXX/s8YC7wZJvyRcADgICPA5tK26tLXfNb/QEXtXT5+fPA1FGy1/nA/YfrA73WVal7MWn/mibsNQ2Y6+mjge01n8kiPjbeZxDnADvMbNDMXgfuBhZX6iwGfuHpNcBnJcnz7zazITPbBezw6zWmzcw2mNlrftpP2lmvNN3YrB0LgXVm9m8zexlYB1w4SrquAFb3qO+OmNnDpP1M2rEYuMsS/cCxkqZR1l7D6jKzPu8XmvOvbuzVjsPxzV7ratK/XjSzJzy9H3gaOKFSrYiPjfcB4gTg79n5P/hfw75Tx8zeBPYBx3XZtrS2nGWkJ4QWEyVtltQv6dJR0HWZT2XXSGptD1vSZl1f21/FnQo8mGWXslc3tNNe2scOhap/GfAnSY9L+soo6PmEpK2SHpA0y/PGhL0kTSJ9yf46y27EXkqvwOcAmypFRXys6IZBQW+QdBVwNvDpLPtkM9staTrwoKQBM9vZkKS1wGozG5L0VdIM7DMN9d0NS4A1dvAuhKNprzGNpAtIA8SCLHuB2+sDwDpJz/gTdhM8QbpfByQtAu4jbTs8VrgY2Ghm+WyjuL0kvZ80KH3TzF7t5bXbMd5nELuBE7PzD3lebR1J7wWOAfZ22ba0NiR9DlgBXGJmQ618M9vtPweBv5CeKhrRZWZ7My13APO6bVtSV8YSKtP/gvbqhnbaS/vYsEg6k3QPF5vZ3lZ+Zq89wG/p7evVjpjZq2Z2wNN/AI6UNJUxYC+nk38VsZekI0mDwy/N7Dc1Vcr4WIlFlbFykGZIg6TXDa1FrVmVOl/n4EXqezw9i4MXqQfp7SJ1N9rmkBblPlLJnwxM8PRU4Dl6tFjXpa5pWfoLQL+9uyC2y/VN9vSUpnR5vZmkBUM1Ya+sj1Nov+j6eQ5eQHystL261HUSaW1tfiX/KODoLN0HXNigrg+27h/pi/YFt11XPlBKl5cfQ1qnOKope/nvfhfwow51ivhYzww7Vg/S6v520hftCs/7LumJHGAicK9/UB4DpmdtV3i7Z4GLRkHbeuCfwBY/fuf584EB/4AMAMsa1vUD4CnvfwMwM2t7jdtyB/DlJnX5+XeAWyrtSttrNfAi8AbpHe8yYDmw3MsF3O66B4CzG7LXcLruAF7O/Guz5093W231+7yiYV3XZ/7VTzaA1flAU7q8zlLSH6/k7UrbawFpjWNbdq8WNeFjEWojCIIgqGW8r0EEQRAEIyQGiCAIgqCWGCCCIAiCWmKACIIgCGqJASIIgiCoJQaIIGiDpGMlXZedHy9pTaG+Ls2jltaUf1TSnSX6DoJ2xJ+5BkEbPO7N/WZ2RgN99ZH+n+NfHeqsB64xsxdK6wkCiBlEEHTiFmCGx/hfJemU1l4BSnti3Cdpne8FcL3S/h1/9YCArX0CZkj6owdxe0TSzGonkk4DhlqDg6TLfb+BrZLyeD5rSf/tHwSNEANEELTnZmCnmZ1lZjfVlJ8BfBH4GPA94DUzmwM8CnzJ6/wMuMHM5gE3Aj+puc4nSQHqWqwEFprZbOCSLH8z8KnD+H2C4JCIaK5BMHI2WIrPv1/SPtITPqRQB2d69M35wL1pixEgxfaqMg14KTvfCNwp6R4gD8y2Bzi+h/qDoCMxQATByBnK0m9n52+TPlvvAV4xs7OGuc5/SEHgADCz5ZLOJQVge1zSPEuRVid63SBohHjFFATt2U/a4nFEWIrZv0vS5fDOvsGza6o+DXy4dSJphpltMrOVpJlFK1zzaUDtfslBUIIYIIKgDf7UvtEXjFeN8DJXAssktSJ91m2R+TAwR+++h1olacAXxPtIUUIh7VH++xHqCIJDJv7MNQjGAJJ+DKw1s/VtyicAD5F2LnuzUXHB/y0xgwiCscH3gUkdyk8Cbo7BIWiSmEEEQRAEtcQMIgiCIKglBoggCIKglhgggiAIglpigAiCIAhqiQEiCIIgqOW/sMLhWL1Rt5wAAAAASUVORK5CYII=\n", + "image/svg+xml": [ + "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n", + "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n", + " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n", + "<!-- Created with matplotlib (https://matplotlib.org/) -->\n", + "<svg height=\"277.314375pt\" version=\"1.1\" viewBox=\"0 0 392.14375 277.314375\" width=\"392.14375pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n", + " <defs>\n", + " <style type=\"text/css\">\n", + "*{stroke-linecap:butt;stroke-linejoin:round;}\n", + " </style>\n", + " </defs>\n", + " <g id=\"figure_1\">\n", + " <g id=\"patch_1\">\n", + " <path d=\"M 0 277.314375 \n", + "L 392.14375 277.314375 \n", + "L 392.14375 0 \n", + "L 0 0 \n", + "z\n", + "\" style=\"fill:none;\"/>\n", + " </g>\n", + " <g id=\"axes_1\">\n", + " <g id=\"patch_2\">\n", + " <path d=\"M 50.14375 239.758125 \n", + "L 384.94375 239.758125 \n", + "L 384.94375 22.318125 \n", + "L 50.14375 22.318125 \n", + "z\n", + "\" style=\"fill:#ffffff;\"/>\n", + " </g>\n", + " <g id=\"matplotlib.axis_1\">\n", + " <g id=\"xtick_1\">\n", + " <g id=\"line2d_1\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 65.361932 239.758125 \n", + "L 65.361932 22.318125 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_2\">\n", + " <defs>\n", + " <path d=\"M 0 0 \n", + "L 0 3.5 \n", + "\" id=\"m3ed7cd1696\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n", + " </defs>\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"65.361932\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_1\">\n", + " <!-- 0.00 -->\n", + " <defs>\n", + " <path d=\"M 31.78125 66.40625 \n", + "Q 24.171875 66.40625 20.328125 58.90625 \n", + "Q 16.5 51.421875 16.5 36.375 \n", + "Q 16.5 21.390625 20.328125 13.890625 \n", + "Q 24.171875 6.390625 31.78125 6.390625 \n", + "Q 39.453125 6.390625 43.28125 13.890625 \n", + "Q 47.125 21.390625 47.125 36.375 \n", + "Q 47.125 51.421875 43.28125 58.90625 \n", + "Q 39.453125 66.40625 31.78125 66.40625 \n", + "z\n", + "M 31.78125 74.21875 \n", + "Q 44.046875 74.21875 50.515625 64.515625 \n", + "Q 56.984375 54.828125 56.984375 36.375 \n", + "Q 56.984375 17.96875 50.515625 8.265625 \n", + "Q 44.046875 -1.421875 31.78125 -1.421875 \n", + "Q 19.53125 -1.421875 13.0625 8.265625 \n", + "Q 6.59375 17.96875 6.59375 36.375 \n", + "Q 6.59375 54.828125 13.0625 64.515625 \n", + "Q 19.53125 74.21875 31.78125 74.21875 \n", + "z\n", + "\" id=\"DejaVuSans-48\"/>\n", + " <path d=\"M 10.6875 12.40625 \n", + "L 21 12.40625 \n", + "L 21 0 \n", + "L 10.6875 0 \n", + "z\n", + "\" id=\"DejaVuSans-46\"/>\n", + " </defs>\n", + " <g transform=\"translate(54.229119 254.356562)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"xtick_2\">\n", + " <g id=\"line2d_3\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 103.59857 239.758125 \n", + "L 103.59857 22.318125 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_4\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"103.59857\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_2\">\n", + " <!-- 0.25 -->\n", + " <defs>\n", + " <path d=\"M 19.1875 8.296875 \n", + "L 53.609375 8.296875 \n", + "L 53.609375 0 \n", + "L 7.328125 0 \n", + "L 7.328125 8.296875 \n", + "Q 12.9375 14.109375 22.625 23.890625 \n", + "Q 32.328125 33.6875 34.8125 36.53125 \n", + "Q 39.546875 41.84375 41.421875 45.53125 \n", + "Q 43.3125 49.21875 43.3125 52.78125 \n", + "Q 43.3125 58.59375 39.234375 62.25 \n", + "Q 35.15625 65.921875 28.609375 65.921875 \n", + "Q 23.96875 65.921875 18.8125 64.3125 \n", + "Q 13.671875 62.703125 7.8125 59.421875 \n", + "L 7.8125 69.390625 \n", + "Q 13.765625 71.78125 18.9375 73 \n", + "Q 24.125 74.21875 28.421875 74.21875 \n", + "Q 39.75 74.21875 46.484375 68.546875 \n", + "Q 53.21875 62.890625 53.21875 53.421875 \n", + "Q 53.21875 48.921875 51.53125 44.890625 \n", + "Q 49.859375 40.875 45.40625 35.40625 \n", + "Q 44.1875 33.984375 37.640625 27.21875 \n", + "Q 31.109375 20.453125 19.1875 8.296875 \n", + "z\n", + "\" id=\"DejaVuSans-50\"/>\n", + " <path d=\"M 10.796875 72.90625 \n", + "L 49.515625 72.90625 \n", + "L 49.515625 64.59375 \n", + "L 19.828125 64.59375 \n", + "L 19.828125 46.734375 \n", + "Q 21.96875 47.46875 24.109375 47.828125 \n", + "Q 26.265625 48.1875 28.421875 48.1875 \n", + "Q 40.625 48.1875 47.75 41.5 \n", + "Q 54.890625 34.8125 54.890625 23.390625 \n", + "Q 54.890625 11.625 47.5625 5.09375 \n", + "Q 40.234375 -1.421875 26.90625 -1.421875 \n", + "Q 22.3125 -1.421875 17.546875 -0.640625 \n", + "Q 12.796875 0.140625 7.71875 1.703125 \n", + "L 7.71875 11.625 \n", + "Q 12.109375 9.234375 16.796875 8.0625 \n", + "Q 21.484375 6.890625 26.703125 6.890625 \n", + "Q 35.15625 6.890625 40.078125 11.328125 \n", + "Q 45.015625 15.765625 45.015625 23.390625 \n", + "Q 45.015625 31 40.078125 35.4375 \n", + "Q 35.15625 39.890625 26.703125 39.890625 \n", + "Q 22.75 39.890625 18.8125 39.015625 \n", + "Q 14.890625 38.140625 10.796875 36.28125 \n", + "z\n", + "\" id=\"DejaVuSans-53\"/>\n", + " </defs>\n", + " <g transform=\"translate(92.465757 254.356562)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"xtick_3\">\n", + " <g id=\"line2d_5\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 141.835207 239.758125 \n", + "L 141.835207 22.318125 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_6\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"141.835207\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_3\">\n", + " <!-- 0.50 -->\n", + " <g transform=\"translate(130.702395 254.356562)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"xtick_4\">\n", + " <g id=\"line2d_7\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 180.071845 239.758125 \n", + "L 180.071845 22.318125 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_8\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"180.071845\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_4\">\n", + " <!-- 0.75 -->\n", + " <defs>\n", + " <path d=\"M 8.203125 72.90625 \n", + "L 55.078125 72.90625 \n", + "L 55.078125 68.703125 \n", + "L 28.609375 0 \n", + "L 18.3125 0 \n", + "L 43.21875 64.59375 \n", + "L 8.203125 64.59375 \n", + "z\n", + "\" id=\"DejaVuSans-55\"/>\n", + " </defs>\n", + " <g transform=\"translate(168.939033 254.356562)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"xtick_5\">\n", + " <g id=\"line2d_9\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 218.308483 239.758125 \n", + "L 218.308483 22.318125 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_10\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"218.308483\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_5\">\n", + " <!-- 1.00 -->\n", + " <defs>\n", + " <path d=\"M 12.40625 8.296875 \n", + "L 28.515625 8.296875 \n", + "L 28.515625 63.921875 \n", + "L 10.984375 60.40625 \n", + "L 10.984375 69.390625 \n", + "L 28.421875 72.90625 \n", + "L 38.28125 72.90625 \n", + "L 38.28125 8.296875 \n", + "L 54.390625 8.296875 \n", + "L 54.390625 0 \n", + "L 12.40625 0 \n", + "z\n", + "\" id=\"DejaVuSans-49\"/>\n", + " </defs>\n", + " <g transform=\"translate(207.17567 254.356562)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-49\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"xtick_6\">\n", + " <g id=\"line2d_11\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 256.54512 239.758125 \n", + "L 256.54512 22.318125 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_12\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"256.54512\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_6\">\n", + " <!-- 1.25 -->\n", + " <g transform=\"translate(245.412308 254.356562)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-49\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"xtick_7\">\n", + " <g id=\"line2d_13\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 294.781758 239.758125 \n", + "L 294.781758 22.318125 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_14\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"294.781758\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_7\">\n", + " <!-- 1.50 -->\n", + " <g transform=\"translate(283.648946 254.356562)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-49\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"xtick_8\">\n", + " <g id=\"line2d_15\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 333.018396 239.758125 \n", + "L 333.018396 22.318125 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_16\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"333.018396\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_8\">\n", + " <!-- 1.75 -->\n", + " <g transform=\"translate(321.885583 254.356562)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-49\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"xtick_9\">\n", + " <g id=\"line2d_17\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 371.255034 239.758125 \n", + "L 371.255034 22.318125 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_18\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"371.255034\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_9\">\n", + " <!-- 2.00 -->\n", + " <g transform=\"translate(360.122221 254.356562)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-50\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_10\">\n", + " <!-- time (s) -->\n", + " <defs>\n", + " <path d=\"M 18.3125 70.21875 \n", + "L 18.3125 54.6875 \n", + "L 36.8125 54.6875 \n", + "L 36.8125 47.703125 \n", + "L 18.3125 47.703125 \n", + "L 18.3125 18.015625 \n", + "Q 18.3125 11.328125 20.140625 9.421875 \n", + "Q 21.96875 7.515625 27.59375 7.515625 \n", + "L 36.8125 7.515625 \n", + "L 36.8125 0 \n", + "L 27.59375 0 \n", + "Q 17.1875 0 13.234375 3.875 \n", + "Q 9.28125 7.765625 9.28125 18.015625 \n", + "L 9.28125 47.703125 \n", + "L 2.6875 47.703125 \n", + "L 2.6875 54.6875 \n", + "L 9.28125 54.6875 \n", + "L 9.28125 70.21875 \n", + "z\n", + "\" id=\"DejaVuSans-116\"/>\n", + " <path d=\"M 9.421875 54.6875 \n", + "L 18.40625 54.6875 \n", + "L 18.40625 0 \n", + "L 9.421875 0 \n", + "z\n", + "M 9.421875 75.984375 \n", + "L 18.40625 75.984375 \n", + "L 18.40625 64.59375 \n", + "L 9.421875 64.59375 \n", + "z\n", + "\" id=\"DejaVuSans-105\"/>\n", + " <path d=\"M 52 44.1875 \n", + "Q 55.375 50.25 60.0625 53.125 \n", + "Q 64.75 56 71.09375 56 \n", + "Q 79.640625 56 84.28125 50.015625 \n", + "Q 88.921875 44.046875 88.921875 33.015625 \n", + "L 88.921875 0 \n", + "L 79.890625 0 \n", + "L 79.890625 32.71875 \n", + "Q 79.890625 40.578125 77.09375 44.375 \n", + "Q 74.3125 48.1875 68.609375 48.1875 \n", + "Q 61.625 48.1875 57.5625 43.546875 \n", + "Q 53.515625 38.921875 53.515625 30.90625 \n", + "L 53.515625 0 \n", + "L 44.484375 0 \n", + "L 44.484375 32.71875 \n", + "Q 44.484375 40.625 41.703125 44.40625 \n", + "Q 38.921875 48.1875 33.109375 48.1875 \n", + "Q 26.21875 48.1875 22.15625 43.53125 \n", + "Q 18.109375 38.875 18.109375 30.90625 \n", + "L 18.109375 0 \n", + "L 9.078125 0 \n", + "L 9.078125 54.6875 \n", + "L 18.109375 54.6875 \n", + "L 18.109375 46.1875 \n", + "Q 21.1875 51.21875 25.484375 53.609375 \n", + "Q 29.78125 56 35.6875 56 \n", + "Q 41.65625 56 45.828125 52.96875 \n", + "Q 50 49.953125 52 44.1875 \n", + "z\n", + "\" id=\"DejaVuSans-109\"/>\n", + " <path d=\"M 56.203125 29.59375 \n", + "L 56.203125 25.203125 \n", + "L 14.890625 25.203125 \n", + "Q 15.484375 15.921875 20.484375 11.0625 \n", + "Q 25.484375 6.203125 34.421875 6.203125 \n", + "Q 39.59375 6.203125 44.453125 7.46875 \n", + "Q 49.3125 8.734375 54.109375 11.28125 \n", + "L 54.109375 2.78125 \n", + "Q 49.265625 0.734375 44.1875 -0.34375 \n", + "Q 39.109375 -1.421875 33.890625 -1.421875 \n", + "Q 20.796875 -1.421875 13.15625 6.1875 \n", + "Q 5.515625 13.8125 5.515625 26.8125 \n", + "Q 5.515625 40.234375 12.765625 48.109375 \n", + "Q 20.015625 56 32.328125 56 \n", + "Q 43.359375 56 49.78125 48.890625 \n", + "Q 56.203125 41.796875 56.203125 29.59375 \n", + "z\n", + "M 47.21875 32.234375 \n", + "Q 47.125 39.59375 43.09375 43.984375 \n", + "Q 39.0625 48.390625 32.421875 48.390625 \n", + "Q 24.90625 48.390625 20.390625 44.140625 \n", + "Q 15.875 39.890625 15.1875 32.171875 \n", + "z\n", + "\" id=\"DejaVuSans-101\"/>\n", + " <path id=\"DejaVuSans-32\"/>\n", + " <path d=\"M 31 75.875 \n", + "Q 24.46875 64.65625 21.28125 53.65625 \n", + "Q 18.109375 42.671875 18.109375 31.390625 \n", + "Q 18.109375 20.125 21.3125 9.0625 \n", + "Q 24.515625 -2 31 -13.1875 \n", + "L 23.1875 -13.1875 \n", + "Q 15.875 -1.703125 12.234375 9.375 \n", + "Q 8.59375 20.453125 8.59375 31.390625 \n", + "Q 8.59375 42.28125 12.203125 53.3125 \n", + "Q 15.828125 64.359375 23.1875 75.875 \n", + "z\n", + "\" id=\"DejaVuSans-40\"/>\n", + " <path d=\"M 44.28125 53.078125 \n", + "L 44.28125 44.578125 \n", + "Q 40.484375 46.53125 36.375 47.5 \n", + "Q 32.28125 48.484375 27.875 48.484375 \n", + "Q 21.1875 48.484375 17.84375 46.4375 \n", + "Q 14.5 44.390625 14.5 40.28125 \n", + "Q 14.5 37.15625 16.890625 35.375 \n", + "Q 19.28125 33.59375 26.515625 31.984375 \n", + "L 29.59375 31.296875 \n", + "Q 39.15625 29.25 43.1875 25.515625 \n", + "Q 47.21875 21.78125 47.21875 15.09375 \n", + "Q 47.21875 7.46875 41.1875 3.015625 \n", + "Q 35.15625 -1.421875 24.609375 -1.421875 \n", + "Q 20.21875 -1.421875 15.453125 -0.5625 \n", + "Q 10.6875 0.296875 5.421875 2 \n", + "L 5.421875 11.28125 \n", + "Q 10.40625 8.6875 15.234375 7.390625 \n", + "Q 20.0625 6.109375 24.8125 6.109375 \n", + "Q 31.15625 6.109375 34.5625 8.28125 \n", + "Q 37.984375 10.453125 37.984375 14.40625 \n", + "Q 37.984375 18.0625 35.515625 20.015625 \n", + "Q 33.0625 21.96875 24.703125 23.78125 \n", + "L 21.578125 24.515625 \n", + "Q 13.234375 26.265625 9.515625 29.90625 \n", + "Q 5.8125 33.546875 5.8125 39.890625 \n", + "Q 5.8125 47.609375 11.28125 51.796875 \n", + "Q 16.75 56 26.8125 56 \n", + "Q 31.78125 56 36.171875 55.265625 \n", + "Q 40.578125 54.546875 44.28125 53.078125 \n", + "z\n", + "\" id=\"DejaVuSans-115\"/>\n", + " <path d=\"M 8.015625 75.875 \n", + "L 15.828125 75.875 \n", + "Q 23.140625 64.359375 26.78125 53.3125 \n", + "Q 30.421875 42.28125 30.421875 31.390625 \n", + "Q 30.421875 20.453125 26.78125 9.375 \n", + "Q 23.140625 -1.703125 15.828125 -13.1875 \n", + "L 8.015625 -13.1875 \n", + "Q 14.5 -2 17.703125 9.0625 \n", + "Q 20.90625 20.125 20.90625 31.390625 \n", + "Q 20.90625 42.671875 17.703125 53.65625 \n", + "Q 14.5 64.65625 8.015625 75.875 \n", + "z\n", + "\" id=\"DejaVuSans-41\"/>\n", + " </defs>\n", + " <g transform=\"translate(198.152344 268.034687)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-116\"/>\n", + " <use x=\"39.208984\" xlink:href=\"#DejaVuSans-105\"/>\n", + " <use x=\"66.992188\" xlink:href=\"#DejaVuSans-109\"/>\n", + " <use x=\"164.404297\" xlink:href=\"#DejaVuSans-101\"/>\n", + " <use x=\"225.927734\" xlink:href=\"#DejaVuSans-32\"/>\n", + " <use x=\"257.714844\" xlink:href=\"#DejaVuSans-40\"/>\n", + " <use x=\"296.728516\" xlink:href=\"#DejaVuSans-115\"/>\n", + " <use x=\"348.828125\" xlink:href=\"#DejaVuSans-41\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"matplotlib.axis_2\">\n", + " <g id=\"ytick_1\">\n", + " <g id=\"line2d_19\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 229.874489 \n", + "L 384.94375 229.874489 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_20\">\n", + " <defs>\n", + " <path d=\"M 0 0 \n", + "L -3.5 0 \n", + "\" id=\"m6dd8a7d0d7\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n", + " </defs>\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"229.874489\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_11\">\n", + " <!-- 0.00 -->\n", + " <g transform=\"translate(20.878125 233.673707)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"ytick_2\">\n", + " <g id=\"line2d_21\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 205.165398 \n", + "L 384.94375 205.165398 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_22\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"205.165398\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_12\">\n", + " <!-- 0.25 -->\n", + " <g transform=\"translate(20.878125 208.964616)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"ytick_3\">\n", + " <g id=\"line2d_23\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 180.456307 \n", + "L 384.94375 180.456307 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_24\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"180.456307\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_13\">\n", + " <!-- 0.50 -->\n", + " <g transform=\"translate(20.878125 184.255526)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"ytick_4\">\n", + " <g id=\"line2d_25\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 155.747216 \n", + "L 384.94375 155.747216 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_26\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"155.747216\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_14\">\n", + " <!-- 0.75 -->\n", + " <g transform=\"translate(20.878125 159.546435)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"ytick_5\">\n", + " <g id=\"line2d_27\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 131.038125 \n", + "L 384.94375 131.038125 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_28\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"131.038125\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_15\">\n", + " <!-- 1.00 -->\n", + " <g transform=\"translate(20.878125 134.837344)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-49\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"ytick_6\">\n", + " <g id=\"line2d_29\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 106.329034 \n", + "L 384.94375 106.329034 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_30\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"106.329034\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_16\">\n", + " <!-- 1.25 -->\n", + " <g transform=\"translate(20.878125 110.128253)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-49\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"ytick_7\">\n", + " <g id=\"line2d_31\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 81.619943 \n", + "L 384.94375 81.619943 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_32\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"81.619943\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_17\">\n", + " <!-- 1.50 -->\n", + " <g transform=\"translate(20.878125 85.419162)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-49\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"ytick_8\">\n", + " <g id=\"line2d_33\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 56.910852 \n", + "L 384.94375 56.910852 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_34\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"56.910852\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_18\">\n", + " <!-- 1.75 -->\n", + " <g transform=\"translate(20.878125 60.710071)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-49\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"ytick_9\">\n", + " <g id=\"line2d_35\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 32.201761 \n", + "L 384.94375 32.201761 \n", + "\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"line2d_36\">\n", + " <g>\n", + " <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"32.201761\"/>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_19\">\n", + " <!-- 2.00 -->\n", + " <g transform=\"translate(20.878125 36.00098)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-50\"/>\n", + " <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n", + " <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n", + " <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"text_20\">\n", + " <!-- voltage (mV) -->\n", + " <defs>\n", + " <path d=\"M 2.984375 54.6875 \n", + "L 12.5 54.6875 \n", + "L 29.59375 8.796875 \n", + "L 46.6875 54.6875 \n", + "L 56.203125 54.6875 \n", + "L 35.6875 0 \n", + "L 23.484375 0 \n", + "z\n", + "\" id=\"DejaVuSans-118\"/>\n", + " <path d=\"M 30.609375 48.390625 \n", + "Q 23.390625 48.390625 19.1875 42.75 \n", + "Q 14.984375 37.109375 14.984375 27.296875 \n", + "Q 14.984375 17.484375 19.15625 11.84375 \n", + "Q 23.34375 6.203125 30.609375 6.203125 \n", + "Q 37.796875 6.203125 41.984375 11.859375 \n", + "Q 46.1875 17.53125 46.1875 27.296875 \n", + "Q 46.1875 37.015625 41.984375 42.703125 \n", + "Q 37.796875 48.390625 30.609375 48.390625 \n", + "z\n", + "M 30.609375 56 \n", + "Q 42.328125 56 49.015625 48.375 \n", + "Q 55.71875 40.765625 55.71875 27.296875 \n", + "Q 55.71875 13.875 49.015625 6.21875 \n", + "Q 42.328125 -1.421875 30.609375 -1.421875 \n", + "Q 18.84375 -1.421875 12.171875 6.21875 \n", + "Q 5.515625 13.875 5.515625 27.296875 \n", + "Q 5.515625 40.765625 12.171875 48.375 \n", + "Q 18.84375 56 30.609375 56 \n", + "z\n", + "\" id=\"DejaVuSans-111\"/>\n", + " <path d=\"M 9.421875 75.984375 \n", + "L 18.40625 75.984375 \n", + "L 18.40625 0 \n", + "L 9.421875 0 \n", + "z\n", + "\" id=\"DejaVuSans-108\"/>\n", + " <path d=\"M 34.28125 27.484375 \n", + "Q 23.390625 27.484375 19.1875 25 \n", + "Q 14.984375 22.515625 14.984375 16.5 \n", + "Q 14.984375 11.71875 18.140625 8.90625 \n", + "Q 21.296875 6.109375 26.703125 6.109375 \n", + "Q 34.1875 6.109375 38.703125 11.40625 \n", + "Q 43.21875 16.703125 43.21875 25.484375 \n", + "L 43.21875 27.484375 \n", + "z\n", + "M 52.203125 31.203125 \n", + "L 52.203125 0 \n", + "L 43.21875 0 \n", + "L 43.21875 8.296875 \n", + "Q 40.140625 3.328125 35.546875 0.953125 \n", + "Q 30.953125 -1.421875 24.3125 -1.421875 \n", + "Q 15.921875 -1.421875 10.953125 3.296875 \n", + "Q 6 8.015625 6 15.921875 \n", + "Q 6 25.140625 12.171875 29.828125 \n", + "Q 18.359375 34.515625 30.609375 34.515625 \n", + "L 43.21875 34.515625 \n", + "L 43.21875 35.40625 \n", + "Q 43.21875 41.609375 39.140625 45 \n", + "Q 35.0625 48.390625 27.6875 48.390625 \n", + "Q 23 48.390625 18.546875 47.265625 \n", + "Q 14.109375 46.140625 10.015625 43.890625 \n", + "L 10.015625 52.203125 \n", + "Q 14.9375 54.109375 19.578125 55.046875 \n", + "Q 24.21875 56 28.609375 56 \n", + "Q 40.484375 56 46.34375 49.84375 \n", + "Q 52.203125 43.703125 52.203125 31.203125 \n", + "z\n", + "\" id=\"DejaVuSans-97\"/>\n", + " <path d=\"M 45.40625 27.984375 \n", + "Q 45.40625 37.75 41.375 43.109375 \n", + "Q 37.359375 48.484375 30.078125 48.484375 \n", + "Q 22.859375 48.484375 18.828125 43.109375 \n", + "Q 14.796875 37.75 14.796875 27.984375 \n", + "Q 14.796875 18.265625 18.828125 12.890625 \n", + "Q 22.859375 7.515625 30.078125 7.515625 \n", + "Q 37.359375 7.515625 41.375 12.890625 \n", + "Q 45.40625 18.265625 45.40625 27.984375 \n", + "z\n", + "M 54.390625 6.78125 \n", + "Q 54.390625 -7.171875 48.1875 -13.984375 \n", + "Q 42 -20.796875 29.203125 -20.796875 \n", + "Q 24.46875 -20.796875 20.265625 -20.09375 \n", + "Q 16.0625 -19.390625 12.109375 -17.921875 \n", + "L 12.109375 -9.1875 \n", + "Q 16.0625 -11.328125 19.921875 -12.34375 \n", + "Q 23.78125 -13.375 27.78125 -13.375 \n", + "Q 36.625 -13.375 41.015625 -8.765625 \n", + "Q 45.40625 -4.15625 45.40625 5.171875 \n", + "L 45.40625 9.625 \n", + "Q 42.625 4.78125 38.28125 2.390625 \n", + "Q 33.9375 0 27.875 0 \n", + "Q 17.828125 0 11.671875 7.65625 \n", + "Q 5.515625 15.328125 5.515625 27.984375 \n", + "Q 5.515625 40.671875 11.671875 48.328125 \n", + "Q 17.828125 56 27.875 56 \n", + "Q 33.9375 56 38.28125 53.609375 \n", + "Q 42.625 51.21875 45.40625 46.390625 \n", + "L 45.40625 54.6875 \n", + "L 54.390625 54.6875 \n", + "z\n", + "\" id=\"DejaVuSans-103\"/>\n", + " <path d=\"M 28.609375 0 \n", + "L 0.78125 72.90625 \n", + "L 11.078125 72.90625 \n", + "L 34.1875 11.53125 \n", + "L 57.328125 72.90625 \n", + "L 67.578125 72.90625 \n", + "L 39.796875 0 \n", + "z\n", + "\" id=\"DejaVuSans-86\"/>\n", + " </defs>\n", + " <g transform=\"translate(14.798438 163.502187)rotate(-90)scale(0.1 -0.1)\">\n", + " <use xlink:href=\"#DejaVuSans-118\"/>\n", + " <use x=\"59.179688\" xlink:href=\"#DejaVuSans-111\"/>\n", + " <use x=\"120.361328\" xlink:href=\"#DejaVuSans-108\"/>\n", + " <use x=\"148.144531\" xlink:href=\"#DejaVuSans-116\"/>\n", + " <use x=\"187.353516\" xlink:href=\"#DejaVuSans-97\"/>\n", + " <use x=\"248.632812\" xlink:href=\"#DejaVuSans-103\"/>\n", + " <use x=\"312.109375\" xlink:href=\"#DejaVuSans-101\"/>\n", + " <use x=\"373.632812\" xlink:href=\"#DejaVuSans-32\"/>\n", + " <use x=\"405.419922\" xlink:href=\"#DejaVuSans-40\"/>\n", + " <use x=\"444.433594\" xlink:href=\"#DejaVuSans-109\"/>\n", + " <use x=\"541.845703\" xlink:href=\"#DejaVuSans-86\"/>\n", + " <use x=\"610.253906\" xlink:href=\"#DejaVuSans-41\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <g id=\"line2d_37\">\n", + " <path clip-path=\"url(#p2444f92044)\" d=\"M 65.361932 131.038125 \n", + "L 71.479794 106.458521 \n", + "L 76.06819 88.955648 \n", + "L 79.127121 78.078953 \n", + "L 82.186052 68.037456 \n", + "L 85.244983 58.989517 \n", + "L 88.303914 51.077827 \n", + "L 89.83338 47.587823 \n", + "L 91.362845 44.427159 \n", + "L 92.892311 41.608309 \n", + "L 94.421776 39.142398 \n", + "L 95.951242 37.039157 \n", + "L 97.480708 35.306887 \n", + "L 99.010173 33.952425 \n", + "L 100.539639 32.981116 \n", + "L 102.069104 32.396792 \n", + "L 103.59857 32.201761 \n", + "L 105.128035 32.396792 \n", + "L 106.657501 32.981116 \n", + "L 108.186966 33.952425 \n", + "L 109.716432 35.306887 \n", + "L 111.245897 37.039157 \n", + "L 112.775363 39.142398 \n", + "L 114.304828 41.608309 \n", + "L 115.834294 44.427159 \n", + "L 117.363759 47.587823 \n", + "L 118.893225 51.077827 \n", + "L 120.42269 54.883398 \n", + "L 123.481621 63.379978 \n", + "L 126.540552 72.943568 \n", + "L 129.599483 83.423344 \n", + "L 132.658414 94.654033 \n", + "L 137.246811 112.518037 \n", + "L 151.012 167.422217 \n", + "L 154.070931 178.652906 \n", + "L 157.129862 189.132682 \n", + "L 160.188793 198.696272 \n", + "L 163.247724 207.192852 \n", + "L 164.77719 210.998423 \n", + "L 166.306655 214.488427 \n", + "L 167.836121 217.649091 \n", + "L 169.365586 220.467941 \n", + "L 170.895052 222.933852 \n", + "L 172.424517 225.037093 \n", + "L 173.953983 226.769363 \n", + "L 175.483448 228.123825 \n", + "L 177.012914 229.095134 \n", + "L 178.54238 229.679458 \n", + "L 180.071845 229.874489 \n", + "L 181.601311 229.679458 \n", + "L 183.130776 229.095134 \n", + "L 184.660242 228.123825 \n", + "L 186.189707 226.769363 \n", + "L 187.719173 225.037093 \n", + "L 189.248638 222.933852 \n", + "L 190.778104 220.467941 \n", + "L 192.307569 217.649091 \n", + "L 193.837035 214.488427 \n", + "L 195.3665 210.998423 \n", + "L 196.895966 207.192852 \n", + "L 199.954897 198.696272 \n", + "L 203.013828 189.132682 \n", + "L 206.072759 178.652906 \n", + "L 209.13169 167.422217 \n", + "L 213.720086 149.558213 \n", + "L 227.485276 94.654033 \n", + "L 230.544207 83.423344 \n", + "L 233.603138 72.943568 \n", + "L 236.662069 63.379978 \n", + "L 239.721 54.883398 \n", + "L 241.250465 51.077827 \n", + "L 242.779931 47.587823 \n", + "L 244.309396 44.427159 \n", + "L 245.838862 41.608309 \n", + "L 247.368327 39.142398 \n", + "L 248.897793 37.039157 \n", + "L 250.427258 35.306887 \n", + "L 251.956724 33.952425 \n", + "L 253.486189 32.981116 \n", + "L 255.015655 32.396792 \n", + "L 256.54512 32.201761 \n", + "L 258.074586 32.396792 \n", + "L 259.604052 32.981116 \n", + "L 261.133517 33.952425 \n", + "L 262.662983 35.306887 \n", + "L 264.192448 37.039157 \n", + "L 265.721914 39.142398 \n", + "L 267.251379 41.608309 \n", + "L 268.780845 44.427159 \n", + "L 270.31031 47.587823 \n", + "L 271.839776 51.077827 \n", + "L 273.369241 54.883398 \n", + "L 276.428172 63.379978 \n", + "L 279.487103 72.943568 \n", + "L 282.546034 83.423344 \n", + "L 285.604965 94.654033 \n", + "L 290.193362 112.518037 \n", + "L 303.958551 167.422217 \n", + "L 307.017482 178.652906 \n", + "L 310.076413 189.132682 \n", + "L 313.135344 198.696272 \n", + "L 316.194275 207.192852 \n", + "L 317.723741 210.998423 \n", + "L 319.253206 214.488427 \n", + "L 320.782672 217.649091 \n", + "L 322.312137 220.467941 \n", + "L 323.841603 222.933852 \n", + "L 325.371068 225.037093 \n", + "L 326.900534 226.769363 \n", + "L 328.429999 228.123825 \n", + "L 329.959465 229.095134 \n", + "L 331.48893 229.679458 \n", + "L 333.018396 229.874489 \n", + "L 334.547861 229.679458 \n", + "L 336.077327 229.095134 \n", + "L 337.606792 228.123825 \n", + "L 339.136258 226.769363 \n", + "L 340.665724 225.037093 \n", + "L 342.195189 222.933852 \n", + "L 343.724655 220.467941 \n", + "L 345.25412 217.649091 \n", + "L 346.783586 214.488427 \n", + "L 348.313051 210.998423 \n", + "L 349.842517 207.192852 \n", + "L 352.901448 198.696272 \n", + "L 355.960379 189.132682 \n", + "L 359.01931 178.652906 \n", + "L 362.078241 167.422217 \n", + "L 366.666637 149.558213 \n", + "L 369.725568 137.244112 \n", + "L 369.725568 137.244112 \n", + "\" style=\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/>\n", + " </g>\n", + " <g id=\"patch_3\">\n", + " <path d=\"M 50.14375 239.758125 \n", + "L 50.14375 22.318125 \n", + "\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"patch_4\">\n", + " <path d=\"M 384.94375 239.758125 \n", + "L 384.94375 22.318125 \n", + "\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"patch_5\">\n", + " <path d=\"M 50.14375 239.758125 \n", + "L 384.94375 239.758125 \n", + "\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"patch_6\">\n", + " <path d=\"M 50.14375 22.318125 \n", + "L 384.94375 22.318125 \n", + "\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n", + " </g>\n", + " <g id=\"text_21\">\n", + " <!-- About as simple as it gets, folks -->\n", + " <defs>\n", + " <path d=\"M 34.1875 63.1875 \n", + "L 20.796875 26.90625 \n", + "L 47.609375 26.90625 \n", + "z\n", + "M 28.609375 72.90625 \n", + "L 39.796875 72.90625 \n", + "L 67.578125 0 \n", + "L 57.328125 0 \n", + "L 50.6875 18.703125 \n", + "L 17.828125 18.703125 \n", + "L 11.1875 0 \n", + "L 0.78125 0 \n", + "z\n", + "\" id=\"DejaVuSans-65\"/>\n", + " <path d=\"M 48.6875 27.296875 \n", + "Q 48.6875 37.203125 44.609375 42.84375 \n", + "Q 40.53125 48.484375 33.40625 48.484375 \n", + "Q 26.265625 48.484375 22.1875 42.84375 \n", + "Q 18.109375 37.203125 18.109375 27.296875 \n", + "Q 18.109375 17.390625 22.1875 11.75 \n", + "Q 26.265625 6.109375 33.40625 6.109375 \n", + "Q 40.53125 6.109375 44.609375 11.75 \n", + "Q 48.6875 17.390625 48.6875 27.296875 \n", + "z\n", + "M 18.109375 46.390625 \n", + "Q 20.953125 51.265625 25.265625 53.625 \n", + "Q 29.59375 56 35.59375 56 \n", + "Q 45.5625 56 51.78125 48.09375 \n", + "Q 58.015625 40.1875 58.015625 27.296875 \n", + "Q 58.015625 14.40625 51.78125 6.484375 \n", + "Q 45.5625 -1.421875 35.59375 -1.421875 \n", + "Q 29.59375 -1.421875 25.265625 0.953125 \n", + "Q 20.953125 3.328125 18.109375 8.203125 \n", + "L 18.109375 0 \n", + "L 9.078125 0 \n", + "L 9.078125 75.984375 \n", + "L 18.109375 75.984375 \n", + "z\n", + "\" id=\"DejaVuSans-98\"/>\n", + " <path d=\"M 8.5 21.578125 \n", + "L 8.5 54.6875 \n", + "L 17.484375 54.6875 \n", + "L 17.484375 21.921875 \n", + "Q 17.484375 14.15625 20.5 10.265625 \n", + "Q 23.53125 6.390625 29.59375 6.390625 \n", + "Q 36.859375 6.390625 41.078125 11.03125 \n", + "Q 45.3125 15.671875 45.3125 23.6875 \n", + "L 45.3125 54.6875 \n", + "L 54.296875 54.6875 \n", + "L 54.296875 0 \n", + "L 45.3125 0 \n", + "L 45.3125 8.40625 \n", + "Q 42.046875 3.421875 37.71875 1 \n", + "Q 33.40625 -1.421875 27.6875 -1.421875 \n", + "Q 18.265625 -1.421875 13.375 4.4375 \n", + "Q 8.5 10.296875 8.5 21.578125 \n", + "z\n", + "M 31.109375 56 \n", + "z\n", + "\" id=\"DejaVuSans-117\"/>\n", + " <path d=\"M 18.109375 8.203125 \n", + "L 18.109375 -20.796875 \n", + "L 9.078125 -20.796875 \n", + "L 9.078125 54.6875 \n", + "L 18.109375 54.6875 \n", + "L 18.109375 46.390625 \n", + "Q 20.953125 51.265625 25.265625 53.625 \n", + "Q 29.59375 56 35.59375 56 \n", + "Q 45.5625 56 51.78125 48.09375 \n", + "Q 58.015625 40.1875 58.015625 27.296875 \n", + "Q 58.015625 14.40625 51.78125 6.484375 \n", + "Q 45.5625 -1.421875 35.59375 -1.421875 \n", + "Q 29.59375 -1.421875 25.265625 0.953125 \n", + "Q 20.953125 3.328125 18.109375 8.203125 \n", + "z\n", + "M 48.6875 27.296875 \n", + "Q 48.6875 37.203125 44.609375 42.84375 \n", + "Q 40.53125 48.484375 33.40625 48.484375 \n", + "Q 26.265625 48.484375 22.1875 42.84375 \n", + "Q 18.109375 37.203125 18.109375 27.296875 \n", + "Q 18.109375 17.390625 22.1875 11.75 \n", + "Q 26.265625 6.109375 33.40625 6.109375 \n", + "Q 40.53125 6.109375 44.609375 11.75 \n", + "Q 48.6875 17.390625 48.6875 27.296875 \n", + "z\n", + "\" id=\"DejaVuSans-112\"/>\n", + " <path d=\"M 11.71875 12.40625 \n", + "L 22.015625 12.40625 \n", + "L 22.015625 4 \n", + "L 14.015625 -11.625 \n", + "L 7.71875 -11.625 \n", + "L 11.71875 4 \n", + "z\n", + "\" id=\"DejaVuSans-44\"/>\n", + " <path d=\"M 37.109375 75.984375 \n", + "L 37.109375 68.5 \n", + "L 28.515625 68.5 \n", + "Q 23.6875 68.5 21.796875 66.546875 \n", + "Q 19.921875 64.59375 19.921875 59.515625 \n", + "L 19.921875 54.6875 \n", + "L 34.71875 54.6875 \n", + "L 34.71875 47.703125 \n", + "L 19.921875 47.703125 \n", + "L 19.921875 0 \n", + "L 10.890625 0 \n", + "L 10.890625 47.703125 \n", + "L 2.296875 47.703125 \n", + "L 2.296875 54.6875 \n", + "L 10.890625 54.6875 \n", + "L 10.890625 58.5 \n", + "Q 10.890625 67.625 15.140625 71.796875 \n", + "Q 19.390625 75.984375 28.609375 75.984375 \n", + "z\n", + "\" id=\"DejaVuSans-102\"/>\n", + " <path d=\"M 9.078125 75.984375 \n", + "L 18.109375 75.984375 \n", + "L 18.109375 31.109375 \n", + "L 44.921875 54.6875 \n", + "L 56.390625 54.6875 \n", + "L 27.390625 29.109375 \n", + "L 57.625 0 \n", + "L 45.90625 0 \n", + "L 18.109375 26.703125 \n", + "L 18.109375 0 \n", + "L 9.078125 0 \n", + "z\n", + "\" id=\"DejaVuSans-107\"/>\n", + " </defs>\n", + " <g transform=\"translate(121.998438 16.318125)scale(0.12 -0.12)\">\n", + " <use xlink:href=\"#DejaVuSans-65\"/>\n", + " <use x=\"68.408203\" xlink:href=\"#DejaVuSans-98\"/>\n", + " <use x=\"131.884766\" xlink:href=\"#DejaVuSans-111\"/>\n", + " <use x=\"193.066406\" xlink:href=\"#DejaVuSans-117\"/>\n", + " <use x=\"256.445312\" xlink:href=\"#DejaVuSans-116\"/>\n", + " <use x=\"295.654297\" xlink:href=\"#DejaVuSans-32\"/>\n", + " <use x=\"327.441406\" xlink:href=\"#DejaVuSans-97\"/>\n", + " <use x=\"388.720703\" xlink:href=\"#DejaVuSans-115\"/>\n", + " <use x=\"440.820312\" xlink:href=\"#DejaVuSans-32\"/>\n", + " <use x=\"472.607422\" xlink:href=\"#DejaVuSans-115\"/>\n", + " <use x=\"524.707031\" xlink:href=\"#DejaVuSans-105\"/>\n", + " <use x=\"552.490234\" xlink:href=\"#DejaVuSans-109\"/>\n", + " <use x=\"649.902344\" xlink:href=\"#DejaVuSans-112\"/>\n", + " <use x=\"713.378906\" xlink:href=\"#DejaVuSans-108\"/>\n", + " <use x=\"741.162109\" xlink:href=\"#DejaVuSans-101\"/>\n", + " <use x=\"802.685547\" xlink:href=\"#DejaVuSans-32\"/>\n", + " <use x=\"834.472656\" xlink:href=\"#DejaVuSans-97\"/>\n", + " <use x=\"895.751953\" xlink:href=\"#DejaVuSans-115\"/>\n", + " <use x=\"947.851562\" xlink:href=\"#DejaVuSans-32\"/>\n", + " <use x=\"979.638672\" xlink:href=\"#DejaVuSans-105\"/>\n", + " <use x=\"1007.421875\" xlink:href=\"#DejaVuSans-116\"/>\n", + " <use x=\"1046.630859\" xlink:href=\"#DejaVuSans-32\"/>\n", + " <use x=\"1078.417969\" xlink:href=\"#DejaVuSans-103\"/>\n", + " <use x=\"1141.894531\" xlink:href=\"#DejaVuSans-101\"/>\n", + " <use x=\"1203.417969\" xlink:href=\"#DejaVuSans-116\"/>\n", + " <use x=\"1242.626953\" xlink:href=\"#DejaVuSans-115\"/>\n", + " <use x=\"1294.726562\" xlink:href=\"#DejaVuSans-44\"/>\n", + " <use x=\"1326.513672\" xlink:href=\"#DejaVuSans-32\"/>\n", + " <use x=\"1358.300781\" xlink:href=\"#DejaVuSans-102\"/>\n", + " <use x=\"1393.505859\" xlink:href=\"#DejaVuSans-111\"/>\n", + " <use x=\"1454.6875\" xlink:href=\"#DejaVuSans-108\"/>\n", + " <use x=\"1482.470703\" xlink:href=\"#DejaVuSans-107\"/>\n", + " <use x=\"1540.380859\" xlink:href=\"#DejaVuSans-115\"/>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " </g>\n", + " <defs>\n", + " <clipPath id=\"p2444f92044\">\n", + " <rect height=\"217.44\" width=\"334.8\" x=\"50.14375\" y=\"22.318125\"/>\n", + " </clipPath>\n", + " </defs>\n", + "</svg>\n" + ], + "text/plain": [ + "<Figure size 432x288 with 1 Axes>" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "t = np.arange(0.0, 2.0, 0.01)\n", + "s = 1 + np.sin(2 * np.pi * t)\n", + "\n", + "fig, ax = plt.subplots()\n", + "ax.plot(t, s)\n", + "\n", + "ax.set(xlabel='time (s)', ylabel='voltage (mV)',\n", + " title='About as simple as it gets, folks')\n", + "\n", + "ax.grid()\n", + "\n", + "fig.savefig('test.png')\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "file_extension": ".py", + "kernelspec": { + "display_name": "Python 3.8.2 64-bit ('venvForWidgets': venv)", + "name": "python_defaultSpec_1591908362720" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "name": "python" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/test/datascience/notebook/withOutputForTrust.ipynb b/src/test/datascience/notebook/withOutputForTrust.ipynb new file mode 100644 index 000000000000..5157197f8d19 --- /dev/null +++ b/src/test/datascience/notebook/withOutputForTrust.ipynb @@ -0,0 +1,106 @@ +{ + "cells": [ + { + "source": [ + "pip list" + ], + "cell_type": "code", + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "Package Version \n-------------------- ----------\nadal 1.2.2 \naltair 4.1.0 \nappdirs 1.4.3 \nappnope 0.1.0 \nattrs 19.3.0 \nazure-common 1.1.25 \nazure-kusto-data 0.0.44 \nazure-kusto-ingest 0.0.44 \nazure-storage-blob 2.1.0 \nazure-storage-common 2.1.0 \nazure-storage-queue 2.1.0 \nbackcall 0.1.0 \nbeakerx 1.4.1 \nblack 19.10b0 \nbleach 3.1.4 \nbqplot 0.12.6 \nbranca 0.3.1 \ncertifi 2020.4.5.1\ncffi 1.14.0 \nchardet 3.0.4 \nclick 7.1.2 \ncryptography 2.9.2 \ncycler 0.10.0 \ndebugpy 1.0.0b7 \ndecorator 4.4.2 \ndefusedxml 0.6.0 \nentrypoints 0.3 \nidna 2.9 \nipydatawidgets 4.0.1 \nipykernel 5.2.1 \nipyleaflet 0.12.4 \nipython 7.13.0 \nipython-genutils 0.2.0 \nipyvolume 0.5.2 \nipywebrtc 0.5.0 \nipywidgets 7.5.1 \nisodate 0.6.0 \njedi 0.17.0 \nJinja2 2.11.2 \njson5 0.9.5 \njsonschema 3.2.0 \njupyter-client 6.1.3 \njupyter-core 4.6.3 \njupyterlab 2.1.3 \njupyterlab-server 1.1.5 \nK3D 2.7.4 \nkiwisolver 1.2.0 \nMarkupSafe 1.1.1 \nmatplotlib 3.2.1 \nmistune 0.8.4 \nmsrest 0.6.13 \nmsrestazure 0.6.3 \nnbconvert 5.6.1 \nnbformat 5.0.5 \nnglview 2.7.5 \nnotebook 6.0.3 \nnumpy 1.18.2 \noauthlib 3.1.0 \npandas 1.0.3 \npandocfilters 1.4.2 \nparso 0.7.0 \npathspec 0.8.0 \npexpect 4.8.0 \npickleshare 0.7.5 \nPillow 7.1.1 \npip 19.2.3 \nprometheus-client 0.7.1 \nprompt-toolkit 3.0.5 \nptyprocess 0.6.0 \npy4j 0.10.9 \npycparser 2.20 \nPygments 2.6.1 \nPyJWT 1.7.1 \npyparsing 2.4.7 \npyrsistent 0.16.0 \npython-dateutil 2.8.1 \npythreejs 2.2.0 \npytz 2019.3 \npyzmq 19.0.0 \nqgrid 1.1.1 \nregex 2020.4.4 \nrequests 2.23.0 \nrequests-oauthlib 1.3.0 \nSend2Trash 1.5.0 \nsetuptools 41.2.0 \nsix 1.14.0 \nterminado 0.8.3 \ntestpath 0.4.4 \ntoml 0.10.0 \ntoolz 0.10.0 \ntornado 6.0.4 \ntraitlets 4.3.3 \ntraittypes 0.2.1 \ntyped-ast 1.4.1 \nurllib3 1.25.9 \nvega-datasets 0.8.0 \nwcwidth 0.1.9 \nwebencodings 0.5.1 \nwidgetsnbextension 3.5.1 \nxarray 0.15.1 \nNote: you may need to restart the kernel to use updated packages.\n" + } + ], + "metadata": { + "tags": ["WOW"] + }, + "execution_count": 3 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# HELLO WORLD" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "output_type": "error", + "ename": "SyntaxError", + "evalue": "invalid syntax (<ipython-input-1-8b7c24be1ec9>, line 1)", + "traceback": [ + "\u001b[0;36m File \u001b[0;32m\"<ipython-input-1-8b7c24be1ec9>\"\u001b[0;36m, line \u001b[0;32m1\u001b[0m\n\u001b[0;31m with Error\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n" + ] + } + ], + "source": [ + "with Error" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "<Figure size 432x288 with 1 Axes>", + "image/svg+xml": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<!-- Created with matplotlib (https://matplotlib.org/) -->\n<svg height=\"277.314375pt\" version=\"1.1\" viewBox=\"0 0 392.14375 277.314375\" width=\"392.14375pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n <defs>\n <style type=\"text/css\">\n*{stroke-linecap:butt;stroke-linejoin:round;}\n </style>\n </defs>\n <g id=\"figure_1\">\n <g id=\"patch_1\">\n <path d=\"M 0 277.314375 \nL 392.14375 277.314375 \nL 392.14375 0 \nL 0 0 \nz\n\" style=\"fill:none;\"/>\n </g>\n <g id=\"axes_1\">\n <g id=\"patch_2\">\n <path d=\"M 50.14375 239.758125 \nL 384.94375 239.758125 \nL 384.94375 22.318125 \nL 50.14375 22.318125 \nz\n\" style=\"fill:#ffffff;\"/>\n </g>\n <g id=\"matplotlib.axis_1\">\n <g id=\"xtick_1\">\n <g id=\"line2d_1\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 65.361932 239.758125 \nL 65.361932 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_2\">\n <defs>\n <path d=\"M 0 0 \nL 0 3.5 \n\" id=\"m3ed7cd1696\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"65.361932\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_1\">\n <!-- 0.00 -->\n <defs>\n <path d=\"M 31.78125 66.40625 \nQ 24.171875 66.40625 20.328125 58.90625 \nQ 16.5 51.421875 16.5 36.375 \nQ 16.5 21.390625 20.328125 13.890625 \nQ 24.171875 6.390625 31.78125 6.390625 \nQ 39.453125 6.390625 43.28125 13.890625 \nQ 47.125 21.390625 47.125 36.375 \nQ 47.125 51.421875 43.28125 58.90625 \nQ 39.453125 66.40625 31.78125 66.40625 \nz\nM 31.78125 74.21875 \nQ 44.046875 74.21875 50.515625 64.515625 \nQ 56.984375 54.828125 56.984375 36.375 \nQ 56.984375 17.96875 50.515625 8.265625 \nQ 44.046875 -1.421875 31.78125 -1.421875 \nQ 19.53125 -1.421875 13.0625 8.265625 \nQ 6.59375 17.96875 6.59375 36.375 \nQ 6.59375 54.828125 13.0625 64.515625 \nQ 19.53125 74.21875 31.78125 74.21875 \nz\n\" id=\"DejaVuSans-48\"/>\n <path d=\"M 10.6875 12.40625 \nL 21 12.40625 \nL 21 0 \nL 10.6875 0 \nz\n\" id=\"DejaVuSans-46\"/>\n </defs>\n <g transform=\"translate(54.229119 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_2\">\n <g id=\"line2d_3\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 103.59857 239.758125 \nL 103.59857 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_4\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"103.59857\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_2\">\n <!-- 0.25 -->\n <defs>\n <path d=\"M 19.1875 8.296875 \nL 53.609375 8.296875 \nL 53.609375 0 \nL 7.328125 0 \nL 7.328125 8.296875 \nQ 12.9375 14.109375 22.625 23.890625 \nQ 32.328125 33.6875 34.8125 36.53125 \nQ 39.546875 41.84375 41.421875 45.53125 \nQ 43.3125 49.21875 43.3125 52.78125 \nQ 43.3125 58.59375 39.234375 62.25 \nQ 35.15625 65.921875 28.609375 65.921875 \nQ 23.96875 65.921875 18.8125 64.3125 \nQ 13.671875 62.703125 7.8125 59.421875 \nL 7.8125 69.390625 \nQ 13.765625 71.78125 18.9375 73 \nQ 24.125 74.21875 28.421875 74.21875 \nQ 39.75 74.21875 46.484375 68.546875 \nQ 53.21875 62.890625 53.21875 53.421875 \nQ 53.21875 48.921875 51.53125 44.890625 \nQ 49.859375 40.875 45.40625 35.40625 \nQ 44.1875 33.984375 37.640625 27.21875 \nQ 31.109375 20.453125 19.1875 8.296875 \nz\n\" id=\"DejaVuSans-50\"/>\n <path d=\"M 10.796875 72.90625 \nL 49.515625 72.90625 \nL 49.515625 64.59375 \nL 19.828125 64.59375 \nL 19.828125 46.734375 \nQ 21.96875 47.46875 24.109375 47.828125 \nQ 26.265625 48.1875 28.421875 48.1875 \nQ 40.625 48.1875 47.75 41.5 \nQ 54.890625 34.8125 54.890625 23.390625 \nQ 54.890625 11.625 47.5625 5.09375 \nQ 40.234375 -1.421875 26.90625 -1.421875 \nQ 22.3125 -1.421875 17.546875 -0.640625 \nQ 12.796875 0.140625 7.71875 1.703125 \nL 7.71875 11.625 \nQ 12.109375 9.234375 16.796875 8.0625 \nQ 21.484375 6.890625 26.703125 6.890625 \nQ 35.15625 6.890625 40.078125 11.328125 \nQ 45.015625 15.765625 45.015625 23.390625 \nQ 45.015625 31 40.078125 35.4375 \nQ 35.15625 39.890625 26.703125 39.890625 \nQ 22.75 39.890625 18.8125 39.015625 \nQ 14.890625 38.140625 10.796875 36.28125 \nz\n\" id=\"DejaVuSans-53\"/>\n </defs>\n <g transform=\"translate(92.465757 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_3\">\n <g id=\"line2d_5\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 141.835207 239.758125 \nL 141.835207 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_6\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"141.835207\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_3\">\n <!-- 0.50 -->\n <g transform=\"translate(130.702395 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_4\">\n <g id=\"line2d_7\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 180.071845 239.758125 \nL 180.071845 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_8\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"180.071845\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_4\">\n <!-- 0.75 -->\n <defs>\n <path d=\"M 8.203125 72.90625 \nL 55.078125 72.90625 \nL 55.078125 68.703125 \nL 28.609375 0 \nL 18.3125 0 \nL 43.21875 64.59375 \nL 8.203125 64.59375 \nz\n\" id=\"DejaVuSans-55\"/>\n </defs>\n <g transform=\"translate(168.939033 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_5\">\n <g id=\"line2d_9\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 218.308483 239.758125 \nL 218.308483 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_10\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"218.308483\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_5\">\n <!-- 1.00 -->\n <defs>\n <path d=\"M 12.40625 8.296875 \nL 28.515625 8.296875 \nL 28.515625 63.921875 \nL 10.984375 60.40625 \nL 10.984375 69.390625 \nL 28.421875 72.90625 \nL 38.28125 72.90625 \nL 38.28125 8.296875 \nL 54.390625 8.296875 \nL 54.390625 0 \nL 12.40625 0 \nz\n\" id=\"DejaVuSans-49\"/>\n </defs>\n <g transform=\"translate(207.17567 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_6\">\n <g id=\"line2d_11\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 256.54512 239.758125 \nL 256.54512 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_12\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"256.54512\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_6\">\n <!-- 1.25 -->\n <g transform=\"translate(245.412308 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_7\">\n <g id=\"line2d_13\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 294.781758 239.758125 \nL 294.781758 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_14\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"294.781758\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_7\">\n <!-- 1.50 -->\n <g transform=\"translate(283.648946 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_8\">\n <g id=\"line2d_15\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 333.018396 239.758125 \nL 333.018396 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_16\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"333.018396\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_8\">\n <!-- 1.75 -->\n <g transform=\"translate(321.885583 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_9\">\n <g id=\"line2d_17\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 371.255034 239.758125 \nL 371.255034 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_18\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"371.255034\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_9\">\n <!-- 2.00 -->\n <g transform=\"translate(360.122221 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"text_10\">\n <!-- time (s) -->\n <defs>\n <path d=\"M 18.3125 70.21875 \nL 18.3125 54.6875 \nL 36.8125 54.6875 \nL 36.8125 47.703125 \nL 18.3125 47.703125 \nL 18.3125 18.015625 \nQ 18.3125 11.328125 20.140625 9.421875 \nQ 21.96875 7.515625 27.59375 7.515625 \nL 36.8125 7.515625 \nL 36.8125 0 \nL 27.59375 0 \nQ 17.1875 0 13.234375 3.875 \nQ 9.28125 7.765625 9.28125 18.015625 \nL 9.28125 47.703125 \nL 2.6875 47.703125 \nL 2.6875 54.6875 \nL 9.28125 54.6875 \nL 9.28125 70.21875 \nz\n\" id=\"DejaVuSans-116\"/>\n <path d=\"M 9.421875 54.6875 \nL 18.40625 54.6875 \nL 18.40625 0 \nL 9.421875 0 \nz\nM 9.421875 75.984375 \nL 18.40625 75.984375 \nL 18.40625 64.59375 \nL 9.421875 64.59375 \nz\n\" id=\"DejaVuSans-105\"/>\n <path d=\"M 52 44.1875 \nQ 55.375 50.25 60.0625 53.125 \nQ 64.75 56 71.09375 56 \nQ 79.640625 56 84.28125 50.015625 \nQ 88.921875 44.046875 88.921875 33.015625 \nL 88.921875 0 \nL 79.890625 0 \nL 79.890625 32.71875 \nQ 79.890625 40.578125 77.09375 44.375 \nQ 74.3125 48.1875 68.609375 48.1875 \nQ 61.625 48.1875 57.5625 43.546875 \nQ 53.515625 38.921875 53.515625 30.90625 \nL 53.515625 0 \nL 44.484375 0 \nL 44.484375 32.71875 \nQ 44.484375 40.625 41.703125 44.40625 \nQ 38.921875 48.1875 33.109375 48.1875 \nQ 26.21875 48.1875 22.15625 43.53125 \nQ 18.109375 38.875 18.109375 30.90625 \nL 18.109375 0 \nL 9.078125 0 \nL 9.078125 54.6875 \nL 18.109375 54.6875 \nL 18.109375 46.1875 \nQ 21.1875 51.21875 25.484375 53.609375 \nQ 29.78125 56 35.6875 56 \nQ 41.65625 56 45.828125 52.96875 \nQ 50 49.953125 52 44.1875 \nz\n\" id=\"DejaVuSans-109\"/>\n <path d=\"M 56.203125 29.59375 \nL 56.203125 25.203125 \nL 14.890625 25.203125 \nQ 15.484375 15.921875 20.484375 11.0625 \nQ 25.484375 6.203125 34.421875 6.203125 \nQ 39.59375 6.203125 44.453125 7.46875 \nQ 49.3125 8.734375 54.109375 11.28125 \nL 54.109375 2.78125 \nQ 49.265625 0.734375 44.1875 -0.34375 \nQ 39.109375 -1.421875 33.890625 -1.421875 \nQ 20.796875 -1.421875 13.15625 6.1875 \nQ 5.515625 13.8125 5.515625 26.8125 \nQ 5.515625 40.234375 12.765625 48.109375 \nQ 20.015625 56 32.328125 56 \nQ 43.359375 56 49.78125 48.890625 \nQ 56.203125 41.796875 56.203125 29.59375 \nz\nM 47.21875 32.234375 \nQ 47.125 39.59375 43.09375 43.984375 \nQ 39.0625 48.390625 32.421875 48.390625 \nQ 24.90625 48.390625 20.390625 44.140625 \nQ 15.875 39.890625 15.1875 32.171875 \nz\n\" id=\"DejaVuSans-101\"/>\n <path id=\"DejaVuSans-32\"/>\n <path d=\"M 31 75.875 \nQ 24.46875 64.65625 21.28125 53.65625 \nQ 18.109375 42.671875 18.109375 31.390625 \nQ 18.109375 20.125 21.3125 9.0625 \nQ 24.515625 -2 31 -13.1875 \nL 23.1875 -13.1875 \nQ 15.875 -1.703125 12.234375 9.375 \nQ 8.59375 20.453125 8.59375 31.390625 \nQ 8.59375 42.28125 12.203125 53.3125 \nQ 15.828125 64.359375 23.1875 75.875 \nz\n\" id=\"DejaVuSans-40\"/>\n <path d=\"M 44.28125 53.078125 \nL 44.28125 44.578125 \nQ 40.484375 46.53125 36.375 47.5 \nQ 32.28125 48.484375 27.875 48.484375 \nQ 21.1875 48.484375 17.84375 46.4375 \nQ 14.5 44.390625 14.5 40.28125 \nQ 14.5 37.15625 16.890625 35.375 \nQ 19.28125 33.59375 26.515625 31.984375 \nL 29.59375 31.296875 \nQ 39.15625 29.25 43.1875 25.515625 \nQ 47.21875 21.78125 47.21875 15.09375 \nQ 47.21875 7.46875 41.1875 3.015625 \nQ 35.15625 -1.421875 24.609375 -1.421875 \nQ 20.21875 -1.421875 15.453125 -0.5625 \nQ 10.6875 0.296875 5.421875 2 \nL 5.421875 11.28125 \nQ 10.40625 8.6875 15.234375 7.390625 \nQ 20.0625 6.109375 24.8125 6.109375 \nQ 31.15625 6.109375 34.5625 8.28125 \nQ 37.984375 10.453125 37.984375 14.40625 \nQ 37.984375 18.0625 35.515625 20.015625 \nQ 33.0625 21.96875 24.703125 23.78125 \nL 21.578125 24.515625 \nQ 13.234375 26.265625 9.515625 29.90625 \nQ 5.8125 33.546875 5.8125 39.890625 \nQ 5.8125 47.609375 11.28125 51.796875 \nQ 16.75 56 26.8125 56 \nQ 31.78125 56 36.171875 55.265625 \nQ 40.578125 54.546875 44.28125 53.078125 \nz\n\" id=\"DejaVuSans-115\"/>\n <path d=\"M 8.015625 75.875 \nL 15.828125 75.875 \nQ 23.140625 64.359375 26.78125 53.3125 \nQ 30.421875 42.28125 30.421875 31.390625 \nQ 30.421875 20.453125 26.78125 9.375 \nQ 23.140625 -1.703125 15.828125 -13.1875 \nL 8.015625 -13.1875 \nQ 14.5 -2 17.703125 9.0625 \nQ 20.90625 20.125 20.90625 31.390625 \nQ 20.90625 42.671875 17.703125 53.65625 \nQ 14.5 64.65625 8.015625 75.875 \nz\n\" id=\"DejaVuSans-41\"/>\n </defs>\n <g transform=\"translate(198.152344 268.034687)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"39.208984\" xlink:href=\"#DejaVuSans-105\"/>\n <use x=\"66.992188\" xlink:href=\"#DejaVuSans-109\"/>\n <use x=\"164.404297\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"225.927734\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"257.714844\" xlink:href=\"#DejaVuSans-40\"/>\n <use x=\"296.728516\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"348.828125\" xlink:href=\"#DejaVuSans-41\"/>\n </g>\n </g>\n </g>\n <g id=\"matplotlib.axis_2\">\n <g id=\"ytick_1\">\n <g id=\"line2d_19\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 229.874489 \nL 384.94375 229.874489 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_20\">\n <defs>\n <path d=\"M 0 0 \nL -3.5 0 \n\" id=\"m6dd8a7d0d7\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"229.874489\"/>\n </g>\n </g>\n <g id=\"text_11\">\n <!-- 0.00 -->\n <g transform=\"translate(20.878125 233.673707)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_2\">\n <g id=\"line2d_21\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 205.165398 \nL 384.94375 205.165398 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_22\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"205.165398\"/>\n </g>\n </g>\n <g id=\"text_12\">\n <!-- 0.25 -->\n <g transform=\"translate(20.878125 208.964616)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_3\">\n <g id=\"line2d_23\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 180.456307 \nL 384.94375 180.456307 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_24\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"180.456307\"/>\n </g>\n </g>\n <g id=\"text_13\">\n <!-- 0.50 -->\n <g transform=\"translate(20.878125 184.255526)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_4\">\n <g id=\"line2d_25\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 155.747216 \nL 384.94375 155.747216 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_26\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"155.747216\"/>\n </g>\n </g>\n <g id=\"text_14\">\n <!-- 0.75 -->\n <g transform=\"translate(20.878125 159.546435)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_5\">\n <g id=\"line2d_27\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 131.038125 \nL 384.94375 131.038125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_28\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"131.038125\"/>\n </g>\n </g>\n <g id=\"text_15\">\n <!-- 1.00 -->\n <g transform=\"translate(20.878125 134.837344)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_6\">\n <g id=\"line2d_29\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 106.329034 \nL 384.94375 106.329034 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_30\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"106.329034\"/>\n </g>\n </g>\n <g id=\"text_16\">\n <!-- 1.25 -->\n <g transform=\"translate(20.878125 110.128253)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_7\">\n <g id=\"line2d_31\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 81.619943 \nL 384.94375 81.619943 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_32\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"81.619943\"/>\n </g>\n </g>\n <g id=\"text_17\">\n <!-- 1.50 -->\n <g transform=\"translate(20.878125 85.419162)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_8\">\n <g id=\"line2d_33\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 56.910852 \nL 384.94375 56.910852 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_34\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"56.910852\"/>\n </g>\n </g>\n <g id=\"text_18\">\n <!-- 1.75 -->\n <g transform=\"translate(20.878125 60.710071)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_9\">\n <g id=\"line2d_35\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 32.201761 \nL 384.94375 32.201761 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_36\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"32.201761\"/>\n </g>\n </g>\n <g id=\"text_19\">\n <!-- 2.00 -->\n <g transform=\"translate(20.878125 36.00098)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"text_20\">\n <!-- voltage (mV) -->\n <defs>\n <path d=\"M 2.984375 54.6875 \nL 12.5 54.6875 \nL 29.59375 8.796875 \nL 46.6875 54.6875 \nL 56.203125 54.6875 \nL 35.6875 0 \nL 23.484375 0 \nz\n\" id=\"DejaVuSans-118\"/>\n <path d=\"M 30.609375 48.390625 \nQ 23.390625 48.390625 19.1875 42.75 \nQ 14.984375 37.109375 14.984375 27.296875 \nQ 14.984375 17.484375 19.15625 11.84375 \nQ 23.34375 6.203125 30.609375 6.203125 \nQ 37.796875 6.203125 41.984375 11.859375 \nQ 46.1875 17.53125 46.1875 27.296875 \nQ 46.1875 37.015625 41.984375 42.703125 \nQ 37.796875 48.390625 30.609375 48.390625 \nz\nM 30.609375 56 \nQ 42.328125 56 49.015625 48.375 \nQ 55.71875 40.765625 55.71875 27.296875 \nQ 55.71875 13.875 49.015625 6.21875 \nQ 42.328125 -1.421875 30.609375 -1.421875 \nQ 18.84375 -1.421875 12.171875 6.21875 \nQ 5.515625 13.875 5.515625 27.296875 \nQ 5.515625 40.765625 12.171875 48.375 \nQ 18.84375 56 30.609375 56 \nz\n\" id=\"DejaVuSans-111\"/>\n <path d=\"M 9.421875 75.984375 \nL 18.40625 75.984375 \nL 18.40625 0 \nL 9.421875 0 \nz\n\" id=\"DejaVuSans-108\"/>\n <path d=\"M 34.28125 27.484375 \nQ 23.390625 27.484375 19.1875 25 \nQ 14.984375 22.515625 14.984375 16.5 \nQ 14.984375 11.71875 18.140625 8.90625 \nQ 21.296875 6.109375 26.703125 6.109375 \nQ 34.1875 6.109375 38.703125 11.40625 \nQ 43.21875 16.703125 43.21875 25.484375 \nL 43.21875 27.484375 \nz\nM 52.203125 31.203125 \nL 52.203125 0 \nL 43.21875 0 \nL 43.21875 8.296875 \nQ 40.140625 3.328125 35.546875 0.953125 \nQ 30.953125 -1.421875 24.3125 -1.421875 \nQ 15.921875 -1.421875 10.953125 3.296875 \nQ 6 8.015625 6 15.921875 \nQ 6 25.140625 12.171875 29.828125 \nQ 18.359375 34.515625 30.609375 34.515625 \nL 43.21875 34.515625 \nL 43.21875 35.40625 \nQ 43.21875 41.609375 39.140625 45 \nQ 35.0625 48.390625 27.6875 48.390625 \nQ 23 48.390625 18.546875 47.265625 \nQ 14.109375 46.140625 10.015625 43.890625 \nL 10.015625 52.203125 \nQ 14.9375 54.109375 19.578125 55.046875 \nQ 24.21875 56 28.609375 56 \nQ 40.484375 56 46.34375 49.84375 \nQ 52.203125 43.703125 52.203125 31.203125 \nz\n\" id=\"DejaVuSans-97\"/>\n <path d=\"M 45.40625 27.984375 \nQ 45.40625 37.75 41.375 43.109375 \nQ 37.359375 48.484375 30.078125 48.484375 \nQ 22.859375 48.484375 18.828125 43.109375 \nQ 14.796875 37.75 14.796875 27.984375 \nQ 14.796875 18.265625 18.828125 12.890625 \nQ 22.859375 7.515625 30.078125 7.515625 \nQ 37.359375 7.515625 41.375 12.890625 \nQ 45.40625 18.265625 45.40625 27.984375 \nz\nM 54.390625 6.78125 \nQ 54.390625 -7.171875 48.1875 -13.984375 \nQ 42 -20.796875 29.203125 -20.796875 \nQ 24.46875 -20.796875 20.265625 -20.09375 \nQ 16.0625 -19.390625 12.109375 -17.921875 \nL 12.109375 -9.1875 \nQ 16.0625 -11.328125 19.921875 -12.34375 \nQ 23.78125 -13.375 27.78125 -13.375 \nQ 36.625 -13.375 41.015625 -8.765625 \nQ 45.40625 -4.15625 45.40625 5.171875 \nL 45.40625 9.625 \nQ 42.625 4.78125 38.28125 2.390625 \nQ 33.9375 0 27.875 0 \nQ 17.828125 0 11.671875 7.65625 \nQ 5.515625 15.328125 5.515625 27.984375 \nQ 5.515625 40.671875 11.671875 48.328125 \nQ 17.828125 56 27.875 56 \nQ 33.9375 56 38.28125 53.609375 \nQ 42.625 51.21875 45.40625 46.390625 \nL 45.40625 54.6875 \nL 54.390625 54.6875 \nz\n\" id=\"DejaVuSans-103\"/>\n <path d=\"M 28.609375 0 \nL 0.78125 72.90625 \nL 11.078125 72.90625 \nL 34.1875 11.53125 \nL 57.328125 72.90625 \nL 67.578125 72.90625 \nL 39.796875 0 \nz\n\" id=\"DejaVuSans-86\"/>\n </defs>\n <g transform=\"translate(14.798438 163.502187)rotate(-90)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-118\"/>\n <use x=\"59.179688\" xlink:href=\"#DejaVuSans-111\"/>\n <use x=\"120.361328\" xlink:href=\"#DejaVuSans-108\"/>\n <use x=\"148.144531\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"187.353516\" xlink:href=\"#DejaVuSans-97\"/>\n <use x=\"248.632812\" xlink:href=\"#DejaVuSans-103\"/>\n <use x=\"312.109375\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"373.632812\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"405.419922\" xlink:href=\"#DejaVuSans-40\"/>\n <use x=\"444.433594\" xlink:href=\"#DejaVuSans-109\"/>\n <use x=\"541.845703\" xlink:href=\"#DejaVuSans-86\"/>\n <use x=\"610.253906\" xlink:href=\"#DejaVuSans-41\"/>\n </g>\n </g>\n </g>\n <g id=\"line2d_37\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 65.361932 131.038125 \nL 71.479794 106.458521 \nL 76.06819 88.955648 \nL 79.127121 78.078953 \nL 82.186052 68.037456 \nL 85.244983 58.989517 \nL 88.303914 51.077827 \nL 89.83338 47.587823 \nL 91.362845 44.427159 \nL 92.892311 41.608309 \nL 94.421776 39.142398 \nL 95.951242 37.039157 \nL 97.480708 35.306887 \nL 99.010173 33.952425 \nL 100.539639 32.981116 \nL 102.069104 32.396792 \nL 103.59857 32.201761 \nL 105.128035 32.396792 \nL 106.657501 32.981116 \nL 108.186966 33.952425 \nL 109.716432 35.306887 \nL 111.245897 37.039157 \nL 112.775363 39.142398 \nL 114.304828 41.608309 \nL 115.834294 44.427159 \nL 117.363759 47.587823 \nL 118.893225 51.077827 \nL 120.42269 54.883398 \nL 123.481621 63.379978 \nL 126.540552 72.943568 \nL 129.599483 83.423344 \nL 132.658414 94.654033 \nL 137.246811 112.518037 \nL 151.012 167.422217 \nL 154.070931 178.652906 \nL 157.129862 189.132682 \nL 160.188793 198.696272 \nL 163.247724 207.192852 \nL 164.77719 210.998423 \nL 166.306655 214.488427 \nL 167.836121 217.649091 \nL 169.365586 220.467941 \nL 170.895052 222.933852 \nL 172.424517 225.037093 \nL 173.953983 226.769363 \nL 175.483448 228.123825 \nL 177.012914 229.095134 \nL 178.54238 229.679458 \nL 180.071845 229.874489 \nL 181.601311 229.679458 \nL 183.130776 229.095134 \nL 184.660242 228.123825 \nL 186.189707 226.769363 \nL 187.719173 225.037093 \nL 189.248638 222.933852 \nL 190.778104 220.467941 \nL 192.307569 217.649091 \nL 193.837035 214.488427 \nL 195.3665 210.998423 \nL 196.895966 207.192852 \nL 199.954897 198.696272 \nL 203.013828 189.132682 \nL 206.072759 178.652906 \nL 209.13169 167.422217 \nL 213.720086 149.558213 \nL 227.485276 94.654033 \nL 230.544207 83.423344 \nL 233.603138 72.943568 \nL 236.662069 63.379978 \nL 239.721 54.883398 \nL 241.250465 51.077827 \nL 242.779931 47.587823 \nL 244.309396 44.427159 \nL 245.838862 41.608309 \nL 247.368327 39.142398 \nL 248.897793 37.039157 \nL 250.427258 35.306887 \nL 251.956724 33.952425 \nL 253.486189 32.981116 \nL 255.015655 32.396792 \nL 256.54512 32.201761 \nL 258.074586 32.396792 \nL 259.604052 32.981116 \nL 261.133517 33.952425 \nL 262.662983 35.306887 \nL 264.192448 37.039157 \nL 265.721914 39.142398 \nL 267.251379 41.608309 \nL 268.780845 44.427159 \nL 270.31031 47.587823 \nL 271.839776 51.077827 \nL 273.369241 54.883398 \nL 276.428172 63.379978 \nL 279.487103 72.943568 \nL 282.546034 83.423344 \nL 285.604965 94.654033 \nL 290.193362 112.518037 \nL 303.958551 167.422217 \nL 307.017482 178.652906 \nL 310.076413 189.132682 \nL 313.135344 198.696272 \nL 316.194275 207.192852 \nL 317.723741 210.998423 \nL 319.253206 214.488427 \nL 320.782672 217.649091 \nL 322.312137 220.467941 \nL 323.841603 222.933852 \nL 325.371068 225.037093 \nL 326.900534 226.769363 \nL 328.429999 228.123825 \nL 329.959465 229.095134 \nL 331.48893 229.679458 \nL 333.018396 229.874489 \nL 334.547861 229.679458 \nL 336.077327 229.095134 \nL 337.606792 228.123825 \nL 339.136258 226.769363 \nL 340.665724 225.037093 \nL 342.195189 222.933852 \nL 343.724655 220.467941 \nL 345.25412 217.649091 \nL 346.783586 214.488427 \nL 348.313051 210.998423 \nL 349.842517 207.192852 \nL 352.901448 198.696272 \nL 355.960379 189.132682 \nL 359.01931 178.652906 \nL 362.078241 167.422217 \nL 366.666637 149.558213 \nL 369.725568 137.244112 \nL 369.725568 137.244112 \n\" style=\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/>\n </g>\n <g id=\"patch_3\">\n <path d=\"M 50.14375 239.758125 \nL 50.14375 22.318125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_4\">\n <path d=\"M 384.94375 239.758125 \nL 384.94375 22.318125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_5\">\n <path d=\"M 50.14375 239.758125 \nL 384.94375 239.758125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_6\">\n <path d=\"M 50.14375 22.318125 \nL 384.94375 22.318125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"text_21\">\n <!-- About as simple as it gets, folks -->\n <defs>\n <path d=\"M 34.1875 63.1875 \nL 20.796875 26.90625 \nL 47.609375 26.90625 \nz\nM 28.609375 72.90625 \nL 39.796875 72.90625 \nL 67.578125 0 \nL 57.328125 0 \nL 50.6875 18.703125 \nL 17.828125 18.703125 \nL 11.1875 0 \nL 0.78125 0 \nz\n\" id=\"DejaVuSans-65\"/>\n <path d=\"M 48.6875 27.296875 \nQ 48.6875 37.203125 44.609375 42.84375 \nQ 40.53125 48.484375 33.40625 48.484375 \nQ 26.265625 48.484375 22.1875 42.84375 \nQ 18.109375 37.203125 18.109375 27.296875 \nQ 18.109375 17.390625 22.1875 11.75 \nQ 26.265625 6.109375 33.40625 6.109375 \nQ 40.53125 6.109375 44.609375 11.75 \nQ 48.6875 17.390625 48.6875 27.296875 \nz\nM 18.109375 46.390625 \nQ 20.953125 51.265625 25.265625 53.625 \nQ 29.59375 56 35.59375 56 \nQ 45.5625 56 51.78125 48.09375 \nQ 58.015625 40.1875 58.015625 27.296875 \nQ 58.015625 14.40625 51.78125 6.484375 \nQ 45.5625 -1.421875 35.59375 -1.421875 \nQ 29.59375 -1.421875 25.265625 0.953125 \nQ 20.953125 3.328125 18.109375 8.203125 \nL 18.109375 0 \nL 9.078125 0 \nL 9.078125 75.984375 \nL 18.109375 75.984375 \nz\n\" id=\"DejaVuSans-98\"/>\n <path d=\"M 8.5 21.578125 \nL 8.5 54.6875 \nL 17.484375 54.6875 \nL 17.484375 21.921875 \nQ 17.484375 14.15625 20.5 10.265625 \nQ 23.53125 6.390625 29.59375 6.390625 \nQ 36.859375 6.390625 41.078125 11.03125 \nQ 45.3125 15.671875 45.3125 23.6875 \nL 45.3125 54.6875 \nL 54.296875 54.6875 \nL 54.296875 0 \nL 45.3125 0 \nL 45.3125 8.40625 \nQ 42.046875 3.421875 37.71875 1 \nQ 33.40625 -1.421875 27.6875 -1.421875 \nQ 18.265625 -1.421875 13.375 4.4375 \nQ 8.5 10.296875 8.5 21.578125 \nz\nM 31.109375 56 \nz\n\" id=\"DejaVuSans-117\"/>\n <path d=\"M 18.109375 8.203125 \nL 18.109375 -20.796875 \nL 9.078125 -20.796875 \nL 9.078125 54.6875 \nL 18.109375 54.6875 \nL 18.109375 46.390625 \nQ 20.953125 51.265625 25.265625 53.625 \nQ 29.59375 56 35.59375 56 \nQ 45.5625 56 51.78125 48.09375 \nQ 58.015625 40.1875 58.015625 27.296875 \nQ 58.015625 14.40625 51.78125 6.484375 \nQ 45.5625 -1.421875 35.59375 -1.421875 \nQ 29.59375 -1.421875 25.265625 0.953125 \nQ 20.953125 3.328125 18.109375 8.203125 \nz\nM 48.6875 27.296875 \nQ 48.6875 37.203125 44.609375 42.84375 \nQ 40.53125 48.484375 33.40625 48.484375 \nQ 26.265625 48.484375 22.1875 42.84375 \nQ 18.109375 37.203125 18.109375 27.296875 \nQ 18.109375 17.390625 22.1875 11.75 \nQ 26.265625 6.109375 33.40625 6.109375 \nQ 40.53125 6.109375 44.609375 11.75 \nQ 48.6875 17.390625 48.6875 27.296875 \nz\n\" id=\"DejaVuSans-112\"/>\n <path d=\"M 11.71875 12.40625 \nL 22.015625 12.40625 \nL 22.015625 4 \nL 14.015625 -11.625 \nL 7.71875 -11.625 \nL 11.71875 4 \nz\n\" id=\"DejaVuSans-44\"/>\n <path d=\"M 37.109375 75.984375 \nL 37.109375 68.5 \nL 28.515625 68.5 \nQ 23.6875 68.5 21.796875 66.546875 \nQ 19.921875 64.59375 19.921875 59.515625 \nL 19.921875 54.6875 \nL 34.71875 54.6875 \nL 34.71875 47.703125 \nL 19.921875 47.703125 \nL 19.921875 0 \nL 10.890625 0 \nL 10.890625 47.703125 \nL 2.296875 47.703125 \nL 2.296875 54.6875 \nL 10.890625 54.6875 \nL 10.890625 58.5 \nQ 10.890625 67.625 15.140625 71.796875 \nQ 19.390625 75.984375 28.609375 75.984375 \nz\n\" id=\"DejaVuSans-102\"/>\n <path d=\"M 9.078125 75.984375 \nL 18.109375 75.984375 \nL 18.109375 31.109375 \nL 44.921875 54.6875 \nL 56.390625 54.6875 \nL 27.390625 29.109375 \nL 57.625 0 \nL 45.90625 0 \nL 18.109375 26.703125 \nL 18.109375 0 \nL 9.078125 0 \nz\n\" id=\"DejaVuSans-107\"/>\n </defs>\n <g transform=\"translate(121.998438 16.318125)scale(0.12 -0.12)\">\n <use xlink:href=\"#DejaVuSans-65\"/>\n <use x=\"68.408203\" xlink:href=\"#DejaVuSans-98\"/>\n <use x=\"131.884766\" xlink:href=\"#DejaVuSans-111\"/>\n <use x=\"193.066406\" xlink:href=\"#DejaVuSans-117\"/>\n <use x=\"256.445312\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"295.654297\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"327.441406\" xlink:href=\"#DejaVuSans-97\"/>\n <use x=\"388.720703\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"440.820312\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"472.607422\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"524.707031\" xlink:href=\"#DejaVuSans-105\"/>\n <use x=\"552.490234\" xlink:href=\"#DejaVuSans-109\"/>\n <use x=\"649.902344\" xlink:href=\"#DejaVuSans-112\"/>\n <use x=\"713.378906\" xlink:href=\"#DejaVuSans-108\"/>\n <use x=\"741.162109\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"802.685547\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"834.472656\" xlink:href=\"#DejaVuSans-97\"/>\n <use x=\"895.751953\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"947.851562\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"979.638672\" xlink:href=\"#DejaVuSans-105\"/>\n <use x=\"1007.421875\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"1046.630859\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"1078.417969\" xlink:href=\"#DejaVuSans-103\"/>\n <use x=\"1141.894531\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"1203.417969\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"1242.626953\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"1294.726562\" xlink:href=\"#DejaVuSans-44\"/>\n <use x=\"1326.513672\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"1358.300781\" xlink:href=\"#DejaVuSans-102\"/>\n <use x=\"1393.505859\" xlink:href=\"#DejaVuSans-111\"/>\n <use x=\"1454.6875\" xlink:href=\"#DejaVuSans-108\"/>\n <use x=\"1482.470703\" xlink:href=\"#DejaVuSans-107\"/>\n <use x=\"1540.380859\" xlink:href=\"#DejaVuSans-115\"/>\n </g>\n </g>\n </g>\n </g>\n <defs>\n <clipPath id=\"p2444f92044\">\n <rect height=\"217.44\" width=\"334.8\" x=\"50.14375\" y=\"22.318125\"/>\n </clipPath>\n </defs>\n</svg>\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYgAAAEWCAYAAAB8LwAVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nOydeXxU1fn/30/2lewJECAhIYCgCAbZFAW1gtVW29pWa1vtZq312721/fZX29rtu7bf9ms396+tSq1Vq9alLgRBCJuAsmcDwpoNyEaWSZ7fH/eOjjHLJJk7d2Zy3q/Xfc3MXc75zJ0z88x5nnPOI6qKwWAwGAx9iXJbgMFgMBhCE2MgDAaDwdAvxkAYDAaDoV+MgTAYDAZDvxgDYTAYDIZ+MQbCYDAYDP1iDIThbUTkQRH5qds6nEBEbhCRfzpUtqv3TUSWisg+t+ofKWLxgIicFJFNfpyvIjLNfh6xbTWUMAZiDCIiZfaXMj5I9RXaX+6YYNTXH6r6sKpe7lb9TqKqa1V1hve1iBwQkcucqEtElonI4QAVdyHwPmCSqi4IUJmGAGIMxBhDRAqBpYACH3RVjGGsUwAcUNU2t4UY+scYiLHHp4Fy4EHgxn6OZ4vISyLSIiJrRKTAe0BElojIZhE5bT8u8Tn2rn+tIvIjEfmz/fI1+/GUiLSKyOK+lYrIAhHZICKnROSYiNwlInH2MRGRX4lInYg0i8hbInJ2f29ORG4SkWpbf42I3OCzf53PeSoit4pIhX3uT0SkWETW23U85lP/MhE5LCL/KiIN9nu9YaAbLCJXich2+72sF5E5g5z7axGptevcKiJL+9yTLfaxEyLyywHKePtfvYj8CZgCPGPf6+8McM137Pt8VEQ+38d9Ey8i/yUih+x6/yAiiSKSDDwPTLTLbhWRif7q7FP/54B7gcV2OT+2939BRCpFpElEnhaRiX6UlSoiq0XkN3Zbeb+I7LY/1yMi8q2hyjAMgKqabQxtQCVwK1AKdAN5PsceBFqAi4B44NfAOvtYJnAS+BQQA1xvv86yjx8ALvMp60fAn+3nhVg9lphBdJUCi+yyC4E9wNfsYyuArUA6IMBZwIR+ykgGmoEZ9usJwGz7+U3e92K/VuDvwDhgNtAJvAIUAWnAbuBG+9xlgAf4pX1fLgbafOp5EPip/XweUAcsBKKxjPABIH6A9/1JIMt+398EjgMJ9rENwKfs5ynAogHKWAYc9nn9rs+in/NX2vXMBpKAP9v3Y5p9/FfA0/Znngo8A/yiv7qGo7MfHX0/k0uABuA8+z7/L/Ban8/Mq/FB4Kf2vdvkvf/2sWPAUvt5BnCe29+7cN1MD2IMISIXYnXrH1PVrUAV8Ik+p/1DVV9T1U7g+1j/8CYDVwIVqvonVfWo6qPAXuADgdCmqltVtdwu+wDwR6wfYrAMWSowExBV3aOqxwYoqhc4W0QSVfWYqu4apNr/UNVm+5ydwD9VtVpVT2P9U57X5/wfqGqnqq4B/gF8rJ8ybwb+qKobVbVHVf8Py/gsGuB9/1lVG+33/d9YP4zeeEI3ME1EslW1VVXLB3kvw+FjwAOquktV27GMOWD11uz38HVVbVLVFuDnwHWDlBconTcA96vqG3b7+x5W+ysc4PyJwBrgr6r6//romSUi41T1pKq+MUI9Yx5jIMYWN2L9CDbYrx/hvW6mWu8TVW0FmrC+iBOBg33OPQjkB0KYiEwXkWdF5LiINGP9KGXbOl4F7gJ+C9SJyN0iMq5vGWr5sj8O3AIcE5F/iMjMQao94fP8TD+vU3xen9R3+8oPYt2TvhQA37TdS6dE5BQweYBzEZFvicgesdx2p7B6L9n24c8B04G9Yrn0rhrkvQyHifh8zn2e52D1Krb66H/B3j8QgdL5rjZmt79GBm5jVwKJwB/67P8I8H7goFhu0ve4NA3+YQzEGEFEErH+OV5s/wgfB74OnCsi5/qcOtnnmhQsN8NReyvg3UwBjtjP27B+WLyM93nuz5LBv8fqkZSo6jjgX7HcSVYBqr9R1VJgFtaP0bf7K0RVX1TV92G5l/YC9/hRtz9k2D54L1Ow7klfaoGfqWq6z5Zk97jehR1v+A7W55KhqunAaez3raoVqno9kAv8O/B4Hw0DMdT9PgZM8nk92ed5A5ZxnO2jP01VvcbyPWWPQmdf3tXG7DKyeKeN9eUeLOP1nG99qrpZVa+29TwFPDYCLQaMgRhLXAP0YP3AzrW3s4C1WIFrL+8XkQvtAO1PgHJVrQWeA6aLyCdEJEZEPm6X9ax93XbgOhGJFZH5wLU+ZdZjuX6KBtGXihU/aLX/9X/Je0BEzheRhSISi2WIOuzy3oWI5InI1faPRSfQ2t95o+DHIhJn/7BfBfy1n3PuAW6x9YqIJIvIlSKS2s+5qVixjXogRkTuwIqJeN/PJ0UkR1V7gVP2bn/ezwkGv9ePAZ8RkbNEJAn4gfeAXdc9wK9EJNfWkS8iK3zKzhKRNH90ihXQv8kPzQCP2rrmijUE++fARtvlOBC3AfuwgvKJ9udzg4ikqWo3VpsKZBsYUxgDMXa4EcvvfEhVj3s3LNfNDfLOHIVHgB9iuZZKsYKoqGoj1o/iN7G6/d8BrvJxV/0AKMYKXP/YLgf72nbgZ8DrttuiP3/8t7DiIS1YP1B/8Tk2zt53EssF0Qj8Zz9lRAHfwPon2oQVw/hSP+eNhON2/UeBh4FbVHVv35NUdQvwBaz7ehJrUMBNA5T5ItY/4P1Y76uDd7t7VgK7RKQVa8DAdap6xg+tvwD+n32v3zOCR1WfB34DrLb1eWMGnfbj7d79trvvZey4iP2eHwWq7fInDqTT/pOR5VP+oKjqy1jt6G9YvZxiBo99oKqKFTM5jDXoIAFrIMUBW/stWLENwwgQ6/4aDIaBEJFlWCOyJg11bjgiImdhBenjVdUTwHIvBL5su58MYYjpQRgMYxAR+ZBY8x0ysOIGzwTSOACo6jpjHMIbYyAMhrHJF7Hma1RhxaYC5YozRBDGxWQwGAyGfjE9CIPBYDD0i2urazpBdna2FhYWjujatrY2kpNHMnTbWYyu4ROq2oyu4WF0DZ+RaNu6dWuDqvY/ETLYa3s4uZWWlupIWb169YivdRKja/iEqjaja3gYXcNnJNqALWrWYjIYDAbDcDAGwmAwGAz9YgyEwWAwGPrFGAiDwWAw9IsxEAaDwWDoF8cMhIhMttMA7haRXSLy1X7OETtNYKWIvCki5/kcu1GsdJAVItJfakyDwWAwOIiT8yA8wDdV9Q17qeOtIvKSqu72OecKoMTeFmLlBFgoIplYK4rOx1p/fquIPK2qJx3UazAYDAYfHDMQaqWEPGY/bxGRPViZoXwNxNXAQ/ZY3HIRSReRCVh5b19S1SYAEXkJa0nh9yRdiUTauzy8XtnIwcY2Kmq66cg+zoUl2aTER9S8RoOLtHZ6WFfRwKGmNqpquunKsdpYUpxpY4Z3CMpaTHZO2deAs1W12Wf/s8C/qeo6+/UrWGvRL8NK3P5Te/8PgDOq+l/9lH0z1nrw5OXlla5atWpEGltbW0lJSRn6RAc541GequxiTa2Hjp53H4uLhgvzY/hISRzJsdJ/AUEkFO7XQISqtlDQ1datPFHRxdojHrr6tLGEaLh4UgzXlMSRGGPa2ECEqi4Ymbbly5dvVdX5/R1z/O+Cnbbyb8DXfI1DoFDVu4G7AebPn6/Lli0bUTllZWWM9NpAsO3QSb738Bscb/Zwzdx8Pjp/ErMnpLHu9XXkTDuXv209zONvHObNph5+c/1clhRnD12og7h9vwYjVLW5rWtDVSPfenQbJ9t7uPa8yXykdBIzxqeybt06MorO4fEth3ly+xHePBXDb284j/OmZLimFdy/XwMRqrog8NocHcVkp4j8G/Cwqj7RzylHeHc+3En2voH2RyQv7z7B9feUExsdxd++tIRffdwyAGlJsSTHCgumZvLv187h71++gIykWG68fxNP7+gvHbLB0D/P7DjKjfdvIj0plr9/+QL+/do5LJiaSVqi1caWFGfzy4/P5YkvLSEuJopP3FPOy7tPuC3b4DJOjmIS4D5gj6r+coDTngY+bY9mWgSctmMXLwKXi0iGndDkcntfxLG+qoEvPbyVGXmpPHHrkkH/tZ2dn8bjtyxh3pQMvrZqG6/sMV9gw9C8sucEX121jbmT0/nbLUs4Oz9twHPnTcngiS8tYcb4cXzp4a2sr2oY8FxD5ONkD+ICrNywl4jIdnt7v4jcIiK32Oc8B1Rj5b+9B7gVwA5O/wTYbG93egPWkURlXQtf/NNWCrOSeeizC8lOiR/ymrSkWB646XxmT0zjtke2sfPI6SAoNYQrO4+c5rZHtjF7YhoPfOZ80pJih7wmKyWehz67gKnZyXzxoa1UnGgJglJDKOKYgVAr3aCo6hxVnWtvz6nqH1T1D/Y5qqpfVtViVT1HrYTv3uvvV9Vp9vaAUzrdoqO7h9se2UZcdBQPfnaBX19cL8nxMdx/0/lkJMXy5UfeoLUzoJkiDRFCa6eH2x55g/SkWO6/6XyShzEKLi0xlgc/s4D42Chue2QbHd09Q19kiDjMTGqX+Plze9h7vIX/+ti55KcnDvv6nNR4/ue6edQ2tXPHUzsdUGgId+54aieHmtr59XXzyEkdunfal4npifzXR89l34kWfvaPPQ4oNIQ6xkC4wOYDTTy04SCfvWAqy2fkjricBVMzue2SEp7YdoTV++oCqNAQ7qzeV8cT245w2yUlLJiaOeJyls3I5XMXTuVP5QfZVBNxXl7DEBgDEWS6PL18/8m3yE9P5Fsrpo+6vC8vL6Y4J5kfPLWTM30HthvGJGe6erjj7zspzknmy8uLR13eNy+fTn56It9/8i26PL0BUGgIF4yBCDL/t/4A+0+08uMPzg7IrNX4mGh+es05HD55ht+XVQZAoSHc+cOaKmqbzvDTa84hPiZ61OUlxcVw59Wzqahr5cH1NQFQaAgXjIEIIqfbu7lrdSUXT8/hsll5ASt3cXEWV86ZwD1ra6hr7ghYuYbwo66lg3vWVnPlnAksLs4KWLmXnpXHshk5/HZ1FafbuwNWriG0MQYiiPyurJLmjm6+e8XMgJf97ctn0N3Ty/+8UhHwsg3hw69frqDL08u3L58R8LJvXzmT5o5ufmd6qmMGYyCCRF1LBw+uP8CH5uZz1oRxAS+/MDuZGxZO4S+baznU2B7w8g2hT21TO6s21/KJhVMozE4OePlnTRjHh+bl88D6A6anOkYwBiJI3Lu2hu6eXr5yaYljddy6fBrRIvx+TZVjdRhCl9+vqSJahC8vn+ZYHV+5pARPTy/3rjOxiLGAMRBB4GRbF38uP8gHzp3oyD87L3njEvjo/Ek8vrWWY6fPOFaPIfQ4frqDx7cc5qPzJ5E3LsGxegqzk/nAuRP5c/lBTrZ1OVaPITQwBiIIPLj+AO1dPY7+s/Nyy8XFqFo9FsPY4d611fSocsvFox/WOhRfXj6N9q4eHlx/wPG6DO5iDITDdHT38PDGg1w6M5fpeamO1zc5M4kr50zgL5traekwo03GAq2dHv6yuZar5kxgcmaS4/VNz0vl0pm5PLzxoFmCI8IxBsJhntlxlIbWLj574dSg1fmZC6bS2unhr1sOB61Og3v8dUstLZ0ePnNB8NrYZy+cSkNrl1l2PsIxBsJBVJX7Xz/AjLxUlgRwTPpQzJ2cTmlBBg+uP0BPr/MZAw3u0dOrPLj+AKUFGcydnB60epcUZzEjL5X719UQjKyUBncwBsJBttWeYs+xZm5cUoiVHiN4fOaCQg41tfNaRX1Q6zUEl9cq6jnY2M6NSwqDWq+I8JkLCtl7vIWtB08GtW5D8DAGwkEe21xLYmw0Hzh3QtDrvnzWeLJT4nhk46Gg120IHo9uPERWchwrZ48Pet0fOHciKfExpo1FMMZAOERbp4dndhzlyjkTSE3wP9dDoIiLieLa0sm8ureO46fNpKZI5ERzB6/sreOj8ycTFxP8r3JyfAzXzJvIs28d41S7GfIaiTiZcvR+EakTkX6TFYjIt30yze0UkR4RybSPHRCRt+xjW/q7PtT5x1vHaOvq4ePnTx76ZIe4fsFkenqVx7bUuqbB4ByPba6lp1e5foF7bewTCwro8vTyxBsRmzJ+TOPk344HgZUDHVTV//RmmgO+B6zpk1Z0uX18voMaHeOxzbUU5SQzv2DgHNNOU5CVzJLiLJ5447AJJEYYqsoT246wuCiLgiznJl8OxayJ45gzKY0ntpkRc5GIkylHXwP8zTByPfCoU1qCTWVdK1sOnuRj8ycHPTjdl2vm5XOgsZ3ttadc1WEILDsOn6amoY0PnZfvthSumZvPziPNJnd1BCJO/rMUkULgWVU9e5BzkoDDwDRvD0JEaoCTgAJ/VNW7B7n+ZuBmgLy8vNJVq1aNSGtrayspKSkjurYvf9nXxYsHuvnlskTS40dng0erq71b+erqdi6aFMOnZg0/7aRTupwkVLUFUtefdnfy2mEPv16eRFLs6P6EjFbX6U7l62XtvH9qLNdOjxuVlkDqcopQ1QUj07Z8+fKtA3pqVNWxDSgEdg5xzseBZ/rsy7cfc4EdwEX+1FdaWqojZfXq1SO+1pcuT4+W/uQl/fz/bQ5IeYHQdevDW3Xuj1/ULk/P6AXZBOp+OUGoagtkG5t35z/11oe3BqS8QOj69H0bdckvXtGent7RC7KJ9M/RCUaiDdiiA/ymhsIopuvo415S1SP2Yx3wJLDABV0jYkNVIw2tnVxbOsltKW/z4Xn5nGzv5rX9Zk5EJLC2op6mti4+NNd995KXD5+Xz5FTZ9h8wOStjiRcNRAikgZcDPzdZ1+yiKR6nwOXA/2OhApFnn3zKKnxMVw8PcdtKW9z0fQcMpJieXKbGWkSCTy57SgZSbFcFEJt7H2z8kiKi+ap7aaNRRJODnN9FNgAzBCRwyLyORG5RURu8TntQ8A/VbXNZ18esE5EdgCbgH+o6gtO6QwkXZ5eXth5nPfNyiMhdvS5gANFbHQUHzh3Ii/tPkGzWcAvrGnp6Oafu45z1ZyJrsx9GIikuBhWzh7Ps28eMwv4RRBOjmK6XlUnqGqsqk5S1ftU9Q+q+gefcx5U1ev6XFetqufa22xV/ZlTGgPN65UNNHd4uHJO8GdOD8U18/LptA2YIXx5YedxOj29XDMvdNxLXq6Zl09Lh4fVe+vclmIIEKHzFyQCeObNo4xLiGFpSeh0/b3Mm5xOQVYSz5jVN8Oap3ccZUpmEudNCd7CfP6ypDiLnNR4s8JrBGEMRIDo6O7hpV0nWDF7fEh1/b2ICFecPYENVY2cbjdupnDkdHs3G6oaueKc8a7Pr+mPmOgoVszOo2xfPWe6jJspEgi9X7IwZW1FAy2doele8rLy7PF4epVX9p5wW4phBLyy9wSeXnVlYT5/WTl7Ame6e8wqwhGCMRAB4tk3j5KeFMsF07LdljIgc/LTmJCWYOIQYcqLu44zflwC504KPfeSl4VFmaQlxvLiLtPGIgFjIAJAR3cPL+8+wcrZ44mNDt1bGhUlrJg9njX762nv8rgtxzAM2rs8rNlfz4rZeURFhZ57yUtsdBSXnZXHy7tP0N3T67YcwygJ3V+zMOL1ygbaunq44pzQdS95uXx2Hp2eXtbsMy6AcOK1/fV0dPey4uzQdS95WTE7j+YOD+XVjW5LMYwSYyACwMt7TpASH8Oioky3pQzJgsJMMpJiecG4AMKKF3YeJyMplgWFod/GLpqeQ2JstHFlRgDGQIyS3l7l5T11XDw9h/iY0JkcNxAx0VG8b1Yer+6po8tjXADhQJenl1f21PG+WXnEhLAL00tCbDTLZ+bw4q4TJid6mBP6rS3EefPIaepbOrlsVq7bUvxm5dnjaen0sL6qwW0pBj9YX2WNkFsZBu4lLytmj6ehtZNth0y+6nDGGIhR8vLuE0RHCctnhI+BWFKcTUp8jBlpEia8uOs4KfExLCkO3RFyfblkZi5x0VHGzRTmGAMxSl7ec4LzCzNITwrcOvhOkxAbzcXTc3hlT53JNBfiqCqv2C7MUFrfayhSE2JZVJzFK2bZjbDGGIhRUNvUzt7jLVx2Vp7bUobN8pm51LV0sutos9tSDIOw62gzdS2dLJ8ZPj1UL5fMyKGmoY2ahrahTzaEJMZAjIKX91gzkt83K/wMxLIZOYhgFlYLcbyfz7IZobe+11BcMtP6Xpg2Fr4YAzEKXt5zgpLcFFeTxo+U7JR45kxK59V95ssbyry6r45zJ6WRnRK4dLHBYkpWEsU5yaw2bSxsMQZihDR3dLOxuonLwrD34GX5jBy2156isbXTbSmGfmhq62J77amwdC95WT4jl43VTbR1mpn74YgxECNkXUUDnl7l0jD+8l4yMxdVWGNSkYYka/bXoUpYjZDryyUzc+nq6eX1SjOkOhxxMqPc/SJSJyL9pgsVkWUiclpEttvbHT7HVorIPhGpFJHvOqVxNKzZV8+4hBjmTg7dhdOG4uyJluviVeMjDkle3VtPdko85+SnuS1lxMwvzCQlPsa4mcIUJ3sQDwIrhzhnrarOtbc7AUQkGvgtcAUwC7heRGY5qHPYqCpr9teztCQnLGa2DkRUlLBsRg6v7a/HYxZWCyk8Pb2s2VfHshk5Ib0431DExURx4bRsVu+tN0OqwxAnU46+BjSN4NIFQKWderQLWAVcHVBxo2T/iVaON3dwcQgljR8pl8zMpbnDwxuHTrktxeDDttpTNHd4wtq95OWSmbkcb+5gz7EWt6UYhkmMy/UvFpEdwFHgW6q6C8gHan3OOQwsHKgAEbkZuBkgLy+PsrKyEQlpbW31+9rna6yMbLGNFZSVVY2oPn8Zjq4R0a1ECzz4zy20z/B/sp/jukZBqGobjq6/7usiWkBO7KWsbF/I6BoJcZ1W7/S+58v5QHH4t7FQ1QUOaFNVxzagENg5wLFxQIr9/P1Ahf38WuBen/M+BdzlT32lpaU6UlavXu33uZ+4Z4Ou+NWaEdc1HIaja6Rc98fhv59g6BopoaptOLpW/s9r+rE/rHdOjA/BuF9X/WatfuR3rw/rmkj4HIPNSLQBW3SA31TXHOiq2qyqrfbz54BYEckGjgCTfU6dZO8LCdo6PWyuOclFEeBe8rJ0ejZ7j7dQ19zhthQDUNfSwZ5jzZHVxkqy2VZ7ipYOkw89nHDNQIjIeLEzr4vIAltLI7AZKBGRqSISB1wHPO2Wzr6UVzfS1dMbEfEHLxeVWO9lnRmKGBJ4h4R6P5dIYGlJDj29yoYqk0QonHBymOujwAZghogcFpHPicgtInKLfcq1wE47BvEb4Dq7x+MBbgNeBPYAj6kVmwgJ1uyvJzE2mvmFGW5LCRizJowjMzmOdRXGQIQCaysayEiKZfbEcW5LCRjnFaSTFBdt/oSEGY4FqVX1+iGO3wXcNcCx54DnnNA1Wtbsr2dJcVZYJAfyl6go4YJp2bxW0YCqYnfsDC6gqqytaODCkvAe3tqX+JhoFhVlsdb8CQkrwncQvwscaGjjYGM7F4fhwmlDsbQkm4bWTvYeN0MR3WTfiRbqWzpZWhI+uR/8ZWlJNjUNbdQ2tbstxeAnxkAMg7V293hpBPmGvXh/kNZWmGU33GTtfm8bi0wDAZheRBhhDMQwWF/ZQH56IoVZSW5LCTgT0hIpyU0xX16XWVvZwLTcFCakJbotJeAU56QwIS2BdZXmT0i4YAyEn/T2KhuqG1lSnBWxPvoLS7LZVNNER3eP21LGJB3dPWysbozI3gOAiLC0JJt1FQ309JplN8IBYyD8ZPexZk61d3PBtMj88oI1rLLT08vmAyNZIcUwWrYcOEmnpzeihrf2ZWlJDs0dHt48bJZ2CQeMgfAT79j0JcVZLitxjoVFmcRGi3EzucTaynpio4WFRZluS3GMC6ZlI2LiEOGCMRB+sq6ygZLcFHLHJbgtxTGS4mIoLcgwX16XWFfRQGlBBklxbi+R5hyZyXGcPTHNzLkJE4yB8INOTw+bDzRFtHvJy4XTstlzrJmmti63pYwpTrV3sftYMxcUR34bu2BaNttqT9LeZbLMhTrGQPjBtkOn6OjujWj3kpfF9nssrzZLIgSTTTVNqMKiMdLGunuULQdOui3FMATGQPjB+soGogQWFkX+l3fOJGtJhPVVxgUQTDZUN5IQG8WcSeGbPc5fzi/MICZKWG/WZQp5jIHwg9erGpkzKZ20xFi3pThObHQUC6ZmmkXVgkx5dROlBRkRtYTLQCTFxTBvSjobTC815DEGYghaOz3sqD3FBdMiv/fgZUlxFlX1bZwwy38HhVPtXew93syiqWOnjS0uzuatw6doNst/hzTGQAzBpppGPL06JoKHXhYXWe/V9CKCw0Y7/rB4DMQfvCwuyqJXYVO1mXMTygxqIERkkoh8S0T+LiKbReQ1EfmdiFwpImPCuKyraCQ+JorzCiJnee+hmDVxHOMSYkwcIkiUvx1/SHdbStCYNyWd+JgoE4cIcQYccC0iD2Dlh34W+HegDkgApgMrge+LyHdV9bVgCHWL9VUNzC/MICE28n3DXqKjhEVFWcZHHCQ2VDUyvyCTuJgx8Z8LgAQ7p4ppY6HNYC3yv1X1clX9jaquV9VKVd2pqk+o6r8Ay4CjA10sIveLSJ2I7Bzg+A0i8qaIvCUi60XkXJ9jB+z920Vky0jf3GjxLn+9ZAy5l7wsKc6itumMWZrZYU62dbH3eAuLInj29EAsKTZzbkKdwQzEFSIyaaCDqtqlqpWDXP8gVk9jIGqAi1X1HOAnwN19ji9X1bmqOn+QMhzF2/0dCxPk+rK42MQhgsHGGssHv2gMDKHui/c9mzk3octgBmIisEFE1orIrSIyrBXEbNfTgBEou1finSlTDgxojNxifWUDqQkxnJMf+WPT+zI9L4Ws5DgTh3CY8upGEmOjx1T8wcucSWkkmzk3IY2oDrzsrljrWl8EXAdcA+wAHgWeUNUhU4+JSCHwrKqePcR53wJmqurn7dc1wElAgT+qat/ehe+1NwM3A+Tl5ZWuWrVqKFn90traSkpKyrv2fXtNO5NSo/jqee6tv9SfrmDxu+0d7D/Zy6+WJb5niXM3dQ1FqGrrT060oJcAACAASURBVNcPXj/DuDj49vnu5X9w8379cmsH9e29/GLpe3OshNPnGCqMRNvy5cu3DuipUVW/NiAaWAFsA9r9vKYQ2DnEOcuBPUCWz758+zEXyyhd5E99paWlOlJWr179rteHT7Zrwe3P6n1rq0dcZiDoqyuYPFx+UAtuf1Yr61rec8xNXUMRqtr66mpq7dSC25/Vu16tcEeQjZv36+41VVpw+7N6/PSZ9xwLl88xlBiJNmCLDvCb6tewCRE5B7gT+C3QCXxvWCZq4HLnAPcCV6vq245IVT1iP9YBTwILAlHfcNho+0XHom/Yi3dcvhmK6Awba7xtbOwFqL1425iJdYUmAxoIESkRkR+IyC7gYaANuFxVF6nqr0dbsYhMAZ4APqWq+332J4tIqvc5cDnQ70goJymvbiQtMZaZ41ODXXXIUJiVxIS0BDYYH7EjlFc3kRgbzTn5Yy/+4OWsCeNIS4w1cYgQZbCF51/Aijd8XFWH/QMtIo9iDYXNFpHDwA+BWABV/QNwB5AF/M72b3vU8oPlAU/a+2KAR1T1heHWP1o21jSxYGomUVGRmV7UH0SExcVZlO2rp7dXx/S9cILy6kbmF2aMqfkPfbHm3GSa+RAhyoAGQlWLfV+LyDjf81V10Dnyqnr9EMc/D3y+n/3VwLnvvSJ4HD11hoON7Xx6caGbMkKCxUVZPPHGEfadaOGsCePclhMxNNnzHz5w7kS3pbjO4qIsXtx1gtqmdiZnvjdYbXCPIf+6iMgXReQ48Caw1d5cm7wWDIxv+B2W2HNATBwisGyqMTEuL942ZuIQoYc/fdtvAWeraqGqTrW3IqeFucnG6ibGJcQwc7z5x5yfnsiUzCQzmSnAbKjyzn8Ye3Ns+lKSa825MW0s9PDHQFQBY2q9hfLqRhZMzSLa+NwBqye1qaaJ3t6B58wYhkd5dRPzCzOIjR678QcvItbaX+XVjd5h7oYQwZ/W+T1gvYj8UUR+492cFuYWx06f4UBju3Ev+bCoKIvTZ7rZc7zZbSkRQWNrJ/tOtBj3kg+LijI5erqD2qYzbksx+DDYKCYvfwReBd4Cep2V4z4bq8fu2jgD8c6aOU3MnmhcIqNl0xhef2kgfNdlmpJlAtWhgj8GIlZVv+G4khBhY00jqQkxZsSODxPTEynIsuIQn7twqttywp7y6kaS4kz8wZdpuSlkp8SxobqRj50/2W05Bht/XEzPi8jNIjJBRDK9m+PKXKK8uomFUzNN/KEPi6ZmsbG6kR4Thxg1Vvwh08QffBARFpo4RMjhTwu9HjsOQYQPcz1+uoOahjbT9e+HRcWZNHd42HPMxCFGwzvxh4j9jzViFhVlcex0B4dMDpKQYUgXk6qOGZ+Cd/7DwjGUPN5fvPekvLqRs8fg8ueBYiznfxiKxbbRLK9upCAr2WU1Bhh8LaYLB7tQRMaJyKDLeIcb5dVNpMbHMGuiiT/05Z04hEkyPxq88YexmGNkKIpzrDiEaWOhw2A9iI+IyH9grcm0FajHykk9DWuJ7gLgm44rDCIbqxtZYOIPA7K4KIvn3jpm4hCjwFp/ycQf+sMbh9hQZeIQocKArVRVvw5cBRwDPoqVFvQbQAlWEp+LVHVzUFQGgZMdvVSb+MOgLCrKMnGIUdDcqew/0cpi08YGZHFRFsebOzjYaOIQocCgMQh7Qb577C2i2ddkTfFYaIKHA7LQx0c8zWUt4cjekz2AWeNrMHznQ4x3WYvBv1FMY4K9J3us+IOZ/zAgE9ISKcwy6zKNlL1NPSTHRZsg/yAU5ySTnRJv2liIYAyEzd6mHs6fmkmM8Q0PyqKiLDbWNNFrfMTDZm9Tj4k/DIG1LlMm5dVNJg4RApiWCtQ1d3C8TVk41XT9h2JxcRYtHR4ONUf8qisBpaG1k6OtamJcfrDIjkPUtRsD4Tb+5INIslOP3mO/LhGRq/wpXETuF5E6Eek3I51Y/EZEKkXkTRE5z+fYjSJSYW83+vuGRoIZm+4/3vkQe5uMgRgO76zxZf6EDIU3T/Weph6XlRj86UE8AHQCi+3XR4Cf+ln+g8DKQY5fgTUqqgS4Gfg9gL2Uxw+BhcAC4IcikuFnncOmvLqRhGiYbeY/DMn4tASmZiebL+8w8bYxM/9haIqyk8lJjWevaWOu44+BKFbV/wC6AVS1HfBrooCqvgYMNuvlauAhtSgH0kVkArACeElVm1T1JPASgxuaUVFe3cj0zGgTf/CTRUWZ7D/ZY+ZDDIMN1Y1MzzBtzB+8+SH2NvWaOITL+LOaa5eIJAIKICLFWD2KQJAP1Pq8PmzvG2j/exCRm7F6H+Tl5VFWVjYsAV09Cl0dTMvsGfa1waC1tTXkdI3r8HDGA3965lUK06LdlvMeQu2ene5UKuvaubpQQ0qXl1C7XwCZ3d2c6lT+8txqxieHllENxfvlJdDa/DEQP8SaTT1ZRB4GLgBuCpiCUaKqdwN3A8yfP1+XLVs27DIuvxTKysoYybVOE4q6zmru4I9vvkJ3xlSWXRR62WdD7Z49++ZRYBvnjk8MKV1eQu1+AUyub+X/dq9Bc6axbMEUt+W8i1C8X14CrW1I06yqLwEfxjIKjwLzVbUsQPUfAXwXf59k7xtovyEEyBuXwPgkMWPV/aS8upGU+BgKxoXWP+FQpig7mbR4YUOVaWNu4s8opvOw1l06BhwFpohIsYj40/sYiqeBT9ujmRYBp1X1GPAicLmIZNjB6cvtfYYQYWZmNJtqmkwcwg/Kq5s4vzDDrPE1DESEszKjTH4Il/HnL83vgHIsN849wAbgr8A+Ebl8sAtF5FH7/BkiclhEPicit4jILfYpzwHVQKVd9q3w9hIfPwE229ud9j5DiDAzM5qWTg+7jp52W0pIU9fSQWVdqxlCPQJmZkZT19JJTUOb21LGLP70Ao4Cn1PVXQAiMgu4E/gO8ATwz4EuVNXrBytYrb8GXx7g2P3A/X7oM7jAzEzrv0V5dSNzJqW7rCZ08c1xfrKqdoizDb7MzLQGQJRXN1GUk+KymtDltf31HGpq53oHYjX+9CCme40DgKruBmaqanXA1RjChvSEKIpyks3a/UPgjT+YOTbDJy9JyE016zINxaObDvH7sipHXJj+GIhdIvJ7EbnY3n4H7BaReOy5EYaxyaKiLDbXNOHpMbOqB6K8upHzCzPM/IcR4J0PscHEIQZEVdlY0+TYKtT+tNqbsGIEX7O3antfN1biIMMYZVFRFi2dHnab/BD9UtfSQVW9yTEyGhYXZ1Hf0km1iUP0S0VdK01tXY61MX9yUp8B/tve+tIacEWGsGHR1HfyQ5g4xHvxxh+8awsZho9vfohiE4d4D173m1NJqPwZ5loiIo+LyG4RqfZujqgxhBW54xIoykk2Y9UHYEN1o8kxMkoKs5LIGxdvYl0DsLG6iYlpCUzKSHSkfH8X6/s94MFyKT0E/NkRNYawY3FRFpsPnDRxiH4or240OUZGiTcOYeZDvBcr/tDIoqIsRJyZY+NPy01U1VcAUdWDqvoj4EpH1BjCjkVFWbR2eth11MQhfKlr7qC6vs0s7x0AFhVZcYiqehOH8KWqvpWG1i5H0yT7YyA6RSQKqBCR20TkQ4BxBhqAd+epNrxDuckxEjB84xCGd9hQ7Xwb88dAfBVIAr4ClAKfBD7tmCJDWJGbmkBxTrL58vah3MQfAkZhVhLjxyWYNtaH8upGJqQlMCUzybE6/DEQharaqqqHVfUzqvoRILSWVzS4yiITh3gP5VWNLDDxh4Bg8lS/F1VlY3UTC6dmOhZ/AP8MxPf83GcYoywutuIQO00cAoATzR1UN5j5D4FkUVEWDa0mDuGlqr6NhtZOx9vYgPMgROQK4P1Avoj8xufQOKwRTQYD8E6e6vLqRuZONvMhvK4QYyACh/debqhuZFquCYFurAlOGxusB3EU2Ap02I/e7WmslKAGAwA5qfFMy00xPmKbt+MPZv2lgFFg4hDvory6ibxx8RRkORd/gEF6EKq6A9ghIn9WVdNjMAzKoqJMnnzjCJ6e3jHvd99Q1cjCokyT/yGAiAiLi7NYW1GPqjrqdw91VJUNVQ1cOC3b8fsw4DdZRN4SkTeBN0Tkzb6bo6oMYceioizaunrGfBziyKkzHGhsZ3FxtttSIo5FRZk0tHZRVT+2V/ipqLPmPywJQhsbbC2mqxyv3RAxvO0jrhrbcQjvsiNLzPpLAeedOEQT03JTXVbjHt42Fow1vgbsQdizpg+q6kGsOMQ59nbG3jckIrJSRPaJSKWIfLef478Ske32tl9ETvkc6/E59vTw35ohmGSnxFNi4hBsqGokMzmOGXlj9wfMKaZkJjEhzcQh1lc1MDkzkckOzn/w4s9ifR8DNgEfBT4GbBSRa/24Lhr4LXAFMAu43s5G9zaq+nVVnauqc4H/xcpQ5+WM95iqftDvd2RwjUVFWWw50ET3GJ0P4fUNLyrKJMrEHwKOd12mjWN4XaaeXqW8usmx1Vv74k808fvA+ap6o6p+GlgA/MCP6xYAlaparapdwCrg6kHOvx541I9yDSHK23GII2MzT/XBxnaOnu4w8QcH8cYhKuvGZhxiz7FmTp/pDkr8AfzLSR2lqnU+rxvxz7DkA75JeA8DC/s7UUQKgKnAqz67E0RkC9aci39T1acGuPZm4GaAvLw8ysrK/JD2XlpbW0d8rZOEk66eTutf3SMvb+Z0UZwLqizcumdltVaCxZiGKsrKat5zPJw+y1CgP13SbvVOH3qxnEunxLqgyt379XyN1cb0xD7Kyireczzg2lR10A34T+BFrCxyNwHPA//ux3XXAvf6vP4UcNcA594O/G+fffn2YxFwACgeqs7S0lIdKatXrx7xtU4Sbrre98sy/fR9G4Mrpg9u3bMvP7xVF/zsJe3t7e33eLh9lm7Tn67e3l5d/POX9dY/bw2+IBs379dN92/US/5r4PpHog3YogP8pg7ZE1DVbwN/BObY292qersftucIMNnn9SR7X39cRx/3kqoesR+rgTJgnh91GlzGWpdp7MUhVJXy6kYWO7g2v2Fs54fo7ullU01TUDMU+hOk/gawUVW/YW9P+ln2ZqBERKaKSByWEXjPaCQRmQlkABt89mWISLz9PBu4ANjtZ70GF1lUlEV7Vw9vjbE4RDDHpo91FhVl0djWRcUYi0O8efg0bV09QW1j/sQSUoF/ishaOx9Enj8FqzX7+jYs99Qe4DFV3SUid4qI76ik64BV+u6/A2cBW0RkB7AaKwZhDEQYsGDq2MwPsb6yATD5p4PBWM0P4cYaX0MGqVX1x8CPRWQO8HFgjYgcVtXL/Lj2OeC5Pvvu6PP6R/1ctx5rzoUhzMhOiWd6Xgrl1U3cusxtNcFjfVVj0Mamj3UmZyaSn55IeXUjn15c6LacoLG+qoGzJowjMzl4A0CGs2hOHXAcaxRTrjNyDJHA4jE2H8Iam94YtLHpYx0RYeEYyw/R0d3DlgMng97G/IlB3CoiZcArQBbwBVWd47QwQ/gy1uIQe44109zhMfGHILKoKIumMRSH2HboFJ2e3qAv4eJPD2Iy8DVVna2qPzKxAMNQeOMQ3jVjIp31VSb+EGwW+6z9NRbYUN1IlMACOwd8sPBnmOv3VHV7MMQYIoOslHhm5KWOmSDi+qpGinOSyRuX4LaUMcOkjHfiEGOBDVUNnDMpnXEJwZ0cOLYX7jc4xqKiTLYcOBnxcQg3xqYb3olDbKxporc3suMQ7V0eth065UqMyxgIgyMsLs7iTHcPO2pPDX1yGLOj9hTtQR6bbrBYbMch9p1ocVuKo2yqacLTq64sIW8MhMERFhdlEyWwtqLBbSmOsraiARGT/8ENLiyxjPK6CG9j6yoaiIuJ4vzC4MYfwBgIg0OkJcUyZ1I6ayvq3ZbiKGsr6pkzKZ30JPcWJxyrTEhLZFpuCq9FfBtrYEFhJolx0UGv2xgIg2NcVJLN9tpTnD7T7bYURzh9ppvttae4qMS4l9xiaUk2m2qa6OjucVuKI5xo7mDfiRaWutTGjIEwOMbS6Tn0auQORdxQ1UivwoXTjIFwi4tKcuj09LLlwEm3pTiC1312oTEQhkhj7uR0UuJjItbNtLainuS4aOZNyXBbyphlYVEmsdES0W0sOyWOs8aPc6V+YyAMjhEbHcWioqyIDVSvrWhgcXEWcTHma+QWSXExlBZk8FoEtrHeXmVdZQMXTst2LYWtadkGR7loejaHmto52NjmtpSAcrCxjUNN7SwtyXFbyphnaUkOe441U9/S6baUgLL3eAsNrV2utjFjIAyO4vXPR1ovYq3LvmHDO3gDuK9XRlobs9xmbrYxYyAMjjI1O5n89MSI8xGvragnPz2Rouxkt6WMeWZPTCMjKTbihruurWhgRl6qq0u4OGogRGSliOwTkUoR+W4/x28SkXoR2W5vn/c5dqOIVNjbjU7qNDiHiHDR9GzWVzXiiZBlNzw9vayvamRpSbZJLxoCREcJF0zLZl1FQ8Qs/93R3cOmA02uDW/14piBEJFo4LfAFcAs4HoRmdXPqX9R1bn2dq99bSbwQ2AhsAD4oYiYoSJhytKSHFo6POw4HBnLbuw4fIqWDo9xL4UQF5XkUNfSGTHLbmysaaLL0+t6G3OyB7EAqFTValXtAlYBV/t57QrgJVVtUtWTwEvASod0GhzmgmnZREcJq/dGhgtg9d56oqOEpdNMgDpUuGi69VlEThurIyE2KqjpRftjyJSjoyAfqPV5fRirR9CXj4jIRcB+4OuqWjvAtfn9VSIiNwM3A+Tl5VFWVjYisa2trSO+1kkiRde0NOHpLdXMjz/mnCgbp+/Z01vOUJwmbNv0+rCui5TPMlgMV9eU1Cie3Lifs9710xF4nL5fqspz288wIz2K8tfXDuvagGtTVUc24FrgXp/XnwLu6nNOFhBvP/8i8Kr9/FvA//M57wfAt4aqs7S0VEfK6tWrR3ytk0SKrt+trtSC25/VY6fOOCPIByfv2bFTZ7Tg9mf1t6srhn1tpHyWwWK4uv7jhT1a9L1/6Km2LmcE2Th9vyrrWrTg9mf1ofU1w752JNqALTrAb6qTLqYjWNnovEyy972Nqjaqqnfw8r1Aqb/XGsKLS2ZaaczL9tW5rGR0ePV7348hdLhkZi49vRr2o5lW77Xa2PIQaGNOGojNQImITBWROOA64GnfE0Rkgs/LDwJ77OcvApeLSIYdnL7c3mcIU6bnpZCfnsire8PbQLy6t46JaQnMyEt1W4qhD3MnZ5CRFPv2D2y4snpfHdPzUpiUkeS2FOcMhKp6gNuwftj3AI+p6i4RuVNEPmif9hUR2SUiO4CvADfZ1zYBP8EyMpuBO+19hjBFRFg+M4d1lQ10esJz5c1OTw/rKhtYPjPXDG8NQaKjhIun51C2v56eMM0y19rpYVNNU0j0HsDheRCq+pyqTlfVYlX9mb3vDlV92n7+PVWdrarnqupyVd3rc+39qjrN3h5wUqchOCyfkUt7Vw+basLT1m+qaaK9q4flM0Ljy2t4L8tn5tLU1hW2Q6rXVdTT3aMh08bMTGpD0FhSnE18TFTYDkVcvbeeuJgolkwz2eNClYun5xAlUBambqbVe+tJTbAWIAwFjIEwBI3EuGgWF2exOkwD1av31bG4KIukOCdHhxtGQ3pSHOdNyeDVMGxjqsrqfXVcND2H2OjQ+GkODRWGMcPyGbnUNLRR0xBeq7t6NS+fYSbHhTrLZ+ay80gzdc0dbksZFruONlPX0hky7iUwBsIQZLzDQ1/efcJlJcPDq/fSs/JcVmIYirfb2J7w6kW8tPsEIrAshP6EGANhCCqTM5OYPXEcL+w67raUYfHCruPMmjCOyZnuDz00DM7M8akUZCWFXRt7cddxzi/MJDsl3m0pb2MMhCHorJw9nq0HT4aNC6CuuYOtB0+y8uzxbksx+IGIsHL2eNZXNnD6TLfbcvyipqGNvcdbWDk7tNqYMRCGoOP9oX0xTNxMXp3GQIQPK84ej6dXeXVvmLQxu7ezIsTamDEQhqAzLTeFopxkXtwZHi6AF3cepyg7mZLcFLelGPxk7qR08sbF80KYtLEXdh5nzqQ08tMT3ZbyLoyBMAQdrwtgQ3Ujp9q73JYzKKfauyivbmTF2ePN7OkwIipKWDF7PGv213OmK7Rn7h87fYbttadYEWLuJTAGwuASK2aPp6dXQ36kySt76vD0akh+eQ2Ds2L2eDq6e1mzP7QnZv5zl+UGC8U2ZgyEwRXmTEpjQlpCyLsAXth1nAlpCczJT3NbimGYLJiaSXpS7Nv+/VDlhZ3HmZabwrQQdGEaA2FwBRHLBfBaRT1tnR635fRLW6eH1/bXs2L2eKKijHsp3IiNjuKys/J4ec8JujyhmQ+9qa2LjTWNITd6yYsxEAbXWHn2eLo8vSG7BHjZvno6Pb1cPttMjgtXVs4eT0uHh9erGtyW0i8v7T5Or4amewmMgTC4yPmFmeSmxvP37UfdltIvT20/Qm5qPAunmsX5wpWl07MZlxDD06HaxrYdpTAribPzx7ktpV+MgTC4RnSUcPXciZTtq6OpLbRGM51s66JsXx1Xz51ItHEvhS3xMdFcOWciL+w8HnKuzKOnzlBe08g18/JDdoScMRAGV/nQvEl4epV/vHXMbSnv4h9vHaO7R7lmXr7bUgyj5EPz8jnT3cNLITYx8+kdR1G19IUqjhoIEVkpIvtEpFJEvtvP8W+IyG4ReVNEXhGRAp9jPSKy3d6e7nutITI4a0IqM/JSeWpbaKUcf3LbEabnpTBrQmh2/Q3+M78gg/z0RJ4IsTb21LYjnDclnYKsZLelDIhjBkJEooHfAlcAs4DrRWRWn9O2AfNVdQ7wOPAfPsfOqOpce/sghohERLhmXj5bD57kUGO723IAONTYztaDJ0O662/wn6go4Zp5E1lXUU9dS2is/7XnWDN7j7eEdO8BnO1BLAAqVbVaVbuAVcDVvieo6mpV9f4qlAOTHNRjCFGunjsRsILCoYBXxzVzQ/vLa/CfD83Lp1fhmR2h4cp8atsRYqKEK+dMdFvKoIiqM8m9ReRaYKWqft5+/SlgoareNsD5dwHHVfWn9msPsB3wAP+mqk8NcN3NwM0AeXl5patWrRqR3tbWVlJSQm+iyljR9W+bznCqQ/nF0sRR/2sfjTZV5Xtrz5CeIHx3QWDXxRkrn2WgCLSuH60/Yz0uGd3nOlpdvap8s+wMBeOi+Fppwqi09GUk2pYvX75VVef3e1BVHdmAa4F7fV5/CrhrgHM/idWDiPfZl28/FgEHgOKh6iwtLdWRsnr16hFf6yRjRdeqTQe14PZndduhk6MuazTath86qQW3P6urNh0ctY6+jJXPMlAEWte9a6u14PZnteJE86jKGa2u1yvqteD2Z/WZHUdGVU5/jEQbsEUH+E110sV0BJjs83qSve9diMhlwPeBD6pqp3e/qh6xH6uBMmCeg1oNLnPFOROIj4nisS21rur4y5Za4mOiWHn2BFd1GALPB8+dSEyU8NiWw67q+MuWWlITYrgsDLITOmkgNgMlIjJVROKA64B3jUYSkXnAH7GMQ53P/gwRibefZwMXALsd1GpwmXEJsVw1ZyJ/33bEtfHqbZ0e/r7tCFfNmUhaYqwrGgzOkZMaz2Vn5fH41sN0etxZ4bWprYvn3zrOh+flkxAb7YqG4eCYgVBVD3Ab8CKwB3hMVXeJyJ0i4h2V9J9ACvDXPsNZzwK2iMgOYDVWDMIYiAjnEwun0NbVw9M73Jn1+vSOo7R19fCJhVNcqd/gPJ9YOIWmti5e3OXOnIi/bT1MV08vn1hYMPTJIUCMk4Wr6nPAc3323eHz/LIBrlsPnOOkNkPocd6UdGaOT+XhjQe57vzJQR1iqqo8svEQM8enct6U9KDVawguF07LZnJmIg+XH+SD5wZ3BFFvr/LopkOUFmQwY3xqUOseKWYmtSFkEBFuWFTAziPNvHHoZFDrfuPQSd46cpobFk4xcx8imKgo4RMLCthY08SeY81BrXttZQPVDW3cEEY9VGMgDCHFR87LZ1xCDPevOxDUeu9fd4BxCTF8+DwzFSfSuX7BZBJjo3ng9Zqg1nv/uhpyUuO5KsTnPvhiDIQhpEiKi+H6hVN4fucxDp8MzszqwyfbeX7nMa5fOIXkeEe9roYQID0pjo+U5vPU9qM0tHYOfUEAqKxrYc3+ej69qIC4mPD52Q0fpYYxw42LCxGRoPUiHnj9ACLCpxcXBqU+g/vctGQqXZ5eHlp/ICj13bu2hviYqLAbAGEMhCHkmJieyDVz83lk00EaHf6H19jaycMbD3L13Inkpwd25rQhdJmWm8KK2Xk8uP4AzR3djtZ15NQZ/vbGYa47fzJZKfGO1hVojIEwhCS3Li+m09PLfeuc9RPft66GTk8vty6b5mg9htDjtuUlNHd4+NOGg47Wc/eaKlTh5ouLHa3HCYyBMIQkxTkpvP+cCTy04aBjyYSa2rp4aMNB3n/2hJBMGG9wlnMmpXHx9BzuW1dDi0O9iOOnO1i1uZaPnDcpLHuoxkAYQpavXVpCe5eHu16tdKT8u16tpL3Lw1cvK3GkfEPo8433TaeprYt7Xqt2pPxfvbQfVbjtkvDsoRoDYQhZSvJS+WjpZP5UfoDapsCOaKptaudP5Qe4tnQS0/PCY9KSIfCcOzmdK8+ZwD1rawKeK6LiRAt/3VrLJxcVMDkzKaBlBwtjIAwhzdffN53oKOFn/9gT0HJ//tweokT4+vumB7RcQ/jx7RUz6O7p5T9e2BewMlWVO5/dTXJcTNj2HsAYCEOIMz4tgX+5pIQXdh3n1b2BWT/n1b0neH7ncb5yaQkT0sLPL2wILIXZyXx+aRGPbz3MxurGgJT5zJvHWFvRwDcvn05mclxAynQDYyAMIc8XlhZRkpvCD57aResoV3pt7fRwx993UZKbwheWFgVIoSHc+eqlJUzKSORfn3yLGSFswgAADCtJREFUju7RrfR6qr2Lnzy7mzmT0vhUmM+tMQbCEPLExUTxbx85h2Onz3DHUztHVdYdf9/J0VNn+MWHzwmrGa0GZ0mMi+bnHzqHqvo2fv7cyN2Zqsp3Hn+TU+1d/PxD5xAdFd7replviCEsKC3I5CuXlvDEtiP8dYRJhR7fepgn3jjCVy4tYX5hZoAVGsKdi6bn8PkLp/LQhoM8/9bIclc/tOEg/9x9gttXzuTs/LQAKww+xkAYwoZ/uaSEJcVZ/OuTb7G+qmFY166vauB7T7zJ4qIsblsevkFDg7N8Z+VM5k5O5+uPbWfbMFcUfmXPCX78zC4unZnLZy+Y6pDC4GIMhCFsiI4Sfv/JUqZmJ/PFh7ayqabJr+s2H2jii3/aSmFWMn/4ZCkx0abZG/onLiaKe2+cT25qAp99cDNvHj7l13VrK+q57ZFtzJ6Yxm+un0dUmLuWvDj6TRGRlSKyT0QqReS7/RyPF5G/2Mc3ikihz7Hv2fv3icgKJ3Uawoe0xFge/MwCcsbF88n7NvK3rYex8q6/F1XliTcOc8O9G8lJjefBzy4gLcmkEjUMTnZKPA99dgHJ8TFcd3c5zw3iblJVHt54kM88sJmCrCTuv+n8iFoR2DEDISLRwG+BK4BZwPUiMqvPaZ8DTqrqNOBXwL/b187CymE9G1gJ/M4uz2BgYnoif7tlCXMnp/PNv+7gxgc2s66igd5ey1D0qvJ6ZQM3PrCZbzy2g7mT0vnbLUvCcqkDgzsUZifzxK1LKMlN4daH3+Dz/7eF8urGt9tYT69Stq+O6+8p5/tP7mRxcRaP3bKYnNTwWoxvKJw0dQuASlWtBhCRVcDVgG9u6auBH9nPHwfuEiud19XAKlXtBGpEpNIub4ODeg1hREZyHI9+YRF/2nCAX71cwSfv20h8TBTZKfHUNZ+hu3cjaYmx/PADs/j04sKwH01iCD65qQk8/qUl3Leuht++WsnLe06QEBtFcrTS8tILdPX0kpkcxy8+fA4fnz85YtxKvshA3fNRFyxyLbBSVT9vv/4UsFBVb/M5Z6d9zmH7dRWwEMtolKvqn+399wHPq+rj/dRzM3AzQF5eXumqVatGpLe1tZWUlNBbsM3oGpquHmV7XQ/Vp3s53dlLUpSH6dkJzMuNJi46dL60oXTPfDG6hqazR9l6ooeDzT00tXWTnRJHcVoUc3OjiQkhwzCSe7Z8+fKtqjq/v2Nh7yxT1buBuwHmz5+vy5YtG1E5ZWVljPRaJzG6/ONyn+ehps2L0TU8Qk2XNxAaarp8CbQ2J4PUR4DJPq8n2fv6PUdEYoA0oNHPaw0Gg8HgIE4aiM1AiYhMFZE4rKDz033OeRq40X5+LfCqWj6vp4Hr7FFOU4ESYJODWg0Gg8HQB8dcTKrqEZHbgBeBaOB+Vd0lIncCW1T1aeA+4E92ELoJy4hgn/cYVkDbA3xZVUe3QIrBYDAYhoWjMQhVfQ54rs++O3yedwAfHeDanwE/c1KfwWAwGAbGTCk1GAwGQ78YA2EwGAyGfjEGwmAwGAz9YgyEwWAwGPrFsZnUbiAi9cDBEV6eDQxvDengYHQNn1DVZnQND6Nr+IxEW4Gq5vR3IKIMxGgQkS0DTTd3E6Nr+ISqNqNreBhdwyfQ2oyLyWAwGAz9YgyEwWAwGPrFGIh3uNttAQNgdA2fUNVmdA0Po2v4BFSbiUEYDAaDoV9MD8JgMBgM/WIMhMFgMBj6JeINhIisFJF9IlIpIt/t53i8iPzFPr5RRAr/f3vnH2NXUcXxz1fENkUCLY2xIL9aJQ1FSlsErRVBTQo1UJSQlECkUqIVIRoDCaZJY0xUkv6hEjDGEIMkpghViUUxtlKBdNmSgm0XBEq7JWhDLFYobTDLr+Mfcx5Mr/e9fd2+ubtZzye52bnz4853zz3vzZ07u2eysm97/rOSFlbbNqDtW5L+JmmbpD9LOjkre0vSFj+qYdRL61oq6aWs/2uzsqslPefH1dW2hXX9MNO0XdIrWVlJe/1c0h7fIbGuXJJudd3bJM3NykraazhdV7qeAUl9kmZnZc97/hZJmxvWdb6kfdn9WpmVdfSBwrpuyjQ96T41xctK2utESRv8u+ApSd+oqVPGx8xs3B6kMOM7genA+4CtwOmVOtcBP/X0EuBXnj7d608ATvXrHNGwtguASZ7+Wkubnx8YRZstBW6raTsFGPSfkz09uSldlfo3kELMF7WXX/s8YC7wZJvyRcADgICPA5tK26tLXfNb/QEXtXT5+fPA1FGy1/nA/YfrA73WVal7MWn/mibsNQ2Y6+mjge01n8kiPjbeZxDnADvMbNDMXgfuBhZX6iwGfuHpNcBnJcnz7zazITPbBezw6zWmzcw2mNlrftpP2lmvNN3YrB0LgXVm9m8zexlYB1w4SrquAFb3qO+OmNnDpP1M2rEYuMsS/cCxkqZR1l7D6jKzPu8XmvOvbuzVjsPxzV7ratK/XjSzJzy9H3gaOKFSrYiPjfcB4gTg79n5P/hfw75Tx8zeBPYBx3XZtrS2nGWkJ4QWEyVtltQv6dJR0HWZT2XXSGptD1vSZl1f21/FnQo8mGWXslc3tNNe2scOhap/GfAnSY9L+soo6PmEpK2SHpA0y/PGhL0kTSJ9yf46y27EXkqvwOcAmypFRXys6IZBQW+QdBVwNvDpLPtkM9staTrwoKQBM9vZkKS1wGozG5L0VdIM7DMN9d0NS4A1dvAuhKNprzGNpAtIA8SCLHuB2+sDwDpJz/gTdhM8QbpfByQtAu4jbTs8VrgY2Ghm+WyjuL0kvZ80KH3TzF7t5bXbMd5nELuBE7PzD3lebR1J7wWOAfZ22ba0NiR9DlgBXGJmQ618M9vtPweBv5CeKhrRZWZ7My13APO6bVtSV8YSKtP/gvbqhnbaS/vYsEg6k3QPF5vZ3lZ+Zq89wG/p7evVjpjZq2Z2wNN/AI6UNJUxYC+nk38VsZekI0mDwy/N7Dc1Vcr4WIlFlbFykGZIg6TXDa1FrVmVOl/n4EXqezw9i4MXqQfp7SJ1N9rmkBblPlLJnwxM8PRU4Dl6tFjXpa5pWfoLQL+9uyC2y/VN9vSUpnR5vZmkBUM1Ya+sj1Nov+j6eQ5eQHystL261HUSaW1tfiX/KODoLN0HXNigrg+27h/pi/YFt11XPlBKl5cfQ1qnOKope/nvfhfwow51ivhYzww7Vg/S6v520hftCs/7LumJHGAicK9/UB4DpmdtV3i7Z4GLRkHbeuCfwBY/fuf584EB/4AMAMsa1vUD4CnvfwMwM2t7jdtyB/DlJnX5+XeAWyrtSttrNfAi8AbpHe8yYDmw3MsF3O66B4CzG7LXcLruAF7O/Guz5093W231+7yiYV3XZ/7VTzaA1flAU7q8zlLSH6/k7UrbawFpjWNbdq8WNeFjEWojCIIgqGW8r0EEQRAEIyQGiCAIgqCWGCCCIAiCWmKACIIgCGqJASIIgiCoJQaIIGiDpGMlXZedHy9pTaG+Ls2jltaUf1TSnSX6DoJ2xJ+5BkEbPO7N/WZ2RgN99ZH+n+NfHeqsB64xsxdK6wkCiBlEEHTiFmCGx/hfJemU1l4BSnti3Cdpne8FcL3S/h1/9YCArX0CZkj6owdxe0TSzGonkk4DhlqDg6TLfb+BrZLyeD5rSf/tHwSNEANEELTnZmCnmZ1lZjfVlJ8BfBH4GPA94DUzmwM8CnzJ6/wMuMHM5gE3Aj+puc4nSQHqWqwEFprZbOCSLH8z8KnD+H2C4JCIaK5BMHI2WIrPv1/SPtITPqRQB2d69M35wL1pixEgxfaqMg14KTvfCNwp6R4gD8y2Bzi+h/qDoCMxQATByBnK0m9n52+TPlvvAV4xs7OGuc5/SEHgADCz5ZLOJQVge1zSPEuRVid63SBohHjFFATt2U/a4nFEWIrZv0vS5fDOvsGza6o+DXy4dSJphpltMrOVpJlFK1zzaUDtfslBUIIYIIKgDf7UvtEXjFeN8DJXAssktSJ91m2R+TAwR+++h1olacAXxPtIUUIh7VH++xHqCIJDJv7MNQjGAJJ+DKw1s/VtyicAD5F2LnuzUXHB/y0xgwiCscH3gUkdyk8Cbo7BIWiSmEEEQRAEtcQMIgiCIKglBoggCIKglhgggiAIglpigAiCIAhqiQEiCIIgqOW/sMLhWL1Rt5wAAAAASUVORK5CYII=\n" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "source": [ + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "t = np.arange(0.0, 2.0, 0.01)\n", + "s = 1 + np.sin(2 * np.pi * t)\n", + "\n", + "fig, ax = plt.subplots()\n", + "ax.plot(t, s)\n", + "\n", + "ax.set(xlabel='time (s)', ylabel='voltage (mV)',\n", + " title='About as simple as it gets, folks')\n", + "\n", + "ax.grid()\n", + "\n", + "fig.savefig('test.png')\n", + "\n", + "plt.show()" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + } + }, + "orig_nbformat": 2, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3, + "kernelspec": { + "name": "python_defaultSpec_1591908362720", + "display_name": "Python 3.8.2 64-bit ('venvForWidgets': venv)" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/test/datascience/notebook/withOutputNew.ipynb b/src/test/datascience/notebook/withOutputNew.ipynb new file mode 100644 index 000000000000..772d898783f0 --- /dev/null +++ b/src/test/datascience/notebook/withOutputNew.ipynb @@ -0,0 +1,123 @@ +{ + "cells": [ + { + "source": [ + "a=1\n", + "a" + ], + "cell_type": "code", + "outputs": [], + "metadata": {}, + "execution_count": null + }, + { + "source": [ + "pip list" + ], + "cell_type": "code", + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "Package Version \n-------------------- ----------\nadal 1.2.2 \naltair 4.1.0 \nappdirs 1.4.3 \nappnope 0.1.0 \nattrs 19.3.0 \nazure-common 1.1.25 \nazure-kusto-data 0.0.44 \nazure-kusto-ingest 0.0.44 \nazure-storage-blob 2.1.0 \nazure-storage-common 2.1.0 \nazure-storage-queue 2.1.0 \nbackcall 0.1.0 \nbeakerx 1.4.1 \nblack 19.10b0 \nbleach 3.1.4 \nbqplot 0.12.6 \nbranca 0.3.1 \ncertifi 2020.4.5.1\ncffi 1.14.0 \nchardet 3.0.4 \nclick 7.1.2 \ncryptography 2.9.2 \ncycler 0.10.0 \ndebugpy 1.0.0b7 \ndecorator 4.4.2 \ndefusedxml 0.6.0 \nentrypoints 0.3 \nidna 2.9 \nipydatawidgets 4.0.1 \nipykernel 5.2.1 \nipyleaflet 0.12.4 \nipython 7.13.0 \nipython-genutils 0.2.0 \nipyvolume 0.5.2 \nipywebrtc 0.5.0 \nipywidgets 7.5.1 \nisodate 0.6.0 \njedi 0.17.0 \nJinja2 2.11.2 \njson5 0.9.5 \njsonschema 3.2.0 \njupyter-client 6.1.3 \njupyter-core 4.6.3 \njupyterlab 2.1.3 \njupyterlab-server 1.1.5 \nK3D 2.7.4 \nkiwisolver 1.2.0 \nMarkupSafe 1.1.1 \nmatplotlib 3.2.1 \nmistune 0.8.4 \nmsrest 0.6.13 \nmsrestazure 0.6.3 \nnbconvert 5.6.1 \nnbformat 5.0.5 \nnglview 2.7.5 \nnotebook 6.0.3 \nnumpy 1.18.2 \noauthlib 3.1.0 \npandas 1.0.3 \npandocfilters 1.4.2 \nparso 0.7.0 \npathspec 0.8.0 \npexpect 4.8.0 \npickleshare 0.7.5 \nPillow 7.1.1 \npip 19.2.3 \nprometheus-client 0.7.1 \nprompt-toolkit 3.0.5 \nptyprocess 0.6.0 \npy4j 0.10.9 \npycparser 2.20 \nPygments 2.6.1 \nPyJWT 1.7.1 \npyparsing 2.4.7 \npyrsistent 0.16.0 \npython-dateutil 2.8.1 \npythreejs 2.2.0 \npytz 2019.3 \npyzmq 19.0.0 \nqgrid 1.1.1 \nregex 2020.4.4 \nrequests 2.23.0 \nrequests-oauthlib 1.3.0 \nSend2Trash 1.5.0 \nsetuptools 41.2.0 \nsix 1.14.0 \nterminado 0.8.3 \ntestpath 0.4.4 \ntoml 0.10.0 \ntoolz 0.10.0 \ntornado 6.0.4 \ntraitlets 4.3.3 \ntraittypes 0.2.1 \ntyped-ast 1.4.1 \nurllib3 1.25.9 \nvega-datasets 0.8.0 \nwcwidth 0.1.9 \nwebencodings 0.5.1 \nwidgetsnbextension 3.5.1 \nxarray 0.15.1 \nNote: you may need to restart the kernel to use updated packages.\n" + } + ], + "metadata": { + "tags": ["WOW"] + }, + "execution_count": 3 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# HELLO WORLD" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "output_type": "error", + "ename": "SyntaxError", + "evalue": "invalid syntax (<ipython-input-1-8b7c24be1ec9>, line 1)", + "traceback": [ + "\u001b[0;36m File \u001b[0;32m\"<ipython-input-1-8b7c24be1ec9>\"\u001b[0;36m, line \u001b[0;32m1\u001b[0m\n\u001b[0;31m with Error\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n" + ] + } + ], + "source": [ + "with Error" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "<Figure size 432x288 with 1 Axes>", + "image/svg+xml": "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<!-- Created with matplotlib (https://matplotlib.org/) -->\n<svg height=\"277.314375pt\" version=\"1.1\" viewBox=\"0 0 392.14375 277.314375\" width=\"392.14375pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n <defs>\n <style type=\"text/css\">\n*{stroke-linecap:butt;stroke-linejoin:round;}\n </style>\n </defs>\n <g id=\"figure_1\">\n <g id=\"patch_1\">\n <path d=\"M 0 277.314375 \nL 392.14375 277.314375 \nL 392.14375 0 \nL 0 0 \nz\n\" style=\"fill:none;\"/>\n </g>\n <g id=\"axes_1\">\n <g id=\"patch_2\">\n <path d=\"M 50.14375 239.758125 \nL 384.94375 239.758125 \nL 384.94375 22.318125 \nL 50.14375 22.318125 \nz\n\" style=\"fill:#ffffff;\"/>\n </g>\n <g id=\"matplotlib.axis_1\">\n <g id=\"xtick_1\">\n <g id=\"line2d_1\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 65.361932 239.758125 \nL 65.361932 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_2\">\n <defs>\n <path d=\"M 0 0 \nL 0 3.5 \n\" id=\"m3ed7cd1696\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"65.361932\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_1\">\n <!-- 0.00 -->\n <defs>\n <path d=\"M 31.78125 66.40625 \nQ 24.171875 66.40625 20.328125 58.90625 \nQ 16.5 51.421875 16.5 36.375 \nQ 16.5 21.390625 20.328125 13.890625 \nQ 24.171875 6.390625 31.78125 6.390625 \nQ 39.453125 6.390625 43.28125 13.890625 \nQ 47.125 21.390625 47.125 36.375 \nQ 47.125 51.421875 43.28125 58.90625 \nQ 39.453125 66.40625 31.78125 66.40625 \nz\nM 31.78125 74.21875 \nQ 44.046875 74.21875 50.515625 64.515625 \nQ 56.984375 54.828125 56.984375 36.375 \nQ 56.984375 17.96875 50.515625 8.265625 \nQ 44.046875 -1.421875 31.78125 -1.421875 \nQ 19.53125 -1.421875 13.0625 8.265625 \nQ 6.59375 17.96875 6.59375 36.375 \nQ 6.59375 54.828125 13.0625 64.515625 \nQ 19.53125 74.21875 31.78125 74.21875 \nz\n\" id=\"DejaVuSans-48\"/>\n <path d=\"M 10.6875 12.40625 \nL 21 12.40625 \nL 21 0 \nL 10.6875 0 \nz\n\" id=\"DejaVuSans-46\"/>\n </defs>\n <g transform=\"translate(54.229119 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_2\">\n <g id=\"line2d_3\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 103.59857 239.758125 \nL 103.59857 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_4\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"103.59857\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_2\">\n <!-- 0.25 -->\n <defs>\n <path d=\"M 19.1875 8.296875 \nL 53.609375 8.296875 \nL 53.609375 0 \nL 7.328125 0 \nL 7.328125 8.296875 \nQ 12.9375 14.109375 22.625 23.890625 \nQ 32.328125 33.6875 34.8125 36.53125 \nQ 39.546875 41.84375 41.421875 45.53125 \nQ 43.3125 49.21875 43.3125 52.78125 \nQ 43.3125 58.59375 39.234375 62.25 \nQ 35.15625 65.921875 28.609375 65.921875 \nQ 23.96875 65.921875 18.8125 64.3125 \nQ 13.671875 62.703125 7.8125 59.421875 \nL 7.8125 69.390625 \nQ 13.765625 71.78125 18.9375 73 \nQ 24.125 74.21875 28.421875 74.21875 \nQ 39.75 74.21875 46.484375 68.546875 \nQ 53.21875 62.890625 53.21875 53.421875 \nQ 53.21875 48.921875 51.53125 44.890625 \nQ 49.859375 40.875 45.40625 35.40625 \nQ 44.1875 33.984375 37.640625 27.21875 \nQ 31.109375 20.453125 19.1875 8.296875 \nz\n\" id=\"DejaVuSans-50\"/>\n <path d=\"M 10.796875 72.90625 \nL 49.515625 72.90625 \nL 49.515625 64.59375 \nL 19.828125 64.59375 \nL 19.828125 46.734375 \nQ 21.96875 47.46875 24.109375 47.828125 \nQ 26.265625 48.1875 28.421875 48.1875 \nQ 40.625 48.1875 47.75 41.5 \nQ 54.890625 34.8125 54.890625 23.390625 \nQ 54.890625 11.625 47.5625 5.09375 \nQ 40.234375 -1.421875 26.90625 -1.421875 \nQ 22.3125 -1.421875 17.546875 -0.640625 \nQ 12.796875 0.140625 7.71875 1.703125 \nL 7.71875 11.625 \nQ 12.109375 9.234375 16.796875 8.0625 \nQ 21.484375 6.890625 26.703125 6.890625 \nQ 35.15625 6.890625 40.078125 11.328125 \nQ 45.015625 15.765625 45.015625 23.390625 \nQ 45.015625 31 40.078125 35.4375 \nQ 35.15625 39.890625 26.703125 39.890625 \nQ 22.75 39.890625 18.8125 39.015625 \nQ 14.890625 38.140625 10.796875 36.28125 \nz\n\" id=\"DejaVuSans-53\"/>\n </defs>\n <g transform=\"translate(92.465757 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_3\">\n <g id=\"line2d_5\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 141.835207 239.758125 \nL 141.835207 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_6\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"141.835207\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_3\">\n <!-- 0.50 -->\n <g transform=\"translate(130.702395 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_4\">\n <g id=\"line2d_7\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 180.071845 239.758125 \nL 180.071845 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_8\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"180.071845\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_4\">\n <!-- 0.75 -->\n <defs>\n <path d=\"M 8.203125 72.90625 \nL 55.078125 72.90625 \nL 55.078125 68.703125 \nL 28.609375 0 \nL 18.3125 0 \nL 43.21875 64.59375 \nL 8.203125 64.59375 \nz\n\" id=\"DejaVuSans-55\"/>\n </defs>\n <g transform=\"translate(168.939033 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_5\">\n <g id=\"line2d_9\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 218.308483 239.758125 \nL 218.308483 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_10\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"218.308483\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_5\">\n <!-- 1.00 -->\n <defs>\n <path d=\"M 12.40625 8.296875 \nL 28.515625 8.296875 \nL 28.515625 63.921875 \nL 10.984375 60.40625 \nL 10.984375 69.390625 \nL 28.421875 72.90625 \nL 38.28125 72.90625 \nL 38.28125 8.296875 \nL 54.390625 8.296875 \nL 54.390625 0 \nL 12.40625 0 \nz\n\" id=\"DejaVuSans-49\"/>\n </defs>\n <g transform=\"translate(207.17567 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_6\">\n <g id=\"line2d_11\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 256.54512 239.758125 \nL 256.54512 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_12\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"256.54512\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_6\">\n <!-- 1.25 -->\n <g transform=\"translate(245.412308 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_7\">\n <g id=\"line2d_13\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 294.781758 239.758125 \nL 294.781758 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_14\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"294.781758\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_7\">\n <!-- 1.50 -->\n <g transform=\"translate(283.648946 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_8\">\n <g id=\"line2d_15\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 333.018396 239.758125 \nL 333.018396 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_16\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"333.018396\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_8\">\n <!-- 1.75 -->\n <g transform=\"translate(321.885583 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"xtick_9\">\n <g id=\"line2d_17\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 371.255034 239.758125 \nL 371.255034 22.318125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_18\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"371.255034\" xlink:href=\"#m3ed7cd1696\" y=\"239.758125\"/>\n </g>\n </g>\n <g id=\"text_9\">\n <!-- 2.00 -->\n <g transform=\"translate(360.122221 254.356562)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"text_10\">\n <!-- time (s) -->\n <defs>\n <path d=\"M 18.3125 70.21875 \nL 18.3125 54.6875 \nL 36.8125 54.6875 \nL 36.8125 47.703125 \nL 18.3125 47.703125 \nL 18.3125 18.015625 \nQ 18.3125 11.328125 20.140625 9.421875 \nQ 21.96875 7.515625 27.59375 7.515625 \nL 36.8125 7.515625 \nL 36.8125 0 \nL 27.59375 0 \nQ 17.1875 0 13.234375 3.875 \nQ 9.28125 7.765625 9.28125 18.015625 \nL 9.28125 47.703125 \nL 2.6875 47.703125 \nL 2.6875 54.6875 \nL 9.28125 54.6875 \nL 9.28125 70.21875 \nz\n\" id=\"DejaVuSans-116\"/>\n <path d=\"M 9.421875 54.6875 \nL 18.40625 54.6875 \nL 18.40625 0 \nL 9.421875 0 \nz\nM 9.421875 75.984375 \nL 18.40625 75.984375 \nL 18.40625 64.59375 \nL 9.421875 64.59375 \nz\n\" id=\"DejaVuSans-105\"/>\n <path d=\"M 52 44.1875 \nQ 55.375 50.25 60.0625 53.125 \nQ 64.75 56 71.09375 56 \nQ 79.640625 56 84.28125 50.015625 \nQ 88.921875 44.046875 88.921875 33.015625 \nL 88.921875 0 \nL 79.890625 0 \nL 79.890625 32.71875 \nQ 79.890625 40.578125 77.09375 44.375 \nQ 74.3125 48.1875 68.609375 48.1875 \nQ 61.625 48.1875 57.5625 43.546875 \nQ 53.515625 38.921875 53.515625 30.90625 \nL 53.515625 0 \nL 44.484375 0 \nL 44.484375 32.71875 \nQ 44.484375 40.625 41.703125 44.40625 \nQ 38.921875 48.1875 33.109375 48.1875 \nQ 26.21875 48.1875 22.15625 43.53125 \nQ 18.109375 38.875 18.109375 30.90625 \nL 18.109375 0 \nL 9.078125 0 \nL 9.078125 54.6875 \nL 18.109375 54.6875 \nL 18.109375 46.1875 \nQ 21.1875 51.21875 25.484375 53.609375 \nQ 29.78125 56 35.6875 56 \nQ 41.65625 56 45.828125 52.96875 \nQ 50 49.953125 52 44.1875 \nz\n\" id=\"DejaVuSans-109\"/>\n <path d=\"M 56.203125 29.59375 \nL 56.203125 25.203125 \nL 14.890625 25.203125 \nQ 15.484375 15.921875 20.484375 11.0625 \nQ 25.484375 6.203125 34.421875 6.203125 \nQ 39.59375 6.203125 44.453125 7.46875 \nQ 49.3125 8.734375 54.109375 11.28125 \nL 54.109375 2.78125 \nQ 49.265625 0.734375 44.1875 -0.34375 \nQ 39.109375 -1.421875 33.890625 -1.421875 \nQ 20.796875 -1.421875 13.15625 6.1875 \nQ 5.515625 13.8125 5.515625 26.8125 \nQ 5.515625 40.234375 12.765625 48.109375 \nQ 20.015625 56 32.328125 56 \nQ 43.359375 56 49.78125 48.890625 \nQ 56.203125 41.796875 56.203125 29.59375 \nz\nM 47.21875 32.234375 \nQ 47.125 39.59375 43.09375 43.984375 \nQ 39.0625 48.390625 32.421875 48.390625 \nQ 24.90625 48.390625 20.390625 44.140625 \nQ 15.875 39.890625 15.1875 32.171875 \nz\n\" id=\"DejaVuSans-101\"/>\n <path id=\"DejaVuSans-32\"/>\n <path d=\"M 31 75.875 \nQ 24.46875 64.65625 21.28125 53.65625 \nQ 18.109375 42.671875 18.109375 31.390625 \nQ 18.109375 20.125 21.3125 9.0625 \nQ 24.515625 -2 31 -13.1875 \nL 23.1875 -13.1875 \nQ 15.875 -1.703125 12.234375 9.375 \nQ 8.59375 20.453125 8.59375 31.390625 \nQ 8.59375 42.28125 12.203125 53.3125 \nQ 15.828125 64.359375 23.1875 75.875 \nz\n\" id=\"DejaVuSans-40\"/>\n <path d=\"M 44.28125 53.078125 \nL 44.28125 44.578125 \nQ 40.484375 46.53125 36.375 47.5 \nQ 32.28125 48.484375 27.875 48.484375 \nQ 21.1875 48.484375 17.84375 46.4375 \nQ 14.5 44.390625 14.5 40.28125 \nQ 14.5 37.15625 16.890625 35.375 \nQ 19.28125 33.59375 26.515625 31.984375 \nL 29.59375 31.296875 \nQ 39.15625 29.25 43.1875 25.515625 \nQ 47.21875 21.78125 47.21875 15.09375 \nQ 47.21875 7.46875 41.1875 3.015625 \nQ 35.15625 -1.421875 24.609375 -1.421875 \nQ 20.21875 -1.421875 15.453125 -0.5625 \nQ 10.6875 0.296875 5.421875 2 \nL 5.421875 11.28125 \nQ 10.40625 8.6875 15.234375 7.390625 \nQ 20.0625 6.109375 24.8125 6.109375 \nQ 31.15625 6.109375 34.5625 8.28125 \nQ 37.984375 10.453125 37.984375 14.40625 \nQ 37.984375 18.0625 35.515625 20.015625 \nQ 33.0625 21.96875 24.703125 23.78125 \nL 21.578125 24.515625 \nQ 13.234375 26.265625 9.515625 29.90625 \nQ 5.8125 33.546875 5.8125 39.890625 \nQ 5.8125 47.609375 11.28125 51.796875 \nQ 16.75 56 26.8125 56 \nQ 31.78125 56 36.171875 55.265625 \nQ 40.578125 54.546875 44.28125 53.078125 \nz\n\" id=\"DejaVuSans-115\"/>\n <path d=\"M 8.015625 75.875 \nL 15.828125 75.875 \nQ 23.140625 64.359375 26.78125 53.3125 \nQ 30.421875 42.28125 30.421875 31.390625 \nQ 30.421875 20.453125 26.78125 9.375 \nQ 23.140625 -1.703125 15.828125 -13.1875 \nL 8.015625 -13.1875 \nQ 14.5 -2 17.703125 9.0625 \nQ 20.90625 20.125 20.90625 31.390625 \nQ 20.90625 42.671875 17.703125 53.65625 \nQ 14.5 64.65625 8.015625 75.875 \nz\n\" id=\"DejaVuSans-41\"/>\n </defs>\n <g transform=\"translate(198.152344 268.034687)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"39.208984\" xlink:href=\"#DejaVuSans-105\"/>\n <use x=\"66.992188\" xlink:href=\"#DejaVuSans-109\"/>\n <use x=\"164.404297\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"225.927734\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"257.714844\" xlink:href=\"#DejaVuSans-40\"/>\n <use x=\"296.728516\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"348.828125\" xlink:href=\"#DejaVuSans-41\"/>\n </g>\n </g>\n </g>\n <g id=\"matplotlib.axis_2\">\n <g id=\"ytick_1\">\n <g id=\"line2d_19\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 229.874489 \nL 384.94375 229.874489 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_20\">\n <defs>\n <path d=\"M 0 0 \nL -3.5 0 \n\" id=\"m6dd8a7d0d7\" style=\"stroke:#000000;stroke-width:0.8;\"/>\n </defs>\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"229.874489\"/>\n </g>\n </g>\n <g id=\"text_11\">\n <!-- 0.00 -->\n <g transform=\"translate(20.878125 233.673707)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_2\">\n <g id=\"line2d_21\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 205.165398 \nL 384.94375 205.165398 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_22\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"205.165398\"/>\n </g>\n </g>\n <g id=\"text_12\">\n <!-- 0.25 -->\n <g transform=\"translate(20.878125 208.964616)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_3\">\n <g id=\"line2d_23\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 180.456307 \nL 384.94375 180.456307 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_24\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"180.456307\"/>\n </g>\n </g>\n <g id=\"text_13\">\n <!-- 0.50 -->\n <g transform=\"translate(20.878125 184.255526)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_4\">\n <g id=\"line2d_25\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 155.747216 \nL 384.94375 155.747216 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_26\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"155.747216\"/>\n </g>\n </g>\n <g id=\"text_14\">\n <!-- 0.75 -->\n <g transform=\"translate(20.878125 159.546435)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_5\">\n <g id=\"line2d_27\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 131.038125 \nL 384.94375 131.038125 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_28\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"131.038125\"/>\n </g>\n </g>\n <g id=\"text_15\">\n <!-- 1.00 -->\n <g transform=\"translate(20.878125 134.837344)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_6\">\n <g id=\"line2d_29\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 106.329034 \nL 384.94375 106.329034 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_30\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"106.329034\"/>\n </g>\n </g>\n <g id=\"text_16\">\n <!-- 1.25 -->\n <g transform=\"translate(20.878125 110.128253)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_7\">\n <g id=\"line2d_31\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 81.619943 \nL 384.94375 81.619943 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_32\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"81.619943\"/>\n </g>\n </g>\n <g id=\"text_17\">\n <!-- 1.50 -->\n <g transform=\"translate(20.878125 85.419162)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-53\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_8\">\n <g id=\"line2d_33\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 56.910852 \nL 384.94375 56.910852 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_34\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"56.910852\"/>\n </g>\n </g>\n <g id=\"text_18\">\n <!-- 1.75 -->\n <g transform=\"translate(20.878125 60.710071)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-49\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-55\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-53\"/>\n </g>\n </g>\n </g>\n <g id=\"ytick_9\">\n <g id=\"line2d_35\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 50.14375 32.201761 \nL 384.94375 32.201761 \n\" style=\"fill:none;stroke:#b0b0b0;stroke-linecap:square;stroke-width:0.8;\"/>\n </g>\n <g id=\"line2d_36\">\n <g>\n <use style=\"stroke:#000000;stroke-width:0.8;\" x=\"50.14375\" xlink:href=\"#m6dd8a7d0d7\" y=\"32.201761\"/>\n </g>\n </g>\n <g id=\"text_19\">\n <!-- 2.00 -->\n <g transform=\"translate(20.878125 36.00098)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-50\"/>\n <use x=\"63.623047\" xlink:href=\"#DejaVuSans-46\"/>\n <use x=\"95.410156\" xlink:href=\"#DejaVuSans-48\"/>\n <use x=\"159.033203\" xlink:href=\"#DejaVuSans-48\"/>\n </g>\n </g>\n </g>\n <g id=\"text_20\">\n <!-- voltage (mV) -->\n <defs>\n <path d=\"M 2.984375 54.6875 \nL 12.5 54.6875 \nL 29.59375 8.796875 \nL 46.6875 54.6875 \nL 56.203125 54.6875 \nL 35.6875 0 \nL 23.484375 0 \nz\n\" id=\"DejaVuSans-118\"/>\n <path d=\"M 30.609375 48.390625 \nQ 23.390625 48.390625 19.1875 42.75 \nQ 14.984375 37.109375 14.984375 27.296875 \nQ 14.984375 17.484375 19.15625 11.84375 \nQ 23.34375 6.203125 30.609375 6.203125 \nQ 37.796875 6.203125 41.984375 11.859375 \nQ 46.1875 17.53125 46.1875 27.296875 \nQ 46.1875 37.015625 41.984375 42.703125 \nQ 37.796875 48.390625 30.609375 48.390625 \nz\nM 30.609375 56 \nQ 42.328125 56 49.015625 48.375 \nQ 55.71875 40.765625 55.71875 27.296875 \nQ 55.71875 13.875 49.015625 6.21875 \nQ 42.328125 -1.421875 30.609375 -1.421875 \nQ 18.84375 -1.421875 12.171875 6.21875 \nQ 5.515625 13.875 5.515625 27.296875 \nQ 5.515625 40.765625 12.171875 48.375 \nQ 18.84375 56 30.609375 56 \nz\n\" id=\"DejaVuSans-111\"/>\n <path d=\"M 9.421875 75.984375 \nL 18.40625 75.984375 \nL 18.40625 0 \nL 9.421875 0 \nz\n\" id=\"DejaVuSans-108\"/>\n <path d=\"M 34.28125 27.484375 \nQ 23.390625 27.484375 19.1875 25 \nQ 14.984375 22.515625 14.984375 16.5 \nQ 14.984375 11.71875 18.140625 8.90625 \nQ 21.296875 6.109375 26.703125 6.109375 \nQ 34.1875 6.109375 38.703125 11.40625 \nQ 43.21875 16.703125 43.21875 25.484375 \nL 43.21875 27.484375 \nz\nM 52.203125 31.203125 \nL 52.203125 0 \nL 43.21875 0 \nL 43.21875 8.296875 \nQ 40.140625 3.328125 35.546875 0.953125 \nQ 30.953125 -1.421875 24.3125 -1.421875 \nQ 15.921875 -1.421875 10.953125 3.296875 \nQ 6 8.015625 6 15.921875 \nQ 6 25.140625 12.171875 29.828125 \nQ 18.359375 34.515625 30.609375 34.515625 \nL 43.21875 34.515625 \nL 43.21875 35.40625 \nQ 43.21875 41.609375 39.140625 45 \nQ 35.0625 48.390625 27.6875 48.390625 \nQ 23 48.390625 18.546875 47.265625 \nQ 14.109375 46.140625 10.015625 43.890625 \nL 10.015625 52.203125 \nQ 14.9375 54.109375 19.578125 55.046875 \nQ 24.21875 56 28.609375 56 \nQ 40.484375 56 46.34375 49.84375 \nQ 52.203125 43.703125 52.203125 31.203125 \nz\n\" id=\"DejaVuSans-97\"/>\n <path d=\"M 45.40625 27.984375 \nQ 45.40625 37.75 41.375 43.109375 \nQ 37.359375 48.484375 30.078125 48.484375 \nQ 22.859375 48.484375 18.828125 43.109375 \nQ 14.796875 37.75 14.796875 27.984375 \nQ 14.796875 18.265625 18.828125 12.890625 \nQ 22.859375 7.515625 30.078125 7.515625 \nQ 37.359375 7.515625 41.375 12.890625 \nQ 45.40625 18.265625 45.40625 27.984375 \nz\nM 54.390625 6.78125 \nQ 54.390625 -7.171875 48.1875 -13.984375 \nQ 42 -20.796875 29.203125 -20.796875 \nQ 24.46875 -20.796875 20.265625 -20.09375 \nQ 16.0625 -19.390625 12.109375 -17.921875 \nL 12.109375 -9.1875 \nQ 16.0625 -11.328125 19.921875 -12.34375 \nQ 23.78125 -13.375 27.78125 -13.375 \nQ 36.625 -13.375 41.015625 -8.765625 \nQ 45.40625 -4.15625 45.40625 5.171875 \nL 45.40625 9.625 \nQ 42.625 4.78125 38.28125 2.390625 \nQ 33.9375 0 27.875 0 \nQ 17.828125 0 11.671875 7.65625 \nQ 5.515625 15.328125 5.515625 27.984375 \nQ 5.515625 40.671875 11.671875 48.328125 \nQ 17.828125 56 27.875 56 \nQ 33.9375 56 38.28125 53.609375 \nQ 42.625 51.21875 45.40625 46.390625 \nL 45.40625 54.6875 \nL 54.390625 54.6875 \nz\n\" id=\"DejaVuSans-103\"/>\n <path d=\"M 28.609375 0 \nL 0.78125 72.90625 \nL 11.078125 72.90625 \nL 34.1875 11.53125 \nL 57.328125 72.90625 \nL 67.578125 72.90625 \nL 39.796875 0 \nz\n\" id=\"DejaVuSans-86\"/>\n </defs>\n <g transform=\"translate(14.798438 163.502187)rotate(-90)scale(0.1 -0.1)\">\n <use xlink:href=\"#DejaVuSans-118\"/>\n <use x=\"59.179688\" xlink:href=\"#DejaVuSans-111\"/>\n <use x=\"120.361328\" xlink:href=\"#DejaVuSans-108\"/>\n <use x=\"148.144531\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"187.353516\" xlink:href=\"#DejaVuSans-97\"/>\n <use x=\"248.632812\" xlink:href=\"#DejaVuSans-103\"/>\n <use x=\"312.109375\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"373.632812\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"405.419922\" xlink:href=\"#DejaVuSans-40\"/>\n <use x=\"444.433594\" xlink:href=\"#DejaVuSans-109\"/>\n <use x=\"541.845703\" xlink:href=\"#DejaVuSans-86\"/>\n <use x=\"610.253906\" xlink:href=\"#DejaVuSans-41\"/>\n </g>\n </g>\n </g>\n <g id=\"line2d_37\">\n <path clip-path=\"url(#p2444f92044)\" d=\"M 65.361932 131.038125 \nL 71.479794 106.458521 \nL 76.06819 88.955648 \nL 79.127121 78.078953 \nL 82.186052 68.037456 \nL 85.244983 58.989517 \nL 88.303914 51.077827 \nL 89.83338 47.587823 \nL 91.362845 44.427159 \nL 92.892311 41.608309 \nL 94.421776 39.142398 \nL 95.951242 37.039157 \nL 97.480708 35.306887 \nL 99.010173 33.952425 \nL 100.539639 32.981116 \nL 102.069104 32.396792 \nL 103.59857 32.201761 \nL 105.128035 32.396792 \nL 106.657501 32.981116 \nL 108.186966 33.952425 \nL 109.716432 35.306887 \nL 111.245897 37.039157 \nL 112.775363 39.142398 \nL 114.304828 41.608309 \nL 115.834294 44.427159 \nL 117.363759 47.587823 \nL 118.893225 51.077827 \nL 120.42269 54.883398 \nL 123.481621 63.379978 \nL 126.540552 72.943568 \nL 129.599483 83.423344 \nL 132.658414 94.654033 \nL 137.246811 112.518037 \nL 151.012 167.422217 \nL 154.070931 178.652906 \nL 157.129862 189.132682 \nL 160.188793 198.696272 \nL 163.247724 207.192852 \nL 164.77719 210.998423 \nL 166.306655 214.488427 \nL 167.836121 217.649091 \nL 169.365586 220.467941 \nL 170.895052 222.933852 \nL 172.424517 225.037093 \nL 173.953983 226.769363 \nL 175.483448 228.123825 \nL 177.012914 229.095134 \nL 178.54238 229.679458 \nL 180.071845 229.874489 \nL 181.601311 229.679458 \nL 183.130776 229.095134 \nL 184.660242 228.123825 \nL 186.189707 226.769363 \nL 187.719173 225.037093 \nL 189.248638 222.933852 \nL 190.778104 220.467941 \nL 192.307569 217.649091 \nL 193.837035 214.488427 \nL 195.3665 210.998423 \nL 196.895966 207.192852 \nL 199.954897 198.696272 \nL 203.013828 189.132682 \nL 206.072759 178.652906 \nL 209.13169 167.422217 \nL 213.720086 149.558213 \nL 227.485276 94.654033 \nL 230.544207 83.423344 \nL 233.603138 72.943568 \nL 236.662069 63.379978 \nL 239.721 54.883398 \nL 241.250465 51.077827 \nL 242.779931 47.587823 \nL 244.309396 44.427159 \nL 245.838862 41.608309 \nL 247.368327 39.142398 \nL 248.897793 37.039157 \nL 250.427258 35.306887 \nL 251.956724 33.952425 \nL 253.486189 32.981116 \nL 255.015655 32.396792 \nL 256.54512 32.201761 \nL 258.074586 32.396792 \nL 259.604052 32.981116 \nL 261.133517 33.952425 \nL 262.662983 35.306887 \nL 264.192448 37.039157 \nL 265.721914 39.142398 \nL 267.251379 41.608309 \nL 268.780845 44.427159 \nL 270.31031 47.587823 \nL 271.839776 51.077827 \nL 273.369241 54.883398 \nL 276.428172 63.379978 \nL 279.487103 72.943568 \nL 282.546034 83.423344 \nL 285.604965 94.654033 \nL 290.193362 112.518037 \nL 303.958551 167.422217 \nL 307.017482 178.652906 \nL 310.076413 189.132682 \nL 313.135344 198.696272 \nL 316.194275 207.192852 \nL 317.723741 210.998423 \nL 319.253206 214.488427 \nL 320.782672 217.649091 \nL 322.312137 220.467941 \nL 323.841603 222.933852 \nL 325.371068 225.037093 \nL 326.900534 226.769363 \nL 328.429999 228.123825 \nL 329.959465 229.095134 \nL 331.48893 229.679458 \nL 333.018396 229.874489 \nL 334.547861 229.679458 \nL 336.077327 229.095134 \nL 337.606792 228.123825 \nL 339.136258 226.769363 \nL 340.665724 225.037093 \nL 342.195189 222.933852 \nL 343.724655 220.467941 \nL 345.25412 217.649091 \nL 346.783586 214.488427 \nL 348.313051 210.998423 \nL 349.842517 207.192852 \nL 352.901448 198.696272 \nL 355.960379 189.132682 \nL 359.01931 178.652906 \nL 362.078241 167.422217 \nL 366.666637 149.558213 \nL 369.725568 137.244112 \nL 369.725568 137.244112 \n\" style=\"fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;\"/>\n </g>\n <g id=\"patch_3\">\n <path d=\"M 50.14375 239.758125 \nL 50.14375 22.318125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_4\">\n <path d=\"M 384.94375 239.758125 \nL 384.94375 22.318125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_5\">\n <path d=\"M 50.14375 239.758125 \nL 384.94375 239.758125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"patch_6\">\n <path d=\"M 50.14375 22.318125 \nL 384.94375 22.318125 \n\" style=\"fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;\"/>\n </g>\n <g id=\"text_21\">\n <!-- About as simple as it gets, folks -->\n <defs>\n <path d=\"M 34.1875 63.1875 \nL 20.796875 26.90625 \nL 47.609375 26.90625 \nz\nM 28.609375 72.90625 \nL 39.796875 72.90625 \nL 67.578125 0 \nL 57.328125 0 \nL 50.6875 18.703125 \nL 17.828125 18.703125 \nL 11.1875 0 \nL 0.78125 0 \nz\n\" id=\"DejaVuSans-65\"/>\n <path d=\"M 48.6875 27.296875 \nQ 48.6875 37.203125 44.609375 42.84375 \nQ 40.53125 48.484375 33.40625 48.484375 \nQ 26.265625 48.484375 22.1875 42.84375 \nQ 18.109375 37.203125 18.109375 27.296875 \nQ 18.109375 17.390625 22.1875 11.75 \nQ 26.265625 6.109375 33.40625 6.109375 \nQ 40.53125 6.109375 44.609375 11.75 \nQ 48.6875 17.390625 48.6875 27.296875 \nz\nM 18.109375 46.390625 \nQ 20.953125 51.265625 25.265625 53.625 \nQ 29.59375 56 35.59375 56 \nQ 45.5625 56 51.78125 48.09375 \nQ 58.015625 40.1875 58.015625 27.296875 \nQ 58.015625 14.40625 51.78125 6.484375 \nQ 45.5625 -1.421875 35.59375 -1.421875 \nQ 29.59375 -1.421875 25.265625 0.953125 \nQ 20.953125 3.328125 18.109375 8.203125 \nL 18.109375 0 \nL 9.078125 0 \nL 9.078125 75.984375 \nL 18.109375 75.984375 \nz\n\" id=\"DejaVuSans-98\"/>\n <path d=\"M 8.5 21.578125 \nL 8.5 54.6875 \nL 17.484375 54.6875 \nL 17.484375 21.921875 \nQ 17.484375 14.15625 20.5 10.265625 \nQ 23.53125 6.390625 29.59375 6.390625 \nQ 36.859375 6.390625 41.078125 11.03125 \nQ 45.3125 15.671875 45.3125 23.6875 \nL 45.3125 54.6875 \nL 54.296875 54.6875 \nL 54.296875 0 \nL 45.3125 0 \nL 45.3125 8.40625 \nQ 42.046875 3.421875 37.71875 1 \nQ 33.40625 -1.421875 27.6875 -1.421875 \nQ 18.265625 -1.421875 13.375 4.4375 \nQ 8.5 10.296875 8.5 21.578125 \nz\nM 31.109375 56 \nz\n\" id=\"DejaVuSans-117\"/>\n <path d=\"M 18.109375 8.203125 \nL 18.109375 -20.796875 \nL 9.078125 -20.796875 \nL 9.078125 54.6875 \nL 18.109375 54.6875 \nL 18.109375 46.390625 \nQ 20.953125 51.265625 25.265625 53.625 \nQ 29.59375 56 35.59375 56 \nQ 45.5625 56 51.78125 48.09375 \nQ 58.015625 40.1875 58.015625 27.296875 \nQ 58.015625 14.40625 51.78125 6.484375 \nQ 45.5625 -1.421875 35.59375 -1.421875 \nQ 29.59375 -1.421875 25.265625 0.953125 \nQ 20.953125 3.328125 18.109375 8.203125 \nz\nM 48.6875 27.296875 \nQ 48.6875 37.203125 44.609375 42.84375 \nQ 40.53125 48.484375 33.40625 48.484375 \nQ 26.265625 48.484375 22.1875 42.84375 \nQ 18.109375 37.203125 18.109375 27.296875 \nQ 18.109375 17.390625 22.1875 11.75 \nQ 26.265625 6.109375 33.40625 6.109375 \nQ 40.53125 6.109375 44.609375 11.75 \nQ 48.6875 17.390625 48.6875 27.296875 \nz\n\" id=\"DejaVuSans-112\"/>\n <path d=\"M 11.71875 12.40625 \nL 22.015625 12.40625 \nL 22.015625 4 \nL 14.015625 -11.625 \nL 7.71875 -11.625 \nL 11.71875 4 \nz\n\" id=\"DejaVuSans-44\"/>\n <path d=\"M 37.109375 75.984375 \nL 37.109375 68.5 \nL 28.515625 68.5 \nQ 23.6875 68.5 21.796875 66.546875 \nQ 19.921875 64.59375 19.921875 59.515625 \nL 19.921875 54.6875 \nL 34.71875 54.6875 \nL 34.71875 47.703125 \nL 19.921875 47.703125 \nL 19.921875 0 \nL 10.890625 0 \nL 10.890625 47.703125 \nL 2.296875 47.703125 \nL 2.296875 54.6875 \nL 10.890625 54.6875 \nL 10.890625 58.5 \nQ 10.890625 67.625 15.140625 71.796875 \nQ 19.390625 75.984375 28.609375 75.984375 \nz\n\" id=\"DejaVuSans-102\"/>\n <path d=\"M 9.078125 75.984375 \nL 18.109375 75.984375 \nL 18.109375 31.109375 \nL 44.921875 54.6875 \nL 56.390625 54.6875 \nL 27.390625 29.109375 \nL 57.625 0 \nL 45.90625 0 \nL 18.109375 26.703125 \nL 18.109375 0 \nL 9.078125 0 \nz\n\" id=\"DejaVuSans-107\"/>\n </defs>\n <g transform=\"translate(121.998438 16.318125)scale(0.12 -0.12)\">\n <use xlink:href=\"#DejaVuSans-65\"/>\n <use x=\"68.408203\" xlink:href=\"#DejaVuSans-98\"/>\n <use x=\"131.884766\" xlink:href=\"#DejaVuSans-111\"/>\n <use x=\"193.066406\" xlink:href=\"#DejaVuSans-117\"/>\n <use x=\"256.445312\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"295.654297\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"327.441406\" xlink:href=\"#DejaVuSans-97\"/>\n <use x=\"388.720703\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"440.820312\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"472.607422\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"524.707031\" xlink:href=\"#DejaVuSans-105\"/>\n <use x=\"552.490234\" xlink:href=\"#DejaVuSans-109\"/>\n <use x=\"649.902344\" xlink:href=\"#DejaVuSans-112\"/>\n <use x=\"713.378906\" xlink:href=\"#DejaVuSans-108\"/>\n <use x=\"741.162109\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"802.685547\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"834.472656\" xlink:href=\"#DejaVuSans-97\"/>\n <use x=\"895.751953\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"947.851562\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"979.638672\" xlink:href=\"#DejaVuSans-105\"/>\n <use x=\"1007.421875\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"1046.630859\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"1078.417969\" xlink:href=\"#DejaVuSans-103\"/>\n <use x=\"1141.894531\" xlink:href=\"#DejaVuSans-101\"/>\n <use x=\"1203.417969\" xlink:href=\"#DejaVuSans-116\"/>\n <use x=\"1242.626953\" xlink:href=\"#DejaVuSans-115\"/>\n <use x=\"1294.726562\" xlink:href=\"#DejaVuSans-44\"/>\n <use x=\"1326.513672\" xlink:href=\"#DejaVuSans-32\"/>\n <use x=\"1358.300781\" xlink:href=\"#DejaVuSans-102\"/>\n <use x=\"1393.505859\" xlink:href=\"#DejaVuSans-111\"/>\n <use x=\"1454.6875\" xlink:href=\"#DejaVuSans-108\"/>\n <use x=\"1482.470703\" xlink:href=\"#DejaVuSans-107\"/>\n <use x=\"1540.380859\" xlink:href=\"#DejaVuSans-115\"/>\n </g>\n </g>\n </g>\n </g>\n <defs>\n <clipPath id=\"p2444f92044\">\n <rect height=\"217.44\" width=\"334.8\" x=\"50.14375\" y=\"22.318125\"/>\n </clipPath>\n </defs>\n</svg>\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYgAAAEWCAYAAAB8LwAVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nOydeXxU1fn/30/2lewJECAhIYCgCAbZFAW1gtVW29pWa1vtZq312721/fZX29rtu7bf9ms396+tSq1Vq9alLgRBCJuAsmcDwpoNyEaWSZ7fH/eOjjHLJJk7d2Zy3q/Xfc3MXc75zJ0z88x5nnPOI6qKwWAwGAx9iXJbgMFgMBhCE2MgDAaDwdAvxkAYDAaDoV+MgTAYDAZDvxgDYTAYDIZ+MQbCYDAYDP1iDIThbUTkQRH5qds6nEBEbhCRfzpUtqv3TUSWisg+t+ofKWLxgIicFJFNfpyvIjLNfh6xbTWUMAZiDCIiZfaXMj5I9RXaX+6YYNTXH6r6sKpe7lb9TqKqa1V1hve1iBwQkcucqEtElonI4QAVdyHwPmCSqi4IUJmGAGIMxBhDRAqBpYACH3RVjGGsUwAcUNU2t4UY+scYiLHHp4Fy4EHgxn6OZ4vISyLSIiJrRKTAe0BElojIZhE5bT8u8Tn2rn+tIvIjEfmz/fI1+/GUiLSKyOK+lYrIAhHZICKnROSYiNwlInH2MRGRX4lInYg0i8hbInJ2f29ORG4SkWpbf42I3OCzf53PeSoit4pIhX3uT0SkWETW23U85lP/MhE5LCL/KiIN9nu9YaAbLCJXich2+72sF5E5g5z7axGptevcKiJL+9yTLfaxEyLyywHKePtfvYj8CZgCPGPf6+8McM137Pt8VEQ+38d9Ey8i/yUih+x6/yAiiSKSDDwPTLTLbhWRif7q7FP/54B7gcV2OT+2939BRCpFpElEnhaRiX6UlSoiq0XkN3Zbeb+I7LY/1yMi8q2hyjAMgKqabQxtQCVwK1AKdAN5PsceBFqAi4B44NfAOvtYJnAS+BQQA1xvv86yjx8ALvMp60fAn+3nhVg9lphBdJUCi+yyC4E9wNfsYyuArUA6IMBZwIR+ykgGmoEZ9usJwGz7+U3e92K/VuDvwDhgNtAJvAIUAWnAbuBG+9xlgAf4pX1fLgbafOp5EPip/XweUAcsBKKxjPABIH6A9/1JIMt+398EjgMJ9rENwKfs5ynAogHKWAYc9nn9rs+in/NX2vXMBpKAP9v3Y5p9/FfA0/Znngo8A/yiv7qGo7MfHX0/k0uABuA8+z7/L/Ban8/Mq/FB4Kf2vdvkvf/2sWPAUvt5BnCe29+7cN1MD2IMISIXYnXrH1PVrUAV8Ik+p/1DVV9T1U7g+1j/8CYDVwIVqvonVfWo6qPAXuADgdCmqltVtdwu+wDwR6wfYrAMWSowExBV3aOqxwYoqhc4W0QSVfWYqu4apNr/UNVm+5ydwD9VtVpVT2P9U57X5/wfqGqnqq4B/gF8rJ8ybwb+qKobVbVHVf8Py/gsGuB9/1lVG+33/d9YP4zeeEI3ME1EslW1VVXLB3kvw+FjwAOquktV27GMOWD11uz38HVVbVLVFuDnwHWDlBconTcA96vqG3b7+x5W+ysc4PyJwBrgr6r6//romSUi41T1pKq+MUI9Yx5jIMYWN2L9CDbYrx/hvW6mWu8TVW0FmrC+iBOBg33OPQjkB0KYiEwXkWdF5LiINGP9KGXbOl4F7gJ+C9SJyN0iMq5vGWr5sj8O3AIcE5F/iMjMQao94fP8TD+vU3xen9R3+8oPYt2TvhQA37TdS6dE5BQweYBzEZFvicgesdx2p7B6L9n24c8B04G9Yrn0rhrkvQyHifh8zn2e52D1Krb66H/B3j8QgdL5rjZmt79GBm5jVwKJwB/67P8I8H7goFhu0ve4NA3+YQzEGEFEErH+OV5s/wgfB74OnCsi5/qcOtnnmhQsN8NReyvg3UwBjtjP27B+WLyM93nuz5LBv8fqkZSo6jjgX7HcSVYBqr9R1VJgFtaP0bf7K0RVX1TV92G5l/YC9/hRtz9k2D54L1Ow7klfaoGfqWq6z5Zk97jehR1v+A7W55KhqunAaez3raoVqno9kAv8O/B4Hw0DMdT9PgZM8nk92ed5A5ZxnO2jP01VvcbyPWWPQmdf3tXG7DKyeKeN9eUeLOP1nG99qrpZVa+29TwFPDYCLQaMgRhLXAP0YP3AzrW3s4C1WIFrL+8XkQvtAO1PgHJVrQWeA6aLyCdEJEZEPm6X9ax93XbgOhGJFZH5wLU+ZdZjuX6KBtGXihU/aLX/9X/Je0BEzheRhSISi2WIOuzy3oWI5InI1faPRSfQ2t95o+DHIhJn/7BfBfy1n3PuAW6x9YqIJIvIlSKS2s+5qVixjXogRkTuwIqJeN/PJ0UkR1V7gVP2bn/ezwkGv9ePAZ8RkbNEJAn4gfeAXdc9wK9EJNfWkS8iK3zKzhKRNH90ihXQv8kPzQCP2rrmijUE++fARtvlOBC3AfuwgvKJ9udzg4ikqWo3VpsKZBsYUxgDMXa4EcvvfEhVj3s3LNfNDfLOHIVHgB9iuZZKsYKoqGoj1o/iN7G6/d8BrvJxV/0AKMYKXP/YLgf72nbgZ8DrttuiP3/8t7DiIS1YP1B/8Tk2zt53EssF0Qj8Zz9lRAHfwPon2oQVw/hSP+eNhON2/UeBh4FbVHVv35NUdQvwBaz7ehJrUMBNA5T5ItY/4P1Y76uDd7t7VgK7RKQVa8DAdap6xg+tvwD+n32v3zOCR1WfB34DrLb1eWMGnfbj7d79trvvZey4iP2eHwWq7fInDqTT/pOR5VP+oKjqy1jt6G9YvZxiBo99oKqKFTM5jDXoIAFrIMUBW/stWLENwwgQ6/4aDIaBEJFlWCOyJg11bjgiImdhBenjVdUTwHIvBL5su58MYYjpQRgMYxAR+ZBY8x0ysOIGzwTSOACo6jpjHMIbYyAMhrHJF7Hma1RhxaYC5YozRBDGxWQwGAyGfjE9CIPBYDD0i2urazpBdna2FhYWjujatrY2kpNHMnTbWYyu4ROq2oyu4WF0DZ+RaNu6dWuDqvY/ETLYa3s4uZWWlupIWb169YivdRKja/iEqjaja3gYXcNnJNqALWrWYjIYDAbDcDAGwmAwGAz9YgyEwWAwGPrFGAiDwWAw9IsxEAaDwWDoF8cMhIhMttMA7haRXSLy1X7OETtNYKWIvCki5/kcu1GsdJAVItJfakyDwWAwOIiT8yA8wDdV9Q17qeOtIvKSqu72OecKoMTeFmLlBFgoIplYK4rOx1p/fquIPK2qJx3UazAYDAYfHDMQaqWEPGY/bxGRPViZoXwNxNXAQ/ZY3HIRSReRCVh5b19S1SYAEXkJa0nh9yRdiUTauzy8XtnIwcY2Kmq66cg+zoUl2aTER9S8RoOLtHZ6WFfRwKGmNqpquunKsdpYUpxpY4Z3CMpaTHZO2deAs1W12Wf/s8C/qeo6+/UrWGvRL8NK3P5Te/8PgDOq+l/9lH0z1nrw5OXlla5atWpEGltbW0lJSRn6RAc541GequxiTa2Hjp53H4uLhgvzY/hISRzJsdJ/AUEkFO7XQISqtlDQ1datPFHRxdojHrr6tLGEaLh4UgzXlMSRGGPa2ECEqi4Ymbbly5dvVdX5/R1z/O+Cnbbyb8DXfI1DoFDVu4G7AebPn6/Lli0bUTllZWWM9NpAsO3QSb738Bscb/Zwzdx8Pjp/ErMnpLHu9XXkTDuXv209zONvHObNph5+c/1clhRnD12og7h9vwYjVLW5rWtDVSPfenQbJ9t7uPa8yXykdBIzxqeybt06MorO4fEth3ly+xHePBXDb284j/OmZLimFdy/XwMRqrog8NocHcVkp4j8G/Cwqj7RzylHeHc+3En2voH2RyQv7z7B9feUExsdxd++tIRffdwyAGlJsSTHCgumZvLv187h71++gIykWG68fxNP7+gvHbLB0D/P7DjKjfdvIj0plr9/+QL+/do5LJiaSVqi1caWFGfzy4/P5YkvLSEuJopP3FPOy7tPuC3b4DJOjmIS4D5gj6r+coDTngY+bY9mWgSctmMXLwKXi0iGndDkcntfxLG+qoEvPbyVGXmpPHHrkkH/tZ2dn8bjtyxh3pQMvrZqG6/sMV9gw9C8sucEX121jbmT0/nbLUs4Oz9twHPnTcngiS8tYcb4cXzp4a2sr2oY8FxD5ONkD+ICrNywl4jIdnt7v4jcIiK32Oc8B1Rj5b+9B7gVwA5O/wTYbG93egPWkURlXQtf/NNWCrOSeeizC8lOiR/ymrSkWB646XxmT0zjtke2sfPI6SAoNYQrO4+c5rZHtjF7YhoPfOZ80pJih7wmKyWehz67gKnZyXzxoa1UnGgJglJDKOKYgVAr3aCo6hxVnWtvz6nqH1T1D/Y5qqpfVtViVT1HrYTv3uvvV9Vp9vaAUzrdoqO7h9se2UZcdBQPfnaBX19cL8nxMdx/0/lkJMXy5UfeoLUzoJkiDRFCa6eH2x55g/SkWO6/6XyShzEKLi0xlgc/s4D42Chue2QbHd09Q19kiDjMTGqX+Plze9h7vIX/+ti55KcnDvv6nNR4/ue6edQ2tXPHUzsdUGgId+54aieHmtr59XXzyEkdunfal4npifzXR89l34kWfvaPPQ4oNIQ6xkC4wOYDTTy04SCfvWAqy2fkjricBVMzue2SEp7YdoTV++oCqNAQ7qzeV8cT245w2yUlLJiaOeJyls3I5XMXTuVP5QfZVBNxXl7DEBgDEWS6PL18/8m3yE9P5Fsrpo+6vC8vL6Y4J5kfPLWTM30HthvGJGe6erjj7zspzknmy8uLR13eNy+fTn56It9/8i26PL0BUGgIF4yBCDL/t/4A+0+08uMPzg7IrNX4mGh+es05HD55ht+XVQZAoSHc+cOaKmqbzvDTa84hPiZ61OUlxcVw59Wzqahr5cH1NQFQaAgXjIEIIqfbu7lrdSUXT8/hsll5ASt3cXEWV86ZwD1ra6hr7ghYuYbwo66lg3vWVnPlnAksLs4KWLmXnpXHshk5/HZ1FafbuwNWriG0MQYiiPyurJLmjm6+e8XMgJf97ctn0N3Ty/+8UhHwsg3hw69frqDL08u3L58R8LJvXzmT5o5ufmd6qmMGYyCCRF1LBw+uP8CH5uZz1oRxAS+/MDuZGxZO4S+baznU2B7w8g2hT21TO6s21/KJhVMozE4OePlnTRjHh+bl88D6A6anOkYwBiJI3Lu2hu6eXr5yaYljddy6fBrRIvx+TZVjdRhCl9+vqSJahC8vn+ZYHV+5pARPTy/3rjOxiLGAMRBB4GRbF38uP8gHzp3oyD87L3njEvjo/Ek8vrWWY6fPOFaPIfQ4frqDx7cc5qPzJ5E3LsGxegqzk/nAuRP5c/lBTrZ1OVaPITQwBiIIPLj+AO1dPY7+s/Nyy8XFqFo9FsPY4d611fSocsvFox/WOhRfXj6N9q4eHlx/wPG6DO5iDITDdHT38PDGg1w6M5fpeamO1zc5M4kr50zgL5traekwo03GAq2dHv6yuZar5kxgcmaS4/VNz0vl0pm5PLzxoFmCI8IxBsJhntlxlIbWLj574dSg1fmZC6bS2unhr1sOB61Og3v8dUstLZ0ePnNB8NrYZy+cSkNrl1l2PsIxBsJBVJX7Xz/AjLxUlgRwTPpQzJ2cTmlBBg+uP0BPr/MZAw3u0dOrPLj+AKUFGcydnB60epcUZzEjL5X719UQjKyUBncwBsJBttWeYs+xZm5cUoiVHiN4fOaCQg41tfNaRX1Q6zUEl9cq6jnY2M6NSwqDWq+I8JkLCtl7vIWtB08GtW5D8DAGwkEe21xLYmw0Hzh3QtDrvnzWeLJT4nhk46Gg120IHo9uPERWchwrZ48Pet0fOHciKfExpo1FMMZAOERbp4dndhzlyjkTSE3wP9dDoIiLieLa0sm8ureO46fNpKZI5ERzB6/sreOj8ycTFxP8r3JyfAzXzJvIs28d41S7GfIaiTiZcvR+EakTkX6TFYjIt30yze0UkR4RybSPHRCRt+xjW/q7PtT5x1vHaOvq4ePnTx76ZIe4fsFkenqVx7bUuqbB4ByPba6lp1e5foF7bewTCwro8vTyxBsRmzJ+TOPk344HgZUDHVTV//RmmgO+B6zpk1Z0uX18voMaHeOxzbUU5SQzv2DgHNNOU5CVzJLiLJ5447AJJEYYqsoT246wuCiLgiznJl8OxayJ45gzKY0ntpkRc5GIkylHXwP8zTByPfCoU1qCTWVdK1sOnuRj8ycHPTjdl2vm5XOgsZ3ttadc1WEILDsOn6amoY0PnZfvthSumZvPziPNJnd1BCJO/rMUkULgWVU9e5BzkoDDwDRvD0JEaoCTgAJ/VNW7B7n+ZuBmgLy8vNJVq1aNSGtrayspKSkjurYvf9nXxYsHuvnlskTS40dng0erq71b+erqdi6aFMOnZg0/7aRTupwkVLUFUtefdnfy2mEPv16eRFLs6P6EjFbX6U7l62XtvH9qLNdOjxuVlkDqcopQ1QUj07Z8+fKtA3pqVNWxDSgEdg5xzseBZ/rsy7cfc4EdwEX+1FdaWqojZfXq1SO+1pcuT4+W/uQl/fz/bQ5IeYHQdevDW3Xuj1/ULk/P6AXZBOp+OUGoagtkG5t35z/11oe3BqS8QOj69H0bdckvXtGent7RC7KJ9M/RCUaiDdiiA/ymhsIopuvo415S1SP2Yx3wJLDABV0jYkNVIw2tnVxbOsltKW/z4Xn5nGzv5rX9Zk5EJLC2op6mti4+NNd995KXD5+Xz5FTZ9h8wOStjiRcNRAikgZcDPzdZ1+yiKR6nwOXA/2OhApFnn3zKKnxMVw8PcdtKW9z0fQcMpJieXKbGWkSCTy57SgZSbFcFEJt7H2z8kiKi+ap7aaNRRJODnN9FNgAzBCRwyLyORG5RURu8TntQ8A/VbXNZ18esE5EdgCbgH+o6gtO6QwkXZ5eXth5nPfNyiMhdvS5gANFbHQUHzh3Ii/tPkGzWcAvrGnp6Oafu45z1ZyJrsx9GIikuBhWzh7Ps28eMwv4RRBOjmK6XlUnqGqsqk5S1ftU9Q+q+gefcx5U1ev6XFetqufa22xV/ZlTGgPN65UNNHd4uHJO8GdOD8U18/LptA2YIXx5YedxOj29XDMvdNxLXq6Zl09Lh4fVe+vclmIIEKHzFyQCeObNo4xLiGFpSeh0/b3Mm5xOQVYSz5jVN8Oap3ccZUpmEudNCd7CfP6ypDiLnNR4s8JrBGEMRIDo6O7hpV0nWDF7fEh1/b2ICFecPYENVY2cbjdupnDkdHs3G6oaueKc8a7Pr+mPmOgoVszOo2xfPWe6jJspEgi9X7IwZW1FAy2doele8rLy7PF4epVX9p5wW4phBLyy9wSeXnVlYT5/WTl7Ame6e8wqwhGCMRAB4tk3j5KeFMsF07LdljIgc/LTmJCWYOIQYcqLu44zflwC504KPfeSl4VFmaQlxvLiLtPGIgFjIAJAR3cPL+8+wcrZ44mNDt1bGhUlrJg9njX762nv8rgtxzAM2rs8rNlfz4rZeURFhZ57yUtsdBSXnZXHy7tP0N3T67YcwygJ3V+zMOL1ygbaunq44pzQdS95uXx2Hp2eXtbsMy6AcOK1/fV0dPey4uzQdS95WTE7j+YOD+XVjW5LMYwSYyACwMt7TpASH8Oioky3pQzJgsJMMpJiecG4AMKKF3YeJyMplgWFod/GLpqeQ2JstHFlRgDGQIyS3l7l5T11XDw9h/iY0JkcNxAx0VG8b1Yer+6po8tjXADhQJenl1f21PG+WXnEhLAL00tCbDTLZ+bw4q4TJid6mBP6rS3EefPIaepbOrlsVq7bUvxm5dnjaen0sL6qwW0pBj9YX2WNkFsZBu4lLytmj6ehtZNth0y+6nDGGIhR8vLuE0RHCctnhI+BWFKcTUp8jBlpEia8uOs4KfExLCkO3RFyfblkZi5x0VHGzRTmGAMxSl7ec4LzCzNITwrcOvhOkxAbzcXTc3hlT53JNBfiqCqv2C7MUFrfayhSE2JZVJzFK2bZjbDGGIhRUNvUzt7jLVx2Vp7bUobN8pm51LV0sutos9tSDIOw62gzdS2dLJ8ZPj1UL5fMyKGmoY2ahrahTzaEJMZAjIKX91gzkt83K/wMxLIZOYhgFlYLcbyfz7IZobe+11BcMtP6Xpg2Fr4YAzEKXt5zgpLcFFeTxo+U7JR45kxK59V95ssbyry6r45zJ6WRnRK4dLHBYkpWEsU5yaw2bSxsMQZihDR3dLOxuonLwrD34GX5jBy2156isbXTbSmGfmhq62J77amwdC95WT4jl43VTbR1mpn74YgxECNkXUUDnl7l0jD+8l4yMxdVWGNSkYYka/bXoUpYjZDryyUzc+nq6eX1SjOkOhxxMqPc/SJSJyL9pgsVkWUiclpEttvbHT7HVorIPhGpFJHvOqVxNKzZV8+4hBjmTg7dhdOG4uyJluviVeMjDkle3VtPdko85+SnuS1lxMwvzCQlPsa4mcIUJ3sQDwIrhzhnrarOtbc7AUQkGvgtcAUwC7heRGY5qHPYqCpr9teztCQnLGa2DkRUlLBsRg6v7a/HYxZWCyk8Pb2s2VfHshk5Ib0431DExURx4bRsVu+tN0OqwxAnU46+BjSN4NIFQKWderQLWAVcHVBxo2T/iVaON3dwcQgljR8pl8zMpbnDwxuHTrktxeDDttpTNHd4wtq95OWSmbkcb+5gz7EWt6UYhkmMy/UvFpEdwFHgW6q6C8gHan3OOQwsHKgAEbkZuBkgLy+PsrKyEQlpbW31+9rna6yMbLGNFZSVVY2oPn8Zjq4R0a1ECzz4zy20z/B/sp/jukZBqGobjq6/7usiWkBO7KWsbF/I6BoJcZ1W7/S+58v5QHH4t7FQ1QUOaFNVxzagENg5wLFxQIr9/P1Ahf38WuBen/M+BdzlT32lpaU6UlavXu33uZ+4Z4Ou+NWaEdc1HIaja6Rc98fhv59g6BopoaptOLpW/s9r+rE/rHdOjA/BuF9X/WatfuR3rw/rmkj4HIPNSLQBW3SA31TXHOiq2qyqrfbz54BYEckGjgCTfU6dZO8LCdo6PWyuOclFEeBe8rJ0ejZ7j7dQ19zhthQDUNfSwZ5jzZHVxkqy2VZ7ipYOkw89nHDNQIjIeLEzr4vIAltLI7AZKBGRqSISB1wHPO2Wzr6UVzfS1dMbEfEHLxeVWO9lnRmKGBJ4h4R6P5dIYGlJDj29yoYqk0QonHBymOujwAZghogcFpHPicgtInKLfcq1wE47BvEb4Dq7x+MBbgNeBPYAj6kVmwgJ1uyvJzE2mvmFGW5LCRizJowjMzmOdRXGQIQCaysayEiKZfbEcW5LCRjnFaSTFBdt/oSEGY4FqVX1+iGO3wXcNcCx54DnnNA1Wtbsr2dJcVZYJAfyl6go4YJp2bxW0YCqYnfsDC6gqqytaODCkvAe3tqX+JhoFhVlsdb8CQkrwncQvwscaGjjYGM7F4fhwmlDsbQkm4bWTvYeN0MR3WTfiRbqWzpZWhI+uR/8ZWlJNjUNbdQ2tbstxeAnxkAMg7V293hpBPmGvXh/kNZWmGU33GTtfm8bi0wDAZheRBhhDMQwWF/ZQH56IoVZSW5LCTgT0hIpyU0xX16XWVvZwLTcFCakJbotJeAU56QwIS2BdZXmT0i4YAyEn/T2KhuqG1lSnBWxPvoLS7LZVNNER3eP21LGJB3dPWysbozI3gOAiLC0JJt1FQ309JplN8IBYyD8ZPexZk61d3PBtMj88oI1rLLT08vmAyNZIcUwWrYcOEmnpzeihrf2ZWlJDs0dHt48bJZ2CQeMgfAT79j0JcVZLitxjoVFmcRGi3EzucTaynpio4WFRZluS3GMC6ZlI2LiEOGCMRB+sq6ygZLcFHLHJbgtxTGS4mIoLcgwX16XWFfRQGlBBklxbi+R5hyZyXGcPTHNzLkJE4yB8INOTw+bDzRFtHvJy4XTstlzrJmmti63pYwpTrV3sftYMxcUR34bu2BaNttqT9LeZbLMhTrGQPjBtkOn6OjujWj3kpfF9nssrzZLIgSTTTVNqMKiMdLGunuULQdOui3FMATGQPjB+soGogQWFkX+l3fOJGtJhPVVxgUQTDZUN5IQG8WcSeGbPc5fzi/MICZKWG/WZQp5jIHwg9erGpkzKZ20xFi3pThObHQUC6ZmmkXVgkx5dROlBRkRtYTLQCTFxTBvSjobTC815DEGYghaOz3sqD3FBdMiv/fgZUlxFlX1bZwwy38HhVPtXew93syiqWOnjS0uzuatw6doNst/hzTGQAzBpppGPL06JoKHXhYXWe/V9CKCw0Y7/rB4DMQfvCwuyqJXYVO1mXMTygxqIERkkoh8S0T+LiKbReQ1EfmdiFwpImPCuKyraCQ+JorzCiJnee+hmDVxHOMSYkwcIkiUvx1/SHdbStCYNyWd+JgoE4cIcQYccC0iD2Dlh34W+HegDkgApgMrge+LyHdV9bVgCHWL9VUNzC/MICE28n3DXqKjhEVFWcZHHCQ2VDUyvyCTuJgx8Z8LgAQ7p4ppY6HNYC3yv1X1clX9jaquV9VKVd2pqk+o6r8Ay4CjA10sIveLSJ2I7Bzg+A0i8qaIvCUi60XkXJ9jB+z920Vky0jf3GjxLn+9ZAy5l7wsKc6itumMWZrZYU62dbH3eAuLInj29EAsKTZzbkKdwQzEFSIyaaCDqtqlqpWDXP8gVk9jIGqAi1X1HOAnwN19ji9X1bmqOn+QMhzF2/0dCxPk+rK42MQhgsHGGssHv2gMDKHui/c9mzk3octgBmIisEFE1orIrSIyrBXEbNfTgBEou1finSlTDgxojNxifWUDqQkxnJMf+WPT+zI9L4Ws5DgTh3CY8upGEmOjx1T8wcucSWkkmzk3IY2oDrzsrljrWl8EXAdcA+wAHgWeUNUhU4+JSCHwrKqePcR53wJmqurn7dc1wElAgT+qat/ehe+1NwM3A+Tl5ZWuWrVqKFn90traSkpKyrv2fXtNO5NSo/jqee6tv9SfrmDxu+0d7D/Zy6+WJb5niXM3dQ1FqGrrT060oJcAACAASURBVNcPXj/DuDj49vnu5X9w8379cmsH9e29/GLpe3OshNPnGCqMRNvy5cu3DuipUVW/NiAaWAFsA9r9vKYQ2DnEOcuBPUCWz758+zEXyyhd5E99paWlOlJWr179rteHT7Zrwe3P6n1rq0dcZiDoqyuYPFx+UAtuf1Yr61rec8xNXUMRqtr66mpq7dSC25/Vu16tcEeQjZv36+41VVpw+7N6/PSZ9xwLl88xlBiJNmCLDvCb6tewCRE5B7gT+C3QCXxvWCZq4HLnAPcCV6vq245IVT1iP9YBTwILAlHfcNho+0XHom/Yi3dcvhmK6Awba7xtbOwFqL1425iJdYUmAxoIESkRkR+IyC7gYaANuFxVF6nqr0dbsYhMAZ4APqWq+332J4tIqvc5cDnQ70goJymvbiQtMZaZ41ODXXXIUJiVxIS0BDYYH7EjlFc3kRgbzTn5Yy/+4OWsCeNIS4w1cYgQZbCF51/Aijd8XFWH/QMtIo9iDYXNFpHDwA+BWABV/QNwB5AF/M72b3vU8oPlAU/a+2KAR1T1heHWP1o21jSxYGomUVGRmV7UH0SExcVZlO2rp7dXx/S9cILy6kbmF2aMqfkPfbHm3GSa+RAhyoAGQlWLfV+LyDjf81V10Dnyqnr9EMc/D3y+n/3VwLnvvSJ4HD11hoON7Xx6caGbMkKCxUVZPPHGEfadaOGsCePclhMxNNnzHz5w7kS3pbjO4qIsXtx1gtqmdiZnvjdYbXCPIf+6iMgXReQ48Caw1d5cm7wWDIxv+B2W2HNATBwisGyqMTEuL942ZuIQoYc/fdtvAWeraqGqTrW3IqeFucnG6ibGJcQwc7z5x5yfnsiUzCQzmSnAbKjyzn8Ye3Ns+lKSa825MW0s9PDHQFQBY2q9hfLqRhZMzSLa+NwBqye1qaaJ3t6B58wYhkd5dRPzCzOIjR678QcvItbaX+XVjd5h7oYQwZ/W+T1gvYj8UUR+492cFuYWx06f4UBju3Ev+bCoKIvTZ7rZc7zZbSkRQWNrJ/tOtBj3kg+LijI5erqD2qYzbksx+DDYKCYvfwReBd4Cep2V4z4bq8fu2jgD8c6aOU3MnmhcIqNl0xhef2kgfNdlmpJlAtWhgj8GIlZVv+G4khBhY00jqQkxZsSODxPTEynIsuIQn7twqttywp7y6kaS4kz8wZdpuSlkp8SxobqRj50/2W05Bht/XEzPi8jNIjJBRDK9m+PKXKK8uomFUzNN/KEPi6ZmsbG6kR4Thxg1Vvwh08QffBARFpo4RMjhTwu9HjsOQYQPcz1+uoOahjbT9e+HRcWZNHd42HPMxCFGwzvxh4j9jzViFhVlcex0B4dMDpKQYUgXk6qOGZ+Cd/7DwjGUPN5fvPekvLqRs8fg8ueBYiznfxiKxbbRLK9upCAr2WU1Bhh8LaYLB7tQRMaJyKDLeIcb5dVNpMbHMGuiiT/05Z04hEkyPxq88YexmGNkKIpzrDiEaWOhw2A9iI+IyH9grcm0FajHykk9DWuJ7gLgm44rDCIbqxtZYOIPA7K4KIvn3jpm4hCjwFp/ycQf+sMbh9hQZeIQocKArVRVvw5cBRwDPoqVFvQbQAlWEp+LVHVzUFQGgZMdvVSb+MOgLCrKMnGIUdDcqew/0cpi08YGZHFRFsebOzjYaOIQocCgMQh7Qb577C2i2ddkTfFYaIKHA7LQx0c8zWUt4cjekz2AWeNrMHznQ4x3WYvBv1FMY4K9J3us+IOZ/zAgE9ISKcwy6zKNlL1NPSTHRZsg/yAU5ySTnRJv2liIYAyEzd6mHs6fmkmM8Q0PyqKiLDbWNNFrfMTDZm9Tj4k/DIG1LlMm5dVNJg4RApiWCtQ1d3C8TVk41XT9h2JxcRYtHR4ONUf8qisBpaG1k6OtamJcfrDIjkPUtRsD4Tb+5INIslOP3mO/LhGRq/wpXETuF5E6Eek3I51Y/EZEKkXkTRE5z+fYjSJSYW83+vuGRoIZm+4/3vkQe5uMgRgO76zxZf6EDIU3T/Weph6XlRj86UE8AHQCi+3XR4Cf+ln+g8DKQY5fgTUqqgS4Gfg9gL2Uxw+BhcAC4IcikuFnncOmvLqRhGiYbeY/DMn4tASmZiebL+8w8bYxM/9haIqyk8lJjWevaWOu44+BKFbV/wC6AVS1HfBrooCqvgYMNuvlauAhtSgH0kVkArACeElVm1T1JPASgxuaUVFe3cj0zGgTf/CTRUWZ7D/ZY+ZDDIMN1Y1MzzBtzB+8+SH2NvWaOITL+LOaa5eIJAIKICLFWD2KQJAP1Pq8PmzvG2j/exCRm7F6H+Tl5VFWVjYsAV09Cl0dTMvsGfa1waC1tTXkdI3r8HDGA3965lUK06LdlvMeQu2ene5UKuvaubpQQ0qXl1C7XwCZ3d2c6lT+8txqxieHllENxfvlJdDa/DEQP8SaTT1ZRB4GLgBuCpiCUaKqdwN3A8yfP1+XLVs27DIuvxTKysoYybVOE4q6zmru4I9vvkJ3xlSWXRR62WdD7Z49++ZRYBvnjk8MKV1eQu1+AUyub+X/dq9Bc6axbMEUt+W8i1C8X14CrW1I06yqLwEfxjIKjwLzVbUsQPUfAXwXf59k7xtovyEEyBuXwPgkMWPV/aS8upGU+BgKxoXWP+FQpig7mbR4YUOVaWNu4s8opvOw1l06BhwFpohIsYj40/sYiqeBT9ujmRYBp1X1GPAicLmIZNjB6cvtfYYQYWZmNJtqmkwcwg/Kq5s4vzDDrPE1DESEszKjTH4Il/HnL83vgHIsN849wAbgr8A+Ebl8sAtF5FH7/BkiclhEPicit4jILfYpzwHVQKVd9q3w9hIfPwE229ud9j5DiDAzM5qWTg+7jp52W0pIU9fSQWVdqxlCPQJmZkZT19JJTUOb21LGLP70Ao4Cn1PVXQAiMgu4E/gO8ATwz4EuVNXrBytYrb8GXx7g2P3A/X7oM7jAzEzrv0V5dSNzJqW7rCZ08c1xfrKqdoizDb7MzLQGQJRXN1GUk+KymtDltf31HGpq53oHYjX+9CCme40DgKruBmaqanXA1RjChvSEKIpyks3a/UPgjT+YOTbDJy9JyE016zINxaObDvH7sipHXJj+GIhdIvJ7EbnY3n4H7BaReOy5EYaxyaKiLDbXNOHpMbOqB6K8upHzCzPM/IcR4J0PscHEIQZEVdlY0+TYKtT+tNqbsGIEX7O3antfN1biIMMYZVFRFi2dHnab/BD9UtfSQVW9yTEyGhYXZ1Hf0km1iUP0S0VdK01tXY61MX9yUp8B/tve+tIacEWGsGHR1HfyQ5g4xHvxxh+8awsZho9vfohiE4d4D173m1NJqPwZ5loiIo+LyG4RqfZujqgxhBW54xIoykk2Y9UHYEN1o8kxMkoKs5LIGxdvYl0DsLG6iYlpCUzKSHSkfH8X6/s94MFyKT0E/NkRNYawY3FRFpsPnDRxiH4or240OUZGiTcOYeZDvBcr/tDIoqIsRJyZY+NPy01U1VcAUdWDqvoj4EpH1BjCjkVFWbR2eth11MQhfKlr7qC6vs0s7x0AFhVZcYiqehOH8KWqvpWG1i5H0yT7YyA6RSQKqBCR20TkQ4BxBhqAd+epNrxDuckxEjB84xCGd9hQ7Xwb88dAfBVIAr4ClAKfBD7tmCJDWJGbmkBxTrL58vah3MQfAkZhVhLjxyWYNtaH8upGJqQlMCUzybE6/DEQharaqqqHVfUzqvoRILSWVzS4yiITh3gP5VWNLDDxh4Bg8lS/F1VlY3UTC6dmOhZ/AP8MxPf83GcYoywutuIQO00cAoATzR1UN5j5D4FkUVEWDa0mDuGlqr6NhtZOx9vYgPMgROQK4P1Avoj8xufQOKwRTQYD8E6e6vLqRuZONvMhvK4QYyACh/debqhuZFquCYFurAlOGxusB3EU2Ap02I/e7WmslKAGAwA5qfFMy00xPmKbt+MPZv2lgFFg4hDvory6ibxx8RRkORd/gEF6EKq6A9ghIn9WVdNjMAzKoqJMnnzjCJ6e3jHvd99Q1cjCokyT/yGAiAiLi7NYW1GPqjrqdw91VJUNVQ1cOC3b8fsw4DdZRN4SkTeBN0Tkzb6bo6oMYceioizaunrGfBziyKkzHGhsZ3FxtttSIo5FRZk0tHZRVT+2V/ipqLPmPywJQhsbbC2mqxyv3RAxvO0jrhrbcQjvsiNLzPpLAeedOEQT03JTXVbjHt42Fow1vgbsQdizpg+q6kGsOMQ59nbG3jckIrJSRPaJSKWIfLef478Ske32tl9ETvkc6/E59vTw35ohmGSnxFNi4hBsqGokMzmOGXlj9wfMKaZkJjEhzcQh1lc1MDkzkckOzn/w4s9ifR8DNgEfBT4GbBSRa/24Lhr4LXAFMAu43s5G9zaq+nVVnauqc4H/xcpQ5+WM95iqftDvd2RwjUVFWWw50ET3GJ0P4fUNLyrKJMrEHwKOd12mjWN4XaaeXqW8usmx1Vv74k808fvA+ap6o6p+GlgA/MCP6xYAlaparapdwCrg6kHOvx541I9yDSHK23GII2MzT/XBxnaOnu4w8QcH8cYhKuvGZhxiz7FmTp/pDkr8AfzLSR2lqnU+rxvxz7DkA75JeA8DC/s7UUQKgKnAqz67E0RkC9aci39T1acGuPZm4GaAvLw8ysrK/JD2XlpbW0d8rZOEk66eTutf3SMvb+Z0UZwLqizcumdltVaCxZiGKsrKat5zPJw+y1CgP13SbvVOH3qxnEunxLqgyt379XyN1cb0xD7Kyireczzg2lR10A34T+BFrCxyNwHPA//ux3XXAvf6vP4UcNcA594O/G+fffn2YxFwACgeqs7S0lIdKatXrx7xtU4Sbrre98sy/fR9G4Mrpg9u3bMvP7xVF/zsJe3t7e33eLh9lm7Tn67e3l5d/POX9dY/bw2+IBs379dN92/US/5r4PpHog3YogP8pg7ZE1DVbwN/BObY292qersftucIMNnn9SR7X39cRx/3kqoesR+rgTJgnh91GlzGWpdp7MUhVJXy6kYWO7g2v2Fs54fo7ullU01TUDMU+hOk/gawUVW/YW9P+ln2ZqBERKaKSByWEXjPaCQRmQlkABt89mWISLz9PBu4ANjtZ70GF1lUlEV7Vw9vjbE4RDDHpo91FhVl0djWRcUYi0O8efg0bV09QW1j/sQSUoF/ishaOx9Enj8FqzX7+jYs99Qe4DFV3SUid4qI76ik64BV+u6/A2cBW0RkB7AaKwZhDEQYsGDq2MwPsb6yATD5p4PBWM0P4cYaX0MGqVX1x8CPRWQO8HFgjYgcVtXL/Lj2OeC5Pvvu6PP6R/1ctx5rzoUhzMhOiWd6Xgrl1U3cusxtNcFjfVVj0Mamj3UmZyaSn55IeXUjn15c6LacoLG+qoGzJowjMzl4A0CGs2hOHXAcaxRTrjNyDJHA4jE2H8Iam94YtLHpYx0RYeEYyw/R0d3DlgMng97G/IlB3CoiZcArQBbwBVWd47QwQ/gy1uIQe44109zhMfGHILKoKIumMRSH2HboFJ2e3qAv4eJPD2Iy8DVVna2qPzKxAMNQeOMQ3jVjIp31VSb+EGwW+6z9NRbYUN1IlMACOwd8sPBnmOv3VHV7MMQYIoOslHhm5KWOmSDi+qpGinOSyRuX4LaUMcOkjHfiEGOBDVUNnDMpnXEJwZ0cOLYX7jc4xqKiTLYcOBnxcQg3xqYb3olDbKxporc3suMQ7V0eth065UqMyxgIgyMsLs7iTHcPO2pPDX1yGLOj9hTtQR6bbrBYbMch9p1ocVuKo2yqacLTq64sIW8MhMERFhdlEyWwtqLBbSmOsraiARGT/8ENLiyxjPK6CG9j6yoaiIuJ4vzC4MYfwBgIg0OkJcUyZ1I6ayvq3ZbiKGsr6pkzKZ30JPcWJxyrTEhLZFpuCq9FfBtrYEFhJolx0UGv2xgIg2NcVJLN9tpTnD7T7bYURzh9ppvttae4qMS4l9xiaUk2m2qa6OjucVuKI5xo7mDfiRaWutTGjIEwOMbS6Tn0auQORdxQ1UivwoXTjIFwi4tKcuj09LLlwEm3pTiC1312oTEQhkhj7uR0UuJjItbNtLainuS4aOZNyXBbyphlYVEmsdES0W0sOyWOs8aPc6V+YyAMjhEbHcWioqyIDVSvrWhgcXEWcTHma+QWSXExlBZk8FoEtrHeXmVdZQMXTst2LYWtadkGR7loejaHmto52NjmtpSAcrCxjUNN7SwtyXFbyphnaUkOe441U9/S6baUgLL3eAsNrV2utjFjIAyO4vXPR1ovYq3LvmHDO3gDuK9XRlobs9xmbrYxYyAMjjI1O5n89MSI8xGvragnPz2Rouxkt6WMeWZPTCMjKTbihruurWhgRl6qq0u4OGogRGSliOwTkUoR+W4/x28SkXoR2W5vn/c5dqOIVNjbjU7qNDiHiHDR9GzWVzXiiZBlNzw9vayvamRpSbZJLxoCREcJF0zLZl1FQ8Qs/93R3cOmA02uDW/14piBEJFo4LfAFcAs4HoRmdXPqX9R1bn2dq99bSbwQ2AhsAD4oYiYoSJhytKSHFo6POw4HBnLbuw4fIqWDo9xL4UQF5XkUNfSGTHLbmysaaLL0+t6G3OyB7EAqFTValXtAlYBV/t57QrgJVVtUtWTwEvASod0GhzmgmnZREcJq/dGhgtg9d56oqOEpdNMgDpUuGi69VlEThurIyE2KqjpRftjyJSjoyAfqPV5fRirR9CXj4jIRcB+4OuqWjvAtfn9VSIiNwM3A+Tl5VFWVjYisa2trSO+1kkiRde0NOHpLdXMjz/mnCgbp+/Z01vOUJwmbNv0+rCui5TPMlgMV9eU1Cie3Lifs9710xF4nL5fqspz288wIz2K8tfXDuvagGtTVUc24FrgXp/XnwLu6nNOFhBvP/8i8Kr9/FvA//M57wfAt4aqs7S0VEfK6tWrR3ytk0SKrt+trtSC25/VY6fOOCPIByfv2bFTZ7Tg9mf1t6srhn1tpHyWwWK4uv7jhT1a9L1/6Km2LmcE2Th9vyrrWrTg9mf1ofU1w752JNqALTrAb6qTLqYjWNnovEyy972Nqjaqqnfw8r1Aqb/XGsKLS2ZaaczL9tW5rGR0ePV7348hdLhkZi49vRr2o5lW77Xa2PIQaGNOGojNQImITBWROOA64GnfE0Rkgs/LDwJ77OcvApeLSIYdnL7c3mcIU6bnpZCfnsire8PbQLy6t46JaQnMyEt1W4qhD3MnZ5CRFPv2D2y4snpfHdPzUpiUkeS2FOcMhKp6gNuwftj3AI+p6i4RuVNEPmif9hUR2SUiO4CvADfZ1zYBP8EyMpuBO+19hjBFRFg+M4d1lQ10esJz5c1OTw/rKhtYPjPXDG8NQaKjhIun51C2v56eMM0y19rpYVNNU0j0HsDheRCq+pyqTlfVYlX9mb3vDlV92n7+PVWdrarnqupyVd3rc+39qjrN3h5wUqchOCyfkUt7Vw+basLT1m+qaaK9q4flM0Ljy2t4L8tn5tLU1hW2Q6rXVdTT3aMh08bMTGpD0FhSnE18TFTYDkVcvbeeuJgolkwz2eNClYun5xAlUBambqbVe+tJTbAWIAwFjIEwBI3EuGgWF2exOkwD1av31bG4KIukOCdHhxtGQ3pSHOdNyeDVMGxjqsrqfXVcND2H2OjQ+GkODRWGMcPyGbnUNLRR0xBeq7t6NS+fYSbHhTrLZ+ay80gzdc0dbksZFruONlPX0hky7iUwBsIQZLzDQ1/efcJlJcPDq/fSs/JcVmIYirfb2J7w6kW8tPsEIrAshP6EGANhCCqTM5OYPXEcL+w67raUYfHCruPMmjCOyZnuDz00DM7M8akUZCWFXRt7cddxzi/MJDsl3m0pb2MMhCHorJw9nq0HT4aNC6CuuYOtB0+y8uzxbksx+IGIsHL2eNZXNnD6TLfbcvyipqGNvcdbWDk7tNqYMRCGoOP9oX0xTNxMXp3GQIQPK84ej6dXeXVvmLQxu7ezIsTamDEQhqAzLTeFopxkXtwZHi6AF3cepyg7mZLcFLelGPxk7qR08sbF80KYtLEXdh5nzqQ08tMT3ZbyLoyBMAQdrwtgQ3Ujp9q73JYzKKfauyivbmTF2ePN7OkwIipKWDF7PGv213OmK7Rn7h87fYbttadYEWLuJTAGwuASK2aPp6dXQ36kySt76vD0akh+eQ2Ds2L2eDq6e1mzP7QnZv5zl+UGC8U2ZgyEwRXmTEpjQlpCyLsAXth1nAlpCczJT3NbimGYLJiaSXpS7Nv+/VDlhZ3HmZabwrQQdGEaA2FwBRHLBfBaRT1tnR635fRLW6eH1/bXs2L2eKKijHsp3IiNjuKys/J4ec8JujyhmQ+9qa2LjTWNITd6yYsxEAbXWHn2eLo8vSG7BHjZvno6Pb1cPttMjgtXVs4eT0uHh9erGtyW0i8v7T5Or4amewmMgTC4yPmFmeSmxvP37UfdltIvT20/Qm5qPAunmsX5wpWl07MZlxDD06HaxrYdpTAribPzx7ktpV+MgTC4RnSUcPXciZTtq6OpLbRGM51s66JsXx1Xz51ItHEvhS3xMdFcOWciL+w8HnKuzKOnzlBe08g18/JDdoScMRAGV/nQvEl4epV/vHXMbSnv4h9vHaO7R7lmXr7bUgyj5EPz8jnT3cNLITYx8+kdR1G19IUqjhoIEVkpIvtEpFJEvtvP8W+IyG4ReVNEXhGRAp9jPSKy3d6e7nutITI4a0IqM/JSeWpbaKUcf3LbEabnpTBrQmh2/Q3+M78gg/z0RJ4IsTb21LYjnDclnYKsZLelDIhjBkJEooHfAlcAs4DrRWRWn9O2AfNVdQ7wOPAfPsfOqOpce/sghohERLhmXj5bD57kUGO723IAONTYztaDJ0O662/wn6go4Zp5E1lXUU9dS2is/7XnWDN7j7eEdO8BnO1BLAAqVbVaVbuAVcDVvieo6mpV9f4qlAOTHNRjCFGunjsRsILCoYBXxzVzQ/vLa/CfD83Lp1fhmR2h4cp8atsRYqKEK+dMdFvKoIiqM8m9ReRaYKWqft5+/SlgoareNsD5dwHHVfWn9msPsB3wAP+mqk8NcN3NwM0AeXl5patWrRqR3tbWVlJSQm+iyljR9W+bznCqQ/nF0sRR/2sfjTZV5Xtrz5CeIHx3QWDXxRkrn2WgCLSuH60/Yz0uGd3nOlpdvap8s+wMBeOi+Fppwqi09GUk2pYvX75VVef3e1BVHdmAa4F7fV5/CrhrgHM/idWDiPfZl28/FgEHgOKh6iwtLdWRsnr16hFf6yRjRdeqTQe14PZndduhk6MuazTath86qQW3P6urNh0ctY6+jJXPMlAEWte9a6u14PZnteJE86jKGa2u1yvqteD2Z/WZHUdGVU5/jEQbsEUH+E110sV0BJjs83qSve9diMhlwPeBD6pqp3e/qh6xH6uBMmCeg1oNLnPFOROIj4nisS21rur4y5Za4mOiWHn2BFd1GALPB8+dSEyU8NiWw67q+MuWWlITYrgsDLITOmkgNgMlIjJVROKA64B3jUYSkXnAH7GMQ53P/gwRibefZwMXALsd1GpwmXEJsVw1ZyJ/33bEtfHqbZ0e/r7tCFfNmUhaYqwrGgzOkZMaz2Vn5fH41sN0etxZ4bWprYvn3zrOh+flkxAb7YqG4eCYgVBVD3Ab8CKwB3hMVXeJyJ0i4h2V9J9ACvDXPsNZzwK2iMgOYDVWDMIYiAjnEwun0NbVw9M73Jn1+vSOo7R19fCJhVNcqd/gPJ9YOIWmti5e3OXOnIi/bT1MV08vn1hYMPTJIUCMk4Wr6nPAc3323eHz/LIBrlsPnOOkNkPocd6UdGaOT+XhjQe57vzJQR1iqqo8svEQM8enct6U9KDVawguF07LZnJmIg+XH+SD5wZ3BFFvr/LopkOUFmQwY3xqUOseKWYmtSFkEBFuWFTAziPNvHHoZFDrfuPQSd46cpobFk4xcx8imKgo4RMLCthY08SeY81BrXttZQPVDW3cEEY9VGMgDCHFR87LZ1xCDPevOxDUeu9fd4BxCTF8+DwzFSfSuX7BZBJjo3ng9Zqg1nv/uhpyUuO5KsTnPvhiDIQhpEiKi+H6hVN4fucxDp8MzszqwyfbeX7nMa5fOIXkeEe9roYQID0pjo+U5vPU9qM0tHYOfUEAqKxrYc3+ej69qIC4mPD52Q0fpYYxw42LCxGRoPUiHnj9ACLCpxcXBqU+g/vctGQqXZ5eHlp/ICj13bu2hviYqLAbAGEMhCHkmJieyDVz83lk00EaHf6H19jaycMbD3L13Inkpwd25rQhdJmWm8KK2Xk8uP4AzR3djtZ15NQZ/vbGYa47fzJZKfGO1hVojIEwhCS3Li+m09PLfeuc9RPft66GTk8vty6b5mg9htDjtuUlNHd4+NOGg47Wc/eaKlTh5ouLHa3HCYyBMIQkxTkpvP+cCTy04aBjyYSa2rp4aMNB3n/2hJBMGG9wlnMmpXHx9BzuW1dDi0O9iOOnO1i1uZaPnDcpLHuoxkAYQpavXVpCe5eHu16tdKT8u16tpL3Lw1cvK3GkfEPo8433TaeprYt7Xqt2pPxfvbQfVbjtkvDsoRoDYQhZSvJS+WjpZP5UfoDapsCOaKptaudP5Qe4tnQS0/PCY9KSIfCcOzmdK8+ZwD1rawKeK6LiRAt/3VrLJxcVMDkzKaBlBwtjIAwhzdffN53oKOFn/9gT0HJ//tweokT4+vumB7RcQ/jx7RUz6O7p5T9e2BewMlWVO5/dTXJcTNj2HsAYCEOIMz4tgX+5pIQXdh3n1b2BWT/n1b0neH7ncb5yaQkT0sLPL2wILIXZyXx+aRGPbz3MxurGgJT5zJvHWFvRwDcvn05mclxAynQDYyAMIc8XlhZRkpvCD57aResoV3pt7fRwx993UZKbwheWFgVIoSHc+eqlJUzKSORfn3yLGSFswgAADCtJREFUju7RrfR6qr2Lnzy7mzmT0vhUmM+tMQbCEPLExUTxbx85h2Onz3DHUztHVdYdf9/J0VNn+MWHzwmrGa0GZ0mMi+bnHzqHqvo2fv7cyN2Zqsp3Hn+TU+1d/PxD5xAdFd7replviCEsKC3I5CuXlvDEtiP8dYRJhR7fepgn3jjCVy4tYX5hZoAVGsKdi6bn8PkLp/LQhoM8/9bIclc/tOEg/9x9gttXzuTs/LQAKww+xkAYwoZ/uaSEJcVZ/OuTb7G+qmFY166vauB7T7zJ4qIsblsevkFDg7N8Z+VM5k5O5+uPbWfbMFcUfmXPCX78zC4unZnLZy+Y6pDC4GIMhCFsiI4Sfv/JUqZmJ/PFh7ayqabJr+s2H2jii3/aSmFWMn/4ZCkx0abZG/onLiaKe2+cT25qAp99cDNvHj7l13VrK+q57ZFtzJ6Yxm+un0dUmLuWvDj6TRGRlSKyT0QqReS7/RyPF5G/2Mc3ikihz7Hv2fv3icgKJ3Uawoe0xFge/MwCcsbF88n7NvK3rYex8q6/F1XliTcOc8O9G8lJjefBzy4gLcmkEjUMTnZKPA99dgHJ8TFcd3c5zw3iblJVHt54kM88sJmCrCTuv+n8iFoR2DEDISLRwG+BK4BZwPUiMqvPaZ8DTqrqNOBXwL/b187CymE9G1gJ/M4uz2BgYnoif7tlCXMnp/PNv+7gxgc2s66igd5ey1D0qvJ6ZQM3PrCZbzy2g7mT0vnbLUvCcqkDgzsUZifzxK1LKMlN4daH3+Dz/7eF8urGt9tYT69Stq+O6+8p5/tP7mRxcRaP3bKYnNTwWoxvKJw0dQuASlWtBhCRVcDVgG9u6auBH9nPHwfuEiud19XAKlXtBGpEpNIub4ODeg1hREZyHI9+YRF/2nCAX71cwSfv20h8TBTZKfHUNZ+hu3cjaYmx/PADs/j04sKwH01iCD65qQk8/qUl3Leuht++WsnLe06QEBtFcrTS8tILdPX0kpkcxy8+fA4fnz85YtxKvshA3fNRFyxyLbBSVT9vv/4UsFBVb/M5Z6d9zmH7dRWwEMtolKvqn+399wHPq+rj/dRzM3AzQF5eXumqVatGpLe1tZWUlNBbsM3oGpquHmV7XQ/Vp3s53dlLUpSH6dkJzMuNJi46dL60oXTPfDG6hqazR9l6ooeDzT00tXWTnRJHcVoUc3OjiQkhwzCSe7Z8+fKtqjq/v2Nh7yxT1buBuwHmz5+vy5YtG1E5ZWVljPRaJzG6/ONyn+ehps2L0TU8Qk2XNxAaarp8CbQ2J4PUR4DJPq8n2fv6PUdEYoA0oNHPaw0Gg8HgIE4aiM1AiYhMFZE4rKDz033OeRq40X5+LfCqWj6vp4Hr7FFOU4ESYJODWg0Gg8HQB8dcTKrqEZHbgBeBaOB+Vd0lIncCW1T1aeA+4E92ELoJy4hgn/cYVkDbA3xZVUe3QIrBYDAYhoWjMQhVfQ54rs++O3yedwAfHeDanwE/c1KfwWAwGAbGTCk1GAwGQ78YA2EwGAyGfjEGwmAwGAz9YgyEwWAwGPrFsZnUbiAi9cDBEV6eDQxvDengYHQNn1DVZnQND6Nr+IxEW4Gq5vR3IKIMxGgQkS0DTTd3E6Nr+ISqNqNreBhdwyfQ2oyLyWAwGAz9YgyEwWAwGPrFGIh3uNttAQNgdA2fUNVmdA0Po2v4BFSbiUEYDAaDoV9MD8JgMBgM/WIMhMFgMBj6JeINhIisFJF9IlIpIt/t53i8iPzFPr5RRAr/f3vnH2NXUcXxz1fENkUCLY2xIL9aJQ1FSlsErRVBTQo1UJSQlECkUqIVIRoDCaZJY0xUkv6hEjDGEIMkpghViUUxtlKBdNmSgm0XBEq7JWhDLFYobTDLr+Mfcx5Mr/e9fd2+ubtZzye52bnz4853zz3vzZ07u2eysm97/rOSFlbbNqDtW5L+JmmbpD9LOjkre0vSFj+qYdRL61oq6aWs/2uzsqslPefH1dW2hXX9MNO0XdIrWVlJe/1c0h7fIbGuXJJudd3bJM3NykraazhdV7qeAUl9kmZnZc97/hZJmxvWdb6kfdn9WpmVdfSBwrpuyjQ96T41xctK2utESRv8u+ApSd+oqVPGx8xs3B6kMOM7genA+4CtwOmVOtcBP/X0EuBXnj7d608ATvXrHNGwtguASZ7+Wkubnx8YRZstBW6raTsFGPSfkz09uSldlfo3kELMF7WXX/s8YC7wZJvyRcADgICPA5tK26tLXfNb/QEXtXT5+fPA1FGy1/nA/YfrA73WVal7MWn/mibsNQ2Y6+mjge01n8kiPjbeZxDnADvMbNDMXgfuBhZX6iwGfuHpNcBnJcnz7zazITPbBezw6zWmzcw2mNlrftpP2lmvNN3YrB0LgXVm9m8zexlYB1w4SrquAFb3qO+OmNnDpP1M2rEYuMsS/cCxkqZR1l7D6jKzPu8XmvOvbuzVjsPxzV7ratK/XjSzJzy9H3gaOKFSrYiPjfcB4gTg79n5P/hfw75Tx8zeBPYBx3XZtrS2nGWkJ4QWEyVtltQv6dJR0HWZT2XXSGptD1vSZl1f21/FnQo8mGWXslc3tNNe2scOhap/GfAnSY9L+soo6PmEpK2SHpA0y/PGhL0kTSJ9yf46y27EXkqvwOcAmypFRXys6IZBQW+QdBVwNvDpLPtkM9staTrwoKQBM9vZkKS1wGozG5L0VdIM7DMN9d0NS4A1dvAuhKNprzGNpAtIA8SCLHuB2+sDwDpJz/gTdhM8QbpfByQtAu4jbTs8VrgY2Ghm+WyjuL0kvZ80KH3TzF7t5bXbMd5nELuBE7PzD3lebR1J7wWOAfZ22ba0NiR9DlgBXGJmQ618M9vtPweBv5CeKhrRZWZ7My13APO6bVtSV8YSKtP/gvbqhnbaS/vYsEg6k3QPF5vZ3lZ+Zq89wG/p7evVjpjZq2Z2wNN/AI6UNJUxYC+nk38VsZekI0mDwy/N7Dc1Vcr4WIlFlbFykGZIg6TXDa1FrVmVOl/n4EXqezw9i4MXqQfp7SJ1N9rmkBblPlLJnwxM8PRU4Dl6tFjXpa5pWfoLQL+9uyC2y/VN9vSUpnR5vZmkBUM1Ya+sj1Nov+j6eQ5eQHystL261HUSaW1tfiX/KODoLN0HXNigrg+27h/pi/YFt11XPlBKl5cfQ1qnOKope/nvfhfwow51ivhYzww7Vg/S6v520hftCs/7LumJHGAicK9/UB4DpmdtV3i7Z4GLRkHbeuCfwBY/fuf584EB/4AMAMsa1vUD4CnvfwMwM2t7jdtyB/DlJnX5+XeAWyrtSttrNfAi8AbpHe8yYDmw3MsF3O66B4CzG7LXcLruAF7O/Guz5093W231+7yiYV3XZ/7VTzaA1flAU7q8zlLSH6/k7UrbawFpjWNbdq8WNeFjEWojCIIgqGW8r0EEQRAEIyQGiCAIgqCWGCCCIAiCWmKACIIgCGqJASIIgiCoJQaIIGiDpGMlXZedHy9pTaG+Ls2jltaUf1TSnSX6DoJ2xJ+5BkEbPO7N/WZ2RgN99ZH+n+NfHeqsB64xsxdK6wkCiBlEEHTiFmCGx/hfJemU1l4BSnti3Cdpne8FcL3S/h1/9YCArX0CZkj6owdxe0TSzGonkk4DhlqDg6TLfb+BrZLyeD5rSf/tHwSNEANEELTnZmCnmZ1lZjfVlJ8BfBH4GPA94DUzmwM8CnzJ6/wMuMHM5gE3Aj+puc4nSQHqWqwEFprZbOCSLH8z8KnD+H2C4JCIaK5BMHI2WIrPv1/SPtITPqRQB2d69M35wL1pixEgxfaqMg14KTvfCNwp6R4gD8y2Bzi+h/qDoCMxQATByBnK0m9n52+TPlvvAV4xs7OGuc5/SEHgADCz5ZLOJQVge1zSPEuRVid63SBohHjFFATt2U/a4nFEWIrZv0vS5fDOvsGza6o+DXy4dSJphpltMrOVpJlFK1zzaUDtfslBUIIYIIKgDf7UvtEXjFeN8DJXAssktSJ91m2R+TAwR+++h1olacAXxPtIUUIh7VH++xHqCIJDJv7MNQjGAJJ+DKw1s/VtyicAD5F2LnuzUXHB/y0xgwiCscH3gUkdyk8Cbo7BIWiSmEEEQRAEtcQMIgiCIKglBoggCIKglhgggiAIglpigAiCIAhqiQEiCIIgqOW/sMLhWL1Rt5wAAAAASUVORK5CYII=\n" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "source": [ + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "t = np.arange(0.0, 2.0, 0.01)\n", + "s = 1 + np.sin(2 * np.pi * t)\n", + "\n", + "fig, ax = plt.subplots()\n", + "ax.plot(t, s)\n", + "\n", + "ax.set(xlabel='time (s)', ylabel='voltage (mV)',\n", + " title='About as simple as it gets, folks')\n", + "\n", + "ax.grid()\n", + "\n", + "fig.savefig('test.png')\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "language_info": { + "name": "python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + } + }, + "orig_nbformat": 2, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3, + "kernelspec": { + "name": "python_defaultSpec_1591908362720", + "display_name": "Python 3.8.2 64-bit ('venvForWidgets': venv)" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/test/datascience/plotViewer.functional.test.tsx b/src/test/datascience/plotViewer.functional.test.tsx new file mode 100644 index 000000000000..970d3b15d357 --- /dev/null +++ b/src/test/datascience/plotViewer.functional.test.tsx @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +import '../../client/common/extensions'; + +import * as assert from 'assert'; +import { ComponentClass, mount, ReactWrapper } from 'enzyme'; +import { parse } from 'node-html-parser'; +import * as React from 'react'; +import { Disposable } from 'vscode'; + +import { IPlotViewerProvider } from '../../client/datascience/types'; +import { MainPanel } from '../../datascience-ui/plot/mainPanel'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; + +// import { asyncDump } from '../common/asyncDump'; +suite('DataScience PlotViewer tests', () => { + const disposables: Disposable[] = []; + let plotViewerProvider: IPlotViewerProvider; + let ioc: DataScienceIocContainer; + + setup(async () => { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(); + await ioc.activate(); + }); + + function mountWebView(): ReactWrapper<any, Readonly<{}>, React.Component> { + // Setup our webview panel + const mounted = ioc.createWebView( + () => mount(<MainPanel skipDefault={true} baseTheme={'vscode-light'} testMode={true} />), + 'default' + ); + + // Make sure the plot viewer provider and execution factory in the container is created (the extension does this on startup in the extension) + plotViewerProvider = ioc.get<IPlotViewerProvider>(IPlotViewerProvider); + + return mounted.wrapper; + } + + function waitForComponentDidUpdate<P, S, C>(component: React.Component<P, S, C>): Promise<void> { + return new Promise((resolve, reject) => { + if (component) { + let originalUpdateFunc = component.componentDidUpdate; + if (originalUpdateFunc) { + originalUpdateFunc = originalUpdateFunc.bind(component); + } + + // tslint:disable-next-line:no-any + component.componentDidUpdate = (prevProps: Readonly<P>, prevState: Readonly<S>, snapshot?: any) => { + // When the component updates, call the original function and resolve our promise + if (originalUpdateFunc) { + originalUpdateFunc(prevProps, prevState, snapshot); + } + + // Reset our update function + component.componentDidUpdate = originalUpdateFunc; + + // Finish the promise + resolve(); + }; + } else { + reject('Cannot find the component for waitForComponentDidUpdate'); + } + }); + } + + function waitForRender<P, S, C>(component: React.Component<P, S, C>, numberOfRenders: number = 1): Promise<void> { + // tslint:disable-next-line:promise-must-complete + return new Promise((resolve, reject) => { + if (component) { + let originalRenderFunc = component.render; + if (originalRenderFunc) { + originalRenderFunc = originalRenderFunc.bind(component); + } + let renderCount = 0; + component.render = () => { + let result: React.ReactNode = null; + + // When the render occurs, call the original function and resolve our promise + if (originalRenderFunc) { + result = originalRenderFunc(); + } + renderCount += 1; + + if (renderCount === numberOfRenders) { + // Reset our render function + component.render = originalRenderFunc; + resolve(); + } + + return result; + }; + } else { + reject('Cannot find the component for waitForRender'); + } + }); + } + + async function waitForUpdate<P, S, C>( + wrapper: ReactWrapper<P, S, C>, + mainClass: ComponentClass<P>, + numberOfRenders: number = 1 + ): Promise<void> { + const mainObj = wrapper.find(mainClass).instance(); + if (mainObj) { + // Hook the render first. + const renderPromise = waitForRender(mainObj, numberOfRenders); + + // First wait for the update + await waitForComponentDidUpdate(mainObj); + + // Force a render + wrapper.update(); + + // Wait for the render + await renderPromise; + + // Force a render + wrapper.update(); + } + } + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + await ioc.dispose(); + delete (global as any).ascquireVsCodeApi; + }); + + suiteTeardown(() => { + // asyncDump(); + }); + + async function waitForPlot(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, svg: string): Promise<void> { + // Get a render promise with the expected number of renders + const renderPromise = waitForUpdate(wrapper, MainPanel, 1); + + // Call our function to add a plot + await plotViewerProvider.showPlot(svg); + + // Wait for all of the renders to go through + await renderPromise; + } + + // tslint:disable-next-line:no-any + function runMountedTest( + name: string, + testFunc: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void> + ) { + test(name, async () => { + const wrapper = mountWebView(); + try { + await testFunc(wrapper); + } finally { + // Make sure to unmount the wrapper or it will interfere with other tests + if (wrapper && wrapper.length) { + wrapper.unmount(); + } + } + }); + } + + function verifySvgValue(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, svg: string) { + const html = wrapper.html(); + const root = parse(html) as any; + const drawnSvgs = root.querySelectorAll('.injected-svg') as SVGSVGElement[]; + assert.equal(drawnSvgs.length, 1, 'Injected svg not found'); + const expectedSvg = (parse(svg) as any) as SVGSVGElement; + const drawnSvg = drawnSvgs[0] as SVGSVGElement; + const drawnPaths = drawnSvg.querySelectorAll('path'); + const expectedPaths = expectedSvg.querySelectorAll('path'); + assert.equal(drawnPaths.length, expectedPaths.length, 'Paths do not match'); + assert.equal(drawnPaths[0].innerHTML, expectedPaths[0].innerHTML, 'Path values do not match'); + } + + const cancelSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>Cancel_16xMD</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M10.475,8l3.469,3.47L11.47,13.944,8,10.475,4.53,13.944,2.056,11.47,5.525,8,2.056,4.53,4.53,2.056,8,5.525l3.47-3.469L13.944,4.53Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-bg" d="M9.061,8l3.469,3.47-1.06,1.06L8,9.061,4.53,12.53,3.47,11.47,6.939,8,3.47,4.53,4.53,3.47,8,6.939,11.47,3.47l1.06,1.06Z"/></g></svg>`; + + runMountedTest('Simple SVG', async (wrapper) => { + await waitForPlot(wrapper, cancelSvg); + verifySvgValue(wrapper, cancelSvg); + }); + + runMountedTest('Export', async (_wrapper) => { + // Export isn't runnable inside of JSDOM. So this test does nothing. + }); +}); diff --git a/src/test/datascience/preWarmVariables.unit.test.ts b/src/test/datascience/preWarmVariables.unit.test.ts new file mode 100644 index 000000000000..ece7d2564e1e --- /dev/null +++ b/src/test/datascience/preWarmVariables.unit.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter } from 'vscode'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { createDeferred } from '../../client/common/utils/async'; +import { Architecture } from '../../client/common/utils/platform'; +import { JupyterInterpreterService } from '../../client/datascience/jupyter/interpreter/jupyterInterpreterService'; +import { PreWarmActivatedJupyterEnvironmentVariables } from '../../client/datascience/preWarmVariables'; +import { EnvironmentActivationService } from '../../client/interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { sleep } from '../core'; + +suite('DataScience - PreWarm Env Vars', () => { + let activationService: IExtensionSingleActivationService; + let envActivationService: IEnvironmentActivationService; + let jupyterInterpreter: JupyterInterpreterService; + let onDidChangeInterpreter: EventEmitter<PythonEnvironment>; + let interpreter: PythonEnvironment; + setup(() => { + interpreter = { + architecture: Architecture.Unknown, + path: '', + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Conda + }; + onDidChangeInterpreter = new EventEmitter<PythonEnvironment>(); + envActivationService = mock(EnvironmentActivationService); + jupyterInterpreter = mock(JupyterInterpreterService); + when(jupyterInterpreter.onDidChangeInterpreter).thenReturn(onDidChangeInterpreter.event); + activationService = new PreWarmActivatedJupyterEnvironmentVariables( + instance(envActivationService), + instance(jupyterInterpreter), + [] + ); + }); + test('Should not pre-warm env variables if there is no jupyter interpreter', async () => { + const envActivated = createDeferred<string>(); + when(jupyterInterpreter.getSelectedInterpreter()).thenResolve(undefined); + when(envActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenCall(() => { + envActivated.reject(new Error('Environment Activated when it should not have been!')); + return Promise.resolve(); + }); + + await activationService.activate(); + + await Promise.race([envActivated.promise, sleep(50)]); + }); + test('Should pre-warm env variables', async () => { + const envActivated = createDeferred<string>(); + when(jupyterInterpreter.getSelectedInterpreter()).thenResolve(interpreter); + when(envActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenCall(() => { + envActivated.resolve(); + return Promise.resolve(); + }); + + await activationService.activate(); + + await envActivated.promise; + verify(envActivationService.getActivatedEnvironmentVariables(undefined, interpreter)).once(); + }); + test('Should pre-warm env variables when jupyter interpreter changes', async () => { + const envActivated = createDeferred<string>(); + when(jupyterInterpreter.getSelectedInterpreter()).thenResolve(undefined); + when(envActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenCall(() => { + envActivated.reject(new Error('Environment Activated when it should not have been!')); + return Promise.resolve(); + }); + + await activationService.activate(); + + await Promise.race([envActivated.promise, sleep(50)]); + + // Change interpreter + when(jupyterInterpreter.getSelectedInterpreter()).thenResolve(interpreter); + when(envActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenCall(() => { + envActivated.resolve(); + return Promise.resolve(); + }); + onDidChangeInterpreter.fire(interpreter); + + await envActivated.promise; + verify(envActivationService.getActivatedEnvironmentVariables(undefined, interpreter)).once(); + }); +}); diff --git a/src/test/datascience/progress/decorators.unit.test.ts b/src/test/datascience/progress/decorators.unit.test.ts new file mode 100644 index 000000000000..099fd7fdf7c1 --- /dev/null +++ b/src/test/datascience/progress/decorators.unit.test.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, deepEqual, instance, mock, verify } from 'ts-mockito'; +import { createDeferred } from '../../../client/common/utils/async'; +import { + disposeRegisteredReporters, + registerReporter, + reportAction +} from '../../../client/datascience/progress/decorator'; +import { ProgressReporter } from '../../../client/datascience/progress/progressReporter'; +import { IProgressReporter, ReportableAction } from '../../../client/datascience/progress/types'; +import { noop } from '../../core'; + +suite('DataScience - Progress Reporter Decorator', () => { + let reporter: IProgressReporter; + + class SomeClassThatDoesSomething { + public readonly something = createDeferred(); + public readonly somethingElse = createDeferred(); + @reportAction(ReportableAction.NotebookStart) + public async doSomething() { + return this.something.promise; + } + @reportAction(ReportableAction.NotebookConnect) + public async doSomethingElse() { + return this.somethingElse.promise; + } + } + class AnotherClassThatDoesSomething { + public readonly something = createDeferred(); + public readonly somethingElse = createDeferred(); + @reportAction(ReportableAction.JupyterSessionWaitForIdleSession) + public async doSomething() { + return this.something.promise; + } + @reportAction(ReportableAction.KernelsGetKernelForRemoteConnection) + public async doSomethingElse() { + return this.somethingElse.promise; + } + } + setup(() => { + reporter = mock(ProgressReporter); + registerReporter(instance(reporter)); + }); + teardown(disposeRegisteredReporters); + + test('Report Progress', async () => { + const cls1 = new SomeClassThatDoesSomething(); + const cls2 = new AnotherClassThatDoesSomething(); + + verify(reporter.report(anything())).never(); + + // Report progress of actions started. + cls1.doSomething().ignoreErrors(); + cls2.doSomething().ignoreErrors(); + + verify(reporter.report(anything())).times(2); + verify(reporter.report(deepEqual({ action: ReportableAction.NotebookStart, phase: 'started' }))).once(); + verify( + reporter.report(deepEqual({ action: ReportableAction.JupyterSessionWaitForIdleSession, phase: 'started' })) + ).once(); + + // Report progress of actions completed (even if promises get rejected). + cls1.something.resolve(); + cls2.something.reject(new Error('Kaboom')); + await Promise.all([cls1.something.promise.catch(noop), cls2.something.promise.catch(noop)]); + + verify(reporter.report(anything())).times(4); + verify(reporter.report(deepEqual({ action: ReportableAction.NotebookStart, phase: 'completed' }))).once(); + verify( + reporter.report( + deepEqual({ action: ReportableAction.JupyterSessionWaitForIdleSession, phase: 'completed' }) + ) + ).once(); + + // Report progress of actions started again. + cls1.doSomethingElse().ignoreErrors(); + cls2.doSomethingElse().ignoreErrors(); + + verify(reporter.report(anything())).times(6); + verify(reporter.report(deepEqual({ action: ReportableAction.NotebookConnect, phase: 'started' }))).once(); + verify( + reporter.report( + deepEqual({ action: ReportableAction.KernelsGetKernelForRemoteConnection, phase: 'started' }) + ) + ).once(); + + // Report progress of actions completed (even if promises get rejected). + cls1.somethingElse.resolve(); + cls2.somethingElse.reject(new Error('Kaboom')); + await Promise.all([cls1.somethingElse.promise.catch(noop), cls2.somethingElse.promise.catch(noop)]); + + verify(reporter.report(anything())).times(8); + verify(reporter.report(deepEqual({ action: ReportableAction.NotebookConnect, phase: 'completed' }))).once(); + verify( + reporter.report( + deepEqual({ action: ReportableAction.KernelsGetKernelForRemoteConnection, phase: 'completed' }) + ) + ).once(); + }); +}); diff --git a/src/test/datascience/progress/progressReporter.unit.test.ts b/src/test/datascience/progress/progressReporter.unit.test.ts new file mode 100644 index 000000000000..ece6031855ef --- /dev/null +++ b/src/test/datascience/progress/progressReporter.unit.test.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { CancellationToken, CancellationTokenSource, Progress as VSCProgress } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { getUserMessageForAction } from '../../../client/datascience/progress/messages'; +import { ProgressReporter } from '../../../client/datascience/progress/progressReporter'; +import { ReportableAction } from '../../../client/datascience/progress/types'; +import { noop, sleep } from '../../core'; +type Task<R> = ( + progress: VSCProgress<{ message?: string; increment?: number }>, + token: CancellationToken +) => Promise<R>; + +// tslint:disable-next-line: max-func-body-length +suite('DataScience - Progress Reporter', () => { + let reporter: ProgressReporter; + let vscodeProgressReporter: VSCProgress<{ message?: string | undefined; increment?: number | undefined }>; + let appShell: IApplicationShell; + class VSCodeReporter { + public report(_value: { message?: string | undefined; increment?: number | undefined }) { + noop(); + } + } + setup(() => { + appShell = mock(ApplicationShell); + vscodeProgressReporter = mock(VSCodeReporter); + reporter = new ProgressReporter(instance(appShell)); + }); + + test('Progress message should not get cancelled', async () => { + let callbackPromise: Promise<{}> | undefined; + const cancel = new CancellationTokenSource(); + when(appShell.withProgress(anything(), anything())).thenCall((_, cb: Task<{}>) => { + return (callbackPromise = cb(instance(vscodeProgressReporter), cancel.token)); + }); + + reporter.createProgressIndicator('Hello World'); + + // appShell.WithProgress should not complete. + const message = await Promise.race([callbackPromise, sleep(500).then(() => 'Timeout')]); + assert.equal(message, 'Timeout'); + verify(vscodeProgressReporter.report(anything())).never(); + }); + + test('Cancel progress message when cancellation is cancelled', async () => { + let callbackPromise: Promise<{}> | undefined; + const cancel = new CancellationTokenSource(); + when(appShell.withProgress(anything(), anything())).thenCall((_, cb: Task<{}>) => { + return (callbackPromise = cb(instance(vscodeProgressReporter), cancel.token)); + }); + + reporter.createProgressIndicator('Hello World'); + + cancel.cancel(); + + // appShell.WithProgress should complete. + await callbackPromise!; + verify(vscodeProgressReporter.report(anything())).never(); + }); + + test('Cancel progress message when disposed', async () => { + let callbackPromise: Promise<{}> | undefined; + const cancel = new CancellationTokenSource(); + when(appShell.withProgress(anything(), anything())).thenCall((_, cb: Task<{}>) => { + return (callbackPromise = cb(instance(vscodeProgressReporter), cancel.token)); + }); + + const disposable = reporter.createProgressIndicator('Hello World'); + + disposable.dispose(); + + // appShell.WithProgress should complete. + await callbackPromise!; + verify(vscodeProgressReporter.report(anything())).never(); + }); + test('Report progress until disposed', async () => { + let callbackPromise: Promise<{}> | undefined; + const cancel = new CancellationTokenSource(); + when(appShell.withProgress(anything(), anything())).thenCall((_, cb: Task<{}>) => { + return (callbackPromise = cb(instance(vscodeProgressReporter), cancel.token)); + }); + + const disposable = reporter.createProgressIndicator('Hello World'); + const progressMessages: string[] = []; + const expectedProgressMessages: string[] = []; + + when(vscodeProgressReporter.report(anything())).thenCall((msg: { message: string }) => + progressMessages.push(msg.message) + ); + // Perform an action and ensure that we display the message. + + //1. Start notebook & ensure we display notebook stating message. + reporter.report({ action: ReportableAction.NotebookStart, phase: 'started' }); + expectedProgressMessages.push(getUserMessageForAction(ReportableAction.NotebookStart)!); + + //2. Get kernel specs & ensure we display kernel specs message. + reporter.report({ action: ReportableAction.KernelsGetKernelSpecs, phase: 'started' }); + expectedProgressMessages.push(getUserMessageForAction(ReportableAction.KernelsGetKernelSpecs)!); + + //3. Register kernel & ensure we display registering message. + reporter.report({ action: ReportableAction.KernelsRegisterKernel, phase: 'started' }); + expectedProgressMessages.push(getUserMessageForAction(ReportableAction.KernelsRegisterKernel)!); + + //4. Wait for idle & ensure we display registering message. + reporter.report({ action: ReportableAction.JupyterSessionWaitForIdleSession, phase: 'started' }); + expectedProgressMessages.push(getUserMessageForAction(ReportableAction.JupyterSessionWaitForIdleSession)!); + + //5. Finish getting kernel specs, should display previous (idle) message again. + reporter.report({ action: ReportableAction.KernelsGetKernelSpecs, phase: 'completed' }); + expectedProgressMessages.push(getUserMessageForAction(ReportableAction.JupyterSessionWaitForIdleSession)!); + + //6. Finish waiting for idle, should display the register kernel as that's still in progress. + reporter.report({ action: ReportableAction.JupyterSessionWaitForIdleSession, phase: 'completed' }); + expectedProgressMessages.push(getUserMessageForAction(ReportableAction.KernelsRegisterKernel)!); + + //6. Finish registering kernel, should display the starting notebook as that's still in progress. + reporter.report({ action: ReportableAction.KernelsRegisterKernel, phase: 'completed' }); + expectedProgressMessages.push(getUserMessageForAction(ReportableAction.NotebookStart)!); + + //6. Finish starting notebook, no new messages to display. + reporter.report({ action: ReportableAction.NotebookStart, phase: 'completed' }); + verify(vscodeProgressReporter.report(anything())).times(expectedProgressMessages.length); + + // Confirm the messages were displayed in the order we expected. + assert.equal(progressMessages.join(', '), expectedProgressMessages.join(', ')); + + // appShell.WithProgress should complete. + disposable.dispose(); + await callbackPromise!; + }); +}); diff --git a/src/test/datascience/raw-kernel/rawKernel.functional.test.ts b/src/test/datascience/raw-kernel/rawKernel.functional.test.ts new file mode 100644 index 000000000000..dc23ab8bf5cb --- /dev/null +++ b/src/test/datascience/raw-kernel/rawKernel.functional.test.ts @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert } from 'chai'; +import { noop } from 'jquery'; +import * as portfinder from 'portfinder'; +import * as uuid from 'uuid/v4'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { createDeferred, sleep } from '../../../client/common/utils/async'; +import { KernelDaemonPool } from '../../../client/datascience/kernel-launcher/kernelDaemonPool'; +import { KernelProcess } from '../../../client/datascience/kernel-launcher/kernelProcess'; +import { createRawKernel, RawKernel } from '../../../client/datascience/raw-kernel/rawKernel'; +import { IDataScienceFileSystem, IJupyterKernelSpec } from '../../../client/datascience/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { DataScienceIocContainer } from '../dataScienceIocContainer'; +import { requestExecute, requestInspect } from './rawKernelTestHelpers'; + +// tslint:disable:no-any no-multiline-string max-func-body-length no-console max-classes-per-file trailing-comma +suite('DataScience raw kernel tests', () => { + let ioc: DataScienceIocContainer; + let rawKernel: RawKernel; + const connectionInfo = { + shell_port: 57718, + iopub_port: 57719, + stdin_port: 57720, + control_port: 57721, + hb_port: 57722, + ip: '127.0.0.1', + key: 'c29c2121-d277576c2c035f0aceeb5068', + transport: 'tcp', + signature_scheme: 'hmac-sha256', + kernel_name: 'python3', + version: 5.1 + }; + let kernelProcess: KernelProcess; + setup(async function () { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(); + await ioc.activate(); + if (ioc.mockJupyter) { + // tslint:disable-next-line: no-invalid-this + this.skip(); + } else { + const port = await portfinder.getPortPromise({ startPort: 57718 }); + rawKernel = await connectToKernel(port); + } + }); + + teardown(async () => { + await disconnectFromKernel(); + await ioc.dispose(); + }); + + async function connectToKernel(startPort: number): Promise<RawKernel> { + connectionInfo.stdin_port = startPort; + connectionInfo.shell_port = startPort + 1; + connectionInfo.iopub_port = startPort + 2; + connectionInfo.hb_port = startPort + 3; + connectionInfo.control_port = startPort + 4; + + // Find our jupyter interpreter + const interpreter = await ioc + .get<IInterpreterService>(IInterpreterService) + .getInterpreterDetails(ioc.getSettings().pythonPath); + assert.ok(interpreter, 'No jupyter interpreter found'); + // Start our kernel + const kernelSpec: IJupyterKernelSpec = { + argv: [interpreter!.path, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], + metadata: { + interpreter + }, + display_name: '', + env: undefined, + language: 'python', + name: '', + path: interpreter!.path, + id: uuid() + }; + kernelProcess = new KernelProcess( + ioc.get<IProcessServiceFactory>(IProcessServiceFactory), + ioc.get<KernelDaemonPool>(KernelDaemonPool), + connectionInfo as any, + { kernelSpec, interpreter, kind: 'startUsingKernelSpec' }, + ioc.get<IDataScienceFileSystem>(IDataScienceFileSystem), + undefined + ); + await kernelProcess.launch(process.cwd()); + return createRawKernel(kernelProcess, uuid()); + } + + async function disconnectFromKernel() { + if (kernelProcess) { + await kernelProcess.dispose().catch(noop); + } + } + + async function shutdown(): Promise<void> { + return rawKernel.shutdown(); + } + + test('Basic connection', async () => { + let exited = false; + kernelProcess.exited(() => (exited = true)); + await shutdown(); + await sleep(2500); // Give time for the shutdown to go across + assert.ok(exited, 'Kernel did not shutdown'); + }); + + test('Basic request', async () => { + const replies = await requestExecute(rawKernel, 'a=1\na'); + const executeResult = replies.find((r) => r.header.msg_type === 'execute_result'); + assert.ok(executeResult, 'Result not found'); + assert.equal((executeResult?.content as any).data['text/plain'], '1', 'Results were not computed'); + }); + + test('Interrupt pending request', async () => { + const executionStarted = createDeferred<void>(); + + // If the interrupt doesn't work, then test will timeout as execution will sleep for `300s`. + // Hence timeout is a test failure. + const longCellExecutionRequest = requestExecute( + rawKernel, + 'import time\nfor i in range(300):\n time.sleep(1)', + executionStarted + ); + + // Wait until the execution has started (cuz we cannot interrupt until exec has started). + await executionStarted.promise; + + // Then throw the interrupt + await rawKernel.interrupt(); + + // Verify our results + const replies = await longCellExecutionRequest; + const executeResult = replies.find((r) => r.header.msg_type === 'execute_reply'); + assert.ok(executeResult, 'Result not found'); + assert.equal((executeResult?.content as any).ename, 'KeyboardInterrupt', 'Interrupt not found'); + + // Based on tests 2s is sufficient. Lets give 10s for CI and slow Windows machines. + }).timeout(10_000); + + test('Multiple requests', async () => { + let replies = await requestExecute(rawKernel, 'a=1\na'); + let executeResult = replies.find((r) => r.header.msg_type === 'execute_result'); + assert.ok(executeResult, 'Result not found'); + replies = await requestExecute(rawKernel, 'a=2\na'); + executeResult = replies.find((r) => r.header.msg_type === 'execute_result'); + assert.ok(executeResult, 'Result 2 not found'); + assert.equal((executeResult?.content as any).data['text/plain'], '2', 'Results were not computed'); + const json = await requestInspect(rawKernel, 'a'); + assert.ok(json, 'Inspect reply was not computed'); + }); + + test('Startup and shutdown', async () => { + let replies = await requestExecute(rawKernel, 'a=1\na'); + let executeResult = replies.find((r) => r.header.msg_type === 'execute_result'); + assert.ok(executeResult, 'Result not found'); + await shutdown(); + await sleep(2500); // Give time for the shutdown to go across + const port = await portfinder.getPortPromise({ startPort: 57418 }); + rawKernel = await connectToKernel(port); + replies = await requestExecute(rawKernel, 'a=1\na'); + executeResult = replies.find((r) => r.header.msg_type === 'execute_result'); + assert.ok(executeResult, 'Result not found'); + }); +}); diff --git a/src/test/datascience/raw-kernel/rawKernelTestHelpers.ts b/src/test/datascience/raw-kernel/rawKernelTestHelpers.ts new file mode 100644 index 000000000000..43d806423f6f --- /dev/null +++ b/src/test/datascience/raw-kernel/rawKernelTestHelpers.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { KernelMessage } from '@jupyterlab/services'; +import { JSONObject } from '@phosphor/coreutils'; +import { createDeferred, Deferred } from '../../../client/common/utils/async'; +import { RawKernel } from '../../../client/datascience/raw-kernel/rawKernel'; + +// tslint:disable: no-any +export async function requestExecute( + rawKernel: RawKernel, + code: string, + started?: Deferred<void> +): Promise<KernelMessage.IMessage[]> { + const waiter = createDeferred<KernelMessage.IMessage<KernelMessage.MessageType>[]>(); + const requestContent = { + code, + silent: false, + store_history: false + }; + + const replies: KernelMessage.IMessage<KernelMessage.MessageType>[] = []; + let foundReply = false; + let foundIdle = false; + const ioPubHandler = (m: KernelMessage.IIOPubMessage) => { + replies.push(m); + if (m.header.msg_type === 'status') { + foundIdle = (m.content as any).execution_state === 'idle'; + if (started && (m.content as any).execution_state === 'busy') { + started.resolve(); + } + } + if (!waiter.resolved && foundReply && foundIdle) { + waiter.resolve(replies); + } + }; + const shellHandler = (m: KernelMessage.IExecuteReplyMsg | KernelMessage.IExecuteRequestMsg) => { + replies.push(m); + if (m.header.msg_type === 'execute_reply') { + foundReply = true; + } + if (!waiter.resolved && foundReply && foundIdle) { + waiter.resolve(replies); + } + }; + const future = rawKernel.requestExecute(requestContent); + future.onIOPub = ioPubHandler; + future.onReply = shellHandler; + rawKernel.requestExecute(requestContent, true); + return waiter.promise.then((m) => { + return m; + }); +} + +export async function requestInspect(rawKernel: RawKernel, code: string): Promise<JSONObject> { + // Create a deferred that will fire when the request completes + const deferred = createDeferred<JSONObject>(); + + rawKernel + .requestInspect({ code, cursor_pos: 0, detail_level: 0 }) + .then((r) => { + if (r && r.content.status === 'ok') { + deferred.resolve(r.content.data); + } else { + deferred.resolve(undefined); + } + }) + .catch((ex) => { + deferred.reject(ex); + }); + + return deferred.promise; +} diff --git a/src/test/datascience/reactHelpers.ts b/src/test/datascience/reactHelpers.ts new file mode 100644 index 000000000000..5ce9a617b721 --- /dev/null +++ b/src/test/datascience/reactHelpers.ts @@ -0,0 +1,548 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// Note: Don't change this to a tsx file as it loads in the unit tests. That will mess up mocha + +// Custom module loader so we can skip loading the 'canvas' module which won't load +// inside of vscode +// tslint:disable:no-var-requires no-require-imports no-any no-function-expression +const Module = require('module'); + +(function () { + const origRequire = Module.prototype.require; + const _require = (context: any, filepath: any) => { + return origRequire.call(context, filepath); + }; + Module.prototype.require = function (filepath: string) { + if (filepath === 'canvas') { + try { + // Make sure we aren't inside of vscode. The nodejs version of Canvas won't match. At least sometimes. + if (require('vscode')) { + return ''; + } + } catch { + // This should happen when not inside vscode. + } + } + // tslint:disable-next-line:no-invalid-this + return _require(this, filepath); + }; +})(); + +// tslint:disable:no-string-literal no-any object-literal-key-quotes max-func-body-length member-ordering +// tslint:disable: no-require-imports no-var-requires + +// Monkey patch the stylesheet impl from jsdom before loading jsdom. +// This is necessary to get slickgrid to work. +const utils = require('jsdom/lib/jsdom/living/generated/utils'); +const ssExports = require('jsdom/lib/jsdom/living/helpers/stylesheets'); +if (ssExports && ssExports.createStylesheet) { + const orig = ssExports.createStylesheet; + ssExports.createStylesheet = (sheetText: any, elementImpl: any, baseURL: any) => { + // Call the original. + orig(sheetText, elementImpl, baseURL); + + // Then pull out the style sheet and add some properties. See the discussion here + // https://github.com/jsdom/jsdom/issues/992 + if (elementImpl.sheet) { + elementImpl.sheet.href = baseURL; + elementImpl.sheet.ownerNode = utils.wrapperForImpl(elementImpl); + } + }; +} + +import { configure } from 'enzyme'; +import * as Adapter from 'enzyme-adapter-react-16'; +import { DOMWindow, JSDOM } from 'jsdom'; + +import { noop } from '../../client/common/utils/misc'; + +class MockCanvas implements CanvasRenderingContext2D { + public canvas!: HTMLCanvasElement; + public restore(): void { + throw new Error('Method not implemented.'); + } + public save(): void { + throw new Error('Method not implemented.'); + } + public getTransform(): DOMMatrix { + throw new Error('Method not implemented.'); + } + public resetTransform(): void { + throw new Error('Method not implemented.'); + } + public rotate(_angle: number): void { + throw new Error('Method not implemented.'); + } + public scale(_x: number, _y: number): void { + throw new Error('Method not implemented.'); + } + public setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void; + public setTransform(transform?: DOMMatrix2DInit | undefined): void; + public setTransform(_a?: any, _b?: any, _c?: any, _d?: any, _e?: any, _f?: any) { + throw new Error('Method not implemented.'); + } + public transform(_a: number, _b: number, _c: number, _d: number, _e: number, _f: number): void { + throw new Error('Method not implemented.'); + } + public translate(_x: number, _y: number): void { + throw new Error('Method not implemented.'); + } + public globalAlpha!: number; + public globalCompositeOperation!: string; + public imageSmoothingEnabled!: boolean; + public imageSmoothingQuality!: ImageSmoothingQuality; + public fillStyle!: string | CanvasGradient | CanvasPattern; + public strokeStyle!: string | CanvasGradient | CanvasPattern; + public createLinearGradient(_x0: number, _y0: number, _x1: number, _y1: number): CanvasGradient { + throw new Error('Method not implemented.'); + } + public createPattern(_image: CanvasImageSource, _repetition: string): CanvasPattern | null { + throw new Error('Method not implemented.'); + } + public createRadialGradient( + _x0: number, + _y0: number, + _r0: number, + _x1: number, + _y1: number, + _r1: number + ): CanvasGradient { + throw new Error('Method not implemented.'); + } + public shadowBlur!: number; + public shadowColor!: string; + public shadowOffsetX!: number; + public shadowOffsetY!: number; + public filter!: string; + public clearRect(_x: number, _y: number, _w: number, _h: number): void { + throw new Error('Method not implemented.'); + } + public fillRect(_x: number, _y: number, _w: number, _h: number): void { + throw new Error('Method not implemented.'); + } + public strokeRect(_x: number, _y: number, _w: number, _h: number): void { + throw new Error('Method not implemented.'); + } + public beginPath(): void { + throw new Error('Method not implemented.'); + } + public clip(fillRule?: 'nonzero' | 'evenodd' | undefined): void; + public clip(path: Path2D, fillRule?: 'nonzero' | 'evenodd' | undefined): void; + public clip(_path?: any, _fillRule?: any) { + throw new Error('Method not implemented.'); + } + public fill(fillRule?: 'nonzero' | 'evenodd' | undefined): void; + public fill(path: Path2D, fillRule?: 'nonzero' | 'evenodd' | undefined): void; + public fill(_path?: any, _fillRule?: any) { + throw new Error('Method not implemented.'); + } + public isPointInPath(x: number, y: number, fillRule?: 'nonzero' | 'evenodd' | undefined): boolean; + public isPointInPath(path: Path2D, x: number, y: number, fillRule?: 'nonzero' | 'evenodd' | undefined): boolean; + public isPointInPath(_path: any, _x: any, _y?: any, _fillRule?: any): boolean { + throw new Error('Method not implemented.'); + } + public isPointInStroke(x: number, y: number): boolean; + public isPointInStroke(path: Path2D, x: number, y: number): boolean; + public isPointInStroke(_path: any, _x: any, _y?: any): boolean { + throw new Error('Method not implemented.'); + } + public stroke(): void; + // tslint:disable-next-line: unified-signatures + public stroke(path: Path2D): void; + public stroke(_path?: any) { + throw new Error('Method not implemented.'); + } + public drawFocusIfNeeded(element: Element): void; + public drawFocusIfNeeded(path: Path2D, element: Element): void; + public drawFocusIfNeeded(_path: any, _element?: any) { + throw new Error('Method not implemented.'); + } + public scrollPathIntoView(): void; + // tslint:disable-next-line: unified-signatures + public scrollPathIntoView(path: Path2D): void; + public scrollPathIntoView(_path?: any) { + throw new Error('Method not implemented.'); + } + public fillText(_text: string, _x: number, _y: number, _maxWidth?: number | undefined): void { + throw new Error('Method not implemented.'); + } + public measureText(_text: string): TextMetrics { + throw new Error('Method not implemented.'); + } + public strokeText(_text: string, _x: number, _y: number, _maxWidth?: number | undefined): void { + throw new Error('Method not implemented.'); + } + public drawImage(image: CanvasImageSource, dx: number, dy: number): void; + public drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void; + public drawImage( + image: CanvasImageSource, + sx: number, + sy: number, + sw: number, + sh: number, + dx: number, + dy: number, + dw: number, + dh: number + ): void; + public drawImage( + _image: any, + _sx: any, + _sy: any, + _sw?: any, + _sh?: any, + _dx?: any, + _dy?: any, + _dw?: any, + _dh?: any + ) { + throw new Error('Method not implemented.'); + } + public createImageData(sw: number, sh: number): ImageData; + public createImageData(imagedata: ImageData): ImageData; + public createImageData(_sw: any, _sh?: any): ImageData { + throw new Error('Method not implemented.'); + } + public getImageData(_sx: number, _sy: number, _sw: number, _sh: number): ImageData { + throw new Error('Method not implemented.'); + } + public putImageData(imagedata: ImageData, dx: number, dy: number): void; + public putImageData( + imagedata: ImageData, + dx: number, + dy: number, + dirtyX: number, + dirtyY: number, + dirtyWidth: number, + dirtyHeight: number + ): void; + public putImageData( + _imagedata: any, + _dx: any, + _dy: any, + _dirtyX?: any, + _dirtyY?: any, + _dirtyWidth?: any, + _dirtyHeight?: any + ) { + throw new Error('Method not implemented.'); + } + public lineCap!: CanvasLineCap; + public lineDashOffset!: number; + public lineJoin!: CanvasLineJoin; + public lineWidth!: number; + public miterLimit!: number; + public getLineDash(): number[] { + throw new Error('Method not implemented.'); + } + public setLineDash(_segments: number[]): void { + throw new Error('Method not implemented.'); + } + public direction!: CanvasDirection; + public font!: string; + public textAlign!: CanvasTextAlign; + public textBaseline!: CanvasTextBaseline; + public arc( + _x: number, + _y: number, + _radius: number, + _startAngle: number, + _endAngle: number, + _anticlockwise?: boolean | undefined + ): void { + throw new Error('Method not implemented.'); + } + public arcTo(_x1: number, _y1: number, _x2: number, _y2: number, _radius: number): void { + throw new Error('Method not implemented.'); + } + public bezierCurveTo(_cp1x: number, _cp1y: number, _cp2x: number, _cp2y: number, _x: number, _y: number): void { + throw new Error('Method not implemented.'); + } + public closePath(): void { + throw new Error('Method not implemented.'); + } + public ellipse( + _x: number, + _y: number, + _radiusX: number, + _radiusY: number, + _rotation: number, + _startAngle: number, + _endAngle: number, + _anticlockwise?: boolean | undefined + ): void { + throw new Error('Method not implemented.'); + } + public lineTo(_x: number, _y: number): void { + throw new Error('Method not implemented.'); + } + public moveTo(_x: number, _y: number): void { + throw new Error('Method not implemented.'); + } + public quadraticCurveTo(_cpx: number, _cpy: number, _x: number, _y: number): void { + throw new Error('Method not implemented.'); + } + public rect(_x: number, _y: number, _w: number, _h: number): void { + throw new Error('Method not implemented.'); + } +} + +const mockCanvas = new MockCanvas(); + +export function setUpDomEnvironment() { + // tslint:disable-next-line:no-http-string + const dom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', { + pretendToBeVisual: true, + url: 'http://localhost' + }); + const { window } = dom; + + // tslint:disable: no-function-expression no-empty + try { + // If running inside of vscode, we need to mock the canvas because the real canvas is not + // returned. + if (require('vscode')) { + window.HTMLCanvasElement.prototype.getContext = (contextId: string, _contextAttributes?: {}): any => { + if (contextId === '2d') { + return mockCanvas; + } + return null; + }; + } + } catch { + noop(); + } + + // tslint:disable-next-line: no-function-expression + window.HTMLCanvasElement.prototype.toDataURL = function () { + return ''; + }; + + // tslist:disable-next-line:no-string-literal no-any + (global as any)['Element'] = window.Element; + // tslist:disable-next-line:no-string-literal no-any + (global as any)['location'] = window.location; + // tslint:disable-next-line:no-string-literal no-any + (global as any)['window'] = window; + // tslint:disable-next-line:no-string-literal no-any + (global as any)['document'] = window.document; + // tslint:disable-next-line:no-string-literal no-any + (global as any)['navigator'] = { + userAgent: 'node.js', + platform: 'node' + }; + (global as any)['Event'] = window.Event; + (global as any)['KeyboardEvent'] = window.KeyboardEvent; + (global as any)['MouseEvent'] = window.MouseEvent; + (global as any)['DocumentFragment'] = window.DocumentFragment; + // tslint:disable-next-line:no-string-literal no-any + (global as any)['getComputedStyle'] = window.getComputedStyle; + // tslint:disable-next-line:no-string-literal no-any + (global as any)['self'] = window; + copyProps(window, global); + + // Special case. Monaco needs queryCommandSupported + (global as any)['document'].queryCommandSupported = () => false; + + // Special case. Transform needs createRange + (global as any)['document'].createRange = () => ({ + createContextualFragment: (str: string) => JSDOM.fragment(str), + setEnd: (_endNode: any, _endOffset: any) => noop(), + setStart: (_startNode: any, _startOffset: any) => noop(), + getBoundingClientRect: () => null, + getClientRects: () => [] + }); + + // Another special case. CodeMirror needs selection + // tslint:disable-next-line:no-string-literal no-any + (global as any)['document'].selection = { + anchorNode: null, + anchorOffset: 0, + baseNode: null, + baseOffset: 0, + extentNode: null, + extentOffset: 0, + focusNode: null, + focusOffset: 0, + isCollapsed: false, + rangeCount: 0, + type: '', + addRange: (_range: Range) => noop(), + createRange: () => null, + collapse: (_parentNode: Node, _offset: number) => noop(), + collapseToEnd: noop, + collapseToStart: noop, + containsNode: (_node: Node, _partlyContained: boolean) => false, + deleteFromDocument: noop, + empty: noop, + extend: (_newNode: Node, _offset: number) => noop(), + getRangeAt: (_index: number) => null, + removeAllRanges: noop, + removeRange: (_range: Range) => noop(), + selectAllChildren: (_parentNode: Node) => noop(), + setBaseAndExtent: (_baseNode: Node, _baseOffset: number, _extentNode: Node, _extentOffset: number) => noop(), + setPosition: (_parentNode: Node, _offset: number) => noop(), + toString: () => '{Selection}' + }; + + // For Jupyter server to load correctly. It expects the window object to not be defined + // tslint:disable-next-line:no-eval no-any + const fetchMod = eval('require')('node-fetch'); + // tslint:disable-next-line:no-string-literal no-any + (global as any)['fetch'] = fetchMod; + // tslint:disable-next-line:no-string-literal no-any + (global as any)['Request'] = fetchMod.Request; + // tslint:disable-next-line:no-string-literal no-any + (global as any)['Headers'] = fetchMod.Headers; + // tslint:disable-next-line:no-string-literal no-eval no-any + (global as any)['WebSocket'] = eval('require')('ws'); + (global as any)['DOMParser'] = dom.window.DOMParser; + (global as any)['Blob'] = dom.window.Blob; + + configure({ adapter: new Adapter() }); + + // Special case for the node_modules\monaco-editor\esm\vs\editor\browser\config\configuration.js. It doesn't + // export the function we need to dispose of the timer it's set. So force it to. + const configurationRegex = /.*(\\|\/)node_modules(\\|\/)monaco-editor(\\|\/)esm(\\|\/)vs(\\|\/)editor(\\|\/)browser(\\|\/)config(\\|\/)configuration\.js/g; + const _oldLoader = require.extensions['.js']; + // tslint:disable-next-line:no-function-expression + require.extensions['.js'] = function (mod: any, filename) { + if (configurationRegex.test(filename)) { + let content = require('fs').readFileSync(filename, 'utf8'); + content += 'export function getCSSBasedConfiguration() { return CSSBasedConfiguration.INSTANCE; };\n'; + mod._compile(content, filename); + } else { + _oldLoader(mod, filename); + } + }; +} + +export function setupTranspile() { + // Some special work for getting the monaco editor to work. + // We need to babel transpile some modules. Monaco-editor is not in commonJS format so imports + // can't be loaded. + require('@babel/register')({ plugins: ['@babel/transform-modules-commonjs'], only: [/monaco-editor/] }); + + // Special case for editor api. Webpack bundles editor.all.js as well. Tests don't. + require('monaco-editor/esm/vs/editor/editor.api'); + require('monaco-editor/esm/vs/editor/editor.all'); +} + +function copyProps(src: any, target: any) { + const props = Object.getOwnPropertyNames(src).filter((prop) => typeof target[prop] === undefined); + props.forEach((p: string) => { + target[p] = src[p]; + }); +} + +// map of string chars to keycodes and whether or not shift has to be hit +// this is necessary to generate keypress/keydown events. +// There doesn't seem to be an official way to do this (according to stack overflow) +// so just hardcoding it here. +const keyMap: { [key: string]: { code: number; shift: boolean } } = { + A: { code: 65, shift: false }, + B: { code: 66, shift: false }, + C: { code: 67, shift: false }, + D: { code: 68, shift: false }, + E: { code: 69, shift: false }, + F: { code: 70, shift: false }, + G: { code: 71, shift: false }, + H: { code: 72, shift: false }, + I: { code: 73, shift: false }, + J: { code: 74, shift: false }, + K: { code: 75, shift: false }, + L: { code: 76, shift: false }, + M: { code: 77, shift: false }, + N: { code: 78, shift: false }, + O: { code: 79, shift: false }, + P: { code: 80, shift: false }, + Q: { code: 81, shift: false }, + R: { code: 82, shift: false }, + S: { code: 83, shift: false }, + T: { code: 84, shift: false }, + U: { code: 85, shift: false }, + V: { code: 86, shift: false }, + W: { code: 87, shift: false }, + X: { code: 88, shift: false }, + Y: { code: 89, shift: false }, + Z: { code: 90, shift: false }, + ESCAPE: { code: 27, shift: false }, + '0': { code: 48, shift: false }, + '1': { code: 49, shift: false }, + '2': { code: 50, shift: false }, + '3': { code: 51, shift: false }, + '4': { code: 52, shift: false }, + '5': { code: 53, shift: false }, + '6': { code: 54, shift: false }, + '7': { code: 55, shift: false }, + '8': { code: 56, shift: false }, + '9': { code: 57, shift: false }, + ')': { code: 48, shift: true }, + '!': { code: 49, shift: true }, + '@': { code: 50, shift: true }, + '#': { code: 51, shift: true }, + $: { code: 52, shift: true }, + '%': { code: 53, shift: true }, + '^': { code: 54, shift: true }, + '&': { code: 55, shift: true }, + '*': { code: 56, shift: true }, + '(': { code: 57, shift: true }, + '[': { code: 219, shift: false }, + '\\': { code: 209, shift: false }, + ']': { code: 221, shift: false }, + '{': { code: 219, shift: true }, + '|': { code: 209, shift: true }, + '}': { code: 221, shift: true }, + ';': { code: 186, shift: false }, + "'": { code: 222, shift: false }, + ':': { code: 186, shift: true }, + '"': { code: 222, shift: true }, + ',': { code: 188, shift: false }, + '.': { code: 190, shift: false }, + '/': { code: 191, shift: false }, + '<': { code: 188, shift: true }, + '>': { code: 190, shift: true }, + '?': { code: 191, shift: true }, + '`': { code: 192, shift: false }, + '~': { code: 192, shift: true }, + ' ': { code: 32, shift: false }, + '\n': { code: 13, shift: false }, + '\r': { code: 0, shift: false } // remove \r from the text. +}; + +export function createMessageEvent(data: any): MessageEvent { + const domWindow = (window as any) as DOMWindow; + return new domWindow.MessageEvent('message', { data }); +} + +export function createKeyboardEvent(type: string, options: KeyboardEventInit): KeyboardEvent { + const domWindow = (window as any) as DOMWindow; + options.bubbles = true; + options.cancelable = true; + + // charCodes and keyCodes are different things. Compute the keycode for cm to work. + // This is the key (on an english qwerty keyboard) that would have to be hit to generate the key + // This site was a great help with the mapping: + // https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes + const upper = options.key!.toUpperCase(); + const keyCode = keyMap.hasOwnProperty(upper) ? keyMap[upper].code : options.key!.charCodeAt(0); + const shift = keyMap.hasOwnProperty(upper) ? keyMap[upper].shift || options.shiftKey : options.shiftKey; + + // JSDOM doesn't support typescript so well. The options are supposed to be flexible to support just about anything, but + // the type KeyboardEventInit only supports the minimum. Stick in extras with some typecasting hacks + return new domWindow.KeyboardEvent(type, ({ ...options, keyCode, shiftKey: shift } as any) as KeyboardEventInit); +} + +export function createInputEvent(): Event { + const domWindow = (window as any) as DOMWindow; + return new domWindow.Event('input', { bubbles: true, cancelable: false }); +} + +export function blurWindow() { + // blur isn't implemented. We just need to dispatch the blur event + const domWindow = (window as any) as DOMWindow; + const blurEvent = new domWindow.Event('blur', { bubbles: true }); + domWindow.dispatchEvent(blurEvent); +} diff --git a/src/test/datascience/remoteTestHelpers.ts b/src/test/datascience/remoteTestHelpers.ts new file mode 100644 index 000000000000..e847f1275ca2 --- /dev/null +++ b/src/test/datascience/remoteTestHelpers.ts @@ -0,0 +1,61 @@ +import { traceInfo } from '../../client/common/logger'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../client/common/process/types'; +import { IDisposableRegistry } from '../../client/common/types'; +import { createDeferred, sleep } from '../../client/common/utils/async'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { getIPConnectionInfo } from './jupyterHelpers'; + +export async function createPythonService( + ioc: DataScienceIocContainer, + versionRequirement?: number +): Promise<IPythonExecutionService | undefined> { + if (!ioc.mockJupyter) { + const python = await ioc.getJupyterCapableInterpreter(); + const pythonFactory = ioc.get<IPythonExecutionFactory>(IPythonExecutionFactory); + + if (python && python.version?.major && (!versionRequirement || python.version?.major > versionRequirement)) { + return pythonFactory.createActivatedEnvironment({ + resource: undefined, + interpreter: python, + allowEnvironmentFetchExceptions: true, + bypassCondaExecution: true + }); + } + } +} + +export async function startRemoteServer( + ioc: DataScienceIocContainer, + pythonService: IPythonExecutionService, + args: string[] +): Promise<string> { + const connectionFound = createDeferred(); + const exeResult = pythonService.execObservable(args, { + throwOnStdErr: false + }); + ioc.get<IDisposableRegistry>(IDisposableRegistry).push(exeResult); + exeResult.out.subscribe( + (output: Output<string>) => { + traceInfo(`Remote server output: ${output.out}`); + const connectionURL = getIPConnectionInfo(output.out); + if (connectionURL) { + connectionFound.resolve(connectionURL); + } + }, + (e) => { + traceInfo(`Remote server error: ${e}`); + connectionFound.reject(e); + } + ); + + traceInfo('Connecting to remote server'); + const connString = await connectionFound.promise; + const uri = connString as string; + + // Wait another 3 seconds to give notebook time to be ready. Not sure + // how else to know when it's okay to connect to. Mac on azure seems + // to connect too fast and then is unable to actually communicate. + await sleep(3000); + + return uri; +} diff --git a/src/test/datascience/serverConfigFiles/jcert.pem b/src/test/datascience/serverConfigFiles/jcert.pem new file mode 100644 index 000000000000..01a4008fdd5b --- /dev/null +++ b/src/test/datascience/serverConfigFiles/jcert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDeTCCAmGgAwIBAgIJAKa7Vk5Yxq+5MA0GCSqGSIb3DQEBCwUAMFMxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdSZWRtb2RuMQ0w +CwYDVQQKDAROb25lMQ4wDAYDVQQDDAVpYW5odTAeFw0xOTA1MTQyMjMzMDZaFw0y +MDA1MTMyMjMzMDZaMFMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9u +MRAwDgYDVQQHDAdSZWRtb2RuMQ0wCwYDVQQKDAROb25lMQ4wDAYDVQQDDAVpYW5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKpEcEMQvPwSw3Xl4cqt +2jwakzDBbRB5dxW+SVUhGnyPc6Ime4HioP1ch3+OqjVnstne2WKk3bDnpTSp2wX8 +yAY64CY/eLexGYVZed7hKu79rFKaEe+W7fjUER+36DsIo5JZ81LpRLCusrUrd2/A +12rAJm8EmPgqdmSZo51PHbuFOLKT+95pNxEkxrsmDCv/jUVU0iGkkAAsizne7gqa +FBMwo+MHd+1OBkxyXdG/0yrFuFV6AZ3KXZBMTZmtrW+eUSHJ/DrVPAdXOvPoO1d3 +9VJndD2UpZgkYX2povBxVdslHctDaNnuhGb463ldQXED4M0Fq6Ojnu/rn3GzmUJ1 +tskCAwEAAaNQME4wHQYDVR0OBBYEFJIGYn57WFv1QzrPbPHzJolZMGJUMB8GA1Ud +IwQYMBaAFJIGYn57WFv1QzrPbPHzJolZMGJUMAwGA1UdEwQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggEBAIc+IlU5ri52OUDBiPWHGQSOMjcCJ9SDjV6Z0GlGRvWlp9Pp +MJraFl7umZa8sZmRHfjX2dm4x3JHw1pcCdicyYy/Hig1A2MiGZLzUjQcO94ZBj2E +pddOria2KgDiXbULprQMyGCR4aNQKs3ycaNufFr53QrLFa8OGpeYS5nILHQkVq4N +pa9sjlsVoafQXlngSYOUt4VUWm2RUZ61jgnITqIause9wlxY7kW/YUXUCMUW/QEE +wCQplIMrHMBzLK7piNZrGMSfB/pXvnM+9zy2lLclI7ipAZCOW44340s/8w5Zc4fm +cryiN3LEGkuFv3XTSgflLk5f0At77yb+lpCoPG8= +-----END CERTIFICATE----- diff --git a/src/test/datascience/serverConfigFiles/jkey.key b/src/test/datascience/serverConfigFiles/jkey.key new file mode 100644 index 000000000000..370a3c5da279 --- /dev/null +++ b/src/test/datascience/serverConfigFiles/jkey.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqRHBDELz8EsN1 +5eHKrdo8GpMwwW0QeXcVvklVIRp8j3OiJnuB4qD9XId/jqo1Z7LZ3tlipN2w56U0 +qdsF/MgGOuAmP3i3sRmFWXne4Sru/axSmhHvlu341BEft+g7CKOSWfNS6USwrrK1 +K3dvwNdqwCZvBJj4KnZkmaOdTx27hTiyk/veaTcRJMa7Jgwr/41FVNIhpJAALIs5 +3u4KmhQTMKPjB3ftTgZMcl3Rv9MqxbhVegGdyl2QTE2Zra1vnlEhyfw61TwHVzrz +6DtXd/VSZ3Q9lKWYJGF9qaLwcVXbJR3LQ2jZ7oRm+Ot5XUFxA+DNBaujo57v659x +s5lCdbbJAgMBAAECggEAf5ri96AnwlLdohIy8g7xK3JPNY8BCgO+N9FwbBhvHUL1 +SmTE00bhmIAsHHDzJyscYyQcj003yEkTCzDxML+NuP2O15tiAWj802+HYn4mCw6a +gx1sq77VglKMstTFetiynhBDx7ODA1cqH5T/4gUIbLytES7E5dgM+sAaWt7cTZgF +M4c48/Rrb/7O11Kkout+6Zv/FVTt6Fqe3HPxuIW/SF0xgSJIH/lWoGlUaG6DxvEl +wZbNhSYg+jDrf/gnhwQR17H/MmchovKiYCPhDKZnzCao+hzC6mDLshKP3gmo0RA0 +zYRFYGOlEph3TKYcqooRub2HRckMBcOBXjrpxIpsAQKBgQDUv2zfTFJwfE4RE5Xv +1NzPhMRMEW5HCx90v6CjY6oqLJK9U/uLH14hdfr2Mb5SPieBEHBuE971hmXqK1Bo +8H7tXbxiZR9P5k1Yz6UhWnHxZeJxsFYT+JVNFjXHRMQEnhHihN/AtZAYmvwp0Mwd +13OJSBAmeXZr5anpU5InyQ4IqQKBgQDM4hiujo7zQf7ZZhcwY3o2YF/xXToKrP7f +hoIvJn3fcdde19SGElVvE2Lir2XTdNEv5VbjOOywHFH6rt8oMEEjnavzq1rgVrjr +3Q1pAHdjQFFyC8lPmakGgQWkay8wc0wIxUoEz4fRnBcOut1azbZ4+3y9Kuzp6MwR +N5CA97RxIQKBgQCXFR4u8Zd19IDIFb2b5PGumV2Bm7tRzm9XTKK6hZOZga/vrg1r +vint30gKwEalRyhsuoztT0U93WTQyFPBQlERJkkbIy76YdW55TQinIVgZfdKv2xR +oG3+oXAthAMkOFEBKVVxGD8tihrbY0EhTBjre/akLAvSEfX5EfUwNdK2iQKBgGoj +awPq6FVOyBaZk8PGlQZccPeaAzqKmlLz3LdOaoD5+cexafC2yLmNQnoKwWaFKuV0 +GsoFsGAfm7yRIRwxu10XDoBiMebsJkpSLuNJkY/CPy8kufpZsT2kU2b0+/JOmIIm +qozJciP9h9hip8+lqDUOm3VoKmmW5zi4H00ghcLhAoGAD4x1nIq0Wthk5BrBmoe/ +NDEjOxIrEsRDLiQ7BsQ3MEqJYaA0h+riesLPnbh65NUGfOpaVqGQ+8+vifDC71ye +qAj3HBiTLwCa88QqRG5h9vybD8LpfoeNA1bVR0VFaFvHsn5CdenxMU2UYTROo589 +Bg0MdwY0jPPPdEjM7GgjxyM= +-----END PRIVATE KEY----- diff --git a/src/test/datascience/serverConfigFiles/remoteNoAuth.py b/src/test/datascience/serverConfigFiles/remoteNoAuth.py new file mode 100644 index 000000000000..e4dea4f52d0e --- /dev/null +++ b/src/test/datascience/serverConfigFiles/remoteNoAuth.py @@ -0,0 +1,4 @@ +# With these settings you can connect to a server with no token and no password +c.NotebookApp.token = '' +c.NotebookApp.open_browser = False +c.NotebookApp.disable_check_xsrf = True \ No newline at end of file diff --git a/src/test/datascience/serverConfigFiles/remotePassword.py b/src/test/datascience/serverConfigFiles/remotePassword.py new file mode 100644 index 000000000000..a79b7d192561 --- /dev/null +++ b/src/test/datascience/serverConfigFiles/remotePassword.py @@ -0,0 +1,6 @@ +c.NotebookApp.ip = "*" +c.NotebookApp.port = 9799 +c.NotebookApp.open_browser = False +# Python +c.NotebookApp.password = "sha1:74182e119a7b:e1b98bbba98f9ada3fd714eda9652437e80082e2" + diff --git a/src/test/datascience/serverConfigFiles/remoteToken.py b/src/test/datascience/serverConfigFiles/remoteToken.py new file mode 100644 index 000000000000..395e1e34daec --- /dev/null +++ b/src/test/datascience/serverConfigFiles/remoteToken.py @@ -0,0 +1 @@ +c.NotebookApp.open_browser = False \ No newline at end of file diff --git a/src/test/datascience/shiftEnterBanner.unit.test.ts b/src/test/datascience/shiftEnterBanner.unit.test.ts new file mode 100644 index 000000000000..37ddf3799ee9 --- /dev/null +++ b/src/test/datascience/shiftEnterBanner.unit.test.ts @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +//tslint:disable:max-func-body-length match-default-export-name no-any no-multiline-string no-trailing-whitespace +import { expect } from 'chai'; +import rewiremock from 'rewiremock'; +import * as typemoq from 'typemoq'; + +import { IApplicationShell } from '../../client/common/application/types'; +import { + IConfigurationService, + IDataScienceSettings, + IPersistentState, + IPersistentStateFactory, + IPythonSettings +} from '../../client/common/types'; +import { Telemetry } from '../../client/datascience/constants'; +import { InteractiveShiftEnterBanner, InteractiveShiftEnterStateKeys } from '../../client/datascience/shiftEnterBanner'; +import { IJupyterExecution } from '../../client/datascience/types'; +import { clearTelemetryReporter } from '../../client/telemetry'; + +suite('Interactive Shift Enter Banner', () => { + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + let appShell: typemoq.IMock<IApplicationShell>; + let jupyterExecution: typemoq.IMock<IJupyterExecution>; + let config: typemoq.IMock<IConfigurationService>; + + class Reporter { + public static eventNames: string[] = []; + public static properties: Record<string, string>[] = []; + public static measures: {}[] = []; + public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { + Reporter.eventNames.push(eventName); + Reporter.properties.push(properties!); + Reporter.measures.push(measures!); + } + } + + setup(() => { + clearTelemetryReporter(); + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + appShell = typemoq.Mock.ofType<IApplicationShell>(); + jupyterExecution = typemoq.Mock.ofType<IJupyterExecution>(); + config = typemoq.Mock.ofType<IConfigurationService>(); + rewiremock.enable(); + rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + }); + + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + Reporter.properties = []; + Reporter.eventNames = []; + Reporter.measures = []; + rewiremock.disable(); + clearTelemetryReporter(); + }); + + test('Shift Enter Banner with Jupyter available', async () => { + const shiftBanner = loadBanner(appShell, jupyterExecution, config, true, true, true, true, true, 'Yes'); + await shiftBanner.showBanner(); + + appShell.verifyAll(); + jupyterExecution.verifyAll(); + config.verifyAll(); + + expect(Reporter.eventNames).to.deep.equal([ + Telemetry.ShiftEnterBannerShown, + Telemetry.EnableInteractiveShiftEnter + ]); + }); + + test('Shift Enter Banner without Jupyter available', async () => { + const shiftBanner = loadBanner(appShell, jupyterExecution, config, true, false, false, true, false, 'Yes'); + await shiftBanner.showBanner(); + + appShell.verifyAll(); + jupyterExecution.verifyAll(); + config.verifyAll(); + + expect(Reporter.eventNames).to.deep.equal([]); + }); + + test("Shift Enter Banner don't check Jupyter when disabled", async () => { + const shiftBanner = loadBanner(appShell, jupyterExecution, config, false, false, false, false, false, 'Yes'); + await shiftBanner.showBanner(); + + appShell.verifyAll(); + jupyterExecution.verifyAll(); + config.verifyAll(); + + expect(Reporter.eventNames).to.deep.equal([]); + }); + + test('Shift Enter Banner changes setting', async () => { + const shiftBanner = loadBanner(appShell, jupyterExecution, config, false, false, false, false, true, 'Yes'); + await shiftBanner.enableInteractiveShiftEnter(); + + appShell.verifyAll(); + jupyterExecution.verifyAll(); + config.verifyAll(); + }); + + test('Shift Enter Banner say no', async () => { + const shiftBanner = loadBanner(appShell, jupyterExecution, config, true, true, true, true, true, 'No'); + await shiftBanner.showBanner(); + + appShell.verifyAll(); + jupyterExecution.verifyAll(); + config.verifyAll(); + + expect(Reporter.eventNames).to.deep.equal([ + Telemetry.ShiftEnterBannerShown, + Telemetry.DisableInteractiveShiftEnter + ]); + }); +}); + +// Create a test banner with the given settings +function loadBanner( + appShell: typemoq.IMock<IApplicationShell>, + jupyterExecution: typemoq.IMock<IJupyterExecution>, + config: typemoq.IMock<IConfigurationService>, + stateEnabled: boolean, + jupyterFound: boolean, + bannerShown: boolean, + executionCalled: boolean, + configCalled: boolean, + questionResponse: string +): InteractiveShiftEnterBanner { + // Config persist state + const persistService: typemoq.IMock<IPersistentStateFactory> = typemoq.Mock.ofType<IPersistentStateFactory>(); + const enabledState: typemoq.IMock<IPersistentState<boolean>> = typemoq.Mock.ofType<IPersistentState<boolean>>(); + enabledState.setup((a) => a.value).returns(() => stateEnabled); + persistService + .setup((a) => + a.createGlobalPersistentState( + typemoq.It.isValue(InteractiveShiftEnterStateKeys.ShowBanner), + typemoq.It.isValue(true) + ) + ) + .returns(() => { + return enabledState.object; + }); + persistService + .setup((a) => + a.createGlobalPersistentState( + typemoq.It.isValue(InteractiveShiftEnterStateKeys.ShowBanner), + typemoq.It.isValue(false) + ) + ) + .returns(() => { + return enabledState.object; + }); + + // Config settings + const pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); + const dataScienceSettings = typemoq.Mock.ofType<IDataScienceSettings>(); + dataScienceSettings.setup((d) => d.enabled).returns(() => true); + dataScienceSettings.setup((d) => d.sendSelectionToInteractiveWindow).returns(() => false); + pythonSettings.setup((p) => p.datascience).returns(() => dataScienceSettings.object); + config.setup((c) => c.getSettings(typemoq.It.isAny())).returns(() => pythonSettings.object); + + // Config Jupyter + jupyterExecution + .setup((j) => j.isNotebookSupported()) + .returns(() => { + return Promise.resolve(jupyterFound); + }) + .verifiable(executionCalled ? typemoq.Times.once() : typemoq.Times.never()); + + const yes = 'Yes'; + const no = 'No'; + + // Config AppShell + appShell + .setup((a) => a.showInformationMessage(typemoq.It.isAny(), typemoq.It.isValue(yes), typemoq.It.isValue(no))) + .returns(() => Promise.resolve(questionResponse)) + .verifiable(bannerShown ? typemoq.Times.once() : typemoq.Times.never()); + + // Config settings + config + .setup((c) => + c.updateSetting( + typemoq.It.isValue('dataScience.sendSelectionToInteractiveWindow'), + typemoq.It.isAny(), + typemoq.It.isAny(), + typemoq.It.isAny() + ) + ) + .returns(() => Promise.resolve()) + .verifiable(configCalled ? typemoq.Times.once() : typemoq.Times.never()); + + return new InteractiveShiftEnterBanner( + appShell.object, + persistService.object, + jupyterExecution.object, + config.object + ); +} diff --git a/src/test/datascience/sub/test.ipynb b/src/test/datascience/sub/test.ipynb new file mode 100644 index 000000000000..ec52bd7942ee --- /dev/null +++ b/src/test/datascience/sub/test.ipynb @@ -0,0 +1 @@ +{"cells":[{"source":"# Change directory to VSCode workspace root so that relative path loads work correctly. Turn this addition off with the DataScience.changeDirOnImportExport setting\r\n# ms-python.python added\r\nimport os\r\ntry:\r\n\tz(os.path.join(os.getcwd(), '..'))\r\n\tprint(os.getcwd())\r\nexcept:\r\n\tpass\r\n","cell_type":"code","outputs":[],"metadata":{},"execution_count":0},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[{"name":"stdout","output_type":"stream","text":"hello\n"},{"data":{"text/plain":"'d:\\\\Training\\\\SnakePython'"},"execution_count":1,"metadata":{},"output_type":"execute_result"}],"source":["print('hello')\n","\n","import os\n","os.getcwd()\n",""]},{"cell_type":"code","execution_count":2,"metadata":{},"outputs":[{"data":{"text/plain":"[<matplotlib.lines.Line2D at 0x18bf15abf28>]"},"execution_count":2,"metadata":{},"output_type":"execute_result"},{"data":{"image/png":"iVBORw0KGgoAAAANSUhEUgAAAU4AAAJCCAYAAACveS6PAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvnQurowAAIABJREFUeJzsvXuQZNd93/f59Xt6uue1M7tYALt4kCBNkJRIewlKls1UJFGkXJbAOJJNOomhlFSIKpZdZZUdUZFLSmirIsepMJWKEguWKFGWYkqi7RJsU6H5kGTJFEksRZAgQOFNAPvenWd3z/T75I97T3fvoGemH/dxzr3nU7WFnZ6e6Yuz937P731EKYXD4XA4JicT9wU4HA6HbTjhdDgcjilxwulwOBxT4oTT4XA4psQJp8PhcEyJE06Hw+GYEiecDofDMSVOOB0Oh2NKnHA6HA7HlOTivoBZWF9fV/fee2/cl+FwOBLGV77ylVtKqY2T3melcN57771cvHgx7stwOBwJQ0RemeR9zlV3OByOKXHC6XA4HFPihNPhcDimxAmnw+FwTIkTTofD4ZgSJ5wOh8MxJU44HQ6HY0qccDocDseUOOF0OByOKXHC6XA4HFPihNPhcDimxAmnw+FwTIkTTofD4ZgSJ5wOh8MxJU44HQ6HY0qccDocDseUOOF0OByOKXHC6XA4HFPihNPhcDimJBDhFJGPicgNEfnGEd8XEfk/ReQFEfm6iPz5ke89IiLP+38eCeJ6HA6HI0yCsjh/DXj/Md//fuAB/8+jwP8DICJrwM8B7wYeAn5ORFYDuiaHw+EIhUCEUyn1H4GtY97yMPDryuOLwIqInAXeB3xGKbWllNoGPsPxAuxwOByxE1WM8y7gtZGvL/mvHfW60Xzum9f5o+dvxn0ZVvL89Rr/wye/xu5BJ+5LsQ6lFD/520/y20+8dvKbHaESlXDKmNfUMa+//heIPCoiF0Xk4s2b8YnW41+7wo/9+kX+3m99jV5/7KU6jqDd7fN3P/Ekv33xEr9z0T380/Lpp6/xr//0Mv/L732TRqsb9+WkmqiE8xJwbuTru4Erx7z+OpRSjymlLiilLmxsbIR2ocfxB8/e4Cd/60lOV4vcqrf4wou3YrkOW/m/Pv8837y6x0a1yG9+6VX6buOZmG6vzz/99LNsVIts73f4jS++EvclpZqohPNx4G/52fXvAHaVUleBTwPfJyKrflLo+/zXjOOlm3V+/De+wpvvqPLv/s5fplrM8btPjtV4xxieurTLL/7Bi/y1d97F//hX/hwv32rwhRc3474sa/jXf3qZF282+EcPv42//MA6//yPXuKg3Yv7slJLUOVI/xL4E+DNInJJRH5URH5cRH7cf8ungJeAF4B/Dvz3AEqpLeAfAU/4fz7iv2Ycv//sTZqdPr/03/wFNqpF3ve2O/j/vnGNZsfdvJPwD3/3G6xXCvzcD7yV73/bWVbLeX7zS85qmoRmp8dHP/sc335uhfe99Qx/57sf4Fa9zf/75VfjvrTUElRW/UNKqbNKqbxS6m6l1K8opf6ZUuqf+d9XSqm/rZR6g1Lq7UqpiyM/+zGl1Bv9P78axPWEwVOXdji7XOLu1TIAD7/jTuqtLr//ZzdivjLzabS6fP3SDn/zoXtYLucp5bP88IVz/IdnrnN9rxn35RnPv/7Ty1zdbfJT73szIsJD963xHfev8Ut/+CKtrtu448B1Dk3I1y/v8ra7lgdff+f9p1ivFJ27PgF/dq2GUvDWO5cGr/3Nh87T6ys+8WWXJDqJr766zXqlwF984/rgtR/5i/dyo9biqUu7MV5ZenHCOQG1ZoeXbzX4thHhzGUz/NVvO8vn/+yGK605gWeu7gHw4Ihw3ru+yLvuXeXzzzqL/SSeu17jTWeqt7321juX/e/V47ik1OOEcwKevrKHUvC2u5dve/0Hvv0s7V6f//SCy64fxzNX9lgp5zm7XLrt9becXeLFG3WUctn1o+j3Fc9dr/PmO24XzrtWFigXsjx3vRbTlaUbJ5wToN2ht991u3C+5axnQb100+36x/HM1T3ecscSIreX7b7xdIV6q8v1vVZMV2Y+r27tc9Dp8ecOCWcmIzxwpuqEMyaccE7AU5d3uXO5xHqleNvr5UKOs8slXrrZiOnKzKfb6/NnV/duc9M1b9yoAPDCDbfxHMWzvjAedtUB3nS64lz1mHDCOQFPXd7l7YfcdM1964u8dMsJ51F8a7NBq9vnwbNjhPO0Fk5nNR3Fs9eOFs4331HlVr3FVqMd9WWlHiecJ7DnJ4YOu+ma+zcWeemmi9MdxdNXXp8Y0mxUi1RLOV50FvuRPHutxvm1MovF3Ou+94Avps5djx4nnCfwjct+fPPulbHfv2+9wl6z63b9I/jm1RqFbIY3+G75KCLCGzYqzlU/hmfHZNQ1b3bCGRtOOE/gqMSQ5v6NRQDnrh/BM1f3eOBMhUJu/K32xtMVXnDJtbG0uj1evtV4XWJIc2bJs9idcEaPE84TeOryLnetLLC2WBj7/fvXPeF82bmbY3nmyt6g+mAcbzxd4Wat5Wphx/DijQa9vnpdKZJGRHjTmSrPXXMbT9Q44TyBp6/s8ba7jn7w714tk88KL95yN+9hbtSa3Kq3xiaGNC6zfjTPXvfiw0cJJ3hJo+du1FyMPWKccB5Dv6+4vH3Avb5VOY5sRrjn1KKzOMfwzDGJIY3OrL/ohPN1/Nm1GvmscN8x99+bzlTY2e9ws+ZqYaPECecx3Gq0aPf63LWycOz77nclSWPRsbe33HG0cJ5bK1PIZVyccwzPXavxho0K+ezRj+kwQeTWL0qccB7DlR1vcs/Z5ROEc6PCK5sNNxH+EFd2mlSLOZbL+SPfk80I968vOotzDM9eqx2ZGNLokqRnXYIoUpxwHsPVnQMA7lwpHfu++9cX6fQUl7b3o7gsa7i6e8DZE9YO8EqSnMV5G/vtLld2mwNhPIr1SoG1xQLPO+GMFCecx3DZF84TXXVXkjSWq7tN7jjBWgd4w+kKr23tu6HQI1zb9bydkzZtrxbWhYoO89SlXf74+fCG7zjhPIYrO00W8lmWF452NYFB8N71rN/O1d0mZ5dOtjjfeLpCX8HL7uEfoIXzzATrd2ap5AZCH+Ljf/It/v7vfC203++E8xiu7h5w50rpdVN9DrO2WGB5Ie+mJI3Q7va5VW9N5KrrWthXNp1waq7tTRZfB7hjqcS13aYrSRrh+l6TM8sn33uz4oTzGK7sHHDnCW46eO7SfeuLzmIa4fpeE6V43QzOcdzhv8eNlxty1bc475jA4rxjuUSr23dNBCNc32typlo8+Y0z4oTzGK7sNrlzgh0fPKvplU2XHNLoB38Si2mtXCCXEedujnB9r8nyQp6FQvbE92p33m08Q67vtQYbchg44TyCVrfHzVprIosT4MxyiRs15y5pru56ibVJLM5MRjhdLboHf4Rru82JrE0YWuzX3MYDeKeC7h50JooPz4oTziOYNKupOV0t0ukptveduwQjFueEG8/pJW/jcXhcmyJGpwX2+q5bP2DguZx2rnr06OL3SS3O01XtLrmbF7yNp1rMURkzR3IcZ5aKbu1GuDZhRQLA6SVPIJzF6aE9F+eqx8CVQfH7pBaTd/PecD3DwOTF7xqvpMatHUCn1+dmvTWxxVnMZVlbLDjh9NHr4Fz1GNDCOUmMDoZuwQ138wKTF79rziyV2D3ouCJ44GatNXFFgubMUsm56j43nHDGx5XdJqcWC5TyJ2c1YeiqO4vT4+pukzunePCHG49bP20xTZoc8t5bdBanz/W9JqV8hqXSZGGiWXDCeQST1nBqFgpZqsWcG+/FsPh9mhjToKTGJYim6hrS3LHsuoc01/ZanFk6uXFlHpxwHoHuGpqG00tFlxlmWPw+aQ0sjNYiuvW7NqiBnW7juVVv0+72w7osa7i+1wzVTQcnnGNRyhtgPEnx9iinqyXnajLS9TLVg++56i5B5D34hVyGlWPG8R1Gu/Vu4/ZinE44Y2Cv2aXR7p04Fekwp5eKztVkWPw+jcW+vJCnkMu45Br+cJTl6VzNM8vOYgfP6LkWcrslOOEcy6DrZVpXvVrkxl4r9d1DQ4tz8o1HRFwtp8+1GSwmbXFe2023xb7X7NLs9EOt4QQnnGOZtoZTc7rqDVvYa3bDuCxrmLb4XXOm6mo5Ybp2S81AOFO+8WiP5bRz1aNn0DU0bYzTj9PdTLm7fmVnuuJ3zZmlUupDHdrVnCYxBLBS9kIdabfYZynlmgUnnGPQJUXrlfFnqR/FhqtFBLybdxo3XXN6qZj6tdvZ79Du9qd21UVkMJczzWiPRScbw8IJ5xi2Gm1Wynlyx5wuOA5XBO8xbfG75sxSiXqrS72V3lDHLBUJmjuWSql31a9H0DUETjjHstlocWpxOmsTRktq0nvzdnpe8fssN65evzRn1vW9M4twnlkupXrtYDjHdNKOv1lxwjmGzXqbU4vTm/qVYo6FfDbVFud2o41SsD5DOciZwYSp9K7fNJPfD6PbLtNc1eEVv4frpoMTzrFsNdqszWBxiojfPZTeB3+z0QaYyWI/7Yq4ubbXJCPDePk0nFkq0ez02TtIb6hDt1uGjRPOMWw22pyaMjGk8Wo50/vgb/vCuVp2oY5ZuLHX5FSlSH7K+DoM43ppjnNG0TUETjhfR6+v2N5vz2QxgZcgSvOgj4HFOcPGUynmKBeyqXbVtxqz33tpP0Kj31fcqLWcqx4HO/tejO5UZbbF36im21Xf8oVz1lBH2s8I395vz2StwzA8oq3+tHGr0aLXV6HXcIITztexOceDD14tYr3VpZHSkprNRhuR2Vx1GLatppXNRpu1GcNE+p7dSqlwak9vlvjwtDjhPMRmffbkBgwzw2m1OrcaLVYW8mQzs81CPJ3y7qHtRpu1GTedpVKejHhWaxrZbngHJa7NUBEzLU44D7HZ8ARvVlf9dMprEWetSNCcWiyk1tXs9RU7Bx1WZ1y/TEZYLRdSa3Fu7WtvcfJxfLPihPMQ88TowHUPzSucq+UCe80unV76BvIO4uvzrN9iIcUW5+wVHdPihPMQm3Udo5tt1xqcneOEcya0tbCTwvPpteDNanGCt+HrcFPa2PLj68sLzuKMnE0/Rjdtn7pmecGLM+2kdNf3hHP2GNOKby2kcf22dIxuDotprZxii3O/zfIcz+40OOE8xLwWUyYjLC/kU3nz9vuK7f3OXK5mmjPDW358fa5Qx2JhIMBpY3u/M9emMw1OOA9xq96eOTGkWS0XBhm+NLF70KHXV3PHOCGdmeGBxTlnqGN7v53KfvVtf6pZFDjhPMQ8nRuatAbo5+ka0qz6Mc7tVMc4Z3/4V8sFen2VylMI5vUWp8EJ5yGCWPzVcj7dD/4c7pL+2TS66pv1NpVijmJu9pFoaynuHpqn62panHCOMOhTn9NVXykXUpnc0NnceTaeUj5LuZBN74M/Zw2izshvpez+U0o5izMutgOoowPP4kyjxbQVgKsOntWZtgcffG9nTotJ/3zaNp6DTo9Wtz9XKdc0BCKcIvJ+EXlWRF4QkQ+P+f5HReRJ/89zIrIz8r3eyPceD+J6ZmXQbjnvg79YoNXtc9DuBXFZ1hBEVhi8GF8a6ziDsJj0z2+mTDgHjSsRuerTnd86BhHJAr8IvBe4BDwhIo8rpZ7R71FK/b2R9/8d4J0jv+JAKfWOea8jCDaDevBHMsMLhekPLbOVzcb8MTogtW2DW402D5ypzPU70hrj1FUsNlmcDwEvKKVeUkq1gU8ADx/z/g8B/zKAzw2cgas555AA3XWUtsx6UDGmtZRWJWzvz++qlwtZCrlM6kIdUfapQzDCeRfw2sjXl/zXXoeI3APcB3x+5OWSiFwUkS+KyAcCuJ6ZCcpVXxnEmdLlbm412oHs+F4dbLoe/Ganx367N/f6iYjXPZSy9dPJ2BVbXHVg3Pywo6pvPwh8Uik1Gvw7r5S6IiL3A58XkaeUUi++7kNEHgUeBTh//vy81zyWeWdJagbuUtp2/UY7kGMLRgd9zHKEhI0MvZ0ANp4Udg9FHeMM4q68BJwb+fpu4MoR7/0gh9x0pdQV/78vAX/A7fHP0fc9ppS6oJS6sLGxMe81j2XeWZIa3b2QtpKk4Fz19A360A9+EBa77h5KE9uNNhmBpQgGfEAwwvkE8ICI3CciBTxxfF12XETeDKwCfzLy2qqIFP2/rwPfBTxz+GejYjOAdkuAlQVtcabnwVdKeYfcBfDgp3HQx7zjDEdJY6hja7/NSrkwt9EzKXO76kqproj8BPBpIAt8TCn1tIh8BLiolNIi+iHgE+r2Jtq3AL8kIn08Ef+F0Wx81GwGZDEVchkqxVyqdv1Gu0e72w8sOQTp6h7a3g9OONcW01cHu93ozDwKchaCiHGilPoU8KlDr/3soa//pzE/9wXg7UFcQxBs1lu8+Y5qIL9rdTGfql1/K4CuIU0aB30EGaNbLRfY2e/Q7fUjGbFmAlF2DYHrHLqNIBd/tVxIlaseVA0sDIdcpCnBseXH6IIYwqurQnYO0rN+UfapgxPOAfq8l6CycmnrVw/S1UyrxblaLpAJIEa3msK2S2dxxkSt2UGp4OrA0jYhaXg66PzJtTQO+vAGfARz76UtRqyUN5wnqhpOcMI5QJe+BHVeSdoym4MY3ZzNA5q0DfrYrM/fNaRJm8XeaPfo9FRkXUPghHPA7kHwwllrpee0xq1Gm0Iuw2Jhvj51TdoGfWzvB+dqDi3OdKxflKdbapxw+gyEM6CShtWUFXF7wfk8IsHU0aVt0MdWY/bz1A+zkrJZCUHWwE6KE04fLZwrAVmcaSvi3j3oBHos62qKTmvs+wO0g3I1S/ksi4Vsao4J1iGdqCYjgRPOATsBu+o6XpUWqylo4VxbTE+MuNbs+ofczZ9Y06Tp3KvtiPvUwQnngD1fOIPqdR26S+lw1XcPuoFbnHrQR9IZWEwBdr6cWkxPqCPIPv9JccLps3vQoZTPUMoHldxIl6u+d9AJdMBCmgZ9DEeiBbjxpMni3G+TzQhLpUAaISfCCafPzn47YIspbRZnsK76SopKavRRvoGu30J6JiRt+X3qQSUmJ8EJp0/QD/5CPksxl0nFzdvt9am3gnXV03QERNClcPp37R2k42z17Ua07ZbghHPA7kFnMA4uCEQkNUXwYVhMaSri1sK5VApYOJsd+v2jZoonh6j71MEJ54Ddg27gQ1BXUtJ2GYbFlKZBH0EnJvXvUgpqreRbnXvN4J/dk3DC6bMbcIwT9Hiv9FhMwcboCrf97iSzd9ChmAsuMQlDEd5LyfotLUSXGAInnAOCjnFCek5rDEM4S/kM+ayw10z+g78bcEUCDP8t0rLxBP3snoQTTqDT69No9wItBwHnqs+DiLC8kE/Fgx/Gpq3jpUnfeHp9Ra3VDTQ+PAlOOAnnwYehq570AH1Y67dUyqfD1WwGL5zLKXHVa81w7r2TcMJJeA/+8kKevoJGO9kB+jCSGwDVhfwgY59kQrE4/Zhf0i323ZDuvZNwwknwk5E0+uZN+sO/G0JyA0iVqx5018vQ4kz2vaf//5zFGQNhupqQfHdpdz+c4PxSKUct4WsH4axfpZgjI8m3OHUMN8p2S3DCCXg3LoQgnCmJM4XhaoK3fkl/8Pt+ciPo9RORVKyfc9VjJOhZnJqBu5QCVz0M4dTdL0olN7lWa3VRKpwHX69fktkLyVs8CSechLdraVc9Dbt+OK56nk5P0ewkd7RcWIk18NYvDfceOIszFnb2OywWsuSzwS7HIDmUgps3HFc9+ZnhsOLr+ncm/d7ba3bIZiSws64mxQkn4T34laLOqif85g2h8wVGQx3JXb8wXc00VCXoioQoR8qBE07AF84QpqvkshkqxVyiS0J6ISU3IB1VCWFMRtIsLeTYTfC9B145UtTxTXDCCcDuQZvlkIYELJVyzmKaEW3FJtlqCquGGLz1S/K9B+H0+U+CE07Cc9XBv3nT8OA7V30m9kJsGVxeyNPu9ml2eoH/blMIo111EpxwEvwQ41GWSsne9cMUTl3UnORQx+5BeMmNtIQ6oh7wAU44AR3jDMviTHaMM2xXc/QzkkiYyY00jJbbO+hGPosTnHDS7PRodvrhuerO4pyZfDZDuZBNtMUUZnIjDRvPXtPFOGMh7M6DpLe9hSmckI6NJ6y1S3qMuNnp0e72naseB6E/+At56q1uYmdyhr9+ucRvPGFZTEl31eNqtwQnnOyEbjHlEn1o1t5Bh0III+U0ST/mNqzmAUh+ci2udktwwjmYjBT0sRmapE9ICtPVhOS76mGW0yQ9xhlmKddJOOGMIEYHyY0zhS6cCY4RK6VCXT+dXEvq+g27rlxWPXKiiNFBst2lMIUzyYMqDjo9Oj0VanIjyesX1/R3cMI5iHFWQ7p5ncU5H0ulHLWEJteiePCTPOjDxThjZO+gQ7WUI5sJZ7pK0k8bjMJVT2pyLWxvB5IdIx7MMnXlSNGz1wy3ZWtocSbvwYdohBOSufFEIpwL+cROSNo96LCQz1LIRS9jqRfOWrNLNcTgcqWU3GG8vb6i1uyG6ioleYr+0NUM7/5LdIyz2Yml3RKccFJrdkIVzmxGqBZzibx5axGUgwyPWE7e+kVRwO3NSkje2kF8szjBCadvcYa7+EmdixhVcmP0s5JEFK768kKeWqtLL4HJtbgmI4ETTs/VDLkObCmh3S96MwjTYk/yaLTdkCs6INnrF9csTnDC6bvqIVucCZ0CX/MTXqEKZ4IHVewedKgWw6vogGQP+ohr+jukXDiVUqEnhyC5U+B1jDNMd6lazCGSXIsp7Ac/yYM+9vxZpnGQauFsdvp0+yoCizM/sM6SRBQWZ8ZPriX2wQ9ZOJcSGiPuh3hI4CSkWjhrEcToILmZzeH6RZFcS9aDD15tb9gWU1Itzlqri1LxdA1ByoVzLwKLCXyLM4GZTW1x6vPjwyKptYjRVHQks4540DXkhDN6oojRwfAft5awAH2t1aWYy4TeubFUSma/da0ZfoxOC3O9laz1C/M8+klIuXBGZXEmc0JSFBYT+KGOhG06APVWd9BZFhaLhSwZIXEx9jinv4MTTiCaGB0kryQkCosJkjkFPqqKDhGhUswlTzgjMnqOIhDhFJH3i8izIvKCiHx4zPd/RERuisiT/p8fG/neIyLyvP/nkSCuZ1IiSw4ltAg5igcfkumqH3R69CKo6ADPMEiacEYVZjuKue96EckCvwi8F7gEPCEijyulnjn01t9SSv3EoZ9dA34OuAAo4Cv+z27Pe12TEJWrntQi5CiaB8B78L2hv33y2WQ4SfWIEmvg3d9Ji6/XW/ZbnA8BLyilXlJKtYFPAA9P+LPvAz6jlNryxfIzwPsDuKaJqDU7iMBiIfxyJEhqjDOaBx+GYpMEonQ1PeFMztrBSEWHxcJ5F/DayNeX/NcO81+KyNdF5JMicm7Knw2FvWaXSjFHJsSWN0hyjDMa4dQPRz1Bw4yjdDUrxVyi1g689SvlM7F5IEF86jjVOVyw+G+Be5VS3wZ8Fvj4FD/rvVHkURG5KCIXb968OfPFjuIN+Ijgxi0ks22w1uxQKUaQVddVCQnaeKIKE3mfkU+kqx7FvXcUQQjnJeDcyNd3A1dG36CU2lRKtfwv/znwFyb92ZHf8ZhS6oJS6sLGxkYAlx3+LE5NJuNlNpOU4Oj1FY12LxqL039AkuSqawswClczia56FF1XxxGEcD4BPCAi94lIAfgg8PjoG0Tk7MiXPwh80//7p4HvE5FVEVkFvs9/LRKicjVh2D2UFKIMzuvPSNLDH1W7KnjinKR7D7xNNK74JgSQVVdKdUXkJ/AELwt8TCn1tIh8BLiolHoc+Lsi8oNAF9gCfsT/2S0R+Ud44gvwEaXU1rzXNCm1VofT1VIkn1Up5hJlMUUao0tkjDO6jWeplKfd7dPq9ijmsqF/XhRE5S0eRSCfrJT6FPCpQ6/97Mjffxr46SN+9mPAx4K4jmmpNbu8YSOaxa+WkhWgjzZGpy3O5IQ69PqFXdEBt1clFCvJEM56qxuZ0TOOZBTFzUiUrnolYXGmqLquAKp+jDNJ7mbNr+gIc4ixRteKJu3+i9NVT61wei1v0RRwQ/JKQqLqugIo5TPkMpKwBz86V1Pf40lav3qERs84UiucrW6fTk9FevMm6caNsgBZRKiUkhUjrrci9Ha0xZmQCUn9vqLe7lKNoOvqKFIrnHsRZjW9z0lW21st4pa3xK2f76pHQdKqEuptb4hxVM/uOFIrnPomiqoWrFrM0er2aXf7kXxe2EQ9ZKFSzCcu1BHVg7+UMFe9HqG3cxSpF84ok0OQnJKaWrNLPisUQx5irKmWcok6PiPqxCRAPSEWe9TP7jhSLJzRuuraLUtKnE5bTCLhZ4XBs9iTsnbghTqiDHNAgixOP1YbVahjHCkWzqhjdLqkJjm7fpQ7frWUS8zaQbSuej6boZTPJKacay/CUrijSLFwRp8c8j43GTdv1MKZpKx6p9en2elHmhWuFJNT1VGPOD8xjtQKp56NGXVJSFIefm8yUpQWp/fgK2X/SaFxJDeWElSVEPcsTkixcOohxpUIWt5gpO0tIe5SVAe1aSrFHN2+opWAqoQou640SWr51TFO56rHwF6zS6UQ/hBjTWXgqidn14/SVU/STM69CLuuNElq+a01u4hAOR9f331qhTP6Bz9Z/dbeCZcRWpwJOj4jjnKaajE5w4x180BURs84Uiyc0WU1AYo5r986CQ++UirSlkEYGfSRgPUbzDKNcIJ5NUHJtVoz3nZLSLVwRvvg637rJDz4jXaPvore1YRkxIijHJCiScq9B9EbPeNIr3C2oh+EmpQAfdSlXN5nJSdGHIurXspTb3fp9xNQldCKd6QcpFk4I84Kg66lS86DH2k5UgJd9Sgf/moxh1LQaNu/flF7i+NIuXBGbHEWk+EuxWMxJaeBYK/ZoZDLRHqMRZLWz4uvO1c9cvQQY33eeVQ4V312khXjjP6ExiQNM466+WIcqRTOqIcYa5ISoI96JB+M9FsnINRRjyNMNNh47F+/ODaew6RSOKMeYqxJyvEZcXS+QHJmcsZhMWkjwfbRfN5pnX1nccZr+WqiAAAgAElEQVSBrmeLuhasWsonopYujnIa8Cxc2x98iCe+vpSQGGdc995h0imcreizwuD9Y7d7fZqdXqSfGzS1ZpdsRigXom15S8qEpDiEs+JXJdi+fsOKBJccipw4ykEgOYM+tKsZ1RBjTVLOHaq3ugMhi4qk1MGaMP0d0iqcMdQhjn6e7bt+rRXdQWOjJCVGvBfh0cCaciFLRpKwaccTZjtMOoUz4hMaNYNjWm0XzpgKkJNwxHK/7/X5R50VFhEqCagjjqMUbhypFs7FGJJDYP/xGfVmN9LJSJpKAs4d2u/0UCqeIbzVUt76sXxxGT2HSbVwxlUSYvuuX291WSxGPwtxqZSzvt86TospCROSTJj+DmkVzoiPttUkJcbZaHVjyWpWSvb3W8eZ3KgmoAHDWZwxUveTG3FkhfXn20xcyaEktA3G1TygP9P2MNFes0MhG22f/zjSKZzNbuTxTUjO8Rn1ZpdKDK76wGK3eOPR//axVSVYvOmAGZORIKXCGZfFVMxlKWTtPt+611ccdHqR1yFCMmoR43Q1E+GqN+OfxQkpFc5GxMc+jGJ7gD6u5gFIRnKtEVNiEpJRzlWLoQZ2HKkUznpMFifYPyFpWJEQvauepBhnLKGiYpZ2r0+ra2/Lb73VjfSspqNIp3A248kKg/0zOYcWUzx1nGB3jLPR8kQrrhjn6DXYSM256vHhxTjjycrZHqCPs44uGTHODgv5LNkYjrbVxkLD4o3HhBMuIaXC2YjTVS/a3b0Rp6u+WMghYncdbJwHjel/M5tDHSYc1AYpFM5eX7HfjicrDH73i8U7/nBASvTrl8kIlYLdMznrrV5sFpP+N7O1gUApFavRM0rqhDPOrLD+XJt3/EbM67dYzFntatabnVgSQ8CgTdZWi73V7dPtq9jWb5T0CmecMc5WF6Xs7LeuxVhOA55g22oxgZeYiWvtbO9cM6XdEtIonDG6muCV1PT6imanH8vnz4tev8WIp79rFi0fjVaLMUa3aHlVwvDec8IZOSa46mBvZrjR7rKQz5LLxnPrVG131VvxHW1r+5CZuJ/dUdIrnHG5S3qYsaUPf9x1dIvFrLUWE8TrqmtLzdb1i/vZHSV9whnzmSWLgyJke2/eOG/cSjFvdQF3XANmwKtKWCzYu/HE2a56mPQJpz9WK66b13Z3Ke5ykEoxa22Yo9Xt0e71Y01uVEr2hjriOrlhHCkUzvha3kY/19Zd3xspF/OD3+5ZWZUQZ7ulZrGYszZM5LLqMRLXCZcaHR+0taSm1orP1QTvwbe1KqEe44APTdXill8T1k+TPuFsdSgX4ukVBvuLkOMcyQfD5JqNFrsJyQ2bXfVGq4sIlPPxTn+HVApnvBaTHolVtzTBEXdyyOZaRBOEc7Fgb8tvrdVlsZAjE5PRM0rqhDPu6SqlfIaMDJNUthFnVhhGR6PZ9/Drf/M4y7kqFs9KiDsxOUrqhLMR83QVEaFSzFlZUmNEVljXwVoY6og7Mak/21bhNGUyEqRQOON2NcG7eW188E3ICg+SaxY+/HEnJvVnNyydlVBv9YxIDEEKhbMWczkN2BugN+HBtznGGfdkKfDWr9NTtLo2ViV0jBhiDAEJp4i8X0SeFZEXROTDY77/kyLyjIh8XUQ+JyL3jHyvJyJP+n8eD+J6jsMEi3PRUnepFnPzANidVa8ZkBW2eUJSo9UbVKXEzdzCKSJZ4BeB7wceBD4kIg8eettXgQtKqW8DPgn8ryPfO1BKvcP/84PzXs9JxB3jBHvjTNpVjzPGabPFWW/GnxW2O7nWjW2q2WGCsDgfAl5QSr2klGoDnwAeHn2DUur3lVL7/pdfBO4O4HOnRillhMVpq3AOssIxrl+5kEXEzgffhKzwotXJtfjOCjtMEMJ5F/DayNeX/NeO4keB3xv5uiQiF0XkiyLygaN+SEQe9d938ebNmzNdaKvbp9NTsVuctk4xj/NoW42Id3yGrQ9+3K5m1VKLc2D0GJJVD+IqxvkdY1N2IvJfAxeA/2zk5fNKqSsicj/weRF5Sin14ut+oVKPAY8BXLhwYaaUoAkFyPrzbbQ4TXDVweLkWiu+Y6k1toY6Wt0+PUOOzYBgLM5LwLmRr+8Grhx+k4h8L/AzwA8qpVr6daXUFf+/LwF/ALwzgGsaiwlZYf35NpaEmOCqg73JtXor/qNtK5Ymh7SHEff6aYIQzieAB0TkPhEpAB8EbsuOi8g7gV/CE80bI6+vikjR//s68F3AMwFc01iMsThLOfoKDjp2FcHXm35WOKZjMzS2Wuxe15UZrrpt62fSSDkIwFVXSnVF5CeATwNZ4GNKqadF5CPARaXU48A/BSrA74gIwKt+Bv0twC+JSB9PxH9BKRW+cBoQ4wTvQSobcH7KpNRbPSqFHP6/YWxULW0bNCErPHrv2YRJQ4whmBgnSqlPAZ869NrPjvz9e4/4uS8Abw/iGiZhMP095pt3dNc/HeuVTEe91Yl90wFvUMX1vWbclzE1JmSFba1K0K66CfcfpKxzaGjux3vz2hqgj3uylKZSsm+mpClZ4UFVgmX3nmkWZyqFM+6b19Yp8Ca0q4KdMU6dFY7bVQc7qxJMyU9oUimccbvqtp47FPcQY40WTpuqEgaupgEF3DZuPE44Y6Te7JLNCKV8vP/bOlRg2/EZJnRdgRfq6CusOj7DhAEfmkULp3OZ4i1q0iWcrS6LhWzsWeFhLZ195UimxDhhOHTEBoYWU/yuetVCV73R6pIRWDDg2AxImXDWml2qMXdugL2uuikWp3Z3bRoGPWxXjf/Bt/H4jJq/acdt9GhSJZwNA3qFwds1M5aVhOissBkxTv/cJos2noYh8XXQySF7Nh0wY0DKKKkSTlMsJhGxrm3woNOjr8zo3BicFGrR+plSCgf6BAJ7whxgzrOrSZ1wmvDgg3++tYUPvgk37/CkUAvXzwiLPUej3bOqKsGkZxdSJpwmmfuLRbuKuAddVwY8+IOqBAuF0xRXvddXVlUlmBIm0qRKOE0y9yulnFXlSANX04De+mFW3aL1a3pZ4bhL4WBkmLFFVQkmGT2QQuE0xdy37aRLk6bT2Hj8g960TcgKD4cZ25MgMqUUTpMa4VRKGbVrLRbsqqUzyVXXVQlWhTpMuvcsLIczaf0gRcLZ7PSNyQqDP6jCIuHUYQUT1s/GqoR6M/4BHxrbZiWYclbYKKkRzmFWOP5yELCvX1h3OZlQTgP2VSU02uY8+LYdEayNHlM2HkijcBqy+LYdn2HKLFONbQfe1QyK0Q3HGtqRHNJJLFPWD1IknA2DssIwHFRhy/EZulfYhKwwWBjqMMjVHLrqttx7/iGBhqwfpEg4TSrgBvsOzdIVCSZkhcHGUIeBwmlJcqjeNCe+rkmNcDYMKqeBYazVlpvXJIsJhqEOWzCpFK6Uz5DNiDWuumlGD6RIOE2qQ4ThoApbaukabXMefLCr88q0UjgRYbGQtebec8IZI4OWN0OSQzo7bUv3Rr3VM+rGtclVN60UDuxaP5OGQGtSI5ymuepVyyzOerNjpHDaUJVgWkUH2FWVUBs8u2aUwkGKhFNnEMuGTJAejkazw+JstHpG3biVkj3HZ5hWQwxY1UBg0ixTTWqEs+Efm5HJmJMVBntKQkxKboBdgypMK4UDu47PMGlAisacKwkZ04YEDMqRLElwmNT5AnYdn2FicsOm4zNMK4WDNAln25xeYbDr+AzTssJg1/EZpsXXQcc4zd90wLxSOEiRcJq2+DYNqmh1+3R6yrAH357jM0xMDlWKWSvWDswrhYOUCadJMSawpySkYaCradNMTiNddYtmJdRbPSeccWHi4lcsKeJuDCYjmbN++lpsmKJvqqve7StaXfOrEjxv0ZyKBEiVcHaMW/zFoh3HZ5hYTmPTTEnTSuFg2AhihcXeNCvMBikSzkarZ1SMCSxy1dtaOM2po1u0yFU3rRQOhqVRNtx/ppXCQYqE08TFXyxmrXjwh9NpzLGYyvksInbUwTaMvPfsEU7TSuEgJcLZ7vZpd/tUDEsO2VISYmJyI5MRrxbRghhxvWVWKRyMJtfMv/9M3HhSIZwmBufBIlfd0PWzxmI3rBQO7DmbvtXt0ekp49YvFcJposUE9hyfYdpIPs1iMUfdguSaqaVwYL6rPqjoKJgTJoKUCOcguWGYu2RLSYi+eU3deEzHyFI4S7Lqw5Fy5iQmIS3CaajFZMuuX291WMhnyRqUFQZ7zqY3sQ7RluRQrWleKRykRDjrA4vJrMW3paTGRIsJ9IFtLrkxC7aUI2lv0bT1S4dwGnjYE4ycO2T6zWugxQQ6uWb+WDkTs+rZjLCQNz+5Zmp8PRXCaeI8RBi1OM22mky0mEBn1c1eu06vT8vAUjjQw4zNXj8T5yRASoTTtPOGNPa46qYKp/nlXKbG18HzeEy/90xdv1QIp6mLX7UkQF9vdQfXahKVQo52t0+nZ25VgqmlcODFiE0XzrqhFR2pEM56q0shlyGfNet/1xaL01xX3fz1M3GylGaxkBschGYqg/yEq+OMHhM7N8CekhBjs+oWrN8wuWHWgw921ME22l1K+Qw5w4wes64mJDyLybwbV++iJj/4YHBWvWR+cq1haHwd7Dgi2FSjJxXCWW/1jBqJpsllM5TyGaNv3l5fcdAx0+IcWuzmliSZWk4D9mTVTVy7VAinqRYT6FpEc2/e4SxO827eYR2sues3EE4Dy5FsyaqbuHapEE5Ty2nA/DhTvWmucNqRHDJ3/SrFPAedHr2+uUNmTGwegJQIp6nmPpgfZzK1lAvsaBs0ev0s6FxzMc4Yqbe6RnZugPlF3EbXIVpgcdZbPQrZDIWceY+aDevXMLSiw7x/zRBoGGrug++qGzxT0ug6RCsefHPvPRvWr25ofiLxwtnvKxptM3ct8C1Og49/MNniLOQ8S87kIu66oaVwYEcdrEsOxcQwK2zqzZu1IitsonCCBck1Qx98MH/ITL+v2DfU6AlEOEXk/SLyrIi8ICIfHvP9ooj8lv/9L4nIvSPf+2n/9WdF5H1BXM8oJruaYP4w3mFyw8yNx/QJSQ1DkxtgvsVpcinc3MIpIlngF4HvBx4EPiQiDx56248C20qpNwIfBf6J/7MPAh8E3gq8H/i//d8XGMZbTKWc0SUhJhdwg7fxmPrgg9kVHaYL5+DZNTBGHITF+RDwglLqJaVUG/gE8PCh9zwMfNz/+yeB7xER8V//hFKqpZR6GXjB/32BYXIdHYxkNg1NEDVaXXIZoWhgVhjscNVNfPDB/JMuTS7lCuJpuAt4beTrS/5rY9+jlOoCu8CpCX8WABF5VEQuisjFmzdvTnxxpo7e15ie2dRZYW+fMw/T62BNL4UDky1OM4+8gWCEc9wTddjvPOo9k/ys96JSjymlLiilLmxsbEx8cX/xDes8//Pfz7vuXZv4Z6LEdOGsGZzcAM+NMzmrbmodIkAxlyGXEWPvPVNPboBghPMScG7k67uBK0e9R0RywDKwNeHPzk0+mzHuhEaN6f3WJic3wBtmbOqDr5Si0TazDhFARIy22E2OrwchnE8AD4jIfSJSwEv2PH7oPY8Dj/h//yHg80op5b/+QT/rfh/wAPDlAK7JGgZtg4bWcnoWk5kPPmhX3cxNZ7/dQykzH3yNyUNmTM5PzH1FSqmuiPwE8GkgC3xMKfW0iHwEuKiUehz4FeBfiMgLeJbmB/2ffVpEfht4BugCf1spZea/YkiYH2fqGjlLUlMpZmm0uyiljIvDmpzc0Jh8UmjD4Kx6IFeklPoU8KlDr/3syN+bwA8f8bM/D/x8ENdhI9WS2THORqvL2eVS3JdxJIvFHEphZKG0qYcEjmJyHWzNYIvTzBqTFLFoeDmSqdNpNCYn10yexakxechMo9Ula2gpnHlXlDJsKEI2zZIbRVtzJq6fyckNjcl1sI1Wj8VC1rgQDDjhjJ1izsv4m3jzKqWMz6qbPJOzYejRtqOYnlU3de2ccMaMiLBYMDPO1Oz06RueFTY5uWZ6nz/o5JB5awdmt6s64TSASjFHzcBypJqfbTUxq6kZDuM1b+MxfU4CDIXTqw40C5PbVZ1wGkClZKa71DC45U1jcr+1yeU0msVijr7yvAvTcK6641gWDZ0Cb3LLm8bk5Fq91SUjsJA3d+OpGHzukKlDjMEJpxGYGmeywtU0uA5WDzE2MSusMbmcy+Q+fyecBmDqMOPB0cAGu5oL+SwZMddiMnntwHyL3dQwkRNOAzC139r0kXygqxLMtdhNXjswVzh1KZyp6+eE0wC8c4fMunHBDlcdzK1FrBvsampMddVb3T7dvjLWYnfCaQD6wTetJMSGIRVgbr91w2BXU1MxtPPK9E3bCacBVEo5un1Fq2tWSUi92UUEygZnhQEqpbxxDz6YP8sUzHXVTa/ocMJpAKbevPVWj8VCjoyhQ6A1poY6ak1zY3QaU111kw9qAyecRqB3VdNuXi84b7a1CeZWJXjT38188DXlfBYR804gGFR0GLp+TjgNwNR+63rbfIsJPKvEtLWzYUAKQCbjVyUY1vJrekWHE04DMLXfut40/8EHMxsIWt0+nZ4y9sEfxUuumbV+dcMnSznhNABTu19ssJjAzHIkk8/LOcxiMUfdsJZf56o7TkSXrJh2zK0NBdzgPVydnqLVNcdi196DDetXLRroqhs+ks8JpwGYmtm0IbkBI1UJBj38ptchjmKixW76sSNOOA2gYqhw1puWZNUNjBHbJpymxYi9ASlZY0vhnHAagN5VTRtm3Gj1qBTzcV/GiZhYB2u6qzlK1cCxhib3qYMTTiPwSkLMymy2u33avb7xLYNgpnDacDSwZtHAGKfJ09/BCacxmOYu2dKnDmZOgbfhhEuNidO5TJ7+Dk44jcG0WkSbHnwTjwi2aeOplnK0e33DqhLMnf4OTjiNwbRzh2xLboBZwml6VniUxYK22M0Rznqr51x1x8mYNozXtgJuMMtVb7S6lAtZsoZmhUcxcf3qrY7R954TTkPw+q1N2vHtcTW1VWfSxmNL8wCYmVzzzhsyNzHphNMQvBhnJ+7LGGCTq57NCOVC1qjMcL3Vs2LtwMxhxl5yyNxSOCechmDaFHOb6hDBvCOWbenzB/NixO1un3bX7FI4J5yGUCmaNcVchw2qBu/6o1SLhoU6LOm6AvM612yoSHDCaQiVYnaw05qAjRZnvWlWqMMWi9M04bQhTOSE0xBMvHmLuQy5rB23iHGhDkuGQMPQsjOl5dcJp2NiTIsz2WQxgXmhDqtinIbVcTpX3TExppWENAzvFT6MaQe21SyZng+Qy2ZYyGeNSa6ZflAbOOE0BtOmwJve8nYYk2ZKdnt9Wt2+0RbTYRaLOeeqT4ETTkMYxJkMefhtspjArAPbGoaflzOOikHnDjlX3TExVcOSQ15yw46MOkClkPMPSIu/KqHmNzLYJJwmWeymH9QGTjiNYZAcMsRd8lrezL1xD2NSv7VN5w1pKsWcMd6OfgZ00spEnHAagolZdRuG8Gp0jNiEOF3dshpY8ITThE0HPG+nlDe7FM7cK0sZpp2tbltyaLB+BmSGtQDZtPGY5aqb3acOTjiNIZsRFvJZIwZ99PqK/bZdrrpJDQQ2TZbSmDSdq97sGt2nDk44jcKUm1dbbSYH5w9jUveLTUOMNSZN5zL9oDZwwmkUphyf0bCgAPkwJoU6rHTVCzmanT5dA6oSbOhac8JpEIuG1NLZUEd3GJMaCAZZYRvXrx3/xuOE0zEVplic2t01Pc40SkWfTW/A+tXbXQq5DHmDs8KH0f/WJtx/zlV3TEXFkPOtB3WIFsXoTDoi2KYBHxqT6mBNP6gNnHAaRcWQKeY2ZoVz2QylfMaIB79h0bEZGpPqiE0/qA2ccBrFojEWp31ZdTCn+6XWNN/VPIxu+Y37/uv2+jQ7feO9HSecBmFKjLNuYVYYzCni9lx1e+LDYI6rrpNTpnddOeE0iErRjEEVNrrqYE7bYKNtX4zTlHmwtmzaTjgNwpRdv9bsks8KxZxdt4cpMyXrFrrqpnRe2VIKZ9eTkXBM2fV1VlhEYr2OaTEpuWabxWlKcsgWb2cu4RSRNRH5jIg87/93dcx73iEifyIiT4vI10Xkb4x879dE5GURedL/8455rsd2dAmGCTev6eUg4/Bc9fgLuG2oQzxMIZehkM3E3vI76LoyfP3mtTg/DHxOKfUA8Dn/68PsA39LKfVW4P3A/yEiKyPf/wdKqXf4f56c83qsxiRX3fTpNOMwwVXv9RWNtn3lSOBt3HHfe7Z0Xc0rnA8DH/f//nHgA4ffoJR6Tin1vP/3K8ANYGPOz00kQ1c93l2/3uoYv+OPw4TjH3SowPTkxjgWDTjwzobzhmB+4TyjlLoK4P/39HFvFpGHgALw4sjLP++78B8VkeIxP/uoiFwUkYs3b96c87LNpGJILZ29rnqeg06PXl/Fdg221sCCGUcs27J+JwqniHxWRL4x5s/D03yQiJwF/gXw3yqldL3NTwN/DngXsAb81FE/r5R6TCl1QSl1YWMjmQarKW2DdcsOatMsGtBvrTc9GzeeqgENGLYkh068OqXU9x71PRG5LiJnlVJXfWG8ccT7loB/D/xDpdQXR373Vf+vLRH5VeDvT3X1CaPqxxXj7n6pW5jcgNtLapYX4onR1iyxmMZRKeW4WWvFeg31Vo9CNkPB8FK4ea/uceAR/++PAL97+A0iUgD+DfDrSqnfOfS9s/5/BS8++o05r8dqTLE4a027zhvSmDBaTltsVq6fAZ1r9VbHirWbVzh/AXiviDwPvNf/GhG5ICK/7L/nrwPvAX5kTNnRb4rIU8BTwDrwj+e8HqsxYVBFp9en1e1baTGZcDb9MLlhX1VCpRR/VUK9aUd8fa4rVEptAt8z5vWLwI/5f/8N4DeO+Pnvnufzk0jcgypsCc6Pw4Sz6a2PccZ8fIYtzQNmBxJSSNyDKmoWP/gmHBFsdYyz6B2fEeeshJoliUknnIYR96CKuiWdG+MwoZxrYHHauH4mxIhbdsTXnXAaRtzdL4MYnQU372GqpfirEuqtDuVClmzGrj5/GIp93PefDZuOE07DqMac2bTaYjLB4rTkwR9H1YBZCbYkh5xwGkbc/cI2x+iyGaFcyMaa4KhZ8uCPQ1cCxCmctZYdcxKccBpG3LV0NmeFwYD1a3WtjA/DyHSumCz2VrdHu9t3MU7H9FRKOfZidDVtLkeC+GsRbXE1x1GJuQ5WjwS04d5zwmkYS6U87W6fVjeeCUn6oTH9sKyjiD1GnIQYZ0wbT63phVhsWD8nnIYx7LeORzj1gI+MhVlhiN/itHWWKYyONYwnRmxTDbETTsOIOzNsw5nWx1GJecKPLXWI4ygXsojEee/ZU0PshNMw9G6714xn17d1FqemWopvpqRSympXXURibfkdDkgx32J3wmkYcdfS1Sw8oXGUSjE3iJVFTbPTp9dXdm88MVrsNjVfOOE0DD2TM86b1wZX6SiqJS85pFT0U+BrLXuSG0dRKcWXXLOphtgJp2HEfdJlw2JXE7yHrq/goBN9cs3mWZyaOOtgbVo/J5yGMewXjinGaXEdIsRbxG3LQWPHUSnlY6tKqLc65DJC0fDp7+CE0zj0bhtXgL6WAIsT4lk/m/v8NXHWwepN2zsQwmyccBpGMZchn5VYLCadFbbBVTqKwcYTw/rVLEpuHEWc5Vw2bdpOOA1Dl4TEsevvt3soZbfFVIkxuTaI0VlaAA/xJodsOl3VCaeBxNX9YlM5yFHE2f2SlPWrt7r0Yzib3iZvxwmngVSK8QToa0mI0cXoqg/PBM9G/tlBodev0Y5n/Wy595xwGohXixijxWTJzTuOOBsIas0uhVyGYs5e4Rxa7PGsX8WCriFwwmkkcWU2bR8pB8MjguMpR+pY3TwA8ZZz2XJQGzjhNJK4Ypw2Tac5irx/Nn0cG0/d8nZViLmcq9VxMU7H7MRVEjKcTmOHu3QUlWI+pgffHovpKOKaydnp9Wl2+tZY7E44DaRaiunB14NkLdn1j6Iao8Vu+9rFde5Qw7KKBCecBlIt5WKZAp+ErDBoiz2e5JotFtNRVAZVCdGun20VHU44DSSuYca1VpdC1u6sMMQ3qML2WaYQ39nqgzCRJevnhNNA4ioJsX3AhyYuV92mzpejiO3eG1R02BFfd8JpIHEVcSchuQHxtQ0mweIcnE0f9b1nWUWHE04DiWsmp+2zODVx1MF6MWl7ssLHEUeow6YhxuCE00jimgKfhKww+BZnM9op8EloHtBUStGfO2TTEGNwwmkkg8xmxG2XScgKgxcn6/YVrW4/ss8cDviwI0Z3HHGcO1S37NgRJ5wGElcRchJidDBaUhPd+tlWTnMcccSI680uIt4RxTbghNNA4mp7S0JWGIbnckdZi2hbOc1xxNG5tuffezZMfwcnnEYS1xR4m4YsHEccJTW2uZrHsRhDcsi2MJETTgPRU+CjdDWbnR7tXp+lhQTE6GIIdSRhQIqmGsPZ9LbVEDvhNJRqKR/prl+zLKt5HMPkWpQWpx6Qkoz1i/psettqiJ1wGkrUFqe2MJIgnHGUc9lWwH0clWI+8rPpa60uVYsqEpxwGkol4inwA4vTkpa344ijgaDe6pIRWMjbkRU+jjiGGdebHas2HSechlKN2OLcS5DFqac7RSmcewcdqqW8NVnh46jGUNXhkkOOQIi6lk6LdBKSQ8VclkIuE3kdZxI2HYhnQpJtpXBOOA2lWoq2li5JMU6IPjO817QrRnccUVcl9PqKRrvnXHXH/ER9/MMwq56Mhz9qi32v2WHJogf/OLTXsRfRxqOPInYWp2Nuop4Cv5eglkGIw2JPnsUZlcVuYymcE05DiXoK/N5Bh0oxRzZjf3ID/HKuSC32BFqcB9GsnxboJYs2HiechlKNuKSm1uwm5sEHP9QRZVXCQScRiTWASiGHSHQWpxZomyx2J5yGEnVms9bsWHXjngLRYoUAACAASURBVMTSQnTJoX5feeU0Cdl4Mhmv5XcvQm8HvH8zW3DCaShRj0ZLUjkNeG6ffiDDptHu0ld2xehOYqmUjyw5pOfO2rRxO+E0lGrE51vXWp2EPfhejLPfD7/felADa9GDfxLVUi6yGKf+HJtCRU44DSXqzObeQXKywuAlOJQalrqESdJKucDbBKLLqjuL0xEQUZ90WWt2rIoxnYRevyjidNqlTdL6eTHiiCzOZpdSPkMhZ48c2XOlKUPvvlHs+kqpRNUhwtBtjiLOaaPFdBLVKGOczY51YY65hFNE1kTkMyLyvP/f1SPe1xORJ/0/j4+8fp+IfMn/+d8SkcI815MkCrkMC/lsJBZTs9On21fJinEu6I0nAovzwL4C7pOoliK0OA/sS0zOa3F+GPicUuoB4HP+1+M4UEq9w//zgyOv/xPgo/7PbwM/Ouf1JIqlhZyzmGZk4KpHuH62WU3HoWOcUQwz3mvaVwM7r3A+DHzc//vHgQ9M+oPizd/6buCTs/x8GoiqJGQQo7Ns1z+Ogaseyfol0+LsK2i0w2/5tXFAyrzCeUYpdRXA/+/pI95XEpGLIvJFEdHieArYUUppf+AScNec15MolhbykZSE7CWwnCZSV73ZoZDNUErAEGPNsO0yAov9wL521ROvVkQ+C9wx5ls/M8XnnFdKXRGR+4HPi8hTwN6Y9x3pF4jIo8CjAOfPn5/io+2lWsqx1WiH/jk2Dlk4iWhd9W6iMuoQbVXHXrNrnat+4r+2Uup7j/qeiFwXkbNKqasicha4ccTvuOL/9yUR+QPgncC/AlZEJOdbnXcDV465jseAxwAuXLgQ3SlSMbJUyvOtW43QPyeJMc58VifXInDVD5LVrgpRhzrsa76Y11V/HHjE//sjwO8efoOIrIpI0f/7OvBdwDPKizr/PvBDx/18mllaiKZfOIkWJ0RXi5i0dlWIrgGj2enR7vatCxPNK5y/ALxXRJ4H3ut/jYhcEJFf9t/zFuCiiHwNTyh/QSn1jP+9nwJ+UkRewIt5/sqc15ModL912JlN7c4m7+GPJrlmYx3iSUQ1Wm7YrmrXvTfX1SqlNoHvGfP6ReDH/L9/AXj7ET//EvDQPNeQZJYW8nT7imanz0IhvMRDremd0LhYsOvmPYmliPqt95pdziyVQv+cKInK4hx2Xdm18bjOIYOJKs5Ua3pDjDMJGWKsWVqIpt86kRbn4N6LyuK0a/2ccBqMztSGnRlOWrulxnPVXYxzFkr5LIVsJvRN29YwkRNOg4nK4txL4IMP2lUPd+06vT777V4iNx6vcy3cjce56o7AGdYihn/z2uYqTYLnqndDTa7pM6GSVscJnsUedqjD1ooOJ5wGE9UxrUks4Ab/pNBen1a3H9pn7CWwBlazVAq/HG5wbIZl6+eE02CiGo2WtPOGNFGsn60W0yREZXFmM0I5xKqRMHDCaTBRDeNNYnIDorHY9xI4GUkTxWg53TXkzfyxByecBlPKZynmMqFaTN4QY/ta3iYhio0nibM4NVEceOcdS23fpuOE03CWFsItqWm0e/4JjfbdvCcRjavuLM558Pr87dt0nHAajhegdw/+LCwvRGBxJjirvrSQ56DTo9MLN7lm473nhNNwvJmcLrkxC1Gc26R/d6WYxPULf7ScrfF1J5yGsxRy98twpJx9N+9JDF31cB/8xUKWXDZ5j1IUoY69A/uOzQAnnMZTLeWohXrjJu9McE0pnyGflXCz6gmcxamJyuJ0rrojcLzkUBTlNMmzOEUk9FpEW13NSQi7nKvXV9Radq6fE07D8UpCwmsbHEynsdBdmoSwR8vZeELjpIQ9Wq5u8b3nhNNwlhbCbRtMcnIIwrfYE21xhhwj3rM4vu6E03DCDtDvNTvkMsJCgk5oHCXsWsSktqtC+NO5bO66csJpOMM4UzgP/+5Bh+WFvHUtb5MSdvfLXrObyPgwQCXkzitbj80AJ5zGszS4ecN5+Hf3OyyX7dvxJ2UpxHOHhu2qyVy/bEaoFHOhxTgHk5FcjNMRNMNDs0ISTt/iTCphuurNTp9OT1kZo5uUMJNrg64rCzceJ5yGsxSyu7Rz0GYlwcK5tJBnvx1O2+CgXTXB6xdmOZfNzRdOOA0n7OTQzn6HlXIhlN9tAkshFnHv+P8myd54wpuVYPNkKSechhN2EXLyXfXw+tV39n3hTHiMeDckV73W7FC2tF3VvitOGcVcxjttMISbt9dX1JrdRAvnMEYcgsW53wZgZSG5FvtyObyqBFsnI4ETTuMRkdDcJf1AJFo4Q6xKGLjqCbY4VxYKgw0iaPYO7G0ecMJpAWHVIqbhwQ/TVd/1XfUkl3OtlPM02j3aIXSu7Ry0WbU0vu6E0wKqIU2BH7iaCX7wtajpeGSQ7By0yWaEagJncWpW/fXbDWPjtriG2AmnBSyVwilC3k2Bq64f/J0QHvykd10BLPsW4e5B8O76zn5n8O9jG044LSCsKfBD4bTTXZqEhXyWQjYTjsW530l0KRIMS63CsthtLYVzwmkBS6VcKCUhabA4RYTlcj6UBMfugb2u5qSshBTqaHZ6NDt9a+89J5wWoC3OoGdy6ofB1pt3UlbLeWdxzogutQo61LFreWLSCacFrJYLtHt99tu9QH/v7kGHxUKWQi7Zt8HKQoGdMGJ0FruakzJMrgW7ftv+73NZdUdo6AD6dsA3785+sruGNCshWpxJX79qMUdGgnfVB11Xlq6fE04L0FZN0Dfv7kF7kDVNMmEIZ7fXp9bsWutqTkomIywv5AO32Hcsr4F1wmkBq6EJZ4flheTWIGpWy4XArXVdV2urxTQNK+VCKJs2OFfdESIrIbrqSe6z1iyX87S6fZqd4GLEw+aB5K/fSjkfeAH8tuUDUpxwWsBKSAH63YOOtTfuNGirJsiNR2eZbXU1p2FlIfhQx85+h0I2Y+1ZV044LUBbhdsB3rxKKXYSPlJOE0YR967lyY1pWCkHX5Xgxdft7bpywmkBhVyGSjEX6IPf7PRpd/upsJjC6FfXQpIGV305BItzu2FvuyU44bSGlYC7X9LQNaQZJtcCXL9UWZx5as0u3QCPH/GObLF303HCaQlBZ4YHFpPFN++krIQw6GPH4hMap2VlcApBcG2/Nk9GAiec1rBSzgca49y1PKs5DaEkh/Y7VEs5shk7Y3TTsBKGxX7gXHVHBHi1dCFkhVNgMZXyWYq5zGCzCIK0VCTASIw4QIt9e9/udlUnnJawGpLFmQbhhBBCHft2x+imQbvqQW08tk9GAiec1rBSLrDX7NDrBzMhyfbpNNMSdNvlTooszkFyLaCSpCTce044LWG1nEep4M5X18c+VBJ87MMoXr91sBa7zRbTNAQ9k1P/HlvbLcEJpzUE3XaZhmMfRlkNIUZss8U0DdVSHglwQtL24Fhle9fPCaclrJSD7R5Kw0i0UYJ01ft9laoYZzYjLJWCqyO2fTISOOG0hqCLuHdT0m6p0RN+gpiiX2936Su7Y3TTslIOLtRh+2QkcMJpDasBx5nSVE4D3oPf7vU5CGBCUtoqEiDYQR87CaghdsJpCSsBF3GnzVUfTtGf/+EfPvj2WkzTslwuBGZxbls+GQmccFrDkt+lEqjFmSLh1EcgBxHqGA74SM/6rSzk2Q0sTGT3ZCRwwmkNIt4RBkFYnP2+Yq+ZTosziCLuQR1iitYvyBjnzr7d7ZYwp3CKyJqIfEZEnvf/uzrmPf+5iDw58qcpIh/wv/drIvLyyPfeMc/1JJ2gMsN7zQ5KkYrzhjRBViUkISs8LSsL3hT4fgANGNsJqEiY1+L8MPA5pdQDwOf8r29DKfX7Sql3KKXeAXw3sA/8h5G3/AP9faXUk3NeT6JZDWig7GbD+x3rFbtv3mkYTkiaf/3SNJJPs1IuoBTUApiQZPtkJJhfOB8GPu7//ePAB054/w8Bv6eU2p/zc1PJajnPdmN+i2mz7onHqcXi3L/LFpYDnAK/s9+mXMhSzNmb3JiWoDeeVLvqwBml1FUA/7+nT3j/B4F/eei1nxeRr4vIR0UkPU/yDAQ1IWmz3gLgVIoszlI+y0I+G0xyaD9diTUItu1yZ79jfUXCiY3KIvJZ4I4x3/qZaT5IRM4Cbwc+PfLyTwPXgALwGPBTwEeO+PlHgUcBzp8/P81HJ4agJiTd8l31NAknBLd+Ww27R6LNwqAqYc4EUbPT46DTsz7McaJwKqW+96jvich1ETmrlLrqC+ONY37VXwf+jVJqsPLaWgVaIvKrwN8/5joewxNXLly4EMyIIMtYKRc46PRodnqU5qiB0xbnWtoe/oDOB79Vb7FeTZdzNKiDbcxnsSdhMhLM76o/Djzi//0R4HePee+HOOSm+2KLeAVdHwC+Mef1JBp9s817xvVmvc1qOU8um65qtNVyftDuNw+36u1UJdYATlW8jeKWv+nOShImI8H8wvkLwHtF5Hngvf7XiMgFEfll/SYRuRc4B/zhoZ//TRF5CngKWAf+8ZzXk2iCOgJis9EaPAhpIojjR5RS3Kq32EjZ+i2VchSyGW7V57v3thr2T0aCCVz141BKbQLfM+b1i8CPjXz9LeCuMe/77nk+P20MRsvNmVm/VW9zatHuHX8Wgkiu1VtdWt1+6uLDIsJ6pTC3xal/fsPyUEe6fDXLCWpC0ma9xXrKLCaA9cUCW432XFP0dSlXKtevWpxbOG/WvJ+3ff2ccFrEakDdL5uNduosJvCsnL4auouzcGtQymX3gz8LpxaDsThzGbE+q+6E0yKCmALf6fXZ2e+kqvhdo60cbfXMwq16+rquNOuVIrdq83k7t3xvJ2P5scpOOC2ilM9SKeYG7uIsbKe0hhOGcbV5rKZBjC6FFud6tchmozXXMOibtRbrVfvvPSeclrFRLXJzrgc/3RYTzGtxej+7msLk2nqlSKen5iqH80q57N90nHBaxkalyI295sw/v9lIb4wuCItT18DmU1YDC8PNdl6L3QmnI3I2luazOIcDPtJnMS0Wcyzks3NbnGncdGAYnrg5Y5xzUANreSkSOOG0jo1KMRBXM7UP/9yhjlYqwxzAoM10Votz96BDp6ecxemIno1qkVqzS3PGQ8c2G23yWWGpNFfvg7VszFmLuJmQGN0saC9lc8b10+uehI3HCadlnK7Ol+DYrLc4tVi0+ryXeVivFOay2G8mJEY3C6vlAtmMzNx2qV38JFQkOOG0DB0fulGbLUG0WU9n8bvGszhne/CbnR61ZjcRFtMsZDLC2hxF8DcT0m4JTjitY2NOi/NWo53a+CZ4JTVbjTadXn/qn90a1MCme/1mFc5bCWm3BCec1jGvcG7WW6ynMKOu0es3SxPBMEZn/4M/K+uVAjdntNiT0m4JTjit49RikYzAjZmFM92u+voccyU3U9w8oNmoFAeW47TcrCWj3RKccFpHNiOcmrEkab/d5aDTS7WrOY/FftNZnIMJSbO0XXqT85Ox6TjhtJBZaznTXPyuGRRxz2BxDmtg07t+65UCrW6femv6Y4KT0m4JTjit5PRScSZX3cXo5utX36x7xwKXC+msgYXhkdKzVCZoVz0JOOG0kLktzhRbTAuFLNVibqb1S0qf9TzM2j2klGKzkYx2S3DCaSW6+6U/5STzNA/4GGXWSeZpbrfUDAZ9TLnxJKndEpxwWsnpapFuX0090PiWi3EC81nsad90dIz41pRT9IdHZiTj3nPCaSEb1RIwfYJjs96mUszNdSZ7Elivztb94lx1WFssIDK9xTnoGkrI+jnhtJBZS2pu1lupjm9qZrE4e33FViN956kfJpfNsFqefuPR3o6LcTpiQw/6uLE33c17eXufO5cXwrgkq1ivFNmbcsLU9n6bvkp3RYJmlmOCk9RuCU44rWRgcU55817eOeCuVSecg7bLKeJ0roZzyKnF6Qel3ExQuyU44bSSxWKOcmG6Sebtbp8btRZ3rTjhnKWW8+quN43qjqVSKNdkE7NUJdxKULslOOG0lo3qdEXwV3cPUApncTJy9tAU63dp+wCAc2vlUK7JJu5YKnJ1tzlVOVyS2i3BCae1nK4WuTnFTM7L/oN/t7M4Zwp1XNrap5DNJCYrPA/n1sq0u/2p1u/6XitRa+eE01KmtTgv73jC6SzOoat+fYrTQi9te/HhpLia83Bu1bO6L23vT/R+pRSvbe0nylp3wmkp05bUXN45QATOuqw6hVyGO5ZKvLo12YMPnkjc7TYdAM6teevw2tbBRO/fPehQa3U574TTETenl0rUml0O2pOV1FzePuB0tUgh5/7JAc6fKvPaFML52vYBd68m58GfB70Ok66fFlhncTpiR2fHJ3WXLu8ccKeLbw44v1bmlc3J1q7R6rLVaDuL06eUz7JRLfLahPeetuzPJWjjccJpKfeuLwLw8q3GRO+/vHPgSpFGuGetzI1aayKLXceHk2Qxzcu51YWJXfWBcK4l5/5zwmkp957yHuJJrKZ+X3F1p+kSQyOc99dvEqtJu6TO4hxybq08lcW5tligWkpG8Ts44bSWlXKBlXKeb22ebHHerLdo9/quFGkEnaiYZOPRNZxOOIecWy1zdbdJd4LTQpOWUQcnnFZzz6nFiYRTP/jO4hxyzykv1DFJZv3S9j7FnKvhHOXc2gK9vhp0VB3Hq1v7icqogxNOq7nvVJlv3Tr5wR/UcK4k6+adh9Vynkoxx6sTbDyvbR1w9+oCIq6GU3Nuwsx6t9fn8s4B5xMU3wQnnFZzz6lFruwenDjl57KzOF+HiHB+rTyZxbmz70qRDqFd75PinFd3m/T6ylmcDnO4b30RpU4uSbq8s8/ygmdhOYbcc6rMKxO56geJyggHwdnlEtmMnJhZH2bUnXA6DOEePzP88gnu+pWdpitFGsP5tTKXtg7oHTOsotbssLPfcRbnIXLZDGeXSydanFo4ncXpMIb7/FrOV06I013ednM4x3H+VJl2r39sz7rLqB/NudWTu69e3donl5HEtfo64bSYlXKB5YX8sUXwSilX/H4Ek5QkDcbJOYvzdZxbW+C17ZNd9btWF8gmbDiKE07LuXd98dgHf++gS73VdcI5hnvWPIv9OKvJFb8fzbnVMjdrrWOTk68lsBQJnHBaz72nysfWcr7qHvwjuXPFS3C8snX0+l3aPmAhn2Ut5Ucqj+PutZPnJbyawOJ3cMJpPfecWuTKzgGt7vhd/2uXdgB4213LUV6WFeSyGe5aWTjBVd/n3Jqr4RzHsJZzvLu+5yfWnMXpMI771sv01dE371df3WG9UnAW5xHcc8J4ueeu17jX7zJy3I62JI+qhX0toRl1cMJpPbp18FtHJIi++to27zi36iymIzi3dnQt541ak29t7nPh3tWIr8oOTleLnFos8LXXdsZ+X1vySUysOeG0nPu0cI6Jc+7st3npZoN3nl+J+rKs4f71RXb2O2NLkp54eRuAh+47FfVlWYGI8O771/jiS5so9fpa2C+/vEUpn+GBM5UYri5cnHBazko5z1Ipx4s3Xy+cT/qWgBPOo/nON3ii+EfP33rd95741hYL+SxvvXMp6suyhu+4/xRXdpuDsq1RvvDiLd517xqlfDaGKwsXJ5yWIyJcuHeNP3r+5ut2/a++ukNG4NvudsJ5FG+5Y4n1SpE/ev7m67735Ze3+PP3rJDPusfkKN7tW+N/8tLmba/f2Gvy3PU63/XG9TguK3TcHZEA3vvgGS5tH/Bn12q3vf7V13Z405mq61E/hkxG+MsPrPNHz9+67Zzw3YMO37y2x7vuXYvx6szngdMV1hYLfPGQcH7hRe/r73qDE06HoXzPW04jAp955vrgtX5f8eSr27zzvEtsnMR73rTOVqPNM1f3Bq/96SvbKAUPOeE8lkxGePd9a3zppa3bXv/jF26xUs7zYELDHE44E8Dpaol3nFu5TThfutVgr9l18c0J+Etv3ADgD58buutf/tYWuYy4jWcC3n3fGpd3DgblR0opvvDCLb7z/lOJa7XUOOFMCO998AxPXd7l6q4XpP/qq15G+M874TyRjWqRB88u3RbnfOLlLd5+9zILheQlNoLmO/wEm3bXX77V4MpuM7HxTZhTOEXkh0XkaRHpi8iFY973fhF5VkReEJEPj7x+n4h8SUSeF5HfEhHX1zYj3/fgGQA+61udf/rqNtVSjvvXk1cKEgbvedMGX3llm0arS7PT4+uXdp2bPiFvOl1ltZznSy977vp/0vFNJ5xH8g3grwH/8ag3iEgW+EXg+4EHgQ+JyIP+t/8J8FGl1APANvCjc15PannDRoX71xf5909d5R//u2f4xBOv8ZfeuE4moa5S0LzngXU6PcWv/PHL/M//9mnavb5LDE1IJiM8dN8af/jcTX7zS6/wb792hbtWFgYnsSaRuYRTKfVNpdSzJ7ztIeAFpdRLSqk28AngYfFaWb4b+KT/vo8DH5jnetKMiPDeB8/wxZe2+OU/fpn/6t3n+d9++Nvjvixr+Av3rlIuZPnfP/Mc/+orl3nfW88k2mIKmr/6bXeys9/mZ/7NN/jyy1u8500bie5Wi6JO5S7gtZGvLwHvBk4BO0qp7sjrd0VwPYnlb7zrHN+4sst/95438J43bcR9OVZRzGX5lUfeRaPV5TvfcIpFV8I1FT/w7XfyV95+lhu1Jld3m7zpTDXuSwqVE+8OEfkscMeYb/2MUup3J/iMcduOOub1o67jUeBRgPPnz0/wsenj/o0Kv/lj3xH3ZViL7iJyzEbWn/SetGnv4zhROJVS3zvnZ1wCzo18fTdwBbgFrIhIzrc69etHXcdjwGMAFy5cOPqQGIfD4QiZKMqRngAe8DPoBeCDwOPK6w/8feCH/Pc9AkxiwTocDkeszFuO9F+IyCXgO4F/LyKf9l+/U0Q+BeBbkz8BfBr4JvDbSqmn/V/xU8BPisgLeDHPX5nnehwOhyMKZNw4KNO5cOGCunjxYtyX4XA4EoaIfEUpdWRNusZ1DjkcDseUOOF0OByOKXHC6XA4HFPihNPhcDimxAmnw+FwTIkTTofD4ZgSJ5wOh8MxJU44HQ6HY0qccDocDseUOOF0OByOKXHC6XA4HFPihNPhcDimxAmnw+FwTIkTTofD4ZgSJ5wOh8MxJU44HQ6HY0qccDocDseUOOF0OByOKXHC6XA4HFNi5ZlDInITeGXKH1vHO5I4DuL87LR/fpr/3+P+fBv/3+9RSv3/7Z1dqBVVFMd/f1KLSvSafZgKZURQD9VFxL5EMEwltCLECJIMQkrIhyBBEOnNoh6KKPqQLKQufVgSSkoFPWmZXL/QvFcRMm9XyNCih7JWD7NPDOPMuTPn7HOsc9YPDrPP3mvPf9as2Yu995zLvXwko/9l4mwESbvK/BOmTtPudv1u9v1863ey775UdxzHqYgnTsdxnIp0U+J8vUu1u12/m30/3/od63vX7HE6juPEoptmnI7jOFHoqMQpaZ6k7yUNSlqV036hpL7QvlPSNRG1p0r6StJBSQckPZVjM1vSaUn94bMmln44/zFJ+8K5d+W0S9JLwf+9knoj6d6Q8qlf0hlJKzM2UX2XtF7SSUn7U3UTJG2XNBCOPQV9lwabAUlLI+o/L+lQuLebJI0v6Fs3Tk3or5X0Y+oeLyjoW3ecNKjdl9I9Jqm/oG8M33PHWjvjj5l1xAe4ADgCTAPGAHuAGzM2TwCvhfISoC+i/iSgN5THAodz9GcDn7XwHhwDJtZpXwBsBQTMBHa2KA4/kfwermW+A7OAXmB/qu45YFUorwLW5fSbABwNx55Q7omkPxcYFcrr8vTLxKkJ/bXA0yXiU3ecNKKdaX8BWNNC33PHWjvj30kzzhnAoJkdNbM/gPeBRRmbRcCGUP4QmCNJMcTNbMjMdofyr8BBYHKMc0dkEfCOJewAxkuaFFljDnDEzKr+gUIlzOxr4FSmOh3fDcB9OV3vAbab2Skz+wXYDsyLoW9m28zsbPi6A5hS9bzN6JekzDhpWDuMp8XAew1cW1n9orHWtvh3UuKcDPyQ+n6ccxPXvzbhAT8NXBb7QsIWwK3Azpzm2yTtkbRV0k2RpQ3YJuk7SY/ntJe5R82yhOJB00rfAa40syFIBhdwRY5NO+4BwDKS2X0eI8WpGVaErYL1BUvVVvt/FzBsZgMF7VF9z4y1tsW/kxJn3swx+5OBMjbNXYR0KfARsNLMzmSad5MsYW8GXgY+iakN3GFmvcB84ElJs7KXl9Mnmv+SxgALgQ9ymlvte1na8QysBs4CGwtMRopTo7wKXAfcAgyRLJnPubycupj+P0T92WY030cYa4Xdcuoq+99JifM4MDX1fQpwoshG0ihgHI0td3KRNJokkBvN7ONsu5mdMbPfQnkLMFrSxFj6ZnYiHE8Cm0iWZWnK3KNmmA/sNrPhnGtrqe+B4drWQziezLFp6T0ILxvuBR62sKmWpUScGsLMhs3sLzP7G3ij4Lwt8z+MqQeAvjrXGMX3grHWtvh3UuL8Frhe0rVh5rME2Jyx2QzU3qI9CHxZ9HBXJeztvAUcNLMXC2yuqu2pSppBcv9/jqR/iaSxtTLJi4r9GbPNwCNKmAmcri1tIlE422il7ynS8V0KfJpj8zkwV1JPWMrODXVNI2ke8Ayw0Mx+L7ApE6dG9dP71fcXnLfMOGmUu4FDZna84Pqi+F5nrLUv/s283fqvfUjeGh8meWu4OtQ9S/IgA1xEsowcBL4BpkXUvpNkyr8X6A+fBcByYHmwWQEcIHmTuQO4PaL+tHDePUGj5n9aX8Ar4f7sA6ZH1L+YJBGOS9W1zHeSBD0E/Ekyi3iMZL/6C2AgHCcE2+nAm6m+y8IzMAg8GlF/kGT/rBb/2i84rga21ItTJP13Q1z3kiSRSVn9onHSrHaof7sW75RtK3wvGmtti7//5ZDjOE5FOmmp7jiO0xY8cTqO41TEE6fjOE5FPHE6juNUxBOn4zhORTxxOo7jVMQTp+M4TkU8cTqO41TkpxNePAAAAAVJREFUH+ktJxYbz4WvAAAAAElFTkSuQmCC\n","text/plain":"<Figure size 360x720 with 1 Axes>"},"metadata":{"needs_background":"light"},"output_type":"display_data"}],"source":["import matplotlib.pyplot as plt\n","import matplotlib as mpl\n","import numpy as np\n","import pandas as pd\n","\n","x = np.linspace(0, 20, 100)\n","fig, ax = plt.subplots(figsize=(5,10))\n","plt.plot(x, np.sin(x))\n",""]}],"nbformat":4,"nbformat_minor":2,"metadata":{"language_info":{"name":"python","codemirror_mode":{"name":"ipython","version":3}},"orig_nbformat":2,"file_extension":".py","mimetype":"text/x-python","name":"python","npconvert_exporter":"python","pygments_lexer":"ipython3","version":3}} diff --git a/src/test/datascience/testHelpers.tsx b/src/test/datascience/testHelpers.tsx new file mode 100644 index 000000000000..d8c07d1236b6 --- /dev/null +++ b/src/test/datascience/testHelpers.tsx @@ -0,0 +1,758 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as assert from 'assert'; +import { mount, ReactWrapper } from 'enzyme'; +import { min } from 'lodash'; +import * as path from 'path'; +import * as React from 'react'; +import { Provider } from 'react-redux'; +import { isString } from 'util'; +import { CancellationToken } from 'vscode'; + +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { IJupyterExecution } from '../../client/datascience/types'; +import { getConnectedInteractiveEditor } from '../../datascience-ui/history-react/interactivePanel'; +import * as InteractiveStore from '../../datascience-ui/history-react/redux/store'; +import { CommonActionType } from '../../datascience-ui/interactive-common/redux/reducers/types'; +import { getConnectedNativeEditor } from '../../datascience-ui/native-editor/nativeEditor'; +import * as NativeStore from '../../datascience-ui/native-editor/redux/store'; +import { IKeyboardEvent } from '../../datascience-ui/react-common/event'; +import { ImageButton } from '../../datascience-ui/react-common/imageButton'; +import { MonacoEditor } from '../../datascience-ui/react-common/monacoEditor'; +import { PostOffice } from '../../datascience-ui/react-common/postOffice'; +import { noop } from '../core'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { IMountedWebView } from './mountedWebView'; +import { createInputEvent, createKeyboardEvent } from './reactHelpers'; +export * from './testHelpersCore'; + +//tslint:disable:trailing-comma no-any no-multiline-string +export enum CellInputState { + Hidden, + Visible, + Collapsed, + Expanded +} + +export enum CellPosition { + First = 'first', + Last = 'last' +} + +async function testInnerLoop( + name: string, + type: 'native' | 'interactive', + testFunc: (type: 'native' | 'interactive') => Promise<void>, + getIOC: () => Promise<DataScienceIocContainer> +) { + const ioc = await getIOC(); + const jupyterExecution = ioc.get<IJupyterExecution>(IJupyterExecution); + if (await jupyterExecution.isNotebookSupported()) { + addMockData(ioc, 'a=1\na', 1); + await testFunc(type); + } else { + // tslint:disable-next-line:no-console + console.log(`${name} skipped, no Jupyter installed.`); + } +} + +export function runDoubleTest( + name: string, + testFunc: (type: 'native' | 'interactive') => Promise<void>, + getIOC: () => Promise<DataScienceIocContainer> +) { + // Just run the test twice. Originally mounted twice, but too hard trying to figure out disposing. + test(`${name} (interactive)`, async () => testInnerLoop(name, 'interactive', testFunc, getIOC)); + test(`${name} (native)`, async () => testInnerLoop(name, 'native', testFunc, getIOC)); +} + +export function runInteractiveTest( + name: string, + testFunc: () => Promise<void>, + getIOC: () => Promise<DataScienceIocContainer> +) { + // Run the test with just the interactive window + test(`${name} (interactive)`, async () => testInnerLoop(name, 'interactive', (_t) => testFunc(), getIOC)); +} +export function runNativeTest( + name: string, + testFunc: () => Promise<void>, + getIOC: () => Promise<DataScienceIocContainer> +) { + // Run the test with just the native window + test(`${name} (native)`, async () => testInnerLoop(name, 'native', (_t) => testFunc(), getIOC)); +} + +export function addMockData( + ioc: DataScienceIocContainer, + code: string, + result: string | number | undefined | string[], + mimeType?: string | string[], + cellType?: string +) { + if (ioc.mockJupyter) { + if (cellType && cellType === 'error') { + ioc.mockJupyter.addError(code, result ? result.toString() : ''); + } else { + if (result) { + ioc.mockJupyter.addCell(code, result, mimeType); + } else { + ioc.mockJupyter.addCell(code); + } + } + } +} + +export function addInputMockData( + ioc: DataScienceIocContainer, + code: string, + result: string | number | undefined, + mimeType?: string, + cellType?: string +) { + if (ioc.mockJupyter) { + if (cellType && cellType === 'error') { + ioc.mockJupyter.addError(code, result ? result.toString() : ''); + } else { + if (result) { + ioc.mockJupyter.addInputCell(code, result, mimeType); + } else { + ioc.mockJupyter.addInputCell(code); + } + } + } +} + +export function addContinuousMockData( + ioc: DataScienceIocContainer, + code: string, + resultGenerator: (c: CancellationToken) => Promise<{ result: string; haveMore: boolean }> +) { + if (ioc.mockJupyter) { + ioc.mockJupyter.addContinuousOutputCell(code, resultGenerator); + } +} + +export function getOutputCell( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + cellType: string, + cellIndex: number | CellPosition +): ReactWrapper<any, Readonly<{}>, React.Component> | undefined { + const foundResult = wrapper.find(cellType); + let targetCell: ReactWrapper | undefined; + // Get the correct result that we are dealing with + if (typeof cellIndex === 'number') { + if (cellIndex >= 0 && cellIndex <= foundResult.length - 1) { + targetCell = foundResult.at(cellIndex); + } + } else if (typeof cellIndex === 'string') { + switch (cellIndex) { + case CellPosition.First: + targetCell = foundResult.first(); + break; + + case CellPosition.Last: + // Skip the input cell on these checks. + targetCell = getLastOutputCell(wrapper, cellType); + break; + + default: + // Fall through, targetCell check will fail out + break; + } + } + + return targetCell; +} + +export function getLastOutputCell( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + cellType: string +): ReactWrapper<any, Readonly<{}>, React.Component> { + // Skip the edit cell if in the interactive window + const count = cellType === 'InteractiveCell' ? 2 : 1; + wrapper.update(); + const foundResult = wrapper.find(cellType); + return getOutputCell(wrapper, cellType, foundResult.length - count)!; +} + +export function verifyCellSource( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + cellType: 'NativeCell' | 'InteractiveCell', + source: string, + cellIndex: number | CellPosition +) { + wrapper.update(); + + const foundResult = wrapper.find(cellType); + assert.ok(foundResult.length >= 1, "Didn't find any cells being rendered"); + let targetCell: ReactWrapper; + let index = 0; + // Get the correct result that we are dealing with + if (typeof cellIndex === 'number') { + if (cellIndex >= 0 && cellIndex <= foundResult.length - 1) { + targetCell = foundResult.at(cellIndex); + } + } else if (typeof cellIndex === 'string') { + switch (cellIndex) { + case CellPosition.First: + targetCell = foundResult.first(); + break; + + case CellPosition.Last: + // Skip the input cell on these checks. + targetCell = getLastOutputCell(wrapper, cellType); + index = foundResult.length - 1; + break; + + default: + // Fall through, targetCell check will fail out + break; + } + } + + // ! is ok here to get rid of undefined type check as we want a fail here if we have not initialized targetCell + assert.ok(targetCell!, "Target cell doesn't exist"); + + const editor = cellType === 'InteractiveCell' ? getInteractiveEditor(wrapper) : getNativeEditor(wrapper, index); + const inst = editor!.instance() as MonacoEditor; + assert.deepStrictEqual(inst.state.model?.getValue(), source, 'Source does not match on cell'); +} + +export function verifyHtmlOnCell( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + cellType: 'NativeCell' | 'InteractiveCell', + html: string | undefined | RegExp, + cellIndex: number | CellPosition +) { + wrapper.update(); + + const foundResult = wrapper.find(cellType); + assert.ok(foundResult.length >= 1, "Didn't find any cells being rendered"); + + let targetCell: ReactWrapper; + // Get the correct result that we are dealing with + if (typeof cellIndex === 'number') { + if (cellIndex >= 0 && cellIndex <= foundResult.length - 1) { + targetCell = foundResult.at(cellIndex); + } + } else if (typeof cellIndex === 'string') { + switch (cellIndex) { + case CellPosition.First: + targetCell = foundResult.first(); + break; + + case CellPosition.Last: + // Skip the input cell on these checks. + targetCell = getLastOutputCell(wrapper, cellType); + break; + + default: + // Fall through, targetCell check will fail out + break; + } + } + + // ! is ok here to get rid of undefined type check as we want a fail here if we have not initialized targetCell + assert.ok(targetCell!, "Target cell doesn't exist"); + + // If html is specified, check it + let output = targetCell!.find('div.cell-output'); + if (output.length <= 0) { + output = targetCell!.find('div.markdown-cell-output'); + } + const outputHtml = output.length > 0 ? output.html() : undefined; + if (html && isString(html)) { + // Extract only the first 100 chars from the input string + const sliced = html.substr(0, min([html.length, 100])); + assert.ok(output.length > 0, 'No output cell found'); + assert.ok(outputHtml?.includes(sliced), `${outputHtml} does not contain ${sliced}`); + } else if (html && outputHtml) { + const regex = html as RegExp; + assert.ok(regex.test(outputHtml), `${outputHtml} does not match ${html}`); + } else { + // html not specified, look for an empty render + assert.ok( + targetCell!.isEmptyRender() || outputHtml === undefined, + `Target cell is not empty render, got this instead: ${outputHtml}` + ); + } +} + +/** + * Creates a keyboard event for a cells. + * + * @export + * @param {(Partial<IKeyboardEvent> & { code: string })} event + * @returns + */ +export function createKeyboardEventForCell(event: Partial<IKeyboardEvent> & { code: string }) { + const defaultKeyboardEvent: IKeyboardEvent = { + altKey: false, + code: '', + ctrlKey: false, + editorInfo: { + contents: '', + isDirty: false, + isFirstLine: false, + isLastLine: false, + isSuggesting: false, + clear: noop + }, + metaKey: false, + preventDefault: noop, + shiftKey: false, + stopPropagation: noop, + target: {} as any + }; + + const defaultEditorInfo = defaultKeyboardEvent.editorInfo!; + const providedEditorInfo = event.editorInfo || {}; + return { + ...defaultKeyboardEvent, + ...event, + editorInfo: { + ...defaultEditorInfo, + ...providedEditorInfo + } + }; +} + +export function isCellSelected( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + cellType: string, + cellIndex: number | CellPosition +): boolean { + try { + verifyCell(wrapper, cellType, { selector: '.cell-wrapper-selected' }, cellIndex); + return true; + } catch { + return false; + } +} + +export function isCellFocused( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + cellType: string, + cellIndex: number | CellPosition +): boolean { + try { + verifyCell(wrapper, cellType, { selector: '.cell-wrapper-focused' }, cellIndex); + return true; + } catch { + return false; + } +} + +export function isCellMarkdown( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + cellType: string, + cellIndex: number | CellPosition +): boolean { + const cell = getOutputCell(wrapper, cellType, cellIndex); + assert.ok(cell, 'Could not find output cell'); + return cell!.props().cellVM.cell.data.cell_type === 'markdown'; +} + +export function verifyCellIndex( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + cellId: string, + expectedCellIndex: number +) { + const nativeCell = wrapper.find(cellId).first().find('NativeCell'); + const secondCell = wrapper.find('NativeCell').at(expectedCellIndex); + assert.equal(nativeCell.html(), secondCell.html()); +} + +function verifyCell( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + cellType: string, + options: { selector: string; shouldNotExist?: boolean }, + cellIndex: number | CellPosition +) { + wrapper.update(); + const foundResult = wrapper.find(cellType); + assert.ok(foundResult.length >= 1, "Didn't find any cells being rendered"); + + let targetCell: ReactWrapper; + // Get the correct result that we are dealing with + if (typeof cellIndex === 'number') { + if (cellIndex >= 0 && cellIndex <= foundResult.length - 1) { + targetCell = foundResult.at(cellIndex); + } + } else if (typeof cellIndex === 'string') { + switch (cellIndex) { + case CellPosition.First: + targetCell = foundResult.first(); + break; + + case CellPosition.Last: + // Skip the input cell on these checks. + targetCell = getLastOutputCell(wrapper, cellType); + break; + + default: + // Fall through, targetCell check will fail out + break; + } + } + + // ! is ok here to get rid of undefined type check as we want a fail here if we have not initialized targetCell + assert.ok(targetCell!, "Target cell doesn't exist"); + + if (options.shouldNotExist) { + assert.ok( + targetCell!.find(options.selector).length === 0, + `Found cells with the matching selector '${options.selector}'` + ); + } else { + assert.ok( + targetCell!.find(options.selector).length >= 1, + `Didn't find any cells with the matching selector '${options.selector}'` + ); + } +} + +export function verifyLastCellInputState( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + cellType: string, + state: CellInputState +) { + const lastCell = getLastOutputCell(wrapper, cellType); + assert.ok(lastCell, "Last cell doesn't exist"); + + const inputBlock = lastCell.find('div.cell-input'); + const toggleButton = lastCell.find('polygon.collapse-input-svg'); + + switch (state) { + case CellInputState.Hidden: + assert.ok(inputBlock.length === 0, 'Cell input not hidden'); + break; + + case CellInputState.Visible: + assert.ok(inputBlock.length === 1, 'Cell input not visible'); + break; + + case CellInputState.Expanded: + assert.ok(toggleButton.html().includes('collapse-input-svg-rotate'), 'Cell input toggle not expanded'); + break; + + case CellInputState.Collapsed: + assert.ok(!toggleButton.html().includes('collapse-input-svg-rotate'), 'Cell input toggle not collapsed'); + break; + + default: + assert.fail('Unknown cellInputStat'); + break; + } +} + +export async function getCellResults( + mountedWebView: IMountedWebView, + cellType: string, + updater: () => Promise<void>, + renderPromiseGenerator?: () => Promise<void> +): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { + // Get a render promise with the expected number of renders + const renderPromise = renderPromiseGenerator + ? renderPromiseGenerator() + : mountedWebView.waitForMessage(InteractiveWindowMessages.ExecutionRendered); + + // Call our function to update the react control + await updater(); + + // Wait for all of the renders to go through + await renderPromise; + + // Update wrapper so that it gets the latest values. + mountedWebView.wrapper.update(); + + // Return the result + return mountedWebView.wrapper.find(cellType); +} + +export function simulateKey( + domNode: HTMLTextAreaElement, + key: string, + shiftDown?: boolean, + ctrlKey?: boolean, + altKey?: boolean, + metaKey?: boolean +) { + // Submit a keypress into the textarea. Simulate doesn't work here because the keydown + // handler is not registered in any react code. It's being handled with DOM events + + // Save current selection start so we move appropriately after the event + const selectionStart = domNode.selectionStart; + + // According to this: + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Usage_notes + // The normal events are + // 1) keydown + // 2) keypress + // 3) keyup + let event = createKeyboardEvent('keydown', { key, code: key, shiftKey: shiftDown, ctrlKey, altKey, metaKey }); + + // Dispatch. Result can be swallowed. If so skip the next event. + let result = domNode.dispatchEvent(event); + if (result) { + event = createKeyboardEvent('keypress', { key, code: key, shiftKey: shiftDown, ctrlKey, altKey, metaKey }); + result = domNode.dispatchEvent(event); + if (result) { + event = createKeyboardEvent('keyup', { key, code: key, shiftKey: shiftDown, ctrlKey, altKey, metaKey }); + domNode.dispatchEvent(event); + + // Update our value. This will reset selection to zero. + const before = domNode.value.slice(0, selectionStart); + const after = domNode.value.slice(selectionStart); + const keyText = key === 'Enter' ? '\n' : key; + + domNode.value = `${before}${keyText}${after}`; + + // Tell the dom node its selection start has changed. Monaco + // reads this to determine where the character went. + domNode.selectionEnd = selectionStart + 1; + domNode.selectionStart = selectionStart + 1; + + // Dispatch an input event so we update the textarea + domNode.dispatchEvent(createInputEvent()); + } + } +} + +export async function submitInput(mountedWebView: IMountedWebView, textArea: HTMLTextAreaElement): Promise<void> { + // Get a render promise with the expected number of renders (how many updates a the shift + enter will cause) + // Should be 6 - 1 for the shift+enter and 5 for the new cell. + const renderPromise = mountedWebView.waitForMessage(InteractiveWindowMessages.ExecutionRendered); + + // Submit a keypress into the textarea + simulateKey(textArea, 'Enter', true); + + return renderPromise; +} + +function enterKey( + textArea: HTMLTextAreaElement, + key: string, + shiftDown?: boolean, + ctrlKey?: boolean, + altKey?: boolean, + metaKey?: boolean +) { + // Simulate a key press + simulateKey(textArea, key, shiftDown, ctrlKey, altKey, metaKey); +} + +export function getInteractiveEditor( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component> +): ReactWrapper<any, Readonly<{}>, React.Component> { + wrapper.update(); + // Find the last cell. It should have a monacoEditor object + const cells = wrapper.find('InteractiveCell'); + const lastCell = cells.last(); + return lastCell.find('MonacoEditor'); +} + +export function getNativeEditor( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + index: number +): ReactWrapper<any, Readonly<{}>, React.Component> | undefined { + // Find the last cell. It should have a monacoEditor object + const cells = wrapper.find('NativeCell'); + const lastCell = index < cells.length ? cells.at(index) : undefined; + return lastCell ? lastCell.find('MonacoEditor') : undefined; +} + +export function getNativeFocusedEditor( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component> +): ReactWrapper<any, Readonly<{}>, React.Component> | undefined { + // Find the last cell. It should have a monacoEditor object + wrapper.update(); + const cells = wrapper.find('NativeCell'); + const focusedCell = cells.find('.cell-wrapper-focused'); + return focusedCell.length > 0 ? focusedCell.find('MonacoEditor') : undefined; +} + +export function injectCode( + editorControl: ReactWrapper<any, Readonly<{}>, React.Component> | undefined, + code: string +): HTMLTextAreaElement | null { + assert.ok(editorControl, 'Editor undefined for injecting code'); + const ecDom = editorControl!.getDOMNode(); + assert.ok(ecDom, 'ec DOM object not found'); + const textArea = ecDom!.querySelector('.overflow-guard')!.querySelector('textarea'); + assert.ok(textArea!, 'Cannot find the textarea inside the monaco editor'); + textArea!.focus(); + + // Just stick directly into the model. + const editor = editorControl!.instance() as MonacoEditor; + assert.ok(editor, 'MonacoEditor not found'); + const monaco = editor.state.editor; + assert.ok(monaco, 'Monaco control not found'); + const model = monaco!.getModel(); + assert.ok(model, 'Monaco model not found'); + model!.setValue(code); + + return textArea; +} + +export function enterEditorKey( + editorControl: ReactWrapper<any, Readonly<{}>, React.Component> | undefined, + keyboardEvent: Partial<IKeyboardEvent> & { code: string } +): HTMLTextAreaElement | null { + const textArea = getTextArea(editorControl); + assert.ok(textArea!, 'Cannot find the textarea inside the monaco editor'); + textArea!.focus(); + + enterKey( + textArea!, + keyboardEvent.code, + keyboardEvent.shiftKey, + keyboardEvent.ctrlKey, + keyboardEvent.altKey, + keyboardEvent.metaKey + ); + + return textArea; +} + +export function typeCode( + editorControl: ReactWrapper<any, Readonly<{}>, React.Component> | undefined, + code: string +): HTMLTextAreaElement | null { + const textArea = getTextArea(editorControl); + assert.ok(textArea!, 'Cannot find the textarea inside the monaco editor'); + textArea!.focus(); + + // Now simulate entering all of the keys + for (let i = 0; i < code.length; i += 1) { + let keyCode = code.charAt(i); + if (keyCode === '\n') { + keyCode = 'Enter'; + } + enterKey(textArea!, keyCode); + } + + return textArea; +} + +function getTextArea( + editorControl: ReactWrapper<any, Readonly<{}>, React.Component> | undefined +): HTMLTextAreaElement | null { + // Find the last cell. It should have a monacoEditor object. We need to search + // through its DOM to find the actual textarea to send input to + // (we can't actually find it with the enzyme wrappers because they only search + // React accessible nodes and the monaco html is not react) + assert.ok(editorControl, 'Editor not defined in order to type code into'); + let ecDom = editorControl!.getDOMNode(); + if ((ecDom as any).length) { + ecDom = (ecDom as any)[0]; + } + assert.ok(ecDom, 'ec DOM object not found'); + return ecDom!.querySelector('.overflow-guard')!.querySelector('textarea'); +} + +export async function enterInput( + mountedWebView: IMountedWebView, + code: string, + resultClass: string +): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { + const editor = + resultClass === 'InteractiveCell' + ? getInteractiveEditor(mountedWebView.wrapper) + : getNativeFocusedEditor(mountedWebView.wrapper); + + // First we have to type the code into the input box + const textArea = typeCode(editor, code); + + // Now simulate a shift enter. This should cause a new cell to be added + await submitInput(mountedWebView, textArea!); + + // Return the result + return mountedWebView.wrapper.find(resultClass); +} + +export function findButton( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + mainClass: React.ComponentClass<any>, + index: number +): ReactWrapper<any, Readonly<{}>, React.Component> | undefined { + const mainObj = wrapper.find(mainClass); + if (mainObj) { + const buttons = mainObj.find(ImageButton); + if (buttons) { + return buttons.at(index); + } + } +} + +export function getMainPanel<P>( + wrapper: ReactWrapper<any, Readonly<{}>>, + mainClass: React.ComponentClass<any> +): P | undefined { + const mainObj = wrapper.find(mainClass); + if (mainObj) { + return (mainObj.instance() as any) as P; + } + + return undefined; +} + +export function toggleCellExpansion(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, cellType: string) { + // Find the last cell added + const lastCell = getLastOutputCell(wrapper, cellType); + assert.ok(lastCell, "Last call doesn't exist"); + + const toggleButton = lastCell.find('button.collapse-input'); + assert.ok(toggleButton); + toggleButton.simulate('click'); +} + +export function escapePath(p: string) { + return p.replace(/\\/g, '\\\\'); +} + +export function srcDirectory() { + return path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); +} + +export function mountConnectedMainPanel(type: 'native' | 'interactive') { + const ConnectedMainPanel = type === 'native' ? getConnectedNativeEditor() : getConnectedInteractiveEditor(); + + // Create the redux store in test mode. + const createStore = type === 'native' ? NativeStore.createStore : InteractiveStore.createStore; + const store = createStore(true, 'vs-light', true, new PostOffice()); + + // Mount this with a react redux provider + return mount( + <Provider store={store}> + <ConnectedMainPanel /> + </Provider> + ); +} + +export function mountComponent<P>(type: 'native' | 'interactive', Component: React.ReactElement<P>) { + // Create the redux store in test mode. + const createStore = type === 'native' ? NativeStore.createStore : InteractiveStore.createStore; + const store = createStore(true, 'vs-light', true, new PostOffice()); + + // Mount this with a react redux provider + return mount(<Provider store={store}>{Component}</Provider>); +} + +// Open up our variable explorer which also triggers a data fetch +export function openVariableExplorer(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) { + const nodes = wrapper.find(Provider); + if (nodes.length > 0) { + const store = nodes.at(0).props().store; + if (store) { + store.dispatch({ type: CommonActionType.TOGGLE_VARIABLE_EXPLORER }); + } + } +} + +export async function waitForVariablesUpdated(mountedWebView: IMountedWebView, numberOfTimes?: number): Promise<void> { + return mountedWebView.waitForMessage(InteractiveWindowMessages.VariablesComplete, { numberOfTimes }); +} diff --git a/src/test/datascience/testHelpersCore.ts b/src/test/datascience/testHelpersCore.ts new file mode 100644 index 000000000000..909d07832ed9 --- /dev/null +++ b/src/test/datascience/testHelpersCore.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { DataScienceIocContainer } from './dataScienceIocContainer'; + +export function addMockData( + ioc: DataScienceIocContainer, + code: string, + result: string | number | undefined | string[], + mimeType?: string | string[], + cellType?: string +) { + if (ioc.mockJupyter) { + if (cellType && cellType === 'error') { + ioc.mockJupyter.addError(code, result ? result.toString() : ''); + } else { + if (result) { + ioc.mockJupyter.addCell(code, result, mimeType); + } else { + ioc.mockJupyter.addCell(code); + } + } + } +} diff --git a/src/test/datascience/testInteractiveWindowProvider.ts b/src/test/datascience/testInteractiveWindowProvider.ts new file mode 100644 index 000000000000..eeee9169bd5d --- /dev/null +++ b/src/test/datascience/testInteractiveWindowProvider.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable, named } from 'inversify'; +import * as uuid from 'uuid/v4'; +import { Memento, Uri } from 'vscode'; +import { IApplicationShell, ILiveShareApi } from '../../client/common/application/types'; +import { + GLOBAL_MEMENTO, + IAsyncDisposableRegistry, + IConfigurationService, + IDisposableRegistry, + IMemento, + InteractiveWindowMode, + Resource +} from '../../client/common/types'; +import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { InteractiveWindowMessageListener } from '../../client/datascience/interactive-common/interactiveWindowMessageListener'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { resetIdentity } from '../../client/datascience/interactive-window/identity'; +import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; +import { InteractiveWindowProvider } from '../../client/datascience/interactive-window/interactiveWindowProvider'; +import { IDataScienceFileSystem, IInteractiveWindow, IInteractiveWindowProvider } from '../../client/datascience/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { IMountedWebView } from './mountedWebView'; +import { mountConnectedMainPanel } from './testHelpers'; +import { WaitForMessageOptions } from './uiTests/helpers'; + +export interface ITestInteractiveWindowProvider extends IInteractiveWindowProvider { + getMountedWebView(window: IInteractiveWindow | undefined): IMountedWebView; + waitForMessage(identity: Uri | undefined, message: string, options?: WaitForMessageOptions): Promise<void>; +} + +@injectable() +export class TestInteractiveWindowProvider extends InteractiveWindowProvider implements ITestInteractiveWindowProvider { + private windowToMountMap = new Map<string, IMountedWebView>(); + private pendingMessageWaits: { + message: string; + options?: WaitForMessageOptions; + deferred: Deferred<void>; + }[] = []; + + constructor( + @inject(ILiveShareApi) liveShare: ILiveShareApi, + @inject(IServiceContainer) private readonly container: IServiceContainer, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(IDataScienceFileSystem) fileSystem: IDataScienceFileSystem, + @inject(IConfigurationService) configService: IConfigurationService, + @inject(IMemento) @named(GLOBAL_MEMENTO) globalMemento: Memento, + @inject(IApplicationShell) appShell: IApplicationShell + ) { + super(liveShare, container, asyncRegistry, disposables, fileSystem, configService, globalMemento, appShell); + + // Reset our identity IDs when we create a new TestInteractiveWindowProvider + resetIdentity(); + } + + public getMountedWebView(window: IInteractiveWindow | undefined): IMountedWebView { + const key = window ? window.identity.toString() : this.windows[0]?.identity.toString(); + if (!this.windowToMountMap.has(key)) { + throw new Error('Test Failure: Window not mounted yet.'); + } + return this.windowToMountMap.get(key)!; + } + + public waitForMessage(identity: Uri | undefined, message: string, options?: WaitForMessageOptions): Promise<void> { + // We may already have this editor. Check. Undefined may also match. + const key = identity ? identity.toString() : this.windows[0] ? this.windows[0].identity.toString() : undefined; + if (key && this.windowToMountMap.has(key)) { + return this.windowToMountMap.get(key)!.waitForMessage(message, options); + } + + // Otherwise pend for the next create. + this.pendingMessageWaits.push({ message, options, deferred: createDeferred() }); + return this.pendingMessageWaits[this.pendingMessageWaits.length - 1].deferred.promise; + } + + protected create(resource: Resource, mode: InteractiveWindowMode): InteractiveWindow { + // Generate the mount wrapper using a custom id + const id = uuid(); + const mounted = this.container + .get<DataScienceIocContainer>(DataScienceIocContainer) + .createWebView(() => mountConnectedMainPanel('interactive'), id); + + // Might have a pending wait for message + if (this.pendingMessageWaits.length) { + const list = [...this.pendingMessageWaits]; + this.pendingMessageWaits = []; + list.forEach((p) => { + mounted + .waitForMessage(p.message, p.options) + .then(() => { + p.deferred.resolve(); + }) + .catch((e) => p.deferred.reject(e)); + }); + } + + // Call the real create + const result = super.create(resource, mode); + + // Associate the real create with our id in order to find the wrapper + const key = result.identity.toString(); + this.windowToMountMap.set(key, mounted); + mounted.onDisposed(() => this.windowToMountMap.delete(key)); + + // During testing the MainPanel sends the init message before our interactive window is created. + // Pretend like it's happening now + // tslint:disable-next-line: no-any + const listener = (result as any).messageListener as InteractiveWindowMessageListener; + listener.onMessage(InteractiveWindowMessages.Started, {}); + + // Also need the css request so that other messages can go through + const webHost = result as InteractiveWindow; + webHost.setTheme(false); + + return result; + } +} diff --git a/src/test/datascience/testNativeEditorProvider.ts b/src/test/datascience/testNativeEditorProvider.ts new file mode 100644 index 000000000000..2b85c3ca2f6f --- /dev/null +++ b/src/test/datascience/testNativeEditorProvider.ts @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import * as uuid from 'uuid/v4'; +import { CustomDocument, Uri, WebviewPanel } from 'vscode'; + +import { + ICommandManager, + ICustomEditorService, + IDocumentManager, + IWorkspaceService +} from '../../client/common/application/types'; +import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry } from '../../client/common/types'; +import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { InteractiveWindowMessageListener } from '../../client/datascience/interactive-common/interactiveWindowMessageListener'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { NativeEditor } from '../../client/datascience/interactive-ipynb/nativeEditor'; +import { NativeEditorProviderOld } from '../../client/datascience/interactive-ipynb/nativeEditorProviderOld'; +import { NativeEditorProvider } from '../../client/datascience/notebookStorage/nativeEditorProvider'; +import { INotebookStorageProvider } from '../../client/datascience/notebookStorage/notebookStorageProvider'; +import { + IDataScienceErrorHandler, + IDataScienceFileSystem, + INotebookEditor, + INotebookEditorProvider, + INotebookModel, + INotebookProvider +} from '../../client/datascience/types'; +import { ClassType, IServiceContainer } from '../../client/ioc/types'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { IMountedWebView, WaitForMessageOptions } from './mountedWebView'; +import { mountConnectedMainPanel } from './testHelpers'; + +export interface ITestNativeEditorProvider extends INotebookEditorProvider { + getMountedWebView(window: INotebookEditor | undefined): IMountedWebView; + waitForMessage(file: Uri | undefined, message: string, options?: WaitForMessageOptions): Promise<void>; + getCustomDocument(file: Uri): CustomDocument | undefined; +} + +// Mixin class to provide common functionality between the two different native editor providers. +function TestNativeEditorProviderMixin<T extends ClassType<NativeEditorProvider>>(SuperClass: T) { + return class extends SuperClass implements ITestNativeEditorProvider { + private windowToMountMap = new Map<string, IMountedWebView>(); + private pendingMessageWaits: { + message: string; + options?: WaitForMessageOptions; + deferred: Deferred<void>; + }[] = []; + + // tslint:disable-next-line: no-any + constructor(...rest: any[]) { + super(...rest); + } + public getMountedWebView(window: INotebookEditor | undefined): IMountedWebView { + const key = window ? window.file.toString() : this.editors[0].file.toString(); + if (!this.windowToMountMap.has(key)) { + throw new Error('Test Failure: Window not mounted yet.'); + } + return this.windowToMountMap.get(key)!; + } + public waitForMessage(file: Uri | undefined, message: string, options?: WaitForMessageOptions): Promise<void> { + // We may already have this editor. Check + const key = file ? file.toString() : undefined; + if (key && this.windowToMountMap.has(key)) { + return this.windowToMountMap.get(key)!.waitForMessage(message, options); + } + + // Otherwise pend for the next create. + this.pendingMessageWaits.push({ message, options, deferred: createDeferred() }); + return this.pendingMessageWaits[this.pendingMessageWaits.length - 1].deferred.promise; + } + + public getCustomDocument(file: Uri) { + return this.customDocuments.get(file.fsPath); + } + + protected createNotebookEditor(model: INotebookModel, panel?: WebviewPanel): NativeEditor { + // Generate the mount wrapper using a custom id + const id = uuid(); + const mounted = this.ioc!.createWebView(() => mountConnectedMainPanel('native'), id); + + // Might have a pending wait for message + if (this.pendingMessageWaits.length) { + const list = [...this.pendingMessageWaits]; + this.pendingMessageWaits = []; + list.forEach((p) => { + mounted + .waitForMessage(p.message, p.options) + .then(() => { + p.deferred.resolve(); + }) + .catch((e) => p.deferred.reject(e)); + }); + } + + // Create the real editor. + const result = super.createNotebookEditor(model, panel); + + // Associate the real create with our mount in order to find the wrapper + const key = result.file.toString(); + this.windowToMountMap.set(key, mounted); + mounted.onDisposed(() => this.windowToMountMap.delete(key)); + + // During testing the MainPanel sends the init message before our interactive window is created. + // Pretend like it's happening now + // tslint:disable-next-line: no-any + const listener = (result as any).messageListener as InteractiveWindowMessageListener; + listener.onMessage(InteractiveWindowMessages.Started, {}); + + // Also need the css request so that other messages can go through + const webHost = result as NativeEditor; + webHost.setTheme(false); + + return result; + } + private get ioc(): DataScienceIocContainer | undefined { + return this.serviceContainer.get<DataScienceIocContainer>(DataScienceIocContainer); + } + }; +} + +@injectable() +export class TestNativeEditorProvider extends TestNativeEditorProviderMixin(NativeEditorProvider) { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(IWorkspaceService) workspace: IWorkspaceService, + @inject(IConfigurationService) configuration: IConfigurationService, + @inject(ICustomEditorService) customEditorService: ICustomEditorService, + @inject(INotebookStorageProvider) storage: INotebookStorageProvider, + @inject(INotebookProvider) notebookProvider: INotebookProvider, + @inject(IDataScienceFileSystem) fs: IDataScienceFileSystem + ) { + super( + serviceContainer, + asyncRegistry, + disposables, + workspace, + configuration, + customEditorService, + storage, + notebookProvider, + fs + ); + } +} + +@injectable() +export class TestNativeEditorProviderOld extends TestNativeEditorProviderMixin(NativeEditorProviderOld) { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(IWorkspaceService) workspace: IWorkspaceService, + @inject(IConfigurationService) configuration: IConfigurationService, + @inject(ICustomEditorService) customEditorService: ICustomEditorService, + @inject(IDataScienceFileSystem) fs: IDataScienceFileSystem, + @inject(IDocumentManager) documentManager: IDocumentManager, + @inject(ICommandManager) cmdManager: ICommandManager, + @inject(IDataScienceErrorHandler) dataScienceErrorHandler: IDataScienceErrorHandler, + @inject(INotebookStorageProvider) storage: INotebookStorageProvider, + @inject(INotebookProvider) notebookProvider: INotebookProvider + ) { + super( + serviceContainer, + asyncRegistry, + disposables, + workspace, + configuration, + customEditorService, + fs, + documentManager, + cmdManager, + dataScienceErrorHandler, + storage, + notebookProvider + ); + } +} diff --git a/src/test/datascience/testPersistentStateFactory.ts b/src/test/datascience/testPersistentStateFactory.ts new file mode 100644 index 000000000000..da364bb4a88d --- /dev/null +++ b/src/test/datascience/testPersistentStateFactory.ts @@ -0,0 +1,53 @@ +import { Memento } from 'vscode'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { IPersistentState, IPersistentStateFactory } from '../../client/common/types'; + +const PrefixesToStore = ['INTERPRETERS_CACHE']; + +// tslint:disable-next-line: no-any +const persistedState = new Map<string, any>(); + +class TestPersistentState<T> implements IPersistentState<T> { + constructor(private key: string, defaultValue?: T | undefined) { + if (defaultValue) { + persistedState.set(key, defaultValue); + } + } + public get value(): T { + return persistedState.get(this.key); + } + public async updateValue(value: T): Promise<void> { + persistedState.set(this.key, value); + } +} + +// This class is used to make certain values persist across tests. +export class TestPersistentStateFactory implements IPersistentStateFactory { + private realStateFactory: PersistentStateFactory; + constructor(globalState: Memento, localState: Memento) { + this.realStateFactory = new PersistentStateFactory(globalState, localState); + } + + public createGlobalPersistentState<T>( + key: string, + defaultValue?: T | undefined, + expiryDurationMs?: number | undefined + ): IPersistentState<T> { + if (PrefixesToStore.find((p) => key.startsWith(p))) { + return new TestPersistentState(key, defaultValue); + } + + return this.realStateFactory.createGlobalPersistentState(key, defaultValue, expiryDurationMs); + } + public createWorkspacePersistentState<T>( + key: string, + defaultValue?: T | undefined, + expiryDurationMs?: number | undefined + ): IPersistentState<T> { + if (PrefixesToStore.find((p) => key.startsWith(p))) { + return new TestPersistentState(key, defaultValue); + } + + return this.realStateFactory.createWorkspacePersistentState(key, defaultValue, expiryDurationMs); + } +} diff --git a/src/test/datascience/testexecutionLogger.ts b/src/test/datascience/testexecutionLogger.ts new file mode 100644 index 000000000000..42a92ea9ac4f --- /dev/null +++ b/src/test/datascience/testexecutionLogger.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { injectable } from 'inversify'; +import { traceInfo } from '../../client/common/logger'; +import { noop } from '../../client/common/utils/misc'; +import { traceCellResults } from '../../client/datascience/common'; +import { ICell, INotebookExecutionLogger } from '../../client/datascience/types'; +import { concatMultilineString } from '../../datascience-ui/common'; + +@injectable() +export class TestExecutionLogger implements INotebookExecutionLogger { + public dispose() { + noop(); + } + public preExecute(cell: ICell, _silent: boolean): Promise<void> { + traceInfo(`Cell Execution for ${cell.id} : \n${concatMultilineString(cell.data.source)}\n`); + return Promise.resolve(); + } + public postExecute(cell: ICell, _silent: boolean): Promise<void> { + traceCellResults(`Cell Execution complete for ${cell.id}\n`, [cell]); + return Promise.resolve(); + } + public onKernelRestarted(): void { + // Can ignore this. + } +} diff --git a/src/test/datascience/trustedNotebooks.functional.test.tsx b/src/test/datascience/trustedNotebooks.functional.test.tsx new file mode 100644 index 000000000000..57d5daef5269 --- /dev/null +++ b/src/test/datascience/trustedNotebooks.functional.test.tsx @@ -0,0 +1,449 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { assert, expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { ReactWrapper } from 'enzyme'; +import * as fs from 'fs-extra'; +import { Disposable } from 'vscode'; +import { noop } from '../../client/common/utils/misc'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { INotebookEditor, INotebookEditorProvider, ITrustService } from '../../client/datascience/types'; +import { CommonActionType } from '../../datascience-ui/interactive-common/redux/reducers/types'; +import { TrustMessage } from '../../datascience-ui/interactive-common/trustMessage'; +import { NativeCell } from '../../datascience-ui/native-editor/nativeCell'; +import { NativeEditor } from '../../datascience-ui/native-editor/nativeEditor'; +import { IKeyboardEvent } from '../../datascience-ui/react-common/event'; +import { ImageButton } from '../../datascience-ui/react-common/imageButton'; +import { MonacoEditor } from '../../datascience-ui/react-common/monacoEditor'; +import { createTemporaryFile } from '../utils/fs'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { IMountedWebView, WaitForMessageOptions } from './mountedWebView'; +import { closeNotebook, openEditor } from './nativeEditorTestHelpers'; +import { + addMockData, + enterEditorKey, + findButton, + getNativeFocusedEditor, + getOutputCell, + isCellFocused, + isCellMarkdown, + isCellSelected, + typeCode, + verifyHtmlOnCell +} from './testHelpers'; +import { ITestNativeEditorProvider } from './testNativeEditorProvider'; + +use(chaiAsPromised); + +function waitForMessage(ioc: DataScienceIocContainer, message: string, options?: WaitForMessageOptions): Promise<void> { + return ioc + .get<ITestNativeEditorProvider>(INotebookEditorProvider) + .getMountedWebView(undefined) + .waitForMessage(message, options); +} +// tslint:disable:no-any no-multiline-string +suite('Notebook trust', () => { + let wrapper: ReactWrapper<any, Readonly<{}>, React.Component>; + let ne: { editor: INotebookEditor; mount: IMountedWebView }; + const disposables: Disposable[] = []; + let ioc: DataScienceIocContainer; + const baseFile = ` +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a=1\\n", + "a" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b=2\\n", + "b" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c=3\\n", + "c" + ] + } + ], + "metadata": { + "file_extension": ".py", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.4" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +}`; + const addedJSON = JSON.parse(baseFile); + addedJSON.cells.splice(3, 0, { + cell_type: 'code', + execution_count: null, + metadata: {}, + outputs: [], + source: ['a'] + }); + + let notebookFile: { + filePath: string; + cleanupCallback: Function; + }; + function initIoc() { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(false); + ioc.forceDataScienceSettingsChanged({ alwaysTrustNotebooks: false }); + return ioc.activate(); + } + function simulateKeyPressOnCell(cellIndex: number, keyboardEvent: Partial<IKeyboardEvent> & { code: string }) { + // Check to see if we have an active focused editor + const editor = getNativeFocusedEditor(wrapper); + + // If we do have one, send the input there, otherwise send it to the outer cell + if (editor) { + simulateKeyPressOnEditor(editor, keyboardEvent); + } else { + simulateKeyPressOnCellInner(cellIndex, keyboardEvent); + } + } + function simulateKeyPressOnEditor( + editorControl: ReactWrapper<any, Readonly<{}>, React.Component> | undefined, + keyboardEvent: Partial<IKeyboardEvent> & { code: string } + ) { + enterEditorKey(editorControl, keyboardEvent); + } + + function simulateKeyPressOnCellInner(cellIndex: number, keyboardEvent: Partial<IKeyboardEvent> & { code: string }) { + wrapper.update(); + let nativeCell = wrapper.find(NativeCell).at(cellIndex); + if (nativeCell.exists()) { + nativeCell.simulate('keydown', { + key: keyboardEvent.code, + shiftKey: keyboardEvent.shiftKey, + ctrlKey: keyboardEvent.ctrlKey, + altKey: keyboardEvent.altKey, + metaKey: keyboardEvent.metaKey + }); + } + wrapper.update(); + // Requery for our cell as something like a 'dd' keydown command can delete it before the press and up + nativeCell = wrapper.find(NativeCell).at(cellIndex); + if (nativeCell.exists()) { + nativeCell.simulate('keypress', { + key: keyboardEvent.code, + shiftKey: keyboardEvent.shiftKey, + ctrlKey: keyboardEvent.ctrlKey, + altKey: keyboardEvent.altKey, + metaKey: keyboardEvent.metaKey + }); + } + nativeCell = wrapper.find(NativeCell).at(cellIndex); + wrapper.update(); + if (nativeCell.exists()) { + nativeCell.simulate('keyup', { + key: keyboardEvent.code, + shiftKey: keyboardEvent.shiftKey, + ctrlKey: keyboardEvent.ctrlKey, + altKey: keyboardEvent.altKey, + metaKey: keyboardEvent.metaKey + }); + } + wrapper.update(); + } + + async function setupFunction(this: Mocha.Context) { + addMockData(ioc, 'b=2\nb', 2); + addMockData(ioc, 'c=3\nc', 3); + // Use a real file so we can save notebook to a file. + // This is used in some tests (saving). + notebookFile = await createTemporaryFile('.ipynb'); + await fs.writeFile(notebookFile.filePath, baseFile); + ne = await openEditor(ioc, baseFile, notebookFile.filePath); + wrapper = ne.mount.wrapper; + } + + function clickCell(cellIndex: number) { + wrapper.update(); + wrapper.find(NativeCell).at(cellIndex).simulate('click'); + wrapper.update(); + } + + async function focusCell(targetCellIndex: number) { + const update = waitForMessage(ioc, InteractiveWindowMessages.FocusedCellEditor); + clickCell(targetCellIndex); + simulateKeyPressOnCell(targetCellIndex, { code: 'Enter', editorInfo: undefined }); + await update; + assert.ok(isCellFocused(wrapper, 'NativeCell', targetCellIndex)); + } + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + await ioc.dispose(); + try { + notebookFile.cleanupCallback(); + } catch { + noop(); + } + }); + + setup(async function () { + await initIoc(); + // tslint:disable-next-line: no-invalid-this + await setupFunction.call(this); + }); + + suite('Open an untrusted notebook', async () => { + test('Outputs are not rendered', () => { + // No outputs should have rendered + assert.throws(() => verifyHtmlOnCell(wrapper, 'NativeCell', '<span>1</span>', 0)); + assert.throws(() => verifyHtmlOnCell(wrapper, 'NativeCell', '<span>2</span>', 1)); + assert.throws(() => verifyHtmlOnCell(wrapper, 'NativeCell', '<span>3</span>', 2)); + }); + test('Cannot edit cell contents', async () => { + await focusCell(0); + + // Try to type code + const editorEnzyme = getNativeFocusedEditor(wrapper); + typeCode(editorEnzyme, 'foo'); + const reactEditor = editorEnzyme!.instance() as MonacoEditor; + const editor = reactEditor.state.editor; + if (editor) { + assert.notInclude(editor.getModel()!.getValue(), 'foo', 'Was able to edit cell in untrusted notebook'); + } + }); + + suite('Buttons are disabled', async () => { + test('Cannot run cell', async () => { + // Click run cell button + const cell = getOutputCell(wrapper, 'NativeCell', 1); + const imageButtons = cell!.find(ImageButton); + const runButton = imageButtons.findWhere((w) => w.props().tooltip === 'Run cell'); + runButton.simulate('click'); + + // Ensure cell was not executed + assert.throws(() => verifyHtmlOnCell(wrapper, 'NativeCell', `2`, 1)); + }); + test('Cannot switch to markdown', async () => { + // Click switch to markdown button + const cell = getOutputCell(wrapper, 'NativeCell', 1); + const imageButtons = cell!.find(ImageButton); + const changeToMarkdown = imageButtons.findWhere((w) => w.props().tooltip === 'Change to markdown'); + changeToMarkdown.simulate('click'); + + // Ensure cell is still code cell + assert.isFalse(isCellMarkdown(wrapper, 'NativeCell', 1)); + }); + test('Cannot insert cell into notebook', async () => { + // Click insert cell button + const insertCellButton = findButton(wrapper, NativeEditor, 5); + insertCellButton?.simulate('click'); + + // No cell should have been added + assert.equal(wrapper.find('NativeCell').length, 3, 'Cell added'); + }); + }); + + suite('Jupyter shortcuts for editing notebook are disabled', async () => { + test('Ctrl+enter does not execute cell', async () => { + const cellIndex = 1; + await focusCell(cellIndex); + + const promise = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { timeoutMs: 5_000 }); + simulateKeyPressOnCell(cellIndex, { code: 'Enter', ctrlKey: true, editorInfo: undefined }); + + // Waiting for an execution rendered message should timeout + await expect(promise).to.eventually.be.rejected; + // No output should have been rendered + assert.throws(() => verifyHtmlOnCell(wrapper, 'NativeCell', '<span>2</span>', cellIndex)); + }); + test('Shift+enter does not execute cell or advance to next cell', async () => { + const cellIndex = 1; + await focusCell(cellIndex); + + const promise = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { timeoutMs: 5_000 }); + simulateKeyPressOnCell(cellIndex, { code: 'Enter', shiftKey: true, editorInfo: undefined }); + + // Waiting for an execution rendered message should timeout + await expect(promise).to.eventually.be.rejected; + // No output should have been rendered + assert.throws(() => verifyHtmlOnCell(wrapper, 'NativeCell', '<span>2</span>', cellIndex)); + // 3rd cell should be neither selected nor focused + assert.isFalse(isCellSelected(wrapper, 'NativeCell', cellIndex + 1)); + assert.isFalse(isCellFocused(wrapper, 'NativeCell', cellIndex + 1)); + }); + test('Alt+enter does not execute cell or add a new cell below', async () => { + assert.equal(wrapper.find('NativeCell').length, 3); + const cellIndex = 1; + await focusCell(cellIndex); + + const promise = waitForMessage(ioc, InteractiveWindowMessages.ExecutionRendered, { timeoutMs: 5_000 }); + simulateKeyPressOnCell(1, { code: 'Enter', altKey: true, editorInfo: undefined }); + + // Waiting for an execution rendered message should timeout + await expect(promise).to.eventually.be.rejected; + // No output should have been rendered + assert.throws(() => verifyHtmlOnCell(wrapper, 'NativeCell', '<span>2</span>', cellIndex)); + // No cell should have been added + assert.equal(wrapper.find('NativeCell').length, 3, 'Cell added'); + }); + test('"a" does not add a cell', async () => { + assert.equal(wrapper.find('NativeCell').length, 3); + const cellIndex = 0; + await focusCell(cellIndex); + + const addedCell = waitForMessage(ioc, CommonActionType.INSERT_ABOVE_AND_FOCUS_NEW_CELL, { + timeoutMs: 5_000 + }); + const update = waitForMessage(ioc, InteractiveWindowMessages.SelectedCell, { timeoutMs: 5_000 }); + simulateKeyPressOnCell(cellIndex, { code: 'a' }); + + await expect(addedCell).to.eventually.be.rejected; + await expect(update).to.eventually.be.rejected; + // There should still be 3 cells. + assert.equal(wrapper.find('NativeCell').length, 3); + }); + test('"b" does not add a cell', async () => { + assert.equal(wrapper.find('NativeCell').length, 3); + const cellIndex = 0; + await focusCell(cellIndex); + + const addedCell = waitForMessage(ioc, CommonActionType.INSERT_BELOW_AND_FOCUS_NEW_CELL, { + timeoutMs: 5_000 + }); + const update = waitForMessage(ioc, InteractiveWindowMessages.SelectedCell, { timeoutMs: 5_000 }); + simulateKeyPressOnCell(cellIndex, { code: 'b' }); + + await expect(addedCell).to.eventually.be.rejected; + await expect(update).to.eventually.be.rejected; + // There should still be 3 cells. + assert.equal(wrapper.find('NativeCell').length, 3); + }); + test('"d" does not delete a cell', async () => { + assert.equal(wrapper.find('NativeCell').length, 3); + const cellIndex = 2; + await focusCell(cellIndex); + + simulateKeyPressOnCell(cellIndex, { code: 'd' }); + simulateKeyPressOnCell(cellIndex, { code: 'd' }); + + // There should still be 3 cells. + assert.equal(wrapper.find('NativeCell').length, 3); + }); + test('"m" does not change a code cell to markdown', async () => { + const cellIndex = 2; + await focusCell(cellIndex); + + const update = waitForMessage(ioc, CommonActionType.CHANGE_CELL_TYPE, { + timeoutMs: 5_000 + }); + simulateKeyPressOnCell(cellIndex, { code: 'm' }); + + await expect(update).to.eventually.be.rejected; + assert.isFalse( + isCellMarkdown(wrapper, 'NativeCell', cellIndex), + 'Code cell in untrusted notebook was changed to markdown' + ); + }); + }); + }); + + suite('Trust an untrusted notebook', async () => { + test('Trust persists when closed and reopened', async () => { + const before = wrapper.find(TrustMessage); + assert.equal(before.text(), 'Not Trusted'); + + // Trust notebook + const trustService = ioc.get<ITrustService>(ITrustService); + await trustService.trustNotebook(ne.editor.model.file, ne.editor.model.getContent()); + + // Close + await closeNotebook(ioc, ne.editor); + + // Reopen + const newNativeEditor = await openEditor(ioc, baseFile, notebookFile.filePath); + const newWrapper = newNativeEditor.mount.wrapper; + + // Verify notebook is now trusted + const after = newWrapper.find(TrustMessage); + assert.equal(after.text(), 'Trusted'); + }); + }); +}); diff --git a/src/test/datascience/uiTests/helpers.ts b/src/test/datascience/uiTests/helpers.ts new file mode 100644 index 000000000000..b6cd14e2ceca --- /dev/null +++ b/src/test/datascience/uiTests/helpers.ts @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as playwright from 'playwright-chromium'; +import { IAsyncDisposable, IDisposable } from '../../../client/common/types'; +import { createDeferred } from '../../../client/common/utils/async'; +import { InteractiveWindowMessages } from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { CssMessages } from '../../../client/datascience/messages'; +import { CommonActionType } from '../../../datascience-ui/interactive-common/redux/reducers/types'; +import { IWebServer } from './webBrowserPanel'; + +// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +export type WaitForMessageOptions = { + /** + * Timeout for waiting for message. + * Defaults to 65_000ms. + * + * @type {number} + */ + timeoutMs?: number; + /** + * Number of times the message should be received. + * Defaults to 1. + * + * @type {number} + */ + numberOfTimes?: number; +}; + +const maxWaitTimeForMessage = 75_000; +/** + * UI could take a while to update, could be slower on CI server. + * (500ms is generally enough, but increasing to 3s to avoid flaky CI tests). + */ +export const waitTimeForUIToUpdate = 10_000; + +export class BaseWebUI implements IAsyncDisposable { + public page?: playwright.Page; + private readonly disposables: IDisposable[] = []; + private readonly webServerPromise = createDeferred<IWebServer>(); + private webServer?: IWebServer; + private browser?: playwright.ChromiumBrowser; + public async dispose() { + while (this.disposables.length) { + this.disposables.shift()?.dispose(); // NOSONAR + } + await this.browser?.close(); + await this.page?.close(); + } + public async type(text: string): Promise<void> { + await this.page?.keyboard.type(text); + } + public _setWebServer(webServer: IWebServer) { + this.webServer = webServer; + this.webServerPromise.resolve(webServer); + } + public async waitUntilLoaded(): Promise<void> { + await this.webServerPromise.promise.then(() => + // The UI is deemed loaded when we have seen all of the following messages. + // We cannot guarantee the order of these messages, however they are all part of the load process. + Promise.all([ + this.waitForMessage(InteractiveWindowMessages.LoadAllCellsComplete), + this.waitForMessage(InteractiveWindowMessages.LoadAllCells), + this.waitForMessage(InteractiveWindowMessages.MonacoReady), // Sometimes the last thing to happen. + this.waitForMessage(InteractiveWindowMessages.SettingsUpdated), + this.waitForMessage(CommonActionType.EDITOR_LOADED), + this.waitForMessage(CommonActionType.CODE_CREATED), // When a cell has been created. + this.waitForMessage(CssMessages.GetMonacoThemeResponse), + this.waitForMessage(CssMessages.GetCssResponse), + this.page?.waitForLoadState() + ]) + ); + } + + public waitForMessageAfterServer(message: string, options?: WaitForMessageOptions): Promise<void> { + return this.webServerPromise.promise.then(() => this.waitForMessage(message, options)); + } + public async waitForMessage(message: string, options?: WaitForMessageOptions): Promise<void> { + if (!this.webServer) { + throw new Error('WebServer not yet started'); + } + const timeoutMs = options && options.timeoutMs ? options.timeoutMs : maxWaitTimeForMessage; + const numberOfTimes = options && options.numberOfTimes ? options.numberOfTimes : 1; + // Wait for the mounted web panel to send a message back to the data explorer + const promise = createDeferred<void>(); + const timer = timeoutMs + ? setTimeout(() => { + if (!promise.resolved) { + promise.reject(new Error(`Waiting for ${message} timed out`)); + } + }, timeoutMs) + : undefined; + let timesMessageReceived = 0; + const dispatchedAction = `DISPATCHED_ACTION_${message}`; + const disposable = this.webServer.onDidReceiveMessage((msg) => { + const messageType = msg.type; + if (messageType !== message && messageType !== dispatchedAction) { + return; + } + timesMessageReceived += 1; + if (timesMessageReceived < numberOfTimes) { + return; + } + if (timer) { + clearTimeout(timer); + } + disposable.dispose(); + if (messageType === message) { + promise.resolve(); + } else { + // It could be a redux dispatched message. + // Wait for 10ms, wait for other stuff to finish. + // We can wait for 100ms or 1s. But thats too long. + // The assumption is that currently we do not have any setTimeouts + // in UI code that's in the magnitude of 100ms or more. + // We do have a couple of setTimeout's, but they wait for 1ms, not 100ms. + // 10ms more than sufficient for all the UI timeouts. + setTimeout(() => promise.resolve(), 10); + } + }); + + return promise.promise; + } + /** + * Opens a browser an loads the webpage, effectively loading the UI. + */ + public async loadUI(url: string) { + // Configure to display browser while debugging. + const openBrowser = process.env.VSC_PYTHON_DS_UI_BROWSER !== undefined; + this.browser = await playwright.chromium.launch({ headless: !openBrowser, devtools: openBrowser }); + await this.browser.newContext(); + this.page = await this.browser.newPage(); + await this.page.goto(url); + } + + public async captureScreenshot(filePath: string): Promise<void> { + if (!(await fs.pathExists(path.basename(filePath)))) { + await fs.ensureDir(path.basename(filePath)); + } + await this.page?.screenshot({ path: filePath }); + // tslint:disable-next-line: no-console + console.info(`Screenshot captured in ${filePath}`); + } +} diff --git a/src/test/datascience/uiTests/ipywidget.ui.functional.test.ts b/src/test/datascience/uiTests/ipywidget.ui.functional.test.ts new file mode 100644 index 000000000000..f6916d491134 --- /dev/null +++ b/src/test/datascience/uiTests/ipywidget.ui.functional.test.ts @@ -0,0 +1,616 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable: no-var-requires no-require-imports no-invalid-this no-any no-invalid-this no-console + +import { nbformat } from '@jupyterlab/coreutils'; +import { assert, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Disposable } from 'vscode'; +import { LocalZMQKernel } from '../../../client/common/experiments/groups'; +import { sleep } from '../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { retryIfFail as retryIfFailOriginal } from '../../common'; +import { mockedVSCodeNamespaces } from '../../vscode-mock'; +import { DataScienceIocContainer } from '../dataScienceIocContainer'; +import { addMockData } from '../testHelpersCore'; +import { waitTimeForUIToUpdate } from './helpers'; +import { openNotebook } from './notebookHelpers'; +import { NotebookEditorUI } from './notebookUi'; + +const sanitize = require('sanitize-filename'); +// Include default timeout. +const retryIfFail = <T>(fn: () => Promise<T>) => retryIfFailOriginal<T>(fn, waitTimeForUIToUpdate); + +use(chaiAsPromised); + +[false, true].forEach((useRawKernel) => { + //import { asyncDump } from '../common/asyncDump'; + suite(`DataScience IPyWidgets (${useRawKernel ? 'With Direct Kernel' : 'With Jupyter Server'})`, () => { + const disposables: Disposable[] = []; + let ioc: DataScienceIocContainer; + + suiteSetup(function () { + // These are UI tests, hence nothing to do with platforms. + this.timeout(30_000); // UI Tests, need time to start jupyter. + this.retries(3); // UI tests can be flaky. + if (!process.env.VSCODE_PYTHON_ROLLING) { + // Skip all tests unless using real jupyter + this.skip(); + } + }); + setup(async function () { + ioc = new DataScienceIocContainer(true); + ioc.setExtensionRootPath(EXTENSION_ROOT_DIR); + if (ioc.mockJupyter && useRawKernel) { + // tslint:disable-next-line: no-invalid-this + this.skip(); + } else { + ioc.setExperimentState(LocalZMQKernel.experiment, useRawKernel); + } + + ioc.registerDataScienceTypes(); + + // Make sure we force auto start (we wait for kernel idle before running) + ioc.forceSettingsChanged(undefined, ioc.getSettings().pythonPath, { + ...ioc.getSettings().datascience, + disableJupyterAutoStart: false + }); + + await ioc.activate(); + }); + teardown(async () => { + sinon.restore(); + mockedVSCodeNamespaces.window?.reset(); + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + await ioc.dispose(); + mockedVSCodeNamespaces.window?.reset(); + }); + let notebookUi: NotebookEditorUI; + teardown(async function () { + if (this.test && this.test.state === 'failed') { + const imageName = `${sanitize(this.currentTest?.title)}.png`; + await notebookUi.captureScreenshot(path.join(os.tmpdir(), 'tmp', 'screenshots', imageName)); + } + }); + function getIpynbFilePath(fileName: string) { + return path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience', 'uiTests', 'notebooks', fileName); + } + async function openNotebookFile(ipynbFile: string) { + const fileContents = await fs.readFile(getIpynbFilePath(ipynbFile), 'utf8'); + // Remove kernel information (in tests, use the current environment), ignore what others used. + const nb = JSON.parse(fileContents) as nbformat.INotebookContent; + if (nb.metadata && nb.metadata.kernelspec) { + delete nb.metadata.kernelspec; + } + // Clear all output (from previous executions). + nb.cells.forEach((cell) => { + if (Array.isArray(cell.outputs)) { + cell.outputs = []; + } + }); + const result = await openNotebook(ioc, disposables, JSON.stringify(nb)); + notebookUi = result.notebookUI; + return result; + } + async function openABCIpynb() { + addMockData(ioc, 'a=1\na', 1); + addMockData(ioc, 'b=2\nb', 2); + addMockData(ioc, 'c=3\nc', 3); + return openNotebookFile('simple_abc.ipynb'); + } + async function openStandardWidgetsIpynb() { + return openNotebookFile('standard_widgets.ipynb'); + } + async function openIPySheetsIpynb() { + return openNotebookFile('ipySheet_widgets.ipynb'); + } + async function openBeakerXIpynb() { + return openNotebookFile('beakerx_widgets.ipynb'); + } + async function openK3DIpynb() { + return openNotebookFile('k3d_widgets.ipynb'); + } + async function openBqplotIpynb() { + return openNotebookFile('bqplot_widgets.ipynb'); + } + async function openIPyVolumeIpynb() { + return openNotebookFile('ipyvolume_widgets.ipynb'); + } + async function openPyThreejsIpynb() { + return openNotebookFile('pythreejs_widgets.ipynb'); + } + async function openOutputAndInteractIpynb() { + return openNotebookFile('outputinteract_widgets.ipynb'); + } + + test('Notebook has 3 cells', async () => { + const { notebookUI } = await openABCIpynb(); + await retryIfFail(async () => { + const count = await notebookUI.getCellCount(); + assert.equal(count, 3); + }); + }); + test('Output displayed after executing a cell', async () => { + const { notebookUI } = await openABCIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(0)); + + await notebookUI.executeCell(0); + + await retryIfFail(async () => { + await assert.eventually.isTrue(notebookUI.cellHasOutput(0)); + const outputHtml = await notebookUI.getCellOutputHTML(0); + assert.include(outputHtml, '<span>1</span>'); + }); + }); + + async function openNotebookAndTestSliderWidget() { + const result = await openStandardWidgetsIpynb(); + const notebookUI = result.notebookUI; + await assert.eventually.isFalse(notebookUI.cellHasOutput(0)); + + await verifySliderWidgetIsAvailableAfterExecution(notebookUI); + + return result; + } + async function verifySliderWidgetIsAvailableAfterExecution(notebookUI: NotebookEditorUI) { + await notebookUI.executeCell(0); + + // Slider output could take a bit. Wait some + await sleep(2000); + + await retryIfFail(async () => { + await assert.eventually.isTrue(notebookUI.cellHasOutput(0)); + const outputHtml = await notebookUI.getCellOutputHTML(0); + + // Should not contain the string representation of widget (rendered when ipywidgets wasn't supported). + // We should only render widget not string representation. + assert.notInclude(outputHtml, 'IntSlider(value=0)'); + + // Ensure Widget HTML exists + assert.include(outputHtml, 'jupyter-widgets'); + assert.include(outputHtml, 'ui-slider'); + assert.include(outputHtml, '<div class="ui-slider'); + }); + } + test('Slider Widget', openNotebookAndTestSliderWidget); + test('Text Widget', async () => { + const { notebookUI } = await openStandardWidgetsIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(1)); + + await notebookUI.executeCell(1); + + await retryIfFail(async () => { + await assert.eventually.isTrue(notebookUI.cellHasOutput(1)); + const outputHtml = await notebookUI.getCellOutputHTML(1); + + // Ensure Widget HTML exists + assert.include(outputHtml, 'jupyter-widgets'); + assert.include(outputHtml, 'widget-text'); + assert.include(outputHtml, '<input type="text'); + }); + }); + test('Checkbox Widget', async () => { + const { notebookUI } = await openStandardWidgetsIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(2)); + + await notebookUI.executeCell(2); + + await retryIfFail(async () => { + await assert.eventually.isTrue(notebookUI.cellHasOutput(2)); + const outputHtml = await notebookUI.getCellOutputHTML(2); + + // Ensure Widget HTML exists + assert.include(outputHtml, 'jupyter-widgets'); + assert.include(outputHtml, 'widget-checkbox'); + assert.include(outputHtml, '<input type="checkbox'); + }); + }); + test('Render ipysheets', async () => { + const { notebookUI } = await openIPySheetsIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(3)); + + await notebookUI.executeCell(1); + await notebookUI.executeCell(3); + + await retryIfFail(async () => { + const cellOutput = await notebookUI.getCellOutputHTML(3); + + // Confirm cells with output has been rendered. + assert.include(cellOutput, 'Hello</td>'); + assert.include(cellOutput, 'World</td>'); + }); + }); + test('Widget renders after closing and re-opening notebook', async () => { + const result = await openNotebookAndTestSliderWidget(); + + await result.notebookUI.page?.close(); + await result.webViewPanel.dispose(); + + // Open the same notebook again and test. + await openNotebookAndTestSliderWidget(); + }); + test('Widget renders after restarting kernel', async () => { + const { notebookUI, notebookEditor } = await openNotebookAndTestSliderWidget(); + + // Clear the output + await notebookUI.clearOutput(); + await retryIfFail(async () => notebookUI.cellHasOutput(0)); + + // Restart the kernel. + await notebookEditor.restartKernel(); + + // Execute cell again and verify output is displayed. + await verifySliderWidgetIsAvailableAfterExecution(notebookUI); + }); + test('Widget renders after interrupting kernel', async () => { + const { notebookUI, notebookEditor } = await openNotebookAndTestSliderWidget(); + + // Clear the output + await notebookUI.clearOutput(); + await retryIfFail(async () => notebookUI.cellHasOutput(0)); + + // Restart the kernel. + await notebookEditor.interruptKernel(); + + // Execute cell again and verify output is displayed. + await verifySliderWidgetIsAvailableAfterExecution(notebookUI); + }); + test('Button Interaction across Cells', async () => { + const { notebookUI } = await openStandardWidgetsIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(3)); + await assert.eventually.isFalse(notebookUI.cellHasOutput(4)); + + await notebookUI.executeCell(3); + await notebookUI.executeCell(4); + + const button = await retryIfFail(async () => { + // Find the button & the lable in cell output for 3 & 4 respectively. + const buttons = await (await notebookUI.getCellOutput(3)).$$('button.widget-button'); + const cell4Output = await notebookUI.getCellOutputHTML(4); + + assert.equal(buttons.length, 1, 'No button'); + assert.include(cell4Output, 'Not Clicked'); + + return buttons[0]; + }); + + // When we click the button, the text in the label will get updated (i.e. output in Cell 4 will be udpated). + await button.click(); + + await retryIfFail(async () => { + const cell4Output = await notebookUI.getCellOutputHTML(4); + assert.include(cell4Output, 'Button Clicked'); + }); + }); + test('Search ipysheets with textbox in another cell', async () => { + const { notebookUI } = await openIPySheetsIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(6)); + await assert.eventually.isFalse(notebookUI.cellHasOutput(7)); + + await notebookUI.executeCell(5); + await notebookUI.executeCell(6); + await notebookUI.executeCell(7); + + // Wait for sheets to get rendered. + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(7); + + assert.include(cellOutputHtml, 'test</td>'); + assert.include(cellOutputHtml, 'train</td>'); + + const cellOutput = await notebookUI.getCellOutput(6); + const highlighted = await cellOutput.$$('td.htSearchResult'); + assert.equal(highlighted.length, 0); + }); + + // Type `test` into textbox. + await retryIfFail(async () => { + const cellOutput = await notebookUI.getCellOutput(6); + const textboxes = await cellOutput.$$('input[type=text]'); + assert.equal(textboxes.length, 1, 'No Texbox'); + await textboxes[0].focus(); + + await notebookUI.type('test'); + }); + + // Confirm cell is filtered and highlighted. + await retryIfFail(async () => { + const cellOutput = await notebookUI.getCellOutput(7); + const highlighted = await cellOutput.$$('td.htSearchResult'); + assert.equal(highlighted.length, 2); + }); + }); + test('Update ipysheets cells with textbox & slider in another cell', async () => { + const { notebookUI } = await openIPySheetsIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(10)); + await assert.eventually.isFalse(notebookUI.cellHasOutput(12)); + await assert.eventually.isFalse(notebookUI.cellHasOutput(13)); + + await notebookUI.executeCell(9); + await notebookUI.executeCell(10); + await notebookUI.executeCell(12); + await notebookUI.executeCell(13); + + // Wait for slider to get rendered with value `0`. + const sliderLabel = await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(10); + + assert.include(cellOutputHtml, 'ui-slider-handle'); + assert.include(cellOutputHtml, 'left: 0%'); + + const cellOutput = await notebookUI.getCellOutput(10); + const sliderLables = await cellOutput.$$('div.widget-readout'); + + return sliderLables[0]; + }); + + // Confirm slider lable reads `0`. + await retryIfFail(async () => { + const sliderValue = await notebookUI.page?.evaluate((ele) => ele.innerHTML.trim(), sliderLabel); + assert.equal(sliderValue || '', '0'); + }); + + // Wait for textbox to get rendered. + const textbox = await retryIfFail(async () => { + const cellOutput = await notebookUI.getCellOutput(12); + const textboxes = await cellOutput.$$('input[type=number]'); + assert.equal(textboxes.length, 1); + + const value = await notebookUI.page?.evaluate((el) => (el as HTMLInputElement).value, textboxes[0]); + assert.equal(value || '', '0'); + + return textboxes[0]; + }); + + // Wait for sheets to get rendered. + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(13); + assert.include(cellOutputHtml, '>50.000</td>'); + assert.notInclude(cellOutputHtml, '>100.000</td>'); + }); + + // Type `50` into textbox. + await retryIfFail(async () => { + await textbox.focus(); + await notebookUI.type('50'); + }); + + // Confirm slider label reads `50`. + await retryIfFail(async () => { + const sliderValue = await notebookUI.page?.evaluate((ele) => ele.innerHTML.trim(), sliderLabel); + assert.equal(sliderValue || '', '50'); + }); + + // Wait for sheets to get updated with calculation. + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(13); + + assert.include(cellOutputHtml, '>50.000</td>'); + assert.include(cellOutputHtml, '>100.000</td>'); + }); + }); + test('Render ipyvolume', async () => { + const { notebookUI } = await openIPyVolumeIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(3)); + + await notebookUI.executeCell(1); + await notebookUI.executeCell(2); + await notebookUI.executeCell(3); + await notebookUI.executeCell(4); + + // Confirm sliders and canvas are rendered. + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(1); + assert.include(cellOutputHtml, '<canvas '); + + const cellOutput = await notebookUI.getCellOutput(1); + const sliders = await cellOutput.$$('div.ui-slider'); + assert.equal(sliders.length, 2); + }); + + // Confirm canvas is rendered. + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(4); + assert.include(cellOutputHtml, '<canvas '); + }); + }); + test('Render pythreejs', async () => { + const { notebookUI } = await openPyThreejsIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(3)); + await assert.eventually.isFalse(notebookUI.cellHasOutput(8)); + + await notebookUI.executeCell(1); + await notebookUI.executeCell(2); + await notebookUI.executeCell(3); + await notebookUI.executeCell(4); + await notebookUI.executeCell(5); + await notebookUI.executeCell(6); + await notebookUI.executeCell(7); + await notebookUI.executeCell(8); + + // Confirm canvas is rendered. + await retryIfFail(async () => { + let cellOutputHtml = await notebookUI.getCellOutputHTML(3); + assert.include(cellOutputHtml, '<canvas '); + // Last cell is flakey. Can take too long to render. We need some way + // to know when a widget is done rendering. + cellOutputHtml = await notebookUI.getCellOutputHTML(8); + assert.include(cellOutputHtml, '<canvas '); + }); + }); + test('Render beakerx', async () => { + const { notebookUI } = await openBeakerXIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(1)); + await assert.eventually.isFalse(notebookUI.cellHasOutput(2)); + + await notebookUI.executeCell(1); + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(1); + // Confirm svg graph has been rendered. + assert.include(cellOutputHtml, '<svg'); + + // Confirm graph legened has been rendered. + const cellOutput = await notebookUI.getCellOutput(1); + const legends = await cellOutput.$$('div.plot-legend'); + assert.isAtLeast(legends.length, 1); + }); + + await notebookUI.executeCell(2); + await retryIfFail(async () => { + // Confirm graph modal dialog has been rendered. + const cellOutput = await notebookUI.getCellOutput(2); + const modals = await cellOutput.$$('div.modal-content'); + assert.isAtLeast(modals.length, 1); + }); + }); + test('Render bqplot', async () => { + const { notebookUI } = await openBqplotIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(2)); + await assert.eventually.isFalse(notebookUI.cellHasOutput(4)); + + await notebookUI.executeCell(1); + await notebookUI.executeCell(2); + + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(2); + // Confirm svg graph has been rendered. + assert.include(cellOutputHtml, '<svg'); + assert.include(cellOutputHtml, 'plotarea_events'); + }); + + // Render empty plot + await notebookUI.executeCell(4); + await retryIfFail(async () => { + const cellOutput = await notebookUI.getCellOutput(4); + // Confirm no points have been rendered. + const dots = await cellOutput.$$('path.dot'); + assert.equal(dots.length, 0); + }); + + // Draw points on previous plot. + await notebookUI.executeCell(5); + await retryIfFail(async () => { + const cellOutput = await notebookUI.getCellOutput(4); + // Confirm points have been rendered. + const dots = await cellOutput.$$('path.dot'); + assert.isAtLeast(dots.length, 1); + }); + + // Chage color of plot points to red. + await notebookUI.executeCell(7); + await retryIfFail(async () => { + const cellOutput = await notebookUI.getCellOutput(4); + const dots = await cellOutput.$$('path.dot'); + assert.isAtLeast(dots.length, 1); + const dotHtml = await notebookUI.page?.evaluate((ele) => ele.outerHTML, dots[0]); + // Confirm color of dot is red. + assert.include(dotHtml || '', 'red'); + }); + + // Chage color of plot points to red. + await notebookUI.executeCell(8); + await retryIfFail(async () => { + const cellOutput = await notebookUI.getCellOutput(4); + const dots = await cellOutput.$$('path.dot'); + assert.isAtLeast(dots.length, 1); + const dotHtml = await notebookUI.page?.evaluate((ele) => ele.outerHTML, dots[0]); + // Confirm color of dot is red. + assert.include(dotHtml || '', 'yellow'); + }); + }); + test('Render output and interact', async () => { + const { notebookUI } = await openOutputAndInteractIpynb(); + await notebookUI.executeCell(0); + await notebookUI.executeCell(1); + + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(1); + // Confirm border is visible + assert.include(cellOutputHtml, 'border'); + }); + + // Run the cell that will stick output into the out border + await notebookUI.executeCell(2); + + // Make sure output is shown + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(1); + // Confirm output went inside of previous cell + assert.include(cellOutputHtml, 'Hello world'); + }); + + // Make sure output on print cell is empty + await retryIfFail(async () => { + const cell = await notebookUI.getCell(2); + const output = await cell.$$('.cell-output-wrapper'); + assert.equal(output.length, 0, 'Cell should not have any output'); + }); + + // interact portion + await notebookUI.executeCell(3); + await notebookUI.executeCell(4); + await notebookUI.executeCell(5); + // See if we have a slider in our output + const slider = await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(5); + assert.include(cellOutputHtml, 'slider', 'Cell output should have rendered a slider'); + const sliderInner = await (await notebookUI.getCellOutput(5)).$$('.slider-container'); + assert.ok(sliderInner.length, 'Slider not found'); + return sliderInner[0]; + }); + + // Click on the slider to change the value. + const rect = await slider.boundingBox(); + if (rect) { + await notebookUI.page?.mouse.move(rect?.x + 5, rect.y + rect.height / 2); + await notebookUI.page?.mouse.down(); + await notebookUI.page?.mouse.up(); + } + + // Make sure the output value has changed to something other than 10 + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(5); + assert.notInclude(cellOutputHtml, '<pre>10', 'Slider click did not update the span'); + }); + }); + test('Render k3d', async () => { + const { notebookUI } = await openK3DIpynb(); + await assert.eventually.isFalse(notebookUI.cellHasOutput(3)); + await assert.eventually.isFalse(notebookUI.cellHasOutput(5)); + + await notebookUI.executeCell(3); + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(3); + // Confirm svg graph has been rendered. + assert.include(cellOutputHtml, '<canvas'); + // Toolbar should be rendered. + assert.include(cellOutputHtml, 'Close Controls'); + // The containing element with a class of `k3d-target` should be rendered. + assert.include(cellOutputHtml, 'k3d-target'); + }); + + await notebookUI.executeCell(5); + await retryIfFail(async () => { + const cellOutputHtml = await notebookUI.getCellOutputHTML(5); + // Slider should be rendered. + assert.include(cellOutputHtml, 'ui-slider'); + }); + }); + }); +}); diff --git a/src/test/datascience/uiTests/notebookHelpers.ts b/src/test/datascience/uiTests/notebookHelpers.ts new file mode 100644 index 000000000000..ae89392153c5 --- /dev/null +++ b/src/test/datascience/uiTests/notebookHelpers.ts @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fs from 'fs-extra'; +import * as getFreePort from 'get-port'; +import { IDisposable } from 'monaco-editor'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { EventEmitter, Uri, ViewColumn, WebviewPanel } from 'vscode'; +import { traceInfo } from '../../../client/common/logger'; +import { noop } from '../../../client/common/utils/misc'; +import { INotebookEditor, INotebookEditorProvider } from '../../../client/datascience/types'; +import { createTemporaryFile } from '../../utils/fs'; +import { mockedVSCodeNamespaces } from '../../vscode-mock'; +import { DataScienceIocContainer } from '../dataScienceIocContainer'; +import { NotebookEditorUI } from './notebookUi'; +import { WebServer } from './webBrowserPanel'; + +async function openNotebookEditor( + iocC: DataScienceIocContainer, + contents: string, + filePath: string = '/usr/home/test.ipynb' +): Promise<INotebookEditor> { + const uri = Uri.file(filePath); + iocC.setFileContents(uri, contents); + traceInfo(`NotebookHelper: opening notebook file ${filePath}`); + const notebookEditorProvider = iocC.get<INotebookEditorProvider>(INotebookEditorProvider); + return uri ? notebookEditorProvider.open(uri) : notebookEditorProvider.createNew(); +} + +async function createNotebookFileWithContents(contents: string, disposables: IDisposable[]): Promise<string> { + const notebookFile = await createTemporaryFile('.ipynb'); + disposables.push({ + dispose: () => { + try { + notebookFile.cleanupCallback.bind(notebookFile); + } catch { + noop(); + } + } + }); + await fs.writeFile(notebookFile.filePath, contents); + return notebookFile.filePath; +} + +function createWebViewPanel(): WebviewPanel { + traceInfo(`creating dummy webview panel`); + const disposeEventEmitter = new EventEmitter<void>(); + const webViewPanel: Partial<WebviewPanel> = { + webview: { + html: '' + // tslint:disable-next-line: no-any + } as any, + reveal: noop, + onDidDispose: disposeEventEmitter.event.bind(disposeEventEmitter), + dispose: () => disposeEventEmitter.fire(), + title: '', + viewType: '', + active: true, + options: {}, + visible: true, + viewColumn: ViewColumn.Active + }; + + mockedVSCodeNamespaces.window + ?.setup((w) => + w.createWebviewPanel(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => { + traceInfo(`Mock webview ${JSON.stringify(webViewPanel)} should be returned.`); + // tslint:disable-next-line: no-any + return webViewPanel as any; + }); + + // tslint:disable-next-line: no-any + return webViewPanel as any; +} + +export async function openNotebook( + ioc: DataScienceIocContainer, + disposables: IDisposable[], + notebookFileContents: string +) { + traceInfo(`Opening notebook for UI tests...`); + const notebookFile = await createNotebookFileWithContents(notebookFileContents, disposables); + traceInfo(`Notebook UI Tests: have file ${notebookFile}`); + + const notebookUI = new NotebookEditorUI(); + disposables.push(notebookUI); + // Wait for UI to load, i.e. until we get the message `LoadAllCellsComplete`. + const uiLoaded = notebookUI.waitUntilLoaded(); + + // tslint:disable-next-line: insecure-random + let port = Math.floor(Math.random() * Math.floor(1000)) + 9000; + try { + port = await getFreePort({ host: 'localhost' }); + process.env.VSC_PYTHON_DS_UI_PORT = port.toString(); + traceInfo(`Notebook UI Tests: have port ${port}`); + } catch (exc) { + traceInfo(`Failed getting a port.`, exc); + } + + // Wait for the browser to launch and open the UI. + // I.e. wait until we open the notebook react ui in browser. + const originalWaitForConnection = WebServer.prototype.waitForConnection; + const waitForConnection = sinon.stub(WebServer.prototype, 'waitForConnection'); + waitForConnection.callsFake(async function (this: WebServer) { + waitForConnection.restore(); + // Hook up the message service with the notebook class. + // Used to send/receive messages (postOffice) via webSockets in webserver. + notebookUI._setWebServer(this); + + // Execute base code. + const promise = originalWaitForConnection.apply(this); + + // Basically we're waiting for web server to wait for connection from browser. + // We're also waiting for UI to connect to backend Url. + await Promise.all([notebookUI.loadUI(`http://localhost:${port}/index.nativeEditor.html`), promise]); + }); + + const webViewPanel = createWebViewPanel(); + traceInfo(`Notebook UI Tests: about to open editor`); + + const notebookEditor = await openNotebookEditor(ioc, notebookFileContents, notebookFile); + traceInfo(`Notebook UI Tests: have editor`); + await uiLoaded; + traceInfo(`Notebook UI Tests: UI complete`); + + // Wait for kernel to be idle before finishing. (Prevents early shutdown problems in tests) + await notebookEditor.notebook?.waitForIdle(60_000); + + // Tell the notebook UI about the editor (it needs it for execution) + notebookUI._setEditor(notebookEditor); + + // Make sure we dispose of the notebook editor + disposables.push(notebookEditor); + + return { notebookEditor, webViewPanel, notebookUI }; +} diff --git a/src/test/datascience/uiTests/notebookUi.ts b/src/test/datascience/uiTests/notebookUi.ts new file mode 100644 index 000000000000..e6f686ac6dbe --- /dev/null +++ b/src/test/datascience/uiTests/notebookUi.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { ElementHandle } from 'playwright-chromium'; +import { sleep } from '../../../client/common/utils/async'; +import { InteractiveWindowMessages } from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { INotebookEditor } from '../../../client/datascience/types'; +import { BaseWebUI } from './helpers'; + +enum CellToolbarButton { + run = 0 +} + +enum MainToolbarButton { + clearOutput = 6 +} + +export class NotebookEditorUI extends BaseWebUI { + private _editor: INotebookEditor | undefined; + public _setEditor(editor: INotebookEditor) { + this._editor = editor; + } + public async getCellCount(): Promise<number> { + const items = await this.page!.$$('.cell-wrapper'); + return items.length; + } + + public async clearOutput(): Promise<void> { + const runButton = await this.getMainToolbarButton(MainToolbarButton.clearOutput); + await runButton.click({ button: 'left', force: true, timeout: 0 }); + } + + public async executeCell(cellIndex: number): Promise<void> { + const renderedPromise = this.waitForMessage(InteractiveWindowMessages.ExecutionRendered); + // Make sure to wait for idle so that the button is clickable. + await this.waitForIdle(); + + // Wait just a bit longer to make sure button is visible (not sure why it isn't clicking the button sometimes) + await sleep(500); + + // Click the run button. + const runButton = await this.getToolbarButton(cellIndex, CellToolbarButton.run); + // tslint:disable-next-line: no-console + console.log(`Executing cell ${cellIndex} by clicking ${runButton.toString()}`); + await Promise.all([runButton.click({ button: 'left', force: true, timeout: 0 }), renderedPromise]); + } + + public async cellHasOutput(cellIndex: number): Promise<boolean> { + const cell = await this.getCell(cellIndex); + const output = await cell.$$('.cell-output-wrapper'); + return output.length > 0; + } + + public async getCellOutputHTML(cellIndex: number): Promise<string> { + const output = await this.getCellOutput(cellIndex); + const outputHtml = await output.getProperty('innerHTML'); + return outputHtml?.toString() || ''; + } + + public async getCellOutput(cellIndex: number): Promise<ElementHandle<Element>> { + const cell = await this.getCell(cellIndex); + const output = await cell.$$('.cell-output-wrapper'); + if (output.length === 0) { + assert.fail('Cell does not have any output'); + } + return output[0]; + } + + public async getCell(cellIndex: number): Promise<ElementHandle<Element>> { + const items = await this.page!.$$('.cell-wrapper'); + return items[cellIndex]; + } + + private waitForIdle(): Promise<void> { + if (this._editor && this._editor.notebook) { + return this._editor.notebook.waitForIdle(60_000); + } + return Promise.resolve(); + } + + private async getMainToolbarButton(button: MainToolbarButton): Promise<ElementHandle<Element>> { + // First wait for the toolbar button to be visible. + await this.page!.waitForFunction( + `document.querySelectorAll('.toolbar-menu-bar button[role=button]').length && document.querySelectorAll('.toolbar-menu-bar button[role=button]')[${button}].clientHeight != 0` + ); + // Then eval the button + const buttons = await this.page!.$$('.toolbar-menu-bar button[role=button]'); + if (buttons.length === 0) { + assert.fail('Main toolbar Buttons not available'); + } + return buttons[button]; + } + private async getCellToolbar(cellIndex: number): Promise<ElementHandle<Element>> { + const cell = await this.getCell(cellIndex); + return cell.$$('.native-editor-celltoolbar-middle').then((items) => items[0]); + } + private async getToolbarButton(cellIndex: number, button: CellToolbarButton): Promise<ElementHandle<Element>> { + const toolbar = await this.getCellToolbar(cellIndex); + return toolbar.$$('button[role=button]').then((items) => items[button]); + } +} diff --git a/src/test/datascience/uiTests/notebooks/beakerx_widgets.ipynb b/src/test/datascience/uiTests/notebooks/beakerx_widgets.ipynb new file mode 100644 index 000000000000..3445e45262a9 --- /dev/null +++ b/src/test/datascience/uiTests/notebooks/beakerx_widgets.ipynb @@ -0,0 +1,58 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Prerequisites\n", + "pip install beakerx" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "from beakerx import *\n", + "plot1 = Plot()\n", + "plot1.add(Bars(displayName=\"Bar\", \n", + " x=[20,40,60], \n", + " y=[100, 120, 90], \n", + " width=10))" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "from beakerx import *\n", + "dataH4 = []\n", + "\n", + "for x in range(1, 1100000):\n", + " dataH4.append(random.gauss(0, 1))\n", + "\n", + "Histogram(data= dataH4, binCount= 10000)" + ] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.1-final" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/src/test/datascience/uiTests/notebooks/bqplot_widgets.ipynb b/src/test/datascience/uiTests/notebooks/bqplot_widgets.ipynb new file mode 100644 index 000000000000..14f544fef83e --- /dev/null +++ b/src/test/datascience/uiTests/notebooks/bqplot_widgets.ipynb @@ -0,0 +1,124 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Prerequisites\n", + "\n", + "pip install bqplot" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": 126, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from bqplot import pyplot as plt\n", + "\n", + "# And creating some random data\n", + "size = 100\n", + "np.random.seed(0)\n", + "x_data = np.arange(size)\n", + "y_data = np.cumsum(np.random.randn(size) * 100.0)" + ] + }, + { + "cell_type": "code", + "execution_count": 127, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(title='My First Plot')\n", + "plt.plot(x_data, y_data)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using `bqplot`'s interactive elements" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": 128, + "metadata": {}, + "outputs": [], + "source": [ + "# Creating a new Figure and setting it's title\n", + "plt.figure(title='My Second Chart')" + ] + }, + { + "cell_type": "code", + "execution_count": 129, + "metadata": {}, + "outputs": [], + "source": [ + "# Let's assign the scatter plot to a variable\n", + "scatter_plot = plt.scatter(x_data, y_data)\n", + "scatter_plot.y = np.cumsum(np.random.randn(size) * 100.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Change color of plots" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Say, the color\n", + "scatter_plot.colors = ['Red']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scatter_plot.colors = ['yellow']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "anaconda-cloud": {}, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.1-final" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/src/test/datascience/uiTests/notebooks/ipySheet_widgets.ipynb b/src/test/datascience/uiTests/notebooks/ipySheet_widgets.ipynb new file mode 100644 index 000000000000..2cf40ef38208 --- /dev/null +++ b/src/test/datascience/uiTests/notebooks/ipySheet_widgets.ipynb @@ -0,0 +1,208 @@ +{ + "cells": [ + { + "source": [ + "# Prerequisites\n", + "\n", + "### pip install ipysheet" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "from ipywidgets import FloatSlider, IntSlider, Image\n", + "import ipysheet" + ] + }, + { + "source": [ + "# 1. Test Rendering a Sheet" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "sheet = ipysheet.sheet(rows=3, columns=4)\n", + "cell1 = ipysheet.cell(0, 0, 'Hello')\n", + "cell2 = ipysheet.cell(2, 0, 'World')\n", + "cell_value = ipysheet.cell(2,2, 42.)\n", + "sheet" + ] + }, + { + "source": [ + "# 2. Test Searching a Sheet (interact with textbox in a different cell)" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "from ipysheet import from_dataframe\n", + "from ipywidgets import Text, VBox, link\n", + "\n", + "df = pd.DataFrame({'A': 1.,\n", + " 'B': pd.Timestamp('20130102'),\n", + " 'C': pd.Series(1, index=list(range(4)), dtype='float32'),\n", + " 'D': np.array([False, True, False, False], dtype='bool'),\n", + " 'E': pd.Categorical([\"test\", \"train\", \"test\", \"train\"]),\n", + " 'F': 'foo'})\n", + "\n", + "df.loc[[0, 2], ['B']] = np.nan\n", + "\n", + "\n", + "sheet2 = from_dataframe(df)\n", + "\n", + "search_box = Text(description='Search:')\n", + "link((search_box, 'value'), (sheet2, 'search_token'))" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "search_box" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "sheet2" + ] + }, + { + "source": [ + "# 3. Test calculations (slider update cell value via python code)" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import FloatSlider, IntSlider, Image, IntText, link\n", + "import ipysheet\n", + "\n", + "slider = IntSlider(description=\"Continuous\", continuous_update=True)\n", + "textbox = IntText(description=\"Continuous\", continuous_update=True)\n", + "\n", + "link((slider, 'value'), (textbox, 'value'))" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [], + "source": [ + "slider" + ] + }, + { + "source": [ + "* Typing value into textbox will move slider\n", + "* The value in cell will also get updated" + ], + "cell_type": "markdown", + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [], + "source": [ + "textbox" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [], + "source": [ + "sheet = ipysheet.sheet()\n", + "\n", + "cell1 = ipysheet.cell(0, 0, slider, style={'min-width': '150px'})\n", + "cell3 = ipysheet.cell(2, 2, 50.)\n", + "cell_sum = ipysheet.cell(3, 2, 50.)\n", + "\n", + "@ipysheet.calculation(inputs=[(cell1, 'value'), cell3], output=cell_sum)\n", + "def calculate(a, b):\n", + " return a + b\n", + "\n", + "sheet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "celltoolbar": "Attachments", + "file_extension": ".py", + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.1-final" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": false, + "sideBar": false, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": false, + "toc_window_display": false + }, + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/test/datascience/uiTests/notebooks/ipyvolume_widgets.ipynb b/src/test/datascience/uiTests/notebooks/ipyvolume_widgets.ipynb new file mode 100644 index 000000000000..a42becbc0da1 --- /dev/null +++ b/src/test/datascience/uiTests/notebooks/ipyvolume_widgets.ipynb @@ -0,0 +1,107 @@ +{ + "nbformat": 4, + "nbformat_minor": 2, + "metadata": { + "language_info": { + "name": "python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "version": "3.8.1-final" + }, + "orig_nbformat": 2, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Install ipyvolume\n", + "pip install ipyvolume" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + ], + "source": [ + "import ipyvolume as ipv\n", + "import numpy as np\n", + "x, y, z, u, v, w = np.random.random((6, 1000))*2-1\n", + "selected = np.random.randint(0, 1000, 100)\n", + "ipv.figure()\n", + "quiver = ipv.quiver(x, y, z, u, v, w, size=5, size_selected=8, selected=selected)\n", + "\n", + "from ipywidgets import FloatSlider, ColorPicker, VBox, jslink\n", + "size = FloatSlider(min=0, max=30, step=0.1)\n", + "size_selected = FloatSlider(min=0, max=30, step=0.1)\n", + "color = ColorPicker()\n", + "color_selected = ColorPicker()\n", + "jslink((quiver, 'size'), (size, 'value'))\n", + "jslink((quiver, 'size_selected'), (size_selected, 'value'))\n", + "jslink((quiver, 'color'), (color, 'value'))\n", + "jslink((quiver, 'color_selected'), (color_selected, 'value'))\n", + "VBox([ipv.gcc(), size, size_selected, color, color_selected])" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import ipyvolume as ipv\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "s = 1/2**0.5\n", + "# 4 vertices for the tetrahedron\n", + "x = np.array([1., -1, 0, 0])\n", + "y = np.array([0, 0, 1., -1])\n", + "z = np.array([-s, -s, s, s])\n", + "# and 4 surfaces (triangles), where the number refer to the vertex index\n", + "triangles = [(0, 1, 2), (0, 1, 3), (0, 2, 3), (1,3,2)]" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + ], + "source": [ + "ipv.figure()\n", + "# we draw the tetrahedron\n", + "mesh = ipv.plot_trisurf(x, y, z, triangles=triangles, color='orange')\n", + "# and also mark the vertices\n", + "ipv.scatter(x, y, z, marker='sphere', color='blue')\n", + "ipv.xyzlim(-2, 2)\n", + "ipv.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ] +} diff --git a/src/test/datascience/uiTests/notebooks/k3d_widgets.ipynb b/src/test/datascience/uiTests/notebooks/k3d_widgets.ipynb new file mode 100644 index 000000000000..383af7e1347d --- /dev/null +++ b/src/test/datascience/uiTests/notebooks/k3d_widgets.ipynb @@ -0,0 +1,141 @@ +{ + "cells": [ + { + "source": [ + "# Prerequisites\n", + "\n", + "### pip install K3D" + ], + "cell_type": "markdown", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "source": [ + "This ipynb file used to fail even with ipywidgets enabled.\n", + "There was a problem with serialization of the array buffer.\n", + "https://github.com/K3D-tools/K3D-jupyter/blob/821a59ed88579afaafababd6291e8692d70eb088/examples/camera_manipulation.ipynb\n", + "\n", + "# Note:\n", + "Other K3D notebooks worked, except this, hence this test is mandatory.\n", + "Who knows what other widgets (or same widgets under different conditions, like K3d) could fail similarly." + ], + "cell_type": "markdown", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# This will render a 3d image, see here for sample output.\n", + "![k3d](https://user-images.githubusercontent.com/1948812/79030314-49dadd80-7b4d-11ea-9a39-03ddad09d119.gif)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "source": [ + "import k3d\n", + "import numpy as np\n", + "from numpy import sin,cos,pi\n", + "from ipywidgets import interact, interactive, fixed\n", + "import ipywidgets as widgets\n", + "import time\n", + "import math\n", + "\n", + "plot = k3d.plot()\n", + "\n", + "plot.camera_auto_fit = False\n", + "\n", + "T = 1.618033988749895\n", + "r = 4.77\n", + "zmin,zmax = -r,r\n", + "xmin,xmax = -r,r\n", + "ymin,ymax = -r,r\n", + "Nx,Ny,Nz = 77,77,77\n", + "\n", + "x = np.linspace(xmin,xmax,Nx)\n", + "y = np.linspace(ymin,ymax,Ny)\n", + "z = np.linspace(zmin,zmax,Nz)\n", + "x,y,z = np.meshgrid(x,y,z,indexing='ij')\n", + "p = 2 - (cos(x + T*y) + cos(x - T*y) + cos(y + T*z) + cos(y - T*z) + cos(z - T*x) + cos(z + T*x))\n", + "iso = k3d.marching_cubes(p.astype(np.float32),xmin=xmin,xmax=xmax,ymin=ymin,ymax=ymax, zmin=zmin, zmax=zmax, level=0.0)\n", + "plot += iso\n", + "\n", + "plot.display()" + ] + }, + { + "source": [ + "# Here we expect 2 sliders to get rendered. Moving those will update the 3d image in previous cell." + ], + "cell_type": "markdown", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "source": [ + "@interact(x=widgets.FloatSlider(value=0,min=-3,max=4,step=0.01))\n", + "def g(x):\n", + " iso.level=x\n", + " \n", + "@interact(rad=widgets.FloatSlider(value=0,min=0,max=2*math.pi,step=0.01))\n", + "def g(rad):\n", + " plot.camera = [3*r*sin(rad),3*r*cos(rad),0,\n", + " 0,0,0,\n", + " 0,0,1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "celltoolbar": "Attachments", + "file_extension": ".py", + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3-final" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": false, + "sideBar": false, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": false, + "toc_window_display": false + }, + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/test/datascience/uiTests/notebooks/outputinteract_widgets.ipynb b/src/test/datascience/uiTests/notebooks/outputinteract_widgets.ipynb new file mode 100644 index 000000000000..62970d142a3d --- /dev/null +++ b/src/test/datascience/uiTests/notebooks/outputinteract_widgets.ipynb @@ -0,0 +1,127 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import ipywidgets as widgets\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "Output(layout=Layout(border='1px solid black'))", + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "3c2839be29b64187917d39dae5635e89" + } + }, + "metadata": {} + } + ], + "source": [ + "\n", + "out = widgets.Output(layout={'border': '1px solid black'})\n", + "out" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "with out:\n", + " out.clear_output()\n", + " for i in range(10):\n", + " print(i, 'Hello world!')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import interact, interactive, fixed, interact_manual\n", + "import ipywidgets as widgets" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def f(x):\n", + " return x" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "interactive(children=(IntSlider(value=10, description='x', max=30, min=-10), Output()), _dom_classes=('widget-…", + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "0447dc864ef043d49d708defc79a1a1b" + } + }, + "metadata": {} + } + ], + "source": [ + "interact(f, x=10);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "file_extension": ".py", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.4-final" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/src/test/datascience/uiTests/notebooks/pythreejs_widgets.ipynb b/src/test/datascience/uiTests/notebooks/pythreejs_widgets.ipynb new file mode 100644 index 000000000000..0d9ec45b6aae --- /dev/null +++ b/src/test/datascience/uiTests/notebooks/pythreejs_widgets.ipynb @@ -0,0 +1,156 @@ +{ + "nbformat": 4, + "nbformat_minor": 2, + "metadata": { + "language_info": { + "name": "python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "version": "3.8.1-final" + }, + "orig_nbformat": 2, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Intall pythreejs\n", + "pip install pythreejs" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from pythreejs import *\n", + "from IPython.display import display\n", + "from math import pi\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "# Reduce repo churn for examples with embedded state:\n", + "from pythreejs._example_helper import use_example_model_ids\n", + "use_example_model_ids()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "BoxGeometry(\n", + " width=5,\n", + " height=10,\n", + " depth=15,\n", + " widthSegments=5,\n", + " heightSegments=10,\n", + " depthSegments=15)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "view_width = 600\n", + "view_height = 400" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "sphere = Mesh(\n", + " SphereBufferGeometry(1, 32, 16),\n", + " MeshStandardMaterial(color='red')\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "cube = Mesh(\n", + " BoxBufferGeometry(1, 1, 1),\n", + " MeshPhysicalMaterial(color='green'),\n", + " position=[2, 0, 4]\n", + ")\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "camera = PerspectiveCamera( position=[10, 6, 10], aspect=view_width/view_height)\n", + "key_light = DirectionalLight(position=[0, 10, 10])\n", + "ambient_light = AmbientLight()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + ], + "source": [ + "f = \"\"\"\n", + "function f(origu, origv, out) {\n", + " // scale u and v to the ranges I want: [0, 2*pi]\n", + " var u = 2*Math.PI*origu;\n", + " var v = 2*Math.PI*origv;\n", + "\n", + " var x = Math.sin(u);\n", + " var y = Math.cos(v);\n", + " var z = Math.cos(u+v);\n", + "\n", + " out.set(x,y,z)\n", + "}\n", + "\"\"\"\n", + "surf_g = ParametricGeometry(func=f, slices=16, stacks=16);\n", + "\n", + "surf1 = Mesh(geometry=surf_g,\n", + " material=MeshLambertMaterial(color='green', side='FrontSide'))\n", + "surf2 = Mesh(geometry=surf_g,\n", + " material=MeshLambertMaterial(color='yellow', side='BackSide'))\n", + "surf = Group(children=[surf1, surf2])\n", + "\n", + "camera2 = PerspectiveCamera( position=[10, 6, 10], aspect=view_width/view_height)\n", + "scene2 = Scene(children=[surf, camera2,\n", + " DirectionalLight(position=[3, 5, 1], intensity=0.6),\n", + " AmbientLight(intensity=0.5)])\n", + "renderer2 = Renderer(camera=camera2, scene=scene2,\n", + " controls=[OrbitControls(controlling=camera2)],\n", + " width=view_width, height=view_height)\n", + "display(renderer2)" + ] + } + ] +} diff --git a/src/test/datascience/uiTests/notebooks/simple_abc.ipynb b/src/test/datascience/uiTests/notebooks/simple_abc.ipynb new file mode 100644 index 000000000000..4817fea1a6b3 --- /dev/null +++ b/src/test/datascience/uiTests/notebooks/simple_abc.ipynb @@ -0,0 +1,59 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "a=1\n", + "a" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "b=2\n", + "b" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "c=3\n", + "c" + ] + } + ], + "metadata": { + "file_extension": ".py", + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.4" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3, + "orig_nbformat": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/test/datascience/uiTests/notebooks/standard_widgets.ipynb b/src/test/datascience/uiTests/notebooks/standard_widgets.ipynb new file mode 100644 index 000000000000..2b69e47fff56 --- /dev/null +++ b/src/test/datascience/uiTests/notebooks/standard_widgets.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "widgets.IntSlider()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "widgets.Text(value='Hello World!', disabled=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "widgets.Checkbox(\n", + " value=False,\n", + " description='Check me',\n", + " disabled=False,\n", + " indent=False\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import display\n", + "import ipywidgets as widgets\n", + "button = widgets.Button(description=\"Click Me!\")\n", + "caption = widgets.Label(value='Not Clicked')\n", + "\n", + "\n", + "display(button)\n", + "\n", + "def on_button_clicked(b):\n", + " caption.value = 'Button Clicked'\n", + "\n", + "button.on_click(on_button_clicked)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "caption" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2a48dc8c3af74bea94394de3cdc744db", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "FloatProgress(value=0.0, max=1.0)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import threading\n", + "from IPython.display import display\n", + "import ipywidgets as widgets\n", + "import time\n", + "progress = widgets.FloatProgress(value=0.0, min=0.0, max=1.0)\n", + "\n", + "def work(progress):\n", + " total = 100\n", + " for i in range(total):\n", + " time.sleep(0.2)\n", + " progress.value = float(i+1)/total\n", + "\n", + "thread = threading.Thread(target=work, args=(progress,))\n", + "display(progress)\n", + "thread.start()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "celltoolbar": "Attachments", + "file_extension": ".py", + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": false, + "sideBar": false, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": false, + "toc_window_display": false + }, + "version": 3 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/test/datascience/uiTests/recorder.ts b/src/test/datascience/uiTests/recorder.ts new file mode 100644 index 000000000000..6445ba2660a9 --- /dev/null +++ b/src/test/datascience/uiTests/recorder.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { InteractiveWindowMessages } from '../../../client/datascience/interactive-common/interactiveWindowTypes'; +import { noop } from '../../core'; +import { IWebServer } from './webBrowserPanel'; + +// tslint:disable: no-any + +export type RequestFromUI = { + type: 'fromUI'; + payload: any; +}; + +export type MessageForUI = { + type: 'forUI'; + payload: any; +}; +function getOnigasmContents(): Buffer | undefined { + // Look for the file next or our current file (this is where it's installed in the vsix) + const filePath = path.join(EXTENSION_ROOT_DIR, 'node_modules', 'onigasm', 'lib', 'onigasm.wasm'); + if (fs.existsSync(filePath)) { + return fs.readFileSync(filePath); + } + return undefined; +} + +export class TestRecorder { + private readonly originalPostMessage: (message: {}) => void; + private messages: (RequestFromUI | MessageForUI)[] = []; + constructor( + private readonly webServer: IWebServer, + public readonly mode: 'record' | 'replay' | 'skip', + private readonly file: string + ) { + this.originalPostMessage = this.webServer.postMessage.bind(this.webServer); + if (mode === 'skip') { + return; + } + this.initialize(); + } + public async end() { + if (this.mode !== 'record') { + return; + } + const messages = JSON.stringify(this.messages, undefined, 4); + await fs.writeFile(this.file, messages, { + encoding: 'utf8' + }); + } + private initialize() { + const disposable = this.webServer.onDidReceiveMessage(this.onDidReceiveMessage, this); + const oldDispose = this.webServer.dispose.bind(this.webServer); + + this.webServer.dispose = () => { + disposable.dispose(); + oldDispose(); + }; + if (this.mode === 'record') { + this.webServer.postMessage = this.postMessage.bind(this); + } else { + // Rehydrate messages to be played back. + this.messages = JSON.parse(fs.readFileSync(this.file, { encoding: 'utf8' })) as any; + // Don't allow anything to interfere with communication with UI (test recorder will do everything). + this.webServer.postMessage = noop as any; + } + } + + private onDidReceiveMessage(message: any) { + if (this.mode === 'record') { + this.messages.push({ payload: message, type: 'fromUI' }); + } else { + // Find the message from the recorded list. + const index = this.messages.findIndex((item) => { + if (item.type === 'fromUI' && item.payload.type === message.type) { + return true; + } + return false; + }); + this.messages.splice(index, 1); + this.sendMessageToUIUntilNextUIRequest(); + } + } + private sendMessageToUIUntilNextUIRequest() { + // Now send all messages till the next request. + const nextRequestIndex = this.messages.findIndex((item) => item.type === 'fromUI'); + if (nextRequestIndex === 0 || this.messages.length === 0) { + return; + } + // Send messages one at a time, with an artifical delay (mimic realworld). + const messagesToSend = this.messages.shift()!; + if ( + messagesToSend.type === 'forUI' && + messagesToSend.payload.type === InteractiveWindowMessages.LoadOnigasmAssemblyResponse + ) { + messagesToSend.payload.payload = getOnigasmContents(); + } + this.originalPostMessage(messagesToSend.payload); + setTimeout(this.sendMessageToUIUntilNextUIRequest.bind(this), 1); + } + private postMessage(message: any): void { + const messageToLog = { ...message }; + if (messageToLog.type === InteractiveWindowMessages.LoadOnigasmAssemblyResponse) { + messageToLog.payload = '<BLAH>'; + } + this.messages.push({ payload: messageToLog, type: 'forUI' }); + // When recording, add a delay of 500ms, so we can record the messages and get the order right. + setTimeout(() => { + this.originalPostMessage(message); + }, 500); + } +} diff --git a/src/test/datascience/uiTests/webBrowserPanel.ts b/src/test/datascience/uiTests/webBrowserPanel.ts new file mode 100644 index 000000000000..bdad9ee1a164 --- /dev/null +++ b/src/test/datascience/uiTests/webBrowserPanel.ts @@ -0,0 +1,250 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import * as cors from 'cors'; +import * as express from 'express'; +import * as http from 'http'; +import { IDisposable } from 'monaco-editor'; +import * as path from 'path'; +import * as socketIO from 'socket.io'; +import { env, Event, EventEmitter, Uri, WebviewOptions, WebviewPanel, window } from 'vscode'; +import { IWebviewPanel, IWebviewPanelOptions } from '../../../client/common/application/types'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { createDeferred } from '../../../client/common/utils/async'; +import { noop } from '../../../client/common/utils/misc'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; + +// tslint:disable: no-any no-console no-require-imports no-var-requires +const nocache = require('nocache'); + +export interface IWebServer extends IDisposable { + onDidReceiveMessage: Event<any>; + postMessage(message: {}): void; + launchServer(cwd: string, resourcesRoot: string, port?: number): Promise<number>; + waitForConnection(): Promise<void>; +} + +export class WebServer implements IWebServer { + public get onDidReceiveMessage() { + return this._onDidReceiveMessage.event; + } + private app?: express.Express; + private io?: socketIO.Server; + private server?: http.Server; + private disposed: boolean = false; + private readonly socketPromise = createDeferred<socketIO.Socket>(); + private readonly _onDidReceiveMessage = new EventEmitter<any>(); + private socket?: socketIO.Socket; + public static create() { + return new WebServer(); + } + public dispose() { + this.server?.close(); + this.io?.close(); + this.disposed = true; + this.socketPromise.promise.then((s) => s.disconnect()).catch(noop); + } + public postMessage(message: {}) { + if (this.disposed) { + return; + } + this.socketPromise.promise + .then(() => { + this.socket?.emit('fromServer', message); + }) + .catch((ex) => { + console.error('Failed to connect to socket', ex); + }); + } + + /** + * Starts a WebServer, and optionally displays a Message when server is ready. + * Used only for debugging and testing purposes. + */ + public async launchServer(cwd: string, resourcesRoot: string, port: number = 0): Promise<number> { + this.app = express(); + this.server = http.createServer(this.app); + this.io = socketIO(this.server); + this.app.use(express.static(resourcesRoot, { cacheControl: false, etag: false })); + this.app.use(express.static(cwd)); + this.app.use(cors()); + // Ensure browser does'nt cache anything (for UI tests/debugging). + this.app.use(nocache()); + this.app.disable('view cache'); + this.app.get('/source', (req, res) => { + // Query has been messed up in sending to the web site. Works in vscode though, so don't try + // to fix the encoding. + const queryKeys = Object.keys(req.query); + const hashKey = queryKeys ? queryKeys.find((q) => q.startsWith('hash=')) : undefined; + if (hashKey) { + const diskLocation = path.join(EXTENSION_ROOT_DIR, 'tmp', 'scripts', hashKey.substr(5), 'index.js'); + res.sendFile(diskLocation); + } else { + res.status(404).end(); + } + }); + + this.io.on('connection', (socket) => { + // Possible we close browser and reconnect, or hit refresh button. + this.socket = socket; + this.socketPromise.resolve(socket); + socket.on('fromClient', (data) => { + this._onDidReceiveMessage.fire(data); + }); + }); + + port = await new Promise<number>((resolve, reject) => { + this.server?.listen(port, () => { + const address = this.server?.address(); + if (address && typeof address !== 'string' && 'port' in address) { + resolve(address.port); + } else { + reject(new Error('Address not available')); + } + }); + }); + + // Display a message if this env variable is set (used when debugging). + // tslint:disable-next-line: no-http-string + const url = `http:///localhost:${port}/index.html`; + if (process.env.VSC_PYTHON_DS_UI_PROMPT) { + window + // tslint:disable-next-line: messages-must-be-localized + .showInformationMessage(`Open browser to '${url}'`, 'Copy') + .then((selection) => { + if (selection === 'Copy') { + env.clipboard.writeText(url).then(noop, noop); + } + }, noop); + } + + return port; + } + + public async waitForConnection(): Promise<void> { + await this.socketPromise.promise; + } +} +/** + * Instead of displaying the UI in VS Code WebViews, we'll display in a browser. + * Ensure environment variable `VSC_PYTHON_DS_UI_PORT` is set to a port number. + * Also, if you set `VSC_PYTHON_DS_UI_PROMPT`, you'll be presented with a VS Code messagebox when URL/endpoint is ready. + */ +export class WebBrowserPanel implements IWebviewPanel, IDisposable { + private panel?: WebviewPanel; + private server?: IWebServer; + private serverUrl: string | undefined; + private loadFailedEmitter = new EventEmitter<void>(); + constructor( + private readonly disposableRegistry: IDisposableRegistry, + private readonly options: IWebviewPanelOptions + ) { + this.disposableRegistry.push(this); + const webViewOptions: WebviewOptions = { + enableScripts: true, + localResourceRoots: [Uri.file(this.options.rootPath), Uri.file(this.options.cwd)] + }; + if (options.webViewPanel) { + this.panel = options.webViewPanel; + this.panel.webview.options = webViewOptions; + } else { + this.panel = window.createWebviewPanel( + options.title.toLowerCase().replace(' ', ''), + options.title, + { viewColumn: options.viewColumn, preserveFocus: true }, + { + retainContextWhenHidden: true, + enableFindWidget: true, + ...webViewOptions + } + ); + } + + this.panel.webview.html = '<!DOCTYPE html><html><html><body><h1>Loading</h1></body>'; + // Reset when the current panel is closed + this.disposableRegistry.push( + this.panel.onDidDispose(() => { + this.panel = undefined; + this.options.listener.dispose().ignoreErrors(); + }) + ); + + this.launchServer(this.options.cwd, this.options.rootPath) + .then((p) => { + this.serverUrl = p; + }) + .catch((ex) => + // tslint:disable-next-line: no-console + console.error('Failed to start Web Browser Panel', ex) + ); + } + + public get loadFailed(): Event<void> { + return this.loadFailedEmitter.event; + } + + public asWebviewUri(localResource: Uri): Uri { + const filePath = localResource.fsPath; + const name = path.basename(path.dirname(filePath)); + if (name !== 'nbextensions' && this.serverUrl) { + // This is a CDN download, Remap to our webserver + const remapped = `${this.serverUrl}/source?hash=${name}`; + return Uri.parse(remapped); + } + return localResource; + } + public setTitle(newTitle: string): void { + if (this.panel) { + this.panel.title = newTitle; + } + } + public async show(preserveFocus: boolean): Promise<void> { + this.panel?.reveal(this.panel?.viewColumn, preserveFocus); + } + public isVisible(): boolean { + return this.panel?.visible === true; + } + public close(): void { + this.dispose(); + } + public isActive(): boolean { + return this.panel?.active === true; + } + public updateCwd(_cwd: string): void { + // Noop + } + public dispose() { + this.server?.dispose(); + this.panel?.dispose(); + } + + public postMessage(message: any) { + this.server?.postMessage(message); + } + + /** + * Starts a WebServer, and optionally displays a Message when server is ready. + * Used only for debugging and testing purposes. + */ + public async launchServer(cwd: string, resourcesRoot: string): Promise<string> { + // If no port is provided, use a random port. + const dsUIPort = parseInt(process.env.VSC_PYTHON_DS_UI_PORT || '', 10); + const portToUse = isNaN(dsUIPort) ? 0 : dsUIPort; + + this.server = WebServer.create(); + this.server.onDidReceiveMessage((data) => { + this.options.listener.onMessage(data.type, data.payload); + }); + + const port = await this.server.launchServer(cwd, resourcesRoot, portToUse); + if (this.panel?.webview) { + // tslint:disable-next-line: no-http-string + const url = `http:///localhost:${port}/index.html`; + this.panel.webview.html = `<!DOCTYPE html><html><html><body><h1>${url}</h1></body>`; + } + await this.server.waitForConnection(); + + // tslint:disable-next-line: no-http-string + return `http://localhost:${port}`; + } +} diff --git a/src/test/datascience/uiTests/webBrowserPanelProvider.ts b/src/test/datascience/uiTests/webBrowserPanelProvider.ts new file mode 100644 index 000000000000..340105f60dfe --- /dev/null +++ b/src/test/datascience/uiTests/webBrowserPanelProvider.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { inject, injectable } from 'inversify'; +import { IWebviewPanel, IWebviewPanelOptions, IWebviewPanelProvider } from '../../../client/common/application/types'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { WebBrowserPanel } from './webBrowserPanel'; + +@injectable() +export class WebBrowserPanelProvider implements IWebviewPanelProvider { + constructor(@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry) {} + + // tslint:disable-next-line:no-any + public async create(options: IWebviewPanelOptions): Promise<IWebviewPanel> { + return new WebBrowserPanel(this.disposableRegistry, options); + } +} diff --git a/src/test/datascience/variableTestHelpers.ts b/src/test/datascience/variableTestHelpers.ts new file mode 100644 index 000000000000..a810a5d8ed1b --- /dev/null +++ b/src/test/datascience/variableTestHelpers.ts @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { expect } from 'chai'; +import { ReactWrapper } from 'enzyme'; +import { parse } from 'node-html-parser'; +import * as React from 'react'; + +import { Uri } from 'vscode'; +import { IDocumentManager } from '../../client/common/application/types'; +import { createDeferred } from '../../client/common/utils/async'; +import { Identifiers } from '../../client/datascience/constants'; +import { getDefaultInteractiveIdentity } from '../../client/datascience/interactive-window/identity'; +import { + IJupyterDebugService, + IJupyterVariable, + IJupyterVariables, + INotebookProvider +} from '../../client/datascience/types'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { getOrCreateInteractiveWindow } from './interactiveWindowTestHelpers'; +import { MockDocumentManager } from './mockDocumentManager'; +import { waitForVariablesUpdated } from './testHelpers'; + +// tslint:disable: no-var-requires no-require-imports no-any chai-vague-errors no-unused-expression + +export async function verifyAfterStep( + ioc: DataScienceIocContainer, + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + verify: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void>, + numberOfRefreshesRequired: number = 1 +) { + const interactive = await getOrCreateInteractiveWindow(ioc); + const debuggerBroke = createDeferred(); + const jupyterDebugger = ioc.get<IJupyterDebugService>(IJupyterDebugService, Identifiers.MULTIPLEXING_DEBUGSERVICE); + jupyterDebugger.onBreakpointHit(() => debuggerBroke.resolve()); + const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; + const file = Uri.file('foo.py'); + docManager.addDocument('a=1\na', file.fsPath); + const debugPromise = interactive.window.debugCode('a=1\na', file, 1, undefined, undefined); + await debuggerBroke.promise; + const variableRefresh = waitForVariablesUpdated(interactive.mount, numberOfRefreshesRequired); + await jupyterDebugger.requestVariables(); // This is necessary because not running inside of VS code. Normally it would do this. + await variableRefresh; + wrapper.update(); + await verify(wrapper); + await jupyterDebugger.continue(); + return debugPromise; +} + +// Verify a set of rows versus a set of expected variables +export function verifyVariables( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + targetVariables: IJupyterVariable[] +) { + // Force an update so we render whatever the current state is + wrapper.update(); + + // Then search for results. + const foundRows = wrapper.find('div.react-grid-Row'); + + expect(foundRows.length).to.be.equal( + targetVariables.length, + 'Different number of variable explorer rows and target variables' + ); + + foundRows.forEach((row, index) => { + verifyRow(row, targetVariables[index]); + }); +} + +const Button_Column = 0; +const Name_Column = Button_Column + 1; +const Type_Column = Name_Column + 1; +const Shape_Column = Type_Column + 1; +const Value_Column = Shape_Column + 1; + +// Verify a single row versus a single expected variable +function verifyRow(rowWrapper: ReactWrapper<any, Readonly<{}>, React.Component>, targetVariable: IJupyterVariable) { + const rowCells = rowWrapper.find('div.react-grid-Cell'); + + expect(rowCells.length).to.be.equal(5, 'Unexpected number of cells in variable explorer row'); + + verifyCell(rowCells.at(Name_Column), targetVariable.name, targetVariable.name); + verifyCell(rowCells.at(Type_Column), targetVariable.type, targetVariable.name); + + if (targetVariable.shape && targetVariable.shape !== '') { + verifyCell(rowCells.at(Shape_Column), targetVariable.shape, targetVariable.name); + } else if (targetVariable.count) { + verifyCell(rowCells.at(Shape_Column), targetVariable.count.toString(), targetVariable.name); + } + + if (targetVariable.value) { + verifyCell(rowCells.at(Value_Column), targetVariable.value, targetVariable.name); + } + + verifyCell(rowCells.at(Button_Column), targetVariable.supportsDataExplorer, targetVariable.name); +} + +// Verify a single cell value against a specific target value +function verifyCell( + cellWrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + value: string | boolean, + targetName: string +) { + const cellHTML = parse(cellWrapper.html()) as any; + const innerHTML = cellHTML.innerHTML; + if (typeof value === 'string') { + // tslint:disable-next-line:no-string-literal + const match = /value="([\s\S]+?)"\s+/.exec(innerHTML); + expect(match).to.not.be.equal(null, `${targetName} does not have a value attribute`); + + // Eliminate whitespace differences + const actualValueNormalized = match![1].replace(/^\s*|\s(?=\s)|\s*$/g, '').replace(/\r\n/g, '\n'); + const expectedValueNormalized = value.replace(/^\s*|\s(?=\s)|\s*$/g, '').replace(/\r\n/g, '\n'); + + expect(actualValueNormalized).to.be.equal( + expectedValueNormalized, + `${targetName} has an unexpected value ${innerHTML} in variable explorer cell` + ); + } else { + if (value) { + expect(innerHTML).to.include('image-button-image', `Image class not found in ${targetName}`); + } else { + expect(innerHTML).to.not.include('image-button-image', `Image class was found ${targetName}`); + } + } +} + +export async function verifyCanFetchData<T>( + ioc: DataScienceIocContainer, + executionCount: number, + name: string, + rows: T[] +) { + const variableFetcher = ioc.get<IJupyterVariables>(IJupyterVariables, Identifiers.ALL_VARIABLES); + const notebookProvider = ioc.get<INotebookProvider>(INotebookProvider); + const notebook = await notebookProvider.getOrCreateNotebook({ + getOnly: true, + identity: getDefaultInteractiveIdentity() + }); + expect(notebook).to.not.be.undefined; + const variableList = await variableFetcher.getVariables(notebook!, { + executionCount, + startIndex: 0, + pageSize: 100, + sortAscending: true, + sortColumn: 'INDEX', + refreshCount: 0 + }); + expect(variableList.pageResponse.length).to.be.greaterThan(0, 'No variables returned'); + const variable = variableList.pageResponse.find((v) => v.name === name); + expect(variable).to.not.be.undefined; + expect(variable?.supportsDataExplorer).to.eq(true, `Variable ${name} does not support data explorer`); + const withInfo = await variableFetcher.getDataFrameInfo(variable!, notebook!); + expect(withInfo.count).to.eq(rows.length, 'Wrong number of rows for variable'); + const fetchedRows = await variableFetcher.getDataFrameRows(withInfo!, notebook!, 0, rows.length); + expect(fetchedRows.data).to.have.length(rows.length, 'Fetched rows data is not the correct size'); + for (let i = 0; i < rows.length; i += 1) { + const fetchedRow = (fetchedRows.data as any)[i]; + const val = fetchedRow['0']; // Column should default to zero for tests calling this. + expect(val).to.be.eq(rows[i], 'Invalid value found'); + } +} diff --git a/src/test/datascience/variableexplorer.functional.test.tsx b/src/test/datascience/variableexplorer.functional.test.tsx new file mode 100644 index 000000000000..59830bcda74c --- /dev/null +++ b/src/test/datascience/variableexplorer.functional.test.tsx @@ -0,0 +1,603 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { ReactWrapper } from 'enzyme'; +import * as React from 'react'; +import * as AdazzleReactDataGrid from 'react-data-grid'; +import { Disposable } from 'vscode'; + +import { RunByLine } from '../../client/common/experiments/groups'; +import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes'; +import { IJupyterVariable } from '../../client/datascience/types'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { takeSnapshot, writeDiffSnapshot } from './helpers'; +import { addCode, getOrCreateInteractiveWindow } from './interactiveWindowTestHelpers'; +import { addCell, createNewEditor } from './nativeEditorTestHelpers'; +import { openVariableExplorer, runDoubleTest, runInteractiveTest, waitForVariablesUpdated } from './testHelpers'; +import { verifyAfterStep, verifyCanFetchData, verifyVariables } from './variableTestHelpers'; + +// tslint:disable: no-var-requires no-require-imports +const rangeInclusive = require('range-inclusive'); + +// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +[false, true].forEach((runByLine) => { + suite(`DataScience Interactive Window variable explorer tests with RunByLine set to ${runByLine}`, () => { + const disposables: Disposable[] = []; + let ioc: DataScienceIocContainer; + let createdNotebook = false; + let snapshot: any; + + suiteSetup(function () { + snapshot = takeSnapshot(); + // These test require python, so only run with a non-mocked jupyter + const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; + if (!isRollingBuild) { + // tslint:disable-next-line:no-console + console.log('Skipping Variable Explorer tests. Requires python environment'); + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + + setup(async () => { + ioc = new DataScienceIocContainer(); + ioc.setExperimentState(RunByLine.experiment, runByLine); + ioc.registerDataScienceTypes(); + createdNotebook = false; + await ioc.activate(); + }); + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + await ioc.dispose(); + }); + + // Uncomment this to debug hangs on exit + suiteTeardown(() => { + // asyncDump(); + writeDiffSnapshot(snapshot, `Variable Explorer ${runByLine}`); + }); + + async function addCodeImpartial( + wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, + code: string, + waitForVariables: boolean = true, + waitForVariablesCount: number = 1, + expectError: boolean = false + ): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { + const nodes = wrapper.find('InteractivePanel'); + if (nodes.length > 0) { + const variablesUpdated = waitForVariables + ? waitForVariablesUpdated(ioc.getInteractiveWebPanel(undefined), waitForVariablesCount) + : Promise.resolve(); + const result = await addCode(ioc, code, expectError); + await variablesUpdated; + return result; + } else { + // For the native editor case, we need to create an editor before hand. + if (!createdNotebook) { + await createNewEditor(ioc); + createdNotebook = true; + } + const variablesUpdated = waitForVariables + ? waitForVariablesUpdated(ioc.getNativeWebPanel(undefined), waitForVariablesCount) + : Promise.resolve(); + await addCell(ioc.getNativeWebPanel(undefined), code, true); + await variablesUpdated; + return wrapper; + } + } + + runInteractiveTest( + 'Variable explorer - Exclude', + async () => { + const basicCode: string = `import numpy as np +import pandas as pd +value = 'hello world'`; + const basicCode2: string = `value2 = 'hello world 2'`; + + const { mount } = await getOrCreateInteractiveWindow(ioc); + const wrapper = mount.wrapper; + + openVariableExplorer(wrapper); + + await addCodeImpartial(wrapper, 'a=1\na'); + await addCodeImpartial(wrapper, basicCode, true); + + // We should show a string and show an int, the modules should be hidden + let targetVariables: IJupyterVariable[] = [ + { + name: 'a', + value: '1', + supportsDataExplorer: false, + type: 'int', + size: 54, + shape: '', + count: 0, + truncated: false + }, + // tslint:disable-next-line:quotemark + { + name: 'value', + value: 'hello world', + supportsDataExplorer: false, + type: 'str', + size: 54, + shape: '', + count: 0, + truncated: false + } + ]; + verifyVariables(wrapper, targetVariables); + + // Update our exclude list to exclude strings + ioc.getSettings().datascience.variableExplorerExclude = `${ + ioc.getSettings().datascience.variableExplorerExclude + };str`; + + // Add another string and check our vars, strings should be hidden + await addCodeImpartial(wrapper, basicCode2, true); + + targetVariables = [ + { + name: 'a', + value: '1', + supportsDataExplorer: false, + type: 'int', + size: 54, + shape: '', + count: 0, + truncated: false + } + ]; + verifyVariables(wrapper, targetVariables); + }, + () => { + return Promise.resolve(ioc); + } + ); + + runInteractiveTest( + 'Variable explorer - Update', + async () => { + const basicCode: string = `value = 'hello world'`; + const basicCode2: string = `value2 = 'hello world 2'`; + + const { mount } = await getOrCreateInteractiveWindow(ioc); + const wrapper = mount.wrapper; + + openVariableExplorer(wrapper); + + await addCodeImpartial(wrapper, 'a=1\na'); + + // Check that we have just the 'a' variable + let targetVariables: IJupyterVariable[] = [ + { + name: 'a', + value: '1', + supportsDataExplorer: false, + type: 'int', + size: 54, + shape: '', + count: 0, + truncated: false + } + ]; + verifyVariables(wrapper, targetVariables); + + // Add another variable and check it + await addCodeImpartial(wrapper, basicCode, true); + + targetVariables = [ + { + name: 'a', + value: '1', + supportsDataExplorer: false, + type: 'int', + size: 54, + shape: '', + count: 0, + truncated: false + }, + { + name: 'value', + value: 'hello world', + supportsDataExplorer: false, + type: 'str', + size: 54, + shape: '', + count: 0, + truncated: false + } + ]; + verifyVariables(wrapper, targetVariables); + + // Add a second variable and check it + await addCodeImpartial(wrapper, basicCode2, true); + + targetVariables = [ + { + name: 'a', + value: '1', + supportsDataExplorer: false, + type: 'int', + size: 54, + shape: '', + count: 0, + truncated: false + }, + { + name: 'value', + value: 'hello world', + supportsDataExplorer: false, + type: 'str', + size: 54, + shape: '', + count: 0, + truncated: false + }, + // tslint:disable-next-line:quotemark + { + name: 'value2', + value: 'hello world 2', + supportsDataExplorer: false, + type: 'str', + size: 54, + shape: '', + count: 0, + truncated: false + } + ]; + verifyVariables(wrapper, targetVariables); + }, + () => { + return Promise.resolve(ioc); + } + ); + + // Test our display of basic types. We render 8 rows by default so only 8 values per test + runInteractiveTest( + 'Variable explorer - Types A', + async () => { + const basicCode: string = `myList = [1, 2, 3] +mySet = set([42]) +myDict = {'a': 1} +myTuple = 1,2,3,4,5,6,7,8,9`; + + const { mount } = await getOrCreateInteractiveWindow(ioc); + const wrapper = mount.wrapper; + + openVariableExplorer(wrapper); + + await addCodeImpartial(wrapper, 'a=1\na'); + await addCodeImpartial(wrapper, basicCode, true, 2); + + const targetVariables: IJupyterVariable[] = [ + { + name: 'a', + value: '1', + supportsDataExplorer: false, + type: 'int', + size: 54, + shape: '', + count: 0, + truncated: false + }, + // tslint:disable-next-line:quotemark + { + name: 'myDict', + value: "{'a': 1}", + supportsDataExplorer: true, + type: 'dict', + size: 54, + shape: '', + count: 1, + truncated: false + }, + { + name: 'myList', + value: '[1, 2, 3]', + supportsDataExplorer: true, + type: 'list', + size: 54, + shape: '', + count: 3, + truncated: false + }, + // Set can vary between python versions, so just don't both to check the value, just see that we got it + { + name: 'mySet', + value: undefined, + supportsDataExplorer: false, + type: 'set', + size: 54, + shape: '', + count: 1, + truncated: false + }, + { + name: 'myTuple', + value: '(1, 2, 3, 4, 5, 6, 7, 8, 9)', + supportsDataExplorer: false, + type: 'tuple', + size: 54, + shape: '9', + count: 0, + truncated: false + } + ]; + verifyVariables(wrapper, targetVariables); + // Step into the first cell over again. Should have the same variables + if (runByLine) { + await verifyAfterStep(ioc, wrapper, () => { + verifyVariables(wrapper, targetVariables); + return Promise.resolve(); + }); + } + + // Restart the kernel and repeat + const iw = await getOrCreateInteractiveWindow(ioc); + + const variablesComplete = iw.mount.waitForMessage(InteractiveWindowMessages.VariablesComplete); + await iw.window.restartKernel(); + await variablesComplete; // Restart should cause a variable refresh + + // Should have no variables + verifyVariables(wrapper, []); + + await addCodeImpartial(wrapper, 'a=1\na', true); + await addCodeImpartial(wrapper, basicCode, true); + + verifyVariables(wrapper, targetVariables); + // Step into the first cell over again. Should have the same variables + if (runByLine) { + await verifyAfterStep(ioc, wrapper, () => { + verifyVariables(wrapper, targetVariables); + return Promise.resolve(); + }); + } + }, + () => { + return Promise.resolve(ioc); + } + ); + + runInteractiveTest( + 'Variable explorer - Basic B', + async () => { + const basicCode: string = `import numpy as np +import pandas as pd +myComplex = complex(1, 1) +myInt = 99999999 +myFloat = 9999.9999 +mynpArray = np.array([1.0, 2.0, 3.0]) +myDataframe = pd.DataFrame(mynpArray) +mySeries = myDataframe[0] +`; + const { mount } = await getOrCreateInteractiveWindow(ioc); + const wrapper = mount.wrapper; + + openVariableExplorer(wrapper); + + await addCodeImpartial(wrapper, 'a=1\na'); + await addCodeImpartial(wrapper, basicCode, true, 2); + + const targetVariables: IJupyterVariable[] = [ + { + name: 'a', + value: '1', + supportsDataExplorer: false, + type: 'int', + size: 54, + shape: '', + count: 0, + truncated: false + }, + { + name: 'myComplex', + value: '(1+1j)', + supportsDataExplorer: false, + type: 'complex', + size: 54, + shape: '', + count: 0, + truncated: false + }, + { + name: 'myDataframe', + value: `0 +0 1.0 +1 2.0 +2 3.0`, + supportsDataExplorer: true, + type: 'DataFrame', + size: 54, + shape: '(3, 1)', + count: 0, + truncated: false + }, + { + name: 'myFloat', + value: '9999.9999', + supportsDataExplorer: false, + type: 'float', + size: 58, + shape: '', + count: 0, + truncated: false + }, + { + name: 'myInt', + value: '99999999', + supportsDataExplorer: false, + type: 'int', + size: 56, + shape: '', + count: 0, + truncated: false + }, + // tslint:disable:no-trailing-whitespace + { + name: 'mySeries', + value: `0 1.0 +1 2.0 +2 3.0 +Name: 0, dtype: float64`, + supportsDataExplorer: true, + type: 'Series', + size: 54, + shape: '(3,)', + count: 0, + truncated: false + }, + { + name: 'mynpArray', + value: '[1. 2. 3.]', + supportsDataExplorer: true, + type: 'ndarray', + size: 54, + shape: '(3,)', + count: 0, + truncated: false + } + ]; + verifyVariables(wrapper, targetVariables); + + // Step into the first cell over again. Should have the same variables + if (runByLine) { + targetVariables[6].value = 'array([1., 2., 3.])'; // Debugger shows np array differently + await verifyAfterStep(ioc, wrapper, () => { + verifyVariables(wrapper, targetVariables); + return Promise.resolve(); + }); + } + }, + () => { + return Promise.resolve(ioc); + } + ); + + function generateVar(v: number): IJupyterVariable { + const valueEntry = Math.pow(v, 2) % 17; + const expectedValue = + valueEntry < 10 + ? `[${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, <...> , ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}]` + : `[${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, <...> , ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}, ${valueEntry}]`; + return { + name: `var${v}`, + value: expectedValue, + supportsDataExplorer: true, + type: 'list', + size: 54, + shape: '', + count: 100000, + truncated: false + }; + } + + // Test our limits. Create 1050 items. Do this with both to make + // sure no perf problems with one or the other and to smoke test the native editor + runDoubleTest( + 'Variable explorer - A lot of items', + async (t) => { + const basicCode: string = `for _i in range(1050): + exec("var{}=[{} ** 2 % 17 for _l in range(100000)]".format(_i, _i))`; + + const { mount } = t === 'native' ? await createNewEditor(ioc) : await getOrCreateInteractiveWindow(ioc); + const wrapper = mount.wrapper; + openVariableExplorer(wrapper); + + // Wait for two variable completes so we get the visible list (should be about 16 items when finished) + await addCodeImpartial(wrapper, basicCode, true, 2); + + const allVariables: IJupyterVariable[] = rangeInclusive(0, 1050) + .map(generateVar) + .sort((a: IJupyterVariable, b: IJupyterVariable) => a.name.localeCompare(b.name)); + + const targetVariables = allVariables.slice(0, 14); + verifyVariables(wrapper, targetVariables); + + // Force a scroll to the bottom + const complete = mount.waitForMessage(InteractiveWindowMessages.VariablesComplete); + const grid = wrapper.find(AdazzleReactDataGrid); + const viewPort = grid.find('Viewport').instance(); + const rowHeight = (viewPort.props as any).rowHeight as number; + const scrollTop = (allVariables.length - 11) * rowHeight; + (viewPort as any).onScroll({ scrollTop, scrollLeft: 0 }); + + // Wait for a variable complete + await complete; + + // Now we should have the bottom. For some reason only 10 come back here. + const bottomVariables = allVariables.slice(1041, 1050); + verifyVariables(wrapper, bottomVariables); + + // Step into the first cell over again. Should have the same variables + if (runByLine && t === 'interactive') { + // Remove values, don't bother checking them as they'll be different from the debugger + const nonValued = bottomVariables + .map((v) => { + return { ...v, value: undefined }; + }) + .slice(0, 9); + await verifyAfterStep( + ioc, + wrapper, + () => { + verifyVariables(wrapper, nonValued); + return Promise.resolve(); + }, + 2 // 2 refreshes because the variable explorer is scrolled to the bottom. + ); + } + }, + () => { + return Promise.resolve(ioc); + } + ); + + runInteractiveTest( + 'Variable explorer - DataFrameInfo and Rows', + async () => { + const basicCode: string = `import numpy as np +import pandas as pd +mynpArray = np.array([1.0, 2.0, 3.0]) +myDataframe = pd.DataFrame(mynpArray) +mySeries = myDataframe[0] +`; + const { mount } = await getOrCreateInteractiveWindow(ioc); + const wrapper = mount.wrapper; + + openVariableExplorer(wrapper); + + await addCodeImpartial(wrapper, 'a=1\na'); + await addCodeImpartial(wrapper, basicCode, true); + + await verifyCanFetchData(ioc, 2, 'myDataframe', [1, 2, 3]); + await verifyCanFetchData(ioc, 2, 'mynpArray', [1, 2, 3]); + await verifyCanFetchData(ioc, 2, 'mySeries', [1, 2, 3]); + + // Step into the first cell over again. Should have the same variables + if (runByLine) { + await verifyAfterStep(ioc, wrapper, async (_w) => { + await verifyCanFetchData(ioc, 2, 'myDataframe', [1, 2, 3]); + await verifyCanFetchData(ioc, 2, 'mynpArray', [1, 2, 3]); + await verifyCanFetchData(ioc, 2, 'mySeries', [1, 2, 3]); + }); + } + }, + () => { + return Promise.resolve(ioc); + } + ); + }); +}); diff --git a/src/test/debugger/common/constants.ts b/src/test/debugger/common/constants.ts new file mode 100644 index 000000000000..a9bcc64f1a24 --- /dev/null +++ b/src/test/debugger/common/constants.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// Sometimes PTVSD can take a while for thread & other events to be reported. +export const DEBUGGER_TIMEOUT = 20000; diff --git a/src/test/debugger/common/protocolparser.test.ts b/src/test/debugger/common/protocolparser.test.ts new file mode 100644 index 000000000000..bb291ebd172a --- /dev/null +++ b/src/test/debugger/common/protocolparser.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { PassThrough } from 'stream'; +import { createDeferred } from '../../../client/common/utils/async'; +import { ProtocolParser } from '../../../client/debugger/extension/helpers/protocolParser'; +import { sleep } from '../../common'; + +suite('Debugging - Protocol Parser', () => { + test('Test request, response and event messages', async () => { + const stream = new PassThrough(); + + const protocolParser = new ProtocolParser(); + protocolParser.connect(stream); + let messagesDetected = 0; + protocolParser.on('data', () => (messagesDetected += 1)); + const requestDetected = new Promise<boolean>((resolve) => { + protocolParser.on('request_initialize', () => resolve(true)); + }); + const responseDetected = new Promise<boolean>((resolve) => { + protocolParser.on('response_initialize', () => resolve(true)); + }); + const eventDetected = new Promise<boolean>((resolve) => { + protocolParser.on('event_initialized', () => resolve(true)); + }); + + stream.write( + 'Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}' + ); + await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); + + stream.write( + 'Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}' + ); + await expect(responseDetected).to.eventually.equal(true, 'response not parsed'); + + stream.write('Content-Length: 63\r\n\r\n{"type": "event", "seq": 1, "event": "initialized", "body": {}}'); + await expect(eventDetected).to.eventually.equal(true, 'event not parsed'); + + expect(messagesDetected).to.be.equal(3, 'incorrect number of protocol messages'); + }); + test('Ensure messages are not received after disposing the parser', async () => { + const stream = new PassThrough(); + + const protocolParser = new ProtocolParser(); + protocolParser.connect(stream); + let messagesDetected = 0; + protocolParser.on('data', () => (messagesDetected += 1)); + const requestDetected = new Promise<boolean>((resolve) => { + protocolParser.on('request_initialize', () => resolve(true)); + }); + stream.write( + 'Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}' + ); + await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); + + protocolParser.dispose(); + + const responseDetected = createDeferred<boolean>(); + protocolParser.on('response_initialize', () => responseDetected.resolve(true)); + + stream.write( + 'Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}' + ); + // Wait for messages to go through and get parsed (unnecenssary, but add for testing edge cases). + await sleep(1000); + expect(responseDetected.completed).to.be.equal(false, 'Promise should not have resolved'); + }); +}); diff --git a/src/test/debugger/envVars.test.ts b/src/test/debugger/envVars.test.ts new file mode 100644 index 000000000000..37eedf8d316d --- /dev/null +++ b/src/test/debugger/envVars.test.ts @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-string-literal no-unused-expression chai-vague-errors max-func-body-length no-any + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as shortid from 'shortid'; +import { ICurrentProcess, IPathUtils } from '../../client/common/types'; +import { IEnvironmentVariablesService } from '../../client/common/variables/types'; +import { + DebugEnvironmentVariablesHelper, + IDebugEnvironmentVariablesService +} from '../../client/debugger/extension/configuration/resolvers/helper'; +import { ConsoleType, LaunchRequestArguments } from '../../client/debugger/types'; +import { isOs, OSType } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; + +use(chaiAsPromised); + +suite('Resolving Environment Variables when Debugging', () => { + let ioc: UnitTestIocContainer; + let debugEnvParser: IDebugEnvironmentVariablesService; + let pathVariableName: string; + let mockProcess: ICurrentProcess; + + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + await initialize(); + }); + + setup(async () => { + initializeDI(); + await initializeTest(); + const envParser = ioc.serviceContainer.get<IEnvironmentVariablesService>(IEnvironmentVariablesService); + const pathUtils = ioc.serviceContainer.get<IPathUtils>(IPathUtils); + mockProcess = ioc.serviceContainer.get<ICurrentProcess>(ICurrentProcess); + debugEnvParser = new DebugEnvironmentVariablesHelper(envParser, pathUtils, mockProcess); + pathVariableName = pathUtils.getPathVariableName(); + }); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await ioc.dispose(); + await closeActiveWindows(); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerProcessTypes(); + ioc.registerFileSystemTypes(); + ioc.registerVariableTypes(); + ioc.registerMockProcess(); + } + + async function testBasicProperties(console: ConsoleType, expectedNumberOfVariables: number) { + const args = ({ + program: '', + pythonPath: '', + args: [], + envFile: '', + console + // tslint:disable-next-line:no-any + } as any) as LaunchRequestArguments; + + const envVars = await debugEnvParser.getEnvironmentVariables(args); + expect(envVars).not.be.undefined; + expect(Object.keys(envVars)).lengthOf(expectedNumberOfVariables, 'Incorrect number of variables'); + expect(envVars).to.have.property('PYTHONUNBUFFERED', '1', 'Property not found'); + expect(envVars).to.have.property('PYTHONIOENCODING', 'UTF-8', 'Property not found'); + } + + test('Confirm basic environment variables exist when launched in external terminal', () => + testBasicProperties('externalTerminal', 2)); + + test('Confirm basic environment variables exist when launched in intergrated terminal', () => + testBasicProperties('integratedTerminal', 2)); + + test('Confirm basic environment variables exist when launched in debug console', async () => { + let expectedNumberOfVariables = Object.keys(mockProcess.env).length; + if (mockProcess.env['PYTHONUNBUFFERED'] === undefined) { + expectedNumberOfVariables += 1; + } + if (mockProcess.env['PYTHONIOENCODING'] === undefined) { + expectedNumberOfVariables += 1; + } + await testBasicProperties('internalConsole', expectedNumberOfVariables); + }); + + async function testJsonEnvVariables(console: ConsoleType, expectedNumberOfVariables: number) { + const prop1 = shortid.generate(); + const prop2 = shortid.generate(); + const prop3 = shortid.generate(); + const env: Record<string, string> = {}; + env[prop1] = prop1; + env[prop2] = prop2; + mockProcess.env[prop3] = prop3; + + const args = ({ + program: '', + pythonPath: '', + args: [], + envFile: '', + console, + env + // tslint:disable-next-line:no-any + } as any) as LaunchRequestArguments; + + const envVars = await debugEnvParser.getEnvironmentVariables(args); + + // tslint:disable-next-line:no-unused-expression chai-vague-errors + expect(envVars).not.be.undefined; + expect(Object.keys(envVars)).lengthOf(expectedNumberOfVariables, 'Incorrect number of variables'); + expect(envVars).to.have.property('PYTHONUNBUFFERED', '1', 'Property not found'); + expect(envVars).to.have.property('PYTHONIOENCODING', 'UTF-8', 'Property not found'); + expect(envVars).to.have.property(prop1, prop1, 'Property not found'); + expect(envVars).to.have.property(prop2, prop2, 'Property not found'); + + if (console === 'internalConsole') { + expect(envVars).to.have.property(prop3, prop3, 'Property not found'); + } else { + expect(envVars).not.to.have.property(prop3, prop3, 'Property not found'); + } + } + + test('Confirm json environment variables exist when launched in external terminal', () => + testJsonEnvVariables('externalTerminal', 2 + 2)); + + test('Confirm json environment variables exist when launched in intergrated terminal', () => + testJsonEnvVariables('integratedTerminal', 2 + 2)); + + test('Confirm json environment variables exist when launched in debug console', async () => { + // Add 3 for the 3 new json env variables + let expectedNumberOfVariables = Object.keys(mockProcess.env).length + 3; + if (mockProcess.env['PYTHONUNBUFFERED'] === undefined) { + expectedNumberOfVariables += 1; + } + if (mockProcess.env['PYTHONIOENCODING'] === undefined) { + expectedNumberOfVariables += 1; + } + await testJsonEnvVariables('internalConsole', expectedNumberOfVariables); + }); + + async function testAppendingOfPaths( + console: ConsoleType, + expectedNumberOfVariables: number, + removePythonPath: boolean + ) { + if (removePythonPath && mockProcess.env.PYTHONPATH !== undefined) { + delete mockProcess.env.PYTHONPATH; + } + + const customPathToAppend = shortid.generate(); + const customPythonPathToAppend = shortid.generate(); + const prop1 = shortid.generate(); + const prop2 = shortid.generate(); + const prop3 = shortid.generate(); + + const env: Record<string, string> = {}; + env[pathVariableName] = customPathToAppend; + env['PYTHONPATH'] = customPythonPathToAppend; + env[prop1] = prop1; + env[prop2] = prop2; + mockProcess.env[prop3] = prop3; + + const args = ({ + program: '', + pythonPath: '', + args: [], + envFile: '', + console, + env + } as any) as LaunchRequestArguments; + + const envVars = await debugEnvParser.getEnvironmentVariables(args); + expect(envVars).not.be.undefined; + expect(Object.keys(envVars)).lengthOf(expectedNumberOfVariables, 'Incorrect number of variables'); + expect(envVars).to.have.property('PYTHONPATH'); + expect(envVars).to.have.property(pathVariableName); + expect(envVars).to.have.property('PYTHONUNBUFFERED', '1', 'Property not found'); + expect(envVars).to.have.property('PYTHONIOENCODING', 'UTF-8', 'Property not found'); + expect(envVars).to.have.property(prop1, prop1, 'Property not found'); + expect(envVars).to.have.property(prop2, prop2, 'Property not found'); + + if (console === 'internalConsole') { + expect(envVars).to.have.property(prop3, prop3, 'Property not found'); + } else { + expect(envVars).not.to.have.property(prop3, prop3, 'Property not found'); + } + + // Confirm the paths have been appended correctly. + const expectedPath = `${customPathToAppend}${path.delimiter}${mockProcess.env[pathVariableName]}`; + expect(envVars).to.have.property(pathVariableName, expectedPath, 'PATH is not correct'); + + // Confirm the paths have been appended correctly. + let expectedPythonPath = customPythonPathToAppend; + if (typeof mockProcess.env.PYTHONPATH === 'string' && mockProcess.env.PYTHONPATH.length > 0) { + expectedPythonPath = customPythonPathToAppend + path.delimiter + mockProcess.env.PYTHONPATH; + } + expect(envVars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH is not correct'); + + if (console === 'internalConsole') { + // All variables in current process must be in here + expect(Object.keys(envVars).length).greaterThan( + Object.keys(mockProcess.env).length, + 'Variables is not a subset' + ); + Object.keys(mockProcess.env).forEach((key) => { + if (key === pathVariableName || key === 'PYTHONPATH') { + return; + } + expect(mockProcess.env[key]).equal( + envVars[key], + `Value for the environment variable '${key}' is incorrect.` + ); + }); + } + } + + test('Confirm paths get appended correctly when using json variables and launched in external terminal', async function () { + // test is flakey on windows, path separator problems. GH issue #4758 + if (isOs(OSType.Windows)) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + await testAppendingOfPaths('externalTerminal', 6, false); + }); + + test('Confirm paths get appended correctly when using json variables and launched in integrated terminal', async function () { + // test is flakey on windows, path separator problems. GH issue #4758 + if (isOs(OSType.Windows)) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + await testAppendingOfPaths('integratedTerminal', 6, false); + }); + + test('Confirm paths get appended correctly when using json variables and launched in debug console', async function () { + // test is flakey on windows, path separator problems. GH issue #4758 + if (isOs(OSType.Windows)) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + + // Add 3 for the 3 new json env variables + let expectedNumberOfVariables = Object.keys(mockProcess.env).length + 3; + if (mockProcess.env['PYTHONUNBUFFERED'] === undefined) { + expectedNumberOfVariables += 1; + } + if (mockProcess.env['PYTHONPATH'] === undefined) { + expectedNumberOfVariables += 1; + } + if (mockProcess.env['PYTHONIOENCODING'] === undefined) { + expectedNumberOfVariables += 1; + } + await testAppendingOfPaths('internalConsole', expectedNumberOfVariables, false); + }); +}); diff --git a/src/test/debugger/extension/adapter/activator.unit.test.ts b/src/test/debugger/extension/adapter/activator.unit.test.ts new file mode 100644 index 000000000000..e449d3d7b2cb --- /dev/null +++ b/src/test/debugger/extension/adapter/activator.unit.test.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { IExtensionSingleActivationService } from '../../../../client/activation/types'; +import { DebugService } from '../../../../client/common/application/debugService'; +import { IDebugService } from '../../../../client/common/application/types'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; +import { DebugAdapterActivator } from '../../../../client/debugger/extension/adapter/activator'; +import { DebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/adapter/factory'; +import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging'; +import { OutdatedDebuggerPromptFactory } from '../../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; +import { AttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/factory'; +import { IAttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/types'; +import { + IDebugAdapterDescriptorFactory, + IDebugSessionLoggingFactory, + IOutdatedDebuggerPromptFactory +} from '../../../../client/debugger/extension/types'; +import { clearTelemetryReporter } from '../../../../client/telemetry'; +import { noop } from '../../../core'; + +// tslint:disable-next-line: max-func-body-length +suite('Debugging - Adapter Factory and logger Registration', () => { + let activator: IExtensionSingleActivationService; + let debugService: IDebugService; + let descriptorFactory: IDebugAdapterDescriptorFactory; + let loggingFactory: IDebugSessionLoggingFactory; + let debuggerPromptFactory: IOutdatedDebuggerPromptFactory; + let disposableRegistry: IDisposableRegistry; + let attachFactory: IAttachProcessProviderFactory; + + setup(() => { + const configurationService = mock(ConfigurationService); + + when(configurationService.getSettings(undefined)).thenReturn(({ + experiments: { enabled: true } + // tslint:disable-next-line: no-any + } as any) as IPythonSettings); + attachFactory = mock(AttachProcessProviderFactory); + + debugService = mock(DebugService); + descriptorFactory = mock(DebugAdapterDescriptorFactory); + loggingFactory = mock(DebugSessionLoggingFactory); + debuggerPromptFactory = mock(OutdatedDebuggerPromptFactory); + disposableRegistry = []; + activator = new DebugAdapterActivator( + instance(debugService), + instance(descriptorFactory), + instance(loggingFactory), + instance(debuggerPromptFactory), + disposableRegistry, + instance(attachFactory) + ); + }); + + teardown(() => { + clearTelemetryReporter(); + }); + + test('Register Debug adapter factory', async () => { + await activator.activate(); + + verify(debugService.registerDebugAdapterTrackerFactory('python', instance(loggingFactory))).once(); + verify(debugService.registerDebugAdapterTrackerFactory('python', instance(debuggerPromptFactory))).once(); + verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(descriptorFactory))).once(); + }); + + test('Register a disposable item', async () => { + const disposable = { dispose: noop }; + when(debugService.registerDebugAdapterTrackerFactory(anything(), anything())).thenReturn(disposable); + when(debugService.registerDebugAdapterDescriptorFactory(anything(), anything())).thenReturn(disposable); + + await activator.activate(); + + assert.deepEqual(disposableRegistry, [disposable, disposable, disposable]); + }); +}); diff --git a/src/test/debugger/extension/adapter/adapter.test.ts b/src/test/debugger/extension/adapter/adapter.test.ts new file mode 100644 index 000000000000..29baffc613bc --- /dev/null +++ b/src/test/debugger/extension/adapter/adapter.test.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { openFile } from '../../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../../../initialize'; +import { DebuggerFixture } from '../../utils'; + +const WS_ROOT = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test'); + +function resolveWSFile(wsRoot: string, ...filePath: string[]): string { + return path.join(wsRoot, ...filePath); +} + +suite('Debugger Integration', () => { + const file = resolveWSFile(WS_ROOT, 'pythonFiles', 'debugging', 'wait_for_file.py'); + const doneFile = resolveWSFile(WS_ROOT, 'should-not-exist'); + const outFile = resolveWSFile(WS_ROOT, 'output.txt'); + const resource = vscode.Uri.file(file); + const defaultScriptArgs = [doneFile]; + let workspaceRoot: vscode.WorkspaceFolder; + let fix: DebuggerFixture; + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + await initialize(); + const ws = vscode.workspace.getWorkspaceFolder(resource); + workspaceRoot = ws!; + expect(workspaceRoot).to.not.equal(undefined, 'missing workspace root'); + }); + setup(async () => { + fix = new DebuggerFixture(); + await initializeTest(); + await openFile(file); + }); + teardown(async () => { + await fix.cleanUp(); + fix.addFSCleanup(outFile); + await closeActiveWindows(); + }); + async function setDone() { + await fs.writeFile(doneFile, ''); + fix.addFSCleanup(doneFile); + } + + type ConfigName = string; + type ScriptArgs = string[]; + const tests: { [key: string]: [ConfigName, ScriptArgs] } = { + // prettier-ignore + 'launch': ['launch a file', [...defaultScriptArgs, outFile]], + // prettier-ignore + 'attach': ['attach to a local port', defaultScriptArgs], + 'attach to PID': ['attach to a local PID', defaultScriptArgs] + // For now we do not worry about "test" debugging. + }; + + suite('run to end', () => { + for (const kind of Object.keys(tests)) { + if (kind === 'attach to PID') { + // Attach-to-pid is still a little finicky + // so we're skipping it for now. + continue; + } + const [configName, scriptArgs] = tests[kind]; + test(kind, async () => { + const session = fix.resolveDebugger(configName, file, scriptArgs, workspaceRoot); + await session.start(); + // Any debugger ops would go here. + await new Promise((r) => setTimeout(r, 300)); // 0.3 seconds + await setDone(); + const result = await session.waitUntilDone(); + + expect(result.exitCode).to.equal(0, 'bad exit code'); + const output = result.stdout !== '' ? result.stdout : fs.readFileSync(outFile).toString(); + expect(output.trim().endsWith('done!')).to.equal(true, `bad output\n${output}`); + }); + } + }); + + suite('handles breakpoint', () => { + for (const kind of ['launch', 'attach']) { + if (kind === 'attach') { + // The test isn't working quite right for attach + // so we skip it for now. + continue; + } + const [configName, scriptArgs] = tests[kind]; + test(kind, async () => { + const session = fix.resolveDebugger(configName, file, scriptArgs, workspaceRoot); + const bp = session.addBreakpoint(file, 21); // line: "time.sleep()" + await session.start(); + await session.waitForBreakpoint(bp); + await setDone(); + const result = await session.waitUntilDone(); + + expect(result.exitCode).to.equal(0, 'bad exit code'); + const output = result.stdout !== '' ? result.stdout : fs.readFileSync(outFile).toString(); + expect(output.trim().endsWith('done!')).to.equal(true, `bad output\n${output}`); + }); + } + }); +}); diff --git a/src/test/debugger/extension/adapter/factory.unit.test.ts b/src/test/debugger/extension/adapter/factory.unit.test.ts new file mode 100644 index 000000000000..75393f95960c --- /dev/null +++ b/src/test/debugger/extension/adapter/factory.unit.test.ts @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +// tslint:disable-next-line: match-default-export-name +import rewiremock from 'rewiremock'; +import { SemVer } from 'semver'; +import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; +import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../../../client/common/application/types'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { IPythonSettings } from '../../../../client/common/types'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { DebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/adapter/factory'; +import { IDebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../../client/interpreter/interpreterService'; +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; +import { clearTelemetryReporter } from '../../../../client/telemetry'; +import { EventName } from '../../../../client/telemetry/constants'; + +use(chaiAsPromised); + +// tslint:disable-next-line: max-func-body-length +suite('Debugging - Adapter Factory', () => { + let factory: IDebugAdapterDescriptorFactory; + let interpreterService: IInterpreterService; + let appShell: IApplicationShell; + + const nodeExecutable = undefined; + const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy', 'adapter'); + const pythonPath = path.join('path', 'to', 'python', 'interpreter'); + const interpreter = { + architecture: Architecture.Unknown, + path: pythonPath, + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown, + version: new SemVer('3.7.4-test') + }; + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + + class Reporter { + public static eventNames: string[] = []; + public static properties: Record<string, string>[] = []; + public static measures: {}[] = []; + public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { + Reporter.eventNames.push(eventName); + Reporter.properties.push(properties!); + Reporter.measures.push(measures!); + } + } + + setup(() => { + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + rewiremock.enable(); + rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + + const configurationService = mock(ConfigurationService); + when(configurationService.getSettings(undefined)).thenReturn(({ + experiments: { enabled: true } + // tslint:disable-next-line: no-any + } as any) as IPythonSettings); + + interpreterService = mock(InterpreterService); + appShell = mock(ApplicationShell); + + when(interpreterService.getInterpreterDetails(pythonPath)).thenResolve(interpreter); + when(interpreterService.getInterpreters(anything())).thenResolve([interpreter]); + + factory = new DebugAdapterDescriptorFactory(instance(interpreterService), instance(appShell)); + }); + + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + Reporter.properties = []; + Reporter.eventNames = []; + Reporter.measures = []; + rewiremock.disable(); + clearTelemetryReporter(); + }); + + function createSession(config: Partial<DebugConfiguration>, workspaceFolder?: WorkspaceFolder): DebugSession { + return { + configuration: { name: '', request: 'launch', type: 'python', ...config }, + id: '', + name: 'python', + type: 'python', + workspaceFolder, + customRequest: () => Promise.resolve() + }; + } + + test('Return the value of configuration.pythonPath as the current python path if it exists', async () => { + const session = createSession({ pythonPath }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepEqual(descriptor, debugExecutable); + }); + + test('Return the path of the active interpreter as the current python path, it exists and configuration.pythonPath is not defined', async () => { + const session = createSession({}); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepEqual(descriptor, debugExecutable); + }); + + test('Return the path of the first available interpreter as the current python path, configuration.pythonPath is not defined and there is no active interpreter', async () => { + const session = createSession({}); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepEqual(descriptor, debugExecutable); + }); + + test('Display a message if no python interpreter is set', async () => { + when(interpreterService.getInterpreters(anything())).thenResolve([]); + const session = createSession({}); + + const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); + + await expect(promise).to.eventually.be.rejectedWith('Debug Adapter Executable not provided'); + verify(appShell.showErrorMessage(anyString())).once(); + }); + + test('Return Debug Adapter server if request is "attach", and port is specified directly', async () => { + const session = createSession({ request: 'attach', port: 5678, host: 'localhost' }); + const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + // Interpreter not needed for host/port + verify(interpreterService.getInterpreters(anything())).never(); + assert.deepEqual(descriptor, debugServer); + }); + + test('Return Debug Adapter server if request is "attach", and connect is specified', async () => { + const session = createSession({ request: 'attach', connect: { port: 5678, host: 'localhost' } }); + const debugServer = new DebugAdapterServer( + session.configuration.connect.port, + session.configuration.connect.host + ); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + // Interpreter not needed for connect + verify(interpreterService.getInterpreters(anything())).never(); + assert.deepEqual(descriptor, debugServer); + }); + + test('Return Debug Adapter executable if request is "attach", and listen is specified', async () => { + const session = createSession({ request: 'attach', listen: { port: 5678, host: 'localhost' } }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + assert.deepEqual(descriptor, debugExecutable); + }); + + test('Throw error if request is "attach", and neither port, processId, listen, nor connect is specified', async () => { + const session = createSession({ + request: 'attach', + port: undefined, + processId: undefined, + listen: undefined, + connect: undefined + }); + + const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); + + await expect(promise).to.eventually.be.rejectedWith( + '"request":"attach" requires either "connect", "listen", or "processId"' + ); + }); + + test('Pass the --log-dir argument to debug adapter if configuration.logToFile is set', async () => { + const session = createSession({ logToFile: true }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [ + debugAdapterPath, + '--log-dir', + EXTENSION_ROOT_DIR + ]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepEqual(descriptor, debugExecutable); + }); + + test("Don't pass the --log-dir argument to debug adapter if configuration.logToFile is not set", async () => { + const session = createSession({}); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepEqual(descriptor, debugExecutable); + }); + + test("Don't pass the --log-dir argument to debugger if configuration.logToFile is set to false", async () => { + const session = createSession({ logToFile: false }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepEqual(descriptor, debugExecutable); + }); + + test('Send attach to local process telemetry if attaching to a local process', async () => { + const session = createSession({ request: 'attach', processId: 1234 }); + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.ok(Reporter.eventNames.includes(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS)); + }); + + test("Don't send any telemetry if not attaching to a local process", async () => { + const session = createSession({}); + + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.ok(Reporter.eventNames.includes(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH)); + }); + + test('Use custom debug adapter path when specified', async () => { + const customAdapterPath = 'custom/debug/adapter/path'; + const session = createSession({ debugAdapterPath: customAdapterPath }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [customAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepEqual(descriptor, debugExecutable); + }); +}); diff --git a/src/test/debugger/extension/adapter/logging.unit.test.ts b/src/test/debugger/extension/adapter/logging.unit.test.ts new file mode 100644 index 000000000000..cae4d7b19328 --- /dev/null +++ b/src/test/debugger/extension/adapter/logging.unit.test.ts @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { DebugSession, WorkspaceFolder } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging'; + +// tslint:disable-next-line: max-func-body-length +suite('Debugging - Session Logging', () => { + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + let loggerFactory: DebugSessionLoggingFactory; + let fsService: FileSystem; + let writeStream: fs.WriteStream; + + setup(() => { + fsService = mock(FileSystem); + writeStream = mock(fs.WriteStream); + + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + + loggerFactory = new DebugSessionLoggingFactory(instance(fsService)); + }); + + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + }); + + function createSession(id: string, workspaceFolder?: WorkspaceFolder): DebugSession { + return { + configuration: { + name: '', + request: 'launch', + type: 'python' + }, + id: id, + name: 'python', + type: 'python', + workspaceFolder, + customRequest: () => Promise.resolve() + }; + } + + function createSessionWithLogging(id: string, logToFile: boolean, workspaceFolder?: WorkspaceFolder): DebugSession { + const session = createSession(id, workspaceFolder); + session.configuration.logToFile = logToFile; + return session; + } + + class TestMessage implements DebugProtocol.ProtocolMessage { + public seq: number; + public type: string; + public id: number; + public format: string; + public variables?: { [key: string]: string }; + public sendTelemetry?: boolean; + public showUser?: boolean; + public url?: string; + public urlLabel?: string; + constructor(id: number, seq: number, type: string) { + this.id = id; + this.format = 'json'; + this.seq = seq; + this.type = type; + } + } + + test('Create logger using session without logToFile', async () => { + const session = createSession('test1'); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + + await loggerFactory.createDebugAdapterTracker(session); + + verify(fsService.createWriteStream(filePath)).never(); + }); + + test('Create logger using session with logToFile set to false', async () => { + const session = createSessionWithLogging('test2', false); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + + when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream)); + when(writeStream.write(anything())).thenReturn(true); + const logger = await loggerFactory.createDebugAdapterTracker(session); + if (logger) { + logger.onWillStartSession!(); + } + + verify(fsService.createWriteStream(filePath)).never(); + verify(writeStream.write(anything())).never(); + }); + + test('Create logger using session with logToFile set to true', async () => { + const session = createSessionWithLogging('test3', true); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + const logs: string[] = []; + + when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream)); + when(writeStream.write(anything())).thenCall((msg) => logs.push(msg)); + + const message = new TestMessage(1, 1, 'test-message'); + const logger = await loggerFactory.createDebugAdapterTracker(session); + + if (logger) { + logger.onWillStartSession!(); + assert.ok(logs.pop()!.includes('Starting Session')); + + logger.onDidSendMessage!(message); + const sentLog = logs.pop(); + assert.ok(sentLog!.includes('Client <-- Adapter')); + assert.ok(sentLog!.includes('test-message')); + + logger.onWillReceiveMessage!(message); + const receivedLog = logs.pop(); + assert.ok(receivedLog!.includes('Client --> Adapter')); + assert.ok(receivedLog!.includes('test-message')); + + logger.onWillStopSession!(); + assert.ok(logs.pop()!.includes('Stopping Session')); + + logger.onError!(new Error('test error message')); + assert.ok(logs.pop()!.includes('Error')); + + logger.onExit!(111, '222'); + const exitLog1 = logs.pop(); + assert.ok(exitLog1!.includes('Exit-Code: 111')); + assert.ok(exitLog1!.includes('Signal: 222')); + + logger.onExit!(undefined, undefined); + const exitLog2 = logs.pop(); + assert.ok(exitLog2!.includes('Exit-Code: 0')); + assert.ok(exitLog2!.includes('Signal: none')); + } + + verify(fsService.createWriteStream(filePath)).once(); + verify(writeStream.write(anything())).times(7); + assert.deepEqual(logs, []); + }); +}); diff --git a/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts new file mode 100644 index 000000000000..cb7643397552 --- /dev/null +++ b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; +import { DebugSession, WorkspaceFolder } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../../../client/common/application/types'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { BrowserService } from '../../../../client/common/net/browser'; +import { IBrowserService, IPythonSettings } from '../../../../client/common/types'; +import { createDeferred, sleep } from '../../../../client/common/utils/async'; +import { Common } from '../../../../client/common/utils/localize'; +import { OutdatedDebuggerPromptFactory } from '../../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; +import { clearTelemetryReporter } from '../../../../client/telemetry'; + +// tslint:disable-next-line: max-func-body-length +suite('Debugging - Outdated Debugger Prompt tests.', () => { + let promptFactory: OutdatedDebuggerPromptFactory; + let appShell: IApplicationShell; + let browserService: IBrowserService; + + const ptvsdOutputEvent: DebugProtocol.OutputEvent = { + seq: 1, + type: 'event', + event: 'output', + body: { category: 'telemetry', output: 'ptvsd', data: { packageVersion: '4.3.2' } } + }; + + const debugpyOutputEvent: DebugProtocol.OutputEvent = { + seq: 1, + type: 'event', + event: 'output', + body: { category: 'telemetry', output: 'debugpy', data: { packageVersion: '1.0.0' } } + }; + + setup(() => { + const configurationService = mock(ConfigurationService); + when(configurationService.getSettings(undefined)).thenReturn(({ + experiments: { enabled: true } + // tslint:disable-next-line: no-any + } as any) as IPythonSettings); + + appShell = mock(ApplicationShell); + browserService = mock(BrowserService); + promptFactory = new OutdatedDebuggerPromptFactory(instance(appShell), instance(browserService)); + }); + + teardown(() => { + clearTelemetryReporter(); + }); + + function createSession(workspaceFolder?: WorkspaceFolder): DebugSession { + return { + configuration: { + name: '', + request: 'launch', + type: 'python' + }, + id: 'test1', + name: 'python', + type: 'python', + workspaceFolder, + customRequest: () => Promise.resolve() + }; + } + + test('Show prompt when attaching to ptvsd, more info is NOT clicked', async () => { + when(appShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve(undefined)); + + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + if (prompter) { + prompter.onDidSendMessage!(ptvsdOutputEvent); + } + + verify(browserService.launch(anyString())).never(); + // First call should show info once + verify(appShell.showInformationMessage(anything(), anything())).once(); + assert(prompter); + + prompter!.onDidSendMessage!(ptvsdOutputEvent); + // Can't use deferred promise here + await sleep(1); + + verify(browserService.launch(anyString())).never(); + // Second time it should not be called, so overall count is one. + verify(appShell.showInformationMessage(anything(), anything())).once(); + }); + + test('Show prompt when attaching to ptvsd, more info is clicked', async () => { + when(appShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve(Common.moreInfo())); + const deferred = createDeferred(); + when(browserService.launch(anything())).thenCall(() => deferred.resolve()); + + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + assert(prompter); + + prompter!.onDidSendMessage!(ptvsdOutputEvent); + await deferred.promise; + + verify(browserService.launch(anything())).once(); + // First call should show info once + verify(appShell.showInformationMessage(anything(), anything())).once(); + + prompter!.onDidSendMessage!(ptvsdOutputEvent); + // The second call does not go through the same path. So we just give enough time for the + // operation to complete. + await sleep(1); + + verify(browserService.launch(anyString())).once(); + // Second time it should not be called, so overall count is one. + verify(appShell.showInformationMessage(anything(), anything())).once(); + }); + + test("Don't show prompt attaching to debugpy", async () => { + when(appShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve(undefined)); + + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + assert(prompter); + + prompter!.onDidSendMessage!(debugpyOutputEvent); + // Can't use deferred promise here + await sleep(1); + + verify(appShell.showInformationMessage(anything(), anything())).never(); + }); + + const someRequest: DebugProtocol.RunInTerminalRequest = { + seq: 1, + type: 'request', + command: 'runInTerminal', + arguments: { + cwd: '', + args: [''] + } + }; + const someEvent: DebugProtocol.ContinuedEvent = { + seq: 1, + type: 'event', + event: 'continued', + body: { threadId: 1, allThreadsContinued: true } + }; + // Notice that this is stdout, not telemetry event. + const someOutputEvent: DebugProtocol.OutputEvent = { + seq: 1, + type: 'event', + event: 'output', + body: { category: 'stdout', output: 'ptvsd' } + }; + + [someRequest, someEvent, someOutputEvent].forEach((message) => { + test(`Don't show prompt when non-telemetry events are seen: ${JSON.stringify(message)}`, async () => { + when(appShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve(undefined)); + + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + assert(prompter); + + prompter!.onDidSendMessage!(message); + // Can't use deferred promise here + await sleep(1); + + verify(appShell.showInformationMessage(anything(), anything())).never(); + }); + }); +}); diff --git a/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts b/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts new file mode 100644 index 000000000000..f1782d5e0429 --- /dev/null +++ b/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; +import '../../../../client/common/extensions'; +import * as launchers from '../../../../client/debugger/extension/adapter/remoteLaunchers'; + +suite('External debugpy Debugger Launcher', () => { + [ + { + testName: 'When path to debugpy does not contains spaces', + path: path.join('path', 'to', 'debugpy'), + expectedPath: 'path/to/debugpy' + }, + { + testName: 'When path to debugpy contains spaces', + path: path.join('path', 'to', 'debugpy', 'with spaces'), + expectedPath: '"path/to/debugpy/with spaces"' + } + ].forEach((testParams) => { + suite(testParams.testName, async () => { + test('Test remote debug launcher args (and do not wait for debugger to attach)', async () => { + const args = launchers.getDebugpyLauncherArgs( + { + host: 'something', + port: 1234, + waitUntilDebuggerAttaches: false + }, + testParams.path + ); + const expectedArgs = [testParams.expectedPath, '--listen', 'something:1234']; + expect(args).to.be.deep.equal(expectedArgs); + }); + test('Test remote debug launcher args (and wait for debugger to attach)', async () => { + const args = launchers.getDebugpyLauncherArgs( + { + host: 'something', + port: 1234, + waitUntilDebuggerAttaches: true + }, + testParams.path + ); + const expectedArgs = [testParams.expectedPath, '--listen', 'something:1234', '--wait-for-client']; + expect(args).to.be.deep.equal(expectedArgs); + }); + }); + }); +}); + +suite('Path To Debugger Package', () => { + const pathToPythonLibDir = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python'); + test('Path to debugpy debugger package', () => { + const actual = launchers.getDebugpyPackagePath(); + const expected = path.join(pathToPythonLibDir, 'debugpy'); + expect(actual).to.be.deep.equal(expected); + }); +}); diff --git a/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts b/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts new file mode 100644 index 000000000000..e74145bb26b2 --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { anything, instance, mock, verify } from 'ts-mockito'; +import { Disposable } from 'vscode'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../../client/common/application/commandManager'; +import { IApplicationShell, ICommandManager } from '../../../../client/common/application/types'; +import { Commands } from '../../../../client/common/constants'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { ProcessServiceFactory } from '../../../../client/common/process/processFactory'; +import { IProcessServiceFactory } from '../../../../client/common/process/types'; +import { IDisposableRegistry } from '../../../../client/common/types'; +import { AttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/factory'; + +suite('Attach to process - attach process provider factory', () => { + let applicationShell: IApplicationShell; + let commandManager: ICommandManager; + let platformService: IPlatformService; + let processServiceFactory: IProcessServiceFactory; + let disposableRegistry: IDisposableRegistry; + + let factory: AttachProcessProviderFactory; + + setup(() => { + applicationShell = mock(ApplicationShell); + commandManager = mock(CommandManager); + platformService = mock(PlatformService); + processServiceFactory = mock(ProcessServiceFactory); + disposableRegistry = []; + + factory = new AttachProcessProviderFactory( + instance(applicationShell), + instance(commandManager), + instance(platformService), + instance(processServiceFactory), + disposableRegistry + ); + }); + + test('Register commands should not fail', () => { + factory.registerCommands(); + + verify(commandManager.registerCommand(Commands.PickLocalProcess, anything(), anything())).once(); + assert.equal((disposableRegistry as Disposable[]).length, 1); + }); +}); diff --git a/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts b/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts new file mode 100644 index 000000000000..8ac667314944 --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts @@ -0,0 +1,458 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { ProcessService } from '../../../../client/common/process/proc'; +import { ProcessServiceFactory } from '../../../../client/common/process/processFactory'; +import { IProcessService, IProcessServiceFactory } from '../../../../client/common/process/types'; +import { OSType } from '../../../../client/common/utils/platform'; +import { AttachProcessProvider } from '../../../../client/debugger/extension/attachQuickPick/provider'; +import { PsProcessParser } from '../../../../client/debugger/extension/attachQuickPick/psProcessParser'; +import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; +import { WmicProcessParser } from '../../../../client/debugger/extension/attachQuickPick/wmicProcessParser'; + +// tslint:disable-next-line: max-func-body-length +suite('Attach to process - process provider', () => { + let platformService: IPlatformService; + let processService: IProcessService; + let processServiceFactory: IProcessServiceFactory; + + let provider: AttachProcessProvider; + + setup(() => { + platformService = mock(PlatformService); + processService = mock(ProcessService); + processServiceFactory = mock(ProcessServiceFactory); + when(processServiceFactory.create()).thenResolve(instance(processService)); + + provider = new AttachProcessProvider(instance(platformService), instance(processServiceFactory)); + }); + + test('The Linux process list command should be called if the platform is Linux', async () => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(true); + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +1 launchd launchd +41 syslogd syslogd +146 kextd kextd +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd' + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd' + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd' + } + ]; + when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput + }); + + const attachItems = await provider._getInternalProcessEntries(); + + verify( + processService.exec(PsProcessParser.psLinuxCommand.command, PsProcessParser.psLinuxCommand.args, anything()) + ).once(); + assert.deepEqual(attachItems, expectedOutput); + }); + + test('The macOS process list command should be called if the platform is macOS', async () => { + when(platformService.isMac).thenReturn(true); + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +1 launchd launchd +41 syslogd syslogd +146 kextd kextd +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd' + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd' + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd' + } + ]; + when(processService.exec(PsProcessParser.psDarwinCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput + }); + + const attachItems = await provider._getInternalProcessEntries(); + + verify( + processService.exec( + PsProcessParser.psDarwinCommand.command, + PsProcessParser.psDarwinCommand.args, + anything() + ) + ).once(); + assert.deepEqual(attachItems, expectedOutput); + }); + + test('The Windows process list command should be called if the platform is Windows', async () => { + const windowsOutput = `CommandLine=\r +Name=System\r +ProcessId=4\r +\r +\r +CommandLine=sihost.exe\r +Name=sihost.exe\r +ProcessId=5728\r +\r +\r +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r +Name=svchost.exe\r +ProcessId=5912\r +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '' + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe' + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc' + } + ]; + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(false); + when(platformService.isWindows).thenReturn(true); + when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ + stdout: windowsOutput + }); + + const attachItems = await provider._getInternalProcessEntries(); + + verify( + processService.exec(WmicProcessParser.wmicCommand.command, WmicProcessParser.wmicCommand.args, anything()) + ).once(); + assert.deepEqual(attachItems, expectedOutput); + }); + + test('An error should be thrown if the platform is neither Linux, macOS or Windows', async () => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(false); + when(platformService.isWindows).thenReturn(false); + when(platformService.osType).thenReturn(OSType.Unknown); + + const promise = provider._getInternalProcessEntries(); + + await expect(promise).to.eventually.be.rejectedWith(`Operating system '${OSType.Unknown}' not supported.`); + }); + + // tslint:disable-next-line: max-func-body-length + suite('POSIX getAttachItems (Linux)', () => { + setup(() => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(true); + }); + + test('Items returned by getAttachItems should be sorted alphabetically', async () => { + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + 1 launchd launchd + 41 syslogd syslogd + 146 kextd kextd +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd' + }, + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd' + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd' + } + ]; + when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + + test('Python processes should be at the top of the list returned by getAttachItems', async () => { + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + 1 launchd launchd + 41 syslogd syslogd + 96 python python + 146 kextd kextd + 31896 python python script.py +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'python', + description: '96', + detail: 'python', + id: '96', + processName: 'python', + commandLine: 'python' + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py' + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd' + }, + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd' + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd' + } + ]; + when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + }); + + // tslint:disable-next-line: max-func-body-length + suite('Windows getAttachItems', () => { + setup(() => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(false); + when(platformService.isWindows).thenReturn(true); + }); + + test('Items returned by getAttachItems should be sorted alphabetically', async () => { + const windowsOutput = `CommandLine=\r +Name=System\r +ProcessId=4\r +\r +\r +CommandLine=\r +Name=svchost.exe\r +ProcessId=5372\r +\r +\r +CommandLine=sihost.exe\r +Name=sihost.exe\r +ProcessId=5728\r +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe' + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '' + }, + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '' + } + ]; + when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ + stdout: windowsOutput + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + + test('Python processes should be at the top of the list returned by getAttachItems', async () => { + const windowsOutput = `CommandLine=\r +Name=System\r +ProcessId=4\r +\r +\r +CommandLine=\r +Name=svchost.exe\r +ProcessId=5372\r +\r +\r +CommandLine=sihost.exe\r +Name=sihost.exe\r +ProcessId=5728\r +\r +\r +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r +Name=svchost.exe\r +ProcessId=5912\r +\r +\r +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r +Name=python.exe\r +ProcessId=6028\r +\r +\r +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py\r +Name=python.exe\r +ProcessId=8026\r + `; + const expectedOutput: IAttachItem[] = [ + { + label: 'python.exe', + description: '8026', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + id: '8026', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py' + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py' + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe' + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '' + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc' + }, + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '' + } + ]; + when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ + stdout: windowsOutput + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + }); +}); diff --git a/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts new file mode 100644 index 000000000000..e7c8b629f956 --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { PsProcessParser } from '../../../../client/debugger/extension/attachQuickPick/psProcessParser'; +import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; + +// tslint:disable-next-line: max-func-body-length +suite('Attach to process - ps process parser (POSIX)', () => { + test('Processes should be parsed correctly if it is valid input', () => { + const input = `\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ + 1 launchd launchd\n\ + 41 syslogd syslogd\n\ + 42 UserEventAgent UserEventAgent (System)\n\ + 45 uninstalld uninstalld\n\ + 146 kextd kextd\n\ +31896 python python script.py\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd' + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd' + }, + { + label: 'UserEventAgent', + description: '42', + detail: 'UserEventAgent (System)', + id: '42', + processName: 'UserEventAgent', + commandLine: 'UserEventAgent (System)' + }, + { + label: 'uninstalld', + description: '45', + detail: 'uninstalld', + id: '45', + processName: 'uninstalld', + commandLine: 'uninstalld' + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd' + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py' + } + ]; + + const output = PsProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Empty lines should be skipped when parsing process list input', () => { + const input = `\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ + 1 launchd launchd\n\ + 41 syslogd syslogd\n\ + 42 UserEventAgent UserEventAgent (System)\n\ +\n\ + 146 kextd kextd\n\ + 31896 python python script.py\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd' + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd' + }, + { + label: 'UserEventAgent', + description: '42', + detail: 'UserEventAgent (System)', + id: '42', + processName: 'UserEventAgent', + commandLine: 'UserEventAgent (System)' + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd' + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py' + } + ]; + + const output = PsProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Incorrectly formatted lines should be skipped when parsing process list input', () => { + const input = `\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ + 1 launchd launchd\n\ + 41 syslogd syslogd\n\ + 42 UserEventAgent UserEventAgent (System)\n\ + 45 uninstalld uninstalld\n\ + 146 kextd kextd\n\ + 31896 python python script.py\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd' + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd' + }, + { + label: 'UserEventAgent', + description: '42', + detail: 'UserEventAgent (System)', + id: '42', + processName: 'UserEventAgent', + commandLine: 'UserEventAgent (System)' + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd' + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py' + } + ]; + + const output = PsProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); +}); diff --git a/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts new file mode 100644 index 000000000000..df24c9579e62 --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; +import { WmicProcessParser } from '../../../../client/debugger/extension/attachQuickPick/wmicProcessParser'; + +// tslint:disable-next-line: max-func-body-length +suite('Attach to process - wmic process parser (Windows)', () => { + test('Processes should be parsed correctly if it is valid input', () => { + const input = ` +CommandLine=\r\n\ +Name=System\r\n\ +ProcessId=4\r\n\ +\r\n\ +\r\n\ +CommandLine=\r\n\ +Name=svchost.exe\r\n\ +ProcessId=5372\r\n\ +\r\n\ +\r\n\ +CommandLine=sihost.exe\r\n\ +Name=sihost.exe\r\n\ +ProcessId=5728\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r\n\ +Name=svchost.exe\r\n\ +ProcessId=5912\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ +Name=python.exe\r\n\ +ProcessId=6028\r\n\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '' + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '' + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe' + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc' + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py' + } + ]; + + const output = WmicProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Incorrectly formatted lines should be skipped when parsing process list input', () => { + const input = ` +CommandLine=\r\n\ +Name=System\r\n\ +ProcessId=4\r\n\ +\r\n\ +\r\n\ +CommandLine=\r\n\ +Name=svchost.exe\r\n\ +ProcessId=5372\r\n\ +\r\n\ +\r\n\ +CommandLine=sihost.exe\r\n\ +Name=sihost.exe\r\n\ +ProcessId=5728\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r\n\ +Name=svchost.exe\r\n\ +IncorrectKey=shouldnt.be.here\r\n\ +ProcessId=5912\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ +Name=python.exe\r\n\ +ProcessId=6028\r\n\ +`; + + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '' + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '' + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe' + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc' + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py' + } + ]; + + const output = WmicProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Command lines starting with a DOS device path prefix should be parsed correctly', () => { + const input = ` +CommandLine=\r\n\ +Name=System\r\n\ +ProcessId=4\r\n\ +\r\n\ +\r\n\ +CommandLine=\\??\\C:\\WINDOWS\\system32\\conhost.exe\r\n\ +Name=conhost.exe\r\n\ +ProcessId=5912\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ +Name=python.exe\r\n\ +ProcessId=6028\r\n\ +`; + + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '' + }, + { + label: 'conhost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\conhost.exe', + id: '5912', + processName: 'conhost.exe', + commandLine: 'C:\\WINDOWS\\system32\\conhost.exe' + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py' + } + ]; + + const output = WmicProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); +}); diff --git a/src/test/debugger/extension/banner.unit.test.ts b/src/test/debugger/extension/banner.unit.test.ts new file mode 100644 index 000000000000..e8ec8824b5b0 --- /dev/null +++ b/src/test/debugger/extension/banner.unit.test.ts @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { DebugSession } from 'vscode'; +import { IApplicationShell, IDebugService } from '../../../client/common/application/types'; +import { + IBrowserService, + IDisposableRegistry, + IPersistentState, + IPersistentStateFactory, + IRandom +} from '../../../client/common/types'; +import { DebuggerTypeName } from '../../../client/debugger/constants'; +import { DebuggerBanner, PersistentStateKeys } from '../../../client/debugger/extension/banner'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Debugging - Banner', () => { + let serviceContainer: typemoq.IMock<IServiceContainer>; + let browser: typemoq.IMock<IBrowserService>; + let launchCounterState: typemoq.IMock<IPersistentState<number>>; + let launchThresholdCounterState: typemoq.IMock<IPersistentState<number | undefined>>; + let showBannerState: typemoq.IMock<IPersistentState<boolean>>; + let userSelected: boolean | undefined; + let userSelectedState: typemoq.IMock<IPersistentState<boolean | undefined>>; + let debugService: typemoq.IMock<IDebugService>; + let appShell: typemoq.IMock<IApplicationShell>; + let runtime: typemoq.IMock<IRandom>; + let banner: DebuggerBanner; + const message = 'Can you please take 2 minutes to tell us how the debugger is working for you?'; + const yes = 'Yes, take survey now'; + const no = 'No thanks'; + const later = 'Remind me later'; + + setup(() => { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + browser = typemoq.Mock.ofType<IBrowserService>(); + debugService = typemoq.Mock.ofType<IDebugService>(); + + launchCounterState = typemoq.Mock.ofType<IPersistentState<number>>(); + showBannerState = typemoq.Mock.ofType<IPersistentState<boolean>>(); + appShell = typemoq.Mock.ofType<IApplicationShell>(); + runtime = typemoq.Mock.ofType<IRandom>(); + launchThresholdCounterState = typemoq.Mock.ofType<IPersistentState<number | undefined>>(); + userSelected = true; + userSelectedState = typemoq.Mock.ofType<IPersistentState<boolean | undefined>>(); + const factory = typemoq.Mock.ofType<IPersistentStateFactory>(); + factory + .setup((f) => + f.createGlobalPersistentState( + typemoq.It.isValue(PersistentStateKeys.DebuggerLaunchCounter), + typemoq.It.isAny() + ) + ) + .returns(() => launchCounterState.object); + factory + .setup((f) => + f.createGlobalPersistentState(typemoq.It.isValue(PersistentStateKeys.ShowBanner), typemoq.It.isAny()) + ) + .returns(() => showBannerState.object); + factory + .setup((f) => + f.createGlobalPersistentState( + typemoq.It.isValue(PersistentStateKeys.DebuggerLaunchThresholdCounter), + typemoq.It.isAny() + ) + ) + .returns(() => launchThresholdCounterState.object); + factory + .setup((f) => + f.createGlobalPersistentState(typemoq.It.isValue(PersistentStateKeys.UserSelected), typemoq.It.isAny()) + ) + .returns(() => userSelectedState.object); + + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IBrowserService))).returns(() => browser.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IPersistentStateFactory))).returns(() => factory.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDebugService))).returns(() => debugService.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IRandom))).returns(() => runtime.object); + userSelectedState.setup((s) => s.value).returns(() => userSelected); + + banner = new DebuggerBanner(serviceContainer.object); + }); + test('Browser is displayed when launching service along with debugger launch counter', async () => { + const debuggerLaunchCounter = 1234; + launchCounterState + .setup((l) => l.value) + .returns(() => debuggerLaunchCounter) + .verifiable(typemoq.Times.once()); + browser + .setup((b) => b.launch(typemoq.It.isValue(`https://www.research.net/r/N7B25RV?n=${debuggerLaunchCounter}`))) + .verifiable(typemoq.Times.once()); + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(message), + typemoq.It.isValue(yes), + typemoq.It.isValue(no), + typemoq.It.isValue(later) + ) + ) + .returns(() => Promise.resolve(yes)); + + await banner.show(); + + launchCounterState.verifyAll(); + browser.verifyAll(); + }); + for (let i = 0; i < 100; i = i + 1) { + const randomSample = i; + const expected = i < 10; + test(`users are selected 10% of the time (random: ${i})`, async () => { + showBannerState.setup((s) => s.value).returns(() => true); + launchCounterState.setup((l) => l.value).returns(() => 10); + launchThresholdCounterState.setup((t) => t.value).returns(() => 10); + userSelected = undefined; + runtime + .setup((r) => r.getRandomInt(typemoq.It.isValue(0), typemoq.It.isValue(100))) + .returns(() => randomSample); + userSelectedState + .setup((u) => u.updateValue(typemoq.It.isValue(expected))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + const selected = await banner.shouldShow(); + + expect(selected).to.be.equal(expected, 'Incorrect value'); + userSelectedState.verifyAll(); + }); + } + for (const randomSample of [0, 10]) { + const expected = randomSample < 10; + test(`user selection does not change (random: ${randomSample})`, async () => { + showBannerState.setup((s) => s.value).returns(() => true); + launchCounterState.setup((l) => l.value).returns(() => 10); + launchThresholdCounterState.setup((t) => t.value).returns(() => 10); + userSelected = undefined; + runtime + .setup((r) => r.getRandomInt(typemoq.It.isValue(0), typemoq.It.isValue(100))) + .returns(() => randomSample); + userSelectedState + .setup((u) => u.updateValue(typemoq.It.isValue(expected))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + const result1 = await banner.shouldShow(); + userSelected = expected; + const result2 = await banner.shouldShow(); + + expect(result1).to.be.equal(expected, `randomSample ${randomSample}`); + expect(result2).to.be.equal(expected, `randomSample ${randomSample}`); + userSelectedState.verifyAll(); + }); + } + test('Increment Debugger Launch Counter when debug session starts', async () => { + let onDidTerminateDebugSessionCb: (e: DebugSession) => Promise<void>; + debugService + .setup((d) => d.onDidTerminateDebugSession(typemoq.It.isAny())) + .callback((cb) => (onDidTerminateDebugSessionCb = cb)) + .verifiable(typemoq.Times.once()); + + const debuggerLaunchCounter = 1234; + launchCounterState + .setup((l) => l.value) + .returns(() => debuggerLaunchCounter) + .verifiable(typemoq.Times.atLeastOnce()); + launchCounterState + .setup((l) => l.updateValue(typemoq.It.isValue(debuggerLaunchCounter + 1))) + .verifiable(typemoq.Times.once()); + showBannerState + .setup((s) => s.value) + .returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + + banner.initialize(); + await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); + + launchCounterState.verifyAll(); + browser.verifyAll(); + debugService.verifyAll(); + showBannerState.verifyAll(); + }); + test('Do not Increment Debugger Launch Counter when debug session starts and Banner is disabled', async () => { + debugService.setup((d) => d.onDidTerminateDebugSession(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + const debuggerLaunchCounter = 1234; + launchCounterState + .setup((l) => l.value) + .returns(() => debuggerLaunchCounter) + .verifiable(typemoq.Times.never()); + launchCounterState + .setup((l) => l.updateValue(typemoq.It.isValue(debuggerLaunchCounter + 1))) + .verifiable(typemoq.Times.never()); + showBannerState + .setup((s) => s.value) + .returns(() => false) + .verifiable(typemoq.Times.atLeastOnce()); + + banner.initialize(); + + launchCounterState.verifyAll(); + browser.verifyAll(); + debugService.verifyAll(); + showBannerState.verifyAll(); + }); + test('shouldShow must return false when Banner is disabled', async () => { + showBannerState + .setup((s) => s.value) + .returns(() => false) + .verifiable(typemoq.Times.once()); + + expect(await banner.shouldShow()).to.be.equal(false, 'Incorrect value'); + + showBannerState.verifyAll(); + }); + test('shouldShow must return false when Banner is enabled and debug counter is not same as threshold', async () => { + showBannerState + .setup((s) => s.value) + .returns(() => true) + .verifiable(typemoq.Times.once()); + launchCounterState + .setup((l) => l.value) + .returns(() => 1) + .verifiable(typemoq.Times.once()); + launchThresholdCounterState + .setup((t) => t.value) + .returns(() => 10) + .verifiable(typemoq.Times.atLeastOnce()); + + expect(await banner.shouldShow()).to.be.equal(false, 'Incorrect value'); + + showBannerState.verifyAll(); + launchCounterState.verifyAll(); + launchThresholdCounterState.verifyAll(); + }); + test('shouldShow must return true when Banner is enabled and debug counter is same as threshold', async () => { + showBannerState + .setup((s) => s.value) + .returns(() => true) + .verifiable(typemoq.Times.once()); + launchCounterState + .setup((l) => l.value) + .returns(() => 10) + .verifiable(typemoq.Times.once()); + launchThresholdCounterState + .setup((t) => t.value) + .returns(() => 10) + .verifiable(typemoq.Times.atLeastOnce()); + + expect(await banner.shouldShow()).to.be.equal(true, 'Incorrect value'); + + showBannerState.verifyAll(); + launchCounterState.verifyAll(); + launchThresholdCounterState.verifyAll(); + }); + test('show must be invoked when shouldShow returns true', async () => { + let onDidTerminateDebugSessionCb: (e: DebugSession) => Promise<void>; + const currentLaunchCounter = 50; + + debugService + .setup((d) => d.onDidTerminateDebugSession(typemoq.It.isAny())) + .callback((cb) => (onDidTerminateDebugSessionCb = cb)) + .verifiable(typemoq.Times.atLeastOnce()); + showBannerState + .setup((s) => s.value) + .returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + launchCounterState + .setup((l) => l.value) + .returns(() => currentLaunchCounter) + .verifiable(typemoq.Times.atLeastOnce()); + launchThresholdCounterState + .setup((t) => t.value) + .returns(() => 10) + .verifiable(typemoq.Times.atLeastOnce()); + launchCounterState + .setup((l) => l.updateValue(typemoq.It.isValue(currentLaunchCounter + 1))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.atLeastOnce()); + + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(message), + typemoq.It.isValue(yes), + typemoq.It.isValue(no), + typemoq.It.isValue(later) + ) + ) + .verifiable(typemoq.Times.once()); + banner.initialize(); + await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); + + appShell.verifyAll(); + showBannerState.verifyAll(); + launchCounterState.verifyAll(); + launchThresholdCounterState.verifyAll(); + }); + test('show must not be invoked the second time after dismissing the message', async () => { + let onDidTerminateDebugSessionCb: (e: DebugSession) => Promise<void>; + let currentLaunchCounter = 50; + + debugService + .setup((d) => d.onDidTerminateDebugSession(typemoq.It.isAny())) + .callback((cb) => (onDidTerminateDebugSessionCb = cb)) + .verifiable(typemoq.Times.atLeastOnce()); + showBannerState + .setup((s) => s.value) + .returns(() => true) + .verifiable(typemoq.Times.atLeastOnce()); + launchCounterState + .setup((l) => l.value) + .returns(() => currentLaunchCounter) + .verifiable(typemoq.Times.atLeastOnce()); + launchThresholdCounterState + .setup((t) => t.value) + .returns(() => 10) + .verifiable(typemoq.Times.atLeastOnce()); + launchCounterState + .setup((l) => l.updateValue(typemoq.It.isAny())) + .callback(() => (currentLaunchCounter = currentLaunchCounter + 1)); + + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(message), + typemoq.It.isValue(yes), + typemoq.It.isValue(no), + typemoq.It.isValue(later) + ) + ) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.once()); + banner.initialize(); + await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); + await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); + await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); + await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); + + appShell.verifyAll(); + showBannerState.verifyAll(); + launchCounterState.verifyAll(); + launchThresholdCounterState.verifyAll(); + expect(currentLaunchCounter).to.be.equal(54); + }); + test("Disabling banner must store value of 'false' in global store", async () => { + showBannerState.setup((s) => s.updateValue(typemoq.It.isValue(false))).verifiable(typemoq.Times.once()); + + await banner.disable(); + + showBannerState.verifyAll(); + }); +}); diff --git a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts new file mode 100644 index 000000000000..c945f55c4f4d --- /dev/null +++ b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect } from 'chai'; +import { instance, mock } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IMultiStepInput, IMultiStepInputFactory } from '../../../../client/common/utils/multiStepInput'; +import { PythonDebugConfigurationService } from '../../../../client/debugger/extension/configuration/debugConfigurationService'; +import { DebugConfigurationProviderFactory } from '../../../../client/debugger/extension/configuration/providers/providerFactory'; +import { IDebugConfigurationResolver } from '../../../../client/debugger/extension/configuration/types'; +import { DebugConfigurationState } from '../../../../client/debugger/extension/types'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Debugging - Configuration Service', () => { + let attachResolver: typemoq.IMock<IDebugConfigurationResolver<AttachRequestArguments>>; + let launchResolver: typemoq.IMock<IDebugConfigurationResolver<LaunchRequestArguments>>; + let configService: TestPythonDebugConfigurationService; + let multiStepFactory: typemoq.IMock<IMultiStepInputFactory>; + let providerFactory: DebugConfigurationProviderFactory; + + class TestPythonDebugConfigurationService extends PythonDebugConfigurationService { + // tslint:disable-next-line:no-unnecessary-override + public async pickDebugConfiguration( + input: IMultiStepInput<DebugConfigurationState>, + state: DebugConfigurationState + ) { + return super.pickDebugConfiguration(input, state); + } + } + setup(() => { + attachResolver = typemoq.Mock.ofType<IDebugConfigurationResolver<AttachRequestArguments>>(); + launchResolver = typemoq.Mock.ofType<IDebugConfigurationResolver<LaunchRequestArguments>>(); + multiStepFactory = typemoq.Mock.ofType<IMultiStepInputFactory>(); + providerFactory = mock(DebugConfigurationProviderFactory); + configService = new TestPythonDebugConfigurationService( + attachResolver.object, + launchResolver.object, + instance(providerFactory), + multiStepFactory.object + ); + }); + test('Should use attach resolver when passing attach config', async () => { + const config = ({ + request: 'attach' + } as any) as AttachRequestArguments; + const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; + const expectedConfig = { yay: 1 }; + + attachResolver + .setup((a) => + a.resolveDebugConfiguration(typemoq.It.isValue(folder), typemoq.It.isValue(config), typemoq.It.isAny()) + ) + .returns(() => Promise.resolve(expectedConfig as any)) + .verifiable(typemoq.Times.once()); + launchResolver + .setup((a) => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + const resolvedConfig = await configService.resolveDebugConfiguration(folder, config as any); + + expect(resolvedConfig).to.deep.equal(expectedConfig); + attachResolver.verifyAll(); + launchResolver.verifyAll(); + }); + [{ request: 'launch' }, { request: undefined }].forEach((config) => { + test(`Should use launch resolver when passing launch config with request=${config.request}`, async () => { + const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; + const expectedConfig = { yay: 1 }; + + launchResolver + .setup((a) => + a.resolveDebugConfiguration( + typemoq.It.isValue(folder), + typemoq.It.isValue((config as any) as LaunchRequestArguments), + typemoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(expectedConfig as any)) + .verifiable(typemoq.Times.once()); + attachResolver + .setup((a) => a.resolveDebugConfiguration(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + const resolvedConfig = await configService.resolveDebugConfiguration(folder, config as any); + + expect(resolvedConfig).to.deep.equal(expectedConfig); + attachResolver.verifyAll(); + launchResolver.verifyAll(); + }); + }); + test('Picker should be displayed', async () => { + // tslint:disable-next-line:no-object-literal-type-assertion + const state = ({ configs: [], folder: {}, token: undefined } as any) as DebugConfigurationState; + const multiStepInput = typemoq.Mock.ofType<IMultiStepInput<DebugConfigurationState>>(); + multiStepInput + .setup((i) => i.showQuickPick(typemoq.It.isAny())) + .returns(() => Promise.resolve(undefined as any)) + .verifiable(typemoq.Times.once()); + + await configService.pickDebugConfiguration(multiStepInput.object, state); + + multiStepInput.verifyAll(); + }); + test('Existing Configuration items must be removed before displaying picker', async () => { + // tslint:disable-next-line:no-object-literal-type-assertion + const state = ({ configs: [1, 2, 3], folder: {}, token: undefined } as any) as DebugConfigurationState; + const multiStepInput = typemoq.Mock.ofType<IMultiStepInput<DebugConfigurationState>>(); + multiStepInput + .setup((i) => i.showQuickPick(typemoq.It.isAny())) + .returns(() => Promise.resolve(undefined as any)) + .verifiable(typemoq.Times.once()); + + await configService.pickDebugConfiguration(multiStepInput.object, state); + + multiStepInput.verifyAll(); + expect(Object.keys(state.config)).to.be.lengthOf(0); + }); + test('Ensure generated config is returned', async () => { + const expectedConfig = { yes: 'Updated' }; + const multiStepInput = { + run: (_: any, state: any) => { + Object.assign(state.config, expectedConfig); + return Promise.resolve(); + } + }; + multiStepFactory + .setup((f) => f.create()) + .returns(() => multiStepInput as any) + .verifiable(typemoq.Times.once()); + configService.pickDebugConfiguration = (_, state) => { + Object.assign(state.config, expectedConfig); + return Promise.resolve(); + }; + const config = await configService.provideDebugConfigurations!({} as any); + + multiStepFactory.verifyAll(); + expect(config).to.deep.equal([expectedConfig]); + }); + test('Ensure `undefined` is returned if QuickPick is cancelled', async () => { + const multiStepInput = { + run: () => Promise.resolve() + }; + const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; + multiStepFactory + .setup((f) => f.create()) + .returns(() => multiStepInput as any) + .verifiable(typemoq.Times.once()); + const config = await configService.resolveDebugConfiguration(folder, {} as any); + + multiStepFactory.verifyAll(); + + expect(config).to.equal(undefined, `Config should be undefined`); + }); +}); diff --git a/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts new file mode 100644 index 000000000000..3108676e8c5f --- /dev/null +++ b/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { deepEqual, instance, mock, verify } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { + CancellationTokenSource, + CompletionItem, + CompletionItemKind, + Position, + SnippetString, + TextDocument, + Uri +} from 'vscode'; +import { LanguageService } from '../../../../../client/common/application/languageService'; +import { ILanguageService } from '../../../../../client/common/application/types'; +import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; +import { LaunchJsonCompletionProvider } from '../../../../../client/debugger/extension/configuration/launch.json/completionProvider'; + +// tslint:disable:no-any no-multiline-string max-func-body-length +suite('Debugging - launch.json Completion Provider', () => { + let completionProvider: LaunchJsonCompletionProvider; + let languageService: ILanguageService; + + setup(() => { + languageService = mock(LanguageService); + completionProvider = new LaunchJsonCompletionProvider(instance(languageService), []); + }); + test('Activation will register the completion provider', async () => { + await completionProvider.activate(); + verify( + languageService.registerCompletionItemProvider(deepEqual({ language: 'json' }), completionProvider) + ).once(); + verify( + languageService.registerCompletionItemProvider(deepEqual({ language: 'jsonc' }), completionProvider) + ).once(); + }); + test('Cannot provide completions for non launch.json files', () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const position = new Position(0, 0); + document.setup((doc) => doc.uri).returns(() => Uri.file(__filename)); + assert.equal(completionProvider.canProvideCompletions(document.object, position), false); + + document.reset(); + document.setup((doc) => doc.uri).returns(() => Uri.file('settings.json')); + assert.equal(completionProvider.canProvideCompletions(document.object, position), false); + }); + function testCanProvideCompletions(position: Position, offset: number, json: string, expectedValue: boolean) { + const document = typemoq.Mock.ofType<TextDocument>(); + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.uri).returns(() => Uri.file('launch.json')); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => offset); + const canProvideCompletions = completionProvider.canProvideCompletions(document.object, position); + assert.equal(canProvideCompletions, expectedValue); + } + test('Cannot provide completions when there is no configurations section in json', () => { + const position = new Position(0, 0); + const config = `{ + "version": "0.1.0" +}`; + testCanProvideCompletions(position, 1, config as any, false); + }); + test('Cannot provide completions when cursor position is not in configurations array', () => { + const position = new Position(0, 0); + const json = `{ + "version": "0.1.0", + "configurations": [] +}`; + testCanProvideCompletions(position, 10, json, false); + }); + test('Cannot provide completions when cursor position is in an empty configurations array', () => { + const position = new Position(0, 0); + const json = `{ + "version": "0.1.0", + "configurations": [ + # Cursor Position + ] +}`; + testCanProvideCompletions(position, json.indexOf('# Cursor Position'), json, true); + }); + test('No Completions for non launch.json', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + document.setup((doc) => doc.uri).returns(() => Uri.file('settings.json')); + const token = new CancellationTokenSource().token; + const position = new Position(0, 0); + + const completions = await completionProvider.provideCompletionItems(document.object, position, token); + + assert.equal(completions.length, 0); + }); + test('No Completions for files ending with launch.json', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + document.setup((doc) => doc.uri).returns(() => Uri.file('x-launch.json')); + const token = new CancellationTokenSource().token; + const position = new Position(0, 0); + + const completions = await completionProvider.provideCompletionItems(document.object, position, token); + + assert.equal(completions.length, 0); + }); + test('Get Completions', async () => { + const json = `{ + "version": "0.1.0", + "configurations": [ + # Cursor Position + ] +}`; + + const document = typemoq.Mock.ofType<TextDocument>(); + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.uri).returns(() => Uri.file('launch.json')); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('# Cursor Position')); + const position = new Position(0, 0); + const token = new CancellationTokenSource().token; + + const completions = await completionProvider.provideCompletionItems(document.object, position, token); + + assert.equal(completions.length, 1); + + const expectedCompletionItem: CompletionItem = { + command: { + command: 'python.SelectAndInsertDebugConfiguration', + title: DebugConfigStrings.launchJsonCompletions.description(), + arguments: [document.object, position, token] + }, + documentation: DebugConfigStrings.launchJsonCompletions.description(), + sortText: 'AAAA', + preselect: true, + kind: CompletionItemKind.Enum, + label: DebugConfigStrings.launchJsonCompletions.label(), + insertText: new SnippetString() + }; + + assert.deepEqual(completions[0], expectedCompletionItem); + }); +}); diff --git a/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts new file mode 100644 index 000000000000..12801cda85ba --- /dev/null +++ b/src/test/debugger/extension/configuration/launch.json/interpreterPathCommand.unit.test.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { CommandManager } from '../../../../../client/common/application/commandManager'; +import { ICommandManager } from '../../../../../client/common/application/types'; +import { ConfigurationService } from '../../../../../client/common/configuration/service'; +import { Commands } from '../../../../../client/common/constants'; +import { IConfigurationService, IDisposable } from '../../../../../client/common/types'; +import { InterpreterPathCommand } from '../../../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand'; + +suite('Interpreter Path Command', () => { + let cmdManager: ICommandManager; + let configService: IConfigurationService; + let interpreterPathCommand: InterpreterPathCommand; + setup(() => { + cmdManager = mock(CommandManager); + configService = mock(ConfigurationService); + interpreterPathCommand = new InterpreterPathCommand(instance(cmdManager), instance(configService), []); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Ensure command is registered with the correct callback handler', async () => { + let getInterpreterPathHandler!: Function; + when(cmdManager.registerCommand(Commands.GetSelectedInterpreterPath, anything())).thenCall((_, cb) => { + getInterpreterPathHandler = cb; + return TypeMoq.Mock.ofType<IDisposable>().object; + }); + + await interpreterPathCommand.activate(); + + verify(cmdManager.registerCommand(Commands.GetSelectedInterpreterPath, anything())).once(); + + const getSelectedInterpreterPath = sinon.stub(InterpreterPathCommand.prototype, '_getSelectedInterpreterPath'); + getInterpreterPathHandler([]); + assert(getSelectedInterpreterPath.calledOnceWith([])); + }); + + test('If `workspaceFolder` property exists in `args`, it is used to retrieve setting from config', async () => { + const args = { workspaceFolder: 'folderPath' }; + when(configService.getSettings(anything())).thenCall((arg) => { + assert.deepEqual(arg, Uri.parse('folderPath')); + // tslint:disable-next-line: no-any + return { pythonPath: 'settingValue' } as any; + }); + const setting = interpreterPathCommand._getSelectedInterpreterPath(args); + expect(setting).to.equal('settingValue'); + }); + + test('If `args[1]` is defined, it is used to retrieve setting from config', async () => { + const args = ['command', 'folderPath']; + when(configService.getSettings(anything())).thenCall((arg) => { + assert.deepEqual(arg, Uri.parse('folderPath')); + // tslint:disable-next-line: no-any + return { pythonPath: 'settingValue' } as any; + }); + const setting = interpreterPathCommand._getSelectedInterpreterPath(args); + expect(setting).to.equal('settingValue'); + }); + + test('If neither of these exists, value of workspace folder is `undefined`', async () => { + const args = ['command']; + // tslint:disable-next-line: no-any + when(configService.getSettings(undefined)).thenReturn({ pythonPath: 'settingValue' } as any); + const setting = interpreterPathCommand._getSelectedInterpreterPath(args); + expect(setting).to.equal('settingValue'); + }); +}); diff --git a/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts new file mode 100644 index 000000000000..6b62a3962f19 --- /dev/null +++ b/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts @@ -0,0 +1,489 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { CancellationTokenSource, DebugConfiguration, Position, Range, TextDocument, TextEditor, Uri } from 'vscode'; +import { CommandManager } from '../../../../../client/common/application/commandManager'; +import { DocumentManager } from '../../../../../client/common/application/documentManager'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../../client/common/application/workspace'; +import { PythonDebugConfigurationService } from '../../../../../client/debugger/extension/configuration/debugConfigurationService'; +import { + LaunchJsonUpdaterService, + LaunchJsonUpdaterServiceHelper +} from '../../../../../client/debugger/extension/configuration/launch.json/updaterService'; +import { IDebugConfigurationService } from '../../../../../client/debugger/extension/types'; + +type LaunchJsonSchema = { + version: string; + configurations: DebugConfiguration[]; +}; + +// tslint:disable:no-any no-multiline-string max-func-body-length +suite('Debugging - launch.json Updater Service', () => { + let helper: LaunchJsonUpdaterServiceHelper; + let commandManager: ICommandManager; + let workspace: IWorkspaceService; + let documentManager: IDocumentManager; + let debugConfigService: IDebugConfigurationService; + const sandbox = sinon.createSandbox(); + setup(() => { + commandManager = mock(CommandManager); + workspace = mock(WorkspaceService); + documentManager = mock(DocumentManager); + debugConfigService = mock(PythonDebugConfigurationService); + sandbox.stub(LaunchJsonUpdaterServiceHelper.prototype, 'isCommaImmediatelyBeforeCursor').returns(false); + helper = new LaunchJsonUpdaterServiceHelper( + instance(commandManager), + instance(workspace), + instance(documentManager), + instance(debugConfigService) + ); + }); + teardown(() => sandbox.restore()); + test('Activation will register the required commands', async () => { + const service = new LaunchJsonUpdaterService( + instance(commandManager), + [], + instance(workspace), + instance(documentManager), + instance(debugConfigService) + ); + await service.activate(); + verify( + commandManager.registerCommand( + 'python.SelectAndInsertDebugConfiguration', + helper.selectAndInsertDebugConfig, + helper + ) + ); + }); + + test('Configuration Array is detected as being empty', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const config: LaunchJsonSchema = { + version: '', + configurations: [] + }; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); + + const isEmpty = helper.isConfigurationArrayEmpty(document.object); + assert.equal(isEmpty, true); + }); + test('Configuration Array is not empty', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const config: LaunchJsonSchema = { + version: '', + configurations: [ + { + name: '', + request: 'launch', + type: 'python' + } + ] + }; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); + + const isEmpty = helper.isConfigurationArrayEmpty(document.object); + assert.equal(isEmpty, false); + }); + test('Cursor is not positioned in the configurations array', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const config: LaunchJsonSchema = { + version: '', + configurations: [ + { + name: '', + request: 'launch', + type: 'python' + } + ] + }; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => 10); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, undefined); + }); + test('Cursor is positioned in the empty configurations array', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const json = `{ + "version": "0.1.0", + "configurations": [ + # Cursor Position + ] + }`; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('#')); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, 'InsideEmptyArray'); + }); + test('Cursor is positioned before an item in the configurations array', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + } + ] +}`; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('{') - 1); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, 'BeforeItem'); + }); + test('Cursor is positioned before an item in the middle of the configurations array', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + },{ + "name":"wow" + } + ] +}`; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf(',{') + 1); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, 'BeforeItem'); + }); + test('Cursor is positioned after an item in the configurations array', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + }] +}`; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('}]') + 1); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, 'AfterItem'); + }); + test('Cursor is positioned after an item in the middle of the configurations array', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + },{ + "name":"wow" + } + ] +}`; + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); + + const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); + assert.equal(cursorPosition, 'AfterItem'); + }); + test('Text to be inserted must be prefixed with a comma', async () => { + const config = {} as any; + const expectedText = `,${JSON.stringify(config)}`; + + const textToInsert = helper.getTextForInsertion(config, 'AfterItem'); + + assert.equal(textToInsert, expectedText); + }); + test('Text to be inserted must not be prefixed with a comma (as a comma already exists)', async () => { + const config = {} as any; + const expectedText = JSON.stringify(config); + + const textToInsert = helper.getTextForInsertion(config, 'AfterItem', 'BeforeCursor'); + + assert.equal(textToInsert, expectedText); + }); + test('Text to be inserted must be suffixed with a comma', async () => { + const config = {} as any; + const expectedText = `${JSON.stringify(config)},`; + + const textToInsert = helper.getTextForInsertion(config, 'BeforeItem'); + + assert.equal(textToInsert, expectedText); + }); + test('Text to be inserted must not be prefixed nor suffixed with commas', async () => { + const config = {} as any; + const expectedText = JSON.stringify(config); + + const textToInsert = helper.getTextForInsertion(config, 'InsideEmptyArray'); + + assert.equal(textToInsert, expectedText); + }); + test('When inserting the debug config into the json file format the document', async () => { + const json = `{ + "version": "0.1.0", + "configurations": [ + { + "name":"wow" + },{ + "name":"wow" + } + ] +}`; + const config = {} as any; + const document = typemoq.Mock.ofType<TextDocument>(); + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => json); + document.setup((doc) => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); + when(documentManager.applyEdit(anything())).thenResolve(); + when(commandManager.executeCommand('editor.action.formatDocument')).thenResolve(); + + await helper.insertDebugConfiguration(document.object, new Position(0, 0), config); + + verify(documentManager.applyEdit(anything())).once(); + verify(commandManager.executeCommand('editor.action.formatDocument')).once(); + }); + test('No changes to configuration if there is not active document', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const position = new Position(0, 0); + const token = new CancellationTokenSource().token; + when(documentManager.activeTextEditor).thenReturn(); + let debugConfigInserted = false; + helper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspace.getWorkspaceFolder(anything())).never(); + assert.equal(debugConfigInserted, false); + }); + test('No changes to configuration if the active document is not same as the document passed in', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const position = new Position(0, 0); + const token = new CancellationTokenSource().token; + const textEditor = typemoq.Mock.ofType<TextEditor>(); + textEditor + .setup((t) => t.document) + .returns(() => 'x' as any) + .verifiable(typemoq.Times.atLeastOnce()); + when(documentManager.activeTextEditor).thenReturn(textEditor.object); + let debugConfigInserted = false; + helper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + verify(documentManager.activeTextEditor).atLeast(1); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspace.getWorkspaceFolder(anything())).never(); + textEditor.verifyAll(); + assert.equal(debugConfigInserted, false); + }); + test('No changes to configuration if cancellation token has been cancelled', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const position = new Position(0, 0); + const tokenSource = new CancellationTokenSource(); + tokenSource.cancel(); + const token = tokenSource.token; + const textEditor = typemoq.Mock.ofType<TextEditor>(); + const docUri = Uri.file(__filename); + const folderUri = Uri.file('Folder Uri'); + const folder = { name: '', index: 0, uri: folderUri }; + document + .setup((doc) => doc.uri) + .returns(() => docUri) + .verifiable(typemoq.Times.atLeastOnce()); + textEditor + .setup((t) => t.document) + .returns(() => document.object) + .verifiable(typemoq.Times.atLeastOnce()); + when(documentManager.activeTextEditor).thenReturn(textEditor.object); + when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); + when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve([''] as any); + let debugConfigInserted = false; + helper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + verify(documentManager.activeTextEditor).atLeast(1); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); + textEditor.verifyAll(); + document.verifyAll(); + assert.equal(debugConfigInserted, false); + }); + test('No changes to configuration if no configuration items are returned', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const position = new Position(0, 0); + const tokenSource = new CancellationTokenSource(); + const token = tokenSource.token; + const textEditor = typemoq.Mock.ofType<TextEditor>(); + const docUri = Uri.file(__filename); + const folderUri = Uri.file('Folder Uri'); + const folder = { name: '', index: 0, uri: folderUri }; + document + .setup((doc) => doc.uri) + .returns(() => docUri) + .verifiable(typemoq.Times.atLeastOnce()); + textEditor + .setup((t) => t.document) + .returns(() => document.object) + .verifiable(typemoq.Times.atLeastOnce()); + when(documentManager.activeTextEditor).thenReturn(textEditor.object); + when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); + when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve([] as any); + let debugConfigInserted = false; + helper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + verify(documentManager.activeTextEditor).atLeast(1); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); + textEditor.verifyAll(); + document.verifyAll(); + assert.equal(debugConfigInserted, false); + }); + test('Changes are made to the configuration', async () => { + const document = typemoq.Mock.ofType<TextDocument>(); + const position = new Position(0, 0); + const tokenSource = new CancellationTokenSource(); + const token = tokenSource.token; + const textEditor = typemoq.Mock.ofType<TextEditor>(); + const docUri = Uri.file(__filename); + const folderUri = Uri.file('Folder Uri'); + const folder = { name: '', index: 0, uri: folderUri }; + document + .setup((doc) => doc.uri) + .returns(() => docUri) + .verifiable(typemoq.Times.atLeastOnce()); + textEditor + .setup((t) => t.document) + .returns(() => document.object) + .verifiable(typemoq.Times.atLeastOnce()); + when(documentManager.activeTextEditor).thenReturn(textEditor.object); + when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); + when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(['config'] as any); + let debugConfigInserted = false; + helper.insertDebugConfiguration = async () => { + debugConfigInserted = true; + }; + + await helper.selectAndInsertDebugConfig(document.object, position, token); + + verify(documentManager.activeTextEditor).atLeast(1); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); + textEditor.verifyAll(); + document.verifyAll(); + assert.equal(debugConfigInserted, true); + }); + test('If cursor is at the begining of line 1 then there is no comma before cursor', async () => { + sandbox.restore(); + const document = typemoq.Mock.ofType<TextDocument>(); + const position = new Position(1, 0); + document + .setup((doc) => doc.lineAt(1)) + .returns(() => ({ range: new Range(1, 0, 1, 1) } as any)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.getText(typemoq.It.isAny())) + .returns(() => '') + .verifiable(typemoq.Times.atLeastOnce()); + + const isBeforeCursor = helper.isCommaImmediatelyBeforeCursor(document.object, position); + + assert.ok(!isBeforeCursor); + document.verifyAll(); + }); + test('If cursor is positioned after some text (not a comma) then detect this', async () => { + sandbox.restore(); + const document = typemoq.Mock.ofType<TextDocument>(); + const position = new Position(2, 2); + document + .setup((doc) => doc.lineAt(2)) + .returns(() => ({ range: new Range(2, 0, 1, 5) } as any)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.getText(typemoq.It.isAny())) + .returns(() => 'Hello') + .verifiable(typemoq.Times.atLeastOnce()); + + const isBeforeCursor = helper.isCommaImmediatelyBeforeCursor(document.object, position); + + assert.ok(!isBeforeCursor); + document.verifyAll(); + }); + test('If cursor is positioned after a comma then detect this', async () => { + sandbox.restore(); + const document = typemoq.Mock.ofType<TextDocument>(); + const position = new Position(2, 2); + document + .setup((doc) => doc.lineAt(2)) + .returns(() => ({ range: new Range(2, 0, 2, 3) } as any)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.getText(typemoq.It.isAny())) + .returns(() => '}, ') + .verifiable(typemoq.Times.atLeastOnce()); + + const isBeforeCursor = helper.isCommaImmediatelyBeforeCursor(document.object, position); + + assert.ok(isBeforeCursor); + document.verifyAll(); + }); + test('If cursor is positioned in an empty line and previous line ends with comma, then detect this', async () => { + sandbox.restore(); + const document = typemoq.Mock.ofType<TextDocument>(); + const position = new Position(2, 2); + document + .setup((doc) => doc.lineAt(1)) + .returns(() => ({ range: new Range(1, 0, 1, 3), text: '}, ' } as any)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.lineAt(2)) + .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as any)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.getText(typemoq.It.isAny())) + .returns(() => ' ') + .verifiable(typemoq.Times.atLeastOnce()); + + const isBeforeCursor = helper.isCommaImmediatelyBeforeCursor(document.object, position); + + assert.ok(isBeforeCursor); + document.verifyAll(); + }); + test('If cursor is positioned in an empty line and previous line does not end with comma, then detect this', async () => { + sandbox.restore(); + const document = typemoq.Mock.ofType<TextDocument>(); + const position = new Position(2, 2); + document + .setup((doc) => doc.lineAt(1)) + .returns(() => ({ range: new Range(1, 0, 1, 3), text: '} ' } as any)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.lineAt(2)) + .returns(() => ({ range: new Range(2, 0, 2, 3), text: ' ' } as any)) + .verifiable(typemoq.Times.atLeastOnce()); + document + .setup((doc) => doc.getText(typemoq.It.isAny())) + .returns(() => ' ') + .verifiable(typemoq.Times.atLeastOnce()); + + const isBeforeCursor = helper.isCommaImmediatelyBeforeCursor(document.object, position); + + assert.ok(!isBeforeCursor); + document.verifyAll(); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts new file mode 100644 index 000000000000..590b3d4c63d3 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../../client/common/application/workspace'; +import { FileSystem } from '../../../../../client/common/platform/fileSystem'; +import { PathUtils } from '../../../../../client/common/platform/pathUtils'; +import { IFileSystem } from '../../../../../client/common/platform/types'; +import { IPathUtils } from '../../../../../client/common/types'; +import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { DjangoLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/djangoLaunch'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; + +suite('Debugging - Configuration Provider Django', () => { + let fs: IFileSystem; + let workspaceService: IWorkspaceService; + let pathUtils: IPathUtils; + let provider: TestDjangoLaunchDebugConfigurationProvider; + let input: MultiStepInput<DebugConfigurationState>; + class TestDjangoLaunchDebugConfigurationProvider extends DjangoLaunchDebugConfigurationProvider { + // tslint:disable-next-line:no-unnecessary-override + public resolveVariables(pythonPath: string, resource: Uri | undefined): string { + return super.resolveVariables(pythonPath, resource); + } + // tslint:disable-next-line:no-unnecessary-override + public async getManagePyPath(folder: WorkspaceFolder): Promise<string | undefined> { + return super.getManagePyPath(folder); + } + } + setup(() => { + fs = mock(FileSystem); + workspaceService = mock(WorkspaceService); + pathUtils = mock(PathUtils); + input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); + provider = new TestDjangoLaunchDebugConfigurationProvider( + instance(fs), + instance(workspaceService), + instance(pathUtils) + ); + }); + test("getManagePyPath should return undefined if file doesn't exist", async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); + when(fs.fileExists(managePyPath)).thenResolve(false); + + const file = await provider.getManagePyPath(folder); + + expect(file).to.be.equal(undefined, 'Should return undefined'); + }); + test('getManagePyPath should file path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); + + when(pathUtils.separator).thenReturn('-'); + when(fs.fileExists(managePyPath)).thenResolve(true); + + const file = await provider.getManagePyPath(folder); + + // tslint:disable-next-line:no-invalid-template-strings + expect(file).to.be.equal('${workspaceFolder}-manage.py'); + }); + test('Resolve variables (with resource)', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(folder); + + const resolvedPath = provider.resolveVariables('${workspaceFolder}/one.py', Uri.file('')); + + expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); + }); + test('Validation of path should return errors if path is undefined', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + + const error = await provider.validateManagePy(folder, ''); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if path is empty', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + + const error = await provider.validateManagePy(folder, '', ''); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is empty', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => ''; + + const error = await provider.validateManagePy(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test("Validation of path should return errors if resolved path doesn't exist", async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz'; + + when(fs.fileExists('xyz')).thenResolve(false); + const error = await provider.validateManagePy(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is non-python', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz.txt'; + + when(fs.fileExists('xyz.txt')).thenResolve(true); + const error = await provider.validateManagePy(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is python', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz.py'; + + when(fs.fileExists('xyz.py')).thenResolve(true); + const error = await provider.validateManagePy(folder, '', 'x'); + + expect(error).to.be.equal(undefined, 'should not have errors'); + }); + test('Launch JSON with valid python path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getManagePyPath = () => Promise.resolve('xyz.py'); + when(pathUtils.separator).thenReturn('-'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: DebugConfigStrings.django.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + program: 'xyz.py', + args: ['runserver'], + django: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with selected managepy path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getManagePyPath = () => Promise.resolve(undefined); + when(pathUtils.separator).thenReturn('-'); + when(input.showInputBox(anything())).thenResolve('hello'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: DebugConfigStrings.django.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + program: 'hello', + args: ['runserver'], + django: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with default managepy path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getManagePyPath = () => Promise.resolve(undefined); + const workspaceFolderToken = '${workspaceFolder}'; + const defaultProgram = `${workspaceFolderToken}-manage.py`; + + when(pathUtils.separator).thenReturn('-'); + when(input.showInputBox(anything())).thenResolve(); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: DebugConfigStrings.django.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + program: defaultProgram, + args: ['runserver'], + django: true + }; + + expect(state.config).to.be.deep.equal(config); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts new file mode 100644 index 000000000000..cb06defdeb9c --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { FileLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/fileLaunch'; + +suite('Debugging - Configuration Provider File', () => { + let provider: FileLaunchDebugConfigurationProvider; + setup(() => { + provider = new FileLaunchDebugConfigurationProvider(); + }); + test('Launch JSON with default managepy path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + + await provider.buildConfiguration(undefined as any, state); + + const config = { + name: DebugConfigStrings.file.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + // tslint:disable-next-line:no-invalid-template-strings + program: '${file}', + console: 'integratedTerminal' + }; + + expect(state.config).to.be.deep.equal(config); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts new file mode 100644 index 000000000000..78a9ea4ef307 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { FileSystem } from '../../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../../client/common/platform/types'; +import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { FlaskLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/flaskLaunch'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; + +suite('Debugging - Configuration Provider Flask', () => { + let fs: IFileSystem; + let provider: TestFlaskLaunchDebugConfigurationProvider; + let input: MultiStepInput<DebugConfigurationState>; + class TestFlaskLaunchDebugConfigurationProvider extends FlaskLaunchDebugConfigurationProvider { + // tslint:disable-next-line:no-unnecessary-override + public async getApplicationPath(folder: WorkspaceFolder): Promise<string | undefined> { + return super.getApplicationPath(folder); + } + } + setup(() => { + fs = mock(FileSystem); + input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); + provider = new TestFlaskLaunchDebugConfigurationProvider(instance(fs)); + }); + test("getApplicationPath should return undefined if file doesn't exist", async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const appPyPath = path.join(folder.uri.fsPath, 'app.py'); + when(fs.fileExists(appPyPath)).thenResolve(false); + + const file = await provider.getApplicationPath(folder); + + expect(file).to.be.equal(undefined, 'Should return undefined'); + }); + test('getApplicationPath should file path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const appPyPath = path.join(folder.uri.fsPath, 'app.py'); + + when(fs.fileExists(appPyPath)).thenResolve(true); + + const file = await provider.getApplicationPath(folder); + + // tslint:disable-next-line:no-invalid-template-strings + expect(file).to.be.equal('app.py'); + }); + test('Launch JSON with valid python path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getApplicationPath = () => Promise.resolve('xyz.py'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: DebugConfigStrings.flask.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + module: 'flask', + env: { + FLASK_APP: 'xyz.py', + FLASK_ENV: 'development', + FLASK_DEBUG: '0' + }, + args: ['run', '--no-debugger'], + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with selected app path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getApplicationPath = () => Promise.resolve(undefined); + + when(input.showInputBox(anything())).thenResolve('hello'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: DebugConfigStrings.flask.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + module: 'flask', + env: { + FLASK_APP: 'hello', + FLASK_ENV: 'development', + FLASK_DEBUG: '0' + }, + args: ['run', '--no-debugger'], + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with default managepy path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getApplicationPath = () => Promise.resolve(undefined); + + when(input.showInputBox(anything())).thenResolve(); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: DebugConfigStrings.flask.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + module: 'flask', + env: { + FLASK_APP: 'app.py', + FLASK_ENV: 'development', + FLASK_DEBUG: '0' + }, + args: ['run', '--no-debugger'], + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts new file mode 100644 index 000000000000..b06f26311d23 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { ModuleLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/moduleLaunch'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; + +suite('Debugging - Configuration Provider Module', () => { + let provider: ModuleLaunchDebugConfigurationProvider; + setup(() => { + provider = new ModuleLaunchDebugConfigurationProvider(); + }); + test('Launch JSON with default module name', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + const input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); + + when(input.showInputBox(anything())).thenResolve(); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: DebugConfigStrings.module.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + module: DebugConfigStrings.module.snippet.default() + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with selected module name', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + const input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); + + when(input.showInputBox(anything())).thenResolve('hello'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: DebugConfigStrings.module.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + module: 'hello' + }; + + expect(state.config).to.be.deep.equal(config); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts b/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts new file mode 100644 index 000000000000..0e25adab85d2 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/pidAttach.unit.test.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { PidAttachDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/pidAttach'; + +suite('Debugging - Configuration Provider File', () => { + let provider: PidAttachDebugConfigurationProvider; + setup(() => { + provider = new PidAttachDebugConfigurationProvider(); + }); + test('Launch JSON with default process id', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + + await provider.buildConfiguration(undefined as any, state); + + const config = { + name: DebugConfigStrings.attachPid.snippet.name(), + type: DebuggerTypeName, + request: 'attach', + // tslint:disable-next-line:no-invalid-template-strings + processId: '${command:pickProcess}' + }; + + expect(state.config).to.be.deep.equal(config); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts b/src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts new file mode 100644 index 000000000000..668439d09940 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/providerFactory.unit.test.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect } from 'chai'; +import { getNamesAndValues } from '../../../../../client/common/utils/enum'; +import { DebugConfigurationProviderFactory } from '../../../../../client/debugger/extension/configuration/providers/providerFactory'; +import { IDebugConfigurationProviderFactory } from '../../../../../client/debugger/extension/configuration/types'; +import { DebugConfigurationType, IDebugConfigurationProvider } from '../../../../../client/debugger/extension/types'; + +suite('Debugging - Configuration Provider Factory', () => { + let mappedProviders: Map<DebugConfigurationType, IDebugConfigurationProvider>; + let factory: IDebugConfigurationProviderFactory; + setup(() => { + mappedProviders = new Map<DebugConfigurationType, IDebugConfigurationProvider>(); + getNamesAndValues<DebugConfigurationType>(DebugConfigurationType).forEach((item) => { + mappedProviders.set(item.value, (item.value as any) as IDebugConfigurationProvider); + }); + factory = new DebugConfigurationProviderFactory( + mappedProviders.get(DebugConfigurationType.launchFlask)!, + mappedProviders.get(DebugConfigurationType.launchDjango)!, + mappedProviders.get(DebugConfigurationType.launchModule)!, + mappedProviders.get(DebugConfigurationType.launchFile)!, + mappedProviders.get(DebugConfigurationType.launchPyramid)!, + mappedProviders.get(DebugConfigurationType.remoteAttach)!, + mappedProviders.get(DebugConfigurationType.pidAttach)! + ); + }); + getNamesAndValues<DebugConfigurationType>(DebugConfigurationType).forEach((item) => { + test(`Configuration Provider for ${item.name}`, () => { + const provider = factory.create(item.value); + expect(provider).to.equal(mappedProviders.get(item.value)); + }); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts new file mode 100644 index 000000000000..e0c5d656d401 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../../client/common/application/workspace'; +import { FileSystem } from '../../../../../client/common/platform/fileSystem'; +import { PathUtils } from '../../../../../client/common/platform/pathUtils'; +import { IFileSystem } from '../../../../../client/common/platform/types'; +import { IPathUtils } from '../../../../../client/common/types'; +import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { PyramidLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/pyramidLaunch'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; + +suite('Debugging - Configuration Provider Pyramid', () => { + let fs: IFileSystem; + let workspaceService: IWorkspaceService; + let pathUtils: IPathUtils; + let provider: TestPyramidLaunchDebugConfigurationProvider; + let input: MultiStepInput<DebugConfigurationState>; + class TestPyramidLaunchDebugConfigurationProvider extends PyramidLaunchDebugConfigurationProvider { + // tslint:disable-next-line:no-unnecessary-override + public resolveVariables(pythonPath: string, resource: Uri | undefined): string { + return super.resolveVariables(pythonPath, resource); + } + // tslint:disable-next-line:no-unnecessary-override + public async getDevelopmentIniPath(folder: WorkspaceFolder): Promise<string | undefined> { + return super.getDevelopmentIniPath(folder); + } + } + setup(() => { + fs = mock(FileSystem); + workspaceService = mock(WorkspaceService); + pathUtils = mock(PathUtils); + input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); + provider = new TestPyramidLaunchDebugConfigurationProvider( + instance(fs), + instance(workspaceService), + instance(pathUtils) + ); + }); + test("getDevelopmentIniPath should return undefined if file doesn't exist", async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); + when(fs.fileExists(managePyPath)).thenResolve(false); + + const file = await provider.getDevelopmentIniPath(folder); + + expect(file).to.be.equal(undefined, 'Should return undefined'); + }); + test('getDevelopmentIniPath should file path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); + + when(pathUtils.separator).thenReturn('-'); + when(fs.fileExists(managePyPath)).thenResolve(true); + + const file = await provider.getDevelopmentIniPath(folder); + + // tslint:disable-next-line:no-invalid-template-strings + expect(file).to.be.equal('${workspaceFolder}-development.ini'); + }); + test('Resolve variables (with resource)', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(folder); + + const resolvedPath = provider.resolveVariables('${workspaceFolder}/one.py', Uri.file('')); + + expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); + }); + test('Validation of path should return errors if path is undefined', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + + const error = await provider.validateIniPath(folder, ''); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if path is empty', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + + const error = await provider.validateIniPath(folder, '', ''); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is empty', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => ''; + + const error = await provider.validateIniPath(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test("Validation of path should return errors if resolved path doesn't exist", async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz'; + + when(fs.fileExists('xyz')).thenResolve(false); + const error = await provider.validateIniPath(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is non-ini', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz.txt'; + + when(fs.fileExists('xyz.txt')).thenResolve(true); + const error = await provider.validateIniPath(folder, '', 'x'); + + expect(error).to.be.length.greaterThan(1); + }); + test('Validation of path should return errors if resolved path is ini', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + provider.resolveVariables = () => 'xyz.ini'; + + when(fs.fileExists('xyz.ini')).thenResolve(true); + const error = await provider.validateIniPath(folder, '', 'x'); + + expect(error).to.be.equal(undefined, 'should not have errors'); + }); + test('Launch JSON with valid ini path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getDevelopmentIniPath = () => Promise.resolve('xyz.ini'); + when(pathUtils.separator).thenReturn('-'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: DebugConfigStrings.pyramid.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + module: 'pyramid.scripts.pserve', + args: ['xyz.ini'], + pyramid: true, + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with selected ini path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getDevelopmentIniPath = () => Promise.resolve(undefined); + when(pathUtils.separator).thenReturn('-'); + when(input.showInputBox(anything())).thenResolve('hello'); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: DebugConfigStrings.pyramid.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + module: 'pyramid.scripts.pserve', + args: ['hello'], + pyramid: true, + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); + test('Launch JSON with default ini path', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + provider.getDevelopmentIniPath = () => Promise.resolve(undefined); + const workspaceFolderToken = '${workspaceFolder}'; + const defaultIni = `${workspaceFolderToken}-development.ini`; + + when(pathUtils.separator).thenReturn('-'); + when(input.showInputBox(anything())).thenResolve(); + + await provider.buildConfiguration(instance(input), state); + + const config = { + name: DebugConfigStrings.pyramid.snippet.name(), + type: DebuggerTypeName, + request: 'launch', + module: 'pyramid.scripts.pserve', + args: [defaultIni], + pyramid: true, + jinja: true + }; + + expect(state.config).to.be.deep.equal(config); + }); +}); diff --git a/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts b/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts new file mode 100644 index 000000000000..0f7e8aedf807 --- /dev/null +++ b/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-template-strings max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; +import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { RemoteAttachDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/remoteAttach'; +import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; +import { AttachRequestArguments } from '../../../../../client/debugger/types'; + +suite('Debugging - Configuration Provider Remote Attach', () => { + let provider: TestRemoteAttachDebugConfigurationProvider; + let input: MultiStepInput<DebugConfigurationState>; + class TestRemoteAttachDebugConfigurationProvider extends RemoteAttachDebugConfigurationProvider { + // tslint:disable-next-line:no-unnecessary-override + public async configurePort( + i: MultiStepInput<DebugConfigurationState>, + config: Partial<AttachRequestArguments> + ) { + return super.configurePort(i, config); + } + } + setup(() => { + input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); + provider = new TestRemoteAttachDebugConfigurationProvider(); + }); + test('Configure port will display prompt', async () => { + when(input.showInputBox(anything())).thenResolve(); + + await provider.configurePort(instance(input), {}); + + verify(input.showInputBox(anything())).once(); + }); + test('Configure port will default to 5678 if entered value is not a number', async () => { + const config: { connect?: { port?: number } } = {}; + when(input.showInputBox(anything())).thenResolve('xyz'); + + await provider.configurePort(instance(input), config); + + verify(input.showInputBox(anything())).once(); + expect(config).to.be.deep.equal({ connect: { port: 5678 } }); + }); + test('Configure port will default to 5678', async () => { + const config: { connect?: { port?: number } } = {}; + when(input.showInputBox(anything())).thenResolve(); + + await provider.configurePort(instance(input), config); + + verify(input.showInputBox(anything())).once(); + expect(config).to.be.deep.equal({ connect: { port: 5678 } }); + }); + test('Configure port will use user selected value', async () => { + const config: { connect?: { port?: number } } = {}; + when(input.showInputBox(anything())).thenResolve('1234'); + + await provider.configurePort(instance(input), config); + + verify(input.showInputBox(anything())).once(); + expect(config).to.be.deep.equal({ connect: { port: 1234 } }); + }); + test('Launch JSON with default host name', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + let portConfigured = false; + when(input.showInputBox(anything())).thenResolve(); + provider.configurePort = () => { + portConfigured = true; + return Promise.resolve(); + }; + + const configurePort = await provider.buildConfiguration(instance(input), state); + if (configurePort) { + await configurePort!(input, state); + } + + const config = { + name: DebugConfigStrings.attach.snippet.name(), + type: DebuggerTypeName, + request: 'attach', + connect: { + host: 'localhost', + port: 5678 + }, + pathMappings: [ + { + localRoot: '${workspaceFolder}', + remoteRoot: '.' + } + ] + }; + + expect(state.config).to.be.deep.equal(config); + expect(portConfigured).to.be.equal(true, 'Port not configured'); + }); + test('Launch JSON with user defined host name', async () => { + const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; + const state = { config: {}, folder }; + let portConfigured = false; + when(input.showInputBox(anything())).thenResolve('Hello'); + provider.configurePort = (_, cfg) => { + portConfigured = true; + cfg.connect!.port = 9999; + return Promise.resolve(); + }; + + const configurePort = await provider.buildConfiguration(instance(input), state); + if (configurePort) { + await configurePort(input, state); + } + + const config = { + name: DebugConfigStrings.attach.snippet.name(), + type: DebuggerTypeName, + request: 'attach', + connect: { + host: 'Hello', + port: 9999 + }, + pathMappings: [ + { + localRoot: '${workspaceFolder}', + remoteRoot: '.' + } + ] + }; + + expect(state.config).to.be.deep.equal(config); + expect(portConfigured).to.be.equal(true, 'Port not configured'); + }); +}); diff --git a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts new file mode 100644 index 000000000000..474e8bf55601 --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts @@ -0,0 +1,514 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-invalid-template-strings no-any no-object-literal-type-assertion no-invalid-this + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; +import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import { IFileSystem, IPlatformService } from '../../../../../client/common/platform/types'; +import { IConfigurationService } from '../../../../../client/common/types'; +import { OSType } from '../../../../../client/common/utils/platform'; +import { AttachConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/attach'; +import { AttachRequestArguments, DebugOptions } from '../../../../../client/debugger/types'; +import { IServiceContainer } from '../../../../../client/ioc/types'; +import { getOSType } from '../../../../common'; +import { getInfoPerOS, setUpOSMocks } from './common'; + +getInfoPerOS().forEach(([osName, osType, path]) => { + if (osType === OSType.Unknown) { + return; + } + + function getAvailableOptions(): string[] { + const options = [DebugOptions.RedirectOutput]; + if (osType === OSType.Windows) { + options.push(DebugOptions.FixFilePathCase); + options.push(DebugOptions.WindowsClient); + } else { + options.push(DebugOptions.UnixClient); + } + options.push(DebugOptions.ShowReturnValue); + return options; + } + + suite(`Debugging - Config Resolver attach, OS = ${osName}`, () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let debugProvider: DebugConfigurationProvider; + let platformService: TypeMoq.IMock<IPlatformService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let documentManager: TypeMoq.IMock<IDocumentManager>; + let configurationService: TypeMoq.IMock<IConfigurationService>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + const debugOptionsAvailable = getAvailableOptions(); + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + setUpOSMocks(osType, platformService); + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + debugProvider = new AttachConfigurationResolver( + workspaceService.object, + documentManager.object, + platformService.object, + configurationService.object + ); + }); + function createMoqWorkspaceFolder(folderPath: string) { + const folder = TypeMoq.Mock.ofType<WorkspaceFolder>(); + folder.setup((f) => f.uri).returns(() => Uri.file(folderPath)); + return folder.object; + } + function setupActiveEditor(fileName: string | undefined, languageId: string) { + if (fileName) { + const textEditor = TypeMoq.Mock.ofType<TextEditor>(); + const document = TypeMoq.Mock.ofType<TextDocument>(); + document.setup((d) => d.languageId).returns(() => languageId); + document.setup((d) => d.fileName).returns(() => fileName); + textEditor.setup((t) => t.document).returns(() => document.object); + documentManager.setup((d) => d.activeTextEditor).returns(() => textEditor.object); + } else { + documentManager.setup((d) => d.activeTextEditor).returns(() => undefined); + } + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) + .returns(() => documentManager.object); + } + function setupWorkspaces(folders: string[]) { + const workspaceFolders = folders.map(createMoqWorkspaceFolder); + workspaceService.setup((w) => w.workspaceFolders).returns(() => workspaceFolders); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + } + test('Defaults should be returned when an empty object is passed with a Workspace Folder and active file', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + request: 'attach' + } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { + request: 'attach' + } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.have.property('host', 'localhost'); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { + setupActiveEditor(undefined, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { + request: 'attach' + } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.have.property('host', 'localhost'); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { + const activeFile = 'xyz.js'; + + setupActiveEditor(activeFile, 'javascript'); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { + request: 'attach' + } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.not.have.property('localRoot'); + expect(debugConfig).to.have.property('host', 'localhost'); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { + const activeFile = 'xyz.py'; + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { + request: 'attach' + } as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); + expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); + expect(debugConfig).to.have.property('host', 'localhost'); + }); + test('Default host should not be added if connect is available.', async () => { + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { + request: 'attach', + connect: { host: 'localhost', port: 5678 } + } as AttachRequestArguments); + + expect(debugConfig).to.not.have.property('host', 'localhost'); + }); + test('Default host should not be added if listen is available.', async () => { + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { + request: 'attach', + listen: { host: 'localhost', port: 5678 } + } as AttachRequestArguments); + + expect(debugConfig).to.not.have.property('host', 'localhost'); + }); + test("Ensure 'localRoot' is left unaltered", async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + request: 'attach' + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + }); + ['localhost', 'LOCALHOST', '127.0.0.1', '::1'].forEach((host) => { + test(`Ensure path mappings are automatically added when host is '${host}'`, async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + host, + request: 'attach' + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + expect(pathMappings).to.be.lengthOf(1); + expect(pathMappings![0].localRoot).to.be.equal(workspaceFolder.uri.fsPath); + expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); + }); + test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}'`, async function () { + if (getOSType() !== OSType.Windows || osType !== OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + host, + request: 'attach' + } as any) as DebugConfiguration); + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + + const expected = Uri.file(path.join('c:', 'Debug', 'Python_Path')).fsPath; + expect(pathMappings![0].localRoot).to.be.equal(expected); + expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); + }); + test(`Ensure drive letter is not lower cased for local path mappings on non-Windows when host is '${host}'`, async function () { + if (getOSType() === OSType.Windows || osType === OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + host, + request: 'attach' + } as any) as DebugConfiguration); + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + + const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path')).fsPath; + expect(pathMappings![0].localRoot).to.be.equal(expected); + expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); + }); + test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}' and with existing path mappings`, async function () { + if (getOSType() !== OSType.Windows || osType !== OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugPathMappings = [ + { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' } + ]; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + pathMappings: debugPathMappings, + host, + request: 'attach' + } as any) as DebugConfiguration); + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + + const expected = Uri.file(path.join('c:', 'Debug', 'Python_Path', localRoot)).fsPath; + expect(pathMappings![0].localRoot).to.be.equal(expected); + expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); + }); + test(`Ensure drive letter is not lower cased for local path mappings on non-Windows when host is '${host}' and with existing path mappings`, async function () { + if (getOSType() === OSType.Windows || osType === OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugPathMappings = [ + { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' } + ]; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + pathMappings: debugPathMappings, + host, + request: 'attach' + } as any) as DebugConfiguration); + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + + const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path', localRoot)).fsPath; + expect(pathMappings![0].localRoot).to.be.equal(expected); + expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); + }); + test(`Ensure local path mappings are not modified when not pointing to a local drive when host is '${host}'`, async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('Server', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + host, + request: 'attach' + } as any) as DebugConfiguration); + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + + expect(pathMappings![0].localRoot).to.be.equal(workspaceFolder.uri.fsPath); + expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); + }); + }); + ['192.168.1.123', 'don.debugger.com'].forEach((host) => { + test(`Ensure path mappings are not automatically added when host is '${host}'`, async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + host, + request: 'attach' + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + expect(pathMappings || []).to.be.lengthOf(0); + }); + }); + test("Ensure 'localRoot' and 'remoteRoot' is used", async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; + const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + remoteRoot, + request: 'attach' + } as any) as DebugConfiguration); + + expect(debugConfig!.pathMappings).to.be.lengthOf(1); + expect(debugConfig!.pathMappings).to.deep.include({ localRoot, remoteRoot }); + }); + test("Ensure 'localRoot' and 'remoteRoot' is used", async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; + const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + remoteRoot, + request: 'attach' + } as any) as DebugConfiguration); + + expect(debugConfig!.pathMappings).to.be.lengthOf(1); + expect(debugConfig!.pathMappings).to.deep.include({ localRoot, remoteRoot }); + }); + test("Ensure 'remoteRoot' is left unaltered", async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + remoteRoot, + request: 'attach' + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('remoteRoot', remoteRoot); + }); + test("Ensure 'port' is left unaltered", async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const port = 12341234; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + port, + request: 'attach' + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('port', port); + }); + test("Ensure 'debugOptions' are left unaltered", async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); + const expectedDebugOptions = debugOptions.slice(); + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + debugOptions, + request: 'attach' + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('debugOptions').to.be.deep.equal(expectedDebugOptions); + }); + + const testsForJustMyCode = [ + { + justMyCode: false, + debugStdLib: true, + expectedResult: false + }, + { + justMyCode: false, + debugStdLib: false, + expectedResult: false + }, + { + justMyCode: false, + debugStdLib: undefined, + expectedResult: false + }, + { + justMyCode: true, + debugStdLib: false, + expectedResult: true + }, + { + justMyCode: true, + debugStdLib: true, + expectedResult: true + }, + { + justMyCode: true, + debugStdLib: undefined, + expectedResult: true + }, + { + justMyCode: undefined, + debugStdLib: false, + expectedResult: true + }, + { + justMyCode: undefined, + debugStdLib: true, + expectedResult: false + }, + { + justMyCode: undefined, + debugStdLib: undefined, + expectedResult: true + } + ]; + test('Ensure justMyCode property is correctly derived from debugStdLib', async () => { + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); + + testsForJustMyCode.forEach(async (testParams) => { + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + debugOptions, + request: 'attach', + justMyCode: testParams.justMyCode, + debugStdLib: testParams.debugStdLib + } as any) as DebugConfiguration); + expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); + }); + }); + }); +}); diff --git a/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts new file mode 100644 index 000000000000..1c8854e5a8bb --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-invalid-template-strings max-func-body-length no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { DebugConfiguration, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { DocumentManager } from '../../../../../client/common/application/documentManager'; +import { IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../../client/common/application/workspace'; +import { ConfigurationService } from '../../../../../client/common/configuration/service'; +import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import { PlatformService } from '../../../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../../../client/common/platform/types'; +import { IConfigurationService } from '../../../../../client/common/types'; +import { BaseConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/base'; +import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; + +suite('Debugging - Config Resolver', () => { + class BaseResolver extends BaseConfigurationResolver<AttachRequestArguments | LaunchRequestArguments> { + public resolveDebugConfiguration( + _folder: WorkspaceFolder | undefined, + _debugConfiguration: DebugConfiguration, + _token?: CancellationToken + ): Promise<AttachRequestArguments | LaunchRequestArguments | undefined> { + throw new Error('Not Implemented'); + } + public getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { + return super.getWorkspaceFolder(folder); + } + public getProgram(): string | undefined { + return super.getProgram(); + } + public resolveAndUpdatePythonPath( + workspaceFolder: Uri | undefined, + debugConfiguration: LaunchRequestArguments + ): void { + return super.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); + } + public debugOption(debugOptions: DebugOptions[], debugOption: DebugOptions) { + return super.debugOption(debugOptions, debugOption); + } + public isLocalHost(hostName?: string) { + return super.isLocalHost(hostName); + } + public isDebuggingFlask(debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>) { + return super.isDebuggingFlask(debugConfiguration); + } + } + let resolver: BaseResolver; + let workspaceService: IWorkspaceService; + let platformService: IPlatformService; + let documentManager: IDocumentManager; + let configurationService: IConfigurationService; + setup(() => { + workspaceService = mock(WorkspaceService); + documentManager = mock(DocumentManager); + platformService = mock(PlatformService); + configurationService = mock(ConfigurationService); + resolver = new BaseResolver( + instance(workspaceService), + instance(documentManager), + instance(platformService), + instance(configurationService) + ); + }); + + test('Program should return filepath of active editor if file is python', () => { + const expectedFileName = 'my.py'; + const editor = typemoq.Mock.ofType<TextEditor>(); + const doc = typemoq.Mock.ofType<TextDocument>(); + + editor + .setup((e) => e.document) + .returns(() => doc.object) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.languageId) + .returns(() => PYTHON_LANGUAGE) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.fileName) + .returns(() => expectedFileName) + .verifiable(typemoq.Times.once()); + when(documentManager.activeTextEditor).thenReturn(editor.object); + + const program = resolver.getProgram(); + + expect(program).to.be.equal(expectedFileName); + }); + test('Program should return undefined if active file is not python', () => { + const editor = typemoq.Mock.ofType<TextEditor>(); + const doc = typemoq.Mock.ofType<TextDocument>(); + + editor + .setup((e) => e.document) + .returns(() => doc.object) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.languageId) + .returns(() => 'C#') + .verifiable(typemoq.Times.once()); + when(documentManager.activeTextEditor).thenReturn(editor.object); + + const program = resolver.getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); + test('Program should return undefined if there is no active editor', () => { + when(documentManager.activeTextEditor).thenReturn(undefined); + + const program = resolver.getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); + test('Should get workspace folder when workspace folder is provided', () => { + const expectedUri = Uri.parse('mock'); + const folder: WorkspaceFolder = { index: 0, uri: expectedUri, name: 'mock' }; + + const uri = resolver.getWorkspaceFolder(folder); + + expect(uri).to.be.deep.equal(expectedUri); + }); + [ + { + title: 'Should get directory of active program when there are not workspace folders', + workspaceFolders: undefined + }, + { title: 'Should get directory of active program when there are 0 workspace folders', workspaceFolders: [] } + ].forEach((item) => { + test(item.title, () => { + const programPath = path.join('one', 'two', 'three.xyz'); + + resolver.getProgram = () => programPath; + when(workspaceService.workspaceFolders).thenReturn(item.workspaceFolders); + + const uri = resolver.getWorkspaceFolder(undefined); + + expect(uri!.fsPath).to.be.deep.equal(Uri.file(path.dirname(programPath)).fsPath); + }); + }); + test('Should return uri of workspace folder if there is only one workspace folder', () => { + const expectedUri = Uri.parse('mock'); + const folder: WorkspaceFolder = { index: 0, uri: expectedUri, name: 'mock' }; + const folders: WorkspaceFolder[] = [folder]; + + resolver.getProgram = () => undefined; + when(workspaceService.workspaceFolders).thenReturn(folders); + + const uri = resolver.getWorkspaceFolder(undefined); + + expect(uri!.fsPath).to.be.deep.equal(expectedUri.fsPath); + }); + test('Should return uri of workspace folder corresponding to program if there is more than one workspace folder', () => { + const programPath = path.join('one', 'two', 'three.xyz'); + const folder1: WorkspaceFolder = { index: 0, uri: Uri.parse('mock'), name: 'mock' }; + const folder2: WorkspaceFolder = { index: 1, uri: Uri.parse('134'), name: 'mock2' }; + const folders: WorkspaceFolder[] = [folder1, folder2]; + + resolver.getProgram = () => programPath; + when(workspaceService.workspaceFolders).thenReturn(folders); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(folder2); + + const uri = resolver.getWorkspaceFolder(undefined); + + expect(uri!.fsPath).to.be.deep.equal(folder2.uri.fsPath); + }); + test('Should return undefined when program does not belong to any of the workspace folders', () => { + const programPath = path.join('one', 'two', 'three.xyz'); + const folder1: WorkspaceFolder = { index: 0, uri: Uri.parse('mock'), name: 'mock' }; + const folder2: WorkspaceFolder = { index: 1, uri: Uri.parse('134'), name: 'mock2' }; + const folders: WorkspaceFolder[] = [folder1, folder2]; + + resolver.getProgram = () => programPath; + when(workspaceService.workspaceFolders).thenReturn(folders); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + + const uri = resolver.getWorkspaceFolder(undefined); + + expect(uri).to.be.deep.equal(undefined, 'not undefined'); + }); + test('Do nothing if debug configuration is undefined', () => { + resolver.resolveAndUpdatePythonPath(undefined, undefined as any); + }); + test('Python path in debug config must point to pythonpath in settings if pythonPath in config is not set', () => { + const config = {}; + const pythonPath = path.join('1', '2', '3'); + + when(configurationService.getSettings(anything())).thenReturn({ pythonPath } as any); + + resolver.resolveAndUpdatePythonPath(undefined, config as any); + + expect(config).to.have.property('pythonPath', pythonPath); + }); + test('Python path in debug config must point to pythonpath in settings if pythonPath in config is ${command:python.interpreterPath}', () => { + const config = { + pythonPath: '${command:python.interpreterPath}' + }; + const pythonPath = path.join('1', '2', '3'); + + when(configurationService.getSettings(anything())).thenReturn({ pythonPath } as any); + + resolver.resolveAndUpdatePythonPath(undefined, config as any); + + expect(config.pythonPath).to.equal(pythonPath); + }); + const localHostTestMatrix: Record<string, boolean> = { + localhost: true, + '127.0.0.1': true, + '::1': true, + '127.0.0.2': false, + '156.1.2.3': false, + '::2': false + }; + Object.keys(localHostTestMatrix).forEach((key) => { + test(`Local host = ${localHostTestMatrix[key]} for ${key}`, () => { + const isLocalHost = resolver.isLocalHost(key); + + expect(isLocalHost).to.equal(localHostTestMatrix[key]); + }); + }); + test('Is debugging flask=true', () => { + const config = { module: 'flask' }; + const isFlask = resolver.isDebuggingFlask(config as any); + expect(isFlask).to.equal(true, 'not flask'); + }); + test('Is debugging flask=false', () => { + const config = { module: 'flask2' }; + const isFlask = resolver.isDebuggingFlask(config as any); + expect(isFlask).to.equal(false, 'flask'); + }); + test('Is debugging flask=false when not defined', () => { + const config = {}; + const isFlask = resolver.isDebuggingFlask(config as any); + expect(isFlask).to.equal(false, 'flask'); + }); +}); diff --git a/src/test/debugger/extension/configuration/resolvers/common.ts b/src/test/debugger/extension/configuration/resolvers/common.ts new file mode 100644 index 000000000000..7c4af1600474 --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/common.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IPlatformService } from '../../../../../client/common/platform/types'; +import { getNamesAndValues } from '../../../../../client/common/utils/enum'; +import { getOSType, OSType } from '../../../../../client/common/utils/platform'; + +const OS_TYPE = getOSType(); + +interface IPathModule { + sep: string; + dirname(path: string): string; + join(...paths: string[]): string; +} + +// The set of information, related to a target OS, that are available +// to tests. The target OS is not necessarily the native OS. +type OSTestInfo = [ + string, // os name + OSType, + IPathModule +]; + +// For each supported OS, provide a set of helpers to use in tests. +export function getInfoPerOS(): OSTestInfo[] { + return getNamesAndValues(OSType).map((os) => { + const osType = os.value as OSType; + return [os.name, osType, getPathModuleForOS(osType)]; + }); +} + +// Decide which "path" module to use. +// By default we use the regular module. +function getPathModuleForOS(osType: OSType): IPathModule { + if (osType === OS_TYPE) { + return path; + } + + // We are testing a different OS from the native one. + // So use a "path" module matching the target OS. + return osType === OSType.Windows ? path.win32 : path.posix; +} + +// Generate the function to use for populating the +// relevant mocks relative to the target OS. +export function setUpOSMocks(osType: OSType, platformService: TypeMoq.IMock<IPlatformService>) { + platformService.setup((p) => p.isWindows).returns(() => osType === OSType.Windows); + platformService.setup((p) => p.isMac).returns(() => osType === OSType.OSX); + platformService.setup((p) => p.isLinux).returns(() => osType === OSType.Linux); +} diff --git a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts new file mode 100644 index 000000000000..ce0796459b32 --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -0,0 +1,862 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-invalid-template-strings no-any no-object-literal-type-assertion + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; +import { IInvalidPythonPathInDebuggerService } from '../../../../../client/application/diagnostics/types'; +import { IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; +import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; +import { IPlatformService } from '../../../../../client/common/platform/types'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../../client/common/process/types'; +import { IConfigurationService, IPythonSettings } from '../../../../../client/common/types'; +import { OSType } from '../../../../../client/common/utils/platform'; +import { DebuggerTypeName } from '../../../../../client/debugger/constants'; +import { IDebugEnvironmentVariablesService } from '../../../../../client/debugger/extension/configuration/resolvers/helper'; +import { LaunchConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/launch'; +import { DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; +import { IInterpreterHelper } from '../../../../../client/interpreter/contracts'; +import { getOSType } from '../../../../common'; +import { getInfoPerOS, setUpOSMocks } from './common'; + +getInfoPerOS().forEach(([osName, osType, path]) => { + if (osType === OSType.Unknown) { + return; + } + + suite(`Debugging - Config Resolver Launch, OS = ${osName}`, () => { + let debugProvider: DebugConfigurationProvider; + let platformService: TypeMoq.IMock<IPlatformService>; + let pythonExecutionService: TypeMoq.IMock<IPythonExecutionService>; + let helper: TypeMoq.IMock<IInterpreterHelper>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let documentManager: TypeMoq.IMock<IDocumentManager>; + let diagnosticsService: TypeMoq.IMock<IInvalidPythonPathInDebuggerService>; + let debugEnvHelper: TypeMoq.IMock<IDebugEnvironmentVariablesService>; + function createMoqWorkspaceFolder(folderPath: string) { + const folder = TypeMoq.Mock.ofType<WorkspaceFolder>(); + folder.setup((f) => f.uri).returns(() => Uri.file(folderPath)); + return folder.object; + } + function setupIoc(pythonPath: string, workspaceFolder?: WorkspaceFolder) { + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + diagnosticsService = TypeMoq.Mock.ofType<IInvalidPythonPathInDebuggerService>(); + debugEnvHelper = TypeMoq.Mock.ofType<IDebugEnvironmentVariablesService>(); + + pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + helper = TypeMoq.Mock.ofType<IInterpreterHelper>(); + pythonExecutionService.setup((x: any) => x.then).returns(() => undefined); + const factory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + factory + .setup((f) => f.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + helper.setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); + diagnosticsService + .setup((h) => h.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)); + + const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + if (workspaceFolder) { + settings.setup((s) => s.envFile).returns(() => path.join(workspaceFolder!.uri.fsPath, '.env2')); + } + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + setUpOSMocks(osType, platformService); + debugEnvHelper + .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})); + + debugProvider = new LaunchConfigurationResolver( + workspaceService.object, + documentManager.object, + diagnosticsService.object, + platformService.object, + configService.object, + debugEnvHelper.object + ); + } + function setupActiveEditor(fileName: string | undefined, languageId: string) { + if (fileName) { + const textEditor = TypeMoq.Mock.ofType<TextEditor>(); + const document = TypeMoq.Mock.ofType<TextDocument>(); + document.setup((d) => d.languageId).returns(() => languageId); + document.setup((d) => d.fileName).returns(() => fileName); + textEditor.setup((t) => t.document).returns(() => document.object); + documentManager.setup((d) => d.activeTextEditor).returns(() => textEditor.object); + } else { + documentManager.setup((d) => d.activeTextEditor).returns(() => undefined); + } + } + function setupWorkspaces(folders: string[]) { + const workspaceFolders = folders.map(createMoqWorkspaceFolder); + workspaceService.setup((w) => w.workspaceFolders).returns(() => workspaceFolders); + } + test('Defaults should be returned when an empty object is passed with a Workspace Folder and active file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + {} as DebugConfiguration + ); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('pythonPath', pythonPath); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('program', pythonFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + // tslint:disable-next-line:no-any + expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + }); + test("Defaults should be returned when an object with 'noDebug' property is passed with a Workspace Folder and active file", async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + noDebug: true + } as any) as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('pythonPath', pythonPath); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('program', pythonFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + // tslint:disable-next-line:no-any + expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, createMoqWorkspaceFolder(path.dirname(pythonFile))); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + const filePath = Uri.file(path.dirname('')).fsPath; + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('pythonPath', pythonPath); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('program', pythonFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + // tslint:disable-next-line:no-any + expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + setupIoc(pythonPath); + setupActiveEditor(undefined, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('pythonPath', pythonPath); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('program', ''); + expect(debugConfig).not.to.have.property('cwd'); + expect(debugConfig).not.to.have.property('envFile'); + expect(debugConfig).to.have.property('env'); + // tslint:disable-next-line:no-any + expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.js'; + setupIoc(pythonPath); + setupActiveEditor(activeFile, 'javascript'); + setupWorkspaces([]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('pythonPath', pythonPath); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('program', ''); + expect(debugConfig).not.to.have.property('cwd'); + expect(debugConfig).not.to.have.property('envFile'); + expect(debugConfig).to.have.property('env'); + // tslint:disable-next-line:no-any + expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const defaultWorkspace = path.join('usr', 'desktop'); + setupIoc(pythonPath, createMoqWorkspaceFolder(defaultWorkspace)); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + const filePath = Uri.file(defaultWorkspace).fsPath; + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + expect(debugConfig).to.have.property('pythonPath', pythonPath); + expect(debugConfig).to.have.property('type', 'python'); + expect(debugConfig).to.have.property('request', 'launch'); + expect(debugConfig).to.have.property('program', activeFile); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); + expect(debugConfig).to.have.property('env'); + // tslint:disable-next-line:no-any + expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); + }); + test("Ensure 'port' is left unaltered", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const port = 12341234; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + port, + request: 'launch' + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('port', port); + }); + test("Ensure 'localRoot' is left unaltered", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + request: 'launch' + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + }); + test("Ensure 'remoteRoot' is left unaltered", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + remoteRoot, + request: 'launch' + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('remoteRoot', remoteRoot); + }); + test("Ensure 'localRoot' and 'remoteRoot' are not used", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; + const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + localRoot, + remoteRoot, + request: 'launch' + } as any) as DebugConfiguration); + + expect(debugConfig!.pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); + }); + test('Ensure non-empty path mappings are used', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const expected = { + localRoot: `Debug_PythonPath_Local_Root_${new Date().toString()}`, + remoteRoot: `Debug_PythonPath_Remote_Root_${new Date().toString()}` + }; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + request: 'launch', + pathMappings: [expected] + } as any) as DebugConfiguration); + + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + expect(pathMappings).to.be.deep.equal([expected]); + }); + test('Ensure replacement in path mappings happens', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + request: 'launch', + pathMappings: [ + { + localRoot: '${workspaceFolder}/spam', + remoteRoot: '${workspaceFolder}/spam' + } + ] + } as any) as DebugConfiguration); + + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + expect(pathMappings).to.be.deep.equal([ + { + localRoot: `${workspaceFolder.uri.fsPath}/spam`, + remoteRoot: '${workspaceFolder}/spam' + } + ]); + }); + test('Ensure path mappings are not automatically added if missing', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + request: 'launch', + localRoot: localRoot + } as any) as DebugConfiguration); + + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + expect(pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); + }); + test('Ensure path mappings are not automatically added if empty', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + request: 'launch', + localRoot: localRoot, + pathMappings: [] + } as any) as DebugConfiguration); + + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + expect(pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); + }); + test('Ensure path mappings are not automatically added to existing', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + request: 'launch', + localRoot: localRoot, + pathMappings: [ + { + localRoot: '/spam', + remoteRoot: '.' + } + ] + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('localRoot', localRoot); + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + expect(pathMappings).to.be.deep.equal([ + { + localRoot: '/spam', + remoteRoot: '.' + } + ]); + }); + test('Ensure drive letter is lower cased for local path mappings on Windows when with existing path mappings', async function () { + if (getOSType() !== OSType.Windows || osType !== OSType.Windows) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + request: 'launch', + pathMappings: [ + { + localRoot, + remoteRoot: '/app/' + } + ] + } as any) as DebugConfiguration); + + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + const expected = Uri.file(`c${localRoot.substring(1)}`).fsPath; + expect(pathMappings).to.deep.equal([ + { + localRoot: expected, + remoteRoot: '/app/' + } + ]); + }); + test('Ensure drive letter is not lower cased for local path mappings on non-Windows when with existing path mappings', async function () { + if (getOSType() === OSType.Windows || osType === OSType.Windows) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + request: 'launch', + pathMappings: [ + { + localRoot, + remoteRoot: '/app/' + } + ] + } as any) as DebugConfiguration); + + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + expect(pathMappings).to.deep.equal([ + { + localRoot, + remoteRoot: '/app/' + } + ]); + }); + test('Ensure local path mappings are not modified when not pointing to a local drive', async () => { + const workspaceFolder = createMoqWorkspaceFolder(path.join('Server', 'Debug', 'Python_Path')); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + request: 'launch', + pathMappings: [ + { + localRoot: '/spam', + remoteRoot: '.' + } + ] + } as any) as DebugConfiguration); + + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + expect(pathMappings).to.deep.equal([ + { + localRoot: '/spam', + remoteRoot: '.' + } + ]); + }); + test('Ensure `${command:python.interpreterPath}` is replaced with actual pythonPath', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupIoc(pythonPath); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + pythonPath: '${command:python.interpreterPath}' + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('pythonPath', pythonPath); + }); + test('Ensure hardcoded pythonPath is left unaltered', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupIoc(pythonPath); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + pythonPath: debugPythonPath + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('pythonPath', debugPythonPath); + }); + test('Test defaults of debugger', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + {} as DebugConfiguration + ); + + expect(debugConfig).to.have.property('console', 'integratedTerminal'); + expect(debugConfig).to.have.property('stopOnEntry', false); + expect(debugConfig).to.have.property('showReturnValue', true); + expect(debugConfig).to.have.property('debugOptions'); + const expectedOptions = [DebugOptions.ShowReturnValue]; + if (osType === OSType.Windows) { + expectedOptions.push(DebugOptions.FixFilePathCase); + } + expect((debugConfig as any).debugOptions).to.be.deep.equal(expectedOptions); + }); + test('Test defaults of python debugger', async () => { + if ('python' === DebuggerTypeName) { + return; + } + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + {} as DebugConfiguration + ); + + expect(debugConfig).to.have.property('stopOnEntry', false); + expect(debugConfig).to.have.property('showReturnValue', true); + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as any).debugOptions).to.be.deep.equal([]); + }); + test('Test overriding defaults of debugger', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + redirectOutput: true, + justMyCode: false + } as LaunchRequestArguments); + + expect(debugConfig).to.have.property('console', 'integratedTerminal'); + expect(debugConfig).to.have.property('stopOnEntry', false); + expect(debugConfig).to.have.property('showReturnValue', true); + expect(debugConfig).to.have.property('redirectOutput', true); + expect(debugConfig).to.have.property('justMyCode', false); + expect(debugConfig).to.have.property('debugOptions'); + const expectedOptions = [ + DebugOptions.DebugStdLib, + DebugOptions.ShowReturnValue, + DebugOptions.RedirectOutput + ]; + if (osType === OSType.Windows) { + expectedOptions.push(DebugOptions.FixFilePathCase); + } + expect((debugConfig as any).debugOptions).to.be.deep.equal(expectedOptions); + }); + const testsForJustMyCode = [ + { + justMyCode: false, + debugStdLib: true, + expectedResult: false + }, + { + justMyCode: false, + debugStdLib: false, + expectedResult: false + }, + { + justMyCode: false, + debugStdLib: undefined, + expectedResult: false + }, + { + justMyCode: true, + debugStdLib: false, + expectedResult: true + }, + { + justMyCode: true, + debugStdLib: true, + expectedResult: true + }, + { + justMyCode: true, + debugStdLib: undefined, + expectedResult: true + }, + { + justMyCode: undefined, + debugStdLib: false, + expectedResult: true + }, + { + justMyCode: undefined, + debugStdLib: true, + expectedResult: false + }, + { + justMyCode: undefined, + debugStdLib: undefined, + expectedResult: true + } + ]; + test('Ensure justMyCode property is correctly derived from debugStdLib', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + testsForJustMyCode.forEach(async (testParams) => { + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + debugStdLib: testParams.debugStdLib, + justMyCode: testParams.justMyCode + } as LaunchRequestArguments); + expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); + }); + }); + const testsForRedirectOutput = [ + { + console: 'internalConsole', + redirectOutput: undefined, + expectedRedirectOutput: true + }, + { + console: 'integratedTerminal', + redirectOutput: undefined, + expectedRedirectOutput: undefined + }, + { + console: 'externalTerminal', + redirectOutput: undefined, + expectedRedirectOutput: undefined + }, + { + console: 'internalConsole', + redirectOutput: false, + expectedRedirectOutput: false + }, + { + console: 'integratedTerminal', + redirectOutput: false, + expectedRedirectOutput: false + }, + { + console: 'externalTerminal', + redirectOutput: false, + expectedRedirectOutput: false + }, + { + console: 'internalConsole', + redirectOutput: true, + expectedRedirectOutput: true + }, + { + console: 'integratedTerminal', + redirectOutput: true, + expectedRedirectOutput: true + }, + { + console: 'externalTerminal', + redirectOutput: true, + expectedRedirectOutput: true + } + ]; + test('Ensure redirectOutput property is correctly derived from console type', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + testsForRedirectOutput.forEach(async (testParams) => { + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + console: testParams.console, + redirectOutput: testParams.redirectOutput + } as LaunchRequestArguments); + expect(debugConfig).to.have.property('redirectOutput', testParams.expectedRedirectOutput); + if (testParams.expectedRedirectOutput) { + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as any).debugOptions).to.contain(DebugOptions.RedirectOutput); + } + }); + }); + test('Test fixFilePathCase', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + {} as DebugConfiguration + ); + if (osType === OSType.Windows) { + expect(debugConfig).to.have.property('debugOptions').contains(DebugOptions.FixFilePathCase); + } else { + expect(debugConfig).to.have.property('debugOptions').not.contains(DebugOptions.FixFilePathCase); + } + }); + test('Jinja added for Pyramid', async () => { + const workspacePath = path.join('usr', 'development', 'wksp1'); + const pythonPath = path.join(workspacePath, 'env', 'bin', 'python'); + const workspaceFolder = createMoqWorkspaceFolder(workspacePath); + const pythonFile = 'xyz.py'; + + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const options = { debugOptions: [DebugOptions.Pyramid], pyramid: true }; + + const debugConfig = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + (options as any) as DebugConfiguration + ); + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); + }); + test('Auto detect flask debugging', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, ({ + module: 'flask' + } as any) as DebugConfiguration); + + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); + }); + test('Test validation of Python Path when launching debugger (with invalid python path)', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup((h) => + h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + redirectOutput: false, + pythonPath + } as LaunchRequestArguments); + + diagnosticsService.verifyAll(); + expect(debugConfig).to.be.equal(undefined, 'Not undefined'); + }); + test('Test validation of Python Path when launching debugger (with valid python path)', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup((h) => + h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + redirectOutput: false, + pythonPath + } as LaunchRequestArguments); + + diagnosticsService.verifyAll(); + expect(debugConfig).to.not.be.equal(undefined, 'is undefined'); + }); + test('Resolve path to envFile', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + const expectedEnvFilePath = `${workspaceFolder.uri.fsPath}${ + osType === OSType.Windows ? '\\' : '/' + }${'wow.envFile'}`; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup((h) => + h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(true)); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { + redirectOutput: false, + pythonPath, + envFile: path.join('${workspaceFolder}', 'wow.envFile') + } as LaunchRequestArguments); + + expect(debugConfig!.envFile).to.be.equal(expectedEnvFilePath); + }); + async function testSetting( + requestType: 'launch' | 'attach', + settings: Record<string, boolean>, + debugOptionName: DebugOptions, + mustHaveDebugOption: boolean + ) { + setupIoc('pythonPath'); + const debugConfiguration: DebugConfiguration = { + request: requestType, + type: 'python', + name: '', + ...settings + }; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfiguration); + if (mustHaveDebugOption) { + expect((debugConfig as any).debugOptions).contains(debugOptionName); + } else { + expect((debugConfig as any).debugOptions).not.contains(debugOptionName); + } + } + type LaunchOrAttach = 'launch' | 'attach'; + const items: LaunchOrAttach[] = ['launch', 'attach']; + items.forEach((requestType) => { + test(`Must not contain Sub Process when not specified (${requestType})`, async () => { + await testSetting(requestType, {}, DebugOptions.SubProcess, false); + }); + test(`Must not contain Sub Process setting=false (${requestType})`, async () => { + await testSetting(requestType, { subProcess: false }, DebugOptions.SubProcess, false); + }); + test(`Must not contain Sub Process setting=true (${requestType})`, async () => { + await testSetting(requestType, { subProcess: true }, DebugOptions.SubProcess, true); + }); + }); + }); +}); diff --git a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts new file mode 100644 index 000000000000..69d353da035a --- /dev/null +++ b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect } from 'chai'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { ChildProcessAttachEventHandler } from '../../../../client/debugger/extension/hooks/childProcessAttachHandler'; +import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; +import { DebuggerEvents } from '../../../../client/debugger/extension/hooks/constants'; +import { AttachRequestArguments } from '../../../../client/debugger/types'; + +suite('Debug - Child Process', () => { + test('Do not attach if the event is undefined', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + await handler.handleCustomEvent(undefined as any); + verify(attachService.attach(anything(), anything())).never(); + }); + test('Do not attach to child process if event is invalid', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: any = {}; + const session: any = {}; + await handler.handleCustomEvent({ event: 'abc', body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Do not attach to child process if ptvsd_attach event is invalid', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: any = {}; + const session: any = {}; + await handler.handleCustomEvent({ event: DebuggerEvents.PtvsdAttachToSubprocess, body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Do not attach to child process if debugpy_attach event is invalid', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: any = {}; + const session: any = {}; + await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Exceptions are not bubbled up if exceptions are thrown', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: AttachRequestArguments = { + name: 'Attach', + type: 'python', + request: 'attach', + port: 1234, + subProcessId: 2 + }; + const session: any = {}; + when(attachService.attach(body, session)).thenThrow(new Error('Kaboom')); + await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session: {} as any }); + verify(attachService.attach(body, anything())).once(); + const [, secondArg] = capture(attachService.attach).last(); + expect(secondArg).to.deep.equal(session); + }); +}); diff --git a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts new file mode 100644 index 000000000000..c92147569be6 --- /dev/null +++ b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { expect } from 'chai'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { DebugService } from '../../../../client/common/application/debugService'; +import { IApplicationShell, IDebugService, IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; + +suite('Debug - Attach to Child Process', () => { + let shell: IApplicationShell; + let debugService: IDebugService; + let workspaceService: IWorkspaceService; + let attachService: ChildProcessAttachService; + + setup(() => { + shell = mock(ApplicationShell); + debugService = mock(DebugService); + workspaceService = mock(WorkspaceService); + attachService = new ChildProcessAttachService( + instance(shell), + instance(debugService), + instance(workspaceService) + ); + }); + + test('Message is not displayed if debugger is launched', async () => { + const data: AttachRequestArguments = { + name: 'Attach', + type: 'python', + request: 'attach', + port: 1234, + subProcessId: 2 + }; + const session: any = {}; + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(true as any); + when(shell.showErrorMessage(anything())).thenResolve(); + + await attachService.attach(data, session); + + verify(workspaceService.hasWorkspaceFolders).once(); + verify(debugService.startDebugging(anything(), anything(), anything())).once(); + verify(shell.showErrorMessage(anything())).never(); + }); + test('Message is displayed if debugger is not launched', async () => { + const data: AttachRequestArguments = { + name: 'Attach', + type: 'python', + request: 'attach', + port: 1234, + subProcessId: 2 + }; + + const session: any = {}; + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(false as any); + when(shell.showErrorMessage(anything())).thenResolve(); + + await attachService.attach(data, session); + + verify(workspaceService.hasWorkspaceFolders).once(); + verify(debugService.startDebugging(anything(), anything(), anything())).once(); + verify(shell.showErrorMessage(anything())).once(); + }); + test('Use correct workspace folder', async () => { + const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; + const wkspace1: WorkspaceFolder = { name: '0', index: 0, uri: Uri.file('0') }; + const wkspace2: WorkspaceFolder = { name: '2', index: 2, uri: Uri.file('2') }; + + const data: AttachRequestArguments = { + name: 'Attach', + type: 'python', + request: 'attach', + port: 1234, + subProcessId: 2, + workspaceFolder: rightWorkspaceFolder.uri.fsPath + }; + + const session: any = {}; + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + when(workspaceService.workspaceFolders).thenReturn([wkspace1, rightWorkspaceFolder, wkspace2]); + when(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).thenResolve(true as any); + + await attachService.attach(data, session); + + verify(workspaceService.hasWorkspaceFolders).once(); + verify(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).once(); + verify(shell.showErrorMessage(anything())).never(); + }); + test('Use empty workspace folder if right one is not found', async () => { + const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; + const wkspace1: WorkspaceFolder = { name: '0', index: 0, uri: Uri.file('0') }; + const wkspace2: WorkspaceFolder = { name: '2', index: 2, uri: Uri.file('2') }; + + const data: AttachRequestArguments = { + name: 'Attach', + type: 'python', + request: 'attach', + port: 1234, + subProcessId: 2, + workspaceFolder: rightWorkspaceFolder.uri.fsPath + }; + + const session: any = {}; + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + when(workspaceService.workspaceFolders).thenReturn([wkspace1, wkspace2]); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); + + await attachService.attach(data, session); + + verify(workspaceService.hasWorkspaceFolders).once(); + verify(debugService.startDebugging(undefined, anything(), anything())).once(); + verify(shell.showErrorMessage(anything())).never(); + }); + test('Validate debug config is passed as is', async () => { + const data: LaunchRequestArguments | AttachRequestArguments = { + request: 'attach', + type: 'python', + name: 'Attach', + port: 1234, + subProcessId: 2, + host: 'localhost' + }; + + const debugConfig = JSON.parse(JSON.stringify(data)); + debugConfig.host = 'localhost'; + const session: any = {}; + + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); + + await attachService.attach(data, session); + + verify(workspaceService.hasWorkspaceFolders).once(); + verify(debugService.startDebugging(undefined, anything(), anything())).once(); + const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); + expect(secondArg).to.deep.equal(debugConfig); + expect(thirdArg).to.deep.equal(session); + verify(shell.showErrorMessage(anything())).never(); + }); + test('Pass data as is if data is attach debug configuration', async () => { + const data: AttachRequestArguments = { + type: 'python', + request: 'attach', + name: '' + }; + const session: any = {}; + const debugConfig = JSON.parse(JSON.stringify(data)); + + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); + + await attachService.attach(data, session); + + verify(workspaceService.hasWorkspaceFolders).once(); + verify(debugService.startDebugging(undefined, anything(), anything())).once(); + const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); + expect(secondArg).to.deep.equal(debugConfig); + expect(thirdArg).to.deep.equal(session); + verify(shell.showErrorMessage(anything())).never(); + }); + test('Validate debug config when parent/root parent was attached', async () => { + const data: AttachRequestArguments = { + request: 'attach', + type: 'python', + name: 'Attach', + host: '123.123.123.123', + port: 1234, + subProcessId: 2 + }; + + const debugConfig = JSON.parse(JSON.stringify(data)); + debugConfig.host = data.host; + debugConfig.port = data.port; + debugConfig.request = 'attach'; + const session: any = {}; + + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); + + await attachService.attach(data, session); + + verify(workspaceService.hasWorkspaceFolders).once(); + verify(debugService.startDebugging(undefined, anything(), anything())).once(); + const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); + expect(secondArg).to.deep.equal(debugConfig); + expect(thirdArg).to.deep.equal(session); + verify(shell.showErrorMessage(anything())).never(); + }); +}); diff --git a/src/test/debugger/extension/serviceRegistry.unit.test.ts b/src/test/debugger/extension/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..53066a3d5571 --- /dev/null +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-invalid-template-strings max-func-body-length no-any + +import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { DebugAdapterActivator } from '../../../client/debugger/extension/adapter/activator'; +import { DebugAdapterDescriptorFactory } from '../../../client/debugger/extension/adapter/factory'; +import { DebugSessionLoggingFactory } from '../../../client/debugger/extension/adapter/logging'; +import { OutdatedDebuggerPromptFactory } from '../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; +import { AttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/factory'; +import { IAttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/types'; +import { DebuggerBanner } from '../../../client/debugger/extension/banner'; +import { PythonDebugConfigurationService } from '../../../client/debugger/extension/configuration/debugConfigurationService'; +import { LaunchJsonCompletionProvider } from '../../../client/debugger/extension/configuration/launch.json/completionProvider'; +import { InterpreterPathCommand } from '../../../client/debugger/extension/configuration/launch.json/interpreterPathCommand'; +import { LaunchJsonUpdaterService } from '../../../client/debugger/extension/configuration/launch.json/updaterService'; +import { DjangoLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/djangoLaunch'; +import { FileLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/fileLaunch'; +import { FlaskLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/flaskLaunch'; +import { ModuleLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/moduleLaunch'; +import { PidAttachDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/pidAttach'; +import { DebugConfigurationProviderFactory } from '../../../client/debugger/extension/configuration/providers/providerFactory'; +import { PyramidLaunchDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/pyramidLaunch'; +import { RemoteAttachDebugConfigurationProvider } from '../../../client/debugger/extension/configuration/providers/remoteAttach'; +import { AttachConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/attach'; +import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; +import { + IDebugConfigurationProviderFactory, + IDebugConfigurationResolver +} from '../../../client/debugger/extension/configuration/types'; +import { ChildProcessAttachEventHandler } from '../../../client/debugger/extension/hooks/childProcessAttachHandler'; +import { ChildProcessAttachService } from '../../../client/debugger/extension/hooks/childProcessAttachService'; +import { IChildProcessAttachService, IDebugSessionEventHandlers } from '../../../client/debugger/extension/hooks/types'; +import { registerTypes } from '../../../client/debugger/extension/serviceRegistry'; +import { + DebugConfigurationType, + IDebugAdapterDescriptorFactory, + IDebugConfigurationProvider, + IDebugConfigurationService, + IDebuggerBanner, + IDebugSessionLoggingFactory, + IOutdatedDebuggerPromptFactory +} from '../../../client/debugger/extension/types'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../../client/debugger/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Debugging - Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + test('Registrations', () => { + registerTypes(instance(serviceManager)); + + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + InterpreterPathCommand + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationService>( + IDebugConfigurationService, + PythonDebugConfigurationService + ) + ).once(); + verify(serviceManager.addSingleton<IDebuggerBanner>(IDebuggerBanner, DebuggerBanner)).once(); + verify( + serviceManager.addSingleton<IChildProcessAttachService>( + IChildProcessAttachService, + ChildProcessAttachService + ) + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + LaunchJsonCompletionProvider + ) + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + LaunchJsonUpdaterService + ) + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + DebugAdapterActivator + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugAdapterDescriptorFactory>( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugSessionEventHandlers>( + IDebugSessionEventHandlers, + ChildProcessAttachEventHandler + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationResolver<LaunchRequestArguments>>( + IDebugConfigurationResolver, + LaunchConfigurationResolver, + 'launch' + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationResolver<AttachRequestArguments>>( + IDebugConfigurationResolver, + AttachConfigurationResolver, + 'attach' + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationProviderFactory>( + IDebugConfigurationProviderFactory, + DebugConfigurationProviderFactory + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationProvider>( + IDebugConfigurationProvider, + FileLaunchDebugConfigurationProvider, + DebugConfigurationType.launchFile + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationProvider>( + IDebugConfigurationProvider, + DjangoLaunchDebugConfigurationProvider, + DebugConfigurationType.launchDjango + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationProvider>( + IDebugConfigurationProvider, + FlaskLaunchDebugConfigurationProvider, + DebugConfigurationType.launchFlask + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationProvider>( + IDebugConfigurationProvider, + RemoteAttachDebugConfigurationProvider, + DebugConfigurationType.remoteAttach + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationProvider>( + IDebugConfigurationProvider, + ModuleLaunchDebugConfigurationProvider, + DebugConfigurationType.launchModule + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationProvider>( + IDebugConfigurationProvider, + PyramidLaunchDebugConfigurationProvider, + DebugConfigurationType.launchPyramid + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationProvider>( + IDebugConfigurationProvider, + PidAttachDebugConfigurationProvider, + DebugConfigurationType.pidAttach + ) + ).once(); + + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + DebugAdapterActivator + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugAdapterDescriptorFactory>( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory + ) + ).once(); + verify( + serviceManager.addSingleton<IDebugSessionLoggingFactory>( + IDebugSessionLoggingFactory, + DebugSessionLoggingFactory + ) + ).once(); + verify( + serviceManager.addSingleton<IOutdatedDebuggerPromptFactory>( + IOutdatedDebuggerPromptFactory, + OutdatedDebuggerPromptFactory + ) + ).once(); + verify( + serviceManager.addSingleton<IAttachProcessProviderFactory>( + IAttachProcessProviderFactory, + AttachProcessProviderFactory + ) + ).once(); + }); +}); diff --git a/src/test/debugger/utils.ts b/src/test/debugger/utils.ts new file mode 100644 index 000000000000..61496a14671f --- /dev/null +++ b/src/test/debugger/utils.ts @@ -0,0 +1,368 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-classes-per-file + +import { expect } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { sleep } from '../../client/common/utils/async'; +import { getDebugpyLauncherArgs } from '../../client/debugger/extension/adapter/remoteLaunchers'; +import { PythonFixture } from '../fixtures'; +import { Proc, ProcOutput, ProcResult } from '../proc'; + +const launchJSON = path.join(EXTENSION_ROOT_DIR, 'src', 'test', '.vscode', 'launch.json'); + +export function getConfig(name: string): vscode.DebugConfiguration { + const configs = fs.readJSONSync(launchJSON); + for (const config of configs.configurations) { + if (config.name === name) { + return config; + } + } + throw Error(`debug config "${name}" not found`); +} + +type DAPSource = 'vscode' | 'debugpy'; +type DAPHandler = (src: DAPSource, msg: DebugProtocol.ProtocolMessage) => void; + +type TrackedDebugger = { + id: number; + output: ProcOutput; + dapHandler?: DAPHandler; + session?: vscode.DebugSession; + exitCode?: number; +}; + +class DebugAdapterTracker { + constructor( + // This contains all the state. + private readonly tracked: TrackedDebugger + ) {} + + // debugpy -> VS Code + // tslint:disable-next-line:no-any + public onDidSendMessage(message: any): void { + this.onDAPMessage('debugpy', message as DebugProtocol.ProtocolMessage); + } + + // VS Code -> debugpy + // tslint:disable-next-line:no-any + public onWillReceiveMessage(message: any): void { + this.onDAPMessage('vscode', message as DebugProtocol.ProtocolMessage); + } + + public onExit(code: number | undefined, signal: string | undefined): void { + if (code) { + this.tracked.exitCode = code; + } else if (signal) { + this.tracked.exitCode = 1; + } else { + this.tracked.exitCode = 0; + } + } + + // The following vscode.DebugAdapterTracker methods are not implemented: + // + // * onWillStartSession(): void; + // * onWillStopSession(): void; + // * onError(error: Error): void; + + private onDAPMessage(src: DAPSource, msg: DebugProtocol.ProtocolMessage) { + // Unomment this to see the DAP messages sent between VS Code and debugpy: + //console.log(`| DAP (${src === 'vscode' ? 'VS Code -> debugpy' : 'debugpy -> VS Code'})\n`, msg, '\n| DAP'); + + // See: https://microsoft.github.io/debug-adapter-protocol/specification + if (this.tracked.dapHandler) { + this.tracked.dapHandler(src, msg); + } + if (msg.type === 'event') { + const event = ((msg as unknown) as DebugProtocol.Event).event; + if (event === 'output') { + this.onOutputEvent((msg as unknown) as DebugProtocol.OutputEvent); + } + } + } + + private onOutputEvent(msg: DebugProtocol.OutputEvent) { + if (msg.body.category === undefined) { + msg.body.category = 'stdout'; + } + const data = Buffer.from(msg.body.output, 'utf-8'); + if (msg.body.category === 'stdout') { + this.tracked.output.addStdout(data); + } else if (msg.body.category === 'stderr') { + this.tracked.output.addStderr(data); + } else { + // Ignore it. + } + } +} + +class Debuggers { + private nextID = 0; + private tracked: { [id: number]: TrackedDebugger } = {}; + private results: { [id: number]: ProcResult } = {}; + + public track(config: vscode.DebugConfiguration, output?: ProcOutput): number { + if (this.nextID === 0) { + vscode.debug.registerDebugAdapterTrackerFactory('python', this); + } + if (output === undefined) { + output = new ProcOutput(); + } + this.nextID += 1; + const id = this.nextID; + this.tracked[id] = { id, output }; + config._test_session_id = id; + return id; + } + + public setDAPHandler(id: number, handler: DAPHandler) { + const tracked = this.tracked[id]; + if (tracked !== undefined) { + tracked.dapHandler = handler; + } + } + + public getSession(id: number): vscode.DebugSession | undefined { + const tracked = this.tracked[id]; + if (tracked === undefined) { + return undefined; + } else { + return tracked.session; + } + } + + public async waitUntilDone(id: number): Promise<ProcResult> { + const cachedResult = this.results[id]; + if (cachedResult !== undefined) { + return cachedResult; + } + const tracked = this.tracked[id]; + if (tracked === undefined) { + throw Error(`untracked debugger ${id}`); + } else { + while (tracked.exitCode === undefined) { + await sleep(10); // milliseconds + } + const result = { + exitCode: tracked.exitCode, + stdout: tracked.output.stdout + }; + this.results[id] = result; + return result; + } + } + + // This is for DebugAdapterTrackerFactory: + public createDebugAdapterTracker(session: vscode.DebugSession): vscode.ProviderResult<vscode.DebugAdapterTracker> { + const id = session.configuration._test_session_id; + const tracked = this.tracked[id]; + if (tracked !== undefined) { + tracked.session = session; + return new DebugAdapterTracker(tracked); + } else if (id !== undefined) { + // This should not have happened, but we don't worry about + // it for now. + } + return undefined; + } +} +const debuggers = new Debuggers(); + +class DebuggerSession { + private started: boolean = false; + private raw: vscode.DebugSession | undefined; + private stopped: { breakpoint: boolean; threadId: number } | undefined; + constructor( + public readonly id: number, + public readonly config: vscode.DebugConfiguration, + private readonly wsRoot?: vscode.WorkspaceFolder, + private readonly proc?: Proc + ) {} + + public async start() { + if (this.started) { + throw Error('already started'); + } + this.started = true; + + // Un-comment this to see the debug config used in this session: + //console.log('|', session.config, '|'); + const started = await vscode.debug.startDebugging(this.wsRoot, this.config); + expect(started).to.be.equal(true, 'Debugger did not sart'); + this.raw = debuggers.getSession(this.id); + expect(this.raw).to.not.equal(undefined, 'session not set'); + } + + public async waitUntilDone(): Promise<ProcResult> { + if (this.proc) { + return this.proc.waitUntilDone(); + } else { + return debuggers.waitUntilDone(this.id); + } + } + + public addBreakpoint(filename: string, line: number, ch?: number): vscode.Breakpoint { + // The arguments are 1-indexed. + const loc = new vscode.Location( + vscode.Uri.file(filename), + // VS Code wants 0-indexed line and column numbers. + // We default to the beginning of the line. + new vscode.Position(line - 1, ch ? ch - 1 : 0) + ); + const bp = new vscode.SourceBreakpoint(loc); + vscode.debug.addBreakpoints([bp]); + return bp; + } + + public async waitForBreakpoint(bp: vscode.Breakpoint, opts: { clear: boolean } = { clear: true }) { + while (!this.stopped || !this.stopped.breakpoint) { + await sleep(10); // milliseconds + } + if (opts.clear) { + vscode.debug.removeBreakpoints([bp]); + await this.raw!.customRequest('continue', { threadId: this.stopped.threadId }); + this.stopped = undefined; + } + } + + public handleDAPMessage(_src: DAPSource, baseMsg: DebugProtocol.ProtocolMessage) { + if (baseMsg.type === 'event') { + const event = ((baseMsg as unknown) as DebugProtocol.Event).event; + if (event === 'stopped') { + const msg = (baseMsg as unknown) as DebugProtocol.StoppedEvent; + this.stopped = { + breakpoint: msg.body.reason === 'breakpoint', + threadId: (msg.body.threadId as unknown) as number + }; + } else { + // For now there aren't any other events we care about. + } + } else if (baseMsg.type === 'request') { + // For now there aren't any requests we care about. + } else if (baseMsg.type === 'response') { + // For now there aren't any responses we care about. + } else { + // This shouldn't happen but for now we don't worry about it. + } + } + + // The old debug adapter tests used + // 'vscode-debugadapter-testsupport'.DebugClient to interact with + // the debugger. This is helpful info when we are considering + // additional debugger-related tests. Here are the methods/props + // the old tests used: + // + // * defaultTimeout + // * start() + // * stop() + // * initializeRequest() + // * configurationSequence() + // * launch() + // * attachRequest() + // * waitForEvent() + // * assertOutput() + // * threadsRequest() + // * continueRequest() + // * scopesRequest() + // * variablesRequest() + // * setBreakpointsRequest() + // * setExceptionBreakpointsRequest() + // * assertStoppedLocation() +} + +export class DebuggerFixture extends PythonFixture { + public resolveDebugger( + configName: string, + file: string, + scriptArgs: string[], + wsRoot?: vscode.WorkspaceFolder + ): DebuggerSession { + const config = getConfig(configName); + let proc: Proc | undefined; + if (config.request === 'launch') { + config.program = file; + config.args = scriptArgs; + config.redirectOutput = false; + // XXX set the file in the current vscode editor? + } else if (config.request === 'attach') { + if (config.port) { + proc = this.runDebugger(config.port, file, ...scriptArgs); + if (wsRoot && config.name === 'attach to a local port') { + config.pathMappings.localRoot = wsRoot.uri.fsPath; + } + } else if (config.processId) { + proc = this.runScript(file, ...scriptArgs); + config.processId = proc.pid; + } else { + throw Error(`unsupported attach config "${configName}"`); + } + if (wsRoot && config.pathMappings) { + config.pathMappings.localRoot = wsRoot.uri.fsPath; + } + } else { + throw Error(`unsupported request type "${config.request}"`); + } + const id = debuggers.track(config); + const session = new DebuggerSession(id, config, wsRoot, proc); + debuggers.setDAPHandler(id, (src, msg) => session.handleDAPMessage(src, msg)); + return session; + } + + public getLaunchTarget(filename: string, args: string[]): vscode.DebugConfiguration { + return { + type: 'python', + name: 'debug', + request: 'launch', + program: filename, + args: args, + console: 'integratedTerminal' + }; + } + + public getAttachTarget(filename: string, args: string[], port?: number): vscode.DebugConfiguration { + if (port) { + this.runDebugger(port, filename, ...args); + return { + type: 'python', + name: 'debug', + request: 'attach', + port: port, + host: 'localhost', + pathMappings: [ + { + // tslint:disable-next-line:no-invalid-template-strings + localRoot: '${workspaceFolder}', + remoteRoot: '.' + } + ] + }; + } else { + const proc = this.runScript(filename, ...args); + return { + type: 'python', + name: 'debug', + request: 'attach', + processId: proc.pid + }; + } + } + + public runDebugger(port: number, filename: string, ...scriptArgs: string[]) { + const args = getDebugpyLauncherArgs({ + host: 'localhost', + port: port, + // This causes problems if we set it to true. + waitUntilDebuggerAttaches: false + }); + args.push(filename, ...scriptArgs); + return this.runScript(args[0], ...args.slice(1)); + } +} diff --git a/src/test/debuggerTest.ts b/src/test/debuggerTest.ts new file mode 100644 index 000000000000..c7fc3e1058d1 --- /dev/null +++ b/src/test/debuggerTest.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-console + +import * as path from 'path'; +import { runTests } from 'vscode-test'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; + +const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); +process.env.IS_CI_SERVER_TEST_DEBUGGER = '1'; +process.env.VSC_PYTHON_CI_TEST = '1'; + +function start() { + console.log('*'.repeat(100)); + console.log('Start Debugger tests'); + runTests({ + extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, + extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), + launchArgs: [workspacePath], + version: 'stable', + extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' } + }).catch((ex) => { + console.error('End Debugger tests (with errors)', ex); + process.exit(1); + }); +} +start(); diff --git a/src/test/extension-version.functional.test.ts b/src/test/extension-version.functional.test.ts new file mode 100644 index 000000000000..f19310c6540b --- /dev/null +++ b/src/test/extension-version.functional.test.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as path from 'path'; +import { ApplicationEnvironment } from '../client/common/application/applicationEnvironment'; +import { IApplicationEnvironment } from '../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../client/common/constants'; + +suite('Extension version tests', () => { + let version: string; + let applicationEnvironment: IApplicationEnvironment; + const branchName = process.env.CI_BRANCH_NAME; + + suiteSetup(async function () { + // Skip the entire suite if running locally + if (!branchName) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + }); + + setup(() => { + applicationEnvironment = new ApplicationEnvironment(undefined as any, undefined as any, undefined as any); + version = applicationEnvironment.packageJson.version; + }); + + test('If we are running a pipeline in the main branch, the extension version in `package.json` should have the "-dev" suffix', async function () { + if (branchName !== 'main') { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + return expect( + version.endsWith('-dev'), + 'When running a pipeline in the main branch, the extension version in package.json should have the -dev suffix' + ).to.be.true; + }); + + test('If we are running a pipeline in the release branch, the extension version in `package.json` should not have the "-dev" suffix', async function () { + if (!branchName!.startsWith('release')) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + return expect( + version.endsWith('-dev'), + 'When running a pipeline in the release branch, the extension version in package.json should not have the -dev suffix' + ).to.be.false; + }); +}); + +suite('Extension localization files', () => { + test('Load localization file', () => { + const filesFailed: string[] = []; + glob.sync('package.nls.*.json', { sync: true, cwd: EXTENSION_ROOT_DIR }).forEach((localizationFile) => { + try { + JSON.parse(fs.readFileSync(path.join(EXTENSION_ROOT_DIR, localizationFile)).toString()); + } catch { + filesFailed.push(localizationFile); + } + }); + + expect(filesFailed).to.be.lengthOf(0, `Failed to load JSON for ${filesFailed.join(', ')}`); + }); +}); diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts new file mode 100644 index 000000000000..0df2aaddffb8 --- /dev/null +++ b/src/test/fixtures.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs-extra'; +import { sleep } from '../client/common/utils/async'; +import { PYTHON_PATH } from './common'; +import { Proc, spawn } from './proc'; + +export type CleanupFunc = (() => void) | (() => Promise<void>); + +export class CleanupFixture { + private cleanups: CleanupFunc[]; + constructor() { + this.cleanups = []; + } + + public addCleanup(cleanup: CleanupFunc) { + this.cleanups.push(cleanup); + } + public addFSCleanup(filename: string) { + this.addCleanup(async () => { + try { + await fs.unlink(filename); + } catch { + // The file is already gone. + } + }); + } + + public async cleanUp() { + const cleanups = this.cleanups; + this.cleanups = []; + + return Promise.all( + cleanups.map(async (cleanup, i) => { + try { + const res = cleanup(); + if (res) { + await res; + } + } catch (err) { + // tslint:disable-next-line:no-console + console.error(`cleanup ${i + 1} failed: ${err}`); + // tslint:disable-next-line:no-console + console.error('moving on...'); + } + }) + ); + } +} + +export class PythonFixture extends CleanupFixture { + public readonly python: string; + constructor( + // If not provided, we will use the global default. + python?: string + ) { + super(); + if (python) { + this.python = python; + } else { + this.python = PYTHON_PATH; + } + } + + public runScript(filename: string, ...args: string[]): Proc { + return this.spawn(filename, ...args); + } + + public runModule(name: string, ...args: string[]): Proc { + return this.spawn('-m', name, ...args); + } + + private spawn(...args: string[]) { + const proc = spawn(this.python, ...args); + this.addCleanup(async () => { + if (!proc.exited) { + await sleep(1000); // Wait a sec before the hammer falls. + try { + proc.raw.kill(); + } catch { + // It already finished. + } + } + }); + return proc; + } +} diff --git a/src/test/format/extension.dispatch.test.ts b/src/test/format/extension.dispatch.test.ts new file mode 100644 index 000000000000..19d937a4cfc6 --- /dev/null +++ b/src/test/format/extension.dispatch.test.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as TypeMoq from 'typemoq'; +import { + CancellationToken, + FormattingOptions, + OnTypeFormattingEditProvider, + Position, + ProviderResult, + TextDocument, + TextEdit +} from 'vscode'; +import { OnTypeFormattingDispatcher } from '../../client/typeFormatters/dispatcher'; + +suite('Formatting - Dispatcher', () => { + const doc = TypeMoq.Mock.ofType<TextDocument>(); + const pos = TypeMoq.Mock.ofType<Position>(); + const opt = TypeMoq.Mock.ofType<FormattingOptions>(); + const token = TypeMoq.Mock.ofType<CancellationToken>(); + const edits = TypeMoq.Mock.ofType<ProviderResult<TextEdit[]>>(); + + test('No providers', async () => { + const dispatcher = new OnTypeFormattingDispatcher({}); + + const triggers = dispatcher.getTriggerCharacters(); + assert.equal(triggers, undefined, 'Trigger was not undefined'); + + const result = await dispatcher.provideOnTypeFormattingEdits( + doc.object, + pos.object, + '\n', + opt.object, + token.object + ); + assert.deepStrictEqual(result, [], 'Did not return an empty list of edits'); + }); + + test('Single provider', () => { + const provider = setupProvider(doc.object, pos.object, ':', opt.object, token.object, edits.object); + + const dispatcher = new OnTypeFormattingDispatcher({ + ':': provider.object + }); + + const triggers = dispatcher.getTriggerCharacters(); + assert.deepStrictEqual(triggers, { first: ':', more: [] }, 'Did not return correct triggers'); + + const result = dispatcher.provideOnTypeFormattingEdits(doc.object, pos.object, ':', opt.object, token.object); + assert.equal(result, edits.object, 'Did not return correct edits'); + + provider.verifyAll(); + }); + + test('Two providers', () => { + const colonProvider = setupProvider(doc.object, pos.object, ':', opt.object, token.object, edits.object); + + const doc2 = TypeMoq.Mock.ofType<TextDocument>(); + const pos2 = TypeMoq.Mock.ofType<Position>(); + const opt2 = TypeMoq.Mock.ofType<FormattingOptions>(); + const token2 = TypeMoq.Mock.ofType<CancellationToken>(); + const edits2 = TypeMoq.Mock.ofType<ProviderResult<TextEdit[]>>(); + + const newlineProvider = setupProvider( + doc2.object, + pos2.object, + '\n', + opt2.object, + token2.object, + edits2.object + ); + + const dispatcher = new OnTypeFormattingDispatcher({ + ':': colonProvider.object, + '\n': newlineProvider.object + }); + + const triggers = dispatcher.getTriggerCharacters(); + assert.deepStrictEqual(triggers, { first: '\n', more: [':'] }, 'Did not return correct triggers'); + + const result = dispatcher.provideOnTypeFormattingEdits(doc.object, pos.object, ':', opt.object, token.object); + assert.equal(result, edits.object, 'Did not return correct editsfor colon provider'); + + const result2 = dispatcher.provideOnTypeFormattingEdits( + doc2.object, + pos2.object, + '\n', + opt2.object, + token2.object + ); + assert.equal(result2, edits2.object, 'Did not return correct edits for newline provider'); + + colonProvider.verifyAll(); + newlineProvider.verifyAll(); + }); + + function setupProvider( + document: TextDocument, + position: Position, + ch: string, + options: FormattingOptions, + cancellationToken: CancellationToken, + result: ProviderResult<TextEdit[]> + ): TypeMoq.IMock<OnTypeFormattingEditProvider> { + const provider = TypeMoq.Mock.ofType<OnTypeFormattingEditProvider>(); + provider + .setup((p) => p.provideOnTypeFormattingEdits(document, position, ch, options, cancellationToken)) + .returns(() => result) + .verifiable(TypeMoq.Times.once()); + return provider; + } +}); diff --git a/src/test/format/extension.format.ds.test.ts b/src/test/format/extension.format.ds.test.ts new file mode 100644 index 000000000000..631c47bfeea6 --- /dev/null +++ b/src/test/format/extension.format.ds.test.ts @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { commands, ConfigurationTarget, Uri } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IPythonToolExecutionService } from '../../client/common/process/types'; +import { IDisposable, Product } from '../../client/common/types'; +import { INotebookEditorProvider } from '../../client/datascience/types'; +import { IExtensionTestApi } from '../common'; +import { + canRunTests, + closeNotebooksAndCleanUpAfterTests, + createTemporaryNotebook, + disposeAllDisposables, + trustAllNotebooks +} from '../datascience/notebook/helper'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize, initializeTest } from '../initialize'; + +// tslint:disable: no-any no-invalid-this +suite('Formatting - Notebooks', () => { + let api: IExtensionTestApi; + suiteSetup(async function () { + api = await initialize(); + if (!(await canRunTests())) { + return this.skip(); + } + }); + suiteTeardown(closeNotebooksAndCleanUpAfterTests); + ['yapf', 'black', 'autopep8'].forEach((formatter) => { + suite(formatter, () => { + const disposables: IDisposable[] = []; + let testIPynb: Uri; + let executionService: IPythonToolExecutionService; + let editorProvider: INotebookEditorProvider; + let fs: IFileSystem; + const product: Product = + formatter === 'yapf' ? Product.yapf : formatter === 'black' ? Product.black : Product.autopep8; + suiteSetup(async () => { + const workspaceService = api.serviceContainer.get<IWorkspaceService>(IWorkspaceService); + const config = workspaceService.getConfiguration( + 'python.formatting', + workspaceService.workspaceFolders![0].uri + ); + await config.update('provider', formatter, ConfigurationTarget.Workspace); + }); + setup(async () => { + sinon.restore(); + await initializeTest(); + await trustAllNotebooks(); + // Don't use same file (due to dirty handling, we might save in dirty.) + // Cuz we won't save to file, hence extension will backup in dirty file and when u re-open it will open from dirty. + const templateIPynb = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'datascience', + 'notebook', + 'test.ipynb' + ); + testIPynb = Uri.file(await createTemporaryNotebook(templateIPynb, disposables)); + executionService = api.serviceContainer.get<IPythonToolExecutionService>(IPythonToolExecutionService); + editorProvider = api.serviceContainer.get<INotebookEditorProvider>(INotebookEditorProvider); + fs = api.serviceContainer.get<IFileSystem>(IFileSystem); + }); + teardown(closeNotebooksAndCleanUpAfterTests); + suiteTeardown(() => disposeAllDisposables(disposables)); + test('Formatted with temporary file when formatting existing saved notebooks (without changes)', async () => { + // Open a new notebook & add a cell + await editorProvider.open(testIPynb); + + // Check if a temp file is created. + const spiedExecution = sinon.spy(executionService, 'exec'); + const spiedWriteFile = sinon.spy(fs, 'writeFile'); + disposables.push({ dispose: () => spiedWriteFile.restore() }); + disposables.push({ dispose: () => spiedExecution.restore() }); + + // Format the cell + await commands.executeCommand('notebook.formatCell'); + + // Verify a temp file was created, having a file starting with thenb file name. + assert.isOk( + spiedWriteFile + .getCalls() + .some( + (call) => + call.args[0].includes(path.basename(testIPynb.fsPath)) && + call.args[0].includes('.ipynb') + ), + 'Temp file not created' + ); + // Verify we tried to format. + assert.isOk( + spiedExecution.getCalls().some((call) => call.args[0].product === product), + 'Not formatted' + ); + }); + test('Formatted with temporary file when formatting untitled notebooks', async () => { + // Open a new notebook & add a cell + await editorProvider.createNew(); + + // Check if a temp file is created. + const spiedExecution = sinon.spy(executionService, 'exec'); + const spiedWriteFile = sinon.spy(fs, 'writeFile'); + disposables.push({ dispose: () => spiedWriteFile.restore() }); + disposables.push({ dispose: () => spiedExecution.restore() }); + + // Format the cell + await commands.executeCommand('notebook.formatCell'); + + // Verify a temp file was created, having a file starting with thenb file name. + assert.isOk( + spiedWriteFile.getCalls().some((call) => call.args[0].includes('.ipynb')), + 'Temp file not created' + ); + // Verify we tried to format. + assert.isOk( + spiedExecution.getCalls().some((call) => call.args[0].product === product), + 'Not formatted' + ); + }); + }); + }); +}); diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts new file mode 100644 index 000000000000..bd543c81e5cc --- /dev/null +++ b/src/test/format/extension.format.test.ts @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { CancellationTokenSource, Position, Uri, window, workspace } from 'vscode'; +import { IProcessServiceFactory } from '../../client/common/process/types'; +import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; +import { BlackFormatter } from '../../client/formatters/blackFormatter'; +import { YapfFormatter } from '../../client/formatters/yapfFormatter'; +import { registerForIOC } from '../../client/pythonEnvironments/legacyIOC'; +import { isPythonVersionInProcess } from '../common'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { MockProcessService } from '../mocks/proc'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; +import { compareFiles } from '../textUtils'; + +const ch = window.createOutputChannel('Tests'); +const formatFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); +const workspaceRootPath = path.join(__dirname, '..', '..', '..', 'src', 'test'); +const originalUnformattedFile = path.join(formatFilesPath, 'fileToFormat.py'); + +const autoPep8FileToFormat = path.join(formatFilesPath, 'autoPep8FileToFormat.py'); +const autoPep8Formatted = path.join(formatFilesPath, 'autoPep8Formatted.py'); +const blackFileToFormat = path.join(formatFilesPath, 'blackFileToFormat.py'); +const blackFormatted = path.join(formatFilesPath, 'blackFormatted.py'); +const yapfFileToFormat = path.join(formatFilesPath, 'yapfFileToFormat.py'); +const yapfFormatted = path.join(formatFilesPath, 'yapfFormatted.py'); + +let formattedYapf = ''; +let formattedBlack = ''; +let formattedAutoPep8 = ''; + +// tslint:disable-next-line:max-func-body-length +suite('Formatting - General', () => { + let ioc: UnitTestIocContainer; + + suiteSetup(async function () { + // https://github.com/microsoft/vscode-python/issues/12564 + // Skipping one test in the file is resulting in the next one failing, so skipping the entire suiteuntil further investigation. + // tslint:disable-next-line: no-invalid-this + return this.skip(); + await initialize(); + initializeDI(); + [autoPep8FileToFormat, blackFileToFormat, yapfFileToFormat].forEach((file) => { + fs.copySync(originalUnformattedFile, file, { overwrite: true }); + }); + formattedYapf = fs.readFileSync(yapfFormatted).toString(); + formattedAutoPep8 = fs.readFileSync(autoPep8Formatted).toString(); + formattedBlack = fs.readFileSync(blackFormatted).toString(); + }); + + async function formattingTestIsBlackSupported(): Promise<boolean> { + const processService = await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create(Uri.file(workspaceRootPath)); + return !(await isPythonVersionInProcess(processService, '2', '3.0', '3.1', '3.2', '3.3', '3.4', '3.5')); + } + + setup(async () => { + await initializeTest(); + initializeDI(); + }); + suiteTeardown(async () => { + [autoPep8FileToFormat, blackFileToFormat, yapfFileToFormat].forEach((file) => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + ch.dispose(); + await closeActiveWindows(); + }); + teardown(async () => { + await ioc.dispose(); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerUnitTestTypes(); + ioc.registerFormatterTypes(); + ioc.registerInterpreterStorageTypes(); + + // Mocks. + ioc.registerMockProcessTypes(); + ioc.registerMockInterpreterTypes(); + + registerForIOC(ioc.serviceManager, ioc.serviceContainer); + } + + async function injectFormatOutput(outputFileName: string) { + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + procService.onExecObservable((_file, args, _options, callback) => { + if (args.indexOf('--diff') >= 0) { + callback({ + out: fs.readFileSync(path.join(formatFilesPath, outputFileName), 'utf8'), + source: 'stdout' + }); + } + }); + } + + async function testFormatting( + formatter: AutoPep8Formatter | BlackFormatter | YapfFormatter, + formattedContents: string, + fileToFormat: string, + outputFileName: string + ) { + const textDocument = await workspace.openTextDocument(fileToFormat); + const textEditor = await window.showTextDocument(textDocument); + const options = { + insertSpaces: textEditor.options.insertSpaces! as boolean, + tabSize: textEditor.options.tabSize! as number + }; + + await injectFormatOutput(outputFileName); + + const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); + await textEditor.edit((editBuilder) => { + edits.forEach((edit) => editBuilder.replace(edit.range, edit.newText)); + }); + compareFiles(formattedContents, textEditor.document.getText()); + } + + test('AutoPep8', async function () { + // https://github.com/microsoft/vscode-python/issues/12564 + // tslint:disable-next-line: no-invalid-this + return this.skip(); + await testFormatting( + new AutoPep8Formatter(ioc.serviceContainer), + formattedAutoPep8, + autoPep8FileToFormat, + 'autopep8.output' + ); + }); + // tslint:disable-next-line:no-function-expression + test('Black', async function () { + // https://github.com/microsoft/vscode-python/issues/12564 + // tslint:disable-next-line: no-invalid-this + return this.skip(); + if (!(await formattingTestIsBlackSupported())) { + // Skip for versions of python below 3.6, as Black doesn't support them at all. + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + await testFormatting( + new BlackFormatter(ioc.serviceContainer), + formattedBlack, + blackFileToFormat, + 'black.output' + ); + }); + test('Yapf', async () => + testFormatting(new YapfFormatter(ioc.serviceContainer), formattedYapf, yapfFileToFormat, 'yapf.output')); + + test('Yapf on dirty file', async () => { + const sourceDir = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); + const targetDir = path.join(__dirname, '..', 'pythonFiles', 'formatting'); + + const originalName = 'formatWhenDirty.py'; + const resultsName = 'formatWhenDirtyResult.py'; + const fileToFormat = path.join(targetDir, originalName); + const formattedFile = path.join(targetDir, resultsName); + + if (!fs.pathExistsSync(targetDir)) { + fs.mkdirpSync(targetDir); + } + fs.copySync(path.join(sourceDir, originalName), fileToFormat, { overwrite: true }); + fs.copySync(path.join(sourceDir, resultsName), formattedFile, { overwrite: true }); + + const textDocument = await workspace.openTextDocument(fileToFormat); + const textEditor = await window.showTextDocument(textDocument); + await textEditor.edit((builder) => { + // Make file dirty. Trailing blanks will be removed. + builder.insert(new Position(0, 0), '\n \n'); + }); + + const dir = path.dirname(fileToFormat); + const configFile = path.join(dir, '.style.yapf'); + try { + // Create yapf configuration file + const content = '[style]\nbased_on_style = pep8\nindent_width=5\n'; + fs.writeFileSync(configFile, content); + + const options = { insertSpaces: textEditor.options.insertSpaces! as boolean, tabSize: 1 }; + const formatter = new YapfFormatter(ioc.serviceContainer); + const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); + await textEditor.edit((editBuilder) => { + edits.forEach((edit) => editBuilder.replace(edit.range, edit.newText)); + }); + + const expected = fs.readFileSync(formattedFile).toString(); + const actual = textEditor.document.getText(); + compareFiles(expected, actual); + } finally { + if (fs.existsSync(configFile)) { + fs.unlinkSync(configFile); + } + } + }); +}); diff --git a/src/test/format/extension.lineFormatter.test.ts b/src/test/format/extension.lineFormatter.test.ts new file mode 100644 index 000000000000..37c6b33edc93 --- /dev/null +++ b/src/test/format/extension.lineFormatter.test.ts @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Position, Range, TextDocument, TextLine } from 'vscode'; +import '../../client/common/extensions'; +import { LineFormatter } from '../../client/formatters/lineFormatter'; + +const formatFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); +const grammarFile = path.join(formatFilesPath, 'pythonGrammar.py'); + +// https://www.python.org/dev/peps/pep-0008/#code-lay-out +// tslint:disable-next-line:max-func-body-length +suite('Formatting - line formatter', () => { + const formatter = new LineFormatter(); + + test('Operator spacing', () => { + testFormatLine('( x +1 )*y/ 3', '(x + 1) * y / 3'); + }); + test('Braces spacing', () => { + testFormatMultiline('foo =(0 ,)', 0, 'foo = (0,)'); + }); + test('Colon regular', () => { + testFormatMultiline('if x == 4 : print x,y; x,y= y, x', 0, 'if x == 4: print x, y; x, y = y, x'); + }); + test('Colon slices', () => { + testFormatLine('x[1: 30]', 'x[1:30]'); + }); + test('Colon slices in arguments', () => { + testFormatLine('spam ( ham[ 1 :3], {eggs : 2})', 'spam(ham[1:3], {eggs: 2})'); + }); + test('Colon slices with double colon', () => { + testFormatLine( + 'ham [1:9 ], ham[ 1: 9: 3], ham[: 9 :3], ham[1: :3], ham [ 1: 9:]', + 'ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]' + ); + }); + test('Colon slices with operators', () => { + testFormatLine('ham [lower+ offset :upper+offset]', 'ham[lower + offset:upper + offset]'); + }); + test('Colon slices with functions', () => { + testFormatLine( + 'ham[ : upper_fn ( x) : step_fn(x )], ham[ :: step_fn(x)]', + 'ham[:upper_fn(x):step_fn(x)], ham[::step_fn(x)]' + ); + }); + test('Colon in for loop', () => { + testFormatLine('for index in range( len(fruits) ): ', 'for index in range(len(fruits)):'); + }); + test('Nested braces', () => { + testFormatLine('[ 1 :[2: (x,),y]]{1}', '[1:[2:(x,), y]]{1}'); + }); + test('Trailing comment', () => { + testFormatMultiline('x=1 # comment', 0, 'x = 1 # comment'); + }); + test('Single comment', () => { + testFormatLine('# comment', '# comment'); + }); + test('Comment with leading whitespace', () => { + testFormatLine(' # comment', ' # comment'); + }); + test('Operators without following space', () => { + testFormatLine('foo( *a, ** b, ! c)', 'foo(*a, **b, !c)'); + }); + test('Brace after keyword', () => { + testFormatLine('for x in(1,2,3)', 'for x in (1, 2, 3)'); + testFormatLine('assert(1,2,3)', 'assert (1, 2, 3)'); + testFormatLine('if (True|False)and(False/True)not (! x )', 'if (True | False) and (False / True) not (!x)'); + testFormatLine('while (True|False)', 'while (True | False)'); + testFormatLine('yield(a%b)', 'yield (a % b)'); + }); + test('Dot operator', () => { + testFormatLine('x.y', 'x.y'); + testFormatLine('5 .y', '5.y'); + }); + test('Unknown tokens no space', () => { + testFormatLine('abc\\n\\', 'abc\\n\\'); + }); + test('Unknown tokens with space', () => { + testFormatLine('abc \\n \\', 'abc \\n \\'); + }); + test('Double asterisk', () => { + testFormatLine('a**2, ** k', 'a ** 2, **k'); + }); + test('Lambda', () => { + testFormatLine('lambda * args, :0', 'lambda *args,: 0'); + }); + test('Comma expression', () => { + testFormatMultiline('x=1,2,3', 0, 'x = 1, 2, 3'); + }); + test('is exression', () => { + testFormatLine('a( (False is 2) is 3)', 'a((False is 2) is 3)'); + }); + test('Function returning tuple', () => { + testFormatMultiline('x,y=f(a)', 0, 'x, y = f(a)'); + }); + test('from. import A', () => { + testFormatLine('from. import A', 'from . import A'); + }); + test('from .. import', () => { + testFormatLine('from ..import', 'from .. import'); + }); + test('from..x import', () => { + testFormatLine('from..x import', 'from ..x import'); + }); + test('Raw strings', () => { + testFormatMultiline('z=r""', 0, 'z = r""'); + testFormatMultiline('z=rf""', 0, 'z = rf""'); + testFormatMultiline('z=R""', 0, 'z = R""'); + testFormatMultiline('z=RF""', 0, 'z = RF""'); + }); + test('Binary @', () => { + testFormatLine('a@ b', 'a @ b'); + }); + test('Unary operators', () => { + testFormatMultiline('x= - y', 0, 'x = -y'); + testFormatMultiline('x= + y', 0, 'x = +y'); + testFormatMultiline('x= ~ y', 0, 'x = ~y'); + testFormatMultiline('x=-1', 0, 'x = -1'); + testFormatMultiline('x= +1', 0, 'x = +1'); + testFormatMultiline('x= ~1 ', 0, 'x = ~1'); + }); + test('Equals with type hints', () => { + testFormatMultiline('def foo(x:int=3,x=100.)', 0, 'def foo(x: int = 3, x=100.)'); + }); + test('Trailing comma', () => { + testFormatLine('a, =[1]', 'a, = [1]'); + }); + test('if()', () => { + testFormatLine('if(True) :', 'if (True):'); + }); + test('lambda arguments', () => { + testFormatMultiline( + 'l4= lambda x =lambda y =lambda z= 1: z: y(): x()', + 0, + 'l4 = lambda x=lambda y=lambda z=1: z: y(): x()' + ); + }); + test('star in multiline arguments', () => { + testFormatMultiline('x = [\n * param1,\n * param2\n]', 1, ' *param1,'); + testFormatMultiline('x = [\n * param1,\n * param2\n]', 2, ' *param2'); + }); + test('arrow operator', () => { + //testFormatMultiline('def f(a, b: 1, e: 3 = 4, f =5, * g: 6, ** k: 11) -> 12: pass', 0, 'def f(a, b: 1, e: 3 = 4, f=5, *g: 6, **k: 11) -> 12: pass'); + testFormatMultiline('def f(a, \n ** k: 11) -> 12: pass', 1, ' **k: 11) -> 12: pass'); + }); + + test('Multiline function call', () => { + testFormatMultiline('def foo(x = 1)', 0, 'def foo(x=1)'); + testFormatMultiline('def foo(a\n, x = 1)', 1, ', x=1)'); + testFormatMultiline('foo(a ,b,\n x = 1)', 1, ' x=1)'); + testFormatMultiline('if True:\n if False:\n foo(a , bar(\n x = 1)', 3, ' x=1)'); + testFormatMultiline('z=foo (0 , x= 1, (3+7) , y , z )', 0, 'z = foo(0, x=1, (3 + 7), y, z)'); + testFormatMultiline('foo (0,\n x= 1,', 1, ' x=1,'); + testFormatMultiline( + // tslint:disable-next-line:no-multiline-string + `async def fetch(): + async with aiohttp.ClientSession() as session: + async with session.ws_connect( + "http://127.0.0.1:8000/", headers = cookie) as ws: # add unwanted spaces`, + 3, + ' "http://127.0.0.1:8000/", headers=cookie) as ws: # add unwanted spaces' + ); + testFormatMultiline('def pos0key1(*, key): return key\npos0key1(key= 100)', 1, 'pos0key1(key=100)'); + testFormatMultiline( + 'def test_string_literals(self):\n x= 1; y =2; self.assertTrue(len(x) == 0 and x == y)', + 1, + ' x = 1; y = 2; self.assertTrue(len(x) == 0 and x == y)' + ); + }); + test('Grammar file', () => { + const content = fs.readFileSync(grammarFile).toString('utf8'); + const lines = content.splitLines({ trim: false, removeEmptyEntries: false }); + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + const actual = formatMultiline(content, i); + assert.equal(actual, line, `Line ${i + 1} changed: '${line.trim()}' to '${actual.trim()}'`); + } + }); + + function testFormatLine(text: string, expected: string): void { + const actual = formatLine(text); + assert.equal(actual, expected); + } + + function testFormatMultiline(content: string, lineNumber: number, expected: string): void { + const actual = formatMultiline(content, lineNumber); + assert.equal(actual, expected); + } + + function formatMultiline(content: string, lineNumber: number): string { + const lines = content.splitLines({ trim: false, removeEmptyEntries: false }); + + const document = TypeMoq.Mock.ofType<TextDocument>(); + document + .setup((x) => x.lineAt(TypeMoq.It.isAnyNumber())) + .returns((n) => { + const line = TypeMoq.Mock.ofType<TextLine>(); + line.setup((x) => x.text).returns(() => lines[n]); + line.setup((x) => x.range).returns( + () => new Range(new Position(n, 0), new Position(n, lines[n].length)) + ); + return line.object; + }); + document + .setup((x) => x.getText(TypeMoq.It.isAny())) + .returns((o) => { + const r = o as Range; + const bits: string[] = []; + + if (r.start.line === r.end.line) { + return lines[r.start.line].substring(r.start.character, r.end.character); + } + + bits.push(lines[r.start.line].substr(r.start.character)); + for (let i = r.start.line + 1; i < r.end.line; i += 1) { + bits.push(lines[i]); + } + bits.push(lines[r.end.line].substring(0, r.end.character)); + return bits.join('\n'); + }); + document + .setup((x) => x.offsetAt(TypeMoq.It.isAny())) + .returns((o) => { + const p = o as Position; + let offset = 0; + for (let i = 0; i < p.line; i += 1) { + offset += lines[i].length + 1; // Accounting for the line break + } + return offset + p.character; + }); + + return formatter.formatLine(document.object, lineNumber); + } + + function formatLine(text: string): string { + const line = TypeMoq.Mock.ofType<TextLine>(); + line.setup((x) => x.text).returns(() => text); + + const document = TypeMoq.Mock.ofType<TextDocument>(); + document.setup((x) => x.lineAt(TypeMoq.It.isAnyNumber())).returns(() => line.object); + + return formatter.formatLine(document.object, 0); + } +}); diff --git a/src/test/format/extension.onEnterFormat.test.ts b/src/test/format/extension.onEnterFormat.test.ts new file mode 100644 index 000000000000..69bf32e47366 --- /dev/null +++ b/src/test/format/extension.onEnterFormat.test.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import { CancellationTokenSource, Position, TextDocument, workspace } from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../client/constants'; +import { OnEnterFormatter } from '../../client/typeFormatters/onEnterFormatter'; +import { closeActiveWindows, initialize } from '../initialize'; + +const formatFilesPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'formatting'); +const unformattedFile = path.join(formatFilesPath, 'fileToFormatOnEnter.py'); + +suite('Formatting - OnEnter provider', () => { + let document: TextDocument; + let formatter: OnEnterFormatter; + suiteSetup(async () => { + await initialize(); + document = await workspace.openTextDocument(unformattedFile); + formatter = new OnEnterFormatter(); + }); + suiteTeardown(closeActiveWindows); + + test('Simple statement', () => testFormattingAtPosition(1, 0, 'x = 1')); + + test('No formatting inside strings (2)', () => doesNotFormat(2, 0)); + + test('No formatting inside strings (3)', () => doesNotFormat(3, 0)); + + test('Whitespace before comment', () => doesNotFormat(4, 0)); + + test('No formatting of comment', () => doesNotFormat(5, 0)); + + test('Formatting line ending in comment', () => testFormattingAtPosition(6, 0, 'x + 1 # ')); + + test('Formatting line with @', () => doesNotFormat(7, 0)); + + test('Formatting line with @', () => doesNotFormat(8, 0)); + + test('Formatting line with unknown neighboring tokens', () => testFormattingAtPosition(9, 0, 'if x <= 1:')); + + test('Formatting line with unknown neighboring tokens', () => testFormattingAtPosition(10, 0, 'if 1 <= x:')); + + test('Formatting method definition with arguments', () => + testFormattingAtPosition(11, 0, 'def __init__(self, age=23)')); + + test('Formatting space after open brace', () => testFormattingAtPosition(12, 0, 'while (1)')); + + test('Formatting line ending in string', () => testFormattingAtPosition(13, 0, 'x + """')); + + function testFormattingAtPosition(line: number, character: number, expectedFormattedString?: string): void { + const token = new CancellationTokenSource().token; + const edits = formatter.provideOnTypeFormattingEdits( + document, + new Position(line, character), + '\n', + { insertSpaces: true, tabSize: 2 }, + token + ); + expect(edits).to.be.lengthOf(1); + expect(edits[0].newText).to.be.equal(expectedFormattedString); + } + function doesNotFormat(line: number, character: number): void { + const token = new CancellationTokenSource().token; + const edits = formatter.provideOnTypeFormattingEdits( + document, + new Position(line, character), + '\n', + { insertSpaces: true, tabSize: 2 }, + token + ); + expect(edits).to.be.lengthOf(0); + } +}); diff --git a/src/test/format/extension.onTypeFormat.test.ts b/src/test/format/extension.onTypeFormat.test.ts new file mode 100644 index 000000000000..767d378db80d --- /dev/null +++ b/src/test/format/extension.onTypeFormat.test.ts @@ -0,0 +1,790 @@ +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. + +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { BlockFormatProviders } from '../../client/typeFormatters/blockFormatProvider'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; + +// tslint:disable: max-func-body-length + +const srcPythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'typeFormatFiles'); +const outPythoFilesPath = path.join(__dirname, 'pythonFiles', 'typeFormatFiles'); + +const tryBlock2OutFilePath = path.join(outPythoFilesPath, 'tryBlocks2.py'); +const tryBlock4OutFilePath = path.join(outPythoFilesPath, 'tryBlocks4.py'); +const tryBlockTabOutFilePath = path.join(outPythoFilesPath, 'tryBlocksTab.py'); + +const elseBlock2OutFilePath = path.join(outPythoFilesPath, 'elseBlocks2.py'); +const elseBlock4OutFilePath = path.join(outPythoFilesPath, 'elseBlocks4.py'); +const elseBlockTabOutFilePath = path.join(outPythoFilesPath, 'elseBlocksTab.py'); + +const elseBlockFirstLine2OutFilePath = path.join(outPythoFilesPath, 'elseBlocksFirstLine2.py'); +const elseBlockFirstLine4OutFilePath = path.join(outPythoFilesPath, 'elseBlocksFirstLine4.py'); +const elseBlockFirstLineTabOutFilePath = path.join(outPythoFilesPath, 'elseBlocksFirstLineTab.py'); + +const provider = new BlockFormatProviders(); + +function testFormatting( + fileToFormat: string, + position: vscode.Position, + expectedEdits: vscode.TextEdit[], + formatOptions: vscode.FormattingOptions +): PromiseLike<void> { + let textDocument: vscode.TextDocument; + return vscode.workspace + .openTextDocument(fileToFormat) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + return provider.provideOnTypeFormattingEdits( + textDocument, + position, + ':', + formatOptions, + new vscode.CancellationTokenSource().token + ); + }) + .then( + (edits) => { + assert.equal(edits.length, expectedEdits.length, 'Number of edits not the same'); + edits.forEach((edit, index) => { + const expectedEdit = expectedEdits[index]; + assert.equal( + edit.newText, + expectedEdit.newText, + `newText for edit is not the same for index = ${index}` + ); + const providedRange = `${edit.range.start.line},${edit.range.start.character},${edit.range.end.line},${edit.range.end.character}`; + const expectedRange = `${expectedEdit.range.start.line},${expectedEdit.range.start.character},${expectedEdit.range.end.line},${expectedEdit.range.end.character}`; + assert.ok( + edit.range.isEqual(expectedEdit.range), + `range for edit is not the same for index = ${index}, provided ${providedRange}, expected ${expectedRange}` + ); + }); + }, + (reason) => { + assert.fail(reason, undefined, 'Type Formatting failed', ''); + } + ); +} + +suite('Else block with if in first line of file', () => { + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['elseBlocksFirstLine2.py', 'elseBlocksFirstLine4.py', 'elseBlocksFirstLineTab.py'].forEach((file) => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { + fs.unlinkSync(targetFile); + } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); + }); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + interface ITestCase { + title: string; + line: number; + column: number; + expectedEdits: vscode.TextEdit[]; + formatOptions: vscode.FormattingOptions; + filePath: string; + } + const testCases: ITestCase[] = [ + { + title: 'else block with 2 spaces', + line: 3, + column: 7, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(3, 0, 3, 2))], + formatOptions: { insertSpaces: true, tabSize: 2 }, + filePath: elseBlockFirstLine2OutFilePath + }, + { + title: 'else block with 4 spaces', + line: 3, + column: 9, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(3, 0, 3, 4))], + formatOptions: { insertSpaces: true, tabSize: 4 }, + filePath: elseBlockFirstLine4OutFilePath + }, + { + title: 'else block with Tab', + line: 3, + column: 6, + expectedEdits: [ + vscode.TextEdit.delete(new vscode.Range(3, 0, 3, 1)), + vscode.TextEdit.insert(new vscode.Position(3, 0), '') + ], + formatOptions: { insertSpaces: false, tabSize: 4 }, + filePath: elseBlockFirstLineTabOutFilePath + } + ]; + + testCases.forEach((testCase, index) => { + test(`${index + 1}. ${testCase.title}`, (done) => { + const pos = new vscode.Position(testCase.line, testCase.column); + testFormatting(testCase.filePath, pos, testCase.expectedEdits, testCase.formatOptions).then(done, done); + }); + }); +}); + +suite('Try blocks with indentation of 2 spaces', () => { + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['tryBlocks2.py'].forEach((file) => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { + fs.unlinkSync(targetFile); + } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); + }); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + interface ITestCase { + title: string; + line: number; + column: number; + expectedEdits: vscode.TextEdit[]; + } + const testCases: ITestCase[] = [ + { + title: 'except off by tab', + line: 6, + column: 22, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(6, 0, 6, 2))] + }, + { + title: 'except off by one should not be formatted', + line: 15, + column: 21, + expectedEdits: [] + }, + { + title: 'except off by tab inside a for loop', + line: 35, + column: 13, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(35, 0, 35, 2))] + }, + { + title: 'except off by one inside a for loop should not be formatted', + line: 47, + column: 12, + expectedEdits: [] + }, + { + title: 'except IOError: off by tab inside a for loop', + line: 54, + column: 19, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(54, 0, 54, 2))] + }, + { + title: 'else: off by tab inside a for loop', + line: 76, + column: 9, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(76, 0, 76, 2))] + }, + { + title: 'except ValueError:: off by tab inside a function', + line: 143, + column: 22, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(143, 0, 143, 2))] + }, + { + title: 'except ValueError as err: off by one inside a function should not be formatted', + line: 157, + column: 25, + expectedEdits: [] + }, + { + title: 'else: off by tab inside function', + line: 172, + column: 11, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(172, 0, 172, 2))] + }, + { + title: 'finally: off by tab inside function', + line: 195, + column: 12, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(195, 0, 195, 2))] + } + ]; + + const formatOptions: vscode.FormattingOptions = { + insertSpaces: true, + tabSize: 2 + }; + + testCases.forEach((testCase, index) => { + test(`${index + 1}. ${testCase.title}`, (done) => { + const pos = new vscode.Position(testCase.line, testCase.column); + testFormatting(tryBlock2OutFilePath, pos, testCase.expectedEdits, formatOptions).then(done, done); + }); + }); +}); + +suite('Try blocks with indentation of 4 spaces', () => { + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['tryBlocks4.py'].forEach((file) => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { + fs.unlinkSync(targetFile); + } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); + }); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + interface ITestCase { + title: string; + line: number; + column: number; + expectedEdits: vscode.TextEdit[]; + } + const testCases: ITestCase[] = [ + { + title: 'except off by tab', + line: 6, + column: 22, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(6, 0, 6, 4))] + }, + { + title: 'except off by one should not be formatted', + line: 15, + column: 21, + expectedEdits: [] + }, + { + title: 'except off by tab inside a for loop', + line: 35, + column: 13, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(35, 0, 35, 4))] + }, + { + title: 'except off by one inside a for loop should not be formatted', + line: 47, + column: 12, + expectedEdits: [] + }, + { + title: 'except IOError: off by tab inside a for loop', + line: 54, + column: 19, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(54, 0, 54, 4))] + }, + { + title: 'else: off by tab inside a for loop', + line: 76, + column: 9, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(76, 0, 76, 4))] + }, + { + title: 'except ValueError:: off by tab inside a function', + line: 143, + column: 22, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(143, 0, 143, 4))] + }, + { + title: 'except ValueError as err: off by one inside a function should not be formatted', + line: 157, + column: 25, + expectedEdits: [] + }, + { + title: 'else: off by tab inside function', + line: 172, + column: 11, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(172, 0, 172, 4))] + }, + { + title: 'finally: off by tab inside function', + line: 195, + column: 12, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(195, 0, 195, 4))] + } + ]; + + const formatOptions: vscode.FormattingOptions = { + insertSpaces: true, + tabSize: 4 + }; + + testCases.forEach((testCase, index) => { + test(`${index + 1}. ${testCase.title}`, (done) => { + const pos = new vscode.Position(testCase.line, testCase.column); + testFormatting(tryBlock4OutFilePath, pos, testCase.expectedEdits, formatOptions).then(done, done); + }); + }); +}); + +suite('Try blocks with indentation of Tab', () => { + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['tryBlocksTab.py'].forEach((file) => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { + fs.unlinkSync(targetFile); + } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); + }); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + interface ITestCase { + title: string; + line: number; + column: number; + expectedEdits: vscode.TextEdit[]; + } + const TAB = ' '; + const testCases: ITestCase[] = [ + { + title: 'except off by tab', + line: 6, + column: 22, + expectedEdits: [ + vscode.TextEdit.delete(new vscode.Range(6, 0, 6, 2)), + vscode.TextEdit.insert(new vscode.Position(6, 0), TAB) + ] + }, + { + title: 'except off by tab inside a for loop', + line: 35, + column: 13, + expectedEdits: [ + vscode.TextEdit.delete(new vscode.Range(35, 0, 35, 2)), + vscode.TextEdit.insert(new vscode.Position(35, 0), TAB) + ] + }, + { + title: 'except IOError: off by tab inside a for loop', + line: 54, + column: 19, + expectedEdits: [ + vscode.TextEdit.delete(new vscode.Range(54, 0, 54, 2)), + vscode.TextEdit.insert(new vscode.Position(54, 0), TAB) + ] + }, + { + title: 'else: off by tab inside a for loop', + line: 76, + column: 9, + expectedEdits: [ + vscode.TextEdit.delete(new vscode.Range(76, 0, 76, 2)), + vscode.TextEdit.insert(new vscode.Position(76, 0), TAB) + ] + }, + { + title: 'except ValueError:: off by tab inside a function', + line: 143, + column: 22, + expectedEdits: [ + vscode.TextEdit.delete(new vscode.Range(143, 0, 143, 2)), + vscode.TextEdit.insert(new vscode.Position(143, 0), TAB) + ] + }, + { + title: 'else: off by tab inside function', + line: 172, + column: 11, + expectedEdits: [ + vscode.TextEdit.delete(new vscode.Range(172, 0, 172, 3)), + vscode.TextEdit.insert(new vscode.Position(172, 0), TAB + TAB) + ] + }, + { + title: 'finally: off by tab inside function', + line: 195, + column: 12, + expectedEdits: [ + vscode.TextEdit.delete(new vscode.Range(195, 0, 195, 2)), + vscode.TextEdit.insert(new vscode.Position(195, 0), TAB) + ] + } + ]; + + const formatOptions: vscode.FormattingOptions = { + insertSpaces: false, + tabSize: 4 + }; + + testCases.forEach((testCase, index) => { + test(`${index + 1}. ${testCase.title}`, (done) => { + const pos = new vscode.Position(testCase.line, testCase.column); + testFormatting(tryBlockTabOutFilePath, pos, testCase.expectedEdits, formatOptions).then(done, done); + }); + }); +}); + +suite('Else blocks with indentation of 2 spaces', () => { + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['elseBlocks2.py'].forEach((file) => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { + fs.unlinkSync(targetFile); + } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); + }); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + // tslint:disable-next-line:interface-name + interface TestCase { + title: string; + line: number; + column: number; + expectedEdits: vscode.TextEdit[]; + } + const testCases: TestCase[] = [ + { + title: 'elif off by tab', + line: 4, + column: 18, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(4, 0, 4, 2))] + }, + { + title: 'elif off by tab', + line: 7, + column: 18, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(7, 0, 7, 2))] + }, + { + title: 'elif off by tab again', + line: 21, + column: 18, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(21, 0, 21, 2))] + }, + { + title: 'else off by tab', + line: 38, + column: 7, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(38, 0, 38, 2))] + }, + { + title: 'else: off by tab inside a for loop', + line: 47, + column: 13, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(47, 0, 47, 2))] + }, + { + title: 'else: off by tab inside a try', + line: 57, + column: 9, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(57, 0, 57, 2))] + }, + { + title: 'elif off by a tab inside a function', + line: 66, + column: 20, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(66, 0, 66, 2))] + }, + { + title: 'elif off by a tab inside a function should not format', + line: 69, + column: 20, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(69, 0, 69, 2))] + }, + { + title: 'elif off by a tab inside a function', + line: 83, + column: 20, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(83, 0, 83, 2))] + }, + { + title: 'else: off by tab inside if of a for and for in a function', + line: 109, + column: 15, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(109, 0, 109, 2))] + }, + { + title: 'else: off by tab inside try in a function', + line: 119, + column: 11, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(119, 0, 119, 2))] + }, + { + title: 'else: off by tab inside while in a function', + line: 134, + column: 9, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(134, 0, 134, 2))] + }, + { + title: 'elif: off by tab inside if but inline with elif', + line: 345, + column: 18, + expectedEdits: [] + }, + { + title: 'elif: off by tab inside if but inline with if', + line: 359, + column: 18, + expectedEdits: [] + } + ]; + + const formatOptions: vscode.FormattingOptions = { + insertSpaces: true, + tabSize: 2 + }; + + testCases.forEach((testCase, index) => { + test(`${index + 1}. ${testCase.title}`, (done) => { + const pos = new vscode.Position(testCase.line, testCase.column); + testFormatting(elseBlock2OutFilePath, pos, testCase.expectedEdits, formatOptions).then(done, done); + }); + }); +}); + +suite('Else blocks with indentation of 4 spaces', () => { + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['elseBlocks4.py'].forEach((file) => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { + fs.unlinkSync(targetFile); + } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); + }); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + interface ITestCase { + title: string; + line: number; + column: number; + expectedEdits: vscode.TextEdit[]; + } + const testCases: ITestCase[] = [ + { + title: 'elif off by tab', + line: 4, + column: 18, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(4, 0, 4, 4))] + }, + { + title: 'elif off by tab', + line: 7, + column: 18, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(7, 0, 7, 4))] + }, + { + title: 'elif off by tab again', + line: 21, + column: 18, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(21, 0, 21, 4))] + }, + { + title: 'else off by tab', + line: 38, + column: 7, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(38, 0, 38, 4))] + }, + { + title: 'else: off by tab inside a for loop', + line: 47, + column: 13, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(47, 0, 47, 4))] + }, + { + title: 'else: off by tab inside a try', + line: 57, + column: 9, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(57, 0, 57, 4))] + }, + { + title: 'elif off by a tab inside a function', + line: 66, + column: 20, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(66, 0, 66, 4))] + }, + { + title: 'elif off by a tab inside a function should not format', + line: 69, + column: 20, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(69, 0, 69, 4))] + }, + { + title: 'elif off by a tab inside a function', + line: 83, + column: 20, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(83, 0, 83, 4))] + }, + { + title: 'else: off by tab inside if of a for and for in a function', + line: 109, + column: 15, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(109, 0, 109, 4))] + }, + { + title: 'else: off by tab inside try in a function', + line: 119, + column: 11, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(119, 0, 119, 4))] + }, + { + title: 'else: off by tab inside while in a function', + line: 134, + column: 9, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(134, 0, 134, 4))] + }, + { + title: 'elif: off by tab inside if but inline with elif', + line: 345, + column: 18, + expectedEdits: [] + } + ]; + + const formatOptions: vscode.FormattingOptions = { + insertSpaces: true, + tabSize: 2 + }; + + testCases.forEach((testCase, index) => { + test(`${index + 1}. ${testCase.title}`, (done) => { + const pos = new vscode.Position(testCase.line, testCase.column); + testFormatting(elseBlock4OutFilePath, pos, testCase.expectedEdits, formatOptions).then(done, done); + }); + }); +}); + +suite('Else blocks with indentation of Tab', () => { + suiteSetup(async () => { + await initialize(); + fs.ensureDirSync(path.dirname(outPythoFilesPath)); + + ['elseBlocksTab.py'].forEach((file) => { + const targetFile = path.join(outPythoFilesPath, file); + if (fs.existsSync(targetFile)) { + fs.unlinkSync(targetFile); + } + fs.copySync(path.join(srcPythoFilesPath, file), targetFile); + }); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + interface ITestCase { + title: string; + line: number; + column: number; + expectedEdits: vscode.TextEdit[]; + } + const testCases: ITestCase[] = [ + { + title: 'elif off by tab', + line: 4, + column: 18, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(4, 0, 4, 1))] + }, + { + title: 'elif off by tab', + line: 7, + column: 18, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(7, 0, 7, 1))] + }, + { + title: 'elif off by tab again', + line: 21, + column: 18, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(21, 0, 21, 1))] + }, + { + title: 'else off by tab', + line: 38, + column: 7, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(38, 0, 38, 1))] + }, + { + title: 'else: off by tab inside a for loop', + line: 47, + column: 13, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(47, 0, 47, 1))] + }, + { + title: 'else: off by tab inside a try', + line: 57, + column: 9, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(57, 0, 57, 1))] + }, + { + title: 'elif off by a tab inside a function', + line: 66, + column: 20, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(66, 0, 66, 1))] + }, + { + title: 'elif off by a tab inside a function should not format', + line: 69, + column: 20, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(69, 0, 69, 1))] + }, + { + title: 'elif off by a tab inside a function', + line: 83, + column: 20, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(83, 0, 83, 1))] + }, + { + title: 'else: off by tab inside if of a for and for in a function', + line: 109, + column: 15, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(109, 0, 109, 1))] + }, + { + title: 'else: off by tab inside try in a function', + line: 119, + column: 11, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(119, 0, 119, 1))] + }, + { + title: 'else: off by tab inside while in a function', + line: 134, + column: 9, + expectedEdits: [vscode.TextEdit.delete(new vscode.Range(134, 0, 134, 1))] + }, + { + title: 'elif: off by tab inside if but inline with elif', + line: 345, + column: 18, + expectedEdits: [] + } + ]; + + const formatOptions: vscode.FormattingOptions = { + insertSpaces: true, + tabSize: 2 + }; + + testCases.forEach((testCase, index) => { + test(`${index + 1}. ${testCase.title}`, (done) => { + const pos = new vscode.Position(testCase.line, testCase.column); + testFormatting(elseBlockTabOutFilePath, pos, testCase.expectedEdits, formatOptions).then(done, done); + }); + }); +}); diff --git a/src/test/format/extension.sort.test.ts b/src/test/format/extension.sort.test.ts new file mode 100644 index 000000000000..e05b4e7a4365 --- /dev/null +++ b/src/test/format/extension.sort.test.ts @@ -0,0 +1,187 @@ +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as fs from 'fs'; +import { EOL } from 'os'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import { commands, ConfigurationTarget, Position, Range, Uri, window, workspace } from 'vscode'; +import { Commands } from '../../client/common/constants'; +import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; +import { InterpreterService } from '../../client/interpreter/interpreterService'; +import { SortImportsEditingProvider } from '../../client/providers/importSortProvider'; +import { ISortImportsEditingProvider } from '../../client/providers/types'; +import { CondaService } from '../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { updateSetting } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_TIMEOUT } from '../initialize'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; + +const sortingPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'sorting'); +const fileToFormatWithoutConfig = path.join(sortingPath, 'noconfig', 'before.py'); +const originalFileToFormatWithoutConfig = path.join(sortingPath, 'noconfig', 'original.py'); +const fileToFormatWithConfig = path.join(sortingPath, 'withconfig', 'before.py'); +const originalFileToFormatWithConfig = path.join(sortingPath, 'withconfig', 'original.py'); +const fileToFormatWithConfig1 = path.join(sortingPath, 'withconfig', 'before.1.py'); +const originalFileToFormatWithConfig1 = path.join(sortingPath, 'withconfig', 'original.1.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Sorting', () => { + let ioc: UnitTestIocContainer; + let sorter: ISortImportsEditingProvider; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(async function () { + const pythonVersion = process.env.CI_PYTHON_VERSION // GHA uses this + ? parseFloat(process.env.CI_PYTHON_VERSION) + : process.env.PythonVersion // Azdo uses this + ? parseFloat(process.env.PythonVersion) + : undefined; + if (pythonVersion && pythonVersion < 3) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + await initialize(); + }); + suiteTeardown(async () => { + fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); + fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); + fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); + await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); + await closeActiveWindows(); + }); + setup(async function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(TEST_TIMEOUT * 2); + await initializeTest(); + initializeDI(); + fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); + fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); + fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); + await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); + await closeActiveWindows(); + sorter = new SortImportsEditingProvider(ioc.serviceContainer); + }); + teardown(async () => { + await ioc.dispose(); + await closeActiveWindows(); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + ioc.registerInterpreterStorageTypes(); + ioc.registerMockInterpreterTypes(); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, instance(mock(CondaService))); + ioc.serviceManager.rebindInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); + } + test('Without Config', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithoutConfig); + await window.showTextDocument(textDocument); + const edit = (await sorter.provideDocumentSortImportsEdits(textDocument.uri))!; + expect(edit.entries()).to.be.lengthOf(1); + const edits = edit.entries()[0][1]; + expect(edits.length).to.equal(4); + assert.equal( + edits.filter((value) => value.newText === EOL && value.range.isEqual(new Range(2, 0, 2, 0))).length, + 1, + 'EOL not found' + ); + assert.equal( + edits.filter((value) => value.newText === '' && value.range.isEqual(new Range(3, 0, 4, 0))).length, + 1, + '"" not found' + ); + assert.equal( + edits.filter( + (value) => + value.newText === `from rope.refactor.extract import ExtractMethod, ExtractVariable${EOL}` && + value.range.isEqual(new Range(15, 0, 15, 0)) + ).length, + 1, + 'Text not found' + ); + assert.equal( + edits.filter((value) => value.newText === '' && value.range.isEqual(new Range(16, 0, 18, 0))).length, + 1, + '"" not found' + ); + }); + + test('Without Config (via Command)', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithoutConfig); + const originalContent = textDocument.getText(); + await window.showTextDocument(textDocument); + await commands.executeCommand(Commands.Sort_Imports); + assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); + }); + + test('With Config', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + await window.showTextDocument(textDocument); + const edit = (await sorter.provideDocumentSortImportsEdits(textDocument.uri))!; + expect(edit).not.to.eq(undefined, 'No edit returned'); + expect(edit.entries()).to.be.lengthOf(1); + const edits = edit.entries()[0][1]; + const newValue = `from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`; + assert.equal( + edits.filter((value) => value.newText === newValue && value.range.isEqual(new Range(0, 0, 3, 0))).length, + 1, + 'New Text not found' + ); + }); + + test('With Config (via Command)', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + const originalContent = textDocument.getText(); + await window.showTextDocument(textDocument); + await commands.executeCommand(Commands.Sort_Imports); + assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); + }); + + test('With Changes and Config in Args', async () => { + await updateSetting( + 'sortImports.args', + ['--sp', path.join(sortingPath, 'withconfig')], + Uri.file(sortingPath), + ConfigurationTarget.Workspace + ); + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + const editor = await window.showTextDocument(textDocument); + await editor.edit((builder) => { + builder.insert(new Position(0, 0), `from third_party import lib0${EOL}`); + }); + const edit = (await sorter.provideDocumentSortImportsEdits(textDocument.uri))!; + expect(edit.entries()).to.be.lengthOf(1); + const edits = edit.entries()[0][1]; + assert.notEqual(edits.length, 0, 'No edits'); + }); + test('With Changes and Config in Args (via Command)', async () => { + await updateSetting( + 'sortImports.args', + ['--sp', path.join(sortingPath, 'withconfig')], + Uri.file(sortingPath), + configTarget + ); + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + const editor = await window.showTextDocument(textDocument); + await editor.edit((builder) => { + builder.insert(new Position(0, 0), `from third_party import lib0${EOL}`); + }); + const originalContent = textDocument.getText(); + await commands.executeCommand(Commands.Sort_Imports); + assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); + }).timeout(TEST_TIMEOUT * 2); + + test('With Changes and Config implicit from cwd', async () => { + const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); + assert.equal(textDocument.isDirty, false, 'Document should initially be unmodified'); + const editor = await window.showTextDocument(textDocument); + await editor.edit((builder) => { + builder.insert(new Position(0, 0), `from third_party import lib0${EOL}`); + }); + assert.equal(textDocument.isDirty, true, 'Document should have been modified (pre sort)'); + await sorter.sortImports(textDocument.uri); + assert.equal(textDocument.isDirty, true, 'Document should have been modified by sorting'); + const newValue = `from third_party import lib0${EOL}from third_party import lib1${EOL}from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`; + assert.equal(textDocument.getText(), newValue); + }); +}); diff --git a/src/test/format/format.helper.test.ts b/src/test/format/format.helper.test.ts new file mode 100644 index 000000000000..9ec60c994cb8 --- /dev/null +++ b/src/test/format/format.helper.test.ts @@ -0,0 +1,114 @@ +import * as assert from 'assert'; +import * as TypeMoq from 'typemoq'; +import { IConfigurationService, IFormattingSettings, Product } from '../../client/common/types'; +import * as EnumEx from '../../client/common/utils/enum'; +import { FormatterHelper } from '../../client/formatters/helper'; +import { FormatterId } from '../../client/formatters/types'; +import { getExtensionSettings } from '../common'; +import { initialize } from '../initialize'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; + +// tslint:disable-next-line:max-func-body-length +suite('Formatting - Helper', () => { + let ioc: UnitTestIocContainer; + let formatHelper: FormatterHelper; + + suiteSetup(initialize); + setup(() => { + ioc = new UnitTestIocContainer(); + + const config = TypeMoq.Mock.ofType<IConfigurationService>(); + config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => getExtensionSettings(undefined)); + + ioc.serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, config.object); + formatHelper = new FormatterHelper(ioc.serviceManager); + }); + + test('Ensure product is set in Execution Info', async () => { + [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { + const info = formatHelper.getExecutionInfo(formatter, []); + assert.equal(info.product, formatter, `Incorrect products for ${formatHelper.translateToId(formatter)}`); + }); + }); + + test('Ensure executable is set in Execution Info', async () => { + const settings = getExtensionSettings(undefined); + + [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { + const info = formatHelper.getExecutionInfo(formatter, []); + const names = formatHelper.getSettingsPropertyNames(formatter); + const execPath = settings.formatting[names.pathName] as string; + + assert.equal( + info.execPath, + execPath, + `Incorrect executable paths for product ${formatHelper.translateToId(formatter)}` + ); + }); + }); + + test('Ensure arguments are set in Execution Info', async () => { + const settings = getExtensionSettings(undefined); + const customArgs = ['1', '2', '3']; + + [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { + const names = formatHelper.getSettingsPropertyNames(formatter); + const args: string[] = Array.isArray(settings.formatting[names.argsName]) + ? (settings.formatting[names.argsName] as string[]) + : []; + const expectedArgs = args.concat(customArgs).join(','); + + assert.equal( + expectedArgs.endsWith(customArgs.join(',')), + true, + `Incorrect custom arguments for product ${formatHelper.translateToId(formatter)}` + ); + }); + }); + + test('Ensure correct setting names are returned', async () => { + [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { + const translatedId = formatHelper.translateToId(formatter)!; + const settings = { + argsName: `${translatedId}Args` as keyof IFormattingSettings, + pathName: `${translatedId}Path` as keyof IFormattingSettings + }; + + assert.deepEqual( + formatHelper.getSettingsPropertyNames(formatter), + settings, + `Incorrect settings for product ${formatHelper.translateToId(formatter)}` + ); + }); + }); + + test('Ensure translation of ids works', async () => { + const formatterMapping = new Map<Product, FormatterId>(); + formatterMapping.set(Product.autopep8, 'autopep8'); + formatterMapping.set(Product.black, 'black'); + formatterMapping.set(Product.yapf, 'yapf'); + + [Product.autopep8, Product.black, Product.yapf].forEach((formatter) => { + const translatedId = formatHelper.translateToId(formatter); + assert.equal( + translatedId, + formatterMapping.get(formatter)!, + `Incorrect translation for product ${formatHelper.translateToId(formatter)}` + ); + }); + }); + + EnumEx.getValues<Product>(Product).forEach((product) => { + const formatterMapping = new Map<Product, FormatterId>(); + formatterMapping.set(Product.autopep8, 'autopep8'); + formatterMapping.set(Product.black, 'black'); + formatterMapping.set(Product.yapf, 'yapf'); + if (formatterMapping.has(product)) { + return; + } + + test(`Ensure translation of ids throws exceptions for unknown formatters (${product})`, async () => { + assert.throws(() => formatHelper.translateToId(product)); + }); + }); +}); diff --git a/src/test/format/formatter.unit.test.ts b/src/test/format/formatter.unit.test.ts new file mode 100644 index 000000000000..679b0d06f093 --- /dev/null +++ b/src/test/format/formatter.unit.test.ts @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import { anything, capture, instance, mock, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { CancellationTokenSource, FormattingOptions, TextDocument, Uri } from 'vscode'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { PythonSettings } from '../../client/common/configSettings'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; +import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; +import { IPythonToolExecutionService } from '../../client/common/process/types'; +import { + ExecutionInfo, + IConfigurationService, + IDisposableRegistry, + IFormattingSettings, + IOutputChannel, + IPythonSettings +} from '../../client/common/types'; +import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; +import { BaseFormatter } from '../../client/formatters/baseFormatter'; +import { BlackFormatter } from '../../client/formatters/blackFormatter'; +import { FormatterHelper } from '../../client/formatters/helper'; +import { IFormatterHelper } from '../../client/formatters/types'; +import { YapfFormatter } from '../../client/formatters/yapfFormatter'; +import { ServiceContainer } from '../../client/ioc/container'; +import { IServiceContainer } from '../../client/ioc/types'; +import { noop } from '../core'; +import { MockOutputChannel } from '../mockClasses'; + +// tslint:disable-next-line: max-func-body-length +suite('Formatting - Test Arguments', () => { + let container: IServiceContainer; + let outputChannel: IOutputChannel; + let workspace: IWorkspaceService; + let settings: IPythonSettings; + const workspaceUri = Uri.file(__dirname); + let document: typemoq.IMock<TextDocument>; + const docUri = Uri.file(__filename); + let pythonToolExecutionService: IPythonToolExecutionService; + const options: FormattingOptions = { insertSpaces: false, tabSize: 1 }; + const formattingSettingsWithPath: IFormattingSettings = { + autopep8Args: ['1', '2'], + autopep8Path: path.join('a', 'exe'), + blackArgs: ['1', '2'], + blackPath: path.join('a', 'exe'), + provider: '', + yapfArgs: ['1', '2'], + yapfPath: path.join('a', 'exe') + }; + + const formattingSettingsWithModuleName: IFormattingSettings = { + autopep8Args: ['1', '2'], + autopep8Path: 'module_name', + blackArgs: ['1', '2'], + blackPath: 'module_name', + provider: '', + yapfArgs: ['1', '2'], + yapfPath: 'module_name' + }; + + setup(() => { + container = mock(ServiceContainer); + outputChannel = mock(MockOutputChannel); + workspace = mock(WorkspaceService); + settings = mock(PythonSettings); + document = typemoq.Mock.ofType<TextDocument>(); + document.setup((doc) => doc.getText(typemoq.It.isAny())).returns(() => ''); + document.setup((doc) => doc.isDirty).returns(() => false); + document.setup((doc) => doc.fileName).returns(() => docUri.fsPath); + document.setup((doc) => doc.uri).returns(() => docUri); + pythonToolExecutionService = mock(PythonToolExecutionService); + + const configService = mock(ConfigurationService); + const formatterHelper = new FormatterHelper(instance(container)); + + const appShell = mock(ApplicationShell); + when(appShell.setStatusBarMessage(anything(), anything())).thenReturn({ dispose: noop }); + + when(configService.getSettings(anything())).thenReturn(instance(settings)); + when(workspace.getWorkspaceFolder(anything())).thenReturn({ name: '', index: 0, uri: workspaceUri }); + when(container.get<IOutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL)).thenReturn( + instance(outputChannel) + ); + when(container.get<IApplicationShell>(IApplicationShell)).thenReturn(instance(appShell)); + when(container.get<IFormatterHelper>(IFormatterHelper)).thenReturn(formatterHelper); + when(container.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspace)); + when(container.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(configService)); + when(container.get<IPythonToolExecutionService>(IPythonToolExecutionService)).thenReturn( + instance(pythonToolExecutionService) + ); + when(container.get<IDisposableRegistry>(IDisposableRegistry)).thenReturn([]); + }); + + async function setupFormatter( + formatter: BaseFormatter, + formattingSettings: IFormattingSettings + ): Promise<ExecutionInfo> { + const token = new CancellationTokenSource().token; + when(settings.formatting).thenReturn(formattingSettings); + when(pythonToolExecutionService.exec(anything(), anything(), anything())).thenResolve({ stdout: '' }); + + await formatter.formatDocument(document.object, options, token); + + const args = capture(pythonToolExecutionService.exec).first(); + return args[0]; + } + test('Ensure blackPath and args used to launch the formatter', async () => { + const formatter = new BlackFormatter(instance(container)); + + const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); + + assert.equal(execInfo.execPath, formattingSettingsWithPath.blackPath); + assert.equal(execInfo.moduleName, undefined); + assert.deepEqual( + execInfo.args, + formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath]) + ); + }); + test('Ensure black modulename and args used to launch the formatter', async () => { + const formatter = new BlackFormatter(instance(container)); + + const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); + + assert.equal(execInfo.execPath, formattingSettingsWithModuleName.blackPath); + assert.equal(execInfo.moduleName, formattingSettingsWithModuleName.blackPath); + assert.deepEqual( + execInfo.args, + formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath]) + ); + }); + test('Ensure autopep8path and args used to launch the formatter', async () => { + const formatter = new AutoPep8Formatter(instance(container)); + + const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); + + assert.equal(execInfo.execPath, formattingSettingsWithPath.autopep8Path); + assert.equal(execInfo.moduleName, undefined); + assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); + }); + test('Ensure autpep8 modulename and args used to launch the formatter', async () => { + const formatter = new AutoPep8Formatter(instance(container)); + + const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); + + assert.equal(execInfo.execPath, formattingSettingsWithModuleName.autopep8Path); + assert.equal(execInfo.moduleName, formattingSettingsWithModuleName.autopep8Path); + assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); + }); + test('Ensure yapfpath and args used to launch the formatter', async () => { + const formatter = new YapfFormatter(instance(container)); + + const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); + + assert.equal(execInfo.execPath, formattingSettingsWithPath.yapfPath); + assert.equal(execInfo.moduleName, undefined); + assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); + }); + test('Ensure yapf modulename and args used to launch the formatter', async () => { + const formatter = new YapfFormatter(instance(container)); + + const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); + + assert.equal(execInfo.execPath, formattingSettingsWithModuleName.yapfPath); + assert.equal(execInfo.moduleName, formattingSettingsWithModuleName.yapfPath); + assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); + }); +}); diff --git a/src/test/index.ts b/src/test/index.ts new file mode 100644 index 000000000000..d8d5a7324540 --- /dev/null +++ b/src/test/index.ts @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-require-imports no-var-requires no-any +// Always place at the top, to ensure other modules are imported first. +require('./common/exitCIAfterTestReporter'); + +if ((Reflect as any).metadata === undefined) { + require('reflect-metadata'); +} + +import * as glob from 'glob'; +import * as Mocha from 'mocha'; +import * as path from 'path'; +import { IS_CI_SERVER_TEST_DEBUGGER, MOCHA_REPORTER_JUNIT } from './ciConstants'; +import { + IS_MULTI_ROOT_TEST, + IS_SMOKE_TEST, + MAX_EXTENSION_ACTIVATION_TIME, + TEST_RETRYCOUNT, + TEST_TIMEOUT +} from './constants'; +import { initialize } from './initialize'; +import { initializeLogger } from './testLogger'; + +initializeLogger(); + +type SetupOptions = Mocha.MochaOptions & { + testFilesSuffix: string; + reporterOptions?: { + mochaFile?: string; + properties?: string; + }; + exit: boolean; +}; + +process.on('unhandledRejection', (ex: any, _a) => { + const message = [`${ex}`]; + if (typeof ex !== 'string' && ex && ex.message) { + message.push(ex.name); + message.push(ex.message); + if (ex.stack) { + message.push(ex.stack); + } + } + // tslint:disable-next-line: no-console + console.log(`Unhandled Promise Rejection with the message ${message.join(', ')}`); +}); + +/** + * Configure the test environment and return the optoins required to run moch tests. + * + * @returns {SetupOptions} + */ +function configure(): SetupOptions { + process.env.VSC_PYTHON_CI_TEST = '1'; + process.env.IS_MULTI_ROOT_TEST = IS_MULTI_ROOT_TEST.toString(); + + // Check for a grep setting. Might be running a subset of the tests + const defaultGrep = process.env.VSC_PYTHON_CI_TEST_GREP; + // Check whether to invert the grep (i.e. test everything that doesn't include the grep). + const invert = (process.env.VSC_PYTHON_CI_TEST_INVERT_GREP || '').length > 0; + + // If running on CI server and we're running the debugger tests, then ensure we only run debug tests. + // We do this to ensure we only run debugger test, as debugger tests are very flaky on CI. + // So the solution is to run them separately and first on CI. + const grep = IS_CI_SERVER_TEST_DEBUGGER ? 'Debug' : defaultGrep; + const testFilesSuffix = process.env.TEST_FILES_SUFFIX || 'test'; + + const options: SetupOptions & { retries: number; invert: boolean } = { + ui: 'tdd', + useColors: true, + invert, + timeout: TEST_TIMEOUT, + retries: TEST_RETRYCOUNT, + grep, + testFilesSuffix, + // Force Mocha to exit after tests. + // It has been observed that this isn't sufficient, hence the reason for src/test/common/exitCIAfterTestReporter.ts + exit: true + }; + + // If the `MOCHA_REPORTER_JUNIT` env var is true, set up the CI reporter for + // reporting to both the console (spec) and to a JUnit XML file. The xml file + // written to is `test-report.xml` in the root folder by default, but can be + // changed by setting env var `MOCHA_FILE` (we do this in our CI). + if (MOCHA_REPORTER_JUNIT) { + options.reporter = 'mocha-multi-reporters'; + const reporterPath = path.join(__dirname, 'common', 'exitCIAfterTestReporter.js'); + options.reporterOptions = { + reporterEnabled: `spec,mocha-junit-reporter,${reporterPath}` + }; + } + + // Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY. + // Since we are not running in a tty environment, we just implement the method statically. + const tty = require('tty'); + if (!tty.getWindowSize) { + tty.getWindowSize = () => [80, 75]; + } + + return options; +} + +/** + * Waits until the Python Extension completes loading or a timeout. + * When running tests within VSC, we need to wait for the Python Extension to complete loading, + * this is where `initialize` comes in, we load the PVSC extension using VSC API, wait for it + * to complete. + * That's when we know out PVSC extension specific code is ready for testing. + * So, this code needs to run always for every test running in VS Code (what we call these `system test`) . + * @returns + */ +function activatePythonExtensionScript() { + const ex = new Error('Failed to initialize Python extension for tests after 3 minutes'); + let timer: NodeJS.Timer | undefined; + const failed = new Promise((_, reject) => { + timer = setTimeout(() => reject(ex), MAX_EXTENSION_ACTIVATION_TIME); + }); + const initializationPromise = initialize(); + const promise = Promise.race([initializationPromise, failed]); + // tslint:disable-next-line: no-console + promise.finally(() => clearTimeout(timer!)).catch((e) => console.error(e)); + return initializationPromise; +} + +/** + * Runner, invoked by VS Code. + * More info https://code.visualstudio.com/api/working-with-extensions/testing-extension + * + * @export + * @returns {Promise<void>} + */ +export async function run(): Promise<void> { + const options = configure(); + const mocha = new Mocha(options); + const testsRoot = path.join(__dirname); + + // Enable source map support. + require('source-map-support').install(); + + // nteract/transforms-full expects to run in the browser so we have to fake + // parts of the browser here. + if (!IS_SMOKE_TEST) { + const reactHelpers = require('./datascience/reactHelpers') as typeof import('./datascience/reactHelpers'); + reactHelpers.setUpDomEnvironment(); + } + + // Ignore `ds.test.js` test files when running other tests. + const ignoreGlob = options.testFilesSuffix.toLowerCase() === 'ds.test' ? [] : ['**/**.ds.test.js']; + const testFiles = await new Promise<string[]>((resolve, reject) => { + glob( + `**/**.${options.testFilesSuffix}.js`, + { ignore: ['**/**.unit.test.js', '**/**.functional.test.js'].concat(ignoreGlob), cwd: testsRoot }, + (error, files) => { + if (error) { + return reject(error); + } + resolve(files); + } + ); + }); + + // Setup test files that need to be run. + testFiles.forEach((file) => mocha.addFile(path.join(testsRoot, file))); + + // tslint:disable: no-console + console.time('Time taken to activate the extension'); + try { + await activatePythonExtensionScript(); + console.timeEnd('Time taken to activate the extension'); + } catch (ex) { + console.error('Failed to activate python extension without errors', ex); + } + + // Run the tests. + await new Promise<void>((resolve, reject) => { + mocha.run((failures) => { + if (failures > 0) { + return reject(new Error(`${failures} total failures`)); + } + resolve(); + }); + }); +} diff --git a/src/test/initialize.ts b/src/test/initialize.ts new file mode 100644 index 000000000000..6f19dbc4a8d1 --- /dev/null +++ b/src/test/initialize.ts @@ -0,0 +1,115 @@ +// tslint:disable:no-string-literal + +import * as path from 'path'; +import * as vscode from 'vscode'; +import type { IExtensionApi } from '../client/api'; +import { + clearPythonPathInWorkspaceFolder, + IExtensionTestApi, + PYTHON_PATH, + resetGlobalPythonPathSetting, + setPythonPathInWorkspaceRoot +} from './common'; +import { IS_SMOKE_TEST, PVSC_EXTENSION_ID_FOR_TESTS } from './constants'; +import { sleep } from './core'; + +export * from './constants'; +export * from './ciConstants'; + +const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +export const multirootPath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc'); +const workspace3Uri = vscode.Uri.file(path.join(multirootPath, 'workspace3')); + +//First thing to be executed. +process.env.VSC_PYTHON_CI_TEST = '1'; + +// Ability to use custom python environments for testing +export async function initializePython() { + await resetGlobalPythonPathSetting(); + await clearPythonPathInWorkspaceFolder(dummyPythonFile); + await clearPythonPathInWorkspaceFolder(workspace3Uri); + await setPythonPathInWorkspaceRoot(PYTHON_PATH); +} + +// tslint:disable-next-line:no-any +export async function initialize(): Promise<IExtensionTestApi> { + await initializePython(); + const api = await activateExtension(); + if (!IS_SMOKE_TEST) { + // When running smoke tests, we won't have access to these. + const configSettings = await import('../client/common/configSettings'); + // Dispose any cached python settings (used only in test env). + configSettings.PythonSettings.dispose(); + } + // tslint:disable-next-line:no-any + return (api as any) as IExtensionTestApi; +} +export async function activateExtension() { + const extension = vscode.extensions.getExtension<IExtensionApi>(PVSC_EXTENSION_ID_FOR_TESTS)!; + const api = await extension.activate(); + // Wait until its ready to use. + await api.ready; + return api; +} +// tslint:disable-next-line:no-any +export async function initializeTest(): Promise<any> { + await initializePython(); + await closeActiveWindows(); + if (!IS_SMOKE_TEST) { + // When running smoke tests, we won't have access to these. + const configSettings = await import('../client/common/configSettings'); + // Dispose any cached python settings (used only in test env). + configSettings.PythonSettings.dispose(); + } +} +export async function closeActiveWindows(): Promise<void> { + await closeActiveNotebooks(); + await closeWindowsInteral(); +} +export async function closeActiveNotebooks(): Promise<void> { + if (!vscode.env.appName.toLowerCase().includes('insiders') || !isANotebookOpen()) { + return; + } + // We could have untitled notebooks, close them by reverting changes. + // tslint:disable-next-line: no-any + while ((vscode as any).notebook.activeNotebookEditor || vscode.window.activeTextEditor) { + await vscode.commands.executeCommand('workbench.action.revertAndCloseActiveEditor'); + } + // Work around VS Code issues (sometimes notebooks do not get closed). + // Hence keep trying. + for (let counter = 0; counter <= 5 && isANotebookOpen(); counter += 1) { + await sleep(counter * 100); + await closeWindowsInteral(); + } +} + +async function closeWindowsInteral() { + return new Promise<void>((resolve, reject) => { + // Attempt to fix #1301. + // Lets not waste too much time. + const timer = setTimeout(() => { + reject(new Error("Command 'workbench.action.closeAllEditors' timed out")); + }, 15000); + vscode.commands.executeCommand('workbench.action.closeAllEditors').then( + () => { + clearTimeout(timer); + resolve(); + }, + (ex) => { + clearTimeout(timer); + reject(ex); + } + ); + }); +} + +function isANotebookOpen() { + // tslint:disable + if ( + Array.isArray((vscode as any).notebook.visibleNotebookEditors) && + (vscode as any).notebook.visibleNotebookEditors.length + ) { + return true; + } + return !!(vscode as any).notebook.activeNotebookEditor; +} diff --git a/src/test/install/channelManager.channels.test.ts b/src/test/install/channelManager.channels.test.ts new file mode 100644 index 000000000000..6686351c16cc --- /dev/null +++ b/src/test/install/channelManager.channels.test.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Container } from 'inversify'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { QuickPickOptions } from 'vscode'; +import { IApplicationShell } from '../../client/common/application/types'; +import { InstallationChannelManager } from '../../client/common/installer/channelManager'; +import { IModuleInstaller } from '../../client/common/installer/types'; +import { Product } from '../../client/common/types'; +import { Architecture } from '../../client/common/utils/platform'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService +} from '../../client/interpreter/autoSelection/types'; +import { IInterpreterLocatorService, PIPENV_SERVICE } from '../../client/interpreter/contracts'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceContainer } from '../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; + +const info: PythonEnvironment = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + envType: EnvironmentType.Unknown, + version: new SemVer('0.0.0-alpha'), + sysPrefix: '', + sysVersion: '' +}; + +// tslint:disable-next-line:max-func-body-length +suite('Installation - installation channels', () => { + let serviceManager: ServiceManager; + let serviceContainer: IServiceContainer; + let pipEnv: TypeMoq.IMock<IInterpreterLocatorService>; + + setup(() => { + const cont = new Container(); + serviceManager = new ServiceManager(cont); + serviceContainer = new ServiceContainer(cont); + pipEnv = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + serviceManager.addSingletonInstance<IInterpreterLocatorService>( + IInterpreterLocatorService, + pipEnv.object, + PIPENV_SERVICE + ); + serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService + ); + serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( + IInterpreterAutoSeletionProxyService, + MockAutoSelectionService + ); + }); + + test('Single channel', async () => { + const installer = mockInstaller(true, ''); + const cm = new InstallationChannelManager(serviceContainer); + const channels = await cm.getInstallationChannels(); + assert.equal(channels.length, 1, 'Incorrect number of channels'); + assert.equal(channels[0], installer.object, 'Incorrect installer'); + }); + + test('Multiple channels', async () => { + const installer1 = mockInstaller(true, '1'); + mockInstaller(false, '2'); + const installer3 = mockInstaller(true, '3'); + + const cm = new InstallationChannelManager(serviceContainer); + const channels = await cm.getInstallationChannels(); + assert.equal(channels.length, 2, 'Incorrect number of channels'); + assert.equal(channels[0], installer1.object, 'Incorrect installer 1'); + assert.equal(channels[1], installer3.object, 'Incorrect installer 2'); + }); + + test('pipenv channel', async () => { + mockInstaller(true, '1'); + mockInstaller(false, '2'); + mockInstaller(true, '3'); + const pipenvInstaller = mockInstaller(true, 'pipenv', 10); + + const interpreter: PythonEnvironment = { + ...info, + path: 'pipenv', + envType: EnvironmentType.VirtualEnv + }; + pipEnv.setup((x) => x.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([interpreter])); + + const cm = new InstallationChannelManager(serviceContainer); + const channels = await cm.getInstallationChannels(); + assert.equal(channels.length, 1, 'Incorrect number of channels'); + assert.equal(channels[0], pipenvInstaller.object, 'Installer must be pipenv'); + }); + + test('Select installer', async () => { + const installer1 = mockInstaller(true, '1'); + const installer2 = mockInstaller(true, '2'); + + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); + + // tslint:disable-next-line:no-any + let items: any[] | undefined; + appShell + .setup((x) => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((i: string[], _o: QuickPickOptions) => { + items = i; + }) + .returns( + () => new Promise<string | undefined>((resolve, _reject) => resolve(undefined)) + ); + + installer1.setup((x) => x.displayName).returns(() => 'Name 1'); + installer2.setup((x) => x.displayName).returns(() => 'Name 2'); + + const cm = new InstallationChannelManager(serviceContainer); + await cm.getInstallationChannel(Product.pylint); + + assert.notEqual(items, undefined, 'showQuickPick not called'); + assert.equal(items!.length, 2, 'Incorrect number of installer shown'); + assert.notEqual(items![0]!.label!.indexOf('Name 1'), -1, 'Incorrect first installer name'); + assert.notEqual(items![1]!.label!.indexOf('Name 2'), -1, 'Incorrect second installer name'); + }); + + function mockInstaller(supported: boolean, name: string, priority?: number): TypeMoq.IMock<IModuleInstaller> { + const installer = TypeMoq.Mock.ofType<IModuleInstaller>(); + installer + .setup((x) => x.isSupported(TypeMoq.It.isAny())) + .returns( + () => new Promise<boolean>((resolve) => resolve(supported)) + ); + installer.setup((x) => x.priority).returns(() => (priority ? priority : 0)); + serviceManager.addSingletonInstance<IModuleInstaller>(IModuleInstaller, installer.object, name); + return installer; + } +}); diff --git a/src/test/install/channelManager.messages.test.ts b/src/test/install/channelManager.messages.test.ts new file mode 100644 index 000000000000..6baba6a7e13f --- /dev/null +++ b/src/test/install/channelManager.messages.test.ts @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Container } from 'inversify'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { IApplicationShell } from '../../client/common/application/types'; +import { InstallationChannelManager } from '../../client/common/installer/channelManager'; +import { IModuleInstaller } from '../../client/common/installer/types'; +import { IPlatformService } from '../../client/common/platform/types'; +import { Product } from '../../client/common/types'; +import { Architecture } from '../../client/common/utils/platform'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService +} from '../../client/interpreter/autoSelection/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceContainer } from '../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; + +const info: PythonEnvironment = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + envType: EnvironmentType.Unknown, + version: new SemVer('0.0.0-alpha'), + sysPrefix: '', + sysVersion: '' +}; + +// tslint:disable-next-line:max-func-body-length +suite('Installation - channel messages', () => { + let serviceContainer: IServiceContainer; + let platform: TypeMoq.IMock<IPlatformService>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let interpreters: TypeMoq.IMock<IInterpreterService>; + + setup(() => { + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + serviceContainer = new ServiceContainer(cont); + + platform = TypeMoq.Mock.ofType<IPlatformService>(); + serviceManager.addSingletonInstance<IPlatformService>(IPlatformService, platform.object); + + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); + + interpreters = TypeMoq.Mock.ofType<IInterpreterService>(); + serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, interpreters.object); + + const moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + serviceManager.addSingletonInstance<IModuleInstaller>(IModuleInstaller, moduleInstaller.object); + serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService + ); + serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( + IInterpreterAutoSeletionProxyService, + MockAutoSelectionService + ); + }); + + test('No installers message: Unknown/Windows', async () => { + platform.setup((x) => x.isWindows).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Unknown, async (message: string, url: string) => { + verifyMessage(message, ['Pip'], ['Conda']); + verifyUrl(url, ['Windows', 'Pip']); + }); + }); + + test('No installers message: Conda/Windows', async () => { + platform.setup((x) => x.isWindows).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Conda, async (message: string, url: string) => { + verifyMessage(message, ['Pip', 'Conda'], []); + verifyUrl(url, ['Windows', 'Pip', 'Conda']); + }); + }); + + test('No installers message: Unknown/Mac', async () => { + platform.setup((x) => x.isWindows).returns(() => false); + platform.setup((x) => x.isMac).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Unknown, async (message: string, url: string) => { + verifyMessage(message, ['Pip'], ['Conda']); + verifyUrl(url, ['Mac', 'Pip']); + }); + }); + + test('No installers message: Conda/Mac', async () => { + platform.setup((x) => x.isWindows).returns(() => false); + platform.setup((x) => x.isMac).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Conda, async (message: string, url: string) => { + verifyMessage(message, ['Pip', 'Conda'], []); + verifyUrl(url, ['Mac', 'Pip', 'Conda']); + }); + }); + + test('No installers message: Unknown/Linux', async () => { + platform.setup((x) => x.isWindows).returns(() => false); + platform.setup((x) => x.isMac).returns(() => false); + platform.setup((x) => x.isLinux).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Unknown, async (message: string, url: string) => { + verifyMessage(message, ['Pip'], ['Conda']); + verifyUrl(url, ['Linux', 'Pip']); + }); + }); + + test('No installers message: Conda/Linux', async () => { + platform.setup((x) => x.isWindows).returns(() => false); + platform.setup((x) => x.isMac).returns(() => false); + platform.setup((x) => x.isLinux).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Conda, async (message: string, url: string) => { + verifyMessage(message, ['Pip', 'Conda'], []); + verifyUrl(url, ['Linux', 'Pip', 'Conda']); + }); + }); + + test('No channels message', async () => { + platform.setup((x) => x.isWindows).returns(() => true); + await testInstallerMissingMessage( + EnvironmentType.Unknown, + async (message: string, url: string) => { + verifyMessage(message, ['Pip'], ['Conda']); + verifyUrl(url, ['Windows', 'Pip']); + }, + 'getInstallationChannel' + ); + }); + + function verifyMessage(message: string, present: string[], missing: string[]) { + for (const p of present) { + assert.equal(message.indexOf(p) >= 0, true, `Message does not contain ${p}.`); + } + for (const m of missing) { + assert.equal(message.indexOf(m) < 0, true, `Message incorrectly contains ${m}.`); + } + } + + function verifyUrl(url: string, terms: string[]) { + assert.equal(url.indexOf('https://') >= 0, true, 'Search Url must be https.'); + for (const term of terms) { + assert.equal(url.indexOf(term) >= 0, true, `Search Url does not contain ${term}.`); + } + } + + async function testInstallerMissingMessage( + interpreterType: EnvironmentType, + verify: (m: string, u: string) => Promise<void>, + methodType: 'showNoInstallersMessage' | 'getInstallationChannel' = 'showNoInstallersMessage' + ): Promise<void> { + const activeInterpreter: PythonEnvironment = { + ...info, + envType: interpreterType, + path: '' + }; + interpreters + .setup((x) => x.getActiveInterpreter(TypeMoq.It.isAny())) + .returns( + () => new Promise<PythonEnvironment>((resolve, _reject) => resolve(activeInterpreter)) + ); + const channels = new InstallationChannelManager(serviceContainer); + + let url: string = ''; + let message: string = ''; + let search: string = ''; + appShell + .setup((x) => x.showErrorMessage(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) + .callback((m: string, s: string) => { + message = m; + search = s; + }) + .returns( + () => new Promise<string>((resolve, _reject) => resolve(search)) + ); + appShell + .setup((x) => x.openUrl(TypeMoq.It.isAnyString())) + .callback((s: string) => { + url = s; + }); + if (methodType === 'showNoInstallersMessage') { + await channels.showNoInstallersMessage(); + } else { + await channels.getInstallationChannel(Product.pylint); + } + await verify(message, url); + } +}); diff --git a/src/test/interpreters/activation/preWarmVariables.unit.test.ts b/src/test/interpreters/activation/preWarmVariables.unit.test.ts new file mode 100644 index 000000000000..0881b5216e1e --- /dev/null +++ b/src/test/interpreters/activation/preWarmVariables.unit.test.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter } from 'vscode'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { PreWarmActivatedEnvironmentVariables } from '../../../client/interpreter/activation/preWarmVariables'; +import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; + +suite('Interpreters Activation - Env Variables', () => { + let activationService: IExtensionSingleActivationService; + let envActivationService: IEnvironmentActivationService; + let interpreterService: IInterpreterService; + let onDidChangeInterpreter: EventEmitter<void>; + setup(() => { + onDidChangeInterpreter = new EventEmitter<void>(); + envActivationService = mock(EnvironmentActivationService); + interpreterService = mock(InterpreterService); + when(interpreterService.onDidChangeInterpreter).thenReturn(onDidChangeInterpreter.event); + activationService = new PreWarmActivatedEnvironmentVariables( + instance(envActivationService), + instance(interpreterService) + ); + }); + test('Should pre-warm env variables', async () => { + when(envActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); + + await activationService.activate(); + + verify(envActivationService.getActivatedEnvironmentVariables(undefined)).once(); + }); + test('Should pre-warm env variables when interpreter changes', async () => { + when(envActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); + + await activationService.activate(); + + verify(envActivationService.getActivatedEnvironmentVariables(undefined)).once(); + + onDidChangeInterpreter.fire(); + + verify(envActivationService.getActivatedEnvironmentVariables(undefined)).twice(); + }); +}); diff --git a/src/test/interpreters/activation/service.unit.test.ts b/src/test/interpreters/activation/service.unit.test.ts new file mode 100644 index 000000000000..980fe8fc4d74 --- /dev/null +++ b/src/test/interpreters/activation/service.unit.test.ts @@ -0,0 +1,337 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { expect } from 'chai'; +import { EOL } from 'os'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { CurrentProcess } from '../../../client/common/process/currentProcess'; +import { ProcessService } from '../../../client/common/process/proc'; +import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { TerminalHelper } from '../../../client/common/terminal/helper'; +import { ITerminalHelper } from '../../../client/common/terminal/types'; +import { ICurrentProcess } from '../../../client/common/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { Architecture, OSType } from '../../../client/common/utils/platform'; +import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6'; +const defaultShells = { + [OSType.Windows]: 'cmd', + [OSType.OSX]: 'bash', + [OSType.Linux]: 'bash', + [OSType.Unknown]: undefined +}; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length +suite('Interpreters Activation - Python Environment Variables', () => { + let service: EnvironmentActivationService; + let helper: ITerminalHelper; + let platform: IPlatformService; + let processServiceFactory: IProcessServiceFactory; + let processService: IProcessService; + let currentProcess: ICurrentProcess; + let envVarsService: IEnvironmentVariablesProvider; + let workspace: IWorkspaceService; + let interpreterService: IInterpreterService; + let onDidChangeEnvVariables: EventEmitter<Uri | undefined>; + let onDidChangeInterpreter: EventEmitter<void>; + const pythonInterpreter: PythonEnvironment = { + path: '/foo/bar/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 + }; + + function initSetup() { + helper = mock(TerminalHelper); + platform = mock(PlatformService); + processServiceFactory = mock(ProcessServiceFactory); + processService = mock(ProcessService); + currentProcess = mock(CurrentProcess); + envVarsService = mock(EnvironmentVariablesProvider); + interpreterService = mock(InterpreterService); + workspace = mock(WorkspaceService); + onDidChangeEnvVariables = new EventEmitter<Uri | undefined>(); + onDidChangeInterpreter = new EventEmitter<void>(); + when(envVarsService.onDidEnvironmentVariablesChange).thenReturn(onDidChangeEnvVariables.event); + when(interpreterService.onDidChangeInterpreter).thenReturn(onDidChangeInterpreter.event); + service = new EnvironmentActivationService( + instance(helper), + instance(platform), + instance(processServiceFactory), + instance(currentProcess), + instance(workspace), + instance(interpreterService), + instance(envVarsService) + ); + } + + function title(resource?: Uri, interpreter?: PythonEnvironment) { + return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; + } + + [undefined, Uri.parse('a')].forEach((resource) => + [undefined, pythonInterpreter].forEach((interpreter) => { + suite(title(resource, interpreter), () => { + setup(initSetup); + test('Unknown os will return empty variables', async () => { + when(platform.osType).thenReturn(OSType.Unknown); + const env = await service.getActivatedEnvironmentVariables(resource); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + }); + + const osTypes = getNamesAndValues<OSType>(OSType).filter((osType) => osType.value !== OSType.Unknown); + + osTypes.forEach((osType) => { + suite(osType.name, () => { + setup(initSetup); + test('getEnvironmentActivationShellCommands will be invoked', async () => { + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).thenResolve(); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).once(); + }); + test('Validate command used to activation and printing env vars', async () => { + const cmd = ['1', '2']; + const envVars = { one: '1', two: '2' }; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + + const shellCmd = capture(processService.shellExec).first()[0]; + + const isolated = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'pyvsc-run-isolated.py'); + const printEnvPyFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'printEnvVariables.py'); + const expectedCommand = [ + ...cmd, + `echo '${getEnvironmentPrefix}'`, + `python ${isolated} ${printEnvPyFile.fileToCommandArgument()}` + ].join(' && '); + + expect(shellCmd).to.equal(expectedCommand); + }); + test('Validate env Vars used to activation and printing env vars', async () => { + const cmd = ['1', '2']; + const envVars = { one: '1', two: '2' }; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + + const options = capture(processService.shellExec).first()[1]; + + const expectedShell = defaultShells[osType.value]; + // tslint:disable-next-line: chai-vague-errors + expect(options).to.deep.equal({ + shell: expectedShell, + env: envVars, + timeout: 30000, + maxBuffer: 1000 * 1000, + throwOnStdErr: false + }); + }); + test('Use current process variables if there are no custom variables', async () => { + const cmd = ['1', '2']; + const envVars = { one: '1', two: '2', PYTHONWARNINGS: 'ignore' }; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve({}); + when(currentProcess.env).thenReturn(envVars); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + verify(currentProcess.env).once(); + + const options = capture(processService.shellExec).first()[1]; + + const expectedShell = defaultShells[osType.value]; + // tslint:disable-next-line: chai-vague-errors + expect(options).to.deep.equal({ + env: envVars, + shell: expectedShell, + timeout: 30000, + maxBuffer: 1000 * 1000, + throwOnStdErr: false + }); + }); + test('Error must be swallowed when activation fails', async () => { + const cmd = ['1', '2']; + const envVars = { one: '1', two: '2' }; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); + when(processService.shellExec(anything(), anything())).thenReject(new Error('kaboom')); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + }); + test('Return parsed variables', async () => { + const cmd = ['1', '2']; + const envVars = { one: '1', two: '2' }; + const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' }; + const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); + when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout }); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.deep.equal(varsFromEnv); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + }); + test('Cache Variables', async () => { + const cmd = ['1', '2']; + const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' }; + const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve({}); + when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout }); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + const env2 = await service.getActivatedEnvironmentVariables(resource, interpreter); + const env3 = await service.getActivatedEnvironmentVariables(resource, interpreter); + + expect(env).to.deep.equal(varsFromEnv); + // All same objects. + expect(env).to.equal(env2).to.equal(env3); + + // All methods invoked only once. + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + }); + async function testClearingCache(bustCache: Function) { + const cmd = ['1', '2']; + const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' }; + const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve({}); + when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout }); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + bustCache(); + const env2 = await service.getActivatedEnvironmentVariables(resource, interpreter); + + expect(env).to.deep.equal(varsFromEnv); + // Objects are different (not same reference). + expect(env).to.not.equal(env2); + // However variables are the same. + expect(env).to.deep.equal(env2); + + // All methods invoked twice as cache was blown. + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter) + ).twice(); + verify(processServiceFactory.create(resource)).twice(); + verify(envVarsService.getEnvironmentVariables(resource)).twice(); + verify(processService.shellExec(anything(), anything())).twice(); + } + test('Cache Variables get cleared when changing interpreter', async () => { + await testClearingCache(onDidChangeInterpreter.fire.bind(onDidChangeInterpreter)); + }); + test('Cache Variables get cleared when changing env variables file', async () => { + await testClearingCache(onDidChangeEnvVariables.fire.bind(onDidChangeEnvVariables)); + }); + }); + }); + }); + }) + ); +}); diff --git a/src/test/interpreters/activation/terminalEnvironmentActivationService.unit.test.ts b/src/test/interpreters/activation/terminalEnvironmentActivationService.unit.test.ts new file mode 100644 index 000000000000..54776fde9cae --- /dev/null +++ b/src/test/interpreters/activation/terminalEnvironmentActivationService.unit.test.ts @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as path from 'path'; +import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { TerminalServiceFactory } from '../../../client/common/terminal/factory'; +import { TerminalService } from '../../../client/common/terminal/service'; +import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { TerminalEnvironmentActivationService } from '../../../client/interpreter/activation/terminalEnvironmentActivationService'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { noop } from '../../core'; + +// tslint:disable-next-line: max-func-body-length +suite('Interpreters Activation - Python Environment Variables (using terminals)', () => { + let envActivationService: IEnvironmentActivationService; + let terminalFactory: ITerminalServiceFactory; + let fs: IFileSystem; + let envVarsProvider: IEnvironmentVariablesProvider; + const jsonFile = path.join('hello', 'output.json'); + let terminal: ITerminalService; + const mockInterpreter: PythonEnvironment = { + architecture: Architecture.Unknown, + path: '', + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Conda + }; + setup(() => { + terminalFactory = mock(TerminalServiceFactory); + terminal = mock(TerminalService); + fs = mock(FileSystem); + envVarsProvider = mock(EnvironmentVariablesProvider); + + when(terminalFactory.getTerminalService(anything())).thenReturn(instance(terminal)); + when(fs.createTemporaryFile(anything())).thenResolve({ dispose: noop, filePath: jsonFile }); + when(terminal.sendCommand(anything(), anything(), anything(), anything())).thenResolve(); + envActivationService = new TerminalEnvironmentActivationService( + instance(terminalFactory), + instance(fs), + instance(envVarsProvider) + ); + }); + + [undefined, Uri.file('some Resource')].forEach((resource) => { + [undefined, mockInterpreter].forEach((interpreter) => { + suite(resource ? 'With a resource' : 'Without a resource', () => { + suite(interpreter ? 'With an interpreter' : 'Without an interpreter', () => { + test('Should create a terminal with user defined custom env vars', async () => { + const customEnv = { HELLO: '1' }; + when(envVarsProvider.getCustomEnvironmentVariables(resource)).thenResolve(customEnv); + + await envActivationService.getActivatedEnvironmentVariables(resource, interpreter); + + const termArgs = capture(terminalFactory.getTerminalService).first()[0]; + assert.equal(termArgs?.env, customEnv); + }); + test('Should destroy the created terminal', async () => { + const customEnv = { HELLO: '1' }; + when(envVarsProvider.getCustomEnvironmentVariables(resource)).thenResolve(customEnv); + + await envActivationService.getActivatedEnvironmentVariables(resource, interpreter); + + verify(terminal.dispose()).once(); + }); + test('Should create a terminal with correct arguments', async () => { + when(envVarsProvider.getCustomEnvironmentVariables(resource)).thenResolve(undefined); + + await envActivationService.getActivatedEnvironmentVariables(resource, interpreter); + + const termArgs = capture(terminalFactory.getTerminalService).first()[0]; + assert.isUndefined(termArgs?.env); + assert.equal(termArgs?.resource, resource); + assert.deepEqual(termArgs?.interpreter, interpreter); + assert.isTrue(termArgs?.hideFromUser); + }); + test('Should create a terminal with correct arguments', async () => { + when(envVarsProvider.getCustomEnvironmentVariables(resource)).thenResolve(undefined); + + await envActivationService.getActivatedEnvironmentVariables(resource, interpreter); + + const termArgs = capture(terminalFactory.getTerminalService).first()[0]; + assert.isUndefined(termArgs?.env); + assert.equal(termArgs?.resource, resource); + assert.deepEqual(termArgs?.interpreter, interpreter); + assert.isTrue(termArgs?.hideFromUser); + }); + test('Should execute python file in terminal (that is what dumps variables into json)', async () => { + when(envVarsProvider.getCustomEnvironmentVariables(resource)).thenResolve(undefined); + const isolated = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'pyvsc-run-isolated.py'); + const pyFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'printEnvVariablesToFile.py'); + + await envActivationService.getActivatedEnvironmentVariables(resource, interpreter); + + const cmd = interpreter?.path || 'python'; + verify( + terminal.sendCommand( + cmd, + deepEqual([isolated, pyFile, jsonFile.fileToCommandArgument()]), + anything(), + false + ) + ).once(); + }); + test('Should return activated environment variables', async () => { + when(envVarsProvider.getCustomEnvironmentVariables(resource)).thenResolve(undefined); + when(fs.readFile(jsonFile)).thenResolve(JSON.stringify({ WOW: '1' })); + + const vars = await envActivationService.getActivatedEnvironmentVariables(resource, interpreter); + + assert.deepEqual(vars, { WOW: '1' }); + }); + }); + }); + }); + }); +}); diff --git a/src/test/interpreters/activation/wrapperEnvironmentActivationService.unit.test.ts b/src/test/interpreters/activation/wrapperEnvironmentActivationService.unit.test.ts new file mode 100644 index 000000000000..5c9ccee929f5 --- /dev/null +++ b/src/test/interpreters/activation/wrapperEnvironmentActivationService.unit.test.ts @@ -0,0 +1,328 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { CryptoUtils } from '../../../client/common/crypto'; +import { ExperimentsManager } from '../../../client/common/experiments/manager'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { ICryptoUtils, IExperimentsManager, IExtensionContext, Resource } from '../../../client/common/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; +import { TerminalEnvironmentActivationService } from '../../../client/interpreter/activation/terminalEnvironmentActivationService'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { WrapperEnvironmentActivationService } from '../../../client/interpreter/activation/wrapperEnvironmentActivationService'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +// tslint:disable-next-line: max-func-body-length +suite('Interpreters Activation - Python Environment Variables (wrap terminal and proc approach)', () => { + let envActivationService: IEnvironmentActivationService; + let procActivation: IEnvironmentActivationService; + let termActivation: IEnvironmentActivationService; + let experiment: IExperimentsManager; + let interpreterService: IInterpreterService; + let workspace: IWorkspaceService; + let envVarsProvider: IEnvironmentVariablesProvider; + let onDidChangeEnvVars: EventEmitter<Resource>; + let crypto: ICryptoUtils; + let fs: IFileSystem; + const mockInterpreter: PythonEnvironment = { + architecture: Architecture.Unknown, + path: '', + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Conda + }; + + // tslint:disable-next-line: max-func-body-length + [undefined, Uri.file('some Resource')].forEach((resource) => { + // tslint:disable-next-line: max-func-body-length + [undefined, mockInterpreter].forEach((interpreter) => { + [undefined, path.join('a')].forEach((storagePath) => { + // tslint:disable-next-line: max-func-body-length + suite(resource ? 'With an extension storagepath' : 'Without an extension storagepath', () => { + suite(resource ? 'With a resource' : 'Without a resource', () => { + setup(() => { + onDidChangeEnvVars = new EventEmitter<Resource>(); + envVarsProvider = mock(EnvironmentVariablesProvider); + procActivation = mock(EnvironmentActivationService); + termActivation = mock(TerminalEnvironmentActivationService); + experiment = mock(ExperimentsManager); + interpreterService = mock(InterpreterService); + workspace = mock(WorkspaceService); + crypto = mock(CryptoUtils); + fs = mock(FileSystem); + const extContext: IExtensionContext = { + get storagePath() { + return storagePath; + } + // tslint:disable-next-line: no-any + } as any; + when(crypto.createHash(anything(), anything(), anything())).thenCall((value) => value); + when(experiment.inExperiment(anything())).thenReturn(true); + when(envVarsProvider.getCustomEnvironmentVariables(anything())).thenCall((value) => + Promise.resolve({ + key: (value || {}).toString() + }) + ); + when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn(onDidChangeEnvVars.event); + when(fs.readFile(anything())).thenReject(new Error('kaboom')); + // Generate a unique key based on resource. + when(workspace.getWorkspaceFolderIdentifier(anything())).thenCall( + (identifier: Resource) => identifier?.fsPath || '' + ); + envActivationService = new WrapperEnvironmentActivationService( + instance(procActivation), + instance(termActivation), + instance(experiment), + instance(interpreterService), + instance(envVarsProvider), + extContext, + instance(fs), + instance(crypto), + [] + ); + }); + + // tslint:disable-next-line: max-func-body-length + suite(interpreter ? 'With an interpreter' : 'Without an interpreter', () => { + test('Environment variables returned by process provider should be used if terminal provider crashes', async () => { + const expectedVars = { WOW: '1' }; + when( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenReject(new Error('kaboom')); + when( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve(expectedVars); + + const vars = await envActivationService.getActivatedEnvironmentVariables( + resource, + interpreter + ); + + assert.deepEqual(vars, expectedVars); + verify( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + verify( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + }); + test('Use cached variables returned by process provider should be used if terminal provider crashes', async () => { + const expectedVars = { WOW: '1' }; + when( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenReject(new Error('kaboom')); + when( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve(expectedVars); + + let vars = await envActivationService.getActivatedEnvironmentVariables( + resource, + interpreter + ); + + assert.deepEqual(vars, expectedVars); + verify( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + verify( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + + vars = await envActivationService.getActivatedEnvironmentVariables( + resource, + interpreter + ); + assert.deepEqual(vars, expectedVars); + verify( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + verify( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + }); + test('Environment variables returned by terminal provider should be used if that returns any variables', async () => { + const expectedVars = { WOW: '1' }; + when( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve(expectedVars); + when( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve({ somethingElse: '1' }); + + const vars = await envActivationService.getActivatedEnvironmentVariables( + resource, + interpreter + ); + + assert.deepEqual(vars, expectedVars); + verify( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + verify( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + }); + test('Environment variables returned by terminal provider should be used if that returns any variables', async () => { + const expectedVars = { WOW: '1' }; + when( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve(expectedVars); + when( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve({ somethingElse: '1' }); + + let vars = await envActivationService.getActivatedEnvironmentVariables( + resource, + interpreter + ); + + assert.deepEqual(vars, expectedVars); + verify( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + verify( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + + vars = await envActivationService.getActivatedEnvironmentVariables( + resource, + interpreter + ); + assert.deepEqual(vars, expectedVars); + verify( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + verify( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + }); + test('Will not use cached info, if passing different resource or interpreter', async () => { + const expectedVars = { WOW: '1' }; + when( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve(expectedVars); + when( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve({ somethingElse: '1' }); + + let vars = await envActivationService.getActivatedEnvironmentVariables( + resource, + interpreter + ); + + assert.deepEqual(vars, expectedVars); + verify( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + verify( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + + // Same resource, hence return cached info. + vars = await envActivationService.getActivatedEnvironmentVariables( + resource, + interpreter + ); + assert.deepEqual(vars, expectedVars); + verify( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + verify( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).once(); + + // Invoke again with a different resource. + const newResource = Uri.file('New Resource'); + when( + termActivation.getActivatedEnvironmentVariables(newResource, anything(), anything()) + ).thenResolve(undefined); + when( + procActivation.getActivatedEnvironmentVariables(newResource, anything(), anything()) + ).thenResolve({ NewVars: '1' }); + + vars = await envActivationService.getActivatedEnvironmentVariables( + newResource, + undefined + ); + assert.deepEqual(vars, { NewVars: '1' }); + verify( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).twice(); + verify( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).twice(); + + // Invoke again with a different python interpreter. + const newInterpreter: PythonEnvironment = { + architecture: Architecture.x64, + path: 'New', + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Pipenv + }; + when( + termActivation.getActivatedEnvironmentVariables( + anything(), + newInterpreter, + anything() + ) + ).thenResolve({ NewPythonVars: '1' }); + when( + procActivation.getActivatedEnvironmentVariables( + anything(), + newInterpreter, + anything() + ) + ).thenResolve(undefined); + + vars = await envActivationService.getActivatedEnvironmentVariables( + newResource, + newInterpreter + ); + assert.deepEqual(vars, { NewPythonVars: '1' }); + verify( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thrice(); + verify( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thrice(); + }); + test('Use variables from file cache', async function () { + if (!storagePath) { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + const expectedVars = { WOW: '1' }; + when( + termActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve(undefined); + when( + procActivation.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve(undefined); + when(fs.readFile(anything())).thenResolve(JSON.stringify({ env: expectedVars })); + + const vars = await envActivationService.getActivatedEnvironmentVariables( + resource, + interpreter + ); + + assert.deepEqual(vars, expectedVars); + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/src/test/interpreters/autoSelection/index.unit.test.ts b/src/test/interpreters/autoSelection/index.unit.test.ts new file mode 100644 index 000000000000..14b697b551fd --- /dev/null +++ b/src/test/interpreters/autoSelection/index.unit.test.ts @@ -0,0 +1,396 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { anything, instance, mock, reset, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PersistentState, PersistentStateFactory } from '../../../client/common/persistentState'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../client/common/types'; +import { createDeferred } from '../../../client/common/utils/async'; +import { InterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection'; +import { InterpreterSecurityService } from '../../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService'; +import { InterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/proxy'; +import { CachedInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/cached'; +import { CurrentPathInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/currentPath'; +import { SettingsInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/settings'; +import { SystemWideInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/system'; +import { WindowsRegistryInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/winRegistry'; +import { WorkspaceVirtualEnvInterpretersAutoSelectionRule } from '../../../client/interpreter/autoSelection/rules/workspaceEnv'; +import { + IInterpreterAutoSelectionRule, + IInterpreterAutoSeletionProxyService, + IInterpreterSecurityService +} from '../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper } from '../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../client/interpreter/helpers'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +const preferredGlobalInterpreter = 'preferredGlobalPyInterpreter'; + +suite('Interpreters - Auto Selection', () => { + let autoSelectionService: InterpreterAutoSelectionServiceTest; + let workspaceService: IWorkspaceService; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let systemInterpreter: IInterpreterAutoSelectionRule; + let currentPathInterpreter: IInterpreterAutoSelectionRule; + let winRegInterpreter: IInterpreterAutoSelectionRule; + let cachedPaths: IInterpreterAutoSelectionRule; + let userDefinedInterpreter: IInterpreterAutoSelectionRule; + let workspaceInterpreter: IInterpreterAutoSelectionRule; + let state: PersistentState<PythonEnvironment | undefined>; + let helper: IInterpreterHelper; + let proxy: IInterpreterAutoSeletionProxyService; + let interpreterSecurityService: IInterpreterSecurityService; + class InterpreterAutoSelectionServiceTest extends InterpreterAutoSelectionService { + public initializeStore(resource: Resource): Promise<void> { + return super.initializeStore(resource); + } + public storeAutoSelectedInterpreter(resource: Resource, interpreter: PythonEnvironment | undefined) { + return super.storeAutoSelectedInterpreter(resource, interpreter); + } + public getAutoSelectedWorkspacePromises() { + return this.autoSelectedWorkspacePromises; + } + } + setup(() => { + interpreterSecurityService = mock(InterpreterSecurityService); + workspaceService = mock(WorkspaceService); + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + systemInterpreter = mock(SystemWideInterpretersAutoSelectionRule); + currentPathInterpreter = mock(CurrentPathInterpretersAutoSelectionRule); + winRegInterpreter = mock(WindowsRegistryInterpretersAutoSelectionRule); + cachedPaths = mock(CachedInterpretersAutoSelectionRule); + userDefinedInterpreter = mock(SettingsInterpretersAutoSelectionRule); + workspaceInterpreter = mock(WorkspaceVirtualEnvInterpretersAutoSelectionRule); + helper = mock(InterpreterHelper); + proxy = mock(InterpreterAutoSeletionProxyService); + when(interpreterSecurityService.isSafe(anything())).thenReturn(undefined); + + autoSelectionService = new InterpreterAutoSelectionServiceTest( + instance(workspaceService), + instance(stateFactory), + instance(fs), + instance(systemInterpreter), + instance(currentPathInterpreter), + instance(winRegInterpreter), + instance(cachedPaths), + instance(userDefinedInterpreter), + instance(workspaceInterpreter), + instance(proxy), + instance(helper), + instance(interpreterSecurityService) + ); + }); + + test('Instance is registered in proxy', () => { + verify(proxy.registerInstance!(autoSelectionService)).once(); + }); + test('Rules are chained in order of preference', () => { + verify(userDefinedInterpreter.setNextRule(instance(workspaceInterpreter))).once(); + verify(workspaceInterpreter.setNextRule(instance(cachedPaths))).once(); + verify(cachedPaths.setNextRule(instance(currentPathInterpreter))).once(); + verify(currentPathInterpreter.setNextRule(instance(winRegInterpreter))).once(); + verify(winRegInterpreter.setNextRule(instance(systemInterpreter))).once(); + verify(systemInterpreter.setNextRule(anything())).never(); + }); + test('Run rules in background', async () => { + let eventFired = false; + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); + autoSelectionService.initializeStore = () => Promise.resolve(); + await autoSelectionService.autoSelectInterpreter(undefined); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + + const allRules = [ + userDefinedInterpreter, + winRegInterpreter, + currentPathInterpreter, + systemInterpreter, + workspaceInterpreter, + cachedPaths + ]; + for (const service of allRules) { + verify(service.autoSelectInterpreter(undefined)).once(); + if (service !== userDefinedInterpreter) { + verify(service.autoSelectInterpreter(anything(), autoSelectionService)).never(); + } + } + verify(userDefinedInterpreter.autoSelectInterpreter(anything(), autoSelectionService)).once(); + }); + test('Run userDefineInterpreter as the first rule', async () => { + let eventFired = false; + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); + autoSelectionService.initializeStore = () => Promise.resolve(); + + await autoSelectionService.autoSelectInterpreter(undefined); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + verify(userDefinedInterpreter.autoSelectInterpreter(undefined, autoSelectionService)).once(); + }); + test('Initialize the store', async () => { + let initialize = false; + let eventFired = false; + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); + autoSelectionService.initializeStore = async () => (initialize = true as any); + + await autoSelectionService.autoSelectInterpreter(undefined); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + expect(initialize).to.be.equal(true, 'Not invoked'); + }); + test('Initializing the store would be executed once', async () => { + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined + ) + ).thenReturn(instance(state)); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.initializeStore(undefined); + + verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); + }); + test("Clear file stored in cache if it doesn't exist", async () => { + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined + ) + ).thenReturn(instance(state)); + when(state.value).thenReturn(interpreterInfo); + when(fs.fileExists(pythonPath)).thenResolve(false); + + await autoSelectionService.initializeStore(undefined); + + verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); + verify(state.value).atLeast(1); + verify(fs.fileExists(pythonPath)).once(); + verify(state.updateValue(undefined)).once(); + }); + test('Should not clear file stored in cache if it does exist', async () => { + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined + ) + ).thenReturn(instance(state)); + when(state.value).thenReturn(interpreterInfo); + when(fs.fileExists(pythonPath)).thenResolve(true); + + await autoSelectionService.initializeStore(undefined); + + verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); + verify(state.value).atLeast(1); + verify(fs.fileExists(pythonPath)).once(); + verify(state.updateValue(undefined)).never(); + }); + test('Store interpreter info in state store when resource is undefined', async () => { + let eventFired = false; + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined + ) + ).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(undefined, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + + verify(state.updateValue(interpreterInfo)).once(); + expect(selectedInterpreter).to.deep.equal(interpreterInfo); + expect(eventFired).to.deep.equal(false, 'event fired'); + }); + test('If interpreter chosen is unsafe, return `undefined` as the autoselected interpreter', async () => { + const interpreterInfo = { path: 'pythonPath' } as any; + autoSelectionService._getAutoSelectedInterpreter = () => interpreterInfo; + reset(interpreterSecurityService); + when(interpreterSecurityService.isSafe(interpreterInfo)).thenReturn(false); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + expect(selectedInterpreter).to.equal(undefined, 'Should be undefined'); + }); + test('Do not store global interpreter info in state store when resource is undefined and version is lower than one already in state', async () => { + let eventFired = false; + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath, version: new SemVer('1.0.0') } as any; + const interpreterInfoInState = { path: pythonPath, version: new SemVer('2.0.0') } as any; + when(fs.fileExists(interpreterInfoInState.path)).thenResolve(true); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined + ) + ).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); + when(state.value).thenReturn(interpreterInfoInState); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(undefined, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + + verify(state.updateValue(anything())).never(); + expect(selectedInterpreter).to.deep.equal(interpreterInfoInState); + expect(eventFired).to.deep.equal(false, 'event fired'); + }); + test('Store global interpreter info in state store when resource is undefined and version is higher than one already in state', async () => { + let eventFired = false; + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath, version: new SemVer('3.0.0') } as any; + const interpreterInfoInState = { path: pythonPath, version: new SemVer('2.0.0') } as any; + when(fs.fileExists(interpreterInfoInState.path)).thenResolve(true); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined + ) + ).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); + when(state.value).thenReturn(interpreterInfoInState); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(undefined, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + + verify(state.updateValue(anything())).once(); + expect(selectedInterpreter).to.deep.equal(interpreterInfo); + expect(eventFired).to.deep.equal(false, 'event fired'); + }); + test('Store global interpreter info in state store', async () => { + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined + ) + ).thenReturn(instance(state)); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.setGlobalInterpreter(interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + + verify(state.updateValue(interpreterInfo)).once(); + expect(selectedInterpreter).to.deep.equal(interpreterInfo); + }); + test('Store interpreter info in state store when resource is defined', async () => { + let eventFired = false; + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + const resource = Uri.parse('one'); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined + ) + ).thenReturn(instance(state)); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => (eventFired = true)); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(resource, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(resource); + + verify(state.updateValue(interpreterInfo)).never(); + expect(selectedInterpreter).to.deep.equal(interpreterInfo); + expect(eventFired).to.deep.equal(false, 'event fired'); + }); + test('Store workspace interpreter info in state store', async () => { + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + const resource = Uri.parse('one'); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined + ) + ).thenReturn(instance(state)); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); + when(workspaceService.getWorkspaceFolderIdentifier(anything(), anything())).thenReturn(''); + const deferred = createDeferred<void>(); + deferred.resolve(); + autoSelectionService.getAutoSelectedWorkspacePromises().set('', deferred); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.setWorkspaceInterpreter(resource, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(resource); + + verify(state.updateValue(interpreterInfo)).once(); + expect(selectedInterpreter).to.deep.equal(interpreterInfo); + }); + test('Return undefined when we do not have a global value', async () => { + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + const resource = Uri.parse('one'); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined + ) + ).thenReturn(instance(state)); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(resource, interpreterInfo); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); + + verify(state.updateValue(interpreterInfo)).never(); + expect(selectedInterpreter === null || selectedInterpreter === undefined).to.equal(true, 'Should be undefined'); + }); + test('Return global value if we do not have a matching value for the resource', async () => { + const pythonPath = 'Hello World'; + const interpreterInfo = { path: pythonPath } as any; + const resource = Uri.parse('one'); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined + ) + ).thenReturn(instance(state)); + const globalInterpreterInfo = { path: 'global Value' }; + when(state.value).thenReturn(globalInterpreterInfo as any); + when(workspaceService.getWorkspaceFolderIdentifier(resource, anything())).thenReturn('1'); + const deferred = createDeferred<void>(); + deferred.resolve(); + autoSelectionService.getAutoSelectedWorkspacePromises().set('', deferred); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(resource, interpreterInfo); + + const anotherResourceOfAnotherWorkspace = Uri.parse('Some other workspace'); + when(workspaceService.getWorkspaceFolderIdentifier(anotherResourceOfAnotherWorkspace, anything())).thenReturn( + '2' + ); + + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(anotherResourceOfAnotherWorkspace); + + verify(state.updateValue(interpreterInfo)).never(); + expect(selectedInterpreter).to.deep.equal(globalInterpreterInfo); + }); +}); diff --git a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts new file mode 100644 index 000000000000..9db6976bdf3b --- /dev/null +++ b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterEvaluation.unit.test.ts @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as Typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IApplicationShell } from '../../../../client/common/application/types'; +import { IBrowserService, IPersistentState } from '../../../../client/common/types'; +import { Common, Interpreters } from '../../../../client/common/utils/localize'; +import { learnMoreOnInterpreterSecurityURI } from '../../../../client/interpreter/autoSelection/constants'; +import { InterpreterEvaluation } from '../../../../client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation'; +import { IInterpreterSecurityStorage } from '../../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; + +const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.learnMore(), Common.doNotShowAgain()]; + +suite('Interpreter Evaluation', () => { + const resource = Uri.parse('a'); + let applicationShell: Typemoq.IMock<IApplicationShell>; + let browserService: Typemoq.IMock<IBrowserService>; + let interpreterHelper: Typemoq.IMock<IInterpreterHelper>; + let interpreterSecurityStorage: Typemoq.IMock<IInterpreterSecurityStorage>; + let unsafeInterpreterPromptEnabled: Typemoq.IMock<IPersistentState<boolean>>; + let areInterpretersInWorkspaceSafe: Typemoq.IMock<IPersistentState<boolean | undefined>>; + let interpreterEvaluation: InterpreterEvaluation; + setup(() => { + applicationShell = Typemoq.Mock.ofType<IApplicationShell>(); + browserService = Typemoq.Mock.ofType<IBrowserService>(); + interpreterHelper = Typemoq.Mock.ofType<IInterpreterHelper>(); + interpreterSecurityStorage = Typemoq.Mock.ofType<IInterpreterSecurityStorage>(); + unsafeInterpreterPromptEnabled = Typemoq.Mock.ofType<IPersistentState<boolean>>(); + areInterpretersInWorkspaceSafe = Typemoq.Mock.ofType<IPersistentState<boolean | undefined>>(); + interpreterSecurityStorage + .setup((i) => i.hasUserApprovedWorkspaceInterpreters(resource)) + .returns(() => areInterpretersInWorkspaceSafe.object); + interpreterSecurityStorage + .setup((i) => i.unsafeInterpreterPromptEnabled) + .returns(() => unsafeInterpreterPromptEnabled.object); + interpreterSecurityStorage.setup((i) => i.storeKeyForWorkspace(resource)).returns(() => Promise.resolve()); + interpreterEvaluation = new InterpreterEvaluation( + applicationShell.object, + browserService.object, + interpreterHelper.object, + interpreterSecurityStorage.object + ); + }); + + suite('Method evaluateIfInterpreterIsSafe()', () => { + test('If no workspaces are opened, return true', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'interpreterPath' } as any; + interpreterHelper.setup((i) => i.getActiveWorkspaceUri(resource)).returns(() => undefined); + const isSafe = await interpreterEvaluation.evaluateIfInterpreterIsSafe(interpreter, resource); + expect(isSafe).to.equal(true, 'Should be true'); + }); + + test('If method inferValueFromStorage() returns a defined value, return the value', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'interpreterPath' } as any; + interpreterHelper + .setup((i) => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + // tslint:disable-next-line: no-any + interpreterEvaluation.inferValueUsingCurrentState = () => 'storageValue' as any; + const isSafe = await interpreterEvaluation.evaluateIfInterpreterIsSafe(interpreter, resource); + expect(isSafe).to.equal('storageValue'); + }); + + test('If method inferValueFromStorage() returns a undefined value, infer the value using the prompt and return it', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'interpreterPath' } as any; + interpreterHelper + .setup((i) => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + interpreterEvaluation.inferValueUsingCurrentState = () => undefined; + // tslint:disable-next-line: no-any + interpreterEvaluation._inferValueUsingPrompt = () => 'promptValue' as any; + const isSafe = await interpreterEvaluation.evaluateIfInterpreterIsSafe(interpreter, resource); + expect(isSafe).to.equal('promptValue'); + }); + }); + + suite('Method inferValueUsingStorage()', () => { + test('If no workspaces are opened, return true', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'interpreterPath' } as any; + interpreterHelper.setup((i) => i.getActiveWorkspaceUri(resource)).returns(() => undefined); + const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + expect(isSafe).to.equal(true, 'Should be true'); + }); + + test('If interpreter is stored outside the workspace, return true', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'interpreterPath' } as any; + interpreterHelper + .setup((i) => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + expect(isSafe).to.equal(true, 'Should be true'); + }); + + test('If interpreter is stored in the workspace but method _areInterpretersInWorkspaceSafe() returns a defined value, return the value', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: `${resource.fsPath}/interpreterPath` } as any; + interpreterHelper + .setup((i) => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + areInterpretersInWorkspaceSafe + .setup((i) => i.value) + // tslint:disable-next-line: no-any + .returns(() => 'areInterpretersInWorkspaceSafeValue' as any); + const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + expect(isSafe).to.equal('areInterpretersInWorkspaceSafeValue'); + }); + + test('If prompt has been disabled, return true', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: `${resource.fsPath}/interpreterPath` } as any; + interpreterHelper + .setup((i) => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + areInterpretersInWorkspaceSafe.setup((i) => i.value).returns(() => undefined); + unsafeInterpreterPromptEnabled.setup((s) => s.value).returns(() => false); + const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + expect(isSafe).to.equal(true, 'Should be true'); + }); + + test('Otherwise return `undefined`', async () => { + // tslint:disable-next-line: no-any + const interpreter = { path: `${resource.fsPath}/interpreterPath` } as any; + interpreterHelper + .setup((i) => i.getActiveWorkspaceUri(resource)) + .returns( + () => + ({ + folderUri: resource + // tslint:disable-next-line: no-any + } as any) + ); + areInterpretersInWorkspaceSafe.setup((i) => i.value).returns(() => undefined); + unsafeInterpreterPromptEnabled.setup((s) => s.value).returns(() => true); + const isSafe = interpreterEvaluation.inferValueUsingCurrentState(interpreter, resource); + expect(isSafe).to.equal(undefined, 'Should be undefined'); + }); + }); + + suite('Method _inferValueUsingPrompt()', () => { + test('Active workspace key is stored in security storage', async () => { + interpreterSecurityStorage + .setup((i) => i.storeKeyForWorkspace(resource)) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + await interpreterEvaluation._inferValueUsingPrompt(resource); + interpreterSecurityStorage.verifyAll(); + }); + test('If `Learn more` is selected, launch URL & keep showing the prompt again until user clicks some other option', async () => { + let promptDisplayCount = 0; + // Select `Learn more` 2 times, then select something else the 3rd time. + const showInformationMessage = () => { + promptDisplayCount += 1; + return Promise.resolve(promptDisplayCount < 3 ? Common.learnMore() : 'Some other option'); + }; + applicationShell + .setup((a) => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .returns(showInformationMessage) + .verifiable(Typemoq.Times.exactly(3)); + browserService + .setup((b) => b.launch(learnMoreOnInterpreterSecurityURI)) + .returns(() => undefined) + .verifiable(Typemoq.Times.exactly(2)); + + await interpreterEvaluation._inferValueUsingPrompt(resource); + + applicationShell.verifyAll(); + browserService.verifyAll(); + }); + + test('If `No` is selected, update the areInterpretersInWorkspaceSafe storage to unsafe and return false', async () => { + applicationShell + .setup((a) => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelNo())) + .verifiable(Typemoq.Times.once()); + areInterpretersInWorkspaceSafe + .setup((i) => i.updateValue(false)) + .returns(() => Promise.resolve(undefined)) + .verifiable(Typemoq.Times.once()); + + const result = await interpreterEvaluation._inferValueUsingPrompt(resource); + expect(result).to.equal(false, 'Should be false'); + + applicationShell.verifyAll(); + areInterpretersInWorkspaceSafe.verifyAll(); + }); + + test('If `Yes` is selected, update the areInterpretersInWorkspaceSafe storage to safe and return true', async () => { + applicationShell + .setup((a) => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes())) + .verifiable(Typemoq.Times.once()); + areInterpretersInWorkspaceSafe + .setup((i) => i.updateValue(true)) + .returns(() => Promise.resolve(undefined)) + .verifiable(Typemoq.Times.once()); + + const result = await interpreterEvaluation._inferValueUsingPrompt(resource); + expect(result).to.equal(true, 'Should be true'); + + applicationShell.verifyAll(); + areInterpretersInWorkspaceSafe.verifyAll(); + }); + + test('If no selection is made, update the areInterpretersInWorkspaceSafe storage to unsafe and return false', async () => { + applicationShell + .setup((a) => a.showInformationMessage(Interpreters.unsafeInterpreterMessage(), ...prompts)) + .returns(() => Promise.resolve(undefined)) + .verifiable(Typemoq.Times.once()); + areInterpretersInWorkspaceSafe + .setup((i) => i.updateValue(false)) + .returns(() => Promise.resolve(undefined)) + .verifiable(Typemoq.Times.once()); + + const result = await interpreterEvaluation._inferValueUsingPrompt(resource); + expect(result).to.equal(false, 'Should be false'); + + applicationShell.verifyAll(); + areInterpretersInWorkspaceSafe.verifyAll(); + }); + }); +}); diff --git a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts new file mode 100644 index 000000000000..aecb047c260e --- /dev/null +++ b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityService.unit.test.ts @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as Typemoq from 'typemoq'; +import { EventEmitter, Uri } from 'vscode'; +import { IPersistentState } from '../../../../client/common/types'; +import { createDeferred, sleep } from '../../../../client/common/utils/async'; +import { InterpreterSecurityService } from '../../../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService'; +import { + IInterpreterEvaluation, + IInterpreterSecurityStorage +} from '../../../../client/interpreter/autoSelection/types'; + +suite('Interpreter Security service', () => { + const safeInterpretersList = ['safe1', 'safe2']; + const unsafeInterpretersList = ['unsafe1', 'unsafe2']; + const resource = Uri.parse('a'); + let interpreterSecurityStorage: Typemoq.IMock<IInterpreterSecurityStorage>; + let interpreterEvaluation: Typemoq.IMock<IInterpreterEvaluation>; + let unsafeInterpreters: Typemoq.IMock<IPersistentState<string[]>>; + let safeInterpreters: Typemoq.IMock<IPersistentState<string[]>>; + let interpreterSecurityService: InterpreterSecurityService; + setup(() => { + interpreterEvaluation = Typemoq.Mock.ofType<IInterpreterEvaluation>(); + unsafeInterpreters = Typemoq.Mock.ofType<IPersistentState<string[]>>(); + safeInterpreters = Typemoq.Mock.ofType<IPersistentState<string[]>>(); + interpreterSecurityStorage = Typemoq.Mock.ofType<IInterpreterSecurityStorage>(); + safeInterpreters.setup((s) => s.value).returns(() => safeInterpretersList); + unsafeInterpreters.setup((s) => s.value).returns(() => unsafeInterpretersList); + interpreterSecurityStorage.setup((p) => p.unsafeInterpreters).returns(() => unsafeInterpreters.object); + interpreterSecurityStorage.setup((p) => p.safeInterpreters).returns(() => safeInterpreters.object); + interpreterSecurityService = new InterpreterSecurityService( + interpreterSecurityStorage.object, + interpreterEvaluation.object + ); + }); + + suite('Method isSafe()', () => { + test('Returns `true` if interpreter is in the safe interpreters list', () => { + // tslint:disable-next-line: no-any + let isSafe = interpreterSecurityService.isSafe({ path: 'safe1' } as any); + expect(isSafe).to.equal(true, ''); + // tslint:disable-next-line: no-any + isSafe = interpreterSecurityService.isSafe({ path: 'safe2' } as any); + expect(isSafe).to.equal(true, ''); + }); + + test('Returns `false` if interpreter is in the unsafe intepreters list', () => { + // tslint:disable-next-line: no-any + let isSafe = interpreterSecurityService.isSafe({ path: 'unsafe1' } as any); + expect(isSafe).to.equal(false, ''); + // tslint:disable-next-line: no-any + isSafe = interpreterSecurityService.isSafe({ path: 'unsafe2' } as any); + expect(isSafe).to.equal(false, ''); + }); + + test('Returns `undefined` if interpreter is not in either of these lists', () => { + // tslint:disable-next-line: no-any + const interpreter = { path: 'random' } as any; + interpreterEvaluation + .setup((i) => i.inferValueUsingCurrentState(interpreter, resource)) + // tslint:disable-next-line: no-any + .returns(() => 'value' as any) + .verifiable(Typemoq.Times.once()); + const isSafe = interpreterSecurityService.isSafe(interpreter, resource); + expect(isSafe).to.equal('value', ''); + interpreterEvaluation.verifyAll(); + }); + }); + + suite('Method evaluateInterpreterSafety()', () => { + test("If interpreter to be evaluated already exists in the safe intepreters list, simply return and don't evaluate", async () => { + const interpreter = { path: 'safe2' }; + interpreterEvaluation + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .verifiable(Typemoq.Times.never()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + }); + + test("If interpreter to be evaluated already exists in the unsafe intepreters list, simply return and don't evaluate", async () => { + const interpreter = { path: 'unsafe1' }; + interpreterEvaluation + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .verifiable(Typemoq.Times.never()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + }); + + test('If interpreter to be evaluated does not exists in the either of the intepreters list, evaluate the interpreters', async () => { + const interpreter = { path: 'notInEitherLists' }; + interpreterEvaluation + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .verifiable(Typemoq.Times.once()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + }); + + test('If interpreter is evaluated to be safe, add it in the safe interpreters list', async () => { + const interpreter = { path: 'notInEitherLists' }; + interpreterEvaluation + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(Typemoq.Times.once()); + safeInterpreters + .setup((s) => s.updateValue(['notInEitherLists', ...safeInterpretersList])) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + safeInterpreters.verifyAll(); + }); + + test('If interpreter is evaluated to be unsafe, add it in the unsafe interpreters list', async () => { + const interpreter = { path: 'notInEitherLists' }; + interpreterEvaluation + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .returns(() => Promise.resolve(false)) + .verifiable(Typemoq.Times.once()); + unsafeInterpreters + .setup((s) => s.updateValue(['notInEitherLists', ...unsafeInterpretersList])) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + unsafeInterpreters.verifyAll(); + }); + + test('Ensure an event is fired at the end of the method execution', async () => { + const _didSafeInterpretersChange = Typemoq.Mock.ofType<EventEmitter<void>>(); + const interpreter = { path: 'notInEitherLists' }; + interpreterEvaluation + .setup((i) => i.evaluateIfInterpreterIsSafe(Typemoq.It.isAny(), Typemoq.It.isAny())) + .returns(() => Promise.resolve(false)); + unsafeInterpreters + .setup((s) => s.updateValue(['notInEitherLists', ...unsafeInterpretersList])) + .returns(() => Promise.resolve()); + interpreterSecurityService._didSafeInterpretersChange = _didSafeInterpretersChange.object; + _didSafeInterpretersChange + .setup((d) => d.fire()) + .returns(() => undefined) + .verifiable(Typemoq.Times.once()); + // tslint:disable-next-line: no-any + await interpreterSecurityService.evaluateAndRecordInterpreterSafety(interpreter as any, resource); + interpreterEvaluation.verifyAll(); + unsafeInterpreters.verifyAll(); + _didSafeInterpretersChange.verifyAll(); + }); + }); + + test('Ensure onDidChangeSafeInterpreters() method captures the fired event', async () => { + const deferred = createDeferred<true>(); + interpreterSecurityService.onDidChangeSafeInterpreters(() => { + deferred.resolve(true); + }); + interpreterSecurityService._didSafeInterpretersChange.fire(); + const eventCaptured = await Promise.race([deferred.promise, sleep(1000).then(() => false)]); + expect(eventCaptured).to.equal(true, 'Event should be captured'); + }); +}); diff --git a/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts new file mode 100644 index 000000000000..7fd4a4779308 --- /dev/null +++ b/src/test/interpreters/autoSelection/interpreterSecurity/interpreterSecurityStorage.unit.test.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as Typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; +import { Commands } from '../../../../client/common/constants'; +import { IDisposable, IPersistentState, IPersistentStateFactory } from '../../../../client/common/types'; +import { + flaggedWorkspacesKeysStorageKey, + safeInterpretersKey, + unsafeInterpreterPromptKey, + unsafeInterpretersKey +} from '../../../../client/interpreter/autoSelection/constants'; +import { InterpreterSecurityStorage } from '../../../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage'; + +suite('Interpreter Security Storage', () => { + const resource = Uri.parse('a'); + let persistentStateFactory: Typemoq.IMock<IPersistentStateFactory>; + let interpreterSecurityStorage: InterpreterSecurityStorage; + let unsafeInterpreters: Typemoq.IMock<IPersistentState<string[]>>; + let safeInterpreters: Typemoq.IMock<IPersistentState<string[]>>; + let flaggedWorkspacesKeysStorage: Typemoq.IMock<IPersistentState<string[]>>; + let commandManager: Typemoq.IMock<ICommandManager>; + let workspaceService: Typemoq.IMock<IWorkspaceService>; + let areInterpretersInWorkspaceSafe: Typemoq.IMock<IPersistentState<boolean | undefined>>; + let unsafeInterpreterPromptEnabled: Typemoq.IMock<IPersistentState<boolean>>; + setup(() => { + persistentStateFactory = Typemoq.Mock.ofType<IPersistentStateFactory>(); + unsafeInterpreters = Typemoq.Mock.ofType<IPersistentState<string[]>>(); + safeInterpreters = Typemoq.Mock.ofType<IPersistentState<string[]>>(); + flaggedWorkspacesKeysStorage = Typemoq.Mock.ofType<IPersistentState<string[]>>(); + unsafeInterpreterPromptEnabled = Typemoq.Mock.ofType<IPersistentState<boolean>>(); + commandManager = Typemoq.Mock.ofType<ICommandManager>(); + workspaceService = Typemoq.Mock.ofType<IWorkspaceService>(); + areInterpretersInWorkspaceSafe = Typemoq.Mock.ofType<IPersistentState<boolean | undefined>>(); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string[]>(unsafeInterpretersKey, [])) + .returns(() => unsafeInterpreters.object); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string[]>(safeInterpretersKey, [])) + .returns(() => safeInterpreters.object); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState(unsafeInterpreterPromptKey, true)) + .returns(() => unsafeInterpreterPromptEnabled.object); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string[]>(flaggedWorkspacesKeysStorageKey, [])) + .returns(() => flaggedWorkspacesKeysStorage.object); + interpreterSecurityStorage = new InterpreterSecurityStorage( + persistentStateFactory.object, + workspaceService.object, + commandManager.object, + [] + ); + }); + + test('Command is registered in the activate() method', async () => { + commandManager + .setup((c) => c.registerCommand(Commands.ResetInterpreterSecurityStorage, Typemoq.It.isAny())) + .returns(() => Typemoq.Mock.ofType<IDisposable>().object) + .verifiable(Typemoq.Times.once()); + + await interpreterSecurityStorage.activate(); + + commandManager.verifyAll(); + }); + + test('Flagged workspace keys are stored correctly', async () => { + flaggedWorkspacesKeysStorage + .setup((f) => f.value) + .returns(() => ['workspace1Key']) + .verifiable(Typemoq.Times.once()); + const workspace2 = Uri.parse('2'); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(workspace2)).returns(() => workspace2.fsPath); + + const workspace2Key = interpreterSecurityStorage._getKeyForWorkspace(workspace2); + + flaggedWorkspacesKeysStorage + .setup((f) => f.updateValue(['workspace1Key', workspace2Key])) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + + await interpreterSecurityStorage.storeKeyForWorkspace(workspace2); + + expect(workspace2Key).to.equal(`ARE_INTERPRETERS_SAFE_FOR_WS_${workspace2.fsPath}`); + }); + + test('All kinds of storages are cleared upon invoking the command', async () => { + const areInterpretersInWorkspace1Safe = Typemoq.Mock.ofType<IPersistentState<boolean | undefined>>(); + const areInterpretersInWorkspace2Safe = Typemoq.Mock.ofType<IPersistentState<boolean | undefined>>(); + + flaggedWorkspacesKeysStorage.setup((f) => f.value).returns(() => ['workspace1Key', 'workspace2Key']); + safeInterpreters + .setup((s) => s.updateValue([])) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + unsafeInterpreters + .setup((s) => s.updateValue([])) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + unsafeInterpreterPromptEnabled + .setup((s) => s.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<boolean | undefined>('workspace1Key', undefined)) + .returns(() => areInterpretersInWorkspace1Safe.object); + areInterpretersInWorkspace1Safe + .setup((s) => s.updateValue(undefined)) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<boolean | undefined>('workspace2Key', undefined)) + .returns(() => areInterpretersInWorkspace2Safe.object); + areInterpretersInWorkspace2Safe + .setup((s) => s.updateValue(undefined)) + .returns(() => Promise.resolve()) + .verifiable(Typemoq.Times.once()); + + await interpreterSecurityStorage.resetInterpreterSecurityStorage(); + + areInterpretersInWorkspace1Safe.verifyAll(); + areInterpretersInWorkspace2Safe.verifyAll(); + safeInterpreters.verifyAll(); + unsafeInterpreterPromptEnabled.verifyAll(); + unsafeInterpreters.verifyAll(); + }); + + test('Method areInterpretersInWorkspaceSafe() returns the areInterpretersInWorkspaceSafe storage', () => { + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + persistentStateFactory + .setup((p) => + p.createGlobalPersistentState<boolean | undefined>( + `ARE_INTERPRETERS_SAFE_FOR_WS_${resource.fsPath}`, + undefined + ) + ) + .returns(() => areInterpretersInWorkspaceSafe.object); + const result = interpreterSecurityStorage.hasUserApprovedWorkspaceInterpreters(resource); + assert(areInterpretersInWorkspaceSafe.object === result); + }); + + test('Get unsafeInterpreterPromptEnabled() returns the unsafeInterpreterPromptEnabled storage', () => { + const result = interpreterSecurityStorage.unsafeInterpreterPromptEnabled; + assert(unsafeInterpreterPromptEnabled.object === result); + }); + + test('Get unsafeInterpreters() returns the unsafeInterpreters storage', () => { + const result = interpreterSecurityStorage.unsafeInterpreters; + assert(unsafeInterpreters.object === result); + }); + + test('Get safeInterpreters() returns the safeInterpreters storage', () => { + const result = interpreterSecurityStorage.safeInterpreters; + assert(safeInterpreters.object === result); + }); +}); diff --git a/src/test/interpreters/autoSelection/proxy.unit.test.ts b/src/test/interpreters/autoSelection/proxy.unit.test.ts new file mode 100644 index 000000000000..e3292a8506e0 --- /dev/null +++ b/src/test/interpreters/autoSelection/proxy.unit.test.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this no-any + +import { expect } from 'chai'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { InterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/proxy'; +import { IInterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/types'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +suite('Interpreters - Auto Selection Proxy', () => { + class InstanceClass implements IInterpreterAutoSeletionProxyService { + public eventEmitter = new EventEmitter<void>(); + constructor(private readonly pythonPath: string = '') {} + public get onDidChangeAutoSelectedInterpreter(): Event<void> { + return this.eventEmitter.event; + } + public getAutoSelectedInterpreter(_resource: Uri): PythonEnvironment { + return { path: this.pythonPath } as any; + } + public async setWorkspaceInterpreter( + _resource: Uri, + _interpreter: PythonEnvironment | undefined + ): Promise<void> { + return; + } + } + + let proxy: InterpreterAutoSeletionProxyService; + setup(() => { + proxy = new InterpreterAutoSeletionProxyService([] as any); + }); + + test('Change evnet is fired', () => { + const obj = new InstanceClass(); + proxy.registerInstance(obj); + let eventRaised = false; + + proxy.onDidChangeAutoSelectedInterpreter(() => (eventRaised = true)); + proxy.registerInstance(obj); + + obj.eventEmitter.fire(); + + expect(eventRaised).to.be.equal(true, 'Change event not fired'); + }); + + [undefined, Uri.parse('one')].forEach((resource) => { + const suffix = resource ? '(with a resource)' : '(without a resource)'; + + test(`getAutoSelectedInterpreter should return undefined when instance isn't registered ${suffix}`, () => { + expect(proxy.getAutoSelectedInterpreter(resource)).to.be.equal(undefined, 'Should be undefined'); + }); + test(`getAutoSelectedInterpreter should invoke instance method when instance isn't registered ${suffix}`, () => { + const pythonPath = 'some python path'; + proxy.registerInstance(new InstanceClass(pythonPath)); + + const value = proxy.getAutoSelectedInterpreter(resource); + + expect(value).to.be.deep.equal({ path: pythonPath }); + }); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/base.unit.test.ts b/src/test/interpreters/autoSelection/rules/base.unit.test.ts new file mode 100644 index 000000000000..28166c4fce64 --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/base.unit.test.ts @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { BaseRuleService, NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { CurrentPathInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/currentPath'; +import { + AutoSelectionRule, + IInterpreterAutoSelectionService +} from '../../../../client/interpreter/autoSelection/types'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +suite('Interpreters - Auto Selection - Base Rule', () => { + let rule: BaseRuleServiceTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState<PythonEnvironment | undefined>; + class BaseRuleServiceTest extends BaseRuleService { + public async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise<void> { + return super.next(resource, manager); + } + public async cacheSelectedInterpreter(resource: Resource, interpreter: PythonEnvironment | undefined) { + return super.cacheSelectedInterpreter(resource, interpreter); + } + public async setGlobalInterpreter( + interpreter?: PythonEnvironment, + manager?: IInterpreterAutoSelectionService + ): Promise<boolean> { + return super.setGlobalInterpreter(interpreter, manager); + } + protected async onAutoSelectInterpreter( + _resource: Uri, + _manager?: IInterpreterAutoSelectionService + ): Promise<NextAction> { + return NextAction.runNextRule; + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + when(stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>(anything(), undefined)).thenReturn( + instance(state) + ); + rule = new BaseRuleServiceTest(AutoSelectionRule.cachedInterpreters, instance(fs), instance(stateFactory)); + }); + + test('State store is created', () => { + verify( + stateFactory.createGlobalPersistentState( + `InterpreterAutoSeletionRule-${AutoSelectionRule.cachedInterpreters}`, + undefined + ) + ).once(); + }); + test('Next rule should be invoked', async () => { + const nextRule = mock(CurrentPathInterpretersAutoSelectionRule); + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.parse('x'); + + rule.setNextRule(instance(nextRule)); + await rule.next(resource, manager); + + verify( + stateFactory.createGlobalPersistentState( + `InterpreterAutoSeletionRule-${AutoSelectionRule.cachedInterpreters}`, + undefined + ) + ).once(); + verify(nextRule.autoSelectInterpreter(resource, manager)).once(); + }); + test('Next rule should not be invoked', async () => { + const nextRule = mock(CurrentPathInterpretersAutoSelectionRule); + const resource = Uri.parse('x'); + + rule.setNextRule(instance(nextRule)); + await rule.next(resource); + + verify( + stateFactory.createGlobalPersistentState( + `InterpreterAutoSeletionRule-${AutoSelectionRule.cachedInterpreters}`, + undefined + ) + ).once(); + verify(nextRule.autoSelectInterpreter(anything(), anything())).never(); + }); + test('State store must be updated', async () => { + const resource = Uri.parse('x'); + const interpreterInfo = { x: '1324' } as any; + when(state.updateValue(anything())).thenResolve(); + + await rule.cacheSelectedInterpreter(resource, interpreterInfo); + + verify(state.updateValue(interpreterInfo)).once(); + }); + test('State store must be cleared when file does not exist', async () => { + const resource = Uri.parse('x'); + const interpreterInfo = { path: '1324' } as any; + when(state.value).thenReturn(interpreterInfo); + when(state.updateValue(anything())).thenResolve(); + when(fs.fileExists(interpreterInfo.path)).thenResolve(false); + + await rule.autoSelectInterpreter(resource); + + verify(state.value).atLeast(1); + verify(state.updateValue(undefined)).once(); + verify(fs.fileExists(interpreterInfo.path)).once(); + }); + test('State store must not be cleared when file exists', async () => { + const resource = Uri.parse('x'); + const interpreterInfo = { path: '1324' } as any; + when(state.value).thenReturn(interpreterInfo); + when(state.updateValue(anything())).thenResolve(); + when(fs.fileExists(interpreterInfo.path)).thenResolve(true); + + await rule.autoSelectInterpreter(resource); + + verify(state.value).atLeast(1); + verify(state.updateValue(anything())).never(); + verify(fs.fileExists(interpreterInfo.path)).once(); + }); + test("Get undefined if there's nothing in state store", async () => { + when(state.value).thenReturn(undefined); + + expect(rule.getPreviouslyAutoSelectedInterpreter(Uri.parse('x'))).to.be.equal(undefined, 'Must be undefined'); + + verify(state.value).atLeast(1); + }); + test('Get value from state store', async () => { + const stateStoreValue = 'x'; + when(state.value).thenReturn(stateStoreValue as any); + + expect(rule.getPreviouslyAutoSelectedInterpreter(Uri.parse('x'))).to.be.equal(stateStoreValue); + + verify(state.value).atLeast(1); + }); + test('setGlobalInterpreter should do nothing if interpreter is undefined or version is empty', async () => { + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1324' } as any; + + const result1 = await rule.setGlobalInterpreter(undefined, instance(manager)); + const result2 = await rule.setGlobalInterpreter(interpreterInfo, instance(manager)); + + verify(manager.setGlobalInterpreter(anything())).never(); + assert.equal(result1, false); + assert.equal(result2, false); + }); + test('setGlobalInterpreter should not update manager if interpreter is not better than one stored in manager', async () => { + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1324', version: new SemVer('1.0.0') } as any; + const interpreterInfoInManager = { path: '2', version: new SemVer('2.0.0') } as any; + when(manager.getAutoSelectedInterpreter(undefined)).thenReturn(interpreterInfoInManager); + + const result = await rule.setGlobalInterpreter(interpreterInfo, instance(manager)); + + verify(manager.getAutoSelectedInterpreter(undefined)).once(); + verify(manager.setGlobalInterpreter(anything())).never(); + assert.equal(result, false); + }); + test('setGlobalInterpreter should update manager if interpreter is better than one stored in manager', async () => { + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1324', version: new SemVer('3.0.0') } as any; + const interpreterInfoInManager = { path: '2', version: new SemVer('2.0.0') } as any; + when(manager.getAutoSelectedInterpreter(undefined)).thenReturn(interpreterInfoInManager); + + const result = await rule.setGlobalInterpreter(interpreterInfo, instance(manager)); + + verify(manager.getAutoSelectedInterpreter(undefined)).once(); + verify(manager.setGlobalInterpreter(anything())).once(); + assert.equal(result, true); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/cached.unit.test.ts b/src/test/interpreters/autoSelection/rules/cached.unit.test.ts new file mode 100644 index 000000000000..65468375f7ac --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/cached.unit.test.ts @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { CachedInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/cached'; +import { SystemWideInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/system'; +import { + IInterpreterAutoSelectionRule, + IInterpreterAutoSelectionService +} from '../../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../../client/interpreter/helpers'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +suite('Interpreters - Auto Selection - Cached Rule', () => { + let rule: CachedInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState<PythonEnvironment | undefined>; + let systemInterpreter: IInterpreterAutoSelectionRule; + let currentPathInterpreter: IInterpreterAutoSelectionRule; + let winRegInterpreter: IInterpreterAutoSelectionRule; + let helper: IInterpreterHelper; + class CachedInterpretersAutoSelectionRuleTest extends CachedInterpretersAutoSelectionRule { + public readonly rules!: IInterpreterAutoSelectionRule[]; + public async setGlobalInterpreter( + interpreter?: PythonEnvironment, + manager?: IInterpreterAutoSelectionService + ): Promise<boolean> { + return super.setGlobalInterpreter(interpreter, manager); + } + public async onAutoSelectInterpreter( + resource: Resource, + manager?: IInterpreterAutoSelectionService + ): Promise<NextAction> { + return super.onAutoSelectInterpreter(resource, manager); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + helper = mock(InterpreterHelper); + systemInterpreter = mock(SystemWideInterpretersAutoSelectionRule); + currentPathInterpreter = mock(SystemWideInterpretersAutoSelectionRule); + winRegInterpreter = mock(SystemWideInterpretersAutoSelectionRule); + + when(stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>(anything(), undefined)).thenReturn( + instance(state) + ); + rule = new CachedInterpretersAutoSelectionRuleTest( + instance(fs), + instance(helper), + instance(stateFactory), + instance(systemInterpreter), + instance(currentPathInterpreter), + instance(winRegInterpreter) + ); + }); + + test('Invoke next rule if there are no cached interpreters', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + + when(systemInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).thenReturn(undefined); + when(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).thenReturn(undefined); + when(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).thenReturn(undefined); + + const nextAction = await rule.onAutoSelectInterpreter(resource, manager); + + verify(systemInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).once(); + verify(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).once(); + verify(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('Invoke next rule if fails to update global state', async () => { + const manager = mock(InterpreterAutoSelectionService); + const winRegInterpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const resource = Uri.file('x'); + + when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(winRegInterpreterInfo); + when(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); + when(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); + when(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(winRegInterpreterInfo); + + const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); + moq.callBase = true; + moq.setup((m) => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(false)) + .verifiable(typemoq.Times.once()); + + const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); + + verify(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + verify(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + verify(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + moq.verifyAll(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('Must not Invoke next rule if updating global state is successful', async () => { + const manager = mock(InterpreterAutoSelectionService); + const winRegInterpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const resource = Uri.file('x'); + + when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(winRegInterpreterInfo); + when(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); + when(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); + when(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(winRegInterpreterInfo); + + const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); + moq.callBase = true; + moq.setup((m) => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + + const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); + + verify(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + verify(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + verify(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); + moq.verifyAll(); + expect(nextAction).to.be.equal(NextAction.exit); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/currentPath.unit.test.ts b/src/test/interpreters/autoSelection/rules/currentPath.unit.test.ts new file mode 100644 index 000000000000..c5717aa90804 --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/currentPath.unit.test.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { CurrentPathInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/currentPath'; +import { IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper, IInterpreterLocatorService } from '../../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../../client/interpreter/helpers'; +import { KnownPathsService } from '../../../../client/pythonEnvironments/discovery/locators/services/KnownPathsService'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +suite('Interpreters - Auto Selection - Current Path Rule', () => { + let rule: CurrentPathInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState<PythonEnvironment | undefined>; + let locator: IInterpreterLocatorService; + let helper: IInterpreterHelper; + class CurrentPathInterpretersAutoSelectionRuleTest extends CurrentPathInterpretersAutoSelectionRule { + public async setGlobalInterpreter( + interpreter?: PythonEnvironment, + manager?: IInterpreterAutoSelectionService + ): Promise<boolean> { + return super.setGlobalInterpreter(interpreter, manager); + } + public async onAutoSelectInterpreter( + resource: Resource, + manager?: IInterpreterAutoSelectionService + ): Promise<NextAction> { + return super.onAutoSelectInterpreter(resource, manager); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + helper = mock(InterpreterHelper); + locator = mock(KnownPathsService); + + when(stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>(anything(), undefined)).thenReturn( + instance(state) + ); + rule = new CurrentPathInterpretersAutoSelectionRuleTest( + instance(fs), + instance(helper), + instance(stateFactory), + instance(locator) + ); + }); + + test('Invoke next rule if there are no interpreters in the current path', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + + when(locator.getInterpreters(resource)).thenResolve([]); + + const nextAction = await rule.onAutoSelectInterpreter(resource, manager); + + verify(locator.getInterpreters(resource)).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('Invoke next rule if fails to update global state', async () => { + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const resource = Uri.file('x'); + + when(helper.getBestInterpreter(anything())).thenReturn(interpreterInfo); + when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); + + const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); + moq.callBase = true; + moq.setup((m) => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(false)) + .verifiable(typemoq.Times.once()); + + const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); + + moq.verifyAll(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('Not Invoke next rule if succeeds to update global state', async () => { + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const resource = Uri.file('x'); + + when(helper.getBestInterpreter(anything())).thenReturn(interpreterInfo); + when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); + + const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); + moq.callBase = true; + moq.setup((m) => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + + const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); + + moq.verifyAll(); + expect(nextAction).to.be.equal(NextAction.exit); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/settings.unit.test.ts b/src/test/interpreters/autoSelection/rules/settings.unit.test.ts new file mode 100644 index 000000000000..eb1d2b33af56 --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/settings.unit.test.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import { expect } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { DeprecatePythonPath } from '../../../../client/common/experiments/groups'; +import { ExperimentsManager } from '../../../../client/common/experiments/manager'; +import { InterpreterPathService } from '../../../../client/common/interpreterPathService'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { + IExperimentsManager, + IInterpreterPathService, + IPersistentStateFactory, + Resource +} from '../../../../client/common/types'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { SettingsInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/settings'; +import { IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +suite('Interpreters - Auto Selection - Settings Rule', () => { + let rule: SettingsInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState<PythonEnvironment | undefined>; + let workspaceService: IWorkspaceService; + let experimentsManager: IExperimentsManager; + let interpreterPathService: IInterpreterPathService; + class SettingsInterpretersAutoSelectionRuleTest extends SettingsInterpretersAutoSelectionRule { + public async onAutoSelectInterpreter( + resource: Resource, + manager?: IInterpreterAutoSelectionService + ): Promise<NextAction> { + return super.onAutoSelectInterpreter(resource, manager); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + workspaceService = mock(WorkspaceService); + experimentsManager = mock(ExperimentsManager); + interpreterPathService = mock(InterpreterPathService); + when(experimentsManager.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).thenReturn(undefined); + + when(stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>(anything(), undefined)).thenReturn( + instance(state) + ); + rule = new SettingsInterpretersAutoSelectionRuleTest( + instance(fs), + instance(stateFactory), + instance(workspaceService), + instance(experimentsManager), + instance(interpreterPathService) + ); + }); + + test('If in experiment, invoke next rule if python Path in user settings is default', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = {}; + + when(experimentsManager.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(interpreterPathService.inspect(undefined)).thenReturn(pythonPathInConfig as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('If in experiment, invoke next rule if python Path in user settings is not defined', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = { globalValue: 'python' }; + + when(experimentsManager.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(interpreterPathService.inspect(undefined)).thenReturn(pythonPathInConfig as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('If in experiment, must not Invoke next rule if python Path in user settings is not default', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = { globalValue: 'something else' }; + + when(experimentsManager.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(interpreterPathService.inspect(undefined)).thenReturn(pythonPathInConfig as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.exit); + }); + + test('If not in experiment, invoke next rule if python Path in user settings is default', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = {}; + const pythonPath = { inspect: () => pythonPathInConfig }; + + when(workspaceService.getConfiguration('python', null as any)).thenReturn(pythonPath as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('If not in experiment, invoke next rule if python Path in user settings is not defined', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = { globalValue: 'python' }; + const pythonPath = { inspect: () => pythonPathInConfig }; + + when(workspaceService.getConfiguration('python', null as any)).thenReturn(pythonPath as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + test('If not in experiment, must not Invoke next rule if python Path in user settings is not default', async () => { + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = { globalValue: 'something else' }; + const pythonPath = { inspect: () => pythonPathInConfig }; + + when(workspaceService.getConfiguration('python', null as any)).thenReturn(pythonPath as any); + + const nextAction = await rule.onAutoSelectInterpreter(undefined, manager); + + expect(nextAction).to.be.equal(NextAction.exit); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/system.unit.test.ts b/src/test/interpreters/autoSelection/rules/system.unit.test.ts new file mode 100644 index 000000000000..f3777f19cdb3 --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/system.unit.test.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { SystemWideInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/system'; +import { IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper, IInterpreterService } from '../../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../../client/interpreter/helpers'; +import { InterpreterService } from '../../../../client/interpreter/interpreterService'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +suite('Interpreters - Auto Selection - System Interpreters Rule', () => { + let rule: SystemWideInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState<PythonEnvironment | undefined>; + let interpreterService: IInterpreterService; + let helper: IInterpreterHelper; + class SystemWideInterpretersAutoSelectionRuleTest extends SystemWideInterpretersAutoSelectionRule { + public async setGlobalInterpreter( + interpreter?: PythonEnvironment, + manager?: IInterpreterAutoSelectionService + ): Promise<boolean> { + return super.setGlobalInterpreter(interpreter, manager); + } + public async onAutoSelectInterpreter( + resource: Resource, + manager?: IInterpreterAutoSelectionService + ): Promise<NextAction> { + return super.onAutoSelectInterpreter(resource, manager); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + helper = mock(InterpreterHelper); + interpreterService = mock(InterpreterService); + + when(stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>(anything(), undefined)).thenReturn( + instance(state) + ); + rule = new SystemWideInterpretersAutoSelectionRuleTest( + instance(fs), + instance(helper), + instance(stateFactory), + instance(interpreterService) + ); + }); + + test('Invoke next rule if there are no interpreters in the current path', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + when(interpreterService.getInterpreters(resource)).thenResolve([]); + when(helper.getBestInterpreter(deepEqual([]))).thenReturn(undefined); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + assert.equal(res, undefined); + return Promise.resolve(false); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, manager); + + verify(interpreterService.getInterpreters(resource)).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); + test('Invoke next rule if there interpreters in the current path but update fails', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + when(interpreterService.getInterpreters(resource)).thenResolve([interpreterInfo]); + when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + expect(res).deep.equal(interpreterInfo); + return Promise.resolve(false); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, manager); + + verify(interpreterService.getInterpreters(resource)).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); + test('Do not Invoke next rule if there interpreters in the current path and update does not fail', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + when(interpreterService.getInterpreters(resource)).thenResolve([interpreterInfo]); + when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + expect(res).deep.equal(interpreterInfo); + return Promise.resolve(true); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, manager); + + verify(interpreterService.getInterpreters(resource)).once(); + expect(nextAction).to.be.equal(NextAction.exit); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/winRegistry.unit.test.ts b/src/test/interpreters/autoSelection/rules/winRegistry.unit.test.ts new file mode 100644 index 000000000000..9fa20cc74233 --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/winRegistry.unit.test.ts @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory, Resource } from '../../../../client/common/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { OSType } from '../../../../client/common/utils/platform'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { NextAction } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { WindowsRegistryInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/winRegistry'; +import { IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper, IInterpreterLocatorService } from '../../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../../client/interpreter/helpers'; +import { WindowsRegistryService } from '../../../../client/pythonEnvironments/discovery/locators/services/windowsRegistryService'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +suite('Interpreters - Auto Selection - Windows Registry Rule', () => { + let rule: WindowsRegistryInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState<PythonEnvironment | undefined>; + let locator: IInterpreterLocatorService; + let platform: IPlatformService; + let helper: IInterpreterHelper; + class WindowsRegistryInterpretersAutoSelectionRuleTest extends WindowsRegistryInterpretersAutoSelectionRule { + public async setGlobalInterpreter( + interpreter?: PythonEnvironment, + manager?: IInterpreterAutoSelectionService + ): Promise<boolean> { + return super.setGlobalInterpreter(interpreter, manager); + } + public async onAutoSelectInterpreter( + resource: Resource, + manager?: IInterpreterAutoSelectionService + ): Promise<NextAction> { + return super.onAutoSelectInterpreter(resource, manager); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + helper = mock(InterpreterHelper); + locator = mock(WindowsRegistryService); + platform = mock(PlatformService); + + when(stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>(anything(), undefined)).thenReturn( + instance(state) + ); + rule = new WindowsRegistryInterpretersAutoSelectionRuleTest( + instance(fs), + instance(helper), + instance(stateFactory), + instance(platform), + instance(locator) + ); + }); + + getNamesAndValues<OSType>(OSType).forEach((osType) => { + test(`Invoke next rule if platform is not windows (${osType.name})`, async function () { + const manager = mock(InterpreterAutoSelectionService); + if (osType.value === OSType.Windows) { + return this.skip(); + } + const resource = Uri.file('x'); + when(platform.osType).thenReturn(osType.value); + + const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); + + verify(platform.osType).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + }); + }); + test('Invoke next rule if there are no interpreters in the registry', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + when(platform.osType).thenReturn(OSType.Windows); + when(locator.getInterpreters(resource)).thenResolve([]); + when(helper.getBestInterpreter(deepEqual([]))).thenReturn(undefined); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + assert.equal(res, undefined); + return Promise.resolve(false); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); + + verify(locator.getInterpreters(resource)).once(); + verify(platform.osType).once(); + verify(helper.getBestInterpreter(deepEqual([]))).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); + test('Invoke next rule if there are interpreters in the registry and update fails', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + when(platform.osType).thenReturn(OSType.Windows); + when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); + when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + expect(res).to.deep.equal(interpreterInfo); + return Promise.resolve(false); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); + + verify(locator.getInterpreters(resource)).once(); + verify(platform.osType).once(); + verify(helper.getBestInterpreter(deepEqual([interpreterInfo]))).once(); + expect(nextAction).to.be.equal(NextAction.runNextRule); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); + test('Do not Invoke next rule if there are interpreters in the registry and update does not fail', async () => { + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + let setGlobalInterpreterInvoked = false; + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + when(platform.osType).thenReturn(OSType.Windows); + when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); + when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); + rule.setGlobalInterpreter = async (res: any) => { + setGlobalInterpreterInvoked = true; + expect(res).to.deep.equal(interpreterInfo); + return Promise.resolve(true); + }; + + const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); + + verify(locator.getInterpreters(resource)).once(); + verify(platform.osType).once(); + verify(helper.getBestInterpreter(deepEqual([interpreterInfo]))).once(); + expect(nextAction).to.be.equal(NextAction.exit); + expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); + }); +}); diff --git a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts new file mode 100644 index 000000000000..62015371b020 --- /dev/null +++ b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts @@ -0,0 +1,325 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unnecessary-override no-any max-func-body-length no-invalid-this + +import { expect } from 'chai'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { DeprecatePythonPath } from '../../../../client/common/experiments/groups'; +import { ExperimentsManager } from '../../../../client/common/experiments/manager'; +import { InterpreterPathService } from '../../../../client/common/interpreterPathService'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; +import { + IExperimentsManager, + IInterpreterPathService, + IPersistentStateFactory, + Resource +} from '../../../../client/common/types'; +import { createDeferred } from '../../../../client/common/utils/async'; +import { OSType } from '../../../../client/common/utils/platform'; +import { InterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection'; +import { BaseRuleService } from '../../../../client/interpreter/autoSelection/rules/baseRule'; +import { WorkspaceVirtualEnvInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/workspaceEnv'; +import { IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper, IInterpreterLocatorService } from '../../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../../client/interpreter/helpers'; +import { KnownPathsService } from '../../../../client/pythonEnvironments/discovery/locators/services/KnownPathsService'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { + type PythonPathInConfig = { workspaceFolderValue: string; workspaceValue: string }; + let rule: WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest; + let stateFactory: IPersistentStateFactory; + let fs: IFileSystem; + let state: PersistentState<PythonEnvironment | undefined>; + let helper: IInterpreterHelper; + let platform: IPlatformService; + let virtualEnvLocator: IInterpreterLocatorService; + let workspaceService: IWorkspaceService; + let experimentsManager: IExperimentsManager; + let interpreterPathService: IInterpreterPathService; + class WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest extends WorkspaceVirtualEnvInterpretersAutoSelectionRule { + public async setGlobalInterpreter( + interpreter?: PythonEnvironment, + manager?: IInterpreterAutoSelectionService + ): Promise<boolean> { + return super.setGlobalInterpreter(interpreter, manager); + } + public async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise<void> { + return super.next(resource, manager); + } + public async cacheSelectedInterpreter(resource: Resource, interpreter: PythonEnvironment | undefined) { + return super.cacheSelectedInterpreter(resource, interpreter); + } + public async getWorkspaceVirtualEnvInterpreters(resource: Resource): Promise<PythonEnvironment[] | undefined> { + return super.getWorkspaceVirtualEnvInterpreters(resource); + } + } + setup(() => { + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState); + fs = mock(FileSystem); + helper = mock(InterpreterHelper); + platform = mock(PlatformService); + workspaceService = mock(WorkspaceService); + virtualEnvLocator = mock(KnownPathsService); + experimentsManager = mock(ExperimentsManager); + interpreterPathService = mock(InterpreterPathService); + + when(stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>(anything(), undefined)).thenReturn( + instance(state) + ); + rule = new WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest( + instance(fs), + instance(helper), + instance(stateFactory), + instance(platform), + instance(workspaceService), + instance(virtualEnvLocator), + instance(experimentsManager), + instance(interpreterPathService) + ); + }); + test('Invoke next rule if there is no workspace', async () => { + const nextRule = mock(BaseRuleService); + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + + rule.setNextRule(nextRule); + when(platform.osType).thenReturn(OSType.OSX); + when(helper.getActiveWorkspaceUri(anything())).thenReturn(undefined); + when(nextRule.autoSelectInterpreter(resource, manager)).thenResolve(); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(resource, manager); + + verify(nextRule.autoSelectInterpreter(resource, manager)).once(); + verify(helper.getActiveWorkspaceUri(anything())).once(); + }); + test('Invoke next rule if resource is undefined', async () => { + const nextRule = mock(BaseRuleService); + const manager = mock(InterpreterAutoSelectionService); + + rule.setNextRule(nextRule); + when(platform.osType).thenReturn(OSType.OSX); + when(helper.getActiveWorkspaceUri(anything())).thenReturn(undefined); + when(nextRule.autoSelectInterpreter(undefined, manager)).thenResolve(); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(undefined, manager); + + verify(nextRule.autoSelectInterpreter(undefined, manager)).once(); + verify(helper.getActiveWorkspaceUri(anything())).once(); + }); + test('Invoke next rule if user has defined a python path in settings', async () => { + const nextRule = mock(BaseRuleService); + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = typemoq.Mock.ofType<PythonPathInConfig>(); + const pythonPathValue = 'Hello there.exe'; + pythonPathInConfig + .setup((p) => p.workspaceFolderValue) + .returns(() => pythonPathValue) + .verifiable(typemoq.Times.once()); + + const pythonPath = { inspect: () => pythonPathInConfig.object }; + const folderUri = Uri.parse('Folder'); + const someUri = Uri.parse('somethign'); + + rule.setNextRule(nextRule); + when(platform.osType).thenReturn(OSType.OSX); + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); + when(nextRule.autoSelectInterpreter(someUri, manager)).thenResolve(); + when(workspaceService.getConfiguration('python', folderUri)).thenReturn(pythonPath as any); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(someUri, manager); + + verify(nextRule.autoSelectInterpreter(someUri, manager)).once(); + verify(helper.getActiveWorkspaceUri(anything())).once(); + pythonPathInConfig.verifyAll(); + }); + test('If in experiment, use new API to fetch settings', async () => { + const nextRule = mock(BaseRuleService); + const manager = mock(InterpreterAutoSelectionService); + const pythonPathInConfig = typemoq.Mock.ofType<PythonPathInConfig>(); + const pythonPathValue = 'Hello there.exe'; + pythonPathInConfig + .setup((p) => p.workspaceFolderValue) + .returns(() => pythonPathValue) + .verifiable(typemoq.Times.once()); + + const pythonPath = { inspect: () => pythonPathInConfig.object }; + const folderUri = Uri.parse('Folder'); + const someUri = Uri.parse('somethign'); + + rule.setNextRule(nextRule); + when(platform.osType).thenReturn(OSType.OSX); + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); + when(nextRule.autoSelectInterpreter(someUri, manager)).thenResolve(); + when(workspaceService.getConfiguration('python', folderUri)).thenReturn(pythonPath as any); + when(experimentsManager.inExperiment(DeprecatePythonPath.experiment)).thenReturn(true); + when(experimentsManager.sendTelemetryIfInExperiment(DeprecatePythonPath.control)).thenReturn(undefined); + when(interpreterPathService.inspect(folderUri)).thenReturn(pythonPathInConfig.object); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(someUri, manager); + + verify(nextRule.autoSelectInterpreter(someUri, manager)).once(); + verify(helper.getActiveWorkspaceUri(anything())).once(); + verify(interpreterPathService.inspect(folderUri)).once(); + pythonPathInConfig.verifyAll(); + }); + test('getWorkspaceVirtualEnvInterpreters will not return any interpreters if there is no workspace ', async () => { + let envs = await rule.getWorkspaceVirtualEnvInterpreters(undefined); + expect(envs || []).to.be.lengthOf(0); + + const resource = Uri.file('x'); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(undefined); + envs = await rule.getWorkspaceVirtualEnvInterpreters(resource); + expect(envs || []).to.be.lengthOf(0); + }); + test('getWorkspaceVirtualEnvInterpreters will not return any interpreters if interpreters are not in workspace folder (windows)', async () => { + const folderPath = path.join('one', 'two', 'three'); + const interpreter1 = { path: path.join('one', 'two', 'bin', 'python.exe') }; + const folderUri = Uri.file(folderPath); + const workspaceFolder: WorkspaceFolder = { name: '', index: 0, uri: folderUri }; + const resource = Uri.file('x'); + const options = { ignoreCache: true }; + + when(virtualEnvLocator.getInterpreters(resource, deepEqual(options))).thenResolve([interpreter1 as any]); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when(platform.osType).thenReturn(OSType.Windows); + + const envs = await rule.getWorkspaceVirtualEnvInterpreters(resource); + expect(envs || []).to.be.lengthOf(0); + }); + test('getWorkspaceVirtualEnvInterpreters will return workspace related virtual interpreters (windows)', async () => { + const folderPath = path.join('one', 'two', 'three'); + const interpreter1 = { path: path.join('one', 'two', 'bin', 'python.exe') }; + const interpreter2 = { path: path.join(folderPath, 'venv', 'bin', 'python.exe') }; + const interpreter3 = { path: path.join(path.join('one', 'two', 'THREE'), 'venv', 'bin', 'python.exe') }; + const folderUri = Uri.file(folderPath); + const workspaceFolder: WorkspaceFolder = { name: '', index: 0, uri: folderUri }; + const resource = Uri.file('x'); + const options = { ignoreCache: true }; + + when(virtualEnvLocator.getInterpreters(resource, deepEqual(options))).thenResolve([ + interpreter1, + interpreter2, + interpreter3 + ] as any); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when(platform.osType).thenReturn(OSType.Windows); + + const envs = await rule.getWorkspaceVirtualEnvInterpreters(resource); + expect(envs).to.be.deep.equal([interpreter2, interpreter3]); + }); + [OSType.OSX, OSType.Linux].forEach((osType) => { + test(`getWorkspaceVirtualEnvInterpreters will not return any interpreters if interpreters are not in workspace folder (${osType})`, async () => { + const folderPath = path.join('one', 'two', 'three'); + const interpreter1 = { path: path.join('one', 'two', 'bin', 'python.exe') }; + const folderUri = Uri.file(folderPath); + const workspaceFolder: WorkspaceFolder = { name: '', index: 0, uri: folderUri }; + const resource = Uri.file('x'); + const options = { ignoreCache: true }; + + when(virtualEnvLocator.getInterpreters(resource, deepEqual(options))).thenResolve([interpreter1 as any]); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when(platform.osType).thenReturn(osType); + + const envs = await rule.getWorkspaceVirtualEnvInterpreters(resource); + expect(envs || []).to.be.lengthOf(0); + }); + test(`getWorkspaceVirtualEnvInterpreters will return workspace related virtual interpreters (${osType})`, async () => { + const folderPath = path.join('one', 'two', 'three'); + const interpreter1 = { path: path.join('one', 'two', 'bin', 'python.exe') }; + const interpreter2 = { path: path.join(folderPath, 'venv', 'bin', 'python.exe') }; + const interpreter3 = { path: path.join(path.join('one', 'two', 'THREE'), 'venv', 'bin', 'python.exe') }; + const folderUri = Uri.file(folderPath); + const workspaceFolder: WorkspaceFolder = { name: '', index: 0, uri: folderUri }; + const resource = Uri.file('x'); + const options = { ignoreCache: true }; + + when(virtualEnvLocator.getInterpreters(resource, deepEqual(options))).thenResolve([ + interpreter1, + interpreter2, + interpreter3 + ] as any); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when(platform.osType).thenReturn(osType); + + const envs = await rule.getWorkspaceVirtualEnvInterpreters(resource); + expect(envs).to.be.deep.equal([interpreter2]); + }); + }); + test('Invoke next rule if there is no workspace', async () => { + const nextRule = mock(BaseRuleService); + const manager = mock(InterpreterAutoSelectionService); + const resource = Uri.file('x'); + + when(nextRule.autoSelectInterpreter(resource, manager)).thenResolve(); + when(helper.getActiveWorkspaceUri(resource)).thenReturn(undefined); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(resource, manager); + + verify(nextRule.autoSelectInterpreter(resource, manager)).once(); + verify(helper.getActiveWorkspaceUri(resource)).once(); + }); + test('Invoke next rule if there is no resouece', async () => { + const nextRule = mock(BaseRuleService); + const manager = mock(InterpreterAutoSelectionService); + + when(nextRule.autoSelectInterpreter(undefined, manager)).thenResolve(); + when(helper.getActiveWorkspaceUri(undefined)).thenReturn(undefined); + + rule.setNextRule(instance(nextRule)); + await rule.autoSelectInterpreter(undefined, manager); + + verify(nextRule.autoSelectInterpreter(undefined, manager)).once(); + verify(helper.getActiveWorkspaceUri(undefined)).once(); + }); + test('Use virtualEnv if that completes with results', async () => { + const folderUri = Uri.parse('Folder'); + const pythonPathInConfig = typemoq.Mock.ofType<PythonPathInConfig>(); + const pythonPath = { inspect: () => pythonPathInConfig.object }; + pythonPathInConfig + .setup((p) => p.workspaceFolderValue) + .returns(() => undefined as any) + .verifiable(typemoq.Times.once()); + pythonPathInConfig + .setup((p) => p.workspaceValue) + .returns(() => undefined as any) + .verifiable(typemoq.Times.once()); + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); + when(workspaceService.getConfiguration('python', folderUri)).thenReturn(pythonPath as any); + + const resource = Uri.file('x'); + const manager = mock(InterpreterAutoSelectionService); + const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; + const nextInvoked = createDeferred(); + + rule.next = () => Promise.resolve(nextInvoked.resolve()); + rule.getWorkspaceVirtualEnvInterpreters = () => Promise.resolve([interpreterInfo]); + when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); + + rule.cacheSelectedInterpreter = () => Promise.resolve(); + + await rule.autoSelectInterpreter(resource, instance(manager)); + + expect(nextInvoked.completed).to.be.equal(true, 'Next rule not invoked'); + verify(helper.getActiveWorkspaceUri(resource)).atLeast(1); + verify(manager.setWorkspaceInterpreter(folderUri, interpreterInfo)).once(); + }); +}); diff --git a/src/test/interpreters/currentPathService.unit.test.ts b/src/test/interpreters/currentPathService.unit.test.ts new file mode 100644 index 000000000000..491f5b062ae3 --- /dev/null +++ b/src/test/interpreters/currentPathService.unit.test.ts @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; +import { + IConfigurationService, + IPersistentState, + IPersistentStateFactory, + IPythonSettings +} from '../../client/common/types'; +import { OSType } from '../../client/common/utils/platform'; +import { IInterpreterVersionService } from '../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; +import { IPythonInPathCommandProvider } from '../../client/interpreter/locators/types'; +import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { + CurrentPathService, + PythonInPathCommandProvider +} from '../../client/pythonEnvironments/discovery/locators/services/currentPathService'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; + +const isolated = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'pyvsc-run-isolated.py'); + +suite('Interpreters CurrentPath Service', () => { + let processService: TypeMoq.IMock<IProcessService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let virtualEnvironmentManager: TypeMoq.IMock<IVirtualEnvironmentManager>; + let interpreterHelper: TypeMoq.IMock<InterpreterHelper>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let currentPathService: CurrentPathService; + let persistentState: TypeMoq.IMock<IPersistentState<PythonEnvironment[]>>; + let platformService: TypeMoq.IMock<IPlatformService>; + let pythonInPathCommandProvider: IPythonInPathCommandProvider; + setup(async () => { + processService = TypeMoq.Mock.ofType<IProcessService>(); + virtualEnvironmentManager = TypeMoq.Mock.ofType<IVirtualEnvironmentManager>(); + interpreterHelper = TypeMoq.Mock.ofType<InterpreterHelper>(); + const configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + configurationService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + const persistentStateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + persistentState = TypeMoq.Mock.ofType<IPersistentState<PythonEnvironment[]>>(); + processService.setup((x: any) => x.then).returns(() => undefined); + persistentState.setup((p) => p.value).returns(() => undefined as any); + persistentState.setup((p) => p.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => persistentState.object); + const procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + procServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager), TypeMoq.It.isAny())) + .returns(() => virtualEnvironmentManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterVersionService), TypeMoq.It.isAny())) + .returns(() => interpreterHelper.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) + .returns(() => fileSystem.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())) + .returns(() => persistentStateFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) + .returns(() => configurationService.object); + pythonInPathCommandProvider = new PythonInPathCommandProvider(platformService.object); + currentPathService = new CurrentPathService( + interpreterHelper.object, + procServiceFactory.object, + pythonInPathCommandProvider, + serviceContainer.object + ); + }); + + [true, false].forEach((isWindows) => { + test(`Interpreters that do not exist on the file system are not excluded from the list (${ + isWindows ? 'windows' : 'not windows' + })`, async () => { + // Specific test for 1305 + const version = new SemVer('1.0.0'); + platformService.setup((p) => p.isWindows).returns(() => isWindows); + platformService.setup((p) => p.osType).returns(() => (isWindows ? OSType.Windows : OSType.Linux)); + interpreterHelper + .setup((v) => v.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version })); + + const execArgs = [isolated, '-c', 'import sys;print(sys.executable)']; + pythonSettings.setup((p) => p.pythonPath).returns(() => 'root:Python'); + processService + .setup((p) => + p.exec(TypeMoq.It.isValue('root:Python'), TypeMoq.It.isValue(execArgs), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve({ stdout: 'c:/root:python' })) + .verifiable(TypeMoq.Times.once()); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(execArgs), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'c:/python1' })) + .verifiable(TypeMoq.Times.once()); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('python2'), TypeMoq.It.isValue(execArgs), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'c:/python2' })) + .verifiable(TypeMoq.Times.once()); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('python3'), TypeMoq.It.isValue(execArgs), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'c:/python3' })) + .verifiable(TypeMoq.Times.once()); + + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue('c:/root:python'))) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue('c:/python1'))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue('c:/python2'))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue('c:/python3'))) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + const interpreters = await currentPathService.getInterpreters(); + processService.verifyAll(); + fileSystem.verifyAll(); + + expect(interpreters).to.be.of.length(2); + expect(interpreters).to.deep.include({ version, path: 'c:/root:python', envType: EnvironmentType.Unknown }); + expect(interpreters).to.deep.include({ version, path: 'c:/python3', envType: EnvironmentType.Unknown }); + }); + }); +}); diff --git a/src/test/interpreters/display.unit.test.ts b/src/test/interpreters/display.unit.test.ts new file mode 100644 index 000000000000..7913a6d51c79 --- /dev/null +++ b/src/test/interpreters/display.unit.test.ts @@ -0,0 +1,369 @@ +import { expect } from 'chai'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { + ConfigurationTarget, + Disposable, + EventEmitter, + StatusBarAlignment, + StatusBarItem, + Uri, + WorkspaceFolder +} from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; +import { IFileSystem } from '../../client/common/platform/types'; +import { + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + IPathUtils, + IPythonSettings, + ReadWrite +} from '../../client/common/types'; +import { Interpreters } from '../../client/common/utils/localize'; +import { Architecture } from '../../client/common/utils/platform'; +import { InterpreterAutoSelectionService } from '../../client/interpreter/autoSelection'; +import { IInterpreterAutoSelectionService } from '../../client/interpreter/autoSelection/types'; +import { + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterService, + IInterpreterStatusbarVisibilityFilter +} from '../../client/interpreter/contracts'; +import { InterpreterDisplay } from '../../client/interpreter/display'; +import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; + +// tslint:disable:no-any max-func-body-length + +const info: PythonEnvironment = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + envType: EnvironmentType.Unknown, + version: new SemVer('0.0.0-alpha'), + sysPrefix: '', + sysVersion: '' +}; + +suite('Interpreters Display', () => { + let applicationShell: TypeMoq.IMock<IApplicationShell>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let virtualEnvMgr: TypeMoq.IMock<IVirtualEnvironmentManager>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let disposableRegistry: Disposable[]; + let statusBar: TypeMoq.IMock<StatusBarItem>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let configurationService: TypeMoq.IMock<IConfigurationService>; + let interpreterDisplay: IInterpreterDisplay; + let interpreterHelper: TypeMoq.IMock<IInterpreterHelper>; + let pathUtils: TypeMoq.IMock<IPathUtils>; + let output: TypeMoq.IMock<IOutputChannel>; + let autoSelection: IInterpreterAutoSelectionService; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + applicationShell = TypeMoq.Mock.ofType<IApplicationShell>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + virtualEnvMgr = TypeMoq.Mock.ofType<IVirtualEnvironmentManager>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + interpreterHelper = TypeMoq.Mock.ofType<IInterpreterHelper>(); + disposableRegistry = []; + statusBar = TypeMoq.Mock.ofType<StatusBarItem>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + pathUtils = TypeMoq.Mock.ofType<IPathUtils>(); + output = TypeMoq.Mock.ofType<IOutputChannel>(); + autoSelection = mock(InterpreterAutoSelectionService); + + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), STANDARD_OUTPUT_CHANNEL)) + .returns(() => output.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => applicationShell.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager))) + .returns(() => virtualEnvMgr.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterHelper))) + .returns(() => interpreterHelper.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterAutoSelectionService))) + .returns(() => instance(autoSelection)); + + applicationShell + .setup((a) => a.createStatusBarItem(TypeMoq.It.isValue(StatusBarAlignment.Left), TypeMoq.It.isValue(100))) + .returns(() => statusBar.object); + pathUtils.setup((p) => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p) => p); + createInterpreterDisplay(); + }); + function createInterpreterDisplay(filters: IInterpreterStatusbarVisibilityFilter[] = []) { + interpreterDisplay = new InterpreterDisplay(serviceContainer.object, filters); + } + function setupWorkspaceFolder(resource: Uri, workspaceFolder?: Uri) { + if (workspaceFolder) { + const mockFolder = TypeMoq.Mock.ofType<WorkspaceFolder>(); + mockFolder.setup((w) => w.uri).returns(() => workspaceFolder); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))) + .returns(() => mockFolder.object); + } else { + workspaceService.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))).returns(() => undefined); + } + } + test('Statusbar must be created and have command name initialized', () => { + statusBar.verify((s) => (s.command = TypeMoq.It.isValue('python.setInterpreter')), TypeMoq.Times.once()); + expect(disposableRegistry).to.be.lengthOf.above(0); + expect(disposableRegistry).contain(statusBar.object); + }); + test('Display name and tooltip must come from interpreter info', async () => { + const resource = Uri.file('x'); + const workspaceFolder = Uri.file('workspace'); + const activeInterpreter: PythonEnvironment = { + ...info, + displayName: 'Dummy_Display_Name', + envType: EnvironmentType.Unknown, + path: path.join('user', 'development', 'env', 'bin', 'python') + }; + setupWorkspaceFolder(resource, workspaceFolder); + when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); + interpreterService + .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve([])); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve(activeInterpreter)); + + await interpreterDisplay.refresh(resource); + + verify(autoSelection.autoSelectInterpreter(anything())).once(); + statusBar.verify((s) => (s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!), TypeMoq.Times.once()); + statusBar.verify((s) => (s.tooltip = TypeMoq.It.isValue(activeInterpreter.path)!), TypeMoq.Times.atLeastOnce()); + }); + test('Log the output channel if displayed needs to be updated with a new interpreter', async () => { + const resource = Uri.file('x'); + const workspaceFolder = Uri.file('workspace'); + const activeInterpreter: PythonEnvironment = { + ...info, + displayName: 'Dummy_Display_Name', + envType: EnvironmentType.Unknown, + path: path.join('user', 'development', 'env', 'bin', 'python') + }; + pathUtils + .setup((p) => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => activeInterpreter.path); + setupWorkspaceFolder(resource, workspaceFolder); + when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); + interpreterService + .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve([])); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve(activeInterpreter)); + output + .setup((o) => o.appendLine(Interpreters.pythonInterpreterPath().format(activeInterpreter.path))) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + await interpreterDisplay.refresh(resource); + + output.verifyAll(); + }); + test('If interpreter is not identified then tooltip should point to python Path', async () => { + const resource = Uri.file('x'); + const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); + const workspaceFolder = Uri.file('workspace'); + const displayName = 'This is the display name'; + + setupWorkspaceFolder(resource, workspaceFolder); + const pythonInterpreter: PythonEnvironment = ({ + displayName, + path: pythonPath + } as any) as PythonEnvironment; + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve(pythonInterpreter)); + + await interpreterDisplay.refresh(resource); + + statusBar.verify((s) => (s.tooltip = TypeMoq.It.isValue(pythonPath)), TypeMoq.Times.atLeastOnce()); + statusBar.verify((s) => (s.text = TypeMoq.It.isValue(displayName)), TypeMoq.Times.once()); + }); + test('If interpreter file does not exist then update status bar accordingly', async () => { + const resource = Uri.file('x'); + const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); + const workspaceFolder = Uri.file('workspace'); + setupWorkspaceFolder(resource, workspaceFolder); + // tslint:disable-next-line:no-any + interpreterService + .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve([{} as any])); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve(undefined)); + configurationService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + pythonSettings.setup((p) => p.pythonPath).returns(() => pythonPath); + fileSystem.setup((f) => f.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); + interpreterHelper + .setup((v) => v.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(undefined)); + virtualEnvMgr + .setup((v) => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve('')); + + await interpreterDisplay.refresh(resource); + + statusBar.verify((s) => (s.color = TypeMoq.It.isValue('yellow')), TypeMoq.Times.once()); + statusBar.verify( + (s) => (s.text = TypeMoq.It.isValue('$(alert) Select Python Interpreter')), + TypeMoq.Times.once() + ); + }); + test('Ensure we try to identify the active workspace when a resource is not provided ', async () => { + const workspaceFolder = Uri.file('x'); + const resource = workspaceFolder; + const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); + const activeInterpreter: PythonEnvironment = { + ...info, + displayName: 'Dummy_Display_Name', + envType: EnvironmentType.Unknown, + companyDisplayName: 'Company Name', + path: pythonPath + }; + fileSystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + virtualEnvMgr + .setup((v) => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve('')); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(activeInterpreter)) + .verifiable(TypeMoq.Times.once()); + interpreterHelper + .setup((i) => i.getActiveWorkspaceUri(undefined)) + .returns(() => { + return { folderUri: workspaceFolder, configTarget: ConfigurationTarget.Workspace }; + }) + .verifiable(TypeMoq.Times.once()); + + await interpreterDisplay.refresh(); + + interpreterHelper.verifyAll(); + interpreterService.verifyAll(); + statusBar.verify((s) => (s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!), TypeMoq.Times.once()); + statusBar.verify((s) => (s.tooltip = TypeMoq.It.isValue(pythonPath)!), TypeMoq.Times.atLeastOnce()); + }); + suite('Visibility', () => { + const resource = Uri.file('x'); + setup(() => { + const workspaceFolder = Uri.file('workspace'); + const activeInterpreter: PythonEnvironment = { + ...info, + displayName: 'Dummy_Display_Name', + envType: EnvironmentType.Unknown, + path: path.join('user', 'development', 'env', 'bin', 'python') + }; + setupWorkspaceFolder(resource, workspaceFolder); + when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); + interpreterService + .setup((i) => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve([])); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))) + .returns(() => Promise.resolve(activeInterpreter)); + }); + test('Status bar must be displayed', async () => { + await interpreterDisplay.refresh(resource); + + statusBar.verify((s) => s.show(), TypeMoq.Times.once()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.never()); + }); + test('Status bar must not be displayed if a filter is registered that needs it to be hidden', async () => { + const filter1: IInterpreterStatusbarVisibilityFilter = { hidden: true }; + const filter2: IInterpreterStatusbarVisibilityFilter = { hidden: false }; + createInterpreterDisplay([filter1, filter2]); + + await interpreterDisplay.refresh(resource); + + statusBar.verify((s) => s.show(), TypeMoq.Times.never()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.once()); + }); + test('Status bar must not be displayed if both filters need it to be hidden', async () => { + const filter1: IInterpreterStatusbarVisibilityFilter = { hidden: true }; + const filter2: IInterpreterStatusbarVisibilityFilter = { hidden: true }; + createInterpreterDisplay([filter1, filter2]); + + await interpreterDisplay.refresh(resource); + + statusBar.verify((s) => s.show(), TypeMoq.Times.never()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.once()); + }); + test('Status bar must be displayed if both filter needs it to be displayed', async () => { + const filter1: IInterpreterStatusbarVisibilityFilter = { hidden: false }; + const filter2: IInterpreterStatusbarVisibilityFilter = { hidden: false }; + createInterpreterDisplay([filter1, filter2]); + + await interpreterDisplay.refresh(resource); + + statusBar.verify((s) => s.show(), TypeMoq.Times.once()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.never()); + }); + test('Status bar must hidden if a filter triggers need for status bar to be hidden', async () => { + const event1 = new EventEmitter<void>(); + const filter1: ReadWrite<IInterpreterStatusbarVisibilityFilter> = { hidden: false, changed: event1.event }; + const event2 = new EventEmitter<void>(); + const filter2: ReadWrite<IInterpreterStatusbarVisibilityFilter> = { hidden: false, changed: event2.event }; + createInterpreterDisplay([filter1, filter2]); + + await interpreterDisplay.refresh(resource); + + statusBar.verify((s) => s.show(), TypeMoq.Times.once()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.never()); + + // Filter one will now want the status bar to get hidden. + statusBar.reset(); + filter1.hidden = true; + event1.fire(); + + statusBar.verify((s) => s.show(), TypeMoq.Times.never()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.once()); + + // Filter two now needs it to be displayed. + statusBar.reset(); + event2.fire(); + + // No changes. + statusBar.verify((s) => s.show(), TypeMoq.Times.never()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.once()); + + // Filter two now needs it to be displayed & filter 1 will allow it to be displayed. + filter1.hidden = false; + statusBar.reset(); + event2.fire(); + + // No changes. + statusBar.verify((s) => s.show(), TypeMoq.Times.once()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.never()); + }); + }); +}); diff --git a/src/test/interpreters/display/interpreterSelectionTip.unit.test.ts b/src/test/interpreters/display/interpreterSelectionTip.unit.test.ts new file mode 100644 index 000000000000..6eb8b046f00a --- /dev/null +++ b/src/test/interpreters/display/interpreterSelectionTip.unit.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { SurveyAndInterpreterTipNotification } from '../../../client/common/experiments/groups'; +import { ExperimentService } from '../../../client/common/experiments/service'; +import { BrowserService } from '../../../client/common/net/browser'; +import { PersistentState, PersistentStateFactory } from '../../../client/common/persistentState'; +import { IBrowserService, IExperimentService, IPersistentState } from '../../../client/common/types'; +import { Common } from '../../../client/common/utils/localize'; +import { InterpreterSelectionTip } from '../../../client/interpreter/display/interpreterSelectionTip'; + +suite('Interpreters - Interpreter Selection Tip', () => { + let selectionTip: InterpreterSelectionTip; + let appShell: IApplicationShell; + let storage: IPersistentState<boolean>; + let experimentService: IExperimentService; + let browserService: IBrowserService; + setup(() => { + const factory = mock(PersistentStateFactory); + storage = mock(PersistentState); + appShell = mock(ApplicationShell); + experimentService = mock(ExperimentService); + browserService = mock(BrowserService); + + when(factory.createGlobalPersistentState('InterpreterSelectionTip', false)).thenReturn(instance(storage)); + + selectionTip = new InterpreterSelectionTip( + instance(appShell), + instance(factory), + instance(experimentService), + instance(browserService) + ); + }); + test('Do not show notification if already shown', async () => { + when(storage.value).thenReturn(true); + + await selectionTip.activate(); + + verify(appShell.showInformationMessage(anything(), anything())).never(); + }); + test('Do not show notification if in neither experiments', async () => { + when(storage.value).thenReturn(false); + when(experimentService.inExperiment(anything())).thenResolve(false); + + await selectionTip.activate(); + + verify(appShell.showInformationMessage(anything(), anything())).never(); + verify(storage.updateValue(true)).once(); + }); + test('Show tip if in tip experiment', async () => { + when(storage.value).thenReturn(false); + when(experimentService.inExperiment(SurveyAndInterpreterTipNotification.tipExperiment)).thenResolve(true); + when(experimentService.inExperiment(SurveyAndInterpreterTipNotification.surveyExperiment)).thenResolve(false); + + await selectionTip.activate(); + + verify(appShell.showInformationMessage(anything(), Common.gotIt())).once(); + verify(storage.updateValue(true)).once(); + }); + test('Show survey link if in survey experiment', async () => { + when(experimentService.inExperiment(SurveyAndInterpreterTipNotification.tipExperiment)).thenResolve(false); + when(experimentService.inExperiment(SurveyAndInterpreterTipNotification.surveyExperiment)).thenResolve(true); + + await selectionTip.activate(); + + verify(appShell.showInformationMessage(anything(), Common.bannerLabelYes(), Common.bannerLabelNo())).once(); + verify(storage.updateValue(true)).once(); + }); + test('Open survey link if in survey experiment and "Yes" is selected', async () => { + when(experimentService.inExperiment(SurveyAndInterpreterTipNotification.tipExperiment)).thenResolve(false); + when(experimentService.inExperiment(SurveyAndInterpreterTipNotification.surveyExperiment)).thenResolve(true); + when(appShell.showInformationMessage(anything(), Common.bannerLabelYes(), Common.bannerLabelNo())).thenResolve( + // tslint:disable-next-line: no-any + Common.bannerLabelYes() as any + ); + + await selectionTip.activate(); + + verify(browserService.launch(anything())).once(); + }); +}); diff --git a/src/test/interpreters/display/progressDisplay.unit.test.ts b/src/test/interpreters/display/progressDisplay.unit.test.ts new file mode 100644 index 000000000000..8dee81840f79 --- /dev/null +++ b/src/test/interpreters/display/progressDisplay.unit.test.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect } from 'chai'; +import { anything, capture, instance, mock, when } from 'ts-mockito'; +import { CancellationToken, Disposable, Progress, ProgressOptions } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { noop } from '../../../client/common/utils/misc'; +import { IInterpreterLocatorProgressService } from '../../../client/interpreter/contracts'; +import { InterpreterLocatorProgressStatubarHandler } from '../../../client/interpreter/display/progressDisplay'; + +type ProgressTask<R> = ( + progress: Progress<{ message?: string; increment?: number }>, + token: CancellationToken +) => Thenable<R>; + +suite('Interpreters - Display Progress', () => { + let refreshingCallback: (e: void) => any | undefined; + let refreshedCallback: (e: void) => any | undefined; + const progressService: IInterpreterLocatorProgressService = { + onRefreshing(listener: (e: void) => any): Disposable { + refreshingCallback = listener; + return { dispose: noop }; + }, + onRefreshed(listener: (e: void) => any): Disposable { + refreshedCallback = listener; + return { dispose: noop }; + }, + register(): void { + noop(); + } + }; + + test('Display loading message when refreshing interpreters for the first time', async () => { + const shell = mock(ApplicationShell); + const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + when(shell.withProgress(anything(), anything())).thenResolve(); + + statusBar.register(); + refreshingCallback(undefined); + + const options = capture(shell.withProgress as any).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(Common.loadingExtension()); + }); + + test('Display refreshing message when refreshing interpreters for the second time', async () => { + const shell = mock(ApplicationShell); + const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + when(shell.withProgress(anything(), anything())).thenResolve(); + + statusBar.register(); + refreshingCallback(undefined); + + let options = capture(shell.withProgress as any).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(Common.loadingExtension()); + + refreshingCallback(undefined); + + options = capture(shell.withProgress as any).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(Interpreters.refreshing()); + }); + + test('Progress message is hidden when loading has completed', async () => { + const shell = mock(ApplicationShell); + const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + when(shell.withProgress(anything(), anything())).thenResolve(); + + statusBar.register(); + refreshingCallback(undefined); + + const options = capture(shell.withProgress as any).last()[0] as ProgressOptions; + const callback = capture(shell.withProgress as any).last()[1] as ProgressTask<void>; + const promise = callback(undefined as any, undefined as any); + + expect(options.title).to.be.equal(Common.loadingExtension()); + + refreshedCallback(undefined); + // Promise must resolve when refreshed callback is invoked. + // When promise resolves, the progress message is hidden by VSC. + await promise; + }); +}); diff --git a/src/test/interpreters/helpers.unit.test.ts b/src/test/interpreters/helpers.unit.test.ts new file mode 100644 index 000000000000..481e6bb5ed31 --- /dev/null +++ b/src/test/interpreters/helpers.unit.test.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, TextDocument, TextEditor, Uri } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; +import { IInterpreterHashProviderFactory } from '../../client/interpreter/locators/types'; +import { IServiceContainer } from '../../client/ioc/types'; + +// tslint:disable:max-func-body-length no-any +suite('Interpreters Display Helper', () => { + let documentManager: TypeMoq.IMock<IDocumentManager>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let helper: InterpreterHelper; + let hashProviderFactory: TypeMoq.IMock<IInterpreterHashProviderFactory>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + hashProviderFactory = TypeMoq.Mock.ofType<IInterpreterHashProviderFactory>(); + + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) + .returns(() => documentManager.object); + + helper = new InterpreterHelper(serviceContainer.object, hashProviderFactory.object); + }); + test('getActiveWorkspaceUri should return undefined if there are no workspaces', () => { + workspaceService.setup((w) => w.workspaceFolders).returns(() => []); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => undefined); + const workspace = helper.getActiveWorkspaceUri(undefined); + expect(workspace).to.be.equal(undefined, 'incorrect value'); + }); + test('getActiveWorkspaceUri should return the workspace if there is only one', () => { + const folderUri = Uri.file('abc'); + // tslint:disable-next-line:no-any + workspaceService.setup((w) => w.workspaceFolders).returns(() => [{ uri: folderUri } as any]); + + const workspace = helper.getActiveWorkspaceUri(undefined); + expect(workspace).to.be.not.equal(undefined, 'incorrect value'); + expect(workspace!.folderUri).to.be.equal(folderUri); + expect(workspace!.configTarget).to.be.equal(ConfigurationTarget.Workspace); + }); + test('getActiveWorkspaceUri should return undefined if we no active editor and have more than one workspace folder', () => { + const folderUri = Uri.file('abc'); + // tslint:disable-next-line:no-any + workspaceService.setup((w) => w.workspaceFolders).returns(() => [{ uri: folderUri } as any, undefined as any]); + documentManager.setup((d) => d.activeTextEditor).returns(() => undefined); + + const workspace = helper.getActiveWorkspaceUri(undefined); + expect(workspace).to.be.equal(undefined, 'incorrect value'); + }); + test('getActiveWorkspaceUri should return undefined of the active editor does not belong to a workspace and if we have more than one workspace folder', () => { + const folderUri = Uri.file('abc'); + const documentUri = Uri.file('file'); + // tslint:disable-next-line:no-any + workspaceService.setup((w) => w.workspaceFolders).returns(() => [{ uri: folderUri } as any, undefined as any]); + const textEditor = TypeMoq.Mock.ofType<TextEditor>(); + const document = TypeMoq.Mock.ofType<TextDocument>(); + textEditor.setup((t) => t.document).returns(() => document.object); + document.setup((d) => d.uri).returns(() => documentUri); + documentManager.setup((d) => d.activeTextEditor).returns(() => textEditor.object); + workspaceService.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))).returns(() => undefined); + + const workspace = helper.getActiveWorkspaceUri(undefined); + expect(workspace).to.be.equal(undefined, 'incorrect value'); + }); + test('getActiveWorkspaceUri should return workspace folder of the active editor if belongs to a workspace and if we have more than one workspace folder', () => { + const folderUri = Uri.file('abc'); + const documentWorkspaceFolderUri = Uri.file('file.abc'); + const documentUri = Uri.file('file'); + // tslint:disable-next-line:no-any + workspaceService.setup((w) => w.workspaceFolders).returns(() => [{ uri: folderUri } as any, undefined as any]); + const textEditor = TypeMoq.Mock.ofType<TextEditor>(); + const document = TypeMoq.Mock.ofType<TextDocument>(); + textEditor.setup((t) => t.document).returns(() => document.object); + document.setup((d) => d.uri).returns(() => documentUri); + documentManager.setup((d) => d.activeTextEditor).returns(() => textEditor.object); + // tslint:disable-next-line:no-any + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))) + .returns(() => { + return { uri: documentWorkspaceFolderUri } as any; + }); + + const workspace = helper.getActiveWorkspaceUri(undefined); + expect(workspace).to.be.not.equal(undefined, 'incorrect value'); + expect(workspace!.folderUri).to.be.equal(documentWorkspaceFolderUri); + expect(workspace!.configTarget).to.be.equal(ConfigurationTarget.WorkspaceFolder); + }); + test('getBestInterpreter should return undefined for an empty list', () => { + expect(helper.getBestInterpreter([])).to.be.equal(undefined, 'should be undefined'); + expect(helper.getBestInterpreter(undefined)).to.be.equal(undefined, 'should be undefined'); + }); + test('getBestInterpreter should return first item if there is only one', () => { + expect(helper.getBestInterpreter(['a'] as any)).to.be.equal('a', 'should be undefined'); + }); + test('getBestInterpreter should return interpreter with highest version', () => { + const interpreter1 = { version: JSON.parse(JSON.stringify(new SemVer('1.0.0-alpha'))) }; + const interpreter2 = { version: JSON.parse(JSON.stringify(new SemVer('3.6.0'))) }; + const interpreter3 = { version: JSON.parse(JSON.stringify(new SemVer('3.7.1-alpha'))) }; + const interpreter4 = { version: JSON.parse(JSON.stringify(new SemVer('3.6.0-alpha'))) }; + const interpreters = [interpreter1, interpreter2, interpreter3, interpreter4] as any; + expect(helper.getBestInterpreter(interpreters)).to.be.deep.equal(interpreter3); + }); +}); diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts new file mode 100644 index 000000000000..eab8c0ae2664 --- /dev/null +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -0,0 +1,665 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any no-unnecessary-override + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { Container } from 'inversify'; +import * as md5 from 'md5'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Disposable, TextDocument, TextEditor, Uri, WorkspaceConfiguration } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { DeprecatePythonPath } from '../../client/common/experiments/groups'; +import { getArchitectureDisplayName } from '../../client/common/platform/registry'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../client/common/process/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentsManager, + IInterpreterPathService, + InterpreterConfigurationScope, + IPersistentState, + IPersistentStateFactory, + IPythonSettings +} from '../../client/common/types'; +import * as EnumEx from '../../client/common/utils/enum'; +import { noop } from '../../client/common/utils/misc'; +import { Architecture } from '../../client/common/utils/platform'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService +} from '../../client/interpreter/autoSelection/types'; +import { IPythonPathUpdaterServiceManager } from '../../client/interpreter/configuration/types'; +import { + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterLocatorService, + INTERPRETER_LOCATOR_SERVICE +} from '../../client/interpreter/contracts'; +import { InterpreterService } from '../../client/interpreter/interpreterService'; +import { IInterpreterHashProvider, IInterpreterHashProviderFactory } from '../../client/interpreter/locators/types'; +import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; +import { PYTHON_PATH } from '../common'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; + +use(chaiAsPromised); + +suite('Interpreters service', () => { + let serviceManager: ServiceManager; + let serviceContainer: ServiceContainer; + let updater: TypeMoq.IMock<IPythonPathUpdaterServiceManager>; + let helper: TypeMoq.IMock<IInterpreterHelper>; + let locator: TypeMoq.IMock<IInterpreterLocatorService>; + let workspace: TypeMoq.IMock<IWorkspaceService>; + let config: TypeMoq.IMock<WorkspaceConfiguration>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let interpreterDisplay: TypeMoq.IMock<IInterpreterDisplay>; + let virtualEnvMgr: TypeMoq.IMock<IVirtualEnvironmentManager>; + let persistentStateFactory: TypeMoq.IMock<IPersistentStateFactory>; + let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>; + let pythonExecutionService: TypeMoq.IMock<IPythonExecutionService>; + let configService: TypeMoq.IMock<IConfigurationService>; + let interpreterPathService: TypeMoq.IMock<IInterpreterPathService>; + let experimentsManager: TypeMoq.IMock<IExperimentsManager>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let hashProviderFactory: TypeMoq.IMock<IInterpreterHashProviderFactory>; + + function setupSuite() { + const cont = new Container(); + serviceManager = new ServiceManager(cont); + serviceContainer = new ServiceContainer(cont); + + experimentsManager = TypeMoq.Mock.ofType<IExperimentsManager>(); + interpreterPathService = TypeMoq.Mock.ofType<IInterpreterPathService>(); + updater = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); + helper = TypeMoq.Mock.ofType<IInterpreterHelper>(); + locator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + config = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + interpreterDisplay = TypeMoq.Mock.ofType<IInterpreterDisplay>(); + virtualEnvMgr = TypeMoq.Mock.ofType<IVirtualEnvironmentManager>(); + persistentStateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + hashProviderFactory = TypeMoq.Mock.ofType<IInterpreterHashProviderFactory>(); + + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + pythonSettings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + + pythonExecutionService.setup((p: any) => p.then).returns(() => undefined); + workspace.setup((x) => x.getConfiguration('python', TypeMoq.It.isAny())).returns(() => config.object); + pythonExecutionFactory + .setup((f) => f.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + fileSystem.setup((fs) => fs.getFileHash(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + const state = { + updateValue: () => Promise.resolve() + }; + return state as any; + }); + + serviceManager.addSingletonInstance<Disposable[]>(IDisposableRegistry, []); + serviceManager.addSingletonInstance<IInterpreterHelper>(IInterpreterHelper, helper.object); + serviceManager.addSingletonInstance<IPythonPathUpdaterServiceManager>( + IPythonPathUpdaterServiceManager, + updater.object + ); + serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspace.object); + serviceManager.addSingletonInstance<IInterpreterLocatorService>( + IInterpreterLocatorService, + locator.object, + INTERPRETER_LOCATOR_SERVICE + ); + serviceManager.addSingletonInstance<IFileSystem>(IFileSystem, fileSystem.object); + serviceManager.addSingletonInstance<IExperimentsManager>(IExperimentsManager, experimentsManager.object); + serviceManager.addSingletonInstance<IInterpreterPathService>( + IInterpreterPathService, + interpreterPathService.object + ); + serviceManager.addSingletonInstance<IInterpreterDisplay>(IInterpreterDisplay, interpreterDisplay.object); + serviceManager.addSingletonInstance<IVirtualEnvironmentManager>( + IVirtualEnvironmentManager, + virtualEnvMgr.object + ); + serviceManager.addSingletonInstance<IPersistentStateFactory>( + IPersistentStateFactory, + persistentStateFactory.object + ); + serviceManager.addSingletonInstance<IPythonExecutionFactory>( + IPythonExecutionFactory, + pythonExecutionFactory.object + ); + serviceManager.addSingletonInstance<IPythonExecutionService>( + IPythonExecutionService, + pythonExecutionService.object + ); + serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService + ); + serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( + IInterpreterAutoSeletionProxyService, + MockAutoSelectionService + ); + serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, configService.object); + } + suite('Misc', () => { + setup(setupSuite); + [undefined, Uri.file('xyz')].forEach((resource) => { + const resourceTestSuffix = `(${resource ? 'with' : 'without'} a resource)`; + + test(`Refresh invokes refresh of display ${resourceTestSuffix}`, async () => { + interpreterDisplay + .setup((i) => i.refresh(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + await service.refresh(resource); + + interpreterDisplay.verifyAll(); + }); + + test(`get Interpreters uses interpreter locactors to get interpreters ${resourceTestSuffix}`, async () => { + locator + .setup((l) => l.getInterpreters(TypeMoq.It.isValue(resource), TypeMoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(TypeMoq.Times.once()); + + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + await service.getInterpreters(resource); + + locator.verifyAll(); + }); + }); + + test('Changes to active document should invoke interpreter.refresh method', async () => { + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + workspace.setup((w) => w.hasWorkspaceFolders).returns(() => true); + workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + let activeTextEditorChangeHandler: Function | undefined; + documentManager + .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((handler) => { + activeTextEditorChangeHandler = handler; + return { dispose: noop }; + }); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + + // tslint:disable-next-line:no-any + service.initialize(); + const textEditor = TypeMoq.Mock.ofType<TextEditor>(); + const uri = Uri.file(path.join('usr', 'file.py')); + const document = TypeMoq.Mock.ofType<TextDocument>(); + textEditor.setup((t) => t.document).returns(() => document.object); + document.setup((d) => d.uri).returns(() => uri); + activeTextEditorChangeHandler!(textEditor.object); + + interpreterDisplay.verify((i) => i.refresh(TypeMoq.It.isValue(uri)), TypeMoq.Times.once()); + }); + + test('If there is no active document then interpreter.refresh should not be invoked', async () => { + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + workspace.setup((w) => w.hasWorkspaceFolders).returns(() => true); + workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + let activeTextEditorChangeHandler: Function | undefined; + documentManager + .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((handler) => { + activeTextEditorChangeHandler = handler; + return { dispose: noop }; + }); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + + // tslint:disable-next-line:no-any + service.initialize(); + activeTextEditorChangeHandler!(); + + interpreterDisplay.verify((i) => i.refresh(TypeMoq.It.isValue(undefined)), TypeMoq.Times.never()); + }); + + test('If user belongs to Deprecate Pythonpath experiment, register the correct handler', async () => { + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + workspace.setup((w) => w.hasWorkspaceFolders).returns(() => true); + workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + let interpreterPathServiceHandler: Function | undefined; + documentManager + .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return { dispose: noop }; + }); + const i: InterpreterConfigurationScope = { + uri: Uri.parse('a'), + configTarget: ConfigurationTarget.Workspace + }; + configService.reset(); + configService + .setup((c) => c.getSettings()) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + configService + .setup((c) => c.getSettings(i.uri)) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + interpreterPathService + .setup((d) => d.onDidChange(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((cb) => (interpreterPathServiceHandler = cb)) + .returns(() => { + return { dispose: noop }; + }); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + + // tslint:disable-next-line:no-any + service.initialize(); + expect(interpreterPathServiceHandler).to.not.equal(undefined, 'Handler not set'); + + interpreterPathServiceHandler!(i); + + // Ensure correct handler was invoked + configService.verifyAll(); + }); + + test('If stored setting is an empty string, refresh the interpreter display', async () => { + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const resource = Uri.parse('a'); + service._pythonPathSetting = ''; + configService.reset(); + configService.setup((c) => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); + interpreterDisplay + .setup((i) => i.refresh()) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + service._onConfigChanged(resource); + interpreterDisplay.verifyAll(); + }); + + test('If stored setting is not equal to current interpreter path setting, refresh the interpreter display', async () => { + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const resource = Uri.parse('a'); + service._pythonPathSetting = 'stored setting'; + configService.reset(); + configService.setup((c) => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); + interpreterDisplay + .setup((i) => i.refresh()) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + service._onConfigChanged(resource); + interpreterDisplay.verifyAll(); + }); + + test('If stored setting is equal to current interpreter path setting, do not refresh the interpreter display', async () => { + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const resource = Uri.parse('a'); + service._pythonPathSetting = 'setting'; + configService.reset(); + configService.setup((c) => c.getSettings(resource)).returns(() => ({ pythonPath: 'setting' } as any)); + interpreterDisplay + .setup((i) => i.refresh()) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + service._onConfigChanged(resource); + interpreterDisplay.verifyAll(); + }); + }); + + suite('Get Interpreter Details', () => { + setup(setupSuite); + [undefined, Uri.file('some workspace')].forEach((resource) => { + test(`Ensure undefined is returned if we're unable to retrieve interpreter info (Resource is ${resource})`, async () => { + const pythonPath = 'SOME VALUE'; + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + locator + .setup((l) => l.getInterpreters(TypeMoq.It.isValue(resource), TypeMoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(TypeMoq.Times.once()); + helper + .setup((h) => h.getInterpreterInformation(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + virtualEnvMgr + .setup((v) => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve('')) + .verifiable(TypeMoq.Times.once()); + virtualEnvMgr + .setup((v) => v.getEnvironmentType(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(EnvironmentType.Unknown)) + .verifiable(TypeMoq.Times.once()); + pythonExecutionService + .setup((p) => p.getExecutablePath()) + .returns(() => Promise.resolve(pythonPath)) + .verifiable(TypeMoq.Times.once()); + + const details = await service.getInterpreterDetails(pythonPath, resource); + + locator.verifyAll(); + pythonExecutionService.verifyAll(); + helper.verifyAll(); + expect(details).to.be.equal(undefined, 'Not undefined'); + }); + }); + }); + + suite('Caching Display name', () => { + setup(() => { + setupSuite(); + fileSystem.reset(); + persistentStateFactory.reset(); + }); + test('Return cached display name', async () => { + const pythonPath = '1234'; + const interpreterInfo: Partial<PythonEnvironment> = { path: pythonPath }; + const hash = `-${md5(JSON.stringify({ ...interpreterInfo, displayName: '' }))}`; + const expectedDisplayName = 'Formatted display name'; + persistentStateFactory + .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + const state = { + updateValue: () => Promise.resolve(), + value: { hash, displayName: expectedDisplayName } + }; + return state as any; + }) + .verifiable(TypeMoq.Times.once()); + + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const displayName = await service.getDisplayName(interpreterInfo, undefined); + + expect(displayName).to.equal(expectedDisplayName); + persistentStateFactory.verifyAll(); + }); + test('Cached display name is not used if file hashes differ', async () => { + const pythonPath = '1234'; + const interpreterInfo: Partial<PythonEnvironment> = { path: pythonPath }; + const fileHash = 'File_Hash'; + const hashProvider = TypeMoq.Mock.ofType<IInterpreterHashProvider>(); + hashProviderFactory + .setup((factory) => factory.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(hashProvider.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + hashProvider + .setup((provider) => provider.getInterpreterHash(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(fileHash)) + .verifiable(TypeMoq.Times.once()); + hashProvider.setup((provider) => (provider as any).then).returns(() => undefined); + const expectedDisplayName = 'Formatted display name'; + persistentStateFactory + .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + const state = { + updateValue: () => Promise.resolve(), + value: { fileHash: 'something else', displayName: expectedDisplayName } + }; + return state as any; + }) + .verifiable(TypeMoq.Times.once()); + + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + const displayName = await service.getDisplayName(interpreterInfo, undefined).catch(() => ''); + + expect(displayName).to.not.equal(expectedDisplayName); + hashProviderFactory.verifyAll(); + hashProvider.verifyAll(); + persistentStateFactory.verifyAll(); + }); + }); + + // This is kind of a verbose test, but we need to ensure we have covered all permutations. + // Also we have special handling for certain types of interpreters. + suite('Display Format (with all permutations)', () => { + setup(setupSuite); + [undefined, Uri.file('xyz')].forEach((resource) => { + [undefined, new SemVer('1.2.3-alpha')].forEach((version) => { + // Forced cast to ignore TS warnings. + (EnumEx.getNamesAndValues<Architecture>(Architecture) as ( + | { name: string; value: Architecture } + | undefined + )[]) + .concat(undefined) + .forEach((arch) => { + [undefined, path.join('a', 'b', 'c', 'd', 'bin', 'python')].forEach((pythonPath) => { + // Forced cast to ignore TS warnings. + (EnumEx.getNamesAndValues<EnvironmentType>(EnvironmentType) as ( + | { name: string; value: EnvironmentType } + | undefined + )[]) + .concat(undefined) + .forEach((interpreterType) => { + [undefined, 'my env name'].forEach((envName) => { + ['', 'my pipenv name'].forEach((pipEnvName) => { + const testName = [ + `${resource ? 'With' : 'Without'} a workspace`, + `${version ? 'with' : 'without'} version information`, + `${arch ? arch.name : 'without'} architecture`, + `${pythonPath ? 'with' : 'without'} python Path`, + `${ + interpreterType + ? `${interpreterType.name} interpreter type` + : 'without interpreter type' + }`, + `${envName ? 'with' : 'without'} environment name`, + `${pipEnvName ? 'with' : 'without'} pip environment` + ].join(', '); + + test(testName, async () => { + const interpreterInfo: Partial<PythonEnvironment> = { + version, + architecture: arch ? arch.value : undefined, + envName, + envType: interpreterType ? interpreterType.value : undefined, + path: pythonPath + }; + + if ( + interpreterInfo.path && + interpreterType && + interpreterType.value === EnvironmentType.Pipenv + ) { + virtualEnvMgr + .setup((v) => + v.getEnvironmentName( + TypeMoq.It.isValue(interpreterInfo.path!), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(pipEnvName)); + } + if (interpreterType) { + helper + .setup((h) => + h.getInterpreterTypeDisplayName( + TypeMoq.It.isValue(interpreterType.value) + ) + ) + .returns(() => `${interpreterType!.name}_display`); + } + + const service = new InterpreterService( + serviceContainer, + hashProviderFactory.object + ); + const expectedDisplayName = buildDisplayName(interpreterInfo); + + const displayName = await service.getDisplayName( + interpreterInfo, + resource + ); + expect(displayName).to.equal(expectedDisplayName); + }); + + function buildDisplayName(interpreterInfo: Partial<PythonEnvironment>) { + const displayNameParts: string[] = ['Python']; + const envSuffixParts: string[] = []; + + if (interpreterInfo.version) { + displayNameParts.push( + `${interpreterInfo.version.major}.${interpreterInfo.version.minor}.${interpreterInfo.version.patch}` + ); + } + if (interpreterInfo.architecture) { + displayNameParts.push( + getArchitectureDisplayName(interpreterInfo.architecture) + ); + } + if ( + !interpreterInfo.envName && + interpreterInfo.path && + interpreterInfo.envType && + interpreterInfo.envType === EnvironmentType.Pipenv && + pipEnvName + ) { + // If we do not have the name of the environment, then try to get it again. + // This can happen based on the context (i.e. resource). + // I.e. we can determine if an environment is PipEnv only when giving it the right workspacec path (i.e. resource). + interpreterInfo.envName = pipEnvName; + } + if (interpreterInfo.envName && interpreterInfo.envName.length > 0) { + envSuffixParts.push(`'${interpreterInfo.envName}'`); + } + if (interpreterInfo.envType) { + envSuffixParts.push(`${interpreterType!.name}_display`); + } + + const envSuffix = + envSuffixParts.length === 0 ? '' : `(${envSuffixParts.join(': ')})`; + return `${displayNameParts.join(' ')} ${envSuffix}`.trim(); + } + }); + }); + }); + }); + }); + }); + }); + }); + + suite('Interpreter Cache', () => { + setup(() => { + setupSuite(); + fileSystem.reset(); + persistentStateFactory.reset(); + }); + test('Ensure cache is returned', async () => { + const fileHash = 'file_hash'; + const pythonPath = 'Some Python Path'; + const hashProvider = TypeMoq.Mock.ofType<IInterpreterHashProvider>(); + hashProviderFactory + .setup((factory) => factory.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(hashProvider.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + hashProvider + .setup((provider) => provider.getInterpreterHash(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(fileHash)) + .verifiable(TypeMoq.Times.once()); + hashProvider.setup((provider) => (provider as any).then).returns(() => undefined); + + const state = TypeMoq.Mock.ofType<IPersistentState<{ fileHash: string; info?: PythonEnvironment }>>(); + const info = { path: 'hell', envType: EnvironmentType.Venv }; + state + .setup((s) => s.value) + .returns(() => { + return { + fileHash, + info: info as any + }; + }) + .verifiable(TypeMoq.Times.atLeastOnce()); + state + .setup((s) => s.updateValue(TypeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + state.setup((s) => (s as any).then).returns(() => undefined); + persistentStateFactory + .setup((f) => f.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => state.object) + .verifiable(TypeMoq.Times.once()); + + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + + const store = await service.getInterpreterCache(pythonPath); + + expect(store.value).to.deep.equal({ fileHash, info }); + state.verifyAll(); + persistentStateFactory.verifyAll(); + hashProviderFactory.verifyAll(); + hashProvider.verifyAll(); + }); + test('Ensure cache is cleared if file hash is different', async () => { + const fileHash = 'file_hash'; + const pythonPath = 'Some Python Path'; + const hashProvider = TypeMoq.Mock.ofType<IInterpreterHashProvider>(); + hashProviderFactory + .setup((factory) => factory.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(hashProvider.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + hashProvider + .setup((provider) => provider.getInterpreterHash(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve('different value')) + .verifiable(TypeMoq.Times.once()); + hashProvider.setup((provider) => (provider as any).then).returns(() => undefined); + + const state = TypeMoq.Mock.ofType<IPersistentState<{ fileHash: string; info?: PythonEnvironment }>>(); + const info = { path: 'hell', envType: EnvironmentType.Venv }; + state + .setup((s) => s.value) + .returns(() => { + return { + fileHash, + info: info as any + }; + }) + .verifiable(TypeMoq.Times.atLeastOnce()); + state + .setup((s) => s.updateValue(TypeMoq.It.isValue({ fileHash: 'different value' }))) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + state.setup((s) => (s as any).then).returns(() => undefined); + persistentStateFactory + .setup((f) => f.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => state.object) + .verifiable(TypeMoq.Times.once()); + + const service = new InterpreterService(serviceContainer, hashProviderFactory.object); + + const store = await service.getInterpreterCache(pythonPath); + + expect(store.value.info).to.deep.equal(info); + state.verifyAll(); + persistentStateFactory.verifyAll(); + hashProviderFactory.verifyAll(); + hashProvider.verifyAll(); + }); + }); +}); diff --git a/src/test/interpreters/interpreterVersion.unit.test.ts b/src/test/interpreters/interpreterVersion.unit.test.ts new file mode 100644 index 000000000000..5c9acf952dc8 --- /dev/null +++ b/src/test/interpreters/interpreterVersion.unit.test.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert, expect } from 'chai'; +import * as path from 'path'; +import * as typeMoq from 'typemoq'; +import '../../client/common/extensions'; +import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; +import { IInterpreterVersionService } from '../../client/interpreter/contracts'; +import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; + +const isolated = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'pyvsc-run-isolated.py'); + +suite('InterpreterVersionService', () => { + let processService: typeMoq.IMock<IProcessService>; + let interpreterVersionService: IInterpreterVersionService; + + setup(() => { + const processFactory = typeMoq.Mock.ofType<IProcessServiceFactory>(); + processService = typeMoq.Mock.ofType<IProcessService>(); + // tslint:disable-next-line:no-any + processService.setup((p: any) => p.then).returns(() => undefined); + + processFactory.setup((p) => p.create()).returns(() => Promise.resolve(processService.object)); + interpreterVersionService = new InterpreterVersionService(processFactory.object); + }); + + suite('getPipVersion', () => { + test('Must return the pip Version.', async () => { + const pythonPath = path.join('a', 'b', 'python'); + const pipVersion = '1.2.3'; + processService + .setup((p) => + p.exec( + typeMoq.It.isValue(pythonPath), + typeMoq.It.isValue([isolated, 'pip', '--version']), + typeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve({ stdout: pipVersion })) + .verifiable(typeMoq.Times.once()); + + const pyVersion = await interpreterVersionService.getPipVersion(pythonPath); + assert.equal(pyVersion, pipVersion, 'Incorrect version'); + }); + + test('Must throw an exception when pip version cannot be determined', async () => { + const pythonPath = path.join('a', 'b', 'python'); + processService + .setup((p) => + p.exec( + typeMoq.It.isValue(pythonPath), + typeMoq.It.isValue([isolated, 'pip', '--version']), + typeMoq.It.isAny() + ) + ) + .returns(() => Promise.reject('error')) + .verifiable(typeMoq.Times.once()); + + const pipVersionPromise = interpreterVersionService.getPipVersion(pythonPath); + await expect(pipVersionPromise).to.be.rejectedWith(); + }); + }); +}); diff --git a/src/test/interpreters/mocks.ts b/src/test/interpreters/mocks.ts new file mode 100644 index 000000000000..e32e5028c039 --- /dev/null +++ b/src/test/interpreters/mocks.ts @@ -0,0 +1,77 @@ +import { injectable } from 'inversify'; +import { IRegistry, RegistryHive } from '../../client/common/platform/types'; +import { IPersistentState } from '../../client/common/types'; +import { Architecture } from '../../client/common/utils/platform'; +import { IInterpreterVersionService } from '../../client/interpreter/contracts'; + +@injectable() +export class MockRegistry implements IRegistry { + constructor( + private keys: { key: string; hive: RegistryHive; arch?: Architecture; values: string[] }[], + private values: { key: string; hive: RegistryHive; arch?: Architecture; value: string; name?: string }[] + ) {} + public async getKeys(key: string, hive: RegistryHive, arch?: Architecture): Promise<string[]> { + const items = this.keys.find((item) => { + if (typeof item.arch === 'number') { + return item.key === key && item.hive === hive && item.arch === arch; + } + return item.key === key && item.hive === hive; + }); + + return items ? Promise.resolve(items.values) : Promise.resolve([]); + } + public async getValue( + key: string, + hive: RegistryHive, + arch?: Architecture, + name?: string + ): Promise<string | undefined | null> { + const items = this.values.find((item) => { + if (item.key !== key || item.hive !== hive) { + return false; + } + if (typeof item.arch === 'number' && item.arch !== arch) { + return false; + } + if (name && item.name !== name) { + return false; + } + return true; + }); + + return items ? Promise.resolve(items.value) : Promise.resolve(null); + } +} + +// tslint:disable-next-line:max-classes-per-file +@injectable() +export class MockInterpreterVersionProvider implements IInterpreterVersionService { + constructor( + private displayName: string, + private useDefaultDisplayName: boolean = false, + private pipVersionPromise?: Promise<string> + ) {} + public async getVersion(_pythonPath: string, defaultDisplayName: string): Promise<string> { + return this.useDefaultDisplayName ? Promise.resolve(defaultDisplayName) : Promise.resolve(this.displayName); + } + public async getPipVersion(_pythonPath: string): Promise<string> { + // tslint:disable-next-line:no-non-null-assertion + return this.pipVersionPromise!; + } + // tslint:disable-next-line:no-empty + public dispose() {} +} + +// tslint:disable-next-line:no-any max-classes-per-file +export class MockState implements IPersistentState<any> { + // tslint:disable-next-line:no-any + constructor(public data: any) {} + // tslint:disable-next-line:no-any + get value(): any { + return this.data; + } + // tslint:disable-next-line:no-any + public async updateValue(data: any): Promise<void> { + this.data = data; + } +} diff --git a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts new file mode 100644 index 000000000000..5ba72dc2c1bf --- /dev/null +++ b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts @@ -0,0 +1,347 @@ +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { DeprecatePythonPath } from '../../client/common/experiments/groups'; +import { IExperimentsManager, IInterpreterPathService } from '../../client/common/types'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { IPythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/types'; +import { IServiceContainer } from '../../client/ioc/types'; + +// tslint:disable:no-invalid-template-strings max-func-body-length + +suite('Python Path Settings Updater', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let experimentsManager: TypeMoq.IMock<IExperimentsManager>; + let interpreterPathService: TypeMoq.IMock<IInterpreterPathService>; + let updaterServiceFactory: IPythonPathUpdaterServiceFactory; + function setupMocks(inExperiment: boolean = false) { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + experimentsManager = TypeMoq.Mock.ofType<IExperimentsManager>(); + experimentsManager.setup((e) => e.inExperiment(TypeMoq.It.isAny())).returns(() => inExperiment); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + interpreterPathService = TypeMoq.Mock.ofType<IInterpreterPathService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IExperimentsManager))) + .returns(() => experimentsManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterPathService))) + .returns(() => interpreterPathService.object); + updaterServiceFactory = new PythonPathUpdaterServiceFactory(serviceContainer.object); + } + function setupConfigProvider(resource?: Uri): TypeMoq.IMock<WorkspaceConfiguration> { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) + .returns(() => workspaceConfig.object); + return workspaceConfig; + } + + suite('When not in Deprecate PythonPath experiment', async () => { + suite('Global', () => { + setup(() => setupMocks(false)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); + const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(); + workspaceConfig + .setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .returns(() => { + // tslint:disable-next-line:no-any + return { globalValue: pythonPath } as any; + }); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + (w) => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); + const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + (w) => + w.update( + TypeMoq.It.isValue('pythonPath'), + TypeMoq.It.isValue(pythonPath), + TypeMoq.It.isValue(true) + ), + TypeMoq.Times.once() + ); + }); + }); + + suite('WorkspaceFolder', () => { + setup(() => setupMocks(false)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig + .setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .returns(() => { + // tslint:disable-next-line:no-any + return { workspaceFolderValue: pythonPath } as any; + }); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + (w) => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + (w) => + w.update( + TypeMoq.It.isValue('pythonPath'), + TypeMoq.It.isValue(pythonPath), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder) + ), + TypeMoq.Times.once() + ); + }); + test('Python Path should be truncated for worspace-relative paths', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; + const expectedPythonPath = path.join('env', 'bin', 'python'); + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + (w) => + w.update( + TypeMoq.It.isValue('pythonPath'), + TypeMoq.It.isValue(expectedPythonPath), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder) + ), + TypeMoq.Times.once() + ); + }); + }); + suite('Workspace (multiroot scenario)', () => { + setup(() => setupMocks(false)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig + .setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))) + .returns(() => { + // tslint:disable-next-line:no-any + return { workspaceValue: pythonPath } as any; + }); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + (w) => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + (w) => + w.update( + TypeMoq.It.isValue('pythonPath'), + TypeMoq.It.isValue(pythonPath), + TypeMoq.It.isValue(false) + ), + TypeMoq.Times.once() + ); + }); + test('Python Path should be truncated for workspace-relative paths', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; + const expectedPythonPath = path.join('env', 'bin', 'python'); + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + + await updater.updatePythonPath(pythonPath); + workspaceConfig.verify( + (w) => + w.update( + TypeMoq.It.isValue('pythonPath'), + TypeMoq.It.isValue(expectedPythonPath), + TypeMoq.It.isValue(false) + ), + TypeMoq.Times.once() + ); + }); + }); + }); + + suite('When in Deprecate PythonPath experiment', async () => { + suite('Global', () => { + setup(() => setupMocks(true)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + interpreterPathService + .setup((i) => i.inspect(undefined)) + .returns(() => { + return { globalValue: pythonPath }; + }); + interpreterPathService + .setup((i) => i.update(undefined, ConfigurationTarget.Global, pythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const pythonPath = `xGlobalPythonPath${new Date().getMilliseconds()}`; + interpreterPathService.setup((i) => i.inspect(undefined)).returns(() => ({})); + + interpreterPathService + .setup((i) => i.update(undefined, ConfigurationTarget.Global, pythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + const updater = updaterServiceFactory.getGlobalPythonPathConfigurationService(); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + }); + + suite('WorkspaceFolder', () => { + setup(() => setupMocks(true)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + interpreterPathService + .setup((i) => i.inspect(workspaceFolder)) + .returns(() => ({ + workspaceFolderValue: pythonPath + })); + interpreterPathService + .setup((i) => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, pythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + interpreterPathService.setup((i) => i.inspect(workspaceFolder)).returns(() => ({})); + interpreterPathService + .setup((i) => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, pythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + test('Python Path should be truncated for workspace-relative paths', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; + const expectedPythonPath = path.join('env', 'bin', 'python'); + const workspaceConfig = setupConfigProvider(workspaceFolder); + workspaceConfig.setup((w) => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); + interpreterPathService.setup((i) => i.inspect(workspaceFolder)).returns(() => ({})); + interpreterPathService + .setup((i) => i.update(workspaceFolder, ConfigurationTarget.WorkspaceFolder, expectedPythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + }); + suite('Workspace (multiroot scenario)', () => { + setup(() => setupMocks(true)); + test('Python Path should not be updated when current pythonPath is the same', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + interpreterPathService + .setup((i) => i.inspect(workspaceFolder)) + .returns(() => ({ workspaceValue: pythonPath })); + interpreterPathService + .setup((i) => i.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + interpreterPathService.verifyAll(); + }); + test('Python Path should be updated when current pythonPath is different', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; + + interpreterPathService.setup((i) => i.inspect(workspaceFolder)).returns(() => ({})); + interpreterPathService + .setup((i) => i.update(workspaceFolder, ConfigurationTarget.Workspace, pythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + + interpreterPathService.verifyAll(); + }); + test('Python Path should be truncated for workspace-relative paths', async () => { + const workspaceFolderPath = path.join('user', 'desktop', 'development'); + const workspaceFolder = Uri.file(workspaceFolderPath); + const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; + const expectedPythonPath = path.join('env', 'bin', 'python'); + + interpreterPathService.setup((i) => i.inspect(workspaceFolder)).returns(() => ({})); + interpreterPathService + .setup((i) => i.update(workspaceFolder, ConfigurationTarget.Workspace, expectedPythonPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const updater = updaterServiceFactory.getWorkspacePythonPathConfigurationService(workspaceFolder); + await updater.updatePythonPath(pythonPath); + + interpreterPathService.verifyAll(); + }); + }); + }); +}); diff --git a/src/test/interpreters/serviceRegistry.unit.test.ts b/src/test/interpreters/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..5e166318533a --- /dev/null +++ b/src/test/interpreters/serviceRegistry.unit.test.ts @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable: no-any + +import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../../client/activation/types'; +import { EnvironmentActivationService } from '../../client/interpreter/activation/service'; +import { TerminalEnvironmentActivationService } from '../../client/interpreter/activation/terminalEnvironmentActivationService'; +import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { InterpreterAutoSelectionService } from '../../client/interpreter/autoSelection'; +import { InterpreterEvaluation } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation'; +import { InterpreterSecurityService } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService'; +import { InterpreterSecurityStorage } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage'; +import { InterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/proxy'; +import { CachedInterpretersAutoSelectionRule } from '../../client/interpreter/autoSelection/rules/cached'; +import { CurrentPathInterpretersAutoSelectionRule } from '../../client/interpreter/autoSelection/rules/currentPath'; +import { SettingsInterpretersAutoSelectionRule } from '../../client/interpreter/autoSelection/rules/settings'; +import { SystemWideInterpretersAutoSelectionRule } from '../../client/interpreter/autoSelection/rules/system'; +import { WindowsRegistryInterpretersAutoSelectionRule } from '../../client/interpreter/autoSelection/rules/winRegistry'; +import { WorkspaceVirtualEnvInterpretersAutoSelectionRule } from '../../client/interpreter/autoSelection/rules/workspaceEnv'; +import { + AutoSelectionRule, + IInterpreterAutoSelectionRule, + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService, + IInterpreterEvaluation, + IInterpreterSecurityService, + IInterpreterSecurityStorage +} from '../../client/interpreter/autoSelection/types'; +import { InterpreterComparer } from '../../client/interpreter/configuration/interpreterComparer'; +import { ResetInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/resetInterpreter'; +import { SetInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter'; +import { SetShebangInterpreterCommand } from '../../client/interpreter/configuration/interpreterSelector/commands/setShebangInterpreter'; +import { InterpreterSelector } from '../../client/interpreter/configuration/interpreterSelector/interpreterSelector'; +import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; +import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; +import { + IInterpreterComparer, + IInterpreterSelector, + IPythonPathUpdaterServiceFactory, + IPythonPathUpdaterServiceManager +} from '../../client/interpreter/configuration/types'; +import { + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterLocatorProgressHandler, + IInterpreterService, + IInterpreterVersionService, + IShebangCodeLensProvider +} from '../../client/interpreter/contracts'; +import { InterpreterDisplay } from '../../client/interpreter/display'; +import { InterpreterSelectionTip } from '../../client/interpreter/display/interpreterSelectionTip'; +import { InterpreterLocatorProgressStatubarHandler } from '../../client/interpreter/display/progressDisplay'; +import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; +import { InterpreterService } from '../../client/interpreter/interpreterService'; +import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion'; +import { registerTypes } from '../../client/interpreter/serviceRegistry'; +import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; +import { CondaInheritEnvPrompt } from '../../client/interpreter/virtualEnvs/condaInheritEnvPrompt'; +import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; +import { VirtualEnvironmentPrompt } from '../../client/interpreter/virtualEnvs/virtualEnvPrompt'; +import { ServiceManager } from '../../client/ioc/serviceManager'; + +suite('Interpreters - Service Registry', () => { + test('Registrations', () => { + const serviceManager = mock(ServiceManager); + registerTypes(instance(serviceManager)); + + [ + [IExtensionSingleActivationService, SetInterpreterCommand], + [IExtensionSingleActivationService, ResetInterpreterCommand], + [IExtensionSingleActivationService, SetShebangInterpreterCommand], + [IExtensionSingleActivationService, InterpreterSecurityStorage], + [IInterpreterEvaluation, InterpreterEvaluation], + [IInterpreterSecurityStorage, InterpreterSecurityStorage], + [IInterpreterSecurityService, InterpreterSecurityService], + + [IVirtualEnvironmentManager, VirtualEnvironmentManager], + [IExtensionActivationService, VirtualEnvironmentPrompt], + [IExtensionSingleActivationService, InterpreterSelectionTip], + + [IInterpreterVersionService, InterpreterVersionService], + + [IInterpreterService, InterpreterService], + [IInterpreterDisplay, InterpreterDisplay], + + [IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory], + [IPythonPathUpdaterServiceManager, PythonPathUpdaterService], + + [IInterpreterSelector, InterpreterSelector], + [IShebangCodeLensProvider, ShebangCodeLensProvider], + [IInterpreterHelper, InterpreterHelper], + [IInterpreterComparer, InterpreterComparer], + + [IInterpreterLocatorProgressHandler, InterpreterLocatorProgressStatubarHandler], + + [IInterpreterAutoSelectionRule, CurrentPathInterpretersAutoSelectionRule, AutoSelectionRule.currentPath], + [IInterpreterAutoSelectionRule, SystemWideInterpretersAutoSelectionRule, AutoSelectionRule.systemWide], + [ + IInterpreterAutoSelectionRule, + WindowsRegistryInterpretersAutoSelectionRule, + AutoSelectionRule.windowsRegistry + ], + [ + IInterpreterAutoSelectionRule, + WorkspaceVirtualEnvInterpretersAutoSelectionRule, + AutoSelectionRule.workspaceVirtualEnvs + ], + [IInterpreterAutoSelectionRule, CachedInterpretersAutoSelectionRule, AutoSelectionRule.cachedInterpreters], + [IInterpreterAutoSelectionRule, SettingsInterpretersAutoSelectionRule, AutoSelectionRule.settings], + [IInterpreterAutoSeletionProxyService, InterpreterAutoSeletionProxyService], + [IInterpreterAutoSelectionService, InterpreterAutoSelectionService], + + [EnvironmentActivationService, EnvironmentActivationService], + [TerminalEnvironmentActivationService, TerminalEnvironmentActivationService], + [IEnvironmentActivationService, EnvironmentActivationService], + [IExtensionActivationService, CondaInheritEnvPrompt] + ].forEach((mapping) => { + verify(serviceManager.addSingleton.apply(serviceManager, mapping as any)).once(); + }); + }); +}); diff --git a/src/test/interpreters/virtualEnvManager.unit.test.ts b/src/test/interpreters/virtualEnvManager.unit.test.ts new file mode 100644 index 000000000000..d4c3de8c7851 --- /dev/null +++ b/src/test/interpreters/virtualEnvManager.unit.test.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IProcessServiceFactory } from '../../client/common/process/types'; +import { IInterpreterLocatorService, IPipEnvService, PIPENV_SERVICE } from '../../client/interpreter/contracts'; +import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; +import { IServiceContainer } from '../../client/ioc/types'; + +suite('Virtual environment manager', () => { + const virtualEnvFolderName = 'virtual Env Folder Name'; + const pythonPath = path.join('a', 'b', virtualEnvFolderName, 'd', 'python'); + + test('Plain Python environment suffix', async () => testSuffix(virtualEnvFolderName)); + test('Plain Python environment suffix with workspace Uri', async () => + testSuffix(virtualEnvFolderName, false, Uri.file(path.join('1', '2', '3', '4')))); + test('Plain Python environment suffix with PipEnv', async () => + testSuffix('workspaceName', true, Uri.file(path.join('1', '2', '3', 'workspaceName')))); + + test('Use environment folder as env name', async () => { + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IPipEnvService))) + .returns(() => TypeMoq.Mock.ofType<IPipEnvService>().object); + const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspaceService.setup((w) => w.hasWorkspaceFolders).returns(() => false); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + + const venvManager = new VirtualEnvironmentManager(serviceContainer.object); + const name = await venvManager.getEnvironmentName(pythonPath); + + expect(name).to.be.equal(virtualEnvFolderName); + }); + + test('Use workspace name as env name', async () => { + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + const pipEnvService = TypeMoq.Mock.ofType<IPipEnvService>(); + pipEnvService + .setup((p) => p.isRelatedPipEnvironment(TypeMoq.It.isAny(), TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IProcessServiceFactory))) + .returns(() => TypeMoq.Mock.ofType<IProcessServiceFactory>().object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IInterpreterLocatorService), TypeMoq.It.isValue(PIPENV_SERVICE))) + .returns(() => pipEnvService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IFileSystem))) + .returns(() => TypeMoq.Mock.ofType<IFileSystem>().object); + const workspaceUri = Uri.file(path.join('root', 'sub', 'wkspace folder')); + const workspaceFolder: WorkspaceFolder = { name: 'wkspace folder', index: 0, uri: workspaceUri }; + const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspaceService.setup((w) => w.hasWorkspaceFolders).returns(() => true); + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + + const venvManager = new VirtualEnvironmentManager(serviceContainer.object); + const name = await venvManager.getEnvironmentName(pythonPath); + + expect(name).to.be.equal(path.basename(workspaceUri.fsPath)); + pipEnvService.verifyAll(); + }); + + async function testSuffix(expectedEnvName: string, isPipEnvironment: boolean = false, resource?: Uri) { + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IProcessServiceFactory))) + .returns(() => TypeMoq.Mock.ofType<IProcessServiceFactory>().object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IFileSystem))) + .returns(() => TypeMoq.Mock.ofType<IFileSystem>().object); + const pipEnvService = TypeMoq.Mock.ofType<IPipEnvService>(); + pipEnvService + .setup((w) => w.isRelatedPipEnvironment(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(isPipEnvironment)); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IInterpreterLocatorService), TypeMoq.It.isValue(PIPENV_SERVICE))) + .returns(() => pipEnvService.object); + const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspaceService.setup((w) => w.hasWorkspaceFolders).returns(() => false); + if (resource) { + const workspaceFolder = TypeMoq.Mock.ofType<WorkspaceFolder>(); + workspaceFolder.setup((w) => w.uri).returns(() => resource); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())) + .returns(() => workspaceFolder.object); + } + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + + const venvManager = new VirtualEnvironmentManager(serviceContainer.object); + + const name = await venvManager.getEnvironmentName(pythonPath, resource); + expect(name).to.be.equal(expectedEnvName, 'Virtual envrironment name suffix is incorrect.'); + } +}); diff --git a/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts new file mode 100644 index 000000000000..190b8723fd1d --- /dev/null +++ b/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts @@ -0,0 +1,521 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import { instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; +import { PersistentStateFactory } from '../../../client/common/persistentState'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { IBrowserService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { createDeferred, createDeferredFromPromise, sleep } from '../../../client/common/utils/async'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { + CondaInheritEnvPrompt, + condaInheritEnvPromptKey +} from '../../../client/interpreter/virtualEnvs/condaInheritEnvPrompt'; +import { EnvironmentType } from '../../../client/pythonEnvironments/info'; + +// tslint:disable:no-any + +// tslint:disable:max-func-body-length +suite('Conda Inherit Env Prompt', async () => { + const resource = Uri.file('a'); + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let platformService: TypeMoq.IMock<IPlatformService>; + let browserService: TypeMoq.IMock<IBrowserService>; + let persistentStateFactory: IPersistentStateFactory; + let notificationPromptEnabled: TypeMoq.IMock<IPersistentState<any>>; + let condaInheritEnvPrompt: CondaInheritEnvPrompt; + function verifyAll() { + workspaceService.verifyAll(); + appShell.verifyAll(); + interpreterService.verifyAll(); + } + + suite('Method shouldShowPrompt()', () => { + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + browserService = TypeMoq.Mock.ofType<IBrowserService>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + persistentStateFactory = mock(PersistentStateFactory); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + browserService.object, + appShell.object, + instance(persistentStateFactory), + platformService.object + ); + }); + test('Returns false if prompt has already been shown in the current session', async () => { + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + browserService.object, + appShell.object, + instance(persistentStateFactory), + platformService.object, + true + ); + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(undefined) as any) + .verifiable(TypeMoq.Times.never()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(true, 'Should be true'); + verifyAll(); + }); + test('Returns false if on Windows', async () => { + platformService + .setup((ps) => ps.isWindows) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + test('Returns false if active interpreter is not of type Conda', async () => { + const interpreter = { + envType: EnvironmentType.Pipenv + }; + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + test('Returns false if no active interpreter is present', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + test('Returns false if settings returned is `undefined`', async () => { + const interpreter = { + envType: EnvironmentType.Conda + }; + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.once()); + workspaceConfig + .setup((ws) => ws.inspect<boolean>('integrated.inheritEnv')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + [ + { + name: 'Returns false if globalValue `terminal.integrated.inheritEnv` setting is set', + settings: { + globalValue: true, + workspaceValue: undefined, + workspaceFolderValue: undefined + } + }, + { + name: 'Returns false if workspaceValue of `terminal.integrated.inheritEnv` setting is set', + settings: { + globalValue: undefined, + workspaceValue: true, + workspaceFolderValue: undefined + } + }, + { + name: 'Returns false if workspaceFolderValue of `terminal.integrated.inheritEnv` setting is set', + settings: { + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: false + } + } + ].forEach((testParams) => { + test(testParams.name, async () => { + const interpreter = { + envType: EnvironmentType.Conda + }; + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.once()); + workspaceConfig + .setup((ws) => ws.inspect<boolean>('integrated.inheritEnv')) + .returns(() => testParams.settings as any); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + }); + test('Returns true otherwise', async () => { + const interpreter = { + envType: EnvironmentType.Conda + }; + const settings = { + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined + }; + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.once()); + workspaceConfig.setup((ws) => ws.inspect<boolean>('integrated.inheritEnv')).returns(() => settings as any); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(true, 'Prompt should be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(true, 'Should be true'); + verifyAll(); + }); + }); + suite('Method activate()', () => { + let initializeInBackground: sinon.SinonStub<any>; + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + browserService = TypeMoq.Mock.ofType<IBrowserService>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + persistentStateFactory = mock(PersistentStateFactory); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Invokes initializeInBackground() in the background', async () => { + const initializeInBackgroundDeferred = createDeferred<void>(); + initializeInBackground = sinon.stub(CondaInheritEnvPrompt.prototype, 'initializeInBackground'); + initializeInBackground.callsFake(() => initializeInBackgroundDeferred.promise); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + browserService.object, + appShell.object, + instance(persistentStateFactory), + platformService.object + ); + + const promise = condaInheritEnvPrompt.activate(resource); + const deferred = createDeferredFromPromise(promise); + await sleep(1); + + // Ensure activate() function has completed while initializeInBackground() is still not resolved + assert.equal(deferred.completed, true); + + initializeInBackgroundDeferred.resolve(); + await sleep(1); + assert.ok(initializeInBackground.calledOnce); + }); + + test('Ignores errors raised by initializeInBackground()', async () => { + initializeInBackground = sinon.stub(CondaInheritEnvPrompt.prototype, 'initializeInBackground'); + initializeInBackground.rejects(new Error('Kaboom')); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + browserService.object, + appShell.object, + instance(persistentStateFactory), + platformService.object + ); + await condaInheritEnvPrompt.activate(resource); + assert.ok(initializeInBackground.calledOnce); + }); + }); + + suite('Method initializeInBackground()', () => { + let shouldShowPrompt: sinon.SinonStub<any>; + let promptAndUpdate: sinon.SinonStub<any>; + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + browserService = TypeMoq.Mock.ofType<IBrowserService>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + persistentStateFactory = mock(PersistentStateFactory); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Show prompt if shouldShowPrompt() returns true', async () => { + shouldShowPrompt = sinon.stub(CondaInheritEnvPrompt.prototype, 'shouldShowPrompt'); + shouldShowPrompt.callsFake(() => Promise.resolve(true)); + promptAndUpdate = sinon.stub(CondaInheritEnvPrompt.prototype, 'promptAndUpdate'); + promptAndUpdate.callsFake(() => Promise.resolve(undefined)); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + browserService.object, + appShell.object, + instance(persistentStateFactory), + platformService.object + ); + await condaInheritEnvPrompt.initializeInBackground(resource); + assert.ok(shouldShowPrompt.calledOnce); + assert.ok(promptAndUpdate.calledOnce); + }); + + test('Do not show prompt if shouldShowPrompt() returns false', async () => { + shouldShowPrompt = sinon.stub(CondaInheritEnvPrompt.prototype, 'shouldShowPrompt'); + shouldShowPrompt.callsFake(() => Promise.resolve(false)); + promptAndUpdate = sinon.stub(CondaInheritEnvPrompt.prototype, 'promptAndUpdate'); + promptAndUpdate.callsFake(() => Promise.resolve(undefined)); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + browserService.object, + appShell.object, + instance(persistentStateFactory), + platformService.object + ); + await condaInheritEnvPrompt.initializeInBackground(resource); + assert.ok(shouldShowPrompt.calledOnce); + assert.ok(promptAndUpdate.notCalled); + }); + }); + + suite('Method promptAndUpdate()', () => { + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.moreInfo()]; + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + persistentStateFactory = mock(PersistentStateFactory); + browserService = TypeMoq.Mock.ofType<IBrowserService>(); + notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<any>>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + when(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).thenReturn( + notificationPromptEnabled.object + ); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + browserService.object, + appShell.object, + instance(persistentStateFactory), + platformService.object + ); + }); + + test('Does not display prompt if it is disabled', async () => { + notificationPromptEnabled + .setup((n) => n.value) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + await condaInheritEnvPrompt.promptAndUpdate(); + verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); + verifyAll(); + notificationPromptEnabled.verifyAll(); + }); + test('Do nothing if no option is selected', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + notificationPromptEnabled + .setup((n) => n.value) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal')) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + workspaceConfig + .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + notificationPromptEnabled + .setup((n) => n.updateValue(false)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + browserService + .setup((b) => b.launch('https://aka.ms/AA66i8f')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + await condaInheritEnvPrompt.promptAndUpdate(); + verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); + verifyAll(); + workspaceConfig.verifyAll(); + notificationPromptEnabled.verifyAll(); + browserService.verifyAll(); + }); + test('Update terminal settings if `Yes` is selected', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + notificationPromptEnabled + .setup((n) => n.value) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes())) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal')) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.once()); + workspaceConfig + .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + notificationPromptEnabled + .setup((n) => n.updateValue(false)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + browserService + .setup((b) => b.launch('https://aka.ms/AA66i8f')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + await condaInheritEnvPrompt.promptAndUpdate(); + verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); + verifyAll(); + workspaceConfig.verifyAll(); + notificationPromptEnabled.verifyAll(); + browserService.verifyAll(); + }); + test('Disable notification prompt if `No` is selected', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + notificationPromptEnabled + .setup((n) => n.value) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelNo())) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal')) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + workspaceConfig + .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + notificationPromptEnabled + .setup((n) => n.updateValue(false)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + browserService + .setup((b) => b.launch('https://aka.ms/AA66i8f')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + await condaInheritEnvPrompt.promptAndUpdate(); + verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); + verifyAll(); + workspaceConfig.verifyAll(); + notificationPromptEnabled.verifyAll(); + browserService.verifyAll(); + }); + test('Launch browser if `More info` option is selected', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + notificationPromptEnabled + .setup((n) => n.value) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage(), ...prompts)) + .returns(() => Promise.resolve(Common.moreInfo())) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal')) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + workspaceConfig + .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + notificationPromptEnabled + .setup((n) => n.updateValue(false)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + browserService + .setup((b) => b.launch('https://aka.ms/AA66i8f')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + await condaInheritEnvPrompt.promptAndUpdate(); + verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); + verifyAll(); + workspaceConfig.verifyAll(); + notificationPromptEnabled.verifyAll(); + browserService.verifyAll(); + }); + }); +}); diff --git a/src/test/interpreters/virtualEnvs/index.unit.test.ts b/src/test/interpreters/virtualEnvs/index.unit.test.ts new file mode 100644 index 000000000000..3e8fefcffc1b --- /dev/null +++ b/src/test/interpreters/virtualEnvs/index.unit.test.ts @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { ITerminalActivationCommandProvider } from '../../../client/common/terminal/types'; +import { ICurrentProcess, IPathUtils } from '../../../client/common/types'; +import { IInterpreterLocatorService, IPipEnvService, PIPENV_SERVICE } from '../../../client/interpreter/contracts'; +import { VirtualEnvironmentManager } from '../../../client/interpreter/virtualEnvs'; +import { IServiceContainer } from '../../../client/ioc/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Virtual Environment Manager', () => { + let process: TypeMoq.IMock<ICurrentProcess>; + let processService: TypeMoq.IMock<IProcessService>; + let pathUtils: TypeMoq.IMock<IPathUtils>; + let virtualEnvMgr: VirtualEnvironmentManager; + let fs: TypeMoq.IMock<IFileSystem>; + let workspace: TypeMoq.IMock<IWorkspaceService>; + let pipEnvService: TypeMoq.IMock<IPipEnvService>; + let terminalActivation: TypeMoq.IMock<ITerminalActivationCommandProvider>; + let platformService: TypeMoq.IMock<IPlatformService>; + + setup(() => { + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + process = TypeMoq.Mock.ofType<ICurrentProcess>(); + processService = TypeMoq.Mock.ofType<IProcessService>(); + const processFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + pathUtils = TypeMoq.Mock.ofType<IPathUtils>(); + fs = TypeMoq.Mock.ofType<IFileSystem>(); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + pipEnvService = TypeMoq.Mock.ofType<IPipEnvService>(); + terminalActivation = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + + processService.setup((p) => (p as any).then).returns(() => undefined); + processFactory.setup((p) => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory))) + .returns(() => processFactory.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => process.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fs.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspace.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterLocatorService), TypeMoq.It.isValue(PIPENV_SERVICE))) + .returns(() => pipEnvService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalActivationCommandProvider), TypeMoq.It.isAny())) + .returns(() => terminalActivation.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())) + .returns(() => platformService.object); + + virtualEnvMgr = new VirtualEnvironmentManager(serviceContainer.object); + }); + + test('Get PyEnv Root from PYENV_ROOT', async () => { + process + .setup((p) => p.env) + .returns(() => { + return { PYENV_ROOT: 'yes' }; + }) + .verifiable(TypeMoq.Times.once()); + + const pyenvRoot = await virtualEnvMgr.getPyEnvRoot(); + + process.verifyAll(); + expect(pyenvRoot).to.equal('yes'); + }); + + test('Get PyEnv Root from current PYENV_ROOT', async () => { + process + .setup((p) => p.env) + .returns(() => { + return {}; + }) + .verifiable(TypeMoq.Times.once()); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('pyenv'), TypeMoq.It.isValue(['root']))) + .returns(() => Promise.resolve({ stdout: 'PROC' })) + .verifiable(TypeMoq.Times.once()); + + const pyenvRoot = await virtualEnvMgr.getPyEnvRoot(); + + process.verifyAll(); + processService.verifyAll(); + expect(pyenvRoot).to.equal('PROC'); + }); + + test('Get default PyEnv Root path', async () => { + process + .setup((p) => p.env) + .returns(() => { + return {}; + }) + .verifiable(TypeMoq.Times.once()); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('pyenv'), TypeMoq.It.isValue(['root']))) + .returns(() => Promise.resolve({ stdout: '', stderr: 'err' })) + .verifiable(TypeMoq.Times.once()); + pathUtils + .setup((p) => p.home) + .returns(() => 'HOME') + .verifiable(TypeMoq.Times.once()); + const pyenvRoot = await virtualEnvMgr.getPyEnvRoot(); + + process.verifyAll(); + processService.verifyAll(); + expect(pyenvRoot).to.equal(path.join('HOME', '.pyenv')); + }); + + test('Get Environment Type, detects venv', async () => { + const pythonPath = path.join('a', 'b', 'c', 'python'); + const dir = path.dirname(pythonPath); + + fs.setup((f) => f.fileExists(TypeMoq.It.isValue(path.join(dir, 'pyvenv.cfg')))) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + const isRecognized = await virtualEnvMgr.isVenvEnvironment(pythonPath); + + expect(isRecognized).to.be.equal(true, 'invalid value'); + fs.verifyAll(); + }); + test('Get Environment Type, does not detect venv incorrectly', async () => { + const pythonPath = path.join('a', 'b', 'c', 'python'); + const dir = path.dirname(pythonPath); + + fs.setup((f) => f.fileExists(TypeMoq.It.isValue(path.join(dir, 'pyvenv.cfg')))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + + const isRecognized = await virtualEnvMgr.isVenvEnvironment(pythonPath); + + expect(isRecognized).to.be.equal(false, 'invalid value'); + fs.verifyAll(); + }); + + test('Get Environment Type, detects pyenv', async () => { + const pythonPath = path.join('py-env-root', 'b', 'c', 'python'); + + process + .setup((p) => p.env) + .returns(() => { + return { PYENV_ROOT: path.join('py-env-root', 'b') }; + }) + .verifiable(TypeMoq.Times.once()); + + const isRecognized = await virtualEnvMgr.isPyEnvEnvironment(pythonPath); + + expect(isRecognized).to.be.equal(true, 'invalid value'); + process.verifyAll(); + }); + + test('Get Environment Type, does not detect pyenv incorrectly', async () => { + const pythonPath = path.join('a', 'b', 'c', 'python'); + + process + .setup((p) => p.env) + .returns(() => { + return { PYENV_ROOT: path.join('py-env-root', 'b') }; + }) + .verifiable(TypeMoq.Times.once()); + + const isRecognized = await virtualEnvMgr.isPyEnvEnvironment(pythonPath); + + expect(isRecognized).to.be.equal(false, 'invalid value'); + process.verifyAll(); + }); + + test('Get Environment Type, detects pipenv', async () => { + const pythonPath = path.join('x', 'b', 'c', 'python'); + workspace + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + const ws = [{ uri: Uri.file('x') }]; + workspace + .setup((w) => w.workspaceFolders) + .returns(() => ws as any) + .verifiable(TypeMoq.Times.atLeastOnce()); + pipEnvService + .setup((p) => p.isRelatedPipEnvironment(TypeMoq.It.isAny(), TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + const isRecognized = await virtualEnvMgr.isPipEnvironment(pythonPath); + + expect(isRecognized).to.be.equal(true, 'invalid value'); + workspace.verifyAll(); + pipEnvService.verifyAll(); + }); + + test('Get Environment Type, does not detect pipenv incorrectly', async () => { + const pythonPath = path.join('x', 'b', 'c', 'python'); + workspace + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + const ws = [{ uri: Uri.file('x') }]; + workspace + .setup((w) => w.workspaceFolders) + .returns(() => ws as any) + .verifiable(TypeMoq.Times.atLeastOnce()); + pipEnvService + .setup((p) => p.isRelatedPipEnvironment(TypeMoq.It.isAny(), TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + + const isRecognized = await virtualEnvMgr.isPipEnvironment(pythonPath); + + expect(isRecognized).to.be.equal(false, 'invalid value'); + workspace.verifyAll(); + pipEnvService.verifyAll(); + }); + + for (const isWindows of [true, false]) { + const testTitleSuffix = `(${isWindows ? 'On Windows' : 'Non-Windows'}})`; + + test(`Get Environment Type, detects virtualenv ${testTitleSuffix}`, async () => { + const pythonPath = path.join('x', 'b', 'c', 'python'); + platformService + .setup((p) => p.isWindows) + .returns(() => isWindows) + .verifiable(TypeMoq.Times.once()); + fs.setup((f) => f.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const isRecognized = await virtualEnvMgr.isVirtualEnvironment(pythonPath); + + expect(isRecognized).to.be.equal(true, 'invalid value'); + platformService.verifyAll(); + }); + + test(`Get Environment Type, does not detect virtualenv incorrectly ${testTitleSuffix}`, async () => { + const pythonPath = path.join('x', 'b', 'c', 'python'); + platformService + .setup((p) => p.isWindows) + .returns(() => isWindows) + .verifiable(TypeMoq.Times.once()); + fs.setup((f) => f.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const isRecognized = await virtualEnvMgr.isVirtualEnvironment(pythonPath); + + expect(isRecognized).to.be.equal(false, 'invalid value'); + platformService.verifyAll(); + }); + } +}); diff --git a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts new file mode 100644 index 000000000000..a86b6a45e6bf --- /dev/null +++ b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Disposable, Uri } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { PersistentStateFactory } from '../../../client/common/persistentState'; +import { IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { Common } from '../../../client/common/utils/localize'; +import { PythonPathUpdaterService } from '../../../client/interpreter/configuration/pythonPathUpdaterService'; +import { IPythonPathUpdaterServiceManager } from '../../../client/interpreter/configuration/types'; +import { + IInterpreterHelper, + IInterpreterLocatorService, + IInterpreterWatcherBuilder +} from '../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../client/interpreter/helpers'; +import { VirtualEnvironmentPrompt } from '../../../client/interpreter/virtualEnvs/virtualEnvPrompt'; +import { CacheableLocatorService } from '../../../client/pythonEnvironments/discovery/locators/services/cacheableLocatorService'; +import { InterpreterWatcherBuilder } from '../../../client/pythonEnvironments/discovery/locators/services/interpreterWatcherBuilder'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +// tslint:disable-next-line:max-func-body-length +suite('Virtual Environment Prompt', () => { + class VirtualEnvironmentPromptTest extends VirtualEnvironmentPrompt { + // tslint:disable-next-line:no-unnecessary-override + public async handleNewEnvironment(resource: Uri): Promise<void> { + await super.handleNewEnvironment(resource); + } + // tslint:disable-next-line:no-unnecessary-override + public async notifyUser(interpreter: PythonEnvironment, resource: Uri): Promise<void> { + await super.notifyUser(interpreter, resource); + } + } + let builder: IInterpreterWatcherBuilder; + let persistentStateFactory: IPersistentStateFactory; + let helper: IInterpreterHelper; + let pythonPathUpdaterService: IPythonPathUpdaterServiceManager; + let locator: IInterpreterLocatorService; + let disposable: Disposable; + let appShell: IApplicationShell; + let environmentPrompt: VirtualEnvironmentPromptTest; + setup(() => { + builder = mock(InterpreterWatcherBuilder); + persistentStateFactory = mock(PersistentStateFactory); + helper = mock(InterpreterHelper); + pythonPathUpdaterService = mock(PythonPathUpdaterService); + locator = mock(CacheableLocatorService); + disposable = mock(Disposable); + appShell = mock(ApplicationShell); + environmentPrompt = new VirtualEnvironmentPromptTest( + instance(builder), + instance(persistentStateFactory), + instance(helper), + instance(pythonPathUpdaterService), + instance(locator), + [instance(disposable)], + instance(appShell) + ); + }); + + test('User is notified if interpreter exists and only python path to global interpreter is specified in settings', async () => { + const resource = Uri.file('a'); + const interpreter1 = { path: 'path/to/interpreter1' }; + const interpreter2 = { path: 'path/to/interpreter2' }; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; + const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + // tslint:disable:no-any + when(locator.getInterpreters(resource)).thenResolve([interpreter1, interpreter2] as any); + when(helper.getBestInterpreter(anything())).thenReturn(interpreter2 as any); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + notificationPromptEnabled.object + ); + notificationPromptEnabled.setup((n) => n.value).returns(() => true); + when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(); + + await environmentPrompt.handleNewEnvironment(resource); + + verify(locator.getInterpreters(resource)).once(); + verify(helper.getBestInterpreter(anything())).once(); + verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); + verify(appShell.showInformationMessage(anything(), ...prompts)).once(); + }); + + test("If user selects 'Yes', python path is updated", async () => { + const resource = Uri.file('a'); + const interpreter1 = { path: 'path/to/interpreter1' }; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; + const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + notificationPromptEnabled.object + ); + notificationPromptEnabled.setup((n) => n.value).returns(() => true); + when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(prompts[0] as any); + when( + pythonPathUpdaterService.updatePythonPath( + interpreter1.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + resource + ) + ).thenResolve(); + + await environmentPrompt.notifyUser(interpreter1 as any, resource); + + verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); + verify(appShell.showInformationMessage(anything(), ...prompts)).once(); + verify( + pythonPathUpdaterService.updatePythonPath( + interpreter1.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + resource + ) + ).once(); + }); + + test("If user selects 'No', no operation is performed", async () => { + const resource = Uri.file('a'); + const interpreter1 = { path: 'path/to/interpreter1' }; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; + const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + notificationPromptEnabled.object + ); + notificationPromptEnabled.setup((n) => n.value).returns(() => true); + when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(prompts[1] as any); + when( + pythonPathUpdaterService.updatePythonPath( + interpreter1.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + resource + ) + ).thenResolve(); + notificationPromptEnabled + .setup((n) => n.updateValue(false)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await environmentPrompt.notifyUser(interpreter1 as any, resource); + + verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); + verify(appShell.showInformationMessage(anything(), ...prompts)).once(); + verify( + pythonPathUpdaterService.updatePythonPath( + interpreter1.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + resource + ) + ).never(); + notificationPromptEnabled.verifyAll(); + }); + + test("If user selects 'Do not show again', prompt is disabled", async () => { + const resource = Uri.file('a'); + const interpreter1 = { path: 'path/to/interpreter1' }; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; + const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + notificationPromptEnabled.object + ); + notificationPromptEnabled.setup((n) => n.value).returns(() => true); + when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(prompts[2] as any); + notificationPromptEnabled + .setup((n) => n.updateValue(false)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await environmentPrompt.notifyUser(interpreter1 as any, resource); + + verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); + verify(appShell.showInformationMessage(anything(), ...prompts)).once(); + notificationPromptEnabled.verifyAll(); + }); + + test('If prompt is disabled, no notification is shown', async () => { + const resource = Uri.file('a'); + const interpreter1 = { path: 'path/to/interpreter1' }; + const prompts = [Common.bannerLabelYes(), Common.bannerLabelNo(), Common.doNotShowAgain()]; + const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + notificationPromptEnabled.object + ); + notificationPromptEnabled.setup((n) => n.value).returns(() => false); + when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(prompts[0] as any); + + await environmentPrompt.notifyUser(interpreter1 as any, resource); + + verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); + verify(appShell.showInformationMessage(anything(), ...prompts)).never(); + }); +}); diff --git a/src/test/language/braceCounter.unit.test.ts b/src/test/language/braceCounter.unit.test.ts new file mode 100644 index 000000000000..4762ae40cd2a --- /dev/null +++ b/src/test/language/braceCounter.unit.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import { BraceCounter } from '../../client/language/braceCounter'; +import { Tokenizer } from '../../client/language/tokenizer'; + +suite('Language.BraceCounter', () => { + test('Brace counting: zero braces', () => { + const counter = new BraceCounter(); + + assert.equal(counter.count, 0); + }); + + ['(x)', '[x]', '{x}'].forEach((text) => { + test(`Brace counting: ${text}`, () => { + const counter = new BraceCounter(); + const tokens = new Tokenizer().tokenize(text); + + assert.equal(tokens.count, 3); + + const openBrace = tokens.getItemAt(0); + const identifier = tokens.getItemAt(1); + const closeBrace = tokens.getItemAt(2); + + assert.ok(counter.countBrace(tokens.getItemAt(0))); + assert.equal(counter.countBrace(tokens.getItemAt(1)), false); + + assert.equal(counter.isOpened(openBrace.type), true); + assert.equal(counter.isOpened(identifier.type), false); + assert.equal(counter.isOpened(closeBrace.type), true); + + assert.ok(counter.countBrace(tokens.getItemAt(2))); + }); + }); + + ['(x))', '[x]]', '{x}}'].forEach((text) => { + test(`Brace counting with additional close brace: ${text}`, () => { + const counter = new BraceCounter(); + const tokens = new Tokenizer().tokenize(text); + + assert.equal(tokens.count, 4); + for (let i = 0; i < tokens.count - 1; i += 1) { + counter.countBrace(tokens.getItemAt(i)); + } + assert.ok(counter.countBrace(tokens.getItemAt(3))); + }); + }); +}); diff --git a/src/test/language/characterStream.unit.test.ts b/src/test/language/characterStream.unit.test.ts new file mode 100644 index 000000000000..1dfb1e91e4e2 --- /dev/null +++ b/src/test/language/characterStream.unit.test.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +// tslint:disable-next-line:import-name +import Char from 'typescript-char'; +import { CharacterStream } from '../../client/language/characterStream'; +import { TextIterator } from '../../client/language/textIterator'; +import { ICharacterStream } from '../../client/language/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Language.CharacterStream', () => { + test('Iteration (string)', async () => { + const content = 'some text'; + const cs = new CharacterStream(content); + testIteration(cs, content); + }); + test('Iteration (iterator)', async () => { + const content = 'some text'; + const cs = new CharacterStream(new TextIterator(content)); + testIteration(cs, content); + }); + test('Positioning', async () => { + const content = 'some text'; + const cs = new CharacterStream(content); + assert.equal(cs.position, 0); + cs.advance(1); + assert.equal(cs.position, 1); + cs.advance(1); + assert.equal(cs.position, 2); + cs.advance(2); + assert.equal(cs.position, 4); + cs.advance(-3); + assert.equal(cs.position, 1); + cs.advance(-3); + assert.equal(cs.position, 0); + cs.advance(100); + assert.equal(cs.position, content.length); + }); + test('Characters', async () => { + const content = 'some \ttext "" \' \' \n text \r\n more text'; + const cs = new CharacterStream(content); + for (let i = 0; i < content.length; i += 1) { + assert.equal(cs.currentChar, content.charCodeAt(i)); + + assert.equal(cs.nextChar, i < content.length - 1 ? content.charCodeAt(i + 1) : 0); + assert.equal(cs.prevChar, i > 0 ? content.charCodeAt(i - 1) : 0); + + assert.equal(cs.lookAhead(2), i < content.length - 2 ? content.charCodeAt(i + 2) : 0); + assert.equal(cs.lookAhead(-2), i > 1 ? content.charCodeAt(i - 2) : 0); + + const ch = content.charCodeAt(i); + const isLineBreak = ch === Char.LineFeed || ch === Char.CarriageReturn; + assert.equal(cs.isAtWhiteSpace(), ch === Char.Tab || ch === Char.Space || isLineBreak); + assert.equal(cs.isAtLineBreak(), isLineBreak); + assert.equal(cs.isAtString(), ch === Char.SingleQuote || ch === Char.DoubleQuote); + + cs.moveNext(); + } + }); + test('Skip', async () => { + const content = 'some \ttext "" \' \' \n text \r\n more text'; + const cs = new CharacterStream(content); + + cs.skipWhitespace(); + assert.equal(cs.position, 0); + + cs.skipToWhitespace(); + assert.equal(cs.position, 4); + + cs.skipToWhitespace(); + assert.equal(cs.position, 4); + + cs.skipWhitespace(); + assert.equal(cs.position, 6); + + cs.skipLineBreak(); + assert.equal(cs.position, 6); + + cs.skipToEol(); + assert.equal(cs.position, 18); + + cs.skipLineBreak(); + assert.equal(cs.position, 19); + }); +}); + +function testIteration(cs: ICharacterStream, content: string) { + assert.equal(cs.position, 0); + assert.equal(cs.length, content.length); + assert.equal(cs.isEndOfStream(), false); + + for (let i = -2; i < content.length + 2; i += 1) { + const ch = cs.charCodeAt(i); + if (i < 0 || i >= content.length) { + assert.equal(ch, 0); + } else { + assert.equal(ch, content.charCodeAt(i)); + } + } + + for (let i = 0; i < content.length; i += 1) { + assert.equal(cs.isEndOfStream(), false); + assert.equal(cs.position, i); + assert.equal(cs.currentChar, content.charCodeAt(i)); + cs.moveNext(); + } + + assert.equal(cs.isEndOfStream(), true); + assert.equal(cs.position, content.length); +} diff --git a/src/test/language/languageConfiguration.unit.test.ts b/src/test/language/languageConfiguration.unit.test.ts new file mode 100644 index 000000000000..194d90638207 --- /dev/null +++ b/src/test/language/languageConfiguration.unit.test.ts @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; + +import { getLanguageConfiguration } from '../../client/language/languageConfiguration'; + +const NEEDS_INDENT = [ + /^break$/, + /^continue$/, + /^raise$/, // only re-raise + /^return\b/ +]; +const INDENT_ON_ENTER = [ + // block-beginning statements + /^async\s+def\b/, + /^async\s+for\b/, + /^async\s+with\b/, + /^class\b/, + /^def\b/, + /^with\b/, + /^try\b/, + /^except\b/, + /^finally\b/, + /^while\b/, + /^for\b/, + /^if\b/, + /^elif\b/, + /^else\b/ +]; +const DEDENT_ON_ENTER = [ + // block-ending statements + // For now we are ignoring "return" completely. + // See https://github.com/microsoft/vscode-python/issues/6564. + ///^return\b/, + /^break$/, + /^continue$/, + /^raise\b/, + /^pass\b/ +]; + +function isMember(line: string, regexes: RegExp[]): boolean { + for (const regex of regexes) { + if (regex.test(line)) { + return true; + } + } + return false; +} + +function resolveExample( + base: string, + leading: string, + postKeyword: string, + preColon: string, + trailing: string +): [string | undefined, string | undefined, boolean] { + let invalid: string | undefined; + if (base.trim() === '') { + invalid = 'blank line'; + } else if (leading === '' && isMember(base, NEEDS_INDENT)) { + invalid = 'expected indent'; + } else if (leading.trim() !== '') { + invalid = 'look-alike - pre-keyword'; + } else if (postKeyword.trim() !== '') { + invalid = 'look-alike - post-keyword'; + } + + let resolvedBase = base; + if (postKeyword !== '') { + if (resolvedBase.includes(' ')) { + const kw = resolvedBase.split(' ', 1)[0]; + const remainder = resolvedBase.substring(kw.length); + resolvedBase = `${kw}${postKeyword} ${remainder}`; + } else { + if (resolvedBase.endsWith(':')) { + resolvedBase = `${resolvedBase.substring(0, resolvedBase.length - 1)}${postKeyword}:`; + } else { + resolvedBase = `${resolvedBase}${postKeyword}`; + } + } + } + if (preColon !== '') { + if (resolvedBase.endsWith(':')) { + resolvedBase = `${resolvedBase.substring(0, resolvedBase.length - 1)}${preColon}:`; + } else { + return [undefined, undefined, true]; + } + } + const example = `${leading}${resolvedBase}${trailing}`; + return [example, invalid, false]; +} + +suite('Language Configuration', () => { + const cfg = getLanguageConfiguration(); + + suite('"brackets"', () => { + test('brackets is not defined', () => { + expect(cfg.brackets).to.be.equal(undefined, 'missing tests'); + }); + }); + + suite('"comments"', () => { + test('comments is not defined', () => { + expect(cfg.comments).to.be.equal(undefined, 'missing tests'); + }); + }); + + suite('"indentationRules"', () => { + test('indentationRules is not defined', () => { + expect(cfg.indentationRules).to.be.equal(undefined, 'missing tests'); + }); + }); + + suite('"onEnterRules"', () => { + const MULTILINE_SEPARATOR_INDENT_REGEX = cfg.onEnterRules![0].beforeText; + const INDENT_ONENTER_REGEX = cfg.onEnterRules![2].beforeText; + const OUTDENT_ONENTER_REGEX = cfg.onEnterRules![3].beforeText; + // To see the actual (non-verbose) regex patterns, un-comment + // the following lines: + //console.log(INDENT_ONENTER_REGEX.source); + //console.log(OUTDENT_ONENTER_REGEX.source); + + test('Multiline separator indent regex should not pick up strings with no multiline separator', async () => { + const result = MULTILINE_SEPARATOR_INDENT_REGEX.test('a = "test"'); + expect(result).to.be.equal( + false, + 'Multiline separator indent regex for regular strings should not have matches' + ); + }); + + test('Multiline separator indent regex should not pick up strings with escaped characters', async () => { + const result = MULTILINE_SEPARATOR_INDENT_REGEX.test("a = 'hello \\n'"); + expect(result).to.be.equal( + false, + 'Multiline separator indent regex for strings with escaped characters should not have matches' + ); + }); + + test('Multiline separator indent regex should pick up strings ending with a multiline separator', async () => { + const result = MULTILINE_SEPARATOR_INDENT_REGEX.test("a = 'multiline \\"); + expect(result).to.be.equal( + true, + 'Multiline separator indent regex for strings with newline separator should have matches' + ); + }); + + [ + // compound statements + 'async def test(self):', + 'async def :', + 'async :', + 'async for spam in bacon:', + 'async with context:', + 'async with context in manager:', + 'class Test:', + 'class Test(object):', + 'class :', + 'def spam():', + 'def spam(self, node, namespace=""):', + 'def :', + 'for item in items:', + 'for item in :', + 'for :', + 'if foo is None:', + 'if :', + 'try:', + "while '::' in macaddress:", + 'while :', + 'with self.test:', + 'with :', + 'elif x < 5:', + 'elif :', + 'else:', + 'except TestError:', + 'except :', + 'finally:', + // simple statemenhts + 'pass', + 'raise Exception(msg)', + 'raise Exception', + 'raise', // re-raise + 'break', + 'continue', + 'return', + 'return True', + 'return (True, False, False)', + 'return [True, False, False]', + 'return {True, False, False}', + 'return (', + 'return [', + 'return {', + 'return', + // bogus + '', + ' ', + ' ' + ].forEach((base) => { + [ + ['', '', '', ''], + // leading + [' ', '', '', ''], + [' ', '', '', ''], // unusual indent + ['\t\t', '', '', ''], + // pre-keyword + ['x', '', '', ''], + // post-keyword + ['', 'x', '', ''], + // pre-colon + ['', '', ' ', ''], + // trailing + ['', '', '', ' '], + ['', '', '', '# a comment'], + ['', '', '', ' # ...'] + ].forEach((whitespace) => { + const [leading, postKeyword, preColon, trailing] = whitespace; + const [_example, invalid, ignored] = resolveExample(base, leading, postKeyword, preColon, trailing); + if (ignored) { + return; + } + const example = _example!; + + if (invalid) { + test(`Line "${example}" ignored (${invalid})`, () => { + let result: boolean; + + result = INDENT_ONENTER_REGEX.test(example); + expect(result).to.be.equal(false, 'unexpected match'); + + result = OUTDENT_ONENTER_REGEX.test(example); + expect(result).to.be.equal(false, 'unexpected match'); + }); + return; + } + + test(`Check indent-on-enter for line "${example}"`, () => { + let expected = false; + if (isMember(base, INDENT_ON_ENTER)) { + expected = true; + } + + const result = INDENT_ONENTER_REGEX.test(example); + + expect(result).to.be.equal(expected, 'unexpected result'); + }); + + test(`Check dedent-on-enter for line "${example}"`, () => { + let expected = false; + if (isMember(base, DEDENT_ON_ENTER)) { + expected = true; + } + + const result = OUTDENT_ONENTER_REGEX.test(example); + + expect(result).to.be.equal(expected, 'unexpected result'); + }); + }); + }); + }); + + suite('"wordPattern"', () => { + test('wordPattern is not defined', () => { + expect(cfg.wordPattern).to.be.equal(undefined, 'missing tests'); + }); + }); +}); diff --git a/src/test/language/textBuilder.unit.test.ts b/src/test/language/textBuilder.unit.test.ts new file mode 100644 index 000000000000..ac90a025c327 --- /dev/null +++ b/src/test/language/textBuilder.unit.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import { TextBuilder } from '../../client/language/textBuilder'; + +// tslint:disable-next-line:max-func-body-length +suite('Language.TextBuilder', () => { + test('Test get text.', () => { + const builder = new TextBuilder(); + builder.append('red'); + builder.append(' '); + builder.append('green'); + builder.append(' '); + builder.append('blue'); + assert.equal(builder.getText(), 'red green blue'); + }); + test('Test get text with ending whitespace.', () => { + const builder = new TextBuilder(); + builder.append('red'); + builder.append(' '); + builder.append('green'); + builder.append(' '); + builder.append('blue'); + builder.append(' '); // it should skip this + assert.equal(builder.getText(), 'red green blue'); + }); + test('Test soft append whitespace to empty string.', () => { + const builder = new TextBuilder(); + builder.softAppendSpace(1); + builder.append('red'); + assert.equal(builder.getText(), 'red'); + }); + test('Test soft append multiple whitespace.', () => { + const builder = new TextBuilder(); + builder.append('red'); + builder.softAppendSpace(2); + builder.append('green'); + assert.equal(builder.getText(), 'red green'); + }); + test('Test soft append multiple whitespace, with existing whitespace.', () => { + const builder = new TextBuilder(); + builder.append('red'); + builder.append(' '); + builder.softAppendSpace(2); + builder.append('green'); + assert.equal(builder.getText(), 'red green'); + }); +}); diff --git a/src/test/language/textIterator.unit.test.ts b/src/test/language/textIterator.unit.test.ts new file mode 100644 index 000000000000..34daa81534cd --- /dev/null +++ b/src/test/language/textIterator.unit.test.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import { TextIterator } from '../../client/language/textIterator'; + +// tslint:disable-next-line:max-func-body-length +suite('Language.TextIterator', () => { + test('Construction', async () => { + const content = 'some text'; + const ti = new TextIterator(content); + assert.equal(ti.length, content.length); + assert.equal(ti.getText(), content); + }); + test('Iteration', async () => { + const content = 'some text'; + const ti = new TextIterator(content); + for (let i = -2; i < content.length + 2; i += 1) { + const ch = ti.charCodeAt(i); + if (i < 0 || i >= content.length) { + assert.equal(ch, 0); + } else { + assert.equal(ch, content.charCodeAt(i)); + } + } + }); +}); diff --git a/src/test/language/textRange.unit.test.ts b/src/test/language/textRange.unit.test.ts new file mode 100644 index 000000000000..224940f6e8b0 --- /dev/null +++ b/src/test/language/textRange.unit.test.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import { TextRange } from '../../client/language/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Language.TextRange', () => { + test('Empty static', async () => { + const e = TextRange.empty; + assert.equal(e.start, 0); + assert.equal(e.end, 0); + assert.equal(e.length, 0); + }); + test('Construction', async () => { + let r = new TextRange(10, 20); + assert.equal(r.start, 10); + assert.equal(r.end, 30); + assert.equal(r.length, 20); + r = new TextRange(10, 0); + assert.equal(r.start, 10); + assert.equal(r.end, 10); + assert.equal(r.length, 0); + }); + test('From bounds', async () => { + let r = TextRange.fromBounds(7, 9); + assert.equal(r.start, 7); + assert.equal(r.end, 9); + assert.equal(r.length, 2); + + r = TextRange.fromBounds(5, 5); + assert.equal(r.start, 5); + assert.equal(r.end, 5); + assert.equal(r.length, 0); + }); + test('Contains', async () => { + const r = TextRange.fromBounds(7, 9); + assert.equal(r.contains(-1), false); + assert.equal(r.contains(6), false); + assert.equal(r.contains(7), true); + assert.equal(r.contains(8), true); + assert.equal(r.contains(9), false); + assert.equal(r.contains(10), false); + }); + test('Exceptions', async () => { + assert.throws(() => { + // @ts-ignore + const e = new TextRange(0, -1); + }, Error); + assert.throws(() => { + // @ts-ignore + const e = TextRange.fromBounds(3, 1); + }, Error); + }); +}); diff --git a/src/test/language/textRangeCollection.unit.test.ts b/src/test/language/textRangeCollection.unit.test.ts new file mode 100644 index 000000000000..53e5ff4dc650 --- /dev/null +++ b/src/test/language/textRangeCollection.unit.test.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import { TextRangeCollection } from '../../client/language/textRangeCollection'; +import { TextRange } from '../../client/language/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Language.TextRangeCollection', () => { + test('Empty', async () => { + const items: TextRange[] = []; + const c = new TextRangeCollection(items); + assert.equal(c.start, 0); + assert.equal(c.end, 0); + assert.equal(c.length, 0); + assert.equal(c.count, 0); + }); + test('Basic', async () => { + const items: TextRange[] = []; + items.push(new TextRange(2, 1)); + items.push(new TextRange(4, 2)); + const c = new TextRangeCollection(items); + assert.equal(c.start, 2); + assert.equal(c.end, 6); + assert.equal(c.length, 4); + assert.equal(c.count, 2); + + assert.equal(c.getItemAt(0).start, 2); + assert.equal(c.getItemAt(0).length, 1); + + assert.equal(c.getItemAt(1).start, 4); + assert.equal(c.getItemAt(1).length, 2); + }); + test('Contains position (simple)', async () => { + const items: TextRange[] = []; + items.push(new TextRange(2, 1)); + items.push(new TextRange(4, 2)); + const c = new TextRangeCollection(items); + const results = [-1, -1, 0, -1, 1, 1, -1]; + for (let i = 0; i < results.length; i += 1) { + const index = c.getItemContaining(i); + assert.equal(index, results[i]); + } + }); + test('Contains position (adjoint)', async () => { + const items: TextRange[] = []; + items.push(new TextRange(2, 1)); + items.push(new TextRange(3, 2)); + const c = new TextRangeCollection(items); + const results = [-1, -1, 0, 1, 1, -1, -1]; + for (let i = 0; i < results.length; i += 1) { + const index = c.getItemContaining(i); + assert.equal(index, results[i]); + } + }); + test('Contains position (out of range)', async () => { + const items: TextRange[] = []; + items.push(new TextRange(2, 1)); + items.push(new TextRange(4, 2)); + const c = new TextRangeCollection(items); + const positions = [-100, -1, 10, 100]; + for (const p of positions) { + const index = c.getItemContaining(p); + assert.equal(index, -1); + } + }); + test('Contains position (empty)', async () => { + const items: TextRange[] = []; + const c = new TextRangeCollection(items); + const positions = [-2, -1, 0, 1, 2, 3]; + for (const p of positions) { + const index = c.getItemContaining(p); + assert.equal(index, -1); + } + }); + test('Item at position', async () => { + const items: TextRange[] = []; + items.push(new TextRange(2, 1)); + items.push(new TextRange(4, 2)); + const c = new TextRangeCollection(items); + const results = [-1, -1, 0, -1, 1, -1, -1]; + for (let i = 0; i < results.length; i += 1) { + const index = c.getItemAtPosition(i); + assert.equal(index, results[i]); + } + }); +}); diff --git a/src/test/language/tokenizer.unit.test.ts b/src/test/language/tokenizer.unit.test.ts new file mode 100644 index 000000000000..e0ad13dc6afd --- /dev/null +++ b/src/test/language/tokenizer.unit.test.ts @@ -0,0 +1,490 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import { TextRangeCollection } from '../../client/language/textRangeCollection'; +import { Tokenizer } from '../../client/language/tokenizer'; +import { TokenizerMode, TokenType } from '../../client/language/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Language.Tokenizer', () => { + test('Empty', () => { + const t = new Tokenizer(); + const tokens = t.tokenize(''); + assert.equal(tokens instanceof TextRangeCollection, true); + assert.equal(tokens.count, 0); + assert.equal(tokens.length, 0); + }); + test('Strings: unclosed', () => { + const t = new Tokenizer(); + const tokens = t.tokenize(' "string" """line1\n#line2"""\t\'un#closed'); + assert.equal(tokens.count, 3); + + const ranges = [1, 8, 10, 18, 29, 10]; + for (let i = 0; i < tokens.count; i += 1) { + assert.equal(tokens.getItemAt(i).start, ranges[2 * i]); + assert.equal(tokens.getItemAt(i).length, ranges[2 * i + 1]); + assert.equal(tokens.getItemAt(i).type, TokenType.String); + } + }); + test('Strings: block next to regular, double-quoted', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('"string""""s2"""'); + assert.equal(tokens.count, 2); + + const ranges = [0, 8, 8, 8]; + for (let i = 0; i < tokens.count; i += 1) { + assert.equal(tokens.getItemAt(i).start, ranges[2 * i]); + assert.equal(tokens.getItemAt(i).length, ranges[2 * i + 1]); + assert.equal(tokens.getItemAt(i).type, TokenType.String); + } + }); + test('Strings: block next to block, double-quoted', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('""""""""'); + assert.equal(tokens.count, 2); + + const ranges = [0, 6, 6, 2]; + for (let i = 0; i < tokens.count; i += 1) { + assert.equal(tokens.getItemAt(i).start, ranges[2 * i]); + assert.equal(tokens.getItemAt(i).length, ranges[2 * i + 1]); + assert.equal(tokens.getItemAt(i).type, TokenType.String); + } + }); + test('Strings: unclosed sequence of quotes', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('"""""'); + assert.equal(tokens.count, 1); + + const ranges = [0, 5]; + for (let i = 0; i < tokens.count; i += 1) { + assert.equal(tokens.getItemAt(i).start, ranges[2 * i]); + assert.equal(tokens.getItemAt(i).length, ranges[2 * i + 1]); + assert.equal(tokens.getItemAt(i).type, TokenType.String); + } + }); + test('Strings: single quote escape', () => { + const t = new Tokenizer(); + // tslint:disable-next-line:quotemark + const tokens = t.tokenize("'\\'quoted\\''"); + assert.equal(tokens.count, 1); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 12); + }); + test('Strings: double quote escape', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('"\\"quoted\\""'); + assert.equal(tokens.count, 1); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 12); + }); + test('Strings: single quoted f-string ', () => { + const t = new Tokenizer(); + // tslint:disable-next-line:quotemark + const tokens = t.tokenize("a+f'quoted'"); + assert.equal(tokens.count, 3); + assert.equal(tokens.getItemAt(0).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(1).type, TokenType.Operator); + assert.equal(tokens.getItemAt(2).type, TokenType.String); + assert.equal(tokens.getItemAt(2).length, 9); + }); + test('Strings: double quoted f-string ', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('x(1,f"quoted")'); + assert.equal(tokens.count, 6); + assert.equal(tokens.getItemAt(0).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(1).type, TokenType.OpenBrace); + assert.equal(tokens.getItemAt(2).type, TokenType.Number); + assert.equal(tokens.getItemAt(3).type, TokenType.Comma); + assert.equal(tokens.getItemAt(4).type, TokenType.String); + assert.equal(tokens.getItemAt(4).length, 9); + assert.equal(tokens.getItemAt(5).type, TokenType.CloseBrace); + }); + test('Strings: single quoted multiline f-string ', () => { + const t = new Tokenizer(); + // tslint:disable-next-line:quotemark + const tokens = t.tokenize("f'''quoted'''"); + assert.equal(tokens.count, 1); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 13); + }); + test('Strings: double quoted multiline f-string ', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('f"""quoted """'); + assert.equal(tokens.count, 1); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 14); + }); + test('Strings: escape at the end of single quoted string ', () => { + const t = new Tokenizer(); + // tslint:disable-next-line:quotemark + const tokens = t.tokenize("'quoted\\'\nx"); + assert.equal(tokens.count, 2); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 9); + assert.equal(tokens.getItemAt(1).type, TokenType.Identifier); + }); + test('Strings: escape at the end of double quoted string ', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('"quoted\\"\nx'); + assert.equal(tokens.count, 2); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 9); + assert.equal(tokens.getItemAt(1).type, TokenType.Identifier); + }); + test('Strings: b/u/r-string', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('b"b" u"u" br"br" ur"ur"'); + assert.equal(tokens.count, 4); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 4); + assert.equal(tokens.getItemAt(1).type, TokenType.String); + assert.equal(tokens.getItemAt(1).length, 4); + assert.equal(tokens.getItemAt(2).type, TokenType.String); + assert.equal(tokens.getItemAt(2).length, 6); + assert.equal(tokens.getItemAt(3).type, TokenType.String); + assert.equal(tokens.getItemAt(3).length, 6); + }); + test('Strings: escape at the end of double quoted string ', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('"quoted\\"\nx'); + assert.equal(tokens.count, 2); + assert.equal(tokens.getItemAt(0).type, TokenType.String); + assert.equal(tokens.getItemAt(0).length, 9); + assert.equal(tokens.getItemAt(1).type, TokenType.Identifier); + }); + test('Comments', () => { + const t = new Tokenizer(); + const tokens = t.tokenize(' #co"""mment1\n\t\n#comm\'ent2 '); + assert.equal(tokens.count, 2); + + const ranges = [1, 12, 15, 11]; + for (let i = 0; i < ranges.length / 2; i += 2) { + assert.equal(tokens.getItemAt(i).start, ranges[i]); + assert.equal(tokens.getItemAt(i).length, ranges[i + 1]); + assert.equal(tokens.getItemAt(i).type, TokenType.Comment); + } + }); + test('Period to operator token', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('x.y'); + assert.equal(tokens.count, 3); + + assert.equal(tokens.getItemAt(0).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(1).type, TokenType.Operator); + assert.equal(tokens.getItemAt(2).type, TokenType.Identifier); + }); + test('@ to operator token', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('@x'); + assert.equal(tokens.count, 2); + + assert.equal(tokens.getItemAt(0).type, TokenType.Operator); + assert.equal(tokens.getItemAt(1).type, TokenType.Identifier); + }); + test('Unknown token', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('`$'); + assert.equal(tokens.count, 1); + + assert.equal(tokens.getItemAt(0).type, TokenType.Unknown); + }); + test('Hex number', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('1 0X2 0x3 0x'); + assert.equal(tokens.count, 5); + + assert.equal(tokens.getItemAt(0).type, TokenType.Number); + assert.equal(tokens.getItemAt(0).length, 1); + + assert.equal(tokens.getItemAt(1).type, TokenType.Number); + assert.equal(tokens.getItemAt(1).length, 3); + + assert.equal(tokens.getItemAt(2).type, TokenType.Number); + assert.equal(tokens.getItemAt(2).length, 3); + + assert.equal(tokens.getItemAt(3).type, TokenType.Number); + assert.equal(tokens.getItemAt(3).length, 1); + + assert.equal(tokens.getItemAt(4).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(4).length, 1); + }); + test('Binary number', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('1 0B1 0b010 0b3 0b'); + assert.equal(tokens.count, 7); + + assert.equal(tokens.getItemAt(0).type, TokenType.Number); + assert.equal(tokens.getItemAt(0).length, 1); + + assert.equal(tokens.getItemAt(1).type, TokenType.Number); + assert.equal(tokens.getItemAt(1).length, 3); + + assert.equal(tokens.getItemAt(2).type, TokenType.Number); + assert.equal(tokens.getItemAt(2).length, 5); + + assert.equal(tokens.getItemAt(3).type, TokenType.Number); + assert.equal(tokens.getItemAt(3).length, 1); + + assert.equal(tokens.getItemAt(4).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(4).length, 2); + + assert.equal(tokens.getItemAt(5).type, TokenType.Number); + assert.equal(tokens.getItemAt(5).length, 1); + + assert.equal(tokens.getItemAt(6).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(6).length, 1); + }); + test('Octal number', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('1 0o4 0o077 -0o200 0o9 0oO'); + assert.equal(tokens.count, 8); + + assert.equal(tokens.getItemAt(0).type, TokenType.Number); + assert.equal(tokens.getItemAt(0).length, 1); + + assert.equal(tokens.getItemAt(1).type, TokenType.Number); + assert.equal(tokens.getItemAt(1).length, 3); + + assert.equal(tokens.getItemAt(2).type, TokenType.Number); + assert.equal(tokens.getItemAt(2).length, 5); + + assert.equal(tokens.getItemAt(3).type, TokenType.Number); + assert.equal(tokens.getItemAt(3).length, 6); + + assert.equal(tokens.getItemAt(4).type, TokenType.Number); + assert.equal(tokens.getItemAt(4).length, 1); + + assert.equal(tokens.getItemAt(5).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(5).length, 2); + + assert.equal(tokens.getItemAt(6).type, TokenType.Number); + assert.equal(tokens.getItemAt(6).length, 1); + + assert.equal(tokens.getItemAt(7).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(7).length, 2); + }); + test('Decimal number', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('-2147483647 ++2147483647'); + assert.equal(tokens.count, 3); + + assert.equal(tokens.getItemAt(0).type, TokenType.Number); + assert.equal(tokens.getItemAt(0).length, 11); + + assert.equal(tokens.getItemAt(1).type, TokenType.Operator); + assert.equal(tokens.getItemAt(1).length, 1); + + assert.equal(tokens.getItemAt(2).type, TokenType.Number); + assert.equal(tokens.getItemAt(2).length, 11); + }); + test('Decimal number operator', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('a[: -1]'); + assert.equal(tokens.count, 5); + + assert.equal(tokens.getItemAt(3).type, TokenType.Number); + assert.equal(tokens.getItemAt(3).length, 2); + }); + test('Floating point number', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('3.0 .2 ++.3e+12 --.4e1'); + assert.equal(tokens.count, 6); + + assert.equal(tokens.getItemAt(0).type, TokenType.Number); + assert.equal(tokens.getItemAt(0).length, 3); + + assert.equal(tokens.getItemAt(1).type, TokenType.Number); + assert.equal(tokens.getItemAt(1).length, 2); + + assert.equal(tokens.getItemAt(2).type, TokenType.Operator); + assert.equal(tokens.getItemAt(2).length, 1); + + assert.equal(tokens.getItemAt(3).type, TokenType.Number); + assert.equal(tokens.getItemAt(3).length, 7); + + assert.equal(tokens.getItemAt(4).type, TokenType.Operator); + assert.equal(tokens.getItemAt(4).length, 1); + + assert.equal(tokens.getItemAt(5).type, TokenType.Number); + assert.equal(tokens.getItemAt(5).length, 5); + }); + test('Floating point numbers with braces', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('(3.0) (.2) (+.3e+12, .4e1; 0)'); + assert.equal(tokens.count, 13); + + assert.equal(tokens.getItemAt(1).type, TokenType.Number); + assert.equal(tokens.getItemAt(1).length, 3); + + assert.equal(tokens.getItemAt(4).type, TokenType.Number); + assert.equal(tokens.getItemAt(4).length, 2); + + assert.equal(tokens.getItemAt(7).type, TokenType.Number); + assert.equal(tokens.getItemAt(7).length, 7); + + assert.equal(tokens.getItemAt(9).type, TokenType.Number); + assert.equal(tokens.getItemAt(9).length, 4); + + assert.equal(tokens.getItemAt(11).type, TokenType.Number); + assert.equal(tokens.getItemAt(11).length, 1); + }); + test('Underscore numbers', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('+1_0_0_0 0_0 .5_00_3e-4 0xCAFE_F00D 10_000_000.0 0b_0011_1111_0100_1110'); + const lengths = [8, 3, 10, 11, 12, 22]; + assert.equal(tokens.count, 6); + + for (let i = 0; i < tokens.count; i += 1) { + assert.equal(tokens.getItemAt(i).type, TokenType.Number); + assert.equal(tokens.getItemAt(i).length, lengths[i]); + } + }); + test('Simple expression, leading minus', () => { + const t = new Tokenizer(); + const tokens = t.tokenize('x == -y'); + assert.equal(tokens.count, 4); + + assert.equal(tokens.getItemAt(0).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(0).length, 1); + + assert.equal(tokens.getItemAt(1).type, TokenType.Operator); + assert.equal(tokens.getItemAt(1).length, 2); + + assert.equal(tokens.getItemAt(2).type, TokenType.Operator); + assert.equal(tokens.getItemAt(2).length, 1); + + assert.equal(tokens.getItemAt(3).type, TokenType.Identifier); + assert.equal(tokens.getItemAt(3).length, 1); + }); + test('Operators', () => { + const text = + '< <> << <<= ' + + '== != > >> >>= >= <=' + + '+ - ~ %' + + '* ** / /= //=' + + '*= += -= ~= %= **= ' + + '& &= | |= ^ ^= ->'; + const tokens = new Tokenizer().tokenize(text); + const lengths = [ + 1, + 2, + 2, + 3, + 2, + 2, + 1, + 2, + 3, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 2, + 3, + 2, + 2, + 2, + 2, + 2, + 3, + 1, + 2, + 1, + 2, + 1, + 2, + 2 + ]; + assert.equal(tokens.count, lengths.length); + for (let i = 0; i < tokens.count; i += 1) { + const t = tokens.getItemAt(i); + assert.equal(t.type, TokenType.Operator, `${t.type} at ${i} is not an operator`); + assert.equal( + t.length, + lengths[i], + `Length ${t.length} at ${i} (text ${text.substr(t.start, t.length)}), expected ${lengths[i]}` + ); + } + }); + + [-1, 10].forEach((start) => { + test(`Exceptions: out-of-range start = ${start}`, () => { + assert.throws(() => { + new Tokenizer().tokenize('', start, 0, TokenizerMode.Full); + }, new Error('Invalid range start')); + }); + }); + [-1, 10].forEach((length) => { + test(`Exceptions: out-of-range length = ${length}`, () => { + assert.throws(() => { + new Tokenizer().tokenize('abc', 1, length, TokenizerMode.Full); + }, new Error('Invalid range length')); + }); + }); + [ + ['(', TokenType.OpenBrace], + [')', TokenType.CloseBrace], + ['[', TokenType.OpenBracket], + [']', TokenType.CloseBracket], + ['{', TokenType.OpenCurly], + ['}', TokenType.CloseCurly], + [',', TokenType.Comma], + [':', TokenType.Colon], + [';', TokenType.Semicolon], + ['.', TokenType.Operator] + ].forEach((pair) => { + const text: string = pair[0] as string; + const expected = pair[1]; + test(`Character tokens: ${text}`, () => { + const tokens = new Tokenizer().tokenize(text); + assert.equal(tokens.getItemAt(0).type, expected); + }); + }); + [ + ['1', TokenType.Number], + ['-1', TokenType.Number], + ['+1', TokenType.Number], + ['.1', TokenType.Number], + ['-.1', TokenType.Number], + ['+.1', TokenType.Number], + ['1_1', TokenType.Number], + ['_1', TokenType.Identifier], + ['-0x1', TokenType.Number], + ['-0X1', TokenType.Number], + ['-0b1', TokenType.Number], + ['-0B1', TokenType.Number], + ['-0o1', TokenType.Number], + ['-0O1', TokenType.Number] + ].forEach((pair) => { + const text: string = pair[0] as string; + const expected = pair[1]; + test(`Possible numbers: ${text}`, () => { + const tokens = new Tokenizer().tokenize(text); + const token = tokens.getItemAt(0); + assert.equal(token.type, expected); + }); + }); + [ + ['(-1', TokenType.Number], + ['[+1', TokenType.Number], + [',-1', TokenType.Number], + [':+1', TokenType.Number], + [';+1', TokenType.Number], + ['=+1', TokenType.Number] + ].forEach((pair) => { + const text: string = pair[0] as string; + const expected = pair[1]; + test(`Numbers after braces or operators: ${text}`, () => { + const tokens = new Tokenizer().tokenize(text); + const token = tokens.getItemAt(1); + assert.equal(token.type, expected); + }); + }); +}); diff --git a/src/test/languageServers/jedi/autocomplete/base.test.ts b/src/test/languageServers/jedi/autocomplete/base.test.ts new file mode 100644 index 000000000000..8086cf18d8e2 --- /dev/null +++ b/src/test/languageServers/jedi/autocomplete/base.test.ts @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unused-variable +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; +import { isOs, isPythonVersion, OSType } from '../../../common'; +import { closeActiveWindows, initialize, initializeTest } from '../../../initialize'; +import { UnitTestIocContainer } from '../../../testing/serviceRegistry'; + +const autoCompPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'autocomp'); +const fileOne = path.join(autoCompPath, 'one.py'); +const fileImport = path.join(autoCompPath, 'imp.py'); +const fileDoc = path.join(autoCompPath, 'doc.py'); +const fileLambda = path.join(autoCompPath, 'lamb.py'); +const fileDecorator = path.join(autoCompPath, 'deco.py'); +const fileEncoding = path.join(autoCompPath, 'four.py'); +const fileEncodingUsed = path.join(autoCompPath, 'five.py'); +const fileSuppress = path.join(autoCompPath, 'suppress.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Autocomplete Base Tests', function () { + // Attempt to fix #1301 + // tslint:disable-next-line:no-invalid-this + this.timeout(60000); + let ioc: UnitTestIocContainer; + let isPy38: boolean; + + suiteSetup(async function () { + // Attempt to fix #1301 + // tslint:disable-next-line:no-invalid-this + this.timeout(60000); + await initialize(); + initializeDI(); + isPy38 = await isPythonVersion('3.8'); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await ioc.dispose(); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } + + test('For "sys."', (done) => { + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileOne) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then(() => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(3, 10); + return vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + }) + .then((list) => { + assert.equal( + list!.items.filter((item) => item.label === 'api_version').length, + 1, + 'api_version not found' + ); + }) + .then(done, done); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/975 + test('For "import *" find a specific completion for known lib [fstat]', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileImport); + await vscode.window.showTextDocument(textDocument); + const lineNum = 1; + const colNum = 4; + const position = new vscode.Position(lineNum, colNum); + const list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + + const indexOfFstat = list!.items.findIndex((val: vscode.CompletionItem) => val.label === 'fstat'); + + assert( + indexOfFstat !== -1, + `fstat was not found as a completion in ${fileImport} at line ${lineNum}, col ${colNum}` + ); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/898 + test('For "f.readlines()"', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileDoc); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(5, 27); + await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + // These are not known to work, jedi issue + // assert.equal(list.items.filter(item => item.label === 'capitalize').length, 1, 'capitalize not found (known not to work, Jedi issue)'); + // assert.notEqual(list.items.filter(item => item.label === 'upper').length, 1, 'upper not found'); + // assert.notEqual(list.items.filter(item => item.label === 'lower').length, 1, 'lower not found'); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/265 + test('For "lambda"', async function () { + if (await isPythonVersion('2')) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + const textDocument = await vscode.workspace.openTextDocument(fileLambda); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(1, 19); + const list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'append').length, 0, 'append not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'clear').length, 0, 'clear not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'count').length, 0, 'cound not found'); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/630 + test('For "abc.decorators"', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileDecorator); + await vscode.window.showTextDocument(textDocument); + let position = new vscode.Position(3, 9); + let list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); + assert.notEqual( + list!.items.filter((item) => item.label === 'abstractmethod').length, + 0, + 'abstractmethod not found' + ); + + position = new vscode.Position(4, 9); + list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); + assert.notEqual( + list!.items.filter((item) => item.label === 'abstractmethod').length, + 0, + 'abstractmethod not found' + ); + + position = new vscode.Position(2, 30); + list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); + assert.notEqual( + list!.items.filter((item) => item.label === 'abstractmethod').length, + 0, + 'abstractmethod not found' + ); + }); + + // https://github.com/DonJayamanne/pythonVSCode/issues/727 + // https://github.com/DonJayamanne/pythonVSCode/issues/746 + // https://github.com/davidhalter/jedi/issues/859 + test('For "time.slee"', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileDoc); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(10, 9); + const list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + + const items = list!.items.filter((item) => item.label === 'sleep'); + assert.notEqual(items.length, 0, 'sleep not found'); + + checkDocumentation(items[0], 'Delay execution for a given number of seconds. The argument may be'); + }); + + test('For custom class', (done) => { + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileOne) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(30, 4); + return vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + }) + .then((list) => { + assert.notEqual(list!.items.filter((item) => item.label === 'method1').length, 0, 'method1 not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'method2').length, 0, 'method2 not found'); + }) + .then(done, done); + }); + + test('With Unicode Characters', (done) => { + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileEncoding) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(25, 4); + return vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + }) + .then((list) => { + const items = list!.items.filter((item) => item.label === 'bar'); + assert.equal(items.length, 1, 'bar not found'); + + const expected1 = '说明 - keep this line, it works'; + checkDocumentation(items[0], expected1); + + const expected2 = '如果存在需要等待审批或正在执行的任务,将不刷新页面'; + checkDocumentation(items[0], expected2); + }) + .then(done, done); + }); + + test('Across files With Unicode Characters', function (done) { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Fix this test. + // See https://github.com/microsoft/vscode-python/issues/10399. + if (isOs(OSType.Windows) && isPy38) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileEncodingUsed) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(1, 5); + return vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + }) + .then((list) => { + let items = list!.items.filter((item) => item.label === 'Foo'); + assert.equal(items.length, 1, 'Foo not found'); + checkDocumentation(items[0], '说明'); + + items = list!.items.filter((item) => item.label === 'showMessage'); + assert.equal(items.length, 1, 'showMessage not found'); + + const expected1 = + 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи.'; + checkDocumentation(items[0], expected1); + + const expected2 = 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.'; + checkDocumentation(items[0], expected2); + }) + .then(done, done); + }); + + // https://github.com/Microsoft/vscode-python/issues/110 + test('Suppress in strings/comments', async () => { + const positions = [ + new vscode.Position(0, 1), // false + new vscode.Position(0, 9), // true + new vscode.Position(0, 12), // false + new vscode.Position(1, 1), // false + new vscode.Position(1, 3), // false + new vscode.Position(2, 7), // false + new vscode.Position(3, 0), // false + new vscode.Position(4, 2), // false + new vscode.Position(4, 8), // false + new vscode.Position(5, 4), // false + new vscode.Position(5, 10) // false + ]; + const expected = [false, true, false, false, false, false, false, false, false, false, false]; + const textDocument = await vscode.workspace.openTextDocument(fileSuppress); + await vscode.window.showTextDocument(textDocument); + for (let i = 0; i < positions.length; i += 1) { + const list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + positions[i] + ); + const result = list!.items.filter((item) => item.label === 'abs').length; + assert.equal( + result > 0, + expected[i], + `Expected ${expected[i]} at position ${positions[i].line}:${positions[i].character} but got ${result}` + ); + } + }); +}); + +// tslint:disable-next-line:no-any +function checkDocumentation(item: vscode.CompletionItem, expectedContains: string): void { + let isValidType = false; + let documentation: string; + + if (typeof item.documentation === 'string') { + isValidType = true; + documentation = item.documentation; + } else { + documentation = (item.documentation as vscode.MarkdownString).value; + isValidType = documentation !== undefined && documentation !== null; + } + assert.equal(isValidType, true, 'Documentation is neither string nor vscode.MarkdownString'); + + const inDoc = documentation.indexOf(expectedContains) >= 0; + assert.equal(inDoc, true, 'Documentation incorrect'); +} diff --git a/src/test/languageServers/jedi/autocomplete/pep484.test.ts b/src/test/languageServers/jedi/autocomplete/pep484.test.ts new file mode 100644 index 000000000000..143bda870d6c --- /dev/null +++ b/src/test/languageServers/jedi/autocomplete/pep484.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; +import { rootWorkspaceUri } from '../../../common'; +import { closeActiveWindows, initialize, initializeTest } from '../../../initialize'; +import { UnitTestIocContainer } from '../../../testing/serviceRegistry'; + +const autoCompPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'autocomp'); +const filePep484 = path.join(autoCompPath, 'pep484.py'); + +suite('Autocomplete PEP 484', () => { + let isPython2: boolean; + let ioc: UnitTestIocContainer; + suiteSetup(async function () { + await initialize(); + initializeDI(); + isPython2 = (await ioc.getPythonMajorVersion(rootWorkspaceUri!)) === 2; + if (isPython2) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + return; + } + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await ioc.dispose(); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } + + test('argument', async () => { + const textDocument = await vscode.workspace.openTextDocument(filePep484); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(2, 27); + const list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'capitalize').length, 0, 'capitalize not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'upper').length, 0, 'upper not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'lower').length, 0, 'lower not found'); + }); + + test('return value', async () => { + const textDocument = await vscode.workspace.openTextDocument(filePep484); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(8, 6); + const list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'bit_length').length, 0, 'bit_length not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'from_bytes').length, 0, 'from_bytes not found'); + }); +}); diff --git a/src/test/languageServers/jedi/autocomplete/pep526.test.ts b/src/test/languageServers/jedi/autocomplete/pep526.test.ts new file mode 100644 index 000000000000..18ac683a2a98 --- /dev/null +++ b/src/test/languageServers/jedi/autocomplete/pep526.test.ts @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; +import { isPythonVersion } from '../../../common'; +import { closeActiveWindows, initialize, initializeTest } from '../../../initialize'; +import { UnitTestIocContainer } from '../../../testing/serviceRegistry'; + +const autoCompPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'autocomp'); +const filePep526 = path.join(autoCompPath, 'pep526.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Autocomplete PEP 526', () => { + let ioc: UnitTestIocContainer; + suiteSetup(async function () { + // Pep526 only valid for 3.6+ (#2545) + if (await isPythonVersion('2', '3.4', '3.5')) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + + await initialize(); + initializeDI(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await ioc.dispose(); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } + test('variable (abc:str)', async () => { + const textDocument = await vscode.workspace.openTextDocument(filePep526); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(9, 8); + const list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'capitalize').length, 0, 'capitalize not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'upper').length, 0, 'upper not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'lower').length, 0, 'lower not found'); + }); + + test('variable (abc: str = "")', async () => { + const textDocument = await vscode.workspace.openTextDocument(filePep526); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(8, 14); + const list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'capitalize').length, 0, 'capitalize not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'upper').length, 0, 'upper not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'lower').length, 0, 'lower not found'); + }); + + test('variable (abc = UNKNOWN # type: str)', async () => { + const textDocument = await vscode.workspace.openTextDocument(filePep526); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(7, 14); + const list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'capitalize').length, 0, 'capitalize not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'upper').length, 0, 'upper not found'); + assert.notEqual(list!.items.filter((item) => item.label === 'lower').length, 0, 'lower not found'); + }); + + test('class methods', async () => { + const textDocument = await vscode.workspace.openTextDocument(filePep526); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + let position = new vscode.Position(20, 4); + let list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'a').length, 0, 'method a not found'); + + position = new vscode.Position(21, 4); + list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'b').length, 0, 'method b not found'); + }); + + test('class method types', async () => { + const textDocument = await vscode.workspace.openTextDocument(filePep526); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(21, 6); + const list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + assert.notEqual(list!.items.filter((item) => item.label === 'bit_length').length, 0, 'bit_length not found'); + }); +}); diff --git a/src/test/languageServers/jedi/completionSource.unit.test.ts b/src/test/languageServers/jedi/completionSource.unit.test.ts new file mode 100644 index 000000000000..48b1610ba944 --- /dev/null +++ b/src/test/languageServers/jedi/completionSource.unit.test.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import * as TypeMoq from 'typemoq'; +import { CancellationTokenSource, CompletionItemKind, Position, SymbolKind, TextDocument, TextLine } from 'vscode'; +import { IAutoCompleteSettings, IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { JediFactory } from '../../../client/languageServices/jediProxyFactory'; +import { CompletionSource } from '../../../client/providers/completionSource'; +import { IItemInfoSource } from '../../../client/providers/itemInfoSource'; +import { IAutoCompleteItem, ICompletionResult, JediProxyHandler } from '../../../client/providers/jediProxy'; + +suite('Completion Provider', () => { + let completionSource: CompletionSource; + let jediHandler: TypeMoq.IMock<JediProxyHandler<ICompletionResult>>; + let autoCompleteSettings: TypeMoq.IMock<IAutoCompleteSettings>; + let itemInfoSource: TypeMoq.IMock<IItemInfoSource>; + setup(() => { + const jediFactory = TypeMoq.Mock.ofType(JediFactory); + jediHandler = TypeMoq.Mock.ofType<JediProxyHandler<ICompletionResult>>(); + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + autoCompleteSettings = TypeMoq.Mock.ofType<IAutoCompleteSettings>(); + autoCompleteSettings = TypeMoq.Mock.ofType<IAutoCompleteSettings>(); + + jediFactory.setup((j) => j.getJediProxyHandler(TypeMoq.It.isAny())).returns(() => jediHandler.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) + .returns(() => configService.object); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + pythonSettings.setup((p) => p.autoComplete).returns(() => autoCompleteSettings.object); + itemInfoSource = TypeMoq.Mock.ofType<IItemInfoSource>(); + completionSource = new CompletionSource(jediFactory.object, serviceContainer.object, itemInfoSource.object); + }); + + async function testDocumentation(source: string, addBrackets: boolean) { + const doc = TypeMoq.Mock.ofType<TextDocument>(); + const position = new Position(1, 1); + const token = new CancellationTokenSource().token; + const lineText = TypeMoq.Mock.ofType<TextLine>(); + const completionResult = TypeMoq.Mock.ofType<ICompletionResult>(); + + const autoCompleteItems: IAutoCompleteItem[] = [ + { + description: 'description', + kind: SymbolKind.Function, + raw_docstring: 'raw docstring', + rawType: CompletionItemKind.Function, + rightLabel: 'right label', + text: 'some text', + type: CompletionItemKind.Function + } + ]; + + autoCompleteSettings.setup((a) => a.addBrackets).returns(() => addBrackets); + doc.setup((d) => d.fileName).returns(() => ''); + doc.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => source); + doc.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns(() => lineText.object); + doc.setup((d) => d.offsetAt(TypeMoq.It.isAny())).returns(() => 0); + lineText.setup((l) => l.text).returns(() => source); + completionResult.setup((c) => c.requestId).returns(() => 1); + completionResult.setup((c) => c.items).returns(() => autoCompleteItems); + completionResult.setup((c: any) => c.then).returns(() => undefined); + jediHandler + .setup((j) => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve(completionResult.object); + }); + + const expectedSource = `${source}${autoCompleteItems[0].text}`; + itemInfoSource + .setup((i) => + i.getItemInfoFromText( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + expectedSource, + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + const [item] = await completionSource.getVsCodeCompletionItems(doc.object, position, token); + await completionSource.getDocumentation(item, token); + itemInfoSource.verifyAll(); + } + + test("Ensure docs are provided when 'addBrackets' setting is false", async () => { + const source = 'if True:\n print("Hello")\n'; + await testDocumentation(source, false); + }); + test("Ensure docs are provided when 'addBrackets' setting is true", async () => { + const source = 'if True:\n print("Hello")\n'; + await testDocumentation(source, true); + }); +}); diff --git a/src/test/languageServers/jedi/definitions/hover.jedi.test.ts b/src/test/languageServers/jedi/definitions/hover.jedi.test.ts new file mode 100644 index 000000000000..f9efcbf91dac --- /dev/null +++ b/src/test/languageServers/jedi/definitions/hover.jedi.test.ts @@ -0,0 +1,552 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; +import { isOs, isPythonVersion, OSType } from '../../../common'; +import { closeActiveWindows, initialize, initializeTest } from '../../../initialize'; +import { normalizeMarkedString } from '../../../textUtils'; + +const autoCompPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'autocomp'); +const hoverPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'hover'); +const fileOne = path.join(autoCompPath, 'one.py'); +const fileThree = path.join(autoCompPath, 'three.py'); +const fileEncoding = path.join(autoCompPath, 'four.py'); +const fileEncodingUsed = path.join(autoCompPath, 'five.py'); +const fileHover = path.join(autoCompPath, 'hoverTest.py'); +const fileStringFormat = path.join(hoverPath, 'functionHover.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Hover Definition (Jedi)', () => { + let isPy38: boolean; + suiteSetup(async () => { + await initialize(); + isPy38 = await isPythonVersion('3.8'); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('Method', (done) => { + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileOne) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(30, 5); + return vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + }) + .then((result) => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal( + `${def[0].range!.start.line},${def[0].range!.start.character}`, + '30,4', + 'Start position is incorrect' + ); + assert.equal( + `${def[0].range!.end.line},${def[0].range!.end.character}`, + '30,11', + 'End position is incorrect' + ); + assert.equal(def[0].contents.length, 1, 'Invalid content items'); + // tslint:disable-next-line:prefer-template + const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; + assert.equal( + normalizeMarkedString(def[0].contents[0]), + expectedContent, + 'function signature incorrect' + ); + }) + .then(done, done); + }); + + test('Across files', function (done) { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Fix this test. + // See https://github.com/microsoft/vscode-python/issues/10399. + if (isOs(OSType.Windows) && isPy38) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileThree) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(1, 12); + return vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + }) + .then((result) => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal( + `${def[0].range!.start.line},${def[0].range!.start.character}`, + '1,9', + 'Start position is incorrect' + ); + assert.equal( + `${def[0].range!.end.line},${def[0].range!.end.character}`, + '1,12', + 'End position is incorrect' + ); + assert.equal( + normalizeMarkedString(def[0].contents[0]), + // tslint:disable-next-line:prefer-template + '```python' + EOL + 'def fun()' + EOL + '```' + EOL + 'This is fun', + 'Invalid contents' + ); + }) + .then(done, done); + }); + + test('With Unicode Characters', (done) => { + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileEncoding) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(25, 6); + return vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + }) + .then((result) => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal( + `${def[0].range!.start.line},${def[0].range!.start.character}`, + '25,4', + 'Start position is incorrect' + ); + assert.equal( + `${def[0].range!.end.line},${def[0].range!.end.character}`, + '25,7', + 'End position is incorrect' + ); + assert.equal( + normalizeMarkedString(def[0].contents[0]), + // tslint:disable-next-line:prefer-template + '```python' + + EOL + + 'def bar()' + + EOL + + '```' + + EOL + + '说明 - keep this line, it works' + + EOL + + 'delete following line, it works' + + EOL + + '如果存在需要等待审批或正在执行的任务,将不刷新页面', + 'Invalid contents' + ); + }) + .then(done, done); + }); + + test('Across files with Unicode Characters', function (done) { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Fix this test. + // See https://github.com/microsoft/vscode-python/issues/10399. + if (isOs(OSType.Windows) && isPy38) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileEncodingUsed) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(1, 11); + return vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + }) + .then((result) => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal( + `${def[0].range!.start.line},${def[0].range!.start.character}`, + '1,5', + 'Start position is incorrect' + ); + assert.equal( + `${def[0].range!.end.line},${def[0].range!.end.character}`, + '1,16', + 'End position is incorrect' + ); + assert.equal( + normalizeMarkedString(def[0].contents[0]), + // tslint:disable-next-line:prefer-template + '```python' + + EOL + + 'def showMessage()' + + EOL + + '```' + + EOL + + 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи. ' + + EOL + + 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.', + 'Invalid contents' + ); + }) + .then(done, done); + }); + + test('Nothing for keywords (class)', (done) => { + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileOne) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(5, 1); + return vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + }) + .then((def) => { + assert.equal(def!.length, 0, 'Definition length is incorrect'); + }) + .then(done, done); + }); + + test('Nothing for keywords (for)', (done) => { + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileHover) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(3, 1); + return vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + }) + .then((def) => { + assert.equal(def!.length, 0, 'Definition length is incorrect'); + }) + .then(done, done); + }); + + test('Highlighting Class', function (done) { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Fix this test. + // See https://github.com/microsoft/vscode-python/issues/10399. + if (isOs(OSType.Windows) && isPy38) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileHover) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(11, 15); + return vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + }) + .then((result) => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal( + `${def[0].range!.start.line},${def[0].range!.start.character}`, + '11,12', + 'Start position is incorrect' + ); + assert.equal( + `${def[0].range!.end.line},${def[0].range!.end.character}`, + '11,18', + 'End position is incorrect' + ); + const documentation = + // tslint:disable-next-line:prefer-template + '```python' + + EOL + + 'class Random(x=None)' + + EOL + + '```' + + EOL + + 'Random number generator base class used by bound module functions.' + + EOL + + '' + + EOL + + "Used to instantiate instances of Random to get generators that don't" + + EOL + + 'share state.' + + EOL + + '' + + EOL + + 'Class Random can also be subclassed if you want to use a different basic' + + EOL + + 'generator of your own devising: in that case, override the following' + + EOL + + 'methods: random(), seed(), getstate(), and setstate().' + + EOL + + 'Optionally, implement a getrandbits() method so that randrange()' + + EOL + + 'can cover arbitrarily large ranges.'; + + assert.equal(normalizeMarkedString(def[0].contents[0]), documentation, 'Invalid contents'); + }) + .then(done, done); + }); + + test('Highlight Method', function (done) { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Fix this test. + // See https://github.com/microsoft/vscode-python/issues/10399. + if (isOs(OSType.Windows) && isPy38) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileHover) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(12, 10); + return vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + }) + .then((result) => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal( + `${def[0].range!.start.line},${def[0].range!.start.character}`, + '12,5', + 'Start position is incorrect' + ); + assert.equal( + `${def[0].range!.end.line},${def[0].range!.end.character}`, + '12,12', + 'End position is incorrect' + ); + assert.equal( + normalizeMarkedString(def[0].contents[0]), + // tslint:disable-next-line:prefer-template + '```python' + + EOL + + 'def randint(a, b)' + + EOL + + '```' + + EOL + + 'Return random integer in range [a, b], including both end points.', + 'Invalid contents' + ); + }) + .then(done, done); + }); + + test('Highlight Function', (done) => { + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileHover) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(8, 14); + return vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + }) + .then((result) => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal( + `${def[0].range!.start.line},${def[0].range!.start.character}`, + '8,11', + 'Start position is incorrect' + ); + assert.equal( + `${def[0].range!.end.line},${def[0].range!.end.character}`, + '8,15', + 'End position is incorrect' + ); + assert.equal( + normalizeMarkedString(def[0].contents[0]), + // tslint:disable-next-line:prefer-template + '```python' + + EOL + + 'def acos(x: SupportsFloat)' + + EOL + + '```' + + EOL + + 'Return the arc cosine (measured in radians) of x.', + 'Invalid contents' + ); + }) + .then(done, done); + }); + + test('Highlight Multiline Method Signature', function (done) { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Fix this test. + // See https://github.com/microsoft/vscode-python/issues/10399. + if (isOs(OSType.Windows) && isPy38) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileHover) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(14, 14); + return vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + }) + .then((result) => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal( + `${def[0].range!.start.line},${def[0].range!.start.character}`, + '14,9', + 'Start position is incorrect' + ); + assert.equal( + `${def[0].range!.end.line},${def[0].range!.end.character}`, + '14,15', + 'End position is incorrect' + ); + assert.equal( + normalizeMarkedString(def[0].contents[0]), + // tslint:disable-next-line:prefer-template + '```python' + + EOL + + 'class Thread(group=None, target=None, name=None, args=(), kwargs=None, verbose=None)' + + EOL + + '```' + + EOL + + 'A class that represents a thread of control.' + + EOL + + '' + + EOL + + 'This class can be safely subclassed in a limited fashion.', + 'Invalid content items' + ); + }) + .then(done, done); + }); + + test('Variable', (done) => { + let textDocument: vscode.TextDocument; + vscode.workspace + .openTextDocument(fileHover) + .then((document) => { + textDocument = document; + return vscode.window.showTextDocument(textDocument); + }) + .then((_editor) => { + assert(vscode.window.activeTextEditor, 'No active editor'); + const position = new vscode.Position(6, 2); + return vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + }) + .then((result) => { + const def = result!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(def[0].contents.length, 1, 'Only expected one result'); + const contents = normalizeMarkedString(def[0].contents[0]); + if (contents.indexOf('```python') === -1) { + assert.fail(contents, '', 'First line is incorrect', 'compare'); + } + if (contents.indexOf('rnd: Random') === -1) { + assert.fail(contents, '', 'Variable name or type are missing', 'compare'); + } + }) + .then(done, done); + }); + + test('Hover over method shows proper text.', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileStringFormat); + await vscode.window.showTextDocument(textDocument); + const position = new vscode.Position(8, 4); + const def = (await vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ))!; + assert.equal(def.length, 1, 'Definition length is incorrect'); + assert.equal(def[0].contents.length, 1, 'Only expected one result'); + const contents = normalizeMarkedString(def[0].contents[0]); + if (contents.indexOf('def my_func') === -1) { + assert.fail(contents, '', "'def my_func' is missing", 'compare'); + } + if (contents.indexOf('This is a test.') === -1 && contents.indexOf('It also includes this text, too.') === -1) { + assert.fail(contents, '', 'Expected custom function text missing', 'compare'); + } + }); +}); diff --git a/src/test/languageServers/jedi/definitions/navigation.test.ts b/src/test/languageServers/jedi/definitions/navigation.test.ts new file mode 100644 index 000000000000..787412d0f0f8 --- /dev/null +++ b/src/test/languageServers/jedi/definitions/navigation.test.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; +import { rootWorkspaceUri } from '../../../common'; +import { closeActiveWindows, initialize, initializeTest } from '../../../initialize'; +import { UnitTestIocContainer } from '../../../testing/serviceRegistry'; + +const decoratorsPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'definition', 'navigation'); +const fileDefinitions = path.join(decoratorsPath, 'definitions.py'); +const fileUsages = path.join(decoratorsPath, 'usages.py'); + +// tslint:disable-next-line:max-func-body-length +suite('Language Server: Definition Navigation', () => { + let isPython2: boolean; + let ioc: UnitTestIocContainer; + + suiteSetup(async () => { + await initialize(); + initializeDI(); + isPython2 = (await ioc.getPythonMajorVersion(rootWorkspaceUri!)) === 2; + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await ioc.dispose(); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } + + const assertFile = (expectedLocation: string, location: vscode.Uri) => { + const relLocation = vscode.workspace.asRelativePath(location); + const expectedRelLocation = vscode.workspace.asRelativePath(expectedLocation); + assert.equal(expectedRelLocation, relLocation, 'Position is in wrong file'); + }; + + const formatPosition = (position: vscode.Position) => { + return `${position.line},${position.character}`; + }; + + const assertRange = (expectedRange: vscode.Range, range: vscode.Range) => { + assert.equal(formatPosition(expectedRange.start), formatPosition(range.start), 'Start position is incorrect'); + assert.equal(formatPosition(expectedRange.end), formatPosition(range.end), 'End position is incorrect'); + }; + + const buildTest = ( + startFile: string, + startPosition: vscode.Position, + expectedFiles: string[], + expectedRanges: vscode.Range[] + ) => { + return async () => { + const textDocument = await vscode.workspace.openTextDocument(startFile); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + + const locations = await vscode.commands.executeCommand<vscode.Location[]>( + 'vscode.executeDefinitionProvider', + textDocument.uri, + startPosition + ); + assert.equal(locations!.length, expectedFiles.length, 'Wrong number of results'); + + for (let i = 0; i < locations!.length; i += 1) { + assertFile(expectedFiles[i], locations![i].uri); + assertRange(expectedRanges[i], locations![i].range!); + } + }; + }; + + test( + 'From own definition', + buildTest(fileDefinitions, new vscode.Position(2, 6), [fileDefinitions], [new vscode.Range(2, 0, 11, 17)]) + ); + + test( + 'Nested function', + buildTest(fileDefinitions, new vscode.Position(11, 16), [fileDefinitions], [new vscode.Range(6, 4, 10, 16)]) + ); + + test( + 'Decorator usage', + buildTest(fileDefinitions, new vscode.Position(13, 1), [fileDefinitions], [new vscode.Range(2, 0, 11, 17)]) + ); + + test( + 'Function decorated by stdlib', + buildTest(fileDefinitions, new vscode.Position(29, 6), [fileDefinitions], [new vscode.Range(21, 0, 27, 17)]) + ); + + test( + 'Function decorated by local decorator', + buildTest(fileDefinitions, new vscode.Position(30, 6), [fileDefinitions], [new vscode.Range(14, 0, 18, 7)]) + ); + + test( + 'Module imported decorator usage', + buildTest(fileUsages, new vscode.Position(3, 15), [fileDefinitions], [new vscode.Range(2, 0, 11, 17)]) + ); + + test( + 'Module imported function decorated by stdlib', + buildTest(fileUsages, new vscode.Position(11, 19), [fileDefinitions], [new vscode.Range(21, 0, 27, 17)]) + ); + + test( + 'Module imported function decorated by local decorator', + buildTest(fileUsages, new vscode.Position(12, 19), [fileDefinitions], [new vscode.Range(14, 0, 18, 7)]) + ); + + test('Specifically imported decorator usage', async () => { + const navigationTest = buildTest(fileUsages, new vscode.Position(7, 1), isPython2 ? [] : [fileDefinitions], [ + new vscode.Range(2, 0, 11, 17) + ]); + await navigationTest(); + }); + + test('Specifically imported function decorated by stdlib', async () => { + const navigationTest = buildTest(fileUsages, new vscode.Position(14, 6), isPython2 ? [] : [fileDefinitions], [ + new vscode.Range(21, 0, 27, 17) + ]); + await navigationTest(); + }); + + test('Specifically imported function decorated by local decorator', async () => { + const navigationTest = buildTest(fileUsages, new vscode.Position(15, 6), isPython2 ? [] : [fileDefinitions], [ + new vscode.Range(14, 0, 18, 7) + ]); + await navigationTest(); + }); +}); diff --git a/src/test/languageServers/jedi/definitions/parallel.jedi.test.ts b/src/test/languageServers/jedi/definitions/parallel.jedi.test.ts new file mode 100644 index 000000000000..e7b612ad6fb7 --- /dev/null +++ b/src/test/languageServers/jedi/definitions/parallel.jedi.test.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; +import { IS_WINDOWS } from '../../../../client/common/platform/constants'; +import { closeActiveWindows, initialize } from '../../../initialize'; +import { normalizeMarkedString } from '../../../textUtils'; + +const autoCompPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'autocomp'); +const fileOne = path.join(autoCompPath, 'one.py'); + +suite('Code, Hover Definition and Intellisense (Jedi)', () => { + suiteSetup(initialize); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('All three together', async () => { + const textDocument = await vscode.workspace.openTextDocument(fileOne); + + let position = new vscode.Position(30, 5); + const hoverDef = await vscode.commands.executeCommand<vscode.Hover[]>( + 'vscode.executeHoverProvider', + textDocument.uri, + position + ); + const codeDef = await vscode.commands.executeCommand<vscode.Location[]>( + 'vscode.executeDefinitionProvider', + textDocument.uri, + position + ); + position = new vscode.Position(3, 10); + const list = await vscode.commands.executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + position + ); + + assert.equal(list!.items.filter((item) => item.label === 'api_version').length, 1, 'api_version not found'); + + assert.equal(codeDef!.length, 1, 'Definition length is incorrect'); + const expectedPath = IS_WINDOWS ? fileOne.toUpperCase() : fileOne; + const actualPath = IS_WINDOWS ? codeDef![0].uri.fsPath.toUpperCase() : codeDef![0].uri.fsPath; + assert.equal(actualPath, expectedPath, 'Incorrect file'); + assert.equal( + `${codeDef![0].range!.start.line},${codeDef![0].range!.start.character}`, + '17,4', + 'Start position is incorrect' + ); + assert.equal( + `${codeDef![0].range!.end.line},${codeDef![0].range!.end.character}`, + '21,11', + 'End position is incorrect' + ); + + assert.equal(hoverDef!.length, 1, 'Definition length is incorrect'); + assert.equal( + `${hoverDef![0].range!.start.line},${hoverDef![0].range!.start.character}`, + '30,4', + 'Start position is incorrect' + ); + assert.equal( + `${hoverDef![0].range!.end.line},${hoverDef![0].range!.end.character}`, + '30,11', + 'End position is incorrect' + ); + assert.equal(hoverDef![0].contents.length, 1, 'Invalid content items'); + // tslint:disable-next-line:prefer-template + const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; + assert.equal(normalizeMarkedString(hoverDef![0].contents[0]), expectedContent, 'function signature incorrect'); + }); +}); diff --git a/src/test/languageServers/jedi/pythonSignatureProvider.unit.test.ts b/src/test/languageServers/jedi/pythonSignatureProvider.unit.test.ts new file mode 100644 index 000000000000..3d4b87ad9dda --- /dev/null +++ b/src/test/languageServers/jedi/pythonSignatureProvider.unit.test.ts @@ -0,0 +1,333 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { assert, expect, use } from 'chai'; +import * as chaipromise from 'chai-as-promised'; +import * as TypeMoq from 'typemoq'; +import { CancellationToken, Position, SignatureHelp, TextDocument, TextLine, Uri } from 'vscode'; +import { JediFactory } from '../../../client/languageServices/jediProxyFactory'; +import { IArgumentsResult, JediProxyHandler } from '../../../client/providers/jediProxy'; +import { isPositionInsideStringOrComment } from '../../../client/providers/providerUtilities'; +import { PythonSignatureProvider } from '../../../client/providers/signatureProvider'; + +use(chaipromise); + +suite('Signature Provider unit tests', () => { + let pySignatureProvider: PythonSignatureProvider; + let jediHandler: TypeMoq.IMock<JediProxyHandler<IArgumentsResult>>; + let argResultItems: IArgumentsResult; + setup(() => { + const jediFactory = TypeMoq.Mock.ofType(JediFactory); + jediHandler = TypeMoq.Mock.ofType<JediProxyHandler<IArgumentsResult>>(); + jediFactory.setup((j) => j.getJediProxyHandler(TypeMoq.It.isAny())).returns(() => jediHandler.object); + pySignatureProvider = new PythonSignatureProvider(jediFactory.object); + argResultItems = { + definitions: [ + { + description: 'The result', + docstring: 'Some docstring goes here.', + name: 'print', + paramindex: 0, + params: [ + { + description: 'Some parameter', + docstring: 'gimme docs', + name: 'param', + value: 'blah' + } + ] + } + ], + requestId: 1 + }; + }); + + function testSignatureReturns(source: string, pos: number): Thenable<SignatureHelp> { + const doc = TypeMoq.Mock.ofType<TextDocument>(); + const position = new Position(0, pos); + const lineText = TypeMoq.Mock.ofType<TextLine>(); + const argsResult = TypeMoq.Mock.ofType<IArgumentsResult>(); + const cancelToken = TypeMoq.Mock.ofType<CancellationToken>(); + cancelToken.setup((ct) => ct.isCancellationRequested).returns(() => false); + + doc.setup((d) => d.fileName).returns(() => ''); + doc.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => source); + doc.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns(() => lineText.object); + doc.setup((d) => d.offsetAt(TypeMoq.It.isAny())).returns(() => pos - 1); // pos is 1-based + const docUri = TypeMoq.Mock.ofType<Uri>(); + docUri.setup((u) => u.scheme).returns(() => 'http'); + doc.setup((d) => d.uri).returns(() => docUri.object); + lineText.setup((l) => l.text).returns(() => source); + argsResult.setup((c) => c.requestId).returns(() => 1); + // tslint:disable-next-line:no-any + argsResult.setup((c) => c.definitions).returns(() => (argResultItems as any)[0].definitions); + jediHandler + .setup((j) => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve(argResultItems); + }); + + return pySignatureProvider.provideSignatureHelp(doc.object, position, cancelToken.object); + } + + function testIsInsideStringOrComment(sourceLine: string, sourcePos: number): boolean { + const textLine: TypeMoq.IMock<TextLine> = TypeMoq.Mock.ofType<TextLine>(); + textLine.setup((t) => t.text).returns(() => sourceLine); + const doc: TypeMoq.IMock<TextDocument> = TypeMoq.Mock.ofType<TextDocument>(); + const pos: Position = new Position(1, sourcePos); + + doc.setup((d) => d.fileName).returns(() => ''); + doc.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => sourceLine); + doc.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); + doc.setup((d) => d.offsetAt(TypeMoq.It.isAny())).returns(() => sourcePos); + + return isPositionInsideStringOrComment(doc.object, pos); + } + + test('Ensure no signature is given within a string.', async () => { + const source = " print('Python is awesome,')\n"; + const sigHelp: SignatureHelp = await testSignatureReturns(source, 27); + expect(sigHelp).to.not.be.equal( + undefined, + 'Expected to get a blank signature item back - did the pattern change here?' + ); + expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a string?'); + }); + test('Ensure no signature is given within a line comment.', async () => { + const source = "# print('Python is awesome,')\n"; + const sigHelp: SignatureHelp = await testSignatureReturns(source, 28); + expect(sigHelp).to.not.be.equal( + undefined, + 'Expected to get a blank signature item back - did the pattern change here?' + ); + expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a full-line comment?'); + }); + test('Ensure no signature is given within a comment tailing a command.', async () => { + const source = " print('Python') # print('is awesome,')\n"; + const sigHelp: SignatureHelp = await testSignatureReturns(source, 38); + expect(sigHelp).to.not.be.equal( + undefined, + 'Expected to get a blank signature item back - did the pattern change here?' + ); + expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a trailing comment?'); + }); + test('Ensure signature is given for built-in print command.', async () => { + const source = " print('Python',)\n"; + let sigHelp: SignatureHelp; + try { + sigHelp = await testSignatureReturns(source, 18); + expect(sigHelp).to.not.equal( + undefined, + 'Expected to get a blank signature item back - did the pattern change here?' + ); + expect(sigHelp.signatures.length).to.not.equal( + 0, + 'Expected dummy argresult back from testing our print signature.' + ); + expect(sigHelp.activeParameter).to.be.equal( + 0, + "Parameter for print should be the first member of the test argresult's params object." + ); + expect(sigHelp.activeSignature).to.be.equal( + 0, + 'The signature for print should be the first member of the test argresult.' + ); + expect(sigHelp.signatures[sigHelp.activeSignature].label).to.be.equal( + 'print(param)', + `Expected arg result calls for specific returned signature of \'print(param)\' but we got ${ + sigHelp.signatures[sigHelp.activeSignature].label + }` + ); + } catch (error) { + assert(false, `Caught exception ${error}`); + } + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected.', () => { + const sourceLine: string = " print('Hello world!')\n"; + const sourcePos: number = sourceLine.length - 1; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.not.be.equal( + true, + [`Position set to the end of ${sourceLine} but `, 'is reported as being within a string or comment.'].join( + '' + ) + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected at end of source.', () => { + const sourceLine: string = " print('Hello world!')\n"; + const sourcePos: number = 0; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.not.be.equal( + true, + [`Position set to the end of ${sourceLine} but `, 'is reported as being within a string or comment.'].join( + '' + ) + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected at beginning of source.', () => { + const sourceLine: string = " print('Hello world!')\n"; + const sourcePos: number = 0; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.not.be.equal( + true, + [ + `Position set to the beginning of ${sourceLine} but `, + 'is reported as being within a string or comment.' + ].join('') + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected within a string.', () => { + const sourceLine: string = " print('Hello world!')\n"; + const sourcePos: number = 16; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal( + true, + [ + `Position set within the string in ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.' + ].join('') + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected immediately before a string.', () => { + const sourceLine: string = " print('Hello world!')\n"; + const sourcePos: number = 8; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal( + false, + [ + `Position set to just before the string in ${sourceLine} (position ${sourcePos}) but `, + 'is reported as being within a string or comment.' + ].join('') + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected immediately in a string.', () => { + const sourceLine: string = " print('Hello world!')\n"; + const sourcePos: number = 9; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal( + true, + [ + `Position set to the start of the string in ${sourceLine} (position ${sourcePos}) but `, + 'is reported as being within a string or comment.' + ].join('') + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected within a comment.', () => { + const sourceLine: string = "# print('Hello world!')\n"; + const sourcePos: number = 16; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal( + true, + [ + `Position set within a full line comment ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.' + ].join('') + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected within a trailing comment.', () => { + const sourceLine: string = " print('Hello world!') # some comment...\n"; + const sourcePos: number = 34; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal( + true, + [ + `Position set within a trailing line comment ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.' + ].join('') + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected at the very end of a trailing comment.', () => { + const sourceLine: string = " print('Hello world!') # some comment...\n"; + const sourcePos: number = sourceLine.length - 1; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal( + true, + [ + `Position set within a trailing line comment ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.' + ].join('') + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected within a multiline string.', () => { + const sourceLine: string = + " stringVal = '''This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!'''\n"; + const sourcePos: number = 48; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal( + true, + [ + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.' + ].join('') + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected at the very last quote on a multiline string.', () => { + const sourceLine: string = + " stringVal = '''This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!'''\n"; + const sourcePos: number = sourceLine.length - 2; // just at the last ' + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal( + true, + [ + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.' + ].join('') + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected within a multiline string (double-quoted).', () => { + const sourceLine: string = + ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!"""\n'; + const sourcePos: number = 48; + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal( + true, + [ + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.' + ].join('') + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected at the very last quote on a multiline string (double-quoted).', () => { + const sourceLine: string = + ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!"""\n'; + const sourcePos: number = sourceLine.length - 2; // just at the last ' + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal( + true, + [ + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.' + ].join('') + ); + }); + test('Ensure isPositionInsideStringOrComment is behaving as expected during construction of a multiline string (double-quoted).', () => { + const sourceLine: string = ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff'; + const sourcePos: number = sourceLine.length - 1; // just at the last position in the string before it's termination + const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); + + expect(isInsideStrComment).to.be.equal( + true, + [ + `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, + 'is reported as NOT being within a string or comment.' + ].join('') + ); + }); +}); diff --git a/src/test/languageServers/jedi/signature/signature.jedi.test.ts b/src/test/languageServers/jedi/signature/signature.jedi.test.ts new file mode 100644 index 000000000000..c6ceb65e1867 --- /dev/null +++ b/src/test/languageServers/jedi/signature/signature.jedi.test.ts @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; +import { rootWorkspaceUri } from '../../../common'; +import { closeActiveWindows, initialize, initializeTest } from '../../../initialize'; +import { UnitTestIocContainer } from '../../../testing/serviceRegistry'; + +const autoCompPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'signature'); + +class SignatureHelpResult { + constructor( + public line: number, + public index: number, + public signaturesCount: number, + public activeParameter: number, + public parameterName: string | null + ) {} +} + +// tslint:disable-next-line:max-func-body-length +suite('Signatures (Jedi)', () => { + let isPython2: boolean; + let ioc: UnitTestIocContainer; + suiteSetup(async () => { + await initialize(); + initializeDI(); + isPython2 = (await ioc.getPythonMajorVersion(rootWorkspaceUri!)) === 2; + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await ioc.dispose(); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } + + test('For ctor', async () => { + const expected = [ + new SignatureHelpResult(5, 11, 0, 0, null), + new SignatureHelpResult(5, 12, 1, 0, 'name'), + new SignatureHelpResult(5, 13, 0, 0, null), + new SignatureHelpResult(5, 14, 0, 0, null), + new SignatureHelpResult(5, 15, 0, 0, null), + new SignatureHelpResult(5, 16, 0, 0, null), + new SignatureHelpResult(5, 17, 0, 0, null), + new SignatureHelpResult(5, 18, 1, 1, 'age'), + new SignatureHelpResult(5, 19, 1, 1, 'age'), + new SignatureHelpResult(5, 20, 0, 0, null) + ]; + + const document = await openDocument(path.join(autoCompPath, 'classCtor.py')); + for (let i = 0; i < expected.length; i += 1) { + await checkSignature(expected[i], document!.uri, i); + } + }); + + test('For intrinsic', async () => { + let expected: SignatureHelpResult[]; + if (isPython2) { + expected = [ + new SignatureHelpResult(0, 0, 0, 0, null), + new SignatureHelpResult(0, 1, 0, 0, null), + new SignatureHelpResult(0, 2, 0, 0, null), + new SignatureHelpResult(0, 3, 0, 0, null), + new SignatureHelpResult(0, 4, 0, 0, null), + new SignatureHelpResult(0, 5, 0, 0, null), + new SignatureHelpResult(0, 6, 1, 0, 'x'), + new SignatureHelpResult(0, 7, 1, 0, 'x') + ]; + } else { + expected = [ + new SignatureHelpResult(0, 0, 0, 0, null), + new SignatureHelpResult(0, 1, 0, 0, null), + new SignatureHelpResult(0, 2, 0, 0, null), + new SignatureHelpResult(0, 3, 0, 0, null), + new SignatureHelpResult(0, 4, 0, 0, null), + new SignatureHelpResult(0, 5, 0, 0, null), + new SignatureHelpResult(0, 6, 2, 0, 'stop'), + new SignatureHelpResult(0, 7, 2, 0, 'stop') + // new SignatureHelpResult(0, 6, 1, 0, 'start'), + // new SignatureHelpResult(0, 7, 1, 0, 'start'), + // new SignatureHelpResult(0, 8, 1, 1, 'stop'), + // new SignatureHelpResult(0, 9, 1, 1, 'stop'), + // new SignatureHelpResult(0, 10, 1, 1, 'stop'), + // new SignatureHelpResult(0, 11, 1, 2, 'step'), + // new SignatureHelpResult(1, 0, 1, 2, 'step') + ]; + } + + const document = await openDocument(path.join(autoCompPath, 'basicSig.py')); + for (let i = 0; i < expected.length; i += 1) { + await checkSignature(expected[i], document!.uri, i); + } + }); + + test('For ellipsis', async function () { + if (isPython2) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + const expected = [ + new SignatureHelpResult(0, 5, 0, 0, null), + new SignatureHelpResult(0, 6, 1, 0, 'values'), + new SignatureHelpResult(0, 7, 1, 0, 'values'), + new SignatureHelpResult(0, 8, 1, 0, 'values'), + new SignatureHelpResult(0, 9, 1, 0, 'values'), + new SignatureHelpResult(0, 10, 1, 0, 'values'), + new SignatureHelpResult(0, 11, 1, 0, 'values'), + new SignatureHelpResult(0, 12, 1, 0, 'values') + ]; + + const document = await openDocument(path.join(autoCompPath, 'ellipsis.py')); + for (let i = 0; i < expected.length; i += 1) { + await checkSignature(expected[i], document!.uri, i); + } + }); + + test('For pow', async () => { + let expected: SignatureHelpResult; + if (isPython2) { + expected = new SignatureHelpResult(0, 4, 4, 0, 'x'); + } else { + expected = new SignatureHelpResult(0, 4, 4, 0, null); + } + + const document = await openDocument(path.join(autoCompPath, 'noSigPy3.py')); + await checkSignature(expected, document!.uri, 0); + }); +}); + +async function openDocument(documentPath: string): Promise<vscode.TextDocument | undefined> { + const document = await vscode.workspace.openTextDocument(documentPath); + await vscode.window.showTextDocument(document!); + return document; +} + +async function checkSignature(expected: SignatureHelpResult, uri: vscode.Uri, caseIndex: number) { + const position = new vscode.Position(expected.line, expected.index); + const actual = await vscode.commands.executeCommand<vscode.SignatureHelp>( + 'vscode.executeSignatureHelpProvider', + uri, + position + ); + assert.equal( + actual!.signatures.length, + expected.signaturesCount, + `Signature count does not match, case ${caseIndex}` + ); + if (expected.signaturesCount > 0) { + assert.equal( + actual!.activeParameter, + expected.activeParameter, + `Parameter index does not match, case ${caseIndex}` + ); + if (expected.parameterName) { + const parameter = actual!.signatures[0].parameters[expected.activeParameter]; + assert.equal(parameter.label, expected.parameterName, `Parameter name is incorrect, case ${caseIndex}`); + } + } +} diff --git a/src/test/languageServers/jedi/symbolProvider.unit.test.ts b/src/test/languageServers/jedi/symbolProvider.unit.test.ts new file mode 100644 index 000000000000..72235e0f4e3c --- /dev/null +++ b/src/test/languageServers/jedi/symbolProvider.unit.test.ts @@ -0,0 +1,461 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any no-require-imports no-var-requires + +import { expect, use } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { + CancellationToken, + CancellationTokenSource, + CompletionItemKind, + DocumentSymbolProvider, + Location, + Range, + SymbolInformation, + SymbolKind, + TextDocument, + Uri +} from 'vscode'; +import { LanguageClient } from 'vscode-languageclient/node'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { parseRange } from '../../../client/common/utils/text'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { JediFactory } from '../../../client/languageServices/jediProxyFactory'; +import { IDefinition, ISymbolResult, JediProxyHandler } from '../../../client/providers/jediProxy'; +import { JediSymbolProvider, LanguageServerSymbolProvider } from '../../../client/providers/symbolProvider'; + +const assertArrays = require('chai-arrays'); +use(assertArrays); + +suite('Jedi Symbol Provider', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let jediHandler: TypeMoq.IMock<JediProxyHandler<ISymbolResult>>; + let jediFactory: TypeMoq.IMock<JediFactory>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let provider: DocumentSymbolProvider; + let uri: Uri; + let doc: TypeMoq.IMock<TextDocument>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + jediFactory = TypeMoq.Mock.ofType(JediFactory); + jediHandler = TypeMoq.Mock.ofType<JediProxyHandler<ISymbolResult>>(); + + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + doc = TypeMoq.Mock.ofType<TextDocument>(); + jediFactory.setup((j) => j.getJediProxyHandler(TypeMoq.It.isAny())).returns(() => jediHandler.object); + + serviceContainer.setup((c) => c.get(IFileSystem)).returns(() => fileSystem.object); + }); + + async function testDocumentation( + requestId: number, + fileName: string, + expectedSize: number, + token?: CancellationToken, + isUntitled = false + ) { + fileSystem.setup((fs) => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => true); + token = token ? token : new CancellationTokenSource().token; + const symbolResult = TypeMoq.Mock.ofType<ISymbolResult>(); + + const definitions: IDefinition[] = [ + { + container: '', + fileName: fileName, + kind: SymbolKind.Array, + range: { endColumn: 0, endLine: 0, startColumn: 0, startLine: 0 }, + rawType: '', + text: '', + type: CompletionItemKind.Class + } + ]; + + uri = Uri.file(fileName); + doc.setup((d) => d.uri).returns(() => uri); + doc.setup((d) => d.fileName).returns(() => fileName); + doc.setup((d) => d.isUntitled).returns(() => isUntitled); + doc.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => ''); + symbolResult.setup((c) => c.requestId).returns(() => requestId); + symbolResult.setup((c) => c.definitions).returns(() => definitions); + symbolResult.setup((c: any) => c.then).returns(() => undefined); + jediHandler + .setup((j) => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(symbolResult.object)); + + const items = await provider.provideDocumentSymbols(doc.object, token); + expect(items).to.be.array(); + expect(items).to.be.ofSize(expectedSize); + } + + test('Ensure symbols are returned', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await testDocumentation(1, __filename, 1); + }); + test('Ensure symbols are returned (for untitled documents)', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await testDocumentation(1, __filename, 1, undefined, true); + }); + test('Ensure symbols are returned with a debounce of 100ms', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await testDocumentation(1, __filename, 1); + }); + test('Ensure symbols are returned with a debounce of 100ms (for untitled documents)', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await testDocumentation(1, __filename, 1, undefined, true); + }); + test('Ensure symbols are not returned when cancelled', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); + const tokenSource = new CancellationTokenSource(); + tokenSource.cancel(); + await testDocumentation(1, __filename, 0, tokenSource.token); + }); + test('Ensure symbols are not returned when cancelled (for untitled documents)', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); + const tokenSource = new CancellationTokenSource(); + tokenSource.cancel(); + await testDocumentation(1, __filename, 0, tokenSource.token, true); + }); + test('Ensure symbols are returned only for the last request', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 100); + await Promise.all([ + testDocumentation(1, __filename, 0), + testDocumentation(2, __filename, 0), + testDocumentation(3, __filename, 1) + ]); + }); + test('Ensure symbols are returned for all the requests when the doc is untitled', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 100); + await Promise.all([ + testDocumentation(1, __filename, 1, undefined, true), + testDocumentation(2, __filename, 1, undefined, true), + testDocumentation(3, __filename, 1, undefined, true) + ]); + }); + test('Ensure symbols are returned for multiple documents', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await Promise.all([testDocumentation(1, 'file1', 1), testDocumentation(2, 'file2', 1)]); + }); + test('Ensure symbols are returned for multiple untitled documents ', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await Promise.all([ + testDocumentation(1, 'file1', 1, undefined, true), + testDocumentation(2, 'file2', 1, undefined, true) + ]); + }); + test('Ensure symbols are returned for multiple documents with a debounce of 100ms', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 100); + await Promise.all([testDocumentation(1, 'file1', 1), testDocumentation(2, 'file2', 1)]); + }); + test('Ensure symbols are returned for multiple untitled documents with a debounce of 100ms', async () => { + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 100); + await Promise.all([ + testDocumentation(1, 'file1', 1, undefined, true), + testDocumentation(2, 'file2', 1, undefined, true) + ]); + }); + test('Ensure IFileSystem.arePathsSame is used', async () => { + doc.setup((d) => d.getText()) + .returns(() => '') + .verifiable(TypeMoq.Times.once()); + doc.setup((d) => d.isDirty) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + doc.setup((d) => d.fileName).returns(() => __filename); + + const symbols = TypeMoq.Mock.ofType<ISymbolResult>(); + symbols.setup((s: any) => s.then).returns(() => undefined); + const definitions: IDefinition[] = []; + for (let counter = 0; counter < 3; counter += 1) { + const def = TypeMoq.Mock.ofType<IDefinition>(); + def.setup((d) => d.fileName).returns(() => counter.toString()); + definitions.push(def.object); + + fileSystem + .setup((fs) => fs.arePathsSame(TypeMoq.It.isValue(counter.toString()), TypeMoq.It.isValue(__filename))) + .returns(() => false) + .verifiable(TypeMoq.Times.exactly(1)); + } + symbols + .setup((s) => s.definitions) + .returns(() => definitions) + .verifiable(TypeMoq.Times.atLeastOnce()); + + jediHandler + .setup((j) => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(symbols.object)) + .verifiable(TypeMoq.Times.once()); + + provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); + await provider.provideDocumentSymbols(doc.object, new CancellationTokenSource().token); + + doc.verifyAll(); + symbols.verifyAll(); + fileSystem.verifyAll(); + jediHandler.verifyAll(); + }); +}); + +suite('Language Server Symbol Provider', () => { + function createLanguageClient(token: CancellationToken, results: [any, any[]][]): TypeMoq.IMock<LanguageClient> { + const langClient = TypeMoq.Mock.ofType<LanguageClient>(undefined, TypeMoq.MockBehavior.Strict); + for (const [doc, symbols] of results) { + langClient + .setup((l) => + l.sendRequest( + TypeMoq.It.isValue('textDocument/documentSymbol'), + TypeMoq.It.isValue(doc), + TypeMoq.It.isValue(token) + ) + ) + .returns(() => Promise.resolve(symbols)) + .verifiable(TypeMoq.Times.once()); + } + return langClient; + } + + function getRawDoc(uri: Uri) { + return { + textDocument: { + uri: uri.toString() + } + }; + } + + test('Ensure symbols are returned - simple', async () => { + const raw = [ + { + name: 'spam', + kind: SymbolKind.Array + 1, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 } + }, + children: [] + } + ]; + const uri = Uri.file(__filename); + const expected = createSymbols(uri, [['spam', SymbolKind.Array, 0]]); + const doc = createDoc(uri); + const token = new CancellationTokenSource().token; + const langClient = createLanguageClient(token, [[getRawDoc(uri), raw]]); + const provider = new LanguageServerSymbolProvider(langClient.object); + + const items = await provider.provideDocumentSymbols(doc.object, token); + + expect(items).to.deep.equal(expected); + doc.verifyAll(); + langClient.verifyAll(); + }); + test('Ensure symbols are returned - minimal', async () => { + const uri = Uri.file(__filename); + + // The test data is loosely based on the "full" test. + const raw = [ + { + name: 'SpamTests', + kind: 5, + range: { + start: { line: 2, character: 6 }, + end: { line: 2, character: 15 } + }, + children: [ + { + name: 'test_all', + kind: 12, + range: { + start: { line: 3, character: 8 }, + end: { line: 3, character: 16 } + }, + children: [ + { + name: 'self', + kind: 13, + range: { + start: { line: 3, character: 17 }, + end: { line: 3, character: 21 } + }, + children: [] + } + ] + }, + { + name: 'assertTrue', + kind: 13, + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 } + }, + children: [] + } + ] + } + ]; + const expected = [ + new SymbolInformation('SpamTests', SymbolKind.Class, '', new Location(uri, new Range(2, 6, 2, 15))), + new SymbolInformation( + 'test_all', + SymbolKind.Function, + 'SpamTests', + new Location(uri, new Range(3, 8, 3, 16)) + ), + new SymbolInformation('self', SymbolKind.Variable, 'test_all', new Location(uri, new Range(3, 17, 3, 21))), + new SymbolInformation( + 'assertTrue', + SymbolKind.Variable, + 'SpamTests', + new Location(uri, new Range(0, 0, 0, 0)) + ) + ]; + + const doc = createDoc(uri); + const token = new CancellationTokenSource().token; + const langClient = createLanguageClient(token, [[getRawDoc(uri), raw]]); + const provider = new LanguageServerSymbolProvider(langClient.object); + + const items = await provider.provideDocumentSymbols(doc.object, token); + + expect(items).to.deep.equal(expected); + }); + test('Ensure symbols are returned - full', async () => { + const uri = Uri.file(__filename); + + // This is the raw symbol data returned by the language server which + // gets converted to SymbolInformation[]. It was captured from an + // actual VS Code session for a file with the following code: + // + // import unittest + // + // class SpamTests(unittest.TestCase): + // def test_all(self): + // self.assertTrue(False) + // + // See: LanguageServerSymbolProvider.provideDocumentSymbols() + // tslint:disable-next-line:no-suspicious-comment + // TODO: Change "raw" once the following issues are resolved: + // * https://github.com/Microsoft/python-language-server/issues/1 + // * https://github.com/Microsoft/python-language-server/issues/2 + const raw = JSON.parse( + '[{"name":"SpamTests","detail":"SpamTests","kind":5,"deprecated":false,"range":{"start":{"line":2,"character":6},"end":{"line":2,"character":15}},"selectionRange":{"start":{"line":2,"character":6},"end":{"line":2,"character":15}},"children":[{"name":"test_all","detail":"test_all","kind":12,"deprecated":false,"range":{"start":{"line":3,"character":4},"end":{"line":4,"character":30}},"selectionRange":{"start":{"line":3,"character":4},"end":{"line":4,"character":30}},"children":[{"name":"self","detail":"self","kind":13,"deprecated":false,"range":{"start":{"line":3,"character":17},"end":{"line":3,"character":21}},"selectionRange":{"start":{"line":3,"character":17},"end":{"line":3,"character":21}},"children":[],"_functionKind":""}],"_functionKind":"function"},{"name":"assertTrue","detail":"assertTrue","kind":13,"deprecated":false,"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":0}},"selectionRange":{"start":{"line":0,"character":0},"end":{"line":0,"character":0}},"children":[],"_functionKind":""}],"_functionKind":"class"}]' + ); + raw[0].children[0].range.start.character = 8; + raw[0].children[0].range.end.line = 3; + raw[0].children[0].range.end.character = 16; + + // This is the data from Jedi corresponding to same Python code + // for which the raw data above was generated. + // See: JediSymbolProvider.provideDocumentSymbols() + const expectedRaw = JSON.parse( + '[{"name":"unittest","kind":1,"location":{"uri":{"$mid":1,"path":"<some file>","scheme":"file"},"range":[{"line":0,"character":7},{"line":0,"character":15}]},"containerName":""},{"name":"SpamTests","kind":4,"location":{"uri":{"$mid":1,"path":"<some file>","scheme":"file"},"range":[{"line":2,"character":0},{"line":4,"character":29}]},"containerName":""},{"name":"test_all","kind":11,"location":{"uri":{"$mid":1,"path":"<some file>","scheme":"file"},"range":[{"line":3,"character":4},{"line":4,"character":29}]},"containerName":"SpamTests"},{"name":"self","kind":12,"location":{"uri":{"$mid":1,"path":"<some file>","scheme":"file"},"range":[{"line":3,"character":17},{"line":3,"character":21}]},"containerName":"test_all"}]' + ); + expectedRaw[1].location.range[0].character = 6; + expectedRaw[1].location.range[1].line = 2; + expectedRaw[1].location.range[1].character = 15; + expectedRaw[2].location.range[0].character = 8; + expectedRaw[2].location.range[1].line = 3; + expectedRaw[2].location.range[1].character = 16; + const expected = normalizeSymbols(uri, expectedRaw); + expected.shift(); // For now, drop the "unittest" symbol. + expected.push( + new SymbolInformation( + 'assertTrue', + SymbolKind.Variable, + 'SpamTests', + new Location(uri, new Range(0, 0, 0, 0)) + ) + ); + + const doc = createDoc(uri); + const token = new CancellationTokenSource().token; + const langClient = createLanguageClient(token, [[getRawDoc(uri), raw]]); + const provider = new LanguageServerSymbolProvider(langClient.object); + + const items = await provider.provideDocumentSymbols(doc.object, token); + + expect(items).to.deep.equal(expected); + }); +}); + +//################################ +// helpers + +function createDoc(uri?: Uri, filename?: string, isUntitled?: boolean, text?: string): TypeMoq.IMock<TextDocument> { + const doc = TypeMoq.Mock.ofType<TextDocument>(undefined, TypeMoq.MockBehavior.Strict); + if (uri !== undefined) { + doc.setup((d) => d.uri).returns(() => uri); + } + if (filename !== undefined) { + doc.setup((d) => d.fileName).returns(() => filename); + } + if (isUntitled !== undefined) { + doc.setup((d) => d.isUntitled).returns(() => isUntitled); + } + if (text !== undefined) { + doc.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => text); + } + return doc; +} + +function createSymbols(uri: Uri, info: [string, SymbolKind, string | number][]): SymbolInformation[] { + const symbols: SymbolInformation[] = []; + for (const [fullName, kind, range] of info) { + const symbol = createSymbol(uri, fullName, kind, range); + symbols.push(symbol); + } + return symbols; +} + +function createSymbol(uri: Uri, fullName: string, kind: SymbolKind, rawRange: string | number = ''): SymbolInformation { + const [containerName, name] = splitParent(fullName); + const range = parseRange(rawRange); + const loc = new Location(uri, range); + return new SymbolInformation(name, kind, containerName, loc); +} + +function normalizeSymbols(uri: Uri, raw: any[]): SymbolInformation[] { + const symbols: SymbolInformation[] = []; + for (const item of raw) { + const symbol = new SymbolInformation( + item.name, + // Type coercion is a bit fuzzy when it comes to enums, so we + // play it safe by explicitly converting. + (SymbolKind as any)[(SymbolKind as any)[item.kind]], + item.containerName, + new Location( + uri, + new Range( + item.location.range[0].line, + item.location.range[0].character, + item.location.range[1].line, + item.location.range[1].character + ) + ) + ); + symbols.push(symbol); + } + return symbols; +} + +/** + * Return [parent name, name] for the given qualified (dotted) name. + * + * Examples: + * 'x.y' -> ['x', 'y'] + * 'x' -> ['', 'x'] + * 'x.y.z' -> ['x.y', 'z'] + * '' -> ['', ''] + */ +export function splitParent(fullName: string): [string, string] { + if (fullName.length === 0) { + return ['', '']; + } + const pos = fullName.lastIndexOf('.'); + if (pos < 0) { + return ['', fullName]; + } + const parentName = fullName.slice(0, pos); + const name = fullName.slice(pos + 1); + return [parentName, name]; +} diff --git a/src/test/linters/common.ts b/src/test/linters/common.ts new file mode 100644 index 000000000000..4c5d03f066ef --- /dev/null +++ b/src/test/linters/common.ts @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as os from 'os'; +import * as TypeMoq from 'typemoq'; +import { DiagnosticSeverity, TextDocument, Uri, WorkspaceFolder } from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; +import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { Product } from '../../client/common/installer/productInstaller'; +import { ProductNames } from '../../client/common/installer/productNames'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { IPythonExecutionFactory, IPythonToolExecutionService } from '../../client/common/process/types'; +import { + Flake8CategorySeverity, + IConfigurationService, + IInstaller, + IMypyCategorySeverity, + IOutputChannel, + IPycodestyleCategorySeverity, + IPylintCategorySeverity, + IPythonSettings +} from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; +import { LinterManager } from '../../client/linters/linterManager'; +import { ILinter, ILinterManager, ILintMessage, LinterId } from '../../client/linters/types'; + +export function newMockDocument(filename: string): TypeMoq.IMock<TextDocument> { + const uri = Uri.file(filename); + const doc = TypeMoq.Mock.ofType<TextDocument>(undefined, TypeMoq.MockBehavior.Strict); + doc.setup((s) => s.uri).returns(() => uri); + return doc; +} + +export function linterMessageAsLine(msg: ILintMessage): string { + switch (msg.provider) { + case 'pydocstyle': { + return `<filename>:${msg.line} spam:${os.EOL}\t${msg.code}: ${msg.message}`; + } + default: { + return `${msg.line},${msg.column},${msg.type},${msg.code}:${msg.message}`; + } + } +} + +export function getLinterID(product: Product): LinterId { + const linterID = LINTERID_BY_PRODUCT.get(product); + if (!linterID) { + throwUnknownProduct(product); + } + return linterID!; +} + +export function getProductName(product: Product, capitalize = true): string { + let prodName = ProductNames.get(product); + if (!prodName) { + prodName = Product[product]; + } + if (capitalize) { + return prodName.charAt(0).toUpperCase() + prodName.slice(1); + } else { + return prodName; + } +} + +export function throwUnknownProduct(product: Product) { + throw Error(`unsupported product ${Product[product]} (${product})`); +} + +export class LintingSettings { + public enabled: boolean; + public ignorePatterns: string[]; + public prospectorEnabled: boolean; + public prospectorArgs: string[]; + public pylintEnabled: boolean; + public pylintArgs: string[]; + public pycodestyleEnabled: boolean; + public pycodestyleArgs: string[]; + public pylamaEnabled: boolean; + public pylamaArgs: string[]; + public flake8Enabled: boolean; + public flake8Args: string[]; + public pydocstyleEnabled: boolean; + public pydocstyleArgs: string[]; + public lintOnSave: boolean; + public maxNumberOfProblems: number; + public pylintCategorySeverity: IPylintCategorySeverity; + public pycodestyleCategorySeverity: IPycodestyleCategorySeverity; + public flake8CategorySeverity: Flake8CategorySeverity; + public mypyCategorySeverity: IMypyCategorySeverity; + public prospectorPath: string; + public pylintPath: string; + public pycodestylePath: string; + public pylamaPath: string; + public flake8Path: string; + public pydocstylePath: string; + public mypyEnabled: boolean; + public mypyArgs: string[]; + public mypyPath: string; + public banditEnabled: boolean; + public banditArgs: string[]; + public banditPath: string; + public pylintUseMinimalCheckers: boolean; + + constructor() { + // mostly from configSettings.ts + + this.enabled = true; + this.ignorePatterns = []; + this.lintOnSave = false; + this.maxNumberOfProblems = 100; + + this.flake8Enabled = false; + this.flake8Path = 'flake8'; + this.flake8Args = []; + this.flake8CategorySeverity = { + E: DiagnosticSeverity.Error, + W: DiagnosticSeverity.Warning, + F: DiagnosticSeverity.Warning + }; + + this.mypyEnabled = false; + this.mypyPath = 'mypy'; + this.mypyArgs = []; + this.mypyCategorySeverity = { + error: DiagnosticSeverity.Error, + note: DiagnosticSeverity.Hint + }; + + this.banditEnabled = false; + this.banditPath = 'bandit'; + this.banditArgs = []; + + this.pycodestyleEnabled = false; + this.pycodestylePath = 'pycodestyle'; + this.pycodestyleArgs = []; + this.pycodestyleCategorySeverity = { + E: DiagnosticSeverity.Error, + W: DiagnosticSeverity.Warning + }; + + this.pylamaEnabled = false; + this.pylamaPath = 'pylama'; + this.pylamaArgs = []; + + this.prospectorEnabled = false; + this.prospectorPath = 'prospector'; + this.prospectorArgs = []; + + this.pydocstyleEnabled = false; + this.pydocstylePath = 'pydocstyle'; + this.pydocstyleArgs = []; + + this.pylintEnabled = false; + this.pylintPath = 'pylint'; + this.pylintArgs = []; + this.pylintCategorySeverity = { + convention: DiagnosticSeverity.Hint, + error: DiagnosticSeverity.Error, + fatal: DiagnosticSeverity.Error, + refactor: DiagnosticSeverity.Hint, + warning: DiagnosticSeverity.Warning + }; + this.pylintUseMinimalCheckers = false; + } +} + +export class BaseTestFixture { + public serviceContainer: TypeMoq.IMock<IServiceContainer>; + public linterManager: LinterManager; + + // services + public workspaceService: TypeMoq.IMock<IWorkspaceService>; + public installer: TypeMoq.IMock<IInstaller>; + public appShell: TypeMoq.IMock<IApplicationShell>; + + // config + public configService: TypeMoq.IMock<IConfigurationService>; + public pythonSettings: TypeMoq.IMock<IPythonSettings>; + public lintingSettings: LintingSettings; + + // data + public outputChannel: TypeMoq.IMock<IOutputChannel>; + + // artifacts + public output: string; + public logged: string[]; + + constructor( + platformService: IPlatformService, + filesystem: IFileSystem, + pythonToolExecService: IPythonToolExecutionService, + pythonExecFactory: IPythonExecutionFactory, + configService?: TypeMoq.IMock<IConfigurationService>, + serviceContainer?: TypeMoq.IMock<IServiceContainer>, + ignoreConfigUpdates = false, + public readonly workspaceDir = '.', + protected readonly printLogs = false + ) { + this.serviceContainer = serviceContainer + ? serviceContainer + : TypeMoq.Mock.ofType<IServiceContainer>(undefined, TypeMoq.MockBehavior.Strict); + + // services + + this.workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(undefined, TypeMoq.MockBehavior.Strict); + this.installer = TypeMoq.Mock.ofType<IInstaller>(undefined, TypeMoq.MockBehavior.Strict); + this.appShell = TypeMoq.Mock.ofType<IApplicationShell>(undefined, TypeMoq.MockBehavior.Strict); + + this.serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) + .returns(() => filesystem); + this.serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) + .returns(() => this.workspaceService.object); + this.serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInstaller), TypeMoq.It.isAny())) + .returns(() => this.installer.object); + this.serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())) + .returns(() => platformService); + this.serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPythonToolExecutionService), TypeMoq.It.isAny())) + .returns(() => pythonToolExecService); + this.serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPythonExecutionFactory), TypeMoq.It.isAny())) + .returns(() => pythonExecFactory); + this.serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) + .returns(() => this.appShell.object); + this.initServices(); + + // config + + this.configService = configService + ? configService + : TypeMoq.Mock.ofType<IConfigurationService>(undefined, TypeMoq.MockBehavior.Strict); + this.pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(undefined, TypeMoq.MockBehavior.Strict); + this.lintingSettings = new LintingSettings(); + + this.serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) + .returns(() => this.configService.object); + this.configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => this.pythonSettings.object); + this.pythonSettings.setup((s) => s.linting).returns(() => this.lintingSettings); + this.initConfig(ignoreConfigUpdates); + + // data + + this.outputChannel = TypeMoq.Mock.ofType<IOutputChannel>(undefined, TypeMoq.MockBehavior.Strict); + + this.serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) + .returns(() => this.outputChannel.object); + this.initData(); + + // artifacts + + this.output = ''; + this.logged = []; + + // linting + + this.linterManager = new LinterManager(this.serviceContainer.object, this.workspaceService.object!); + this.serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) + .returns(() => this.linterManager); + } + + public async getLinter(product: Product, enabled = true): Promise<ILinter> { + const info = this.linterManager.getLinterInfo(product); + // tslint:disable-next-line:no-any + (this.lintingSettings as any)[info.enabledSettingName] = enabled; + + await this.linterManager.setActiveLintersAsync([product]); + await this.linterManager.enableLintingAsync(enabled); + return this.linterManager.createLinter(product, this.outputChannel.object, this.serviceContainer.object); + } + + public async getEnabledLinter(product: Product): Promise<ILinter> { + return this.getLinter(product, true); + } + + public async getDisabledLinter(product: Product): Promise<ILinter> { + return this.getLinter(product, false); + } + + protected newMockDocument(filename: string): TypeMoq.IMock<TextDocument> { + return newMockDocument(filename); + } + + private initServices(): void { + const workspaceFolder = TypeMoq.Mock.ofType<WorkspaceFolder>(undefined, TypeMoq.MockBehavior.Strict); + workspaceFolder.setup((f) => f.uri).returns(() => Uri.file(this.workspaceDir)); + this.workspaceService + .setup((s) => s.getWorkspaceFolder(TypeMoq.It.isAny())) + .returns(() => workspaceFolder.object); + + this.appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + } + + private initConfig(ignoreUpdates = false): void { + this.configService + .setup((c) => + c.updateSetting(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .callback((setting, value) => { + if (ignoreUpdates) { + return; + } + const prefix = 'linting.'; + if (setting.startsWith(prefix)) { + // tslint:disable-next-line:no-any + (this.lintingSettings as any)[setting.substring(prefix.length)] = value; + } + }) + .returns(() => Promise.resolve(undefined)); + + this.pythonSettings.setup((s) => s.languageServer).returns(() => LanguageServerType.Jedi); + } + + private initData(): void { + this.outputChannel + .setup((o) => o.appendLine(TypeMoq.It.isAny())) + .callback((line) => { + if (this.output === '') { + this.output = line; + } else { + this.output = `${this.output}${os.EOL}${line}`; + } + }); + this.outputChannel + .setup((o) => o.append(TypeMoq.It.isAny())) + .callback((data) => { + this.output += data; + }); + this.outputChannel.setup((o) => o.show()); + } +} diff --git a/src/test/linters/lint.args.test.ts b/src/test/linters/lint.args.test.ts new file mode 100644 index 000000000000..0cac56804c11 --- /dev/null +++ b/src/test/linters/lint.args.test.ts @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { expect } from 'chai'; +import { Container } from 'inversify'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { CancellationTokenSource, OutputChannel, TextDocument, Uri, WorkspaceFolder } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import '../../client/common/extensions'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { + IConfigurationService, + IInstaller, + ILintingSettings, + IOutputChannel, + IPythonSettings +} from '../../client/common/types'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService +} from '../../client/interpreter/autoSelection/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { Bandit } from '../../client/linters/bandit'; +import { BaseLinter } from '../../client/linters/baseLinter'; +import { Flake8 } from '../../client/linters/flake8'; +import { LinterManager } from '../../client/linters/linterManager'; +import { MyPy } from '../../client/linters/mypy'; +import { Prospector } from '../../client/linters/prospector'; +import { Pycodestyle } from '../../client/linters/pycodestyle'; +import { PyDocStyle } from '../../client/linters/pydocstyle'; +import { PyLama } from '../../client/linters/pylama'; +import { Pylint } from '../../client/linters/pylint'; +import { ILinterManager, ILintingEngine } from '../../client/linters/types'; +import { initialize } from '../initialize'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; + +suite('Linting - Arguments', () => { + [undefined, path.join('users', 'dev_user')].forEach((workspaceUri) => { + [ + Uri.file(path.join('users', 'dev_user', 'development path to', 'one.py')), + Uri.file(path.join('users', 'dev_user', 'development', 'one.py')) + ].forEach((fileUri) => { + suite( + `File path ${fileUri.fsPath.indexOf(' ') > 0 ? 'with' : 'without'} spaces and ${ + workspaceUri ? 'without' : 'with' + } a workspace`, + () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let engine: TypeMoq.IMock<ILintingEngine>; + let configService: TypeMoq.IMock<IConfigurationService>; + let docManager: TypeMoq.IMock<IDocumentManager>; + let settings: TypeMoq.IMock<IPythonSettings>; + let lm: ILinterManager; + let serviceContainer: ServiceContainer; + let document: TypeMoq.IMock<TextDocument>; + let outputChannel: TypeMoq.IMock<OutputChannel>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + const cancellationToken = new CancellationTokenSource().token; + suiteSetup(initialize); + setup(async () => { + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + + serviceContainer = new ServiceContainer(cont); + outputChannel = TypeMoq.Mock.ofType<OutputChannel>(); + + const fs = TypeMoq.Mock.ofType<IFileSystem>(); + fs.setup((x) => x.fileExists(TypeMoq.It.isAny())).returns( + () => new Promise<boolean>((resolve, _reject) => resolve(true)) + ); + fs.setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns( + () => true + ); + serviceManager.addSingletonInstance<IFileSystem>(IFileSystem, fs.object); + + serviceManager.addSingletonInstance(IOutputChannel, outputChannel.object); + + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + serviceManager.addSingletonInstance<IInterpreterService>( + IInterpreterService, + interpreterService.object + ); + serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService + ); + serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( + IInterpreterAutoSeletionProxyService, + MockAutoSelectionService + ); + engine = TypeMoq.Mock.ofType<ILintingEngine>(); + serviceManager.addSingletonInstance<ILintingEngine>(ILintingEngine, engine.object); + + docManager = TypeMoq.Mock.ofType<IDocumentManager>(); + serviceManager.addSingletonInstance<IDocumentManager>(IDocumentManager, docManager.object); + + const lintSettings = TypeMoq.Mock.ofType<ILintingSettings>(); + lintSettings.setup((x) => x.enabled).returns(() => true); + lintSettings.setup((x) => x.lintOnSave).returns(() => true); + + settings = TypeMoq.Mock.ofType<IPythonSettings>(); + settings.setup((x) => x.linting).returns(() => lintSettings.object); + + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceManager.addSingletonInstance<IConfigurationService>( + IConfigurationService, + configService.object + ); + + const workspaceFolder: WorkspaceFolder | undefined = workspaceUri + ? { uri: Uri.file(workspaceUri), index: 0, name: '' } + : undefined; + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())) + .returns(() => workspaceFolder); + serviceManager.addSingletonInstance<IWorkspaceService>( + IWorkspaceService, + workspaceService.object + ); + + const installer = TypeMoq.Mock.ofType<IInstaller>(); + serviceManager.addSingletonInstance<IInstaller>(IInstaller, installer.object); + + const platformService = TypeMoq.Mock.ofType<IPlatformService>(); + serviceManager.addSingletonInstance<IPlatformService>(IPlatformService, platformService.object); + + lm = new LinterManager(serviceContainer, workspaceService.object); + serviceManager.addSingletonInstance<ILinterManager>(ILinterManager, lm); + document = TypeMoq.Mock.ofType<TextDocument>(); + }); + + async function testLinter(linter: BaseLinter, expectedArgs: string[]) { + document.setup((d) => d.uri).returns(() => fileUri); + + let invoked = false; + (linter as any).run = (args: string[]) => { + expect(args).to.deep.equal(expectedArgs); + invoked = true; + return Promise.resolve([]); + }; + await linter.lint(document.object, cancellationToken); + expect(invoked).to.be.equal(true, 'method not invoked'); + } + test('Flake8', async () => { + const linter = new Flake8(outputChannel.object, serviceContainer); + const expectedArgs = ['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', fileUri.fsPath]; + await testLinter(linter, expectedArgs); + }); + test('Pycodestyle', async () => { + const linter = new Pycodestyle(outputChannel.object, serviceContainer); + const expectedArgs = ['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', fileUri.fsPath]; + await testLinter(linter, expectedArgs); + }); + test('Prospector', async () => { + const linter = new Prospector(outputChannel.object, serviceContainer); + const expectedPath = workspaceUri + ? fileUri.fsPath.substring(workspaceUri.length + 2) + : path.basename(fileUri.fsPath); + const expectedArgs = ['--absolute-paths', '--output-format=json', expectedPath]; + await testLinter(linter, expectedArgs); + }); + test('Pylama', async () => { + const linter = new PyLama(outputChannel.object, serviceContainer); + const expectedArgs = ['--format=parsable', fileUri.fsPath]; + await testLinter(linter, expectedArgs); + }); + test('MyPy', async () => { + const linter = new MyPy(outputChannel.object, serviceContainer); + const expectedArgs = [fileUri.fsPath]; + await testLinter(linter, expectedArgs); + }); + test('Pydocstyle', async () => { + const linter = new PyDocStyle(outputChannel.object, serviceContainer); + const expectedArgs = [fileUri.fsPath]; + await testLinter(linter, expectedArgs); + }); + test('Pylint', async () => { + const linter = new Pylint(outputChannel.object, serviceContainer); + document.setup((d) => d.uri).returns(() => fileUri); + + let invoked = false; + (linter as any).run = (args: any[], _doc: any, _token: any) => { + expect(args[args.length - 1]).to.equal(fileUri.fsPath); + invoked = true; + return Promise.resolve([]); + }; + await linter.lint(document.object, cancellationToken); + expect(invoked).to.be.equal(true, 'method not invoked'); + }); + test('Bandit', async () => { + const linter = new Bandit(outputChannel.object, serviceContainer); + const expectedArgs = [ + '-f', + 'custom', + '--msg-template', + '{line},0,{severity},{test_id}:{msg}', + '-n', + '-1', + fileUri.fsPath + ]; + await testLinter(linter, expectedArgs); + }); + } + ); + }); + }); +}); diff --git a/src/test/linters/lint.functional.test.ts b/src/test/linters/lint.functional.test.ts new file mode 100644 index 000000000000..7d7f30676cb6 --- /dev/null +++ b/src/test/linters/lint.functional.test.ts @@ -0,0 +1,855 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import * as child_process from 'child_process'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { CancellationTokenSource, TextDocument, TextLine, Uri } from 'vscode'; +import { Product } from '../../client/common/installer/productInstaller'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { PlatformService } from '../../client/common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { BufferDecoder } from '../../client/common/process/decoder'; +import { ProcessServiceFactory } from '../../client/common/process/processFactory'; +import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; +import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; +import { + IBufferDecoder, + IProcessLogger, + IPythonExecutionFactory, + IPythonToolExecutionService +} from '../../client/common/process/types'; +import { IConfigurationService, IDisposableRegistry } from '../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; +import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; +import { IServiceContainer } from '../../client/ioc/types'; +import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; +import { ILintMessage, LinterId, LintMessageSeverity } from '../../client/linters/types'; +import { WindowsStoreInterpreter } from '../../client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter'; +import { deleteFile, PYTHON_PATH } from '../common'; +import { BaseTestFixture, getLinterID, getProductName, newMockDocument, throwUnknownProduct } from './common'; + +const workspaceDir = path.join(__dirname, '..', '..', '..', 'src', 'test'); +const workspaceUri = Uri.file(workspaceDir); +const pythonFilesDir = path.join(workspaceDir, 'pythonFiles', 'linting'); +const fileToLint = path.join(pythonFilesDir, 'file.py'); + +const linterConfigDirs = new Map<LinterId, string>([ + [LinterId.Flake8, path.join(pythonFilesDir, 'flake8config')], + [LinterId.PyCodeStyle, path.join(pythonFilesDir, 'pycodestyleconfig')], + [LinterId.PyDocStyle, path.join(pythonFilesDir, 'pydocstyleconfig27')], + [LinterId.PyLint, path.join(pythonFilesDir, 'pylintconfig')] +]); +const linterConfigRCFiles = new Map<LinterId, string>([ + [LinterId.PyLint, '.pylintrc'], + [LinterId.PyDocStyle, '.pydocstyle'] +]); + +const pylintMessagesToBeReturned: ILintMessage[] = [ + { + line: 24, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 30, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 34, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0012', + message: 'Locally enabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 40, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 44, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0012', + message: 'Locally enabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 55, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 59, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0012', + message: 'Locally enabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 62, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling undefined-variable (E0602)', + provider: '', + type: 'warning' + }, + { + line: 70, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 84, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 87, + column: 0, + severity: LintMessageSeverity.Hint, + code: 'C0304', + message: 'Final newline missing', + provider: '', + type: 'warning' + }, + { + line: 11, + column: 20, + severity: LintMessageSeverity.Warning, + code: 'W0613', + message: "Unused argument 'arg'", + provider: '', + type: 'warning' + }, + { + line: 26, + column: 14, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blop' member", + provider: '', + type: 'warning' + }, + { + line: 36, + column: 14, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 46, + column: 18, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 61, + column: 18, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 72, + column: 18, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 75, + column: 18, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 77, + column: 14, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 83, + column: 14, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + } +]; +const flake8MessagesToBeReturned: ILintMessage[] = [ + { + line: 5, + column: 1, + severity: LintMessageSeverity.Error, + code: 'E302', + message: 'expected 2 blank lines, found 1', + provider: '', + type: 'E' + }, + { + line: 19, + column: 15, + severity: LintMessageSeverity.Error, + code: 'E127', + message: 'continuation line over-indented for visual indent', + provider: '', + type: 'E' + }, + { + line: 24, + column: 23, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 62, + column: 30, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 70, + column: 22, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 80, + column: 5, + severity: LintMessageSeverity.Error, + code: 'E303', + message: 'too many blank lines (2)', + provider: '', + type: 'E' + }, + { + line: 87, + column: 24, + severity: LintMessageSeverity.Warning, + code: 'W292', + message: 'no newline at end of file', + provider: '', + type: 'E' + } +]; +const pycodestyleMessagesToBeReturned: ILintMessage[] = [ + { + line: 5, + column: 1, + severity: LintMessageSeverity.Error, + code: 'E302', + message: 'expected 2 blank lines, found 1', + provider: '', + type: 'E' + }, + { + line: 19, + column: 15, + severity: LintMessageSeverity.Error, + code: 'E127', + message: 'continuation line over-indented for visual indent', + provider: '', + type: 'E' + }, + { + line: 24, + column: 23, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 62, + column: 30, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 70, + column: 22, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 80, + column: 5, + severity: LintMessageSeverity.Error, + code: 'E303', + message: 'too many blank lines (2)', + provider: '', + type: 'E' + }, + { + line: 87, + column: 24, + severity: LintMessageSeverity.Warning, + code: 'W292', + message: 'no newline at end of file', + provider: '', + type: 'E' + } +]; +const pydocstyleMessagesToBeReturned: ILintMessage[] = [ + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'e')", + column: 0, + line: 1, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 't')", + column: 0, + line: 5, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D102', + severity: LintMessageSeverity.Information, + message: 'Missing docstring in public method', + column: 4, + line: 8, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D401', + severity: LintMessageSeverity.Information, + message: "First line should be in imperative mood ('thi', not 'this')", + column: 4, + line: 11, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('This', not 'this')", + column: 4, + line: 11, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'e')", + column: 4, + line: 11, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('And', not 'and')", + column: 4, + line: 15, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 't')", + column: 4, + line: 15, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 21, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 21, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 28, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 28, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 38, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 38, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 53, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 53, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 68, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 68, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 80, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 80, + type: '', + provider: 'pydocstyle' + } +]; + +const filteredFlake8MessagesToBeReturned: ILintMessage[] = [ + { + line: 87, + column: 24, + severity: LintMessageSeverity.Warning, + code: 'W292', + message: 'no newline at end of file', + provider: '', + type: '' + } +]; +const filteredPycodestyleMessagesToBeReturned: ILintMessage[] = [ + { + line: 87, + column: 24, + severity: LintMessageSeverity.Warning, + code: 'W292', + message: 'no newline at end of file', + provider: '', + type: '' + } +]; + +function getMessages(product: Product): ILintMessage[] { + switch (product) { + case Product.pylint: { + return pylintMessagesToBeReturned; + } + case Product.flake8: { + return flake8MessagesToBeReturned; + } + case Product.pycodestyle: { + return pycodestyleMessagesToBeReturned; + } + case Product.pydocstyle: { + return pydocstyleMessagesToBeReturned; + } + default: { + throwUnknownProduct(product); + return []; // to quiet tslint + } + } +} + +async function getInfoForConfig(product: Product) { + const prodID = getLinterID(product); + const dirname = linterConfigDirs.get(prodID); + assert.notEqual(dirname, undefined, `tests not set up for ${Product[product]}`); + + const filename = path.join(dirname!, product === Product.pylint ? 'file2.py' : 'file.py'); + let messagesToBeReceived: ILintMessage[] = []; + switch (product) { + case Product.flake8: { + messagesToBeReceived = filteredFlake8MessagesToBeReturned; + break; + } + case Product.pycodestyle: { + messagesToBeReceived = filteredPycodestyleMessagesToBeReturned; + break; + } + default: { + break; + } + } + const basename = linterConfigRCFiles.get(prodID); + return { + filename, + messagesToBeReceived, + origRCFile: basename ? path.join(dirname!, basename) : '' + }; +} + +class TestFixture extends BaseTestFixture { + constructor(printLogs = false) { + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(undefined, TypeMoq.MockBehavior.Strict); + const configService = TypeMoq.Mock.ofType<IConfigurationService>(undefined, TypeMoq.MockBehavior.Strict); + const processLogger = TypeMoq.Mock.ofType<IProcessLogger>(undefined, TypeMoq.MockBehavior.Strict); + const filesystem = new FileSystem(); + processLogger + .setup((p) => p.logProcess(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return; + }); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IProcessLogger), TypeMoq.It.isAny())) + .returns(() => processLogger.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) + .returns(() => filesystem); + + const platformService = new PlatformService(); + + super( + platformService, + filesystem, + TestFixture.newPythonToolExecService(serviceContainer.object), + TestFixture.newPythonExecFactory(serviceContainer, configService.object), + configService, + serviceContainer, + false, + workspaceDir, + printLogs + ); + + this.pythonSettings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); + } + + private static newPythonToolExecService(serviceContainer: IServiceContainer): IPythonToolExecutionService { + // We do not worry about the IProcessServiceFactory possibly + // needed by PythonToolExecutionService. + return new PythonToolExecutionService(serviceContainer); + } + + private static newPythonExecFactory( + serviceContainer: TypeMoq.IMock<IServiceContainer>, + configService: IConfigurationService + ): IPythonExecutionFactory { + const envVarsService = TypeMoq.Mock.ofType<IEnvironmentVariablesProvider>( + undefined, + TypeMoq.MockBehavior.Strict + ); + envVarsService + .setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(process.env)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())) + .returns(() => envVarsService.object); + const disposableRegistry: IDisposableRegistry = []; + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) + .returns(() => disposableRegistry); + + const envActivationService = TypeMoq.Mock.ofType<IEnvironmentActivationService>( + undefined, + TypeMoq.MockBehavior.Strict + ); + + const decoder = new BufferDecoder(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IBufferDecoder), TypeMoq.It.isAny())) + .returns(() => decoder); + + const interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(undefined, TypeMoq.MockBehavior.Strict); + interpreterService.setup((i) => i.hasInterpreters).returns(() => Promise.resolve(true)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) + .returns(() => interpreterService.object); + + const condaService = TypeMoq.Mock.ofType<ICondaService>(undefined, TypeMoq.MockBehavior.Strict); + condaService + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAnyString())) + .returns(() => Promise.resolve(undefined)); + condaService.setup((c) => c.getCondaVersion()).returns(() => Promise.resolve(undefined)); + condaService.setup((c) => c.getCondaFile()).returns(() => Promise.resolve('conda')); + + const processLogger = TypeMoq.Mock.ofType<IProcessLogger>(undefined, TypeMoq.MockBehavior.Strict); + processLogger + .setup((p) => p.logProcess(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return; + }); + const procServiceFactory = new ProcessServiceFactory( + envVarsService.object, + processLogger.object, + decoder, + disposableRegistry + ); + const windowsStoreInterpreter = mock(WindowsStoreInterpreter); + const platformService = mock<IPlatformService>(); + return new PythonExecutionFactory( + serviceContainer.object, + envActivationService.object, + procServiceFactory, + configService, + condaService.object, + decoder, + instance(windowsStoreInterpreter), + instance(platformService) + ); + } + + public makeDocument(filename: string): TextDocument { + const doc = newMockDocument(filename); + doc.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns((lno) => { + const lines = fs.readFileSync(filename).toString().split(os.EOL); + const textline = TypeMoq.Mock.ofType<TextLine>(undefined, TypeMoq.MockBehavior.Strict); + textline.setup((t) => t.text).returns(() => lines[lno]); + return textline.object; + }); + return doc.object; + } +} + +// tslint:disable-next-line:max-func-body-length +suite('Linting Functional Tests', () => { + const pythonPath = child_process.execSync(`${PYTHON_PATH} -c "import sys;print(sys.executable)"`); + // tslint:disable-next-line: no-console + console.log(`Testing linter with python ${pythonPath}`); + + // These are integration tests that mock out everything except + // the filesystem and process execution. + // tslint:disable-next-line:no-any + async function testLinterMessages( + fixture: TestFixture, + product: Product, + pythonFile: string, + messagesToBeReceived: ILintMessage[] + ) { + const doc = fixture.makeDocument(pythonFile); + await fixture.linterManager.setActiveLintersAsync([product], doc.uri); + const linter = await fixture.linterManager.createLinter( + product, + fixture.outputChannel.object, + fixture.serviceContainer.object + ); + + const messages = await linter.lint(doc, new CancellationTokenSource().token); + + if (messagesToBeReceived.length === 0) { + assert.equal(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); + } else { + if (fixture.output.indexOf('ENOENT') === -1) { + // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. + // Looks like pylint stops linting as soon as it comes across any ERRORS. + assert.notEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); + } + } + } + for (const product of LINTERID_BY_PRODUCT.keys()) { + test(getProductName(product), async function () { + if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + + const fixture = new TestFixture(); + const messagesToBeReturned = getMessages(product); + await testLinterMessages(fixture, product, fileToLint, messagesToBeReturned); + }); + } + for (const product of LINTERID_BY_PRODUCT.keys()) { + // tslint:disable-next-line:max-func-body-length + test(`${getProductName(product)} with config in root`, async function () { + if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + + const fixture = new TestFixture(); + if (product === Product.pydocstyle) { + fixture.lintingSettings.pylintUseMinimalCheckers = false; + } + + const { filename, messagesToBeReceived, origRCFile } = await getInfoForConfig(product); + let rcfile = ''; + async function cleanUp() { + if (rcfile !== '') { + await deleteFile(rcfile); + } + } + if (origRCFile !== '') { + rcfile = path.join(workspaceUri.fsPath, path.basename(origRCFile)); + await fs.copy(origRCFile, rcfile); + } + + try { + await testLinterMessages(fixture, product, filename, messagesToBeReceived); + } finally { + await cleanUp(); + } + }); + } + + async function testLinterMessageCount( + fixture: TestFixture, + product: Product, + pythonFile: string, + messageCountToBeReceived: number + ) { + const doc = fixture.makeDocument(pythonFile); + await fixture.linterManager.setActiveLintersAsync([product], doc.uri); + const linter = await fixture.linterManager.createLinter( + product, + fixture.outputChannel.object, + fixture.serviceContainer.object + ); + + const messages = await linter.lint(doc, new CancellationTokenSource().token); + + assert.equal( + messages.length, + messageCountToBeReceived, + 'Expected number of lint errors does not match lint error count' + ); + } + test('Three line output counted as one message', async () => { + const maxErrors = 5; + const fixture = new TestFixture(); + fixture.lintingSettings.maxNumberOfProblems = maxErrors; + await testLinterMessageCount( + fixture, + Product.pylint, + path.join(pythonFilesDir, 'threeLineLints.py'), + maxErrors + ); + }); +}); diff --git a/src/test/linters/lint.manager.unit.test.ts b/src/test/linters/lint.manager.unit.test.ts new file mode 100644 index 000000000000..be23cef27d9b --- /dev/null +++ b/src/test/linters/lint.manager.unit.test.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { IConfigurationService, IPythonSettings } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { LinterManager } from '../../client/linters/linterManager'; + +const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + +// setup class instance +class TestLinterManager extends LinterManager { + public enableUnconfiguredLintersCallCount: number = 0; + + protected async enableUnconfiguredLinters(_resource?: Uri): Promise<void> { + this.enableUnconfiguredLintersCallCount += 1; + } +} + +function getServiceContainerMockForLinterManagerTests(): TypeMoq.IMock<IServiceContainer> { + // setup test mocks + const serviceContainerMock = TypeMoq.Mock.ofType<IServiceContainer>(); + + const pythonSettingsMock = TypeMoq.Mock.ofType<IPythonSettings>(); + const configMock = TypeMoq.Mock.ofType<IConfigurationService>(); + configMock.setup((cm) => cm.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettingsMock.object); + serviceContainerMock.setup((c) => c.get(IConfigurationService)).returns(() => configMock.object); + + const pythonConfig = { + // tslint:disable-next-line:no-empty + inspect: () => {} + }; + workspaceService + .setup((x) => x.getConfiguration('python', TypeMoq.It.isAny())) + // tslint:disable-next-line:no-any + .returns(() => pythonConfig as any); + serviceContainerMock.setup((c) => c.get(IWorkspaceService)).returns(() => workspaceService.object); + + return serviceContainerMock; +} + +// tslint:disable-next-line:max-func-body-length +suite('Lint Manager Unit Tests', () => { + test('Linter manager isLintingEnabled checks availability when silent = false.', async () => { + // set expectations + const expectedCallCount = 1; + const silentFlag = false; + + // get setup + const serviceContainerMock = getServiceContainerMockForLinterManagerTests(); + + // make the call + const lm = new TestLinterManager(serviceContainerMock.object, workspaceService.object); + await lm.isLintingEnabled(silentFlag); + + // test expectations + expect(lm.enableUnconfiguredLintersCallCount).to.equal(expectedCallCount); + }); + + test('Linter manager isLintingEnabled does not check availability when silent = true.', async () => { + // set expectations + const expectedCallCount = 0; + const silentFlag = true; + + // get setup + const serviceContainerMock = getServiceContainerMockForLinterManagerTests(); + + // make the call + const lm: TestLinterManager = new TestLinterManager(serviceContainerMock.object, workspaceService.object); + await lm.isLintingEnabled(silentFlag); + + // test expectations + expect(lm.enableUnconfiguredLintersCallCount).to.equal(expectedCallCount); + }); + + test('Linter manager getActiveLinters checks availability when silent = false.', async () => { + // set expectations + const expectedCallCount = 1; + const silentFlag = false; + + // get setup + const serviceContainerMock = getServiceContainerMockForLinterManagerTests(); + + // make the call + const lm: TestLinterManager = new TestLinterManager(serviceContainerMock.object, workspaceService.object); + await lm.getActiveLinters(silentFlag); + + // test expectations + expect(lm.enableUnconfiguredLintersCallCount).to.equal(expectedCallCount); + }); + + test('Linter manager getActiveLinters checks availability when silent = true.', async () => { + // set expectations + const expectedCallCount = 0; + const silentFlag = true; + + // get setup + const serviceContainerMock = getServiceContainerMockForLinterManagerTests(); + + // make the call + const lm: TestLinterManager = new TestLinterManager(serviceContainerMock.object, workspaceService.object); + await lm.getActiveLinters(silentFlag); + + // test expectations + expect(lm.enableUnconfiguredLintersCallCount).to.equal(expectedCallCount); + }); +}); diff --git a/src/test/linters/lint.multilinter.test.ts b/src/test/linters/lint.multilinter.test.ts new file mode 100644 index 000000000000..ebc804b2e9c5 --- /dev/null +++ b/src/test/linters/lint.multilinter.test.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, DiagnosticCollection, Uri, window, workspace } from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; +import { ICommandManager } from '../../client/common/application/types'; +import { Product } from '../../client/common/installer/productInstaller'; +import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; +import { ExecutionResult, IPythonToolExecutionService, SpawnOptions } from '../../client/common/process/types'; +import { ExecutionInfo, IConfigurationService } from '../../client/common/types'; +import { ILinterManager } from '../../client/linters/types'; +import { deleteFile, IExtensionTestApi, PythonSettingKeys, rootWorkspaceUri } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; + +const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); +const pythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'linting'); + +// Mocked out python tool execution (all we need is mocked linter return values). +class MockPythonToolExecService extends PythonToolExecutionService { + // Mocked samples of linter messages from flake8 and pylint: + public flake8Msg = + '1,1,W,W391:blank line at end of file\ns:142:13), <anonymous>:1\n1,7,E,E999:SyntaxError: invalid syntax\n'; + + public pylintMsg = + "************* Module print\ns:142:13), <anonymous>:1\n1,0,error,syntax-error:Missing parentheses in call to 'print'. Did you mean print(x)? (<unknown>, line 1)\n"; + + // Depending on moduleName being exec'd, return the appropriate sample. + public async exec( + executionInfo: ExecutionInfo, + _options: SpawnOptions, + _resource: Uri + ): Promise<ExecutionResult<string>> { + let msg = this.flake8Msg; + if (executionInfo.moduleName === 'pylint') { + msg = this.pylintMsg; + } + return { stdout: msg }; + } +} + +// tslint:disable-next-line:max-func-body-length +suite('Linting - Multiple Linters Enabled Test', () => { + let api: IExtensionTestApi; + let configService: IConfigurationService; + let linterManager: ILinterManager; + + suiteSetup(async () => { + api = await initialize(); + configService = api.serviceContainer.get<IConfigurationService>(IConfigurationService); + linterManager = api.serviceContainer.get<ILinterManager>(ILinterManager); + }); + setup(async () => { + await initializeTest(); + await resetSettings(); + + // We only want to return some valid strings from linters, we don't care if they + // are being returned by actual linters (we aren't testing linters here, only how + // our code responds to those linters). + api.serviceManager.rebind<IPythonToolExecutionService>(IPythonToolExecutionService, MockPythonToolExecService); + }); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await closeActiveWindows(); + await resetSettings(); + await deleteFile(path.join(workspaceUri.fsPath, '.pylintrc')); + await deleteFile(path.join(workspaceUri.fsPath, '.pydocstyle')); + + // Restore the execution service as it was... + api.serviceManager.rebind<IPythonToolExecutionService>(IPythonToolExecutionService, PythonToolExecutionService); + }); + + async function resetSettings() { + // Don't run these updates in parallel, as they are updating the same file. + const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + + await configService.updateSetting('linting.enabled', true, rootWorkspaceUri, target); + await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); + await configService.updateSetting('linting.pylintUseMinimalCheckers', false, workspaceUri); + + linterManager.getAllLinterInfos().forEach(async (x) => { + await configService.updateSetting(makeSettingKey(x.product), false, rootWorkspaceUri, target); + }); + } + + function makeSettingKey(product: Product): PythonSettingKeys { + return `linting.${linterManager.getLinterInfo(product).enabledSettingName}` as PythonSettingKeys; + } + + test('Multiple linters', async () => { + await closeActiveWindows(); + const document = await workspace.openTextDocument(path.join(pythoFilesPath, 'print.py')); + await window.showTextDocument(document); + await configService.updateSetting( + 'languageServer', + LanguageServerType.Jedi, + undefined, + ConfigurationTarget.Workspace + ); + await configService.updateSetting('linting.enabled', true, workspaceUri); + await configService.updateSetting('linting.pylintUseMinimalCheckers', false, workspaceUri); + await configService.updateSetting('linting.pylintEnabled', true, workspaceUri); + await configService.updateSetting('linting.flake8Enabled', true, workspaceUri); + + const commands = api.serviceContainer.get<ICommandManager>(ICommandManager); + + const collection = (await commands.executeCommand('python.runLinting')) as DiagnosticCollection; + assert.notEqual(collection, undefined, 'python.runLinting did not return valid diagnostics collection.'); + + const messages = collection!.get(document.uri); + assert.notEqual(messages!.length, 0, 'No diagnostic messages.'); + assert.notEqual(messages!.filter((x) => x.source === 'pylint').length, 0, 'No pylint messages.'); + assert.notEqual(messages!.filter((x) => x.source === 'flake8').length, 0, 'No flake8 messages.'); + }); +}); diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts new file mode 100644 index 000000000000..f7dcebd731f5 --- /dev/null +++ b/src/test/linters/lint.multiroot.test.ts @@ -0,0 +1,151 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import { CancellationTokenSource, ConfigurationTarget, OutputChannel, Uri, workspace } from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; +import { PythonSettings } from '../../client/common/configSettings'; +import { + CTagsProductPathService, + DataScienceProductPathService, + FormatterProductPathService, + LinterProductPathService, + RefactoringLibraryProductPathService, + TestFrameworkProductPathService +} from '../../client/common/installer/productPath'; +import { ProductService } from '../../client/common/installer/productService'; +import { IProductPathService, IProductService } from '../../client/common/installer/types'; +import { IConfigurationService, IOutputChannel, Product, ProductType } from '../../client/common/types'; +import { ILinter, ILinterManager } from '../../client/linters/types'; +import { TEST_OUTPUT_CHANNEL } from '../../client/testing/common/constants'; +import { TEST_TIMEOUT } from '../constants'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; + +// tslint:disable:max-func-body-length no-invalid-this + +const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); + +suite('Multiroot Linting', () => { + const pylintSetting = 'linting.pylintEnabled'; + const flake8Setting = 'linting.flake8Enabled'; + + let ioc: UnitTestIocContainer; + suiteSetup(function () { + if (!IS_MULTI_ROOT_TEST) { + this.skip(); + } + return initialize(); + }); + setup(async () => { + initializeDI(); + await initializeTest(); + }); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await ioc.dispose(); + await closeActiveWindows(); + PythonSettings.dispose(); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(false); + ioc.registerProcessTypes(); + ioc.registerLinterTypes(); + ioc.registerVariableTypes(); + ioc.registerFileSystemTypes(); + ioc.registerMockInterpreterTypes(); + ioc.registerInterpreterStorageTypes(); + ioc.serviceManager.addSingletonInstance<IProductService>(IProductService, new ProductService()); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + CTagsProductPathService, + ProductType.WorkspaceSymbols + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + FormatterProductPathService, + ProductType.Formatter + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + LinterProductPathService, + ProductType.Linter + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + TestFrameworkProductPathService, + ProductType.TestFramework + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + RefactoringLibraryProductPathService, + ProductType.RefactoringLibrary + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + DataScienceProductPathService, + ProductType.DataScience + ); + } + + async function createLinter(product: Product): Promise<ILinter> { + const mockOutputChannel = ioc.serviceContainer.get<OutputChannel>(IOutputChannel, TEST_OUTPUT_CHANNEL); + const lm = ioc.serviceContainer.get<ILinterManager>(ILinterManager); + return lm.createLinter(product, mockOutputChannel, ioc.serviceContainer); + } + async function testLinterInWorkspaceFolder( + product: Product, + workspaceFolderRelativePath: string, + mustHaveErrors: boolean + ): Promise<void> { + const fileToLint = path.join(multirootPath, workspaceFolderRelativePath, 'file.py'); + const cancelToken = new CancellationTokenSource(); + const document = await workspace.openTextDocument(fileToLint); + + const linter = await createLinter(product); + const messages = await linter.lint(document, cancelToken.token); + + const errorMessage = mustHaveErrors ? 'No errors returned by linter' : 'Errors returned by linter'; + assert.equal(messages.length > 0, mustHaveErrors, errorMessage); + } + + test('Enabling Pylint in root and also in Workspace, should return errors', async () => { + await runTest(Product.pylint, true, true, pylintSetting); + }).timeout(TEST_TIMEOUT * 2); + test('Enabling Pylint in root and disabling in Workspace, should not return errors', async () => { + await runTest(Product.pylint, true, false, pylintSetting); + }).timeout(TEST_TIMEOUT * 2); + test('Disabling Pylint in root and enabling in Workspace, should return errors', async () => { + await runTest(Product.pylint, false, true, pylintSetting); + }).timeout(TEST_TIMEOUT * 2); + + test('Enabling Flake8 in root and also in Workspace, should return errors', async () => { + await runTest(Product.flake8, true, true, flake8Setting); + }).timeout(TEST_TIMEOUT * 2); + test('Enabling Flake8 in root and disabling in Workspace, should not return errors', async () => { + await runTest(Product.flake8, true, false, flake8Setting); + }).timeout(TEST_TIMEOUT * 2); + test('Disabling Flake8 in root and enabling in Workspace, should return errors', async () => { + await runTest(Product.flake8, false, true, flake8Setting); + }).timeout(TEST_TIMEOUT * 2); + + async function runTest(product: Product, global: boolean, wks: boolean, setting: string): Promise<void> { + const config = ioc.serviceContainer.get<IConfigurationService>(IConfigurationService); + await config.updateSetting( + 'languageServer', + LanguageServerType.Jedi, + Uri.file(multirootPath), + ConfigurationTarget.Global + ); + await Promise.all([ + config.updateSetting(setting, global, Uri.file(multirootPath), ConfigurationTarget.Global), + config.updateSetting(setting, wks, Uri.file(multirootPath), ConfigurationTarget.Workspace) + ]); + await testLinterInWorkspaceFolder(product, 'workspace1', wks); + await Promise.all( + [ConfigurationTarget.Global, ConfigurationTarget.Workspace].map((configTarget) => + config.updateSetting(setting, undefined, Uri.file(multirootPath), configTarget) + ) + ); + } +}); diff --git a/src/test/linters/lint.provider.test.ts b/src/test/linters/lint.provider.test.ts new file mode 100644 index 000000000000..d5f9d3f3220a --- /dev/null +++ b/src/test/linters/lint.provider.test.ts @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { Container } from 'inversify'; +import * as TypeMoq from 'typemoq'; +import * as vscode from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; +import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { IFileSystem } from '../../client/common/platform/types'; +import { + GLOBAL_MEMENTO, + IConfigurationService, + IInstaller, + ILintingSettings, + IMemento, + IPersistentStateFactory, + IPythonSettings, + Product, + WORKSPACE_MEMENTO +} from '../../client/common/types'; +import { createDeferred } from '../../client/common/utils/async'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService +} from '../../client/interpreter/autoSelection/types'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { AvailableLinterActivator } from '../../client/linters/linterAvailability'; +import { LinterManager } from '../../client/linters/linterManager'; +import { IAvailableLinterActivator, ILinterManager, ILintingEngine } from '../../client/linters/types'; +import { LinterProvider } from '../../client/providers/linterProvider'; +import { initialize } from '../initialize'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; +import { MockMemento } from '../mocks/mementos'; + +// tslint:disable-next-line:max-func-body-length +suite('Linting - Provider', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let engine: TypeMoq.IMock<ILintingEngine>; + let configService: TypeMoq.IMock<IConfigurationService>; + let docManager: TypeMoq.IMock<IDocumentManager>; + let settings: TypeMoq.IMock<IPythonSettings>; + let lm: ILinterManager; + let serviceContainer: ServiceContainer; + let emitter: vscode.EventEmitter<vscode.TextDocument>; + let document: TypeMoq.IMock<vscode.TextDocument>; + let fs: TypeMoq.IMock<IFileSystem>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let linterInstaller: TypeMoq.IMock<IInstaller>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let workspaceConfig: TypeMoq.IMock<vscode.WorkspaceConfiguration>; + + suiteSetup(initialize); + setup(async () => { + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + + serviceContainer = new ServiceContainer(cont); + + fs = TypeMoq.Mock.ofType<IFileSystem>(); + fs.setup((x) => x.fileExists(TypeMoq.It.isAny())).returns( + () => new Promise<boolean>((resolve, _reject) => resolve(true)) + ); + fs.setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); + serviceManager.addSingletonInstance<IFileSystem>(IFileSystem, fs.object); + + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, interpreterService.object); + + engine = TypeMoq.Mock.ofType<ILintingEngine>(); + serviceManager.addSingletonInstance<ILintingEngine>(ILintingEngine, engine.object); + + docManager = TypeMoq.Mock.ofType<IDocumentManager>(); + serviceManager.addSingletonInstance<IDocumentManager>(IDocumentManager, docManager.object); + + const lintSettings = TypeMoq.Mock.ofType<ILintingSettings>(); + lintSettings.setup((x) => x.enabled).returns(() => true); + lintSettings.setup((x) => x.lintOnSave).returns(() => true); + + settings = TypeMoq.Mock.ofType<IPythonSettings>(); + settings.setup((x) => x.linting).returns(() => lintSettings.object); + settings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); + + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, configService.object); + + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + linterInstaller = TypeMoq.Mock.ofType<IInstaller>(); + + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspaceConfig = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>(); + workspaceService + .setup((w) => w.getConfiguration('python', TypeMoq.It.isAny())) + .returns(() => workspaceConfig.object); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + + serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); + serviceManager.addSingletonInstance<IInstaller>(IInstaller, linterInstaller.object); + serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspaceService.object); + serviceManager.add(IAvailableLinterActivator, AvailableLinterActivator); + serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService + ); + serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( + IInterpreterAutoSeletionProxyService, + MockAutoSelectionService + ); + serviceManager.addSingleton<IPersistentStateFactory>(IPersistentStateFactory, PersistentStateFactory); + serviceManager.addSingleton<vscode.Memento>(IMemento, MockMemento, GLOBAL_MEMENTO); + serviceManager.addSingleton<vscode.Memento>(IMemento, MockMemento, WORKSPACE_MEMENTO); + lm = new LinterManager(serviceContainer, workspaceService.object); + serviceManager.addSingletonInstance<ILinterManager>(ILinterManager, lm); + emitter = new vscode.EventEmitter<vscode.TextDocument>(); + document = TypeMoq.Mock.ofType<vscode.TextDocument>(); + }); + + test('Lint on open file', async () => { + docManager.setup((x) => x.onDidOpenTextDocument).returns(() => emitter.event); + document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.py')); + document.setup((x) => x.languageId).returns(() => 'python'); + + const linterProvider = new LinterProvider(serviceContainer); + await linterProvider.activate(); + emitter.fire(document.object); + engine.verify((x) => x.lintDocument(document.object, 'auto'), TypeMoq.Times.once()); + }); + + test('Lint on save file', async () => { + docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); + document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.py')); + document.setup((x) => x.languageId).returns(() => 'python'); + + const linterProvider = new LinterProvider(serviceContainer); + await linterProvider.activate(); + emitter.fire(document.object); + engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.once()); + }); + + test('No lint on open other files', async () => { + docManager.setup((x) => x.onDidOpenTextDocument).returns(() => emitter.event); + document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.cs')); + document.setup((x) => x.languageId).returns(() => 'csharp'); + + const linterProvider = new LinterProvider(serviceContainer); + await linterProvider.activate(); + emitter.fire(document.object); + engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); + }); + + test('No lint on save other files', async () => { + docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); + document.setup((x) => x.uri).returns(() => vscode.Uri.file('test.cs')); + document.setup((x) => x.languageId).returns(() => 'csharp'); + + const linterProvider = new LinterProvider(serviceContainer); + await linterProvider.activate(); + emitter.fire(document.object); + engine.verify((x) => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); + }); + + test('Lint on change interpreters', async () => { + const e = new vscode.EventEmitter<void>(); + interpreterService.setup((x) => x.onDidChangeInterpreter).returns(() => e.event); + + const linterProvider = new LinterProvider(serviceContainer); + await linterProvider.activate(); + e.fire(); + engine.verify((x) => x.lintOpenPythonFiles(), TypeMoq.Times.once()); + }); + + test('Lint on save pylintrc', async () => { + docManager.setup((x) => x.onDidSaveTextDocument).returns(() => emitter.event); + document.setup((x) => x.uri).returns(() => vscode.Uri.file('.pylintrc')); + + await lm.setActiveLintersAsync([Product.pylint]); + const linterProvider = new LinterProvider(serviceContainer); + await linterProvider.activate(); + emitter.fire(document.object); + + const deferred = createDeferred<void>(); + setTimeout(() => deferred.resolve(), 2000); + await deferred.promise; + engine.verify((x) => x.lintOpenPythonFiles(), TypeMoq.Times.once()); + }); + + test('Diagnostic cleared on file close', async () => testClearDiagnosticsOnClose(true)); + test('Diagnostic not cleared on file opened in another tab', async () => testClearDiagnosticsOnClose(false)); + + async function testClearDiagnosticsOnClose(closed: boolean) { + docManager.setup((x) => x.onDidCloseTextDocument).returns(() => emitter.event); + + const uri = vscode.Uri.file('test.py'); + document.setup((x) => x.uri).returns(() => uri); + document.setup((x) => x.isClosed).returns(() => closed); + + docManager.setup((x) => x.textDocuments).returns(() => (closed ? [] : [document.object])); + const linterProvider = new LinterProvider(serviceContainer); + await linterProvider.activate(); + + emitter.fire(document.object); + const timesExpected = closed ? TypeMoq.Times.once() : TypeMoq.Times.never(); + engine.verify((x) => x.clearDiagnostics(TypeMoq.It.isAny()), timesExpected); + } +}); diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts new file mode 100644 index 000000000000..71b3c694bab4 --- /dev/null +++ b/src/test/linters/lint.test.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { Product } from '../../client/common/installer/productInstaller'; +import { + CTagsProductPathService, + DataScienceProductPathService, + FormatterProductPathService, + LinterProductPathService, + RefactoringLibraryProductPathService, + TestFrameworkProductPathService +} from '../../client/common/installer/productPath'; +import { ProductService } from '../../client/common/installer/productService'; +import { IProductPathService, IProductService } from '../../client/common/installer/types'; +import { IConfigurationService, ProductType } from '../../client/common/types'; +import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; +import { LinterManager } from '../../client/linters/linterManager'; +import { ILinterManager } from '../../client/linters/types'; +import { rootWorkspaceUri } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; + +const workspaceDir = path.join(__dirname, '..', '..', '..', 'src', 'test'); +const workspaceUri = Uri.file(workspaceDir); + +// tslint:disable-next-line:max-func-body-length +suite('Linting Settings', () => { + let ioc: UnitTestIocContainer; + let linterManager: ILinterManager; + let configService: IConfigurationService; + + suiteSetup(async function () { + // These tests are still consistently failing during teardown. + // See https://github.com/Microsoft/vscode-python/issues/4326. + // tslint:disable-next-line:no-invalid-this + this.skip(); + + await initialize(); + }); + setup(async () => { + initializeDI(); + await initializeTest(); + }); + suiteTeardown(closeActiveWindows); + teardown(async () => { + await ioc.dispose(); + await closeActiveWindows(); + await resetSettings(); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(false); + ioc.registerProcessTypes(); + ioc.registerLinterTypes(); + ioc.registerVariableTypes(); + ioc.registerPlatformTypes(); + linterManager = new LinterManager(ioc.serviceContainer, new WorkspaceService()); + configService = ioc.serviceContainer.get<IConfigurationService>(IConfigurationService); + ioc.serviceManager.addSingletonInstance<IProductService>(IProductService, new ProductService()); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + CTagsProductPathService, + ProductType.WorkspaceSymbols + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + FormatterProductPathService, + ProductType.Formatter + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + LinterProductPathService, + ProductType.Linter + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + TestFrameworkProductPathService, + ProductType.TestFramework + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + RefactoringLibraryProductPathService, + ProductType.RefactoringLibrary + ); + ioc.serviceManager.addSingleton<IProductPathService>( + IProductPathService, + DataScienceProductPathService, + ProductType.DataScience + ); + } + + async function resetSettings(lintingEnabled = true) { + // Don't run these updates in parallel, as they are updating the same file. + const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + + await configService.updateSetting('linting.enabled', lintingEnabled, rootWorkspaceUri, target); + await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); + await configService.updateSetting('linting.pylintUseMinimalCheckers', false, workspaceUri); + + linterManager.getAllLinterInfos().forEach(async (x) => { + const settingKey = `linting.${x.enabledSettingName}`; + await configService.updateSetting(settingKey, false, rootWorkspaceUri, target); + }); + } + + test('enable through manager (global)', async () => { + const settings = configService.getSettings(); + await resetSettings(false); + + await linterManager.enableLintingAsync(false); + assert.equal(settings.linting.enabled, false, 'mismatch'); + + await linterManager.enableLintingAsync(true); + assert.equal(settings.linting.enabled, true, 'mismatch'); + }); + + for (const product of LINTERID_BY_PRODUCT.keys()) { + test(`enable through manager (${Product[product]})`, async () => { + const settings = configService.getSettings(); + await resetSettings(); + + // tslint:disable-next-line:no-any + assert.equal((settings.linting as any)[`${Product[product]}Enabled`], false, 'mismatch'); + + await linterManager.setActiveLintersAsync([product]); + + // tslint:disable-next-line:no-any + assert.equal((settings.linting as any)[`${Product[product]}Enabled`], true, 'mismatch'); + linterManager.getAllLinterInfos().forEach(async (x) => { + if (x.product !== product) { + // tslint:disable-next-line:no-any + assert.equal((settings.linting as any)[x.enabledSettingName], false, 'mismatch'); + } + }); + }); + } +}); diff --git a/src/test/linters/lint.unit.test.ts b/src/test/linters/lint.unit.test.ts new file mode 100644 index 000000000000..7c92fac4c0cc --- /dev/null +++ b/src/test/linters/lint.unit.test.ts @@ -0,0 +1,818 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import * as os from 'os'; +import * as TypeMoq from 'typemoq'; +import { CancellationTokenSource, TextDocument, TextLine } from 'vscode'; +import { Product } from '../../client/common/installer/productInstaller'; +import { ProductNames } from '../../client/common/installer/productNames'; +import { ProductService } from '../../client/common/installer/productService'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + IPythonToolExecutionService +} from '../../client/common/process/types'; +import { ProductType } from '../../client/common/types'; +import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; +import { ILintMessage, LintMessageSeverity } from '../../client/linters/types'; +import { BaseTestFixture, getLinterID, getProductName, linterMessageAsLine, throwUnknownProduct } from './common'; + +const pylintMessagesToBeReturned: ILintMessage[] = [ + { + line: 24, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 30, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 34, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0012', + message: 'Locally enabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 40, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 44, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0012', + message: 'Locally enabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 55, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 59, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0012', + message: 'Locally enabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 62, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling undefined-variable (E0602)', + provider: '', + type: 'warning' + }, + { + line: 70, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 84, + column: 0, + severity: LintMessageSeverity.Information, + code: 'I0011', + message: 'Locally disabling no-member (E1101)', + provider: '', + type: 'warning' + }, + { + line: 87, + column: 0, + severity: LintMessageSeverity.Hint, + code: 'C0304', + message: 'Final newline missing', + provider: '', + type: 'warning' + }, + { + line: 11, + column: 20, + severity: LintMessageSeverity.Warning, + code: 'W0613', + message: "Unused argument 'arg'", + provider: '', + type: 'warning' + }, + { + line: 26, + column: 14, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blop' member", + provider: '', + type: 'warning' + }, + { + line: 36, + column: 14, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 46, + column: 18, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 61, + column: 18, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 72, + column: 18, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 75, + column: 18, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 77, + column: 14, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + }, + { + line: 83, + column: 14, + severity: LintMessageSeverity.Error, + code: 'E1101', + message: "Instance of 'Foo' has no 'blip' member", + provider: '', + type: 'warning' + } +]; +const flake8MessagesToBeReturned: ILintMessage[] = [ + { + line: 5, + column: 1, + severity: LintMessageSeverity.Error, + code: 'E302', + message: 'expected 2 blank lines, found 1', + provider: '', + type: 'E' + }, + { + line: 19, + column: 15, + severity: LintMessageSeverity.Error, + code: 'E127', + message: 'continuation line over-indented for visual indent', + provider: '', + type: 'E' + }, + { + line: 24, + column: 23, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 62, + column: 30, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 70, + column: 22, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 80, + column: 5, + severity: LintMessageSeverity.Error, + code: 'E303', + message: 'too many blank lines (2)', + provider: '', + type: 'E' + }, + { + line: 87, + column: 24, + severity: LintMessageSeverity.Warning, + code: 'W292', + message: 'no newline at end of file', + provider: '', + type: 'E' + } +]; +const pycodestyleMessagesToBeReturned: ILintMessage[] = [ + { + line: 5, + column: 1, + severity: LintMessageSeverity.Error, + code: 'E302', + message: 'expected 2 blank lines, found 1', + provider: '', + type: 'E' + }, + { + line: 19, + column: 15, + severity: LintMessageSeverity.Error, + code: 'E127', + message: 'continuation line over-indented for visual indent', + provider: '', + type: 'E' + }, + { + line: 24, + column: 23, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 62, + column: 30, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 70, + column: 22, + severity: LintMessageSeverity.Error, + code: 'E261', + message: 'at least two spaces before inline comment', + provider: '', + type: 'E' + }, + { + line: 80, + column: 5, + severity: LintMessageSeverity.Error, + code: 'E303', + message: 'too many blank lines (2)', + provider: '', + type: 'E' + }, + { + line: 87, + column: 24, + severity: LintMessageSeverity.Warning, + code: 'W292', + message: 'no newline at end of file', + provider: '', + type: 'E' + } +]; +const pydocstyleMessagesToBeReturned: ILintMessage[] = [ + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'e')", + column: 0, + line: 1, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 't')", + column: 0, + line: 5, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D102', + severity: LintMessageSeverity.Information, + message: 'Missing docstring in public method', + column: 4, + line: 8, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D401', + severity: LintMessageSeverity.Information, + message: "First line should be in imperative mood ('thi', not 'this')", + column: 4, + line: 11, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('This', not 'this')", + column: 4, + line: 11, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'e')", + column: 4, + line: 11, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('And', not 'and')", + column: 4, + line: 15, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 't')", + column: 4, + line: 15, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 21, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 21, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 28, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 28, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 38, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 38, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 53, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 53, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 68, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 68, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D403', + severity: LintMessageSeverity.Information, + message: "First word of the first line should be properly capitalized ('Test', not 'test')", + column: 4, + line: 80, + type: '', + provider: 'pydocstyle' + }, + { + code: 'D400', + severity: LintMessageSeverity.Information, + message: "First line should end with a period (not 'g')", + column: 4, + line: 80, + type: '', + provider: 'pydocstyle' + } +]; + +class TestFixture extends BaseTestFixture { + public platformService: TypeMoq.IMock<IPlatformService>; + public filesystem: TypeMoq.IMock<IFileSystem>; + public pythonToolExecService: TypeMoq.IMock<IPythonToolExecutionService>; + public pythonExecService: TypeMoq.IMock<IPythonExecutionService>; + public pythonExecFactory: TypeMoq.IMock<IPythonExecutionFactory>; + + constructor(workspaceDir = '.', printLogs = false) { + const platformService = TypeMoq.Mock.ofType<IPlatformService>(undefined, TypeMoq.MockBehavior.Strict); + const filesystem = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); + const pythonToolExecService = TypeMoq.Mock.ofType<IPythonToolExecutionService>( + undefined, + TypeMoq.MockBehavior.Strict + ); + const pythonExecFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(undefined, TypeMoq.MockBehavior.Strict); + super( + platformService.object, + filesystem.object, + pythonToolExecService.object, + pythonExecFactory.object, + undefined, + undefined, + true, + workspaceDir, + printLogs + ); + + this.platformService = platformService; + this.filesystem = filesystem; + this.pythonToolExecService = pythonToolExecService; + this.pythonExecService = TypeMoq.Mock.ofType<IPythonExecutionService>(undefined, TypeMoq.MockBehavior.Strict); + this.pythonExecFactory = pythonExecFactory; + + this.filesystem.setup((f) => f.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + + // tslint:disable-next-line:no-any + this.pythonExecService.setup((s: any) => s.then).returns(() => undefined); + this.pythonExecService + .setup((s) => s.isModuleInstalled(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)); + + this.pythonExecFactory + .setup((f) => f.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(this.pythonExecService.object)); + } + + public makeDocument(product: Product, filename: string): TextDocument { + const doc = this.newMockDocument(filename); + if (product === Product.pydocstyle) { + const dummyLine = TypeMoq.Mock.ofType<TextLine>(undefined, TypeMoq.MockBehavior.Strict); + dummyLine.setup((d) => d.text).returns(() => ' ...'); + doc.setup((s) => s.lineAt(TypeMoq.It.isAny())).returns(() => dummyLine.object); + } + return doc.object; + } + + public setDefaultMessages(product: Product): ILintMessage[] { + let messages: ILintMessage[]; + switch (product) { + case Product.pylint: { + messages = pylintMessagesToBeReturned; + break; + } + case Product.flake8: { + messages = flake8MessagesToBeReturned; + break; + } + case Product.pycodestyle: { + messages = pycodestyleMessagesToBeReturned; + break; + } + case Product.pydocstyle: { + messages = pydocstyleMessagesToBeReturned; + break; + } + default: { + throwUnknownProduct(product); + return []; // to quiet tslint + } + } + this.setMessages(messages, product); + return messages; + } + + public setMessages(messages: ILintMessage[], product?: Product) { + if (messages.length === 0) { + this.setStdout(''); + return; + } + + const lines: string[] = []; + for (const msg of messages) { + if (msg.provider === '' && product) { + msg.provider = getLinterID(product); + } + const line = linterMessageAsLine(msg); + lines.push(line); + } + this.setStdout(lines.join(os.EOL) + os.EOL); + } + + public setStdout(stdout: string) { + this.pythonToolExecService + .setup((s) => s.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: stdout })); + } +} + +// tslint:disable-next-line:max-func-body-length +suite('Linting Scenarios', () => { + // Note that these aren't actually unit tests. Instead they are + // integration tests with heavy usage of mocks. + + test('No linting with PyLint (enabled) when disabled at top-level', async () => { + const product = Product.pylint; + const fixture = new TestFixture(); + fixture.lintingSettings.enabled = false; + fixture.setDefaultMessages(product); + const linter = await fixture.getEnabledLinter(product); + + const messages = await linter.lint( + fixture.makeDocument(product, 'spam.py'), + new CancellationTokenSource().token + ); + + assert.equal( + messages.length, + 0, + `Unexpected linter errors when linting is disabled, Output - ${fixture.output}` + ); + }); + + test('No linting with Pylint disabled (and Flake8 enabled)', async () => { + const product = Product.pylint; + const fixture = new TestFixture(); + fixture.lintingSettings.enabled = true; + fixture.lintingSettings.flake8Enabled = true; + fixture.setDefaultMessages(Product.pylint); + const linter = await fixture.getDisabledLinter(product); + + const messages = await linter.lint( + fixture.makeDocument(product, 'spam.py'), + new CancellationTokenSource().token + ); + + assert.equal( + messages.length, + 0, + `Unexpected linter errors when linting is disabled, Output - ${fixture.output}` + ); + }); + + async function testEnablingDisablingOfLinter(fixture: TestFixture, product: Product, enabled: boolean) { + fixture.lintingSettings.enabled = true; + fixture.setDefaultMessages(product); + if (enabled) { + fixture.setDefaultMessages(product); + } + const linter = await fixture.getLinter(product, enabled); + + const messages = await linter.lint( + fixture.makeDocument(product, 'spam.py'), + new CancellationTokenSource().token + ); + + if (enabled) { + assert.notEqual( + messages.length, + 0, + `Expected linter errors when linter is enabled, Output - ${fixture.output}` + ); + } else { + assert.equal( + messages.length, + 0, + `Unexpected linter errors when linter is disabled, Output - ${fixture.output}` + ); + } + } + for (const product of LINTERID_BY_PRODUCT.keys()) { + for (const enabled of [false, true]) { + test(`${enabled ? 'Enable' : 'Disable'} ${getProductName(product)} and run linter`, async function () { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Add coverage for these linters. + if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + + const fixture = new TestFixture(); + await testEnablingDisablingOfLinter(fixture, product, enabled); + }); + } + } + for (const useMinimal of [true, false]) { + for (const enabled of [true, false]) { + test(`PyLint ${enabled ? 'enabled' : 'disabled'} with${ + useMinimal ? '' : 'out' + } minimal checkers`, async () => { + const fixture = new TestFixture(); + fixture.lintingSettings.pylintUseMinimalCheckers = useMinimal; + await testEnablingDisablingOfLinter(fixture, Product.pylint, enabled); + }); + } + } + + async function testLinterMessages(fixture: TestFixture, product: Product) { + const messagesToBeReceived = fixture.setDefaultMessages(product); + const linter = await fixture.getEnabledLinter(product); + + const messages = await linter.lint( + fixture.makeDocument(product, 'spam.py'), + new CancellationTokenSource().token + ); + + if (messagesToBeReceived.length === 0) { + assert.equal(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); + } else { + if (fixture.output.indexOf('ENOENT') === -1) { + // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. + // Looks like pylint stops linting as soon as it comes across any ERRORS. + assert.notEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); + } + } + } + for (const product of LINTERID_BY_PRODUCT.keys()) { + test(`Check ${getProductName(product)} messages`, async function () { + // tslint:disable-next-line:no-suspicious-comment + // TODO: Add coverage for these linters. + if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some((p) => p === product)) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + + const fixture = new TestFixture(); + await testLinterMessages(fixture, product); + }); + } + + async function testLinterMessageCount(fixture: TestFixture, product: Product, messageCountToBeReceived: number) { + fixture.setDefaultMessages(product); + const linter = await fixture.getEnabledLinter(product); + + const messages = await linter.lint( + fixture.makeDocument(product, 'spam.py'), + new CancellationTokenSource().token + ); + + assert.equal( + messages.length, + messageCountToBeReceived, + `Expected number of lint errors does not match lint error count, Output - ${fixture.output}` + ); + } + test('Three line output counted as one message (Pylint)', async () => { + const maxErrors = 5; + const fixture = new TestFixture(); + fixture.lintingSettings.maxNumberOfProblems = maxErrors; + + await testLinterMessageCount(fixture, Product.pylint, maxErrors); + }); +}); + +const PRODUCTS = Object.keys(Product) + // tslint:disable-next-line:no-any + .filter((key) => !isNaN(Number(Product[key as any]))) + // tslint:disable-next-line:no-any + .map((key) => Product[key as any]); + +// tslint:disable-next-line:max-func-body-length +suite('Linting Products', () => { + const prodService = new ProductService(); + + test('All linting products are represented by linters', async () => { + for (const product of PRODUCTS) { + // tslint:disable-next-line:no-any + if (prodService.getProductType(product as any) !== ProductType.Linter) { + continue; + } + // tslint:disable-next-line:no-any + const found = LINTERID_BY_PRODUCT.get(product as any); + // tslint:disable-next-line:no-any + assert.notEqual(found, undefined, `did find linter ${Product[product as any]}`); + } + }); + + test('All linters match linting products', async () => { + for (const product of LINTERID_BY_PRODUCT.keys()) { + const prodType = prodService.getProductType(product); + assert.notEqual(prodType, undefined, `${Product[product]} is not not properly registered`); + assert.equal(prodType, ProductType.Linter, `${Product[product]} is not a linter product`); + } + }); + + test('All linting product names match linter IDs', async () => { + for (const [product, linterID] of LINTERID_BY_PRODUCT) { + const prodName = ProductNames.get(product); + assert.equal(prodName, linterID, 'product name does not match linter ID'); + } + }); +}); diff --git a/src/test/linters/lintengine.test.ts b/src/test/linters/lintengine.test.ts new file mode 100644 index 000000000000..e2db5059dfab --- /dev/null +++ b/src/test/linters/lintengine.test.ts @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as TypeMoq from 'typemoq'; +import { OutputChannel, TextDocument, Uri } from 'vscode'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; +import '../../client/common/extensions'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IConfigurationService, ILintingSettings, IOutputChannel, IPythonSettings } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { LintingEngine } from '../../client/linters/lintingEngine'; +import { ILinterManager, ILintingEngine } from '../../client/linters/types'; +import { initialize } from '../initialize'; + +// tslint:disable-next-line:max-func-body-length +suite('Linting - LintingEngine', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let lintManager: TypeMoq.IMock<ILinterManager>; + let settings: TypeMoq.IMock<IPythonSettings>; + let lintSettings: TypeMoq.IMock<ILintingSettings>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let lintingEngine: ILintingEngine; + + suiteSetup(initialize); + setup(async () => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + + const docManager = TypeMoq.Mock.ofType<IDocumentManager>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())) + .returns(() => docManager.object); + + const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) + .returns(() => workspaceService.object); + + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) + .returns(() => fileSystem.object); + + lintSettings = TypeMoq.Mock.ofType<ILintingSettings>(); + settings = TypeMoq.Mock.ofType<IPythonSettings>(); + + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + configService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + configService.setup((x) => x.isTestExecution()).returns(() => true); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) + .returns(() => configService.object); + + const outputChannel = TypeMoq.Mock.ofType<OutputChannel>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) + .returns(() => outputChannel.object); + + lintManager = TypeMoq.Mock.ofType<ILinterManager>(); + lintManager.setup((x) => x.isLintingEnabled(TypeMoq.It.isAny())).returns(async () => true); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) + .returns(() => lintManager.object); + + lintingEngine = new LintingEngine(serviceContainer.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ILintingEngine), TypeMoq.It.isAny())) + .returns(() => lintingEngine); + }); + + test('Ensure document.uri is passed into isLintingEnabled', () => { + const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); + try { + lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); + } catch { + lintManager.verify( + (l) => l.isLintingEnabled(TypeMoq.It.isAny(), TypeMoq.It.isValue(doc.uri)), + TypeMoq.Times.once() + ); + } + }); + test('Ensure document.uri is passed into createLinter', () => { + const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); + try { + lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); + } catch { + lintManager.verify( + (l) => + l.createLinter( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isValue(doc.uri) + ), + TypeMoq.Times.atLeastOnce() + ); + } + }); + + test('Verify files that match ignore pattern are not linted', async () => { + const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, true, ['a*.py']); + await lintingEngine.lintDocument(doc, 'auto'); + lintManager.verify( + (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + + test('Ensure non-Python files are not linted', async () => { + const doc = mockTextDocument('a.ts', 'typescript', true); + await lintingEngine.lintDocument(doc, 'auto'); + lintManager.verify( + (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + + test('Ensure files with git scheme are not linted', async () => { + const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'git'); + await lintingEngine.lintDocument(doc, 'auto'); + lintManager.verify( + (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + test('Ensure files with showModifications scheme are not linted', async () => { + const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'showModifications'); + await lintingEngine.lintDocument(doc, 'auto'); + lintManager.verify( + (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + test('Ensure files with svn scheme are not linted', async () => { + const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'svn'); + await lintingEngine.lintDocument(doc, 'auto'); + lintManager.verify( + (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + + test('Ensure non-existing files are not linted', async () => { + const doc = mockTextDocument('file.py', PYTHON_LANGUAGE, false, []); + await lintingEngine.lintDocument(doc, 'auto'); + lintManager.verify( + (l) => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + }); + + function mockTextDocument( + fileName: string, + language: string, + exists: boolean, + ignorePattern: string[] = [], + scheme?: string + ): TextDocument { + fileSystem.setup((x) => x.fileExists(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(exists)); + + lintSettings.setup((l) => l.ignorePatterns).returns(() => ignorePattern); + settings.setup((x) => x.linting).returns(() => lintSettings.object); + + const doc = TypeMoq.Mock.ofType<TextDocument>(); + if (scheme) { + doc.setup((d) => d.uri).returns(() => Uri.parse(`${scheme}:${fileName}`)); + } else { + doc.setup((d) => d.uri).returns(() => Uri.file(fileName)); + } + doc.setup((d) => d.fileName).returns(() => fileName); + doc.setup((d) => d.languageId).returns(() => language); + return doc.object; + } +}); diff --git a/src/test/linters/linter.availability.unit.test.ts b/src/test/linters/linter.availability.unit.test.ts new file mode 100644 index 000000000000..23e11ec8e9cc --- /dev/null +++ b/src/test/linters/linter.availability.unit.test.ts @@ -0,0 +1,861 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../client/common/platform/types'; +import { + IConfigurationService, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, + Product +} from '../../client/common/types'; +import { Common, Linters } from '../../client/common/utils/localize'; +import { AvailableLinterActivator } from '../../client/linters/linterAvailability'; +import { LinterInfo } from '../../client/linters/linterInfo'; +import { IAvailableLinterActivator, ILinterInfo, LinterId } from '../../client/linters/types'; + +// tslint:disable:max-func-body-length no-any +suite('Linter Availability Provider tests', () => { + test('Availability feature is disabled when global default for languageServer === Jedi.', async () => { + // set expectations + const languageServerValue = LanguageServerType.Jedi; + const expectedResult = false; + + // arrange + const [ + appShellMock, + fsMock, + workspaceServiceMock, + configServiceMock, + factoryMock + ] = getDependenciesForAvailabilityTests(); + setupConfigurationServiceForJediSettingsTest(languageServerValue, configServiceMock); + + // call + const availabilityProvider = new AvailableLinterActivator( + appShellMock.object, + fsMock.object, + workspaceServiceMock.object, + configServiceMock.object, + factoryMock.object + ); + + // check expectaions + expect(availabilityProvider.isFeatureEnabled).is.equal( + expectedResult, + 'Avaialability feature should be disabled when python.languageServer is Jedi' + ); + workspaceServiceMock.verifyAll(); + }); + + test('Availability feature is enabled when global default for languageServer is Microsoft.', async () => { + // set expectations + const languageServerValue = LanguageServerType.Microsoft; + const expectedResult = true; + + // arrange + const [ + appShellMock, + fsMock, + workspaceServiceMock, + configServiceMock, + factoryMock + ] = getDependenciesForAvailabilityTests(); + setupConfigurationServiceForJediSettingsTest(languageServerValue, configServiceMock); + + const availabilityProvider = new AvailableLinterActivator( + appShellMock.object, + fsMock.object, + workspaceServiceMock.object, + configServiceMock.object, + factoryMock.object + ); + + expect(availabilityProvider.isFeatureEnabled).is.equal( + expectedResult, + 'Avaialability feature should be enabled when python.languageServer defaults to non-Jedi' + ); + workspaceServiceMock.verifyAll(); + }); + + test('Prompt will be performed when linter is not configured at all for the workspace, workspace-folder, or the user', async () => { + // setup expectations + const pylintUserValue = undefined; + const pylintWorkspaceValue = undefined; + const pylintWorkspaceFolderValue = undefined; + const expectedResult = true; + + const [ + appShellMock, + fsMock, + workspaceServiceMock, + configServiceMock, + factoryMock, + linterInfo + ] = getDependenciesForAvailabilityTests(); + setupWorkspaceMockForLinterConfiguredTests( + pylintUserValue, + pylintWorkspaceValue, + pylintWorkspaceFolderValue, + workspaceServiceMock + ); + + const availabilityProvider = new AvailableLinterActivator( + appShellMock.object, + fsMock.object, + workspaceServiceMock.object, + configServiceMock.object, + factoryMock.object + ); + + const result = availabilityProvider.isLinterUsingDefaultConfiguration(linterInfo); + + expect(result).to.equal(expectedResult, 'Linter is unconfigured but prompt did not get raised'); + workspaceServiceMock.verifyAll(); + }); + + test('No prompt performed when linter is configured as enabled for the workspace', async () => { + // setup expectations + const pylintUserValue = undefined; + const pylintWorkspaceValue = true; + const pylintWorkspaceFolderValue = undefined; + const expectedResult = false; + + const [ + appShellMock, + fsMock, + workspaceServiceMock, + configServiceMock, + factoryMock, + linterInfo + ] = getDependenciesForAvailabilityTests(); + setupWorkspaceMockForLinterConfiguredTests( + pylintUserValue, + pylintWorkspaceValue, + pylintWorkspaceFolderValue, + workspaceServiceMock + ); + + const availabilityProvider = new AvailableLinterActivator( + appShellMock.object, + fsMock.object, + workspaceServiceMock.object, + configServiceMock.object, + factoryMock.object + ); + + const result = availabilityProvider.isLinterUsingDefaultConfiguration(linterInfo); + expect(result).to.equal( + expectedResult, + 'Available linter prompt should not be shown when linter is configured for workspace.' + ); + workspaceServiceMock.verifyAll(); + }); + + test('No prompt performed when linter is configured as enabled for the entire user', async () => { + // setup expectations + const pylintUserValue = true; + const pylintWorkspaceValue = undefined; + const pylintWorkspaceFolderValue = undefined; + const expectedResult = false; + + // arrange + const [ + appShellMock, + fsMock, + workspaceServiceMock, + configServiceMock, + factoryMock, + linterInfo + ] = getDependenciesForAvailabilityTests(); + setupWorkspaceMockForLinterConfiguredTests( + pylintUserValue, + pylintWorkspaceValue, + pylintWorkspaceFolderValue, + workspaceServiceMock + ); + const availabilityProvider = new AvailableLinterActivator( + appShellMock.object, + fsMock.object, + workspaceServiceMock.object, + configServiceMock.object, + factoryMock.object + ); + + const result = availabilityProvider.isLinterUsingDefaultConfiguration(linterInfo); + expect(result).to.equal( + expectedResult, + 'Available linter prompt should not be shown when linter is configured for user.' + ); + workspaceServiceMock.verifyAll(); + }); + + test('No prompt performed when linter is configured as enabled for the workspace-folder', async () => { + // setup expectations + const pylintUserValue = undefined; + const pylintWorkspaceValue = undefined; + const pylintWorkspaceFolderValue = true; + const expectedResult = false; + + // arrange + const [ + appShellMock, + fsMock, + workspaceServiceMock, + configServiceMock, + factoryMock, + linterInfo + ] = getDependenciesForAvailabilityTests(); + setupWorkspaceMockForLinterConfiguredTests( + pylintUserValue, + pylintWorkspaceValue, + pylintWorkspaceFolderValue, + workspaceServiceMock + ); + const availabilityProvider = new AvailableLinterActivator( + appShellMock.object, + fsMock.object, + workspaceServiceMock.object, + configServiceMock.object, + factoryMock.object + ); + + const result = availabilityProvider.isLinterUsingDefaultConfiguration(linterInfo); + expect(result).to.equal( + expectedResult, + 'Available linter prompt should not be shown when linter is configured for workspace-folder.' + ); + workspaceServiceMock.verifyAll(); + }); + + async function testForLinterPromptResponse( + promptAction: 'enable' | 'ignore' | 'disablePrompt' | undefined, + promptEnabled = true + ): Promise<boolean> { + // arrange + const [appShellMock, fsMock, workspaceServiceMock, , factoryMock] = getDependenciesForAvailabilityTests(); + const configServiceMock = TypeMoq.Mock.ofType<IConfigurationService>(); + + const linterInfo = new (class extends LinterInfo { + public testIsEnabled: boolean = promptAction === 'enable' ? true : false; + + public async enableAsync(enabled: boolean, _resource?: Uri): Promise<void> { + this.testIsEnabled = enabled; + return Promise.resolve(); + } + })(Product.pylint, LinterId.PyLint, configServiceMock.object, ['.pylintrc', 'pylintrc']); + + const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + factoryMock + .setup((f) => f.createWorkspacePersistentState(TypeMoq.It.isAny(), true)) + .returns(() => notificationPromptEnabled.object); + notificationPromptEnabled.setup((n) => n.value).returns(() => promptEnabled); + const selections: ['enable', 'ignore', 'disablePrompt'] = ['enable', 'ignore', 'disablePrompt']; + const optButtons = [Linters.enableLinter().format(linterInfo.id), Common.notNow(), Common.doNotShowAgain()]; + if (promptEnabled) { + appShellMock + .setup((ap) => + ap.showInformationMessage( + TypeMoq.It.isValue(Linters.enablePylint().format(linterInfo.id)), + TypeMoq.It.isValue(Linters.enableLinter().format(linterInfo.id)), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(promptAction ? optButtons[selections.indexOf(promptAction)] : undefined)) + .verifiable(TypeMoq.Times.once()); + if (promptAction === 'disablePrompt') { + notificationPromptEnabled + .setup((n) => n.updateValue(false)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + } + } else { + appShellMock + .setup((ap) => + ap.showInformationMessage( + TypeMoq.It.isValue(Linters.enablePylint().format(linterInfo.id)), + TypeMoq.It.isValue(Linters.enableLinter().format(linterInfo.id)), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => Promise.resolve(promptAction ? optButtons[selections.indexOf(promptAction)] : undefined)) + .verifiable(TypeMoq.Times.never()); + } + + // perform test + const availabilityProvider = new AvailableLinterActivator( + appShellMock.object, + fsMock.object, + workspaceServiceMock.object, + configServiceMock.object, + factoryMock.object + ); + const result = await availabilityProvider.promptToConfigureAvailableLinter(linterInfo); + if (promptEnabled && promptAction === 'enable') { + expect(linterInfo.testIsEnabled).to.equal( + true, + 'LinterInfo test class was not updated as a result of the test.' + ); + } + + appShellMock.verifyAll(); + notificationPromptEnabled.verifyAll(); + + return result; + } + + test('Linter is enabled after being prompted and "Enable <linter>" is selected', async () => { + // set expectations + const expectedResult = true; + const promptAction = 'enable'; + + // run scenario + const result = await testForLinterPromptResponse(promptAction); + + // test results + expect(result).to.equal( + expectedResult, + 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.' + ); + }); + + test('Linter is left unconfigured and prompt is disabled when "Do not show again" is selected', async () => { + // set expectations + const expectedResult = false; + const promptAction = 'disablePrompt'; + + // run scenario + const result = await testForLinterPromptResponse(promptAction); + + // test results + expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return false.'); + }); + + test('Linter is left unconfigured and no notification is shown if prompt is disabled', async () => { + // set expectations + const expectedResult = false; + const promptAction = 'disablePrompt'; + + // run scenario + const result = await testForLinterPromptResponse(promptAction, false); + + // test results + expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return false.'); + }); + + test('Linter is left unconfigured after being prompted and the prompt is disabled without any selection made', async () => { + // set expectation + const promptAction = undefined; + const expectedResult = false; + + // run scenario + const result = await testForLinterPromptResponse(promptAction); + + // test results + expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return false.'); + }); + + test('Linter is left unconfigured when "Not now" is selected', async () => { + // set expectation + const promptAction = 'ignore'; + const expectedResult = false; + + // run scenario + const result = await testForLinterPromptResponse(promptAction); + + // test results + expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return false.'); + }); + + // Options to test the implementation of the IAvailableLinterActivator. + // All options default to values that would otherwise allow the prompt to appear. + class AvailablityTestOverallOptions { + public languageServerValue = LanguageServerType.Microsoft; + public pylintUserEnabled?: boolean; + public pylintWorkspaceEnabled?: boolean; + public pylintWorkspaceFolderEnabled?: boolean; + public linterIsInstalled: boolean = true; + public promptAction?: 'enable' | 'disablePrompt' | 'ignore'; + } + + async function performTestOfOverallImplementation(options: AvailablityTestOverallOptions): Promise<boolean> { + // arrange + const [ + appShellMock, + fsMock, + workspaceServiceMock, + configServiceMock, + factoryMock, + linterInfo + ] = getDependenciesForAvailabilityTests(); + const selections: ['enable', 'ignore', 'disablePrompt'] = ['enable', 'ignore', 'disablePrompt']; + const optButtons = [Linters.enableLinter().format(linterInfo.id), Common.notNow(), Common.doNotShowAgain()]; + appShellMock + .setup((ap) => + ap.showInformationMessage( + TypeMoq.It.isValue(Linters.enablePylint().format(linterInfo.id)), + TypeMoq.It.isValue(Linters.enableLinter().format(linterInfo.id)), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + ) + ) + .returns(() => + Promise.resolve(options.promptAction ? optButtons[selections.indexOf(options.promptAction)] : undefined) + ) + .verifiable(TypeMoq.Times.once()); + + const workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; + workspaceServiceMock + .setup((c) => c.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceServiceMock + .setup((c) => c.workspaceFolders) + .returns(() => [workspaceFolder]) + .verifiable(TypeMoq.Times.once()); + fsMock + .setup((fs) => fs.fileExists(TypeMoq.It.isAny())) + .returns(async () => options.linterIsInstalled) + .verifiable(TypeMoq.Times.atLeastOnce()); + + setupConfigurationServiceForJediSettingsTest(options.languageServerValue, configServiceMock); + setupWorkspaceMockForLinterConfiguredTests( + options.pylintUserEnabled, + options.pylintWorkspaceEnabled, + options.pylintWorkspaceFolderEnabled, + workspaceServiceMock + ); + + const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + factoryMock + .setup((f) => f.createWorkspacePersistentState(TypeMoq.It.isAny(), true)) + .returns(() => notificationPromptEnabled.object); + notificationPromptEnabled.setup((n) => n.value).returns(() => true); + // perform test + const availabilityProvider: IAvailableLinterActivator = new AvailableLinterActivator( + appShellMock.object, + fsMock.object, + workspaceServiceMock.object, + configServiceMock.object, + factoryMock.object + ); + return availabilityProvider.promptIfLinterAvailable(linterInfo); + } + + test('Overall implementation does not change configuration when feature disabled', async () => { + // set expectations + const testOpts = new AvailablityTestOverallOptions(); + testOpts.languageServerValue = LanguageServerType.Jedi; + const expectedResult = false; + + // arrange + const result = await performTestOfOverallImplementation(testOpts); + + // perform test + expect(expectedResult).to.equal( + result, + 'promptIfLinterAvailable should not change any configuration when python.languageServer is Jedi.' + ); + }); + + test('Overall implementation does not change configuration when linter is configured (enabled)', async () => { + // set expectations + const testOpts = new AvailablityTestOverallOptions(); + testOpts.pylintWorkspaceEnabled = true; + const expectedResult = false; + + // arrange + const result = await performTestOfOverallImplementation(testOpts); + + // perform test + expect(expectedResult).to.equal( + result, + 'Configuration should not change if the linter is configured in any way.' + ); + }); + + test('Overall implementation does not change configuration when linter is configured (disabled)', async () => { + // set expectations + const testOpts = new AvailablityTestOverallOptions(); + testOpts.pylintWorkspaceEnabled = false; + const expectedResult = false; + + // arrange + const result = await performTestOfOverallImplementation(testOpts); + + expect(expectedResult).to.equal( + result, + 'Configuration should not change if the linter is disabled in any way.' + ); + }); + + test('Overall implementation does not change configuration when linter is unavailable in current workspace environment', async () => { + // set expectations + const testOpts = new AvailablityTestOverallOptions(); + testOpts.pylintWorkspaceEnabled = true; + const expectedResult = false; + + // arrange + const result = await performTestOfOverallImplementation(testOpts); + + expect(expectedResult).to.equal( + result, + 'Configuration should not change if the linter is unavailable in the current workspace environment.' + ); + }); + + test('Overall implementation does not change configuration when user is prompted and prompt is dismissed', async () => { + // set expectations + const testOpts = new AvailablityTestOverallOptions(); + testOpts.promptAction = undefined; // just being explicit for test readability - this is the default + const expectedResult = false; + + // arrange + const result = await performTestOfOverallImplementation(testOpts); + + expect(expectedResult).to.equal( + result, + 'Configuration should not change if the user is prompted and they dismiss the prompt.' + ); + }); + + test('Overall implementation does not change configuration when user is prompted and "Do not show again" is selected', async () => { + // set expectations + const testOpts = new AvailablityTestOverallOptions(); + testOpts.promptAction = 'disablePrompt'; + const expectedResult = false; + + // arrange + const result = await performTestOfOverallImplementation(testOpts); + + expect(expectedResult).to.equal( + result, + 'Configuration should change if the user is prompted and they choose to update the linter config.' + ); + }); + + test('Overall implementation does not change configuration when user is prompted and "Not now" is selected', async () => { + // set expectations + const testOpts = new AvailablityTestOverallOptions(); + testOpts.promptAction = 'ignore'; + const expectedResult = false; + + // arrange + const result = await performTestOfOverallImplementation(testOpts); + + expect(expectedResult).to.equal( + result, + 'Configuration should change if the user is prompted and they choose to update the linter config.' + ); + }); + + test('Overall implementation changes configuration when user is prompted and "Enable <linter>" is selected', async () => { + // set expectations + const testOpts = new AvailablityTestOverallOptions(); + testOpts.promptAction = 'enable'; + const expectedResult = true; + + // arrange + const result = await performTestOfOverallImplementation(testOpts); + + expect(expectedResult).to.equal( + result, + 'Configuration should change if the user is prompted and they choose to update the linter config.' + ); + }); + + test('Discovery of linter is available in the environment returns true when it succeeds and is present', async () => { + // set expectations + const linterIsInstalled = true; + const expectedResult = true; + + // arrange + const [ + appShellMock, + fsMock, + workspaceServiceMock, + configServiceMock, + factoryMock, + linterInfo + ] = getDependenciesForAvailabilityTests(); + setupInstallerForAvailabilityTest(linterInfo, linterIsInstalled, fsMock, workspaceServiceMock); + + // perform test + const availabilityProvider = new AvailableLinterActivator( + appShellMock.object, + fsMock.object, + workspaceServiceMock.object, + configServiceMock.object, + factoryMock.object + ); + const result = await availabilityProvider.isLinterAvailable(linterInfo, undefined); + + expect(result).to.equal( + expectedResult, + 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.' + ); + fsMock.verifyAll(); + workspaceServiceMock.verifyAll(); + }); + + test('Discovery of linter is available in the environment returns false when it succeeds and is not present', async () => { + // set expectations + const linterIsInstalled = false; + const expectedResult = false; + + // arrange + const [ + appShellMock, + fsMock, + workspaceServiceMock, + configServiceMock, + factoryMock, + linterInfo + ] = getDependenciesForAvailabilityTests(); + setupInstallerForAvailabilityTest(linterInfo, linterIsInstalled, fsMock, workspaceServiceMock); + + // perform test + const availabilityProvider = new AvailableLinterActivator( + appShellMock.object, + fsMock.object, + workspaceServiceMock.object, + configServiceMock.object, + factoryMock.object + ); + const result = await availabilityProvider.isLinterAvailable(linterInfo, undefined); + + expect(result).to.equal( + expectedResult, + 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.' + ); + fsMock.verifyAll(); + workspaceServiceMock.verifyAll(); + }); + + suite('Linter Availability', () => { + let availabilityProvider: AvailableLinterActivator; + let workspaceService: IWorkspaceService; + let fs: IFileSystem; + const defaultWorkspace: WorkspaceFolder = { + uri: Uri.file(path.join('a', 'b', 'default')), + name: 'default', + index: 0 + }; + const resource = Uri.file(__dirname); + setup(() => { + workspaceService = mock(WorkspaceService); + fs = mock(FileSystem); + + availabilityProvider = new AvailableLinterActivator( + instance(mock(ApplicationShell)), + instance(fs), + instance(workspaceService), + instance(mock(ConfigurationService)), + instance(mock(PersistentStateFactory)) + ); + }); + test('No linters when there are no workspaces', async () => { + when(workspaceService.hasWorkspaceFolders).thenReturn(false); + const linterInfo = ({} as any) as ILinterInfo; + const available = await availabilityProvider.isLinterAvailable(linterInfo, undefined); + + expect(available).to.equal(false, 'Should be false'); + }); + + [undefined, { uri: Uri.file(path.join('c', 'd', 'resource')), name: 'another', index: 10 }].forEach( + (workspaceFolderRelatedToResource) => { + const testSuffix = workspaceFolderRelatedToResource + ? '(has a corresponding workspace)' + : '(use default workspace)'; + // If there's a workspace, then access default workspace. + const workspaceFolder = workspaceFolderRelatedToResource || defaultWorkspace; + test(`No linters when there are no config files ${testSuffix}`, async () => { + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + const linterInfo = ({ configFileNames: [] } as any) as ILinterInfo; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolderRelatedToResource); + when(workspaceService.workspaceFolders).thenReturn([defaultWorkspace]); + const available = await availabilityProvider.isLinterAvailable(linterInfo, resource); + + expect(available).to.equal(false, 'Should be false'); + verify(workspaceService.getWorkspaceFolder(resource)).once(); + verify(fs.fileExists(anything())).never(); + // If there's a workspace, then access default workspace. + if (workspaceFolderRelatedToResource) { + verify(workspaceService.workspaceFolders).never(); + } else { + verify(workspaceService.workspaceFolders).once(); + } + }); + test(`No linters when there none of the config files exist ${testSuffix}`, async () => { + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + const linterInfo = ({ configFileNames: ['1', '2'] } as any) as ILinterInfo; + when(fs.fileExists(anything())).thenResolve(false); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolderRelatedToResource); + when(workspaceService.workspaceFolders).thenReturn([defaultWorkspace]); + + const available = await availabilityProvider.isLinterAvailable(linterInfo, resource); + + expect(available).to.equal(false, 'Should be false'); + verify(workspaceService.getWorkspaceFolder(resource)).once(); + verify(fs.fileExists(anything())).twice(); + verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '1'))).once(); + verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '2'))).once(); + if (workspaceFolderRelatedToResource) { + verify(workspaceService.workspaceFolders).never(); + } else { + verify(workspaceService.workspaceFolders).once(); + } + }); + test(`Linters exist when all of the config files exist ${testSuffix}`, async () => { + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + const linterInfo = ({ configFileNames: ['1', '2'] } as any) as ILinterInfo; + when(fs.fileExists(anything())).thenResolve(true); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolderRelatedToResource); + when(workspaceService.workspaceFolders).thenReturn([defaultWorkspace]); + + const available = await availabilityProvider.isLinterAvailable(linterInfo, resource); + + expect(available).to.equal(true, 'Should be true'); + verify(workspaceService.getWorkspaceFolder(resource)).once(); + verify(fs.fileExists(anything())).once(); + verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '1'))).once(); + // Check only the first file, if that exists, no point checking the rest. + verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '2'))).never(); + if (workspaceFolderRelatedToResource) { + verify(workspaceService.workspaceFolders).never(); + } else { + verify(workspaceService.workspaceFolders).once(); + } + }); + test(`Linters exist when one of the config files exist ${testSuffix}`, async () => { + when(workspaceService.hasWorkspaceFolders).thenReturn(true); + const linterInfo = ({ configFileNames: ['1', '2', '3'] } as any) as ILinterInfo; + when(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '1'))).thenResolve(false); + when(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '2'))).thenResolve(true); + when(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '3'))).thenResolve(false); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolderRelatedToResource); + when(workspaceService.workspaceFolders).thenReturn([defaultWorkspace]); + + const available = await availabilityProvider.isLinterAvailable(linterInfo, resource); + + expect(available).to.equal(true, 'Should be true'); + verify(workspaceService.getWorkspaceFolder(resource)).once(); + verify(fs.fileExists(anything())).twice(); + verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '1'))).once(); + verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '2'))).once(); + // Check only the second file, if that exists, no point checking the rest. + verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '3'))).never(); + if (workspaceFolderRelatedToResource) { + verify(workspaceService.workspaceFolders).never(); + } else { + verify(workspaceService.workspaceFolders).once(); + } + }); + } + ); + }); +}); + +function setupWorkspaceMockForLinterConfiguredTests( + enabledForUser: boolean | undefined, + enabeldForWorkspace: boolean | undefined, + enabledForWorkspaceFolder: boolean | undefined, + workspaceServiceMock?: TypeMoq.IMock<IWorkspaceService> +): TypeMoq.IMock<IWorkspaceService> { + if (!workspaceServiceMock) { + workspaceServiceMock = TypeMoq.Mock.ofType<IWorkspaceService>(); + } + const workspaceConfiguration = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceConfiguration + .setup((wc) => wc.inspect(TypeMoq.It.isValue('pylintEnabled'))) + .returns(() => { + return { + key: '', + globalValue: enabledForUser, + defaultValue: false, + workspaceFolderValue: enabeldForWorkspace, + workspaceValue: enabledForWorkspaceFolder + }; + }) + .verifiable(TypeMoq.Times.once()); + + workspaceServiceMock + .setup((ws) => ws.getConfiguration(TypeMoq.It.isValue('python.linting'), TypeMoq.It.isAny())) + .returns(() => workspaceConfiguration.object) + .verifiable(TypeMoq.Times.once()); + + return workspaceServiceMock; +} + +function setupConfigurationServiceForJediSettingsTest( + languageServerValue: LanguageServerType, + configServiceMock: TypeMoq.IMock<IConfigurationService> +): [TypeMoq.IMock<IConfigurationService>, TypeMoq.IMock<IPythonSettings>] { + if (!configServiceMock) { + configServiceMock = TypeMoq.Mock.ofType<IConfigurationService>(); + } + const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + pythonSettings.setup((ps) => ps.languageServer).returns(() => languageServerValue); + + configServiceMock.setup((cs) => cs.getSettings()).returns(() => pythonSettings.object); + return [configServiceMock, pythonSettings]; +} + +function setupInstallerForAvailabilityTest( + _linterInfo: LinterInfo, + linterIsInstalled: boolean, + fsMock: TypeMoq.IMock<IFileSystem>, + workspaceServiceMock: TypeMoq.IMock<IWorkspaceService> +): TypeMoq.IMock<IFileSystem> { + if (!fsMock) { + fsMock = TypeMoq.Mock.ofType<IFileSystem>(); + } + const workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; + workspaceServiceMock + .setup((c) => c.hasWorkspaceFolders) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + workspaceServiceMock.setup((c) => c.workspaceFolders).returns(() => [workspaceFolder]); + workspaceServiceMock.setup((c) => c.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); + fsMock + .setup((fs) => fs.fileExists(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(linterIsInstalled)) + .verifiable(TypeMoq.Times.atLeastOnce()); + + return fsMock; +} + +function getDependenciesForAvailabilityTests(): [ + TypeMoq.IMock<IApplicationShell>, + TypeMoq.IMock<IFileSystem>, + TypeMoq.IMock<IWorkspaceService>, + TypeMoq.IMock<IConfigurationService>, + TypeMoq.IMock<IPersistentStateFactory>, + LinterInfo +] { + const configServiceMock = TypeMoq.Mock.ofType<IConfigurationService>(); + return [ + TypeMoq.Mock.ofType<IApplicationShell>(), + TypeMoq.Mock.ofType<IFileSystem>(), + TypeMoq.Mock.ofType<IWorkspaceService>(), + TypeMoq.Mock.ofType<IConfigurationService>(), + TypeMoq.Mock.ofType<IPersistentStateFactory>(), + new LinterInfo(Product.pylint, LinterId.PyLint, configServiceMock.object, ['.pylintrc', 'pylintrc']) + ]; +} diff --git a/src/test/linters/linterCommands.unit.test.ts b/src/test/linters/linterCommands.unit.test.ts new file mode 100644 index 000000000000..4f7259fd41d7 --- /dev/null +++ b/src/test/linters/linterCommands.unit.test.ts @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length messages-must-be-localized + +import { expect } from 'chai'; +import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../../client/common/application/types'; +import { Commands } from '../../client/common/constants'; +import { Product } from '../../client/common/types'; +import { ServiceContainer } from '../../client/ioc/container'; +import { LinterCommands } from '../../client/linters/linterCommands'; +import { LinterManager } from '../../client/linters/linterManager'; +import { LintingEngine } from '../../client/linters/lintingEngine'; +import { ILinterInfo, ILinterManager, ILintingEngine } from '../../client/linters/types'; + +suite('Linting - Linter Commands', () => { + let linterCommands: LinterCommands; + let manager: ILinterManager; + let shell: IApplicationShell; + let docManager: IDocumentManager; + let cmdManager: ICommandManager; + let lintingEngine: ILintingEngine; + setup(() => { + const svcContainer = mock(ServiceContainer); + manager = mock(LinterManager); + shell = mock(ApplicationShell); + docManager = mock(DocumentManager); + cmdManager = mock(CommandManager); + lintingEngine = mock(LintingEngine); + when(svcContainer.get<ILinterManager>(ILinterManager)).thenReturn(instance(manager)); + when(svcContainer.get<IApplicationShell>(IApplicationShell)).thenReturn(instance(shell)); + when(svcContainer.get<IDocumentManager>(IDocumentManager)).thenReturn(instance(docManager)); + when(svcContainer.get<ICommandManager>(ICommandManager)).thenReturn(instance(cmdManager)); + when(svcContainer.get<ILintingEngine>(ILintingEngine)).thenReturn(instance(lintingEngine)); + linterCommands = new LinterCommands(instance(svcContainer)); + }); + + test('Commands are registered', () => { + verify(cmdManager.registerCommand(Commands.Set_Linter, anything())).once(); + verify(cmdManager.registerCommand(Commands.Enable_Linter, anything())).once(); + verify(cmdManager.registerCommand(Commands.Run_Linter, anything())).once(); + }); + + test('Run Linting method will lint all open files', async () => { + when(lintingEngine.lintOpenPythonFiles()).thenResolve('Hello' as any); + + const result = await linterCommands.runLinting(); + + expect(result).to.be.equal('Hello'); + }); + + async function testEnableLintingWithCurrentState(currentState: boolean, selectedState: 'on' | 'off' | undefined) { + when(manager.isLintingEnabled(true, anything())).thenResolve(currentState); + const expectedQuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${currentState ? 'on' : 'off'}` + }; + when(shell.showQuickPick(anything(), anything())).thenResolve(selectedState as any); + + await linterCommands.enableLintingAsync(); + + verify(shell.showQuickPick(anything(), anything())).once(); + const options = capture(shell.showQuickPick).last()[0]; + const quickPickOptions = capture(shell.showQuickPick).last()[1]; + expect(options).to.deep.equal(['on', 'off']); + expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); + + if (selectedState) { + verify(manager.enableLintingAsync(selectedState === 'on', anything())).once(); + } else { + verify(manager.enableLintingAsync(anything(), anything())).never(); + } + } + test("Enable linting should check if linting is enabled, and display current state of 'on' and select nothing", async () => { + await testEnableLintingWithCurrentState(true, undefined); + }); + test("Enable linting should check if linting is enabled, and display current state of 'on' and select 'on'", async () => { + await testEnableLintingWithCurrentState(true, 'on'); + }); + test("Enable linting should check if linting is enabled, and display current state of 'on' and select 'off'", async () => { + await testEnableLintingWithCurrentState(true, 'off'); + }); + test("Enable linting should check if linting is enabled, and display current state of 'off' and select 'on'", async () => { + await testEnableLintingWithCurrentState(true, 'on'); + }); + test("Enable linting should check if linting is enabled, and display current state of 'off' and select 'off'", async () => { + await testEnableLintingWithCurrentState(true, 'off'); + }); + + test('Set Linter should display a quickpick', async () => { + when(manager.getAllLinterInfos()).thenReturn([]); + when(manager.getActiveLinters(true, anything())).thenResolve([]); + when(shell.showQuickPick(anything(), anything())).thenResolve(); + const expectedQuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: 'current: none' + }; + + await linterCommands.setLinterAsync(); + + verify(shell.showQuickPick(anything(), anything())); + const quickPickOptions = capture(shell.showQuickPick).last()[1]; + expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); + }); + test('Set Linter should display a quickpick and currently active linter when only one is enabled', async () => { + const linterId = 'Hello World'; + const activeLinters: ILinterInfo[] = [{ id: linterId } as any]; + when(manager.getAllLinterInfos()).thenReturn([]); + when(manager.getActiveLinters(true, anything())).thenResolve(activeLinters); + when(shell.showQuickPick(anything(), anything())).thenResolve(); + const expectedQuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: `current: ${linterId}` + }; + + await linterCommands.setLinterAsync(); + + verify(shell.showQuickPick(anything(), anything())).once(); + const quickPickOptions = capture(shell.showQuickPick).last()[1]; + expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); + }); + test('Set Linter should display a quickpick and with message about multiple linters being enabled', async () => { + const activeLinters: ILinterInfo[] = [{ id: 'linterId' } as any, { id: 'linterId2' } as any]; + when(manager.getAllLinterInfos()).thenReturn([]); + when(manager.getActiveLinters(true, anything())).thenResolve(activeLinters); + when(shell.showQuickPick(anything(), anything())).thenResolve(); + const expectedQuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: 'current: multiple selected' + }; + + await linterCommands.setLinterAsync(); + + verify(shell.showQuickPick(anything(), anything())); + const quickPickOptions = capture(shell.showQuickPick).last()[1]; + expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); + }); + test('Selecting a linter should display warning message about multiple linters', async () => { + const linters: ILinterInfo[] = [{ id: '1' }, { id: '2' }, { id: '3', product: 'Three' }] as any; + const activeLinters: ILinterInfo[] = [{ id: '1' }, { id: '3' }] as any; + when(manager.getAllLinterInfos()).thenReturn(linters); + when(manager.getActiveLinters(true, anything())).thenResolve(activeLinters); + when(shell.showQuickPick(anything(), anything())).thenResolve('3' as any); + when(shell.showWarningMessage(anything(), 'Yes', 'No')).thenResolve('Yes' as any); + const expectedQuickPickOptions = { + matchOnDetail: true, + matchOnDescription: true, + placeHolder: 'current: multiple selected' + }; + + await linterCommands.setLinterAsync(); + + verify(shell.showQuickPick(anything(), anything())).once(); + verify(shell.showWarningMessage(anything(), 'Yes', 'No')).once(); + const quickPickOptions = capture(shell.showQuickPick).last()[1]; + expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); + verify(manager.setActiveLintersAsync(deepEqual([('Three' as any) as Product]), anything())).once(); + }); +}); diff --git a/src/test/linters/linterManager.unit.test.ts b/src/test/linters/linterManager.unit.test.ts new file mode 100644 index 000000000000..efdc927eaa9d --- /dev/null +++ b/src/test/linters/linterManager.unit.test.ts @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length messages-must-be-localized + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService +} from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { ProductNames } from '../../client/common/installer/productNames'; +import { ProductService } from '../../client/common/installer/productService'; +import { IConfigurationService, Product, ProductType } from '../../client/common/types'; +import { getNamesAndValues } from '../../client/common/utils/enum'; +import { ServiceContainer } from '../../client/ioc/container'; +import { LinterInfo } from '../../client/linters/linterInfo'; +import { LinterManager } from '../../client/linters/linterManager'; +import { LintingEngine } from '../../client/linters/lintingEngine'; +import { ILinterInfo, ILintingEngine } from '../../client/linters/types'; + +suite('Linting - Linter Manager', () => { + let linterManager: LinterManagerTest; + let shell: IApplicationShell; + let docManager: IDocumentManager; + let cmdManager: ICommandManager; + let lintingEngine: ILintingEngine; + let configService: IConfigurationService; + let workspaceService: IWorkspaceService; + class LinterManagerTest extends LinterManager { + // Override base class property to make it public. + public linters!: ILinterInfo[]; + public async enableUnconfiguredLinters(resource?: Uri) { + await super.enableUnconfiguredLinters(resource); + } + } + setup(() => { + const svcContainer = mock(ServiceContainer); + shell = mock(ApplicationShell); + docManager = mock(DocumentManager); + cmdManager = mock(CommandManager); + lintingEngine = mock(LintingEngine); + configService = mock(ConfigurationService); + workspaceService = mock(WorkspaceService); + when(svcContainer.get<IApplicationShell>(IApplicationShell)).thenReturn(instance(shell)); + when(svcContainer.get<IDocumentManager>(IDocumentManager)).thenReturn(instance(docManager)); + when(svcContainer.get<ICommandManager>(ICommandManager)).thenReturn(instance(cmdManager)); + when(svcContainer.get<ILintingEngine>(ILintingEngine)).thenReturn(instance(lintingEngine)); + when(svcContainer.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(configService)); + when(svcContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspaceService)); + linterManager = new LinterManagerTest(instance(svcContainer), instance(workspaceService)); + }); + + test('Get all linters will return a list of all linters', () => { + const linters = linterManager.getAllLinterInfos(); + + expect(linters).to.be.lengthOf(8); + + const productService = new ProductService(); + const linterProducts = getNamesAndValues<Product>(Product) + .filter((product) => productService.getProductType(product.value) === ProductType.Linter) + .map((item) => ProductNames.get(item.value)); + expect(linters.map((item) => item.id).sort()).to.be.deep.equal(linterProducts.sort()); + }); + + test('Get linter info for non-linter product should throw an exception', () => { + const productService = new ProductService(); + getNamesAndValues<Product>(Product).forEach((prod) => { + if (productService.getProductType(prod.value) === ProductType.Linter) { + const info = linterManager.getLinterInfo(prod.value); + expect(info.id).to.equal(ProductNames.get(prod.value)); + expect(info).not.to.be.equal(undefined, 'should not be unedfined'); + } else { + expect(() => linterManager.getLinterInfo(prod.value)).to.throw(); + } + }); + }); + test('Pylint configuration file watch', async () => { + const pylint = linterManager.getLinterInfo(Product.pylint); + assert.equal(pylint.configFileNames.length, 2, 'Pylint configuration file count is incorrect.'); + assert.notEqual(pylint.configFileNames.indexOf('pylintrc'), -1, 'Pylint configuration files miss pylintrc.'); + assert.notEqual(pylint.configFileNames.indexOf('.pylintrc'), -1, 'Pylint configuration files miss .pylintrc.'); + }); + + [undefined, Uri.parse('something')].forEach((resource) => { + const testResourceSuffix = `(${resource ? 'with a resource' : 'without a resource'})`; + [true, false].forEach((enabled) => { + const testSuffix = `(${enabled ? 'enable' : 'disable'}) & ${testResourceSuffix}`; + test(`Enable linting should update config ${testSuffix}`, async () => { + when(configService.updateSetting('linting.enabled', enabled, resource)).thenResolve(); + + await linterManager.enableLintingAsync(enabled, resource); + + verify(configService.updateSetting('linting.enabled', enabled, resource)).once(); + }); + }); + test(`getActiveLinters will check if linter is enabled and in silent mode ${testResourceSuffix}`, async () => { + const linterInfo = mock(LinterInfo); + const instanceOfLinterInfo = instance(linterInfo); + linterManager.linters = [instanceOfLinterInfo]; + when(linterInfo.isEnabled(resource)).thenReturn(true); + + const linters = await linterManager.getActiveLinters(true, resource); + + verify(linterInfo.isEnabled(resource)).once(); + expect(linters[0]).to.deep.equal(instanceOfLinterInfo); + }); + test(`getActiveLinters will check if linter is enabled and not in silent mode ${testResourceSuffix}`, async () => { + const linterInfo = mock(LinterInfo); + const instanceOfLinterInfo = instance(linterInfo); + linterManager.linters = [instanceOfLinterInfo]; + when(linterInfo.isEnabled(resource)).thenReturn(true); + let enableUnconfiguredLintersInvoked = false; + linterManager.enableUnconfiguredLinters = async () => { + enableUnconfiguredLintersInvoked = true; + }; + + const linters = await linterManager.getActiveLinters(false, resource); + + verify(linterInfo.isEnabled(resource)).once(); + expect(linters[0]).to.deep.equal(instanceOfLinterInfo); + expect(enableUnconfiguredLintersInvoked).to.equal(true, 'not invoked'); + }); + + test(`setActiveLintersAsync with invalid products does nothing ${testResourceSuffix}`, async () => { + let getActiveLintersInvoked = false; + linterManager.getActiveLinters = async () => { + getActiveLintersInvoked = true; + return []; + }; + + await linterManager.setActiveLintersAsync([Product.ctags, Product.pytest], resource); + + expect(getActiveLintersInvoked).to.be.equal(false, 'Should not be invoked'); + }); + test(`setActiveLintersAsync with single product will disable it then enable it ${testResourceSuffix}`, async () => { + const linterInfo = mock(LinterInfo); + const instanceOfLinterInfo = instance(linterInfo); + linterManager.linters = [instanceOfLinterInfo]; + when(linterInfo.product).thenReturn(Product.flake8); + when(linterInfo.enableAsync(false, resource)).thenResolve(); + linterManager.getActiveLinters = () => Promise.resolve([instanceOfLinterInfo]); + linterManager.enableLintingAsync = () => Promise.resolve(); + + await linterManager.setActiveLintersAsync([Product.flake8], resource); + + verify(linterInfo.enableAsync(false, resource)).atLeast(1); + verify(linterInfo.enableAsync(true, resource)).atLeast(1); + }); + test(`setActiveLintersAsync with single product will disable all existing then enable the necessary two ${testResourceSuffix}`, async () => { + const linters = new Map<Product, LinterInfo>(); + const linterInstances = new Map<Product, LinterInfo>(); + linterManager.linters = []; + [Product.flake8, Product.mypy, Product.prospector, Product.bandit, Product.pydocstyle].forEach( + (product) => { + const linterInfo = mock(LinterInfo); + const instanceOfLinterInfo = instance(linterInfo); + linterManager.linters.push(instanceOfLinterInfo); + linters.set(product, linterInfo); + linterInstances.set(product, instanceOfLinterInfo); + when(linterInfo.product).thenReturn(product); + when(linterInfo.enableAsync(anything(), resource)).thenResolve(); + } + ); + + linterManager.getActiveLinters = () => Promise.resolve(Array.from(linterInstances.values())); + linterManager.enableLintingAsync = () => Promise.resolve(); + + const lintersToEnable = [Product.flake8, Product.mypy, Product.pydocstyle]; + await linterManager.setActiveLintersAsync([Product.flake8, Product.mypy, Product.pydocstyle], resource); + + linters.forEach((item, product) => { + verify(item.enableAsync(false, resource)).atLeast(1); + if (lintersToEnable.indexOf(product) >= 0) { + verify(item.enableAsync(true, resource)).atLeast(1); + } + }); + }); + }); +}); diff --git a/src/test/linters/linterinfo.unit.test.ts b/src/test/linters/linterinfo.unit.test.ts new file mode 100644 index 000000000000..a648c1949d46 --- /dev/null +++ b/src/test/linters/linterinfo.unit.test.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:chai-vague-errors no-unused-expression max-func-body-length no-any + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { LanguageServerType } from '../../client/activation/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { PylintLinterInfo } from '../../client/linters/linterInfo'; + +suite('Linter Info - Pylint', () => { + const workspace = mock(WorkspaceService); + const config = mock(ConfigurationService); + + test('Test disabled when Pylint is explicitly disabled', async () => { + const linterInfo = new PylintLinterInfo(instance(config), instance(workspace), []); + + when(config.getSettings(anything())).thenReturn({ + linting: { pylintEnabled: false }, + languageServer: LanguageServerType.Jedi + } as any); + + expect(linterInfo.isEnabled()).to.be.false; + }); + test('Test disabled when Jedi is enabled and Pylint is explicitly disabled', async () => { + const linterInfo = new PylintLinterInfo(instance(config), instance(workspace), []); + + when(config.getSettings(anything())).thenReturn({ + linting: { pylintEnabled: false }, + languageServer: LanguageServerType.Jedi + } as any); + + expect(linterInfo.isEnabled()).to.be.false; + }); + test('Test enabled when Jedi is enabled and Pylint is explicitly enabled', async () => { + const linterInfo = new PylintLinterInfo(instance(config), instance(workspace), []); + + when(config.getSettings(anything())).thenReturn({ + linting: { pylintEnabled: true }, + languageServer: LanguageServerType.Jedi + } as any); + + expect(linterInfo.isEnabled()).to.be.true; + }); + test('Test disabled when using language server and Pylint is not configured', async () => { + const linterInfo = new PylintLinterInfo(instance(config), instance(workspace), []); + + when(config.getSettings(anything())).thenReturn({ + linting: { pylintEnabled: true }, + languageServer: LanguageServerType.Microsoft + } as any); + + const pythonConfig = { + // tslint:disable-next-line:no-empty + inspect: () => {} + }; + when(workspace.getConfiguration('python', anything())).thenReturn(pythonConfig as any); + + expect(linterInfo.isEnabled()).to.be.false; + }); + test('Should inspect the value of linting.pylintEnabled when using language server', async () => { + const linterInfo = new PylintLinterInfo(instance(config), instance(workspace), []); + const inspectStub = sinon.stub(); + const pythonConfig = { + inspect: inspectStub + }; + + when(config.getSettings(anything())).thenReturn({ + linting: { pylintEnabled: true }, + languageServer: LanguageServerType.Microsoft + } as any); + when(workspace.getConfiguration('python', anything())).thenReturn(pythonConfig as any); + + expect(linterInfo.isEnabled()).to.be.false; + expect(inspectStub.calledOnceWith('linting.pylintEnabled')).to.be.true; + }); + const testsForisEnabled = [ + { + testName: 'When workspaceFolder setting is provided', + inspection: { workspaceFolderValue: true } + }, + { + testName: 'When workspace setting is provided', + inspection: { workspaceValue: true } + }, + { + testName: 'When global setting is provided', + inspection: { globalValue: true } + } + ]; + + suite('Test is enabled when using Language Server and Pylint is configured', () => { + testsForisEnabled.forEach((testParams) => { + test(testParams.testName, async () => { + // tslint:disable-next-line:no-shadowed-variable + const config = mock(ConfigurationService); + const workspaceService = mock(WorkspaceService); + const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); + + const pythonConfig = { + inspect: () => testParams.inspection + }; + when(config.getSettings(anything())).thenReturn({ + linting: { pylintEnabled: true }, + languageServer: LanguageServerType.Microsoft + } as any); + when(workspaceService.getConfiguration('python', anything())).thenReturn(pythonConfig as any); + + expect(linterInfo.isEnabled()).to.be.true; + }); + }); + }); +}); diff --git a/src/test/linters/mypy.unit.test.ts b/src/test/linters/mypy.unit.test.ts new file mode 100644 index 000000000000..b0d0843a9173 --- /dev/null +++ b/src/test/linters/mypy.unit.test.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-object-literal-type-assertion + +import { expect } from 'chai'; +import { parseLine } from '../../client/linters/baseLinter'; +import { REGEX } from '../../client/linters/mypy'; +import { ILintMessage, LinterId } from '../../client/linters/types'; + +// This following is a real-world example. See gh=2380. +// tslint:disable-next-line:no-multiline-string +const output = ` +provider.pyi:10: error: Incompatible types in assignment (expression has type "str", variable has type "int") +provider.pyi:11: error: Name 'not_declared_var' is not defined +provider.pyi:12:21: error: Expression has type "Any" +`; + +suite('Linting - MyPy', () => { + test('regex', async () => { + const lines = output.split('\n'); + const tests: [string, ILintMessage][] = [ + [ + lines[1], + { + code: undefined, + message: 'Incompatible types in assignment (expression has type "str", variable has type "int")', + column: 0, + line: 10, + type: 'error', + provider: 'mypy' + } as ILintMessage + ], + [ + lines[2], + { + code: undefined, + message: "Name 'not_declared_var' is not defined", + column: 0, + line: 11, + type: 'error', + provider: 'mypy' + } as ILintMessage + ], + [ + lines[3], + { + code: undefined, + message: 'Expression has type "Any"', + column: 21, + line: 12, + type: 'error', + provider: 'mypy' + } as ILintMessage + ] + ]; + for (const [line, expected] of tests) { + const msg = parseLine(line, REGEX, LinterId.MyPy); + + expect(msg).to.deep.equal(expected); + } + }); +}); diff --git a/src/test/linters/pylint.test.ts b/src/test/linters/pylint.test.ts new file mode 100644 index 000000000000..02b19a7ef9bd --- /dev/null +++ b/src/test/linters/pylint.test.ts @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Container } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { + CancellationTokenSource, + DiagnosticSeverity, + OutputChannel, + TextDocument, + Uri, + WorkspaceConfiguration, + WorkspaceFolder +} from 'vscode'; +import { LanguageServerType } from '../../client/activation/types'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { IPythonToolExecutionService } from '../../client/common/process/types'; +import { ExecutionInfo, IConfigurationService, IInstaller, IPythonSettings } from '../../client/common/types'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService +} from '../../client/interpreter/autoSelection/types'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { LinterManager } from '../../client/linters/linterManager'; +import { Pylint } from '../../client/linters/pylint'; +import { ILinterManager } from '../../client/linters/types'; +import { MockLintingSettings } from '../mockClasses'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; + +// tslint:disable-next-line:max-func-body-length +suite('Linting - Pylint', () => { + const basePath = '/user/a/b/c/d'; + const pylintrc = 'pylintrc'; + const dotPylintrc = '.pylintrc'; + + let fileSystem: TypeMoq.IMock<IFileSystem>; + let platformService: TypeMoq.IMock<IPlatformService>; + let workspace: TypeMoq.IMock<IWorkspaceService>; + let execService: TypeMoq.IMock<IPythonToolExecutionService>; + let config: TypeMoq.IMock<IConfigurationService>; + let workspaceConfig: TypeMoq.IMock<WorkspaceConfiguration>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let serviceContainer: ServiceContainer; + + setup(() => { + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + fileSystem + .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) + .returns((a, b) => a === b); + + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + platformService.setup((x) => x.isWindows).returns(() => false); + + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + execService = TypeMoq.Mock.ofType<IPythonToolExecutionService>(); + + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + serviceContainer = new ServiceContainer(cont); + + serviceManager.addSingletonInstance<IFileSystem>(IFileSystem, fileSystem.object); + serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspace.object); + serviceManager.addSingletonInstance<IPythonToolExecutionService>( + IPythonToolExecutionService, + execService.object + ); + serviceManager.addSingletonInstance<IPlatformService>(IPlatformService, platformService.object); + serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService + ); + serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( + IInterpreterAutoSeletionProxyService, + MockAutoSelectionService + ); + + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + pythonSettings.setup((p) => p.languageServer).returns(() => LanguageServerType.Jedi); + + config = TypeMoq.Mock.ofType<IConfigurationService>(); + config.setup((c) => c.getSettings()).returns(() => pythonSettings.object); + + workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspace.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + + serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, config.object); + const linterManager = new LinterManager(serviceContainer, workspace.object); + serviceManager.addSingletonInstance<ILinterManager>(ILinterManager, linterManager); + const installer = TypeMoq.Mock.ofType<IInstaller>(); + serviceManager.addSingletonInstance<IInstaller>(IInstaller, installer.object); + }); + + test('pylintrc in the file folder', async () => { + fileSystem.setup((x) => x.fileExists(path.join(basePath, pylintrc))).returns(() => Promise.resolve(true)); + let result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); + expect(result).to.be.equal(true, `'${pylintrc}' not detected in the file folder.`); + + fileSystem.setup((x) => x.fileExists(path.join(basePath, dotPylintrc))).returns(() => Promise.resolve(true)); + result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); + expect(result).to.be.equal(true, `'${dotPylintrc}' not detected in the file folder.`); + }); + test('pylintrc up the module tree', async () => { + const module1 = path.join('/user/a/b/c/d', '__init__.py'); + const module2 = path.join('/user/a/b/c', '__init__.py'); + const module3 = path.join('/user/a/b', '__init__.py'); + const rc = path.join('/user/a/b/c', pylintrc); + + fileSystem.setup((x) => x.fileExists(module1)).returns(() => Promise.resolve(true)); + fileSystem.setup((x) => x.fileExists(module2)).returns(() => Promise.resolve(true)); + fileSystem.setup((x) => x.fileExists(module3)).returns(() => Promise.resolve(true)); + fileSystem.setup((x) => x.fileExists(rc)).returns(() => Promise.resolve(true)); + + const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); + expect(result).to.be.equal(true, `'${pylintrc}' not detected in the module tree.`); + }); + test('.pylintrc up the module tree', async () => { + // Don't use path.join since it will use / on Travis and Mac + const module1 = path.join('/user/a/b/c/d', '__init__.py'); + const module2 = path.join('/user/a/b/c', '__init__.py'); + const module3 = path.join('/user/a/b', '__init__.py'); + const rc = path.join('/user/a/b/c', pylintrc); + + fileSystem.setup((x) => x.fileExists(module1)).returns(() => Promise.resolve(true)); + fileSystem.setup((x) => x.fileExists(module2)).returns(() => Promise.resolve(true)); + fileSystem.setup((x) => x.fileExists(module3)).returns(() => Promise.resolve(true)); + fileSystem.setup((x) => x.fileExists(rc)).returns(() => Promise.resolve(true)); + + const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); + expect(result).to.be.equal(true, `'${dotPylintrc}' not detected in the module tree.`); + }); + test('.pylintrc up the ~ folder', async () => { + const home = os.homedir(); + const rc = path.join(home, dotPylintrc); + fileSystem.setup((x) => x.fileExists(rc)).returns(() => Promise.resolve(true)); + + const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); + expect(result).to.be.equal(true, `'${dotPylintrc}' not detected in the ~ folder.`); + }); + test('pylintrc up the ~/.config folder', async () => { + const home = os.homedir(); + const rc = path.join(home, '.config', pylintrc); + fileSystem.setup((x) => x.fileExists(rc)).returns(() => Promise.resolve(true)); + + const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); + expect(result).to.be.equal(true, `'${pylintrc}' not detected in the ~/.config folder.`); + }); + test('pylintrc in the /etc folder', async () => { + const rc = path.join('/etc', pylintrc); + fileSystem.setup((x) => x.fileExists(rc)).returns(() => Promise.resolve(true)); + + const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); + expect(result).to.be.equal(true, `'${pylintrc}' not detected in the /etc folder.`); + }); + test('pylintrc between file and workspace root', async () => { + const root = '/user/a'; + const midFolder = '/user/a/b'; + fileSystem.setup((x) => x.fileExists(path.join(midFolder, pylintrc))).returns(() => Promise.resolve(true)); + + const result = await Pylint.hasConfigurationFileInWorkspace(fileSystem.object, basePath, root); + expect(result).to.be.equal(true, `'${pylintrc}' not detected in the workspace tree.`); + }); + + test('minArgs - pylintrc between the file and the workspace root', async () => { + fileSystem.setup((x) => x.fileExists(path.join('/user/a/b', pylintrc))).returns(() => Promise.resolve(true)); + + await testPylintArguments('/user/a/b/c', '/user/a', false); + }); + + test('minArgs - no pylintrc between the file and the workspace root', async () => { + await testPylintArguments('/user/a/b/c', '/user/a', true); + }); + + test('minArgs - pylintrc next to the file', async () => { + const fileFolder = '/user/a/b/c'; + fileSystem.setup((x) => x.fileExists(path.join(fileFolder, pylintrc))).returns(() => Promise.resolve(true)); + + await testPylintArguments(fileFolder, '/user/a', false); + }); + + test('minArgs - pylintrc at the workspace root', async () => { + const root = '/user/a'; + fileSystem.setup((x) => x.fileExists(path.join(root, pylintrc))).returns(() => Promise.resolve(true)); + + await testPylintArguments('/user/a/b/c', root, false); + }); + + async function testPylintArguments(fileFolder: string, wsRoot: string, expectedMinArgs: boolean): Promise<void> { + const outputChannel = TypeMoq.Mock.ofType<OutputChannel>(); + const pylinter = new Pylint(outputChannel.object, serviceContainer); + + const document = TypeMoq.Mock.ofType<TextDocument>(); + document.setup((x) => x.uri).returns(() => Uri.file(path.join(fileFolder, 'test.py'))); + + const wsf = TypeMoq.Mock.ofType<WorkspaceFolder>(); + wsf.setup((x) => x.uri).returns(() => Uri.file(wsRoot)); + + workspace.setup((x) => x.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => wsf.object); + + let execInfo: ExecutionInfo | undefined; + execService + .setup((x) => x.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((e: ExecutionInfo, _b, _c) => { + execInfo = e; + }) + .returns(() => Promise.resolve({ stdout: '', stderr: '' })); + + const lintSettings = new MockLintingSettings(); + lintSettings.pylintUseMinimalCheckers = true; + // tslint:disable-next-line:no-string-literal + lintSettings['pylintPath'] = 'pyLint'; + // tslint:disable-next-line:no-string-literal + lintSettings['pylintEnabled'] = true; + + const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + settings.setup((x) => x.linting).returns(() => lintSettings); + settings.setup((x) => x.languageServer).returns(() => LanguageServerType.Jedi); + config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + + await pylinter.lint(document.object, new CancellationTokenSource().token); + expect( + execInfo!.args.findIndex((x) => x.indexOf('--disable=all') >= 0), + 'Minimal args passed to pylint while pylintrc exists.' + ).to.be.eq(expectedMinArgs ? 0 : -1); + } + test('Negative column numbers should be treated 0', async () => { + const fileFolder = '/user/a/b/c'; + const outputChannel = TypeMoq.Mock.ofType<OutputChannel>(); + const pylinter = new Pylint(outputChannel.object, serviceContainer); + + const document = TypeMoq.Mock.ofType<TextDocument>(); + document.setup((x) => x.uri).returns(() => Uri.file(path.join(fileFolder, 'test.py'))); + + const wsf = TypeMoq.Mock.ofType<WorkspaceFolder>(); + wsf.setup((x) => x.uri).returns(() => Uri.file(fileFolder)); + + workspace.setup((x) => x.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => wsf.object); + + const linterOutput = [ + 'No config file found, using default configuration', + '************* Module test', + '1,1,convention,C0111:Missing module docstring', + '3,-1,error,E1305:Too many arguments for format string' + ].join(os.EOL); + execService + .setup((x) => x.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: linterOutput, stderr: '' })); + + const lintSettings = new MockLintingSettings(); + lintSettings.pylintUseMinimalCheckers = false; + lintSettings.maxNumberOfProblems = 1000; + lintSettings.pylintPath = 'pyLint'; + lintSettings.pylintEnabled = true; + lintSettings.pylintCategorySeverity = { + convention: DiagnosticSeverity.Hint, + error: DiagnosticSeverity.Error, + fatal: DiagnosticSeverity.Error, + refactor: DiagnosticSeverity.Hint, + warning: DiagnosticSeverity.Warning + }; + + const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + settings.setup((x) => x.linting).returns(() => lintSettings); + settings.setup((x) => x.languageServer).returns(() => LanguageServerType.Jedi); + config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + + const messages = await pylinter.lint(document.object, new CancellationTokenSource().token); + expect(messages).to.be.lengthOf(2); + expect(messages[0].column).to.be.equal(1); + expect(messages[1].column).to.be.equal(0); + }); +}); diff --git a/src/test/linters/pylint.unit.test.ts b/src/test/linters/pylint.unit.test.ts new file mode 100644 index 000000000000..b09bc1feecb3 --- /dev/null +++ b/src/test/linters/pylint.unit.test.ts @@ -0,0 +1,537 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-any +// tslint:disable: max-classes-per-file + +import { assert, expect } from 'chai'; +import * as os from 'os'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { mock } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import * as vscode from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { PlatformService } from '../../client/common/platform/platformService'; +import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; +import { IConfigurationService, IOutputChannel } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { Pylint } from '../../client/linters/pylint'; +import { ILinterInfo, ILinterManager, ILintMessage, LintMessageSeverity } from '../../client/linters/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Pylint - Function hasConfigurationFile()', () => { + const folder = path.join('user', 'a', 'b', 'c', 'd'); + const oldValueOfPYLINTRC = process.env.PYLINTRC; + const pylintrcFiles = ['pylintrc', '.pylintrc']; + const pylintrc = 'pylintrc'; + const dotPylintrc = '.pylintrc'; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let platformService: TypeMoq.IMock<IPlatformService>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + fileSystem + .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) + .returns((a, b) => a === b); + + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + }); + + teardown(() => { + if (oldValueOfPYLINTRC === undefined) { + delete process.env.PYLINTRC; + } else { + process.env.PYLINTRC = oldValueOfPYLINTRC; + } + }); + + pylintrcFiles.forEach((pylintrcFile) => { + test(`If ${pylintrcFile} exists in the current working directory, return true`, async () => { + fileSystem + .setup((x) => x.fileExists(path.join(folder, pylintrc))) + .returns(() => Promise.resolve(pylintrc === pylintrcFile)); + fileSystem + .setup((x) => x.fileExists(path.join(folder, dotPylintrc))) + .returns(() => Promise.resolve(dotPylintrc === pylintrcFile)); + const hasConfig = await Pylint.hasConfigurationFile(fileSystem.object, folder, platformService.object); + expect(hasConfig).to.equal(true, 'Should return true'); + }); + + test(`If the current working directory is in a Python module, Pylint searches up the hierarchy of Python modules until it finds a ${pylintrcFile} file. And if ${pylintrcFile} exists, return true`, async () => { + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a', 'b', 'c', 'd'), '__init__.py'))) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a', 'b', 'c', 'd'), pylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.atLeastOnce()); + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a', 'b', 'c', 'd'), dotPylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.atLeastOnce()); + + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a', 'b', 'c'), '__init__.py'))) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a', 'b', 'c'), pylintrc))) + .returns(() => Promise.resolve(pylintrc === pylintrcFile)); + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a', 'b', 'c'), dotPylintrc))) + .returns(() => Promise.resolve(dotPylintrc === pylintrcFile)); + const hasConfig = await Pylint.hasConfigurationFile(fileSystem.object, folder, platformService.object); + expect(hasConfig).to.equal(true, 'Should return true'); + fileSystem.verifyAll(); + platformService.verifyAll(); + }); + + test(`If ${pylintrcFile} exists in the home directory, return true`, async () => { + const home = os.homedir(); + fileSystem.setup((x) => x.fileExists(path.join(folder, pylintrc))).returns(() => Promise.resolve(false)); + fileSystem.setup((x) => x.fileExists(path.join(folder, dotPylintrc))).returns(() => Promise.resolve(false)); + fileSystem + .setup((x) => x.fileExists(path.join(folder, '__init__.py'))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(home, '.config', pylintrc))) + .returns(() => Promise.resolve(pylintrc === pylintrcFile)); + fileSystem + .setup((x) => x.fileExists(path.join(home, dotPylintrc))) + .returns(() => Promise.resolve(dotPylintrc === pylintrcFile)); + const hasConfig = await Pylint.hasConfigurationFile(fileSystem.object, folder, platformService.object); + expect(hasConfig).to.equal(true, 'Should return true'); + fileSystem.verifyAll(); + platformService.verifyAll(); + }); + }); + + test('If /etc/pylintrc exists in non-Windows platform, return true', async function () { + if (new PlatformService().isWindows) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + const home = os.homedir(); + fileSystem + .setup((x) => x.fileExists(path.join(folder, pylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(folder, dotPylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(folder, '__init__.py'))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(home, '.config', pylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(home, dotPylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + platformService.setup((x) => x.isWindows).returns(() => false); + fileSystem.setup((x) => x.fileExists(path.join('/etc', pylintrc))).returns(() => Promise.resolve(true)); + const hasConfig = await Pylint.hasConfigurationFile(fileSystem.object, folder, platformService.object); + expect(hasConfig).to.equal(true, 'Should return true'); + fileSystem.verifyAll(); + platformService.verifyAll(); + }); + + test('If none of the pylintrc configuration files exist anywhere, return false', async () => { + const home = os.homedir(); + fileSystem + .setup((x) => x.fileExists(path.join(folder, pylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(folder, dotPylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(folder, '__init__.py'))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(home, '.config', pylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(home, dotPylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + platformService + .setup((x) => x.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join('/etc', pylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + const hasConfig = await Pylint.hasConfigurationFile(fileSystem.object, folder, platformService.object); + expect(hasConfig).to.equal(false, 'Should return false'); + fileSystem.verifyAll(); + platformService.verifyAll(); + }); + + test('If process.env.PYLINTRC contains the path to pylintrc, return true', async () => { + process.env.PYLINTRC = path.join('path', 'to', 'pylintrc'); + const hasConfig = await Pylint.hasConfigurationFile(fileSystem.object, folder, platformService.object); + expect(hasConfig).to.equal(true, 'Should return true'); + }); +}); + +// tslint:disable-next-line:max-func-body-length +suite('Pylint - Function hasConfigurationFileInWorkspace()', () => { + const pylintrc = 'pylintrc'; + const dotPylintrc = '.pylintrc'; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let platformService: TypeMoq.IMock<IPlatformService>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + fileSystem + .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) + .returns((a, b) => a === b); + + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + }); + + test('If none of the pylintrc files exist up to the workspace root, return false', async () => { + const folder = path.join('user', 'a', 'b', 'c'); + const root = path.join('user', 'a'); + + const rootPathItems = ['user', 'a']; + const folderPathItems = ['b', 'c']; // full folder path will be prefixed by root path + let rootPath = ''; + rootPathItems.forEach((item) => { + rootPath = path.join(rootPath, item); + fileSystem + .setup((x) => x.fileExists(path.join(rootPath, pylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.never()); + fileSystem + .setup((x) => x.fileExists(path.join(rootPath, dotPylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.never()); + }); + let relativeFolderPath = ''; + folderPathItems.forEach((item) => { + relativeFolderPath = path.join(relativeFolderPath, item); + const absoluteFolderPath = path.join(rootPath, relativeFolderPath); + fileSystem + .setup((x) => x.fileExists(path.join(absoluteFolderPath, pylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(absoluteFolderPath, dotPylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + }); + + const hasConfig = await Pylint.hasConfigurationFileInWorkspace(fileSystem.object, folder, root); + expect(hasConfig).to.equal(false, 'Should return false'); + fileSystem.verifyAll(); + }); + + [pylintrc, dotPylintrc].forEach((pylintrcFile) => { + test(`If ${pylintrcFile} exists while traversing up to the workspace root, return true`, async () => { + const folder = path.join('user', 'a', 'b', 'c'); + const root = path.join('user', 'a'); + + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a', 'b', 'c'), pylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a', 'b', 'c'), dotPylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a', 'b'), pylintrc))) + .returns(() => Promise.resolve(pylintrc === pylintrcFile)); + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a', 'b'), dotPylintrc))) + .returns(() => Promise.resolve(dotPylintrc === pylintrcFile)); + + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a'), pylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.never()); + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user', 'a'), dotPylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.never()); + + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user'), dotPylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.never()); + fileSystem + .setup((x) => x.fileExists(path.join(path.join('user'), dotPylintrc))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.never()); + + const hasConfig = await Pylint.hasConfigurationFileInWorkspace(fileSystem.object, folder, root); + expect(hasConfig).to.equal(true, 'Should return true'); + fileSystem.verifyAll(); + }); + }); +}); + +// tslint:disable-next-line:max-func-body-length +suite('Pylint - Function runLinter()', () => { + let fileSystem: TypeMoq.IMock<IFileSystem>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let configService: TypeMoq.IMock<IConfigurationService>; + let manager: TypeMoq.IMock<ILinterManager>; + let output: TypeMoq.IMock<IOutputChannel>; + let _info: TypeMoq.IMock<ILinterInfo>; + let platformService: TypeMoq.IMock<IPlatformService>; + let run: sinon.SinonStub<any>; + let parseMessagesSeverity: sinon.SinonStub<any>; + const minArgs = [ + '--disable=all', + '--enable=F' + + ',unreachable,duplicate-key,unnecessary-semicolon' + + ',global-variable-not-assigned,unused-variable' + + ',unused-wildcard-import,binary-op-exception' + + ',bad-format-string,anomalous-backslash-in-string' + + ',bad-open-mode' + + ',E0001,E0011,E0012,E0100,E0101,E0102,E0103,E0104,E0105,E0107' + + ',E0108,E0110,E0111,E0112,E0113,E0114,E0115,E0116,E0117,E0118' + + ',E0202,E0203,E0211,E0213,E0236,E0237,E0238,E0239,E0240,E0241' + + ',E0301,E0302,E0303,E0401,E0402,E0601,E0602,E0603,E0604,E0611' + + ',E0632,E0633,E0701,E0702,E0703,E0704,E0710,E0711,E0712,E1003' + + ',E1101,E1102,E1111,E1120,E1121,E1123,E1124,E1125,E1126,E1127' + + ',E1128,E1129,E1130,E1131,E1132,E1133,E1134,E1135,E1136,E1137' + + ',E1138,E1139,E1200,E1201,E1205,E1206,E1300,E1301,E1302,E1303' + + ',E1304,E1305,E1306,E1310,E1700,E1701' + ]; + const doc = { + uri: vscode.Uri.file('path/to/doc') + }; + const args = [ + "--msg-template='{line},{column},{category},{symbol}:{msg}'", + '--reports=n', + '--output-format=text', + doc.uri.fsPath + ]; + const original_hasConfigurationFileInWorkspace = Pylint.hasConfigurationFileInWorkspace; + const original_hasConfigurationFile = Pylint.hasConfigurationFile; + + class PylintTest extends Pylint { + public async run( + _args: string[], + _document: vscode.TextDocument, + _cancellation: vscode.CancellationToken, + _regEx: string + ): Promise<ILintMessage[]> { + return []; + } + public parseMessagesSeverity(_error: string, _categorySeverity: any): LintMessageSeverity { + return 'Severity' as any; + } + public get info(): ILinterInfo { + return _info.object; + } + // tslint:disable-next-line: no-unnecessary-override + public async runLinter( + document: vscode.TextDocument, + cancellation: vscode.CancellationToken + ): Promise<ILintMessage[]> { + return super.runLinter(document, cancellation); + } + public getWorkspaceRootPath(_document: vscode.TextDocument): string { + return 'path/to/workspaceRoot'; + } + } + + setup(() => { + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + _info = TypeMoq.Mock.ofType<ILinterInfo>(); + output = TypeMoq.Mock.ofType<IOutputChannel>(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + manager = TypeMoq.Mock.ofType<ILinterManager>(); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ILinterManager))).returns(() => manager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + fileSystem + .setup((x) => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) + .returns((a, b) => a === b); + manager.setup((m) => m.getLinterInfo(TypeMoq.It.isAny())).returns(() => undefined as any); + }); + + teardown(() => { + Pylint.hasConfigurationFileInWorkspace = original_hasConfigurationFileInWorkspace; + Pylint.hasConfigurationFile = original_hasConfigurationFile; + sinon.restore(); + }); + + test('Use minimal checkers if a) setting to use minimal checkers is true, b) there are no custom arguments and c) there is no pylintrc file next to the file or at the workspace root and above', async () => { + const settings = { + linting: { + pylintUseMinimalCheckers: true + } + }; + configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as any); + _info.setup((info) => info.linterArgs(doc.uri)).returns(() => []); + Pylint.hasConfigurationFileInWorkspace = () => Promise.resolve(false); + Pylint.hasConfigurationFile = () => Promise.resolve(false); + run = sinon.stub(PylintTest.prototype, 'run'); + run.callsFake(() => Promise.resolve([])); + parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); + parseMessagesSeverity.callsFake(() => 'Severity'); + const pylint = new PylintTest(output.object, serviceContainer.object); + await pylint.runLinter(doc as any, mock(vscode.CancellationTokenSource).token); + assert.deepEqual(run.args[0][0], minArgs.concat(args)); + assert.ok(parseMessagesSeverity.notCalled); + assert.ok(run.calledOnce); + }); + + test('Do not use minimal checkers if setting to use minimal checkers is false', async () => { + const settings = { + linting: { + pylintUseMinimalCheckers: false + } + }; + configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as any); + _info.setup((info) => info.linterArgs(doc.uri)).returns(() => []); + Pylint.hasConfigurationFileInWorkspace = () => Promise.resolve(false); + Pylint.hasConfigurationFile = () => Promise.resolve(false); + run = sinon.stub(PylintTest.prototype, 'run'); + run.callsFake(() => Promise.resolve([])); + parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); + parseMessagesSeverity.callsFake(() => 'Severity'); + const pylint = new PylintTest(output.object, serviceContainer.object); + await pylint.runLinter(doc as any, mock(vscode.CancellationTokenSource).token); + assert.deepEqual(run.args[0][0], args); + assert.ok(parseMessagesSeverity.notCalled); + assert.ok(run.calledOnce); + }); + + test('Do not use minimal checkers if there are custom arguments', async () => { + const settings = { + linting: { + pylintUseMinimalCheckers: true + } + }; + configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as any); + _info.setup((info) => info.linterArgs(doc.uri)).returns(() => ['customArg1', 'customArg2']); + Pylint.hasConfigurationFileInWorkspace = () => Promise.resolve(false); + Pylint.hasConfigurationFile = () => Promise.resolve(false); + run = sinon.stub(PylintTest.prototype, 'run'); + run.callsFake(() => Promise.resolve([])); + parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); + parseMessagesSeverity.callsFake(() => 'Severity'); + const pylint = new PylintTest(output.object, serviceContainer.object); + await pylint.runLinter(doc as any, mock(vscode.CancellationTokenSource).token); + assert.deepEqual(run.args[0][0], args); + assert.ok(parseMessagesSeverity.notCalled); + assert.ok(run.calledOnce); + }); + + test('Do not use minimal checkers if there is a pylintrc file in the current working directory or when traversing the workspace up to its root (hasConfigurationFileInWorkspace() returns true)', async () => { + const settings = { + linting: { + pylintUseMinimalCheckers: true + } + }; + configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as any); + _info.setup((info) => info.linterArgs(doc.uri)).returns(() => []); + Pylint.hasConfigurationFileInWorkspace = () => Promise.resolve(true); // This implies method hasConfigurationFileInWorkspace() returns true + Pylint.hasConfigurationFile = () => Promise.resolve(false); + run = sinon.stub(PylintTest.prototype, 'run'); + run.callsFake(() => Promise.resolve([])); + parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); + parseMessagesSeverity.callsFake(() => 'Severity'); + const pylint = new PylintTest(output.object, serviceContainer.object); + await pylint.runLinter(doc as any, mock(vscode.CancellationTokenSource).token); + assert.deepEqual(run.args[0][0], args); + assert.ok(parseMessagesSeverity.notCalled); + assert.ok(run.calledOnce); + }); + + test('Do not use minimal checkers if a pylintrc file exists in the process, in the current working directory or up in the hierarchy tree (hasConfigurationFile() returns true)', async () => { + const settings = { + linting: { + pylintUseMinimalCheckers: true + } + }; + configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as any); + _info.setup((info) => info.linterArgs(doc.uri)).returns(() => []); + Pylint.hasConfigurationFileInWorkspace = () => Promise.resolve(false); + Pylint.hasConfigurationFile = () => Promise.resolve(true); // This implies method hasConfigurationFile() returns true + run = sinon.stub(PylintTest.prototype, 'run'); + run.callsFake(() => Promise.resolve([])); + parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); + parseMessagesSeverity.callsFake(() => 'Severity'); + const pylint = new PylintTest(output.object, serviceContainer.object); + await pylint.runLinter(doc as any, mock(vscode.CancellationTokenSource).token); + assert.deepEqual(run.args[0][0], args); + assert.ok(parseMessagesSeverity.notCalled); + assert.ok(run.calledOnce); + }); + + test('Message returned by runLinter() is as expected', async () => { + const message = [ + { + type: 'messageType' + } + ]; + const expectedResult = [ + { + type: 'messageType', + severity: 'LintMessageSeverity' + } + ]; + const settings = { + linting: { + pylintUseMinimalCheckers: true + } + }; + configService.setup((c) => c.getSettings(doc.uri)).returns(() => settings as any); + _info.setup((info) => info.linterArgs(doc.uri)).returns(() => []); + Pylint.hasConfigurationFileInWorkspace = () => Promise.resolve(false); + Pylint.hasConfigurationFile = () => Promise.resolve(false); + run = sinon.stub(PylintTest.prototype, 'run'); + run.callsFake(() => Promise.resolve(message as any)); + parseMessagesSeverity = sinon.stub(PylintTest.prototype, 'parseMessagesSeverity'); + parseMessagesSeverity.callsFake(() => 'LintMessageSeverity'); + const pylint = new PylintTest(output.object, serviceContainer.object); + const result = await pylint.runLinter(doc as any, mock(vscode.CancellationTokenSource).token); + assert.deepEqual(result, expectedResult as any); + assert.ok(parseMessagesSeverity.calledOnce); + assert.ok(run.calledOnce); + }); +}); diff --git a/src/test/linters/serviceRegistry.unit.test.ts b/src/test/linters/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..4f63e9fae940 --- /dev/null +++ b/src/test/linters/serviceRegistry.unit.test.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionActivationService } from '../../client/activation/types'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceManager } from '../../client/ioc/types'; +import { AvailableLinterActivator } from '../../client/linters/linterAvailability'; +import { LinterManager } from '../../client/linters/linterManager'; +import { LintingEngine } from '../../client/linters/lintingEngine'; +import { registerTypes } from '../../client/linters/serviceRegistry'; +import { IAvailableLinterActivator, ILinterManager, ILintingEngine } from '../../client/linters/types'; +import { LinterProvider } from '../../client/providers/linterProvider'; + +suite('Linters Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify(serviceManager.addSingleton<ILintingEngine>(ILintingEngine, LintingEngine)).once(); + verify(serviceManager.addSingleton<ILinterManager>(ILinterManager, LinterManager)).once(); + verify( + serviceManager.add<IAvailableLinterActivator>(IAvailableLinterActivator, AvailableLinterActivator) + ).once(); + verify( + serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, LinterProvider) + ).once(); + }); +}); diff --git a/src/test/markdown/restTextConverter.test.ts b/src/test/markdown/restTextConverter.test.ts new file mode 100644 index 000000000000..20cd05e55a93 --- /dev/null +++ b/src/test/markdown/restTextConverter.test.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { RestTextConverter } from '../../client/common/markdown/restTextConverter'; +import { compareFiles } from '../textUtils'; + +const srcPythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'markdown'); + +async function testConversion(fileName: string): Promise<void> { + const cvt = new RestTextConverter(); + const file = path.join(srcPythoFilesPath, fileName); + const source = await fs.readFile(`${file}.pydoc`, 'utf8'); + const actual = cvt.toMarkdown(source); + const expected = await fs.readFile(`${file}.md`, 'utf8'); + compareFiles(expected, actual); +} + +// tslint:disable-next-line:max-func-body-length +suite('Hover - RestTextConverter', () => { + test('scipy', async () => testConversion('scipy')); + test('scipy.spatial', async () => testConversion('scipy.spatial')); + test('scipy.spatial.distance', async () => testConversion('scipy.spatial.distance')); + test('anydbm', async () => testConversion('anydbm')); + test('aifc', async () => testConversion('aifc')); + test('astroid', async () => testConversion('astroid')); +}); diff --git a/src/test/mockClasses.ts b/src/test/mockClasses.ts new file mode 100644 index 000000000000..cca3df5787bc --- /dev/null +++ b/src/test/mockClasses.ts @@ -0,0 +1,89 @@ +import * as vscode from 'vscode'; +import { + Flake8CategorySeverity, + ILintingSettings, + IMypyCategorySeverity, + IPycodestyleCategorySeverity, + IPylintCategorySeverity +} from '../client/common/types'; + +export class MockOutputChannel implements vscode.OutputChannel { + public name: string; + public output: string; + public isShown!: boolean; + constructor(name: string) { + this.name = name; + this.output = ''; + } + public append(value: string) { + this.output += value; + } + public appendLine(value: string) { + this.append(value); + this.append('\n'); + } + // tslint:disable-next-line:no-empty + public clear() {} + public show(preservceFocus?: boolean): void; + public show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; + // tslint:disable-next-line:no-any + public show(_x?: any, _y?: any): void { + this.isShown = true; + } + public hide() { + this.isShown = false; + } + // tslint:disable-next-line:no-empty + public dispose() {} +} + +export class MockStatusBarItem implements vscode.StatusBarItem { + public alignment!: vscode.StatusBarAlignment; + public priority!: number; + public text!: string; + public tooltip!: string; + public color!: string; + public command!: string; + // tslint:disable-next-line:no-empty + public show(): void {} + // tslint:disable-next-line:no-empty + public hide(): void {} + // tslint:disable-next-line:no-empty + public dispose(): void {} +} + +export class MockLintingSettings implements ILintingSettings { + public enabled!: boolean; + public ignorePatterns!: string[]; + public prospectorEnabled!: boolean; + public prospectorArgs!: string[]; + public pylintEnabled!: boolean; + public pylintArgs!: string[]; + public pycodestyleEnabled!: boolean; + public pycodestyleArgs!: string[]; + public pylamaEnabled!: boolean; + public pylamaArgs!: string[]; + public flake8Enabled!: boolean; + public flake8Args!: string[]; + public pydocstyleEnabled!: boolean; + public pydocstyleArgs!: string[]; + public lintOnSave!: boolean; + public maxNumberOfProblems!: number; + public pylintCategorySeverity!: IPylintCategorySeverity; + public pycodestyleCategorySeverity!: IPycodestyleCategorySeverity; + public flake8CategorySeverity!: Flake8CategorySeverity; + public mypyCategorySeverity!: IMypyCategorySeverity; + public prospectorPath!: string; + public pylintPath!: string; + public pycodestylePath!: string; + public pylamaPath!: string; + public flake8Path!: string; + public pydocstylePath!: string; + public mypyEnabled!: boolean; + public mypyArgs!: string[]; + public mypyPath!: string; + public banditEnabled!: boolean; + public banditArgs!: string[]; + public banditPath!: string; + public pylintUseMinimalCheckers!: boolean; +} diff --git a/src/test/mocks/autoSelector.ts b/src/test/mocks/autoSelector.ts new file mode 100644 index 000000000000..e8a4b0fc76a4 --- /dev/null +++ b/src/test/mocks/autoSelector.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { Event, EventEmitter } from 'vscode'; +import { Resource } from '../../client/common/types'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService +} from '../../client/interpreter/autoSelection/types'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; + +@injectable() +export class MockAutoSelectionService + implements IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService { + public async setWorkspaceInterpreter(_resource: Resource, _interpreter: PythonEnvironment): Promise<void> { + return Promise.resolve(); + } + public async setGlobalInterpreter(_interpreter: PythonEnvironment): Promise<void> { + return; + } + get onDidChangeAutoSelectedInterpreter(): Event<void> { + return new EventEmitter<void>().event; + } + public autoSelectInterpreter(_resource: Resource): Promise<void> { + return Promise.resolve(); + } + public getAutoSelectedInterpreter(_resource: Resource): PythonEnvironment | undefined { + return; + } + public registerInstance(_instance: IInterpreterAutoSeletionProxyService): void { + return; + } +} diff --git a/src/test/mocks/mementos.ts b/src/test/mocks/mementos.ts new file mode 100644 index 000000000000..77fde4c8e505 --- /dev/null +++ b/src/test/mocks/mementos.ts @@ -0,0 +1,26 @@ +import { injectable } from 'inversify'; +import { Memento } from 'vscode'; + +@injectable() +export class MockMemento implements Memento { + // Note: This has to be called _value so that it matches + // what VS code has for a memento. We use this to eliminate a bad bug + // with writing too much data to global storage. See bug https://github.com/microsoft/vscode-python/issues/9159 + private _value: Record<string, {}> = {}; + // @ts-ignore + // tslint:disable-next-line:no-any + public get(key: any, defaultValue?: any); + public get<T>(key: string, defaultValue?: T): T { + const exists = this._value.hasOwnProperty(key); + // tslint:disable-next-line:no-any + return exists ? this._value[key] : (defaultValue! as any); + } + // tslint:disable-next-line:no-any + public update(key: string, value: any): Thenable<void> { + this._value[key] = value; + return Promise.resolve(); + } + public clear() { + this._value = {}; + } +} diff --git a/src/test/mocks/moduleInstaller.ts b/src/test/mocks/moduleInstaller.ts new file mode 100644 index 000000000000..68e3444d14a5 --- /dev/null +++ b/src/test/mocks/moduleInstaller.ts @@ -0,0 +1,23 @@ +import { EventEmitter } from 'events'; +import { Uri } from 'vscode'; +import { IModuleInstaller } from '../../client/common/installer/types'; + +export class MockModuleInstaller extends EventEmitter implements IModuleInstaller { + constructor(public readonly displayName: string, private supported: boolean) { + super(); + } + + public get name(): string { + return 'mock'; + } + + public get priority(): number { + return 0; + } + public async installModule(name: string, _resource?: Uri): Promise<void> { + this.emit('installModule', name); + } + public async isSupported(_resource?: Uri): Promise<boolean> { + return this.supported; + } +} diff --git a/src/test/mocks/proc.ts b/src/test/mocks/proc.ts new file mode 100644 index 000000000000..59f069cd4d33 --- /dev/null +++ b/src/test/mocks/proc.ts @@ -0,0 +1,91 @@ +import 'rxjs/add/observable/of'; + +import { EventEmitter } from 'events'; +import { Observable } from 'rxjs/Observable'; + +import { + ExecutionResult, + IProcessService, + ObservableExecutionResult, + Output, + ShellOptions, + SpawnOptions +} from '../../client/common/process/types'; +import { noop } from '../core'; + +type ExecObservableCallback = (result: Observable<Output<string>> | Output<string>) => void; +type ExecCallback = (result: ExecutionResult<string>) => void; + +export const IOriginalProcessService = Symbol('IProcessService'); + +export class MockProcessService extends EventEmitter implements IProcessService { + constructor(private procService: IProcessService) { + super(); + } + public onExecObservable( + handler: (file: string, args: string[], options: SpawnOptions, callback: ExecObservableCallback) => void + ) { + this.on('execObservable', handler); + } + public execObservable(file: string, args: string[], options: SpawnOptions = {}): ObservableExecutionResult<string> { + let value: Observable<Output<string>> | Output<string> | undefined; + let valueReturned = false; + this.emit('execObservable', file, args, options, (result: Observable<Output<string>> | Output<string>) => { + value = result; + valueReturned = true; + }); + + if (valueReturned) { + const output = value as Output<string>; + if (['stderr', 'stdout'].some((source) => source === output.source)) { + return { + // tslint:disable-next-line:no-any + proc: {} as any, + out: Observable.of(output), + dispose: () => { + noop(); + } + }; + } else { + return { + // tslint:disable-next-line:no-any + proc: {} as any, + out: value as Observable<Output<string>>, + dispose: () => { + noop(); + } + }; + } + } else { + return this.procService.execObservable(file, args, options); + } + } + public onExec(handler: (file: string, args: string[], options: SpawnOptions, callback: ExecCallback) => void) { + this.on('exec', handler); + } + public async exec(file: string, args: string[], options: SpawnOptions = {}): Promise<ExecutionResult<string>> { + let value: ExecutionResult<string> | undefined; + let valueReturned = false; + this.emit('exec', file, args, options, (result: ExecutionResult<string>) => { + value = result; + valueReturned = true; + }); + + return valueReturned ? value! : this.procService.exec(file, args, options); + } + + public async shellExec(command: string, options?: ShellOptions): Promise<ExecutionResult<string>> { + let value: ExecutionResult<string> | undefined; + let valueReturned = false; + this.emit('shellExec', command, options, (result: ExecutionResult<string>) => { + value = result; + valueReturned = true; + }); + + return valueReturned ? value! : this.procService.shellExec(command, options); + } + + public dispose() { + return; + } +} diff --git a/src/test/mocks/process.ts b/src/test/mocks/process.ts new file mode 100644 index 000000000000..1d92693d8923 --- /dev/null +++ b/src/test/mocks/process.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { injectable } from 'inversify'; +import * as TypeMoq from 'typemoq'; +import { ICurrentProcess } from '../../client/common/types'; +import { EnvironmentVariables } from '../../client/common/variables/types'; + +@injectable() +export class MockProcess implements ICurrentProcess { + constructor(public env: EnvironmentVariables = { ...process.env }) {} + public on(_event: string | symbol, _listener: Function): this { + return this; + } + public get argv(): string[] { + return []; + } + public get stdout(): NodeJS.WriteStream { + return TypeMoq.Mock.ofType<NodeJS.WriteStream>().object; + } + public get stdin(): NodeJS.ReadStream { + return TypeMoq.Mock.ofType<NodeJS.ReadStream>().object; + } + + public get execPath(): string { + return ''; + } +} diff --git a/src/test/mocks/vsc/README.md b/src/test/mocks/vsc/README.md new file mode 100644 index 000000000000..2803528e6276 --- /dev/null +++ b/src/test/mocks/vsc/README.md @@ -0,0 +1,7 @@ +# This folder contains classes exposed by VS Code required in running the unit tests. + +- These classes are only used when running unit tests that are not hosted by VS Code. +- So even if these classes were buggy, it doesn't matter, running the tests under VS Code host will ensure the right classes are available. +- The purpose of these classes are to avoid having to use VS Code as the hosting environment for the tests, making it faster to run the tests and not have to rely on VS Code host to run the tests. +- Everything in here must either be within a namespace prefixed with `vscMock` or exported types must be prefixed with `vscMock`. + This is to prevent developers from accidentally importing them into their Code. Even if they did, the extension would fail to load and tests would fail. diff --git a/src/test/mocks/vsc/arrays.ts b/src/test/mocks/vsc/arrays.ts new file mode 100644 index 000000000000..9291745a066f --- /dev/null +++ b/src/test/mocks/vsc/arrays.ts @@ -0,0 +1,415 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +// tslint:disable:all + +export namespace vscMockArrays { + /** + * Returns the last element of an array. + * @param array The array. + * @param n Which element from the end (default is zero). + */ + export function tail<T>(array: T[], n: number = 0): T { + return array[array.length - (1 + n)]; + } + + export function equals<T>(one: T[], other: T[], itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { + if (one.length !== other.length) { + return false; + } + + for (let i = 0, len = one.length; i < len; i++) { + if (!itemEquals(one[i], other[i])) { + return false; + } + } + + return true; + } + + export function binarySearch<T>(array: T[], key: T, comparator: (op1: T, op2: T) => number): number { + let low = 0, + high = array.length - 1; + + while (low <= high) { + let mid = ((low + high) / 2) | 0; + let comp = comparator(array[mid], key); + if (comp < 0) { + low = mid + 1; + } else if (comp > 0) { + high = mid - 1; + } else { + return mid; + } + } + return -(low + 1); + } + + /** + * Takes a sorted array and a function p. The array is sorted in such a way that all elements where p(x) is false + * are located before all elements where p(x) is true. + * @returns the least x for which p(x) is true or array.length if no element fullfills the given function. + */ + export function findFirst<T>(array: T[], p: (x: T) => boolean): number { + let low = 0, + high = array.length; + if (high === 0) { + return 0; // no children + } + while (low < high) { + let mid = Math.floor((low + high) / 2); + if (p(array[mid])) { + high = mid; + } else { + low = mid + 1; + } + } + return low; + } + + /** + * Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort` + * so only use this when actually needing stable sort. + */ + export function mergeSort<T>(data: T[], compare: (a: T, b: T) => number): T[] { + _divideAndMerge(data, compare); + return data; + } + + function _divideAndMerge<T>(data: T[], compare: (a: T, b: T) => number): void { + if (data.length <= 1) { + // sorted + return; + } + const p = (data.length / 2) | 0; + const left = data.slice(0, p); + const right = data.slice(p); + + _divideAndMerge(left, compare); + _divideAndMerge(right, compare); + + let leftIdx = 0; + let rightIdx = 0; + let i = 0; + while (leftIdx < left.length && rightIdx < right.length) { + let ret = compare(left[leftIdx], right[rightIdx]); + if (ret <= 0) { + // smaller_equal -> take left to preserve order + data[i++] = left[leftIdx++]; + } else { + // greater -> take right + data[i++] = right[rightIdx++]; + } + } + while (leftIdx < left.length) { + data[i++] = left[leftIdx++]; + } + while (rightIdx < right.length) { + data[i++] = right[rightIdx++]; + } + } + + export function groupBy<T>(data: T[], compare: (a: T, b: T) => number): T[][] { + const result: T[][] = []; + let currentGroup: T[]; + for (const element of mergeSort(data.slice(0), compare)) { + // @ts-ignore + if (!currentGroup || compare(currentGroup[0], element) !== 0) { + currentGroup = [element]; + result.push(currentGroup); + } else { + currentGroup.push(element); + } + } + return result; + } + + type IMutableSplice<T> = Array<T> & + any & { + deleteCount: number; + }; + type ISplice<T> = Array<T> & any; + + /** + * Diffs two *sorted* arrays and computes the splices which apply the diff. + */ + export function sortedDiff<T>(before: T[], after: T[], compare: (a: T, b: T) => number): ISplice<T>[] { + const result: IMutableSplice<T>[] = []; + + function pushSplice(start: number, deleteCount: number, toInsert: T[]): void { + if (deleteCount === 0 && toInsert.length === 0) { + return; + } + + const latest = result[result.length - 1]; + + if (latest && latest.start + latest.deleteCount === start) { + latest.deleteCount += deleteCount; + latest.toInsert.push(...toInsert); + } else { + result.push({ start, deleteCount, toInsert }); + } + } + + let beforeIdx = 0; + let afterIdx = 0; + + while (true) { + if (beforeIdx === before.length) { + pushSplice(beforeIdx, 0, after.slice(afterIdx)); + break; + } + if (afterIdx === after.length) { + pushSplice(beforeIdx, before.length - beforeIdx, []); + break; + } + + const beforeElement = before[beforeIdx]; + const afterElement = after[afterIdx]; + const n = compare(beforeElement, afterElement); + if (n === 0) { + // equal + beforeIdx += 1; + afterIdx += 1; + } else if (n < 0) { + // beforeElement is smaller -> before element removed + pushSplice(beforeIdx, 1, []); + beforeIdx += 1; + } else if (n > 0) { + // beforeElement is greater -> after element added + pushSplice(beforeIdx, 0, [afterElement]); + afterIdx += 1; + } + } + + return result; + } + + /** + * Takes two *sorted* arrays and computes their delta (removed, added elements). + * Finishes in `Math.min(before.length, after.length)` steps. + * @param before + * @param after + * @param compare + */ + export function delta<T>(before: T[], after: T[], compare: (a: T, b: T) => number): { removed: T[]; added: T[] } { + const splices = sortedDiff(before, after, compare); + const removed: T[] = []; + const added: T[] = []; + + for (const splice of splices) { + removed.push(...before.slice(splice.start, splice.start + splice.deleteCount)); + added.push(...splice.toInsert); + } + + return { removed, added }; + } + + /** + * Returns the top N elements from the array. + * + * Faster than sorting the entire array when the array is a lot larger than N. + * + * @param array The unsorted array. + * @param compare A sort function for the elements. + * @param n The number of elements to return. + * @return The first n elemnts from array when sorted with compare. + */ + export function top<T>(array: T[], compare: (a: T, b: T) => number, n: number): T[] { + if (n === 0) { + return []; + } + const result = array.slice(0, n).sort(compare); + topStep(array, compare, result, n, array.length); + return result; + } + + function topStep<T>(array: T[], compare: (a: T, b: T) => number, result: T[], i: number, m: number): void { + for (const n = result.length; i < m; i++) { + const element = array[i]; + if (compare(element, result[n - 1]) < 0) { + result.pop(); + const j = findFirst(result, (e) => compare(element, e) < 0); + result.splice(j, 0, element); + } + } + } + + /** + * @returns a new array with all undefined or null values removed. The original array is not modified at all. + */ + export function coalesce<T>(array: T[]): T[] { + if (!array) { + return array; + } + + return array.filter((e) => !!e); + } + + /** + * Moves the element in the array for the provided positions. + */ + export function move(array: any[], from: number, to: number): void { + array.splice(to, 0, array.splice(from, 1)[0]); + } + + /** + * @returns {{false}} if the provided object is an array + * and not empty. + */ + export function isFalsyOrEmpty(obj: any): boolean { + return !Array.isArray(obj) || (<Array<any>>obj).length === 0; + } + + /** + * Removes duplicates from the given array. The optional keyFn allows to specify + * how elements are checked for equalness by returning a unique string for each. + */ + export function distinct<T>(array: T[], keyFn?: (t: T) => string): T[] { + if (!keyFn) { + return array.filter((element, position) => { + return array.indexOf(element) === position; + }); + } + + const seen: Record<string, boolean> = Object.create(null); + return array.filter((elem) => { + const key = keyFn(elem); + if (seen[key]) { + return false; + } + + seen[key] = true; + + return true; + }); + } + + export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean { + const seen: Record<string, boolean> = Object.create(null); + + return (element) => { + const key = keyFn(element); + + if (seen[key]) { + return false; + } + + seen[key] = true; + return true; + }; + } + + export function firstIndex<T>(array: T[], fn: (item: T) => boolean): number { + for (let i = 0; i < array.length; i++) { + const element = array[i]; + + if (fn(element)) { + return i; + } + } + + return -1; + } + // @ts-ignore + export function first<T>(array: T[], fn: (item: T) => boolean, notFoundValue: T = null): T { + const index = firstIndex(array, fn); + return index < 0 ? notFoundValue : array[index]; + } + + export function commonPrefixLength<T>( + one: T[], + other: T[], + equals: (a: T, b: T) => boolean = (a, b) => a === b + ): number { + let result = 0; + + for (let i = 0, len = Math.min(one.length, other.length); i < len && equals(one[i], other[i]); i++) { + result++; + } + + return result; + } + + export function flatten<T>(arr: T[][]): T[] { + // @ts-ignore + return [].concat(...arr); + } + + export function range(to: number): number[]; + export function range(from: number, to: number): number[]; + export function range(arg: number, to?: number): number[] { + let from = typeof to === 'number' ? arg : 0; + + if (typeof to === 'number') { + from = arg; + } else { + from = 0; + to = arg; + } + + const result: number[] = []; + + if (from <= to) { + for (let i = from; i < to; i++) { + result.push(i); + } + } else { + for (let i = from; i > to; i--) { + result.push(i); + } + } + + return result; + } + + export function fill<T>(num: number, valueFn: () => T, arr: T[] = []): T[] { + for (let i = 0; i < num; i++) { + arr[i] = valueFn(); + } + + return arr; + } + + export function index<T>(array: T[], indexer: (t: T) => string): Record<string, T>; + export function index<T, R>(array: T[], indexer: (t: T) => string, merger?: (t: T, r: R) => R): Record<string, R>; + export function index<T, R>( + array: T[], + indexer: (t: T) => string, + merger: (t: T, r: R) => R = (t) => t as any + ): Record<string, R> { + return array.reduce((r, t) => { + const key = indexer(t); + r[key] = merger(t, r[key]); + return r; + }, Object.create(null)); + } + + /** + * Inserts an element into an array. Returns a function which, when + * called, will remove that element from the array. + */ + export function insert<T>(array: T[], element: T): () => void { + array.push(element); + + return () => { + const index = array.indexOf(element); + if (index > -1) { + array.splice(index, 1); + } + }; + } + + /** + * Insert `insertArr` inside `target` at `insertIndex`. + * Please don't touch unless you understand https://jsperf.com/inserting-an-array-within-an-array + */ + export function arrayInsert<T>(target: T[], insertIndex: number, insertArr: T[]): T[] { + const before = target.slice(0, insertIndex); + const after = target.slice(insertIndex); + return before.concat(insertArr, after); + } +} diff --git a/src/test/mocks/vsc/charCode.ts b/src/test/mocks/vsc/charCode.ts new file mode 100644 index 000000000000..b13bc31658d1 --- /dev/null +++ b/src/test/mocks/vsc/charCode.ts @@ -0,0 +1,426 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* tslint:disable */ + +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030a, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030b, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030c, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030d, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030e, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030f, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031a, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031b, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031c, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031d, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031e, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031f, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032a, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032b, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032c, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032d, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032e, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032f, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033a, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033b, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033c, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033d, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033e, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033f, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034a, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034b, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034c, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034d, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034e, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034f, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035a, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035b, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035c, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035d, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035e, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035f, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036a, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036b, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036c, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036d, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036e, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036f, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR_2028 = 8232, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + U_CIRCUMFLEX = 0x005e, // U+005E CIRCUMFLEX + U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS + U_MACRON = 0x00af, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00b8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02c2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02c3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02c4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02c5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02d2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02d3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02d4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02d5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02d6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02d7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02d8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02d9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02da, // U+02DA RING ABOVE + U_OGONEK = 0x02db, // U+02DB OGONEK + U_SMALL_TILDE = 0x02dc, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02dd, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02de, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02df, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02e5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02e6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02e7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02e8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02e9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02ea, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02eb, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ed, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02ef, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02f0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02f1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02f2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02f3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02f4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02f5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02f6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02f7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02f8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02f9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02fa, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02fb, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02fc, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02fd, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02fe, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02ff, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1fbd, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1fbf, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1fc0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1fc1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1fcd, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1fce, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1fcf, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1fdd, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1fde, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1fdf, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1fed, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1fee, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1fef, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1ffd, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1ffe, // U+1FFE GREEK DASIA + + U_OVERLINE = 0x203e, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279 +} diff --git a/src/test/mocks/vsc/extHostedTypes.ts b/src/test/mocks/vsc/extHostedTypes.ts new file mode 100644 index 000000000000..35d5f7b82132 --- /dev/null +++ b/src/test/mocks/vsc/extHostedTypes.ts @@ -0,0 +1,2145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +// import * as crypto from 'crypto'; + +// tslint:disable:all + +import { relative } from 'path'; +import * as vscode from 'vscode'; +import { vscMockHtmlContent } from './htmlContent'; +import { vscMockStrings } from './strings'; +import { vscUri } from './uri'; +import { generateUuid } from './uuid'; + +export namespace vscMockExtHostedTypes { + export enum CellKind { + Markdown = 1, + Code = 2 + } + + export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3 + } + export enum NotebookCellRunState { + Running = 1, + Idle = 2, + Success = 3, + Error = 4 + } + + export interface IRelativePattern { + base: string; + pattern: string; + pathToRelative(from: string, to: string): string; + } + + // tslint:disable:all + const illegalArgument = (msg = 'Illegal Argument') => new Error(msg); + + export class Disposable { + static from(...disposables: { dispose(): any }[]): Disposable { + return new Disposable(function () { + if (disposables) { + for (let disposable of disposables) { + if (disposable && typeof disposable.dispose === 'function') { + disposable.dispose(); + } + } + // @ts-ignore + disposables = undefined; + } + }); + } + + private _callOnDispose: Function; + + constructor(callOnDispose: Function) { + this._callOnDispose = callOnDispose; + } + + dispose(): any { + if (typeof this._callOnDispose === 'function') { + this._callOnDispose(); + // @ts-ignore + this._callOnDispose = undefined; + } + } + } + + export class Position { + static Min(...positions: Position[]): Position { + let result = positions.pop(); + for (let p of positions) { + // @ts-ignore + if (p.isBefore(result)) { + result = p; + } + } + // @ts-ignore + return result; + } + + static Max(...positions: Position[]): Position { + let result = positions.pop(); + for (let p of positions) { + // @ts-ignore + if (p.isAfter(result)) { + result = p; + } + } + // @ts-ignore + return result; + } + + static isPosition(other: any): other is Position { + if (!other) { + return false; + } + if (other instanceof Position) { + return true; + } + let { line, character } = <Position>other; + if (typeof line === 'number' && typeof character === 'number') { + return true; + } + return false; + } + + private _line: number; + private _character: number; + + get line(): number { + return this._line; + } + + get character(): number { + return this._character; + } + + constructor(line: number, character: number) { + if (line < 0) { + throw illegalArgument('line must be non-negative'); + } + if (character < 0) { + throw illegalArgument('character must be non-negative'); + } + this._line = line; + this._character = character; + } + + isBefore(other: Position): boolean { + if (this._line < other._line) { + return true; + } + if (other._line < this._line) { + return false; + } + return this._character < other._character; + } + + isBeforeOrEqual(other: Position): boolean { + if (this._line < other._line) { + return true; + } + if (other._line < this._line) { + return false; + } + return this._character <= other._character; + } + + isAfter(other: Position): boolean { + return !this.isBeforeOrEqual(other); + } + + isAfterOrEqual(other: Position): boolean { + return !this.isBefore(other); + } + + isEqual(other: Position): boolean { + return this._line === other._line && this._character === other._character; + } + + compareTo(other: Position): number { + if (this._line < other._line) { + return -1; + } else if (this._line > other.line) { + return 1; + } else { + // equal line + if (this._character < other._character) { + return -1; + } else if (this._character > other._character) { + return 1; + } else { + // equal line and character + return 0; + } + } + } + + translate(change: { lineDelta?: number; characterDelta?: number }): Position; + // @ts-ignore + translate(lineDelta?: number, characterDelta?: number): Position; + translate( + lineDeltaOrChange: number | { lineDelta?: number; characterDelta?: number }, + characterDelta: number = 0 + ): Position { + if (lineDeltaOrChange === null || characterDelta === null) { + throw illegalArgument(); + } + + let lineDelta: number; + if (typeof lineDeltaOrChange === 'undefined') { + lineDelta = 0; + } else if (typeof lineDeltaOrChange === 'number') { + lineDelta = lineDeltaOrChange; + } else { + lineDelta = typeof lineDeltaOrChange.lineDelta === 'number' ? lineDeltaOrChange.lineDelta : 0; + characterDelta = + typeof lineDeltaOrChange.characterDelta === 'number' ? lineDeltaOrChange.characterDelta : 0; + } + + if (lineDelta === 0 && characterDelta === 0) { + return this; + } + return new Position(this.line + lineDelta, this.character + characterDelta); + } + + with(change: { line?: number; character?: number }): Position; + // @ts-ignore + with(line?: number, character?: number): Position; + with( + lineOrChange: number | { line?: number; character?: number }, + character: number = this.character + ): Position { + if (lineOrChange === null || character === null) { + throw illegalArgument(); + } + + let line: number; + if (typeof lineOrChange === 'undefined') { + line = this.line; + } else if (typeof lineOrChange === 'number') { + line = lineOrChange; + } else { + line = typeof lineOrChange.line === 'number' ? lineOrChange.line : this.line; + character = typeof lineOrChange.character === 'number' ? lineOrChange.character : this.character; + } + + if (line === this.line && character === this.character) { + return this; + } + return new Position(line, character); + } + + toJSON(): any { + return { line: this.line, character: this.character }; + } + } + + export class Range { + static isRange(thing: any): thing is vscode.Range { + if (thing instanceof Range) { + return true; + } + if (!thing) { + return false; + } + return Position.isPosition((<Range>thing).start) && Position.isPosition(<Range>thing.end); + } + + protected _start: Position; + protected _end: Position; + + get start(): Position { + return this._start; + } + + get end(): Position { + return this._end; + } + + constructor(start: Position, end: Position); + constructor(startLine: number, startColumn: number, endLine: number, endColumn: number); + constructor( + startLineOrStart: number | Position, + startColumnOrEnd: number | Position, + endLine?: number, + endColumn?: number + ) { + let start: Position; + let end: Position; + + if ( + typeof startLineOrStart === 'number' && + typeof startColumnOrEnd === 'number' && + typeof endLine === 'number' && + typeof endColumn === 'number' + ) { + start = new Position(startLineOrStart, startColumnOrEnd); + end = new Position(endLine, endColumn); + } else if (startLineOrStart instanceof Position && startColumnOrEnd instanceof Position) { + start = startLineOrStart; + end = startColumnOrEnd; + } + // @ts-ignore + if (!start || !end) { + throw new Error('Invalid arguments'); + } + + if (start.isBefore(end)) { + this._start = start; + this._end = end; + } else { + this._start = end; + this._end = start; + } + } + + contains(positionOrRange: Position | Range): boolean { + if (positionOrRange instanceof Range) { + return this.contains(positionOrRange._start) && this.contains(positionOrRange._end); + } else if (positionOrRange instanceof Position) { + if (positionOrRange.isBefore(this._start)) { + return false; + } + if (this._end.isBefore(positionOrRange)) { + return false; + } + return true; + } + return false; + } + + isEqual(other: Range): boolean { + return this._start.isEqual(other._start) && this._end.isEqual(other._end); + } + + intersection(other: Range): Range { + let start = Position.Max(other.start, this._start); + let end = Position.Min(other.end, this._end); + if (start.isAfter(end)) { + // this happens when there is no overlap: + // |-----| + // |----| + // @ts-ignore + return undefined; + } + return new Range(start, end); + } + + union(other: Range): Range { + if (this.contains(other)) { + return this; + } else if (other.contains(this)) { + return other; + } + let start = Position.Min(other.start, this._start); + let end = Position.Max(other.end, this.end); + return new Range(start, end); + } + + get isEmpty(): boolean { + return this._start.isEqual(this._end); + } + + get isSingleLine(): boolean { + return this._start.line === this._end.line; + } + + with(change: { start?: Position; end?: Position }): Range; + // @ts-ignore + with(start?: Position, end?: Position): Range; + with(startOrChange: Position | { start?: Position; end?: Position }, end: Position = this.end): Range { + if (startOrChange === null || end === null) { + throw illegalArgument(); + } + + let start: Position; + if (!startOrChange) { + start = this.start; + } else if (Position.isPosition(startOrChange)) { + start = startOrChange; + } else { + start = startOrChange.start || this.start; + end = startOrChange.end || this.end; + } + + if (start.isEqual(this._start) && end.isEqual(this.end)) { + return this; + } + return new Range(start, end); + } + + toJSON(): any { + return [this.start, this.end]; + } + } + + export class Selection extends Range { + static isSelection(thing: any): thing is Selection { + if (thing instanceof Selection) { + return true; + } + if (!thing) { + return false; + } + return ( + Range.isRange(thing) && + Position.isPosition((<Selection>thing).anchor) && + Position.isPosition((<Selection>thing).active) && + typeof (<Selection>thing).isReversed === 'boolean' + ); + } + + private _anchor: Position; + + public get anchor(): Position { + return this._anchor; + } + + private _active: Position; + + public get active(): Position { + return this._active; + } + + constructor(anchor: Position, active: Position); + constructor(anchorLine: number, anchorColumn: number, activeLine: number, activeColumn: number); + constructor( + anchorLineOrAnchor: number | Position, + anchorColumnOrActive: number | Position, + activeLine?: number, + activeColumn?: number + ) { + let anchor: Position; + let active: Position; + + if ( + typeof anchorLineOrAnchor === 'number' && + typeof anchorColumnOrActive === 'number' && + typeof activeLine === 'number' && + typeof activeColumn === 'number' + ) { + anchor = new Position(anchorLineOrAnchor, anchorColumnOrActive); + active = new Position(activeLine, activeColumn); + } else if (anchorLineOrAnchor instanceof Position && anchorColumnOrActive instanceof Position) { + anchor = anchorLineOrAnchor; + active = anchorColumnOrActive; + } + // @ts-ignore + if (!anchor || !active) { + throw new Error('Invalid arguments'); + } + + super(anchor, active); + + this._anchor = anchor; + this._active = active; + } + + get isReversed(): boolean { + return this._anchor === this._end; + } + + toJSON() { + return { + start: this.start, + end: this.end, + active: this.active, + anchor: this.anchor + }; + } + } + + export enum EndOfLine { + LF = 1, + CRLF = 2 + } + + export class TextEdit { + static isTextEdit(thing: any): thing is TextEdit { + if (thing instanceof TextEdit) { + return true; + } + if (!thing) { + return false; + } + return Range.isRange(<TextEdit>thing) && typeof (<TextEdit>thing).newText === 'string'; + } + + static replace(range: Range, newText: string): TextEdit { + return new TextEdit(range, newText); + } + + static insert(position: Position, newText: string): TextEdit { + return TextEdit.replace(new Range(position, position), newText); + } + + static delete(range: Range): TextEdit { + return TextEdit.replace(range, ''); + } + + static setEndOfLine(eol: EndOfLine): TextEdit { + // @ts-ignore + let ret = new TextEdit(undefined, undefined); + ret.newEol = eol; + return ret; + } + // @ts-ignore + protected _range: Range; + // @ts-ignore + protected _newText: string; + // @ts-ignore + protected _newEol: EndOfLine; + + get range(): Range { + return this._range; + } + + set range(value: Range) { + if (value && !Range.isRange(value)) { + throw illegalArgument('range'); + } + this._range = value; + } + + get newText(): string { + return this._newText || ''; + } + + set newText(value: string) { + if (value && typeof value !== 'string') { + throw illegalArgument('newText'); + } + this._newText = value; + } + + get newEol(): EndOfLine { + return this._newEol; + } + + set newEol(value: EndOfLine) { + if (value && typeof value !== 'number') { + throw illegalArgument('newEol'); + } + this._newEol = value; + } + + constructor(range: Range, newText: string) { + this.range = range; + this.newText = newText; + } + + toJSON(): any { + return { + range: this.range, + newText: this.newText, + newEol: this._newEol + }; + } + } + + export class WorkspaceEdit implements vscode.WorkspaceEdit { + replaceCells( + _uri: vscode.Uri, + _start: number, + _end: number, + _cells: vscode.NotebookCellData[], + _metadata?: vscode.WorkspaceEditEntryMetadata + ): void { + // Noop. + } + + replaceCellOutput( + _uri: vscode.Uri, + _index: number, + _outputs: vscode.CellOutput[], + _metadata?: vscode.WorkspaceEditEntryMetadata + ): void { + // Noop. + } + + replaceCellMetadata( + _uri: vscode.Uri, + _index: number, + _cellMetadata: vscode.NotebookCellMetadata, + _metadata?: vscode.WorkspaceEditEntryMetadata + ): void { + // Noop. + } + + private _seqPool: number = 0; + + private _resourceEdits: { seq: number; from: vscUri.URI; to: vscUri.URI }[] = []; + private _textEdits = new Map<string, { seq: number; uri: vscUri.URI; edits: TextEdit[] }>(); + + // createResource(uri: vscode.Uri): void { + // this.renameResource(undefined, uri); + // } + + // deleteResource(uri: vscode.Uri): void { + // this.renameResource(uri, undefined); + // } + + // renameResource(from: vscode.Uri, to: vscode.Uri): void { + // this._resourceEdits.push({ seq: this._seqPool++, from, to }); + // } + + // resourceEdits(): [vscode.Uri, vscode.Uri][] { + // return this._resourceEdits.map(({ from, to }) => (<[vscode.Uri, vscode.Uri]>[from, to])); + // } + + createFile(_uri: vscode.Uri, _options?: { overwrite?: boolean; ignoreIfExists?: boolean }): void { + throw new Error('Method not implemented.'); + } + deleteFile(_uri: vscode.Uri, _options?: { recursive?: boolean; ignoreIfNotExists?: boolean }): void { + throw new Error('Method not implemented.'); + } + renameFile( + _oldUri: vscode.Uri, + _newUri: vscode.Uri, + _options?: { overwrite?: boolean; ignoreIfExists?: boolean } + ): void { + throw new Error('Method not implemented.'); + } + + replace(uri: vscUri.URI, range: Range, newText: string): void { + let edit = new TextEdit(range, newText); + let array = this.get(uri); + if (array) { + array.push(edit); + } else { + array = [edit]; + } + this.set(uri, array); + } + + insert(resource: vscUri.URI, position: Position, newText: string): void { + this.replace(resource, new Range(position, position), newText); + } + + delete(resource: vscUri.URI, range: Range): void { + this.replace(resource, range, ''); + } + + has(uri: vscUri.URI): boolean { + return this._textEdits.has(uri.toString()); + } + + set(uri: vscUri.URI, edits: TextEdit[]): void { + let data = this._textEdits.get(uri.toString()); + if (!data) { + data = { seq: this._seqPool++, uri, edits: [] }; + this._textEdits.set(uri.toString(), data); + } + if (!edits) { + // @ts-ignore + data.edits = undefined; + } else { + data.edits = edits.slice(0); + } + } + + get(uri: vscUri.URI): TextEdit[] { + if (!this._textEdits.has(uri.toString())) { + // @ts-ignore + return undefined; + } + // @ts-ignore + const { edits } = this._textEdits.get(uri.toString()); + return edits ? edits.slice() : undefined; + } + + entries(): [vscUri.URI, TextEdit[]][] { + const res: [vscUri.URI, TextEdit[]][] = []; + this._textEdits.forEach((value) => res.push([value.uri, value.edits])); + return res.slice(); + } + + allEntries(): ([vscUri.URI, TextEdit[]] | [vscUri.URI, vscUri.URI])[] { + return this.entries(); + // // use the 'seq' the we have assigned when inserting + // // the operation and use that order in the resulting + // // array + // const res: ([vscUri.URI, TextEdit[]] | [vscUri.URI,vscUri.URI])[] = []; + // this._textEdits.forEach(value => { + // const { seq, uri, edits } = value; + // res[seq] = [uri, edits]; + // }); + // this._resourceEdits.forEach(value => { + // const { seq, from, to } = value; + // res[seq] = [from, to]; + // }); + // return res; + } + + get size(): number { + return this._textEdits.size + this._resourceEdits.length; + } + + toJSON(): any { + return this.entries(); + } + } + + export class SnippetString { + static isSnippetString(thing: any): thing is SnippetString { + if (thing instanceof SnippetString) { + return true; + } + if (!thing) { + return false; + } + return typeof (<SnippetString>thing).value === 'string'; + } + + private static _escape(value: string): string { + return value.replace(/\$|}|\\/g, '\\$&'); + } + + private _tabstop: number = 1; + + value: string; + + constructor(value?: string) { + this.value = value || ''; + } + + appendText(string: string): SnippetString { + this.value += SnippetString._escape(string); + return this; + } + + appendTabstop(number: number = this._tabstop++): SnippetString { + this.value += '$'; + this.value += number; + return this; + } + + appendPlaceholder( + value: string | ((snippet: SnippetString) => any), + number: number = this._tabstop++ + ): SnippetString { + if (typeof value === 'function') { + const nested = new SnippetString(); + nested._tabstop = this._tabstop; + value(nested); + this._tabstop = nested._tabstop; + value = nested.value; + } else { + value = SnippetString._escape(value); + } + + this.value += '${'; + this.value += number; + this.value += ':'; + this.value += value; + this.value += '}'; + + return this; + } + + appendChoice(values: string[], number: number = this._tabstop++): SnippetString { + const value = SnippetString._escape(values.toString()); + + this.value += '${'; + this.value += number; + this.value += '|'; + this.value += value; + this.value += '|}'; + + return this; + } + + appendVariable(name: string, defaultValue?: string | ((snippet: SnippetString) => any)): SnippetString { + if (typeof defaultValue === 'function') { + const nested = new SnippetString(); + nested._tabstop = this._tabstop; + defaultValue(nested); + this._tabstop = nested._tabstop; + defaultValue = nested.value; + } else if (typeof defaultValue === 'string') { + defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); + } + + this.value += '${'; + this.value += name; + if (defaultValue) { + this.value += ':'; + this.value += defaultValue; + } + this.value += '}'; + + return this; + } + } + + export enum DiagnosticTag { + Unnecessary = 1 + } + + export enum DiagnosticSeverity { + Hint = 3, + Information = 2, + Warning = 1, + Error = 0 + } + + export class Location { + static isLocation(thing: any): thing is Location { + if (thing instanceof Location) { + return true; + } + if (!thing) { + return false; + } + return Range.isRange((<Location>thing).range) && vscUri.URI.isUri((<Location>thing).uri); + } + + uri: vscUri.URI; + // @ts-ignore + range: Range; + + constructor(uri: vscUri.URI, rangeOrPosition: Range | Position) { + this.uri = uri; + + if (!rangeOrPosition) { + //that's OK + } else if (rangeOrPosition instanceof Range) { + this.range = rangeOrPosition; + } else if (rangeOrPosition instanceof Position) { + this.range = new Range(rangeOrPosition, rangeOrPosition); + } else { + throw new Error('Illegal argument'); + } + } + + toJSON(): any { + return { + uri: this.uri, + range: this.range + }; + } + } + + export class DiagnosticRelatedInformation { + static is(thing: any): thing is DiagnosticRelatedInformation { + if (!thing) { + return false; + } + return ( + typeof (<DiagnosticRelatedInformation>thing).message === 'string' && + (<DiagnosticRelatedInformation>thing).location && + Range.isRange((<DiagnosticRelatedInformation>thing).location.range) && + vscUri.URI.isUri((<DiagnosticRelatedInformation>thing).location.uri) + ); + } + + location: Location; + message: string; + + constructor(location: Location, message: string) { + this.location = location; + this.message = message; + } + } + + export class Diagnostic { + range: Range; + message: string; + // @ts-ignore + source: string; + // @ts-ignore + code: string | number; + severity: DiagnosticSeverity; + // @ts-ignore + relatedInformation: DiagnosticRelatedInformation[]; + customTags?: DiagnosticTag[]; + + constructor(range: Range, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error) { + this.range = range; + this.message = message; + this.severity = severity; + } + + toJSON(): any { + return { + severity: DiagnosticSeverity[this.severity], + message: this.message, + range: this.range, + source: this.source, + code: this.code + }; + } + } + + export class Hover { + public contents: vscode.MarkdownString[] | vscode.MarkedString[]; + public range: Range; + + constructor( + contents: vscode.MarkdownString | vscode.MarkedString | vscode.MarkdownString[] | vscode.MarkedString[], + range?: Range + ) { + if (!contents) { + throw new Error('Illegal argument, contents must be defined'); + } + if (Array.isArray(contents)) { + this.contents = <vscode.MarkdownString[] | vscode.MarkedString[]>contents; + } else if (vscMockHtmlContent.isMarkdownString(contents)) { + this.contents = [contents]; + } else { + this.contents = [contents]; + } + // @ts-ignore + this.range = range; + } + } + + export enum DocumentHighlightKind { + Text = 0, + Read = 1, + Write = 2 + } + + export class DocumentHighlight { + range: Range; + kind: DocumentHighlightKind; + + constructor(range: Range, kind: DocumentHighlightKind = DocumentHighlightKind.Text) { + this.range = range; + this.kind = kind; + } + + toJSON(): any { + return { + range: this.range, + kind: DocumentHighlightKind[this.kind] + }; + } + } + + export enum SymbolKind { + File = 0, + Module = 1, + Namespace = 2, + Package = 3, + Class = 4, + Method = 5, + Property = 6, + Field = 7, + Constructor = 8, + Enum = 9, + Interface = 10, + Function = 11, + Variable = 12, + Constant = 13, + String = 14, + Number = 15, + Boolean = 16, + Array = 17, + Object = 18, + Key = 19, + Null = 20, + EnumMember = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25 + } + + export class SymbolInformation { + name: string; + // @ts-ignore + location: Location; + kind: SymbolKind; + containerName: string; + + constructor(name: string, kind: SymbolKind, containerName: string, location: Location); + constructor(name: string, kind: SymbolKind, range: Range, uri?: vscUri.URI, containerName?: string); + constructor( + name: string, + kind: SymbolKind, + rangeOrContainer: string | Range, + locationOrUri?: Location | vscUri.URI, + containerName?: string + ) { + this.name = name; + this.kind = kind; + // @ts-ignore + this.containerName = containerName; + + if (typeof rangeOrContainer === 'string') { + this.containerName = rangeOrContainer; + } + + if (locationOrUri instanceof Location) { + this.location = locationOrUri; + } else if (rangeOrContainer instanceof Range) { + // @ts-ignore + this.location = new Location(locationOrUri, rangeOrContainer); + } + } + + toJSON(): any { + return { + name: this.name, + kind: SymbolKind[this.kind], + location: this.location, + containerName: this.containerName + }; + } + } + + export class SymbolInformation2 extends SymbolInformation { + definingRange: Range; + children: SymbolInformation2[]; + constructor(name: string, kind: SymbolKind, containerName: string, location: Location) { + super(name, kind, containerName, location); + + this.children = []; + this.definingRange = location.range; + } + } + + export enum CodeActionTrigger { + Automatic = 1, + Manual = 2 + } + + export class CodeAction { + title: string; + + command?: vscode.Command; + + edit?: WorkspaceEdit; + + dianostics?: Diagnostic[]; + + kind?: CodeActionKind; + + constructor(title: string, kind?: CodeActionKind) { + this.title = title; + this.kind = kind; + } + } + + export class CodeActionKind { + private static readonly sep = '.'; + + public static readonly Empty = new CodeActionKind(''); + public static readonly QuickFix = CodeActionKind.Empty.append('quickfix'); + public static readonly Refactor = CodeActionKind.Empty.append('refactor'); + public static readonly RefactorExtract = CodeActionKind.Refactor.append('extract'); + public static readonly RefactorInline = CodeActionKind.Refactor.append('inline'); + public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); + public static readonly Source = CodeActionKind.Empty.append('source'); + public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports'); + + constructor(public readonly value: string) {} + + public append(parts: string): CodeActionKind { + return new CodeActionKind(this.value ? this.value + CodeActionKind.sep + parts : parts); + } + + public contains(other: CodeActionKind): boolean { + return ( + this.value === other.value || vscMockStrings.startsWith(other.value, this.value + CodeActionKind.sep) + ); + } + } + + export class CodeLens { + range: Range; + + command: vscode.Command; + + constructor(range: Range, command?: vscode.Command) { + this.range = range; + // @ts-ignore + this.command = command; + } + + get isResolved(): boolean { + return !!this.command; + } + } + + export class MarkdownString { + value: string; + isTrusted?: boolean; + + constructor(value?: string) { + this.value = value || ''; + } + + appendText(value: string): MarkdownString { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); + return this; + } + + appendMarkdown(value: string): MarkdownString { + this.value += value; + return this; + } + + appendCodeblock(code: string, language: string = ''): MarkdownString { + this.value += '\n```'; + this.value += language; + this.value += '\n'; + this.value += code; + this.value += '\n```\n'; + return this; + } + } + + export class ParameterInformation { + label: string; + documentation?: string | MarkdownString; + + constructor(label: string, documentation?: string | MarkdownString) { + this.label = label; + this.documentation = documentation; + } + } + + export class SignatureInformation { + label: string; + documentation?: string | MarkdownString; + parameters: ParameterInformation[]; + + constructor(label: string, documentation?: string | MarkdownString) { + this.label = label; + this.documentation = documentation; + this.parameters = []; + } + } + + export class SignatureHelp { + signatures: SignatureInformation[]; + // @ts-ignore + activeSignature: number; + // @ts-ignore + activeParameter: number; + + constructor() { + this.signatures = []; + } + } + + export enum CompletionTriggerKind { + Invoke = 0, + TriggerCharacter = 1, + TriggerForIncompleteCompletions = 2 + } + + export interface CompletionContext { + triggerKind: CompletionTriggerKind; + triggerCharacter: string; + } + + export enum CompletionItemKind { + Text = 0, + Method = 1, + Function = 2, + Constructor = 3, + Field = 4, + Variable = 5, + Class = 6, + Interface = 7, + Module = 8, + Property = 9, + Unit = 10, + Value = 11, + Enum = 12, + Keyword = 13, + Snippet = 14, + Color = 15, + File = 16, + Reference = 17, + Folder = 18, + EnumMember = 19, + Constant = 20, + Struct = 21, + Event = 22, + Operator = 23, + TypeParameter = 24, + User = 25, + Issue = 26 + } + + export enum CompletionItemTag { + Deprecated = 1 + } + + export interface CompletionItemLabel { + name: string; + signature?: string; + qualifier?: string; + type?: string; + } + + export class CompletionItem { + // @ts-ignore + label: string; + label2?: CompletionItemLabel; + kind?: CompletionItemKind; + tags?: CompletionItemTag[]; + detail?: string; + documentation?: string | MarkdownString; + sortText?: string; + filterText?: string; + preselect?: boolean; + insertText?: string | SnippetString; + keepWhitespace?: boolean; + range?: Range; + commitCharacters?: string[]; + textEdit?: TextEdit; + additionalTextEdits?: TextEdit[]; + command?: vscode.Command; + + constructor(label: string, kind?: CompletionItemKind) { + this.label = label; + this.kind = kind; + } + + toJSON(): any { + return { + label: this.label, + label2: this.label2, + kind: this.kind && CompletionItemKind[this.kind], + detail: this.detail, + documentation: this.documentation, + sortText: this.sortText, + filterText: this.filterText, + preselect: this.preselect, + insertText: this.insertText, + textEdit: this.textEdit + }; + } + } + + export class CompletionList { + isIncomplete?: boolean; + + items: vscode.CompletionItem[]; + + constructor(items: vscode.CompletionItem[] = [], isIncomplete: boolean = false) { + this.items = items; + this.isIncomplete = isIncomplete; + } + } + + export enum ViewColumn { + Active = -1, + Beside = -2, + One = 1, + Two = 2, + Three = 3, + Four = 4, + Five = 5, + Six = 6, + Seven = 7, + Eight = 8, + Nine = 9 + } + + export enum StatusBarAlignment { + Left = 1, + Right = 2 + } + + export enum TextEditorLineNumbersStyle { + Off = 0, + On = 1, + Relative = 2 + } + + export enum TextDocumentSaveReason { + Manual = 1, + AfterDelay = 2, + FocusOut = 3 + } + + export enum TextEditorRevealType { + Default = 0, + InCenter = 1, + InCenterIfOutsideViewport = 2, + AtTop = 3 + } + + export enum TextEditorSelectionChangeKind { + Keyboard = 1, + Mouse = 2, + Command = 3 + } + + /** + * These values match very carefully the values of `TrackedRangeStickiness` + */ + export enum DecorationRangeBehavior { + /** + * TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + */ + OpenOpen = 0, + /** + * TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + */ + ClosedClosed = 1, + /** + * TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + */ + OpenClosed = 2, + /** + * TrackedRangeStickiness.GrowsOnlyWhenTypingAfter + */ + ClosedOpen = 3 + } + + export namespace TextEditorSelectionChangeKind { + export function fromValue(s: string) { + switch (s) { + case 'keyboard': + return TextEditorSelectionChangeKind.Keyboard; + case 'mouse': + return TextEditorSelectionChangeKind.Mouse; + case 'api': + return TextEditorSelectionChangeKind.Command; + } + return undefined; + } + } + + export class DocumentLink { + range: Range; + + target: vscUri.URI; + + constructor(range: Range, target: vscUri.URI) { + if (target && !(target instanceof vscUri.URI)) { + throw illegalArgument('target'); + } + if (!Range.isRange(range) || range.isEmpty) { + throw illegalArgument('range'); + } + this.range = range; + this.target = target; + } + } + + export class Color { + readonly red: number; + readonly green: number; + readonly blue: number; + readonly alpha: number; + + constructor(red: number, green: number, blue: number, alpha: number) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } + } + + export type IColorFormat = string | { opaque: string; transparent: string }; + + export class ColorInformation { + range: Range; + + color: Color; + + constructor(range: Range, color: Color) { + if (color && !(color instanceof Color)) { + throw illegalArgument('color'); + } + if (!Range.isRange(range) || range.isEmpty) { + throw illegalArgument('range'); + } + this.range = range; + this.color = color; + } + } + + export class ColorPresentation { + label: string; + textEdit?: TextEdit; + additionalTextEdits?: TextEdit[]; + + constructor(label: string) { + if (!label || typeof label !== 'string') { + throw illegalArgument('label'); + } + this.label = label; + } + } + + export enum ColorFormat { + RGB = 0, + HEX = 1, + HSL = 2 + } + + export enum SourceControlInputBoxValidationType { + Error = 0, + Warning = 1, + Information = 2 + } + + export enum TaskRevealKind { + Always = 1, + + Silent = 2, + + Never = 3 + } + + export enum TaskPanelKind { + Shared = 1, + + Dedicated = 2, + + New = 3 + } + + export class TaskGroup implements vscode.TaskGroup { + private _id: string; + + public static Clean: TaskGroup = new TaskGroup('clean', 'Clean'); + + public static Build: TaskGroup = new TaskGroup('build', 'Build'); + + public static Rebuild: TaskGroup = new TaskGroup('rebuild', 'Rebuild'); + + public static Test: TaskGroup = new TaskGroup('test', 'Test'); + + public static from(value: string) { + switch (value) { + case 'clean': + return TaskGroup.Clean; + case 'build': + return TaskGroup.Build; + case 'rebuild': + return TaskGroup.Rebuild; + case 'test': + return TaskGroup.Test; + default: + return undefined; + } + } + + constructor(id: string, _label: string) { + if (typeof id !== 'string') { + throw illegalArgument('name'); + } + if (typeof _label !== 'string') { + throw illegalArgument('name'); + } + this._id = id; + } + + get id(): string { + return this._id; + } + } + + export class ProcessExecution implements vscode.ProcessExecution { + private _process: string; + private _args: string[]; + // @ts-ignore + private _options: vscode.ProcessExecutionOptions; + + constructor(process: string, options?: vscode.ProcessExecutionOptions); + constructor(process: string, args: string[], options?: vscode.ProcessExecutionOptions); + constructor( + process: string, + varg1?: string[] | vscode.ProcessExecutionOptions, + varg2?: vscode.ProcessExecutionOptions + ) { + if (typeof process !== 'string') { + throw illegalArgument('process'); + } + this._process = process; + if (varg1 !== void 0) { + if (Array.isArray(varg1)) { + this._args = varg1; + // @ts-ignore + this._options = varg2; + } else { + this._options = varg1; + } + } + // @ts-ignore + if (this._args === void 0) { + this._args = []; + } + } + + get process(): string { + return this._process; + } + + set process(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('process'); + } + this._process = value; + } + + get args(): string[] { + return this._args; + } + + set args(value: string[]) { + if (!Array.isArray(value)) { + value = []; + } + this._args = value; + } + + get options(): vscode.ProcessExecutionOptions { + return this._options; + } + + set options(value: vscode.ProcessExecutionOptions) { + this._options = value; + } + + public computeId(): string { + // const hash = crypto.createHash('md5'); + // hash.update('process'); + // if (this._process !== void 0) { + // hash.update(this._process); + // } + // if (this._args && this._args.length > 0) { + // for (let arg of this._args) { + // hash.update(arg); + // } + // } + // return hash.digest('hex'); + throw new Error('Not supported'); + } + } + + export class ShellExecution implements vscode.ShellExecution { + // @ts-ignore + + private _commandLine: string; + // @ts-ignore + private _command: string | vscode.ShellQuotedString; + // @ts-ignore + private _args: (string | vscode.ShellQuotedString)[]; + private _options: vscode.ShellExecutionOptions; + + constructor(commandLine: string, options?: vscode.ShellExecutionOptions); + constructor( + command: string | vscode.ShellQuotedString, + args: (string | vscode.ShellQuotedString)[], + options?: vscode.ShellExecutionOptions + ); + constructor( + arg0: string | vscode.ShellQuotedString, + arg1?: vscode.ShellExecutionOptions | (string | vscode.ShellQuotedString)[], + arg2?: vscode.ShellExecutionOptions + ) { + if (Array.isArray(arg1)) { + if (!arg0) { + throw illegalArgument("command can't be undefined or null"); + } + if (typeof arg0 !== 'string' && typeof arg0.value !== 'string') { + throw illegalArgument('command'); + } + this._command = arg0; + this._args = arg1 as (string | vscode.ShellQuotedString)[]; + // @ts-ignore + this._options = arg2; + } else { + if (typeof arg0 !== 'string') { + throw illegalArgument('commandLine'); + } + this._commandLine = arg0; + // @ts-ignore + this._options = arg1; + } + } + + get commandLine(): string { + return this._commandLine; + } + + set commandLine(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('commandLine'); + } + this._commandLine = value; + } + + get command(): string | vscode.ShellQuotedString { + return this._command; + } + + set command(value: string | vscode.ShellQuotedString) { + if (typeof value !== 'string' && typeof value.value !== 'string') { + throw illegalArgument('command'); + } + this._command = value; + } + + get args(): (string | vscode.ShellQuotedString)[] { + return this._args; + } + + set args(value: (string | vscode.ShellQuotedString)[]) { + this._args = value || []; + } + + get options(): vscode.ShellExecutionOptions { + return this._options; + } + + set options(value: vscode.ShellExecutionOptions) { + this._options = value; + } + + public computeId(): string { + // const hash = crypto.createHash('md5'); + // hash.update('shell'); + // if (this._commandLine !== void 0) { + // hash.update(this._commandLine); + // } + // if (this._command !== void 0) { + // hash.update(typeof this._command === 'string' ? this._command : this._command.value); + // } + // if (this._args && this._args.length > 0) { + // for (let arg of this._args) { + // hash.update(typeof arg === 'string' ? arg : arg.value); + // } + // } + // return hash.digest('hex'); + throw new Error('Not spported'); + } + } + + export enum ShellQuoting { + Escape = 1, + Strong = 2, + Weak = 3 + } + + export enum TaskScope { + Global = 1, + Workspace = 2 + } + + export class Task implements vscode.Task { + private static ProcessType: string = 'process'; + private static ShellType: string = 'shell'; + private static EmptyType: string = '$empty'; + + private __id: string | undefined; + + private _definition!: vscode.TaskDefinition; + private _scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined; + private _name!: string; + private _execution: ProcessExecution | ShellExecution | undefined; + private _problemMatchers: string[]; + private _hasDefinedMatchers: boolean; + private _isBackground: boolean; + private _source!: string; + private _group: TaskGroup | undefined; + private _presentationOptions: vscode.TaskPresentationOptions; + private _runOptions: vscode.RunOptions; + + constructor( + definition: vscode.TaskDefinition, + name: string, + source: string, + execution?: ProcessExecution | ShellExecution, + problemMatchers?: string | string[] + ); + constructor( + definition: vscode.TaskDefinition, + scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, + name: string, + source: string, + execution?: ProcessExecution | ShellExecution, + problemMatchers?: string | string[] + ); + constructor( + definition: vscode.TaskDefinition, + arg2: string | (vscode.TaskScope.Global | vscode.TaskScope.Workspace) | vscode.WorkspaceFolder, + arg3: any, + arg4?: any, + arg5?: any, + arg6?: any + ) { + this.definition = definition; + let problemMatchers: string | string[]; + if (typeof arg2 === 'string') { + this.name = arg2; + this.source = arg3; + this.execution = arg4; + problemMatchers = arg5; + } else if (arg2 === TaskScope.Global || arg2 === TaskScope.Workspace) { + this.target = arg2; + this.name = arg3; + this.source = arg4; + this.execution = arg5; + problemMatchers = arg6; + } else { + this.target = arg2; + this.name = arg3; + this.source = arg4; + this.execution = arg5; + problemMatchers = arg6; + } + if (typeof problemMatchers === 'string') { + this._problemMatchers = [problemMatchers]; + this._hasDefinedMatchers = true; + } else if (Array.isArray(problemMatchers)) { + this._problemMatchers = problemMatchers; + this._hasDefinedMatchers = true; + } else { + this._problemMatchers = []; + this._hasDefinedMatchers = false; + } + this._isBackground = false; + this._presentationOptions = Object.create(null); + this._runOptions = Object.create(null); + } + + get _id(): string | undefined { + return this.__id; + } + + set _id(value: string | undefined) { + this.__id = value; + } + + private clear(): void { + if (this.__id === undefined) { + return; + } + this.__id = undefined; + this._scope = undefined; + this.computeDefinitionBasedOnExecution(); + } + + private computeDefinitionBasedOnExecution(): void { + if (this._execution instanceof ProcessExecution) { + this._definition = { + type: Task.ProcessType, + id: this._execution.computeId() + }; + } else if (this._execution instanceof ShellExecution) { + this._definition = { + type: Task.ShellType, + id: this._execution.computeId() + }; + } else { + this._definition = { + type: Task.EmptyType, + id: generateUuid() + }; + } + } + + get definition(): vscode.TaskDefinition { + return this._definition; + } + + set definition(value: vscode.TaskDefinition) { + if (value === undefined || value === null) { + throw illegalArgument("Kind can't be undefined or null"); + } + this.clear(); + this._definition = value; + } + + get scope(): vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined { + return this._scope; + } + + set target(value: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder) { + this.clear(); + this._scope = value; + } + + get name(): string { + return this._name; + } + + set name(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('name'); + } + this.clear(); + this._name = value; + } + + get execution(): ProcessExecution | ShellExecution | undefined { + return this._execution; + } + + set execution(value: ProcessExecution | ShellExecution | undefined) { + if (value === null) { + value = undefined; + } + this.clear(); + this._execution = value; + let type = this._definition.type; + if (Task.EmptyType === type || Task.ProcessType === type || Task.ShellType === type) { + this.computeDefinitionBasedOnExecution(); + } + } + + get problemMatchers(): string[] { + return this._problemMatchers; + } + + set problemMatchers(value: string[]) { + if (!Array.isArray(value)) { + this.clear(); + this._problemMatchers = []; + this._hasDefinedMatchers = false; + return; + } else { + this.clear(); + this._problemMatchers = value; + this._hasDefinedMatchers = true; + } + } + + get hasDefinedMatchers(): boolean { + return this._hasDefinedMatchers; + } + + get isBackground(): boolean { + return this._isBackground; + } + + set isBackground(value: boolean) { + if (value !== true && value !== false) { + value = false; + } + this.clear(); + this._isBackground = value; + } + + get source(): string { + return this._source; + } + + set source(value: string) { + if (typeof value !== 'string' || value.length === 0) { + throw illegalArgument('source must be a string of length > 0'); + } + this.clear(); + this._source = value; + } + + get group(): TaskGroup | undefined { + return this._group; + } + + set group(value: TaskGroup | undefined) { + if (value === null) { + value = undefined; + } + this.clear(); + this._group = value; + } + + get presentationOptions(): vscode.TaskPresentationOptions { + return this._presentationOptions; + } + + set presentationOptions(value: vscode.TaskPresentationOptions) { + if (value === null || value === undefined) { + value = Object.create(null); + } + this.clear(); + this._presentationOptions = value; + } + + get runOptions(): vscode.RunOptions { + return this._runOptions; + } + + set runOptions(value: vscode.RunOptions) { + if (value === null || value === undefined) { + value = Object.create(null); + } + this.clear(); + this._runOptions = value; + } + } + + export enum ProgressLocation { + SourceControl = 1, + Window = 10, + Notification = 15 + } + + export class TreeItem { + label?: string; + resourceUri?: vscUri.URI; + iconPath?: string | vscUri.URI | { light: string | vscUri.URI; dark: string | vscUri.URI }; + command?: vscode.Command; + contextValue?: string; + tooltip?: string; + + constructor(label: string, collapsibleState?: vscode.TreeItemCollapsibleState); + constructor(resourceUri: vscUri.URI, collapsibleState?: vscode.TreeItemCollapsibleState); + constructor( + arg1: string | vscUri.URI, + public collapsibleState: vscode.TreeItemCollapsibleState = TreeItemCollapsibleState.None + ) { + if (arg1 instanceof vscUri.URI) { + this.resourceUri = arg1; + } else { + this.label = arg1; + } + } + } + + export enum TreeItemCollapsibleState { + None = 0, + Collapsed = 1, + Expanded = 2 + } + + export class ThemeIcon { + static readonly File = new ThemeIcon('file'); + + static readonly Folder = new ThemeIcon('folder'); + + readonly id: string; + + private constructor(id: string) { + this.id = id; + } + } + + export class ThemeColor { + id: string; + constructor(id: string) { + this.id = id; + } + } + + export enum ConfigurationTarget { + Global = 1, + + Workspace = 2, + + WorkspaceFolder = 3 + } + + export class RelativePattern implements IRelativePattern { + base: string; + pattern: string; + + constructor(base: vscode.WorkspaceFolder | string, pattern: string) { + if (typeof base !== 'string') { + if (!base || !vscUri.URI.isUri(base.uri)) { + throw illegalArgument('base'); + } + } + + if (typeof pattern !== 'string') { + throw illegalArgument('pattern'); + } + + this.base = typeof base === 'string' ? base : base.uri.fsPath; + this.pattern = pattern; + } + + public pathToRelative(from: string, to: string): string { + return relative(from, to); + } + } + + export class Breakpoint { + readonly enabled: boolean; + readonly condition?: string; + readonly hitCondition?: string; + readonly logMessage?: string; + + protected constructor(enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { + this.enabled = typeof enabled === 'boolean' ? enabled : true; + if (typeof condition === 'string') { + this.condition = condition; + } + if (typeof hitCondition === 'string') { + this.hitCondition = hitCondition; + } + if (typeof logMessage === 'string') { + this.logMessage = logMessage; + } + } + } + + export class SourceBreakpoint extends Breakpoint { + readonly location: Location; + + constructor( + location: Location, + enabled?: boolean, + condition?: string, + hitCondition?: string, + logMessage?: string + ) { + super(enabled, condition, hitCondition, logMessage); + if (location === null) { + throw illegalArgument('location'); + } + this.location = location; + } + } + + export class FunctionBreakpoint extends Breakpoint { + readonly functionName: string; + + constructor( + functionName: string, + enabled?: boolean, + condition?: string, + hitCondition?: string, + logMessage?: string + ) { + super(enabled, condition, hitCondition, logMessage); + if (!functionName) { + throw illegalArgument('functionName'); + } + this.functionName = functionName; + } + } + + export class DebugAdapterExecutable { + readonly command: string; + readonly args: string[]; + + constructor(command: string, args?: string[]) { + this.command = command; + // @ts-ignore + this.args = args; + } + } + + export class DebugAdapterServer { + readonly port: number; + readonly host?: string; + + constructor(port: number, host?: string) { + this.port = port; + this.host = host; + } + } + + export enum LogLevel { + Trace = 1, + Debug = 2, + Info = 3, + Warning = 4, + Error = 5, + Critical = 6, + Off = 7 + } + + //#region file api + + export enum FileChangeType { + Changed = 1, + Created = 2, + Deleted = 3 + } + + export class FileSystemError extends Error { + static FileExists(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryExists', FileSystemError.FileExists); + } + static FileNotFound(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryNotFound', FileSystemError.FileNotFound); + } + static FileNotADirectory(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryNotADirectory', FileSystemError.FileNotADirectory); + } + static FileIsADirectory(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryIsADirectory', FileSystemError.FileIsADirectory); + } + static NoPermissions(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'NoPermissions', FileSystemError.NoPermissions); + } + static Unavailable(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'Unavailable', FileSystemError.Unavailable); + } + + constructor(uriOrMessage?: string | vscUri.URI, code?: string, terminator?: Function) { + super(vscUri.URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); + this.name = code ? `${code} (FileSystemError)` : `FileSystemError`; + + // workaround when extending builtin objects and when compiling to ES5, see: + // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work + if (typeof (<any>Object).setPrototypeOf === 'function') { + (<any>Object).setPrototypeOf(this, FileSystemError.prototype); + } + + if (typeof Error.captureStackTrace === 'function' && typeof terminator === 'function') { + // nice stack traces + Error.captureStackTrace(this, terminator); + } + } + public get code(): string { + return ''; + } + } + + //#endregion + + //#region folding api + + export class FoldingRange { + start: number; + + end: number; + + kind?: FoldingRangeKind; + + constructor(start: number, end: number, kind?: FoldingRangeKind) { + this.start = start; + this.end = end; + this.kind = kind; + } + } + + export enum FoldingRangeKind { + Comment = 1, + Imports = 2, + Region = 3 + } + + //#endregion + + export enum CommentThreadCollapsibleState { + /** + * Determines an item is collapsed + */ + Collapsed = 0, + /** + * Determines an item is expanded + */ + Expanded = 1 + } + + export class QuickInputButtons { + static readonly Back: vscode.QuickInputButton = { iconPath: 'back' }; + } +} diff --git a/src/test/mocks/vsc/htmlContent.ts b/src/test/mocks/vsc/htmlContent.ts new file mode 100644 index 000000000000..61d3c7b7b995 --- /dev/null +++ b/src/test/mocks/vsc/htmlContent.ts @@ -0,0 +1,102 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { vscMockArrays } from './arrays'; +// tslint:disable:all + +export namespace vscMockHtmlContent { + export interface IMarkdownString { + value: string; + isTrusted?: boolean; + } + + export class MarkdownString implements IMarkdownString { + value: string; + isTrusted?: boolean; + + constructor(value: string = '') { + this.value = value; + } + + appendText(value: string): MarkdownString { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); + return this; + } + + appendMarkdown(value: string): MarkdownString { + this.value += value; + return this; + } + + appendCodeblock(langId: string, code: string): MarkdownString { + this.value += '\n```'; + this.value += langId; + this.value += '\n'; + this.value += code; + this.value += '\n```\n'; + return this; + } + } + + export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownString[]): boolean { + if (isMarkdownString(oneOrMany)) { + return !oneOrMany.value; + } else if (Array.isArray(oneOrMany)) { + return oneOrMany.every(isEmptyMarkdownString); + } else { + return true; + } + } + + export function isMarkdownString(thing: any): thing is IMarkdownString { + if (thing instanceof MarkdownString) { + return true; + } else if (thing && typeof thing === 'object') { + return ( + typeof (<IMarkdownString>thing).value === 'string' && + (typeof (<IMarkdownString>thing).isTrusted === 'boolean' || + (<IMarkdownString>thing).isTrusted === void 0) + ); + } + return false; + } + + export function markedStringsEquals( + a: IMarkdownString | IMarkdownString[], + b: IMarkdownString | IMarkdownString[] + ): boolean { + if (!a && !b) { + return true; + } else if (!a || !b) { + return false; + } else if (Array.isArray(a) && Array.isArray(b)) { + return vscMockArrays.equals(a, b, markdownStringEqual); + } else if (isMarkdownString(a) && isMarkdownString(b)) { + return markdownStringEqual(a, b); + } else { + return false; + } + } + + function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { + if (a === b) { + return true; + } else if (!a || !b) { + return false; + } else { + return a.value === b.value && a.isTrusted === b.isTrusted; + } + } + + export function removeMarkdownEscapes(text: string): string { + if (!text) { + return text; + } + return text.replace(/\\([\\`*_{}[\]()#+\-.!])/g, '$1'); + } +} diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts new file mode 100644 index 000000000000..9f6fb3c2ab76 --- /dev/null +++ b/src/test/mocks/vsc/index.ts @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this no-require-imports no-var-requires no-any max-classes-per-file + +import { EventEmitter as NodeEventEmitter } from 'events'; +import * as vscode from 'vscode'; +// export * from './range'; +// export * from './position'; +// export * from './selection'; +export * from './extHostedTypes'; +export * from './uri'; + +export namespace vscMock { + export enum ExtensionKind { + /** + * Extension runs where the UI runs. + */ + UI = 1, + + /** + * Extension runs where the remote extension host runs. + */ + Workspace = 2 + } + + export class Disposable { + constructor(private callOnDispose: Function) {} + public dispose(): any { + if (this.callOnDispose) { + this.callOnDispose(); + } + } + } + + export class EventEmitter<T> implements vscode.EventEmitter<T> { + public event: vscode.Event<T>; + public emitter: NodeEventEmitter; + constructor() { + // @ts-ignore + this.event = this.add.bind(this); + this.emitter = new NodeEventEmitter(); + } + public fire(data?: T): void { + this.emitter.emit('evt', data); + } + public dispose(): void { + this.emitter.removeAllListeners(); + } + + protected add = (listener: (e: T) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable => { + const bound = _thisArgs ? listener.bind(_thisArgs) : listener; + this.emitter.addListener('evt', bound); + return ({ + dispose: () => { + this.emitter.removeListener('evt', bound); + } + } as any) as Disposable; + }; + } + + export class CancellationToken extends EventEmitter<any> implements vscode.CancellationToken { + public isCancellationRequested!: boolean; + public onCancellationRequested: vscode.Event<any>; + constructor() { + super(); + // @ts-ignore + this.onCancellationRequested = this.add.bind(this); + } + public cancel() { + this.isCancellationRequested = true; + this.fire(); + } + } + + export class CancellationTokenSource { + public token: CancellationToken; + constructor() { + this.token = new CancellationToken(); + } + public cancel(): void { + this.token.cancel(); + } + public dispose(): void { + this.token.dispose(); + } + } + + export class CodeAction { + public title: string; + public edit?: vscode.WorkspaceEdit; + public diagnostics?: vscode.Diagnostic[]; + public command?: vscode.Command; + public kind?: CodeActionKind; + public isPreferred?: boolean; + constructor(_title: string, _kind?: CodeActionKind) { + this.title = _title; + this.kind = _kind; + } + } + + export enum CompletionItemKind { + Text = 0, + Method = 1, + Function = 2, + Constructor = 3, + Field = 4, + Variable = 5, + Class = 6, + Interface = 7, + Module = 8, + Property = 9, + Unit = 10, + Value = 11, + Enum = 12, + Keyword = 13, + Snippet = 14, + Color = 15, + Reference = 17, + File = 16, + Folder = 18, + EnumMember = 19, + Constant = 20, + Struct = 21, + Event = 22, + Operator = 23, + TypeParameter = 24, + User = 25, + Issue = 26 + } + export enum SymbolKind { + File = 0, + Module = 1, + Namespace = 2, + Package = 3, + Class = 4, + Method = 5, + Property = 6, + Field = 7, + Constructor = 8, + Enum = 9, + Interface = 10, + Function = 11, + Variable = 12, + Constant = 13, + String = 14, + Number = 15, + Boolean = 16, + Array = 17, + Object = 18, + Key = 19, + Null = 20, + EnumMember = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25 + } + export enum IndentAction { + None = 0, + Indent = 1, + IndentOutdent = 2, + Outdent = 3 + } + + export class CodeActionKind { + public static readonly Empty: CodeActionKind = new CodeActionKind('empty'); + public static readonly QuickFix: CodeActionKind = new CodeActionKind('quick.fix'); + + public static readonly Refactor: CodeActionKind = new CodeActionKind('refactor'); + + public static readonly RefactorExtract: CodeActionKind = new CodeActionKind('refactor.extract'); + + public static readonly RefactorInline: CodeActionKind = new CodeActionKind('refactor.inline'); + + public static readonly RefactorRewrite: CodeActionKind = new CodeActionKind('refactor.rewrite'); + public static readonly Source: CodeActionKind = new CodeActionKind('source'); + public static readonly SourceOrganizeImports: CodeActionKind = new CodeActionKind('source.organize.imports'); + public static readonly SourceFixAll: CodeActionKind = new CodeActionKind('source.fix.all'); + + private constructor(private _value: string) {} + + public append(parts: string): CodeActionKind { + return new CodeActionKind(`${this._value}.${parts}`); + } + public intersects(other: CodeActionKind): boolean { + return this._value.includes(other._value) || other._value.includes(this._value); + } + + public contains(other: CodeActionKind): boolean { + return this._value.startsWith(other._value); + } + + public get value(): string { + return this._value; + } + } + + // tslint:disable-next-line: interface-name + export interface DebugAdapterExecutableOptions { + env?: { [key: string]: string }; + cwd?: string; + } + + export class DebugAdapterServer { + constructor(public readonly port: number, public readonly host?: string) {} + } + export class DebugAdapterExecutable { + constructor( + public readonly command: string, + public readonly args: string[] = [], + public readonly options?: DebugAdapterExecutableOptions + ) {} + } + + export enum FileType { + Unknown = 0, + File = 1, + Directory = 2, + SymbolicLink = 64 + } +} diff --git a/src/test/mocks/vsc/position.ts b/src/test/mocks/vsc/position.ts new file mode 100644 index 000000000000..b3dff6149f3a --- /dev/null +++ b/src/test/mocks/vsc/position.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +// tslint:disable:all +export namespace vscMockPosition { + /** + * A position in the editor. This interface is suitable for serialization. + */ + export interface IPosition { + /** + * line number (starts at 1) + */ + readonly lineNumber: number; + /** + * column (the first character in a line is between column 1 and column 2) + */ + readonly column: number; + } + + /** + * A position in the editor. + */ + export class Position { + /** + * line number (starts at 1) + */ + public readonly lineNumber: number; + /** + * column (the first character in a line is between column 1 and column 2) + */ + public readonly column: number; + + constructor(lineNumber: number, column: number) { + this.lineNumber = lineNumber; + this.column = column; + } + + /** + * Test if this position equals other position + */ + public equals(other: IPosition): boolean { + return Position.equals(this, other); + } + + /** + * Test if position `a` equals position `b` + */ + public static equals(a: IPosition, b: IPosition): boolean { + if (!a && !b) { + return true; + } + return !!a && !!b && a.lineNumber === b.lineNumber && a.column === b.column; + } + + /** + * Test if this position is before other position. + * If the two positions are equal, the result will be false. + */ + public isBefore(other: IPosition): boolean { + return Position.isBefore(this, other); + } + + /** + * Test if position `a` is before position `b`. + * If the two positions are equal, the result will be false. + */ + public static isBefore(a: IPosition, b: IPosition): boolean { + if (a.lineNumber < b.lineNumber) { + return true; + } + if (b.lineNumber < a.lineNumber) { + return false; + } + return a.column < b.column; + } + + /** + * Test if this position is before other position. + * If the two positions are equal, the result will be true. + */ + public isBeforeOrEqual(other: IPosition): boolean { + return Position.isBeforeOrEqual(this, other); + } + + /** + * Test if position `a` is before position `b`. + * If the two positions are equal, the result will be true. + */ + public static isBeforeOrEqual(a: IPosition, b: IPosition): boolean { + if (a.lineNumber < b.lineNumber) { + return true; + } + if (b.lineNumber < a.lineNumber) { + return false; + } + return a.column <= b.column; + } + + /** + * A function that compares positions, useful for sorting + */ + public static compare(a: IPosition, b: IPosition): number { + let aLineNumber = a.lineNumber | 0; + let bLineNumber = b.lineNumber | 0; + + if (aLineNumber === bLineNumber) { + let aColumn = a.column | 0; + let bColumn = b.column | 0; + return aColumn - bColumn; + } + + return aLineNumber - bLineNumber; + } + + /** + * Clone this position. + */ + public clone(): Position { + return new Position(this.lineNumber, this.column); + } + + /** + * Convert to a human-readable representation. + */ + public toString(): string { + return '(' + this.lineNumber + ',' + this.column + ')'; + } + + // --- + + /** + * Create a `Position` from an `IPosition`. + */ + public static lift(pos: IPosition): Position { + return new Position(pos.lineNumber, pos.column); + } + + /** + * Test if `obj` is an `IPosition`. + */ + public static isIPosition(obj: any): obj is IPosition { + return obj && typeof obj.lineNumber === 'number' && typeof obj.column === 'number'; + } + } +} diff --git a/src/test/mocks/vsc/range.ts b/src/test/mocks/vsc/range.ts new file mode 100644 index 000000000000..c2667e2ce9bc --- /dev/null +++ b/src/test/mocks/vsc/range.ts @@ -0,0 +1,405 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +// tslint:disable:all +import { vscMockPosition } from './position'; + +export namespace vscMockRange { + /** + * A range in the editor. This interface is suitable for serialization. + */ + export interface IRange { + /** + * Line number on which the range starts (starts at 1). + */ + readonly startLineNumber: number; + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + readonly startColumn: number; + /** + * Line number on which the range ends. + */ + readonly endLineNumber: number; + /** + * Column on which the range ends in line `endLineNumber`. + */ + readonly endColumn: number; + } + + /** + * A range in the editor. (startLineNumber,startColumn) is <= (endLineNumber,endColumn) + */ + export class Range { + /** + * Line number on which the range starts (starts at 1). + */ + public readonly startLineNumber: number; + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + public readonly startColumn: number; + /** + * Line number on which the range ends. + */ + public readonly endLineNumber: number; + /** + * Column on which the range ends in line `endLineNumber`. + */ + public readonly endColumn: number; + + constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) { + if (startLineNumber > endLineNumber || (startLineNumber === endLineNumber && startColumn > endColumn)) { + this.startLineNumber = endLineNumber; + this.startColumn = endColumn; + this.endLineNumber = startLineNumber; + this.endColumn = startColumn; + } else { + this.startLineNumber = startLineNumber; + this.startColumn = startColumn; + this.endLineNumber = endLineNumber; + this.endColumn = endColumn; + } + } + + /** + * Test if this range is empty. + */ + public isEmpty(): boolean { + return Range.isEmpty(this); + } + + /** + * Test if `range` is empty. + */ + public static isEmpty(range: IRange): boolean { + return range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn; + } + + /** + * Test if position is in this range. If the position is at the edges, will return true. + */ + public containsPosition(position: vscMockPosition.IPosition): boolean { + return Range.containsPosition(this, position); + } + + /** + * Test if `position` is in `range`. If the position is at the edges, will return true. + */ + public static containsPosition(range: IRange, position: vscMockPosition.IPosition): boolean { + if (position.lineNumber < range.startLineNumber || position.lineNumber > range.endLineNumber) { + return false; + } + if (position.lineNumber === range.startLineNumber && position.column < range.startColumn) { + return false; + } + if (position.lineNumber === range.endLineNumber && position.column > range.endColumn) { + return false; + } + return true; + } + + /** + * Test if range is in this range. If the range is equal to this range, will return true. + */ + public containsRange(range: IRange): boolean { + return Range.containsRange(this, range); + } + + /** + * Test if `otherRange` is in `range`. If the ranges are equal, will return true. + */ + public static containsRange(range: IRange, otherRange: IRange): boolean { + if ( + otherRange.startLineNumber < range.startLineNumber || + otherRange.endLineNumber < range.startLineNumber + ) { + return false; + } + if (otherRange.startLineNumber > range.endLineNumber || otherRange.endLineNumber > range.endLineNumber) { + return false; + } + if (otherRange.startLineNumber === range.startLineNumber && otherRange.startColumn < range.startColumn) { + return false; + } + if (otherRange.endLineNumber === range.endLineNumber && otherRange.endColumn > range.endColumn) { + return false; + } + return true; + } + + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public plusRange(range: IRange): Range { + return Range.plusRange(this, range); + } + + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public static plusRange(a: IRange, b: IRange): Range { + var startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number; + if (b.startLineNumber < a.startLineNumber) { + startLineNumber = b.startLineNumber; + startColumn = b.startColumn; + } else if (b.startLineNumber === a.startLineNumber) { + startLineNumber = b.startLineNumber; + startColumn = Math.min(b.startColumn, a.startColumn); + } else { + startLineNumber = a.startLineNumber; + startColumn = a.startColumn; + } + + if (b.endLineNumber > a.endLineNumber) { + endLineNumber = b.endLineNumber; + endColumn = b.endColumn; + } else if (b.endLineNumber === a.endLineNumber) { + endLineNumber = b.endLineNumber; + endColumn = Math.max(b.endColumn, a.endColumn); + } else { + endLineNumber = a.endLineNumber; + endColumn = a.endColumn; + } + + return new Range(startLineNumber, startColumn, endLineNumber, endColumn); + } + + /** + * A intersection of the two ranges. + */ + public intersectRanges(range: IRange): Range { + return Range.intersectRanges(this, range); + } + + /** + * A intersection of the two ranges. + */ + public static intersectRanges(a: IRange, b: IRange): Range { + var resultStartLineNumber = a.startLineNumber, + resultStartColumn = a.startColumn, + resultEndLineNumber = a.endLineNumber, + resultEndColumn = a.endColumn, + otherStartLineNumber = b.startLineNumber, + otherStartColumn = b.startColumn, + otherEndLineNumber = b.endLineNumber, + otherEndColumn = b.endColumn; + + if (resultStartLineNumber < otherStartLineNumber) { + resultStartLineNumber = otherStartLineNumber; + resultStartColumn = otherStartColumn; + } else if (resultStartLineNumber === otherStartLineNumber) { + resultStartColumn = Math.max(resultStartColumn, otherStartColumn); + } + + if (resultEndLineNumber > otherEndLineNumber) { + resultEndLineNumber = otherEndLineNumber; + resultEndColumn = otherEndColumn; + } else if (resultEndLineNumber === otherEndLineNumber) { + resultEndColumn = Math.min(resultEndColumn, otherEndColumn); + } + + // Check if selection is now empty + if (resultStartLineNumber > resultEndLineNumber) { + // @ts-ignore + return null; + } + if (resultStartLineNumber === resultEndLineNumber && resultStartColumn > resultEndColumn) { + // @ts-ignore + return null; + } + return new Range(resultStartLineNumber, resultStartColumn, resultEndLineNumber, resultEndColumn); + } + + /** + * Test if this range equals other. + */ + public equalsRange(other: IRange): boolean { + return Range.equalsRange(this, other); + } + + /** + * Test if range `a` equals `b`. + */ + public static equalsRange(a: IRange, b: IRange): boolean { + return ( + !!a && + !!b && + a.startLineNumber === b.startLineNumber && + a.startColumn === b.startColumn && + a.endLineNumber === b.endLineNumber && + a.endColumn === b.endColumn + ); + } + + /** + * Return the end position (which will be after or equal to the start position) + */ + public getEndPosition(): vscMockPosition.Position { + return new vscMockPosition.Position(this.endLineNumber, this.endColumn); + } + + /** + * Return the start position (which will be before or equal to the end position) + */ + public getStartPosition(): vscMockPosition.Position { + return new vscMockPosition.Position(this.startLineNumber, this.startColumn); + } + + /** + * Transform to a user presentable string representation. + */ + public toString(): string { + return ( + '[' + + this.startLineNumber + + ',' + + this.startColumn + + ' -> ' + + this.endLineNumber + + ',' + + this.endColumn + + ']' + ); + } + + /** + * Create a new range using this range's start position, and using endLineNumber and endColumn as the end position. + */ + public setEndPosition(endLineNumber: number, endColumn: number): Range { + return new Range(this.startLineNumber, this.startColumn, endLineNumber, endColumn); + } + + /** + * Create a new range using this range's end position, and using startLineNumber and startColumn as the start position. + */ + public setStartPosition(startLineNumber: number, startColumn: number): Range { + return new Range(startLineNumber, startColumn, this.endLineNumber, this.endColumn); + } + + /** + * Create a new empty range using this range's start position. + */ + public collapseToStart(): Range { + return Range.collapseToStart(this); + } + + /** + * Create a new empty range using this range's start position. + */ + public static collapseToStart(range: IRange): Range { + return new Range(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn); + } + + // --- + + public static fromPositions(start: vscMockPosition.IPosition, end: vscMockPosition.IPosition = start): Range { + return new Range(start.lineNumber, start.column, end.lineNumber, end.column); + } + + /** + * Create a `Range` from an `IRange`. + */ + public static lift(range: IRange): Range { + if (!range) { + // @ts-ignore + return null; + } + return new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + } + + /** + * Test if `obj` is an `IRange`. + */ + public static isIRange(obj: any): obj is IRange { + return ( + obj && + typeof obj.startLineNumber === 'number' && + typeof obj.startColumn === 'number' && + typeof obj.endLineNumber === 'number' && + typeof obj.endColumn === 'number' + ); + } + + /** + * Test if the two ranges are touching in any way. + */ + public static areIntersectingOrTouching(a: IRange, b: IRange): boolean { + // Check if `a` is before `b` + if ( + a.endLineNumber < b.startLineNumber || + (a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn) + ) { + return false; + } + + // Check if `b` is before `a` + if ( + b.endLineNumber < a.startLineNumber || + (b.endLineNumber === a.startLineNumber && b.endColumn < a.startColumn) + ) { + return false; + } + + // These ranges must intersect + return true; + } + + /** + * A function that compares ranges, useful for sorting ranges + * It will first compare ranges on the startPosition and then on the endPosition + */ + public static compareRangesUsingStarts(a: IRange, b: IRange): number { + let aStartLineNumber = a.startLineNumber | 0; + let bStartLineNumber = b.startLineNumber | 0; + + if (aStartLineNumber === bStartLineNumber) { + let aStartColumn = a.startColumn | 0; + let bStartColumn = b.startColumn | 0; + + if (aStartColumn === bStartColumn) { + let aEndLineNumber = a.endLineNumber | 0; + let bEndLineNumber = b.endLineNumber | 0; + + if (aEndLineNumber === bEndLineNumber) { + let aEndColumn = a.endColumn | 0; + let bEndColumn = b.endColumn | 0; + return aEndColumn - bEndColumn; + } + return aEndLineNumber - bEndLineNumber; + } + return aStartColumn - bStartColumn; + } + return aStartLineNumber - bStartLineNumber; + } + + /** + * A function that compares ranges, useful for sorting ranges + * It will first compare ranges on the endPosition and then on the startPosition + */ + public static compareRangesUsingEnds(a: IRange, b: IRange): number { + if (a.endLineNumber === b.endLineNumber) { + if (a.endColumn === b.endColumn) { + if (a.startLineNumber === b.startLineNumber) { + return a.startColumn - b.startColumn; + } + return a.startLineNumber - b.startLineNumber; + } + return a.endColumn - b.endColumn; + } + return a.endLineNumber - b.endLineNumber; + } + + /** + * Test if the range spans multiple lines. + */ + public static spansMultipleLines(range: IRange): boolean { + return range.endLineNumber > range.startLineNumber; + } + } +} diff --git a/src/test/mocks/vsc/selection.ts b/src/test/mocks/vsc/selection.ts new file mode 100644 index 000000000000..9e82b905f7cc --- /dev/null +++ b/src/test/mocks/vsc/selection.ts @@ -0,0 +1,245 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +// tslint:disable:all +import { vscMockPosition } from './position'; +import { vscMockRange } from './range'; +export namespace vscMockSelection { + /** + * A selection in the editor. + * The selection is a range that has an orientation. + */ + export interface ISelection { + /** + * The line number on which the selection has started. + */ + readonly selectionStartLineNumber: number; + /** + * The column on `selectionStartLineNumber` where the selection has started. + */ + readonly selectionStartColumn: number; + /** + * The line number on which the selection has ended. + */ + readonly positionLineNumber: number; + /** + * The column on `positionLineNumber` where the selection has ended. + */ + readonly positionColumn: number; + } + + /** + * The direction of a selection. + */ + export enum SelectionDirection { + /** + * The selection starts above where it ends. + */ + LTR, + /** + * The selection starts below where it ends. + */ + RTL + } + + /** + * A selection in the editor. + * The selection is a range that has an orientation. + */ + export class Selection extends vscMockRange.Range { + /** + * The line number on which the selection has started. + */ + public readonly selectionStartLineNumber: number; + /** + * The column on `selectionStartLineNumber` where the selection has started. + */ + public readonly selectionStartColumn: number; + /** + * The line number on which the selection has ended. + */ + public readonly positionLineNumber: number; + /** + * The column on `positionLineNumber` where the selection has ended. + */ + public readonly positionColumn: number; + + constructor( + selectionStartLineNumber: number, + selectionStartColumn: number, + positionLineNumber: number, + positionColumn: number + ) { + super(selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn); + this.selectionStartLineNumber = selectionStartLineNumber; + this.selectionStartColumn = selectionStartColumn; + this.positionLineNumber = positionLineNumber; + this.positionColumn = positionColumn; + } + + /** + * Clone this selection. + */ + public clone(): Selection { + return new Selection( + this.selectionStartLineNumber, + this.selectionStartColumn, + this.positionLineNumber, + this.positionColumn + ); + } + + /** + * Transform to a human-readable representation. + */ + public toString(): string { + return ( + '[' + + this.selectionStartLineNumber + + ',' + + this.selectionStartColumn + + ' -> ' + + this.positionLineNumber + + ',' + + this.positionColumn + + ']' + ); + } + + /** + * Test if equals other selection. + */ + public equalsSelection(other: ISelection): boolean { + return Selection.selectionsEqual(this, other); + } + + /** + * Test if the two selections are equal. + */ + public static selectionsEqual(a: ISelection, b: ISelection): boolean { + return ( + a.selectionStartLineNumber === b.selectionStartLineNumber && + a.selectionStartColumn === b.selectionStartColumn && + a.positionLineNumber === b.positionLineNumber && + a.positionColumn === b.positionColumn + ); + } + + /** + * Get directions (LTR or RTL). + */ + public getDirection(): SelectionDirection { + if ( + this.selectionStartLineNumber === this.startLineNumber && + this.selectionStartColumn === this.startColumn + ) { + return SelectionDirection.LTR; + } + return SelectionDirection.RTL; + } + + /** + * Create a new selection with a different `positionLineNumber` and `positionColumn`. + */ + public setEndPosition(endLineNumber: number, endColumn: number): Selection { + if (this.getDirection() === SelectionDirection.LTR) { + return new Selection(this.startLineNumber, this.startColumn, endLineNumber, endColumn); + } + return new Selection(endLineNumber, endColumn, this.startLineNumber, this.startColumn); + } + + /** + * Get the position at `positionLineNumber` and `positionColumn`. + */ + public getPosition(): vscMockPosition.Position { + return new vscMockPosition.Position(this.positionLineNumber, this.positionColumn); + } + + /** + * Create a new selection with a different `selectionStartLineNumber` and `selectionStartColumn`. + */ + public setStartPosition(startLineNumber: number, startColumn: number): Selection { + if (this.getDirection() === SelectionDirection.LTR) { + return new Selection(startLineNumber, startColumn, this.endLineNumber, this.endColumn); + } + return new Selection(this.endLineNumber, this.endColumn, startLineNumber, startColumn); + } + + // ---- + + /** + * Create a `Selection` from one or two positions + */ + public static fromPositions( + start: vscMockPosition.IPosition, + end: vscMockPosition.IPosition = start + ): Selection { + return new Selection(start.lineNumber, start.column, end.lineNumber, end.column); + } + + /** + * Create a `Selection` from an `ISelection`. + */ + public static liftSelection(sel: ISelection): Selection { + return new Selection( + sel.selectionStartLineNumber, + sel.selectionStartColumn, + sel.positionLineNumber, + sel.positionColumn + ); + } + + /** + * `a` equals `b`. + */ + public static selectionsArrEqual(a: ISelection[], b: ISelection[]): boolean { + if ((a && !b) || (!a && b)) { + return false; + } + if (!a && !b) { + return true; + } + if (a.length !== b.length) { + return false; + } + for (var i = 0, len = a.length; i < len; i++) { + if (!this.selectionsEqual(a[i], b[i])) { + return false; + } + } + return true; + } + + /** + * Test if `obj` is an `ISelection`. + */ + public static isISelection(obj: any): obj is ISelection { + return ( + obj && + typeof obj.selectionStartLineNumber === 'number' && + typeof obj.selectionStartColumn === 'number' && + typeof obj.positionLineNumber === 'number' && + typeof obj.positionColumn === 'number' + ); + } + + /** + * Create with a direction. + */ + public static createWithDirection( + startLineNumber: number, + startColumn: number, + endLineNumber: number, + endColumn: number, + direction: SelectionDirection + ): Selection { + if (direction === SelectionDirection.LTR) { + return new Selection(startLineNumber, startColumn, endLineNumber, endColumn); + } + + return new Selection(endLineNumber, endColumn, startLineNumber, startColumn); + } + } +} diff --git a/src/test/mocks/vsc/strings.ts b/src/test/mocks/vsc/strings.ts new file mode 100644 index 000000000000..a1feac2d2668 --- /dev/null +++ b/src/test/mocks/vsc/strings.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +// tslint:disable:all + +export namespace vscMockStrings { + /** + * Determines if haystack starts with needle. + */ + export function startsWith(haystack: string, needle: string): boolean { + if (haystack.length < needle.length) { + return false; + } + + for (let i = 0; i < needle.length; i++) { + if (haystack[i] !== needle[i]) { + return false; + } + } + + return true; + } + + /** + * Determines if haystack ends with needle. + */ + export function endsWith(haystack: string, needle: string): boolean { + let diff = haystack.length - needle.length; + if (diff > 0) { + return haystack.indexOf(needle, diff) === diff; + } else if (diff === 0) { + return haystack === needle; + } else { + return false; + } + } +} diff --git a/src/test/mocks/vsc/telemetryReporter.ts b/src/test/mocks/vsc/telemetryReporter.ts new file mode 100644 index 000000000000..d0250eb1cf5e --- /dev/null +++ b/src/test/mocks/vsc/telemetryReporter.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:all +export class vscMockTelemetryReporter { + constructor() { + // + } + + public sendTelemetryEvent(): void { + // + } +} diff --git a/src/test/mocks/vsc/uri.ts b/src/test/mocks/vsc/uri.ts new file mode 100644 index 000000000000..aa5f38a25555 --- /dev/null +++ b/src/test/mocks/vsc/uri.ts @@ -0,0 +1,728 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +/* tslint:disable */ + +import { CharCode } from './charCode'; +import * as path from 'path'; + +export namespace vscUri { + const isWindows = /^win/.test(process.platform); + /*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + const _schemePattern = /^\w[\w\d+.-]*$/; + const _singleSlashStart = /^\//; + const _doubleSlashStart = /^\/\//; + + let _throwOnMissingSchema: boolean = true; + + /** + * @internal + */ + export function setUriThrowOnMissingScheme(value: boolean): boolean { + const old = _throwOnMissingSchema; + _throwOnMissingSchema = value; + return old; + } + + function _validateUri(ret: URI, _strict?: boolean): void { + // scheme, must be set + // if (!ret.scheme) { + // // if (_strict || _throwOnMissingSchema) { + // // throw new Error(`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`); + // // } else { + // console.warn(`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`); + // // } + // } + + // scheme, https://tools.ietf.org/html/rfc3986#section-3.1 + // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + if (ret.scheme && !_schemePattern.test(ret.scheme)) { + throw new Error('[UriError]: Scheme contains illegal characters.'); + } + + // path, http://tools.ietf.org/html/rfc3986#section-3.3 + // If a URI contains an authority component, then the path component + // must either be empty or begin with a slash ("/") character. If a URI + // does not contain an authority component, then the path cannot begin + // with two slash characters ("//"). + if (ret.path) { + if (ret.authority) { + if (!_singleSlashStart.test(ret.path)) { + throw new Error( + '[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character' + ); + } + } else { + if (_doubleSlashStart.test(ret.path)) { + throw new Error( + '[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")' + ); + } + } + } + } + + // for a while we allowed uris *without* schemes and this is the migration + // for them, e.g. an uri without scheme and without strict-mode warns and falls + // back to the file-scheme. that should cause the least carnage and still be a + // clear warning + function _schemeFix(scheme: string, _strict: boolean): string { + if (_strict || _throwOnMissingSchema) { + return scheme || _empty; + } + if (!scheme) { + // tslint:disable-next-line: no-console + console.trace('BAD uri lacks scheme, falling back to file-scheme.'); + scheme = 'file'; + } + return scheme; + } + + // implements a bit of https://tools.ietf.org/html/rfc3986#section-5 + function _referenceResolution(scheme: string, path: string): string { + // the slash-character is our 'default base' as we don't + // support constructing URIs relative to other URIs. This + // also means that we alter and potentially break paths. + // see https://tools.ietf.org/html/rfc3986#section-5.1.4 + switch (scheme) { + case 'https': + case 'http': + case 'file': + if (!path) { + path = _slash; + } else if (path[0] !== _slash) { + path = _slash + path; + } + break; + } + return path; + } + + const _empty = ''; + const _slash = '/'; + const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; + + /** + * Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986. + * This class is a simple parser which creates the basic component parts + * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation + * and encoding. + * + * foo://example.com:8042/over/there?name=ferret#nose + * \_/ \______________/\_________/ \_________/ \__/ + * | | | | | + * scheme authority path query fragment + * | _____________________|__ + * / \ / \ + * urn:example:animal:ferret:nose + */ + // tslint:disable-next-line: no-use-before-declare + export class URI implements UriComponents { + static isUri(thing: any): thing is URI { + if (thing instanceof URI) { + return true; + } + if (!thing) { + return false; + } + return ( + typeof (<URI>thing).authority === 'string' && + typeof (<URI>thing).fragment === 'string' && + typeof (<URI>thing).path === 'string' && + typeof (<URI>thing).query === 'string' && + typeof (<URI>thing).scheme === 'string' && + typeof (<URI>thing).fsPath === 'function' && + typeof (<URI>thing).with === 'function' && + typeof (<URI>thing).toString === 'function' + ); + } + + /** + * scheme is the 'http' part of 'http://www.msft.com/some/path?query#fragment'. + * The part before the first colon. + */ + readonly scheme: string; + + /** + * authority is the 'www.msft.com' part of 'http://www.msft.com/some/path?query#fragment'. + * The part between the first double slashes and the next slash. + */ + readonly authority: string; + + /** + * path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly path: string; + + /** + * query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly query: string; + + /** + * fragment is the 'fragment' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly fragment: string; + + /** + * @internal + */ + protected constructor( + scheme: string, + authority?: string, + path?: string, + query?: string, + fragment?: string, + _strict?: boolean + ); + + /** + * @internal + */ + protected constructor(components: UriComponents); + + /** + * @internal + */ + protected constructor( + schemeOrData: string | UriComponents, + authority?: string, + path?: string, + query?: string, + fragment?: string, + _strict: boolean = false + ) { + if (typeof schemeOrData === 'object') { + this.scheme = schemeOrData.scheme || _empty; + this.authority = schemeOrData.authority || _empty; + this.path = schemeOrData.path || _empty; + this.query = schemeOrData.query || _empty; + this.fragment = schemeOrData.fragment || _empty; + // no validation because it's this URI + // that creates uri components. + // _validateUri(this); + } else { + this.scheme = _schemeFix(schemeOrData, _strict); + this.authority = authority || _empty; + this.path = _referenceResolution(this.scheme, path || _empty); + this.query = query || _empty; + this.fragment = fragment || _empty; + + _validateUri(this, _strict); + } + } + + // ---- filesystem path ----------------------- + + /** + * Returns a string representing the corresponding file system path of this URI. + * Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the + * platform specific path separator. + * + * * Will *not* validate the path for invalid characters and semantics. + * * Will *not* look at the scheme of this URI. + * * The result shall *not* be used for display purposes but for accessing a file on disk. + * + * + * The *difference* to `URI#path` is the use of the platform specific separator and the handling + * of UNC paths. See the below sample of a file-uri with an authority (UNC path). + * + * ```ts + const u = URI.parse('file://server/c$/folder/file.txt') + u.authority === 'server' + u.path === '/shares/c$/file.txt' + u.fsPath === '\\server\c$\folder\file.txt' + ``` + * + * Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path, + * namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working + * with URIs that represent files on disk (`file` scheme). + */ + get fsPath(): string { + // if (this.scheme !== 'file') { + // console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`); + // } + return _makeFsPath(this); + } + + // ---- modify to new ------------------------- + + with(change: { + scheme?: string; + authority?: string | null; + path?: string | null; + query?: string | null; + fragment?: string | null; + }): URI { + if (!change) { + return this; + } + + let { scheme, authority, path, query, fragment } = change; + if (scheme === undefined) { + scheme = this.scheme; + } else if (scheme === null) { + scheme = _empty; + } + if (authority === undefined) { + authority = this.authority; + } else if (authority === null) { + authority = _empty; + } + if (path === undefined) { + path = this.path; + } else if (path === null) { + path = _empty; + } + if (query === undefined) { + query = this.query; + } else if (query === null) { + query = _empty; + } + if (fragment === undefined) { + fragment = this.fragment; + } else if (fragment === null) { + fragment = _empty; + } + + if ( + scheme === this.scheme && + authority === this.authority && + path === this.path && + query === this.query && + fragment === this.fragment + ) { + return this; + } + + return new _URI(scheme, authority, path, query, fragment); + } + + // ---- parse & validate ------------------------ + + /** + * Creates a new URI from a string, e.g. `http://www.msft.com/some/path`, + * `file:///usr/home`, or `scheme:with/path`. + * + * @param value A string which represents an URI (see `URI#toString`). + * @param {boolean} [_strict=false] + */ + static parse(value: string, _strict: boolean = false): URI { + const match = _regexp.exec(value); + if (!match) { + return new _URI(_empty, _empty, _empty, _empty, _empty); + } + return new _URI( + match[2] || _empty, + decodeURIComponent(match[4] || _empty), + decodeURIComponent(match[5] || _empty), + decodeURIComponent(match[7] || _empty), + decodeURIComponent(match[9] || _empty), + _strict + ); + } + + /** + * Creates a new URI from a file system path, e.g. `c:\my\files`, + * `/usr/home`, or `\\server\share\some\path`. + * + * The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument + * as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as** + * `URI.parse('file://' + path)` because the path might contain characters that are + * interpreted (# and ?). See the following sample: + * ```ts + const good = URI.file('/coding/c#/project1'); + good.scheme === 'file'; + good.path === '/coding/c#/project1'; + good.fragment === ''; + const bad = URI.parse('file://' + '/coding/c#/project1'); + bad.scheme === 'file'; + bad.path === '/coding/c'; // path is now broken + bad.fragment === '/project1'; + ``` + * + * @param path A file system path (see `URI#fsPath`) + */ + static file(path: string): URI { + let authority = _empty; + + // normalize to fwd-slashes on windows, + // on other systems bwd-slashes are valid + // filename character, eg /f\oo/ba\r.txt + if (isWindows) { + path = path.replace(/\\/g, _slash); + } + + // check for authority as used in UNC shares + // or use the path as given + if (path[0] === _slash && path[1] === _slash) { + const idx = path.indexOf(_slash, 2); + if (idx === -1) { + authority = path.substring(2); + path = _slash; + } else { + authority = path.substring(2, idx); + path = path.substring(idx) || _slash; + } + } + + return new _URI('file', authority, path, _empty, _empty); + } + + static from(components: { + scheme: string; + authority?: string; + path?: string; + query?: string; + fragment?: string; + }): URI { + return new _URI( + components.scheme, + components.authority, + components.path, + components.query, + components.fragment + ); + } + + // ---- printing/externalize --------------------------- + + /** + * Creates a string representation for this URI. It's guaranteed that calling + * `URI.parse` with the result of this function creates an URI which is equal + * to this URI. + * + * * The result shall *not* be used for display purposes but for externalization or transport. + * * The result will be encoded using the percentage encoding and encoding happens mostly + * ignore the scheme-specific encoding rules. + * + * @param skipEncoding Do not encode the result, default is `false` + */ + toString(skipEncoding: boolean = false): string { + return _asFormatted(this, skipEncoding); + } + + toJSON(): UriComponents { + return this; + } + + static revive(data: UriComponents | URI): URI; + static revive(data: UriComponents | URI | undefined): URI | undefined; + static revive(data: UriComponents | URI | null): URI | null; + static revive(data: UriComponents | URI | undefined | null): URI | undefined | null; + static revive(data: UriComponents | URI | undefined | null): URI | undefined | null { + if (!data) { + return data; + } else if (data instanceof URI) { + return data; + } else { + const result = new _URI(data); + result._formatted = (<UriState>data).external; + result._fsPath = (<UriState>data)._sep === _pathSepMarker ? (<UriState>data).fsPath : null; + return result; + } + } + + static joinPath(uri: URI, ...pathFragment: string[]): URI { + if (!uri.path) { + throw new Error(`[UriError]: cannot call joinPaths on URI without path`); + } + let newPath: string; + if (isWindows && uri.scheme === 'file') { + newPath = URI.file(path.join(uri.fsPath, ...pathFragment)).path; + } else { + newPath = path.join(uri.path, ...pathFragment); + } + return uri.with({ path: newPath }); + } + } + + export interface UriComponents { + scheme: string; + authority: string; + path: string; + query: string; + fragment: string; + } + + interface UriState extends UriComponents { + $mid: number; + external: string; + fsPath: string; + _sep: 1 | undefined; + } + + const _pathSepMarker = isWindows ? 1 : undefined; + + // tslint:disable-next-line:class-name + class _URI extends URI { + _formatted: string | null = null; + _fsPath: string | null = null; + constructor( + schemeOrData: string | UriComponents, + authority?: string, + path?: string, + query?: string, + fragment?: string, + _strict: boolean = false + ) { + super(schemeOrData as any, authority, path, query, fragment, _strict); + this._fsPath = this.fsPath; + } + get fsPath(): string { + if (!this._fsPath) { + this._fsPath = _makeFsPath(this); + } + return this._fsPath; + } + + toString(skipEncoding: boolean = false): string { + if (!skipEncoding) { + if (!this._formatted) { + this._formatted = _asFormatted(this, false); + } + return this._formatted; + } else { + // we don't cache that + return _asFormatted(this, true); + } + } + + toJSON(): UriComponents { + const res = <UriState>{ + $mid: 1 + }; + // cached state + if (this._fsPath) { + res.fsPath = this._fsPath; + if (_pathSepMarker) { + res._sep = _pathSepMarker; + } + } + if (this._formatted) { + res.external = this._formatted; + } + // uri components + if (this.path) { + res.path = this.path; + } + if (this.scheme) { + res.scheme = this.scheme; + } + if (this.authority) { + res.authority = this.authority; + } + if (this.query) { + res.query = this.query; + } + if (this.fragment) { + res.fragment = this.fragment; + } + return res; + } + } + + // reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2 + const encodeTable: { [ch: number]: string } = { + [CharCode.Colon]: '%3A', // gen-delims + [CharCode.Slash]: '%2F', + [CharCode.QuestionMark]: '%3F', + [CharCode.Hash]: '%23', + [CharCode.OpenSquareBracket]: '%5B', + [CharCode.CloseSquareBracket]: '%5D', + [CharCode.AtSign]: '%40', + + [CharCode.ExclamationMark]: '%21', // sub-delims + [CharCode.DollarSign]: '%24', + [CharCode.Ampersand]: '%26', + [CharCode.SingleQuote]: '%27', + [CharCode.OpenParen]: '%28', + [CharCode.CloseParen]: '%29', + [CharCode.Asterisk]: '%2A', + [CharCode.Plus]: '%2B', + [CharCode.Comma]: '%2C', + [CharCode.Semicolon]: '%3B', + [CharCode.Equals]: '%3D', + + [CharCode.Space]: '%20' + }; + + function encodeURIComponentFast(uriComponent: string, allowSlash: boolean): string { + let res: string | undefined = undefined; + let nativeEncodePos = -1; + + for (let pos = 0; pos < uriComponent.length; pos++) { + const code = uriComponent.charCodeAt(pos); + + // unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3 + if ( + (code >= CharCode.a && code <= CharCode.z) || + (code >= CharCode.A && code <= CharCode.Z) || + (code >= CharCode.Digit0 && code <= CharCode.Digit9) || + code === CharCode.Dash || + code === CharCode.Period || + code === CharCode.Underline || + code === CharCode.Tilde || + (allowSlash && code === CharCode.Slash) + ) { + // check if we are delaying native encode + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos)); + nativeEncodePos = -1; + } + // check if we write into a new string (by default we try to return the param) + if (res !== undefined) { + res += uriComponent.charAt(pos); + } + } else { + // encoding needed, we need to allocate a new string + if (res === undefined) { + res = uriComponent.substr(0, pos); + } + + // check with default table first + const escaped = encodeTable[code]; + if (escaped !== undefined) { + // check if we are delaying native encode + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos)); + nativeEncodePos = -1; + } + + // append escaped variant to result + res += escaped; + } else if (nativeEncodePos === -1) { + // use native encode only when needed + nativeEncodePos = pos; + } + } + } + + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos)); + } + + return res !== undefined ? res : uriComponent; + } + + function encodeURIComponentMinimal(path: string): string { + let res: string | undefined = undefined; + for (let pos = 0; pos < path.length; pos++) { + const code = path.charCodeAt(pos); + if (code === CharCode.Hash || code === CharCode.QuestionMark) { + if (res === undefined) { + res = path.substr(0, pos); + } + res += encodeTable[code]; + } else { + if (res !== undefined) { + res += path[pos]; + } + } + } + return res !== undefined ? res : path; + } + + /** + * Compute `fsPath` for the given uri + */ + function _makeFsPath(uri: URI): string { + let value: string; + if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') { + // unc path: file://shares/c$/far/boo + value = `//${uri.authority}${uri.path}`; + } else if ( + uri.path.charCodeAt(0) === CharCode.Slash && + ((uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z) || + (uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z)) && + uri.path.charCodeAt(2) === CharCode.Colon + ) { + // windows drive letter: file:///c:/far/boo + value = uri.path[1].toLowerCase() + uri.path.substr(2); + } else { + // other path + value = uri.path; + } + if (isWindows) { + value = value.replace(/\//g, '\\'); + } + return value; + } + + /** + * Create the external version of a uri + */ + function _asFormatted(uri: URI, skipEncoding: boolean): string { + const encoder = !skipEncoding ? encodeURIComponentFast : encodeURIComponentMinimal; + + let res = ''; + let { scheme, authority, path, query, fragment } = uri; + if (scheme) { + res += scheme; + res += ':'; + } + if (authority || scheme === 'file') { + res += _slash; + res += _slash; + } + if (authority) { + let idx = authority.indexOf('@'); + if (idx !== -1) { + // <user>@<auth> + const userinfo = authority.substr(0, idx); + authority = authority.substr(idx + 1); + idx = userinfo.indexOf(':'); + if (idx === -1) { + res += encoder(userinfo, false); + } else { + // <user>:<pass>@<auth> + res += encoder(userinfo.substr(0, idx), false); + res += ':'; + res += encoder(userinfo.substr(idx + 1), false); + } + res += '@'; + } + authority = authority.toLowerCase(); + idx = authority.indexOf(':'); + if (idx === -1) { + res += encoder(authority, false); + } else { + // <auth>:<port> + res += encoder(authority.substr(0, idx), false); + res += authority.substr(idx); + } + } + if (path) { + // lower-case windows drive letters in /C:/fff or C:/fff + if (path.length >= 3 && path.charCodeAt(0) === CharCode.Slash && path.charCodeAt(2) === CharCode.Colon) { + const code = path.charCodeAt(1); + if (code >= CharCode.A && code <= CharCode.Z) { + path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3 + } + } else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) { + const code = path.charCodeAt(0); + if (code >= CharCode.A && code <= CharCode.Z) { + path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3 + } + } + // encode the rest of the path + res += encoder(path, true); + } + if (query) { + res += '?'; + res += encoder(query, false); + } + if (fragment) { + res += '#'; + res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment; + } + return res; + } +} diff --git a/src/test/mocks/vsc/uuid.ts b/src/test/mocks/vsc/uuid.ts new file mode 100644 index 000000000000..2ddb98481445 --- /dev/null +++ b/src/test/mocks/vsc/uuid.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +/** + * Represents a UUID as defined by rfc4122. + */ +// tslint:disable-next-line: interface-name +export interface UUID { + /** + * @returns the canonical representation in sets of hexadecimal numbers separated by dashes. + */ + asHex(): string; +} + +class ValueUUID implements UUID { + constructor(public _value: string) { + // empty + } + + public asHex(): string { + return this._value; + } +} + +class V4UUID extends ValueUUID { + private static readonly _chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + + private static readonly _timeHighBits = ['8', '9', 'a', 'b']; + + private static _oneOf(array: string[]): string { + // tslint:disable:insecure-random + return array[Math.floor(array.length * Math.random())]; + } + + private static _randomHex(): string { + return V4UUID._oneOf(V4UUID._chars); + } + + // tslint:disable-next-line: member-ordering + constructor() { + super( + [ + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + '4', + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + V4UUID._oneOf(V4UUID._timeHighBits), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex() + ].join('') + ); + } +} + +export function v4(): UUID { + return new V4UUID(); +} + +const _UUIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export function isUUID(value: string): boolean { + return _UUIDPattern.test(value); +} + +/** + * Parses a UUID that is of the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. + * @param value A uuid string. + */ +export function parse(value: string): UUID { + if (!isUUID(value)) { + throw new Error('invalid uuid'); + } + + return new ValueUUID(value); +} + +export function generateUuid(): string { + return v4().asHex(); +} diff --git a/src/test/multiRootTest.ts b/src/test/multiRootTest.ts new file mode 100644 index 000000000000..5859708a8e99 --- /dev/null +++ b/src/test/multiRootTest.ts @@ -0,0 +1,28 @@ +// tslint:disable:no-console + +import * as path from 'path'; +import { runTests } from 'vscode-test'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; +import { initializeLogger } from './testLogger'; + +const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); +process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; +process.env.VSC_PYTHON_CI_TEST = '1'; + +initializeLogger(); + +function start() { + console.log('*'.repeat(100)); + console.log('Start Multiroot tests'); + runTests({ + extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, + extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), + launchArgs: [workspacePath], + version: 'stable', + extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' } + }).catch((ex) => { + console.error('End Multiroot tests (with errors)', ex); + process.exit(1); + }); +} +start(); diff --git a/src/test/multiRootWkspc/disableLinters/.vscode/tags b/src/test/multiRootWkspc/disableLinters/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/test/multiRootWkspc/disableLinters/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/multiRootWkspc/disableLinters/file.py b/src/test/multiRootWkspc/disableLinters/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/disableLinters/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/multi.code-workspace b/src/test/multiRootWkspc/multi.code-workspace new file mode 100644 index 000000000000..6a9901d9df65 --- /dev/null +++ b/src/test/multiRootWkspc/multi.code-workspace @@ -0,0 +1,30 @@ +{ + "folders": [ + { + "path": "workspace1" + }, + { + "path": "workspace2" + }, + { + "path": "workspace3" + }, + { + "path": "parent\\child" + }, + { + "path": "disableLinters" + } + ], + "settings": { + "python.linting.flake8Enabled": false, + "python.linting.banditEnabled": true, + "python.linting.mypyEnabled": true, + "python.linting.pydocstyleEnabled": true, + "python.linting.pylamaEnabled": true, + "python.linting.pylintEnabled": false, + "python.linting.pycodestyleEnabled": true, + "python.linting.prospectorEnabled": true, + "python.workspaceSymbols.enabled": true + } +} diff --git a/src/test/multiRootWkspc/parent/child/.vscode/settings.json b/src/test/multiRootWkspc/parent/child/.vscode/settings.json new file mode 100644 index 000000000000..656ad4032082 --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.enabled": true +} diff --git a/src/test/multiRootWkspc/parent/child/.vscode/tags b/src/test/multiRootWkspc/parent/child/.vscode/tags new file mode 100644 index 000000000000..e6791c755b0f --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/.vscode/tags @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Child2Class ..\\childFile.py /^class Child2Class(object):$/;" kind:class line:5 +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\childFile.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\childFile.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +childFile.py ..\\childFile.py 1;" kind:file line:1 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfChild ..\\childFile.py /^ def meth1OfChild(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/multiRootWkspc/parent/child/childFile.py b/src/test/multiRootWkspc/parent/child/childFile.py new file mode 100644 index 000000000000..31d6fc7b4a18 --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/childFile.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Child2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfChild(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/multiRootWkspc/parent/child/file.py b/src/test/multiRootWkspc/parent/child/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/parent/child/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace1/.vscode/settings.json b/src/test/multiRootWkspc/workspace1/.vscode/settings.json new file mode 100644 index 000000000000..a783cfe01962 --- /dev/null +++ b/src/test/multiRootWkspc/workspace1/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.linting.pylintEnabled": true, + "python.linting.enabled": false, + "python.linting.flake8Enabled": true +} diff --git a/src/test/multiRootWkspc/workspace1/.vscode/tags b/src/test/multiRootWkspc/workspace1/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/test/multiRootWkspc/workspace1/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/multiRootWkspc/workspace1/file.py b/src/test/multiRootWkspc/workspace1/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/workspace1/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace2/.vscode/settings.json b/src/test/multiRootWkspc/workspace2/.vscode/settings.json new file mode 100644 index 000000000000..385728982cfa --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace2.tags.file", + "python.workspaceSymbols.enabled": true +} diff --git a/src/test/multiRootWkspc/workspace2/file.py b/src/test/multiRootWkspc/workspace2/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace2/workspace2.tags.file b/src/test/multiRootWkspc/workspace2/workspace2.tags.file new file mode 100644 index 000000000000..2d54e7ed7c7b --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/workspace2.tags.file @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^class Foo(object):$/;" kind:class line:5 +Workspace2Class C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^class Workspace2Class(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfWorkspace2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py /^ def meth1OfWorkspace2(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\file.py /^ def meth8(self):$/;" kind:member line:80 +workspace2File.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace2\\workspace2File.py 1;" kind:file line:1 diff --git a/src/test/multiRootWkspc/workspace2/workspace2File.py b/src/test/multiRootWkspc/workspace2/workspace2File.py new file mode 100644 index 000000000000..61aa87c55fed --- /dev/null +++ b/src/test/multiRootWkspc/workspace2/workspace2File.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Workspace2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfWorkspace2(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/multiRootWkspc/workspace3/.vscode/settings.json b/src/test/multiRootWkspc/workspace3/.vscode/settings.json new file mode 100644 index 000000000000..8779a0c08efe --- /dev/null +++ b/src/test/multiRootWkspc/workspace3/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace3.tags.file" +} diff --git a/src/test/multiRootWkspc/workspace3/file.py b/src/test/multiRootWkspc/workspace3/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/multiRootWkspc/workspace3/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/multiRootWkspc/workspace3/workspace3.tags.file b/src/test/multiRootWkspc/workspace3/workspace3.tags.file new file mode 100644 index 000000000000..9a141392d6ae --- /dev/null +++ b/src/test/multiRootWkspc/workspace3/workspace3.tags.file @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\test\\multiRootWkspc\\workspace3\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/test/performance/load.perf.test.ts b/src/test/performance/load.perf.test.ts new file mode 100644 index 000000000000..05e3ff90627a --- /dev/null +++ b/src/test/performance/load.perf.test.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this no-console + +import { expect } from 'chai'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import * as path from 'path'; +import { commands, extensions } from 'vscode'; +import { PVSC_EXTENSION_ID } from '../../client/common/constants'; +import { StopWatch } from '../../client/common/utils/stopWatch'; + +const AllowedIncreaseInActivationDelayInMS = 500; + +suite('Activation Times', () => { + if (process.env.ACTIVATION_TIMES_LOG_FILE_PATH) { + const logFile = process.env.ACTIVATION_TIMES_LOG_FILE_PATH; + const sampleCounter = fs.existsSync(logFile) + ? fs.readFileSync(logFile, { encoding: 'utf8' }).toString().split(/\r?\n/g).length + : 1; + if (sampleCounter > 5) { + return; + } + test(`Capture Extension Activation Times (Version: ${process.env.ACTIVATION_TIMES_EXT_VERSION}, sample: ${sampleCounter})`, async () => { + const pythonExtension = extensions.getExtension(PVSC_EXTENSION_ID); + if (!pythonExtension) { + throw new Error('Python Extension not found'); + } + const stopWatch = new StopWatch(); + await pythonExtension!.activate(); + const elapsedTime = stopWatch.elapsedTime; + if (elapsedTime > 10) { + await fs.ensureDir(path.dirname(logFile)); + await fs.appendFile(logFile, `${elapsedTime}${EOL}`, { encoding: 'utf8' }); + console.log(`Loaded in ${elapsedTime}ms`); + } + commands.executeCommand('workbench.action.reloadWindow'); + }); + } + + if ( + process.env.ACTIVATION_TIMES_DEV_LOG_FILE_PATHS && + process.env.ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS && + process.env.ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS + ) { + test('Test activation times of Dev vs Release Extension', async () => { + function getActivationTimes(files: string[]) { + const activationTimes: number[] = []; + for (const file of files) { + fs.readFileSync(file, { encoding: 'utf8' }) + .toString() + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => parseInt(line, 10)) + .forEach((item) => activationTimes.push(item)); + } + return activationTimes; + } + const devActivationTimes = getActivationTimes(JSON.parse(process.env.ACTIVATION_TIMES_DEV_LOG_FILE_PATHS!)); + const releaseActivationTimes = getActivationTimes( + JSON.parse(process.env.ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS!) + ); + const languageServerActivationTimes = getActivationTimes( + JSON.parse(process.env.ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS!) + ); + const devActivationAvgTime = + devActivationTimes.reduce((sum, item) => sum + item, 0) / devActivationTimes.length; + const releaseActivationAvgTime = + releaseActivationTimes.reduce((sum, item) => sum + item, 0) / releaseActivationTimes.length; + const languageServerActivationAvgTime = + languageServerActivationTimes.reduce((sum, item) => sum + item, 0) / + languageServerActivationTimes.length; + + console.log(`Dev version loaded in ${devActivationAvgTime}ms`); + console.log(`Release version loaded in ${releaseActivationAvgTime}ms`); + console.log(`Language server loaded in ${languageServerActivationAvgTime}ms`); + + expect(devActivationAvgTime - releaseActivationAvgTime).to.be.lessThan( + AllowedIncreaseInActivationDelayInMS, + 'Activation times have increased above allowed threshold.' + ); + }); + } +}); diff --git a/src/test/performance/sample.py b/src/test/performance/sample.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/performance/settings.json b/src/test/performance/settings.json new file mode 100644 index 000000000000..ffc9d2a990cd --- /dev/null +++ b/src/test/performance/settings.json @@ -0,0 +1 @@ +{ "python.languageServer": "Jedi" } diff --git a/src/test/performanceTest.ts b/src/test/performanceTest.ts new file mode 100644 index 000000000000..df2208e19481 --- /dev/null +++ b/src/test/performanceTest.ts @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/* +Comparing performance metrics is not easy (the metrics can and always get skewed). +One approach is to run the tests multile times and gather multiple sample data. +For Extension activation times, we load both extensions x times, and re-load the window y times in each x load. +I.e. capture averages by giving the extensions sufficient time to warm up. +This block of code merely launches the tests by using either the dev or release version of the extension, +and spawning the tests (mimic user starting tests from command line), this way we can run tests multiple times. +*/ + +// tslint:disable:no-console no-require-imports no-var-requires + +// Must always be on top to setup expected env. +process.env.VSC_PYTHON_PERF_TEST = '1'; + +import { spawn } from 'child_process'; +import * as download from 'download'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as request from 'request'; +import { LanguageServerType } from '../client/activation/types'; +import { EXTENSION_ROOT_DIR, PVSC_EXTENSION_ID } from '../client/common/constants'; +import { unzip } from './common'; +import { initializeLogger } from './testLogger'; + +initializeLogger(); + +const NamedRegexp = require('named-js-regexp'); +const del = require('del'); + +const tmpFolder = path.join(EXTENSION_ROOT_DIR, 'tmp'); +const publishedExtensionPath = path.join(tmpFolder, 'ext', 'testReleaseExtensionsFolder'); +const logFilesPath = path.join(tmpFolder, 'test', 'logs'); + +enum Version { + Dev, + Release +} + +class TestRunner { + public async start() { + await del([path.join(tmpFolder, '**')]); + await this.extractLatestExtension(publishedExtensionPath); + + const timesToLoadEachVersion = 2; + const devLogFiles: string[] = []; + const releaseLogFiles: string[] = []; + const languageServerLogFiles: string[] = []; + + for (let i = 0; i < timesToLoadEachVersion; i += 1) { + await this.enableLanguageServer(false); + + const devLogFile = path.join(logFilesPath, `dev_loadtimes${i}.txt`); + console.log(`Start Performance Tests: Counter ${i}, for Dev version with Jedi`); + await this.capturePerfTimes(Version.Dev, devLogFile); + devLogFiles.push(devLogFile); + + const releaseLogFile = path.join(logFilesPath, `release_loadtimes${i}.txt`); + console.log(`Start Performance Tests: Counter ${i}, for Release version with Jedi`); + await this.capturePerfTimes(Version.Release, releaseLogFile); + releaseLogFiles.push(releaseLogFile); + + // Language server. + await this.enableLanguageServer(true); + const languageServerLogFile = path.join(logFilesPath, `languageServer_loadtimes${i}.txt`); + console.log(`Start Performance Tests: Counter ${i}, for Release version with language server`); + await this.capturePerfTimes(Version.Release, languageServerLogFile); + languageServerLogFiles.push(languageServerLogFile); + } + + console.log('Compare Performance Results'); + await this.runPerfTest(devLogFiles, releaseLogFiles, languageServerLogFiles); + } + private async enableLanguageServer(enable: boolean) { + const settings = `{ "python.languageServer": "${ + enable ? LanguageServerType.Microsoft : LanguageServerType.Jedi + }" }`; + await fs.writeFile(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'performance', 'settings.json'), settings); + } + + private async capturePerfTimes(version: Version, logFile: string) { + const releaseVersion = await this.getReleaseVersion(); + const devVersion = await this.getDevVersion(); + await fs.ensureDir(path.dirname(logFile)); + const env: Record<string, {}> = { + ACTIVATION_TIMES_LOG_FILE_PATH: logFile, + ACTIVATION_TIMES_EXT_VERSION: version === Version.Release ? releaseVersion : devVersion, + CODE_EXTENSIONS_PATH: version === Version.Release ? publishedExtensionPath : EXTENSION_ROOT_DIR + }; + + await this.launchTest(env); + } + private async runPerfTest(devLogFiles: string[], releaseLogFiles: string[], languageServerLogFiles: string[]) { + const env: Record<string, {}> = { + ACTIVATION_TIMES_DEV_LOG_FILE_PATHS: JSON.stringify(devLogFiles), + ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS: JSON.stringify(releaseLogFiles), + ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS: JSON.stringify(languageServerLogFiles) + }; + + await this.launchTest(env); + } + + private async launchTest(customEnvVars: Record<string, {}>) { + await new Promise((resolve, reject) => { + const env: Record<string, string> = { + TEST_FILES_SUFFIX: 'perf.test', + CODE_TESTS_WORKSPACE: path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'performance'), + ...process.env, + ...customEnvVars + }; + + const proc = spawn('node', [path.join(__dirname, 'standardTest.js')], { cwd: EXTENSION_ROOT_DIR, env }); + proc.stdout.pipe(process.stdout); + proc.stderr.pipe(process.stderr); + proc.on('error', reject); + proc.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(`Failed with code ${code}.`); + } + }); + }); + } + + private async extractLatestExtension(targetDir: string): Promise<void> { + const extensionFile = await this.downloadExtension(); + await unzip(extensionFile, targetDir); + } + + private async getReleaseVersion(): Promise<string> { + const url = `https://marketplace.visualstudio.com/items?itemName=${PVSC_EXTENSION_ID}`; + const content = await new Promise<string>((resolve, reject) => { + request(url, (error, response, body) => { + if (error) { + return reject(error); + } + if (response.statusCode === 200) { + return resolve(body); + } + reject(`Status code of ${response.statusCode} received.`); + }); + }); + const re = NamedRegexp('"version"S?:S?"(:<version>\\d{4}\\.\\d{1,2}\\.\\d{1,2})"', 'g'); + const matches = re.exec(content); + return matches.groups().version; + } + + private async getDevVersion(): Promise<string> { + // tslint:disable-next-line:non-literal-require + return require(path.join(EXTENSION_ROOT_DIR, 'package.json')).version; + } + + private async downloadExtension(): Promise<string> { + const version = await this.getReleaseVersion(); + const url = `https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/${version}/vspackage`; + const destination = path.join(__dirname, `extension${version}.zip`); + if (await fs.pathExists(destination)) { + return destination; + } + + await download(url, path.dirname(destination), { filename: path.basename(destination) }); + return destination; + } +} + +new TestRunner().start().catch((ex) => console.error('Error in running Performance Tests', ex)); diff --git a/src/test/pipRequirements/example-requirements.txt b/src/test/pipRequirements/example-requirements.txt new file mode 100644 index 000000000000..bc4af132bdb5 --- /dev/null +++ b/src/test/pipRequirements/example-requirements.txt @@ -0,0 +1,37 @@ +# https://www.python.org/dev/peps/pep-0508/#names +This-name_has.everything42 # End-of-line comment. + +# https://www.python.org/dev/peps/pep-0508/#extras +project[extras] + +# https://www.python.org/dev/peps/pep-0508/#versions +# https://www.python.org/dev/peps/pep-0440/ +project > 2.0.0 +project~=1.0.0 +project>1.0.0,<2.0.0 +project==1.0.0.dev1 + +# https://www.python.org/dev/peps/pep-0508/#environment-markers +project;python_version>'3.0' +project~=2.0.0;os_name=="linux" + +# https://pip.readthedocs.io/en/stable/reference/pip_install/#requirements-file-format +# Continuation line. +project \ + >1.0.0 + +# Options +## Stand-alone w/o argument. +--no-links +## Stand-alone w/ argument. +-c constraints.txt +-e git://git.myproject.org/MyProject#egg=MyProject +## Part of requirement. +FooProject >= 1.2 --global-option="--no-user-cfg" + +# File path. +./some/file +some/file + +# URL. +https://some-site.ca/project.whl diff --git a/src/test/proc.ts b/src/test/proc.ts new file mode 100644 index 000000000000..d674d29b8cc0 --- /dev/null +++ b/src/test/proc.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as cp from 'child_process'; +import { sleep } from '../client/common/utils/async'; + +type OutStream = 'stdout' | 'stderr'; + +export class ProcOutput { + private readonly output: [OutStream, Buffer][] = []; + public get stdout(): string { + return this.dump('stdout'); + } + public get stderr(): string { + return this.dump('stderr'); + } + public get combined(): string { + return this.dump(); + } + public addStdout(data: Buffer) { + this.output.push(['stdout', data]); + } + public addStderr(data: Buffer) { + this.output.push(['stdout', data]); + } + private dump(which?: OutStream) { + let out = ''; + for (const [stream, data] of this.output) { + if (!which || which !== stream) { + continue; + } + out += data.toString(); + } + return out; + } +} + +export type ProcResult = { + exitCode: number; + stdout: string; +}; + +interface IRawProc extends cp.ChildProcess { + // Apparently the type declaration doesn't expose exitCode. + // See: https://nodejs.org/api/child_process.html#child_process_subprocess_exitcode + exitCode: number | null; +} + +export class Proc { + public readonly raw: IRawProc; + private readonly output: ProcOutput; + private result: ProcResult | undefined; + constructor(raw: cp.ChildProcess, output: ProcOutput) { + this.raw = (raw as unknown) as IRawProc; + this.output = output; + } + public get pid(): number { + return this.raw.pid; + } + public get exited(): boolean { + return this.raw.exitCode !== null; + } + public async waitUntilDone(): Promise<ProcResult> { + if (this.result) { + return this.result; + } + while (this.raw.exitCode === null) { + await sleep(10); // milliseconds + } + this.result = { + exitCode: this.raw.exitCode, + stdout: this.output.stdout + }; + return this.result; + } +} + +export function spawn(executable: string, ...args: string[]) { + // Un-comment this to see the executed command: + //console.log(`|${executable} ${args.join(' ')}|`); + const output = new ProcOutput(); + const raw = cp.spawn(executable, args); + raw.stdout.on('data', (data: Buffer) => output.addStdout(data)); + raw.stderr.on('data', (data: Buffer) => output.addStderr(data)); + return new Proc(raw, output); +} diff --git a/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts b/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts new file mode 100644 index 000000000000..136271b3e4e5 --- /dev/null +++ b/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { CodeActionContext, CodeActionKind, Diagnostic, Range, TextDocument, Uri } from 'vscode'; +import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; + +suite('LaunchJson CodeAction Provider', () => { + const documentUri = Uri.parse('a'); + let document: TypeMoq.IMock<TextDocument>; + let range: TypeMoq.IMock<Range>; + let context: TypeMoq.IMock<CodeActionContext>; + let diagnostic: TypeMoq.IMock<Diagnostic>; + let codeActionsProvider: LaunchJsonCodeActionProvider; + + setup(() => { + codeActionsProvider = new LaunchJsonCodeActionProvider(); + document = TypeMoq.Mock.ofType<TextDocument>(); + range = TypeMoq.Mock.ofType<Range>(); + context = TypeMoq.Mock.ofType<CodeActionContext>(); + diagnostic = TypeMoq.Mock.ofType<Diagnostic>(); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => 'Diagnostic text'); + document.setup((d) => d.uri).returns(() => documentUri); + context.setup((c) => c.diagnostics).returns(() => [diagnostic.object]); + }); + + test('Ensure correct code action is returned if diagnostic message equals `Incorrect type. Expected "string".`', async () => { + diagnostic.setup((d) => d.message).returns(() => 'Incorrect type. Expected "string".'); + diagnostic.setup((d) => d.range).returns(() => new Range(2, 0, 7, 8)); + + const codeActions = codeActionsProvider.provideCodeActions(document.object, range.object, context.object); + + // Now ensure that the code action object is as expected + expect(codeActions).to.have.length(1); + expect(codeActions[0].kind).to.eq(CodeActionKind.QuickFix); + expect(codeActions[0].title).to.equal('Convert to "Diagnostic text"'); + + // Ensure the correct TextEdit is provided + const entries = codeActions[0].edit!.entries(); + // Edits the correct document is edited + assert.deepEqual(entries[0][0], documentUri); + const edit = entries[0][1][0]; + // Final text is as expected + expect(edit.newText).to.equal('"Diagnostic text"'); + // Text edit range is as expected + expect(edit.range.isEqual(new Range(2, 0, 7, 8))).to.equal(true, 'Text edit range not as expected'); + }); + + test('Ensure no code action is returned if diagnostic message does not equal `Incorrect type. Expected "string".`', async () => { + diagnostic.setup((d) => d.message).returns(() => 'Random diagnostic message'); + + const codeActions = codeActionsProvider.provideCodeActions(document.object, range.object, context.object); + + expect(codeActions).to.have.length(0); + }); +}); diff --git a/src/test/providers/codeActionProvider/main.unit.test.ts b/src/test/providers/codeActionProvider/main.unit.test.ts new file mode 100644 index 000000000000..a29b78fcd4fb --- /dev/null +++ b/src/test/providers/codeActionProvider/main.unit.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable: match-default-export-name +import { assert, expect } from 'chai'; +import rewiremock from 'rewiremock'; +import * as typemoq from 'typemoq'; +import { CodeActionProvider, CodeActionProviderMetadata, DocumentSelector } from 'vscode'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; +import { CodeActionProviderService } from '../../../client/providers/codeActionProvider/main'; + +suite('Code Action Provider service', async () => { + setup(() => { + rewiremock.disable(); + }); + test('Code actions are registered correctly', async () => { + let selector: DocumentSelector; + let provider: CodeActionProvider; + let metadata: CodeActionProviderMetadata; + const vscodeMock = { + languages: { + registerCodeActionsProvider: ( + _selector: DocumentSelector, + _provider: CodeActionProvider, + _metadata: CodeActionProviderMetadata + ) => { + selector = _selector; + provider = _provider; + metadata = _metadata; + } + }, + CodeActionKind: { + QuickFix: 'CodeAction' + } + }; + rewiremock.enable(); + rewiremock('vscode').with(vscodeMock); + const quickFixService = new CodeActionProviderService(typemoq.Mock.ofType<IDisposableRegistry>().object); + + await quickFixService.activate(); + + // Ensure QuickFixLaunchJson is registered with correct arguments + assert.deepEqual(selector!, { + scheme: 'file', + language: 'jsonc', + pattern: '**/launch.json' + }); + assert.deepEqual(metadata!, { + // tslint:disable-next-line:no-any + providedCodeActionKinds: ['CodeAction' as any] + }); + expect(provider!).instanceOf(LaunchJsonCodeActionProvider); + }); +}); diff --git a/src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts b/src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts new file mode 100644 index 000000000000..e6fad21c7750 --- /dev/null +++ b/src/test/providers/codeActionProvider/pythonCodeActionsProvider.unit.test.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { CancellationToken, CodeActionContext, CodeActionKind, Range, TextDocument, Uri } from 'vscode'; +import { PythonCodeActionProvider } from '../../../client/providers/codeActionProvider/pythonCodeActionProvider'; + +suite('Python CodeAction Provider', () => { + let codeActionsProvider: PythonCodeActionProvider; + let document: TypeMoq.IMock<TextDocument>; + let range: TypeMoq.IMock<Range>; + let context: TypeMoq.IMock<CodeActionContext>; + let token: TypeMoq.IMock<CancellationToken>; + + setup(() => { + codeActionsProvider = new PythonCodeActionProvider(); + document = TypeMoq.Mock.ofType<TextDocument>(); + range = TypeMoq.Mock.ofType<Range>(); + context = TypeMoq.Mock.ofType<CodeActionContext>(); + token = TypeMoq.Mock.ofType<CancellationToken>(); + }); + + test('Ensure it always returns a source.organizeImports CodeAction', async () => { + document.setup((d) => d.uri).returns(() => Uri.file('hello.ipynb')); + const codeActions = await codeActionsProvider.provideCodeActions( + document.object, + range.object, + context.object, + token.object + ); + + assert.isArray(codeActions, 'codeActionsProvider.provideCodeActions did not return an array'); + + const organizeImportsCodeAction = (codeActions || []).filter( + (codeAction) => codeAction.kind === CodeActionKind.SourceOrganizeImports + ); + expect(organizeImportsCodeAction).to.have.length(1); + expect(organizeImportsCodeAction[0].kind).to.eq(CodeActionKind.SourceOrganizeImports); + }); + test('Ensure it does not returns a source.organizeImports CodeAction for Notebook Cells', async () => { + document.setup((d) => d.uri).returns(() => Uri.file('hello.ipynb').with({ scheme: 'vscode-notebook-cell' })); + const codeActions = await codeActionsProvider.provideCodeActions( + document.object, + range.object, + context.object, + token.object + ); + + assert.isArray(codeActions, 'codeActionsProvider.provideCodeActions did not return an array'); + + const organizeImportsCodeAction = (codeActions || []).filter( + (codeAction) => codeAction.kind === CodeActionKind.SourceOrganizeImports + ); + expect(organizeImportsCodeAction).to.have.length(0); + }); +}); diff --git a/src/test/providers/foldingProvider.test.ts b/src/test/providers/foldingProvider.test.ts new file mode 100644 index 000000000000..b4fec804ea39 --- /dev/null +++ b/src/test/providers/foldingProvider.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import { CancellationTokenSource, FoldingRange, FoldingRangeKind, workspace } from 'vscode'; +import { DocStringFoldingProvider } from '../../client/providers/docStringFoldingProvider'; + +type FileFoldingRanges = { file: string; ranges: FoldingRange[] }; +const pythonFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'folding'); + +// tslint:disable-next-line:max-func-body-length +suite('Provider - Folding Provider', () => { + const docStringFileAndExpectedFoldingRanges: FileFoldingRanges[] = [ + { + file: path.join(pythonFilesPath, 'attach_server.py'), + ranges: [ + new FoldingRange(0, 14), + new FoldingRange(44, 73, FoldingRangeKind.Comment), + new FoldingRange(98, 146), + new FoldingRange(152, 153, FoldingRangeKind.Comment), + new FoldingRange(312, 320), + new FoldingRange(327, 329) + ] + }, + { + file: path.join(pythonFilesPath, 'visualstudio_ipython_repl.py'), + ranges: [ + new FoldingRange(0, 14), + new FoldingRange(78, 79, FoldingRangeKind.Comment), + new FoldingRange(81, 82, FoldingRangeKind.Comment), + new FoldingRange(92, 93, FoldingRangeKind.Comment), + new FoldingRange(108, 109, FoldingRangeKind.Comment), + new FoldingRange(139, 140, FoldingRangeKind.Comment), + new FoldingRange(169, 170, FoldingRangeKind.Comment), + new FoldingRange(275, 277, FoldingRangeKind.Comment), + new FoldingRange(319, 320, FoldingRangeKind.Comment) + ] + }, + { + file: path.join(pythonFilesPath, 'visualstudio_py_debugger.py'), + ranges: [ + new FoldingRange(0, 15, FoldingRangeKind.Comment), + new FoldingRange(22, 25, FoldingRangeKind.Comment), + new FoldingRange(47, 48, FoldingRangeKind.Comment), + new FoldingRange(69, 70, FoldingRangeKind.Comment), + new FoldingRange(96, 97, FoldingRangeKind.Comment), + new FoldingRange(105, 106, FoldingRangeKind.Comment), + new FoldingRange(141, 142, FoldingRangeKind.Comment), + new FoldingRange(149, 162, FoldingRangeKind.Comment), + new FoldingRange(165, 166, FoldingRangeKind.Comment), + new FoldingRange(207, 208, FoldingRangeKind.Comment), + new FoldingRange(235, 237, FoldingRangeKind.Comment), + new FoldingRange(240, 241, FoldingRangeKind.Comment), + new FoldingRange(300, 301, FoldingRangeKind.Comment), + new FoldingRange(334, 335, FoldingRangeKind.Comment), + new FoldingRange(346, 348, FoldingRangeKind.Comment), + new FoldingRange(499, 500, FoldingRangeKind.Comment), + new FoldingRange(558, 559, FoldingRangeKind.Comment), + new FoldingRange(602, 604, FoldingRangeKind.Comment), + new FoldingRange(608, 609, FoldingRangeKind.Comment), + new FoldingRange(612, 614, FoldingRangeKind.Comment), + new FoldingRange(637, 638, FoldingRangeKind.Comment) + ] + }, + { + file: path.join(pythonFilesPath, 'visualstudio_py_repl.py'), + ranges: [] + } + ]; + + docStringFileAndExpectedFoldingRanges.forEach((item) => { + test(`Test Docstring folding regions '${path.basename(item.file)}'`, async () => { + const document = await workspace.openTextDocument(item.file); + const provider = new DocStringFoldingProvider(); + const ranges = await provider.provideFoldingRanges(document, {}, new CancellationTokenSource().token); + expect(ranges).to.be.lengthOf(item.ranges.length); + ranges!.forEach((range) => { + const index = item.ranges.findIndex( + (searchItem) => searchItem.start === range.start && searchItem.end === range.end + ); + expect(index).to.be.greaterThan(-1, `${range.start}, ${range.end} not found`); + }); + }); + }); +}); diff --git a/src/test/providers/importSortProvider.unit.test.ts b/src/test/providers/importSortProvider.unit.test.ts new file mode 100644 index 000000000000..07ed714f082d --- /dev/null +++ b/src/test/providers/importSortProvider.unit.test.ts @@ -0,0 +1,696 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { assert, expect } from 'chai'; +import { ChildProcess } from 'child_process'; +import { EOL } from 'os'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import { Subscriber } from 'rxjs/Subscriber'; +import * as sinon from 'sinon'; +import { Writable } from 'stream'; +import * as TypeMoq from 'typemoq'; +import { Range, TextDocument, TextEditor, TextLine, Uri, WorkspaceEdit } from 'vscode'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../../client/common/application/types'; +import { Commands, EXTENSION_ROOT_DIR, STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; +import { ProcessService } from '../../client/common/process/proc'; +import { + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonExecutionService, + Output +} from '../../client/common/process/types'; +import { + IConfigurationService, + IDisposableRegistry, + IEditorUtils, + IOutputChannel, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, + ISortImportSettings +} from '../../client/common/types'; +import { createDeferred, createDeferredFromPromise } from '../../client/common/utils/async'; +import { Common, Diagnostics } from '../../client/common/utils/localize'; +import { noop } from '../../client/common/utils/misc'; +import { IServiceContainer } from '../../client/ioc/types'; +import { SortImportsEditingProvider } from '../../client/providers/importSortProvider'; +import { sleep } from '../core'; + +const ISOLATED = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'pyvsc-run-isolated.py'); + +suite('Import Sort Provider', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let shell: TypeMoq.IMock<IApplicationShell>; + let documentManager: TypeMoq.IMock<IDocumentManager>; + let configurationService: TypeMoq.IMock<IConfigurationService>; + let pythonExecFactory: TypeMoq.IMock<IPythonExecutionFactory>; + let processServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; + let editorUtils: TypeMoq.IMock<IEditorUtils>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let persistentStateFactory: TypeMoq.IMock<IPersistentStateFactory>; + let output: TypeMoq.IMock<IOutputChannel>; + let sortProvider: SortImportsEditingProvider; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + shell = TypeMoq.Mock.ofType<IApplicationShell>(); + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + pythonExecFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + editorUtils = TypeMoq.Mock.ofType<IEditorUtils>(); + persistentStateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + output = TypeMoq.Mock.ofType<IOutputChannel>(); + serviceContainer.setup((c) => c.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL)).returns(() => output.object); + serviceContainer.setup((c) => c.get(IPersistentStateFactory)).returns(() => persistentStateFactory.object); + serviceContainer.setup((c) => c.get(ICommandManager)).returns(() => commandManager.object); + serviceContainer.setup((c) => c.get(IDocumentManager)).returns(() => documentManager.object); + serviceContainer.setup((c) => c.get(IApplicationShell)).returns(() => shell.object); + serviceContainer.setup((c) => c.get(IConfigurationService)).returns(() => configurationService.object); + serviceContainer.setup((c) => c.get(IPythonExecutionFactory)).returns(() => pythonExecFactory.object); + serviceContainer.setup((c) => c.get(IProcessServiceFactory)).returns(() => processServiceFactory.object); + serviceContainer.setup((c) => c.get(IEditorUtils)).returns(() => editorUtils.object); + serviceContainer.setup((c) => c.get(IDisposableRegistry)).returns(() => []); + configurationService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + sortProvider = new SortImportsEditingProvider(serviceContainer.object); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Ensure command is registered', () => { + commandManager + .setup((c) => + c.registerCommand( + TypeMoq.It.isValue(Commands.Sort_Imports), + TypeMoq.It.isAny(), + TypeMoq.It.isValue(sortProvider) + ) + ) + .verifiable(TypeMoq.Times.once()); + + sortProvider.registerCommands(); + commandManager.verifyAll(); + }); + test("Ensure message is displayed when no doc is opened and uri isn't provided", async () => { + documentManager + .setup((d) => d.activeTextEditor) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + shell + .setup((s) => s.showErrorMessage(TypeMoq.It.isValue('Please open a Python file to sort the imports.'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + await sortProvider.sortImports(); + + shell.verifyAll(); + documentManager.verifyAll(); + }); + test("Ensure message is displayed when uri isn't provided and current doc is non-python", async () => { + const mockEditor = TypeMoq.Mock.ofType<TextEditor>(); + const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); + mockDoc + .setup((d) => d.languageId) + .returns(() => 'xyz') + .verifiable(TypeMoq.Times.atLeastOnce()); + mockEditor + .setup((d) => d.document) + .returns(() => mockDoc.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + + documentManager + .setup((d) => d.activeTextEditor) + .returns(() => mockEditor.object) + .verifiable(TypeMoq.Times.once()); + shell + .setup((s) => s.showErrorMessage(TypeMoq.It.isValue('Please open a Python file to sort the imports.'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + await sortProvider.sortImports(); + + mockEditor.verifyAll(); + mockDoc.verifyAll(); + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure document is opened', async () => { + const uri = Uri.file('TestDoc'); + + documentManager + .setup((d) => d.openTextDocument(TypeMoq.It.isValue(uri))) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager.setup((d) => d.activeTextEditor).verifiable(TypeMoq.Times.never()); + shell + .setup((s) => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + await sortProvider.sortImports(uri).catch(noop); + + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure no edits are provided when there is only one line', async () => { + const uri = Uri.file('TestDoc'); + const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); + // tslint:disable-next-line:no-any + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc + .setup((d) => d.lineCount) + .returns(() => 1) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup((d) => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + shell + .setup((s) => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + const edit = await sortProvider.sortImports(uri); + + expect(edit).to.be.equal(undefined, 'not undefined'); + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure no edits are provided when there are no lines', async () => { + const uri = Uri.file('TestDoc'); + const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); + // tslint:disable-next-line:no-any + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc + .setup((d) => d.lineCount) + .returns(() => 0) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup((d) => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + shell + .setup((s) => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + const edit = await sortProvider.sortImports(uri); + + expect(edit).to.be.equal(undefined, 'not undefined'); + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure empty line is added when line does not end with an empty line', async () => { + const uri = Uri.file('TestDoc'); + const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc + .setup((d) => d.lineCount) + .returns(() => 10) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const lastLine = TypeMoq.Mock.ofType<TextLine>(); + let editApplied: WorkspaceEdit | undefined; + lastLine + .setup((l) => l.text) + .returns(() => '1234') + .verifiable(TypeMoq.Times.atLeastOnce()); + lastLine + .setup((l) => l.range) + .returns(() => new Range(1, 0, 10, 1)) + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc + .setup((d) => d.lineAt(TypeMoq.It.isValue(9))) + .returns(() => lastLine.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup((d) => d.applyEdit(TypeMoq.It.isAny())) + .callback((e) => (editApplied = e)) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup((d) => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + shell + .setup((s) => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + sortProvider.provideDocumentSortImportsEdits = () => Promise.resolve(undefined); + await sortProvider.sortImports(uri); + + expect(editApplied).not.to.be.equal(undefined, 'Applied edit is undefined'); + expect(editApplied!.entries()).to.be.lengthOf(1); + expect(editApplied!.entries()[0][1]).to.be.lengthOf(1); + expect(editApplied!.entries()[0][1][0].newText).to.be.equal(EOL); + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure no edits are provided when there is only one line (when using provider method)', async () => { + const uri = Uri.file('TestDoc'); + const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc + .setup((d) => d.lineCount) + .returns(() => 1) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup((d) => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + shell + .setup((s) => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + const edit = await sortProvider.provideDocumentSortImportsEdits(uri); + + expect(edit).to.be.equal(undefined, 'not undefined'); + shell.verifyAll(); + documentManager.verifyAll(); + }); + + test('Ensure no edits are provided when there are no lines (when using provider method)', async () => { + const uri = Uri.file('TestDoc'); + const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc + .setup((d) => d.lineCount) + .returns(() => 0) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup((d) => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + shell + .setup((s) => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + const edit = await sortProvider.provideDocumentSortImportsEdits(uri); + + expect(edit).to.be.equal(undefined, 'not undefined'); + shell.verifyAll(); + documentManager.verifyAll(); + }); + + test('Ensure stdin is used for sorting (with custom isort path)', async () => { + const uri = Uri.file('something.py'); + const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); + const processService = TypeMoq.Mock.ofType<ProcessService>(); + processService.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc + .setup((d) => d.lineCount) + .returns(() => 10) + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc + .setup((d) => d.getText(TypeMoq.It.isAny())) + .returns(() => 'Hello') + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc + .setup((d) => d.isDirty) + .returns(() => true) + .verifiable(TypeMoq.Times.never()); + mockDoc + .setup((d) => d.uri) + .returns(() => uri) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup((d) => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + pythonSettings + .setup((s) => s.sortImports) + .returns(() => { + return ({ path: 'CUSTOM_ISORT', args: ['1', '2'] } as any) as ISortImportSettings; + }) + .verifiable(TypeMoq.Times.once()); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)) + .verifiable(TypeMoq.Times.once()); + + let actualSubscriber: Subscriber<Output<string>>; + const stdinStream = TypeMoq.Mock.ofType<Writable>(); + stdinStream.setup((s) => s.write('Hello')).verifiable(TypeMoq.Times.once()); + stdinStream + .setup((s) => s.end()) + .callback(() => { + actualSubscriber.next({ source: 'stdout', out: 'DIFF' }); + actualSubscriber.complete(); + }) + .verifiable(TypeMoq.Times.once()); + const childProcess = TypeMoq.Mock.ofType<ChildProcess>(); + childProcess.setup((p) => p.stdin).returns(() => stdinStream.object); + const executionResult = { + proc: childProcess.object, + out: new Observable<Output<string>>((subscriber) => (actualSubscriber = subscriber)), + dispose: noop + }; + const expectedArgs = ['-', '--diff', '1', '2']; + processService + .setup((p) => + p.execObservable( + TypeMoq.It.isValue('CUSTOM_ISORT'), + TypeMoq.It.isValue(expectedArgs), + TypeMoq.It.isValue({ token: undefined, cwd: path.sep }) + ) + ) + .returns(() => executionResult) + .verifiable(TypeMoq.Times.once()); + const expectedEdit = new WorkspaceEdit(); + editorUtils + .setup((e) => + e.getWorkspaceEditsFromPatch( + TypeMoq.It.isValue('Hello'), + TypeMoq.It.isValue('DIFF'), + TypeMoq.It.isAny() + ) + ) + .returns(() => expectedEdit) + .verifiable(TypeMoq.Times.once()); + + const edit = await sortProvider._provideDocumentSortImportsEdits(uri); + + expect(edit).to.be.equal(expectedEdit); + shell.verifyAll(); + mockDoc.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure stdin is used for sorting', async () => { + const uri = Uri.file('something.py'); + const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); + const processService = TypeMoq.Mock.ofType<ProcessService>(); + processService.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc + .setup((d) => d.lineCount) + .returns(() => 10) + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc + .setup((d) => d.getText(TypeMoq.It.isAny())) + .returns(() => 'Hello') + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc + .setup((d) => d.isDirty) + .returns(() => true) + .verifiable(TypeMoq.Times.never()); + mockDoc + .setup((d) => d.uri) + .returns(() => uri) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup((d) => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + pythonSettings + .setup((s) => s.sortImports) + .returns(() => { + return ({ args: ['1', '2'] } as any) as ISortImportSettings; + }) + .verifiable(TypeMoq.Times.once()); + + const processExeService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + processExeService.setup((p: any) => p.then).returns(() => undefined); + pythonExecFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processExeService.object)) + .verifiable(TypeMoq.Times.once()); + + let actualSubscriber: Subscriber<Output<string>>; + const stdinStream = TypeMoq.Mock.ofType<Writable>(); + stdinStream.setup((s) => s.write('Hello')).verifiable(TypeMoq.Times.once()); + stdinStream + .setup((s) => s.end()) + .callback(() => { + actualSubscriber.next({ source: 'stdout', out: 'DIFF' }); + actualSubscriber.complete(); + }) + .verifiable(TypeMoq.Times.once()); + const childProcess = TypeMoq.Mock.ofType<ChildProcess>(); + childProcess.setup((p) => p.stdin).returns(() => stdinStream.object); + const executionResult = { + proc: childProcess.object, + out: new Observable<Output<string>>((subscriber) => (actualSubscriber = subscriber)), + dispose: noop + }; + const importScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'sortImports.py'); + const expectedArgs = [ISOLATED, importScript, '-', '--diff', '1', '2']; + processExeService + .setup((p) => + p.execObservable( + TypeMoq.It.isValue(expectedArgs), + TypeMoq.It.isValue({ token: undefined, cwd: path.sep }) + ) + ) + .returns(() => executionResult) + .verifiable(TypeMoq.Times.once()); + const expectedEdit = new WorkspaceEdit(); + editorUtils + .setup((e) => + e.getWorkspaceEditsFromPatch( + TypeMoq.It.isValue('Hello'), + TypeMoq.It.isValue('DIFF'), + TypeMoq.It.isAny() + ) + ) + .returns(() => expectedEdit) + .verifiable(TypeMoq.Times.once()); + + const edit = await sortProvider._provideDocumentSortImportsEdits(uri); + + expect(edit).to.be.equal(expectedEdit); + shell.verifyAll(); + mockDoc.verifyAll(); + documentManager.verifyAll(); + }); + + test('If a second sort command is initiated before the execution of first one is finished, discard the result from first isort process', async () => { + // ----------------------Common setup between the 2 commands--------------------------- + const uri = Uri.file('something.py'); + const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); + const processService = TypeMoq.Mock.ofType<ProcessService>(); + processService.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup((d) => d.lineCount).returns(() => 10); + mockDoc.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => 'Hello'); + mockDoc.setup((d) => d.isDirty).returns(() => true); + mockDoc.setup((d) => d.uri).returns(() => uri); + documentManager + .setup((d) => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)); + pythonSettings + .setup((s) => s.sortImports) + .returns(() => { + return ({ path: 'CUSTOM_ISORT', args: [] } as any) as ISortImportSettings; + }); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + const result = new WorkspaceEdit(); + editorUtils + .setup((e) => + e.getWorkspaceEditsFromPatch( + TypeMoq.It.isValue('Hello'), + TypeMoq.It.isValue('DIFF'), + TypeMoq.It.isAny() + ) + ) + .returns(() => result); + + // ----------------------Run the command once---------------------- + let firstSubscriber: Subscriber<Output<string>>; + const firstProcessResult = createDeferred<Output<string> | undefined>(); + const stdinStream1 = TypeMoq.Mock.ofType<Writable>(); + stdinStream1.setup((s) => s.write('Hello')); + stdinStream1 + .setup((s) => s.end()) + .callback(async () => { + // Wait until the process has returned with results + const processResult = await firstProcessResult.promise; + firstSubscriber.next(processResult); + firstSubscriber.complete(); + }) + .verifiable(TypeMoq.Times.once()); + const firstChildProcess = TypeMoq.Mock.ofType<ChildProcess>(); + firstChildProcess.setup((p) => p.stdin).returns(() => stdinStream1.object); + const firstExecutionResult = { + proc: firstChildProcess.object, + out: new Observable<Output<string>>((subscriber) => (firstSubscriber = subscriber)), + dispose: noop + }; + processService + .setup((p) => p.execObservable(TypeMoq.It.isValue('CUSTOM_ISORT'), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => firstExecutionResult); + + // The first execution isn't immediately resolved, so don't wait on the promise + const firstExecutionDeferred = createDeferredFromPromise(sortProvider.provideDocumentSortImportsEdits(uri)); + // Yield control to the first execution, so all the mock setups are used. + await sleep(1); + + // ----------------------Run the command again---------------------- + let secondSubscriber: Subscriber<Output<string>>; + const stdinStream2 = TypeMoq.Mock.ofType<Writable>(); + stdinStream2.setup((s) => s.write('Hello')); + stdinStream2 + .setup((s) => s.end()) + .callback(() => { + // The second process immediately returns with results + secondSubscriber.next({ source: 'stdout', out: 'DIFF' }); + secondSubscriber.complete(); + }) + .verifiable(TypeMoq.Times.once()); + const secondChildProcess = TypeMoq.Mock.ofType<ChildProcess>(); + secondChildProcess.setup((p) => p.stdin).returns(() => stdinStream2.object); + const secondExecutionResult = { + proc: secondChildProcess.object, + out: new Observable<Output<string>>((subscriber) => (secondSubscriber = subscriber)), + dispose: noop + }; + processService.reset(); + processService.setup((d: any) => d.then).returns(() => undefined); + processService + .setup((p) => p.execObservable(TypeMoq.It.isValue('CUSTOM_ISORT'), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => secondExecutionResult); + + // // The second execution should immediately return with results + let edit = await sortProvider.provideDocumentSortImportsEdits(uri); + + // ----------------------Verify results---------------------- + expect(edit).to.be.equal(result, 'Second execution result is incorrect'); + expect(firstExecutionDeferred.completed).to.equal(false, "The first execution shouldn't finish yet"); + stdinStream2.verifyAll(); + + // The first process returns with results + firstProcessResult.resolve({ source: 'stdout', out: 'DIFF' }); + + edit = await firstExecutionDeferred.promise; + expect(edit).to.be.equal(undefined, 'The results from the first execution should be discarded'); + stdinStream1.verifyAll(); + }); + + test('If isort raises a warning message related to isort5 upgrade guide, show message', async () => { + const _showWarningAndOptionallyShowOutput = sinon.stub( + SortImportsEditingProvider.prototype, + '_showWarningAndOptionallyShowOutput' + ); + _showWarningAndOptionallyShowOutput.resolves(); + const uri = Uri.file('something.py'); + const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); + const processService = TypeMoq.Mock.ofType<ProcessService>(); + processService.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup((d) => d.lineCount).returns(() => 10); + mockDoc.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => 'Hello'); + mockDoc.setup((d) => d.isDirty).returns(() => true); + mockDoc.setup((d) => d.uri).returns(() => uri); + documentManager + .setup((d) => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)); + pythonSettings + .setup((s) => s.sortImports) + .returns(() => { + return ({ path: 'CUSTOM_ISORT', args: [] } as any) as ISortImportSettings; + }); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + const result = new WorkspaceEdit(); + editorUtils + .setup((e) => + e.getWorkspaceEditsFromPatch( + TypeMoq.It.isValue('Hello'), + TypeMoq.It.isValue('DIFF'), + TypeMoq.It.isAny() + ) + ) + .returns(() => result); + + // ----------------------Run the command---------------------- + let subscriber: Subscriber<Output<string>>; + const stdinStream = TypeMoq.Mock.ofType<Writable>(); + stdinStream.setup((s) => s.write('Hello')); + stdinStream + .setup((s) => s.end()) + .callback(() => { + subscriber.next({ source: 'stdout', out: 'DIFF' }); + subscriber.next({ source: 'stderr', out: 'Some warning related to isort5 (W0503)' }); + subscriber.complete(); + }) + .verifiable(TypeMoq.Times.once()); + const childProcess = TypeMoq.Mock.ofType<ChildProcess>(); + childProcess.setup((p) => p.stdin).returns(() => stdinStream.object); + const executionResult = { + proc: childProcess.object, + out: new Observable<Output<string>>((s) => (subscriber = s)), + dispose: noop + }; + processService.reset(); + processService.setup((d: any) => d.then).returns(() => undefined); + processService + .setup((p) => p.execObservable(TypeMoq.It.isValue('CUSTOM_ISORT'), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => executionResult); + + const edit = await sortProvider.provideDocumentSortImportsEdits(uri); + + // ----------------------Verify results---------------------- + expect(edit).to.be.equal(result, 'Execution result is incorrect'); + assert.ok(_showWarningAndOptionallyShowOutput.calledOnce); + stdinStream.verifyAll(); + }); + + test('If user clicks show output on the isort5 warning prompt, show the Python output', async () => { + const neverShowAgain = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAny(), false)) + .returns(() => neverShowAgain.object); + neverShowAgain.setup((p) => p.value).returns(() => false); + shell + .setup((s) => + s.showWarningMessage( + Diagnostics.checkIsort5UpgradeGuide(), + Common.openOutputPanel(), + Common.doNotShowAgain() + ) + ) + .returns(() => Promise.resolve(Common.openOutputPanel())); + output.setup((o) => o.show(true)).verifiable(TypeMoq.Times.once()); + await sortProvider._showWarningAndOptionallyShowOutput(); + output.verifyAll(); + }); + + test('If user clicks do not show again on the isort5 warning prompt, do not show the prompt again', async () => { + const neverShowAgain = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAny(), false)) + .returns(() => neverShowAgain.object); + let doNotShowAgainValue = false; + neverShowAgain.setup((p) => p.value).returns(() => doNotShowAgainValue); + neverShowAgain + .setup((p) => p.updateValue(true)) + .returns(() => { + doNotShowAgainValue = true; + return Promise.resolve(); + }); + shell + .setup((s) => + s.showWarningMessage( + Diagnostics.checkIsort5UpgradeGuide(), + Common.openOutputPanel(), + Common.doNotShowAgain() + ) + ) + .returns(() => Promise.resolve(Common.doNotShowAgain())) + .verifiable(TypeMoq.Times.once()); + + await sortProvider._showWarningAndOptionallyShowOutput(); + shell.verifyAll(); + + await sortProvider._showWarningAndOptionallyShowOutput(); + await sortProvider._showWarningAndOptionallyShowOutput(); + shell.verifyAll(); + }); +}); diff --git a/src/test/providers/repl.unit.test.ts b/src/test/providers/repl.unit.test.ts new file mode 100644 index 000000000000..31a213f3ac33 --- /dev/null +++ b/src/test/providers/repl.unit.test.ts @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Uri } from 'vscode'; +import { + IActiveResourceService, + ICommandManager, + IDocumentManager, + IWorkspaceService +} from '../../client/common/application/types'; +import { Commands } from '../../client/common/constants'; +import { IServiceContainer } from '../../client/ioc/types'; +import { ReplProvider } from '../../client/providers/replProvider'; +import { ICodeExecutionService } from '../../client/terminals/types'; + +// tslint:disable-next-line:max-func-body-length +suite('REPL Provider', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let workspace: TypeMoq.IMock<IWorkspaceService>; + let codeExecutionService: TypeMoq.IMock<ICodeExecutionService>; + let documentManager: TypeMoq.IMock<IDocumentManager>; + let activeResourceService: TypeMoq.IMock<IActiveResourceService>; + let replProvider: ReplProvider; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + codeExecutionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + activeResourceService = TypeMoq.Mock.ofType<IActiveResourceService>(); + serviceContainer.setup((c) => c.get(ICommandManager)).returns(() => commandManager.object); + serviceContainer.setup((c) => c.get(IWorkspaceService)).returns(() => workspace.object); + serviceContainer + .setup((c) => c.get(ICodeExecutionService, TypeMoq.It.isValue('repl'))) + .returns(() => codeExecutionService.object); + serviceContainer.setup((c) => c.get(IDocumentManager)).returns(() => documentManager.object); + serviceContainer.setup((c) => c.get(IActiveResourceService)).returns(() => activeResourceService.object); + }); + teardown(() => { + try { + replProvider.dispose(); + // tslint:disable-next-line:no-empty + } catch {} + }); + + test('Ensure command is registered', () => { + replProvider = new ReplProvider(serviceContainer.object); + commandManager.verify( + (c) => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once() + ); + }); + + test('Ensure command handler is disposed', () => { + const disposable = TypeMoq.Mock.ofType<Disposable>(); + commandManager + .setup((c) => + c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => disposable.object); + + replProvider = new ReplProvider(serviceContainer.object); + replProvider.dispose(); + + disposable.verify((d) => d.dispose(), TypeMoq.Times.once()); + }); + + test('Ensure execution is carried smoothly in the handler if there are no errors', () => { + const resource = Uri.parse('a'); + const disposable = TypeMoq.Mock.ofType<Disposable>(); + let commandHandler: undefined | (() => void); + commandManager + .setup((c) => + c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns((_cmd, callback) => { + commandHandler = callback; + return disposable.object; + }); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); + + replProvider = new ReplProvider(serviceContainer.object); + expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); + commandHandler!.call(replProvider); + + serviceContainer.verify( + (c) => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('repl')), + TypeMoq.Times.once() + ); + codeExecutionService.verify((c) => c.initializeRepl(TypeMoq.It.isValue(resource)), TypeMoq.Times.once()); + }); +}); diff --git a/src/test/providers/serviceRegistry.unit.test.ts b/src/test/providers/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..7c55645bd473 --- /dev/null +++ b/src/test/providers/serviceRegistry.unit.test.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceManager } from '../../client/ioc/types'; +import { CodeActionProviderService } from '../../client/providers/codeActionProvider/main'; +import { SortImportsEditingProvider } from '../../client/providers/importSortProvider'; +import { registerTypes } from '../../client/providers/serviceRegistry'; +import { ISortImportsEditingProvider } from '../../client/providers/types'; + +suite('Common Providers Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify( + serviceManager.addSingleton<ISortImportsEditingProvider>( + ISortImportsEditingProvider, + SortImportsEditingProvider + ) + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + CodeActionProviderService + ) + ).once(); + }); +}); diff --git a/src/test/providers/shebangCodeLenseProvider.unit.test.ts b/src/test/providers/shebangCodeLenseProvider.unit.test.ts new file mode 100644 index 000000000000..c878cb34a9fa --- /dev/null +++ b/src/test/providers/shebangCodeLenseProvider.unit.test.ts @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { TextDocument, TextLine, Uri } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { PlatformService } from '../../client/common/platform/platformService'; +import { IPlatformService } from '../../client/common/platform/types'; +import { ProcessServiceFactory } from '../../client/common/process/processFactory'; +import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; +import { IConfigurationService, IPythonSettings } from '../../client/common/types'; +import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; + +// tslint:disable-next-line:max-func-body-length +suite('Shebang detection', () => { + let configurationService: IConfigurationService; + let pythonSettings: typemoq.IMock<IPythonSettings>; + let workspaceService: IWorkspaceService; + let provider: ShebangCodeLensProvider; + let factory: IProcessServiceFactory; + let processService: typemoq.IMock<IProcessService>; + let platformService: typemoq.IMock<PlatformService>; + setup(() => { + pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); + configurationService = mock(ConfigurationService); + workspaceService = mock(WorkspaceService); + factory = mock(ProcessServiceFactory); + processService = typemoq.Mock.ofType<IProcessService>(); + platformService = typemoq.Mock.ofType<IPlatformService>(); + // tslint:disable-next-line:no-any + processService.setup((p) => (p as any).then).returns(() => undefined); + when(configurationService.getSettings(anything())).thenReturn(pythonSettings.object); + when(factory.create(anything())).thenResolve(processService.object); + provider = new ShebangCodeLensProvider( + instance(factory), + instance(configurationService), + platformService.object, + instance(workspaceService) + ); + }); + function createDocument( + firstLine: string, + uri = Uri.parse('xyz.py') + ): [typemoq.IMock<TextDocument>, typemoq.IMock<TextLine>] { + const doc = typemoq.Mock.ofType<TextDocument>(); + const line = typemoq.Mock.ofType<TextLine>(); + + line.setup((l) => l.isEmptyOrWhitespace) + .returns(() => firstLine.length === 0) + .verifiable(typemoq.Times.once()); + line.setup((l) => l.text).returns(() => firstLine); + + doc.setup((d) => d.lineAt(typemoq.It.isValue(0))) + .returns(() => line.object) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.uri).returns(() => uri); + + return [doc, line]; + } + test('Shebang should be empty when first line is empty when resolving shebang as interpreter', async () => { + const [document, line] = createDocument(''); + + const shebang = await provider.detectShebang(document.object, true); + + document.verifyAll(); + line.verifyAll(); + expect(shebang).to.be.equal(undefined, 'Shebang should be undefined'); + }); + test('Shebang should be empty when first line is empty when not resolving shebang as interpreter', async () => { + const [document, line] = createDocument(''); + + const shebang = await provider.detectShebang(document.object, false); + + document.verifyAll(); + line.verifyAll(); + expect(shebang).to.be.equal(undefined, 'Shebang should be undefined'); + }); + test('Shebang should be returned as it is when not resolving shebang as interpreter', async () => { + const [document, line] = createDocument('#!HELLO'); + + const shebang = await provider.detectShebang(document.object, false); + + document.verifyAll(); + line.verifyAll(); + expect(shebang).to.be.equal('HELLO', 'Shebang should be HELLO'); + }); + test('Shebang should be empty when python path is invalid in shebang', async () => { + const [document, line] = createDocument('#!HELLO'); + + processService + .setup((p) => p.exec(typemoq.It.isValue('HELLO'), typemoq.It.isAny())) + .returns(() => Promise.reject()) + .verifiable(typemoq.Times.once()); + + const shebang = await provider.detectShebang(document.object, true); + + document.verifyAll(); + line.verifyAll(); + expect(shebang).to.be.equal(undefined, 'Shebang should be undefined'); + processService.verifyAll(); + }); + test('Shebang should be returned when python path is valid', async () => { + const [document, line] = createDocument('#!HELLO'); + + processService + .setup((p) => p.exec(typemoq.It.isValue('HELLO'), typemoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'THIS_IS_IT' })) + .verifiable(typemoq.Times.once()); + + const shebang = await provider.detectShebang(document.object, true); + + document.verifyAll(); + line.verifyAll(); + expect(shebang).to.be.equal('THIS_IS_IT'); + processService.verifyAll(); + }); + test("Shebang should be returned when python path is valid and text is'/usr/bin/env python'", async () => { + const [document, line] = createDocument('#!/usr/bin/env python'); + platformService + .setup((p) => p.isWindows) + .returns(() => false) + .verifiable(typemoq.Times.once()); + processService + .setup((p) => p.exec(typemoq.It.isValue('/usr/bin/env'), typemoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'THIS_IS_IT' })) + .verifiable(typemoq.Times.once()); + + const shebang = await provider.detectShebang(document.object, true); + + document.verifyAll(); + line.verifyAll(); + expect(shebang).to.be.equal('THIS_IS_IT'); + processService.verifyAll(); + platformService.verifyAll(); + }); + test("Shebang should be returned when python path is valid and text is'/usr/bin/env python' and is windows", async () => { + const [document, line] = createDocument('#!/usr/bin/env python'); + platformService + .setup((p) => p.isWindows) + .returns(() => true) + .verifiable(typemoq.Times.once()); + processService + .setup((p) => p.exec(typemoq.It.isValue('/usr/bin/env python'), typemoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'THIS_IS_IT' })) + .verifiable(typemoq.Times.once()); + + const shebang = await provider.detectShebang(document.object, true); + + document.verifyAll(); + line.verifyAll(); + expect(shebang).to.be.equal('THIS_IS_IT'); + processService.verifyAll(); + platformService.verifyAll(); + }); + + test("No code lens when there's no shebang", async () => { + const [document] = createDocument(''); + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + processService + .setup((p) => p.exec(typemoq.It.isValue('python'), typemoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'python' })) + .verifiable(typemoq.Times.once()); + + provider.detectShebang = () => Promise.resolve(''); + + const codeLenses = await provider.provideCodeLenses(document.object); + + expect(codeLenses).to.be.lengthOf(0); + }); + test('No code lens when shebang is an empty string', async () => { + const [document] = createDocument('#!'); + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + processService + .setup((p) => p.exec(typemoq.It.isValue('python'), typemoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'python' })) + .verifiable(typemoq.Times.once()); + + provider.detectShebang = () => Promise.resolve(''); + + const codeLenses = await provider.provideCodeLenses(document.object); + + expect(codeLenses).to.be.lengthOf(0); + }); + test('No code lens when python path in settings is the same as that in shebang', async () => { + const [document] = createDocument('#!python'); + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + processService + .setup((p) => p.exec(typemoq.It.isValue('python'), typemoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'python' })) + .verifiable(typemoq.Times.once()); + + provider.detectShebang = () => Promise.resolve('python'); + + const codeLenses = await provider.provideCodeLenses(document.object); + + expect(codeLenses).to.be.lengthOf(0); + }); + test('Code lens returned when python path in settings is different to one in shebang', async () => { + const [document] = createDocument('#!python'); + pythonSettings.setup((p) => p.pythonPath).returns(() => 'different'); + processService + .setup((p) => p.exec(typemoq.It.isValue('different'), typemoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'different' })) + .verifiable(typemoq.Times.once()); + + provider.detectShebang = () => Promise.resolve('python'); + + const codeLenses = await provider.provideCodeLenses(document.object); + + expect(codeLenses).to.be.lengthOf(1); + expect(codeLenses[0].command!.command).to.equal('python.setShebangInterpreter'); + expect(codeLenses[0].command!.title).to.equal('Set as interpreter'); + expect(codeLenses[0].range.start.character).to.equal(0); + expect(codeLenses[0].range.start.line).to.equal(0); + expect(codeLenses[0].range.end.line).to.equal(0); + }); +}); diff --git a/src/test/providers/terminal.unit.test.ts b/src/test/providers/terminal.unit.test.ts new file mode 100644 index 000000000000..aa1fc837cfd1 --- /dev/null +++ b/src/test/providers/terminal.unit.test.ts @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Terminal, Uri } from 'vscode'; +import { IActiveResourceService, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; +import { Commands } from '../../client/common/constants'; +import { TerminalService } from '../../client/common/terminal/service'; +import { ITerminalActivator, ITerminalServiceFactory } from '../../client/common/terminal/types'; +import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { TerminalProvider } from '../../client/providers/terminalProvider'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal Provider', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let workspace: TypeMoq.IMock<IWorkspaceService>; + let activeResourceService: TypeMoq.IMock<IActiveResourceService>; + let terminalProvider: TerminalProvider; + const resource = Uri.parse('a'); + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + activeResourceService = TypeMoq.Mock.ofType<IActiveResourceService>(); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + serviceContainer.setup((c) => c.get(ICommandManager)).returns(() => commandManager.object); + serviceContainer.setup((c) => c.get(IWorkspaceService)).returns(() => workspace.object); + serviceContainer.setup((c) => c.get(IActiveResourceService)).returns(() => activeResourceService.object); + }); + teardown(() => { + try { + terminalProvider.dispose(); + // tslint:disable-next-line:no-empty + } catch {} + }); + + test('Ensure command is registered', () => { + terminalProvider = new TerminalProvider(serviceContainer.object); + commandManager.verify( + (c) => + c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once() + ); + }); + + test('Ensure command handler is disposed', () => { + const disposable = TypeMoq.Mock.ofType<Disposable>(); + commandManager + .setup((c) => + c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => disposable.object); + + terminalProvider = new TerminalProvider(serviceContainer.object); + terminalProvider.dispose(); + + disposable.verify((d) => d.dispose(), TypeMoq.Times.once()); + }); + + test('Ensure terminal is created and displayed when command is invoked', () => { + const disposable = TypeMoq.Mock.ofType<Disposable>(); + let commandHandler: undefined | (() => void); + commandManager + .setup((c) => + c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns((_cmd, callback) => { + commandHandler = callback; + return disposable.object; + }); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); + workspace.setup((w) => w.workspaceFolders).returns(() => undefined); + + terminalProvider = new TerminalProvider(serviceContainer.object); + expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); + + const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))) + .returns(() => terminalServiceFactory.object); + const terminalService = TypeMoq.Mock.ofType<TerminalService>(); + terminalServiceFactory + .setup((t) => t.createTerminalService(TypeMoq.It.isValue(resource), TypeMoq.It.isValue('Python'))) + .returns(() => terminalService.object); + + commandHandler!.call(terminalProvider); + activeResourceService.verifyAll(); + terminalService.verify((t) => t.show(false), TypeMoq.Times.once()); + }); + + // tslint:disable-next-line: max-func-body-length + suite('terminal.activateCurrentTerminal setting', () => { + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let terminalSettings: TypeMoq.IMock<ITerminalSettings>; + let configService: TypeMoq.IMock<IConfigurationService>; + let terminalActivator: TypeMoq.IMock<ITerminalActivator>; + let terminal: TypeMoq.IMock<Terminal>; + + setup(() => { + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + activeResourceService = TypeMoq.Mock.ofType<IActiveResourceService>(); + + terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>(); + pythonSettings.setup((s) => s.terminal).returns(() => terminalSettings.object); + + terminalActivator = TypeMoq.Mock.ofType<ITerminalActivator>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalActivator))) + .returns(() => terminalActivator.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IActiveResourceService))) + .returns(() => activeResourceService.object); + + terminal = TypeMoq.Mock.ofType<Terminal>(); + terminal + .setup((c) => c.creationOptions) + .returns(() => { + return { hideFromUser: false }; + }); + }); + + test('If terminal.activateCurrentTerminal setting is set, provided terminal should be activated', async () => { + terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => true); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); + + terminalProvider = new TerminalProvider(serviceContainer.object); + await terminalProvider.initialize(terminal.object); + + terminalActivator.verify( + (a) => a.activateEnvironmentInTerminal(terminal.object, TypeMoq.It.isAny()), + TypeMoq.Times.once() + ); + configService.verifyAll(); + activeResourceService.verifyAll(); + }); + + test('If terminal.activateCurrentTerminal setting is not set, provided terminal should not be activated', async () => { + terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => false); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); + + terminalProvider = new TerminalProvider(serviceContainer.object); + await terminalProvider.initialize(terminal.object); + + terminalActivator.verify( + (a) => a.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + activeResourceService.verifyAll(); + configService.verifyAll(); + }); + + test('If terminal.activateCurrentTerminal setting is set, but hideFromUser is true, provided terminal should not be activated', async () => { + terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => true); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); + + terminal + .setup((c) => c.creationOptions) + .returns(() => { + return { hideFromUser: true }; + }); + + terminalProvider = new TerminalProvider(serviceContainer.object); + await terminalProvider.initialize(terminal.object); + + terminalActivator.verify( + (a) => a.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + activeResourceService.verifyAll(); + configService.verifyAll(); + }); + + test('terminal.activateCurrentTerminal setting is set but provided terminal is undefined', async () => { + terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => true); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); + + terminalProvider = new TerminalProvider(serviceContainer.object); + await terminalProvider.initialize(undefined); + + terminalActivator.verify( + (a) => a.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + activeResourceService.verifyAll(); + configService.verifyAll(); + }); + + test('Exceptions are swallowed if initializing terminal provider fails', async () => { + terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => true); + configService.setup((c) => c.getSettings(resource)).throws(new Error('Kaboom')); + activeResourceService.setup((a) => a.getActiveResource()).returns(() => resource); + + terminalProvider = new TerminalProvider(serviceContainer.object); + try { + await terminalProvider.initialize(undefined); + } catch (ex) { + assert(false, `No error should be thrown, ${ex}`); + } + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/common.ts b/src/test/pythonEnvironments/base/common.ts new file mode 100644 index 000000000000..85278be83424 --- /dev/null +++ b/src/test/pythonEnvironments/base/common.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { createDeferred, flattenIterator } from '../../../client/common/utils/async'; +import { Architecture } from '../../../client/common/utils/platform'; +import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../client/common/utils/version'; +import { + PythonEnvInfo, + PythonEnvKind, + PythonReleaseLevel, + PythonVersion +} from '../../../client/pythonEnvironments/base/info'; +import { Locator, PythonEnvsIterator, PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator'; +import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; + +export function createEnv( + name: string, + versionStr: string, + kind?: PythonEnvKind, + executable?: string, + idStr?: string +): PythonEnvInfo { + if (kind === undefined) { + kind = PythonEnvKind.Unknown; + } + if (executable === undefined || executable === '') { + executable = 'python'; + } + const id = idStr ? idStr : `${kind}-${name}`; + const version = parseVersion(versionStr); + return { + id, + kind, + version, + name, + location: '', + arch: Architecture.x86, + executable: { + filename: executable, + sysPrefix: '', + mtime: -1, + ctime: -1 + }, + distro: { org: '' } + }; +} + +function parseVersion(versionStr: string): PythonVersion { + const parsed = parseBasicVersionInfo<PythonVersion>(versionStr); + if (!parsed) { + if (versionStr === '') { + return EMPTY_VERSION as PythonVersion; + } + throw Error(`invalid version ${versionStr}`); + } + const { version, after } = parsed; + const match = after.match(/^(a|b|rc)(\d+)$/); + if (match) { + const [, levelStr, serialStr ] = match; + let level: PythonReleaseLevel; + if (levelStr === 'a') { + level = PythonReleaseLevel.Alpha; + } else if (levelStr === 'b') { + level = PythonReleaseLevel.Beta; + } else if (levelStr === 'rc') { + level = PythonReleaseLevel.Candidate; + } else { + throw Error('unreachable!'); + } + version.release = { + level, + serial: parseInt(serialStr, 10) + }; + } + return version; +} + +export function createLocatedEnv( + location: string, + versionStr: string, + kind = PythonEnvKind.Unknown, + executable = 'python', + idStr?: string +): PythonEnvInfo { + if (!idStr) { + idStr = `${kind}-${location}`; + } + const env = createEnv('', versionStr, kind, executable, idStr); + env.location = location; + return env; +} + +export class SimpleLocator extends Locator { + private deferred = createDeferred<void>(); + constructor( + private envs: PythonEnvInfo[], + private callbacks?: { + resolve?: null | ((env: PythonEnvInfo) => Promise<PythonEnvInfo | undefined>); + before?: Promise<void>; + after?: Promise<void>; + beforeEach?(e: PythonEnvInfo): Promise<void>; + afterEach?(e: PythonEnvInfo): Promise<void>; + onQuery?(query: PythonLocatorQuery | undefined, envs: PythonEnvInfo[]): Promise<PythonEnvInfo[]>; + } + ) { + super(); + } + public get done(): Promise<void> { + return this.deferred.promise; + } + public fire(event: PythonEnvsChangedEvent) { + this.emitter.fire(event); + } + public iterEnvs(query?: PythonLocatorQuery): PythonEnvsIterator { + const deferred = this.deferred; + const callbacks = this.callbacks; + let envs = this.envs; + async function* iterator() { + if (callbacks?.onQuery !== undefined) { + envs = await callbacks.onQuery(query, envs); + } + if (callbacks?.before !== undefined) { + await callbacks.before; + } + //yield* envs; + for (const env of envs) { + if (callbacks?.beforeEach !== undefined) { + await callbacks.beforeEach(env); + } + yield env; + if (callbacks?.afterEach !== undefined) { + await callbacks.afterEach(env); + } + } + if (callbacks?.after!== undefined) { + await callbacks.after; + } + deferred.resolve(); + } + return iterator(); + } + public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> { + const envInfo: PythonEnvInfo = typeof env === 'string' ? createEnv('', '', undefined, env) : env; + if (this.callbacks?.resolve === undefined) { + return envInfo; + } else if (this.callbacks?.resolve === null) { + return undefined; + } else { + return this.callbacks.resolve(envInfo); + } + } +} + +export async function getEnvs(iterator: PythonEnvsIterator): Promise<PythonEnvInfo[]> { + return flattenIterator(iterator); +} diff --git a/src/test/pythonEnvironments/base/locators.unit.test.ts b/src/test/pythonEnvironments/base/locators.unit.test.ts new file mode 100644 index 000000000000..d7500fc37a05 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators.unit.test.ts @@ -0,0 +1,371 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Uri } from 'vscode'; +import { createDeferred } from '../../../client/common/utils/async'; +import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator'; +import { DisableableLocator, Locators } from '../../../client/pythonEnvironments/base/locators'; +import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; +import { createEnv, createLocatedEnv, getEnvs, SimpleLocator } from './common'; + +suite('Python envs locators - Locators', () => { + suite('onChanged consolidates', () => { + test('one', () => { + const event1: PythonEnvsChangedEvent = {}; + const expected = [event1]; + const sub1 = new SimpleLocator([]); + const locators = new Locators([sub1]); + + const events: PythonEnvsChangedEvent[] = []; + locators.onChanged((e) => events.push(e)); + sub1.fire(event1); + + assert.deepEqual(events, expected); + }); + + test('many', () => { + const loc1 = Uri.file('some-dir'); + const event1: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown, searchLocation: loc1 }; + const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Venv }; + const event3: PythonEnvsChangedEvent = {}; + const event4: PythonEnvsChangedEvent = { searchLocation: loc1 }; + const event5: PythonEnvsChangedEvent = {}; + const expected = [event1, event2, event3, event4, event5]; + const sub1 = new SimpleLocator([]); + const sub2 = new SimpleLocator([]); + const sub3 = new SimpleLocator([]); + const locators = new Locators([sub1, sub2, sub3]); + + const events: PythonEnvsChangedEvent[] = []; + locators.onChanged((e) => events.push(e)); + sub2.fire(event1); + sub3.fire(event2); + sub1.fire(event3); + sub2.fire(event4); + sub1.fire(event5); + + assert.deepEqual(events, expected); + }); + }); + + suite('iterEnvs() consolidates', () => { + test('no envs', async () => { + const expected: PythonEnvInfo[] = []; + const sub1 = new SimpleLocator([]); + const locators = new Locators([sub1]); + + const iterator = locators.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('one', async () => { + const env1 = createEnv('foo', '3.8', PythonEnvKind.Venv); + const expected: PythonEnvInfo[] = [env1]; + const sub1 = new SimpleLocator(expected); + const locators = new Locators([sub1]); + + const iterator = locators.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('many', async () => { + const env1 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); + const env3 = createEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createEnv('hello world', '3.8', PythonEnvKind.System); + const expected = [env1, env2, env3, env4, env5]; + const sub1 = new SimpleLocator([env1]); + const sub2 = new SimpleLocator([], { before: sub1.done }); + const sub3 = new SimpleLocator([env2, env3, env4], { before: sub2.done }); + const sub4 = new SimpleLocator([env5], { before: sub3.done }); + const locators = new Locators([sub1, sub2, sub3, sub4]); + + const iterator = locators.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('with query', async () => { + const expected: PythonLocatorQuery = { + kinds: [PythonEnvKind.Venv], + searchLocations: [Uri.file('???')] + }; + let query: PythonLocatorQuery | undefined; + async function onQuery(q: PythonLocatorQuery | undefined, e: PythonEnvInfo[]) { + query = q; + return e; + } + const env1 = createEnv('foo', '3.8', PythonEnvKind.Venv); + const sub1 = new SimpleLocator([env1], { onQuery }); + const locators = new Locators([sub1]); + + const iterator = locators.iterEnvs(expected); + await getEnvs(iterator); + + assert.deepEqual(query, expected); + }); + + test('iterate out of order', async () => { + const env1 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); + const env3 = createEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createEnv('hello world', '3.8', PythonEnvKind.System); + const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.Custom); + const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Custom); + const expected = [env5, env1, env2, env3, env4, env6, env7]; + const sub4 = new SimpleLocator([env5]); + const sub2 = new SimpleLocator([env1], { before: sub4.done }); + const sub1 = new SimpleLocator([]); + const sub3 = new SimpleLocator([env2, env3, env4], { before: sub2.done }); + const sub5 = new SimpleLocator([env6, env7], { before: sub3.done }); + const locators = new Locators([sub1, sub2, sub3, sub4, sub5]); + + const iterator = locators.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('iterate intermingled', async () => { + const env1 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); + const env3 = createEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createEnv('hello world', '3.8', PythonEnvKind.System); + const expected = [env1, env4, env2, env5, env3]; + const deferred1 = createDeferred<void>(); + const deferred2 = createDeferred<void>(); + const deferred4 = createDeferred<void>(); + const deferred5 = createDeferred<void>(); + const sub1 = new SimpleLocator([env1, env2, env3], { + beforeEach: async (env) => { + if (env === env2) { + await deferred4.promise; + } else if (env === env3) { + await deferred5.promise; + } + }, + afterEach: async (env) => { + if (env === env1) { + deferred1.resolve(); + } else if (env === env2) { + deferred2.resolve(); + } + } + }); + const sub2 = new SimpleLocator([env4, env5], { + beforeEach: async (env) => { + if (env === env4) { + await deferred1.promise; + } else if (env === env5) { + await deferred2.promise; + } + }, + afterEach: async (env) => { + if (env === env4) { + deferred4.resolve(); + } else if (env === env5) { + deferred5.resolve(); + } + } + }); + const locators = new Locators([sub1, sub2]); + + const iterator = locators.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + }); + + suite('resolveEnv()', () => { + test('one wrapped', async () => { + const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const expected = env1; + const calls: number[] = []; + const sub1 = new SimpleLocator([env1], { resolve: async (e) => { + calls.push(1); + return e; + }}); + const locators = new Locators([sub1]); + + const resolved = await locators.resolveEnv(env1); + + assert.deepEqual(resolved, expected); + assert.deepEqual(calls, [1]); + }); + + test('first one resolves', async () => { + const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const expected = env1; + const calls: number[] = []; + const sub1 = new SimpleLocator([env1], { resolve: async (e) => { + calls.push(1); + return e; + }}); + const sub2 = new SimpleLocator([env1], { resolve: async (e) => { + calls.push(2); + return e; + }}); + const locators = new Locators([sub1, sub2]); + + const resolved = await locators.resolveEnv(env1); + + assert.deepEqual(resolved, expected); + assert.deepEqual(calls, [1]); + }); + + test('second one resolves', async () => { + const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const expected = env1; + const calls: number[] = []; + const sub1 = new SimpleLocator([env1], { resolve: async (_e) => { + calls.push(1); + return undefined; + }}); + const sub2 = new SimpleLocator([env1], { resolve: async (e) => { + calls.push(2); + return e; + }}); + const locators = new Locators([sub1, sub2]); + + const resolved = await locators.resolveEnv(env1); + + assert.deepEqual(resolved, expected); + assert.deepEqual(calls, [1, 2]); + }); + + test('none resolve', async () => { + const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const calls: number[] = []; + const sub1 = new SimpleLocator([env1], { resolve: async (_e) => { + calls.push(1); + return undefined; + }}); + const sub2 = new SimpleLocator([env1], { resolve: async (_e) => { + calls.push(2); + return undefined; + }}); + const locators = new Locators([sub1, sub2]); + + const resolved = await locators.resolveEnv(env1); + + assert.equal(resolved, undefined); + assert.deepEqual(calls, [1, 2]); + }); + }); +}); + +suite('Python envs locators - DisableableLocator', () => { + suite('onChanged', () => { + test('fires if enabled', () => { + const event1: PythonEnvsChangedEvent = {}; + const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown }; + const expected = [event1, event2]; + const sub = new SimpleLocator([]); + const locator = new DisableableLocator(sub); + const events: PythonEnvsChangedEvent[] = []; + locator.onChanged((e) => events.push(e)); + + sub.fire(event1); + sub.fire(event2); + + assert.deepEqual(events, expected); + }); + + test('does not fire if disabled', () => { + const event1: PythonEnvsChangedEvent = {}; + const event2: PythonEnvsChangedEvent = {}; + const expected: PythonEnvsChangedEvent[] = []; + const sub = new SimpleLocator([]); + const locator = new DisableableLocator(sub); + const events: PythonEnvsChangedEvent[] = []; + locator.onChanged((e) => events.push(e)); + + locator.disable(); + sub.fire(event1); + sub.fire(event2); + + assert.deepEqual(events, expected); + }); + + test('follows enabled state', () => { + const event1: PythonEnvsChangedEvent = {}; + const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown }; + const event3: PythonEnvsChangedEvent = { kind: PythonEnvKind.Venv }; + const expected = [event1, event3]; + const sub = new SimpleLocator([]); + const locator = new DisableableLocator(sub); + const events: PythonEnvsChangedEvent[] = []; + locator.onChanged((e) => events.push(e)); + + sub.fire(event1); + locator.disable(); + sub.fire(event2); + locator.enable(); + sub.fire(event3); + + assert.deepEqual(events, expected); + }); + }); + + suite('iterEnvs()', () => { + test('pass-through if enabled', async () => { + const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const expected = [env1]; + const sub = new SimpleLocator([env1]); + const locator = new DisableableLocator(sub); + + const iterator = locator.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('empty if disabled', async () => { + const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const expected: PythonEnvInfo[] = []; + const sub = new SimpleLocator([env1]); + const locator = new DisableableLocator(sub); + + locator.disable(); + const iterator = locator.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + }); + + suite('resolveEnv()', () => { + test('pass-through if enabled', async () => { + const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const expected = env1; + const sub = new SimpleLocator([env1]); + const locator = new DisableableLocator(sub); + + const resolved = await locator.resolveEnv(env1); + + assert.deepEqual(resolved, expected); + }); + + test('empty if disabled', async () => { + const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const sub = new SimpleLocator([env1]); + const locator = new DisableableLocator(sub); + + locator.disable(); + const resolved = await locator.resolveEnv(env1); + + assert.equal(resolved, undefined); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/watcher.unit.test.ts b/src/test/pythonEnvironments/base/watcher.unit.test.ts new file mode 100644 index 000000000000..67b81afd2ce4 --- /dev/null +++ b/src/test/pythonEnvironments/base/watcher.unit.test.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Uri } from 'vscode'; +import { PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { + BasicPythonEnvsChangedEvent, + PythonEnvsChangedEvent, + PythonEnvsWatcher +} from '../../../client/pythonEnvironments/base/watcher'; + +const KINDS_TO_TEST = [ + PythonEnvKind.Unknown, + PythonEnvKind.System, + PythonEnvKind.Custom, + PythonEnvKind.OtherGlobal, + PythonEnvKind.Venv, + PythonEnvKind.Conda, + PythonEnvKind.OtherVirtual +]; + +suite('Python envs watcher - PythonEnvsWatcher', () => { + const location = Uri.file('some-dir'); + + suite('fire()', () => { + test('empty event', () => { + const expected: PythonEnvsChangedEvent = {}; + const watcher = new PythonEnvsWatcher(); + let event: PythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.equal(event, expected); + }); + + KINDS_TO_TEST.forEach((kind) => { + test(`non-empty event ("${kind}")`, () => { + const expected: PythonEnvsChangedEvent = { + kind: kind, + searchLocation: location + }; + const watcher = new PythonEnvsWatcher(); + let event: PythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.equal(event, expected); + }); + }); + + test('kind-only', () => { + const expected: PythonEnvsChangedEvent = { kind: PythonEnvKind.Venv }; + const watcher = new PythonEnvsWatcher(); + let event: PythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.equal(event, expected); + }); + + test('searchLocation-only', () => { + const expected: PythonEnvsChangedEvent = { searchLocation: Uri.file('foo') }; + const watcher = new PythonEnvsWatcher(); + let event: PythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.equal(event, expected); + }); + }); + + suite('using BasicPythonEnvsChangedEvent', () => { + test('empty event', () => { + const expected: BasicPythonEnvsChangedEvent = {}; + const watcher = new PythonEnvsWatcher<BasicPythonEnvsChangedEvent>(); + let event: BasicPythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.equal(event, expected); + }); + + KINDS_TO_TEST.forEach((kind) => { + test(`non-empty event ("${kind}")`, () => { + const expected: BasicPythonEnvsChangedEvent = { + kind: kind + }; + const watcher = new PythonEnvsWatcher<BasicPythonEnvsChangedEvent>(); + let event: BasicPythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.equal(event, expected); + }); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/watchers.unit.test.ts b/src/test/pythonEnvironments/base/watchers.unit.test.ts new file mode 100644 index 000000000000..719b318d93b3 --- /dev/null +++ b/src/test/pythonEnvironments/base/watchers.unit.test.ts @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Uri } from 'vscode'; +import { PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { PythonEnvsChangedEvent, PythonEnvsWatcher } from '../../../client/pythonEnvironments/base/watcher'; +import { DisableableEnvsWatcher, PythonEnvsWatchers } from '../../../client/pythonEnvironments/base/watchers'; + +suite('Python envs watchers - PythonEnvsWatchers', () => { + suite('onChanged consolidates', () => { + test('empty', () => { + const watcher = new PythonEnvsWatchers([]); + + assert.ok(watcher); + }); + + test('one', () => { + const event1: PythonEnvsChangedEvent = {}; + const expected = [event1]; + const sub1 = new PythonEnvsWatcher(); + const watcher = new PythonEnvsWatchers([sub1]); + + const events: PythonEnvsChangedEvent[] = []; + watcher.onChanged((e) => events.push(e)); + sub1.fire(event1); + + assert.deepEqual(events, expected); + }); + + test('many', () => { + const loc1 = Uri.file('some-dir'); + const event1: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown, searchLocation: loc1 }; + const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Venv }; + const event3: PythonEnvsChangedEvent = {}; + const event4: PythonEnvsChangedEvent = { searchLocation: loc1 }; + const event5: PythonEnvsChangedEvent = {}; + const expected = [event1, event2, event3, event4, event5]; + const sub1 = new PythonEnvsWatcher(); + const sub2 = new PythonEnvsWatcher(); + const sub3 = new PythonEnvsWatcher(); + const watcher = new PythonEnvsWatchers([sub1, sub2, sub3]); + + const events: PythonEnvsChangedEvent[] = []; + watcher.onChanged((e) => events.push(e)); + sub2.fire(event1); + sub3.fire(event2); + sub1.fire(event3); + sub2.fire(event4); + sub1.fire(event5); + + assert.deepEqual(events, expected); + }); + }); +}); + +suite('Python envs watchers - DisableableEnvsWatcher', () => { + test('enabled by default', () => { + const event1: PythonEnvsChangedEvent = {}; + const expected = [event1]; + const sub = new PythonEnvsWatcher(); + const watcher = new DisableableEnvsWatcher(sub); + const events: PythonEnvsChangedEvent[] = []; + watcher.onChanged((e) => events.push(e)); + + sub.fire(event1); + + assert.deepEqual(events, expected); + }); + + suite('onChanged', () => { + test('fires if enabled', () => { + const event1: PythonEnvsChangedEvent = {}; + const event2: PythonEnvsChangedEvent = {}; + const expected = [event1, event2]; + const sub = new PythonEnvsWatcher(); + const watcher = new DisableableEnvsWatcher(sub); + const events: PythonEnvsChangedEvent[] = []; + watcher.onChanged((e) => events.push(e)); + + watcher.enable(); + sub.fire(event1); + sub.fire(event2); + + assert.deepEqual(events, expected); + }); + + test('does not fire if disabled', () => { + const event1: PythonEnvsChangedEvent = {}; + const event2: PythonEnvsChangedEvent = {}; + const expected: PythonEnvsChangedEvent[] = []; + const sub = new PythonEnvsWatcher(); + const watcher = new DisableableEnvsWatcher(sub); + const events: PythonEnvsChangedEvent[] = []; + watcher.onChanged((e) => events.push(e)); + + watcher.disable(); + sub.fire(event1); + sub.fire(event2); + + assert.deepEqual(events, expected); + }); + + test('follows enabled state', () => { + const event1: PythonEnvsChangedEvent = {}; + const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown }; + const event3: PythonEnvsChangedEvent = { kind: PythonEnvKind.Venv }; + const expected = [event1, event3]; + const sub = new PythonEnvsWatcher(); + const watcher = new DisableableEnvsWatcher(sub); + const events: PythonEnvsChangedEvent[] = []; + watcher.onChanged((e) => events.push(e)); + + watcher.enable(); + sub.fire(event1); + watcher.disable(); + sub.fire(event2); + watcher.enable(); + sub.fire(event3); + + assert.deepEqual(events, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/commonTestConstants.ts b/src/test/pythonEnvironments/common/commonTestConstants.ts new file mode 100644 index 000000000000..1d868dc9e2a9 --- /dev/null +++ b/src/test/pythonEnvironments/common/commonTestConstants.ts @@ -0,0 +1,14 @@ +import * as path from 'path'; + +export const TEST_LAYOUT_ROOT = path.join( + __dirname, + '..', + '..', + '..', + '..', + 'src', + 'test', + 'pythonEnvironments', + 'common', + 'envlayouts', +); diff --git a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts new file mode 100644 index 000000000000..7008006377b6 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as platformApis from '../../../client/common/utils/platform'; +import { identifyEnvironment } from '../../../client/pythonEnvironments/common/environmentIdentifier'; +import { EnvironmentType } from '../../../client/pythonEnvironments/info'; +import { TEST_LAYOUT_ROOT } from './commonTestConstants'; + +suite('Environment Identifier', () => { + suite('Conda', () => { + test('Conda layout with conda-meta and python binary in the same directory', async () => { + const interpreterPath: string = path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'); + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, EnvironmentType.Conda); + }); + test('Conda layout with conda-meta and python binary in a sub directory', async () => { + const interpreterPath: string = path.join(TEST_LAYOUT_ROOT, 'conda2', 'bin', 'python'); + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, EnvironmentType.Conda); + }); + }); + + suite('Windows Store', () => { + let getEnvVar: sinon.SinonStub; + const fakeLocalAppDataPath = 'X:\\users\\user\\AppData\\Local'; + const fakeProgramFilesPath = 'X:\\Program Files'; + const executable = ['python.exe', 'python3.exe', 'python3.8.exe']; + suiteSetup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVar.withArgs('LOCALAPPDATA').returns(fakeLocalAppDataPath); + getEnvVar.withArgs('ProgramFiles').returns(fakeProgramFilesPath); + }); + suiteTeardown(() => { + getEnvVar.restore(); + }); + executable.forEach((exe) => { + test(`Path to local app data windows store interpreter (${exe})`, async () => { + const interpreterPath = path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe); + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, EnvironmentType.WindowsStore); + }); + test(`Path to local app data windows store interpreter app sub-directory (${exe})`, async () => { + const interpreterPath = path.join( + fakeLocalAppDataPath, + 'Microsoft', + 'WindowsApps', + 'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', + exe, + ); + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, EnvironmentType.WindowsStore); + }); + test(`Path to program files windows store interpreter app sub-directory (${exe})`, async () => { + const interpreterPath = path.join( + fakeProgramFilesPath, + 'WindowsApps', + 'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', + exe, + ); + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, EnvironmentType.WindowsStore); + }); + test(`Local app data not set (${exe})`, async () => { + getEnvVar.withArgs('LOCALAPPDATA').returns(undefined); + const interpreterPath = path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe); + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, EnvironmentType.WindowsStore); + }); + test(`Program files app data not set (${exe})`, async () => { + getEnvVar.withArgs('ProgramFiles').returns(undefined); + const interpreterPath = path.join( + fakeProgramFilesPath, + 'WindowsApps', + 'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', + exe, + ); + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, EnvironmentType.WindowsStore); + }); + test(`Path using forward slashes (${exe})`, async () => { + const interpreterPath = path + .join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe) + .replace('\\', '/'); + const envType: EnvironmentType = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, EnvironmentType.WindowsStore); + }); + test(`Path using long path style slashes (${exe})`, async () => { + const interpreterPath = path + .join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe) + .replace('\\', '/'); + const envType: EnvironmentType = await identifyEnvironment(`\\\\?\\${interpreterPath}`); + assert.deepEqual(envType, EnvironmentType.WindowsStore); + }); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/envlayouts/conda1/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/conda1/conda-meta/history new file mode 100644 index 000000000000..a329d0a79b88 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/conda1/conda-meta/history @@ -0,0 +1 @@ +Usually contains command that was used to create or update the conda environment with time stamps. diff --git a/src/test/pythonEnvironments/common/envlayouts/conda1/python.exe b/src/test/pythonEnvironments/common/envlayouts/conda1/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/conda1/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/conda2/bin/python b/src/test/pythonEnvironments/common/envlayouts/conda2/bin/python new file mode 100644 index 000000000000..590cf8f553ef --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/conda2/bin/python @@ -0,0 +1 @@ +Not a real python binary diff --git a/src/test/pythonEnvironments/common/envlayouts/conda2/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/conda2/conda-meta/history new file mode 100644 index 000000000000..a329d0a79b88 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/conda2/conda-meta/history @@ -0,0 +1 @@ +Usually contains command that was used to create or update the conda environment with time stamps. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.7.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.7.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.7.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.8.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.8.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.8.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/idle.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/idle.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/idle.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.7.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.7.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.7.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.8.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.8.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.8.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/windowsUtils.unit.test.ts b/src/test/pythonEnvironments/common/windowsUtils.unit.test.ts new file mode 100644 index 000000000000..d190fdf2386c --- /dev/null +++ b/src/test/pythonEnvironments/common/windowsUtils.unit.test.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { isWindowsPythonExe } from '../../../client/pythonEnvironments/common/windowsUtils'; + +suite('Windows Utils tests', () => { + const testParams = [ + { path: 'python.exe', expected: true }, + { path: 'python3.exe', expected: true }, + { path: 'python38.exe', expected: true }, + { path: 'python3.8.exe', expected: true }, + { path: 'python', expected: false }, + { path: 'python3', expected: false }, + { path: 'python38', expected: false }, + { path: 'python3.8', expected: false }, + { path: 'idle.exe', expected: false }, + { path: 'pip.exe', expected: false }, + { path: 'python.dll', expected: false }, + { path: 'python3.dll', expected: false }, + { path: 'python3.8.dll', expected: false }, + ]; + + testParams.forEach((testParam) => { + test(`Python executable check ${testParam.expected ? 'should match' : 'should not match'} this path: ${testParam.path}`, () => { + assert.deepEqual(isWindowsPythonExe(testParam.path), testParam.expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/globalenv.unit.test.ts b/src/test/pythonEnvironments/discovery/globalenv.unit.test.ts new file mode 100644 index 000000000000..f8240b996f7c --- /dev/null +++ b/src/test/pythonEnvironments/discovery/globalenv.unit.test.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +suite('getPyenvTypeFinder()', () => { + // We will pull tests over from src/test/interpreters/virtualEnvs/index.unit.test.ts at some point. +}); + +suite('getPyenvRootFinder()', () => { + // We will pull tests over from src/test/interpreters/virtualEnvs/index.unit.test.ts at some point. +}); diff --git a/src/test/pythonEnvironments/discovery/locators/cacheableLocatorService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/cacheableLocatorService.unit.test.ts new file mode 100644 index 000000000000..a4e65aadaf13 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/cacheableLocatorService.unit.test.ts @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-classes-per-file max-func-body-length + +import { expect } from 'chai'; +import * as md5 from 'md5'; +import { + anything, instance, mock, verify, when, +} from 'ts-mockito'; +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { Resource } from '../../../../client/common/types'; +import { noop } from '../../../../client/common/utils/misc'; +import { IInterpreterWatcher } from '../../../../client/interpreter/contracts'; +import { ServiceContainer } from '../../../../client/ioc/container'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { CacheableLocatorService } from '../../../../client/pythonEnvironments/discovery/locators/services/cacheableLocatorService'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +suite('Interpreters - Cacheable Locator Service', () => { + suite('Caching', () => { + class Locator extends CacheableLocatorService { + constructor(name: string, serviceCcontainer: IServiceContainer, private readonly mockLocator: MockLocator) { + super(name, serviceCcontainer); + } + + public dispose() { + noop(); + } + + protected async getInterpretersImplementation(_resource?: Uri): Promise<PythonEnvironment[]> { + return this.mockLocator.getInterpretersImplementation(); + } + + protected getCachedInterpreters(_resource?: Uri): PythonEnvironment[] | undefined { + return this.mockLocator.getCachedInterpreters(); + } + + protected async cacheInterpreters(_interpreters: PythonEnvironment[], _resource?: Uri) { + return this.mockLocator.cacheInterpreters(); + } + + protected getCacheKey(_resource?: Uri) { + return this.mockLocator.getCacheKey(); + } + } + class MockLocator { + public async getInterpretersImplementation(): Promise<PythonEnvironment[]> { + return []; + } + + public getCachedInterpreters(): PythonEnvironment[] | undefined { + return undefined; + } + + public async cacheInterpreters() { + return undefined; + } + + public getCacheKey(): string { + return ''; + } + } + let serviceContainer: ServiceContainer; + setup(() => { + serviceContainer = mock(ServiceContainer); + }); + + test('Interpreters must be retrieved once, then cached', async () => { + const expectedInterpreters = [1, 2] as any; + const mockedLocatorForVerification = mock(MockLocator); + const locator = new (class extends Locator { + protected async addHandlersForInterpreterWatchers( + _cacheKey: string, + _resource: Resource, + ): Promise<void> { + noop(); + } + })('dummy', instance(serviceContainer), instance(mockedLocatorForVerification)); + + when(mockedLocatorForVerification.getInterpretersImplementation()).thenResolve(expectedInterpreters); + when(mockedLocatorForVerification.getCacheKey()).thenReturn('xyz'); + when(mockedLocatorForVerification.getCachedInterpreters()).thenResolve(); + + const [items1, items2, items3] = await Promise.all([ + locator.getInterpreters(), + locator.getInterpreters(), + locator.getInterpreters(), + ]); + expect(items1).to.be.deep.equal(expectedInterpreters); + expect(items2).to.be.deep.equal(expectedInterpreters); + expect(items3).to.be.deep.equal(expectedInterpreters); + + verify(mockedLocatorForVerification.getInterpretersImplementation()).once(); + verify(mockedLocatorForVerification.getCachedInterpreters()).atLeast(1); + verify(mockedLocatorForVerification.cacheInterpreters()).atLeast(1); + }); + + test('Ensure onDidCreate event handler is attached', async () => { + const mockedLocatorForVerification = mock(MockLocator); + class Watcher implements IInterpreterWatcher { + public onDidCreate( + _listener: (e: Resource) => any, + _thisArgs?: any, + _disposables?: Disposable[], + ): Disposable { + return { dispose: noop }; + } + } + const watcher: IInterpreterWatcher = mock(Watcher); + + const locator = new (class extends Locator { + protected async getInterpreterWatchers(_resource: Resource): Promise<IInterpreterWatcher[]> { + return [instance(watcher)]; + } + })('dummy', instance(serviceContainer), instance(mockedLocatorForVerification)); + + await locator.getInterpreters(); + + verify(watcher.onDidCreate(anything(), anything(), anything())).once(); + }); + + test('Ensure cache is cleared when watcher event fires', async () => { + const expectedInterpreters = [1, 2] as any; + const mockedLocatorForVerification = mock(MockLocator); + class Watcher implements IInterpreterWatcher { + private listner?: (e: Resource) => any; + + public onDidCreate( + listener: (e: Resource) => any, + _thisArgs?: any, + _disposables?: Disposable[], + ): Disposable { + this.listner = listener; + return { dispose: noop }; + } + + public invokeListeners() { + this.listner!(undefined); + } + } + const watcher = new Watcher(); + + const locator = new (class extends Locator { + protected async getInterpreterWatchers(_resource: Resource): Promise<IInterpreterWatcher[]> { + return [watcher]; + } + })('dummy', instance(serviceContainer), instance(mockedLocatorForVerification)); + + when(mockedLocatorForVerification.getInterpretersImplementation()).thenResolve(expectedInterpreters); + when(mockedLocatorForVerification.getCacheKey()).thenReturn('xyz'); + when(mockedLocatorForVerification.getCachedInterpreters()).thenResolve(); + + const [items1, items2, items3] = await Promise.all([ + locator.getInterpreters(), + locator.getInterpreters(), + locator.getInterpreters(), + ]); + expect(items1).to.be.deep.equal(expectedInterpreters); + expect(items2).to.be.deep.equal(expectedInterpreters); + expect(items3).to.be.deep.equal(expectedInterpreters); + + verify(mockedLocatorForVerification.getInterpretersImplementation()).once(); + verify(mockedLocatorForVerification.getCachedInterpreters()).atLeast(1); + verify(mockedLocatorForVerification.cacheInterpreters()).once(); + + watcher.invokeListeners(); + + const [items4, items5, items6] = await Promise.all([ + locator.getInterpreters(), + locator.getInterpreters(), + locator.getInterpreters(), + ]); + expect(items4).to.be.deep.equal(expectedInterpreters); + expect(items5).to.be.deep.equal(expectedInterpreters); + expect(items6).to.be.deep.equal(expectedInterpreters); + + // We must get the list of interperters again and cache the new result again. + verify(mockedLocatorForVerification.getInterpretersImplementation()).twice(); + verify(mockedLocatorForVerification.cacheInterpreters()).twice(); + }); + test('Ensure locating event is raised', async () => { + const mockedLocatorForVerification = mock(MockLocator); + const locator = new (class extends Locator { + protected async getInterpreterWatchers(_resource: Resource): Promise<IInterpreterWatcher[]> { + return []; + } + })('dummy', instance(serviceContainer), instance(mockedLocatorForVerification)); + + let locatingEventRaised = false; + locator.onLocating(() => (locatingEventRaised = true)); + + when(mockedLocatorForVerification.getInterpretersImplementation()).thenResolve([1, 2] as any); + when(mockedLocatorForVerification.getCacheKey()).thenReturn('xyz'); + when(mockedLocatorForVerification.getCachedInterpreters()).thenResolve(); + + await locator.getInterpreters(); + expect(locatingEventRaised).to.be.equal(true, 'Locating Event not raised'); + }); + }); + suite('Cache Key', () => { + class Locator extends CacheableLocatorService { + public dispose() { + noop(); + } + + // tslint:disable-next-line:no-unnecessary-override + public getCacheKey(resource?: Uri) { + return super.getCacheKey(resource); + } + + protected async getInterpretersImplementation(_resource?: Uri): Promise<PythonEnvironment[]> { + return []; + } + + protected getCachedInterpreters(_resource?: Uri): PythonEnvironment[] | undefined { + return []; + } + + protected async cacheInterpreters(_interpreters: PythonEnvironment[], _resource?: Uri) { + noop(); + } + } + let serviceContainer: ServiceContainer; + setup(() => { + serviceContainer = mock(ServiceContainer); + }); + + test('Cache Key must contain name of locator', async () => { + const locator = new Locator('hello-World', instance(serviceContainer)); + + const key = locator.getCacheKey(); + + expect(key).contains('hello-World'); + }); + + test('Cache Key must not contain path to workspace', async () => { + const workspace = mock(WorkspaceService); + const workspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file(__dirname) }; + + when(workspace.hasWorkspaceFolders).thenReturn(true); + when(workspace.workspaceFolders).thenReturn([workspaceFolder]); + when(workspace.getWorkspaceFolder(anything())).thenReturn(workspaceFolder); + when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspace)); + when(serviceContainer.get<IWorkspaceService>(IWorkspaceService, anything())).thenReturn( + instance(workspace), + ); + + const locator = new Locator('hello-World', instance(serviceContainer), false); + + const key = locator.getCacheKey(Uri.file('something')); + + expect(key).contains('hello-World'); + expect(key).not.contains(md5(workspaceFolder.uri.fsPath)); + }); + + test('Cache Key must contain path to workspace', async () => { + const workspace = mock(WorkspaceService); + const workspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file(__dirname) }; + const resource = Uri.file('a'); + + when(workspace.hasWorkspaceFolders).thenReturn(true); + when(workspace.workspaceFolders).thenReturn([workspaceFolder]); + when(workspace.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspace)); + when(serviceContainer.get<IWorkspaceService>(IWorkspaceService, anything())).thenReturn( + instance(workspace), + ); + + const locator = new Locator('hello-World', instance(serviceContainer), true); + + const key = locator.getCacheKey(resource); + + expect(key).contains('hello-World'); + expect(key).contains(md5(workspaceFolder.uri.fsPath)); + }); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/condaEnvFileService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/condaEnvFileService.unit.test.ts new file mode 100644 index 000000000000..b41d11374b28 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/condaEnvFileService.unit.test.ts @@ -0,0 +1,133 @@ +import * as assert from 'assert'; +import { EOL } from 'os'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory } from '../../../../client/common/types'; +import { + ICondaService, + IInterpreterHelper, + IInterpreterLocatorService, +} from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { AnacondaCompanyName } from '../../../../client/pythonEnvironments/discovery/locators/services/conda'; +import { CondaEnvFileService } from '../../../../client/pythonEnvironments/discovery/locators/services/condaEnvFileService'; +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; +import { MockState } from '../../../interpreters/mocks'; + +const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); +const environmentsFilePath = path.join(environmentsPath, 'environments.txt'); + +// tslint:disable-next-line:max-func-body-length +suite('Interpreters from Conda Environments Text File', () => { + let condaService: TypeMoq.IMock<ICondaService>; + let interpreterHelper: TypeMoq.IMock<IInterpreterHelper>; + let condaFileProvider: IInterpreterLocatorService; + let fileSystem: TypeMoq.IMock<IFileSystem>; + setup(() => { + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + const stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory))) + .returns(() => stateFactory.object); + const state = new MockState(undefined); + stateFactory + .setup((s) => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => state); + + condaService = TypeMoq.Mock.ofType<ICondaService>(); + interpreterHelper = TypeMoq.Mock.ofType<IInterpreterHelper>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + condaFileProvider = new CondaEnvFileService( + interpreterHelper.object, + condaService.object, + fileSystem.object, + serviceContainer.object, + ); + }); + test('Must return an empty list if environment file cannot be found', async () => { + condaService.setup((c) => c.condaEnvironmentsFile).returns(() => undefined); + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: undefined })); + const interpreters = await condaFileProvider.getInterpreters(); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must return an empty list for an empty file', async () => { + condaService.setup((c) => c.condaEnvironmentsFile).returns(() => environmentsFilePath); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(environmentsFilePath))) + .returns(() => Promise.resolve(true)); + fileSystem + .setup((fs) => fs.readFile(TypeMoq.It.isValue(environmentsFilePath))) + .returns(() => Promise.resolve('')); + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: undefined })); + const interpreters = await condaFileProvider.getInterpreters(); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + + async function filterFilesInEnvironmentsFileAndReturnValidItems(isWindows: boolean) { + const validPaths = [ + path.join(environmentsPath, 'conda', 'envs', 'numpy'), + path.join(environmentsPath, 'conda', 'envs', 'scipy'), + ]; + const interpreterPaths = [ + path.join(environmentsPath, 'xyz', 'one'), + path.join(environmentsPath, 'xyz', 'two'), + path.join(environmentsPath, 'xyz', 'python.exe'), + ].concat(validPaths); + condaService.setup((c) => c.condaEnvironmentsFile).returns(() => environmentsFilePath); + condaService + .setup((c) => c.getInterpreterPath(TypeMoq.It.isAny())) + .returns((environmentPath) => (isWindows + ? path.join(environmentPath, 'python.exe') + : path.join(environmentPath, 'bin', 'python'))); + condaService + .setup((c) => c.getCondaEnvironments(TypeMoq.It.isAny())) + .returns(() => { + const condaEnvironments = validPaths.map((item) => ({ + path: item, + name: path.basename(item), + })); + return Promise.resolve(condaEnvironments); + }); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(environmentsFilePath))) + .returns(() => Promise.resolve(true)); + fileSystem + .setup((fs) => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((p1: string, p2: string) => (isWindows ? p1 === p2 : p1.toUpperCase() === p2.toUpperCase())); + validPaths.forEach((validPath) => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)); + }); + + fileSystem + .setup((fs) => fs.readFile(TypeMoq.It.isValue(environmentsFilePath))) + .returns(() => Promise.resolve(interpreterPaths.join(EOL))); + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: undefined })); + + const interpreters = await condaFileProvider.getInterpreters(); + + const expectedPythonPath = isWindows + ? path.join(validPaths[0], 'python.exe') + : path.join(validPaths[0], 'bin', 'python'); + assert.equal(interpreters.length, 2, 'Incorrect number of entries'); + assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect display name'); + assert.equal(interpreters[0].path, expectedPythonPath, 'Incorrect path'); + assert.equal(interpreters[0].envPath, validPaths[0], 'Incorrect envpath'); + assert.equal(interpreters[0].envType, EnvironmentType.Conda, 'Incorrect type'); + } + test('Must filter files in the list and return valid items (non windows)', async () => { + await filterFilesInEnvironmentsFileAndReturnValidItems(false); + }); + test('Must filter files in the list and return valid items (windows)', async () => { + await filterFilesInEnvironmentsFileAndReturnValidItems(true); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/condaEnvService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/condaEnvService.unit.test.ts new file mode 100644 index 000000000000..f3aba85024cd --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/condaEnvService.unit.test.ts @@ -0,0 +1,476 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { IPersistentStateFactory } from '../../../../client/common/types'; +import { ICondaService, IInterpreterHelper } from '../../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../../client/interpreter/helpers'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { + AnacondaCompanyName, + CondaInfo, + parseCondaInfo, +} from '../../../../client/pythonEnvironments/discovery/locators/services/conda'; +import { CondaEnvService } from '../../../../client/pythonEnvironments/discovery/locators/services/condaEnvService'; +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; +import { MockState } from '../../../interpreters/mocks'; +import { UnitTestIocContainer } from '../../../testing/serviceRegistry'; + +const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); + +// tslint:disable-next-line:max-func-body-length +suite('Interpreters from Conda Environments', () => { + let ioc: UnitTestIocContainer; + let condaProvider: CondaEnvService; + let condaService: TypeMoq.IMock<ICondaService>; + let interpreterHelper: TypeMoq.IMock<InterpreterHelper>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + setup(() => { + initializeDI(); + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + const stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory))) + .returns(() => stateFactory.object); + const state = new MockState(undefined); + stateFactory + .setup((s) => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => state); + + condaService = TypeMoq.Mock.ofType<ICondaService>(); + interpreterHelper = TypeMoq.Mock.ofType<InterpreterHelper>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + condaProvider = new CondaEnvService( + condaService.object, + interpreterHelper.object, + serviceContainer.object, + fileSystem.object, + ); + }); + teardown(() => ioc.dispose()); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + } + function _parseCondaInfo(info: CondaInfo, conda: ICondaService, fs: IFileSystem, h: IInterpreterHelper) { + return parseCondaInfo(info, conda.getInterpreterPath, fs.fileExists, h.getInterpreterInformation); + } + + test('Must return an empty list for empty json', async () => { + const interpreters = await _parseCondaInfo( + // tslint:disable-next-line:no-any prefer-type-cast + {} as any, + condaService.object, + fileSystem.object, + interpreterHelper.object, + ); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + + async function extractDisplayNameFromVersionInfo(isWindows: boolean) { + const info = { + envs: [ + path.join(environmentsPath, 'conda', 'envs', 'numpy'), + path.join(environmentsPath, 'conda', 'envs', 'scipy'), + ], + default_prefix: '', + 'sys.version': + '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]', + }; + condaService + .setup((c) => c.getInterpreterPath(TypeMoq.It.isAny())) + .returns((environmentPath) => (isWindows + ? path.join(environmentPath, 'python.exe') + : path.join(environmentPath, 'bin', 'python'))); + info.envs.forEach((validPath) => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)); + }); + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: undefined })); + + const interpreters = await _parseCondaInfo( + info, + condaService.object, + fileSystem.object, + interpreterHelper.object, + ); + assert.equal(interpreters.length, 2, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal( + interpreters[0].companyDisplayName, + AnacondaCompanyName, + 'Incorrect company display name for first env', + ); + assert.equal(interpreters[0].envType, EnvironmentType.Conda, 'Environment not detected as a conda environment'); + + const path2 = path.join(info.envs[1], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[1].path, path2, 'Incorrect path for first env'); + assert.equal( + interpreters[1].companyDisplayName, + AnacondaCompanyName, + 'Incorrect company display name for first env', + ); + assert.equal(interpreters[1].envType, EnvironmentType.Conda, 'Environment not detected as a conda environment'); + } + test('Must extract display name from version info (non windows)', async () => { + await extractDisplayNameFromVersionInfo(false); + }); + test('Must extract display name from version info (windows)', async () => { + await extractDisplayNameFromVersionInfo(true); + }); + async function extractDisplayNameFromVersionInfoSuffixedWithEnvironmentName(isWindows: boolean) { + const info = { + envs: [ + path.join(environmentsPath, 'conda', 'envs', 'numpy'), + path.join(environmentsPath, 'conda', 'envs', 'scipy'), + ], + default_prefix: path.join(environmentsPath, 'conda', 'envs', 'root'), + 'sys.version': + '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]', + }; + condaService + .setup((c) => c.getInterpreterPath(TypeMoq.It.isAny())) + .returns((environmentPath) => (isWindows + ? path.join(environmentPath, 'python.exe') + : path.join(environmentPath, 'bin', 'python'))); + info.envs.forEach((validPath) => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)); + }); + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: undefined })); + condaService.setup((c) => c.getCondaFile()).returns(() => Promise.resolve('conda')); + condaService.setup((c) => c.getCondaInfo()).returns(() => Promise.resolve(info)); + condaService + .setup((c) => c.getCondaEnvironments(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([ + { name: 'base', path: environmentsPath }, + { name: 'numpy', path: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { name: 'scipy', path: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, + ])); + fileSystem + .setup((fs) => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((p1: string, p2: string) => (isWindows ? p1 === p2 : p1.toUpperCase() === p2.toUpperCase())); + + const interpreters = await condaProvider.getInterpreters(); + assert.equal(interpreters.length, 2, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal( + interpreters[0].companyDisplayName, + AnacondaCompanyName, + 'Incorrect company display name for first env', + ); + assert.equal(interpreters[0].envType, EnvironmentType.Conda, 'Environment not detected as a conda environment'); + + const path2 = path.join(info.envs[1], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[1].path, path2, 'Incorrect path for first env'); + assert.equal( + interpreters[1].companyDisplayName, + AnacondaCompanyName, + 'Incorrect company display name for first env', + ); + assert.equal(interpreters[1].envType, EnvironmentType.Conda, 'Environment not detected as a conda environment'); + } + test('Must extract display name from version info suffixed with the environment name (oxs/linux)', async () => { + await extractDisplayNameFromVersionInfoSuffixedWithEnvironmentName(false); + }); + test('Must extract display name from version info suffixed with the environment name (windows)', async () => { + await extractDisplayNameFromVersionInfoSuffixedWithEnvironmentName(true); + }); + + async function useDefaultNameIfSysVersionIsInvalid(isWindows: boolean) { + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')], + default_prefix: '', + 'sys.version': '3.6.1 |Anaonda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]', + }; + condaService + .setup((c) => c.getInterpreterPath(TypeMoq.It.isAny())) + .returns((environmentPath) => (isWindows + ? path.join(environmentPath, 'python.exe') + : path.join(environmentPath, 'bin', 'python'))); + info.envs.forEach((validPath) => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)); + }); + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: undefined })); + + const interpreters = await _parseCondaInfo( + info, + condaService.object, + fileSystem.object, + interpreterHelper.object, + ); + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal( + interpreters[0].companyDisplayName, + AnacondaCompanyName, + 'Incorrect company display name for first env', + ); + assert.equal(interpreters[0].envType, EnvironmentType.Conda, 'Environment not detected as a conda environment'); + } + test('Must use the default display name if sys.version is invalid (non windows)', async () => { + await useDefaultNameIfSysVersionIsInvalid(false); + }); + test('Must use the default display name if sys.version is invalid (windows)', async () => { + await useDefaultNameIfSysVersionIsInvalid(true); + }); + + async function useDefaultNameIfSysVersionIsValidAndSuffixWithEnvironmentName(isWindows: boolean) { + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')], + default_prefix: '', + 'sys.version': '3.6.1 |Anaonda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]', + }; + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: undefined })); + condaService.setup((c) => c.getCondaInfo()).returns(() => Promise.resolve(info)); + condaService + .setup((c) => c.getCondaEnvironments(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([ + { name: 'base', path: environmentsPath }, + { name: 'numpy', path: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { name: 'scipy', path: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, + ])); + condaService + .setup((c) => c.getInterpreterPath(TypeMoq.It.isAny())) + .returns((environmentPath) => (isWindows + ? path.join(environmentPath, 'python.exe') + : path.join(environmentPath, 'bin', 'python'))); + info.envs.forEach((validPath) => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)); + }); + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + fileSystem + .setup((fs) => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((p1: string, p2: string) => (isWindows ? p1 === p2 : p1.toUpperCase() === p2.toUpperCase())); + + const interpreters = await condaProvider.getInterpreters(); + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal( + interpreters[0].companyDisplayName, + AnacondaCompanyName, + 'Incorrect company display name for first env', + ); + assert.equal(interpreters[0].envType, EnvironmentType.Conda, 'Environment not detected as a conda environment'); + } + test('Must use the default display name if sys.version is invalid and suffixed with environment name (non windows)', async () => { + await useDefaultNameIfSysVersionIsValidAndSuffixWithEnvironmentName(false); + }); + test('Must use the default display name if sys.version is invalid and suffixed with environment name (windows)', async () => { + await useDefaultNameIfSysVersionIsValidAndSuffixWithEnvironmentName(false); + }); + + async function useDefaultNameIfSysVersionIsEmpty(isWindows: boolean) { + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')], + }; + condaService + .setup((c) => c.getInterpreterPath(TypeMoq.It.isAny())) + .returns((environmentPath) => (isWindows + ? path.join(environmentPath, 'python.exe') + : path.join(environmentPath, 'bin', 'python'))); + info.envs.forEach((validPath) => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)); + }); + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: undefined })); + + const interpreters = await _parseCondaInfo( + info, + condaService.object, + fileSystem.object, + interpreterHelper.object, + ); + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal( + interpreters[0].companyDisplayName, + AnacondaCompanyName, + 'Incorrect company display name for first env', + ); + assert.equal(interpreters[0].envType, EnvironmentType.Conda, 'Environment not detected as a conda environment'); + } + + test('Must use the default display name if sys.version is empty (non windows)', async () => { + await useDefaultNameIfSysVersionIsEmpty(false); + }); + test('Must use the default display name if sys.version is empty (windows)', async () => { + await useDefaultNameIfSysVersionIsEmpty(true); + }); + + async function useDefaultNameIfSysVersionIsEmptyAndSuffixWithEnvironmentName(isWindows: boolean) { + const info = { + envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')], + }; + condaService + .setup((c) => c.getInterpreterPath(TypeMoq.It.isAny())) + .returns((environmentPath) => (isWindows + ? path.join(environmentPath, 'python.exe') + : path.join(environmentPath, 'bin', 'python'))); + info.envs.forEach((validPath) => { + const pythonPath = isWindows ? path.join(validPath, 'python.exe') : path.join(validPath, 'bin', 'python'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)); + }); + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: undefined })); + condaService.setup((c) => c.getCondaFile()).returns(() => Promise.resolve('conda')); + condaService.setup((c) => c.getCondaInfo()).returns(() => Promise.resolve(info)); + condaService + .setup((c) => c.getCondaEnvironments(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([ + { name: 'base', path: environmentsPath }, + { name: 'numpy', path: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { name: 'scipy', path: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, + ])); + fileSystem + .setup((fs) => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((p1: string, p2: string) => (isWindows ? p1 === p2 : p1.toUpperCase() === p2.toUpperCase())); + + const interpreters = await condaProvider.getInterpreters(); + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + + const path1 = path.join(info.envs[0], isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal( + interpreters[0].companyDisplayName, + AnacondaCompanyName, + 'Incorrect company display name for first env', + ); + assert.equal(interpreters[0].envType, EnvironmentType.Conda, 'Environment not detected as a conda environment'); + } + test('Must use the default display name if sys.version is empty and suffixed with environment name (non windows)', async () => { + await useDefaultNameIfSysVersionIsEmptyAndSuffixWithEnvironmentName(false); + }); + test('Must use the default display name if sys.version is empty and suffixed with environment name (windows)', async () => { + await useDefaultNameIfSysVersionIsEmptyAndSuffixWithEnvironmentName(true); + }); + + async function includeDefaultPrefixIntoListOfInterpreters(isWindows: boolean) { + const info = { + default_prefix: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + }; + condaService + .setup((c) => c.getInterpreterPath(TypeMoq.It.isAny())) + .returns((environmentPath) => (isWindows + ? path.join(environmentPath, 'python.exe') + : path.join(environmentPath, 'bin', 'python'))); + const pythonPath = isWindows + ? path.join(info.default_prefix, 'python.exe') + : path.join(info.default_prefix, 'bin', 'python'); + fileSystem.setup((fs) => fs.fileExists(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: undefined })); + + const interpreters = await _parseCondaInfo( + info, + condaService.object, + fileSystem.object, + interpreterHelper.object, + ); + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + + const path1 = path.join(info.default_prefix, isWindows ? 'python.exe' : path.join('bin', 'python')); + assert.equal(interpreters[0].path, path1, 'Incorrect path for first env'); + assert.equal( + interpreters[0].companyDisplayName, + AnacondaCompanyName, + 'Incorrect company display name for first env', + ); + assert.equal(interpreters[0].envType, EnvironmentType.Conda, 'Environment not detected as a conda environment'); + } + test('Must include the default_prefix into the list of interpreters (non windows)', async () => { + await includeDefaultPrefixIntoListOfInterpreters(false); + }); + test('Must include the default_prefix into the list of interpreters (windows)', async () => { + await includeDefaultPrefixIntoListOfInterpreters(true); + }); + + async function excludeInterpretersThatDoNotExistOnFileSystem(isWindows: boolean) { + const info = { + envs: [ + path.join(environmentsPath, 'conda', 'envs', 'numpy'), + path.join(environmentsPath, 'path0', 'one.exe'), + path.join(environmentsPath, 'path1', 'one.exe'), + path.join(environmentsPath, 'path2', 'one.exe'), + path.join(environmentsPath, 'conda', 'envs', 'scipy'), + path.join(environmentsPath, 'path3', 'three.exe'), + ], + }; + const validPaths = info.envs.filter((_, index) => index % 2 === 0); + interpreterHelper + .setup((i) => i.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: undefined })); + validPaths.forEach((envPath) => { + condaService + .setup((c) => c.getInterpreterPath(TypeMoq.It.isValue(envPath))) + .returns((environmentPath) => (isWindows + ? path.join(environmentPath, 'python.exe') + : path.join(environmentPath, 'bin', 'python'))); + const pythonPath = isWindows ? path.join(envPath, 'python.exe') : path.join(envPath, 'bin', 'python'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)); + }); + + const interpreters = await _parseCondaInfo( + info, + condaService.object, + fileSystem.object, + interpreterHelper.object, + ); + + assert.equal(interpreters.length, validPaths.length, 'Incorrect number of entries'); + validPaths.forEach((envPath, index) => { + assert.equal(interpreters[index].envPath!, envPath, 'Incorrect env path'); + const pythonPath = isWindows ? path.join(envPath, 'python.exe') : path.join(envPath, 'bin', 'python'); + assert.equal(interpreters[index].path, pythonPath, 'Incorrect python Path'); + }); + } + + test('Must exclude interpreters that do not exist on disc (non windows)', async () => { + await excludeInterpretersThatDoNotExistOnFileSystem(false); + }); + test('Must exclude interpreters that do not exist on disc (windows)', async () => { + await excludeInterpretersThatDoNotExistOnFileSystem(true); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/condaHelper.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/condaHelper.unit.test.ts new file mode 100644 index 000000000000..a2ea82d84f69 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/condaHelper.unit.test.ts @@ -0,0 +1,106 @@ +import * as assert from 'assert'; +import { expect } from 'chai'; +import { + AnacondaDisplayName, + CondaInfo, +} from '../../../../client/pythonEnvironments/discovery/locators/services/conda'; +import { + getDisplayName, + parseCondaEnvFileContents, +} from '../../../../client/pythonEnvironments/discovery/locators/services/condaHelper'; + +// tslint:disable-next-line:max-func-body-length +suite('Interpreters display name from Conda Environments', () => { + test('Must return default display name for invalid Conda Info', () => { + assert.equal(getDisplayName(), AnacondaDisplayName, 'Incorrect display name'); + assert.equal(getDisplayName({}), AnacondaDisplayName, 'Incorrect display name'); + }); + test('Must return at least Python Version', () => { + const info: CondaInfo = { + python_version: '3.6.1.final.10', + }; + const displayName = getDisplayName(info); + assert.equal(displayName, AnacondaDisplayName, 'Incorrect display name'); + }); + test('Must return info without first part if not a python version', () => { + const info: CondaInfo = { + 'sys.version': + '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]', + }; + const displayName = getDisplayName(info); + assert.equal(displayName, 'Anaconda 4.4.0 (64-bit)', 'Incorrect display name'); + }); + test("Must return info without prefixing with word 'Python'", () => { + const info: CondaInfo = { + python_version: '3.6.1.final.10', + 'sys.version': + '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]', + }; + const displayName = getDisplayName(info); + assert.equal(displayName, 'Anaconda 4.4.0 (64-bit)', 'Incorrect display name'); + }); + test('Must include Ananconda name if Company name not found', () => { + const info: CondaInfo = { + python_version: '3.6.1.final.10', + 'sys.version': '3.6.1 |4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]', + }; + const displayName = getDisplayName(info); + assert.equal(displayName, `4.4.0 (64-bit) : ${AnacondaDisplayName}`, 'Incorrect display name'); + }); + test('Parse conda environments', () => { + // tslint:disable-next-line:no-multiline-string + const environments = ` +# conda environments: +# +base * /Users/donjayamanne/anaconda3 + * /Users/donjayamanne/anaconda3 +one /Users/donjayamanne/anaconda3/envs/one + one /Users/donjayamanne/anaconda3/envs/ one +one two /Users/donjayamanne/anaconda3/envs/one two +three /Users/donjayamanne/anaconda3/envs/three + /Users/donjayamanne/anaconda3/envs/four + /Users/donjayamanne/anaconda3/envs/five six +aaaa_bbbb_cccc_dddd_eeee_ffff_gggg /Users/donjayamanne/anaconda3/envs/aaaa_bbbb_cccc_dddd_eeee_ffff_gggg +aaaa_bbbb_cccc_dddd_eeee_ffff_gggg * /Users/donjayamanne/anaconda3/envs/aaaa_bbbb_cccc_dddd_eeee_ffff_gggg +with*star /Users/donjayamanne/anaconda3/envs/with*star +with*one*two*three*four*five*six*seven* /Users/donjayamanne/anaconda3/envs/with*one*two*three*four*five*six*seven* +with*one*two*three*four*five*six*seven* * /Users/donjayamanne/anaconda3/envs/with*one*two*three*four*five*six*seven* + /Users/donjayamanne/anaconda3/envs/seven `; // note the space after seven + + const expectedList = [ + { name: 'base', path: '/Users/donjayamanne/anaconda3', isActive: true }, + { name: '', path: '/Users/donjayamanne/anaconda3', isActive: true }, + { name: 'one', path: '/Users/donjayamanne/anaconda3/envs/one', isActive: false }, + { name: ' one', path: '/Users/donjayamanne/anaconda3/envs/ one', isActive: false }, + { name: 'one two', path: '/Users/donjayamanne/anaconda3/envs/one two', isActive: false }, + { name: 'three', path: '/Users/donjayamanne/anaconda3/envs/three', isActive: false }, + { name: '', path: '/Users/donjayamanne/anaconda3/envs/four', isActive: false }, + { name: '', path: '/Users/donjayamanne/anaconda3/envs/five six', isActive: false }, + { + name: 'aaaa_bbbb_cccc_dddd_eeee_ffff_gggg', + path: '/Users/donjayamanne/anaconda3/envs/aaaa_bbbb_cccc_dddd_eeee_ffff_gggg', + isActive: false, + }, + { + name: 'aaaa_bbbb_cccc_dddd_eeee_ffff_gggg', + path: '/Users/donjayamanne/anaconda3/envs/aaaa_bbbb_cccc_dddd_eeee_ffff_gggg', + isActive: true, + }, + { name: 'with*star', path: '/Users/donjayamanne/anaconda3/envs/with*star', isActive: false }, + { + name: 'with*one*two*three*four*five*six*seven*', + path: '/Users/donjayamanne/anaconda3/envs/with*one*two*three*four*five*six*seven*', + isActive: false, + }, + { + name: 'with*one*two*three*four*five*six*seven*', + path: '/Users/donjayamanne/anaconda3/envs/with*one*two*three*four*five*six*seven*', + isActive: true, + }, + { name: '', path: '/Users/donjayamanne/anaconda3/envs/seven ', isActive: false }, + ]; + + const list = parseCondaEnvFileContents(environments); + expect(list).deep.equal(expectedList); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/condaService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/condaService.unit.test.ts new file mode 100644 index 000000000000..09375027bdd8 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/condaService.unit.test.ts @@ -0,0 +1,1057 @@ +// tslint:disable:no-require-imports no-var-requires no-any max-func-body-length +import * as assert from 'assert'; +import { expect } from 'chai'; +import { EOL } from 'os'; +import * as path from 'path'; +import { parse, SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { Disposable, EventEmitter } from 'vscode'; + +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { FileSystemPaths, FileSystemPathUtils } from '../../../../client/common/platform/fs-paths'; +import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../../../client/common/process/types'; +import { ITerminalActivationCommandProvider } from '../../../../client/common/terminal/types'; +import { IConfigurationService, IPersistentStateFactory, IPythonSettings } from '../../../../client/common/types'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { IInterpreterLocatorService, IInterpreterService } from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { CondaService } from '../../../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { MockState } from '../../../interpreters/mocks'; + +const untildify: (value: string) => string = require('untildify'); + +const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); +const info: PythonEnvironment = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + envType: EnvironmentType.Unknown, + version: new SemVer('0.0.0-alpha'), + sysPrefix: '', + sysVersion: '', +}; + +suite('Interpreters Conda Service', () => { + let processService: TypeMoq.IMock<IProcessService>; + let platformService: TypeMoq.IMock<IPlatformService>; + let condaService: CondaService; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let config: TypeMoq.IMock<IConfigurationService>; + let settings: TypeMoq.IMock<IPythonSettings>; + let registryInterpreterLocatorService: TypeMoq.IMock<IInterpreterLocatorService>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; + let persistentStateFactory: TypeMoq.IMock<IPersistentStateFactory>; + let condaPathSetting: string; + let disposableRegistry: Disposable[]; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let mockState: MockState; + let terminalProvider: TypeMoq.IMock<ITerminalActivationCommandProvider>; + setup(async () => { + condaPathSetting = ''; + processService = TypeMoq.Mock.ofType<IProcessService>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + persistentStateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + registryInterpreterLocatorService = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + config = TypeMoq.Mock.ofType<IConfigurationService>(); + settings = TypeMoq.Mock.ofType<IPythonSettings>(); + procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + processService.setup((x: any) => x.then).returns(() => undefined); + procServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + disposableRegistry = []; + const e = new EventEmitter<void>(); + interpreterService.setup((x) => x.onDidChangeInterpreter).returns(() => e.event); + resetMockState(undefined); + persistentStateFactory + .setup((s) => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => mockState); + + terminalProvider = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); + terminalProvider.setup((p) => p.isShellSupported(TypeMoq.It.isAny())).returns(() => true); + terminalProvider + .setup((p) => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(['activate'])); + terminalProvider + .setup((p) => p.getActivationCommandsForInterpreter!(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(['activate'])); + + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())) + .returns(() => procServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())) + .returns(() => platformService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) + .returns(() => fileSystem.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) + .returns(() => config.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalActivationCommandProvider), TypeMoq.It.isAny())) + .returns(() => terminalProvider.object); + serviceContainer + .setup((c) => c.getAll(TypeMoq.It.isValue(ITerminalActivationCommandProvider), TypeMoq.It.isAny())) + .returns(() => [terminalProvider.object]); + config.setup((c) => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => settings.object); + settings.setup((p) => p.condaPath).returns(() => condaPathSetting); + fileSystem + .setup((fs) => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((p1, p2) => { + const utils = FileSystemPathUtils.withDefaults( + FileSystemPaths.withDefaults(platformService.object.isWindows), + ); + return utils.arePathsSame(p1, p2); + }); + + condaService = new CondaService( + procServiceFactory.object, + platformService.object, + fileSystem.object, + persistentStateFactory.object, + config.object, + disposableRegistry, + workspaceService.object, + registryInterpreterLocatorService.object, + ); + }); + + function resetMockState(data: any) { + mockState = new MockState(data); + } + + async function identifyPythonPathAsCondaEnvironment( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + pythonPath: string, + ) { + platformService.setup((p) => p.isLinux).returns(() => isLinux); + platformService.setup((p) => p.isWindows).returns(() => isWindows); + platformService.setup((p) => p.isMac).returns(() => isOsx); + + const isCondaEnv = await condaService.isCondaEnvironment(pythonPath); + expect(isCondaEnv).to.be.equal(true, 'Path not identified as a conda path'); + } + + test('Correctly identifies a python path as a conda environment (windows)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await identifyPythonPathAsCondaEnvironment(true, false, false, pythonPath); + }); + + test('Correctly identifies a python path as a conda environment (linux)', async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await identifyPythonPathAsCondaEnvironment(false, false, true, pythonPath); + }); + + test('Correctly identifies a python path as a conda environment (osx)', async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await identifyPythonPathAsCondaEnvironment(false, true, false, pythonPath); + }); + + async function identifyPythonPathAsNonCondaEnvironment( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + pythonPath: string, + ) { + platformService.setup((p) => p.isLinux).returns(() => isLinux); + platformService.setup((p) => p.isWindows).returns(() => isWindows); + platformService.setup((p) => p.isMac).returns(() => isOsx); + + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(false)); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))) + .returns(() => Promise.resolve(false)); + + const isCondaEnv = await condaService.isCondaEnvironment(pythonPath); + expect(isCondaEnv).to.be.equal(false, 'Path incorrectly identified as a conda path'); + } + + test('Correctly identifies a python path as a non-conda environment (windows)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); + await identifyPythonPathAsNonCondaEnvironment(true, false, false, pythonPath); + }); + + test('Correctly identifies a python path as a non-conda environment (linux)', async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await identifyPythonPathAsNonCondaEnvironment(false, false, true, pythonPath); + }); + + test('Correctly identifies a python path as a non-conda environment (osx)', async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await identifyPythonPathAsNonCondaEnvironment(false, true, false, pythonPath); + }); + + async function checkCondaNameAndPathForCondaEnvironments( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + pythonPath: string, + condaEnvsPath: string, + expectedCondaEnv?: { name: string; path: string }, + ) { + const condaEnvironments = [ + { name: 'One', path: path.join(condaEnvsPath, 'one') }, + { name: 'Three', path: path.join(condaEnvsPath, 'three') }, + { name: 'Seven', path: path.join(condaEnvsPath, 'seven') }, + { name: 'Eight', path: path.join(condaEnvsPath, 'Eight 8') }, + { name: 'nine 9', path: path.join(condaEnvsPath, 'nine 9') }, + ]; + + platformService.setup((p) => p.isLinux).returns(() => isLinux); + platformService.setup((p) => p.isWindows).returns(() => isWindows); + platformService.setup((p) => p.isMac).returns(() => isOsx); + + resetMockState({ data: condaEnvironments }); + + const condaEnv = await condaService.getCondaEnvironment(pythonPath); + expect(condaEnv).deep.equal(expectedCondaEnv, 'Conda environment not identified'); + } + + test('Correctly retrieves conda environment (windows)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'python.exe'); + const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); + + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await checkCondaNameAndPathForCondaEnvironments(true, false, false, pythonPath, condaEnvDir, { + name: 'One', + path: path.dirname(pythonPath), + }); + }); + + test('Correctly retrieves conda environment with spaces in env name (windows)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'eight 8', 'python.exe'); + const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); + + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await checkCondaNameAndPathForCondaEnvironments(true, false, false, pythonPath, condaEnvDir, { + name: 'Eight', + path: path.dirname(pythonPath), + }); + }); + + test('Correctly retrieves conda environment (osx)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'bin', 'python'); + const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); + + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await checkCondaNameAndPathForCondaEnvironments(false, true, false, pythonPath, condaEnvDir, { + name: 'One', + path: path.join(path.dirname(pythonPath), '..'), + }); + }); + + test('Correctly retrieves conda environment with spaces in env name (osx)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'Eight 8', 'bin', 'python'); + const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); + + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await checkCondaNameAndPathForCondaEnvironments(false, true, false, pythonPath, condaEnvDir, { + name: 'Eight', + path: path.join(path.dirname(pythonPath), '..'), + }); + }); + + test('Correctly retrieves conda environment (linux)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'bin', 'python'); + const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); + + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await checkCondaNameAndPathForCondaEnvironments(false, false, true, pythonPath, condaEnvDir, { + name: 'One', + path: path.join(path.dirname(pythonPath), '..'), + }); + }); + + test('Correctly retrieves conda environment with spaces in env name (linux)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'Eight 8', 'bin', 'python'); + const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); + + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await checkCondaNameAndPathForCondaEnvironments(false, false, true, pythonPath, condaEnvDir, { + name: 'Eight', + path: path.join(path.dirname(pythonPath), '..'), + }); + }); + + test('Ignore cache if environment is not found in the cache (conda env is detected second time round)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'newEnvironment', 'python.exe'); + const condaEnvsPath = path.join('c', 'users', 'xyz', '.conda', 'envs'); + + const condaEnvironments = [ + { name: 'One', path: path.join(condaEnvsPath, 'one') }, + { name: 'Three', path: path.join(condaEnvsPath, 'three') }, + { name: 'Seven', path: path.join(condaEnvsPath, 'seven') }, + { name: 'Eight', path: path.join(condaEnvsPath, 'Eight 8') }, + { name: 'nine 9', path: path.join(condaEnvsPath, 'nine 9') }, + ]; + + platformService.setup((p) => p.isLinux).returns(() => false); + platformService.setup((p) => p.isWindows).returns(() => true); + platformService.setup((p) => p.isMac).returns(() => false); + + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); + resetMockState({ data: condaEnvironments }); + + const envList = [ + '# conda environments:', + '#', + 'base * /Users/donjayamanne/anaconda3', + 'one /Users/donjayamanne/anaconda3/envs/one', + 'one two /Users/donjayamanne/anaconda3/envs/one two', + 'py27 /Users/donjayamanne/anaconda3/envs/py27', + 'py36 /Users/donjayamanne/anaconda3/envs/py36', + 'three /Users/donjayamanne/anaconda3/envs/three', + `newEnvironment ${path.join(condaEnvsPath, 'newEnvironment')}`, + ]; + + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'xyz' })); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: envList.join(EOL) })); + + const condaEnv = await condaService.getCondaEnvironment(pythonPath); + expect(condaEnv).deep.equal( + { name: 'newEnvironment', path: path.dirname(pythonPath) }, + 'Conda environment not identified after ignoring cache', + ); + expect(mockState.data.data).lengthOf(7, 'Incorrect number of items in the cache'); + }); + + test('Ignore cache if environment is not found in the cache (cond env is not detected in conda env list)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'newEnvironment', 'python.exe'); + const condaEnvsPath = path.join('c', 'users', 'xyz', '.conda', 'envs'); + + const condaEnvironments = [ + { name: 'One', path: path.join(condaEnvsPath, 'one') }, + { name: 'Three', path: path.join(condaEnvsPath, 'three') }, + { name: 'Seven', path: path.join(condaEnvsPath, 'seven') }, + { name: 'Eight', path: path.join(condaEnvsPath, 'Eight 8') }, + { name: 'nine 9', path: path.join(condaEnvsPath, 'nine 9') }, + ]; + + platformService.setup((p) => p.isLinux).returns(() => false); + platformService.setup((p) => p.isWindows).returns(() => true); + platformService.setup((p) => p.isMac).returns(() => false); + + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); + resetMockState({ data: condaEnvironments }); + + const envList = [ + '# conda environments:', + '#', + 'base * /Users/donjayamanne/anaconda3', + 'one /Users/donjayamanne/anaconda3/envs/one', + 'one two /Users/donjayamanne/anaconda3/envs/one two', + 'py27 /Users/donjayamanne/anaconda3/envs/py27', + 'py36 /Users/donjayamanne/anaconda3/envs/py36', + 'three /Users/donjayamanne/anaconda3/envs/three', + ]; + + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'xyz' })); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: envList.join(EOL) })); + + const condaEnv = await condaService.getCondaEnvironment(pythonPath); + expect(condaEnv).deep.equal(undefined, 'Conda environment incorrectly identified after ignoring cache'); + expect(mockState.data.data).lengthOf(6, 'Incorrect number of items in the cache'); + }); + + test('Must use Conda env from Registry to locate conda.exe', async () => { + const condaPythonExePath = path.join('dumyPath', 'environments', 'conda', 'Scripts', 'python.exe'); + const registryInterpreters: PythonEnvironment[] = [ + { + displayName: 'One', + path: path.join(environmentsPath, 'path1', 'one.exe'), + companyDisplayName: 'One 1', + version: new SemVer('1.0.0'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Anaconda', + path: condaPythonExePath, + companyDisplayName: 'Two 2', + version: new SemVer('1.11.0'), + envType: EnvironmentType.Conda, + }, + { + displayName: 'Three', + path: path.join(environmentsPath, 'path2', 'one.exe'), + companyDisplayName: 'Three 3', + version: new SemVer('2.10.1'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Seven', + path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + companyDisplayName: 'Continuum Analytics, Inc.', + envType: EnvironmentType.Unknown, + }, + ].map((item) => ({ ...info, ...item })); + const condaInterpreterIndex = registryInterpreters.findIndex((i) => i.displayName === 'Anaconda'); + const expectedCodnaPath = path.join( + path.dirname(registryInterpreters[condaInterpreterIndex].path), + 'conda.exe', + ); + platformService.setup((p) => p.isWindows).returns(() => true); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('Not Found'))); + registryInterpreterLocatorService + .setup((r) => r.getInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(registryInterpreters)); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isAny())) + .returns((file: string) => Promise.resolve(file === expectedCodnaPath)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, expectedCodnaPath, 'Failed to identify conda.exe'); + }); + + test('Must use Conda env from Registry to latest version of locate conda.exe', async () => { + const condaPythonExePath = path.join('dumyPath', 'environments'); + const registryInterpreters: PythonEnvironment[] = [ + { + displayName: 'One', + path: path.join(environmentsPath, 'path1', 'one.exe'), + companyDisplayName: 'One 1', + version: new SemVer('1.0.0'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Anaconda', + path: path.join(condaPythonExePath, 'conda1', 'Scripts', 'python.exe'), + companyDisplayName: 'Two 1', + version: new SemVer('1.11.0'), + envType: EnvironmentType.Conda, + }, + { + displayName: 'Anaconda', + path: path.join(condaPythonExePath, 'conda211', 'Scripts', 'python.exe'), + companyDisplayName: 'Two 2.11', + version: new SemVer('2.11.0'), + envType: EnvironmentType.Conda, + }, + { + displayName: 'Anaconda', + path: path.join(condaPythonExePath, 'conda231', 'Scripts', 'python.exe'), + companyDisplayName: 'Two 2.31', + version: new SemVer('2.31.0'), + envType: EnvironmentType.Conda, + }, + { + displayName: 'Anaconda', + path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), + companyDisplayName: 'Two 2.21', + version: new SemVer('2.21.0'), + envType: EnvironmentType.Conda, + }, + { + displayName: 'Three', + path: path.join(environmentsPath, 'path2', 'one.exe'), + companyDisplayName: 'Three 3', + version: new SemVer('2.10.1'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Seven', + path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + companyDisplayName: 'Continuum Analytics, Inc.', + envType: EnvironmentType.Unknown, + }, + ].map((item) => ({ ...info, ...item })); + const indexOfLatestVersion = 3; + const expectedCodnaPath = path.join(path.dirname(registryInterpreters[indexOfLatestVersion].path), 'conda.exe'); + platformService.setup((p) => p.isWindows).returns(() => true); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('Not Found'))); + registryInterpreterLocatorService + .setup((r) => r.getInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(registryInterpreters)); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isAny())) + .returns((file: string) => Promise.resolve(file === expectedCodnaPath)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, expectedCodnaPath, 'Failed to identify conda.exe'); + }); + + test("Must use 'conda' if conda.exe cannot be located using registry entries", async () => { + const condaPythonExePath = path.join('dumyPath', 'environments'); + const registryInterpreters: PythonEnvironment[] = [ + { + displayName: 'One', + path: path.join(environmentsPath, 'path1', 'one.exe'), + companyDisplayName: 'One 1', + version: new SemVer('1.0.0'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Anaconda', + path: path.join(condaPythonExePath, 'conda1', 'Scripts', 'python.exe'), + companyDisplayName: 'Two 1', + version: new SemVer('1.11.0'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Anaconda', + path: path.join(condaPythonExePath, 'conda211', 'Scripts', 'python.exe'), + companyDisplayName: 'Two 2.11', + version: new SemVer('2.11.0'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Anaconda', + path: path.join(condaPythonExePath, 'conda231', 'Scripts', 'python.exe'), + companyDisplayName: 'Two 2.31', + version: new SemVer('2.31.0'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Anaconda', + path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), + companyDisplayName: 'Two 2.21', + version: new SemVer('2.21.0'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Three', + path: path.join(environmentsPath, 'path2', 'one.exe'), + companyDisplayName: 'Three 3', + version: new SemVer('2.10.1'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Seven', + path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + companyDisplayName: 'Continuum Analytics, Inc.', + envType: EnvironmentType.Unknown, + }, + ].map((item) => ({ ...info, ...item })); + platformService.setup((p) => p.isWindows).returns(() => true); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('Not Found'))); + registryInterpreterLocatorService + .setup((r) => r.getInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(registryInterpreters)); + fileSystem.setup((fs) => fs.search(TypeMoq.It.isAnyString())).returns(async () => []); + fileSystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns((_file: string) => Promise.resolve(false)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, 'conda', 'Failed to identify conda.exe'); + }); + + test('Get conda file from default/known locations', async () => { + const expected = 'C:/ProgramData/Miniconda2/Scripts/conda.exe'; + + platformService.setup((p) => p.isWindows).returns(() => true); + + fileSystem.setup((f) => f.search(TypeMoq.It.isAnyString())).returns(() => Promise.resolve([expected])); + const CondaServiceForTesting = class extends CondaService { + public async isCondaInCurrentPath() { + return false; + } + }; + const condaSrv = new CondaServiceForTesting( + procServiceFactory.object, + platformService.object, + fileSystem.object, + persistentStateFactory.object, + config.object, + disposableRegistry, + workspaceService.object, + ); + + const result = await condaSrv.getCondaFile(); + expect(result).is.equal(expected); + }); + + test("Must use 'python.condaPath' setting if set", async () => { + condaPathSetting = 'spam-spam-conda-spam-spam'; + // We ensure that conda would otherwise be found. + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']))) + .returns(() => Promise.resolve({ stdout: 'xyz' })) + .verifiable(TypeMoq.Times.never()); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, 'spam-spam-conda-spam-spam', 'Failed to identify conda.exe'); + + // We should not try to call other unwanted methods. + processService.verifyAll(); + registryInterpreterLocatorService.verify((r) => r.getInterpreters(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + + test("Must use 'conda' if is available in the current path", async () => { + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']))) + .returns(() => Promise.resolve({ stdout: 'xyz' })); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, 'conda', 'Failed to identify conda.exe'); + + // We should not try to call other unwanted methods. + registryInterpreterLocatorService.verify((r) => r.getInterpreters(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + + test('Must invoke process only once to check if conda is in the current path', async () => { + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']))) + .returns(() => Promise.resolve({ stdout: 'xyz' })); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, 'conda', 'Failed to identify conda.exe'); + processService.verify( + (p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + + // We should not try to call other unwanted methods. + registryInterpreterLocatorService.verify((r) => r.getInterpreters(TypeMoq.It.isAny()), TypeMoq.Times.never()); + + await condaService.getCondaFile(); + processService.verify( + (p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + }); + + [ + '~/anaconda/bin/conda', + '~/miniconda/bin/conda', + '~/anaconda2/bin/conda', + '~/miniconda2/bin/conda', + '~/anaconda3/bin/conda', + '~/miniconda3/bin/conda', + ].forEach((knownLocation) => { + test(`Must return conda path from known location '${knownLocation}' (non windows)`, async () => { + const expectedCondaLocation = untildify(knownLocation); + platformService.setup((p) => p.isWindows).returns(() => false); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('Not Found'))); + fileSystem + .setup((fs) => fs.search(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([expectedCondaLocation])); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(expectedCondaLocation))) + .returns(() => Promise.resolve(true)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, expectedCondaLocation, 'Failed to identify'); + }); + }); + + test("Must return 'conda' if conda could not be found in known locations", async () => { + platformService.setup((p) => p.isWindows).returns(() => false); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('Not Found'))); + fileSystem.setup((fs) => fs.search(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + fileSystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns((_file: string) => Promise.resolve(false)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, 'conda', 'Failed to identify'); + }); + + test('Correctly identify interpreter location relative to entironment path (non windows)', async () => { + const environmentPath = path.join('a', 'b', 'c'); + platformService.setup((p) => p.isWindows).returns(() => false); + const pythonPath = condaService.getInterpreterPath(environmentPath); + assert.equal(pythonPath, path.join(environmentPath, 'bin', 'python'), 'Incorrect path'); + }); + + test('Correctly identify interpreter location relative to entironment path (windows)', async () => { + const environmentPath = path.join('a', 'b', 'c'); + platformService.setup((p) => p.isWindows).returns(() => true); + const pythonPath = condaService.getInterpreterPath(environmentPath); + assert.equal(pythonPath, path.join(environmentPath, 'python.exe'), 'Incorrect path'); + }); + + test('Returns condaInfo when conda exists', async () => { + const expectedInfo = { + envs: [ + path.join(environmentsPath, 'conda', 'envs', 'numpy'), + path.join(environmentsPath, 'conda', 'envs', 'scipy'), + ], + default_prefix: '', + 'sys.version': + '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]', + }; + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'xyz' })); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: JSON.stringify(expectedInfo) })); + + const condaInfo = await condaService.getCondaInfo(); + assert.deepEqual(condaInfo, expectedInfo, 'Conda info does not match'); + }); + + test("Returns undefined if there's and error in getting the info", async () => { + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'xyz' })); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('unknown'))); + + const condaInfo = await condaService.getCondaInfo(); + assert.equal(condaInfo, undefined, 'Conda info does not match'); + }); + + test('Returns conda environments when conda exists', async () => { + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'xyz' })); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: '' })); + const environments = await condaService.getCondaEnvironments(true); + assert.equal(environments, undefined, 'Conda environments do not match'); + }); + + test('Logs information message when conda does not exist', async () => { + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('Not Found'))); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('Not Found'))); + const environments = await condaService.getCondaEnvironments(true); + assert.equal(environments, undefined, 'Conda environments do not match'); + }); + + test('Returns cached conda environments', async () => { + resetMockState({ data: 'CachedInfo' }); + + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'xyz' })); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: '' })); + const environments = await condaService.getCondaEnvironments(false); + assert.equal(environments, 'CachedInfo', 'Conda environments do not match'); + }); + + test('Subsequent list of environments will be retrieved from cache', async () => { + const envList = [ + '# conda environments:', + '#', + 'base * /Users/donjayamanne/anaconda3', + 'one /Users/donjayamanne/anaconda3/envs/one', + 'one two /Users/donjayamanne/anaconda3/envs/one two', + 'py27 /Users/donjayamanne/anaconda3/envs/py27', + 'py36 /Users/donjayamanne/anaconda3/envs/py36', + 'three /Users/donjayamanne/anaconda3/envs/three', + ]; + + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'xyz' })); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: envList.join(EOL) })); + const environments = await condaService.getCondaEnvironments(false); + expect(environments).lengthOf(6, 'Incorrect number of environments'); + expect(mockState.data.data).lengthOf(6, 'Incorrect number of environments in cache'); + + mockState.data.data = []; + const environmentsFetchedAgain = await condaService.getCondaEnvironments(false); + expect(environmentsFetchedAgain).lengthOf(0, 'Incorrect number of environments fetched from cache'); + }); + + test("Returns undefined if there's and error in getting the info", async () => { + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'xyz' })); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('unknown'))); + + const condaInfo = await condaService.getCondaInfo(); + assert.equal(condaInfo, undefined, 'Conda info does not match'); + }); + + test('Must use Conda env from Registry to locate conda.exe', async () => { + const condaPythonExePath = path.join( + __dirname, + '..', + '..', + '..', + 'src', + 'test', + 'pythonFiles', + 'environments', + 'conda', + 'Scripts', + 'python.exe', + ); + const registryInterpreters: PythonEnvironment[] = [ + { + displayName: 'One', + path: path.join(environmentsPath, 'path1', 'one.exe'), + companyDisplayName: 'One 1', + version: new SemVer('1.0.0'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Anaconda', + path: condaPythonExePath, + companyDisplayName: 'Two 2', + version: new SemVer('1.11.0'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Three', + path: path.join(environmentsPath, 'path2', 'one.exe'), + companyDisplayName: 'Three 3', + version: new SemVer('2.10.1'), + envType: EnvironmentType.Unknown, + }, + { + displayName: 'Seven', + path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + companyDisplayName: 'Continuum Analytics, Inc.', + envType: EnvironmentType.Unknown, + }, + ].map((item) => ({ ...info, ...item })); + + const expectedCodaExe = path.join(path.dirname(condaPythonExePath), 'conda.exe'); + + platformService.setup((p) => p.isWindows).returns(() => true); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('Not Found'))); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(expectedCodaExe))) + .returns(() => Promise.resolve(true)); + registryInterpreterLocatorService + .setup((r) => r.getInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(registryInterpreters)); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, expectedCodaExe, 'Failed to identify conda.exe'); + }); + + test('isAvailable will return true if conda is available', async () => { + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: '4.4.4' })); + const isAvailable = await condaService.isCondaAvailable(); + assert.equal(isAvailable, true); + }); + + test('isAvailable will return false if conda is not available', async () => { + condaService.getCondaFile = () => Promise.resolve('conda'); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('not found'))); + fileSystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + fileSystem.setup((fs) => fs.search(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + platformService.setup((p) => p.isWindows).returns(() => false); + condaService.getCondaInfo = () => Promise.reject('Not Found'); + const isAvailable = await condaService.isCondaAvailable(); + assert.equal(isAvailable, false); + }); + + test('Version info from conda process will be returned in getCondaVersion', async () => { + condaService.getCondaInfo = () => Promise.reject('Not Found'); + condaService.getCondaFile = () => Promise.resolve('conda'); + const expectedVersion = parse('4.4.4')!.raw; + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: '4.4.4' })); + + const version = await condaService.getCondaVersion(); + assert.equal(version!.raw, expectedVersion); + }); + + test('isCondaInCurrentPath will return true if conda is available', async () => { + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'xyz' })); + const isAvailable = await condaService.isCondaInCurrentPath(); + assert.equal(isAvailable, true); + }); + + test('isCondaInCurrentPath will return false if conda is not available', async () => { + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('not found'))); + fileSystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + platformService.setup((p) => p.isWindows).returns(() => false); + + const isAvailable = await condaService.isCondaInCurrentPath(); + assert.equal(isAvailable, false); + }); + + async function testFailureOfGettingCondaEnvironments( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + pythonPath: string, + ) { + platformService.setup((p) => p.isLinux).returns(() => isLinux); + platformService.setup((p) => p.isWindows).returns(() => isWindows); + platformService.setup((p) => p.isMac).returns(() => isOsx); + + resetMockState({ data: undefined }); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'some value' })); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())) + .returns(() => Promise.reject(new Error('Failed'))); + const condaEnv = await condaService.getCondaEnvironment(pythonPath); + expect(condaEnv).to.be.equal(undefined, 'Conda should be undefined'); + } + test('Fails to identify an environment as a conda env (windows)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'python.exe'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await testFailureOfGettingCondaEnvironments(true, false, false, pythonPath); + }); + test('Fails to identify an environment as a conda env (linux)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'python'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await testFailureOfGettingCondaEnvironments(false, false, true, pythonPath); + }); + test('Fails to identify an environment as a conda env (osx)', async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'python'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await testFailureOfGettingCondaEnvironments(false, true, false, pythonPath); + }); + + type InterpreterSearchTestParams = { + pythonPath: string; + environmentName: string; + isLinux: boolean; + expectedCondaPath: string; + }; + + const testsForInterpreter: InterpreterSearchTestParams[] = [ + { + pythonPath: path.join('users', 'foo', 'envs', 'test1', 'python'), + environmentName: 'test1', + isLinux: true, + expectedCondaPath: path.join('users', 'foo', 'bin', 'conda'), + }, + { + pythonPath: path.join('users', 'foo', 'envs', 'test2', 'python'), + environmentName: 'test2', + isLinux: true, + expectedCondaPath: path.join('users', 'foo', 'envs', 'test2', 'conda'), + }, + { + pythonPath: path.join('users', 'foo', 'envs', 'test3', 'python'), + environmentName: 'test3', + isLinux: false, + expectedCondaPath: path.join('users', 'foo', 'Scripts', 'conda.exe'), + }, + { + pythonPath: path.join('users', 'foo', 'envs', 'test4', 'python'), + environmentName: 'test4', + isLinux: false, + expectedCondaPath: path.join('users', 'foo', 'conda.exe'), + }, + ]; + + testsForInterpreter.forEach((t) => { + test(`Finds conda.exe for subenvironment ${t.environmentName}`, async () => { + platformService.setup((p) => p.isLinux).returns(() => t.isLinux); + platformService.setup((p) => p.isWindows).returns(() => !t.isLinux); + platformService.setup((p) => p.isMac).returns(() => false); + fileSystem + .setup((f) => f.fileExists( + TypeMoq.It.is((p) => { + if (p === t.expectedCondaPath) { + return true; + } + return false; + }), + )) + .returns(() => Promise.resolve(true)); + + const condaFile = await condaService.getCondaFileFromInterpreter(t.pythonPath, t.environmentName); + assert.equal(condaFile, t.expectedCondaPath); + }); + test(`Finds conda.exe for different ${t.environmentName}`, async () => { + platformService.setup((p) => p.isLinux).returns(() => t.isLinux); + platformService.setup((p) => p.isWindows).returns(() => !t.isLinux); + platformService.setup((p) => p.isMac).returns(() => false); + fileSystem + .setup((f) => f.fileExists( + TypeMoq.It.is((p) => { + if (p === t.expectedCondaPath) { + return true; + } + return false; + }), + )) + .returns(() => Promise.resolve(true)); + + const condaFile = await condaService.getCondaFileFromInterpreter(t.pythonPath, undefined); + + // This should only work if the expectedConda path has the original environment name in it + if (t.expectedCondaPath.includes(t.environmentName)) { + assert.equal(condaFile, t.expectedCondaPath); + } else { + assert.equal(condaFile, undefined); + } + }); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/hasProviderFactory.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/hasProviderFactory.unit.test.ts new file mode 100644 index 000000000000..8960905eb1f0 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/hasProviderFactory.unit.test.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-classes-per-file max-func-body-length + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { + instance, mock, verify, when, +} from 'ts-mockito'; +import { Uri } from 'vscode'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { IConfigurationService } from '../../../../client/common/types'; +import { IInterpreterHashProvider } from '../../../../client/interpreter/locators/types'; +import { InterpreterHashProvider } from '../../../../client/pythonEnvironments/discovery/locators/services/hashProvider'; +import { InterpeterHashProviderFactory } from '../../../../client/pythonEnvironments/discovery/locators/services/hashProviderFactory'; +import { WindowsStoreInterpreter } from '../../../../client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter'; + +use(chaiAsPromised); + +suite('Interpretersx - Interpreter Hash Provider Factory', () => { + let configService: IConfigurationService; + let windowsStoreInterpreter: WindowsStoreInterpreter; + let standardHashProvider: IInterpreterHashProvider; + let factory: InterpeterHashProviderFactory; + setup(() => { + configService = mock(ConfigurationService); + windowsStoreInterpreter = mock(WindowsStoreInterpreter); + standardHashProvider = mock(InterpreterHashProvider); + const windowsStoreInstance = instance(windowsStoreInterpreter); + (windowsStoreInstance as any).then = undefined; + factory = new InterpeterHashProviderFactory( + instance(configService), + windowsStoreInstance, + windowsStoreInstance, + instance(standardHashProvider), + ); + }); + test('When provided python path is not a window store interpreter return standard hash provider', async () => { + const pythonPath = 'NonWindowsInterpreterPath'; + when(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).thenReturn(false); + + const provider = await factory.create({ pythonPath }); + + expect(provider).to.deep.equal(instance(standardHashProvider)); + verify(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).once(); + }); + test('When provided python path is a windows store interpreter return windows store hash provider', async () => { + const pythonPath = 'NonWindowsInterpreterPath'; + when(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).thenReturn(true); + + const provider = await factory.create({ pythonPath }); + + expect(provider).to.deep.equal(instance(windowsStoreInterpreter)); + verify(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).once(); + }); + test('When provided resource resolves to a python path that is not a window store interpreter return standard hash provider', async () => { + const pythonPath = 'NonWindowsInterpreterPath'; + const resource = Uri.file('1'); + when(configService.getSettings(resource)).thenReturn({ pythonPath } as any); + when(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).thenReturn(false); + + const provider = await factory.create({ resource }); + + expect(provider).to.deep.equal(instance(standardHashProvider)); + verify(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).once(); + }); + test('When provided resource resolves to a python path that is a windows store interpreter return windows store hash provider', async () => { + const pythonPath = 'NonWindowsInterpreterPath'; + const resource = Uri.file('1'); + when(configService.getSettings(resource)).thenReturn({ pythonPath } as any); + when(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).thenReturn(true); + + const provider = await factory.create({ resource }); + + expect(provider).to.deep.equal(instance(windowsStoreInterpreter)); + verify(windowsStoreInterpreter.isWindowsStoreInterpreter(pythonPath)).once(); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/hashProvider.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/hashProvider.unit.test.ts new file mode 100644 index 000000000000..aeebbc08b678 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/hashProvider.unit.test.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-classes-per-file max-func-body-length + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { + instance, mock, verify, when, +} from 'ts-mockito'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { InterpreterHashProvider } from '../../../../client/pythonEnvironments/discovery/locators/services/hashProvider'; + +use(chaiAsPromised); + +suite('Interpreters - Interpreter Hash Provider', () => { + let hashProvider: InterpreterHashProvider; + let fs: IFileSystem; + setup(() => { + fs = mock(FileSystem); + hashProvider = new InterpreterHashProvider(instance(fs)); + }); + test('Get hash from fs', async () => { + const pythonPath = 'WindowsInterpreterPath'; + when(fs.getFileHash(pythonPath)).thenResolve('hash'); + + const hash = await hashProvider.getInterpreterHash(pythonPath); + + expect(hash).to.equal('hash'); + verify(fs.getFileHash(pythonPath)).once(); + }); + test('Exceptios from fs.getFilehash will be bubbled up', async () => { + const pythonPath = 'WindowsInterpreterPath'; + when(fs.getFileHash(pythonPath)).thenReject(new Error('Kaboom')); + + const promise = hashProvider.getInterpreterHash(pythonPath); + + verify(fs.getFileHash(pythonPath)).once(); + await expect(promise).to.eventually.be.rejectedWith('Kaboom'); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/helpers.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/helpers.unit.test.ts new file mode 100644 index 000000000000..1f597d9782e1 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/helpers.unit.test.ts @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { + anything, instance, mock, when, +} from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { IInterpreterHelper, IInterpreterLocatorHelper } from '../../../../client/interpreter/contracts'; +import { IPipEnvServiceHelper } from '../../../../client/interpreter/locators/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { InterpreterLocatorHelper } from '../../../../client/pythonEnvironments/discovery/locators/helpers'; +import { PipEnvServiceHelper } from '../../../../client/pythonEnvironments/discovery/locators/services/pipEnvServiceHelper'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +enum OS { + Windows = 'Windows', + Linux = 'Linux', + Mac = 'Mac' +} + +suite('Interpreters - Locators Helper', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let platform: TypeMoq.IMock<IPlatformService>; + let helper: IInterpreterLocatorHelper; + let fs: TypeMoq.IMock<IFileSystem>; + let pipEnvHelper: IPipEnvServiceHelper; + let interpreterServiceHelper: TypeMoq.IMock<IInterpreterHelper>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + platform = TypeMoq.Mock.ofType<IPlatformService>(); + fs = TypeMoq.Mock.ofType<IFileSystem>(); + pipEnvHelper = mock(PipEnvServiceHelper); + interpreterServiceHelper = TypeMoq.Mock.ofType<IInterpreterHelper>(); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platform.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fs.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterHelper))) + .returns(() => interpreterServiceHelper.object); + + helper = new InterpreterLocatorHelper(fs.object, instance(pipEnvHelper)); + }); + test('Ensure default Mac interpreter is not excluded from the list of interpreters', async () => { + platform.setup((p) => p.isWindows).returns(() => false); + platform.setup((p) => p.isLinux).returns(() => false); + platform + .setup((p) => p.isMac) + .returns(() => true) + .verifiable(TypeMoq.Times.never()); + fs.setup((f) => f.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const interpreters: PythonEnvironment[] = []; + ['conda', 'virtualenv', 'mac', 'pyenv'].forEach((name) => { + const interpreter = { + architecture: Architecture.Unknown, + displayName: name, + path: path.join('users', 'python', 'bin', name), + sysPrefix: name, + sysVersion: name, + envType: EnvironmentType.Unknown, + version: new SemVer('0.0.0-alpha'), + }; + interpreters.push(interpreter); + + // Treat 'mac' as as mac interpreter. + interpreterServiceHelper + .setup((i) => i.isMacDefaultPythonPath(TypeMoq.It.isValue(interpreter.path))) + .returns(() => name === 'mac') + .verifiable(TypeMoq.Times.never()); + }); + + const expectedInterpreters = interpreters.slice(0); + when(pipEnvHelper.getPipEnvInfo(anything())).thenResolve(); + + const items = await helper.mergeInterpreters(interpreters); + + interpreterServiceHelper.verifyAll(); + platform.verifyAll(); + fs.verifyAll(); + expect(items).to.be.lengthOf(4); + expect(items).to.be.deep.equal(expectedInterpreters); + }); + getNamesAndValues<OS>(OS).forEach((os) => { + test(`Ensure duplicates are removed (same version and same interpreter directory on ${os.name})`, async () => { + interpreterServiceHelper.setup((i) => i.isMacDefaultPythonPath(TypeMoq.It.isAny())).returns(() => false); + platform.setup((p) => p.isWindows).returns(() => os.value === OS.Windows); + platform.setup((p) => p.isLinux).returns(() => os.value === OS.Linux); + platform.setup((p) => p.isMac).returns(() => os.value === OS.Mac); + fs.setup((f) => f.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((a, b) => a === b) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const interpreters: PythonEnvironment[] = []; + const expectedInterpreters: PythonEnvironment[] = []; + // Unique python paths and versions. + ['3.6', '3.6', '2.7', '2.7'].forEach((name, index) => { + const interpreter = { + architecture: Architecture.Unknown, + displayName: name, + path: path.join('users', `python${name}${index}`, 'bin', name + index.toString()), + sysPrefix: name, + sysVersion: name, + envType: EnvironmentType.Unknown, + version: new SemVer(`3.${parseInt(name.substr(-1), 10)}.0-final`), + }; + interpreters.push(interpreter); + expectedInterpreters.push(interpreter); + }); + // Same versions, but different executables. + ['3.6', '3.6', '3.7', '3.7'].forEach((name, index) => { + const interpreter = { + architecture: Architecture.Unknown, + displayName: name, + path: path.join('users', 'python', 'bin', 'python.exe'), + sysPrefix: name, + sysVersion: name, + envType: EnvironmentType.Unknown, + version: new SemVer(`3.${parseInt(name.substr(-1), 10)}.0-final`), + }; + + const duplicateInterpreter = { + architecture: Architecture.Unknown, + displayName: name, + path: path.join('users', 'python', 'bin', `python${name}.exe`), + sysPrefix: name, + sysVersion: name, + envType: EnvironmentType.Unknown, + version: new SemVer(interpreter.version.raw), + }; + + interpreters.push(interpreter); + interpreters.push(duplicateInterpreter); + if (index % 2 === 1) { + expectedInterpreters.push(interpreter); + } + }); + + when(pipEnvHelper.getPipEnvInfo(anything())).thenResolve(); + const items = await helper.mergeInterpreters(interpreters); + + interpreterServiceHelper.verifyAll(); + platform.verifyAll(); + fs.verifyAll(); + expect(items).to.be.lengthOf(expectedInterpreters.length); + expect(items).to.be.deep.equal(expectedInterpreters); + }); + }); + getNamesAndValues<OS>(OS).forEach((os) => { + test(`Ensure interpreter types are identified from other locators (${os.name})`, async () => { + interpreterServiceHelper.setup((i) => i.isMacDefaultPythonPath(TypeMoq.It.isAny())).returns(() => false); + platform.setup((p) => p.isWindows).returns(() => os.value === OS.Windows); + platform.setup((p) => p.isLinux).returns(() => os.value === OS.Linux); + platform.setup((p) => p.isMac).returns(() => os.value === OS.Mac); + fs.setup((f) => f.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((a, b) => a === b && a === path.join('users', 'python', 'bin')) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const interpreters: PythonEnvironment[] = []; + const expectedInterpreters: PythonEnvironment[] = []; + ['3.6', '3.6'].forEach((name, index) => { + // Ensure the type in the first item is 'Unknown', + // and type in second item is known (e.g. Conda). + const type = index === 0 ? EnvironmentType.Unknown : EnvironmentType.Pipenv; + const interpreter = { + architecture: Architecture.Unknown, + displayName: name, + path: path.join('users', 'python', 'bin', 'python.exe'), + sysPrefix: name, + sysVersion: name, + envType: type, + version: new SemVer(`3.${parseInt(name.substr(-1), 10)}.0-final`), + }; + interpreters.push(interpreter); + + if (index === 1) { + expectedInterpreters.push(interpreter); + } + }); + + when(pipEnvHelper.getPipEnvInfo(anything())).thenResolve(); + const items = await helper.mergeInterpreters(interpreters); + + interpreterServiceHelper.verifyAll(); + platform.verifyAll(); + fs.verifyAll(); + expect(items).to.be.lengthOf(1); + expect(items).to.be.deep.equal(expectedInterpreters); + }); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts new file mode 100644 index 000000000000..557b59e5ebd3 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { IDisposableRegistry } from '../../../../client/common/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { Architecture, OSType } from '../../../../client/common/utils/platform'; +import { + CONDA_ENV_FILE_SERVICE, + CONDA_ENV_SERVICE, + CURRENT_PATH_SERVICE, + GLOBAL_VIRTUAL_ENV_SERVICE, + IInterpreterLocatorHelper, + IInterpreterLocatorService, + KNOWN_PATH_SERVICE, + PIPENV_SERVICE, + WINDOWS_REGISTRY_SERVICE, + WORKSPACE_VIRTUAL_ENV_SERVICE, +} from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { PythonInterpreterLocatorService } from '../../../../client/pythonEnvironments/discovery/locators'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +suite('Interpreters - Locators Index', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let platformSvc: TypeMoq.IMock<IPlatformService>; + let helper: TypeMoq.IMock<IInterpreterLocatorHelper>; + let locator: IInterpreterLocatorService; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + platformSvc = TypeMoq.Mock.ofType<IPlatformService>(); + helper = TypeMoq.Mock.ofType<IInterpreterLocatorHelper>(); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformSvc.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterLocatorHelper))) + .returns(() => helper.object); + + locator = new PythonInterpreterLocatorService(serviceContainer.object); + }); + [undefined, Uri.file('Something')].forEach((resource) => { + getNamesAndValues<OSType>(OSType).forEach((osType) => { + if (osType.value === OSType.Unknown) { + return; + } + const testSuffix = `(on ${osType.name}, with${resource ? '' : 'out'} a resource)`; + test(`All Interpreter Sources are used ${testSuffix}`, async () => { + const locatorsTypes: string[] = []; + if (osType.value === OSType.Windows) { + locatorsTypes.push(WINDOWS_REGISTRY_SERVICE); + } + platformSvc.setup((p) => p.osType).returns(() => osType.value); + platformSvc.setup((p) => p.isWindows).returns(() => osType.value === OSType.Windows); + platformSvc.setup((p) => p.isLinux).returns(() => osType.value === OSType.Linux); + platformSvc.setup((p) => p.isMac).returns(() => osType.value === OSType.OSX); + + locatorsTypes.push(CONDA_ENV_SERVICE); + locatorsTypes.push(CONDA_ENV_FILE_SERVICE); + locatorsTypes.push(PIPENV_SERVICE); + locatorsTypes.push(GLOBAL_VIRTUAL_ENV_SERVICE); + locatorsTypes.push(WORKSPACE_VIRTUAL_ENV_SERVICE); + locatorsTypes.push(KNOWN_PATH_SERVICE); + locatorsTypes.push(CURRENT_PATH_SERVICE); + + const locatorsWithInterpreters = locatorsTypes.map((typeName) => { + const interpreter: PythonEnvironment = { + architecture: Architecture.Unknown, + displayName: typeName, + path: typeName, + sysPrefix: typeName, + sysVersion: typeName, + envType: EnvironmentType.Unknown, + version: new SemVer('0.0.0-alpha'), + }; + + const typeLocator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + typeLocator + .setup((l) => l.hasInterpreters) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + typeLocator + .setup((l) => l.getInterpreters(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve([interpreter])) + .verifiable(TypeMoq.Times.once()); + + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterLocatorService), TypeMoq.It.isValue(typeName))) + .returns(() => typeLocator.object); + + return { + type: typeName, + locator: typeLocator, + interpreters: [interpreter], + }; + }); + + helper + .setup((h) => h.mergeInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(locatorsWithInterpreters.map((item) => item.interpreters[0]))) + .verifiable(TypeMoq.Times.once()); + + await locator.getInterpreters(resource); + + locatorsWithInterpreters.forEach((item) => item.locator.verifyAll()); + helper.verifyAll(); + }); + test(`Interpreter Sources are sorted correctly and merged ${testSuffix}`, async () => { + const locatorsTypes: string[] = []; + if (osType.value === OSType.Windows) { + locatorsTypes.push(WINDOWS_REGISTRY_SERVICE); + } + platformSvc.setup((p) => p.osType).returns(() => osType.value); + platformSvc.setup((p) => p.isWindows).returns(() => osType.value === OSType.Windows); + platformSvc.setup((p) => p.isLinux).returns(() => osType.value === OSType.Linux); + platformSvc.setup((p) => p.isMac).returns(() => osType.value === OSType.OSX); + + locatorsTypes.push(CONDA_ENV_SERVICE); + locatorsTypes.push(CONDA_ENV_FILE_SERVICE); + locatorsTypes.push(PIPENV_SERVICE); + locatorsTypes.push(GLOBAL_VIRTUAL_ENV_SERVICE); + locatorsTypes.push(WORKSPACE_VIRTUAL_ENV_SERVICE); + locatorsTypes.push(KNOWN_PATH_SERVICE); + locatorsTypes.push(CURRENT_PATH_SERVICE); + + const locatorsWithInterpreters = locatorsTypes.map((typeName) => { + const interpreter: PythonEnvironment = { + architecture: Architecture.Unknown, + displayName: typeName, + path: typeName, + sysPrefix: typeName, + sysVersion: typeName, + envType: EnvironmentType.Unknown, + version: new SemVer('0.0.0-alpha'), + }; + + const typeLocator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + typeLocator + .setup((l) => l.hasInterpreters) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + typeLocator + .setup((l) => l.getInterpreters(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve([interpreter])) + .verifiable(TypeMoq.Times.once()); + + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterLocatorService), TypeMoq.It.isValue(typeName))) + .returns(() => typeLocator.object); + + return { + type: typeName, + locator: typeLocator, + interpreters: [interpreter], + }; + }); + + const expectedInterpreters = locatorsWithInterpreters.map((item) => item.interpreters[0]); + + helper + .setup((h) => h.mergeInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(expectedInterpreters)) + .verifiable(TypeMoq.Times.once()); + + const interpreters = await locator.getInterpreters(resource); + + locatorsWithInterpreters.forEach((item) => item.locator.verifyAll()); + helper.verifyAll(); + expect(interpreters).to.be.deep.equal(expectedInterpreters); + }); + test(`didTriggerInterpreterSuggestions is set to true in the locators if onSuggestion is true ${testSuffix}`, async () => { + const locatorsTypes: string[] = []; + if (osType.value === OSType.Windows) { + locatorsTypes.push(WINDOWS_REGISTRY_SERVICE); + } + platformSvc.setup((p) => p.osType).returns(() => osType.value); + platformSvc.setup((p) => p.isWindows).returns(() => osType.value === OSType.Windows); + platformSvc.setup((p) => p.isLinux).returns(() => osType.value === OSType.Linux); + platformSvc.setup((p) => p.isMac).returns(() => osType.value === OSType.OSX); + + locatorsTypes.push(CONDA_ENV_SERVICE); + locatorsTypes.push(CONDA_ENV_FILE_SERVICE); + locatorsTypes.push(PIPENV_SERVICE); + locatorsTypes.push(GLOBAL_VIRTUAL_ENV_SERVICE); + locatorsTypes.push(WORKSPACE_VIRTUAL_ENV_SERVICE); + locatorsTypes.push(KNOWN_PATH_SERVICE); + locatorsTypes.push(CURRENT_PATH_SERVICE); + + const locatorsWithInterpreters = locatorsTypes.map((typeName) => { + const interpreter: PythonEnvironment = { + architecture: Architecture.Unknown, + displayName: typeName, + path: typeName, + sysPrefix: typeName, + sysVersion: typeName, + envType: EnvironmentType.Unknown, + version: new SemVer('0.0.0-alpha'), + }; + + const typeLocator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); + typeLocator + .setup((l) => l.hasInterpreters) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + + typeLocator + .setup((l) => l.getInterpreters(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve([interpreter])) + .verifiable(TypeMoq.Times.once()); + + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterLocatorService), TypeMoq.It.isValue(typeName))) + .returns(() => typeLocator.object); + + return { + type: typeName, + locator: typeLocator, + interpreters: [interpreter], + }; + }); + + helper + .setup((h) => h.mergeInterpreters(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(locatorsWithInterpreters.map((item) => item.interpreters[0]))); + + await locator.getInterpreters(resource, { onSuggestion: true }); + + locatorsWithInterpreters.forEach((item) => item.locator.verify( + (l) => (l.didTriggerInterpreterSuggestions = true), TypeMoq.Times.once(), + )); + expect(locator.didTriggerInterpreterSuggestions).to.equal( + true, + 'didTriggerInterpreterSuggestions should be set to true.', + ); + }); + }); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/interpreterFilter.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/interpreterFilter.unit.test.ts new file mode 100644 index 000000000000..96b4ec449675 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/interpreterFilter.unit.test.ts @@ -0,0 +1,58 @@ +import { expect } from 'chai'; +import { isHiddenInterpreter } from '../../../../client/pythonEnvironments/discovery/locators/services/interpreterFilter'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +// tslint:disable:no-unused-expression + +suite('Interpreters - Filter', () => { + const doNotHideThesePaths = [ + 'python', + 'python.exe', + 'python2', + 'python2.exe', + 'python38', + 'python3.8.exe', + 'C:\\Users\\SomeUser\\AppData\\Local\\Microsoft\\WindowsApps\\python.exe', + '%USERPROFILE%\\AppData\\Local\\Microsoft\\WindowsApps\\python.exe', + '%LOCALAPPDATA%\\Microsoft\\WindowsApps\\python.exe', + ]; + const hideThesePaths = [ + '%USERPROFILE%\\AppData\\Local\\Microsoft\\WindowsApps\\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\\python.exe', + 'C:\\Users\\SomeUser\\AppData\\Local\\Microsoft\\WindowsApps\\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\\python.exe', + '%USERPROFILE%\\AppData\\Local\\Microsoft\\WindowsApps\\PythonSoftwareFoundation\\python.exe', + 'C:\\Users\\SomeUser\\AppData\\Local\\Microsoft\\WindowsApps\\PythonSoftwareFoundation\\python.exe', + '%LOCALAPPDATA%\\Microsoft\\WindowsApps\\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\\python.exe', + '%LOCALAPPDATA%\\Microsoft\\WindowsApps\\PythonSoftwareFoundation\\python.exe', + 'C:\\Program Files\\WindowsApps\\python.exe', + 'C:\\Program Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\\python.exe', + 'C:\\Program Files\\WindowsApps\\PythonSoftwareFoundation\\python.exe', + ]; + + function getInterpreterFromPath(interpreterPath: string): PythonEnvironment { + return { + path: interpreterPath, + sysVersion: '', + sysPrefix: '', + architecture: 1, + companyDisplayName: '', + displayName: 'python', + envType: EnvironmentType.WindowsStore, + envName: '', + envPath: '', + cachedEntry: false, + }; + } + + doNotHideThesePaths.forEach((interpreterPath) => { + test(`Interpreter path should NOT be hidden - ${interpreterPath}`, () => { + const interpreter: PythonEnvironment = getInterpreterFromPath(interpreterPath); + expect(isHiddenInterpreter(interpreter), `${interpreterPath} should NOT be treated as hidden.`).to.be.false; + }); + }); + hideThesePaths.forEach((interpreterPath) => { + test(`Interpreter path should be hidden - ${interpreterPath}`, () => { + const interpreter: PythonEnvironment = getInterpreterFromPath(interpreterPath); + expect(isHiddenInterpreter(interpreter), `${interpreterPath} should be treated as hidden.`).to.be.true; + }); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/interpreterLocatorService.testvirtualenvs.ts b/src/test/pythonEnvironments/discovery/locators/interpreterLocatorService.testvirtualenvs.ts new file mode 100644 index 000000000000..51e49c8f859b --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/interpreterLocatorService.testvirtualenvs.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import { RegistryImplementation } from '../../../../client/common/platform/registry'; +import { IRegistry } from '../../../../client/common/platform/types'; +import { IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE } from '../../../../client/interpreter/contracts'; +import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { getOSType, OSType } from '../../../common'; +import { TEST_TIMEOUT } from '../../../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../../../initialize'; +import { UnitTestIocContainer } from '../../../testing/serviceRegistry'; + +suite('Python interpreter locator service', () => { + let ioc: UnitTestIocContainer; + let interpreters: PythonEnvironment[]; + suiteSetup(async function () { + // https://github.com/microsoft/vscode-python/issues/12634 + // tslint:disable-next-line: no-invalid-this + return this.skip(); + // tslint:disable-next-line:no-invalid-this + this.timeout(getOSType() === OSType.Windows ? TEST_TIMEOUT * 7 : TEST_TIMEOUT * 2); + await initialize(); + initializeDI(); + const locator = ioc.serviceContainer.get<IInterpreterLocatorService>( + IInterpreterLocatorService, + INTERPRETER_LOCATOR_SERVICE, + ); + interpreters = await locator.getInterpreters(); + }); + + setup(async () => { + await initializeTest(); + initializeDI(); + }); + + teardown(async () => { + await ioc.dispose(); + await closeActiveWindows(); + }); + suiteTeardown(closeActiveWindows); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerUnitTestTypes(); + ioc.registerMockProcessTypes(); + ioc.registerVariableTypes(); + ioc.registerInterpreterTypes(); + ioc.serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation); + } + + test('Ensure we are getting conda environment created using command `conda create -n "test_env1" -y python`', async () => { + // Created in CI using command `conda create -n "test_env1" -y python` + const filteredInterpreters = interpreters.filter( + (i) => i.envName === 'test_env1' && i.envType === EnvironmentType.Conda, + ); + expect(filteredInterpreters.length).to.be.greaterThan(0, 'Environment test_env1 not found'); + }); + test('Ensure we are getting conda environment created using command `conda create -p "./test_env2`"', async () => { + // Created in CI using command `conda create -p "./test_env2" -y python` + const filteredInterpreters = interpreters.filter((i) => { + let dirName = path.dirname(i.path); + if (dirName.endsWith('bin') || dirName.endsWith('Scripts')) { + dirName = path.dirname(dirName); + } + return dirName.endsWith('test_env2') && i.envType === EnvironmentType.Conda; + }); + expect(filteredInterpreters.length).to.be.greaterThan(0, 'Environment test_env2 not found'); + }); + test('Ensure we are getting conda environment created using command `conda create -p "<HOME>/test_env3" -y python`', async () => { + // Created in CI using command `conda create -p "<HOME>/test_env3" -y python` + const filteredInterpreters = interpreters.filter((i) => { + let dirName = path.dirname(i.path); + if (dirName.endsWith('bin') || dirName.endsWith('Scripts')) { + dirName = path.dirname(dirName); + } + return dirName.endsWith('test_env3') && i.envType === EnvironmentType.Conda; + }); + expect(filteredInterpreters.length).to.be.greaterThan(0, 'Environment test_env3 not found'); + }); + + test('Ensure we are getting the base conda environment', async () => { + // Base conda environment in CI + const filteredInterpreters = interpreters.filter( + (i) => (i.envName === 'base' || i.envName === 'miniconda') && i.envType === EnvironmentType.Conda, + ); + expect(filteredInterpreters.length).to.be.greaterThan(0, 'Base environment not found'); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/interpreterWatcherBuilder.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/interpreterWatcherBuilder.unit.test.ts new file mode 100644 index 000000000000..3d21f5d228cb --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/interpreterWatcherBuilder.unit.test.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-classes-per-file max-func-body-length + +import { expect } from 'chai'; +import { + anything, instance, mock, when, +} from 'ts-mockito'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { IInterpreterWatcher, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../../../../client/interpreter/contracts'; +import { ServiceContainer } from '../../../../client/ioc/container'; +import { InterpreterWatcherBuilder } from '../../../../client/pythonEnvironments/discovery/locators/services/interpreterWatcherBuilder'; + +suite('Interpreters - Watcher Builder', () => { + test('Build Workspace Virtual Env Watcher', async () => { + const workspaceService = mock(WorkspaceService); + const serviceContainer = mock(ServiceContainer); + const builder = new InterpreterWatcherBuilder(instance(workspaceService), instance(serviceContainer)); + const watcher = { register: () => Promise.resolve() }; + + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(); + when(serviceContainer.get<IInterpreterWatcher>(IInterpreterWatcher, WORKSPACE_VIRTUAL_ENV_SERVICE)).thenReturn( + (watcher as any) as IInterpreterWatcher, + ); + + const item = await builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined); + + expect(item).to.be.equal(watcher, 'invalid'); + }); + test('Ensure we cache Workspace Virtual Env Watcher', async () => { + const workspaceService = mock(WorkspaceService); + const serviceContainer = mock(ServiceContainer); + const builder = new InterpreterWatcherBuilder(instance(workspaceService), instance(serviceContainer)); + const watcher = { register: () => Promise.resolve() }; + + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(); + when(serviceContainer.get<IInterpreterWatcher>(IInterpreterWatcher, WORKSPACE_VIRTUAL_ENV_SERVICE)).thenReturn( + (watcher as any) as IInterpreterWatcher, + ); + + const [item1, item2, item3] = await Promise.all([ + builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined), + builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined), + builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined), + ]); + + expect(item1).to.be.equal(watcher, 'invalid'); + expect(item2).to.be.equal(watcher, 'invalid'); + expect(item3).to.be.equal(watcher, 'invalid'); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/knownPathService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/knownPathService.unit.test.ts new file mode 100644 index 000000000000..0903efaa4a3c --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/knownPathService.unit.test.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { ICurrentProcess, IPathUtils } from '../../../../client/common/types'; +import { IKnownSearchPathsForInterpreters } from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { KnownSearchPathsForInterpreters } from '../../../../client/pythonEnvironments/discovery/locators/services/KnownPathsService'; + +suite('Interpreters Known Paths', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let currentProcess: TypeMoq.IMock<ICurrentProcess>; + let platformService: TypeMoq.IMock<IPlatformService>; + let pathUtils: TypeMoq.IMock<IPathUtils>; + let knownSearchPaths: IKnownSearchPathsForInterpreters; + + setup(async () => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + currentProcess = TypeMoq.Mock.ofType<ICurrentProcess>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + pathUtils = TypeMoq.Mock.ofType<IPathUtils>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICurrentProcess), TypeMoq.It.isAny())) + .returns(() => currentProcess.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())) + .returns(() => platformService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPathUtils), TypeMoq.It.isAny())) + .returns(() => pathUtils.object); + + knownSearchPaths = new KnownSearchPathsForInterpreters(serviceContainer.object); + }); + + test('Ensure known list of paths are returned', async () => { + const pathDelimiter = 'X'; + const pathsInPATHVar = [path.join('a', 'b', 'c'), '', path.join('1', '2'), '3']; + pathUtils.setup((p) => p.delimiter).returns(() => pathDelimiter); + platformService.setup((p) => p.isWindows).returns(() => true); + platformService.setup((p) => p.pathVariableName).returns(() => 'PATH'); + currentProcess + .setup((p) => p.env) + .returns(() => ({ PATH: pathsInPATHVar.join(pathDelimiter) })); + + const expectedPaths = [...pathsInPATHVar].filter((item) => item.length > 0); + + const paths = knownSearchPaths.getSearchPaths(); + + expect(paths).to.deep.equal(expectedPaths); + }); + + test('Ensure known list of paths are returned on non-windows', async () => { + const homeDir = '/users/peter Smith'; + const pathDelimiter = 'X'; + pathUtils.setup((p) => p.delimiter).returns(() => pathDelimiter); + pathUtils.setup((p) => p.home).returns(() => homeDir); + platformService.setup((p) => p.isWindows).returns(() => false); + platformService.setup((p) => p.pathVariableName).returns(() => 'PATH'); + currentProcess + .setup((p) => p.env) + .returns(() => ({ PATH: '' })); + + const expectedPaths: string[] = []; + ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin'].forEach((p) => { + expectedPaths.push(p); + expectedPaths.push(path.join(homeDir, p)); + }); + + expectedPaths.push(path.join(homeDir, 'anaconda', 'bin')); + expectedPaths.push(path.join(homeDir, 'python', 'bin')); + + const paths = knownSearchPaths.getSearchPaths(); + + expect(paths).to.deep.equal(expectedPaths); + }); + + test('Ensure PATH variable and known list of paths are merged on non-windows', async () => { + const homeDir = '/users/peter Smith'; + const pathDelimiter = 'X'; + const pathsInPATHVar = [path.join('a', 'b', 'c'), '', path.join('1', '2'), '3']; + pathUtils.setup((p) => p.delimiter).returns(() => pathDelimiter); + pathUtils.setup((p) => p.home).returns(() => homeDir); + platformService.setup((p) => p.isWindows).returns(() => false); + platformService.setup((p) => p.pathVariableName).returns(() => 'PATH'); + currentProcess + .setup((p) => p.env) + .returns(() => ({ PATH: pathsInPATHVar.join(pathDelimiter) })); + + const expectedPaths = [...pathsInPATHVar].filter((item) => item.length > 0); + ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin'].forEach((p) => { + expectedPaths.push(p); + expectedPaths.push(path.join(homeDir, p)); + }); + + expectedPaths.push(path.join(homeDir, 'anaconda', 'bin')); + expectedPaths.push(path.join(homeDir, 'python', 'bin')); + + const paths = knownSearchPaths.getSearchPaths(); + + expect(paths).to.deep.equal(expectedPaths); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/pipEnvService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/pipEnvService.unit.test.ts new file mode 100644 index 000000000000..e2dc12d169e7 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/pipEnvService.unit.test.ts @@ -0,0 +1,331 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import * as sinon from 'sinon'; +import { + anything, instance, mock, when, +} from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../../../client/common/application/types'; +import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../../../client/common/process/types'; +import { + IConfigurationService, + ICurrentProcess, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, +} from '../../../../client/common/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { IEnvironmentVariablesProvider } from '../../../../client/common/variables/types'; +import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; +import { IPipEnvServiceHelper } from '../../../../client/interpreter/locators/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { PipEnvService } from '../../../../client/pythonEnvironments/discovery/locators/services/pipEnvService'; +import { PipEnvServiceHelper } from '../../../../client/pythonEnvironments/discovery/locators/services/pipEnvServiceHelper'; +import * as Telemetry from '../../../../client/telemetry'; +import { EventName } from '../../../../client/telemetry/constants'; + +enum OS { + Mac, + Windows, + Linux +} + +suite('Interpreters - PipEnv', () => { + const rootWorkspace = Uri.file(path.join('usr', 'desktop', 'wkspc1')).fsPath; + getNamesAndValues(OS).forEach((os) => { + [undefined, Uri.file(path.join(rootWorkspace, 'one.py'))].forEach((resource) => { + const testSuffix = ` (${os.name}, ${resource ? 'with' : 'without'} a workspace)`; + + let pipEnvService: PipEnvService; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let interpreterHelper: TypeMoq.IMock<IInterpreterHelper>; + let processService: TypeMoq.IMock<IProcessService>; + let currentProcess: TypeMoq.IMock<ICurrentProcess>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let persistentStateFactory: TypeMoq.IMock<IPersistentStateFactory>; + let envVarsProvider: TypeMoq.IMock<IEnvironmentVariablesProvider>; + let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; + let platformService: TypeMoq.IMock<IPlatformService>; + let config: TypeMoq.IMock<IConfigurationService>; + let settings: TypeMoq.IMock<IPythonSettings>; + let pipenvPathSetting: string; + let pipEnvServiceHelper: IPipEnvServiceHelper; + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + interpreterHelper = TypeMoq.Mock.ofType<IInterpreterHelper>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + processService = TypeMoq.Mock.ofType<IProcessService>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + currentProcess = TypeMoq.Mock.ofType<ICurrentProcess>(); + persistentStateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + envVarsProvider = TypeMoq.Mock.ofType<IEnvironmentVariablesProvider>(); + procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + pipEnvServiceHelper = mock(PipEnvServiceHelper); + processService.setup((x: any) => x.then).returns(() => undefined); + procServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + + // tslint:disable-next-line:no-any + const persistentState = TypeMoq.Mock.ofType<IPersistentState<any>>(); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => persistentState.object); + persistentStateFactory + .setup((p) => p.createWorkspacePersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => persistentState.object); + persistentState.setup((p) => p.value).returns(() => undefined); + persistentState.setup((p) => p.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + + const workspaceFolder = TypeMoq.Mock.ofType<WorkspaceFolder>(); + workspaceFolder.setup((w) => w.uri).returns(() => Uri.file(rootWorkspace)); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())) + .returns(() => workspaceFolder.object); + workspaceService.setup((w) => w.rootPath).returns(() => rootWorkspace); + + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())) + .returns(() => procServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterHelper))) + .returns(() => interpreterHelper.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICurrentProcess))) + .returns(() => currentProcess.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => appShell.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory))) + .returns(() => persistentStateFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider))) + .returns(() => envVarsProvider.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) + .returns(() => config.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPipEnvServiceHelper), TypeMoq.It.isAny())) + .returns(() => instance(pipEnvServiceHelper)); + + when(pipEnvServiceHelper.trackWorkspaceFolder(anything(), anything())).thenResolve(); + config = TypeMoq.Mock.ofType<IConfigurationService>(); + settings = TypeMoq.Mock.ofType<IPythonSettings>(); + config.setup((c) => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => settings.object); + settings.setup((p) => p.pipenvPath).returns(() => pipenvPathSetting); + pipenvPathSetting = 'pipenv'; + + pipEnvService = new PipEnvService(serviceContainer.object); + }); + + suite('With didTriggerInterpreterSuggestions set to true', () => { + setup(() => { + sinon.stub(pipEnvService, 'didTriggerInterpreterSuggestions').get(() => true); + }); + + teardown(() => { + sinon.restore(); + }); + + test(`Should return an empty list'${testSuffix}`, () => { + const environments = pipEnvService.getInterpreters(resource); + expect(environments).to.be.eventually.deep.equal([]); + }); + test(`Should return an empty list if there is no \'PipFile\'${testSuffix}`, async () => { + const env = {}; + envVarsProvider + .setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})) + .verifiable(TypeMoq.Times.once()); + currentProcess.setup((c) => c.env).returns(() => env); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.once()); + const environments = await pipEnvService.getInterpreters(resource); + + expect(environments).to.be.deep.equal([]); + fileSystem.verifyAll(); + }); + test(`Should display warning message if there is a \'PipFile\' but \'pipenv --version\' fails ${testSuffix}`, async () => { + const env = {}; + currentProcess.setup((c) => c.env).returns(() => env); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.reject('')); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))) + .returns(() => Promise.resolve(true)); + const warningMessage = "Workspace contains Pipfile but 'pipenv' was not found. Make sure 'pipenv' is on the PATH."; + appShell + .setup((a) => a.showWarningMessage(warningMessage)) + .returns(() => Promise.resolve('')) + .verifiable(TypeMoq.Times.once()); + const environments = await pipEnvService.getInterpreters(resource); + + expect(environments).to.be.deep.equal([]); + appShell.verifyAll(); + }); + test(`Should display warning message if there is a \'PipFile\' but \'pipenv --venv\' fails with stderr ${testSuffix}`, async () => { + const env = {}; + currentProcess.setup((c) => c.env).returns(() => env); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stderr: '', stdout: 'pipenv, version 2018.11.26' })); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isValue(['--venv']), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stderr: 'Aborted!', stdout: '' })); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))) + .returns(() => Promise.resolve(true)); + const warningMessage = 'Workspace contains Pipfile but the associated virtual environment has not been setup. Setup the virtual environment manually if needed.'; + appShell + .setup((a) => a.showWarningMessage(warningMessage)) + .returns(() => Promise.resolve('')) + .verifiable(TypeMoq.Times.once()); + const environments = await pipEnvService.getInterpreters(resource); + + expect(environments).to.be.deep.equal([]); + appShell.verifyAll(); + }); + test(`Should return interpreter information${testSuffix}`, async () => { + const env = {}; + const pythonPath = 'one'; + envVarsProvider + .setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})) + .verifiable(TypeMoq.Times.once()); + currentProcess.setup((c) => c.env).returns(() => env); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: pythonPath })); + interpreterHelper + .setup((v) => v.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: new SemVer('1.0.0') })); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))) + .returns(() => Promise.resolve(true)) + .verifiable(); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)) + .verifiable(); + + const environments = await pipEnvService.getInterpreters(resource); + + expect(environments).to.be.lengthOf(1); + fileSystem.verifyAll(); + }); + test(`Should return interpreter information using PipFile defined in Env variable${testSuffix}`, async () => { + const envPipFile = 'XYZ'; + const env = { + PIPENV_PIPFILE: envPipFile, + }; + const pythonPath = 'one'; + envVarsProvider + .setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})) + .verifiable(TypeMoq.Times.once()); + currentProcess.setup((c) => c.env).returns(() => env); + processService + .setup((p) => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: pythonPath })); + interpreterHelper + .setup((v) => v.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ version: new SemVer('1.0.0') })); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))) + .returns(() => Promise.resolve(false)) + .verifiable(TypeMoq.Times.never()); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, envPipFile)))) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)) + .verifiable(); + const environments = await pipEnvService.getInterpreters(resource); + + expect(environments).to.be.lengthOf(1); + fileSystem.verifyAll(); + }); + test("Must use 'python.pipenvPath' setting", async () => { + pipenvPathSetting = 'spam-spam-pipenv-spam-spam'; + const pipenvExe = pipEnvService.executable; + assert.equal(pipenvExe, 'spam-spam-pipenv-spam-spam', 'Failed to identify pipenv.exe'); + }); + + test('Should send telemetry event when calling getInterpreters', async () => { + const sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent'); + + await pipEnvService.getInterpreters(resource); + + sinon.assert.calledWith(sendTelemetryStub, EventName.PIPENV_INTERPRETER_DISCOVERY); + sinon.restore(); + }); + }); + + suite('With didTriggerInterpreterSuggestions set to false', () => { + setup(() => { + sinon.stub(pipEnvService, 'didTriggerInterpreterSuggestions').get(() => false); + }); + + teardown(() => { + sinon.restore(); + }); + + test('isRelatedPipEnvironment should exit early', async () => { + processService + .setup((p) => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.never()); + + const result = await pipEnvService.isRelatedPipEnvironment('foo', 'some/python/path'); + + expect(result).to.be.equal(false, 'isRelatedPipEnvironment should return false.'); + processService.verifyAll(); + }); + + test('Executable getter should return an empty string', () => { + const { executable } = pipEnvService; + + expect(executable).to.be.equal('', 'The executable getter should return an empty string.'); + }); + + test('getInterpreters should exit early', async () => { + processService + .setup((p) => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.never()); + + const interpreters = await pipEnvService.getInterpreters(resource); + + expect(interpreters).to.be.lengthOf(0); + processService.verifyAll(); + }); + }); + }); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/progressService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/progressService.unit.test.ts new file mode 100644 index 000000000000..8a3d27df4667 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/progressService.unit.test.ts @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-classes-per-file max-func-body-length + +import { expect } from 'chai'; +import { + anything, instance, mock, when, +} from 'ts-mockito'; +import { Disposable, Uri } from 'vscode'; +import { createDeferred } from '../../../../client/common/utils/async'; +import { noop } from '../../../../client/common/utils/misc'; +import { IInterpreterLocatorService } from '../../../../client/interpreter/contracts'; +import { ServiceContainer } from '../../../../client/ioc/container'; +import { InterpreterLocatorProgressService } from '../../../../client/pythonEnvironments/discovery/locators/progressService'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; +import { sleep } from '../../../core'; + +suite('Interpreters - Locator Progress', () => { + class Locator implements IInterpreterLocatorService { + public get hasInterpreters(): Promise<boolean> { + return Promise.resolve(true); + } + + public locatingCallback?: (e: Promise<PythonEnvironment[]>) => any; + + public onLocating( + listener: (e: Promise<PythonEnvironment[]>) => any, + _thisArgs?: any, + _disposables?: Disposable[], + ): Disposable { + this.locatingCallback = listener; + return { dispose: noop }; + } + + public getInterpreters(_resource?: Uri): Promise<PythonEnvironment[]> { + return Promise.resolve([]); + } + + public dispose() { + noop(); + } + } + + test('Must raise refreshing event', async () => { + const serviceContainer = mock(ServiceContainer); + const locator = new Locator(); + when(serviceContainer.getAll(anything())).thenReturn([locator]); + const progress = new InterpreterLocatorProgressService(instance(serviceContainer), []); + progress.register(); + + let refreshingInvoked = false; + progress.onRefreshing(() => (refreshingInvoked = true)); + let refreshedInvoked = false; + progress.onRefreshed(() => (refreshedInvoked = true)); + + const locatingDeferred = createDeferred<PythonEnvironment[]>(); + locator.locatingCallback!.bind(progress)(locatingDeferred.promise); + expect(refreshingInvoked).to.be.equal(true, 'Refreshing Not invoked'); + expect(refreshedInvoked).to.be.equal(false, 'Refreshed invoked'); + }); + test('Must raise refreshed event', async () => { + const serviceContainer = mock(ServiceContainer); + const locator = new Locator(); + when(serviceContainer.getAll(anything())).thenReturn([locator]); + const progress = new InterpreterLocatorProgressService(instance(serviceContainer), []); + progress.register(); + + let refreshingInvoked = false; + progress.onRefreshing(() => (refreshingInvoked = true)); + let refreshedInvoked = false; + progress.onRefreshed(() => (refreshedInvoked = true)); + + const locatingDeferred = createDeferred<PythonEnvironment[]>(); + locator.locatingCallback!.bind(progress)(locatingDeferred.promise); + locatingDeferred.resolve(); + + await sleep(10); + expect(refreshingInvoked).to.be.equal(true, 'Refreshing Not invoked'); + expect(refreshedInvoked).to.be.equal(true, 'Refreshed not invoked'); + }); + test('Must raise refreshed event only when all locators have completed', async () => { + const serviceContainer = mock(ServiceContainer); + const locator1 = new Locator(); + const locator2 = new Locator(); + const locator3 = new Locator(); + when(serviceContainer.getAll(anything())).thenReturn([locator1, locator2, locator3]); + const progress = new InterpreterLocatorProgressService(instance(serviceContainer), []); + progress.register(); + + let refreshingInvoked = false; + progress.onRefreshing(() => (refreshingInvoked = true)); + let refreshedInvoked = false; + progress.onRefreshed(() => (refreshedInvoked = true)); + + const locatingDeferred1 = createDeferred<PythonEnvironment[]>(); + locator1.locatingCallback!.bind(progress)(locatingDeferred1.promise); + + const locatingDeferred2 = createDeferred<PythonEnvironment[]>(); + locator2.locatingCallback!.bind(progress)(locatingDeferred2.promise); + + const locatingDeferred3 = createDeferred<PythonEnvironment[]>(); + locator3.locatingCallback!.bind(progress)(locatingDeferred3.promise); + + locatingDeferred1.resolve(); + + await sleep(10); + expect(refreshingInvoked).to.be.equal(true, 'Refreshing Not invoked'); + expect(refreshedInvoked).to.be.equal(false, 'Refreshed invoked'); + + locatingDeferred2.resolve(); + + await sleep(10); + expect(refreshedInvoked).to.be.equal(false, 'Refreshed invoked'); + + locatingDeferred3.resolve(); + + await sleep(10); + expect(refreshedInvoked).to.be.equal(true, 'Refreshed not invoked'); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/venv.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/venv.unit.test.ts new file mode 100644 index 000000000000..2cd43a21a274 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/venv.unit.test.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Container } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IConfigurationService, ICurrentProcess, IPythonSettings } from '../../../../client/common/types'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService, +} from '../../../../client/interpreter/autoSelection/types'; +import { IVirtualEnvironmentManager } from '../../../../client/interpreter/virtualEnvs/types'; +import { ServiceContainer } from '../../../../client/ioc/container'; +import { ServiceManager } from '../../../../client/ioc/serviceManager'; +import { GlobalVirtualEnvironmentsSearchPathProvider } from '../../../../client/pythonEnvironments/discovery/locators/services/globalVirtualEnvService'; +import { WorkspaceVirtualEnvironmentsSearchPathProvider } from '../../../../client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvService'; +import { MockAutoSelectionService } from '../../../mocks/autoSelector'; + +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify: (value: string) => string = require('untildify'); + +// tslint:disable-next-line: max-func-body-length +suite('Virtual environments', () => { + let serviceManager: ServiceManager; + let serviceContainer: ServiceContainer; + let settings: TypeMoq.IMock<IPythonSettings>; + let config: TypeMoq.IMock<IConfigurationService>; + let workspace: TypeMoq.IMock<IWorkspaceService>; + let process: TypeMoq.IMock<ICurrentProcess>; + let virtualEnvMgr: TypeMoq.IMock<IVirtualEnvironmentManager>; + + setup(() => { + const cont = new Container(); + serviceManager = new ServiceManager(cont); + serviceContainer = new ServiceContainer(cont); + + settings = TypeMoq.Mock.ofType<IPythonSettings>(); + config = TypeMoq.Mock.ofType<IConfigurationService>(); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + process = TypeMoq.Mock.ofType<ICurrentProcess>(); + virtualEnvMgr = TypeMoq.Mock.ofType<IVirtualEnvironmentManager>(); + + config.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + + serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, config.object); + serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspace.object); + serviceManager.addSingletonInstance<ICurrentProcess>(ICurrentProcess, process.object); + serviceManager.addSingletonInstance<IVirtualEnvironmentManager>( + IVirtualEnvironmentManager, + virtualEnvMgr.object, + ); + serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService, + ); + serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( + IInterpreterAutoSeletionProxyService, + MockAutoSelectionService, + ); + }); + + test('Global search paths', async () => { + const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); + + const homedir = os.homedir(); + const folders = ['Envs', 'testpath']; + settings.setup((x) => x.venvFolders).returns(() => folders); + virtualEnvMgr.setup((v) => v.getPyEnvRoot(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + let paths = await pathProvider.getSearchPaths(); + let expected = ['envs', '.pyenv', '.direnv', '.virtualenvs', ...folders].map((item) => path.join(homedir, item)); + + virtualEnvMgr.verifyAll(); + expect(paths).to.deep.equal(expected, 'Global search folder list is incorrect.'); + + virtualEnvMgr.reset(); + virtualEnvMgr.setup((v) => v.getPyEnvRoot(TypeMoq.It.isAny())).returns(() => Promise.resolve('pyenv_path')); + paths = await pathProvider.getSearchPaths(); + + virtualEnvMgr.verifyAll(); + expected = expected.concat(['pyenv_path', path.join('pyenv_path', 'versions')]); + expect(paths).to.deep.equal(expected, 'pyenv path not resolved correctly.'); + }); + + test('Global search paths with duplicates', async () => { + const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); + + const folders = ['.virtualenvs', '.direnv']; + settings.setup((x) => x.venvFolders).returns(() => folders); + const paths = await pathProvider.getSearchPaths(); + + expect([...new Set(paths)]).to.deep.equal( + paths, + 'Duplicates are not removed from the list of global search paths', + ); + }); + + test('Global search paths with tilde path in the WORKON_HOME environment variable', async () => { + const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); + + const homedir = os.homedir(); + const workonFolder = path.join('~', '.workonFolder'); + process + .setup((p) => p.env) + .returns(() => ({ WORKON_HOME: workonFolder })); + settings.setup((x) => x.venvFolders).returns(() => []); + + const paths = await pathProvider.getSearchPaths(); + const expected = ['envs', '.pyenv', '.direnv', '.virtualenvs'].map((item) => path.join(homedir, item)); + expected.push(untildify(workonFolder)); + + expect(paths).to.deep.equal(expected, 'WORKON_HOME environment variable not read.'); + }); + + test('Global search paths with absolute path in the WORKON_HOME environment variable', async () => { + const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); + + const homedir = os.homedir(); + const workonFolder = path.join('path', 'to', '.workonFolder'); + process + .setup((p) => p.env) + .returns(() => ({ WORKON_HOME: workonFolder })); + settings.setup((x) => x.venvFolders).returns(() => []); + + const paths = await pathProvider.getSearchPaths(); + const expected = ['envs', '.pyenv', '.direnv', '.virtualenvs'].map((item) => path.join(homedir, item)); + expected.push(workonFolder); + + expect(paths).to.deep.equal(expected, 'WORKON_HOME environment variable not read.'); + }); + + test('Workspace search paths', async () => { + settings.setup((x) => x.venvPath).returns(() => path.join('~', 'foo')); + + const wsRoot = TypeMoq.Mock.ofType<WorkspaceFolder>(); + wsRoot.setup((x) => x.uri).returns(() => Uri.file('root')); + + const folder1 = TypeMoq.Mock.ofType<WorkspaceFolder>(); + folder1.setup((x) => x.uri).returns(() => Uri.file('dir1')); + + workspace.setup((x) => x.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => wsRoot.object); + workspace.setup((x) => x.workspaceFolders).returns(() => [wsRoot.object, folder1.object]); + + const pathProvider = new WorkspaceVirtualEnvironmentsSearchPathProvider(serviceContainer); + const paths = await pathProvider.getSearchPaths(Uri.file('')); + + const homedir = os.homedir(); + const isWindows = new PlatformService(); + const fixCase = (item: string) => (isWindows ? item.toUpperCase() : item); + const expected = [path.join(homedir, 'foo'), 'root', path.join('root', '.direnv')] + .map((item) => Uri.file(item).fsPath) + .map(fixCase); + expect(paths.map(fixCase)).to.deep.equal(expected, 'Workspace venv folder search list does not match.'); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/windowsRegistryService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsRegistryService.unit.test.ts new file mode 100644 index 000000000000..b067f6ebd73a --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/windowsRegistryService.unit.test.ts @@ -0,0 +1,996 @@ +import * as assert from 'assert'; +import * as fsextra from 'fs-extra'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem, IPlatformService, RegistryHive } from '../../../../client/common/platform/types'; +import { IPathUtils, IPersistentStateFactory } from '../../../../client/common/types'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; +import { IWindowsStoreInterpreter } from '../../../../client/interpreter/locators/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { WindowsRegistryService } from '../../../../client/pythonEnvironments/discovery/locators/services/windowsRegistryService'; +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { MockRegistry, MockState } from '../../../interpreters/mocks'; + +const environmentsPath = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'pythonFiles', 'environments'); + +// tslint:disable:max-func-body-length no-octal-literal no-invalid-this + +suite('Interpreters from Windows Registry (unit)', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let interpreterHelper: TypeMoq.IMock<IInterpreterHelper>; + let platformService: TypeMoq.IMock<IPlatformService>; + let fs: TypeMoq.IMock<IFileSystem>; + let windowsStoreInterpreter: TypeMoq.IMock<IWindowsStoreInterpreter>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + const stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); + interpreterHelper = TypeMoq.Mock.ofType<IInterpreterHelper>(); + const pathUtils = TypeMoq.Mock.ofType<IPathUtils>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + fs = TypeMoq.Mock.ofType<IFileSystem>(); + windowsStoreInterpreter = TypeMoq.Mock.ofType<IWindowsStoreInterpreter>(); + windowsStoreInterpreter.setup((w) => w.isHiddenInterpreter(TypeMoq.It.isAny())).returns(() => false); + windowsStoreInterpreter.setup((w) => w.isWindowsStoreInterpreter(TypeMoq.It.isAny())).returns(() => false); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPersistentStateFactory))) + .returns(() => stateFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterHelper))) + .returns(() => interpreterHelper.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fs.object); + pathUtils + .setup((p) => p.basename(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((p: string) => p.split(/[\\,\/]/).reverse()[0]); + // So effectively these are functional tests... + fs.setup((f) => f.fileExists(TypeMoq.It.isAny())).returns((filename) => fsextra.pathExists(filename)); + const state = new MockState(undefined); + interpreterHelper + .setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())) + // tslint:disable-next-line:no-empty no-any + .returns(() => Promise.resolve({} as any)); + stateFactory + .setup((s) => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => state); + }); + function setup64Bit(is64Bit: boolean) { + platformService.setup((ps) => ps.is64bit).returns(() => is64Bit); + return platformService.object; + } + test('Must return an empty list (x86)', async () => { + const registry = new MockRegistry([], []); + const winRegistry = new WindowsRegistryService( + registry, + setup64Bit(false), + serviceContainer.object, + windowsStoreInterpreter.object, + ); + platformService.setup((p) => p.isWindows).returns(() => true); + + const interpreters = await winRegistry.getInterpreters(); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must return an empty list (x64)', async () => { + const registry = new MockRegistry([], []); + const winRegistry = new WindowsRegistryService( + registry, + setup64Bit(true), + serviceContainer.object, + windowsStoreInterpreter.object, + ); + platformService.setup((p) => p.isWindows).returns(() => true); + + const interpreters = await winRegistry.getInterpreters(); + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must return a single entry', async () => { + const registryKeys = [ + { + key: '\\Software\\Python', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company One'], + }, + { + key: '\\Software\\Python\\Company One', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company One\\Tag1'], + }, + ]; + const registryValues = [ + { + key: '\\Software\\Python\\Company One', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'Display Name for Company One', + name: 'DisplayName', + }, + { + key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path1'), + }, + { + key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path1', 'one.exe'), + name: 'ExecutablePath', + }, + { + key: '\\Software\\Python\\Company One\\Tag1', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: '9.9.9.final', + name: 'SysVersion', + }, + { + key: '\\Software\\Python\\Company One\\Tag1', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'DisplayName.Tag1', + name: 'DisplayName', + }, + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService( + registry, + setup64Bit(false), + serviceContainer.object, + windowsStoreInterpreter.object, + ); + + interpreterHelper.reset(); + interpreterHelper + .setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ architecture: Architecture.x86 })); + platformService.setup((p) => p.isWindows).returns(() => true); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal( + interpreters[0].path, + path.join(environmentsPath, 'path1', 'one.exe'), + 'Incorrect executable path', + ); + assert.equal(interpreters[0].version!.raw, '9.9.9-final', 'Incorrect version'); + }); + test('Must default names for PythonCore and exe', async () => { + const registryKeys = [ + { + key: '\\Software\\Python', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\PythonCore'], + }, + { + key: '\\Software\\Python\\PythonCore', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\PythonCore\\9.9.9-final'], + }, + ]; + const registryValues = [ + { + key: '\\Software\\Python\\PythonCore\\9.9.9-final\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path1'), + }, + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService( + registry, + setup64Bit(false), + serviceContainer.object, + windowsStoreInterpreter.object, + ); + + interpreterHelper.reset(); + interpreterHelper + .setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ architecture: Architecture.x86 })); + platformService.setup((p) => p.isWindows).returns(() => true); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Python Software Foundation', 'Incorrect company name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[0].version!.raw, '9.9.9-final', 'Incorrect version'); + }); + test("Must ignore company 'PyLauncher'", async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const registryKeys = [ + { + key: '\\Software\\Python', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\PyLauncher'], + }, + { + key: '\\Software\\Python\\PythonCore', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\PyLauncher\\Tag1'], + }, + ]; + const registryValues = [ + { + key: '\\Software\\Python\\PyLauncher\\Tag1\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'c:/temp/Install Path Tag1', + }, + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService( + registry, + setup64Bit(false), + serviceContainer.object, + windowsStoreInterpreter.object, + ); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + }); + test('Must return a single entry and when registry contains only the InstallPath', async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const registryKeys = [ + { + key: '\\Software\\Python', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company One'], + }, + { + key: '\\Software\\Python\\Company One', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company One\\9.9.9-final'], + }, + ]; + const registryValues = [ + { + key: '\\Software\\Python\\Company One\\9.9.9-final\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path1'), + }, + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService( + registry, + setup64Bit(false), + serviceContainer.object, + windowsStoreInterpreter.object, + ); + interpreterHelper.reset(); + interpreterHelper + .setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ architecture: Architecture.x86 })); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Company One', 'Incorrect company name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[0].version!.raw, '9.9.9-final', 'Incorrect version'); + assert.equal(interpreters[0].envType, EnvironmentType.Unknown, 'Incorrect type'); + }); + test('Must return a single entry with a type of WindowsStore', async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const registryKeys = [ + { + key: '\\Software\\Python', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company One'], + }, + { + key: '\\Software\\Python\\Company One', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company One\\9.9.9-final'], + }, + ]; + const registryValues = [ + { + key: '\\Software\\Python\\Company One\\9.9.9-final\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path1'), + }, + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService( + registry, + setup64Bit(false), + serviceContainer.object, + windowsStoreInterpreter.object, + ); + interpreterHelper.reset(); + interpreterHelper + .setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ architecture: Architecture.x86 })); + windowsStoreInterpreter.reset(); + const expectedPythonPath = path.join(environmentsPath, 'path1', 'python.exe'); + windowsStoreInterpreter + .setup((w) => w.isHiddenInterpreter(TypeMoq.It.isValue(expectedPythonPath))) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + windowsStoreInterpreter + .setup((w) => w.isWindowsStoreInterpreter(TypeMoq.It.isValue(expectedPythonPath))) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 1, 'Incorrect number of entries'); + assert.equal(interpreters[0].envType, EnvironmentType.WindowsStore, 'Incorrect type'); + windowsStoreInterpreter.verifyAll(); + }); + test('Must not return any interpreters (must ignore internal windows store intrepreters)', async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const registryKeys = [ + { + key: '\\Software\\Python', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company One'], + }, + { + key: '\\Software\\Python\\Company One', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company One\\9.9.9-final'], + }, + ]; + const registryValues = [ + { + key: '\\Software\\Python\\Company One\\9.9.9-final\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path1'), + }, + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService( + registry, + setup64Bit(false), + serviceContainer.object, + windowsStoreInterpreter.object, + ); + interpreterHelper.reset(); + interpreterHelper + .setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ architecture: Architecture.x86 })); + windowsStoreInterpreter.reset(); + const expectedPythonPath = path.join(environmentsPath, 'path1', 'python.exe'); + windowsStoreInterpreter + .setup((w) => w.isHiddenInterpreter(TypeMoq.It.isValue(expectedPythonPath))) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 0, 'Incorrect number of entries'); + windowsStoreInterpreter.verifyAll(); + }); + test('Must return multiple entries', async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const registryKeys = [ + { + key: '\\Software\\Python', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: [ + '\\Software\\Python\\Company One', + '\\Software\\Python\\Company Two', + '\\Software\\Python\\Company Three', + ], + }, + { + key: '\\Software\\Python\\Company One', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company One\\1.0.0', '\\Software\\Python\\Company One\\2.0.0'], + }, + { + key: '\\Software\\Python\\Company Two', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: [ + '\\Software\\Python\\Company Two\\3.0.0', + '\\Software\\Python\\Company Two\\4.0.0', + '\\Software\\Python\\Company Two\\5.0.0', + ], + }, + { + key: '\\Software\\Python\\Company Three', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company Three\\6.0.0'], + }, + { + key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['7.0.0'], + }, + { + key: '\\Software\\Python\\Company A', + hive: RegistryHive.HKLM, + arch: Architecture.x86, + values: ['8.0.0'], + }, + ]; + const registryValues = [ + { + key: '\\Software\\Python\\Company One', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'Display Name for Company One', + name: 'DisplayName', + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path1'), + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path1', 'python.exe'), + name: 'ExecutablePath', + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path2'), + name: 'SysVersion', + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'DisplayName.Tag1', + name: 'DisplayName', + }, + + { + key: '\\Software\\Python\\Company One\\2.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path2'), + }, + { + key: '\\Software\\Python\\Company One\\2.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path2', 'python.exe'), + name: 'ExecutablePath', + }, + + { + key: '\\Software\\Python\\Company Two\\3.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path3'), + }, + { + key: '\\Software\\Python\\Company Two\\3.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: '3.0.0', + name: 'SysVersion', + }, + + { + key: '\\Software\\Python\\Company Two\\4.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + }, + { + key: '\\Software\\Python\\Company Two\\4.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'DisplayName.Tag B', + name: 'DisplayName', + }, + + { + key: '\\Software\\Python\\Company Two\\5.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'scipy'), + }, + + { + key: '\\Software\\Python\\Company Three\\6.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + }, + + { + key: '\\Software\\Python\\Company A\\8.0.0\\InstallPath', + hive: RegistryHive.HKLM, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), + }, + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService( + registry, + setup64Bit(false), + serviceContainer.object, + windowsStoreInterpreter.object, + ); + interpreterHelper.reset(); + interpreterHelper + .setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ architecture: Architecture.x86 })); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 4, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[0].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[0].version!.raw, '1.0.0', 'Incorrect version'); + + assert.equal(interpreters[1].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[1].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal(interpreters[1].path, path.join(environmentsPath, 'path2', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[1].version!.raw, '2.0.0', 'Incorrect version'); + + assert.equal(interpreters[2].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[2].companyDisplayName, 'Company Two', 'Incorrect company name'); + assert.equal( + interpreters[2].path, + path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), + 'Incorrect path', + ); + assert.equal(interpreters[2].version!.raw, '4.0.0', 'Incorrect version'); + + assert.equal(interpreters[3].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[3].companyDisplayName, 'Company Two', 'Incorrect company name'); + assert.equal( + interpreters[3].path, + path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), + 'Incorrect path', + ); + assert.equal(interpreters[3].version!.raw, '5.0.0', 'Incorrect version'); + }); + test('Must return multiple entries excluding the invalid registry items and duplicate paths', async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const registryKeys = [ + { + key: '\\Software\\Python', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: [ + '\\Software\\Python\\Company One', + '\\Software\\Python\\Company Two', + '\\Software\\Python\\Company Three', + '\\Software\\Python\\Company Four', + '\\Software\\Python\\Company Five', + 'Missing Tag', + ], + }, + { + key: '\\Software\\Python\\Company One', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company One\\1.0.0', '\\Software\\Python\\Company One\\2.0.0'], + }, + { + key: '\\Software\\Python\\Company Two', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: [ + '\\Software\\Python\\Company Two\\3.0.0', + '\\Software\\Python\\Company Two\\4.0.0', + '\\Software\\Python\\Company Two\\5.0.0', + ], + }, + { + key: '\\Software\\Python\\Company Three', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company Three\\6.0.0'], + }, + { + key: '\\Software\\Python\\Company Four', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company Four\\7.0.0'], + }, + { + key: '\\Software\\Python\\Company Five', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company Five\\8.0.0'], + }, + { + key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['9.0.0'], + }, + { + key: '\\Software\\Python\\Company A', + hive: RegistryHive.HKLM, + arch: Architecture.x86, + values: ['10.0.0'], + }, + ]; + const registryValues: { + key: string; + hive: RegistryHive; + arch?: Architecture; + value: string; + name?: string; + }[] = [ + { + key: '\\Software\\Python\\Company One', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'Display Name for Company One', + name: 'DisplayName', + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), + name: 'ExecutablePath', + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: '1.0.0-final', + name: 'SysVersion', + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'DisplayName.Tag1', + name: 'DisplayName', + }, + + { + key: '\\Software\\Python\\Company One\\2.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'scipy'), + }, + { + key: '\\Software\\Python\\Company One\\2.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), + name: 'ExecutablePath', + }, + + { + key: '\\Software\\Python\\Company Two\\3.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path1'), + }, + { + key: '\\Software\\Python\\Company Two\\3.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: '3.0.0', + name: 'SysVersion', + }, + + { + key: '\\Software\\Python\\Company Two\\4.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path2'), + }, + { + key: '\\Software\\Python\\Company Two\\4.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'DisplayName.Tag B', + name: 'DisplayName', + }, + + { + key: '\\Software\\Python\\Company Two\\5.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + }, + + { + key: '\\Software\\Python\\Company Five\\8.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + // tslint:disable-next-line:no-any + value: <any>undefined, + }, + + { + key: '\\Software\\Python\\Company Three\\6.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + }, + + { + key: '\\Software\\Python\\Company A\\10.0.0\\InstallPath', + hive: RegistryHive.HKLM, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + }, + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService( + registry, + setup64Bit(false), + serviceContainer.object, + windowsStoreInterpreter.object, + ); + interpreterHelper.reset(); + interpreterHelper + .setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ architecture: Architecture.x86 })); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 4, 'Incorrect number of entries'); + assert.equal(interpreters[0].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal( + interpreters[0].path, + path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), + 'Incorrect path', + ); + assert.equal(interpreters[0].version!.raw, '1.0.0', 'Incorrect version'); + + assert.equal(interpreters[1].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[1].companyDisplayName, 'Display Name for Company One', 'Incorrect company name'); + assert.equal( + interpreters[1].path, + path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), + 'Incorrect path', + ); + assert.equal(interpreters[1].version!.raw, '2.0.0', 'Incorrect version'); + + assert.equal(interpreters[2].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[2].companyDisplayName, 'Company Two', 'Incorrect company name'); + assert.equal(interpreters[2].path, path.join(environmentsPath, 'path1', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[2].version!.raw, '3.0.0', 'Incorrect version'); + + assert.equal(interpreters[3].architecture, Architecture.x86, 'Incorrect arhictecture'); + assert.equal(interpreters[3].companyDisplayName, 'Company Two', 'Incorrect company name'); + assert.equal(interpreters[3].path, path.join(environmentsPath, 'path2', 'python.exe'), 'Incorrect path'); + assert.equal(interpreters[3].version!.raw, '4.0.0', 'Incorrect version'); + }); + test('Must return multiple entries excluding the invalid registry items and nonexistent paths', async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const registryKeys = [ + { + key: '\\Software\\Python', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: [ + '\\Software\\Python\\Company One', + '\\Software\\Python\\Company Two', + '\\Software\\Python\\Company Three', + '\\Software\\Python\\Company Four', + '\\Software\\Python\\Company Five', + 'Missing Tag', + ], + }, + { + key: '\\Software\\Python\\Company One', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company One\\1.0.0', '\\Software\\Python\\Company One\\Tag2'], + }, + { + key: '\\Software\\Python\\Company Two', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: [ + '\\Software\\Python\\Company Two\\Tag A', + '\\Software\\Python\\Company Two\\2.0.0', + '\\Software\\Python\\Company Two\\Tag C', + ], + }, + { + key: '\\Software\\Python\\Company Three', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company Three\\Tag !'], + }, + { + key: '\\Software\\Python\\Company Four', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company Four\\Four !'], + }, + { + key: '\\Software\\Python\\Company Five', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + values: ['\\Software\\Python\\Company Five\\Five !'], + }, + { + key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'], + }, + { + key: '\\Software\\Python\\Company A', + hive: RegistryHive.HKLM, + arch: Architecture.x86, + values: ['Another Tag'], + }, + ]; + const registryValues: { + key: string; + hive: RegistryHive; + arch?: Architecture; + value: string; + name?: string; + }[] = [ + { + key: '\\Software\\Python\\Company One', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'Display Name for Company One', + name: 'DisplayName', + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'numpy'), + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), + name: 'ExecutablePath', + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'Version.Tag1', + name: 'SysVersion', + }, + { + key: '\\Software\\Python\\Company One\\1.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'DisplayName.Tag1', + name: 'DisplayName', + }, + + { + key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy'), + }, + { + key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy', 'python.exe'), + name: 'ExecutablePath', + }, + + { + key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'non-existent-path'), + }, + { + key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: '2.0.0', + name: 'SysVersion', + }, + + { + key: '\\Software\\Python\\Company Two\\2.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'path2'), + }, + { + key: '\\Software\\Python\\Company Two\\2.0.0\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: 'DisplayName.Tag B', + name: 'DisplayName', + }, + + { + key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy'), + }, + + { + key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + // tslint:disable-next-line:no-any + value: <any>undefined, + }, + + { + key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', + hive: RegistryHive.HKCU, + arch: Architecture.x86, + value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy'), + }, + + { + key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', + hive: RegistryHive.HKLM, + arch: Architecture.x86, + value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy'), + }, + ]; + const registry = new MockRegistry(registryKeys, registryValues); + const winRegistry = new WindowsRegistryService( + registry, + setup64Bit(false), + serviceContainer.object, + windowsStoreInterpreter.object, + ); + interpreterHelper.reset(); + interpreterHelper + .setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ architecture: Architecture.x86 })); + + const interpreters = await winRegistry.getInterpreters(); + + assert.equal(interpreters.length, 2, 'Incorrect number of entries'); + + assert.equal(interpreters[0].architecture, Architecture.x86, '1. Incorrect arhictecture'); + assert.equal(interpreters[0].companyDisplayName, 'Display Name for Company One', '1. Incorrect company name'); + assert.equal( + interpreters[0].path, + path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), + '1. Incorrect path', + ); + assert.equal(interpreters[0].version!.raw, '1.0.0', '1. Incorrect version'); + + assert.equal(interpreters[1].architecture, Architecture.x86, '2. Incorrect arhictecture'); + assert.equal(interpreters[1].companyDisplayName, 'Company Two', '2. Incorrect company name'); + assert.equal(interpreters[1].path, path.join(environmentsPath, 'path2', 'python.exe'), '2. Incorrect path'); + assert.equal(interpreters[1].version!.raw, '2.0.0', '2. Incorrect version'); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/windowsStoreInterpreter.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsStoreInterpreter.unit.test.ts new file mode 100644 index 000000000000..95c89f86f723 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/windowsStoreInterpreter.unit.test.ts @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-classes-per-file max-func-body-length + +import { expect } from 'chai'; +import { + anything, deepEqual, instance, mock, verify, when, +} from 'ts-mockito'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { PythonExecutionFactory } from '../../../../client/common/process/pythonExecutionFactory'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../client/common/process/types'; +import { IPersistentStateFactory } from '../../../../client/common/types'; +import { ServiceContainer } from '../../../../client/ioc/container'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { WindowsStoreInterpreter } from '../../../../client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter'; + +suite('Interpreters - Windows Store Interpreter', () => { + let windowsStoreInterpreter: WindowsStoreInterpreter; + let fs: IFileSystem; + let persistanceStateFactory: IPersistentStateFactory; + let executionFactory: IPythonExecutionFactory; + let serviceContainer: IServiceContainer; + setup(() => { + fs = mock(FileSystem); + persistanceStateFactory = mock(PersistentStateFactory); + executionFactory = mock(PythonExecutionFactory); + serviceContainer = mock(ServiceContainer); + when(serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory)).thenReturn( + instance(executionFactory), + ); + windowsStoreInterpreter = new WindowsStoreInterpreter( + instance(serviceContainer), + instance(persistanceStateFactory), + instance(fs), + ); + }); + const windowsStoreInterpreters = [ + '\\\\Program Files\\WindowsApps\\Something\\Python.exe', + '..\\Program Files\\WindowsApps\\Something\\Python.exe', + '..\\one\\Program Files\\WindowsApps\\Something\\Python.exe', + 'C:\\Program Files\\WindowsApps\\Something\\Python.exe', + 'C:\\Program Files\\WindowsApps\\Python.exe', + 'C:\\Microsoft\\WindowsApps\\Something\\Python.exe', + 'C:\\Microsoft\\WindowsApps\\Python.exe', + 'C:\\Microsoft\\WindowsApps\\PythonSoftwareFoundation\\Python.exe', + 'C:\\microsoft\\WindowsApps\\PythonSoftwareFoundation\\Something\\Python.exe', + ]; + for (const interpreter of windowsStoreInterpreters) { + test(`${interpreter} must be identified as a windows store interpter`, () => { + expect(windowsStoreInterpreter.isWindowsStoreInterpreter(interpreter)).to.equal(true, 'Must be true'); + }); + test(`${interpreter.toLowerCase()} must be identified as a windows store interpter (ignoring case)`, () => { + expect(windowsStoreInterpreter.isWindowsStoreInterpreter(interpreter.toLowerCase())).to.equal( + true, + 'Must be true', + ); + expect(windowsStoreInterpreter.isWindowsStoreInterpreter(interpreter.toUpperCase())).to.equal( + true, + 'Must be true', + ); + }); + test(`D${interpreter.substring( + 1, + )} must be identified as a windows store interpter (ignoring driver letter)`, () => { + expect(windowsStoreInterpreter.isWindowsStoreInterpreter(`D${interpreter.substring(1)}`)).to.equal( + true, + 'Must be true', + ); + }); + test(`${interpreter.replace( + /\\/g, + '/', + )} must be identified as a windows store interpter (ignoring path separator)`, () => { + expect(windowsStoreInterpreter.isWindowsStoreInterpreter(interpreter.replace(/\\/g, '/'))).to.equal( + true, + 'Must be true', + ); + }); + } + const nonWindowsStoreInterpreters = [ + '..\\Program Filess\\WindowsApps\\Something\\Python.exe', + 'C:\\Program Filess\\WindowsApps\\Something\\Python.exe', + 'C:\\Program Files\\WindowsAppss\\Python.exe', + 'C:\\Microsofts\\WindowsApps\\Something\\Python.exe', + 'C:\\Microsoft\\WindowsAppss\\Python.exe', + 'C:\\Microsofts\\WindowsApps\\PythonSoftwareFoundation\\Python.exe', + 'C:\\microsoft\\WindowsAppss\\PythonSoftwareFoundation\\Something\\Python.exe', + 'C:\\Python\\python.exe', + 'C:\\Program Files\\Python\\python.exe', + 'C:\\Program Files\\Microsoft\\Python\\python.exe', + '..\\apps\\Python.exe', + 'C:\\Apps\\Python.exe', + ]; + for (const interpreter of nonWindowsStoreInterpreters) { + test(`${interpreter} must not be identified as a windows store interpter`, () => { + expect(windowsStoreInterpreter.isHiddenInterpreter(interpreter)).to.equal(false, 'Must be false'); + expect(windowsStoreInterpreter.isHiddenInterpreter(interpreter.replace(/\\/g, '/'))).to.equal( + false, + 'Must be false', + ); + expect(windowsStoreInterpreter.isWindowsStoreInterpreter(interpreter)).to.equal(false, 'Must be false'); + expect(windowsStoreInterpreter.isWindowsStoreInterpreter(interpreter.replace(/\\/g, '/'))).to.equal( + false, + 'Must be false', + ); + expect(windowsStoreInterpreter.isHiddenInterpreter(interpreter.toLowerCase())).to.equal( + false, + 'Must be false', + ); + expect(windowsStoreInterpreter.isWindowsStoreInterpreter(interpreter.toUpperCase())).to.equal( + false, + 'Must be false', + ); + expect(windowsStoreInterpreter.isWindowsStoreInterpreter(`D${interpreter.substring(1)}`)).to.equal( + false, + 'Must be false', + ); + }); + } + const windowsStoreHiddenInterpreters = [ + 'C:\\Program Files\\WindowsApps\\Something\\Python.exe', + 'C:\\Program Files\\WindowsApps\\Python.exe', + 'C:\\Microsoft\\WindowsApps\\PythonSoftwareFoundation\\Python.exe', + 'C:\\microsoft\\WindowsApps\\PythonSoftwareFoundation\\Something\\Python.exe', + ]; + for (const interpreter of windowsStoreHiddenInterpreters) { + test(`${interpreter} must be identified as a windows store (hidden) interpter`, () => { + expect(windowsStoreInterpreter.isHiddenInterpreter(interpreter)).to.equal(true, 'Must be true'); + }); + test(`${interpreter.toLowerCase()} must be identified as a windows store (hidden) interpter (ignoring case)`, () => { + expect(windowsStoreInterpreter.isHiddenInterpreter(interpreter.toLowerCase())).to.equal( + true, + 'Must be true', + ); + expect(windowsStoreInterpreter.isHiddenInterpreter(interpreter.toUpperCase())).to.equal( + true, + 'Must be true', + ); + }); + test(`${interpreter} must be identified as a windows store (hidden) interpter (ignoring driver letter)`, () => { + expect(windowsStoreInterpreter.isHiddenInterpreter(`D${interpreter.substring(1)}`)).to.equal( + true, + 'Must be true', + ); + }); + } + const nonWindowsStoreHiddenInterpreters = [ + 'C:\\Microsofts\\WindowsApps\\Something\\Python.exe', + 'C:\\Microsoft\\WindowsAppss\\Python.exe', + ]; + for (const interpreter of nonWindowsStoreHiddenInterpreters) { + test(`${interpreter} must not be identified as a windows store (hidden) interpter`, () => { + expect(windowsStoreInterpreter.isHiddenInterpreter(interpreter)).to.equal(false, 'Must be true'); + }); + } + + test('Getting hash should get hash of python executable', async () => { + const pythonPath = 'WindowsInterpreterPath'; + + const stateStore = mock<PersistentState<string | undefined>>(PersistentState); + const key = `WINDOWS_STORE_INTERPRETER_HASH_${pythonPath}`; + const pythonService = mock<IPythonExecutionService>(); + const pythonServiceInstance = instance(pythonService); + (pythonServiceInstance as any).then = undefined; + const oneHour = 60 * 60 * 1000; + + when( + persistanceStateFactory.createGlobalPersistentState<string | undefined>(key, undefined, oneHour), + ).thenReturn(instance(stateStore)); + when(stateStore.value).thenReturn(); + when(executionFactory.create(deepEqual({ pythonPath }))).thenResolve(pythonServiceInstance); + when(pythonService.getExecutablePath()).thenResolve('FullyQualifiedPathToPythonExec'); + when(fs.getFileHash('FullyQualifiedPathToPythonExec')).thenResolve('hash'); + when(stateStore.updateValue('hash')).thenResolve(); + + const hash = await windowsStoreInterpreter.getInterpreterHash(pythonPath); + + verify(persistanceStateFactory.createGlobalPersistentState(key, undefined, oneHour)).once(); + verify(stateStore.value).once(); + verify(executionFactory.create(deepEqual({ pythonPath }))).once(); + verify(pythonService.getExecutablePath()).once(); + verify(fs.getFileHash('FullyQualifiedPathToPythonExec')).once(); + verify(stateStore.updateValue('hash')).once(); + expect(hash).to.equal('hash'); + }); + + test('Getting hash from cache', async () => { + const pythonPath = 'WindowsInterpreterPath'; + + const stateStore = mock<PersistentState<string | undefined>>(PersistentState); + const key = `WINDOWS_STORE_INTERPRETER_HASH_${pythonPath}`; + const oneHour = 60 * 60 * 1000; + + when( + persistanceStateFactory.createGlobalPersistentState<string | undefined>(key, undefined, oneHour), + ).thenReturn(instance(stateStore)); + when(stateStore.value).thenReturn('fileHash'); + const hash = await windowsStoreInterpreter.getInterpreterHash(pythonPath); + + verify(persistanceStateFactory.createGlobalPersistentState(key, undefined, oneHour)).once(); + verify(stateStore.value).atLeast(1); + verify(executionFactory.create(anything())).never(); + verify(fs.getFileHash(anything())).never(); + verify(stateStore.updateValue(anything())).never(); + expect(hash).to.equal('fileHash'); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts new file mode 100644 index 000000000000..6bbba7c7bef2 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/windowsStoreLocator.unit.test.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as platformApis from '../../../../client/common/utils/platform'; +import * as storeApis from '../../../../client/pythonEnvironments/discovery/locators/services/windowsStoreLocator'; +import { TEST_LAYOUT_ROOT } from '../../common/commonTestConstants'; + +suite('Windows Store Utils', () => { + let getEnvVar: sinon.SinonStub; + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData); + }); + teardown(() => { + getEnvVar.restore(); + }); + test('Store Python Interpreters', async () => { + const expected = [ + path.join(testStoreAppRoot, 'python.exe'), + path.join(testStoreAppRoot, 'python3.7.exe'), + path.join(testStoreAppRoot, 'python3.8.exe'), + path.join(testStoreAppRoot, 'python3.exe'), + ]; + + const actual = await storeApis.getWindowsStorePythonExes(); + assert.deepEqual(actual, expected); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvService.test.ts b/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvService.test.ts new file mode 100644 index 000000000000..f10d7353dacb --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvService.test.ts @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-classes-per-file max-func-body-length no-invalid-this +import { expect } from 'chai'; +import { exec } from 'child_process'; +import * as path from 'path'; +import { promisify } from 'util'; +import { Uri } from 'vscode'; +import '../../../../client/common/extensions'; +import { createDeferredFromPromise, Deferred } from '../../../../client/common/utils/async'; +import { StopWatch } from '../../../../client/common/utils/stopWatch'; +import { + IInterpreterLocatorService, + IInterpreterWatcherBuilder, + WORKSPACE_VIRTUAL_ENV_SERVICE, +} from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { WorkspaceVirtualEnvWatcherService } from '../../../../client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvWatcherService'; +import { IS_CI_SERVER } from '../../../ciConstants'; +import { + deleteFiles, + getOSType, + isPythonVersionInProcess, + OSType, + PYTHON_PATH, + rootWorkspaceUri, + waitForCondition, +} from '../../../common'; +import { IS_MULTI_ROOT_TEST } from '../../../constants'; +import { sleep } from '../../../core'; +import { initialize, multirootPath } from '../../../initialize'; + +const execAsync = promisify(exec); +async function run(argv: string[], cwd: string) { + const cmdline = argv.join(' '); + const { stderr } = await execAsync(cmdline, { + cwd, + }); + if (stderr && stderr.length > 0) { + throw Error(stderr); + } +} + +class Venvs { + constructor(private readonly cwd: string, private readonly prefix = '.venv-') {} + + public async create(name: string): Promise<string> { + const venvRoot = this.resolve(name); + const argv = [PYTHON_PATH.fileToCommandArgument(), '-m', 'venv', venvRoot]; + try { + await run(argv, this.cwd); + } catch (err) { + throw new Error(`Failed to create Env ${path.basename(venvRoot)}, ${PYTHON_PATH}, Error: ${err}`); + } + return venvRoot; + } + + public async cleanUp() { + const globPattern = path.join(this.cwd, `${this.prefix}*`); + await deleteFiles(globPattern); + } + + private getID(name: string): string { + // Ensure env is random to avoid conflicts in tests (currupting test data). + const now = new Date().getTime().toString(); + return `${this.prefix}${name}${now}`; + } + + private resolve(name: string): string { + const id = this.getID(name); + return path.join(this.cwd, id); + } +} + +const baseTimeoutMs = 30_000; +const timeoutMs = IS_CI_SERVER ? baseTimeoutMs * 4 : baseTimeoutMs; +suite('Interpreters - Workspace VirtualEnv Service', function () { + suiteSetup(async function () { + // tslint:disable-next-line:no-suspicious-comment + // TODO: https://github.com/microsoft/vscode-python/issues/13649 + // These tests have been disabled due to flakiness. It's likely + // that we will replace them while refactoring the locators + // rather than fix these tests. + return this.skip(); + }); + + this.timeout(timeoutMs); + this.retries(0); + + const workspaceUri = IS_MULTI_ROOT_TEST ? Uri.file(path.join(multirootPath, 'workspace3')) : rootWorkspaceUri!; + // "workspace4 does not exist. + const workspace4 = Uri.file(path.join(multirootPath, 'workspace4')); + const venvs = new Venvs(workspaceUri.fsPath); + + let serviceContainer: IServiceContainer; + let locator: IInterpreterLocatorService; + + async function manuallyTriggerFSWatcher(deferred: Deferred<void>) { + // Monitoring files on virtualized environments can be finicky... + // Lets trigger the fs watcher manually for the tests. + const stopWatch = new StopWatch(); + const builder = serviceContainer.get<IInterpreterWatcherBuilder>(IInterpreterWatcherBuilder); + const watcher = (await builder.getWorkspaceVirtualEnvInterpreterWatcher( + workspaceUri, + )) as WorkspaceVirtualEnvWatcherService; + const binDir = getOSType() === OSType.Windows ? 'Scripts' : 'bin'; + const executable = getOSType() === OSType.Windows ? 'python.exe' : 'python'; + while (!deferred.completed && stopWatch.elapsedTime < timeoutMs - 10_000) { + const pythonPath = path.join(workspaceUri.fsPath, binDir, executable); + watcher.createHandler(Uri.file(pythonPath)).ignoreErrors(); + await sleep(1000); + } + } + async function waitForInterpreterToBeDetected(venvRoot: string) { + const envNameToLookFor = path.basename(venvRoot); + const predicate = async () => { + const items = await locator.getInterpreters(workspaceUri); + return items.some((item) => item.envName === envNameToLookFor); + }; + const promise = waitForCondition( + predicate, + timeoutMs, + `${envNameToLookFor}, Environment not detected in the workspace ${workspaceUri.fsPath}`, + ); + const deferred = createDeferredFromPromise(promise); + manuallyTriggerFSWatcher(deferred).ignoreErrors(); + await deferred.promise; + } + async function createVirtualEnvironment(envSuffix: string) { + return venvs.create(envSuffix); + } + + suiteSetup(async function () { + // skip for Python < 3, no venv support + if (await isPythonVersionInProcess(undefined, '2')) { + return this.skip(); + } + + serviceContainer = (await initialize()).serviceContainer; + locator = serviceContainer.get<IInterpreterLocatorService>( + IInterpreterLocatorService, + WORKSPACE_VIRTUAL_ENV_SERVICE, + ); + // This test is required, we need to wait for interpreter listing completes, + // before proceeding with other tests. + await venvs.cleanUp(); + await locator.getInterpreters(workspaceUri); + }); + + suiteTeardown(async () => venvs.cleanUp()); + teardown(async () => venvs.cleanUp()); + + test('Detect Virtual Environment', async () => { + const envName = await createVirtualEnvironment('one'); + await waitForInterpreterToBeDetected(envName); + }); + + test('Detect a new Virtual Environment', async () => { + const env1 = await createVirtualEnvironment('first'); + await waitForInterpreterToBeDetected(env1); + + // Ensure second environment in our workspace folder is detected when created. + const env2 = await createVirtualEnvironment('second'); + await waitForInterpreterToBeDetected(env2); + }); + + test('Detect a new Virtual Environment, and other workspace folder must not be affected (multiroot)', async function () { + if (!IS_MULTI_ROOT_TEST) { + return this.skip(); + } + // There should be nothing in workspacec4. + let items4 = await locator.getInterpreters(workspace4); + expect(items4).to.be.lengthOf(0); + + const [env1, env2] = await Promise.all([ + createVirtualEnvironment('first3'), + createVirtualEnvironment('second3'), + ]); + await Promise.all([waitForInterpreterToBeDetected(env1), waitForInterpreterToBeDetected(env2)]); + + // Workspace4 should still not have any interpreters. + items4 = await locator.getInterpreters(workspace4); + expect(items4).to.be.lengthOf(0); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvService.unit.test.ts new file mode 100644 index 000000000000..a7b86cb239aa --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvService.unit.test.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-classes-per-file max-func-body-length + +import { expect } from 'chai'; +import { + anything, instance, mock, verify, when, +} from 'ts-mockito'; +import { Uri } from 'vscode'; +import { IInterpreterWatcher } from '../../../../client/interpreter/contracts'; +import { ServiceContainer } from '../../../../client/ioc/container'; +import { InterpreterWatcherBuilder } from '../../../../client/pythonEnvironments/discovery/locators/services/interpreterWatcherBuilder'; +import { WorkspaceVirtualEnvService } from '../../../../client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvService'; + +suite('Interpreters - Workspace VirtualEnv Service', () => { + test('Get list of watchers', async () => { + const serviceContainer = mock(ServiceContainer); + const builder = mock(InterpreterWatcherBuilder); + const locator = new (class extends WorkspaceVirtualEnvService { + // tslint:disable-next-line:no-unnecessary-override + public async getInterpreterWatchers(resource: Uri | undefined): Promise<IInterpreterWatcher[]> { + return super.getInterpreterWatchers(resource); + } + })(undefined as any, instance(serviceContainer), instance(builder)); + + const watchers = 1 as any; + when(builder.getWorkspaceVirtualEnvInterpreterWatcher(anything())).thenResolve(watchers); + + const items = await locator.getInterpreterWatchers(undefined); + + expect(items).to.deep.equal([watchers]); + verify(builder.getWorkspaceVirtualEnvInterpreterWatcher(anything())).once(); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvWatcherService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvWatcherService.unit.test.ts new file mode 100644 index 000000000000..6d9aacd653b2 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/workspaceVirtualEnvWatcherService.unit.test.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-classes-per-file max-func-body-length no-invalid-this + +import { expect } from 'chai'; +import * as path from 'path'; +import { + anything, instance, mock, verify, when, +} from 'ts-mockito'; +import { + Disposable, FileSystemWatcher, Uri, WorkspaceFolder, +} from 'vscode'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { isUnitTestExecution } from '../../../../client/common/constants'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { PythonExecutionFactory } from '../../../../client/common/process/pythonExecutionFactory'; +import { sleep } from '../../../../client/common/utils/async'; +import { noop } from '../../../../client/common/utils/misc'; +import { OSType } from '../../../../client/common/utils/platform'; +import { WorkspaceVirtualEnvWatcherService } from '../../../../client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvWatcherService'; + +suite('Interpreters - Workspace VirtualEnv Watcher Service', () => { + let disposables: Disposable[] = []; + setup(function () { + if (!isUnitTestExecution()) { + return this.skip(); + } + }); + teardown(() => { + disposables.forEach((d) => { + try { + d.dispose(); + } catch { + noop(); + } + }); + disposables = []; + }); + + async function checkForFileChanges(os: OSType, resource: Uri | undefined, hasWorkspaceFolder: boolean) { + const workspaceService = mock(WorkspaceService); + const platformService = mock(PlatformService); + const execFactory = mock(PythonExecutionFactory); + const watcher = new WorkspaceVirtualEnvWatcherService( + [], + instance(workspaceService), + instance(platformService), + instance(execFactory), + ); + + when(platformService.isWindows).thenReturn(os === OSType.Windows); + when(platformService.isLinux).thenReturn(os === OSType.Linux); + when(platformService.isMac).thenReturn(os === OSType.OSX); + + class FSWatcher { + public onDidCreate(_listener: (e: Uri) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { + return { dispose: noop }; + } + } + + const workspaceFolder: WorkspaceFolder = { name: 'one', index: 1, uri: Uri.file(path.join('root', 'dev')) }; + if (!hasWorkspaceFolder || !resource) { + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + } else { + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + } + + const fsWatcher = mock(FSWatcher); + when(workspaceService.createFileSystemWatcher(anything())).thenReturn( + instance((fsWatcher as any) as FileSystemWatcher), + ); + + await watcher.register(resource); + + verify(workspaceService.createFileSystemWatcher(anything())).twice(); + verify(fsWatcher.onDidCreate(anything(), anything(), anything())).twice(); + } + for (const uri of [undefined, Uri.file('abc')]) { + for (const hasWorkspaceFolder of [true, false]) { + const uriSuffix = uri + ? ` (with resource & ${hasWorkspaceFolder ? 'with' : 'without'} workspace folder)` + : ''; + test(`Register for file changes on windows ${uriSuffix}`, async () => { + await checkForFileChanges(OSType.Windows, uri, hasWorkspaceFolder); + }); + test(`Register for file changes on Mac ${uriSuffix}`, async () => { + await checkForFileChanges(OSType.OSX, uri, hasWorkspaceFolder); + }); + test(`Register for file changes on Linux ${uriSuffix}`, async () => { + await checkForFileChanges(OSType.Linux, uri, hasWorkspaceFolder); + }); + } + } + async function ensureFileChanesAreHandled(os: OSType) { + const workspaceService = mock(WorkspaceService); + const platformService = mock(PlatformService); + const execFactory = mock(PythonExecutionFactory); + const watcher = new WorkspaceVirtualEnvWatcherService( + disposables, + instance(workspaceService), + instance(platformService), + instance(execFactory), + ); + + when(platformService.isWindows).thenReturn(os === OSType.Windows); + when(platformService.isLinux).thenReturn(os === OSType.Linux); + when(platformService.isMac).thenReturn(os === OSType.OSX); + + class FSWatcher { + private listener?: (e: Uri) => any; + + public onDidCreate(listener: (e: Uri) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { + this.listener = listener; + return { dispose: noop }; + } + + public invokeListener(e: Uri) { + this.listener!(e); + } + } + const fsWatcher = new FSWatcher(); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + when(workspaceService.createFileSystemWatcher(anything())).thenReturn((fsWatcher as any) as FileSystemWatcher); + await watcher.register(undefined); + let invoked = false; + watcher.onDidCreate(() => (invoked = true), watcher); + + fsWatcher.invokeListener(Uri.file('')); + // We need this sleep, as we have a debounce (so lets wait). + await sleep(10); + + expect(invoked).to.be.equal(true, 'invalid'); + } + test('Check file change handler on Windows', async () => { + await ensureFileChanesAreHandled(OSType.Windows); + }); + test('Check file change handler on Mac', async () => { + await ensureFileChanesAreHandled(OSType.OSX); + }); + test('Check file change handler on Linux', async () => { + await ensureFileChanesAreHandled(OSType.Linux); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/subenv.unit.test.ts b/src/test/pythonEnvironments/discovery/subenv.unit.test.ts new file mode 100644 index 000000000000..47f648079ba8 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/subenv.unit.test.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import * as sut from '../../../client/pythonEnvironments/discovery/subenv'; +import { EnvironmentType } from '../../../client/pythonEnvironments/info'; + +suite('getName()', () => { + // We will pull tests over from src/test/interpreters/virtualEnvs/index.unit.test.ts at some point. +}); + +suite('getType()', () => { + interface IFinders { + venv(python: string): Promise<EnvironmentType | undefined>; + pyenv(python: string): Promise<EnvironmentType | undefined>; + pipenv(python: string): Promise<EnvironmentType | undefined>; + virtualenv(python: string): Promise<EnvironmentType | undefined>; + } + let finders: TypeMoq.IMock<IFinders>; + setup(() => { + finders = TypeMoq.Mock.ofType<IFinders>(undefined, TypeMoq.MockBehavior.Strict); + }); + function verifyAll() { + finders.verifyAll(); + } + + test('detects the first type', async () => { + const python = 'x/y/z/bin/python'; + finders + .setup((f) => f.venv(python)) + // found + .returns(() => Promise.resolve(EnvironmentType.Venv)); + + const result = await sut.getType(python, [ + (p: string) => finders.object.venv(p), + (p: string) => finders.object.pyenv(p), + (p: string) => finders.object.pipenv(p), + (p: string) => finders.object.virtualenv(p), + ]); + + expect(result).to.equal(EnvironmentType.Venv, 'broken'); + verifyAll(); + }); + + test('detects the second type', async () => { + const python = 'x/y/z/bin/python'; + finders + .setup((f) => f.venv(python)) + // not found + .returns(() => Promise.resolve(undefined)); + finders + .setup((f) => f.pyenv(python)) + // found + .returns(() => Promise.resolve(EnvironmentType.Pyenv)); + + const result = await sut.getType(python, [ + (p: string) => finders.object.venv(p), + (p: string) => finders.object.pyenv(p), + (p: string) => finders.object.pipenv(p), + (p: string) => finders.object.virtualenv(p), + ]); + + expect(result).to.equal(EnvironmentType.Pyenv, 'broken'); + verifyAll(); + }); + + test('does not detect the type', async () => { + const python = 'x/y/z/bin/python'; + finders + .setup((f) => f.venv(python)) + // not found + .returns(() => Promise.resolve(undefined)); + finders + .setup((f) => f.pyenv(python)) + // not found + .returns(() => Promise.resolve(undefined)); + finders + .setup((f) => f.pipenv(python)) + // not found + .returns(() => Promise.resolve(undefined)); + finders + .setup((f) => f.virtualenv(python)) + // not found + .returns(() => Promise.resolve(undefined)); + + const result = await sut.getType(python, [ + (p: string) => finders.object.venv(p), + (p: string) => finders.object.pyenv(p), + (p: string) => finders.object.pipenv(p), + (p: string) => finders.object.virtualenv(p), + ]); + + expect(result).to.equal(undefined, 'broken'); + verifyAll(); + }); +}); + +suite('getNameFinders()', () => { + // We will pull tests over from src/test/interpreters/virtualEnvs/index.unit.test.ts at some point. +}); + +suite('getTypeFinders()', () => { + // We will pull tests over from src/test/interpreters/virtualEnvs/index.unit.test.ts at some point. +}); + +suite('getVenvTypeFinder()', () => { + // We will pull tests over from src/test/interpreters/virtualEnvs/index.unit.test.ts at some point. +}); + +suite('getVirtualenvTypeFinder()', () => { + // We will pull tests over from src/test/interpreters/virtualEnvs/index.unit.test.ts at some point. +}); + +suite('getPipenvTypeFinder()', () => { + // We will pull tests over from src/test/interpreters/virtualEnvs/index.unit.test.ts at some point. +}); diff --git a/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts b/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts new file mode 100644 index 000000000000..9d810f96f2d9 --- /dev/null +++ b/src/test/pythonEnvironments/info/environmentInfoService.functional.test.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { ImportMock } from 'ts-mock-imports'; +import { ExecutionResult } from '../../../client/common/process/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import * as ExternalDep from '../../../client/pythonEnvironments/common/externalDependencies'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { + EnvironmentInfoService, + EnvironmentInfoServiceQueuePriority, +} from '../../../client/pythonEnvironments/info/environmentInfoService'; + +suite('Environment Info Service', () => { + let stubShellExec: sinon.SinonStub; + + function createExpectedEnvInfo(path: string): PythonEnvironment { + return { + path, + architecture: Architecture.x64, + sysVersion: undefined, + sysPrefix: 'path', + pipEnvWorkspaceFolder: undefined, + version: { + build: [], + major: 3, + minor: 8, + patch: 3, + prerelease: ['final'], + raw: '3.8.3-final', + }, + companyDisplayName: '', + displayName: '', + envType: EnvironmentType.Unknown, + envName: '', + envPath: '', + cachedEntry: false, + }; + } + + setup(() => { + stubShellExec = ImportMock.mockFunction( + ExternalDep, + 'shellExecute', + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stdout: + '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "version": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', + }); + }), + ); + }); + teardown(() => { + stubShellExec.restore(); + }); + test('Add items to queue and get results', async () => { + const envService = new EnvironmentInfoService(); + const promises: Promise<PythonEnvironment | undefined>[] = []; + const expected: PythonEnvironment[] = []; + for (let i = 0; i < 10; i = i + 1) { + const path = `any-path${i}`; + if (i < 5) { + promises.push(envService.getEnvironmentInfo(path)); + } else { + promises.push(envService.getEnvironmentInfo(path, EnvironmentInfoServiceQueuePriority.High)); + } + expected.push(createExpectedEnvInfo(path)); + } + + await Promise.all(promises).then((r) => { + // The processing order is non-deterministic since we don't know + // how long each work item will take. So we compare here with + // results of processing in the same order as we have collected + // the promises. + assert.deepEqual(r, expected); + }); + }); + + test('Add same item to queue', async () => { + const envService = new EnvironmentInfoService(); + const promises: Promise<PythonEnvironment | undefined>[] = []; + const expected: PythonEnvironment[] = []; + + const path = 'any-path'; + // Clear call counts + stubShellExec.resetHistory(); + // Evaluate once so the result is cached. + await envService.getEnvironmentInfo(path); + + for (let i = 0; i < 10; i = i + 1) { + promises.push(envService.getEnvironmentInfo(path)); + expected.push(createExpectedEnvInfo(path)); + } + + await Promise.all(promises).then((r) => { + assert.deepEqual(r, expected); + }); + assert.ok(stubShellExec.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/info/executable.unit.test.ts b/src/test/pythonEnvironments/info/executable.unit.test.ts new file mode 100644 index 000000000000..56e81fca8ad5 --- /dev/null +++ b/src/test/pythonEnvironments/info/executable.unit.test.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { join as pathJoin } from 'path'; +import { IMock, Mock, MockBehavior } from 'typemoq'; +import { StdErrError } from '../../../client/common/process/types'; +import { buildPythonExecInfo } from '../../../client/pythonEnvironments/exec'; +import { getExecutablePath } from '../../../client/pythonEnvironments/info/executable'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; + +const isolated = pathJoin(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'pyvsc-run-isolated.py'); + +type ExecResult = { + stdout: string; +}; +interface IDeps { + exec(command: string, args: string[]): Promise<ExecResult>; +} + +suite('getExecutablePath()', () => { + let deps: IMock<IDeps>; + const python = buildPythonExecInfo('path/to/python'); + + setup(() => { + deps = Mock.ofType<IDeps>(undefined, MockBehavior.Strict); + }); + + test('should get the value by running python', async () => { + const expected = 'path/to/dummy/executable'; + const argv = [isolated, '-c', 'import sys;print(sys.executable)']; + deps.setup((d) => d.exec(python.command, argv)) + // Return the expected value. + .returns(() => Promise.resolve({ stdout: expected })); + const exec = async (c: string, a: string[]) => deps.object.exec(c, a); + + const result = await getExecutablePath(python, exec); + + expect(result).to.equal(expected, 'getExecutablePath() should return get the value by running Python'); + deps.verifyAll(); + }); + + test('should throw if exec() fails', async () => { + const stderr = 'oops'; + const argv = [isolated, '-c', 'import sys;print(sys.executable)']; + deps.setup((d) => d.exec(python.command, argv)) + // Throw an error. + .returns(() => Promise.reject(new StdErrError(stderr))); + const exec = async (c: string, a: string[]) => deps.object.exec(c, a); + + const result = getExecutablePath(python, exec); + + expect(result).to.eventually.be.rejectedWith(stderr); + deps.verifyAll(); + }); +}); diff --git a/src/test/pythonEnvironments/info/index.unit.test.ts b/src/test/pythonEnvironments/info/index.unit.test.ts new file mode 100644 index 000000000000..be3f0b5d4f71 --- /dev/null +++ b/src/test/pythonEnvironments/info/index.unit.test.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// Move all the tests from `helper.unit.test.ts` here once `helper.ts` which contains +// the old merge environments implementation is removed. diff --git a/src/test/pythonEnvironments/info/interpreter.unit.test.ts b/src/test/pythonEnvironments/info/interpreter.unit.test.ts new file mode 100644 index 000000000000..e6a11ac1db1e --- /dev/null +++ b/src/test/pythonEnvironments/info/interpreter.unit.test.ts @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { join as pathJoin } from 'path'; +import { SemVer } from 'semver'; +import { + IMock, It as TypeMoqIt, Mock, MockBehavior, +} from 'typemoq'; +import { StdErrError } from '../../../client/common/process/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { buildPythonExecInfo } from '../../../client/pythonEnvironments/exec'; +import { getInterpreterInfo } from '../../../client/pythonEnvironments/info/interpreter'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; + +const isolated = pathJoin(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'pyvsc-run-isolated.py'); +const script = pathJoin(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles', 'interpreterInfo.py'); + +suite('extractInterpreterInfo()', () => { + // Tests go here. +}); + +type ShellExecResult = { + stdout: string; + stderr?: string; +}; +interface IDeps { + shellExec(command: string, timeout: number): Promise<ShellExecResult>; +} + +suite('getInterpreterInfo()', () => { + let deps: IMock<IDeps>; + const python = buildPythonExecInfo('path/to/python'); + + setup(() => { + deps = Mock.ofType<IDeps>(undefined, MockBehavior.Strict); + }); + + test('should call exec() with the proper command and timeout', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate', 1], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true, + }; + const cmd = `"${python.command}" "${isolated}" "${script}"`; + deps + // Checking the args is the key point of this test. + .setup((d) => d.shellExec(cmd, 15000)) + .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + const shellExec = async (c: string, t: number) => deps.object.shellExec(c, t); + + await getInterpreterInfo(python, shellExec); + + deps.verifyAll(); + }); + + test('should quote spaces in the command', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate', 1], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true, + }; + const _python = buildPythonExecInfo(' path to /my python '); + const cmd = `" path to /my python " "${isolated}" "${script}"`; + deps + // Checking the args is the key point of this test. + .setup((d) => d.shellExec(cmd, 15000)) + .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + const shellExec = async (c: string, t: number) => deps.object.shellExec(c, t); + + await getInterpreterInfo(_python, shellExec); + + deps.verifyAll(); + }); + + test('should handle multi-command (e.g. conda)', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate', 1], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true, + }; + const _python = buildPythonExecInfo(['path/to/conda', 'run', '-n', 'my-env', 'python']); + const cmd = `"path/to/conda" "run" "-n" "my-env" "python" "${isolated}" "${script}"`; + deps + // Checking the args is the key point of this test. + .setup((d) => d.shellExec(cmd, 15000)) + .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + const shellExec = async (c: string, t: number) => deps.object.shellExec(c, t); + + await getInterpreterInfo(_python, shellExec); + + deps.verifyAll(); + }); + + test('should return an object if exec() is successful', async () => { + const expected = { + architecture: Architecture.x64, + path: python.command, + version: new SemVer('3.7.5-candidate'), + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + sysVersion: undefined, + }; + const json = { + versionInfo: [3, 7, 5, 'candidate', 1], + sysPrefix: expected.sysPrefix, + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true, + }; + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + const shellExec = async (c: string, t: number) => deps.object.shellExec(c, t); + + const result = await getInterpreterInfo(python, shellExec); + + expect(result).to.deep.equal(expected, 'broken'); + deps.verifyAll(); + }); + + test('should return an object if the version info contains less than 4 items', async () => { + const expected = { + architecture: Architecture.x64, + path: python.command, + version: new SemVer('3.7.5'), + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + sysVersion: undefined, + }; + const json = { + versionInfo: [3, 7, 5], + sysPrefix: expected.sysPrefix, + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true, + }; + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + const shellExec = async (c: string, t: number) => deps.object.shellExec(c, t); + + const result = await getInterpreterInfo(python, shellExec); + + expect(result).to.deep.equal(expected, 'broken'); + deps.verifyAll(); + }); + + test('should return an object with the architecture value set to x86 if json.is64bit is not 64bit', async () => { + const expected = { + architecture: Architecture.x86, + path: python.command, + version: new SemVer('3.7.5-candidate'), + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + sysVersion: undefined, + }; + const json = { + versionInfo: [3, 7, 5, 'candidate'], + sysPrefix: expected.sysPrefix, + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: false, + }; + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); + const shellExec = async (c: string, t: number) => deps.object.shellExec(c, t); + + const result = await getInterpreterInfo(python, shellExec); + + expect(result).to.deep.equal(expected, 'broken'); + deps.verifyAll(); + }); + + test('should return undefined if the result of exec() writes to stderr', async () => { + const err = new StdErrError('oops!'); + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => Promise.reject(err)); + const shellExec = async (c: string, t: number) => deps.object.shellExec(c, t); + + const result = getInterpreterInfo(python, shellExec); + + await expect(result).to.eventually.be.rejectedWith(err); + deps.verifyAll(); + }); + + test('should fail if exec() fails (e.g. the script times out)', async () => { + const err = new Error('oops'); + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => Promise.reject(err)); + const shellExec = async (c: string, t: number) => deps.object.shellExec(c, t); + + const result = getInterpreterInfo(python, shellExec); + + await expect(result).to.eventually.be.rejectedWith(err); + deps.verifyAll(); + }); + + test('should fail if the json value returned by interpreterInfo.py is not valid', async () => { + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => Promise.resolve({ stdout: 'bad json' })); + const shellExec = async (c: string, t: number) => deps.object.shellExec(c, t); + + const result = getInterpreterInfo(python, shellExec); + + await expect(result).to.eventually.be.rejected; + deps.verifyAll(); + }); +}); diff --git a/src/test/pythonEnvironments/info/pythonVersion.unit.test.ts b/src/test/pythonEnvironments/info/pythonVersion.unit.test.ts new file mode 100644 index 000000000000..70dbd90cacfa --- /dev/null +++ b/src/test/pythonEnvironments/info/pythonVersion.unit.test.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { join as pathJoin } from 'path'; +import { It as TypeMoqIt, Mock, MockBehavior } from 'typemoq'; +import { getPythonVersion, parsePythonVersion } from '../../../client/pythonEnvironments/info/pythonVersion'; + +interface IDeps { + exec(cmd: string, args: string[]): Promise<{ stdout: string }>; +} + +suite('parsePythonVersion()', () => { + test('Must convert undefined if empty string', async () => { + // tslint:disable-next-line: no-any + assert.equal(parsePythonVersion(undefined as any), undefined); + assert.equal(parsePythonVersion(''), undefined); + }); + test('Must convert version correctly', async () => { + const version = parsePythonVersion('3.7.1')!; + assert.equal(version.raw, '3.7.1'); + assert.equal(version.major, 3); + assert.equal(version.minor, 7); + assert.equal(version.patch, 1); + assert.deepEqual(version.prerelease, []); + }); + test('Must convert version correctly with pre-release', async () => { + const version = parsePythonVersion('3.7.1-alpha')!; + assert.equal(version.raw, '3.7.1-alpha'); + assert.equal(version.major, 3); + assert.equal(version.minor, 7); + assert.equal(version.patch, 1); + assert.deepEqual(version.prerelease, ['alpha']); + }); + test('Must remove invalid pre-release channels', async () => { + assert.deepEqual(parsePythonVersion('3.7.1-alpha')!.prerelease, ['alpha']); + assert.deepEqual(parsePythonVersion('3.7.1-beta')!.prerelease, ['beta']); + assert.deepEqual(parsePythonVersion('3.7.1-candidate')!.prerelease, ['candidate']); + assert.deepEqual(parsePythonVersion('3.7.1-final')!.prerelease, ['final']); + assert.deepEqual(parsePythonVersion('3.7.1-unknown')!.prerelease, []); + assert.deepEqual(parsePythonVersion('3.7.1-')!.prerelease, []); + assert.deepEqual(parsePythonVersion('3.7.1-prerelease')!.prerelease, []); + }); + test('Must default versions partgs to 0 if they are not numeric', async () => { + assert.deepEqual(parsePythonVersion('3.B.1')!.raw, '3.0.1'); + assert.deepEqual(parsePythonVersion('3.B.C')!.raw, '3.0.0'); + assert.deepEqual(parsePythonVersion('A.B.C')!.raw, '0.0.0'); + }); +}); + +suite('getPythonVersion()', () => { + test('Must return the Python Version', async () => { + const pythonPath = pathJoin('a', 'b', 'python'); + const expected = 'Output from the Procecss'; + const mock = Mock.ofType<IDeps>(undefined, MockBehavior.Strict); + mock.setup((p) => p.exec(TypeMoqIt.isValue(pythonPath), TypeMoqIt.isValue(['--version']))) + // Fake the process stdout. + .returns(() => Promise.resolve({ stdout: expected })); + const exec = (c: string, a: string[]) => mock.object.exec(c, a); + + const pyVersion = await getPythonVersion(pythonPath, 'DEFAULT_TEST_VALUE', exec); + + assert.equal(pyVersion, expected, 'Incorrect version'); + mock.verifyAll(); + }); + + test('Must return the default value when Python path is invalid', async () => { + const pythonPath = pathJoin('a', 'b', 'python'); + const mock = Mock.ofType<IDeps>(undefined, MockBehavior.Strict); + mock.setup((p) => p.exec(TypeMoqIt.isValue(pythonPath), TypeMoqIt.isValue(['--version']))) + // Fake the process stdout. + .returns(() => Promise.reject({})); + const exec = (c: string, a: string[]) => mock.object.exec(c, a); + + const pyVersion = await getPythonVersion(pythonPath, 'DEFAULT_TEST_VALUE', exec); + + assert.equal(pyVersion, 'DEFAULT_TEST_VALUE', 'Incorrect version'); + }); +}); diff --git a/src/test/pythonEnvironments/legacyIOC.unit.test.ts b/src/test/pythonEnvironments/legacyIOC.unit.test.ts new file mode 100644 index 000000000000..36d1b6541fba --- /dev/null +++ b/src/test/pythonEnvironments/legacyIOC.unit.test.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable: no-any + +import { + anything, instance, mock, verify, +} from 'ts-mockito'; +import { + CONDA_ENV_FILE_SERVICE, + CONDA_ENV_SERVICE, + CURRENT_PATH_SERVICE, + GLOBAL_VIRTUAL_ENV_SERVICE, + ICondaService, + IInterpreterLocatorHelper, + IInterpreterLocatorProgressService, + IInterpreterLocatorService, + IInterpreterWatcher, + IInterpreterWatcherBuilder, + IKnownSearchPathsForInterpreters, + INTERPRETER_LOCATOR_SERVICE, + IVirtualEnvironmentsSearchPathProvider, + KNOWN_PATH_SERVICE, + PIPENV_SERVICE, + WINDOWS_REGISTRY_SERVICE, + WORKSPACE_VIRTUAL_ENV_SERVICE, +} from '../../client/interpreter/contracts'; +import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from '../../client/interpreter/locators/types'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { PythonInterpreterLocatorService } from '../../client/pythonEnvironments/discovery/locators'; +import { InterpreterLocatorHelper } from '../../client/pythonEnvironments/discovery/locators/helpers'; +import { InterpreterLocatorProgressService } from '../../client/pythonEnvironments/discovery/locators/progressService'; +import { CondaEnvFileService } from '../../client/pythonEnvironments/discovery/locators/services/condaEnvFileService'; +import { CondaEnvService } from '../../client/pythonEnvironments/discovery/locators/services/condaEnvService'; +import { CondaService } from '../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { + CurrentPathService, + PythonInPathCommandProvider, +} from '../../client/pythonEnvironments/discovery/locators/services/currentPathService'; +import { + GlobalVirtualEnvironmentsSearchPathProvider, + GlobalVirtualEnvService, +} from '../../client/pythonEnvironments/discovery/locators/services/globalVirtualEnvService'; +import { InterpreterHashProvider } from '../../client/pythonEnvironments/discovery/locators/services/hashProvider'; +import { InterpeterHashProviderFactory } from '../../client/pythonEnvironments/discovery/locators/services/hashProviderFactory'; +import { InterpreterWatcherBuilder } from '../../client/pythonEnvironments/discovery/locators/services/interpreterWatcherBuilder'; +import { + KnownPathsService, + KnownSearchPathsForInterpreters, +} from '../../client/pythonEnvironments/discovery/locators/services/KnownPathsService'; +import { PipEnvService } from '../../client/pythonEnvironments/discovery/locators/services/pipEnvService'; +import { PipEnvServiceHelper } from '../../client/pythonEnvironments/discovery/locators/services/pipEnvServiceHelper'; +import { WindowsRegistryService } from '../../client/pythonEnvironments/discovery/locators/services/windowsRegistryService'; +import { WindowsStoreInterpreter } from '../../client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter'; +import { + WorkspaceVirtualEnvironmentsSearchPathProvider, + WorkspaceVirtualEnvService, +} from '../../client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvService'; +import { WorkspaceVirtualEnvWatcherService } from '../../client/pythonEnvironments/discovery/locators/services/workspaceVirtualEnvWatcherService'; +import { IEnvironmentInfoService } from '../../client/pythonEnvironments/info/environmentInfoService'; +import { registerForIOC } from '../../client/pythonEnvironments/legacyIOC'; + +suite('Interpreters - Service Registry', () => { + test('Registrations', () => { + const serviceManager = mock(ServiceManager); + const serviceContainer = mock(ServiceContainer); + registerForIOC(instance(serviceManager), instance(serviceContainer)); + verify(serviceManager.addSingleton(IKnownSearchPathsForInterpreters, KnownSearchPathsForInterpreters)).once(); + verify(serviceManager.addSingleton(IVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvironmentsSearchPathProvider, 'global')).once(); + verify(serviceManager.addSingleton(IVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvironmentsSearchPathProvider, 'workspace')).once(); + + verify(serviceManager.addSingleton(ICondaService, CondaService)).once(); + verify(serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelper)).once(); + verify(serviceManager.addSingleton(IPythonInPathCommandProvider, PythonInPathCommandProvider)).once(); + + verify(serviceManager.addSingleton(IInterpreterWatcherBuilder, InterpreterWatcherBuilder)).once(); + + verify( + serviceManager.addSingleton( + IInterpreterLocatorService, + PythonInterpreterLocatorService, + INTERPRETER_LOCATOR_SERVICE, + ), + ).once(); + verify( + serviceManager.addSingleton(IInterpreterLocatorService, CondaEnvFileService, CONDA_ENV_FILE_SERVICE), + ).once(); + verify(serviceManager.addSingleton(IInterpreterLocatorService, CondaEnvService, CONDA_ENV_SERVICE)).once(); + verify( + serviceManager.addSingleton(IInterpreterLocatorService, CurrentPathService, CURRENT_PATH_SERVICE), + ).once(); + verify( + serviceManager.addSingleton( + IInterpreterLocatorService, + GlobalVirtualEnvService, + GLOBAL_VIRTUAL_ENV_SERVICE, + ), + ).once(); + verify( + serviceManager.addSingleton( + IInterpreterLocatorService, + WorkspaceVirtualEnvService, + WORKSPACE_VIRTUAL_ENV_SERVICE, + ), + ).once(); + verify(serviceManager.addSingleton(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE)).once(); + + verify( + serviceManager.addSingleton( + IInterpreterLocatorService, + WindowsRegistryService, + WINDOWS_REGISTRY_SERVICE, + ), + ).once(); + verify(serviceManager.addSingleton(IInterpreterLocatorService, KnownPathsService, KNOWN_PATH_SERVICE)).once(); + + verify(serviceManager.addSingleton(IInterpreterLocatorHelper, InterpreterLocatorHelper)).once(); + verify( + serviceManager.addSingleton( + IInterpreterLocatorProgressService, + InterpreterLocatorProgressService, + ), + ).once(); + + verify(serviceManager.addSingleton(WindowsStoreInterpreter, WindowsStoreInterpreter)).once(); + verify(serviceManager.addSingleton(InterpreterHashProvider, InterpreterHashProvider)).once(); + verify(serviceManager.addSingleton(InterpeterHashProviderFactory, InterpeterHashProviderFactory)).once(); + + verify( + serviceManager.add<IInterpreterWatcher>( + IInterpreterWatcher, + WorkspaceVirtualEnvWatcherService, + WORKSPACE_VIRTUAL_ENV_SERVICE, + ), + ).once(); + verify( + serviceManager.addSingletonInstance<IEnvironmentInfoService>(IEnvironmentInfoService, anything()), + ).once(); + }); +}); diff --git a/src/test/pythonFiles/autocomp/deco.py b/src/test/pythonFiles/autocomp/deco.py new file mode 100644 index 000000000000..b843741ef647 --- /dev/null +++ b/src/test/pythonFiles/autocomp/deco.py @@ -0,0 +1,6 @@ + +import abc +class Decorator(metaclass=abc.ABCMeta): + @abc.-# no abstract class + @abc.abstractclassmethod + \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/doc.py b/src/test/pythonFiles/autocomp/doc.py new file mode 100644 index 000000000000..a0d62874538f --- /dev/null +++ b/src/test/pythonFiles/autocomp/doc.py @@ -0,0 +1,11 @@ +import os + +if os.path.exists(("/etc/hosts")): + with open("/etc/hosts", "a") as f: + for line in f.readlines(): + content = line.upper() + + + +import time +time.slee \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/five.py b/src/test/pythonFiles/autocomp/five.py new file mode 100644 index 000000000000..507c5fed967c --- /dev/null +++ b/src/test/pythonFiles/autocomp/five.py @@ -0,0 +1,2 @@ +import four +four.showMessage() diff --git a/src/test/pythonFiles/autocomp/four.py b/src/test/pythonFiles/autocomp/four.py new file mode 100644 index 000000000000..470338f71157 --- /dev/null +++ b/src/test/pythonFiles/autocomp/four.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# pylint: disable=E0401, W0512 + +import os + + +class Foo(object): + '''说明''' + + @staticmethod + def bar(): + """ + 说明 - keep this line, it works + delete following line, it works + 如果存在需要等待审批或正在执行的任务,将不刷新页面 + """ + return os.path.exists('c:/') + +def showMessage(): + """ + Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи. + Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку. + """ + print('1234') + +Foo.bar() +showMessage() \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/hoverTest.py b/src/test/pythonFiles/autocomp/hoverTest.py new file mode 100644 index 000000000000..0ff88d80dffc --- /dev/null +++ b/src/test/pythonFiles/autocomp/hoverTest.py @@ -0,0 +1,16 @@ +import random +import math + +for x in range(0, 10): + print(x) + +rnd = random.Random() +print(rnd.randint(0, 5)) +print(math.acos(90)) + +import misc +rnd2 = misc.Random() +rnd2.randint() + +t = misc.Thread() +t.__init__() \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/imp.py b/src/test/pythonFiles/autocomp/imp.py new file mode 100644 index 000000000000..0d0c98ed1cde --- /dev/null +++ b/src/test/pythonFiles/autocomp/imp.py @@ -0,0 +1,2 @@ +from os import * +fsta \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/lamb.py b/src/test/pythonFiles/autocomp/lamb.py new file mode 100644 index 000000000000..05b92f5cd581 --- /dev/null +++ b/src/test/pythonFiles/autocomp/lamb.py @@ -0,0 +1,2 @@ +instant_print = lambda x: [print(x), sys.stdout.flush(), sys.stderr.flush()] +instant_print("X"). \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/misc.py b/src/test/pythonFiles/autocomp/misc.py new file mode 100644 index 000000000000..3d4a54cbc145 --- /dev/null +++ b/src/test/pythonFiles/autocomp/misc.py @@ -0,0 +1,1905 @@ +"""Thread module emulating a subset of Java's threading model.""" + +import sys as _sys + +try: + import thread +except ImportError: + del _sys.modules[__name__] + raise + +import warnings + +from collections import deque as _deque +from itertools import count as _count +from time import time as _time, sleep as _sleep +from traceback import format_exc as _format_exc + +# Note regarding PEP 8 compliant aliases +# This threading model was originally inspired by Java, and inherited +# the convention of camelCase function and method names from that +# language. While those names are not in any imminent danger of being +# deprecated, starting with Python 2.6, the module now provides a +# PEP 8 compliant alias for any such method name. +# Using the new PEP 8 compliant names also facilitates substitution +# with the multiprocessing module, which doesn't provide the old +# Java inspired names. + + +# Rename some stuff so "from threading import *" is safe +__all__ = ['activeCount', 'active_count', 'Condition', 'currentThread', + 'current_thread', 'enumerate', 'Event', + 'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Thread', + 'Timer', 'setprofile', 'settrace', 'local', 'stack_size'] + +_start_new_thread = thread.start_new_thread +_allocate_lock = thread.allocate_lock +_get_ident = thread.get_ident +ThreadError = thread.error +del thread + + +# sys.exc_clear is used to work around the fact that except blocks +# don't fully clear the exception until 3.0. +warnings.filterwarnings('ignore', category=DeprecationWarning, + module='threading', message='sys.exc_clear') + +# Debug support (adapted from ihooks.py). +# All the major classes here derive from _Verbose. We force that to +# be a new-style class so that all the major classes here are new-style. +# This helps debugging (type(instance) is more revealing for instances +# of new-style classes). + +_VERBOSE = False + +if __debug__: + + class _Verbose(object): + + def __init__(self, verbose=None): + if verbose is None: + verbose = _VERBOSE + self.__verbose = verbose + + def _note(self, format, *args): + if self.__verbose: + format = format % args + # Issue #4188: calling current_thread() can incur an infinite + # recursion if it has to create a DummyThread on the fly. + ident = _get_ident() + try: + name = _active[ident].name + except KeyError: + name = "<OS thread %d>" % ident + format = "%s: %s\n" % (name, format) + _sys.stderr.write(format) + +else: + # Disable this when using "python -O" + class _Verbose(object): + def __init__(self, verbose=None): + pass + def _note(self, *args): + pass + +# Support for profile and trace hooks + +_profile_hook = None +_trace_hook = None + +def setprofile(func): + """Set a profile function for all threads started from the threading module. + + The func will be passed to sys.setprofile() for each thread, before its + run() method is called. + + """ + global _profile_hook + _profile_hook = func + +def settrace(func): + """Set a trace function for all threads started from the threading module. + + The func will be passed to sys.settrace() for each thread, before its run() + method is called. + + """ + global _trace_hook + _trace_hook = func + +# Synchronization classes + +Lock = _allocate_lock + +def RLock(*args, **kwargs): + """Factory function that returns a new reentrant lock. + + A reentrant lock must be released by the thread that acquired it. Once a + thread has acquired a reentrant lock, the same thread may acquire it again + without blocking; the thread must release it once for each time it has + acquired it. + + """ + return _RLock(*args, **kwargs) + +class _RLock(_Verbose): + """A reentrant lock must be released by the thread that acquired it. Once a + thread has acquired a reentrant lock, the same thread may acquire it + again without blocking; the thread must release it once for each time it + has acquired it. + """ + + def __init__(self, verbose=None): + _Verbose.__init__(self, verbose) + self.__block = _allocate_lock() + self.__owner = None + self.__count = 0 + + def __repr__(self): + owner = self.__owner + try: + owner = _active[owner].name + except KeyError: + pass + return "<%s owner=%r count=%d>" % ( + self.__class__.__name__, owner, self.__count) + + def acquire(self, blocking=1): + """Acquire a lock, blocking or non-blocking. + + When invoked without arguments: if this thread already owns the lock, + increment the recursion level by one, and return immediately. Otherwise, + if another thread owns the lock, block until the lock is unlocked. Once + the lock is unlocked (not owned by any thread), then grab ownership, set + the recursion level to one, and return. If more than one thread is + blocked waiting until the lock is unlocked, only one at a time will be + able to grab ownership of the lock. There is no return value in this + case. + + When invoked with the blocking argument set to true, do the same thing + as when called without arguments, and return true. + + When invoked with the blocking argument set to false, do not block. If a + call without an argument would block, return false immediately; + otherwise, do the same thing as when called without arguments, and + return true. + + """ + me = _get_ident() + if self.__owner == me: + self.__count = self.__count + 1 + if __debug__: + self._note("%s.acquire(%s): recursive success", self, blocking) + return 1 + rc = self.__block.acquire(blocking) + if rc: + self.__owner = me + self.__count = 1 + if __debug__: + self._note("%s.acquire(%s): initial success", self, blocking) + else: + if __debug__: + self._note("%s.acquire(%s): failure", self, blocking) + return rc + + __enter__ = acquire + + def release(self): + """Release a lock, decrementing the recursion level. + + If after the decrement it is zero, reset the lock to unlocked (not owned + by any thread), and if any other threads are blocked waiting for the + lock to become unlocked, allow exactly one of them to proceed. If after + the decrement the recursion level is still nonzero, the lock remains + locked and owned by the calling thread. + + Only call this method when the calling thread owns the lock. A + RuntimeError is raised if this method is called when the lock is + unlocked. + + There is no return value. + + """ + if self.__owner != _get_ident(): + raise RuntimeError("cannot release un-acquired lock") + self.__count = count = self.__count - 1 + if not count: + self.__owner = None + self.__block.release() + if __debug__: + self._note("%s.release(): final release", self) + else: + if __debug__: + self._note("%s.release(): non-final release", self) + + def __exit__(self, t, v, tb): + self.release() + + # Internal methods used by condition variables + + def _acquire_restore(self, count_owner): + count, owner = count_owner + self.__block.acquire() + self.__count = count + self.__owner = owner + if __debug__: + self._note("%s._acquire_restore()", self) + + def _release_save(self): + if __debug__: + self._note("%s._release_save()", self) + count = self.__count + self.__count = 0 + owner = self.__owner + self.__owner = None + self.__block.release() + return (count, owner) + + def _is_owned(self): + return self.__owner == _get_ident() + + +def Condition(*args, **kwargs): + """Factory function that returns a new condition variable object. + + A condition variable allows one or more threads to wait until they are + notified by another thread. + + If the lock argument is given and not None, it must be a Lock or RLock + object, and it is used as the underlying lock. Otherwise, a new RLock object + is created and used as the underlying lock. + + """ + return _Condition(*args, **kwargs) + +class _Condition(_Verbose): + """Condition variables allow one or more threads to wait until they are + notified by another thread. + """ + + def __init__(self, lock=None, verbose=None): + _Verbose.__init__(self, verbose) + if lock is None: + lock = RLock() + self.__lock = lock + # Export the lock's acquire() and release() methods + self.acquire = lock.acquire + self.release = lock.release + # If the lock defines _release_save() and/or _acquire_restore(), + # these override the default implementations (which just call + # release() and acquire() on the lock). Ditto for _is_owned(). + try: + self._release_save = lock._release_save + except AttributeError: + pass + try: + self._acquire_restore = lock._acquire_restore + except AttributeError: + pass + try: + self._is_owned = lock._is_owned + except AttributeError: + pass + self.__waiters = [] + + def __enter__(self): + return self.__lock.__enter__() + + def __exit__(self, *args): + return self.__lock.__exit__(*args) + + def __repr__(self): + return "<Condition(%s, %d)>" % (self.__lock, len(self.__waiters)) + + def _release_save(self): + self.__lock.release() # No state to save + + def _acquire_restore(self, x): + self.__lock.acquire() # Ignore saved state + + def _is_owned(self): + # Return True if lock is owned by current_thread. + # This method is called only if __lock doesn't have _is_owned(). + if self.__lock.acquire(0): + self.__lock.release() + return False + else: + return True + + def wait(self, timeout=None): + """Wait until notified or until a timeout occurs. + + If the calling thread has not acquired the lock when this method is + called, a RuntimeError is raised. + + This method releases the underlying lock, and then blocks until it is + awakened by a notify() or notifyAll() call for the same condition + variable in another thread, or until the optional timeout occurs. Once + awakened or timed out, it re-acquires the lock and returns. + + When the timeout argument is present and not None, it should be a + floating point number specifying a timeout for the operation in seconds + (or fractions thereof). + + When the underlying lock is an RLock, it is not released using its + release() method, since this may not actually unlock the lock when it + was acquired multiple times recursively. Instead, an internal interface + of the RLock class is used, which really unlocks it even when it has + been recursively acquired several times. Another internal interface is + then used to restore the recursion level when the lock is reacquired. + + """ + if not self._is_owned(): + raise RuntimeError("cannot wait on un-acquired lock") + waiter = _allocate_lock() + waiter.acquire() + self.__waiters.append(waiter) + saved_state = self._release_save() + try: # restore state no matter what (e.g., KeyboardInterrupt) + if timeout is None: + waiter.acquire() + if __debug__: + self._note("%s.wait(): got it", self) + else: + # Balancing act: We can't afford a pure busy loop, so we + # have to sleep; but if we sleep the whole timeout time, + # we'll be unresponsive. The scheme here sleeps very + # little at first, longer as time goes on, but never longer + # than 20 times per second (or the timeout time remaining). + endtime = _time() + timeout + delay = 0.0005 # 500 us -> initial delay of 1 ms + while True: + gotit = waiter.acquire(0) + if gotit: + break + remaining = endtime - _time() + if remaining <= 0: + break + delay = min(delay * 2, remaining, .05) + _sleep(delay) + if not gotit: + if __debug__: + self._note("%s.wait(%s): timed out", self, timeout) + try: + self.__waiters.remove(waiter) + except ValueError: + pass + else: + if __debug__: + self._note("%s.wait(%s): got it", self, timeout) + finally: + self._acquire_restore(saved_state) + + def notify(self, n=1): + """Wake up one or more threads waiting on this condition, if any. + + If the calling thread has not acquired the lock when this method is + called, a RuntimeError is raised. + + This method wakes up at most n of the threads waiting for the condition + variable; it is a no-op if no threads are waiting. + + """ + if not self._is_owned(): + raise RuntimeError("cannot notify on un-acquired lock") + __waiters = self.__waiters + waiters = __waiters[:n] + if not waiters: + if __debug__: + self._note("%s.notify(): no waiters", self) + return + self._note("%s.notify(): notifying %d waiter%s", self, n, + n!=1 and "s" or "") + for waiter in waiters: + waiter.release() + try: + __waiters.remove(waiter) + except ValueError: + pass + + def notifyAll(self): + """Wake up all threads waiting on this condition. + + If the calling thread has not acquired the lock when this method + is called, a RuntimeError is raised. + + """ + self.notify(len(self.__waiters)) + + notify_all = notifyAll + + +def Semaphore(*args, **kwargs): + """A factory function that returns a new semaphore. + + Semaphores manage a counter representing the number of release() calls minus + the number of acquire() calls, plus an initial value. The acquire() method + blocks if necessary until it can return without making the counter + negative. If not given, value defaults to 1. + + """ + return _Semaphore(*args, **kwargs) + +class _Semaphore(_Verbose): + """Semaphores manage a counter representing the number of release() calls + minus the number of acquire() calls, plus an initial value. The acquire() + method blocks if necessary until it can return without making the counter + negative. If not given, value defaults to 1. + + """ + + # After Tim Peters' semaphore class, but not quite the same (no maximum) + + def __init__(self, value=1, verbose=None): + if value < 0: + raise ValueError("semaphore initial value must be >= 0") + _Verbose.__init__(self, verbose) + self.__cond = Condition(Lock()) + self.__value = value + + def acquire(self, blocking=1): + """Acquire a semaphore, decrementing the internal counter by one. + + When invoked without arguments: if the internal counter is larger than + zero on entry, decrement it by one and return immediately. If it is zero + on entry, block, waiting until some other thread has called release() to + make it larger than zero. This is done with proper interlocking so that + if multiple acquire() calls are blocked, release() will wake exactly one + of them up. The implementation may pick one at random, so the order in + which blocked threads are awakened should not be relied on. There is no + return value in this case. + + When invoked with blocking set to true, do the same thing as when called + without arguments, and return true. + + When invoked with blocking set to false, do not block. If a call without + an argument would block, return false immediately; otherwise, do the + same thing as when called without arguments, and return true. + + """ + rc = False + with self.__cond: + while self.__value == 0: + if not blocking: + break + if __debug__: + self._note("%s.acquire(%s): blocked waiting, value=%s", + self, blocking, self.__value) + self.__cond.wait() + else: + self.__value = self.__value - 1 + if __debug__: + self._note("%s.acquire: success, value=%s", + self, self.__value) + rc = True + return rc + + __enter__ = acquire + + def release(self): + """Release a semaphore, incrementing the internal counter by one. + + When the counter is zero on entry and another thread is waiting for it + to become larger than zero again, wake up that thread. + + """ + with self.__cond: + self.__value = self.__value + 1 + if __debug__: + self._note("%s.release: success, value=%s", + self, self.__value) + self.__cond.notify() + + def __exit__(self, t, v, tb): + self.release() + + +def BoundedSemaphore(*args, **kwargs): + """A factory function that returns a new bounded semaphore. + + A bounded semaphore checks to make sure its current value doesn't exceed its + initial value. If it does, ValueError is raised. In most situations + semaphores are used to guard resources with limited capacity. + + If the semaphore is released too many times it's a sign of a bug. If not + given, value defaults to 1. + + Like regular semaphores, bounded semaphores manage a counter representing + the number of release() calls minus the number of acquire() calls, plus an + initial value. The acquire() method blocks if necessary until it can return + without making the counter negative. If not given, value defaults to 1. + + """ + return _BoundedSemaphore(*args, **kwargs) + +class _BoundedSemaphore(_Semaphore): + """A bounded semaphore checks to make sure its current value doesn't exceed + its initial value. If it does, ValueError is raised. In most situations + semaphores are used to guard resources with limited capacity. + """ + + def __init__(self, value=1, verbose=None): + _Semaphore.__init__(self, value, verbose) + self._initial_value = value + + def release(self): + """Release a semaphore, incrementing the internal counter by one. + + When the counter is zero on entry and another thread is waiting for it + to become larger than zero again, wake up that thread. + + If the number of releases exceeds the number of acquires, + raise a ValueError. + + """ + with self._Semaphore__cond: + if self._Semaphore__value >= self._initial_value: + raise ValueError("Semaphore released too many times") + self._Semaphore__value += 1 + self._Semaphore__cond.notify() + + +def Event(*args, **kwargs): + """A factory function that returns a new event. + + Events manage a flag that can be set to true with the set() method and reset + to false with the clear() method. The wait() method blocks until the flag is + true. + + """ + return _Event(*args, **kwargs) + +class _Event(_Verbose): + """A factory function that returns a new event object. An event manages a + flag that can be set to true with the set() method and reset to false + with the clear() method. The wait() method blocks until the flag is true. + + """ + + # After Tim Peters' event class (without is_posted()) + + def __init__(self, verbose=None): + _Verbose.__init__(self, verbose) + self.__cond = Condition(Lock()) + self.__flag = False + + def _reset_internal_locks(self): + # private! called by Thread._reset_internal_locks by _after_fork() + self.__cond.__init__() + + def isSet(self): + 'Return true if and only if the internal flag is true.' + return self.__flag + + is_set = isSet + + def set(self): + """Set the internal flag to true. + + All threads waiting for the flag to become true are awakened. Threads + that call wait() once the flag is true will not block at all. + + """ + self.__cond.acquire() + try: + self.__flag = True + self.__cond.notify_all() + finally: + self.__cond.release() + + def clear(self): + """Reset the internal flag to false. + + Subsequently, threads calling wait() will block until set() is called to + set the internal flag to true again. + + """ + self.__cond.acquire() + try: + self.__flag = False + finally: + self.__cond.release() + + def wait(self, timeout=None): + """Block until the internal flag is true. + + If the internal flag is true on entry, return immediately. Otherwise, + block until another thread calls set() to set the flag to true, or until + the optional timeout occurs. + + When the timeout argument is present and not None, it should be a + floating point number specifying a timeout for the operation in seconds + (or fractions thereof). + + This method returns the internal flag on exit, so it will always return + True except if a timeout is given and the operation times out. + + """ + self.__cond.acquire() + try: + if not self.__flag: + self.__cond.wait(timeout) + return self.__flag + finally: + self.__cond.release() + +# Helper to generate new thread names +_counter = _count().next +_counter() # Consume 0 so first non-main thread has id 1. +def _newname(template="Thread-%d"): + return template % _counter() + +# Active thread administration +_active_limbo_lock = _allocate_lock() +_active = {} # maps thread id to Thread object +_limbo = {} + + +# Main class for threads + +class Thread(_Verbose): + """A class that represents a thread of control. + + This class can be safely subclassed in a limited fashion. + + """ + __initialized = False + # Need to store a reference to sys.exc_info for printing + # out exceptions when a thread tries to use a global var. during interp. + # shutdown and thus raises an exception about trying to perform some + # operation on/with a NoneType + __exc_info = _sys.exc_info + # Keep sys.exc_clear too to clear the exception just before + # allowing .join() to return. + __exc_clear = _sys.exc_clear + + def __init__(self, group=None, target=None, name=None, + args=(), kwargs=None, verbose=None): + """This constructor should always be called with keyword arguments. Arguments are: + + *group* should be None; reserved for future extension when a ThreadGroup + class is implemented. + + *target* is the callable object to be invoked by the run() + method. Defaults to None, meaning nothing is called. + + *name* is the thread name. By default, a unique name is constructed of + the form "Thread-N" where N is a small decimal number. + + *args* is the argument tuple for the target invocation. Defaults to (). + + *kwargs* is a dictionary of keyword arguments for the target + invocation. Defaults to {}. + + If a subclass overrides the constructor, it must make sure to invoke + the base class constructor (Thread.__init__()) before doing anything + else to the thread. + +""" + assert group is None, "group argument must be None for now" + _Verbose.__init__(self, verbose) + if kwargs is None: + kwargs = {} + self.__target = target + self.__name = str(name or _newname()) + self.__args = args + self.__kwargs = kwargs + self.__daemonic = self._set_daemon() + self.__ident = None + self.__started = Event() + self.__stopped = False + self.__block = Condition(Lock()) + self.__initialized = True + # sys.stderr is not stored in the class like + # sys.exc_info since it can be changed between instances + self.__stderr = _sys.stderr + + def _reset_internal_locks(self): + # private! Called by _after_fork() to reset our internal locks as + # they may be in an invalid state leading to a deadlock or crash. + if hasattr(self, '_Thread__block'): # DummyThread deletes self.__block + self.__block.__init__() + self.__started._reset_internal_locks() + + @property + def _block(self): + # used by a unittest + return self.__block + + def _set_daemon(self): + # Overridden in _MainThread and _DummyThread + return current_thread().daemon + + def __repr__(self): + assert self.__initialized, "Thread.__init__() was not called" + status = "initial" + if self.__started.is_set(): + status = "started" + if self.__stopped: + status = "stopped" + if self.__daemonic: + status += " daemon" + if self.__ident is not None: + status += " %s" % self.__ident + return "<%s(%s, %s)>" % (self.__class__.__name__, self.__name, status) + + def start(self): + """Start the thread's activity. + + It must be called at most once per thread object. It arranges for the + object's run() method to be invoked in a separate thread of control. + + This method will raise a RuntimeError if called more than once on the + same thread object. + + """ + if not self.__initialized: + raise RuntimeError("thread.__init__() not called") + if self.__started.is_set(): + raise RuntimeError("threads can only be started once") + if __debug__: + self._note("%s.start(): starting thread", self) + with _active_limbo_lock: + _limbo[self] = self + try: + _start_new_thread(self.__bootstrap, ()) + except Exception: + with _active_limbo_lock: + del _limbo[self] + raise + self.__started.wait() + + def run(self): + """Method representing the thread's activity. + + You may override this method in a subclass. The standard run() method + invokes the callable object passed to the object's constructor as the + target argument, if any, with sequential and keyword arguments taken + from the args and kwargs arguments, respectively. + + """ + try: + if self.__target: + self.__target(*self.__args, **self.__kwargs) + finally: + # Avoid a refcycle if the thread is running a function with + # an argument that has a member that points to the thread. + del self.__target, self.__args, self.__kwargs + + def __bootstrap(self): + # Wrapper around the real bootstrap code that ignores + # exceptions during interpreter cleanup. Those typically + # happen when a daemon thread wakes up at an unfortunate + # moment, finds the world around it destroyed, and raises some + # random exception *** while trying to report the exception in + # __bootstrap_inner() below ***. Those random exceptions + # don't help anybody, and they confuse users, so we suppress + # them. We suppress them only when it appears that the world + # indeed has already been destroyed, so that exceptions in + # __bootstrap_inner() during normal business hours are properly + # reported. Also, we only suppress them for daemonic threads; + # if a non-daemonic encounters this, something else is wrong. + try: + self.__bootstrap_inner() + except: + if self.__daemonic and _sys is None: + return + raise + + def _set_ident(self): + self.__ident = _get_ident() + + def __bootstrap_inner(self): + try: + self._set_ident() + self.__started.set() + with _active_limbo_lock: + _active[self.__ident] = self + del _limbo[self] + if __debug__: + self._note("%s.__bootstrap(): thread started", self) + + if _trace_hook: + self._note("%s.__bootstrap(): registering trace hook", self) + _sys.settrace(_trace_hook) + if _profile_hook: + self._note("%s.__bootstrap(): registering profile hook", self) + _sys.setprofile(_profile_hook) + + try: + self.run() + except SystemExit: + if __debug__: + self._note("%s.__bootstrap(): raised SystemExit", self) + except: + if __debug__: + self._note("%s.__bootstrap(): unhandled exception", self) + # If sys.stderr is no more (most likely from interpreter + # shutdown) use self.__stderr. Otherwise still use sys (as in + # _sys) in case sys.stderr was redefined since the creation of + # self. + if _sys and _sys.stderr is not None: + print>>_sys.stderr, ("Exception in thread %s:\n%s" % + (self.name, _format_exc())) + elif self.__stderr is not None: + # Do the best job possible w/o a huge amt. of code to + # approximate a traceback (code ideas from + # Lib/traceback.py) + exc_type, exc_value, exc_tb = self.__exc_info() + try: + print>>self.__stderr, ( + "Exception in thread " + self.name + + " (most likely raised during interpreter shutdown):") + print>>self.__stderr, ( + "Traceback (most recent call last):") + while exc_tb: + print>>self.__stderr, ( + ' File "%s", line %s, in %s' % + (exc_tb.tb_frame.f_code.co_filename, + exc_tb.tb_lineno, + exc_tb.tb_frame.f_code.co_name)) + exc_tb = exc_tb.tb_next + print>>self.__stderr, ("%s: %s" % (exc_type, exc_value)) + # Make sure that exc_tb gets deleted since it is a memory + # hog; deleting everything else is just for thoroughness + finally: + del exc_type, exc_value, exc_tb + else: + if __debug__: + self._note("%s.__bootstrap(): normal return", self) + finally: + # Prevent a race in + # test_threading.test_no_refcycle_through_target when + # the exception keeps the target alive past when we + # assert that it's dead. + self.__exc_clear() + finally: + with _active_limbo_lock: + self.__stop() + try: + # We don't call self.__delete() because it also + # grabs _active_limbo_lock. + del _active[_get_ident()] + except: + pass + + def __stop(self): + # DummyThreads delete self.__block, but they have no waiters to + # notify anyway (join() is forbidden on them). + if not hasattr(self, '_Thread__block'): + return + self.__block.acquire() + self.__stopped = True + self.__block.notify_all() + self.__block.release() + + def __delete(self): + "Remove current thread from the dict of currently running threads." + + # Notes about running with dummy_thread: + # + # Must take care to not raise an exception if dummy_thread is being + # used (and thus this module is being used as an instance of + # dummy_threading). dummy_thread.get_ident() always returns -1 since + # there is only one thread if dummy_thread is being used. Thus + # len(_active) is always <= 1 here, and any Thread instance created + # overwrites the (if any) thread currently registered in _active. + # + # An instance of _MainThread is always created by 'threading'. This + # gets overwritten the instant an instance of Thread is created; both + # threads return -1 from dummy_thread.get_ident() and thus have the + # same key in the dict. So when the _MainThread instance created by + # 'threading' tries to clean itself up when atexit calls this method + # it gets a KeyError if another Thread instance was created. + # + # This all means that KeyError from trying to delete something from + # _active if dummy_threading is being used is a red herring. But + # since it isn't if dummy_threading is *not* being used then don't + # hide the exception. + + try: + with _active_limbo_lock: + del _active[_get_ident()] + # There must not be any python code between the previous line + # and after the lock is released. Otherwise a tracing function + # could try to acquire the lock again in the same thread, (in + # current_thread()), and would block. + except KeyError: + if 'dummy_threading' not in _sys.modules: + raise + + def join(self, timeout=None): + """Wait until the thread terminates. + + This blocks the calling thread until the thread whose join() method is + called terminates -- either normally or through an unhandled exception + or until the optional timeout occurs. + + When the timeout argument is present and not None, it should be a + floating point number specifying a timeout for the operation in seconds + (or fractions thereof). As join() always returns None, you must call + isAlive() after join() to decide whether a timeout happened -- if the + thread is still alive, the join() call timed out. + + When the timeout argument is not present or None, the operation will + block until the thread terminates. + + A thread can be join()ed many times. + + join() raises a RuntimeError if an attempt is made to join the current + thread as that would cause a deadlock. It is also an error to join() a + thread before it has been started and attempts to do so raises the same + exception. + + """ + if not self.__initialized: + raise RuntimeError("Thread.__init__() not called") + if not self.__started.is_set(): + raise RuntimeError("cannot join thread before it is started") + if self is current_thread(): + raise RuntimeError("cannot join current thread") + + if __debug__: + if not self.__stopped: + self._note("%s.join(): waiting until thread stops", self) + self.__block.acquire() + try: + if timeout is None: + while not self.__stopped: + self.__block.wait() + if __debug__: + self._note("%s.join(): thread stopped", self) + else: + deadline = _time() + timeout + while not self.__stopped: + delay = deadline - _time() + if delay <= 0: + if __debug__: + self._note("%s.join(): timed out", self) + break + self.__block.wait(delay) + else: + if __debug__: + self._note("%s.join(): thread stopped", self) + finally: + self.__block.release() + + @property + def name(self): + """A string used for identification purposes only. + + It has no semantics. Multiple threads may be given the same name. The + initial name is set by the constructor. + + """ + assert self.__initialized, "Thread.__init__() not called" + return self.__name + + @name.setter + def name(self, name): + assert self.__initialized, "Thread.__init__() not called" + self.__name = str(name) + + @property + def ident(self): + """Thread identifier of this thread or None if it has not been started. + + This is a nonzero integer. See the thread.get_ident() function. Thread + identifiers may be recycled when a thread exits and another thread is + created. The identifier is available even after the thread has exited. + + """ + assert self.__initialized, "Thread.__init__() not called" + return self.__ident + + def isAlive(self): + """Return whether the thread is alive. + + This method returns True just before the run() method starts until just + after the run() method terminates. The module function enumerate() + returns a list of all alive threads. + + """ + assert self.__initialized, "Thread.__init__() not called" + return self.__started.is_set() and not self.__stopped + + is_alive = isAlive + + @property + def daemon(self): + """A boolean value indicating whether this thread is a daemon thread (True) or not (False). + + This must be set before start() is called, otherwise RuntimeError is + raised. Its initial value is inherited from the creating thread; the + main thread is not a daemon thread and therefore all threads created in + the main thread default to daemon = False. + + The entire Python program exits when no alive non-daemon threads are + left. + + """ + assert self.__initialized, "Thread.__init__() not called" + return self.__daemonic + + @daemon.setter + def daemon(self, daemonic): + if not self.__initialized: + raise RuntimeError("Thread.__init__() not called") + if self.__started.is_set(): + raise RuntimeError("cannot set daemon status of active thread"); + self.__daemonic = daemonic + + def isDaemon(self): + return self.daemon + + def setDaemon(self, daemonic): + self.daemon = daemonic + + def getName(self): + return self.name + + def setName(self, name): + self.name = name + +# The timer class was contributed by Itamar Shtull-Trauring + +def Timer(*args, **kwargs): + """Factory function to create a Timer object. + + Timers call a function after a specified number of seconds: + + t = Timer(30.0, f, args=[], kwargs={}) + t.start() + t.cancel() # stop the timer's action if it's still waiting + + """ + return _Timer(*args, **kwargs) + +class _Timer(Thread): + """Call a function after a specified number of seconds: + + t = Timer(30.0, f, args=[], kwargs={}) + t.start() + t.cancel() # stop the timer's action if it's still waiting + + """ + + def __init__(self, interval, function, args=[], kwargs={}): + Thread.__init__(self) + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + self.finished = Event() + + def cancel(self): + """Stop the timer if it hasn't finished yet""" + self.finished.set() + + def run(self): + self.finished.wait(self.interval) + if not self.finished.is_set(): + self.function(*self.args, **self.kwargs) + self.finished.set() + +# Special thread class to represent the main thread +# This is garbage collected through an exit handler + +class _MainThread(Thread): + + def __init__(self): + Thread.__init__(self, name="MainThread") + self._Thread__started.set() + self._set_ident() + with _active_limbo_lock: + _active[_get_ident()] = self + + def _set_daemon(self): + return False + + def _exitfunc(self): + self._Thread__stop() + t = _pickSomeNonDaemonThread() + if t: + if __debug__: + self._note("%s: waiting for other threads", self) + while t: + t.join() + t = _pickSomeNonDaemonThread() + if __debug__: + self._note("%s: exiting", self) + self._Thread__delete() + +def _pickSomeNonDaemonThread(): + for t in enumerate(): + if not t.daemon and t.is_alive(): + return t + return None + + +# Dummy thread class to represent threads not started here. +# These aren't garbage collected when they die, nor can they be waited for. +# If they invoke anything in threading.py that calls current_thread(), they +# leave an entry in the _active dict forever after. +# Their purpose is to return *something* from current_thread(). +# They are marked as daemon threads so we won't wait for them +# when we exit (conform previous semantics). + +class _DummyThread(Thread): + + def __init__(self): + Thread.__init__(self, name=_newname("Dummy-%d")) + + # Thread.__block consumes an OS-level locking primitive, which + # can never be used by a _DummyThread. Since a _DummyThread + # instance is immortal, that's bad, so release this resource. + del self._Thread__block + + self._Thread__started.set() + self._set_ident() + with _active_limbo_lock: + _active[_get_ident()] = self + + def _set_daemon(self): + return True + + def join(self, timeout=None): + assert False, "cannot join a dummy thread" + + +# Global API functions + +def currentThread(): + """Return the current Thread object, corresponding to the caller's thread of control. + + If the caller's thread of control was not created through the threading + module, a dummy thread object with limited functionality is returned. + + """ + try: + return _active[_get_ident()] + except KeyError: + ##print "current_thread(): no current thread for", _get_ident() + return _DummyThread() + +current_thread = currentThread + +def activeCount(): + """Return the number of Thread objects currently alive. + + The returned count is equal to the length of the list returned by + enumerate(). + + """ + with _active_limbo_lock: + return len(_active) + len(_limbo) + +active_count = activeCount + +def _enumerate(): + # Same as enumerate(), but without the lock. Internal use only. + return _active.values() + _limbo.values() + +def enumerate(): + """Return a list of all Thread objects currently alive. + + The list includes daemonic threads, dummy thread objects created by + current_thread(), and the main thread. It excludes terminated threads and + threads that have not yet been started. + + """ + with _active_limbo_lock: + return _active.values() + _limbo.values() + +from thread import stack_size + +# Create the main thread object, +# and make it available for the interpreter +# (Py_Main) as threading._shutdown. + +_shutdown = _MainThread()._exitfunc + +# get thread-local implementation, either from the thread +# module, or from the python fallback + +try: + from thread import _local as local +except ImportError: + from _threading_local import local + + +def _after_fork(): + # This function is called by Python/ceval.c:PyEval_ReInitThreads which + # is called from PyOS_AfterFork. Here we cleanup threading module state + # that should not exist after a fork. + + # Reset _active_limbo_lock, in case we forked while the lock was held + # by another (non-forked) thread. http://bugs.python.org/issue874900 + global _active_limbo_lock + _active_limbo_lock = _allocate_lock() + + # fork() only copied the current thread; clear references to others. + new_active = {} + current = current_thread() + with _active_limbo_lock: + for thread in _enumerate(): + # Any lock/condition variable may be currently locked or in an + # invalid state, so we reinitialize them. + if hasattr(thread, '_reset_internal_locks'): + thread._reset_internal_locks() + if thread is current: + # There is only one active thread. We reset the ident to + # its new value since it can have changed. + ident = _get_ident() + thread._Thread__ident = ident + new_active[ident] = thread + else: + # All the others are already stopped. + thread._Thread__stop() + + _limbo.clear() + _active.clear() + _active.update(new_active) + assert len(_active) == 1 + + +# Self-test code + +def _test(): + + class BoundedQueue(_Verbose): + + def __init__(self, limit): + _Verbose.__init__(self) + self.mon = RLock() + self.rc = Condition(self.mon) + self.wc = Condition(self.mon) + self.limit = limit + self.queue = _deque() + + def put(self, item): + self.mon.acquire() + while len(self.queue) >= self.limit: + self._note("put(%s): queue full", item) + self.wc.wait() + self.queue.append(item) + self._note("put(%s): appended, length now %d", + item, len(self.queue)) + self.rc.notify() + self.mon.release() + + def get(self): + self.mon.acquire() + while not self.queue: + self._note("get(): queue empty") + self.rc.wait() + item = self.queue.popleft() + self._note("get(): got %s, %d left", item, len(self.queue)) + self.wc.notify() + self.mon.release() + return item + + class ProducerThread(Thread): + + def __init__(self, queue, quota): + Thread.__init__(self, name="Producer") + self.queue = queue + self.quota = quota + + def run(self): + from random import random + counter = 0 + while counter < self.quota: + counter = counter + 1 + self.queue.put("%s.%d" % (self.name, counter)) + _sleep(random() * 0.00001) + + + class ConsumerThread(Thread): + + def __init__(self, queue, count): + Thread.__init__(self, name="Consumer") + self.queue = queue + self.count = count + + def run(self): + while self.count > 0: + item = self.queue.get() + print item + self.count = self.count - 1 + + NP = 3 + QL = 4 + NI = 5 + + Q = BoundedQueue(QL) + P = [] + for i in range(NP): + t = ProducerThread(Q, NI) + t.name = ("Producer-%d" % (i+1)) + P.append(t) + C = ConsumerThread(Q, NI*NP) + for t in P: + t.start() + _sleep(0.000001) + C.start() + for t in P: + t.join() + C.join() + + +class Random(_random.Random): + """Random number generator base class used by bound module functions. + + Used to instantiate instances of Random to get generators that don't + share state. + + Class Random can also be subclassed if you want to use a different basic + generator of your own devising: in that case, override the following + methods: random(), seed(), getstate(), and setstate(). + Optionally, implement a getrandbits() method so that randrange() + can cover arbitrarily large ranges. + + """ + + VERSION = 3 # used by getstate/setstate + + def __init__(self, x=None): + """Initialize an instance. + + Optional argument x controls seeding, as for Random.seed(). + """ + + self.seed(x) + self.gauss_next = None + + def seed(self, a=None, version=2): + """Initialize internal state from hashable object. + + None or no argument seeds from current time or from an operating + system specific randomness source if available. + + For version 2 (the default), all of the bits are used if *a* is a str, + bytes, or bytearray. For version 1, the hash() of *a* is used instead. + + If *a* is an int, all bits are used. + + """ + + if a is None: + try: + # Seed with enough bytes to span the 19937 bit + # state space for the Mersenne Twister + a = int.from_bytes(_urandom(2500), 'big') + except NotImplementedError: + import time + a = int(time.time() * 256) # use fractional seconds + + if version == 2: + if isinstance(a, (str, bytes, bytearray)): + if isinstance(a, str): + a = a.encode() + a += _sha512(a).digest() + a = int.from_bytes(a, 'big') + + super().seed(a) + self.gauss_next = None + + def getstate(self): + """Return internal state; can be passed to setstate() later.""" + return self.VERSION, super().getstate(), self.gauss_next + + def setstate(self, state): + """Restore internal state from object returned by getstate().""" + version = state[0] + if version == 3: + version, internalstate, self.gauss_next = state + super().setstate(internalstate) + elif version == 2: + version, internalstate, self.gauss_next = state + # In version 2, the state was saved as signed ints, which causes + # inconsistencies between 32/64-bit systems. The state is + # really unsigned 32-bit ints, so we convert negative ints from + # version 2 to positive longs for version 3. + try: + internalstate = tuple(x % (2**32) for x in internalstate) + except ValueError as e: + raise TypeError from e + super().setstate(internalstate) + else: + raise ValueError("state with version %s passed to " + "Random.setstate() of version %s" % + (version, self.VERSION)) + +## ---- Methods below this point do not need to be overridden when +## ---- subclassing for the purpose of using a different core generator. + +## -------------------- pickle support ------------------- + + # Issue 17489: Since __reduce__ was defined to fix #759889 this is no + # longer called; we leave it here because it has been here since random was + # rewritten back in 2001 and why risk breaking something. + def __getstate__(self): # for pickle + return self.getstate() + + def __setstate__(self, state): # for pickle + self.setstate(state) + + def __reduce__(self): + return self.__class__, (), self.getstate() + +## -------------------- integer methods ------------------- + + def randrange(self, start, stop=None, step=1, _int=int): + """Choose a random item from range(start, stop[, step]). + + This fixes the problem with randint() which includes the + endpoint; in Python this is usually not what you want. + + """ + + # This code is a bit messy to make it fast for the + # common case while still doing adequate error checking. + istart = _int(start) + if istart != start: + raise ValueError("non-integer arg 1 for randrange()") + if stop is None: + if istart > 0: + return self._randbelow(istart) + raise ValueError("empty range for randrange()") + + # stop argument supplied. + istop = _int(stop) + if istop != stop: + raise ValueError("non-integer stop for randrange()") + width = istop - istart + if step == 1 and width > 0: + return istart + self._randbelow(width) + if step == 1: + raise ValueError("empty range for randrange() (%d,%d, %d)" % (istart, istop, width)) + + # Non-unit step argument supplied. + istep = _int(step) + if istep != step: + raise ValueError("non-integer step for randrange()") + if istep > 0: + n = (width + istep - 1) // istep + elif istep < 0: + n = (width + istep + 1) // istep + else: + raise ValueError("zero step for randrange()") + + if n <= 0: + raise ValueError("empty range for randrange()") + + return istart + istep*self._randbelow(n) + + def randint(self, a, b): + """Return random integer in range [a, b], including both end points. + """ + + return self.randrange(a, b+1) + + def _randbelow(self, n, int=int, maxsize=1<<BPF, type=type, + Method=_MethodType, BuiltinMethod=_BuiltinMethodType): + "Return a random int in the range [0,n). Raises ValueError if n==0." + + random = self.random + getrandbits = self.getrandbits + # Only call self.getrandbits if the original random() builtin method + # has not been overridden or if a new getrandbits() was supplied. + if type(random) is BuiltinMethod or type(getrandbits) is Method: + k = n.bit_length() # don't use (n-1) here because n can be 1 + r = getrandbits(k) # 0 <= r < 2**k + while r >= n: + r = getrandbits(k) + return r + # There's an overridden random() method but no new getrandbits() method, + # so we can only use random() from here. + if n >= maxsize: + _warn("Underlying random() generator does not supply \n" + "enough bits to choose from a population range this large.\n" + "To remove the range limitation, add a getrandbits() method.") + return int(random() * n) + rem = maxsize % n + limit = (maxsize - rem) / maxsize # int(limit * maxsize) % n == 0 + r = random() + while r >= limit: + r = random() + return int(r*maxsize) % n + +## -------------------- sequence methods ------------------- + + def choice(self, seq): + """Choose a random element from a non-empty sequence.""" + try: + i = self._randbelow(len(seq)) + except ValueError: + raise IndexError('Cannot choose from an empty sequence') + return seq[i] + + def shuffle(self, x, random=None): + """Shuffle list x in place, and return None. + + Optional argument random is a 0-argument function returning a + random float in [0.0, 1.0); if it is the default None, the + standard random.random will be used. + + """ + + if random is None: + randbelow = self._randbelow + for i in reversed(range(1, len(x))): + # pick an element in x[:i+1] with which to exchange x[i] + j = randbelow(i+1) + x[i], x[j] = x[j], x[i] + else: + _int = int + for i in reversed(range(1, len(x))): + # pick an element in x[:i+1] with which to exchange x[i] + j = _int(random() * (i+1)) + x[i], x[j] = x[j], x[i] + + def sample(self, population, k): + """Chooses k unique random elements from a population sequence or set. + + Returns a new list containing elements from the population while + leaving the original population unchanged. The resulting list is + in selection order so that all sub-slices will also be valid random + samples. This allows raffle winners (the sample) to be partitioned + into grand prize and second place winners (the subslices). + + Members of the population need not be hashable or unique. If the + population contains repeats, then each occurrence is a possible + selection in the sample. + + To choose a sample in a range of integers, use range as an argument. + This is especially fast and space efficient for sampling from a + large population: sample(range(10000000), 60) + """ + + # Sampling without replacement entails tracking either potential + # selections (the pool) in a list or previous selections in a set. + + # When the number of selections is small compared to the + # population, then tracking selections is efficient, requiring + # only a small set and an occasional reselection. For + # a larger number of selections, the pool tracking method is + # preferred since the list takes less space than the + # set and it doesn't suffer from frequent reselections. + + if isinstance(population, _Set): + population = tuple(population) + if not isinstance(population, _Sequence): + raise TypeError("Population must be a sequence or set. For dicts, use list(d).") + randbelow = self._randbelow + n = len(population) + if not 0 <= k <= n: + raise ValueError("Sample larger than population") + result = [None] * k + setsize = 21 # size of a small set minus size of an empty list + if k > 5: + setsize += 4 ** _ceil(_log(k * 3, 4)) # table size for big sets + if n <= setsize: + # An n-length list is smaller than a k-length set + pool = list(population) + for i in range(k): # invariant: non-selected at [0,n-i) + j = randbelow(n-i) + result[i] = pool[j] + pool[j] = pool[n-i-1] # move non-selected item into vacancy + else: + selected = set() + selected_add = selected.add + for i in range(k): + j = randbelow(n) + while j in selected: + j = randbelow(n) + selected_add(j) + result[i] = population[j] + return result + +## -------------------- real-valued distributions ------------------- + +## -------------------- uniform distribution ------------------- + + def uniform(self, a, b): + "Get a random number in the range [a, b) or [a, b] depending on rounding." + return a + (b-a) * self.random() + +## -------------------- triangular -------------------- + + def triangular(self, low=0.0, high=1.0, mode=None): + """Triangular distribution. + + Continuous distribution bounded by given lower and upper limits, + and having a given mode value in-between. + + http://en.wikipedia.org/wiki/Triangular_distribution + + """ + u = self.random() + try: + c = 0.5 if mode is None else (mode - low) / (high - low) + except ZeroDivisionError: + return low + if u > c: + u = 1.0 - u + c = 1.0 - c + low, high = high, low + return low + (high - low) * (u * c) ** 0.5 + +## -------------------- normal distribution -------------------- + + def normalvariate(self, mu, sigma): + """Normal distribution. + + mu is the mean, and sigma is the standard deviation. + + """ + # mu = mean, sigma = standard deviation + + # Uses Kinderman and Monahan method. Reference: Kinderman, + # A.J. and Monahan, J.F., "Computer generation of random + # variables using the ratio of uniform deviates", ACM Trans + # Math Software, 3, (1977), pp257-260. + + random = self.random + while 1: + u1 = random() + u2 = 1.0 - random() + z = NV_MAGICCONST*(u1-0.5)/u2 + zz = z*z/4.0 + if zz <= -_log(u2): + break + return mu + z*sigma + +## -------------------- lognormal distribution -------------------- + + def lognormvariate(self, mu, sigma): + """Log normal distribution. + + If you take the natural logarithm of this distribution, you'll get a + normal distribution with mean mu and standard deviation sigma. + mu can have any value, and sigma must be greater than zero. + + """ + return _exp(self.normalvariate(mu, sigma)) + +## -------------------- exponential distribution -------------------- + + def expovariate(self, lambd): + """Exponential distribution. + + lambd is 1.0 divided by the desired mean. It should be + nonzero. (The parameter would be called "lambda", but that is + a reserved word in Python.) Returned values range from 0 to + positive infinity if lambd is positive, and from negative + infinity to 0 if lambd is negative. + + """ + # lambd: rate lambd = 1/mean + # ('lambda' is a Python reserved word) + + # we use 1-random() instead of random() to preclude the + # possibility of taking the log of zero. + return -_log(1.0 - self.random())/lambd + +## -------------------- von Mises distribution -------------------- + + def vonmisesvariate(self, mu, kappa): + """Circular data distribution. + + mu is the mean angle, expressed in radians between 0 and 2*pi, and + kappa is the concentration parameter, which must be greater than or + equal to zero. If kappa is equal to zero, this distribution reduces + to a uniform random angle over the range 0 to 2*pi. + + """ + # mu: mean angle (in radians between 0 and 2*pi) + # kappa: concentration parameter kappa (>= 0) + # if kappa = 0 generate uniform random angle + + # Based upon an algorithm published in: Fisher, N.I., + # "Statistical Analysis of Circular Data", Cambridge + # University Press, 1993. + + # Thanks to Magnus Kessler for a correction to the + # implementation of step 4. + + random = self.random + if kappa <= 1e-6: + return TWOPI * random() + + s = 0.5 / kappa + r = s + _sqrt(1.0 + s * s) + + while 1: + u1 = random() + z = _cos(_pi * u1) + + d = z / (r + z) + u2 = random() + if u2 < 1.0 - d * d or u2 <= (1.0 - d) * _exp(d): + break + + q = 1.0 / r + f = (q + z) / (1.0 + q * z) + u3 = random() + if u3 > 0.5: + theta = (mu + _acos(f)) % TWOPI + else: + theta = (mu - _acos(f)) % TWOPI + + return theta + +## -------------------- gamma distribution -------------------- + + def gammavariate(self, alpha, beta): + """Gamma distribution. Not the gamma function! + + Conditions on the parameters are alpha > 0 and beta > 0. + + The probability distribution function is: + + x ** (alpha - 1) * math.exp(-x / beta) + pdf(x) = -------------------------------------- + math.gamma(alpha) * beta ** alpha + + """ + + # alpha > 0, beta > 0, mean is alpha*beta, variance is alpha*beta**2 + + # Warning: a few older sources define the gamma distribution in terms + # of alpha > -1.0 + if alpha <= 0.0 or beta <= 0.0: + raise ValueError('gammavariate: alpha and beta must be > 0.0') + + random = self.random + if alpha > 1.0: + + # Uses R.C.H. Cheng, "The generation of Gamma + # variables with non-integral shape parameters", + # Applied Statistics, (1977), 26, No. 1, p71-74 + + ainv = _sqrt(2.0 * alpha - 1.0) + bbb = alpha - LOG4 + ccc = alpha + ainv + + while 1: + u1 = random() + if not 1e-7 < u1 < .9999999: + continue + u2 = 1.0 - random() + v = _log(u1/(1.0-u1))/ainv + x = alpha*_exp(v) + z = u1*u1*u2 + r = bbb+ccc*v-x + if r + SG_MAGICCONST - 4.5*z >= 0.0 or r >= _log(z): + return x * beta + + elif alpha == 1.0: + # expovariate(1) + u = random() + while u <= 1e-7: + u = random() + return -_log(u) * beta + + else: # alpha is between 0 and 1 (exclusive) + + # Uses ALGORITHM GS of Statistical Computing - Kennedy & Gentle + + while 1: + u = random() + b = (_e + alpha)/_e + p = b*u + if p <= 1.0: + x = p ** (1.0/alpha) + else: + x = -_log((b-p)/alpha) + u1 = random() + if p > 1.0: + if u1 <= x ** (alpha - 1.0): + break + elif u1 <= _exp(-x): + break + return x * beta + +## -------------------- Gauss (faster alternative) -------------------- + + def gauss(self, mu, sigma): + """Gaussian distribution. + + mu is the mean, and sigma is the standard deviation. This is + slightly faster than the normalvariate() function. + + Not thread-safe without a lock around calls. + + """ + + # When x and y are two variables from [0, 1), uniformly + # distributed, then + # + # cos(2*pi*x)*sqrt(-2*log(1-y)) + # sin(2*pi*x)*sqrt(-2*log(1-y)) + # + # are two *independent* variables with normal distribution + # (mu = 0, sigma = 1). + # (Lambert Meertens) + # (corrected version; bug discovered by Mike Miller, fixed by LM) + + # Multithreading note: When two threads call this function + # simultaneously, it is possible that they will receive the + # same return value. The window is very small though. To + # avoid this, you have to use a lock around all calls. (I + # didn't want to slow this down in the serial case by using a + # lock here.) + + random = self.random + z = self.gauss_next + self.gauss_next = None + if z is None: + x2pi = random() * TWOPI + g2rad = _sqrt(-2.0 * _log(1.0 - random())) + z = _cos(x2pi) * g2rad + self.gauss_next = _sin(x2pi) * g2rad + + return mu + z*sigma + +## -------------------- beta -------------------- +## See +## http://mail.python.org/pipermail/python-bugs-list/2001-January/003752.html +## for Ivan Frohne's insightful analysis of why the original implementation: +## +## def betavariate(self, alpha, beta): +## # Discrete Event Simulation in C, pp 87-88. +## +## y = self.expovariate(alpha) +## z = self.expovariate(1.0/beta) +## return z/(y+z) +## +## was dead wrong, and how it probably got that way. + + def betavariate(self, alpha, beta): + """Beta distribution. + + Conditions on the parameters are alpha > 0 and beta > 0. + Returned values range between 0 and 1. + + """ + + # This version due to Janne Sinkkonen, and matches all the std + # texts (e.g., Knuth Vol 2 Ed 3 pg 134 "the beta distribution"). + y = self.gammavariate(alpha, 1.) + if y == 0: + return 0.0 + else: + return y / (y + self.gammavariate(beta, 1.)) + +## -------------------- Pareto -------------------- + + def paretovariate(self, alpha): + """Pareto distribution. alpha is the shape parameter.""" + # Jain, pg. 495 + + u = 1.0 - self.random() + return 1.0 / u ** (1.0/alpha) + +## -------------------- Weibull -------------------- + + def weibullvariate(self, alpha, beta): + """Weibull distribution. + + alpha is the scale parameter and beta is the shape parameter. + + """ + # Jain, pg. 499; bug fix courtesy Bill Arms + + u = 1.0 - self.random() + return alpha * (-_log(u)) ** (1.0/beta) + +## --------------- Operating System Random Source ------------------ + + +if __name__ == '__main__': + _test() + diff --git a/src/test/pythonFiles/autocomp/one.py b/src/test/pythonFiles/autocomp/one.py new file mode 100644 index 000000000000..5e5708fd92f0 --- /dev/null +++ b/src/test/pythonFiles/autocomp/one.py @@ -0,0 +1,31 @@ + +import sys + +print(sys.api_version) + +class Class1(object): + """Some class + And the second line + """ + + description = "Run isort on modules registered in setuptools" + user_options = [] + + def __init__(self, file_path=None, file_contents=None): + self.prop1 = '' + self.prop2 = 1 + + def method1(self): + """ + This is method1 + """ + pass + + def method2(self): + """ + This is method2 + """ + pass + +obj = Class1() +obj.method1() \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/pep484.py b/src/test/pythonFiles/autocomp/pep484.py new file mode 100644 index 000000000000..79edec69ae1a --- /dev/null +++ b/src/test/pythonFiles/autocomp/pep484.py @@ -0,0 +1,12 @@ + +def greeting(name: str) -> str: + return 'Hello ' + name.upper() + + +def add(num1, num2) -> int: + return num1 + num2 + +add().bit_length() + + + diff --git a/src/test/pythonFiles/autocomp/pep526.py b/src/test/pythonFiles/autocomp/pep526.py new file mode 100644 index 000000000000..d8cd0300ed0d --- /dev/null +++ b/src/test/pythonFiles/autocomp/pep526.py @@ -0,0 +1,22 @@ + + +PEP_526_style: str = "hello world" +captain: str # Note: no initial value! +PEP_484_style = SOMETHING # type: str + + +PEP_484_style.upper() +PEP_526_style.upper() +captain.upper() + +# https://github.com/DonJayamanne/pythonVSCode/issues/918 +class A: + a = 0 + + +class B: + b: int = 0 + + +A().a # -> Autocomplete works +B().b.bit_length() # -> Autocomplete doesn't work \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/suppress.py b/src/test/pythonFiles/autocomp/suppress.py new file mode 100644 index 000000000000..9f74959ef14b --- /dev/null +++ b/src/test/pythonFiles/autocomp/suppress.py @@ -0,0 +1,6 @@ +"string" #comment +""" +content +""" +#comment +'un#closed diff --git a/src/test/pythonFiles/autocomp/three.py b/src/test/pythonFiles/autocomp/three.py new file mode 100644 index 000000000000..35ad7f399172 --- /dev/null +++ b/src/test/pythonFiles/autocomp/three.py @@ -0,0 +1,2 @@ +import two +two.ct().fun() \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/two.py b/src/test/pythonFiles/autocomp/two.py new file mode 100644 index 000000000000..99a6e3c4bdf1 --- /dev/null +++ b/src/test/pythonFiles/autocomp/two.py @@ -0,0 +1,6 @@ +class ct: + def fun(): + """ + This is fun + """ + pass \ No newline at end of file diff --git a/src/test/pythonFiles/autoimport/one.py b/src/test/pythonFiles/autoimport/one.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/autoimport/two/__init__.py b/src/test/pythonFiles/autoimport/two/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/autoimport/two/three.py b/src/test/pythonFiles/autoimport/two/three.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/datascience/simple_nb.ipynb b/src/test/pythonFiles/datascience/simple_nb.ipynb new file mode 100644 index 000000000000..bebbed6c7cd4 --- /dev/null +++ b/src/test/pythonFiles/datascience/simple_nb.ipynb @@ -0,0 +1,41 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "with open('ds_n.log', 'a') as fp:\n", + " fp.write('Hello World')\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "nbformat": 4, + "nbformat_minor": 2, + "metadata": { + "language_info": { + "name": "python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "version": "3.7.4" + }, + "orig_nbformat": 2, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + } +} \ No newline at end of file diff --git a/src/test/pythonFiles/datascience/simple_note_book.py b/src/test/pythonFiles/datascience/simple_note_book.py new file mode 100644 index 000000000000..ace41e3f5c44 --- /dev/null +++ b/src/test/pythonFiles/datascience/simple_note_book.py @@ -0,0 +1,7 @@ +# %% +import os.path +dir_path = os.path.dirname(os.path.realpath(__file__)) + +with open(os.path.join(dir_path, 'ds.log'), 'a') as fp: + fp.write('Hello World') + diff --git a/src/test/pythonFiles/debugging/forever.py b/src/test/pythonFiles/debugging/forever.py new file mode 100644 index 000000000000..5dba1eb33df8 --- /dev/null +++ b/src/test/pythonFiles/debugging/forever.py @@ -0,0 +1,5 @@ +import time + +for i in range(1000): + time.sleep(1) + print(i) diff --git a/src/test/pythonFiles/debugging/logMessage.py b/src/test/pythonFiles/debugging/logMessage.py new file mode 100644 index 000000000000..ec04cd68c76e --- /dev/null +++ b/src/test/pythonFiles/debugging/logMessage.py @@ -0,0 +1,4 @@ +import time +a = 1 +b = 2 +c = a + b diff --git a/src/test/pythonFiles/debugging/loopyTest.py b/src/test/pythonFiles/debugging/loopyTest.py new file mode 100644 index 000000000000..03c95d371918 --- /dev/null +++ b/src/test/pythonFiles/debugging/loopyTest.py @@ -0,0 +1,2 @@ +for i in range(10): + print(i) diff --git a/src/test/pythonFiles/debugging/multiThread.py b/src/test/pythonFiles/debugging/multiThread.py new file mode 100644 index 000000000000..588971ffb502 --- /dev/null +++ b/src/test/pythonFiles/debugging/multiThread.py @@ -0,0 +1,14 @@ +import sys +import threading +import time + +def bar(): + time.sleep(2) + print('bar') + +def foo(x): + while True: + bar() + +threading.Thread(target = lambda: foo(2), name="foo").start() +foo(1) diff --git a/src/test/pythonFiles/debugging/printSysArgv.py b/src/test/pythonFiles/debugging/printSysArgv.py new file mode 100644 index 000000000000..d14add5dd7f9 --- /dev/null +++ b/src/test/pythonFiles/debugging/printSysArgv.py @@ -0,0 +1,4 @@ +import sys +import time +sys.stdout.write(','.join(sys.argv[1:])) +sys.stdout.flush() diff --git a/src/test/pythonFiles/debugging/sample2.py b/src/test/pythonFiles/debugging/sample2.py new file mode 100644 index 000000000000..99a40ac1eb21 --- /dev/null +++ b/src/test/pythonFiles/debugging/sample2.py @@ -0,0 +1,13 @@ +import time +time.sleep(3) +a = 1 +b = 2 +print(a + b) + +def do_something(name): + print("inside") + print(name) + +do_something("Do that") + +print("hello world") diff --git a/src/test/pythonFiles/debugging/sample2WithoutSleep.py b/src/test/pythonFiles/debugging/sample2WithoutSleep.py new file mode 100644 index 000000000000..88b11216bc70 --- /dev/null +++ b/src/test/pythonFiles/debugging/sample2WithoutSleep.py @@ -0,0 +1,13 @@ +import time +# time.sleep(3) +a = 1 +b = 2 +print(a + b) + +def do_something(name): + print("inside") + print(name) + +do_something("Do that") + +print("hello world") diff --git a/src/test/pythonFiles/debugging/sample3WithEx.py b/src/test/pythonFiles/debugging/sample3WithEx.py new file mode 100644 index 000000000000..5d6a54a419a1 --- /dev/null +++ b/src/test/pythonFiles/debugging/sample3WithEx.py @@ -0,0 +1,9 @@ +print("hello") + +def this_will_throw_an_error(): + print("inside") + print(1/0) + +this_will_throw_an_error() + +print("bye") diff --git a/src/test/pythonFiles/debugging/sampleWithAssertEx.py b/src/test/pythonFiles/debugging/sampleWithAssertEx.py new file mode 100644 index 000000000000..2cfffa40db4b --- /dev/null +++ b/src/test/pythonFiles/debugging/sampleWithAssertEx.py @@ -0,0 +1 @@ +assert False diff --git a/src/test/pythonFiles/debugging/sampleWithSleep.py b/src/test/pythonFiles/debugging/sampleWithSleep.py new file mode 100644 index 000000000000..7a84f4f0da0c --- /dev/null +++ b/src/test/pythonFiles/debugging/sampleWithSleep.py @@ -0,0 +1,8 @@ +import time +import os +print(os.getpid()) +time.sleep(1) +for i in 10000: + time.sleep(0.1) + print(i) +print('end') diff --git a/src/test/pythonFiles/debugging/simplePrint.py b/src/test/pythonFiles/debugging/simplePrint.py new file mode 100644 index 000000000000..8cde7829c178 --- /dev/null +++ b/src/test/pythonFiles/debugging/simplePrint.py @@ -0,0 +1 @@ +print("hello world") diff --git a/src/test/pythonFiles/debugging/stackFrame.py b/src/test/pythonFiles/debugging/stackFrame.py new file mode 100644 index 000000000000..18755e7a856d --- /dev/null +++ b/src/test/pythonFiles/debugging/stackFrame.py @@ -0,0 +1,10 @@ +import time + +def foo(): + time.sleep(3) + print(1) + +def bar(): + foo() + +bar() diff --git a/src/test/pythonFiles/debugging/startAndWait.py b/src/test/pythonFiles/debugging/startAndWait.py new file mode 100644 index 000000000000..c9f1a913e98d --- /dev/null +++ b/src/test/pythonFiles/debugging/startAndWait.py @@ -0,0 +1,2 @@ +import time +time.sleep(10) diff --git a/src/test/pythonFiles/debugging/stdErrOutput.py b/src/test/pythonFiles/debugging/stdErrOutput.py new file mode 100644 index 000000000000..ef576d80d8a8 --- /dev/null +++ b/src/test/pythonFiles/debugging/stdErrOutput.py @@ -0,0 +1,4 @@ +import sys +import time +sys.stderr.write('error output') +sys.stderr.flush() diff --git a/src/test/pythonFiles/debugging/stdOutOutput.py b/src/test/pythonFiles/debugging/stdOutOutput.py new file mode 100644 index 000000000000..e750f3c1fcbe --- /dev/null +++ b/src/test/pythonFiles/debugging/stdOutOutput.py @@ -0,0 +1,4 @@ +import sys +import time +sys.stdout.write('normal output') +sys.stdout.flush() diff --git a/src/test/pythonFiles/debugging/wait_for_file.py b/src/test/pythonFiles/debugging/wait_for_file.py new file mode 100644 index 000000000000..72dc90bda61e --- /dev/null +++ b/src/test/pythonFiles/debugging/wait_for_file.py @@ -0,0 +1,35 @@ +import os.path +import sys +import time + + +try: + _, filename = sys.argv +except ValueError: + _, filename, outfile = sys.argv + sys.stdout = open(outfile, 'w') +print('waiting for file {!r}'.format(filename)) + +# We use sys.stdout.write() instead of print() because Python 2... + +if not os.path.exists(filename): + time.sleep(0.1) + sys.stdout.write('.') + sys.stdout.flush() +i = 1 +while not os.path.exists(filename): + time.sleep(0.1) + if i % 10 == 0: + sys.stdout.write(' ') + if i % 600 == 0: + if i == 600: + sys.stdout.write('\n = 1 minute =\n') + else: + sys.stdout.write('\n = {} minutes =\n'.format(i // 600)) + elif i % 100 == 0: + sys.stdout.write('\n') + sys.stdout.write('.') + sys.stdout.flush() + i += 1 +print('\nfound file {!r}'.format(filename)) +print('done!') diff --git a/src/test/pythonFiles/definition/await.test.py b/src/test/pythonFiles/definition/await.test.py new file mode 100644 index 000000000000..7b4acd876c27 --- /dev/null +++ b/src/test/pythonFiles/definition/await.test.py @@ -0,0 +1,19 @@ +# https://github.com/DonJayamanne/pythonVSCode/issues/962 + +class A: + def __init__(self): + self.test_value = 0 + + async def test(self): + pass + + async def test2(self): + await self.test() + +async def testthis(): + """ + Wow + """ + pass + +await testthis() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/five.py b/src/test/pythonFiles/definition/five.py new file mode 100644 index 000000000000..507c5fed967c --- /dev/null +++ b/src/test/pythonFiles/definition/five.py @@ -0,0 +1,2 @@ +import four +four.showMessage() diff --git a/src/test/pythonFiles/definition/four.py b/src/test/pythonFiles/definition/four.py new file mode 100644 index 000000000000..470338f71157 --- /dev/null +++ b/src/test/pythonFiles/definition/four.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# pylint: disable=E0401, W0512 + +import os + + +class Foo(object): + '''说明''' + + @staticmethod + def bar(): + """ + 说明 - keep this line, it works + delete following line, it works + 如果存在需要等待审批或正在执行的任务,将不刷新页面 + """ + return os.path.exists('c:/') + +def showMessage(): + """ + Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи. + Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку. + """ + print('1234') + +Foo.bar() +showMessage() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/navigation/__init__.py b/src/test/pythonFiles/definition/navigation/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/definition/navigation/definitions.py b/src/test/pythonFiles/definition/navigation/definitions.py new file mode 100644 index 000000000000..a8379a49f960 --- /dev/null +++ b/src/test/pythonFiles/definition/navigation/definitions.py @@ -0,0 +1,31 @@ +from contextlib import contextmanager + +def my_decorator(fn): + """ + This is my decorator. + """ + def wrapper(*args, **kwargs): + """ + This is the wrapper. + """ + return 42 + return wrapper + +@my_decorator +def thing(arg): + """ + Thing which is decorated. + """ + pass + +@contextmanager +def my_context_manager(): + """ + This is my context manager. + """ + print("before") + yield + print("after") + +with my_context_manager(): + thing(19) diff --git a/src/test/pythonFiles/definition/navigation/usages.py b/src/test/pythonFiles/definition/navigation/usages.py new file mode 100644 index 000000000000..deb6d78edc15 --- /dev/null +++ b/src/test/pythonFiles/definition/navigation/usages.py @@ -0,0 +1,16 @@ +import definitions +from .definitions import my_context_manager, my_decorator, thing + +@definitions.my_decorator +def one(): + pass + +@my_decorator +def two(): + pass + +with definitions.my_context_manager(): + definitions.thing(19) + +with my_context_manager(): + thing(19) diff --git a/src/test/pythonFiles/definition/one.py b/src/test/pythonFiles/definition/one.py new file mode 100644 index 000000000000..f1e3d75ffcbc --- /dev/null +++ b/src/test/pythonFiles/definition/one.py @@ -0,0 +1,46 @@ + +import sys + +print(sys.api_version) + +class Class1(object): + """Some class + And the second line + """ + + description = "Run isort on modules registered in setuptools" + user_options = [] + + def __init__(self, file_path=None, file_contents=None): + self.prop1 = '' + self.prop2 = 1 + + def method1(self): + """ + This is method1 + """ + pass + + def method2(self): + """ + This is method2 + """ + pass + +obj = Class1() +obj.method1() + +def function1(): + print("SOMETHING") + + +def function2(): + print("SOMETHING") + +def function3(): + print("SOMETHING") + +def function4(): + print("SOMETHING") + +function1() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/three.py b/src/test/pythonFiles/definition/three.py new file mode 100644 index 000000000000..35ad7f399172 --- /dev/null +++ b/src/test/pythonFiles/definition/three.py @@ -0,0 +1,2 @@ +import two +two.ct().fun() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/two.py b/src/test/pythonFiles/definition/two.py new file mode 100644 index 000000000000..99a6e3c4bdf1 --- /dev/null +++ b/src/test/pythonFiles/definition/two.py @@ -0,0 +1,6 @@ +class ct: + def fun(): + """ + This is fun + """ + pass \ No newline at end of file diff --git a/src/test/pythonFiles/docstrings/one.py b/src/test/pythonFiles/docstrings/one.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/dummy.py b/src/test/pythonFiles/dummy.py new file mode 100644 index 000000000000..10f13768abe0 --- /dev/null +++ b/src/test/pythonFiles/dummy.py @@ -0,0 +1 @@ +#dummy file to be opened by Test VS Code instance, so that Python Configuration (workspace configuration will be initialized) \ No newline at end of file diff --git a/src/test/pythonFiles/environments/conda/Scripts/conda.exe b/src/test/pythonFiles/environments/conda/Scripts/conda.exe new file mode 100644 index 000000000000..aaa0af66f262 --- /dev/null +++ b/src/test/pythonFiles/environments/conda/Scripts/conda.exe @@ -0,0 +1 @@ +// Test file \ No newline at end of file diff --git a/src/test/pythonFiles/environments/conda/bin/python b/src/test/pythonFiles/environments/conda/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/bin/python b/src/test/pythonFiles/environments/conda/envs/numpy/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/python.exe b/src/test/pythonFiles/environments/conda/envs/numpy/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/conda/envs/scipy/bin/python b/src/test/pythonFiles/environments/conda/envs/scipy/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/conda/envs/scipy/python.exe b/src/test/pythonFiles/environments/conda/envs/scipy/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path1/one b/src/test/pythonFiles/environments/path1/one new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path1/one.exe b/src/test/pythonFiles/environments/path1/one.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path1/python.exe b/src/test/pythonFiles/environments/path1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path2/one b/src/test/pythonFiles/environments/path2/one new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path2/one.exe b/src/test/pythonFiles/environments/path2/one.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/environments/path2/python.exe b/src/test/pythonFiles/environments/path2/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/exclusions/Lib/fileLib.py b/src/test/pythonFiles/exclusions/Lib/fileLib.py new file mode 100644 index 000000000000..50000adeda40 --- /dev/null +++ b/src/test/pythonFiles/exclusions/Lib/fileLib.py @@ -0,0 +1 @@ + a \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/Lib/site-packages/sitePackages.py b/src/test/pythonFiles/exclusions/Lib/site-packages/sitePackages.py new file mode 100644 index 000000000000..dad1af98c7f5 --- /dev/null +++ b/src/test/pythonFiles/exclusions/Lib/site-packages/sitePackages.py @@ -0,0 +1 @@ + b \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/dir1/dir1file.py b/src/test/pythonFiles/exclusions/dir1/dir1file.py new file mode 100644 index 000000000000..fe453b3fcc6a --- /dev/null +++ b/src/test/pythonFiles/exclusions/dir1/dir1file.py @@ -0,0 +1 @@ + for \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/dir1/dir2/dir2file.py b/src/test/pythonFiles/exclusions/dir1/dir2/dir2file.py new file mode 100644 index 000000000000..fe453b3fcc6a --- /dev/null +++ b/src/test/pythonFiles/exclusions/dir1/dir2/dir2file.py @@ -0,0 +1 @@ + for \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/one.py b/src/test/pythonFiles/exclusions/one.py new file mode 100644 index 000000000000..8c68a1c1fee2 --- /dev/null +++ b/src/test/pythonFiles/exclusions/one.py @@ -0,0 +1 @@ + if \ No newline at end of file diff --git a/src/test/pythonFiles/folding/attach_server.py b/src/test/pythonFiles/folding/attach_server.py new file mode 100644 index 000000000000..9c331d6c49e1 --- /dev/null +++ b/src/test/pythonFiles/folding/attach_server.py @@ -0,0 +1,337 @@ +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. + +__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" +__version__ = "3.0.0.0" + +__all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] + +import atexit +import getpass +import os +import os.path +import platform +import socket +import struct +import sys +import threading +try: + import thread +except ImportError: + import _thread as thread +try: + import ssl +except ImportError: + ssl = None + +import ptvsd.visualstudio_py_debugger as vspd +import ptvsd.visualstudio_py_repl as vspr +from ptvsd.visualstudio_py_util import to_bytes, read_bytes, read_int, read_string, write_bytes, write_int, write_string + + +# The server (i.e. the Python app) waits on a TCP port provided. Whenever anything connects to that port, +# it immediately sends the octet sequence 'PTVSDBG', followed by version number represented as int64, +# and then waits for the client to respond with the same exact byte sequence. After signatures are thereby +# exchanged and found to match, the client is expected to provide a string secret (in the usual debugger +# string format, None/ACII/Unicode prefix + length + data), which can be an empty string to designate the +# lack of a specified secret. +# +# If the secret does not match the one expected by the server, it responds with 'RJCT', and then closes +# the connection. Otherwise, the server responds with 'ACPT', and awaits a 4-octet command. The following +# commands are recognized: +# +# 'INFO' +# Report information about the process. The server responds with the following information, in order: +# - Process ID (int64) +# - Executable name (string) +# - User name (string) +# - Implementation name (string) +# and then immediately closes connection. Note, all string fields can be empty or null strings. +# +# 'ATCH' +# Attach debugger to the process. If successful, the server responds with 'ACPT', followed by process ID +# (int64), and then the Python language version that the server is running represented by three int64s - +# major, minor, micro; From there on the socket is assumed to be using the normal PTVS debugging protocol. +# If attaching was not successful (which can happen if some other debugger is already attached), the server +# responds with 'RJCT' and closes the connection. +# +# 'REPL' +# Attach REPL to the process. If successful, the server responds with 'ACPT', and from there on the socket +# is assumed to be using the normal PTVS REPL protocol. If not successful (which can happen if there is +# no debugger attached), the server responds with 'RJCT' and closes the connection. + +PTVS_VER = '2.2' +DEFAULT_PORT = 5678 +PTVSDBG_VER = 6 # must be kept in sync with DebuggerProtocolVersion in PythonRemoteProcess.cs +PTVSDBG = to_bytes('PTVSDBG') +ACPT = to_bytes('ACPT') +RJCT = to_bytes('RJCT') +INFO = to_bytes('INFO') +ATCH = to_bytes('ATCH') +REPL = to_bytes('REPL') + +PY_ROOT = os.path.normcase(__file__) +while os.path.basename(PY_ROOT) != 'pythonFiles': + PY_ROOT = os.path.dirname(PY_ROOT) + +_attach_enabled = False +_attached = threading.Event() + + +class AttachAlreadyEnabledError(Exception): + """`ptvsd.enable_attach` has already been called in this process.""" + + +def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, keyfile = None, redirect_output = True): + """Enables Python Tools for Visual Studio to attach to this process remotely + to debug Python code. + + Parameters + ---------- + secret : str + Used to validate the clients - only those clients providing the valid + secret will be allowed to connect to this server. On client side, the + secret is prepended to the Qualifier string, separated from the + hostname by ``'@'``, e.g.: ``'secret@myhost.cloudapp.net:5678'``. If + secret is ``None``, there's no validation, and any client can connect + freely. + address : (str, int), optional + Specifies the interface and port on which the debugging server should + listen for TCP connections. It is in the same format as used for + regular sockets of the `socket.AF_INET` family, i.e. a tuple of + ``(hostname, port)``. On client side, the server is identified by the + Qualifier string in the usual ``'hostname:port'`` format, e.g.: + ``'myhost.cloudapp.net:5678'``. Default is ``('0.0.0.0', 5678)``. + certfile : str, optional + Used to enable SSL. If not specified, or if set to ``None``, the + connection between this program and the debugger will be unsecure, + and can be intercepted on the wire. If specified, the meaning of this + parameter is the same as for `ssl.wrap_socket`. + keyfile : str, optional + Used together with `certfile` when SSL is enabled. Its meaning is the + same as for ``ssl.wrap_socket``. + redirect_output : bool, optional + Specifies whether any output (on both `stdout` and `stderr`) produced + by this program should be sent to the debugger. Default is ``True``. + + Notes + ----- + This function returns immediately after setting up the debugging server, + and does not block program execution. If you need to block until debugger + is attached, call `ptvsd.wait_for_attach`. The debugger can be detached + and re-attached multiple times after `enable_attach` is called. + + This function can only be called once during the lifetime of the process. + On a second call, `AttachAlreadyEnabledError` is raised. In circumstances + where the caller does not control how many times the function will be + called (e.g. when a script with a single call is run more than once by + a hosting app or framework), the call should be wrapped in ``try..except``. + + Only the thread on which this function is called, and any threads that are + created after it returns, will be visible in the debugger once it is + attached. Any threads that are already running before this function is + called will not be visible. + """ + + if not ssl and (certfile or keyfile): + raise ValueError('could not import the ssl module - SSL is not supported on this version of Python') + + if sys.platform == 'cli': + # Check that IronPython was launched with -X:Frames and -X:Tracing, since we can't register our trace + # func on the thread that calls enable_attach otherwise + import clr + x_tracing = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Tracing + x_frames = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Frames + if not x_tracing or not x_frames: + raise RuntimeError('IronPython must be started with -X:Tracing and -X:Frames options to support PTVS remote debugging.') + + global _attach_enabled + if _attach_enabled: + raise AttachAlreadyEnabledError('ptvsd.enable_attach() has already been called in this process.') + _attach_enabled = True + + atexit.register(vspd.detach_process_and_notify_debugger) + + server = socket.socket(proto=socket.IPPROTO_TCP) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(address) + server.listen(1) + def server_thread_func(): + while True: + client = None + raw_client = None + try: + client, addr = server.accept() + if certfile: + client = ssl.wrap_socket(client, server_side = True, ssl_version = ssl.PROTOCOL_TLSv1, certfile = certfile, keyfile = keyfile) + write_bytes(client, PTVSDBG) + write_int(client, PTVSDBG_VER) + + response = read_bytes(client, 7) + if response != PTVSDBG: + continue + dbg_ver = read_int(client) + if dbg_ver != PTVSDBG_VER: + continue + + client_secret = read_string(client) + if secret is None or secret == client_secret: + write_bytes(client, ACPT) + else: + write_bytes(client, RJCT) + continue + + response = read_bytes(client, 4) + + if response == INFO: + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + exe = sys.executable or '' + write_string(client, exe) + + try: + username = getpass.getuser() + except AttributeError: + username = '' + write_string(client, username) + + try: + impl = platform.python_implementation() + except AttributeError: + try: + impl = sys.implementation.name + except AttributeError: + impl = 'Python' + + major, minor, micro, release_level, serial = sys.version_info + + os_and_arch = platform.system() + if os_and_arch == "": + os_and_arch = sys.platform + try: + if sys.maxsize > 2**32: + os_and_arch += ' 64-bit' + else: + os_and_arch += ' 32-bit' + except AttributeError: + pass + + version = '%s %s.%s.%s (%s)' % (impl, major, minor, micro, os_and_arch) + write_string(client, version) + + # Don't just drop the connection - let the debugger close it after it finishes reading. + client.recv(1) + + elif response == ATCH: + debug_options = vspd.parse_debug_options(read_string(client)) + debug_options.setdefault('rules', []).append({ + 'path': PY_ROOT, + 'include': False, + }) + if redirect_output: + debug_options.add('RedirectOutput') + + if vspd.DETACHED: + write_bytes(client, ACPT) + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + major, minor, micro, release_level, serial = sys.version_info + write_int(client, major) + write_int(client, minor) + write_int(client, micro) + + vspd.attach_process_from_socket(client, debug_options, report = True) + vspd.mark_all_threads_for_break(vspd.STEPPING_ATTACH_BREAK) + _attached.set() + client = None + else: + write_bytes(client, RJCT) + + elif response == REPL: + if not vspd.DETACHED: + write_bytes(client, ACPT) + vspd.connect_repl_using_socket(client) + client = None + else: + write_bytes(client, RJCT) + + except (socket.error, OSError): + pass + finally: + if client is not None: + client.close() + + server_thread = threading.Thread(target = server_thread_func) + server_thread.setDaemon(True) + server_thread.start() + + frames = [] + f = sys._getframe() + while True: + f = f.f_back + if f is None: + break + frames.append(f) + frames.reverse() + cur_thread = vspd.new_thread() + for f in frames: + cur_thread.push_frame(f) + def replace_trace_func(): + for f in frames: + f.f_trace = cur_thread.trace_func + replace_trace_func() + sys.settrace(cur_thread.trace_func) + vspd.intercept_threads(for_attach = True) + + +# Alias for convenience of users of pydevd +settrace = enable_attach + + +def wait_for_attach(timeout = None): + """If a PTVS remote debugger is attached, returns immediately. Otherwise, + blocks until a remote debugger attaches to this process, or until the + optional timeout occurs. + + Parameters + ---------- + timeout : float, optional + The timeout for the operation in seconds (or fractions thereof). + """ + if vspd.DETACHED: + _attached.clear() + _attached.wait(timeout) + + +def break_into_debugger(): + """If a PTVS remote debugger is attached, pauses execution of all threads, + and breaks into the debugger with current thread as active. + """ + if not vspd.DETACHED: + vspd.SEND_BREAK_COMPLETE = thread.get_ident() + vspd.mark_all_threads_for_break() + +def is_attached(): + """Returns ``True`` if debugger is attached, ``False`` otherwise.""" + return not vspd.DETACHED diff --git a/src/test/pythonFiles/folding/empty.py b/src/test/pythonFiles/folding/empty.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/folding/miscSamples.py b/src/test/pythonFiles/folding/miscSamples.py new file mode 100644 index 000000000000..01495fb0ee9c --- /dev/null +++ b/src/test/pythonFiles/folding/miscSamples.py @@ -0,0 +1,40 @@ + +def one(): + """comment""" + pass + +def two(): + value = """a doc string with single and double quotes "This is how it's done" """ + pass + +def three(): + """a doc string with single and double quotes "This is how it's done" + Another line + """ + pass + +def four(): + '''a doc string with single and double quotes "This is how it's done" ''' + pass + +def five(): + '''a doc string with single and double quotes "This is how it's done" + Another line + ''' + pass + +def six(): + """ s1 """ """ s2 """ + pass + +def seven(): + value = """ s1 """ """ s2 """ + pass + +def eight(): + ''' s1 ''' ''' s2 ''' + pass + +def nine(): + value = ''' s1 ''' ''' s2 ''' + pass diff --git a/src/test/pythonFiles/folding/noComments.py b/src/test/pythonFiles/folding/noComments.py new file mode 100644 index 000000000000..4f0f7c5ec235 --- /dev/null +++ b/src/test/pythonFiles/folding/noComments.py @@ -0,0 +1,285 @@ +__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" +__version__ = "3.0.0.0" + +__all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] + +import atexit +import getpass +import os +import os.path +import platform +import socket +import struct +import sys +import threading +try: + import thread +except ImportError: + import _thread as thread +try: + import ssl +except ImportError: + ssl = None + +import ptvsd.visualstudio_py_debugger as vspd +import ptvsd.visualstudio_py_repl as vspr +from ptvsd.visualstudio_py_util import to_bytes, read_bytes, read_int, read_string, write_bytes, write_int, write_string + +PTVS_VER = '2.2' +DEFAULT_PORT = 5678 +PTVSDBG_VER = 6 +PTVSDBG = to_bytes('PTVSDBG') +ACPT = to_bytes('ACPT') +RJCT = to_bytes('RJCT') +INFO = to_bytes('INFO') +ATCH = to_bytes('ATCH') +REPL = to_bytes('REPL') + +PY_ROOT = os.path.normcase(__file__) +while os.path.basename(PY_ROOT) != 'pythonFiles': + PY_ROOT = os.path.dirname(PY_ROOT) + +_attach_enabled = False +_attached = threading.Event() + + +class AttachAlreadyEnabledError(Exception): + """`ptvsd.enable_attach` has already been called in this process.""" + + +def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, keyfile = None, redirect_output = True): + """Enables Python Tools for Visual Studio to attach to this process remotely + to debug Python code. + + Parameters + ---------- + secret : str + Used to validate the clients - only those clients providing the valid + secret will be allowed to connect to this server. On client side, the + secret is prepended to the Qualifier string, separated from the + hostname by ``'@'``, e.g.: ``'secret@myhost.cloudapp.net:5678'``. If + secret is ``None``, there's no validation, and any client can connect + freely. + address : (str, int), optional + Specifies the interface and port on which the debugging server should + listen for TCP connections. It is in the same format as used for + regular sockets of the `socket.AF_INET` family, i.e. a tuple of + ``(hostname, port)``. On client side, the server is identified by the + Qualifier string in the usual ``'hostname:port'`` format, e.g.: + ``'myhost.cloudapp.net:5678'``. Default is ``('0.0.0.0', 5678)``. + certfile : str, optional + Used to enable SSL. If not specified, or if set to ``None``, the + connection between this program and the debugger will be unsecure, + and can be intercepted on the wire. If specified, the meaning of this + parameter is the same as for `ssl.wrap_socket`. + keyfile : str, optional + Used together with `certfile` when SSL is enabled. Its meaning is the + same as for ``ssl.wrap_socket``. + redirect_output : bool, optional + Specifies whether any output (on both `stdout` and `stderr`) produced + by this program should be sent to the debugger. Default is ``True``. + + Notes + ----- + This function returns immediately after setting up the debugging server, + and does not block program execution. If you need to block until debugger + is attached, call `ptvsd.wait_for_attach`. The debugger can be detached + and re-attached multiple times after `enable_attach` is called. + + This function can only be called once during the lifetime of the process. + On a second call, `AttachAlreadyEnabledError` is raised. In circumstances + where the caller does not control how many times the function will be + called (e.g. when a script with a single call is run more than once by + a hosting app or framework), the call should be wrapped in ``try..except``. + + Only the thread on which this function is called, and any threads that are + created after it returns, will be visible in the debugger once it is + attached. Any threads that are already running before this function is + called will not be visible. + """ + + if not ssl and (certfile or keyfile): + raise ValueError('could not import the ssl module - SSL is not supported on this version of Python') + + if sys.platform == 'cli': + import clr + x_tracing = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Tracing + x_frames = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Frames + if not x_tracing or not x_frames: + raise RuntimeError('IronPython must be started with -X:Tracing and -X:Frames options to support PTVS remote debugging.') + + global _attach_enabled + if _attach_enabled: + raise AttachAlreadyEnabledError('ptvsd.enable_attach() has already been called in this process.') + _attach_enabled = True + + atexit.register(vspd.detach_process_and_notify_debugger) + + server = socket.socket(proto=socket.IPPROTO_TCP) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(address) + server.listen(1) + def server_thread_func(): + while True: + client = None + raw_client = None + try: + client, addr = server.accept() + if certfile: + client = ssl.wrap_socket(client, server_side = True, ssl_version = ssl.PROTOCOL_TLSv1, certfile = certfile, keyfile = keyfile) + write_bytes(client, PTVSDBG) + write_int(client, PTVSDBG_VER) + + response = read_bytes(client, 7) + if response != PTVSDBG: + continue + dbg_ver = read_int(client) + if dbg_ver != PTVSDBG_VER: + continue + + client_secret = read_string(client) + if secret is None or secret == client_secret: + write_bytes(client, ACPT) + else: + write_bytes(client, RJCT) + continue + + response = read_bytes(client, 4) + + if response == INFO: + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + exe = sys.executable or '' + write_string(client, exe) + + try: + username = getpass.getuser() + except AttributeError: + username = '' + write_string(client, username) + + try: + impl = platform.python_implementation() + except AttributeError: + try: + impl = sys.implementation.name + except AttributeError: + impl = 'Python' + + major, minor, micro, release_level, serial = sys.version_info + + os_and_arch = platform.system() + if os_and_arch == "": + os_and_arch = sys.platform + try: + if sys.maxsize > 2**32: + os_and_arch += ' 64-bit' + else: + os_and_arch += ' 32-bit' + except AttributeError: + pass + + version = '%s %s.%s.%s (%s)' % (impl, major, minor, micro, os_and_arch) + write_string(client, version) + + client.recv(1) + + elif response == ATCH: + debug_options = vspd.parse_debug_options(read_string(client)) + debug_options.setdefault('rules', []).append({ + 'path': PY_ROOT, + 'include': False, + }) + if redirect_output: + debug_options.add('RedirectOutput') + + if vspd.DETACHED: + write_bytes(client, ACPT) + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + major, minor, micro, release_level, serial = sys.version_info + write_int(client, major) + write_int(client, minor) + write_int(client, micro) + + vspd.attach_process_from_socket(client, debug_options, report = True) + vspd.mark_all_threads_for_break(vspd.STEPPING_ATTACH_BREAK) + _attached.set() + client = None + else: + write_bytes(client, RJCT) + + elif response == REPL: + if not vspd.DETACHED: + write_bytes(client, ACPT) + vspd.connect_repl_using_socket(client) + client = None + else: + write_bytes(client, RJCT) + + except (socket.error, OSError): + pass + finally: + if client is not None: + client.close() + + server_thread = threading.Thread(target = server_thread_func) + server_thread.setDaemon(True) + server_thread.start() + + frames = [] + f = sys._getframe() + while True: + f = f.f_back + if f is None: + break + frames.append(f) + frames.reverse() + cur_thread = vspd.new_thread() + for f in frames: + cur_thread.push_frame(f) + def replace_trace_func(): + for f in frames: + f.f_trace = cur_thread.trace_func + replace_trace_func() + sys.settrace(cur_thread.trace_func) + vspd.intercept_threads(for_attach = True) + + +settrace = enable_attach + + +def wait_for_attach(timeout = None): + """If a PTVS remote debugger is attached, returns immediately. Otherwise, + blocks until a remote debugger attaches to this process, or until the + optional timeout occurs. + + Parameters + ---------- + timeout : float, optional + The timeout for the operation in seconds (or fractions thereof). + """ + if vspd.DETACHED: + _attached.clear() + _attached.wait(timeout) + + +def break_into_debugger(): + """If a PTVS remote debugger is attached, pauses execution of all threads, + and breaks into the debugger with current thread as active. + """ + if not vspd.DETACHED: + vspd.SEND_BREAK_COMPLETE = thread.get_ident() + vspd.mark_all_threads_for_break() + +def is_attached(): + """Returns ``True`` if debugger is attached, ``False`` otherwise.""" + return not vspd.DETACHED diff --git a/src/test/pythonFiles/folding/noDocStrings.py b/src/test/pythonFiles/folding/noDocStrings.py new file mode 100644 index 000000000000..f5750dbfde78 --- /dev/null +++ b/src/test/pythonFiles/folding/noDocStrings.py @@ -0,0 +1,273 @@ +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. + +__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" +__version__ = "3.0.0.0" + +__all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] + +import atexit +import getpass +import os +import os.path +import platform +import socket +import struct +import sys +import threading +try: + import thread +except ImportError: + import _thread as thread +try: + import ssl +except ImportError: + ssl = None + +import ptvsd.visualstudio_py_debugger as vspd +import ptvsd.visualstudio_py_repl as vspr +from ptvsd.visualstudio_py_util import to_bytes, read_bytes, read_int, read_string, write_bytes, write_int, write_string + + +# The server (i.e. the Python app) waits on a TCP port provided. Whenever anything connects to that port, +# it immediately sends the octet sequence 'PTVSDBG', followed by version number represented as int64, +# and then waits for the client to respond with the same exact byte sequence. After signatures are thereby +# exchanged and found to match, the client is expected to provide a string secret (in the usual debugger +# string format, None/ACII/Unicode prefix + length + data), which can be an empty string to designate the +# lack of a specified secret. +# +# If the secret does not match the one expected by the server, it responds with 'RJCT', and then closes +# the connection. Otherwise, the server responds with 'ACPT', and awaits a 4-octet command. The following +# commands are recognized: +# +# 'INFO' +# Report information about the process. The server responds with the following information, in order: +# - Process ID (int64) +# - Executable name (string) +# - User name (string) +# - Implementation name (string) +# and then immediately closes connection. Note, all string fields can be empty or null strings. +# +# 'ATCH' +# Attach debugger to the process. If successful, the server responds with 'ACPT', followed by process ID +# (int64), and then the Python language version that the server is running represented by three int64s - +# major, minor, micro; From there on the socket is assumed to be using the normal PTVS debugging protocol. +# If attaching was not successful (which can happen if some other debugger is already attached), the server +# responds with 'RJCT' and closes the connection. +# +# 'REPL' +# Attach REPL to the process. If successful, the server responds with 'ACPT', and from there on the socket +# is assumed to be using the normal PTVS REPL protocol. If not successful (which can happen if there is +# no debugger attached), the server responds with 'RJCT' and closes the connection. + +PTVS_VER = '2.2' +DEFAULT_PORT = 5678 +PTVSDBG_VER = 6 # must be kept in sync with DebuggerProtocolVersion in PythonRemoteProcess.cs +PTVSDBG = to_bytes('PTVSDBG') +ACPT = to_bytes('ACPT') +RJCT = to_bytes('RJCT') +INFO = to_bytes('INFO') +ATCH = to_bytes('ATCH') +REPL = to_bytes('REPL') + +PY_ROOT = os.path.normcase(__file__) +while os.path.basename(PY_ROOT) != 'pythonFiles': + PY_ROOT = os.path.dirname(PY_ROOT) + +_attach_enabled = False +_attached = threading.Event() + + +class AttachAlreadyEnabledError(Exception): + + +def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, keyfile = None, redirect_output = True): + if not ssl and (certfile or keyfile): + raise ValueError('could not import the ssl module - SSL is not supported on this version of Python') + + if sys.platform == 'cli': + # Check that IronPython was launched with -X:Frames and -X:Tracing, since we can't register our trace + # func on the thread that calls enable_attach otherwise + import clr + x_tracing = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Tracing + x_frames = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Frames + if not x_tracing or not x_frames: + raise RuntimeError('IronPython must be started with -X:Tracing and -X:Frames options to support PTVS remote debugging.') + + global _attach_enabled + if _attach_enabled: + raise AttachAlreadyEnabledError('ptvsd.enable_attach() has already been called in this process.') + _attach_enabled = True + + atexit.register(vspd.detach_process_and_notify_debugger) + + server = socket.socket(proto=socket.IPPROTO_TCP) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(address) + server.listen(1) + def server_thread_func(): + while True: + client = None + raw_client = None + try: + client, addr = server.accept() + if certfile: + client = ssl.wrap_socket(client, server_side = True, ssl_version = ssl.PROTOCOL_TLSv1, certfile = certfile, keyfile = keyfile) + write_bytes(client, PTVSDBG) + write_int(client, PTVSDBG_VER) + + response = read_bytes(client, 7) + if response != PTVSDBG: + continue + dbg_ver = read_int(client) + if dbg_ver != PTVSDBG_VER: + continue + + client_secret = read_string(client) + if secret is None or secret == client_secret: + write_bytes(client, ACPT) + else: + write_bytes(client, RJCT) + continue + + response = read_bytes(client, 4) + + if response == INFO: + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + exe = sys.executable or '' + write_string(client, exe) + + try: + username = getpass.getuser() + except AttributeError: + username = '' + write_string(client, username) + + try: + impl = platform.python_implementation() + except AttributeError: + try: + impl = sys.implementation.name + except AttributeError: + impl = 'Python' + + major, minor, micro, release_level, serial = sys.version_info + + os_and_arch = platform.system() + if os_and_arch == "": + os_and_arch = sys.platform + try: + if sys.maxsize > 2**32: + os_and_arch += ' 64-bit' + else: + os_and_arch += ' 32-bit' + except AttributeError: + pass + + version = '%s %s.%s.%s (%s)' % (impl, major, minor, micro, os_and_arch) + write_string(client, version) + + # Don't just drop the connection - let the debugger close it after it finishes reading. + client.recv(1) + + elif response == ATCH: + debug_options = vspd.parse_debug_options(read_string(client)) + debug_options.setdefault('rules', []).append({ + 'path': PY_ROOT, + 'include': False, + }) + if redirect_output: + debug_options.add('RedirectOutput') + + if vspd.DETACHED: + write_bytes(client, ACPT) + try: + pid = os.getpid() + except AttributeError: + pid = 0 + write_int(client, pid) + + major, minor, micro, release_level, serial = sys.version_info + write_int(client, major) + write_int(client, minor) + write_int(client, micro) + + vspd.attach_process_from_socket(client, debug_options, report = True) + vspd.mark_all_threads_for_break(vspd.STEPPING_ATTACH_BREAK) + _attached.set() + client = None + else: + write_bytes(client, RJCT) + + elif response == REPL: + if not vspd.DETACHED: + write_bytes(client, ACPT) + vspd.connect_repl_using_socket(client) + client = None + else: + write_bytes(client, RJCT) + + except (socket.error, OSError): + pass + finally: + if client is not None: + client.close() + + server_thread = threading.Thread(target = server_thread_func) + server_thread.setDaemon(True) + server_thread.start() + + frames = [] + f = sys._getframe() + while True: + f = f.f_back + if f is None: + break + frames.append(f) + frames.reverse() + cur_thread = vspd.new_thread() + for f in frames: + cur_thread.push_frame(f) + def replace_trace_func(): + for f in frames: + f.f_trace = cur_thread.trace_func + replace_trace_func() + sys.settrace(cur_thread.trace_func) + vspd.intercept_threads(for_attach = True) + + +# Alias for convenience of users of pydevd +settrace = enable_attach + + +def wait_for_attach(timeout = None): + if vspd.DETACHED: + _attached.clear() + _attached.wait(timeout) + + +def break_into_debugger(): + if not vspd.DETACHED: + vspd.SEND_BREAK_COMPLETE = thread.get_ident() + vspd.mark_all_threads_for_break() + +def is_attached(): + return not vspd.DETACHED diff --git a/pythonFiles/PythonTools/visualstudio_ipython_repl.py b/src/test/pythonFiles/folding/visualstudio_ipython_repl.py similarity index 90% rename from pythonFiles/PythonTools/visualstudio_ipython_repl.py rename to src/test/pythonFiles/folding/visualstudio_ipython_repl.py index d09e3988d9db..33aa109de971 100644 --- a/pythonFiles/PythonTools/visualstudio_ipython_repl.py +++ b/src/test/pythonFiles/folding/visualstudio_ipython_repl.py @@ -1,16 +1,16 @@ # Python Tools for Visual Studio # Copyright(c) Microsoft Corporation # All rights reserved. -# +# # Licensed under the Apache License, Version 2.0 (the License); you may not use # this file except in compliance with the License. You may obtain a copy of the # License at http://www.apache.org/licenses/LICENSE-2.0 -# +# # THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS # OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY # IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, # MERCHANTABLITY OR NON-INFRINGEMENT. -# +# # See the Apache Version 2.0 License for specific language governing # permissions and limitations under the License. @@ -62,10 +62,10 @@ def is_ipython_versionorgreater(major, minor): else: import IPython.zmq KernelClient = object # was split out from KernelManager in 1.0 - from IPython.zmq.kernelmanager import (KernelManager, - ShellSocketChannel as ShellChannel, - SubSocketChannel as IOPubChannel, - StdInSocketChannel as StdInChannel, + from IPython.zmq.kernelmanager import (KernelManager, + ShellSocketChannel as ShellChannel, + SubSocketChannel as IOPubChannel, + StdInSocketChannel as StdInChannel, HBSocketChannel as HBChannel) from IPython.utils.traitlets import Type @@ -80,11 +80,11 @@ def is_ipython_versionorgreater(major, minor): # Channels which forward events # Description of the messaging protocol -# http://ipython.scipy.org/doc/manual/html/development/messaging.html +# http://ipython.scipy.org/doc/manual/html/development/messaging.html class DefaultHandler(object): - def unknown_command(self, content): + def unknown_command(self, content): import pprint print('unknown command ' + str(type(self))) pprint.pprint(content) @@ -93,15 +93,15 @@ def call_handlers(self, msg): # msg_type: # execute_reply msg_type = 'handle_' + msg['msg_type'] - + getattr(self, msg_type, self.unknown_command)(msg['content']) - + class VsShellChannel(DefaultHandler, ShellChannel): - + def handle_execute_reply(self, content): # we could have a payload here... payload = content['payload'] - + for item in payload: data = item.get('data') if data is not None: @@ -119,7 +119,7 @@ def handle_execute_reply(self, content): if output is not None: self._vs_backend.write_stdout(output) self._vs_backend.send_command_executed() - + def handle_inspect_reply(self, content): self.handle_object_info_reply(content) @@ -143,14 +143,14 @@ def call_handlers(self, msg): if not parent or parent.get('session') == self.session.session: msg_type = 'handle_' + msg['msg_type'] getattr(self, msg_type, self.unknown_command)(msg['content']) - + def handle_display_data(self, content): # called when user calls display() data = content.get('data', None) - + if data is not None: self.write_data(data) - + def handle_stream(self, content): stream_name = content['name'] if is_ipython_versionorgreater(3, 0): @@ -162,19 +162,23 @@ def handle_stream(self, content): elif stream_name == 'stderr': self._vs_backend.write_stderr(output) # TODO: stdin can show up here, do we echo that? - + def handle_execute_result(self, content): self.handle_execute_output(content) def handle_execute_output(self, content): - # called when an expression statement is printed, we treat + # called when an expression statement is printed, we treat # identical to stream output but it always goes to stdout output = content['data'] execution_count = content['execution_count'] self._vs_backend.execution_count = execution_count + 1 - self._vs_backend.send_prompt('\r\nIn [%d]: ' % (execution_count + 1), ' ' + ('.' * (len(str(execution_count + 1)) + 2)) + ': ', False) + self._vs_backend.send_prompt( + '\r\nIn [%d]: ' % (execution_count + 1), + ' ' + ('.' * (len(str(execution_count + 1)) + 2)) + ': ', + allow_multiple_statements=True + ) self.write_data(output, execution_count) - + def write_data(self, data, execution_count = None): output_xaml = data.get('application/xaml+xml', None) if output_xaml is not None: @@ -182,22 +186,22 @@ def write_data(self, data, execution_count = None): if isinstance(output_xaml, str) and sys.version_info[0] >= 3: output_xaml = output_xaml.encode('ascii') self._vs_backend.write_xaml(decodestring(output_xaml)) - self._vs_backend.write_stdout('\n') + self._vs_backend.write_stdout('\n') return except: pass - + output_png = data.get('image/png', None) if output_png is not None: try: if isinstance(output_png, str) and sys.version_info[0] >= 3: output_png = output_png.encode('ascii') self._vs_backend.write_png(decodestring(output_png)) - self._vs_backend.write_stdout('\n') + self._vs_backend.write_stdout('\n') return except: pass - + output_str = data.get('text/plain', None) if output_str is not None: if execution_count is not None: @@ -206,7 +210,7 @@ def write_data(self, data, execution_count = None): output_str = 'Out[' + str(execution_count) + ']: ' + output_str self._vs_backend.write_stdout(output_str) - self._vs_backend.write_stdout('\n') + self._vs_backend.write_stdout('\n') return def handle_error(self, content): @@ -216,13 +220,17 @@ def handle_error(self, content): tb = content['traceback'] self._vs_backend.write_stderr('\n'.join(tb)) self._vs_backend.write_stdout('\n') - + def handle_execute_input(self, content): # just a rebroadcast of the command to be executed, can be ignored self._vs_backend.execution_count += 1 - self._vs_backend.send_prompt('\r\nIn [%d]: ' % (self._vs_backend.execution_count), ' ' + ('.' * (len(str(self._vs_backend.execution_count)) + 2)) + ': ', False) + self._vs_backend.send_prompt( + '\r\nIn [%d]: ' % (self._vs_backend.execution_count), + ' ' + ('.' * (len(str(self._vs_backend.execution_count)) + 2)) + ': ', + allow_multiple_statements=True + ) pass - + def handle_status(self, content): pass @@ -237,9 +245,9 @@ def handle_input_request(self, content): # queue this to another thread so we don't block the channel def read_and_respond(): value = self._vs_backend.read_line() - + self.input(value) - + thread.start_new_thread(read_and_respond, ()) @@ -263,7 +271,7 @@ def __init__(self, mod_name = '__main__', launch_file = None): self.launch_file = launch_file self.mod_name = mod_name self.km = VsKernelManager() - + if is_ipython_versionorgreater(0, 13): # http://pytools.codeplex.com/workitem/759 # IPython stopped accepting the ipython flag and switched to launcher, the new @@ -276,7 +284,7 @@ def __init__(self, mod_name = '__main__', launch_file = None): self.exit_lock.acquire() # used as an event self.members_lock = thread.allocate_lock() self.members_lock.acquire() - + self.km.shell_channel._vs_backend = self self.km.stdin_channel._vs_backend = self if is_ipython_versionorgreater(1, 0): @@ -290,7 +298,7 @@ def get_extra_arguments(self): if sys.version <= '2.': return [unicode('--pylab=inline')] return ['--pylab=inline'] - + def execute_file_as_main(self, filename, arg_string): f = open(filename, 'rb') try: @@ -303,63 +311,62 @@ def execute_file_as_main(self, filename, arg_string): sys.argv = %(args)r __file__ = %(filename)r del sys -exec(compile(%(contents)r, %(filename)r, 'exec')) +exec(compile(%(contents)r, %(filename)r, 'exec')) ''' % {'filename' : filename, 'contents':contents, 'args': args} - + self.run_command(code, True) def execution_loop(self): - # launch the startup script if one has been specified - if self.launch_file: - self.execute_file_as_main(self.launch_file, None) - # we've got a bunch of threads setup for communication, we just block - # here until we're requested to exit. - self.send_prompt('\r\nIn [1]: ', ' ...: ', False) + # here until we're requested to exit. + self.send_prompt('\r\nIn [1]: ', ' ...: ', allow_multiple_statements=True) self.exit_lock.acquire() - + def run_command(self, command, silent = False): if is_ipython_versionorgreater(3, 0): self.km.execute(command, silent) else: self.km.shell_channel.execute(command, silent) - def execute_file(self, filename, args): - self.execute_file_as_main(filename, args) + def execute_file_ex(self, filetype, filename, args): + if filetype == 'script': + self.execute_file_as_main(filename, args) + else: + raise NotImplementedError("Cannot execute %s file" % filetype) def exit_process(self): self.exit_lock.release() def get_members(self, expression): - """returns a tuple of the type name, instance members, and type members""" + """returns a tuple of the type name, instance members, and type members""" text = expression + '.' if is_ipython_versionorgreater(3, 0): self.km.complete(text) else: self.km.shell_channel.complete(text, text, 1) - + self.members_lock.acquire() - + reply = self.complete_reply - + res = {} text_len = len(text) for member in reply['matches']: res[member[text_len:]] = 'object' return ('unknown', res, {}) - + def get_signatures(self, expression): """returns doc, args, vargs, varkw, defaults.""" - + if is_ipython_versionorgreater(3, 0): self.km.inspect(expression, None, 2) else: self.km.shell_channel.object_info(expression) - + self.members_lock.acquire() - - reply = self.object_info_reply + + reply = self.object_info_reply if is_ipython_versionorgreater(3, 0): data = reply['data'] text = data['text/plain'] @@ -377,10 +384,10 @@ def get_signatures(self, expression): def interrupt_main(self): """aborts the current running command""" self.km.interrupt_kernel() - + def set_current_module(self, module): pass - + def get_module_names(self): """returns a list of module names""" return [] @@ -391,7 +398,7 @@ def flush(self): def init_debugger(self): from os import path self.run_command(''' -def __visualstudio_debugger_init(): +def __visualstudio_debugger_init(): import sys sys.path.append(''' + repr(path.dirname(__file__)) + ''') import visualstudio_py_debugger @@ -420,4 +427,4 @@ def do_detach(): class IPythonBackendWithoutPyLab(IPythonBackend): def get_extra_arguments(self): - return [] + return [] \ No newline at end of file diff --git a/src/test/pythonFiles/folding/visualstudio_ipython_repl_double_quotes.py b/src/test/pythonFiles/folding/visualstudio_ipython_repl_double_quotes.py new file mode 100644 index 000000000000..473046639147 --- /dev/null +++ b/src/test/pythonFiles/folding/visualstudio_ipython_repl_double_quotes.py @@ -0,0 +1,430 @@ +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. + +"""Implements REPL support over IPython/ZMQ for VisualStudio""" + +__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" +__version__ = "3.0.0.0" + +import re +import sys +from visualstudio_py_repl import BasicReplBackend, ReplBackend, UnsupportedReplException, _command_line_to_args_list +from visualstudio_py_util import to_bytes +try: + import thread +except: + import _thread as thread # Renamed as Py3k + +from base64 import decodestring + +try: + import IPython +except ImportError: + exc_value = sys.exc_info()[1] + raise UnsupportedReplException('IPython mode requires IPython 0.11 or later: ' + str(exc_value)) + +def is_ipython_versionorgreater(major, minor): + """checks if we are at least a specific IPython version""" + match = re.match('(\d+).(\d+)', IPython.__version__) + if match: + groups = match.groups() + if int(groups[0]) > major: + return True + elif int(groups[0]) == major: + return int(groups[1]) >= minor + + return False + +remove_escapes = re.compile(r'\x1b[^m]*m') + +try: + if is_ipython_versionorgreater(3, 0): + from IPython.kernel import KernelManager + from IPython.kernel.channels import HBChannel + from IPython.kernel.threaded import (ThreadedZMQSocketChannel, ThreadedKernelClient as KernelClient) + ShellChannel = StdInChannel = IOPubChannel = ThreadedZMQSocketChannel + elif is_ipython_versionorgreater(1, 0): + from IPython.kernel import KernelManager, KernelClient + from IPython.kernel.channels import ShellChannel, HBChannel, StdInChannel, IOPubChannel + else: + import IPython.zmq + KernelClient = object # was split out from KernelManager in 1.0 + from IPython.zmq.kernelmanager import (KernelManager, + ShellSocketChannel as ShellChannel, + SubSocketChannel as IOPubChannel, + StdInSocketChannel as StdInChannel, + HBSocketChannel as HBChannel) + + from IPython.utils.traitlets import Type +except ImportError: + exc_value = sys.exc_info()[1] + raise UnsupportedReplException(str(exc_value)) + + +# TODO: SystemExit exceptions come back to us as strings, can we automatically exit when ones raised somehow? + +##### +# Channels which forward events + +# Description of the messaging protocol +# http://ipython.scipy.org/doc/manual/html/development/messaging.html + + +class DefaultHandler(object): + def unknown_command(self, content): + import pprint + print('unknown command ' + str(type(self))) + pprint.pprint(content) + + def call_handlers(self, msg): + # msg_type: + # execute_reply + msg_type = 'handle_' + msg['msg_type'] + + getattr(self, msg_type, self.unknown_command)(msg['content']) + +class VsShellChannel(DefaultHandler, ShellChannel): + + def handle_execute_reply(self, content): + # we could have a payload here... + payload = content['payload'] + + for item in payload: + data = item.get('data') + if data is not None: + try: + # Could be named km.sub_channel for very old IPython, but + # those versions should not put 'data' in this payload + write_data = self._vs_backend.km.iopub_channel.write_data + except AttributeError: + pass + else: + write_data(data) + continue + + output = item.get('text', None) + if output is not None: + self._vs_backend.write_stdout(output) + self._vs_backend.send_command_executed() + + def handle_inspect_reply(self, content): + self.handle_object_info_reply(content) + + def handle_object_info_reply(self, content): + self._vs_backend.object_info_reply = content + self._vs_backend.members_lock.release() + + def handle_complete_reply(self, content): + self._vs_backend.complete_reply = content + self._vs_backend.members_lock.release() + + def handle_kernel_info_reply(self, content): + self._vs_backend.write_stdout(content['banner']) + + +class VsIOPubChannel(DefaultHandler, IOPubChannel): + def call_handlers(self, msg): + # only output events from our session or no sessions + # https://pytools.codeplex.com/workitem/1622 + parent = msg.get('parent_header') + if not parent or parent.get('session') == self.session.session: + msg_type = 'handle_' + msg['msg_type'] + getattr(self, msg_type, self.unknown_command)(msg['content']) + + def handle_display_data(self, content): + # called when user calls display() + data = content.get('data', None) + + if data is not None: + self.write_data(data) + + def handle_stream(self, content): + stream_name = content['name'] + if is_ipython_versionorgreater(3, 0): + output = content['text'] + else: + output = content['data'] + if stream_name == 'stdout': + self._vs_backend.write_stdout(output) + elif stream_name == 'stderr': + self._vs_backend.write_stderr(output) + # TODO: stdin can show up here, do we echo that? + + def handle_execute_result(self, content): + self.handle_execute_output(content) + + def handle_execute_output(self, content): + # called when an expression statement is printed, we treat + # identical to stream output but it always goes to stdout + output = content['data'] + execution_count = content['execution_count'] + self._vs_backend.execution_count = execution_count + 1 + self._vs_backend.send_prompt( + '\r\nIn [%d]: ' % (execution_count + 1), + ' ' + ('.' * (len(str(execution_count + 1)) + 2)) + ': ', + allow_multiple_statements=True + ) + self.write_data(output, execution_count) + + def write_data(self, data, execution_count = None): + output_xaml = data.get('application/xaml+xml', None) + if output_xaml is not None: + try: + if isinstance(output_xaml, str) and sys.version_info[0] >= 3: + output_xaml = output_xaml.encode('ascii') + self._vs_backend.write_xaml(decodestring(output_xaml)) + self._vs_backend.write_stdout('\n') + return + except: + pass + + output_png = data.get('image/png', None) + if output_png is not None: + try: + if isinstance(output_png, str) and sys.version_info[0] >= 3: + output_png = output_png.encode('ascii') + self._vs_backend.write_png(decodestring(output_png)) + self._vs_backend.write_stdout('\n') + return + except: + pass + + output_str = data.get('text/plain', None) + if output_str is not None: + if execution_count is not None: + if '\n' in output_str: + output_str = '\n' + output_str + output_str = 'Out[' + str(execution_count) + ']: ' + output_str + + self._vs_backend.write_stdout(output_str) + self._vs_backend.write_stdout('\n') + return + + def handle_error(self, content): + # TODO: this includes escape sequences w/ color, we need to unescape that + ename = content['ename'] + evalue = content['evalue'] + tb = content['traceback'] + self._vs_backend.write_stderr('\n'.join(tb)) + self._vs_backend.write_stdout('\n') + + def handle_execute_input(self, content): + # just a rebroadcast of the command to be executed, can be ignored + self._vs_backend.execution_count += 1 + self._vs_backend.send_prompt( + '\r\nIn [%d]: ' % (self._vs_backend.execution_count), + ' ' + ('.' * (len(str(self._vs_backend.execution_count)) + 2)) + ': ', + allow_multiple_statements=True + ) + pass + + def handle_status(self, content): + pass + + # Backwards compat w/ 0.13 + handle_pyin = handle_execute_input + handle_pyout = handle_execute_output + handle_pyerr = handle_error + + +class VsStdInChannel(DefaultHandler, StdInChannel): + def handle_input_request(self, content): + # queue this to another thread so we don't block the channel + def read_and_respond(): + value = self._vs_backend.read_line() + + self.input(value) + + thread.start_new_thread(read_and_respond, ()) + + +class VsHBChannel(DefaultHandler, HBChannel): + pass + + +class VsKernelManager(KernelManager, KernelClient): + shell_channel_class = Type(VsShellChannel) + if is_ipython_versionorgreater(1, 0): + iopub_channel_class = Type(VsIOPubChannel) + else: + sub_channel_class = Type(VsIOPubChannel) + stdin_channel_class = Type(VsStdInChannel) + hb_channel_class = Type(VsHBChannel) + + +class IPythonBackend(ReplBackend): + def __init__(self, mod_name = '__main__', launch_file = None): + ReplBackend.__init__(self) + self.launch_file = launch_file + self.mod_name = mod_name + self.km = VsKernelManager() + + if is_ipython_versionorgreater(0, 13): + # http://pytools.codeplex.com/workitem/759 + # IPython stopped accepting the ipython flag and switched to launcher, the new + # default is what we want though. + self.km.start_kernel(**{'extra_arguments': self.get_extra_arguments()}) + else: + self.km.start_kernel(**{'ipython': True, 'extra_arguments': self.get_extra_arguments()}) + self.km.start_channels() + self.exit_lock = thread.allocate_lock() + self.exit_lock.acquire() # used as an event + self.members_lock = thread.allocate_lock() + self.members_lock.acquire() + + self.km.shell_channel._vs_backend = self + self.km.stdin_channel._vs_backend = self + if is_ipython_versionorgreater(1, 0): + self.km.iopub_channel._vs_backend = self + else: + self.km.sub_channel._vs_backend = self + self.km.hb_channel._vs_backend = self + self.execution_count = 1 + + def get_extra_arguments(self): + if sys.version <= '2.': + return [unicode('--pylab=inline')] + return ['--pylab=inline'] + + def execute_file_as_main(self, filename, arg_string): + f = open(filename, 'rb') + try: + contents = f.read().replace(to_bytes("\r\n"), to_bytes("\n")) + finally: + f.close() + args = [filename] + _command_line_to_args_list(arg_string) + code = """ +import sys +sys.argv = %(args)r +__file__ = %(filename)r +del sys +exec(compile(%(contents)r, %(filename)r, 'exec')) +""" % {'filename' : filename, 'contents':contents, 'args': args} + + self.run_command(code, True) + + def execution_loop(self): + # we've got a bunch of threads setup for communication, we just block + # here until we're requested to exit. + self.send_prompt('\r\nIn [1]: ', ' ...: ', allow_multiple_statements=True) + self.exit_lock.acquire() + + def run_command(self, command, silent = False): + if is_ipython_versionorgreater(3, 0): + self.km.execute(command, silent) + else: + self.km.shell_channel.execute(command, silent) + + def execute_file_ex(self, filetype, filename, args): + if filetype == 'script': + self.execute_file_as_main(filename, args) + else: + raise NotImplementedError("Cannot execute %s file" % filetype) + + def exit_process(self): + self.exit_lock.release() + + def get_members(self, expression): + """returns a tuple of the type name, instance members, and type members""" + text = expression + '.' + if is_ipython_versionorgreater(3, 0): + self.km.complete(text) + else: + self.km.shell_channel.complete(text, text, 1) + + self.members_lock.acquire() + + reply = self.complete_reply + + res = {} + text_len = len(text) + for member in reply['matches']: + res[member[text_len:]] = 'object' + + return ('unknown', res, {}) + + def get_signatures(self, expression): + """returns doc, args, vargs, varkw, defaults.""" + + if is_ipython_versionorgreater(3, 0): + self.km.inspect(expression, None, 2) + else: + self.km.shell_channel.object_info(expression) + + self.members_lock.acquire() + + reply = self.object_info_reply + if is_ipython_versionorgreater(3, 0): + data = reply['data'] + text = data['text/plain'] + text = remove_escapes.sub('', text) + return [(text, (), None, None, [])] + else: + argspec = reply['argspec'] + defaults = argspec['defaults'] + if defaults is not None: + defaults = [repr(default) for default in defaults] + else: + defaults = [] + return [(reply['docstring'], argspec['args'], argspec['varargs'], argspec['varkw'], defaults)] + + def interrupt_main(self): + """aborts the current running command""" + self.km.interrupt_kernel() + + def set_current_module(self, module): + pass + + def get_module_names(self): + """returns a list of module names""" + return [] + + def flush(self): + pass + + def init_debugger(self): + from os import path + self.run_command(""" +def __visualstudio_debugger_init(): + import sys + sys.path.append(""" + repr(path.dirname(__file__)) + """) + import visualstudio_py_debugger + new_thread = visualstudio_py_debugger.new_thread() + sys.settrace(new_thread.trace_func) + visualstudio_py_debugger.intercept_threads(True) + +__visualstudio_debugger_init() +del __visualstudio_debugger_init +""", True) + + def attach_process(self, port, debugger_id): + self.run_command(""" +def __visualstudio_debugger_attach(): + import visualstudio_py_debugger + + def do_detach(): + visualstudio_py_debugger.DETACH_CALLBACKS.remove(do_detach) + + visualstudio_py_debugger.DETACH_CALLBACKS.append(do_detach) + visualstudio_py_debugger.attach_process(""" + str(port) + """, """ + repr(debugger_id) + """, report = True, block = True) + +__visualstudio_debugger_attach() +del __visualstudio_debugger_attach +""", True) + +class IPythonBackendWithoutPyLab(IPythonBackend): + def get_extra_arguments(self): + return [] diff --git a/src/test/pythonFiles/folding/visualstudio_py_debugger.py b/src/test/pythonFiles/folding/visualstudio_py_debugger.py new file mode 100644 index 000000000000..ec18ff8c63b0 --- /dev/null +++ b/src/test/pythonFiles/folding/visualstudio_py_debugger.py @@ -0,0 +1,644 @@ +# Python Tools for Visual Studio +# Copyright(c) Microsoft Corporation +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the License); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at http://www.apache.org/licenses/LICENSE-2.0 +# +# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +# MERCHANTABLITY OR NON-INFRINGEMENT. +# +# See the Apache Version 2.0 License for specific language governing +# permissions and limitations under the License. +# With number of modifications by Don Jayamanne + +from __future__ import with_statement + +__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" +__version__ = "3.0.0.0" + +# This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) +# attach scenario, it is loaded on the injected debugger attach thread, and if threading module +# hasn't been loaded already, it will assume that the thread on which it is being loaded is the +# main thread. This will cause issues when the thread goes away after attach completes. +_threading = None + +import sys +import ctypes +try: + import thread +except ImportError: + import _thread as thread +import socket +import struct +import weakref +import traceback +import types +import bisect +from os import path +import ntpath +import runpy +import datetime +from codecs import BOM_UTF8 + +try: + # In the local attach scenario, visualstudio_py_util is injected into globals() + # by PyDebugAttach before loading this module, and cannot be imported. + _vspu = visualstudio_py_util +except: + try: + import visualstudio_py_util as _vspu + except ImportError: + import ptvsd.visualstudio_py_util as _vspu + +to_bytes = _vspu.to_bytes +exec_file = _vspu.exec_file +exec_module = _vspu.exec_module +exec_code = _vspu.exec_code +read_bytes = _vspu.read_bytes +read_int = _vspu.read_int +read_string = _vspu.read_string +write_bytes = _vspu.write_bytes +write_int = _vspu.write_int +write_string = _vspu.write_string +safe_repr = _vspu.SafeRepr() + +try: + # In the local attach scenario, visualstudio_py_repl is injected into globals() + # by PyDebugAttach before loading this module, and cannot be imported. + _vspr = visualstudio_py_repl +except: + try: + import visualstudio_py_repl as _vspr + except ImportError: + import ptvsd.visualstudio_py_repl as _vspr + +try: + import stackless +except ImportError: + stackless = None + +try: + xrange +except: + xrange = range + +if sys.platform == 'cli': + import clr + from System.Runtime.CompilerServices import ConditionalWeakTable + IPY_SEEN_MODULES = ConditionalWeakTable[object, object]() + +# Import encodings early to avoid import on the debugger thread, which may cause deadlock +from encodings import utf_8 + +# WARNING: Avoid imports beyond this point, specifically on the debugger thread, as this may cause +# deadlock where the debugger thread performs an import while a user thread has the import lock + +# save start_new_thread so we can call it later, we'll intercept others calls to it. + +debugger_dll_handle = None +DETACHED = True +def thread_creator(func, args, kwargs = {}, *extra_args): + if not isinstance(args, tuple): + # args is not a tuple. This may be because we have become bound to a + # class, which has offset our arguments by one. + if isinstance(kwargs, tuple): + func, args = args, kwargs + kwargs = extra_args[0] if len(extra_args) > 0 else {} + + return _start_new_thread(new_thread_wrapper, (func, args, kwargs)) + +_start_new_thread = thread.start_new_thread +THREADS = {} +THREADS_LOCK = thread.allocate_lock() +MODULES = [] + +BREAK_ON_SYSTEMEXIT_ZERO = False +DEBUG_STDLIB = False +DJANGO_DEBUG = False + +RICH_EXCEPTIONS = False +IGNORE_DJANGO_TEMPLATE_WARNINGS = False + +# Py3k compat - alias unicode to str +try: + unicode +except: + unicode = str + +# A value of a synthesized child. The string is passed through to the variable list, and type is not displayed at all. +class SynthesizedValue(object): + def __init__(self, repr_value='', len_value=None): + self.repr_value = repr_value + self.len_value = len_value + def __repr__(self): + return self.repr_value + def __len__(self): + return self.len_value + +# Specifies list of files not to debug. Can be extended by other modules +# (the REPL does this for $attach support and not stepping into the REPL). +DONT_DEBUG = [path.normcase(__file__), path.normcase(_vspu.__file__)] +if sys.version_info >= (3, 3): + DONT_DEBUG.append(path.normcase('<frozen importlib._bootstrap>')) +if sys.version_info >= (3, 5): + DONT_DEBUG.append(path.normcase('<frozen importlib._bootstrap_external>')) + +# Contains information about all breakpoints in the process. Keys are line numbers on which +# there are breakpoints in any file, and values are dicts. For every line number, the +# corresponding dict contains all the breakpoints that fall on that line. The keys in that +# dict are tuples of the form (filename, breakpoint_id), each entry representing a single +# breakpoint, and values are BreakpointInfo objects. +# +# For example, given the following breakpoints: +# +# 1. In 'main.py' at line 10. +# 2. In 'main.py' at line 20. +# 3. In 'module.py' at line 10. +# +# the contents of BREAKPOINTS would be: +# {10: {('main.py', 1): ..., ('module.py', 3): ...}, 20: {('main.py', 2): ... }} +BREAKPOINTS = {} + +# Contains information about all pending (i.e. not yet bound) breakpoints in the process. +# Elements are BreakpointInfo objects. +PENDING_BREAKPOINTS = set() + +# Must be in sync with enum PythonBreakpointConditionKind in PythonBreakpoint.cs +BREAKPOINT_CONDITION_ALWAYS = 0 +BREAKPOINT_CONDITION_WHEN_TRUE = 1 +BREAKPOINT_CONDITION_WHEN_CHANGED = 2 + +# Must be in sync with enum PythonBreakpointPassCountKind in PythonBreakpoint.cs +BREAKPOINT_PASS_COUNT_ALWAYS = 0 +BREAKPOINT_PASS_COUNT_EVERY = 1 +BREAKPOINT_PASS_COUNT_WHEN_EQUAL = 2 +BREAKPOINT_PASS_COUNT_WHEN_EQUAL_OR_GREATER = 3 + +## Begin modification by Don Jayamanne +DJANGO_VERSIONS_IDENTIFIED = False +IS_DJANGO18 = False +IS_DJANGO19 = False +IS_DJANGO19_OR_HIGHER = False + +try: + dict_contains = dict.has_key +except: + try: + #Py3k does not have has_key anymore, and older versions don't have __contains__ + dict_contains = dict.__contains__ + except: + try: + dict_contains = dict.has_key + except NameError: + def dict_contains(d, key): + return d.has_key(key) +## End modification by Don Jayamanne + +class BreakpointInfo(object): + __slots__ = [ + 'breakpoint_id', 'filename', 'lineno', 'condition_kind', 'condition', + 'pass_count_kind', 'pass_count', 'is_bound', 'last_condition_value', + 'hit_count' + ] + + # For "when changed" breakpoints, this is used as the initial value of last_condition_value, + # such that it is guaranteed to not compare equal to any other value that it will get later. + _DUMMY_LAST_VALUE = object() + + def __init__(self, breakpoint_id, filename, lineno, condition_kind, condition, pass_count_kind, pass_count): + self.breakpoint_id = breakpoint_id + self.filename = filename + self.lineno = lineno + self.condition_kind = condition_kind + self.condition = condition + self.pass_count_kind = pass_count_kind + self.pass_count = pass_count + self.is_bound = False + self.last_condition_value = BreakpointInfo._DUMMY_LAST_VALUE + self.hit_count = 0 + + @staticmethod + def find_by_id(breakpoint_id): + for line, bp_dict in BREAKPOINTS.items(): + for (filename, bp_id), bp in bp_dict.items(): + if bp_id == breakpoint_id: + return bp + return None + +# lock for calling .send on the socket +send_lock = thread.allocate_lock() + +class _SendLockContextManager(object): + """context manager for send lock. Handles both acquiring/releasing the + send lock as well as detaching the debugger if the remote process + is disconnected""" + + def __enter__(self): + # mark that we're about to do socket I/O so we won't deliver + # debug events when we're debugging the standard library + cur_thread = get_thread_from_id(thread.get_ident()) + if cur_thread is not None: + cur_thread.is_sending = True + + send_lock.acquire() + + def __exit__(self, exc_type, exc_value, tb): + send_lock.release() + + # start sending debug events again + cur_thread = get_thread_from_id(thread.get_ident()) + if cur_thread is not None: + cur_thread.is_sending = False + + if exc_type is not None: + detach_threads() + detach_process() + # swallow the exception, we're no longer debugging + return True + +_SendLockCtx = _SendLockContextManager() + +SEND_BREAK_COMPLETE = False + +STEPPING_OUT = -1 # first value, we decrement below this +STEPPING_NONE = 0 +STEPPING_BREAK = 1 +STEPPING_LAUNCH_BREAK = 2 +STEPPING_ATTACH_BREAK = 3 +STEPPING_INTO = 4 +STEPPING_OVER = 5 # last value, we increment past this. + +USER_STEPPING = (STEPPING_OUT, STEPPING_INTO, STEPPING_OVER) + +FRAME_KIND_NONE = 0 +FRAME_KIND_PYTHON = 1 +FRAME_KIND_DJANGO = 2 + +DJANGO_BUILTINS = {'True': True, 'False': False, 'None': None} + +PYTHON_EVALUATION_RESULT_REPR_KIND_NORMAL = 0 # regular repr and hex repr (if applicable) for the evaluation result; length is len(result) +PYTHON_EVALUATION_RESULT_REPR_KIND_RAW = 1 # repr is raw representation of the value - see TYPES_WITH_RAW_REPR; length is len(repr) +PYTHON_EVALUATION_RESULT_REPR_KIND_RAWLEN = 2 # same as above, but only the length is reported, not the actual value + +PYTHON_EVALUATION_RESULT_EXPANDABLE = 1 +PYTHON_EVALUATION_RESULT_METHOD_CALL = 2 +PYTHON_EVALUATION_RESULT_SIDE_EFFECTS = 4 +PYTHON_EVALUATION_RESULT_RAW = 8 +PYTHON_EVALUATION_RESULT_HAS_RAW_REPR = 16 + +# Don't show attributes of these types if they come from the class (assume they are methods). +METHOD_TYPES = ( + types.FunctionType, + types.MethodType, + types.BuiltinFunctionType, + type("".__repr__), # method-wrapper +) + +# repr() for these types can be used as input for eval() to get the original value. +# float is intentionally not included because it is not always round-trippable (e.g inf, nan). +TYPES_WITH_ROUND_TRIPPING_REPR = set((type(None), int, bool, str, unicode)) +if sys.version[0] == '3': + TYPES_WITH_ROUND_TRIPPING_REPR.add(bytes) +else: + TYPES_WITH_ROUND_TRIPPING_REPR.add(long) + +# repr() for these types can be used as input for eval() to get the original value, provided that the same is true for all their elements. +COLLECTION_TYPES_WITH_ROUND_TRIPPING_REPR = set((tuple, list, set, frozenset)) + +# eval(repr(x)), but optimized for common types for which it is known that result == x. +def eval_repr(x): + def is_repr_round_tripping(x): + # Do exact type checks here - subclasses can override __repr__. + if type(x) in TYPES_WITH_ROUND_TRIPPING_REPR: + return True + elif type(x) in COLLECTION_TYPES_WITH_ROUND_TRIPPING_REPR: + # All standard sequence types are round-trippable if their elements are. + return all((is_repr_round_tripping(item) for item in x)) + else: + return False + if is_repr_round_tripping(x): + return x + else: + return eval(repr(x), {}) + +# key is type, value is function producing the raw repr +TYPES_WITH_RAW_REPR = { + unicode: (lambda s: s) +} + +# bytearray is 2.6+ +try: + # getfilesystemencoding is used here because it effectively corresponds to the notion of "locale encoding": + # current ANSI codepage on Windows, LC_CTYPE on Linux, UTF-8 on OS X - which is exactly what we want. + TYPES_WITH_RAW_REPR[bytearray] = lambda b: b.decode(sys.getfilesystemencoding(), 'ignore') +except: + pass + +if sys.version[0] == '3': + TYPES_WITH_RAW_REPR[bytes] = TYPES_WITH_RAW_REPR[bytearray] +else: + TYPES_WITH_RAW_REPR[str] = TYPES_WITH_RAW_REPR[unicode] + +if sys.version[0] == '3': + # work around a crashing bug on CPython 3.x where they take a hard stack overflow + # we'll never see this exception but it'll allow us to keep our try/except handler + # the same across all versions of Python + class StackOverflowException(Exception): pass +else: + StackOverflowException = RuntimeError + +ASBR = to_bytes('ASBR') +SETL = to_bytes('SETL') +THRF = to_bytes('THRF') +DETC = to_bytes('DETC') +NEWT = to_bytes('NEWT') +EXTT = to_bytes('EXTT') +EXIT = to_bytes('EXIT') +EXCP = to_bytes('EXCP') +EXC2 = to_bytes('EXC2') +MODL = to_bytes('MODL') +STPD = to_bytes('STPD') +BRKS = to_bytes('BRKS') +BRKF = to_bytes('BRKF') +BRKH = to_bytes('BRKH') +BRKC = to_bytes('BRKC') +BKHC = to_bytes('BKHC') +LOAD = to_bytes('LOAD') +EXCE = to_bytes('EXCE') +EXCR = to_bytes('EXCR') +CHLD = to_bytes('CHLD') +OUTP = to_bytes('OUTP') +REQH = to_bytes('REQH') +LAST = to_bytes('LAST') + +def get_thread_from_id(id): + THREADS_LOCK.acquire() + try: + return THREADS.get(id) + finally: + THREADS_LOCK.release() + +def should_send_frame(frame): + return (frame is not None and + frame.f_code not in DEBUG_ENTRYPOINTS and + path.normcase(frame.f_code.co_filename) not in DONT_DEBUG) + +KNOWN_DIRECTORIES = set((None, '')) +KNOWN_ZIPS = set() + +def is_file_in_zip(filename): + parent, name = path.split(path.abspath(filename)) + if parent in KNOWN_DIRECTORIES: + return False + elif parent in KNOWN_ZIPS: + return True + elif path.isdir(parent): + KNOWN_DIRECTORIES.add(parent) + return False + else: + KNOWN_ZIPS.add(parent) + return True + +def lookup_builtin(name, frame): + try: + return frame.f_builtins.get(bits) + except: + # http://ironpython.codeplex.com/workitem/30908 + builtins = frame.f_globals['__builtins__'] + if not isinstance(builtins, dict): + builtins = builtins.__dict__ + return builtins.get(name) + +def lookup_local(frame, name): + bits = name.split('.') + obj = frame.f_locals.get(bits[0]) or frame.f_globals.get(bits[0]) or lookup_builtin(bits[0], frame) + bits.pop(0) + while bits and obj is not None and type(obj) is types.ModuleType: + obj = getattr(obj, bits.pop(0), None) + return obj + +if sys.version_info[0] >= 3: + _EXCEPTIONS_MODULE = 'builtins' +else: + _EXCEPTIONS_MODULE = 'exceptions' + +def get_exception_name(exc_type): + if exc_type.__module__ == _EXCEPTIONS_MODULE: + return exc_type.__name__ + else: + return exc_type.__module__ + '.' + exc_type.__name__ + +# These constants come from Visual Studio - enum_EXCEPTION_STATE +BREAK_MODE_NEVER = 0 +BREAK_MODE_ALWAYS = 1 +BREAK_MODE_UNHANDLED = 32 + +BREAK_TYPE_NONE = 0 +BREAK_TYPE_UNHANDLED = 1 +BREAK_TYPE_HANDLED = 2 + +class ExceptionBreakInfo(object): + BUILT_IN_HANDLERS = { + path.normcase('<frozen importlib._bootstrap>'): ((None, None, '*'),), + path.normcase('build\\bdist.win32\\egg\\pkg_resources.py'): ((None, None, '*'),), + path.normcase('build\\bdist.win-amd64\\egg\\pkg_resources.py'): ((None, None, '*'),), + } + + def __init__(self): + self.default_mode = BREAK_MODE_UNHANDLED + self.break_on = { } + self.handler_cache = dict(self.BUILT_IN_HANDLERS) + self.handler_lock = thread.allocate_lock() + self.add_exception('exceptions.IndexError', BREAK_MODE_NEVER) + self.add_exception('builtins.IndexError', BREAK_MODE_NEVER) + self.add_exception('exceptions.KeyError', BREAK_MODE_NEVER) + self.add_exception('builtins.KeyError', BREAK_MODE_NEVER) + self.add_exception('exceptions.AttributeError', BREAK_MODE_NEVER) + self.add_exception('builtins.AttributeError', BREAK_MODE_NEVER) + self.add_exception('exceptions.StopIteration', BREAK_MODE_NEVER) + self.add_exception('builtins.StopIteration', BREAK_MODE_NEVER) + self.add_exception('exceptions.GeneratorExit', BREAK_MODE_NEVER) + self.add_exception('builtins.GeneratorExit', BREAK_MODE_NEVER) + + def clear(self): + self.default_mode = BREAK_MODE_UNHANDLED + self.break_on.clear() + self.handler_cache = dict(self.BUILT_IN_HANDLERS) + + def should_break(self, thread, ex_type, ex_value, trace): + probe_stack() + name = get_exception_name(ex_type) + mode = self.break_on.get(name, self.default_mode) + break_type = BREAK_TYPE_NONE + if mode & BREAK_MODE_ALWAYS: + if self.is_handled(thread, ex_type, ex_value, trace): + break_type = BREAK_TYPE_HANDLED + else: + break_type = BREAK_TYPE_UNHANDLED + elif (mode & BREAK_MODE_UNHANDLED) and not self.is_handled(thread, ex_type, ex_value, trace): + break_type = BREAK_TYPE_UNHANDLED + + if break_type: + if issubclass(ex_type, SystemExit): + if not BREAK_ON_SYSTEMEXIT_ZERO: + if not ex_value or (isinstance(ex_value, SystemExit) and not ex_value.code): + break_type = BREAK_TYPE_NONE + + return break_type + + def is_handled(self, thread, ex_type, ex_value, trace): + if trace is None: + # get out if we didn't get a traceback + return False + + if trace.tb_next is not None: + if should_send_frame(trace.tb_next.tb_frame) and should_debug_code(trace.tb_next.tb_frame.f_code): + # don't break if this is not the top of the traceback, + # unless the previous frame was not debuggable + return True + + cur_frame = trace.tb_frame + + while should_send_frame(cur_frame) and cur_frame.f_code is not None and cur_frame.f_code.co_filename is not None: + filename = path.normcase(cur_frame.f_code.co_filename) + if is_file_in_zip(filename): + # File is in a zip, so assume it handles exceptions + return True + + if not is_same_py_file(filename, __file__): + handlers = self.handler_cache.get(filename) + + if handlers is None: + # req handlers for this file from the debug engine + self.handler_lock.acquire() + + with _SendLockCtx: + write_bytes(conn, REQH) + write_string(conn, filename) + + # wait for the handler data to be received + self.handler_lock.acquire() + self.handler_lock.release() + + handlers = self.handler_cache.get(filename) + + if handlers is None: + # no code available, so assume unhandled + return False + + line = cur_frame.f_lineno + for line_start, line_end, expressions in handlers: + if line_start is None or line_start <= line < line_end: + if '*' in expressions: + return True + + for text in expressions: + try: + res = lookup_local(cur_frame, text) + if res is not None and issubclass(ex_type, res): + return True + except: + pass + + cur_frame = cur_frame.f_back + + return False + + def add_exception(self, name, mode=BREAK_MODE_UNHANDLED): + if name.startswith(_EXCEPTIONS_MODULE + '.'): + name = name[len(_EXCEPTIONS_MODULE) + 1:] + self.break_on[name] = mode + +BREAK_ON = ExceptionBreakInfo() + +def probe_stack(depth = 10): + """helper to make sure we have enough stack space to proceed w/o corrupting + debugger state.""" + if depth == 0: + return + probe_stack(depth - 1) + +PREFIXES = [path.normcase(sys.prefix)] +# If we're running in a virtual env, DEBUG_STDLIB should respect this too. +if hasattr(sys, 'base_prefix'): + PREFIXES.append(path.normcase(sys.base_prefix)) +if hasattr(sys, 'real_prefix'): + PREFIXES.append(path.normcase(sys.real_prefix)) + +def should_debug_code(code): + if not code or not code.co_filename: + return False + + filename = path.normcase(code.co_filename) + if not DEBUG_STDLIB: + for prefix in PREFIXES: + if prefix != '' and filename.startswith(prefix): + return False + + for dont_debug_file in DONT_DEBUG: + if is_same_py_file(filename, dont_debug_file): + return False + + if is_file_in_zip(filename): + # file in inside an egg or zip, so we can't debug it + return False + + return True + +attach_lock = thread.allocate() +attach_sent_break = False + +local_path_to_vs_path = {} + +def breakpoint_path_match(vs_path, local_path): + vs_path_norm = path.normcase(vs_path) + local_path_norm = path.normcase(local_path) + if local_path_to_vs_path.get(local_path_norm) == vs_path_norm: + return True + + # Walk the local filesystem from local_path up, matching agains win_path component by component, + # and stop when we no longer see an __init__.py. This should give a reasonably close approximation + # of matching the package name. + while True: + local_path, local_name = path.split(local_path) + vs_path, vs_name = ntpath.split(vs_path) + # Match the last component in the path. If one or both components are unavailable, then + # we have reached the root on the corresponding path without successfully matching. + if not local_name or not vs_name or path.normcase(local_name) != path.normcase(vs_name): + return False + # If we have an __init__.py, this module was inside the package, and we still need to match + # thatpackage, so walk up one level and keep matching. Otherwise, we've walked as far as we + # needed to, and matched all names on our way, so this is a match. + if not path.exists(path.join(local_path, '__init__.py')): + break + + local_path_to_vs_path[local_path_norm] = vs_path_norm + return True + +def update_all_thread_stacks(blocking_thread = None, check_is_blocked = True): + THREADS_LOCK.acquire() + all_threads = list(THREADS.values()) + THREADS_LOCK.release() + + for cur_thread in all_threads: + if cur_thread is blocking_thread: + continue + + cur_thread._block_starting_lock.acquire() + if not check_is_blocked or not cur_thread._is_blocked: + # release the lock, we're going to run user code to evaluate the frames + cur_thread._block_starting_lock.release() + + frames = cur_thread.get_frame_list() + + # re-acquire the lock and make sure we're still not blocked. If so send + # the frame list. + cur_thread._block_starting_lock.acquire() + if not check_is_blocked or not cur_thread._is_blocked: + cur_thread.send_frame_list(frames) + + cur_thread._block_starting_lock.release() diff --git a/src/test/pythonFiles/folding/visualstudio_py_repl.py b/src/test/pythonFiles/folding/visualstudio_py_repl.py new file mode 100644 index 000000000000..595922f8f9cc --- /dev/null +++ b/src/test/pythonFiles/folding/visualstudio_py_repl.py @@ -0,0 +1,520 @@ +# Python Tools for Visual Studio + +# Copyright(c) Microsoft Corporation + +# All rights reserved. + +from __future__ import with_statement + +__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" +__version__ = "3.0.0.0" + +# This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) + +# attach scenario, it is loaded on the injected debugger attach thread, and if threading module + +# hasn't been loaded already, it will assume that the thread on which it is being loaded is the + +# main thread. This will cause issues when the thread goes away after attach completes. + +try: + import thread +except ImportError: + # Renamed in Python3k + import _thread as thread +try: + from ssl import SSLError +except: + SSLError = None + +import sys +import socket +import select +import time +import struct +import imp +import traceback +import random +import os +import inspect +import types +from collections import deque + +try: + # In the local attach scenario, visualstudio_py_util is injected into globals() + + # by PyDebugAttach before loading this module, and cannot be imported. + _vspu = visualstudio_py_util +except: + try: + import visualstudio_py_util as _vspu + except ImportError: + import ptvsd.visualstudio_py_util as _vspu +to_bytes = _vspu.to_bytes +read_bytes = _vspu.read_bytes +read_int = _vspu.read_int +read_string = _vspu.read_string +write_bytes = _vspu.write_bytes +write_int = _vspu.write_int +write_string = _vspu.write_string + +try: + unicode +except NameError: + unicode = str + +try: + BaseException +except NameError: + # BaseException not defined until Python 2.5 + BaseException = Exception + +DEBUG = os.environ.get('DEBUG_REPL') is not None + +PY_ROOT = os.path.normcase(__file__) +while os.path.basename(PY_ROOT) != 'pythonFiles': + PY_ROOT = os.path.dirname(PY_ROOT) + +__all__ = ['ReplBackend', 'BasicReplBackend', 'BACKEND'] + +def _debug_write(out): + if DEBUG: + sys.__stdout__.write(out) + sys.__stdout__.flush() + + +class SafeSendLock(object): + """a lock which ensures we're released if we take a KeyboardInterrupt exception acquiring it""" + def __init__(self): + self.lock = thread.allocate_lock() + + def __enter__(self): + self.acquire() + + def __exit__(self, exc_type, exc_value, tb): + self.release() + + def acquire(self): + try: + self.lock.acquire() + except KeyboardInterrupt: + try: + self.lock.release() + except: + pass + raise + + def release(self): + self.lock.release() + +def _command_line_to_args_list(cmdline): + """splits a string into a list using Windows command line syntax.""" + args_list = [] + + if cmdline and cmdline.strip(): + from ctypes import c_int, c_voidp, c_wchar_p + from ctypes import byref, POINTER, WinDLL + + clta = WinDLL('shell32').CommandLineToArgvW + clta.argtypes = [c_wchar_p, POINTER(c_int)] + clta.restype = POINTER(c_wchar_p) + + lf = WinDLL('kernel32').LocalFree + lf.argtypes = [c_voidp] + + pNumArgs = c_int() + r = clta(cmdline, byref(pNumArgs)) + if r: + for index in range(0, pNumArgs.value): + if sys.hexversion >= 0x030000F0: + argval = r[index] + else: + argval = r[index].encode('ascii', 'replace') + args_list.append(argval) + lf(r) + else: + sys.stderr.write('Error parsing script arguments:\n') + sys.stderr.write(cmdline + '\n') + + return args_list + + +class UnsupportedReplException(Exception): + def __init__(self, reason): + self.reason = reason + +# save the start_new_thread so we won't debug/break into the REPL comm thread. +start_new_thread = thread.start_new_thread +class ReplBackend(object): + """back end for executing REPL code. This base class handles all of the communication with the remote process while derived classes implement the actual inspection and introspection.""" + _MRES = to_bytes('MRES') + _SRES = to_bytes('SRES') + _MODS = to_bytes('MODS') + _IMGD = to_bytes('IMGD') + _PRPC = to_bytes('PRPC') + _RDLN = to_bytes('RDLN') + _STDO = to_bytes('STDO') + _STDE = to_bytes('STDE') + _DBGA = to_bytes('DBGA') + _DETC = to_bytes('DETC') + _DPNG = to_bytes('DPNG') + _DXAM = to_bytes('DXAM') + _CHWD = to_bytes('CHWD') + + _MERR = to_bytes('MERR') + _SERR = to_bytes('SERR') + _ERRE = to_bytes('ERRE') + _EXIT = to_bytes('EXIT') + _DONE = to_bytes('DONE') + _MODC = to_bytes('MODC') + + def __init__(self, *args, **kwargs): + import threading + self.conn = None + self.send_lock = SafeSendLock() + self.input_event = threading.Lock() + self.input_event.acquire() # lock starts acquired (we use it like a manual reset event) + self.input_string = None + self.exit_requested = False + + def connect(self, port): + self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.conn.connect(('127.0.0.1', port)) + + # start a new thread for communicating w/ the remote process + start_new_thread(self._repl_loop, ()) + + def connect_using_socket(self, socket): + self.conn = socket + start_new_thread(self._repl_loop, ()) + + def _repl_loop(self): + """loop on created thread which processes communicates with the REPL window""" + try: + while True: + if self.check_for_exit_repl_loop(): + break + + # we receive a series of 4 byte commands. Each command then + + # has it's own format which we must parse before continuing to + + # the next command. + self.flush() + self.conn.settimeout(10) + + # 2.x raises SSLError in case of timeout (http://bugs.python.org/issue10272) + if SSLError: + timeout_exc_types = (socket.timeout, SSLError) + else: + timeout_exc_types = socket.timeout + try: + inp = read_bytes(self.conn, 4) + except timeout_exc_types: + r, w, x = select.select([], [], [self.conn], 0) + if x: + # an exception event has occurred on the socket... + raise + continue + + self.conn.settimeout(None) + if inp == '': + break + self.flush() + + cmd = ReplBackend._COMMANDS.get(inp) + if cmd is not None: + cmd(self) + except: + _debug_write('error in repl loop') + _debug_write(traceback.format_exc()) + self.exit_process() + + time.sleep(2) # try and exit gracefully, then interrupt main if necessary + + if sys.platform == 'cli': + # just kill us as fast as possible + import System + System.Environment.Exit(1) + + self.interrupt_main() + + def check_for_exit_repl_loop(self): + return False + + def _cmd_run(self): + """runs the received snippet of code""" + self.run_command(read_string(self.conn)) + + def _cmd_abrt(self): + """aborts the current running command""" + # abort command, interrupts execution of the main thread. + self.interrupt_main() + + def _cmd_exit(self): + """exits the interactive process""" + self.exit_requested = True + self.exit_process() + + def _cmd_mems(self): + """gets the list of members available for the given expression""" + expression = read_string(self.conn) + try: + name, inst_members, type_members = self.get_members(expression) + except: + with self.send_lock: + write_bytes(self.conn, ReplBackend._MERR) + _debug_write('error in eval') + _debug_write(traceback.format_exc()) + else: + with self.send_lock: + write_bytes(self.conn, ReplBackend._MRES) + write_string(self.conn, name) + self._write_member_dict(inst_members) + self._write_member_dict(type_members) + + def _cmd_sigs(self): + """gets the signatures for the given expression""" + expression = read_string(self.conn) + try: + sigs = self.get_signatures(expression) + except: + with self.send_lock: + write_bytes(self.conn, ReplBackend._SERR) + _debug_write('error in eval') + _debug_write(traceback.format_exc()) + else: + with self.send_lock: + write_bytes(self.conn, ReplBackend._SRES) + # single overload + write_int(self.conn, len(sigs)) + for doc, args, vargs, varkw, defaults in sigs: + # write overload + write_string(self.conn, (doc or '')[:4096]) + arg_count = len(args) + (vargs is not None) + (varkw is not None) + write_int(self.conn, arg_count) + + def_values = [''] * (len(args) - len(defaults)) + ['=' + d for d in defaults] + for arg, def_value in zip(args, def_values): + write_string(self.conn, (arg or '') + def_value) + if vargs is not None: + write_string(self.conn, '*' + vargs) + if varkw is not None: + write_string(self.conn, '**' + varkw) + + def _cmd_setm(self): + global exec_mod + """sets the current module which code will execute against""" + mod_name = read_string(self.conn) + self.set_current_module(mod_name) + + def _cmd_sett(self): + """sets the current thread and frame which code will execute against""" + thread_id = read_int(self.conn) + frame_id = read_int(self.conn) + frame_kind = read_int(self.conn) + self.set_current_thread_and_frame(thread_id, frame_id, frame_kind) + + def _cmd_mods(self): + """gets the list of available modules""" + try: + res = self.get_module_names() + res.sort() + except: + res = [] + + with self.send_lock: + write_bytes(self.conn, ReplBackend._MODS) + write_int(self.conn, len(res)) + for name, filename in res: + write_string(self.conn, name) + write_string(self.conn, filename) + + def _cmd_inpl(self): + """handles the input command which returns a string of input""" + self.input_string = read_string(self.conn) + self.input_event.release() + + def _cmd_excf(self): + """handles executing a single file""" + filename = read_string(self.conn) + args = read_string(self.conn) + self.execute_file(filename, args) + + def _cmd_excx(self): + """handles executing a single file, module or process""" + filetype = read_string(self.conn) + filename = read_string(self.conn) + args = read_string(self.conn) + self.execute_file_ex(filetype, filename, args) + + def _cmd_debug_attach(self): + import visualstudio_py_debugger + port = read_int(self.conn) + id = read_string(self.conn) + debug_options = visualstudio_py_debugger.parse_debug_options(read_string(self.conn)) + debug_options.setdefault('rules', []).append({ + 'path': PY_ROOT, + 'include': False, + }) + self.attach_process(port, id, debug_options) + + _COMMANDS = { + to_bytes('run '): _cmd_run, + to_bytes('abrt'): _cmd_abrt, + to_bytes('exit'): _cmd_exit, + to_bytes('mems'): _cmd_mems, + to_bytes('sigs'): _cmd_sigs, + to_bytes('mods'): _cmd_mods, + to_bytes('setm'): _cmd_setm, + to_bytes('sett'): _cmd_sett, + to_bytes('inpl'): _cmd_inpl, + to_bytes('excf'): _cmd_excf, + to_bytes('excx'): _cmd_excx, + to_bytes('dbga'): _cmd_debug_attach, + } + + def _write_member_dict(self, mem_dict): + write_int(self.conn, len(mem_dict)) + for name, type_name in mem_dict.items(): + write_string(self.conn, name) + write_string(self.conn, type_name) + + def on_debugger_detach(self): + with self.send_lock: + write_bytes(self.conn, ReplBackend._DETC) + + def init_debugger(self): + from os import path + sys.path.append(path.dirname(__file__)) + import visualstudio_py_debugger + new_thread = visualstudio_py_debugger.new_thread() + sys.settrace(new_thread.trace_func) + visualstudio_py_debugger.intercept_threads(True) + + def send_image(self, filename): + with self.send_lock: + write_bytes(self.conn, ReplBackend._IMGD) + write_string(self.conn, filename) + + def write_png(self, image_bytes): + with self.send_lock: + write_bytes(self.conn, ReplBackend._DPNG) + write_int(self.conn, len(image_bytes)) + write_bytes(self.conn, image_bytes) + + def write_xaml(self, xaml_bytes): + with self.send_lock: + write_bytes(self.conn, ReplBackend._DXAM) + write_int(self.conn, len(xaml_bytes)) + write_bytes(self.conn, xaml_bytes) + + def send_prompt(self, ps1, ps2, allow_multiple_statements): + """sends the current prompt to the interactive window""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._PRPC) + write_string(self.conn, ps1) + write_string(self.conn, ps2) + write_int(self.conn, 1 if allow_multiple_statements else 0) + + def send_cwd(self): + """sends the current working directory""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._CHWD) + write_string(self.conn, os.getcwd()) + + def send_error(self): + """reports that an error occured to the interactive window""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._ERRE) + + def send_exit(self): + """reports the that the REPL process has exited to the interactive window""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._EXIT) + + def send_command_executed(self): + with self.send_lock: + write_bytes(self.conn, ReplBackend._DONE) + + def send_modules_changed(self): + with self.send_lock: + write_bytes(self.conn, ReplBackend._MODC) + + def read_line(self): + """reads a line of input from standard input""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._RDLN) + self.input_event.acquire() + return self.input_string + + def write_stdout(self, value): + """writes a string to standard output in the remote console""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._STDO) + write_string(self.conn, value) + + def write_stderr(self, value): + """writes a string to standard input in the remote console""" + with self.send_lock: + write_bytes(self.conn, ReplBackend._STDE) + write_string(self.conn, value) + + ################################################################ + + # Implementation of execution, etc... + + def execution_loop(self): + """starts processing execution requests""" + raise NotImplementedError + + def run_command(self, command): + """runs the specified command which is a string containing code""" + raise NotImplementedError + + def execute_file(self, filename, args): + """executes the given filename as the main module""" + return self.execute_file_ex('script', filename, args) + + def execute_file_ex(self, filetype, filename, args): + """executes the given filename as a 'script', 'module' or 'process'.""" + raise NotImplementedError + + def interrupt_main(self): + """aborts the current running command""" + raise NotImplementedError + + def exit_process(self): + """exits the REPL process""" + raise NotImplementedError + + def get_members(self, expression): + """returns a tuple of the type name, instance members, and type members""" + raise NotImplementedError + + def get_signatures(self, expression): + """returns doc, args, vargs, varkw, defaults.""" + raise NotImplementedError + + def set_current_module(self, module): + """sets the module which code executes against""" + raise NotImplementedError + + def set_current_thread_and_frame(self, thread_id, frame_id, frame_kind): + """sets the current thread and frame which code will execute against""" + raise NotImplementedError + + def get_module_names(self): + """returns a list of module names""" + raise NotImplementedError + + def flush(self): + """flushes the stdout/stderr buffers""" + raise NotImplementedError + + def attach_process(self, port, debugger_id, debug_options): + """starts processing execution requests""" + raise NotImplementedError + +def exit_work_item(): + sys.exit(0) diff --git a/src/test/pythonFiles/formatting/autoPep8Formatted.py b/src/test/pythonFiles/formatting/autoPep8Formatted.py new file mode 100644 index 000000000000..e63158d6d4fd --- /dev/null +++ b/src/test/pythonFiles/formatting/autoPep8Formatted.py @@ -0,0 +1,32 @@ + +import math +import sys + + +def example1(): + # This is a long comment. This should be wrapped to fit within 72 characters. + some_tuple = (1, 2, 3, 'a') + some_variable = {'long': 'Long code lines should be wrapped within 79 characters.', + 'other': [math.pi, 100, 200, 300, 9876543210, 'This is a long string that goes on'], + 'more': {'inner': 'This whole logical line should be wrapped.', some_tuple: [1, + 20, 300, 40000, 500000000, 60000000000000000]}} + return (some_tuple, some_variable) + + +def example2(): return {'has_key() is deprecated': True}.has_key( + {'f': 2}.has_key('')) + + +class Example3(object): + def __init__(self, bar): + # Comments should have a space after the hash. + if bar: + bar += 1 + bar = bar * bar + return bar + else: + some_string = """ + Indentation in multiline strings should not be touched. +Only actual code should be reindented. +""" + return (sys.path, some_string) diff --git a/src/test/pythonFiles/formatting/autopep8.output b/src/test/pythonFiles/formatting/autopep8.output new file mode 100644 index 000000000000..80cb3a445811 --- /dev/null +++ b/src/test/pythonFiles/formatting/autopep8.output @@ -0,0 +1,50 @@ +--- original/C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py ++++ fixed/C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py +@@ -1,22 +1,32 @@ + +-import math, sys; ++import math ++import sys ++ + + def example1(): +- ####This is a long comment. This should be wrapped to fit within 72 characters. +- some_tuple=( 1,2, 3,'a' ); +- some_variable={'long':'Long code lines should be wrapped within 79 characters.', +- 'other':[math.pi, 100,200,300,9876543210,'This is a long string that goes on'], +- 'more':{'inner':'This whole logical line should be wrapped.',some_tuple:[1, +- 20,300,40000,500000000,60000000000000000]}} ++ # This is a long comment. This should be wrapped to fit within 72 characters. ++ some_tuple = (1, 2, 3, 'a') ++ some_variable = {'long': 'Long code lines should be wrapped within 79 characters.', ++ 'other': [math.pi, 100, 200, 300, 9876543210, 'This is a long string that goes on'], ++ 'more': {'inner': 'This whole logical line should be wrapped.', some_tuple: [1, ++ 20, 300, 40000, 500000000, 60000000000000000]}} + return (some_tuple, some_variable) +-def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key('')); +-class Example3( object ): +- def __init__ ( self, bar ): +- #Comments should have a space after the hash. +- if bar : bar+=1; bar=bar* bar ; return bar +- else: +- some_string = """ ++ ++ ++def example2(): return {'has_key() is deprecated': True}.has_key( ++ {'f': 2}.has_key('')) ++ ++ ++class Example3(object): ++ def __init__(self, bar): ++ # Comments should have a space after the hash. ++ if bar: ++ bar += 1 ++ bar = bar * bar ++ return bar ++ else: ++ some_string = """ + Indentation in multiline strings should not be touched. + Only actual code should be reindented. + """ +- return (sys.path, some_string) ++ return (sys.path, some_string) \ No newline at end of file diff --git a/src/test/pythonFiles/formatting/black.output b/src/test/pythonFiles/formatting/black.output new file mode 100644 index 000000000000..4c14d61f2b9b --- /dev/null +++ b/src/test/pythonFiles/formatting/black.output @@ -0,0 +1,59 @@ +--- C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py 2020-05-11 18:56:39.835398 +0000 ++++ C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py 2020-05-11 19:05:50.969508 +0000 +@@ -1,23 +1,42 @@ ++import math, sys + +-import math, sys; + + def example1(): + ####This is a long comment. This should be wrapped to fit within 72 characters. +- some_tuple=( 1,2, 3,'a' ); +- some_variable={'long':'Long code lines should be wrapped within 79 characters.', +- 'other':[math.pi, 100,200,300,9876543210,'This is a long string that goes on'], +- 'more':{'inner':'This whole logical line should be wrapped.',some_tuple:[1, +- 20,300,40000,500000000,60000000000000000]}} ++ some_tuple = (1, 2, 3, "a") ++ some_variable = { ++ "long": "Long code lines should be wrapped within 79 characters.", ++ "other": [ ++ math.pi, ++ 100, ++ 200, ++ 300, ++ 9876543210, ++ "This is a long string that goes on", ++ ], ++ "more": { ++ "inner": "This whole logical line should be wrapped.", ++ some_tuple: [1, 20, 300, 40000, 500000000, 60000000000000000], ++ }, ++ } + return (some_tuple, some_variable) +-def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key('')); +-class Example3( object ): +- def __init__ ( self, bar ): +- #Comments should have a space after the hash. +- if bar : bar+=1; bar=bar* bar ; return bar +- else: +- some_string = """ ++ ++ ++def example2(): ++ return {"has_key() is deprecated": True}.has_key({"f": 2}.has_key("")) ++ ++ ++class Example3(object): ++ def __init__(self, bar): ++ # Comments should have a space after the hash. ++ if bar: ++ bar += 1 ++ bar = bar * bar ++ return bar ++ else: ++ some_string = """ + Indentation in multiline strings should not be touched. + Only actual code should be reindented. + """ +- return (sys.path, some_string) ++ return (sys.path, some_string) + \ No newline at end of file diff --git a/src/test/pythonFiles/formatting/blackFormatted.py b/src/test/pythonFiles/formatting/blackFormatted.py new file mode 100644 index 000000000000..e7bca8b1298c --- /dev/null +++ b/src/test/pythonFiles/formatting/blackFormatted.py @@ -0,0 +1,41 @@ +import math, sys + + +def example1(): + ####This is a long comment. This should be wrapped to fit within 72 characters. + some_tuple = (1, 2, 3, "a") + some_variable = { + "long": "Long code lines should be wrapped within 79 characters.", + "other": [ + math.pi, + 100, + 200, + 300, + 9876543210, + "This is a long string that goes on", + ], + "more": { + "inner": "This whole logical line should be wrapped.", + some_tuple: [1, 20, 300, 40000, 500000000, 60000000000000000], + }, + } + return (some_tuple, some_variable) + + +def example2(): + return {"has_key() is deprecated": True}.has_key({"f": 2}.has_key("")) + + +class Example3(object): + def __init__(self, bar): + # Comments should have a space after the hash. + if bar: + bar += 1 + bar = bar * bar + return bar + else: + some_string = """ + Indentation in multiline strings should not be touched. +Only actual code should be reindented. +""" + return (sys.path, some_string) diff --git a/src/test/pythonFiles/formatting/dummy.ts b/src/test/pythonFiles/formatting/dummy.ts new file mode 100644 index 000000000000..cbab6669e3b8 --- /dev/null +++ b/src/test/pythonFiles/formatting/dummy.ts @@ -0,0 +1,4 @@ +// Dummy ts file to ensure this folder gets created in output directory. + +// Code to ensure linter doesn't complain about empty files. +const a = '1'; diff --git a/src/test/pythonFiles/formatting/fileToFormat.py b/src/test/pythonFiles/formatting/fileToFormat.py new file mode 100644 index 000000000000..5b544bd8504d --- /dev/null +++ b/src/test/pythonFiles/formatting/fileToFormat.py @@ -0,0 +1,22 @@ + +import math, sys; + +def example1(): + ####This is a long comment. This should be wrapped to fit within 72 characters. + some_tuple=( 1,2, 3,'a' ); + some_variable={'long':'Long code lines should be wrapped within 79 characters.', + 'other':[math.pi, 100,200,300,9876543210,'This is a long string that goes on'], + 'more':{'inner':'This whole logical line should be wrapped.',some_tuple:[1, + 20,300,40000,500000000,60000000000000000]}} + return (some_tuple, some_variable) +def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key('')); +class Example3( object ): + def __init__ ( self, bar ): + #Comments should have a space after the hash. + if bar : bar+=1; bar=bar* bar ; return bar + else: + some_string = """ + Indentation in multiline strings should not be touched. +Only actual code should be reindented. +""" + return (sys.path, some_string) diff --git a/src/test/pythonFiles/formatting/fileToFormatOnEnter.py b/src/test/pythonFiles/formatting/fileToFormatOnEnter.py new file mode 100644 index 000000000000..8adfd1fa1233 --- /dev/null +++ b/src/test/pythonFiles/formatting/fileToFormatOnEnter.py @@ -0,0 +1,13 @@ +x=1 +"""x=1 +""" + # comment +# x=1 +x+1 # +@x +x.y +if x<=1: +if 1<=x: +def __init__(self, age = 23) +while(1) +x+""" diff --git a/src/test/pythonFiles/formatting/formatWhenDirty.py b/src/test/pythonFiles/formatting/formatWhenDirty.py new file mode 100644 index 000000000000..3fe1b80fde86 --- /dev/null +++ b/src/test/pythonFiles/formatting/formatWhenDirty.py @@ -0,0 +1,3 @@ +x = 0 +if x > 0: + x = 1 diff --git a/src/test/pythonFiles/formatting/formatWhenDirtyResult.py b/src/test/pythonFiles/formatting/formatWhenDirtyResult.py new file mode 100644 index 000000000000..d0ae06a2a59b --- /dev/null +++ b/src/test/pythonFiles/formatting/formatWhenDirtyResult.py @@ -0,0 +1,3 @@ +x = 0 +if x > 0: + x = 1 diff --git a/src/test/pythonFiles/formatting/pythonGrammar.py b/src/test/pythonFiles/formatting/pythonGrammar.py new file mode 100644 index 000000000000..937cba401d3f --- /dev/null +++ b/src/test/pythonFiles/formatting/pythonGrammar.py @@ -0,0 +1,1572 @@ +# Python test set -- part 1, grammar. +# This just tests whether the parser accepts them all. + +from test.support import check_syntax_error +import inspect +import unittest +import sys +# testing import * +from sys import * + +# different import patterns to check that __annotations__ does not interfere +# with import machinery +import test.ann_module as ann_module +import typing +from collections import ChainMap +from test import ann_module2 +import test + +# These are shared with test_tokenize and other test modules. +# +# Note: since several test cases filter out floats by looking for "e" and ".", +# don't add hexadecimal literals that contain "e" or "E". +VALID_UNDERSCORE_LITERALS = [ + '0_0_0', + '4_2', + '1_0000_0000', + '0b1001_0100', + '0xffff_ffff', + '0o5_7_7', + '1_00_00.5', + '1_00_00.5e5', + '1_00_00e5_1', + '1e1_0', + '.1_4', + '.1_4e1', + '0b_0', + '0x_f', + '0o_5', + '1_00_00j', + '1_00_00.5j', + '1_00_00e5_1j', + '.1_4j', + '(1_2.5+3_3j)', + '(.5_6j)', +] +INVALID_UNDERSCORE_LITERALS = [ + # Trailing underscores: + '0_', + '42_', + '1.4j_', + '0x_', + '0b1_', + '0xf_', + '0o5_', + '0 if 1_Else 1', + # Underscores in the base selector: + '0_b0', + '0_xf', + '0_o5', + # Old-style octal, still disallowed: + '0_7', + '09_99', + # Multiple consecutive underscores: + '4_______2', + '0.1__4', + '0.1__4j', + '0b1001__0100', + '0xffff__ffff', + '0x___', + '0o5__77', + '1e1__0', + '1e1__0j', + # Underscore right before a dot: + '1_.4', + '1_.4j', + # Underscore right after a dot: + '1._4', + '1._4j', + '._5', + '._5j', + # Underscore right after a sign: + '1.0e+_1', + '1.0e+_1j', + # Underscore right before j: + '1.4_j', + '1.4e5_j', + # Underscore right before e: + '1_e1', + '1.4_e1', + '1.4_e1j', + # Underscore right after e: + '1e_1', + '1.4e_1', + '1.4e_1j', + # Complex cases with parens: + '(1+1.5_j_)', + '(1+1.5_j)', +] + + +class TokenTests(unittest.TestCase): + + def test_backslash(self): + # Backslash means line continuation: + x = 1 \ + + 1 + self.assertEqual(x, 2, 'backslash for line continuation') + + # Backslash does not means continuation in comments :\ + x = 0 + self.assertEqual(x, 0, 'backslash ending comment') + + def test_plain_integers(self): + self.assertEqual(type(000), type(0)) + self.assertEqual(0xff, 255) + self.assertEqual(0o377, 255) + self.assertEqual(2147483647, 0o17777777777) + self.assertEqual(0b1001, 9) + # "0x" is not a valid literal + self.assertRaises(SyntaxError, eval, "0x") + from sys import maxsize + if maxsize == 2147483647: + self.assertEqual(-2147483647 - 1, -0o20000000000) + # XXX -2147483648 + self.assertTrue(0o37777777777 > 0) + self.assertTrue(0xffffffff > 0) + self.assertTrue(0b1111111111111111111111111111111 > 0) + for s in ('2147483648', '0o40000000000', '0x100000000', + '0b10000000000000000000000000000000'): + try: + x = eval(s) + except OverflowError: + self.fail("OverflowError on huge integer literal %r" % s) + elif maxsize == 9223372036854775807: + self.assertEqual(-9223372036854775807 - 1, -0o1000000000000000000000) + self.assertTrue(0o1777777777777777777777 > 0) + self.assertTrue(0xffffffffffffffff > 0) + self.assertTrue(0b11111111111111111111111111111111111111111111111111111111111111 > 0) + for s in '9223372036854775808', '0o2000000000000000000000', \ + '0x10000000000000000', \ + '0b100000000000000000000000000000000000000000000000000000000000000': + try: + x = eval(s) + except OverflowError: + self.fail("OverflowError on huge integer literal %r" % s) + else: + self.fail('Weird maxsize value %r' % maxsize) + + def test_long_integers(self): + x = 0 + x = 0xffffffffffffffff + x = 0Xffffffffffffffff + x = 0o77777777777777777 + x = 0O77777777777777777 + x = 123456789012345678901234567890 + x = 0b100000000000000000000000000000000000000000000000000000000000000000000 + x = 0B111111111111111111111111111111111111111111111111111111111111111111111 + + def test_floats(self): + x = 3.14 + x = 314. + x = 0.314 + # XXX x = 000.314 + x = .314 + x = 3e14 + x = 3E14 + x = 3e-14 + x = 3e+14 + x = 3.e14 + x = .3e14 + x = 3.1e4 + + def test_float_exponent_tokenization(self): + # See issue 21642. + self.assertEqual(1 if 1 else 0, 1) + self.assertEqual(1 if 0 else 0, 0) + self.assertRaises(SyntaxError, eval, "0 if 1Else 0") + + def test_underscore_literals(self): + for lit in VALID_UNDERSCORE_LITERALS: + self.assertEqual(eval(lit), eval(lit.replace('_', ''))) + for lit in INVALID_UNDERSCORE_LITERALS: + self.assertRaises(SyntaxError, eval, lit) + # Sanity check: no literal begins with an underscore + self.assertRaises(NameError, eval, "_0") + + def test_string_literals(self): + x = ''; y = ""; self.assertTrue(len(x) == 0 and x == y) + x = '\''; y = "'"; self.assertTrue(len(x) == 1 and x == y and ord(x) == 39) + x = '"'; y = "\""; self.assertTrue(len(x) == 1 and x == y and ord(x) == 34) + x = "doesn't \"shrink\" does it" + y = 'doesn\'t "shrink" does it' + self.assertTrue(len(x) == 24 and x == y) + x = "does \"shrink\" doesn't it" + y = 'does "shrink" doesn\'t it' + self.assertTrue(len(x) == 24 and x == y) + x = """ +The "quick" +brown fox +jumps over +the 'lazy' dog. +""" + y = '\nThe "quick"\nbrown fox\njumps over\nthe \'lazy\' dog.\n' + self.assertEqual(x, y) + y = ''' +The "quick" +brown fox +jumps over +the 'lazy' dog. +''' + self.assertEqual(x, y) + y = "\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the 'lazy' dog.\n\ +" + self.assertEqual(x, y) + y = '\n\ +The \"quick\"\n\ +brown fox\n\ +jumps over\n\ +the \'lazy\' dog.\n\ +' + self.assertEqual(x, y) + + def test_ellipsis(self): + x = ... + self.assertTrue(x is Ellipsis) + self.assertRaises(SyntaxError, eval, ".. .") + + def test_eof_error(self): + samples = ("def foo(", "\ndef foo(", "def foo(\n") + for s in samples: + with self.assertRaises(SyntaxError) as cm: + compile(s, "<test>", "exec") + self.assertIn("unexpected EOF", str(cm.exception)) + +var_annot_global: int # a global annotated is necessary for test_var_annot + +# custom namespace for testing __annotations__ + +class CNS: + def __init__(self): + self._dct = {} + def __setitem__(self, item, value): + self._dct[item.lower()] = value + def __getitem__(self, item): + return self._dct[item] + + +class GrammarTests(unittest.TestCase): + + check_syntax_error = check_syntax_error + + # single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE + # XXX can't test in a script -- this rule is only used when interactive + + # file_input: (NEWLINE | stmt)* ENDMARKER + # Being tested as this very moment this very module + + # expr_input: testlist NEWLINE + # XXX Hard to test -- used only in calls to input() + + def test_eval_input(self): + # testlist ENDMARKER + x = eval('1, 0 or 1') + + def test_var_annot_basics(self): + # all these should be allowed + var1: int = 5 + var2: [int, str] + my_lst = [42] + def one(): + return 1 + int.new_attr: int + [list][0]: type + my_lst[one() - 1]: int = 5 + self.assertEqual(my_lst, [5]) + + def test_var_annot_syntax_errors(self): + # parser pass + check_syntax_error(self, "def f: int") + check_syntax_error(self, "x: int: str") + check_syntax_error(self, "def f():\n" + " nonlocal x: int\n") + # AST pass + check_syntax_error(self, "[x, 0]: int\n") + check_syntax_error(self, "f(): int\n") + check_syntax_error(self, "(x,): int") + check_syntax_error(self, "def f():\n" + " (x, y): int = (1, 2)\n") + # symtable pass + check_syntax_error(self, "def f():\n" + " x: int\n" + " global x\n") + check_syntax_error(self, "def f():\n" + " global x\n" + " x: int\n") + + def test_var_annot_basic_semantics(self): + # execution order + with self.assertRaises(ZeroDivisionError): + no_name[does_not_exist]: no_name_again = 1 / 0 + with self.assertRaises(NameError): + no_name[does_not_exist]: 1 / 0 = 0 + global var_annot_global + + # function semantics + def f(): + st: str = "Hello" + a.b: int = (1, 2) + return st + self.assertEqual(f.__annotations__, {}) + def f_OK(): + x: 1 / 0 + f_OK() + def fbad(): + x: int + print(x) + with self.assertRaises(UnboundLocalError): + fbad() + def f2bad(): + (no_such_global): int + print(no_such_global) + try: + f2bad() + except Exception as e: + self.assertIs(type(e), NameError) + + # class semantics + class C: + __foo: int + s: str = "attr" + z = 2 + def __init__(self, x): + self.x: int = x + self.assertEqual(C.__annotations__, {'_C__foo': int, 's': str}) + with self.assertRaises(NameError): + class CBad: + no_such_name_defined.attr: int = 0 + with self.assertRaises(NameError): + class Cbad2(C): + x: int + x.y: list = [] + + def test_var_annot_metaclass_semantics(self): + class CMeta(type): + @classmethod + def __prepare__(metacls, name, bases, **kwds): + return {'__annotations__': CNS()} + class CC(metaclass=CMeta): + XX: 'ANNOT' + self.assertEqual(CC.__annotations__['xx'], 'ANNOT') + + def test_var_annot_module_semantics(self): + with self.assertRaises(AttributeError): + print(test.__annotations__) + self.assertEqual(ann_module.__annotations__, + {1: 2, 'x': int, 'y': str, 'f': typing.Tuple[int, int]}) + self.assertEqual(ann_module.M.__annotations__, + {'123': 123, 'o': type}) + self.assertEqual(ann_module2.__annotations__, {}) + + def test_var_annot_in_module(self): + # check that functions fail the same way when executed + # outside of module where they were defined + from test.ann_module3 import f_bad_ann, g_bad_ann, D_bad_ann + with self.assertRaises(NameError): + f_bad_ann() + with self.assertRaises(NameError): + g_bad_ann() + with self.assertRaises(NameError): + D_bad_ann(5) + + def test_var_annot_simple_exec(self): + gns = {}; lns = {} + exec("'docstring'\n" + "__annotations__[1] = 2\n" + "x: int = 5\n", gns, lns) + self.assertEqual(lns["__annotations__"], {1: 2, 'x': int}) + with self.assertRaises(KeyError): + gns['__annotations__'] + + def test_var_annot_custom_maps(self): + # tests with custom locals() and __annotations__ + ns = {'__annotations__': CNS()} + exec('X: int; Z: str = "Z"; (w): complex = 1j', ns) + self.assertEqual(ns['__annotations__']['x'], int) + self.assertEqual(ns['__annotations__']['z'], str) + with self.assertRaises(KeyError): + ns['__annotations__']['w'] + nonloc_ns = {} + class CNS2: + def __init__(self): + self._dct = {} + def __setitem__(self, item, value): + nonlocal nonloc_ns + self._dct[item] = value + nonloc_ns[item] = value + def __getitem__(self, item): + return self._dct[item] + exec('x: int = 1', {}, CNS2()) + self.assertEqual(nonloc_ns['__annotations__']['x'], int) + + def test_var_annot_refleak(self): + # complex case: custom locals plus custom __annotations__ + # this was causing refleak + cns = CNS() + nonloc_ns = {'__annotations__': cns} + class CNS2: + def __init__(self): + self._dct = {'__annotations__': cns} + def __setitem__(self, item, value): + nonlocal nonloc_ns + self._dct[item] = value + nonloc_ns[item] = value + def __getitem__(self, item): + return self._dct[item] + exec('X: str', {}, CNS2()) + self.assertEqual(nonloc_ns['__annotations__']['x'], str) + + def test_funcdef(self): + ### [decorators] 'def' NAME parameters ['->' test] ':' suite + ### decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE + ### decorators: decorator+ + ### parameters: '(' [typedargslist] ')' + ### typedargslist: ((tfpdef ['=' test] ',')* + ### ('*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef) + ### | tfpdef ['=' test] (',' tfpdef ['=' test])* [',']) + ### tfpdef: NAME [':' test] + ### varargslist: ((vfpdef ['=' test] ',')* + ### ('*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef) + ### | vfpdef ['=' test] (',' vfpdef ['=' test])* [',']) + ### vfpdef: NAME + def f1(): pass + f1() + f1(*()) + f1(*(), **{}) + def f2(one_argument): pass + def f3(two, arguments): pass + self.assertEqual(f2.__code__.co_varnames, ('one_argument',)) + self.assertEqual(f3.__code__.co_varnames, ('two', 'arguments')) + def a1(one_arg,): pass + def a2(two, args,): pass + def v0(*rest): pass + def v1(a, *rest): pass + def v2(a, b, *rest): pass + + f1() + f2(1) + f2(1,) + f3(1, 2) + f3(1, 2,) + v0() + v0(1) + v0(1,) + v0(1, 2) + v0(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) + v1(1) + v1(1,) + v1(1, 2) + v1(1, 2, 3) + v1(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) + v2(1, 2) + v2(1, 2, 3) + v2(1, 2, 3, 4) + v2(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) + + def d01(a=1): pass + d01() + d01(1) + d01(*(1,)) + d01(*[] or [2]) + d01(*() or (), *{} and (), **() or {}) + d01(**{'a': 2}) + d01(**{'a': 2} or {}) + def d11(a, b=1): pass + d11(1) + d11(1, 2) + d11(1, **{'b': 2}) + def d21(a, b, c=1): pass + d21(1, 2) + d21(1, 2, 3) + d21(*(1, 2, 3)) + d21(1, *(2, 3)) + d21(1, 2, *(3,)) + d21(1, 2, **{'c': 3}) + def d02(a=1, b=2): pass + d02() + d02(1) + d02(1, 2) + d02(*(1, 2)) + d02(1, *(2,)) + d02(1, **{'b': 2}) + d02(**{'a': 1, 'b': 2}) + def d12(a, b=1, c=2): pass + d12(1) + d12(1, 2) + d12(1, 2, 3) + def d22(a, b, c=1, d=2): pass + d22(1, 2) + d22(1, 2, 3) + d22(1, 2, 3, 4) + def d01v(a=1, *rest): pass + d01v() + d01v(1) + d01v(1, 2) + d01v(*(1, 2, 3, 4)) + d01v(*(1,)) + d01v(**{'a': 2}) + def d11v(a, b=1, *rest): pass + d11v(1) + d11v(1, 2) + d11v(1, 2, 3) + def d21v(a, b, c=1, *rest): pass + d21v(1, 2) + d21v(1, 2, 3) + d21v(1, 2, 3, 4) + d21v(*(1, 2, 3, 4)) + d21v(1, 2, **{'c': 3}) + def d02v(a=1, b=2, *rest): pass + d02v() + d02v(1) + d02v(1, 2) + d02v(1, 2, 3) + d02v(1, *(2, 3, 4)) + d02v(**{'a': 1, 'b': 2}) + def d12v(a, b=1, c=2, *rest): pass + d12v(1) + d12v(1, 2) + d12v(1, 2, 3) + d12v(1, 2, 3, 4) + d12v(*(1, 2, 3, 4)) + d12v(1, 2, *(3, 4, 5)) + d12v(1, *(2,), **{'c': 3}) + def d22v(a, b, c=1, d=2, *rest): pass + d22v(1, 2) + d22v(1, 2, 3) + d22v(1, 2, 3, 4) + d22v(1, 2, 3, 4, 5) + d22v(*(1, 2, 3, 4)) + d22v(1, 2, *(3, 4, 5)) + d22v(1, *(2, 3), **{'d': 4}) + + # keyword argument type tests + try: + str('x', **{b'foo': 1}) + except TypeError: + pass + else: + self.fail('Bytes should not work as keyword argument names') + # keyword only argument tests + def pos0key1(*, key): return key + pos0key1(key=100) + def pos2key2(p1, p2, *, k1, k2=100): return p1, p2, k1, k2 + pos2key2(1, 2, k1=100) + pos2key2(1, 2, k1=100, k2=200) + pos2key2(1, 2, k2=100, k1=200) + def pos2key2dict(p1, p2, *, k1=100, k2, **kwarg): return p1, p2, k1, k2, kwarg + pos2key2dict(1, 2, k2=100, tokwarg1=100, tokwarg2=200) + pos2key2dict(1, 2, tokwarg1=100, tokwarg2=200, k2=100) + + self.assertRaises(SyntaxError, eval, "def f(*): pass") + self.assertRaises(SyntaxError, eval, "def f(*,): pass") + self.assertRaises(SyntaxError, eval, "def f(*, **kwds): pass") + + # keyword arguments after *arglist + def f(*args, **kwargs): + return args, kwargs + self.assertEqual(f(1, x=2, *[3, 4], y=5), ((1, 3, 4), + {'x': 2, 'y': 5})) + self.assertEqual(f(1, *(2, 3), 4), ((1, 2, 3, 4), {})) + self.assertRaises(SyntaxError, eval, "f(1, x=2, *(3,4), x=5)") + self.assertEqual(f(**{'eggs': 'scrambled', 'spam': 'fried'}), + ((), {'eggs': 'scrambled', 'spam': 'fried'})) + self.assertEqual(f(spam='fried', **{'eggs': 'scrambled'}), + ((), {'eggs': 'scrambled', 'spam': 'fried'})) + + # Check ast errors in *args and *kwargs + check_syntax_error(self, "f(*g(1=2))") + check_syntax_error(self, "f(**g(1=2))") + + # argument annotation tests + def f(x) -> list: pass + self.assertEqual(f.__annotations__, {'return': list}) + def f(x: int): pass + self.assertEqual(f.__annotations__, {'x': int}) + def f(*x: str): pass + self.assertEqual(f.__annotations__, {'x': str}) + def f(**x: float): pass + self.assertEqual(f.__annotations__, {'x': float}) + def f(x, y: 1 + 2): pass + self.assertEqual(f.__annotations__, {'y': 3}) + def f(a, b: 1, c: 2, d): pass + self.assertEqual(f.__annotations__, {'b': 1, 'c': 2}) + def f(a, b: 1, c: 2, d, e: 3 = 4, f=5, *g: 6): pass + self.assertEqual(f.__annotations__, + {'b': 1, 'c': 2, 'e': 3, 'g': 6}) + def f(a, b: 1, c: 2, d, e: 3 = 4, f=5, *g: 6, h: 7, i=8, j: 9 = 10, + **k: 11) -> 12: pass + self.assertEqual(f.__annotations__, + {'b': 1, 'c': 2, 'e': 3, 'g': 6, 'h': 7, 'j': 9, + 'k': 11, 'return': 12}) + # Check for issue #20625 -- annotations mangling + class Spam: + def f(self, *, __kw: 1): + pass + class Ham(Spam): pass + self.assertEqual(Spam.f.__annotations__, {'_Spam__kw': 1}) + self.assertEqual(Ham.f.__annotations__, {'_Spam__kw': 1}) + # Check for SF Bug #1697248 - mixing decorators and a return annotation + def null(x): return x + @null + def f(x) -> list: pass + self.assertEqual(f.__annotations__, {'return': list}) + + # test closures with a variety of opargs + closure = 1 + def f(): return closure + def f(x=1): return closure + def f(*, k=1): return closure + def f() -> int: return closure + + # Check trailing commas are permitted in funcdef argument list + def f(a,): pass + def f(*args,): pass + def f(**kwds,): pass + def f(a, *args,): pass + def f(a, **kwds,): pass + def f(*args, b,): pass + def f(*, b,): pass + def f(*args, **kwds,): pass + def f(a, *args, b,): pass + def f(a, *, b,): pass + def f(a, *args, **kwds,): pass + def f(*args, b, **kwds,): pass + def f(*, b, **kwds,): pass + def f(a, *args, b, **kwds,): pass + def f(a, *, b, **kwds,): pass + + def test_lambdef(self): + ### lambdef: 'lambda' [varargslist] ':' test + l1 = lambda: 0 + self.assertEqual(l1(), 0) + l2 = lambda: a[d] # XXX just testing the expression + l3 = lambda: [2 < x for x in [-1, 3, 0]] + self.assertEqual(l3(), [0, 1, 0]) + l4 = lambda x=lambda y=lambda z=1: z: y(): x() + self.assertEqual(l4(), 1) + l5 = lambda x, y, z=2: x + y + z + self.assertEqual(l5(1, 2), 5) + self.assertEqual(l5(1, 2, 3), 6) + check_syntax_error(self, "lambda x: x = 2") + check_syntax_error(self, "lambda (None,): None") + l6 = lambda x, y, *, k=20: x + y + k + self.assertEqual(l6(1, 2), 1 + 2 + 20) + self.assertEqual(l6(1, 2, k=10), 1 + 2 + 10) + + # check that trailing commas are permitted + l10 = lambda a,: 0 + l11 = lambda *args,: 0 + l12 = lambda **kwds,: 0 + l13 = lambda a, *args,: 0 + l14 = lambda a, **kwds,: 0 + l15 = lambda *args, b,: 0 + l16 = lambda *, b,: 0 + l17 = lambda *args, **kwds,: 0 + l18 = lambda a, *args, b,: 0 + l19 = lambda a, *, b,: 0 + l20 = lambda a, *args, **kwds,: 0 + l21 = lambda *args, b, **kwds,: 0 + l22 = lambda *, b, **kwds,: 0 + l23 = lambda a, *args, b, **kwds,: 0 + l24 = lambda a, *, b, **kwds,: 0 + + + ### stmt: simple_stmt | compound_stmt + # Tested below + + def test_simple_stmt(self): + ### simple_stmt: small_stmt (';' small_stmt)* [';'] + x = 1; pass; del x + def foo(): + # verify statements that end with semi-colons + x = 1; pass; del x; + foo() + + ### small_stmt: expr_stmt | pass_stmt | del_stmt | flow_stmt | import_stmt | global_stmt | access_stmt + # Tested below + + def test_expr_stmt(self): + # (exprlist '=')* exprlist + 1 + 1, 2, 3 + x = 1 + x = 1, 2, 3 + x = y = z = 1, 2, 3 + x, y, z = 1, 2, 3 + abc = a, b, c = x, y, z = xyz = 1, 2, (3, 4) + + check_syntax_error(self, "x + 1 = 1") + check_syntax_error(self, "a + 1 = b + 2") + + # Check the heuristic for print & exec covers significant cases + # As well as placing some limits on false positives + def test_former_statements_refer_to_builtins(self): + keywords = "print", "exec" + # Cases where we want the custom error + cases = [ + "{} foo", + "{} {{1:foo}}", + "if 1: {} foo", + "if 1: {} {{1:foo}}", + "if 1:\n {} foo", + "if 1:\n {} {{1:foo}}", + ] + for keyword in keywords: + custom_msg = "call to '{}'".format(keyword) + for case in cases: + source = case.format(keyword) + with self.subTest(source=source): + with self.assertRaisesRegex(SyntaxError, custom_msg): + exec(source) + source = source.replace("foo", "(foo.)") + with self.subTest(source=source): + with self.assertRaisesRegex(SyntaxError, "invalid syntax"): + exec(source) + + def test_del_stmt(self): + # 'del' exprlist + abc = [1, 2, 3] + x, y, z = abc + xyz = x, y, z + + del abc + del x, y, (z, xyz) + + def test_pass_stmt(self): + # 'pass' + pass + + # flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt + # Tested below + + def test_break_stmt(self): + # 'break' + while 1: break + + def test_continue_stmt(self): + # 'continue' + i = 1 + while i: i = 0; continue + + msg = "" + while not msg: + msg = "ok" + try: + continue + msg = "continue failed to continue inside try" + except: + msg = "continue inside try called except block" + if msg != "ok": + self.fail(msg) + + msg = "" + while not msg: + msg = "finally block not called" + try: + continue + finally: + msg = "ok" + if msg != "ok": + self.fail(msg) + + def test_break_continue_loop(self): + # This test warrants an explanation. It is a test specifically for SF bugs + # #463359 and #462937. The bug is that a 'break' statement executed or + # exception raised inside a try/except inside a loop, *after* a continue + # statement has been executed in that loop, will cause the wrong number of + # arguments to be popped off the stack and the instruction pointer reset to + # a very small number (usually 0.) Because of this, the following test + # *must* written as a function, and the tracking vars *must* be function + # arguments with default values. Otherwise, the test will loop and loop. + + def test_inner(extra_burning_oil=1, count=0): + big_hippo = 2 + while big_hippo: + count += 1 + try: + if extra_burning_oil and big_hippo == 1: + extra_burning_oil -= 1 + break + big_hippo -= 1 + continue + except: + raise + if count > 2 or big_hippo != 1: + self.fail("continue then break in try/except in loop broken!") + test_inner() + + def test_return(self): + # 'return' [testlist] + def g1(): return + def g2(): return 1 + g1() + x = g2() + check_syntax_error(self, "class foo:return 1") + + def test_break_in_finally(self): + count = 0 + while count < 2: + count += 1 + try: + pass + finally: + break + self.assertEqual(count, 1) + + count = 0 + while count < 2: + count += 1 + try: + continue + finally: + break + self.assertEqual(count, 1) + + count = 0 + while count < 2: + count += 1 + try: + 1 / 0 + finally: + break + self.assertEqual(count, 1) + + for count in [0, 1]: + self.assertEqual(count, 0) + try: + pass + finally: + break + self.assertEqual(count, 0) + + for count in [0, 1]: + self.assertEqual(count, 0) + try: + continue + finally: + break + self.assertEqual(count, 0) + + for count in [0, 1]: + self.assertEqual(count, 0) + try: + 1 / 0 + finally: + break + self.assertEqual(count, 0) + + def test_continue_in_finally(self): + count = 0 + while count < 2: + count += 1 + try: + pass + finally: + continue + break + self.assertEqual(count, 2) + + count = 0 + while count < 2: + count += 1 + try: + break + finally: + continue + self.assertEqual(count, 2) + + count = 0 + while count < 2: + count += 1 + try: + 1 / 0 + finally: + continue + break + self.assertEqual(count, 2) + + for count in [0, 1]: + try: + pass + finally: + continue + break + self.assertEqual(count, 1) + + for count in [0, 1]: + try: + break + finally: + continue + self.assertEqual(count, 1) + + for count in [0, 1]: + try: + 1 / 0 + finally: + continue + break + self.assertEqual(count, 1) + + def test_return_in_finally(self): + def g1(): + try: + pass + finally: + return 1 + self.assertEqual(g1(), 1) + + def g2(): + try: + return 2 + finally: + return 3 + self.assertEqual(g2(), 3) + + def g3(): + try: + 1 / 0 + finally: + return 4 + self.assertEqual(g3(), 4) + + def test_yield(self): + # Allowed as standalone statement + def g(): yield 1 + def g(): yield from () + # Allowed as RHS of assignment + def g(): x = yield 1 + def g(): x = yield from () + # Ordinary yield accepts implicit tuples + def g(): yield 1, 1 + def g(): x = yield 1, 1 + # 'yield from' does not + check_syntax_error(self, "def g(): yield from (), 1") + check_syntax_error(self, "def g(): x = yield from (), 1") + # Requires parentheses as subexpression + def g(): 1, (yield 1) + def g(): 1, (yield from ()) + check_syntax_error(self, "def g(): 1, yield 1") + check_syntax_error(self, "def g(): 1, yield from ()") + # Requires parentheses as call argument + def g(): f((yield 1)) + def g(): f((yield 1), 1) + def g(): f((yield from ())) + def g(): f((yield from ()), 1) + check_syntax_error(self, "def g(): f(yield 1)") + check_syntax_error(self, "def g(): f(yield 1, 1)") + check_syntax_error(self, "def g(): f(yield from ())") + check_syntax_error(self, "def g(): f(yield from (), 1)") + # Not allowed at top level + check_syntax_error(self, "yield") + check_syntax_error(self, "yield from") + # Not allowed at class scope + check_syntax_error(self, "class foo:yield 1") + check_syntax_error(self, "class foo:yield from ()") + # Check annotation refleak on SyntaxError + check_syntax_error(self, "def g(a:(yield)): pass") + + def test_yield_in_comprehensions(self): + # Check yield in comprehensions + def g(): [x for x in [(yield 1)]] + def g(): [x for x in [(yield from ())]] + + check = self.check_syntax_error + check("def g(): [(yield x) for x in ()]", + "'yield' inside list comprehension") + check("def g(): [x for x in () if not (yield x)]", + "'yield' inside list comprehension") + check("def g(): [y for x in () for y in [(yield x)]]", + "'yield' inside list comprehension") + check("def g(): {(yield x) for x in ()}", + "'yield' inside set comprehension") + check("def g(): {(yield x): x for x in ()}", + "'yield' inside dict comprehension") + check("def g(): {x: (yield x) for x in ()}", + "'yield' inside dict comprehension") + check("def g(): ((yield x) for x in ())", + "'yield' inside generator expression") + check("def g(): [(yield from x) for x in ()]", + "'yield' inside list comprehension") + check("class C: [(yield x) for x in ()]", + "'yield' inside list comprehension") + check("[(yield x) for x in ()]", + "'yield' inside list comprehension") + + def test_raise(self): + # 'raise' test [',' test] + try: raise RuntimeError('just testing') + except RuntimeError: pass + try: raise KeyboardInterrupt + except KeyboardInterrupt: pass + + def test_import(self): + # 'import' dotted_as_names + import sys + import time, sys + # 'from' dotted_name 'import' ('*' | '(' import_as_names ')' | import_as_names) + from time import time + from time import (time) + # not testable inside a function, but already done at top of the module + # from sys import * + from sys import path, argv + from sys import (path, argv) + from sys import (path, argv,) + + def test_global(self): + # 'global' NAME (',' NAME)* + global a + global a, b + global one, two, three, four, five, six, seven, eight, nine, ten + + def test_nonlocal(self): + # 'nonlocal' NAME (',' NAME)* + x = 0 + y = 0 + def f(): + nonlocal x + nonlocal x, y + + def test_assert(self): + # assertTruestmt: 'assert' test [',' test] + assert 1 + assert 1, 1 + assert lambda x: x + assert 1, lambda x: x + 1 + + try: + assert True + except AssertionError as e: + self.fail("'assert True' should not have raised an AssertionError") + + try: + assert True, 'this should always pass' + except AssertionError as e: + self.fail("'assert True, msg' should not have " + "raised an AssertionError") + + # these tests fail if python is run with -O, so check __debug__ + @unittest.skipUnless(__debug__, "Won't work if __debug__ is False") + def testAssert2(self): + try: + assert 0, "msg" + except AssertionError as e: + self.assertEqual(e.args[0], "msg") + else: + self.fail("AssertionError not raised by assert 0") + + try: + assert False + except AssertionError as e: + self.assertEqual(len(e.args), 0) + else: + self.fail("AssertionError not raised by 'assert False'") + + + ### compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | funcdef | classdef + # Tested below + + def test_if(self): + # 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite] + if 1: pass + if 1: pass + else: pass + if 0: pass + elif 0: pass + if 0: pass + elif 0: pass + elif 0: pass + elif 0: pass + else: pass + + def test_while(self): + # 'while' test ':' suite ['else' ':' suite] + while 0: pass + while 0: pass + else: pass + + # Issue1920: "while 0" is optimized away, + # ensure that the "else" clause is still present. + x = 0 + while 0: + x = 1 + else: + x = 2 + self.assertEqual(x, 2) + + def test_for(self): + # 'for' exprlist 'in' exprlist ':' suite ['else' ':' suite] + for i in 1, 2, 3: pass + for i, j, k in (): pass + else: pass + class Squares: + def __init__(self, max): + self.max = max + self.sofar = [] + def __len__(self): return len(self.sofar) + def __getitem__(self, i): + if not 0 <= i < self.max: raise IndexError + n = len(self.sofar) + while n <= i: + self.sofar.append(n * n) + n = n + 1 + return self.sofar[i] + n = 0 + for x in Squares(10): n = n + x + if n != 285: + self.fail('for over growing sequence') + + result = [] + for x, in [(1,), (2,), (3,)]: + result.append(x) + self.assertEqual(result, [1, 2, 3]) + + def test_try(self): + ### try_stmt: 'try' ':' suite (except_clause ':' suite)+ ['else' ':' suite] + ### | 'try' ':' suite 'finally' ':' suite + ### except_clause: 'except' [expr ['as' expr]] + try: + 1 / 0 + except ZeroDivisionError: + pass + else: + pass + try: 1 / 0 + except EOFError: pass + except TypeError as msg: pass + except: pass + else: pass + try: 1 / 0 + except (EOFError, TypeError, ZeroDivisionError): pass + try: 1 / 0 + except (EOFError, TypeError, ZeroDivisionError) as msg: pass + try: pass + finally: pass + + def test_suite(self): + # simple_stmt | NEWLINE INDENT NEWLINE* (stmt NEWLINE*)+ DEDENT + if 1: pass + if 1: + pass + if 1: + # + # + # + pass + pass + # + pass + # + + def test_test(self): + ### and_test ('or' and_test)* + ### and_test: not_test ('and' not_test)* + ### not_test: 'not' not_test | comparison + if not 1: pass + if 1 and 1: pass + if 1 or 1: pass + if not not not 1: pass + if not 1 and 1 and 1: pass + if 1 and 1 or 1 and 1 and 1 or not 1 and 1: pass + + def test_comparison(self): + ### comparison: expr (comp_op expr)* + ### comp_op: '<'|'>'|'=='|'>='|'<='|'!='|'in'|'not' 'in'|'is'|'is' 'not' + if 1: pass + x = (1 == 1) + if 1 == 1: pass + if 1 != 1: pass + if 1 < 1: pass + if 1 > 1: pass + if 1 <= 1: pass + if 1 >= 1: pass + if 1 is 1: pass + if 1 is not 1: pass + if 1 in (): pass + if 1 not in (): pass + if 1 < 1 > 1 == 1 >= 1 <= 1 != 1 in 1 not in 1 is 1 is not 1: pass + + def test_binary_mask_ops(self): + x = 1 & 1 + x = 1 ^ 1 + x = 1 | 1 + + def test_shift_ops(self): + x = 1 << 1 + x = 1 >> 1 + x = 1 << 1 >> 1 + + def test_additive_ops(self): + x = 1 + x = 1 + 1 + x = 1 - 1 - 1 + x = 1 - 1 + 1 - 1 + 1 + + def test_multiplicative_ops(self): + x = 1 * 1 + x = 1 / 1 + x = 1 % 1 + x = 1 / 1 * 1 % 1 + + def test_unary_ops(self): + x = +1 + x = -1 + x = ~1 + x = ~1 ^ 1 & 1 | 1 & 1 ^ -1 + x = -1 * 1 / 1 + 1 * 1 - -1 * 1 + + def test_selectors(self): + ### trailer: '(' [testlist] ')' | '[' subscript ']' | '.' NAME + ### subscript: expr | [expr] ':' [expr] + + import sys, time + c = sys.path[0] + x = time.time() + x = sys.modules['time'].time() + a = '01234' + c = a[0] + c = a[-1] + s = a[0:5] + s = a[:5] + s = a[0:] + s = a[:] + s = a[-5:] + s = a[:-1] + s = a[-4:-3] + # A rough test of SF bug 1333982. http://python.org/sf/1333982 + # The testing here is fairly incomplete. + # Test cases should include: commas with 1 and 2 colons + d = {} + d[1] = 1 + d[1,] = 2 + d[1, 2] = 3 + d[1, 2, 3] = 4 + L = list(d) + L.sort(key=lambda x: (type(x).__name__, x)) + self.assertEqual(str(L), '[1, (1,), (1, 2), (1, 2, 3)]') + + def test_atoms(self): + ### atom: '(' [testlist] ')' | '[' [testlist] ']' | '{' [dictsetmaker] '}' | NAME | NUMBER | STRING + ### dictsetmaker: (test ':' test (',' test ':' test)* [',']) | (test (',' test)* [',']) + + x = (1) + x = (1 or 2 or 3) + x = (1 or 2 or 3, 2, 3) + + x = [] + x = [1] + x = [1 or 2 or 3] + x = [1 or 2 or 3, 2, 3] + x = [] + + x = {} + x = {'one': 1} + x = {'one': 1,} + x = {'one' or 'two': 1 or 2} + x = {'one': 1, 'two': 2} + x = {'one': 1, 'two': 2,} + x = {'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6} + + x = {'one'} + x = {'one', 1,} + x = {'one', 'two', 'three'} + x = {2, 3, 4,} + + x = x + x = 'x' + x = 123 + + ### exprlist: expr (',' expr)* [','] + ### testlist: test (',' test)* [','] + # These have been exercised enough above + + def test_classdef(self): + # 'class' NAME ['(' [testlist] ')'] ':' suite + class B: pass + class B2(): pass + class C1(B): pass + class C2(B): pass + class D(C1, C2, B): pass + class C: + def meth1(self): pass + def meth2(self, arg): pass + def meth3(self, a1, a2): pass + + # decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE + # decorators: decorator+ + # decorated: decorators (classdef | funcdef) + def class_decorator(x): return x + @class_decorator + class G: pass + + def test_dictcomps(self): + # dictorsetmaker: ( (test ':' test (comp_for | + # (',' test ':' test)* [','])) | + # (test (comp_for | (',' test)* [','])) ) + nums = [1, 2, 3] + self.assertEqual({i: i + 1 for i in nums}, {1: 2, 2: 3, 3: 4}) + + def test_listcomps(self): + # list comprehension tests + nums = [1, 2, 3, 4, 5] + strs = ["Apple", "Banana", "Coconut"] + spcs = [" Apple", " Banana ", "Coco nut "] + + self.assertEqual([s.strip() for s in spcs], ['Apple', 'Banana', 'Coco nut']) + self.assertEqual([3 * x for x in nums], [3, 6, 9, 12, 15]) + self.assertEqual([x for x in nums if x > 2], [3, 4, 5]) + self.assertEqual([(i, s) for i in nums for s in strs], + [(1, 'Apple'), (1, 'Banana'), (1, 'Coconut'), + (2, 'Apple'), (2, 'Banana'), (2, 'Coconut'), + (3, 'Apple'), (3, 'Banana'), (3, 'Coconut'), + (4, 'Apple'), (4, 'Banana'), (4, 'Coconut'), + (5, 'Apple'), (5, 'Banana'), (5, 'Coconut')]) + self.assertEqual([(i, s) for i in nums for s in [f for f in strs if "n" in f]], + [(1, 'Banana'), (1, 'Coconut'), (2, 'Banana'), (2, 'Coconut'), + (3, 'Banana'), (3, 'Coconut'), (4, 'Banana'), (4, 'Coconut'), + (5, 'Banana'), (5, 'Coconut')]) + self.assertEqual([(lambda a:[a ** i for i in range(a + 1)])(j) for j in range(5)], + [[1], [1, 1], [1, 2, 4], [1, 3, 9, 27], [1, 4, 16, 64, 256]]) + + def test_in_func(l): + return [0 < x < 3 for x in l if x > 2] + + self.assertEqual(test_in_func(nums), [False, False, False]) + + def test_nested_front(): + self.assertEqual([[y for y in [x, x + 1]] for x in [1, 3, 5]], + [[1, 2], [3, 4], [5, 6]]) + + test_nested_front() + + check_syntax_error(self, "[i, s for i in nums for s in strs]") + check_syntax_error(self, "[x if y]") + + suppliers = [ + (1, "Boeing"), + (2, "Ford"), + (3, "Macdonalds") + ] + + parts = [ + (10, "Airliner"), + (20, "Engine"), + (30, "Cheeseburger") + ] + + suppart = [ + (1, 10), (1, 20), (2, 20), (3, 30) + ] + + x = [ + (sname, pname) + for (sno, sname) in suppliers + for (pno, pname) in parts + for (sp_sno, sp_pno) in suppart + if sno == sp_sno and pno == sp_pno + ] + + self.assertEqual(x, [('Boeing', 'Airliner'), ('Boeing', 'Engine'), ('Ford', 'Engine'), + ('Macdonalds', 'Cheeseburger')]) + + def test_genexps(self): + # generator expression tests + g = ([x for x in range(10)] for x in range(1)) + self.assertEqual(next(g), [x for x in range(10)]) + try: + next(g) + self.fail('should produce StopIteration exception') + except StopIteration: + pass + + a = 1 + try: + g = (a for d in a) + next(g) + self.fail('should produce TypeError') + except TypeError: + pass + + self.assertEqual(list((x, y) for x in 'abcd' for y in 'abcd'), [(x, y) for x in 'abcd' for y in 'abcd']) + self.assertEqual(list((x, y) for x in 'ab' for y in 'xy'), [(x, y) for x in 'ab' for y in 'xy']) + + a = [x for x in range(10)] + b = (x for x in (y for y in a)) + self.assertEqual(sum(b), sum([x for x in range(10)])) + + self.assertEqual(sum(x ** 2 for x in range(10)), sum([x ** 2 for x in range(10)])) + self.assertEqual(sum(x * x for x in range(10) if x % 2), sum([x * x for x in range(10) if x % 2])) + self.assertEqual(sum(x for x in (y for y in range(10))), sum([x for x in range(10)])) + self.assertEqual(sum(x for x in (y for y in (z for z in range(10)))), sum([x for x in range(10)])) + self.assertEqual(sum(x for x in [y for y in (z for z in range(10))]), sum([x for x in range(10)])) + self.assertEqual(sum(x for x in (y for y in (z for z in range(10) if True)) if True), sum([x for x in range(10)])) + self.assertEqual(sum(x for x in (y for y in (z for z in range(10) if True) if False) if True), 0) + check_syntax_error(self, "foo(x for x in range(10), 100)") + check_syntax_error(self, "foo(100, x for x in range(10))") + + def test_comprehension_specials(self): + # test for outmost iterable precomputation + x = 10; g = (i for i in range(x)); x = 5 + self.assertEqual(len(list(g)), 10) + + # This should hold, since we're only precomputing outmost iterable. + x = 10; t = False; g = ((i, j) for i in range(x) if t for j in range(x)) + x = 5; t = True; + self.assertEqual([(i, j) for i in range(10) for j in range(5)], list(g)) + + # Grammar allows multiple adjacent 'if's in listcomps and genexps, + # even though it's silly. Make sure it works (ifelse broke this.) + self.assertEqual([x for x in range(10) if x % 2 if x % 3], [1, 5, 7]) + self.assertEqual(list(x for x in range(10) if x % 2 if x % 3), [1, 5, 7]) + + # verify unpacking single element tuples in listcomp/genexp. + self.assertEqual([x for x, in [(4,), (5,), (6,)]], [4, 5, 6]) + self.assertEqual(list(x for x, in [(7,), (8,), (9,)]), [7, 8, 9]) + + def test_with_statement(self): + class manager(object): + def __enter__(self): + return (1, 2) + def __exit__(self, *args): + pass + + with manager(): + pass + with manager() as x: + pass + with manager() as (x, y): + pass + with manager(), manager(): + pass + with manager() as x, manager() as y: + pass + with manager() as x, manager(): + pass + + def test_if_else_expr(self): + # Test ifelse expressions in various cases + def _checkeval(msg, ret): + "helper to check that evaluation of expressions is done correctly" + print(msg) + return ret + + # the next line is not allowed anymore + #self.assertEqual([ x() for x in lambda: True, lambda: False if x() ], [True]) + self.assertEqual([x() for x in (lambda:True, lambda:False) if x()], [True]) + self.assertEqual([x(False) for x in (lambda x:False if x else True, lambda x:True if x else False) if x(False)], [True]) + self.assertEqual((5 if 1 else _checkeval("check 1", 0)), 5) + self.assertEqual((_checkeval("check 2", 0) if 0 else 5), 5) + self.assertEqual((5 and 6 if 0 else 1), 1) + self.assertEqual(((5 and 6) if 0 else 1), 1) + self.assertEqual((5 and (6 if 1 else 1)), 6) + self.assertEqual((0 or _checkeval("check 3", 2) if 0 else 3), 3) + self.assertEqual((1 or _checkeval("check 4", 2) if 1 else _checkeval("check 5", 3)), 1) + self.assertEqual((0 or 5 if 1 else _checkeval("check 6", 3)), 5) + self.assertEqual((not 5 if 1 else 1), False) + self.assertEqual((not 5 if 0 else 1), 1) + self.assertEqual((6 + 1 if 1 else 2), 7) + self.assertEqual((6 - 1 if 1 else 2), 5) + self.assertEqual((6 * 2 if 1 else 4), 12) + self.assertEqual((6 / 2 if 1 else 3), 3) + self.assertEqual((6 < 4 if 0 else 2), 2) + + def test_paren_evaluation(self): + self.assertEqual(16 // (4 // 2), 8) + self.assertEqual((16 // 4) // 2, 2) + self.assertEqual(16 // 4 // 2, 2) + self.assertTrue(False is (2 is 3)) + self.assertFalse((False is 2) is 3) + self.assertFalse(False is 2 is 3) + + def test_matrix_mul(self): + # This is not intended to be a comprehensive test, rather just to be few + # samples of the @ operator in test_grammar.py. + class M: + def __matmul__(self, o): + return 4 + def __imatmul__(self, o): + self.other = o + return self + m = M() + self.assertEqual(m @ m, 4) + m @= 42 + self.assertEqual(m.other, 42) + + def test_async_await(self): + async def test(): + def sum(): + pass + if 1: + await someobj() + + self.assertEqual(test.__name__, 'test') + self.assertTrue(bool(test.__code__.co_flags & inspect.CO_COROUTINE)) + + def decorator(func): + setattr(func, '_marked', True) + return func + + @decorator + async def test2(): + return 22 + self.assertTrue(test2._marked) + self.assertEqual(test2.__name__, 'test2') + self.assertTrue(bool(test2.__code__.co_flags & inspect.CO_COROUTINE)) + + def test_async_for(self): + class Done(Exception): pass + + class AIter: + def __aiter__(self): + return self + async def __anext__(self): + raise StopAsyncIteration + + async def foo(): + async for i in AIter(): + pass + async for i, j in AIter(): + pass + async for i in AIter(): + pass + else: + pass + raise Done + + with self.assertRaises(Done): + foo().send(None) + + def test_async_with(self): + class Done(Exception): pass + + class manager: + async def __aenter__(self): + return (1, 2) + async def __aexit__(self, *exc): + return False + + async def foo(): + async with manager(): + pass + async with manager() as x: + pass + async with manager() as (x, y): + pass + async with manager(), manager(): + pass + async with manager() as x, manager() as y: + pass + async with manager() as x, manager(): + pass + raise Done + + with self.assertRaises(Done): + foo().send(None) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/formatting/yapf.output b/src/test/pythonFiles/formatting/yapf.output new file mode 100644 index 000000000000..43897b1753b0 --- /dev/null +++ b/src/test/pythonFiles/formatting/yapf.output @@ -0,0 +1,57 @@ +--- C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py (original) ++++ C:\GIT\issues\s p\vscode-python\src\test\pythonFiles\formatting\fileToformat.py (reformatted) +@@ -1,22 +1,40 @@ ++import math, sys + +-import math, sys; + + def example1(): + ####This is a long comment. This should be wrapped to fit within 72 characters. +- some_tuple=( 1,2, 3,'a' ); +- some_variable={'long':'Long code lines should be wrapped within 79 characters.', +- 'other':[math.pi, 100,200,300,9876543210,'This is a long string that goes on'], +- 'more':{'inner':'This whole logical line should be wrapped.',some_tuple:[1, +- 20,300,40000,500000000,60000000000000000]}} ++ some_tuple = (1, 2, 3, 'a') ++ some_variable = { ++ 'long': ++ 'Long code lines should be wrapped within 79 characters.', ++ 'other': [ ++ math.pi, 100, 200, 300, 9876543210, ++ 'This is a long string that goes on' ++ ], ++ 'more': { ++ 'inner': 'This whole logical line should be wrapped.', ++ some_tuple: [1, 20, 300, 40000, 500000000, 60000000000000000] ++ } ++ } + return (some_tuple, some_variable) +-def example2(): return {'has_key() is deprecated':True}.has_key({'f':2}.has_key('')); +-class Example3( object ): +- def __init__ ( self, bar ): +- #Comments should have a space after the hash. +- if bar : bar+=1; bar=bar* bar ; return bar +- else: +- some_string = """ ++ ++ ++def example2(): ++ return { ++ 'has_key() is deprecated': True ++ }.has_key({'f': 2}.has_key('')) ++ ++ ++class Example3(object): ++ def __init__(self, bar): ++ #Comments should have a space after the hash. ++ if bar: ++ bar += 1 ++ bar = bar * bar ++ return bar ++ else: ++ some_string = """ + Indentation in multiline strings should not be touched. + Only actual code should be reindented. + """ +- return (sys.path, some_string) ++ return (sys.path, some_string) diff --git a/src/test/pythonFiles/formatting/yapfFormatted.py b/src/test/pythonFiles/formatting/yapfFormatted.py new file mode 100644 index 000000000000..aa3b079379a2 --- /dev/null +++ b/src/test/pythonFiles/formatting/yapfFormatted.py @@ -0,0 +1,40 @@ +import math, sys + + +def example1(): + ####This is a long comment. This should be wrapped to fit within 72 characters. + some_tuple = (1, 2, 3, 'a') + some_variable = { + 'long': + 'Long code lines should be wrapped within 79 characters.', + 'other': [ + math.pi, 100, 200, 300, 9876543210, + 'This is a long string that goes on' + ], + 'more': { + 'inner': 'This whole logical line should be wrapped.', + some_tuple: [1, 20, 300, 40000, 500000000, 60000000000000000] + } + } + return (some_tuple, some_variable) + + +def example2(): + return { + 'has_key() is deprecated': True + }.has_key({'f': 2}.has_key('')) + + +class Example3(object): + def __init__(self, bar): + #Comments should have a space after the hash. + if bar: + bar += 1 + bar = bar * bar + return bar + else: + some_string = """ + Indentation in multiline strings should not be touched. +Only actual code should be reindented. +""" + return (sys.path, some_string) diff --git a/src/test/pythonFiles/hover/functionHover.py b/src/test/pythonFiles/hover/functionHover.py new file mode 100644 index 000000000000..a0f765a5a41f --- /dev/null +++ b/src/test/pythonFiles/hover/functionHover.py @@ -0,0 +1,9 @@ +def my_func(): + """ + This is a test. + + It also includes this text, too. + """ + pass + +my_func() diff --git a/src/test/pythonFiles/hover/stringFormat.py b/src/test/pythonFiles/hover/stringFormat.py new file mode 100644 index 000000000000..b54311aa83c1 --- /dev/null +++ b/src/test/pythonFiles/hover/stringFormat.py @@ -0,0 +1,7 @@ + +def print_hello(name): + """say hello to name on stdout. + :param name: the name. + """ + print('hello {0}'.format(name).capitalize()) + diff --git a/src/test/pythonFiles/linting/file.py b/src/test/pythonFiles/linting/file.py new file mode 100644 index 000000000000..7b625a769243 --- /dev/null +++ b/src/test/pythonFiles/linting/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print (self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/pythonFiles/linting/flake8config/.flake8 b/src/test/pythonFiles/linting/flake8config/.flake8 new file mode 100644 index 000000000000..99ff2b9f819c --- /dev/null +++ b/src/test/pythonFiles/linting/flake8config/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = E302,E901,E127,E261,E261,E261,E303 \ No newline at end of file diff --git a/src/test/pythonFiles/linting/flake8config/file.py b/src/test/pythonFiles/linting/flake8config/file.py new file mode 100644 index 000000000000..047ba0dc679e --- /dev/null +++ b/src/test/pythonFiles/linting/flake8config/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop \ No newline at end of file diff --git a/src/test/pythonFiles/linting/minCheck.py b/src/test/pythonFiles/linting/minCheck.py new file mode 100644 index 000000000000..d93fa56f7e8a --- /dev/null +++ b/src/test/pythonFiles/linting/minCheck.py @@ -0,0 +1 @@ +filter(lambda x: x == 1, [1, 1, 2]) diff --git a/src/test/pythonFiles/linting/print.py b/src/test/pythonFiles/linting/print.py new file mode 100644 index 000000000000..fca61311fc84 --- /dev/null +++ b/src/test/pythonFiles/linting/print.py @@ -0,0 +1 @@ +print x \ No newline at end of file diff --git a/src/test/pythonFiles/linting/pycodestyleconfig/.pycodestyle b/src/test/pythonFiles/linting/pycodestyleconfig/.pycodestyle new file mode 100644 index 000000000000..b7c78f49db84 --- /dev/null +++ b/src/test/pythonFiles/linting/pycodestyleconfig/.pycodestyle @@ -0,0 +1,2 @@ +[pycodestyle] +ignore = E302,E901,E127,E261,E261,E261,E303 diff --git a/src/test/pythonFiles/linting/pycodestyleconfig/file.py b/src/test/pythonFiles/linting/pycodestyleconfig/file.py new file mode 100644 index 000000000000..047ba0dc679e --- /dev/null +++ b/src/test/pythonFiles/linting/pycodestyleconfig/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop \ No newline at end of file diff --git a/src/test/pythonFiles/linting/pydocstyleconfig27/.pydocstyle b/src/test/pythonFiles/linting/pydocstyleconfig27/.pydocstyle new file mode 100644 index 000000000000..19020834ad32 --- /dev/null +++ b/src/test/pythonFiles/linting/pydocstyleconfig27/.pydocstyle @@ -0,0 +1,2 @@ +[pydocstyle] +ignore=D400,D401,D402,D403,D404,D203,D102,D107 diff --git a/src/test/pythonFiles/linting/pydocstyleconfig27/file.py b/src/test/pythonFiles/linting/pydocstyleconfig27/file.py new file mode 100644 index 000000000000..047ba0dc679e --- /dev/null +++ b/src/test/pythonFiles/linting/pydocstyleconfig27/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop \ No newline at end of file diff --git a/src/test/pythonFiles/linting/pylintconfig/.pylintrc b/src/test/pythonFiles/linting/pylintconfig/.pylintrc new file mode 100644 index 000000000000..59444d78c3a3 --- /dev/null +++ b/src/test/pythonFiles/linting/pylintconfig/.pylintrc @@ -0,0 +1,2 @@ +[MESSAGES CONTROL] +disable=I0011,I0012,C0304,C0103,W0613,E0001,E1101 diff --git a/src/test/pythonFiles/linting/pylintconfig/file.py b/src/test/pythonFiles/linting/pylintconfig/file.py new file mode 100644 index 000000000000..047ba0dc679e --- /dev/null +++ b/src/test/pythonFiles/linting/pylintconfig/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop \ No newline at end of file diff --git a/src/test/pythonFiles/linting/pylintconfig/file2.py b/src/test/pythonFiles/linting/pylintconfig/file2.py new file mode 100644 index 000000000000..f375c984aa2e --- /dev/null +++ b/src/test/pythonFiles/linting/pylintconfig/file2.py @@ -0,0 +1,19 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """meth1""" + print self.blop + + def meth2(self, arg): + """meth2""" + # pylint: disable=unused-argument + print self\ + + "foo" diff --git a/src/test/pythonFiles/linting/threeLineLints.py b/src/test/pythonFiles/linting/threeLineLints.py new file mode 100644 index 000000000000..e8b578d93f11 --- /dev/null +++ b/src/test/pythonFiles/linting/threeLineLints.py @@ -0,0 +1,24 @@ +"""pylint messages with three lines of output""" + +__revision__ = None + +class Foo(object): + + def __init__(self): + pass + + def meth1(self,arg): + """missing a space between 'self' and 'arg'. This should trigger the + following three line lint warning:: + + C: 10, 0: Exactly one space required after comma + def meth1(self,arg): + ^ (bad-whitespace) + + The following three lines of tuples should also cause three-line lint + errors due to "Exactly one space required after comma" messages. + """ + a = (1,2) + b = (1,2) + c = (1,2) + print (self) diff --git a/src/test/pythonFiles/markdown/aifc.md b/src/test/pythonFiles/markdown/aifc.md new file mode 100644 index 000000000000..fff22dece1e5 --- /dev/null +++ b/src/test/pythonFiles/markdown/aifc.md @@ -0,0 +1,142 @@ +Stuff to parse AIFF-C and AIFF files. + +Unless explicitly stated otherwise, the description below is true +both for AIFF-C files and AIFF files. + +An AIFF-C file has the following structure. +```html + +-----------------+ + | FORM | + +-----------------+ + | size | + +----+------------+ + | | AIFC | + | +------------+ + | | chunks | + | | . | + | | . | + | | . | + +----+------------+ +``` +An AIFF file has the string "AIFF" instead of "AIFC". + +A chunk consists of an identifier (4 bytes) followed by a size (4 bytes, +big endian order), followed by the data. The size field does not include +the size of the 8 byte header. + +The following chunk types are recognized. +```html + FVER + version number of AIFF-C defining document (AIFF-C only). + MARK + # of markers (2 bytes) + list of markers: + marker ID (2 bytes, must be 0) + position (4 bytes) + marker name ("pstring") + COMM + # of channels (2 bytes) + # of sound frames (4 bytes) + size of the samples (2 bytes) + sampling frequency (10 bytes, IEEE 80-bit extended + floating point) + in AIFF-C files only: + compression type (4 bytes) + human-readable version of compression type ("pstring") + SSND + offset (4 bytes, not used by this program) + blocksize (4 bytes, not used by this program) + sound data +``` +A pstring consists of 1 byte length, a string of characters, and 0 or 1 +byte pad to make the total length even. + +Usage. + +Reading AIFF files: +```html + f = aifc.open(file, 'r') +``` +where file is either the name of a file or an open file pointer. +The open file pointer must have methods read(), seek(), and close(). +In some types of audio files, if the setpos() method is not used, +the seek() method is not necessary. + +This returns an instance of a class with the following public methods: +```html + getnchannels() -- returns number of audio channels (1 for + mono, 2 for stereo) + getsampwidth() -- returns sample width in bytes + getframerate() -- returns sampling frequency + getnframes() -- returns number of audio frames + getcomptype() -- returns compression type ('NONE' for AIFF files) + getcompname() -- returns human-readable version of + compression type ('not compressed' for AIFF files) + getparams() -- returns a tuple consisting of all of the + above in the above order + getmarkers() -- get the list of marks in the audio file or None + if there are no marks + getmark(id) -- get mark with the specified id (raises an error + if the mark does not exist) + readframes(n) -- returns at most n frames of audio + rewind() -- rewind to the beginning of the audio stream + setpos(pos) -- seek to the specified position + tell() -- return the current position + close() -- close the instance (make it unusable) +``` +The position returned by tell(), the position given to setpos() and +the position of marks are all compatible and have nothing to do with +the actual position in the file. +The close() method is called automatically when the class instance +is destroyed. + +Writing AIFF files: +```html + f = aifc.open(file, 'w') +``` +where file is either the name of a file or an open file pointer. +The open file pointer must have methods write(), tell(), seek(), and +close(). + +This returns an instance of a class with the following public methods: +```html + aiff() -- create an AIFF file (AIFF-C default) + aifc() -- create an AIFF-C file + setnchannels(n) -- set the number of channels + setsampwidth(n) -- set the sample width + setframerate(n) -- set the frame rate + setnframes(n) -- set the number of frames + setcomptype(type, name) + -- set the compression type and the + human-readable compression type + setparams(tuple) + -- set all parameters at once + setmark(id, pos, name) + -- add specified mark to the list of marks + tell() -- return current position in output file (useful + in combination with setmark()) + writeframesraw(data) + -- write audio frames without pathing up the + file header + writeframes(data) + -- write audio frames and patch up the file header + close() -- patch up the file header and close the + output file +``` +You should set the parameters before the first writeframesraw or +writeframes. The total number of frames does not need to be set, +but when it is set to the correct value, the header does not have to +be patched up. +It is best to first set all parameters, perhaps possibly the +compression type, and then write audio frames using writeframesraw. +When all frames have been written, either call writeframes('') or +close() to patch up the sizes in the header. +Marks can be added anytime. If there are any marks, you must call +close() after all frames have been written. +The close() method is called automatically when the class instance +is destroyed. + +When a file is opened with the extension '.aiff', an AIFF file is +written, otherwise an AIFF-C file is written. This default can be +changed by calling aiff() or aifc() before the first writeframes or +writeframesraw. \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/aifc.pydoc b/src/test/pythonFiles/markdown/aifc.pydoc new file mode 100644 index 000000000000..a4cc346d5531 --- /dev/null +++ b/src/test/pythonFiles/markdown/aifc.pydoc @@ -0,0 +1,134 @@ +Stuff to parse AIFF-C and AIFF files. + +Unless explicitly stated otherwise, the description below is true +both for AIFF-C files and AIFF files. + +An AIFF-C file has the following structure. + + +-----------------+ + | FORM | + +-----------------+ + | <size> | + +----+------------+ + | | AIFC | + | +------------+ + | | <chunks> | + | | . | + | | . | + | | . | + +----+------------+ + +An AIFF file has the string "AIFF" instead of "AIFC". + +A chunk consists of an identifier (4 bytes) followed by a size (4 bytes, +big endian order), followed by the data. The size field does not include +the size of the 8 byte header. + +The following chunk types are recognized. + + FVER + <version number of AIFF-C defining document> (AIFF-C only). + MARK + <# of markers> (2 bytes) + list of markers: + <marker ID> (2 bytes, must be > 0) + <position> (4 bytes) + <marker name> ("pstring") + COMM + <# of channels> (2 bytes) + <# of sound frames> (4 bytes) + <size of the samples> (2 bytes) + <sampling frequency> (10 bytes, IEEE 80-bit extended + floating point) + in AIFF-C files only: + <compression type> (4 bytes) + <human-readable version of compression type> ("pstring") + SSND + <offset> (4 bytes, not used by this program) + <blocksize> (4 bytes, not used by this program) + <sound data> + +A pstring consists of 1 byte length, a string of characters, and 0 or 1 +byte pad to make the total length even. + +Usage. + +Reading AIFF files: + f = aifc.open(file, 'r') +where file is either the name of a file or an open file pointer. +The open file pointer must have methods read(), seek(), and close(). +In some types of audio files, if the setpos() method is not used, +the seek() method is not necessary. + +This returns an instance of a class with the following public methods: + getnchannels() -- returns number of audio channels (1 for + mono, 2 for stereo) + getsampwidth() -- returns sample width in bytes + getframerate() -- returns sampling frequency + getnframes() -- returns number of audio frames + getcomptype() -- returns compression type ('NONE' for AIFF files) + getcompname() -- returns human-readable version of + compression type ('not compressed' for AIFF files) + getparams() -- returns a tuple consisting of all of the + above in the above order + getmarkers() -- get the list of marks in the audio file or None + if there are no marks + getmark(id) -- get mark with the specified id (raises an error + if the mark does not exist) + readframes(n) -- returns at most n frames of audio + rewind() -- rewind to the beginning of the audio stream + setpos(pos) -- seek to the specified position + tell() -- return the current position + close() -- close the instance (make it unusable) +The position returned by tell(), the position given to setpos() and +the position of marks are all compatible and have nothing to do with +the actual position in the file. +The close() method is called automatically when the class instance +is destroyed. + +Writing AIFF files: + f = aifc.open(file, 'w') +where file is either the name of a file or an open file pointer. +The open file pointer must have methods write(), tell(), seek(), and +close(). + +This returns an instance of a class with the following public methods: + aiff() -- create an AIFF file (AIFF-C default) + aifc() -- create an AIFF-C file + setnchannels(n) -- set the number of channels + setsampwidth(n) -- set the sample width + setframerate(n) -- set the frame rate + setnframes(n) -- set the number of frames + setcomptype(type, name) + -- set the compression type and the + human-readable compression type + setparams(tuple) + -- set all parameters at once + setmark(id, pos, name) + -- add specified mark to the list of marks + tell() -- return current position in output file (useful + in combination with setmark()) + writeframesraw(data) + -- write audio frames without pathing up the + file header + writeframes(data) + -- write audio frames and patch up the file header + close() -- patch up the file header and close the + output file +You should set the parameters before the first writeframesraw or +writeframes. The total number of frames does not need to be set, +but when it is set to the correct value, the header does not have to +be patched up. +It is best to first set all parameters, perhaps possibly the +compression type, and then write audio frames using writeframesraw. +When all frames have been written, either call writeframes('') or +close() to patch up the sizes in the header. +Marks can be added anytime. If there are any marks, you must call +close() after all frames have been written. +The close() method is called automatically when the class instance +is destroyed. + +When a file is opened with the extension '.aiff', an AIFF file is +written, otherwise an AIFF-C file is written. This default can be +changed by calling aiff() or aifc() before the first writeframes or +writeframesraw. \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/anydbm.md b/src/test/pythonFiles/markdown/anydbm.md new file mode 100644 index 000000000000..e5914dcbadde --- /dev/null +++ b/src/test/pythonFiles/markdown/anydbm.md @@ -0,0 +1,33 @@ +Generic interface to all dbm clones. + +Instead of +```html + import dbm + d = dbm.open(file, 'w', 0666) +``` +use +```html + import anydbm + d = anydbm.open(file, 'w') +``` +The returned object is a dbhash, gdbm, dbm or dumbdbm object, +dependent on the type of database being opened (determined by whichdb +module) in the case of an existing dbm. If the dbm does not exist and +the create or new flag ('c' or 'n') was specified, the dbm type will +be determined by the availability of the modules (tested in the above +order). + +It has the following interface (key and data are strings): +```html + d[key] = data # store data at key (may override data at + # existing key) + data = d[key] # retrieve data at key (raise KeyError if no + # such key) + del d[key] # delete data stored at key (raises KeyError + # if no such key) + flag = key in d # true if the key exists + list = d.keys() # return a list of all existing keys (slow!) +``` +Future versions may change the order in which implementations are +tested for existence, and add interfaces to other dbm-like +implementations. \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/anydbm.pydoc b/src/test/pythonFiles/markdown/anydbm.pydoc new file mode 100644 index 000000000000..2d46b5881789 --- /dev/null +++ b/src/test/pythonFiles/markdown/anydbm.pydoc @@ -0,0 +1,33 @@ +Generic interface to all dbm clones. + +Instead of + + import dbm + d = dbm.open(file, 'w', 0666) + +use + + import anydbm + d = anydbm.open(file, 'w') + +The returned object is a dbhash, gdbm, dbm or dumbdbm object, +dependent on the type of database being opened (determined by whichdb +module) in the case of an existing dbm. If the dbm does not exist and +the create or new flag ('c' or 'n') was specified, the dbm type will +be determined by the availability of the modules (tested in the above +order). + +It has the following interface (key and data are strings): + + d[key] = data # store data at key (may override data at + # existing key) + data = d[key] # retrieve data at key (raise KeyError if no + # such key) + del d[key] # delete data stored at key (raises KeyError + # if no such key) + flag = key in d # true if the key exists + list = d.keys() # return a list of all existing keys (slow!) + +Future versions may change the order in which implementations are +tested for existence, and add interfaces to other dbm-like +implementations. \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/astroid.md b/src/test/pythonFiles/markdown/astroid.md new file mode 100644 index 000000000000..b5ece21c1faf --- /dev/null +++ b/src/test/pythonFiles/markdown/astroid.md @@ -0,0 +1,24 @@ +Python Abstract Syntax Tree New Generation + +The aim of this module is to provide a common base representation of +python source code for projects such as pychecker, pyreverse, +pylint... Well, actually the development of this library is essentially +governed by pylint's needs. + +It extends class defined in the python's \_ast module with some +additional methods and attributes. Instance attributes are added by a +builder object, which can either generate extended ast (let's call +them astroid ;) by visiting an existent ast tree or by inspecting living +object. Methods are added by monkey patching ast classes. + +Main modules are: +```html +* nodes and scoped_nodes for more information about methods and + attributes added to different node classes + +* the manager contains a high level object to get astroid trees from + source files and living objects. It maintains a cache of previously + constructed tree for quick access + +* builder contains the class responsible to build astroid trees +``` \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/astroid.pydoc b/src/test/pythonFiles/markdown/astroid.pydoc new file mode 100644 index 000000000000..84d58487ead5 --- /dev/null +++ b/src/test/pythonFiles/markdown/astroid.pydoc @@ -0,0 +1,23 @@ +Python Abstract Syntax Tree New Generation + +The aim of this module is to provide a common base representation of +python source code for projects such as pychecker, pyreverse, +pylint... Well, actually the development of this library is essentially +governed by pylint's needs. + +It extends class defined in the python's _ast module with some +additional methods and attributes. Instance attributes are added by a +builder object, which can either generate extended ast (let's call +them astroid ;) by visiting an existent ast tree or by inspecting living +object. Methods are added by monkey patching ast classes. + +Main modules are: + +* nodes and scoped_nodes for more information about methods and + attributes added to different node classes + +* the manager contains a high level object to get astroid trees from + source files and living objects. It maintains a cache of previously + constructed tree for quick access + +* builder contains the class responsible to build astroid trees \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/scipy.md b/src/test/pythonFiles/markdown/scipy.md new file mode 100644 index 000000000000..d28c1e290abe --- /dev/null +++ b/src/test/pythonFiles/markdown/scipy.md @@ -0,0 +1,47 @@ +### SciPy: A scientific computing package for Python + +Documentation is available in the docstrings and +online at https://docs.scipy.org. + +#### Contents +SciPy imports all the functions from the NumPy namespace, and in +addition provides: + +#### Subpackages +Using any of these subpackages requires an explicit import. For example, +`import scipy.cluster`. +```html + cluster --- Vector Quantization / Kmeans + fftpack --- Discrete Fourier Transform algorithms + integrate --- Integration routines + interpolate --- Interpolation Tools + io --- Data input and output + linalg --- Linear algebra routines + linalg.blas --- Wrappers to BLAS library + linalg.lapack --- Wrappers to LAPACK library + misc --- Various utilities that don't have + another home. + ndimage --- n-dimensional image package + odr --- Orthogonal Distance Regression + optimize --- Optimization Tools + signal --- Signal Processing Tools + sparse --- Sparse Matrices + sparse.linalg --- Sparse Linear Algebra + sparse.linalg.dsolve --- Linear Solvers + sparse.linalg.dsolve.umfpack --- :Interface to the UMFPACK library: + Conjugate Gradient Method (LOBPCG) + sparse.linalg.eigen --- Sparse Eigenvalue Solvers + sparse.linalg.eigen.lobpcg --- Locally Optimal Block Preconditioned + Conjugate Gradient Method (LOBPCG) + spatial --- Spatial data structures and algorithms + special --- Special functions + stats --- Statistical Functions +``` +#### Utility tools +```html + test --- Run scipy unittests + show_config --- Show scipy build configuration + show_numpy_config --- Show numpy build configuration + __version__ --- Scipy version string + __numpy_version__ --- Numpy version string +``` \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/scipy.pydoc b/src/test/pythonFiles/markdown/scipy.pydoc new file mode 100644 index 000000000000..293445fbea5b --- /dev/null +++ b/src/test/pythonFiles/markdown/scipy.pydoc @@ -0,0 +1,53 @@ +SciPy: A scientific computing package for Python +================================================ + +Documentation is available in the docstrings and +online at https://docs.scipy.org. + +Contents +-------- +SciPy imports all the functions from the NumPy namespace, and in +addition provides: + +Subpackages +----------- +Using any of these subpackages requires an explicit import. For example, +``import scipy.cluster``. + +:: + + cluster --- Vector Quantization / Kmeans + fftpack --- Discrete Fourier Transform algorithms + integrate --- Integration routines + interpolate --- Interpolation Tools + io --- Data input and output + linalg --- Linear algebra routines + linalg.blas --- Wrappers to BLAS library + linalg.lapack --- Wrappers to LAPACK library + misc --- Various utilities that don't have + another home. + ndimage --- n-dimensional image package + odr --- Orthogonal Distance Regression + optimize --- Optimization Tools + signal --- Signal Processing Tools + sparse --- Sparse Matrices + sparse.linalg --- Sparse Linear Algebra + sparse.linalg.dsolve --- Linear Solvers + sparse.linalg.dsolve.umfpack --- :Interface to the UMFPACK library: + Conjugate Gradient Method (LOBPCG) + sparse.linalg.eigen --- Sparse Eigenvalue Solvers + sparse.linalg.eigen.lobpcg --- Locally Optimal Block Preconditioned + Conjugate Gradient Method (LOBPCG) + spatial --- Spatial data structures and algorithms + special --- Special functions + stats --- Statistical Functions + +Utility tools +------------- +:: + + test --- Run scipy unittests + show_config --- Show scipy build configuration + show_numpy_config --- Show numpy build configuration + __version__ --- Scipy version string + __numpy_version__ --- Numpy version string \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/scipy.spatial.distance.md b/src/test/pythonFiles/markdown/scipy.spatial.distance.md new file mode 100644 index 000000000000..276acddef787 --- /dev/null +++ b/src/test/pythonFiles/markdown/scipy.spatial.distance.md @@ -0,0 +1,54 @@ +### Distance computations (module:`scipy.spatial.distance`) + + +#### Function Reference + +Distance matrix computation from a collection of raw observation vectors +stored in a rectangular array. +```html + pdist -- pairwise distances between observation vectors. + cdist -- distances between two collections of observation vectors + squareform -- convert distance matrix to a condensed one and vice versa + directed_hausdorff -- directed Hausdorff distance between arrays +``` +Predicates for checking the validity of distance matrices, both +condensed and redundant. Also contained in this module are functions +for computing the number of observations in a distance matrix. +```html + is_valid_dm -- checks for a valid distance matrix + is_valid_y -- checks for a valid condensed distance matrix + num_obs_dm -- # of observations in a distance matrix + num_obs_y -- # of observations in a condensed distance matrix +``` +Distance functions between two numeric vectors `u` and `v`. Computing +distances over a large collection of vectors is inefficient for these +functions. Use `pdist` for this purpose. +```html + braycurtis -- the Bray-Curtis distance. + canberra -- the Canberra distance. + chebyshev -- the Chebyshev distance. + cityblock -- the Manhattan distance. + correlation -- the Correlation distance. + cosine -- the Cosine distance. + euclidean -- the Euclidean distance. + mahalanobis -- the Mahalanobis distance. + minkowski -- the Minkowski distance. + seuclidean -- the normalized Euclidean distance. + sqeuclidean -- the squared Euclidean distance. + wminkowski -- (deprecated) alias of `minkowski`. +``` +Distance functions between two boolean vectors (representing sets) `u` and +`v`. As in the case of numerical vectors, `pdist` is more efficient for +computing the distances between all pairs. +```html + dice -- the Dice dissimilarity. + hamming -- the Hamming distance. + jaccard -- the Jaccard distance. + kulsinski -- the Kulsinski distance. + rogerstanimoto -- the Rogers-Tanimoto dissimilarity. + russellrao -- the Russell-Rao dissimilarity. + sokalmichener -- the Sokal-Michener dissimilarity. + sokalsneath -- the Sokal-Sneath dissimilarity. + yule -- the Yule dissimilarity. +``` +:func:`hamming` also operates over discrete numerical vectors. \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/scipy.spatial.distance.pydoc b/src/test/pythonFiles/markdown/scipy.spatial.distance.pydoc new file mode 100644 index 000000000000..cfc9b7008b99 --- /dev/null +++ b/src/test/pythonFiles/markdown/scipy.spatial.distance.pydoc @@ -0,0 +1,71 @@ + +===================================================== +Distance computations (:mod:`scipy.spatial.distance`) +===================================================== + +.. sectionauthor:: Damian Eads + +Function Reference +------------------ + +Distance matrix computation from a collection of raw observation vectors +stored in a rectangular array. + +.. autosummary:: + :toctree: generated/ + + pdist -- pairwise distances between observation vectors. + cdist -- distances between two collections of observation vectors + squareform -- convert distance matrix to a condensed one and vice versa + directed_hausdorff -- directed Hausdorff distance between arrays + +Predicates for checking the validity of distance matrices, both +condensed and redundant. Also contained in this module are functions +for computing the number of observations in a distance matrix. + +.. autosummary:: + :toctree: generated/ + + is_valid_dm -- checks for a valid distance matrix + is_valid_y -- checks for a valid condensed distance matrix + num_obs_dm -- # of observations in a distance matrix + num_obs_y -- # of observations in a condensed distance matrix + +Distance functions between two numeric vectors ``u`` and ``v``. Computing +distances over a large collection of vectors is inefficient for these +functions. Use ``pdist`` for this purpose. + +.. autosummary:: + :toctree: generated/ + + braycurtis -- the Bray-Curtis distance. + canberra -- the Canberra distance. + chebyshev -- the Chebyshev distance. + cityblock -- the Manhattan distance. + correlation -- the Correlation distance. + cosine -- the Cosine distance. + euclidean -- the Euclidean distance. + mahalanobis -- the Mahalanobis distance. + minkowski -- the Minkowski distance. + seuclidean -- the normalized Euclidean distance. + sqeuclidean -- the squared Euclidean distance. + wminkowski -- (deprecated) alias of `minkowski`. + +Distance functions between two boolean vectors (representing sets) ``u`` and +``v``. As in the case of numerical vectors, ``pdist`` is more efficient for +computing the distances between all pairs. + +.. autosummary:: + :toctree: generated/ + + dice -- the Dice dissimilarity. + hamming -- the Hamming distance. + jaccard -- the Jaccard distance. + kulsinski -- the Kulsinski distance. + rogerstanimoto -- the Rogers-Tanimoto dissimilarity. + russellrao -- the Russell-Rao dissimilarity. + sokalmichener -- the Sokal-Michener dissimilarity. + sokalsneath -- the Sokal-Sneath dissimilarity. + yule -- the Yule dissimilarity. + +:func:`hamming` also operates over discrete numerical vectors. diff --git a/src/test/pythonFiles/markdown/scipy.spatial.md b/src/test/pythonFiles/markdown/scipy.spatial.md new file mode 100644 index 000000000000..2d5e891db625 --- /dev/null +++ b/src/test/pythonFiles/markdown/scipy.spatial.md @@ -0,0 +1,65 @@ +### Spatial algorithms and data structures (module:`scipy.spatial`) + + +### Nearest-neighbor Queries +```html + KDTree -- class for efficient nearest-neighbor queries + cKDTree -- class for efficient nearest-neighbor queries (faster impl.) + distance -- module containing many different distance measures + Rectangle +``` +### Delaunay Triangulation, Convex Hulls and Voronoi Diagrams +```html + Delaunay -- compute Delaunay triangulation of input points + ConvexHull -- compute a convex hull for input points + Voronoi -- compute a Voronoi diagram hull from input points + SphericalVoronoi -- compute a Voronoi diagram from input points on the surface of a sphere + HalfspaceIntersection -- compute the intersection points of input halfspaces +``` +### Plotting Helpers +```html + delaunay_plot_2d -- plot 2-D triangulation + convex_hull_plot_2d -- plot 2-D convex hull + voronoi_plot_2d -- plot 2-D voronoi diagram +``` +### Simplex representation +The simplices (triangles, tetrahedra, ...) appearing in the Delaunay +tesselation (N-dim simplices), convex hull facets, and Voronoi ridges +(N-1 dim simplices) are represented in the following scheme: +```html + tess = Delaunay(points) + hull = ConvexHull(points) + voro = Voronoi(points) + + # coordinates of the j-th vertex of the i-th simplex + tess.points[tess.simplices[i, j], :] # tesselation element + hull.points[hull.simplices[i, j], :] # convex hull facet + voro.vertices[voro.ridge_vertices[i, j], :] # ridge between Voronoi cells +``` +For Delaunay triangulations and convex hulls, the neighborhood +structure of the simplices satisfies the condition: +```html + `tess.neighbors[i,j]` is the neighboring simplex of the i-th + simplex, opposite to the j-vertex. It is -1 in case of no + neighbor. +``` +Convex hull facets also define a hyperplane equation: +```html + (hull.equations[i,:-1] * coord).sum() + hull.equations[i,-1] == 0 +``` +Similar hyperplane equations for the Delaunay triangulation correspond +to the convex hull facets on the corresponding N+1 dimensional +paraboloid. + +The Delaunay triangulation objects offer a method for locating the +simplex containing a given point, and barycentric coordinate +computations. + +#### Functions +```html + tsearch + distance_matrix + minkowski_distance + minkowski_distance_p + procrustes +``` \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/scipy.spatial.pydoc b/src/test/pythonFiles/markdown/scipy.spatial.pydoc new file mode 100644 index 000000000000..1613b94384b7 --- /dev/null +++ b/src/test/pythonFiles/markdown/scipy.spatial.pydoc @@ -0,0 +1,86 @@ +============================================================= +Spatial algorithms and data structures (:mod:`scipy.spatial`) +============================================================= + +.. currentmodule:: scipy.spatial + +Nearest-neighbor Queries +======================== +.. autosummary:: + :toctree: generated/ + + KDTree -- class for efficient nearest-neighbor queries + cKDTree -- class for efficient nearest-neighbor queries (faster impl.) + distance -- module containing many different distance measures + Rectangle + +Delaunay Triangulation, Convex Hulls and Voronoi Diagrams +========================================================= + +.. autosummary:: + :toctree: generated/ + + Delaunay -- compute Delaunay triangulation of input points + ConvexHull -- compute a convex hull for input points + Voronoi -- compute a Voronoi diagram hull from input points + SphericalVoronoi -- compute a Voronoi diagram from input points on the surface of a sphere + HalfspaceIntersection -- compute the intersection points of input halfspaces + +Plotting Helpers +================ + +.. autosummary:: + :toctree: generated/ + + delaunay_plot_2d -- plot 2-D triangulation + convex_hull_plot_2d -- plot 2-D convex hull + voronoi_plot_2d -- plot 2-D voronoi diagram + +.. seealso:: :ref:`Tutorial <qhulltutorial>` + + +Simplex representation +====================== +The simplices (triangles, tetrahedra, ...) appearing in the Delaunay +tesselation (N-dim simplices), convex hull facets, and Voronoi ridges +(N-1 dim simplices) are represented in the following scheme:: + + tess = Delaunay(points) + hull = ConvexHull(points) + voro = Voronoi(points) + + # coordinates of the j-th vertex of the i-th simplex + tess.points[tess.simplices[i, j], :] # tesselation element + hull.points[hull.simplices[i, j], :] # convex hull facet + voro.vertices[voro.ridge_vertices[i, j], :] # ridge between Voronoi cells + +For Delaunay triangulations and convex hulls, the neighborhood +structure of the simplices satisfies the condition: + + ``tess.neighbors[i,j]`` is the neighboring simplex of the i-th + simplex, opposite to the j-vertex. It is -1 in case of no + neighbor. + +Convex hull facets also define a hyperplane equation:: + + (hull.equations[i,:-1] * coord).sum() + hull.equations[i,-1] == 0 + +Similar hyperplane equations for the Delaunay triangulation correspond +to the convex hull facets on the corresponding N+1 dimensional +paraboloid. + +The Delaunay triangulation objects offer a method for locating the +simplex containing a given point, and barycentric coordinate +computations. + +Functions +--------- + +.. autosummary:: + :toctree: generated/ + + tsearch + distance_matrix + minkowski_distance + minkowski_distance_p + procrustes \ No newline at end of file diff --git a/src/test/pythonFiles/refactoring/source folder/with empty line.py b/src/test/pythonFiles/refactoring/source folder/with empty line.py new file mode 100644 index 000000000000..01ed75727900 --- /dev/null +++ b/src/test/pythonFiles/refactoring/source folder/with empty line.py @@ -0,0 +1,8 @@ +import os + +def one(): + return True + +def two(): + if one(): + print("A" + one()) diff --git a/src/test/pythonFiles/refactoring/source folder/without empty line.py b/src/test/pythonFiles/refactoring/source folder/without empty line.py new file mode 100644 index 000000000000..a449eb106f5c --- /dev/null +++ b/src/test/pythonFiles/refactoring/source folder/without empty line.py @@ -0,0 +1,8 @@ +import os + +def one(): + return True + +def two(): + if one(): + print("A" + one()) \ No newline at end of file diff --git a/src/test/pythonFiles/refactoring/standAlone/refactor.py b/src/test/pythonFiles/refactoring/standAlone/refactor.py new file mode 100644 index 000000000000..ee941dd45ebf --- /dev/null +++ b/src/test/pythonFiles/refactoring/standAlone/refactor.py @@ -0,0 +1,245 @@ +# Arguments are: +# 1. Working directory. +# 2. Rope folder + +import io +import sys +import json +import traceback +import rope + +from rope.base import libutils +from rope.refactor.rename import Rename +from rope.refactor.extract import ExtractMethod, ExtractVariable +import rope.base.project +import rope.base.taskhandle + +WORKSPACE_ROOT = sys.argv[1] +ROPE_PROJECT_FOLDER = sys.argv[2] + + +class RefactorProgress(): + """ + Refactor progress information + """ + + def __init__(self, name='Task Name', message=None, percent=0): + self.name = name + self.message = message + self.percent = percent + + +class ChangeType(): + """ + Change Type Enum + """ + EDIT = 0 + NEW = 1 + DELETE = 2 + + +class Change(): + """ + """ + EDIT = 0 + NEW = 1 + DELETE = 2 + + def __init__(self, filePath, fileMode=ChangeType.EDIT, diff=""): + self.filePath = filePath + self.diff = diff + self.fileMode = fileMode + + +class BaseRefactoring(object): + """ + Base class for refactorings + """ + + def __init__(self, project, resource, name="Refactor", progressCallback=None): + self._progressCallback = progressCallback + self._handle = rope.base.taskhandle.TaskHandle(name) + self._handle.add_observer(self._update_progress) + self.project = project + self.resource = resource + self.changes = [] + + def _update_progress(self): + jobset = self._handle.current_jobset() + if jobset and not self._progressCallback is None: + progress = RefactorProgress() + # getting current job set name + if jobset.get_name() is not None: + progress.name = jobset.get_name() + # getting active job name + if jobset.get_active_job_name() is not None: + progress.message = jobset.get_active_job_name() + # adding done percent + percent = jobset.get_percent_done() + if percent is not None: + progress.percent = percent + if not self._progressCallback is None: + self._progressCallback(progress) + + def stop(self): + self._handle.stop() + + def refactor(self): + try: + self.onRefactor() + except rope.base.exceptions.InterruptedTaskError: + # we can ignore this exception, as user has cancelled refactoring + pass + + def onRefactor(self): + """ + To be implemented by each base class + """ + pass + + +class RenameRefactor(BaseRefactoring): + + def __init__(self, project, resource, name="Rename", progressCallback=None, startOffset=None, newName="new_Name"): + BaseRefactoring.__init__(self, project, resource, + name, progressCallback) + self._newName = newName + self.startOffset = startOffset + + def onRefactor(self): + renamed = Rename(self.project, self.resource, self.startOffset) + changes = renamed.get_changes(self._newName, task_handle=self._handle) + for item in changes.changes: + if isinstance(item, rope.base.change.ChangeContents): + self.changes.append( + Change(item.resource.real_path, ChangeType.EDIT, item.get_description())) + else: + raise Exception('Unknown Change') + + +class ExtractVariableRefactor(BaseRefactoring): + + def __init__(self, project, resource, name="Extract Variable", progressCallback=None, startOffset=None, endOffset=None, newName="new_Name", similar=False, global_=False): + BaseRefactoring.__init__(self, project, resource, + name, progressCallback) + self._newName = newName + self._startOffset = startOffset + self._endOffset = endOffset + self._similar = similar + self._global = global_ + + def onRefactor(self): + renamed = ExtractVariable( + self.project, self.resource, self._startOffset, self._endOffset) + changes = renamed.get_changes( + self._newName, self._similar, self._global) + for item in changes.changes: + if isinstance(item, rope.base.change.ChangeContents): + self.changes.append( + Change(item.resource.real_path, ChangeType.EDIT, item.get_description())) + else: + raise Exception('Unknown Change') + + +class ExtractMethodRefactor(ExtractVariableRefactor): + + def __init__(self, project, resource, name="Extract Method", progressCallback=None, startOffset=None, endOffset=None, newName="new_Name", similar=False, global_=False): + ExtractVariableRefactor.__init__(self, project, resource, + name, progressCallback, startOffset=startOffset, endOffset=endOffset, newName=newName, similar=similar, global_=global_) + def onRefactor(self): + renamed = ExtractMethod( + self.project, self.resource, self._startOffset, self._endOffset) + changes = renamed.get_changes( + self._newName, self._similar, self._global) + for item in changes.changes: + if isinstance(item, rope.base.change.ChangeContents): + self.changes.append( + Change(item.resource.real_path, ChangeType.EDIT, item.get_description())) + else: + raise Exception('Unknown Change') + + +class RopeRefactoring(object): + + def __init__(self): + self.default_sys_path = sys.path + self._input = io.open(sys.stdin.fileno(), encoding='utf-8') + + def _extractVariable(self, filePath, start, end, newName): + """ + Extracts a variable + """ + project = rope.base.project.Project(WORKSPACE_ROOT, ropefolder=ROPE_PROJECT_FOLDER, save_history=False) + resourceToRefactor = libutils.path_to_resource(project, filePath) + refactor = ExtractVariableRefactor(project, resourceToRefactor, startOffset=start, endOffset=end, newName=newName) + refactor.refactor() + changes = refactor.changes + project.close() + valueToReturn = [] + for change in changes: + valueToReturn.append({'diff':change.diff}) + return valueToReturn + + def _extractMethod(self, filePath, start, end, newName): + """ + Extracts a method + """ + project = rope.base.project.Project(WORKSPACE_ROOT, ropefolder=ROPE_PROJECT_FOLDER, save_history=False) + resourceToRefactor = libutils.path_to_resource(project, filePath) + refactor = ExtractMethodRefactor(project, resourceToRefactor, startOffset=start, endOffset=end, newName=newName) + refactor.refactor() + changes = refactor.changes + project.close() + valueToReturn = [] + for change in changes: + valueToReturn.append({'diff':change.diff}) + return valueToReturn + + def _serialize(self, identifier, results): + """ + Serializes the refactor results + """ + return json.dumps({'id': identifier, 'results': results}) + + def _deserialize(self, request): + """Deserialize request from VSCode. + + Args: + request: String with raw request from VSCode. + + Returns: + Python dictionary with request data. + """ + return json.loads(request) + + def _process_request(self, request): + """Accept serialized request from VSCode and write response. + """ + request = self._deserialize(request) + lookup = request.get('lookup', '') + + if lookup == '': + pass + elif lookup == 'extract_variable': + changes = self._extractVariable(request['file'], int(request['start']), int(request['end']), request['name']) + return self._write_response(self._serialize(request['id'], changes)) + elif lookup == 'extract_method': + changes = self._extractMethod(request['file'], int(request['start']), int(request['end']), request['name']) + return self._write_response(self._serialize(request['id'], changes)) + + def _write_response(self, response): + sys.stdout.write(response + '\n') + sys.stdout.flush() + + def watch(self): + self._write_response("STARTED") + while True: + try: + self._process_request(self._input.readline()) + except Exception as ex: + message = ex.message + ' \n' + traceback.format_exc() + sys.stderr.write(str(len(message)) + ':' + message) + sys.stderr.flush() + +if __name__ == '__main__': + RopeRefactoring().watch() diff --git a/src/test/pythonFiles/shebang/plain.py b/src/test/pythonFiles/shebang/plain.py new file mode 100644 index 000000000000..72f63e675db1 --- /dev/null +++ b/src/test/pythonFiles/shebang/plain.py @@ -0,0 +1,2 @@ + +print("dummy") diff --git a/src/test/pythonFiles/shebang/shebang.py b/src/test/pythonFiles/shebang/shebang.py new file mode 100644 index 000000000000..20d13ba825fb --- /dev/null +++ b/src/test/pythonFiles/shebang/shebang.py @@ -0,0 +1,3 @@ +#!python + +print("dummy") diff --git a/src/test/pythonFiles/shebang/shebangEnv.py b/src/test/pythonFiles/shebang/shebangEnv.py new file mode 100644 index 000000000000..c08ab31b509f --- /dev/null +++ b/src/test/pythonFiles/shebang/shebangEnv.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +print("dummy") diff --git a/src/test/pythonFiles/shebang/shebangInvalid.py b/src/test/pythonFiles/shebang/shebangInvalid.py new file mode 100644 index 000000000000..8a66a42523fb --- /dev/null +++ b/src/test/pythonFiles/shebang/shebangInvalid.py @@ -0,0 +1,3 @@ +#!/usr/bin/env1234 python + +print("dummy") diff --git a/src/test/pythonFiles/signature/basicSig.py b/src/test/pythonFiles/signature/basicSig.py new file mode 100644 index 000000000000..66ad4cbd0483 --- /dev/null +++ b/src/test/pythonFiles/signature/basicSig.py @@ -0,0 +1,2 @@ +range(c, 1, + diff --git a/src/test/pythonFiles/signature/classCtor.py b/src/test/pythonFiles/signature/classCtor.py new file mode 100644 index 000000000000..baa4045489e7 --- /dev/null +++ b/src/test/pythonFiles/signature/classCtor.py @@ -0,0 +1,6 @@ +class Person: + def __init__(self, name, age = 23): + self.name = name + self.age = age + +p1 = Person('Bob', ) diff --git a/src/test/pythonFiles/signature/ellipsis.py b/src/test/pythonFiles/signature/ellipsis.py new file mode 100644 index 000000000000..c34faa6d231a --- /dev/null +++ b/src/test/pythonFiles/signature/ellipsis.py @@ -0,0 +1 @@ +print(a, b, c) diff --git a/src/test/pythonFiles/signature/noSigPy3.py b/src/test/pythonFiles/signature/noSigPy3.py new file mode 100644 index 000000000000..3d814698b7fe --- /dev/null +++ b/src/test/pythonFiles/signature/noSigPy3.py @@ -0,0 +1 @@ +pow() diff --git a/src/test/pythonFiles/sorting/noconfig/after.py b/src/test/pythonFiles/sorting/noconfig/after.py new file mode 100644 index 000000000000..b768c396014c --- /dev/null +++ b/src/test/pythonFiles/sorting/noconfig/after.py @@ -0,0 +1,16 @@ +import io; sys; json +import traceback + +import rope +import rope.base.project +import rope.base.taskhandle +from rope.base import libutils +from rope.refactor.extract import ExtractMethod, ExtractVariable +from rope.refactor.rename import Rename + +WORKSPACE_ROOT = sys.argv[1] +ROPE_PROJECT_FOLDER = sys.argv[2] + + +def test(): + pass diff --git a/src/test/pythonFiles/sorting/noconfig/before.py b/src/test/pythonFiles/sorting/noconfig/before.py new file mode 100644 index 000000000000..fcd7318b5c02 --- /dev/null +++ b/src/test/pythonFiles/sorting/noconfig/before.py @@ -0,0 +1,18 @@ +import io; sys; json +import traceback +import rope + +import rope.base.project +import rope.base.taskhandle + +WORKSPACE_ROOT = sys.argv[1] +ROPE_PROJECT_FOLDER = sys.argv[2] + + +def test(): + pass + +from rope.base import libutils +from rope.refactor.rename import Rename +from rope.refactor.extract import ExtractMethod, ExtractVariable + \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/noconfig/original.py b/src/test/pythonFiles/sorting/noconfig/original.py new file mode 100644 index 000000000000..fcd7318b5c02 --- /dev/null +++ b/src/test/pythonFiles/sorting/noconfig/original.py @@ -0,0 +1,18 @@ +import io; sys; json +import traceback +import rope + +import rope.base.project +import rope.base.taskhandle + +WORKSPACE_ROOT = sys.argv[1] +ROPE_PROJECT_FOLDER = sys.argv[2] + + +def test(): + pass + +from rope.base import libutils +from rope.refactor.rename import Rename +from rope.refactor.extract import ExtractMethod, ExtractVariable + \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/.isort.cfg b/src/test/pythonFiles/sorting/withconfig/.isort.cfg new file mode 100644 index 000000000000..68da732e2b4b --- /dev/null +++ b/src/test/pythonFiles/sorting/withconfig/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +force_single_line=True \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/after.py b/src/test/pythonFiles/sorting/withconfig/after.py new file mode 100644 index 000000000000..e1fd315dbf92 --- /dev/null +++ b/src/test/pythonFiles/sorting/withconfig/after.py @@ -0,0 +1,3 @@ +from third_party import (lib1, lib2, lib3, + lib4, lib5, lib6, + lib7, lib8, lib9) \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/before.1.py b/src/test/pythonFiles/sorting/withconfig/before.1.py new file mode 100644 index 000000000000..e1fd315dbf92 --- /dev/null +++ b/src/test/pythonFiles/sorting/withconfig/before.1.py @@ -0,0 +1,3 @@ +from third_party import (lib1, lib2, lib3, + lib4, lib5, lib6, + lib7, lib8, lib9) \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/before.py b/src/test/pythonFiles/sorting/withconfig/before.py new file mode 100644 index 000000000000..e1fd315dbf92 --- /dev/null +++ b/src/test/pythonFiles/sorting/withconfig/before.py @@ -0,0 +1,3 @@ +from third_party import (lib1, lib2, lib3, + lib4, lib5, lib6, + lib7, lib8, lib9) \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/original.1.py b/src/test/pythonFiles/sorting/withconfig/original.1.py new file mode 100644 index 000000000000..e1fd315dbf92 --- /dev/null +++ b/src/test/pythonFiles/sorting/withconfig/original.1.py @@ -0,0 +1,3 @@ +from third_party import (lib1, lib2, lib3, + lib4, lib5, lib6, + lib7, lib8, lib9) \ No newline at end of file diff --git a/src/test/pythonFiles/sorting/withconfig/original.py b/src/test/pythonFiles/sorting/withconfig/original.py new file mode 100644 index 000000000000..e1fd315dbf92 --- /dev/null +++ b/src/test/pythonFiles/sorting/withconfig/original.py @@ -0,0 +1,3 @@ +from third_party import (lib1, lib2, lib3, + lib4, lib5, lib6, + lib7, lib8, lib9) \ No newline at end of file diff --git a/src/test/pythonFiles/symbolFiles/childFile.py b/src/test/pythonFiles/symbolFiles/childFile.py new file mode 100644 index 000000000000..31d6fc7b4a18 --- /dev/null +++ b/src/test/pythonFiles/symbolFiles/childFile.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Child2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfChild(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/pythonFiles/symbolFiles/file.py b/src/test/pythonFiles/symbolFiles/file.py new file mode 100644 index 000000000000..27509dd2fcd6 --- /dev/null +++ b/src/test/pythonFiles/symbolFiles/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print(self) + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print (self\ + + "foo") + + def meth3(self): + """test one line disabling""" + # no error + print (self.bla) # pylint: disable=no-member + # error + print (self.blop) + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) + # pylint: enable=no-member + # error + print (self.blip) + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + if self.blop: + # pylint: enable=no-member + # error + print (self.blip) + else: + # no error + print (self.blip) + # no error + print (self.blip) + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print (self.bla) + try: + # pylint: enable=no-member + # error + print (self.blip) + except UndefinedName: # pylint: disable=undefined-variable + # no error + print (self.blip) + # no error + print (self.blip) + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print (self.blip) + else: + # error + print (self.blip) + # error + print (self.blip) + + + def meth8(self): + """test late disabling""" + # error + print (self.blip) + # pylint: disable=no-member + # no error + print (self.bla) + print (self.blop) diff --git a/src/test/pythonFiles/symbolFiles/workspace2File.py b/src/test/pythonFiles/symbolFiles/workspace2File.py new file mode 100644 index 000000000000..61aa87c55fed --- /dev/null +++ b/src/test/pythonFiles/symbolFiles/workspace2File.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Workspace2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfWorkspace2(self, arg): + """this issues a message""" + print (self) diff --git a/src/test/pythonFiles/terminalExec/sample1_normalized.py b/src/test/pythonFiles/terminalExec/sample1_normalized.py new file mode 100644 index 000000000000..8591baeb6489 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample1_normalized.py @@ -0,0 +1,25 @@ +# Sample block 1 + +def square(x): + return x**2 + +print('hello') +# Sample block 2 + +a = 2 + +if a < 2: + print('less than 2') +else: + print('more than 2') + +print('hello') +# Sample block 3 + +for i in range(5): + print(i) + print(i) + print(i) + print(i) + +print('complete') diff --git a/src/test/pythonFiles/terminalExec/sample1_raw.py b/src/test/pythonFiles/terminalExec/sample1_raw.py new file mode 100644 index 000000000000..fe050c7af289 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample1_raw.py @@ -0,0 +1,24 @@ +# Sample block 1 +def square(x): + return x**2 + +print('hello') +# Sample block 2 +a = 2 +if a < 2: + print('less than 2') +else: + print('more than 2') + +print('hello') + +# Sample block 3 +for i in range(5): + print(i) + + print(i) + print(i) + + print(i) + +print('complete') diff --git a/src/test/pythonFiles/terminalExec/sample2_normalized.py b/src/test/pythonFiles/terminalExec/sample2_normalized.py new file mode 100644 index 000000000000..a333d4e0daae --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample2_normalized.py @@ -0,0 +1,7 @@ +def add(x, y): + """Adds x to y""" + # Some comment + return x + y + +v = add(1, 7) +print(v) diff --git a/src/test/pythonFiles/terminalExec/sample2_raw.py b/src/test/pythonFiles/terminalExec/sample2_raw.py new file mode 100644 index 000000000000..6ab7e67637f4 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample2_raw.py @@ -0,0 +1,8 @@ +def add(x, y): + """Adds x to y""" + # Some comment + + return x + y + +v = add(1, 7) +print(v) diff --git a/src/test/pythonFiles/terminalExec/sample3_normalized.py b/src/test/pythonFiles/terminalExec/sample3_normalized.py new file mode 100644 index 000000000000..4fa62091c66d --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample3_normalized.py @@ -0,0 +1,5 @@ +if True: + print(1) + print(2) + +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample3_raw.py b/src/test/pythonFiles/terminalExec/sample3_raw.py new file mode 100644 index 000000000000..fee6c839aa89 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample3_raw.py @@ -0,0 +1,6 @@ +if True: + print(1) + + print(2) + +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample4_normalized.py b/src/test/pythonFiles/terminalExec/sample4_normalized.py new file mode 100644 index 000000000000..2c49d10253ff --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample4_normalized.py @@ -0,0 +1,6 @@ +class pc(object): + def __init__(self, pcname, model): + self.pcname = pcname + self.model = model + def print_name(self): + print('Workstation name is', self.pcname, 'model is', self.model) diff --git a/src/test/pythonFiles/terminalExec/sample4_raw.py b/src/test/pythonFiles/terminalExec/sample4_raw.py new file mode 100644 index 000000000000..fbf0d68fe5f8 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample4_raw.py @@ -0,0 +1,7 @@ +class pc(object): + def __init__(self, pcname, model): + self.pcname = pcname + self.model = model + + def print_name(self): + print('Workstation name is', self.pcname, 'model is', self.model) diff --git a/src/test/pythonFiles/terminalExec/sample5_normalized.py b/src/test/pythonFiles/terminalExec/sample5_normalized.py new file mode 100644 index 000000000000..822d51bd15d9 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample5_normalized.py @@ -0,0 +1,8 @@ +for i in range(10): + print('a') + for j in range(5): + print('b') + print('b2') + for k in range(2): + print('c') + print('done with first loop') diff --git a/src/test/pythonFiles/terminalExec/sample5_raw.py b/src/test/pythonFiles/terminalExec/sample5_raw.py new file mode 100644 index 000000000000..19caa9cf26a6 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample5_raw.py @@ -0,0 +1,11 @@ +for i in range(10): + print('a') + for j in range(5): + print('b') + + print('b2') + + for k in range(2): + print('c') + + print('done with first loop') diff --git a/src/test/pythonFiles/terminalExec/sample6_normalized.py b/src/test/pythonFiles/terminalExec/sample6_normalized.py new file mode 100644 index 000000000000..242bf35abea8 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample6_normalized.py @@ -0,0 +1,15 @@ +if True: + print(1) +else: print(2) + +print('🔨') +print(3) +print(3) + +if True: + print(1) +else: print(2) + +if True: + print(1) +else: print(2) diff --git a/src/test/pythonFiles/terminalExec/sample6_raw.py b/src/test/pythonFiles/terminalExec/sample6_raw.py new file mode 100644 index 000000000000..b064ca962070 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample6_raw.py @@ -0,0 +1,12 @@ +if True: + print(1) +else: print(2) +print('🔨') +print(3) +print(3) +if True: + print(1) +else: print(2) +if True: + print(1) +else: print(2) diff --git a/src/test/pythonFiles/terminalExec/sample7_normalized.py b/src/test/pythonFiles/terminalExec/sample7_normalized.py new file mode 100644 index 000000000000..2288800fc985 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample7_normalized.py @@ -0,0 +1,8 @@ +if True: + print(1) + print(1) +else: + print(2) + print(2) + +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample7_raw.py b/src/test/pythonFiles/terminalExec/sample7_raw.py new file mode 100644 index 000000000000..62d01b9659c6 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample7_raw.py @@ -0,0 +1,9 @@ +if True: + print(1) + + print(1) +else: + print(2) + + print(2) +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample8_normalized.py b/src/test/pythonFiles/terminalExec/sample8_normalized.py new file mode 100644 index 000000000000..2288800fc985 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample8_normalized.py @@ -0,0 +1,8 @@ +if True: + print(1) + print(1) +else: + print(2) + print(2) + +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample8_raw.py b/src/test/pythonFiles/terminalExec/sample8_raw.py new file mode 100644 index 000000000000..7920e6bce0d3 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample8_raw.py @@ -0,0 +1,9 @@ + if True: + print(1) + + print(1) + else: + print(2) + + print(2) + print(3) diff --git a/src/test/pythonFiles/terminalExec/sample_normalized.py b/src/test/pythonFiles/terminalExec/sample_normalized.py new file mode 100644 index 000000000000..8ee9b90cdd27 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample_normalized.py @@ -0,0 +1,5 @@ +import sys +print(sys.executable) +print("1234") +print(1) +print(2) diff --git a/src/test/pythonFiles/terminalExec/sample_raw.py b/src/test/pythonFiles/terminalExec/sample_raw.py new file mode 100644 index 000000000000..d1b32aaf606c --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample_raw.py @@ -0,0 +1,8 @@ +import sys + +print(sys.executable) + +print("1234") + +print(1) +print(2) diff --git a/src/test/pythonFiles/testFiles/counter/tests/__init__.py b/src/test/pythonFiles/testFiles/counter/tests/__init__.py new file mode 100644 index 000000000000..e02abfc9b0e1 --- /dev/null +++ b/src/test/pythonFiles/testFiles/counter/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/src/test/pythonFiles/testFiles/counter/tests/test_unit_test_counter.py b/src/test/pythonFiles/testFiles/counter/tests/test_unit_test_counter.py new file mode 100644 index 000000000000..687af033be05 --- /dev/null +++ b/src/test/pythonFiles/testFiles/counter/tests/test_unit_test_counter.py @@ -0,0 +1,17 @@ +import unittest + + +class UnitTestCounts(unittest.TestCase): + """Tests for ensuring the counter in the status bar is correct for unit tests.""" + + def test_assured_fail(self): + self.assertEqual(1, 2, 'This test is intended to fail.') + + def test_assured_success(self): + self.assertNotEqual(1, 2, 'This test is intended to not fail. (1 == 2 should never be equal)') + + def test_assured_fail_2(self): + self.assertGreater(1, 2, 'This test is intended to fail.') + + def test_assured_success_2(self): + self.assertFalse(1 == 2, 'This test is intended to not fail. (1 == 2 should always be false)') diff --git a/src/test/pythonFiles/testFiles/cwd/src/tests/test_cwd.py b/src/test/pythonFiles/testFiles/cwd/src/tests/test_cwd.py new file mode 100644 index 000000000000..33fb0fce9ba6 --- /dev/null +++ b/src/test/pythonFiles/testFiles/cwd/src/tests/test_cwd.py @@ -0,0 +1,14 @@ +import sys +import os + +import unittest + +class Test_Current_Working_Directory(unittest.TestCase): + def test_cwd(self): + testDir = os.path.join(os.getcwd(), 'test') + testFileDir = os.path.dirname(os.path.abspath(__file__)) + self.assertEqual(testDir, testFileDir, 'Not equal' + testDir + testFileDir) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py new file mode 100644 index 000000000000..db18d3885488 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_one.py @@ -0,0 +1,8 @@ +import unittest + +class Test_test_one_1(unittest.TestCase): + def test_1_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py new file mode 100644 index 000000000000..4e1a6151deb1 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.py @@ -0,0 +1,8 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt new file mode 100644 index 000000000000..4e1a6151deb1 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.txt @@ -0,0 +1,8 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt new file mode 100644 index 000000000000..b70c80df1619 --- /dev/null +++ b/src/test/pythonFiles/testFiles/debuggerTest/tests/test_debugger_two.updated.txt @@ -0,0 +1,14 @@ +import unittest + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + + def test_2_1_2(self): + self.assertEqual(1,1,'Not equal') + + def test_2_1_3(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/multi/tests/more_tests/__init__.py b/src/test/pythonFiles/testFiles/multi/tests/more_tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/testFiles/multi/tests/more_tests/test_three.py b/src/test/pythonFiles/testFiles/multi/tests/more_tests/test_three.py new file mode 100644 index 000000000000..9cea70ae7ca6 --- /dev/null +++ b/src/test/pythonFiles/testFiles/multi/tests/more_tests/test_three.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_test3(unittest.TestCase): + def test_3A(self): + self.assertEqual(1, 2-1, "Not implemented") + + def test_3B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_3C(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/multi/tests/test_one.py b/src/test/pythonFiles/testFiles/multi/tests/test_one.py new file mode 100644 index 000000000000..e869986b6ead --- /dev/null +++ b/src/test/pythonFiles/testFiles/multi/tests/test_one.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/multi/tests/test_two.py b/src/test/pythonFiles/testFiles/multi/tests/test_two.py new file mode 100644 index 000000000000..f3fef9c9b1eb --- /dev/null +++ b/src/test/pythonFiles/testFiles/multi/tests/test_two.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_test2(unittest.TestCase): + def test_2A(self): + self.fail("Not implemented") + + def test_2B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_2C(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/five.output b/src/test/pythonFiles/testFiles/noseFiles/five.output new file mode 100644 index 000000000000..8b0d557303f7 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/five.output @@ -0,0 +1,121 @@ +nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] +nose.plugins.manager: DEBUG: Configuring plugins +nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x10de9cf98>, <nose.plugins.logcapture.LogCapture object at 0x10dd8a3c8>, <nose.plugins.deprecated.Deprecated object at 0x10df5beb8>, <nose.plugins.skip.Skip object at 0x10dfa6908>, <nose.plugins.collect.CollectOnly object at 0x10e0731d0>] +nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x10e073cc0: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test_', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x10cf0b470>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x10cee6518>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test_'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path +nose.plugins.collect: DEBUG: Preparing test loader +nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x10d556780> +nose.core: DEBUG: defaultTest . +nose.core: DEBUG: Test names are ['.'] +nose.core: DEBUG: createTests called with None +nose.loader: DEBUG: load from . (None) +nose.selector: DEBUG: Test name . resolved to file ., module None, call None +nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None +nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4530410048)>]) +nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4530410048)> +nose.core: DEBUG: runTests called +nose.suite: DEBUG: precache is [] +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None +nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] +nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> +nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True +nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e089898>) +nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e089588>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> +nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) +test_Root_A (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) +test_Root_B (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) +test_Root_c (test_root.Test_Root_test1) ... ok +nose.suite: DEBUG: precache is [] + +---------------------------------------------------------------------- +Ran 3 tests in 0.022s + +OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/four.output b/src/test/pythonFiles/testFiles/noseFiles/four.output new file mode 100644 index 000000000000..511f0b1c863c --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/four.output @@ -0,0 +1,205 @@ +nose.config: INFO: Set working dir to /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific +nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] +nose.plugins.manager: DEBUG: Configuring plugins +nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x105fd4048>, <nose.plugins.logcapture.LogCapture object at 0x105ebd470>, <nose.plugins.deprecated.Deprecated object at 0x106077a58>, <nose.plugins.skip.Skip object at 0x1060d9828>, <nose.plugins.collect.CollectOnly object at 0x1061a6208>] +nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x1061a6cf8: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': ['specific'], 'py3where': None, 'testMatch': 'tst', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x1054dc4a8>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x1054dc4e0>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('tst'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific') +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific into sys.path +nose.plugins.collect: DEBUG: Preparing test loader +nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x1056897f0> +nose.core: DEBUG: defaultTest . +nose.core: DEBUG: Test names are ['.'] +nose.core: DEBUG: createTests called with None +nose.loader: DEBUG: load from . (None) +nose.selector: DEBUG: Test name . resolved to file ., module None, call None +nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific module None call None +nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4397453944)>]) +nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4397453944)> +nose.core: DEBUG: runTests called +nose.suite: DEBUG: precache is [] +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py module tst_unittest_one call None +nose.importer: DEBUG: Import tst_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific +nose.importer: DEBUG: find module part tst_unittest_one (tst_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific'] +nose.loader: DEBUG: Load from module <module 'tst_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py'> +nose.selector: DEBUG: wantModule <module 'tst_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py'>? True +nose.selector: DEBUG: wantClass <class 'tst_unittest_one.Test_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'tst_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'tst_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.tst_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.tst_B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1061bd978>) +nose.plugins.collect: DEBUG: Add test tst_A (tst_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test tst_B (tst_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1061bd9b0>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<tst_unittest_one.Test_test1 testMethod=tst_A>), Test(<tst_unittest_one.Test_test1 testMethod=tst_B>)]> +nose.plugins.collect: DEBUG: Preparing test case tst_A (tst_unittest_one.Test_test1) +tst_A (tst_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case tst_B (tst_unittest_one.Test_test1) +tst_B (tst_unittest_one.Test_test1) ... ok +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py module tst_unittest_two call None +nose.importer: DEBUG: Import tst_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific +nose.importer: DEBUG: find module part tst_unittest_two (tst_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific'] +nose.loader: DEBUG: Load from module <module 'tst_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py'> +nose.selector: DEBUG: wantModule <module 'tst_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py'>? True +nose.selector: DEBUG: wantClass <class 'tst_unittest_two.Tst_test2'>? True +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.id>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.run>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'tst_unittest_two.Tst_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'tst_unittest_two.Tst_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.tst_A2>? True +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.tst_B2>? True +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.tst_C2>? True +nose.selector: DEBUG: wantMethod <unbound method Tst_test2.tst_D2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1061bdfd0>) +nose.plugins.collect: DEBUG: Add test tst_A2 (tst_unittest_two.Tst_test2) +nose.plugins.collect: DEBUG: Add test tst_B2 (tst_unittest_two.Tst_test2) +nose.plugins.collect: DEBUG: Add test tst_C2 (tst_unittest_two.Tst_test2) +nose.plugins.collect: DEBUG: Add test tst_D2 (tst_unittest_two.Tst_test2) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1061bd518>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<tst_unittest_two.Tst_test2 testMethod=tst_A2>), Test(<tst_unittest_two.Tst_test2 testMethod=tst_B2>), Test(<tst_unittest_two.Tst_test2 testMethod=tst_C2>), Test(<tst_unittest_two.Tst_test2 testMethod=tst_D2>)]> +nose.plugins.collect: DEBUG: Preparing test case tst_A2 (tst_unittest_two.Tst_test2) +tst_A2 (tst_unittest_two.Tst_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case tst_B2 (tst_unittest_two.Tst_test2) +tst_B2 (tst_unittest_two.Tst_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case tst_C2 (tst_unittest_two.Tst_test2) +tst_C2 (tst_unittest_two.Tst_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case tst_D2 (tst_unittest_two.Tst_test2) +tst_D2 (tst_unittest_two.Tst_test2) ... ok +nose.suite: DEBUG: precache is [] + +---------------------------------------------------------------------- +Ran 6 tests in 0.033s + +OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/one.output b/src/test/pythonFiles/testFiles/noseFiles/one.output new file mode 100644 index 000000000000..cafdaf5b906a --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/one.output @@ -0,0 +1,211 @@ +nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] +nose.plugins.manager: DEBUG: Configuring plugins +nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x108249f98>, <nose.plugins.logcapture.LogCapture object at 0x108139390>, <nose.plugins.deprecated.Deprecated object at 0x108307e80>, <nose.plugins.skip.Skip object at 0x108354908>, <nose.plugins.collect.CollectOnly object at 0x108423198>] +nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x108423c88: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': '(?:^|[\\b_\\./-])[Tt]est', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x107758438>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x107296390>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('(?:^|[\\b_\\./-])[Tt]est'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single') +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single into sys.path +nose.plugins.collect: DEBUG: Preparing test loader +nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x107905780> +nose.core: DEBUG: defaultTest . +nose.core: DEBUG: Test names are ['.'] +nose.core: DEBUG: createTests called with None +nose.loader: DEBUG: load from . (None) +nose.selector: DEBUG: Test name . resolved to file ., module None, call None +nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single module None call None +nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4433617416)>]) +nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4433617416)> +nose.core: DEBUG: runTests called +nose.suite: DEBUG: precache is [] +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py module test_root call None +nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single +nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single'] +nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py'> +nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py'>? True +nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10843a748>) +nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10843a898>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> +nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) +test_Root_A (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) +test_Root_B (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) +test_Root_c (test_root.Test_Root_test1) ... ok +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests? True +nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x1083f38e0>) +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests into sys.path +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py module test_one call None +nose.importer: DEBUG: Import test_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests +nose.importer: DEBUG: find module part test_one (test_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests'] +nose.loader: DEBUG: Load from module <module 'test_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py'> +nose.selector: DEBUG: wantModule <module 'test_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py'>? True +nose.selector: DEBUG: wantClass <class 'test_one.Test_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10843a898>) +nose.plugins.collect: DEBUG: Add test test_A (test_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_B (test_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_c (test_one.Test_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10843a898>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_one.Test_test1 testMethod=test_A>), Test(<test_one.Test_test1 testMethod=test_B>), Test(<test_one.Test_test1 testMethod=test_c>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_one.Test_test1 testMethod=test_A>), Test(<test_one.Test_test1 testMethod=test_B>), Test(<test_one.Test_test1 testMethod=test_c>)]>]> +nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests +nose.plugins.collect: DEBUG: Preparing test case test_A (test_one.Test_test1) +test_A (test_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (test_one.Test_test1) +test_B (test_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_c (test_one.Test_test1) +test_c (test_one.Test_test1) ... ok +nose.suite: DEBUG: precache is [] + +---------------------------------------------------------------------- +Ran 6 tests in 0.023s + +OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.five.output b/src/test/pythonFiles/testFiles/noseFiles/run.five.output new file mode 100644 index 000000000000..640132ffe72e --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/run.five.output @@ -0,0 +1,567 @@ +nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] +nose.plugins.manager: DEBUG: Configuring plugins +nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x10b97a048>, <nose.plugins.logcapture.LogCapture object at 0x10b863400>, <nose.plugins.deprecated.Deprecated object at 0x10ba32f28>, <nose.plugins.skip.Skip object at 0x10ba7f978>, <nose.plugins.collect.CollectOnly object at 0x10bb4d240>] +nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x10bb4dd30: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x10ae81470>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x10ae81518>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path +nose.plugins.collect: DEBUG: Preparing test loader +nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x10b02f7f0> +nose.core: DEBUG: defaultTest . +nose.core: DEBUG: Test names are ['.'] +nose.core: DEBUG: createTests called with None +nose.loader: DEBUG: load from . (None) +nose.selector: DEBUG: Test name . resolved to file ., module None, call None +nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None +nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4491453048)>]) +nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4491453048)> +nose.core: DEBUG: runTests called +nose.suite: DEBUG: precache is [] +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/five.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.four.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.four.result? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.result? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.three.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.three.result? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.result? False +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None +nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] +nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> +nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True +nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb628d0>) +nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10bb62630>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> +nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) +test_Root_A (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) +test_Root_B (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) +test_Root_c (test_root.Test_Root_test1) ... ok +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True +nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x10bb218e0>) +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None +nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> +nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True +nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb62e10>) +nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) +nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10bb62c50>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None +nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb7e4e0>) +nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10bb62d30>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None +nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb7eba8>) +nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb7eba8>) +nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10bb7eb38>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None +nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> +nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True +nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb89588>) +nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10bb7eac8>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> +nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) +test4A (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) +test4B (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) +test_A (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) +test_B (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) +test_c (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) +test_A2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) +test_B2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) +test_C2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) +test_D2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) +test_222A2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) +test_222B2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) +test_A (unittest_three_test.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) +test_B (unittest_three_test.Test_test3) ... ok +nose.suite: DEBUG: precache is [] + +---------------------------------------------------------------------- +Ran 16 tests in 0.048s + +OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.five.result b/src/test/pythonFiles/testFiles/noseFiles/run.five.result new file mode 100644 index 000000000000..97c7e0e0216f --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/run.five.result @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="1" errors="0" failures="1" skip="0"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.four.output b/src/test/pythonFiles/testFiles/noseFiles/run.four.output new file mode 100644 index 000000000000..aa01067d7925 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/run.four.output @@ -0,0 +1,565 @@ +nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] +nose.plugins.manager: DEBUG: Configuring plugins +nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x10e107048>, <nose.plugins.logcapture.LogCapture object at 0x10dfef400>, <nose.plugins.deprecated.Deprecated object at 0x10e1bef98>, <nose.plugins.skip.Skip object at 0x10e20b860>, <nose.plugins.collect.CollectOnly object at 0x10e2d9208>] +nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x10e2d9cf8: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x10d60d470>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x10d60d518>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path +nose.plugins.collect: DEBUG: Preparing test loader +nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x10d7bb7f0> +nose.core: DEBUG: defaultTest . +nose.core: DEBUG: Test names are ['.'] +nose.core: DEBUG: createTests called with None +nose.loader: DEBUG: load from . (None) +nose.selector: DEBUG: Test name . resolved to file ., module None, call None +nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None +nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4532920952)>]) +nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4532920952)> +nose.core: DEBUG: runTests called +nose.suite: DEBUG: precache is [] +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/five.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.result? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.three.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.three.result? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.result? False +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None +nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] +nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> +nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True +nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e2ee8d0>) +nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e2ee630>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> +nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) +test_Root_A (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) +test_Root_B (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) +test_Root_c (test_root.Test_Root_test1) ... ok +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True +nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x10e2ad8e0>) +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None +nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> +nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True +nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e2eee10>) +nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) +nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e2eec50>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None +nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e30a4e0>) +nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e2eed30>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None +nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e30aba8>) +nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e30aba8>) +nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e30ab38>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None +nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> +nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True +nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e315588>) +nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e30aac8>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> +nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) +test4A (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) +test4B (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) +test_A (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) +test_B (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) +test_c (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) +test_A2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) +test_B2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) +test_C2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) +test_D2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) +test_222A2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) +test_222B2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) +test_A (unittest_three_test.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) +test_B (unittest_three_test.Test_test3) ... ok +nose.suite: DEBUG: precache is [] + +---------------------------------------------------------------------- +Ran 16 tests in 0.061s + +OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.four.result b/src/test/pythonFiles/testFiles/noseFiles/run.four.result new file mode 100644 index 000000000000..828e4a74b06a --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/run.four.result @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="3" errors="0" failures="1" skip="1"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_B" time="0.000"></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping +]]></skipped></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.one.output b/src/test/pythonFiles/testFiles/noseFiles/run.one.output new file mode 100644 index 000000000000..475ac92d3bb4 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/run.one.output @@ -0,0 +1,558 @@ +nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] +nose.plugins.manager: DEBUG: Configuring plugins +nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x105a14048>, <nose.plugins.logcapture.LogCapture object at 0x1058fb400>, <nose.plugins.deprecated.Deprecated object at 0x105acaf98>, <nose.plugins.skip.Skip object at 0x105b179e8>, <nose.plugins.collect.CollectOnly object at 0x105be4208>] +nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x105be4cf8: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x104f1a128>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x104a7c438>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path +nose.plugins.collect: DEBUG: Preparing test loader +nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x1050c77f0> +nose.core: DEBUG: defaultTest . +nose.core: DEBUG: Test names are ['.'] +nose.core: DEBUG: createTests called with None +nose.loader: DEBUG: load from . (None) +nose.selector: DEBUG: Test name . resolved to file ., module None, call None +nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None +nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4391412344)>]) +nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4391412344)> +nose.core: DEBUG: runTests called +nose.suite: DEBUG: precache is [] +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/five.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None +nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] +nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> +nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True +nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105bfa8d0>) +nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x105bfa630>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> +nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) +test_Root_A (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) +test_Root_B (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) +test_Root_c (test_root.Test_Root_test1) ... ok +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True +nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x105bb68e0>) +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None +nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> +nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True +nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105bfae10>) +nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) +nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x105bfac50>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None +nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105c164e0>) +nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x105bfad30>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None +nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105c16ba8>) +nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105c16ba8>) +nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x105c16b38>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None +nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> +nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True +nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105c21588>) +nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x105c16ac8>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> +nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) +test4A (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) +test4B (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) +test_A (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) +test_B (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) +test_c (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) +test_A2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) +test_B2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) +test_C2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) +test_D2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) +test_222A2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) +test_222B2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) +test_A (unittest_three_test.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) +test_B (unittest_three_test.Test_test3) ... ok +nose.suite: DEBUG: precache is [] + +---------------------------------------------------------------------- +Ran 16 tests in 0.048s + +OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.one.result b/src/test/pythonFiles/testFiles/noseFiles/run.one.result new file mode 100644 index 000000000000..59de2cfcdcc8 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/run.one.result @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="16" errors="1" failures="7" skip="2"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_B" time="0.000"></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping +]]></skipped></testcase><testcase classname="test4.Test_test3" name="test4A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py", line 6, in test4A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test4.Test_test3" name="test4B" time="0.000"></testcase><testcase classname="test_unittest_one.Test_test1" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py", line 8, in test_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_unittest_one.Test_test1" name="test_B" time="0.000"></testcase><testcase classname="test_unittest_one.Test_test1" name="test_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping +]]></skipped></testcase><testcase classname="test_unittest_two.Test_test2" name="test_A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 5, in test_A2 + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_B2" time="0.000"></testcase><testcase classname="test_unittest_two.Test_test2" name="test_C2" time="0.000"><failure type="builtins.AssertionError" message="1 != 2 : Not equal"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 11, in test_C2 + self.assertEqual(1,2,'Not equal') + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 829, in assertEqual + assertion_func(first, second, msg=msg) + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 822, in _baseAssertEqual + raise self.failureException(msg) +AssertionError: 1 != 2 : Not equal +]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_D2" time="0.000"><error type="builtins.ArithmeticError" message=""><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 14, in test_D2 + raise ArithmeticError() +ArithmeticError +]]></error></testcase><testcase classname="test_unittest_two.Test_test2a" name="test_222A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 19, in test_222A2 + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_unittest_two.Test_test2a" name="test_222B2" time="0.000"></testcase><testcase classname="unittest_three_test.Test_test3" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py", line 6, in test_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="unittest_three_test.Test_test3" name="test_B" time="0.000"></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.three.output b/src/test/pythonFiles/testFiles/noseFiles/run.three.output new file mode 100644 index 000000000000..da1ec6bc25c9 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/run.three.output @@ -0,0 +1,563 @@ +nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] +nose.plugins.manager: DEBUG: Configuring plugins +nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x1065d3048>, <nose.plugins.logcapture.LogCapture object at 0x1064ba438>, <nose.plugins.deprecated.Deprecated object at 0x106635fd0>, <nose.plugins.skip.Skip object at 0x1066d7860>, <nose.plugins.collect.CollectOnly object at 0x1066d79b0>] +nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x1067a3d30: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x105ad9438>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x105ad94e0>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path +nose.plugins.collect: DEBUG: Preparing test loader +nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x105c877f0> +nose.core: DEBUG: defaultTest . +nose.core: DEBUG: Test names are ['.'] +nose.core: DEBUG: createTests called with None +nose.loader: DEBUG: load from . (None) +nose.selector: DEBUG: Test name . resolved to file ., module None, call None +nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None +nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4403733168)>]) +nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4403733168)> +nose.core: DEBUG: runTests called +nose.suite: DEBUG: precache is [] +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/five.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.result? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.result? False +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None +nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] +nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> +nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True +nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067ba908>) +nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1067ba668>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> +nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) +test_Root_A (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) +test_Root_B (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) +test_Root_c (test_root.Test_Root_test1) ... ok +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True +nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x1067798e0>) +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None +nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> +nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True +nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067bae48>) +nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) +nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1067bac88>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None +nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067d6518>) +nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1067bad68>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None +nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067d6be0>) +nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067d6be0>) +nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1067d6b70>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None +nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> +nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True +nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067e15c0>) +nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1067d6b00>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> +nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) +test4A (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) +test4B (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) +test_A (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) +test_B (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) +test_c (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) +test_A2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) +test_B2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) +test_C2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) +test_D2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) +test_222A2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) +test_222B2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) +test_A (unittest_three_test.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) +test_B (unittest_three_test.Test_test3) ... ok +nose.suite: DEBUG: precache is [] + +---------------------------------------------------------------------- +Ran 16 tests in 0.047s + +OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.three.result b/src/test/pythonFiles/testFiles/noseFiles/run.three.result new file mode 100644 index 000000000000..828e4a74b06a --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/run.three.result @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="3" errors="0" failures="1" skip="1"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_B" time="0.000"></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping +]]></skipped></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result b/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result new file mode 100644 index 000000000000..b60e8229c55d --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="8" errors="1" failures="7" skip="0"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test4.Test_test3" name="test4A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py", line 6, in test4A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_unittest_one.Test_test1" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py", line 8, in test_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 5, in test_A2 + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_C2" time="0.000"><failure type="builtins.AssertionError" message="1 != 2 : Not equal"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 11, in test_C2 + self.assertEqual(1,2,'Not equal') + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 829, in assertEqual + assertion_func(first, second, msg=msg) + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 822, in _baseAssertEqual + raise self.failureException(msg) +AssertionError: 1 != 2 : Not equal +]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_D2" time="0.000"><error type="builtins.ArithmeticError" message=""><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 14, in test_D2 + raise ArithmeticError() +ArithmeticError +]]></error></testcase><testcase classname="test_unittest_two.Test_test2a" name="test_222A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 19, in test_222A2 + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="unittest_three_test.Test_test3" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py", line 6, in test_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.two.output b/src/test/pythonFiles/testFiles/noseFiles/run.two.output new file mode 100644 index 000000000000..31a5a5e9c34b --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/run.two.output @@ -0,0 +1,560 @@ +nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] +nose.plugins.manager: DEBUG: Configuring plugins +nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x1041fe048>, <nose.plugins.logcapture.LogCapture object at 0x1040e5438>, <nose.plugins.deprecated.Deprecated object at 0x1042b5fd0>, <nose.plugins.skip.Skip object at 0x104301a58>, <nose.plugins.collect.CollectOnly object at 0x1043d0278>] +nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x1043d0d68: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x103704160>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x1031996d8>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path +nose.plugins.collect: DEBUG: Preparing test loader +nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x1038b17f0> +nose.core: DEBUG: defaultTest . +nose.core: DEBUG: Test names are ['.'] +nose.core: DEBUG: createTests called with None +nose.loader: DEBUG: load from . (None) +nose.selector: DEBUG: Test name . resolved to file ., module None, call None +nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None +nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4366152424)>]) +nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4366152424)> +nose.core: DEBUG: runTests called +nose.suite: DEBUG: precache is [] +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/five.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.result? False +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None +nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] +nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> +nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True +nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1043e3940>) +nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1043e3ef0>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> +nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) +test_Root_A (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) +test_Root_B (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) +test_Root_c (test_root.Test_Root_test1) ... ok +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True +nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x1043a48e0>) +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None +nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> +nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True +nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1043e3e48>) +nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) +nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1043e3c88>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None +nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x104401518>) +nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1043e3d68>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None +nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x104401be0>) +nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x104401be0>) +nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x104401b70>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None +nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> +nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True +nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10440c588>) +nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x104401b00>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> +nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) +test4A (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) +test4B (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) +test_A (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) +test_B (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) +test_c (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) +test_A2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) +test_B2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) +test_C2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) +test_D2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) +test_222A2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) +test_222B2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) +test_A (unittest_three_test.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) +test_B (unittest_three_test.Test_test3) ... ok +nose.suite: DEBUG: precache is [] + +---------------------------------------------------------------------- +Ran 16 tests in 0.137s + +OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.two.result b/src/test/pythonFiles/testFiles/noseFiles/run.two.result new file mode 100644 index 000000000000..59de2cfcdcc8 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/run.two.result @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="16" errors="1" failures="7" skip="2"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_B" time="0.000"></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping +]]></skipped></testcase><testcase classname="test4.Test_test3" name="test4A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py", line 6, in test4A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test4.Test_test3" name="test4B" time="0.000"></testcase><testcase classname="test_unittest_one.Test_test1" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py", line 8, in test_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_unittest_one.Test_test1" name="test_B" time="0.000"></testcase><testcase classname="test_unittest_one.Test_test1" name="test_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping +]]></skipped></testcase><testcase classname="test_unittest_two.Test_test2" name="test_A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 5, in test_A2 + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_B2" time="0.000"></testcase><testcase classname="test_unittest_two.Test_test2" name="test_C2" time="0.000"><failure type="builtins.AssertionError" message="1 != 2 : Not equal"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 11, in test_C2 + self.assertEqual(1,2,'Not equal') + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 829, in assertEqual + assertion_func(first, second, msg=msg) + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 822, in _baseAssertEqual + raise self.failureException(msg) +AssertionError: 1 != 2 : Not equal +]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_D2" time="0.000"><error type="builtins.ArithmeticError" message=""><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 14, in test_D2 + raise ArithmeticError() +ArithmeticError +]]></error></testcase><testcase classname="test_unittest_two.Test_test2a" name="test_222A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 19, in test_222A2 + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="test_unittest_two.Test_test2a" name="test_222B2" time="0.000"></testcase><testcase classname="unittest_three_test.Test_test3" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor + yield + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run + testMethod() + File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py", line 6, in test_A + self.fail("Not implemented") + File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail + raise self.failureException(msg) +AssertionError: Not implemented +]]></failure></testcase><testcase classname="unittest_three_test.Test_test3" name="test_B" time="0.000"></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py new file mode 100644 index 000000000000..4825f3a4db3b --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py @@ -0,0 +1,15 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def tst_A(self): + self.fail("Not implemented") + + def tst_B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py new file mode 100644 index 000000000000..c9a76c07f933 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py @@ -0,0 +1,18 @@ +import unittest + +class Tst_test2(unittest.TestCase): + def tst_A2(self): + self.fail("Not implemented") + + def tst_B2(self): + self.assertEqual(1,1,'Not equal') + + def tst_C2(self): + self.assertEqual(1,2,'Not equal') + + def tst_D2(self): + raise ArithmeticError() + pass + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/test_root.py b/src/test/pythonFiles/testFiles/noseFiles/test_root.py new file mode 100644 index 000000000000..452813e9a079 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/test_root.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_Root_test1(unittest.TestCase): + def test_Root_A(self): + self.fail("Not implemented") + + def test_Root_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_Root_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py new file mode 100644 index 000000000000..734b84cd342e --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py @@ -0,0 +1,13 @@ +import unittest + + +class Test_test3(unittest.TestCase): + def test4A(self): + self.fail("Not implemented") + + def test4B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py new file mode 100644 index 000000000000..e869986b6ead --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py new file mode 100644 index 000000000000..ad89d873e879 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py @@ -0,0 +1,32 @@ +import unittest + +class Test_test2(unittest.TestCase): + def test_A2(self): + self.fail("Not implemented") + + def test_B2(self): + self.assertEqual(1,1,'Not equal') + + def test_C2(self): + self.assertEqual(1,2,'Not equal') + + def test_D2(self): + raise ArithmeticError() + pass + +class Test_test2a(unittest.TestCase): + def test_222A2(self): + self.fail("Not implemented") + + def test_222B2(self): + self.assertEqual(1,1,'Not equal') + + class Test_test2a1(unittest.TestCase): + def test_222A2wow(self): + self.fail("Not implemented") + + def test_222B2wow(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py b/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py new file mode 100644 index 000000000000..507e6af02063 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py @@ -0,0 +1,13 @@ +import unittest + + +class Test_test3(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/three.output b/src/test/pythonFiles/testFiles/noseFiles/three.output new file mode 100644 index 000000000000..a57dae74d180 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/three.output @@ -0,0 +1,555 @@ +nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] +nose.plugins.manager: DEBUG: Configuring plugins +nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x1091f9048>, <nose.plugins.logcapture.LogCapture object at 0x1090e2400>, <nose.plugins.deprecated.Deprecated object at 0x10929dac8>, <nose.plugins.skip.Skip object at 0x1092fe8d0>, <nose.plugins.collect.CollectOnly object at 0x1093cb208>] +nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x1093cbcf8: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x108701438>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x1087014e0>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path +nose.plugins.collect: DEBUG: Preparing test loader +nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x1088ae7f0> +nose.core: DEBUG: defaultTest . +nose.core: DEBUG: Test names are ['.'] +nose.core: DEBUG: createTests called with None +nose.loader: DEBUG: load from . (None) +nose.selector: DEBUG: Test name . resolved to file ., module None, call None +nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None +nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4450030200)>]) +nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4450030200)> +nose.core: DEBUG: runTests called +nose.suite: DEBUG: precache is [] +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None +nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles +nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] +nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> +nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True +nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1093e18d0>) +nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1093e1630>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> +nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) +test_Root_A (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) +test_Root_B (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) +test_Root_c (test_root.Test_Root_test1) ... ok +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True +nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x10939d8e0>) +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None +nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> +nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True +nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1093e1e10>) +nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) +nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1093e1c50>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None +nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1093fd4e0>) +nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1093e1d30>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None +nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> +nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True +nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1093fdba8>) +nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) +nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1093fdba8>) +nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1093fdb38>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None +nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] +nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> +nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True +nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x109408588>) +nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1093fdac8>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> +nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests +nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) +test4A (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) +test4B (test4.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) +test_A (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) +test_B (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) +test_c (test_unittest_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) +test_A2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) +test_B2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) +test_C2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) +test_D2 (test_unittest_two.Test_test2) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) +test_222A2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) +test_222B2 (test_unittest_two.Test_test2a) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) +test_A (unittest_three_test.Test_test3) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) +test_B (unittest_three_test.Test_test3) ... ok +nose.suite: DEBUG: precache is [] + +---------------------------------------------------------------------- +Ran 16 tests in 0.052s + +OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/two.output b/src/test/pythonFiles/testFiles/noseFiles/two.output new file mode 100644 index 000000000000..25fcf10c93d5 --- /dev/null +++ b/src/test/pythonFiles/testFiles/noseFiles/two.output @@ -0,0 +1,211 @@ +nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] +nose.plugins.manager: DEBUG: Configuring plugins +nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x10bf02fd0>, <nose.plugins.logcapture.LogCapture object at 0x10bdf2390>, <nose.plugins.deprecated.Deprecated object at 0x10bfc0ef0>, <nose.plugins.skip.Skip object at 0x10c00d978>, <nose.plugins.collect.CollectOnly object at 0x10c0da1d0>] +nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x10c0dacc0: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': '(?:^|[\\b_\\./-])[Tt]est', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x10b411438>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x10af4f320>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('(?:^|[\\b_\\./-])[Tt]est'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single') +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single into sys.path +nose.plugins.collect: DEBUG: Preparing test loader +nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x10b5be780> +nose.core: DEBUG: defaultTest . +nose.core: DEBUG: Test names are ['.'] +nose.core: DEBUG: createTests called with None +nose.loader: DEBUG: load from . (None) +nose.selector: DEBUG: Test name . resolved to file ., module None, call None +nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single module None call None +nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4497277504)>]) +nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4497277504)> +nose.core: DEBUG: runTests called +nose.suite: DEBUG: precache is [] +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py module test_root call None +nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single +nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single'] +nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py'> +nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py'>? True +nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10c0f0780>) +nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10c0f08d0>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> +nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) +test_Root_A (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) +test_Root_B (test_root.Test_Root_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) +test_Root_c (test_root.Test_Root_test1) ... ok +nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests? True +nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x10c0ac8e0>) +nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests +nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests into sys.path +nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py? True +nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py (None) +nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py, module None, call None +nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py module test_one call None +nose.importer: DEBUG: Import test_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests +nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests +nose.importer: DEBUG: find module part test_one (test_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests'] +nose.loader: DEBUG: Load from module <module 'test_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py'> +nose.selector: DEBUG: wantModule <module 'test_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py'>? True +nose.selector: DEBUG: wantClass <class 'test_one.Test_test1'>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None +nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_one.Test_test1'>>? None +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True +nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True +nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10c0f08d0>) +nose.plugins.collect: DEBUG: Add test test_A (test_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_B (test_one.Test_test1) +nose.plugins.collect: DEBUG: Add test test_c (test_one.Test_test1) +nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10c0f08d0>) +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_one.Test_test1 testMethod=test_A>), Test(<test_one.Test_test1 testMethod=test_B>), Test(<test_one.Test_test1 testMethod=test_c>)]> +nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_one.Test_test1 testMethod=test_A>), Test(<test_one.Test_test1 testMethod=test_B>), Test(<test_one.Test_test1 testMethod=test_c>)]>]> +nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests +nose.plugins.collect: DEBUG: Preparing test case test_A (test_one.Test_test1) +test_A (test_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_B (test_one.Test_test1) +test_B (test_one.Test_test1) ... ok +nose.plugins.collect: DEBUG: Preparing test case test_c (test_one.Test_test1) +test_c (test_one.Test_test1) ... ok +nose.suite: DEBUG: precache is [] + +---------------------------------------------------------------------- +Ran 6 tests in 0.188s + +OK diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/five.output b/src/test/pythonFiles/testFiles/pytestFiles/results/five.output new file mode 100644 index 000000000000..c7b9d058f784 --- /dev/null +++ b/src/test/pythonFiles/testFiles/pytestFiles/results/five.output @@ -0,0 +1,375 @@ +[ + { + "rootid": ".", + "root": "/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard", + "parents": [ + { + "id": "./test_root.py", + "kind": "file", + "name": "test_root.py", + "parentid": ".", + "relpath": "./test_root.py" + }, + { + "id": "./test_root.py::Test_Root_test1", + "kind": "suite", + "name": "Test_Root_test1", + "parentid": "./test_root.py" + }, + { + "id": "./tests", + "kind": "folder", + "name": "tests", + "parentid": ".", + "relpath": "./tests" + }, + { + "id": "./tests/test_another_pytest.py", + "kind": "file", + "name": "test_another_pytest.py", + "parentid": "./tests", + "relpath": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username", + "kind": "function", + "name": "test_parametrized_username", + "parentid": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py", + "kind": "file", + "name": "test_foreign_nested_tests.py", + "parentid": "./tests", + "relpath": "./tests/test_foreign_nested_tests.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests", + "kind": "suite", + "name": "TestNestedForeignTests", + "parentid": "./tests/test_foreign_nested_tests.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere", + "kind": "suite", + "name": "TestInheritingHere", + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests", + "kind": "suite", + "name": "TestExtraNestedForeignTests", + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_pytest.py", + "kind": "file", + "name": "test_pytest.py", + "parentid": "./tests", + "relpath": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp", + "kind": "suite", + "name": "Test_CheckMyApp", + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA", + "kind": "suite", + "name": "Test_NestedClassA", + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A", + "kind": "suite", + "name": "Test_nested_classB_Of_A", + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username", + "kind": "function", + "name": "test_parametrized_username", + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_unittest_one.py", + "kind": "file", + "name": "test_unittest_one.py", + "parentid": "./tests", + "relpath": "./tests/test_unittest_one.py" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1", + "kind": "suite", + "name": "Test_test1", + "parentid": "./tests/test_unittest_one.py" + }, + { + "id": "./tests/test_unittest_two.py", + "kind": "file", + "name": "test_unittest_two.py", + "parentid": "./tests", + "relpath": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2", + "kind": "suite", + "name": "Test_test2", + "parentid": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a", + "kind": "suite", + "name": "Test_test2a", + "parentid": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/unittest_three_test.py", + "kind": "file", + "name": "unittest_three_test.py", + "parentid": "./tests", + "relpath": "./tests/unittest_three_test.py" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3", + "kind": "suite", + "name": "Test_test3", + "parentid": "./tests/unittest_three_test.py" + } + ], + "tests": [ + { + "id": "./test_root.py::Test_Root_test1::test_Root_A", + "name": "test_Root_A", + "source": "./test_root.py:6", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./test_root.py::Test_Root_test1::test_Root_B", + "name": "test_Root_B", + "source": "./test_root.py:9", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./test_root.py::Test_Root_test1::test_Root_c", + "name": "test_Root_c", + "source": "./test_root.py:12", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./tests/test_another_pytest.py::test_username", + "name": "test_username", + "source": "tests/test_another_pytest.py:12", + "markers": [], + "parentid": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[one]", + "name": "test_parametrized_username[one]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[two]", + "name": "test_parametrized_username[two]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[three]", + "name": "test_parametrized_username[three]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign", + "name": "test_super_deep_foreign", + "source": "tests/external.py:2", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test", + "name": "test_foreign_test", + "source": "tests/external.py:4", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal", + "name": "test_nested_normal", + "source": "tests/test_foreign_nested_tests.py:5", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal", + "name": "test_normal", + "source": "tests/test_foreign_nested_tests.py:7", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check", + "name": "test_simple_check", + "source": "tests/test_pytest.py:6", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check", + "name": "test_complex_check", + "source": "tests/test_pytest.py:9", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB", + "name": "test_nested_class_methodB", + "source": "tests/test_pytest.py:13", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d", + "name": "test_d", + "source": "tests/test_pytest.py:16", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC", + "name": "test_nested_class_methodC", + "source": "tests/test_pytest.py:18", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check2", + "name": "test_simple_check2", + "source": "tests/test_pytest.py:21", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check2", + "name": "test_complex_check2", + "source": "tests/test_pytest.py:23", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::test_username", + "name": "test_username", + "source": "tests/test_pytest.py:35", + "markers": [], + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[one]", + "name": "test_parametrized_username[one]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[two]", + "name": "test_parametrized_username[two]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[three]", + "name": "test_parametrized_username[three]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_A", + "name": "test_A", + "source": "tests/test_unittest_one.py:6", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_B", + "name": "test_B", + "source": "tests/test_unittest_one.py:9", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_c", + "name": "test_c", + "source": "tests/test_unittest_one.py:12", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_A2", + "name": "test_A2", + "source": "tests/test_unittest_two.py:3", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_B2", + "name": "test_B2", + "source": "tests/test_unittest_two.py:6", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_C2", + "name": "test_C2", + "source": "tests/test_unittest_two.py:9", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_D2", + "name": "test_D2", + "source": "tests/test_unittest_two.py:12", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a::test_222A2", + "name": "test_222A2", + "source": "tests/test_unittest_two.py:17", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2a" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a::test_222B2", + "name": "test_222B2", + "source": "tests/test_unittest_two.py:20", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2a" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3::test_A", + "name": "test_A", + "source": "tests/unittest_three_test.py:4", + "markers": [], + "parentid": "./tests/unittest_three_test.py::Test_test3" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3::test_B", + "name": "test_B", + "source": "tests/unittest_three_test.py:7", + "markers": [], + "parentid": "./tests/unittest_three_test.py::Test_test3" + } + ] + } +] \ No newline at end of file diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/five.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/five.xml new file mode 100644 index 000000000000..87d7abeb58ce --- /dev/null +++ b/src/test/pythonFiles/testFiles/pytestFiles/results/five.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="1" name="pytest" skips="0" tests="1" time="0.050"><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="6" name="test_Root_A" time="0.0016579627990722656"><failure message="AssertionError: Not implemented">self = &lt;test_root.Test_Root_test1 testMethod=test_Root_A&gt; + + def test_Root_A(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +test_root.py:8: AssertionError</failure></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/four.output b/src/test/pythonFiles/testFiles/pytestFiles/results/four.output new file mode 100644 index 000000000000..c7b9d058f784 --- /dev/null +++ b/src/test/pythonFiles/testFiles/pytestFiles/results/four.output @@ -0,0 +1,375 @@ +[ + { + "rootid": ".", + "root": "/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard", + "parents": [ + { + "id": "./test_root.py", + "kind": "file", + "name": "test_root.py", + "parentid": ".", + "relpath": "./test_root.py" + }, + { + "id": "./test_root.py::Test_Root_test1", + "kind": "suite", + "name": "Test_Root_test1", + "parentid": "./test_root.py" + }, + { + "id": "./tests", + "kind": "folder", + "name": "tests", + "parentid": ".", + "relpath": "./tests" + }, + { + "id": "./tests/test_another_pytest.py", + "kind": "file", + "name": "test_another_pytest.py", + "parentid": "./tests", + "relpath": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username", + "kind": "function", + "name": "test_parametrized_username", + "parentid": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py", + "kind": "file", + "name": "test_foreign_nested_tests.py", + "parentid": "./tests", + "relpath": "./tests/test_foreign_nested_tests.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests", + "kind": "suite", + "name": "TestNestedForeignTests", + "parentid": "./tests/test_foreign_nested_tests.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere", + "kind": "suite", + "name": "TestInheritingHere", + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests", + "kind": "suite", + "name": "TestExtraNestedForeignTests", + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_pytest.py", + "kind": "file", + "name": "test_pytest.py", + "parentid": "./tests", + "relpath": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp", + "kind": "suite", + "name": "Test_CheckMyApp", + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA", + "kind": "suite", + "name": "Test_NestedClassA", + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A", + "kind": "suite", + "name": "Test_nested_classB_Of_A", + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username", + "kind": "function", + "name": "test_parametrized_username", + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_unittest_one.py", + "kind": "file", + "name": "test_unittest_one.py", + "parentid": "./tests", + "relpath": "./tests/test_unittest_one.py" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1", + "kind": "suite", + "name": "Test_test1", + "parentid": "./tests/test_unittest_one.py" + }, + { + "id": "./tests/test_unittest_two.py", + "kind": "file", + "name": "test_unittest_two.py", + "parentid": "./tests", + "relpath": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2", + "kind": "suite", + "name": "Test_test2", + "parentid": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a", + "kind": "suite", + "name": "Test_test2a", + "parentid": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/unittest_three_test.py", + "kind": "file", + "name": "unittest_three_test.py", + "parentid": "./tests", + "relpath": "./tests/unittest_three_test.py" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3", + "kind": "suite", + "name": "Test_test3", + "parentid": "./tests/unittest_three_test.py" + } + ], + "tests": [ + { + "id": "./test_root.py::Test_Root_test1::test_Root_A", + "name": "test_Root_A", + "source": "./test_root.py:6", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./test_root.py::Test_Root_test1::test_Root_B", + "name": "test_Root_B", + "source": "./test_root.py:9", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./test_root.py::Test_Root_test1::test_Root_c", + "name": "test_Root_c", + "source": "./test_root.py:12", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./tests/test_another_pytest.py::test_username", + "name": "test_username", + "source": "tests/test_another_pytest.py:12", + "markers": [], + "parentid": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[one]", + "name": "test_parametrized_username[one]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[two]", + "name": "test_parametrized_username[two]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[three]", + "name": "test_parametrized_username[three]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign", + "name": "test_super_deep_foreign", + "source": "tests/external.py:2", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test", + "name": "test_foreign_test", + "source": "tests/external.py:4", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal", + "name": "test_nested_normal", + "source": "tests/test_foreign_nested_tests.py:5", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal", + "name": "test_normal", + "source": "tests/test_foreign_nested_tests.py:7", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check", + "name": "test_simple_check", + "source": "tests/test_pytest.py:6", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check", + "name": "test_complex_check", + "source": "tests/test_pytest.py:9", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB", + "name": "test_nested_class_methodB", + "source": "tests/test_pytest.py:13", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d", + "name": "test_d", + "source": "tests/test_pytest.py:16", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC", + "name": "test_nested_class_methodC", + "source": "tests/test_pytest.py:18", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check2", + "name": "test_simple_check2", + "source": "tests/test_pytest.py:21", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check2", + "name": "test_complex_check2", + "source": "tests/test_pytest.py:23", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::test_username", + "name": "test_username", + "source": "tests/test_pytest.py:35", + "markers": [], + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[one]", + "name": "test_parametrized_username[one]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[two]", + "name": "test_parametrized_username[two]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[three]", + "name": "test_parametrized_username[three]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_A", + "name": "test_A", + "source": "tests/test_unittest_one.py:6", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_B", + "name": "test_B", + "source": "tests/test_unittest_one.py:9", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_c", + "name": "test_c", + "source": "tests/test_unittest_one.py:12", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_A2", + "name": "test_A2", + "source": "tests/test_unittest_two.py:3", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_B2", + "name": "test_B2", + "source": "tests/test_unittest_two.py:6", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_C2", + "name": "test_C2", + "source": "tests/test_unittest_two.py:9", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_D2", + "name": "test_D2", + "source": "tests/test_unittest_two.py:12", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a::test_222A2", + "name": "test_222A2", + "source": "tests/test_unittest_two.py:17", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2a" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a::test_222B2", + "name": "test_222B2", + "source": "tests/test_unittest_two.py:20", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2a" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3::test_A", + "name": "test_A", + "source": "tests/unittest_three_test.py:4", + "markers": [], + "parentid": "./tests/unittest_three_test.py::Test_test3" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3::test_B", + "name": "test_B", + "source": "tests/unittest_three_test.py:7", + "markers": [], + "parentid": "./tests/unittest_three_test.py::Test_test3" + } + ] + } +] \ No newline at end of file diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/four.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/four.xml new file mode 100644 index 000000000000..b13d0a4c1fc3 --- /dev/null +++ b/src/test/pythonFiles/testFiles/pytestFiles/results/four.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="1" name="pytest" skips="1" tests="3" time="0.052"><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="6" name="test_Root_A" time="0.0012121200561523438"><failure message="AssertionError: Not implemented">self = &lt;test_root.Test_Root_test1 testMethod=test_Root_A&gt; + + def test_Root_A(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +test_root.py:8: AssertionError</failure></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="9" name="test_Root_B" time="0.0005743503570556641"></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="12" name="test_Root_c" time="0.0008814334869384766"><skipped message="demonstrating skipping" type="pytest.skip">test_root.py:12: &lt;py._xmlgen.raw object at 0x10a139048&gt;</skipped></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/one.output b/src/test/pythonFiles/testFiles/pytestFiles/results/one.output new file mode 100644 index 000000000000..c7b9d058f784 --- /dev/null +++ b/src/test/pythonFiles/testFiles/pytestFiles/results/one.output @@ -0,0 +1,375 @@ +[ + { + "rootid": ".", + "root": "/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard", + "parents": [ + { + "id": "./test_root.py", + "kind": "file", + "name": "test_root.py", + "parentid": ".", + "relpath": "./test_root.py" + }, + { + "id": "./test_root.py::Test_Root_test1", + "kind": "suite", + "name": "Test_Root_test1", + "parentid": "./test_root.py" + }, + { + "id": "./tests", + "kind": "folder", + "name": "tests", + "parentid": ".", + "relpath": "./tests" + }, + { + "id": "./tests/test_another_pytest.py", + "kind": "file", + "name": "test_another_pytest.py", + "parentid": "./tests", + "relpath": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username", + "kind": "function", + "name": "test_parametrized_username", + "parentid": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py", + "kind": "file", + "name": "test_foreign_nested_tests.py", + "parentid": "./tests", + "relpath": "./tests/test_foreign_nested_tests.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests", + "kind": "suite", + "name": "TestNestedForeignTests", + "parentid": "./tests/test_foreign_nested_tests.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere", + "kind": "suite", + "name": "TestInheritingHere", + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests", + "kind": "suite", + "name": "TestExtraNestedForeignTests", + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_pytest.py", + "kind": "file", + "name": "test_pytest.py", + "parentid": "./tests", + "relpath": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp", + "kind": "suite", + "name": "Test_CheckMyApp", + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA", + "kind": "suite", + "name": "Test_NestedClassA", + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A", + "kind": "suite", + "name": "Test_nested_classB_Of_A", + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username", + "kind": "function", + "name": "test_parametrized_username", + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_unittest_one.py", + "kind": "file", + "name": "test_unittest_one.py", + "parentid": "./tests", + "relpath": "./tests/test_unittest_one.py" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1", + "kind": "suite", + "name": "Test_test1", + "parentid": "./tests/test_unittest_one.py" + }, + { + "id": "./tests/test_unittest_two.py", + "kind": "file", + "name": "test_unittest_two.py", + "parentid": "./tests", + "relpath": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2", + "kind": "suite", + "name": "Test_test2", + "parentid": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a", + "kind": "suite", + "name": "Test_test2a", + "parentid": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/unittest_three_test.py", + "kind": "file", + "name": "unittest_three_test.py", + "parentid": "./tests", + "relpath": "./tests/unittest_three_test.py" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3", + "kind": "suite", + "name": "Test_test3", + "parentid": "./tests/unittest_three_test.py" + } + ], + "tests": [ + { + "id": "./test_root.py::Test_Root_test1::test_Root_A", + "name": "test_Root_A", + "source": "./test_root.py:6", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./test_root.py::Test_Root_test1::test_Root_B", + "name": "test_Root_B", + "source": "./test_root.py:9", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./test_root.py::Test_Root_test1::test_Root_c", + "name": "test_Root_c", + "source": "./test_root.py:12", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./tests/test_another_pytest.py::test_username", + "name": "test_username", + "source": "tests/test_another_pytest.py:12", + "markers": [], + "parentid": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[one]", + "name": "test_parametrized_username[one]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[two]", + "name": "test_parametrized_username[two]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[three]", + "name": "test_parametrized_username[three]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign", + "name": "test_super_deep_foreign", + "source": "tests/external.py:2", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test", + "name": "test_foreign_test", + "source": "tests/external.py:4", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal", + "name": "test_nested_normal", + "source": "tests/test_foreign_nested_tests.py:5", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal", + "name": "test_normal", + "source": "tests/test_foreign_nested_tests.py:7", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check", + "name": "test_simple_check", + "source": "tests/test_pytest.py:6", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check", + "name": "test_complex_check", + "source": "tests/test_pytest.py:9", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB", + "name": "test_nested_class_methodB", + "source": "tests/test_pytest.py:13", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d", + "name": "test_d", + "source": "tests/test_pytest.py:16", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC", + "name": "test_nested_class_methodC", + "source": "tests/test_pytest.py:18", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check2", + "name": "test_simple_check2", + "source": "tests/test_pytest.py:21", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check2", + "name": "test_complex_check2", + "source": "tests/test_pytest.py:23", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::test_username", + "name": "test_username", + "source": "tests/test_pytest.py:35", + "markers": [], + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[one]", + "name": "test_parametrized_username[one]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[two]", + "name": "test_parametrized_username[two]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[three]", + "name": "test_parametrized_username[three]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_A", + "name": "test_A", + "source": "tests/test_unittest_one.py:6", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_B", + "name": "test_B", + "source": "tests/test_unittest_one.py:9", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_c", + "name": "test_c", + "source": "tests/test_unittest_one.py:12", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_A2", + "name": "test_A2", + "source": "tests/test_unittest_two.py:3", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_B2", + "name": "test_B2", + "source": "tests/test_unittest_two.py:6", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_C2", + "name": "test_C2", + "source": "tests/test_unittest_two.py:9", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_D2", + "name": "test_D2", + "source": "tests/test_unittest_two.py:12", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a::test_222A2", + "name": "test_222A2", + "source": "tests/test_unittest_two.py:17", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2a" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a::test_222B2", + "name": "test_222B2", + "source": "tests/test_unittest_two.py:20", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2a" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3::test_A", + "name": "test_A", + "source": "tests/unittest_three_test.py:4", + "markers": [], + "parentid": "./tests/unittest_three_test.py::Test_test3" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3::test_B", + "name": "test_B", + "source": "tests/unittest_three_test.py:7", + "markers": [], + "parentid": "./tests/unittest_three_test.py::Test_test3" + } + ] + } +] \ No newline at end of file diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/one.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/one.xml new file mode 100644 index 000000000000..e4d7a513e119 --- /dev/null +++ b/src/test/pythonFiles/testFiles/pytestFiles/results/one.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="11" name="pytest" skips="3" tests="33" time="0.210"><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="6" name="test_Root_A" time="0.001688241958618164"><failure message="AssertionError: Not implemented">self = &lt;test_root.Test_Root_test1 testMethod=test_Root_A&gt; + + def test_Root_A(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +test_root.py:8: AssertionError</failure></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="9" name="test_Root_B" time="0.0007982254028320312"></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="12" name="test_Root_c" time="0.0004982948303222656"><skipped message="demonstrating skipping" type="pytest.skip">test_root.py:12: &lt;py._xmlgen.raw object at 0x1024cf048&gt;</skipped></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="12" name="test_username" time="0.0006861686706542969"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[one]" time="0.0006616115570068359"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[two]" time="0.0005772113800048828"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[three]" time="0.0009157657623291016"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; + + def test_parametrized_username(non_parametrized_username): +&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] +E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] + +tests/test_another_pytest.py:17: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.().TestExtraNestedForeignTests.()" file="tests/external.py" line="2" name="test_super_deep_foreign" time="0.0021979808807373047"><failure message="AssertionError">self = &lt;tests.external.ForeignTests.TestExtraNestedForeignTests object at 0x10fb685c0&gt; + + def test_super_deep_foreign(self): +&gt; assert False +E AssertionError + +tests/external.py:4: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/external.py" line="4" name="test_foreign_test" time="0.0007357597351074219"><failure message="AssertionError">self = &lt;tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere object at 0x10fb74898&gt; + + def test_foreign_test(self): +&gt; assert False +E AssertionError + +tests/external.py:6: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/test_foreign_nested_tests.py" line="5" name="test_nested_normal" time="0.0006644725799560547"></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests" file="tests/test_foreign_nested_tests.py" line="7" name="test_normal" time="0.0007319450378417969"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="6" name="test_simple_check" time="0.0006330013275146484"><skipped message="demonstrating skipping" type="pytest.skip">/Users/donjayamanne/anaconda3/lib/python3.6/site-packages/_pytest/nose.py:23: &lt;py._xmlgen.raw object at 0x1024fb518&gt;</skipped></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="9" name="test_complex_check" time="0.0006620883941650391"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="13" name="test_nested_class_methodB" time="0.0004994869232177734"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.().Test_nested_classB_Of_A.()" file="tests/test_pytest.py" line="16" name="test_d" time="0.0006279945373535156"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="18" name="test_nested_class_methodC" time="0.0005779266357421875"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="21" name="test_simple_check2" time="0.000728607177734375"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="23" name="test_complex_check2" time="0.0005090236663818359"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="35" name="test_username" time="0.0008552074432373047"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[one]" time="0.0010302066802978516"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[two]" time="0.0009279251098632812"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[three]" time="0.001287698745727539"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; + + def test_parametrized_username(non_parametrized_username): +&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] +E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] + +tests/test_pytest.py:40: AssertionError</failure></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="6" name="test_A" time="0.0006275177001953125"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_one.Test_test1 testMethod=test_A&gt; + + def test_A(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +tests/test_unittest_one.py:8: AssertionError</failure></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="9" name="test_B" time="0.00047135353088378906"></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="12" name="test_c" time="0.0005207061767578125"><skipped message="demonstrating skipping" type="pytest.skip">tests/test_unittest_one.py:12: &lt;py._xmlgen.raw object at 0x102504cc0&gt;</skipped></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="3" name="test_A2" time="0.0006039142608642578"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_two.Test_test2 testMethod=test_A2&gt; + + def test_A2(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +tests/test_unittest_two.py:5: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="6" name="test_B2" time="0.0007021427154541016"></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="9" name="test_C2" time="0.0008001327514648438"><failure message="AssertionError: 1 != 2 : Not equal">self = &lt;test_unittest_two.Test_test2 testMethod=test_C2&gt; + + def test_C2(self): +&gt; self.assertEqual(1,2,&apos;Not equal&apos;) +E AssertionError: 1 != 2 : Not equal + +tests/test_unittest_two.py:11: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="12" name="test_D2" time="0.0005772113800048828"><failure message="ArithmeticError">self = &lt;test_unittest_two.Test_test2 testMethod=test_D2&gt; + + def test_D2(self): +&gt; raise ArithmeticError() +E ArithmeticError + +tests/test_unittest_two.py:14: ArithmeticError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2a" file="tests/test_unittest_two.py" line="17" name="test_222A2" time="0.0005698204040527344"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_two.Test_test2a testMethod=test_222A2&gt; + + def test_222A2(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +tests/test_unittest_two.py:19: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2a" file="tests/test_unittest_two.py" line="20" name="test_222B2" time="0.0004627704620361328"></testcase><testcase classname="tests.unittest_three_test.Test_test3" file="tests/unittest_three_test.py" line="4" name="test_A" time="0.0006659030914306641"><failure message="AssertionError: Not implemented">self = &lt;unittest_three_test.Test_test3 testMethod=test_A&gt; + + def test_A(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +tests/unittest_three_test.py:6: AssertionError</failure></testcase><testcase classname="tests.unittest_three_test.Test_test3" file="tests/unittest_three_test.py" line="7" name="test_B" time="0.0006167888641357422"></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/three.output b/src/test/pythonFiles/testFiles/pytestFiles/results/three.output new file mode 100644 index 000000000000..c7b9d058f784 --- /dev/null +++ b/src/test/pythonFiles/testFiles/pytestFiles/results/three.output @@ -0,0 +1,375 @@ +[ + { + "rootid": ".", + "root": "/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard", + "parents": [ + { + "id": "./test_root.py", + "kind": "file", + "name": "test_root.py", + "parentid": ".", + "relpath": "./test_root.py" + }, + { + "id": "./test_root.py::Test_Root_test1", + "kind": "suite", + "name": "Test_Root_test1", + "parentid": "./test_root.py" + }, + { + "id": "./tests", + "kind": "folder", + "name": "tests", + "parentid": ".", + "relpath": "./tests" + }, + { + "id": "./tests/test_another_pytest.py", + "kind": "file", + "name": "test_another_pytest.py", + "parentid": "./tests", + "relpath": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username", + "kind": "function", + "name": "test_parametrized_username", + "parentid": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py", + "kind": "file", + "name": "test_foreign_nested_tests.py", + "parentid": "./tests", + "relpath": "./tests/test_foreign_nested_tests.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests", + "kind": "suite", + "name": "TestNestedForeignTests", + "parentid": "./tests/test_foreign_nested_tests.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere", + "kind": "suite", + "name": "TestInheritingHere", + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests", + "kind": "suite", + "name": "TestExtraNestedForeignTests", + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_pytest.py", + "kind": "file", + "name": "test_pytest.py", + "parentid": "./tests", + "relpath": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp", + "kind": "suite", + "name": "Test_CheckMyApp", + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA", + "kind": "suite", + "name": "Test_NestedClassA", + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A", + "kind": "suite", + "name": "Test_nested_classB_Of_A", + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username", + "kind": "function", + "name": "test_parametrized_username", + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_unittest_one.py", + "kind": "file", + "name": "test_unittest_one.py", + "parentid": "./tests", + "relpath": "./tests/test_unittest_one.py" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1", + "kind": "suite", + "name": "Test_test1", + "parentid": "./tests/test_unittest_one.py" + }, + { + "id": "./tests/test_unittest_two.py", + "kind": "file", + "name": "test_unittest_two.py", + "parentid": "./tests", + "relpath": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2", + "kind": "suite", + "name": "Test_test2", + "parentid": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a", + "kind": "suite", + "name": "Test_test2a", + "parentid": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/unittest_three_test.py", + "kind": "file", + "name": "unittest_three_test.py", + "parentid": "./tests", + "relpath": "./tests/unittest_three_test.py" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3", + "kind": "suite", + "name": "Test_test3", + "parentid": "./tests/unittest_three_test.py" + } + ], + "tests": [ + { + "id": "./test_root.py::Test_Root_test1::test_Root_A", + "name": "test_Root_A", + "source": "./test_root.py:6", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./test_root.py::Test_Root_test1::test_Root_B", + "name": "test_Root_B", + "source": "./test_root.py:9", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./test_root.py::Test_Root_test1::test_Root_c", + "name": "test_Root_c", + "source": "./test_root.py:12", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./tests/test_another_pytest.py::test_username", + "name": "test_username", + "source": "tests/test_another_pytest.py:12", + "markers": [], + "parentid": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[one]", + "name": "test_parametrized_username[one]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[two]", + "name": "test_parametrized_username[two]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[three]", + "name": "test_parametrized_username[three]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign", + "name": "test_super_deep_foreign", + "source": "tests/external.py:2", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test", + "name": "test_foreign_test", + "source": "tests/external.py:4", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal", + "name": "test_nested_normal", + "source": "tests/test_foreign_nested_tests.py:5", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal", + "name": "test_normal", + "source": "tests/test_foreign_nested_tests.py:7", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check", + "name": "test_simple_check", + "source": "tests/test_pytest.py:6", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check", + "name": "test_complex_check", + "source": "tests/test_pytest.py:9", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB", + "name": "test_nested_class_methodB", + "source": "tests/test_pytest.py:13", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d", + "name": "test_d", + "source": "tests/test_pytest.py:16", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC", + "name": "test_nested_class_methodC", + "source": "tests/test_pytest.py:18", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check2", + "name": "test_simple_check2", + "source": "tests/test_pytest.py:21", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check2", + "name": "test_complex_check2", + "source": "tests/test_pytest.py:23", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::test_username", + "name": "test_username", + "source": "tests/test_pytest.py:35", + "markers": [], + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[one]", + "name": "test_parametrized_username[one]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[two]", + "name": "test_parametrized_username[two]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[three]", + "name": "test_parametrized_username[three]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_A", + "name": "test_A", + "source": "tests/test_unittest_one.py:6", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_B", + "name": "test_B", + "source": "tests/test_unittest_one.py:9", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_c", + "name": "test_c", + "source": "tests/test_unittest_one.py:12", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_A2", + "name": "test_A2", + "source": "tests/test_unittest_two.py:3", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_B2", + "name": "test_B2", + "source": "tests/test_unittest_two.py:6", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_C2", + "name": "test_C2", + "source": "tests/test_unittest_two.py:9", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_D2", + "name": "test_D2", + "source": "tests/test_unittest_two.py:12", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a::test_222A2", + "name": "test_222A2", + "source": "tests/test_unittest_two.py:17", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2a" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a::test_222B2", + "name": "test_222B2", + "source": "tests/test_unittest_two.py:20", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2a" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3::test_A", + "name": "test_A", + "source": "tests/unittest_three_test.py:4", + "markers": [], + "parentid": "./tests/unittest_three_test.py::Test_test3" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3::test_B", + "name": "test_B", + "source": "tests/unittest_three_test.py:7", + "markers": [], + "parentid": "./tests/unittest_three_test.py::Test_test3" + } + ] + } +] \ No newline at end of file diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/three.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/three.xml new file mode 100644 index 000000000000..0d1e912f656c --- /dev/null +++ b/src/test/pythonFiles/testFiles/pytestFiles/results/three.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="1" name="pytest" skips="0" tests="4" time="0.048"><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="12" name="test_username" time="0.001523733139038086"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[one]" time="0.0007066726684570312"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[two]" time="0.0009090900421142578"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[three]" time="0.0011417865753173828"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; + + def test_parametrized_username(non_parametrized_username): +&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] +E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] + +tests/test_another_pytest.py:17: AssertionError</failure></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/two.again.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/two.again.xml new file mode 100644 index 000000000000..af1ee36ca7b7 --- /dev/null +++ b/src/test/pythonFiles/testFiles/pytestFiles/results/two.again.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="10" name="pytest" skips="0" tests="11" time="0.080"><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="6" name="test_Root_A" time="0.0010533332824707031"><failure message="AssertionError: Not implemented">self = &lt;test_root.Test_Root_test1 testMethod=test_Root_A&gt; + + def test_Root_A(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +test_root.py:8: AssertionError</failure></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[three]" time="0.0008268356323242188"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; + + def test_parametrized_username(non_parametrized_username): +&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] +E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] + +tests/test_another_pytest.py:17: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.().TestExtraNestedForeignTests.()" file="tests/external.py" line="2" name="test_super_deep_foreign" time="0.0021979808807373047"><failure message="AssertionError">self = &lt;tests.external.ForeignTests.TestExtraNestedForeignTests object at 0x10fb685c0&gt; + + def test_super_deep_foreign(self): +&gt; assert False +E AssertionError + +tests/external.py:4: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/external.py" line="4" name="test_foreign_test" time="0.0007357597351074219"><failure message="AssertionError">self = &lt;tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere object at 0x10fb74898&gt; + + def test_foreign_test(self): +&gt; assert False +E AssertionError + +tests/external.py:6: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/test_foreign_nested_tests.py" line="5" name="test_nested_normal" time="0.0006644725799560547"></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests" file="tests/test_foreign_nested_tests.py" line="7" name="test_normal" time="0.0007319450378417969"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="6" name="test_simple_check" time="0.0006330013275146484"><skipped message="demonstrating skipping" type="pytest.skip">/Users/donjayamanne/anaconda3/lib/python3.6/site-packages/_pytest/nose.py:23: &lt;py._xmlgen.raw object at 0x1024fb518&gt;</skipped></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="9" name="test_complex_check" time="0.0006620883941650391"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="13" name="test_nested_class_methodB" time="0.0004994869232177734"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.().Test_nested_classB_Of_A.()" file="tests/test_pytest.py" line="16" name="test_d" time="0.0006279945373535156"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="18" name="test_nested_class_methodC" time="0.0005779266357421875"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="21" name="test_simple_check2" time="0.000728607177734375"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="23" name="test_complex_check2" time="0.0005090236663818359"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="35" name="test_username" time="0.0008552074432373047"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[one]" time="0.0010302066802978516"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[two]" time="0.0009279251098632812"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[three]" time="0.001287698745727539"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; + + def test_parametrized_username(non_parametrized_username): +&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] +E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] + +tests/test_pytest.py:40: AssertionError</failure></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[three]" time="0.0009989738464355469"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; + + def test_parametrized_username(non_parametrized_username): +&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] +E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] + +tests/test_pytest.py:40: AssertionError</failure></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="6" name="test_A" time="0.00090789794921875"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_one.Test_test1 testMethod=test_A&gt; + + def test_A(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +tests/test_unittest_one.py:8: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="3" name="test_A2" time="0.0007557868957519531"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_two.Test_test2 testMethod=test_A2&gt; + + def test_A2(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +tests/test_unittest_two.py:5: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="9" name="test_C2" time="0.0006463527679443359"><failure message="AssertionError: 1 != 2 : Not equal">self = &lt;test_unittest_two.Test_test2 testMethod=test_C2&gt; + + def test_C2(self): +&gt; self.assertEqual(1,2,&apos;Not equal&apos;) +E AssertionError: 1 != 2 : Not equal + +tests/test_unittest_two.py:11: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="12" name="test_D2" time="0.00047707557678222656"><failure message="ArithmeticError">self = &lt;test_unittest_two.Test_test2 testMethod=test_D2&gt; + + def test_D2(self): +&gt; raise ArithmeticError() +E ArithmeticError + +tests/test_unittest_two.py:14: ArithmeticError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2a" file="tests/test_unittest_two.py" line="17" name="test_222A2" time="0.00048279762268066406"></testcase><testcase classname="tests.unittest_three_test.Test_test3" file="tests/unittest_three_test.py" line="4" name="test_A" time="0.000579833984375"><failure message="AssertionError: Not implemented">self = &lt;unittest_three_test.Test_test3 testMethod=test_A&gt; + + def test_A(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +tests/unittest_three_test.py:6: AssertionError</failure></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/two.output b/src/test/pythonFiles/testFiles/pytestFiles/results/two.output new file mode 100644 index 000000000000..c7b9d058f784 --- /dev/null +++ b/src/test/pythonFiles/testFiles/pytestFiles/results/two.output @@ -0,0 +1,375 @@ +[ + { + "rootid": ".", + "root": "/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard", + "parents": [ + { + "id": "./test_root.py", + "kind": "file", + "name": "test_root.py", + "parentid": ".", + "relpath": "./test_root.py" + }, + { + "id": "./test_root.py::Test_Root_test1", + "kind": "suite", + "name": "Test_Root_test1", + "parentid": "./test_root.py" + }, + { + "id": "./tests", + "kind": "folder", + "name": "tests", + "parentid": ".", + "relpath": "./tests" + }, + { + "id": "./tests/test_another_pytest.py", + "kind": "file", + "name": "test_another_pytest.py", + "parentid": "./tests", + "relpath": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username", + "kind": "function", + "name": "test_parametrized_username", + "parentid": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py", + "kind": "file", + "name": "test_foreign_nested_tests.py", + "parentid": "./tests", + "relpath": "./tests/test_foreign_nested_tests.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests", + "kind": "suite", + "name": "TestNestedForeignTests", + "parentid": "./tests/test_foreign_nested_tests.py" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere", + "kind": "suite", + "name": "TestInheritingHere", + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests", + "kind": "suite", + "name": "TestExtraNestedForeignTests", + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_pytest.py", + "kind": "file", + "name": "test_pytest.py", + "parentid": "./tests", + "relpath": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp", + "kind": "suite", + "name": "Test_CheckMyApp", + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA", + "kind": "suite", + "name": "Test_NestedClassA", + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A", + "kind": "suite", + "name": "Test_nested_classB_Of_A", + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username", + "kind": "function", + "name": "test_parametrized_username", + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_unittest_one.py", + "kind": "file", + "name": "test_unittest_one.py", + "parentid": "./tests", + "relpath": "./tests/test_unittest_one.py" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1", + "kind": "suite", + "name": "Test_test1", + "parentid": "./tests/test_unittest_one.py" + }, + { + "id": "./tests/test_unittest_two.py", + "kind": "file", + "name": "test_unittest_two.py", + "parentid": "./tests", + "relpath": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2", + "kind": "suite", + "name": "Test_test2", + "parentid": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a", + "kind": "suite", + "name": "Test_test2a", + "parentid": "./tests/test_unittest_two.py" + }, + { + "id": "./tests/unittest_three_test.py", + "kind": "file", + "name": "unittest_three_test.py", + "parentid": "./tests", + "relpath": "./tests/unittest_three_test.py" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3", + "kind": "suite", + "name": "Test_test3", + "parentid": "./tests/unittest_three_test.py" + } + ], + "tests": [ + { + "id": "./test_root.py::Test_Root_test1::test_Root_A", + "name": "test_Root_A", + "source": "./test_root.py:6", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./test_root.py::Test_Root_test1::test_Root_B", + "name": "test_Root_B", + "source": "./test_root.py:9", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./test_root.py::Test_Root_test1::test_Root_c", + "name": "test_Root_c", + "source": "./test_root.py:12", + "markers": [], + "parentid": "./test_root.py::Test_Root_test1" + }, + { + "id": "./tests/test_another_pytest.py::test_username", + "name": "test_username", + "source": "tests/test_another_pytest.py:12", + "markers": [], + "parentid": "./tests/test_another_pytest.py" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[one]", + "name": "test_parametrized_username[one]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[two]", + "name": "test_parametrized_username[two]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_another_pytest.py::test_parametrized_username[three]", + "name": "test_parametrized_username[three]", + "source": "tests/test_another_pytest.py:15", + "markers": [], + "parentid": "./tests/test_another_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign", + "name": "test_super_deep_foreign", + "source": "tests/external.py:2", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test", + "name": "test_foreign_test", + "source": "tests/external.py:4", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal", + "name": "test_nested_normal", + "source": "tests/test_foreign_nested_tests.py:5", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" + }, + { + "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal", + "name": "test_normal", + "source": "tests/test_foreign_nested_tests.py:7", + "markers": [], + "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check", + "name": "test_simple_check", + "source": "tests/test_pytest.py:6", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check", + "name": "test_complex_check", + "source": "tests/test_pytest.py:9", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB", + "name": "test_nested_class_methodB", + "source": "tests/test_pytest.py:13", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d", + "name": "test_d", + "source": "tests/test_pytest.py:16", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC", + "name": "test_nested_class_methodC", + "source": "tests/test_pytest.py:18", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check2", + "name": "test_simple_check2", + "source": "tests/test_pytest.py:21", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check2", + "name": "test_complex_check2", + "source": "tests/test_pytest.py:23", + "markers": [], + "parentid": "./tests/test_pytest.py::Test_CheckMyApp" + }, + { + "id": "./tests/test_pytest.py::test_username", + "name": "test_username", + "source": "tests/test_pytest.py:35", + "markers": [], + "parentid": "./tests/test_pytest.py" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[one]", + "name": "test_parametrized_username[one]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[two]", + "name": "test_parametrized_username[two]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_pytest.py::test_parametrized_username[three]", + "name": "test_parametrized_username[three]", + "source": "tests/test_pytest.py:38", + "markers": [], + "parentid": "./tests/test_pytest.py::test_parametrized_username" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_A", + "name": "test_A", + "source": "tests/test_unittest_one.py:6", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_B", + "name": "test_B", + "source": "tests/test_unittest_one.py:9", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_one.py::Test_test1::test_c", + "name": "test_c", + "source": "tests/test_unittest_one.py:12", + "markers": [], + "parentid": "./tests/test_unittest_one.py::Test_test1" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_A2", + "name": "test_A2", + "source": "tests/test_unittest_two.py:3", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_B2", + "name": "test_B2", + "source": "tests/test_unittest_two.py:6", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_C2", + "name": "test_C2", + "source": "tests/test_unittest_two.py:9", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2::test_D2", + "name": "test_D2", + "source": "tests/test_unittest_two.py:12", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a::test_222A2", + "name": "test_222A2", + "source": "tests/test_unittest_two.py:17", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2a" + }, + { + "id": "./tests/test_unittest_two.py::Test_test2a::test_222B2", + "name": "test_222B2", + "source": "tests/test_unittest_two.py:20", + "markers": [], + "parentid": "./tests/test_unittest_two.py::Test_test2a" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3::test_A", + "name": "test_A", + "source": "tests/unittest_three_test.py:4", + "markers": [], + "parentid": "./tests/unittest_three_test.py::Test_test3" + }, + { + "id": "./tests/unittest_three_test.py::Test_test3::test_B", + "name": "test_B", + "source": "tests/unittest_three_test.py:7", + "markers": [], + "parentid": "./tests/unittest_three_test.py::Test_test3" + } + ] + } +] \ No newline at end of file diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/two.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/two.xml new file mode 100644 index 000000000000..e4d7a513e119 --- /dev/null +++ b/src/test/pythonFiles/testFiles/pytestFiles/results/two.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="11" name="pytest" skips="3" tests="33" time="0.210"><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="6" name="test_Root_A" time="0.001688241958618164"><failure message="AssertionError: Not implemented">self = &lt;test_root.Test_Root_test1 testMethod=test_Root_A&gt; + + def test_Root_A(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +test_root.py:8: AssertionError</failure></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="9" name="test_Root_B" time="0.0007982254028320312"></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="12" name="test_Root_c" time="0.0004982948303222656"><skipped message="demonstrating skipping" type="pytest.skip">test_root.py:12: &lt;py._xmlgen.raw object at 0x1024cf048&gt;</skipped></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="12" name="test_username" time="0.0006861686706542969"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[one]" time="0.0006616115570068359"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[two]" time="0.0005772113800048828"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[three]" time="0.0009157657623291016"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; + + def test_parametrized_username(non_parametrized_username): +&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] +E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] + +tests/test_another_pytest.py:17: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.().TestExtraNestedForeignTests.()" file="tests/external.py" line="2" name="test_super_deep_foreign" time="0.0021979808807373047"><failure message="AssertionError">self = &lt;tests.external.ForeignTests.TestExtraNestedForeignTests object at 0x10fb685c0&gt; + + def test_super_deep_foreign(self): +&gt; assert False +E AssertionError + +tests/external.py:4: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/external.py" line="4" name="test_foreign_test" time="0.0007357597351074219"><failure message="AssertionError">self = &lt;tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere object at 0x10fb74898&gt; + + def test_foreign_test(self): +&gt; assert False +E AssertionError + +tests/external.py:6: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/test_foreign_nested_tests.py" line="5" name="test_nested_normal" time="0.0006644725799560547"></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests" file="tests/test_foreign_nested_tests.py" line="7" name="test_normal" time="0.0007319450378417969"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="6" name="test_simple_check" time="0.0006330013275146484"><skipped message="demonstrating skipping" type="pytest.skip">/Users/donjayamanne/anaconda3/lib/python3.6/site-packages/_pytest/nose.py:23: &lt;py._xmlgen.raw object at 0x1024fb518&gt;</skipped></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="9" name="test_complex_check" time="0.0006620883941650391"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="13" name="test_nested_class_methodB" time="0.0004994869232177734"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.().Test_nested_classB_Of_A.()" file="tests/test_pytest.py" line="16" name="test_d" time="0.0006279945373535156"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="18" name="test_nested_class_methodC" time="0.0005779266357421875"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="21" name="test_simple_check2" time="0.000728607177734375"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="23" name="test_complex_check2" time="0.0005090236663818359"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="35" name="test_username" time="0.0008552074432373047"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[one]" time="0.0010302066802978516"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[two]" time="0.0009279251098632812"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[three]" time="0.001287698745727539"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; + + def test_parametrized_username(non_parametrized_username): +&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] +E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] + +tests/test_pytest.py:40: AssertionError</failure></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="6" name="test_A" time="0.0006275177001953125"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_one.Test_test1 testMethod=test_A&gt; + + def test_A(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +tests/test_unittest_one.py:8: AssertionError</failure></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="9" name="test_B" time="0.00047135353088378906"></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="12" name="test_c" time="0.0005207061767578125"><skipped message="demonstrating skipping" type="pytest.skip">tests/test_unittest_one.py:12: &lt;py._xmlgen.raw object at 0x102504cc0&gt;</skipped></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="3" name="test_A2" time="0.0006039142608642578"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_two.Test_test2 testMethod=test_A2&gt; + + def test_A2(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +tests/test_unittest_two.py:5: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="6" name="test_B2" time="0.0007021427154541016"></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="9" name="test_C2" time="0.0008001327514648438"><failure message="AssertionError: 1 != 2 : Not equal">self = &lt;test_unittest_two.Test_test2 testMethod=test_C2&gt; + + def test_C2(self): +&gt; self.assertEqual(1,2,&apos;Not equal&apos;) +E AssertionError: 1 != 2 : Not equal + +tests/test_unittest_two.py:11: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="12" name="test_D2" time="0.0005772113800048828"><failure message="ArithmeticError">self = &lt;test_unittest_two.Test_test2 testMethod=test_D2&gt; + + def test_D2(self): +&gt; raise ArithmeticError() +E ArithmeticError + +tests/test_unittest_two.py:14: ArithmeticError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2a" file="tests/test_unittest_two.py" line="17" name="test_222A2" time="0.0005698204040527344"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_two.Test_test2a testMethod=test_222A2&gt; + + def test_222A2(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +tests/test_unittest_two.py:19: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2a" file="tests/test_unittest_two.py" line="20" name="test_222B2" time="0.0004627704620361328"></testcase><testcase classname="tests.unittest_three_test.Test_test3" file="tests/unittest_three_test.py" line="4" name="test_A" time="0.0006659030914306641"><failure message="AssertionError: Not implemented">self = &lt;unittest_three_test.Test_test3 testMethod=test_A&gt; + + def test_A(self): +&gt; self.fail(&quot;Not implemented&quot;) +E AssertionError: Not implemented + +tests/unittest_three_test.py:6: AssertionError</failure></testcase><testcase classname="tests.unittest_three_test.Test_test3" file="tests/unittest_three_test.py" line="7" name="test_B" time="0.0006167888641357422"></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/single/test_root.py b/src/test/pythonFiles/testFiles/single/test_root.py new file mode 100644 index 000000000000..452813e9a079 --- /dev/null +++ b/src/test/pythonFiles/testFiles/single/test_root.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_Root_test1(unittest.TestCase): + def test_Root_A(self): + self.fail("Not implemented") + + def test_Root_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_Root_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/single/tests/test_one.py b/src/test/pythonFiles/testFiles/single/tests/test_one.py new file mode 100644 index 000000000000..e869986b6ead --- /dev/null +++ b/src/test/pythonFiles/testFiles/single/tests/test_one.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_one.py new file mode 100644 index 000000000000..72db843aa2af --- /dev/null +++ b/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_one.py @@ -0,0 +1,19 @@ +import unittest + +class Test_test_one_1(unittest.TestCase): + def test_1_1_1(self): + self.assertEqual(1,1,'Not equal') + + def test_1_1_2(self): + self.assertEqual(1,2,'Not equal') + + @unittest.skip("demonstrating skipping") + def test_1_1_3(self): + self.assertEqual(1,2,'Not equal') + +class Test_test_one_2(unittest.TestCase): + def test_1_2_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_two.py new file mode 100644 index 000000000000..abac1b49023f --- /dev/null +++ b/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_two.py @@ -0,0 +1,19 @@ +import unittest + +class Test_test_two_1(unittest.TestCase): + def test_1_1_1(self): + self.assertEqual(1,1,'Not equal') + + def test_1_1_2(self): + self.assertEqual(1,2,'Not equal') + + @unittest.skip("demonstrating skipping") + def test_1_1_3(self): + self.assertEqual(1,2,'Not equal') + +class Test_test_two_2(unittest.TestCase): + def test_2_1_1(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/standard/test_root.py b/src/test/pythonFiles/testFiles/standard/test_root.py new file mode 100644 index 000000000000..452813e9a079 --- /dev/null +++ b/src/test/pythonFiles/testFiles/standard/test_root.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_Root_test1(unittest.TestCase): + def test_Root_A(self): + self.fail("Not implemented") + + def test_Root_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_Root_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/standard/tests/__init__.py b/src/test/pythonFiles/testFiles/standard/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonFiles/testFiles/standard/tests/external.py b/src/test/pythonFiles/testFiles/standard/tests/external.py new file mode 100644 index 000000000000..e7446cadb184 --- /dev/null +++ b/src/test/pythonFiles/testFiles/standard/tests/external.py @@ -0,0 +1,6 @@ +class ForeignTests: + class TestExtraNestedForeignTests: + def test_super_deep_foreign(self): + assert False + def test_foreign_test(self): + assert False diff --git a/src/test/pythonFiles/testFiles/standard/tests/test_another_pytest.py b/src/test/pythonFiles/testFiles/standard/tests/test_another_pytest.py new file mode 100644 index 000000000000..129bc168f0d5 --- /dev/null +++ b/src/test/pythonFiles/testFiles/standard/tests/test_another_pytest.py @@ -0,0 +1,18 @@ +# content of tests/test_something.py +import pytest +import unittest + +@pytest.fixture +def parametrized_username(): + return 'overridden-username' + +@pytest.fixture(params=['one', 'two', 'three']) +def non_parametrized_username(request): + return request.param + +def test_username(parametrized_username): + assert parametrized_username == 'overridden-username' + +def test_parametrized_username(non_parametrized_username): + assert non_parametrized_username in ['one', 'two', 'threes'] + diff --git a/src/test/pythonFiles/testFiles/standard/tests/test_foreign_nested_tests.py b/src/test/pythonFiles/testFiles/standard/tests/test_foreign_nested_tests.py new file mode 100644 index 000000000000..60df159b4c6d --- /dev/null +++ b/src/test/pythonFiles/testFiles/standard/tests/test_foreign_nested_tests.py @@ -0,0 +1,9 @@ +from .external import ForeignTests + + +class TestNestedForeignTests: + class TestInheritingHere(ForeignTests): + def test_nested_normal(self): + assert True + def test_normal(self): + assert True diff --git a/src/test/pythonFiles/testFiles/standard/tests/test_pytest.py b/src/test/pythonFiles/testFiles/standard/tests/test_pytest.py new file mode 100644 index 000000000000..dc5798306bb6 --- /dev/null +++ b/src/test/pythonFiles/testFiles/standard/tests/test_pytest.py @@ -0,0 +1,41 @@ +# content of tests/test_something.py +import pytest +import unittest + +# content of check_myapp.py +class Test_CheckMyApp: + @unittest.skip("demonstrating skipping") + def test_simple_check(self): + pass + def test_complex_check(self): + pass + + class Test_NestedClassA: + def test_nested_class_methodB(self): + assert True + class Test_nested_classB_Of_A: + def test_d(self): + assert True + def test_nested_class_methodC(self): + assert True + + def test_simple_check2(self): + pass + def test_complex_check2(self): + pass + + +@pytest.fixture +def parametrized_username(): + return 'overridden-username' + +@pytest.fixture(params=['one', 'two', 'three']) +def non_parametrized_username(request): + return request.param + +def test_username(parametrized_username): + assert parametrized_username == 'overridden-username' + +def test_parametrized_username(non_parametrized_username): + assert non_parametrized_username in ['one', 'two', 'threes'] + diff --git a/src/test/pythonFiles/testFiles/standard/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/standard/tests/test_unittest_one.py new file mode 100644 index 000000000000..e869986b6ead --- /dev/null +++ b/src/test/pythonFiles/testFiles/standard/tests/test_unittest_one.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/standard/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/standard/tests/test_unittest_two.py new file mode 100644 index 000000000000..ad89d873e879 --- /dev/null +++ b/src/test/pythonFiles/testFiles/standard/tests/test_unittest_two.py @@ -0,0 +1,32 @@ +import unittest + +class Test_test2(unittest.TestCase): + def test_A2(self): + self.fail("Not implemented") + + def test_B2(self): + self.assertEqual(1,1,'Not equal') + + def test_C2(self): + self.assertEqual(1,2,'Not equal') + + def test_D2(self): + raise ArithmeticError() + pass + +class Test_test2a(unittest.TestCase): + def test_222A2(self): + self.fail("Not implemented") + + def test_222B2(self): + self.assertEqual(1,1,'Not equal') + + class Test_test2a1(unittest.TestCase): + def test_222A2wow(self): + self.fail("Not implemented") + + def test_222B2wow(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/standard/tests/unittest_three_test.py b/src/test/pythonFiles/testFiles/standard/tests/unittest_three_test.py new file mode 100644 index 000000000000..507e6af02063 --- /dev/null +++ b/src/test/pythonFiles/testFiles/standard/tests/unittest_three_test.py @@ -0,0 +1,13 @@ +import unittest + + +class Test_test3(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/unittestsWithConfigs/other/test_pytest.py b/src/test/pythonFiles/testFiles/unittestsWithConfigs/other/test_pytest.py new file mode 100644 index 000000000000..dc5798306bb6 --- /dev/null +++ b/src/test/pythonFiles/testFiles/unittestsWithConfigs/other/test_pytest.py @@ -0,0 +1,41 @@ +# content of tests/test_something.py +import pytest +import unittest + +# content of check_myapp.py +class Test_CheckMyApp: + @unittest.skip("demonstrating skipping") + def test_simple_check(self): + pass + def test_complex_check(self): + pass + + class Test_NestedClassA: + def test_nested_class_methodB(self): + assert True + class Test_nested_classB_Of_A: + def test_d(self): + assert True + def test_nested_class_methodC(self): + assert True + + def test_simple_check2(self): + pass + def test_complex_check2(self): + pass + + +@pytest.fixture +def parametrized_username(): + return 'overridden-username' + +@pytest.fixture(params=['one', 'two', 'three']) +def non_parametrized_username(request): + return request.param + +def test_username(parametrized_username): + assert parametrized_username == 'overridden-username' + +def test_parametrized_username(non_parametrized_username): + assert non_parametrized_username in ['one', 'two', 'threes'] + diff --git a/src/test/pythonFiles/testFiles/unittestsWithConfigs/other/test_unittest_one.py b/src/test/pythonFiles/testFiles/unittestsWithConfigs/other/test_unittest_one.py new file mode 100644 index 000000000000..e869986b6ead --- /dev/null +++ b/src/test/pythonFiles/testFiles/unittestsWithConfigs/other/test_unittest_one.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/unittestsWithConfigs/pytest.ini b/src/test/pythonFiles/testFiles/unittestsWithConfigs/pytest.ini new file mode 100644 index 000000000000..45c88355be9d --- /dev/null +++ b/src/test/pythonFiles/testFiles/unittestsWithConfigs/pytest.ini @@ -0,0 +1,3 @@ +# content of pytest.ini +[pytest] +testpaths = other \ No newline at end of file diff --git a/src/test/pythonFiles/testFiles/unittestsWithConfigs/test_root.py b/src/test/pythonFiles/testFiles/unittestsWithConfigs/test_root.py new file mode 100644 index 000000000000..452813e9a079 --- /dev/null +++ b/src/test/pythonFiles/testFiles/unittestsWithConfigs/test_root.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_Root_test1(unittest.TestCase): + def test_Root_A(self): + self.fail("Not implemented") + + def test_Root_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_Root_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_another_pytest.py b/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_another_pytest.py new file mode 100644 index 000000000000..129bc168f0d5 --- /dev/null +++ b/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_another_pytest.py @@ -0,0 +1,18 @@ +# content of tests/test_something.py +import pytest +import unittest + +@pytest.fixture +def parametrized_username(): + return 'overridden-username' + +@pytest.fixture(params=['one', 'two', 'three']) +def non_parametrized_username(request): + return request.param + +def test_username(parametrized_username): + assert parametrized_username == 'overridden-username' + +def test_parametrized_username(non_parametrized_username): + assert non_parametrized_username in ['one', 'two', 'threes'] + diff --git a/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_pytest.py b/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_pytest.py new file mode 100644 index 000000000000..dc5798306bb6 --- /dev/null +++ b/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_pytest.py @@ -0,0 +1,41 @@ +# content of tests/test_something.py +import pytest +import unittest + +# content of check_myapp.py +class Test_CheckMyApp: + @unittest.skip("demonstrating skipping") + def test_simple_check(self): + pass + def test_complex_check(self): + pass + + class Test_NestedClassA: + def test_nested_class_methodB(self): + assert True + class Test_nested_classB_Of_A: + def test_d(self): + assert True + def test_nested_class_methodC(self): + assert True + + def test_simple_check2(self): + pass + def test_complex_check2(self): + pass + + +@pytest.fixture +def parametrized_username(): + return 'overridden-username' + +@pytest.fixture(params=['one', 'two', 'three']) +def non_parametrized_username(request): + return request.param + +def test_username(parametrized_username): + assert parametrized_username == 'overridden-username' + +def test_parametrized_username(non_parametrized_username): + assert non_parametrized_username in ['one', 'two', 'threes'] + diff --git a/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_unittest_one.py new file mode 100644 index 000000000000..e869986b6ead --- /dev/null +++ b/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_unittest_one.py @@ -0,0 +1,19 @@ +import sys +import os + +import unittest + +class Test_test1(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + @unittest.skip("demonstrating skipping") + def test_c(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_unittest_two.py new file mode 100644 index 000000000000..ad89d873e879 --- /dev/null +++ b/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/test_unittest_two.py @@ -0,0 +1,32 @@ +import unittest + +class Test_test2(unittest.TestCase): + def test_A2(self): + self.fail("Not implemented") + + def test_B2(self): + self.assertEqual(1,1,'Not equal') + + def test_C2(self): + self.assertEqual(1,2,'Not equal') + + def test_D2(self): + raise ArithmeticError() + pass + +class Test_test2a(unittest.TestCase): + def test_222A2(self): + self.fail("Not implemented") + + def test_222B2(self): + self.assertEqual(1,1,'Not equal') + + class Test_test2a1(unittest.TestCase): + def test_222A2wow(self): + self.fail("Not implemented") + + def test_222B2wow(self): + self.assertEqual(1,1,'Not equal') + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/unittest_three_test.py b/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/unittest_three_test.py new file mode 100644 index 000000000000..507e6af02063 --- /dev/null +++ b/src/test/pythonFiles/testFiles/unittestsWithConfigs/tests/unittest_three_test.py @@ -0,0 +1,13 @@ +import unittest + + +class Test_test3(unittest.TestCase): + def test_A(self): + self.fail("Not implemented") + + def test_B(self): + self.assertEqual(1, 1, 'Not equal') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/test/pythonFiles/typeFormatFiles/elseBlocks2.py b/src/test/pythonFiles/typeFormatFiles/elseBlocks2.py new file mode 100644 index 000000000000..da4614982080 --- /dev/null +++ b/src/test/pythonFiles/typeFormatFiles/elseBlocks2.py @@ -0,0 +1,365 @@ +var = 100 +if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var +else: + print "4 - Got a false expression value" + print var + +var = 100 +if var == 200: + print "1 - Got a true expression value" + print var +elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var +else: + print "4 - Got a false expression value" + print var + +var = 100 +if var == 200: + print "1 - Got a true expression value" + print var +elif var == 150: + print "2 - Got a true expression value" + print var +elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + for n in range(2, 10): + for x in range(2, n): + if n % x == 0: + print n, 'equals', x, '*', n/x + break + else: + # loop fell through without finding a factor + print n, 'is a prime number' + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def test(): + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + for n in range(2, 10): + for x in range(2, n): + if n % x == 0: + print n, 'equals', x, '*', n/x + break + else: + # loop fell through without finding a factor + print n, 'is a prime number' + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def ask_ok(prompt, retries=4, complaint='Yes or no, please!'): + while True: + ok = raw_input(prompt) + if ok in ('y', 'ye', 'yes'): + return True + if ok in ('n', 'no', 'nop', 'nope'): + return False + retries = retries - 1 + if retries < 0: + raise IOError('refusenik user') + print complaint + else: + pass + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def minus(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + finally: + print("executing finally clause") + +class DoSomething(): + def test(): + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + for n in range(2, 10): + for x in range(2, n): + if n % x == 0: + print n, 'equals', x, '*', n/x + break + else: + # loop fell through without finding a factor + print n, 'is a prime number' + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def ask_ok(prompt, retries=4, complaint='Yes or no, please!'): + while True: + ok = raw_input(prompt) + if ok in ('y', 'ye', 'yes'): + return True + if ok in ('n', 'no', 'nop', 'nope'): + return False + retries = retries - 1 + if retries < 0: + raise IOError('refusenik user') + print complaint + else: + pass + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def minus(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + finally: + print("executing finally clause") + +var = 100 +if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var +else: + print "4 - Got a false expression value" + print var + +var = 100 +if var == 200: + print "1 - Got a true expression value" + print var + if var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var +else: + print "4 - Got a false expression value" + print var diff --git a/src/test/pythonFiles/typeFormatFiles/elseBlocks4.py b/src/test/pythonFiles/typeFormatFiles/elseBlocks4.py new file mode 100644 index 000000000000..c8213d6c4c12 --- /dev/null +++ b/src/test/pythonFiles/typeFormatFiles/elseBlocks4.py @@ -0,0 +1,351 @@ +var = 100 +if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var +else: + print "4 - Got a false expression value" + print var + +var = 100 +if var == 200: + print "1 - Got a true expression value" + print var +elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var +else: + print "4 - Got a false expression value" + print var + +var = 100 +if var == 200: + print "1 - Got a true expression value" + print var +elif var == 150: + print "2 - Got a true expression value" + print var +elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + for n in range(2, 10): + for x in range(2, n): + if n % x == 0: + print n, 'equals', x, '*', n/x + break + else: + # loop fell through without finding a factor + print n, 'is a prime number' + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def test(): + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + for n in range(2, 10): + for x in range(2, n): + if n % x == 0: + print n, 'equals', x, '*', n/x + break + else: + # loop fell through without finding a factor + print n, 'is a prime number' + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def ask_ok(prompt, retries=4, complaint='Yes or no, please!'): + while True: + ok = raw_input(prompt) + if ok in ('y', 'ye', 'yes'): + return True + if ok in ('n', 'no', 'nop', 'nope'): + return False + retries = retries - 1 + if retries < 0: + raise IOError('refusenik user') + print complaint + else: + pass + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def minus(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + finally: + print("executing finally clause") + +class DoSomething(): + def test(): + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + for n in range(2, 10): + for x in range(2, n): + if n % x == 0: + print n, 'equals', x, '*', n/x + break + else: + # loop fell through without finding a factor + print n, 'is a prime number' + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def ask_ok(prompt, retries=4, complaint='Yes or no, please!'): + while True: + ok = raw_input(prompt) + if ok in ('y', 'ye', 'yes'): + return True + if ok in ('n', 'no', 'nop', 'nope'): + return False + retries = retries - 1 + if retries < 0: + raise IOError('refusenik user') + print complaint + else: + pass + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def minus(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + finally: + print("executing finally clause") + + var = 100 +if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var +else: + print "4 - Got a false expression value" + print var diff --git a/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLine2.py b/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLine2.py new file mode 100644 index 000000000000..b99c1738d297 --- /dev/null +++ b/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLine2.py @@ -0,0 +1,4 @@ +if True == True: + a = 2 + b = 3 + else: \ No newline at end of file diff --git a/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLine4.py b/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLine4.py new file mode 100644 index 000000000000..64ad7dfb7e1a --- /dev/null +++ b/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLine4.py @@ -0,0 +1,4 @@ +if True == True: + a = 2 + b = 3 + else: \ No newline at end of file diff --git a/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLineTab.py b/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLineTab.py new file mode 100644 index 000000000000..39cea5e8caf5 --- /dev/null +++ b/src/test/pythonFiles/typeFormatFiles/elseBlocksFirstLineTab.py @@ -0,0 +1,4 @@ +if True == True: + a = 2 + b = 3 + else: \ No newline at end of file diff --git a/src/test/pythonFiles/typeFormatFiles/elseBlocksTab.py b/src/test/pythonFiles/typeFormatFiles/elseBlocksTab.py new file mode 100644 index 000000000000..e92233ea9ba0 --- /dev/null +++ b/src/test/pythonFiles/typeFormatFiles/elseBlocksTab.py @@ -0,0 +1,351 @@ +var = 100 +if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var +else: + print "4 - Got a false expression value" + print var + +var = 100 +if var == 200: + print "1 - Got a true expression value" + print var +elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var +else: + print "4 - Got a false expression value" + print var + +var = 100 +if var == 200: + print "1 - Got a true expression value" + print var +elif var == 150: + print "2 - Got a true expression value" + print var +elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + for n in range(2, 10): + for x in range(2, n): + if n % x == 0: + print n, 'equals', x, '*', n/x + break + else: + # loop fell through without finding a factor + print n, 'is a prime number' + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def test(): + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + for n in range(2, 10): + for x in range(2, n): + if n % x == 0: + print n, 'equals', x, '*', n/x + break + else: + # loop fell through without finding a factor + print n, 'is a prime number' + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def ask_ok(prompt, retries=4, complaint='Yes or no, please!'): + while True: + ok = raw_input(prompt) + if ok in ('y', 'ye', 'yes'): + return True + if ok in ('n', 'no', 'nop', 'nope'): + return False + retries = retries - 1 + if retries < 0: + raise IOError('refusenik user') + print complaint + else: + pass + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def minus(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + finally: + print("executing finally clause") + +class DoSomething(): + def test(): + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + var = 100 + if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var + else: + print "4 - Got a false expression value" + print var + + for n in range(2, 10): + for x in range(2, n): + if n % x == 0: + print n, 'equals', x, '*', n/x + break + else: + # loop fell through without finding a factor + print n, 'is a prime number' + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def ask_ok(prompt, retries=4, complaint='Yes or no, please!'): + while True: + ok = raw_input(prompt) + if ok in ('y', 'ye', 'yes'): + return True + if ok in ('n', 'no', 'nop', 'nope'): + return False + retries = retries - 1 + if retries < 0: + raise IOError('refusenik user') + print complaint + else: + pass + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def minus(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #except should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + + def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + finally: + print("executing finally clause") + + var = 100 +if var == 200: + print "1 - Got a true expression value" + print var + elif var == 150: + print "2 - Got a true expression value" + print var + elif var == 100: + print "3 - Got a true expression value" + print var +else: + print "4 - Got a false expression value" + print var diff --git a/src/test/pythonFiles/typeFormatFiles/tryBlocks2.py b/src/test/pythonFiles/typeFormatFiles/tryBlocks2.py new file mode 100644 index 000000000000..504feeeb3ca2 --- /dev/null +++ b/src/test/pythonFiles/typeFormatFiles/tryBlocks2.py @@ -0,0 +1,208 @@ + +while True: + try: + x = int(input("Please enter a number: ")) + break + # except should be in same column as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + + +while True: + try: + x = int(input("Please enter a number: ")) + break + # except should be in same column as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + +class B(Exception): + pass + +class C(B): + pass + +class D(C): + pass + +for cls in [B, C, D]: + try: + raise cls() + except D: + print("D") + except C: + print("C") + # except should be in same level as except + except B: + print("B") + + +for cls in [B, C, D]: + try: + raise cls() + except D: + print("D") + except C: + print("C") + # except should be in same level as except + except B: + print("B") + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + #except should be in same level as try + except IOError: + print('cannot open', arg) + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + #except should be in same level as try + except IOError: + print('cannot open', arg) + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def minus(): + while True: + try: + x = int(input("Please enter a number: ")) + break + #except should be in same level as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + +def minus(): + while True: + try: + x = int(input("Please enter a number: ")) + break + #except should be in same level as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + + +def zero(): + for cls in [B, C, D]: + try: + raise cls() + #except should be in same level as try: + except D: + print("D") + except C: + print("C") + except B: + print("B") + +def zero(): + for cls in [B, C, D]: + try: + raise cls() + except D: + print("D") + #except should be in same level as try: + except C: + print("C") + except B: + print("B") + +def one(): + import sys + + try: + f = open('myfile.txt') + s = f.readline() + i = int(s.strip()) + except OSError as err: + print("OS error: {0}".format(err)) + # except should be in same level as except + except ValueError: + print("Could not convert data to an integer.") + except: + print("Unexpected error:", sys.exc_info()[0]) + raise + +def one(): + import sys + + try: + f = open('myfile.txt') + s = f.readline() + i = int(s.strip()) + # except should be in same level as except + except OSError as err: + print("OS error: {0}".format(err)) + except ValueError: + print("Could not convert data to an integer.") + except: + print("Unexpected error:", sys.exc_info()[0]) + raise + +def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + # finally should be in same level as except + finally: + print("executing finally clause") + +def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + # finally should be in same level as except + finally: + print("executing finally clause") \ No newline at end of file diff --git a/src/test/pythonFiles/typeFormatFiles/tryBlocks4.py b/src/test/pythonFiles/typeFormatFiles/tryBlocks4.py new file mode 100644 index 000000000000..ce9e444cabbf --- /dev/null +++ b/src/test/pythonFiles/typeFormatFiles/tryBlocks4.py @@ -0,0 +1,208 @@ + +while True: + try: + x = int(input("Please enter a number: ")) + break + # except should be in same column as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + + +while True: + try: + x = int(input("Please enter a number: ")) + break + # except should be in same column as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + +class B(Exception): + pass + +class C(B): + pass + +class D(C): + pass + +for cls in [B, C, D]: + try: + raise cls() + except D: + print("D") + except C: + print("C") + # except should be in same level as except + except B: + print("B") + + +for cls in [B, C, D]: + try: + raise cls() + except D: + print("D") + except C: + print("C") + # except should be in same level as except + except B: + print("B") + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + #except should be in same level as try + except IOError: + print('cannot open', arg) + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + #except should be in same level as try + except IOError: + print('cannot open', arg) + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def minus(): + while True: + try: + x = int(input("Please enter a number: ")) + break + #except should be in same level as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + +def minus(): + while True: + try: + x = int(input("Please enter a number: ")) + break + #except should be in same level as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + + +def zero(): + for cls in [B, C, D]: + try: + raise cls() + #except should be in same level as try: + except D: + print("D") + except C: + print("C") + except B: + print("B") + +def zero(): + for cls in [B, C, D]: + try: + raise cls() + except D: + print("D") + #except should be in same level as try: + except C: + print("C") + except B: + print("B") + +def one(): + import sys + + try: + f = open('myfile.txt') + s = f.readline() + i = int(s.strip()) + except OSError as err: + print("OS error: {0}".format(err)) + # except should be in same level as except + except ValueError: + print("Could not convert data to an integer.") + except: + print("Unexpected error:", sys.exc_info()[0]) + raise + +def one(): + import sys + + try: + f = open('myfile.txt') + s = f.readline() + i = int(s.strip()) + # except should be in same level as except + except OSError as err: + print("OS error: {0}".format(err)) + except ValueError: + print("Could not convert data to an integer.") + except: + print("Unexpected error:", sys.exc_info()[0]) + raise + +def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + # finally should be in same level as except + finally: + print("executing finally clause") + +def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + # finally should be in same level as except + finally: + print("executing finally clause") \ No newline at end of file diff --git a/src/test/pythonFiles/typeFormatFiles/tryBlocksTab.py b/src/test/pythonFiles/typeFormatFiles/tryBlocksTab.py new file mode 100644 index 000000000000..d94e057d493e --- /dev/null +++ b/src/test/pythonFiles/typeFormatFiles/tryBlocksTab.py @@ -0,0 +1,208 @@ + +while True: + try: + x = int(input("Please enter a number: ")) + break + # except should be in same column as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + + +while True: + try: + x = int(input("Please enter a number: ")) + break + # except should be in same column as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + +class B(Exception): + pass + +class C(B): + pass + +class D(C): + pass + +for cls in [B, C, D]: + try: + raise cls() + except D: + print("D") + except C: + print("C") + # except should be in same level as except + except B: + print("B") + + +for cls in [B, C, D]: + try: + raise cls() + except D: + print("D") + except C: + print("C") + # except should be in same level as except + except B: + print("B") + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + #except should be in same level as try + except IOError: + print('cannot open', arg) + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + #except should be in same level as try + except IOError: + print('cannot open', arg) + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + #else should be in same level as try + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def minus(): + while True: + try: + x = int(input("Please enter a number: ")) + break + #except should be in same level as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + +def minus(): + while True: + try: + x = int(input("Please enter a number: ")) + break + #except should be in same level as try: + except ValueError: + print("Oops! That was no valid number. Try again...") + + +def zero(): + for cls in [B, C, D]: + try: + raise cls() + #except should be in same level as try: + except D: + print("D") + except C: + print("C") + except B: + print("B") + +def zero(): + for cls in [B, C, D]: + try: + raise cls() + except D: + print("D") + #except should be in same level as try: + except C: + print("C") + except B: + print("B") + +def one(): + import sys + + try: + f = open('myfile.txt') + s = f.readline() + i = int(s.strip()) + except OSError as err: + print("OS error: {0}".format(err)) + # except should be in same level as except + except ValueError: + print("Could not convert data to an integer.") + except: + print("Unexpected error:", sys.exc_info()[0]) + raise + +def one(): + import sys + + try: + f = open('myfile.txt') + s = f.readline() + i = int(s.strip()) + # except should be in same level as except + except OSError as err: + print("OS error: {0}".format(err)) + except ValueError: + print("Could not convert data to an integer.") + except: + print("Unexpected error:", sys.exc_info()[0]) + raise + +def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def two(): + for arg in sys.argv[1:]: + try: + f = open(arg, 'r') + except IOError: + print('cannot open', arg) + # else should be in same level as except + else: + print(arg, 'has', len(f.readlines()), 'lines') + f.close() + +def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + # finally should be in same level as except + finally: + print("executing finally clause") + +def divide(x, y): + try: + result = x / y + except ZeroDivisionError: + print("division by zero!") + else: + print("result is", result) + # finally should be in same level as except + finally: + print("executing finally clause") \ No newline at end of file diff --git a/src/test/refactor/extension.refactor.extract.method.test.ts b/src/test/refactor/extension.refactor.extract.method.test.ts new file mode 100644 index 000000000000..48c51428673a --- /dev/null +++ b/src/test/refactor/extension.refactor.extract.method.test.ts @@ -0,0 +1,204 @@ +// tslint:disable:interface-name no-any max-func-body-length estrict-plus-operands no-empty + +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import { + commands, + Position, + Range, + Selection, + TextEditorCursorStyle, + TextEditorLineNumbersStyle, + TextEditorOptions, + Uri, + window, + workspace +} from 'vscode'; +import { getTextEditsFromPatch } from '../../client/common/editor'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../client/common/process/types'; +import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; +import { InterpreterService } from '../../client/interpreter/interpreterService'; +import { extractMethod } from '../../client/providers/simpleRefactorProvider'; +import { CondaService } from '../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { RefactorProxy } from '../../client/refactor/proxy'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; +import { closeActiveWindows, initialize, initializeTest } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; + +const refactorSourceFile = path.join( + __dirname, + '..', + '..', + '..', + 'src', + 'test', + 'pythonFiles', + 'refactoring', + 'standAlone', + 'refactor.py' +); +const refactorTargetFileDir = path.join( + __dirname, + '..', + '..', + '..', + 'out', + 'test', + 'pythonFiles', + 'refactoring', + 'standAlone' +); + +interface RenameResponse { + results: [{ diff: string }]; +} + +suite('Method Extraction', () => { + // Hack hac hack + const oldExecuteCommand = commands.executeCommand; + const options: TextEditorOptions = { + cursorStyle: TextEditorCursorStyle.Line, + insertSpaces: true, + lineNumbers: TextEditorLineNumbersStyle.Off, + tabSize: 4 + }; + let refactorTargetFile = ''; + let ioc: UnitTestIocContainer; + suiteSetup(initialize); + suiteTeardown(() => { + commands.executeCommand = oldExecuteCommand; + return closeActiveWindows(); + }); + setup(async () => { + initializeDI(); + refactorTargetFile = path.join(refactorTargetFileDir, `refactor${new Date().getTime()}.py`); + fs.copySync(refactorSourceFile, refactorTargetFile, { overwrite: true }); + await initializeTest(); + (commands as any).executeCommand = (_cmd: any) => Promise.resolve(); + }); + teardown(async () => { + commands.executeCommand = oldExecuteCommand; + try { + await fs.unlink(refactorTargetFile); + } catch {} + await closeActiveWindows(); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerProcessTypes(); + ioc.registerVariableTypes(); + ioc.registerInterpreterStorageTypes(); + ioc.registerMockInterpreterTypes(); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, instance(mock(CondaService))); + ioc.serviceManager.rebindInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); + } + function createPythonExecGetter(workspaceRoot: string): () => Promise<IPythonExecutionService> { + return async () => { + const factory = ioc.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); + return factory.create({ resource: Uri.file(workspaceRoot) }); + }; + } + + async function testingMethodExtraction(shouldError: boolean, startPos: Position, endPos: Position): Promise<void> { + const rangeOfTextToExtract = new Range(startPos, endPos); + const workspaceRoot = path.dirname(refactorTargetFile); + const proxy = new RefactorProxy(workspaceRoot, createPythonExecGetter(workspaceRoot)); + + // tslint:disable-next-line:no-multiline-string + const DIFF = `--- a/refactor.py\n+++ b/refactor.py\n@@ -237,9 +237,12 @@\n try:\n self._process_request(self._input.readline())\n except Exception as ex:\n- message = ex.message + ' \\n' + traceback.format_exc()\n- sys.stderr.write(str(len(message)) + ':' + message)\n- sys.stderr.flush()\n+ self.myNewMethod(ex)\n+\n+ def myNewMethod(self, ex):\n+ message = ex.message + ' \\n' + traceback.format_exc()\n+ sys.stderr.write(str(len(message)) + ':' + message)\n+ sys.stderr.flush()\n \n if __name__ == '__main__':\n RopeRefactoring().watch()\n`; + const mockTextDoc = await workspace.openTextDocument(refactorTargetFile); + const expectedTextEdits = getTextEditsFromPatch(mockTextDoc.getText(), DIFF); + try { + const response = await proxy.extractMethod<RenameResponse>( + mockTextDoc, + 'myNewMethod', + refactorTargetFile, + rangeOfTextToExtract, + options + ); + if (shouldError) { + assert.fail('No error', 'Error', 'Extraction should fail with an error', ''); + } + const textEdits = getTextEditsFromPatch(mockTextDoc.getText(), DIFF); + assert.equal(response.results.length, 1, 'Invalid number of items in response'); + assert.equal(textEdits.length, expectedTextEdits.length, 'Invalid number of Text Edits'); + textEdits.forEach((edit) => { + const foundEdit = expectedTextEdits.filter( + (item) => item.newText === edit.newText && item.range.isEqual(edit.range) + ); + assert.equal(foundEdit.length, 1, 'Edit not found'); + }); + } catch (error) { + if (!shouldError) { + // Wait a minute this shouldn't work, what's going on + assert.equal('Error', 'No error', `${error}`); + } + } + } + + test('Extract Method', async () => { + const startPos = new Position(239, 0); + const endPos = new Position(241, 35); + await testingMethodExtraction(false, startPos, endPos); + }); + + test('Extract Method will fail if complete statements are not selected', async () => { + const startPos = new Position(239, 30); + const endPos = new Position(241, 35); + await testingMethodExtraction(true, startPos, endPos); + }); + + async function testingMethodExtractionEndToEnd( + shouldError: boolean, + startPos: Position, + endPos: Position + ): Promise<void> { + const ch = new MockOutputChannel('Python'); + const rangeOfTextToExtract = new Range(startPos, endPos); + + const textDocument = await workspace.openTextDocument(refactorTargetFile); + const editor = await window.showTextDocument(textDocument); + + editor.selections = [new Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end)]; + editor.selection = new Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end); + + try { + await extractMethod(editor, rangeOfTextToExtract, ch, ioc.serviceContainer); + if (shouldError) { + assert.fail('No error', 'Error', 'Extraction should fail with an error', ''); + } + + const newMethodRefLine = textDocument.lineAt(editor.selection.start); + assert.equal(ch.output.length, 0, 'Output channel is not empty'); + assert.equal( + textDocument + .lineAt(newMethodRefLine.lineNumber + 2) + .text.trim() + .indexOf('def newmethod'), + 0, + 'New Method not created' + ); + assert.equal(newMethodRefLine.text.trim().startsWith('self.newmethod'), true, 'New Method not being used'); + } catch (error) { + if (!shouldError) { + assert.equal('Error', 'No error', `${error}`); + } + } + } + + // This test fails on linux (text document not getting updated in time) + test('Extract Method (end to end)', async () => { + const startPos = new Position(239, 0); + const endPos = new Position(241, 35); + await testingMethodExtractionEndToEnd(false, startPos, endPos); + }); + + test('Extract Method will fail if complete statements are not selected', async () => { + const startPos = new Position(239, 30); + const endPos = new Position(241, 35); + await testingMethodExtractionEndToEnd(true, startPos, endPos); + }); +}); diff --git a/src/test/refactor/extension.refactor.extract.var.test.ts b/src/test/refactor/extension.refactor.extract.var.test.ts new file mode 100644 index 000000000000..dd178ffe7906 --- /dev/null +++ b/src/test/refactor/extension.refactor.extract.var.test.ts @@ -0,0 +1,207 @@ +// tslint:disable:interface-name no-any max-func-body-length estrict-plus-operands no-empty + +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { + commands, + Position, + Range, + Selection, + TextEditorCursorStyle, + TextEditorLineNumbersStyle, + TextEditorOptions, + Uri, + window, + workspace +} from 'vscode'; +import { getTextEditsFromPatch } from '../../client/common/editor'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../client/common/process/types'; +import { extractVariable } from '../../client/providers/simpleRefactorProvider'; +import { RefactorProxy } from '../../client/refactor/proxy'; +import { isPythonVersion } from '../common'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; +import { closeActiveWindows, initialize, initializeTest, IS_CI_SERVER } from './../initialize'; +import { MockOutputChannel } from './../mockClasses'; + +const refactorSourceFile = path.join( + __dirname, + '..', + '..', + '..', + 'src', + 'test', + 'pythonFiles', + 'refactoring', + 'standAlone', + 'refactor.py' +); +const refactorTargetFileDir = path.join( + __dirname, + '..', + '..', + '..', + 'out', + 'test', + 'pythonFiles', + 'refactoring', + 'standAlone' +); + +interface RenameResponse { + results: [{ diff: string }]; +} + +suite('Variable Extraction', () => { + // Hack hac hack + const oldExecuteCommand = commands.executeCommand; + const options: TextEditorOptions = { + cursorStyle: TextEditorCursorStyle.Line, + insertSpaces: true, + lineNumbers: TextEditorLineNumbersStyle.Off, + tabSize: 4 + }; + let refactorTargetFile = ''; + let ioc: UnitTestIocContainer; + suiteSetup(initialize); + suiteTeardown(() => { + commands.executeCommand = oldExecuteCommand; + return closeActiveWindows(); + }); + setup(async () => { + initializeDI(); + refactorTargetFile = path.join(refactorTargetFileDir, `refactor${new Date().getTime()}.py`); + fs.copySync(refactorSourceFile, refactorTargetFile, { overwrite: true }); + await initializeTest(); + (<any>commands).executeCommand = (_cmd: any) => Promise.resolve(); + }); + teardown(async () => { + commands.executeCommand = oldExecuteCommand; + try { + await fs.unlink(refactorTargetFile); + } catch {} + await closeActiveWindows(); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerProcessTypes(); + ioc.registerVariableTypes(); + ioc.registerInterpreterStorageTypes(); + ioc.registerMockInterpreterTypes(); + } + function createPythonExecGetter(workspaceRoot: string): () => Promise<IPythonExecutionService> { + return async () => { + const factory = ioc.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); + return factory.create({ resource: Uri.file(workspaceRoot) }); + }; + } + + async function testingVariableExtraction( + shouldError: boolean, + startPos: Position, + endPos: Position + ): Promise<void> { + const rangeOfTextToExtract = new Range(startPos, endPos); + const workspaceRoot = path.dirname(refactorTargetFile); + const proxy = new RefactorProxy(workspaceRoot, createPythonExecGetter(workspaceRoot)); + + const DIFF = + '--- a/refactor.py\n+++ b/refactor.py\n@@ -232,7 +232,8 @@\n sys.stdout.flush()\n \n def watch(self):\n- self._write_response("STARTED")\n+ myNewVariable = "STARTED"\n+ self._write_response(myNewVariable)\n while True:\n try:\n self._process_request(self._input.readline())\n'; + const mockTextDoc = await workspace.openTextDocument(refactorTargetFile); + const expectedTextEdits = getTextEditsFromPatch(mockTextDoc.getText(), DIFF); + try { + const response = await proxy.extractVariable<RenameResponse>( + mockTextDoc, + 'myNewVariable', + refactorTargetFile, + rangeOfTextToExtract, + options + ); + if (shouldError) { + assert.fail('No error', 'Error', 'Extraction should fail with an error', ''); + } + const textEdits = getTextEditsFromPatch(mockTextDoc.getText(), DIFF); + assert.equal(response.results.length, 1, 'Invalid number of items in response'); + assert.equal(textEdits.length, expectedTextEdits.length, 'Invalid number of Text Edits'); + textEdits.forEach((edit) => { + const foundEdit = expectedTextEdits.filter( + (item) => item.newText === edit.newText && item.range.isEqual(edit.range) + ); + assert.equal(foundEdit.length, 1, 'Edit not found'); + }); + } catch (error) { + if (!shouldError) { + assert.equal('Error', 'No error', `${error}`); + } + } + } + + // tslint:disable-next-line:no-function-expression + test('Extract Variable', async function () { + if (isPythonVersion('3.7')) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } else { + const startPos = new Position(234, 29); + const endPos = new Position(234, 38); + await testingVariableExtraction(false, startPos, endPos); + } + }); + + test('Extract Variable fails if whole string not selected', async () => { + const startPos = new Position(234, 20); + const endPos = new Position(234, 38); + await testingVariableExtraction(true, startPos, endPos); + }); + + async function testingVariableExtractionEndToEnd( + shouldError: boolean, + startPos: Position, + endPos: Position + ): Promise<void> { + const ch = new MockOutputChannel('Python'); + const rangeOfTextToExtract = new Range(startPos, endPos); + + const textDocument = await workspace.openTextDocument(refactorTargetFile); + const editor = await window.showTextDocument(textDocument); + + editor.selections = [new Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end)]; + editor.selection = new Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end); + try { + await extractVariable(editor, rangeOfTextToExtract, ch, ioc.serviceContainer); + if (shouldError) { + assert.fail('No error', 'Error', 'Extraction should fail with an error', ''); + } + assert.equal(ch.output.length, 0, 'Output channel is not empty'); + + const newVarDefLine = textDocument.lineAt(editor.selection.start); + const newVarRefLine = textDocument.lineAt(newVarDefLine.lineNumber + 1); + + assert.equal(newVarDefLine.text.trim().indexOf('newvariable'), 0, 'New Variable not created'); + assert.equal(newVarDefLine.text.trim().endsWith('= "STARTED"'), true, 'Started Text Assigned to variable'); + assert.equal(newVarRefLine.text.indexOf('(newvariable') >= 0, true, 'New Variable not being used'); + } catch (error) { + if (!shouldError) { + assert.fail('Error', 'No error', `${error}`); + } + } + } + + // This test fails on linux (text document not getting updated in time) + test('Extract Variable (end to end)', async function () { + if (!IS_CI_SERVER) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + const startPos = new Position(234, 29); + const endPos = new Position(234, 38); + await testingVariableExtractionEndToEnd(false, startPos, endPos); + }); + + test('Extract Variable fails if whole string not selected (end to end)', async () => { + const startPos = new Position(234, 20); + const endPos = new Position(234, 38); + await testingVariableExtractionEndToEnd(true, startPos, endPos); + }); +}); diff --git a/src/test/refactor/rename.test.ts b/src/test/refactor/rename.test.ts new file mode 100644 index 000000000000..a192bf36dc56 --- /dev/null +++ b/src/test/refactor/rename.test.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { EOL } from 'os'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import * as typeMoq from 'typemoq'; +import { + Range, + TextEditorCursorStyle, + TextEditorLineNumbersStyle, + TextEditorOptions, + Uri, + window, + workspace +} from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import '../../client/common/extensions'; +import { IPlatformService } from '../../client/common/platform/types'; +import { BufferDecoder } from '../../client/common/process/decoder'; +import { ProcessService } from '../../client/common/process/proc'; +import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; +import { + IProcessLogger, + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonExecutionService +} from '../../client/common/process/types'; +import { IConfigurationService, IPythonSettings } from '../../client/common/types'; +import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; +import { IServiceContainer } from '../../client/ioc/types'; +import { WindowsStoreInterpreter } from '../../client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter'; +import { RefactorProxy } from '../../client/refactor/proxy'; +import { PYTHON_PATH } from '../common'; +import { closeActiveWindows, initialize, initializeTest } from './../initialize'; + +// tslint:disable:no-any +// tslint:disable: max-func-body-length + +type RenameResponse = { + results: [{ diff: string }]; +}; + +suite('Refactor Rename', () => { + const options: TextEditorOptions = { + cursorStyle: TextEditorCursorStyle.Line, + insertSpaces: true, + lineNumbers: TextEditorLineNumbersStyle.Off, + tabSize: 4 + }; + let pythonSettings: typeMoq.IMock<IPythonSettings>; + let serviceContainer: typeMoq.IMock<IServiceContainer>; + suiteSetup(initialize); + setup(async () => { + pythonSettings = typeMoq.Mock.ofType<IPythonSettings>(); + pythonSettings.setup((p) => p.pythonPath).returns(() => PYTHON_PATH); + const configService = typeMoq.Mock.ofType<IConfigurationService>(); + configService.setup((c) => c.getSettings(typeMoq.It.isAny())).returns(() => pythonSettings.object); + const condaService = typeMoq.Mock.ofType<ICondaService>(); + const processServiceFactory = typeMoq.Mock.ofType<IProcessServiceFactory>(); + processServiceFactory + .setup((p) => p.create(typeMoq.It.isAny())) + .returns(() => Promise.resolve(new ProcessService(new BufferDecoder()))); + const interpreterService = typeMoq.Mock.ofType<IInterpreterService>(); + interpreterService.setup((i) => i.hasInterpreters).returns(() => Promise.resolve(true)); + const envActivationService = typeMoq.Mock.ofType<IEnvironmentActivationService>(); + envActivationService + .setup((e) => e.getActivatedEnvironmentVariables(typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + envActivationService + .setup((e) => e.getActivatedEnvironmentVariables(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + envActivationService + .setup((e) => + e.getActivatedEnvironmentVariables(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(undefined)); + serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IConfigurationService), typeMoq.It.isAny())) + .returns(() => configService.object); + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IProcessServiceFactory), typeMoq.It.isAny())) + .returns(() => processServiceFactory.object); + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IInterpreterService), typeMoq.It.isAny())) + .returns(() => interpreterService.object); + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IEnvironmentActivationService), typeMoq.It.isAny())) + .returns(() => envActivationService.object); + const windowsStoreInterpreter = mock(WindowsStoreInterpreter); + const platformService = mock<IPlatformService>(); + + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IPythonExecutionFactory), typeMoq.It.isAny())) + .returns( + () => + new PythonExecutionFactory( + serviceContainer.object, + undefined as any, + processServiceFactory.object, + configService.object, + condaService.object, + undefined as any, + instance(windowsStoreInterpreter), + instance(platformService) + ) + ); + const processLogger = typeMoq.Mock.ofType<IProcessLogger>(); + processLogger + .setup((p) => p.logProcess(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + return; + }); + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IProcessLogger), typeMoq.It.isAny())) + .returns(() => processLogger.object); + await initializeTest(); + }); + teardown(closeActiveWindows); + suiteTeardown(closeActiveWindows); + function createPythonExecGetter(workspaceRoot: string): () => Promise<IPythonExecutionService> { + return async () => { + const factory = serviceContainer.object.get<IPythonExecutionFactory>(IPythonExecutionFactory); + return factory.create({ resource: Uri.file(workspaceRoot) }); + }; + } + + test('Rename function in source without a trailing empty line', async () => { + const sourceFile = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'pythonFiles', + 'refactoring', + 'source folder', + 'without empty line.py' + ); + const expectedDiff = `--- a/${path.basename(sourceFile)}${EOL}+++ b/${path.basename( + sourceFile + )}${EOL}@@ -1,8 +1,8 @@${EOL} import os${EOL} ${EOL}-def one():${EOL}+def three():${EOL} return True${EOL} ${EOL} def two():${EOL}- if one():${EOL}- print(\"A\" + one())${EOL}+ if three():${EOL}+ print(\"A\" + three())${EOL}`.splitLines( + { removeEmptyEntries: false, trim: false } + ); + const workspaceRoot = path.dirname(sourceFile); + + const proxy = new RefactorProxy(workspaceRoot, createPythonExecGetter(workspaceRoot)); + const textDocument = await workspace.openTextDocument(sourceFile); + await window.showTextDocument(textDocument); + + const response = await proxy.rename<RenameResponse>( + textDocument, + 'three', + sourceFile, + new Range(7, 20, 7, 23), + options + ); + expect(response.results).to.be.lengthOf(1); + expect(response.results[0].diff.splitLines({ removeEmptyEntries: false, trim: false })).to.be.deep.equal( + expectedDiff + ); + }); + test('Rename function in source with a trailing empty line', async () => { + const sourceFile = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'pythonFiles', + 'refactoring', + 'source folder', + 'with empty line.py' + ); + const expectedDiff = `--- a/${path.basename(sourceFile)}${EOL}+++ b/${path.basename( + sourceFile + )}${EOL}@@ -1,8 +1,8 @@${EOL} import os${EOL} ${EOL}-def one():${EOL}+def three():${EOL} return True${EOL} ${EOL} def two():${EOL}- if one():${EOL}- print(\"A\" + one())${EOL}+ if three():${EOL}+ print(\"A\" + three())${EOL}`.splitLines( + { removeEmptyEntries: false, trim: false } + ); + const workspaceRoot = path.dirname(sourceFile); + + const proxy = new RefactorProxy(workspaceRoot, createPythonExecGetter(workspaceRoot)); + const textDocument = await workspace.openTextDocument(sourceFile); + await window.showTextDocument(textDocument); + + const response = await proxy.rename<RenameResponse>( + textDocument, + 'three', + sourceFile, + new Range(7, 20, 7, 23), + options + ); + expect(response.results).to.be.lengthOf(1); + expect(response.results[0].diff.splitLines({ removeEmptyEntries: false, trim: false })).to.be.deep.equal( + expectedDiff + ); + }); +}); diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts new file mode 100644 index 000000000000..4322740192ad --- /dev/null +++ b/src/test/serviceRegistry.ts @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fsextra from 'fs-extra'; +import { Container } from 'inversify'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Memento, OutputChannel, Uri } from 'vscode'; +import { STANDARD_OUTPUT_CHANNEL } from '../client/common/constants'; +import { IS_WINDOWS } from '../client/common/platform/constants'; +import { convertStat, FileSystem, FileSystemUtils, RawFileSystem } from '../client/common/platform/fileSystem'; +import { PathUtils } from '../client/common/platform/pathUtils'; +import { PlatformService } from '../client/common/platform/platformService'; +import { RegistryImplementation } from '../client/common/platform/registry'; +import { registerTypes as platformRegisterTypes } from '../client/common/platform/serviceRegistry'; +import { FileStat, FileType, IFileSystem, IPlatformService, IRegistry } from '../client/common/platform/types'; +import { BufferDecoder } from '../client/common/process/decoder'; +import { ProcessService } from '../client/common/process/proc'; +import { PythonExecutionFactory } from '../client/common/process/pythonExecutionFactory'; +import { PythonToolExecutionService } from '../client/common/process/pythonToolService'; +import { registerTypes as processRegisterTypes } from '../client/common/process/serviceRegistry'; +import { + IBufferDecoder, + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonToolExecutionService +} from '../client/common/process/types'; +import { registerTypes as commonRegisterTypes } from '../client/common/serviceRegistry'; +import { + GLOBAL_MEMENTO, + ICurrentProcess, + IDisposableRegistry, + IMemento, + IOutputChannel, + IPathUtils, + IsWindows, + WORKSPACE_MEMENTO +} from '../client/common/types'; +import { createDeferred } from '../client/common/utils/async'; +import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; +import { registerTypes as formattersRegisterTypes } from '../client/formatters/serviceRegistry'; +import { EnvironmentActivationService } from '../client/interpreter/activation/service'; +import { IEnvironmentActivationService } from '../client/interpreter/activation/types'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSeletionProxyService +} from '../client/interpreter/autoSelection/types'; +import { IInterpreterService } from '../client/interpreter/contracts'; +import { InterpreterService } from '../client/interpreter/interpreterService'; +import { registerInterpreterTypes } from '../client/interpreter/serviceRegistry'; +import { ServiceContainer } from '../client/ioc/container'; +import { ServiceManager } from '../client/ioc/serviceManager'; +import { IServiceContainer, IServiceManager } from '../client/ioc/types'; +import { registerTypes as lintersRegisterTypes } from '../client/linters/serviceRegistry'; +import { registerForIOC } from '../client/pythonEnvironments/legacyIOC'; +import { TEST_OUTPUT_CHANNEL } from '../client/testing/common/constants'; +import { registerTypes as unittestsRegisterTypes } from '../client/testing/serviceRegistry'; +import { MockOutputChannel } from './mockClasses'; +import { MockAutoSelectionService } from './mocks/autoSelector'; +import { MockMemento } from './mocks/mementos'; +import { MockProcessService } from './mocks/proc'; +import { MockProcess } from './mocks/process'; + +// This is necessary for unit tests and functional tests, since they +// do not run under VS Code so they do not have access to the actual +// "vscode" namespace. +export class FakeVSCodeFileSystemAPI { + public async readFile(uri: Uri): Promise<Uint8Array> { + return fsextra.readFile(uri.fsPath); + } + public async writeFile(uri: Uri, content: Uint8Array): Promise<void> { + return fsextra.writeFile(uri.fsPath, Buffer.from(content)); + } + public async delete(uri: Uri, _options?: { recursive: boolean; useTrash: boolean }): Promise<void> { + return ( + fsextra + // Make sure the file exists before deleting. + .stat(uri.fsPath) + .then(() => fsextra.remove(uri.fsPath)) + ); + } + public async stat(uri: Uri): Promise<FileStat> { + const filename = uri.fsPath; + + let filetype = FileType.Unknown; + let stat = await fsextra.lstat(filename); + if (stat.isSymbolicLink()) { + filetype = FileType.SymbolicLink; + stat = await fsextra.stat(filename); + } + if (stat.isFile()) { + filetype |= FileType.File; + } else if (stat.isDirectory()) { + filetype |= FileType.Directory; + } + return convertStat(stat, filetype); + } + public async readDirectory(uri: Uri): Promise<[string, FileType][]> { + const names: string[] = await fsextra.readdir(uri.fsPath); + const promises = names.map((name) => { + const filename = path.join(uri.fsPath, name); + return ( + fsextra + // Get the lstat info and deal with symlinks if necessary. + .lstat(filename) + .then(async (stat) => { + let filetype = FileType.Unknown; + if (stat.isFile()) { + filetype = FileType.File; + } else if (stat.isDirectory()) { + filetype = FileType.Directory; + } else if (stat.isSymbolicLink()) { + filetype = FileType.SymbolicLink; + stat = await fsextra.stat(filename); + if (stat.isFile()) { + filetype |= FileType.File; + } else if (stat.isDirectory()) { + filetype |= FileType.Directory; + } + } + return [name, filetype] as [string, FileType]; + }) + .catch(() => [name, FileType.Unknown] as [string, FileType]) + ); + }); + return Promise.all(promises); + } + public async createDirectory(uri: Uri): Promise<void> { + return fsextra.mkdirp(uri.fsPath); + } + public async copy(src: Uri, dest: Uri): Promise<void> { + const deferred = createDeferred<void>(); + const rs = fsextra + // Set an error handler on the stream. + .createReadStream(src.fsPath) + .on('error', (err) => { + deferred.reject(err); + }); + const ws = fsextra + .createWriteStream(dest.fsPath) + // Set an error & close handler on the stream. + .on('error', (err) => { + deferred.reject(err); + }) + .on('close', () => { + deferred.resolve(); + }); + rs.pipe(ws); + return deferred.promise; + } + public async rename(src: Uri, dest: Uri): Promise<void> { + return fsextra.rename(src.fsPath, dest.fsPath); + } +} +export class LegacyFileSystem extends FileSystem { + constructor() { + super(); + const vscfs = new FakeVSCodeFileSystemAPI(); + const raw = RawFileSystem.withDefaults(undefined, vscfs); + this.utils = FileSystemUtils.withDefaults(raw); + } +} + +export class IocContainer { + // This may be set (before any registration happens) to indicate + // whether or not IOC should depend on the VS Code API (e.g. the + // "vscode" module). So in "functional" tests, this should be set + // to "false". + public useVSCodeAPI = true; + + public readonly serviceManager: IServiceManager; + public readonly serviceContainer: IServiceContainer; + + private disposables: Disposable[] = []; + + constructor() { + const cont = new Container(); + this.serviceManager = new ServiceManager(cont); + this.serviceContainer = new ServiceContainer(cont); + + this.serviceManager.addSingletonInstance<IServiceContainer>(IServiceContainer, this.serviceContainer); + this.serviceManager.addSingletonInstance<Disposable[]>(IDisposableRegistry, this.disposables); + this.serviceManager.addSingleton<Memento>(IMemento, MockMemento, GLOBAL_MEMENTO); + this.serviceManager.addSingleton<Memento>(IMemento, MockMemento, WORKSPACE_MEMENTO); + + const stdOutputChannel = new MockOutputChannel('Python'); + this.disposables.push(stdOutputChannel); + this.serviceManager.addSingletonInstance<OutputChannel>( + IOutputChannel, + stdOutputChannel, + STANDARD_OUTPUT_CHANNEL + ); + const testOutputChannel = new MockOutputChannel('Python Test - UnitTests'); + this.disposables.push(testOutputChannel); + this.serviceManager.addSingletonInstance<OutputChannel>(IOutputChannel, testOutputChannel, TEST_OUTPUT_CHANNEL); + + this.serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService + ); + this.serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>( + IInterpreterAutoSeletionProxyService, + MockAutoSelectionService + ); + } + public async dispose(): Promise<void> { + for (const disposable of this.disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise<any>; + if (promise) { + await promise; + } + } + this.disposables = []; + this.serviceManager.dispose(); + } + + public registerCommonTypes(registerFileSystem: boolean = true) { + commonRegisterTypes(this.serviceManager); + if (registerFileSystem) { + this.registerFileSystemTypes(); + } + } + public registerFileSystemTypes() { + this.serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService); + this.serviceManager.addSingleton<IFileSystem>( + IFileSystem, + // Maybe use fake vscode.workspace.filesystem API: + this.useVSCodeAPI ? FileSystem : LegacyFileSystem + ); + } + public registerProcessTypes() { + processRegisterTypes(this.serviceManager); + const mockEnvironmentActivationService = mock(EnvironmentActivationService); + when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); + when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenResolve(); + when( + mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve(); + this.serviceManager.addSingletonInstance<IEnvironmentActivationService>( + IEnvironmentActivationService, + instance(mockEnvironmentActivationService) + ); + } + public registerVariableTypes() { + variableRegisterTypes(this.serviceManager); + } + public registerUnitTestTypes() { + unittestsRegisterTypes(this.serviceManager); + } + public registerLinterTypes() { + lintersRegisterTypes(this.serviceManager); + } + public registerFormatterTypes() { + formattersRegisterTypes(this.serviceManager); + } + public registerPlatformTypes() { + platformRegisterTypes(this.serviceManager); + } + public registerInterpreterTypes() { + // This method registers all interpreter types except `IInterpreterAutoSeletionProxyService` & `IEnvironmentActivationService`, as it's already registered in the constructor & registerMockProcessTypes() respectively + registerInterpreterTypes(this.serviceManager); + } + public registerMockProcessTypes() { + this.serviceManager.addSingleton<IBufferDecoder>(IBufferDecoder, BufferDecoder); + const processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + // tslint:disable-next-line:no-any + const processService = new MockProcessService(new ProcessService(new BufferDecoder(), process.env as any)); + processServiceFactory.setup((f) => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService)); + this.serviceManager.addSingletonInstance<IProcessServiceFactory>( + IProcessServiceFactory, + processServiceFactory.object + ); + this.serviceManager.addSingleton<IPythonExecutionFactory>(IPythonExecutionFactory, PythonExecutionFactory); + this.serviceManager.addSingleton<IPythonToolExecutionService>( + IPythonToolExecutionService, + PythonToolExecutionService + ); + this.serviceManager.addSingleton<IEnvironmentActivationService>( + IEnvironmentActivationService, + EnvironmentActivationService + ); + const mockEnvironmentActivationService = mock(EnvironmentActivationService); + when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); + when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenResolve(); + when( + mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve(); + this.serviceManager.rebindInstance<IEnvironmentActivationService>( + IEnvironmentActivationService, + instance(mockEnvironmentActivationService) + ); + } + + public registerMockInterpreterTypes() { + this.serviceManager.addSingleton<IInterpreterService>(IInterpreterService, InterpreterService); + this.serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation); + registerForIOC(this.serviceManager, this.serviceContainer); + } + + public registerMockProcess() { + this.serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS); + + this.serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); + this.serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, MockProcess); + } +} diff --git a/src/test/smoke/common.ts b/src/test/smoke/common.ts new file mode 100644 index 000000000000..4c8fa12b97cd --- /dev/null +++ b/src/test/smoke/common.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-invalid-this no-default-export no-console + +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import * as glob from 'glob'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { SMOKE_TEST_EXTENSIONS_DIR } from '../constants'; +import { noop, sleep } from '../core'; + +export async function updateSetting(setting: string, value: any) { + const resource = vscode.workspace.workspaceFolders![0].uri; + await vscode.workspace + .getConfiguration('python', resource) + .update(setting, value, vscode.ConfigurationTarget.WorkspaceFolder); +} +export async function removeLanguageServerFiles() { + const folders = await getLanguageServerFolders(); + await Promise.all(folders.map((item) => fs.remove(item).catch(noop))); +} +async function getLanguageServerFolders(): Promise<string[]> { + return new Promise<string[]>((resolve, reject) => { + glob('languageServer.*', { cwd: SMOKE_TEST_EXTENSIONS_DIR }, (ex, matches) => { + ex ? reject(ex) : resolve(matches.map((item) => path.join(SMOKE_TEST_EXTENSIONS_DIR, item))); + }); + }); +} +export function isJediEnabled() { + const resource = vscode.workspace.workspaceFolders![0].uri; + const settings = vscode.workspace.getConfiguration('python', resource); + return settings.get<string>('languageServer') === 'Jedi'; +} +export async function enableJedi(enable: boolean | undefined) { + if (isJediEnabled() === enable) { + return; + } + await updateSetting('languageServer', 'Jedi'); +} +export async function openFileAndWaitForLS(file: string): Promise<vscode.TextDocument> { + const textDocument = await vscode.workspace.openTextDocument(file); + await vscode.window.showTextDocument(textDocument); + assert(vscode.window.activeTextEditor, 'No active editor'); + // Make sure LS completes file loading and analysis. + // In test mode it awaits for the completion before trying + // to fetch data for completion, hover.etc. + await vscode.commands.executeCommand( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + new vscode.Position(0, 0) + ); + // For for LS to get extracted. + await sleep(10_000); + return textDocument; +} diff --git a/src/test/smoke/datascience.smoke.test.ts b/src/test/smoke/datascience.smoke.test.ts new file mode 100644 index 000000000000..0712239654e5 --- /dev/null +++ b/src/test/smoke/datascience.smoke.test.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-invalid-this no-any + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { openFile, setAutoSaveDelayInWorkspaceRoot, waitForCondition } from '../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; +import { noop, sleep } from '../core'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; + +const timeoutForCellToRun = 3 * 60 * 1_000; + +suite('Smoke Test: Interactive Window', () => { + suiteSetup(async function () { + if (!IS_SMOKE_TEST) { + return this.skip(); + } + await initialize(); + await setAutoSaveDelayInWorkspaceRoot(1); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('Run Cell in interactive window', async () => { + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'pythonFiles', + 'datascience', + 'simple_note_book.py' + ); + const outputFile = path.join(path.dirname(file), 'ds.log'); + if (await fs.pathExists(outputFile)) { + await fs.unlink(outputFile); + } + const textDocument = await openFile(file); + + // Wait for code lenses to get detected. + await sleep(1_000); + + await vscode.commands.executeCommand<void>('python.datascience.runallcells', textDocument.uri); + const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); + await waitForCondition(checkIfFileHasBeenCreated, timeoutForCellToRun, `"${outputFile}" file not created`); + }).timeout(timeoutForCellToRun); + + test('Run Cell in native editor', async () => { + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'pythonFiles', + 'datascience', + 'simple_nb.ipynb' + ); + const fileContents = await fs.readFile(file, { encoding: 'utf-8' }); + const outputFile = path.join(path.dirname(file), 'ds_n.log'); + await fs.writeFile(file, fileContents.replace("'ds_n.log'", `'${outputFile.replace(/\\/g, '/')}'`), { + encoding: 'utf-8' + }); + if (await fs.pathExists(outputFile)) { + await fs.unlink(outputFile); + } + // Ignore exceptions (as native editor closes the document as soon as its opened); + await openFile(file).catch(noop); + + // Wait for 15 seconds for notebook to launch. + // Unfortunately there's no way to know for sure it has completely loaded. + await sleep(15_000); + + await vscode.commands.executeCommand<void>('python.datascience.notebookeditor.runallcells'); + const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); + await waitForCondition(checkIfFileHasBeenCreated, timeoutForCellToRun, `"${outputFile}" file not created`); + + // Give time for the file to be saved before we shutdown + await sleep(300); + }).timeout(timeoutForCellToRun); +}); diff --git a/src/test/smoke/languageServer.smoke.test.ts b/src/test/smoke/languageServer.smoke.test.ts new file mode 100644 index 000000000000..1e21bda47b94 --- /dev/null +++ b/src/test/smoke/languageServer.smoke.test.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-invalid-this no-any + +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { updateSetting } from '../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; +import { sleep } from '../core'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { openFileAndWaitForLS } from './common'; + +const fileDefinitions = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'definitions.py' +); + +suite('Smoke Test: Language Server', () => { + suiteSetup(async function () { + if (!IS_SMOKE_TEST) { + return this.skip(); + } + await updateSetting( + 'linting.ignorePatterns', + ['**/dir1/**'], + vscode.workspace.workspaceFolders![0].uri, + vscode.ConfigurationTarget.WorkspaceFolder + ); + await initialize(); + }); + setup(async () => { + await initializeTest(); + await closeActiveWindows(); + }); + suiteTeardown(async () => { + await closeActiveWindows(); + await updateSetting( + 'linting.ignorePatterns', + undefined, + vscode.workspace.workspaceFolders![0].uri, + vscode.ConfigurationTarget.WorkspaceFolder + ); + }); + teardown(closeActiveWindows); + + test('Definitions', async () => { + const startPosition = new vscode.Position(13, 6); + const textDocument = await openFileAndWaitForLS(fileDefinitions); + let tested = false; + for (let i = 0; i < 5; i += 1) { + const locations = await vscode.commands.executeCommand<vscode.Location[]>( + 'vscode.executeDefinitionProvider', + textDocument.uri, + startPosition + ); + if (locations && locations.length > 0) { + expect(locations![0].uri.fsPath).to.contain(path.basename(fileDefinitions)); + tested = true; + break; + } else { + // Wait for LS to start. + await sleep(5_000); + } + } + if (!tested) { + assert.fail('Failled to test definitions'); + } + }); +}); diff --git a/src/test/smoke/runInTerminal.smoke.test.ts b/src/test/smoke/runInTerminal.smoke.test.ts new file mode 100644 index 000000000000..013e6bff396d --- /dev/null +++ b/src/test/smoke/runInTerminal.smoke.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-invalid-this no-any + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { openFile, waitForCondition } from '../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; + +suite('Smoke Test: Run Python File In Terminal', () => { + suiteSetup(async function () { + if (!IS_SMOKE_TEST) { + return this.skip(); + } + await initialize(); + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('Exec', async () => { + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'testExecInTerminal.py' + ); + const outputFile = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'testExecInTerminal.log' + ); + if (await fs.pathExists(outputFile)) { + await fs.unlink(outputFile); + } + const textDocument = await openFile(file); + + await vscode.commands.executeCommand<void>('python.execInTerminal', textDocument.uri); + const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); + await waitForCondition(checkIfFileHasBeenCreated, 30_000, `"${outputFile}" file not created`); + }); +}); diff --git a/src/test/smokeTest.ts b/src/test/smokeTest.ts new file mode 100644 index 000000000000..79e44f1dcf3d --- /dev/null +++ b/src/test/smokeTest.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-console no-require-imports no-var-requires + +// Must always be on top to setup expected env. +process.env.VSC_PYTHON_SMOKE_TEST = '1'; + +import { spawn } from 'child_process'; +import * as fs from 'fs-extra'; +import * as glob from 'glob'; +import * as path from 'path'; +import { unzip } from './common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, SMOKE_TEST_EXTENSIONS_DIR } from './constants'; + +class TestRunner { + public async start() { + console.log('Start Test Runner'); + await this.enableLanguageServer(true); + await this.extractLatestExtension(SMOKE_TEST_EXTENSIONS_DIR); + await this.launchSmokeTests(); + } + private async launchSmokeTests() { + const env: Record<string, {}> = { + VSC_PYTHON_SMOKE_TEST: '1', + CODE_EXTENSIONS_PATH: SMOKE_TEST_EXTENSIONS_DIR + }; + + await this.launchTest(env); + } + private async enableLanguageServer(enable: boolean) { + // When running smoke tests, we won't have access to unbundled files. + const settings = `{ "python.languageServer": ${enable ? '"Microsoft"' : '"Jedi"'} }`; + await fs.ensureDir( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', '.vscode') + ); + await fs.writeFile( + path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + '.vscode', + 'settings.json' + ), + settings + ); + } + private async launchTest(customEnvVars: Record<string, {}>) { + console.log('Launch tests in test runner'); + await new Promise((resolve, reject) => { + const env: Record<string, string> = { + TEST_FILES_SUFFIX: 'smoke.test', + IS_SMOKE_TEST: 'true', + CODE_TESTS_WORKSPACE: path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests' + ), + ...process.env, + ...customEnvVars + }; + const proc = spawn('node', [path.join(__dirname, 'standardTest.js')], { + cwd: EXTENSION_ROOT_DIR_FOR_TESTS, + env + }); + proc.stdout.pipe(process.stdout); + proc.stderr.pipe(process.stderr); + proc.on('error', reject); + proc.on('exit', (code) => { + console.log(`Tests Exited with code ${code}`); + if (code === 0) { + resolve(); + } else { + reject(`Failed with code ${code}.`); + } + }); + }); + } + + private async extractLatestExtension(targetDir: string): Promise<void> { + const extensionFile = await new Promise<string>((resolve, reject) => + glob('*.vsix', (ex, files) => (ex ? reject(ex) : resolve(files[0]))) + ); + await unzip(extensionFile, targetDir); + } +} + +new TestRunner().start().catch((ex) => { + console.error('Error in running Smoke Tests', ex); + // Exit with non zero exit code, so CI fails. + process.exit(1); +}); diff --git a/src/test/sourceMapSupport.test.ts b/src/test/sourceMapSupport.test.ts new file mode 100644 index 000000000000..db7148b20831 --- /dev/null +++ b/src/test/sourceMapSupport.test.ts @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any no-unused-expression chai-vague-errors no-unnecessary-override max-func-body-length max-classes-per-file + +import { expect } from 'chai'; +import * as fs from 'fs'; +import { ConfigurationTarget, Disposable } from 'vscode'; +import { FileSystem } from '../client/common/platform/fileSystem'; +import { Diagnostics } from '../client/common/utils/localize'; +import { SourceMapSupport } from '../client/sourceMapSupport'; +import { noop } from './core'; + +suite('Source Map Support', () => { + function createVSCStub(isEnabled: boolean = false, selectDisableButton: boolean = false) { + const stubInfo = { + configValueRetrieved: false, + configValueUpdated: false, + messageDisplayed: false + }; + const vscode = { + workspace: { + getConfiguration: (setting: string, _defaultValue: any) => { + if (setting !== 'python.diagnostics') { + return; + } + return { + get: (prop: string) => { + stubInfo.configValueRetrieved = prop === 'sourceMapsEnabled'; + return isEnabled; + }, + update: (prop: string, value: boolean, scope: ConfigurationTarget) => { + if ( + prop === 'sourceMapsEnabled' && + value === false && + scope === ConfigurationTarget.Global + ) { + stubInfo.configValueUpdated = true; + } + } + }; + } + }, + window: { + showWarningMessage: () => { + stubInfo.messageDisplayed = true; + return Promise.resolve(selectDisableButton ? Diagnostics.disableSourceMaps() : undefined); + } + }, + ConfigurationTarget: ConfigurationTarget + }; + return { stubInfo, vscode }; + } + + const disposables: Disposable[] = []; + teardown(() => { + disposables.forEach((disposable) => { + try { + disposable.dispose(); + } catch { + noop(); + } + }); + }); + test('When disabling source maps, the map file is renamed and vice versa', async () => { + const fileSystem = new FileSystem(); + const jsFile = await fileSystem.createTemporaryFile('.js'); + disposables.push(jsFile); + const mapFile = `${jsFile.filePath}.map`; + disposables.push({ + dispose: () => fs.unlinkSync(mapFile) + }); + await fileSystem.writeFile(mapFile, 'ABC'); + expect(await fileSystem.fileExists(mapFile)).to.be.true; + + const stub = createVSCStub(true, true); + const instance = new (class extends SourceMapSupport { + public async enableSourceMap(enable: boolean, sourceFile: string) { + return super.enableSourceMap(enable, sourceFile); + } + })(stub.vscode as any); + + await instance.enableSourceMap(false, jsFile.filePath); + + expect(await fileSystem.fileExists(jsFile.filePath)).to.be.equal(true, 'Source file does not exist'); + expect(await fileSystem.fileExists(mapFile)).to.be.equal(false, 'Source map file not renamed'); + expect(await fileSystem.fileExists(`${mapFile}.disabled`)).to.be.equal(true, 'Expected renamed file not found'); + + await instance.enableSourceMap(true, jsFile.filePath); + + expect(await fileSystem.fileExists(jsFile.filePath)).to.be.equal(true, 'Source file does not exist'); + expect(await fileSystem.fileExists(mapFile)).to.be.equal(true, 'Source map file not found'); + expect(await fileSystem.fileExists(`${mapFile}.disabled`)).to.be.equal(false, 'Source map file not renamed'); + }); +}); diff --git a/src/test/sourceMapSupport.unit.test.ts b/src/test/sourceMapSupport.unit.test.ts new file mode 100644 index 000000000000..437a7463269e --- /dev/null +++ b/src/test/sourceMapSupport.unit.test.ts @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-unused-expression chai-vague-errors no-unnecessary-override max-func-body-length max-classes-per-file match-default-export-name + +import { expect } from 'chai'; +import * as path from 'path'; +import rewiremock from 'rewiremock'; +import * as sinon from 'sinon'; +import { ConfigurationTarget, Disposable } from 'vscode'; +import { Diagnostics } from '../client/common/utils/localize'; +import { EXTENSION_ROOT_DIR } from '../client/constants'; +import { initialize, SourceMapSupport } from '../client/sourceMapSupport'; +import { noop, sleep } from './core'; + +suite('Source Map Support', () => { + function createVSCStub(isEnabled: boolean = false, selectDisableButton: boolean = false) { + const stubInfo = { + configValueRetrieved: false, + configValueUpdated: false, + messageDisplayed: false + }; + const vscode = { + workspace: { + // tslint:disable-next-line: no-any + getConfiguration: (setting: string, _defaultValue: any) => { + if (setting !== 'python.diagnostics') { + return; + } + return { + get: (prop: string) => { + stubInfo.configValueRetrieved = prop === 'sourceMapsEnabled'; + return isEnabled; + }, + update: (prop: string, value: boolean, scope: ConfigurationTarget) => { + if ( + prop === 'sourceMapsEnabled' && + value === false && + scope === ConfigurationTarget.Global + ) { + stubInfo.configValueUpdated = true; + } + } + }; + } + }, + window: { + showWarningMessage: () => { + stubInfo.messageDisplayed = true; + return Promise.resolve(selectDisableButton ? Diagnostics.disableSourceMaps() : undefined); + } + }, + ConfigurationTarget: ConfigurationTarget + }; + return { stubInfo, vscode }; + } + + const disposables: Disposable[] = []; + teardown(() => { + rewiremock.disable(); + disposables.forEach((disposable) => { + try { + disposable.dispose(); + } catch { + noop(); + } + }); + }); + test('Test message is not displayed when source maps are not enabled', async () => { + const stub = createVSCStub(false); + // tslint:disable-next-line: no-any + initialize(stub.vscode as any); + await sleep(100); + expect(stub.stubInfo.configValueRetrieved).to.be.equal(true, 'Config Value not retrieved'); + expect(stub.stubInfo.messageDisplayed).to.be.equal(false, 'Message displayed'); + }); + test('Test message is displayed when source maps are not enabled', async () => { + const stub = createVSCStub(true); + const instance = new (class extends SourceMapSupport { + protected async enableSourceMaps(_enable: boolean) { + noop(); + } + // tslint:disable-next-line: no-any + })(stub.vscode as any); + rewiremock.enable(); + const installStub = sinon.stub(); + rewiremock('source-map-support').with({ install: installStub }); + await instance.initialize(); + + expect(installStub.callCount).to.be.equal(1); + expect(stub.stubInfo.configValueRetrieved).to.be.equal(true, 'Config Value not retrieved'); + expect(stub.stubInfo.messageDisplayed).to.be.equal(true, 'Message displayed'); + expect(stub.stubInfo.configValueUpdated).to.be.equal(false, 'Config Value updated'); + }); + test('Test message is not displayed when source maps are not enabled', async () => { + const stub = createVSCStub(true, true); + const instance = new (class extends SourceMapSupport { + protected async enableSourceMaps(_enable: boolean) { + noop(); + } + // tslint:disable-next-line: no-any + })(stub.vscode as any); + + await instance.initialize(); + expect(stub.stubInfo.configValueRetrieved).to.be.equal(true, 'Config Value not retrieved'); + expect(stub.stubInfo.messageDisplayed).to.be.equal(true, 'Message displayed'); + expect(stub.stubInfo.configValueUpdated).to.be.equal(true, 'Config Value not updated'); + }); + async function testRenamingFilesWhenEnablingDisablingSourceMaps(enableSourceMaps: boolean) { + const stub = createVSCStub(true, true); + const sourceFilesPassed: string[] = []; + const instance = new (class extends SourceMapSupport { + public async enableSourceMaps(enable: boolean) { + return super.enableSourceMaps(enable); + } + public async enableSourceMap(enable: boolean, sourceFile: string) { + expect(enable).to.equal(enableSourceMaps); + sourceFilesPassed.push(sourceFile); + return Promise.resolve(); + } + // tslint:disable-next-line: no-any + })(stub.vscode as any); + + await instance.enableSourceMaps(enableSourceMaps); + const extensionSourceMap = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'extension.js'); + const debuggerSourceMap = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'debugAdapter', 'main.js'); + expect(sourceFilesPassed).to.deep.equal([extensionSourceMap, debuggerSourceMap]); + } + test('Rename extension and debugger source maps when enabling source maps', () => + testRenamingFilesWhenEnablingDisablingSourceMaps(true)); + test('Rename extension and debugger source maps when disabling source maps', () => + testRenamingFilesWhenEnablingDisablingSourceMaps(false)); +}); diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts new file mode 100644 index 000000000000..4a6dd9f2e287 --- /dev/null +++ b/src/test/standardTest.ts @@ -0,0 +1,39 @@ +// tslint:disable:no-console + +import * as path from 'path'; +import { runTests } from 'vscode-test'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; +import { initializeLogger } from './testLogger'; + +initializeLogger(); + +process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; +process.env.VSC_PYTHON_CI_TEST = '1'; +const workspacePath = process.env.CODE_TESTS_WORKSPACE + ? process.env.CODE_TESTS_WORKSPACE + : path.join(__dirname, '..', '..', 'src', 'test'); +const extensionDevelopmentPath = process.env.CODE_EXTENSIONS_PATH + ? process.env.CODE_EXTENSIONS_PATH + : EXTENSION_ROOT_DIR_FOR_TESTS; + +const channel = (process.env.VSC_PYTHON_CI_TEST_VSC_CHANNEL || '').toLowerCase().includes('insiders') + ? 'insiders' + : 'stable'; + +function start() { + console.log('*'.repeat(100)); + console.log('Start Standard tests'); + runTests({ + extensionDevelopmentPath: extensionDevelopmentPath, + extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), + launchArgs: ['--disable-extensions', workspacePath] + .concat(channel === 'insiders' ? ['--enable-proposed-api'] : []) + .concat(['--timeout', '5000']), + version: channel, + extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' } + }).catch((ex) => { + console.error('End Standard tests (with errors)', ex); + process.exit(1); + }); +} +start(); diff --git a/src/test/startPage/startPage.functional.test.tsx b/src/test/startPage/startPage.functional.test.tsx new file mode 100644 index 000000000000..803a43688f81 --- /dev/null +++ b/src/test/startPage/startPage.functional.test.tsx @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import { ComponentClass, mount, ReactWrapper } from 'enzyme'; +import * as React from 'react'; +import { IStartPage } from '../../client/common/startPage/types'; +import { StartPage } from '../../datascience-ui/startPage/startPage'; +import { DataScienceIocContainer } from '../datascience/dataScienceIocContainer'; + +suite('StartPage tests', () => { + let start: IStartPage; + let ioc: DataScienceIocContainer; + + setup(async () => { + process.env.UITEST_DISABLE_INSIDERS = '1'; + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(); + await ioc.activate(); + }); + + teardown(async () => { + await ioc.dispose(); + }); + + // tslint:disable-next-line: no-any + function mountWebView(): ReactWrapper<any, Readonly<{}>, React.Component> { + // Setup our webview panel + const wrapper = ioc.createWebView( + () => mount(<StartPage skipDefault={true} baseTheme={'vscode-light'} testMode={true} />), + 'default' + ); + + // Make sure the plot viewer provider and execution factory in the container is created (the extension does this on startup in the extension) + start = ioc.get<IStartPage>(IStartPage); + + return wrapper.wrapper; + } + + // tslint:disable:no-any + function runMountedTest( + name: string, + testFunc: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void> + ) { + test(name, async () => { + const wrapper = mountWebView(); + try { + await testFunc(wrapper); + } finally { + // Make sure to unmount the wrapper or it will interfere with other tests + if (wrapper && wrapper.length) { + wrapper.unmount(); + } + } + }); + } + + function waitForComponentDidUpdate<P, S, C>(component: React.Component<P, S, C>): Promise<void> { + return new Promise((resolve, reject) => { + if (component) { + let originalUpdateFunc = component.componentDidUpdate; + if (originalUpdateFunc) { + originalUpdateFunc = originalUpdateFunc.bind(component); + } + + // tslint:disable-next-line:no-any + component.componentDidUpdate = (prevProps: Readonly<P>, prevState: Readonly<S>, snapshot?: any) => { + // When the component updates, call the original function and resolve our promise + if (originalUpdateFunc) { + originalUpdateFunc(prevProps, prevState, snapshot); + } + + // Reset our update function + component.componentDidUpdate = originalUpdateFunc; + + // Finish the promise + resolve(); + }; + } else { + reject('Cannot find the component for waitForComponentDidUpdate'); + } + }); + } + + async function waitForUpdate<P, S, C>(wrapper: ReactWrapper<P, S, C>, mainClass: ComponentClass<P>): Promise<void> { + const mainObj = wrapper.find(mainClass).instance(); + if (mainObj) { + // First wait for the update + await waitForComponentDidUpdate(mainObj); + + // Force a render + wrapper.update(); + } + } + + async function waitForStartPage(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>): Promise<void> { + // Get a render promise with the expected number of renders + const renderPromise = waitForUpdate(wrapper, StartPage); + + // Call our function to add a plot + await start.open(); + + // Wait for all of the renders to go through + await renderPromise; + } + + const startPageDom = + '<div class="title-row"><div class="title-icon"><i class="image-button-image"></i></div><div class="title">'; + + runMountedTest('Load Start Page', async (wrapper) => { + await waitForStartPage(wrapper); + const dom = wrapper.getDOMNode(); + assert.ok(dom.innerHTML.includes(startPageDom), 'DOM is not loading correctly'); + }); +}); diff --git a/src/test/startPage/startPage.unit.test.ts b/src/test/startPage/startPage.unit.test.ts new file mode 100644 index 000000000000..4c06f56d7245 --- /dev/null +++ b/src/test/startPage/startPage.unit.test.ts @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import * as assert from 'assert'; +import * as typemoq from 'typemoq'; +import { Memento } from 'vscode'; +import { + IApplicationEnvironment, + IApplicationShell, + ICommandManager, + IDocumentManager, + IWebviewPanelProvider, + IWorkspaceService +} from '../../client/common/application/types'; +import { IFileSystem } from '../../client/common/platform/types'; +import { StartPage } from '../../client/common/startPage/startPage'; +import { IStartPage } from '../../client/common/startPage/types'; +import { IConfigurationService, IExtensionContext } from '../../client/common/types'; +import { ICodeCssGenerator, INotebookEditorProvider, IThemeFinder } from '../../client/datascience/types'; +import { MockPythonSettings } from '../datascience/mockPythonSettings'; +import { MockAutoSelectionService } from '../mocks/autoSelector'; + +suite('StartPage tests', () => { + let startPage: IStartPage; + let provider: typemoq.IMock<IWebviewPanelProvider>; + let cssGenerator: typemoq.IMock<ICodeCssGenerator>; + let themeFinder: typemoq.IMock<IThemeFinder>; + let configuration: typemoq.IMock<IConfigurationService>; + let workspaceService: typemoq.IMock<IWorkspaceService>; + let file: typemoq.IMock<IFileSystem>; + let notebookEditorProvider: typemoq.IMock<INotebookEditorProvider>; + let commandManager: typemoq.IMock<ICommandManager>; + let documentManager: typemoq.IMock<IDocumentManager>; + let appShell: typemoq.IMock<IApplicationShell>; + let context: typemoq.IMock<IExtensionContext>; + let appEnvironment: typemoq.IMock<IApplicationEnvironment>; + let memento: typemoq.IMock<Memento>; + const dummySettings = new MockPythonSettings(undefined, new MockAutoSelectionService()); + + function setupVersions(savedVersion: string, actualVersion: string) { + context.setup((c) => c.globalState).returns(() => memento.object); + memento.setup((m) => m.get(typemoq.It.isAnyString())).returns(() => savedVersion); + memento + .setup((m) => m.update(typemoq.It.isAnyString(), typemoq.It.isAnyString())) + .returns(() => Promise.resolve()); + const packageJson = { + version: actualVersion + }; + appEnvironment.setup((ae) => ae.packageJson).returns(() => packageJson); + } + + function reset() { + context.reset(); + memento.reset(); + appEnvironment.reset(); + } + + setup(async () => { + provider = typemoq.Mock.ofType<IWebviewPanelProvider>(); + cssGenerator = typemoq.Mock.ofType<ICodeCssGenerator>(); + themeFinder = typemoq.Mock.ofType<IThemeFinder>(); + configuration = typemoq.Mock.ofType<IConfigurationService>(); + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + file = typemoq.Mock.ofType<IFileSystem>(); + notebookEditorProvider = typemoq.Mock.ofType<INotebookEditorProvider>(); + commandManager = typemoq.Mock.ofType<ICommandManager>(); + documentManager = typemoq.Mock.ofType<IDocumentManager>(); + appShell = typemoq.Mock.ofType<IApplicationShell>(); + context = typemoq.Mock.ofType<IExtensionContext>(); + appEnvironment = typemoq.Mock.ofType<IApplicationEnvironment>(); + memento = typemoq.Mock.ofType<Memento>(); + + configuration.setup((cs) => cs.getSettings(undefined)).returns(() => dummySettings); + + startPage = new StartPage( + provider.object, + cssGenerator.object, + themeFinder.object, + configuration.object, + workspaceService.object, + file.object, + notebookEditorProvider.object, + commandManager.object, + documentManager.object, + appShell.object, + context.object, + appEnvironment.object + ); + }); + + test('Check extension version', async () => { + let savedVersion: string; + let actualVersion: string; + + // Version has not changed + savedVersion = '2020.6.0-dev'; + actualVersion = '2020.6.0-dev'; + setupVersions(savedVersion, actualVersion); + + const test1 = await startPage.extensionVersionChanged(); + assert.equal(test1, false, 'The version is the same, start page should not open.'); + reset(); + + // actual version is older + savedVersion = '2020.6.0-dev'; + actualVersion = '2020.5.0-dev'; + setupVersions(savedVersion, actualVersion); + + const test2 = await startPage.extensionVersionChanged(); + assert.equal(test2, false, 'The actual version is older, start page should not open.'); + reset(); + + // actual version is newer + savedVersion = '2020.6.0-dev'; + actualVersion = '2020.6.1'; + setupVersions(savedVersion, actualVersion); + + const test3 = await startPage.extensionVersionChanged(); + assert.equal(test3, true, 'The actual version is newer, start page should open.'); + reset(); + }); +}); diff --git a/src/test/startupTelemetry.unit.test.ts b/src/test/startupTelemetry.unit.test.ts new file mode 100644 index 000000000000..0099f4d4ff5e --- /dev/null +++ b/src/test/startupTelemetry.unit.test.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../client/common/application/types'; +import { DeprecatePythonPath } from '../client/common/experiments/groups'; +import { IExperimentsManager, IInterpreterPathService } from '../client/common/types'; +import { IServiceContainer } from '../client/ioc/types'; +import { hasUserDefinedPythonPath } from '../client/startupTelemetry'; + +suite('Startup Telemetry - hasUserDefinedPythonPath()', async () => { + const resource = Uri.parse('a'); + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let experimentsManager: TypeMoq.IMock<IExperimentsManager>; + let interpreterPathService: TypeMoq.IMock<IInterpreterPathService>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + experimentsManager = TypeMoq.Mock.ofType<IExperimentsManager>(); + interpreterPathService = TypeMoq.Mock.ofType<IInterpreterPathService>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + serviceContainer.setup((s) => s.get(IExperimentsManager)).returns(() => experimentsManager.object); + serviceContainer.setup((s) => s.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); + }); + + function setupConfigProvider(): TypeMoq.IMock<WorkspaceConfiguration> { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) + .returns(() => workspaceConfig.object); + return workspaceConfig; + } + + [undefined, 'python'].forEach((globalValue) => { + [undefined, 'python'].forEach((workspaceValue) => { + [undefined, 'python'].forEach((workspaceFolderValue) => { + test(`Return false if using settings equals {globalValue: ${globalValue}, workspaceValue: ${workspaceValue}, workspaceFolderValue: ${workspaceFolderValue}}`, () => { + experimentsManager + .setup((e) => e.inExperiment(DeprecatePythonPath.experiment)) + .returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig + .setup((w) => w.inspect('pythonPath')) + // tslint:disable-next-line: no-any + .returns(() => ({ globalValue, workspaceValue, workspaceFolderValue } as any)); + const result = hasUserDefinedPythonPath(resource, serviceContainer.object); + expect(result).to.equal(false, 'Should be false'); + }); + }); + }); + }); + + test('Return true if using setting value equals something else', () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + const workspaceConfig = setupConfigProvider(); + // tslint:disable-next-line: no-any + workspaceConfig.setup((w) => w.inspect('pythonPath')).returns(() => ({ globalValue: 'something else' } as any)); + const result = hasUserDefinedPythonPath(resource, serviceContainer.object); + expect(result).to.equal(true, 'Should be true'); + }); + + test('If in Deprecate PythonPath experiment, use the new API to inspect settings', () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + interpreterPathService + .setup((i) => i.inspect(resource)) + .returns(() => ({})) + .verifiable(TypeMoq.Times.once()); + hasUserDefinedPythonPath(resource, serviceContainer.object); + interpreterPathService.verifyAll(); + }); +}); diff --git a/src/test/telemetry/envFileTelemetry.unit.test.ts b/src/test/telemetry/envFileTelemetry.unit.test.ts new file mode 100644 index 000000000000..9a37cf76cc47 --- /dev/null +++ b/src/test/telemetry/envFileTelemetry.unit.test.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { anyString, instance, mock, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../client/common/platform/types'; +import * as Telemetry from '../../client/telemetry'; +import { EventName } from '../../client/telemetry/constants'; +import { + EnvFileTelemetryTests, + sendActivationTelemetry, + sendFileCreationTelemetry, + sendSettingTelemetry +} from '../../client/telemetry/envFileTelemetry'; + +suite('Env file telemetry', () => { + const defaultEnvFileValue = 'someDefaultValue'; + const resource = Uri.parse('foo'); + + let telemetryEvent: { eventName: EventName; hasCustomEnvPath: boolean } | undefined; + let sendTelemetryStub: sinon.SinonStub; + let workspaceService: IWorkspaceService; + let fileSystem: IFileSystem; + + setup(() => { + fileSystem = mock(FileSystem); + workspaceService = mock(WorkspaceService); + + const mockWorkspaceConfig = { + inspect: () => ({ + defaultValue: defaultEnvFileValue + }) + }; + + // tslint:disable-next-line: no-any + when(workspaceService.getConfiguration('python')).thenReturn(mockWorkspaceConfig as any); + + const mockSendTelemetryEvent = ( + eventName: EventName, + _: number | undefined, + { hasCustomEnvPath }: { hasCustomEnvPath: boolean } + ) => { + telemetryEvent = { + eventName, + hasCustomEnvPath + }; + }; + + sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + }); + + teardown(() => { + telemetryEvent = undefined; + sinon.restore(); + EnvFileTelemetryTests.resetState(); + }); + + test('Setting telemetry should be sent with hasCustomEnvPath at true if the python.envFile setting is different from the default value', () => { + sendSettingTelemetry(instance(workspaceService), 'bar'); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepEqual(telemetryEvent, { eventName: EventName.ENVFILE_WORKSPACE, hasCustomEnvPath: true }); + }); + + test('Setting telemetry should not be sent if a telemetry event has already been sent', () => { + EnvFileTelemetryTests.setState({ telemetrySent: true }); + + sendSettingTelemetry(instance(workspaceService), 'bar'); + + sinon.assert.notCalled(sendTelemetryStub); + assert.deepEqual(telemetryEvent, undefined); + }); + + test('Setting telemetry should not be sent if the python.envFile setting is the same as the default value', () => { + EnvFileTelemetryTests.setState({ defaultSetting: defaultEnvFileValue }); + + sendSettingTelemetry(instance(workspaceService), defaultEnvFileValue); + + sinon.assert.notCalled(sendTelemetryStub); + assert.deepEqual(telemetryEvent, undefined); + }); + + test('File creation telemetry should be sent if no telemetry event has been sent before', () => { + sendFileCreationTelemetry(); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepEqual(telemetryEvent, { eventName: EventName.ENVFILE_WORKSPACE, hasCustomEnvPath: false }); + }); + + test('File creation telemetry should not be sent if a telemetry event has already been sent', () => { + EnvFileTelemetryTests.setState({ telemetrySent: true }); + + sendFileCreationTelemetry(); + + sinon.assert.notCalled(sendTelemetryStub); + assert.deepEqual(telemetryEvent, undefined); + }); + + test('Activation telemetry should be sent if no telemetry event has been sent before, and a .env file exists', async () => { + when(fileSystem.fileExists(anyString())).thenResolve(true); + + await sendActivationTelemetry(instance(fileSystem), instance(workspaceService), resource); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepEqual(telemetryEvent, { eventName: EventName.ENVFILE_WORKSPACE, hasCustomEnvPath: false }); + }); + + test('Activation telemetry should not be sent if a telemetry event has already been sent', async () => { + EnvFileTelemetryTests.setState({ telemetrySent: true }); + + await sendActivationTelemetry(instance(fileSystem), instance(workspaceService), resource); + + sinon.assert.notCalled(sendTelemetryStub); + assert.deepEqual(telemetryEvent, undefined); + }); + + test('Activation telemetry should not be sent if no .env file exists', async () => { + when(fileSystem.fileExists(anyString())).thenResolve(false); + + await sendActivationTelemetry(instance(fileSystem), instance(workspaceService), resource); + + sinon.assert.notCalled(sendTelemetryStub); + assert.deepEqual(telemetryEvent, undefined); + }); +}); diff --git a/src/test/telemetry/extensionInstallTelemetry.unit.test.ts b/src/test/telemetry/extensionInstallTelemetry.unit.test.ts new file mode 100644 index 000000000000..47e25eca05fa --- /dev/null +++ b/src/test/telemetry/extensionInstallTelemetry.unit.test.ts @@ -0,0 +1,29 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { anyString, instance, mock, when } from 'ts-mockito'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../client/common/platform/types'; +import * as Telemetry from '../../client/telemetry'; +import { setExtensionInstallTelemetryProperties } from '../../client/telemetry/extensionInstallTelemetry'; + +suite('Extension Install Telemetry', () => { + let fs: IFileSystem; + let telemetryPropertyStub: sinon.SinonStub; + setup(() => { + fs = mock(FileSystem); + telemetryPropertyStub = sinon.stub(Telemetry, 'setSharedProperty'); + }); + teardown(() => { + telemetryPropertyStub.restore(); + }); + test('PythonCodingPack exists', async () => { + when(fs.fileExists(anyString())).thenResolve(true); + await setExtensionInstallTelemetryProperties(instance(fs)); + assert.ok(telemetryPropertyStub.calledOnceWithExactly('installSource', 'pythonCodingPack')); + }); + test('PythonCodingPack does not exists', async () => { + when(fs.fileExists(anyString())).thenResolve(false); + await setExtensionInstallTelemetryProperties(instance(fs)); + assert.ok(telemetryPropertyStub.calledOnceWithExactly('installSource', 'marketPlace')); + }); +}); diff --git a/src/test/telemetry/importTracker.unit.test.ts b/src/test/telemetry/importTracker.unit.test.ts new file mode 100644 index 000000000000..8c702ea11151 --- /dev/null +++ b/src/test/telemetry/importTracker.unit.test.ts @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +//tslint:disable:max-func-body-length match-default-export-name no-any no-multiline-string no-trailing-whitespace +import { expect } from 'chai'; +import rewiremock from 'rewiremock'; +import * as TypeMoq from 'typemoq'; +import { EventEmitter, TextDocument } from 'vscode'; + +import { IDocumentManager } from '../../client/common/application/types'; +import { generateCells } from '../../client/datascience/cellFactory'; +import { INotebookEditor, INotebookEditorProvider, INotebookModel } from '../../client/datascience/types'; +import { EventName } from '../../client/telemetry/constants'; +import { ImportTracker } from '../../client/telemetry/importTracker'; +import { createDocument } from '../datascience/editor-integration/helpers'; + +suite('Import Tracker', () => { + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + // tslint:disable-next-line:no-require-imports + const hashJs = require('hash.js'); + let importTracker: ImportTracker; + let documentManager: TypeMoq.IMock<IDocumentManager>; + let nativeProvider: TypeMoq.IMock<INotebookEditorProvider>; + let openedEventEmitter: EventEmitter<TextDocument>; + let savedEventEmitter: EventEmitter<TextDocument>; + let openedNotebookEmitter: EventEmitter<INotebookEditor>; + let closedNotebookEmitter: EventEmitter<INotebookEditor>; + const pandasHash: string = hashJs.sha256().update('pandas').digest('hex'); + const elephasHash: string = hashJs.sha256().update('elephas').digest('hex'); + const kerasHash: string = hashJs.sha256().update('keras').digest('hex'); + const pysparkHash: string = hashJs.sha256().update('pyspark').digest('hex'); + const sparkdlHash: string = hashJs.sha256().update('sparkdl').digest('hex'); + const numpyHash: string = hashJs.sha256().update('numpy').digest('hex'); + const scipyHash: string = hashJs.sha256().update('scipy').digest('hex'); + const sklearnHash: string = hashJs.sha256().update('sklearn').digest('hex'); + const randomHash: string = hashJs.sha256().update('random').digest('hex'); + + class Reporter { + public static eventNames: string[] = []; + public static properties: Record<string, string>[] = []; + public static measures: {}[] = []; + + public static expectHashes(...hashes: string[]) { + expect(Reporter.eventNames).to.contain(EventName.HASHED_PACKAGE_PERF); + if (hashes.length > 0) { + expect(Reporter.eventNames).to.contain(EventName.HASHED_PACKAGE_NAME); + } + + Reporter.properties.pop(); // HASHED_PACKAGE_PERF + expect(Reporter.properties).to.deep.equal(hashes.map((hash) => ({ hashedName: hash }))); + } + + public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { + Reporter.eventNames.push(eventName); + Reporter.properties.push(properties!); + Reporter.measures.push(measures!); + } + } + + setup(() => { + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + + openedEventEmitter = new EventEmitter<TextDocument>(); + savedEventEmitter = new EventEmitter<TextDocument>(); + openedNotebookEmitter = new EventEmitter<INotebookEditor>(); + closedNotebookEmitter = new EventEmitter<INotebookEditor>(); + + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + documentManager.setup((a) => a.onDidOpenTextDocument).returns(() => openedEventEmitter.event); + documentManager.setup((a) => a.onDidSaveTextDocument).returns(() => savedEventEmitter.event); + + nativeProvider = TypeMoq.Mock.ofType<INotebookEditorProvider>(); + nativeProvider.setup((n) => n.onDidOpenNotebookEditor).returns(() => openedNotebookEmitter.event); + nativeProvider.setup((n) => n.onDidCloseNotebookEditor).returns(() => closedNotebookEmitter.event); + nativeProvider.setup((n) => n.editors).returns(() => []); + + rewiremock.enable(); + rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + + importTracker = new ImportTracker(documentManager.object, nativeProvider.object); + }); + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + Reporter.properties = []; + Reporter.eventNames = []; + Reporter.measures = []; + rewiremock.disable(); + }); + + function emitDocEvent(code: string, ev: EventEmitter<TextDocument>) { + const textDoc = createDocument(code, 'foo.py', 1, TypeMoq.Times.atMost(100), true); + ev.fire(textDoc.object); + } + + function emitNotebookEvent(code: string, ev: EventEmitter<INotebookEditor>) { + const notebook = TypeMoq.Mock.ofType<INotebookEditor>(); + const model = TypeMoq.Mock.ofType<INotebookModel>(); + notebook.setup((n) => n.model).returns(() => model.object); + model.setup((m) => m.cells).returns(() => generateCells(undefined, code, 'foo.py', 0, false, '1')); + ev.fire(notebook.object); + } + + test('Open document', () => { + emitDocEvent('import pandas\r\n', openedEventEmitter); + + Reporter.expectHashes(pandasHash); + }); + + test('Already opened documents', async () => { + const doc = createDocument('import pandas\r\n', 'foo.py', 1, TypeMoq.Times.atMost(100), true); + documentManager.setup((d) => d.textDocuments).returns(() => [doc.object]); + await importTracker.activate(); + + Reporter.expectHashes(pandasHash); + }); + + test('Open notebook', () => { + emitNotebookEvent('import pandas\r\n', openedNotebookEmitter); + Reporter.expectHashes(pandasHash); + }); + + test('Close notebook', () => { + emitNotebookEvent('import pandas\r\n', closedNotebookEmitter); + Reporter.expectHashes(pandasHash); + }); + + test('Execute notebook', async () => { + await importTracker.postExecute( + generateCells(undefined, 'import pandas\r\n', 'foo.py', 0, false, '1')[0], + false + ); + Reporter.expectHashes(pandasHash); + }); + + test('Save document', () => { + emitDocEvent('import pandas\r\n', savedEventEmitter); + + Reporter.expectHashes(pandasHash); + }); + + test('from <pkg>._ import _, _', () => { + const elephas = ` + from elephas.java import java_classes, adapter + from keras.models import Sequential + from keras.layers import Dense + + + model = Sequential() + model.add(Dense(units=64, activation='relu', input_dim=100)) + model.add(Dense(units=10, activation='softmax')) + model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy']) + + model.save('test.h5') + + + kmi = java_classes.KerasModelImport + file = java_classes.File("test.h5") + + java_model = kmi.importKerasSequentialModelAndWeights(file.absolutePath) + + weights = adapter.retrieve_keras_weights(java_model) + model.set_weights(weights)`; + + emitDocEvent(elephas, savedEventEmitter); + Reporter.expectHashes(elephasHash, kerasHash); + }); + + test('from <pkg>._ import _', () => { + const pyspark = `from pyspark.ml.classification import LogisticRegression + from pyspark.ml.evaluation import MulticlassClassificationEvaluator + from pyspark.ml import Pipeline + from sparkdl import DeepImageFeaturizer + + featurizer = DeepImageFeaturizer(inputCol="image", outputCol="features", modelName="InceptionV3") + lr = LogisticRegression(maxIter=20, regParam=0.05, elasticNetParam=0.3, labelCol="label") + p = Pipeline(stages=[featurizer, lr]) + + model = p.fit(train_images_df) # train_images_df is a dataset of images and labels + + # Inspect training error + df = model.transform(train_images_df.limit(10)).select("image", "probability", "uri", "label") + predictionAndLabels = df.select("prediction", "label") + evaluator = MulticlassClassificationEvaluator(metricName="accuracy") + print("Training set accuracy = " + str(evaluator.evaluate(predictionAndLabels)))`; + + emitDocEvent(pyspark, savedEventEmitter); + Reporter.expectHashes(pysparkHash, sparkdlHash); + }); + + test('import <pkg> as _', () => { + const code = `import pandas as pd +import numpy as np +import random as rnd + +def simplify_ages(df): + df.Age = df.Age.fillna(-0.5) + bins = (-1, 0, 5, 12, 18, 25, 35, 60, 120) + group_names = ['Unknown', 'Baby', 'Child', 'Teenager', 'Student', 'Young Adult', 'Adult', 'Senior'] + categories = pd.cut(df.Age, bins, labels=group_names) + df.Age = categories + return df`; + emitDocEvent(code, savedEventEmitter); + Reporter.expectHashes(pandasHash, numpyHash, randomHash); + }); + + test('from <pkg> import _', () => { + const code = `from scipy import special +def drumhead_height(n, k, distance, angle, t): + kth_zero = special.jn_zeros(n, k)[-1] + return np.cos(t) * np.cos(n*angle) * special.jn(n, distance*kth_zero) +theta = np.r_[0:2*np.pi:50j] +radius = np.r_[0:1:50j] +x = np.array([r * np.cos(theta) for r in radius]) +y = np.array([r * np.sin(theta) for r in radius]) +z = np.array([drumhead_height(1, 1, r, theta, 0.5) for r in radius])`; + emitDocEvent(code, savedEventEmitter); + Reporter.expectHashes(scipyHash); + }); + + test('from <pkg> import _ as _', () => { + const code = `from pandas import DataFrame as df`; + emitDocEvent(code, savedEventEmitter); + Reporter.expectHashes(pandasHash); + }); + + test('import <pkg1>, <pkg2>', () => { + const code = ` +def drumhead_height(n, k, distance, angle, t): + import sklearn, pandas + return np.cos(t) * np.cos(n*angle) * special.jn(n, distance*kth_zero) +theta = np.r_[0:2*np.pi:50j] +radius = np.r_[0:1:50j] +x = np.array([r * np.cos(theta) for r in radius]) +y = np.array([r * np.sin(theta) for r in radius]) +z = np.array([drumhead_height(1, 1, r, theta, 0.5) for r in radius])`; + emitDocEvent(code, savedEventEmitter); + Reporter.expectHashes(sklearnHash, pandasHash); + }); + + test('Import from within a function', () => { + const code = ` +def drumhead_height(n, k, distance, angle, t): + import sklearn as sk + return np.cos(t) * np.cos(n*angle) * special.jn(n, distance*kth_zero) +theta = np.r_[0:2*np.pi:50j] +radius = np.r_[0:1:50j] +x = np.array([r * np.cos(theta) for r in radius]) +y = np.array([r * np.sin(theta) for r in radius]) +z = np.array([drumhead_height(1, 1, r, theta, 0.5) for r in radius])`; + emitDocEvent(code, savedEventEmitter); + Reporter.expectHashes(sklearnHash); + }); + + test('Do not send the same package twice', () => { + const code = ` +import pandas +import pandas`; + emitDocEvent(code, savedEventEmitter); + Reporter.expectHashes(pandasHash); + }); + + test('Ignore relative imports', () => { + const code = 'from .pandas import not_real'; + emitDocEvent(code, savedEventEmitter); + Reporter.expectHashes(); + }); + + test('Ignore docstring for `from` imports', () => { + const code = `""" +from numpy import the random function +"""`; + emitDocEvent(code, savedEventEmitter); + Reporter.expectHashes(); + }); + + test('Ignore docstring for `import` imports', () => { + const code = `""" +import numpy for all the things +"""`; + emitDocEvent(code, savedEventEmitter); + Reporter.expectHashes(); + }); +}); diff --git a/src/test/telemetry/index.unit.test.ts b/src/test/telemetry/index.unit.test.ts new file mode 100644 index 000000000000..13c8487fb5b7 --- /dev/null +++ b/src/test/telemetry/index.unit.test.ts @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +//tslint:disable:max-func-body-length match-default-export-name no-any +import { expect } from 'chai'; +import rewiremock from 'rewiremock'; +import * as TypeMoq from 'typemoq'; + +import { instance, mock, verify, when } from 'ts-mockito'; +import { WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { EXTENSION_ROOT_DIR } from '../../client/constants'; +import { + _resetSharedProperties, + clearTelemetryReporter, + isTelemetryDisabled, + sendTelemetryEvent, + setSharedProperty +} from '../../client/telemetry'; + +suite('Telemetry', () => { + let workspaceService: IWorkspaceService; + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + + class Reporter { + public static eventName: string[] = []; + public static properties: Record<string, string>[] = []; + public static measures: {}[] = []; + public static errorProps: string[] | undefined; + public static clear() { + Reporter.eventName = []; + Reporter.properties = []; + Reporter.measures = []; + Reporter.errorProps = undefined; + } + public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { + Reporter.eventName.push(eventName); + Reporter.properties.push(properties!); + Reporter.measures.push(measures!); + } + public sendTelemetryErrorEvent(eventName: string, properties?: {}, measures?: {}, errorProps?: string[]) { + this.sendTelemetryEvent(eventName, properties, measures); + Reporter.errorProps = errorProps; + } + } + + setup(() => { + workspaceService = mock(WorkspaceService); + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + clearTelemetryReporter(); + Reporter.clear(); + }); + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + rewiremock.disable(); + _resetSharedProperties(); + }); + + const testsForisTelemetryDisabled = [ + { + testName: 'Returns true when globalValue is set to false', + settings: { globalValue: false }, + expectedResult: true + }, + { + testName: 'Returns false otherwise', + settings: {}, + expectedResult: false + } + ]; + + suite('Function isTelemetryDisabled()', () => { + testsForisTelemetryDisabled.forEach((testParams) => { + test(testParams.testName, async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + when(workspaceService.getConfiguration('telemetry')).thenReturn(workspaceConfig.object); + workspaceConfig + .setup((c) => c.inspect<string>('enableTelemetry')) + .returns(() => testParams.settings as any) + .verifiable(TypeMoq.Times.once()); + + expect(isTelemetryDisabled(instance(workspaceService))).to.equal(testParams.expectedResult); + + verify(workspaceService.getConfiguration('telemetry')).once(); + workspaceConfig.verifyAll(); + }); + }); + }); + + test('Send Telemetry', () => { + rewiremock.enable(); + rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + + const eventName = 'Testing'; + const properties = { hello: 'world', foo: 'bar' }; + const measures = { start: 123, end: 987 }; + + // tslint:disable-next-line:no-any + sendTelemetryEvent(eventName as any, measures, properties as any); + + expect(Reporter.eventName).to.deep.equal([eventName]); + expect(Reporter.measures).to.deep.equal([measures]); + expect(Reporter.properties).to.deep.equal([properties]); + }); + test('Send Telemetry with no properties', () => { + rewiremock.enable(); + rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + + const eventName = 'Testing'; + + sendTelemetryEvent(eventName as any); + + expect(Reporter.eventName).to.deep.equal([eventName]); + expect(Reporter.measures).to.deep.equal([undefined], 'Measures should be empty'); + expect(Reporter.properties).to.deep.equal([{}], 'Properties should be empty'); + }); + test('Send Telemetry with shared properties', () => { + rewiremock.enable(); + rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + + const eventName = 'Testing'; + const properties = { hello: 'world', foo: 'bar' }; + const measures = { start: 123, end: 987 }; + const expectedProperties = { ...properties, one: 'two' }; + + setSharedProperty('one' as any, 'two' as any); + + // tslint:disable-next-line:no-any + sendTelemetryEvent(eventName as any, measures, properties as any); + + expect(Reporter.eventName).to.deep.equal([eventName]); + expect(Reporter.measures).to.deep.equal([measures]); + expect(Reporter.properties).to.deep.equal([expectedProperties]); + }); + test('Shared properties will replace existing ones', () => { + rewiremock.enable(); + rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + + const eventName = 'Testing'; + const properties = { hello: 'world', foo: 'bar' }; + const measures = { start: 123, end: 987 }; + const expectedProperties = { ...properties, foo: 'baz' }; + + setSharedProperty('foo' as any, 'baz' as any); + + // tslint:disable-next-line:no-any + sendTelemetryEvent(eventName as any, measures, properties as any); + + expect(Reporter.eventName).to.deep.equal([eventName]); + expect(Reporter.measures).to.deep.equal([measures]); + expect(Reporter.properties).to.deep.equal([expectedProperties]); + }); + test('Send Error Telemetry', () => { + rewiremock.enable(); + const error = new Error('Boo'); + rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + + const eventName = 'Testing'; + const properties = { hello: 'world', foo: 'bar' }; + const measures = { start: 123, end: 987 }; + + // tslint:disable-next-line:no-any + sendTelemetryEvent(eventName as any, measures, properties as any, error); + + const expectedErrorProperties = { + originalEventName: eventName + }; + + expect(Reporter.eventName).to.deep.equal(['ERROR']); + expect(Reporter.measures).to.deep.equal([measures]); + expect(Reporter.properties[0].stackTrace).to.be.length.greaterThan(1); + delete Reporter.properties[0].stackTrace; + expect(Reporter.properties).to.deep.equal([expectedErrorProperties]); + expect(Reporter.errorProps).to.deep.equal([]); + }); + test('Send Error Telemetry with stack trace', () => { + rewiremock.enable(); + const error = new Error('Boo'); + const root = EXTENSION_ROOT_DIR.replace(/\\/g, '/'); + error.stack = [ + 'Error: Boo', + `at Context.test (${root}/src/test/telemetry/index.unit.test.ts:50:23)`, + `at callFn (${root}/node_modules/mocha/lib/runnable.js:372:21)`, + `at Test.Runnable.run (${root}/node_modules/mocha/lib/runnable.js:364:7)`, + `at Runner.runTest (${root}/node_modules/mocha/lib/runner.js:455:10)`, + `at ${root}/node_modules/mocha/lib/runner.js:573:12`, + `at next (${root}/node_modules/mocha/lib/runner.js:369:14)`, + `at ${root}/node_modules/mocha/lib/runner.js:379:7`, + `at next (${root}/node_modules/mocha/lib/runner.js:303:14)`, + `at ${root}/node_modules/mocha/lib/runner.js:342:7`, + `at done (${root}/node_modules/mocha/lib/runnable.js:319:5)`, + `at callFn (${root}/node_modules/mocha/lib/runnable.js:395:7)`, + `at Hook.Runnable.run (${root}/node_modules/mocha/lib/runnable.js:364:7)`, + `at next (${root}/node_modules/mocha/lib/runner.js:317:10)`, + `at Immediate.<anonymous> (${root}/node_modules/mocha/lib/runner.js:347:5)`, + 'at runCallback (timers.js:789:20)', + 'at tryOnImmediate (timers.js:751:5)', + 'at processImmediate [as _immediateCallback] (timers.js:722:5)' + ].join('\n\t'); + rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + + const eventName = 'Testing'; + const properties = { hello: 'world', foo: 'bar' }; + const measures = { start: 123, end: 987 }; + + // tslint:disable-next-line:no-any + sendTelemetryEvent(eventName as any, measures, properties as any, error); + + const expectedErrorProperties = { + originalEventName: eventName + }; + + const stackTrace = Reporter.properties[0].stackTrace; + delete Reporter.properties[0].stackTrace; + + expect(Reporter.eventName).to.deep.equal(['ERROR']); + expect(Reporter.measures).to.deep.equal([measures]); + expect(Reporter.properties).to.deep.equal([expectedErrorProperties]); + expect(stackTrace).to.be.length.greaterThan(1); + expect(Reporter.errorProps).to.deep.equal([]); + + const expectedStack = [ + `at Context.test ${root}/src/test/telemetry/index.unit.test.ts:50:23`, + `at callFn ${root}/node_modules/mocha/lib/runnable.js:372:21`, + `at Test.Runnable.run ${root}/node_modules/mocha/lib/runnable.js:364:7`, + `at Runner.runTest ${root}/node_modules/mocha/lib/runner.js:455:10`, + `at ${root}/node_modules/mocha/lib/runner.js:573:12`, + `at next ${root}/node_modules/mocha/lib/runner.js:369:14`, + `at ${root}/node_modules/mocha/lib/runner.js:379:7`, + `at next ${root}/node_modules/mocha/lib/runner.js:303:14`, + `at ${root}/node_modules/mocha/lib/runner.js:342:7`, + `at done ${root}/node_modules/mocha/lib/runnable.js:319:5`, + `at callFn ${root}/node_modules/mocha/lib/runnable.js:395:7`, + `at Hook.Runnable.run ${root}/node_modules/mocha/lib/runnable.js:364:7`, + `at next ${root}/node_modules/mocha/lib/runner.js:317:10`, + `at Immediate ${root}/node_modules/mocha/lib/runner.js:347:5`, + 'at runCallback timers.js:789:20', + 'at tryOnImmediate timers.js:751:5', + 'at processImmediate [as _immediateCallback] timers.js:722:5' + ].join('\n\t'); + + expect(stackTrace).to.be.equal(expectedStack); + }); +}); diff --git a/src/test/terminals/activation.unit.test.ts b/src/test/terminals/activation.unit.test.ts new file mode 100644 index 000000000000..3b82fed815bd --- /dev/null +++ b/src/test/terminals/activation.unit.test.ts @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { EventEmitter, Extension, Terminal } from 'vscode'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { TerminalManager } from '../../client/common/application/terminalManager'; +import { IActiveResourceService, ICommandManager, ITerminalManager } from '../../client/common/application/types'; +import { CODE_RUNNER_EXTENSION_ID } from '../../client/common/constants'; +import { TerminalActivator } from '../../client/common/terminal/activator'; +import { ITerminalActivator } from '../../client/common/terminal/types'; +import { IExtensions } from '../../client/common/types'; +import { ExtensionActivationForTerminalActivation, TerminalAutoActivation } from '../../client/terminals/activation'; +import { ITerminalAutoActivation } from '../../client/terminals/types'; +import { noop } from '../core'; + +// tslint:disable-next-line: max-func-body-length +suite('Terminal', () => { + suite('Terminal - Terminal Activation', () => { + let autoActivation: ITerminalAutoActivation; + let manager: ITerminalManager; + let activator: ITerminalActivator; + let resourceService: IActiveResourceService; + let onDidOpenTerminalEventEmitter: EventEmitter<Terminal>; + let terminal: Terminal; + let nonActivatedTerminal: Terminal; + + setup(() => { + manager = mock(TerminalManager); + activator = mock(TerminalActivator); + resourceService = mock(ActiveResourceService); + onDidOpenTerminalEventEmitter = new EventEmitter<Terminal>(); + when(manager.onDidOpenTerminal).thenReturn(onDidOpenTerminalEventEmitter.event); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); + + autoActivation = new TerminalAutoActivation( + instance(manager), + [], + instance(activator), + instance(resourceService) + ); + + terminal = { + dispose: noop, + hide: noop, + name: 'Some Name', + creationOptions: {}, + processId: Promise.resolve(0), + sendText: noop, + show: noop, + exitStatus: { code: 0 } + }; + nonActivatedTerminal = { + dispose: noop, + hide: noop, + creationOptions: { hideFromUser: true }, + name: 'Something', + processId: Promise.resolve(0), + sendText: noop, + show: noop, + exitStatus: { code: 0 } + }; + autoActivation.register(); + }); + // teardown(() => fakeTimer.uninstall()); + + test('Should activate terminal', async () => { + // Trigger opening a terminal. + // tslint:disable-next-line: no-any + await ((onDidOpenTerminalEventEmitter.fire(terminal) as any) as Promise<void>); + + // The terminal should get activated. + verify(activator.activateEnvironmentInTerminal(terminal, anything())).once(); + }); + test('Should not activate terminal if name starts with specific prefix', async () => { + // Trigger opening a terminal. + // tslint:disable-next-line: no-any + await ((onDidOpenTerminalEventEmitter.fire(nonActivatedTerminal) as any) as Promise<void>); + + // The terminal should get activated. + verify(activator.activateEnvironmentInTerminal(anything(), anything())).never(); + }); + }); + suite('Terminal - Extension Activation', () => { + let commands: TypeMoq.IMock<ICommandManager>; + let extensions: TypeMoq.IMock<IExtensions>; + let extensionsChangeEvent: EventEmitter<void>; + let activation: ExtensionActivationForTerminalActivation; + setup(() => { + commands = TypeMoq.Mock.ofType<ICommandManager>(undefined, TypeMoq.MockBehavior.Strict); + extensions = TypeMoq.Mock.ofType<IExtensions>(undefined, TypeMoq.MockBehavior.Strict); + extensionsChangeEvent = new EventEmitter<void>(); + extensions.setup((e) => e.onDidChange).returns(() => extensionsChangeEvent.event); + }); + + teardown(() => { + extensionsChangeEvent.dispose(); + }); + + function verifyAll() { + commands.verifyAll(); + extensions.verifyAll(); + } + + test("If code runner extension is installed, don't show the play icon", async () => { + // tslint:disable-next-line:no-any + const extension = TypeMoq.Mock.ofType<Extension<any>>(undefined, TypeMoq.MockBehavior.Strict); + extensions + .setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)) + .returns(() => extension.object) + .verifiable(TypeMoq.Times.once()); + activation = new ExtensionActivationForTerminalActivation(commands.object, extensions.object, []); + + commands + .setup((c) => c.executeCommand('setContext', 'python.showPlayIcon', true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + commands + .setup((c) => c.executeCommand('setContext', 'python.showPlayIcon', false)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await activation.activate(); + + verifyAll(); + }); + + test('If code runner extension is not installed, show the play icon', async () => { + extensions + .setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + activation = new ExtensionActivationForTerminalActivation(commands.object, extensions.object, []); + + commands + .setup((c) => c.executeCommand('setContext', 'python.showPlayIcon', true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + commands + .setup((c) => c.executeCommand('setContext', 'python.showPlayIcon', false)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await activation.activate(); + verifyAll(); + }); + }); +}); diff --git a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts new file mode 100644 index 000000000000..1765fa1e8d1b --- /dev/null +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Disposable, TextDocument, TextEditor, Uri } from 'vscode'; + +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IPythonExtensionBanner } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CodeExecutionManager } from '../../../client/terminals/codeExecution/codeExecutionManager'; +import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from '../../../client/terminals/types'; + +// tslint:disable:no-multiline-string no-trailing-whitespace max-func-body-length no-any +suite('Terminal - Code Execution Manager', () => { + let executionManager: ICodeExecutionManager; + let workspace: TypeMoq.IMock<IWorkspaceService>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let disposables: Disposable[] = []; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let documentManager: TypeMoq.IMock<IDocumentManager>; + let shiftEnterBanner: TypeMoq.IMock<IPythonExtensionBanner>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + setup(() => { + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + fileSystem.setup((f) => f.readFile(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + shiftEnterBanner = TypeMoq.Mock.ofType<IPythonExtensionBanner>(); + shiftEnterBanner + .setup((b) => b.showBanner()) + .returns(() => { + return Promise.resolve(); + }); + workspace + .setup((c) => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return { + dispose: () => void 0 + }; + }); + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + executionManager = new CodeExecutionManager( + commandManager.object, + documentManager.object, + disposables, + fileSystem.object, + shiftEnterBanner.object, + serviceContainer.object + ); + }); + teardown(() => { + disposables.forEach((disposable) => { + if (disposable) { + disposable.dispose(); + } + }); + + disposables = []; + }); + + test('Ensure commands are registered', async () => { + const registered: string[] = []; + commandManager + .setup((c) => c.registerCommand) + .returns(() => { + return (command: string, _callback: (...args: any[]) => any, _thisArg?: any) => { + registered.push(command); + return { dispose: () => void 0 }; + }; + }); + + executionManager.registerCommands(); + + const sorted = registered.sort(); + expect(sorted).to.deep.equal([ + Commands.Exec_In_Terminal, + Commands.Exec_In_Terminal_Icon, + Commands.Exec_Selection_In_Django_Shell, + Commands.Exec_Selection_In_Terminal + ]); + }); + + test('Ensure executeFileInterTerminal will do nothing if no file is avialble', async () => { + let commandHandler: undefined | (() => Promise<void>); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + + await commandHandler!(); + helper.verify(async (h) => h.getFileToExecute(), TypeMoq.Times.once()); + }); + + test('Ensure executeFileInterTerminal will use provided file', async () => { + let commandHandler: undefined | ((file: Uri) => Promise<void>); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + + const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))) + .returns(() => executionService.object); + + const fileToExecute = Uri.file('x'); + await commandHandler!(fileToExecute); + helper.verify(async (h) => h.getFileToExecute(), TypeMoq.Times.never()); + executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + }); + + test('Ensure executeFileInterTerminal will use active file', async () => { + let commandHandler: undefined | ((file: Uri) => Promise<void>); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const fileToExecute = Uri.file('x'); + const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup(async (h) => h.getFileToExecute()).returns(() => Promise.resolve(fileToExecute)); + const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))) + .returns(() => executionService.object); + + await commandHandler!(fileToExecute); + executionService.verify(async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + }); + + async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { + let commandHandler: undefined | (() => Promise<void>); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === commandId) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionSericeId))) + .returns(() => executionService.object); + documentManager.setup((d) => d.activeTextEditor).returns(() => undefined); + + await commandHandler!(); + executionService.verify(async (e) => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); + } + + test('Ensure executeSelectionInTerminal will do nothing if theres no active document', async () => { + await testExecutionOfSelectionWithoutAnyActiveDocument(Commands.Exec_Selection_In_Terminal, 'standard'); + }); + + test('Ensure executeSelectionInDjangoShell will do nothing if theres no active document', async () => { + await testExecutionOfSelectionWithoutAnyActiveDocument(Commands.Exec_Selection_In_Django_Shell, 'djangoShell'); + }); + + async function testExecutionOfSlectionWithoutAnythingSelected(commandId: string, executionServiceId: string) { + let commandHandler: undefined | (() => Promise<void>); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === commandId) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup((h) => h.getSelectedTextToExecute).returns(() => () => Promise.resolve('')); + const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))) + .returns(() => executionService.object); + documentManager + .setup((d) => d.activeTextEditor) + .returns(() => { + return {} as any; + }); + + await commandHandler!(); + executionService.verify(async (e) => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); + } + + test('Ensure executeSelectionInTerminal will do nothing if no text is selected', async () => { + await testExecutionOfSlectionWithoutAnythingSelected(Commands.Exec_Selection_In_Terminal, 'standard'); + }); + + test('Ensure executeSelectionInDjangoShell will do nothing if no text is selected', async () => { + await testExecutionOfSlectionWithoutAnythingSelected(Commands.Exec_Selection_In_Django_Shell, 'djangoShell'); + }); + + async function testExecutionOfSelectionIsSentToTerminal(commandId: string, executionServiceId: string) { + let commandHandler: undefined | (() => Promise<void>); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === commandId) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const textSelected = 'abcd'; + const activeDocumentUri = Uri.file('abc'); + const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup((h) => h.getSelectedTextToExecute).returns(() => () => Promise.resolve(textSelected)); + helper + .setup((h) => h.normalizeLines) + .returns(() => () => Promise.resolve(textSelected)) + .verifiable(TypeMoq.Times.once()); + const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))) + .returns(() => executionService.object); + const document = TypeMoq.Mock.ofType<TextDocument>(); + document.setup((d) => d.uri).returns(() => activeDocumentUri); + const activeEditor = TypeMoq.Mock.ofType<TextEditor>(); + activeEditor.setup((e) => e.document).returns(() => document.object); + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + + await commandHandler!(); + executionService.verify( + async (e) => e.execute(TypeMoq.It.isValue(textSelected), TypeMoq.It.isValue(activeDocumentUri)), + TypeMoq.Times.once() + ); + helper.verifyAll(); + } + test('Ensure executeSelectionInTerminal will normalize selected text and send it to the terminal', async () => { + await testExecutionOfSelectionIsSentToTerminal(Commands.Exec_Selection_In_Terminal, 'standard'); + }); + + test('Ensure executeSelectionInDjangoShell will normalize selected text and send it to the terminal', async () => { + await testExecutionOfSelectionIsSentToTerminal(Commands.Exec_Selection_In_Django_Shell, 'djangoShell'); + }); +}); diff --git a/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts new file mode 100644 index 000000000000..47f4a4a1f6e4 --- /dev/null +++ b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-multiline-string no-trailing-whitespace + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { createCondaEnv } from '../../../client/common/process/pythonEnvironment'; +import { createPythonProcessService } from '../../../client/common/process/pythonProcess'; +import { IProcessService, IPythonExecutionFactory } from '../../../client/common/process/types'; +import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; +import { DjangoShellCodeExecutionProvider } from '../../../client/terminals/codeExecution/djangoShellCodeExecution'; +import { ICodeExecutionService } from '../../../client/terminals/types'; +import { PYTHON_PATH } from '../../common'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal - Django Shell Code Execution', () => { + let executor: ICodeExecutionService; + let terminalSettings: TypeMoq.IMock<ITerminalSettings>; + let terminalService: TypeMoq.IMock<ITerminalService>; + let workspace: TypeMoq.IMock<IWorkspaceService>; + let platform: TypeMoq.IMock<IPlatformService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let settings: TypeMoq.IMock<IPythonSettings>; + let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>; + let disposables: Disposable[] = []; + setup(() => { + const terminalFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>(); + terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>(); + terminalService = TypeMoq.Mock.ofType<ITerminalService>(); + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspace + .setup((c) => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return { + dispose: () => void 0 + }; + }); + platform = TypeMoq.Mock.ofType<IPlatformService>(); + const documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + const commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + executor = new DjangoShellCodeExecutionProvider( + terminalFactory.object, + configService.object, + workspace.object, + documentManager.object, + platform.object, + commandManager.object, + fileSystem.object, + disposables + ); + + terminalFactory.setup((f) => f.getTerminalService(TypeMoq.It.isAny())).returns(() => terminalService.object); + + settings = TypeMoq.Mock.ofType<IPythonSettings>(); + settings.setup((s) => s.terminal).returns(() => terminalSettings.object); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + }); + teardown(() => { + disposables.forEach((disposable) => { + if (disposable) { + disposable.dispose(); + } + }); + + disposables = []; + }); + + async function testReplCommandArguments( + isWindows: boolean, + pythonPath: string, + expectedPythonPath: string, + terminalArgs: string[], + expectedTerminalArgs: string[], + resource?: Uri + ) { + platform.setup((p) => p.isWindows).returns(() => isWindows); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + + const replCommandArgs = await (executor as DjangoShellCodeExecutionProvider).getExecutableInfo(resource); + expect(replCommandArgs).not.to.be.an('undefined', 'Command args is undefined'); + expect(replCommandArgs.command).to.be.equal(expectedPythonPath, 'Incorrect python path'); + expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect arguments'); + } + + test('Ensure fully qualified python path is escaped when building repl args on Windows', async () => { + const pythonPath = 'c:\\program files\\python\\python.exe'; + const terminalArgs = ['-a', 'b', 'c']; + const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); + + await testReplCommandArguments( + true, + pythonPath, + 'c:/program files/python/python.exe', + terminalArgs, + expectedTerminalArgs + ); + }); + + test('Ensure fully qualified python path is returned as is, when building repl args on Windows', async () => { + const pythonPath = 'c:/program files/python/python.exe'; + const terminalArgs = ['-a', 'b', 'c']; + const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); + + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + }); + + test('Ensure python path is returned as is, when building repl args on Windows', async () => { + const pythonPath = PYTHON_PATH; + const terminalArgs = ['-a', 'b', 'c']; + const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); + + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + }); + + test('Ensure fully qualified python path is returned as is, on non Windows', async () => { + const pythonPath = 'usr/bin/python'; + const terminalArgs = ['-a', 'b', 'c']; + const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); + + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + }); + + test('Ensure python path is returned as is, on non Windows', async () => { + const pythonPath = PYTHON_PATH; + const terminalArgs = ['-a', 'b', 'c']; + const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); + + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + }); + + test('Ensure current workspace folder (containing spaces) is used to prefix manage.py', async () => { + const pythonPath = 'python1234'; + const terminalArgs = ['-a', 'b', 'c']; + const workspaceUri = Uri.file(path.join('c', 'usr', 'program files')); + const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); + const expectedTerminalArgs = terminalArgs.concat( + `${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument()}`, + 'shell' + ); + + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + }); + + test('Ensure current workspace folder (without spaces) is used to prefix manage.py', async () => { + const pythonPath = 'python1234'; + const terminalArgs = ['-a', 'b', 'c']; + const workspaceUri = Uri.file(path.join('c', 'usr', 'programfiles')); + const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); + const expectedTerminalArgs = terminalArgs.concat( + path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument(), + 'shell' + ); + + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + }); + + test('Ensure default workspace folder (containing spaces) is used to prefix manage.py', async () => { + const pythonPath = 'python1234'; + const terminalArgs = ['-a', 'b', 'c']; + const workspaceUri = Uri.file(path.join('c', 'usr', 'program files')); + const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + workspace.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + const expectedTerminalArgs = terminalArgs.concat( + `${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument()}`, + 'shell' + ); + + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + }); + + test('Ensure default workspace folder (without spaces) is used to prefix manage.py', async () => { + const pythonPath = 'python1234'; + const terminalArgs = ['-a', 'b', 'c']; + const workspaceUri = Uri.file(path.join('c', 'usr', 'programfiles')); + const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + workspace.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + const expectedTerminalArgs = terminalArgs.concat( + path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument(), + 'shell' + ); + + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + }); + + async function testReplCondaCommandArguments( + pythonPath: string, + terminalArgs: string[], + condaEnv: { name: string; path: string }, + resource?: Uri + ) { + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + + const condaFile = 'conda'; + const processService = TypeMoq.Mock.ofType<IProcessService>(); + const env = createCondaEnv(condaFile, condaEnv, pythonPath, processService.object, fileSystem.object); + const procs = createPythonProcessService(processService.object, env); + const condaExecutionService = { + getInterpreterInformation: env.getInterpreterInformation, + getExecutablePath: env.getExecutablePath, + isModuleInstalled: env.isModuleInstalled, + getExecutionInfo: env.getExecutionInfo, + execObservable: procs.execObservable, + execModuleObservable: procs.execModuleObservable, + exec: procs.exec, + execModule: procs.execModule + }; + const expectedTerminalArgs = [...terminalArgs, 'manage.py', 'shell']; + pythonExecutionFactory + .setup((p) => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(condaExecutionService)); + + const replCommandArgs = await (executor as DjangoShellCodeExecutionProvider).getExecutableInfo(resource); + + expect(replCommandArgs).not.to.be.an('undefined', 'Conda command args are undefined'); + expect(replCommandArgs.command).to.be.equal(pythonPath, 'Repl should use python not conda'); + expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect terminal arguments'); + } + + test('Ensure conda args including env name are passed when using a conda environment with a name', async () => { + const pythonPath = 'c:/program files/python/python.exe'; + const condaPath = { name: 'foo-env', path: 'path/to/foo-env' }; + const terminalArgs = ['-a', 'b', '-c']; + + await testReplCondaCommandArguments(pythonPath, terminalArgs, condaPath); + }); + + test('Ensure conda args including env path are passed when using a conda environment with an empty name', async () => { + const pythonPath = 'c:/program files/python/python.exe'; + const condaPath = { name: '', path: 'path/to/foo-env' }; + const terminalArgs = ['-a', 'b', '-c']; + + await testReplCondaCommandArguments(pythonPath, terminalArgs, condaPath); + }); +}); diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts new file mode 100644 index 000000000000..843ef8b7c771 --- /dev/null +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -0,0 +1,339 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode'; +import { IApplicationShell, IDocumentManager } from '../../../client/common/application/types'; +import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../../client/common/constants'; +import '../../../client/common/extensions'; +import { BufferDecoder } from '../../../client/common/process/decoder'; +import { ProcessService } from '../../../client/common/process/proc'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { Architecture, OSType } from '../../../client/common/utils/platform'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; +import { ICodeExecutionHelper } from '../../../client/terminals/types'; +import { isOs, isPythonVersion, PYTHON_PATH } from '../../common'; + +const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'terminalExec'); + +// tslint:disable-next-line:max-func-body-length +suite('Terminal - Code Execution Helper', () => { + let documentManager: TypeMoq.IMock<IDocumentManager>; + let applicationShell: TypeMoq.IMock<IApplicationShell>; + let helper: ICodeExecutionHelper; + let document: TypeMoq.IMock<TextDocument>; + let editor: TypeMoq.IMock<TextEditor>; + let processService: TypeMoq.IMock<IProcessService>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + const workingPython: PythonEnvironment = { + path: PYTHON_PATH, + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + displayName: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64 + }; + + setup(() => { + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + applicationShell = TypeMoq.Mock.ofType<IApplicationShell>(); + const envVariablesProvider = TypeMoq.Mock.ofType<IEnvironmentVariablesProvider>(); + processService = TypeMoq.Mock.ofType<IProcessService>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + // tslint:disable-next-line:no-any + processService.setup((x: any) => x.then).returns(() => undefined); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(workingPython)); + const processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + envVariablesProvider + .setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())) + .returns(() => processServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) + .returns(() => interpreterService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())) + .returns(() => documentManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) + .returns(() => applicationShell.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())) + .returns(() => envVariablesProvider.object); + helper = new CodeExecutionHelper(serviceContainer.object); + + document = TypeMoq.Mock.ofType<TextDocument>(); + editor = TypeMoq.Mock.ofType<TextEditor>(); + editor.setup((e) => e.document).returns(() => document.object); + }); + + async function ensureBlankLinesAreRemoved(source: string, expectedSource: string) { + const actualProcessService = new ProcessService(new BufferDecoder()); + processService + .setup((p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => { + return actualProcessService.exec.apply(actualProcessService, [file, args, options]); + }); + const normalizedZCode = await helper.normalizeLines(source); + // In case file has been saved with different line endings. + expectedSource = expectedSource.splitLines({ removeEmptyEntries: false, trim: false }).join(EOL); + expect(normalizedZCode).to.be.equal(expectedSource); + } + test('Ensure blank lines are NOT removed when code is not indented (simple)', async function () { + // This test has not been working for many months in Python 2.7 under + // Windows.Tracked by #2544. + if (isOs(OSType.Windows) && (await isPythonVersion('2.7'))) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + + const code = [ + 'import sys', + '', + '', + '', + 'print(sys.executable)', + '', + 'print("1234")', + '', + '', + 'print(1)', + 'print(2)' + ]; + const expectedCode = code.filter((line) => line.trim().length > 0).join(EOL); + await ensureBlankLinesAreRemoved(code.join(EOL), expectedCode); + }); + test('Ensure there are no multiple-CR elements in the normalized code.', async () => { + const code = [ + 'import sys', + '', + '', + '', + 'print(sys.executable)', + '', + 'print("1234")', + '', + '', + 'print(1)', + 'print(2)' + ]; + const actualProcessService = new ProcessService(new BufferDecoder()); + processService + .setup((p) => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((_file, args, options) => { + return actualProcessService.exec.apply(actualProcessService, [PYTHON_PATH, args, options]); + }); + const normalizedCode = await helper.normalizeLines(code.join(EOL)); + const doubleCrIndex = normalizedCode.indexOf('\r\r'); + expect(doubleCrIndex).to.be.equal(-1, 'Double CR (CRCRLF) line endings detected in normalized code snippet.'); + }); + ['', '1', '2', '3', '4', '5', '6', '7', '8'].forEach((fileNameSuffix) => { + test(`Ensure blank lines are removed (Sample${fileNameSuffix})`, async function () { + // This test has not been working for many months in Python 2.7 under + // Windows.Tracked by #2544. + if (isOs(OSType.Windows) && (await isPythonVersion('2.7'))) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + + const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); + const expectedCode = await fs.readFile( + path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), + 'utf8' + ); + await ensureBlankLinesAreRemoved(code, expectedCode); + }); + test(`Ensure last two blank lines are preserved (Sample${fileNameSuffix})`, async function () { + // This test has not been working for many months in Python 2.7 under + // Windows.Tracked by #2544. + if (isOs(OSType.Windows) && (await isPythonVersion('2.7'))) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + + const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); + const expectedCode = await fs.readFile( + path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), + 'utf8' + ); + await ensureBlankLinesAreRemoved(code + EOL, expectedCode + EOL); + }); + test(`Ensure last two blank lines are preserved even if we have more than 2 trailing blank lines (Sample${fileNameSuffix})`, async function () { + // This test has not been working for many months in Python 2.7 under + // Windows.Tracked by #2544. + if (isOs(OSType.Windows) && (await isPythonVersion('2.7'))) { + // tslint:disable-next-line:no-invalid-this + return this.skip(); + } + + const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); + const expectedCode = await fs.readFile( + path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), + 'utf8' + ); + await ensureBlankLinesAreRemoved(code + EOL + EOL + EOL + EOL, expectedCode + EOL); + }); + }); + test("Display message if there's no active file", async () => { + documentManager.setup((doc) => doc.activeTextEditor).returns(() => undefined); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.an('undefined'); + applicationShell.verify((a) => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + }); + + test('Display message if active file is unsaved', async () => { + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); + document.setup((doc) => doc.isUntitled).returns(() => true); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.an('undefined'); + applicationShell.verify((a) => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + }); + + test('Display message if active file is non-python', async () => { + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.languageId).returns(() => 'html'); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.an('undefined'); + applicationShell.verify((a) => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + }); + + test('Returns file uri', async () => { + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); + const expectedUri = Uri.file('one.py'); + document.setup((doc) => doc.uri).returns(() => expectedUri); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.deep.equal(expectedUri); + }); + + test('Returns file uri even if saving fails', async () => { + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.isDirty).returns(() => true); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); + document.setup((doc) => doc.save()).returns(() => Promise.resolve(false)); + const expectedUri = Uri.file('one.py'); + document.setup((doc) => doc.uri).returns(() => expectedUri); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.deep.equal(expectedUri); + }); + + test('Dirty files are saved', async () => { + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.isDirty).returns(() => true); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); + const expectedUri = Uri.file('one.py'); + document.setup((doc) => doc.uri).returns(() => expectedUri); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.deep.equal(expectedUri); + document.verify((doc) => doc.save(), TypeMoq.Times.once()); + }); + + test('Non-Dirty files are not-saved', async () => { + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.isDirty).returns(() => false); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); + const expectedUri = Uri.file('one.py'); + document.setup((doc) => doc.uri).returns(() => expectedUri); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.deep.equal(expectedUri); + document.verify((doc) => doc.save(), TypeMoq.Times.never()); + }); + + test('Returns current line if nothing is selected', async () => { + const lineContents = 'Line Contents'; + editor.setup((e) => e.selection).returns(() => new Selection(3, 0, 3, 0)); + const textLine = TypeMoq.Mock.ofType<TextLine>(); + textLine.setup((t) => t.text).returns(() => lineContents); + document.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal(lineContents); + }); + + test('Returns selected text', async () => { + const lineContents = 'Line Contents'; + editor.setup((e) => e.selection).returns(() => new Selection(3, 0, 10, 5)); + const textLine = TypeMoq.Mock.ofType<TextLine>(); + textLine.setup((t) => t.text).returns(() => lineContents); + document + .setup((d) => d.getText(TypeMoq.It.isAny())) + .returns((r: Range) => `${r.start.line}.${r.start.character}.${r.end.line}.${r.end.character}`); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal('3.0.10.5'); + }); + + test('saveFileIfDirty will not fail if file is not opened', async () => { + documentManager + .setup((d) => d.textDocuments) + .returns(() => []) + .verifiable(TypeMoq.Times.once()); + + await helper.saveFileIfDirty(Uri.file(`${__filename}.py`)); + documentManager.verifyAll(); + }); + + test('File will be saved if file is dirty', async () => { + documentManager + .setup((d) => d.textDocuments) + .returns(() => [document.object]) + .verifiable(TypeMoq.Times.once()); + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.isDirty).returns(() => true); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); + const expectedUri = Uri.file('one.py'); + document.setup((doc) => doc.uri).returns(() => expectedUri); + + await helper.saveFileIfDirty(expectedUri); + documentManager.verifyAll(); + document.verify((doc) => doc.save(), TypeMoq.Times.once()); + }); + + test('File will be not saved if file is not dirty', async () => { + documentManager + .setup((d) => d.textDocuments) + .returns(() => [document.object]) + .verifiable(TypeMoq.Times.once()); + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.isDirty).returns(() => false); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); + const expectedUri = Uri.file('one.py'); + document.setup((doc) => doc.uri).returns(() => expectedUri); + + await helper.saveFileIfDirty(expectedUri); + documentManager.verifyAll(); + document.verify((doc) => doc.save(), TypeMoq.Times.never()); + }); +}); diff --git a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts new file mode 100644 index 000000000000..eb05531b0405 --- /dev/null +++ b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts @@ -0,0 +1,560 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-multiline-string no-trailing-whitespace max-func-body-length + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { createCondaEnv } from '../../../client/common/process/pythonEnvironment'; +import { createPythonProcessService } from '../../../client/common/process/pythonProcess'; +import { IProcessService, IPythonExecutionFactory } from '../../../client/common/process/types'; +import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; +import { noop } from '../../../client/common/utils/misc'; +import { DjangoShellCodeExecutionProvider } from '../../../client/terminals/codeExecution/djangoShellCodeExecution'; +import { ReplProvider } from '../../../client/terminals/codeExecution/repl'; +import { TerminalCodeExecutionProvider } from '../../../client/terminals/codeExecution/terminalCodeExecution'; +import { ICodeExecutionService } from '../../../client/terminals/types'; +import { PYTHON_PATH } from '../../common'; + +suite('Terminal - Code Execution', () => { + ['Terminal Execution', 'Repl Execution', 'Django Execution'].forEach((testSuiteName) => { + let terminalSettings: TypeMoq.IMock<ITerminalSettings>; + let terminalService: TypeMoq.IMock<ITerminalService>; + let workspace: TypeMoq.IMock<IWorkspaceService>; + let platform: TypeMoq.IMock<IPlatformService>; + let workspaceFolder: TypeMoq.IMock<WorkspaceFolder>; + let settings: TypeMoq.IMock<IPythonSettings>; + let disposables: Disposable[] = []; + let executor: ICodeExecutionService; + let expectedTerminalTitle: string | undefined; + let terminalFactory: TypeMoq.IMock<ITerminalServiceFactory>; + let documentManager: TypeMoq.IMock<IDocumentManager>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>; + let isDjangoRepl: boolean; + + teardown(() => { + disposables.forEach((disposable) => { + if (disposable) { + disposable.dispose(); + } + }); + + disposables = []; + }); + + setup(() => { + terminalFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>(); + terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>(); + terminalService = TypeMoq.Mock.ofType<ITerminalService>(); + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + platform = TypeMoq.Mock.ofType<IPlatformService>(); + workspaceFolder = TypeMoq.Mock.ofType<WorkspaceFolder>(); + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + settings = TypeMoq.Mock.ofType<IPythonSettings>(); + settings.setup((s) => s.terminal).returns(() => terminalSettings.object); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + + switch (testSuiteName) { + case 'Terminal Execution': { + executor = new TerminalCodeExecutionProvider( + terminalFactory.object, + configService.object, + workspace.object, + disposables, + platform.object + ); + break; + } + case 'Repl Execution': { + executor = new ReplProvider( + terminalFactory.object, + configService.object, + workspace.object, + disposables, + platform.object + ); + expectedTerminalTitle = 'REPL'; + break; + } + case 'Django Execution': { + isDjangoRepl = true; + workspace + .setup((w) => + w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => { + return { dispose: noop }; + }); + executor = new DjangoShellCodeExecutionProvider( + terminalFactory.object, + configService.object, + workspace.object, + documentManager.object, + platform.object, + commandManager.object, + fileSystem.object, + disposables + ); + expectedTerminalTitle = 'Django Shell'; + break; + } + default: { + break; + } + } + }); + + suite(`${testSuiteName} (validation of title)`, () => { + setup(() => { + terminalFactory + .setup((f) => f.getTerminalService(TypeMoq.It.isAny(), TypeMoq.It.isValue(expectedTerminalTitle))) + .returns(() => terminalService.object); + }); + + async function ensureTerminalIsCreatedUponInvokingInitializeRepl( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean + ): Promise<void> { + platform.setup((p) => p.isWindows).returns(() => isWindows); + platform.setup((p) => p.isMac).returns(() => isOsx); + platform.setup((p) => p.isLinux).returns(() => isLinux); + settings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); + terminalSettings.setup((t) => t.launchArgs).returns(() => []); + + await executor.initializeRepl(); + } + + test('Ensure terminal is created upon invoking initializeRepl (windows)', async () => { + await ensureTerminalIsCreatedUponInvokingInitializeRepl(true, false, false); + }); + + test('Ensure terminal is created upon invoking initializeRepl (osx)', async () => { + await ensureTerminalIsCreatedUponInvokingInitializeRepl(false, true, false); + }); + + test('Ensure terminal is created upon invoking initializeRepl (linux)', async () => { + await ensureTerminalIsCreatedUponInvokingInitializeRepl(false, false, true); + }); + }); + + suite(testSuiteName, async function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(5000); // Activation of terminals take some time (there's a delay in the code to account for VSC Terminal issues). + setup(() => { + terminalFactory + .setup((f) => f.getTerminalService(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => terminalService.object); + }); + + async function ensureWeSetCurrentDirectoryBeforeExecutingAFile(_isWindows: boolean): Promise<void> { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => true); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceFolder.setup((w) => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); + platform.setup((p) => p.isWindows).returns(() => false); + settings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); + terminalSettings.setup((t) => t.launchArgs).returns(() => []); + + await executor.executeFile(file); + + terminalService.verify( + async (t) => + t.sendText(TypeMoq.It.isValue(`cd ${path.dirname(file.fsPath).fileToCommandArgument()}`)), + TypeMoq.Times.once() + ); + } + test('Ensure we set current directory before executing file (non windows)', async () => { + await ensureWeSetCurrentDirectoryBeforeExecutingAFile(false); + }); + test('Ensure we set current directory before executing file (windows)', async () => { + await ensureWeSetCurrentDirectoryBeforeExecutingAFile(true); + }); + + async function ensureWeWetCurrentDirectoryAndQuoteBeforeExecutingFile(isWindows: boolean): Promise<void> { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => true); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceFolder.setup((w) => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); + platform.setup((p) => p.isWindows).returns(() => isWindows); + settings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); + terminalSettings.setup((t) => t.launchArgs).returns(() => []); + + await executor.executeFile(file); + const dir = path.dirname(file.fsPath).fileToCommandArgument(); + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isValue(`cd ${dir}`)), TypeMoq.Times.once()); + } + + test('Ensure we set current directory (and quote it when containing spaces) before executing file (non windows)', async () => { + await ensureWeWetCurrentDirectoryAndQuoteBeforeExecutingFile(false); + }); + + test('Ensure we set current directory (and quote it when containing spaces) before executing file (windows)', async () => { + await ensureWeWetCurrentDirectoryAndQuoteBeforeExecutingFile(true); + }); + + async function ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileInSameDirectory( + isWindows: boolean + ): Promise<void> { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => true); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceFolder + .setup((w) => w.uri) + .returns(() => Uri.file(path.join('c', 'path', 'to', 'file with spaces in path'))); + platform.setup((p) => p.isWindows).returns(() => isWindows); + settings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); + terminalSettings.setup((t) => t.launchArgs).returns(() => []); + + await executor.executeFile(file); + + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + } + test('Ensure we do not set current directory before executing file if in the same directory (non windows)', async () => { + await ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileInSameDirectory(false); + }); + test('Ensure we do not set current directory before executing file if in the same directory (windows)', async () => { + await ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileInSameDirectory(true); + }); + + async function ensureWeSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory( + isWindows: boolean + ): Promise<void> { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => true); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + platform.setup((p) => p.isWindows).returns(() => isWindows); + settings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); + terminalSettings.setup((t) => t.launchArgs).returns(() => []); + + await executor.executeFile(file); + + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.once()); + } + test('Ensure we set current directory before executing file if file is not in a workspace (non windows)', async () => { + await ensureWeSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory(false); + }); + test('Ensure we set current directory before executing file if file is not in a workspace (windows)', async () => { + await ensureWeSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory(true); + }); + + async function testFileExecution( + isWindows: boolean, + pythonPath: string, + terminalArgs: string[], + file: Uri + ): Promise<void> { + platform.setup((p) => p.isWindows).returns(() => isWindows); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => false); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + pythonExecutionFactory + .setup((p) => + p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(undefined)); + + await executor.executeFile(file); + const expectedPythonPath = isWindows ? pythonPath.replace(/\\/g, '/') : pythonPath; + const expectedArgs = terminalArgs.concat(file.fsPath.fileToCommandArgument()); + terminalService.verify( + async (t) => + t.sendCommand(TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(expectedArgs)), + TypeMoq.Times.once() + ); + } + + test('Ensure python file execution script is sent to terminal on windows', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + await testFileExecution(true, PYTHON_PATH, [], file); + }); + + test('Ensure python file execution script is sent to terminal on windows with fully qualified python path', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + await testFileExecution(true, 'c:\\program files\\python', [], file); + }); + + test('Ensure python file execution script is not quoted when no spaces in file path', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + await testFileExecution(true, PYTHON_PATH, [], file); + }); + + test('Ensure python file execution script supports custom python arguments', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + await testFileExecution(false, PYTHON_PATH, ['-a', '-b', '-c'], file); + }); + + async function testCondaFileExecution( + pythonPath: string, + terminalArgs: string[], + file: Uri, + condaEnv: { name: string; path: string } + ): Promise<void> { + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => false); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + + const condaFile = 'conda'; + const procService = TypeMoq.Mock.ofType<IProcessService>(); + const env = createCondaEnv(condaFile, condaEnv, pythonPath, procService.object, fileSystem.object); + const procs = createPythonProcessService(procService.object, env); + const condaExecutionService = { + getInterpreterInformation: env.getInterpreterInformation, + getExecutablePath: env.getExecutablePath, + isModuleInstalled: env.isModuleInstalled, + getExecutionInfo: env.getExecutionInfo, + execObservable: procs.execObservable, + execModuleObservable: procs.execModuleObservable, + exec: procs.exec, + execModule: procs.execModule + }; + pythonExecutionFactory + .setup((p) => + p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(condaExecutionService)); + + await executor.executeFile(file); + + const expectedArgs = [...terminalArgs, file.fsPath.fileToCommandArgument()]; + + terminalService.verify( + async (t) => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedArgs)), + TypeMoq.Times.once() + ); + } + + test('Ensure conda args with conda env name are sent to terminal if there is a conda environment with a name', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + await testCondaFileExecution(PYTHON_PATH, ['-a', '-b', '-c'], file, { + name: 'foo-env', + path: 'path/to/foo-env' + }); + }); + + test('Ensure conda args with conda env path are sent to terminal if there is a conda environment without a name', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + await testCondaFileExecution(PYTHON_PATH, ['-a', '-b', '-c'], file, { + name: '', + path: 'path/to/foo-env' + }); + }); + + async function testReplCommandArguments( + isWindows: boolean, + pythonPath: string, + expectedPythonPath: string, + terminalArgs: string[] + ) { + pythonExecutionFactory + .setup((p) => + p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(undefined)); + platform.setup((p) => p.isWindows).returns(() => isWindows); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; + + const replCommandArgs = await (executor as TerminalCodeExecutionProvider).getExecutableInfo(); + expect(replCommandArgs).not.to.be.an('undefined', 'Command args is undefined'); + expect(replCommandArgs.command).to.be.equal(expectedPythonPath, 'Incorrect python path'); + expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect arguments'); + } + + test('Ensure fully qualified python path is escaped when building repl args on Windows', async () => { + const pythonPath = 'c:\\program files\\python\\python.exe'; + const terminalArgs = ['-a', 'b', 'c']; + + await testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs); + }); + + test('Ensure fully qualified python path is returned as is, when building repl args on Windows', async () => { + const pythonPath = 'c:/program files/python/python.exe'; + const terminalArgs = ['-a', 'b', 'c']; + + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); + }); + + test('Ensure python path is returned as is, when building repl args on Windows', async () => { + const pythonPath = PYTHON_PATH; + const terminalArgs = ['-a', 'b', 'c']; + + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); + }); + + test('Ensure fully qualified python path is returned as is, on non Windows', async () => { + const pythonPath = 'usr/bin/python'; + const terminalArgs = ['-a', 'b', 'c']; + + await testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + }); + + test('Ensure python path is returned as is, on non Windows', async () => { + const pythonPath = PYTHON_PATH; + const terminalArgs = ['-a', 'b', 'c']; + + await testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + }); + + async function testReplCondaCommandArguments( + pythonPath: string, + terminalArgs: string[], + condaEnv: { name: string; path: string } + ) { + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + + const condaFile = 'conda'; + const procService = TypeMoq.Mock.ofType<IProcessService>(); + const env = createCondaEnv(condaFile, condaEnv, pythonPath, procService.object, fileSystem.object); + const procs = createPythonProcessService(procService.object, env); + const condaExecutionService = { + getInterpreterInformation: env.getInterpreterInformation, + getExecutablePath: env.getExecutablePath, + isModuleInstalled: env.isModuleInstalled, + getExecutionInfo: env.getExecutionInfo, + execObservable: procs.execObservable, + execModuleObservable: procs.execModuleObservable, + exec: procs.exec, + execModule: procs.execModule + }; + pythonExecutionFactory + .setup((p) => + p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(condaExecutionService)); + + const djangoArgs = isDjangoRepl ? ['manage.py', 'shell'] : []; + const expectedTerminalArgs = [...terminalArgs, ...djangoArgs]; + + const replCommandArgs = await (executor as TerminalCodeExecutionProvider).getExecutableInfo(); + + expect(replCommandArgs).not.to.be.an('undefined', 'Conda command args are undefined'); + expect(replCommandArgs.command).to.be.equal(pythonPath, 'Repl needs to use python, not conda'); + expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect terminal arguments'); + } + + test('Ensure conda args with env name are returned when building repl args with a conda env with a name', async () => { + await testReplCondaCommandArguments(PYTHON_PATH, ['-a', 'b', 'c'], { + name: 'foo-env', + path: 'path/to/foo-env' + }); + }); + + test('Ensure conda args with env path are returned when building repl args with a conda env without a name', async () => { + await testReplCondaCommandArguments(PYTHON_PATH, ['-a', 'b', 'c'], { + name: '', + path: 'path/to/foo-env' + }); + }); + + test('Ensure nothing happens when blank text is sent to the terminal', async () => { + await executor.execute(''); + await executor.execute(' '); + // tslint:disable-next-line:no-any + await executor.execute((undefined as any) as string); + + terminalService.verify( + async (t) => t.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never() + ); + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + + test('Ensure repl is initialized once before sending text to the repl', async () => { + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + + platform.setup((p) => p.isWindows).returns(() => false); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + + await executor.execute('cmd1'); + await executor.execute('cmd2'); + await executor.execute('cmd3'); + + const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; + terminalService.verify( + async (t) => + t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), + TypeMoq.Times.once() + ); + }); + + test('Ensure repl is re-initialized when terminal is closed', async () => { + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + platform.setup((p) => p.isWindows).returns(() => false); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + + let closeTerminalCallback: undefined | (() => void); + terminalService + .setup((t) => t.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((callback) => { + closeTerminalCallback = callback; + return { + dispose: noop + }; + }); + + await executor.execute('cmd1'); + await executor.execute('cmd2'); + await executor.execute('cmd3'); + + const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; + + expect(closeTerminalCallback).not.to.be.an('undefined', 'Callback not initialized'); + terminalService.verify( + async (t) => + t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), + TypeMoq.Times.once() + ); + + closeTerminalCallback!.call(terminalService.object); + await executor.execute('cmd4'); + terminalService.verify( + async (t) => + t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), + TypeMoq.Times.exactly(2) + ); + + closeTerminalCallback!.call(terminalService.object); + await executor.execute('cmd5'); + terminalService.verify( + async (t) => + t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), + TypeMoq.Times.exactly(3) + ); + }); + + test('Ensure code is sent to terminal', async () => { + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + platform.setup((p) => p.isWindows).returns(() => false); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + + await executor.execute('cmd1'); + terminalService.verify(async (t) => t.sendText('cmd1'), TypeMoq.Times.once()); + + await executor.execute('cmd2'); + terminalService.verify(async (t) => t.sendText('cmd2'), TypeMoq.Times.once()); + }); + }); + }); +}); diff --git a/src/test/terminals/serviceRegistry.unit.test.ts b/src/test/terminals/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..67446f44e601 --- /dev/null +++ b/src/test/terminals/serviceRegistry.unit.test.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as typemoq from 'typemoq'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { IServiceManager } from '../../client/ioc/types'; +import { ExtensionActivationForTerminalActivation, TerminalAutoActivation } from '../../client/terminals/activation'; +import { CodeExecutionManager } from '../../client/terminals/codeExecution/codeExecutionManager'; +import { DjangoShellCodeExecutionProvider } from '../../client/terminals/codeExecution/djangoShellCodeExecution'; +import { CodeExecutionHelper } from '../../client/terminals/codeExecution/helper'; +import { ReplProvider } from '../../client/terminals/codeExecution/repl'; +import { TerminalCodeExecutionProvider } from '../../client/terminals/codeExecution/terminalCodeExecution'; +import { registerTypes } from '../../client/terminals/serviceRegistry'; +import { + ICodeExecutionHelper, + ICodeExecutionManager, + ICodeExecutionService, + ITerminalAutoActivation +} from '../../client/terminals/types'; + +suite('Terminal - Service Registry', () => { + test('Ensure all services get registered', () => { + const services = typemoq.Mock.ofType<IServiceManager>(undefined, typemoq.MockBehavior.Strict); + [ + [ICodeExecutionHelper, CodeExecutionHelper], + [ICodeExecutionManager, CodeExecutionManager], + [ICodeExecutionService, DjangoShellCodeExecutionProvider, 'djangoShell'], + [IExtensionSingleActivationService, ExtensionActivationForTerminalActivation], + [ICodeExecutionService, ReplProvider, 'repl'], + [ITerminalAutoActivation, TerminalAutoActivation], + [ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'] + ].forEach((args) => { + if (args.length === 2) { + services + .setup((s) => + s.addSingleton( + // tslint:disable-next-line:no-any + typemoq.It.isValue(args[0] as any), + typemoq.It.is((value) => args[1] === value) + ) + ) + .verifiable(typemoq.Times.once()); + } else { + services + .setup((s) => + s.addSingleton( + // tslint:disable-next-line:no-any + typemoq.It.isValue(args[0] as any), + typemoq.It.is((value) => args[1] === value), + // tslint:disable-next-line:no-any + typemoq.It.isValue(args[2] as any) + ) + ) + .verifiable(typemoq.Times.once()); + } + }); + + registerTypes(services.object); + + services.verifyAll(); + }); +}); diff --git a/src/test/testBootstrap.ts b/src/test/testBootstrap.ts new file mode 100644 index 000000000000..06d65d41f8fe --- /dev/null +++ b/src/test/testBootstrap.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ChildProcess, spawn, SpawnOptions } from 'child_process'; +import * as fs from 'fs-extra'; +import { AddressInfo, createServer, Server } from 'net'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../client/constants'; +import { noop, sleep } from './core'; +import { initializeLogger } from './testLogger'; + +initializeLogger(); + +// tslint:disable:no-console + +/* +This is a simple work around for tests tasks not completing on Azure Pipelines. +What's been happening is, the tests run however for some readon the Node propcess (VS Code) does not exit. +Here's what we've tried thus far: +* Dispose all timers +* Close all open streams/sockets. +* Use `process.exit` and use the VSC commands to close itself. + +Final solution: +* Start a node.js procecss + * This process will start a socket server + * This procecss will start the tests in a separate procecss (spawn) +* When the tests have completed, + * Send a message to the socket server with a flag (true/false whether tests passed/failed) +* Socket server (main procecss) will receive the test status flag. + * This will kill the spawned process + * This main process will kill itself with exit code 0 if tests pass succesfully, else 1. +*/ + +const testFile = process.argv[2]; +const portFile = path.join(EXTENSION_ROOT_DIR, 'port.txt'); + +let proc: ChildProcess | undefined; +let server: Server | undefined; + +async function deletePortFile() { + try { + if (await fs.pathExists(portFile)) { + await fs.unlink(portFile); + } + } catch { + noop(); + } +} +async function end(exitCode: number) { + if (exitCode === 0) { + console.log('Exiting without errors'); + } else { + console.error('Exiting with test failures'); + } + if (proc) { + try { + const procToKill = proc; + proc = undefined; + console.log('Killing VSC'); + await deletePortFile(); + // Wait for the std buffers to get flushed before killing. + await sleep(5_000); + procToKill.kill(); + } catch { + noop(); + } + } + if (server) { + server.close(); + } + // Exit with required code. + process.exit(exitCode); +} + +async function startSocketServer() { + return new Promise((resolve) => { + server = createServer((socket) => { + socket.on('data', (buffer) => { + const data = buffer.toString('utf8'); + console.log(`Exit code from Tests is ${data}`); + const code = parseInt(data.substring(0, 1), 10); + end(code).catch(noop); + }); + socket.on('error', (ex) => { + // Just log it, no need to do anything else. + console.error(ex); + }); + }); + + server.listen({ host: '127.0.0.1', port: 0 }, async () => { + const port = (server!.address() as AddressInfo).port; + console.log(`Test server listening on port ${port}`); + await deletePortFile(); + await fs.writeFile(portFile, port.toString()); + resolve(); + }); + server.on('error', (ex) => { + // Just log it, no need to do anything else. + console.error(ex); + }); + }); +} + +async function start() { + await startSocketServer(); + const options: SpawnOptions = { cwd: process.cwd(), env: process.env, detached: true, stdio: 'inherit' }; + proc = spawn(process.execPath, [testFile], options); + proc.once('close', end); +} + +start().catch((ex) => { + console.error('File testBootstrap.ts failed with Errors', ex); + process.exit(1); +}); diff --git a/src/test/testLogger.ts b/src/test/testLogger.ts new file mode 100644 index 000000000000..5090e23d1faf --- /dev/null +++ b/src/test/testLogger.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// IMPORTANT: This file should only be importing from the '../client/logging' directory, as we +// delete everything in '../client' except for '../client/logging' before running smoke tests. + +import { LogLevel } from '../client/logging/levels'; +import { configureLogger, createLogger, getPreDefinedConfiguration, logToAll } from '../client/logging/logger'; + +const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; +const monkeyPatchLogger = createLogger(); + +export function initializeLogger() { + const config = getPreDefinedConfiguration(); + if (isCI && process.env.VSC_PYTHON_LOG_FILE) { + delete config.console; + // This is a separate logger that matches our config but + // does not do any console logging. + configureLogger(monkeyPatchLogger, config); + // Send console.*() to the non-console loggers. + monkeypatchConsole(); + } +} + +/** + * What we're doing here is monkey patching the console.log so we can + * send everything sent to console window into our logs. This is only + * required when we're directly writing to `console.log` or not using + * our `winston logger`. This is something we'd generally turn on only + * on CI so we can see everything logged to the console window + * (via the logs). + */ +function monkeypatchConsole() { + // The logging "streams" (methods) of the node console. + const streams = ['log', 'error', 'warn', 'info', 'debug', 'trace']; + const levels: { [key: string]: LogLevel } = { + error: LogLevel.Error, + warn: LogLevel.Warn + }; + // tslint:disable-next-line:no-any + const consoleAny: any = console; + for (const stream of streams) { + // Using symbols guarantee the properties will be unique & prevents + // clashing with names other code/library may create or have created. + // We could use a closure but it's a bit trickier. + const sym = Symbol.for(stream); + consoleAny[sym] = consoleAny[stream]; + // tslint:disable-next-line: no-function-expression + consoleAny[stream] = function () { + const args = Array.prototype.slice.call(arguments); + const fn = consoleAny[sym]; + fn(...args); + const level = levels[stream] || LogLevel.Info; + logToAll([monkeyPatchLogger], level, args); + }; + } +} diff --git a/src/test/testRunner.ts b/src/test/testRunner.ts new file mode 100644 index 000000000000..12788f9c93dd --- /dev/null +++ b/src/test/testRunner.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-require-imports no-var-requires import-name no-function-expression no-any prefer-template no-console no-var-self +// Most of the source is in node_modules/vscode/lib/testrunner.js + +'use strict'; +import * as glob from 'glob'; +import * as Mocha from 'mocha'; +import * as path from 'path'; +import { IS_SMOKE_TEST, MAX_EXTENSION_ACTIVATION_TIME } from './constants'; +import { initialize } from './initialize'; + +// Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY. +// Since we are not running in a tty environment, we just implement the method statically. +const tty = require('tty'); +if (!tty.getWindowSize) { + tty.getWindowSize = function (): number[] { + return [80, 75]; + }; +} + +let mocha = new Mocha(<any>{ + ui: 'tdd', + colors: true +}); + +export type SetupOptions = Mocha.MochaOptions & { + testFilesSuffix?: string; + reporterOptions?: { + mochaFile?: string; + properties?: string; + }; +}; + +let testFilesGlob = 'test'; + +export function configure(setupOptions: SetupOptions): void { + if (setupOptions.testFilesSuffix) { + testFilesGlob = setupOptions.testFilesSuffix; + } + // Force Mocha to exit. + (setupOptions as any).exit = true; + mocha = new Mocha(setupOptions); +} + +export async function run(): Promise<void> { + const testsRoot = path.join(__dirname); + // Enable source map support. + require('source-map-support').install(); + + // nteract/transforms-full expects to run in the browser so we have to fake + // parts of the browser here. + if (!IS_SMOKE_TEST) { + const reactHelpers = require('./datascience/reactHelpers') as typeof import('./datascience/reactHelpers'); + reactHelpers.setUpDomEnvironment(); + } + + /** + * Waits until the Python Extension completes loading or a timeout. + * When running tests within VSC, we need to wait for the Python Extension to complete loading, + * this is where `initialize` comes in, we load the PVSC extension using VSC API, wait for it + * to complete. + * That's when we know out PVSC extension specific code is ready for testing. + * So, this code needs to run always for every test running in VS Code (what we call these `system test`) . + * @returns + */ + function initializationScript() { + const ex = new Error('Failed to initialize Python extension for tests after 3 minutes'); + let timer: NodeJS.Timer | undefined; + const failed = new Promise((_, reject) => { + timer = setTimeout(() => reject(ex), MAX_EXTENSION_ACTIVATION_TIME); + }); + const promise = Promise.race([initialize(), failed]); + promise.then(() => clearTimeout(timer!)).catch(() => clearTimeout(timer!)); + return promise; + } + // Run the tests. + await new Promise<void>((resolve, reject) => { + glob( + `**/**.${testFilesGlob}.js`, + { ignore: ['**/**.unit.test.js', '**/**.functional.test.js'], cwd: testsRoot }, + (error, files) => { + if (error) { + return reject(error); + } + try { + files.forEach((file) => mocha.addFile(path.join(testsRoot, file))); + initializationScript() + .then(() => + mocha.run((failures) => + failures > 0 ? reject(new Error(`${failures} total failures`)) : resolve() + ) + ) + .catch(reject); + } catch (error) { + return reject(error); + } + } + ); + }); +} diff --git a/src/test/testing/argsService.test.ts b/src/test/testing/argsService.test.ts new file mode 100644 index 000000000000..2bddbba45cf2 --- /dev/null +++ b/src/test/testing/argsService.test.ts @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { strictEqual } from 'assert'; +import { expect } from 'chai'; +import { spawnSync } from 'child_process'; +import * as typeMoq from 'typemoq'; +import { Product } from '../../client/common/types'; +import { getNamesAndValues } from '../../client/common/utils/enum'; +import { IServiceContainer } from '../../client/ioc/types'; +import { ArgumentsHelper } from '../../client/testing/common/argumentsHelper'; +import { UNIT_TEST_PRODUCTS } from '../../client/testing/common/constants'; +import { ArgumentsService as NoseTestArgumentsService } from '../../client/testing/nosetest/services/argsService'; +import { ArgumentsService as PyTestArgumentsService } from '../../client/testing/pytest/services/argsService'; +import { IArgumentsHelper, IArgumentsService } from '../../client/testing/types'; +import { ArgumentsService as UnitTestArgumentsService } from '../../client/testing/unittest/services/argsService'; +import { PYTHON_PATH } from '../common'; +import { TEST_TIMEOUT } from '../constants'; + +suite('ArgsService: Common', () => { + UNIT_TEST_PRODUCTS.forEach((product) => { + const productNames = getNamesAndValues(Product); + const productName = productNames.find((item) => item.value === product)!.name; + suite(productName, () => { + let argumentsService: IArgumentsService; + let moduleName = ''; + let expectedWithArgs: string[] = []; + let expectedWithoutArgs: string[] = []; + + setup(function () { + // Take the spawning of process into account. + // tslint:disable-next-line:no-invalid-this + this.timeout(TEST_TIMEOUT * 2); + const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + + const argsHelper = new ArgumentsHelper(); + + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IArgumentsHelper), typeMoq.It.isAny())) + .returns(() => argsHelper); + + switch (product) { + case Product.unittest: { + argumentsService = new UnitTestArgumentsService(serviceContainer.object); + moduleName = 'unittest'; + break; + } + case Product.nosetest: { + argumentsService = new NoseTestArgumentsService(serviceContainer.object); + moduleName = 'nose'; + break; + } + case Product.pytest: { + moduleName = 'pytest'; + argumentsService = new PyTestArgumentsService(serviceContainer.object); + break; + } + default: { + throw new Error('Unrecognized Test Framework'); + } + } + + expectedWithArgs = getOptions(product, moduleName, true); + expectedWithoutArgs = getOptions(product, moduleName, false); + }); + + test('Check for new/unrecognized options with values', () => { + const options = argumentsService.getKnownOptions(); + const optionsNotFound = expectedWithArgs.filter((item) => options.withArgs.indexOf(item) === -1); + + strictEqual(optionsNotFound.length, 0, `unhandled flags: ${optionsNotFound.join(',')}`); + }); + test('Check for new/unrecognized options without values', () => { + const options = argumentsService.getKnownOptions(); + const optionsNotFound = expectedWithoutArgs.filter((item) => options.withoutArgs.indexOf(item) === -1); + + strictEqual(optionsNotFound.length, 0, `unhandled flags: ${optionsNotFound.join(',')}`); + }); + test('Test getting value for an option with a single value', () => { + for (const option of expectedWithArgs) { + const args = [ + '--some-option-with-a-value', + '1234', + '--another-value-with-inline=1234', + option, + 'abcd' + ]; + const value = argumentsService.getOptionValue(args, option); + expect(value).to.equal('abcd'); + } + }); + test('Test getting value for an option with a multiple value', () => { + for (const option of expectedWithArgs) { + const args = [ + '--some-option-with-a-value', + '1234', + '--another-value-with-inline=1234', + option, + 'abcd', + option, + 'xyz' + ]; + const value = argumentsService.getOptionValue(args, option); + expect(value).to.deep.equal(['abcd', 'xyz']); + } + }); + test('Test filtering of arguments', () => { + const args: string[] = []; + const knownOptions = argumentsService.getKnownOptions(); + const argumentsToRemove: string[] = []; + const expectedFilteredArgs: string[] = []; + // Generate some random arguments. + for (let i = 0; i < 5; i += 1) { + args.push(knownOptions.withArgs[i], `Random Value ${i}`); + args.push(knownOptions.withoutArgs[i]); + + if (i % 2 === 0) { + argumentsToRemove.push(knownOptions.withArgs[i], knownOptions.withoutArgs[i]); + } else { + expectedFilteredArgs.push(knownOptions.withArgs[i], `Random Value ${i}`); + expectedFilteredArgs.push(knownOptions.withoutArgs[i]); + } + } + + const filteredArgs = argumentsService.filterArguments(args, argumentsToRemove); + expect(filteredArgs).to.be.deep.equal(expectedFilteredArgs); + }); + }); + }); +}); + +function getOptions(product: Product, moduleName: string, withValues: boolean) { + const result = spawnSync(PYTHON_PATH, ['-m', moduleName, '-h']); + const output = result.stdout.toString(); + + // Our regex isn't the best, so lets exclude stuff that shouldn't be captured. + const knownOptionsWithoutArgs: string[] = []; + const knownOptionsWithArgs: string[] = []; + if (product === Product.pytest) { + knownOptionsWithArgs.push(...['-c', '-k', '-p', '-r']); + } + + if (withValues) { + return getOptionsWithArguments(output) + .concat(...knownOptionsWithArgs) + .filter((item) => knownOptionsWithoutArgs.indexOf(item) === -1) + .sort(); + } else { + return ( + getOptionsWithoutArguments(output) + .concat(...knownOptionsWithoutArgs) + .filter((item) => knownOptionsWithArgs.indexOf(item) === -1) + // In pytest, any option beginning with --log- is known to have args. + .filter((item) => (product === Product.pytest ? !item.startsWith('--log-') : true)) + .sort() + ); + } +} + +function getOptionsWithoutArguments(output: string) { + return getMatches('\\s{1,}(-{1,2}[A-Za-z0-9-]+)(?:,|\\s{2,})', output); +} +function getOptionsWithArguments(output: string) { + return getMatches('\\s{1,}(-{1,2}[A-Za-z0-9-]+)(?:=|\\s{0,1}[A-Z])', output); +} + +// tslint:disable-next-line:no-any +function getMatches(pattern: any, str: string) { + const matches: string[] = []; + const regex = new RegExp(pattern, 'gm'); + let result: RegExpExecArray | null = regex.exec(str); + while (result !== null) { + if (result.index === regex.lastIndex) { + regex.lastIndex += 1; + } + matches.push(result[1].trim()); + result = regex.exec(str); + } + return matches + .sort() + .reduce<string[]>((items, item) => (items.indexOf(item) === -1 ? items.concat([item]) : items), []); +} diff --git a/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts b/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts new file mode 100644 index 000000000000..7dcfda29a19d --- /dev/null +++ b/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { Extension } from 'vscode'; +import { LanguageServerType } from '../../../client/activation/types'; +import { IApplicationEnvironment, IApplicationShell } from '../../../client/common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants'; +import { TryPylance } from '../../../client/common/experiments/groups'; +import { + IConfigurationService, + IExperimentService, + IExtensions, + IPersistentState, + IPersistentStateFactory, + IPythonSettings +} from '../../../client/common/types'; +import { Common, Pylance } from '../../../client/common/utils/localize'; +import { + getPylanceExtensionUri, + ProposeLSStateKeys, + ProposePylanceBanner +} from '../../../client/languageServices/proposeLanguageServerBanner'; +import * as Telemetry from '../../../client/telemetry'; +import { EventName } from '../../../client/telemetry/constants'; + +interface IExperimentLsCombination { + inExperiment: boolean; + lsType: LanguageServerType; + shouldShowBanner: boolean; +} +const testData: IExperimentLsCombination[] = [ + { inExperiment: true, lsType: LanguageServerType.None, shouldShowBanner: true }, + { inExperiment: true, lsType: LanguageServerType.Microsoft, shouldShowBanner: true }, + { inExperiment: true, lsType: LanguageServerType.Node, shouldShowBanner: false }, + { inExperiment: true, lsType: LanguageServerType.Jedi, shouldShowBanner: false }, + { inExperiment: false, lsType: LanguageServerType.None, shouldShowBanner: false }, + { inExperiment: false, lsType: LanguageServerType.Microsoft, shouldShowBanner: false }, + { inExperiment: false, lsType: LanguageServerType.Node, shouldShowBanner: false }, + { inExperiment: false, lsType: LanguageServerType.Jedi, shouldShowBanner: false } +]; + +suite('Propose Pylance Banner', () => { + let config: typemoq.IMock<IConfigurationService>; + let appShell: typemoq.IMock<IApplicationShell>; + let appEnv: typemoq.IMock<IApplicationEnvironment>; + let settings: typemoq.IMock<IPythonSettings>; + let sendTelemetryStub: sinon.SinonStub; + let telemetryEvent: { eventName: EventName; properties: { userAction: string } } | undefined; + + const message = Pylance.proposePylanceMessage(); + const yes = Pylance.tryItNow(); + const no = Common.bannerLabelNo(); + const later = Pylance.remindMeLater(); + + setup(() => { + config = typemoq.Mock.ofType<IConfigurationService>(); + settings = typemoq.Mock.ofType<IPythonSettings>(); + config.setup((x) => x.getSettings(typemoq.It.isAny())).returns(() => settings.object); + appShell = typemoq.Mock.ofType<IApplicationShell>(); + appEnv = typemoq.Mock.ofType<IApplicationEnvironment>(); + appEnv.setup((x) => x.uriScheme).returns(() => 'scheme'); + + sendTelemetryStub = sinon + .stub(Telemetry, 'sendTelemetryEvent') + .callsFake((eventName: EventName, _, properties: { userAction: string }) => { + telemetryEvent = { + eventName, + properties + }; + }); + }); + + teardown(() => { + telemetryEvent = undefined; + sinon.restore(); + Telemetry._resetSharedProperties(); + }); + + testData.forEach((t) => { + test(`${t.inExperiment ? 'In' : 'Not in'} experiment and "python.languageServer": "${t.lsType}" should ${ + t.shouldShowBanner ? 'show' : 'not show' + } banner`, async () => { + settings.setup((x) => x.languageServer).returns(() => t.lsType); + const testBanner = preparePopup(true, appShell.object, appEnv.object, config.object, t.inExperiment, false); + const actual = await testBanner.shouldShowBanner(); + expect(actual).to.be.equal(t.shouldShowBanner, `shouldShowBanner() returned ${actual}`); + }); + }); + testData.forEach((t) => { + test(`When Pylance is installed, banner should not be shown when "python.languageServer": "${t.lsType}"`, async () => { + settings.setup((x) => x.languageServer).returns(() => t.lsType); + const testBanner = preparePopup(true, appShell.object, appEnv.object, config.object, t.inExperiment, true); + const actual = await testBanner.shouldShowBanner(); + expect(actual).to.be.equal(false, `shouldShowBanner() returned ${actual}`); + }); + }); + test('Do not show banner when it is disabled', async () => { + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(message), + typemoq.It.isValue(yes), + typemoq.It.isValue(no), + typemoq.It.isValue(later) + ) + ) + .verifiable(typemoq.Times.never()); + const testBanner = preparePopup(false, appShell.object, appEnv.object, config.object, true, false); + await testBanner.showBanner(); + appShell.verifyAll(); + }); + test('Clicking No should disable the banner', async () => { + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(message), + typemoq.It.isValue(yes), + typemoq.It.isValue(no), + typemoq.It.isValue(later) + ) + ) + .returns(async () => no) + .verifiable(typemoq.Times.once()); + appShell.setup((a) => a.openUrl(getPylanceExtensionUri(appEnv.object))).verifiable(typemoq.Times.never()); + + const testBanner = preparePopup(true, appShell.object, appEnv.object, config.object, true, false); + await testBanner.showBanner(); + + expect(testBanner.enabled).to.be.equal(false, 'Banner should be permanently disabled when user clicked No'); + appShell.verifyAll(); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepEqual(telemetryEvent, { + eventName: EventName.LANGUAGE_SERVER_TRY_PYLANCE, + properties: { userAction: 'no' } + }); + }); + test('Clicking Later should disable banner in session', async () => { + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(message), + typemoq.It.isValue(yes), + typemoq.It.isValue(no), + typemoq.It.isValue(later) + ) + ) + .returns(async () => later) + .verifiable(typemoq.Times.once()); + appShell.setup((a) => a.openUrl(getPylanceExtensionUri(appEnv.object))).verifiable(typemoq.Times.never()); + + const testBanner = preparePopup(true, appShell.object, appEnv.object, config.object, true, false); + await testBanner.showBanner(); + + expect(testBanner.enabled).to.be.equal( + true, + 'Banner should not be permanently disabled when user clicked Later' + ); + appShell.verifyAll(); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepEqual(telemetryEvent, { + eventName: EventName.LANGUAGE_SERVER_TRY_PYLANCE, + properties: { + userAction: 'later' + } + }); + }); + test('Clicking Yes opens the extension marketplace entry', async () => { + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(message), + typemoq.It.isValue(yes), + typemoq.It.isValue(no), + typemoq.It.isValue(later) + ) + ) + .returns(async () => yes) + .verifiable(typemoq.Times.once()); + appShell.setup((a) => a.openUrl(getPylanceExtensionUri(appEnv.object))).verifiable(typemoq.Times.once()); + + const testBanner = preparePopup(true, appShell.object, appEnv.object, config.object, true, false); + await testBanner.showBanner(); + + expect(testBanner.enabled).to.be.equal(false, 'Banner should be permanently disabled after opening store URL'); + appShell.verifyAll(); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepEqual(telemetryEvent, { + eventName: EventName.LANGUAGE_SERVER_TRY_PYLANCE, + properties: { + userAction: 'yes' + } + }); + }); +}); + +function preparePopup( + enabledValue: boolean, + appShell: IApplicationShell, + appEnv: IApplicationEnvironment, + config: IConfigurationService, + inExperiment: boolean, + pylanceInstalled: boolean +): ProposePylanceBanner { + const myfactory = typemoq.Mock.ofType<IPersistentStateFactory>(); + const val = typemoq.Mock.ofType<IPersistentState<boolean>>(); + val.setup((a) => a.updateValue(typemoq.It.isValue(true))).returns(() => { + enabledValue = true; + return Promise.resolve(); + }); + val.setup((a) => a.updateValue(typemoq.It.isValue(false))).returns(() => { + enabledValue = false; + return Promise.resolve(); + }); + val.setup((a) => a.value).returns(() => { + return enabledValue; + }); + myfactory + .setup((a) => + a.createGlobalPersistentState(typemoq.It.isValue(ProposeLSStateKeys.ShowBanner), typemoq.It.isValue(true)) + ) + .returns(() => { + return val.object; + }); + myfactory + .setup((a) => + a.createGlobalPersistentState(typemoq.It.isValue(ProposeLSStateKeys.ShowBanner), typemoq.It.isValue(false)) + ) + .returns(() => { + return val.object; + }); + + const experiments = typemoq.Mock.ofType<IExperimentService>(); + experiments.setup((x) => x.inExperiment(TryPylance.experiment)).returns(() => Promise.resolve(inExperiment)); + + const extensions = typemoq.Mock.ofType<IExtensions>(); + // tslint:disable-next-line: no-any + const extension = typemoq.Mock.ofType<Extension<any>>(); + extensions + .setup((x) => x.getExtension(PYLANCE_EXTENSION_ID)) + .returns(() => (pylanceInstalled ? extension.object : undefined)); + return new ProposePylanceBanner(appShell, appEnv, myfactory.object, config, experiments.object, extensions.object); +} diff --git a/src/test/testing/codeLenses/testFiles.unit.test.ts b/src/test/testing/codeLenses/testFiles.unit.test.ts new file mode 100644 index 000000000000..861263d2b575 --- /dev/null +++ b/src/test/testing/codeLenses/testFiles.unit.test.ts @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { assert, expect } from 'chai'; +import { mock } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { DocumentSymbolProvider, EventEmitter, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { LanguageServerSymbolProvider } from '../../../client/providers/symbolProvider'; +import { TestFileCodeLensProvider } from '../../../client/testing/codeLenses/testFiles'; +import { ITestCollectionStorageService } from '../../../client/testing/common/types'; + +// tslint:disable-next-line: max-func-body-length +suite('Code lenses - Test files', () => { + let testCollectionStorage: typemoq.IMock<ITestCollectionStorageService>; + let workspaceService: typemoq.IMock<IWorkspaceService>; + let fileSystem: typemoq.IMock<IFileSystem>; + let serviceContainer: typemoq.IMock<IServiceContainer>; + let symbolProvider: DocumentSymbolProvider; + let onDidChange: EventEmitter<void>; + let codeLensProvider: TestFileCodeLensProvider; + setup(() => { + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + fileSystem = typemoq.Mock.ofType<IFileSystem>(); + testCollectionStorage = typemoq.Mock.ofType<ITestCollectionStorageService>(); + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + symbolProvider = mock(LanguageServerSymbolProvider); + onDidChange = new EventEmitter<void>(); + serviceContainer + .setup((c) => c.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer.setup((c) => c.get(typemoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + codeLensProvider = new TestFileCodeLensProvider( + onDidChange, + symbolProvider, + testCollectionStorage.object, + serviceContainer.object + ); + }); + + teardown(() => { + onDidChange.dispose(); + }); + + test('Function getTestFileWhichNeedsCodeLens() returns `undefined` if there are no workspace corresponding to document', async () => { + const document = { + uri: Uri.file('path/to/document') + }; + workspaceService + .setup((w) => w.getWorkspaceFolder(document.uri)) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + testCollectionStorage + .setup((w) => w.getTests(typemoq.It.isAny())) + .returns(() => undefined) + .verifiable(typemoq.Times.never()); + const files = codeLensProvider.getTestFileWhichNeedsCodeLens(document as any); + expect(files).to.equal(undefined, 'No files should be returned'); + workspaceService.verifyAll(); + testCollectionStorage.verifyAll(); + }); + + test('Function getTestFileWhichNeedsCodeLens() returns `undefined` if test storage is empty', async () => { + const document = { + uri: Uri.file('path/to/document') + }; + const workspaceUri = Uri.file('path/to/workspace'); + const workspace = { uri: workspaceUri }; + workspaceService + .setup((w) => w.getWorkspaceFolder(document.uri)) + .returns(() => workspace as any) + .verifiable(typemoq.Times.once()); + testCollectionStorage + .setup((w) => w.getTests(workspaceUri)) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + const files = codeLensProvider.getTestFileWhichNeedsCodeLens(document as any); + expect(files).to.equal(undefined, 'No files should be returned'); + workspaceService.verifyAll(); + testCollectionStorage.verifyAll(); + }); + + test('Function getTestFileWhichNeedsCodeLens() returns `undefined` if tests returned from storage does not contain document', async () => { + const document = { + uri: Uri.file('path/to/document5') + }; + const workspaceUri = Uri.file('path/to/workspace'); + const workspace = { uri: workspaceUri }; + const tests = { + testFiles: [ + { + fullPath: 'path/to/document1' + }, + { + fullPath: 'path/to/document2' + } + ] + }; + workspaceService + .setup((w) => w.getWorkspaceFolder(document.uri)) + .returns(() => workspace as any) + .verifiable(typemoq.Times.once()); + testCollectionStorage + .setup((w) => w.getTests(workspaceUri)) + .returns(() => tests as any) + .verifiable(typemoq.Times.once()); + fileSystem.setup((f) => f.arePathsSame('path/to/document1', 'path/to/document5')).returns(() => false); + fileSystem.setup((f) => f.arePathsSame('path/to/document2', 'path/to/document5')).returns(() => false); + const files = codeLensProvider.getTestFileWhichNeedsCodeLens(document as any); + expect(files).to.equal(undefined, 'No files should be returned'); + workspaceService.verifyAll(); + testCollectionStorage.verifyAll(); + }); + + test('Function getTestFileWhichNeedsCodeLens() returns test file if tests returned from storage contains document', async () => { + const document = { + uri: Uri.file('path/to/document2') + }; + const workspaceUri = Uri.file('path/to/workspace'); + const workspace = { uri: workspaceUri }; + const testFile2 = { + fullPath: Uri.file('path/to/document2').fsPath + }; + const tests = { + testFiles: [ + { + fullPath: Uri.file('path/to/document1').fsPath + }, + testFile2 + ] + }; + workspaceService + .setup((w) => w.getWorkspaceFolder(typemoq.It.isValue(document.uri))) + .returns(() => workspace as any) + .verifiable(typemoq.Times.once()); + testCollectionStorage + .setup((w) => w.getTests(typemoq.It.isValue(workspaceUri))) + .returns(() => tests as any) + .verifiable(typemoq.Times.once()); + fileSystem + .setup((f) => f.arePathsSame(Uri.file('/path/to/document1').fsPath, Uri.file('/path/to/document2').fsPath)) + .returns(() => false); + fileSystem + .setup((f) => f.arePathsSame(Uri.file('/path/to/document2').fsPath, Uri.file('/path/to/document2').fsPath)) + .returns(() => true); + const files = codeLensProvider.getTestFileWhichNeedsCodeLens(document as any); + assert.deepEqual(files, testFile2 as any); + workspaceService.verifyAll(); + testCollectionStorage.verifyAll(); + }); +}); diff --git a/src/test/testing/common/argsHelper.unit.test.ts b/src/test/testing/common/argsHelper.unit.test.ts new file mode 100644 index 000000000000..32061dad8a31 --- /dev/null +++ b/src/test/testing/common/argsHelper.unit.test.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any no-conditional-assignment no-increment-decrement no-invalid-this no-require-imports no-var-requires +import { expect, use } from 'chai'; +import { ArgumentsHelper } from '../../../client/testing/common/argumentsHelper'; +import { IArgumentsHelper } from '../../../client/testing/types'; +const assertArrays = require('chai-arrays'); +use(assertArrays); + +suite('Unit Tests - Arguments Helper', () => { + let argsHelper: IArgumentsHelper; + setup(() => { + argsHelper = new ArgumentsHelper(); + }); + + test('Get Option Value', () => { + const args = ['-abc', '1234', 'zys', '--root', 'value']; + const value = argsHelper.getOptionValues(args, '--root'); + expect(value).to.not.be.array(); + expect(value).to.be.deep.equal('value'); + }); + test('Get Option Value when using =', () => { + const args = ['-abc', '1234', 'zys', '--root=value']; + const value = argsHelper.getOptionValues(args, '--root'); + expect(value).to.not.be.array(); + expect(value).to.be.deep.equal('value'); + }); + test('Get Option Values', () => { + const args = ['-abc', '1234', 'zys', '--root', 'value1', '--root', 'value2']; + const values = argsHelper.getOptionValues(args, '--root'); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(2); + expect(values).to.be.deep.equal(['value1', 'value2']); + }); + test('Get Option Values when using =', () => { + const args = ['-abc', '1234', 'zys', '--root=value1', '--root=value2']; + const values = argsHelper.getOptionValues(args, '--root'); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(2); + expect(values).to.be.deep.equal(['value1', 'value2']); + }); + test('Get Positional options', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--no-value-option', 'value2']; + const values = argsHelper.getPositionalArguments(args, ['--value-option', '-abc'], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(1); + expect(values).to.be.deep.equal(['value2']); + }); + test('Get multiple Positional options', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--no-value-option', 'value2', 'value3']; + const values = argsHelper.getPositionalArguments(args, ['--value-option', '-abc'], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(2); + expect(values).to.be.deep.equal(['value2', 'value3']); + }); + test('Get multiple Positional options and inline values', () => { + const args = ['-abc=1234', '--value-option=value1', '--no-value-option', 'value2', 'value3']; + const values = argsHelper.getPositionalArguments(args, ['--value-option', '-abc'], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(2); + expect(values).to.be.deep.equal(['value2', 'value3']); + }); + test('Get Positional options with trailing value option', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--value-option', 'value2', 'value3']; + const values = argsHelper.getPositionalArguments(args, ['--value-option', '-abc'], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(1); + expect(values).to.be.deep.equal(['value3']); + }); + test('Get multiple Positional options with trailing value option', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--value-option', 'value2', 'value3', '4']; + const values = argsHelper.getPositionalArguments(args, ['--value-option', '-abc'], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(2); + expect(values).to.be.deep.equal(['value3', '4']); + }); + test('Get Positional options with unknown args', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--value-option', 'value2', 'value3', '4']; + const values = argsHelper.getPositionalArguments(args, ['-abc'], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(4); + expect(values).to.be.deep.equal(['value1', 'value2', 'value3', '4']); + }); + test('Get Positional options with no options parameters', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--value-option', 'value2', 'value3', '4']; + const values = argsHelper.getPositionalArguments(args); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(5); + expect(values).to.be.deep.equal(['1234', 'value1', 'value2', 'value3', '4']); + expect(values).to.be.deep.equal(argsHelper.getPositionalArguments(args, [], [])); + }); + test('Filter to remove those with values', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--value-option', 'value2', 'value3', '4']; + const values = argsHelper.filterArguments(args, ['--value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(4); + expect(values).to.be.deep.equal(['-abc', '1234', 'value3', '4']); + }); + test('Filter to remove those without values', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--no-value-option', 'value2', 'value3', '4']; + const values = argsHelper.filterArguments(args, [], ['--no-value-option']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(7); + expect(values).to.be.deep.equal(['-abc', '1234', '--value-option', 'value1', 'value2', 'value3', '4']); + }); + test('Filter to remove those with and without values', () => { + const args = ['-abc', '1234', '--value-option', 'value1', '--value-option', 'value2', 'value3', '4']; + const values = argsHelper.filterArguments(args, ['--value-option'], ['-abc']); + expect(values).to.be.array(); + expect(values).to.be.lengthOf(3); + expect(values).to.be.deep.equal(['1234', 'value3', '4']); + }); +}); diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts new file mode 100644 index 000000000000..c1782b7c8027 --- /dev/null +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -0,0 +1,595 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { CancellationTokenSource, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; +import { IInvalidPythonPathInDebuggerService } from '../../../client/application/diagnostics/types'; +import { + IApplicationShell, + IDebugService, + IDocumentManager, + IWorkspaceService +} from '../../../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import '../../../client/common/extensions'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { IConfigurationService, IPythonSettings, ITestingSettings } from '../../../client/common/types'; +import { DebuggerTypeName } from '../../../client/debugger/constants'; +import { IDebugEnvironmentVariablesService } from '../../../client/debugger/extension/configuration/resolvers/helper'; +import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; +import { DebugOptions } from '../../../client/debugger/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { DebugLauncher } from '../../../client/testing/common/debugLauncher'; +import { LaunchOptions, TestProvider } from '../../../client/testing/common/types'; +import { isOs, OSType } from '../../common'; + +use(chaiAsPromised); + +// tslint:disable-next-line:max-func-body-length no-any +suite('Unit Tests - Debug Launcher', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let unitTestSettings: TypeMoq.IMock<ITestingSettings>; + let debugLauncher: DebugLauncher; + let debugService: TypeMoq.IMock<IDebugService>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let platformService: TypeMoq.IMock<IPlatformService>; + let filesystem: TypeMoq.IMock<IFileSystem>; + let settings: TypeMoq.IMock<IPythonSettings>; + let debugEnvHelper: TypeMoq.IMock<IDebugEnvironmentVariablesService>; + let hasWorkspaceFolders: boolean; + setup(async () => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(undefined, TypeMoq.MockBehavior.Strict); + const configService = TypeMoq.Mock.ofType<IConfigurationService>(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + + debugService = TypeMoq.Mock.ofType<IDebugService>(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDebugService))).returns(() => debugService.object); + + hasWorkspaceFolders = true; + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(undefined, TypeMoq.MockBehavior.Strict); + workspaceService.setup((u) => u.hasWorkspaceFolders).returns(() => hasWorkspaceFolders); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + + platformService = TypeMoq.Mock.ofType<IPlatformService>(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + + filesystem = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => filesystem.object); + + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(undefined, TypeMoq.MockBehavior.Strict); + appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + + settings = TypeMoq.Mock.ofType<IPythonSettings>(undefined, TypeMoq.MockBehavior.Strict); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + + unitTestSettings = TypeMoq.Mock.ofType<ITestingSettings>(undefined, TypeMoq.MockBehavior.Strict); + settings.setup((p) => p.testing).returns(() => unitTestSettings.object); + + debugEnvHelper = TypeMoq.Mock.ofType<IDebugEnvironmentVariablesService>(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDebugEnvironmentVariablesService))) + .returns(() => debugEnvHelper.object); + + debugLauncher = new DebugLauncher(serviceContainer.object, getNewResolver(configService.object)); + }); + function getNewResolver(configService: IConfigurationService) { + const validator = TypeMoq.Mock.ofType<IInvalidPythonPathInDebuggerService>( + undefined, + TypeMoq.MockBehavior.Strict + ); + validator + .setup((v) => v.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)); + return new LaunchConfigurationResolver( + workspaceService.object, + TypeMoq.Mock.ofType<IDocumentManager>(undefined, TypeMoq.MockBehavior.Strict).object, + validator.object, + platformService.object, + configService, + debugEnvHelper.object + ); + } + function setupDebugManager( + workspaceFolder: WorkspaceFolder, + expected: DebugConfiguration, + testProvider: TestProvider + ) { + platformService.setup((p) => p.isWindows).returns(() => /^win/.test(process.platform)); + settings.setup((p) => p.pythonPath).returns(() => 'python'); + settings.setup((p) => p.envFile).returns(() => __filename); + const args = expected.args; + const debugArgs = testProvider === 'unittest' ? args.filter((item: string) => item !== '--debug') : args; + expected.args = debugArgs; + + debugEnvHelper + .setup((d) => d.getEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(expected.env)); + + //debugService.setup(d => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected))) + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected))) + .returns((_wspc: WorkspaceFolder, _expectedParam: DebugConfiguration) => { + return Promise.resolve(undefined as any); + }) + .verifiable(TypeMoq.Times.once()); + } + function createWorkspaceFolder(folderPath: string): WorkspaceFolder { + return { + index: 0, + name: path.basename(folderPath), + uri: Uri.file(folderPath) + }; + } + function getTestLauncherScript(testProvider: TestProvider) { + switch (testProvider) { + case 'unittest': { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'visualstudio_py_testlauncher.py'); + } + case 'pytest': + case 'nosetest': { + return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testlauncher.py'); + } + default: { + throw new Error(`Unknown test provider '${testProvider}'`); + } + } + } + function getDefaultDebugConfig(): DebugConfiguration { + return { + name: 'Debug Unit Test', + type: DebuggerTypeName, + request: 'launch', + console: 'internalConsole', + env: {}, + envFile: __filename, + stopOnEntry: false, + showReturnValue: true, + redirectOutput: true, + debugStdLib: false, + subProcess: true + }; + } + function setupSuccess( + options: LaunchOptions, + testProvider: TestProvider, + expected?: DebugConfiguration, + debugConfigs?: string | DebugConfiguration[] + ) { + const testLaunchScript = getTestLauncherScript(testProvider); + + const workspaceFolders = [createWorkspaceFolder(options.cwd), createWorkspaceFolder('five/six/seven')]; + workspaceService.setup((u) => u.workspaceFolders).returns(() => workspaceFolders); + workspaceService.setup((u) => u.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolders[0]); + + if (!debugConfigs) { + filesystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + } else { + filesystem.setup((fs) => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + if (typeof debugConfigs !== 'string') { + debugConfigs = JSON.stringify({ + version: '0.1.0', + configurations: debugConfigs + }); + } + filesystem + .setup((fs) => fs.readFile(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(debugConfigs as string)); + } + + if (!expected) { + expected = getDefaultDebugConfig(); + } + expected.rules = [{ path: path.join(EXTENSION_ROOT_DIR, 'pythonFiles'), include: false }]; + if (testProvider === 'unittest') { + expected.program = testLaunchScript; + expected.args = options.args; + } else { + expected.program = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'pyvsc-run-isolated.py'); + expected.args = [testLaunchScript, ...options.args]; + } + + if (!expected.cwd) { + expected.cwd = workspaceFolders[0].uri.fsPath; + } + + // added by LaunchConfigurationResolver: + if (!expected.pythonPath) { + expected.pythonPath = 'python'; + } + expected.workspaceFolder = workspaceFolders[0].uri.fsPath; + expected.debugOptions = []; + if (expected.justMyCode === undefined) { + // Populate justMyCode using debugStdLib + expected.justMyCode = !expected.debugStdLib; + } + if (!expected.justMyCode) { + expected.debugOptions.push(DebugOptions.DebugStdLib); + } + if (expected.stopOnEntry) { + expected.debugOptions.push(DebugOptions.StopOnEntry); + } + if (expected.showReturnValue) { + expected.debugOptions.push(DebugOptions.ShowReturnValue); + } + if (expected.redirectOutput) { + expected.debugOptions.push(DebugOptions.RedirectOutput); + } + if (expected.subProcess) { + expected.debugOptions.push(DebugOptions.SubProcess); + } + if (isOs(OSType.Windows)) { + expected.debugOptions.push(DebugOptions.FixFilePathCase); + } + + setupDebugManager(workspaceFolders[0], expected, testProvider); + } + + const testProviders: TestProvider[] = ['nosetest', 'pytest', 'unittest']; + // tslint:disable-next-line:max-func-body-length + testProviders.forEach((testProvider) => { + const testTitleSuffix = `(Test Framework '${testProvider}')`; + + test(`Must launch debugger ${testTitleSuffix}`, async () => { + const options = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider + }; + setupSuccess(options, testProvider); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + test(`Must launch debugger with arguments ${testTitleSuffix}`, async () => { + const options = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py', '--debug', '1'], + testProvider + }; + setupSuccess(options, testProvider); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + test(`Must not launch debugger if cancelled ${testTitleSuffix}`, async () => { + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve(undefined as any); + }) + .verifiable(TypeMoq.Times.never()); + + const cancellationToken = new CancellationTokenSource(); + cancellationToken.cancel(); + const token = cancellationToken.token; + const options: LaunchOptions = { cwd: '', args: [], token, testProvider }; + + await expect(debugLauncher.launchDebugger(options)).to.be.eventually.equal(undefined, 'not undefined'); + + debugService.verifyAll(); + }); + test(`Must throw an exception if there are no workspaces ${testTitleSuffix}`, async () => { + hasWorkspaceFolders = false; + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined as any)) + .verifiable(TypeMoq.Times.never()); + + const options: LaunchOptions = { cwd: '', args: [], testProvider }; + + await expect(debugLauncher.launchDebugger(options)).to.eventually.rejectedWith('Please open a workspace'); + + debugService.verifyAll(); + }); + }); + + test('Tries launch.json first', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest' + }; + const expected = getDefaultDebugConfig(); + expected.name = 'spam'; + setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: DebuggerTypeName, request: 'test' }]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Full debug config', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest' + }; + const expected = { + name: 'my tests', + type: DebuggerTypeName, + request: 'launch', + pythonPath: 'some/dir/bin/py3', + stopOnEntry: true, + showReturnValue: true, + console: 'integratedTerminal', + cwd: 'some/dir', + env: { + SPAM: 'EGGS' + }, + envFile: 'some/dir/.env', + redirectOutput: false, + debugStdLib: true, + justMyCode: false, + // added by LaunchConfigurationResolver: + internalConsoleOptions: 'neverOpen', + subProcess: true + }; + setupSuccess(options, 'unittest', expected, [ + { + name: 'my tests', + type: DebuggerTypeName, + request: 'test', + pythonPath: expected.pythonPath, + stopOnEntry: expected.stopOnEntry, + showReturnValue: expected.showReturnValue, + console: expected.console, + cwd: expected.cwd, + env: expected.env, + envFile: expected.envFile, + redirectOutput: expected.redirectOutput, + debugStdLib: expected.debugStdLib, + justMyCode: undefined + } + ]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Uses first entry', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest' + }; + const expected = getDefaultDebugConfig(); + expected.name = 'spam1'; + setupSuccess(options, 'unittest', expected, [ + { name: 'spam1', type: DebuggerTypeName, request: 'test' }, + { name: 'spam2', type: DebuggerTypeName, request: 'test' }, + { name: 'spam3', type: DebuggerTypeName, request: 'test' } + ]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Handles bad JSON', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest' + }; + const expected = getDefaultDebugConfig(); + setupSuccess(options, 'unittest', expected, ']'); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + const malformedFiles = [ + '// test 1', + '// test 2 \n\ + { \n\ + "name": "spam", \n\ + "type": "python", \n\ + "request": "test" \n\ + } \n\ + ', + '// test 3 \n\ + [ \n\ + { \n\ + "name": "spam", \n\ + "type": "python", \n\ + "request": "test" \n\ + } \n\ + ] \n\ + ', + '// test 4 \n\ + { \n\ + "configurations": [ \n\ + { \n\ + "name": "spam", \n\ + "type": "python", \n\ + "request": "test" \n\ + } \n\ + ] \n\ + } \n\ + ' + ]; + for (const text of malformedFiles) { + const testID = text.split('\n')[0].substring(3).trim(); + test(`Handles malformed launch.json - ${testID}`, async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest' + }; + const expected = getDefaultDebugConfig(); + setupSuccess(options, 'unittest', expected, text); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + } + + test('Handles bad debug config items', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest' + }; + const expected = getDefaultDebugConfig(); + // tslint:disable:no-object-literal-type-assertion + setupSuccess(options, 'unittest', expected, [ + {} as DebugConfiguration, + { name: 'spam1' } as DebugConfiguration, + { name: 'spam2', type: DebuggerTypeName } as DebugConfiguration, + { name: 'spam3', request: 'test' } as DebugConfiguration, + { type: DebuggerTypeName } as DebugConfiguration, + { type: DebuggerTypeName, request: 'test' } as DebugConfiguration, + { request: 'test' } as DebugConfiguration + ]); + // tslint:enable:no-object-literal-type-assertion + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Handles non-python debug configs', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest' + }; + const expected = getDefaultDebugConfig(); + setupSuccess(options, 'unittest', expected, [{ name: 'foo', type: 'other', request: 'bar' }]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Handles bogus python debug configs', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest' + }; + const expected = getDefaultDebugConfig(); + setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: DebuggerTypeName, request: 'bogus' }]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Handles non-test debug config', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest' + }; + const expected = getDefaultDebugConfig(); + setupSuccess(options, 'unittest', expected, [ + { name: 'spam', type: DebuggerTypeName, request: 'launch' }, + { name: 'spam', type: DebuggerTypeName, request: 'attach' } + ]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Handles mixed debug config', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest' + }; + const expected = getDefaultDebugConfig(); + expected.name = 'spam2'; + setupSuccess(options, 'unittest', expected, [ + { name: 'foo1', type: 'other', request: 'bar' }, + { name: 'foo2', type: 'other', request: 'bar' }, + { name: 'spam1', type: DebuggerTypeName, request: 'launch' }, + { name: 'spam2', type: DebuggerTypeName, request: 'test' }, + { name: 'spam3', type: DebuggerTypeName, request: 'attach' }, + { name: 'xyz', type: 'another', request: 'abc' } + ]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Handles comments', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest' + }; + const expected = getDefaultDebugConfig(); + expected.name = 'spam'; + expected.stopOnEntry = true; + setupSuccess( + options, + 'unittest', + expected, + ' \n\ + { \n\ + "version": "0.1.0", \n\ + "configurations": [ \n\ + // my thing \n\ + { \n\ + // "test" debug config \n\ + "name": "spam", /* non-empty */ \n\ + "type": "python", /* must be "python" */ \n\ + "request": "test", /* must be "test" */ \n\ + // extra stuff here: \n\ + "stopOnEntry": true \n\ + } \n\ + ] \n\ + } \n\ + ' + ); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + test('Ensure trailing commands in JSON are handled', async () => { + const workspaceFolder = { name: 'abc', index: 0, uri: Uri.file(__filename) }; + const filename = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); + const jsonc = '{"version":"1234", "configurations":[1,2,],}'; + filesystem.setup((fs) => fs.fileExists(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(true)); + filesystem.setup((fs) => fs.readFile(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(jsonc)); + + const configs = await debugLauncher.readAllDebugConfigs(workspaceFolder); + + expect(configs).to.be.deep.equal([1, 2]); + }); + test('Ensure empty configuration is returned when launch.json cannot be parsed', async () => { + const workspaceFolder = { name: 'abc', index: 0, uri: Uri.file(__filename) }; + const filename = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); + const jsonc = '{"version":"1234"'; + + filesystem.setup((fs) => fs.fileExists(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(true)); + filesystem.setup((fs) => fs.readFile(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(jsonc)); + + const configs = await debugLauncher.readAllDebugConfigs(workspaceFolder); + + expect(configs).to.be.deep.equal([]); + }); +}); diff --git a/src/test/testing/common/managers/baseTestManager.unit.test.ts b/src/test/testing/common/managers/baseTestManager.unit.test.ts new file mode 100644 index 000000000000..47adcb9809c8 --- /dev/null +++ b/src/test/testing/common/managers/baseTestManager.unit.test.ts @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Disposable, OutputChannel, Uri } from 'vscode'; +import { CommandManager } from '../../../../client/common/application/commandManager'; +import { ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { PythonSettings } from '../../../../client/common/configSettings'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { ModuleNotInstalledError } from '../../../../client/common/errors/moduleNotInstalledError'; +import { ProductInstaller } from '../../../../client/common/installer/productInstaller'; +import { + IConfigurationService, + IDisposableRegistry, + IInstaller, + IOutputChannel, + IPythonSettings +} from '../../../../client/common/types'; +import { ServiceContainer } from '../../../../client/ioc/container'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { CommandSource, TEST_OUTPUT_CHANNEL } from '../../../../client/testing/common/constants'; +import { TestCollectionStorageService } from '../../../../client/testing/common/services/storageService'; +import { TestResultsService } from '../../../../client/testing/common/services/testResultsService'; +import { TestsStatusUpdaterService } from '../../../../client/testing/common/services/testsStatusService'; +import { UnitTestDiagnosticService } from '../../../../client/testing/common/services/unitTestDiagnosticService'; +import { TestsHelper } from '../../../../client/testing/common/testUtils'; +import { + ITestCollectionStorageService, + ITestDiscoveryService, + ITestManager, + ITestMessageService, + ITestResultsService, + ITestsHelper, + ITestsStatusUpdaterService +} from '../../../../client/testing/common/types'; +import { TestManager as NoseTestManager } from '../../../../client/testing/nosetest/main'; +import { TestManager as PyTestTestManager } from '../../../../client/testing/pytest/main'; +import { ArgumentsService } from '../../../../client/testing/pytest/services/argsService'; +import { TestDiscoveryService } from '../../../../client/testing/pytest/services/discoveryService'; +import { TestMessageService } from '../../../../client/testing/pytest/services/testMessageService'; +import { IArgumentsService, ITestDiagnosticService, ITestManagerRunner } from '../../../../client/testing/types'; +import { TestManager as UnitTestTestManager } from '../../../../client/testing/unittest/main'; +import { TestManagerRunner } from '../../../../client/testing/unittest/runner'; +import { noop } from '../../../core'; +import { MockOutputChannel } from '../../../mockClasses'; + +// tslint:disable: max-func-body-length +suite('Unit Tests - Base Test Manager', () => { + [ + { name: 'nose', class: NoseTestManager }, + { name: 'pytest', class: PyTestTestManager }, + { name: 'unittest', class: UnitTestTestManager } + ].forEach((item) => { + suite(item.name, () => { + let testManager: ITestManager; + const workspaceFolder = Uri.file(__dirname); + let serviceContainer: IServiceContainer; + let configService: IConfigurationService; + let settings: IPythonSettings; + let outputChannel: IOutputChannel; + let storageService: ITestCollectionStorageService; + let resultsService: ITestResultsService; + let workspaceService: IWorkspaceService; + let diagnosticService: ITestDiagnosticService; + let statusUpdater: ITestsStatusUpdaterService; + let commandManager: ICommandManager; + let testDiscoveryService: ITestDiscoveryService; + let installer: IInstaller; + const sandbox = sinon.createSandbox(); + suiteTeardown(() => sandbox.restore()); + setup(() => { + serviceContainer = mock(ServiceContainer); + settings = mock(PythonSettings); + configService = mock(ConfigurationService); + outputChannel = mock(MockOutputChannel); + storageService = mock(TestCollectionStorageService); + resultsService = mock(TestResultsService); + workspaceService = mock(WorkspaceService); + diagnosticService = mock(UnitTestDiagnosticService); + statusUpdater = mock(TestsStatusUpdaterService); + commandManager = mock(CommandManager); + testDiscoveryService = mock(TestDiscoveryService); + installer = mock(ProductInstaller); + + const argsService = mock(ArgumentsService); + const testsHelper = mock(TestsHelper); + const runner = mock(TestManagerRunner); + const messageService = mock(TestMessageService); + + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn( + instance(configService) + ); + when(serviceContainer.get<Disposable[]>(IDisposableRegistry)).thenReturn([]); + when(serviceContainer.get<OutputChannel>(IOutputChannel, TEST_OUTPUT_CHANNEL)).thenReturn( + instance(outputChannel) + ); + when(serviceContainer.get<ITestCollectionStorageService>(ITestCollectionStorageService)).thenReturn( + instance(storageService) + ); + when(serviceContainer.get<ITestResultsService>(ITestResultsService)).thenReturn( + instance(resultsService) + ); + when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspaceService)); + when(serviceContainer.get<ITestDiagnosticService>(ITestDiagnosticService)).thenReturn( + instance(diagnosticService) + ); + when(serviceContainer.get<ITestsStatusUpdaterService>(ITestsStatusUpdaterService)).thenReturn( + instance(statusUpdater) + ); + when(serviceContainer.get<ICommandManager>(ICommandManager)).thenReturn(instance(commandManager)); + + when(serviceContainer.get<IArgumentsService>(IArgumentsService, anything())).thenReturn( + instance(argsService) + ); + when(serviceContainer.get<ITestsHelper>(ITestsHelper)).thenReturn(instance(testsHelper)); + when(serviceContainer.get<ITestManagerRunner>(ITestManagerRunner, anything())).thenReturn( + instance(runner) + ); + when(serviceContainer.get<ITestMessageService>(ITestMessageService, anything())).thenReturn( + instance(messageService) + ); + + when(serviceContainer.get<ITestDiscoveryService>(ITestDiscoveryService, anything())).thenReturn( + instance(testDiscoveryService) + ); + when(serviceContainer.get<IInstaller>(IInstaller)).thenReturn(instance(installer)); + + when(configService.getSettings(anything())).thenReturn(instance(settings)); + when(commandManager.executeCommand(anything(), anything(), anything())).thenResolve(); + + sandbox.restore(); + sandbox.stub(item.class.prototype, 'getDiscoveryOptions').callsFake(() => ({} as any)); + + testManager = new item.class(workspaceFolder, workspaceFolder.fsPath, instance(serviceContainer)); + }); + + test('Discovering tests should display test manager', async () => { + // We don't care about failures in running code + // Just test our expectations, ignore everything else. + await testManager.discoverTests(CommandSource.auto, true, true, true).catch(noop); + + verify(commandManager.executeCommand('setContext', 'testsDiscovered', true)).once(); + }); + test('When failing to discover tests prompt to install test framework', async function () { + if (item.name === 'unittest') { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + when(testDiscoveryService.discoverTests(anything())).thenReject(new ModuleNotInstalledError('Kaboom')); + when(installer.isInstalled(anything(), anything())).thenResolve(false); + when(installer.promptToInstall(anything(), anything())).thenResolve(); + + // We don't care about failures in running code + // Just test our expectations, ignore everything else. + await testManager.discoverTests(CommandSource.ui, true, false, true).catch(noop); + + verify(installer.isInstalled(anything(), anything())).once(); + verify(installer.promptToInstall(anything(), anything())).once(); + }); + test('When failing to discover tests do not prompt to install test framework', async function () { + if (item.name === 'unittest') { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + when(testDiscoveryService.discoverTests(anything())).thenReject(new Error('Kaboom')); + when(installer.isInstalled(anything(), anything())).thenResolve(false); + when(installer.promptToInstall(anything(), anything())).thenResolve(); + + // We don't care about failures in running code + // Just test our expectations, ignore everything else. + await testManager.discoverTests(CommandSource.ui, true, false, true).catch(noop); + + verify(installer.isInstalled(anything(), anything())).never(); + verify(installer.promptToInstall(anything(), anything())).never(); + }); + test('When failing to discover tests do not prompt to install test framework if installed', async function () { + if (item.name === 'unittest') { + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + + when(testDiscoveryService.discoverTests(anything())).thenReject(new ModuleNotInstalledError('Kaboom')); + when(installer.isInstalled(anything(), anything())).thenResolve(true); + when(installer.promptToInstall(anything(), anything())).thenResolve(); + + // We don't care about failures in running code + // Just test our expectations, ignore everything else. + await testManager.discoverTests(CommandSource.ui, true, false, true).catch(noop); + + verify(installer.isInstalled(anything(), anything())).once(); + verify(installer.promptToInstall(anything(), anything())).never(); + }); + }); + }); +}); diff --git a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts new file mode 100644 index 000000000000..7baa68ef07e4 --- /dev/null +++ b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import * as TypeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; +import { IInstaller, IOutputChannel, Product } from '../../../../client/common/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { TEST_OUTPUT_CHANNEL, UNIT_TEST_PRODUCTS } from '../../../../client/testing/common/constants'; +import { TestConfigurationManager } from '../../../../client/testing/common/managers/testConfigurationManager'; +import { UnitTestProduct } from '../../../../client/testing/common/types'; +import { ITestConfigSettingsService } from '../../../../client/testing/types'; + +class MockTestConfigurationManager extends TestConfigurationManager { + public requiresUserToConfigure(_wkspace: Uri): Promise<boolean> { + throw new Error('Method not implemented.'); + } + public configure(_wkspace: any): Promise<any> { + throw new Error('Method not implemented.'); + } +} + +suite('Unit Test Configuration Manager (unit)', () => { + UNIT_TEST_PRODUCTS.forEach((product) => { + const prods = getNamesAndValues(Product); + const productName = prods.filter((item) => item.value === product)[0]; + suite(productName.name, () => { + const workspaceUri = Uri.file(__dirname); + let manager: TestConfigurationManager; + let configService: TypeMoq.IMock<ITestConfigSettingsService>; + + setup(() => { + configService = TypeMoq.Mock.ofType<ITestConfigSettingsService>(); + const outputChannel = TypeMoq.Mock.ofType<OutputChannel>().object; + const installer = TypeMoq.Mock.ofType<IInstaller>().object; + const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(TEST_OUTPUT_CHANNEL))) + .returns(() => outputChannel); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ITestConfigSettingsService))) + .returns(() => configService.object); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(IInstaller))).returns(() => installer); + manager = new MockTestConfigurationManager( + workspaceUri, + product as UnitTestProduct, + serviceContainer.object + ); + }); + + test('Enabling a test product shoud disable other products', async () => { + UNIT_TEST_PRODUCTS.filter((item) => item !== product).forEach((productToDisable) => { + configService + .setup((c) => c.disable(TypeMoq.It.isValue(workspaceUri), TypeMoq.It.isValue(productToDisable))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + }); + configService + .setup((c) => c.enable(TypeMoq.It.isValue(workspaceUri), TypeMoq.It.isValue(product))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + await manager.enable(); + configService.verifyAll(); + }); + }); + }); +}); diff --git a/src/test/testing/common/services/configSettingService.unit.test.ts b/src/test/testing/common/services/configSettingService.unit.test.ts new file mode 100644 index 000000000000..09e947efb25f --- /dev/null +++ b/src/test/testing/common/services/configSettingService.unit.test.ts @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect, use } from 'chai'; +import * as chaiPromise from 'chai-as-promised'; +import * as typeMoq from 'typemoq'; +import { Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { Product } from '../../../../client/common/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { UNIT_TEST_PRODUCTS } from '../../../../client/testing/common/constants'; +import { + BufferedTestConfigSettingsService, + TestConfigSettingsService +} from '../../../../client/testing/common/services/configSettingService'; +import { UnitTestProduct } from '../../../../client/testing/common/types'; +import { ITestConfigSettingsService } from '../../../../client/testing/types'; + +use(chaiPromise); + +const updateMethods: (keyof Omit<ITestConfigSettingsService, 'getTestEnablingSetting'>)[] = [ + 'updateTestArgs', + 'disable', + 'enable' +]; + +suite('Unit Tests - ConfigSettingsService', () => { + UNIT_TEST_PRODUCTS.forEach((product) => { + const prods = getNamesAndValues(Product); + const productName = prods.filter((item) => item.value === product)[0]; + const workspaceUri = Uri.file(__filename); + updateMethods.forEach((updateMethod) => { + suite(`Test '${updateMethod}' method with ${productName.name}`, () => { + let testConfigSettingsService: ITestConfigSettingsService; + let workspaceService: typeMoq.IMock<IWorkspaceService>; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + workspaceService = typeMoq.Mock.ofType<IWorkspaceService>(); + + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + testConfigSettingsService = new TestConfigSettingsService(serviceContainer.object); + }); + function getTestArgSetting(prod: UnitTestProduct) { + switch (prod) { + case Product.unittest: + return 'testing.unittestArgs'; + case Product.pytest: + return 'testing.pytestArgs'; + case Product.nosetest: + return 'testing.nosetestArgs'; + default: + throw new Error('Invalid Test Product'); + } + } + function getTestEnablingSetting(prod: UnitTestProduct) { + switch (prod) { + case Product.unittest: + return 'testing.unittestEnabled'; + case Product.pytest: + return 'testing.pytestEnabled'; + case Product.nosetest: + return 'testing.nosetestsEnabled'; + default: + throw new Error('Invalid Test Product'); + } + } + function getExpectedValueAndSettings(): { configValue: any; configName: string } { + switch (updateMethod) { + case 'disable': { + return { configValue: false, configName: getTestEnablingSetting(product) }; + } + case 'enable': { + return { configValue: true, configName: getTestEnablingSetting(product) }; + } + case 'updateTestArgs': { + return { configValue: ['one', 'two', 'three'], configName: getTestArgSetting(product) }; + } + default: { + throw new Error('Invalid Method'); + } + } + } + test('Update Test Arguments with workspace Uri without workspaces', async () => { + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => false) + .verifiable(typeMoq.Times.atLeastOnce()); + + const pythonConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'))) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + + pythonConfig + .setup((p) => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Update Test Arguments with workspace Uri with one workspace', async () => { + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typeMoq.Times.atLeastOnce()); + + const workspaceFolder = typeMoq.Mock.ofType<WorkspaceFolder>(); + workspaceFolder + .setup((w) => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + + const pythonConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((w) => + w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri)) + ) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + pythonConfig + .setup((p) => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Update Test Arguments with workspace Uri with more than one workspace and uri belongs to a workspace', async () => { + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typeMoq.Times.atLeastOnce()); + + const workspaceFolder = typeMoq.Mock.ofType<WorkspaceFolder>(); + workspaceFolder + .setup((w) => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder.object, workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) + .returns(() => workspaceFolder.object) + .verifiable(typeMoq.Times.once()); + + const pythonConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((w) => + w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri)) + ) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + pythonConfig + .setup((p) => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Expect an exception when updating Test Arguments with workspace Uri with more than one workspace and uri does not belong to a workspace', async () => { + workspaceService + .setup((w) => w.hasWorkspaceFolders) + .returns(() => true) + .verifiable(typeMoq.Times.atLeastOnce()); + + const workspaceFolder = typeMoq.Mock.ofType<WorkspaceFolder>(); + workspaceFolder + .setup((w) => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder.object, workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + + const { configValue } = getExpectedValueAndSettings(); + + const promise = testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + expect(promise).to.eventually.rejectedWith(); + workspaceService.verifyAll(); + }); + }); + }); + }); +}); + +suite('Unit Tests - BufferedTestConfigSettingsService', () => { + test('config changes are pushed when apply() is called', async () => { + const testDir = '/my/project'; + const newArgs: string[] = ['-x', '--spam=42']; + const cfg = typeMoq.Mock.ofType<ITestConfigSettingsService>(undefined, typeMoq.MockBehavior.Strict); + cfg.setup((c) => + c.updateTestArgs( + typeMoq.It.isValue(testDir), + typeMoq.It.isValue(Product.pytest), + typeMoq.It.isValue(newArgs) + ) + ) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + cfg.setup((c) => c.disable(typeMoq.It.isValue(testDir), typeMoq.It.isValue(Product.unittest))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + cfg.setup((c) => c.disable(typeMoq.It.isValue(testDir), typeMoq.It.isValue(Product.nosetest))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + cfg.setup((c) => c.enable(typeMoq.It.isValue(testDir), typeMoq.It.isValue(Product.pytest))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + const delayed = new BufferedTestConfigSettingsService(); + await delayed.updateTestArgs(testDir, Product.pytest, newArgs); + await delayed.disable(testDir, Product.unittest); + await delayed.disable(testDir, Product.nosetest); + await delayed.enable(testDir, Product.pytest); + await delayed.apply(cfg.object); + + // Ideally we would verify that the ops were applied in their + // original order. Unfortunately, the version of TypeMoq we're + // using does not give us that option. + cfg.verifyAll(); + }); + + test('applied changes are cleared', async () => { + const cfg = typeMoq.Mock.ofType<ITestConfigSettingsService>(undefined, typeMoq.MockBehavior.Strict); + cfg.setup((c) => c.enable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + const delayed = new BufferedTestConfigSettingsService(); + await delayed.enable('/my/project', Product.pytest); + await delayed.apply(cfg.object); + await delayed.apply(cfg.object); + + cfg.verifyAll(); + }); +}); diff --git a/src/test/testing/common/services/contextService.unit.test.ts b/src/test/testing/common/services/contextService.unit.test.ts new file mode 100644 index 000000000000..357a46dcdae1 --- /dev/null +++ b/src/test/testing/common/services/contextService.unit.test.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { CommandManager } from '../../../../client/common/application/commandManager'; +import { ICommandManager } from '../../../../client/common/application/types'; +import { TestContextService } from '../../../../client/testing/common/services/contextService'; +import { TestCollectionStorageService } from '../../../../client/testing/common/services/storageService'; +import { + ITestCollectionStorageService, + ITestContextService, + TestStatus +} from '../../../../client/testing/common/types'; +import { UnitTestManagementService } from '../../../../client/testing/main'; +import { ITestManagementService, WorkspaceTestStatus } from '../../../../client/testing/types'; + +// tslint:disable:no-any max-func-body-length +suite('Unit Tests - Context Service', () => { + let cmdManager: ICommandManager; + let contextService: ITestContextService; + let storage: ITestCollectionStorageService; + let mgr: ITestManagementService; + const workspaceUri = Uri.file(__filename); + type StatusChangeHandler = (status: WorkspaceTestStatus) => Promise<void>; + setup(() => { + cmdManager = mock(CommandManager); + storage = mock(TestCollectionStorageService); + mgr = mock(UnitTestManagementService); + contextService = new TestContextService(instance(storage), instance(mgr), instance(cmdManager)); + }); + + test('register will add event handler', () => { + let invoked = false; + const fn = () => (invoked = true); + when(mgr.onDidStatusChange).thenReturn(fn as any); + + contextService.register(); + + assert.equal(invoked, true); + }); + test('Status change without tests does not update hasFailedTests', async () => { + let handler!: StatusChangeHandler; + const fn = (cb: StatusChangeHandler) => (handler = cb); + when(mgr.onDidStatusChange).thenReturn(fn as any); + when(storage.getTests(workspaceUri)).thenReturn(); + contextService.register(); + + await handler.bind(contextService)({ status: TestStatus.Discovering, workspace: workspaceUri }); + + verify(cmdManager.executeCommand('setContext', 'hasFailedTests', anything())).never(); + }); + test('Status change without a summary does not update hasFailedTests', async () => { + let handler!: StatusChangeHandler; + const fn = (cb: StatusChangeHandler) => (handler = cb); + when(mgr.onDidStatusChange).thenReturn(fn as any); + when(storage.getTests(workspaceUri)).thenReturn({} as any); + contextService.register(); + + await handler.bind(contextService)({ status: TestStatus.Discovering, workspace: workspaceUri }); + + verify(cmdManager.executeCommand('setContext', 'hasFailedTests', anything())).never(); + }); + test('Status change with a summary updates hasFailedTests to false ', async () => { + let handler!: StatusChangeHandler; + const fn = (cb: StatusChangeHandler) => (handler = cb); + when(mgr.onDidStatusChange).thenReturn(fn as any); + when(storage.getTests(anything())).thenReturn({ summary: { failures: 0 } } as any); + contextService.register(); + + await handler.bind(contextService)({ status: TestStatus.Discovering, workspace: workspaceUri }); + + verify(cmdManager.executeCommand('setContext', 'hasFailedTests', false)).once(); + }); + test('Status change with a summary and failures updates hasFailedTests to false', async () => { + let handler!: StatusChangeHandler; + const fn = (cb: StatusChangeHandler) => (handler = cb); + when(mgr.onDidStatusChange).thenReturn(fn as any); + when(storage.getTests(anything())).thenReturn({ summary: { failures: 1 } } as any); + contextService.register(); + + await handler.bind(contextService)({ status: TestStatus.Discovering, workspace: workspaceUri }); + + verify(cmdManager.executeCommand('setContext', 'hasFailedTests', true)).once(); + }); + test('Status change with status of running', async () => { + let handler!: StatusChangeHandler; + const fn = (cb: StatusChangeHandler) => (handler = cb); + when(mgr.onDidStatusChange).thenReturn(fn as any); + when(storage.getTests(anything())).thenReturn({} as any); + contextService.register(); + + await handler.bind(contextService)({ status: TestStatus.Running, workspace: workspaceUri }); + + verify(cmdManager.executeCommand('setContext', 'runningTests', true)).once(); + verify(cmdManager.executeCommand('setContext', 'discoveringTests', false)).once(); + verify(cmdManager.executeCommand('setContext', 'busyTests', true)).once(); + }); + test('Status change with status of discovering', async () => { + let handler!: StatusChangeHandler; + const fn = (cb: StatusChangeHandler) => (handler = cb); + when(mgr.onDidStatusChange).thenReturn(fn as any); + when(storage.getTests(anything())).thenReturn({} as any); + contextService.register(); + + await handler.bind(contextService)({ status: TestStatus.Discovering, workspace: workspaceUri }); + + verify(cmdManager.executeCommand('setContext', 'runningTests', false)).once(); + verify(cmdManager.executeCommand('setContext', 'discoveringTests', true)).once(); + verify(cmdManager.executeCommand('setContext', 'busyTests', true)).once(); + }); + test('Status change with status of others', async () => { + let handler!: StatusChangeHandler; + const fn = (cb: StatusChangeHandler) => (handler = cb); + when(mgr.onDidStatusChange).thenReturn(fn as any); + when(storage.getTests(anything())).thenReturn({} as any); + contextService.register(); + + await handler.bind(contextService)({ status: TestStatus.Error, workspace: workspaceUri }); + await handler.bind(contextService)({ status: TestStatus.Fail, workspace: workspaceUri }); + await handler.bind(contextService)({ status: TestStatus.Idle, workspace: workspaceUri }); + await handler.bind(contextService)({ status: TestStatus.Pass, workspace: workspaceUri }); + await handler.bind(contextService)({ status: TestStatus.Skipped, workspace: workspaceUri }); + await handler.bind(contextService)({ status: TestStatus.Unknown, workspace: workspaceUri }); + + verify(cmdManager.executeCommand('setContext', 'runningTests', false)).once(); + verify(cmdManager.executeCommand('setContext', 'discoveringTests', false)).once(); + verify(cmdManager.executeCommand('setContext', 'busyTests', false)).once(); + + verify(cmdManager.executeCommand('setContext', 'runningTests', true)).never(); + verify(cmdManager.executeCommand('setContext', 'discoveringTests', true)).never(); + verify(cmdManager.executeCommand('setContext', 'busyTests', true)).never(); + }); +}); diff --git a/src/test/testing/common/services/discoveredTestParser.unit.test.ts b/src/test/testing/common/services/discoveredTestParser.unit.test.ts new file mode 100644 index 000000000000..9238e75822db --- /dev/null +++ b/src/test/testing/common/services/discoveredTestParser.unit.test.ts @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { TestDiscoveredTestParser } from '../../../../client/testing/common/services/discoveredTestParser'; +import { Tests } from '../../../../client/testing/common/types'; + +// tslint:disable:no-any max-func-body-length +suite('Services - Discovered test parser', () => { + let workspaceService: typemoq.IMock<IWorkspaceService>; + let parser: TestDiscoveredTestParser; + setup(() => { + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Parse returns empty tests if resource does not belong to workspace', () => { + // That is, getWorkspaceFolder() returns undefined. + const expectedTests: Tests = { + rootTestFolders: [], + summary: { errors: 0, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFolders: [], + testFunctions: [], + testSuites: [] + }; + const discoveredTests = [ + { + root: 'path/to/testDataRoot' + } + ]; + const buildChildren = sinon.stub(TestDiscoveredTestParser.prototype, 'buildChildren'); + buildChildren.callsFake(() => undefined); + workspaceService + .setup((w) => w.getWorkspaceFolder(typemoq.It.isAny())) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + parser = new TestDiscoveredTestParser(workspaceService.object); + const result = parser.parse(Uri.file('path/to/resource'), discoveredTests as any); + assert.ok(buildChildren.notCalled); + assert.deepEqual(expectedTests, result); + workspaceService.verifyAll(); + }); + + test('Parse returns expected tests otherwise', () => { + const discoveredTests = [ + { + root: 'path/to/testDataRoot1', + rootid: 'rootId1' + }, + { + root: 'path/to/testDataRoot2', + rootid: 'rootId2' + } + ]; + const workspaceUri = Uri.file('path/to/workspace'); + const workspace = { uri: workspaceUri }; + const expectedTests: Tests = { + rootTestFolders: [ + { + name: 'path/to/testDataRoot1', + folders: [], + time: 0, + testFiles: [], + resource: workspaceUri, + nameToRun: 'rootId1' + }, + { + name: 'path/to/testDataRoot2', + folders: [], + time: 0, + testFiles: [], + resource: workspaceUri, + nameToRun: 'rootId2' + } + ], + summary: { errors: 0, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFolders: [ + { + name: 'path/to/testDataRoot1', + folders: [], + time: 0, + testFiles: [], + resource: workspaceUri, + nameToRun: 'rootId1' + }, + { + name: 'path/to/testDataRoot2', + folders: [], + time: 0, + testFiles: [], + resource: workspaceUri, + nameToRun: 'rootId2' + } + ], + testFunctions: [], + testSuites: [] + }; + const buildChildren = sinon.stub(TestDiscoveredTestParser.prototype, 'buildChildren'); + buildChildren.callsFake(() => undefined); + workspaceService + .setup((w) => w.getWorkspaceFolder(typemoq.It.isAny())) + .returns(() => workspace as any) + .verifiable(typemoq.Times.once()); + parser = new TestDiscoveredTestParser(workspaceService.object); + const result = parser.parse(workspaceUri, discoveredTests as any); + assert.ok(buildChildren.calledTwice); + assert.deepEqual(expectedTests, result); + }); +}); diff --git a/src/test/testing/common/services/discovery.unit.test.ts b/src/test/testing/common/services/discovery.unit.test.ts new file mode 100644 index 000000000000..e7b54d2ec0cc --- /dev/null +++ b/src/test/testing/common/services/discovery.unit.test.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import { deepEqual, instance, mock, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { CancellationTokenSource, OutputChannel, Uri, ViewColumn } from 'vscode'; +import { PythonExecutionFactory } from '../../../../client/common/process/pythonExecutionFactory'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + IPythonExecutionService, + SpawnOptions +} from '../../../../client/common/process/types'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { TestDiscoveredTestParser } from '../../../../client/testing/common/services/discoveredTestParser'; +import { TestsDiscoveryService } from '../../../../client/testing/common/services/discovery'; +import { DiscoveredTests, ITestDiscoveredTestParser } from '../../../../client/testing/common/services/types'; +import { TestDiscoveryOptions, Tests } from '../../../../client/testing/common/types'; +import { MockOutputChannel } from '../../../mockClasses'; + +// tslint:disable:no-unnecessary-override no-any +suite('Unit Tests - Common Discovery', () => { + let output: OutputChannel; + let discovery: TestsDiscoveryService; + let executionFactory: IPythonExecutionFactory; + let parser: ITestDiscoveredTestParser; + setup(() => { + // tslint:disable-next-line:no-use-before-declare + output = mock(StubOutput); + executionFactory = mock(PythonExecutionFactory); + parser = mock(TestDiscoveredTestParser); + discovery = new TestsDiscoveryService(instance(executionFactory), instance(parser), instance(output)); + }); + test('Use parser to parse results', async () => { + const options: TestDiscoveryOptions = { + args: [], + cwd: __dirname, + workspaceFolder: Uri.file(__dirname), + ignoreCache: false, + token: new CancellationTokenSource().token, + outChannel: new MockOutputChannel('Test') + }; + const discoveredTests: DiscoveredTests[] = [{ hello: 1 } as any]; + const parsedResult = ({ done: true } as any) as Tests; + discovery.exec = () => Promise.resolve(discoveredTests); + when(parser.parse(options.workspaceFolder, deepEqual(discoveredTests))).thenResolve(parsedResult as any); + + const tests = await discovery.discoverTests(options); + + assert.deepEqual(tests, parsedResult); + }); + test('Invoke Python Code to discover tests', async () => { + const options: TestDiscoveryOptions = { + args: ['1', '2', '3'], + cwd: __dirname, + workspaceFolder: Uri.file(__dirname), + ignoreCache: false, + token: new CancellationTokenSource().token, + outChannel: new MockOutputChannel('Test') + }; + const discoveredTests = '[1]'; + const execService = typemoq.Mock.ofType<IPythonExecutionService>(); + execService.setup((e: any) => e.then).returns(() => undefined); + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: options.workspaceFolder + }; + const pythonFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testing_tools', 'run_adapter.py'); + const spawnOptions: SpawnOptions = { + token: options.token, + cwd: options.cwd, + throwOnStdErr: true + }; + + when(executionFactory.createActivatedEnvironment(deepEqual(creationOptions))).thenResolve(execService.object); + const executionResult = { stdout: JSON.stringify(discoveredTests) }; + execService + .setup((e) => e.exec(typemoq.It.isValue([pythonFile, ...options.args]), typemoq.It.isValue(spawnOptions))) + .returns(() => Promise.resolve(executionResult)); + + const result = await discovery.exec(options); + + execService.verifyAll(); + assert.deepEqual(result, discoveredTests); + }); +}); + +// tslint:disable:no-empty + +//class StubOutput implements OutputChannel { +class StubOutput { + constructor(public name: string) {} + public append(_value: string) {} + public appendLine(_value: string) {} + public clear() {} + //public show(_preserveFocus?: boolean) {} + public show(_column?: ViewColumn | boolean, _preserveFocus?: boolean) {} + public hide() {} + public dispose() {} +} diff --git a/src/test/testing/common/services/storageService.unit.test.ts b/src/test/testing/common/services/storageService.unit.test.ts new file mode 100644 index 000000000000..62e15894fc5e --- /dev/null +++ b/src/test/testing/common/services/storageService.unit.test.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { copyDesiredTestResults } from '../../../../client/testing/common/testUtils'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + TestFile, + TestFolder, + TestFunction, + Tests, + TestStatus, + TestSuite +} from '../../../../client/testing/common/types'; +import { TestDataItemType } from '../../../../client/testing/types'; +import { createMockTestDataItem } from '../testUtils.unit.test'; + +// tslint:disable:no-any max-func-body-length +suite('Unit Tests - Storage Service', () => { + let testData1: Tests; + let testData2: Tests; + setup(() => { + setupTestData1(); + setupTestData2(); + }); + + function setupTestData1() { + const folder1 = createMockTestDataItem<TestFolder>(TestDataItemType.folder, '1'); + const file1 = createMockTestDataItem<TestFile>(TestDataItemType.file, '1'); + folder1.testFiles.push(file1); + const suite1 = createMockTestDataItem<TestSuite>(TestDataItemType.suite, '1'); + const suite2 = createMockTestDataItem<TestSuite>(TestDataItemType.suite, '2'); + const fn1 = createMockTestDataItem<TestFunction>(TestDataItemType.function, '1'); + const fn2 = createMockTestDataItem<TestFunction>(TestDataItemType.function, '2'); + const fn3 = createMockTestDataItem<TestFunction>(TestDataItemType.function, '3'); + file1.suites.push(suite1); + file1.suites.push(suite2); + file1.functions.push(fn1); + suite1.functions.push(fn2); + suite2.functions.push(fn3); + const flattendSuite1: FlattenedTestSuite = { + testSuite: suite1, + xmlClassName: suite1.xmlName + } as any; + const flattendSuite2: FlattenedTestSuite = { + testSuite: suite2, + xmlClassName: suite2.xmlName + } as any; + const flattendFn1: FlattenedTestFunction = { + testFunction: fn1, + xmlClassName: fn1.name + } as any; + const flattendFn2: FlattenedTestFunction = { + testFunction: fn2, + xmlClassName: fn2.name + } as any; + const flattendFn3: FlattenedTestFunction = { + testFunction: fn3, + xmlClassName: fn3.name + } as any; + testData1 = { + rootTestFolders: [folder1], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1], + testFolders: [folder1], + testFunctions: [flattendFn1, flattendFn2, flattendFn3], + testSuites: [flattendSuite1, flattendSuite2] + }; + } + + function setupTestData2() { + const folder1 = createMockTestDataItem<TestFolder>(TestDataItemType.folder, '1'); + const file1 = createMockTestDataItem<TestFile>(TestDataItemType.file, '1'); + folder1.testFiles.push(file1); + const suite1 = createMockTestDataItem<TestSuite>(TestDataItemType.suite, '1'); + const suite2 = createMockTestDataItem<TestSuite>(TestDataItemType.suite, '2'); + const fn1 = createMockTestDataItem<TestFunction>(TestDataItemType.function, '1'); + const fn2 = createMockTestDataItem<TestFunction>(TestDataItemType.function, '2'); + const fn3 = createMockTestDataItem<TestFunction>(TestDataItemType.function, '3'); + file1.suites.push(suite1); + file1.suites.push(suite2); + suite1.functions.push(fn1); + suite1.functions.push(fn2); + suite2.functions.push(fn3); + const flattendSuite1: FlattenedTestSuite = { + testSuite: suite1, + xmlClassName: suite1.xmlName + } as any; + const flattendSuite2: FlattenedTestSuite = { + testSuite: suite2, + xmlClassName: suite2.xmlName + } as any; + const flattendFn1: FlattenedTestFunction = { + testFunction: fn1, + xmlClassName: fn1.name + } as any; + const flattendFn2: FlattenedTestFunction = { + testFunction: fn2, + xmlClassName: fn2.name + } as any; + const flattendFn3: FlattenedTestFunction = { + testFunction: fn3, + xmlClassName: fn3.name + } as any; + testData2 = { + rootTestFolders: [folder1], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1], + testFolders: [folder1], + testFunctions: [flattendFn1, flattendFn2, flattendFn3], + testSuites: [flattendSuite1, flattendSuite2] + }; + } + + test('Merge Status from existing tests', () => { + testData1.testFunctions[0].testFunction.passed = true; + testData1.testFunctions[1].testFunction.status = TestStatus.Fail; + testData1.testFunctions[2].testFunction.time = 1234; + + assert.notDeepEqual(testData1.testFunctions[0].testFunction, testData2.testFunctions[0].testFunction); + assert.notDeepEqual(testData1.testFunctions[1].testFunction, testData2.testFunctions[1].testFunction); + assert.notDeepEqual(testData1.testFunctions[2].testFunction, testData2.testFunctions[2].testFunction); + + copyDesiredTestResults(testData1, testData2); + + // Function 1 is in a different suite now, hence should not get updated. + assert.notDeepEqual(testData1.testFunctions[0].testFunction, testData2.testFunctions[0].testFunction); + assert.deepEqual(testData1.testFunctions[1].testFunction, testData2.testFunctions[1].testFunction); + assert.deepEqual(testData1.testFunctions[2].testFunction, testData2.testFunctions[2].testFunction); + }); +}); diff --git a/src/test/testing/common/services/testResultsService.unit.test.ts b/src/test/testing/common/services/testResultsService.unit.test.ts new file mode 100644 index 000000000000..880d03982ff2 --- /dev/null +++ b/src/test/testing/common/services/testResultsService.unit.test.ts @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { TestResultsService } from '../../../../client/testing/common/services/testResultsService'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + ITestVisitor, + TestFile, + TestFolder, + TestFunction, + Tests, + TestStatus, + TestSuite +} from '../../../../client/testing/common/types'; +import { TestDataItemType } from '../../../../client/testing/types'; +import { createMockTestDataItem } from '../testUtils.unit.test'; + +// tslint:disable:no-any max-func-body-length +suite('Unit Tests - Tests Results Service', () => { + let testResultsService: TestResultsService; + let resultResetVisitor: typemoq.IMock<ITestVisitor>; + let tests!: Tests; + // tslint:disable:one-variable-per-declaration + let folder1: TestFolder, + folder2: TestFolder, + folder3: TestFolder, + folder4: TestFolder, + folder5: TestFolder, + suite1: TestSuite, + suite2: TestSuite, + suite3: TestSuite, + suite4: TestSuite, + suite5: TestSuite; + let file1: TestFile, file2: TestFile, file3: TestFile, file4: TestFile, file5: TestFile; + setup(() => { + resultResetVisitor = typemoq.Mock.ofType<ITestVisitor>(); + folder1 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + folder2 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + folder3 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + folder4 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + folder5 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + folder1.folders.push(folder2); + folder1.folders.push(folder3); + folder2.folders.push(folder4); + folder3.folders.push(folder5); + + file1 = createMockTestDataItem<TestFile>(TestDataItemType.file); + file2 = createMockTestDataItem<TestFile>(TestDataItemType.file); + file3 = createMockTestDataItem<TestFile>(TestDataItemType.file); + file4 = createMockTestDataItem<TestFile>(TestDataItemType.file); + file5 = createMockTestDataItem<TestFile>(TestDataItemType.file); + folder1.testFiles.push(file1); + folder3.testFiles.push(file2); + folder3.testFiles.push(file3); + folder4.testFiles.push(file5); + folder5.testFiles.push(file4); + + suite1 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + suite2 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + suite3 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + suite4 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + suite5 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const fn1 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + fn1.passed = true; + const fn2 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + fn2.passed = undefined; + const fn3 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + fn3.passed = true; + const fn4 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + fn4.passed = false; + const fn5 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + fn5.passed = undefined; + const fn6 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + fn6.passed = true; + const fn7 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + fn7.passed = undefined; + const fn8 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + fn8.passed = false; + const fn9 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + fn9.passed = true; + const fn10 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + fn10.passed = true; + const fn11 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + fn11.passed = true; + file1.suites.push(suite1); + file1.suites.push(suite2); + file3.suites.push(suite3); + suite3.suites.push(suite4); + suite4.suites.push(suite5); + file1.functions.push(fn1); + file1.functions.push(fn2); + file2.functions.push(fn8); + file4.functions.push(fn9); + file4.functions.push(fn11); + file5.functions.push(fn10); + suite1.functions.push(fn3); + suite1.functions.push(fn4); + suite2.functions.push(fn6); + suite3.functions.push(fn5); + suite5.functions.push(fn7); + const flattendSuite1: FlattenedTestSuite = { + testSuite: suite1, + xmlClassName: suite1.xmlName + } as any; + const flattendSuite2: FlattenedTestSuite = { + testSuite: suite2, + xmlClassName: suite2.xmlName + } as any; + const flattendSuite3: FlattenedTestSuite = { + testSuite: suite3, + xmlClassName: suite3.xmlName + } as any; + const flattendSuite4: FlattenedTestSuite = { + testSuite: suite4, + xmlClassName: suite4.xmlName + } as any; + const flattendSuite5: FlattenedTestSuite = { + testSuite: suite5, + xmlClassName: suite5.xmlName + } as any; + const flattendFn1: FlattenedTestFunction = { + testFunction: fn1, + xmlClassName: fn1.name + } as any; + const flattendFn2: FlattenedTestFunction = { + testFunction: fn2, + xmlClassName: fn2.name + } as any; + const flattendFn3: FlattenedTestFunction = { + testFunction: fn3, + xmlClassName: fn3.name + } as any; + const flattendFn4: FlattenedTestFunction = { + testFunction: fn4, + xmlClassName: fn4.name + } as any; + const flattendFn5: FlattenedTestFunction = { + testFunction: fn5, + xmlClassName: fn5.name + } as any; + const flattendFn6: FlattenedTestFunction = { + testFunction: fn6, + xmlClassName: fn6.name + } as any; + const flattendFn7: FlattenedTestFunction = { + testFunction: fn7, + xmlClassName: fn7.name + } as any; + const flattendFn8: FlattenedTestFunction = { + testFunction: fn8, + xmlClassName: fn8.name + } as any; + const flattendFn9: FlattenedTestFunction = { + testFunction: fn9, + xmlClassName: fn9.name + } as any; + const flattendFn10: FlattenedTestFunction = { + testFunction: fn10, + xmlClassName: fn10.name + } as any; + const flattendFn11: FlattenedTestFunction = { + testFunction: fn11, + xmlClassName: fn11.name + } as any; + tests = { + rootTestFolders: [folder1], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1, file2, file3, file4, file5], + testFolders: [folder1, folder2, folder3, folder4, folder5], + testFunctions: [ + flattendFn1, + flattendFn2, + flattendFn3, + flattendFn4, + flattendFn5, + flattendFn6, + flattendFn7, + flattendFn8, + flattendFn9, + flattendFn10, + flattendFn11 + ], + testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] + }; + testResultsService = new TestResultsService(resultResetVisitor.object); + }); + + test('If any test fails, parent fails', () => { + testResultsService.updateResults(tests); + expect(suite1.status).to.equal(TestStatus.Fail); + expect(file1.status).to.equal(TestStatus.Fail); + expect(folder1.status).to.equal(TestStatus.Fail); + expect(file2.status).to.equal(TestStatus.Fail); + expect(folder3.status).to.equal(TestStatus.Fail); + }); + + test('If all tests pass, parent passes', () => { + testResultsService.updateResults(tests); + expect(file4.status).to.equal(TestStatus.Pass); + expect(folder5.status).to.equal(TestStatus.Pass); + expect(folder2.status).to.equal(TestStatus.Pass); + }); + + test('If no tests run, parent status is not run', () => { + testResultsService.updateResults(tests); + expect(suite3.status).to.equal(TestStatus.Unknown); + expect(suite4.status).to.equal(TestStatus.Unknown); + expect(suite5.status).to.equal(TestStatus.Unknown); + expect(file3.status).to.equal(TestStatus.Unknown); + }); + + test('Number of functions passed, not run and failed are correctly calculated', () => { + testResultsService.updateResults(tests); + + expect(file1.functionsPassed).to.equal(3); + expect(folder2.functionsPassed).to.equal(1); + expect(folder3.functionsPassed).to.equal(2); + expect(folder1.functionsPassed).to.equal(6); + + expect(file1.functionsFailed).to.equal(1); + expect(folder2.functionsFailed).to.equal(0); + expect(folder3.functionsFailed).to.equal(1); + expect(folder1.functionsFailed).to.equal(2); + + expect(file1.functionsDidNotRun).to.equal(1); + expect(suite4.functionsDidNotRun).to.equal(1); + expect(suite3.functionsDidNotRun).to.equal(2); + expect(folder1.functionsDidNotRun).to.equal(3); + }); +}); diff --git a/src/test/testing/common/services/testStatusService.unit.test.ts b/src/test/testing/common/services/testStatusService.unit.test.ts new file mode 100644 index 000000000000..f2a689c3d5a2 --- /dev/null +++ b/src/test/testing/common/services/testStatusService.unit.test.ts @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { TestCollectionStorageService } from '../../../../client/testing/common/services/storageService'; +import { TestsStatusUpdaterService } from '../../../../client/testing/common/services/testsStatusService'; +import { visitRecursive } from '../../../../client/testing/common/testVisitors/visitor'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + ITestCollectionStorageService, + ITestsStatusUpdaterService, + TestFile, + TestFolder, + TestFunction, + Tests, + TestStatus, + TestSuite +} from '../../../../client/testing/common/types'; +import { TestDataItem, TestDataItemType } from '../../../../client/testing/types'; +import { createMockTestDataItem } from '../testUtils.unit.test'; + +// tslint:disable:no-any max-func-body-length +suite('Unit Tests - Tests Status Updater', () => { + let storage: ITestCollectionStorageService; + let updater: ITestsStatusUpdaterService; + const workspaceUri = Uri.file(__filename); + let tests!: Tests; + setup(() => { + storage = mock(TestCollectionStorageService); + updater = new TestsStatusUpdaterService(instance(storage)); + const folder1 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder2 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder3 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder4 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder5 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + folder1.folders.push(folder2); + folder1.folders.push(folder3); + folder2.folders.push(folder4); + folder3.folders.push(folder5); + + const file1 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file2 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file3 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file4 = createMockTestDataItem<TestFile>(TestDataItemType.file); + folder1.testFiles.push(file1); + folder3.testFiles.push(file2); + folder3.testFiles.push(file3); + folder5.testFiles.push(file4); + + const suite1 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite2 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite3 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite4 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite5 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const fn1 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn2 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn3 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn4 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn5 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + file1.suites.push(suite1); + file1.suites.push(suite2); + file3.suites.push(suite3); + suite3.suites.push(suite4); + suite4.suites.push(suite5); + file1.functions.push(fn1); + file1.functions.push(fn2); + suite1.functions.push(fn3); + suite1.functions.push(fn4); + suite3.functions.push(fn5); + const flattendSuite1: FlattenedTestSuite = { + testSuite: suite1, + xmlClassName: suite1.xmlName + } as any; + const flattendSuite2: FlattenedTestSuite = { + testSuite: suite2, + xmlClassName: suite2.xmlName + } as any; + const flattendSuite3: FlattenedTestSuite = { + testSuite: suite3, + xmlClassName: suite3.xmlName + } as any; + const flattendSuite4: FlattenedTestSuite = { + testSuite: suite4, + xmlClassName: suite4.xmlName + } as any; + const flattendSuite5: FlattenedTestSuite = { + testSuite: suite5, + xmlClassName: suite5.xmlName + } as any; + const flattendFn1: FlattenedTestFunction = { + testFunction: fn1, + xmlClassName: fn1.name + } as any; + const flattendFn2: FlattenedTestFunction = { + testFunction: fn2, + xmlClassName: fn2.name + } as any; + const flattendFn3: FlattenedTestFunction = { + testFunction: fn3, + xmlClassName: fn3.name + } as any; + const flattendFn4: FlattenedTestFunction = { + testFunction: fn4, + xmlClassName: fn4.name + } as any; + const flattendFn5: FlattenedTestFunction = { + testFunction: fn5, + xmlClassName: fn5.name + } as any; + tests = { + rootTestFolders: [folder1], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1, file2, file3, file4], + testFolders: [folder1, folder2, folder3, folder4, folder5], + testFunctions: [flattendFn1, flattendFn2, flattendFn3, flattendFn4, flattendFn5], + testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] + }; + when(storage.getTests(workspaceUri)).thenReturn(tests); + }); + + test('Updating discovery status will recursively update all items and triggers an update for each', () => { + updater.updateStatusAsDiscovering(workspaceUri, tests); + + function validate(item: TestDataItem) { + assert.equal(item.status, TestStatus.Discovering); + verify(storage.update(workspaceUri, item)).once(); + } + tests.testFolders.forEach(validate); + tests.testFiles.forEach(validate); + tests.testFunctions.forEach((func) => validate(func.testFunction)); + tests.testSuites.forEach((suite) => validate(suite.testSuite)); + }); + test('Updating unknown status will recursively update all items and triggers an update for each', () => { + updater.updateStatusAsUnknown(workspaceUri, tests); + + function validate(item: TestDataItem) { + assert.equal(item.status, TestStatus.Unknown); + verify(storage.update(workspaceUri, item)).once(); + } + tests.testFolders.forEach(validate); + tests.testFiles.forEach(validate); + tests.testFunctions.forEach((func) => validate(func.testFunction)); + tests.testSuites.forEach((suite) => validate(suite.testSuite)); + }); + test('Updating running status will recursively update all items and triggers an update for each', () => { + updater.updateStatusAsRunning(workspaceUri, tests); + + function validate(item: TestDataItem) { + assert.equal(item.status, TestStatus.Running); + verify(storage.update(workspaceUri, item)).once(); + } + tests.testFolders.forEach(validate); + tests.testFiles.forEach(validate); + tests.testFunctions.forEach((func) => validate(func.testFunction)); + tests.testSuites.forEach((suite) => validate(suite.testSuite)); + }); + test('Updating running status for failed tests will recursively update all items and triggers an update for each', () => { + tests.testFolders[1].status = TestStatus.Fail; + tests.testFolders[2].status = TestStatus.Error; + tests.testFiles[2].status = TestStatus.Fail; + tests.testFiles[3].status = TestStatus.Error; + tests.testFunctions[2].testFunction.status = TestStatus.Fail; + tests.testFunctions[3].testFunction.status = TestStatus.Error; + tests.testFunctions[4].testFunction.status = TestStatus.Pass; + tests.testSuites[1].testSuite.status = TestStatus.Fail; + tests.testSuites[2].testSuite.status = TestStatus.Error; + + updater.updateStatusAsRunningFailedTests(workspaceUri, tests); + + // Do not update status of folders and files. + assert.equal(tests.testFolders[1].status, TestStatus.Fail); + assert.equal(tests.testFolders[2].status, TestStatus.Error); + assert.equal(tests.testFiles[2].status, TestStatus.Fail); + assert.equal(tests.testFiles[3].status, TestStatus.Error); + + // Update status of test functions and suites. + const updatedItems: TestDataItem[] = []; + const visitor = (item: TestDataItem) => { + if (item.status && item.status !== TestStatus.Pass) { + updatedItems.push(item); + } + }; + const failedItems = [ + tests.testFunctions[2].testFunction, + tests.testFunctions[3].testFunction, + tests.testSuites[1].testSuite, + tests.testSuites[2].testSuite + ]; + failedItems.forEach((failedItem) => visitRecursive(tests, failedItem, visitor)); + + for (const item of updatedItems) { + assert.equal(item.status, TestStatus.Running); + verify(storage.update(workspaceUri, item)).once(); + } + + // Only items with status Fail & Error should be modified + assert.equal(tests.testFunctions[4].testFunction.status, TestStatus.Pass); + + // Should only be called for failed items. + verify(storage.update(workspaceUri, anything())).times(updatedItems.length); + }); + test('Updating idle status for runnings tests will recursively update all items and triggers an update for each', () => { + tests.testFolders[1].status = TestStatus.Running; + tests.testFolders[2].status = TestStatus.Running; + tests.testFiles[2].status = TestStatus.Running; + tests.testFiles[3].status = TestStatus.Running; + tests.testFunctions[2].testFunction.status = TestStatus.Running; + tests.testFunctions[3].testFunction.status = TestStatus.Running; + tests.testSuites[1].testSuite.status = TestStatus.Running; + tests.testSuites[2].testSuite.status = TestStatus.Running; + + updater.updateStatusOfRunningTestsAsIdle(workspaceUri, tests); + + const updatedItems: TestDataItem[] = []; + updatedItems.push(tests.testFolders[1]); + updatedItems.push(tests.testFolders[2]); + updatedItems.push(tests.testFiles[2]); + updatedItems.push(tests.testFiles[3]); + updatedItems.push(tests.testFunctions[2].testFunction); + updatedItems.push(tests.testFunctions[3].testFunction); + updatedItems.push(tests.testSuites[1].testSuite); + updatedItems.push(tests.testSuites[2].testSuite); + + for (const item of updatedItems) { + assert.equal(item.status, TestStatus.Idle); + verify(storage.update(workspaceUri, item)).once(); + } + + // Should only be called for failed items. + verify(storage.update(workspaceUri, anything())).times(updatedItems.length); + }); + test('Triggers an update for each', () => { + updater.triggerUpdatesToTests(workspaceUri, tests); + + const updatedItems: TestDataItem[] = [ + ...tests.testFolders, + ...tests.testFiles, + ...tests.testFunctions.map((item) => item.testFunction), + ...tests.testSuites.map((item) => item.testSuite) + ]; + + for (const item of updatedItems) { + verify(storage.update(workspaceUri, item)).once(); + } + + verify(storage.update(workspaceUri, anything())).times(updatedItems.length); + }); +}); diff --git a/src/test/testing/common/testUtils.unit.test.ts b/src/test/testing/common/testUtils.unit.test.ts new file mode 100644 index 000000000000..82822609856f --- /dev/null +++ b/src/test/testing/common/testUtils.unit.test.ts @@ -0,0 +1,698 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { + getChildren, + getParent, + getParentFile, + getParentSuite, + getTestDataItemType, + getTestFile, + getTestFolder, + getTestFunction, + getTestSuite +} from '../../../client/testing/common/testUtils'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + SubtestParent, + TestFile, + TestFolder, + TestFunction, + Tests, + TestSuite +} from '../../../client/testing/common/types'; +import { TestDataItem, TestDataItemType, TestWorkspaceFolder } from '../../../client/testing/types'; + +// tslint:disable:prefer-template + +function longestCommonSubstring(strings: string[]): string { + strings = strings.concat().sort(); + let substr = strings.shift() || ''; + strings.forEach((str) => { + for (const [idx, ch] of [...substr].entries()) { + if (str[idx] !== ch) { + substr = substr.substring(0, idx); + break; + } + } + }); + return substr; +} + +export function createMockTestDataItem<T extends TestDataItem>( + type: TestDataItemType, + nameSuffix: string = '', + name?: string, + nameToRun?: string +) { + const folder: TestFolder = { + resource: Uri.file(__filename), + folders: [], + name: name || 'Some Folder' + nameSuffix, + nameToRun: nameToRun || name || ' Some Folder' + nameSuffix, + testFiles: [], + time: 0 + }; + const file: TestFile = { + resource: Uri.file(__filename), + name: name || 'Some File' + nameSuffix, + nameToRun: nameToRun || name || ' Some File' + nameSuffix, + fullPath: __filename, + xmlName: name || 'some xml name' + nameSuffix, + functions: [], + suites: [], + time: 0 + }; + const func: TestFunction = { + resource: Uri.file(__filename), + name: name || 'Some Function' + nameSuffix, + nameToRun: nameToRun || name || ' Some Function' + nameSuffix, + time: 0 + }; + const suite: TestSuite = { + resource: Uri.file(__filename), + name: name || 'Some Suite' + nameSuffix, + nameToRun: nameToRun || name || ' Some Suite' + nameSuffix, + functions: [], + isInstance: true, + isUnitTest: false, + suites: [], + xmlName: name || 'some name' + nameSuffix, + time: 0 + }; + + switch (type) { + case TestDataItemType.file: + return file as T; + case TestDataItemType.folder: + return folder as T; + case TestDataItemType.function: + return func as T; + case TestDataItemType.suite: + return suite as T; + case TestDataItemType.workspaceFolder: + return new TestWorkspaceFolder({ uri: Uri.file(''), name: 'a', index: 0 }) as T; + default: + throw new Error(`Unknown type ${type}`); + } +} + +export function createSubtestParent(funcs: TestFunction[]): SubtestParent { + const name = longestCommonSubstring(funcs.map((func) => func.name)); + const nameToRun = longestCommonSubstring(funcs.map((func) => func.nameToRun)); + const subtestParent: SubtestParent = { + name: name, + nameToRun: nameToRun, + asSuite: { + resource: Uri.file(__filename), + name: name, + nameToRun: nameToRun, + functions: funcs, + suites: [], + isUnitTest: false, + isInstance: false, + xmlName: '', + time: 0 + }, + time: 0 + }; + funcs.forEach((func) => { + func.subtestParent = subtestParent; + }); + return subtestParent; +} + +export function createTests( + folders: TestFolder[], + files: TestFile[], + suites: TestSuite[], + funcs: TestFunction[] +): Tests { + // tslint:disable:no-any + return { + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + rootTestFolders: folders.length > 0 ? [folders[0]] : [], + testFolders: folders, + testFiles: files, + testSuites: suites.map((suite) => { + return { + testSuite: suite, + xmlClassName: suite.xmlName + } as any; + }), + testFunctions: funcs.map((func) => { + return { + testFunction: func, + xmlClassName: func.name + } as any; + }) + }; +} + +// tslint:disable:max-func-body-length no-any +suite('Unit Tests - TestUtils', () => { + test('Get TestDataItemType for Folders', () => { + const item = createMockTestDataItem(TestDataItemType.folder); + assert.equal(getTestDataItemType(item), TestDataItemType.folder); + }); + test('Get TestDataItemType for Files', () => { + const item = createMockTestDataItem(TestDataItemType.file); + assert.equal(getTestDataItemType(item), TestDataItemType.file); + }); + test('Get TestDataItemType for Functions', () => { + const item = createMockTestDataItem(TestDataItemType.function); + assert.equal(getTestDataItemType(item), TestDataItemType.function); + }); + test('Get TestDataItemType for Suites', () => { + const item = createMockTestDataItem(TestDataItemType.suite); + assert.equal(getTestDataItemType(item), TestDataItemType.suite); + }); + test('Casting to a specific items', () => { + for (const typeName of getNamesAndValues<TestDataItemType>(TestDataItemType)) { + const item = createMockTestDataItem(typeName.value); + const file = getTestFile(item); + const folder = getTestFolder(item); + const suite = getTestSuite(item); + const func = getTestFunction(item); + + switch (typeName.value) { + case TestDataItemType.file: { + assert.equal(file, item); + assert.equal(folder, undefined); + assert.equal(suite, undefined); + assert.equal(func, undefined); + break; + } + case TestDataItemType.folder: { + assert.equal(file, undefined); + assert.equal(folder, item); + assert.equal(suite, undefined); + assert.equal(func, undefined); + break; + } + case TestDataItemType.function: { + assert.equal(file, undefined); + assert.equal(folder, undefined); + assert.equal(suite, undefined); + assert.equal(func, item); + break; + } + case TestDataItemType.suite: { + assert.equal(file, undefined); + assert.equal(folder, undefined); + assert.equal(suite, item); + assert.equal(func, undefined); + break; + } + case TestDataItemType.workspaceFolder: { + assert.equal(file, undefined); + assert.equal(folder, undefined); + assert.equal(suite, undefined); + assert.equal(func, undefined); + break; + } + default: + throw new Error(`Unknown type ${typeName.name},${typeName.value}`); + } + } + }); + test('Get Parent of folder', () => { + const folder1 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder2 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder3 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder4 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder5 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + folder1.folders.push(folder2); + folder1.folders.push(folder3); + folder2.folders.push(folder4); + folder3.folders.push(folder5); + const tests: Tests = { + rootTestFolders: [folder1], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [], + testFolders: [folder1, folder2, folder3, folder4, folder5], + testFunctions: [], + testSuites: [] + }; + assert.equal(getParent(tests, folder1), undefined); + assert.equal(getParent(tests, folder2), folder1); + assert.equal(getParent(tests, folder3), folder1); + assert.equal(getParent(tests, folder4), folder2); + assert.equal(getParent(tests, folder5), folder3); + }); + test('Get Parent of file', () => { + const folder1 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder2 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder3 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder4 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder5 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + folder1.folders.push(folder2); + folder1.folders.push(folder3); + folder2.folders.push(folder4); + folder3.folders.push(folder5); + + const file1 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file2 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file3 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file4 = createMockTestDataItem<TestFile>(TestDataItemType.file); + folder1.testFiles.push(file1); + folder3.testFiles.push(file2); + folder3.testFiles.push(file3); + folder5.testFiles.push(file4); + const tests: Tests = { + rootTestFolders: [folder1], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1, file2, file3, file4], + testFolders: [folder1, folder2, folder3, folder4, folder5], + testFunctions: [], + testSuites: [] + }; + assert.equal(getParent(tests, file1), folder1); + assert.equal(getParent(tests, file2), folder3); + assert.equal(getParent(tests, file3), folder3); + assert.equal(getParent(tests, file4), folder5); + }); + test('Get Parent File', () => { + const file1 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file2 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file3 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file4 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const suite1 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite2 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite3 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite4 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite5 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const fn1 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn2 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn3 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn4 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn5 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + file1.suites.push(suite1); + file1.suites.push(suite2); + file3.suites.push(suite3); + suite3.suites.push(suite4); + suite4.suites.push(suite5); + file1.functions.push(fn1); + file1.functions.push(fn2); + suite1.functions.push(fn3); + suite1.functions.push(fn4); + suite3.functions.push(fn5); + const flattendSuite1: FlattenedTestSuite = { + testSuite: suite1, + xmlClassName: suite1.xmlName + } as any; + const flattendSuite2: FlattenedTestSuite = { + testSuite: suite2, + xmlClassName: suite2.xmlName + } as any; + const flattendSuite3: FlattenedTestSuite = { + testSuite: suite3, + xmlClassName: suite3.xmlName + } as any; + const flattendSuite4: FlattenedTestSuite = { + testSuite: suite4, + xmlClassName: suite4.xmlName + } as any; + const flattendSuite5: FlattenedTestSuite = { + testSuite: suite5, + xmlClassName: suite5.xmlName + } as any; + const flattendFn1: FlattenedTestFunction = { + testFunction: fn1, + xmlClassName: fn1.name + } as any; + const flattendFn2: FlattenedTestFunction = { + testFunction: fn2, + xmlClassName: fn2.name + } as any; + const flattendFn3: FlattenedTestFunction = { + testFunction: fn3, + xmlClassName: fn3.name + } as any; + const flattendFn4: FlattenedTestFunction = { + testFunction: fn4, + xmlClassName: fn4.name + } as any; + const flattendFn5: FlattenedTestFunction = { + testFunction: fn5, + xmlClassName: fn5.name + } as any; + const tests: Tests = { + rootTestFolders: [], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1, file2, file3, file4], + testFolders: [], + testFunctions: [flattendFn1, flattendFn2, flattendFn3, flattendFn4, flattendFn5], + testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] + }; + // Test parent file of functions (standalone and those in suites). + assert.equal(getParentFile(tests, fn1), file1); + assert.equal(getParentFile(tests, fn2), file1); + assert.equal(getParentFile(tests, fn3), file1); + assert.equal(getParentFile(tests, fn4), file1); + assert.equal(getParentFile(tests, fn5), file3); + + // Test parent file of suites (standalone and nested suites). + assert.equal(getParentFile(tests, suite1), file1); + assert.equal(getParentFile(tests, suite2), file1); + assert.equal(getParentFile(tests, suite3), file3); + assert.equal(getParentFile(tests, suite4), file3); + assert.equal(getParentFile(tests, suite5), file3); + }); + test('Get Parent Suite', () => { + const file1 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file2 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file3 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file4 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const suite1 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite2 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite3 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite4 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite5 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const fn1 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn2 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn3 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn4 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn5 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + file1.suites.push(suite1); + file1.suites.push(suite2); + file3.suites.push(suite3); + suite3.suites.push(suite4); + suite4.suites.push(suite5); + file1.functions.push(fn1); + file1.functions.push(fn2); + suite1.functions.push(fn3); + suite1.functions.push(fn4); + suite3.functions.push(fn5); + const flattendSuite1: FlattenedTestSuite = { + testSuite: suite1, + xmlClassName: suite1.xmlName + } as any; + const flattendSuite2: FlattenedTestSuite = { + testSuite: suite2, + xmlClassName: suite2.xmlName + } as any; + const flattendSuite3: FlattenedTestSuite = { + testSuite: suite3, + xmlClassName: suite3.xmlName + } as any; + const flattendSuite4: FlattenedTestSuite = { + testSuite: suite4, + xmlClassName: suite4.xmlName + } as any; + const flattendSuite5: FlattenedTestSuite = { + testSuite: suite5, + xmlClassName: suite5.xmlName + } as any; + const flattendFn1: FlattenedTestFunction = { + testFunction: fn1, + xmlClassName: fn1.name + } as any; + const flattendFn2: FlattenedTestFunction = { + testFunction: fn2, + xmlClassName: fn2.name + } as any; + const flattendFn3: FlattenedTestFunction = { + testFunction: fn3, + xmlClassName: fn3.name + } as any; + const flattendFn4: FlattenedTestFunction = { + testFunction: fn4, + xmlClassName: fn4.name + } as any; + const flattendFn5: FlattenedTestFunction = { + testFunction: fn5, + xmlClassName: fn5.name + } as any; + const tests: Tests = { + rootTestFolders: [], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1, file2, file3, file4], + testFolders: [], + testFunctions: [flattendFn1, flattendFn2, flattendFn3, flattendFn4, flattendFn5], + testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] + }; + // Test parent file of functions (standalone and those in suites). + assert.equal(getParentSuite(tests, fn1), undefined); + assert.equal(getParentSuite(tests, fn2), undefined); + assert.equal(getParentSuite(tests, fn3), suite1); + assert.equal(getParentSuite(tests, fn4), suite1); + assert.equal(getParentSuite(tests, fn5), suite3); + + // Test parent file of suites (standalone and nested suites). + assert.equal(getParentSuite(tests, suite1), undefined); + assert.equal(getParentSuite(tests, suite2), undefined); + assert.equal(getParentSuite(tests, suite3), undefined); + assert.equal(getParentSuite(tests, suite4), suite3); + assert.equal(getParentSuite(tests, suite5), suite4); + }); + test('Get Parent file throws an exception', () => { + const file1 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const suite1 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const fn1 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const flattendSuite1: FlattenedTestSuite = { + testSuite: suite1, + xmlClassName: suite1.xmlName + } as any; + const flattendFn1: FlattenedTestFunction = { + testFunction: fn1, + xmlClassName: fn1.name + } as any; + const tests: Tests = { + rootTestFolders: [], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1], + testFolders: [], + testFunctions: [flattendFn1], + testSuites: [flattendSuite1] + }; + assert.throws(() => getParentFile(tests, fn1), new RegExp('No parent file for provided test item')); + assert.throws(() => getParentFile(tests, suite1), new RegExp('No parent file for provided test item')); + }); + test('Get parent of orphaned items', () => { + const file1 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const suite1 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const fn1 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const flattendSuite1: FlattenedTestSuite = { + testSuite: suite1, + xmlClassName: suite1.xmlName + } as any; + const flattendFn1: FlattenedTestFunction = { + testFunction: fn1, + xmlClassName: fn1.name + } as any; + const tests: Tests = { + rootTestFolders: [], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1], + testFolders: [], + testFunctions: [flattendFn1], + testSuites: [flattendSuite1] + }; + assert.equal(getParent(tests, fn1), undefined); + assert.equal(getParent(tests, suite1), undefined); + }); + test('Get Parent of suite', () => { + const file1 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file2 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file3 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file4 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const suite1 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite2 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite3 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite4 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite5 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + file1.suites.push(suite1); + file1.suites.push(suite2); + file3.suites.push(suite3); + suite3.suites.push(suite4); + suite4.suites.push(suite5); + const flattendSuite1: FlattenedTestSuite = { + testSuite: suite1, + xmlClassName: suite1.xmlName + } as any; + const flattendSuite2: FlattenedTestSuite = { + testSuite: suite2, + xmlClassName: suite2.xmlName + } as any; + const flattendSuite3: FlattenedTestSuite = { + testSuite: suite3, + xmlClassName: suite3.xmlName + } as any; + const flattendSuite4: FlattenedTestSuite = { + testSuite: suite4, + xmlClassName: suite4.xmlName + } as any; + const flattendSuite5: FlattenedTestSuite = { + testSuite: suite5, + xmlClassName: suite5.xmlName + } as any; + const tests: Tests = { + rootTestFolders: [], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1, file2, file3, file4], + testFolders: [], + testFunctions: [], + testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] + }; + assert.equal(getParent(tests, suite1), file1); + assert.equal(getParent(tests, suite2), file1); + assert.equal(getParent(tests, suite3), file3); + assert.equal(getParent(tests, suite4), suite3); + assert.equal(getParent(tests, suite5), suite4); + }); + test('Get Parent of function', () => { + const file1 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file2 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file3 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file4 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const suite1 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite2 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite3 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite4 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite5 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const fn1 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn2 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn3 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn4 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn5 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + file1.suites.push(suite1); + file1.suites.push(suite2); + file3.suites.push(suite3); + suite3.suites.push(suite4); + suite4.suites.push(suite5); + file1.functions.push(fn1); + file1.functions.push(fn2); + suite1.functions.push(fn3); + suite1.functions.push(fn4); + suite3.functions.push(fn5); + const flattendSuite1: FlattenedTestSuite = { + testSuite: suite1, + xmlClassName: suite1.xmlName + } as any; + const flattendSuite2: FlattenedTestSuite = { + testSuite: suite2, + xmlClassName: suite2.xmlName + } as any; + const flattendSuite3: FlattenedTestSuite = { + testSuite: suite3, + xmlClassName: suite3.xmlName + } as any; + const flattendSuite4: FlattenedTestSuite = { + testSuite: suite4, + xmlClassName: suite4.xmlName + } as any; + const flattendSuite5: FlattenedTestSuite = { + testSuite: suite5, + xmlClassName: suite5.xmlName + } as any; + const flattendFn1: FlattenedTestFunction = { + testFunction: fn1, + xmlClassName: fn1.name + } as any; + const flattendFn2: FlattenedTestFunction = { + testFunction: fn2, + xmlClassName: fn2.name + } as any; + const flattendFn3: FlattenedTestFunction = { + testFunction: fn3, + xmlClassName: fn3.name + } as any; + const flattendFn4: FlattenedTestFunction = { + testFunction: fn4, + xmlClassName: fn4.name + } as any; + const flattendFn5: FlattenedTestFunction = { + testFunction: fn5, + xmlClassName: fn5.name + } as any; + const tests: Tests = { + rootTestFolders: [], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1, file2, file3, file4], + testFolders: [], + testFunctions: [flattendFn1, flattendFn2, flattendFn3, flattendFn4, flattendFn5], + testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] + }; + assert.equal(getParent(tests, fn1), file1); + assert.equal(getParent(tests, fn2), file1); + assert.equal(getParent(tests, fn3), suite1); + assert.equal(getParent(tests, fn4), suite1); + assert.equal(getParent(tests, fn5), suite3); + }); + test('Get parent of parameterized function', () => { + const folder = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const file = createMockTestDataItem<TestFile>(TestDataItemType.file); + const func1 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const func2 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const func3 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const subParent1 = createSubtestParent([func2, func3]); + const suite = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const func4 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const func5 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const func6 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const subParent2 = createSubtestParent([func5, func6]); + folder.testFiles.push(file); + file.functions.push(func1); + file.functions.push(func2); + file.functions.push(func3); + file.suites.push(suite); + suite.functions.push(func4); + suite.functions.push(func5); + suite.functions.push(func6); + const tests = createTests([folder], [file], [suite], [func1, func2, func3, func4, func5, func6]); + + assert.equal(getParent(tests, folder), undefined); + assert.equal(getParent(tests, file), folder); + assert.equal(getParent(tests, func1), file); + assert.equal(getParent(tests, subParent1.asSuite), file); + assert.equal(getParent(tests, func2), subParent1.asSuite); + assert.equal(getParent(tests, func3), subParent1.asSuite); + assert.equal(getParent(tests, suite), file); + assert.equal(getParent(tests, func4), suite); + assert.equal(getParent(tests, subParent2.asSuite), suite); + assert.equal(getParent(tests, func5), subParent2.asSuite); + assert.equal(getParent(tests, func6), subParent2.asSuite); + }); + test('Get children of parameterized function', () => { + const filename = path.join('tests', 'test_spam.py'); + const folder = createMockTestDataItem<TestFolder>(TestDataItemType.folder, 'tests'); + const file = createMockTestDataItem<TestFile>(TestDataItemType.file, filename); + const func1 = createMockTestDataItem<TestFunction>(TestDataItemType.function, 'test_x'); + const func2 = createMockTestDataItem<TestFunction>(TestDataItemType.function, 'test_y'); + const func3 = createMockTestDataItem<TestFunction>(TestDataItemType.function, 'test_z'); + const subParent1 = createSubtestParent([func2, func3]); + const suite = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const func4 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const func5 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const func6 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const subParent2 = createSubtestParent([func5, func6]); + folder.testFiles.push(file); + file.functions.push(func1); + file.functions.push(func2); + file.functions.push(func3); + file.suites.push(suite); + suite.functions.push(func4); + suite.functions.push(func5); + suite.functions.push(func6); + + assert.deepEqual(getChildren(folder), [file]); + assert.deepEqual(getChildren(file), [func1, suite, subParent1.asSuite]); + assert.deepEqual(getChildren(func1), []); + assert.deepEqual(getChildren(subParent1.asSuite), [func2, func3]); + assert.deepEqual(getChildren(func2), []); + assert.deepEqual(getChildren(func3), []); + assert.deepEqual(getChildren(suite), [func4, subParent2.asSuite]); + assert.deepEqual(getChildren(func4), []); + assert.deepEqual(getChildren(subParent2.asSuite), [func5, func6]); + assert.deepEqual(getChildren(func5), []); + assert.deepEqual(getChildren(func6), []); + }); +}); diff --git a/src/test/testing/common/testVisitors/resultResetVisitor.unit.test.ts b/src/test/testing/common/testVisitors/resultResetVisitor.unit.test.ts new file mode 100644 index 000000000000..cb09323e87b2 --- /dev/null +++ b/src/test/testing/common/testVisitors/resultResetVisitor.unit.test.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { TestResultResetVisitor } from '../../../../client/testing/common/testVisitors/resultResetVisitor'; +import { TestStatus } from '../../../../client/testing/common/types'; + +// tslint:disable-next-line: max-func-body-length +suite('Result reset visitor', async () => { + let resultResetVisitor: TestResultResetVisitor; + setup(() => { + resultResetVisitor = new TestResultResetVisitor(); + }); + + test('Method visitTestFunction() resets visited function nodes', async () => { + const testFunction = { + passed: true, + time: 102, + message: 'yo', + traceback: 'sd', + status: TestStatus.Fail, + functionsDidNotRun: 12, + functionsPassed: 1, + functionsFailed: 5 + }; + const expectedTestFunction = { + passed: undefined, + time: 0, + message: '', + traceback: '', + status: TestStatus.Unknown, + functionsDidNotRun: 0, + functionsPassed: 0, + functionsFailed: 0 + }; + // tslint:disable-next-line: no-any + resultResetVisitor.visitTestFunction(testFunction as any); + // tslint:disable-next-line: no-any + assert.deepEqual(testFunction, expectedTestFunction as any); + }); + + test('Method visitTestSuite() resets visited suite nodes', async () => { + const testSuite = { + passed: true, + time: 102, + status: TestStatus.Fail, + functionsDidNotRun: 12, + functionsPassed: 1, + functionsFailed: 5 + }; + const expectedTestSuite = { + passed: undefined, + time: 0, + status: TestStatus.Unknown, + functionsDidNotRun: 0, + functionsPassed: 0, + functionsFailed: 0 + }; + // tslint:disable-next-line: no-any + resultResetVisitor.visitTestSuite(testSuite as any); + // tslint:disable-next-line: no-any + assert.deepEqual(testSuite, expectedTestSuite as any); + }); + + test('Method visitTestFile() resets visited file nodes', async () => { + const testFile = { + passed: true, + time: 102, + status: TestStatus.Fail, + functionsDidNotRun: 12, + functionsPassed: 1, + functionsFailed: 5 + }; + const expectedTestFile = { + passed: undefined, + time: 0, + status: TestStatus.Unknown, + functionsDidNotRun: 0, + functionsPassed: 0, + functionsFailed: 0 + }; + // tslint:disable-next-line: no-any + resultResetVisitor.visitTestFile(testFile as any); + // tslint:disable-next-line: no-any + assert.deepEqual(testFile, expectedTestFile as any); + }); + + test('Method visitTestFolder() resets visited folder nodes', async () => { + const testFolder = { + passed: true, + time: 102, + status: TestStatus.Fail, + functionsDidNotRun: 12, + functionsPassed: 1, + functionsFailed: 5 + }; + const expectedTestFolder = { + passed: undefined, + time: 0, + status: TestStatus.Unknown, + functionsDidNotRun: 0, + functionsPassed: 0, + functionsFailed: 0 + }; + // tslint:disable-next-line: no-any + resultResetVisitor.visitTestFolder(testFolder as any); + // tslint:disable-next-line: no-any + assert.deepEqual(testFolder, expectedTestFolder as any); + }); +}); diff --git a/src/test/testing/common/trackEnablement.unit.test.ts b/src/test/testing/common/trackEnablement.unit.test.ts new file mode 100644 index 000000000000..f1d8c94eed80 --- /dev/null +++ b/src/test/testing/common/trackEnablement.unit.test.ts @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { Product } from '../../../client/common/types'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { EnablementTracker } from '../../../client/testing/common/enablementTracker'; +import { TestConfigSettingsService } from '../../../client/testing/common/services/configSettingService'; +import { TestsHelper } from '../../../client/testing/common/testUtils'; +import { TestFlatteningVisitor } from '../../../client/testing/common/testVisitors/flatteningVisitor'; +import { ITestsHelper, TestProvider } from '../../../client/testing/common/types'; +import { ITestConfigSettingsService } from '../../../client/testing/types'; +import { noop } from '../../core'; + +// tslint:disable-next-line: max-func-body-length +suite('Unit Tests - Track Enablement', () => { + const sandbox = sinon.createSandbox(); + let workspaceService: IWorkspaceService; + let configService: ITestConfigSettingsService; + let testsHelper: ITestsHelper; + let enablementTracker: EnablementTracker; + setup(() => { + sandbox.restore(); + workspaceService = mock(WorkspaceService); + configService = mock(TestConfigSettingsService); + testsHelper = new TestsHelper(instance(mock(TestFlatteningVisitor)), instance(mock(ServiceContainer))); + }); + teardown(() => { + sandbox.restore(); + }); + function createEnablementTracker() { + return new EnablementTracker(instance(workspaceService), [], instance(configService), testsHelper); + } + test('Add handler for onDidChangeConfiguration', async () => { + const stub = sinon.stub(); + when(workspaceService.onDidChangeConfiguration).thenReturn(stub); + + enablementTracker = createEnablementTracker(); + + await enablementTracker.activate(); + + assert.ok(stub.calledOnce); + }); + test('handler for onDidChangeConfiguration is onDidChangeConfiguration', async () => { + const stub = sinon.stub(); + when(workspaceService.onDidChangeConfiguration).thenReturn(stub); + + enablementTracker = createEnablementTracker(); + await enablementTracker.activate(); + + assert.equal(stub.args[0][0], enablementTracker.onDidChangeConfiguration); + assert.equal(stub.args[0][1], enablementTracker); + }); + test('If there are no workspaces and nothing changed, then do not send telemetry', async () => { + const telemetryReporter = sandbox.stub(EnablementTracker.prototype, 'sendTelemetry'); + telemetryReporter.callsFake(noop); + const affectsConfiguration = sinon.stub().returns(false); + when(workspaceService.workspaceFolders).thenReturn([]); + + enablementTracker = createEnablementTracker(); + enablementTracker.onDidChangeConfiguration({ affectsConfiguration }); + + assert.ok(telemetryReporter.notCalled); + assert.ok(affectsConfiguration.callCount > 0); + }); + test('Check whether unittest, pytest and nose settings have been enabled', async () => { + const expectedSettingsChecked = [ + 'python.testing.nosetestEnabled', + 'python.testing.unittestEnabled', + 'python.testing.pytestEnabled' + ]; + + const telemetryReporter = sandbox.stub(EnablementTracker.prototype, 'sendTelemetry'); + telemetryReporter.callsFake(noop); + const affectsConfiguration = sinon.stub().returns(false); + when(workspaceService.workspaceFolders).thenReturn([]); + when(configService.getTestEnablingSetting(Product.unittest)).thenReturn('testing.unittestEnabled'); + when(configService.getTestEnablingSetting(Product.pytest)).thenReturn('testing.pytestEnabled'); + when(configService.getTestEnablingSetting(Product.nosetest)).thenReturn('testing.nosetestEnabled'); + + enablementTracker = createEnablementTracker(); + enablementTracker.onDidChangeConfiguration({ affectsConfiguration }); + + verify(workspaceService.getConfiguration(anything(), anything())).never(); + assert.ok(telemetryReporter.notCalled); + assert.ok(affectsConfiguration.callCount > 0); + const settingsChecked = [ + affectsConfiguration.args[0][0], + affectsConfiguration.args[1][0], + affectsConfiguration.args[2][0] + ]; + assert.deepEqual(settingsChecked.sort(), expectedSettingsChecked.sort()); + }); + test('Check settings related to unittest, pytest and nose', async () => { + const expectedSettingsChecked = [ + 'python.testing.nosetestEnabled', + 'python.testing.unittestEnabled', + 'python.testing.pytestEnabled' + ]; + const expectedSettingsRetrieved = [ + 'testing.nosetestEnabled', + 'testing.unittestEnabled', + 'testing.pytestEnabled' + ]; + + const telemetryReporter = sandbox.stub(EnablementTracker.prototype, 'sendTelemetry'); + telemetryReporter.callsFake(noop); + const affectsConfiguration = sinon.stub().returns(true); + const getConfigSettings = sinon.stub<[string], boolean>().returns(false); + + when(workspaceService.workspaceFolders).thenReturn([]); + // tslint:disable-next-line: no-any + when(workspaceService.getConfiguration('python', anything())).thenReturn({ get: getConfigSettings } as any); + when(configService.getTestEnablingSetting(Product.unittest)).thenReturn('testing.unittestEnabled'); + when(configService.getTestEnablingSetting(Product.pytest)).thenReturn('testing.pytestEnabled'); + when(configService.getTestEnablingSetting(Product.nosetest)).thenReturn('testing.nosetestEnabled'); + + enablementTracker = createEnablementTracker(); + enablementTracker.onDidChangeConfiguration({ affectsConfiguration }); + + verify(workspaceService.getConfiguration(anything(), anything())).atLeast(3); + assert.ok(telemetryReporter.notCalled); + assert.ok(affectsConfiguration.callCount > 0); + const settingsChecked = [ + affectsConfiguration.args[0][0], + affectsConfiguration.args[1][0], + affectsConfiguration.args[2][0] + ]; + assert.deepEqual(settingsChecked.sort(), expectedSettingsChecked.sort()); + + const settingsRetrieved = [ + getConfigSettings.args[0][0], + getConfigSettings.args[1][0], + getConfigSettings.args[2][0] + ]; + assert.deepEqual(settingsRetrieved.sort(), expectedSettingsRetrieved.sort()); + }); + function testSendingTelemetry(sendForProvider: TestProvider) { + const expectedSettingsChecked = [ + 'python.testing.nosetestEnabled', + 'python.testing.unittestEnabled', + 'python.testing.pytestEnabled' + ]; + const expectedSettingsRetrieved = [ + 'testing.nosetestEnabled', + 'testing.unittestEnabled', + 'testing.pytestEnabled' + ]; + + const telemetryReporter = sandbox.stub(EnablementTracker.prototype, 'sendTelemetry'); + telemetryReporter.callsFake(noop); + const affectsConfiguration = sinon.stub().returns(true); + const getConfigSettings = sinon + .stub<[string], boolean>() + .callsFake((setting) => setting.includes(sendForProvider)); + + when(workspaceService.workspaceFolders).thenReturn([]); + // tslint:disable-next-line: no-any + when(workspaceService.getConfiguration('python', anything())).thenReturn({ get: getConfigSettings } as any); + when(configService.getTestEnablingSetting(Product.unittest)).thenReturn('testing.unittestEnabled'); + when(configService.getTestEnablingSetting(Product.pytest)).thenReturn('testing.pytestEnabled'); + when(configService.getTestEnablingSetting(Product.nosetest)).thenReturn('testing.nosetestEnabled'); + + enablementTracker = createEnablementTracker(); + enablementTracker.onDidChangeConfiguration({ affectsConfiguration }); + + verify(workspaceService.getConfiguration(anything(), anything())).atLeast(3); + assert.equal(telemetryReporter.callCount, 1); + assert.deepEqual(telemetryReporter.args[0][0], { [sendForProvider]: true }); + assert.ok(affectsConfiguration.callCount > 0); + const settingsChecked = [ + affectsConfiguration.args[0][0], + affectsConfiguration.args[1][0], + affectsConfiguration.args[2][0] + ]; + assert.deepEqual(settingsChecked.sort(), expectedSettingsChecked.sort()); + + const settingsRetrieved = [ + getConfigSettings.args[0][0], + getConfigSettings.args[1][0], + getConfigSettings.args[2][0] + ]; + assert.deepEqual(settingsRetrieved.sort(), expectedSettingsRetrieved.sort()); + } + test('Send telemetry for unittest', () => testSendingTelemetry('unittest')); + test('Send telemetry for pytest', () => testSendingTelemetry('pytest')); + test('Send telemetry for nosetest', () => testSendingTelemetry('nosetest')); +}); diff --git a/src/test/testing/common/xUnitParser.unit.test.ts b/src/test/testing/common/xUnitParser.unit.test.ts new file mode 100644 index 000000000000..5b8498acb663 --- /dev/null +++ b/src/test/testing/common/xUnitParser.unit.test.ts @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length + +'use strict'; + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { IXUnitParser, Tests, TestStatus } from '../../../client/testing/common/types'; +import { XUnitParser } from '../../../client/testing/common/xUnitParser'; +import { createDeclaratively, createEmptyResults, TestItem } from '../results'; + +suite('Testing - parse JUnit XML file', () => { + let parser: IXUnitParser; + let fs: typeMoq.IMock<IFileSystem>; + setup(() => { + fs = typeMoq.Mock.ofType<IFileSystem>(undefined, typeMoq.MockBehavior.Strict); + parser = new XUnitParser(fs.object); + }); + + function fixResult(node: TestItem, file: string, line: number) { + switch (node.status) { + case TestStatus.Pass: + node.passed = true; + break; + case TestStatus.Fail: + case TestStatus.Error: + node.passed = false; + break; + default: + node.passed = undefined; + } + node.file = file; + node.line = line; + } + + test('legacy - success with single passing test', async () => { + const tests = createDeclaratively(` + ./ + test_spam.py + <Tests> + test_spam + `); + const expected = createDeclaratively(` + ./ + test_spam.py + <Tests> + test_spam P 1.001 + `); + fixResult(expected.testFunctions[0].testFunction, 'test_spam.py', 3); + const filename = 'x/y/z/results.xml'; + fs.setup((f) => f.readFile(filename)).returns(() => + Promise.resolve(` + <?xml version="1.0" encoding="utf-8"?> + <testsuite errors="0" failures="0" hostname="linux-desktop" name="pytest" skipped="0" tests="1" time="1.011" timestamp="2019-08-29T15:59:08.757654"> + <testcase classname="test_spam.Tests" file="test_spam.py" line="3" name="test_spam" time="1.001"> + </testcase> + </testsuite> + `) + ); + + await parser.updateResultsFromXmlLogFile(tests, filename); + + expect(tests).to.deep.equal(expected); + fs.verifyAll(); + }); + + test('success with single passing test', async () => { + const tests = createDeclaratively(` + ./ + test_spam.py + <Tests> + test_spam + `); + const expected = createDeclaratively(` + ./ + test_spam.py + <Tests> + test_spam P 0.001 + `); + fixResult(expected.testFunctions[0].testFunction, 'test_spam.py', 3); + const filename = 'x/y/z/results.xml'; + fs.setup((f) => f.readFile(filename)).returns(() => + Promise.resolve(` + <?xml version="1.0" encoding="utf-8"?> + <testsuites> + <testsuite errors="0" failures="0" hostname="vm-dev-linux-desktop" name="pytest" skipped="0" tests="1" time="0.011" timestamp="2019-09-05T17:17:35.868863"> + <testcase classname="test_spam.Tests" file="test_spam.py" line="3" name="test_spam" time="0.001"> + </testcase> + </testsuite> + </testsuites> + `) + ); + + await parser.updateResultsFromXmlLogFile(tests, filename); + + expect(tests).to.deep.equal(expected); + fs.verifyAll(); + }); + + test('no discovered tests', async () => { + const tests: Tests = createEmptyResults(); + const expected: Tests = createEmptyResults(); + expected.summary.passed = 1; // That's a little strange... + const filename = 'x/y/z/results.xml'; + fs.setup((f) => f.readFile(filename)).returns(() => + Promise.resolve(` + <?xml version="1.0" encoding="utf-8"?> + <testsuite errors="0" failures="0" hostname="linux-desktop" name="pytest" skipped="0" tests="1" time="0.011" timestamp="2019-08-29T15:59:08.757654"> + <testcase classname="test_spam.Tests" file="test_spam.py" line="3" name="test_spam" time="0.001"> + </testcase> + </testsuite> + `) + ); + + await parser.updateResultsFromXmlLogFile(tests, filename); + + expect(tests).to.deep.equal(expected); + fs.verifyAll(); + }); + + test('no tests run', async () => { + const tests: Tests = createEmptyResults(); + const expected: Tests = createEmptyResults(); + const filename = 'x/y/z/results.xml'; + fs.setup((f) => f.readFile(filename)).returns(() => + Promise.resolve(` + <?xml version="1.0" encoding="utf-8"?> + <testsuite errors="0" failures="0" hostname="linux-desktop" name="pytest" skipped="0" tests="0" time="0.011" timestamp="2019-08-29T15:59:08.757654"> + </testsuite> + `) + ); + + await parser.updateResultsFromXmlLogFile(tests, filename); + + expect(tests).to.deep.equal(expected); + fs.verifyAll(); + }); + + // Missing tests (see https://github.com/microsoft/vscode-python/issues/7447): + // * simple pytest + // * simple nose + // * complex + // * error + // * failure + // * skipped + // * no clobber old if not matching + // * ... +}); diff --git a/src/test/testing/configuration.unit.test.ts b/src/test/testing/configuration.unit.test.ts new file mode 100644 index 000000000000..fcd254ec2995 --- /dev/null +++ b/src/test/testing/configuration.unit.test.ts @@ -0,0 +1,518 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri, WorkspaceConfiguration } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; +import { + IConfigurationService, + IInstaller, + IOutputChannel, + IPythonSettings, + ITestingSettings, + Product +} from '../../client/common/types'; +import { getNamesAndValues } from '../../client/common/utils/enum'; +import { IServiceContainer } from '../../client/ioc/types'; +import { TEST_OUTPUT_CHANNEL, UNIT_TEST_PRODUCTS } from '../../client/testing/common/constants'; +import { TestsHelper } from '../../client/testing/common/testUtils'; +import { TestFlatteningVisitor } from '../../client/testing/common/testVisitors/flatteningVisitor'; +import { ITestsHelper } from '../../client/testing/common/types'; +import { UnitTestConfigurationService } from '../../client/testing/configuration'; +import { + ITestConfigSettingsService, + ITestConfigurationManager, + ITestConfigurationManagerFactory +} from '../../client/testing/types'; + +suite('Unit Tests - ConfigurationService', () => { + UNIT_TEST_PRODUCTS.forEach((product) => { + const prods = getNamesAndValues(Product); + const productName = prods.filter((item) => item.value === product)[0]; + const workspaceUri = Uri.file(__filename); + suite(productName.name, () => { + let testConfigService: typeMoq.IMock<UnitTestConfigurationService>; + let workspaceService: typeMoq.IMock<IWorkspaceService>; + let factory: typeMoq.IMock<ITestConfigurationManagerFactory>; + let testSettingsService: typeMoq.IMock<ITestConfigSettingsService>; + let appShell: typeMoq.IMock<IApplicationShell>; + let unitTestSettings: typeMoq.IMock<ITestingSettings>; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(undefined, typeMoq.MockBehavior.Strict); + const configurationService = typeMoq.Mock.ofType<IConfigurationService>( + undefined, + typeMoq.MockBehavior.Strict + ); + appShell = typeMoq.Mock.ofType<IApplicationShell>(undefined, typeMoq.MockBehavior.Strict); + const outputChannel = typeMoq.Mock.ofType<OutputChannel>(undefined, typeMoq.MockBehavior.Strict); + const installer = typeMoq.Mock.ofType<IInstaller>(undefined, typeMoq.MockBehavior.Strict); + workspaceService = typeMoq.Mock.ofType<IWorkspaceService>(undefined, typeMoq.MockBehavior.Strict); + factory = typeMoq.Mock.ofType<ITestConfigurationManagerFactory>(undefined, typeMoq.MockBehavior.Strict); + testSettingsService = typeMoq.Mock.ofType<ITestConfigSettingsService>( + undefined, + typeMoq.MockBehavior.Strict + ); + unitTestSettings = typeMoq.Mock.ofType<ITestingSettings>(); + const pythonSettings = typeMoq.Mock.ofType<IPythonSettings>(undefined, typeMoq.MockBehavior.Strict); + + pythonSettings.setup((p) => p.testing).returns(() => unitTestSettings.object); + configurationService.setup((c) => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); + + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))) + .returns(() => outputChannel.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IApplicationShell))) + .returns(() => appShell.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(ITestConfigurationManagerFactory))) + .returns(() => factory.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(ITestConfigSettingsService))) + .returns(() => testSettingsService.object); + const commands = typeMoq.Mock.ofType<ICommandManager>(undefined, typeMoq.MockBehavior.Strict); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(ICommandManager))) + .returns(() => commands.object); + const flattener = typeMoq.Mock.ofType<TestFlatteningVisitor>(undefined, typeMoq.MockBehavior.Strict); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(ITestsHelper))) + .returns(() => new TestsHelper(flattener.object, serviceContainer.object)); + testConfigService = typeMoq.Mock.ofType( + UnitTestConfigurationService, + typeMoq.MockBehavior.Loose, + true, + serviceContainer.object + ); + }); + test('Enable Test when setting testing.promptToConfigure is enabled', async () => { + const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>( + undefined, + typeMoq.MockBehavior.Strict + ); + configMgr + .setup((c) => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory + .setup((f) => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( + undefined, + typeMoq.MockBehavior.Strict + ); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig + .setup((w) => w.get(typeMoq.It.isValue('testing.promptToConfigure'))) + .returns(() => true) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.enableTest(workspaceUri, product); + + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Enable Test when setting testing.promptToConfigure is disabled', async () => { + const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>( + undefined, + typeMoq.MockBehavior.Strict + ); + configMgr + .setup((c) => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory + .setup((f) => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( + undefined, + typeMoq.MockBehavior.Strict + ); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig + .setup((w) => w.get(typeMoq.It.isValue('testing.promptToConfigure'))) + .returns(() => false) + .verifiable(typeMoq.Times.once()); + + workspaceConfig + .setup((w) => + w.update(typeMoq.It.isValue('testing.promptToConfigure'), typeMoq.It.isValue(undefined)) + ) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.enableTest(workspaceUri, product); + + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Enable Test when setting testing.promptToConfigure is disabled and fail to update the settings', async () => { + const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>( + undefined, + typeMoq.MockBehavior.Strict + ); + configMgr + .setup((c) => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory + .setup((f) => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( + undefined, + typeMoq.MockBehavior.Strict + ); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig + .setup((w) => w.get(typeMoq.It.isValue('testing.promptToConfigure'))) + .returns(() => false) + .verifiable(typeMoq.Times.once()); + + const errorMessage = 'Update Failed'; + const updateFailError = new Error(errorMessage); + workspaceConfig + .setup((w) => + w.update(typeMoq.It.isValue('testing.promptToConfigure'), typeMoq.It.isValue(undefined)) + ) + .returns(() => Promise.reject(updateFailError)) + .verifiable(typeMoq.Times.once()); + + const promise = testConfigService.target.enableTest(workspaceUri, product); + + await expect(promise).to.eventually.be.rejectedWith(errorMessage); + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Select Test runner displays 3 items', async () => { + const placeHolder = 'Some message'; + appShell + .setup((s) => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + .callback((items) => expect(items).be.lengthOf(3)) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.selectTestRunner(placeHolder); + appShell.verifyAll(); + }); + test('Ensure selected item is returned', async () => { + const placeHolder = 'Some message'; + const indexes = [Product.unittest, Product.pytest, Product.nosetest]; + appShell + .setup((s) => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + .callback((items) => expect(items).be.lengthOf(3)) + .returns((items) => items[indexes.indexOf(product)]) + .verifiable(typeMoq.Times.once()); + + const selectedItem = await testConfigService.target.selectTestRunner(placeHolder); + expect(selectedItem).to.be.equal(product); + appShell.verifyAll(); + }); + test('Ensure undefined is returned when nothing is selected', async () => { + const placeHolder = 'Some message'; + appShell + .setup((s) => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + const selectedItem = await testConfigService.target.selectTestRunner(placeHolder); + expect(selectedItem).to.be.equal(undefined, 'invalid value'); + appShell.verifyAll(); + }); + test('Prompt to enable a test if a test framework is not enabled', async () => { + unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); + unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); + unitTestSettings.setup((u) => u.nosetestsEnabled).returns(() => false); + + appShell + .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + let exceptionThrown = false; + try { + await testConfigService.target.displayTestFrameworkError(workspaceUri); + } catch (exc) { + if (exc !== null) { + throw exc; + } + exceptionThrown = true; + } + + expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); + appShell.verifyAll(); + }); + test('Prompt to select a test if a test framework is not enabled', async () => { + unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); + unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); + unitTestSettings.setup((u) => u.nosetestsEnabled).returns(() => false); + + appShell + .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_msg, option) => Promise.resolve(option)) + .verifiable(typeMoq.Times.once()); + + let exceptionThrown = false; + let selectTestRunnerInvoked = false; + try { + testConfigService.callBase = false; + testConfigService + .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) + .returns(() => { + selectTestRunnerInvoked = true; + return Promise.resolve(undefined); + }); + await testConfigService.target.displayTestFrameworkError(workspaceUri); + } catch (exc) { + if (exc !== null) { + throw exc; + } + exceptionThrown = true; + } + + expect(selectTestRunnerInvoked).to.be.equal(true, 'Method not invoked'); + expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); + appShell.verifyAll(); + }); + test('Configure selected test framework and disable others', async () => { + unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); + unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); + unitTestSettings.setup((u) => u.nosetestsEnabled).returns(() => false); + + const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( + undefined, + typeMoq.MockBehavior.Strict + ); + workspaceConfig + .setup((w) => w.get(typeMoq.It.isAny())) + .returns(() => true) + .verifiable(typeMoq.Times.once()); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + appShell + .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_msg, option) => Promise.resolve(option)) + .verifiable(typeMoq.Times.once()); + + let selectTestRunnerInvoked = false; + testConfigService.callBase = false; + testConfigService + .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) + .returns(() => { + selectTestRunnerInvoked = true; + return Promise.resolve(product as any); + }); + + const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>( + undefined, + typeMoq.MockBehavior.Strict + ); + factory + .setup((f) => + f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny()) + ) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + configMgr + .setup((c) => c.configure(typeMoq.It.isValue(workspaceUri))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + configMgr + .setup((c) => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.displayTestFrameworkError(workspaceUri); + + expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); + appShell.verifyAll(); + factory.verifyAll(); + configMgr.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('If more than one test framework is enabled, then prompt to select a test framework', async () => { + unitTestSettings.setup((u) => u.pytestEnabled).returns(() => true); + unitTestSettings.setup((u) => u.unittestEnabled).returns(() => true); + unitTestSettings.setup((u) => u.nosetestsEnabled).returns(() => true); + + appShell + .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.never()); + appShell + .setup((s) => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + let exceptionThrown = false; + try { + await testConfigService.target.displayTestFrameworkError(workspaceUri); + } catch (exc) { + if (exc !== null) { + throw exc; + } + exceptionThrown = true; + } + + expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); + appShell.verifyAll(); + }); + test('If more than one test framework is enabled, then prompt to select a test framework and enable test, but do not configure', async () => { + unitTestSettings.setup((u) => u.pytestEnabled).returns(() => true); + unitTestSettings.setup((u) => u.unittestEnabled).returns(() => true); + unitTestSettings.setup((u) => u.nosetestsEnabled).returns(() => true); + + appShell + .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns((_msg, option) => Promise.resolve(option)) + .verifiable(typeMoq.Times.never()); + + let selectTestRunnerInvoked = false; + testConfigService.callBase = false; + testConfigService + .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) + .returns(() => { + selectTestRunnerInvoked = true; + return Promise.resolve(product as any); + }); + + let enableTestInvoked = false; + testConfigService + .setup((t) => t.enableTest(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) + .returns(() => { + enableTestInvoked = true; + return Promise.resolve(); + }); + + const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>( + undefined, + typeMoq.MockBehavior.Strict + ); + factory + .setup((f) => + f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny()) + ) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + configMgr + .setup((c) => c.configure(typeMoq.It.isValue(workspaceUri))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.never()); + configMgr + .setup((c) => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.displayTestFrameworkError(workspaceUri); + + expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); + expect(enableTestInvoked).to.be.equal(false, 'Enable Test is invoked'); + factory.verifyAll(); + appShell.verifyAll(); + configMgr.verifyAll(); + }); + + test('Prompt to enable and configure selected test framework', async () => { + unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); + unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); + unitTestSettings.setup((u) => u.nosetestsEnabled).returns(() => false); + + const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( + undefined, + typeMoq.MockBehavior.Strict + ); + workspaceConfig + .setup((w) => w.get(typeMoq.It.isAny())) + .returns(() => true) + .verifiable(typeMoq.Times.once()); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + appShell + .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .verifiable(typeMoq.Times.never()); + + let selectTestRunnerInvoked = false; + testConfigService.callBase = false; + testConfigService + .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) + .returns(() => { + selectTestRunnerInvoked = true; + return Promise.resolve(product as any); + }); + + const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>(); + factory + .setup((f) => + f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny()) + ) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + configMgr + .setup((c) => c.configure(typeMoq.It.isValue(workspaceUri))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + configMgr + .setup((c) => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + const configManagersToVerify: typeof configMgr[] = [configMgr]; + + await testConfigService.target.promptToEnableAndConfigureTestFramework(workspaceUri); + + expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); + appShell.verifyAll(); + factory.verifyAll(); + for (const item of configManagersToVerify) { + item.verifyAll(); + } + workspaceConfig.verifyAll(); + }); + }); + }); +}); diff --git a/src/test/testing/configurationFactory.unit.test.ts b/src/test/testing/configurationFactory.unit.test.ts new file mode 100644 index 000000000000..d19484cdb32b --- /dev/null +++ b/src/test/testing/configurationFactory.unit.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; +import { IInstaller, IOutputChannel, Product } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { TEST_OUTPUT_CHANNEL } from '../../client/testing/common/constants'; +import { TestConfigurationManagerFactory } from '../../client/testing/configurationFactory'; +import * as nose from '../../client/testing/nosetest/testConfigurationManager'; +import * as pytest from '../../client/testing/pytest/testConfigurationManager'; +import { ITestConfigSettingsService, ITestConfigurationManagerFactory } from '../../client/testing/types'; +import * as unittest from '../../client/testing/unittest/testConfigurationManager'; + +use(chaiAsPromised); + +suite('Unit Tests - ConfigurationManagerFactory', () => { + let factory: ITestConfigurationManagerFactory; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + const outputChannel = typeMoq.Mock.ofType<OutputChannel>(); + const installer = typeMoq.Mock.ofType<IInstaller>(); + const testConfigService = typeMoq.Mock.ofType<ITestConfigSettingsService>(); + + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))) + .returns(() => outputChannel.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(ITestConfigSettingsService))) + .returns(() => testConfigService.object); + factory = new TestConfigurationManagerFactory(serviceContainer.object); + }); + test('Create Unit Test Configuration', async () => { + const configMgr = factory.create(Uri.file(__filename), Product.unittest); + expect(configMgr).to.be.instanceOf(unittest.ConfigurationManager); + }); + test('Create pytest Configuration', async () => { + const configMgr = factory.create(Uri.file(__filename), Product.pytest); + expect(configMgr).to.be.instanceOf(pytest.ConfigurationManager); + }); + test('Create nose Configuration', async () => { + const configMgr = factory.create(Uri.file(__filename), Product.nosetest); + expect(configMgr).to.be.instanceOf(nose.ConfigurationManager); + }); +}); diff --git a/src/test/testing/debugger.test.ts b/src/test/testing/debugger.test.ts new file mode 100644 index 000000000000..2b8172b2a527 --- /dev/null +++ b/src/test/testing/debugger.test.ts @@ -0,0 +1,260 @@ +import { assert, expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import { ConfigurationTarget } from 'vscode'; +import { createDeferred } from '../../client/common/utils/async'; +import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; +import { InterpreterService } from '../../client/interpreter/interpreterService'; +import { CondaService } from '../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { TestManagerRunner as NoseTestManagerRunner } from '../../client/testing//nosetest/runner'; +import { TestManagerRunner as PytestManagerRunner } from '../../client/testing//pytest/runner'; +import { TestManagerRunner as UnitTestTestManagerRunner } from '../../client/testing//unittest/runner'; +import { ArgumentsHelper } from '../../client/testing/common/argumentsHelper'; +import { + CANCELLATION_REASON, + CommandSource, + NOSETEST_PROVIDER, + PYTEST_PROVIDER, + UNITTEST_PROVIDER +} from '../../client/testing/common/constants'; +import { TestRunner } from '../../client/testing/common/runner'; +import { + ITestDebugLauncher, + ITestManagerFactory, + ITestMessageService, + ITestRunner, + IXUnitParser, + TestProvider +} from '../../client/testing/common/types'; +import { XUnitParser } from '../../client/testing/common/xUnitParser'; +import { ArgumentsService as NoseTestArgumentsService } from '../../client/testing/nosetest/services/argsService'; +import { ArgumentsService as PyTestArgumentsService } from '../../client/testing/pytest/services/argsService'; +import { TestMessageService } from '../../client/testing/pytest/services/testMessageService'; +import { IArgumentsHelper, IArgumentsService, ITestManagerRunner, IUnitTestHelper } from '../../client/testing/types'; +import { UnitTestHelper } from '../../client/testing/unittest/helper'; +import { ArgumentsService as UnitTestArgumentsService } from '../../client/testing/unittest/services/argsService'; +import { deleteDirectory, rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_TIMEOUT } from './../initialize'; +import { MockDebugLauncher } from './mocks'; +import { UnitTestIocContainer } from './serviceRegistry'; + +use(chaiAsPromised); + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); +const defaultUnitTestArgs = ['-v', '-s', '.', '-p', '*test*.py']; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests - debugging', () => { + let ioc: UnitTestIocContainer; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(async function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(TEST_TIMEOUT * 2); + // Test discovery is where the delay is, hence give 10 seconds (as we discover tests at least twice in each test). + await initialize(); + await Promise.all([ + updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget), + updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget), + updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget) + ]); + }); + setup(async function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(TEST_TIMEOUT * 2); // This hook requires more timeout as we're deleting files as well + await deleteDirectory(path.join(testFilesPath, '.cache')); + await initializeTest(); + initializeDI(); + }); + teardown(async function () { + // It's been observed that each call to `updateSetting` can take upto 20 seconds on Windows, hence increasing timeout. + // tslint:disable-next-line:no-invalid-this + this.timeout(TEST_TIMEOUT * 3); + await ioc.dispose(); + await Promise.all([ + updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget), + updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget), + updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget) + ]); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerProcessTypes(); + ioc.registerVariableTypes(); + + ioc.registerTestParsers(); + ioc.registerTestVisitors(); + ioc.registerTestDiscoveryServices(); + ioc.registerTestDiagnosticServices(); + ioc.registerTestResultsHelper(); + ioc.registerTestStorage(); + ioc.registerTestsHelper(); + ioc.registerTestManagers(); + ioc.registerMockUnitTestSocketServer(); + ioc.registerInterpreterStorageTypes(); + ioc.registerMockInterpreterTypes(); + ioc.serviceManager.add<IArgumentsHelper>(IArgumentsHelper, ArgumentsHelper); + ioc.serviceManager.add<ITestRunner>(ITestRunner, TestRunner); + ioc.serviceManager.add<IXUnitParser>(IXUnitParser, XUnitParser); + ioc.serviceManager.add<IUnitTestHelper>(IUnitTestHelper, UnitTestHelper); + ioc.serviceManager.add<IArgumentsService>(IArgumentsService, NoseTestArgumentsService, NOSETEST_PROVIDER); + ioc.serviceManager.add<IArgumentsService>(IArgumentsService, PyTestArgumentsService, PYTEST_PROVIDER); + ioc.serviceManager.add<IArgumentsService>(IArgumentsService, UnitTestArgumentsService, UNITTEST_PROVIDER); + ioc.serviceManager.add<ITestManagerRunner>(ITestManagerRunner, PytestManagerRunner, PYTEST_PROVIDER); + ioc.serviceManager.add<ITestManagerRunner>(ITestManagerRunner, NoseTestManagerRunner, NOSETEST_PROVIDER); + ioc.serviceManager.add<ITestManagerRunner>(ITestManagerRunner, UnitTestTestManagerRunner, UNITTEST_PROVIDER); + ioc.serviceManager.addSingleton<ITestDebugLauncher>(ITestDebugLauncher, MockDebugLauncher); + ioc.serviceManager.addSingleton<ITestMessageService>(ITestMessageService, TestMessageService, PYTEST_PROVIDER); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, instance(mock(CondaService))); + ioc.serviceManager.rebindInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); + } + + async function testStartingDebugger(testProvider: TestProvider) { + const testManager = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory)( + testProvider, + rootWorkspaceUri!, + testFilesPath + ); + const mockDebugLauncher = ioc.serviceContainer.get<MockDebugLauncher>(ITestDebugLauncher); + const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + + const deferred = createDeferred<string>(); + const testFunction = [tests.testFunctions[0].testFunction]; + const runningPromise = testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); + + // This promise should never resolve nor reject. + runningPromise + .then(() => deferred.reject("Debugger stopped when it shouldn't have")) + .catch((error) => deferred.reject(error)); + + mockDebugLauncher.launched + .then((launched) => { + if (launched) { + deferred.resolve(''); + } else { + deferred.reject('Debugger not launched'); + } + }) + .catch((error) => deferred.reject(error)); + + await deferred.promise; + } + + test('Debugger should start (unittest)', async () => { + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + await testStartingDebugger('unittest'); + }); + + test('Debugger should start (pytest)', async () => { + await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + await testStartingDebugger('pytest'); + }); + + test('Debugger should start (nosetest)', async () => { + await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + await testStartingDebugger('nosetest'); + }); + + async function testStoppingDebugger(testProvider: TestProvider) { + const testManager = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory)( + testProvider, + rootWorkspaceUri!, + testFilesPath + ); + const mockDebugLauncher = ioc.serviceContainer.get<MockDebugLauncher>(ITestDebugLauncher); + const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + + const testFunction = [tests.testFunctions[0].testFunction]; + const runningPromise = testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); + const launched = await mockDebugLauncher.launched; + assert.isTrue(launched, 'Debugger not launched'); + + const discoveryPromise = testManager.discoverTests(CommandSource.commandPalette, true, true, true); + await expect(runningPromise).to.be.rejectedWith( + CANCELLATION_REASON, + 'Incorrect reason for ending the debugger' + ); + await ioc.dispose(); // will cancel test discovery + await expect(discoveryPromise).to.be.rejectedWith( + CANCELLATION_REASON, + 'Incorrect reason for ending the debugger' + ); + } + + test('Debugger should stop when user invokes a test discovery (unittest)', async () => { + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + await testStoppingDebugger('unittest'); + }); + + test('Debugger should stop when user invokes a test discovery (pytest)', async () => { + await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + await testStoppingDebugger('pytest'); + }); + + test('Debugger should stop when user invokes a test discovery (nosetest)', async () => { + await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + await testStoppingDebugger('nosetest'); + }); + + async function testDebuggerWhenRediscoveringTests(testProvider: TestProvider) { + const testManager = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory)( + testProvider, + rootWorkspaceUri!, + testFilesPath + ); + const mockDebugLauncher = ioc.serviceContainer.get<MockDebugLauncher>(ITestDebugLauncher); + const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + + const testFunction = [tests.testFunctions[0].testFunction]; + const runningPromise = testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); + const launched = await mockDebugLauncher.launched; + assert.isTrue(launched, 'Debugger not launched'); + + const discoveryPromise = testManager.discoverTests(CommandSource.commandPalette, false, true); + const deferred = createDeferred<string>(); + + discoveryPromise + // tslint:disable-next-line:no-unsafe-any + .then(() => deferred.resolve('')) + // tslint:disable-next-line:no-unsafe-any + .catch((ex) => deferred.reject(ex)); + + // This promise should never resolve nor reject. + runningPromise + .then(() => "Debugger stopped when it shouldn't have") + .catch(() => "Debugger crashed when it shouldn't have") + // tslint:disable-next-line: no-floating-promises + .then((error) => { + deferred.reject(error); + }); + + // Should complete without any errors + await deferred.promise; + } + + test('Debugger should not stop when test discovery is invoked automatically by extension (unittest)', async () => { + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + await testDebuggerWhenRediscoveringTests('unittest'); + }); + + test('Debugger should not stop when test discovery is invoked automatically by extension (pytest)', async () => { + await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + await testDebuggerWhenRediscoveringTests('pytest'); + }); + + test('Debugger should not stop when test discovery is invoked automatically by extension (nosetest)', async () => { + await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + await testDebuggerWhenRediscoveringTests('nosetest'); + }); +}); diff --git a/src/test/testing/display/main.unit.test.ts b/src/test/testing/display/main.unit.test.ts new file mode 100644 index 000000000000..d529dc25b183 --- /dev/null +++ b/src/test/testing/display/main.unit.test.ts @@ -0,0 +1,516 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { StatusBarItem, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import '../../../client/common/extensions'; +import { IConfigurationService, IPythonSettings, ITestingSettings } from '../../../client/common/types'; +import { createDeferred } from '../../../client/common/utils/async'; +import { Testing } from '../../../client/common/utils/localize'; +import { noop } from '../../../client/common/utils/misc'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CANCELLATION_REASON } from '../../../client/testing/common/constants'; +import { ITestsHelper, Tests } from '../../../client/testing/common/types'; +import { TestResultDisplay } from '../../../client/testing/display/main'; +import { sleep } from '../../core'; + +suite('Unit Tests - TestResultDisplay', () => { + const workspaceUri = Uri.file(__filename); + let appShell: typeMoq.IMock<IApplicationShell>; + let unitTestSettings: typeMoq.IMock<ITestingSettings>; + let serviceContainer: typeMoq.IMock<IServiceContainer>; + let display: TestResultDisplay; + let testsHelper: typeMoq.IMock<ITestsHelper>; + let configurationService: typeMoq.IMock<IConfigurationService>; + let cmdManager: typeMoq.IMock<ICommandManager>; + setup(() => { + serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + configurationService = typeMoq.Mock.ofType<IConfigurationService>(); + appShell = typeMoq.Mock.ofType<IApplicationShell>(); + unitTestSettings = typeMoq.Mock.ofType<ITestingSettings>(); + const pythonSettings = typeMoq.Mock.ofType<IPythonSettings>(); + testsHelper = typeMoq.Mock.ofType<ITestsHelper>(); + cmdManager = typeMoq.Mock.ofType<ICommandManager>(); + + pythonSettings.setup((p) => p.testing).returns(() => unitTestSettings.object); + configurationService.setup((c) => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); + + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(ITestsHelper))).returns(() => testsHelper.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(ICommandManager))).returns(() => cmdManager.object); + }); + teardown(() => { + try { + display.dispose(); + } catch { + noop(); + } + }); + function createTestResultDisplay() { + display = new TestResultDisplay(serviceContainer.object); + } + test('Should create a status bar item upon instantiation', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + appShell.verifyAll(); + }); + test('Should be disabled upon instantiation', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + appShell.verifyAll(); + expect(display.enabled).to.be.equal(false, 'not disabled'); + }); + test('Enable display should show the statusbar', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.enabled = true; + statusBar.verifyAll(); + }); + test('Disable display should hide the statusbar', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.hide()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.enabled = false; + statusBar.verifyAll(); + }); + test('Ensure status bar is displayed and updated with progress with ability to stop tests', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.displayProgressStatus(createDeferred<Tests>().promise, false); + + statusBar.verifyAll(); + statusBar.verify( + (s) => (s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test)), + typeMoq.Times.atLeastOnce() + ); + statusBar.verify((s) => (s.text = typeMoq.It.isValue('$(stop) Running Tests')), typeMoq.Times.atLeastOnce()); + }); + test('Ensure status bar is updated with success with ability to view ui without any results', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred<Tests>(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify( + (s) => (s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test)), + typeMoq.Times.atLeastOnce() + ); + statusBar.verify((s) => (s.text = typeMoq.It.isValue('$(stop) Running Tests')), typeMoq.Times.atLeastOnce()); + + const tests = typeMoq.Mock.ofType<Tests>(); + tests.setup((t: any) => t.then).returns(() => undefined); + tests + .setup((t) => t.summary) + .returns(() => { + return { errors: 0, failures: 0, passed: 0, skipped: 0 }; + }) + .verifiable(typeMoq.Times.atLeastOnce()); + + appShell + .setup((a) => + a.showWarningMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + def.resolve(tests.object); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify((s) => (s.command = typeMoq.It.isValue(Commands.Tests_View_UI)), typeMoq.Times.atLeastOnce()); + }); + test('Ensure status bar is updated with success with ability to view ui with results', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred<Tests>(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify( + (s) => (s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test)), + typeMoq.Times.atLeastOnce() + ); + statusBar.verify((s) => (s.text = typeMoq.It.isValue('$(stop) Running Tests')), typeMoq.Times.atLeastOnce()); + + const tests = typeMoq.Mock.ofType<Tests>(); + tests.setup((t: any) => t.then).returns(() => undefined); + tests + .setup((t) => t.summary) + .returns(() => { + return { errors: 0, failures: 0, passed: 1, skipped: 0 }; + }) + .verifiable(typeMoq.Times.atLeastOnce()); + + appShell + .setup((a) => + a.showWarningMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.never()); + + def.resolve(tests.object); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify((s) => (s.command = typeMoq.It.isValue(Commands.Tests_View_UI)), typeMoq.Times.atLeastOnce()); + }); + test('Ensure status bar is updated with error when cancelled by user with ability to view ui with results', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred<Tests>(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify( + (s) => (s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test)), + typeMoq.Times.atLeastOnce() + ); + statusBar.verify((s) => (s.text = typeMoq.It.isValue('$(stop) Running Tests')), typeMoq.Times.atLeastOnce()); + + testsHelper.setup((t) => t.displayTestErrorMessage(typeMoq.It.isAny())).verifiable(typeMoq.Times.never()); + + def.reject(CANCELLATION_REASON); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify((s) => (s.command = typeMoq.It.isValue(Commands.Tests_View_UI)), typeMoq.Times.atLeastOnce()); + testsHelper.verifyAll(); + }); + test('Ensure status bar is updated, and error message display with error in running tests, with ability to view ui with results', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred<Tests>(); + + display.displayProgressStatus(def.promise, false); + + statusBar.verifyAll(); + statusBar.verify( + (s) => (s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test)), + typeMoq.Times.atLeastOnce() + ); + statusBar.verify((s) => (s.text = typeMoq.It.isValue('$(stop) Running Tests')), typeMoq.Times.atLeastOnce()); + + testsHelper.setup((t) => t.displayTestErrorMessage(typeMoq.It.isAny())).verifiable(typeMoq.Times.once()); + + def.reject('Some other reason'); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify((s) => (s.command = typeMoq.It.isValue(Commands.Tests_View_UI)), typeMoq.Times.atLeastOnce()); + testsHelper.verifyAll(); + }); + + test('Ensure status bar is displayed and updated with progress with ability to stop test discovery', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + display.displayDiscoverStatus(createDeferred<Tests>().promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify( + (s) => (s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery)), + typeMoq.Times.atLeastOnce() + ); + statusBar.verify( + (s) => (s.text = typeMoq.It.isValue('$(stop) Discovering Tests')), + typeMoq.Times.atLeastOnce() + ); + }); + test('Ensure status bar is displayed and updated with success and no tests, with ability to view ui to view results of test discovery', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred<Tests>(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify( + (s) => (s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery)), + typeMoq.Times.atLeastOnce() + ); + statusBar.verify( + (s) => (s.text = typeMoq.It.isValue('$(stop) Discovering Tests')), + typeMoq.Times.atLeastOnce() + ); + + const tests = typeMoq.Mock.ofType<Tests>(); + appShell + .setup((a) => + a.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + def.resolve(undefined as any); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify((s) => (s.command = typeMoq.It.isValue(Commands.Tests_View_UI)), typeMoq.Times.atLeastOnce()); + }); + test('Ensure tests are disabled when there are errors and user choses to disable tests', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + cmdManager + .setup((c) => + c.executeCommand( + typeMoq.It.isValue('setContext'), + typeMoq.It.isValue('testsDiscovered'), + typeMoq.It.isValue(false) + ) + ) + .verifiable(typeMoq.Times.once()); + createTestResultDisplay(); + const def = createDeferred<Tests>(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify( + (s) => (s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery)), + typeMoq.Times.atLeastOnce() + ); + statusBar.verify( + (s) => (s.text = typeMoq.It.isValue('$(stop) Discovering Tests')), + typeMoq.Times.atLeastOnce() + ); + + const tests = typeMoq.Mock.ofType<Tests>(); + appShell + .setup((a) => + a.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(Testing.disableTests())) + .verifiable(typeMoq.Times.once()); + + for (const setting of [ + 'testing.promptToConfigure', + 'testing.pytestEnabled', + 'testing.unittestEnabled', + 'testing.nosetestsEnabled' + ]) { + configurationService + .setup((c) => c.updateSetting(typeMoq.It.isValue(setting), typeMoq.It.isValue(false))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + } + def.resolve(undefined as any); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify((s) => (s.command = typeMoq.It.isValue(Commands.Tests_View_UI)), typeMoq.Times.atLeastOnce()); + configurationService.verifyAll(); + cmdManager.verifyAll(); + }); + test('Ensure corresponding command is executed when there are errors and user choses to configure test framework', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred<Tests>(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify( + (s) => (s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery)), + typeMoq.Times.atLeastOnce() + ); + statusBar.verify( + (s) => (s.text = typeMoq.It.isValue('$(stop) Discovering Tests')), + typeMoq.Times.atLeastOnce() + ); + + const tests = typeMoq.Mock.ofType<Tests>(); + appShell + .setup((a) => + a.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny()) + ) + .returns(() => Promise.resolve(Testing.configureTests())) + .verifiable(typeMoq.Times.once()); + + const undefinedArg = typeMoq.It.isValue(undefined); + cmdManager + .setup((c) => + c.executeCommand( + typeMoq.It.isValue(Commands.Tests_Configure as any), + undefinedArg, + undefinedArg, + undefinedArg + ) + ) + .returns(() => Promise.resolve() as any) + .verifiable(typeMoq.Times.once()); + def.resolve(undefined as any); + await sleep(1); + + tests.verifyAll(); + appShell.verifyAll(); + statusBar.verify((s) => (s.command = typeMoq.It.isValue(Commands.Tests_View_UI)), typeMoq.Times.atLeastOnce()); + cmdManager.verifyAll(); + }); + test('Ensure status bar is displayed and updated with error info when test discovery is cancelled by the user', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred<Tests>(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify( + (s) => (s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery)), + typeMoq.Times.atLeastOnce() + ); + statusBar.verify( + (s) => (s.text = typeMoq.It.isValue('$(stop) Discovering Tests')), + typeMoq.Times.atLeastOnce() + ); + + appShell.setup((a) => a.showErrorMessage(typeMoq.It.isAny())).verifiable(typeMoq.Times.never()); + + def.reject(CANCELLATION_REASON); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify((s) => (s.command = typeMoq.It.isValue(Commands.Tests_Discover)), typeMoq.Times.atLeastOnce()); + configurationService.verifyAll(); + }); + test('Ensure status bar is displayed and updated with error info, and message is displayed when test discovery is fails due to errors', async () => { + const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); + appShell + .setup((a) => a.createStatusBarItem(typeMoq.It.isAny())) + .returns(() => statusBar.object) + .verifiable(typeMoq.Times.once()); + + statusBar.setup((s) => s.show()).verifiable(typeMoq.Times.once()); + + createTestResultDisplay(); + const def = createDeferred<Tests>(); + + display.displayDiscoverStatus(def.promise, false).ignoreErrors(); + + statusBar.verifyAll(); + statusBar.verify( + (s) => (s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery)), + typeMoq.Times.atLeastOnce() + ); + statusBar.verify( + (s) => (s.text = typeMoq.It.isValue('$(stop) Discovering Tests')), + typeMoq.Times.atLeastOnce() + ); + + appShell.setup((a) => a.showErrorMessage(typeMoq.It.isAny())).verifiable(typeMoq.Times.once()); + + def.reject('some weird error'); + await sleep(1); + + appShell.verifyAll(); + statusBar.verify((s) => (s.command = typeMoq.It.isValue(Commands.Tests_Discover)), typeMoq.Times.atLeastOnce()); + configurationService.verifyAll(); + }); +}); diff --git a/src/test/testing/display/picker.functional.test.ts b/src/test/testing/display/picker.functional.test.ts new file mode 100644 index 000000000000..14de3b7f66e4 --- /dev/null +++ b/src/test/testing/display/picker.functional.test.ts @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; +import { ITestCollectionStorageService, TestFunction, Tests } from '../../../client/testing/common/types'; +import { TestDisplay } from '../../../client/testing/display/picker'; +import { createEmptyResults } from '../results'; + +// tslint:disable:no-any + +// tslint:disable-next-line: max-func-body-length +suite('Testing - TestDisplay', () => { + const wkspace = Uri.file(__dirname); + let mockedCommandManager: ICommandManager; + let mockedServiceContainer: IServiceContainer; + let mockedTestCollectionStorage: ITestCollectionStorageService; + let mockedAppShell: IApplicationShell; + let testDisplay: TestDisplay; + + function fullPathInTests(collectedTests: Tests, fullpath?: string): Tests { + collectedTests.testFiles = [ + { + fullPath: fullpath ? fullpath : 'path/to/testfile', + ...anything() + } + ]; + return collectedTests; + } + + setup(() => { + mockedCommandManager = mock(CommandManager); + mockedServiceContainer = mock(ServiceContainer); + mockedTestCollectionStorage = mock(TestCollectionStorageService); + mockedAppShell = mock(ApplicationShell); + when(mockedServiceContainer.get<ITestCollectionStorageService>(ITestCollectionStorageService)).thenReturn( + instance(mockedTestCollectionStorage) + ); + when(mockedServiceContainer.get<IApplicationShell>(IApplicationShell)).thenReturn(instance(mockedAppShell)); + + testDisplay = new TestDisplay(instance(mockedServiceContainer), instance(mockedCommandManager)); + }); + + suite('displayFunctionTestPickerUI', () => { + const paths: { [key: string]: any } = { + match: { + fullPath: '/path/to/testfile', + fileName: '/path/to/testfile' + }, + mismatch: { + fullPath: '/path/to/testfile', + fileName: '/testfile/to/path' + } + }; + let tests: Tests; + + function codeLensTestFunctions(testfunctions?: TestFunction[]): TestFunction[] { + if (!testfunctions) { + return [{ ...anything() }]; + } + const functions: TestFunction[] = []; + testfunctions.forEach((fn) => functions.push(fn)); + return functions; + } + + setup(() => { + tests = createEmptyResults(); + when(mockedServiceContainer.get<IFileSystem>(IFileSystem)).thenReturn(new FileSystem()); + when(mockedTestCollectionStorage.getTests(wkspace)).thenReturn(tests); + when(mockedAppShell.showQuickPick(anything(), anything())).thenResolve(); + }); + + test(`Test that a dropdown picker for parametrized tests is shown if compared paths are equal (OS independent) (#8627)`, () => { + const { fullPath, fileName } = paths.match; + fullPathInTests(tests, fullPath); + + testDisplay.displayFunctionTestPickerUI( + CommandSource.commandPalette, + wkspace, + 'rootDirectory', + Uri.file(fileName), + codeLensTestFunctions() + ); + + verify(mockedAppShell.showQuickPick(anything(), anything())).once(); + }); + + test(`Test that a dropdown picker for parametrized tests is NOT shown if compared paths are NOT equal (OS independent) (#8627)`, () => { + const { fullPath, fileName } = paths.mismatch; + fullPathInTests(tests, fullPath); + + testDisplay.displayFunctionTestPickerUI( + CommandSource.commandPalette, + wkspace, + 'rootDirectory', + Uri.file(fileName), + codeLensTestFunctions() + ); + + verify(mockedAppShell.showQuickPick(anything(), anything())).never(); + }); + + test(`Test that clicking a codelens on parametrized tests opens a dropdown picker on windows (#8627)`, function () { + if (process.platform !== 'win32') { + // tslint:disable-next-line: no-invalid-this + this.skip(); + } + // The error described in #8627 originated from the problem that the casing of the drive letter was different + // in a test items fullPath property to the one of a file that contained the clicked parametrized test. + const fileName = 'c:\\path\\to\\testfile'; + fullPathInTests(tests, 'C:\\path\\to\\testfile'); + + testDisplay.displayFunctionTestPickerUI( + CommandSource.commandPalette, + wkspace, + 'rootDirectory', + Uri.file(fileName), + codeLensTestFunctions() + ); + + verify(mockedAppShell.showQuickPick(anything(), anything())).once(); + }); + }); +}); diff --git a/src/test/testing/display/picker.unit.test.ts b/src/test/testing/display/picker.unit.test.ts new file mode 100644 index 000000000000..d0b77e003890 --- /dev/null +++ b/src/test/testing/display/picker.unit.test.ts @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; +import { ITestCollectionStorageService, TestFunction, Tests, TestsToRun } from '../../../client/testing/common/types'; +import { onItemSelected, TestDisplay, Type } from '../../../client/testing/display/picker'; +import { createEmptyResults } from '../results'; + +// tslint:disable:no-any +// tslint:disable:max-func-body-length + +suite('Unit Tests - Picker (execution of commands)', () => { + getNamesAndValues<Type>(Type).forEach((item) => { + getNamesAndValues<CommandSource>(CommandSource).forEach((commandSource) => { + [true, false].forEach((debug) => { + test(`Invoking command for selection ${item.name} from ${commandSource.name} (${ + debug ? 'Debug' : 'No debug' + })`, async () => { + const commandManager = mock(CommandManager); + const workspaceUri = Uri.file(__filename); + + const testFunction = 'some test Function'; + const testFunctions = [ + { + name: 'some_name', + nameToRun: 'some_name_to_run', + time: 0, + resource: workspaceUri + } + ]; + const selection = { type: item.value, fn: { testFunction }, fns: testFunctions }; + + // Getting the value of CommandSource.commandPalette in getNamesAndValues(CommandSource) + // fails because the names and values object is build by accessing the CommandSource enum + // properties by value. In case of commandpalette the property is commandPalette and the + // respective value is commandpalette which do not match and thus return undefined for value. + if (commandSource.name === 'commandpalette') { + commandSource.value = CommandSource.commandPalette; + } + + onItemSelected( + instance(commandManager), + commandSource.value, + workspaceUri, + selection as any, + debug + ); + + switch (selection.type) { + case Type.Null: { + verify(commandManager.executeCommand(anything())).never(); + const args: any[] = []; + for (let i = 0; i <= 7; i += 1) { + args.push(anything()); + } + verify(commandManager.executeCommand(anything(), ...args)).never(); + return; + } + case Type.RunAll: { + verify( + commandManager.executeCommand( + Commands.Tests_Run, + undefined, + commandSource.value, + workspaceUri, + undefined + ) + ).once(); + return; + } + case Type.RunParametrized: { + verify( + commandManager.executeCommand( + Commands.Tests_Run_Parametrized, + undefined, + commandSource.value, + workspaceUri, + selection.fns, + debug + ) + ).once(); + return; + } + case Type.ReDiscover: { + verify( + commandManager.executeCommand( + Commands.Tests_Discover, + undefined, + commandSource.value, + workspaceUri + ) + ).once(); + return; + } + case Type.ViewTestOutput: { + verify( + commandManager.executeCommand(Commands.Tests_ViewOutput, undefined, commandSource.value) + ).once(); + return; + } + case Type.RunFailed: { + verify( + commandManager.executeCommand( + Commands.Tests_Run_Failed, + undefined, + commandSource.value, + workspaceUri + ) + ).once(); + return; + } + case Type.SelectAndRunMethod: { + const cmd = debug + ? Commands.Tests_Select_And_Debug_Method + : Commands.Tests_Select_And_Run_Method; + verify( + commandManager.executeCommand(cmd, undefined, commandSource.value, workspaceUri) + ).once(); + return; + } + case Type.RunMethod: { + const testsToRun: TestsToRun = { testFunction: ['something' as any] }; + verify( + commandManager.executeCommand( + Commands.Tests_Run, + undefined, + commandSource.value, + workspaceUri, + testsToRun + ) + ).never(); + return; + } + case Type.DebugMethod: { + const testsToRun: TestsToRun = { testFunction: ['something' as any] }; + verify( + commandManager.executeCommand( + Commands.Tests_Debug, + undefined, + commandSource.value, + workspaceUri, + testsToRun + ) + ).never(); + return; + } + case Type.Configure: { + verify( + commandManager.executeCommand( + Commands.Tests_Configure, + undefined, + commandSource.value, + workspaceUri + ) + ).once(); + return; + } + default: { + return; + } + } + }); + }); + }); + }); +}); + +suite('Testing - TestDisplay', () => { + const wkspace = Uri.file(__dirname); + let mockedCommandManager: ICommandManager; + let mockedServiceContainer: IServiceContainer; + let mockedTestCollectionStorage: ITestCollectionStorageService; + let mockedAppShell: IApplicationShell; + let mockedFileSytem: IFileSystem; + let testDisplay: TestDisplay; + + function fullPathInTests(collectedTests: Tests, fullpath?: string): Tests { + collectedTests.testFiles = [ + { + fullPath: fullpath ? fullpath : 'path/to/testfile', + ...anything() + } + ]; + return collectedTests; + } + + setup(() => { + mockedCommandManager = mock(CommandManager); + mockedServiceContainer = mock(ServiceContainer); + mockedTestCollectionStorage = mock(TestCollectionStorageService); + mockedAppShell = mock(ApplicationShell); + when(mockedServiceContainer.get<ITestCollectionStorageService>(ITestCollectionStorageService)).thenReturn( + instance(mockedTestCollectionStorage) + ); + when(mockedServiceContainer.get<IApplicationShell>(IApplicationShell)).thenReturn(instance(mockedAppShell)); + + testDisplay = new TestDisplay(instance(mockedServiceContainer), instance(mockedCommandManager)); + }); + + suite('displayFunctionTestPickerUI', () => { + const fileName = Uri.file('path/to/testfile'); + let tests: Tests; + + function codeLensTestFunctions(testfunctions?: TestFunction[]): TestFunction[] { + if (!testfunctions) { + return [{ ...anything() }]; + } + const functions: TestFunction[] = []; + testfunctions.forEach((fn) => functions.push(fn)); + return functions; + } + + setup(() => { + tests = createEmptyResults(); + mockedFileSytem = mock(FileSystem); + when(mockedServiceContainer.get<IFileSystem>(IFileSystem)).thenReturn(instance(mockedFileSytem)); + when(mockedTestCollectionStorage.getTests(wkspace)).thenReturn(tests); + when(mockedAppShell.showQuickPick(anything(), anything())).thenResolve(); + }); + + test(`Test that a dropdown picker for parametrized tests is shown if compared paths are equal (#8627)`, () => { + fullPathInTests(tests); + when(mockedFileSytem.arePathsSame(anything(), anything())).thenReturn(true); + + testDisplay.displayFunctionTestPickerUI( + CommandSource.commandPalette, + wkspace, + 'rootDirectory', + fileName, + codeLensTestFunctions() + ); + + verify(mockedAppShell.showQuickPick(anything(), anything())).once(); + }); + + test(`Test that a dropdown picker for parametrized tests is NOT shown if compared paths are NOT equal (#8627)`, () => { + fullPathInTests(tests); + when(mockedFileSytem.arePathsSame(anything(), anything())).thenReturn(false); + + testDisplay.displayFunctionTestPickerUI( + CommandSource.commandPalette, + wkspace, + 'rootDirectory', + fileName, + codeLensTestFunctions() + ); + + verify(mockedAppShell.showQuickPick(anything(), anything())).never(); + }); + }); +}); diff --git a/src/test/testing/explorer/explorerTestData.ts b/src/test/testing/explorer/explorerTestData.ts new file mode 100644 index 000000000000..bd8acf2b17c1 --- /dev/null +++ b/src/test/testing/explorer/explorerTestData.ts @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/** + * Test utilities for testing the TestViewTreeProvider class. + */ + +import { join, parse as path_parse } from 'path'; +import * as tsmockito from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IDisposable, IDisposableRegistry } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { TestsHelper } from '../../../client/testing/common/testUtils'; +import { TestFlatteningVisitor } from '../../../client/testing/common/testVisitors/flatteningVisitor'; +import { + ITestCollectionStorageService, + TestFile, + TestFolder, + TestFunction, + Tests, + TestSuite +} from '../../../client/testing/common/types'; +import { TestTreeViewProvider } from '../../../client/testing/explorer/testTreeViewProvider'; +import { ITestManagementService } from '../../../client/testing/types'; + +/** + * Disposable class that doesn't do anything, help for event-registration against + * ITestManagementService. + */ +export class ExplorerTestsDisposable implements IDisposable { + // tslint:disable-next-line:no-empty + public dispose() {} +} + +export function getMockTestFolder(folderPath: string, testFiles: TestFile[] = []): TestFolder { + // tslint:disable-next-line:no-unnecessary-local-variable + const folder: TestFolder = { + resource: Uri.file(__filename), + folders: [], + name: folderPath, + nameToRun: folderPath, + testFiles: testFiles, + time: 0 + }; + + return folder; +} + +export function getMockTestFile( + filePath: string, + testSuites: TestSuite[] = [], + testFunctions: TestFunction[] = [] +): TestFile { + // tslint:disable-next-line:no-unnecessary-local-variable + const testFile: TestFile = { + resource: Uri.file(__filename), + name: path_parse(filePath).base, + nameToRun: filePath, + time: 0, + fullPath: join(__dirname, filePath), + functions: testFunctions, + suites: testSuites, + xmlName: filePath.replace(/\//g, '.') + }; + + return testFile; +} + +export function getMockTestSuite( + suiteNameToRun: string, + testFunctions: TestFunction[] = [], + subSuites: TestSuite[] = [], + instance: boolean = true, + unitTest: boolean = true +): TestSuite { + const suiteNameChunks = suiteNameToRun.split('::'); + const suiteName = suiteNameChunks[suiteNameChunks.length - 1]; + + // tslint:disable-next-line:no-unnecessary-local-variable + const testSuite: TestSuite = { + resource: Uri.file(__filename), + functions: testFunctions, + isInstance: instance, + isUnitTest: unitTest, + name: suiteName, + nameToRun: suiteNameToRun, + suites: subSuites, + time: 0, + xmlName: suiteNameToRun.replace(/\//g, '.').replace(/\:\:/g, ':') + }; + return testSuite; +} + +export function getMockTestFunction(fnNameToRun: string): TestFunction { + const fnNameChunks = fnNameToRun.split('::'); + const fnName = fnNameChunks[fnNameChunks.length - 1]; + + // tslint:disable-next-line:no-unnecessary-local-variable + const fn: TestFunction = { + resource: Uri.file(__filename), + name: fnName, + nameToRun: fnNameToRun, + time: 0 + }; + + return fn; +} + +/** + * Return a basic hierarchy of test data items for use in testing. + * + * @returns Array containing the items broken out from the hierarchy (all items are linked to one another) + */ +export function getTestExplorerViewItemData(): [TestFolder, TestFile, TestFunction, TestSuite, TestFunction] { + let testFolder: TestFolder; + let testFile: TestFile; + let testSuite: TestSuite; + let testFunction: TestFunction; + let testSuiteFunction: TestFunction; + + testSuiteFunction = getMockTestFunction('workspace/test_folder/test_file.py::test_suite::test_suite_function'); + testSuite = getMockTestSuite('workspace/test_folder/test_file.py::test_suite', [testSuiteFunction]); + testFunction = getMockTestFunction('workspace/test_folder/test_file.py::test_function'); + testFile = getMockTestFile('workspace/test_folder/test_file.py', [testSuite], [testFunction]); + testFolder = getMockTestFolder('workspace/test_folder', [testFile]); + + return [testFolder, testFile, testFunction, testSuite, testSuiteFunction]; +} + +/** + * Return an instance of `TestsHelper` that can be used in a unit test scenario. + * + * @returns An instance of `TestsHelper` class with mocked AppShell & ICommandManager members. + */ +export function getTestHelperInstance(): TestsHelper { + const appShellMoq = typemoq.Mock.ofType<IApplicationShell>(); + const commMgrMoq = typemoq.Mock.ofType<ICommandManager>(); + const serviceContainerMoq = typemoq.Mock.ofType<IServiceContainer>(); + + serviceContainerMoq + .setup((a) => a.get(typemoq.It.isValue(IApplicationShell), typemoq.It.isAny())) + .returns(() => appShellMoq.object); + serviceContainerMoq + .setup((a) => a.get(typemoq.It.isValue(ICommandManager), typemoq.It.isAny())) + .returns(() => commMgrMoq.object); + + return new TestsHelper(new TestFlatteningVisitor(), serviceContainerMoq.object); +} + +/** + * Creates mock `Tests` data suitable for testing the TestTreeViewProvider with. + */ +export function createMockTestsData(testData?: TestFile[]): Tests { + if (testData === undefined) { + let testFile: TestFile; + + [, testFile] = getTestExplorerViewItemData(); + + testData = [testFile]; + } + + const testHelper = getTestHelperInstance(); + return testHelper.flattenTestFiles(testData, __dirname); +} + +export function createMockTestStorageService(testData?: Tests): typemoq.IMock<ITestCollectionStorageService> { + const testStoreMoq = typemoq.Mock.ofType<ITestCollectionStorageService>(); + + if (!testData) { + testData = createMockTestsData(); + } + + testStoreMoq.setup((t) => t.getTests(typemoq.It.isAny())).returns(() => testData); + + return testStoreMoq; +} + +/** + * Create an ITestManagementService that will work for the TeestTreeViewProvider in a unit test scenario. + * + * Provider an 'onDidStatusChange' hook that can be called, but that does nothing. + */ +export function createMockUnitTestMgmtService(): typemoq.IMock<ITestManagementService> { + const unitTestMgmtSrvMoq = typemoq.Mock.ofType<ITestManagementService>(); + unitTestMgmtSrvMoq + .setup((u) => u.onDidStatusChange(typemoq.It.isAny())) + .returns(() => new ExplorerTestsDisposable()); + return unitTestMgmtSrvMoq; +} + +/** + * Create an IWorkspaceService mock that will work with the TestTreeViewProvider class. + * + * @param workspaceFolderPath Optional, the path to use as the current Resource-path for + * the tests within the TestTree. + */ +export function createMockWorkspaceService(): typemoq.IMock<IWorkspaceService> { + const workspcSrvMoq = typemoq.Mock.ofType<IWorkspaceService>(); + class ExplorerTestsWorkspaceFolder implements WorkspaceFolder { + public get uri(): Uri { + return Uri.parse(''); + } + public get name(): string { + return path_parse(this.uri.fsPath).base; + } + public get index(): number { + return 0; + } + } + workspcSrvMoq.setup((w) => w.workspaceFolders).returns(() => [new ExplorerTestsWorkspaceFolder()]); + return workspcSrvMoq; +} + +/** + * Create a testable mocked up version of the TestExplorerViewProvider. Creates any + * mocked dependencies not provided in the parameters. + * + * @param {ITestCollectionStorageService} [testStore] Test storage service, provides access to the Tests structure that the view is built from. + * @param {Tests} [testsData] + * @param {ITestManagementService} [unitTestMgmtService] Unit test management service that provides the 'onTestStatusUpdated' event. + * @param {IWorkspaceService} [workspaceService] Workspace service used to determine the current workspace that the test view is showing. + * @param {ICommandManager} [commandManager] + */ +export function createMockTestExplorer( + testStore?: ITestCollectionStorageService, + testsData?: Tests, + unitTestMgmtService?: ITestManagementService, + workspaceService?: IWorkspaceService, + commandManager?: ICommandManager +): TestTreeViewProvider { + if (!testStore) { + testStore = createMockTestStorageService(testsData).object; + } + + if (!unitTestMgmtService) { + unitTestMgmtService = createMockUnitTestMgmtService().object; + } + + if (!workspaceService) { + workspaceService = createMockWorkspaceService().object; + } + if (!commandManager) { + commandManager = tsmockito.instance(tsmockito.mock(CommandManager)); + } + + const dispRegMoq = typemoq.Mock.ofType<IDisposableRegistry>(); + dispRegMoq.setup((d) => d.push(typemoq.It.isAny())); + + return new TestTreeViewProvider( + testStore, + unitTestMgmtService, + workspaceService, + commandManager, + dispRegMoq.object + ); +} diff --git a/src/test/testing/explorer/failedTestHandler.unit.test.ts b/src/test/testing/explorer/failedTestHandler.unit.test.ts new file mode 100644 index 000000000000..e38715a41958 --- /dev/null +++ b/src/test/testing/explorer/failedTestHandler.unit.test.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { ICommandManager } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; +import { + ITestCollectionStorageService, + TestFile, + TestFolder, + TestFunction, + TestStatus, + TestSuite +} from '../../../client/testing/common/types'; +import { FailedTestHandler } from '../../../client/testing/explorer/failedTestHandler'; +import { noop, sleep } from '../../core'; + +// tslint:disable:no-any + +suite('Unit Tests Test Explorer View Items', () => { + let failedTestHandler: FailedTestHandler; + let commandManager: ICommandManager; + let testStorageService: ITestCollectionStorageService; + setup(() => { + commandManager = mock(CommandManager); + testStorageService = mock(TestCollectionStorageService); + failedTestHandler = new FailedTestHandler([], instance(commandManager), instance(testStorageService)); + }); + + test('Activation will add command handlers', async () => { + when(testStorageService.onDidChange).thenReturn(noop as any); + + await failedTestHandler.activate(); + + verify(testStorageService.onDidChange).once(); + }); + test('Change handler will invoke the command to reveal the nodes (for failed and errored items)', async () => { + const uri = Uri.file(__filename); + const failedFunc1: TestFunction = { + name: 'fn1', + time: 0, + resource: uri, + nameToRun: 'fn1', + status: TestStatus.Error + }; + const failedFunc2: TestFunction = { + name: 'fn2', + time: 0, + resource: uri, + nameToRun: 'fn2', + status: TestStatus.Fail + }; + when(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, anything())).thenResolve(); + + failedTestHandler.onDidChangeTestData({ uri, data: failedFunc1 }); + failedTestHandler.onDidChangeTestData({ uri, data: failedFunc2 }); + + // wait for debouncing to take effect. + await sleep(1); + + verify(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, anything())).times(2); + verify(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, failedFunc1)).once(); + verify(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, failedFunc2)).once(); + }); + test('Change handler will not invoke the command to reveal the nodes (for failed and errored suites, files & folders)', async () => { + const uri = Uri.file(__filename); + const failedSuite: TestSuite = { + name: 'suite1', + time: 0, + resource: uri, + nameToRun: 'suite1', + functions: [], + isInstance: false, + isUnitTest: false, + suites: [], + xmlName: 'suite1', + status: TestStatus.Error + }; + const failedFile: TestFile = { + name: 'suite1', + time: 0, + resource: uri, + nameToRun: 'file', + functions: [], + suites: [], + xmlName: 'file', + status: TestStatus.Error, + fullPath: '' + }; + const failedFolder: TestFolder = { + name: 'suite1', + time: 0, + resource: uri, + nameToRun: 'file', + testFiles: [], + folders: [], + status: TestStatus.Error + }; + when(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, anything())).thenResolve(); + + failedTestHandler.onDidChangeTestData({ uri, data: failedSuite }); + failedTestHandler.onDidChangeTestData({ uri, data: failedFile }); + failedTestHandler.onDidChangeTestData({ uri, data: failedFolder }); + + // wait for debouncing to take effect. + await sleep(1); + + verify(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, anything())).never(); + }); +}); diff --git a/src/test/testing/explorer/testExplorerCommandHandler.unit.test.ts b/src/test/testing/explorer/testExplorerCommandHandler.unit.test.ts new file mode 100644 index 000000000000..403c3a8438ee --- /dev/null +++ b/src/test/testing/explorer/testExplorerCommandHandler.unit.test.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IDisposable } from '@phosphor/disposable'; +import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { ICommandManager } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { TestFile, TestFunction, TestsToRun, TestSuite } from '../../../client/testing/common/types'; +import { TestExplorerCommandHandler } from '../../../client/testing/explorer/commandHandlers'; +import { TestTreeViewProvider } from '../../../client/testing/explorer/testTreeViewProvider'; +import { ITestExplorerCommandHandler } from '../../../client/testing/navigation/types'; +import { ITestDataItemResource } from '../../../client/testing/types'; + +// tslint:disable:no-any max-func-body-length +suite('Unit Tests - Test Explorer Command Handler', () => { + let commandHandler: ITestExplorerCommandHandler; + let cmdManager: ICommandManager; + let testResourceMapper: ITestDataItemResource; + + setup(() => { + cmdManager = mock(CommandManager); + testResourceMapper = mock(TestTreeViewProvider); + commandHandler = new TestExplorerCommandHandler(instance(cmdManager), instance(testResourceMapper)); + }); + test('Commands are registered', () => { + commandHandler.register(); + + verify(cmdManager.registerCommand(Commands.runTestNode, anything(), commandHandler)).once(); + verify(cmdManager.registerCommand(Commands.debugTestNode, anything(), commandHandler)).once(); + verify(cmdManager.registerCommand(Commands.openTestNodeInEditor, anything(), commandHandler)).once(); + }); + test('Handlers are disposed', () => { + const disposable1 = typemoq.Mock.ofType<IDisposable>(); + const disposable2 = typemoq.Mock.ofType<IDisposable>(); + const disposable3 = typemoq.Mock.ofType<IDisposable>(); + + when(cmdManager.registerCommand(Commands.runTestNode, anything(), commandHandler)).thenReturn( + disposable1.object + ); + when(cmdManager.registerCommand(Commands.debugTestNode, anything(), commandHandler)).thenReturn( + disposable2.object + ); + when(cmdManager.registerCommand(Commands.openTestNodeInEditor, anything(), commandHandler)).thenReturn( + disposable3.object + ); + + commandHandler.register(); + commandHandler.dispose(); + + disposable1.verify((d) => d.dispose(), typemoq.Times.once()); + disposable2.verify((d) => d.dispose(), typemoq.Times.once()); + disposable3.verify((d) => d.dispose(), typemoq.Times.once()); + }); + async function testOpeningTestNode( + data: TestFile | TestSuite | TestFunction, + expectedCommand: 'navigateToTestFunction' | 'navigateToTestSuite' | 'navigateToTestFile' + ) { + const resource = Uri.file(__filename); + when(testResourceMapper.getResource(data)).thenReturn(resource); + + commandHandler.register(); + + const handler = (capture(cmdManager.registerCommand as any).last()[1] as any) as Function; + await handler.bind(commandHandler)(data); + + verify(cmdManager.executeCommand(expectedCommand, resource, data, true)).once(); + } + test('Opening a file will invoke correct command', async () => { + const testFilePath = 'some file path'; + const data: TestFile = { fullPath: testFilePath } as any; + await testOpeningTestNode(data, Commands.navigateToTestFile); + }); + test('Opening a test suite will invoke correct command', async () => { + const data: TestSuite = { suites: [] } as any; + await testOpeningTestNode(data, Commands.navigateToTestSuite); + }); + test('Opening a test function will invoke correct command', async () => { + const data: TestFunction = { name: 'hello' } as any; + await testOpeningTestNode(data, Commands.navigateToTestFunction); + }); + async function testRunOrDebugTestNode( + data: TestFile | TestSuite | TestFunction, + expectedTestRun: TestsToRun, + runType: 'run' | 'debug' + ) { + const resource = Uri.file(__filename); + when(testResourceMapper.getResource(data)).thenReturn(resource); + + commandHandler.register(); + + const capturedCommand = capture(cmdManager.registerCommand as any); + const handler = ((runType === 'run' + ? capturedCommand.first()[1] + : capturedCommand.second()[1]) as any) as Function; + await handler.bind(commandHandler)(data); + + const cmd = runType === 'run' ? Commands.Tests_Run : Commands.Tests_Debug; + verify( + cmdManager.executeCommand(cmd, undefined, CommandSource.testExplorer, resource, deepEqual(expectedTestRun)) + ).once(); + } + test('Running a file will invoke correct command', async () => { + const testFilePath = 'some file path'; + const data: TestFile = { fullPath: testFilePath } as any; + await testRunOrDebugTestNode(data, { testFile: [data] }, 'run'); + }); + test('Running a suite will invoke correct command', async () => { + const data: TestSuite = { suites: [] } as any; + await testRunOrDebugTestNode(data, { testSuite: [data] }, 'run'); + }); + test('Running a function will invoke correct command', async () => { + const data: TestSuite = { suites: [] } as any; + await testRunOrDebugTestNode(data, { testSuite: [data] }, 'run'); + }); + test('Debugging a file will invoke correct command', async () => { + const testFilePath = 'some file path'; + const data: TestFile = { fullPath: testFilePath } as any; + await testRunOrDebugTestNode(data, { testFile: [data] }, 'debug'); + }); + test('Debugging a suite will invoke correct command', async () => { + const data: TestSuite = { suites: [] } as any; + await testRunOrDebugTestNode(data, { testSuite: [data] }, 'debug'); + }); + test('Debugging a function will invoke correct command', async () => { + const data: TestSuite = { suites: [] } as any; + await testRunOrDebugTestNode(data, { testSuite: [data] }, 'debug'); + }); +}); diff --git a/src/test/testing/explorer/testTreeViewItem.unit.test.ts b/src/test/testing/explorer/testTreeViewItem.unit.test.ts new file mode 100644 index 000000000000..423665c35d2a --- /dev/null +++ b/src/test/testing/explorer/testTreeViewItem.unit.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { Uri } from 'vscode'; +import { Commands } from '../../../client/common/constants'; +import { TestFile, TestFolder, TestFunction, TestSuite } from '../../../client/testing/common/types'; +import { TestTreeItem } from '../../../client/testing/explorer/testTreeViewItem'; +import { TestDataItemType } from '../../../client/testing/types'; +import { createMockTestDataItem, createSubtestParent } from '../common/testUtils.unit.test'; +import { getTestExplorerViewItemData } from './explorerTestData'; + +suite('Unit Tests Test Explorer View Items', () => { + let testFolder: TestFolder; + let testFile: TestFile; + let testSuite: TestSuite; + let testFunction: TestFunction; + let testSuiteFunction: TestFunction; + const resource = Uri.file(__filename); + setup(() => { + [testFolder, testFile, testFunction, testSuite, testSuiteFunction] = getTestExplorerViewItemData(); + }); + + test('Test root folder created into test view item', () => { + const viewItem = new TestTreeItem(resource, testFolder); + expect(viewItem.contextValue).is.equal('folder'); + }); + + test('Test file created into test view item', () => { + const viewItem = new TestTreeItem(resource, testFile); + expect(viewItem.contextValue).is.equal('file'); + }); + + test('Test suite created into test view item', () => { + const viewItem = new TestTreeItem(resource, testSuite); + expect(viewItem.contextValue).is.equal('suite'); + }); + + test('Test function created into test view item', () => { + const viewItem = new TestTreeItem(resource, testFunction); + expect(viewItem.contextValue).is.equal('function'); + }); + + test('Test suite function created into test view item', () => { + const viewItem = new TestTreeItem(resource, testSuiteFunction); + expect(viewItem.contextValue).is.equal('function'); + }); + + test('Test subtest parent created into test view item', () => { + const subtestParent = createSubtestParent([ + createMockTestDataItem<TestFunction>(TestDataItemType.function, 'test_x'), + createMockTestDataItem<TestFunction>(TestDataItemType.function, 'test_y') + ]); + + const viewItem = new TestTreeItem(resource, subtestParent.asSuite); + + expect(viewItem.contextValue).is.equal('suite'); + expect(viewItem.command!.command).is.equal(Commands.navigateToTestFunction); + }); + + test('Test subtest created into test view item', () => { + createSubtestParent([testFunction]); // sets testFunction.subtestParent + + const viewItem = new TestTreeItem(resource, testFunction); + + expect(viewItem.contextValue).is.equal('function'); + }); +}); diff --git a/src/test/testing/explorer/testTreeViewProvider.unit.test.ts b/src/test/testing/explorer/testTreeViewProvider.unit.test.ts new file mode 100644 index 000000000000..e38dedec2f8c --- /dev/null +++ b/src/test/testing/explorer/testTreeViewProvider.unit.test.ts @@ -0,0 +1,995 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import { instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { TreeItemCollapsibleState, Uri } from 'vscode'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { Commands } from '../../../client/common/constants'; +import { IDisposable } from '../../../client/common/types'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; +import { getTestDataItemType } from '../../../client/testing/common/testUtils'; +import { + ITestCollectionStorageService, + TestFile, + TestFolder, + Tests, + TestStatus +} from '../../../client/testing/common/types'; +import { TestTreeItem } from '../../../client/testing/explorer/testTreeViewItem'; +import { TestTreeViewProvider } from '../../../client/testing/explorer/testTreeViewProvider'; +import { UnitTestManagementService } from '../../../client/testing/main'; +import { TestDataItem, TestDataItemType, TestWorkspaceFolder } from '../../../client/testing/types'; +import { noop } from '../../core'; +import { + createMockTestExplorer as createMockTestTreeProvider, + createMockTestsData, + getMockTestFile, + getMockTestFolder, + getMockTestFunction, + getMockTestSuite +} from './explorerTestData'; + +// tslint:disable:no-any + +/** + * Class that is useful to track any Tree View update requests made by the view provider. + */ +class TestExplorerCaptureRefresh implements IDisposable { + public refreshCount: number = 0; // this counts the number of times 'onDidChangeTreeData' is emitted. + + private disposable: IDisposable; + + constructor(private testViewProvider: TestTreeViewProvider, disposableContainer: IDisposable[]) { + this.disposable = this.testViewProvider.onDidChangeTreeData(this.onRefreshOcured.bind(this)); + disposableContainer.push(this); + } + + public dispose() { + this.disposable.dispose(); + } + + private onRefreshOcured(_testDataItem?: TestDataItem): void { + this.refreshCount = this.refreshCount + 1; + } +} + +// tslint:disable:max-func-body-length +suite('Unit Tests Test Explorer TestTreeViewProvider', () => { + suite('Misc', () => { + const testResource: Uri = Uri.parse('anything'); + let disposables: IDisposable[] = []; + + teardown(() => { + disposables.forEach((disposableItem: IDisposable) => { + disposableItem.dispose(); + }); + disposables = []; + }); + + test('Create the initial view and ensure it provides a default view', async () => { + const testTreeProvider = createMockTestTreeProvider(); + expect(testTreeProvider).is.not.equal( + undefined, + 'Could not create a mock test explorer, check the parameters of the test setup.' + ); + const treeRoot = await testTreeProvider.getChildren(); + expect(treeRoot.length).to.be.greaterThan( + 0, + 'No children returned from default view of the TreeViewProvider.' + ); + }); + + test('Ensure that updates from the test manager propagate to the TestExplorer', async () => { + const testsData = createMockTestsData(); + const workspaceService = mock(WorkspaceService); + const testStore = mock(TestCollectionStorageService); + const workspaceFolder = { uri: Uri.file(''), name: 'root', index: 0 }; + when(workspaceService.getWorkspaceFolder(testResource)).thenReturn(workspaceFolder); + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(noop as any); + when(testStore.getTests(testResource)).thenReturn(testsData); + when(testStore.onDidChange).thenReturn(noop as any); + const changeItem = testsData.testFolders[1].testFiles[0].functions[0]; + const testTreeProvider = createMockTestTreeProvider( + instance(testStore), + testsData, + undefined, + instance(workspaceService) + ); + const refreshCap = new TestExplorerCaptureRefresh(testTreeProvider, disposables); + + testTreeProvider.refresh(testResource); + const originalTreeItem = (await testTreeProvider.getTreeItem(changeItem)) as TestTreeItem; + const origStatus = originalTreeItem.testStatus; + + changeItem.status = TestStatus.Fail; + testTreeProvider.refresh(testResource); + const changedTreeItem = (await testTreeProvider.getTreeItem(changeItem)) as TestTreeItem; + const updatedStatus = changedTreeItem.testStatus; + + expect(origStatus).to.not.equal(updatedStatus); + expect(refreshCap.refreshCount).to.equal(2); + }); + + test('When the test data is updated, the update event is emitted', () => { + const testsData = createMockTestsData(); + const workspaceService = mock(WorkspaceService); + const testStore = mock(TestCollectionStorageService); + const workspaceFolder = { uri: Uri.file(''), name: 'root', index: 0 }; + when(workspaceService.getWorkspaceFolder(testResource)).thenReturn(workspaceFolder); + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(noop as any); + when(testStore.getTests(testResource)).thenReturn(testsData); + when(testStore.onDidChange).thenReturn(noop as any); + const testView = createMockTestTreeProvider( + instance(testStore), + testsData, + undefined, + instance(workspaceService) + ); + + const refreshCap = new TestExplorerCaptureRefresh(testView, disposables); + testView.refresh(testResource); + + expect(refreshCap.refreshCount).to.be.equal(1); + }); + + test('A test file is added/removed/renamed', async () => { + // create an inital test tree with a single file. + const fn = getMockTestFunction('test/test_fl.py::test_fn1'); + const fl1 = getMockTestFile('test/test_fl.py', [], [fn]); + const originalTestData = createMockTestsData([fl1]); + + // create an updated test tree, similar to the first, but with a new file + const origName = 'test_fl2'; + const afn = getMockTestFunction(`test/${origName}.py::test_2fn1`); + const fl2 = getMockTestFile(`test/${origName}.py`, [], [afn]); + const updatedTestData = createMockTestsData([fl1, fl2]); + + let testData = originalTestData; + const testStoreMoq = typemoq.Mock.ofType<ITestCollectionStorageService>(); + testStoreMoq.setup((a) => a.getTests(typemoq.It.isAny())).returns(() => testData); + + const testTreeProvider = createMockTestTreeProvider(testStoreMoq.object); + + testTreeProvider.refresh(testResource); + let unchangedItem = await testTreeProvider.getTreeItem(fl1); + expect(unchangedItem).to.not.be.equal(undefined, 'The file that will always be present, is not present.'); + + testData = updatedTestData; + testTreeProvider.refresh(testResource); + + unchangedItem = await testTreeProvider.getTreeItem(fl1); + expect(unchangedItem).to.not.be.equal(undefined, 'The file that will always be present, is not present.'); + let addedTreeItem = (await testTreeProvider.getTreeItem(fl2)) as TestTreeItem; + expect(addedTreeItem).to.not.be.equal( + undefined, + 'The file has been added to the tests tree but not found?' + ); + expect(addedTreeItem.data.name).to.be.equal(`${origName}.py`); + + // change the name of the added file... + const newName = 'test_file_two'; + afn.name = afn.name.replace(origName, newName); + afn.nameToRun = afn.nameToRun.replace(origName, newName); + fl2.name = fl2.name.replace(origName, newName); + fl2.fullPath = fl2.fullPath.replace(origName, newName); + fl2.nameToRun = fl2.nameToRun.replace(origName, newName); + fl2.xmlName = fl2.xmlName.replace(origName, newName); + + testTreeProvider.refresh(testResource); + + unchangedItem = await testTreeProvider.getTreeItem(fl1); + expect(unchangedItem).to.not.be.equal(undefined, 'The file that will always be present, is not present.'); + addedTreeItem = (await testTreeProvider.getTreeItem(fl2)) as TestTreeItem; + expect(addedTreeItem).to.not.be.equal( + undefined, + 'The file has been updated in the tests tree but in tree view?' + ); + expect(addedTreeItem.data.name).to.be.equal(`${newName}.py`); + }); + + test('A test suite is added/removed/renamed', async () => { + // create an inital test tree with a single file containing a single suite. + const sfn = getMockTestFunction('test/test_fl.py::suite1::test_fn'); + const suite = getMockTestSuite('test/test_fl.py::suite1', [sfn]); + const fl1 = getMockTestFile('test/test_fl.py', [suite]); + const originalTestData = createMockTestsData([fl1]); + + // create an updated test tree, similar to the first, but with a new file + const origName = 'suite2'; + const sfn2 = getMockTestFunction(`test/test_fl.py::${origName}::test_fn`); + const suite2 = getMockTestSuite(`test/test_fl.py::${origName}`, [sfn2]); + const fl1_update = getMockTestFile('test/test_fl.py', [suite, suite2]); + const updatedTestData = createMockTestsData([fl1_update]); + + let testData = originalTestData; + const testStoreMoq = typemoq.Mock.ofType<ITestCollectionStorageService>(); + testStoreMoq.setup((a) => a.getTests(typemoq.It.isAny())).returns(() => testData); + + const testTreeProvider = createMockTestTreeProvider(testStoreMoq.object); + + testTreeProvider.refresh(testResource); + let unchangedItem = await testTreeProvider.getTreeItem(suite); + expect(unchangedItem).to.not.be.equal(undefined, 'The suite that will always be present, is not present.'); + + testData = updatedTestData; + testTreeProvider.refresh(testResource); + + unchangedItem = await testTreeProvider.getTreeItem(suite); + expect(unchangedItem).to.not.be.equal(undefined, 'The suite that will always be present, is not present.'); + let addedTreeItem = (await testTreeProvider.getTreeItem(suite2)) as TestTreeItem; + expect(addedTreeItem).to.not.be.equal( + undefined, + 'The suite has been added to the tests tree but not found?' + ); + + const newName = 'suite_two'; + suite2.name = suite2.name.replace(origName, newName); + suite2.nameToRun = suite2.nameToRun.replace(origName, newName); + suite2.xmlName = suite2.xmlName.replace(origName, newName); + + testTreeProvider.refresh(testResource); + + unchangedItem = await testTreeProvider.getTreeItem(suite); + expect(unchangedItem).to.not.be.equal(undefined, 'The suite that will always be present, is not present.'); + addedTreeItem = (await testTreeProvider.getTreeItem(suite2)) as TestTreeItem; + expect(addedTreeItem).to.not.be.equal( + undefined, + 'The suite has been updated in the tests tree but in tree view?' + ); + expect(addedTreeItem.data.name).to.be.equal(newName); + }); + + test('A test function is added/removed/renamed', async () => { + // create an inital test tree with a single file containing a single suite. + const fn = getMockTestFunction('test/test_fl.py::test_fn'); + const fl1 = getMockTestFile('test/test_fl.py', [], [fn]); + const originalTestData = createMockTestsData([fl1]); + + // create an updated test tree, similar to the first, but with a new function + const origName = 'test_fn2'; + const fn2 = getMockTestFunction(`test/test_fl.py::${origName}`); + const fl1_update = getMockTestFile('test/test_fl.py', [], [fn, fn2]); + const updatedTestData = createMockTestsData([fl1_update]); + + let testData = originalTestData; + const testStoreMoq = typemoq.Mock.ofType<ITestCollectionStorageService>(); + testStoreMoq.setup((a) => a.getTests(typemoq.It.isAny())).returns(() => testData); + + const testTreeProvider = createMockTestTreeProvider(testStoreMoq.object); + + testTreeProvider.refresh(testResource); + let unchangedItem = await testTreeProvider.getTreeItem(fn); + expect(unchangedItem).to.not.be.equal( + undefined, + 'The function that will always be present, is not present.' + ); + + testData = updatedTestData; + testTreeProvider.refresh(testResource); + + unchangedItem = await testTreeProvider.getTreeItem(fn); + expect(unchangedItem).to.not.be.equal( + undefined, + 'The function that will always be present, is not present.' + ); + let addedTreeItem = (await testTreeProvider.getTreeItem(fn2)) as TestTreeItem; + expect(addedTreeItem).to.not.be.equal( + undefined, + 'The function has been added to the tests tree but not found?' + ); + expect(addedTreeItem.data.name).to.be.equal('test_fn2'); + + const newName = 'test_func_two'; + fn2.name = fn2.name.replace(origName, newName); + fn2.nameToRun = fn2.nameToRun.replace(origName, newName); + + testTreeProvider.refresh(testResource); + + unchangedItem = await testTreeProvider.getTreeItem(fn); + expect(unchangedItem).to.not.be.equal( + undefined, + 'The function that will always be present, is not present.' + ); + addedTreeItem = (await testTreeProvider.getTreeItem(fn2)) as TestTreeItem; + expect(addedTreeItem).to.not.be.equal( + undefined, + 'The function has been updated in the tests tree but in tree view?' + ); + expect(addedTreeItem.data.name).to.be.equal(newName); + }); + + test('A test status changes and is reflected in the tree view', async () => { + // create a single file with a single function + const testFunction = getMockTestFunction('test/test_file.py::test_fn'); + testFunction.status = TestStatus.Pass; + const testFile = getMockTestFile('test/test_file.py', [], [testFunction]); + const testData = createMockTestsData([testFile]); + + const testTreeProvider = createMockTestTreeProvider(undefined, testData); + + // test's initial state is success + testTreeProvider.refresh(testResource); + const treeItem = (await testTreeProvider.getTreeItem(testFunction)) as TestTreeItem; + expect(treeItem.testStatus).to.be.equal(TestStatus.Pass); + + // test's next state is fail + testFunction.status = TestStatus.Fail; + testTreeProvider.refresh(testResource); + let updatedTreeItem = (await testTreeProvider.getTreeItem(testFunction)) as TestTreeItem; + expect(updatedTreeItem.testStatus).to.be.equal(TestStatus.Fail); + + // test's next state is skip + testFunction.status = TestStatus.Skipped; + testTreeProvider.refresh(testResource); + updatedTreeItem = (await testTreeProvider.getTreeItem(testFunction)) as TestTreeItem; + expect(updatedTreeItem.testStatus).to.be.equal(TestStatus.Skipped); + }); + + test('Get parent is working for each item type', async () => { + // create a single folder/file/suite/test setup + const testFunction = getMockTestFunction('test/test_file.py::test_suite::test_fn'); + const testSuite = getMockTestSuite('test/test_file.py::test_suite', [testFunction]); + const outerTestFunction = getMockTestFunction('test/test_file.py::test_outer_fn'); + const testFile = getMockTestFile('test/test_file.py', [testSuite], [outerTestFunction]); + const testData = createMockTestsData([testFile]); + + const testTreeProvider = createMockTestTreeProvider(undefined, testData); + + // build up the view item tree + testTreeProvider.refresh(testResource); + + let parent = (await testTreeProvider.getParent(testFunction))!; + expect(parent.name).to.be.equal( + testSuite.name, + 'Function within a test suite not returning the suite as parent.' + ); + let parentType = getTestDataItemType(parent); + expect(parentType).to.be.equal(TestDataItemType.suite); + + parent = (await testTreeProvider.getParent(testSuite))!; + expect(parent.name).to.be.equal( + testFile.name, + 'Suite within a test file not returning the test file as parent.' + ); + parentType = getTestDataItemType(parent); + expect(parentType).to.be.equal(TestDataItemType.file); + + parent = (await testTreeProvider.getParent(outerTestFunction))!; + expect(parent.name).to.be.equal( + testFile.name, + 'Function within a test file not returning the test file as parent.' + ); + parentType = getTestDataItemType(parent); + expect(parentType).to.be.equal(TestDataItemType.file); + + parent = (await testTreeProvider.getParent(testFile))!; + parentType = getTestDataItemType(parent!); + expect(parentType).to.be.equal(TestDataItemType.folder); + }); + + test('Get children is working for each item type', async () => { + // create a single folder/file/suite/test setup + const testFunction = getMockTestFunction('test/test_file.py::test_suite::test_fn'); + const testSuite = getMockTestSuite('test/test_file.py::test_suite', [testFunction]); + const outerTestFunction = getMockTestFunction('test/test_file.py::test_outer_fn'); + const testFile = getMockTestFile('test/test_file.py', [testSuite], [outerTestFunction]); + const testData = createMockTestsData([testFile]); + + const testTreeProvider = createMockTestTreeProvider(undefined, testData); + + // build up the view item tree + testTreeProvider.refresh(testResource); + + let children = await testTreeProvider.getChildren(testFunction); + expect(children.length).to.be.equal(0, 'A function should never have children.'); + + children = await testTreeProvider.getChildren(testSuite); + expect(children.length).to.be.equal(1, 'Suite a single function should only return one child.'); + children.forEach((child: TestDataItem) => { + expect(child.name).oneOf(['test_fn']); + expect(getTestDataItemType(child)).to.be.equal(TestDataItemType.function); + }); + + children = await testTreeProvider.getChildren(outerTestFunction); + expect(children.length).to.be.equal(0, 'A function should never have children.'); + + children = await testTreeProvider.getChildren(testFile); + expect(children.length).to.be.equal( + 2, + 'A file with one suite and one function should have a total of 2 children.' + ); + children.forEach((child: TestDataItem) => { + expect(child.name).oneOf(['test_suite', 'test_outer_fn']); + }); + }); + + test('Tree items for subtests are correct', async () => { + const resource = Uri.file(__filename); + // Set up the folder & file. + const folder = getMockTestFolder('tests'); + const file = getMockTestFile(`${folder.name}/test_file.py`); + folder.testFiles.push(file); + // Set up the file-level tests. + const func1 = getMockTestFunction(`${file.name}::test_spam`); + file.functions.push(func1); + const func2 = getMockTestFunction(`${file.name}::test_ham[1-2]`); + func2.subtestParent = { + name: 'test_ham', + nameToRun: `${file.name}::test_ham`, + asSuite: { + resource: resource, + name: 'test_ham', + nameToRun: `${file.name}::test_ham`, + functions: [func2], + suites: [], + isUnitTest: false, + isInstance: false, + xmlName: 'test_ham', + time: 0 + }, + time: 0 + }; + file.functions.push(func2); + const func3 = getMockTestFunction(`${file.name}::test_ham[3-4]`); + func3.subtestParent = func2.subtestParent; + func3.subtestParent.asSuite.functions.push(func3); + file.functions.push(func3); + // Set up the suite. + const suite = getMockTestSuite(`${file.name}::MyTests`); + file.suites.push(suite); + const func4 = getMockTestFunction('MyTests::test_foo'); + suite.functions.push(func4); + const func5 = getMockTestFunction('MyTests::test_bar[2-3]'); + func5.subtestParent = { + name: 'test_bar', + nameToRun: `${file.name}::MyTests::test_bar`, + asSuite: { + resource: resource, + name: 'test_bar', + nameToRun: `${file.name}::MyTests::test_bar`, + functions: [func5], + suites: [], + isUnitTest: false, + isInstance: false, + xmlName: 'test_bar', + time: 0 + }, + time: 0 + }; + suite.functions.push(func5); + // Set up the tests data. + const testData = createMockTestsData([file]); + + const testExplorer = createMockTestTreeProvider(undefined, testData); + const items = [ + await testExplorer.getTreeItem(func1), + await testExplorer.getTreeItem(func2), + await testExplorer.getTreeItem(func3), + await testExplorer.getTreeItem(func4), + await testExplorer.getTreeItem(func5), + await testExplorer.getTreeItem(file), + await testExplorer.getTreeItem(suite), + await testExplorer.getTreeItem(func2.subtestParent.asSuite), + await testExplorer.getTreeItem(func5.subtestParent.asSuite) + ]; + + expect(items).to.deep.equal([ + new TestTreeItem(func1.resource, func1), + new TestTreeItem(func2.resource, func2), + new TestTreeItem(func3.resource, func3), + new TestTreeItem(func4.resource, func4), + new TestTreeItem(func5.resource, func5), + new TestTreeItem(file.resource, file), + new TestTreeItem(suite.resource, suite), + new TestTreeItem(resource, func2.subtestParent.asSuite), + new TestTreeItem(resource, func5.subtestParent.asSuite) + ]); + }); + + test('Parents for subtests are correct', async () => { + const resource = Uri.file(__filename); + // Set up the folder & file. + const folder = getMockTestFolder('tests'); + const file = getMockTestFile(`${folder.name}/test_file.py`); + folder.testFiles.push(file); + // Set up the file-level tests. + const func1 = getMockTestFunction(`${file.name}::test_spam`); + file.functions.push(func1); + const func2 = getMockTestFunction(`${file.name}::test_ham[1-2]`); + func2.subtestParent = { + name: 'test_ham', + nameToRun: `${file.name}::test_ham`, + asSuite: { + resource: resource, + name: 'test_ham', + nameToRun: `${file.name}::test_ham`, + functions: [func2], + suites: [], + isUnitTest: false, + isInstance: false, + xmlName: 'test_ham', + time: 0 + }, + time: 0 + }; + file.functions.push(func2); + const func3 = getMockTestFunction(`${file.name}::test_ham[3-4]`); + func3.subtestParent = func2.subtestParent; + func3.subtestParent.asSuite.functions.push(func3); + file.functions.push(func3); + // Set up the suite. + const suite = getMockTestSuite(`${file.name}::MyTests`); + file.suites.push(suite); + const func4 = getMockTestFunction('MyTests::test_foo'); + suite.functions.push(func4); + const func5 = getMockTestFunction('MyTests::test_bar[2-3]'); + func5.subtestParent = { + name: 'test_bar', + nameToRun: `${file.name}::MyTests::test_bar`, + asSuite: { + resource: resource, + name: 'test_bar', + nameToRun: `${file.name}::MyTests::test_bar`, + functions: [func5], + suites: [], + isUnitTest: false, + isInstance: false, + xmlName: 'test_bar', + time: 0 + }, + time: 0 + }; + suite.functions.push(func5); + // Set up the tests data. + const testData = createMockTestsData([file]); + + const testExplorer = createMockTestTreeProvider(undefined, testData); + const parents = [ + await testExplorer.getParent(func1), + await testExplorer.getParent(func2), + await testExplorer.getParent(func3), + await testExplorer.getParent(func4), + await testExplorer.getParent(func5), + await testExplorer.getParent(suite), + await testExplorer.getParent(func2.subtestParent.asSuite), + await testExplorer.getParent(func3.subtestParent.asSuite), + await testExplorer.getParent(func5.subtestParent.asSuite) + ]; + + expect(parents).to.deep.equal([ + file, + func2.subtestParent.asSuite, + func3.subtestParent.asSuite, + suite, + func5.subtestParent.asSuite, + file, + file, + file, + suite + ]); + }); + test('Children for subtests are correct', async () => { + const resource = Uri.file(__filename); + // Set up the folder & file. + const folder = getMockTestFolder('tests'); + const file = getMockTestFile(`${folder.name}/test_file.py`); + folder.testFiles.push(file); + // Set up the file-level tests. + const func1 = getMockTestFunction(`${file.name}::test_spam`); + file.functions.push(func1); + const func2 = getMockTestFunction(`${file.name}::test_ham[1-2]`); + func2.subtestParent = { + name: 'test_ham', + nameToRun: `${file.name}::test_ham`, + asSuite: { + resource: resource, + name: 'test_ham', + nameToRun: `${file.name}::test_ham`, + functions: [func2], + suites: [], + isUnitTest: false, + isInstance: false, + xmlName: 'test_ham', + time: 0 + }, + time: 0 + }; + file.functions.push(func2); + const func3 = getMockTestFunction(`${file.name}::test_ham[3-4]`); + func3.subtestParent = func2.subtestParent; + func3.subtestParent.asSuite.functions.push(func3); + file.functions.push(func3); + // Set up the suite. + const suite = getMockTestSuite(`${file.name}::MyTests`); + file.suites.push(suite); + const func4 = getMockTestFunction('MyTests::test_foo'); + suite.functions.push(func4); + const func5 = getMockTestFunction('MyTests::test_bar[2-3]'); + func5.subtestParent = { + name: 'test_bar', + nameToRun: `${file.name}::MyTests::test_bar`, + asSuite: { + resource: resource, + name: 'test_bar', + nameToRun: `${file.name}::MyTests::test_bar`, + functions: [func5], + suites: [], + isUnitTest: false, + isInstance: false, + xmlName: 'test_bar', + time: 0 + }, + time: 0 + }; + suite.functions.push(func5); + // Set up the tests data. + const testData = createMockTestsData([file]); + + const testExplorer = createMockTestTreeProvider(undefined, testData); + const childrens = [ + await testExplorer.getChildren(func1), + await testExplorer.getChildren(func2), + await testExplorer.getChildren(func3), + await testExplorer.getChildren(func4), + await testExplorer.getChildren(func5), + await testExplorer.getChildren(file), + await testExplorer.getChildren(suite), + await testExplorer.getChildren(func2.subtestParent.asSuite), + await testExplorer.getChildren(func3.subtestParent.asSuite), + await testExplorer.getChildren(func5.subtestParent.asSuite) + ]; + + expect(childrens).to.deep.equal([ + [], + [], + [], + [], + [], + [func1, suite, func2.subtestParent.asSuite], + [func4, func5.subtestParent.asSuite], + [func2, func3], + [func2, func3], + [func5] + ]); + test('Get children will discover only once', async () => { + const commandManager = mock(CommandManager); + const testStore = mock(TestCollectionStorageService); + const testWorkspaceFolder = new TestWorkspaceFolder({ uri: Uri.file(__filename), name: '', index: 0 }); + when(testStore.getTests(testWorkspaceFolder.workspaceFolder.uri)).thenReturn(); + when(testStore.onDidChange).thenReturn(noop as any); + + const testTreeProvider = createMockTestTreeProvider( + instance(testStore), + undefined, + undefined, + undefined, + instance(commandManager) + ); + + let tests = await testTreeProvider.getChildren(testWorkspaceFolder); + + expect(tests).to.be.lengthOf(0); + verify( + commandManager.executeCommand( + Commands.Tests_Discover, + testWorkspaceFolder, + CommandSource.testExplorer, + undefined + ) + ).once(); + + tests = await testTreeProvider.getChildren(testWorkspaceFolder); + expect(tests).to.be.lengthOf(0); + verify( + commandManager.executeCommand( + Commands.Tests_Discover, + testWorkspaceFolder, + CommandSource.testExplorer, + undefined + ) + ).once(); + }); + }); + test('Expand tree item if it does not have any parent', async () => { + const commandManager = mock(CommandManager); + const testStore = mock(TestCollectionStorageService); + const testWorkspaceFolder = new TestWorkspaceFolder({ uri: Uri.file(__filename), name: '', index: 0 }); + when(testStore.getTests(testWorkspaceFolder.workspaceFolder.uri)).thenReturn(); + when(testStore.onDidChange).thenReturn(noop as any); + const testTreeProvider = createMockTestTreeProvider( + instance(testStore), + undefined, + undefined, + undefined, + instance(commandManager) + ); + + // No parent + testTreeProvider.getParent = () => Promise.resolve(undefined); + + const element: TestFile = { + fullPath: __filename, + functions: [], + suites: [], + name: 'name', + time: 0, + resource: Uri.file(__filename), + xmlName: '', + nameToRun: '' + }; + + const node = await testTreeProvider.getTreeItem(element); + + expect(node.collapsibleState).to.equal(TreeItemCollapsibleState.Expanded); + }); + test('Expand tree item if the parent is the Workspace Folder in a multiroot scenario', async () => { + const commandManager = mock(CommandManager); + const testStore = mock(TestCollectionStorageService); + const testWorkspaceFolder = new TestWorkspaceFolder({ uri: Uri.file(__filename), name: '', index: 0 }); + when(testStore.getTests(testWorkspaceFolder.workspaceFolder.uri)).thenReturn(); + when(testStore.onDidChange).thenReturn(noop as any); + const testTreeProvider = createMockTestTreeProvider( + instance(testStore), + undefined, + undefined, + undefined, + instance(commandManager) + ); + + // Has a workspace folder as parent. + const parentFolder = new TestWorkspaceFolder({ name: '', index: 0, uri: Uri.file(__filename) }); + + testTreeProvider.getParent = () => Promise.resolve(parentFolder); + + const element: TestFile = { + fullPath: __filename, + functions: [], + suites: [], + name: 'name', + time: 0, + resource: Uri.file(__filename), + xmlName: '', + nameToRun: '' + }; + + const node = await testTreeProvider.getTreeItem(element); + + expect(node.collapsibleState).to.equal(TreeItemCollapsibleState.Expanded); + }); + test('Do not expand tree item if it does not have any parent', async () => { + const commandManager = mock(CommandManager); + const testStore = mock(TestCollectionStorageService); + const testWorkspaceFolder = new TestWorkspaceFolder({ uri: Uri.file(__filename), name: '', index: 0 }); + when(testStore.getTests(testWorkspaceFolder.workspaceFolder.uri)).thenReturn(); + when(testStore.onDidChange).thenReturn(noop as any); + const testTreeProvider = createMockTestTreeProvider( + instance(testStore), + undefined, + undefined, + undefined, + instance(commandManager) + ); + + // Has a parent folder + const parentFolder: TestFolder = { + name: '', + nameToRun: '', + resource: Uri.file(__filename), + time: 0, + testFiles: [], + folders: [] + }; + + testTreeProvider.getParent = () => Promise.resolve(parentFolder); + + const element: TestFile = { + fullPath: __filename, + functions: [], + suites: [], + name: 'name', + time: 0, + resource: Uri.file(__filename), + xmlName: '', + nameToRun: '' + }; + + const node = await testTreeProvider.getTreeItem(element); + + expect(node.collapsibleState).to.not.equal(TreeItemCollapsibleState.Expanded); + }); + }); + suite('Root Nodes', () => { + let treeProvider: TestTreeViewProvider; + setup(() => { + const store = mock(TestCollectionStorageService); + const managementService = mock(UnitTestManagementService); + when(managementService.onDidStatusChange).thenReturn(noop as any); + when(store.onDidChange).thenReturn(noop as any); + const workspace = mock(WorkspaceService); + when(workspace.onDidChangeWorkspaceFolders).thenReturn(noop as any); + const commandManager = mock(CommandManager); + treeProvider = new TestTreeViewProvider( + instance(store), + instance(managementService), + instance(workspace), + instance(commandManager), + [] + ); + }); + test('The root folder will not be displayed if there are no tests', async () => { + const children = treeProvider.getRootNodes(); + + expect(children).to.deep.equal([]); + }); + test('The root folder will not be displayed if there are no test files directly under the root', async () => { + const folder1: TestFolder = { + folders: [], + name: 'child', + nameToRun: 'child', + testFiles: [], + time: 0, + resource: Uri.file(__filename) + }; + const tests: Tests = { + rootTestFolders: [folder1], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [], + testFunctions: [], + testFolders: [], + testSuites: [] + }; + const children = treeProvider.getRootNodes(tests); + + expect(children).to.deep.equal([]); + }); + test('Files & folders under root folder are returned as children', async () => { + const rootFolderPath = path.join('a', 'b', 'root'); + const child1FolderPath = path.join('a', 'b', 'root', 'child1'); + const child2FolderPath = path.join('a', 'b', 'root', 'child2'); + const file1: TestFile = { + fullPath: path.join(rootFolderPath, 'file1'), + functions: [], + name: 'file', + nameToRun: 'file', + resource: Uri.file('file'), + suites: [], + time: 0, + xmlName: 'file' + }; + const file2: TestFile = { + fullPath: path.join(rootFolderPath, 'file2'), + functions: [], + name: 'file2', + nameToRun: 'file2', + resource: Uri.file('file2'), + suites: [], + time: 0, + xmlName: 'file2' + }; + const file3: TestFile = { + fullPath: path.join(child1FolderPath, 'file1'), + functions: [], + name: 'file3', + nameToRun: 'file3', + resource: Uri.file('file3'), + suites: [], + time: 0, + xmlName: 'file3' + }; + const child2Folder: TestFolder = { + folders: [], + name: child2FolderPath, + nameToRun: 'child3', + testFiles: [], + time: 0, + resource: Uri.file(__filename) + }; + const child1Folder: TestFolder = { + folders: [child2Folder], + name: child1FolderPath, + nameToRun: 'child2', + testFiles: [file3], + time: 0, + resource: Uri.file(__filename) + }; + const rootFolder: TestFolder = { + folders: [child1Folder], + name: rootFolderPath, + nameToRun: 'child', + testFiles: [file1, file2], + time: 0, + resource: Uri.file(__filename) + }; + const tests: Tests = { + rootTestFolders: [rootFolder], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1, file2, file3], + testFunctions: [], + testFolders: [rootFolder, child1Folder, child2Folder], + testSuites: [] + }; + const children = treeProvider.getRootNodes(tests); + + expect(children).to.be.lengthOf(3); + expect(children).to.deep.equal([file1, file2, child1Folder]); + }); + test('Root folders are returned as children', async () => { + const child1FolderPath = path.join('a', 'b', 'root1', 'child1'); + const child2FolderPath = path.join('a', 'b', 'root1', 'child1', 'child2'); + const child3FolderPath = path.join('a', 'b', 'root2', 'child3'); + const file1: TestFile = { + fullPath: path.join(child3FolderPath, 'file1'), + functions: [], + name: 'file', + nameToRun: 'file', + resource: Uri.file('file'), + suites: [], + time: 0, + xmlName: 'file' + }; + const file2: TestFile = { + fullPath: path.join(child3FolderPath, 'file2'), + functions: [], + name: 'file2', + nameToRun: 'file2', + resource: Uri.file('file2'), + suites: [], + time: 0, + xmlName: 'file2' + }; + const file3: TestFile = { + fullPath: path.join(child3FolderPath, 'file3'), + functions: [], + name: 'file3', + nameToRun: 'file3', + resource: Uri.file('file3'), + suites: [], + time: 0, + xmlName: 'file3' + }; + const child2Folder: TestFolder = { + folders: [], + name: child2FolderPath, + nameToRun: 'child3', + testFiles: [file2], + time: 0, + resource: Uri.file(__filename) + }; + const child1Folder: TestFolder = { + folders: [child2Folder], + name: child1FolderPath, + nameToRun: 'child2', + testFiles: [file1], + time: 0, + resource: Uri.file(__filename) + }; + const child3Folder: TestFolder = { + folders: [], + name: child3FolderPath, + nameToRun: 'child', + testFiles: [file3], + time: 0, + resource: Uri.file(__filename) + }; + const tests: Tests = { + rootTestFolders: [child1Folder, child3Folder], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1, file2, file3], + testFunctions: [], + testFolders: [child3Folder, child1Folder, child2Folder], + testSuites: [] + }; + const children = treeProvider.getRootNodes(tests); + + expect(children).to.be.lengthOf(2); + expect(children).to.deep.equal([child1Folder, child3Folder]); + }); + }); +}); diff --git a/src/test/testing/explorer/treeView.unit.test.ts b/src/test/testing/explorer/treeView.unit.test.ts new file mode 100644 index 000000000000..1222a55a238e --- /dev/null +++ b/src/test/testing/explorer/treeView.unit.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { TreeView } from 'vscode'; +import { ApplicationShell } from '../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import { TestTreeViewProvider } from '../../../client/testing/explorer/testTreeViewProvider'; +import { TreeViewService } from '../../../client/testing/explorer/treeView'; +import { ITestTreeViewProvider, TestDataItem } from '../../../client/testing/types'; + +// tslint:disable:no-any + +suite('Unit Tests Test Explorer Tree View', () => { + let treeViewService: TreeViewService; + let treeView: typemoq.IMock<TreeView<TestDataItem>>; + let commandManager: ICommandManager; + let appShell: IApplicationShell; + let treeViewProvider: ITestTreeViewProvider; + setup(() => { + commandManager = mock(CommandManager); + treeViewProvider = mock(TestTreeViewProvider); + appShell = mock(ApplicationShell); + treeView = typemoq.Mock.ofType<TreeView<TestDataItem>>(); + treeViewService = new TreeViewService( + instance(treeViewProvider), + [], + instance(appShell), + instance(commandManager) + ); + }); + + test('Activation will create the treeview', async () => { + await treeViewService.activate(); + verify( + appShell.createTreeView( + 'python_tests', + deepEqual({ showCollapseAll: true, treeDataProvider: instance(treeViewProvider) }) + ) + ).once(); + }); + test('Activation will add command handlers', async () => { + await treeViewService.activate(); + verify( + commandManager.registerCommand( + Commands.Test_Reveal_Test_Item, + treeViewService.onRevealTestItem, + treeViewService + ) + ).once(); + }); + test('Invoking the command handler will reveal the node in the tree', async () => { + const data = {} as any; + treeView + .setup((t) => t.reveal(typemoq.It.isAny(), { select: false })) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + when(appShell.createTreeView('python_tests', anything())).thenReturn(treeView.object); + + await treeViewService.activate(); + await treeViewService.onRevealTestItem(data); + + treeView.verifyAll(); + }); +}); diff --git a/src/test/testing/helper.ts b/src/test/testing/helper.ts new file mode 100644 index 000000000000..3f9ffbf44c89 --- /dev/null +++ b/src/test/testing/helper.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { sep } from 'path'; +import { Uri } from 'vscode'; +import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { Tests } from '../../client/testing/common/types'; + +export const RESOURCE = Uri.file(__filename); + +export function lookForTestFile(tests: Tests, testFile: string) { + let found: boolean; + // Perform case insensitive search on windows. + if (IS_WINDOWS) { + // In the mock output, we'd have paths separated using '/' (but on windows, path separators are '\') + const testFileToSearch = testFile.split(sep).join('/'); + found = tests.testFiles.some( + (t) => + (t.name.toUpperCase() === testFile.toUpperCase() || + t.name.toUpperCase() === testFileToSearch.toUpperCase()) && + t.nameToRun.toUpperCase() === t.name.toUpperCase() + ); + } else { + found = tests.testFiles.some((t) => t.name === testFile && t.nameToRun === t.name); + } + assert.equal(found, true, `Test File not found '${testFile}'`); +} + +// Return a filename that uses the OS-specific path separator. +// +// Only "/" (forward slash) in the given filename is affected. +// +// This helps with readability in test code. It allows us to use +// literals for filenames and dirnames instead of path.join(). +export function fixPath(filename: string): string { + return filename.replace(/\//, sep); +} + +// Return the indentation part of the given line. +export function getIndent(line: string): string { + const found = line.match(/^ */); + return found![0]; +} + +// Return the dedented lines in the given text. +// +// This is used to represent text concisely and readably, which is +// particularly useful for declarative definitions (e.g. in tests). +// +// (inspired by Python's textwrap.dedent()) +export function getDedentedLines(text: string): string[] { + const linesep = text.includes('\r') ? '\r\n' : '\n'; + const lines = text.split(linesep); + if (!lines) { + return [text]; + } + + if (lines[0] !== '') { + throw Error('expected actual first line to be blank'); + } + lines.shift(); + + if (lines[0] === '') { + throw Error('expected "first" line to not be blank'); + } + const leading = getIndent(lines[0]).length; + + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]; + if (getIndent(line).length < leading) { + throw Error(`line ${i} has less indent than the "first" line`); + } + lines[i] = line.substring(leading); + } + + return lines; +} diff --git a/src/test/testing/main.unit.test.ts b/src/test/testing/main.unit.test.ts new file mode 100644 index 000000000000..147fbee07733 --- /dev/null +++ b/src/test/testing/main.unit.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Disposable } from 'vscode'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { ICommandManager } from '../../client/common/application/types'; +import { AlwaysDisplayTestExplorerGroups } from '../../client/common/experiments/groups'; +import { ExperimentsManager } from '../../client/common/experiments/manager'; +import { IDisposableRegistry, IExperimentsManager } from '../../client/common/types'; +import { ServiceContainer } from '../../client/ioc/container'; +import { IServiceContainer } from '../../client/ioc/types'; +import { JediSymbolProvider } from '../../client/providers/symbolProvider'; +import { UnitTestManagementService } from '../../client/testing/main'; + +suite('Unit Tests - ManagementService', () => { + suite('Experiments', () => { + let serviceContainer: IServiceContainer; + let sandbox: sinon.SinonSandbox; + let experiment: IExperimentsManager; + let commandManager: ICommandManager; + let testManagementService: UnitTestManagementService; + setup(() => { + serviceContainer = mock(ServiceContainer); + sandbox = sinon.createSandbox(); + + sandbox.stub(UnitTestManagementService.prototype, 'registerSymbolProvider'); + sandbox.stub(UnitTestManagementService.prototype, 'registerCommands'); + sandbox.stub(UnitTestManagementService.prototype, 'registerHandlers'); + sandbox.stub(UnitTestManagementService.prototype, 'autoDiscoverTests').callsFake(() => Promise.resolve()); + + experiment = mock(ExperimentsManager); + commandManager = mock(CommandManager); + + when(serviceContainer.get<Disposable[]>(IDisposableRegistry)).thenReturn([]); + when(serviceContainer.get<IExperimentsManager>(IExperimentsManager)).thenReturn(instance(experiment)); + when(serviceContainer.get<ICommandManager>(ICommandManager)).thenReturn(instance(commandManager)); + when(commandManager.executeCommand(anything(), anything(), anything())).thenResolve(); + + testManagementService = new UnitTestManagementService(instance(serviceContainer)); + }); + teardown(() => { + sandbox.restore(); + }); + + test('Execute command if in experiment', async () => { + when(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.experiment)).thenReturn(true); + + await testManagementService.activate(instance(mock(JediSymbolProvider))); + + verify(commandManager.executeCommand('setContext', 'testsDiscovered', true)).once(); + verify(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.experiment)).once(); + verify(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.control)).never(); + verify(experiment.sendTelemetryIfInExperiment(anything())).never(); + }); + test('If not in experiment, check and send Telemetry for control group and do not execute command', async () => { + when(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.experiment)).thenReturn(false); + + await testManagementService.activate(instance(mock(JediSymbolProvider))); + + verify(commandManager.executeCommand('setContext', 'testsDiscovered', anything())).never(); + verify(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.experiment)).once(); + verify(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.control)).never(); + verify(experiment.sendTelemetryIfInExperiment(AlwaysDisplayTestExplorerGroups.control)).once(); + }); + }); +}); diff --git a/src/test/testing/mocks.ts b/src/test/testing/mocks.ts new file mode 100644 index 000000000000..b41fc3e2da6a --- /dev/null +++ b/src/test/testing/mocks.ts @@ -0,0 +1,136 @@ +import { EventEmitter } from 'events'; +import { injectable } from 'inversify'; +import { CancellationToken, Disposable, Uri } from 'vscode'; +import { Product } from '../../client/common/types'; +import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { IServiceContainer } from '../../client/ioc/types'; +import { CANCELLATION_REASON } from '../../client/testing/common/constants'; +import { BaseTestManager } from '../../client/testing/common/managers/baseTestManager'; +import { + ITestDebugLauncher, + ITestDiscoveryService, + IUnitTestSocketServer, + LaunchOptions, + TestDiscoveryOptions, + TestProvider, + Tests, + TestsToRun +} from '../../client/testing/common/types'; + +@injectable() +export class MockDebugLauncher implements ITestDebugLauncher, Disposable { + public get launched(): Promise<boolean> { + return this._launched.promise; + } + public get debuggerPromise(): Deferred<Tests> { + // tslint:disable-next-line:no-non-null-assertion + return this._promise!; + } + public get cancellationToken(): CancellationToken { + if (this._token === undefined) { + throw Error('debugger not launched'); + } + return this._token; + } + // tslint:disable-next-line:variable-name + private _launched: Deferred<boolean>; + // tslint:disable-next-line:variable-name + private _promise?: Deferred<Tests>; + // tslint:disable-next-line:variable-name + private _token?: CancellationToken; + constructor() { + this._launched = createDeferred<boolean>(); + } + public async getLaunchOptions(_resource?: Uri): Promise<{ port: number; host: string }> { + return { port: 0, host: 'localhost' }; + } + public async launchDebugger(options: LaunchOptions): Promise<void> { + this._launched.resolve(true); + // tslint:disable-next-line:no-non-null-assertion + this._token = options.token!; + this._promise = createDeferred<Tests>(); + // tslint:disable-next-line:no-non-null-assertion + options.token!.onCancellationRequested(() => { + if (this._promise) { + this._promise.reject('Mock-User Cancelled'); + } + }); + return (this._promise.promise as {}) as Promise<void>; + } + public dispose() { + this._promise = undefined; + } +} + +@injectable() +export class MockTestManagerWithRunningTests extends BaseTestManager { + // tslint:disable-next-line:no-any + public readonly runnerDeferred = createDeferred<Tests>(); + public readonly enabled = true; + // tslint:disable-next-line:no-any + public readonly discoveryDeferred = createDeferred<Tests>(); + constructor( + testProvider: TestProvider, + product: Product, + workspaceFolder: Uri, + rootDirectory: string, + serviceContainer: IServiceContainer + ) { + super(testProvider, product, workspaceFolder, rootDirectory, serviceContainer); + } + protected getDiscoveryOptions(_ignoreCache: boolean) { + // tslint:disable-next-line:no-object-literal-type-assertion + return {} as TestDiscoveryOptions; + } + // tslint:disable-next-line:no-any + protected async runTestImpl( + _tests: Tests, + _testsToRun?: TestsToRun, + _runFailedTests?: boolean, + _debug?: boolean + ): Promise<Tests> { + // tslint:disable-next-line:no-non-null-assertion + this.testRunnerCancellationToken!.onCancellationRequested(() => { + this.runnerDeferred.reject(CANCELLATION_REASON); + }); + return this.runnerDeferred.promise; + } + protected async discoverTestsImpl(_ignoreCache: boolean, _debug?: boolean): Promise<Tests> { + // tslint:disable-next-line:no-non-null-assertion + this.testDiscoveryCancellationToken!.onCancellationRequested(() => { + this.discoveryDeferred.reject(CANCELLATION_REASON); + }); + return this.discoveryDeferred.promise; + } +} + +@injectable() +export class MockDiscoveryService implements ITestDiscoveryService { + constructor(private discoverPromise: Promise<Tests>) {} + public async discoverTests(_options: TestDiscoveryOptions): Promise<Tests> { + return this.discoverPromise; + } +} + +// tslint:disable-next-line:max-classes-per-file +@injectable() +export class MockUnitTestSocketServer extends EventEmitter implements IUnitTestSocketServer { + private results: {}[] = []; + public reset() { + this.removeAllListeners(); + } + public addResults(results: {}[]) { + this.results.push(...results); + } + public async start(options: { port: number; host: string } = { port: 0, host: 'localhost' }): Promise<number> { + this.results.forEach((result) => { + this.emit('result', result); + }); + this.results = []; + return typeof options.port === 'number' ? options.port! : 0; + } + // tslint:disable-next-line:no-empty + public stop(): void {} + // tslint:disable-next-line:no-empty + public dispose() {} +} diff --git a/src/test/testing/navigation/commandHandlers.unit.test.ts b/src/test/testing/navigation/commandHandlers.unit.test.ts new file mode 100644 index 000000000000..be420ec7ab76 --- /dev/null +++ b/src/test/testing/navigation/commandHandlers.unit.test.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { CommandManager } from '../../../client/common/application/commandManager'; +import { ICommandManager } from '../../../client/common/application/types'; +import { AsyncDisposableRegistry } from '../../../client/common/asyncDisposableRegistry'; +import { Commands } from '../../../client/common/constants'; +import { IDisposable, IDisposableRegistry } from '../../../client/common/types'; +import { TestCodeNavigatorCommandHandler } from '../../../client/testing/navigation/commandHandler'; +import { TestFileCodeNavigator } from '../../../client/testing/navigation/fileNavigator'; +import { TestFunctionCodeNavigator } from '../../../client/testing/navigation/functionNavigator'; +import { TestSuiteCodeNavigator } from '../../../client/testing/navigation/suiteNavigator'; +import { ITestCodeNavigator, ITestCodeNavigatorCommandHandler } from '../../../client/testing/navigation/types'; + +// tslint:disable:max-func-body-length +suite('Unit Tests - Navigation Command Handler', () => { + let commandHandler: ITestCodeNavigatorCommandHandler; + let cmdManager: ICommandManager; + let fileHandler: ITestCodeNavigator; + let functionHandler: ITestCodeNavigator; + let suiteHandler: ITestCodeNavigator; + let disposableRegistry: IDisposableRegistry; + setup(() => { + cmdManager = mock(CommandManager); + fileHandler = mock(TestFileCodeNavigator); + functionHandler = mock(TestFunctionCodeNavigator); + suiteHandler = mock(TestSuiteCodeNavigator); + // tslint:disable-next-line: no-any + disposableRegistry = mock(AsyncDisposableRegistry) as any; + commandHandler = new TestCodeNavigatorCommandHandler( + instance(cmdManager), + instance(fileHandler), + instance(functionHandler), + instance(suiteHandler), + instance(disposableRegistry) + ); + }); + test('Ensure Navigation handlers are registered', async () => { + commandHandler.register(); + verify( + cmdManager.registerCommand( + Commands.navigateToTestFile, + instance(fileHandler).navigateTo, + instance(fileHandler) + ) + ).once(); + verify( + cmdManager.registerCommand( + Commands.navigateToTestFunction, + instance(functionHandler).navigateTo, + instance(functionHandler) + ) + ).once(); + verify( + cmdManager.registerCommand( + Commands.navigateToTestSuite, + instance(suiteHandler).navigateTo, + instance(suiteHandler) + ) + ).once(); + }); + test('Ensure handlers are disposed', async () => { + const disposable1 = typemoq.Mock.ofType<IDisposable>(); + const disposable2 = typemoq.Mock.ofType<IDisposable>(); + const disposable3 = typemoq.Mock.ofType<IDisposable>(); + when( + cmdManager.registerCommand( + Commands.navigateToTestFile, + instance(fileHandler).navigateTo, + instance(fileHandler) + ) + ).thenReturn(disposable1.object); + when( + cmdManager.registerCommand( + Commands.navigateToTestFunction, + instance(functionHandler).navigateTo, + instance(functionHandler) + ) + ).thenReturn(disposable2.object); + when( + cmdManager.registerCommand( + Commands.navigateToTestSuite, + instance(suiteHandler).navigateTo, + instance(suiteHandler) + ) + ).thenReturn(disposable3.object); + + commandHandler.register(); + commandHandler.dispose(); + + disposable1.verify((d) => d.dispose(), typemoq.Times.once()); + disposable2.verify((d) => d.dispose(), typemoq.Times.once()); + disposable3.verify((d) => d.dispose(), typemoq.Times.once()); + }); + test('Ensure command handler is reigstered to be disposed', async () => { + commandHandler.register(); + verify(disposableRegistry.push(commandHandler)).once(); + }); +}); diff --git a/src/test/testing/navigation/fileNavigator.unit.test.ts b/src/test/testing/navigation/fileNavigator.unit.test.ts new file mode 100644 index 000000000000..4147a2e3def8 --- /dev/null +++ b/src/test/testing/navigation/fileNavigator.unit.test.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaisAsPromised from 'chai-as-promised'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { TestFileCodeNavigator } from '../../../client/testing/navigation/fileNavigator'; +import { TestNavigatorHelper } from '../../../client/testing/navigation/helper'; +import { ITestNavigatorHelper } from '../../../client/testing/navigation/types'; + +use(chaisAsPromised); + +// tslint:disable:max-func-body-length no-any +suite('Unit Tests - Navigation File', () => { + let navigator: TestFileCodeNavigator; + let helper: ITestNavigatorHelper; + setup(() => { + helper = mock(TestNavigatorHelper); + navigator = new TestFileCodeNavigator(instance(helper)); + }); + test('Ensure file is opened', async () => { + const filePath = Uri.file('some file Path'); + when(helper.openFile(anything())).thenResolve(); + + await navigator.navigateTo(filePath, { fullPath: filePath.fsPath } as any, false); + + verify(helper.openFile(anything())).once(); + expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); + }); + test('Ensure errors are swallowed', async () => { + const filePath = Uri.file('some file Path'); + when(helper.openFile(anything())).thenReject(new Error('kaboom')); + + await navigator.navigateTo(filePath, { fullPath: filePath.fsPath } as any, false); + + verify(helper.openFile(anything())).once(); + expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); + }); +}); diff --git a/src/test/testing/navigation/functionNavigator.unit.test.ts b/src/test/testing/navigation/functionNavigator.unit.test.ts new file mode 100644 index 000000000000..c883a3fb97a4 --- /dev/null +++ b/src/test/testing/navigation/functionNavigator.unit.test.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaisAsPromised from 'chai-as-promised'; +import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { + Location, + Range, + SymbolInformation, + SymbolKind, + TextDocument, + TextEditor, + TextEditorRevealType, + Uri +} from 'vscode'; +import { DocumentManager } from '../../../client/common/application/documentManager'; +import { IDocumentManager } from '../../../client/common/application/types'; +import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; +import { ITestCollectionStorageService } from '../../../client/testing/common/types'; +import { TestFunctionCodeNavigator } from '../../../client/testing/navigation/functionNavigator'; +import { TestNavigatorHelper } from '../../../client/testing/navigation/helper'; +import { ITestNavigatorHelper } from '../../../client/testing/navigation/types'; + +use(chaisAsPromised); + +// tslint:disable:max-func-body-length no-any +suite('Unit Tests - Navigation Function', () => { + let navigator: TestFunctionCodeNavigator; + let helper: ITestNavigatorHelper; + let docManager: IDocumentManager; + let doc: typemoq.IMock<TextDocument>; + let editor: typemoq.IMock<TextEditor>; + let storage: ITestCollectionStorageService; + setup(() => { + doc = typemoq.Mock.ofType<TextDocument>(); + editor = typemoq.Mock.ofType<TextEditor>(); + helper = mock(TestNavigatorHelper); + docManager = mock(DocumentManager); + storage = mock(TestCollectionStorageService); + navigator = new TestFunctionCodeNavigator(instance(helper), instance(docManager), instance(storage)); + }); + test('Ensure file is opened', async () => { + const filePath = Uri.file('some file Path'); + when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); + const flattenedFn = { parentTestFile: { fullPath: filePath.fsPath }, testFunction: {} }; + when(storage.findFlattendTestFunction(filePath, anything())).thenReturn(flattenedFn as any); + + await navigator.navigateTo(filePath, {} as any); + + verify(helper.openFile(anything())).once(); + expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); + }); + test('Ensure errors are swallowed', async () => { + const filePath = Uri.file('some file Path'); + when(helper.openFile(anything())).thenReject(new Error('kaboom')); + const flattenedFn = { parentTestFile: { fullPath: filePath.fsPath }, testFunction: {} }; + when(storage.findFlattendTestFunction(filePath, anything())).thenReturn(flattenedFn as any); + + await navigator.navigateTo(filePath, {} as any); + + verify(helper.openFile(anything())).once(); + expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); + }); + async function navigateToFunction(focusCode: boolean) { + const filePath = Uri.file('some file Path'); + const line = 999; + when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); + const flattenedFn = { parentTestFile: { fullPath: filePath.fsPath }, testFunction: { name: 'function_name' } }; + when(storage.findFlattendTestFunction(filePath, anything())).thenReturn(flattenedFn as any); + const range = new Range(line, 0, line, 0); + const symbol: SymbolInformation = { + containerName: '', + kind: SymbolKind.Function, + name: 'function_name', + location: new Location(Uri.file(__filename), range) + }; + when(helper.findSymbol(doc.object, anything(), anything())).thenResolve(symbol); + + await navigator.navigateTo(filePath, { name: 'function_name' } as any, focusCode); + + verify(helper.openFile(anything())).once(); + verify(helper.findSymbol(doc.object, anything(), anything())).once(); + expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); + if (focusCode) { + verify( + docManager.showTextDocument(doc.object, deepEqual({ preserveFocus: false, selection: range })) + ).once(); + } else { + editor.verify((e) => e.revealRange(typemoq.It.isAny(), TextEditorRevealType.Default), typemoq.Times.once()); + } + } + test('Ensure we use line number from test function when navigating in file (without focusing code)', async () => { + await navigateToFunction(false); + }); + test('Ensure we use line number from test function when navigating in file (focusing code)', async () => { + await navigateToFunction(true); + }); + test('Ensure file is opened and range not revealed', async () => { + const filePath = Uri.file('some file Path'); + when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); + const flattenedFn = { parentTestFile: { fullPath: filePath.fsPath }, testFunction: {} }; + when(storage.findFlattendTestFunction(filePath, anything())).thenReturn(flattenedFn as any); + const search = (s: SymbolInformation) => s.kind === SymbolKind.Function && s.name === 'Hello'; + when(helper.findSymbol(doc.object, search, anything())).thenResolve(); + + await navigator.navigateTo(filePath, {} as any); + + verify(helper.openFile(anything())).once(); + expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); + editor.verify((e) => e.revealRange(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + }); +}); diff --git a/src/test/testing/navigation/helper.unit.test.ts b/src/test/testing/navigation/helper.unit.test.ts new file mode 100644 index 000000000000..bd0ec9aea0d5 --- /dev/null +++ b/src/test/testing/navigation/helper.unit.test.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaisAsPromised from 'chai-as-promised'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { + CancellationTokenSource, + DocumentSymbolProvider, + SymbolInformation, + SymbolKind, + TextDocument, + TextEditor, + Uri +} from 'vscode'; +import { DocumentManager } from '../../../client/common/application/documentManager'; +import { IDocumentManager } from '../../../client/common/application/types'; +import { LanguageServerSymbolProvider } from '../../../client/providers/symbolProvider'; +import { TestNavigatorHelper } from '../../../client/testing/navigation/helper'; + +use(chaisAsPromised); + +// tslint:disable:max-func-body-length no-any +suite('Unit Tests - Navigation Helper', () => { + let helper: TestNavigatorHelper; + let docManager: IDocumentManager; + let doc: typemoq.IMock<TextDocument>; + let editor: typemoq.IMock<TextEditor>; + let symbolProvider: DocumentSymbolProvider; + setup(() => { + doc = typemoq.Mock.ofType<TextDocument>(); + editor = typemoq.Mock.ofType<TextEditor>(); + doc.setup((d: any) => d.then).returns(() => undefined); + editor.setup((e: any) => e.then).returns(() => undefined); + docManager = mock(DocumentManager); + symbolProvider = mock(LanguageServerSymbolProvider); + helper = new TestNavigatorHelper(instance(docManager), instance(symbolProvider)); + }); + test('Ensure file is opened', async () => { + const filePath = Uri.file('some file Path'); + when(docManager.openTextDocument(anything())).thenResolve(doc.object as any); + when(docManager.showTextDocument(doc.object)).thenResolve(editor.object as any); + + const [d, e] = await helper.openFile(filePath); + + verify(docManager.openTextDocument(filePath)).once(); + verify(docManager.showTextDocument(doc.object)).once(); + expect(d).to.deep.equal(doc.object); + expect(e).to.deep.equal(editor.object); + }); + test('No symbols if symbol provider is not registered', async () => { + const token = new CancellationTokenSource().token; + const predicate = (s: SymbolInformation) => s.kind === SymbolKind.Function && s.name === ''; + const symbol = await helper.findSymbol(doc.object, predicate, token); + expect(symbol).to.equal(undefined, 'Must be undefined'); + }); + test('No symbols if no symbols', async () => { + const token = new CancellationTokenSource().token; + when(symbolProvider.provideDocumentSymbols(doc.object, token)).thenResolve([] as any); + + const predicate = (s: SymbolInformation) => s.kind === SymbolKind.Function && s.name === ''; + const symbol = await helper.findSymbol(doc.object, predicate, token); + + expect(symbol).to.equal(undefined, 'Must be undefined'); + verify(symbolProvider.provideDocumentSymbols(doc.object, token)).once(); + }); + test('Returns matching symbol', async () => { + const symbols: SymbolInformation[] = [ + { containerName: '', kind: SymbolKind.Function, name: '1', location: undefined as any }, + { containerName: '', kind: SymbolKind.Class, name: '2', location: undefined as any }, + { containerName: '', kind: SymbolKind.File, name: '2', location: undefined as any } + ]; + const token = new CancellationTokenSource().token; + when(symbolProvider.provideDocumentSymbols(doc.object, token)).thenResolve(symbols as any); + + const predicate = (s: SymbolInformation) => s.kind === SymbolKind.Class && s.name === '2'; + const symbol = await helper.findSymbol(doc.object, predicate, token); + + expect(symbol).to.deep.equal(symbols[1]); + verify(symbolProvider.provideDocumentSymbols(doc.object, token)).once(); + }); +}); diff --git a/src/test/testing/navigation/serviceRegistry.unit.test.ts b/src/test/testing/navigation/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..85687b9c606a --- /dev/null +++ b/src/test/testing/navigation/serviceRegistry.unit.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { use } from 'chai'; +import * as chaisAsPromised from 'chai-as-promised'; +import { anything, instance, mock, verify } from 'ts-mockito'; +import { IDocumentSymbolProvider } from '../../../client/common/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { TestCodeNavigatorCommandHandler } from '../../../client/testing/navigation/commandHandler'; +import { TestFileCodeNavigator } from '../../../client/testing/navigation/fileNavigator'; +import { TestFunctionCodeNavigator } from '../../../client/testing/navigation/functionNavigator'; +import { TestNavigatorHelper } from '../../../client/testing/navigation/helper'; +import { registerTypes } from '../../../client/testing/navigation/serviceRegistry'; +import { TestSuiteCodeNavigator } from '../../../client/testing/navigation/suiteNavigator'; +import { TestFileSymbolProvider } from '../../../client/testing/navigation/symbolProvider'; +import { + ITestCodeNavigator, + ITestCodeNavigatorCommandHandler, + ITestNavigatorHelper, + NavigableItemType +} from '../../../client/testing/navigation/types'; + +use(chaisAsPromised); + +// tslint:disable:max-func-body-length no-any +suite('Unit Tests - Navigation Service Registry', () => { + test('Ensure services are registered', async () => { + const serviceManager = mock(ServiceManager); + + registerTypes(instance(serviceManager)); + + verify(serviceManager.addSingleton<ITestNavigatorHelper>(ITestNavigatorHelper, TestNavigatorHelper)).once(); + verify( + serviceManager.addSingleton<ITestCodeNavigatorCommandHandler>( + ITestCodeNavigatorCommandHandler, + TestCodeNavigatorCommandHandler + ) + ).once(); + verify( + serviceManager.addSingleton<ITestCodeNavigator>( + ITestCodeNavigator, + TestFileCodeNavigator, + NavigableItemType.testFile + ) + ).once(); + verify( + serviceManager.addSingleton<ITestCodeNavigator>( + ITestCodeNavigator, + TestFunctionCodeNavigator, + NavigableItemType.testFunction + ) + ).once(); + verify( + serviceManager.addSingleton<ITestCodeNavigator>( + ITestCodeNavigator, + TestSuiteCodeNavigator, + NavigableItemType.testSuite + ) + ).once(); + verify(serviceManager.addSingleton<IDocumentSymbolProvider>(anything(), TestFileSymbolProvider, 'test')).once(); + }); +}); diff --git a/src/test/testing/navigation/suiteNavigator.unit.test.ts b/src/test/testing/navigation/suiteNavigator.unit.test.ts new file mode 100644 index 000000000000..df06fba2b6b4 --- /dev/null +++ b/src/test/testing/navigation/suiteNavigator.unit.test.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaisAsPromised from 'chai-as-promised'; +import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { + Location, + Range, + SymbolInformation, + SymbolKind, + TextDocument, + TextEditor, + TextEditorRevealType, + Uri +} from 'vscode'; +import { DocumentManager } from '../../../client/common/application/documentManager'; +import { IDocumentManager } from '../../../client/common/application/types'; +import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; +import { ITestCollectionStorageService } from '../../../client/testing/common/types'; +import { TestNavigatorHelper } from '../../../client/testing/navigation/helper'; +import { TestSuiteCodeNavigator } from '../../../client/testing/navigation/suiteNavigator'; +import { ITestNavigatorHelper } from '../../../client/testing/navigation/types'; + +use(chaisAsPromised); + +// tslint:disable:max-func-body-length no-any +suite('Unit Tests - Navigation Suite', () => { + let navigator: TestSuiteCodeNavigator; + let helper: ITestNavigatorHelper; + let docManager: IDocumentManager; + let doc: typemoq.IMock<TextDocument>; + let editor: typemoq.IMock<TextEditor>; + let storage: ITestCollectionStorageService; + setup(() => { + doc = typemoq.Mock.ofType<TextDocument>(); + editor = typemoq.Mock.ofType<TextEditor>(); + helper = mock(TestNavigatorHelper); + docManager = mock(DocumentManager); + storage = mock(TestCollectionStorageService); + navigator = new TestSuiteCodeNavigator(instance(helper), instance(docManager), instance(storage)); + }); + test('Ensure file is opened', async () => { + const filePath = Uri.file('some file Path'); + when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); + const flattenedSuite = { parentTestFile: { fullPath: filePath.fsPath }, testSuite: {} }; + when(storage.findFlattendTestSuite(filePath, anything())).thenReturn(flattenedSuite as any); + + await navigator.navigateTo(filePath, {} as any); + + verify(helper.openFile(anything())).once(); + expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); + }); + test('Ensure errors are swallowed', async () => { + const filePath = Uri.file('some file Path'); + when(helper.openFile(anything())).thenReject(new Error('kaboom')); + const flattenedSuite = { parentTestFile: { fullPath: filePath.fsPath }, testSuite: {} }; + when(storage.findFlattendTestSuite(filePath, anything())).thenReturn(flattenedSuite as any); + + await navigator.navigateTo(filePath, {} as any); + + verify(helper.openFile(anything())).once(); + expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); + }); + async function navigateUsingLineFromSuite(focusCode: boolean) { + const filePath = Uri.file('some file Path'); + const line = 999; + when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); + const flattenedSuite = { parentTestFile: { fullPath: filePath.fsPath }, testSuite: { name: 'suite_name' } }; + when(storage.findFlattendTestSuite(filePath, anything())).thenReturn(flattenedSuite as any); + const range = new Range(line, 0, line, 0); + const symbol: SymbolInformation = { + containerName: '', + kind: SymbolKind.Class, + name: 'suite_name', + location: new Location(Uri.file(__filename), range) + }; + when(helper.findSymbol(doc.object, anything(), anything())).thenResolve(symbol); + + await navigator.navigateTo(filePath, { name: 'suite_name' } as any, focusCode); + + verify(helper.openFile(anything())).once(); + verify(helper.findSymbol(doc.object, anything(), anything())).once(); + expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); + if (focusCode) { + verify( + docManager.showTextDocument(doc.object, deepEqual({ preserveFocus: false, selection: range })) + ).once(); + } else { + editor.verify((e) => e.revealRange(range, TextEditorRevealType.Default), typemoq.Times.once()); + } + } + test('Ensure we use line number from test suite when navigating in file (without focusing code)', async () => { + await navigateUsingLineFromSuite(false); + }); + test('Ensure we use line number from test suite when navigating in file (focusing code)', async () => { + await navigateUsingLineFromSuite(true); + }); + async function navigateFromSuite(focusCode: boolean) { + const filePath = Uri.file('some file Path'); + const line = 999; + when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); + const flattenedSuite = { parentTestFile: { fullPath: filePath.fsPath }, testSuite: { line } }; + when(storage.findFlattendTestSuite(filePath, anything())).thenReturn(flattenedSuite as any); + const range = new Range(line, 0, line, 0); + + await navigator.navigateTo(filePath, { line } as any, focusCode); + + verify(helper.openFile(anything())).once(); + verify(helper.findSymbol(anything(), anything(), anything())).never(); + expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); + if (focusCode) { + verify( + docManager.showTextDocument(doc.object, deepEqual({ preserveFocus: false, selection: range })) + ).once(); + } else { + editor.verify((e) => e.revealRange(range, TextEditorRevealType.Default), typemoq.Times.once()); + } + } + test('Navigating in file (without focusing code)', async () => { + await navigateFromSuite(false); + }); + test('Navigating in file (focusing code)', async () => { + await navigateFromSuite(true); + }); + test('Ensure file is opened and range not revealed', async () => { + const filePath = Uri.file('some file Path'); + when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); + const flattenedSuite = { parentTestFile: { fullPath: filePath.fsPath }, testSuite: {} }; + when(storage.findFlattendTestSuite(filePath, anything())).thenReturn(flattenedSuite as any); + const search = (s: SymbolInformation) => s.kind === SymbolKind.Class && s.name === 'Hello'; + when(helper.findSymbol(doc.object, search, anything())).thenResolve(); + + await navigator.navigateTo(filePath, {} as any); + + verify(helper.openFile(anything())).once(); + expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); + editor.verify((e) => e.revealRange(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + }); +}); diff --git a/src/test/testing/navigation/symbolNavigator.unit.test.ts b/src/test/testing/navigation/symbolNavigator.unit.test.ts new file mode 100644 index 000000000000..744ad16b0ab2 --- /dev/null +++ b/src/test/testing/navigation/symbolNavigator.unit.test.ts @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import { + CancellationToken, + CancellationTokenSource, + Range, + SymbolInformation, + SymbolKind, + TextDocument, + Uri +} from 'vscode'; +import { + ExecutionResult, + IPythonExecutionFactory, + IPythonExecutionService +} from '../../../client/common/process/types'; +import { IDocumentSymbolProvider } from '../../../client/common/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { TestFileSymbolProvider } from '../../../client/testing/navigation/symbolProvider'; + +// tslint:disable:max-func-body-length no-any +suite('Unit Tests - Navigation Command Handler', () => { + let symbolProvider: IDocumentSymbolProvider; + let pythonExecFactory: typemoq.IMock<IPythonExecutionFactory>; + let pythonService: typemoq.IMock<IPythonExecutionService>; + let doc: typemoq.IMock<TextDocument>; + let token: CancellationToken; + setup(() => { + pythonService = typemoq.Mock.ofType<IPythonExecutionService>(); + pythonExecFactory = typemoq.Mock.ofType<IPythonExecutionFactory>(); + + // Both typemoq and ts-mockito fail to resolve promises on dynamically created mocks + // A solution is to mock the `then` on the mock that the `Promise` resolves to. + // typemoq: https://github.com/florinn/typemoq/issues/66#issuecomment-315681245 + // ts-mockito: https://github.com/NagRock/ts-mockito/issues/163#issuecomment-536210863 + // In this case, the factory below returns a promise that is a mock of python service + // so we need to mock the `then` on the service. + pythonService.setup((x: any) => x.then).returns(() => undefined); + + pythonExecFactory + .setup((factory) => factory.create(typemoq.It.isAny())) + .returns(async () => pythonService.object); + + doc = typemoq.Mock.ofType<TextDocument>(); + token = new CancellationTokenSource().token; + }); + test('Ensure no symbols are returned when file has not been saved', async () => { + doc.setup((d) => d.isUntitled) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + symbolProvider = new TestFileSymbolProvider(pythonExecFactory.object); + const symbols = await symbolProvider.provideDocumentSymbols(doc.object, token); + + expect(symbols).to.be.lengthOf(0); + doc.verifyAll(); + }); + test('Ensure no symbols are returned when there are errors in running the code', async () => { + doc.setup((d) => d.isUntitled) + .returns(() => false) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.isDirty) + .returns(() => false) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.uri) + .returns(() => Uri.file(__filename)) + .verifiable(typemoq.Times.atLeastOnce()); + + pythonService + .setup((service) => service.exec(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(async () => { + return { stdout: '' }; + }); + + symbolProvider = new TestFileSymbolProvider(pythonExecFactory.object); + const symbols = await symbolProvider.provideDocumentSymbols(doc.object, token); + + expect(symbols).to.be.lengthOf(0); + doc.verifyAll(); + }); + test('Ensure no symbols are returned when there are no symbols to be returned', async () => { + const docUri = Uri.file(__filename); + const args = [ + path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'pyvsc-run-isolated.py'), + path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'symbolProvider.py'), + docUri.fsPath + ]; + const proc: ExecutionResult<string> = { + stdout: JSON.stringify({ classes: [], methods: [], functions: [] }) + }; + doc.setup((d) => d.isUntitled) + .returns(() => false) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.isDirty) + .returns(() => false) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.uri) + .returns(() => docUri) + .verifiable(typemoq.Times.atLeastOnce()); + + pythonService + .setup((service) => service.exec(typemoq.It.isValue(args), typemoq.It.isAny())) + .returns(async () => proc) + .verifiable(typemoq.Times.once()); + + symbolProvider = new TestFileSymbolProvider(pythonExecFactory.object); + const symbols = await symbolProvider.provideDocumentSymbols(doc.object, token); + + expect(symbols).to.be.lengthOf(0); + doc.verifyAll(); + pythonService.verifyAll(); + }); + test('Ensure symbols are returned', async () => { + const docUri = Uri.file(__filename); + const args = [ + path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'pyvsc-run-isolated.py'), + path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'symbolProvider.py'), + docUri.fsPath + ]; + const proc: ExecutionResult<string> = { + stdout: JSON.stringify({ + classes: [ + { + namespace: '1', + name: 'one', + kind: SymbolKind.Class, + range: { start: { line: 1, character: 2 }, end: { line: 3, character: 4 } } + } + ], + methods: [ + { + namespace: '2', + name: 'two', + kind: SymbolKind.Class, + range: { start: { line: 5, character: 6 }, end: { line: 7, character: 8 } } + } + ], + functions: [ + { + namespace: '3', + name: 'three', + kind: SymbolKind.Class, + range: { start: { line: 9, character: 10 }, end: { line: 11, character: 12 } } + } + ] + }) + }; + doc.setup((d) => d.isUntitled) + .returns(() => false) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.isDirty) + .returns(() => false) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.uri) + .returns(() => docUri) + .verifiable(typemoq.Times.atLeastOnce()); + + pythonService + .setup((service) => service.exec(typemoq.It.isValue(args), typemoq.It.isAny())) + .returns(async () => proc) + .verifiable(typemoq.Times.once()); + + symbolProvider = new TestFileSymbolProvider(pythonExecFactory.object); + const symbols = (await symbolProvider.provideDocumentSymbols(doc.object, token)) as SymbolInformation[]; + + expect(symbols).to.be.lengthOf(3); + doc.verifyAll(); + pythonService.verifyAll(); + expect(symbols[0].kind).to.be.equal(SymbolKind.Class); + expect(symbols[0].name).to.be.equal('one'); + expect(symbols[0].location.range).to.be.deep.equal(new Range(1, 2, 3, 4)); + + expect(symbols[1].kind).to.be.equal(SymbolKind.Method); + expect(symbols[1].name).to.be.equal('two'); + expect(symbols[1].location.range).to.be.deep.equal(new Range(5, 6, 7, 8)); + + expect(symbols[2].kind).to.be.equal(SymbolKind.Function); + expect(symbols[2].name).to.be.equal('three'); + expect(symbols[2].location.range).to.be.deep.equal(new Range(9, 10, 11, 12)); + }); +}); diff --git a/src/test/testing/nosetest/nosetest.argsService.unit.test.ts b/src/test/testing/nosetest/nosetest.argsService.unit.test.ts new file mode 100644 index 000000000000..0b5224176b1a --- /dev/null +++ b/src/test/testing/nosetest/nosetest.argsService.unit.test.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import * as typeMoq from 'typemoq'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ArgumentsHelper } from '../../../client/testing/common/argumentsHelper'; +import { ArgumentsService as NoseTestArgumentsService } from '../../../client/testing/nosetest/services/argsService'; +import { IArgumentsHelper } from '../../../client/testing/types'; + +suite('ArgsService: nosetest', () => { + let argumentsService: NoseTestArgumentsService; + + suiteSetup(() => { + const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + + const argsHelper = new ArgumentsHelper(); + + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IArgumentsHelper), typeMoq.It.isAny())) + .returns(() => argsHelper); + + argumentsService = new NoseTestArgumentsService(serviceContainer.object); + }); + + test('Test getting the test folder in nosetest', () => { + const dir = path.join('a', 'b', 'c'); + const args = ['--one', '--three', dir]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in nosetest (with multiple dirs)', () => { + const dir = path.join('a', 'b', 'c'); + const dir2 = path.join('a', 'b', '2'); + const args = ['anzy', '--one', '--three', dir, dir2]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(3); + expect(testDirs[0]).to.equal('anzy'); + expect(testDirs[1]).to.equal(dir); + expect(testDirs[2]).to.equal(dir2); + }); +}); diff --git a/src/test/testing/nosetest/nosetest.discovery.unit.test.ts b/src/test/testing/nosetest/nosetest.discovery.unit.test.ts new file mode 100644 index 000000000000..a6dfb25e46cb --- /dev/null +++ b/src/test/testing/nosetest/nosetest.discovery.unit.test.ts @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable-next-line:max-func-body-length + +import { expect, use } from 'chai'; +import * as chaipromise from 'chai-as-promised'; +import * as typeMoq from 'typemoq'; +import { CancellationToken } from 'vscode'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { NOSETEST_PROVIDER } from '../../../client/testing/common/constants'; +import { + ITestDiscoveryService, + ITestRunner, + ITestsParser, + Options, + TestDiscoveryOptions, + Tests +} from '../../../client/testing/common/types'; +import { TestDiscoveryService } from '../../../client/testing/nosetest/services/discoveryService'; +import { IArgumentsService, TestFilter } from '../../../client/testing/types'; + +use(chaipromise); + +// tslint:disable-next-line: max-func-body-length +suite('Unit Tests - nose - Discovery', () => { + let discoveryService: ITestDiscoveryService; + let argsService: typeMoq.IMock<IArgumentsService>; + let testParser: typeMoq.IMock<ITestsParser>; + let runner: typeMoq.IMock<ITestRunner>; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + argsService = typeMoq.Mock.ofType<IArgumentsService>(); + testParser = typeMoq.Mock.ofType<ITestsParser>(); + runner = typeMoq.Mock.ofType<ITestRunner>(); + + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IArgumentsService), typeMoq.It.isAny())) + .returns(() => argsService.object); + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(ITestRunner), typeMoq.It.isAny())) + .returns(() => runner.object); + + discoveryService = new TestDiscoveryService(serviceContainer.object, testParser.object); + }); + test('Ensure discovery is invoked with the right args', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFunctions: [], + testSuites: [], + rootTestFolders: [], + testFolders: [] + }; + argsService + .setup((a) => a.filterArguments(typeMoq.It.isValue(args), typeMoq.It.isValue(TestFilter.discovery))) + .returns(() => []) + .verifiable(typeMoq.Times.once()); + runner + .setup((r) => r.run(typeMoq.It.isValue(NOSETEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('--collect-only'); + expect(opts.args).to.include('-vvv'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser + .setup((t) => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType<TestDiscoveryOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + options.setup((o) => o.args).returns(() => args); + options.setup((o) => o.token).returns(() => token.object); + token.setup((t) => t.isCancellationRequested).returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + argsService.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is cancelled', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFunctions: [], + testSuites: [], + rootTestFolders: [], + testFolders: [] + }; + argsService + .setup((a) => a.filterArguments(typeMoq.It.isValue(args), typeMoq.It.isValue(TestFilter.discovery))) + .returns(() => []) + .verifiable(typeMoq.Times.once()); + runner + .setup((r) => r.run(typeMoq.It.isValue(NOSETEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('--collect-only'); + expect(opts.args).to.include('-vvv'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser + .setup((t) => t.parse(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.never()); + + const options = typeMoq.Mock.ofType<TestDiscoveryOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + token + .setup((t) => t.isCancellationRequested) + .returns(() => true) + .verifiable(typeMoq.Times.once()); + + options.setup((o) => o.args).returns(() => args); + options.setup((o) => o.token).returns(() => token.object); + const promise = discoveryService.discoverTests(options.object); + + await expect(promise).to.eventually.be.rejectedWith('cancelled'); + argsService.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); +}); diff --git a/src/test/testing/nosetest/nosetest.disovery.test.ts b/src/test/testing/nosetest/nosetest.disovery.test.ts new file mode 100644 index 000000000000..b1ca58f72b78 --- /dev/null +++ b/src/test/testing/nosetest/nosetest.disovery.test.ts @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { registerForIOC } from '../../../client/pythonEnvironments/legacyIOC'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { ITestManagerFactory } from '../../../client/testing/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { MockProcessService } from '../../mocks/proc'; +import { lookForTestFile } from '../helper'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; + +const PYTHON_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles'); +const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'pythonFiles', + 'testFiles', + 'single' +); +const filesToDelete = [ + path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), + path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') +]; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests - nose - discovery with mocked process output', () => { + let ioc: UnitTestIocContainer; + const configTarget = IS_MULTI_ROOT_TEST + ? vscode.ConfigurationTarget.WorkspaceFolder + : vscode.ConfigurationTarget.Workspace; + + suiteSetup(async () => { + filesToDelete.forEach((file) => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); + await initialize(); + }); + suiteTeardown(async () => { + await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); + filesToDelete.forEach((file) => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + }); + setup(async () => { + await initializeTest(); + initializeDI(); + }); + teardown(async () => { + await ioc.dispose(); + await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + + ioc.registerMockProcessTypes(); + ioc.registerInterpreterStorageTypes(); + + ioc.serviceManager.addSingletonInstance<IInterpreterService>( + IInterpreterService, + instance(mock(InterpreterService)) + ); + + registerForIOC(ioc.serviceManager, ioc.serviceContainer); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, instance(mock(CondaService))); + } + + async function injectTestDiscoveryOutput(outputFileName: string) { + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + procService.onExecObservable((_file, args, _options, callback) => { + if (args.indexOf('--collect-only') >= 0) { + let out = fs.readFileSync(path.join(UNITTEST_TEST_FILES_PATH, outputFileName), 'utf8'); + // Value in the test files. + out = out.replace( + /\/Users\/donjayamanne\/.vscode\/extensions\/pythonVSCode\/src\/test\/pythonFiles/g, + PYTHON_FILES_PATH + ); + callback({ + out, + source: 'stdout' + }); + } + }); + } + + test('Discover Tests (single test file)', async () => { + await injectTestDiscoveryOutput('one.output'); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + lookForTestFile(tests, path.join('tests', 'test_one.py')); + }); + + test('Check that nameToRun in testSuites has class name after : (single test file)', async () => { + await injectTestDiscoveryOutput('two.output'); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + assert.equal( + tests.testSuites.every((t) => t.testSuite.name === t.testSuite.nameToRun.split(':')[1]), + true, + 'Suite name does not match class name' + ); + }); + test('Discover Tests (-m=test)', async () => { + await injectTestDiscoveryOutput('three.output'); + await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 5, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 16, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 6, 'Incorrect number of test suites'); + lookForTestFile(tests, path.join('tests', 'test_unittest_one.py')); + lookForTestFile(tests, path.join('tests', 'test_unittest_two.py')); + lookForTestFile(tests, path.join('tests', 'unittest_three_test.py')); + lookForTestFile(tests, path.join('tests', 'test4.py')); + lookForTestFile(tests, 'test_root.py'); + }); + + test('Discover Tests (-w=specific -m=tst)', async () => { + await injectTestDiscoveryOutput('four.output'); + await updateSetting('testing.nosetestArgs', ['-w', 'specific', '-m', 'tst'], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + lookForTestFile(tests, path.join('specific', 'tst_unittest_one.py')); + lookForTestFile(tests, path.join('specific', 'tst_unittest_two.py')); + }); + + test('Discover Tests (-m=test_)', async () => { + await injectTestDiscoveryOutput('five.output'); + await updateSetting('testing.nosetestArgs', ['-m', 'test_'], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + lookForTestFile(tests, 'test_root.py'); + }); +}); diff --git a/src/test/testing/nosetest/nosetest.run.test.ts b/src/test/testing/nosetest/nosetest.run.test.ts new file mode 100644 index 000000000000..e9b0c84ee6cf --- /dev/null +++ b/src/test/testing/nosetest/nosetest.run.test.ts @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { ITestManagerFactory, TestsToRun } from '../../../client/testing/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { MockProcessService } from '../../mocks/proc'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; + +const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'pythonFiles', + 'testFiles', + 'single' +); +const filesToDelete = [ + path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), + path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') +]; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests - nose - run against actual python process', () => { + let ioc: UnitTestIocContainer; + const configTarget = IS_MULTI_ROOT_TEST + ? vscode.ConfigurationTarget.WorkspaceFolder + : vscode.ConfigurationTarget.Workspace; + + suiteSetup(async () => { + filesToDelete.forEach((file) => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); + await initialize(); + }); + suiteTeardown(async () => { + await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); + filesToDelete.forEach((file) => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + }); + setup(async () => { + await initializeTest(); + initializeDI(); + }); + teardown(async () => { + await ioc.dispose(); + await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + + ioc.registerMockProcessTypes(); + ioc.registerMockInterpreterTypes(); + ioc.registerInterpreterStorageTypes(); + } + + async function injectTestDiscoveryOutput(outputFileName: string) { + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + procService.onExecObservable((_file, args, _options, callback) => { + if (args.indexOf('--collect-only') >= 0) { + callback({ + out: fs + .readFileSync(path.join(UNITTEST_TEST_FILES_PATH, outputFileName), 'utf8') + .replace( + /\/Users\/donjayamanne\/.vscode\/extensions\/pythonVSCode\/src\/test\/pythonFiles\/testFiles\/noseFiles/g, + UNITTEST_TEST_FILES_PATH + ), + source: 'stdout' + }); + } + }); + } + + async function injectTestRunOutput(outputFileName: string, failedOutput: boolean = false) { + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + procService.onExecObservable((_file, args, _options, callback) => { + if (failedOutput && args.indexOf('--failed') === -1) { + return; + } + + const index = args.findIndex((arg) => arg.startsWith('--xunit-file=')); + if (index >= 0) { + const fileName = args[index].substr('--xunit-file='.length); + const contents = fs.readFileSync(path.join(UNITTEST_TEST_FILES_PATH, outputFileName), 'utf8'); + fs.writeFileSync(fileName, contents, 'utf8'); + callback({ out: '', source: 'stdout' }); + } + }); + } + + test('Run Tests', async () => { + await injectTestDiscoveryOutput('run.one.output'); + await injectTestRunOutput('run.one.result'); + await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); + const results = await testManager.runTest(CommandSource.ui); + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 7, 'Failures'); + assert.equal(results.summary.passed, 6, 'Passed'); + assert.equal(results.summary.skipped, 2, 'skipped'); + }); + + test('Run Failed Tests', async () => { + await injectTestDiscoveryOutput('run.two.output'); + await injectTestRunOutput('run.two.result'); + await injectTestRunOutput('run.two.again.result', true); + await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); + let results = await testManager.runTest(CommandSource.ui); + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 7, 'Failures'); + assert.equal(results.summary.passed, 6, 'Passed'); + assert.equal(results.summary.skipped, 2, 'skipped'); + + results = await testManager.runTest(CommandSource.ui, undefined, true); + assert.equal(results.summary.errors, 1, 'Errors again'); + assert.equal(results.summary.failures, 7, 'Failures again'); + assert.equal(results.summary.passed, 0, 'Passed again'); + assert.equal(results.summary.skipped, 0, 'skipped again'); + }); + + test('Run Specific Test File', async () => { + await injectTestDiscoveryOutput('run.three.output'); + await injectTestRunOutput('run.three.result'); + await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testFileToRun = tests.testFiles.find((t) => t.fullPath.endsWith('test_root.py')); + assert.ok(testFileToRun, 'Test file not found'); + // tslint:disable-next-line:no-non-null-assertion + const testFile: TestsToRun = { testFile: [testFileToRun!], testFolder: [], testFunction: [], testSuite: [] }; + const results = await testManager.runTest(CommandSource.ui, testFile); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 1, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + }); + + test('Run Specific Test Suite', async () => { + await injectTestDiscoveryOutput('run.four.output'); + await injectTestRunOutput('run.four.result'); + await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testSuiteToRun = tests.testSuites.find((s) => s.xmlClassName === 'test_root.Test_Root_test1'); + assert.ok(testSuiteToRun, 'Test suite not found'); + // tslint:disable-next-line:no-non-null-assertion + const testSuite: TestsToRun = { + testFile: [], + testFolder: [], + testFunction: [], + testSuite: [testSuiteToRun!.testSuite] + }; + const results = await testManager.runTest(CommandSource.ui, testSuite); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 1, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + }); + + test('Run Specific Test Function', async () => { + await injectTestDiscoveryOutput('run.five.output'); + await injectTestRunOutput('run.five.result'); + await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testFnToRun = tests.testFunctions.find((f) => f.xmlClassName === 'test_root.Test_Root_test1'); + assert.ok(testFnToRun, 'Test function not found'); + // tslint:disable-next-line:no-non-null-assertion + const testFn: TestsToRun = { + testFile: [], + testFolder: [], + testFunction: [testFnToRun!.testFunction], + testSuite: [] + }; + const results = await testManager.runTest(CommandSource.ui, testFn); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 0, 'Passed'); + assert.equal(results.summary.skipped, 0, 'skipped'); + }); +}); diff --git a/src/test/testing/nosetest/nosetest.test.ts b/src/test/testing/nosetest/nosetest.test.ts new file mode 100644 index 000000000000..305ee65d75ab --- /dev/null +++ b/src/test/testing/nosetest/nosetest.test.ts @@ -0,0 +1,79 @@ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { ITestManagerFactory } from '../../../client/testing/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { lookForTestFile } from '../helper'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; + +const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'pythonFiles', + 'testFiles', + 'single' +); +const filesToDelete = [ + path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), + path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') +]; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests - nose - discovery against actual python process', () => { + let ioc: UnitTestIocContainer; + const configTarget = IS_MULTI_ROOT_TEST + ? vscode.ConfigurationTarget.WorkspaceFolder + : vscode.ConfigurationTarget.Workspace; + + suiteSetup(async () => { + filesToDelete.forEach((file) => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); + await initialize(); + }); + suiteTeardown(async () => { + await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); + filesToDelete.forEach((file) => { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + }); + }); + setup(async () => { + await initializeTest(); + initializeDI(); + }); + teardown(async () => { + await ioc.dispose(); + await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerProcessTypes(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + ioc.registerMockInterpreterTypes(); + ioc.registerInterpreterStorageTypes(); + } + + test('Discover Tests (single test file)', async () => { + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + lookForTestFile(tests, path.join('tests', 'test_one.py')); + }); +}); diff --git a/src/test/testing/pytest/pytest.argsService.unit.test.ts b/src/test/testing/pytest/pytest.argsService.unit.test.ts new file mode 100644 index 000000000000..86d6a0878758 --- /dev/null +++ b/src/test/testing/pytest/pytest.argsService.unit.test.ts @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import * as typeMoq from 'typemoq'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ArgumentsHelper } from '../../../client/testing/common/argumentsHelper'; +import { ArgumentsService as PyTestArgumentsService } from '../../../client/testing/pytest/services/argsService'; +import { IArgumentsHelper } from '../../../client/testing/types'; + +suite('ArgsService: pytest', () => { + let argumentsService: PyTestArgumentsService; + + suiteSetup(() => { + const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + + const argsHelper = new ArgumentsHelper(); + + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IArgumentsHelper), typeMoq.It.isAny())) + .returns(() => argsHelper); + + argumentsService = new PyTestArgumentsService(serviceContainer.object); + }); + + test('Test getting the test folder in pytest', () => { + const dir = path.join('a', 'b', 'c'); + const args = ['--one', '--rootdir', dir]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in pytest (with folder before the arguments)', () => { + const dir = path.join('a', 'b', 'c'); + const args = [dir, '--one', '--rootdir']; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in pytest (with multiple dirs)', () => { + const dir = path.join('a', 'b', 'c'); + const dir2 = path.join('a', 'b', '2'); + const args = ['anzy', '--one', '--rootdir', dir, '--rootdir', dir2]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(2); + expect(testDirs[0]).to.equal(dir); + expect(testDirs[1]).to.equal(dir2); + }); + test('Test getting the test folder in pytest (with multiple dirs in the middle)', () => { + const dir = path.join('a', 'b', 'c'); + const dir2 = path.join('a', 'b', '2'); + const args = ['anzy', '--one', '--rootdir', dir, '--rootdir', dir2, '-xyz']; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(2); + expect(testDirs[0]).to.equal(dir); + expect(testDirs[1]).to.equal(dir2); + }); + test('Test getting the test folder in pytest (with single positional dir)', () => { + const dir = path.join('a', 'b', 'c'); + const args = ['--one', dir]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in pytest (with multiple positional dirs)', () => { + const dir = path.join('a', 'b', 'c'); + const dir2 = path.join('a', 'b', '2'); + const args = ['--one', dir, dir2]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(2); + expect(testDirs[0]).to.equal(dir); + expect(testDirs[1]).to.equal(dir2); + }); + test('Test getting the test folder in pytest (with multiple dirs excluding python files)', () => { + const dir = path.join('a', 'b', 'c'); + const dir2 = path.join('a', 'b', '2'); + const args = ['anzy', '--one', dir, dir2, path.join(dir, 'one.py')]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(3); + expect(testDirs[0]).to.equal('anzy'); + expect(testDirs[1]).to.equal(dir); + expect(testDirs[2]).to.equal(dir2); + }); + test('Test getting the list of known options for pytest', () => { + const knownOptions = argumentsService.getKnownOptions(); + expect(knownOptions.withArgs.length).to.not.equal(0); + expect(knownOptions.withoutArgs.length).to.not.equal(0); + }); + test('Test calling ArgumentsService.getOptionValue with the option followed by the value', () => { + const knownOptionsWithValues = argumentsService.getKnownOptions().withArgs; + knownOptionsWithValues.forEach((option) => { + const args = ['--foo', '--bar', 'arg1', option, 'value1']; + expect(argumentsService.getOptionValue(args, option)).to.deep.equal('value1'); + }); + }); + test('Test calling ArgumentsService.getOptionValue with the inline option syntax', () => { + const knownOptionsWithValues = argumentsService.getKnownOptions().withArgs; + knownOptionsWithValues.forEach((option) => { + const args = ['--foo', '--bar', 'arg1', `${option}=value1`]; + expect(argumentsService.getOptionValue(args, option)).to.deep.equal('value1'); + }); + }); +}); diff --git a/src/test/testing/pytest/pytest.discovery.test.ts b/src/test/testing/pytest/pytest.discovery.test.ts new file mode 100644 index 000000000000..b74fcb787484 --- /dev/null +++ b/src/test/testing/pytest/pytest.discovery.test.ts @@ -0,0 +1,1016 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { createPythonEnv } from '../../../client/common/process/pythonEnvironment'; +import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; +import { createPythonProcessService } from '../../../client/common/process/pythonProcess'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IBufferDecoder, + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonExecutionService +} from '../../../client/common/process/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { WindowsStoreInterpreter } from '../../../client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter'; +import { registerForIOC } from '../../../client/pythonEnvironments/legacyIOC'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { ITestManagerFactory } from '../../../client/testing/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../../initialize'; +import { MockProcessService } from '../../mocks/proc'; +import { UnitTestIocContainer } from '../serviceRegistry'; + +const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'pythonFiles', + 'testFiles', + 'single' +); +const UNITTEST_TEST_FILES_PATH_WITH_CONFIGS = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'pythonFiles', + 'testFiles', + 'unittestsWithConfigs' +); +const unitTestTestFilesCwdPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src'); + +/* +These test results are from `/src/test/pythonFiles/testFiles/...` directories. +Run the command `python <ExtensionDir>/pythonFiles/testing_tools/run_adapter.py discover pytest -- -s --cache-clear` to get the JSON output. +*/ + +// tslint:disable:max-func-body-length +suite('Unit Tests - pytest - discovery with mocked process output', () => { + let ioc: UnitTestIocContainer; + const configTarget = IS_MULTI_ROOT_TEST + ? vscode.ConfigurationTarget.WorkspaceFolder + : vscode.ConfigurationTarget.Workspace; + @injectable() + class ExecutionFactory extends PythonExecutionFactory { + constructor( + @inject(IServiceContainer) private readonly _serviceContainer: IServiceContainer, + @inject(IEnvironmentActivationService) activationHelper: IEnvironmentActivationService, + @inject(IProcessServiceFactory) processServiceFactory: IProcessServiceFactory, + @inject(IConfigurationService) private readonly _configService: IConfigurationService, + @inject(ICondaService) condaService: ICondaService, + @inject(WindowsStoreInterpreter) windowsStoreInterpreter: WindowsStoreInterpreter, + @inject(IBufferDecoder) decoder: IBufferDecoder, + @inject(IPlatformService) platformService: IPlatformService + ) { + super( + _serviceContainer, + activationHelper, + processServiceFactory, + _configService, + condaService, + decoder, + windowsStoreInterpreter, + platformService + ); + } + public async createActivatedEnvironment( + options: ExecutionFactoryCreateWithEnvironmentOptions + ): Promise<IPythonExecutionService> { + const pythonPath = options.interpreter + ? options.interpreter.path + : this._configService.getSettings(options.resource).pythonPath; + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + const fileSystem = this._serviceContainer.get<IFileSystem>(IFileSystem); + const env = createPythonEnv(pythonPath, procService, fileSystem); + const procs = createPythonProcessService(procService, env); + return { + getInterpreterInformation: () => env.getInterpreterInformation(), + getExecutablePath: () => env.getExecutablePath(), + isModuleInstalled: (m) => env.isModuleInstalled(m), + getExecutionInfo: (a) => env.getExecutionInfo(a), + execObservable: (a, o) => procs.execObservable(a, o), + execModuleObservable: (m, a, o) => procs.execModuleObservable(m, a, o), + exec: (a, o) => procs.exec(a, o), + execModule: (m, a, o) => procs.execModule(m, a, o) + }; + } + } + suiteSetup(async () => { + await initialize(); + await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); + }); + setup(async () => { + await initializeTest(); + initializeDI(); + }); + teardown(async () => { + await ioc.dispose(); + await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + + // Mocks. + ioc.registerMockProcessTypes(); + ioc.registerInterpreterStorageTypes(); + ioc.serviceManager.addSingletonInstance<IInterpreterService>( + IInterpreterService, + instance(mock(InterpreterService)) + ); + ioc.serviceManager.rebind<IPythonExecutionFactory>(IPythonExecutionFactory, ExecutionFactory); + registerForIOC(ioc.serviceManager, ioc.serviceContainer); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, instance(mock(CondaService))); + } + + async function injectTestDiscoveryOutput(output: string) { + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + procService.onExec((_file, args, _options, callback) => { + if (args.indexOf('discover') >= 0 && args.indexOf('pytest') >= 0) { + callback({ + stdout: output + }); + } + }); + } + + test('Discover Tests (single test file)', async () => { + await injectTestDiscoveryOutput( + JSON.stringify([ + { + rootid: '.', + root: + '/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single', + parents: [ + { + id: './test_root.py', + kind: 'file', + name: 'test_root.py', + relpath: './test_root.py', + parentid: '.' + }, + { + id: './test_root.py::Test_Root_test1', + kind: 'suite', + name: 'Test_Root_test1', + parentid: './test_root.py' + }, + { + id: './tests', + kind: 'folder', + name: 'tests', + relpath: './tests', + parentid: '.' + }, + { + id: './tests/test_one.py', + kind: 'file', + name: 'test_one.py', + relpath: './tests/test_one.py', + parentid: './tests' + }, + { + id: './tests/test_one.py::Test_test1', + kind: 'suite', + name: 'Test_test1', + parentid: './tests/test_one.py' + } + ], + tests: [ + { + id: './test_root.py::Test_Root_test1::test_Root_A', + name: 'test_Root_A', + source: './test_root.py:6', + markers: [], + parentid: './test_root.py::Test_Root_test1' + }, + { + id: './test_root.py::Test_Root_test1::test_Root_B', + name: 'test_Root_B', + source: './test_root.py:9', + markers: [], + parentid: './test_root.py::Test_Root_test1' + }, + { + id: './test_root.py::Test_Root_test1::test_Root_c', + name: 'test_Root_c', + source: './test_root.py:12', + markers: [], + parentid: './test_root.py::Test_Root_test1' + }, + { + id: './tests/test_one.py::Test_test1::test_A', + name: 'test_A', + source: 'tests/test_one.py:6', + markers: [], + parentid: './tests/test_one.py::Test_test1' + }, + { + id: './tests/test_one.py::Test_test1::test_B', + name: 'test_B', + source: 'tests/test_one.py:9', + markers: [], + parentid: './tests/test_one.py::Test_test1' + }, + { + id: './tests/test_one.py::Test_test1::test_c', + name: 'test_c', + source: 'tests/test_one.py:12', + markers: [], + parentid: './tests/test_one.py::Test_test1' + } + ] + } + ]) + ); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const diagnosticCollectionUris: vscode.Uri[] = []; + testManager.diagnosticCollection.forEach((uri) => { + diagnosticCollectionUris.push(uri); + }); + assert.equal(diagnosticCollectionUris.length, 0, 'Should not have diagnostics yet'); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_one.py'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_root.py'), + true, + 'Test File not found' + ); + }); + + test('Discover Tests (pattern = test_)', async () => { + await injectTestDiscoveryOutput( + JSON.stringify([ + { + rootid: '.', + root: + '/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard', + parents: [ + { + id: './test_root.py', + relpath: './test_root.py', + kind: 'file', + name: 'test_root.py', + parentid: '.' + }, + { + id: './test_root.py::Test_Root_test1', + kind: 'suite', + name: 'Test_Root_test1', + parentid: './test_root.py' + }, + { + id: './tests', + relpath: './tests', + kind: 'folder', + name: 'tests', + parentid: '.' + }, + { + id: './tests/test_another_pytest.py', + relpath: './tests/test_another_pytest.py', + kind: 'file', + name: 'test_another_pytest.py', + parentid: './tests' + }, + { + id: './tests/test_another_pytest.py::test_parametrized_username', + kind: 'function', + name: 'test_parametrized_username', + parentid: './tests/test_another_pytest.py' + }, + { + id: './tests/test_foreign_nested_tests.py', + relpath: './tests/test_foreign_nested_tests.py', + kind: 'file', + name: 'test_foreign_nested_tests.py', + parentid: './tests' + }, + { + id: './tests/test_foreign_nested_tests.py::TestNestedForeignTests', + kind: 'suite', + name: 'TestNestedForeignTests', + parentid: './tests/test_foreign_nested_tests.py' + }, + { + id: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere', + kind: 'suite', + name: 'TestInheritingHere', + parentid: './tests/test_foreign_nested_tests.py::TestNestedForeignTests' + }, + { + id: + './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests', + kind: 'suite', + name: 'TestExtraNestedForeignTests', + parentid: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere' + }, + { + id: './tests/test_pytest.py', + relpath: './tests/test_pytest.py', + kind: 'file', + name: 'test_pytest.py', + parentid: './tests' + }, + { + id: './tests/test_pytest.py::Test_CheckMyApp', + kind: 'suite', + name: 'Test_CheckMyApp', + parentid: './tests/test_pytest.py' + }, + { + id: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA', + kind: 'suite', + name: 'Test_NestedClassA', + parentid: './tests/test_pytest.py::Test_CheckMyApp' + }, + { + id: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A', + kind: 'suite', + name: 'Test_nested_classB_Of_A', + parentid: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' + }, + { + id: './tests/test_pytest.py::test_parametrized_username', + kind: 'function', + name: 'test_parametrized_username', + parentid: './tests/test_pytest.py' + }, + { + id: './tests/test_unittest_one.py', + relpath: './tests/test_unittest_one.py', + kind: 'file', + name: 'test_unittest_one.py', + parentid: './tests' + }, + { + id: './tests/test_unittest_one.py::Test_test1', + kind: 'suite', + name: 'Test_test1', + parentid: './tests/test_unittest_one.py' + }, + { + id: './tests/test_unittest_two.py', + relpath: './tests/test_unittest_two.py', + kind: 'file', + name: 'test_unittest_two.py', + parentid: './tests' + }, + { + id: './tests/test_unittest_two.py::Test_test2', + kind: 'suite', + name: 'Test_test2', + parentid: './tests/test_unittest_two.py' + }, + { + id: './tests/test_unittest_two.py::Test_test2a', + kind: 'suite', + name: 'Test_test2a', + parentid: './tests/test_unittest_two.py' + }, + { + id: './tests/unittest_three_test.py', + relpath: './tests/unittest_three_test.py', + kind: 'file', + name: 'unittest_three_test.py', + parentid: './tests' + }, + { + id: './tests/unittest_three_test.py::Test_test3', + kind: 'suite', + name: 'Test_test3', + parentid: './tests/unittest_three_test.py' + } + ], + tests: [ + { + id: './test_root.py::Test_Root_test1::test_Root_A', + name: 'test_Root_A', + source: './test_root.py:6', + markers: [], + parentid: './test_root.py::Test_Root_test1' + }, + { + id: './test_root.py::Test_Root_test1::test_Root_B', + name: 'test_Root_B', + source: './test_root.py:9', + markers: [], + parentid: './test_root.py::Test_Root_test1' + }, + { + id: './test_root.py::Test_Root_test1::test_Root_c', + name: 'test_Root_c', + source: './test_root.py:12', + markers: [], + parentid: './test_root.py::Test_Root_test1' + }, + { + id: './tests/test_another_pytest.py::test_username', + name: 'test_username', + source: 'tests/test_another_pytest.py:12', + markers: [], + parentid: './tests/test_another_pytest.py' + }, + { + id: './tests/test_another_pytest.py::test_parametrized_username[one]', + name: 'test_parametrized_username[one]', + source: 'tests/test_another_pytest.py:15', + markers: [], + parentid: './tests/test_another_pytest.py::test_parametrized_username' + }, + { + id: './tests/test_another_pytest.py::test_parametrized_username[two]', + name: 'test_parametrized_username[two]', + source: 'tests/test_another_pytest.py:15', + markers: [], + parentid: './tests/test_another_pytest.py::test_parametrized_username' + }, + { + id: './tests/test_another_pytest.py::test_parametrized_username[three]', + name: 'test_parametrized_username[three]', + source: 'tests/test_another_pytest.py:15', + markers: [], + parentid: './tests/test_another_pytest.py::test_parametrized_username' + }, + { + id: + './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign', + name: 'test_super_deep_foreign', + source: 'tests/external.py:2', + markers: [], + parentid: + './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests' + }, + { + id: + './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test', + name: 'test_foreign_test', + source: 'tests/external.py:4', + markers: [], + parentid: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere' + }, + { + id: + './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal', + name: 'test_nested_normal', + source: 'tests/test_foreign_nested_tests.py:5', + markers: [], + parentid: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere' + }, + { + id: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal', + name: 'test_normal', + source: 'tests/test_foreign_nested_tests.py:7', + markers: [], + parentid: './tests/test_foreign_nested_tests.py::TestNestedForeignTests' + }, + { + id: './tests/test_pytest.py::Test_CheckMyApp::test_simple_check', + name: 'test_simple_check', + source: 'tests/test_pytest.py:6', + markers: [], + parentid: './tests/test_pytest.py::Test_CheckMyApp' + }, + { + id: './tests/test_pytest.py::Test_CheckMyApp::test_complex_check', + name: 'test_complex_check', + source: 'tests/test_pytest.py:9', + markers: [], + parentid: './tests/test_pytest.py::Test_CheckMyApp' + }, + { + id: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB', + name: 'test_nested_class_methodB', + source: 'tests/test_pytest.py:13', + markers: [], + parentid: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' + }, + { + id: + './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d', + name: 'test_d', + source: 'tests/test_pytest.py:16', + markers: [], + parentid: + './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A' + }, + { + id: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC', + name: 'test_nested_class_methodC', + source: 'tests/test_pytest.py:18', + markers: [], + parentid: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' + }, + { + id: './tests/test_pytest.py::Test_CheckMyApp::test_simple_check2', + name: 'test_simple_check2', + source: 'tests/test_pytest.py:21', + markers: [], + parentid: './tests/test_pytest.py::Test_CheckMyApp' + }, + { + id: './tests/test_pytest.py::Test_CheckMyApp::test_complex_check2', + name: 'test_complex_check2', + source: 'tests/test_pytest.py:23', + markers: [], + parentid: './tests/test_pytest.py::Test_CheckMyApp' + }, + { + id: './tests/test_pytest.py::test_username', + name: 'test_username', + source: 'tests/test_pytest.py:35', + markers: [], + parentid: './tests/test_pytest.py' + }, + { + id: './tests/test_pytest.py::test_parametrized_username[one]', + name: 'test_parametrized_username[one]', + source: 'tests/test_pytest.py:38', + markers: [], + parentid: './tests/test_pytest.py::test_parametrized_username' + }, + { + id: './tests/test_pytest.py::test_parametrized_username[two]', + name: 'test_parametrized_username[two]', + source: 'tests/test_pytest.py:38', + markers: [], + parentid: './tests/test_pytest.py::test_parametrized_username' + }, + { + id: './tests/test_pytest.py::test_parametrized_username[three]', + name: 'test_parametrized_username[three]', + source: 'tests/test_pytest.py:38', + markers: [], + parentid: './tests/test_pytest.py::test_parametrized_username' + }, + { + id: './tests/test_unittest_one.py::Test_test1::test_A', + name: 'test_A', + source: 'tests/test_unittest_one.py:6', + markers: [], + parentid: './tests/test_unittest_one.py::Test_test1' + }, + { + id: './tests/test_unittest_one.py::Test_test1::test_B', + name: 'test_B', + source: 'tests/test_unittest_one.py:9', + markers: [], + parentid: './tests/test_unittest_one.py::Test_test1' + }, + { + id: './tests/test_unittest_one.py::Test_test1::test_c', + name: 'test_c', + source: 'tests/test_unittest_one.py:12', + markers: [], + parentid: './tests/test_unittest_one.py::Test_test1' + }, + { + id: './tests/test_unittest_two.py::Test_test2::test_A2', + name: 'test_A2', + source: 'tests/test_unittest_two.py:3', + markers: [], + parentid: './tests/test_unittest_two.py::Test_test2' + }, + { + id: './tests/test_unittest_two.py::Test_test2::test_B2', + name: 'test_B2', + source: 'tests/test_unittest_two.py:6', + markers: [], + parentid: './tests/test_unittest_two.py::Test_test2' + }, + { + id: './tests/test_unittest_two.py::Test_test2::test_C2', + name: 'test_C2', + source: 'tests/test_unittest_two.py:9', + markers: [], + parentid: './tests/test_unittest_two.py::Test_test2' + }, + { + id: './tests/test_unittest_two.py::Test_test2::test_D2', + name: 'test_D2', + source: 'tests/test_unittest_two.py:12', + markers: [], + parentid: './tests/test_unittest_two.py::Test_test2' + }, + { + id: './tests/test_unittest_two.py::Test_test2a::test_222A2', + name: 'test_222A2', + source: 'tests/test_unittest_two.py:17', + markers: [], + parentid: './tests/test_unittest_two.py::Test_test2a' + }, + { + id: './tests/test_unittest_two.py::Test_test2a::test_222B2', + name: 'test_222B2', + source: 'tests/test_unittest_two.py:20', + markers: [], + parentid: './tests/test_unittest_two.py::Test_test2a' + }, + { + id: './tests/unittest_three_test.py::Test_test3::test_A', + name: 'test_A', + source: 'tests/unittest_three_test.py:4', + markers: [], + parentid: './tests/unittest_three_test.py::Test_test3' + }, + { + id: './tests/unittest_three_test.py::Test_test3::test_B', + name: 'test_B', + source: 'tests/unittest_three_test.py:7', + markers: [], + parentid: './tests/unittest_three_test.py::Test_test3' + } + ] + } + ]) + ); + await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const diagnosticCollectionUris: vscode.Uri[] = []; + testManager.diagnosticCollection.forEach((uri) => { + diagnosticCollectionUris.push(uri); + }); + assert.equal(diagnosticCollectionUris.length, 0, 'Should not have diagnostics yet'); + assert.equal(tests.testFiles.length, 7, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 33, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 11, 'Incorrect number of test suites'); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_foreign_nested_tests.py'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_unittest_one.py'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_unittest_two.py'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'unittest_three_test.py'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_pytest.py'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_another_pytest.py'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_root.py'), + true, + 'Test File not found' + ); + }); + + test('Discover Tests (pattern = _test)', async () => { + await injectTestDiscoveryOutput( + JSON.stringify([ + { + rootid: '.', + root: + '/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard', + parents: [ + { + id: './tests', + kind: 'folder', + name: 'tests', + relpath: './tests', + parentid: '.' + }, + { + id: './tests/unittest_three_test.py', + kind: 'file', + name: 'unittest_three_test.py', + relpath: './tests/unittest_three_test.py', + parentid: './tests' + }, + { + id: './tests/unittest_three_test.py::Test_test3', + kind: 'suite', + name: 'Test_test3', + parentid: './tests/unittest_three_test.py' + } + ], + tests: [ + { + id: './tests/unittest_three_test.py::Test_test3::test_A', + name: 'test_A', + source: 'tests/unittest_three_test.py:4', + markers: [], + parentid: './tests/unittest_three_test.py::Test_test3' + }, + { + id: './tests/unittest_three_test.py::Test_test3::test_B', + name: 'test_B', + source: 'tests/unittest_three_test.py:7', + markers: [], + parentid: './tests/unittest_three_test.py::Test_test3' + } + ] + } + ]) + ); + await updateSetting('testing.pytestArgs', ['-k=_test.py'], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const diagnosticCollectionUris: vscode.Uri[] = []; + testManager.diagnosticCollection.forEach((uri) => { + diagnosticCollectionUris.push(uri); + }); + assert.equal(diagnosticCollectionUris.length, 0, 'Should not have diagnostics yet'); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + assert.equal( + tests.testFiles.some((t) => t.name === 'unittest_three_test.py'), + true, + 'Test File not found' + ); + }); + + test('Discover Tests (with config)', async () => { + await injectTestDiscoveryOutput( + JSON.stringify([ + { + rootid: '.', + root: + '/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/unittestsWithConfigs', + parents: [ + { + id: './other', + relpath: './other', + kind: 'folder', + name: 'other', + parentid: '.' + }, + { + id: './other/test_pytest.py', + relpath: './other/test_pytest.py', + kind: 'file', + name: 'test_pytest.py', + parentid: './other' + }, + { + id: './other/test_pytest.py::Test_CheckMyApp', + kind: 'suite', + name: 'Test_CheckMyApp', + parentid: './other/test_pytest.py' + }, + { + id: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA', + kind: 'suite', + name: 'Test_NestedClassA', + parentid: './other/test_pytest.py::Test_CheckMyApp' + }, + { + id: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A', + kind: 'suite', + name: 'Test_nested_classB_Of_A', + parentid: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' + }, + { + id: './other/test_pytest.py::test_parametrized_username', + kind: 'function', + name: 'test_parametrized_username', + parentid: './other/test_pytest.py' + }, + { + id: './other/test_unittest_one.py', + relpath: './other/test_unittest_one.py', + kind: 'file', + name: 'test_unittest_one.py', + parentid: './other' + }, + { + id: './other/test_unittest_one.py::Test_test1', + kind: 'suite', + name: 'Test_test1', + parentid: './other/test_unittest_one.py' + } + ], + tests: [ + { + id: './other/test_pytest.py::Test_CheckMyApp::test_simple_check', + name: 'test_simple_check', + source: 'other/test_pytest.py:6', + markers: [], + parentid: './other/test_pytest.py::Test_CheckMyApp' + }, + { + id: './other/test_pytest.py::Test_CheckMyApp::test_complex_check', + name: 'test_complex_check', + source: 'other/test_pytest.py:9', + markers: [], + parentid: './other/test_pytest.py::Test_CheckMyApp' + }, + { + id: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB', + name: 'test_nested_class_methodB', + source: 'other/test_pytest.py:13', + markers: [], + parentid: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' + }, + { + id: + './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d', + name: 'test_d', + source: 'other/test_pytest.py:16', + markers: [], + parentid: + './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A' + }, + { + id: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC', + name: 'test_nested_class_methodC', + source: 'other/test_pytest.py:18', + markers: [], + parentid: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' + }, + { + id: './other/test_pytest.py::Test_CheckMyApp::test_simple_check2', + name: 'test_simple_check2', + source: 'other/test_pytest.py:21', + markers: [], + parentid: './other/test_pytest.py::Test_CheckMyApp' + }, + { + id: './other/test_pytest.py::Test_CheckMyApp::test_complex_check2', + name: 'test_complex_check2', + source: 'other/test_pytest.py:23', + markers: [], + parentid: './other/test_pytest.py::Test_CheckMyApp' + }, + { + id: './other/test_pytest.py::test_username', + name: 'test_username', + source: 'other/test_pytest.py:35', + markers: [], + parentid: './other/test_pytest.py' + }, + { + id: './other/test_pytest.py::test_parametrized_username[one]', + name: 'test_parametrized_username[one]', + source: 'other/test_pytest.py:38', + markers: [], + parentid: './other/test_pytest.py::test_parametrized_username' + }, + { + id: './other/test_pytest.py::test_parametrized_username[two]', + name: 'test_parametrized_username[two]', + source: 'other/test_pytest.py:38', + markers: [], + parentid: './other/test_pytest.py::test_parametrized_username' + }, + { + id: './other/test_pytest.py::test_parametrized_username[three]', + name: 'test_parametrized_username[three]', + source: 'other/test_pytest.py:38', + markers: [], + parentid: './other/test_pytest.py::test_parametrized_username' + }, + { + id: './other/test_unittest_one.py::Test_test1::test_A', + name: 'test_A', + source: 'other/test_unittest_one.py:6', + markers: [], + parentid: './other/test_unittest_one.py::Test_test1' + }, + { + id: './other/test_unittest_one.py::Test_test1::test_B', + name: 'test_B', + source: 'other/test_unittest_one.py:9', + markers: [], + parentid: './other/test_unittest_one.py::Test_test1' + }, + { + id: './other/test_unittest_one.py::Test_test1::test_c', + name: 'test_c', + source: 'other/test_unittest_one.py:12', + markers: [], + parentid: './other/test_unittest_one.py::Test_test1' + } + ] + } + ]) + ); + await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH_WITH_CONFIGS); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const diagnosticCollectionUris: vscode.Uri[] = []; + testManager.diagnosticCollection.forEach((uri) => { + diagnosticCollectionUris.push(uri); + }); + assert.equal(diagnosticCollectionUris.length, 0, 'Should not have diagnostics yet'); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 14, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 4, 'Incorrect number of test suites'); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_unittest_one.py'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_pytest.py'), + true, + 'Test File not found' + ); + }); + + test('Setting cwd should return tests', async () => { + await injectTestDiscoveryOutput( + JSON.stringify([ + { + rootid: '.', + root: + '/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/cwd/src', + parents: [ + { + id: './tests', + kind: 'folder', + name: 'tests', + relpath: './tests', + parentid: '.' + }, + { + id: './tests/test_cwd.py', + kind: 'file', + name: 'test_cwd.py', + relpath: './tests/test_cwd.py', + parentid: './tests' + }, + { + id: './tests/test_cwd.py::Test_Current_Working_Directory', + kind: 'suite', + name: 'Test_Current_Working_Directory', + parentid: './tests/test_cwd.py' + } + ], + tests: [ + { + id: './tests/test_cwd.py::Test_Current_Working_Directory::test_cwd', + name: 'test_cwd', + source: 'tests/test_cwd.py:6', + markers: [], + parentid: './tests/test_cwd.py::Test_Current_Working_Directory' + } + ] + } + ]) + ); + await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('pytest', rootWorkspaceUri!, unitTestTestFilesCwdPath); + + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const diagnosticCollectionUris: vscode.Uri[] = []; + testManager.diagnosticCollection.forEach((uri) => { + diagnosticCollectionUris.push(uri); + }); + assert.equal(diagnosticCollectionUris.length, 0, 'Should not have diagnostics yet'); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFolders.length, 2, 'Incorrect number of test folders'); + assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + }); +}); diff --git a/src/test/testing/pytest/pytest.run.test.ts b/src/test/testing/pytest/pytest.run.test.ts new file mode 100644 index 000000000000..f872b1fd880c --- /dev/null +++ b/src/test/testing/pytest/pytest.run.test.ts @@ -0,0 +1,646 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as fs from 'fs'; +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { createPythonEnv } from '../../../client/common/process/pythonEnvironment'; +import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; +import { createPythonProcessService } from '../../../client/common/process/pythonProcess'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IBufferDecoder, + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonExecutionService +} from '../../../client/common/process/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { WindowsStoreInterpreter } from '../../../client/pythonEnvironments/discovery/locators/services/windowsStoreInterpreter'; +import { registerForIOC } from '../../../client/pythonEnvironments/legacyIOC'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { UnitTestDiagnosticService } from '../../../client/testing/common/services/unitTestDiagnosticService'; +import { + FlattenedTestFunction, + ITestManager, + ITestManagerFactory, + Tests, + TestStatus, + TestsToRun +} from '../../../client/testing/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { TEST_TIMEOUT } from '../../constants'; +import { MockProcessService } from '../../mocks/proc'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; +import { ITestDetails, ITestScenarioDetails, testScenarios } from './pytest_run_tests_data'; + +// tslint:disable:max-func-body-length + +const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); +const PYTEST_RESULTS_PATH = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'pythonFiles', + 'testFiles', + 'pytestFiles', + 'results' +); + +interface IResultsSummaryCount { + passes: number; + skips: number; + failures: number; + errors: number; +} + +/** + * Establishing what tests should be run (so that they can be passed to the test manager) can be + * dependant on the test discovery process having occurred. If the scenario has any properties that + * indicate its testsToRun property needs to be generated, then this process is done by using + * properties of the scenario to determine which test folders/files/suites/functions should be + * used from the tests object created by the test discovery process. + * + * @param scenario The testing scenario to emulate. + * @param tests The tests that were discovered. + */ +async function getScenarioTestsToRun(scenario: ITestScenarioDetails, tests: Tests): Promise<TestsToRun> { + const generateTestsToRun = scenario.testSuiteIndex || scenario.testFunctionIndex; + if (scenario.testsToRun === undefined && generateTestsToRun) { + scenario.testsToRun = { + testFolder: [], + testFile: [], + testSuite: [], + testFunction: [] + }; + if (scenario.testSuiteIndex) { + scenario.testsToRun.testSuite!.push(tests.testSuites[scenario.testSuiteIndex].testSuite); + } + if (scenario.testFunctionIndex) { + scenario.testsToRun.testFunction!.push(tests.testSuites[scenario.testFunctionIndex].testSuite); + } + } + return scenario.testsToRun!; +} + +/** + * Run the tests and return the results. + * + * In the case of a failed test run, some test details can be marked through the passOnFailedRun property to pass on a + * failed run. This is meant to simulate a test or the thing it's meant to test being fixed. + * + * @param testManager The test manager used to run the tests. + * @param testsToRun The tests that the test manager should run. + * @param failedRun Whether or not the current test run is for failed tests from a previous run. + */ +async function getResultsFromTestManagerRunTest( + testManager: ITestManager, + testsToRun: TestsToRun, + failedRun: boolean = false +): Promise<Tests> { + if (failedRun) { + return testManager.runTest(CommandSource.ui, undefined, true); + } else { + return testManager.runTest(CommandSource.ui, testsToRun); + } +} + +/** + * Get the number of passes/skips/failures/errors for a test run based on the test details for a scenario. + * + * In the case of a failed test run, some test details can be marked through the passOnFailedRun property to pass on a + * failed run. This is meant to simulate a test or the thing it's meant to test being fixed. + * + * @param testDetails All the test details for a scenario. + * @param failedRun Whether or not the current test run is for failed tests from a previous run. + */ +function getExpectedSummaryCount(testDetails: ITestDetails[], failedRun: boolean): IResultsSummaryCount { + const summaryCount: IResultsSummaryCount = { + passes: 0, + skips: 0, + failures: 0, + errors: 0 + }; + testDetails.forEach((td) => { + let tStatus = td.status; + if (failedRun && td.passOnFailedRun) { + tStatus = TestStatus.Pass; + } + switch (tStatus) { + case TestStatus.Pass: { + summaryCount.passes += 1; + break; + } + case TestStatus.Skipped: { + summaryCount.skips += 1; + break; + } + case TestStatus.Fail: { + summaryCount.failures += 1; + break; + } + case TestStatus.Error: { + summaryCount.errors += 1; + break; + } + default: { + throw Error('Unsupported TestStatus'); + } + } + }); + return summaryCount; +} + +/** + * Get all the test details associated with a file. + * + * @param testDetails All the test details for a scenario. + * @param fileName The name of the file to find test details for. + */ +function getRelevantTestDetailsForFile(testDetails: ITestDetails[], fileName: string): ITestDetails[] { + return testDetails.filter((td) => { + return td.fileName === fileName; + }); +} + +/** + * Every failed/skipped test in a file should should have an associated Diagnostic for it. This calculates and returns the + * expected number of Diagnostics based on the expected test details for that file. In the event of a normal test run, + * skipped tests will be included in the results, and thus will be included in the testDetails argument. But if it's a + * failed test run, skipped tests will not be attempted again, so they will not be included in the testDetails argument. + * + * In the case of a failed test run, some test details can be marked through the passOnFailedRun property to pass on a + * failed run. This is meant to simulate a test or the thing it's meant to test being fixed. + * + * @param testDetails All the test details for a file for the tests that were run. + * @param skippedTestDetails All the test details for skipped tests for a file. + * @param failedRun Whether or not the current test run is for failed tests from a previous run. + */ +function getIssueCountFromRelevantTestDetails( + testDetails: ITestDetails[], + skippedTestDetails: ITestDetails[], + failedRun: boolean = false +): number { + const relevantIssueDetails = testDetails.filter((td) => { + return td.status !== TestStatus.Pass && !(failedRun && td.passOnFailedRun); + }); + // If it's a failed run, the skipped tests won't be included in testDetails, but should still be included as they still aren't passing. + return relevantIssueDetails.length + (failedRun ? skippedTestDetails.length : 0); +} + +/** + * Get the Diagnostic associated with the FlattenedTestFunction. + * + * @param diagnostics The array of Diagnostics for a file. + * @param testFunc The FlattenedTestFunction to find the Diagnostic for. + */ +function getDiagnosticForTestFunc( + diagnostics: readonly vscode.Diagnostic[], + testFunc: FlattenedTestFunction +): vscode.Diagnostic { + return diagnostics.find((diag) => { + return testFunc.testFunction.nameToRun === diag.code; + })!; +} + +/** + * Get a list of all the unique files found in a given testDetails array. + * + * @param testDetails All the test details for a scenario. + */ +function getUniqueIssueFilesFromTestDetails(testDetails: ITestDetails[]): string[] { + return testDetails.reduce<string[]>((filtered, issue) => { + if (filtered.indexOf(issue.fileName) === -1 && issue.fileName !== undefined) { + filtered.push(issue.fileName); + } + return filtered; + }, []); +} + +/** + * Of all the test details that were run for a scenario, given a file location, get all those that were skipped. + * + * @param testDetails All test details that should have been run for the scenario. + * @param fileName The location of a file that had tests run. + */ +function getRelevantSkippedIssuesFromTestDetailsForFile(testDetails: ITestDetails[], fileName: string): ITestDetails[] { + return testDetails.filter((td) => { + return td.fileName === fileName && td.status === TestStatus.Skipped; + }); +} + +/** + * Get the FlattenedTestFunction from the test results that's associated with the given testDetails object. + * + * @param ioc IOC Test Container + * @param results Results of the test run. + * @param testFileUri The Uri of the test file that was run. + * @param testDetails The details of a particular test. + */ +function getTestFuncFromResultsByTestFileAndName( + ioc: UnitTestIocContainer, + results: Tests, + testFileUri: vscode.Uri, + testDetails: ITestDetails +): FlattenedTestFunction { + const fileSystem = ioc.serviceContainer.get<IFileSystem>(IFileSystem); + return results.testFunctions.find((test) => { + return ( + fileSystem.arePathsSame(vscode.Uri.file(test.parentTestFile.fullPath).fsPath, testFileUri.fsPath) && + test.testFunction.name === testDetails.testName + ); + })!; +} + +/** + * Generate a Diagnostic object (including DiagnosticRelatedInformation) using the provided test details that reflects + * what the Diagnostic for the associated test should be in order for it to be compared to by the actual Diagnostic + * for the test. + * + * @param testDetails Test details for a specific test. + */ +async function getExpectedDiagnosticFromTestDetails(testDetails: ITestDetails): Promise<vscode.Diagnostic> { + const relatedInfo: vscode.DiagnosticRelatedInformation[] = []; + const testFilePath = path.join(UNITTEST_TEST_FILES_PATH, testDetails.fileName); + const testFileUri = vscode.Uri.file(testFilePath); + let expectedSourceTestFilePath = testFilePath; + if (testDetails.imported) { + expectedSourceTestFilePath = path.join(UNITTEST_TEST_FILES_PATH, testDetails.sourceFileName!); + } + const expectedSourceTestFileUri = vscode.Uri.file(expectedSourceTestFilePath); + const diagMsgPrefix = new UnitTestDiagnosticService().getMessagePrefix(testDetails.status); + const expectedDiagMsg = `${diagMsgPrefix ? `${diagMsgPrefix}: ` : ''}${testDetails.message}`; + let expectedDiagRange = testDetails.testDefRange; + let expectedSeverity = vscode.DiagnosticSeverity.Error; + if (testDetails.status === TestStatus.Skipped) { + // Stack should stop at the test definition line. + expectedSeverity = vscode.DiagnosticSeverity.Information; + } + if (testDetails.imported) { + // Stack should include the class furthest down the chain from the file that was executed. + relatedInfo.push( + new vscode.DiagnosticRelatedInformation( + new vscode.Location(testFileUri, testDetails.classDefRange!), + testDetails.simpleClassName! + ) + ); + expectedDiagRange = testDetails.classDefRange; + } + relatedInfo.push( + new vscode.DiagnosticRelatedInformation( + new vscode.Location(expectedSourceTestFileUri, testDetails.testDefRange!), + testDetails.sourceTestName + ) + ); + if (testDetails.status !== TestStatus.Skipped) { + relatedInfo.push( + new vscode.DiagnosticRelatedInformation( + new vscode.Location(expectedSourceTestFileUri, testDetails.issueRange!), + testDetails.issueLineText! + ) + ); + } else { + expectedSeverity = vscode.DiagnosticSeverity.Information; + } + + const expectedDiagnostic = new vscode.Diagnostic(expectedDiagRange!, expectedDiagMsg, expectedSeverity); + expectedDiagnostic.source = 'pytest'; + expectedDiagnostic.code = testDetails.nameToRun; + expectedDiagnostic.relatedInformation = relatedInfo; + return expectedDiagnostic; +} + +async function testResultsSummary(results: Tests, expectedSummaryCount: IResultsSummaryCount) { + const totalTests = + results.summary.passed + results.summary.skipped + results.summary.failures + results.summary.errors; + assert.notEqual(totalTests, 0); + assert.equal(results.summary.passed, expectedSummaryCount.passes, 'Passed'); + assert.equal(results.summary.skipped, expectedSummaryCount.skips, 'Skipped'); + assert.equal(results.summary.failures, expectedSummaryCount.failures, 'Failures'); + assert.equal(results.summary.errors, expectedSummaryCount.errors, 'Errors'); +} + +async function testDiagnostic(diagnostic: vscode.Diagnostic, expectedDiagnostic: vscode.Diagnostic) { + assert.equal(diagnostic.code, expectedDiagnostic.code, 'Diagnostic code'); + assert.equal(diagnostic.message, expectedDiagnostic.message, 'Diagnostic message'); + assert.equal(diagnostic.severity, expectedDiagnostic.severity, 'Diagnostic severity'); + assert.equal(diagnostic.range.start.line, expectedDiagnostic.range.start.line, 'Diagnostic range start line'); + assert.equal( + diagnostic.range.start.character, + expectedDiagnostic.range.start.character, + 'Diagnostic range start character' + ); + assert.equal(diagnostic.range.end.line, expectedDiagnostic.range.end.line, 'Diagnostic range end line'); + assert.equal( + diagnostic.range.end.character, + expectedDiagnostic.range.end.character, + 'Diagnostic range end character' + ); + assert.equal(diagnostic.source, expectedDiagnostic.source, 'Diagnostic source'); + assert.equal( + diagnostic.relatedInformation!.length, + expectedDiagnostic.relatedInformation!.length, + 'DiagnosticRelatedInformation count' + ); +} + +async function testDiagnosticRelatedInformation( + relatedInfo: vscode.DiagnosticRelatedInformation, + expectedRelatedInfo: vscode.DiagnosticRelatedInformation +) { + assert.equal(relatedInfo.message, expectedRelatedInfo.message, 'DiagnosticRelatedInfo definition'); + assert.equal( + relatedInfo.location.range.start.line, + expectedRelatedInfo.location.range.start.line, + 'DiagnosticRelatedInfo definition range start line' + ); + assert.equal( + relatedInfo.location.range.start.character, + expectedRelatedInfo.location.range.start.character, + 'DiagnosticRelatedInfo definition range start character' + ); + assert.equal( + relatedInfo.location.range.end.line, + expectedRelatedInfo.location.range.end.line, + 'DiagnosticRelatedInfo definition range end line' + ); + assert.equal( + relatedInfo.location.range.end.character, + expectedRelatedInfo.location.range.end.character, + 'DiagnosticRelatedInfo definition range end character' + ); +} + +suite('Unit Tests - pytest - run with mocked process output', () => { + let ioc: UnitTestIocContainer; + const configTarget = IS_MULTI_ROOT_TEST + ? vscode.ConfigurationTarget.WorkspaceFolder + : vscode.ConfigurationTarget.Workspace; + @injectable() + class ExecutionFactory extends PythonExecutionFactory { + constructor( + @inject(IServiceContainer) private readonly _serviceContainer: IServiceContainer, + @inject(IEnvironmentActivationService) activationHelper: IEnvironmentActivationService, + @inject(IProcessServiceFactory) processServiceFactory: IProcessServiceFactory, + @inject(IConfigurationService) private readonly _configService: IConfigurationService, + @inject(ICondaService) condaService: ICondaService, + @inject(WindowsStoreInterpreter) windowsStoreInterpreter: WindowsStoreInterpreter, + @inject(IBufferDecoder) decoder: IBufferDecoder, + @inject(IPlatformService) platformService: IPlatformService + ) { + super( + _serviceContainer, + activationHelper, + processServiceFactory, + _configService, + condaService, + decoder, + windowsStoreInterpreter, + platformService + ); + } + public async createActivatedEnvironment( + options: ExecutionFactoryCreateWithEnvironmentOptions + ): Promise<IPythonExecutionService> { + const pythonPath = options.interpreter + ? options.interpreter.path + : this._configService.getSettings(options.resource).pythonPath; + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + const fileSystem = this._serviceContainer.get<IFileSystem>(IFileSystem); + const env = createPythonEnv(pythonPath, procService, fileSystem); + const procs = createPythonProcessService(procService, env); + return { + getInterpreterInformation: () => env.getInterpreterInformation(), + getExecutablePath: () => env.getExecutablePath(), + isModuleInstalled: (m) => env.isModuleInstalled(m), + getExecutionInfo: (a) => env.getExecutionInfo(a), + execObservable: (a, o) => procs.execObservable(a, o), + execModuleObservable: (m, a, o) => procs.execModuleObservable(m, a, o), + exec: (a, o) => procs.exec(a, o), + execModule: (m, a, o) => procs.execModule(m, a, o) + }; + } + } + // tslint:disable-next-line: no-function-expression + suiteSetup(async function () { + // tslint:disable-next-line: no-invalid-this + this.timeout(TEST_TIMEOUT * 2); + // tslint:disable: no-console + console.time('Pytest before all hook'); + await initialize(); + console.timeLog('Pytest before all hook'); + await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); + console.timeEnd('Pytest before all hook'); + // tslint:enable: no-console + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + // Mocks. + ioc.registerMockProcessTypes(); + ioc.registerInterpreterStorageTypes(); + + ioc.serviceManager.addSingletonInstance<IInterpreterService>( + IInterpreterService, + instance(mock(InterpreterService)) + ); + ioc.serviceManager.rebind<IPythonExecutionFactory>(IPythonExecutionFactory, ExecutionFactory); + + registerForIOC(ioc.serviceManager, ioc.serviceContainer); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, instance(mock(CondaService))); + } + + async function injectTestDiscoveryOutput(outputFileName: string) { + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + procService.onExec((_file, args, _options, callback) => { + if (args.indexOf('discover') >= 0 && args.indexOf('pytest') >= 0) { + let stdout = fs.readFileSync(path.join(PYTEST_RESULTS_PATH, outputFileName), 'utf8'); + stdout = stdout.replace( + /\/Users\/donjayamanne\/.vscode-insiders\/extensions\/pythonVSCode\/src\/test\/pythonFiles\/testFiles/g, + path.dirname(UNITTEST_TEST_FILES_PATH) + ); + stdout = stdout.replace(/\\/g, '/'); + callback({ stdout }); + } + }); + } + async function injectTestRunOutput(outputFileName: string, failedOutput: boolean = false) { + const junitXmlArgs = '--junit-xml='; + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + procService.onExecObservable((_file, args, _options, callback) => { + if (failedOutput && args.indexOf('--last-failed') === -1) { + return; + } + const index = args.findIndex((arg) => arg.startsWith(junitXmlArgs)); + if (index >= 0) { + const fileName = args[index].substr(junitXmlArgs.length); + const contents = fs.readFileSync(path.join(PYTEST_RESULTS_PATH, outputFileName), 'utf8'); + fs.writeFileSync(fileName, contents, 'utf8'); + callback({ out: '', source: 'stdout' }); + } + }); + } + function getScenarioTestDetails(scenario: ITestScenarioDetails, failedRun: boolean): ITestDetails[] { + if (scenario.shouldRunFailed && failedRun) { + return scenario.testDetails!.filter((td) => { + return td.status === TestStatus.Fail; + })!; + } + return scenario.testDetails!; + } + testScenarios.forEach((scenario) => { + suite(scenario.scenarioName, () => { + let testDetails: ITestDetails[]; + let factory: ITestManagerFactory; + let testManager: ITestManager; + let results: Tests; + let diagnostics: readonly vscode.Diagnostic[]; + suiteSetup(async function () { + // This "before all" hook is doing way more than normal + // tslint:disable-next-line: no-invalid-this + this.timeout(TEST_TIMEOUT * 2); + await initializeTest(); + initializeDI(); + await injectTestDiscoveryOutput(scenario.discoveryOutput); + await injectTestRunOutput(scenario.runOutput); + if (scenario.shouldRunFailed === true) { + await injectTestRunOutput(scenario.failedRunOutput!, true); + } + await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + scenario.testsToRun = await getScenarioTestsToRun(scenario, tests); + }); + suiteTeardown(async () => { + await ioc.dispose(); + await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); + }); + // tslint:disable: max-func-body-length + const shouldRunProperly = (suiteName: string, failedRun = false) => { + suite(suiteName, () => { + testDetails = getScenarioTestDetails(scenario, failedRun); + const uniqueIssueFiles = getUniqueIssueFilesFromTestDetails(testDetails); + let expectedSummaryCount: IResultsSummaryCount; + suiteSetup(async () => { + testDetails = getScenarioTestDetails(scenario, failedRun); + results = await getResultsFromTestManagerRunTest(testManager, scenario.testsToRun!, failedRun); + expectedSummaryCount = getExpectedSummaryCount(testDetails, failedRun); + }); + test('Test results summary', async () => { + await testResultsSummary(results, expectedSummaryCount); + }); + uniqueIssueFiles.forEach((fileName) => { + suite(fileName, () => { + let testFileUri: vscode.Uri; + const relevantTestDetails = getRelevantTestDetailsForFile(testDetails, fileName); + const relevantSkippedIssues = getRelevantSkippedIssuesFromTestDetailsForFile( + scenario.testDetails!, + fileName + ); + suiteSetup(async () => { + testFileUri = vscode.Uri.file(path.join(UNITTEST_TEST_FILES_PATH, fileName)); + diagnostics = testManager.diagnosticCollection.get(testFileUri)!; + getIssueCountFromRelevantTestDetails( + relevantTestDetails, + relevantSkippedIssues, + failedRun + ); + }); + // test('Test DiagnosticCollection', async () => { assert.equal(diagnostics.length, expectedDiagnosticCount, 'Diagnostics count'); }); + const validateTestFunctionAndDiagnostics = (td: ITestDetails) => { + suite(td.testName, () => { + let testFunc: FlattenedTestFunction; + let expectedStatus: TestStatus; + let diagnostic: vscode.Diagnostic; + let expectedDiagnostic: vscode.Diagnostic; + suiteSetup(async () => { + testFunc = getTestFuncFromResultsByTestFileAndName( + ioc, + results, + testFileUri, + td + )!; + expectedStatus = failedRun && td.passOnFailedRun ? TestStatus.Pass : td.status; + }); + suite('TestFunction', async () => { + test('Status', async () => { + assert.equal(testFunc.testFunction.status, expectedStatus, 'Test status'); + }); + }); + if (td.status !== TestStatus.Pass && !(failedRun && td.passOnFailedRun)) { + suite('Diagnostic', async () => { + suiteSetup(async () => { + diagnostic = getDiagnosticForTestFunc(diagnostics, testFunc)!; + expectedDiagnostic = await getExpectedDiagnosticFromTestDetails(td); + }); + test('Test Diagnostic', async () => { + await testDiagnostic(diagnostic, expectedDiagnostic); + }); + suite('Test DiagnosticRelatedInformation', async () => { + if (td.imported) { + test('Class Definition', async () => { + await testDiagnosticRelatedInformation( + diagnostic.relatedInformation![0], + expectedDiagnostic.relatedInformation![0] + ); + }); + } + test('Test Function Definition', async () => { + await testDiagnosticRelatedInformation( + diagnostic.relatedInformation![td.imported ? 1 : 0], + expectedDiagnostic.relatedInformation![td.imported ? 1 : 0] + ); + }); + if (td.status !== TestStatus.Skipped) { + test('Failure Line', async () => { + await testDiagnosticRelatedInformation( + diagnostic.relatedInformation![(td.imported ? 1 : 0) + 1], + expectedDiagnostic.relatedInformation![ + (td.imported ? 1 : 0) + 1 + ] + ); + }); + } + }); + }); + } + }); + }; + relevantTestDetails.forEach((td: ITestDetails) => { + validateTestFunctionAndDiagnostics(td); + }); + if (failedRun) { + relevantSkippedIssues.forEach((td: ITestDetails) => { + validateTestFunctionAndDiagnostics(td); + }); + } + }); + }); + }); + }; + shouldRunProperly('Run'); + if (scenario.shouldRunFailed) { + shouldRunProperly('Run Failed', true); + } + }); + }); +}); diff --git a/src/test/testing/pytest/pytest.test.ts b/src/test/testing/pytest/pytest.test.ts new file mode 100644 index 000000000000..489612b889e9 --- /dev/null +++ b/src/test/testing/pytest/pytest.test.ts @@ -0,0 +1,67 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { ITestManagerFactory } from '../../../client/testing/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; + +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'pythonFiles', + 'testFiles', + 'single' +); + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests - pytest - discovery against actual python process', () => { + let ioc: UnitTestIocContainer; + const configTarget = IS_MULTI_ROOT_TEST + ? vscode.ConfigurationTarget.WorkspaceFolder + : vscode.ConfigurationTarget.Workspace; + suiteSetup(async () => { + await initialize(); + await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); + }); + setup(async () => { + await initializeTest(); + initializeDI(); + }); + teardown(async () => { + await ioc.dispose(); + await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerProcessTypes(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + ioc.registerMockInterpreterTypes(); + ioc.registerInterpreterStorageTypes(); + } + + test('Discover Tests (single test file)', async () => { + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_one.py'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_root.py'), + true, + 'Test File not found' + ); + }); +}); diff --git a/src/test/testing/pytest/pytest.testMessageService.test.ts b/src/test/testing/pytest/pytest.testMessageService.test.ts new file mode 100644 index 000000000000..8396db04bf88 --- /dev/null +++ b/src/test/testing/pytest/pytest.testMessageService.test.ts @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as fs from 'fs'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import * as typeMoq from 'typemoq'; +import * as vscode from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { ProductNames } from '../../../client/common/installer/productNames'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { Product } from '../../../client/common/types'; +import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { TestDiscoveredTestParser } from '../../../client/testing/common/services/discoveredTestParser'; +import { TestResultsService } from '../../../client/testing/common/services/testResultsService'; +import { DiscoveredTests } from '../../../client/testing/common/services/types'; +import { ITestVisitor, TestDiscoveryOptions, Tests, TestStatus } from '../../../client/testing/common/types'; +import { XUnitParser } from '../../../client/testing/common/xUnitParser'; +import { TestMessageService } from '../../../client/testing/pytest/services/testMessageService'; +import { + ILocationStackFrameDetails, + IPythonTestMessage, + PythonTestMessageSeverity +} from '../../../client/testing/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../../initialize'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { ITestDetails, testScenarios } from './pytest_run_tests_data'; + +// tslint:disable:max-func-body-length + +const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); +const PYTEST_RESULTS_PATH = path.join( + EXTENSION_ROOT_DIR, + 'src', + 'test', + 'pythonFiles', + 'testFiles', + 'pytestFiles', + 'results' +); + +const filterdTestScenarios = testScenarios.filter((ts) => { + return !ts.shouldRunFailed; +}); + +async function testMessageProperties( + message: IPythonTestMessage, + expectedMessage: IPythonTestMessage, + imported: boolean = false, + status: TestStatus +) { + assert.equal(message.code, expectedMessage.code, 'IPythonTestMessage code'); + assert.equal(message.message, expectedMessage.message, 'IPythonTestMessage message'); + assert.equal(message.severity, expectedMessage.severity, 'IPythonTestMessage severity'); + assert.equal(message.provider, expectedMessage.provider, 'IPythonTestMessage provider'); + assert.isNumber(message.testTime, 'IPythonTestMessage testTime'); + assert.equal(message.status, expectedMessage.status, 'IPythonTestMessage status'); + assert.equal(message.testFilePath, expectedMessage.testFilePath, 'IPythonTestMessage testFilePath'); + if (status !== TestStatus.Pass) { + assert.equal( + message.locationStack![0].lineText, + expectedMessage.locationStack![0].lineText, + 'IPythonTestMessage line text' + ); + assert.equal( + message.locationStack![0].location.uri.fsPath, + expectedMessage.locationStack![0].location.uri.fsPath, + 'IPythonTestMessage locationStack fsPath' + ); + if (status !== TestStatus.Skipped) { + assert.equal( + message.locationStack![1].lineText, + expectedMessage.locationStack![1].lineText, + 'IPythonTestMessage line text' + ); + assert.equal( + message.locationStack![1].location.uri.fsPath, + expectedMessage.locationStack![1].location.uri.fsPath, + 'IPythonTestMessage locationStack fsPath' + ); + } + if (imported) { + assert.equal( + message.locationStack![2].lineText, + expectedMessage.locationStack![2].lineText, + 'IPythonTestMessage imported line text' + ); + assert.equal( + message.locationStack![2].location.uri.fsPath, + expectedMessage.locationStack![2].location.uri.fsPath, + 'IPythonTestMessage imported location fsPath' + ); + } + } +} + +/** + * Generate a Diagnostic object (including DiagnosticRelatedInformation) using the provided test details that reflects + * what the Diagnostic for the associated test should be in order for it to be compared to by the actual Diagnostic + * for the test. + * + * @param testDetails Test details for a specific test. + */ +async function getExpectedLocationStackFromTestDetails( + testDetails: ITestDetails +): Promise<ILocationStackFrameDetails[]> { + const locationStack: ILocationStackFrameDetails[] = []; + const testFilePath = path.join(UNITTEST_TEST_FILES_PATH, testDetails.fileName); + const testFileUri = vscode.Uri.file(testFilePath); + let expectedSourceTestFilePath = testFilePath; + if (testDetails.imported) { + expectedSourceTestFilePath = path.join(UNITTEST_TEST_FILES_PATH, testDetails.sourceFileName!); + } + const expectedSourceTestFileUri = vscode.Uri.file(expectedSourceTestFilePath); + if (testDetails.imported) { + // Stack should include the class furthest down the chain from the file that was executed. + locationStack.push({ + location: new vscode.Location(testFileUri, testDetails.classDefRange!), + lineText: testDetails.simpleClassName! + }); + } + locationStack.push({ + location: new vscode.Location(expectedSourceTestFileUri, testDetails.testDefRange!), + lineText: testDetails.sourceTestName + }); + if (testDetails.status !== TestStatus.Skipped) { + locationStack.push({ + location: new vscode.Location(expectedSourceTestFileUri, testDetails.issueRange!), + lineText: testDetails.issueLineText! + }); + } + return locationStack; +} + +// tslint:disable-next-line: max-func-body-length +suite('Unit Tests - PyTest - TestMessageService', () => { + let ioc: UnitTestIocContainer; + const filesystem = new FileSystem(); + const configTarget = IS_MULTI_ROOT_TEST + ? vscode.ConfigurationTarget.WorkspaceFolder + : vscode.ConfigurationTarget.Workspace; + suiteSetup(async () => { + await initialize(); + await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); + }); + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + // Mocks. + ioc.registerMockProcessTypes(); + ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); + ioc.serviceManager.addSingletonInstance<IInterpreterService>( + IInterpreterService, + instance(mock(InterpreterService)) + ); + } + // Build tests for the test data that is relevant for this platform. + filterdTestScenarios.forEach((scenario) => { + suite(scenario.scenarioName, async () => { + let testMessages: IPythonTestMessage[]; + suiteSetup(async () => { + await initializeTest(); + initializeDI(); + // Setup the service container for use by the parser. + const testVisitor = typeMoq.Mock.ofType<ITestVisitor>(); + const outChannel = typeMoq.Mock.ofType<vscode.OutputChannel>(); + const cancelToken = typeMoq.Mock.ofType<vscode.CancellationToken>(); + cancelToken.setup((c) => c.isCancellationRequested).returns(() => false); + const options: TestDiscoveryOptions = { + args: [], + cwd: UNITTEST_TEST_FILES_PATH, + ignoreCache: true, + outChannel: outChannel.object, + token: cancelToken.object, + workspaceFolder: vscode.Uri.file(__dirname) + }; + // Setup the parser. + const workspaceService = ioc.serviceContainer.get<IWorkspaceService>(IWorkspaceService); + const parser = new TestDiscoveredTestParser(workspaceService); + const discoveryOutput = fs + .readFileSync(path.join(PYTEST_RESULTS_PATH, scenario.discoveryOutput), 'utf8') + .replace( + /\/Users\/donjayamanne\/.vscode-insiders\/extensions\/pythonVSCode\/src\/test\/pythonFiles\/testFiles/g, + path.dirname(UNITTEST_TEST_FILES_PATH) + ) + .replace(/\\/g, '/'); + const discoveredTest: DiscoveredTests[] = JSON.parse(discoveryOutput); + options.workspaceFolder = vscode.Uri.file(discoveredTest[0].root); + const parsedTests: Tests = parser.parse(options.workspaceFolder, discoveredTest); + const xUnitParser = new XUnitParser(filesystem); + await xUnitParser.updateResultsFromXmlLogFile( + parsedTests, + path.join(PYTEST_RESULTS_PATH, scenario.runOutput) + ); + const testResultsService = new TestResultsService(testVisitor.object); + testResultsService.updateResults(parsedTests); + const testMessageService = new TestMessageService(ioc.serviceContainer); + testMessages = await testMessageService.getFilteredTestMessages(UNITTEST_TEST_FILES_PATH, parsedTests); + }); + suiteTeardown(async () => { + await ioc.dispose(); + await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); + }); + scenario.testDetails!.forEach((td) => { + suite(td.nameToRun, () => { + let testMessage: IPythonTestMessage; + let expectedMessage: IPythonTestMessage; + suiteSetup(async () => { + let expectedSeverity: PythonTestMessageSeverity; + if (td.status === TestStatus.Error || td.status === TestStatus.Fail) { + expectedSeverity = PythonTestMessageSeverity.Error; + } else if (td.status === TestStatus.Skipped) { + expectedSeverity = PythonTestMessageSeverity.Skip; + } else { + expectedSeverity = PythonTestMessageSeverity.Pass; + } + const expectedLocationStack = await getExpectedLocationStackFromTestDetails(td); + expectedMessage = { + code: td.nameToRun, + message: td.message, + severity: expectedSeverity, + provider: ProductNames.get(Product.pytest)!, + testTime: 0, + status: td.status, + locationStack: expectedLocationStack, + testFilePath: path.join(UNITTEST_TEST_FILES_PATH, td.fileName) + }; + testMessage = testMessages.find((tm) => tm.code === td.nameToRun)!; + }); + test('Message', async () => { + await testMessageProperties(testMessage, expectedMessage, td.imported, td.status); + }); + }); + }); + }); + }); +}); diff --git a/src/test/testing/pytest/pytest_run_tests_data.ts b/src/test/testing/pytest/pytest_run_tests_data.ts new file mode 100644 index 000000000000..33114b43569a --- /dev/null +++ b/src/test/testing/pytest/pytest_run_tests_data.ts @@ -0,0 +1,472 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import * as vscode from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { TestStatus, TestsToRun } from '../../../client/testing/common/types'; + +// tslint:disable: no-any + +const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); + +export interface ITestDetails { + className: string; + nameToRun: string; + fileName: string; + sourceFileName?: string; + testName: string; + simpleClassName?: string; + sourceTestName: string; + imported: boolean; + passOnFailedRun?: boolean; + status: TestStatus; + classDefRange?: vscode.Range; + testDefRange?: vscode.Range; + issueRange?: vscode.Range; + issueLineText?: string; + message?: string; + expectedDiagnostic?: vscode.Diagnostic; +} + +export const allTestDetails: ITestDetails[] = [ + { + className: 'test_root.Test_Root_test1', + nameToRun: './test_root.py::Test_Root_test1::test_Root_A', + fileName: 'test_root.py', + testName: 'test_Root_A', + sourceTestName: 'test_Root_A', + testDefRange: new vscode.Range(6, 8, 6, 19), + issueRange: new vscode.Range(7, 8, 7, 36), + issueLineText: 'self.fail("Not implemented")', + message: 'AssertionError: Not implemented', + imported: false, + status: TestStatus.Fail + }, + { + className: 'test_root.Test_Root_test1', + nameToRun: './test_root.py::Test_Root_test1::test_Root_B', + fileName: 'test_root.py', + testName: 'test_Root_B', + sourceTestName: 'test_Root_B', + imported: false, + status: TestStatus.Pass + }, + { + className: 'test_root.Test_Root_test1', + nameToRun: './test_root.py::Test_Root_test1::test_Root_c', + fileName: 'test_root.py', + testName: 'test_Root_c', + sourceTestName: 'test_Root_c', + testDefRange: new vscode.Range(13, 8, 13, 19), + message: 'demonstrating skipping', + imported: false, + status: TestStatus.Skipped + }, + { + className: 'tests.test_another_pytest', + nameToRun: './tests/test_another_pytest.py::test_username', + fileName: path.join(...'tests/test_another_pytest.py'.split('/')), + testName: 'test_username', + sourceTestName: 'test_username', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_another_pytest', + nameToRun: './tests/test_another_pytest.py::test_parametrized_username[one]', + fileName: path.join(...'tests/test_another_pytest.py'.split('/')), + testName: 'test_parametrized_username[one]', + sourceTestName: 'test_parametrized_username', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_another_pytest', + nameToRun: './tests/test_another_pytest.py::test_parametrized_username[two]', + fileName: path.join(...'tests/test_another_pytest.py'.split('/')), + testName: 'test_parametrized_username[two]', + sourceTestName: 'test_parametrized_username', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_another_pytest', + nameToRun: './tests/test_another_pytest.py::test_parametrized_username[three]', + fileName: path.join(...'tests/test_another_pytest.py'.split('/')), + testName: 'test_parametrized_username[three]', + sourceTestName: 'test_parametrized_username', + testDefRange: new vscode.Range(15, 4, 15, 30), + issueRange: new vscode.Range(16, 4, 16, 64), + issueLineText: "assert non_parametrized_username in ['one', 'two', 'threes']", + message: "AssertionError: assert 'three' in ['one', 'two', 'threes']", + imported: false, + status: TestStatus.Fail + }, + { + className: + 'tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.().TestExtraNestedForeignTests.()', + nameToRun: + './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign', + simpleClassName: 'TestInheritingHere', + fileName: path.join(...'tests/test_foreign_nested_tests.py'.split('/')), + testName: 'test_super_deep_foreign', + sourceTestName: 'test_super_deep_foreign', + sourceFileName: path.join(...'tests/external.py'.split('/')), + classDefRange: new vscode.Range(4, 10, 4, 28), + testDefRange: new vscode.Range(2, 12, 2, 35), + issueRange: new vscode.Range(3, 12, 3, 24), + issueLineText: 'assert False', + message: 'AssertionError', + imported: true, + status: TestStatus.Fail + }, + { + className: 'tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()', + nameToRun: + './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test', + simpleClassName: 'TestInheritingHere', + fileName: path.join(...'tests/test_foreign_nested_tests.py'.split('/')), + testName: 'test_foreign_test', + sourceTestName: 'test_foreign_test', + sourceFileName: path.join(...'tests/external.py'.split('/')), + classDefRange: new vscode.Range(4, 10, 4, 28), + testDefRange: new vscode.Range(4, 8, 4, 25), + issueRange: new vscode.Range(5, 8, 5, 20), + issueLineText: 'assert False', + message: 'AssertionError', + imported: true, + status: TestStatus.Fail + }, + { + className: 'tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()', + nameToRun: + './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal', + fileName: path.join(...'tests/test_foreign_nested_tests.py'.split('/')), + testName: 'test_nested_normal', + sourceTestName: 'test_nested_normal', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_foreign_nested_tests.TestNestedForeignTests', + nameToRun: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal', + fileName: path.join(...'tests/test_foreign_nested_tests.py'.split('/')), + testName: 'test_normal', + sourceTestName: 'test_normal', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_pytest.Test_CheckMyApp', + nameToRun: './tests/test_pytest.py::Test_CheckMyApp::test_simple_check', + fileName: path.join(...'tests/test_pytest.py'.split('/')), + testName: 'test_simple_check', + sourceTestName: 'test_simple_check', + testDefRange: new vscode.Range(7, 8, 7, 25), + message: 'demonstrating skipping', + imported: false, + status: TestStatus.Skipped + }, + { + className: 'tests.test_pytest.Test_CheckMyApp', + nameToRun: './tests/test_pytest.py::Test_CheckMyApp::test_complex_check', + fileName: path.join(...'tests/test_pytest.py'.split('/')), + testName: 'test_complex_check', + sourceTestName: 'test_complex_check', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()', + nameToRun: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB', + fileName: path.join(...'tests/test_pytest.py'.split('/')), + testName: 'test_nested_class_methodB', + sourceTestName: 'test_nested_class_methodB', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.().Test_nested_classB_Of_A.()', + nameToRun: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d', + fileName: path.join(...'tests/test_pytest.py'.split('/')), + testName: 'test_d', + sourceTestName: 'test_d', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()', + nameToRun: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC', + fileName: path.join(...'tests/test_pytest.py'.split('/')), + testName: 'test_nested_class_methodC', + sourceTestName: 'test_nested_class_methodC', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_pytest.Test_CheckMyApp', + nameToRun: './tests/test_pytest.py::Test_CheckMyApp::test_simple_check2', + fileName: path.join(...'tests/test_pytest.py'.split('/')), + testName: 'test_simple_check2', + sourceTestName: 'test_simple_check2', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_pytest.Test_CheckMyApp', + nameToRun: './tests/test_pytest.py::Test_CheckMyApp::test_complex_check2', + fileName: path.join(...'tests/test_pytest.py'.split('/')), + testName: 'test_complex_check2', + sourceTestName: 'test_complex_check2', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_pytest', + nameToRun: './tests/test_pytest.py::test_username', + fileName: path.join(...'tests/test_pytest.py'.split('/')), + testName: 'test_username', + sourceTestName: 'test_username', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_pytest', + nameToRun: './tests/test_pytest.py::test_parametrized_username[one]', + fileName: path.join(...'tests/test_pytest.py'.split('/')), + testName: 'test_parametrized_username[one]', + sourceTestName: 'test_parametrized_username', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_pytest', + nameToRun: './tests/test_pytest.py::test_parametrized_username[two]', + fileName: path.join(...'tests/test_pytest.py'.split('/')), + testName: 'test_parametrized_username[two]', + sourceTestName: 'test_parametrized_username', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_pytest', + nameToRun: './tests/test_pytest.py::test_parametrized_username[three]', + fileName: path.join(...'tests/test_pytest.py'.split('/')), + testName: 'test_parametrized_username[three]', + sourceTestName: 'test_parametrized_username', + testDefRange: new vscode.Range(38, 4, 38, 30), + issueRange: new vscode.Range(39, 4, 39, 64), + issueLineText: "assert non_parametrized_username in ['one', 'two', 'threes']", + message: "AssertionError: assert 'three' in ['one', 'two', 'threes']", + imported: false, + status: TestStatus.Fail + }, + { + className: 'tests.test_unittest_one.Test_test1', + nameToRun: './tests/test_unittest_one.py::Test_test1::test_A', + fileName: path.join(...'tests/test_unittest_one.py'.split('/')), + testName: 'test_A', + sourceTestName: 'test_A', + testDefRange: new vscode.Range(6, 8, 6, 14), + issueRange: new vscode.Range(7, 8, 7, 36), + issueLineText: 'self.fail("Not implemented")', + message: 'AssertionError: Not implemented', + imported: false, + status: TestStatus.Fail + }, + { + className: 'tests.test_unittest_one.Test_test1', + nameToRun: './tests/test_unittest_one.py::Test_test1::test_B', + fileName: path.join(...'tests/test_unittest_one.py'.split('/')), + testName: 'test_B', + sourceTestName: 'test_B', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_unittest_one.Test_test1', + nameToRun: './tests/test_unittest_one.py::Test_test1::test_c', + fileName: path.join(...'tests/test_unittest_one.py'.split('/')), + testName: 'test_c', + sourceTestName: 'test_c', + testDefRange: new vscode.Range(13, 8, 13, 14), + message: 'demonstrating skipping', + imported: false, + status: TestStatus.Skipped + }, + { + className: 'tests.test_unittest_two.Test_test2', + nameToRun: './tests/test_unittest_two.py::Test_test2::test_A2', + fileName: path.join(...'tests/test_unittest_two.py'.split('/')), + testName: 'test_A2', + sourceTestName: 'test_A2', + testDefRange: new vscode.Range(3, 8, 3, 15), + issueRange: new vscode.Range(4, 8, 4, 36), + issueLineText: 'self.fail("Not implemented")', + message: 'AssertionError: Not implemented', + imported: false, + status: TestStatus.Fail + }, + { + className: 'tests.test_unittest_two.Test_test2', + nameToRun: './tests/test_unittest_two.py::Test_test2::test_B2', + fileName: path.join(...'tests/test_unittest_two.py'.split('/')), + testName: 'test_B2', + sourceTestName: 'test_B2', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.test_unittest_two.Test_test2', + nameToRun: './tests/test_unittest_two.py::Test_test2::test_C2', + fileName: path.join(...'tests/test_unittest_two.py'.split('/')), + testName: 'test_C2', + sourceTestName: 'test_C2', + testDefRange: new vscode.Range(9, 8, 9, 15), + issueRange: new vscode.Range(10, 8, 10, 41), + issueLineText: "self.assertEqual(1,2,'Not equal')", + message: 'AssertionError: 1 != 2 : Not equal', + imported: false, + status: TestStatus.Fail + }, + { + className: 'tests.test_unittest_two.Test_test2', + nameToRun: './tests/test_unittest_two.py::Test_test2::test_D2', + fileName: path.join(...'tests/test_unittest_two.py'.split('/')), + testName: 'test_D2', + sourceTestName: 'test_D2', + testDefRange: new vscode.Range(12, 8, 12, 15), + issueRange: new vscode.Range(13, 8, 13, 31), + issueLineText: 'raise ArithmeticError()', + message: 'ArithmeticError', + imported: false, + status: TestStatus.Fail + }, + { + className: 'tests.test_unittest_two.Test_test2a', + nameToRun: './tests/test_unittest_two.py::Test_test2a::test_222A2', + fileName: path.join(...'tests/test_unittest_two.py'.split('/')), + testName: 'test_222A2', + sourceTestName: 'test_222A2', + testDefRange: new vscode.Range(17, 8, 17, 18), + issueRange: new vscode.Range(18, 8, 18, 36), + issueLineText: 'self.fail("Not implemented")', + message: 'AssertionError: Not implemented', + imported: false, + passOnFailedRun: true, + status: TestStatus.Fail + }, + { + className: 'tests.test_unittest_two.Test_test2a', + nameToRun: './tests/test_unittest_two.py::Test_test2a::test_222B2', + fileName: path.join(...'tests/test_unittest_two.py'.split('/')), + testName: 'test_222B2', + sourceTestName: 'test_222B2', + imported: false, + status: TestStatus.Pass + }, + { + className: 'tests.unittest_three_test.Test_test3', + nameToRun: './tests/unittest_three_test.py::Test_test3::test_A', + fileName: path.join(...'tests/unittest_three_test.py'.split('/')), + testName: 'test_A', + sourceTestName: 'test_A', + testDefRange: new vscode.Range(4, 8, 4, 14), + issueRange: new vscode.Range(5, 8, 5, 36), + issueLineText: 'self.fail("Not implemented")', + message: 'AssertionError: Not implemented', + imported: false, + status: TestStatus.Fail + }, + { + className: 'tests.unittest_three_test.Test_test3', + nameToRun: './tests/unittest_three_test.py::Test_test3::test_B', + fileName: path.join(...'tests/unittest_three_test.py'.split('/')), + testName: 'test_B', + sourceTestName: 'test_B', + imported: false, + status: TestStatus.Pass + } +]; + +export interface ITestScenarioDetails { + scenarioName: string; + discoveryOutput: string; + runOutput: string; + testsToRun?: TestsToRun; + testDetails?: ITestDetails[]; + testSuiteIndex?: number; + testFunctionIndex?: number; + shouldRunFailed?: boolean; + failedRunOutput?: string; +} + +export const testScenarios: ITestScenarioDetails[] = [ + { + scenarioName: 'Run Tests', + discoveryOutput: 'one.output', + runOutput: 'one.xml', + testsToRun: undefined as any, + testDetails: allTestDetails.filter(() => { + return true; + }) + }, + { + scenarioName: 'Run Specific Test File', + discoveryOutput: 'three.output', + runOutput: 'three.xml', + testsToRun: { + testFile: [ + { + fullPath: path.join(UNITTEST_TEST_FILES_PATH, 'tests', 'test_another_pytest.py'), + name: 'tests/test_another_pytest.py', + nameToRun: 'tests/test_another_pytest.py', + xmlName: 'tests/test_another_pytest.py', + functions: [], + suites: [], + time: 0 + } + ], + testFolder: [], + testFunction: [], + testSuite: [] + }, + testDetails: allTestDetails.filter((td) => { + return td.fileName === path.join('tests', 'test_another_pytest.py'); + }) + }, + { + scenarioName: 'Run Specific Test Suite', + discoveryOutput: 'four.output', + runOutput: 'four.xml', + testsToRun: undefined as any, + testSuiteIndex: 0, + testDetails: allTestDetails.filter((td) => { + return td.className === 'test_root.Test_Root_test1'; + }) + }, + { + scenarioName: 'Run Specific Test Function', + discoveryOutput: 'five.output', + runOutput: 'five.xml', + testsToRun: undefined as any, + testFunctionIndex: 0, + testDetails: allTestDetails.filter((td) => { + return td.testName === 'test_Root_A'; + }) + }, + { + scenarioName: 'Run Failed Tests', + discoveryOutput: 'two.output', + runOutput: 'two.xml', + testsToRun: undefined as any, + testDetails: allTestDetails.filter((_td) => { + return true; + }), + shouldRunFailed: true, + failedRunOutput: 'two.again.xml' + } +]; diff --git a/src/test/testing/pytest/pytest_unittest_parser_data.ts b/src/test/testing/pytest/pytest_unittest_parser_data.ts new file mode 100644 index 000000000000..30bdf9d1cec4 --- /dev/null +++ b/src/test/testing/pytest/pytest_unittest_parser_data.ts @@ -0,0 +1,2087 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// disable the ' quotemark, as we need to consume many strings from stdout that use that +// test delimiter exclusively. + +// tslint:disable:quotemark + +export enum PytestDataPlatformType { + NonWindows = 'non-windows', + Windows = 'windows' +} + +export type PytestDiscoveryScenario = { + pytest_version_spec: string; + platform: string; + description: string; + rootdir: string; + test_functions: string[]; + functionCount: number; + stdout: string[]; +}; + +// Data to test the pytest unit test parser with. See pytest.discovery.unit.test.ts. +export const pytestScenarioData: PytestDiscoveryScenario[] = [ + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.NonWindows, + description: + 'Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'src/test_things.py::test_things_major', + 'test/this/is/deep/testing/test_very_deeply.py::test_math_works' + ], + functionCount: 9, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 9 items', + "<Module 'src/test_things.py'>", + " <Function 'test_things_major'>", + " <Function 'test_things_minor'>", + "<Module 'src/under/test_other_stuff.py'>", + " <Function 'test_machine_values'>", + "<Module 'src/under/test_stuff.py'>", + " <Function 'test_platform'>", + "<Module 'test/test_other_other_things.py'>", + " <Function 'test_sys_ver'>", + "<Module 'test/test_other_things.py'>", + " <Function 'test_sys_ver'>", + "<Module 'test/this/is/deep/testing/test_deeply.py'>", + " <Function 'test_json_works'>", + " <Function 'test_json_numbers_work'>", + "<Module 'test/this/is/deep/testing/test_very_deeply.py'>", + " <Function 'test_math_works'>", + '', + '========================= no tests ran in 0.02 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.NonWindows, + description: + 'Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'src/test_things.py::test_things_major', + 'test/this/is/deep/testing/test_very_deeply.py::test_math_works' + ], + functionCount: 9, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 9 items', + "<Module 'src/test_things.py'>", + " <Function 'test_things_major'>", + " <Function 'test_things_minor'>", + "<Module 'src/under/test_other_stuff.py'>", + " <Function 'test_machine_values'>", + "<Module 'src/under/test_stuff.py'>", + " <Function 'test_platform'>", + "<Module 'test/test_other_other_things.py'>", + " <Function 'test_sys_ver'>", + "<Module 'test/test_other_things.py'>", + " <Function 'test_sys_ver'>", + "<Module 'test/this/is/deep/testing/test_deeply.py'>", + " <Function 'test_json_works'>", + " <Function 'test_json_numbers_work'>", + "<Module 'test/this/is/deep/testing/test_very_deeply.py'>", + " <Function 'test_math_works'>", + '', + '========================= no tests ran in 0.18 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.NonWindows, + description: + 'Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'src/test_things.py::test_things_major', + 'test/this/is/deep/testing/test_very_deeply.py::test_math_works' + ], + functionCount: 9, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 9 items', + '<Module src/test_things.py>', + ' <Function test_things_major>', + ' <Function test_things_minor>', + '<Module src/under/test_other_stuff.py>', + ' <Function test_machine_values>', + '<Module src/under/test_stuff.py>', + ' <Function test_platform>', + '<Module test/test_other_other_things.py>', + ' <Function test_sys_ver>', + '<Module test/test_other_things.py>', + ' <Function test_sys_ver>', + '<Module test/this/is/deep/testing/test_deeply.py>', + ' <Function test_json_works>', + ' <Function test_json_numbers_work>', + '<Module test/this/is/deep/testing/test_very_deeply.py>', + ' <Function test_math_works>', + '', + '========================= no tests ran in 0.18 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['src/test_things.py::test_things_major', 'src/under/test_stuff.py::test_platform'], + functionCount: 5, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 5 items', + "<Module 'src/test_things.py'>", + " <Function 'test_things_major'>", + " <Function 'test_things_minor'>", + "<Module 'src/test_things_again.py'>", + " <Function 'test_it_over_again'>", + "<Module 'src/under/test_other_stuff.py'>", + " <Function 'test_machine_values'>", + "<Module 'src/under/test_stuff.py'>", + " <Function 'test_platform'>", + '', + '========================= no tests ran in 0.05 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['src/test_things.py::test_things_major', 'src/under/test_stuff.py::test_platform'], + functionCount: 5, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 5 items', + "<Module 'src/test_things.py'>", + " <Function 'test_things_major'>", + " <Function 'test_things_minor'>", + "<Module 'src/test_things_again.py'>", + " <Function 'test_it_over_again'>", + "<Module 'src/under/test_other_stuff.py'>", + " <Function 'test_machine_values'>", + "<Module 'src/under/test_stuff.py'>", + " <Function 'test_platform'>", + '', + '========================= no tests ran in 0.03 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['src/test_things.py::test_things_major', 'src/under/test_stuff.py::test_platform'], + functionCount: 5, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 5 items', + '<Module src/test_things.py>', + ' <Function test_things_major>', + ' <Function test_things_minor>', + '<Module src/test_things_again.py>', + ' <Function test_it_over_again>', + '<Module src/under/test_other_stuff.py>', + ' <Function test_machine_values>', + '<Module src/under/test_stuff.py>', + ' <Function test_platform>', + '', + '========================= no tests ran in 0.03 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 test modules in root folder and two more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['test_things.py::test_things_major', 'under/test_stuff.py::test_platform'], + functionCount: 5, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 5 items', + "<Module 'test_things.py'>", + " <Function 'test_things_major'>", + " <Function 'test_things_minor'>", + "<Module 'test_things_again.py'>", + " <Function 'test_it_over_again'>", + "<Module 'under/test_other_stuff.py'>", + " <Function 'test_machine_values'>", + "<Module 'under/test_stuff.py'>", + " <Function 'test_platform'>", + '', + '========================= no tests ran in 0.12 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 test modules in root folder and two more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['test_things.py::test_things_major', 'under/test_stuff.py::test_platform'], + functionCount: 5, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 5 items', + "<Module 'test_things.py'>", + " <Function 'test_things_major'>", + " <Function 'test_things_minor'>", + "<Module 'test_things_again.py'>", + " <Function 'test_it_over_again'>", + "<Module 'under/test_other_stuff.py'>", + " <Function 'test_machine_values'>", + "<Module 'under/test_stuff.py'>", + " <Function 'test_platform'>", + '', + '========================= no tests ran in 0.12 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 test modules in root folder and two more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['test_things.py::test_things_major', 'under/test_stuff.py::test_platform'], + functionCount: 5, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 5 items', + '<Module test_things.py>', + ' <Function test_things_major>', + ' <Function test_things_minor>', + '<Module test_things_again.py>', + ' <Function test_it_over_again>', + '<Module under/test_other_stuff.py>', + ' <Function test_machine_values>', + '<Module under/test_stuff.py>', + ' <Function test_platform>', + '', + '========================= no tests ran in 0.12 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 test modules in a subfolder off the root.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['under/test_other_stuff.py::test_machine_values', 'under/test_stuff.py::test_platform'], + functionCount: 2, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 2 items', + "<Module 'under/test_other_stuff.py'>", + " <Function 'test_machine_values'>", + "<Module 'under/test_stuff.py'>", + " <Function 'test_platform'>", + '', + '========================= no tests ran in 0.06 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 test modules in a subfolder off the root.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['under/test_other_stuff.py::test_machine_values', 'under/test_stuff.py::test_platform'], + functionCount: 2, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 2 items', + "<Module 'under/test_other_stuff.py'>", + " <Function 'test_machine_values'>", + "<Module 'under/test_stuff.py'>", + " <Function 'test_platform'>", + '', + '========================= no tests ran in 0.05 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 test modules in a subfolder off the root.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['under/test_other_stuff.py::test_machine_values', 'under/test_stuff.py::test_platform'], + functionCount: 2, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 2 items', + '<Module under/test_other_stuff.py>', + ' <Function test_machine_values>', + '<Module under/test_stuff.py>', + ' <Function test_platform>', + '', + '========================= no tests ran in 0.05 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 modules at the topmost level.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['test_other_stuff.py::test_machine_values', 'test_stuff.py::test_platform'], + functionCount: 2, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 2 items', + "<Module 'test_other_stuff.py'>", + " <Function 'test_machine_values'>", + "<Module 'test_stuff.py'>", + " <Function 'test_platform'>", + '', + '========================= no tests ran in 0.05 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 modules at the topmost level.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['test_other_stuff.py::test_machine_values', 'test_stuff.py::test_platform'], + functionCount: 2, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 2 items', + "<Module 'test_other_stuff.py'>", + " <Function 'test_machine_values'>", + "<Module 'test_stuff.py'>", + " <Function 'test_platform'>", + '', + '========================= no tests ran in 0.05 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Non-package source, 2 modules at the topmost level.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['test_other_stuff.py::test_machine_values', 'test_stuff.py::test_platform'], + functionCount: 2, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 2 items', + '<Module test_other_stuff.py>', + ' <Function test_machine_values>', + '<Module test_stuff.py>', + ' <Function test_platform>', + '', + '========================= no tests ran in 0.05 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.NonWindows, + description: + 'Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test_basic_root.py::test_basic_major', + 'test/test_other_basic.py::test_basic_major_minor_internal', + 'test/subdir/under/another/subdir/test_other_basic_sub.py::test_basic_major_minor' + ], + functionCount: 16, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 16 items', + "<Module 'test_basic_root.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test_other_basic_root.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + "<Module 'test/test_basic.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test/test_other_basic.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + "<Module 'test/subdir/under/another/subdir/test_basic_sub.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test/subdir/under/another/subdir/test_other_basic_sub.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + "<Module 'test/uneven/folders/test_basic_uneven.py'>", + " <Function 'test_basic_major_uneven'>", + " <Function 'test_basic_minor_uneven'>", + "<Module 'test/uneven/folders/test_other_basic_uneven.py'>", + " <Function 'test_basic_major_minor_uneven'>", + " <Function 'test_basic_major_minor_internal_uneven'>", + '', + '========================= no tests ran in 0.07 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.NonWindows, + description: + 'Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test_basic_root.py::test_basic_major', + 'test/test_other_basic.py::test_basic_major_minor_internal', + 'test/subdir/under/another/subdir/test_other_basic_sub.py::test_basic_major_minor', + 'test/uneven/folders/test_other_basic_uneven.py::test_basic_major_minor_internal_uneven' + ], + functionCount: 16, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 16 items', + "<Package '/home/user/test/pytest_scenario'>", + " <Module 'test_basic_root.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_other_basic_root.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + " <Package '/home/user/test/pytest_scenario/test'>", + " <Module 'test_basic.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_other_basic.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + " <Package '/home/user/test/pytest_scenario/test/subdir'>", + " <Package '/home/user/test/pytest_scenario/test/subdir/under'>", + " <Package '/home/user/test/pytest_scenario/test/subdir/under/another'>", + " <Package '/home/user/test/pytest_scenario/test/subdir/under/another/subdir'>", + " <Module 'test_basic_sub.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_other_basic_sub.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + " <Package '/home/user/test/pytest_scenario/test/uneven'>", + " <Package '/home/user/test/pytest_scenario/test/uneven/folders'>", + " <Module 'test_basic_uneven.py'>", + " <Function 'test_basic_major_uneven'>", + " <Function 'test_basic_minor_uneven'>", + " <Module 'test_other_basic_uneven.py'>", + " <Function 'test_basic_major_minor_uneven'>", + " <Function 'test_basic_major_minor_internal_uneven'>", + '', + '========================= no tests ran in 0.13 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.NonWindows, + description: + 'Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test_basic_root.py::test_basic_major', + 'test/test_other_basic.py::test_basic_major_minor_internal', + 'test/subdir/under/another/subdir/test_other_basic_sub.py::test_basic_major_minor', + 'test/uneven/folders/test_other_basic_uneven.py::test_basic_major_minor_internal_uneven' + ], + functionCount: 16, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 16 items', + '<Package /home/user/test/pytest_scenario>', + ' <Module test_basic_root.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_other_basic_root.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + ' <Package /home/user/test/pytest_scenario/test>', + ' <Module test_basic.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_other_basic.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + ' <Package /home/user/test/pytest_scenario/test/subdir>', + ' <Package /home/user/test/pytest_scenario/test/subdir/under>', + ' <Package /home/user/test/pytest_scenario/test/subdir/under/another>', + ' <Package /home/user/test/pytest_scenario/test/subdir/under/another/subdir>', + ' <Module test_basic_sub.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_other_basic_sub.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + ' <Package /home/user/test/pytest_scenario/test/uneven>', + ' <Package /home/user/test/pytest_scenario/test/uneven/folders>', + ' <Module test_basic_uneven.py>', + ' <Function test_basic_major_uneven>', + ' <Function test_basic_minor_uneven>', + ' <Module test_other_basic_uneven.py>', + ' <Function test_basic_major_minor_uneven>', + ' <Function test_basic_major_minor_internal_uneven>', + '', + '========================= no tests ran in 0.13 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.NonWindows, + description: + 'Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test/test_other_basic.py::test_basic_major_minor_internal', + 'test/subdir/test_other_basic_sub.py::test_basic_major_minor' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + "<Module 'test/test_basic.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test/test_basic_root.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test/test_other_basic.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + "<Module 'test/test_other_basic_root.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + "<Module 'test/subdir/test_basic_sub.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test/subdir/test_other_basic_sub.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + '', + '========================= no tests ran in 0.18 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.NonWindows, + description: + 'Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test/test_other_basic.py::test_basic_major_minor_internal', + 'test/subdir/test_other_basic_sub.py::test_basic_major_minor' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + "<Package '/home/user/test/pytest_scenario'>", + " <Package '/home/user/test/pytest_scenario/test'>", + " <Module 'test_basic.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_basic_root.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_other_basic.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + " <Module 'test_other_basic_root.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + " <Package '/home/user/test/pytest_scenario/test/subdir'>", + " <Module 'test_basic_sub.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_other_basic_sub.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + '', + '========================= no tests ran in 0.07 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.NonWindows, + description: + 'Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test/test_other_basic.py::test_basic_major_minor_internal', + 'test/subdir/test_other_basic_sub.py::test_basic_major_minor' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + '<Package /home/user/test/pytest_scenario>', + ' <Package /home/user/test/pytest_scenario/test>', + ' <Module test_basic.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_basic_root.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_other_basic.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + ' <Module test_other_basic_root.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + ' <Package /home/user/test/pytest_scenario/test/subdir>', + ' <Module test_basic_sub.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_other_basic_sub.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + '', + '========================= no tests ran in 0.07 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.NonWindows, + description: 'Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test_other_basic_root.py::test_basic_major_minor_internal', + 'test/test_other_basic_sub.py::test_basic_major_minor' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + "<Module 'test_basic.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test_basic_root.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test_other_basic.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + "<Module 'test_other_basic_root.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + "<Module 'test/test_basic_sub.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test/test_other_basic_sub.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + '', + '========================= no tests ran in 0.18 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test_other_basic_root.py::test_basic_major_minor_internal', + 'test/test_basic_sub.py::test_basic_major', + 'test/test_basic_sub.py::test_basic_minor' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + "<Package '/home/user/test/pytest_scenario'>", + " <Module 'test_basic.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_basic_root.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_other_basic.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + " <Module 'test_other_basic_root.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + " <Package '/home/user/test/pytest_scenario/test'>", + " <Module 'test_basic_sub.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_other_basic_sub.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + '', + '========================= no tests ran in 0.22 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test_other_basic_root.py::test_basic_major_minor_internal', + 'test/test_basic_sub.py::test_basic_major', + 'test/test_basic_sub.py::test_basic_minor' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + '<Package /home/user/test/pytest_scenario>', + ' <Module test_basic.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_basic_root.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_other_basic.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + ' <Module test_other_basic_root.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + ' <Package /home/user/test/pytest_scenario/test>', + ' <Module test_basic_sub.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_other_basic_sub.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + '', + '========================= no tests ran in 0.22 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.NonWindows, + description: 'Package-based source, 2+ test modules in a subfolder off the root.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test/test_basic.py::test_basic_minor', + 'test/test_other_basic.py::test_basic_major_minor', + 'test/test_other_basic_root.py::test_basic_major_minor', + 'test/test_other_basic_sub.py::test_basic_major_minor_internal' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + "<Module 'test/test_basic.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test/test_basic_root.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test/test_basic_sub.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test/test_other_basic.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + "<Module 'test/test_other_basic_root.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + "<Module 'test/test_other_basic_sub.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + '', + '========================= no tests ran in 0.15 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Package-based source, 2+ test modules in a subfolder off the root.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test/test_basic.py::test_basic_minor', + 'test/test_other_basic.py::test_basic_major_minor', + 'test/test_other_basic_root.py::test_basic_major_minor', + 'test/test_other_basic_sub.py::test_basic_major_minor_internal' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + "<Package '/home/user/test/pytest_scenario'>", + " <Package '/home/user/test/pytest_scenario/test'>", + " <Module 'test_basic.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_basic_root.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_basic_sub.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_other_basic.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + " <Module 'test_other_basic_root.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + " <Module 'test_other_basic_sub.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + '', + '========================= no tests ran in 0.15 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Package-based source, 2+ test modules in a subfolder off the root.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test/test_basic.py::test_basic_minor', + 'test/test_other_basic.py::test_basic_major_minor', + 'test/test_other_basic_root.py::test_basic_major_minor', + 'test/test_other_basic_sub.py::test_basic_major_minor_internal' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + '<Package /home/user/test/pytest_scenario>', + ' <Package /home/user/test/pytest_scenario/test>', + ' <Module test_basic.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_basic_root.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_basic_sub.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_other_basic.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + ' <Module test_other_basic_root.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + ' <Module test_other_basic_sub.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + '', + '========================= no tests ran in 0.15 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.NonWindows, + description: 'Package-based source, 2+ modules at the topmost level.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test_basic.py::test_basic_major', + 'test_basic_root.py::test_basic_major', + 'test_other_basic_root.py::test_basic_major_minor', + 'test_other_basic_sub.py::test_basic_major_minor_internal' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + "<Module 'test_basic.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test_basic_root.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test_basic_sub.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + "<Module 'test_other_basic.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + "<Module 'test_other_basic_root.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + "<Module 'test_other_basic_sub.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + '', + '========================= no tests ran in 0.23 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Package-based source, 2+ modules at the topmost level.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test_basic.py::test_basic_major', + 'test_basic_root.py::test_basic_major', + 'test_other_basic_root.py::test_basic_major_minor', + 'test_other_basic_sub.py::test_basic_major_minor_internal' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + "<Package '/home/user/test/pytest_scenario'>", + " <Module 'test_basic.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_basic_root.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_basic_sub.py'>", + " <Function 'test_basic_major'>", + " <Function 'test_basic_minor'>", + " <Module 'test_other_basic.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + " <Module 'test_other_basic_root.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + " <Module 'test_other_basic_sub.py'>", + " <Function 'test_basic_major_minor'>", + " <Function 'test_basic_major_minor_internal'>", + '', + '========================= no tests ran in 0.16 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Package-based source, 2+ modules at the topmost level.', + rootdir: '/home/user/test/pytest_scenario', + test_functions: [ + 'test_basic.py::test_basic_major', + 'test_basic_root.py::test_basic_major', + 'test_other_basic_root.py::test_basic_major_minor', + 'test_other_basic_sub.py::test_basic_major_minor_internal' + ], + functionCount: 12, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 12 items', + '<Package /home/user/test/pytest_scenario>', + ' <Module test_basic.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_basic_root.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_basic_sub.py>', + ' <Function test_basic_major>', + ' <Function test_basic_minor>', + ' <Module test_other_basic.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + ' <Module test_other_basic_root.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + ' <Module test_other_basic_sub.py>', + ' <Function test_basic_major_minor>', + ' <Function test_basic_major_minor_internal>', + '', + '========================= no tests ran in 0.16 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.Windows, + description: + 'Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'other_tests/test_base_stuff.py::test_do_other_test', + 'other_tests/test_base_stuff.py::test_do_test', + 'tests/further_tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_100', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 8 items', + "<Module 'other_tests/test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'tests/further_tests/test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/further_tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + "<Module 'tests/further_tests/deeper/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + '', + '======================== no tests ran in 0.30 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.Windows, + description: + 'Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'other_tests/test_base_stuff.py::test_do_other_test', + 'other_tests/test_base_stuff.py::test_do_test', + 'tests/further_tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_100', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Package 'e:\\\\user\\\\test\\\\pytest_scenario'>", + " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\other_tests'>", + " <Module 'test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests'>", + " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests'>", + " <Module 'test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + " <Module 'test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests\\\\deeper'>", + " <Module 'test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + '', + '======================== no tests ran in 0.42 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.Windows, + description: + 'Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'other_tests/test_base_stuff.py::test_do_other_test', + 'other_tests/test_base_stuff.py::test_do_test', + 'tests/further_tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_100', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + '<Package e:\\\\user\\\\test\\\\pytest_scenario>', + ' <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\other_tests>', + ' <Module test_base_stuff.py>', + ' <Function test_do_test>', + ' <Function test_do_other_test>', + ' <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests>', + ' <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests>', + ' <Module test_gimme_5.py>', + ' <Function test_gimme_5>', + ' <Module test_multiply.py>', + ' <Function test_times_10>', + ' <Function test_times_2>', + ' <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests\\\\deeper>', + ' <Module test_more_multiply.py>', + ' <Function test_times_100>', + ' <Function test_times_negative_1>', + '', + '======================== no tests ran in 0.42 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.Windows, + description: + 'Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'other_tests/test_base_stuff.py::test_do_other_test', + 'other_tests/test_base_stuff.py::test_do_test', + 'tests/further_tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_100', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Module 'other_tests/test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'tests/further_tests/test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/further_tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + "<Module 'tests/further_tests/deeper/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + '', + '======================== no tests ran in 0.11 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.Windows, + description: + 'Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'other_tests/test_base_stuff.py::test_do_other_test', + 'other_tests/test_base_stuff.py::test_do_test', + 'tests/further_tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_100', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Module 'other_tests/test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'tests/further_tests/test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/further_tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + "<Module 'tests/further_tests/deeper/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + '', + '======================== no tests ran in 0.17 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.Windows, + description: + 'Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'other_tests/test_base_stuff.py::test_do_other_test', + 'other_tests/test_base_stuff.py::test_do_test', + 'tests/further_tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_100', + 'tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + '<Module other_tests/test_base_stuff.py>', + ' <Function test_do_test>', + ' <Function test_do_other_test>', + '<Module tests/further_tests/test_gimme_5.py>', + ' <Function test_gimme_5>', + '<Module tests/further_tests/test_multiply.py>', + ' <Function test_times_10>', + ' <Function test_times_2>', + '<Module tests/further_tests/deeper/test_more_multiply.py>', + ' <Function test_times_100>', + ' <Function test_times_negative_1>', + '', + '======================== no tests ran in 0.17 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.Windows, + description: + 'Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_more_multiply.py::test_times_100', + 'tests/further_tests/test_more_multiply.py::test_times_negative_1', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Module 'tests/test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'tests/test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/further_tests/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + "<Module 'tests/further_tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.26 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.Windows, + description: + 'Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_more_multiply.py::test_times_100', + 'tests/further_tests/test_more_multiply.py::test_times_negative_1', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Package 'e:\\\\user\\\\test\\\\pytest_scenario'>", + " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests'>", + " <Module 'test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + " <Module 'test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests'>", + " <Module 'test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + " <Module 'test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.38 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.Windows, + description: + 'Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_more_multiply.py::test_times_100', + 'tests/further_tests/test_more_multiply.py::test_times_negative_1', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + '<Package e:\\\\user\\\\test\\\\pytest_scenario>', + ' <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests>', + ' <Module test_base_stuff.py>', + ' <Function test_do_test>', + ' <Function test_do_other_test>', + ' <Module test_gimme_5.py>', + ' <Function test_gimme_5>', + ' <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests>', + ' <Module test_more_multiply.py>', + ' <Function test_times_100>', + ' <Function test_times_negative_1>', + ' <Module test_multiply.py>', + ' <Function test_times_10>', + ' <Function test_times_2>', + '', + '======================== no tests ran in 0.38 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_more_multiply.py::test_times_100', + 'tests/further_tests/test_more_multiply.py::test_times_negative_1', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Module 'tests/test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'tests/test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/further_tests/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + "<Module 'tests/further_tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.17 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_more_multiply.py::test_times_100', + 'tests/further_tests/test_more_multiply.py::test_times_negative_1', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Module 'tests/test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'tests/test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/further_tests/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + "<Module 'tests/further_tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.20 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/further_tests/test_more_multiply.py::test_times_100', + 'tests/further_tests/test_more_multiply.py::test_times_negative_1', + 'tests/further_tests/test_multiply.py::test_times_10', + 'tests/further_tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + '<Module tests/test_base_stuff.py>', + ' <Function test_do_test>', + ' <Function test_do_other_test>', + '<Module tests/test_gimme_5.py>', + ' <Function test_gimme_5>', + '<Module tests/further_tests/test_more_multiply.py>', + ' <Function test_times_100>', + ' <Function test_times_negative_1>', + '<Module tests/further_tests/test_multiply.py>', + ' <Function test_times_10>', + ' <Function test_times_2>', + '', + '======================== no tests ran in 0.20 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.Windows, + description: 'Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Module 'test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + "<Module 'tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.26 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Package 'e:\\\\user\\\\test\\\\pytest_scenario'>", + " <Module 'test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + " <Module 'test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests'>", + " <Module 'test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + " <Module 'test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.66 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + '<Package e:\\\\user\\\\test\\\\pytest_scenario>', + ' <Module test_base_stuff.py>', + ' <Function test_do_test>', + ' <Function test_do_other_test>', + ' <Module test_gimme_5.py>', + ' <Function test_gimme_5>', + ' <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests>', + ' <Module test_more_multiply.py>', + ' <Function test_times_100>', + ' <Function test_times_negative_1>', + ' <Module test_multiply.py>', + ' <Function test_times_10>', + ' <Function test_times_2>', + '', + '======================== no tests ran in 0.66 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2+ test modules in root folder and two more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Module 'test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + "<Module 'tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.11 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2+ test modules in root folder and two more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Module 'test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + "<Module 'tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.41 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2+ test modules in root folder and two more in one (direct) subfolder.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + '<Module test_base_stuff.py>', + ' <Function test_do_test>', + ' <Function test_do_other_test>', + '<Module test_gimme_5.py>', + ' <Function test_gimme_5>', + '<Module tests/test_more_multiply.py>', + ' <Function test_times_100>', + ' <Function test_times_negative_1>', + '<Module tests/test_multiply.py>', + ' <Function test_times_10>', + ' <Function test_times_2>', + '', + '======================== no tests ran in 0.41 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.Windows, + description: 'Package-based source, 2+ test modules in a subfolder off the root.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Module 'tests/test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'tests/test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + "<Module 'tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.20 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Package-based source, 2+ test modules in a subfolder off the root.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Package 'e:\\\\user\\\\test\\\\pytest_scenario'>", + " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests'>", + " <Module 'test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + " <Module 'test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + " <Module 'test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + " <Module 'test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.26 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Package-based source, 2+ test modules in a subfolder off the root.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + '<Package e:\\\\user\\\\test\\\\pytest_scenario>', + ' <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests>', + ' <Module test_base_stuff.py>', + ' <Function test_do_test>', + ' <Function test_do_other_test>', + ' <Module test_gimme_5.py>', + ' <Function test_gimme_5>', + ' <Module test_more_multiply.py>', + ' <Function test_times_100>', + ' <Function test_times_negative_1>', + ' <Module test_multiply.py>', + ' <Function test_times_10>', + ' <Function test_times_2>', + '', + '======================== no tests ran in 0.26 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2+ test modules in a subfolder off the root.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Module 'tests/test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'tests/test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + "<Module 'tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.26 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2+ test modules in a subfolder off the root.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + "<Module 'tests/test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'tests/test_gimme_5.py'>", + " <Function 'test_gimme_5'>", + "<Module 'tests/test_more_multiply.py'>", + " <Function 'test_times_100'>", + " <Function 'test_times_negative_1'>", + "<Module 'tests/test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.26 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2+ test modules in a subfolder off the root.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'tests/test_base_stuff.py::test_do_test', + 'tests/test_base_stuff.py::test_do_other_test', + 'tests/test_gimme_5.py::test_gimme_5', + 'tests/test_more_multiply.py::test_times_100', + 'tests/test_more_multiply.py::test_times_negative_1', + 'tests/test_multiply.py::test_times_10', + 'tests/test_multiply.py::test_times_2' + ], + functionCount: 7, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 7 items', + '<Module tests/test_base_stuff.py>', + ' <Function test_do_test>', + ' <Function test_do_other_test>', + '<Module tests/test_gimme_5.py>', + ' <Function test_gimme_5>', + '<Module tests/test_more_multiply.py>', + ' <Function test_times_100>', + ' <Function test_times_negative_1>', + '<Module tests/test_multiply.py>', + ' <Function test_times_10>', + ' <Function test_times_2>', + '', + '======================== no tests ran in 0.26 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.Windows, + description: 'Package-based source, 2+ modules at the topmost level.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'test_multiply.py::test_times_10', + 'test_multiply.py::test_times_2' + ], + functionCount: 4, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 4 items', + "<Module 'test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.17 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Package-based source, 2+ modules at the topmost level.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'test_multiply.py::test_times_10', + 'test_multiply.py::test_times_2' + ], + functionCount: 4, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 4 items', + "<Package 'e:\\\\user\\\\test\\\\pytest_scenario'>", + " <Module 'test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + " <Module 'test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.37 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Package-based source, 2+ modules at the topmost level.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'test_multiply.py::test_times_10', + 'test_multiply.py::test_times_2' + ], + functionCount: 4, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 4 items', + '<Package e:\\\\user\\\\test\\\\pytest_scenario>', + ' <Module test_base_stuff.py>', + ' <Function test_do_test>', + ' <Function test_do_other_test>', + ' <Module test_multiply.py>', + ' <Function test_times_10>', + ' <Function test_times_2>', + '', + '======================== no tests ran in 0.37 seconds =========================' + ] + }, + { + pytest_version_spec: '< 3.7', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2 modules at the topmost level.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'test_multiply.py::test_times_10', + 'test_multiply.py::test_times_2' + ], + functionCount: 4, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 4 items', + "<Module 'test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.18 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 3.7 < 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2 modules at the topmost level.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'test_multiply.py::test_times_10', + 'test_multiply.py::test_times_2' + ], + functionCount: 4, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 4 items', + "<Module 'test_base_stuff.py'>", + " <Function 'test_do_test'>", + " <Function 'test_do_other_test'>", + "<Module 'test_multiply.py'>", + " <Function 'test_times_10'>", + " <Function 'test_times_2'>", + '', + '======================== no tests ran in 0.36 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.Windows, + description: 'Non-package source, 2 modules at the topmost level.', + rootdir: 'e:\\user\\test\\pytest_scenario', + test_functions: [ + 'test_base_stuff.py::test_do_test', + 'test_base_stuff.py::test_do_other_test', + 'test_multiply.py::test_times_10', + 'test_multiply.py::test_times_2' + ], + functionCount: 4, + stdout: [ + '============================= test session starts =============================', + 'platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1', + 'rootdir: e:\\user\\test\\pytest_scenario, inifile:', + 'collected 4 items', + '<Module test_base_stuff.py>', + ' <Function test_do_test>', + ' <Function test_do_other_test>', + '<Module test_multiply.py>', + ' <Function test_times_10>', + ' <Function test_times_2>', + '', + '======================== no tests ran in 0.36 seconds =========================' + ] + }, + { + pytest_version_spec: '>= 4.1', + platform: PytestDataPlatformType.NonWindows, + description: 'Parameterized tests', + rootdir: '/home/user/test/pytest_scenario', + test_functions: ['tests/test_spam.py::test_with_subtests[1-2]', 'tests/test_spam.py::test_with_subtests[3-4]'], + functionCount: 2, + stdout: [ + '============================= test session starts ==============================', + 'platform linux -- Python 3.7.1, pytest-4.2.1, py-1.7.0, pluggy-0.8.1', + 'rootdir: /home/user/test/pytest_scenario, inifile:', + 'collected 2 items', + '<Package /home/user/test/pytest_scenario/tests>', + ' <Module test_spam.py>', + ' <Function test_with_subtests[1-2]>', + ' <Function test_with_subtests[3-4]>', + '', + '========================= no tests ran in 0.02 seconds =========================' + ] + } +]; diff --git a/src/test/testing/pytest/services/discoveryService.unit.test.ts b/src/test/testing/pytest/services/discoveryService.unit.test.ts new file mode 100644 index 000000000000..ea46e70c4bb4 --- /dev/null +++ b/src/test/testing/pytest/services/discoveryService.unit.test.ts @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect } from 'chai'; +import { deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { CancellationTokenSource, Uri } from 'vscode'; +import { ServiceContainer } from '../../../../client/ioc/container'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { PYTEST_PROVIDER } from '../../../../client/testing/common/constants'; +import { TestsDiscoveryService } from '../../../../client/testing/common/services/discovery'; +import { TestsHelper } from '../../../../client/testing/common/testUtils'; +import { + ITestDiscoveryService, + ITestsHelper, + TestDiscoveryOptions, + Tests +} from '../../../../client/testing/common/types'; +import { ArgumentsService } from '../../../../client/testing/pytest/services/argsService'; +import { TestDiscoveryService } from '../../../../client/testing/pytest/services/discoveryService'; +import { IArgumentsService, TestFilter } from '../../../../client/testing/types'; +import { MockOutputChannel } from '../../../mockClasses'; + +// tslint:disable: no-unnecessary-override no-any +suite('Unit Tests - PyTest - Discovery', () => { + class DiscoveryService extends TestDiscoveryService { + public buildTestCollectionArgs(options: TestDiscoveryOptions): string[] { + return super.buildTestCollectionArgs(options); + } + public discoverTestsInTestDirectory(options: TestDiscoveryOptions): Promise<Tests> { + return super.discoverTestsInTestDirectory(options); + } + } + let discoveryService: DiscoveryService; + let serviceContainer: IServiceContainer; + let argsService: IArgumentsService; + let helper: ITestsHelper; + setup(() => { + serviceContainer = mock(ServiceContainer); + helper = mock(TestsHelper); + argsService = mock(ArgumentsService); + + when(serviceContainer.get<IArgumentsService>(IArgumentsService, PYTEST_PROVIDER)).thenReturn( + instance(argsService) + ); + when(serviceContainer.get<ITestsHelper>(ITestsHelper)).thenReturn(instance(helper)); + discoveryService = new DiscoveryService(instance(serviceContainer)); + }); + test('Ensure discovery is invoked when there are no test directories', async () => { + const options: TestDiscoveryOptions = { + args: ['some args'], + cwd: Uri.file(__dirname).fsPath, + ignoreCache: true, + outChannel: new MockOutputChannel('Tests'), + token: new CancellationTokenSource().token, + workspaceFolder: Uri.file(__dirname) + }; + const args = ['1', '2', '3']; + const discoveredTests = ('Hello World' as any) as Tests; + discoveryService.buildTestCollectionArgs = () => args; + discoveryService.discoverTestsInTestDirectory = () => Promise.resolve(discoveredTests); + when(argsService.getTestFolders(deepEqual(options.args))).thenReturn([]); + + const tests = await discoveryService.discoverTests(options); + + expect(tests).equal(discoveredTests); + }); + test('Ensure discovery is invoked when there are multiple test directories', async () => { + const options: TestDiscoveryOptions = { + args: ['some args'], + cwd: Uri.file(__dirname).fsPath, + ignoreCache: true, + outChannel: new MockOutputChannel('Tests'), + token: new CancellationTokenSource().token, + workspaceFolder: Uri.file(__dirname) + }; + const args = ['1', '2', '3']; + discoveryService.buildTestCollectionArgs = () => args; + const directories = ['a', 'b']; + discoveryService.discoverTestsInTestDirectory = async (opts) => { + const dir = opts.args[opts.args.length - 1]; + if (dir === 'a') { + return ('Result A' as any) as Tests; + } + if (dir === 'b') { + return ('Result B' as any) as Tests; + } + throw new Error('Unrecognized directory'); + }; + when(argsService.getTestFolders(deepEqual(options.args))).thenReturn(directories); + when(helper.mergeTests(deepEqual([('Result A' as any) as Tests, ('Result B' as any) as Tests]))).thenReturn( + 'mergedTests' as any + ); + + const tests = await discoveryService.discoverTests(options); + + verify(helper.mergeTests(deepEqual([('Result A' as any) as Tests, ('Result B' as any) as Tests]))).once(); + expect(tests).equal('mergedTests'); + }); + test('Build collection arguments', async () => { + const options: TestDiscoveryOptions = { + args: ['some args', 'and some more'], + cwd: Uri.file(__dirname).fsPath, + ignoreCache: false, + outChannel: new MockOutputChannel('Tests'), + token: new CancellationTokenSource().token, + workspaceFolder: Uri.file(__dirname) + }; + + const filteredArgs = options.args; + const expectedArgs = ['--rootdir', Uri.file(__dirname).fsPath, '-s', ...filteredArgs]; + when(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).thenReturn(filteredArgs); + + const args = discoveryService.buildTestCollectionArgs(options); + + expect(args).deep.equal(expectedArgs); + verify(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).once(); + }); + test('Build collection arguments with ignore in args', async () => { + const options: TestDiscoveryOptions = { + args: ['some args', 'and some more', '--cache-clear'], + cwd: Uri.file(__dirname).fsPath, + ignoreCache: true, + outChannel: new MockOutputChannel('Tests'), + token: new CancellationTokenSource().token, + workspaceFolder: Uri.file(__dirname) + }; + + const filteredArgs = options.args; + const expectedArgs = ['--rootdir', Uri.file(__dirname).fsPath, '-s', ...filteredArgs]; + when(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).thenReturn(filteredArgs); + + const args = discoveryService.buildTestCollectionArgs(options); + + expect(args).deep.equal(expectedArgs); + verify(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).once(); + }); + test('Build collection arguments (& ignore)', async () => { + const options: TestDiscoveryOptions = { + args: ['some args', 'and some more'], + cwd: Uri.file(__dirname).fsPath, + ignoreCache: true, + outChannel: new MockOutputChannel('Tests'), + token: new CancellationTokenSource().token, + workspaceFolder: Uri.file(__dirname) + }; + + const filteredArgs = options.args; + const expectedArgs = ['--rootdir', Uri.file(__dirname).fsPath, '-s', '--cache-clear', ...filteredArgs]; + when(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).thenReturn(filteredArgs); + + const args = discoveryService.buildTestCollectionArgs(options); + + expect(args).deep.equal(expectedArgs); + verify(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).once(); + }); + test('Discover using common discovery', async () => { + const options: TestDiscoveryOptions = { + args: ['some args', 'and some more'], + cwd: Uri.file(__dirname).fsPath, + ignoreCache: true, + outChannel: new MockOutputChannel('Tests'), + token: new CancellationTokenSource().token, + workspaceFolder: Uri.file(__dirname) + }; + const expectedDiscoveryArgs = ['discover', 'pytest', '--', ...options.args]; + const discoveryOptions = { ...options }; + discoveryOptions.args = expectedDiscoveryArgs; + + const commonDiscoveryService = mock(TestsDiscoveryService); + const discoveredTests = ('Hello' as any) as Tests; + when(serviceContainer.get<ITestDiscoveryService>(ITestDiscoveryService, 'common')).thenReturn( + instance(commonDiscoveryService) + ); + when(commonDiscoveryService.discoverTests(deepEqual(discoveryOptions))).thenResolve(discoveredTests); + + const tests = await discoveryService.discoverTestsInTestDirectory(options); + + verify(commonDiscoveryService.discoverTests(deepEqual(discoveryOptions))).once(); + expect(tests).equal(discoveredTests); + }); +}); diff --git a/src/test/testing/rediscover.test.ts b/src/test/testing/rediscover.test.ts new file mode 100644 index 000000000000..fe6a82e43b63 --- /dev/null +++ b/src/test/testing/rediscover.test.ts @@ -0,0 +1,95 @@ +import { assert } from 'chai'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import { ConfigurationTarget } from 'vscode'; +import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; +import { InterpreterService } from '../../client/interpreter/interpreterService'; +import { CondaService } from '../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { CommandSource } from '../../client/testing/common/constants'; +import { ITestManagerFactory, TestProvider } from '../../client/testing/common/types'; +import { deleteDirectory, deleteFile, rootWorkspaceUri, updateSetting } from '../common'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_TIMEOUT } from './../initialize'; +import { UnitTestIocContainer } from './serviceRegistry'; + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); +const testFile = path.join(testFilesPath, 'tests', 'test_debugger_two.py'); +const testFileWithFewTests = path.join(testFilesPath, 'tests', 'test_debugger_two.txt'); +const testFileWithMoreTests = path.join(testFilesPath, 'tests', 'test_debugger_two.updated.txt'); +const defaultUnitTestArgs = ['-v', '-s', '.', '-p', '*test*.py']; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests re-discovery', () => { + let ioc: UnitTestIocContainer; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + suiteSetup(async () => { + await initialize(); + }); + setup(async function () { + // tslint:disable-next-line:no-invalid-this + this.timeout(TEST_TIMEOUT * 2); // This hook requires more timeout as we're dealing with files as well + await fs.copy(testFileWithFewTests, testFile, { overwrite: true }); + await deleteDirectory(path.join(testFilesPath, '.cache')); + await resetSettings(); + await initializeTest(); + initializeDI(); + }); + teardown(async function () { + // This is doing a lot more than what a teardown does normally, so increasing the timeout. + // tslint:disable-next-line: no-invalid-this + this.timeout(TEST_TIMEOUT * 2); + await ioc.dispose(); + await resetSettings(); + await fs.copy(testFileWithFewTests, testFile, { overwrite: true }); + await deleteFile(path.join(path.dirname(testFile), `${path.basename(testFile, '.py')}.pyc`)); + }); + + async function resetSettings() { + await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); + await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); + } + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerProcessTypes(); + ioc.registerVariableTypes(); + ioc.registerUnitTestTypes(); + ioc.registerInterpreterStorageTypes(); + ioc.registerMockInterpreterTypes(); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, instance(mock(CondaService))); + ioc.serviceManager.rebindInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); + } + + async function discoverUnitTests(testProvider: TestProvider) { + const testManager = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory)( + testProvider, + rootWorkspaceUri!, + testFilesPath + ); + let tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + await deleteFile(path.join(path.dirname(testFile), `${path.basename(testFile, '.py')}.pyc`)); + await fs.copy(testFileWithMoreTests, testFile, { overwrite: true }); + tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFunctions.length, 4, 'Incorrect number of updated test functions'); + } + + test('Re-discover tests (unittest)', async () => { + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + await discoverUnitTests('unittest'); + }); + + test('Re-discover tests (pytest)', async () => { + await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); + await discoverUnitTests('pytest'); + }); + + test('Re-discover tests (nosetest)', async () => { + await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); + await discoverUnitTests('nosetest'); + }); +}); diff --git a/src/test/testing/results.ts b/src/test/testing/results.ts new file mode 100644 index 000000000000..824ef1c1fa4a --- /dev/null +++ b/src/test/testing/results.ts @@ -0,0 +1,560 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { Uri } from 'vscode'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + SubtestParent, + TestFile, + TestFolder, + TestFunction, + TestingType, + TestProvider, + TestResult, + Tests, + TestStatus, + TestSuite, + TestSummary +} from '../../client/testing/common/types'; +import { fixPath, getDedentedLines, getIndent, RESOURCE } from './helper'; + +type SuperTest = TestFunction & { + subtests: TestFunction[]; +}; + +export type TestItem = TestFolder | TestFile | TestSuite | SuperTest | TestFunction; + +export type TestNode = TestItem & { + testType: TestingType; +}; + +// Return an initialized test results. +export function createEmptyResults(): Tests { + return { + summary: { + passed: 0, + failures: 0, + errors: 0, + skipped: 0 + }, + testFiles: [], + testFunctions: [], + testSuites: [], + testFolders: [], + rootTestFolders: [] + }; +} + +// Increment the appropriate summary property. +export function updateSummary(summary: TestSummary, status: TestStatus) { + switch (status) { + case TestStatus.Pass: + summary.passed += 1; + break; + case TestStatus.Fail: + summary.failures += 1; + break; + case TestStatus.Error: + summary.errors += 1; + break; + case TestStatus.Skipped: + summary.skipped += 1; + break; + default: + // Do not update the results. + } +} + +// Return the file found walking up the parents, if any. +// +// There should only be one parent file. +export function findParentFile(parents: TestNode[]): TestFile | undefined { + // Iterate in reverse order. + for (let i = parents.length; i > 0; i -= 1) { + const parent = parents[i - 1]; + if (parent.testType === TestingType.file) { + return parent as TestFile; + } + } + return; +} + +// Return the first suite found walking up the parents, if any. +export function findParentSuite(parents: TestNode[]): TestSuite | undefined { + // Iterate in reverse order. + for (let i = parents.length; i > 0; i -= 1) { + const parent = parents[i - 1]; + if (parent.testType === TestingType.suite) { + return parent as TestSuite; + } + } + return; +} + +// Return the "flattened" test suite node. +export function flattenSuite(node: TestSuite, parents: TestNode[]): FlattenedTestSuite { + const found = findParentFile(parents); + if (!found) { + throw Error('parent file not found'); + } + const parentFile: TestFile = found; + return { + testSuite: node, + parentTestFile: parentFile, + xmlClassName: node.xmlName + }; +} + +// Return the "flattened" test function node. +export function flattenFunction(node: TestFunction, parents: TestNode[]): FlattenedTestFunction { + const found = findParentFile(parents); + if (!found) { + throw Error('parent file not found'); + } + const parentFile: TestFile = found; + const parentSuite = findParentSuite(parents); + return { + testFunction: node, + parentTestFile: parentFile, + parentTestSuite: parentSuite, + xmlClassName: parentSuite ? parentSuite.xmlName : '' + }; +} + +// operations on raw test nodes +export namespace nodes { + // Set the result-oriented properties back to their "unset" values. + export function resetResult(node: TestNode) { + node.time = 0; + node.status = TestStatus.Unknown; + } + + //******************************** + // builders for empty low-level test results + + export function createFolderResults(dirname: string, nameToRun?: string, resource: Uri = RESOURCE): TestNode { + dirname = fixPath(dirname); + return { + resource: resource, + name: dirname, + nameToRun: nameToRun || dirname, + folders: [], + testFiles: [], + testType: TestingType.folder, + // result + time: 0, + status: TestStatus.Unknown + }; + } + + export function createFileResults( + filename: string, + nameToRun?: string, + xmlName?: string, + resource: Uri = RESOURCE + ): TestNode { + filename = fixPath(filename); + if (!xmlName) { + xmlName = filename + .replace(/\.[^.]+$/, '') + .replace(/[\\\/]/, '.') + .replace(/^[.\\\/]*/, ''); + } + return { + resource: resource, + fullPath: filename, + name: path.basename(filename), + nameToRun: nameToRun || filename, + xmlName: xmlName!, + suites: [], + functions: [], + testType: TestingType.file, + // result + time: 0, + status: TestStatus.Unknown + }; + } + + export function createSuiteResults( + name: string, + nameToRun?: string, + xmlName?: string, + provider: TestProvider = 'pytest', + isInstance: boolean = false, + resource: Uri = RESOURCE + ): TestNode { + return { + resource: resource, + name: name, + nameToRun: nameToRun || '', // must be set for parent + xmlName: xmlName || '', // must be set for parent + isUnitTest: provider === 'unittest', + isInstance: isInstance, + suites: [], + functions: [], + testType: TestingType.suite, + // result + time: 0, + status: TestStatus.Unknown + }; + } + + export function createTestResults( + name: string, + nameToRun?: string, + subtestParent?: SubtestParent, + resource: Uri = RESOURCE + ): TestNode { + return { + resource: resource, + name: name, + nameToRun: nameToRun || name, + subtestParent: subtestParent, + testType: TestingType.function, + // result + time: 0, + status: TestStatus.Unknown + }; + } + + //******************************** + // adding children to low-level nodes + + export function addDiscoveredSubFolder( + parent: TestFolder, + basename: string, + nameToRun?: string, + resource?: Uri + ): TestNode { + const dirname = path.join(parent.name, fixPath(basename)); + const subFolder = createFolderResults(dirname, nameToRun, resource || parent.resource || RESOURCE); + parent.folders.push(subFolder as TestFolder); + return subFolder; + } + + export function addDiscoveredFile( + parent: TestFolder, + basename: string, + nameToRun?: string, + xmlName?: string, + resource?: Uri + ): TestNode { + const filename = path.join(parent.name, fixPath(basename)); + const file = createFileResults(filename, nameToRun, xmlName, resource || parent.resource || RESOURCE); + parent.testFiles.push(file as TestFile); + return file; + } + + export function addDiscoveredSuite( + parent: TestFile | TestSuite, + name: string, + nameToRun?: string, + xmlName?: string, + provider: TestProvider = 'pytest', + isInstance?: boolean, + resource?: Uri + ): TestNode { + if (!nameToRun) { + const sep = provider === 'pytest' ? '::' : '.'; + nameToRun = `${parent.nameToRun}${sep}${name}`; + } + const suite = createSuiteResults( + name, + nameToRun!, + xmlName || `${parent.xmlName}.${name}`, + provider, + isInstance, + resource || parent.resource || RESOURCE + ); + parent.suites.push(suite as TestSuite); + return suite; + } + + export function addDiscoveredTest( + parent: TestFile | TestSuite, + name: string, + nameToRun?: string, + provider: TestProvider = 'pytest', + resource?: Uri + ): TestNode { + if (!nameToRun) { + const sep = provider === 'pytest' ? '::' : '.'; + nameToRun = `${parent.nameToRun}${sep}${name}`; + } + const test = createTestResults(name, nameToRun, undefined, resource || parent.resource || RESOURCE); + parent.functions.push(test as TestFunction); + return test; + } + + export function addDiscoveredSubtest( + parent: SuperTest, + name: string, + nameToRun?: string, + provider: TestProvider = 'pytest', + resource?: Uri + ): TestNode { + const subtest = createTestResults( + name, + nameToRun!, + { + name: parent.name, + nameToRun: parent.nameToRun, + asSuite: createSuiteResults( + parent.name, + parent.nameToRun, + '', + provider, + false, + parent.resource + ) as TestSuite, + time: 0 + }, + resource || parent.resource || RESOURCE + ); + (subtest as TestFunction).subtestParent!.asSuite.functions.push(subtest); + parent.subtests.push(subtest as TestFunction); + return subtest; + } +} + +namespace declarative { + type TestParent = TestNode & { + indent: string; + }; + + type ParsedTestNode = { + indent: string; + name: string; + testType: TestingType; + result: TestResult; + }; + + // Return a test tree built from concise declarative text. + export function parseResults(text: string, tests: Tests, provider: TestProvider, resource: Uri) { + // Build the tree (and populate the return value at the same time). + const parents: TestParent[] = []; + let prev: TestParent; + for (const line of getDedentedLines(text)) { + if (line.trim() === '') { + continue; + } + const parsed = parseTestLine(line); + + let node: TestNode; + if (isRootNode(parsed)) { + parents.length = 0; // Clear the array. + node = nodes.createFolderResults(parsed.name, undefined, resource); + tests.rootTestFolders.push(node as TestFolder); + tests.testFolders.push(node as TestFolder); + } else { + const parent = setMatchingParent(parents, prev!, parsed.indent); + node = buildDiscoveredChildNode(parent, parsed.name, parsed.testType, provider, resource); + switch (parsed.testType) { + case TestingType.folder: + tests.testFolders.push(node as TestFolder); + break; + case TestingType.file: + tests.testFiles.push(node as TestFile); + break; + case TestingType.suite: + tests.testSuites.push(flattenSuite(node as TestSuite, parents)); + break; + case TestingType.function: + // This does not deal with subtests? + tests.testFunctions.push(flattenFunction(node as TestFunction, parents)); + break; + default: + } + } + + // Set the result. + node.status = parsed.result.status; + node.time = parsed.result.time; + updateSummary(tests.summary, node.status!); + + // Prepare for the next line. + prev = node as TestParent; + prev.indent = parsed.indent; + } + } + + // Determine the kind, indent, and result info based on the line. + function parseTestLine(line: string): ParsedTestNode { + if (line.includes('\\')) { + throw Error('expected / as path separator (even on Windows)'); + } + + const indent = getIndent(line); + line = line.trim(); + + const parts = line.split(' '); + let name = parts.shift(); + if (!name) { + throw Error('missing name'); + } + + // Determine the type from the name. + let testType: TestingType; + if (name.endsWith('/')) { + // folder + testType = TestingType.folder; + while (name.endsWith('/')) { + name = name.slice(0, -1); + } + } else if (name.includes('.')) { + // file + if (name.includes('/')) { + throw Error('filename must not include directories'); + } + testType = TestingType.file; + } else if (name.startsWith('<')) { + // suite + if (!name.endsWith('>')) { + throw Error('suite missing closing bracket'); + } + testType = TestingType.suite; + name = name.slice(1, -1); + } else { + // test + testType = TestingType.function; + } + + // Parse the results. + const result: TestResult = { + time: 0 + }; + if (parts.length !== 0 && testType !== TestingType.function) { + throw Error('non-test nodes do not have results'); + } + switch (parts.length) { + case 0: + break; + case 1: + // tslint:disable-next-line:no-any + if (isNaN(parts[0] as any)) { + throw Error(`expected a time (float), got ${parts[0]}`); + } + result.time = parseFloat(parts[0]); + break; + case 2: + switch (parts[0]) { + case 'P': + result.status = TestStatus.Pass; + break; + case 'F': + result.status = TestStatus.Fail; + break; + case 'E': + result.status = TestStatus.Error; + break; + case 'S': + result.status = TestStatus.Skipped; + break; + default: + throw Error('expected a status and then a time'); + } + // tslint:disable-next-line:no-any + if (isNaN(parts[1] as any)) { + throw Error(`expected a time (float), got ${parts[1]}`); + } + result.time = parseFloat(parts[1]); + break; + default: + throw Error('too many items on line'); + } + + return { + indent: indent, + name: name, + testType: testType, + result: result + }; + } + + function isRootNode(parsed: ParsedTestNode): boolean { + if (parsed.indent === '') { + if (parsed.testType !== TestingType.folder) { + throw Error('a top-level node must be a folder'); + } + return true; + } + return false; + } + + function setMatchingParent(parents: TestParent[], prev: TestParent, parsedIndent: string): TestParent { + let current = parents.length > 0 ? parents[parents.length - 1] : prev; + if (parsedIndent.length > current.indent.length) { + parents.push(prev); + current = prev; + } else { + while (parsedIndent !== current.indent) { + if (parsedIndent.length > current.indent.length) { + throw Error('mis-aligned indentation'); + } + + parents.pop(); + if (parents.length === 0) { + throw Error('mis-aligned indentation'); + } + current = parents[parents.length - 1]; + } + } + return current; + } + + function buildDiscoveredChildNode( + parent: TestParent, + name: string, + testType: TestingType, + provider: TestProvider, + resource?: Uri + ): TestNode { + switch (testType) { + case TestingType.folder: + if (parent.testType !== TestingType.folder) { + throw Error('parent must be a folder'); + } + return nodes.addDiscoveredSubFolder(parent as TestFolder, name, undefined, resource); + case TestingType.file: + if (parent.testType !== TestingType.folder) { + throw Error('parent must be a folder'); + } + return nodes.addDiscoveredFile(parent as TestFolder, name, undefined, undefined, resource); + case TestingType.suite: + let suiteParent: TestFile | TestSuite; + if (parent.testType === TestingType.file) { + suiteParent = parent as TestFile; + } else if (parent.testType === TestingType.suite) { + suiteParent = parent as TestSuite; + } else { + throw Error('parent must be a file or suite'); + } + return nodes.addDiscoveredSuite(suiteParent, name, undefined, undefined, provider, undefined, resource); + case TestingType.function: + let funcParent: TestFile | TestSuite; + if (parent.testType === TestingType.file) { + funcParent = parent as TestFile; + } else if (parent.testType === TestingType.suite) { + funcParent = parent as TestSuite; + } else if (parent.testType === TestingType.function) { + throw Error('not finished: use addDiscoveredSubTest()'); + } else { + throw Error('parent must be a file, suite, or function'); + } + return nodes.addDiscoveredTest(funcParent, name, undefined, provider, resource); + default: + throw Error('unsupported'); + } + } +} + +// Return a test tree built from concise declarative text. +export function createDeclaratively(text: string, provider: TestProvider = 'pytest', resource: Uri = RESOURCE): Tests { + const tests = createEmptyResults(); + declarative.parseResults(text, tests, provider, resource); + return tests; +} diff --git a/src/test/testing/serviceRegistry.ts b/src/test/testing/serviceRegistry.ts new file mode 100644 index 000000000000..0a9df7793822 --- /dev/null +++ b/src/test/testing/serviceRegistry.ts @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { Uri } from 'vscode'; + +import { IProcessServiceFactory } from '../../client/common/process/types'; +import { CodeCssGenerator } from '../../client/datascience/codeCssGenerator'; +import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; +import { InteractiveWindowProvider } from '../../client/datascience/interactive-window/interactiveWindowProvider'; +import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; +import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter'; +import { JupyterServerWrapper } from '../../client/datascience/jupyter/jupyterServerWrapper'; +import { + ICodeCssGenerator, + IInteractiveWindow, + IInteractiveWindowProvider, + IJupyterExecution, + INotebookImporter, + INotebookServer +} from '../../client/datascience/types'; +import { InterpreterEvaluation } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterEvaluation'; +import { InterpreterSecurityService } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityService'; +import { InterpreterSecurityStorage } from '../../client/interpreter/autoSelection/interpreterSecurity/interpreterSecurityStorage'; +import { + IInterpreterEvaluation, + IInterpreterSecurityService, + IInterpreterSecurityStorage +} from '../../client/interpreter/autoSelection/types'; +import { IInterpreterHelper } from '../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; +import { IServiceContainer } from '../../client/ioc/types'; +import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../client/testing/common/constants'; +import { TestContextService } from '../../client/testing/common/services/contextService'; +import { TestDiscoveredTestParser } from '../../client/testing/common/services/discoveredTestParser'; +import { TestsDiscoveryService } from '../../client/testing/common/services/discovery'; +import { TestCollectionStorageService } from '../../client/testing/common/services/storageService'; +import { TestManagerService } from '../../client/testing/common/services/testManagerService'; +import { TestResultsService } from '../../client/testing/common/services/testResultsService'; +import { TestsStatusUpdaterService } from '../../client/testing/common/services/testsStatusService'; +import { ITestDiscoveredTestParser } from '../../client/testing/common/services/types'; +import { UnitTestDiagnosticService } from '../../client/testing/common/services/unitTestDiagnosticService'; +import { TestsHelper } from '../../client/testing/common/testUtils'; +import { TestFlatteningVisitor } from '../../client/testing/common/testVisitors/flatteningVisitor'; +import { TestResultResetVisitor } from '../../client/testing/common/testVisitors/resultResetVisitor'; +import { + ITestCollectionStorageService, + ITestContextService, + ITestDiscoveryService, + ITestManager, + ITestManagerFactory, + ITestManagerService, + ITestManagerServiceFactory, + ITestResultsService, + ITestsHelper, + ITestsParser, + ITestsStatusUpdaterService, + ITestVisitor, + IUnitTestSocketServer, + TestProvider +} from '../../client/testing/common/types'; +import { TestManager as NoseTestManager } from '../../client/testing/nosetest/main'; +import { TestDiscoveryService as NoseTestDiscoveryService } from '../../client/testing/nosetest/services/discoveryService'; +import { TestsParser as NoseTestTestsParser } from '../../client/testing/nosetest/services/parserService'; +import { TestManager as PyTestTestManager } from '../../client/testing/pytest/main'; +import { TestDiscoveryService as PytestTestDiscoveryService } from '../../client/testing/pytest/services/discoveryService'; +import { ITestDiagnosticService } from '../../client/testing/types'; +import { TestManager as UnitTestTestManager } from '../../client/testing/unittest/main'; +import { TestDiscoveryService as UnitTestTestDiscoveryService } from '../../client/testing/unittest/services/discoveryService'; +import { TestsParser as UnitTestTestsParser } from '../../client/testing/unittest/services/parserService'; +import { getPythonSemVer } from '../common'; +import { IocContainer } from '../serviceRegistry'; +import { MockUnitTestSocketServer } from './mocks'; + +export class UnitTestIocContainer extends IocContainer { + constructor() { + super(); + } + public async getPythonMajorVersion(resource: Uri): Promise<number> { + const procServiceFactory = this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory); + const procService = await procServiceFactory.create(resource); + const pythonVersion = await getPythonSemVer(procService); + if (pythonVersion) { + return pythonVersion.major; + } else { + return -1; // log warning already issued by underlying functions... + } + } + + public registerTestVisitors() { + this.serviceManager.add<ITestVisitor>(ITestVisitor, TestFlatteningVisitor, 'TestFlatteningVisitor'); + this.serviceManager.add<ITestVisitor>(ITestVisitor, TestResultResetVisitor, 'TestResultResetVisitor'); + this.serviceManager.addSingleton<ITestsStatusUpdaterService>( + ITestsStatusUpdaterService, + TestsStatusUpdaterService + ); + this.serviceManager.addSingleton<ITestContextService>(ITestContextService, TestContextService); + } + + public registerTestStorage() { + this.serviceManager.addSingleton<ITestCollectionStorageService>( + ITestCollectionStorageService, + TestCollectionStorageService + ); + } + + public registerTestsHelper() { + this.serviceManager.addSingleton<ITestsHelper>(ITestsHelper, TestsHelper); + } + + public registerTestResultsHelper() { + this.serviceManager.add<ITestResultsService>(ITestResultsService, TestResultsService); + } + + public registerTestParsers() { + this.serviceManager.add<ITestsParser>(ITestsParser, UnitTestTestsParser, UNITTEST_PROVIDER); + this.serviceManager.add<ITestsParser>(ITestsParser, NoseTestTestsParser, NOSETEST_PROVIDER); + } + + public registerTestDiscoveryServices() { + this.serviceManager.add<ITestDiscoveryService>( + ITestDiscoveryService, + UnitTestTestDiscoveryService, + UNITTEST_PROVIDER + ); + this.serviceManager.add<ITestDiscoveryService>( + ITestDiscoveryService, + PytestTestDiscoveryService, + PYTEST_PROVIDER + ); + this.serviceManager.add<ITestDiscoveryService>( + ITestDiscoveryService, + NoseTestDiscoveryService, + NOSETEST_PROVIDER + ); + this.serviceManager.add<ITestDiscoveryService>(ITestDiscoveryService, TestsDiscoveryService, 'common'); + this.serviceManager.add<ITestDiscoveredTestParser>(ITestDiscoveredTestParser, TestDiscoveredTestParser); + } + + public registerTestDiagnosticServices() { + this.serviceManager.addSingleton<ITestDiagnosticService>(ITestDiagnosticService, UnitTestDiagnosticService); + } + + public registerTestManagers() { + this.serviceManager.addFactory<ITestManager>(ITestManagerFactory, (context) => { + return (testProvider: TestProvider, workspaceFolder: Uri, rootDirectory: string) => { + const serviceContainer = context.container.get<IServiceContainer>(IServiceContainer); + + switch (testProvider) { + case NOSETEST_PROVIDER: { + return new NoseTestManager(workspaceFolder, rootDirectory, serviceContainer); + } + case PYTEST_PROVIDER: { + return new PyTestTestManager(workspaceFolder, rootDirectory, serviceContainer); + } + case UNITTEST_PROVIDER: { + return new UnitTestTestManager(workspaceFolder, rootDirectory, serviceContainer); + } + default: { + throw new Error(`Unrecognized test provider '${testProvider}'`); + } + } + }; + }); + } + + public registerInterpreterStorageTypes() { + this.serviceManager.add<IInterpreterSecurityStorage>(IInterpreterSecurityStorage, InterpreterSecurityStorage); + this.serviceManager.add<IInterpreterSecurityService>(IInterpreterSecurityService, InterpreterSecurityService); + this.serviceManager.add<IInterpreterEvaluation>(IInterpreterEvaluation, InterpreterEvaluation); + this.serviceManager.add<IInterpreterHelper>(IInterpreterHelper, InterpreterHelper); + } + + public registerTestManagerService() { + this.serviceManager.addFactory<ITestManagerService>(ITestManagerServiceFactory, (context) => { + return (workspaceFolder: Uri) => { + const serviceContainer = context.container.get<IServiceContainer>(IServiceContainer); + const testsHelper = context.container.get<ITestsHelper>(ITestsHelper); + return new TestManagerService(workspaceFolder, testsHelper, serviceContainer); + }; + }); + } + + public registerMockUnitTestSocketServer() { + this.serviceManager.addSingleton<IUnitTestSocketServer>(IUnitTestSocketServer, MockUnitTestSocketServer); + } + + public registerDataScienceTypes() { + this.serviceManager.addSingleton<IJupyterExecution>(IJupyterExecution, JupyterExecutionFactory); + this.serviceManager.addSingleton<IInteractiveWindowProvider>( + IInteractiveWindowProvider, + InteractiveWindowProvider + ); + this.serviceManager.add<IInteractiveWindow>(IInteractiveWindow, InteractiveWindow); + this.serviceManager.add<INotebookImporter>(INotebookImporter, JupyterImporter); + this.serviceManager.add<INotebookServer>(INotebookServer, JupyterServerWrapper); + this.serviceManager.addSingleton<ICodeCssGenerator>(ICodeCssGenerator, CodeCssGenerator); + } +} diff --git a/src/test/testing/stoppingDiscoverAndTest.test.ts b/src/test/testing/stoppingDiscoverAndTest.test.ts new file mode 100644 index 000000000000..c10bc5f1c69c --- /dev/null +++ b/src/test/testing/stoppingDiscoverAndTest.test.ts @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { Product } from '../../client/common/types'; +import { createDeferred } from '../../client/common/utils/async'; +import { CANCELLATION_REASON, CommandSource, UNITTEST_PROVIDER } from '../../client/testing/common/constants'; +import { ITestDiscoveryService } from '../../client/testing/common/types'; +import { initialize, initializeTest } from '../initialize'; +import { MockDiscoveryService, MockTestManagerWithRunningTests } from './mocks'; +import { UnitTestIocContainer } from './serviceRegistry'; + +use(chaiAsPromised); + +const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); +// tslint:disable-next-line:variable-name +const EmptyTests = { + summary: { + passed: 0, + failures: 0, + errors: 0, + skipped: 0 + }, + testFiles: [], + testFunctions: [], + testSuites: [], + testFolders: [], + rootTestFolders: [] +}; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests Stopping Discovery and Runner', () => { + let ioc: UnitTestIocContainer; + suiteSetup(initialize); + setup(async () => { + await initializeTest(); + initializeDI(); + }); + teardown(() => ioc.dispose()); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerProcessTypes(); + ioc.registerVariableTypes(); + + ioc.registerTestParsers(); + ioc.registerTestVisitors(); + ioc.registerTestResultsHelper(); + ioc.registerTestStorage(); + ioc.registerTestsHelper(); + ioc.registerTestDiagnosticServices(); + ioc.registerInterpreterStorageTypes(); + ioc.registerMockInterpreterTypes(); + } + + test('Running tests should not stop existing discovery', async () => { + const mockTestManager = new MockTestManagerWithRunningTests( + UNITTEST_PROVIDER, + Product.unittest, + Uri.file(testFilesPath), + testFilesPath, + ioc.serviceContainer + ); + ioc.serviceManager.addSingletonInstance<ITestDiscoveryService>( + ITestDiscoveryService, + new MockDiscoveryService(mockTestManager.discoveryDeferred.promise), + UNITTEST_PROVIDER + ); + + const discoveryPromise = mockTestManager.discoverTests(CommandSource.auto); + mockTestManager.discoveryDeferred.resolve(EmptyTests); + const runningPromise = mockTestManager.runTest(CommandSource.ui); + const deferred = createDeferred<string>(); + + // This promise should never resolve nor reject. + runningPromise + .then(() => Promise.reject("Debugger stopped when it shouldn't have")) + .catch((error) => deferred.reject(error)); + + discoveryPromise + .then((result) => { + if (result === EmptyTests) { + deferred.resolve(''); + } else { + deferred.reject('tests not empty'); + } + }) + .catch((error) => deferred.reject(error)); + + await deferred.promise; + }); + + test('Discovering tests should stop running tests', async () => { + const mockTestManager = new MockTestManagerWithRunningTests( + UNITTEST_PROVIDER, + Product.unittest, + Uri.file(testFilesPath), + testFilesPath, + ioc.serviceContainer + ); + ioc.serviceManager.addSingletonInstance<ITestDiscoveryService>( + ITestDiscoveryService, + new MockDiscoveryService(mockTestManager.discoveryDeferred.promise), + UNITTEST_PROVIDER + ); + mockTestManager.discoveryDeferred.resolve(EmptyTests); + await mockTestManager.discoverTests(CommandSource.auto); + const runPromise = mockTestManager.runTest(CommandSource.ui); + // tslint:disable-next-line:no-string-based-set-timeout + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // User manually discovering tests will kill the existing test runner. + await mockTestManager.discoverTests(CommandSource.ui, true, false, true); + await expect(runPromise).to.eventually.be.rejectedWith(CANCELLATION_REASON); + }); +}); diff --git a/src/test/testing/unittest/unittest.argsService.unit.test.ts b/src/test/testing/unittest/unittest.argsService.unit.test.ts new file mode 100644 index 000000000000..8c8ad61ae446 --- /dev/null +++ b/src/test/testing/unittest/unittest.argsService.unit.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import * as typeMoq from 'typemoq'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ArgumentsHelper } from '../../../client/testing/common/argumentsHelper'; +import { IArgumentsHelper } from '../../../client/testing/types'; +import { ArgumentsService as UnittestArgumentsService } from '../../../client/testing/unittest/services/argsService'; + +suite('ArgsService: unittest', () => { + let argumentsService: UnittestArgumentsService; + + suiteSetup(() => { + const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + + const argsHelper = new ArgumentsHelper(); + + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IArgumentsHelper), typeMoq.It.isAny())) + .returns(() => argsHelper); + + argumentsService = new UnittestArgumentsService(serviceContainer.object); + }); + + test('Test getting the test folder in unittest with -s', () => { + const dir = path.join('a', 'b', 'c'); + const args = ['anzy', '--one', '--three', '-s', dir]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in unittest with -s in the middle', () => { + const dir = path.join('a', 'b', 'c'); + const args = ['anzy', '--one', '--three', '-s', dir, 'some other', '--value', '1234']; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in unittest with --start-directory', () => { + const dir = path.join('a', 'b', 'c'); + const args = ['anzy', '--one', '--three', '--start-directory', dir]; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); + test('Test getting the test folder in unittest with --start-directory in the middle', () => { + const dir = path.join('a', 'b', 'c'); + const args = ['anzy', '--one', '--three', '--start-directory', dir, 'some other', '--value', '1234']; + const testDirs = argumentsService.getTestFolders(args); + expect(testDirs).to.be.lengthOf(1); + expect(testDirs[0]).to.equal(dir); + }); +}); diff --git a/src/test/testing/unittest/unittest.diagnosticService.unit.test.ts b/src/test/testing/unittest/unittest.diagnosticService.unit.test.ts new file mode 100644 index 000000000000..7904441997d1 --- /dev/null +++ b/src/test/testing/unittest/unittest.diagnosticService.unit.test.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { DiagnosticSeverity } from 'vscode'; +import * as localize from '../../../client/common/utils/localize'; +import { UnitTestDiagnosticService } from '../../../client/testing/common/services/unitTestDiagnosticService'; +import { TestStatus } from '../../../client/testing/common/types'; +import { PythonTestMessageSeverity } from '../../../client/testing/types'; + +suite('UnitTestDiagnosticService: unittest', () => { + let diagnosticService: UnitTestDiagnosticService; + + suiteSetup(() => { + diagnosticService = new UnitTestDiagnosticService(); + }); + suite('TestStatus: Error', () => { + let actualPrefix: string; + let actualSeverity: DiagnosticSeverity; + let expectedPrefix: string; + let expectedSeverity: DiagnosticSeverity; + suiteSetup(() => { + actualPrefix = diagnosticService.getMessagePrefix(TestStatus.Error)!; + actualSeverity = diagnosticService.getSeverity(PythonTestMessageSeverity.Error)!; + expectedPrefix = localize.Testing.testErrorDiagnosticMessage(); + expectedSeverity = DiagnosticSeverity.Error; + }); + test('Message Prefix', () => { + assert.equal(actualPrefix, expectedPrefix); + }); + test('Severity', () => { + assert.equal(actualSeverity, expectedSeverity); + }); + }); + suite('TestStatus: Fail', () => { + let actualPrefix: string; + let actualSeverity: DiagnosticSeverity; + let expectedPrefix: string; + let expectedSeverity: DiagnosticSeverity; + suiteSetup(() => { + actualPrefix = diagnosticService.getMessagePrefix(TestStatus.Fail)!; + actualSeverity = diagnosticService.getSeverity(PythonTestMessageSeverity.Failure)!; + expectedPrefix = localize.Testing.testFailDiagnosticMessage(); + expectedSeverity = DiagnosticSeverity.Error; + }); + test('Message Prefix', () => { + assert.equal(actualPrefix, expectedPrefix); + }); + test('Severity', () => { + assert.equal(actualSeverity, expectedSeverity); + }); + }); + suite('TestStatus: Skipped', () => { + let actualPrefix: string; + let actualSeverity: DiagnosticSeverity; + let expectedPrefix: string; + let expectedSeverity: DiagnosticSeverity; + suiteSetup(() => { + actualPrefix = diagnosticService.getMessagePrefix(TestStatus.Skipped)!; + actualSeverity = diagnosticService.getSeverity(PythonTestMessageSeverity.Skip)!; + expectedPrefix = localize.Testing.testSkippedDiagnosticMessage(); + expectedSeverity = DiagnosticSeverity.Information; + }); + test('Message Prefix', () => { + assert.equal(actualPrefix, expectedPrefix); + }); + test('Severity', () => { + assert.equal(actualSeverity, expectedSeverity); + }); + }); +}); diff --git a/src/test/testing/unittest/unittest.discovery.test.ts b/src/test/testing/unittest/unittest.discovery.test.ts new file mode 100644 index 000000000000..63f8fa998515 --- /dev/null +++ b/src/test/testing/unittest/unittest.discovery.test.ts @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import { ConfigurationTarget } from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { registerForIOC } from '../../../client/pythonEnvironments/legacyIOC'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { ITestManagerFactory } from '../../../client/testing/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { MockProcessService } from '../../mocks/proc'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; + +const testFilesPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles'); +const UNITTEST_TEST_FILES_PATH = path.join(testFilesPath, 'standard'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(testFilesPath, 'single'); +const unitTestTestFilesCwdPath = path.join(testFilesPath, 'cwd', 'src'); +const defaultUnitTestArgs = ['-v', '-s', '.', '-p', '*test*.py']; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests - unittest - discovery with mocked process output', () => { + let ioc: UnitTestIocContainer; + const rootDirectory = UNITTEST_TEST_FILES_PATH; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + + suiteSetup(async () => { + await initialize(); + await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + }); + setup(async () => { + const cachePath = path.join(UNITTEST_TEST_FILES_PATH, '.cache'); + if (await fs.pathExists(cachePath)) { + await fs.remove(cachePath); + } + await initializeTest(); + initializeDI(); + }); + teardown(async () => { + await ioc.dispose(); + await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerUnitTestTypes(); + + // Mocks. + ioc.registerMockProcessTypes(); + ioc.registerInterpreterStorageTypes(); + ioc.serviceManager.addSingletonInstance<IInterpreterService>( + IInterpreterService, + instance(mock(InterpreterService)) + ); + registerForIOC(ioc.serviceManager, ioc.serviceContainer); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, instance(mock(CondaService))); + } + + async function injectTestDiscoveryOutput(output: string) { + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + procService.onExecObservable((_file, args, _options, callback) => { + if ( + args.length > 1 && + args[0] === '-c' && + args[1].includes('import unittest') && + args[1].includes('loader = unittest.TestLoader()') + ) { + callback({ + // Ensure any spaces added during code formatting or the like are removed. + out: output + .split(/\r?\n/g) + .map((item) => item.trim()) + .join(EOL), + source: 'stdout' + }); + } + }); + } + + test('Discover Tests (single test file)', async () => { + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + // tslint:disable-next-line:no-multiline-string + await injectTestDiscoveryOutput(`start + test_one.Test_test1.test_A + test_one.Test_test1.test_B + test_one.Test_test1.test_c + `); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_one.py' && t.nameToRun === 'test_one'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFunctions.some( + (t) => t.testFunction.name === 'test_A' && t.testFunction.nameToRun === 'test_one.Test_test1.test_A' + ), + true, + 'Test File not found' + ); + }); + + test('Discover Tests', async () => { + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + // tslint:disable-next-line:no-multiline-string + await injectTestDiscoveryOutput(`start + test_unittest_one.Test_test1.test_A + test_unittest_one.Test_test1.test_B + test_unittest_one.Test_test1.test_c + test_unittest_two.Test_test2.test_A2 + test_unittest_two.Test_test2.test_B2 + test_unittest_two.Test_test2.test_C2 + test_unittest_two.Test_test2.test_D2 + test_unittest_two.Test_test2a.test_222A2 + test_unittest_two.Test_test2a.test_222B2 + `); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, rootDirectory); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 9, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 3, 'Incorrect number of test suites'); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_unittest_one.py' && t.nameToRun === 'test_unittest_one'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_unittest_two.py' && t.nameToRun === 'test_unittest_two'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFunctions.some( + (t) => + t.testFunction.name === 'test_A' && + t.testFunction.nameToRun === 'test_unittest_one.Test_test1.test_A' + ), + true, + 'Test File not found' + ); + assert.equal( + tests.testFunctions.some( + (t) => + t.testFunction.name === 'test_A2' && + t.testFunction.nameToRun === 'test_unittest_two.Test_test2.test_A2' + ), + true, + 'Test File not found' + ); + }); + + test('Discover Tests (pattern = *_test_*.py)', async () => { + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=*_test*.py'], rootWorkspaceUri, configTarget); + // tslint:disable-next-line:no-multiline-string + await injectTestDiscoveryOutput(`start + unittest_three_test.Test_test3.test_A + unittest_three_test.Test_test3.test_B + `); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, rootDirectory); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + assert.equal( + tests.testFiles.some((t) => t.name === 'unittest_three_test.py' && t.nameToRun === 'unittest_three_test'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFunctions.some( + (t) => + t.testFunction.name === 'test_A' && + t.testFunction.nameToRun === 'unittest_three_test.Test_test3.test_A' + ), + true, + 'Test File not found' + ); + }); + + test('Setting cwd should return tests', async () => { + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); + // tslint:disable-next-line:no-multiline-string + await injectTestDiscoveryOutput(`start + test_cwd.Test_Current_Working_Directory.test_cwd + `); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, unitTestTestFilesCwdPath); + + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); + assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + }); +}); diff --git a/src/test/testing/unittest/unittest.discovery.unit.test.ts b/src/test/testing/unittest/unittest.discovery.unit.test.ts new file mode 100644 index 000000000000..1d03baada13e --- /dev/null +++ b/src/test/testing/unittest/unittest.discovery.unit.test.ts @@ -0,0 +1,625 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length + +import { expect, use } from 'chai'; +import * as chaipromise from 'chai-as-promised'; +import * as path from 'path'; +import * as typeMoq from 'typemoq'; +import { CancellationToken, Uri } from 'vscode'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; +import { TestsHelper } from '../../../client/testing/common/testUtils'; +import { TestFlatteningVisitor } from '../../../client/testing/common/testVisitors/flatteningVisitor'; +import { + ITestDiscoveryService, + ITestRunner, + ITestsParser, + Options, + TestDiscoveryOptions, + Tests, + UnitTestParserOptions +} from '../../../client/testing/common/types'; +import { IArgumentsHelper } from '../../../client/testing/types'; +import { TestDiscoveryService } from '../../../client/testing/unittest/services/discoveryService'; +import { TestsParser } from '../../../client/testing/unittest/services/parserService'; + +use(chaipromise); + +suite('Unit Tests - Unittest - Discovery', () => { + let discoveryService: ITestDiscoveryService; + let argsHelper: typeMoq.IMock<IArgumentsHelper>; + let testParser: typeMoq.IMock<ITestsParser>; + let runner: typeMoq.IMock<ITestRunner>; + let serviceContainer: typeMoq.IMock<IServiceContainer>; + const dir = path.join('a', 'b', 'c'); + const pattern = 'Pattern_To_Search_For'; + setup(() => { + serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); + argsHelper = typeMoq.Mock.ofType<IArgumentsHelper>(); + testParser = typeMoq.Mock.ofType<ITestsParser>(); + runner = typeMoq.Mock.ofType<ITestRunner>(); + + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(IArgumentsHelper), typeMoq.It.isAny())) + .returns(() => argsHelper.object); + serviceContainer + .setup((s) => s.get(typeMoq.It.isValue(ITestRunner), typeMoq.It.isAny())) + .returns(() => runner.object); + + discoveryService = new TestDiscoveryService(serviceContainer.object, testParser.object); + }); + test('Ensure discovery is invoked with the right args with start directory defined with -s', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFunctions: [], + testSuites: [], + rootTestFolders: [], + testFolders: [] + }; + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-s'))) + .returns(() => dir) + .verifiable(typeMoq.Times.atLeastOnce()); + runner + .setup((r) => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.contain(dir); + expect(opts.args[1]).to.not.contain('loader.discover("."'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser + .setup((t) => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType<TestDiscoveryOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + options.setup((o) => o.args).returns(() => args); + options.setup((o) => o.token).returns(() => token.object); + token.setup((t) => t.isCancellationRequested).returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + argsHelper.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is invoked with the right args with start directory defined with --start-directory', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFunctions: [], + testSuites: [], + rootTestFolders: [], + testFolders: [] + }; + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-s'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.atLeastOnce()); + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--start-directory'))) + .returns(() => dir) + .verifiable(typeMoq.Times.atLeastOnce()); + runner + .setup((r) => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.contain(dir); + expect(opts.args[1]).to.not.contain('loader.discover("."'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser + .setup((t) => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType<TestDiscoveryOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + options.setup((o) => o.args).returns(() => args); + options.setup((o) => o.token).returns(() => token.object); + token.setup((t) => t.isCancellationRequested).returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + argsHelper.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is invoked with the right args without a start directory', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFunctions: [], + testSuites: [], + rootTestFolders: [], + testFolders: [] + }; + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-s'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.atLeastOnce()); + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--start-directory'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.atLeastOnce()); + runner + .setup((r) => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.not.contain(dir); + expect(opts.args[1]).to.contain('loader.discover("."'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser + .setup((t) => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType<TestDiscoveryOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + options.setup((o) => o.args).returns(() => args); + options.setup((o) => o.token).returns(() => token.object); + token.setup((t) => t.isCancellationRequested).returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + argsHelper.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is invoked with the right args without a pattern defined with -p', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFunctions: [], + testSuites: [], + rootTestFolders: [], + testFolders: [] + }; + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-p'))) + .returns(() => pattern) + .verifiable(typeMoq.Times.atLeastOnce()); + runner + .setup((r) => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.contain(pattern); + expect(opts.args[1]).to.not.contain('test*.py'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser + .setup((t) => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType<TestDiscoveryOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + options.setup((o) => o.args).returns(() => args); + options.setup((o) => o.token).returns(() => token.object); + token.setup((t) => t.isCancellationRequested).returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + argsHelper.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is invoked with the right args without a pattern defined with --pattern', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFunctions: [], + testSuites: [], + rootTestFolders: [], + testFolders: [] + }; + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-p'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.atLeastOnce()); + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--pattern'))) + .returns(() => pattern) + .verifiable(typeMoq.Times.atLeastOnce()); + runner + .setup((r) => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.contain(pattern); + expect(opts.args[1]).to.not.contain('test*.py'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser + .setup((t) => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType<TestDiscoveryOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + options.setup((o) => o.args).returns(() => args); + options.setup((o) => o.token).returns(() => token.object); + token.setup((t) => t.isCancellationRequested).returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + argsHelper.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is invoked with the right args without a pattern not defined', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFunctions: [], + testSuites: [], + rootTestFolders: [], + testFolders: [] + }; + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-p'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.atLeastOnce()); + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--pattern'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.atLeastOnce()); + runner + .setup((r) => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .callback((_, opts: Options) => { + expect(opts.args).to.include('-c'); + expect(opts.args[1]).to.not.contain(pattern); + expect(opts.args[1]).to.contain('test*.py'); + }) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser + .setup((t) => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.once()); + + const options = typeMoq.Mock.ofType<TestDiscoveryOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + options.setup((o) => o.args).returns(() => args); + options.setup((o) => o.token).returns(() => token.object); + token.setup((t) => t.isCancellationRequested).returns(() => false); + + const result = await discoveryService.discoverTests(options.object); + + expect(result).to.be.equal(tests); + argsHelper.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery is cancelled', async () => { + const args: string[] = []; + const runOutput = 'xyz'; + const tests: Tests = { + summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, + testFiles: [], + testFunctions: [], + testSuites: [], + rootTestFolders: [], + testFolders: [] + }; + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-p'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.atLeastOnce()); + argsHelper + .setup((a) => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--pattern'))) + .returns(() => undefined) + .verifiable(typeMoq.Times.atLeastOnce()); + runner + .setup((r) => r.run(typeMoq.It.isValue(UNITTEST_PROVIDER), typeMoq.It.isAny())) + .returns(() => Promise.resolve(runOutput)) + .verifiable(typeMoq.Times.once()); + testParser + .setup((t) => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) + .returns(() => tests) + .verifiable(typeMoq.Times.never()); + + const options = typeMoq.Mock.ofType<TestDiscoveryOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + options.setup((o) => o.args).returns(() => args); + options.setup((o) => o.token).returns(() => token.object); + token.setup((t) => t.isCancellationRequested).returns(() => true); + + const promise = discoveryService.discoverTests(options.object); + + await expect(promise).to.eventually.be.rejectedWith('cancelled'); + argsHelper.verifyAll(); + runner.verifyAll(); + testParser.verifyAll(); + }); + test('Ensure discovery resolves test suites in n-depth directories', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + const wspace = typeMoq.Mock.ofType<Uri>(); + opts.setup((o) => o.token).returns(() => token.object); + opts.setup((o) => o.workspaceFolder).returns(() => wspace.object); + token.setup((t) => t.isCancellationRequested).returns(() => true); + opts.setup((o) => o.cwd).returns(() => '/home/user/dev'); + opts.setup((o) => o.startDirectory).returns(() => '/home/user/dev/tests'); + + const discoveryOutput: string = [ + 'start', + 'apptests.debug.class_name.RootClassName.test_root', + 'apptests.debug.class_name.RootClassName.test_root_other', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', + '' + ].join('\n'); + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(3); + expect(tests.testFunctions.length).to.be.equal(6); + expect(tests.testSuites.length).to.be.equal(3); + expect(tests.testFolders.length).to.be.equal(5); + + // now ensure that each test function belongs within a single test suite... + tests.testFunctions.forEach((fn) => { + if (fn.parentTestSuite) { + const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestSuite.nameToRun); + expect(testPrefix).to.equal( + true, + [ + `function ${fn.testFunction.name} has a parent suite ${fn.parentTestSuite.name}, `, + `but the parent suite 'nameToRun' (${fn.parentTestSuite.nameToRun}) isn't the `, + `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})` + ].join('') + ); + } + }); + }); + test('Ensure discovery resolves test files in n-depth directories', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + const wspace = typeMoq.Mock.ofType<Uri>(); + opts.setup((o) => o.token).returns(() => token.object); + opts.setup((o) => o.workspaceFolder).returns(() => wspace.object); + token.setup((t) => t.isCancellationRequested).returns(() => true); + opts.setup((o) => o.cwd).returns(() => '/home/user/dev'); + opts.setup((o) => o.startDirectory).returns(() => '/home/user/dev/tests'); + + const discoveryOutput: string = [ + 'start', + 'apptests.debug.class_name.RootClassName.test_root', + 'apptests.debug.class_name.RootClassName.test_root_other', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', + '' + ].join('\n'); + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(3); + expect(tests.testFunctions.length).to.be.equal(6); + expect(tests.testSuites.length).to.be.equal(3); + expect(tests.testFolders.length).to.be.equal(5); + + // now ensure that the 'nameToRun' for each test function begins with its file's a single test suite... + tests.testFunctions.forEach((fn) => { + if (fn.parentTestSuite) { + const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestFile.nameToRun); + expect(testPrefix).to.equal( + true, + [ + `function ${fn.testFunction.name} was found in file ${fn.parentTestFile.name}, `, + `but the parent file 'nameToRun' (${fn.parentTestFile.nameToRun}) isn't the `, + `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})` + ].join('') + ); + } + }); + }); + test('Ensure discovery resolves test suites in n-depth directories when no start directory is given', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + const wspace = typeMoq.Mock.ofType<Uri>(); + opts.setup((o) => o.token).returns(() => token.object); + opts.setup((o) => o.workspaceFolder).returns(() => wspace.object); + token.setup((t) => t.isCancellationRequested).returns(() => true); + opts.setup((o) => o.cwd).returns(() => '/home/user/dev'); + opts.setup((o) => o.startDirectory).returns(() => ''); + + const discoveryOutput: string = [ + 'start', + 'apptests.debug.class_name.RootClassName.test_root', + 'apptests.debug.class_name.RootClassName.test_root_other', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', + '' + ].join('\n'); + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(3); + expect(tests.testFunctions.length).to.be.equal(6); + expect(tests.testSuites.length).to.be.equal(3); + expect(tests.testFolders.length).to.be.equal(4); + + // now ensure that each test function belongs within a single test suite... + tests.testFunctions.forEach((fn) => { + if (fn.parentTestSuite) { + const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestSuite.nameToRun); + expect(testPrefix).to.equal( + true, + [ + `function ${fn.testFunction.name} has a parent suite ${fn.parentTestSuite.name}, `, + `but the parent suite 'nameToRun' (${fn.parentTestSuite.nameToRun}) isn't the `, + `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})` + ].join('') + ); + } + }); + }); + test('Ensure discovery resolves test suites in n-depth directories when a relative start directory is given', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + const wspace = typeMoq.Mock.ofType<Uri>(); + opts.setup((o) => o.token).returns(() => token.object); + opts.setup((o) => o.workspaceFolder).returns(() => wspace.object); + token.setup((t) => t.isCancellationRequested).returns(() => true); + opts.setup((o) => o.cwd).returns(() => '/home/user/dev'); + opts.setup((o) => o.startDirectory).returns(() => './tests'); + + const discoveryOutput: string = [ + 'start', + 'apptests.debug.class_name.RootClassName.test_root', + 'apptests.debug.class_name.RootClassName.test_root_other', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first', + 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', + 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', + '' + ].join('\n'); + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(3); + expect(tests.testFunctions.length).to.be.equal(6); + expect(tests.testSuites.length).to.be.equal(3); + expect(tests.testFolders.length).to.be.equal(5); + + // now ensure that each test function belongs within a single test suite... + tests.testFunctions.forEach((fn) => { + if (fn.parentTestSuite) { + const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestSuite.nameToRun); + expect(testPrefix).to.equal( + true, + [ + `function ${fn.testFunction.name} has a parent suite ${fn.parentTestSuite.name}, `, + `but the parent suite 'nameToRun' (${fn.parentTestSuite.nameToRun}) isn't the `, + `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})` + ].join('') + ); + } + }); + }); + test('Ensure discovery will not fail with blank content', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + const wspace = typeMoq.Mock.ofType<Uri>(); + opts.setup((o) => o.token).returns(() => token.object); + opts.setup((o) => o.workspaceFolder).returns(() => wspace.object); + token.setup((t) => t.isCancellationRequested).returns(() => true); + opts.setup((o) => o.cwd).returns(() => '/home/user/dev'); + opts.setup((o) => o.startDirectory).returns(() => './tests'); + + const tests: Tests = testsParser.parse('', opts.object); + + expect(tests.testFiles.length).to.be.equal(0); + expect(tests.testFunctions.length).to.be.equal(0); + expect(tests.testSuites.length).to.be.equal(0); + expect(tests.testFolders.length).to.be.equal(0); + }); + test('Ensure discovery will not fail with corrupt content', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + const wspace = typeMoq.Mock.ofType<Uri>(); + opts.setup((o) => o.token).returns(() => token.object); + opts.setup((o) => o.workspaceFolder).returns(() => wspace.object); + token.setup((t) => t.isCancellationRequested).returns(() => true); + opts.setup((o) => o.cwd).returns(() => '/home/user/dev'); + opts.setup((o) => o.startDirectory).returns(() => './tests'); + + const discoveryOutput: string = [ + 'a;lskdjfa', + 'allikbrilkpdbfkdfbalk;nfm', + '', + ';;h,spmn,nlikmslkjls.bmnl;klkjna;jdfngad,lmvnjkldfhb', + '' + ].join('\n'); + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(0); + expect(tests.testFunctions.length).to.be.equal(0); + expect(tests.testSuites.length).to.be.equal(0); + expect(tests.testFolders.length).to.be.equal(0); + }); + test('Ensure discovery resolves when no tests are found in the given path', async () => { + const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); + + const testsParser: TestsParser = new TestsParser(testHelper); + + const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); + const token = typeMoq.Mock.ofType<CancellationToken>(); + const wspace = typeMoq.Mock.ofType<Uri>(); + opts.setup((o) => o.token).returns(() => token.object); + opts.setup((o) => o.workspaceFolder).returns(() => wspace.object); + token.setup((t) => t.isCancellationRequested).returns(() => true); + opts.setup((o) => o.cwd).returns(() => '/home/user/dev'); + opts.setup((o) => o.startDirectory).returns(() => './tests'); + + const discoveryOutput: string = 'start'; + + const tests: Tests = testsParser.parse(discoveryOutput, opts.object); + + expect(tests.testFiles.length).to.be.equal(0); + expect(tests.testFunctions.length).to.be.equal(0); + expect(tests.testSuites.length).to.be.equal(0); + expect(tests.testFolders.length).to.be.equal(0); + }); +}); diff --git a/src/test/testing/unittest/unittest.run.test.ts b/src/test/testing/unittest/unittest.run.test.ts new file mode 100644 index 000000000000..cd821f1a8be0 --- /dev/null +++ b/src/test/testing/unittest/unittest.run.test.ts @@ -0,0 +1,519 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import { EOL } from 'os'; +import * as path from 'path'; +import { instance, mock } from 'ts-mockito'; +import { ConfigurationTarget } from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { registerForIOC } from '../../../client/pythonEnvironments/legacyIOC'; +import { ArgumentsHelper } from '../../../client/testing/common/argumentsHelper'; +import { CommandSource, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; +import { TestRunner } from '../../../client/testing/common/runner'; +import { + ITestManagerFactory, + ITestRunner, + IUnitTestSocketServer, + TestsToRun +} from '../../../client/testing/common/types'; +import { + IArgumentsHelper, + IArgumentsService, + ITestManagerRunner, + IUnitTestHelper +} from '../../../client/testing/types'; +import { UnitTestHelper } from '../../../client/testing/unittest/helper'; +import { TestManagerRunner } from '../../../client/testing/unittest/runner'; +import { ArgumentsService } from '../../../client/testing/unittest/services/argsService'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { MockProcessService } from '../../mocks/proc'; +import { MockUnitTestSocketServer } from '../mocks'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; + +// tslint:disable:max-func-body-length + +const testFilesPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles'); +const UNITTEST_TEST_FILES_PATH = path.join(testFilesPath, 'standard'); +const unitTestSpecificTestFilesPath = path.join(testFilesPath, 'specificTest'); +const defaultUnitTestArgs = ['-v', '-s', '.', '-p', '*test*.py']; + +suite('Unit Tests - unittest - run with mocked process output', () => { + let ioc: UnitTestIocContainer; + const rootDirectory = UNITTEST_TEST_FILES_PATH; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + + suiteSetup(async () => { + await initialize(); + await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + }); + setup(async () => { + const cachePath = path.join(UNITTEST_TEST_FILES_PATH, '.cache'); + if (await fs.pathExists(cachePath)) { + await fs.remove(cachePath); + } + await initializeTest(); + initializeDI(); + await ignoreTestLauncher(); + }); + teardown(async () => { + await ioc.dispose(); + await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); + }); + + interface ITestConfiguration { + patternSwitch: string; + startDirSwitch: string; + } + + function buildTestCliSwitches(): ITestConfiguration[] { + const switches: ITestConfiguration[] = []; + ['-p', '--pattern'].forEach((p) => { + ['-s', '--start - directory'].forEach((s) => { + switches.push({ + patternSwitch: p, + startDirSwitch: s + }); + }); + }); + return switches; + } + const cliSwitches = buildTestCliSwitches(); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + + // Mocks. + ioc.registerMockProcessTypes(); + ioc.registerMockUnitTestSocketServer(); + + // Standard unit test stypes. + ioc.registerTestDiscoveryServices(); + ioc.registerTestDiagnosticServices(); + ioc.registerTestManagers(); + ioc.registerTestManagerService(); + ioc.registerTestParsers(); + ioc.registerTestResultsHelper(); + ioc.registerTestsHelper(); + ioc.registerTestStorage(); + ioc.registerTestVisitors(); + ioc.registerInterpreterStorageTypes(); + ioc.serviceManager.add<IArgumentsService>(IArgumentsService, ArgumentsService, UNITTEST_PROVIDER); + ioc.serviceManager.add<IArgumentsHelper>(IArgumentsHelper, ArgumentsHelper); + ioc.serviceManager.add<ITestManagerRunner>(ITestManagerRunner, TestManagerRunner, UNITTEST_PROVIDER); + ioc.serviceManager.add<ITestRunner>(ITestRunner, TestRunner); + ioc.serviceManager.add<IUnitTestHelper>(IUnitTestHelper, UnitTestHelper); + ioc.serviceManager.addSingletonInstance<IInterpreterService>( + IInterpreterService, + instance(mock(InterpreterService)) + ); + registerForIOC(ioc.serviceManager, ioc.serviceContainer); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, instance(mock(CondaService))); + } + + async function ignoreTestLauncher() { + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + // When running the python test launcher, just return. + procService.onExecObservable((_file, args, _options, callback) => { + if (args.length > 1 && args[0].endsWith('visualstudio_py_testlauncher.py')) { + callback({ out: '', source: 'stdout' }); + } + }); + } + async function injectTestDiscoveryOutput(output: string) { + const procService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + procService.onExecObservable((_file, args, _options, callback) => { + if ( + args.length > 1 && + args[0] === '-c' && + args[1].includes('import unittest') && + args[1].includes('loader = unittest.TestLoader()') + ) { + callback({ + // Ensure any spaces added during code formatting or the like are removed + out: output + .split(/\r?\n/g) + .map((item) => item.trim()) + .join(EOL), + source: 'stdout' + }); + } + }); + } + function injectTestSocketServerResults(results: {}[]) { + // Add results to be sent by unit test socket server. + const socketServer = ioc.serviceContainer.get<MockUnitTestSocketServer>(IUnitTestSocketServer); + socketServer.reset(); + socketServer.addResults(results); + } + + // tslint:disable-next-line:max-func-body-length + cliSwitches.forEach((cfg) => { + test(`Run Tests [${cfg.startDirSwitch}, ${cfg.patternSwitch}]`, async () => { + await updateSetting( + 'testing.unittestArgs', + ['-v', cfg.startDirSwitch, './tests', cfg.patternSwitch, 'test_unittest*.py'], + rootWorkspaceUri, + configTarget + ); + // tslint:disable-next-line:no-multiline-string + await injectTestDiscoveryOutput(`start + test_unittest_one.Test_test1.test_A + test_unittest_one.Test_test1.test_B + test_unittest_one.Test_test1.test_c + test_unittest_two.Test_test2.test_A2 + test_unittest_two.Test_test2.test_B2 + test_unittest_two.Test_test2.test_C2 + test_unittest_two.Test_test2.test_D2 + test_unittest_two.Test_test2a.test_222A2 + test_unittest_two.Test_test2a.test_222B2 + `); + const resultsToSend = [ + { + outcome: 'failed', + traceback: 'AssertionError: Not implemented\n', + message: 'Not implemented', + test: 'test_unittest_one.Test_test1.test_A' + }, + { outcome: 'passed', traceback: null, message: null, test: 'test_unittest_one.Test_test1.test_B' }, + { outcome: 'skipped', traceback: null, message: null, test: 'test_unittest_one.Test_test1.test_c' }, + { + outcome: 'failed', + traceback: 'raise self.failureException(msg)\nAssertionError: Not implemented\n', + message: 'Not implemented', + test: 'test_unittest_two.Test_test2.test_A2' + }, + { outcome: 'passed', traceback: null, message: null, test: 'test_unittest_two.Test_test2.test_B2' }, + { + outcome: 'failed', + traceback: 'raise self.failureException(msg)\nAssertionError: 1 != 2 : Not equal\n', + message: '1 != 2 : Not equal', + test: 'test_unittest_two.Test_test2.test_C2' + }, + { + outcome: 'error', + traceback: 'raise ArithmeticError()\nArithmeticError\n', + message: '', + test: 'test_unittest_two.Test_test2.test_D2' + }, + { + outcome: 'failed', + traceback: 'raise self.failureException(msg)\nAssertionError: Not implemented\n', + message: 'Not implemented', + test: 'test_unittest_two.Test_test2a.test_222A2' + }, + { outcome: 'passed', traceback: null, message: null, test: 'test_unittest_two.Test_test2a.test_222B2' } + ]; + injectTestSocketServerResults(resultsToSend); + + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, rootDirectory); + const results = await testManager.runTest(CommandSource.ui); + + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 4, 'Failures'); + assert.equal(results.summary.passed, 3, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + }); + + test(`Run Failed Tests [${cfg.startDirSwitch}, ${cfg.patternSwitch}]`, async () => { + await updateSetting( + 'testing.unittestArgs', + [`${cfg.startDirSwitch}=./tests`, `${cfg.patternSwitch}=test_unittest*.py`], + rootWorkspaceUri, + configTarget + ); + // tslint:disable-next-line:no-multiline-string + await injectTestDiscoveryOutput(`start + test_unittest_one.Test_test1.test_A + test_unittest_one.Test_test1.test_B + test_unittest_one.Test_test1.test_c + test_unittest_two.Test_test2.test_A2 + test_unittest_two.Test_test2.test_B2 + test_unittest_two.Test_test2.test_C2 + test_unittest_two.Test_test2.test_D2 + test_unittest_two.Test_test2a.test_222A2 + test_unittest_two.Test_test2a.test_222B2 + `); + + const resultsToSend = [ + { + outcome: 'failed', + traceback: 'raise self.failureException(msg)\nAssertionError: Not implemented\n', + message: 'Not implemented', + test: 'test_unittest_one.Test_test1.test_A' + }, + { outcome: 'passed', traceback: null, message: null, test: 'test_unittest_one.Test_test1.test_B' }, + { outcome: 'skipped', traceback: null, message: null, test: 'test_unittest_one.Test_test1.test_c' }, + { + outcome: 'failed', + traceback: 'raise self.failureException(msg)\nAssertionError: Not implemented\n', + message: 'Not implemented', + test: 'test_unittest_two.Test_test2.test_A2' + }, + { outcome: 'passed', traceback: null, message: null, test: 'test_unittest_two.Test_test2.test_B2' }, + { + outcome: 'failed', + traceback: 'raise self.failureException(msg)\nAssertionError: 1 != 2 : Not equal\n', + message: '1 != 2 : Not equal', + test: 'test_unittest_two.Test_test2.test_C2' + }, + { + outcome: 'error', + traceback: 'raise ArithmeticError()\nArithmeticError\n', + message: '', + test: 'test_unittest_two.Test_test2.test_D2' + }, + { + outcome: 'failed', + traceback: 'raise self.failureException(msg)\nAssertionError: Not implemented\n', + message: 'Not implemented', + test: 'test_unittest_two.Test_test2a.test_222A2' + }, + { outcome: 'passed', traceback: null, message: null, test: 'test_unittest_two.Test_test2a.test_222B2' } + ]; + injectTestSocketServerResults(resultsToSend); + + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, rootDirectory); + let results = await testManager.runTest(CommandSource.ui); + assert.equal(results.summary.errors, 1, 'Errors'); + assert.equal(results.summary.failures, 4, 'Failures'); + assert.equal(results.summary.passed, 3, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + + const failedResultsToSend = [ + { + outcome: 'failed', + traceback: 'raise self.failureException(msg)\nAssertionError: Not implemented\n', + message: 'Not implemented', + test: 'test_unittest_one.Test_test1.test_A' + }, + { + outcome: 'failed', + traceback: 'raise self.failureException(msg)\nAssertionError: Not implemented\n', + message: 'Not implemented', + test: 'test_unittest_two.Test_test2.test_A2' + }, + { + outcome: 'failed', + traceback: 'raise self.failureException(msg)\nAssertionError: 1 != 2 : Not equal\n', + message: '1 != 2 : Not equal', + test: 'test_unittest_two.Test_test2.test_C2' + }, + { + outcome: 'error', + traceback: 'raise ArithmeticError()\nArithmeticError\n', + message: '', + test: 'test_unittest_two.Test_test2.test_D2' + }, + { + outcome: 'failed', + traceback: 'raise self.failureException(msg)\nAssertionError: Not implemented\n', + message: 'Not implemented', + test: 'test_unittest_two.Test_test2a.test_222A2' + } + ]; + injectTestSocketServerResults(failedResultsToSend); + + results = await testManager.runTest(CommandSource.ui, undefined, true); + assert.equal(results.summary.errors, 1, 'Failed Errors'); + assert.equal(results.summary.failures, 4, 'Failed Failures'); + assert.equal(results.summary.passed, 0, 'Failed Passed'); + assert.equal(results.summary.skipped, 0, 'Failed skipped'); + }); + + test(`Run Specific Test File [${cfg.startDirSwitch}, ${cfg.patternSwitch}]`, async () => { + await updateSetting( + 'testing.unittestArgs', + [`${cfg.startDirSwitch}=./tests`, `${cfg.patternSwitch}=test_unittest*.py`], + rootWorkspaceUri, + configTarget + ); + + // tslint:disable-next-line:no-multiline-string + await injectTestDiscoveryOutput(`start + test_unittest_one.Test_test_one_1.test_1_1_1 + test_unittest_one.Test_test_one_1.test_1_1_2 + test_unittest_one.Test_test_one_1.test_1_1_3 + test_unittest_one.Test_test_one_2.test_1_2_1 + test_unittest_two.Test_test_two_1.test_1_1_1 + test_unittest_two.Test_test_two_1.test_1_1_2 + test_unittest_two.Test_test_two_1.test_1_1_3 + test_unittest_two.Test_test_two_2.test_2_1_1 + `); + + const resultsToSend = [ + { + outcome: 'passed', + traceback: null, + message: null, + test: 'test_unittest_one.Test_test_one_1.test_1_1_1' + }, + { + outcome: 'failed', + traceback: 'AssertionError: 1 != 2 : Not equal\n', + message: '1 != 2 : Not equal', + test: 'test_unittest_one.Test_test_one_1.test_1_1_2' + }, + { + outcome: 'skipped', + traceback: null, + message: null, + test: 'test_unittest_one.Test_test_one_1.test_1_1_3' + }, + { + outcome: 'passed', + traceback: null, + message: null, + test: 'test_unittest_one.Test_test_one_2.test_1_2_1' + } + ]; + injectTestSocketServerResults(resultsToSend); + + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, unitTestSpecificTestFilesPath); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + + // tslint:disable-next-line:no-non-null-assertion + const testFileToTest = tests.testFiles.find((f) => f.name === 'test_unittest_one.py')!; + const testFile: TestsToRun = { + testFile: [testFileToTest], + testFolder: [], + testFunction: [], + testSuite: [] + }; + const results = await testManager.runTest(CommandSource.ui, testFile); + + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 2, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + }); + + test(`Run Specific Test Suite [${cfg.startDirSwitch}, ${cfg.patternSwitch}]`, async () => { + await updateSetting( + 'testing.unittestArgs', + [`${cfg.startDirSwitch}=./tests`, `${cfg.patternSwitch}=test_unittest*.py`], + rootWorkspaceUri, + configTarget + ); + // tslint:disable-next-line:no-multiline-string + await injectTestDiscoveryOutput(`start + test_unittest_one.Test_test_one_1.test_1_1_1 + test_unittest_one.Test_test_one_1.test_1_1_2 + test_unittest_one.Test_test_one_1.test_1_1_3 + test_unittest_one.Test_test_one_2.test_1_2_1 + test_unittest_two.Test_test_two_1.test_1_1_1 + test_unittest_two.Test_test_two_1.test_1_1_2 + test_unittest_two.Test_test_two_1.test_1_1_3 + test_unittest_two.Test_test_two_2.test_2_1_1 + `); + + const resultsToSend = [ + { + outcome: 'passed', + traceback: null, + message: null, + test: 'test_unittest_one.Test_test_one_1.test_1_1_1' + }, + { + outcome: 'failed', + traceback: 'AssertionError: 1 != 2 : Not equal\n', + message: '1 != 2 : Not equal', + test: 'test_unittest_one.Test_test_one_1.test_1_1_2' + }, + { + outcome: 'skipped', + traceback: null, + message: null, + test: 'test_unittest_one.Test_test_one_1.test_1_1_3' + }, + { + outcome: 'passed', + traceback: null, + message: null, + test: 'test_unittest_one.Test_test_one_2.test_1_2_1' + } + ]; + injectTestSocketServerResults(resultsToSend); + + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, unitTestSpecificTestFilesPath); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + + // tslint:disable-next-line:no-non-null-assertion + const testSuiteToTest = tests.testSuites.find((s) => s.testSuite.name === 'Test_test_one_1')!.testSuite; + const testSuite: TestsToRun = { + testFile: [], + testFolder: [], + testFunction: [], + testSuite: [testSuiteToTest] + }; + const results = await testManager.runTest(CommandSource.ui, testSuite); + + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 2, 'Passed'); + assert.equal(results.summary.skipped, 1, 'skipped'); + }); + + test(`Run Specific Test Function [${cfg.startDirSwitch}, ${cfg.patternSwitch}]`, async () => { + await updateSetting( + 'testing.unittestArgs', + [`${cfg.startDirSwitch}=./tests`, `${cfg.patternSwitch}=test_unittest*.py`], + rootWorkspaceUri, + configTarget + ); + // tslint:disable-next-line:no-multiline-string + await injectTestDiscoveryOutput(`start + test_unittest_one.Test_test1.test_A + test_unittest_one.Test_test1.test_B + test_unittest_one.Test_test1.test_c + test_unittest_two.Test_test2.test_A2 + test_unittest_two.Test_test2.test_B2 + test_unittest_two.Test_test2.test_C2 + test_unittest_two.Test_test2.test_D2 + test_unittest_two.Test_test2a.test_222A2 + test_unittest_two.Test_test2a.test_222B2 + `); + + const resultsToSend = [ + { + outcome: 'failed', + traceback: 'AssertionError: Not implemented\n', + message: 'Not implemented', + test: 'test_unittest_one.Test_test1.test_A' + } + ]; + injectTestSocketServerResults(resultsToSend); + + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, rootDirectory); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testFn: TestsToRun = { + testFile: [], + testFolder: [], + testFunction: [tests.testFunctions[0].testFunction], + testSuite: [] + }; + const results = await testManager.runTest(CommandSource.ui, testFn); + assert.equal(results.summary.errors, 0, 'Errors'); + assert.equal(results.summary.failures, 1, 'Failures'); + assert.equal(results.summary.passed, 0, 'Passed'); + assert.equal(results.summary.skipped, 0, 'skipped'); + }); + }); +}); diff --git a/src/test/testing/unittest/unittest.test.ts b/src/test/testing/unittest/unittest.test.ts new file mode 100644 index 000000000000..858cefbc0a91 --- /dev/null +++ b/src/test/testing/unittest/unittest.test.ts @@ -0,0 +1,266 @@ +'use strict'; + +import * as assert from 'assert'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { ConfigurationTarget } from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { CondaService } from '../../../client/pythonEnvironments/discovery/locators/services/condaService'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { ITestManagerFactory, TestFile, TestFunction, Tests, TestsToRun } from '../../../client/testing/common/types'; +import { rootWorkspaceUri, updateSetting } from '../../common'; +import { UnitTestIocContainer } from '../serviceRegistry'; +import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; + +const testFilesPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles'); +const UNITTEST_TEST_FILES_PATH = path.join(testFilesPath, 'standard'); +const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(testFilesPath, 'single'); +const UNITTEST_MULTI_TEST_FILE_PATH = path.join(testFilesPath, 'multi'); +const UNITTEST_COUNTS_TEST_FILE_PATH = path.join(testFilesPath, 'counter'); +const defaultUnitTestArgs = ['-v', '-s', '.', '-p', '*test*.py']; + +// tslint:disable-next-line:max-func-body-length +suite('Unit Tests - unittest - discovery against actual python process', () => { + let ioc: UnitTestIocContainer; + const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + + suiteSetup(async () => { + await initialize(); + await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri!, configTarget); + }); + setup(async () => { + const cachePath = path.join(UNITTEST_TEST_FILES_PATH, '.cache'); + if (await fs.pathExists(cachePath)) { + await fs.remove(cachePath); + } + await initializeTest(); + initializeDI(); + }); + teardown(async () => { + await ioc.dispose(); + await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri!, configTarget); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerUnitTestTypes(); + ioc.registerProcessTypes(); + ioc.registerInterpreterStorageTypes(); + ioc.registerMockInterpreterTypes(); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, instance(mock(CondaService))); + ioc.serviceManager.rebindInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); + const mockEnvironmentActivationService = mock(EnvironmentActivationService); + when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); + when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenResolve(); + when( + mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything(), anything(), anything()) + ).thenResolve(); + ioc.serviceManager.rebindInstance<IEnvironmentActivationService>( + IEnvironmentActivationService, + instance(mockEnvironmentActivationService) + ); + } + + test('Discover Tests (single test file)', async () => { + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_one.py' && t.nameToRun === 'test_one'), + true, + 'Test File not found' + ); + assert.equal( + tests.testFunctions.some( + (t) => t.testFunction.name === 'test_A' && t.testFunction.nameToRun === 'test_one.Test_test1.test_A' + ), + true, + 'Test File not found' + ); + }); + + test('Discover Tests (many test files, subdir included)', async () => { + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_MULTI_TEST_FILE_PATH); + const tests = await testManager.discoverTests(CommandSource.ui, true, true); + assert.equal(tests.testFiles.length, 3, 'Incorrect number of test files'); + assert.equal(tests.testFunctions.length, 9, 'Incorrect number of test functions'); + assert.equal(tests.testSuites.length, 3, 'Incorrect number of test suites'); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_one.py' && t.nameToRun === 'test_one'), + true, + 'Test File one not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_two.py' && t.nameToRun === 'test_two'), + true, + 'Test File two not found' + ); + assert.equal( + tests.testFiles.some((t) => t.name === 'test_three.py' && t.nameToRun === 'more_tests.test_three'), + true, + 'Test File three not found' + ); + assert.equal( + tests.testFunctions.some( + (t) => t.testFunction.name === 'test_A' && t.testFunction.nameToRun === 'test_one.Test_test1.test_A' + ), + true, + 'Test File one not found' + ); + assert.equal( + tests.testFunctions.some( + (t) => t.testFunction.name === 'test_2A' && t.testFunction.nameToRun === 'test_two.Test_test2.test_2A' + ), + true, + 'Test File two not found' + ); + assert.equal( + tests.testFunctions.some( + (t) => + t.testFunction.name === 'test_3A' && + t.testFunction.nameToRun === 'more_tests.test_three.Test_test3.test_3A' + ), + true, + 'Test File three not found' + ); + }); + + test('Run single test', async () => { + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_MULTI_TEST_FILE_PATH); + const testsDiscovered: Tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testFile: TestFile | undefined = testsDiscovered.testFiles.find((value: TestFile) => + value.nameToRun.endsWith('_three') + ); + assert.notEqual(testFile, undefined, 'No test file suffixed with _3A in test files.'); + assert.equal(testFile!.suites.length, 1, 'Expected only 1 test suite in test file three.'); + const testFunc: TestFunction | undefined = testFile!.suites[0].functions.find( + (value: TestFunction) => value.name === 'test_3A' + ); + assert.notEqual(testFunc, undefined, 'No test in file test_three.py named test_3A'); + const testsToRun: TestsToRun = { + testFunction: [testFunc!] + }; + const testRunResult: Tests = await testManager.runTest(CommandSource.ui, testsToRun); + assert.equal( + testRunResult.summary.failures + testRunResult.summary.passed + testRunResult.summary.skipped, + 1, + 'Expected to see only 1 test run in the summary for tests run.' + ); + assert.equal(testRunResult.summary.errors, 0, 'Unexpected: Test file ran with errors.'); + assert.equal(testRunResult.summary.failures, 0, 'Unexpected: Test has failed during test run.'); + assert.equal( + testRunResult.summary.passed, + 1, + `Only one test should have passed during our test run. Instead, ${testRunResult.summary.passed} passed.` + ); + assert.equal( + testRunResult.summary.skipped, + 0, + `Expected to have skipped 0 tests during this test-run. Instead, ${testRunResult.summary.skipped} where skipped.` + ); + }); + + test('Ensure correct test count for running a set of tests multiple times', async function () { + // https://github.com/microsoft/vscode-python/issues/11634 + // tslint:disable-next-line: no-invalid-this + return this.skip(); + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_COUNTS_TEST_FILE_PATH); + const testsDiscovered: Tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testsFile: TestFile | undefined = testsDiscovered.testFiles.find((value: TestFile) => + value.name.startsWith('test_unit_test_counter') + ); + assert.notEqual( + testsFile, + undefined, + `No test file suffixed with _counter in test files. Looked in ${UNITTEST_COUNTS_TEST_FILE_PATH}.` + ); + assert.equal(testsFile!.suites.length, 1, 'Expected only 1 test suite in counter test file.'); + const testsToRun: TestsToRun = { + testFolder: [testsDiscovered.testFolders[0]] + }; + + // ensure that each re-run of the unit tests in question result in the same summary count information. + let testRunResult: Tests = await testManager.runTest(CommandSource.ui, testsToRun); + assert.equal( + testRunResult.summary.failures, + 2, + 'This test was written assuming there was 2 tests run that would fail. (iteration 1)' + ); + assert.equal( + testRunResult.summary.passed, + 2, + 'This test was written assuming there was 2 tests run that would succeed. (iteration 1)' + ); + + testRunResult = await testManager.runTest(CommandSource.ui, testsToRun); + assert.equal( + testRunResult.summary.failures, + 2, + 'This test was written assuming there was 2 tests run that would fail. (iteration 2)' + ); + assert.equal( + testRunResult.summary.passed, + 2, + 'This test was written assuming there was 2 tests run that would succeed. (iteration 2)' + ); + }); + + test('Re-run failed tests results in the correct number of tests counted', async function () { + // https://github.com/microsoft/vscode-python/issues/11634 + // tslint:disable-next-line: no-invalid-this + return this.skip(); + await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); + const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); + const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_COUNTS_TEST_FILE_PATH); + const testsDiscovered: Tests = await testManager.discoverTests(CommandSource.ui, true, true); + const testsFile: TestFile | undefined = testsDiscovered.testFiles.find((value: TestFile) => + value.name.startsWith('test_unit_test_counter') + ); + assert.notEqual( + testsFile, + undefined, + `No test file suffixed with _counter in test files. Looked in ${UNITTEST_COUNTS_TEST_FILE_PATH}.` + ); + assert.equal(testsFile!.suites.length, 1, 'Expected only 1 test suite in counter test file.'); + const testsToRun: TestsToRun = { + testFolder: [testsDiscovered.testFolders[0]] + }; + + // ensure that each re-run of the unit tests in question result in the same summary count information. + let testRunResult: Tests = await testManager.runTest(CommandSource.ui, testsToRun); + assert.equal( + testRunResult.summary.failures, + 2, + 'This test was written assuming there was 2 tests run that would fail. (iteration 1)' + ); + assert.equal( + testRunResult.summary.passed, + 2, + 'This test was written assuming there was 2 tests run that would succeed. (iteration 1)' + ); + + testRunResult = await testManager.runTest(CommandSource.ui, testsToRun, true); + assert.equal( + testRunResult.summary.failures, + 2, + 'This test was written assuming there was 2 tests run that would fail. (iteration 2)' + ); + }); +}); diff --git a/src/test/testing/unittest/unittest.unit.test.ts b/src/test/testing/unittest/unittest.unit.test.ts new file mode 100644 index 000000000000..58d771ca9c60 --- /dev/null +++ b/src/test/testing/unittest/unittest.unit.test.ts @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { anything, capture, instance, mock, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { + IConfigurationService, + IDisposableRegistry, + IOutputChannel, + IPythonSettings +} from '../../../client/common/types'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { ArgumentsHelper } from '../../../client/testing/common/argumentsHelper'; +import { CommandSource } from '../../../client/testing/common/constants'; +import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; +import { TestResultsService } from '../../../client/testing/common/services/testResultsService'; +import { TestsStatusUpdaterService } from '../../../client/testing/common/services/testsStatusService'; +import { TestsHelper } from '../../../client/testing/common/testUtils'; +import { TestResultResetVisitor } from '../../../client/testing/common/testVisitors/resultResetVisitor'; +import { + FlattenedTestFunction, + FlattenedTestSuite, + ITestResultsService, + ITestsHelper, + ITestsStatusUpdaterService, + TestFile, + TestFolder, + TestFunction, + Tests, + TestStatus, + TestSuite +} from '../../../client/testing/common/types'; +import { + IArgumentsHelper, + IArgumentsService, + ITestManagerRunner, + TestDataItemType +} from '../../../client/testing/types'; +import { TestManager } from '../../../client/testing/unittest/main'; +import { TestManagerRunner } from '../../../client/testing/unittest/runner'; +import { ArgumentsService } from '../../../client/testing/unittest/services/argsService'; +import { MockOutputChannel } from '../../mockClasses'; +import { createMockTestDataItem } from '../common/testUtils.unit.test'; + +// tslint:disable:max-func-body-length no-any +suite('Unit Tests - unittest - run failed tests', () => { + let testManager: TestManager; + const workspaceFolder = Uri.file(__dirname); + let serviceContainer: IServiceContainer; + let testsHelper: ITestsHelper; + let testManagerRunner: ITestManagerRunner; + let tests: Tests; + function createTestData() { + const folder1 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder2 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder3 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder4 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + const folder5 = createMockTestDataItem<TestFolder>(TestDataItemType.folder); + folder1.folders.push(folder2); + folder1.folders.push(folder3); + folder2.folders.push(folder4); + folder3.folders.push(folder5); + + const file1 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file2 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file3 = createMockTestDataItem<TestFile>(TestDataItemType.file); + const file4 = createMockTestDataItem<TestFile>(TestDataItemType.file); + folder1.testFiles.push(file1); + folder3.testFiles.push(file2); + folder3.testFiles.push(file3); + folder5.testFiles.push(file4); + + const suite1 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite2 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite3 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite4 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const suite5 = createMockTestDataItem<TestSuite>(TestDataItemType.suite); + const fn1 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn2 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn3 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn4 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + const fn5 = createMockTestDataItem<TestFunction>(TestDataItemType.function); + file1.suites.push(suite1); + file1.suites.push(suite2); + file3.suites.push(suite3); + suite3.suites.push(suite4); + suite4.suites.push(suite5); + file1.functions.push(fn1); + file1.functions.push(fn2); + suite1.functions.push(fn3); + suite1.functions.push(fn4); + suite3.functions.push(fn5); + const flattendSuite1: FlattenedTestSuite = { + testSuite: suite1, + xmlClassName: suite1.xmlName + } as any; + const flattendSuite2: FlattenedTestSuite = { + testSuite: suite2, + xmlClassName: suite2.xmlName + } as any; + const flattendSuite3: FlattenedTestSuite = { + testSuite: suite3, + xmlClassName: suite3.xmlName + } as any; + const flattendSuite4: FlattenedTestSuite = { + testSuite: suite4, + xmlClassName: suite4.xmlName + } as any; + const flattendSuite5: FlattenedTestSuite = { + testSuite: suite5, + xmlClassName: suite5.xmlName + } as any; + const flattendFn1: FlattenedTestFunction = { + testFunction: fn1, + xmlClassName: fn1.name + } as any; + const flattendFn2: FlattenedTestFunction = { + testFunction: fn2, + xmlClassName: fn2.name + } as any; + const flattendFn3: FlattenedTestFunction = { + testFunction: fn3, + xmlClassName: fn3.name + } as any; + const flattendFn4: FlattenedTestFunction = { + testFunction: fn4, + xmlClassName: fn4.name + } as any; + const flattendFn5: FlattenedTestFunction = { + testFunction: fn5, + xmlClassName: fn5.name + } as any; + tests = { + rootTestFolders: [folder1], + summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, + testFiles: [file1, file2, file3, file4], + testFolders: [folder1, folder2, folder3, folder4, folder5], + testFunctions: [flattendFn1, flattendFn2, flattendFn3, flattendFn4, flattendFn5], + testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] + }; + } + setup(() => { + createTestData(); + serviceContainer = mock(ServiceContainer); + testsHelper = mock(TestsHelper); + testManagerRunner = mock(TestManagerRunner); + const testStorage = mock(TestCollectionStorageService); + const workspaceService = mock(WorkspaceService); + const svcInstance = instance(serviceContainer); + when(testStorage.getTests(anything())).thenReturn(tests); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ name: '', index: 0, uri: workspaceFolder }); + when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspaceService)); + when(serviceContainer.get<IArgumentsHelper>(IArgumentsHelper)).thenReturn(new ArgumentsHelper()); + when(serviceContainer.get<IArgumentsService>(IArgumentsService, anything())).thenReturn( + new ArgumentsService(svcInstance) + ); + when(serviceContainer.get<ITestsHelper>(ITestsHelper)).thenReturn(instance(testsHelper)); + when(serviceContainer.get<ITestManagerRunner>(ITestManagerRunner, anything())).thenReturn( + instance(testManagerRunner) + ); + when(serviceContainer.get<ITestsStatusUpdaterService>(ITestsStatusUpdaterService)).thenReturn( + new TestsStatusUpdaterService(instance(testStorage)) + ); + when(serviceContainer.get<ITestResultsService>(ITestResultsService)).thenReturn( + new TestResultsService(new TestResultResetVisitor()) + ); + when(serviceContainer.get<IOutputChannel>(IOutputChannel)).thenReturn(instance(mock(MockOutputChannel))); + when(serviceContainer.get<IOutputChannel>(IOutputChannel)).thenReturn(instance(mock(MockOutputChannel))); + when(serviceContainer.get<IDisposableRegistry>(IDisposableRegistry)).thenReturn([]); + const settingsService = mock(ConfigurationService); + const settings: IPythonSettings = { + testing: { + unittestArgs: [] + } + } as any; + when(settingsService.getSettings(anything())).thenReturn(settings); + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(settingsService)); + + testManager = new TestManager(workspaceFolder, workspaceFolder.fsPath, svcInstance); + }); + + test('Run Failed tests', async () => { + testManager.discoverTests = () => Promise.resolve(tests); + when(testsHelper.shouldRunAllTests(anything())).thenReturn(false); + when(testManagerRunner.runTest(anything(), anything(), anything())).thenResolve(undefined as any); + (testManager as any).tests = tests; + tests.testFunctions[0].testFunction.status = TestStatus.Fail; + tests.testFunctions[2].testFunction.status = TestStatus.Fail; + + await testManager.runTest(CommandSource.testExplorer, undefined, true); + + const options = capture(testManagerRunner.runTest).last()[1]; + assert.deepEqual(options.tests, tests); + assert.equal(options.testsToRun!.testFile!.length, 0); + assert.equal(options.testsToRun!.testFolder!.length, 0); + assert.equal(options.testsToRun!.testSuite!.length, 0); + assert.equal(options.testsToRun!.testFunction!.length, 2); + assert.deepEqual(options.testsToRun!.testFunction![0], tests.testFunctions[0].testFunction); + assert.deepEqual(options.testsToRun!.testFunction![1], tests.testFunctions[2].testFunction); + }); + test('Run All tests', async () => { + testManager.discoverTests = () => Promise.resolve(tests); + when(testsHelper.shouldRunAllTests(anything())).thenReturn(false); + when(testManagerRunner.runTest(anything(), anything(), anything())).thenResolve(undefined as any); + (testManager as any).tests = tests; + + await testManager.runTest(CommandSource.testExplorer, undefined, true); + + const options = capture(testManagerRunner.runTest).last()[1]; + assert.deepEqual(options.tests, tests); + assert.equal(options.testsToRun!.testFile!.length, 0); + assert.equal(options.testsToRun!.testFolder!.length, 0); + assert.equal(options.testsToRun!.testSuite!.length, 0); + assert.equal(options.testsToRun!.testFunction!.length, 0); + }); +}); diff --git a/src/test/textUtils.ts b/src/test/textUtils.ts new file mode 100644 index 000000000000..36c877887130 --- /dev/null +++ b/src/test/textUtils.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { MarkedString } from 'vscode'; + +export function normalizeMarkedString(content: MarkedString): string { + return typeof content === 'string' ? content : content.value; +} + +export function compareFiles(expectedContent: string, actualContent: string) { + const expectedLines = expectedContent.split(/\r?\n/); + const actualLines = actualContent.split(/\r?\n/); + + for (let i = 0; i < Math.min(expectedLines.length, actualLines.length); i += 1) { + const e = expectedLines[i]; + const a = actualLines[i]; + expect(e, `Difference at line ${i}`).to.be.equal(a); + } + + expect( + actualLines.length, + expectedLines.length > actualLines.length + ? 'Actual contains more lines than expected' + : 'Expected contains more lines than the actual' + ).to.be.equal(expectedLines.length); +} diff --git a/src/test/unittests.ts b/src/test/unittests.ts new file mode 100644 index 000000000000..066d4b628012 --- /dev/null +++ b/src/test/unittests.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// Not sure why but on windows, if you execute a process from the System32 directory, it will just crash Node. +// Not throw an exception, just make node exit. +// However if a system32 process is run first, everything works. +import * as child_process from 'child_process'; +import * as os from 'os'; +if (os.platform() === 'win32') { + const proc = child_process.spawn('C:\\Windows\\System32\\Reg.exe', ['/?']); + proc.on('error', () => { + // tslint:disable-next-line: no-console + console.error('error during reg.exe'); + }); +} + +// tslint:disable:no-any no-require-imports no-var-requires +if ((Reflect as any).metadata === undefined) { + require('reflect-metadata'); +} + +process.env.VSC_PYTHON_CI_TEST = '1'; +process.env.VSC_PYTHON_UNIT_TEST = '1'; +process.env.NODE_ENV = 'production'; // Make sure react is using production bits or we can run out of memory. + +import { setUpDomEnvironment, setupTranspile } from './datascience/reactHelpers'; +import { initialize } from './vscode-mock'; + +// Custom module loader so we skip .css files that break non webpack wrapped compiles +// tslint:disable-next-line:no-var-requires no-require-imports +const Module = require('module'); + +// Required for DS functional tests. +// tslint:disable-next-line:no-function-expression +(function () { + const origRequire = Module.prototype.require; + const _require = (context: any, filepath: any) => { + return origRequire.call(context, filepath); + }; + Module.prototype.require = function (filepath: string) { + if (filepath.endsWith('.css') || filepath.endsWith('.svg')) { + return ''; + } + if (filepath.startsWith('expose-loader?')) { + // Pull out the thing to expose + const queryEnd = filepath.indexOf('!'); + if (queryEnd >= 0) { + const query = filepath.substring('expose-loader?'.length, queryEnd); + // tslint:disable-next-line:no-invalid-this + (global as any)[query] = _require(this, filepath.substring(queryEnd + 1)); + return ''; + } + } + if (filepath.startsWith('slickgrid/slick.core')) { + // Special case. This module sticks something into the global 'window' object. + // tslint:disable-next-line:no-invalid-this + const result = _require(this, filepath); + + // However it doesn't look in the 'window' object later. we have to move it to + // the globals when in node.js + if ((window as any).Slick) { + (global as any).Slick = (window as any).Slick; + } + + return result; + } + // tslint:disable-next-line:no-invalid-this + return _require(this, filepath); + }; +})(); + +// Setting up DOM env and transpile is required for the react & monaco related tests. +// However this takes around 40s to setup on Mac, hence slowing down testing/development. +// Allowing ability to disable this (faster local development & testing, saving minutes). +if (process.argv.indexOf('--fast') === -1) { + // nteract/transforms-full expects to run in the browser so we have to fake + // parts of the browser here. + setUpDomEnvironment(); + + // Also have to setup babel to get the monaco editor to work. + setupTranspile(); +} + +initialize(); diff --git a/src/test/utils/fs.ts b/src/test/utils/fs.ts new file mode 100644 index 000000000000..f92ffbfd91c6 --- /dev/null +++ b/src/test/utils/fs.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as tmp from 'tmp'; + +export function createTemporaryFile( + extension: string, + temporaryDirectory?: string +): Promise<{ filePath: string; cleanupCallback: Function }> { + // tslint:disable-next-line:no-any + const options: any = { postfix: extension }; + if (temporaryDirectory) { + options.dir = temporaryDirectory; + } + + return new Promise<{ filePath: string; cleanupCallback: Function }>((resolve, reject) => { + tmp.file(options, (err, tmpFile, _fd, cleanupCallback) => { + if (err) { + return reject(err); + } + resolve({ filePath: tmpFile, cleanupCallback: cleanupCallback }); + }); + }); +} diff --git a/src/test/utils/interpreters.ts b/src/test/utils/interpreters.ts new file mode 100644 index 000000000000..c9bf2004c890 --- /dev/null +++ b/src/test/utils/interpreters.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Architecture } from '../../client/common/utils/platform'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; + +/** + * Creates a PythonInterpreter object for testing purposes, with unique name, version and path. + * If required a custom name, version and the like can be provided. + * + * @export + * @param {Partial<PythonEnvironment>} [info] + * @returns {PythonEnvironment} + */ +export function createPythonInterpreter(info?: Partial<PythonEnvironment>): PythonEnvironment { + const rnd = new Date().getTime().toString(); + return { + displayName: `Something${rnd}`, + architecture: Architecture.Unknown, + path: `somePath${rnd}`, + sysPrefix: `someSysPrefix${rnd}`, + sysVersion: `1.1.1`, + envType: EnvironmentType.Unknown, + ...(info || {}) + }; +} diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts new file mode 100644 index 000000000000..2ee192f112e1 --- /dev/null +++ b/src/test/vscode-mock.ts @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-invalid-this no-require-imports no-var-requires no-any + +import * as TypeMoq from 'typemoq'; +import * as vscode from 'vscode'; +import * as vscodeMocks from './mocks/vsc'; +import { vscMockTelemetryReporter } from './mocks/vsc/telemetryReporter'; +const Module = require('module'); + +type VSCode = typeof vscode; + +const mockedVSCode: Partial<VSCode> = {}; +export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: TypeMoq.IMock<VSCode[P]> } = {}; +const originalLoad = Module._load; + +function generateMock<K extends keyof VSCode>(name: K): void { + const mockedObj = TypeMoq.Mock.ofType<VSCode[K]>(); + (mockedVSCode as any)[name] = mockedObj.object; + mockedVSCodeNamespaces[name] = mockedObj as any; +} + +class MockClipboard { + private text: string = ''; + public readText(): Promise<string> { + return Promise.resolve(this.text); + } + public async writeText(value: string): Promise<void> { + this.text = value; + } +} +export function initialize() { + generateMock('workspace'); + generateMock('window'); + generateMock('commands'); + generateMock('languages'); + generateMock('env'); + generateMock('debug'); + generateMock('scm'); + generateNotebookMocks(); + + // Use mock clipboard fo testing purposes. + const clipboard = new MockClipboard(); + mockedVSCodeNamespaces.env?.setup((e) => e.clipboard).returns(() => clipboard); + mockedVSCodeNamespaces.env?.setup((e) => e.appName).returns(() => 'Insider'); + + // When upgrading to npm 9-10, this might have to change, as we could have explicit imports (named imports). + Module._load = function (request: any, _parent: any) { + if (request === 'vscode') { + return mockedVSCode; + } + if (request === 'vscode-extension-telemetry') { + return { default: vscMockTelemetryReporter as any }; + } + // less files need to be in import statements to be converted to css + // But we don't want to try to load them in the mock vscode + if (/\.less$/.test(request)) { + return; + } + return originalLoad.apply(this, arguments); + }; +} + +mockedVSCode.Disposable = vscodeMocks.vscMock.Disposable as any; +mockedVSCode.ExtensionKind = vscodeMocks.vscMock.ExtensionKind; +mockedVSCode.CodeAction = vscodeMocks.vscMock.CodeAction; +mockedVSCode.EventEmitter = vscodeMocks.vscMock.EventEmitter; +mockedVSCode.CancellationTokenSource = vscodeMocks.vscMock.CancellationTokenSource; +mockedVSCode.CompletionItemKind = vscodeMocks.vscMock.CompletionItemKind; +mockedVSCode.SymbolKind = vscodeMocks.vscMock.SymbolKind; +mockedVSCode.IndentAction = vscodeMocks.vscMock.IndentAction; +mockedVSCode.Uri = vscodeMocks.vscUri.URI as any; +mockedVSCode.Range = vscodeMocks.vscMockExtHostedTypes.Range; +mockedVSCode.Position = vscodeMocks.vscMockExtHostedTypes.Position; +mockedVSCode.Selection = vscodeMocks.vscMockExtHostedTypes.Selection; +mockedVSCode.Location = vscodeMocks.vscMockExtHostedTypes.Location; +mockedVSCode.SymbolInformation = vscodeMocks.vscMockExtHostedTypes.SymbolInformation; +mockedVSCode.CompletionItem = vscodeMocks.vscMockExtHostedTypes.CompletionItem; +mockedVSCode.CompletionItemKind = vscodeMocks.vscMockExtHostedTypes.CompletionItemKind; +mockedVSCode.CodeLens = vscodeMocks.vscMockExtHostedTypes.CodeLens; +mockedVSCode.DiagnosticSeverity = vscodeMocks.vscMockExtHostedTypes.DiagnosticSeverity; +mockedVSCode.SnippetString = vscodeMocks.vscMockExtHostedTypes.SnippetString; +mockedVSCode.ConfigurationTarget = vscodeMocks.vscMockExtHostedTypes.ConfigurationTarget; +mockedVSCode.StatusBarAlignment = vscodeMocks.vscMockExtHostedTypes.StatusBarAlignment; +mockedVSCode.SignatureHelp = vscodeMocks.vscMockExtHostedTypes.SignatureHelp; +mockedVSCode.DocumentLink = vscodeMocks.vscMockExtHostedTypes.DocumentLink; +mockedVSCode.TextEdit = vscodeMocks.vscMockExtHostedTypes.TextEdit; +mockedVSCode.WorkspaceEdit = vscodeMocks.vscMockExtHostedTypes.WorkspaceEdit; +mockedVSCode.RelativePattern = vscodeMocks.vscMockExtHostedTypes.RelativePattern; +mockedVSCode.ProgressLocation = vscodeMocks.vscMockExtHostedTypes.ProgressLocation; +mockedVSCode.ViewColumn = vscodeMocks.vscMockExtHostedTypes.ViewColumn; +mockedVSCode.TextEditorRevealType = vscodeMocks.vscMockExtHostedTypes.TextEditorRevealType; +mockedVSCode.TreeItem = vscodeMocks.vscMockExtHostedTypes.TreeItem; +mockedVSCode.TreeItemCollapsibleState = vscodeMocks.vscMockExtHostedTypes.TreeItemCollapsibleState; +mockedVSCode.CodeActionKind = vscodeMocks.vscMock.CodeActionKind; +mockedVSCode.DebugAdapterExecutable = vscodeMocks.vscMock.DebugAdapterExecutable; +mockedVSCode.DebugAdapterServer = vscodeMocks.vscMock.DebugAdapterServer; +mockedVSCode.QuickInputButtons = vscodeMocks.vscMockExtHostedTypes.QuickInputButtons; +mockedVSCode.FileType = vscodeMocks.vscMock.FileType; +mockedVSCode.FileSystemError = vscodeMocks.vscMockExtHostedTypes.FileSystemError; +(mockedVSCode as any).CellKind = vscodeMocks.vscMockExtHostedTypes.CellKind; +(mockedVSCode as any).CellOutputKind = vscodeMocks.vscMockExtHostedTypes.CellOutputKind; +(mockedVSCode as any).NotebookCellRunState = vscodeMocks.vscMockExtHostedTypes.NotebookCellRunState; + +// This API is used in src/client/telemetry/telemetry.ts +const extensions = TypeMoq.Mock.ofType<typeof vscode.extensions>(); +extensions.setup((e) => e.all).returns(() => []); +const extension = TypeMoq.Mock.ofType<vscode.Extension<any>>(); +const packageJson = TypeMoq.Mock.ofType<any>(); +const contributes = TypeMoq.Mock.ofType<any>(); +extension.setup((e) => e.packageJSON).returns(() => packageJson.object); +packageJson.setup((p) => p.contributes).returns(() => contributes.object); +contributes.setup((p) => p.debuggers).returns(() => [{ aiKey: '' }]); +extensions.setup((e) => e.getExtension(TypeMoq.It.isAny())).returns(() => extension.object); +mockedVSCode.extensions = extensions.object; + +function generateNotebookMocks() { + const mockedObj = TypeMoq.Mock.ofType<{}>(); + (mockedVSCode as any).notebook = mockedObj.object; + (mockedVSCodeNamespaces as any).notebook = mockedObj as any; +} diff --git a/src/test/workspaceSymbols/common.ts b/src/test/workspaceSymbols/common.ts new file mode 100644 index 000000000000..b9635d0d139d --- /dev/null +++ b/src/test/workspaceSymbols/common.ts @@ -0,0 +1,12 @@ +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { PythonSettings } from '../../client/common/configSettings'; + +export async function enableDisableWorkspaceSymbols( + resource: Uri, + enabled: boolean, + configTarget: ConfigurationTarget +) { + const settings = workspace.getConfiguration('python', resource); + await settings.update('workspaceSymbols.enabled', enabled, configTarget); + PythonSettings.dispose(); +} diff --git a/src/test/workspaceSymbols/generator.unit.test.ts b/src/test/workspaceSymbols/generator.unit.test.ts new file mode 100644 index 000000000000..6bfa1d77301a --- /dev/null +++ b/src/test/workspaceSymbols/generator.unit.test.ts @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { Uri } from 'vscode'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { IApplicationShell } from '../../client/common/application/types'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../client/common/platform/types'; +import { ProcessService } from '../../client/common/process/proc'; +import { IProcessService, IProcessServiceFactory, Output } from '../../client/common/process/types'; +import { IConfigurationService, IOutputChannel, IPythonSettings } from '../../client/common/types'; +import { Generator } from '../../client/workspaceSymbols/generator'; +use(chaiAsPromised); + +// tslint:disable-next-line:max-func-body-length +suite('Workspace Symbols Generator', () => { + let configurationService: IConfigurationService; + let pythonSettings: typemoq.IMock<IPythonSettings>; + let generator: Generator; + let factory: typemoq.IMock<IProcessServiceFactory>; + let shell: IApplicationShell; + let processService: IProcessService; + let fs: IFileSystem; + const folderUri = Uri.parse(path.join('a', 'b', 'c')); + setup(() => { + pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); + configurationService = mock(ConfigurationService); + factory = typemoq.Mock.ofType<IProcessServiceFactory>(); + shell = mock(ApplicationShell); + fs = mock(FileSystem); + processService = mock(ProcessService); + factory.setup((f) => f.create(typemoq.It.isAny())).returns(() => Promise.resolve(instance(processService))); + when(configurationService.getSettings(anything())).thenReturn(pythonSettings.object); + const outputChannel = typemoq.Mock.ofType<IOutputChannel>(); + generator = new Generator( + folderUri, + outputChannel.object, + instance(shell), + instance(fs), + factory.object, + instance(configurationService) + ); + }); + test('should be disabled', () => { + const workspaceSymbols = { enabled: false } as any; + pythonSettings.setup((p) => p.workspaceSymbols).returns(() => workspaceSymbols); + + expect(generator.enabled).to.be.equal(false, 'not disabled'); + }); + test('should be enabled', () => { + const workspaceSymbols = { enabled: true } as any; + pythonSettings.setup((p) => p.workspaceSymbols).returns(() => workspaceSymbols); + + expect(generator.enabled).to.be.equal(true, 'not enabled'); + }); + test('Check tagFilePath', () => { + const workspaceSymbols = { tagFilePath: '1234' } as any; + pythonSettings.setup((p) => p.workspaceSymbols).returns(() => workspaceSymbols); + + expect(generator.tagFilePath).to.be.equal('1234'); + }); + test('Throw error when generating tags', async () => { + const ctagsPath = 'CTAG_PATH'; + const workspaceSymbols = { + enabled: true, + tagFilePath: '1234', + exclusionPatterns: [], + ctagsPath + } as any; + pythonSettings.setup((p) => p.workspaceSymbols).returns(() => workspaceSymbols); + when(fs.directoryExists(anything())).thenResolve(true); + const observable = { + out: { + subscribe: (cb: (out: Output<string>) => void, _errorCb: any, done: Function) => { + cb({ source: 'stderr', out: 'KABOOM' }); + done(); + } + } + }; + when(processService.execObservable(ctagsPath, anything(), anything())).thenReturn(observable as any); + + const promise = generator.generateWorkspaceTags(); + await expect(promise).to.eventually.be.rejectedWith('KABOOM'); + verify(shell.setStatusBarMessage(anything(), anything())).once(); + }); + test('Does not throw error when generating tags', async () => { + const ctagsPath = 'CTAG_PATH'; + const workspaceSymbols = { + enabled: true, + tagFilePath: '1234', + exclusionPatterns: [], + ctagsPath + } as any; + pythonSettings.setup((p) => p.workspaceSymbols).returns(() => workspaceSymbols); + when(fs.directoryExists(anything())).thenResolve(true); + const observable = { + out: { + subscribe: (cb: (out: Output<string>) => void, _errorCb: any, done: Function) => { + cb({ source: 'stdout', out: '' }); + done(); + } + } + }; + when(processService.execObservable(ctagsPath, anything(), anything())).thenReturn(observable as any); + + await generator.generateWorkspaceTags(); + verify(shell.setStatusBarMessage(anything(), anything())).once(); + }); +}); diff --git a/src/test/workspaceSymbols/main.unit.test.ts b/src/test/workspaceSymbols/main.unit.test.ts new file mode 100644 index 000000000000..35b8f101d2d7 --- /dev/null +++ b/src/test/workspaceSymbols/main.unit.test.ts @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { anyString, anything, instance, mock, reset, verify, when } from 'ts-mockito'; +import { EventEmitter, TextDocument, Uri, WorkspaceFolder } from 'vscode'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService +} from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { ConfigurationService } from '../../client/common/configuration/service'; +import { Commands, STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../client/common/platform/types'; +import { ProcessService } from '../../client/common/process/proc'; +import { ProcessServiceFactory } from '../../client/common/process/processFactory'; +import { IProcessService, IProcessServiceFactory, Output } from '../../client/common/process/types'; +import { IConfigurationService, IOutputChannel } from '../../client/common/types'; +import { sleep } from '../../client/common/utils/async'; +import { ServiceContainer } from '../../client/ioc/container'; +import { IServiceContainer } from '../../client/ioc/types'; +import { WorkspaceSymbols } from '../../client/workspaceSymbols/main'; +import { MockOutputChannel } from '../mockClasses'; + +use(chaiAsPromised); + +// tslint:disable: no-any +// tslint:disable-next-line: max-func-body-length +suite('Workspace symbols main', () => { + const mockDisposable = { + dispose: () => { + return; + } + }; + const ctagsPath = 'CTAG_PATH'; + const observable = { + out: { + subscribe: (cb: (out: Output<string>) => void, _errorCb: any, done: Function) => { + cb({ source: 'stdout', out: '' }); + done(); + } + } + }; + + let outputChannel: IOutputChannel; + let commandManager: ICommandManager; + let fileSystem: IFileSystem; + let workspaceService: IWorkspaceService; + let processServiceFactory: IProcessServiceFactory; + let processService: IProcessService; + let applicationShell: IApplicationShell; + let configurationService: IConfigurationService; + let documentManager: IDocumentManager; + let serviceContainer: IServiceContainer; + let workspaceFolders: WorkspaceFolder[]; + let workspaceSymbols: WorkspaceSymbols; + let shellOutput: string; + let eventEmitter: EventEmitter<TextDocument>; + + setup(() => { + eventEmitter = new EventEmitter<TextDocument>(); + shellOutput = ''; + workspaceFolders = [{ name: 'root', index: 0, uri: Uri.file('folder') }]; + + outputChannel = mock(MockOutputChannel); + commandManager = mock(CommandManager); + fileSystem = mock(FileSystem); + workspaceService = mock(WorkspaceService); + processServiceFactory = mock(ProcessServiceFactory); + processService = mock(ProcessService); + applicationShell = mock(ApplicationShell); + configurationService = mock(ConfigurationService); + documentManager = mock(DocumentManager); + serviceContainer = mock(ServiceContainer); + + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(() => mockDisposable as any); + when(documentManager.onDidSaveTextDocument).thenReturn(eventEmitter.event); + when(commandManager.registerCommand(anything(), anything())).thenReturn(mockDisposable as any); + when(fileSystem.directoryExists(anything())).thenResolve(true); + when(fileSystem.fileExists(anything())).thenResolve(false); + when(processServiceFactory.create()).thenResolve(instance(processService)); + when(processService.execObservable(ctagsPath, anything(), anything())).thenReturn(observable as any); + when(applicationShell.setStatusBarMessage(anyString(), anything())).thenCall((text: string) => { + shellOutput += text; + return mockDisposable; + }); + + when(serviceContainer.get<IOutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL)).thenReturn( + instance(outputChannel) + ); + when(serviceContainer.get<ICommandManager>(ICommandManager)).thenReturn(instance(commandManager)); + when(serviceContainer.get<IFileSystem>(IFileSystem)).thenReturn(instance(fileSystem)); + when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspaceService)); + when(serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory)).thenReturn( + instance(processServiceFactory) + ); + when(serviceContainer.get<IApplicationShell>(IApplicationShell)).thenReturn(instance(applicationShell)); + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn( + instance(configurationService) + ); + when(serviceContainer.get<IDocumentManager>(IDocumentManager)).thenReturn(instance(documentManager)); + }); + + teardown(() => { + workspaceSymbols.dispose(); + }); + + test('Should not rebuild on start if the setting is disabled', () => { + when(workspaceService.workspaceFolders).thenReturn(workspaceFolders); + when(configurationService.getSettings(anything())).thenReturn({ + workspaceSymbols: { rebuildOnStart: false } + } as any); + + workspaceSymbols = new WorkspaceSymbols(instance(serviceContainer)); + + assert.equal(shellOutput, ''); + }); + + test("Should not rebuild on start if we don't have a workspace folder", () => { + when(workspaceService.workspaceFolders).thenReturn([]); + when(configurationService.getSettings(anything())).thenReturn({ + workspaceSymbols: { rebuildOnStart: false } + } as any); + + workspaceSymbols = new WorkspaceSymbols(instance(serviceContainer)); + + assert.equal(shellOutput, ''); + }); + + test('Should rebuild on start if the setting is enabled and we have a workspace folder', async () => { + when(workspaceService.workspaceFolders).thenReturn(workspaceFolders); + when(configurationService.getSettings(anything())).thenReturn({ + workspaceSymbols: { + ctagsPath, + enabled: true, + exclusionPatterns: [], + rebuildOnStart: true, + tagFilePath: 'foo' + } + } as any); + + workspaceSymbols = new WorkspaceSymbols(instance(serviceContainer)); + await sleep(1); + + assert.equal(shellOutput, 'Generating Tags'); + }); + + test('Should rebuild on save if the setting is enabled', async () => { + when(workspaceService.workspaceFolders).thenReturn(workspaceFolders); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(workspaceFolders[0]); + when(configurationService.getSettings(anything())).thenReturn({ + workspaceSymbols: { + ctagsPath, + enabled: true, + exclusionPatterns: [], + rebuildOnFileSave: true, + tagFilePath: 'foo' + } + } as any); + + workspaceSymbols = new WorkspaceSymbols(instance(serviceContainer)); + eventEmitter.fire({ uri: Uri.file('folder') } as any); + await sleep(1); + + assert.equal(shellOutput, 'Generating Tags'); + }); + + test('Command `Build Workspace symbols` is registered with the correct callback handlers and executing it returns `undefined` list if generating workspace tags fails with error', async () => { + let buildWorkspaceSymbolsHandler!: Function; + when(workspaceService.workspaceFolders).thenReturn(workspaceFolders); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(workspaceFolders[0]); + when(configurationService.getSettings(anything())).thenReturn({ + workspaceSymbols: { + ctagsPath, + enabled: true, + exclusionPatterns: [], + rebuildOnFileSave: true, + tagFilePath: 'foo' + } + } as any); + reset(commandManager); + when(commandManager.registerCommand(anything(), anything())).thenCall((commandID, cb) => { + expect(commandID).to.equal(Commands.Build_Workspace_Symbols); + buildWorkspaceSymbolsHandler = cb; + return mockDisposable; + }); + reset(applicationShell); + when(applicationShell.setStatusBarMessage(anyString(), anything())).thenThrow( + new Error('Generating workspace tags failed with Error') + ); + + workspaceSymbols = new WorkspaceSymbols(instance(serviceContainer)); + expect(buildWorkspaceSymbolsHandler).to.not.equal(undefined, 'Handler not registered'); + const symbols = await buildWorkspaceSymbolsHandler(); + + verify(commandManager.registerCommand(anything(), anything())).once(); + assert.deepEqual(symbols, [undefined]); + }); + + test('Should not rebuild on save if the setting is disabled', () => { + when(workspaceService.workspaceFolders).thenReturn(workspaceFolders); + when(configurationService.getSettings(anything())).thenReturn({ + workspaceSymbols: { + ctagsPath, + enabled: true, + exclusionPatterns: [], + rebuildOnFileSave: false, + tagFilePath: 'foo' + } + } as any); + + workspaceSymbols = new WorkspaceSymbols(instance(serviceContainer)); + + assert.equal(shellOutput, ''); + }); +}); diff --git a/src/test/workspaceSymbols/provider.unit.test.ts b/src/test/workspaceSymbols/provider.unit.test.ts new file mode 100644 index 000000000000..3cd753443a5f --- /dev/null +++ b/src/test/workspaceSymbols/provider.unit.test.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import * as assert from 'assert'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { CancellationTokenSource, Uri } from 'vscode'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { ICommandManager } from '../../client/common/application/types'; +import { Commands } from '../../client/common/constants'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../client/common/platform/types'; +import { Generator } from '../../client/workspaceSymbols/generator'; +import { WorkspaceSymbolProvider } from '../../client/workspaceSymbols/provider'; +use(chaiAsPromised); + +const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); + +// tslint:disable-next-line:max-func-body-length +suite('Workspace Symbols Provider', () => { + let generator: Generator; + let fs: IFileSystem; + let commandManager: ICommandManager; + setup(() => { + fs = mock(FileSystem); + commandManager = mock(CommandManager); + generator = mock(Generator); + }); + test('Returns 0 tags without any generators', async () => { + const provider = new WorkspaceSymbolProvider(instance(fs), instance(commandManager), []); + + const tags = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + + expect(tags).to.be.lengthOf(0); + }); + test("Builds tags when a tag file doesn't exist", async () => { + const provider = new WorkspaceSymbolProvider(instance(fs), instance(commandManager), [instance(generator)]); + const tagFilePath = 'No existing tagFilePath'; + when(generator.tagFilePath).thenReturn(tagFilePath); + when(fs.fileExists(tagFilePath)).thenResolve(false); + when(commandManager.executeCommand(Commands.Build_Workspace_Symbols, true, anything())).thenResolve(); + + const tags = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + + expect(tags).to.be.lengthOf(0); + verify(commandManager.executeCommand(Commands.Build_Workspace_Symbols, true, anything())).once(); + }); + test("Builds tags when a tag file doesn't exist", async () => { + const provider = new WorkspaceSymbolProvider(instance(fs), instance(commandManager), [instance(generator)]); + const tagFilePath = 'No existing tagFilePath'; + when(generator.tagFilePath).thenReturn(tagFilePath); + when(fs.fileExists(tagFilePath)).thenResolve(false); + when(commandManager.executeCommand(Commands.Build_Workspace_Symbols, true, anything())).thenResolve(); + + const tags = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + + expect(tags).to.be.lengthOf(0); + verify(commandManager.executeCommand(Commands.Build_Workspace_Symbols, true, anything())).once(); + }); + test('Symbols should not be returned when disabled', async () => { + const provider = new WorkspaceSymbolProvider(instance(fs), instance(commandManager), [instance(generator)]); + const tagFilePath = 'existing tagFilePath'; + when(generator.tagFilePath).thenReturn(tagFilePath); + when(generator.enabled).thenReturn(false); + when(fs.fileExists(tagFilePath)).thenResolve(true); + when(commandManager.executeCommand(Commands.Build_Workspace_Symbols, true, anything())).thenResolve(); + + const tags = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + + expect(tags).to.be.lengthOf(0); + verify(commandManager.executeCommand(Commands.Build_Workspace_Symbols, true, anything())).never(); + }); + test('symbols should be returned when enabeld and vice versa', async () => { + const provider = new WorkspaceSymbolProvider(instance(fs), instance(commandManager), [instance(generator)]); + const tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags'); + when(generator.tagFilePath).thenReturn(tagFilePath); + when(generator.workspaceFolder).thenReturn(workspaceUri); + when(generator.enabled).thenReturn(true); + when(fs.fileExists(tagFilePath)).thenResolve(true); + when(commandManager.executeCommand(Commands.Build_Workspace_Symbols, true, anything())).thenResolve(); + + const tags = await provider.provideWorkspaceSymbols('', new CancellationTokenSource().token); + + expect(tags).to.be.lengthOf(100); + verify(commandManager.executeCommand(Commands.Build_Workspace_Symbols, true, anything())).never(); + }); + test('symbols should be filtered correctly', async () => { + const provider = new WorkspaceSymbolProvider(instance(fs), instance(commandManager), [instance(generator)]); + const tagFilePath = path.join(workspaceUri.fsPath, '.vscode', 'tags'); + when(generator.tagFilePath).thenReturn(tagFilePath); + when(generator.workspaceFolder).thenReturn(workspaceUri); + when(generator.enabled).thenReturn(true); + when(fs.fileExists(tagFilePath)).thenResolve(true); + when(commandManager.executeCommand(Commands.Build_Workspace_Symbols, true, anything())).thenResolve(); + + const symbols = await provider.provideWorkspaceSymbols('meth1Of', new CancellationTokenSource().token); + + expect(symbols).to.be.length.greaterThan(0); + verify(commandManager.executeCommand(Commands.Build_Workspace_Symbols, true, anything())).never(); + + assert.equal(symbols.length >= 2, true, 'Incorrect number of symbols returned'); + assert.notEqual( + symbols.findIndex((sym) => sym.location.uri.fsPath.endsWith('childFile.py')), + -1, + 'File with symbol not found in child workspace folder' + ); + assert.notEqual( + symbols.findIndex((sym) => sym.location.uri.fsPath.endsWith('workspace2File.py')), + -1, + 'File with symbol not found in child workspace folder' + ); + + const symbolsForMeth = await provider.provideWorkspaceSymbols('meth', new CancellationTokenSource().token); + assert.equal(symbolsForMeth.length >= 10, true, 'Incorrect number of symbols returned'); + assert.notEqual( + symbolsForMeth.findIndex((sym) => sym.location.uri.fsPath.endsWith('childFile.py')), + -1, + 'Symbols not returned for childFile.py' + ); + assert.notEqual( + symbolsForMeth.findIndex((sym) => sym.location.uri.fsPath.endsWith('workspace2File.py')), + -1, + 'Symbols not returned for workspace2File.py' + ); + assert.notEqual( + symbolsForMeth.findIndex((sym) => sym.location.uri.fsPath.endsWith('file.py')), + -1, + 'Symbols not returned for file.py' + ); + }); +}); diff --git a/src/testMultiRootWkspc/disableLinters/.vscode/tags b/src/testMultiRootWkspc/disableLinters/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/testMultiRootWkspc/disableLinters/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/testMultiRootWkspc/disableLinters/file.py b/src/testMultiRootWkspc/disableLinters/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/disableLinters/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace new file mode 100644 index 000000000000..9d64c3f8b53b --- /dev/null +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -0,0 +1,50 @@ +{ + "folders": [ + { + "path": "workspace1" + }, + { + "path": "workspace2" + }, + { + "path": "workspace3" + }, + { + "path": "workspace4" + }, + { + "path": "workspace5" + }, + { + "path": "smokeTests" + }, + { + "path": "parent\\child" + }, + { + "path": "disableLinters" + }, + { + "path": "../test" + } + ], + "settings": { + "python.linting.flake8Enabled": false, + "python.linting.banditEnabled": false, + "python.linting.mypyEnabled": false, + "python.linting.pydocstyleEnabled": false, + "python.linting.pylamaEnabled": false, + "python.linting.pylintEnabled": true, + "python.linting.pycodestyleEnabled": false, + "python.linting.prospectorEnabled": false, + "python.workspaceSymbols.enabled": false, + "python.formatting.provider": "yapf", + "python.sortImports.args": [ + "-sp", + "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/sorting/withconfig" + ], + "python.linting.lintOnSave": false, + "python.linting.enabled": true, + "python.pythonPath": "python" + } +} diff --git a/src/testMultiRootWkspc/parent/child/.vscode/settings.json b/src/testMultiRootWkspc/parent/child/.vscode/settings.json new file mode 100644 index 000000000000..2c4fa010619f --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.enabled": false +} diff --git a/src/testMultiRootWkspc/parent/child/.vscode/tags b/src/testMultiRootWkspc/parent/child/.vscode/tags new file mode 100644 index 000000000000..e6791c755b0f --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/.vscode/tags @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Child2Class ..\\childFile.py /^class Child2Class(object):$/;" kind:class line:5 +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\childFile.py /^ def __init__(self):$/;" kind:member line:8 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\childFile.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +childFile.py ..\\childFile.py 1;" kind:file line:1 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfChild ..\\childFile.py /^ def meth1OfChild(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/testMultiRootWkspc/parent/child/childFile.py b/src/testMultiRootWkspc/parent/child/childFile.py new file mode 100644 index 000000000000..31d6fc7b4a18 --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/childFile.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Child2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfChild(self, arg): + """this issues a message""" + print (self) diff --git a/src/testMultiRootWkspc/parent/child/file.py b/src/testMultiRootWkspc/parent/child/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/parent/child/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/smokeTests/.gitignore b/src/testMultiRootWkspc/smokeTests/.gitignore new file mode 100644 index 000000000000..9e87779bf2e4 --- /dev/null +++ b/src/testMultiRootWkspc/smokeTests/.gitignore @@ -0,0 +1,2 @@ +.vscode/** +*.log diff --git a/src/testMultiRootWkspc/smokeTests/definitions.py b/src/testMultiRootWkspc/smokeTests/definitions.py new file mode 100644 index 000000000000..a8379a49f960 --- /dev/null +++ b/src/testMultiRootWkspc/smokeTests/definitions.py @@ -0,0 +1,31 @@ +from contextlib import contextmanager + +def my_decorator(fn): + """ + This is my decorator. + """ + def wrapper(*args, **kwargs): + """ + This is the wrapper. + """ + return 42 + return wrapper + +@my_decorator +def thing(arg): + """ + Thing which is decorated. + """ + pass + +@contextmanager +def my_context_manager(): + """ + This is my context manager. + """ + print("before") + yield + print("after") + +with my_context_manager(): + thing(19) diff --git a/src/testMultiRootWkspc/smokeTests/testExecInTerminal.py b/src/testMultiRootWkspc/smokeTests/testExecInTerminal.py new file mode 100644 index 000000000000..d83d46a740d9 --- /dev/null +++ b/src/testMultiRootWkspc/smokeTests/testExecInTerminal.py @@ -0,0 +1,16 @@ +import getopt +import sys +import os + +optlist, args = getopt.getopt(sys.argv, '') + +# If the caller has not specified the output file, create one for them with +# the same name as the caller script, but with a .log extension. +log_file = os.path.splitext(sys.argv[0])[0] + '.log' + +# If the output file is given, use that instead. +if len(args) == 2: + log_file = args[1] + +with open(log_file, "a") as f: + f.write(sys.executable) diff --git a/src/testMultiRootWkspc/workspace1/.vscode/settings.json b/src/testMultiRootWkspc/workspace1/.vscode/settings.json new file mode 100644 index 000000000000..1e5ea7556081 --- /dev/null +++ b/src/testMultiRootWkspc/workspace1/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.linting.enabled": true +} diff --git a/src/testMultiRootWkspc/workspace1/.vscode/tags b/src/testMultiRootWkspc/workspace1/.vscode/tags new file mode 100644 index 000000000000..4739b4629cfb --- /dev/null +++ b/src/testMultiRootWkspc/workspace1/.vscode/tags @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo ..\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ ..\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ ..\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py ..\\file.py 1;" kind:file line:1 +meth1 ..\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 ..\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 ..\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 ..\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 ..\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 ..\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 ..\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 ..\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/testMultiRootWkspc/workspace1/file.py b/src/testMultiRootWkspc/workspace1/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/workspace1/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/workspace2/.vscode/settings.json b/src/testMultiRootWkspc/workspace2/.vscode/settings.json new file mode 100644 index 000000000000..3705457b09a7 --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceFolder}/workspace2.tags.file", + "python.workspaceSymbols.enabled": false +} diff --git a/src/testMultiRootWkspc/workspace2/file.py b/src/testMultiRootWkspc/workspace2/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/workspace2/workspace2.tags.file b/src/testMultiRootWkspc/workspace2/workspace2.tags.file new file mode 100644 index 000000000000..375785e2a94e --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/workspace2.tags.file @@ -0,0 +1,24 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^class Foo(object):$/;" kind:class line:5 +Workspace2Class C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^class Workspace2Class(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def __init__(self):$/;" kind:member line:8 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^__revision__ = None$/;" kind:variable line:3 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth1OfWorkspace2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py /^ def meth1OfWorkspace2(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\file.py /^ def meth8(self):$/;" kind:member line:80 +workspace2File.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace2\\workspace2File.py 1;" kind:file line:1 diff --git a/src/testMultiRootWkspc/workspace2/workspace2File.py b/src/testMultiRootWkspc/workspace2/workspace2File.py new file mode 100644 index 000000000000..61aa87c55fed --- /dev/null +++ b/src/testMultiRootWkspc/workspace2/workspace2File.py @@ -0,0 +1,13 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Workspace2Class(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1OfWorkspace2(self, arg): + """this issues a message""" + print (self) diff --git a/src/testMultiRootWkspc/workspace3/.vscode/settings.json b/src/testMultiRootWkspc/workspace3/.vscode/settings.json new file mode 100644 index 000000000000..8779a0c08efe --- /dev/null +++ b/src/testMultiRootWkspc/workspace3/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.workspaceSymbols.tagFilePath": "${workspaceRoot}/workspace3.tags.file" +} diff --git a/src/testMultiRootWkspc/workspace3/file.py b/src/testMultiRootWkspc/workspace3/file.py new file mode 100644 index 000000000000..439f899e9e22 --- /dev/null +++ b/src/testMultiRootWkspc/workspace3/file.py @@ -0,0 +1,87 @@ +"""pylint option block-disable""" + +__revision__ = None + +class Foo(object): + """block-disable test""" + + def __init__(self): + pass + + def meth1(self, arg): + """this issues a message""" + print self + + def meth2(self, arg): + """and this one not""" + # pylint: disable=unused-argument + print self\ + + "foo" + + def meth3(self): + """test one line disabling""" + # no error + print self.bla # pylint: disable=no-member + # error + print self.blop + + def meth4(self): + """test re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + print self.blop + # pylint: enable=no-member + # error + print self.blip + + def meth5(self): + """test IF sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + if self.blop: + # pylint: enable=no-member + # error + print self.blip + else: + # no error + print self.blip + # no error + print self.blip + + def meth6(self): + """test TRY/EXCEPT sub-block re-enabling""" + # pylint: disable=no-member + # no error + print self.bla + try: + # pylint: enable=no-member + # error + print self.blip + except UndefinedName: # pylint: disable=undefined-variable + # no error + print self.blip + # no error + print self.blip + + def meth7(self): + """test one line block opening disabling""" + if self.blop: # pylint: disable=no-member + # error + print self.blip + else: + # error + print self.blip + # error + print self.blip + + + def meth8(self): + """test late disabling""" + # error + print self.blip + # pylint: disable=no-member + # no error + print self.bla + print self.blop diff --git a/src/testMultiRootWkspc/workspace3/workspace3.tags.file b/src/testMultiRootWkspc/workspace3/workspace3.tags.file new file mode 100644 index 000000000000..3a65841e2aff --- /dev/null +++ b/src/testMultiRootWkspc/workspace3/workspace3.tags.file @@ -0,0 +1,19 @@ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 0.0.0 /f9e6e3c1/ +Foo C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^class Foo(object):$/;" kind:class line:5 +__init__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def __init__(self):$/;" kind:member line:8 +__revision__ C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^__revision__ = None$/;" kind:variable line:3 +file.py C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py 1;" kind:file line:1 +meth1 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth1(self, arg):$/;" kind:member line:11 +meth2 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth2(self, arg):$/;" kind:member line:15 +meth3 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth3(self):$/;" kind:member line:21 +meth4 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth4(self):$/;" kind:member line:28 +meth5 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth5(self):$/;" kind:member line:38 +meth6 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth6(self):$/;" kind:member line:53 +meth7 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth7(self):$/;" kind:member line:68 +meth8 C:\\Users\\dojayama\\.vscode\\extensions\\pythonVSCode\\src\\testMultiRootWkspc\\workspace3\\file.py /^ def meth8(self):$/;" kind:member line:80 diff --git a/src/testMultiRootWkspc/workspace4/.env b/src/testMultiRootWkspc/workspace4/.env new file mode 100644 index 000000000000..0ae33fd6779f --- /dev/null +++ b/src/testMultiRootWkspc/workspace4/.env @@ -0,0 +1,2 @@ +X1234PYEXTUNITTESTVAR=1234 +PYTHONPATH=../workspace5 diff --git a/src/testMultiRootWkspc/workspace4/.env2 b/src/testMultiRootWkspc/workspace4/.env2 new file mode 100644 index 000000000000..c3152625b1a0 --- /dev/null +++ b/src/testMultiRootWkspc/workspace4/.env2 @@ -0,0 +1 @@ +X12345PYEXTUNITTESTVAR=12345 diff --git a/src/testMultiRootWkspc/workspace4/.env5 b/src/testMultiRootWkspc/workspace4/.env5 new file mode 100644 index 000000000000..5a5fd7c1816c --- /dev/null +++ b/src/testMultiRootWkspc/workspace4/.env5 @@ -0,0 +1,7 @@ +X=1 +Y=2 +PYTHONPATH=/usr/one/three:/usr/one/four +# Unix PATH variable +PATH=/usr/x:/usr/y +# Windows Path variable +Path=/usr/x:/usr/y diff --git a/src/testMultiRootWkspc/workspace4/.env6 b/src/testMultiRootWkspc/workspace4/.env6 new file mode 100644 index 000000000000..76459c0f68cc --- /dev/null +++ b/src/testMultiRootWkspc/workspace4/.env6 @@ -0,0 +1,3 @@ +REPO=/home/user/git/foobar +PYTHONPATH=${REPO}/foo:${REPO}/bar +PYTHON=${BINDIR}/python3 diff --git a/src/testMultiRootWkspc/workspace4/.vscode/settings.json b/src/testMultiRootWkspc/workspace4/.vscode/settings.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/src/testMultiRootWkspc/workspace4/.vscode/settings.json @@ -0,0 +1 @@ +{} diff --git a/src/testMultiRootWkspc/workspace4/one.py b/src/testMultiRootWkspc/workspace4/one.py new file mode 100644 index 000000000000..635f08868a11 --- /dev/null +++ b/src/testMultiRootWkspc/workspace4/one.py @@ -0,0 +1,2 @@ +from hello import world +print(world.sayHello()) diff --git a/src/testMultiRootWkspc/workspace5/.vscode/settings.json b/src/testMultiRootWkspc/workspace5/.vscode/settings.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/.vscode/settings.json @@ -0,0 +1 @@ +{} diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/__init__.py b/src/testMultiRootWkspc/workspace5/djangoApp/home/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/templates/index.html b/src/testMultiRootWkspc/workspace5/djangoApp/home/templates/index.html new file mode 100644 index 000000000000..6ca5107d23d6 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/home/templates/index.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> + <body> + + <h1>Hello {{ value_from_server }}!</h1> + <h1>Hello {{ another_value_from_server }}!</h1> + + </body> +</html> diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/urls.py b/src/testMultiRootWkspc/workspace5/djangoApp/home/urls.py new file mode 100644 index 000000000000..70a9606e88e6 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/home/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url('', views.index, name='index'), +] diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/home/views.py b/src/testMultiRootWkspc/workspace5/djangoApp/home/views.py new file mode 100644 index 000000000000..0494f868dc6f --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/home/views.py @@ -0,0 +1,10 @@ +from django.shortcuts import render +from django.template import loader + + +def index(request): + context = { + 'value_from_server':'this_is_a_value_from_server', + 'another_value_from_server':'this_is_another_value_from_server' + } + return render(request, 'index.html', context) diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/manage.py b/src/testMultiRootWkspc/workspace5/djangoApp/manage.py new file mode 100644 index 000000000000..afbc784aafd8 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/__init__.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py new file mode 100644 index 000000000000..4e182517ca2a --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/settings.py @@ -0,0 +1,93 @@ +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 1.11.2. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.11/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '5u06*)07dvd+=kn)zqp8#b0^qt@*$8=nnjc&&0lzfc28(wns&l' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.contenttypes', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ +] + +ROOT_URLCONF = 'mysite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': ['home/templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.11/ref/settings/#databases + +DATABASES = { +} + + +# Password validation +# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.11/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.11/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/urls.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/urls.py new file mode 100644 index 000000000000..9db383365e3e --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/urls.py @@ -0,0 +1,23 @@ +"""mysite URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.11/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url, include +from django.contrib import admin +from django.views.generic import RedirectView + +urlpatterns = [ + url(r'^home/', include('home.urls')), + url('', RedirectView.as_view(url='/home/')), +] diff --git a/src/testMultiRootWkspc/workspace5/djangoApp/mysite/wsgi.py b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/wsgi.py new file mode 100644 index 000000000000..74e7daeefe76 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/djangoApp/mysite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for mysite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings") + +application = get_wsgi_application() diff --git a/src/testMultiRootWkspc/workspace5/flaskApp/run.py b/src/testMultiRootWkspc/workspace5/flaskApp/run.py new file mode 100644 index 000000000000..9c3172c3e918 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/flaskApp/run.py @@ -0,0 +1,13 @@ +from flask import Flask, render_template +app = Flask(__name__) + + +@app.route('/') +def hello(): + return render_template('index.html', + value_from_server='this_is_a_value_from_server', + another_value_from_server='this_is_another_value_from_server') + + +if __name__ == '__main__': + app.run() diff --git a/src/testMultiRootWkspc/workspace5/flaskApp/templates/index.html b/src/testMultiRootWkspc/workspace5/flaskApp/templates/index.html new file mode 100644 index 000000000000..6ca5107d23d6 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/flaskApp/templates/index.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> + <body> + + <h1>Hello {{ value_from_server }}!</h1> + <h1>Hello {{ another_value_from_server }}!</h1> + + </body> +</html> diff --git a/src/testMultiRootWkspc/workspace5/hello/__init__.py b/src/testMultiRootWkspc/workspace5/hello/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/testMultiRootWkspc/workspace5/hello/world.py b/src/testMultiRootWkspc/workspace5/hello/world.py new file mode 100644 index 000000000000..553782e40de3 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/hello/world.py @@ -0,0 +1,2 @@ +def sayHello(): + return "Hello" diff --git a/src/testMultiRootWkspc/workspace5/mymod/__init__.py b/src/testMultiRootWkspc/workspace5/mymod/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/testMultiRootWkspc/workspace5/mymod/__main__.py b/src/testMultiRootWkspc/workspace5/mymod/__main__.py new file mode 100644 index 000000000000..f1a18139c84a --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/mymod/__main__.py @@ -0,0 +1 @@ +print("Hello world!") diff --git a/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-nowait.py b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-nowait.py new file mode 100644 index 000000000000..75d9766db981 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-nowait.py @@ -0,0 +1 @@ +print('hello world') diff --git a/src/testMultiRootWkspc/workspace5/remoteDebugger-start.py b/src/testMultiRootWkspc/workspace5/remoteDebugger-start.py new file mode 100644 index 000000000000..9fa815038e88 --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/remoteDebugger-start.py @@ -0,0 +1,17 @@ +import sys +import time + +def main(): + sys.stdout.write('this is stdout') + sys.stdout.flush() + sys.stderr.write('this is stderr') + sys.stderr.flush() + # Give the debugger some time to add a breakpoint. + time.sleep(5) + for i in range(1): + time.sleep(0.5) + pass + + print('this is print') + +main() diff --git a/src/testMultiRootWkspc/workspace5/remoteDebugger.py b/src/testMultiRootWkspc/workspace5/remoteDebugger.py new file mode 100644 index 000000000000..26ae431cc1cd --- /dev/null +++ b/src/testMultiRootWkspc/workspace5/remoteDebugger.py @@ -0,0 +1,17 @@ +import sys +import ptvsd +import time + +sys.stdout.write('start') +sys.stdout.flush() +address = ('127.0.0.1', int(sys.argv[1])) +ptvsd.enable_attach('super_secret', address) +ptvsd.wait_for_attach() + +sys.stdout.write('attached') +sys.stdout.flush() +# Give the debugger some time to add a breakpoint. +time.sleep(2) + +sys.stdout.write('end') +sys.stdout.flush() diff --git a/syntaxes/pip-requirements.tmLanguage.json b/syntaxes/pip-requirements.tmLanguage.json new file mode 100644 index 000000000000..ea0c69b19f65 --- /dev/null +++ b/syntaxes/pip-requirements.tmLanguage.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "pip requirements", + "scopeName": "source.pip-requirements", + "patterns": [ + { + "explanation": "Line continuation", + "match": "\\s*\\\\s*$", + "name": "constant.character.escape" + }, + { + "match": "#.*", + "name": "comment.line.number-sign" + }, + { + "begin": "'", + "end": "'", + "name": "string.quoted.single" + }, + { + "begin": "\"", + "end": "\"", + "name": "string.quoted.double" + }, + { + "match": "/?(\\S+/)+\\S*", + "name": "string.path" + }, + { + "explanation": "project name", + "match": "^\\s*([A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9]|[A-Za-z0-9])", + "captures": { + "1": { + "name": "entity.name.class" + } + } + }, + { + "explanation": "extras", + "match": "\\[([^\\]]+)\\]", + "captures": { + "1": { + "name": "entity.name.tag" + } + } + }, + { + "explanation": "version specification", + "match": "(<|<=|!=|==|>=|>|~=|===)\\s*([\\w.*+!-]+)", + "captures": { + "1": { + "name": "keyword.operator.comparison" + }, + "2": { + "name": "constant.numeric" + } + } + }, + { + "explanation": "environment markers", + "match": ";\\s*(python_version|python_full_version|os_name|sys_platform|platform_release|platform_system|platform_version|platform_machine|platform_python_implementation|implementation_name|implementation_version|extra)\\s*(<|<=|!=|==|>=|>|~=|===)", + "captures": { + "1": { + "name": "entity.name.selector" + }, + "2": { + "name": "keyword.operator.comparison" + } + } + }, + { + "explanation": "command-line options (e.g. `--no-links` or `-c`)", + "match": "-[^\\s=]+", + "name": "entity.other.attribute-name" + } + ] +} diff --git a/tsconfig.datascience-ui.json b/tsconfig.datascience-ui.json new file mode 100644 index 000000000000..d15dc2121b44 --- /dev/null +++ b/tsconfig.datascience-ui.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "module": "esnext", + "moduleResolution": "node", + "importHelpers": true, + "target": "es5", + "outDir": "out", + "lib": ["es6", "dom"], + "jsx": "react", + "sourceMap": true, + "rootDirs": ["node_modules/vsls", "src", "types"], + "paths": { + "*": ["types/*"] + }, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "noImplicitThis": false, + "noUnusedLocals": true, + "noUnusedParameters": false, + "strict": true + }, + "exclude": [".vscode-test", ".vscode test", "src/test", "src/server", "src/client", "build"] +} diff --git a/tsconfig.extension.json b/tsconfig.extension.json new file mode 100644 index 000000000000..d29570404119 --- /dev/null +++ b/tsconfig.extension.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "module": "commonjs", + "target": "es6", + "outDir": "out", + "lib": [ + "es6", + "es2018", + "ES2019" + ], + "jsx": "react", + "sourceMap": true, + "rootDir": "src", + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "noImplicitThis": false + }, + "exclude": [ + "node_modules", + ".vscode-test", + ".vscode test", + "src/datascience-ui", + "build" + ] +} diff --git a/tsconfig.json b/tsconfig.json index e81c5b71ee75..f1e8c222bfbc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,39 @@ { "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": ["types/*"] + }, "module": "commonjs", - "target": "es5", - "outDir": "./out", - "noLib": true, - "sourceMap": true + "target": "es2018", + "outDir": "out", + "lib": ["es6", "es2018", "dom", "ES2019"], + "jsx": "react", + "sourceMap": true, + "rootDir": "src", + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true }, "exclude": [ "node_modules", + ".vscode-test", + ".vscode test", "src/server/node_modules", "src/client/node_modules", "src/server/src/typings", - "src/client/src/typings" + "src/client/src/typings", + "src/ipywidgets", + "src/smoke", + "src/test/datascience/extensionapi", + "build", + "out", + "ipywidgets", + "tmp" ] -} \ No newline at end of file +} diff --git a/tsfmt.json b/tsfmt.json new file mode 100644 index 000000000000..6d9806a01c23 --- /dev/null +++ b/tsfmt.json @@ -0,0 +1,17 @@ +{ + "tabSize": 4, + "indentSize": 4, + "newLineCharacter": "\n", + "convertTabsToSpaces": false, + "insertSpaceAfterCommaDelimiter": true, + "insertSpaceAfterSemicolonInForStatements": true, + "insertSpaceBeforeAndAfterBinaryOperators": true, + "insertSpaceAfterKeywordsInControlFlowStatements": true, + "insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, + "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, + "insertSpaceBeforeFunctionParenthesis": false, + "placeOpenBraceOnNewLineForFunctions": false, + "placeOpenBraceOnNewLineForControlBlocks": false +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 000000000000..0b83af565aa2 --- /dev/null +++ b/tslint.json @@ -0,0 +1,57 @@ +{ + "rulesDirectory": ["./build/tslint-rules"], + "linterOptions": { + "exclude": ["src/ipywidgets"] + }, + "extends": ["tslint-eslint-rules", "tslint-microsoft-contrib", "tslint-plugin-prettier", "tslint-config-prettier"], + "rules": { + "prettier": true, + "messages-must-be-localized": true, + "no-unused-expression": true, + "no-duplicate-variable": true, + "curly": true, + "non-literal-fs-path": false, + "newline-per-chained-call": false, + "class-name": true, + "semicolon": [true, "always", "strict-bound-class-methods"], + "triple-equals": true, + "no-relative-imports": false, + "max-line-length": false, + "max-func-body-length": false, + "typedef": false, + "no-string-throw": true, + "missing-jsdoc": false, + "one-line": [true, "check-catch", "check-finally", "check-else"], + "no-parameter-properties": false, + "no-parameter-reassignment": false, + "no-reserved-keywords": false, + "newline-before-return": false, + "export-name": false, + "align": false, + "linebreak-style": false, + "strict-boolean-expressions": false, + "await-promise": [true, "Thenable", "PromiseLike"], + "completed-docs": false, + "no-unsafe-any": false, + "no-backbone-get-set-outside-model": false, + "underscore-consistent-invocation": false, + "no-void-expression": false, + "no-non-null-assertion": false, + "prefer-type-cast": false, + "promise-function-async": false, + "function-name": false, + "variable-name": false, + "no-import-side-effect": false, + "no-string-based-set-timeout": false, + "no-floating-promises": true, + "no-empty-interface": false, + "no-bitwise": false, + "eofline": true, + "switch-final-break": false, + "no-implicit-dependencies": ["vscode"], + "no-unnecessary-type-assertion": false, + "no-submodule-imports": false, + "no-redundant-jsdoc": false, + "binary-expression-operand-order": false + } +} diff --git a/types/@nteract/transform-dataresource.d.ts b/types/@nteract/transform-dataresource.d.ts new file mode 100644 index 000000000000..ac38b46b19d4 --- /dev/null +++ b/types/@nteract/transform-dataresource.d.ts @@ -0,0 +1,4 @@ +declare module '@nteract/transform-dataresource' { + export = index; + const index: any; +} diff --git a/types/@nteract/transform-geojson.d.ts b/types/@nteract/transform-geojson.d.ts new file mode 100644 index 000000000000..6993907366e2 --- /dev/null +++ b/types/@nteract/transform-geojson.d.ts @@ -0,0 +1,4 @@ +declare module '@nteract/transform-geojson' { + export = index; + const index: any; +} diff --git a/types/@nteract/transform-model-debug.d.ts b/types/@nteract/transform-model-debug.d.ts new file mode 100644 index 000000000000..c173dbbd65ab --- /dev/null +++ b/types/@nteract/transform-model-debug.d.ts @@ -0,0 +1,10 @@ +declare module '@nteract/transform-model-debug' { + export default class _default { + static MIMETYPE: string; + constructor(...args: any[]); + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(): any; + } +} diff --git a/types/@nteract/transform-plotly.d.ts b/types/@nteract/transform-plotly.d.ts new file mode 100644 index 000000000000..d647dcd7775c --- /dev/null +++ b/types/@nteract/transform-plotly.d.ts @@ -0,0 +1,26 @@ +declare module '@nteract/transform-plotly' { + export function PlotlyNullTransform(): any; + export namespace PlotlyNullTransform { + const MIMETYPE: string; + } + export class PlotlyTransform { + static MIMETYPE: string; + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + } + export default class _default { + static MIMETYPE: string; + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + } +} diff --git a/types/@nteract/transform-vsdom.d.ts b/types/@nteract/transform-vsdom.d.ts new file mode 100644 index 000000000000..d98c5141a4f6 --- /dev/null +++ b/types/@nteract/transform-vsdom.d.ts @@ -0,0 +1,12 @@ +declare module '@nteract/transform-vdom' { + export class VDOM { + static MIMETYPE: string; + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + } +} diff --git a/types/@nteract/transforms.d.ts b/types/@nteract/transforms.d.ts new file mode 100644 index 000000000000..846e5ef8267a --- /dev/null +++ b/types/@nteract/transforms.d.ts @@ -0,0 +1,106 @@ +declare module '@nteract/transforms' { + export class GIFTransform { + static MIMETYPE: string; + constructor(...args: any[]); + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + } + export class HTMLTransform { + static MIMETYPE: string; + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + } + export class JPEGTransform { + static MIMETYPE: string; + constructor(...args: any[]); + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + } + export class JSONTransform { + static MIMETYPE: string; + static defaultProps: { + data: {}; + metadata: {}; + theme: string; + }; + static handles(mimetype: any): any; + constructor(props: any); + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + shouldExpandNode(): any; + } + export class JavaScriptTransform { + static MIMETYPE: string; + static handles(mimetype: any): any; + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + } + export function LaTeXTransform(props: any, context: any): any; + export namespace LaTeXTransform { + const MIMETYPE: string; + namespace contextTypes { + function MathJax(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any; + namespace MathJax { + function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any; + } + function MathJaxContext(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any; + namespace MathJaxContext { + function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any; + } + } + } + export class MarkdownTransform { + static MIMETYPE: string; + constructor(...args: any[]); + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + } + export class PNGTransform { + static MIMETYPE: string; + constructor(...args: any[]); + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + } + export class SVGTransform { + static MIMETYPE: string; + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + } + export class TextTransform { + static MIMETYPE: string; + constructor(...args: any[]); + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + } + export const displayOrder: string[]; + export function registerTransform(_ref: any, transform: any): any; + export function richestMimetype(bundle: any, ...args: any[]): any; + export const standardDisplayOrder: string[]; + + export let standardTransforms: {}; + export namespace transforms {} +} diff --git a/types/ansi-to-html.d.ts b/types/ansi-to-html.d.ts new file mode 100644 index 000000000000..c35b0f3c8439 --- /dev/null +++ b/types/ansi-to-html.d.ts @@ -0,0 +1,10 @@ +declare module 'ansi-to-html' { + export = ansiToHtml; + class ansiToHtml { + constructor(options?: any); + opts: any; + stack: any; + stickyStack: any; + toHtml(input: any): any; + } +} diff --git a/types/react-data-grid.d.ts b/types/react-data-grid.d.ts new file mode 100644 index 000000000000..9f9c9fe87481 --- /dev/null +++ b/types/react-data-grid.d.ts @@ -0,0 +1,709 @@ +// Type definitions for react-data-grid 4.0 +// Project: https://github.com/adazzle/react-data-grid.git +// Definitions by: Simon Gellis <https://github.com/SupernaviX>, Kieran Peat <https://github.com/KieranPeat>, Martin Novak <https://github.com/martinnov92>, Sebastijan Grabar <https://github.com/baso53> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.8 + +// Copied here so that could fix this to work with an older version of React. + +/// <reference types="react" /> + +declare namespace AdazzleReactDataGrid { + interface ExcelColumn { + editable: boolean; + name: any; + key: string; + width: number; + resizeable: boolean; + filterable: boolean; + } + + interface EditorBaseProps { + value: any; + column: ExcelColumn; + height: number; + onBlur: () => void; + onCommit: () => void; + onCommitCancel: () => void; + rowData: any; + rowMetaData: any; + } + + interface SelectionParams<T> { + rowIdx: number; + row: T; + } + + interface GridProps<T> { + /** + * Gets the data to render in each row. Required. + * Can be an array or a function that takes an index and returns an object. + */ + rowGetter: Array<T> | ((rowIdx: number) => T) + /** + * The total number of rows to render. Required. + */ + rowsCount: number + /** + * The columns to render. + */ + columns?: Array<Column<T>> + + /** + * Invoked when the user changes the value of a single cell. + * Should update that cell's value. + * @param e Information about the event + */ + onRowUpdated?: (e: RowUpdateEvent<T>) => void + /** + * Invoked when the user pulls down the drag handle of an editable cell. + * Should update the values of the selected cells. + * @param e Information about the event + */ + onCellsDragged?: (e: CellDragEvent) => void + /** + * Invoked when the user double clicks on the drag handle of an editable cell. + * Should update the values of the cells beneath the selected cell. + * @param e Information about the event + */ + onDragHandleDoubleClick?: (e: DragHandleDoubleClickEvent<T>) => void + /** + * Invoked when the user copies a value from one cell and pastes it into another (in the same column). + * Should update the value of the cell in row e.toRow. + * @param e Information about the event + */ + onCellCopyPaste?: (e: CellCopyPasteEvent) => void + /** + * Invoked after the user updates the grid rows in any way. + * @param e Information about the event + */ + onGridRowsUpdated?: (e: GridRowsUpdatedEvent<T>) => void + + /** + * A toolbar to display above the grid. + * Consider using the toolbar included in "react-data-grid/addons". + */ + toolbar?: React.ReactElement<any> + /** + * A context menu to disiplay when the user right-clicks a cell. + * Consider using "react-contextmenu", included in "react-data-grid/addons". + */ + contextMenu?: React.ReactElement<any> + /** + * A react component to customize how rows are rendered. + * If you want to define your own, consider extending ReactDataGrid.Row. + */ + rowRenderer?: React.ReactElement<any> | React.ComponentClass<any> | React.StatelessComponent<any> + /** + * A component to display when there are no rows to render. + */ + emptyRowsView?: React.ComponentClass<any> | React.StatelessComponent<any> + + /** + * The minimum width of the entire grid in pixels. + */ + minWidth?: number + /** + * The minimum height of the entire grid in pixels. + * @default 350 + */ + minHeight?: number + /** + * The height of each individual row in pixels. + * @default 35 + */ + rowHeight?: number + /** + * The height of the header row in pixels. + * @default rowHeight + */ + headerRowHeight?: number + /** + * The height of the header filter row in pixels. + * @default 45 + */ + headerFiltersHeight?: number + /** + * The minimum width of each column in pixels. + * @default 80 + */ + minColumnWidth?: number + /** + * Invoked when a column has been resized. + * @param index The index of the column + * @param width The new width of the column + */ + onColumnResize?: (index: number, width: number) => void + + /** + * Controls what happens when the user navigates beyond the first or last cells. + * 'loopOverRow' will navigate to the beginning/end of the current row. + * 'changeRow' will navigate to the beginning of the next row or the end of the last. + * 'none' will do nothing. + * @default none + */ + cellNavigationMode?: 'none' | 'loopOverRow' | 'changeRow' + + /** + * Called when the user sorts the grid by some column. + * Should update the order of the rows returned by rowGetter. + * @param sortColumn The name of the column being sorted by + * @param sortDirection The direction to sort ('ASC'/'DESC'/'NONE') + */ + onGridSort?: (sortColumn: string, sortDirection: 'ASC' | 'DESC' | 'NONE') => void + + /** + * Initial sorting direction + */ + sortDirection?: 'ASC' | 'DESC' | 'NONE' + + /** + * key of the initial sorted column + */ + sortColumn?: string + + /** + * Called when the user filters a column by some value. + * Should restrict the rows in rowGetter to only things that match the filter. + * @param filter The filter being added + */ + onAddFilter?: (filter: Filter) => void + /** + * Called when the user clears all filters. + * Should restore the rows in rowGetter to their original state. + */ + onClearFilters?: () => void + + /** + * When set to true or 'multi', enables multiple row select. + * When set to 'single', enables single row select. + * When set to false or not set, disables row select. + * @default false + */ + enableRowSelect?: boolean | 'single' | 'multi' + /** + * Called when a row is selected. + * @param rows The (complete) current selection of rows. + */ + onRowSelect?: (rows: Array<T>) => void + /** + * A property that's unique to every row. + * This property is required to enable row selection. + * @default 'id' + */ + rowKey?: string + + /** + * Enables cells to be selected when clicked. + * @default false + */ + enableCellSelect?: boolean + + /** + * Enables cells to be dragged and dropped + * @default false + */ + enableDragAndDrop?: boolean + + /** + * Called when a cell is selected. + * @param coordinates The row and column indices of the selected cell. + */ + onCellSelected?: (coordinates: {rowIdx: number, idx: number}) => void + /** + * Called when a cell is deselected. + * @param coordinates The row and column indices of the deselected cell. + */ + onCellDeSelected?: (coordinates: {rowIdx: number, idx: number}) => void + + /** + * How long to wait before rendering a new row while scrolling in milliseconds. + * @default 0 + */ + rowScrollTimeout?: number + /** + * Options object for selecting rows + */ + rowSelection?: { + showCheckbox?: boolean + enableShiftSelect?: boolean + onRowsSelected?: (rows: Array<SelectionParams<T>>) => void, + onRowsDeselected?: (rows: Array<SelectionParams<T>>) => void, + selectBy?: { + indexes?: Array<number>; + keys?: { rowKey: string, values: Array<any> }; + isSelectedKey?: string; + } + } + /** + * A custom formatter for the select all checkbox cell + * @default react-data-grid/src/formatters/SelectAll.js + */ + selectAllRenderer?: React.ComponentClass<any> | React.StatelessComponent<any>; + /** + * A custom formatter for select row column + * @default AdazzleReactDataGridPlugins.Editors.CheckboxEditor + */ + rowActionsCell?: React.ComponentClass<any> | React.StatelessComponent<any>; + /** + * An event function called when a row is clicked. + * Clicking the header row will trigger a call with -1 for the rowIdx. + * @param rowIdx zero index number of row clicked + * @param row object behind the row + */ + onRowClick?: (rowIdx: number, row: T) => void + onRowDoubleClick?: (rowIdx: number, row: T) => void + + /** + * An event function called when a row is expanded with the toggle + * @param props OnRowExpandToggle object + */ + onRowExpandToggle?: (props: OnRowExpandToggle ) => void + + /** + * Responsible for returning an Array of values that can be used for filtering + * a column that is column.filterable and using a column.filterRenderer that + * displays a list of options. + * @param columnKey the column key that we are looking to pull values from + */ + getValidFilterValues?: (columnKey: string) => Array<any> + + getCellActions?: (column: Column<T>, row: T) => (ActionButton | ActionMenu)[] + } + + type ActionButton = { + icon: string; + callback: () => void; + } + + type ActionMenu = { + icon: string; + actions: { + icon: string; + text: string; + callback: () => void; + }[]; + } + + /** + * Information about a specific column to be rendered. + */ + interface Column<T> { + /** + * A unique key for this column. Required. + * Each row should have a property with this name, which contains this column's value. + */ + key: string + /** + * This column's display name. Required. + */ + name: string + /** + * A custom width for this specific column. + * @default minColumnWidth from the ReactDataGrid + */ + width?: number + /** + * Whether this column can be resized by the user. + * @default false + */ + resizable?: boolean + /** + * Whether this column should stay fixed on the left as the user scrolls horizontally. + * @default false + */ + locked?: boolean + /** + * Whether this column can be edited. + * @default false + */ + editable?: boolean + /** + * Whether the rows in the grid can be sorted by this column. + * @default false + */ + sortable?: boolean + /** + * Whether the rows in the grid can be filtered by this column. + * @default false + */ + filterable?: boolean; + /** + * A custom formatter for this column's filter. + */ + filterRenderer?: React.ReactElement<any> | React.ComponentClass<any> | React.StatelessComponent<any>; + /** + * The editor for this column. Several editors are available in "react-data-grid/addons". + * @default A simple text editor + */ + editor?: + | React.ReactElement<EditorBaseProps> + | React.ComponentClass<EditorBaseProps> + | React.StatelessComponent<EditorBaseProps>; + /** + * A custom read-only formatter for this column. An image formatter is available in "react-data-grid/addons". + */ + formatter?: React.ReactElement<any> | React.ComponentClass<any> | React.StatelessComponent<any> + /** + * A custom formatter for this column's header. + */ + headerRenderer?: React.ReactElement<any> | React.ComponentClass<any> | React.StatelessComponent<any> + /** + * Events to be bound to the cells in this specific column. + * Each event must respect this standard in order to work correctly: + * @example + * function onXxx(ev :SyntheticEvent, (rowIdx, idx, name): args) + */ + events?: { + [name: string]: ColumnEventCallback + }; + /** + * Retrieve meta data about the row, optionally provide column as a second argument + */ + getRowMetaData?: (rowdata: T, column?: Column<T>) => any; + /** + * A class name to be applied to the cells in the column + */ + cellClass?: string; + /** + * Whether this column can be dragged (re-arranged). + * @default false + */ + draggable?: boolean; + } + + interface ColumnEventCallback { + /** + * A callback for a native react event on a specific cell. + * @param ev The react event + * @param args The row and column coordinates of the cell, and the name of the event. + */ + (ev: React.SyntheticEvent<any>, args: {rowIdx: number, idx: number, name: string}): void + } + + /** + * Information about a row update. Generic event type returns untyped row, use parameterized type with the row type as the parameter + * @default T = any + */ + interface RowUpdateEvent<T = any> { + /** + * The index of the updated row. + */ + rowIdx: number + /** + * The columns that were updated and their values. + */ + updated: T + /** + * The name of the column that was updated. + */ + cellKey: string + /** + * The name of the key pressed to trigger the event ('Tab', 'Enter', etc.). + */ + key: string + } + + /** + * Information about a cell drag + */ + interface CellDragEvent { + /** + * The name of the column that was dragged. + */ + cellKey: string + /** + * The row where the drag began. + */ + fromRow: number + /** + * The row where the drag ended. + */ + toRow: number + /** + * The value of the cell that was dragged. + */ + value: any + } + + /** + * Information about a drag handle double click. Generic event type returns untyped row, use parameterized type with the row type as the parameter + * @default T = any + */ + interface DragHandleDoubleClickEvent<T = any> { + /** + * The row where the double click occurred. + */ + rowIdx: number + /** + * The column where the double click occurred. + */ + idx: number + /** + * The values of the row. + */ + rowData: T + /** + * The double click event. + */ + e: React.SyntheticEvent<any> + } + + /** + * Information about a copy paste + */ + interface CellCopyPasteEvent { + /** + * The row that was pasted to. + */ + rowIdx: number + /** + * The value that was pasted. + */ + value: any + /** + * The row that was copied from. + */ + fromRow: number + /** + * The row that was pasted to. + */ + toRow: number + /** + * The key of the column where the copy paste occurred. + */ + cellKey: string + } + + /** + * Information about some update to the grid's contents. Generic event type returns untyped row, use parameterized type with the row type as the parameter + * @default T = any + */ + interface GridRowsUpdatedEvent<T = any> { + /** + * The key of the column where the event occurred. + */ + cellKey: string + /** + * The top row affected by the event. + */ + fromRow: number + /** + * The bottom row affected by the event. + */ + toRow: number + /** + * The columns that were updated and their values. + */ + updated: T + /** + * The action that occurred to trigger this event. + * One of 'cellUpdate', 'cellDrag', 'columnFill', or 'copyPaste'. + */ + action: 'cellUpdate' | 'cellDrag' | 'columnFill' | 'copyPaste' + } + + /** + * Information about the row toggler + */ + interface OnRowExpandToggle { + /** + * The name of the column group the row is in + */ + columnGroupName: string + /** + * The name of the expanded row + */ + name: string + /** + * If it should expand or not + */ + shouldExpand: boolean + } + + /** + * Some filter to be applied to the grid's contents + */ + interface Filter { + /** + * The key of the column being filtered. + */ + columnKey: string + /** + * The term to filter by. + */ + filterTerm: string + } + + /** + * Excel-like grid component built with React, with editors, keyboard navigation, copy & paste, and the like + * http://adazzle.github.io/react-data-grid/ + */ + export class ReactDataGrid<T> extends React.Component<GridProps<T>> { + /** + * Opens the editor for the cell (idx) in the given row (rowIdx). If the column is not editable then nothing will happen. + */ + openCellEditor(rowIdx: number, idx: number): void; + } + export namespace ReactDataGrid { + // Useful types + export import Column = AdazzleReactDataGrid.Column; + export import Filter = AdazzleReactDataGrid.Filter; + + // Various events + export import RowUpdateEvent = AdazzleReactDataGrid.RowUpdateEvent; + export import SelectionParams = AdazzleReactDataGrid.SelectionParams; + export import CellDragEvent = AdazzleReactDataGrid.CellDragEvent; + export import DragHandleDoubleClickEvent = AdazzleReactDataGrid.DragHandleDoubleClickEvent; + export import CellCopyPasteEvent = AdazzleReactDataGrid.CellCopyPasteEvent; + export import GridRowsUpdatedEvent = AdazzleReactDataGrid.GridRowsUpdatedEvent; + export import OnRowExpandToggle = AdazzleReactDataGrid.OnRowExpandToggle; + + export namespace editors { + class EditorBase<P = {}, S = {}> extends React.Component<P & EditorBaseProps, S> { + getStyle(): { width: string }; + + getValue(): any; + + getInputNode(): Element | null | Text; + + inheritContainerStyles(): boolean; + } + } + + // Actual classes exposed on module.exports + /** + * A react component that renders a row of the grid + */ + export class Row extends React.Component<any> { } + /** + * A react coponent that renders a cell of the grid + */ + export class Cell extends React.Component<any> { } + } + } + + declare namespace AdazzleReactDataGridPlugins { + interface AutoCompleteEditorProps { + onCommit?: () => void; + options?: Array<{ id: any; title: string }>; + label?: any; + value?: any; + height?: number; + valueParams?: string[]; + column?: AdazzleReactDataGrid.ExcelColumn; + resultIdentifier?: string; + search?: string; + onKeyDown?: () => void; + onFocus?: () => void; + editorDisplayValue?: (column: AdazzleReactDataGrid.ExcelColumn, value: any) => string; + } + + interface AutoCompleteTokensEditorProps { + options: Array<string | { id: number; caption: string }>; + column?: AdazzleReactDataGrid.ExcelColumn; + value?: any[]; + } + + interface DropDownEditorProps { + options: + Array<string | { + id: string; + title: string; + value: string; + text: string; + }>; + } + + export namespace Editors { + export class AutoComplete extends React.Component<AutoCompleteEditorProps> {} + export class AutoCompleteTokensEditor extends React.Component<AutoCompleteTokensEditorProps> {} + export class DropDownEditor extends React.Component<DropDownEditorProps> {} + + // TODO: refine types for these addons + export class SimpleTextEditor extends React.Component<any> {} + export class CheckboxEditor extends React.Component<any> {} + } + export namespace Filters { + export class NumericFilter extends React.Component<any> { } + export class AutoCompleteFilter extends React.Component<any> { } + export class MultiSelectFilter extends React.Component<any> { } + export class SingleSelectFilter extends React.Component<any> { } + } + export namespace Formatters { + export class ImageFormatter extends React.Component<any> { } + export class DropDownFormatter extends React.Component<any> { } + } + export class Toolbar extends React.Component<any> {} + export namespace DraggableHeader { + export class DraggableContainer extends React.Component<any>{ } + } + export namespace Data { + export const Selectors: { + getRows: (state: object) => object[]; + getSelectedRowsByKey: (state: object) => object[]; + } + } + // TODO: re-export the react-contextmenu typings once those exist + // https://github.com/vkbansal/react-contextmenu/issues/10 + export namespace Menu { + export class ContextMenu extends React.Component<any> { } + export class MenuHeader extends React.Component<any> { } + export class MenuItem extends React.Component<any> { } + export class SubMenu extends React.Component<any> { } + export const monitor: { + getItem(): any + getPosition(): any + hideMenu(): void + }; + export function connect(Menu: any): any; + export function ContextMenuLayer( + identifier: any, + configure?: (props: any) => any + ): (Component: any) => any + } + } + + declare module "react-data-grid" { + import ReactDataGrid = AdazzleReactDataGrid.ReactDataGrid; + + // commonjs export + export = ReactDataGrid; + } + + declare module "react-data-grid-addons" { + import Plugins = AdazzleReactDataGridPlugins; + import Editors = Plugins.Editors; + import Filters = Plugins.Filters; + import Formatters = Plugins.Formatters; + import Toolbar = Plugins.Toolbar; + import Menu = Plugins.Menu; + import Data = Plugins.Data; + import DraggableHeader = Plugins.DraggableHeader; + + // ES6 named exports + export { + Editors, + Filters, + Formatters, + Toolbar, + Menu, + Data, + DraggableHeader + } + + // attach to window + global { + interface Window { + ReactDataGridPlugins: { + Editors: typeof Editors, + Filters: typeof Filters, + Formatters: typeof Formatters, + Toolbar: typeof Toolbar, + Menu: typeof Menu, + Data: typeof Data, + DraggableHeader: typeof DraggableHeader + } + } + } + } diff --git a/types/react-svg-pan-zoom.d.ts b/types/react-svg-pan-zoom.d.ts new file mode 100644 index 000000000000..7d65bb5b3027 --- /dev/null +++ b/types/react-svg-pan-zoom.d.ts @@ -0,0 +1,215 @@ +// Type definitions for react-svg-pan-zoom 2.5 +// Project: https://github.com/chrvadala/react-svg-pan-zoom#readme, https://chrvadala.github.io/react-svg-pan-zoom +// Definitions by: Huy Nguyen <https://github.com/huy-nguyen> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.8 + +// Copied here so could add UncontrolledReactSVGPanZoom +declare module 'react-svg-pan-zoom'; + +/// <reference types="react" /> + +import * as React from 'react'; + +// String constants: +export const MODE_IDLE = 'idle'; +export const MODE_PANNING = 'panning'; +export const MODE_ZOOMING = 'zooming'; + +export const TOOL_AUTO = 'auto'; +export const TOOL_NONE = 'none'; +export const TOOL_PAN = 'pan'; +export const TOOL_ZOOM_IN = 'zoom-in'; +export const TOOL_ZOOM_OUT = 'zoom-out'; + +export const POSITION_NONE = 'none'; +export const POSITION_TOP = 'top'; +export const POSITION_RIGHT = 'right'; +export const POSITION_BOTTOM = 'bottom'; +export const POSITION_LEFT = 'left'; + +export type Mode = typeof MODE_IDLE | typeof MODE_PANNING | typeof MODE_ZOOMING; + +export interface Value { + version: 2; + mode: Mode; + focus: boolean; + a: number; + b: number; + c: number; + d: number; + e: number; + f: number; + viewerWidth: number; + viewerHeight: number; + SVGWidth: number; + SVGHeight: number; + startX?: number | null; + startY?: number | null; + endX?: number | null; + endY?: number | null; +} + +export type Tool = typeof TOOL_AUTO | typeof TOOL_NONE | typeof TOOL_PAN | + typeof TOOL_ZOOM_IN | typeof TOOL_ZOOM_OUT; +export type ToolbarPosition = typeof POSITION_NONE | typeof POSITION_TOP | typeof POSITION_RIGHT | + typeof POSITION_BOTTOM | typeof POSITION_LEFT; + +export interface OptionalProps { + // background of the viewer + background: string; + + // background of the svg + SVGBackground: string; + + // value of the viewer (current point of view) + value: Value | null; + + // default value of the viewer + defaultValue?: Value; + + // default tool to start with + defaultTool?: Tool; + + // CSS style of the Viewer + style: object; + + // className of the Viewer + className: string; + + // detect zoom operation performed trough pinch gesture or mouse scroll + detectWheel: boolean; + + // perform PAN if the mouse is on viewer border + detectAutoPan: boolean; + + // toolbar props + toolbarProps: { position: ToolbarPosition }; + + // handler something changed + onChangeValue(value: Value): void; + + // handler tool changed + onChangeTool(tool: Tool): void; + + // Note: The `T` type parameter is the type of the `target` of the event: + // handler click + onClick<T>(event: ViewerMouseEvent<T>): void; + + // handler double click + onDoubleClick<T>(event: ViewerMouseEvent<T>): void; + + // handler mouseup + onMouseUp<T>(event: ViewerMouseEvent<T>): void; + + // handler mousemove + onMouseMove<T>(event: ViewerMouseEvent<T>): void; + + // handler mousedown + onMouseDown<T>(event: ViewerMouseEvent<T>): void; + + // if disabled the user can move the image outside the viewer + preventPanOutside: boolean; + + // how much scale in or out + scaleFactor: number; + + // current active tool (TOOL_NONE, TOOL_PAN, TOOL_ZOOM_IN, TOOL_ZOOM_OUT) + tool: Tool; + + // modifier keys //https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState + modifierKeys: string[]; + + // override default toolbar component + // TODO: specify function type more clearly + customToolbar: React.Component<any> | React.StatelessComponent<any>; + customMiniature: React.Component<any> | React.StatelessComponent<any>; + + // How about touch events? They are in README but not in `propTypes`. +} + +export interface RequiredProps { + // width of the viewer displayed on screen + width: number; + // height of the viewer displayed on screen + height: number; + + // accept only one node SVG + // TODO: Figure out how to constrain `children` or maybe just leave it commented out + // because `children` is already implicit props + // children: () => any; +} + +export type Props = RequiredProps & Partial<OptionalProps>; + +export class ReactSVGPanZoom extends React.Component<Props> { + pan(SVGDeltaX: number, SVGDeltaY: number): void; + zoom(SVGPointX: number, SVGPointY: number, scaleFactor: number): void; + fitSelection(selectionSVGPointX: number, selectionSVGPointY: number, selectionWidth: number, selectionHeight: number): void; + fitToViewer(): void; + setPointOnViewerCenter(SVGPointX: number, SVGPointY: number, zoomLevel: number): void; + reset(): void; + zoomOnViewerCenter(scaleFactor: number): void; + getValue(): Value; + setValue(value: Value): void; + getTool(): Tool; + setTool(tool: Tool): void; +} + +export class UncontrolledReactSVGPanZoom extends React.Component<Props> { + pan(SVGDeltaX: number, SVGDeltaY: number): void; + zoom(SVGPointX: number, SVGPointY: number, scaleFactor: number): void; + fitSelection(selectionSVGPointX: number, selectionSVGPointY: number, selectionWidth: number, selectionHeight: number): void; + fitToViewer(): void; + setPointOnViewerCenter(SVGPointX: number, SVGPointY: number, zoomLevel: number): void; + reset(): void; + zoomOnViewerCenter(scaleFactor: number): void; + changeValue(value: Value): void; + changeTool(tool: Tool): void; +} + +export interface Point { + x: number; + y: number; +} + +export interface ViewerMouseEvent<T> { + originalEvent: React.MouseEvent<T>; + SVGViewer: SVGSVGElement; + point: Point; + x: number; + y: number; + scaleFactor: number; + translationX: number; + translationY: number; + preventDefault(): void; + stopPropagation(): void; +} + +export interface ViewerTouchEvent<T> { + originalEvent: React.TouchEvent<T>; + SVGViewer: SVGSVGElement; + points: Point[]; + changedPoints: Point[]; + scaleFactor: number; + translationX: number; + translationY: number; + preventDefault(): void; + stopPropagation(): void; +} + +// Utility functions exposed: +export function pan(value: Value, SVGDeltaX: number, SVGDeltaY: number, panLimit?: number): Value; + +export function zoom(value: Value, SVGPointX: number, SVGPointY: number, scaleFactor: number): Value; + +export function fitSelection( + value: Value, selectionSVGPointX: number, selectionSVGPointY: number, selectionWidth: number, selectionHeight: number): Value; + +export function fitToViewer(value: Value): Value; + +export function zoomOnViewerCenter(value: Value, scaleFactor: number): Value; + +export function setPointOnViewerCenter(value: Value, SVGPointX: number, SVGPointY: number, zoomLevel: number): Value; + +export function reset(value: Value): Value; diff --git a/types/react-svgmt.d.ts b/types/react-svgmt.d.ts new file mode 100644 index 000000000000..4cee2e8d6dfa --- /dev/null +++ b/types/react-svgmt.d.ts @@ -0,0 +1,28 @@ +declare module 'react-svgmt' { + export class SvgLoader { + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + props: any; + state: any; + context: any; + refs: any; + } + export class SvgProxy { + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + props: any; + state: any; + context: any; + refs: any; + } +} \ No newline at end of file diff --git a/types/react-tabulator.d.ts b/types/react-tabulator.d.ts new file mode 100644 index 000000000000..a0a0c35a36a0 --- /dev/null +++ b/types/react-tabulator.d.ts @@ -0,0 +1,28 @@ +declare module 'react-tabulator' { + export class React15Tabulator { + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + props: any; + state: any; + context: any; + refs: any; + } + export default class _default { + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + props: any; + state: any; + context: any; + refs: any; + } +} \ No newline at end of file diff --git a/types/slickgrid/index.d.ts b/types/slickgrid/index.d.ts new file mode 100644 index 000000000000..91c10af275d6 --- /dev/null +++ b/types/slickgrid/index.d.ts @@ -0,0 +1,1812 @@ +// Type definitions for SlickGrid 2.1.0 +// Project: https://github.com/mleibman/SlickGrid +// Definitions by: Josh Baldwin <https://github.com/jbaldwin> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.3 + +/* +SlickGrid-2.1.d.ts may be freely distributed under the MIT license. + +Copyright (c) 2013 Josh Baldwin https://github.com/jbaldwin/SlickGrid.d.ts + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +*/ + +/// <reference types="jquery" /> + +interface DOMEvent extends Event { } + +declare namespace Slick { + + /** + * slick.core.js + **/ + + /** + * An event object for passing data to event handlers and letting them control propagation. + * <p>This is pretty much identical to how W3C and jQuery implement events.</p> + * @class EventData + * @constructor + **/ + export class EventData { + + constructor(); + + /*** + * Stops event from propagating up the DOM tree. + * @method stopPropagation + */ + public stopPropagation(): void; + + /*** + * Returns whether stopPropagation was called on this event object. + * @method isPropagationStopped + * @return {Boolean} + */ + public isPropagationStopped(): boolean; + + /*** + * Prevents the rest of the handlers from being executed. + * @method stopImmediatePropagation + */ + public stopImmediatePropagation(): void; + + /*** + * Returns whether stopImmediatePropagation was called on this event object.\ + * @method isImmediatePropagationStopped + * @return {Boolean} + */ + public isImmediatePropagationStopped(): boolean; + } + + /*** + * A simple publisher-subscriber implementation. + * @class Event + * @constructor + */ + export class Event<T> { + + constructor(); + + /*** + * Adds an event handler to be called when the event is fired. + * <p>Event handler will receive two arguments - an <code>EventData</code> and the <code>data</code> + * object the event was fired with.<p> + * @method subscribe + * @param fn {Function} Event handler. + */ + public subscribe(fn: (e: EventData, data: T) => void): void; + public subscribe(fn: (e: DOMEvent, data: T) => void): void; + + /*** + * Removes an event handler added with <code>subscribe(fn)</code>. + * @method unsubscribe + * @param fn {Function} Event handler to be removed. + */ + public unsubscribe(fn: (e: EventData, data: T) => void): void; + public unsubscribe(fn: (e: DOMEvent, data: T) => void): void; + + /*** + * Fires an event notifying all subscribers. + * @method notify + * @param args {Object} Additional data object to be passed to all handlers. + * @param e {EventData} + * Optional. + * An <code>EventData</code> object to be passed to all handlers. + * For DOM events, an existing W3C/jQuery event object can be passed in. + * @param scope {Object} + * Optional. + * The scope ("this") within which the handler will be executed. + * If not specified, the scope will be set to the <code>Event</code> instance. + * @return Last run callback result. + * @note slick.core.Event.notify shows this method as returning a value, type is unknown. + */ + public notify(args?: T, e?: EventData, scope?: any): any; + public notify(args?: T, e?: DOMEvent, scope?: any): any; + + } + + // todo: is this private? there are no comments in the code + export class EventHandler<T = any> { + constructor(); + + public subscribe(event: Event<T>, handler: (e: EventData, data: T) => void): EventHandler; + public unsubscribe(event: Event<T>, handler: (e: EventData, data: T) => void): EventHandler; + public unsubscribeAll(): EventHandler; + } + + /*** + * A structure containing a range of cells. + * @class Range + **/ + export class Range { + + /** + * A structure containing a range of cells. + * @constructor + * @param fromRow {Integer} Starting row. + * @param fromCell {Integer} Starting cell. + * @param toRow {Integer} Optional. Ending row. Defaults to <code>fromRow</code>. + * @param toCell {Integer} Optional. Ending cell. Defaults to <code>fromCell</code>. + **/ + constructor(fromRow: number, fromCell: number, toRow?: number, toCell?: number); + + /*** + * @property fromRow + * @type {Integer} + */ + public fromRow: number; + + /*** + * @property fromCell + * @type {Integer} + */ + public fromCell: number; + + /*** + * @property toRow + * @type {Integer} + */ + public toRow: number; + + /*** + * @property toCell + * @type {Integer} + */ + public toCell: number; + + /*** + * Returns whether a range represents a single row. + * @method isSingleRow + * @return {Boolean} + */ + public isSingleRow(): boolean; + + /*** + * Returns whether a range represents a single cell. + * @method isSingleCell + * @return {Boolean} + */ + public isSingleCell(): boolean; + + /*** + * Returns whether a range contains a given cell. + * @method contains + * @param row {Integer} + * @param cell {Integer} + * @return {Boolean} + */ + public contains(row: number, cell: number): boolean; + + /*** + * Returns a readable representation of a range. + * @method toString + * @return {String} + */ + public toString(): string; + + } + + /*** + * A base class that all special / non-data rows (like Group and GroupTotals) derive from. + * @class NonDataItem + * @constructor + */ + export class NonDataRow { + + } + + /*** + * Information about a group of rows. + * @class Group + * @extends Slick.NonDataItem + * @constructor + */ + export class Group<T extends SlickData> extends NonDataRow { + + constructor(); + + /** + * Grouping level, starting with 0. + * @property level + * @type {Number} + */ + public level: number; + + /*** + * Number of rows in the group. + * @property count + * @type {Integer} + */ + public count: number; + + /*** + * Grouping value. + * @property value + * @type {Object} + */ + public value: any; + + /*** + * Formatted display value of the group. + * @property title + * @type {String} + */ + public title: string; + + /*** + * Whether a group is collapsed. + * @property collapsed + * @type {Boolean} + */ + public collapsed: boolean; + + /*** + * GroupTotals, if any. + * @property totals + * @type {GroupTotals} + */ + public totals: GroupTotals<T>; + + /** + * Rows that are part of the group. + * @property rows + * @type {Array} + */ + public rows: T[]; + + /** + * Sub-groups that are part of the group. + * @property groups + * @type {Array} + */ + public groups: Group<T>[]; + + /** + * A unique key used to identify the group. This key can be used in calls to DataView + * collapseGroup() or expandGroup(). + * @property groupingKey + * @type {Object} + */ + public groupingKey: any; + + /*** + * Compares two Group instances. + * @method equals + * @return {Boolean} + * @param group {Group} Group instance to compare to. + * todo: this is on the prototype (NonDataRow()) instance, not Group, maybe doesn't matter? + */ + public equals(group: Group<T>): boolean; + } + + /*** + * Information about group totals. + * An instance of GroupTotals will be created for each totals row and passed to the aggregators + * so that they can store arbitrary data in it. That data can later be accessed by group totals + * formatters during the display. + * @class GroupTotals + * @extends Slick.NonDataItem + * @constructor + */ + export class GroupTotals<T> extends NonDataRow { + + constructor(); + + /*** + * Parent Group. + * @param group + * @type {Group} + */ + public group: Group<T>; + + } + + /*** + * A locking helper to track the active edit controller and ensure that only a single controller + * can be active at a time. This prevents a whole class of state and validation synchronization + * issues. An edit controller (such as SlickGrid) can query if an active edit is in progress + * and attempt a commit or cancel before proceeding. + * @class EditorLock + * @constructor + */ + export class EditorLock<T extends Slick.SlickData> { + + constructor(); + + /*** + * Returns true if a specified edit controller is active (has the edit lock). + * If the parameter is not specified, returns true if any edit controller is active. + * @method isActive + * @param editController {EditController} + * @return {Boolean} + */ + public isActive(editController: Editors.Editor<T>): boolean; + + /*** + * Sets the specified edit controller as the active edit controller (acquire edit lock). + * If another edit controller is already active, and exception will be thrown. + * @method activate + * @param editController {EditController} edit controller acquiring the lock + */ + public activate(editController: Editors.Editor<T>): void; + + /*** + * Unsets the specified edit controller as the active edit controller (release edit lock). + * If the specified edit controller is not the active one, an exception will be thrown. + * @method deactivate + * @param editController {EditController} edit controller releasing the lock + */ + public deactivate(editController: Editors.Editor<T>): void; + + /*** + * Attempts to commit the current edit by calling "commitCurrentEdit" method on the active edit + * controller and returns whether the commit attempt was successful (commit may fail due to validation + * errors, etc.). Edit controller's "commitCurrentEdit" must return true if the commit has succeeded + * and false otherwise. If no edit controller is active, returns true. + * @method commitCurrentEdit + * @return {Boolean} + */ + public commitCurrentEdit(): boolean; + + /*** + * Attempts to cancel the current edit by calling "cancelCurrentEdit" method on the active edit + * controller and returns whether the edit was successfully cancelled. If no edit controller is + * active, returns true. + * @method cancelCurrentEdit + * @return {Boolean} + */ + public cancelCurrentEdit(): boolean; + } + + /** + * A global singleton editor lock. + * @class GlobalEditorLock + * @static + * @constructor + **/ + export var GlobalEditorLock: EditorLock<Slick.SlickData>; + + /** + * slick.grid.js + **/ + + /** + * Options which you can apply to the columns objects. + **/ + export interface Column<T extends Slick.SlickData> { + + /** + * This accepts a function of the form function(cellNode, row, dataContext, colDef) and is used to post-process the cell's DOM node / nodes + * @param cellNode + * @param row + * @param dataContext + * @param colDef + * @return + **/ + asyncPostRender?: (cellNode: any, row: any, dataContext: any, colDef: any) => void; + + /** + * Used by the the slick.rowMoveManager.js plugin for moving rows. Has no effect without the plugin installed. + **/ + behavior?: any; + + /** + * In the "Add New" row, determines whether clicking cells in this column can trigger row addition. If true, clicking on the cell in this column in the "Add New" row will not trigger row addition. + **/ + cannotTriggerInsert?: boolean; + + /** + * Accepts a string as a class name, applies that class to every row cell in the column. + **/ + cssClass?: string; + + /** + * When set to true, the first user click on the header will do a descending sort. When set to false, the first user click on the header will do an ascending sort. + **/ + defaultSortAsc?: boolean; + + /** + * The editor for cell edits {TextEditor, IntegerEditor, DateEditor...} See slick.editors.js + **/ + editor?: any; // typeof Editors.Editor<T>; + + /** + * The property name in the data object to pull content from. (This is assumed to be on the root of the data object.) + **/ + field?: string; + + /** + * When set to false, clicking on a cell in this column will not select the row for that cell. The cells in this column will also be skipped during tab navigation. + **/ + focusable?: boolean; + + /** + * This accepts a function of the form function(row, cell, value, columnDef, dataContext) and returns a formatted version of the data in each cell of this column. For example, setting formatter to function(r, c, v, cd, dc) { return "Hello!"; } would overwrite every value in the column with "Hello!" See defaultFormatter in slick.grid.js for an example formatter. + * @param row + * @param cell + * @param value + * @param columnDef + * @param dataContext + * @return + **/ + formatter?: Formatter<T>; + + /** + * Accepts a string as a class name, applies that class to the cell for the column header. + **/ + headerCssClass?: string; + + /** + * A unique identifier for the column within the grid. + **/ + id?: string; + + /** + * Set the maximum allowable width of this column, in pixels. + **/ + maxWidth?: number; + + /** + * Set the minimum allowable width of this column, in pixels. + **/ + minWidth?: number; + + /** + * The text to display on the column heading. + **/ + name?: string; + + /** + * If set to true, whenever this column is resized, the entire table view will rerender. + **/ + rerenderOnResize?: boolean; + + /** + * If false, column can no longer be resized. + **/ + resizable?: boolean; + + /** + * If false, when a row is selected, the CSS class for selected cells ("selected" by default) is not applied to the cell in this column. + **/ + selectable?: boolean; + + /** + * If true, the column will be sortable by clicking on the header. + **/ + sortable?: boolean; + + /** + * If set to a non-empty string, a tooltip will appear on hover containing the string. + **/ + toolTip?: string; + + /** + * Width of the column in pixels. (May often be overridden by things like minWidth, maxWidth, forceFitColumns, etc.) + **/ + width?: number; + } + + export interface EditorFactory { + getEditor<T>(column: Column<T>): Editors.Editor<T>; + } + + export interface FormatterFactory<T extends SlickData> { + getFormatter(column: Column<T>): Formatter<any>; + } + + export interface GridOptions<T extends SlickData> { + + /** + * Makes cell editors load asynchronously after a small delay. This greatly increases keyboard navigation speed. + **/ + asyncEditorLoading?: boolean; + + /** + * Delay after which cell editor is loaded. Ignored unless asyncEditorLoading is true. + **/ + asyncEditorLoadDelay?: number; + + /** + * + **/ + asyncPostRenderDelay?: number; + + /** + * Cell will not automatically go into edit mode when selected. + **/ + autoEdit?: boolean; + + /** + * + **/ + autoHeight?: boolean; + + /** + * A CSS class to apply to flashing cells via flashCell(). + **/ + cellFlashingCssClass?: string; + + /** + * A CSS class to apply to cells highlighted via setHighlightedCells(). + **/ + cellHighlightCssClass?: string; + + /** + * + **/ + dataItemColumnValueExtractor?: (item: any, columnDef: any) => any; + + /** + * + **/ + defaultColumnWidth?: number; + + /** + * + **/ + defaultFormatter?: Formatter<T>; + + /** + * + **/ + editable?: boolean; + + /** + * Not listed as a default under options in slick.grid.js + **/ + editCommandHandler?: any; // queueAndExecuteCommand + + /** + * A factory object responsible to creating an editor for a given cell. Must implement getEditor(column). + **/ + editorFactory?: EditorFactory; + + /** + * A Slick.EditorLock instance to use for controlling concurrent data edits. + **/ + editorLock?: EditorLock<T>; + + /** + * If true, a blank row will be displayed at the bottom - typing values in that row will add a new one. Must subscribe to onAddNewRow to save values. + **/ + enableAddRow?: boolean; + + /** + * If true, async post rendering will occur and asyncPostRender delegates on columns will be called. + **/ + enableAsyncPostRender?: boolean; + + /** + * *WARNING*: Not contained in SlickGrid 2.1, may be deprecated + **/ + enableCellRangeSelection?: any; + + /** + * Appears to enable cell virtualisation for optimised speed with large datasets + **/ + enableCellNavigation?: boolean; + + /** + * + **/ + enableColumnReorder?: boolean; + + /** + * *WARNING*: Not contained in SlickGrid 2.1, may be deprecated + **/ + enableRowReordering?: any; + + /** + * + **/ + enableTextSelectionOnCells?: boolean; + + /** + * @see Example: Explicit Initialization + **/ + explicitInitialization?: boolean; + + /** + * Force column sizes to fit into the container (preventing horizontal scrolling). Effectively sets column width to be 1/Number of Columns which on small containers may not be desirable + **/ + forceFitColumns?: boolean; + + /** + * + **/ + forceSyncScrolling?: boolean; + + /** + * A factory object responsible to creating a formatter for a given cell. Must implement getFormatter(column). + **/ + formatterFactory?: FormatterFactory<T>; + + /** + * Will expand the table row divs to the full width of the container, table cell divs will remain aligned to the left + **/ + fullWidthRows?: boolean; + + /** + * + **/ + headerRowHeight?: number; + + /** + * + **/ + leaveSpaceForNewRows?: boolean; + + /** + * @see Example: Multi-Column Sort + **/ + multiColumnSort?: boolean; + + /** + * + **/ + multiSelect?: boolean; + + /** + * + **/ + rowHeight?: number; + + /** + * + **/ + selectedCellCssClass?: string; + + /** + * + **/ + showHeaderRow?: boolean; + + /** + * If true, the column being resized will change its width as the mouse is dragging the resize handle. If false, the column will resize after mouse drag ends. + **/ + syncColumnCellResize?: boolean; + + /** + * + **/ + topPanelHeight?: number; + + + addNewRowCssClass?: string; + alwaysAllowHorizontalScroll?: boolean; + alwaysShowVerticalScroll?: boolean; + asyncPostRenderCleanupDelay?: number; + createFooterRow?: boolean; + createPreHeaderPanel?: boolean; + doPaging?: boolean; + editorCellNavOnLRKeys?: boolean; + emulatePagingWhenScrolling?: boolean; + enableAsyncPostRenderCleanup?: boolean; + footerRowHeight?: number; + frozenBottom?: boolean; + frozenColumn?: number; + frozenRow?: number; + minRowBuffer?: number; + numberedMultiColumnSort?: boolean; + preHeaderPanelHeight?: number; + preserveCopiedSelectionOnPaste?: boolean; + showCellSelection?: boolean; + showFooterRow?: boolean; + showPreHeaderPanel?: boolean; + showTopPanel?: boolean; + sortColNumberInSeparateSpan?: boolean; + suppressActiveCellChangeOnEdit?: boolean; + tristateMultiColumnSort?: boolean; + viewportClass?: string; + } + + export interface DataProvider<T extends SlickData> { + /** + * Returns the number of data items in the set. + */ + getLength(): number; + + /** + * Returns the item at a given index. + * @param index + */ + getItem(index: number): T; + + /** + * Returns the metadata for the item at a given index (optional). + * @param index + */ + getItemMetadata?(index: number): RowMetadata<T>; + } + + export interface SlickData { + // todo ? might be able to leave as empty + } + + export interface RowMetadata<T> { + /** + * One or more (space-separated) CSS classes to be added to the entire row. + */ + cssClasses?: string; + + /** + * Whether or not any cells in the row can be set as "active". + */ + focusable?: boolean; + + /** + * Whether or not a row or any cells in it can be selected. + */ + selectable?: boolean; + + /** + * Metadata related to individual columns + */ + columns?: { + /** + * Metadata indexed by column id + */ + [index: string]: ColumnMetadata<T>; + /** + * Metadata indexed by column index + */ + [index: number]: ColumnMetadata<T>; + }; + } + + export interface ColumnMetadata<T extends SlickData> { + /** + * Whether or not a cell can be set as "active". + */ + focusable?: boolean; + + /** + * Whether or not a cell can be selected. + */ + selectable?: boolean; + + /** + * A custom cell formatter. + */ + formatter?: Formatter<T>; + + /** + * A custom cell editor. + */ + editor?: Slick.Editors.Editor<T>; + + /** + * Number of columns this cell will span. Can also contain "*" to indicate that the cell should span the rest of the row. + */ + colspan?: number | string; + } + + /** + * Selecting cells in SlickGrid is handled by a selection model. + * Selection models are controllers responsible for handling user interactions and notifying subscribers of the changes in the selection. Selection is represented as an array of Slick.Range objects. + * You can get the current selection model from the grid by calling getSelectionModel() and set a different one using setSelectionModel(selectionModel). By default, no selection model is set. + * The grid also provides two helper methods to simplify development - getSelectedRows() and setSelectedRows(rowsArray), as well as an onSelectedRowsChanged event. + * SlickGrid includes two pre-made selection models - Slick.CellSelectionModel and Slick.RowSelectionModel, but you can easily write a custom one. + **/ + export class SelectionModel<T extends SlickData, E> { + /** + * An initializer function that will be called with an instance of the grid whenever a selection model is registered with setSelectionModel. The selection model can use this to initialize its state and subscribe to grid events. + **/ + init(grid: Grid<T>): void; + + /** + * A destructor function that will be called whenever a selection model is unregistered from the grid by a call to setSelectionModel with another selection model or whenever a grid with this selection model is destroyed. The selection model can use this destructor to unsubscribe from grid events and release all resources (remove DOM nodes, event listeners, etc.). + **/ + destroy(): void; + + onSelectedRangesChanged: Slick.Event<E>; + } + + export class Grid<T extends SlickData> { + + /** + * Create an instance of the grid. + * @param container Container node to create the grid in. This can be a DOM Element, a jQuery node, or a jQuery selector. + * @param data Databinding source. This can either be a regular JavaScript array or a custom object exposing getItem(index) and getLength() functions. + * @param columns An array of column definition objects. See Column Options for a list of options that can be included on each column definition object. + * @param options Additional options. See Grid Options for a list of options that can be included. + **/ + constructor( + container: string | HTMLElement | JQuery, + data: T[] | DataProvider<T>, + columns: Column<T>[], + options: GridOptions<T>); + + // #region Core + + /** + * Initializes the grid. Called after plugins are registered. Normally, this is called by the constructor, so you don't need to call it. However, in certain cases you may need to delay the initialization until some other process has finished. In that case, set the explicitInitialization option to true and call the grid.init() manually. + **/ + public init(): void; + + /** + * todo: no docs + **/ + public destroy(): void; + + /** + * Returns an array of every data object, unless you're using DataView in which case it returns a DataView object. + * @return + **/ + public getData(): any; + //public getData(): T[]; + // Issue: typescript limitation, cannot differentiate calls by return type only, so need to cast to DataView or T[]. + //public getData(): DataView; + + /** + * Returns the databinding item at a given position. + * @param index Item index. + * @return + **/ + public getDataItem(index: number): T; + + /** + * Sets a new source for databinding and removes all rendered rows. Note that this doesn't render the new rows - you can follow it with a call to render() to do that. + * @param newData New databinding source using a regular JavaScript array.. + * @param scrollToTop If true, the grid will reset the vertical scroll position to the top of the grid. + **/ + public setData(newData: T[], scrollToTop: boolean): void; + + /** + * Sets a new source for databinding and removes all rendered rows. Note that this doesn't render the new rows - you can follow it with a call to render() to do that. + * @param newData New databinding source using a custom object exposing getItem(index) and getLength() functions. + * @param scrollToTop If true, the grid will reset the vertical scroll position to the top of the grid. + **/ + public setData(newData: DataProvider<T>, scrollToTop: boolean): void; + + /** + * Returns the size of the databinding source. + * @return + **/ + public getDataLength(): number; + + /** + * Returns an object containing all of the Grid options set on the grid. See a list of Grid Options here. + * @return + **/ + public getOptions(): GridOptions<any>; + + /** + * Returns an array of row indices corresponding to the currently selected rows. + * @return + **/ + public getSelectedRows(): number[]; + + /** + * Returns the current SelectionModel. See here for more information about SelectionModels. + * @return + **/ + public getSelectionModel(): SelectionModel<any, any>; + + /** + * Extends grid options with a given hash. If an there is an active edit, the grid will attempt to commit the changes and only continue if the attempt succeeds. + * @options An object with configuration options. + **/ + public setOptions(options: GridOptions<T>): void; + + /** + * Accepts an array of row indices and applies the current selectedCellCssClass to the cells in the row, respecting whether cells have been flagged as selectable. + * @param rowsArray An array of row numbers. + **/ + public setSelectedRows(rowsArray: number[]): void; + + /** + * Returns container's HTML node (the element passed into Grid constructor). + */ + public getContainerNode(): HTMLElement; + + /** + * Unregisters a current selection model and registers a new one. See the definition of SelectionModel for more information. + * @selectionModel A SelectionModel. + **/ + public setSelectionModel(selectionModel: SelectionModel<T, any>): void; // todo: don't know the type of the event data type + + // #endregion Core + + // #region Columns + + /** + * Proportionately resizes all columns to fill available horizontal space. This does not take the cell contents into consideration. + **/ + public autosizeColumns(): void; + + /** + * Returns the index of a column with a given id. Since columns can be reordered by the user, this can be used to get the column definition independent of the order: + * @param id A column id. + * @return + **/ + public getColumnIndex(id: string): number; + + /** + * Returns an array of column definitions, containing the option settings for each individual column. + * @return + **/ + public getColumns(): Column<T>[]; + + /** + * Sets grid columns. Column headers will be recreated and all rendered rows will be removed. To rerender the grid (if necessary), call render(). + * @param columnDefinitions An array of column definitions. + **/ + public setColumns(columnDefinitions: Column<T>[]): void; + + /** + * Accepts a columnId string and an ascending boolean. Applies a sort glyph in either ascending or descending form to the header of the column. Note that this does not actually sort the column. It only adds the sort glyph to the header. + * @param columnId + * @param ascending + **/ + public setSortColumn(columnId: string, ascending: boolean): void; + + /** + * Accepts an array of objects in the form [ { columnId: [string], sortAsc: [boolean] }, ... ]. When called, this will apply a sort glyph in either ascending or descending form to the header of each column specified in the array. Note that this does not actually sort the column. It only adds the sort glyph to the header + * @param cols + **/ + public setSortColumns(cols: { columnId: string; sortAsc: boolean }[]): void; + + /** + * todo: no docs or comments available + * @return + **/ + public getSortColumns(): { columnId: string; sortAsc: boolean }[]; + + /** + * Updates an existing column definition and a corresponding header DOM element with the new title and tooltip. + * @param columnId Column id. + * @param title New column name. + * @param toolTip New column tooltip. + **/ + public updateColumnHeader(columnId: string, title?: string, toolTip?: string): void; + + // #endregion Columns + + // #region Cells + + /** + * Adds an "overlay" of CSS classes to cell DOM elements. SlickGrid can have many such overlays associated with different keys and they are frequently used by plugins. For example, SlickGrid uses this method internally to decorate selected cells with selectedCellCssClass (see options). + * @param key A unique key you can use in calls to setCellCssStyles and removeCellCssStyles. If a hash with that key has already been set, an exception will be thrown. + * @param hash A hash of additional cell CSS classes keyed by row number and then by column id. Multiple CSS classes can be specified and separated by space. + * @example + * { + * 0: { + * "number_column": "cell-bold", + * "title_column": "cell-title cell-highlighted" + * }, + * 4: { + * "percent_column": "cell-highlighted" + * } + * } + **/ + public addCellCssStyles(key: string, hash: CellCssStylesHash): void; + + /** + * Returns true if you can click on a given cell and make it the active focus. + * @param row A row index. + * @param col A column index. + * @return + **/ + public canCellBeActive(row: number, col: number): boolean; + + /** + * Returns true if selecting the row causes this particular cell to have the selectedCellCssClass applied to it. A cell can be selected if it exists and if it isn't on an empty / "Add New" row and if it is not marked as "unselectable" in the column definition. + * @param row A row index. + * @param col A column index. + * @return + **/ + public canCellBeSelected(row: number, col: number): boolean; + + /** + * Attempts to switch the active cell into edit mode. Will throw an error if the cell is set to be not editable. Uses the specified editor, otherwise defaults to any default editor for that given cell. + * @param editor A SlickGrid editor (see examples in slick.editors.js). + **/ + public editActiveCell(editor: Editors.Editor<T>): void; + + /** + * Flashes the cell twice by toggling the CSS class 4 times. + * @param row A row index. + * @param cell A column index. + * @param speed (optional) - The milliseconds delay between the toggling calls. Defaults to 100 ms. + **/ + public flashCell(row: number, cell: number, speed?: number): void; + + /** + * Returns an object representing the coordinates of the currently active cell: + * @example + * { + * row: activeRow, + * cell: activeCell + * } + * @return + **/ + public getActiveCell(): Cell; + + /** + * Returns the DOM element containing the currently active cell. If no cell is active, null is returned. + * @return + **/ + public getActiveCellNode(): HTMLElement; + + /** + * Returns an object representing information about the active cell's position. All coordinates are absolute and take into consideration the visibility and scrolling position of all ancestors. + * @return + **/ + public getActiveCellPosition(): CellPosition; + + /** + * Accepts a key name, returns the group of CSS styles defined under that name. See setCellCssStyles for more info. + * @param key A string. + * @return + **/ + public getCellCssStyles(key: string): CellCssStylesHash; + + /** + * Returns the active cell editor. If there is no actively edited cell, null is returned. + * @return + **/ + public getCellEditor(): Editors.Editor<any>; + + /** + * Returns a hash containing row and cell indexes from a standard W3C/jQuery event. + * @param e A standard W3C/jQuery event. + * @return + **/ + public getCellFromEvent(e: DOMEvent): Cell; + + /** + * Returns a hash containing row and cell indexes. Coordinates are relative to the top left corner of the grid beginning with the first row (not including the column headers). + * @param x An x coordinate. + * @param y A y coordinate. + * @return + **/ + public getCellFromPoint(x: number, y: number): Cell; + + /** + * Returns a DOM element containing a cell at a given row and cell. + * @param row A row index. + * @param cell A column index. + * @return + **/ + public getCellNode(row: number, cell: number): HTMLElement; + + /** + * Returns an object representing information about a cell's position. All coordinates are absolute and take into consideration the visibility and scrolling position of all ancestors. + * @param row A row index. + * @param cell A column index. + * @return + **/ + public getCellNodeBox(row: number, cell: number): CellPosition; + + /** + * Accepts a row integer and a cell integer, scrolling the view to the row where row is its row index, and cell is its cell index. Optionally accepts a forceEdit boolean which, if true, will attempt to initiate the edit dialogue for the field in the specified cell. + * Unlike setActiveCell, this scrolls the row into the viewport and sets the keyboard focus. + * @param row A row index. + * @param cell A column index. + * @param forceEdit If true, will attempt to initiate the edit dialogue for the field in the specified cell. + * @return + **/ + public gotoCell(row: number, cell: number, forceEdit?: boolean): void; + + /** + * todo: no docs + * @return + **/ + public getTopPanel(): HTMLElement; + + /** + * todo: no docs + * @param visible + **/ + public setTopPanelVisibility(visible: boolean): void; + + /** + * todo: no docs + * @param visible + **/ + public setHeaderRowVisibility(visible: boolean): void; + + /** + * todo: no docs + * @return + **/ + public getHeaderRow(): HTMLElement; + + /** + * todo: no docs, return type is probably wrong -> "return $header && $header[0]" + * @param columnId + * @return + **/ + public getHeaderRowColumn(columnId: string): Column<any>; + + /** + * todo: no docs + * @return + **/ + public getGridPosition(): CellPosition; + + /** + * Switches the active cell one row down skipping unselectable cells. Returns a boolean saying whether it was able to complete or not. + * @return + **/ + public navigateDown(): boolean; + + /** + * Switches the active cell one cell left skipping unselectable cells. Unline navigatePrev, navigateLeft stops at the first cell of the row. Returns a boolean saying whether it was able to complete or not. + * @return + **/ + public navigateLeft(): boolean; + + /** + * Tabs over active cell to the next selectable cell. Returns a boolean saying whether it was able to complete or not. + * @return + **/ + public navigateNext(): boolean; + + /** + * Tabs over active cell to the previous selectable cell. Returns a boolean saying whether it was able to complete or not. + * @return + **/ + public navigatePrev(): boolean; + + /** + * Switches the active cell one cell right skipping unselectable cells. Unline navigateNext, navigateRight stops at the last cell of the row. Returns a boolean saying whether it was able to complete or not. + * @return + **/ + public navigateRight(): boolean; + + /** + * Switches the active cell one row up skipping unselectable cells. Returns a boolean saying whether it was able to complete or not. + * @return + **/ + public navigateUp(): boolean; + + public navigatePageUp(): boolean; + public navigatePageDown(): boolean; + public navigateTop(): boolean; + public navigateBottom(): boolean; + public navigateRowStart(): boolean; + public navigateRowEnd(): boolean; + + /** + * Removes an "overlay" of CSS classes from cell DOM elements. See setCellCssStyles for more. + * @param key A string key. + **/ + public removeCellCssStyles(key: string): void; + + /** + * Resets active cell. + **/ + public resetActiveCell(): void; + + /** + * Sets an active cell. + * @param row A row index. + * @param cell A column index. + **/ + public setActiveCell(row: number, cell: number): void; + + /** + * Sets CSS classes to specific grid cells by calling removeCellCssStyles(key) followed by addCellCssStyles(key, hash). key is name for this set of styles so you can reference it later - to modify it or remove it, for example. hash is a per-row-index, per-column-name nested hash of CSS classes to apply. + * Suppose you have a grid with columns: + * ["login", "name", "birthday", "age", "likes_icecream", "favorite_cake"] + * ...and you'd like to highlight the "birthday" and "age" columns for people whose birthday is today, in this case, rows at index 0 and 9. (The first and tenth row in the grid). + * @param key A string key. Will overwrite any data already associated with this key. + * @param hash A hash of additional cell CSS classes keyed by row number and then by column id. Multiple CSS classes can be specified and separated by space. + **/ + public setCellCssStyles(key: string, hash: CellCssStylesHash): void; + + // #endregion Cells + + // #region Events + + public onScroll: Slick.Event<OnScrollEventArgs<T>>; + public onSort: Slick.Event<OnSortEventArgs<T>>; + public onHeaderMouseEnter: Slick.Event<OnHeaderMouseEventArgs<T>>; + public onHeaderMouseLeave: Slick.Event<OnHeaderMouseEventArgs<T>>; + public onHeaderContextMenu: Slick.Event<OnHeaderContextMenuEventArgs<T>>; + public onHeaderClick: Slick.Event<OnHeaderClickEventArgs<T>>; + public onHeaderCellRendered: Slick.Event<OnHeaderCellRenderedEventArgs<T>>; + public onBeforeHeaderCellDestroy: Slick.Event<OnBeforeHeaderCellDestroyEventArgs<T>>; + public onHeaderRowCellRendered: Slick.Event<OnHeaderRowCellRenderedEventArgs<T>>; + public onBeforeHeaderRowCellDestroy: Slick.Event<OnBeforeHeaderRowCellDestroyEventArgs<T>>; + public onMouseEnter: Slick.Event<OnMouseEnterEventArgs<T>>; + public onMouseLeave: Slick.Event<OnMouseLeaveEventArgs<T>>; + public onClick: Slick.Event<OnClickEventArgs<T>>; + public onDblClick: Slick.Event<OnDblClickEventArgs<T>>; + public onContextMenu: Slick.Event<OnContextMenuEventArgs<T>>; + public onKeyDown: Slick.Event<OnKeyDownEventArgs<T>>; + public onAddNewRow: Slick.Event<OnAddNewRowEventArgs<T>>; + public onValidationError: Slick.Event<OnValidationErrorEventArgs<T>>; + public onColumnsReordered: Slick.Event<OnColumnsReorderedEventArgs<T>>; + public onColumnsResized: Slick.Event<OnColumnsResizedEventArgs<T>>; + public onCellChange: Slick.Event<OnCellChangeEventArgs<T>>; + public onBeforeEditCell: Slick.Event<OnBeforeEditCellEventArgs<T>>; + public onBeforeCellEditorDestroy: Slick.Event<OnBeforeCellEditorDestroyEventArgs<T>>; + public onBeforeDestroy: Slick.Event<OnBeforeDestroyEventArgs<T>>; + public onActiveCellChanged: Slick.Event<OnActiveCellChangedEventArgs<T>>; + public onActiveCellPositionChanged: Slick.Event<OnActiveCellPositionChangedEventArgs<T>>; + public onDragInit: Slick.Event<OnDragInitEventArgs<T>>; + public onDragStart: Slick.Event<OnDragStartEventArgs<T>>; + public onDrag: Slick.Event<OnDragEventArgs<T>>; + public onDragEnd: Slick.Event<OnDragEndEventArgs<T>>; + public onSelectedRowsChanged: Slick.Event<OnSelectedRowsChangedEventArgs<T>>; + public onCellCssStylesChanged: Slick.Event<OnCellCssStylesChangedEventArgs<T>>; + public onViewportChanged: Slick.Event<OnViewportChangedEventArgs<T>>; + // #endregion Events + + // #region Plugins + + public registerPlugin(plugin: Plugin<T>): void; + public unregisterPlugin(plugin: Plugin<T>): void; + + // #endregion Plugins + + // #region Rendering + + public render(): void; + public invalidate(): void; + public invalidateRow(row: number): void; + public invalidateRows(rows: number[]): void; + public invalidateAllRows(): void; + public updateCell(row: number, cell: number): void; + public updateRow(row: number): void; + public getViewport(viewportTop?: number, viewportLeft?: number): Viewport; + public getRenderedRange(viewportTop?: number, viewportLeft?: number): Viewport; + public resizeCanvas(): void; + public updateRowCount(): void; + public scrollRowIntoView(row: number, doPaging: boolean): void; + public scrollRowToTop(row: number): void; + public scrollCellIntoView(row: number, cell: number, doPaging: boolean): void; + public getCanvasNode(): HTMLCanvasElement; + public focus(): void; + + // #endregion Rendering + + // #region Editors + + public getEditorLock(): EditorLock<any>; + public getEditController(): { commitCurrentEdit(): boolean; cancelCurrentEdit(): boolean; }; + + // #endregion Editors + } + + export interface GridEventArgs<T extends SlickData> { + grid: Grid<T>; + } + + export interface OnCellCssStylesChangedEventArgs<T extends SlickData> extends GridEventArgs<T> { + key: string; + hash: CellCssStylesHash; + } + + export interface OnSelectedRowsChangedEventArgs<T extends SlickData> extends GridEventArgs<T> { + rows: number[]; + } + + export interface OnDragEndEventArgs<T extends SlickData> extends GridEventArgs<T> { + // todo: need to understand $canvas drag event parameter's 'dd' object + // the documentation is not enlightening + } + + export interface OnDragEventArgs<T extends SlickData> extends GridEventArgs<T> { + // todo: need to understand $canvas drag event parameter's 'dd' object + // the documentation is not enlightening + } + + export interface OnDragStartEventArgs<T extends SlickData> extends GridEventArgs<T> { + // todo: need to understand $canvas drag event parameter's 'dd' object + // the documentation is not enlightening + } + + export interface OnDragInitEventArgs<T extends SlickData> extends GridEventArgs<T> { + // todo: need to understand $canvas drag event parameter's 'dd' object + // the documentation is not enlightening + } + + export interface OnActiveCellPositionChangedEventArgs<T extends SlickData> extends GridEventArgs<T> { + + } + + export interface OnActiveCellChangedEventArgs<T extends SlickData> extends GridEventArgs<T> { + row: number; + cell: number; + } + + export interface OnBeforeDestroyEventArgs<T extends SlickData> extends GridEventArgs<T> { + + } + + export interface OnBeforeCellEditorDestroyEventArgs<T extends SlickData> extends GridEventArgs<T> { + editor: Editors.Editor<T>; + } + + export interface OnBeforeEditCellEventArgs<T extends SlickData> extends GridEventArgs<T> { + row: number; + cell: number; + item: T; + column: Column<T>; + } + + export interface OnCellChangeEventArgs<T extends SlickData> extends GridEventArgs<T> { + row: number; + cell: number; + item: T; + } + + export interface OnColumnsResizedEventArgs<T extends SlickData> extends GridEventArgs<T> { + + } + + export interface OnColumnsReorderedEventArgs<T extends SlickData> extends GridEventArgs<T> { + + } + + export interface OnValidationErrorEventArgs<T extends SlickData> extends GridEventArgs<T> { + editor: Editors.Editor<T>; + cellNode: HTMLElement; + validationResults: ValidateResults; + row: number; + cell: number; + column: Column<T>; + } + + export interface OnAddNewRowEventArgs<T extends SlickData> extends GridEventArgs<T> { + item: T; + column: Column<T>; + } + + export interface OnKeyDownEventArgs<T extends SlickData> extends GridEventArgs<T> { + row: number; + cell: number; + } + + export interface OnContextMenuEventArgs<T extends SlickData> extends GridEventArgs<T> { + + } + + export interface OnDblClickEventArgs<T extends SlickData> extends GridEventArgs<T> { + row: number; + cell: number; + } + + export interface OnClickEventArgs<T extends SlickData> extends GridEventArgs<T> { + row: number; + cell: number; + } + + export interface OnMouseLeaveEventArgs<T extends SlickData> extends GridEventArgs<T> { + + } + + export interface OnMouseEnterEventArgs<T extends SlickData> extends GridEventArgs<T> { + + } + + export interface OnBeforeHeaderRowCellDestroyEventArgs<T extends SlickData> extends GridEventArgs<T> { + node: HTMLElement; // todo: might be JQuery instance + column: Column<T>; + } + + export interface OnHeaderRowCellRenderedEventArgs<T extends SlickData> extends GridEventArgs<T> { + node: HTMLElement; // todo: might be JQuery instance + column: Column<T>; + } + + export interface OnBeforeHeaderCellDestroyEventArgs<T extends SlickData> extends GridEventArgs<T> { + node: HTMLElement; // todo: might be JQuery instance + column: Column<T>; + } + + export interface OnHeaderCellRenderedEventArgs<T extends SlickData> extends GridEventArgs<T> { + node: HTMLElement; // todo: might be JQuery instance + column: Column<T>; + } + + export interface OnHeaderClickEventArgs<T extends SlickData> extends GridEventArgs<T> { + column: Column<T>; + } + + export interface OnHeaderContextMenuEventArgs<T extends SlickData> extends GridEventArgs<T> { + column: Column<T>; + } + + export interface OnHeaderMouseEventArgs<T extends SlickData> extends GridEventArgs<T> { + column: Column<T>; + } + + export interface OnSortEventArgs<T extends SlickData> extends GridEventArgs<T> { + multiColumnSort: boolean; + + // Single column returned + sortCol?: Column<T>; + sortAsc: boolean; + + // Multiple columns returned + sortCols?: SortColumn<T>[]; + } + + export interface OnScrollEventArgs<T extends SlickData> extends GridEventArgs<T> { + scrollLeft: number; + scrollTop: number; + } + + export interface OnViewportChangedEventArgs<T extends SlickData> extends GridEventArgs<T> { + + } + + export interface SortColumn<T extends SlickData> { + sortCol: Column<T>; + sortAsc: boolean; + } + + export interface Cell { + row: number; + cell: number; + } + + export interface Position { + top: number; + left: number; + } + + export interface CellPosition extends Position { + bottom: number; + height: number; + right: number; + visible: boolean; + width: number; + } + + export interface CellCssStylesHash { + [index: number]: { + [id: string]: string; + }; + } + + export interface Viewport { + top: number; + bottom: number; + leftPx: number; + rightPx: number; + } + + export interface ValidateResults { + valid: boolean; + msg: string; + } + + export namespace Editors { + + export interface EditorOptions<T extends Slick.SlickData> { + column: Column<T>; + container: HTMLElement; + grid: Grid<T>; + + item?: T; + commitChanges?: () => void; + cancelChanges?: () => void; + gridPosition?: CellPosition; + position?: CellPosition; + + } + + export class Editor<T extends Slick.SlickData> { + constructor(args: EditorOptions<T>); + public init(): void; + public destroy(): void; + public focus(): void; + public loadValue(item: T): void; + public applyValue(item: T, state: string): void; + public isValueChanged(): boolean; + public serializeValue(): any; + public validate(): ValidateResults; + } + + export class Text<T extends Slick.SlickData> extends Editor<T> { + constructor(args: EditorOptions<T>); + + public getValue(): string; + public setValue(val: string): void; + public serializeValue(): string; + } + + export class Integer<T extends Slick.SlickData> extends Editor<T> { + constructor(args: EditorOptions<T>); + + public serializeValue(): number; + } + + export class Date<T extends Slick.SlickData> extends Editor<T> { + constructor(args: EditorOptions<T>); + + public show(): void; + public hide(): void; + public position(position: Position): void; + public serializeValue(): string; + } + + export class YesNoSelect<T extends Slick.SlickData> extends Editor<T> { + constructor(args: EditorOptions<T>); + + public serializeValue(): boolean; + } + + export class Checkbox<T extends Slick.SlickData> extends Editor<T> { + constructor(args: EditorOptions<T>); + + public serializeValue(): boolean; + + } + export class PercentComplete<T extends Slick.SlickData> extends Editor<T> { + constructor(args: EditorOptions<T>); + + public serializeValue(): number; + } + + export class LongText<T extends Slick.SlickData> extends Editor<T> { + constructor(args: EditorOptions<T>); + + public handleKeyDown(e: DOMEvent): void; + public save(): void; + public cancel(): void; + public hide(): void; + public show(): void; + public position(position: Position): void; + public serializeValue(): string; + } + } + + export interface Formatter<T extends SlickData> { + (row: number, cell: number, value: any, columnDef: Column<T>, dataContext: SlickData): string; + } + + export module Formatters { + var PercentComplete: Formatter<Slick.SlickData>; + var PercentCompleteBar: Formatter<Slick.SlickData>; + var YesNo: Formatter<Slick.SlickData>; + var Checkmark: Formatter<Slick.SlickData>; + } + + export module Data { + + export interface DataViewOptions<T extends Slick.SlickData> { + groupItemMetadataProvider?: GroupItemMetadataProvider<T>; + inlineFilters?: boolean; + } + + /** + * Item -> Data by index + * Row -> Data by row + **/ + export class DataView<T extends Slick.SlickData> implements DataProvider<T> { + + constructor(options?: DataViewOptions<T>); + + public beginUpdate(): void; + public endUpdate(): void; + public setPagingOptions(args: PagingOptions): void; + public getPagingInfo(): PagingOptions; + public getItems(): T[]; + public setItems(data: T[], objectIdProperty?: string): void; + public setFilter(filterFn: (item: T, args: any) => boolean): void; // todo: typeof(args) + public sort(comparer: Function, ascending: boolean): void; // todo: typeof(comparer), should be the same callback as Array.sort + public fastSort(field: string, ascending: boolean): void; + public fastSort(field: Function, ascending: boolean): void; // todo: typeof(field), should be the same callback as Array.sort + public reSort(): void; + public setGrouping(groupingInfos: GroupingOptions<T> | GroupingOptions<T>[]): void; + public getGrouping(): GroupingOptions<T>[]; + + /** + * @deprecated + **/ + public groupBy(valueGetter: any, valueFormatter: any, sortComparer: any): void; + + /** + * @deprecated + **/ + public setAggregators(groupAggregators: any, includeCollapsed: any): void; + + /** + * @param level Optional level to collapse. If not specified, applies to all levels. + **/ + public collapseAllGroups(level?: number): void; + + /** + * @param level Optional level to collapse. If not specified, applies to all levels. + **/ + public expandAllGroups(level?: number): void; + + /** + * @param varArgs Either a Slick.Group's "groupingKey" property, or a + * variable argument list of grouping values denoting a unique path to the row. For + * example, calling collapseGroup('high', '10%') will collapse the '10%' subgroup of + * the 'high' setGrouping. + */ + public collapseGroup(...varArgs: string[]): void; + + /** + * @param varArgs Either a Slick.Group's "groupingKey" property, or a + * variable argument list of grouping values denoting a unique path to the row. For + * example, calling expandGroup('high', '10%') will expand the '10%' subgroup of + * the 'high' setGrouping. + */ + public expandGroup(...varArgs: string[]): void; + public getGroups(): Group<T>[]; + public getIdxById(id: string): number; + public getRowById(id: string): number; + public getItemById(id: any): T; + public getItemByIdx(idx: number): T; + public mapRowsToIds(rowArray: number[]): string[]; + public setRefreshHints(hints: RefreshHints): void; + public setFilterArgs(args: any): void; + public refresh(): void; + public updateItem(id: string, item: T): void; + public insertItem(insertBefore: number, item: T): void; + public addItem(item: T): void; + public deleteItem(id: string): void; + public syncGridSelection(grid: Grid<T>, preserveHidden: boolean): void; + public syncGridCellCssStyles(grid: Grid<T>, key: string): void; + + public getLength(): number; + public getItem(index: number): T; + public getItemMetadata(index: number): RowMetadata<T>; + + public onRowCountChanged: Slick.Event<OnRowCountChangedEventData>; + public onRowsChanged: Slick.Event<OnRowsChangedEventData>; + public onPagingInfoChanged: Slick.Event<OnPagingInfoChangedEventData>; + } + + export interface GroupingOptions<T> { + getter?: ((item?: T) => any) | string; + formatter?: (item?: T) => string; + comparer?: (a: Group<T>, b: Group<T>) => number; + predefinedValues?: any[]; // todo + aggregators?: Aggregators.Aggregator<T>[]; + aggregateEmpty?: boolean; + aggregateCollapsed?: boolean; + aggregateChildGroups?: boolean; + collapsed?: boolean; + displayTotalsRow?: boolean; + } + + export interface PagingOptions { + pageSize?: number; + pageNum?: number; + totalRows?: number; + totalPages?: number; + } + + export interface RefreshHints { + isFilterNarrowing?: boolean; + isFilterExpanding?: boolean; + isFilterUnchanged?: boolean; + ignoreDiffsBefore?: boolean; + ignoreDiffsAfter?: boolean; + } + + export interface OnRowCountChangedEventData { + // empty + } + export interface OnRowsChangedEventData { + rows: number[]; + } + export interface OnPagingInfoChangedEventData extends PagingOptions { + + } + + export module Aggregators { + export class Aggregator<T extends Slick.SlickData> { + public field: string; + public init(): void; + public accumulate(item: T): void; + public storeResult(groupTotals: GroupTotals<T>): void; + } + + export class Avg<T> extends Aggregator<T> { + + } + + export class Min<T> extends Aggregator<T> { + + } + + export class Max<T> extends Aggregator<T> { + + } + + export class Sum<T> extends Aggregator<T> { + + } + } + + /*** + * Provides item metadata for group (Slick.Group) and totals (Slick.Totals) rows produced by the DataView. + * This metadata overrides the default behavior and formatting of those rows so that they appear and function + * correctly when processed by the grid. + * + * This class also acts as a grid plugin providing event handlers to expand & collapse groups. + * If "grid.registerPlugin(...)" is not called, expand & collapse will not work. + * + * @class GroupItemMetadataProvider + * @module Data + * @namespace Slick.Data + * @constructor + * @param options + */ + export class GroupItemMetadataProvider<T extends Slick.SlickData> { + public init(): void; + public destroy(): void; + public getGroupRowMetadata(item?: Group<T>): RowMetadata<T>; + public getTotalsRowMetadata(item?: GroupTotals<T>): RowMetadata<T>; + } + + export interface GroupItemMetadataProviderOptions { + groupCssClass?: string; + groupTitleCssClass?: string; + totalsCssClass?: string; + groupFocusable?: boolean; + totalsFocusable?: boolean; + toggleCssClass?: string; + toggleExpandedCssCass?: string; + toggleCollapsedCssClass?: string; + enableExpandCollapse?: boolean; + } + + //export class RemoteModel { + // public data: any; + + // public clear(): any; + // public isDataLoaded(): any; + // public ensureData(): any; + // public reloadData(): any; + // public setSort(): any; + // public setSearch(): any; + + // public onDataLoading: Slick.Event<OnDataLoadingEventData>; + // public onDataLoaded: Slick.Event<OnDataLoadedEventData>; + + //} + + //export interface OnDataLoadingEventData { + + //} + + //export interface OnDataLoadedEventData { + + //} + } + + export class Plugin<T extends Slick.SlickData> { + + constructor(options?: PluginOptions); + public init(grid: Grid<T>): void; + public destroy(): void; + } + + export interface PluginOptions { + // extend your plugin options here + } + +} diff --git a/types/slickgrid/plugins/slick.autotooltips.d.ts b/types/slickgrid/plugins/slick.autotooltips.d.ts new file mode 100644 index 000000000000..2c33e524311a --- /dev/null +++ b/types/slickgrid/plugins/slick.autotooltips.d.ts @@ -0,0 +1,35 @@ +// Type definitions for SlickGrid AutoToolTips Plugin 2.1.0 +// Project: https://github.com/mleibman/SlickGrid +// Definitions by: Ryo Iwamoto <https://github.com/ryiwamoto> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + + + +declare namespace Slick { + export interface SlickGridAutoTooltipsOption extends PluginOptions { + /** + * Enable tooltip for grid cells + * @default true + */ + enableForCells?: boolean; + + /** + * Enable tooltip for header cells + * @default false + */ + enableForHeaderCells?: boolean; + + /** + * The maximum length for a tooltip + * @default null + */ + maxToolTipLength?: number; + } + + /** + * AutoTooltips plugin to show/hide tooltips when columns are too narrow to fit content. + */ + export class AutoTooltips extends Plugin<Slick.SlickData> { + constructor(option?: SlickGridAutoTooltipsOption); + } +} diff --git a/types/slickgrid/plugins/slick.checkboxselectcolumn.d.ts b/types/slickgrid/plugins/slick.checkboxselectcolumn.d.ts new file mode 100644 index 000000000000..ab97e4612389 --- /dev/null +++ b/types/slickgrid/plugins/slick.checkboxselectcolumn.d.ts @@ -0,0 +1,39 @@ +// Type definitions for SlickGrid CheckboxSelectColumn Plugin 2.1.0 +// Project: https://github.com/mleibman/SlickGrid +// Definitions by: berwyn <https://github.com/berwyn> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +declare namespace Slick { + export interface SlickGridCheckBoxSelectColumnOptions extends PluginOptions { + /** + * Column to add the checkbox to + * @default "_checkbox_selector" + */ + columnId?: string; + + /** + * CSS class to be added to cells in this column + * @default null + */ + cssClass?: string; + + /** + * Tooltip text to display for this column + * @default "Select/Deselect All" + */ + toolTip?: string; + + /** + * Width of the column + * @default 30 + */ + width?: number; + } + + export class CheckboxSelectColumn<T extends Slick.SlickData> extends Plugin<T> { + constructor(options?: SlickGridCheckBoxSelectColumnOptions); + init(grid: Slick.Grid<T>): void; + destroy(): void; + getColumnDefinition(): Slick.ColumnMetadata<T>; + } +} diff --git a/types/slickgrid/plugins/slick.columnpicker.d.ts b/types/slickgrid/plugins/slick.columnpicker.d.ts new file mode 100644 index 000000000000..e0a0269282de --- /dev/null +++ b/types/slickgrid/plugins/slick.columnpicker.d.ts @@ -0,0 +1,18 @@ +// Type definitions for SlickGrid ColumnPicker Control 2.1.0 +// Project: https://github.com/mleibman/SlickGrid +// Definitions by: berwyn <https://github.com/berwyn> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +declare namespace Slick { + export namespace Controls { + export interface SlickColumnPickerOptions { + fadeSpeed?: number; + } + + export class ColumnPicker<T extends Slick.SlickData> { + constructor(columns: Slick.Column<T>[], grid: Slick.Grid<T>, options: SlickColumnPickerOptions); + getAllColumns(): Slick.Column<T>[]; + destroy(): void; + } + } +} diff --git a/types/slickgrid/plugins/slick.headerbuttons.d.ts b/types/slickgrid/plugins/slick.headerbuttons.d.ts new file mode 100644 index 000000000000..a4191a79f36a --- /dev/null +++ b/types/slickgrid/plugins/slick.headerbuttons.d.ts @@ -0,0 +1,41 @@ +// Type definitions for SlickGrid HeaderButtons Plugin 2.1.0 +// Project: https://github.com/mleibman/SlickGrid +// Definitions by: Derek Cicerone <https://github.com/derekcicerone/> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + + + +declare namespace Slick { + + export interface Column<T extends SlickData> { + header?: Header; + } + + export interface Header { + buttons: HeaderButton[]; + } + + export interface HeaderButton { + command?: string; + cssClass?: string; + handler?: Function; + image?: string; + showOnHover?: boolean; + tooltip?: string; + } + + export interface OnCommandEventArgs<T extends SlickData> { + grid: Grid<T>; + column: Column<T>; + command: string; + button: HeaderButton; + } + + export module Plugins { + + export class HeaderButtons<T extends SlickData> extends Plugin<T> { + constructor(); + public onCommand: Event<OnCommandEventArgs<T>>; + } + } +} diff --git a/types/slickgrid/plugins/slick.rowselectionmodel.d.ts b/types/slickgrid/plugins/slick.rowselectionmodel.d.ts new file mode 100644 index 000000000000..2c06f6359133 --- /dev/null +++ b/types/slickgrid/plugins/slick.rowselectionmodel.d.ts @@ -0,0 +1,20 @@ +// Type definitions for SlickGrid RowSelectionModel Plugin 2.1.0 +// Project: https://github.com/mleibman/SlickGrid +// Definitions by: Derek Cicerone <https://github.com/derekcicerone/> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + + + +declare namespace Slick { + class RowSelectionModel<T extends SlickData, E> extends SelectionModel<T, E> { + constructor(options?:{selectActiveRow:boolean;}); + + getSelectedRows():number[]; + + setSelectedRows(rows:number[]):void; + + getSelectedRanges():number[]; + + setSelectedRanges(ranges:number[]):void; + } +} diff --git a/types/svg-inline-react.d.ts b/types/svg-inline-react.d.ts new file mode 100644 index 000000000000..46e748682709 --- /dev/null +++ b/types/svg-inline-react.d.ts @@ -0,0 +1,28 @@ +declare module 'svg-inline-react' { + export class InlineSVG { + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + props: any; + state: any; + context: any; + refs: any; + } + export default class _default { + constructor(...args: any[]); + componentDidMount(): void; + componentDidUpdate(): void; + forceUpdate(callback: any): void; + render(): any; + setState(partialState: any, callback: any): void; + shouldComponentUpdate(nextProps: any): any; + props: any; + state: any; + context: any; + refs: any; + } +} diff --git a/types/vscode-proposed/index.d.ts b/types/vscode-proposed/index.d.ts new file mode 100644 index 000000000000..2abb5824d26f --- /dev/null +++ b/types/vscode-proposed/index.d.ts @@ -0,0 +1,691 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Event, + GlobPattern, + Uri, + TextDocument, + ViewColumn, + CancellationToken, + Disposable, + DocumentSelector, + ProviderResult, + WorkspaceEditEntryMetadata, + Command, + AccessibilityInformation +} from 'vscode'; + +// Copy nb section from https://github.com/microsoft/vscode/blob/master/src/vs/vscode.proposed.d.ts. +export enum CellKind { + Markdown = 1, + Code = 2 +} + +export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3 +} + +export interface CellStreamOutput { + outputKind: CellOutputKind.Text; + text: string; +} + +export interface CellErrorOutput { + outputKind: CellOutputKind.Error; + /** + * Exception Name + */ + ename: string; + /** + * Exception Value + */ + evalue: string; + /** + * Exception call stack + */ + traceback: string[]; +} + +export interface NotebookCellOutputMetadata { + /** + * Additional attributes of a cell metadata. + */ + custom?: { [key: string]: any }; +} + +export interface CellDisplayOutput { + outputKind: CellOutputKind.Rich; + /** + * { mime_type: value } + * + * Example: + * ```json + * { + * "outputKind": vscode.CellOutputKind.Rich, + * "data": { + * "text/html": [ + * "<h1>Hello</h1>" + * ], + * "text/plain": [ + * "<IPython.lib.display.IFrame at 0x11dee3e80>" + * ] + * } + * } + */ + data: { [key: string]: any }; + + readonly metadata?: NotebookCellOutputMetadata; +} + +export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; + +export enum NotebookCellRunState { + Running = 1, + Idle = 2, + Success = 3, + Error = 4 +} + +export enum NotebookRunState { + Running = 1, + Idle = 2 +} + +export interface NotebookCellMetadata { + /** + * Controls whether a cell's editor is editable/readonly. + */ + editable?: boolean; + + /** + * Controls if the cell is executable. + * This metadata is ignored for markdown cell. + */ + runnable?: boolean; + + /** + * Controls if the cell has a margin to support the breakpoint UI. + * This metadata is ignored for markdown cell. + */ + breakpointMargin?: boolean; + + /** + * Whether the [execution order](#NotebookCellMetadata.executionOrder) indicator will be displayed. + * Defaults to true. + */ + hasExecutionOrder?: boolean; + + /** + * The order in which this cell was executed. + */ + executionOrder?: number; + + /** + * A status message to be shown in the cell's status bar + */ + statusMessage?: string; + + /** + * The cell's current run state + */ + runState?: NotebookCellRunState; + + /** + * If the cell is running, the time at which the cell started running + */ + runStartTime?: number; + + /** + * The total duration of the cell's last run + */ + lastRunDuration?: number; + + /** + * Whether a code cell's editor is collapsed + */ + inputCollapsed?: boolean; + + /** + * Whether a code cell's outputs are collapsed + */ + outputCollapsed?: boolean; + + /** + * Additional attributes of a cell metadata. + */ + custom?: { [key: string]: any }; +} + +export interface NotebookCell { + readonly notebook: NotebookDocument; + readonly uri: Uri; + readonly cellKind: CellKind; + readonly document: TextDocument; + readonly language: string; + outputs: CellOutput[]; + metadata: NotebookCellMetadata; +} + +export interface NotebookDocumentMetadata { + /** + * Controls if users can add or delete cells + * Defaults to true + */ + editable?: boolean; + + /** + * Controls whether the full notebook can be run at once. + * Defaults to true + */ + runnable?: boolean; + + /** + * Default value for [cell editable metadata](#NotebookCellMetadata.editable). + * Defaults to true. + */ + cellEditable?: boolean; + + /** + * Default value for [cell runnable metadata](#NotebookCellMetadata.runnable). + * Defaults to true. + */ + cellRunnable?: boolean; + + /** + * Default value for [cell hasExecutionOrder metadata](#NotebookCellMetadata.hasExecutionOrder). + * Defaults to true. + */ + cellHasExecutionOrder?: boolean; + + displayOrder?: GlobPattern[]; + + /** + * Additional attributes of the document metadata. + */ + custom?: { [key: string]: any }; + + /** + * The document's current run state + */ + runState?: NotebookRunState; +} + +export interface NotebookDocument { + readonly uri: Uri; + readonly version: number; + readonly fileName: string; + readonly viewType: string; + readonly isDirty: boolean; + readonly isUntitled: boolean; + readonly cells: ReadonlyArray<NotebookCell>; + languages: string[]; + metadata: NotebookDocumentMetadata; +} + +export interface NotebookConcatTextDocument { + uri: Uri; + isClosed: boolean; + dispose(): void; + onDidChange: Event<void>; + version: number; + getText(): string; + getText(range: Range): string; + + offsetAt(position: Position): number; + positionAt(offset: number): Position; + validateRange(range: Range): Range; + validatePosition(position: Position): Position; + + locationAt(positionOrRange: Position | Range): Location; + positionAt(location: Location): Position; + contains(uri: Uri): boolean; +} + +export interface WorkspaceEdit { + replaceCells( + uri: Uri, + start: number, + end: number, + cells: NotebookCellData[], + metadata?: WorkspaceEditEntryMetadata + ): void; + replaceCellOutput(uri: Uri, index: number, outputs: CellOutput[], metadata?: WorkspaceEditEntryMetadata): void; + replaceCellMetadata( + uri: Uri, + index: number, + cellMetadata: NotebookCellMetadata, + metadata?: WorkspaceEditEntryMetadata + ): void; +} + +export interface NotebookEditorCellEdit { + replaceCells(start: number, end: number, cells: NotebookCellData[]): void; + replaceOutput(index: number, outputs: CellOutput[]): void; + replaceMetadata(index: number, metadata: NotebookCellMetadata): void; + + /** @deprecated */ + insert( + index: number, + content: string | string[], + language: string, + type: CellKind, + outputs: CellOutput[], + metadata: NotebookCellMetadata | undefined + ): void; + /** @deprecated */ + delete(index: number): void; +} + +export interface NotebookCellRange { + readonly start: number; + readonly end: number; +} + +export enum NotebookEditorRevealType { + /** + * The range will be revealed with as little scrolling as possible. + */ + Default = 0, + /** + * The range will always be revealed in the center of the viewport. + */ + InCenter = 1, + /** + * If the range is outside the viewport, it will be revealed in the center of the viewport. + * Otherwise, it will be revealed with as little scrolling as possible. + */ + InCenterIfOutsideViewport = 2 +} + +export interface NotebookEditor { + /** + * The document associated with this notebook editor. + */ + readonly document: NotebookDocument; + + /** + * The primary selected cell on this notebook editor. + */ + readonly selection?: NotebookCell; + + /** + * The current visible ranges in the editor (vertically). + */ + readonly visibleRanges: NotebookCellRange[]; + + /** + * The column in which this editor shows. + */ + readonly viewColumn?: ViewColumn; + + /** + * Whether the panel is active (focused by the user). + */ + readonly active: boolean; + + /** + * Whether the panel is visible. + */ + readonly visible: boolean; + + /** + * Fired when the panel is disposed. + */ + readonly onDidDispose: Event<void>; + + /** + * Active kernel used in the editor + */ + readonly kernel?: NotebookKernel; + + /** + * Fired when the output hosting webview posts a message. + */ + readonly onDidReceiveMessage: Event<any>; + /** + * Post a message to the output hosting webview. + * + * Messages are only delivered if the editor is live. + * + * @param message Body of the message. This must be a string or other json serilizable object. + */ + postMessage(message: any): Thenable<boolean>; + + /** + * Convert a uri for the local file system to one that can be used inside outputs webview. + */ + asWebviewUri(localResource: Uri): Uri; + + edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable<boolean>; + + revealRange(range: NotebookCellRange, revealType?: NotebookEditorRevealType): void; +} + +export interface NotebookOutputSelector { + mimeTypes?: string[]; +} + +export interface NotebookRenderRequest { + output: CellDisplayOutput; + mimeType: string; + outputId: string; +} + +export interface NotebookCellsChangeData { + readonly start: number; + readonly deletedCount: number; + readonly deletedItems: NotebookCell[]; + readonly items: NotebookCell[]; +} + +export interface NotebookCellsChangeEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly changes: ReadonlyArray<NotebookCellsChangeData>; +} + +export interface NotebookCellMoveEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly index: number; + readonly newIndex: number; +} + +export interface NotebookCellOutputsChangeEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly cells: NotebookCell[]; +} + +export interface NotebookCellLanguageChangeEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly cell: NotebookCell; + readonly language: string; +} + +export interface NotebookCellMetadataChangeEvent { + readonly document: NotebookDocument; + readonly cell: NotebookCell; +} + +export interface NotebookEditorSelectionChangeEvent { + readonly notebookEditor: NotebookEditor; + readonly selection?: NotebookCell; +} + +export interface NotebookEditorVisibleRangesChangeEvent { + readonly notebookEditor: NotebookEditor; + readonly visibleRanges: ReadonlyArray<NotebookCellRange>; +} + +export interface NotebookCellData { + readonly cellKind: CellKind; + readonly source: string; + readonly language: string; + readonly outputs: CellOutput[]; + readonly metadata: NotebookCellMetadata | undefined; +} + +export interface NotebookData { + readonly cells: NotebookCellData[]; + readonly languages: string[]; + readonly metadata: NotebookDocumentMetadata; +} + +interface NotebookDocumentContentChangeEvent { + /** + * The document that the edit is for. + */ + readonly document: NotebookDocument; +} + +interface NotebookDocumentEditEvent { + /** + * The document that the edit is for. + */ + readonly document: NotebookDocument; + + /** + * Undo the edit operation. + * + * This is invoked by VS Code when the user undoes this edit. To implement `undo`, your + * extension should restore the document and editor to the state they were in just before this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + undo(): Thenable<void> | void; + + /** + * Redo the edit operation. + * + * This is invoked by VS Code when the user redoes this edit. To implement `redo`, your + * extension should restore the document and editor to the state they were in just after this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + redo(): Thenable<void> | void; + + /** + * Display name describing the edit. + * + * This will be shown to users in the UI for undo/redo operations. + */ + readonly label?: string; +} + +interface NotebookDocumentBackup { + /** + * Unique identifier for the backup. + * + * This id is passed back to your extension in `openCustomDocument` when opening a notebook editor from a backup. + */ + readonly id: string; + + /** + * Delete the current backup. + * + * This is called by VS Code when it is clear the current backup is no longer needed, such as when a new backup + * is made or when the file is saved. + */ + delete(): void; +} + +interface NotebookDocumentBackupContext { + readonly destination: Uri; +} + +interface NotebookDocumentOpenContext { + readonly backupId?: string; +} + +/** + * Communication object passed to the {@link NotebookContentProvider} and + * {@link NotebookOutputRenderer} to communicate with the webview. + */ +export interface NotebookCommunication { + /** + * ID of the editor this object communicates with. A single notebook + * document can have multiple attached webviews and editors, when the + * notebook is split for instance. The editor ID lets you differentiate + * between them. + */ + readonly editorId: string; + + /** + * Fired when the output hosting webview posts a message. + */ + readonly onDidReceiveMessage: Event<any>; + /** + * Post a message to the output hosting webview. + * + * Messages are only delivered if the editor is live. + * + * @param message Body of the message. This must be a string or other json serilizable object. + */ + postMessage(message: any): Thenable<boolean>; + + /** + * Convert a uri for the local file system to one that can be used inside outputs webview. + */ + asWebviewUri(localResource: Uri): Uri; +} + +export interface NotebookContentProvider { + /** + * Content providers should always use [file system providers](#FileSystemProvider) to + * resolve the raw content for `uri` as the resouce is not necessarily a file on disk. + */ + openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Promise<NotebookData>; + resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Promise<void>; + saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise<void>; + saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise<void>; + readonly onDidChangeNotebook: Event<NotebookDocumentContentChangeEvent | NotebookDocumentEditEvent>; + backupNotebook( + document: NotebookDocument, + context: NotebookDocumentBackupContext, + cancellation: CancellationToken + ): Promise<NotebookDocumentBackup>; +} + +export interface NotebookKernel { + readonly id?: string; + label: string; + description?: string; + detail?: string; + isPreferred?: boolean; + preloads?: Uri[]; + executeCell(document: NotebookDocument, cell: NotebookCell): void; + cancelCellExecution(document: NotebookDocument, cell: NotebookCell): void; + executeAllCells(document: NotebookDocument): void; + cancelAllCellsExecution(document: NotebookDocument): void; +} + +export interface NotebookDocumentFilter { + viewType?: string | string[]; + filenamePattern?: GlobPattern | { include: GlobPattern; exclude: GlobPattern }; +} + +export interface NotebookKernelProvider<T extends NotebookKernel = NotebookKernel> { + onDidChangeKernels?: Event<NotebookDocument | undefined>; + provideKernels(document: NotebookDocument, token: CancellationToken): ProviderResult<T[]>; + resolveKernel?( + kernel: T, + document: NotebookDocument, + webview: NotebookCommunication, + token: CancellationToken + ): ProviderResult<void>; +} + +/** + * Represents the alignment of status bar items. + */ +export enum NotebookCellStatusBarAlignment { + /** + * Aligned to the left side. + */ + Left = 1, + + /** + * Aligned to the right side. + */ + Right = 2 +} + +export interface NotebookCellStatusBarItem { + readonly cell: NotebookCell; + readonly alignment: NotebookCellStatusBarAlignment; + readonly priority?: number; + text: string; + tooltip: string | undefined; + command: string | Command | undefined; + accessibilityInformation?: AccessibilityInformation; + show(): void; + hide(): void; + dispose(): void; +} + +export namespace notebook { + export function registerNotebookContentProvider( + notebookType: string, + provider: NotebookContentProvider, + options?: { + /** + * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. + */ + transientOutputs: boolean; + /** + * Controls if a meetadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientMetadata: { [K in keyof NotebookCellMetadata]?: boolean }; + } + ): Disposable; + + export function registerNotebookKernelProvider( + selector: NotebookDocumentFilter, + provider: NotebookKernelProvider + ): Disposable; + + export const onDidOpenNotebookDocument: Event<NotebookDocument>; + export const onDidCloseNotebookDocument: Event<NotebookDocument>; + export const onDidSaveNotebookDocument: Event<NotebookDocument>; + + /** + * All currently known notebook documents. + */ + export const notebookDocuments: ReadonlyArray<NotebookDocument>; + + export const visibleNotebookEditors: NotebookEditor[]; + export const onDidChangeVisibleNotebookEditors: Event<NotebookEditor[]>; + + export const activeNotebookEditor: NotebookEditor | undefined; + export const onDidChangeActiveNotebookEditor: Event<NotebookEditor | undefined>; + export const onDidChangeNotebookEditorSelection: Event<NotebookEditorSelectionChangeEvent>; + export const onDidChangeNotebookEditorVisibleRanges: Event<NotebookEditorVisibleRangesChangeEvent>; + export const onDidChangeNotebookCells: Event<NotebookCellsChangeEvent>; + export const onDidChangeCellOutputs: Event<NotebookCellOutputsChangeEvent>; + export const onDidChangeCellLanguage: Event<NotebookCellLanguageChangeEvent>; + export const onDidChangeCellMetadata: Event<NotebookCellMetadataChangeEvent>; + /** + * Create a document that is the concatenation of all notebook cells. By default all code-cells are included + * but a selector can be provided to narrow to down the set of cells. + * + * @param notebook + * @param selector + */ + export function createConcatTextDocument( + notebook: NotebookDocument, + selector?: DocumentSelector + ): NotebookConcatTextDocument; + + export const onDidChangeActiveNotebookKernel: Event<{ + document: NotebookDocument; + kernel: NotebookKernel | undefined; + }>; + + /** + * Creates a notebook cell status bar [item](#NotebookCellStatusBarItem). + * It will be disposed automatically when the notebook document is closed or the cell is deleted. + * + * @param cell The cell on which this item should be shown. + * @param alignment The alignment of the item. + * @param priority The priority of the item. Higher values mean the item should be shown more to the left. + * @return A new status bar item. + */ + export function createCellStatusBarItem( + cell: NotebookCell, + alignment?: NotebookCellStatusBarAlignment, + priority?: number + ): NotebookCellStatusBarItem; +} diff --git a/types/vscode.proposed.d.ts b/types/vscode.proposed.d.ts new file mode 100644 index 000000000000..9f80ab43e6df --- /dev/null +++ b/types/vscode.proposed.d.ts @@ -0,0 +1,678 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Copy nb section from https://github.com/microsoft/vscode/blob/master/src/vs/vscode.proposed.d.ts. +declare module 'vscode' { + export enum CellKind { + Markdown = 1, + Code = 2 + } + + export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3 + } + + export interface CellStreamOutput { + outputKind: CellOutputKind.Text; + text: string; + } + + export interface CellErrorOutput { + outputKind: CellOutputKind.Error; + /** + * Exception Name + */ + ename: string; + /** + * Exception Value + */ + evalue: string; + /** + * Exception call stack + */ + traceback: string[]; + } + + export interface NotebookCellOutputMetadata { + /** + * Additional attributes of a cell metadata. + */ + custom?: { [key: string]: any }; + } + + export interface CellDisplayOutput { + outputKind: CellOutputKind.Rich; + /** + * { mime_type: value } + * + * Example: + * ```json + * { + * "outputKind": vscode.CellOutputKind.Rich, + * "data": { + * "text/html": [ + * "<h1>Hello</h1>" + * ], + * "text/plain": [ + * "<IPython.lib.display.IFrame at 0x11dee3e80>" + * ] + * } + * } + */ + data: { [key: string]: any }; + + readonly metadata?: NotebookCellOutputMetadata; + } + + export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; + + export enum NotebookCellRunState { + Running = 1, + Idle = 2, + Success = 3, + Error = 4 + } + + export enum NotebookRunState { + Running = 1, + Idle = 2 + } + + export interface NotebookCellMetadata { + /** + * Controls whether a cell's editor is editable/readonly. + */ + editable?: boolean; + + /** + * Controls if the cell is executable. + * This metadata is ignored for markdown cell. + */ + runnable?: boolean; + + /** + * Controls if the cell has a margin to support the breakpoint UI. + * This metadata is ignored for markdown cell. + */ + breakpointMargin?: boolean; + + /** + * Whether the [execution order](#NotebookCellMetadata.executionOrder) indicator will be displayed. + * Defaults to true. + */ + hasExecutionOrder?: boolean; + + /** + * The order in which this cell was executed. + */ + executionOrder?: number; + + /** + * A status message to be shown in the cell's status bar + */ + statusMessage?: string; + + /** + * The cell's current run state + */ + runState?: NotebookCellRunState; + + /** + * If the cell is running, the time at which the cell started running + */ + runStartTime?: number; + + /** + * The total duration of the cell's last run + */ + lastRunDuration?: number; + + /** + * Whether a code cell's editor is collapsed + */ + inputCollapsed?: boolean; + + /** + * Whether a code cell's outputs are collapsed + */ + outputCollapsed?: boolean; + + /** + * Additional attributes of a cell metadata. + */ + custom?: { [key: string]: any }; + } + + export interface NotebookCell { + readonly notebook: NotebookDocument; + readonly uri: Uri; + readonly cellKind: CellKind; + readonly document: TextDocument; + readonly language: string; + outputs: CellOutput[]; + metadata: NotebookCellMetadata; + } + + export interface NotebookDocumentMetadata { + /** + * Controls if users can add or delete cells + * Defaults to true + */ + editable?: boolean; + + /** + * Controls whether the full notebook can be run at once. + * Defaults to true + */ + runnable?: boolean; + + /** + * Default value for [cell editable metadata](#NotebookCellMetadata.editable). + * Defaults to true. + */ + cellEditable?: boolean; + + /** + * Default value for [cell runnable metadata](#NotebookCellMetadata.runnable). + * Defaults to true. + */ + cellRunnable?: boolean; + + /** + * Default value for [cell hasExecutionOrder metadata](#NotebookCellMetadata.hasExecutionOrder). + * Defaults to true. + */ + cellHasExecutionOrder?: boolean; + + displayOrder?: GlobPattern[]; + + /** + * Additional attributes of the document metadata. + */ + custom?: { [key: string]: any }; + + /** + * The document's current run state + */ + runState?: NotebookRunState; + } + + export interface NotebookDocument { + readonly uri: Uri; + readonly version: number; + readonly fileName: string; + readonly viewType: string; + readonly isDirty: boolean; + readonly isUntitled: boolean; + readonly cells: ReadonlyArray<NotebookCell>; + languages: string[]; + metadata: NotebookDocumentMetadata; + } + + export interface NotebookConcatTextDocument { + uri: Uri; + isClosed: boolean; + dispose(): void; + onDidChange: Event<void>; + version: number; + getText(): string; + getText(range: Range): string; + + offsetAt(position: Position): number; + positionAt(offset: number): Position; + validateRange(range: Range): Range; + validatePosition(position: Position): Position; + + locationAt(positionOrRange: Position | Range): Location; + positionAt(location: Location): Position; + contains(uri: Uri): boolean; + } + + export interface WorkspaceEdit { + replaceCells( + uri: Uri, + start: number, + end: number, + cells: NotebookCellData[], + metadata?: WorkspaceEditEntryMetadata + ): void; + replaceCellOutput(uri: Uri, index: number, outputs: CellOutput[], metadata?: WorkspaceEditEntryMetadata): void; + replaceCellMetadata( + uri: Uri, + index: number, + cellMetadata: NotebookCellMetadata, + metadata?: WorkspaceEditEntryMetadata + ): void; + } + + export interface NotebookEditorCellEdit { + replaceCells(start: number, end: number, cells: NotebookCellData[]): void; + replaceOutput(index: number, outputs: CellOutput[]): void; + replaceMetadata(index: number, metadata: NotebookCellMetadata): void; + + /** @deprecated */ + insert( + index: number, + content: string | string[], + language: string, + type: CellKind, + outputs: CellOutput[], + metadata: NotebookCellMetadata | undefined + ): void; + /** @deprecated */ + delete(index: number): void; + } + + export interface NotebookCellRange { + readonly start: number; + readonly end: number; + } + + export enum NotebookEditorRevealType { + /** + * The range will be revealed with as little scrolling as possible. + */ + Default = 0, + /** + * The range will always be revealed in the center of the viewport. + */ + InCenter = 1, + /** + * If the range is outside the viewport, it will be revealed in the center of the viewport. + * Otherwise, it will be revealed with as little scrolling as possible. + */ + InCenterIfOutsideViewport = 2 + } + + export interface NotebookEditor { + /** + * The document associated with this notebook editor. + */ + readonly document: NotebookDocument; + + /** + * The primary selected cell on this notebook editor. + */ + readonly selection?: NotebookCell; + + /** + * The current visible ranges in the editor (vertically). + */ + readonly visibleRanges: NotebookCellRange[]; + + /** + * The column in which this editor shows. + */ + readonly viewColumn?: ViewColumn; + + /** + * Whether the panel is active (focused by the user). + */ + readonly active: boolean; + + /** + * Whether the panel is visible. + */ + readonly visible: boolean; + + /** + * Fired when the panel is disposed. + */ + readonly onDidDispose: Event<void>; + + /** + * Active kernel used in the editor + */ + readonly kernel?: NotebookKernel; + + /** + * Fired when the output hosting webview posts a message. + */ + readonly onDidReceiveMessage: Event<any>; + /** + * Post a message to the output hosting webview. + * + * Messages are only delivered if the editor is live. + * + * @param message Body of the message. This must be a string or other json serilizable object. + */ + postMessage(message: any): Thenable<boolean>; + + /** + * Convert a uri for the local file system to one that can be used inside outputs webview. + */ + asWebviewUri(localResource: Uri): Uri; + + edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable<boolean>; + + revealRange(range: NotebookCellRange, revealType?: NotebookEditorRevealType): void; + } + + export interface NotebookOutputSelector { + mimeTypes?: string[]; + } + + export interface NotebookRenderRequest { + output: CellDisplayOutput; + mimeType: string; + outputId: string; + } + + export interface NotebookCellsChangeData { + readonly start: number; + readonly deletedCount: number; + readonly deletedItems: NotebookCell[]; + readonly items: NotebookCell[]; + } + + export interface NotebookCellsChangeEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly changes: ReadonlyArray<NotebookCellsChangeData>; + } + + export interface NotebookCellMoveEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly index: number; + readonly newIndex: number; + } + + export interface NotebookCellOutputsChangeEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly cells: NotebookCell[]; + } + + export interface NotebookCellLanguageChangeEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly cell: NotebookCell; + readonly language: string; + } + + export interface NotebookCellMetadataChangeEvent { + readonly document: NotebookDocument; + readonly cell: NotebookCell; + } + + export interface NotebookEditorSelectionChangeEvent { + readonly notebookEditor: NotebookEditor; + readonly selection?: NotebookCell; + } + + export interface NotebookEditorVisibleRangesChangeEvent { + readonly notebookEditor: NotebookEditor; + readonly visibleRanges: ReadonlyArray<NotebookCellRange>; + } + + export interface NotebookCellData { + readonly cellKind: CellKind; + readonly source: string; + readonly language: string; + readonly outputs: CellOutput[]; + readonly metadata: NotebookCellMetadata | undefined; + } + + export interface NotebookData { + readonly cells: NotebookCellData[]; + readonly languages: string[]; + readonly metadata: NotebookDocumentMetadata; + } + + interface NotebookDocumentContentChangeEvent { + /** + * The document that the edit is for. + */ + readonly document: NotebookDocument; + } + + interface NotebookDocumentEditEvent { + /** + * The document that the edit is for. + */ + readonly document: NotebookDocument; + + /** + * Undo the edit operation. + * + * This is invoked by VS Code when the user undoes this edit. To implement `undo`, your + * extension should restore the document and editor to the state they were in just before this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + undo(): Thenable<void> | void; + + /** + * Redo the edit operation. + * + * This is invoked by VS Code when the user redoes this edit. To implement `redo`, your + * extension should restore the document and editor to the state they were in just after this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + redo(): Thenable<void> | void; + + /** + * Display name describing the edit. + * + * This will be shown to users in the UI for undo/redo operations. + */ + readonly label?: string; + } + + interface NotebookDocumentBackup { + /** + * Unique identifier for the backup. + * + * This id is passed back to your extension in `openCustomDocument` when opening a notebook editor from a backup. + */ + readonly id: string; + + /** + * Delete the current backup. + * + * This is called by VS Code when it is clear the current backup is no longer needed, such as when a new backup + * is made or when the file is saved. + */ + delete(): void; + } + + interface NotebookDocumentBackupContext { + readonly destination: Uri; + } + + interface NotebookDocumentOpenContext { + readonly backupId?: string; + } + + /** + * Communication object passed to the {@link NotebookContentProvider} and + * {@link NotebookOutputRenderer} to communicate with the webview. + */ + export interface NotebookCommunication { + /** + * ID of the editor this object communicates with. A single notebook + * document can have multiple attached webviews and editors, when the + * notebook is split for instance. The editor ID lets you differentiate + * between them. + */ + readonly editorId: string; + + /** + * Fired when the output hosting webview posts a message. + */ + readonly onDidReceiveMessage: Event<any>; + /** + * Post a message to the output hosting webview. + * + * Messages are only delivered if the editor is live. + * + * @param message Body of the message. This must be a string or other json serilizable object. + */ + postMessage(message: any): Thenable<boolean>; + + /** + * Convert a uri for the local file system to one that can be used inside outputs webview. + */ + asWebviewUri(localResource: Uri): Uri; + } + + export interface NotebookContentProvider { + /** + * Content providers should always use [file system providers](#FileSystemProvider) to + * resolve the raw content for `uri` as the resouce is not necessarily a file on disk. + */ + openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Promise<NotebookData>; + resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Promise<void>; + saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise<void>; + saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise<void>; + readonly onDidChangeNotebook: Event<NotebookDocumentContentChangeEvent | NotebookDocumentEditEvent>; + backupNotebook( + document: NotebookDocument, + context: NotebookDocumentBackupContext, + cancellation: CancellationToken + ): Promise<NotebookDocumentBackup>; + } + + export interface NotebookKernel { + readonly id?: string; + label: string; + description?: string; + detail?: string; + isPreferred?: boolean; + preloads?: Uri[]; + executeCell(document: NotebookDocument, cell: NotebookCell): void; + cancelCellExecution(document: NotebookDocument, cell: NotebookCell): void; + executeAllCells(document: NotebookDocument): void; + cancelAllCellsExecution(document: NotebookDocument): void; + } + + export interface NotebookDocumentFilter { + viewType?: string | string[]; + filenamePattern?: GlobPattern | { include: GlobPattern; exclude: GlobPattern }; + } + + export interface NotebookKernelProvider<T extends NotebookKernel = NotebookKernel> { + onDidChangeKernels?: Event<NotebookDocument | undefined>; + provideKernels(document: NotebookDocument, token: CancellationToken): ProviderResult<T[]>; + resolveKernel?( + kernel: T, + document: NotebookDocument, + webview: NotebookCommunication, + token: CancellationToken + ): ProviderResult<void>; + } + + /** + * Represents the alignment of status bar items. + */ + export enum NotebookCellStatusBarAlignment { + /** + * Aligned to the left side. + */ + Left = 1, + + /** + * Aligned to the right side. + */ + Right = 2 + } + + export interface NotebookCellStatusBarItem { + readonly cell: NotebookCell; + readonly alignment: NotebookCellStatusBarAlignment; + readonly priority?: number; + text: string; + tooltip: string | undefined; + command: string | Command | undefined; + accessibilityInformation?: AccessibilityInformation; + show(): void; + hide(): void; + dispose(): void; + } + + export namespace notebook { + export function registerNotebookContentProvider( + notebookType: string, + provider: NotebookContentProvider, + options?: { + /** + * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. + */ + transientOutputs: boolean; + /** + * Controls if a meetadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientMetadata: { [K in keyof NotebookCellMetadata]?: boolean }; + } + ): Disposable; + + export function registerNotebookKernelProvider( + selector: NotebookDocumentFilter, + provider: NotebookKernelProvider + ): Disposable; + + export const onDidOpenNotebookDocument: Event<NotebookDocument>; + export const onDidCloseNotebookDocument: Event<NotebookDocument>; + export const onDidSaveNotebookDocument: Event<NotebookDocument>; + + /** + * All currently known notebook documents. + */ + export const notebookDocuments: ReadonlyArray<NotebookDocument>; + + export const visibleNotebookEditors: NotebookEditor[]; + export const onDidChangeVisibleNotebookEditors: Event<NotebookEditor[]>; + + export const activeNotebookEditor: NotebookEditor | undefined; + export const onDidChangeActiveNotebookEditor: Event<NotebookEditor | undefined>; + export const onDidChangeNotebookEditorSelection: Event<NotebookEditorSelectionChangeEvent>; + export const onDidChangeNotebookEditorVisibleRanges: Event<NotebookEditorVisibleRangesChangeEvent>; + export const onDidChangeNotebookCells: Event<NotebookCellsChangeEvent>; + export const onDidChangeCellOutputs: Event<NotebookCellOutputsChangeEvent>; + export const onDidChangeCellLanguage: Event<NotebookCellLanguageChangeEvent>; + export const onDidChangeCellMetadata: Event<NotebookCellMetadataChangeEvent>; + /** + * Create a document that is the concatenation of all notebook cells. By default all code-cells are included + * but a selector can be provided to narrow to down the set of cells. + * + * @param notebook + * @param selector + */ + export function createConcatTextDocument( + notebook: NotebookDocument, + selector?: DocumentSelector + ): NotebookConcatTextDocument; + + export const onDidChangeActiveNotebookKernel: Event<{ + document: NotebookDocument; + kernel: NotebookKernel | undefined; + }>; + + /** + * Creates a notebook cell status bar [item](#NotebookCellStatusBarItem). + * It will be disposed automatically when the notebook document is closed or the cell is deleted. + * + * @param cell The cell on which this item should be shown. + * @param alignment The alignment of the item. + * @param priority The priority of the item. Higher values mean the item should be shown more to the left. + * @return A new status bar item. + */ + export function createCellStatusBarItem( + cell: NotebookCell, + alignment?: NotebookCellStatusBarAlignment, + priority?: number + ): NotebookCellStatusBarItem; + } +} diff --git a/typings/dom.fix.rx.compiler.d.ts b/typings/dom.fix.rx.compiler.d.ts new file mode 100644 index 000000000000..64ced3161585 --- /dev/null +++ b/typings/dom.fix.rx.compiler.d.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * These are fake dom type definitions that rxjs depends on. + * Another solution is to add the 'dom' lib to tsconfig, but that's even worse. + * We don't need dom, as the extension does nothing with the dom (dom = HTML entities and the like). + */ +// tslint:disable: interface-name +interface EventTarget { } +interface NodeList { } +interface HTMLCollection { } +interface XMLHttpRequest { } +interface Event { } +interface MessageEvent { } +interface CloseEvent { } +interface WebSocket { } diff --git a/typings/extensions.d.ts b/typings/extensions.d.ts new file mode 100644 index 000000000000..4a423f329d57 --- /dev/null +++ b/typings/extensions.d.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** +* @typedef {Object} SplitLinesOptions +* @property {boolean} [trim=true] - Whether to trim the lines. +* @property {boolean} [removeEmptyEntries=true] - Whether to remove empty entries. +*/ + +// https://stackoverflow.com/questions/39877156/how-to-extend-string-prototype-and-use-it-next-in-typescript +// tslint:disable-next-line:interface-name +declare interface String { + /** + * Split a string using the cr and lf characters and return them as an array. + * By default lines are trimmed and empty lines are removed. + * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. + */ + splitLines(splitOptions?: { trim: boolean, removeEmptyEntries?: boolean }): string[]; + /** + * Appropriately formats a string so it can be used as an argument for a command in a shell. + * E.g. if an argument contains a space, then it will be enclosed within double quotes. + */ + toCommandArgument(): string; + /** + * Appropriately formats a a file path so it can be used as an argument for a command in a shell. + * E.g. if an argument contains a space, then it will be enclosed within double quotes. + */ + fileToCommandArgument(): string; + /** + * String.format() implementation. + * Tokens such as {0}, {1} will be replaced with corresponding positional arguments. + */ + format(...args: string[]): string; + /** + * String.trimQuotes implementation + * Removes leading and trailing quotes from a string + */ + trimQuotes(): string; +} + +// tslint:disable-next-line:interface-name +declare interface Promise<T> { + /** + * Catches task errors and ignores them. + */ + ignoreErrors(): void; +} diff --git a/typings/index.d.ts b/typings/index.d.ts new file mode 100644 index 000000000000..26538bbff554 --- /dev/null +++ b/typings/index.d.ts @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + + +// Added to allow compilation of backbone types pulled in from ipywidgets (@jupyterlab/widgets). +declare module JQuery { + type TriggeredEvent = any; +} + +declare module '@phosphor/coreutils' { + export class PromiseDelegate<T> { + /** + * The promise wrapped by the delegate. + */ + public readonly promise: Promise<T>; + /** + * Construct a new promise delegate. + */ + constructor(); + /** + * Reject the wrapped promise with the given value. + * + * @reason - The reason for rejecting the promise. + */ + reject(reason: any): void; + /** + * Resolve the wrapped promise with the given value. + * + * @param value - The value to use for resolving the promise. + */ + resolve(value: T | PromiseLike<T>): void; + } + /** + * A type definition for the MimeData class. + * Based on http://phosphorjs.github.io/phosphor/api/coreutils/classes/mimedata.html + */ + export class MimeData { + private _types: string[]; + private _values: any[]; + public clear(): void; + public clearData(mime: string): void; + public getData(mime: string): any | undefined; + public hasData(mime: string): boolean; + public setData(mime: string, data: any): void; + public types(): string[]; + } + /** + * The namespace for UUID related functionality. + */ + export namespace UUID { + /** + * A function which generates UUID v4 identifiers. + * @returns A new UUID v4 string. + */ + const uuid4: () => string; + } + /** + * A type alias for a JSON primitive. + */ + export type JSONPrimitive = boolean | number | string | null | undefined; + /** + * A type alias for a JSON value. + */ + export type JSONValue = JSONPrimitive | JSONObject | JSONArray; + /** + * A type definition for a JSON object. + */ + export interface JSONObject { + [key: string]: JSONValue; + } + /** + * A type definition for a JSON array. + */ + export interface JSONArray extends Array<JSONValue> { } + /** + * A type definition for a readonly JSON object. + */ + export interface ReadonlyJSONObject { + readonly [key: string]: ReadonlyJSONValue; + } + /** + * A type definition for a readonly JSON array. + */ + export interface ReadonlyJSONArray extends ReadonlyArray<ReadonlyJSONValue> { } + /** + * A type alias for a readonly JSON value. + */ + export type ReadonlyJSONValue = JSONPrimitive | ReadonlyJSONObject | ReadonlyJSONArray; + /** + * The namespace for JSON-specific functions. + */ + export namespace JSONExt { + /** + * A shared frozen empty JSONObject + */ + const emptyObject: ReadonlyJSONObject; + /** + * A shared frozen empty JSONArray + */ + const emptyArray: ReadonlyJSONArray; + /** + * Test whether a JSON value is a primitive. + * + * @param value - The JSON value of interest. + * + * @returns `true` if the value is a primitive,`false` otherwise. + */ + function isPrimitive(value: ReadonlyJSONValue): value is JSONPrimitive; + /** + * Test whether a JSON value is an array. + * + * @param value - The JSON value of interest. + * + * @returns `true` if the value is a an array, `false` otherwise. + */ + function isArray(value: JSONValue): value is JSONArray; + function isArray(value: ReadonlyJSONValue): value is ReadonlyJSONArray; + /** + * Test whether a JSON value is an object. + * + * @param value - The JSON value of interest. + * + * @returns `true` if the value is a an object, `false` otherwise. + */ + function isObject(value: JSONValue): value is JSONObject; + function isObject(value: ReadonlyJSONValue): value is ReadonlyJSONObject; + /** + * Compare two JSON values for deep equality. + * + * @param first - The first JSON value of interest. + * + * @param second - The second JSON value of interest. + * + * @returns `true` if the values are equivalent, `false` otherwise. + */ + function deepEqual(first: ReadonlyJSONValue, second: ReadonlyJSONValue): boolean; + /** + * Create a deep copy of a JSON value. + * + * @param value - The JSON value to copy. + * + * @returns A deep copy of the given JSON value. + */ + function deepCopy<T extends ReadonlyJSONValue>(value: T): T; + } + + export class Token<T> { + /** + * The human readable name for the token. + * + * #### Notes + * This can be useful for debugging and logging. + */ + public readonly name: string; + private _tokenStructuralPropertyT; + /** + * Construct a new token. + * + * @param name - A human readable name for the token. + */ + constructor(name: string); + } +} diff --git a/typings/node.d.ts b/typings/node.d.ts deleted file mode 100644 index 5ed7730b6c5f..000000000000 --- a/typings/node.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference path="../node_modules/vscode/typings/node.d.ts" /> \ No newline at end of file diff --git a/typings/vscode-proposed/index.d.ts b/typings/vscode-proposed/index.d.ts new file mode 100644 index 000000000000..2abb5824d26f --- /dev/null +++ b/typings/vscode-proposed/index.d.ts @@ -0,0 +1,691 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Event, + GlobPattern, + Uri, + TextDocument, + ViewColumn, + CancellationToken, + Disposable, + DocumentSelector, + ProviderResult, + WorkspaceEditEntryMetadata, + Command, + AccessibilityInformation +} from 'vscode'; + +// Copy nb section from https://github.com/microsoft/vscode/blob/master/src/vs/vscode.proposed.d.ts. +export enum CellKind { + Markdown = 1, + Code = 2 +} + +export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3 +} + +export interface CellStreamOutput { + outputKind: CellOutputKind.Text; + text: string; +} + +export interface CellErrorOutput { + outputKind: CellOutputKind.Error; + /** + * Exception Name + */ + ename: string; + /** + * Exception Value + */ + evalue: string; + /** + * Exception call stack + */ + traceback: string[]; +} + +export interface NotebookCellOutputMetadata { + /** + * Additional attributes of a cell metadata. + */ + custom?: { [key: string]: any }; +} + +export interface CellDisplayOutput { + outputKind: CellOutputKind.Rich; + /** + * { mime_type: value } + * + * Example: + * ```json + * { + * "outputKind": vscode.CellOutputKind.Rich, + * "data": { + * "text/html": [ + * "<h1>Hello</h1>" + * ], + * "text/plain": [ + * "<IPython.lib.display.IFrame at 0x11dee3e80>" + * ] + * } + * } + */ + data: { [key: string]: any }; + + readonly metadata?: NotebookCellOutputMetadata; +} + +export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; + +export enum NotebookCellRunState { + Running = 1, + Idle = 2, + Success = 3, + Error = 4 +} + +export enum NotebookRunState { + Running = 1, + Idle = 2 +} + +export interface NotebookCellMetadata { + /** + * Controls whether a cell's editor is editable/readonly. + */ + editable?: boolean; + + /** + * Controls if the cell is executable. + * This metadata is ignored for markdown cell. + */ + runnable?: boolean; + + /** + * Controls if the cell has a margin to support the breakpoint UI. + * This metadata is ignored for markdown cell. + */ + breakpointMargin?: boolean; + + /** + * Whether the [execution order](#NotebookCellMetadata.executionOrder) indicator will be displayed. + * Defaults to true. + */ + hasExecutionOrder?: boolean; + + /** + * The order in which this cell was executed. + */ + executionOrder?: number; + + /** + * A status message to be shown in the cell's status bar + */ + statusMessage?: string; + + /** + * The cell's current run state + */ + runState?: NotebookCellRunState; + + /** + * If the cell is running, the time at which the cell started running + */ + runStartTime?: number; + + /** + * The total duration of the cell's last run + */ + lastRunDuration?: number; + + /** + * Whether a code cell's editor is collapsed + */ + inputCollapsed?: boolean; + + /** + * Whether a code cell's outputs are collapsed + */ + outputCollapsed?: boolean; + + /** + * Additional attributes of a cell metadata. + */ + custom?: { [key: string]: any }; +} + +export interface NotebookCell { + readonly notebook: NotebookDocument; + readonly uri: Uri; + readonly cellKind: CellKind; + readonly document: TextDocument; + readonly language: string; + outputs: CellOutput[]; + metadata: NotebookCellMetadata; +} + +export interface NotebookDocumentMetadata { + /** + * Controls if users can add or delete cells + * Defaults to true + */ + editable?: boolean; + + /** + * Controls whether the full notebook can be run at once. + * Defaults to true + */ + runnable?: boolean; + + /** + * Default value for [cell editable metadata](#NotebookCellMetadata.editable). + * Defaults to true. + */ + cellEditable?: boolean; + + /** + * Default value for [cell runnable metadata](#NotebookCellMetadata.runnable). + * Defaults to true. + */ + cellRunnable?: boolean; + + /** + * Default value for [cell hasExecutionOrder metadata](#NotebookCellMetadata.hasExecutionOrder). + * Defaults to true. + */ + cellHasExecutionOrder?: boolean; + + displayOrder?: GlobPattern[]; + + /** + * Additional attributes of the document metadata. + */ + custom?: { [key: string]: any }; + + /** + * The document's current run state + */ + runState?: NotebookRunState; +} + +export interface NotebookDocument { + readonly uri: Uri; + readonly version: number; + readonly fileName: string; + readonly viewType: string; + readonly isDirty: boolean; + readonly isUntitled: boolean; + readonly cells: ReadonlyArray<NotebookCell>; + languages: string[]; + metadata: NotebookDocumentMetadata; +} + +export interface NotebookConcatTextDocument { + uri: Uri; + isClosed: boolean; + dispose(): void; + onDidChange: Event<void>; + version: number; + getText(): string; + getText(range: Range): string; + + offsetAt(position: Position): number; + positionAt(offset: number): Position; + validateRange(range: Range): Range; + validatePosition(position: Position): Position; + + locationAt(positionOrRange: Position | Range): Location; + positionAt(location: Location): Position; + contains(uri: Uri): boolean; +} + +export interface WorkspaceEdit { + replaceCells( + uri: Uri, + start: number, + end: number, + cells: NotebookCellData[], + metadata?: WorkspaceEditEntryMetadata + ): void; + replaceCellOutput(uri: Uri, index: number, outputs: CellOutput[], metadata?: WorkspaceEditEntryMetadata): void; + replaceCellMetadata( + uri: Uri, + index: number, + cellMetadata: NotebookCellMetadata, + metadata?: WorkspaceEditEntryMetadata + ): void; +} + +export interface NotebookEditorCellEdit { + replaceCells(start: number, end: number, cells: NotebookCellData[]): void; + replaceOutput(index: number, outputs: CellOutput[]): void; + replaceMetadata(index: number, metadata: NotebookCellMetadata): void; + + /** @deprecated */ + insert( + index: number, + content: string | string[], + language: string, + type: CellKind, + outputs: CellOutput[], + metadata: NotebookCellMetadata | undefined + ): void; + /** @deprecated */ + delete(index: number): void; +} + +export interface NotebookCellRange { + readonly start: number; + readonly end: number; +} + +export enum NotebookEditorRevealType { + /** + * The range will be revealed with as little scrolling as possible. + */ + Default = 0, + /** + * The range will always be revealed in the center of the viewport. + */ + InCenter = 1, + /** + * If the range is outside the viewport, it will be revealed in the center of the viewport. + * Otherwise, it will be revealed with as little scrolling as possible. + */ + InCenterIfOutsideViewport = 2 +} + +export interface NotebookEditor { + /** + * The document associated with this notebook editor. + */ + readonly document: NotebookDocument; + + /** + * The primary selected cell on this notebook editor. + */ + readonly selection?: NotebookCell; + + /** + * The current visible ranges in the editor (vertically). + */ + readonly visibleRanges: NotebookCellRange[]; + + /** + * The column in which this editor shows. + */ + readonly viewColumn?: ViewColumn; + + /** + * Whether the panel is active (focused by the user). + */ + readonly active: boolean; + + /** + * Whether the panel is visible. + */ + readonly visible: boolean; + + /** + * Fired when the panel is disposed. + */ + readonly onDidDispose: Event<void>; + + /** + * Active kernel used in the editor + */ + readonly kernel?: NotebookKernel; + + /** + * Fired when the output hosting webview posts a message. + */ + readonly onDidReceiveMessage: Event<any>; + /** + * Post a message to the output hosting webview. + * + * Messages are only delivered if the editor is live. + * + * @param message Body of the message. This must be a string or other json serilizable object. + */ + postMessage(message: any): Thenable<boolean>; + + /** + * Convert a uri for the local file system to one that can be used inside outputs webview. + */ + asWebviewUri(localResource: Uri): Uri; + + edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable<boolean>; + + revealRange(range: NotebookCellRange, revealType?: NotebookEditorRevealType): void; +} + +export interface NotebookOutputSelector { + mimeTypes?: string[]; +} + +export interface NotebookRenderRequest { + output: CellDisplayOutput; + mimeType: string; + outputId: string; +} + +export interface NotebookCellsChangeData { + readonly start: number; + readonly deletedCount: number; + readonly deletedItems: NotebookCell[]; + readonly items: NotebookCell[]; +} + +export interface NotebookCellsChangeEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly changes: ReadonlyArray<NotebookCellsChangeData>; +} + +export interface NotebookCellMoveEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly index: number; + readonly newIndex: number; +} + +export interface NotebookCellOutputsChangeEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly cells: NotebookCell[]; +} + +export interface NotebookCellLanguageChangeEvent { + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly cell: NotebookCell; + readonly language: string; +} + +export interface NotebookCellMetadataChangeEvent { + readonly document: NotebookDocument; + readonly cell: NotebookCell; +} + +export interface NotebookEditorSelectionChangeEvent { + readonly notebookEditor: NotebookEditor; + readonly selection?: NotebookCell; +} + +export interface NotebookEditorVisibleRangesChangeEvent { + readonly notebookEditor: NotebookEditor; + readonly visibleRanges: ReadonlyArray<NotebookCellRange>; +} + +export interface NotebookCellData { + readonly cellKind: CellKind; + readonly source: string; + readonly language: string; + readonly outputs: CellOutput[]; + readonly metadata: NotebookCellMetadata | undefined; +} + +export interface NotebookData { + readonly cells: NotebookCellData[]; + readonly languages: string[]; + readonly metadata: NotebookDocumentMetadata; +} + +interface NotebookDocumentContentChangeEvent { + /** + * The document that the edit is for. + */ + readonly document: NotebookDocument; +} + +interface NotebookDocumentEditEvent { + /** + * The document that the edit is for. + */ + readonly document: NotebookDocument; + + /** + * Undo the edit operation. + * + * This is invoked by VS Code when the user undoes this edit. To implement `undo`, your + * extension should restore the document and editor to the state they were in just before this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + undo(): Thenable<void> | void; + + /** + * Redo the edit operation. + * + * This is invoked by VS Code when the user redoes this edit. To implement `redo`, your + * extension should restore the document and editor to the state they were in just after this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + redo(): Thenable<void> | void; + + /** + * Display name describing the edit. + * + * This will be shown to users in the UI for undo/redo operations. + */ + readonly label?: string; +} + +interface NotebookDocumentBackup { + /** + * Unique identifier for the backup. + * + * This id is passed back to your extension in `openCustomDocument` when opening a notebook editor from a backup. + */ + readonly id: string; + + /** + * Delete the current backup. + * + * This is called by VS Code when it is clear the current backup is no longer needed, such as when a new backup + * is made or when the file is saved. + */ + delete(): void; +} + +interface NotebookDocumentBackupContext { + readonly destination: Uri; +} + +interface NotebookDocumentOpenContext { + readonly backupId?: string; +} + +/** + * Communication object passed to the {@link NotebookContentProvider} and + * {@link NotebookOutputRenderer} to communicate with the webview. + */ +export interface NotebookCommunication { + /** + * ID of the editor this object communicates with. A single notebook + * document can have multiple attached webviews and editors, when the + * notebook is split for instance. The editor ID lets you differentiate + * between them. + */ + readonly editorId: string; + + /** + * Fired when the output hosting webview posts a message. + */ + readonly onDidReceiveMessage: Event<any>; + /** + * Post a message to the output hosting webview. + * + * Messages are only delivered if the editor is live. + * + * @param message Body of the message. This must be a string or other json serilizable object. + */ + postMessage(message: any): Thenable<boolean>; + + /** + * Convert a uri for the local file system to one that can be used inside outputs webview. + */ + asWebviewUri(localResource: Uri): Uri; +} + +export interface NotebookContentProvider { + /** + * Content providers should always use [file system providers](#FileSystemProvider) to + * resolve the raw content for `uri` as the resouce is not necessarily a file on disk. + */ + openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Promise<NotebookData>; + resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Promise<void>; + saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise<void>; + saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise<void>; + readonly onDidChangeNotebook: Event<NotebookDocumentContentChangeEvent | NotebookDocumentEditEvent>; + backupNotebook( + document: NotebookDocument, + context: NotebookDocumentBackupContext, + cancellation: CancellationToken + ): Promise<NotebookDocumentBackup>; +} + +export interface NotebookKernel { + readonly id?: string; + label: string; + description?: string; + detail?: string; + isPreferred?: boolean; + preloads?: Uri[]; + executeCell(document: NotebookDocument, cell: NotebookCell): void; + cancelCellExecution(document: NotebookDocument, cell: NotebookCell): void; + executeAllCells(document: NotebookDocument): void; + cancelAllCellsExecution(document: NotebookDocument): void; +} + +export interface NotebookDocumentFilter { + viewType?: string | string[]; + filenamePattern?: GlobPattern | { include: GlobPattern; exclude: GlobPattern }; +} + +export interface NotebookKernelProvider<T extends NotebookKernel = NotebookKernel> { + onDidChangeKernels?: Event<NotebookDocument | undefined>; + provideKernels(document: NotebookDocument, token: CancellationToken): ProviderResult<T[]>; + resolveKernel?( + kernel: T, + document: NotebookDocument, + webview: NotebookCommunication, + token: CancellationToken + ): ProviderResult<void>; +} + +/** + * Represents the alignment of status bar items. + */ +export enum NotebookCellStatusBarAlignment { + /** + * Aligned to the left side. + */ + Left = 1, + + /** + * Aligned to the right side. + */ + Right = 2 +} + +export interface NotebookCellStatusBarItem { + readonly cell: NotebookCell; + readonly alignment: NotebookCellStatusBarAlignment; + readonly priority?: number; + text: string; + tooltip: string | undefined; + command: string | Command | undefined; + accessibilityInformation?: AccessibilityInformation; + show(): void; + hide(): void; + dispose(): void; +} + +export namespace notebook { + export function registerNotebookContentProvider( + notebookType: string, + provider: NotebookContentProvider, + options?: { + /** + * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. + */ + transientOutputs: boolean; + /** + * Controls if a meetadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientMetadata: { [K in keyof NotebookCellMetadata]?: boolean }; + } + ): Disposable; + + export function registerNotebookKernelProvider( + selector: NotebookDocumentFilter, + provider: NotebookKernelProvider + ): Disposable; + + export const onDidOpenNotebookDocument: Event<NotebookDocument>; + export const onDidCloseNotebookDocument: Event<NotebookDocument>; + export const onDidSaveNotebookDocument: Event<NotebookDocument>; + + /** + * All currently known notebook documents. + */ + export const notebookDocuments: ReadonlyArray<NotebookDocument>; + + export const visibleNotebookEditors: NotebookEditor[]; + export const onDidChangeVisibleNotebookEditors: Event<NotebookEditor[]>; + + export const activeNotebookEditor: NotebookEditor | undefined; + export const onDidChangeActiveNotebookEditor: Event<NotebookEditor | undefined>; + export const onDidChangeNotebookEditorSelection: Event<NotebookEditorSelectionChangeEvent>; + export const onDidChangeNotebookEditorVisibleRanges: Event<NotebookEditorVisibleRangesChangeEvent>; + export const onDidChangeNotebookCells: Event<NotebookCellsChangeEvent>; + export const onDidChangeCellOutputs: Event<NotebookCellOutputsChangeEvent>; + export const onDidChangeCellLanguage: Event<NotebookCellLanguageChangeEvent>; + export const onDidChangeCellMetadata: Event<NotebookCellMetadataChangeEvent>; + /** + * Create a document that is the concatenation of all notebook cells. By default all code-cells are included + * but a selector can be provided to narrow to down the set of cells. + * + * @param notebook + * @param selector + */ + export function createConcatTextDocument( + notebook: NotebookDocument, + selector?: DocumentSelector + ): NotebookConcatTextDocument; + + export const onDidChangeActiveNotebookKernel: Event<{ + document: NotebookDocument; + kernel: NotebookKernel | undefined; + }>; + + /** + * Creates a notebook cell status bar [item](#NotebookCellStatusBarItem). + * It will be disposed automatically when the notebook document is closed or the cell is deleted. + * + * @param cell The cell on which this item should be shown. + * @param alignment The alignment of the item. + * @param priority The priority of the item. Higher values mean the item should be shown more to the left. + * @return A new status bar item. + */ + export function createCellStatusBarItem( + cell: NotebookCell, + alignment?: NotebookCellStatusBarAlignment, + priority?: number + ): NotebookCellStatusBarItem; +} diff --git a/typings/vscode-typings.d.ts b/typings/vscode-typings.d.ts deleted file mode 100644 index 5590dc8ce888..000000000000 --- a/typings/vscode-typings.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference path="../node_modules/vscode/typings/index.d.ts" /> diff --git a/vscode-python-signing.csproj b/vscode-python-signing.csproj new file mode 100644 index 000000000000..7fb333c4b277 --- /dev/null +++ b/vscode-python-signing.csproj @@ -0,0 +1,20 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFramework>netcoreapp2.0</TargetFramework> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="MicroBuild.Core" Version="0.2.0" /> + </ItemGroup> + <PropertyGroup> + <VsixOutPath>$(OutputPath)\python-$(Branch).vsix</VsixOutPath> + <VscePath>$(UserProfile)\AppData\Roaming\npm\vsce</VscePath> + </PropertyGroup> + <Target Name="BeforeBuild"> + <Exec Command="$(VscePath) package --out $(VsixOutPath)" /> + </Target> + <ItemGroup> + <FilesToSign Include="$(VsixOutPath)"> + <Authenticode>VsixSHA2</Authenticode> + </FilesToSign> + </ItemGroup> +</Project>